From 6b399e5c6b648ac30a3bdaf3262469e5f253c2f2 Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Thu, 12 Mar 2020 19:17:18 +0000 Subject: [PATCH 001/922] (ci) Updated version for next development release [skip deploy] --- hikari/_about.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hikari/_about.py b/hikari/_about.py index 5ca888dd2c..2c24768be2 100644 --- a/hikari/_about.py +++ b/hikari/_about.py @@ -22,5 +22,5 @@ __copyright__ = "© 2019-2020 Nekokatt" __email__ = "3903853-nekokatt@users.noreply.gitlab.com" __license__ = "LGPL-3.0-ONLY" -__version__ = "0.0.81" +__version__ = "0.0.82.dev" __url__ = "https://gitlab.com/nekokatt/hikari" From 9d26e847a60a8cd4c5bff98bb1f4975f92d6956d Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Thu, 12 Mar 2020 19:24:20 +0000 Subject: [PATCH 002/922] (ci) Updated version for next development release [skip deploy] --- hikari/_about.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hikari/_about.py b/hikari/_about.py index e3dcf8c9b1..e127e9db43 100644 --- a/hikari/_about.py +++ b/hikari/_about.py @@ -22,5 +22,5 @@ __copyright__ = "© 2019-2020 Nekokatt" __email__ = "3903853-nekokatt@users.noreply.gitlab.com" __license__ = "LGPL-3.0-ONLY" -__version__ = "1.0.0" +__version__ = "1.0.1.dev" __url__ = "https://gitlab.com/nekokatt/hikari" From 5abdc837a2473f50583fcad0b53fd28a4f4e2972 Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Thu, 12 Mar 2020 22:59:23 +0000 Subject: [PATCH 003/922] Added base model definitions [skip deploy] --- hikari/core/__init__.py | 19 +++++ hikari/core/entities.py | 131 +++++++++++++++++++++++++++++ requirements.txt | 3 +- tests/hikari/core/__init__.py | 18 ++++ tests/hikari/core/test_entities.py | 99 ++++++++++++++++++++++ 5 files changed, 269 insertions(+), 1 deletion(-) create mode 100644 hikari/core/__init__.py create mode 100644 hikari/core/entities.py create mode 100644 tests/hikari/core/__init__.py create mode 100644 tests/hikari/core/test_entities.py diff --git a/hikari/core/__init__.py b/hikari/core/__init__.py new file mode 100644 index 0000000000..91d1cfa1df --- /dev/null +++ b/hikari/core/__init__.py @@ -0,0 +1,19 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# Copyright © Nekokatt 2019-2020 +# +# This file is part of Hikari. +# +# Hikari is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Hikari is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Hikari. If not, see . +"""The core API for interacting with Discord directly.""" diff --git a/hikari/core/entities.py b/hikari/core/entities.py new file mode 100644 index 0000000000..7cd9178462 --- /dev/null +++ b/hikari/core/entities.py @@ -0,0 +1,131 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# Copyright © Nekokatt 2019-2020 +# +# This file is part of Hikari. +# +# Hikari is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Hikari is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Hikari. If not, see . +"""Datastructure bases.""" +import datetime +import functools + +import cattr + +import attr +import typing + +from hikari.internal_utilities import dates + + +class HikariConverter(cattr.Converter): + pass + + +class Entity: + """A model entity.""" + + __slots__ = () + _converter = HikariConverter() + + @classmethod + def __init_class__(cls, converter: cattr.Converter): + ... + + def __init_subclass__(cls, **kwargs): + super().__init_subclass__() + cls.__init_class__(cls._converter) + + def serialize(self): + """ + Returns + ------- + + A JSON-compatible representation of this object. + """ + return self._converter.unstructure(self) + + @classmethod + def deserialize(cls, payload): + """ + Parameters + ---------- + payload: + The payload to deserialize. + + Returns + ------- + The structured object. + """ + return cls._converter.structure(payload, cls) + + +@functools.total_ordering +class Snowflake(Entity, typing.SupportsInt): + """A concrete representation of a unique identifier for an object on + Discord. + """ + + __slots__ = ("value",) + + def __init__(self, value: typing.Union[int, str]) -> None: + self.value = int(value) + + @classmethod + def __init_class__(cls, converter): + converter.register_structure_hook(cls, lambda data, t: t(data)) + converter.register_unstructure_hook(cls, str) + ... + + @property + def created_at(self) -> datetime.datetime: + """When the object was created.""" + epoch = self.value >> 22 + return dates.discord_epoch_to_datetime(epoch) + + @property + def internal_worker_id(self) -> int: + """The internal worker ID that created this object on Discord.""" + return (self.value & 0x3E0_000) >> 17 + + @property + def internal_process_id(self) -> int: + """The internal process ID that created this object on Discord.""" + return (self.value & 0x1F_000) >> 12 + + @property + def increment(self) -> int: + """The increment of Discord's system when this object was made.""" + return self.value & 0xFFF + + def __int__(self): + return self.value + + def __repr__(self): + return repr(self.value) + + def __str__(self): + return str(self.value) + + def __eq__(self, other): + return isinstance(other, (int, typing.SupportsInt)) and int(other) == self.value + + def __lt__(self, other): + return self.value < int(other) + + +@attr.s(slots=True) +class Hashable(Entity): + """An entity that has an integer ID of some sort.""" + + id: Snowflake = attr.ib(hash=True, eq=True, repr=True) diff --git a/requirements.txt b/requirements.txt index 0610a943c5..7278d34f39 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,3 @@ aiohttp==3.6.2 -async-timeout==3.0.1 +attrs==19.3.0 +cattrs==1.0.0 diff --git a/tests/hikari/core/__init__.py b/tests/hikari/core/__init__.py new file mode 100644 index 0000000000..1c1502a5ca --- /dev/null +++ b/tests/hikari/core/__init__.py @@ -0,0 +1,18 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# Copyright © Nekoka.tt 2019-2020 +# +# This file is part of Hikari. +# +# Hikari is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Hikari is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Hikari. If not, see . diff --git a/tests/hikari/core/test_entities.py b/tests/hikari/core/test_entities.py new file mode 100644 index 0000000000..b3d732534c --- /dev/null +++ b/tests/hikari/core/test_entities.py @@ -0,0 +1,99 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# Copyright © Nekoka.tt 2019-2020 +# +# This file is part of Hikari. +# +# Hikari is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Hikari is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Hikari. If not, see . +import datetime + +import attr +import cymock +import pytest + +from hikari.core import entities + + +class TestEntity: + def test_init_subclass_invokes_init_class(self): + class SubEntity(entities.Entity): + __init_class__ = cymock.MagicMock() + + SubEntity.__init_class__.assert_called_once_with(entities.Entity._converter) + + def test_serialize_unstructures_instance(self): + @attr.s(slots=True) + class SubEntity(entities.Entity): + a_number: int = attr.ib() + a_string: str = attr.ib() + + instance = SubEntity(9, "18") + assert instance.serialize() == {"a_number": 9, "a_string": "18"} + + def test_deserialize_structures_new_instance(self): + @attr.s(slots=True) + class SubEntity(entities.Entity): + a_number: int = attr.ib() + a_string: str = attr.ib() + + assert SubEntity.deserialize({"a_number": 9, "a_string": "18"}) == SubEntity(9, "18") + + +class TestSnowflake: + @pytest.fixture() + def raw_id(self): + return 537_340_989_808_050_216 + + @pytest.fixture() + def neko_snowflake(self, raw_id): + return entities.Snowflake(raw_id) + + def test_created_at(self, neko_snowflake): + assert neko_snowflake.created_at == datetime.datetime(2019, 1, 22, 18, 41, 15, 283_000).replace( + tzinfo=datetime.timezone.utc + ) + + def test_increment(self, neko_snowflake): + assert neko_snowflake.increment == 40 + + def test_internal_process_id(self, neko_snowflake): + assert neko_snowflake.internal_process_id == 0 + + def test_internal_worker_id(self, neko_snowflake): + assert neko_snowflake.internal_worker_id == 2 + + def test_int_cast(self, neko_snowflake, raw_id): + assert int(neko_snowflake) == raw_id + + def test_str_cast(self, neko_snowflake, raw_id): + assert str(neko_snowflake) == str(raw_id) + + def test_repr_cast(self, neko_snowflake, raw_id): + assert repr(neko_snowflake) == repr(raw_id) + + def test_eq(self, neko_snowflake, raw_id): + assert neko_snowflake == raw_id + assert neko_snowflake == entities.Snowflake(raw_id) + assert str(raw_id) != neko_snowflake + + def test_lt(self, neko_snowflake, raw_id): + assert neko_snowflake < raw_id + 1 + + +class TestHashable: + def test_hashable_serializes_id_to_int(self): + assert entities.Hashable(entities.Snowflake(12)).serialize() == {"id": "12"} + + def test_hashable_deserializes_id_to_int(self): + assert entities.Hashable.deserialize({"id": "12"}) == entities.Hashable(entities.Snowflake(12)) From 1b666c05d543cd940d9ec0b8d6f0359525e39de7 Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Thu, 12 Mar 2020 23:08:28 +0000 Subject: [PATCH 004/922] Temporarily disabled Py3.9 due to cattrs bug [skip deploy] --- gitlab/test.yml | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/gitlab/test.yml b/gitlab/test.yml index f1f9a2be94..a960dd6037 100644 --- a/gitlab/test.yml +++ b/gitlab/test.yml @@ -76,10 +76,10 @@ dependency_scanning: image: python:3.8.1 extends: .venv -.cpython-3.9-rc: - interruptible: true - image: python:3.9-rc - extends: .venv +#.cpython-3.9-rc: +# interruptible: true +# image: python:3.9-rc +# extends: .venv .cpython-tool-alpine: interruptible: true @@ -136,10 +136,10 @@ pytest-c3.8.2: - .cpython-3.8.2 - .nox-test -pytest-c3.9-rc: - extends: - - .cpython-3.9-rc - - .nox-test +#pytest-c3.9-rc: +# extends: +# - .cpython-3.9-rc +# - .nox-test install-c3.8.0: extends: @@ -156,10 +156,10 @@ install-c3.8.2: - .cpython-3.8.2 - .nox-pip-install -install-c3.9-rc: - extends: - - .cpython-3.9-rc - - .nox-pip-install +#install-c3.9-rc: +# extends: +# - .cpython-3.9-rc +# - .nox-pip-install verify-c3.8.0-pypi: extends: @@ -176,10 +176,10 @@ verify-c3.8.2-pypi: - .cpython-3.8.2 - .nox-pip-install-showtime -verify-c3.9-rc-pypi: - extends: - - .cpython-3.9-rc - - .nox-pip-install-showtime +#verify-c3.9-rc-pypi: +# extends: +# - .cpython-3.9-rc +# - .nox-pip-install-showtime coverage-coalesce: extends: From 80a78f68668eb672aad385391b14d3d49c69538c Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Sat, 14 Mar 2020 20:00:44 +0000 Subject: [PATCH 005/922] Added skeleton entities. --- hikari/core/__init__.py | 8 + hikari/core/channels.py | 71 ++++++++ hikari/core/entities.py | 63 ++----- hikari/core/events.py | 254 +++++++++++++++++++++++++++++ hikari/core/gateway_bot.py | 31 ++++ hikari/core/guilds.py | 47 ++++++ hikari/core/invites.py | 26 +++ hikari/core/messages.py | 56 +++++++ hikari/core/oauth2.py | 31 ++++ hikari/core/users.py | 31 ++++ tests/hikari/core/test_entities.py | 35 ---- 11 files changed, 573 insertions(+), 80 deletions(-) create mode 100644 hikari/core/channels.py create mode 100644 hikari/core/events.py create mode 100644 hikari/core/gateway_bot.py create mode 100644 hikari/core/guilds.py create mode 100644 hikari/core/invites.py create mode 100644 hikari/core/messages.py create mode 100644 hikari/core/oauth2.py create mode 100644 hikari/core/users.py diff --git a/hikari/core/__init__.py b/hikari/core/__init__.py index 91d1cfa1df..03837dfe9f 100644 --- a/hikari/core/__init__.py +++ b/hikari/core/__init__.py @@ -17,3 +17,11 @@ # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . """The core API for interacting with Discord directly.""" +from . import channels +from . import entities +from . import events +from . import gateway_bot +from . import guilds +from . import invites +from . import messages +from . import users diff --git a/hikari/core/channels.py b/hikari/core/channels.py new file mode 100644 index 0000000000..e0768b16cb --- /dev/null +++ b/hikari/core/channels.py @@ -0,0 +1,71 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# Copyright © Nekoka.tt 2019-2020 +# +# This file is part of Hikari. +# +# Hikari is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Hikari is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Hikari. If not, see . +import attr + +from hikari.core import entities + + +@attr.s(slots=True) +class Channel(entities.UniqueEntity): + ... + + +@attr.s(slots=False) +class TextChannel(Channel): + ... + + +@attr.s(slots=False) +class DMChannel(Channel): + ... + + +@attr.s(slots=False) +class GroupDMChannel(DMChannel): + ... + + +@attr.s(slots=False) +class GuildChannel(Channel): + ... + + +@attr.s(slots=False) +class GuildTextChannel(GuildChannel, TextChannel): + ... + + +@attr.s(slots=False) +class GuildVoiceChannel(GuildChannel): + ... + + +@attr.s(slots=False) +class GuildCategory(GuildChannel): + ... + + +@attr.s(slots=False) +class GuildStoreChannel(GuildChannel): + ... + + +@attr.s(slots=False) +class GuildNewsChannel(GuildChannel, TextChannel): + ... diff --git a/hikari/core/entities.py b/hikari/core/entities.py index 7cd9178462..10513e18ab 100644 --- a/hikari/core/entities.py +++ b/hikari/core/entities.py @@ -19,59 +19,35 @@ """Datastructure bases.""" import datetime import functools - -import cattr +import typing import attr -import typing from hikari.internal_utilities import dates +RawEntityT = typing.Union[ + None, bool, int, float, str, bytes, typing.Sequence[typing.Any], typing.Mapping[str, typing.Any] +] -class HikariConverter(cattr.Converter): - pass - +T_conta = typing.TypeVar("T_contra", contravariant=True) -class Entity: - """A model entity.""" +# DO NOT ADD ATTRIBUTES TO THIS CLASS. +@attr.s(slots=True) +class HikariEntity: __slots__ = () - _converter = HikariConverter() - - @classmethod - def __init_class__(cls, converter: cattr.Converter): - ... - - def __init_subclass__(cls, **kwargs): - super().__init_subclass__() - cls.__init_class__(cls._converter) - def serialize(self): - """ - Returns - ------- - - A JSON-compatible representation of this object. - """ - return self._converter.unstructure(self) +# DO NOT ADD ATTRIBUTES TO THIS CLASS. +@attr.s(slots=True) +class Deserializable: @classmethod - def deserialize(cls, payload): - """ - Parameters - ---------- - payload: - The payload to deserialize. - - Returns - ------- - The structured object. - """ - return cls._converter.structure(payload, cls) + def deserialize(cls: typing.Type[T_conta], payload: RawEntityT) -> T_conta: + raise NotImplementedError() @functools.total_ordering -class Snowflake(Entity, typing.SupportsInt): +class Snowflake(HikariEntity, typing.SupportsInt): """A concrete representation of a unique identifier for an object on Discord. """ @@ -81,12 +57,6 @@ class Snowflake(Entity, typing.SupportsInt): def __init__(self, value: typing.Union[int, str]) -> None: self.value = int(value) - @classmethod - def __init_class__(cls, converter): - converter.register_structure_hook(cls, lambda data, t: t(data)) - converter.register_unstructure_hook(cls, str) - ... - @property def created_at(self) -> datetime.datetime: """When the object was created.""" @@ -123,9 +93,12 @@ def __eq__(self, other): def __lt__(self, other): return self.value < int(other) + def serialize(self) -> str: + return str(self.value) + @attr.s(slots=True) -class Hashable(Entity): +class UniqueEntity(HikariEntity): """An entity that has an integer ID of some sort.""" id: Snowflake = attr.ib(hash=True, eq=True, repr=True) diff --git a/hikari/core/events.py b/hikari/core/events.py new file mode 100644 index 0000000000..4348b71316 --- /dev/null +++ b/hikari/core/events.py @@ -0,0 +1,254 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# Copyright © Nekoka.tt 2019-2020 +# +# This file is part of Hikari. +# +# Hikari is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Hikari is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Hikari. If not, see . + +import typing + +import attr + +from hikari.core import entities +from hikari.core import guilds as guilds_ +from hikari.core import users + +T_contra = typing.TypeVar("T_contra", contravariant=True) + + +@attr.s(slots=True, auto_attribs=True) +class HikariEvent(entities.HikariEntity): + ... + + +# Synthetic event, is not deserialized +@attr.s(slots=True, auto_attribs=True) +class ConnectedEvent(HikariEvent): + ... + + +# Synthetic event, is not deserialized +@attr.s(slots=True, auto_attribs=True) +class DisconnectedEvent(HikariEvent): + ... + + +# Synthetic event, is not deserialized +@attr.s(slots=True, auto_attribs=True) +class ReconnectedEvent(HikariEvent): + ... + + +# Synthetic event, is not deserialized +@attr.s(slots=True, auto_attribs=True) +class StartedEvent(HikariEvent): + ... + + +# Synthetic event, is not deserialized +@attr.s(slots=True, auto_attribs=True) +class StoppingEvent(HikariEvent): + ... + + +# Synthetic event, is not deserialized +@attr.s(slots=True, auto_attribs=True) +class StoppedEvent(HikariEvent): + ... + + +@attr.s(slots=True, auto_attribs=True) +class ReadyEvent(HikariEvent, entities.Deserializable): + v: int + user: users.User + guilds: guilds_.Guild + session_id: str + shard_id: int + shard_count: int + + +@attr.s(slots=True, auto_attribs=True) +class ResumedEvent(HikariEvent, entities.Deserializable): + ... + + +@attr.s(slots=True, auto_attribs=True) +class ChannelCreateEvent(HikariEvent, entities.Deserializable): + ... + + +@attr.s(slots=True, auto_attribs=True) +class ChannelUpdateEvent(HikariEvent, entities.Deserializable): + ... + + +@attr.s(slots=True, auto_attribs=True) +class ChannelDeleteEvent(HikariEvent, entities.Deserializable): + ... + + +@attr.s(slots=True, auto_attribs=True) +class ChannelPinAddEvent(HikariEvent, entities.Deserializable): + ... + + +@attr.s(slots=True, auto_attribs=True) +class ChannelPinRemoveEvent(HikariEvent, entities.Deserializable): + ... + + +@attr.s(slots=True, auto_attribs=True) +class GuildCreateEvent(HikariEvent, entities.Deserializable): + ... + + +@attr.s(slots=True, auto_attribs=True) +class GuildUpdateEvent(HikariEvent, entities.Deserializable): + ... + + +@attr.s(slots=True, auto_attribs=True) +class GuildDeleteEvent(HikariEvent, entities.Deserializable): + ... + + +@attr.s(slots=True, auto_attribs=True) +class GuildBanAddEvent(HikariEvent, entities.Deserializable): + ... + + +@attr.s(slots=True, auto_attribs=True) +class GuildBanRemoveEvent(HikariEvent, entities.Deserializable): + ... + + +@attr.s(slots=True, auto_attribs=True) +class GuildEmojisUpdateEvent(HikariEvent, entities.Deserializable): + ... + + +@attr.s(slots=True, auto_attribs=True) +class GuildIntegrationsUpdateEvent(HikariEvent, entities.Deserializable): + ... + + +@attr.s(slots=True, auto_attribs=True) +class GuildMemberAddEvent(HikariEvent, entities.Deserializable): + ... + + +@attr.s(slots=True, auto_attribs=True) +class GuildMemberRemoveEvent(HikariEvent, entities.Deserializable): + ... + + +@attr.s(slots=True, auto_attribs=True) +class GuildMemberUpdateEvent(HikariEvent, entities.Deserializable): + ... + + +@attr.s(slots=True, auto_attribs=True) +class GuildRoleCreateEvent(HikariEvent, entities.Deserializable): + ... + + +@attr.s(slots=True, auto_attribs=True) +class GuildRoleUpdateEvent(HikariEvent, entities.Deserializable): + ... + + +@attr.s(slots=True, auto_attribs=True) +class GuildRoleDeleteEvent(HikariEvent, entities.Deserializable): + ... + + +@attr.s(slots=True, auto_attribs=True) +class InviteCreateEvent(HikariEvent, entities.Deserializable): + ... + + +@attr.s(slots=True, auto_attribs=True) +class InviteDeleteEvent(HikariEvent, entities.Deserializable): + ... + + +@attr.s(slots=True, auto_attribs=True) +class MessageCreateEvent(HikariEvent, entities.Deserializable): + ... + + +@attr.s(slots=True, auto_attribs=True) +class MessageUpdateEvent(HikariEvent, entities.Deserializable): + ... + + +@attr.s(slots=True, auto_attribs=True) +class MessageDeleteEvent(HikariEvent, entities.Deserializable): + ... + + +@attr.s(slots=True, auto_attribs=True) +class MessageDeleteBulkEvent(HikariEvent, entities.Deserializable): + ... + + +@attr.s(slots=True, auto_attribs=True) +class MessageReactionAddEvent(HikariEvent, entities.Deserializable): + ... + + +@attr.s(slots=True, auto_attribs=True) +class MessageReactionRemoveEvent(HikariEvent, entities.Deserializable): + ... + + +@attr.s(slots=True, auto_attribs=True) +class MessageReactionRemoveAllEvent(HikariEvent, entities.Deserializable): + ... + + +@attr.s(slots=True, auto_attribs=True) +class MessageReactionRemoveEmojiEvent(HikariEvent, entities.Deserializable): + ... + + +@attr.s(slots=True, auto_attribs=True) +class PresenceUpdateEvent(HikariEvent, entities.Deserializable): + ... + + +@attr.s(slots=True, auto_attribs=True) +class TypingStartEvent(HikariEvent, entities.Deserializable): + ... + + +@attr.s(slots=True, auto_attribs=True) +class UserUpdateEvent(HikariEvent, entities.Deserializable): + ... + + +@attr.s(slots=True, auto_attribs=True) +class VoiceStateUpdateEvent(HikariEvent, entities.Deserializable): + ... + + +@attr.s(slots=True, auto_attribs=True) +class VoiceServerUpdateEvent(HikariEvent, entities.Deserializable): + ... + + +@attr.s(slots=True, auto_attribs=True) +class WebhookUpdate(HikariEvent, entities.Deserializable): + ... diff --git a/hikari/core/gateway_bot.py b/hikari/core/gateway_bot.py new file mode 100644 index 0000000000..829b4e2432 --- /dev/null +++ b/hikari/core/gateway_bot.py @@ -0,0 +1,31 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# Copyright © Nekoka.tt 2019-2020 +# +# This file is part of Hikari. +# +# Hikari is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Hikari is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Hikari. If not, see . +import attr + +from hikari.core import entities + + +@attr.s(slots=True, auto_attribs=True) +class GatewayBot(entities.HikariEntity, entities.Deserializable): + ... + + +@attr.s(slots=True, auto_attribs=True) +class SessionStartLimit(entities.HikariEntity, entities.Deserializable): + ... diff --git a/hikari/core/guilds.py b/hikari/core/guilds.py new file mode 100644 index 0000000000..113bb7588f --- /dev/null +++ b/hikari/core/guilds.py @@ -0,0 +1,47 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# Copyright © Nekoka.tt 2019-2020 +# +# This file is part of Hikari. +# +# Hikari is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Hikari is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Hikari. If not, see . +import attr + +from hikari.core import entities +from hikari.core import users + + +@attr.s(slots=True) +class Guild(entities.UniqueEntity,entities.Deserializable): + ... + + +@attr.s(slots=True, auto_attribs=True) +class Member(entities.UniqueEntity, entities.Deserializable): + user: users.User + + +@attr.s(slots=True, auto_attribs=True) +class Presence(entities.HikariEntity, entities.Deserializable): + ... + + +@attr.s(slots=True) +class Integration(entities.UniqueEntity, entities.Deserializable): + ... + + +@attr.s(slots=True) +class Ban(entities.HikariEntity, entities.Deserializable): + ... diff --git a/hikari/core/invites.py b/hikari/core/invites.py new file mode 100644 index 0000000000..656a28b0da --- /dev/null +++ b/hikari/core/invites.py @@ -0,0 +1,26 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# Copyright © Nekoka.tt 2019-2020 +# +# This file is part of Hikari. +# +# Hikari is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Hikari is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Hikari. If not, see . +import attr + +from hikari.core import entities + + +@attr.s(slots=True) +class Invite(entities.HikariEntity, entities.Deserializable): + ... diff --git a/hikari/core/messages.py b/hikari/core/messages.py new file mode 100644 index 0000000000..334fdf5b9f --- /dev/null +++ b/hikari/core/messages.py @@ -0,0 +1,56 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# Copyright © Nekoka.tt 2019-2020 +# +# This file is part of Hikari. +# +# Hikari is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Hikari is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Hikari. If not, see . +import attr + +from hikari.core import entities + + +@attr.s(slots=True) +class Message(entities.UniqueEntity, entities.Deserializable): + ... + + +@attr.s(slots=True) +class Attachment(entities.UniqueEntity, entities.Deserializable): + ... + + +@attr.s(slots=True) +class Embed(entities.HikariEntity, entities.Deserializable): + ... + + +@attr.s(slots=True) +class Emoji(entities.HikariEntity, entities.Deserializable): + ... + + +@attr.s(slots=True) +class UnicodeEmoji(Emoji, entities.Deserializable): + ... + + +@attr.s(slots=False) +class CustomEmoji(entities.UniqueEntity, Emoji, entities.Deserializable): + ... + + +@attr.s(slots=True) +class Reaction(entities.HikariEntity, entities.Deserializable): + ... diff --git a/hikari/core/oauth2.py b/hikari/core/oauth2.py new file mode 100644 index 0000000000..c0c53d0536 --- /dev/null +++ b/hikari/core/oauth2.py @@ -0,0 +1,31 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# Copyright © Nekoka.tt 2019-2020 +# +# This file is part of Hikari. +# +# Hikari is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Hikari is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Hikari. If not, see . +import attr + +from hikari.core import entities + + +@attr.s(slots=True) +class Owner(entities.UniqueEntity, entities.Deserializable): + ... + + +@attr.s(slots=True) +class Team(entities.UniqueEntity, entities.Deserializable): + ... diff --git a/hikari/core/users.py b/hikari/core/users.py new file mode 100644 index 0000000000..936b685288 --- /dev/null +++ b/hikari/core/users.py @@ -0,0 +1,31 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# Copyright © Nekoka.tt 2019-2020 +# +# This file is part of Hikari. +# +# Hikari is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Hikari is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Hikari. If not, see . +import attr + +from hikari.core import entities + + +@attr.s(slots=True) +class User(entities.UniqueEntity, entities.Deserializable): + ... + + +@attr.s(slots=True) +class MyUser(User): + ... diff --git a/tests/hikari/core/test_entities.py b/tests/hikari/core/test_entities.py index b3d732534c..46ccbce56c 100644 --- a/tests/hikari/core/test_entities.py +++ b/tests/hikari/core/test_entities.py @@ -18,38 +18,11 @@ # along with Hikari. If not, see . import datetime -import attr -import cymock import pytest from hikari.core import entities -class TestEntity: - def test_init_subclass_invokes_init_class(self): - class SubEntity(entities.Entity): - __init_class__ = cymock.MagicMock() - - SubEntity.__init_class__.assert_called_once_with(entities.Entity._converter) - - def test_serialize_unstructures_instance(self): - @attr.s(slots=True) - class SubEntity(entities.Entity): - a_number: int = attr.ib() - a_string: str = attr.ib() - - instance = SubEntity(9, "18") - assert instance.serialize() == {"a_number": 9, "a_string": "18"} - - def test_deserialize_structures_new_instance(self): - @attr.s(slots=True) - class SubEntity(entities.Entity): - a_number: int = attr.ib() - a_string: str = attr.ib() - - assert SubEntity.deserialize({"a_number": 9, "a_string": "18"}) == SubEntity(9, "18") - - class TestSnowflake: @pytest.fixture() def raw_id(self): @@ -89,11 +62,3 @@ def test_eq(self, neko_snowflake, raw_id): def test_lt(self, neko_snowflake, raw_id): assert neko_snowflake < raw_id + 1 - - -class TestHashable: - def test_hashable_serializes_id_to_int(self): - assert entities.Hashable(entities.Snowflake(12)).serialize() == {"id": "12"} - - def test_hashable_deserializes_id_to_int(self): - assert entities.Hashable.deserialize({"id": "12"}) == entities.Hashable(entities.Snowflake(12)) From e60837603246808c74e9bcd2825a1148ec72429e Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Sat, 14 Mar 2020 20:14:23 +0000 Subject: [PATCH 006/922] Added pre-commit hook because it might be useful. --- hikari/core/guilds.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hikari/core/guilds.py b/hikari/core/guilds.py index 113bb7588f..916a1a5277 100644 --- a/hikari/core/guilds.py +++ b/hikari/core/guilds.py @@ -23,7 +23,7 @@ @attr.s(slots=True) -class Guild(entities.UniqueEntity,entities.Deserializable): +class Guild(entities.UniqueEntity, entities.Deserializable): ... From ff5eca75c8be6421482596e430c33fbcba7d8429 Mon Sep 17 00:00:00 2001 From: Faster Speeding Date: Sat, 14 Mar 2020 12:47:45 +0000 Subject: [PATCH 007/922] Make Snowflake hashable and remove redundant int instance check. --- hikari/core/entities.py | 7 +++++-- tests/hikari/core/test_entities.py | 3 +++ 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/hikari/core/entities.py b/hikari/core/entities.py index 10513e18ab..61570eb2be 100644 --- a/hikari/core/entities.py +++ b/hikari/core/entities.py @@ -78,6 +78,9 @@ def increment(self) -> int: """The increment of Discord's system when this object was made.""" return self.value & 0xFFF + def __hash__(self): + return self.value + def __int__(self): return self.value @@ -88,7 +91,7 @@ def __str__(self): return str(self.value) def __eq__(self, other): - return isinstance(other, (int, typing.SupportsInt)) and int(other) == self.value + return isinstance(other, typing.SupportsInt) and int(other) == self.value def __lt__(self, other): return self.value < int(other) @@ -97,7 +100,7 @@ def serialize(self) -> str: return str(self.value) -@attr.s(slots=True) +@attr.s(slots=True, hash=True) class UniqueEntity(HikariEntity): """An entity that has an integer ID of some sort.""" diff --git a/tests/hikari/core/test_entities.py b/tests/hikari/core/test_entities.py index 46ccbce56c..3b838a52ce 100644 --- a/tests/hikari/core/test_entities.py +++ b/tests/hikari/core/test_entities.py @@ -46,6 +46,9 @@ def test_internal_process_id(self, neko_snowflake): def test_internal_worker_id(self, neko_snowflake): assert neko_snowflake.internal_worker_id == 2 + def test_hash(self, neko_snowflake, raw_id): + assert hash(neko_snowflake) == raw_id + def test_int_cast(self, neko_snowflake, raw_id): assert int(neko_snowflake) == raw_id From 74b07e04fbd1ed13393e6000e9488de32337534b Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Sun, 15 Mar 2020 12:07:26 +0000 Subject: [PATCH 008/922] Started guild implementation, moved snowflake to own file, made deserializable a structural protocol for duck typing. Reordered more things, moved more guild related entities into the guild module (should help with circular ref issues later) --- hikari/core/__init__.py | 15 + hikari/core/channels.py | 41 +- hikari/core/entities.py | 89 +-- hikari/core/events.py | 118 ++-- hikari/core/gateway_bot.py | 6 +- hikari/core/guilds.py | 508 +++++++++++++++++- hikari/core/invites.py | 4 +- hikari/core/messages.py | 21 +- hikari/core/oauth2.py | 8 +- hikari/core/permissions.py | 152 ++++++ hikari/core/snowflakes.py | 109 ++++ hikari/core/users.py | 6 +- hikari/internal_utilities/cache.py | 2 - hikari/internal_utilities/containers.py | 1 - hikari/net/codes.py | 1 + pre-commit | 5 + requirements.txt | 1 - .../{test_entities.py => test_snowflake.py} | 6 +- 18 files changed, 919 insertions(+), 174 deletions(-) create mode 100644 hikari/core/permissions.py create mode 100644 hikari/core/snowflakes.py create mode 100755 pre-commit rename tests/hikari/core/{test_entities.py => test_snowflake.py} (93%) diff --git a/hikari/core/__init__.py b/hikari/core/__init__.py index 03837dfe9f..1499b168c0 100644 --- a/hikari/core/__init__.py +++ b/hikari/core/__init__.py @@ -24,4 +24,19 @@ from . import guilds from . import invites from . import messages +from . import oauth2 +from . import permissions +from . import snowflakes from . import users + +from .channels import * +from .entities import * +from .events import * +from .gateway_bot import * +from .guilds import * +from .invites import * +from .messages import * +from .oauth2 import * +from .permissions import * +from .snowflakes import * +from .users import * diff --git a/hikari/core/channels.py b/hikari/core/channels.py index e0768b16cb..7e19779007 100644 --- a/hikari/core/channels.py +++ b/hikari/core/channels.py @@ -16,18 +16,15 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . +__all__ = ["Channel", "DMChannel", "GroupDMChannel"] + import attr -from hikari.core import entities +from hikari.core import snowflakes @attr.s(slots=True) -class Channel(entities.UniqueEntity): - ... - - -@attr.s(slots=False) -class TextChannel(Channel): +class Channel(snowflakes.UniqueEntity): ... @@ -39,33 +36,3 @@ class DMChannel(Channel): @attr.s(slots=False) class GroupDMChannel(DMChannel): ... - - -@attr.s(slots=False) -class GuildChannel(Channel): - ... - - -@attr.s(slots=False) -class GuildTextChannel(GuildChannel, TextChannel): - ... - - -@attr.s(slots=False) -class GuildVoiceChannel(GuildChannel): - ... - - -@attr.s(slots=False) -class GuildCategory(GuildChannel): - ... - - -@attr.s(slots=False) -class GuildStoreChannel(GuildChannel): - ... - - -@attr.s(slots=False) -class GuildNewsChannel(GuildChannel, TextChannel): - ... diff --git a/hikari/core/entities.py b/hikari/core/entities.py index 61570eb2be..3dd4e64c80 100644 --- a/hikari/core/entities.py +++ b/hikari/core/entities.py @@ -17,91 +17,42 @@ # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . """Datastructure bases.""" -import datetime -import functools +__all__ = ["HikariEntity", "ISerializable", "IDeserializable", "RawEntityT"] + +import abc import typing import attr -from hikari.internal_utilities import dates - RawEntityT = typing.Union[ None, bool, int, float, str, bytes, typing.Sequence[typing.Any], typing.Mapping[str, typing.Any] ] -T_conta = typing.TypeVar("T_contra", contravariant=True) +T_contra = typing.TypeVar("T_contra", contravariant=True) +T_co = typing.TypeVar("T_co", covariant=True) -# DO NOT ADD ATTRIBUTES TO THIS CLASS. @attr.s(slots=True) -class HikariEntity: - __slots__ = () +class HikariEntity(metaclass=abc.ABCMeta): + """The base for any entity used in this API.""" - -# DO NOT ADD ATTRIBUTES TO THIS CLASS. -@attr.s(slots=True) -class Deserializable: - @classmethod - def deserialize(cls: typing.Type[T_conta], payload: RawEntityT) -> T_conta: - raise NotImplementedError() + __slots__ = () -@functools.total_ordering -class Snowflake(HikariEntity, typing.SupportsInt): - """A concrete representation of a unique identifier for an object on - Discord. +class IDeserializable(typing.Protocol): + """An interface for any type that allows deserialization from a raw value + into a Hikari entity. """ - __slots__ = ("value",) - - def __init__(self, value: typing.Union[int, str]) -> None: - self.value = int(value) - - @property - def created_at(self) -> datetime.datetime: - """When the object was created.""" - epoch = self.value >> 22 - return dates.discord_epoch_to_datetime(epoch) - - @property - def internal_worker_id(self) -> int: - """The internal worker ID that created this object on Discord.""" - return (self.value & 0x3E0_000) >> 17 - - @property - def internal_process_id(self) -> int: - """The internal process ID that created this object on Discord.""" - return (self.value & 0x1F_000) >> 12 - - @property - def increment(self) -> int: - """The increment of Discord's system when this object was made.""" - return self.value & 0xFFF - - def __hash__(self): - return self.value - - def __int__(self): - return self.value - - def __repr__(self): - return repr(self.value) - - def __str__(self): - return str(self.value) - - def __eq__(self, other): - return isinstance(other, typing.SupportsInt) and int(other) == self.value - - def __lt__(self, other): - return self.value < int(other) - - def serialize(self) -> str: - return str(self.value) + @classmethod + def deserialize(cls: typing.Type[T_contra], payload: RawEntityT) -> T_contra: + ... -@attr.s(slots=True, hash=True) -class UniqueEntity(HikariEntity): - """An entity that has an integer ID of some sort.""" +class ISerializable(typing.Protocol): + """An interface for any type that allows serialization from a Hikari entity + into a raw value. + """ - id: Snowflake = attr.ib(hash=True, eq=True, repr=True) + def serialize(self: T_co) -> RawEntityT: + ... diff --git a/hikari/core/events.py b/hikari/core/events.py index 4348b71316..46ebd2fbaf 100644 --- a/hikari/core/events.py +++ b/hikari/core/events.py @@ -17,6 +17,52 @@ # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . +__all__ = [ + "HikariEvent", + "ConnectedEvent", + "DisconnectedEvent", + "ReconnectedEvent", + "StartedEvent", + "StoppingEvent", + "StoppedEvent", + "ReadyEvent", + "ResumedEvent", + "ChannelCreateEvent", + "ChannelUpdateEvent", + "ChannelDeleteEvent", + "ChannelPinAddEvent", + "ChannelPinRemoveEvent", + "GuildCreateEvent", + "GuildUpdateEvent", + "GuildDeleteEvent", + "GuildBanAddEvent", + "GuildBanRemoveEvent", + "GuildEmojisUpdateEvent", + "GuildIntegrationsUpdateEvent", + "GuildMemberAddEvent", + "GuildMemberUpdateEvent", + "GuildMemberRemoveEvent", + "GuildRoleCreateEvent", + "GuildRoleUpdateEvent", + "GuildRoleDeleteEvent", + "InviteCreateEvent", + "InviteDeleteEvent", + "MessageCreateEvent", + "MessageUpdateEvent", + "MessageDeleteEvent", + "MessageDeleteBulkEvent", + "MessageReactionAddEvent", + "MessageReactionRemoveEvent", + "MessageReactionRemoveAllEvent", + "MessageReactionRemoveEmojiEvent", + "PresenceUpdateEvent", + "TypingStartEvent", + "UserUpdateEvent", + "VoiceStateUpdateEvent", + "VoiceServerUpdateEvent", + "WebhookUpdate", +] + import typing import attr @@ -70,7 +116,7 @@ class StoppedEvent(HikariEvent): @attr.s(slots=True, auto_attribs=True) -class ReadyEvent(HikariEvent, entities.Deserializable): +class ReadyEvent(HikariEvent): v: int user: users.User guilds: guilds_.Guild @@ -80,175 +126,175 @@ class ReadyEvent(HikariEvent, entities.Deserializable): @attr.s(slots=True, auto_attribs=True) -class ResumedEvent(HikariEvent, entities.Deserializable): +class ResumedEvent(HikariEvent): ... @attr.s(slots=True, auto_attribs=True) -class ChannelCreateEvent(HikariEvent, entities.Deserializable): +class ChannelCreateEvent(HikariEvent): ... @attr.s(slots=True, auto_attribs=True) -class ChannelUpdateEvent(HikariEvent, entities.Deserializable): +class ChannelUpdateEvent(HikariEvent): ... @attr.s(slots=True, auto_attribs=True) -class ChannelDeleteEvent(HikariEvent, entities.Deserializable): +class ChannelDeleteEvent(HikariEvent): ... @attr.s(slots=True, auto_attribs=True) -class ChannelPinAddEvent(HikariEvent, entities.Deserializable): +class ChannelPinAddEvent(HikariEvent): ... @attr.s(slots=True, auto_attribs=True) -class ChannelPinRemoveEvent(HikariEvent, entities.Deserializable): +class ChannelPinRemoveEvent(HikariEvent): ... @attr.s(slots=True, auto_attribs=True) -class GuildCreateEvent(HikariEvent, entities.Deserializable): +class GuildCreateEvent(HikariEvent): ... @attr.s(slots=True, auto_attribs=True) -class GuildUpdateEvent(HikariEvent, entities.Deserializable): +class GuildUpdateEvent(HikariEvent): ... @attr.s(slots=True, auto_attribs=True) -class GuildDeleteEvent(HikariEvent, entities.Deserializable): +class GuildDeleteEvent(HikariEvent): ... @attr.s(slots=True, auto_attribs=True) -class GuildBanAddEvent(HikariEvent, entities.Deserializable): +class GuildBanAddEvent(HikariEvent): ... @attr.s(slots=True, auto_attribs=True) -class GuildBanRemoveEvent(HikariEvent, entities.Deserializable): +class GuildBanRemoveEvent(HikariEvent): ... @attr.s(slots=True, auto_attribs=True) -class GuildEmojisUpdateEvent(HikariEvent, entities.Deserializable): +class GuildEmojisUpdateEvent(HikariEvent): ... @attr.s(slots=True, auto_attribs=True) -class GuildIntegrationsUpdateEvent(HikariEvent, entities.Deserializable): +class GuildIntegrationsUpdateEvent(HikariEvent): ... @attr.s(slots=True, auto_attribs=True) -class GuildMemberAddEvent(HikariEvent, entities.Deserializable): +class GuildMemberAddEvent(HikariEvent): ... @attr.s(slots=True, auto_attribs=True) -class GuildMemberRemoveEvent(HikariEvent, entities.Deserializable): +class GuildMemberRemoveEvent(HikariEvent): ... @attr.s(slots=True, auto_attribs=True) -class GuildMemberUpdateEvent(HikariEvent, entities.Deserializable): +class GuildMemberUpdateEvent(HikariEvent): ... @attr.s(slots=True, auto_attribs=True) -class GuildRoleCreateEvent(HikariEvent, entities.Deserializable): +class GuildRoleCreateEvent(HikariEvent): ... @attr.s(slots=True, auto_attribs=True) -class GuildRoleUpdateEvent(HikariEvent, entities.Deserializable): +class GuildRoleUpdateEvent(HikariEvent): ... @attr.s(slots=True, auto_attribs=True) -class GuildRoleDeleteEvent(HikariEvent, entities.Deserializable): +class GuildRoleDeleteEvent(HikariEvent): ... @attr.s(slots=True, auto_attribs=True) -class InviteCreateEvent(HikariEvent, entities.Deserializable): +class InviteCreateEvent(HikariEvent): ... @attr.s(slots=True, auto_attribs=True) -class InviteDeleteEvent(HikariEvent, entities.Deserializable): +class InviteDeleteEvent(HikariEvent): ... @attr.s(slots=True, auto_attribs=True) -class MessageCreateEvent(HikariEvent, entities.Deserializable): +class MessageCreateEvent(HikariEvent): ... @attr.s(slots=True, auto_attribs=True) -class MessageUpdateEvent(HikariEvent, entities.Deserializable): +class MessageUpdateEvent(HikariEvent): ... @attr.s(slots=True, auto_attribs=True) -class MessageDeleteEvent(HikariEvent, entities.Deserializable): +class MessageDeleteEvent(HikariEvent): ... @attr.s(slots=True, auto_attribs=True) -class MessageDeleteBulkEvent(HikariEvent, entities.Deserializable): +class MessageDeleteBulkEvent(HikariEvent): ... @attr.s(slots=True, auto_attribs=True) -class MessageReactionAddEvent(HikariEvent, entities.Deserializable): +class MessageReactionAddEvent(HikariEvent): ... @attr.s(slots=True, auto_attribs=True) -class MessageReactionRemoveEvent(HikariEvent, entities.Deserializable): +class MessageReactionRemoveEvent(HikariEvent): ... @attr.s(slots=True, auto_attribs=True) -class MessageReactionRemoveAllEvent(HikariEvent, entities.Deserializable): +class MessageReactionRemoveAllEvent(HikariEvent): ... @attr.s(slots=True, auto_attribs=True) -class MessageReactionRemoveEmojiEvent(HikariEvent, entities.Deserializable): +class MessageReactionRemoveEmojiEvent(HikariEvent): ... @attr.s(slots=True, auto_attribs=True) -class PresenceUpdateEvent(HikariEvent, entities.Deserializable): +class PresenceUpdateEvent(HikariEvent): ... @attr.s(slots=True, auto_attribs=True) -class TypingStartEvent(HikariEvent, entities.Deserializable): +class TypingStartEvent(HikariEvent): ... @attr.s(slots=True, auto_attribs=True) -class UserUpdateEvent(HikariEvent, entities.Deserializable): +class UserUpdateEvent(HikariEvent): ... @attr.s(slots=True, auto_attribs=True) -class VoiceStateUpdateEvent(HikariEvent, entities.Deserializable): +class VoiceStateUpdateEvent(HikariEvent): ... @attr.s(slots=True, auto_attribs=True) -class VoiceServerUpdateEvent(HikariEvent, entities.Deserializable): +class VoiceServerUpdateEvent(HikariEvent): ... @attr.s(slots=True, auto_attribs=True) -class WebhookUpdate(HikariEvent, entities.Deserializable): +class WebhookUpdate(HikariEvent): ... diff --git a/hikari/core/gateway_bot.py b/hikari/core/gateway_bot.py index 829b4e2432..ee9164a218 100644 --- a/hikari/core/gateway_bot.py +++ b/hikari/core/gateway_bot.py @@ -16,16 +16,18 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . +__all__ = ["GatewayBot"] + import attr from hikari.core import entities @attr.s(slots=True, auto_attribs=True) -class GatewayBot(entities.HikariEntity, entities.Deserializable): +class GatewayBot(entities.HikariEntity): ... @attr.s(slots=True, auto_attribs=True) -class SessionStartLimit(entities.HikariEntity, entities.Deserializable): +class SessionStartLimit(entities.HikariEntity): ... diff --git a/hikari/core/guilds.py b/hikari/core/guilds.py index 916a1a5277..c66dd852f0 100644 --- a/hikari/core/guilds.py +++ b/hikari/core/guilds.py @@ -16,32 +16,528 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . +"""Components and entities that are used to describe guilds on Discord. +""" + +__all__ = [ + "GuildEmoji", + "GuildChannel", + "GuildTextChannel", + "GuildNewsChannel", + "GuildStoreChannel", + "GuildVoiceChannel", + "GuildCategory", + "GuildRole", + "GuildFeature", + "GuildSystemChannelFlag", + "GuildMessageNotificationsLevel", + "GuildExplicitContentFilterLevel", + "GuildMFALevel", + "GuildVerificationLevel", + "GuildPremiumTier", + "Guild", + "GuildMember", + "GuildMemberPresence", + "GuildIntegration", + "GuildMemberBan", +] + +import datetime +import enum +import typing + import attr +from hikari.core import channels from hikari.core import entities +from hikari.core import messages +from hikari.core import permissions as permissions_ +from hikari.core import snowflakes from hikari.core import users -@attr.s(slots=True) -class Guild(entities.UniqueEntity, entities.Deserializable): +@attr.s(slots=False) +class GuildEmoji(snowflakes.UniqueEntity, messages.Emoji): + ... + + +@attr.s(slots=False) +class GuildChannel(channels.Channel): + """The base for anything that is a guild channel.""" + + +@attr.s(slots=False) +class GuildTextChannel(GuildChannel): + ... + + +@attr.s(slots=False) +class GuildVoiceChannel(GuildChannel): + ... + + +@attr.s(slots=False) +class GuildCategory(GuildChannel): ... +@attr.s(slots=False) +class GuildStoreChannel(GuildChannel): + ... + + +@attr.s(slots=False) +class GuildNewsChannel(GuildChannel): + ... + + +class GuildExplicitContentFilterLevel(enum.IntEnum): + """Represents the explicit content filter setting for a guild.""" + + #: No explicit content filter. + DISABLED = 0 + + #: Filter posts from anyone without a role. + MEMBERS_WITHOUT_ROLES = 1 + + #: Filter all posts. + ALL_MEMBERS = 2 + + +class GuildFeature(str, enum.Enum): + """Features that a guild can provide.""" + + #: Guild has access to set an animated guild icon. + ANIMATED_ICON = "ANIMATED_ICON" + #: Guild has access to set a guild banner image. + BANNER = "BANNER" + #: Guild has access to use commerce features (i.e. create store channels). + COMMERCE = "COMMERCE" + #: Guild is able to be discovered in the directory. + DISCOVERABLE = "DISCOVERABLE" + #: Guild is able to be featured in the directory. + FEATURABLE = "FEATURABLE" + #: Guild has access to set an invite splash background. + INVITE_SPLASH = "INVITE_SPLASH" + #: More emojis can be hosted in this guild than normal. + MORE_EMOJI = "MORE_EMOJI" + #: Guild has access to create news channels. + NEWS = "NEWS" + #: People can view channels in this guild without joining. + LURKABLE = "LURKABLE" + #: Guild is partnered. + PARTNERED = "PARTNERED" + #: Guild is public, go figure. + PUBLIC = "PUBLIC" + #: Guild cannot be public. Who would have guessed? + PUBLIC_DISABLED = "PUBLIC_DISABLED" + #: Guild has access to set a vanity URL. + VANITY_URL = "VANITY_URL" + #: Guild is verified. + VERIFIED = "VERIFIED" + #: Guild has access to set 384kbps bitrate in voice (previously + #: VIP voice servers). + VIP_REGIONS = "VIP_REGIONS" + + +class GuildMessageNotificationsLevel(enum.IntEnum): + """Represents the default notification level for new messages in a guild.""" + + #: Notify users when any message is sent. + ALL_MESSAGES = 0 + + #: Only notify users when they are @mentioned. + ONLY_MENTIONS = 1 + + +class GuildMFALevel(enum.IntEnum): + """Represents the multi-factor authorization requirement for a guild.""" + + #: No MFA requirement. + NONE = 0 + + #: MFA requirement. + ELEVATED = 1 + + +class GuildPremiumTier(enum.IntEnum): + """Tier for Discord Nitro boosting in a guild.""" + + #: No Nitro boosts. + NONE = 0 + + #: Level 1 Nitro boost. + TIER_1 = 1 + + #: Level 2 Nitro boost. + TIER_2 = 2 + + #: Level 3 Nitro boost. + TIER_3 = 3 + + +class GuildSystemChannelFlag(enum.IntFlag): + """Defines which features are suppressed in the system channel.""" + + #: Display a message about new users joining. + SUPPRESS_USER_JOIN = 1 + #: Display a message when the guild is Nitro boosted. + SUPPRESS_PREMIUM_SUBSCRIPTION = 2 + + +class GuildVerificationLevel(enum.IntEnum): + """Represents the level of verification a user needs to provide for their + account before being allowed to participate in a guild.""" + + #: Unrestricted + NONE = 0 + + #: Must have a verified email on their account. + LOW = 1 + + #: Must have been registered on Discord for more than 5 minutes. + MEDIUM = 2 + + #: (╯°□°)╯︵ ┻━┻ - must be a member of the guild for longer than 10 minutes. + HIGH = 3 + + #: ┻━┻ミヽ(ಠ益ಠ)ノ彡┻━┻ - must have a verified phone number. + VERY_HIGH = 4 + + @attr.s(slots=True, auto_attribs=True) -class Member(entities.UniqueEntity, entities.Deserializable): +class GuildMember(snowflakes.UniqueEntity): user: users.User @attr.s(slots=True, auto_attribs=True) -class Presence(entities.HikariEntity, entities.Deserializable): +class GuildMemberPresence(entities.HikariEntity): ... @attr.s(slots=True) -class Integration(entities.UniqueEntity, entities.Deserializable): +class GuildIntegration(snowflakes.UniqueEntity): ... @attr.s(slots=True) -class Ban(entities.HikariEntity, entities.Deserializable): +class GuildMemberBan(entities.HikariEntity): ... + + +@attr.s(slots=True) +class GuildRole(entities.HikariEntity): + ... + + +@attr.s(slots=True, auto_attribs=True) +class Guild(snowflakes.UniqueEntity): + """A representation of a guild on Discord. + + Notes + ----- + + If a guild object is considered to be unavailable, then the state of any + other fields other than the :attr:`is_unavailable` and :attr:`id` members + may be ``None``, outdated, or incorrect. If a guild is unavailable, then + the contents of any other fields should be ignored. + """ + + #: The name of the guild. + #: + #: :type: :class:`str` + name: str + + #: The hash for the guild icon, if there is one. + #: + #: :type: :class:`str`, optional + icon_hash: typing.Optional[str] + + #: The hash of the splash for the guild, if there is one. + #: + #: :type: :class:`str`, optional + splash_hash: typing.Optional[str] + + #: The hash of the discovery splash for the guild, if there is one. + #: + #: :type: :class:`str`, optional + discovery_splash: typing.Optional[str] + + #: The ID of the owner of this guild. + #: + #: :type: :class:`snowflakes.Snowflake` + owner_id: snowflakes.Snowflake + + #: The guild level permissions that apply to the bot user. + #: + #: :type: :class:`hikari.core.permissions.Permission` + my_permissions: permissions_.Permission + + #: The voice region for the guild. + #: + #: :type: :class:`str` + region: str + + #: The ID for the channel that AFK voice users get sent to, if set for the + #: guild. + #: + #: :type: :class:`snowflakes.Snowflake`, optional + afk_channel_id: typing.Optional[snowflakes.Snowflake] + + #: How long a voice user has to be AFK for before they are classed as being + #: AFK and are moved to the AFK channel (:attr:`afk_channel_id`). + #: + #: :type: :class:`datetime.timedelta` + afk_timeout: datetime.timedelta + + # TODO: document when this is not specified. + # FIXME: do we need a field for this, or can we infer it from the `embed_channel_id`? + #: Defines if the guild embed is enabled or not. This information may not + #: be present, in which case, it will be ``None`` instead. + #: + #: :type: :class:`bool`, optional + is_embed_enabled: typing.Optional[bool] + + #: The channel ID that the guild embed will generate an invite to, if + #: enabled for this guild. If not enabled, it will be ``None``. + #: + #: :type: :class:`snowflakes.Snowflake`, optional + embed_channel_id: typing.Optional[snowflakes.Snowflake] + + #: The verification level required for a user to participate in this guild. + #: + #: :type: :class:`GuildVerificationLevel` + verification_level: GuildVerificationLevel + + #: The default setting for message notifications in this guild. + #: + #: :type: :class:`GuildMessageNotificationsLevel` + default_message_notifications: GuildMessageNotificationsLevel + + #: The setting for the explicit content filter in this guild. + #: + #: :type: :class:`GuildExplicitContentFilterLevel` + explicit_content_filter: GuildExplicitContentFilterLevel + + #: The roles in this guild, represented as a mapping of ID to role object. + #: + #: :type: :class:`typing.Mapping` [ :class:`snowflakes.Snowflake`, :class:`GuildRole` ] + roles: typing.Mapping[snowflakes.Snowflake, GuildRole] + + #: The emojis that this guild provides, represented as a mapping of ID to + #: emoji object. + #: + #: :type: :class:`typing.Mapping` [ :class:`snowflakes.Snowflake`, :class:`GuildEmoji` ] + emojis: typing.Mapping[snowflakes.Snowflake, GuildEmoji] + + #: A set of the features in this guild. + #: + #: :type: :class:`typing.Set` [ :class:`GuildFeature` ] + features: typing.Set[GuildFeature] + + #: The required MFA level for users wishing to participate in this guild. + #: + #: :type: :class:`GuildMFALevel` + mfa_level: GuildMFALevel + + #: The ID of the application that created this guild, if it was created by + #: a bot. If not, this is always ``None``. + #: + #: :type: :class:`snowflakes.Snowflake`, optional + application_id: typing.Optional[snowflakes.Snowflake] + + # TODO: document in which cases this information is not available. + #: Describes whether the guild widget is enabled or not. If this information + #: is not present, this will be ``None``. + #: + #: :type: :class:`bool`, optional + is_widget_enabled: typing.Optional[bool] + + #: The channel ID that the widget's generated invite will send the user to, + #: if enabled. If this information is unavailable, this will be ``None``. + #: + #: :type: :class:`snowflakes.Snowflake`, optional + widget_channel_id: typing.Optional[snowflakes.Snowflake] + + #: The ID of the system channel (where welcome messages and Nitro boost + #: messages are sent), or ``None`` if it is not enabled. + #: :type: :class:`snowflakes.Snowflake`, optional + system_channel_id: typing.Optional[snowflakes.Snowflake] + + #: Flags for the guild system channel to describe which notification + #: features are suppressed. + #: + #: :type: :class:`GuildSystemChannelFlag` + system_channel_flags: GuildSystemChannelFlag + + #: The ID of the channel where guilds with the :obj:`GuildFeature.PUBLIC` + #: :attr:`features` display rules and guidelines. If the + #: :obj:`GuildFeature.PUBLIC` feature is not defined, then this is ``None``. + #: + #: :type: :class:`snowflakes.Snowflake`, optional + rules_channel_id: typing.Optional[snowflakes.Snowflake] + + #: The date and time that the bot user joined this guild. + #: + #: This information is only available if the guild was sent via a + #: `GUILD_CREATE` event. If the guild is received from any other place, + #: this will always be ``None``. + #: + #: :type: :class:`datetime.datetime`, optional + joined_at: typing.Optional[datetime.datetime] + + #: Whether the guild is considered to be large or not. + #: + #: This information is only available if the guild was sent via a + #: `GUILD_CREATE` event. If the guild is received from any other place, + #: this will always be ``None``. + #: + #: The implications of a large guild are that presence information will + #: not be sent about members who are offline or invisible. + #: + #: :type: :class:`bool`, optional + is_large: typing.Optional[bool] + + #: Whether the guild is unavailable or not. + #: + #: This information is only available if the guild was sent via a + #: `GUILD_CREATE` event. If the guild is received from any other place, + #: this will always be ``None``. + #: + #: An unavailable guild cannot be interacted with, and most information may + #: be outdated or missing if that is the case. + is_unavailable: typing.Optional[bool] + + #: The number of members in this guild. + #: + #: This information is only available if the guild was sent via a + #: `GUILD_CREATE` event. If the guild is received from any other place, + #: this will always be ``None``. + #: + #: :type: :class:`int`, optional + member_count: typing.Optional[int] + + #: A mapping of ID to the corresponding guild members in this guild. + #: + #: This information is only available if the guild was sent via a + #: `GUILD_CREATE` event. If the guild is received from any other place, + #: this will always be ``None``. + #: + #: Additionally, any offline members may not be included here, especially + #: if there are more members than the large threshold set for the gateway + #: this object was send with. + #: + #: This information will only be updated if your shards have the correct + #: intents set for any update events. + #: + #: Essentially, you should not trust the information here to be a full + #: representation. If you need complete accurate information, you should + #: query the members using the appropriate API call instead. + #: + #: :type: :class:`typing.Mapping` [ :class:`snowflakes.Snowflake`, :class:`GuildMember` ], optional + members: typing.Optional[typing.Mapping[snowflakes.Snowflake, GuildMember]] + + #: A mapping of ID to the corresponding guild channels in this guild. + #: + #: This information is only available if the guild was sent via a + #: `GUILD_CREATE` event. If the guild is received from any other place, + #: this will always be ``None``. + #: + #: Additionally, any channels that you lack permissions to see will not be + #: defined here. + #: + #: This information will only be updated if your shards have the correct + #: intents set for any update events. + #: + #: To retrieve a list of channels in any other case, you should make an + #: appropriate API call to retrieve this information. + #: + #: :type: :class:`typing.Mapping` [ :class:`snowflakes.Snowflake`, :class:`GuildChannel` ], optional + channels: typing.Optional[typing.Mapping[snowflakes.Snowflake, GuildChannel]] + + #: A mapping of member ID to the corresponding presence information for + #: the given member, if available. + #: + #: This information is only available if the guild was sent via a + #: `GUILD_CREATE` event. If the guild is received from any other place, + #: this will always be ``None``. + #: + #: Additionally, any channels that you lack permissions to see will not be + #: defined here. + #: + #: This information will only be updated if your shards have the correct + #: intents set for any update events. + #: + #: To retrieve a list of presences in any other case, you should make an + #: appropriate API call to retrieve this information. + #: + #: :type: :class:`typing.Mapping` [ :class:`snowflakes.Snowflake`, :class:`GuildMemberPresence` ], optional + presences: typing.Optional[typing.Mapping[snowflakes.Snowflake, GuildMemberPresence]] + + #: The maximum number of presences for the guild. If this is ``None``, then + #: the default value is used (currently 5000). + #: + #: :type: :class:`int`, optional + max_presences: typing.Optional[int] + + #: The maximum number of members allowed in this guild. + #: + #: This information may not be present, in which case, it will be ``None``. + #: + #: :type: :class:`int`, optional + max_members: typing.Optional[int] + + #: The vanity URL code for the guild's vanity URL. + #: This is only present if :obj:`GuildFeatures.VANITY_URL` is in the + #: :attr:`features` for this guild. If not, this will always be ``None``. + #: + #: :type: :class:`str`, optional + vanity_url_code: typing.Optional[str] + + #: The guild's description. + #: + #: This is only present if certain :attr:`features` are set in this guild. + #: Otherwise, this will always be ``None``. For all other purposes, it is + #: ``None``. + #: + #: :type: :class:`str`, optional + description: typing.Optional[str] + + #: The hash for the guild's banner. + #: This is only present if the guild has :obj:`GuildFeatures.BANNER` in the + #: :attr:`features` for this guild. For all other purposes, it is ``None``. + #: + #: :type: :class:`str`, optional + banner_hash: typing.Optional[str] + + #: The premium tier for this guild. + #: + #: :type: :class:`GuildPremiumTier` + premium_tier: GuildPremiumTier + + #: The number of nitro boosts that the server currently has. This + #: information may not be present, in which case, it will be ``None``. + #: + #: :type: :class:`int`, optional + premium_subscription_count: typing.Optional[int] + + #: The preferred locale to use for this guild. + #: + #: This appears to only be present if :obj:`GuildFeatures.PUBLIC` is in the + #: :attr:`features` for this guild. For all other purposes, it should be + #: considered to be ``None`` until more clarification is given by Discord. + #: + #: :type: :class:`str`, optional + preferred_locale: typing.Optional[str] + + #: The channel ID of the channel where admins and moderators receive notices + #: from Discord. + #: + #: This is only present if :obj:`GuildFeatures.PUBLIC` is in the + #: :attr:`features` for this guild. For all other purposes, it should be + #: considered to be ``None``. + #: + #: :type: :class:`snowflakes.Snowflake`, optional + public_updates_channel_id: typing.Optional[snowflakes.Snowflake] + + # TODO: voice states diff --git a/hikari/core/invites.py b/hikari/core/invites.py index 656a28b0da..8047c612e9 100644 --- a/hikari/core/invites.py +++ b/hikari/core/invites.py @@ -16,11 +16,13 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . +__all__ = ["Invite"] + import attr from hikari.core import entities @attr.s(slots=True) -class Invite(entities.HikariEntity, entities.Deserializable): +class Invite(entities.HikariEntity): ... diff --git a/hikari/core/messages.py b/hikari/core/messages.py index 334fdf5b9f..7c8aac0121 100644 --- a/hikari/core/messages.py +++ b/hikari/core/messages.py @@ -16,41 +16,40 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . + +__all__ = ["Message", "Attachment", "Embed", "Emoji", "UnicodeEmoji", "Reaction"] + import attr from hikari.core import entities +from hikari.core import snowflakes @attr.s(slots=True) -class Message(entities.UniqueEntity, entities.Deserializable): +class Message(snowflakes.UniqueEntity): ... @attr.s(slots=True) -class Attachment(entities.UniqueEntity, entities.Deserializable): +class Attachment(snowflakes.UniqueEntity): ... @attr.s(slots=True) -class Embed(entities.HikariEntity, entities.Deserializable): +class Embed(entities.HikariEntity): ... @attr.s(slots=True) -class Emoji(entities.HikariEntity, entities.Deserializable): +class Emoji(entities.HikariEntity): ... @attr.s(slots=True) -class UnicodeEmoji(Emoji, entities.Deserializable): - ... - - -@attr.s(slots=False) -class CustomEmoji(entities.UniqueEntity, Emoji, entities.Deserializable): +class UnicodeEmoji(Emoji): ... @attr.s(slots=True) -class Reaction(entities.HikariEntity, entities.Deserializable): +class Reaction(entities.HikariEntity): ... diff --git a/hikari/core/oauth2.py b/hikari/core/oauth2.py index c0c53d0536..f748977ac4 100644 --- a/hikari/core/oauth2.py +++ b/hikari/core/oauth2.py @@ -16,16 +16,18 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . +__all__ = ["Owner", "Team"] + import attr -from hikari.core import entities +from hikari.core import snowflakes @attr.s(slots=True) -class Owner(entities.UniqueEntity, entities.Deserializable): +class Owner(snowflakes.UniqueEntity): ... @attr.s(slots=True) -class Team(entities.UniqueEntity, entities.Deserializable): +class Team(snowflakes.UniqueEntity): ... diff --git a/hikari/core/permissions.py b/hikari/core/permissions.py new file mode 100644 index 0000000000..c245a0dd6e --- /dev/null +++ b/hikari/core/permissions.py @@ -0,0 +1,152 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# Copyright © Nekoka.tt 2019-2020 +# +# This file is part of Hikari. +# +# Hikari is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Hikari is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Hikari. If not, see . +"""Bitfield of permissions. +""" +__all__ = ["Permission"] + +import enum + + +class Permission(enum.IntFlag): + """ + Represents the permissions available in a given channel or guild. + + This is an int-flag enum. This means that you can **combine multiple + permissions together** into one value using the bitwise-OR "`|`" operator. + + .. code-block:: python + + my_perms = Permission.MANAGE_CHANNELS | Permission.MANAGE_GUILD + + your_perms = ( + Permission.CREATE_INSTANT_INVITE + | Permission.KICK_MEMBERS + | Permission.BAN_MEMBERS + | Permission.MANAGE_GUILD + ) + + You can **check if a permission is present** in a set of combined + permissions by using the bitwise-AND "`&`" operator. This will return + the int-value of the permission if it is present, or `0` if not present. + + .. code-block:: python + + my_perms = Permission.MANAGE_CHANNELS | Permission.MANAGE_GUILD + + if my_perms & Permission.MANAGE_CHANNELS: + if my_perms & Permission.MANAGE_GUILD: + print("I have the permission to both manage the guild and the channels in it!") + else: + print("I have the permission to manage channels!") + else: + print("I don't have the permission to manage channels!") + + # Or you could simplify it: + + if my_perms & (Permission.MANAGE_CHANNELS | Permission.MANAGE_GUILD): + print("I have the permission to both manage the guild and the channels in it!") + elif my_perms & Permission.MANAGE_CHANNELS: + print("I have the permission to manage channels!") + else: + print("I don't have the permission to manage channels!") + + If you need to **check that a permission is not present**, you can use the + bitwise-XOR "`^`" operator to check. If the permission is not present, it + will return a non-zero value, otherwise if it is present, it will return 0. + + .. code-block:: python + + my_perms = Permission.MANAGE_CHANNELS | Permission.MANAGE_GUILD + + if my_perms ^ Permission.MANAGE_CHANNELS: + print("Please give me the MANAGE_CHANNELS permission!") + + Lastly, if you need all the permissions set except the permission you want, + you can use the inversion operator "`~`" to do that. + + .. code-block:: python + + # All permissions except ADMINISTRATOR. + my_perms = ~Permission.ADMINISTRATOR + + """ + + #: Empty permission. + NONE = 0x0 + #: Allows creation of instant invites. + CREATE_INSTANT_INVITE = 0x1 + #: Allows kicking members + KICK_MEMBERS = 0x2 + #: Allows banning members. + BAN_MEMBERS = 0x4 + #: Allows all permissions and bypasses channel permission overwrites. + ADMINISTRATOR = 0x8 + #: Allows management and editing of channels. + MANAGE_CHANNELS = 0x10 + #: Allows management and editing of the guild. + MANAGE_GUILD = 0x20 + #: Allows for the addition of reactions to messages. + ADD_REACTIONS = 0x40 + #: Allows for viewing of audit logs. + VIEW_AUDIT_LOG = 0x80 + #: Allows for using priority speaker in a voice channel. + PRIORITY_SPEAKER = 0x1_00 + #: Allows the user to go live. + STREAM = 0x2_00 + #: Allows guild members to view a channel, which includes reading messages in text channels. + VIEW_CHANNEL = 0x4_00 + #: Allows for sending messages in a channel. + SEND_MESSAGES = 0x8_00 + #: Allows for sending of `/tts` messages. + SEND_TTS_MESSAGES = 0x10_00 + #: Allows for deletion of other users messages. + MANAGE_MESSAGES = 0x20_00 + #: Links sent by users with this permission will be auto-embedded. + EMBED_LINKS = 0x40_00 + #: Allows for uploading images and files + ATTACH_FILES = 0x80_00 + #: Allows for reading of message history. + READ_MESSAGE_HISTORY = 0x1_00_00 + #: Allows for using the `@everyone` tag to notify all users in a channel, and the + #: `@here` tag to notify all online users in a channel. + MENTION_EVERYONE = 0x2_00_00 + #: Allows the usage of custom emojis from other servers. + USE_EXTERNAL_EMOJIS = 0x4_00_00 + #: Allows for joining of a voice channel. + CONNECT = 0x10_00_00 + #: Allows for speaking in a voice channel. + SPEAK = 0x20_00_00 + #: Allows for muting members in a voice channel. + MUTE_MEMBERS = 0x40_00_00 + #: Allows for deafening of members in a voice channel. + DEAFEN_MEMBERS = 0x80_00_00 + #: Allows for moving of members between voice channels. + MOVE_MEMBERS = 0x1_00_00_00 + #: Allows for using voice-activity-detection in a voice channel. + USE_VAD = 0x2_00_00_00 + #: Allows for modification of own nickname. + CHANGE_NICKNAME = 0x4_00_00_00 + #: Allows for modification of other users nicknames. + MANAGE_NICKNAMES = 0x8_00_00_00 + #: Allows management and editing of roles. + MANAGE_ROLES = 0x10_00_00_00 + #: Allows management and editing of webhooks. + MANAGE_WEBHOOKS = 0x20_00_00_00 + #: Allows management and editing of emojis. + MANAGE_EMOJIS = 0x40_00_00_00 diff --git a/hikari/core/snowflakes.py b/hikari/core/snowflakes.py new file mode 100644 index 0000000000..879e765bd2 --- /dev/null +++ b/hikari/core/snowflakes.py @@ -0,0 +1,109 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# Copyright © Nekoka.tt 2019-2020 +# +# This file is part of Hikari. +# +# Hikari is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Hikari is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Hikari. If not, see . +"""A representation of the Discord Snowflake datatype, which is a 64-bit integer +used to uniquely identify entities on the server. +""" + +__all__ = ["Snowflake", "UniqueEntity"] + +import datetime +import functools +import typing + +import attr + +from hikari.core import entities +from hikari.core.entities import HikariEntity +from hikari.internal_utilities import dates + + +@functools.total_ordering +class Snowflake(entities.HikariEntity, typing.SupportsInt): + """A concrete representation of a unique identifier for an object on + Discord. + + This object can be treated as a regular :class:`int` for most purposes. + """ + + __slots__ = ("_value",) + + #: The integer value of this ID. + #: + #: :type: :class:`int` + _value: int + + def __init__(self, value: typing.Union[int, str]) -> None: + self._value = int(value) + + @property + def created_at(self) -> datetime.datetime: + """When the object was created.""" + epoch = self._value >> 22 + return dates.discord_epoch_to_datetime(epoch) + + @property + def internal_worker_id(self) -> int: + """The internal worker ID that created this object on Discord.""" + return (self._value & 0x3E0_000) >> 17 + + @property + def internal_process_id(self) -> int: + """The internal process ID that created this object on Discord.""" + return (self._value & 0x1F_000) >> 12 + + @property + def increment(self) -> int: + """The increment of Discord's system when this object was made.""" + return self._value & 0xFFF + + def __hash__(self): + return hash(self._value) + + def __int__(self): + return self._value + + def __repr__(self): + return repr(self._value) + + def __str__(self): + return str(self._value) + + def __eq__(self, other): + return isinstance(other, typing.SupportsInt) and int(other) == self._value + + def __lt__(self, other): + return self._value < int(other) + + def serialize(self) -> str: + """Generate a JSON-friendly representation of this object.""" + return str(self._value) + + @classmethod + def deserialize(cls, value: str) -> "Snowflake": + return cls(value) + + +@attr.s(slots=True) +class UniqueEntity(HikariEntity): + """An entity that has an integer ID of some sort.""" + + #: The ID of this entity. + #: + #: :type: :class:`Snowflake` + id: Snowflake = attr.ib(hash=True, eq=True, repr=True) diff --git a/hikari/core/users.py b/hikari/core/users.py index 936b685288..249a42bff2 100644 --- a/hikari/core/users.py +++ b/hikari/core/users.py @@ -16,13 +16,15 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . +__all__ = ["User", "MyUser"] + import attr -from hikari.core import entities +from hikari.core import snowflakes @attr.s(slots=True) -class User(entities.UniqueEntity, entities.Deserializable): +class User(snowflakes.UniqueEntity): ... diff --git a/hikari/internal_utilities/cache.py b/hikari/internal_utilities/cache.py index f27e771dee..d48426867c 100644 --- a/hikari/internal_utilities/cache.py +++ b/hikari/internal_utilities/cache.py @@ -27,14 +27,12 @@ import os import typing - ReturnT = typing.TypeVar("ReturnT") ClassT = typing.TypeVar("ClassT") CallT = typing.Callable[..., ReturnT] CachedFunctionT = typing.Callable[..., ReturnT] CachedPropertyFunctionT = typing.Callable[[ClassT], ReturnT] - # Hacky workaround to Sphinx being unable to document cached properties. We simply make the # decorators return their inputs when this is True. __is_sphinx = os.getenv("SPHINX_IS_GENERATING_DOCUMENTATION") is not None diff --git a/hikari/internal_utilities/containers.py b/hikari/internal_utilities/containers.py index 1f3201f9c5..d341a8ed0c 100644 --- a/hikari/internal_utilities/containers.py +++ b/hikari/internal_utilities/containers.py @@ -34,7 +34,6 @@ HashableT = typing.TypeVar("HashableT", bound=typing.Hashable) ValueT = typing.TypeVar("ValueT") - #: An immutable indexable container of elements with zero size. EMPTY_SEQUENCE: typing.Sequence = tuple() #: An immutable unordered container of elements with zero size. diff --git a/hikari/net/codes.py b/hikari/net/codes.py index 4caeaf686f..494359a3cc 100644 --- a/hikari/net/codes.py +++ b/hikari/net/codes.py @@ -23,6 +23,7 @@ import enum + # Doesnt work correctly with enums, so since this file is all enums, ignore # pylint: disable=no-member class HTTPStatusCode(enum.IntEnum): diff --git a/pre-commit b/pre-commit new file mode 100755 index 0000000000..0e00fd39ca --- /dev/null +++ b/pre-commit @@ -0,0 +1,5 @@ +#!/bin/sh + +# Put this in .git/hooks to make sure 'black' runs every time you commit. + +nox -sformat diff --git a/requirements.txt b/requirements.txt index 7278d34f39..095c1502ff 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,2 @@ aiohttp==3.6.2 attrs==19.3.0 -cattrs==1.0.0 diff --git a/tests/hikari/core/test_entities.py b/tests/hikari/core/test_snowflake.py similarity index 93% rename from tests/hikari/core/test_entities.py rename to tests/hikari/core/test_snowflake.py index 3b838a52ce..918e18cb98 100644 --- a/tests/hikari/core/test_entities.py +++ b/tests/hikari/core/test_snowflake.py @@ -20,7 +20,7 @@ import pytest -from hikari.core import entities +from hikari.core import snowflakes class TestSnowflake: @@ -30,7 +30,7 @@ def raw_id(self): @pytest.fixture() def neko_snowflake(self, raw_id): - return entities.Snowflake(raw_id) + return snowflakes.Snowflake(raw_id) def test_created_at(self, neko_snowflake): assert neko_snowflake.created_at == datetime.datetime(2019, 1, 22, 18, 41, 15, 283_000).replace( @@ -60,7 +60,7 @@ def test_repr_cast(self, neko_snowflake, raw_id): def test_eq(self, neko_snowflake, raw_id): assert neko_snowflake == raw_id - assert neko_snowflake == entities.Snowflake(raw_id) + assert neko_snowflake == snowflakes.Snowflake(raw_id) assert str(raw_id) != neko_snowflake def test_lt(self, neko_snowflake, raw_id): From 10608b20e1d0ff1d7831f3e5b0470938e4897abe Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Sun, 15 Mar 2020 16:59:53 +0000 Subject: [PATCH 009/922] Added marshaller logic that can use attr metadata to work out how to unmarshal things. This now removes any type hint usage. Benchmarks: 100,000 parses of a guild object (excluding parsing child models) was around 10.5s; averaging 90ns per guild parse. --- hikari/core/channels.py | 4 +- hikari/core/entities.py | 20 +- hikari/core/guilds.py | 179 +++++++++----- hikari/core/snowflakes.py | 8 +- hikari/core/users.py | 6 +- hikari/internal_utilities/marshaller.py | 233 +++++++++++++++++++ hikari/internal_utilities/transformations.py | 4 + requirements.txt | 1 + tests/hikari/core/test_guilds.py | 194 +++++++++++++++ 9 files changed, 568 insertions(+), 81 deletions(-) create mode 100644 hikari/internal_utilities/marshaller.py create mode 100644 tests/hikari/core/test_guilds.py diff --git a/hikari/core/channels.py b/hikari/core/channels.py index 7e19779007..b00f497501 100644 --- a/hikari/core/channels.py +++ b/hikari/core/channels.py @@ -28,11 +28,11 @@ class Channel(snowflakes.UniqueEntity): ... -@attr.s(slots=False) +@attr.s(slots=True) class DMChannel(Channel): ... -@attr.s(slots=False) +@attr.s(slots=True) class GroupDMChannel(DMChannel): ... diff --git a/hikari/core/entities.py b/hikari/core/entities.py index 3dd4e64c80..c06b9c614c 100644 --- a/hikari/core/entities.py +++ b/hikari/core/entities.py @@ -17,12 +17,12 @@ # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . """Datastructure bases.""" -__all__ = ["HikariEntity", "ISerializable", "IDeserializable", "RawEntityT"] +__all__ = ["HikariEntity", "Serializable", "Deserializable", "RawEntityT"] import abc import typing -import attr +from hikari.internal_utilities import marshaller RawEntityT = typing.Union[ None, bool, int, float, str, bytes, typing.Sequence[typing.Any], typing.Mapping[str, typing.Any] @@ -32,27 +32,29 @@ T_co = typing.TypeVar("T_co", covariant=True) -@attr.s(slots=True) +@marshaller.attrs(slots=True) class HikariEntity(metaclass=abc.ABCMeta): """The base for any entity used in this API.""" __slots__ = () -class IDeserializable(typing.Protocol): - """An interface for any type that allows deserialization from a raw value +class Deserializable: + """A mixin for any type that allows deserialization from a raw value into a Hikari entity. """ + __slots__ = () @classmethod def deserialize(cls: typing.Type[T_contra], payload: RawEntityT) -> T_contra: - ... + return marshaller.HIKARI_ENTITY_MARSHALLER.deserialize(payload, cls) -class ISerializable(typing.Protocol): - """An interface for any type that allows serialization from a Hikari entity +class Serializable: + """A mixin for any type that allows serialization from a Hikari entity into a raw value. """ + __slots__ = () def serialize(self: T_co) -> RawEntityT: - ... + return marshaller.HIKARI_ENTITY_MARSHALLER.serialize(self) diff --git a/hikari/core/guilds.py b/hikari/core/guilds.py index c66dd852f0..6a42ea5461 100644 --- a/hikari/core/guilds.py +++ b/hikari/core/guilds.py @@ -18,6 +18,8 @@ # along with Hikari. If not, see . """Components and entities that are used to describe guilds on Discord. """ +from __future__ import annotations + __all__ = [ "GuildEmoji", @@ -46,51 +48,60 @@ import enum import typing -import attr - from hikari.core import channels from hikari.core import entities from hikari.core import messages from hikari.core import permissions as permissions_ from hikari.core import snowflakes from hikari.core import users +from hikari.internal_utilities import dates +from hikari.internal_utilities import marshaller +from hikari.internal_utilities import transformations -@attr.s(slots=False) -class GuildEmoji(snowflakes.UniqueEntity, messages.Emoji): +@marshaller.attrs(slots=True) +class GuildEmoji(snowflakes.UniqueEntity, messages.Emoji, entities.Deserializable): ... -@attr.s(slots=False) -class GuildChannel(channels.Channel): +@marshaller.attrs(slots=True) +class GuildChannel(channels.Channel, entities.Deserializable): """The base for anything that is a guild channel.""" -@attr.s(slots=False) +@marshaller.attrs(slots=True) class GuildTextChannel(GuildChannel): ... -@attr.s(slots=False) +@marshaller.attrs(slots=True) class GuildVoiceChannel(GuildChannel): ... -@attr.s(slots=False) +@marshaller.attrs(slots=True) class GuildCategory(GuildChannel): ... -@attr.s(slots=False) +@marshaller.attrs(slots=True) class GuildStoreChannel(GuildChannel): ... -@attr.s(slots=False) +@marshaller.attrs(slots=True) class GuildNewsChannel(GuildChannel): ... +def parse_guild_channel(payload) -> GuildChannel: + class Duff: + id = snowflakes.Snowflake(123) + + # FIXME: implement properly + return Duff() + + class GuildExplicitContentFilterLevel(enum.IntEnum): """Represents the explicit content filter setting for a guild.""" @@ -205,33 +216,34 @@ class GuildVerificationLevel(enum.IntEnum): VERY_HIGH = 4 -@attr.s(slots=True, auto_attribs=True) -class GuildMember(snowflakes.UniqueEntity): - user: users.User +@marshaller.attrs(slots=True) +class GuildMember(entities.HikariEntity, entities.Deserializable): + user: users.User = marshaller.attrib(deserializer=users.User.deserialize) -@attr.s(slots=True, auto_attribs=True) +# Wait, so is Presence just an extension of Member? Should we subclass it? +@marshaller.attrs(slots=True) class GuildMemberPresence(entities.HikariEntity): - ... + user: users.User = marshaller.attrib(deserializer=users.User.deserialize) -@attr.s(slots=True) +@marshaller.attrs(slots=True) class GuildIntegration(snowflakes.UniqueEntity): ... -@attr.s(slots=True) +@marshaller.attrs(slots=True) class GuildMemberBan(entities.HikariEntity): ... -@attr.s(slots=True) -class GuildRole(entities.HikariEntity): +@marshaller.attrs(slots=True) +class GuildRole(snowflakes.UniqueEntity, entities.Deserializable): ... -@attr.s(slots=True, auto_attribs=True) -class Guild(snowflakes.UniqueEntity): +@marshaller.attrs(slots=True) +class Guild(snowflakes.UniqueEntity, entities.Deserializable): """A representation of a guild on Discord. Notes @@ -246,49 +258,55 @@ class Guild(snowflakes.UniqueEntity): #: The name of the guild. #: #: :type: :class:`str` - name: str + name: str = marshaller.attrib(deserializer=str) #: The hash for the guild icon, if there is one. #: #: :type: :class:`str`, optional - icon_hash: typing.Optional[str] + icon_hash: typing.Optional[str] = marshaller.attrib(raw_name="icon", optional=True, deserializer=str) #: The hash of the splash for the guild, if there is one. #: #: :type: :class:`str`, optional - splash_hash: typing.Optional[str] + splash_hash: typing.Optional[str] = marshaller.attrib(raw_name="splash", deserializer=str, optional=True) #: The hash of the discovery splash for the guild, if there is one. #: #: :type: :class:`str`, optional - discovery_splash: typing.Optional[str] + discovery_splash_hash: typing.Optional[str] = marshaller.attrib( + raw_name="discovery_splash", deserializer=str, optional=True + ) #: The ID of the owner of this guild. #: #: :type: :class:`snowflakes.Snowflake` - owner_id: snowflakes.Snowflake + owner_id: snowflakes.Snowflake = marshaller.attrib(deserializer=snowflakes.Snowflake) #: The guild level permissions that apply to the bot user. #: #: :type: :class:`hikari.core.permissions.Permission` - my_permissions: permissions_.Permission + my_permissions: permissions_.Permission = marshaller.attrib( + raw_name="permissions", deserializer=permissions_.Permission + ) #: The voice region for the guild. #: #: :type: :class:`str` - region: str + region: str = marshaller.attrib(deserializer=str) #: The ID for the channel that AFK voice users get sent to, if set for the #: guild. #: #: :type: :class:`snowflakes.Snowflake`, optional - afk_channel_id: typing.Optional[snowflakes.Snowflake] + afk_channel_id: typing.Optional[snowflakes.Snowflake] = marshaller.attrib(deserializer=str, optional=True) #: How long a voice user has to be AFK for before they are classed as being #: AFK and are moved to the AFK channel (:attr:`afk_channel_id`). #: #: :type: :class:`datetime.timedelta` - afk_timeout: datetime.timedelta + afk_timeout: datetime.timedelta = marshaller.attrib( + raw_name="afk_timeout", deserializer=lambda seconds: datetime.timedelta(seconds=seconds) + ) # TODO: document when this is not specified. # FIXME: do we need a field for this, or can we infer it from the `embed_channel_id`? @@ -296,86 +314,110 @@ class Guild(snowflakes.UniqueEntity): #: be present, in which case, it will be ``None`` instead. #: #: :type: :class:`bool`, optional - is_embed_enabled: typing.Optional[bool] + is_embed_enabled: typing.Optional[bool] = marshaller.attrib( + raw_name="embed_enabled", optional=True, deserializer=bool + ) #: The channel ID that the guild embed will generate an invite to, if #: enabled for this guild. If not enabled, it will be ``None``. #: #: :type: :class:`snowflakes.Snowflake`, optional - embed_channel_id: typing.Optional[snowflakes.Snowflake] + embed_channel_id: typing.Optional[snowflakes.Snowflake] = marshaller.attrib( + deserializer=snowflakes.Snowflake, optional=True + ) #: The verification level required for a user to participate in this guild. #: #: :type: :class:`GuildVerificationLevel` - verification_level: GuildVerificationLevel + verification_level: GuildVerificationLevel = marshaller.attrib(deserializer=GuildVerificationLevel) #: The default setting for message notifications in this guild. #: #: :type: :class:`GuildMessageNotificationsLevel` - default_message_notifications: GuildMessageNotificationsLevel + default_message_notifications: GuildMessageNotificationsLevel = marshaller.attrib( + deserializer=GuildMessageNotificationsLevel + ) #: The setting for the explicit content filter in this guild. #: #: :type: :class:`GuildExplicitContentFilterLevel` - explicit_content_filter: GuildExplicitContentFilterLevel + explicit_content_filter: GuildExplicitContentFilterLevel = marshaller.attrib( + deserializer=GuildExplicitContentFilterLevel + ) #: The roles in this guild, represented as a mapping of ID to role object. #: #: :type: :class:`typing.Mapping` [ :class:`snowflakes.Snowflake`, :class:`GuildRole` ] - roles: typing.Mapping[snowflakes.Snowflake, GuildRole] + roles: typing.Mapping[snowflakes.Snowflake, GuildRole] = marshaller.attrib( + deserializer=lambda roles: {r.id: r for r in map(GuildRole.deserialize, roles)}, + ) #: The emojis that this guild provides, represented as a mapping of ID to #: emoji object. #: #: :type: :class:`typing.Mapping` [ :class:`snowflakes.Snowflake`, :class:`GuildEmoji` ] - emojis: typing.Mapping[snowflakes.Snowflake, GuildEmoji] + emojis: typing.Mapping[snowflakes.Snowflake, GuildEmoji] = marshaller.attrib( + deserializer=lambda emojis: {e.id: e for e in map(GuildEmoji.deserialize, emojis)}, + ) #: A set of the features in this guild. #: #: :type: :class:`typing.Set` [ :class:`GuildFeature` ] - features: typing.Set[GuildFeature] + features: typing.Set[GuildFeature] = marshaller.attrib( + deserializer=lambda features: {transformations.try_cast(f, GuildFeature, f) for f in features}, + ) #: The required MFA level for users wishing to participate in this guild. #: #: :type: :class:`GuildMFALevel` - mfa_level: GuildMFALevel + mfa_level: GuildMFALevel = marshaller.attrib(deserializer=GuildMFALevel) #: The ID of the application that created this guild, if it was created by #: a bot. If not, this is always ``None``. #: #: :type: :class:`snowflakes.Snowflake`, optional - application_id: typing.Optional[snowflakes.Snowflake] + application_id: typing.Optional[snowflakes.Snowflake] = marshaller.attrib( + deserializer=snowflakes.Snowflake, optional=True + ) # TODO: document in which cases this information is not available. #: Describes whether the guild widget is enabled or not. If this information #: is not present, this will be ``None``. #: #: :type: :class:`bool`, optional - is_widget_enabled: typing.Optional[bool] + is_widget_enabled: typing.Optional[bool] = marshaller.attrib( + raw_name="widget_enabled", optional=True, deserializer=bool + ) #: The channel ID that the widget's generated invite will send the user to, #: if enabled. If this information is unavailable, this will be ``None``. #: #: :type: :class:`snowflakes.Snowflake`, optional - widget_channel_id: typing.Optional[snowflakes.Snowflake] + widget_channel_id: typing.Optional[snowflakes.Snowflake] = marshaller.attrib( + optional=True, deserializer=snowflakes.Snowflake + ) #: The ID of the system channel (where welcome messages and Nitro boost #: messages are sent), or ``None`` if it is not enabled. #: :type: :class:`snowflakes.Snowflake`, optional - system_channel_id: typing.Optional[snowflakes.Snowflake] + system_channel_id: typing.Optional[snowflakes.Snowflake] = marshaller.attrib( + optional=True, deserializer=snowflakes.Snowflake + ) #: Flags for the guild system channel to describe which notification #: features are suppressed. #: #: :type: :class:`GuildSystemChannelFlag` - system_channel_flags: GuildSystemChannelFlag + system_channel_flags: GuildSystemChannelFlag = marshaller.attrib(deserializer=GuildSystemChannelFlag) #: The ID of the channel where guilds with the :obj:`GuildFeature.PUBLIC` #: :attr:`features` display rules and guidelines. If the #: :obj:`GuildFeature.PUBLIC` feature is not defined, then this is ``None``. #: #: :type: :class:`snowflakes.Snowflake`, optional - rules_channel_id: typing.Optional[snowflakes.Snowflake] + rules_channel_id: typing.Optional[snowflakes.Snowflake] = marshaller.attrib( + optional=True, deserializer=snowflakes.Snowflake + ) #: The date and time that the bot user joined this guild. #: @@ -384,7 +426,9 @@ class Guild(snowflakes.UniqueEntity): #: this will always be ``None``. #: #: :type: :class:`datetime.datetime`, optional - joined_at: typing.Optional[datetime.datetime] + joined_at: typing.Optional[datetime.datetime] = marshaller.attrib( + raw_name="joined_at", deserializer=dates.parse_iso_8601_ts, + ) #: Whether the guild is considered to be large or not. #: @@ -396,7 +440,7 @@ class Guild(snowflakes.UniqueEntity): #: not be sent about members who are offline or invisible. #: #: :type: :class:`bool`, optional - is_large: typing.Optional[bool] + is_large: typing.Optional[bool] = marshaller.attrib(raw_name="large", optional=True, deserializer=bool) #: Whether the guild is unavailable or not. #: @@ -406,7 +450,7 @@ class Guild(snowflakes.UniqueEntity): #: #: An unavailable guild cannot be interacted with, and most information may #: be outdated or missing if that is the case. - is_unavailable: typing.Optional[bool] + is_unavailable: typing.Optional[bool] = marshaller.attrib(raw_name="unavailable", optional=True, deserializer=bool) #: The number of members in this guild. #: @@ -415,7 +459,7 @@ class Guild(snowflakes.UniqueEntity): #: this will always be ``None``. #: #: :type: :class:`int`, optional - member_count: typing.Optional[int] + member_count: typing.Optional[int] = marshaller.attrib(optional=True, deserializer=int) #: A mapping of ID to the corresponding guild members in this guild. #: @@ -435,7 +479,9 @@ class Guild(snowflakes.UniqueEntity): #: query the members using the appropriate API call instead. #: #: :type: :class:`typing.Mapping` [ :class:`snowflakes.Snowflake`, :class:`GuildMember` ], optional - members: typing.Optional[typing.Mapping[snowflakes.Snowflake, GuildMember]] + members: typing.Optional[typing.Mapping[snowflakes.Snowflake, GuildMember]] = marshaller.attrib( + deserializer=lambda members: {m.user.id: m for m in map(GuildMember.deserialize, members)}, optional=True, + ) #: A mapping of ID to the corresponding guild channels in this guild. #: @@ -453,7 +499,9 @@ class Guild(snowflakes.UniqueEntity): #: appropriate API call to retrieve this information. #: #: :type: :class:`typing.Mapping` [ :class:`snowflakes.Snowflake`, :class:`GuildChannel` ], optional - channels: typing.Optional[typing.Mapping[snowflakes.Snowflake, GuildChannel]] + channels: typing.Optional[typing.Mapping[snowflakes.Snowflake, GuildChannel]] = marshaller.attrib( + deserializer=lambda guild_channels: {c.id: c for c in map(parse_guild_channel, guild_channels)}, optional=True, + ) #: A mapping of member ID to the corresponding presence information for #: the given member, if available. @@ -472,27 +520,30 @@ class Guild(snowflakes.UniqueEntity): #: appropriate API call to retrieve this information. #: #: :type: :class:`typing.Mapping` [ :class:`snowflakes.Snowflake`, :class:`GuildMemberPresence` ], optional - presences: typing.Optional[typing.Mapping[snowflakes.Snowflake, GuildMemberPresence]] + presences: typing.Optional[typing.Mapping[snowflakes.Snowflake, GuildMemberPresence]] = marshaller.attrib( + deserializer=lambda presences: {p.user.id: p for p in map(GuildMemberPresence.deserialize, presences)}, + optional=True, + ) #: The maximum number of presences for the guild. If this is ``None``, then #: the default value is used (currently 5000). #: #: :type: :class:`int`, optional - max_presences: typing.Optional[int] + max_presences: typing.Optional[int] = marshaller.attrib(optional=True, deserializer=int) #: The maximum number of members allowed in this guild. #: #: This information may not be present, in which case, it will be ``None``. #: #: :type: :class:`int`, optional - max_members: typing.Optional[int] + max_members: typing.Optional[int] = marshaller.attrib(optional=True, deserializer=int) #: The vanity URL code for the guild's vanity URL. #: This is only present if :obj:`GuildFeatures.VANITY_URL` is in the #: :attr:`features` for this guild. If not, this will always be ``None``. #: #: :type: :class:`str`, optional - vanity_url_code: typing.Optional[str] + vanity_url_code: typing.Optional[str] = marshaller.attrib(optional=True, deserializer=str) #: The guild's description. #: @@ -501,25 +552,25 @@ class Guild(snowflakes.UniqueEntity): #: ``None``. #: #: :type: :class:`str`, optional - description: typing.Optional[str] + description: typing.Optional[str] = marshaller.attrib(optional=True, deserializer=str) #: The hash for the guild's banner. #: This is only present if the guild has :obj:`GuildFeatures.BANNER` in the #: :attr:`features` for this guild. For all other purposes, it is ``None``. #: #: :type: :class:`str`, optional - banner_hash: typing.Optional[str] + banner_hash: typing.Optional[str] = marshaller.attrib(raw_name="banner", optional=True, deserializer=str) #: The premium tier for this guild. #: #: :type: :class:`GuildPremiumTier` - premium_tier: GuildPremiumTier + premium_tier: GuildPremiumTier = marshaller.attrib(deserializer=GuildPremiumTier) #: The number of nitro boosts that the server currently has. This #: information may not be present, in which case, it will be ``None``. #: #: :type: :class:`int`, optional - premium_subscription_count: typing.Optional[int] + premium_subscription_count: typing.Optional[int] = marshaller.attrib(optional=True, deserializer=int) #: The preferred locale to use for this guild. #: @@ -528,7 +579,7 @@ class Guild(snowflakes.UniqueEntity): #: considered to be ``None`` until more clarification is given by Discord. #: #: :type: :class:`str`, optional - preferred_locale: typing.Optional[str] + preferred_locale: typing.Optional[str] = marshaller.attrib(optional=True, deserializer=str) #: The channel ID of the channel where admins and moderators receive notices #: from Discord. @@ -538,6 +589,6 @@ class Guild(snowflakes.UniqueEntity): #: considered to be ``None``. #: #: :type: :class:`snowflakes.Snowflake`, optional - public_updates_channel_id: typing.Optional[snowflakes.Snowflake] - - # TODO: voice states + public_updates_channel_id: typing.Optional[snowflakes.Snowflake] = marshaller.attrib( + optional=True, deserializer=snowflakes.Snowflake + ) diff --git a/hikari/core/snowflakes.py b/hikari/core/snowflakes.py index 879e765bd2..4268d6ab3b 100644 --- a/hikari/core/snowflakes.py +++ b/hikari/core/snowflakes.py @@ -29,8 +29,8 @@ import attr from hikari.core import entities -from hikari.core.entities import HikariEntity from hikari.internal_utilities import dates +from hikari.internal_utilities import marshaller @functools.total_ordering @@ -99,11 +99,11 @@ def deserialize(cls, value: str) -> "Snowflake": return cls(value) -@attr.s(slots=True) -class UniqueEntity(HikariEntity): +@marshaller.attrs(slots=True) +class UniqueEntity(entities.HikariEntity): """An entity that has an integer ID of some sort.""" #: The ID of this entity. #: #: :type: :class:`Snowflake` - id: Snowflake = attr.ib(hash=True, eq=True, repr=True) + id: Snowflake = marshaller.attrib(hash=True, eq=True, repr=True, deserializer=Snowflake, serializer=str) diff --git a/hikari/core/users.py b/hikari/core/users.py index 249a42bff2..7e2e10854a 100644 --- a/hikari/core/users.py +++ b/hikari/core/users.py @@ -20,11 +20,13 @@ import attr +from hikari.core import entities from hikari.core import snowflakes +from hikari.internal_utilities import marshaller -@attr.s(slots=True) -class User(snowflakes.UniqueEntity): +@marshaller.attrs() +class User(snowflakes.UniqueEntity, entities.Deserializable): ... diff --git a/hikari/internal_utilities/marshaller.py b/hikari/internal_utilities/marshaller.py new file mode 100644 index 0000000000..31b9f6d1d5 --- /dev/null +++ b/hikari/internal_utilities/marshaller.py @@ -0,0 +1,233 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# Copyright © Nekoka.tt 2019-2020 +# +# This file is part of Hikari. +# +# Hikari is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Hikari is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Hikari. If not, see . +""" +This is an internal marshalling utility used by internal API components. + +Warnings +-------- +You should not change anything in this file, if you do, you will likely get +unexpected behaviour elsewhere. +""" + +import typing + +import attr + +from hikari.internal_utilities import assertions + +_RAW_NAME_ATTR = __name__ + "_RAW_NAME" +_SERIALIZER_ATTR = __name__ + "_SERIALIZER" +_DESERIALIZER_ATTR = __name__ + "_DESERIALIZER" +_TRANSIENT_ATTR = __name__ + "_TRANSIENT" +_OPTIONAL_ATTR = __name__ + "_OPTIONAL" + +MARSHALLER_META_ATTR = "__hikari_marshaller_meta_attr__" + +T_contra = typing.TypeVar("T_contra", contravariant=True) + + +def attrib( + *, + # Mandatory! We do not want to rely on type annotations alone, as they will + # break if we use __future__.annotations anywhere. If we relied on the + # field type, that would work, but attrs doesn't let us supply field.type + # as an attr.ib() kwargs AND use type hints at the same time, and without + # type hints, the library loses the ability to be type checked properly + # anymore, so we have to pass this explicitly regardless. + deserializer: typing.Callable[[typing.Any], typing.Any], + raw_name: typing.Optional[str] = None, + transient: bool = False, + optional: bool = False, + serializer: typing.Callable[[typing.Any], typing.Any] = None, + **kwargs, +): + metadata = kwargs.setdefault("metadata", {}) + metadata[_RAW_NAME_ATTR] = raw_name + metadata[_SERIALIZER_ATTR] = serializer + metadata[_DESERIALIZER_ATTR] = deserializer + metadata[_TRANSIENT_ATTR] = transient + metadata[_OPTIONAL_ATTR] = optional + return attr.ib(**kwargs) + + +def _no_deserialize(name): + def error(*_, **__) -> typing.NoReturn: + raise TypeError(f"Field {name} does not support deserialization") + + return error + + +def _no_serialize(name): + def error(*_, **__) -> typing.NoReturn: + raise TypeError(f"Field {name} does not support serialization") + + return error + + +class _AttributeDescriptor: + __slots__ = ("raw_name", "field_name", "is_optional", "is_transient", "deserializer", "serializer") + + def __init__( + self, + raw_name: str, + field_name: str, + is_optional: bool, + is_transient: bool, + deserializer: typing.Callable[[typing.Any], typing.Any], + serializer: typing.Callable[[typing.Any], typing.Any], + ) -> None: + self.raw_name = raw_name + self.field_name = field_name + self.is_optional = is_optional + self.is_transient = is_transient # Do not serialize + self.deserializer = deserializer + self.serializer = serializer + + +class _EntityDescriptor: + __slots__ = ("entity_type", "attribs") + + def __init__(self, entity_type: typing.Type, attribs: typing.Collection[_AttributeDescriptor],) -> None: + self.entity_type = entity_type + self.attribs = tuple(attribs) + + +def _construct_attribute_descriptor(field: attr.Attribute) -> _AttributeDescriptor: + raw_name = typing.cast(str, field.metadata.get(_RAW_NAME_ATTR) or field.name) + field_name = typing.cast(str, field.name) + + return _AttributeDescriptor( + raw_name=raw_name, + field_name=field_name, + is_optional=field.metadata[_OPTIONAL_ATTR], + is_transient=field.metadata[_TRANSIENT_ATTR], + deserializer=field.metadata[_DESERIALIZER_ATTR], + serializer=field.metadata[_SERIALIZER_ATTR] or _no_serialize(field_name), + ) + + +def _construct_entity_descriptor(entity: typing.Any): + assertions.assert_that( + hasattr(entity, "__attrs_attrs__"), + f"{entity.__module__}.{entity.__qualname__} is not an attr class", + error_type=TypeError, + ) + + return _EntityDescriptor(entity, [_construct_attribute_descriptor(field) for field in attr.fields(entity)]) + + +class HikariEntityMarshaller: + """ + This is a global marshaller helper that can help to deserialize and + serialize any internal components that are decorated with the + :obj:`marshaller_aware` decorator, and that are :mod:`attr` classes. + """ + + def __init__(self): + self._registered_entities: typing.MutableMapping[typing.Type, _EntityDescriptor] = {} + + def register(self, cls): + entity_descriptor = _construct_entity_descriptor(cls) + self._registered_entities[cls] = entity_descriptor + return cls + + def deserialize(self, raw_data: typing.Mapping[str, typing.Any], target_type: typing.Type[T_contra]) -> T_contra: + """Deserialize a given raw data item into the target type. + + Parameters + ---------- + raw_data + The raw data to deserialize. + target_type + The type to deserialize to. + + Returns + ------- + The deserialized instance. + """ + try: + descriptor = self._registered_entities[target_type] + except KeyError: + raise TypeError(f"No registered entity {target_type.__module__}.{target_type.__qualname__}") + + kwargs = {} + + for a in descriptor.attribs: + if a.raw_name not in raw_data: + if not a.is_optional: + raise ValueError( + f"Non-optional field {a.field_name} (from raw {a.raw_name}) is not specified in the input " + f"payload\n\n{raw_data}" + ) + kwargs[a.field_name] = None + continue + + try: + data = raw_data[a.raw_name] + # Use the deserializer if it is there, otherwise use the constructor of the type of the field. + kwargs[a.field_name] = a.deserializer(data) if a.deserializer else data + except Exception: + raise ValueError( + "Failed to deserialize data to instance of " + f"{target_type.__module__}.{target_type.__qualname__} because marshalling failed on " + f"attribute {a.field_name}" + ) + + return target_type(**kwargs) + + def serialize(self, obj: typing.Optional[typing.Any]) -> typing.Optional[typing.Dict[str, typing.Any]]: + """Serialize a given entity into a raw data item. + + Parameters + ---------- + obj + The entity to serialize. + + Returns + ------- + The serialized raw data item. + """ + if obj is None: + return None + + input_type = type(obj) + + try: + descriptor = self._registered_entities[input_type] + except KeyError: + raise TypeError(f"No registered entity {input_type.__module__}.{input_type.__qualname__}") + + raw_data = {} + + for a in descriptor.attribs: + if a.is_transient: + continue + value = getattr(obj, a.field_name) + raw_data[a.raw_name] = a.serializer(value) or repr(value) + + return raw_data + + +HIKARI_ENTITY_MARSHALLER = HikariEntityMarshaller() + + +def attrs(**kwargs): + assertions.assert_that(not kwargs.get("auto_attribs"), "Cannot use auto attribs here") + kwargs["auto_attribs"] = False + return lambda cls: kwargs.pop("marshaller", HIKARI_ENTITY_MARSHALLER).register(attr.s(**kwargs)(cls)) diff --git a/hikari/internal_utilities/transformations.py b/hikari/internal_utilities/transformations.py index e2a67c2bdf..35078a33fd 100644 --- a/hikari/internal_utilities/transformations.py +++ b/hikari/internal_utilities/transformations.py @@ -123,3 +123,7 @@ def image_bytes_to_image_data(img_bytes: bytes) -> typing.Optional[str]: image_data = base64.b64encode(img_bytes).decode() return f"data:{img_type};base64,{image_data}" + + +def try_cast_or_defer_unary_operator(type_): + return lambda data: try_cast(data, type_, data) diff --git a/requirements.txt b/requirements.txt index 095c1502ff..d83fb8060d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,3 @@ aiohttp==3.6.2 attrs==19.3.0 +typing_inspect==0.5.0 diff --git a/tests/hikari/core/test_guilds.py b/tests/hikari/core/test_guilds.py new file mode 100644 index 0000000000..e0a437273e --- /dev/null +++ b/tests/hikari/core/test_guilds.py @@ -0,0 +1,194 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# Copyright © Nekoka.tt 2019-2020 +# +# This file is part of Hikari. +# +# Hikari is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Hikari is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along ith Hikari. If not, see . +from __future__ import annotations + +from timeit import timeit + +from hikari.core import guilds + +import pytest + + +@pytest.fixture +def test_emoji_payload(): + return { + "id": "41771983429993937", + "name": "LUL", + "roles": ["41771983429993000", "41771983429993111"], + "user": { + "username": "Luigi", + "discriminator": "0002", + "id": "96008815106887111", + "avatar": "5500909a3274e1812beb4e8de6631111", + }, + "require_colons": True, + "managed": False, + "animated": False, + } + + +@pytest.fixture +def test_roles_payloads(): + return [ + { + "id": "41771983423143936", + "name": "WE DEM BOYZZ!!!!!!", + "color": 3_447_003, + "hoist": True, + "position": 0, + "permissions": 66_321_471, + "managed": False, + "mentionable": False, + }, + { + "id": "1111223", + "name": "some unfunny pun here", + "color": 0xFF00FF, + "hoist": False, + "position": 1, + "permissions": 1, + "managed": False, + "mentionable": True, + }, + ] + + +@pytest.fixture +def test_channel_payloads(): + return [ + { + "type": 0, + "id": "1234567", + "guild_id": "696969", + "position": 100, + "permission_overwrites": [], + "nsfw": True, + "parent_id": None, + "rate_limit_per_user": 420, + "topic": "nsfw stuff", + "name": "shh!", + }, + { + "type": 4, + "id": "123456", + "guild_id": "54321", + "position": 69, + "permission_overwrites": [], + "name": "dank category", + }, + { + "type": 2, + "id": "9292929", + "guild_id": "929", + "position": 66, + "permission_overwrites": [], + "name": "roy rodgers mc freely", + "bitrate": 999, + "user_limit": 0, + "parent_id": "42", + }, + ] + + +@pytest.fixture +def test_member_payload(): + return { + "nick": "foobarbaz", + "roles": ["11111", "22222", "33333", "44444"], + "joined_at": "2015-04-26T06:26:56.936000+00:00", + "premium_since": "2019-05-17T06:26:56.936000+00:00", + # These should be completely ignored. + "deaf": False, + "mute": True, + "user": { + "id": "123456", + "username": "Boris Johnson", + "discriminator": "6969", + "avatar": "1a2b3c4d", + "mfa_enabled": True, + "locale": "gb", + "flags": 0b00101101, + "premium_type": 0b1101101, + }, + } + + +@pytest.fixture +def test_voice_state_payload(): + return { + "channel_id": "432123321", + "user_id": "6543453", + "session_id": "350a109226bd6f43c81f12c7c08de20a", + "deaf": False, + "mute": True, + "self_deaf": True, + "self_mute": False, + "self_stream": True, + "suppress": False, + } + + +@pytest.fixture +def test_guild_payload( + test_emoji_payload, test_roles_payloads, test_channel_payloads, test_member_payload, test_voice_state_payload +): + return { + "id": "265828729970753537", + "afk_channel_id": "99998888777766", + "owner_id": "6969696", + "region": "eu-central", + "system_channel_id": "19216801", + "application_id": "10987654321", + "name": "L33t guild", + "icon": "1a2b3c4d", + "splash": "0ff0ff0ff", + "afk_timeout": 1200, + "verification_level": 4, + "default_message_notifications": 1, + "explicit_content_filter": 2, + "roles": test_roles_payloads, + "emojis": [test_emoji_payload], + "features": ["ANIMATED_ICON", "MORE_EMOJI", "NEWS", "SOME_UNDOCUMENTED_FEATURE"], + "voice_states": [test_voice_state_payload], + "member_count": 14, + "mfa_level": 1, + "joined_at": "2019-05-17T06:26:56.936000+00:00", + "large": False, + "unavailable": False, + "permissions": 66_321_471, + "members": [test_member_payload], + "channels": test_channel_payloads, + "max_members": 25000, + "vanity_url_code": "loool", + "description": "This is a server I guess, its a bit crap though", + "banner": "1a2b3c", + "premium_tier": 2, + "premium_subscription_count": 1, + "preferred_locale": "en-GB", + "system_channel_flags": 3, + "rules_channel_id": "42042069", + } + + +class TestGuildMarshalling: + def test_deserialize_guild(self, test_guild_payload): + # TODO: this + scope = {**globals()} + scope.update(locals()) + print(timeit("guilds.Guild.deserialize(test_guild_payload)", globals=scope, number=100_000) / 100_000) From 22f23b8571c5366621596c0f2409c3a4eec0363b Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Sun, 15 Mar 2020 17:17:12 +0000 Subject: [PATCH 010/922] PR feedback and fixes to LGTM complaints --- hikari/core/__init__.py | 44 +++---- hikari/core/entities.py | 2 + hikari/core/guilds.py | 3 - hikari/core/permissions.py | 3 +- hikari/core/snowflakes.py | 2 - hikari/internal_utilities/marshaller.py | 157 +++++++++++++++++++----- 6 files changed, 153 insertions(+), 58 deletions(-) diff --git a/hikari/core/__init__.py b/hikari/core/__init__.py index 1499b168c0..fc10b8f708 100644 --- a/hikari/core/__init__.py +++ b/hikari/core/__init__.py @@ -17,26 +17,26 @@ # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . """The core API for interacting with Discord directly.""" -from . import channels -from . import entities -from . import events -from . import gateway_bot -from . import guilds -from . import invites -from . import messages -from . import oauth2 -from . import permissions -from . import snowflakes -from . import users +from hikari.core import channels +from hikari.core import entities +from hikari.core import events +from hikari.core import gateway_bot +from hikari.core import guilds +from hikari.core import invites +from hikari.core import messages +from hikari.core import oauth2 +from hikari.core import permissions +from hikari.core import snowflakes +from hikari.core import users -from .channels import * -from .entities import * -from .events import * -from .gateway_bot import * -from .guilds import * -from .invites import * -from .messages import * -from .oauth2 import * -from .permissions import * -from .snowflakes import * -from .users import * +from hikari.core.channels import * +from hikari.core.entities import * +from hikari.core.events import * +from hikari.core.gateway_bot import * +from hikari.core.guilds import * +from hikari.core.invites import * +from hikari.core.messages import * +from hikari.core.oauth2 import * +from hikari.core.permissions import * +from hikari.core.snowflakes import * +from hikari.core.users import * diff --git a/hikari/core/entities.py b/hikari/core/entities.py index c06b9c614c..78f26dfa52 100644 --- a/hikari/core/entities.py +++ b/hikari/core/entities.py @@ -43,6 +43,7 @@ class Deserializable: """A mixin for any type that allows deserialization from a raw value into a Hikari entity. """ + __slots__ = () @classmethod @@ -54,6 +55,7 @@ class Serializable: """A mixin for any type that allows serialization from a Hikari entity into a raw value. """ + __slots__ = () def serialize(self: T_co) -> RawEntityT: diff --git a/hikari/core/guilds.py b/hikari/core/guilds.py index 6a42ea5461..5a1e124d02 100644 --- a/hikari/core/guilds.py +++ b/hikari/core/guilds.py @@ -18,9 +18,6 @@ # along with Hikari. If not, see . """Components and entities that are used to describe guilds on Discord. """ -from __future__ import annotations - - __all__ = [ "GuildEmoji", "GuildChannel", diff --git a/hikari/core/permissions.py b/hikari/core/permissions.py index c245a0dd6e..111c75a8a1 100644 --- a/hikari/core/permissions.py +++ b/hikari/core/permissions.py @@ -24,8 +24,7 @@ class Permission(enum.IntFlag): - """ - Represents the permissions available in a given channel or guild. + """Represents the permissions available in a given channel or guild. This is an int-flag enum. This means that you can **combine multiple permissions together** into one value using the bitwise-OR "`|`" operator. diff --git a/hikari/core/snowflakes.py b/hikari/core/snowflakes.py index 4268d6ab3b..433d2e2128 100644 --- a/hikari/core/snowflakes.py +++ b/hikari/core/snowflakes.py @@ -26,8 +26,6 @@ import functools import typing -import attr - from hikari.core import entities from hikari.internal_utilities import dates from hikari.internal_utilities import marshaller diff --git a/hikari/internal_utilities/marshaller.py b/hikari/internal_utilities/marshaller.py index 31b9f6d1d5..13a31a1843 100644 --- a/hikari/internal_utilities/marshaller.py +++ b/hikari/internal_utilities/marshaller.py @@ -16,8 +16,7 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . -""" -This is an internal marshalling utility used by internal API components. +"""This is an internal marshalling utility used by internal API components. Warnings -------- @@ -39,7 +38,7 @@ MARSHALLER_META_ATTR = "__hikari_marshaller_meta_attr__" -T_contra = typing.TypeVar("T_contra", contravariant=True) +EntityT = typing.TypeVar("EntityT", contravariant=True) def attrib( @@ -54,9 +53,38 @@ def attrib( raw_name: typing.Optional[str] = None, transient: bool = False, optional: bool = False, - serializer: typing.Callable[[typing.Any], typing.Any] = None, + serializer: typing.Optional[typing.Callable[[typing.Any], typing.Any]] = None, **kwargs, -): +) -> typing.Any: + """Create an :func:`attr.ib` with marshaller metadata attached. + + Parameters + ---------- + deserializer : :obj:`typing.Callable` [ [ :obj:`typing.Any` ], :obj:`typing.Any` ] + The deserializer to use to deserialize raw elements. + raw_name : :obj:`str`, optional + The raw name of the element in its raw serialized form. If not provided, + then this will use the field's default name later. + transient : :obj:`bool` + If True, the field is marked as transient, meaning it will not be + serialized. Defaults to False. + optional : :obj:`bool` + If True, the field is marked as being allowed to be `None`. If a field + is not optional, and a `None` value is passed to it later on, then + an exception will be raised. + serializer : :obj:`typing.Callable` [ [ :obj:`typing.Any` ], :obj:`typing.Any` ], optional + The serializer to use. If not specified, then serializing the entire + class that this attribute is in will trigger a :class:`TypeError` + later. + **kwargs : + Any kwargs to pass to :func:`attr.ib`. + + Returns + ------- + :obj:`typing.Any` + The result of :func:`attr.ib` internally being called with additional + metadata. + """ metadata = kwargs.setdefault("metadata", {}) metadata[_RAW_NAME_ATTR] = raw_name metadata[_SERIALIZER_ATTR] = serializer @@ -66,13 +94,6 @@ def attrib( return attr.ib(**kwargs) -def _no_deserialize(name): - def error(*_, **__) -> typing.NoReturn: - raise TypeError(f"Field {name} does not support deserialization") - - return error - - def _no_serialize(name): def error(*_, **__) -> typing.NoReturn: raise TypeError(f"Field {name} does not support serialization") @@ -133,45 +154,74 @@ def _construct_entity_descriptor(entity: typing.Any): class HikariEntityMarshaller: - """ - This is a global marshaller helper that can help to deserialize and + """This is a global marshaller helper that can help to deserialize and serialize any internal components that are decorated with the - :obj:`marshaller_aware` decorator, and that are :mod:`attr` classes. + :obj:`attrs` decorator, and that are :mod:`attr` classes using fields + with the :obj:`attrib` function call descriptor. """ - def __init__(self): + def __init__(self) -> None: self._registered_entities: typing.MutableMapping[typing.Type, _EntityDescriptor] = {} - def register(self, cls): + def register(self, cls: typing.Type[EntityT]) -> typing.Type[EntityT]: + """Registers an attrs type for fast future deserialization. + + Parameters + ---------- + cls : :obj:`typing.Type` [ :obj:`typing.Any` ] + The type to register. + + Returns + ------- + :obj:`typing.Type` [ :obj:`typing.Any` ] + The input argument. This enables this to be used as a decorator if + desired. + + Raises + ------ + :obj:`TypeError` + If the class is not an :mod:`attrs` class. + """ entity_descriptor = _construct_entity_descriptor(cls) self._registered_entities[cls] = entity_descriptor return cls - def deserialize(self, raw_data: typing.Mapping[str, typing.Any], target_type: typing.Type[T_contra]) -> T_contra: + def deserialize(self, raw_data: typing.Mapping[str, typing.Any], target_type: typing.Type[EntityT]) -> EntityT: """Deserialize a given raw data item into the target type. Parameters ---------- - raw_data + raw_data : :obj:`typing.Mapping` [ :obj:`str`, :obj:`typing.Any` ] The raw data to deserialize. - target_type + target_type : :obj:`typing.Type` [ :obj:`typing.Any` ] The type to deserialize to. Returns ------- - The deserialized instance. + :obj:`typing.Any` + The deserialized instance. + + Raises + ------ + :obj:`LookupError` + If the entity is not registered. + :obj:`AttributeError` + If the field is not optional, but the field was not present in the + raw payload, or it was present, but it was assigned `None`. + :obj:`TypeError` + If the deserialization call failed for some reason. """ try: descriptor = self._registered_entities[target_type] except KeyError: - raise TypeError(f"No registered entity {target_type.__module__}.{target_type.__qualname__}") + raise LookupError(f"No registered entity {target_type.__module__}.{target_type.__qualname__}") kwargs = {} for a in descriptor.attribs: - if a.raw_name not in raw_data: + if a.raw_name not in raw_data or raw_data[a.raw_name] is None: if not a.is_optional: - raise ValueError( + raise AttributeError( f"Non-optional field {a.field_name} (from raw {a.raw_name}) is not specified in the input " f"payload\n\n{raw_data}" ) @@ -183,7 +233,7 @@ def deserialize(self, raw_data: typing.Mapping[str, typing.Any], target_type: ty # Use the deserializer if it is there, otherwise use the constructor of the type of the field. kwargs[a.field_name] = a.deserializer(data) if a.deserializer else data except Exception: - raise ValueError( + raise TypeError( "Failed to deserialize data to instance of " f"{target_type.__module__}.{target_type.__qualname__} because marshalling failed on " f"attribute {a.field_name}" @@ -191,17 +241,23 @@ def deserialize(self, raw_data: typing.Mapping[str, typing.Any], target_type: ty return target_type(**kwargs) - def serialize(self, obj: typing.Optional[typing.Any]) -> typing.Optional[typing.Dict[str, typing.Any]]: + def serialize(self, obj: typing.Optional[typing.Any]) -> typing.Optional[typing.Mapping[str, typing.Any]]: """Serialize a given entity into a raw data item. Parameters ---------- - obj + obj : :class:`typing.Any`, optional The entity to serialize. Returns ------- - The serialized raw data item. + :class:`typing.Mapping` [ :class:`str`, :class:`typing.Any` ], optional + The serialized raw data item. + + Raises + ------ + :class:`LookupError` + If the entity is not registered. """ if obj is None: return None @@ -211,7 +267,7 @@ def serialize(self, obj: typing.Optional[typing.Any]) -> typing.Optional[typing. try: descriptor = self._registered_entities[input_type] except KeyError: - raise TypeError(f"No registered entity {input_type.__module__}.{input_type.__qualname__}") + raise LookupError(f"No registered entity {input_type.__module__}.{input_type.__qualname__}") raw_data = {} @@ -228,6 +284,49 @@ def serialize(self, obj: typing.Optional[typing.Any]) -> typing.Optional[typing. def attrs(**kwargs): + """Creates a decorator for a class to make it into an :mod:`attrs` class. + + This decorator will register the + + Parameters + ---------- + **kwargs : + Any kwargs to pass to :func:`attr.s`. + + Other Parameters + ---------------- + auto_attribs : :obj:`bool` + This must always be ``False`` if specified, or a :class:`ValueError` + will be raised, as this feature is not compatible with this marshaller + implementation. If not specified, it will default to ``False``. + marshaller : :obj:`HikariEntityMarshaller` + If specified, this should be an instance of a marshaller to use. For + most internal purposes, you want to not specify this, since it will + then default to the hikari-global marshaller instead. This is useful, + however, for testing and for external usage. + + Returns + ------- + A decorator to decorate a class with. + + Raises + ------ + :class:`ValueError` + If you attempt to use the `auto_attribs` feature provided by + :mod:`attr`. + + Example + ------- + + .. code-block:: python + + @attrs() + class MyEntity: + id: int = attrib(deserializer=int, serializer=str) + password: str = attrib(deserializer=int, transient=True) + ... + + """ assertions.assert_that(not kwargs.get("auto_attribs"), "Cannot use auto attribs here") kwargs["auto_attribs"] = False return lambda cls: kwargs.pop("marshaller", HIKARI_ENTITY_MARSHALLER).register(attr.s(**kwargs)(cls)) From 7dd46a842d9c0f1a05cc2d863cc4f00ed66ff0fb Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Sun, 15 Mar 2020 19:16:51 +0000 Subject: [PATCH 011/922] Added tests for marshaller --- tests/hikari/__init__.py | 1 - tests/hikari/core/test_guilds.py | 10 - .../internal_utilities/test_marshaller.py | 172 +++++++++++++++++ .../test_marshaller_pep563.py | 179 ++++++++++++++++++ 4 files changed, 351 insertions(+), 11 deletions(-) create mode 100644 tests/hikari/internal_utilities/test_marshaller.py create mode 100644 tests/hikari/internal_utilities/test_marshaller_pep563.py diff --git a/tests/hikari/__init__.py b/tests/hikari/__init__.py index 5ae423e749..c9c74ac9bb 100644 --- a/tests/hikari/__init__.py +++ b/tests/hikari/__init__.py @@ -18,7 +18,6 @@ # along with Hikari. If not, see . import asyncio - _real_new_event_loop = asyncio.new_event_loop diff --git a/tests/hikari/core/test_guilds.py b/tests/hikari/core/test_guilds.py index e0a437273e..25d804239f 100644 --- a/tests/hikari/core/test_guilds.py +++ b/tests/hikari/core/test_guilds.py @@ -20,8 +20,6 @@ from timeit import timeit -from hikari.core import guilds - import pytest @@ -184,11 +182,3 @@ def test_guild_payload( "system_channel_flags": 3, "rules_channel_id": "42042069", } - - -class TestGuildMarshalling: - def test_deserialize_guild(self, test_guild_payload): - # TODO: this - scope = {**globals()} - scope.update(locals()) - print(timeit("guilds.Guild.deserialize(test_guild_payload)", globals=scope, number=100_000) / 100_000) diff --git a/tests/hikari/internal_utilities/test_marshaller.py b/tests/hikari/internal_utilities/test_marshaller.py new file mode 100644 index 0000000000..b8c03f1e40 --- /dev/null +++ b/tests/hikari/internal_utilities/test_marshaller.py @@ -0,0 +1,172 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# Copyright © Nekoka.tt 2019-2020 +# +# This file is part of Hikari. +# +# Hikari is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Hikari is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Hikari. If not, see . +import cymock as mock +import pytest + +from hikari.internal_utilities import marshaller +from tests.hikari import _helpers + + +class TestAttrib: + def test_invokes_attrs(self): + deserializer = lambda _: _ + serializer = lambda _: _ + + with mock.patch("attr.ib") as attrib: + marshaller.attrib( + deserializer=deserializer, + raw_name="foo", + transient=False, + optional=True, + serializer=serializer, + foo=12, + bar="hello, world", + ) + + attrib.assert_called_once_with( + foo=12, + bar="hello, world", + metadata={ + marshaller._RAW_NAME_ATTR: "foo", + marshaller._SERIALIZER_ATTR: serializer, + marshaller._DESERIALIZER_ATTR: deserializer, + marshaller._TRANSIENT_ATTR: False, + marshaller._OPTIONAL_ATTR: True, + }, + ) + + +class TestAttrs: + def test_invokes_attrs(self): + marshaller_mock = mock.create_autospec(marshaller.HikariEntityMarshaller, spec_set=True) + + kwargs = {"foo": 9, "bar": "lol", "marshaller": marshaller_mock} + + marshaller_mock.register = mock.MagicMock(wraps=lambda c: c) + + with mock.patch("attr.s", return_value=lambda c: c) as attrs: + + @marshaller.attrs(**kwargs) + class Foo: + bar = 69 + + assert Foo is not None + assert Foo.bar == 69 + + attrs.assert_called_once_with(foo=9, bar="lol", auto_attribs=False) + marshaller_mock.register.assert_called_once_with(Foo) + + +class TestMarshaller: + @pytest.fixture() + def marshaller_impl(self): + return marshaller.HikariEntityMarshaller() + + def test_deserialize(self, marshaller_impl): + deserialized_id = mock.MagicMock() + id_deserializer = mock.MagicMock(return_value=deserialized_id) + + @marshaller.attrs(marshaller=marshaller_impl) + class User: + id: int = marshaller.attrib(deserializer=id_deserializer) + some_list: list = marshaller.attrib(deserializer=lambda items: [str(i) for i in items]) + + result = marshaller_impl.deserialize({"id": "12345", "some_list": [True, False, "foo", 12, 3.4]}, User) + + assert isinstance(result, User) + assert result.id == deserialized_id + assert result.some_list == ["True", "False", "foo", "12", "3.4"] + + def test_deserialize_optional_success_if_specified(self, marshaller_impl): + @marshaller.attrs(marshaller=marshaller_impl) + class User: + id: int = marshaller.attrib(optional=True, deserializer=str) + + result = marshaller_impl.deserialize({"id": 12345,}, User) + + assert isinstance(result, User) + assert result.id == "12345" + + def test_deserialize_optional_success_if_not_specified(self, marshaller_impl): + @marshaller.attrs(marshaller=marshaller_impl) + class User: + id: int = marshaller.attrib(optional=True, deserializer=str) + + result = marshaller_impl.deserialize({"id": None,}, User) + + assert isinstance(result, User) + assert result.id is None + + @_helpers.assert_raises(type_=AttributeError) + def test_deserialize_fail_on_None_if_not_optional(self, marshaller_impl): + @marshaller.attrs(marshaller=marshaller_impl) + class User: + id: int = marshaller.attrib(optional=False, deserializer=str) + + marshaller_impl.deserialize({"id": None,}, User) + + @_helpers.assert_raises(type_=TypeError) + def test_deserialize_fail_on_Error(self, marshaller_impl): + die = mock.MagicMock(side_effect=RuntimeError) + + @marshaller.attrs(marshaller=marshaller_impl) + class User: + id: int = marshaller.attrib(deserializer=die) + + marshaller_impl.deserialize({"id": 123,}, User) + + def test_serialize(self, marshaller_impl): + @marshaller.attrs(marshaller=marshaller_impl) + class User: + id: int = marshaller.attrib(deserializer=..., serializer=str) + some_list: list = marshaller.attrib(deserializer=..., serializer=lambda i: list(map(int, i))) + + u = User(12, ["9", "18", "27", "36"]) + + assert marshaller_impl.serialize(u) == {"id": "12", "some_list": [9, 18, 27, 36]} + + def test_serialize_transient(self, marshaller_impl): + @marshaller.attrs(marshaller=marshaller_impl) + class User: + id: int = marshaller.attrib(deserializer=..., serializer=str) + some_list: list = marshaller.attrib( + deserializer=..., transient=True, + ) + + u = User(12, ["9", "18", "27", "36"]) + + assert marshaller_impl.serialize(u) == { + "id": "12", + } + + @_helpers.assert_raises(type_=LookupError) + def test_deserialize_on_unregistered_class_raises_LookupError(self, marshaller_impl): + class Foo: + pass + + marshaller_impl.deserialize({}, Foo) + + @_helpers.assert_raises(type_=LookupError) + def test_serialize_on_unregistered_class_raises_LookupError(self, marshaller_impl): + class Foo: + pass + + f = Foo() + + marshaller_impl.serialize(f) diff --git a/tests/hikari/internal_utilities/test_marshaller_pep563.py b/tests/hikari/internal_utilities/test_marshaller_pep563.py new file mode 100644 index 0000000000..d57fcf2281 --- /dev/null +++ b/tests/hikari/internal_utilities/test_marshaller_pep563.py @@ -0,0 +1,179 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# Copyright © Nekoka.tt 2019-2020 +# +# This file is part of Hikari. +# +# Hikari is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Hikari is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Hikari. If not, see . +""" +Same as the marshaller tests, but with PEP 563 POSTPONED TYPE ANNOTATIONS +future support enabled, to prove type hints do not interfere with this +mechanism if they are postponed and evaluated as string literals. +""" +from __future__ import annotations + +import cymock as mock +import pytest + +from hikari.internal_utilities import marshaller +from tests.hikari import _helpers + + +class TestAttribPep563: + def test_invokes_attrs(self): + deserializer = lambda _: _ + serializer = lambda _: _ + + with mock.patch("attr.ib") as attrib: + marshaller.attrib( + deserializer=deserializer, + raw_name="foo", + transient=False, + optional=True, + serializer=serializer, + foo=12, + bar="hello, world", + ) + + attrib.assert_called_once_with( + foo=12, + bar="hello, world", + metadata={ + marshaller._RAW_NAME_ATTR: "foo", + marshaller._SERIALIZER_ATTR: serializer, + marshaller._DESERIALIZER_ATTR: deserializer, + marshaller._TRANSIENT_ATTR: False, + marshaller._OPTIONAL_ATTR: True, + }, + ) + + +class TestAttrsPep563: + def test_invokes_attrs(self): + marshaller_mock = mock.create_autospec(marshaller.HikariEntityMarshaller, spec_set=True) + + kwargs = {"foo": 9, "bar": "lol", "marshaller": marshaller_mock} + + marshaller_mock.register = mock.MagicMock(wraps=lambda c: c) + + with mock.patch("attr.s", return_value=lambda c: c) as attrs: + + @marshaller.attrs(**kwargs) + class Foo: + bar = 69 + + assert Foo is not None + assert Foo.bar == 69 + + attrs.assert_called_once_with(foo=9, bar="lol", auto_attribs=False) + marshaller_mock.register.assert_called_once_with(Foo) + + +class TestMarshallerPep563: + @pytest.fixture() + def marshaller_impl(self): + return marshaller.HikariEntityMarshaller() + + def test_deserialize(self, marshaller_impl): + deserialized_id = mock.MagicMock() + id_deserializer = mock.MagicMock(return_value=deserialized_id) + + @marshaller.attrs(marshaller=marshaller_impl) + class User: + id: int = marshaller.attrib(deserializer=id_deserializer) + some_list: list = marshaller.attrib(deserializer=lambda items: [str(i) for i in items]) + + result = marshaller_impl.deserialize({"id": "12345", "some_list": [True, False, "foo", 12, 3.4]}, User) + + assert isinstance(result, User) + assert result.id == deserialized_id + assert result.some_list == ["True", "False", "foo", "12", "3.4"] + + def test_deserialize_optional_success_if_specified(self, marshaller_impl): + @marshaller.attrs(marshaller=marshaller_impl) + class User: + id: int = marshaller.attrib(optional=True, deserializer=str) + + result = marshaller_impl.deserialize({"id": 12345,}, User) + + assert isinstance(result, User) + assert result.id == "12345" + + def test_deserialize_optional_success_if_not_specified(self, marshaller_impl): + @marshaller.attrs(marshaller=marshaller_impl) + class User: + id: int = marshaller.attrib(optional=True, deserializer=str) + + result = marshaller_impl.deserialize({"id": None,}, User) + + assert isinstance(result, User) + assert result.id is None + + @_helpers.assert_raises(type_=AttributeError) + def test_deserialize_fail_on_None_if_not_optional(self, marshaller_impl): + @marshaller.attrs(marshaller=marshaller_impl) + class User: + id: int = marshaller.attrib(optional=False, deserializer=str) + + marshaller_impl.deserialize({"id": None,}, User) + + @_helpers.assert_raises(type_=TypeError) + def test_deserialize_fail_on_Error(self, marshaller_impl): + die = mock.MagicMock(side_effect=RuntimeError) + + @marshaller.attrs(marshaller=marshaller_impl) + class User: + id: int = marshaller.attrib(deserializer=die) + + marshaller_impl.deserialize({"id": 123,}, User) + + def test_serialize(self, marshaller_impl): + @marshaller.attrs(marshaller=marshaller_impl) + class User: + id: int = marshaller.attrib(deserializer=..., serializer=str) + some_list: list = marshaller.attrib(deserializer=..., serializer=lambda i: list(map(int, i))) + + u = User(12, ["9", "18", "27", "36"]) + + assert marshaller_impl.serialize(u) == {"id": "12", "some_list": [9, 18, 27, 36]} + + def test_serialize_transient(self, marshaller_impl): + @marshaller.attrs(marshaller=marshaller_impl) + class User: + id: int = marshaller.attrib(deserializer=..., serializer=str) + some_list: list = marshaller.attrib( + deserializer=..., transient=True, + ) + + u = User(12, ["9", "18", "27", "36"]) + + assert marshaller_impl.serialize(u) == { + "id": "12", + } + + @_helpers.assert_raises(type_=LookupError) + def test_deserialize_on_unregistered_class_raises_LookupError(self, marshaller_impl): + class Foo: + pass + + marshaller_impl.deserialize({}, Foo) + + @_helpers.assert_raises(type_=LookupError) + def test_serialize_on_unregistered_class_raises_LookupError(self, marshaller_impl): + class Foo: + pass + + f = Foo() + + marshaller_impl.serialize(f) From 9b0b659721fe30f66d99247d3740c081ffa58b9f Mon Sep 17 00:00:00 2001 From: Faster Speeding Date: Sun, 15 Mar 2020 23:53:29 +0000 Subject: [PATCH 012/922] Split optional into is_none and is_undefined for marshaller.attrib plus allow for default factories. --- hikari/core/guilds.py | 60 +++++++------- hikari/internal_utilities/marshaller.py | 59 +++++++++---- .../internal_utilities/test_marshaller.py | 83 ++++++++++++++++--- .../test_marshaller_pep563.py | 83 ++++++++++++++++--- 4 files changed, 218 insertions(+), 67 deletions(-) diff --git a/hikari/core/guilds.py b/hikari/core/guilds.py index 5a1e124d02..b67fbfedb0 100644 --- a/hikari/core/guilds.py +++ b/hikari/core/guilds.py @@ -260,18 +260,18 @@ class Guild(snowflakes.UniqueEntity, entities.Deserializable): #: The hash for the guild icon, if there is one. #: #: :type: :class:`str`, optional - icon_hash: typing.Optional[str] = marshaller.attrib(raw_name="icon", optional=True, deserializer=str) + icon_hash: typing.Optional[str] = marshaller.attrib(raw_name="icon", deserializer=str, if_none=None) #: The hash of the splash for the guild, if there is one. #: #: :type: :class:`str`, optional - splash_hash: typing.Optional[str] = marshaller.attrib(raw_name="splash", deserializer=str, optional=True) + splash_hash: typing.Optional[str] = marshaller.attrib(raw_name="splash", deserializer=str, if_none=None) #: The hash of the discovery splash for the guild, if there is one. #: #: :type: :class:`str`, optional discovery_splash_hash: typing.Optional[str] = marshaller.attrib( - raw_name="discovery_splash", deserializer=str, optional=True + raw_name="discovery_splash", deserializer=str, if_none=None ) #: The ID of the owner of this guild. @@ -295,7 +295,7 @@ class Guild(snowflakes.UniqueEntity, entities.Deserializable): #: guild. #: #: :type: :class:`snowflakes.Snowflake`, optional - afk_channel_id: typing.Optional[snowflakes.Snowflake] = marshaller.attrib(deserializer=str, optional=True) + afk_channel_id: typing.Optional[snowflakes.Snowflake] = marshaller.attrib(deserializer=str, if_none=None) #: How long a voice user has to be AFK for before they are classed as being #: AFK and are moved to the AFK channel (:attr:`afk_channel_id`). @@ -312,7 +312,7 @@ class Guild(snowflakes.UniqueEntity, entities.Deserializable): #: #: :type: :class:`bool`, optional is_embed_enabled: typing.Optional[bool] = marshaller.attrib( - raw_name="embed_enabled", optional=True, deserializer=bool + raw_name="embed_enabled", if_undefined=lambda: False, deserializer=bool ) #: The channel ID that the guild embed will generate an invite to, if @@ -320,7 +320,7 @@ class Guild(snowflakes.UniqueEntity, entities.Deserializable): #: #: :type: :class:`snowflakes.Snowflake`, optional embed_channel_id: typing.Optional[snowflakes.Snowflake] = marshaller.attrib( - deserializer=snowflakes.Snowflake, optional=True + deserializer=snowflakes.Snowflake, if_none=None, if_undefined=None ) #: The verification level required for a user to participate in this guild. @@ -374,7 +374,7 @@ class Guild(snowflakes.UniqueEntity, entities.Deserializable): #: #: :type: :class:`snowflakes.Snowflake`, optional application_id: typing.Optional[snowflakes.Snowflake] = marshaller.attrib( - deserializer=snowflakes.Snowflake, optional=True + deserializer=snowflakes.Snowflake, if_none=None ) # TODO: document in which cases this information is not available. @@ -383,7 +383,7 @@ class Guild(snowflakes.UniqueEntity, entities.Deserializable): #: #: :type: :class:`bool`, optional is_widget_enabled: typing.Optional[bool] = marshaller.attrib( - raw_name="widget_enabled", optional=True, deserializer=bool + raw_name="widget_enabled", if_undefined=None, deserializer=bool ) #: The channel ID that the widget's generated invite will send the user to, @@ -391,14 +391,14 @@ class Guild(snowflakes.UniqueEntity, entities.Deserializable): #: #: :type: :class:`snowflakes.Snowflake`, optional widget_channel_id: typing.Optional[snowflakes.Snowflake] = marshaller.attrib( - optional=True, deserializer=snowflakes.Snowflake + if_undefined=None, if_none=None, deserializer=snowflakes.Snowflake ) #: The ID of the system channel (where welcome messages and Nitro boost #: messages are sent), or ``None`` if it is not enabled. #: :type: :class:`snowflakes.Snowflake`, optional system_channel_id: typing.Optional[snowflakes.Snowflake] = marshaller.attrib( - optional=True, deserializer=snowflakes.Snowflake + if_none=None, deserializer=snowflakes.Snowflake ) #: Flags for the guild system channel to describe which notification @@ -413,7 +413,7 @@ class Guild(snowflakes.UniqueEntity, entities.Deserializable): #: #: :type: :class:`snowflakes.Snowflake`, optional rules_channel_id: typing.Optional[snowflakes.Snowflake] = marshaller.attrib( - optional=True, deserializer=snowflakes.Snowflake + if_none=None, deserializer=snowflakes.Snowflake ) #: The date and time that the bot user joined this guild. @@ -437,7 +437,7 @@ class Guild(snowflakes.UniqueEntity, entities.Deserializable): #: not be sent about members who are offline or invisible. #: #: :type: :class:`bool`, optional - is_large: typing.Optional[bool] = marshaller.attrib(raw_name="large", optional=True, deserializer=bool) + is_large: typing.Optional[bool] = marshaller.attrib(raw_name="large", if_undefined=None, deserializer=bool) #: Whether the guild is unavailable or not. #: @@ -447,7 +447,9 @@ class Guild(snowflakes.UniqueEntity, entities.Deserializable): #: #: An unavailable guild cannot be interacted with, and most information may #: be outdated or missing if that is the case. - is_unavailable: typing.Optional[bool] = marshaller.attrib(raw_name="unavailable", optional=True, deserializer=bool) + is_unavailable: typing.Optional[bool] = marshaller.attrib( + raw_name="unavailable", if_undefined=None, deserializer=bool + ) #: The number of members in this guild. #: @@ -456,7 +458,7 @@ class Guild(snowflakes.UniqueEntity, entities.Deserializable): #: this will always be ``None``. #: #: :type: :class:`int`, optional - member_count: typing.Optional[int] = marshaller.attrib(optional=True, deserializer=int) + member_count: typing.Optional[int] = marshaller.attrib(if_undefined=None, deserializer=int) #: A mapping of ID to the corresponding guild members in this guild. #: @@ -477,7 +479,7 @@ class Guild(snowflakes.UniqueEntity, entities.Deserializable): #: #: :type: :class:`typing.Mapping` [ :class:`snowflakes.Snowflake`, :class:`GuildMember` ], optional members: typing.Optional[typing.Mapping[snowflakes.Snowflake, GuildMember]] = marshaller.attrib( - deserializer=lambda members: {m.user.id: m for m in map(GuildMember.deserialize, members)}, optional=True, + deserializer=lambda members: {m.user.id: m for m in map(GuildMember.deserialize, members)}, if_undefined=None, ) #: A mapping of ID to the corresponding guild channels in this guild. @@ -497,7 +499,8 @@ class Guild(snowflakes.UniqueEntity, entities.Deserializable): #: #: :type: :class:`typing.Mapping` [ :class:`snowflakes.Snowflake`, :class:`GuildChannel` ], optional channels: typing.Optional[typing.Mapping[snowflakes.Snowflake, GuildChannel]] = marshaller.attrib( - deserializer=lambda guild_channels: {c.id: c for c in map(parse_guild_channel, guild_channels)}, optional=True, + deserializer=lambda guild_channels: {c.id: c for c in map(parse_guild_channel, guild_channels)}, + if_undefined=None, ) #: A mapping of member ID to the corresponding presence information for @@ -519,28 +522,28 @@ class Guild(snowflakes.UniqueEntity, entities.Deserializable): #: :type: :class:`typing.Mapping` [ :class:`snowflakes.Snowflake`, :class:`GuildMemberPresence` ], optional presences: typing.Optional[typing.Mapping[snowflakes.Snowflake, GuildMemberPresence]] = marshaller.attrib( deserializer=lambda presences: {p.user.id: p for p in map(GuildMemberPresence.deserialize, presences)}, - optional=True, + if_undefined=None, ) #: The maximum number of presences for the guild. If this is ``None``, then #: the default value is used (currently 5000). #: #: :type: :class:`int`, optional - max_presences: typing.Optional[int] = marshaller.attrib(optional=True, deserializer=int) + max_presences: typing.Optional[int] = marshaller.attrib(if_none=None, if_undefined=None, deserializer=int) #: The maximum number of members allowed in this guild. #: #: This information may not be present, in which case, it will be ``None``. #: #: :type: :class:`int`, optional - max_members: typing.Optional[int] = marshaller.attrib(optional=True, deserializer=int) + max_members: typing.Optional[int] = marshaller.attrib(if_undefined=None, deserializer=int) #: The vanity URL code for the guild's vanity URL. #: This is only present if :obj:`GuildFeatures.VANITY_URL` is in the #: :attr:`features` for this guild. If not, this will always be ``None``. #: #: :type: :class:`str`, optional - vanity_url_code: typing.Optional[str] = marshaller.attrib(optional=True, deserializer=str) + vanity_url_code: typing.Optional[str] = marshaller.attrib(if_none=None, deserializer=str) #: The guild's description. #: @@ -549,14 +552,14 @@ class Guild(snowflakes.UniqueEntity, entities.Deserializable): #: ``None``. #: #: :type: :class:`str`, optional - description: typing.Optional[str] = marshaller.attrib(optional=True, deserializer=str) + description: typing.Optional[str] = marshaller.attrib(if_none=None, deserializer=str) #: The hash for the guild's banner. #: This is only present if the guild has :obj:`GuildFeatures.BANNER` in the #: :attr:`features` for this guild. For all other purposes, it is ``None``. #: #: :type: :class:`str`, optional - banner_hash: typing.Optional[str] = marshaller.attrib(raw_name="banner", optional=True, deserializer=str) + banner_hash: typing.Optional[str] = marshaller.attrib(raw_name="banner", if_none=None, deserializer=str) #: The premium tier for this guild. #: @@ -567,16 +570,15 @@ class Guild(snowflakes.UniqueEntity, entities.Deserializable): #: information may not be present, in which case, it will be ``None``. #: #: :type: :class:`int`, optional - premium_subscription_count: typing.Optional[int] = marshaller.attrib(optional=True, deserializer=int) + premium_subscription_count: typing.Optional[int] = marshaller.attrib(if_undefined=None, deserializer=int) #: The preferred locale to use for this guild. #: - #: This appears to only be present if :obj:`GuildFeatures.PUBLIC` is in the - #: :attr:`features` for this guild. For all other purposes, it should be - #: considered to be ``None`` until more clarification is given by Discord. + #: This can only be change if :obj:`GuildFeatures.PUBLIC` is in the + #: :attr:`features` for this guild and will otherwise default to `en-US`. #: - #: :type: :class:`str`, optional - preferred_locale: typing.Optional[str] = marshaller.attrib(optional=True, deserializer=str) + #: :type: :class:`str` + preferred_locale: str = marshaller.attrib(deserializer=str) #: The channel ID of the channel where admins and moderators receive notices #: from Discord. @@ -587,5 +589,5 @@ class Guild(snowflakes.UniqueEntity, entities.Deserializable): #: #: :type: :class:`snowflakes.Snowflake`, optional public_updates_channel_id: typing.Optional[snowflakes.Snowflake] = marshaller.attrib( - optional=True, deserializer=snowflakes.Snowflake + if_none=None, deserializer=snowflakes.Snowflake ) diff --git a/hikari/internal_utilities/marshaller.py b/hikari/internal_utilities/marshaller.py index 13a31a1843..be9fd75000 100644 --- a/hikari/internal_utilities/marshaller.py +++ b/hikari/internal_utilities/marshaller.py @@ -34,10 +34,13 @@ _SERIALIZER_ATTR = __name__ + "_SERIALIZER" _DESERIALIZER_ATTR = __name__ + "_DESERIALIZER" _TRANSIENT_ATTR = __name__ + "_TRANSIENT" -_OPTIONAL_ATTR = __name__ + "_OPTIONAL" +_IF_UNDEFINED = __name__ + "IF_UNDEFINED" +_IF_NONE = __name__ + "_IF_NONE" MARSHALLER_META_ATTR = "__hikari_marshaller_meta_attr__" +RAISE = object() + EntityT = typing.TypeVar("EntityT", contravariant=True) @@ -50,9 +53,10 @@ def attrib( # type hints, the library loses the ability to be type checked properly # anymore, so we have to pass this explicitly regardless. deserializer: typing.Callable[[typing.Any], typing.Any], + if_none: typing.Union[typing.Callable[..., typing.Any], None, type(RAISE)] = RAISE, + if_undefined: typing.Union[typing.Callable[..., typing.Any], None, type(RAISE)] = RAISE, raw_name: typing.Optional[str] = None, transient: bool = False, - optional: bool = False, serializer: typing.Optional[typing.Callable[[typing.Any], typing.Any]] = None, **kwargs, ) -> typing.Any: @@ -68,10 +72,16 @@ def attrib( transient : :obj:`bool` If True, the field is marked as transient, meaning it will not be serialized. Defaults to False. - optional : :obj:`bool` - If True, the field is marked as being allowed to be `None`. If a field - is not optional, and a `None` value is passed to it later on, then - an exception will be raised. + if_none : :obj:`typing.Union` [ :obj:`typing.Callable` [ ... , :obj:`typing.Any` ], :obj:`None` ], optional + Either a default factory function called to get the default for when + this field is `None` or `None` to specify that this should default + to `None`. Will raise an exception when `None` is received for this + field later if this isn't specified. + if_undefined : :obj:`typing.Union` [ :obj:`typing.Callable` [ ... , :obj:`typing.Any` ], :obj:`None` ], optional + Either a default factory function called to get the default for when + this field isn't defined or `None` to specify that this should default + to `None`. Will raise an exception when this field is undefined later + on if this isn't specified. serializer : :obj:`typing.Callable` [ [ :obj:`typing.Any` ], :obj:`typing.Any` ], optional The serializer to use. If not specified, then serializing the entire class that this attribute is in will trigger a :class:`TypeError` @@ -89,8 +99,9 @@ class that this attribute is in will trigger a :class:`TypeError` metadata[_RAW_NAME_ATTR] = raw_name metadata[_SERIALIZER_ATTR] = serializer metadata[_DESERIALIZER_ATTR] = deserializer + metadata[_IF_NONE] = if_none + metadata[_IF_UNDEFINED] = if_undefined metadata[_TRANSIENT_ATTR] = transient - metadata[_OPTIONAL_ATTR] = optional return attr.ib(**kwargs) @@ -102,20 +113,22 @@ def error(*_, **__) -> typing.NoReturn: class _AttributeDescriptor: - __slots__ = ("raw_name", "field_name", "is_optional", "is_transient", "deserializer", "serializer") + __slots__ = ("raw_name", "field_name", "if_none", "if_undefined", "is_transient", "deserializer", "serializer") def __init__( self, raw_name: str, field_name: str, - is_optional: bool, + if_none: typing.Callable[..., typing.Any], + if_undefined: typing.Callable[..., typing.Any], is_transient: bool, deserializer: typing.Callable[[typing.Any], typing.Any], serializer: typing.Callable[[typing.Any], typing.Any], ) -> None: self.raw_name = raw_name self.field_name = field_name - self.is_optional = is_optional + self.if_none = if_none + self.if_undefined = if_undefined self.is_transient = is_transient # Do not serialize self.deserializer = deserializer self.serializer = serializer @@ -136,7 +149,8 @@ def _construct_attribute_descriptor(field: attr.Attribute) -> _AttributeDescript return _AttributeDescriptor( raw_name=raw_name, field_name=field_name, - is_optional=field.metadata[_OPTIONAL_ATTR], + if_none=field.metadata[_IF_NONE], + if_undefined=field.metadata[_IF_UNDEFINED], is_transient=field.metadata[_TRANSIENT_ATTR], deserializer=field.metadata[_DESERIALIZER_ATTR], serializer=field.metadata[_SERIALIZER_ATTR] or _no_serialize(field_name), @@ -219,17 +233,30 @@ def deserialize(self, raw_data: typing.Mapping[str, typing.Any], target_type: ty kwargs = {} for a in descriptor.attribs: - if a.raw_name not in raw_data or raw_data[a.raw_name] is None: - if not a.is_optional: + if a.raw_name not in raw_data: + if a.if_undefined is RAISE: + raise AttributeError( + f"Required field {a.field_name} (from raw {a.raw_name}) is not specified in the input " + f"payload\n\n{raw_data}" + ) + elif a.if_undefined: + kwargs[a.field_name] = a.if_undefined() + else: + kwargs[a.field_name] = None + continue + elif (data := raw_data[a.raw_name]) is None: + if a.if_none is RAISE: raise AttributeError( - f"Non-optional field {a.field_name} (from raw {a.raw_name}) is not specified in the input " + f"Non-nullable field {a.field_name} (from raw {a.raw_name}) is `None` in the input " f"payload\n\n{raw_data}" ) - kwargs[a.field_name] = None + elif a.if_none: + kwargs[a.field_name] = a.if_none() + else: + kwargs[a.field_name] = None continue try: - data = raw_data[a.raw_name] # Use the deserializer if it is there, otherwise use the constructor of the type of the field. kwargs[a.field_name] = a.deserializer(data) if a.deserializer else data except Exception: diff --git a/tests/hikari/internal_utilities/test_marshaller.py b/tests/hikari/internal_utilities/test_marshaller.py index b8c03f1e40..42df7c1019 100644 --- a/tests/hikari/internal_utilities/test_marshaller.py +++ b/tests/hikari/internal_utilities/test_marshaller.py @@ -28,12 +28,16 @@ def test_invokes_attrs(self): deserializer = lambda _: _ serializer = lambda _: _ + mock_default_factory_1 = mock.MagicMock + mock_default_factory_2 = mock.MagicMock + with mock.patch("attr.ib") as attrib: marshaller.attrib( deserializer=deserializer, raw_name="foo", + if_none=mock_default_factory_1, + if_undefined=mock_default_factory_2, transient=False, - optional=True, serializer=serializer, foo=12, bar="hello, world", @@ -47,7 +51,8 @@ def test_invokes_attrs(self): marshaller._SERIALIZER_ATTR: serializer, marshaller._DESERIALIZER_ATTR: deserializer, marshaller._TRANSIENT_ATTR: False, - marshaller._OPTIONAL_ATTR: True, + marshaller._IF_UNDEFINED: mock_default_factory_2, + marshaller._IF_NONE: mock_default_factory_1, }, ) @@ -93,33 +98,89 @@ class User: assert result.id == deserialized_id assert result.some_list == ["True", "False", "foo", "12", "3.4"] - def test_deserialize_optional_success_if_specified(self, marshaller_impl): + def test_deserialize_not_required_success_if_specified(self, marshaller_impl): + @marshaller.attrs(marshaller=marshaller_impl) + class User: + id: int = marshaller.attrib(if_undefined=None, deserializer=str) + + result = marshaller_impl.deserialize({"id": 12345}, User) + + assert isinstance(result, User) + assert result.id == "12345" + + def test_deserialize_not_required_success_if_not_specified(self, marshaller_impl): + @marshaller.attrs(marshaller=marshaller_impl) + class User: + id: int = marshaller.attrib(if_undefined=None, deserializer=str) + + result = marshaller_impl.deserialize({}, User) + + assert isinstance(result, User) + assert result.id is None + + def test_deserialize_calls_if_undefined_if_not_none_and_field_not_present(self, marshaller_impl): + mock_result = mock.MagicMock() + mock_callable = mock.MagicMock(return_value=mock_result) + + @marshaller.attrs(marshaller=marshaller_impl) + class User: + id: int = marshaller.attrib(if_undefined=mock_callable, deserializer=str) + + result = marshaller_impl.deserialize({}, User) + + assert isinstance(result, User) + assert result.id is mock_result + mock_callable.assert_called_once() + + @_helpers.assert_raises(type_=AttributeError) + def test_deserialize_fail_on_unspecified_if_required(self, marshaller_impl): @marshaller.attrs(marshaller=marshaller_impl) class User: - id: int = marshaller.attrib(optional=True, deserializer=str) + id: int = marshaller.attrib(deserializer=str) + + marshaller_impl.deserialize({}, User) - result = marshaller_impl.deserialize({"id": 12345,}, User) + def test_deserialize_nullable_success_if_not_null(self, marshaller_impl): + @marshaller.attrs(marshaller=marshaller_impl) + class User: + id: int = marshaller.attrib(if_none=None, deserializer=str) + + result = marshaller_impl.deserialize({"id": 12345}, User) assert isinstance(result, User) assert result.id == "12345" - def test_deserialize_optional_success_if_not_specified(self, marshaller_impl): + def test_deserialize_nullable_success_if_null(self, marshaller_impl): @marshaller.attrs(marshaller=marshaller_impl) class User: - id: int = marshaller.attrib(optional=True, deserializer=str) + id: int = marshaller.attrib(if_none=None, deserializer=str) - result = marshaller_impl.deserialize({"id": None,}, User) + result = marshaller_impl.deserialize({"id": None}, User) assert isinstance(result, User) assert result.id is None + def test_deserialize_calls_if_none_if_not_none_and_data_is_none(self, marshaller_impl): + mock_result = mock.MagicMock() + mock_callable = mock.MagicMock(return_value=mock_result) + + @marshaller.attrs(marshaller=marshaller_impl) + class User: + id: int = marshaller.attrib(if_none=mock_callable, deserializer=str) + + result = marshaller_impl.deserialize({"id": None}, User) + + assert isinstance(result, User) + assert result.id is mock_result + mock_callable.assert_called_once() + @_helpers.assert_raises(type_=AttributeError) - def test_deserialize_fail_on_None_if_not_optional(self, marshaller_impl): + def test_deserialize_fail_on_None_if_not_nullable(self, marshaller_impl): @marshaller.attrs(marshaller=marshaller_impl) class User: - id: int = marshaller.attrib(optional=False, deserializer=str) + id: int = marshaller.attrib(deserializer=str) - marshaller_impl.deserialize({"id": None,}, User) + marshaller_impl.deserialize({"id": None}, User) @_helpers.assert_raises(type_=TypeError) def test_deserialize_fail_on_Error(self, marshaller_impl): diff --git a/tests/hikari/internal_utilities/test_marshaller_pep563.py b/tests/hikari/internal_utilities/test_marshaller_pep563.py index d57fcf2281..10392e61dd 100644 --- a/tests/hikari/internal_utilities/test_marshaller_pep563.py +++ b/tests/hikari/internal_utilities/test_marshaller_pep563.py @@ -35,12 +35,16 @@ def test_invokes_attrs(self): deserializer = lambda _: _ serializer = lambda _: _ + mock_default_factory_1 = mock.MagicMock + mock_default_factory_2 = mock.MagicMock + with mock.patch("attr.ib") as attrib: marshaller.attrib( deserializer=deserializer, raw_name="foo", + if_none=mock_default_factory_1, + if_undefined=mock_default_factory_2, transient=False, - optional=True, serializer=serializer, foo=12, bar="hello, world", @@ -54,7 +58,8 @@ def test_invokes_attrs(self): marshaller._SERIALIZER_ATTR: serializer, marshaller._DESERIALIZER_ATTR: deserializer, marshaller._TRANSIENT_ATTR: False, - marshaller._OPTIONAL_ATTR: True, + marshaller._IF_UNDEFINED: mock_default_factory_2, + marshaller._IF_NONE: mock_default_factory_1, }, ) @@ -100,33 +105,89 @@ class User: assert result.id == deserialized_id assert result.some_list == ["True", "False", "foo", "12", "3.4"] - def test_deserialize_optional_success_if_specified(self, marshaller_impl): + def test_deserialize_not_required_success_if_specified(self, marshaller_impl): + @marshaller.attrs(marshaller=marshaller_impl) + class User: + id: int = marshaller.attrib(if_undefined=None, deserializer=str) + + result = marshaller_impl.deserialize({"id": 12345}, User) + + assert isinstance(result, User) + assert result.id == "12345" + + def test_deserialize_not_required_success_if_not_specified(self, marshaller_impl): + @marshaller.attrs(marshaller=marshaller_impl) + class User: + id: int = marshaller.attrib(if_undefined=None, deserializer=str) + + result = marshaller_impl.deserialize({}, User) + + assert isinstance(result, User) + assert result.id is None + + def test_deserialize_calls_if_undefined_if_not_none_and_field_not_present(self, marshaller_impl): + mock_result = mock.MagicMock() + mock_callable = mock.MagicMock(return_value=mock_result) + + @marshaller.attrs(marshaller=marshaller_impl) + class User: + id: int = marshaller.attrib(if_undefined=mock_callable, deserializer=str) + + result = marshaller_impl.deserialize({}, User) + + assert isinstance(result, User) + assert result.id is mock_result + mock_callable.assert_called_once() + + @_helpers.assert_raises(type_=AttributeError) + def test_deserialize_fail_on_unspecified_if_required(self, marshaller_impl): @marshaller.attrs(marshaller=marshaller_impl) class User: - id: int = marshaller.attrib(optional=True, deserializer=str) + id: int = marshaller.attrib(deserializer=str) + + marshaller_impl.deserialize({}, User) - result = marshaller_impl.deserialize({"id": 12345,}, User) + def test_deserialize_nullable_success_if_not_null(self, marshaller_impl): + @marshaller.attrs(marshaller=marshaller_impl) + class User: + id: int = marshaller.attrib(if_none=None, deserializer=str) + + result = marshaller_impl.deserialize({"id": 12345}, User) assert isinstance(result, User) assert result.id == "12345" - def test_deserialize_optional_success_if_not_specified(self, marshaller_impl): + def test_deserialize_nullable_success_if_null(self, marshaller_impl): @marshaller.attrs(marshaller=marshaller_impl) class User: - id: int = marshaller.attrib(optional=True, deserializer=str) + id: int = marshaller.attrib(if_none=None, deserializer=str) - result = marshaller_impl.deserialize({"id": None,}, User) + result = marshaller_impl.deserialize({"id": None}, User) assert isinstance(result, User) assert result.id is None + def test_deserialize_calls_if_none_if_not_none_and_data_is_none(self, marshaller_impl): + mock_result = mock.MagicMock() + mock_callable = mock.MagicMock(return_value=mock_result) + + @marshaller.attrs(marshaller=marshaller_impl) + class User: + id: int = marshaller.attrib(if_none=mock_callable, deserializer=str) + + result = marshaller_impl.deserialize({"id": None}, User) + + assert isinstance(result, User) + assert result.id is mock_result + mock_callable.assert_called_once() + @_helpers.assert_raises(type_=AttributeError) - def test_deserialize_fail_on_None_if_not_optional(self, marshaller_impl): + def test_deserialize_fail_on_None_if_not_nullable(self, marshaller_impl): @marshaller.attrs(marshaller=marshaller_impl) class User: - id: int = marshaller.attrib(optional=False, deserializer=str) + id: int = marshaller.attrib(deserializer=str) - marshaller_impl.deserialize({"id": None,}, User) + marshaller_impl.deserialize({"id": None}, User) @_helpers.assert_raises(type_=TypeError) def test_deserialize_fail_on_Error(self, marshaller_impl): From bdb001860b3020a7f16e9ca152067afa9533577d Mon Sep 17 00:00:00 2001 From: davfsa Date: Mon, 16 Mar 2020 09:41:12 +0100 Subject: [PATCH 013/922] Switch docstring styles to numpy - Added missing documentation --- gitlab/pages.yml | 3 + hikari/__init__.py | 4 +- hikari/errors.py | 14 +- hikari/internal_utilities/__init__.py | 8 +- hikari/internal_utilities/aio.py | 106 +- hikari/internal_utilities/assertions.py | 8 +- hikari/internal_utilities/cache.py | 71 +- hikari/internal_utilities/containers.py | 4 +- hikari/internal_utilities/dates.py | 64 +- hikari/internal_utilities/loggers.py | 26 +- hikari/internal_utilities/singleton_meta.py | 30 +- hikari/internal_utilities/storage.py | 27 +- hikari/internal_utilities/transformations.py | 69 +- hikari/net/__init__.py | 3 +- hikari/net/base_http_client.py | 190 +- hikari/net/codes.py | 37 +- hikari/net/errors.py | 40 +- hikari/net/gateway.py | 195 +- hikari/net/http_client.py | 3163 ++++++++++-------- hikari/net/ratelimits.py | 194 +- hikari/net/routes.py | 108 +- hikari/net/user_agent.py | 55 +- hikari/net/versions.py | 12 +- tests/hikari/net/test_http_client.py | 20 +- 24 files changed, 2426 insertions(+), 2025 deletions(-) diff --git a/gitlab/pages.yml b/gitlab/pages.yml index cbe6ed6209..a41dcac4cf 100644 --- a/gitlab/pages.yml +++ b/gitlab/pages.yml @@ -62,6 +62,9 @@ test-pages: script: - pip install requests nox - bash tasks/retry_aborts.sh nox -s documentation + only: + - merge_requests + - branches except: - master - staging diff --git a/hikari/__init__.py b/hikari/__init__.py index a7fc4348df..a2eef25497 100644 --- a/hikari/__init__.py +++ b/hikari/__init__.py @@ -16,9 +16,7 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . -""" -Hikari's core framework for writing Discord bots in Python. -""" +"""Hikari's core framework for writing Discord bots in Python.""" from hikari import errors from hikari import net from hikari._about import __author__ diff --git a/hikari/errors.py b/hikari/errors.py index c12827754e..d2c9d9595c 100644 --- a/hikari/errors.py +++ b/hikari/errors.py @@ -16,18 +16,18 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . -""" -Core errors that may be raised by this API implementation. -""" +"""Core errors that may be raised by this API implementation.""" __all__ = ["HikariError"] class HikariError(RuntimeError): - """ - Base for an error raised by this API. Any errors should derive from this. + """Base for an error raised by this API. + + Any errors should derive from this. - Note: - You should never initialize this exception directly. + Note + ---- + You should never initialize this exception directly. """ __slots__ = () diff --git a/hikari/internal_utilities/__init__.py b/hikari/internal_utilities/__init__.py index 019aae3887..9f715f0f62 100644 --- a/hikari/internal_utilities/__init__.py +++ b/hikari/internal_utilities/__init__.py @@ -16,9 +16,11 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . -""" -Various utilities used internally within this API. These are not bound to the versioning contact, and are considered -to be implementation detail that could change at any time, so should not be used outside this library. +"""Various utilities used internally within this API. + +These are not bound to the versioning contact, and are considered to be +implementation detail that could change at any time, so should not be +used outside this library. """ from hikari.internal_utilities import aio from hikari.internal_utilities import assertions diff --git a/hikari/internal_utilities/aio.py b/hikari/internal_utilities/aio.py index 62b0f505b6..95265620ef 100644 --- a/hikari/internal_utilities/aio.py +++ b/hikari/internal_utilities/aio.py @@ -16,9 +16,7 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . -""" -Asyncio extensions and utilities. -""" +"""Asyncio extensions and utilities.""" __all__ = [ "CoroutineFunctionT", "PartialCoroutineProtocolT", @@ -30,6 +28,7 @@ import asyncio import dataclasses import typing +import logging import weakref from hikari.internal_utilities import assertions @@ -40,7 +39,7 @@ class PartialCoroutineProtocolT(typing.Protocol[ReturnT]): - """Represents the type of a :class:`functools.partial` wrapping an :mod:`asyncio` coroutine.""" + """Represents the type of a :obj:`functools.partial` wrapping an :mod:`asyncio` coroutine.""" def __call__(self, *args, **kwargs) -> typing.Coroutine[None, None, ReturnT]: ... @@ -93,10 +92,20 @@ class EventDelegate: Parameters ---------- - exception_event: :obj:`str` + exception_event : :obj:`str` The event to invoke if an exception is caught. """ + #: The event to invoke if an exception is caught. + #: + #: :type: :obj:`str` + exception_event: str + + #: The logger used to write log messages. + #: + #: :type: :obj:`logging.Logger` + logger: logging.Logger + __slots__ = ("exception_event", "logger", "_listeners", "_waiters") def __init__(self, exception_event: str) -> None: @@ -106,14 +115,19 @@ def __init__(self, exception_event: str) -> None: self.exception_event = exception_event def add(self, name: str, coroutine_function: CoroutineFunctionT) -> None: - """ - Register a new event callback to a given event name. + """Register a new event callback to a given event name. - Args: - name: - The name of the event to register to. - coroutine_function: - The event callback to invoke when this event is fired. + Parameters + ---------- + name : :obj:`str` + The name of the event to register to. + coroutine_function + The event callback to invoke when this event is fired. + + Raises + ------ + :obj:`TypeError` + If ``coroutine_function`` is not a coroutine. """ assertions.assert_that( asyncio.iscoroutinefunction(coroutine_function), "You must subscribe a coroutine function only", TypeError @@ -123,15 +137,16 @@ def add(self, name: str, coroutine_function: CoroutineFunctionT) -> None: self._listeners[name].append(coroutine_function) def remove(self, name: str, coroutine_function: CoroutineFunctionT) -> None: - """ - Remove the given coroutine function from the handlers for the given event. The name is mandatory to enable - supporting registering the same event callback for multiple event types. - - Args: - name: - The event to remove from. - coroutine_function: - The event callback to remove. + """Remove the given coroutine function from the handlers for the given event. + + The name is mandatory to enable supporting registering the same event callback for multiple event types. + + Parameters + ---------- + name : :obj:`str` + The event to remove from. + coroutine_function + The event callback to remove. """ if name in self._listeners and coroutine_function in self._listeners[name]: if len(self._listeners[name]) - 1 == 0: @@ -149,12 +164,12 @@ def dispatch(self, name: str, *args): ---------- name: :obj:`str` The name of the event to dispatch. - *args: zero or more :obj:`typing.Any` + *args The parameters to pass to the event callback. Returns ------- - :obj:`asyncio.Future`: + :obj:`asyncio.Future` This may be a gathering future of the callbacks to invoke, or it may be a completed future object. Regardless, this result will be scheduled on the event loop automatically, and does not need to be @@ -211,10 +226,10 @@ def handle_exception( This implementation will check to see if the event that triggered the exception is an exception event. If this exceptino was caused by the - :attr:`exception_event`, then nothing is dispatched (thus preventing + ``exception_event``, then nothing is dispatched (thus preventing an exception handler recursively re-triggering itself). Otherwise, an - :attr:`exception_event` is dispatched with a - :class:`EventExceptionContext` as the sole parameter. + ``exception_event`` is dispatched with a + :obj:`EventExceptionContext` as the sole parameter. Parameters ---------- @@ -224,7 +239,7 @@ def handle_exception( The name of the event that triggered the exception. args: :obj:`typing.Sequence` [ :obj:`typing.Any` ] The arguments passed to the event that threw an exception. - callback: :obj:`CoroutineFunctionT` + callback The callback that threw the exception. """ # Do not recurse if a dodgy exception handler is added. @@ -264,22 +279,25 @@ def wait_for( leak memory if you do this from an event listener that gets repeatedly called. If you want to do this, you should consider using an event listener instead of this function. - predicate : + predicate : :obj:`typing.Callable` [ ..., :obj:`bool` ] A function that takes the arguments for the event and returns True if it is a match, or False if it should be ignored. This cannot be a coroutine function. Returns ------- - A future that when awaited will provide a the arguments passed to the - first matching event. If no arguments are passed to the event, then - `None` is the result. If one argument is passed to the event, then - that argument is the result, otherwise a tuple of arguments is the - result instead. - - Note that awaiting this result will raise an :obj:`asyncio.TimeoutError` - if the timeout is hit and no match is found. If the predicate throws - any exception, this is raised immediately. + :obj:`asyncio.Future` + A future that when awaited will provide a the arguments passed to the + first matching event. If no arguments are passed to the event, then + `None` is the result. If one argument is passed to the event, then + that argument is the result, otherwise a tuple of arguments is the + result instead. + + Note + ---- + Awaiting this result will raise an :obj:`asyncio.TimeoutError` if the timeout + is hit and no match is found. If the predicate throws any exception, + this is raised immediately. """ future = asyncio.get_event_loop().create_future() if name not in self._waiters: @@ -292,14 +310,16 @@ def wait_for( def completed_future(result: typing.Any = None) -> asyncio.Future: - """ - Create a future on the current running loop that is completed, then return it. + """Create a future on the current running loop that is completed, then return it. - Args: - result: - The value to set for the result of the future. + Parameters + --------- + result : :obj:`typing.Any` + The value to set for the result of the future. - Returns: + Returns + ------- + :obj:`asyncio.Future` The completed future. """ future = asyncio.get_event_loop().create_future() diff --git a/hikari/internal_utilities/assertions.py b/hikari/internal_utilities/assertions.py index 44a27d9af2..745e5feedd 100644 --- a/hikari/internal_utilities/assertions.py +++ b/hikari/internal_utilities/assertions.py @@ -16,8 +16,8 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . -""" -Assertions of things. These are functions that validate a value, expected to return the value on success but error +"""Assertions of things. +These are functions that validate a value, expected to return the value on success but error on any failure. """ __all__ = [ @@ -32,13 +32,13 @@ def assert_that(condition: bool, message: str = None, error_type: type = ValueError) -> None: - """Raises a ValueError with the optional description if the given condition is falsified.""" + """Raises a :obj:`ValueError` with the optional description if the given condition is falsified.""" if not condition: raise error_type(message or "condition must not be False") def assert_not_none(value: ValueT, message: typing.Optional[str] = None) -> ValueT: - """Raises a ValueError with the optional description if the given value is None.""" + """Raises a :obj:`ValueError` with the optional description if the given value is ``None``.""" if value is None: raise ValueError(message or "value must not be None") return value diff --git a/hikari/internal_utilities/cache.py b/hikari/internal_utilities/cache.py index d48426867c..2c5afcd6e3 100644 --- a/hikari/internal_utilities/cache.py +++ b/hikari/internal_utilities/cache.py @@ -16,10 +16,16 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . -""" -Provides mechanisms to cache results of calls lazily. -""" -__all__ = ["cached_function"] +"""Provides mechanisms to cache results of calls lazily.""" +__all__ = [ + "CachedFunctionT", + "CachedPropertyFunctionT", + "CachedFunction", + "CachedProperty", + "AsyncCachedProperty", + "cached_function", + "cached_property", +] import asyncio import functools @@ -43,12 +49,12 @@ def __noop_decorator(func): # pragma: no cover class CachedFunction: - """ - Wraps a call, some arguments, and some keyword arguments in a partial and stores the + """Wraps a call, some arguments, and some keyword arguments in a partial and stores the result of the call for later invocations. - Warning: - This is not thread safe! + Warning + ------- + This is not thread safe! """ _sentinel = object() @@ -88,12 +94,11 @@ def fn_wrapper(): class CachedProperty: - """ - A get/delete descriptor to wrap a no-args method which can cache the result of the - call for future retrieval. Calling `del` on the property will flush the cache. + """A get/delete descriptor to wrap a no-args method which can cache the result of the + call for future retrieval. Calling :func:`del` on the property will flush the cache. This will misbehave on class methods and static methods, and will not work on - non-instance functions. For general functions, you should consider :class:`CachedFunction` + non-instance functions. For general functions, you should consider :obj:`CachedFunction` instead. """ @@ -125,9 +130,7 @@ def __delete__(self, instance: ClassT): class AsyncCachedProperty(CachedProperty): - """ - Cached property implementation that supports awaitable coroutines. - """ + """Cached property implementation that supports awaitable coroutines.""" __slots__ = () @@ -145,21 +148,23 @@ def __get__(self, instance: typing.Optional[ClassT], owner: typing.Type[ClassT]) def cached_function(*args, **kwargs) -> typing.Callable[[CachedFunctionT], typing.Callable[[], ReturnT]]: - """ - Create a wrapped cached call decorator. This remembers the last result - of the given call forever until cleared. - - Note: - This is not useful for instance methods on classes, you should use - a :class:`CachedProperty` instead for those. You should also not expect - thread safety here. Coroutines will be detected and dealt with as futures. - This is lazily evaluated. - - Args: - *args: - Any arguments to call the call with. - **kwargs: - Any kwargs to call the call with. + """Create a wrapped cached call decorator. + + This remembers the last result of the given call forever until cleared. + + Parameters + ----------- + *args + Any arguments to call the call with. + **kwargs + Any kwargs to call the call with. + + Note + ---- + This is not useful for instance methods on classes, you should use + a :obj:`CachedProperty` instead for those. You should also not expect + thread safety here. Coroutines will be detected and dealt with as futures. + This is lazily evaluated. """ def decorator(func): @@ -171,9 +176,9 @@ def decorator(func): def cached_property( *, cache_name=None ) -> typing.Callable[[CachedPropertyFunctionT], typing.Union[CachedProperty, AsyncCachedProperty]]: - """ - Makes a slots-compatible cached property. If using slots, you should specify the `cache_name` - directly. + """Makes a slots-compatible cached property. + + If using slots, you should specify the ``cache_name`` directly. """ def decorator(func: CachedPropertyFunctionT) -> typing.Union[CachedProperty, AsyncCachedProperty]: diff --git a/hikari/internal_utilities/containers.py b/hikari/internal_utilities/containers.py index d341a8ed0c..a6934c2f46 100644 --- a/hikari/internal_utilities/containers.py +++ b/hikari/internal_utilities/containers.py @@ -16,9 +16,7 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . -""" -Custom data structures and constant values. -""" +"""Custom data structures and constant values.""" __all__ = [ "EMPTY_SEQUENCE", "EMPTY_SET", diff --git a/hikari/internal_utilities/dates.py b/hikari/internal_utilities/dates.py index 76b240fa8a..686e7ad9f4 100644 --- a/hikari/internal_utilities/dates.py +++ b/hikari/internal_utilities/dates.py @@ -16,9 +16,7 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . -""" -Date/Time utilities. -""" +"""Date/Time utilities.""" __all__ = ["parse_http_date", "parse_iso_8601_ts", "discord_epoch_to_datetime", "unix_epoch_to_ts"] import datetime @@ -27,15 +25,16 @@ def parse_http_date(date_str: str) -> datetime.datetime: - """ - Return the HTTP date as a datetime object. + """Return the HTTP date as a datetime object. - Args: - date_str: - The RFC-2822 (section 3.3) compliant date string to parse. + Parameters + ---------- + date_str : :obj:`str` + The RFC-2822 (section 3.3) compliant date string to parse. - See: - https://www.ietf.org/rfc/rfc2822.txt + See also + -------- + ``_ """ return email.utils.parsedate_to_datetime(date_str) @@ -48,11 +47,16 @@ def parse_http_date(date_str: str) -> datetime.datetime: def parse_iso_8601_ts(date_string: str) -> datetime.datetime: - """ - Parses an ISO 8601 date string into a datetime object + """Parses an ISO 8601 date string into a datetime object - See: - https://en.wikipedia.org/wiki/ISO_8601 + Parameters + ---------- + date_string : :obj:`str` + The ISO 8601 compliant date string to parse. + + See also + -------- + ``_ """ year, month, day = map(int, ISO_8601_DATE_PART.findall(date_string)[0]) @@ -79,25 +83,33 @@ def parse_iso_8601_ts(date_string: str) -> datetime.datetime: DISCORD_EPOCH = 1_420_070_400 -def discord_epoch_to_datetime(epoch) -> datetime.datetime: - """ - Args: - epoch: - Number of milliseconds since 1/1/2015 (UTC) +def discord_epoch_to_datetime(epoch: int) -> datetime.datetime: + """Parses a discord epoch into a datetime object - Returns: + Parameters + ---------- + epoch : :obj:`int` + Number of milliseconds since 1/1/2015 (UTC) + + Returns + ------- + :obj:`datetime.datetime` Number of seconds since 1/1/1970 within a datetime object (UTC). """ return datetime.datetime.fromtimestamp(epoch / 1000 + DISCORD_EPOCH, datetime.timezone.utc) -def unix_epoch_to_ts(epoch) -> datetime.datetime: - """ - Args: - epoch: - Number of milliseconds since 1/1/1970 (UTC) +def unix_epoch_to_ts(epoch: int) -> datetime.datetime: + """Parses a unix epoch to a datetime object + + Parameters + ---------- + epoch : :obj:`int` + Number of milliseconds since 1/1/1970 (UTC) - Returns: + Returns + ------- + :obj:`datetime.datetime` Number of seconds since 1/1/1970 within a datetime object (UTC). """ return datetime.datetime.fromtimestamp(epoch / 1000, datetime.timezone.utc) diff --git a/hikari/internal_utilities/loggers.py b/hikari/internal_utilities/loggers.py index 792533b2ae..4b29ae5162 100644 --- a/hikari/internal_utilities/loggers.py +++ b/hikari/internal_utilities/loggers.py @@ -16,9 +16,7 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . -""" -Utilities for creating and naming loggers in this library in a consistent way. -""" +"""Utilities for creating and naming loggers in this library in a consistent way.""" __all__ = ["get_named_logger"] import logging @@ -26,8 +24,7 @@ def get_named_logger(obj: typing.Any, *extra_objs: typing.Any) -> logging.Logger: - """ - Builds an appropriately named logger. + """Builds an appropriately named logger. If the passed object is an instance of a class, the class is used instead. @@ -35,14 +32,17 @@ def get_named_logger(obj: typing.Any, *extra_objs: typing.Any) -> logging.Logger If a string is provided, then the string is used as the name. This is not recommended. - Args: - obj: - the object to study to produce a logger for. - extra_objs: - optional extra components to add to the end of the logger name. - - Returns: - a created logger. + Parameters + ---------- + obj + The object to study to produce a logger for. + extra_objs + optional extra components to add to the end of the logger name. + + Returns + ------- + :obj:`logging.Logger` + A created logger. """ if not isinstance(obj, str): if not isinstance(obj, type): diff --git a/hikari/internal_utilities/singleton_meta.py b/hikari/internal_utilities/singleton_meta.py index 2738eac8d8..f1e4f049f5 100644 --- a/hikari/internal_utilities/singleton_meta.py +++ b/hikari/internal_utilities/singleton_meta.py @@ -16,24 +16,32 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . +"""Singleton metaclass""" __all__ = ["SingletonMeta"] class SingletonMeta(type): - """ - Metaclass that makes the class a singleton. Once an instance has been defined at runtime, it will - exist until the interpreter that created it is terminated. + """Metaclass that makes the class a singleton. + + Once an instance has been defined at runtime, it will exist until the interpreter + that created it is terminated. + + Example + -------- + .. code-block:: python - >>> class Unknown(metaclass=SingletonMeta): - ... def __init__(self): - ... print("Initialized an Unknown!") - >>> Unknown() is Unknown() # True + >>> class Unknown(metaclass=SingletonMeta): + ... def __init__(self): + ... print("Initialized an Unknown!") + >>> Unknown() is Unknown() # True - Note: - the constructors of these classes must not take any arguments other than `self`. + Note + ---- + The constructors of these classes must not take any arguments other than ``self``. - Warning: - this is not thread safe. + Warning + ------- + This is not thread safe. """ ___instances___ = {} diff --git a/hikari/internal_utilities/storage.py b/hikari/internal_utilities/storage.py index 75c31f6cbd..97d833f587 100644 --- a/hikari/internal_utilities/storage.py +++ b/hikari/internal_utilities/storage.py @@ -16,24 +16,27 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . -""" -IO utilities. -""" +"""IO utilities.""" __all__ = ["make_resource_seekable", "FileLikeT", "BytesLikeT"] import io import typing -def make_resource_seekable(resource) -> typing.Union[io.BytesIO, io.StringIO]: - """ - Given some representation of data, make a seekable resource to use. This supports bytes, bytearray, memoryview, - and strings. Anything else is just returned. +def make_resource_seekable(resource: typing.Any) -> typing.Union[io.BytesIO, io.StringIO]: + """Given some representation of data, make a seekable resource to use. + + This supports :obj:`bytes`, :obj:`bytearray`, :obj:`memoryview`, and :obj:`str`. + Anything else is just returned. + + Parameters + ---------- + resource : :obj:`typing.Any` + The resource to check. - Args: - resource: - the resource to check. - Returns: + Returns + ------- + :obj:`typing.Union` [ :obj:`io.BytesIO`, :obj:`io.StringIO` ] An stream-compatible resource where possible. """ if isinstance(resource, (bytes, bytearray)): @@ -46,7 +49,7 @@ def make_resource_seekable(resource) -> typing.Union[io.BytesIO, io.StringIO]: return resource -#: A bytes-like object, such as a :class:`str`, raw :class:`bytes`, or view across a bytes-like object. +#: A bytes-like object, such as a :obj:`str`, raw :obj:`bytes`, or view across a bytes-like object. BytesLikeT = typing.Union[bytes, bytearray, memoryview, str, io.StringIO, io.BytesIO] #: Type description for any object that can be considered to be file-like. diff --git a/hikari/internal_utilities/transformations.py b/hikari/internal_utilities/transformations.py index 35078a33fd..eaf41c36d1 100644 --- a/hikari/internal_utilities/transformations.py +++ b/hikari/internal_utilities/transformations.py @@ -16,9 +16,7 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . -""" -Basic transformation utilities. -""" +"""Basic transformation utilities.""" __all__ = [ "CastInputT", "CastOutputT", @@ -44,8 +42,8 @@ def nullable_cast(value: CastInputT, cast: TypeCastT) -> ResultT: """ - Attempts to cast the given `value` with the given `cast`, but only if the `value` is - not `None`. If it is `None`, then `None` is returned instead. + Attempts to cast the given ``value`` with the given ``cast``, but only if the + ``value`` is not ``None``. If it is ``None``, then ``None`` is returned instead. """ if value is None: return None @@ -53,9 +51,10 @@ def nullable_cast(value: CastInputT, cast: TypeCastT) -> ResultT: def try_cast(value: CastInputT, cast: TypeCastT, default: DefaultT = None) -> ResultT: - """ - Try to cast the given value to the given cast. If it throws a :class:`Exception` or derivative, it will - return `default` instead of the cast value instead. + """Try to cast the given value to the given cast. + + If it throws a :obj:`Exception` or derivative, it will return ``default`` instead + of the cast value instead. """ with contextlib.suppress(Exception): return cast(value) @@ -68,18 +67,18 @@ def put_if_specified( value: typing.Any, type_after: typing.Optional[TypeCastT] = None, ) -> None: - """ - Add a value to the mapping under the given key as long as the value is not :obj:`typing.Literal[...]` - - Args: - mapping: - The mapping to add to. - key: - The key to add the value under. - value: - The value to add. - type_after: - Optional type to apply to the value when added. + """Add a value to the mapping under the given key as long as the value is not :obj:`typing.Literal` + + Parameters + ---------- + mapping : :obj:`typing.Dict` [ :obj:`typing.Hashable`, :obj:`typing.Any` ] + The mapping to add to. + key : :obj:`typing.Hashable` + The key to add the value under. + value : :obj:`typing.Any` + The value to add. + type_after : :obj:`TypeCastT`, optional + Type to apply to the value when added. """ if value is not ...: if type_after: @@ -88,23 +87,27 @@ def put_if_specified( mapping[key] = value -def image_bytes_to_image_data(img_bytes: bytes) -> typing.Optional[str]: - """ - Encode image bytes into an image data string. +def image_bytes_to_image_data(img_bytes: typing.Optional[bytes] = None) -> typing.Optional[str]: + """Encode image bytes into an image data string. - Args: - img_bytes: - The image bytes or `None`. + Parameters + ---------- + img_bytes : :obj:`bytes`, optional + The image bytes. - Raises: - ValueError: - If the image type passed is not supported. + Raises + ------ + :obj:`ValueError` + If the image type passed is not supported. - Returns: - The image_bytes given encoded into an image data string or `None`. + Returns + ------- + :obj:`str`, optional + The ``image_bytes`` given encoded into an image data string or ``None``. - Note: - Supported image types: .png, .jpeg, .jfif, .gif, .webp + Note + ---- + Supported image types: ``.png``, ``.jpeg``, ``.jfif``, ``.gif``, ``.webp`` """ if img_bytes is None: return None diff --git a/hikari/net/__init__.py b/hikari/net/__init__.py index 020c0426dd..d277291605 100644 --- a/hikari/net/__init__.py +++ b/hikari/net/__init__.py @@ -19,8 +19,7 @@ """Network components for the Hikari Discord API. These components describe the low level parts of Hikari. No model classes exist -for these; the majority of communication is done via JSON arrays and objects -(:class:`dict` and :class:`list` objects). +for these; the majority of communication is done via JSON arrays and objects. """ from hikari.net import base_http_client from hikari.net import codes diff --git a/hikari/net/base_http_client.py b/hikari/net/base_http_client.py index d2468173dd..c4e486e2d8 100644 --- a/hikari/net/base_http_client.py +++ b/hikari/net/base_http_client.py @@ -16,10 +16,8 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . -""" -Provides a base utility class for any component needing an HTTP session that supports -proxying, SSL configuration, and a standard easy-to-use interface. -""" +"""Provides a base utility class for any component needing an HTTP session +that supports proxying, SSL configuration, and a standard easy-to-use interface.""" __all__ = ["BaseHTTPClient"] import abc @@ -37,27 +35,32 @@ class BaseHTTPClient(abc.ABC): - """ - Base utility class for any component which uses an HTTP session. Each instance represents a - session. This class handles consuming and managing optional settings such as retries, proxies, - and SSL customisation if desired. + """Base utility class for any component which uses an HTTP session. + + Each instance represents a session. This class handles consuming and managing + optional settings such as retries, proxies, and SSL customisation if desired. + Examples + -------- This can be used in a context manager: - >>> class HTTPClientImpl(BaseHTTPClient): - ... def __init__(self, *args, **kwargs): - ... super().__init__(*args, **kwargs) - ... def request(self, *args, **kwargs): - ... return super()._request(*args, **kwargs) + .. code-block:: python + + >>> class HTTPClientImpl(BaseHTTPClient): + ... def __init__(self, *args, **kwargs): + ... super().__init__(*args, **kwargs) + ... def request(self, *args, **kwargs): + ... return super()._request(*args, **kwargs) - >>> async with HTTPClientImpl() as client: - ... async with client.request("GET", "https://some-websi.te") as resp: - ... resp.raise_for_status() - ... body = await resp.read() + >>> async with HTTPClientImpl() as client: + ... async with client.request("GET", "https://some-websi.te") as resp: + ... resp.raise_for_status() + ... body = await resp.read() - Warning: - This must be initialized within a coroutine while an event loop is active - and registered to the current thread. + Warning + ------- + This must be initialized within a coroutine while an event loop is active + and registered to the current thread. """ DELETE = "delete" @@ -71,7 +74,6 @@ class BaseHTTPClient(abc.ABC): "client_session", "in_count", "logger", - "max_retries", "proxy_auth", "proxy_headers", "proxy_url", @@ -81,66 +83,68 @@ class BaseHTTPClient(abc.ABC): "verify_ssl", ) - #: Whether to allow following of redirects or not. Generally you do not want this. + #: Whether to allow following of redirects or not. Generally you do not want this #: as it poses a security risk. #: - #: :type: :class:`bool` + #: :type: :obj:`bool` allow_redirects: bool #: The underlying client session used to make low level HTTP requests. #: - #: :type: :class:`aiohttp.ClientSession` + #: :type: :obj:`aiohttp.ClientSession` client_session: aiohttp.ClientSession #: The number of requests that have been made. This acts as a unique ID for each request. #: - #: :type: :class:`int` + #: :type: :obj:`int` in_count: int #: The logger used to write log messages. #: - #: :type: :class:`logging.Logger` + #: :type: :obj:`logging.Logger` logger: logging.Logger #: The asyncio event loop being used. #: - #: :type: :class:`asyncio.AbstractEventLoop` + #: :type: :obj:`asyncio.AbstractEventLoop` loop: asyncio.AbstractEventLoop #: Proxy authorization info. #: - #: :type: :class:`aiohttp.BasicAuth` or `None` + #: :type: :obj:`aiohttp.BasicAuth`, optional proxy_auth: typing.Optional[aiohttp.BasicAuth] #: Proxy headers. #: - #: :type: :class:`aiohttp.typedefs.LooseHeaders` or `None` + #: :type: :obj:`aiohttp.typedefs.LooseHeaders`, optional proxy_headers: typing.Optional[aiohttp.typedefs.LooseHeaders] #: Proxy URL to use. #: - #: :type: :class:`str` or `None` + #: :type: :obj:`str`, optional proxy_url: typing.Optional[str] #: SSL context to use. #: - #: :type: :class:`ssl.SSLContext` or `None` + #: :type: :obj:`ssl.SSLContext`, optional ssl_context: typing.Optional[ssl.SSLContext] - #: Response timeout. + #: Response timeout or``None`` if you are using the + #: default for :mod:`aiohttp`. #: - #: :type: :class:`float` or `None` if using the default for `aiohttp`. + #: :type: :obj:`float`, optional timeout: typing.Optional[float] #: The user agent being used. #: - #: Warning: - #: Certain areas of the Discord API may enforce specific user agents - #: to be used for requests. You should not overwrite this generated value - #: unless you know what you are doing. Invalid useragents may lead to - #: bot account deauthorization. + #: Warning + #: ------- + #: Certain areas of the Discord API may enforce specific user agents + #: to be used for requests. You should not overwrite this generated value + #: unless you know what you are doing. Invalid useragents may lead to + #: bot account deauthorization. #: - #: :type: :class:`str` + #: :type: :obj:`str` user_agent: str #: Whether to verify SSL certificates or not. Generally you want this turned on @@ -149,7 +153,7 @@ class BaseHTTPClient(abc.ABC): #: stuck behind a proxy that cannot verify the certificates correctly, or are #: having other SSL-related issues, you may wish to turn this off. #: - #: :type: :class:`bool` + #: :type: :obj:`bool` verify_ssl: bool @abc.abstractmethod @@ -157,109 +161,111 @@ def __init__( self, *, allow_redirects: bool = False, - json_serialize: typing.Callable = None, - connector: aiohttp.BaseConnector = None, - proxy_headers: aiohttp.typedefs.LooseHeaders = None, - proxy_auth: aiohttp.BasicAuth = None, - proxy_url: str = None, - ssl_context: ssl.SSLContext = None, + json_serialize: typing.Optional[typing.Callable] = None, + connector: typing.Optional[aiohttp.BaseConnector] = None, + proxy_headers: typing.Optional[aiohttp.typedefs.LooseHeaders] = None, + proxy_auth: typing.Optional[aiohttp.BasicAuth] = None, + proxy_url: typing.Optional[str] = None, + ssl_context: typing.Optional[ssl.SSLContext] = None, verify_ssl: bool = True, - timeout: float = None, + timeout: typing.Optional[float] = None, ) -> None: """ - Args: - allow_redirects: - defaults to False for security reasons. If you find you are receiving multiple redirection responses - causing requests to fail, it is probably worth enabling this. - connector: - the :class:`aiohttp.BaseConnector` to use for the client session, or `None` if you wish to use the - default instead. - json_serialize: - a callable that consumes a Python object and returns a JSON-encoded string. - This defaults to :func:`json.dumps`. - proxy_auth: - optional proxy authentication to use. - proxy_headers: - optional proxy headers to pass. - proxy_url: - optional proxy URL to use. - ssl_context: - optional SSL context to use. - verify_ssl: - defaulting to True, setting this to false will disable SSL verification. - timeout: - optional timeout to apply to individual HTTP requests. + Parameters + ---------- + allow_redirects : :obj:`bool` + If you find you are receiving multiple redirection responses causing + requests to fail, it is probably worth enabling this. Defaults to ``False`` + for security reasons. + json_serialize : :obj:`typing.Callable`, optional + A callable that consumes a Python object and returns a JSON-encoded string. + This defaults to :func:`json.dumps`. + connector : :obj:`aiohttp.BaseConnector`, optional + The :obj:`aiohttp.BaseConnector` to use for the client session, or ``None`` + if you wish to use the default instead. + proxy_headers : :obj:`aiohttp.typedefs.LooseHeaders`, optional + Proxy headers to pass. + proxy_auth : :obj:`aiohttp.BasicAuth`, optional + Proxy authentication to use. + proxy_url : :obj:`str`, optional + Proxy URL to use. + ssl_context : :obj:`ssl.SSLContext`, optional + SSL context to use. + verify_ssl : :obj:`bool` + Wheather to verify SSL. + timeout : :obj:`float`, optional + Timeout to apply to individual HTTP requests. """ #: Whether to allow redirects or not. #: - #: :type: :class:`bool` + #: :type: :obj:`bool` self.allow_redirects = allow_redirects #: The HTTP client session to use. #: - #: :type: :class:`aiohttp.ClientSession` + #: :type: :obj:`aiohttp.ClientSession` self.client_session = aiohttp.ClientSession( connector=connector, version=aiohttp.HttpVersion11, json_serialize=json_serialize or json.dumps, ) #: The logger to use for this object. #: - #: :type: :class:`logging.Logger` - + #: :type: :obj:`logging.Logger` self.logger = loggers.get_named_logger(self) + #: User agent to use. #: - #: :type: :class:`str` + #: :type: :obj:`str` self.user_agent = user_agent.user_agent() - #: If `true`, this will enforce SSL signed certificate verification, otherwise it will + #: If ``True``, this will enforce SSL signed certificate verification, otherwise it will #: ignore potentially malicious SSL certificates. #: - #: :type: :class:`bool` + #: :type: :obj:`bool` self.verify_ssl = verify_ssl #: Optional proxy URL to use for HTTP requests. #: - #: :type: :class:`str` + #: :type: :obj:`str` self.proxy_url = proxy_url #: Optional authorization to use if using a proxy. #: - #: :type: :class:`aiohttp.BasicAuth` + #: :type: :obj:`aiohttp.BasicAuth` self.proxy_auth = proxy_auth #: Optional proxy headers to pass. #: - #: :type: :class:`aiohttp.typedefs.LooseHeaders` + #: :type: :obj:`aiohttp.typedefs.LooseHeaders` self.proxy_headers = proxy_headers #: Optional SSL context to use. #: - #: :type: :class:`ssl.SSLContext` + #: :type: :obj:`ssl.SSLContext` self.ssl_context: ssl.SSLContext = ssl_context #: Optional timeout for HTTP requests. #: - #: :type: :class:`float` + #: :type: :obj:`float` self.timeout = timeout #: How many responses have been received. #: - #: :type: :class:`int` + #: :type: :obj:`int` self.in_count = 0 def _request(self, method, uri, **kwargs): - """ - Calls :meth:`aiohttp.ClientSession.request` and returns the context manager result. - - Args: - method: - The HTTP method to use. - uri: - The URI to send to. - **kwargs: - Any other parameters to pass to the `request` method when invoking it. + """Calls :func:`aiohttp.ClientSession.request` and returns the context manager result. + + Parameters + ---------- + method + The HTTP method to use. + uri + The URI to send to. + **kwargs + Any other parameters to pass to :func:`aiohttp.ClientSession.request` when invoking it. """ return self.client_session.request( method, diff --git a/hikari/net/codes.py b/hikari/net/codes.py index 494359a3cc..9b2338288c 100644 --- a/hikari/net/codes.py +++ b/hikari/net/codes.py @@ -16,9 +16,7 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . -""" -Enumerations for opcodes and status codes. -""" +"""Enumerations for opcodes and status codes.""" __all__ = ["HTTPStatusCode", "GatewayCloseCode", "GatewayOpcode", "JSONErrorCode", "GatewayIntent"] import enum @@ -27,9 +25,7 @@ # Doesnt work correctly with enums, so since this file is all enums, ignore # pylint: disable=no-member class HTTPStatusCode(enum.IntEnum): - """HTTP status codes that a conforming HTTP server should give us on - Discord. - """ + """HTTP status codes that a conforming HTTP server should give us on Discord.""" CONTINUE = 100 @@ -70,7 +66,6 @@ class GatewayCloseCode(enum.IntEnum): Notes ----- - Any codes greater than or equal to `4000` are server-side codes. Any codes between `1000` and `1999` inclusive are generally client-side codes. """ @@ -211,28 +206,32 @@ class JSONErrorCode(enum.IntEnum): #: Bots cannot use this endpoint #: - #: Note: - #: You should never expect to receive this in normal API usage. + #: Note + #: ---- + #: You should never expect to receive this in normal API usage. USERS_ONLY = 20_001 #: Only bots can use this endpoint. #: - #: Note: - #: You should never expect to receive this in normal API usage. + #: Note + #: ---- + #: You should never expect to receive this in normal API usage. BOTS_ONLY = 20_002 #: Maximum number of guilds reached (100) #: - #: Note: - #: You should never expect to receive this in normal API usage as this only applies to user accounts. - #: This is unlimited for bot accounts. + #: Note + #: ---- + #: You should never expect to receive this in normal API usage as this only applies to user accounts. + #: This is unlimited for bot accounts. MAX_GUILDS_REACHED = 30_001 #: Maximum number of friends reached (1000) #: - #: Note: - #: You should never expect to receive this in normal API usage as this only applies to user accounts. - #: Bots cannot have friends. + #: Note + #: ---- + #: You should never expect to receive this in normal API usage as this only applies to user accounts. + #: Bots cannot have friends :( . MAX_FRIENDS_REACHED = 30_002 #: Maximum number of pins reached (50) @@ -349,11 +348,11 @@ class GatewayIntent(enum.IntFlag): Notes ----- Discord now places limits on certain events you can receive without whitelisting your bot first. On the - `Bot` tab in the developer's portal for your bot, you should now have the option to enable functionality + ``Bot`` tab in the developer's portal for your bot, you should now have the option to enable functionality for receiving these events. If you attempt to request an intent type that you have not whitelisted your bot for, you will be - disconnected on startup with a `4014` closure code. + disconnected on startup with a ``4014`` closure code. """ #: Subscribes to the following events: diff --git a/hikari/net/errors.py b/hikari/net/errors.py index 5fa5e32a01..5f906647e6 100644 --- a/hikari/net/errors.py +++ b/hikari/net/errors.py @@ -16,9 +16,7 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . -""" -Errors that can be raised by networking components. -""" +"""Errors that can be raised by networking components.""" __all__ = [ "GatewayError", "GatewayClientClosedError", @@ -75,7 +73,7 @@ class GatewayClientClosedError(GatewayError): A string explaining the issue. """ - def __init__(self, reason="The gateway client has been closed") -> None: + def __init__(self, reason: str = "The gateway client has been closed") -> None: super().__init__(reason) @@ -84,16 +82,18 @@ class GatewayServerClosedConnectionError(GatewayError): Parameters ---------- - close_code : :obj:`hikari.net.codes.GatewayCloseCode` or :obj:`int` or :obj:`None` + close_code : :obj:`hikari.net.codes.GatewayCloseCode`, :obj:`int`, optional The close code provided by the server, if there was one. - reason : :obj:`str` + reason : :obj:`str`, optional A string explaining the issue. """ close_code: typing.Union[codes.GatewayCloseCode, int, None] def __init__( - self, close_code: typing.Union[codes.GatewayCloseCode, int, None] = None, reason: typing.Optional[str] = None + self, + close_code: typing.Optional[typing.Union[codes.GatewayCloseCode, int]] = None, + reason: typing.Optional[str] = None, ) -> None: if reason is None: try: @@ -169,7 +169,7 @@ class GatewayZombiedError(GatewayClientClosedError): """An exception raised if a shard becomes zombied. This means that Discord is no longer responding to us, and we have - disconnected due to a timeout + disconnected due to a timeout. """ def __init__(self) -> None: @@ -208,13 +208,13 @@ class CodedHTTPError(HTTPError): The HTTP route that was being invoked when this exception occurred. message : :obj:`str`, optional An optional message if provided in the response payload. - json_code : :obj:`hikari.net.codes.JSONErrorCode` or :obj:`int`, optional + json_code : :obj:`hikari.net.codes.JSONErrorCode`, :obj:`int`, optional An optional error code the server provided us. """ #: The HTTP status code that was returned by the server. #: - #: :type: :obj:`int` or :obj:`int` or :obj:`hikari.net.codes.HTTPStatusCode` + #: :type: :obj:`int` or :obj:`hikari.net.codes.HTTPStatusCode` status: typing.Union[int, codes.HTTPStatusCode] #: The HTTP route that was being invoked when this exception occurred. @@ -225,14 +225,14 @@ class CodedHTTPError(HTTPError): #: An optional contextual message the server provided us with in the #: response body. # - #: :type: :obj: `str`, optional + #: :type: :obj:`str`, optional message: typing.Optional[str] #: An optional contextual error code the server provided us with in the #: response body. # #: :type: :obj:`hikari.net.codes.JSONErrorCode` or :obj:`int`, optional - json_code: typing.Union[codes.JSONErrorCode, int, None] + json_code: typing.Optional[typing.Union[codes.JSONErrorCode, int]] def __init__( self, @@ -286,7 +286,7 @@ class BadRequestHTTPError(CodedHTTPError): The HTTP route that was being invoked when this exception occurred. message : :obj:`str`, optional An optional message if provided in the response payload. - json_code : :obj:`hikari.net.codes.JSONErrorCode` or :obj:`int`, optional + json_code : :obj:`hikari.net.codes.JSONErrorCode`, :obj:`int`, optional An optional error code the server provided us. """ @@ -294,7 +294,7 @@ def __init__( self, route: routes.CompiledRoute, message: typing.Optional[str], - json_code: typing.Union[codes.JSONErrorCode, int, None], + json_code: typing.Optional[typing.Union[codes.JSONErrorCode, int]], ) -> None: super().__init__(codes.HTTPStatusCode.BAD_REQUEST, route, message, json_code) @@ -311,7 +311,7 @@ class UnauthorizedHTTPError(ClientHTTPError): The HTTP route that was being invoked when this exception occurred. message : :obj:`str`, optional An optional message if provided in the response payload. - json_code : :obj:`hikari.net.codes.JSONErrorCode` or :obj:`int`, optional + json_code : :obj:`hikari.net.codes.JSONErrorCode`, :obj:`int`, optional An optional error code the server provided us. """ @@ -319,7 +319,7 @@ def __init__( self, route: routes.CompiledRoute, message: typing.Optional[str], - json_code: typing.Union[codes.JSONErrorCode, int, None], + json_code: typing.Optional[typing.Union[codes.JSONErrorCode, int]], ) -> None: super().__init__(codes.HTTPStatusCode.UNAUTHORIZED, route, message, json_code) @@ -339,7 +339,7 @@ class ForbiddenHTTPError(ClientHTTPError): The HTTP route that was being invoked when this exception occurred. message : :obj:`str`, optional An optional message if provided in the response payload. - json_code : :obj:`hikari.net.codes.JSONErrorCode` or :obj:`int`, optional + json_code : :obj:`hikari.net.codes.JSONErrorCode`, :obj:`int`, optional An optional error code the server provided us. """ @@ -347,7 +347,7 @@ def __init__( self, route: routes.CompiledRoute, message: typing.Optional[str], - json_code: typing.Union[codes.JSONErrorCode, int, None], + json_code: typing.Optional[typing.Union[codes.JSONErrorCode, int]], ) -> None: super().__init__(codes.HTTPStatusCode.FORBIDDEN, route, message, json_code) @@ -366,7 +366,7 @@ class NotFoundHTTPError(ClientHTTPError): The HTTP route that was being invoked when this exception occurred. message : :obj:`str`, optional An optional message if provided in the response payload. - json_code : :obj:`hikari.net.codes.JSONErrorCode` or :obj:`int`, optional + json_code : :obj:`hikari.net.codes.JSONErrorCode`, :obj:`int`, optional An optional error code the server provided us. """ @@ -374,6 +374,6 @@ def __init__( self, route: routes.CompiledRoute, message: typing.Optional[str], - json_code: typing.Union[codes.JSONErrorCode, int, None], + json_code: typing.Optional[typing.Union[codes.JSONErrorCode, int]], ) -> None: super().__init__(codes.HTTPStatusCode.NOT_FOUND, route, message, json_code) diff --git a/hikari/net/gateway.py b/hikari/net/gateway.py index 69ccab579d..faa8307a3a 100644 --- a/hikari/net/gateway.py +++ b/hikari/net/gateway.py @@ -16,17 +16,19 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . -""" -Single-threaded asyncio V6 Gateway implementation. Handles regular heartbeating in a background task +"""Single-threaded asyncio Gateway implementation. + +Handles regular heartbeating in a background task on the same event loop. Implements zlib transport compression only. Can be used as the main gateway connection for a single-sharded bot, or the gateway connection for a specific shard in a swarm of shards making up a larger bot. -References: - - IANA WS closure code standards: https://www.iana.org/assignments/websocket/websocket.xhtml - - Gateway documentation: https://discordapp.com/developers/docs/topics/gateway - - Opcode documentation: https://discordapp.com/developers/docs/topics/opcodes-and-status-codes +See also +-------- +* IANA WS closure code standards: https://www.iana.org/assignments/websocket/websocket.xhtml +* Gateway documentation: https://discordapp.com/developers/docs/topics/gateway +* Opcode documentation: https://discordapp.com/developers/docs/topics/opcodes-and-status-codes """ __all__ = ["GatewayStatus", "GatewayClient"] @@ -55,9 +57,7 @@ class GatewayStatus(str, enum.Enum): - """ - Various states that a gateway connection can be in. - """ + """Various states that a gateway connection can be in.""" OFFLINE = "offline" CONNECTING = "connecting" @@ -79,18 +79,18 @@ class GatewayClient: application of events that occur, and to allow you to change your presence, amongst other real-time applications. - Each :class:`GatewayClient` represents a single shard. + Each :obj:`GatewayClient` represents a single shard. Expected events that may be passed to the event dispatcher are documented in the `gateway event reference `_. No normalization of the gateway event names occurs. In addition to this, a few internal events can also be triggered to notify you of changes to the connection state. - * `CONNECT` - fired on initial connection to Discord. - * `RECONNECT` - fired if we have previously been connected to Discord - but are making a new - connection on an existing :class:`GatewayClient` instance. - * `DISCONNECT` - fired when the connection is closed for any reason. + + * ``CONNECT`` - fired on initial connection to Discord. + * ``RECONNECT`` - fired if we have previously been connected to Discord + but are making a new connection on an existing :obj:`GatewayClient` instance. + * ``DISCONNECT`` - fired when the connection is closed for any reason. Parameters ---------- @@ -99,7 +99,7 @@ class GatewayClient: If False, no payloads are compressed. You usually want to keep this enabled. connector: :obj:`aiohttp.BaseConnector`, optional - The :class:`aiohttp.BaseConnector` to use for the HTTP session that + The :obj:`aiohttp.BaseConnector` to use for the HTTP session that gets upgraded to a websocket connection. You can use this to customise connection pooling, etc. debug: :obj:`bool` @@ -110,11 +110,11 @@ class GatewayClient: dispatch: dispatch function The function to invoke with any dispatched events. This must not be a coroutine function, and must take three arguments only. The first is - the reference to this :class:`GatewayClient` The second is the + the reference to this :obj:`GatewayClient` The second is the event name. - initial_presence: :obj:`dict`, optional - A raw JSON object as a :class:`dict` that should be set as the initial - presence of the bot user once online. If `None`, then it will be set to + initial_presence: :obj:`typing.Dict`, optional + A raw JSON object as a :obj:`typing.Dict` that should be set as the initial + presence of the bot user once online. If ``None``, then it will be set to the default, which is showing up as online without a custom status message. intents: :obj:`hikari.net.codes.GatewayIntent`, optional @@ -132,31 +132,31 @@ class GatewayClient: sent automatically, and must manually request that member chunks be sent using :meth:`request_member_chunks`. proxy_auth: :obj:`aiohttp.BasicAuth`, optional - Optional :class:`aiohttp.BasicAuth` object that can be provided to - allow authenticating with a proxy if you use one. Leave `None` to + Optional :obj:`aiohttp.BasicAuth` object that can be provided to + allow authenticating with a proxy if you use one. Leave ``None`` to ignore. proxy_headers: :obj:`aiohttp.typedefs.LooseHeaders`, optional - Optional :class:`aiohttp.typedefs.LooseHeaders` to provide as headers - to allow the connection through a proxy if you use one. Leave `None` + Optional :obj:`aiohttp.typedefs.LooseHeaders` to provide as headers + to allow the connection through a proxy if you use one. Leave ``None`` to ignore. proxy_url: :obj:`str`, optional - Optional :class:`str` to use for a proxy server. If `None`, then it + Optional :obj:`str` to use for a proxy server. If ``None``, then it is ignored. session_id: :obj:`str`, optional - The session ID to use. If specified along with a `seq`, then the - gateway client will attempt to RESUME an existing session rather than - re-IDENTIFY. Otherwise, it will be ignored. + The session ID to use. If specified along with ``seq``, then the + gateway client will attempt to ``RESUME`` an existing session rather than + re-``IDENTIFY``. Otherwise, it will be ignored. seq: :obj:`int`, optional - The sequence number to use. If specified along with a `session_id`, then - the gateway client will attempt to RESUME an existing session rather - than re-IDENTIFY. Otherwise, it will be ignored. + The sequence number to use. If specified along with ``session_id``, then + the gateway client will attempt to ``RESUME`` an existing session rather + than re-``IDENTIFY``. Otherwise, it will be ignored. shard_id: :obj:`int` - The shard ID of this gateway client. Defaults to 0. + The shard ID of this gateway client. Defaults to ``0``. shard_count: :obj:`int` - The number of shards on this gateway. Defaults to 1, which implies no + The number of shards on this gateway. Defaults to ``1``, which implies no sharding is taking place. ssl_context: :obj:`ssl.SSLContext`, optional - An optional custom :class:`ssl.SSLContext` to provide to customise how + An optional custom :obj:`ssl.SSLContext` to provide to customise how SSL works. token: :obj:`str` The mandatory bot token for the bot account to use, minus the "Bot" @@ -164,7 +164,7 @@ class GatewayClient: url: :obj:`str` The websocket URL to use. verify_ssl: :obj:`bool` - If True, SSL verification is enabled, which is generally what you want. + If ``True``, SSL verification is enabled, which is generally what you want. If you get SSL issues, you can try turning this off at your own risk. version: :obj:`hikari.net.versions.GatewayVersion` The version of the gateway API to use. Defaults to the most recent @@ -213,13 +213,13 @@ class GatewayClient: #: An event that is set when the connection closes. #: - #: :type: :class:`asyncio.Event` + #: :type: :obj:`asyncio.Event` closed_event: asyncio.Event #: The number of times we have disconnected from the gateway on this #: client instance. #: - #: :type: :class:`int` + #: :type: :obj:`int` disconnect_count: int #: The dispatch method to call when dispatching a new event. This is @@ -227,88 +227,88 @@ class GatewayClient: dispatch: DispatchT #: The heartbeat interval Discord instructed the client to beat at. - #: This is `nan` until this information is received. + #: This is ``nan`` until this information is received. #: - #: :type: :class:`float` + #: :type: :obj:`float` heartbeat_interval: float #: The most recent heartbeat latency measurement in seconds. This is - #: `nan` until this information is available. The latency is calculated - #: as the time between sending a `HEARTBEAT` payload and receiving a - #: `HEARTBEAT_ACK` response. + #: ``nan`` until this information is available. The latency is calculated + #: as the time between sending a ``HEARTBEAT`` payload and receiving a + #: ``HEARTBEAT_ACK`` response. #: - #: :type: :class:`float` + #: :type: :obj:`float` heartbeat_latency: float - #: An event that is set when Discord sends a `HELLO` payload. This + #: An event that is set when Discord sends a ``HELLO`` payload. This #: indicates some sort of connection has successfully been made. #: - #: :type: :class:`asyncio.Event` + #: :type: :obj:`asyncio.Event` hello_event: asyncio.Event - #: An event that is set when the client has successfully `IDENTIFY`ed - #: or `RESUMED` with the gateway. This indicates regular communication + #: An event that is set when the client has successfully ``IDENTIFY``ed + #: or ``RESUMED`` with the gateway. This indicates regular communication #: can now take place on the connection and events can be expected to #: be received. #: - #: :type: :class:`asyncio.Event` + #: :type: :obj:`asyncio.Event` identify_event: asyncio.Event - #: The monotonic timestamp that the last `HEARTBEAT` was sent at, or - #: `nan` if no `HEARTBEAT` has yet been sent. + #: The monotonic timestamp that the last ``HEARTBEAT`` was sent at, or + #: ``nan`` if no ``HEARTBEAT`` has yet been sent. #: - #: :type: :class:`float` + #: :type: :obj:`float` last_heartbeat_sent: float #: The monotonic timestamp at which the last payload was received from - #: Discord. If this was more than the :attr:`heartbeat_interval` from + #: Discord. If this was more than the ``heartbeat_interval`` from #: the current time, then the connection is assumed to be zombied and - #: is shut down. If no messages have been received yet, this is `nan`. + #: is shut down. If no messages have been received yet, this is ``nan``. #: - #: :type: :class:`float` + #: :type: :obj:`float` last_message_received: float #: The logger used for dumping information about what this client is doing. #: - #: :type: :class:`logging.Logger` + #: :type: :obj:`logging.Logger` logger: logging.Logger #: An event that is set when something requests that the connection #: should close somewhere. #: - #: :type: :class:`asyncio.Event` + #: :type: :obj:`asyncio.Event` requesting_close_event: asyncio.Event #: The current session ID, if known. #: - #: :type: :class:`str` or `None` + #: :type: :obj:`str`, optional session_id: typing.Optional[str] #: The current sequence number for state synchronization with the API, #: if known. #: - #: :type: :class:`int` or `None`. + #: :type: :obj:`int`, optional. seq: typing.Optional[int] #: The shard ID. #: - #: :type: :class:`int` + #: :type: :obj:`int` shard_id: int #: The number of shards in use for the bot. #: - #: :type: :class:`int` + #: :type: :obj:`int` shard_count: int #: The current status of the gateway. This can be used to print out #: informative context for large sharded bots. #: - #: :type: :class:`GatewayStatus` + #: :type: :obj:`GatewayStatus` status: GatewayStatus #: The API version to use on Discord. #: - #: :type: :class:`hikari.net.versions.GatewayVersion` + #: :type: :obj:`hikari.net.versions.GatewayVersion` version: versions.GatewayVersion def __init__( @@ -391,19 +391,21 @@ def __init__( @property def uptime(self) -> datetime.timedelta: - """ + """The amount of time the connection has been running for. + Returns ------- :obj:`datetime.timedelta` The amount of time the connection has been running for. If it isn't - running, this will always return 0 seconds. + running, this will always return ``0`` seconds. """ delta = time.perf_counter() - self._connected_at return datetime.timedelta(seconds=0 if math.isnan(delta) else delta) @property def is_connected(self) -> bool: - """ + """Wether the gateway is connecter or not. + Returns ------- :obj:`bool` @@ -414,26 +416,31 @@ def is_connected(self) -> bool: @property def intents(self) -> typing.Optional[codes.GatewayIntent]: - """ + """The intents being used. + + If this is ``None``, no intent usage was being + used on this shard. On V6 this would be regular usage as prior to + the intents change in January 2020. If on V7, you just won't be + able to connect at all to the gateway. + Returns ------- :obj:`hikari.net.codes.GatewayIntent`, optional - The intents being used. If this is None, no intent usage was being - used on this shard. On V6 this would be regular usage as prior to - the intents change in January 2020. If on V7, you just won't be - able to connect at all to the gateway. + The intents being used. """ return self._intents @property def reconnect_count(self) -> int: - """ + """The ammount of times the gateway has reconnected since initialization. + + This can be used as a debugging context, but is also used internally + for exception management. + Returns ------- :obj:`int` - The number of times this client has been reconnected since it was - initialized. This can be used as a debugging context, but is also - used internally for exception management. + The ammount of times the gateway has reconnected since initialization. """ # 0 disconnects + not is_connected => 0 # 0 disconnects + is_connected => 0 @@ -445,10 +452,12 @@ def reconnect_count(self) -> int: @property def current_presence(self) -> typing.Dict: - """ + """The current presence of the gateway. + Returns ------- - The current presence for the shard. + :obj:`typing.Dict` + The current presence for the gateway. """ # Make a shallow copy to prevent mutation. return dict(self._presence or {}) @@ -465,7 +474,7 @@ async def request_guild_members(self, guild_id, *guild_ids, **kwargs): """Requests the guild members for a guild or set of guilds. These guilds must be being served by this shard, and the results will be - provided to the dispatcher with `GUILD_MEMBER_CHUNK` events. + provided to the dispatcher with ``GUILD_MEMBER_CHUNK`` events. Parameters ---------- @@ -479,22 +488,22 @@ async def request_guild_members(self, guild_id, *guild_ids, **kwargs): Keyword Args ------------ limit : :obj:`int` - Limit for the number of members to respond with. Set to 0 to be + Limit for the number of members to respond with. Set to ``0`` to be unlimited. query : :obj:`str` An optional string to filter members with. If specified, only members who have a username starting with this string will be returned. - user_ids : `list` [ `str` ] + user_ids : :obj:`typing.Itinerable` [ :obj:`str` ] An optional list of user IDs to return member info about. - Notes - ----- - You may not specify `user_ids` at the same time as `limit` and - `query`. Likewise, if you specify one of `limit` or `query`, the - other must also be included. The default if no optional arguments - are specified is to use a `limit` of `0` and a `query` of - `""` (empty-string). + Note + ---- + You may not specify ``user_ids`` at the same time as ``limit`` and + ``query``. Likewise, if you specify one of ``limit`` or ``query``, + the other must also be included. The default, if no optional arguments + are specified, is to use a ``limit`` of ``0`` and a ``query`` of + ``""`` (empty-string). """ guilds = [guild_id, *guild_ids] constraints = {} @@ -519,7 +528,7 @@ async def update_presence(self, presence: typing.Dict) -> None: Parameters ---------- - presence : :obj:`dict` + presence : :obj:`typing.Dict` The new presence payload to set. """ presence.setdefault("since", None) @@ -537,7 +546,7 @@ async def close(self, close_code: int = 1000) -> None: Parameters ---------- close_code : :obj:`int` - The close code to use. Defaults to `1000` (normal closure). + The close code to use. Defaults to ``1000`` (normal closure). """ if not self.requesting_close_event.is_set(): self.status = GatewayStatus.SHUTTING_DOWN @@ -550,16 +559,14 @@ async def close(self, close_code: int = 1000) -> None: self.closed_event.set() async def connect(self, client_session_type=aiohttp.ClientSession) -> None: - """ - Connect to the gateway and return when it closes (usually with some - form of exception documented in :mod:`hikari.net.errors`. + """Connect to the gateway and return when it closes (usually with some + form of exception documented in :mod:`hikari.net.errors`). Parameters ---------- - client_session_type : :obj:`type` + client_session_type The client session implementation to use. You generally do not want - to change this from the default, which is - :obj:`aiohttp.ClientSession`. + to change this from the default, which is :obj:`aiohttp.ClientSession`. """ if self.is_connected: raise RuntimeError("Already connected") diff --git a/hikari/net/http_client.py b/hikari/net/http_client.py index 19dcdb3f45..7e1842bd5d 100644 --- a/hikari/net/http_client.py +++ b/hikari/net/http_client.py @@ -16,10 +16,7 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . -""" -Implementation of a basic HTTP client that uses aiohttp to interact with the -V6 Discord API. -""" +"""Implementation of a basic HTTP client that uses aiohttp to interact with the Discord API.""" __all__ = ["HTTPClient"] import asyncio @@ -46,9 +43,7 @@ class HTTPClient(base_http_client.BaseHTTPClient): - """ - A RESTful client to allow you to interact with the Discord API. - """ + """A RESTful client to allow you to interact with the Discord API.""" _AUTHENTICATION_SCHEMES = ("Bearer", "Bot") @@ -258,23 +253,29 @@ async def _handle_bad_response( async def get_gateway(self) -> str: """ - Returns: + Returns + ------- + :obj:`str` A static URL to use to connect to the gateway with. - Note: - Users are expected to attempt to cache this result. + Note + ---- + Users are expected to attempt to cache this result. """ result = await self._request(routes.GATEWAY.compile(self.GET)) return result["url"] async def get_gateway_bot(self) -> typing.Dict: """ - Returns: - An object containing a `url` to connect to, an :class:`int` number of shards recommended to use - for connecting, and a `session_start_limit` object. + Returns + ------- + :obj:`typing.Dict` + An object containing a ``url`` to connect to, an :obj:`int` number of shards recommended to use + for connecting, and a ``session_start_limit`` object. - Note: - Unlike `get_gateway`, this requires a valid token to work. + Note + ---- + Unlike :meth:`get_gateway`, this requires a valid token to work. """ return await self._request(routes.GATEWAY_BOT.compile(self.GET)) @@ -286,27 +287,31 @@ async def get_guild_audit_log( action_type: typing.Union[typing.Literal[...], int] = ..., limit: typing.Union[typing.Literal[...], int] = ..., ) -> typing.Dict: - """ - Get an audit log object for the given guild. + """Get an audit log object for the given guild. - Args: - guild_id: - The guild ID to look up. - user_id: - Optional user ID to filter by. - action_type: - Optional action type to look up. - limit: - Optional limit to apply to the number of records. Defaults to 50. Must be between 1 and 100 inclusive. + Parameters + ---------- + guild_id : :obj:`str` + The guild ID to look up. + user_id : :obj:`str` + If specified, the user ID to filter by. + action_type : :obj:`int` + If specified, the action type to look up. + limit : :obj:`int` + If specified, the limit to apply to the number of records. + Defaults to ``50``. Must be between ``1`` and ``100`` inclusive. - Returns: + Returns + ------- + :obj:`typing.Dict` An audit log object. - Raises: - hikari.net.errors.ForbiddenHTTPError: - If you lack the given permissions to view an audit log. - hikari.net.errors.NotFoundHTTPError: - If the guild does not exist. + Raises + ------ + :obj:`hikari.net.errors.ForbiddenHTTPError` + If you lack the given permissions to view an audit log. + :obj:`hikari.net.errors.NotFoundHTTPError` + If the guild does not exist. """ query = {} transformations.put_if_specified(query, "user_id", user_id) @@ -316,21 +321,24 @@ async def get_guild_audit_log( return await self._request(route, query=query) async def get_channel(self, channel_id: str) -> typing.Dict: - """ - Get a channel object from a given channel ID. + """Get a channel object from a given channel ID. - Args: - channel_id: - The channel ID to look up. + Parameters + ---------- + channel_id : :obj:`str` + The channel ID to look up. - Returns: + Returns + ------- + :obj:`typing.Dict` The channel object that has been found. - Raises: - hikari.net.errors.ForbiddenHTTPError: - If the current token doesn't have access to the channel. - hikari.net.errors.NotFoundHTTPError: - If the channel does not exist. + Raises + ------ + :obj:`hikari.net.errors.ForbiddenHTTPError` + If you don't have access to the channel. + :obj:`hikari.net.errors.NotFoundHTTPError` + If the channel does not exist. """ route = routes.CHANNEL.compile(self.GET, channel_id=channel_id) return await self._request(route) @@ -339,6 +347,7 @@ async def modify_channel( # lgtm [py/similar-function] self, channel_id: str, *, + name: typing.Union[typing.Literal[...], str] = ..., position: typing.Union[typing.Literal[...], int] = ..., topic: typing.Union[typing.Literal[...], str] = ..., nsfw: typing.Union[typing.Literal[...], bool] = ..., @@ -349,50 +358,64 @@ async def modify_channel( # lgtm [py/similar-function] parent_id: typing.Union[typing.Literal[...], str] = ..., reason: typing.Union[typing.Literal[...], str] = ..., ) -> typing.Dict: - """ - Update one or more aspects of a given channel ID. - - Args: - channel_id: - The channel ID to update. This must be between 2 and 100 characters in length. - position: - An optional position to change to. - topic: - An optional topic to set. This is only applicable to text channels. This must be between 0 and 1024 - characters in length. - nsfw: - An optional flag to set the channel as NSFW or not. Only applicable to text channels. - rate_limit_per_user: - An optional number of seconds the user has to wait before sending another message. This will - not apply to bots, or to members with `manage_messages` or `manage_channel` permissions. This must be - between 0 and 21600 seconds. This only applies to text channels. - bitrate: - The optional bitrate in bits per second allowable for the channel. This only applies to voice channels - and must be between 8000 and 96000 or 128000 for VIP servers. - user_limit: - The optional max number of users to allow in a voice channel. This must be between 0 and 99 inclusive, - where 0 implies no limit. - permission_overwrites: - An optional list of permission overwrites that are category specific to replace the existing overwrites - with. - parent_id: - The optional parent category ID to set for the channel. - reason: - An optional audit log reason explaining why the change was made. + """Update one or more aspects of a given channel ID. + + Parameters + ---------- + channel_id : :obj:`str` + The channel ID to update. + name : :obj:`str` + If specified, the new name for the channel.This must be + between ``2`` and ``100`` characters in length. + position : :obj:`int` + If specified, the position to change the channel to. + topic : :obj:`str` + If specified, the topic to set. This is only applicable to + text channels. This must be between ``0`` and ``1024`` + characters in length. + nsfw : :obj:`bool` + If specified, wheather the channel will be marked as NSFW. + Only applicable to text channels. + rate_limit_per_user : :obj:`int` + If specified, the number of seconds the user has to wait before sending + another message. This will not apply to bots, or to members with + ``MANAGE_MESSAGES`` or ``MANAGE_CHANNEL`` permissions. This must + be between ``0`` and ``21600`` seconds. + bitrate : :obj:`int` + If specified, the bitrate in bits per second allowable for the channel. + This only applies to voice channels and must be between ``8000`` + and ``96000`` for normal servers or ``8000`` and ``128000`` for + VIP servers. + user_limit : :obj:`int` + If specified, the new max number of users to allow in a voice channel. + This must be between ``0`` and ``99`` inclusive, where + ``0`` implies no limit. + permission_overwrites : :obj:`typing.Sequence` [ :obj:`typing.Dict` ] + If specified, the new list of permission overwrites that are category + specific to replace the existing overwrites with. + parent_id : :obj:`str` + If specified, the new parent category ID to set for the channel. + reason : :obj:`str` + If specified, the audit log reason explaining why the operation + was performed. - Returns: + Returns + ------- + :obj:`typing.Dict` The channel object that has been modified. - Raises: - hikari.net.errors.NotFoundHTTPError: - If the channel does not exist. - hikari.net.errors.ForbiddenHTTPError: - If you lack the permission to make the change. - hikari.net.errors.BadRequestHTTPError: - If you provide incorrect options for the corresponding channel type (e.g. a `bitrate` for a text - channel). + Raises + ------ + :obj:`hikari.net.errors.NotFoundHTTPError` + If the channel does not exist. + :obj:`hikari.net.errors.ForbiddenHTTPError` + If you lack the permission to make the change. + :obj:`hikari.net.errors.BadRequestHTTPError` + If you provide incorrect options for the corresponding channel type + (e.g. a ``bitrate`` for a text channel). """ payload = {} + transformations.put_if_specified(payload, "name", name) transformations.put_if_specified(payload, "position", position) transformations.put_if_specified(payload, "topic", topic) transformations.put_if_specified(payload, "nsfw", nsfw) @@ -405,24 +428,29 @@ async def modify_channel( # lgtm [py/similar-function] return await self._request(route, json_body=payload, reason=reason) async def delete_close_channel(self, channel_id: str) -> None: - """ - Delete the given channel ID, or if it is a DM, close it. - Args: - channel_id: - The channel ID to delete, or the user ID of the direct message to close. + """Delete the given channel ID, or if it is a DM, close it. + + Parameters + ---------- + channel_id : :obj:`str` + The channel ID to delete, or the user ID of the direct message to close. - Returns: - Nothing, unlike what the API specifies. This is done to maintain consistency with other calls of a similar - nature in this API wrapper. + Returns + ------- + ``None`` + Nothing, unlike what the API specifies. This is done to maintain + consistency with other calls of a similar nature in this API wrapper. - Warning: - Deleted channels cannot be un-deleted. Deletion of DMs is able to be undone by reopening the DM. + Raises + ------ + :obj:`hikari.net.errors.NotFoundHTTPError` + If the channel does not exist. + :obj:`hikari.net.errors.ForbiddenHTTPError` + If you do not have permission to delete the channel. - Raises: - hikari.net.errors.NotFoundHTTPError: - If the channel does not exist - hikari.net.errors.ForbiddenHTTPError: - If you do not have permission to delete the channel. + Warning + ------- + Deleted channels cannot be un-deleted. Deletion of DMs is able to be undone by reopening the DM. """ route = routes.CHANNEL.compile(self.DELETE, channel_id=channel_id) await self._request(route) @@ -436,43 +464,51 @@ async def get_channel_messages( before: typing.Union[typing.Literal[...], str] = ..., around: typing.Union[typing.Literal[...], str] = ..., ) -> typing.Sequence[typing.Dict]: - """ - Retrieve message history for a given channel. If a user is provided, retrieve the DM history. - - Args: - channel_id: - The channel ID to retrieve messages from. - limit: - Optional number of messages to return. Must be between 1 and 100 inclusive, and defaults to 50 if - unspecified. - after: - A message ID. If provided, only return messages sent AFTER this message. - before: - A message ID. If provided, only return messages sent BEFORE this message. - around: - A message ID. If provided, only return messages sent AROUND this message. - - Warning: - You can only specify a maximum of one from `before`, `after`, and `around`. Specifying more than one will - cause a :class:`hikari.net.errors.BadRequestHTTPError` to be raised. - - Note: - If you are missing the `VIEW_CHANNEL` permission, you will receive a :class:`hikari.net.errors.ForbiddenHTTPError`. - If you are instead missing the `READ_MESSAGE_HISTORY` permission, you will always receive zero results, and - thus an empty list will be returned instead. - - Returns: + """Retrieve message history for a given channel. + If a user is provided, retrieve the DM history. + + Parameters + ---------- + channel_id : :obj:`str` + The ID of the channel to retrieve the messages from. + limit : :obj:`int` + If specified, the number of messages to return. Must be + between ``1`` and ``100`` inclusive.Defaults to ``50`` + if unspecified. + after : :obj:`str` + A message ID. If specified, only return messages sent AFTER this message. + before : :obj:`str` + A message ID. If specified, only return messages sent BEFORE this message. + around : :obj:`str` + A message ID. If specified, only return messages sent AROUND this message. + + Returns + ------- + :obj:`typing.Sequence` [ :obj:`typing.Dict` ] A list of message objects. - Raises: - hikari.net.errors.ForbiddenHTTPError: - If you lack permission to read the channel. - hikari.net.errors.BadRequestHTTPError: - If your query is malformed, has an invalid value for `limit`, or contains more than one of `after`, - `before` and `around`. - hikari.net.errors.NotFoundHTTPError: - If the given `channel_id` was not found, or the message ID provided for one of the filter arguments - is not found. + Raises + ------ + :obj:`hikari.net.errors.ForbiddenHTTPError` + If you lack permission to read the channel. + :obj:`hikari.net.errors.BadRequestHTTPError` + If your query is malformed, has an invalid value for ``limit``, + or contains more than one of ``after``, ``before`` and ``around``. + :obj:`hikari.net.errors.NotFoundHTTPError` + If the channel is not found, or the message + provided for one of the filter arguments is not found. + + Note + ---- + If you are missing the ``VIEW_CHANNEL`` permission, you will receive a + :obj:`hikari.net.errors.ForbiddenHTTPError`. If you are instead missing + the ``READ_MESSAGE_HISTORY`` permission, you will always receive + zero results, and thus an empty list will be returned instead. + + Warning + ------- + You can only specify a maximum of one from ``before``, ``after``, and ``around``. + Specifying more than one will cause a :obj:`hikari.net.errors.BadRequestHTTPError` to be raised. """ query = {} transformations.put_if_specified(query, "limit", limit) @@ -483,26 +519,30 @@ async def get_channel_messages( return await self._request(route, query=query) async def get_channel_message(self, channel_id: str, message_id: str) -> typing.Dict: - """ - Get the message with the given message ID from the channel with the given channel ID. + """Get the message with the given message ID from the channel with the given channel ID. - Args: - channel_id: - The channel to look in. - message_id: - The message to retrieve. + Parameters + ---------- + channel_id : :obj:`str` + The ID of the channel to get the message from. + message_id : :obj:`str` + The ID of the message to retrieve. - Returns: + Returns + ------- + :obj:`typing.Dict` A message object. - Note: - This requires the `READ_MESSAGE_HISTORY` permission to be set. + Note + ---- + This requires the ``READ_MESSAGE_HISTORY`` permission. - Raises: - hikari.net.errors.ForbiddenHTTPError: - If you lack permission to see the message. - hikari.net.errors.NotFoundHTTPError: - If the message ID or channel ID is not found. + Raises + ------ + :obj:`hikari.net.errors.ForbiddenHTTPError` + If you lack permission to see the message. + :obj:`hikari.net.errors.NotFoundHTTPError` + If the channel or message is not found. """ route = routes.CHANNEL_MESSAGE.compile(self.GET, channel_id=channel_id, message_id=message_id) return await self._request(route) @@ -513,52 +553,62 @@ async def create_message( *, content: typing.Union[typing.Literal[...], str] = ..., nonce: typing.Union[typing.Literal[...], str] = ..., - tts: bool = False, + tts: typing.Union[typing.Literal[...], bool] = ..., files: typing.Union[typing.Literal[...], typing.Sequence[typing.Tuple[str, storage.FileLikeT]]] = ..., embed: typing.Union[typing.Literal[...], typing.Dict] = ..., allowed_mentions: typing.Union[typing.Literal[...], typing.Dict] = ..., ) -> typing.Dict: - """ - Create a message in the given channel or DM. - - Args: - channel_id: - The channel or user ID to send to. - content: - If specified, the message content to send with the message. - nonce: - An optional ID to send for opportunistic message creation. This doesn't serve any real purpose for - general use, and can usually be ignored. - tts: - If `True`, then the message will be sent as a TTS message. - files: - If specified, this should be a list of between 1 and 5 tuples. Each tuple should consist of the - file name, and either raw :class:`bytes` or an :class:`io.IOBase` derived object with a seek that - points to a buffer containing said file. - embed: - If specified, the embed to send with the message. - allowed_mentions: - If specified, the mentions to ping with the message. If not specified, will ping all mentions. - - Raises: - hikari.net.errors.NotFoundHTTPError: - If the channel ID is not found. - hikari.net.errors.BadRequestHTTPError: - This can be raised if the file is too large; if the embed exceeds the defined limits; - if the message content is specified only and empty or greater than 2000 characters; - if neither content, file or embed are specified; if there is a duplicate id in allowed_mention; - if select parse all users/roles mentions but then specify them. - hikari.net.errors.ForbiddenHTTPError: - If you lack permissions to send to this channel. - - Returns: + """Create a message in the given channel or DM. + + Parameters + ---------- + channel_id : :obj:`str` + The channel or user ID to send to. + content : :obj:`str` + If specified, the message content to send with the message. + nonce : :obj:`str` + If specified, an optional ID to send for opportunistic message + creation. This doesn't serve any real purpose for general use, + and can usually be ignored. + tts : :obj:`bool` + If specified, whether the message will be sent as a TTS message. + files : :obj:`typing.Sequence` [ :obj:`typing.Tuple` [ :obj:`str`, :obj:`storage.FileLikeT` ] ] + If specified, this should be a list of between 1 and 5 tuples. + Each tuple should consist of the file name, and either + raw :obj:`bytes` or an :obj:`io.IOBase` derived object with + a seek that points to a buffer containing said file. + embed : :obj:`typing.Dict` + If specified, the embed to send with the message. + allowed_mentions : :obj:`typing.Dict` + If specified, the mentions to parse from the ``content``. + If not specified, will parse all mentions from the ``content``. + + Returns + ------- + :obj:`typing.Dict` The created message object. + + Raises + ------ + :obj:`hikari.net.errors.NotFoundHTTPError` + If the channel is not found. + :obj:`hikari.net.errors.BadRequestHTTPError` + This can be raised if the file is too large; if the embed exceeds + the defined limits; if the message content is specified only and + empty or greater than 2000 characters; if neither content, file + or embed are specified; if there is a duplicate id in only of the + fields in ``allowed_mentions``; if you specify to parse all + users/roles mentions but also specify which users/roles to + parse only. + :obj:`hikari.net.errors.ForbiddenHTTPError` + If you lack permissions to send to this channel. """ form = aiohttp.FormData() - json_payload = {"tts": tts} + json_payload = {} transformations.put_if_specified(json_payload, "content", content) transformations.put_if_specified(json_payload, "nonce", nonce) + transformations.put_if_specified(json_payload, "tts", tts) transformations.put_if_specified(json_payload, "embed", embed) transformations.put_if_specified(json_payload, "allowed_mentions", allowed_mentions) @@ -575,94 +625,103 @@ async def create_message( return await self._request(route, form_body=form, re_seekable_resources=re_seekable_resources) async def create_reaction(self, channel_id: str, message_id: str, emoji: str) -> None: - """ - Add a reaction to the given message in the given channel or user DM. - - Args: - channel_id: - The ID of the channel to add the reaction in. - message_id: - The ID of the message to add the reaction in. - emoji: - The emoji to add. This can either be a series of unicode characters making up a valid Discord - emoji, or it can be in the form of name:id for a custom emoji. - - Raises: - hikari.net.errors.ForbiddenHTTPError: - If this is the first reaction using this specific emoji on this message and you lack the `ADD_REACTIONS` - permission. If you lack `READ_MESSAGE_HISTORY`, this may also raise this error. - hikari.net.errors.NotFoundHTTPError: - If the channel or message is not found, or if the emoji is not found. - hikari.net.errors.BadRequestHTTPError: - If the emoji is not valid, unknown, or formatted incorrectly + """Add a reaction to the given message in the given channel or user DM. + + Parameters + ---------- + channel_id : :obj:`str` + The ID of the channel to get the message from. + message_id : :obj:`str` + The ID of the message to add the reaction in. + emoji : :obj:`str` + The emoji to add. This can either be a series of unicode + characters making up a valid Discord emoji, or it can be a + snowflake ID for a custom emoji. + + Raises + ------ + :obj:`hikari.net.errors.ForbiddenHTTPError` + If this is the first reaction using this specific emoji on this + message and you lack the ``ADD_REACTIONS`` permission. If you lack + ``READ_MESSAGE_HISTORY``, this may also raise this error. + :obj:`hikari.net.errors.NotFoundHTTPError` + If the channel or message is not found, or if the emoji is not found. + :obj:`hikari.net.errors.BadRequestHTTPError` + If the emoji is not valid, unknown, or formatted incorrectly. """ route = routes.OWN_REACTION.compile(self.PUT, channel_id=channel_id, message_id=message_id, emoji=emoji) await self._request(route) async def delete_own_reaction(self, channel_id: str, message_id: str, emoji: str) -> None: - """ - Remove a reaction you made using a given emoji from a given message in a given channel or user DM. + """Remove a reaction you made using a given emoji from a given message in a given channel or user DM. - Args: - channel_id: - The ID of the channel to delete the reaction from. - message_id: - The ID of the message to delete the reaction from. - emoji: - The emoji to delete. This can either be a series of unicode characters making up a valid Discord - emoji, or it can be a snowflake ID for a custom emoji. + Parameters + ---------- + channel_id : :obj:`str` + The ID of the channel to get the message from. + message_id : :obj:`str` + The ID of the message to delete the reaction from. + emoji : :obj:`str` + The emoji to delete. This can either be a series of unicode + characters making up a valid Discord emoji, or it can be a + snowflake ID for a custom emoji. - Raises: - hikari.net.errors.ForbiddenHTTPError: - If you lack permission to do this. - hikari.net.errors.NotFoundHTTPError: - If the channel or message or emoji is not found. + Raises + ------ + :obj:`hikari.net.errors.ForbiddenHTTPError` + If you lack permission to do this. + :obj:`hikari.net.errors.NotFoundHTTPError` + If the channel or message or emoji is not found. """ route = routes.OWN_REACTION.compile(self.DELETE, channel_id=channel_id, message_id=message_id, emoji=emoji) await self._request(route) async def delete_all_reactions_for_emoji(self, channel_id: str, message_id: str, emoji: str) -> None: - """ - Remove all reactions for a single given emoji on a given message in a given channel or user DM. + """Remove all reactions for a single given emoji on a given message in a given channel or user DM. - Args: - channel_id: - The channel ID to remove from. - message_id: - The message ID to remove from. - emoji: - The emoji to delete. This can either be a series of unicode characters making up a valid Discord - emoji, or it can be a snowflake ID for a custom emoji. + Parameters + ---------- + channel_id : :obj:`str` + The ID of the channel to get the message from. + message_id : :obj:`str` + The ID of the message to delete the reactions from. + emoji : :obj:`str` + The emoji to delete. This can either be a series of unicode + characters making up a valid Discord emoji, or it can be a + snowflake ID for a custom emoji. - Raises: - hikari.net.errors.NotFoundError: - If the channel or message or emoji or user is not found. - hikari.net.errors.ForbiddenError: - If you lack the `MANAGE_MESSAGES` permission, or are in DMs. + Raises + ------ + :obj:`hikari.net.errors.NotFoundError` + If the channel or message or emoji or user is not found. + :obj:`hikari.net.errors.ForbiddenError` + If you lack the ``MANAGE_MESSAGES`` permission, or are in DMs. """ route = routes.REACTION_EMOJI.compile(self.DELETE, channel_id=channel_id, message_id=message_id, emoji=emoji) await self._request(route) async def delete_user_reaction(self, channel_id: str, message_id: str, emoji: str, user_id: str) -> None: - """ - Remove a reaction made by a given user using a given emoji on a given message in a given channel or user DM. - - Args: - channel_id: - The channel ID to remove from. - message_id: - The message ID to remove from. - emoji: - The emoji to delete. This can either be a series of unicode characters making up a valid Discord - emoji, or it can be a snowflake ID for a custom emoji. - user_id: - The ID of the user who made the reaction that you wish to remove. - - Raises: - hikari.net.errors.NotFoundHTTPError: - If the channel or message or emoji or user is not found. - hikari.net.errors.ForbiddenHTTPError: - If you lack the `MANAGE_MESSAGES` permission, or are in DMs. + """Remove a reaction made by a given user using a given emoji on a given message in a given channel or user DM. + + Parameters + ---------- + channel_id : :obj:`str` + The ID of the channel to get the message from. + message_id : :obj:`str` + The ID of the message to remove the reaction from. + emoji : :obj:`str` + The emoji to delete. This can either be a series of unicode + characters making up a valid Discord emoji, or it can be a + snowflake ID for a custom emoji. + user_id : :obj:`str` + The ID of the user who made the reaction that you wish to remove. + + Raises + ------ + :obj:`hikari.net.errors.NotFoundHTTPError` + If the channel or message or emoji or user is not found. + :obj:`hikari.net.errors.ForbiddenHTTPError` + If you lack the ``MANAGE_MESSAGES`` permission, or are in DMs. """ route = routes.REACTION_EMOJI_USER.compile( self.DELETE, channel_id=channel_id, message_id=message_id, emoji=emoji, user_id=user_id, @@ -678,32 +737,38 @@ async def get_reactions( after: typing.Union[typing.Literal[...], str] = ..., limit: typing.Union[typing.Literal[...], int] = ..., ) -> typing.Sequence[typing.Dict]: - """ - Get a list of users who reacted with the given emoji on the given message in the given channel or user DM. - - Args: - channel_id: - The channel to get the message from. - message_id: - The ID of the message to retrieve. - emoji: - The emoji to get. This can either be a series of unicode characters making up a valid Discord - emoji, or it can be a snowflake ID for a custom emoji. - after: - An optional user ID. If specified, only users with a snowflake that is lexicographically greater than - the value will be returned. - limit: - An optional limit of the number of values to return. Must be between 1 and 100 inclusive. If - unspecified, it defaults to 25. - - Returns: + """Get a list of users who reacted with the given emoji on + the given message in the given channel or user DM. + + Parameters + ---------- + channel_id : :obj:`str` + The ID of the channel to get the message from. + message_id : :obj:`str` + The ID of the message to get the reactions from. + emoji : :obj:`str` + The emoji to get. This can either be a series of unicode + characters making up a valid Discord emoji, or it can be a + snowflake ID for a custom emoji. + after : :obj:`str` + If specified, the user ID. If specified, only users with a snowflake + that is lexicographically greater thanthe value will be returned. + limit : :obj:`str` + If specified, the limit of the number of values to return. Must be + between ``1`` and ``100`` inclusive. If unspecified, + defaults to ``25``. + + Returns + ------- + :obj:`typing.Sequence` [ :obj:`typing.Dict` ] A list of user objects. - Raises: - hikari.net.errors.ForbiddenHTTPError: - If the current token lacks access to this message. - hikari.net.errors.NotFoundHTTPError: - If the target entity doesn't exist. + Raises + ------ + :obj:`hikari.net.errors.ForbiddenHTTPError` + If you lack access to the message. + :obj:`hikari.net.errors.NotFoundHTTPError` + If the channel or message is not found. """ query = {} transformations.put_if_specified(query, "after", after) @@ -712,20 +777,21 @@ async def get_reactions( return await self._request(route, query=query) async def delete_all_reactions(self, channel_id: str, message_id: str) -> None: - """ - Deletes all reactions from a given message in a given channel. + """Deletes all reactions from a given message in a given channel. - Args: - channel_id: - The channel ID to remove reactions within. - message_id: - The message ID to remove reactions from. + Parameters + ---------- + channel_id : :obj:`str` + The ID of the channel to get the message from. + message_id: + The ID of the message to remove all reactions from. - Raises: - hikari.net.errors.NotFoundHTTPError: - If the channel_id or message_id was not found. - hikari.net.errors.ForbiddenHTTPError: - If you lack the `MANAGE_MESSAGES` permission. + Raises + ------ + :obj:`hikari.net.errors.NotFoundHTTPError` + If the channel or message is not found. + :obj:`hikari.net.errors.ForbiddenHTTPError` + If you lack the ``MANAGE_MESSAGES`` permission. """ route = routes.ALL_REACTIONS.compile(self.DELETE, channel_id=channel_id, message_id=message_id) await self._request(route) @@ -735,37 +801,43 @@ async def edit_message( channel_id: str, message_id: str, *, - content: typing.Union[typing.Literal[...], str] = ..., - embed: typing.Union[typing.Literal[...], typing.Dict] = ..., + content: typing.Optional[typing.Union[typing.Literal[...], str]] = ..., + embed: typing.Optional[typing.Union[typing.Literal[...], typing.Dict]] = ..., flags: typing.Union[typing.Literal[...], int] = ..., ) -> typing.Dict: - """ - Update the given message. - - Args: - channel_id: - The channel ID (or user ID if a direct message) to operate in. - message_id: - The message ID to edit. - content: - Optional string content to replace with in the message. If unspecified, it is not changed. - embed: - Optional embed to replace with in the message. If unspecified, it is not changed. - flags: - Optional integer to replace the message's current flags. If unspecified, it is not changed. - - Returns: - A replacement message object. - - Raises: - hikari.net.errors.NotFoundHTTPError: - If the channel_id or message_id is not found. - hikari.net.errors.BadRequestHTTPError: - If the embed exceeds any of the embed limits if specified, or the content is specified and consists - only of whitespace, is empty, or is more than 2,000 characters in length. - hikari.net.errors.ForbiddenHTTPError: - If you try to edit content or embed on a message you did not author or try to edit the flags - on a message you did not author without the `MANAGE_MESSAGES` permission. + """Update the given message. + + Parameters + ---------- + channel_id : :obj:`str` + The ID of the channel to get the message from. + message_id : :obj:`str` + The ID of the message to edit. + content : :obj:`str`, optional + If specified, the string content to replace with in the message. + If ``None``, the content will be removed from the message. + embed : :obj:`typing.Dict`, optional + If specified, the embed to replace with in the message. + If ``None``, the embed will be removed from the message. + flags : :obj:`int` + If specified, the integer to replace the message's current flags. + + Returns + ------- + :obj:`typing.Dict` + The edited message object. + + Raises + ------ + :obj:`hikari.net.errors.NotFoundHTTPError` + If the channel or message is not found. + :obj:`hikari.net.errors.BadRequestHTTPError` + If the embed exceeds any of the embed limits if specified, or the content is specified and consists + only of whitespace, is empty, or is more than 2,000 characters in length. This can also be caused + by trying to send an empty message (no content and no embed). + :obj:`hikari.net.errors.ForbiddenHTTPError` + If you try to edit content or embed on a message you did not author or try to edit the flags + on a message you did not author without the ``MANAGE_MESSAGES`` permission. """ payload = {} transformations.put_if_specified(payload, "content", content) @@ -775,50 +847,51 @@ async def edit_message( return await self._request(route, json_body=payload) async def delete_message(self, channel_id: str, message_id: str) -> None: - """ - Delete a message in a given channel. + """Delete a message in a given channel. - Args: - channel_id: - The channel ID or user ID that the message was sent to. - message_id: - The message ID that was sent. + Parameters + ---------- + channel_id : :obj:`str` + The ID of the channel to get the message from. + message_id : :obj:`str` + The ID of the message to delete. - Raises: - hikari.net.errors.ForbiddenHTTPError: - If you did not author the message and are in a DM, or if you did not author the message and lack the - `MANAGE_MESSAGES` permission in a guild channel. - hikari.net.errors.NotFoundHTTPError: - If the channel or message was not found. + Raises + ------ + :obj:`hikari.net.errors.ForbiddenHTTPError` + If you did not author the message and are in a DM, or if you did not author the message and lack the + ``MANAGE_MESSAGES`` permission in a guild channel. + :obj:`hikari.net.errors.NotFoundHTTPError` + If the channel or message is not found. """ route = routes.CHANNEL_MESSAGE.compile(self.DELETE, channel_id=channel_id, message_id=message_id) await self._request(route) async def bulk_delete_messages(self, channel_id: str, messages: typing.Sequence[str]) -> None: - """ - Delete multiple messages in one request. - - Args: - channel_id: - The channel_id to delete from. - messages: - A list of 2-100 message IDs to remove in the channel. - - Raises: - hikari.net.errors.NotFoundHTTPError: - If the channel_id is not found. - hikari.net.errors.ForbiddenHTTPError: - If you lack the `MANAGE_MESSAGES` permission in the channel. - hikari.net.errors.BadRequestHTTPError: - If any of the messages passed are older than 2 weeks in age or any duplicate message IDs are passed. + """Delete multiple messages in a given channel. - Notes: - This can only be used on guild text channels. - - Any message IDs that do not exist or are invalid add towards the total 100 max messages to remove. + Parameters + ---------- + channel_id : :obj:`str` + The ID of the channel to get the message from. + messages : :obj:`typing.Sequence` [ :obj:`str` ] + A list of 2-100 message IDs to remove in the channel. - This can only delete messages that are newer than 2 weeks in age. If any of the messages are older than 2 weeks - then this call will fail. + Raises + ------ + :obj:`hikari.net.errors.NotFoundHTTPError` + If the channel is not found. + :obj:`hikari.net.errors.ForbiddenHTTPError` + If you lack the ``MANAGE_MESSAGES`` permission in the channel. + :obj:`hikari.net.errors.BadRequestHTTPError` + If any of the messages passed are older than 2 weeks in age or any duplicate message IDs are passed. + + Note + ---- + This can only be used on guild text channels. + Any message IDs that do not exist or are invalid still add towards the total 100 max messages to remove. + This can only delete messages that are newer than 2 weeks in age. If any of the messages are older than 2 weeks + then this call will fail. """ payload = {"messages": messages} route = routes.CHANNEL_MESSAGES_BULK_DELETE.compile(self.POST, channel_id=channel_id) @@ -831,31 +904,34 @@ async def edit_channel_permissions( *, allow: typing.Union[typing.Literal[...], int] = ..., deny: typing.Union[typing.Literal[...], int] = ..., - type_: typing.Union[typing.Literal[...], str] = ..., + type_: typing.Literal[..., "member", "role"] = ..., reason: typing.Union[typing.Literal[...], str] = ..., ) -> None: - """ - Edit permissions for a given channel. - - Args: - channel_id: - The channel to edit permissions for. - overwrite_id: - The overwrite ID to edit. - allow: - The bitwise value of all permissions to set to be allowed. - deny: - The bitwise value of all permissions to set to be denied. - type_: - "member" if it is for a member, or "role" if it is for a role. - reason: - An optional audit log reason explaining why the change was made. - - Raises: - hikari.net.errors.NotFoundHTTPError: - If the target channel or overwrite doesn't exist. - hikari.net.errors.ForbiddenHTTPError: - If the current token lacks permission to do this. + """Edit permissions for a given channel. + + Parameters + ---------- + channel_id : :obj:`str` + The ID of the channel to edit permissions for. + overwrite_id : :obj:`str` + The overwrite ID to edit. + allow : :obj:`int` + If specified, the bitwise value of all permissions to set to be allowed. + deny : :obj:`int` + If specified, the bitwise value of all permissions to set to be denied. + type_ : :obj:`typing.Literal` [ ``"member"``, ``"role"``] + If specified, the type of overwrite. "member" if it is for a member, + or "role" if it is for a role. + reason : :obj:`str` + If specified, the audit log reason explaining why the operation + was performed. + + Raises + ------ + :obj:`hikari.net.errors.NotFoundHTTPError` + If the target channel or overwrite doesn't exist. + :obj:`hikari.net.errors.ForbiddenHTTPError` + If you lack permission to do this. """ payload = {} transformations.put_if_specified(payload, "allow", allow) @@ -865,21 +941,24 @@ async def edit_channel_permissions( await self._request(route, json_body=payload, reason=reason) async def get_channel_invites(self, channel_id: str) -> typing.Sequence[typing.Dict]: - """ - Get invites for a given channel. + """Get invites for a given channel. - Args: - channel_id: - The channel to get invites for. + Parameters + ---------- + channel_id : :obj:`str` + The ID of the channel to get invites for. - Returns: + Returns + ------- + :obj:`typing.Sequence` [ :obj:`typing.Dict` ] A list of invite objects. - Raises: - hikari.net.errors.ForbiddenHTTPError: - If you lack the `MANAGE_CHANNELS` permission. - hikari.net.errors.NotFoundHTTPError: - If the channel does not exist. + Raises + ------ + :obj:`hikari.net.errors.ForbiddenHTTPError` + If you lack the ``MANAGE_CHANNELS`` permission. + :obj:`hikari.net.errors.NotFoundHTTPError` + If the channel does not exist. """ route = routes.CHANNEL_INVITES.compile(self.GET, channel_id=channel_id) return await self._request(route) @@ -896,38 +975,45 @@ async def create_channel_invite( target_user_type: typing.Union[typing.Literal[...], int] = ..., reason: typing.Union[typing.Literal[...], str] = ..., ) -> typing.Dict: - """ - Create a new invite for the given channel. - - Args: - channel_id: - The channel ID to create the invite for. - max_age: - The max age of the invite in seconds, defaults to 86400 (24 hours). Set to 0 to never expire. - max_uses: - The max number of uses this invite can have, or 0 for unlimited (as per the default). - temporary: - If `True`, grant temporary membership, meaning the user is kicked when their session ends unless they - are given a role. Defaults to `False`. - unique: - If `True`, never reuse a similar invite. Defaults to `False`. - target_user: - The ID of the user this invite should target, if set. - target_user_type: - The type of target for this invite, if set. - reason: - An optional audit log reason explaining why the change was made. - - Returns: + """Create a new invite for the given channel. + + Parameters + ---------- + channel_id : :obj:`str` + The ID of the channel to create the invite for. + max_age : :obj:`int` + If specified, the max age of the invite in seconds, defaults to + ``86400`` (24 hours). + Set to ``0`` to never expire. + max_uses : :obj:`int` + If specified, the max number of uses this invite can have, or ``0`` for + unlimited (as per the default). + temporary : :obj:`bool` + If specified, whether to grant temporary membership, meaning the user + is kicked when their session ends unless they are given a role. + unique : :obj:`bool` + If specified, whether to try to reuse a similar invite. + target_user : :obj:`str` + If specified, the ID of the user this invite should target. + target_user_type : :obj:`int` + If specified, the type of target for this invite. + reason : :obj:`str` + If specified, the audit log reason explaining why the operation + was performed. + + Returns + ------- + :obj:`typing.Dict` An invite object. - Raises: - hikari.net.errors.ForbiddenHTTPError: - If you lack the `CREATE_INSTANT_MESSAGES` permission. - hikari.net.errors.NotFoundHTTPError: - If the channel does not exist. - hikari.net.errors.BadRequestHTTPError: - If the arguments provided are not valid (e.g. negative age, etc). + Raises + ------ + :obj:`hikari.net.errors.ForbiddenHTTPError` + If you lack the ``CREATE_INSTANT_MESSAGES`` permission. + :obj:`hikari.net.errors.NotFoundHTTPError` + If the channel does not exist. + :obj:`hikari.net.errors.BadRequestHTTPError` + If the arguments provided are not valid (e.g. negative age, etc). """ payload = {} transformations.put_if_specified(payload, "max_age", max_age) @@ -940,138 +1026,158 @@ async def create_channel_invite( return await self._request(route, json_body=payload, reason=reason) async def delete_channel_permission(self, channel_id: str, overwrite_id: str) -> None: - """ - Delete a channel permission overwrite for a user or a role in a channel. + """Delete a channel permission overwrite for a user or a role in a channel. - Args: - channel_id: - The channel ID to delete from. - overwrite_id: - The override ID to remove. + Parameters + ---------- + channel_id : :obj:`str` + The ID of the channel to delete the overwire from. + overwrite_id : :obj:`str` + The ID of the overwrite to remove. - Raises: - hikari.net.errors.NotFoundHTTPError: - If the overwrite or channel ID does not exist. - hikari.net.errors.ForbiddenHTTPError: - If you lack the `MANAGE_ROLES` permission for that channel. + Raises + ------ + :obj:`hikari.net.errors.NotFoundHTTPError` + If the overwrite or channel do not exist. + :obj:`hikari.net.errors.ForbiddenHTTPError` + If you lack the ``MANAGE_ROLES`` permission for that channel. """ route = routes.CHANNEL_PERMISSIONS.compile(self.DELETE, channel_id=channel_id, overwrite_id=overwrite_id) await self._request(route) async def trigger_typing_indicator(self, channel_id: str) -> None: - """ - Trigger the account to appear to be typing for the next 10 seconds in the given channel. + """Trigger the account to appear to be typing for the next 10 seconds in the given channel. - Args: - channel_id: - The channel ID to appear to be typing in. This may be a user ID if you wish to appear to be typing - in DMs. + Parameters + ---------- + channel_id : :obj:`str` + The ID of the channel to appear to be typing in. This may be + a user ID if you wish to appear to be typing in DMs. - Raises: - hikari.net.errors.NotFoundHTTPError: - If the channel is not found. - hikari.net.errors.ForbiddenHTTPError: - If you are not in the guild the channel belongs to. + Raises + ------ + :obj:`hikari.net.errors.NotFoundHTTPError` + If the channel is not found. + :obj:`hikari.net.errors.ForbiddenHTTPError` + If you are not able to type in the channel. """ route = routes.CHANNEL_TYPING.compile(self.POST, channel_id=channel_id) await self._request(route) async def get_pinned_messages(self, channel_id: str) -> typing.Sequence[typing.Dict]: - """ - Get pinned messages for a given channel. + """Get pinned messages for a given channel. - Args: - channel_id: - The channel ID to get messages for. + Parameters + ---------- + channel_id : :obj:`str` + The channel ID to get messages from. - Returns: + Returns + ------- + :obj:`typing.Sequence` [ :obj:`typing.Dict` ] A list of messages. - Raises: - hikari.net.errors.NotFoundHTTPError: - If no channel matching the ID exists. - hikari.net.errors.ForbiddenHTTPError: - If you lack permission to do this. + Raises + ------ + :obj:`hikari.net.errors.NotFoundHTTPError` + If the channel is not found. + :obj:`hikari.net.errors.ForbiddenHTTPError` + If you are not able to see the channel. + + Note + ---- + If you are not able to see the pinned message (eg. you are missing ``READ_MESSAGE_HISTORY`` + and the pinned message is an old message), it will not be returned. """ route = routes.CHANNEL_PINS.compile(self.GET, channel_id=channel_id) return await self._request(route) async def add_pinned_channel_message(self, channel_id: str, message_id: str) -> None: - """ - Add a pinned message to the channel. + """Add a pinned message to the channel. - Args: - channel_id: - The channel ID to add a pin to. - message_id: - The message in the channel to pin. + Parameters + ---------- + channel_id : :obj:`str` + The ID of the channel to pin a message to. + message_id : :obj:`str` + The ID of the message to pin. - Raises: - hikari.net.errors.ForbiddenHTTPError: - If you lack the `MANAGE_MESSAGES` permission. - hikari.net.errors.NotFoundHTTPError: - If the message or channel does not exist. + Raises + ------ + :obj:`hikari.net.errors.ForbiddenHTTPError` + If you lack the ``MANAGE_MESSAGES`` permission. + :obj:`hikari.net.errors.NotFoundHTTPError` + If the message or channel do not exist. """ route = routes.CHANNEL_PINS.compile(self.PUT, channel_id=channel_id, message_id=message_id) await self._request(route) async def delete_pinned_channel_message(self, channel_id: str, message_id: str) -> None: - """ - Remove a pinned message from the channel. This will only unpin the message. It will not delete it. + """Remove a pinned message from the channel. + + This will only unpin the message, not delete it. - Args: - channel_id: - The channel ID to remove a pin from. - message_id: - The message in the channel to unpin. + Parameters + ---------- + channel_id : :obj:`str` + The ID of the channel to remove a pin from. + message_id : :obj:`str` + The ID of the message to unpin. - Raises: - hikari.net.errors.ForbiddenHTTPError: - If you lack the `MANAGE_MESSAGES` permission. - hikari.net.errors.NotFoundHTTPError: - If the message or channel does not exist. + Raises + ------ + :obj:`hikari.net.errors.ForbiddenHTTPError` + If you lack the ``MANAGE_MESSAGES`` permission. + :obj:`hikari.net.errors.NotFoundHTTPError` + If the message or channel do not exist. """ route = routes.CHANNEL_PIN.compile(self.DELETE, channel_id=channel_id, message_id=message_id) await self._request(route) async def list_guild_emojis(self, guild_id: str) -> typing.Sequence[typing.Dict]: - """ - Gets emojis for a given guild ID. + """Gets emojis for a given guild ID. - Args: - guild_id: - The guild ID to get the emojis for. + Parameters + ---------- + guild_id : :obj:`str` + The ID of the guild to get the emojis for. - Returns: + Returns + ------- + :obj:`typing.Sequence` [ :obj:`typing.Dict` ] A list of emoji objects. - Raises: - hikari.net.errors.NotFoundHTTPError: - If the guild is not found. - hikari.net.errors.ForbiddenHTTPError: - If you aren't a member of said guild. + Raises + ------ + :obj:`hikari.net.errors.NotFoundHTTPError` + If the guild is not found. + :obj:`hikari.net.errors.ForbiddenHTTPError` + If you aren't a member of the guild. """ route = routes.GUILD_EMOJIS.compile(self.GET, guild_id=guild_id) return await self._request(route) async def get_guild_emoji(self, guild_id: str, emoji_id: str) -> typing.Dict: - """ - Gets an emoji from a given guild and emoji IDs + """Gets an emoji from a given guild and emoji IDs. - Args: - guild_id: - The ID of the guild to get the emoji from. - emoji_id: - The ID of the emoji to get. + Parameters + ---------- + guild_id : :obj:`str` + The ID of the guild to get the emoji from. + emoji_id : :obj:`str` + The ID of the emoji to get. - Returns: + Returns + ------- + :obj:`typing.Dict` An emoji object. - Raises: - hikari.net.errors.NotFoundHTTPError: - If either the guild or the emoji aren't found. - hikari.net.errors.ForbiddenHTTPError: - If you aren't a member of said guild. + Raises + ------ + :obj:`hikari.net.errors.NotFoundHTTPError` + If either the guild or the emoji aren't found. + :obj:`hikari.net.errors.ForbiddenHTTPError` + If you aren't a member of said guild. """ route = routes.GUILD_EMOJI.compile(self.GET, guild_id=guild_id, emoji_id=emoji_id) return await self._request(route) @@ -1085,31 +1191,38 @@ async def create_guild_emoji( roles: typing.Union[typing.Literal[...], typing.Sequence[str]] = ..., reason: typing.Union[typing.Literal[...], str] = ..., ) -> typing.Dict: - """ - Creates a new emoji for a given guild. - - Args: - guild_id: - The ID of the guild to create the emoji in. - name: - The new emoji's name. - image: - The 128x128 image in bytes form. - roles: - A list of roles for which the emoji will be whitelisted. If empty, all roles are whitelisted. - reason: - An optional audit log reason explaining why the change was made. - - Returns: + """Creates a new emoji for a given guild. + + Parameters + ---------- + guild_id : :obj:`str` + The ID of the guild to create the emoji in. + name : :obj:`str` + The new emoji's name. + image : :obj:`bytes` + The 128x128 image in bytes form. + roles : :obj:`typing.Sequence` [ :obj:`str` ] + If specified, a list of roles for which the emoji will be whitelisted. + If empty, all roles are whitelisted. + reason : :obj:`str` + If specified, the audit log reason explaining why the operation + was performed. + + Returns + ------- + :obj:`typing.Dict` The newly created emoji object. - Raises: - hikari.net.errors.NotFoundHTTPError: - If the guild is not found. - hikari.net.errors.ForbiddenHTTPError: - If you either lack the `MANAGE_EMOJIS` permission or aren't a member of said guild. - hikari.net.errors.BadRequestHTTPError: - If you attempt to upload an image larger than 256kb, an empty image or an invalid image format. + Raises + ------ + :obj:`ValueError` + If ``image`` is ``None``. + :obj:`hikari.net.errors.NotFoundHTTPError` + If the guild is not found. + :obj:`hikari.net.errors.ForbiddenHTTPError` + If you either lack the ``MANAGE_EMOJIS`` permission or aren't a member of said guild. + :obj:`hikari.net.errors.BadRequestHTTPError` + If you attempt to upload an image larger than 256kb, an empty image or an invalid image format. """ assertions.assert_not_none(image, "image must be a valid image") payload = { @@ -1129,31 +1242,35 @@ async def modify_guild_emoji( roles: typing.Union[typing.Literal[...], typing.Sequence[str]] = ..., reason: typing.Union[typing.Literal[...], str] = ..., ) -> typing.Dict: - """ - Edits an emoji of a given guild - - Args: - guild_id: - The ID of the guild to which the edited emoji belongs to. - emoji_id: - The ID of the edited emoji. - name: - The new emoji name string. Keep unspecified to keep the name the same. - roles: - A list of IDs for the new whitelisted roles. - Set to an empty list to whitelist all roles. - Keep unspecified to leave the same roles already set. - reason: - An optional audit log reason explaining why the change was made. - - Returns: + """Edits an emoji of a given guild + + Parameters + ---------- + guild_id : :obj:`str` + The ID of the guild to which the edited emoji belongs to. + emoji_id : :obj:`str` + The ID of the edited emoji. + name : :obj:`str` + If specified, a new emoji name string. Keep unspecified to keep the name the same. + roles : :obj:`typing.Sequence` [ :obj:`str` ] + If specified, a list of IDs for the new whitelisted roles. + Set to an empty list to whitelist all roles. + Keep unspecified to leave the same roles already set. + reason : :obj:`str` + If specified, the audit log reason explaining why the operation + was performed. + + Returns + ------- + :obj:`typing.Dict` The updated emoji object. - Raises: - hikari.net.errors.NotFoundHTTPError: - If either the guild or the emoji aren't found. - hikari.net.errors.ForbiddenHTTPError: - If you either lack the `MANAGE_EMOJIS` permission or are not a member of the given guild. + Raises + ------ + :obj:`hikari.net.errors.NotFoundHTTPError` + If either the guild or the emoji aren't found. + :obj:`hikari.net.errors.ForbiddenHTTPError` + If you either lack the ``MANAGE_EMOJIS`` permission or are not a member of the given guild. """ payload = {} transformations.put_if_specified(payload, "name", name) @@ -1162,20 +1279,21 @@ async def modify_guild_emoji( return await self._request(route, json_body=payload, reason=reason) async def delete_guild_emoji(self, guild_id: str, emoji_id: str) -> None: - """ - Deletes an emoji from a given guild + """Deletes an emoji from a given guild - Args: - guild_id: - The ID of the guild to delete the emoji from. - emoji_id: - The ID of the emoji to be deleted. + Parameters + ---------- + guild_id : :obj:`str` + The ID of the guild to delete the emoji from. + emoji_id : :obj:`str` + The ID of the emoji to be deleted. - Raises: - hikari.net.errors.NotFoundHTTPError: - If either the guild or the emoji aren't found. - hikari.net.errors.ForbiddenHTTPError: - If you either lack the `MANAGE_EMOJIS` permission or aren't a member of said guild. + Raises + ------ + :obj:`hikari.net.errors.NotFoundHTTPError` + If either the guild or the emoji aren't found. + :obj:`hikari.net.errors.ForbiddenHTTPError` + If you either lack the ``MANAGE_EMOJIS`` permission or aren't a member of said guild. """ route = routes.GUILD_EMOJI.compile(self.DELETE, guild_id=guild_id, emoji_id=emoji_id) await self._request(route) @@ -1192,36 +1310,40 @@ async def create_guild( roles: typing.Union[typing.Literal[...], typing.Sequence[typing.Dict]] = ..., channels: typing.Union[typing.Literal[...], typing.Sequence[typing.Dict]] = ..., ) -> typing.Dict: - """ - Creates a new guild. Can only be used by bots in less than 10 guilds. - - Args: - name: - The name string for the new guild (2-100 characters). - region: - The voice region ID for new guild. You can use `list_voice_regions` to see which region IDs are - available. - icon: - The guild icon image in bytes form. - verification_level: - The verification level integer (0-5). - default_message_notifications: - The default notification level integer (0-1). - explicit_content_filter: - The explicit content filter integer (0-2). - roles: - An array of role objects to be created alongside the guild. First element changes the `@everyone` role. - channels: - An array of channel objects to be created alongside the guild. - - Returns: + """Creates a new guild. Can only be used by bots in less than 10 guilds. + + Parameters + ---------- + name : :obj:`str` + The name string for the new guild (2-100 characters). + region : :obj:`str` + If specified, the voice region ID for new guild. You can use + :meth:`list_voice_regions` to see which region IDs are available. + icon : :obj:`bytes` + If specified, the guild icon image in bytes form. + verification_level : :obj:`int` + If specified, the verification level integer (0-5). + default_message_notifications : :obj:`int` + If specified, the default notification level integer (0-1). + explicit_content_filter : :obj:`int` + If specified, the explicit content filter integer (0-2). + roles : :obj:`typing.Sequence` [ :obj:`typing.Dict` ] + If specified, an array of role objects to be created alongside the + guild. First element changes the ``@everyone`` role. + channels : :obj:`typing.Sequence` [ :obj:`typing.Dict` ] + If specified, an array of channel objects to be created alongside the guild. + + Returns + ------- + :obj:`typing.Dict` The newly created guild object. - Raises: - hikari.net.errors.ForbiddenHTTPError: - If your bot is on 10 or more guilds. - hikari.net.errors.BadRequestHTTPError: - If you provide unsupported fields like `parent_id` in channel objects. + Raises + ------ + :obj:`hikari.net.errors.ForbiddenHTTPError` + If you are on ``10`` or more guilds. + :obj:`hikari.net.errors.BadRequestHTTPError` + If you provide unsupported fields like ``parent_id`` in channel objects. """ payload = {"name": name} transformations.put_if_specified(payload, "region", region) @@ -1235,21 +1357,24 @@ async def create_guild( return await self._request(route, json_body=payload) async def get_guild(self, guild_id: str) -> typing.Dict: - """ - Gets a given guild's object. + """Gets a given guild's object. - Args: - guild_id: - The ID of the guild to get. + Parameters + ---------- + guild_id : :obj:`str` + The ID of the guild to get. - Returns: + Returns + ------- + :obj:`typing.Dict` The requested guild object. - Raises: - hikari.net.errors.NotFoundHTTPError: - If the guild is not found. - hikari.net.errors.ForbiddenHTTPError: - If the current token doesn't have access to the guild. + Raises + ------ + :obj:`hikari.net.errors.NotFoundHTTPError` + If the guild is not found. + :obj:`hikari.net.errors.ForbiddenHTTPError` + If you don't have access to the guild. """ route = routes.GUILD.compile(self.GET, guild_id=guild_id) return await self._request(route) @@ -1272,46 +1397,50 @@ async def modify_guild( # lgtm [py/similar-function] system_channel_id: typing.Union[typing.Literal[...], str] = ..., reason: typing.Union[typing.Literal[...], str] = ..., ) -> typing.Dict: - """ - Edits a given guild. - - Args: - guild_id: - The ID of the guild to be edited. - name: - The new name string. - region: - The voice region ID for new guild. You can use `list_voice_regions` to see which region IDs are - available. - verification_level: - The verification level integer (0-5). - default_message_notifications: - The default notification level integer (0-1). - explicit_content_filter: - The explicit content filter integer (0-2). - afk_channel_id: - The ID for the AFK voice channel. - afk_timeout: - The AFK timeout period in seconds - icon: - The guild icon image in bytes form. - owner_id: - The ID of the new guild owner. - splash: - The new splash image in bytes form. - system_channel_id: - The ID of the new system channel. - reason: - Optional reason to apply to the audit log. - - Returns: + """Edits a given guild. + + Parameters + ---------- + guild_id : :obj:`str` + The ID of the guild to be edited. + name : :obj:`str` + If specified, the new name string for the guild (2-100 characters). + region : :obj:`str` + If specified, the new voice region ID for guild. You can use + :meth:`list_voice_regions` to see which region IDs are available. + verification_level : :obj:`int` + If specified, the new verification level integer (0-5). + default_message_notifications : :obj:`int` + If specified, the new default notification level integer (0-1). + explicit_content_filter : :obj:`int` + If specified, the new explicit content filter integer (0-2). + afk_channel_id : :obj:`str` + If specified, the new ID for the AFK voice channel. + afk_timeout : :obj:`int` + If specified, the new AFK timeout period in seconds + icon : :obj:`bytes` + If specified, the new guild icon image in bytes form. + owner_id : :obj:`str` + If specified, the new ID of the new guild owner. + splash : :obj:`bytes` + If specified, the new new splash image in bytes form. + system_channel_id : :obj:`str` + If specified, the new ID of the new system channel. + reason : :obj:`str` + If specified, the audit log reason explaining why the operation + was performed. + + Returns + ------- + :obj:`typing.Dict` The edited guild object. - Raises: - hikari.net.errors.NotFoundHTTPError: - If the guild is not found. - hikari.net.errors.ForbiddenHTTPError: - If you lack the `MANAGE_GUILD` permission or are not in the guild. + Raises + ------ + :obj:`hikari.net.errors.NotFoundHTTPError` + If the guild is not found. + :obj:`hikari.net.errors.ForbiddenHTTPError` + If you lack the ``MANAGE_GUILD`` permission or are not in the guild. """ payload = {} transformations.put_if_specified(payload, "name", name) @@ -1331,39 +1460,45 @@ async def modify_guild( # lgtm [py/similar-function] # pylint: enable=too-many-locals async def delete_guild(self, guild_id: str) -> None: - """ - Permanently deletes the given guild. You must be owner. + """Permanently deletes the given guild. + + You must be owner of the guild to perform this action. - Args: - guild_id: - The ID of the guild to be deleted. + Parameters + ---------- + guild_id : :obj:`str` + The ID of the guild to be deleted. - Raises: - hikari.net.errors.NotFoundHTTPError: - If the guild is not found. - hikari.net.errors.ForbiddenHTTPError: - If you're not the guild owner. + Raises + ------ + :obj:`hikari.net.errors.NotFoundHTTPError` + If the guild is not found. + :obj:`hikari.net.errors.ForbiddenHTTPError` + If you are not the guild owner. """ route = routes.GUILD.compile(self.DELETE, guild_id=guild_id) await self._request(route) async def get_guild_channels(self, guild_id: str) -> typing.Sequence[typing.Dict]: - """ - Gets all the channels for a given guild. + """Gets all the channels for a given guild. - Args: - guild_id: - The ID of the guild to get the channels from. + Parameters + ---------- + guild_id : :obj:`str` + The ID of the guild to get the channels from. - Returns: + Returns + ------- + :obj:`typing.Sequence` [ :obj:`typing.Dict` ] A list of channel objects. - Raises: - hikari.net.errors.NotFoundHTTPError: - If the guild is not found. - hikari.net.errors.ForbiddenHTTPError: - If you're not in the guild. - """ + Raises + ------ + :obj:`hikari.net.errors.NotFoundHTTPError` + If the guild is not found. + :obj:`hikari.net.errors.ForbiddenHTTPError` + If you are not in the guild. + """ route = routes.GUILD_CHANNELS.compile(self.GET, guild_id=guild_id) return await self._request(route) @@ -1373,116 +1508,138 @@ async def create_guild_channel( name: str, *, type_: typing.Union[typing.Literal[...], int] = ..., + position: typing.Union[typing.Literal[...], int] = ..., topic: typing.Union[typing.Literal[...], str] = ..., + nsfw: typing.Union[typing.Literal[...], bool] = ..., + rate_limit_per_user: typing.Union[typing.Literal[...], int] = ..., bitrate: typing.Union[typing.Literal[...], int] = ..., user_limit: typing.Union[typing.Literal[...], int] = ..., - rate_limit_per_user: typing.Union[typing.Literal[...], int] = ..., - position: typing.Union[typing.Literal[...], int] = ..., permission_overwrites: typing.Union[typing.Literal[...], typing.Sequence[typing.Dict]] = ..., parent_id: typing.Union[typing.Literal[...], str] = ..., - nsfw: typing.Union[typing.Literal[...], bool] = ..., reason: typing.Union[typing.Literal[...], str] = ..., ) -> typing.Dict: - """ - Creates a channel in a given guild. - - Args: - guild_id: - The ID of the guild to create the channel in. - name: - The new channel name string (2-100 characters). - type_: - The channel type integer (0-6). - topic: - The string for the channel topic (0-1024 characters). - bitrate: - The bitrate integer (in bits) for the voice channel, if applicable. - user_limit: - The maximum user count for the voice channel, if applicable. - rate_limit_per_user: - The seconds a user has to wait before posting another message (0-21600). - Having the `MANAGE_MESSAGES` or `MANAGE_CHANNELS` permissions gives you immunity. - position: - The sorting position for the channel. - permission_overwrites: - A list of overwrite objects to apply to the channel. - parent_id: - The ID of the parent category. - nsfw: - Marks the channel as NSFW if `True`. - reason: - The optional reason for the operation being performed. - - Returns: + """Creates a channel in a given guild. + + Parameters + ---------- + guild_id: + The ID of the guild to create the channel in. + name : :obj:`str` + If specified, the name for the channel.This must be + between ``2`` and ``100`` characters in length. + type_: :obj:`int` + If specified, the channel type integer (0-6). + position : :obj:`int` + If specified, the position to change the channel to. + topic : :obj:`str` + If specified, the topic to set. This is only applicable to + text channels. This must be between ``0`` and ``1024`` + characters in length. + nsfw : :obj:`bool` + If specified, whether the channel will be marked as NSFW. + Only applicable to text channels. + rate_limit_per_user : :obj:`int` + If specified, the number of seconds the user has to wait before sending + another message. This will not apply to bots, or to members with + ``MANAGE_MESSAGES`` or ``MANAGE_CHANNEL`` permissions. This must + be between ``0`` and ``21600`` seconds. + bitrate : :obj:`int` + If specified, the bitrate in bits per second allowable for the channel. + This only applies to voice channels and must be between ``8000`` + and ``96000`` for normal servers or ``8000`` and ``128000`` for + VIP servers. + user_limit : :obj:`int` + If specified, the max number of users to allow in a voice channel. + This must be between ``0`` and ``99`` inclusive, where + ``0`` implies no limit. + permission_overwrites : :obj:`typing.Sequence` [ :obj:`typing.Dict` ] + If specified, the list of permission overwrites that are category + specific to replace the existing overwrites with. + parent_id : :obj:`str` + If specified, the parent category ID to set for the channel. + reason : :obj:`str` + If specified, the audit log reason explaining why the operation + was performed. + + Returns + ------- + :obj:`typing.Dict` The newly created channel object. - Raises: - hikari.net.errors.NotFoundHTTPError: - If the guild is not found. - hikari.net.errors.ForbiddenHTTPError: - If you lack the `MANAGE_CHANNEL` permission or are not in the target guild or are not in the guild. - hikari.net.errors.BadRequestHTTPError: - If you omit the `name` argument. + Raises + ------ + :obj:`hikari.net.errors.NotFoundHTTPError` + If the guild is not found. + :obj:`hikari.net.errors.ForbiddenHTTPError` + If you lack the ``MANAGE_CHANNEL`` permission or are not in the guild. + :obj:`hikari.net.errors.BadRequestHTTPError` + If you provide incorrect options for the corresponding channel type + (e.g. a ``bitrate`` for a text channel). """ - payload = {"name": name} + payload = {} + transformations.put_if_specified(payload, "name", name) transformations.put_if_specified(payload, "type", type_) + transformations.put_if_specified(payload, "position", position) transformations.put_if_specified(payload, "topic", topic) + transformations.put_if_specified(payload, "nsfw", nsfw) + transformations.put_if_specified(payload, "rate_limit_per_user", rate_limit_per_user) transformations.put_if_specified(payload, "bitrate", bitrate) transformations.put_if_specified(payload, "user_limit", user_limit) - transformations.put_if_specified(payload, "rate_limit_per_user", rate_limit_per_user) - transformations.put_if_specified(payload, "position", position) transformations.put_if_specified(payload, "permission_overwrites", permission_overwrites) transformations.put_if_specified(payload, "parent_id", parent_id) - transformations.put_if_specified(payload, "nsfw", nsfw) route = routes.GUILD_CHANNELS.compile(self.POST, guild_id=guild_id) return await self._request(route, json_body=payload, reason=reason) async def modify_guild_channel_positions( self, guild_id: str, channel: typing.Tuple[str, int], *channels: typing.Tuple[str, int] ) -> None: - """ - Edits the position of one or more given channels. - - Args: - guild_id: - The ID of the guild in which to edit the channels. - channel: - The first channel to change the position of. This is a tuple of the channel ID and the integer position. - channels: - Optional additional channels to change the position of. These must be tuples of the channel ID and the - integer positions to change to. - - Raises: - hikari.net.errors.NotFoundHTTPError: - If either the guild or any of the channels aren't found. - hikari.net.errors.ForbiddenHTTPError: - If you either lack the `MANAGE_CHANNELS` permission or are not a member of said guild or are not in - The guild. - hikari.net.errors.BadRequestHTTPError: - If you provide anything other than the `id` and `position` fields for the channels. + """Edits the position of one or more given channels. + + Parameters + ---------- + guild_id : :obj:`str` + The ID of the guild in which to edit the channels. + channel : :obj:`typing.Tuple` [ :obj:`str`, :obj:`int` ] + The first channel to change the position of. This is a tuple of the channel ID and the integer position. + *channels : :obj:`typing.Tuple` [ :obj:`str`, :obj:`int` ] + Optional additional channels to change the position of. These must be tuples of the channel ID and the + integer positions to change to. + + Raises + ------ + :obj:`hikari.net.errors.NotFoundHTTPError` + If either the guild or any of the channels aren't found. + :obj:`hikari.net.errors.ForbiddenHTTPError` + If you either lack the ``MANAGE_CHANNELS`` permission or are not a member of said guild or are not in + the guild. + :obj:`hikari.net.errors.BadRequestHTTPError` + If you provide anything other than the ``id`` and ``position`` fields for the channels. """ payload = [{"id": ch[0], "position": ch[1]} for ch in (channel, *channels)] route = routes.GUILD_CHANNELS.compile(self.PATCH, guild_id=guild_id) await self._request(route, json_body=payload) async def get_guild_member(self, guild_id: str, user_id: str) -> typing.Dict: - """ - Gets a given guild member. - - Args: - guild_id: - The ID of the guild to get the member from. - user_id: - The ID of the member to get. + """Gets a given guild member. - Returns: + Parameters + ---------- + guild_id : :obj:`str` + The ID of the guild to get the member from. + user_id : :obj:`str` + The ID of the member to get. + + Returns + ------- + :obj:`typing.Dict` The requested member object. - Raises: - hikari.net.errors.NotFoundHTTPError: - If either the guild or the member aren't found. - hikari.net.errors.ForbiddenHTTPError: - If you don't have access to the target guild. + Raises + ------ + :obj:`hikari.net.errors.NotFoundHTTPError` + If either the guild or the member aren't found. + :obj:`hikari.net.errors.ForbiddenHTTPError` + If you don't have access to the target guild. """ route = routes.GUILD_MEMBER.compile(self.GET, guild_id=guild_id, user_id=user_id) return await self._request(route) @@ -1494,19 +1651,21 @@ async def list_guild_members( limit: typing.Union[typing.Literal[...], int] = ..., after: typing.Union[typing.Literal[...], str] = ..., ) -> typing.Sequence[typing.Dict]: - """ - Lists all members of a given guild. + """Lists all members of a given guild. - Args: - guild_id: + Parameters + ---------- + guild_id : :obj:`str` The ID of the guild to get the members from. - limit: - The maximum number of members to return (1-1000). - after: - The highest ID in the previous page. This is used for retrieving more than 1000 members in a server - using consecutive requests. + limit : :obj:`int` + If specified, the maximum number of members to return. This has to be between + ``1`` and ``1000`` inclusive. + after : :obj:`str` + If specified, the highest ID in the previous page. This is used for retrieving more + than ``1000`` members in a server using consecutive requests. - Example: + Example + ------- .. code-block:: python members = [] @@ -1517,20 +1676,23 @@ async def list_guild_members( members += next_members if len(next_members) == 1000: - last_id = max(m["id"] for m in next_members) + last_id = next_members[-1] else: break - Returns: + Returns + ------- + :obj:`typing.Dict` A list of member objects. - Raises: - hikari.net.errors.NotFoundHTTPError: - If the guild is not found. - hikari.net.errors.ForbiddenHTTPError: - If you are not in the guild. - hikari.net.errors.BadRequestHTTPError: - If you provide invalid values for the `limit` and `after` fields. + Raises + ------ + :obj:`hikari.net.errors.NotFoundHTTPError` + If the guild is not found. + :obj:`hikari.net.errors.ForbiddenHTTPError` + If you are not in the guild. + :obj:`hikari.net.errors.BadRequestHTTPError` + If you provide invalid values for the ``limit`` or `after`` fields. """ query = {} transformations.put_if_specified(query, "limit", limit) @@ -1543,43 +1705,48 @@ async def modify_guild_member( # lgtm [py/similar-function] guild_id: str, user_id: str, *, - nick: typing.Union[None, typing.Literal[...], str] = ..., + nick: typing.Optional[typing.Union[typing.Literal[...], str]] = ..., roles: typing.Union[typing.Literal[...], typing.Sequence[str]] = ..., mute: typing.Union[typing.Literal[...], bool] = ..., deaf: typing.Union[typing.Literal[...], bool] = ..., - channel_id: typing.Union[None, typing.Literal[...], str] = ..., + channel_id: typing.Optional[typing.Union[typing.Literal[...], str]] = ..., reason: typing.Union[typing.Literal[...], str] = ..., ) -> None: - """ - Edits a member of a given guild. - - Args: - guild_id: - The ID of the guild to edit the member from. - user_id: - The ID of the member to edit. - nick: - The new nickname string. Setting it to None explicitly will clear the nickname. - roles: - A list of role IDs the member should have. - mute: - Whether the user should be muted in the voice channel or not, if applicable. - deaf: - Whether the user should be deafen in the voice channel or not, if applicable. - channel_id: - The ID of the channel to move the member to, if applicable. Pass None to disconnect the user. - reason: - Optional reason to add to audit logs for the guild explaining why the operation was performed. - Raises: - hikari.net.errors.NotFoundHTTPError: - If either the guild, user, channel or any of the roles aren't found. - hikari.net.errors.ForbiddenHTTPError: - If you lack any of the applicable permissions - (`MANAGE_NICKNAMES`, `MANAGE_ROLES`, `MUTE_MEMBERS`, `DEAFEN_MEMBERS` or `MOVE_MEMBERS`). - Note that to move a member you must also have permission to connect to the end channel. - This will also be raised if you're not in the guild. - hikari.net.errors.BadRequestHTTPError: - If you pass `mute`, `deaf` or `channel_id` while the member is not connected to a voice channel. + """Edits a member of a given guild. + + Parameters + ---------- + guild_id : :obj:`str` + The ID of the guild to edit the member from. + user_id : :obj:`str` + The ID of the member to edit. + nick : :obj:`str` + If specified, the new nickname string. Setting it to ``None`` explicitly + will clear the nickname. + roles : :obj:`str` + If specified, a list of role IDs the member should have. + mute : :obj:`bool` + If specified, whether the user should be muted in the voice channel or not. + deaf : :obj:`bool` + If specified, whether the user should be deafen in the voice channel or not. + channel_id : :obj:`str` + If specified, the ID of the channel to move the member to. Setting it to + ``None`` explicitly will disconnect the user. + reason : :obj:`str` + If specified, the audit log reason explaining why the operation + was performed. + + Raises + ------ + :obj:`hikari.net.errors.NotFoundHTTPError` + If either the guild, user, channel or any of the roles aren't found. + :obj:`hikari.net.errors.ForbiddenHTTPError` + If you lack any of the applicable permissions + (``MANAGE_NICKNAMES``, ``MANAGE_ROLES``, ``MUTE_MEMBERS``, ``DEAFEN_MEMBERS`` or ``MOVE_MEMBERS``). + Note that to move a member you must also have permission to connect to the end channel. + This will also be raised if you're not in the guild. + :obj:`hikari.net.errors.BadRequestHTTPError` + If you pass ```mute``, ``deaf`` or ``channel_id`` while the member is not connected to a voice channel. """ payload = {} transformations.put_if_specified(payload, "nick", nick) @@ -1593,24 +1760,26 @@ async def modify_guild_member( # lgtm [py/similar-function] async def modify_current_user_nick( self, guild_id: str, nick: typing.Optional[str], *, reason: typing.Union[typing.Literal[...], str] = ..., ) -> None: - """ - Edits the current user's nickname for a given guild. + """Edits the current user's nickname for a given guild. - Args: - guild_id: - The ID of the guild you want to change the nick on. - nick: - The new nick string. Setting this to `None` clears the nickname. - reason: - Optional reason to add to audit logs for the guild explaining why the operation was performed. + Parameters + ---------- + guild_id : :obj:`str` + The ID of the guild you want to change the nick on. + nick : :obj:`str`, optional + The new nick string. Setting this to `None` clears the nickname. + reason : :obj:`str` + If specified, the audit log reason explaining why the operation + was performed. - Raises: - hikari.net.errors.NotFoundHTTPError: - If the guild is not found. - hikari.net.errors.ForbiddenHTTPError: - If you lack the `CHANGE_NICKNAME` permission or are not in the guild. - hikari.net.errors.BadRequestHTTPError: - If you provide a disallowed nickname, one that is too long, or one that is empty. + Raises + ------ + :obj:`hikari.net.errors.NotFoundHTTPError` + If the guild is not found. + :obj:`hikari.net.errors.ForbiddenHTTPError` + If you lack the ``CHANGE_NICKNAME`` permission or are not in the guild. + :obj:`hikari.net.errors.BadRequestHTTPError` + If you provide a disallowed nickname, one that is too long, or one that is empty. """ payload = {"nick": nick} route = routes.OWN_GUILD_NICKNAME.compile(self.PATCH, guild_id=guild_id) @@ -1619,24 +1788,26 @@ async def modify_current_user_nick( async def add_guild_member_role( self, guild_id: str, user_id: str, role_id: str, *, reason: typing.Union[typing.Literal[...], str] = ..., ) -> None: - """ - Adds a role to a given member. + """Adds a role to a given member. - Args: - guild_id: - The ID of the guild the member belongs to. - user_id: - The ID of the member you want to add the role to. - role_id: - The ID of the role you want to add. - reason: - Optional reason to add to audit logs for the guild explaining why the operation was performed. + Parameters + ---------- + guild_id : :obj:`str` + The ID of the guild the member belongs to. + user_id : :obj:`str` + The ID of the member you want to add the role to. + role_id : :obj:`str` + The ID of the role you want to add. + reason : :obj:`str` + If specified, the audit log reason explaining why the operation + was performed. - Raises: - hikari.net.errors.NotFoundHTTPError: - If either the guild, member or role aren't found. - hikari.net.errors.ForbiddenHTTPError: - If you lack the `MANAGE_ROLES` permission or are not in the guild. + Raises + ------ + :obj:`hikari.net.errors.NotFoundHTTPError` + If either the guild, member or role aren't found. + :obj:`hikari.net.errors.ForbiddenHTTPError` + If you lack the ``MANAGE_ROLES`` permission or are not in the guild. """ route = routes.GUILD_MEMBER_ROLE.compile(self.PUT, guild_id=guild_id, user_id=user_id, role_id=role_id) await self._request(route, reason=reason) @@ -1644,24 +1815,26 @@ async def add_guild_member_role( async def remove_guild_member_role( self, guild_id: str, user_id: str, role_id: str, *, reason: typing.Union[typing.Literal[...], str] = ..., ) -> None: - """ - Removed a role from a given member. + """Removed a role from a given member. - Args: - guild_id: - The ID of the guild the member belongs to. - user_id: - The ID of the member you want to remove the role from. - role_id: - The ID of the role you want to remove. - reason: - Optional reason to add to audit logs for the guild explaining why the operation was performed. + Parameters + ---------- + guild_id : :obj:`str` + The ID of the guild the member belongs to. + user_id : :obj:`str` + The ID of the member you want to remove the role from. + role_id : :obj:`str` + The ID of the role you want to remove. + reason : :obj:`str` + If specified, the audit log reason explaining why the operation + was performed. - Raises: - hikari.net.errors.NotFoundHTTPError: - If either the guild, member or role aren't found. - hikari.net.errors.ForbiddenHTTPError: - If you lack the `MANAGE_ROLES` permission or are not in the guild. + Raises + ------ + :obj:`hikari.net.errors.NotFoundHTTPError` + If either the guild, member or role aren't found. + :obj:`hikari.net.errors.ForbiddenHTTPError` + If you lack the ``MANAGE_ROLES`` permission or are not in the guild. """ route = routes.GUILD_MEMBER_ROLE.compile(self.DELETE, guild_id=guild_id, user_id=user_id, role_id=role_id) await self._request(route, reason=reason) @@ -1669,64 +1842,72 @@ async def remove_guild_member_role( async def remove_guild_member( self, guild_id: str, user_id: str, *, reason: typing.Union[typing.Literal[...], str] = ..., ) -> None: - """ - Kicks a user from a given guild. + """Kicks a user from a given guild. - Args: - guild_id: - The ID of the guild the member belongs to. - user_id: - The ID of the member you want to kick. - reason: - Optional reason to add to audit logs for the guild explaining why the operation was performed. + Parameters + ---------- + guild_id: :obj:`str` + The ID of the guild the member belongs to. + user_id: :obj:`str` + The ID of the member you want to kick. + reason : :obj:`str` + If specified, the audit log reason explaining why the operation + was performed. - Raises: - hikari.net.errors.NotFoundHTTPError: - If either the guild or member aren't found. - hikari.net.errors.ForbiddenHTTPError: - If you lack the `KICK_MEMBERS` permission or are not in the guild. + Raises + ------ + :obj:`hikari.net.errors.NotFoundHTTPError` + If either the guild or member aren't found. + :obj:`hikari.net.errors.ForbiddenHTTPError` + If you lack the ``KICK_MEMBERS`` permission or are not in the guild. """ route = routes.GUILD_MEMBER.compile(self.DELETE, guild_id=guild_id, user_id=user_id) await self._request(route, reason=reason) async def get_guild_bans(self, guild_id: str) -> typing.Sequence[typing.Dict]: - """ - Gets the bans for a given guild. + """Gets the bans for a given guild. - Args: - guild_id: - The ID of the guild you want to get the bans from. + Parameters + ---------- + guild_id : :obj:`str` + The ID of the guild you want to get the bans from. - Returns: + Returns + ------- + :obj:`typing.Sequence` [ :obj:`typing.Dict` ] A list of ban objects. - Raises: - hikari.net.errors.NotFoundHTTPError: - If the guild is not found. - hikari.net.errors.ForbiddenHTTPError: - If you lack the `BAN_MEMBERS` permission or are not in the guild. + Raises + ------ + :obj:`hikari.net.errors.NotFoundHTTPError` + If the guild is not found. + :obj:`hikari.net.errors.ForbiddenHTTPError` + If you lack the ``BAN_MEMBERS`` permission or are not in the guild. """ route = routes.GUILD_BANS.compile(self.GET, guild_id=guild_id) return await self._request(route) async def get_guild_ban(self, guild_id: str, user_id: str) -> typing.Dict: - """ - Gets a ban from a given guild. + """Gets a ban from a given guild. - Args: - guild_id: - The ID of the guild you want to get the ban from. - user_id: - The ID of the user to get the ban information for. + Parameters + ---------- + guild_id : :obj:`str` + The ID of the guild you want to get the ban from. + user_id : :obj:`str` + The ID of the user to get the ban information for. - Returns: + Returns + ------- + :obj:`typing.Dict` A ban object for the requested user. - Raises: - hikari.net.errors.NotFoundHTTPError: - If either the guild or the user aren't found, or if the user is not banned. - hikari.net.errors.ForbiddenHTTPError: - If you lack the `BAN_MEMBERS` permission or are not in the guild. + Raises + ------ + :obj:`hikari.net.errors.NotFoundHTTPError` + If either the guild or the user aren't found, or if the user is not banned. + :obj:`hikari.net.errors.ForbiddenHTTPError` + If you lack the ``BAN_MEMBERS`` permission or are not in the guild. """ route = routes.GUILD_BAN.compile(self.GET, guild_id=guild_id, user_id=user_id) return await self._request(route) @@ -1739,24 +1920,27 @@ async def create_guild_ban( delete_message_days: typing.Union[typing.Literal[...], int] = ..., reason: typing.Union[typing.Literal[...], str] = ..., ) -> None: - """ - Bans a user from a given guild. + """Bans a user from a given guild. - Args: - guild_id: - The ID of the guild the member belongs to. - user_id: - The ID of the member you want to ban. - delete_message_days: - How many days of messages from the user should be removed. Default is to not delete anything. - reason: - Optional reason to add to audit logs for the guild explaining why the operation was performed. + Parameters + ---------- + guild_id : :obj:`str` + The ID of the guild the member belongs to. + user_id : :obj:`str` + The ID of the member you want to ban. + delete_message_days : :obj:`str` + If specified, how many days of messages from the user should + be removed. + reason : :obj:`str` + If specified, the audit log reason explaining why the operation + was performed. - Raises: - hikari.net.errors.NotFoundHTTPError: - If either the guild or member aren't found. - hikari.net.errors.ForbiddenHTTPError: - If you lack the `BAN_MEMBERS` permission or are not in the guild. + Raises + ------ + :obj:`hikari.net.errors.NotFoundHTTPError` + If either the guild or member aren't found. + :obj:`hikari.net.errors.ForbiddenHTTPError` + If you lack the ``BAN_MEMBERS`` permission or are not in the guild. """ query = {} transformations.put_if_specified(query, "delete-message-days", delete_message_days) @@ -1767,42 +1951,47 @@ async def create_guild_ban( async def remove_guild_ban( self, guild_id: str, user_id: str, *, reason: typing.Union[typing.Literal[...], str] = ..., ) -> None: - """ - Un-bans a user from a given guild. + """Un-bans a user from a given guild. - Args: - guild_id: - The ID of the guild to un-ban the user from. - user_id: - The ID of the user you want to un-ban. - reason: - Optional reason to add to audit logs for the guild explaining why the operation was performed. + Parameters + ---------- + guild_id : :obj:`str` + The ID of the guild to un-ban the user from. + user_id : :obj:`str` + The ID of the user you want to un-ban. + reason : :obj:`str` + If specified, the audit log reason explaining why the operation + was performed. - Raises: - hikari.net.errors.NotFoundHTTPError: - If either the guild or member aren't found, or the member is not banned. - hikari.net.errors.ForbiddenHTTPError: - If you lack the `BAN_MEMBERS` permission or are not a in the guild. + Raises + ------ + :obj:`hikari.net.errors.NotFoundHTTPError` + If either the guild or member aren't found, or the member is not banned. + :obj:`hikari.net.errors.ForbiddenHTTPError` + If you lack the ``BAN_MEMBERS`` permission or are not a in the guild. """ route = routes.GUILD_BAN.compile(self.DELETE, guild_id=guild_id, user_id=user_id) await self._request(route, reason=reason) async def get_guild_roles(self, guild_id: str) -> typing.Sequence[typing.Dict]: - """ - Gets the roles for a given guild. + """Gets the roles for a given guild. - Args: - guild_id: - The ID of the guild you want to get the roles from. + Parameters + ---------- + guild_id : :obj:`str` + The ID of the guild you want to get the roles from. - Returns: + Returns + ------- + :obj:`typing.Sequence` [ :obj:`typing.Dict` ] A list of role objects. - Raises: - hikari.net.errors.NotFoundHTTPError: - If the guild is not found. - hikari.net.errors.ForbiddenHTTPError: - If you're not in the guild. + Raises + ------ + :obj:`hikari.net.errors.NotFoundHTTPError` + If the guild is not found. + :obj:`hikari.net.errors.ForbiddenHTTPError` + If you're not in the guild. """ route = routes.GUILD_ROLES.compile(self.GET, guild_id=guild_id) return await self._request(route) @@ -1818,35 +2007,39 @@ async def create_guild_role( mentionable: typing.Union[typing.Literal[...], bool] = ..., reason: typing.Union[typing.Literal[...], str] = ..., ) -> typing.Dict: - """ - Creates a new role for a given guild. - - Args: - guild_id: - The ID of the guild you want to create the role on. - name: - The new role name string. - permissions: - The permissions integer for the role. - color: - The color for the new role. - hoist: - Whether the role should hoist or not. - mentionable: - Whether the role should be able to be mentioned by users or not. - reason: - Optional reason to add to audit logs for the guild explaining why the operation was performed. - - Returns: + """Creates a new role for a given guild. + + Parameters + ---------- + guild_id : :obj:`str` + The ID of the guild you want to create the role on. + name : :obj:`str` + If specified, the new role name string. + permissions : :obj:`int` + If specified, the permissions integer for the role. + color : :obj:`int` + If specified, the color for the role. + hoist : :obj:`bool` + If specified, whether the role will be hoisted. + mentionable : :obj:`bool` + If specified, whether the role will be able to be mentioned by any user. + reason : :obj:`str` + If specified, the audit log reason explaining why the operation + was performed. + + Returns + ------- + :obj:`typing.Dict` The newly created role object. - Raises: - hikari.net.errors.NotFoundHTTPError: - If the guild is not found. - hikari.net.errors.ForbiddenHTTPError: - If you lack the `MANAGE_ROLES` permission or you're not in the guild. - hikari.net.errors.BadRequestHTTPError: - If you provide invalid values for the role attributes. + Raises + ------ + :obj:`hikari.net.errors.NotFoundHTTPError` + If the guild is not found. + :obj:`hikari.net.errors.ForbiddenHTTPError` + If you lack the ``MANAGE_ROLES`` permission or you're not in the guild. + :obj:`hikari.net.errors.BadRequestHTTPError` + If you provide invalid values for the role attributes. """ payload = {} transformations.put_if_specified(payload, "name", name) @@ -1860,27 +2053,30 @@ async def create_guild_role( async def modify_guild_role_positions( self, guild_id: str, role: typing.Tuple[str, int], *roles: typing.Tuple[str, int] ) -> typing.Sequence[typing.Dict]: - """ - Edits the position of two or more roles in a given guild. + """Edits the position of two or more roles in a given guild. - Args: - guild_id: - The ID of the guild the roles belong to. - role: - The first role to move. - roles: - Optional extra roles to move. + Parameters + ---------- + guild_id: + The ID of the guild the roles belong to. + role: + The first role to move. + *roles: + Optional extra roles to move. - Returns: + Returns + ------- + :obj:`typing.Sequence` [ :obj:`typing.Dict` ] A list of all the guild roles. - Raises: - hikari.net.errors.NotFoundHTTPError: - If either the guild or any of the roles aren't found. - hikari.net.errors.ForbiddenHTTPError: - If you lack the `MANAGE_ROLES` permission or you're not in the guild. - hikari.net.errors.BadRequestHTTPError: - If you provide invalid values for the `position` fields. + Raises + ------ + :obj:`hikari.net.errors.NotFoundHTTPError` + If either the guild or any of the roles aren't found. + :obj:`hikari.net.errors.ForbiddenHTTPError` + If you lack the ``MANAGE_ROLES`` permission or you're not in the guild. + :obj:`hikari.net.errors.BadRequestHTTPError` + If you provide invalid values for the `position` fields. """ payload = [{"id": r[0], "position": r[1]} for r in (role, *roles)] route = routes.GUILD_ROLES.compile(self.PATCH, guild_id=guild_id) @@ -1898,37 +2094,41 @@ async def modify_guild_role( # lgtm [py/similar-function] mentionable: typing.Union[typing.Literal[...], bool] = ..., reason: typing.Union[typing.Literal[...], str] = ..., ) -> typing.Dict: - """ - Edits a role in a given guild. - - Args: - guild_id: - The ID of the guild the role belong to. - role_id: - The ID of the role you want to edit. - name: - THe new role's name string. - permissions: - The new permissions integer for the role. - color: - The new color for the new role. - hoist: - Whether the role should hoist or not. - mentionable: - Whether the role should be mentionable or not. - reason: - Optional reason to add to audit logs for the guild explaining why the operation was performed. + """Edits a role in a given guild. + + Parameters + ---------- + guild_id : :obj:`str` + The ID of the guild the role belong to. + role_id : :obj:`str` + The ID of the role you want to edit. + name : :obj:`str` + If specified, the new role's name string. + permissions : :obj:`int` + If specified, the new permissions integer for the role. + color : :obj:`int` + If specified, the new color for the new role. + hoist : :obj:`bool` + If specified, whether the role should hoist or not. + mentionable : :obj:`bool` + If specified, whether the role should be mentionable or not. + reason : :obj:`str` + If specified, the audit log reason explaining why the operation + was performed. - Returns: + Returns + ------- + :obj:`typing.Dict` The edited role object. - Raises: - hikari.net.errors.NotFoundHTTPError: - If either the guild or role aren't found. - hikari.net.errors.ForbiddenHTTPError: - If you lack the `MANAGE_ROLES` permission or you're not in the guild. - hikari.net.errors.BadRequestHTTPError: - If you provide invalid values for the role attributes. + Raises + ------ + :obj:`hikari.net.errors.NotFoundHTTPError` + If either the guild or role aren't found. + :obj:`hikari.net.errors.ForbiddenHTTPError` + If you lack the ``MANAGE_ROLES`` permission or you're not in the guild. + :obj:`hikari.net.errors.BadRequestHTTPError` + If you provide invalid values for the role attributes. """ payload = {} transformations.put_if_specified(payload, "name", name) @@ -1940,44 +2140,48 @@ async def modify_guild_role( # lgtm [py/similar-function] return await self._request(route, json_body=payload, reason=reason) async def delete_guild_role(self, guild_id: str, role_id: str) -> None: - """ - Deletes a role from a given guild. + """Deletes a role from a given guild. - Args: - guild_id: - The ID of the guild you want to remove the role from. - role_id: - The ID of the role you want to delete. + Parameters + ---------- + guild_id : :obj:`str` + The ID of the guild you want to remove the role from. + role_id : :obj:`str` + The ID of the role you want to delete. - Raises: - hikari.net.errors.NotFoundHTTPError: - If either the guild or the role aren't found. - hikari.net.errors.ForbiddenHTTPError: - If you lack the `MANAGE_ROLES` permission or are not in the guild. + Raises + ------ + :obj:`hikari.net.errors.NotFoundHTTPError` + If either the guild or the role aren't found. + :obj:`hikari.net.errors.ForbiddenHTTPError` + If you lack the ``MANAGE_ROLES`` permission or are not in the guild. """ route = routes.GUILD_ROLE.compile(self.DELETE, guild_id=guild_id, role_id=role_id) await self._request(route) async def get_guild_prune_count(self, guild_id: str, days: int) -> int: - """ - Gets the estimated prune count for a given guild. + """Gets the estimated prune count for a given guild. - Args: - guild_id: - The ID of the guild you want to get the count for. - days: - The number of days to count prune for (at least 1). + Parameters + ---------- + guild_id : :obj:`str` + The ID of the guild you want to get the count for. + days : :obj:`int` + The number of days to count prune for (at least 1). - Returns: - the number of members estimated to be pruned. + Returns + ------- + :obj:`int` + The number of members estimated to be pruned. - Raises: - hikari.net.errors.NotFoundHTTPError: - If the guild is not found. - hikari.net.errors.ForbiddenHTTPError: - If you lack the `KICK_MEMBERS` or you are not in the guild. - hikari.net.errors.BadRequestHTTPError: - If you pass an invalid amount of days. + Raises + ------ + :obj:`hikari.net.errors.NotFoundHTTPError` + If the guild is not found. + :obj:`hikari.net.errors.ForbiddenHTTPError` + If you lack the ``KICK_MEMBERS`` or you are not in the guild. + :obj:`hikari.net.errors.BadRequestHTTPError` + If you pass an invalid amount of days. """ payload = {"days": days} route = routes.GUILD_PRUNE.compile(self.GET, guild_id=guild_id) @@ -1992,35 +2196,38 @@ async def begin_guild_prune( compute_prune_count: typing.Union[typing.Literal[...], bool] = ..., reason: typing.Union[typing.Literal[...], str] = ..., ) -> typing.Optional[int]: - """ - Prunes members of a given guild based on the number of inactive days. - - Args: - guild_id: - The ID of the guild you want to prune member of. - days: - The number of inactivity days you want to use as filter. - compute_prune_count: - Whether a count of pruned members is returned or not. Discouraged for large guilds out of politeness. - reason: - Optional reason to add to audit logs for the guild explaining why the operation was performed. - - Returns: - or `None` if `compute_prune_count` is `False`, or an :class:`int` representing the number - of members who were kicked. - - Raises: - hikari.net.errors.NotFoundHTTPError: - If the guild is not found: - hikari.net.errors.ForbiddenHTTPError: - If you lack the `KICK_MEMBER` permission or are not in the guild. - hikari.net.errors.BadRequestHTTPError: - If you provide invalid values for the `days` and `compute_prune_count` fields. - """ - query = { - "days": days, - "compute_prune_count": compute_prune_count if compute_prune_count is not ... else False, - } + """Prunes members of a given guild based on the number of inactive days. + + Parameters + ---------- + guild_id : :obj:`str` + The ID of the guild you want to prune member of. + days : :obj:`int` + The number of inactivity days you want to use as filter. + compute_prune_count : :obj:`bool` + Whether a count of pruned members is returned or not. + Discouraged for large guilds out of politeness. + reason : :obj:`str` + If specified, the audit log reason explaining why the operation + was performed. + + Returns + ------- + :obj:`int`, optional + The number of members who were kicked if ``compute_prune_count`` + is ``True``, else ``None``. + + Raises + ------ + :obj:`hikari.net.errors.NotFoundHTTPError` + If the guild is not found: + :obj:`hikari.net.errors.ForbiddenHTTPError` + If you lack the ``KICK_MEMBER`` permission or are not in the guild. + :obj:`hikari.net.errors.BadRequestHTTPError` + If you provide invalid values for the ``days`` or ``compute_prune_count`` fields. + """ + query = {"days": days} + transformations.put_if_specified(query, "compute_prune_count", compute_prune_count, str) route = routes.GUILD_PRUNE.compile(self.POST, guild_id=guild_id) result = await self._request(route, query=query, reason=reason) @@ -2030,61 +2237,70 @@ async def begin_guild_prune( return None async def get_guild_voice_regions(self, guild_id: str) -> typing.Sequence[typing.Dict]: - """ - Gets the voice regions for a given guild. + """Gets the voice regions for a given guild. - Args: - guild_id: - The ID of the guild to get the voice regions for. + Parameters + ---------- + guild_id : :obj:`str` + The ID of the guild to get the voice regions for. - Returns: + Returns + ------- + :obj:`typing.Sequence` [ :obj:`typing.Dict` ] A list of voice region objects. - Raises: - hikari.net.errors.NotFoundHTTPError: - If the guild is not found: - hikari.net.errors.ForbiddenHTTPError: - If you are not in the guild. + Raises + ------ + :obj:`hikari.net.errors.NotFoundHTTPError` + If the guild is not found. + :obj:`hikari.net.errors.ForbiddenHTTPError` + If you are not in the guild. """ route = routes.GUILD_VOICE_REGIONS.compile(self.GET, guild_id=guild_id) return await self._request(route) async def get_guild_invites(self, guild_id: str) -> typing.Sequence[typing.Dict]: - """ - Gets the invites for a given guild. + """Gets the invites for a given guild. - Args: - guild_id: - The ID of the guild to get the invites for. + Parameters + ---------- + guild_id : :obj:`str` + The ID of the guild to get the invites for. - Returns: + Returns + ------- + :obj:`typing.Sequence` [ :obj:`typing.Dict` ] A list of invite objects (with metadata). - Raises: - hikari.net.errors.NotFoundHTTPError: - If the guild is not found. - hikari.net.errors.ForbiddenHTTPError: - If you lack the `MANAGE_GUILD` permission or are not in the guild. + Raises + ------ + :obj:`hikari.net.errors.NotFoundHTTPError` + If the guild is not found. + :obj:`hikari.net.errors.ForbiddenHTTPError` + If you lack the ``MANAGE_GUILD`` permission or are not in the guild. """ route = routes.GUILD_INVITES.compile(self.GET, guild_id=guild_id) return await self._request(route) async def get_guild_integrations(self, guild_id: str) -> typing.Sequence[typing.Dict]: - """ - Gets the integrations for a given guild. + """Gets the integrations for a given guild. - Args: - guild_id: - The ID of the guild to get the integrations for. + Parameters + ---------- + guild_id: + The ID of the guild to get the integrations for. - Returns: + Returns + ------- + :obj:`typing.Sequence` [ :obj:`typing.Dict` ] A list of integration objects. - Raises: - hikari.net.errors.NotFoundHTTPError: - If the guild is not found. - hikari.net.errors.ForbiddenHTTPError: - If you lack the `MANAGE_GUILD` permission or are not in the guild. + Raises + ------ + :obj:`hikari.net.errors.NotFoundHTTPError` + If the guild is not found. + :obj:`hikari.net.errors.ForbiddenHTTPError` + If you lack the ``MANAGE_GUILD`` permission or are not in the guild. """ route = routes.GUILD_INTEGRATIONS.compile(self.GET, guild_id=guild_id) return await self._request(route) @@ -2092,27 +2308,31 @@ async def get_guild_integrations(self, guild_id: str) -> typing.Sequence[typing. async def create_guild_integration( self, guild_id: str, type_: str, integration_id: str, *, reason: typing.Union[typing.Literal[...], str] = ..., ) -> typing.Dict: - """ - Creates an integrations for a given guild. + """Creates an integrations for a given guild. - Args: - guild_id: - The ID of the guild to create the integrations in. - type_: - The integration type string (e.g. "twitch" or "youtube"). - integration_id: - The ID for the new integration. - reason: - Optional reason to add to audit logs for the guild explaining why the operation was performed. + Parameters + ---------- + guild_id : :obj:`str` + The ID of the guild to create the integrations in. + type_ : :obj:`str` + The integration type string (e.g. "twitch" or "youtube"). + integration_id : :obj:`str` + The ID for the new integration. + reason : :obj:`str` + If specified, the audit log reason explaining why the operation + was performed. - Returns: + Returns + ------- + :obj:`typing.Dict` The newly created integration object. - Raises: - hikari.net.errors.NotFoundHTTPError: - If the guild is not found. - hikari.net.errors.ForbiddenHTTPError: - If you lack the `MANAGE_GUILD` permission or are not in the guild. + Raises + ------ + :obj:`hikari.net.errors.NotFoundHTTPError` + If the guild is not found. + :obj:`hikari.net.errors.ForbiddenHTTPError` + If you lack the ``MANAGE_GUILD`` permission or are not in the guild. """ payload = {"type": type_, "id": integration_id} route = routes.GUILD_INTEGRATIONS.compile(self.POST, guild_id=guild_id) @@ -2128,28 +2348,32 @@ async def modify_guild_integration( enable_emojis: typing.Union[typing.Literal[...], bool] = ..., reason: typing.Union[typing.Literal[...], str] = ..., ) -> None: - """ - Edits an integrations for a given guild. - - Args: - guild_id: - The ID of the guild to which the integration belongs to. - integration_id: - The ID of the integration. - expire_behaviour: - The behaviour for when an integration subscription lapses. - expire_grace_period: - Time interval in seconds in which the integration will ignore lapsed subscriptions. - enable_emojis: - Whether emojis should be synced for this integration. - reason: - Optional reason to add to audit logs for the guild explaining why the operation was performed. - - Raises: - hikari.net.errors.NotFoundHTTPError: - If either the guild or the integration aren't found. - hikari.net.errors.ForbiddenHTTPError: - If you lack the `MANAGE_GUILD` permission or are not in the guild. + """Edits an integrations for a given guild. + + Parameters + ---------- + guild_id : :obj:`str` + The ID of the guild to which the integration belongs to. + integration_id : :obj:`str` + The ID of the integration. + expire_behaviour : :obj:`int` + If specified, the behaviour for when an integration subscription + lapses. + expire_grace_period : :obj:`int` + If specified, time interval in seconds in which the integration + will ignore lapsed subscriptions. + enable_emojis : :obj:`bool` + If specified, whether emojis should be synced for this integration. + reason : :obj:`str` + If specified, the audit log reason explaining why the operation + was performed. + + Raises + ------ + :obj:`hikari.net.errors.NotFoundHTTPError` + If either the guild or the integration aren't found. + :obj:`hikari.net.errors.ForbiddenHTTPError` + If you lack the ``MANAGE_GUILD`` permission or are not in the guild. """ payload = {} transformations.put_if_specified(payload, "expire_behaviour", expire_behaviour) @@ -2162,61 +2386,67 @@ async def modify_guild_integration( async def delete_guild_integration( self, guild_id: str, integration_id: str, *, reason: typing.Union[typing.Literal[...], str] = ..., ) -> None: - """ - Deletes an integration for the given guild. + """Deletes an integration for the given guild. - Args: - guild_id: - The ID of the guild from which to delete an integration. - integration_id: - The ID of the integration to delete. - reason: - Optional reason to add to audit logs for the guild explaining why the operation was performed. + Parameters + ---------- + guild_id : :obj:`str` + The ID of the guild to which the integration belongs to. + integration_id : :obj:`str` + The ID of the integration to delete. + reason : :obj:`str` + If specified, the audit log reason explaining why the operation + was performed. - Raises: - hikari.net.errors.NotFoundHTTPError: + Raises + ------ + :obj:`hikari.net.errors.NotFoundHTTPError` If either the guild or the integration aren't found. - hikari.net.errors.ForbiddenHTTPError: + :obj:`hikari.net.errors.ForbiddenHTTPError` If you lack the `MANAGE_GUILD` permission or are not in the guild. """ route = routes.GUILD_INTEGRATION.compile(self.DELETE, guild_id=guild_id, integration_id=integration_id) await self._request(route, reason=reason) async def sync_guild_integration(self, guild_id: str, integration_id: str) -> None: - """ - Syncs the given integration. + """Syncs the given integration. - Args: - guild_id: - The ID of the guild to which the integration belongs to. - integration_id: - The ID of the integration to sync. + Parameters + ---------- + guild_id : :obj:`str` + The ID of the guild to which the integration belongs to. + integration_id : :obj:`str` + The ID of the integration to sync. - Raises: - hikari.net.errors.NotFoundHTTPError: - If either the guild or the integration aren't found. - hikari.net.errors.ForbiddenHTTPError: - If you lack the `MANAGE_GUILD` permission or are not in the guild. + Raises + ------ + :obj:`hikari.net.errors.NotFoundHTTPError` + If either the guild or the integration aren't found. + :obj:`hikari.net.errors.ForbiddenHTTPError` + If you lack the ``MANAGE_GUILD`` permission or are not in the guild. """ route = routes.GUILD_INTEGRATION_SYNC.compile(self.POST, guild_id=guild_id, integration_id=integration_id) await self._request(route) async def get_guild_embed(self, guild_id: str) -> typing.Dict: - """ - Gets the embed for a given guild. + """Gets the embed for a given guild. - Args: - guild_id: - The ID of the guild to get the embed for. + Parameters + ---------- + guild_id : :obj:`str` + The ID of the guild to get the embed for. - Returns: + Returns + ------- + :obj:`typing.Dict` A guild embed object. - Raises: - hikari.net.errors.NotFoundHTTPError: - If the guild is not found. - hikari.net.errors.ForbiddenHTTPError: - If you either lack the `MANAGE_GUILD` permission or are not in the guild. + Raises + ------ + :obj:`hikari.net.errors.NotFoundHTTPError` + If the guild is not found. + :obj:`hikari.net.errors.ForbiddenHTTPError` + If you either lack the ``MANAGE_GUILD`` permission or are not in the guild. """ route = routes.GUILD_EMBED.compile(self.GET, guild_id=guild_id) return await self._request(route) @@ -2224,25 +2454,29 @@ async def get_guild_embed(self, guild_id: str) -> typing.Dict: async def modify_guild_embed( self, guild_id: str, embed: typing.Dict, *, reason: typing.Union[typing.Literal[...], str] = ..., ) -> typing.Dict: - """ - Edits the embed for a given guild. + """Edits the embed for a given guild. - Args: - guild_id: - The ID of the guild to edit the embed for. - embed: - The new embed object to be set. - reason: - Optional reason to add to audit logs for the guild explaining why the operation was performed. + Parameters + ---------- + guild_id : :obj:`str` + The ID of the guild to edit the embed for. + embed : :obj:`typing.Dict` + The new embed object to be set. + reason : :obj:`str` + If specified, the audit log reason explaining why the operation + was performed. - Returns: + Returns + ------- + :obj:`typing.Dict` The updated embed object. - Raises: - hikari.net.errors.NotFoundHTTPError: - If the guild is not found. - hikari.net.errors.ForbiddenHTTPError: - If you either lack the `MANAGE_GUILD` permission or are not in the guild. + Raises + ------ + :obj:`hikari.net.errors.NotFoundHTTPError` + If the guild is not found. + :obj:`hikari.net.errors.ForbiddenHTTPError` + If you either lack the ``MANAGE_GUILD`` permission or are not in the guild. """ route = routes.GUILD_EMBED.compile(self.PATCH, guild_id=guild_id) return await self._request(route, json_body=embed, reason=reason) @@ -2251,41 +2485,51 @@ async def get_guild_vanity_url(self, guild_id: str) -> typing.Dict: """ Gets the vanity URL for a given guild. - Args: - guild_id: - The ID of the guild to get the vanity URL for. + Parameters + ---------- + guild_id : :obj:`str` + The ID of the guild to get the vanity URL for. - Returns: - A partial invite object containing the vanity URL in the `code` field. + Returns + ------- + :obj:`typing.Dict` + A partial invite object containing the vanity URL in the ``code`` field. - Raises: - hikari.net.errors.NotFoundHTTPError: - If the guild is not found. - hikari.net.errors.ForbiddenHTTPError: - If you either lack the `MANAGE_GUILD` permission or are not in the guild. + Raises + ------ + :obj:`hikari.net.errors.NotFoundHTTPError` + If the guild is not found. + :obj:`hikari.net.errors.ForbiddenHTTPError` + If you either lack the ``MANAGE_GUILD`` permission or are not in the guild. """ route = routes.GUILD_VANITY_URL.compile(self.GET, guild_id=guild_id) return await self._request(route) - def get_guild_widget_image_url(self, guild_id: str, *, style: typing.Union[typing.Literal[...], str] = ...) -> str: - """ - Get the URL for a guild widget. + def get_guild_widget_image_url( + self, guild_id: str, *, style: typing.Literal[..., "shield", "banner1", "banner2", "banner3", "banner4"] = ..., + ) -> str: + """Get the URL for a guild widget. - Args: - guild_id: - The guild ID to use for the widget. - style: - Optional and one of "shield", "banner1", "banner2", "banner3" or "banner4". + Parameters + ---------- + guild_id : :obj:`str` + The guild ID to use for the widget. + style : :obj:`typing.Literal` [ ``"shield"``, ``"banner1"``, ``"banner2"``, ``"banner3"``, ``"banner4"`` ] + If specified, the syle of the widget. - Returns: + Returns + ------- + :obj:`str` A URL to retrieve a PNG widget for your guild. - Note: - This does not actually make any form of request, and shouldn't be awaited. Thus, it doesn't have rate limits - either. + Note + ---- + This does not actually make any form of request, and shouldn't be awaited. + Thus, it doesn't have rate limits either. - Warning: - The guild must have the widget enabled in the guild settings for this to be valid. + Warning + ------- + The guild must have the widget enabled in the guild settings for this to be valid. """ query = "" if style is ... else f"?style={style}" return f"{self.base_url}/guilds/{guild_id}/widget.png" + query @@ -2293,91 +2537,84 @@ def get_guild_widget_image_url(self, guild_id: str, *, style: typing.Union[typin async def get_invite( self, invite_code: str, *, with_counts: typing.Union[typing.Literal[...], bool] = ... ) -> typing.Dict: - """ - Gets the given invite. + """Gets the given invite. - Args: - invite_code: - The ID for wanted invite. - with_counts: - If `True`, attempt to count the number of times the invite has been used, otherwise (and as the - default), do not try to track this information. + Parameters + ---------- + invite_code : :str: + The ID for wanted invite. + with_counts : :bool: + If specified, wheter to attempt to count the number of + times the invite has been used. - Returns: + Returns + ------- + :obj:`typing.Dict` The requested invite object. - Raises: - hikari.net.errors.NotFoundHTTPError: - If the invite is not found. + Raises + ------ + :obj:`hikari.net.errors.NotFoundHTTPError` + If the invite is not found. """ query = {} transformations.put_if_specified(query, "with_counts", with_counts, str) route = routes.INVITE.compile(self.GET, invite_code=invite_code) return await self._request(route, query=query) - async def delete_invite(self, invite_code: str) -> typing.Dict: - """ - Deletes a given invite. + async def delete_invite(self, invite_code: str) -> None: + """Deletes a given invite. - Args: - invite_code: - The ID for the invite to be deleted. + Parameters + ---------- + invite_code : :obj:`str` + The ID for the invite to be deleted. - Returns: - The deleted invite object. + Returns + ------- + ``None`` # Marker + Nothing, unlike what the API specifies. This is done to maintain + consistency with other calls of a similar nature in this API wrapper. - Raises: - hikari.net.errors.NotFoundHTTPError: - If the invite is not found. - hikari.net.errors.ForbiddenHTTPError - If you lack either `MANAGE_CHANNELS` on the channel the invite belongs to or `MANAGE_GUILD` for - guild-global delete. + Raises + ------ + :obj:`hikari.net.errors.NotFoundHTTPError` + If the invite is not found. + :obj:`hikari.net.errors.ForbiddenHTTPError` + If you lack either ``MANAGE_CHANNELS`` on the channel the invite + belongs to or ``MANAGE_GUILD`` for guild-global delete. """ route = routes.INVITE.compile(self.DELETE, invite_code=invite_code) return await self._request(route) - ########## - # OAUTH2 # - ########## - - async def get_current_application_info(self) -> typing.Dict: - """ - Get the current application information. - - Returns: - An application info object. - """ - route = routes.OAUTH2_APPLICATIONS_ME.compile(self.GET) - return await self._request(route) - - ########## - # USERS # - ########## - async def get_current_user(self) -> typing.Dict: - """ - Gets the current user that is represented by token given to the client. + """Gets the current user that is represented by token given to the client. - Returns: + Returns + ------- + :obj:`typing.Dict` The current user object. """ route = routes.OWN_USER.compile(self.GET) return await self._request(route) async def get_user(self, user_id: str) -> typing.Dict: - """ - Gets a given user. + """Gets a given user. - Args: - user_id: - The ID of the user to get. + Parameters + ---------- + user_id : :obj:`str` + The ID of the user to get. - Returns: + Returns + ------- + :obj:`typing.Dict` The requested user object. - Raises: - hikari.net.errors.NotFoundHTTPError: - If the user is not found. + Raises + ------ + :obj:`hikari.net.errors.NotFoundHTTPError` + If the user is not found. """ route = routes.USER.compile(self.GET, user_id=user_id) return await self._request(route) @@ -2386,24 +2623,27 @@ async def modify_current_user( self, *, username: typing.Union[typing.Literal[...], str] = ..., - avatar: typing.Union[None, typing.Literal[...], bytes] = ..., + avatar: typing.Optional[typing.Union[typing.Literal[...], bytes]] = ..., ) -> typing.Dict: - """ - Edits the current user. If any arguments are unspecified, then that subject is not changed on Discord. + """Edits the current user. - Args: - username: - The new username string. If unspecified, then it is not changed. - avatar: - The new avatar image in bytes form. If unspecified, then it is not changed. If it is `None`, the - avatar is removed. + Parameters + ---------- + username : :obj:`str` + If specified, the new username string. + avatar : :obj:`bytes` + If specified, the new avatar image in bytes form. + If it is ``None``, the avatar is removed. - Returns: + Returns + ------- + :obj:`typing.Dict` The updated user object. - Raises: - hikari.net.errors.BadRequestHTTPError: - If you pass username longer than the limit (2-32) or an invalid image. + Raises + ------ + :obj:`hikari.net.errors.BadRequestHTTPError` + If you pass username longer than the limit (2-32) or an invalid image. """ payload = {} transformations.put_if_specified(payload, "username", username) @@ -2413,11 +2653,14 @@ async def modify_current_user( async def get_current_user_connections(self) -> typing.Sequence[typing.Dict]: """ - Gets the current user's connections. This endpoint can be used with both Bearer and Bot tokens - but will usually return an empty list for bots (with there being some exceptions to this - like user accounts that have been converted to bots). + Gets the current user's connections. This endpoint can be + used with both ``Bearer`` and ``Bot`` tokens but will usually return an + empty list for bots (with there being some exceptions to this, like + user accounts that have been converted to bots). - Returns: + Returns + ------- + :obj:`typing.Sequence` [ :obj:`typing.Dict` ] A list of connection objects. """ route = routes.OWN_CONNECTIONS.compile(self.GET) @@ -2430,15 +2673,30 @@ async def get_current_user_guilds( after: typing.Union[typing.Literal[...], str] = ..., limit: typing.Union[typing.Literal[...], int] = ..., ) -> typing.Sequence[typing.Dict]: - """ - Gets the guilds the current user is in. + """Gets the guilds the current user is in. + + Parameters + ---------- + before : :obj:`str` + If specified, the guild ID to get guilds before it. + + after : :obj:`str` + If specified, the guild ID to get guilds after it. + + limit : :obj:`int` + If specified, the limit of guilds to get. Has to be between + ``1`` and ``100``. - Returns: + Returns + ------- + :obj:`typing.Sequence` [ :obj:`typing.Dict` ] A list of partial guild objects. - Raises: - hikari.net.errors.BadRequestHTTPError: - If you pass both `before` and `after`. + Raises + ------ + :obj:`hikari.net.errors.BadRequestHTTPError` + If you pass both ``before`` and ``after`` or an + invalid value for ``limit``. """ query = {} transformations.put_if_specified(query, "before", before) @@ -2448,48 +2706,54 @@ async def get_current_user_guilds( return await self._request(route, query=query) async def leave_guild(self, guild_id: str) -> None: - """ - Makes the current user leave a given guild. + """Makes the current user leave a given guild. - Args: - guild_id: - The ID of the guild to leave. + Parameters + ---------- + guild_id : :obj:`str` + The ID of the guild to leave. - Raises: - hikari.net.errors.NotFoundHTTPError: - If the guild is not found. + Raises + ------ + :obj:`hikari.net.errors.NotFoundHTTPError` + If the guild is not found. """ route = routes.LEAVE_GUILD.compile(self.DELETE, guild_id=guild_id) await self._request(route) async def create_dm(self, recipient_id: str) -> typing.Dict: - """ - Creates a new DM channel with a given user. + """Creates a new DM channel with a given user. - Args: - recipient_id: - The ID of the user to create the new DM channel with. + Parameters + ---------- + recipient_id : :obj:`str` + The ID of the user to create the new DM channel with. - Returns: + Returns + ------- + :obj:`typing.Dict` The newly created DM channel object. - Raises: - hikari.net.errors.NotFoundHTTPError: - If the recipient is not found. + Raises + ------ + :obj:`hikari.net.errors.NotFoundHTTPError` + If the recipient is not found. """ payload = {"recipient_id": recipient_id} route = routes.OWN_DMS.compile(self.POST) return await self._request(route, json_body=payload) async def list_voice_regions(self) -> typing.Sequence[typing.Dict]: - """ - Get the voice regions that are available. + """Get the voice regions that are available. - Returns: + Returns + ------- + :obj:`typing.Sequence` [ :obj:`typing.Dict` ] A list of voice regions available - Note: - This does not include VIP servers. + Note + ---- + This does not include VIP servers. """ route = routes.VOICE_REGIONS.compile(self.GET) return await self._request(route) @@ -2505,26 +2769,32 @@ async def create_webhook( """ Creates a webhook for a given channel. - Args: - channel_id: - The ID of the channel for webhook to be created in. - name: - The webhook's name string. - avatar: - The avatar image in bytes form. If unspecified, no avatar is made. - reason: - An optional audit log reason explaining why the change was made. - - Returns: + Parameters + ---------- + channel_id : :obj:`str` + The ID of the channel for webhook to be created in. + name : :obj:`str` + The webhook's name string. + avatar : :obj:`bytes` + If specified, the avatar image in bytes form. + reason : :obj:`str` + If specified, the audit log reason explaining why the operation + was performed. + + Returns + ------- + :obj:`typing.Dict` The newly created webhook object. - Raises: - hikari.net.errors.NotFoundHTTPError: - If the channel is not found. - hikari.net.errors.ForbiddenHTTPError: - If you either lack the `MANAGE_WEBHOOKS` permission or can not see the given channel. - hikari.net.errors.BadRequestHTTPError: - If the avatar image is too big or the format is invalid. + Raises + ------ + :obj:`hikari.net.errors.NotFoundHTTPError` + If the channel is not found. + :obj:`hikari.net.errors.ForbiddenHTTPError` + If you either lack the ``MANAGE_WEBHOOKS`` permission or + can not see the given channel. + :obj:`hikari.net.errors.BadRequestHTTPError` + If the avatar image is too big or the format is invalid. """ payload = {"name": name} transformations.put_if_specified(payload, "avatar", avatar, transformations.image_bytes_to_image_data) @@ -2532,41 +2802,49 @@ async def create_webhook( return await self._request(route, json_body=payload, reason=reason) async def get_channel_webhooks(self, channel_id: str) -> typing.Sequence[typing.Dict]: - """ - Gets all webhooks from a given channel. + """Gets all webhooks from a given channel. - Args: - channel_id: - The ID of the channel to get the webhooks from. + Parameters + ---------- + channel_id : :obj:`str` + The ID of the channel to get the webhooks from. - Returns: + Returns + ------- + :obj:`typing.Sequence` [ :obj:`typing.Dict` ] A list of webhook objects for the give channel. - Raises: - hikari.net.errors.NotFoundHTTPError: - If the channel is not found. - hikari.net.errors.ForbiddenHTTPError: - If you either lack the `MANAGE_WEBHOOKS` permission or can not see the given channel. + Raises + ------ + :obj:`hikari.net.errors.NotFoundHTTPError` + If the channel is not found. + :obj:`hikari.net.errors.ForbiddenHTTPError` + If you either lack the ``MANAGE_WEBHOOKS`` permission or + can not see the given channel. """ route = routes.CHANNEL_WEBHOOKS.compile(self.GET, channel_id=channel_id) return await self._request(route) async def get_guild_webhooks(self, guild_id: str) -> typing.Sequence[typing.Dict]: - """ - Gets all webhooks for a given guild. + """Gets all webhooks for a given guild. - Args: - guild_id: - The ID for the guild to get the webhooks from. + Parameters + ---------- + guild_id : :obj:`str` + The ID for the guild to get the webhooks from. - Returns: + Returns + ------- + :obj:`typing.Sequence` [ :obj:`typing.Dict` ] A list of webhook objects for the given guild. - Raises: - hikari.net.errors.NotFoundHTTPError: - If the guild is not found. - hikari.net.errors.ForbiddenHTTPError: - If you either lack the `MANAGE_WEBHOOKS` permission or aren't a member of the given guild. + Raises + ------ + :obj:`hikari.net.errors.NotFoundHTTPError` + If the guild is not found. + :obj:`hikari.net.errors.ForbiddenHTTPError` + If you either lack the ``MANAGE_WEBHOOKS`` permission or + aren't a member of the given guild. """ route = routes.GUILD_WEBHOOKS.compile(self.GET, guild_id=guild_id) return await self._request(route) @@ -2574,25 +2852,29 @@ async def get_guild_webhooks(self, guild_id: str) -> typing.Sequence[typing.Dict async def get_webhook( self, webhook_id: str, *, webhook_token: typing.Union[typing.Literal[...], str] = ... ) -> typing.Dict: - """ - Gets a given webhook. + """Gets a given webhook. - Args: - webhook_id: - The ID of the webhook to get. - webhook_token: - If specified, the webhook token to use to get it (bypassing bot authorization). + Parameters + ---------- + webhook_id : :obj:`str` + The ID of the webhook to get. + webhook_token : :obj:`str` + If specified, the webhook token to use to get it (bypassing bot authorization). - Returns: + Returns + ------- + :obj:`typing.Dict` The requested webhook object. - Raises: - hikari.net.errors.NotFoundHTTPError: - If the webhook is not found. - ForbiddenHTTPError: - If you're not in the guild that owns this webhook or lack the `MANAGE_WEBHOOKS` permission. - hikari.net.errors.UnauthorizedHTTPError: - If you pass a token that's invalid for the target webhook. + Raises + ------ + :obj:`hikari.net.errors.NotFoundHTTPError` + If the webhook is not found. + :obj:`hikari.net.errors.ForbiddenHTTPError` + If you're not in the guild that owns this webhook or + lack the ``MANAGE_WEBHOOKS`` permission. + :obj:`hikari.net.errors.UnauthorizedHTTPError` + If you pass a token that's invalid for the target webhook. """ if webhook_token is ...: route = routes.WEBHOOK.compile(self.GET, webhook_id=webhook_id) @@ -2606,39 +2888,44 @@ async def modify_webhook( *, webhook_token: typing.Union[typing.Literal[...], str] = ..., name: typing.Union[typing.Literal[...], str] = ..., - avatar: typing.Union[None, typing.Literal[...], bytes] = ..., + avatar: typing.Optional[typing.Union[typing.Literal[...], bytes]] = ..., channel_id: typing.Union[typing.Literal[...], str] = ..., reason: typing.Union[typing.Literal[...], str] = ..., ) -> typing.Dict: - """ - Edits a given webhook. - - Args: - webhook_id: - The ID of the webhook to edit. - webhook_token: - If specified, the webhook token to use to modify it (bypassing bot authorization). - name: - The new name string. - avatar: - The new avatar image in bytes form. If unspecified, it is not changed, but if None, then - it is removed. - channel_id: - The ID of the new channel the given webhook should be moved to. - reason: - An optional audit log reason explaining why the change was made. - - Returns: + """Edits a given webhook. + + Parameters + ---------- + webhook_id : :obj:`str` + The ID of the webhook to edit. + webhook_token : :obj:`str` + If specified, the webhook token to use to modify it (bypassing bot authorization). + name : :obj:`str` + If specified, the new name string. + avatar : :obj:`bytes` + If specified, the new avatar image in bytes form. If None, then + it is removed. + channel_id : :obj:`str` + If specified, the ID of the new channel the given + webhook should be moved to. + reason : :obj:`str` + If specified, the audit log reason explaining why the operation + was performed. + + Returns + ------- + :obj:`typing.Dict` The updated webhook object. - Raises: - hikari.net.errors.NotFoundHTTPError: - If either the webhook or the channel aren't found. - hikari.net.errors.ForbiddenHTTPError: - If you either lack the `MANAGE_WEBHOOKS` permission or aren't a member of the guild this webhook - belongs to. - hikari.net.errors.UnauthorizedHTTPError: - If you pass a token that's invalid for the target webhook. + Raises + ------ + :obj:`hikari.net.errors.NotFoundHTTPError` + If either the webhook or the channel aren't found. + :obj:`hikari.net.errors.ForbiddenHTTPError` + If you either lack the ``MANAGE_WEBHOOKS`` permission or + aren't a member of the guild this webhook belongs to. + :obj:`hikari.net.errors.UnauthorizedHTTPError` + If you pass a token that's invalid for the target webhook. """ payload = {} transformations.put_if_specified(payload, "name", name) @@ -2655,22 +2942,24 @@ async def modify_webhook( async def delete_webhook( self, webhook_id: str, *, webhook_token: typing.Union[typing.Literal[...], str] = ... ) -> None: - """ - Deletes a given webhook. - - Args: - webhook_id: - The ID of the webhook to delete - webhook_token: - If specified, the webhook token to use to delete it (bypassing bot authorization). - - Raises: - hikari.net.errors.NotFoundHTTPError: - If the webhook is not found. - hikari.net.errors.ForbiddenHTTPError: - If you either lack the `MANAGE_WEBHOOKS` permission or aren't a member of the guild this webhook - belongs to. - hikari.net.errors.UnauthorizedHTTPError: + """Deletes a given webhook. + + Parameters + ---------- + webhook_id : :obj:`str` + The ID of the webhook to delete + webhook_token : :obj:`str` + If specified, the webhook token to use to + delete it (bypassing bot authorization). + + Raises + ------ + :obj:`hikari.net.errors.NotFoundHTTPError` + If the webhook is not found. + :obj:`hikari.net.errors.ForbiddenHTTPError` + If you either lack the ``MANAGE_WEBHOOKS`` permission or + aren't a member of the guild this webhook belongs to. + :obj:`hikari.net.errors.UnauthorizedHTTPError` If you pass a token that's invalid for the target webhook. """ if webhook_token is ...: @@ -2687,13 +2976,13 @@ async def execute_webhook( content: typing.Union[typing.Literal[...], str] = ..., username: typing.Union[typing.Literal[...], str] = ..., avatar_url: typing.Union[typing.Literal[...], str] = ..., - tts: bool = False, - wait: bool = False, + tts: typing.Union[typing.Literal[...], bool] = ..., + wait: typing.Union[typing.Literal[...], bool] = ..., file: typing.Union[typing.Literal[...], typing.Tuple[str, storage.FileLikeT]] = ..., embeds: typing.Union[typing.Literal[...], typing.Sequence[typing.Dict]] = ..., + allowed_mentions: typing.Union[typing.Literal[...], typing.Dict] = ..., ) -> typing.Optional[typing.Dict]: - """ - Create a message in the given channel or DM. + """Create a message in the given channel or DM. Parameters ---------- @@ -2701,49 +2990,61 @@ async def execute_webhook( The ID of the webhook to execute. webhook_token : :obj:`str` The token of the webhook to execute. - content : :obj:`str`, optional - The webhook message content to send. - username : :obj:`str`, optional - Used to override the webhook's username for this request. - avatar_url : :obj:`str`, optional - The url of an image to override the webhook's avatar with - for this message. - tts : :obj:`bool`, optional - Whether this webhook should create a TTS message. - wait : :obj:`bool`, optional - Whether this request should wait for the webhook to be executed - and return the resultant message object. - file : :obj:`bytes` or :obj:`io.IOBase`, optional - A tuple of the file name and either raw bytes or a io.IOBase - derived object that points to a buffer containing said file. - embeds : :obj:`typing.Sequence` [:obj:`dict`], optional - A sequence of embed objects that will be sent with this message. - - Raises - ------ - hikari.net.errors.NotFoundHTTPError + content : :obj:`str` + If specified, the webhook message content to send. + username : :obj:`str` + If specified, the username to override the webhook's username + for this request. + avatar_url : :obj:`str` + If specified, the url of an image to override the webhook's + avatar with for this request. + tts : :obj:`bool` + If specified, whether this webhook should create a TTS message. + wait : :obj:`bool` + If specified, whether this request should wait for the webhook + to be executed and return the resultant message object. + file : :obj:`typing.Tuple` [ :obj:`str`, :obj:`storage.FileLikeT` ] + If specified, a tuple of the file name and either raw :obj:`bytes` + or a :obj:`io.IOBase` derived object that points to a buffer + containing said file. + embeds : :obj:`typing.Sequence` [:obj:`typing.Dict`] + If specified, the sequence of embed objects that will be sent + with this message. + allowed_mentions : :obj:`typing.Dict` + If specified, the mentions to parse from the ``content``. + If not specified, will parse all mentions from the ``content``. + + Raises + ------ + :obj:`hikari.net.errors.NotFoundHTTPError` If the channel ID or webhook ID is not found. - hikari.net.errors.BadRequestHTTPError: - If the file is too large, the embed exceeds the defined limits, - if the message content is specified and empty or greater than 2000 - characters, or if neither of content, file or embed are specified. - hikari.net.errors.ForbiddenHTTPError: + :obj:`hikari.net.errors.BadRequestHTTPError` + This can be raised if the file is too large; if the embed exceeds + the defined limits; if the message content is specified only and + empty or greater than 2000 characters; if neither content, file + or embed are specified; if there is a duplicate id in only of the + fields in ``allowed_mentions``; if you specify to parse all + users/roles mentions but also specify which users/roles to + parse only. + :obj:`hikari.net.errors.ForbiddenHTTPError` If you lack permissions to send to this channel. - hikari.net.errors.UnauthorizedHTTPError: + :obj:`hikari.net.errors.UnauthorizedHTTPError` If you pass a token that's invalid for the target webhook. Returns ------- - :obj:`hikari.internal_utilities.typing.Dict` or :obj:`None` - The created message object if wait is True else None. + :obj:`hikari.internal_utilities.typing.Dict`, optional + The created message object if ``wait`` is ``True``, else ``None``. """ form = aiohttp.FormData() - json_payload = {"tts": tts} + json_payload = {} transformations.put_if_specified(json_payload, "content", content) transformations.put_if_specified(json_payload, "username", username) transformations.put_if_specified(json_payload, "avatar_url", avatar_url) + transformations.put_if_specified(json_payload, "tts", tts) transformations.put_if_specified(json_payload, "embeds", embeds) + transformations.put_if_specified(json_payload, "allowed_mentions", allowed_mentions) form.add_field("payload_json", json.dumps(json_payload), content_type="application/json") @@ -2755,11 +3056,29 @@ async def execute_webhook( else: re_seekable_resources = [] + query = {} + transformations.put_if_specified(query, "wait", wait, str) + route = routes.WEBHOOK_WITH_TOKEN.compile(self.POST, webhook_id=webhook_id, webhook_token=webhook_token) return await self._request( route, form_body=form, re_seekable_resources=re_seekable_resources, - query={"wait": str(wait)}, + query=query, suppress_authorization_header=True, ) + + ########## + # OAUTH2 # + ########## + + async def get_current_application_info(self) -> typing.Dict: + """Get the current application information. + + Returns + ------- + :obj:`typing.Dict` + An application info object. + """ + route = routes.OAUTH2_APPLICATIONS_ME.compile(self.GET) + return await self._request(route) diff --git a/hikari/net/ratelimits.py b/hikari/net/ratelimits.py index d1d2f09248..39fe90107e 100644 --- a/hikari/net/ratelimits.py +++ b/hikari/net/ratelimits.py @@ -24,45 +24,45 @@ What is the theory behind this implementation? ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -In this module, we refer to a :class:`hikari.net.routes.CompiledRoute` as a +In this module, we refer to a :obj:`hikari.net.routes.CompiledRoute` as a definition of a route with specific major parameter values included (e.g. -`POST /channels/123/messages`), and a :class:`hikari.net.routes.RouteTemplate` +``POST /channels/123/messages``), and a :obj:`hikari.net.routes.RouteTemplate` as a definition of a route without specific parameter values included (e.g. -`POST /channels/{channel_id}/messages`). We can compile a -:class:`hikari.net.routes.CompiledRoute` from a -:class:`hikari.net.routes.RouteTemplate` by providing the corresponding +``POST /channels/{channel_id}/messages``). We can compile a +:obj:`hikari.net.routes.CompiledRoute` from a +:obj:`hikari.net.routes.RouteTemplate` by providing the corresponding parameters as kwargs, as you may already know. In this module, a "bucket" is an internal data structure that tracks and enforces the rate limit state for a specific -:class:`hikari.net.routes.CompiledRoute`, and can manage delaying tasks in the +:obj:`hikari.net.routes.CompiledRoute`, and can manage delaying tasks in the event that we begin to get rate limited. It also supports providing in-order execution of queued tasks. Discord allocates types of buckets to routes. If you are making a request and there is a valid rate limit on the route you hit, you should receive an -`X-RateLimit-Bucket` header from the server in your response. This is a hash +``X-RateLimit-Bucket`` header from the server in your response. This is a hash that identifies a route based on internal criteria that does not include major -parameters. This `X-RateLimitBucket` is known in this module as an +parameters. This ``X-RateLimitBucket`` is known in this module as an "bucket hash". This means that generally, the route `POST /channels/123/messages` and -`POST /channels/456/messages` will usually sit in the same bucket, but -`GET /channels/123/messages/789` and `PATCH /channels/123/messages/789` will +``POST /channels/456/messages`` will usually sit in the same bucket, but +``GET /channels/123/messages/789`` and ``PATCH /channels/123/messages/789`` will usually not share the same bucket. Discord may or may not change this at any time, so hard coding this logic is not a useful thing to be doing. Rate limits, on the other hand, apply to a bucket and are specific to the major -parameters of the compiled route. This means that `POST /channels/123/messages` -and `POST /channels/456/messages` do not share the same real bucket, despite +parameters of the compiled route. This means that ``POST /channels/123/messages`` +and ``POST /channels/456/messages`` do not share the same real bucket, despite Discord providing the same bucket hash. A -:class:`hikari.net.ratelimits.RealBucketHash`, therefore, is the :class:`str` +:obj:`hikari.net.ratelimits.RealBucketHash`, therefore, is the :obj:`str` hash of the bucket that Discord sends us in a response concatenated to the corresponding major parameters. This is used for quick bucket indexing internally in this module. One issue that occurs from this is that we cannot effectively hash a -:class:`hikari.net.routes.CompiledRoute` that has not yet been hit, meaning that +:obj:`hikari.net.routes.CompiledRoute` that has not yet been hit, meaning that until we receive a response from this endpoint, we have no idea what our rate limits could be, nor the bucket that they sit in. This is usually not problematic, as the first request to an endpoint should never be rate limited @@ -76,20 +76,20 @@ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Each time you :meth:`hikari.net.ratelimits.RateLimiter.acquire` a request -timeslice for a given :class:`hikari.net.ratelimits.CompiledRoute`, several +timeslice for a given :obj:`hikari.net.ratelimits.CompiledRoute`, several things happen. The first is that we attempt to find the existing bucket for that route, if there is one, or get an unknown bucket otherwise. This is done -by creating a :class:`hikari.net.ratelimits.RealBucketHash` from the compiled +by creating a :obj:`hikari.net.ratelimits.RealBucketHash` from the compiled route. The initial hash is calculated using a lookup table that maps -:class:`hikari.net.ratelimits.CompiledRoute` objects to their corresponding +:obj:`hikari.net.ratelimits.CompiledRoute` objects to their corresponding initial hash codes, or to the unknown bucket hash code if not yet known. This initial hash is processed by the :class`hikari.net.ratelimits.CompiledRoute` to -provide the :class:`RealBucketHash` we need to get the route's bucket object +provide the :obj:`RealBucketHash` we need to get the route's bucket object internally. The :meth:`hikari.net.ratelimits.RateLimiter.acquire` method will take the bucket and acquire a new timeslice on it. This takes the form of a -:class:`asyncio.Future` which should be awaited by the caller and will complete +:obj:`asyncio.Future` which should be awaited by the caller and will complete once the caller is allowed to make a request. Most of the time, this is done instantly, but if the bucket has an active rate limit preventing requests being sent, then the future will be paused until the rate limit is over. This may be @@ -104,14 +104,14 @@ becomes empty. The result of :meth:`hikari.net.ratelimits.RateLimiter.acquire` is a tuple of a -:class:`asyncio.Future` to await on which completes when you are allowed to +:obj:`asyncio.Future` to await on which completes when you are allowed to proceed with making a request, and a :class`RealBucketHash` which should be stored temporarily. This will be explained in the next section. When you make your response, you should be sure to set the -`X-RateLimit-Precision` header to `millisecond` to ensure a much greater +``X-RateLimit-Precision`` header to `millisecond` to ensure a much greater accuracy against rounding errors for rate limits (reduces the error margin from -1 second to 1 millisecond). +`1` second to `1` millisecond). Handling the rate limit headers of a response ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -120,18 +120,18 @@ the vital rate limit headers manually and parse them to the correct data types. These headers are: -- `Date`: the response date on the server. This should be parsed to a - :class:`datetime.datetime` using :meth:`email.utils.parsedate_to_datetime`. -- `X-RateLimit-Limit`: an :class:`int` describing the max requests in the bucket +* ``Date``: the response date on the server. This should be parsed to a + :obj:`datetime.datetime` using :func:`email.utils.parsedate_to_datetime`. +* ``X-RateLimit-Limit``: an :obj:`int` describing the max requests in the bucket from empty to being rate limited. -- `X-RateLimit-Remaining`: an :class:`int` describing the remaining number of +* ``X-RateLimit-Remaining``: an :obj:`int` describing the remaining number of requests before rate limiting occurs in the current window. -- `X-RateLimit-Bucket`: a :class:`str` containing the initial bucket hash. -- `X-RateLimit-Reset`: a :class:`float` containing the number of seconds since +* ``X-RateLimit-Bucket``: a :obj:`str` containing the initial bucket hash. +* ``X-RateLimit-Reset``: a :obj:`float` containing the number of seconds since 1st January 1970 at 0:00:00 UTC at which the current ratelimit window - resets. This should be parsed to a :class:`datetime` using - :meth:`datetime.datetime.fromtimestamp`, passing - :attr:`datetime.timezone.utc` as a second parameter. + resets. This should be parsed to a :obj:`datetime` using + :func:`datetime.datetime.fromtimestamp`, passing + :obj:`datetime.timezone.utc` as a second parameter. Each of the above values should be passed to the :meth:`hikari.net.ratelimits.RateLimiter.update_rate_limits` method @@ -145,16 +145,16 @@ Tidying up ~~~~~~~~~~ -To prevent unused buckets cluttering up memory, each :class:`RateLimiter` -instance spins up a :class:`asyncio.Task` that periodically locks the bucket +To prevent unused buckets cluttering up memory, each :obj:`RateLimiter` +instance spins up a :obj:`asyncio.Task` that periodically locks the bucket list (not threadsafe, only using the concept of asyncio not yielding in regular functions) and disposes of any clearly stale buckets that are no longer needed. These will be recreated again in the future if they are needed. -When shutting down an application, one must remember to :class:`close` the -:class:`RateLimiter` that has been used. This will ensure the garbage collection +When shutting down an application, one must remember to :meth:`close` the +:obj:`RateLimiter` that has been used. This will ensure the garbage collection task is stopped, and will also ensure any remaining futures in any bucket queues -have an :class:`asyncio.CancelledException` set on them to prevent deadlocking +have an :obj:`asyncio.CancelledError` set on them to prevent deadlocking ratelimited calls that may be waiting to be unlocked. """ __all__ = [ @@ -232,7 +232,7 @@ class BurstRateLimiter(IRateLimiter, abc.ABC): #: :type: :obj:`str` name: str - #: The throttling task, or `None` if it isn't running. + #: The throttling task, or ``None``` if it isn't running. #: #: :type: :obj:`asyncio.Task`, optional throttle_task: typing.Optional[asyncio.Task] @@ -289,7 +289,7 @@ def close(self) -> None: @property def is_empty(self) -> bool: - """Return True if no futures are on the queue being rate limited.""" + """Return ``True`` if no futures are on the queue being rate limited.""" return len(self.queue) == 0 @@ -346,16 +346,15 @@ def throttle(self, retry_after: float) -> None: How long to sleep for before unlocking and releasing any futures in the queue. - Notes - ----- - + Note + ---- This will invoke :meth:`unlock_later` as a scheduled task in the future (it will not await it to finish) When the :meth:`unlock_later` coroutine function completes, it should be - expected to set the :attr:`throttle_task` to `None`. This means you can + expected to set the :attr:`throttle_task` to ``None``. This means you can check if throttling is occurring by checking if :attr:`throttle_task` - is not `None`. + is not ``None``. If this is invoked while another throttle is in progress, that one is cancelled and a new one is started. This enables new rate limits to @@ -376,15 +375,15 @@ async def unlock_later(self, retry_after: float) -> None: How long to sleep for before unlocking and releasing any futures in the queue. - Notes - ----- + Note + ---- You shouldn't need to invoke this directly. Call :meth:`throttle` instead. When the :meth:`unlock_later` coroutine function completes, it should be - expected to set the :attr:`throttle_task` to `None`. This means you can + expected to set the :attr:`throttle_task` to ``None``. This means you can check if throttling is occurring by checking if :attr:`throttle_task` - is not `None`. + is not ``None``. """ self.logger.warning("you are being globally rate limited for %ss", retry_after) @@ -429,7 +428,7 @@ class WindowedBurstRateLimiter(BurstRateLimiter): #: :type: :obj:`float` reset_at: float - #: The number of :meth:`acquire`s left in this window before you will get + #: The number of :meth:`acquire`'s left in this window before you will get #: rate limited. #: #: :type: :obj:`int` @@ -440,7 +439,7 @@ class WindowedBurstRateLimiter(BurstRateLimiter): #: :type: :obj:`float` period: float - #: The maximum number of :meth:`acquire`s allowed in this time window. + #: The maximum number of :meth:`acquire`'s allowed in this time window. #: #: :type: :obj:`int` limit: int @@ -489,12 +488,12 @@ def get_time_until_reset(self, now: float) -> float: Returns ------- + :obj:`float` + The time left to sleep before the rate limit is reset. If no rate limit + is in effect, then this will return ``0.0`` instead. - The time left to sleep before the rate limit is reset. If no rate limit - is in effect, then this will return `0.0` instead. - - Warnings - -------- + Warning + ------- Invoking this method will update the internal state if we were previously rate limited, but at the given time are no longer under that limit. This makes it imperative that you only pass the current timestamp @@ -516,10 +515,10 @@ def is_rate_limited(self, now: float) -> bool: Returns ------- :obj:`bool` - `True` if we are being rate limited. `False` if we are not. + ``True`` if we are being rate limited. ``False`` if we are not. - Warnings - -------- + Warning + ------- Invoking this method will update the internal state if we were previously rate limited, but at the given time are no longer under that limit. This makes it imperative that you only pass the current timestamp @@ -543,16 +542,15 @@ async def throttle(self) -> None: Iterates repeatedly while the queue is not empty, adhering to any rate limits that occur in the mean time. - Notes - ----- - + Note + ---- You should usually not need to invoke this directly, but if you do, - ensure to call it using :obj:`asyncio.create_task`, and store the + ensure to call it using :func:`asyncio.create_task`, and store the task immediately in :attr:`throttle_task`. When this coroutine function completes, it will set the - :attr:`throttle_task` to `None`. This means you can check if throttling - is occurring by checking if :attr:`throttle_task` is not `None`. + :attr:`throttle_task` to ``None``. This means you can check if throttling + is occurring by checking if :attr:`throttle_task` is not ``None``. """ self.logger.debug( "you are being rate limited on bucket %s, backing off for %ss", @@ -577,7 +575,7 @@ class HTTPBucketRateLimiter(WindowedBurstRateLimiter): Component to represent an active rate limit bucket on a specific HTTP route with a specific major parameter combo. - This is somewhat similar to the :class:`WindowedBurstRateLimiter` in how it + This is somewhat similar to the :obj:`WindowedBurstRateLimiter` in how it works. This algorithm will use fixed-period time windows that have a given limit @@ -608,7 +606,7 @@ def __init__(self, name: str, compiled_route: routes.CompiledRoute) -> None: @property def is_unknown(self) -> bool: - """Return True if the bucket represents an UNKNOWN bucket.""" + """Return ``True`` if the bucket represents an ``UNKNOWN`` bucket.""" return self.name.startswith(UNKNOWN_HASH) def acquire(self) -> asyncio.Future: @@ -620,8 +618,8 @@ def acquire(self) -> asyncio.Future: A future that should be awaited immediately. Once the future completes, you are allowed to proceed with your operation. - Notes - ----- + Note + ---- You should afterwards invoke :meth:`update_rate_limit` to update any rate limit information you are made aware of. """ @@ -632,7 +630,6 @@ def update_rate_limit(self, remaining: int, limit: int, reset_at: float) -> None Parameters ---------- - remaining : :obj:`int` The calls remaining in this time window. limit : :obj:`int` @@ -640,9 +637,9 @@ def update_rate_limit(self, remaining: int, limit: int, reset_at: float) -> None reset_at : :obj:`float` The epoch at which to reset the limit. - Notes - ----- - The `reset_at` epoch is expected to be a :func:`time.perf_counter` + Note + ---- + The :attr:`reset_at` epoch is expected to be a :func:`time.perf_counter` monotonic epoch, rather than a :func:`time.time` date-based epoch. """ self.remaining = remaining @@ -653,10 +650,10 @@ def update_rate_limit(self, remaining: int, limit: int, reset_at: float) -> None def drip(self) -> None: """Decrement the remaining count for this bucket. - Notes - ----- + Note + ---- If the bucket is marked as :attr:`is_unknown`, then this will not do - anything. "Unknown" buckets have infinite rate limits. + anything. ``Unknown`` buckets have infinite rate limits. """ # We don't drip unknown buckets: we can't rate limit them as we don't know their real bucket hash or # the current rate limit values Discord put on them... @@ -668,7 +665,7 @@ class HTTPBucketRateLimiterManager: """The main rate limiter implementation for HTTP clients. This is designed to provide bucketed rate limiting for Discord HTTP - endpoints that respects the `X-RateLimit-Bucket` rate limit header. To do + endpoints that respects the ``X-RateLimit-Bucket`` rate limit header. To do this, it makes the assumption that any limit can change at any time. """ @@ -680,12 +677,12 @@ class HTTPBucketRateLimiterManager: "logger", ) - #: Maps compiled routes to their `X-RateLimit-Bucket` header being used. + #: Maps compiled routes to their ``X-RateLimit-Bucket`` header being used. #: #: :type: :obj:`typing.MutableMapping` [ :obj:`hikari.net.routes.CompiledRoute`, :obj:`str` ] routes_to_hashes: typing.MutableMapping[routes.CompiledRoute, str] - #: Maps full bucket hashes (`X-RateLimit-Bucket` appended with a hash of + #: Maps full bucket hashes (``X-RateLimit-Bucket`` appended with a hash of #: major parameters used in that compiled route) to their corresponding rate #: limiters. #: @@ -745,7 +742,6 @@ def close(self) -> None: Once this has been called, this object is considered to be effectively dead. To reuse it, one should create a new instance. """ - self.closed_event.set() for bucket in self.real_hashes_to_buckets.values(): bucket.close() @@ -764,7 +760,7 @@ async def gc(self, poll_period: float = 20) -> None: Parameters ---------- poll_period : :obj:`float` - The period to poll at. This defaults to once every 20 seconds. + The period to poll at. This defaults to once every ``20`` seconds. Warnings -------- @@ -795,8 +791,8 @@ def do_gc_pass(self): If the removed routes are used again in the future, they will be re-cached automatically. - Warnings - -------- + Warning + ------- You generally have no need to invoke this directly. Use :meth:`start` and :meth:`close` to control this instead. """ @@ -820,7 +816,6 @@ def acquire(self, compiled_route: routes.CompiledRoute) -> asyncio.Future: Parameters ---------- - compiled_route : :obj:`hikari.net.routes.CompiledRoute` The route to get the bucket for. @@ -830,9 +825,8 @@ def acquire(self, compiled_route: routes.CompiledRoute) -> asyncio.Future: A future to await that completes when you are allowed to run your request logic. - Notes - ----- - + Note + ---- The returned future MUST be awaited, and will complete when your turn to make a call comes along. You are expected to await this and then immediately make your HTTP call. The returned future may already be @@ -874,17 +868,17 @@ def update_rate_limits( compiled_route : :obj:`hikari.net.routes.CompiledRoute` The compiled route to get the bucket for. bucket_header : :obj:`str`, optional - The `X-RateLimit-Bucket` header that was provided in the response, - or `None` if not present. + The ``X-RateLimit-Bucket`` header that was provided in the response, + or ``None`` if not present. remaining_header : :obj:`int` - The `X-RateLimit-Remaining` header cast to an :class:`int`. + The ``X-RateLimit-Remaining`` header cast to an :obj:`int`. limit_header : :obj:`int` - The `X-RateLimit-Limit` header cast to an :class:`int`. + The ``X-RateLimit-Limit`` header cast to an :obj:`int`. date_header : :obj:`datetime.datetime` - The `Date` header value as a :class:`datetime.datetime`. + The ``Date`` header value as a :obj:`datetime.datetime`. reset_at_header : :obj:`datetime.datetime` - The `X-RateLimit-Reset` header value as a - :class:`datetime.datetime`. + The ``X-RateLimit-Reset`` header value as a + :obj:`datetime.datetime`. """ self.routes_to_hashes[compiled_route] = bucket_header @@ -918,13 +912,13 @@ class ExponentialBackOff: ---------- base : :obj:`float` - The base to use. Defaults to 2. + The base to use. Defaults to ``2``. maximum : :obj:`float`, optional - If not `None`, then this is the max value the backoff can be in a - single iteration before an :class:`asyncio.TimeoutError` is raised. - Defaults to 64 seconds. + If not ``None``, then this is the max value the backoff can be in a + single iteration before an :obj:`asyncio.TimeoutError` is raised. + Defaults to ``64`` seconds. jitter_multiplier : :obj:`float` - The multiplier for the random jitter. Defaults to 1. Set to 0 to disable + The multiplier for the random jitter. Defaults to ``1``. Set to ``0`` to disable jitter. """ @@ -940,13 +934,13 @@ class ExponentialBackOff: #: :type: :obj:`int` increment: int - #: If not `None`, then this is the max value the backoff can be in a - #: single iteration before an :class:`asyncio.TimeoutError` is raised. + #: If not ``None```, then this is the max value the backoff can be in a + #: single iteration before an :obj:`asyncio.TimeoutError` is raised. #: #: :type: :obj:`float`, optional maximum: typing.Optional[float] - #: The multiplier for the random jitter. Defaults to 1. Set to 0 to disable + #: The multiplier for the random jitter. Defaults to ``1`. Set to ``0``` to disable #: jitter. #: #: :type: :obj:`float` diff --git a/hikari/net/routes.py b/hikari/net/routes.py index e244d37ac9..8bb80021d3 100644 --- a/hikari/net/routes.py +++ b/hikari/net/routes.py @@ -16,41 +16,45 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . -""" -Provides the valid routes that can be used on the API, as well as mechanisms to aid -with rate limit bucketing. -""" +"""Provides the valid routes that can be used on the API, as well as mechanisms to aid +with rate limit bucketing.""" __all__ = ["CompiledRoute", "RouteTemplate"] -from typing import Any -from typing import Collection -from typing import FrozenSet +import typing DEFAULT_MAJOR_PARAMS = {"channel_id", "guild_id", "webhook_id"} class CompiledRoute: - """ - A compiled representation of a route ready to be made into a full URL and to be used for a request. - - Args: - method: - The HTTP method to use. - path: - The path with any major parameters interpolated in. - major_params_hash: - The part of the hash identifier to use for the compiled set of major parameters. + """A compiled representation of a route ready to be made into a full URL and to be used for a request. + + Parameters + ---------- + method : :obj:`str` + The HTTP method to use. + path : :obj:`str` + The path with any major parameters interpolated in. + major_params_hash : :obj:`str` + The part of the hash identifier to use for the compiled set of major parameters. """ __slots__ = ("method", "major_params_hash", "compiled_path", "hash_code", "__weakref__") #: The method to use on the route. + #: + #: :type: :obj:`str` method: str #: The major parameters in a bucket hash-compatible representation. + #: + #: :type: :obj:`str` major_params_hash: str #: The compiled route path to use + #: + #: :type: :obj:`str` compiled_path: str #: The hash code + #: + #: :type: :obj:`int` hash_code: int def __init__(self, method: str, path_template: str, path: str, major_params_hash: str) -> None: @@ -60,12 +64,16 @@ def __init__(self, method: str, path_template: str, path: str, major_params_hash self.hash_code = hash((path_template, major_params_hash)) def create_url(self, base_url: str) -> str: - """ - Args: - base_url: - The base of the URL to prepend to the compiled path. + """Creates the full URL with which you can make a request. + + Parameters + ---------- + base_url : :obj:`str` + The base of the URL to prepend to the compiled path. - Returns: + Returns + ------- + :obj:`str` The full URL for the route. """ @@ -96,50 +104,56 @@ def __str__(self) -> str: class RouteTemplate: - """ - A template used to create compiled routes for specific parameters. These compiled routes are used to identify - rate limit buckets. - - Args: - path_template: - The template string for the path to use. - major_params: - A collection of major parameter names that appear in the template path. - If not specified, the default major parameter names are extracted and used in-place. + """A template used to create compiled routes for specific parameters. + + These compiled routes are used to identify rate limit buckets. + + Parameters + ---------- + path_template : :obj:`str` + The template string for the path to use. + major_params : :obj:`str` + A collection of major parameter names that appear in the template path. + If not specified, the default major parameter names are extracted and used in-place. """ __slots__ = ("path_template", "major_params") #: The template string used for the path. + #: + #: :type: :obj:`str` path_template: str #: Major parameter names that appear in the template path. - major_params: FrozenSet[str] - - def __init__(self, path_template: str, major_params: Collection[str] = None) -> None: + #: + #: :type: :obj:`typing.FrozenSet` [ :obj:`str` ] + major_params: typing.FrozenSet[str] + def __init__(self, path_template: str, major_params: typing.Collection[str] = None) -> None: self.path_template = path_template if major_params is None: self.major_params = frozenset(p for p in DEFAULT_MAJOR_PARAMS if f"{{{p}}}" in path_template) else: self.major_params = frozenset(major_params) - def compile(self, method, /, **params: Any) -> CompiledRoute: - """ - Generate a formatted :class:`CompiledRoute` for this route, taking into account any URL parameters that have - been passed, and extracting the major params for bucket hash operations accordingly. + def compile(self, method: str, /, **kwargs: typing.Any) -> CompiledRoute: + """Generate a formatted :obj:`CompiledRoute` for this route, taking into account any URL parameters that have + been passed, and extracting the :attr:major_params" for bucket hash operations accordingly. - Args: - method: - The method to use. - **params: - any parameters to interpolate into the route path. + Parameters + ---------- + method : :obj:`str` + The method to use. + **kwargs : :obj:`typing.Any` + Any parameters to interpolate into the route path. - Returns: + Returns + ------- + :obj:`CompiledRoute` The compiled route. """ - major_hash_part = "-".join((str(params[p]) for p in self.major_params)) + major_hash_part = "-".join((str(kwargs[p]) for p in self.major_params)) - return CompiledRoute(method, self.path_template, self.path_template.format_map(params), major_hash_part) + return CompiledRoute(method, self.path_template, self.path_template.format_map(kwargs), major_hash_part) def __repr__(self) -> str: this_type = type(self).__name__ diff --git a/hikari/net/user_agent.py b/hikari/net/user_agent.py index 522e34a366..42dcdc1c0b 100644 --- a/hikari/net/user_agent.py +++ b/hikari/net/user_agent.py @@ -16,8 +16,7 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . -""" -Anonymous system information that we have to provide to Discord when using their API. +"""Anonymous system information that we have to provide to Discord when using their API. This information contains details such as the version of Python you are using, and the version of this library, the OS you are making requests from, etc. @@ -34,11 +33,17 @@ @cache.cached_function() def library_version() -> str: - """ - Returns: + """The version of the library being used. + + Returns + ------- + :obj:`str` A string representing the version of this library. - Example: + Example + ------- + .. code-block:: python + >>> from hikari.net import user_agent >>> print(user_agent.library_version()) hikari 0.0.71 @@ -50,11 +55,17 @@ def library_version() -> str: @cache.cached_function() def python_version() -> str: - """ - Returns: + """The python version being used. + + Returns + ------- + :obj:`str` A string representing the version of this release of Python. - Example: + Example + ------- + .. code-block:: python + >>> from hikari.net import user_agent >>> print(user_agent.python_version()) CPython 3.8.1 GCC 9.2.0 @@ -70,16 +81,20 @@ def python_version() -> str: @cache.cached_function() def system_type() -> str: - """ - Returns: + """The operating system being used. + + Returns + ------- + :obj:`str` A string representing the system being used. - Example: + Example + ------- + .. code-block:: python + >>> from hikari.net import user_agent >>> print(user_agent.system_type()) Linux-5.4.15-2-MANJARO-x86_64-with-glibc2.2.5 - - I use arch btw. """ # Might change this eventually to be more detailed, who knows. return platform.platform() @@ -87,12 +102,18 @@ def system_type() -> str: @cache.cached_function() def user_agent() -> str: - """ - Returns: - The string to use for the library `User-Agent` HTTP header that is required + """The user agent of the bot + + Returns + ------- + :obj:`str` + The string to use for the library ``User-Agent`` HTTP header that is required to be sent with every HTTP request. - Example: + Example + ------- + .. code-block:: python + >>> from hikari.net import user_agent >>> print(user_agent.user_agent()) DiscordBot (https://gitlab.com/nekokatt/hikari, 0.0.71) CPython 3.8.1 GCC 9.2.0 Linux diff --git a/hikari/net/versions.py b/hikari/net/versions.py index e259feec3c..880bcd1b67 100644 --- a/hikari/net/versions.py +++ b/hikari/net/versions.py @@ -16,18 +16,14 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . -""" -API version enumeration. -""" +"""API version enumeration.""" __all__ = ["HTTPAPIVersion", "GatewayVersion"] import enum class HTTPAPIVersion(enum.IntEnum): - """ - Supported versions for the REST API. - """ + """Supported versions for the REST API.""" #: The V6 API. This is currently the stable release that should be used unless you have a reason #: to use V7 otherwise. @@ -42,9 +38,7 @@ class HTTPAPIVersion(enum.IntEnum): class GatewayVersion(enum.IntEnum): - """ - Supported versions for the Gateway. - """ + """Supported versions for the Gateway.""" #: The V6 API. This is currently the stable release that should be used unless you have a reason #: to use V7 otherwise. diff --git a/tests/hikari/net/test_http_client.py b/tests/hikari/net/test_http_client.py index 5ac7767728..5493c9b72b 100644 --- a/tests/hikari/net/test_http_client.py +++ b/tests/hikari/net/test_http_client.py @@ -635,7 +635,7 @@ async def test_create_message_without_optionals(self, http_client_impl): assert await http_client_impl.create_message("22222222") is mock_response routes.CHANNEL_MESSAGES.compile.assert_called_once_with(http_client_impl.POST, channel_id="22222222") mock_form.add_field.assert_called_once_with( - "payload_json", json.dumps({"tts": False}), content_type="application/json" + "payload_json", json.dumps({}), content_type="application/json" ) http_client_impl._request.assert_called_once_with(mock_route, form_body=mock_form, re_seekable_resources=[]) @@ -1563,9 +1563,7 @@ async def test_begin_guild_prune_without_optionals_returns_none(self, http_clien mock_route = mock.MagicMock(routes.GUILD_PRUNE) with mock.patch.object(routes, "GUILD_PRUNE", compile=mock.MagicMock(return_value=mock_route)): assert await http_client_impl.begin_guild_prune("39393", 14) is None - http_client_impl._request.assert_called_once_with( - mock_route, query={"days": 14, "compute_prune_count": False}, reason=... - ) + http_client_impl._request.assert_called_once_with(mock_route, query={"days": 14}, reason=...) @pytest.mark.asyncio async def test_begin_guild_prune_with_optionals(self, http_client_impl): @@ -1576,7 +1574,7 @@ async def test_begin_guild_prune_with_optionals(self, http_client_impl): await http_client_impl.begin_guild_prune("39393", 14, compute_prune_count=True, reason="BYEBYE") == 32 ) http_client_impl._request.assert_called_once_with( - mock_route, query={"days": 14, "compute_prune_count": True}, reason="BYEBYE" + mock_route, query={"days": 14, "compute_prune_count": "True"}, reason="BYEBYE" ) @pytest.mark.asyncio @@ -2040,7 +2038,7 @@ async def test_execute_webhook_without_optionals(self, http_client_impl): mock_form = mock.MagicMock(spec_set=aiohttp.FormData, add_field=mock.MagicMock()) mock_route = mock.MagicMock(routes.WEBHOOK_WITH_TOKEN) http_client_impl._request.return_value = None - mock_json = '{"tts": "False"}' + mock_json = "{}" with mock.patch.object(aiohttp, "FormData", autospec=True, return_value=mock_form): with mock.patch.object(routes, "WEBHOOK_WITH_TOKEN", compile=mock.MagicMock(return_value=mock_route)): with mock.patch.object(json, "dumps", return_value=mock_json): @@ -2048,14 +2046,10 @@ async def test_execute_webhook_without_optionals(self, http_client_impl): routes.WEBHOOK_WITH_TOKEN.compile.assert_called_once_with( http_client_impl.POST, webhook_id="9393939", webhook_token="a_webhook_token" ) - json.dumps.assert_called_once_with({"tts": False}) + json.dumps.assert_called_once_with({}) mock_form.add_field.assert_called_once_with("payload_json", mock_json, content_type="application/json") http_client_impl._request.assert_called_once_with( - mock_route, - form_body=mock_form, - re_seekable_resources=[], - query={"wait": "False"}, - suppress_authorization_header=True, + mock_route, form_body=mock_form, re_seekable_resources=[], query={}, suppress_authorization_header=True, ) # cymock doesn't work right with the patch @@ -2087,6 +2081,7 @@ async def test_execute_webhook_with_optionals( wait=True, file=("file.txt", b"4444ididid"), embeds=[{"type": "rich", "description": "A DESCRIPTION"}], + allowed_mentions={"users": ["123"], "roles": ["456"]}, ) assert response is mock_response make_resource_seekable.assert_called_once_with(b"4444ididid") @@ -2100,6 +2095,7 @@ async def test_execute_webhook_with_optionals( "username": "agent 42", "avatar_url": "https://localhost.bump", "embeds": [{"type": "rich", "description": "A DESCRIPTION"}], + "allowed_mentions": {"users": ["123"], "roles": ["456"]}, } ) From f4503dc032f0b55c2d04f818477d06dfc3965eb0 Mon Sep 17 00:00:00 2001 From: davfsa Date: Wed, 18 Mar 2020 07:53:33 +0100 Subject: [PATCH 014/922] Fix typehints and docstrings for consistency --- hikari/core/guilds.py | 98 +++++++++++++++++++------------------- hikari/core/permissions.py | 19 ++++---- hikari/core/snowflakes.py | 2 +- hikari/net/http_client.py | 68 +++++++++++++------------- 4 files changed, 94 insertions(+), 93 deletions(-) diff --git a/hikari/core/guilds.py b/hikari/core/guilds.py index b67fbfedb0..f5034ebf3d 100644 --- a/hikari/core/guilds.py +++ b/hikari/core/guilds.py @@ -243,9 +243,8 @@ class GuildRole(snowflakes.UniqueEntity, entities.Deserializable): class Guild(snowflakes.UniqueEntity, entities.Deserializable): """A representation of a guild on Discord. - Notes - ----- - + Note + ---- If a guild object is considered to be unavailable, then the state of any other fields other than the :attr:`is_unavailable` and :attr:`id` members may be ``None``, outdated, or incorrect. If a guild is unavailable, then @@ -254,53 +253,53 @@ class Guild(snowflakes.UniqueEntity, entities.Deserializable): #: The name of the guild. #: - #: :type: :class:`str` + #: :type: :obj`str` name: str = marshaller.attrib(deserializer=str) #: The hash for the guild icon, if there is one. #: - #: :type: :class:`str`, optional + #: :type: :obj`str`, optional icon_hash: typing.Optional[str] = marshaller.attrib(raw_name="icon", deserializer=str, if_none=None) #: The hash of the splash for the guild, if there is one. #: - #: :type: :class:`str`, optional + #: :type: :obj`str`, optional splash_hash: typing.Optional[str] = marshaller.attrib(raw_name="splash", deserializer=str, if_none=None) #: The hash of the discovery splash for the guild, if there is one. #: - #: :type: :class:`str`, optional + #: :type: :obj`str`, optional discovery_splash_hash: typing.Optional[str] = marshaller.attrib( raw_name="discovery_splash", deserializer=str, if_none=None ) #: The ID of the owner of this guild. #: - #: :type: :class:`snowflakes.Snowflake` + #: :type: :obj`snowflakes.Snowflake` owner_id: snowflakes.Snowflake = marshaller.attrib(deserializer=snowflakes.Snowflake) #: The guild level permissions that apply to the bot user. #: - #: :type: :class:`hikari.core.permissions.Permission` + #: :type: :obj`hikari.core.permissions.Permission` my_permissions: permissions_.Permission = marshaller.attrib( raw_name="permissions", deserializer=permissions_.Permission ) #: The voice region for the guild. #: - #: :type: :class:`str` + #: :type: :obj`str` region: str = marshaller.attrib(deserializer=str) #: The ID for the channel that AFK voice users get sent to, if set for the #: guild. #: - #: :type: :class:`snowflakes.Snowflake`, optional + #: :type: :obj`snowflakes.Snowflake`, optional afk_channel_id: typing.Optional[snowflakes.Snowflake] = marshaller.attrib(deserializer=str, if_none=None) #: How long a voice user has to be AFK for before they are classed as being #: AFK and are moved to the AFK channel (:attr:`afk_channel_id`). #: - #: :type: :class:`datetime.timedelta` + #: :type: :obj`datetime.timedelta` afk_timeout: datetime.timedelta = marshaller.attrib( raw_name="afk_timeout", deserializer=lambda seconds: datetime.timedelta(seconds=seconds) ) @@ -310,7 +309,7 @@ class Guild(snowflakes.UniqueEntity, entities.Deserializable): #: Defines if the guild embed is enabled or not. This information may not #: be present, in which case, it will be ``None`` instead. #: - #: :type: :class:`bool`, optional + #: :type: :obj`bool`, optional is_embed_enabled: typing.Optional[bool] = marshaller.attrib( raw_name="embed_enabled", if_undefined=lambda: False, deserializer=bool ) @@ -318,33 +317,33 @@ class Guild(snowflakes.UniqueEntity, entities.Deserializable): #: The channel ID that the guild embed will generate an invite to, if #: enabled for this guild. If not enabled, it will be ``None``. #: - #: :type: :class:`snowflakes.Snowflake`, optional + #: :type: :obj`snowflakes.Snowflake`, optional embed_channel_id: typing.Optional[snowflakes.Snowflake] = marshaller.attrib( deserializer=snowflakes.Snowflake, if_none=None, if_undefined=None ) #: The verification level required for a user to participate in this guild. #: - #: :type: :class:`GuildVerificationLevel` + #: :type: :obj`GuildVerificationLevel` verification_level: GuildVerificationLevel = marshaller.attrib(deserializer=GuildVerificationLevel) #: The default setting for message notifications in this guild. #: - #: :type: :class:`GuildMessageNotificationsLevel` + #: :type: :obj`GuildMessageNotificationsLevel` default_message_notifications: GuildMessageNotificationsLevel = marshaller.attrib( deserializer=GuildMessageNotificationsLevel ) #: The setting for the explicit content filter in this guild. #: - #: :type: :class:`GuildExplicitContentFilterLevel` + #: :type: :obj`GuildExplicitContentFilterLevel` explicit_content_filter: GuildExplicitContentFilterLevel = marshaller.attrib( deserializer=GuildExplicitContentFilterLevel ) #: The roles in this guild, represented as a mapping of ID to role object. #: - #: :type: :class:`typing.Mapping` [ :class:`snowflakes.Snowflake`, :class:`GuildRole` ] + #: :type: :obj`typing.Mapping` [ :obj`snowflakes.Snowflake`, :obj`GuildRole` ] roles: typing.Mapping[snowflakes.Snowflake, GuildRole] = marshaller.attrib( deserializer=lambda roles: {r.id: r for r in map(GuildRole.deserialize, roles)}, ) @@ -352,27 +351,27 @@ class Guild(snowflakes.UniqueEntity, entities.Deserializable): #: The emojis that this guild provides, represented as a mapping of ID to #: emoji object. #: - #: :type: :class:`typing.Mapping` [ :class:`snowflakes.Snowflake`, :class:`GuildEmoji` ] + #: :type: :obj`typing.Mapping` [ :obj`snowflakes.Snowflake`, :obj`GuildEmoji` ] emojis: typing.Mapping[snowflakes.Snowflake, GuildEmoji] = marshaller.attrib( deserializer=lambda emojis: {e.id: e for e in map(GuildEmoji.deserialize, emojis)}, ) #: A set of the features in this guild. #: - #: :type: :class:`typing.Set` [ :class:`GuildFeature` ] + #: :type: :obj`typing.Set` [ :obj`GuildFeature` ] features: typing.Set[GuildFeature] = marshaller.attrib( deserializer=lambda features: {transformations.try_cast(f, GuildFeature, f) for f in features}, ) #: The required MFA level for users wishing to participate in this guild. #: - #: :type: :class:`GuildMFALevel` + #: :type: :obj`GuildMFALevel` mfa_level: GuildMFALevel = marshaller.attrib(deserializer=GuildMFALevel) #: The ID of the application that created this guild, if it was created by #: a bot. If not, this is always ``None``. #: - #: :type: :class:`snowflakes.Snowflake`, optional + #: :type: :obj`snowflakes.Snowflake`, optional application_id: typing.Optional[snowflakes.Snowflake] = marshaller.attrib( deserializer=snowflakes.Snowflake, if_none=None ) @@ -381,7 +380,7 @@ class Guild(snowflakes.UniqueEntity, entities.Deserializable): #: Describes whether the guild widget is enabled or not. If this information #: is not present, this will be ``None``. #: - #: :type: :class:`bool`, optional + #: :type: :obj`bool`, optional is_widget_enabled: typing.Optional[bool] = marshaller.attrib( raw_name="widget_enabled", if_undefined=None, deserializer=bool ) @@ -389,14 +388,15 @@ class Guild(snowflakes.UniqueEntity, entities.Deserializable): #: The channel ID that the widget's generated invite will send the user to, #: if enabled. If this information is unavailable, this will be ``None``. #: - #: :type: :class:`snowflakes.Snowflake`, optional + #: :type: :obj`snowflakes.Snowflake`, optional widget_channel_id: typing.Optional[snowflakes.Snowflake] = marshaller.attrib( if_undefined=None, if_none=None, deserializer=snowflakes.Snowflake ) #: The ID of the system channel (where welcome messages and Nitro boost #: messages are sent), or ``None`` if it is not enabled. - #: :type: :class:`snowflakes.Snowflake`, optional + #: + #: :type: :obj`snowflakes.Snowflake`, optional system_channel_id: typing.Optional[snowflakes.Snowflake] = marshaller.attrib( if_none=None, deserializer=snowflakes.Snowflake ) @@ -404,14 +404,14 @@ class Guild(snowflakes.UniqueEntity, entities.Deserializable): #: Flags for the guild system channel to describe which notification #: features are suppressed. #: - #: :type: :class:`GuildSystemChannelFlag` + #: :type: :obj`GuildSystemChannelFlag` system_channel_flags: GuildSystemChannelFlag = marshaller.attrib(deserializer=GuildSystemChannelFlag) #: The ID of the channel where guilds with the :obj:`GuildFeature.PUBLIC` #: :attr:`features` display rules and guidelines. If the #: :obj:`GuildFeature.PUBLIC` feature is not defined, then this is ``None``. #: - #: :type: :class:`snowflakes.Snowflake`, optional + #: :type: :obj`snowflakes.Snowflake`, optional rules_channel_id: typing.Optional[snowflakes.Snowflake] = marshaller.attrib( if_none=None, deserializer=snowflakes.Snowflake ) @@ -419,10 +419,10 @@ class Guild(snowflakes.UniqueEntity, entities.Deserializable): #: The date and time that the bot user joined this guild. #: #: This information is only available if the guild was sent via a - #: `GUILD_CREATE` event. If the guild is received from any other place, + #: ``GUILD_CREATE`` event. If the guild is received from any other place, #: this will always be ``None``. #: - #: :type: :class:`datetime.datetime`, optional + #: :type: :obj`datetime.datetime`, optional joined_at: typing.Optional[datetime.datetime] = marshaller.attrib( raw_name="joined_at", deserializer=dates.parse_iso_8601_ts, ) @@ -430,19 +430,19 @@ class Guild(snowflakes.UniqueEntity, entities.Deserializable): #: Whether the guild is considered to be large or not. #: #: This information is only available if the guild was sent via a - #: `GUILD_CREATE` event. If the guild is received from any other place, + #: ``GUILD_CREATE`` event. If the guild is received from any other place, #: this will always be ``None``. #: #: The implications of a large guild are that presence information will #: not be sent about members who are offline or invisible. #: - #: :type: :class:`bool`, optional + #: :type: :obj`bool`, optional is_large: typing.Optional[bool] = marshaller.attrib(raw_name="large", if_undefined=None, deserializer=bool) #: Whether the guild is unavailable or not. #: #: This information is only available if the guild was sent via a - #: `GUILD_CREATE` event. If the guild is received from any other place, + #: ``GUILD_CREATE`` event. If the guild is received from any other place, #: this will always be ``None``. #: #: An unavailable guild cannot be interacted with, and most information may @@ -457,13 +457,13 @@ class Guild(snowflakes.UniqueEntity, entities.Deserializable): #: `GUILD_CREATE` event. If the guild is received from any other place, #: this will always be ``None``. #: - #: :type: :class:`int`, optional + #: :type: :obj`int`, optional member_count: typing.Optional[int] = marshaller.attrib(if_undefined=None, deserializer=int) #: A mapping of ID to the corresponding guild members in this guild. #: #: This information is only available if the guild was sent via a - #: `GUILD_CREATE` event. If the guild is received from any other place, + #: ``GUILD_CREATE`` event. If the guild is received from any other place, #: this will always be ``None``. #: #: Additionally, any offline members may not be included here, especially @@ -477,7 +477,7 @@ class Guild(snowflakes.UniqueEntity, entities.Deserializable): #: representation. If you need complete accurate information, you should #: query the members using the appropriate API call instead. #: - #: :type: :class:`typing.Mapping` [ :class:`snowflakes.Snowflake`, :class:`GuildMember` ], optional + #: :type: :obj`typing.Mapping` [ :obj`snowflakes.Snowflake`, :obj`GuildMember` ], optional members: typing.Optional[typing.Mapping[snowflakes.Snowflake, GuildMember]] = marshaller.attrib( deserializer=lambda members: {m.user.id: m for m in map(GuildMember.deserialize, members)}, if_undefined=None, ) @@ -485,7 +485,7 @@ class Guild(snowflakes.UniqueEntity, entities.Deserializable): #: A mapping of ID to the corresponding guild channels in this guild. #: #: This information is only available if the guild was sent via a - #: `GUILD_CREATE` event. If the guild is received from any other place, + #: ``GUILD_CREATE`` event. If the guild is received from any other place, #: this will always be ``None``. #: #: Additionally, any channels that you lack permissions to see will not be @@ -497,7 +497,7 @@ class Guild(snowflakes.UniqueEntity, entities.Deserializable): #: To retrieve a list of channels in any other case, you should make an #: appropriate API call to retrieve this information. #: - #: :type: :class:`typing.Mapping` [ :class:`snowflakes.Snowflake`, :class:`GuildChannel` ], optional + #: :type: :obj`typing.Mapping` [ :obj`snowflakes.Snowflake`, :obj`GuildChannel` ], optional channels: typing.Optional[typing.Mapping[snowflakes.Snowflake, GuildChannel]] = marshaller.attrib( deserializer=lambda guild_channels: {c.id: c for c in map(parse_guild_channel, guild_channels)}, if_undefined=None, @@ -507,7 +507,7 @@ class Guild(snowflakes.UniqueEntity, entities.Deserializable): #: the given member, if available. #: #: This information is only available if the guild was sent via a - #: `GUILD_CREATE` event. If the guild is received from any other place, + #: ``GUILD_CREATE`` event. If the guild is received from any other place, #: this will always be ``None``. #: #: Additionally, any channels that you lack permissions to see will not be @@ -519,7 +519,7 @@ class Guild(snowflakes.UniqueEntity, entities.Deserializable): #: To retrieve a list of presences in any other case, you should make an #: appropriate API call to retrieve this information. #: - #: :type: :class:`typing.Mapping` [ :class:`snowflakes.Snowflake`, :class:`GuildMemberPresence` ], optional + #: :type: :obj`typing.Mapping` [ :obj`snowflakes.Snowflake`, :obj`GuildMemberPresence` ], optional presences: typing.Optional[typing.Mapping[snowflakes.Snowflake, GuildMemberPresence]] = marshaller.attrib( deserializer=lambda presences: {p.user.id: p for p in map(GuildMemberPresence.deserialize, presences)}, if_undefined=None, @@ -528,21 +528,21 @@ class Guild(snowflakes.UniqueEntity, entities.Deserializable): #: The maximum number of presences for the guild. If this is ``None``, then #: the default value is used (currently 5000). #: - #: :type: :class:`int`, optional + #: :type: :obj`int`, optional max_presences: typing.Optional[int] = marshaller.attrib(if_none=None, if_undefined=None, deserializer=int) #: The maximum number of members allowed in this guild. #: #: This information may not be present, in which case, it will be ``None``. #: - #: :type: :class:`int`, optional + #: :type: :obj`int`, optional max_members: typing.Optional[int] = marshaller.attrib(if_undefined=None, deserializer=int) #: The vanity URL code for the guild's vanity URL. #: This is only present if :obj:`GuildFeatures.VANITY_URL` is in the #: :attr:`features` for this guild. If not, this will always be ``None``. #: - #: :type: :class:`str`, optional + #: :type: :obj`str`, optional vanity_url_code: typing.Optional[str] = marshaller.attrib(if_none=None, deserializer=str) #: The guild's description. @@ -551,33 +551,33 @@ class Guild(snowflakes.UniqueEntity, entities.Deserializable): #: Otherwise, this will always be ``None``. For all other purposes, it is #: ``None``. #: - #: :type: :class:`str`, optional + #: :type: :obj`str`, optional description: typing.Optional[str] = marshaller.attrib(if_none=None, deserializer=str) #: The hash for the guild's banner. #: This is only present if the guild has :obj:`GuildFeatures.BANNER` in the #: :attr:`features` for this guild. For all other purposes, it is ``None``. #: - #: :type: :class:`str`, optional + #: :type: :obj`str`, optional banner_hash: typing.Optional[str] = marshaller.attrib(raw_name="banner", if_none=None, deserializer=str) #: The premium tier for this guild. #: - #: :type: :class:`GuildPremiumTier` + #: :type: :obj`GuildPremiumTier` premium_tier: GuildPremiumTier = marshaller.attrib(deserializer=GuildPremiumTier) #: The number of nitro boosts that the server currently has. This #: information may not be present, in which case, it will be ``None``. #: - #: :type: :class:`int`, optional + #: :type: :obj`int`, optional premium_subscription_count: typing.Optional[int] = marshaller.attrib(if_undefined=None, deserializer=int) #: The preferred locale to use for this guild. #: #: This can only be change if :obj:`GuildFeatures.PUBLIC` is in the - #: :attr:`features` for this guild and will otherwise default to `en-US`. + #: :attr:`features` for this guild and will otherwise default to ``en-US```. #: - #: :type: :class:`str` + #: :type: :obj`str` preferred_locale: str = marshaller.attrib(deserializer=str) #: The channel ID of the channel where admins and moderators receive notices @@ -587,7 +587,7 @@ class Guild(snowflakes.UniqueEntity, entities.Deserializable): #: :attr:`features` for this guild. For all other purposes, it should be #: considered to be ``None``. #: - #: :type: :class:`snowflakes.Snowflake`, optional + #: :type: :obj`snowflakes.Snowflake`, optional public_updates_channel_id: typing.Optional[snowflakes.Snowflake] = marshaller.attrib( if_none=None, deserializer=snowflakes.Snowflake ) diff --git a/hikari/core/permissions.py b/hikari/core/permissions.py index 111c75a8a1..9f0f965784 100644 --- a/hikari/core/permissions.py +++ b/hikari/core/permissions.py @@ -27,7 +27,7 @@ class Permission(enum.IntFlag): """Represents the permissions available in a given channel or guild. This is an int-flag enum. This means that you can **combine multiple - permissions together** into one value using the bitwise-OR "`|`" operator. + permissions together** into one value using the bitwise-OR operator (``|``). .. code-block:: python @@ -41,8 +41,8 @@ class Permission(enum.IntFlag): ) You can **check if a permission is present** in a set of combined - permissions by using the bitwise-AND "`&`" operator. This will return - the int-value of the permission if it is present, or `0` if not present. + permissions by using the bitwise-AND operator (``&``). This will return + the int-value of the permission if it is present, or ``0`` if not present. .. code-block:: python @@ -66,8 +66,8 @@ class Permission(enum.IntFlag): print("I don't have the permission to manage channels!") If you need to **check that a permission is not present**, you can use the - bitwise-XOR "`^`" operator to check. If the permission is not present, it - will return a non-zero value, otherwise if it is present, it will return 0. + bitwise-XOR operator (``^``) to check. If the permission is not present, it + will return a non-zero value, otherwise if it is present, it will return ``0``. .. code-block:: python @@ -77,7 +77,7 @@ class Permission(enum.IntFlag): print("Please give me the MANAGE_CHANNELS permission!") Lastly, if you need all the permissions set except the permission you want, - you can use the inversion operator "`~`" to do that. + you can use the inversion operator (``~``) to do that. .. code-block:: python @@ -112,7 +112,7 @@ class Permission(enum.IntFlag): VIEW_CHANNEL = 0x4_00 #: Allows for sending messages in a channel. SEND_MESSAGES = 0x8_00 - #: Allows for sending of `/tts` messages. + #: Allows for sending of ``/tts`` messages. SEND_TTS_MESSAGES = 0x10_00 #: Allows for deletion of other users messages. MANAGE_MESSAGES = 0x20_00 @@ -122,8 +122,9 @@ class Permission(enum.IntFlag): ATTACH_FILES = 0x80_00 #: Allows for reading of message history. READ_MESSAGE_HISTORY = 0x1_00_00 - #: Allows for using the `@everyone` tag to notify all users in a channel, and the - #: `@here` tag to notify all online users in a channel. + #: Allows for using the ``@everyone`` tag to notify all users in a channel, and the + #: ``@here`` tag to notify all online users in a channel, and the ``@role`` tag (even + #: if the role is not mentionable) to notify all users with that role in a channel. MENTION_EVERYONE = 0x2_00_00 #: Allows the usage of custom emojis from other servers. USE_EXTERNAL_EMOJIS = 0x4_00_00 diff --git a/hikari/core/snowflakes.py b/hikari/core/snowflakes.py index 433d2e2128..5dd51a457a 100644 --- a/hikari/core/snowflakes.py +++ b/hikari/core/snowflakes.py @@ -103,5 +103,5 @@ class UniqueEntity(entities.HikariEntity): #: The ID of this entity. #: - #: :type: :class:`Snowflake` + #: :type: :obj:`Snowflake` id: Snowflake = marshaller.attrib(hash=True, eq=True, repr=True, deserializer=Snowflake, serializer=str) diff --git a/hikari/net/http_client.py b/hikari/net/http_client.py index 7e1842bd5d..4acaa4716a 100644 --- a/hikari/net/http_client.py +++ b/hikari/net/http_client.py @@ -573,7 +573,7 @@ async def create_message( tts : :obj:`bool` If specified, whether the message will be sent as a TTS message. files : :obj:`typing.Sequence` [ :obj:`typing.Tuple` [ :obj:`str`, :obj:`storage.FileLikeT` ] ] - If specified, this should be a list of between 1 and 5 tuples. + If specified, this should be a list of between ``1`` and ``5`` tuples. Each tuple should consist of the file name, and either raw :obj:`bytes` or an :obj:`io.IOBase` derived object with a seek that points to a buffer containing said file. @@ -595,7 +595,7 @@ async def create_message( :obj:`hikari.net.errors.BadRequestHTTPError` This can be raised if the file is too large; if the embed exceeds the defined limits; if the message content is specified only and - empty or greater than 2000 characters; if neither content, file + empty or greater than ``2000`` characters; if neither content, file or embed are specified; if there is a duplicate id in only of the fields in ``allowed_mentions``; if you specify to parse all users/roles mentions but also specify which users/roles to @@ -832,9 +832,11 @@ async def edit_message( :obj:`hikari.net.errors.NotFoundHTTPError` If the channel or message is not found. :obj:`hikari.net.errors.BadRequestHTTPError` - If the embed exceeds any of the embed limits if specified, or the content is specified and consists - only of whitespace, is empty, or is more than 2,000 characters in length. This can also be caused - by trying to send an empty message (no content and no embed). + This can be raised if the embed exceeds the defined limits; + if the message content is specified only and empty or greater + than ``2000`` characters; if neither content, file or embed + are specified. + parse only. :obj:`hikari.net.errors.ForbiddenHTTPError` If you try to edit content or embed on a message you did not author or try to edit the flags on a message you did not author without the ``MANAGE_MESSAGES`` permission. @@ -875,7 +877,7 @@ async def bulk_delete_messages(self, channel_id: str, messages: typing.Sequence[ channel_id : :obj:`str` The ID of the channel to get the message from. messages : :obj:`typing.Sequence` [ :obj:`str` ] - A list of 2-100 message IDs to remove in the channel. + A list of ``2-100`` message IDs to remove in the channel. Raises ------ @@ -884,13 +886,13 @@ async def bulk_delete_messages(self, channel_id: str, messages: typing.Sequence[ :obj:`hikari.net.errors.ForbiddenHTTPError` If you lack the ``MANAGE_MESSAGES`` permission in the channel. :obj:`hikari.net.errors.BadRequestHTTPError` - If any of the messages passed are older than 2 weeks in age or any duplicate message IDs are passed. + If any of the messages passed are older than ``2`` weeks in age or any duplicate message IDs are passed. Note ---- This can only be used on guild text channels. - Any message IDs that do not exist or are invalid still add towards the total 100 max messages to remove. - This can only delete messages that are newer than 2 weeks in age. If any of the messages are older than 2 weeks + Any message IDs that do not exist or are invalid still add towards the total ``100`` max messages to remove. + This can only delete messages that are newer than ``2`` weeks in age. If any of the messages are older than ``2`` weeks then this call will fail. """ payload = {"messages": messages} @@ -904,7 +906,7 @@ async def edit_channel_permissions( *, allow: typing.Union[typing.Literal[...], int] = ..., deny: typing.Union[typing.Literal[...], int] = ..., - type_: typing.Literal[..., "member", "role"] = ..., + type_: typing.Union[typing.Literal[...], str] = ..., reason: typing.Union[typing.Literal[...], str] = ..., ) -> None: """Edit permissions for a given channel. @@ -919,9 +921,9 @@ async def edit_channel_permissions( If specified, the bitwise value of all permissions to set to be allowed. deny : :obj:`int` If specified, the bitwise value of all permissions to set to be denied. - type_ : :obj:`typing.Literal` [ ``"member"``, ``"role"``] - If specified, the type of overwrite. "member" if it is for a member, - or "role" if it is for a role. + type_ : :obj:`str`] + If specified, the type of overwrite. ``"member"`` if it is for a member, + or ``"role"`` if it is for a role. reason : :obj:`str` If specified, the audit log reason explaining why the operation was performed. @@ -983,7 +985,7 @@ async def create_channel_invite( The ID of the channel to create the invite for. max_age : :obj:`int` If specified, the max age of the invite in seconds, defaults to - ``86400`` (24 hours). + ``86400`` (``24`` hours). Set to ``0`` to never expire. max_uses : :obj:`int` If specified, the max number of uses this invite can have, or ``0`` for @@ -1046,7 +1048,7 @@ async def delete_channel_permission(self, channel_id: str, overwrite_id: str) -> await self._request(route) async def trigger_typing_indicator(self, channel_id: str) -> None: - """Trigger the account to appear to be typing for the next 10 seconds in the given channel. + """Trigger the account to appear to be typing for the next ``10`` seconds in the given channel. Parameters ---------- @@ -1200,7 +1202,7 @@ async def create_guild_emoji( name : :obj:`str` The new emoji's name. image : :obj:`bytes` - The 128x128 image in bytes form. + The ``128x128`` image in bytes form. roles : :obj:`typing.Sequence` [ :obj:`str` ] If specified, a list of roles for which the emoji will be whitelisted. If empty, all roles are whitelisted. @@ -1222,7 +1224,7 @@ async def create_guild_emoji( :obj:`hikari.net.errors.ForbiddenHTTPError` If you either lack the ``MANAGE_EMOJIS`` permission or aren't a member of said guild. :obj:`hikari.net.errors.BadRequestHTTPError` - If you attempt to upload an image larger than 256kb, an empty image or an invalid image format. + If you attempt to upload an image larger than ``256kb``, an empty image or an invalid image format. """ assertions.assert_not_none(image, "image must be a valid image") payload = { @@ -1310,23 +1312,23 @@ async def create_guild( roles: typing.Union[typing.Literal[...], typing.Sequence[typing.Dict]] = ..., channels: typing.Union[typing.Literal[...], typing.Sequence[typing.Dict]] = ..., ) -> typing.Dict: - """Creates a new guild. Can only be used by bots in less than 10 guilds. + """Creates a new guild. Can only be used by bots in less than ``10`` guilds. Parameters ---------- name : :obj:`str` - The name string for the new guild (2-100 characters). + The name string for the new guild (``2-100`` characters). region : :obj:`str` If specified, the voice region ID for new guild. You can use :meth:`list_voice_regions` to see which region IDs are available. icon : :obj:`bytes` If specified, the guild icon image in bytes form. verification_level : :obj:`int` - If specified, the verification level integer (0-5). + If specified, the verification level integer (``0-5``). default_message_notifications : :obj:`int` - If specified, the default notification level integer (0-1). + If specified, the default notification level integer (``0-1``). explicit_content_filter : :obj:`int` - If specified, the explicit content filter integer (0-2). + If specified, the explicit content filter integer (``0-2``). roles : :obj:`typing.Sequence` [ :obj:`typing.Dict` ] If specified, an array of role objects to be created alongside the guild. First element changes the ``@everyone`` role. @@ -1404,16 +1406,16 @@ async def modify_guild( # lgtm [py/similar-function] guild_id : :obj:`str` The ID of the guild to be edited. name : :obj:`str` - If specified, the new name string for the guild (2-100 characters). + If specified, the new name string for the guild (``2-100`` characters). region : :obj:`str` If specified, the new voice region ID for guild. You can use :meth:`list_voice_regions` to see which region IDs are available. verification_level : :obj:`int` - If specified, the new verification level integer (0-5). + If specified, the new verification level integer (``0-5``). default_message_notifications : :obj:`int` - If specified, the new default notification level integer (0-1). + If specified, the new default notification level integer (``0-1``). explicit_content_filter : :obj:`int` - If specified, the new explicit content filter integer (0-2). + If specified, the new explicit content filter integer (``0-2``). afk_channel_id : :obj:`str` If specified, the new ID for the AFK voice channel. afk_timeout : :obj:`int` @@ -1528,7 +1530,7 @@ async def create_guild_channel( If specified, the name for the channel.This must be between ``2`` and ``100`` characters in length. type_: :obj:`int` - If specified, the channel type integer (0-6). + If specified, the channel type integer (``0-6``). position : :obj:`int` If specified, the position to change the channel to. topic : :obj:`str` @@ -2167,7 +2169,7 @@ async def get_guild_prune_count(self, guild_id: str, days: int) -> int: guild_id : :obj:`str` The ID of the guild you want to get the count for. days : :obj:`int` - The number of days to count prune for (at least 1). + The number of days to count prune for (at least ``1``). Returns ------- @@ -2505,16 +2507,14 @@ async def get_guild_vanity_url(self, guild_id: str) -> typing.Dict: route = routes.GUILD_VANITY_URL.compile(self.GET, guild_id=guild_id) return await self._request(route) - def get_guild_widget_image_url( - self, guild_id: str, *, style: typing.Literal[..., "shield", "banner1", "banner2", "banner3", "banner4"] = ..., - ) -> str: + def get_guild_widget_image_url(self, guild_id: str, *, style: typing.Union[typing.Literal[...], str] = ...,) -> str: """Get the URL for a guild widget. Parameters ---------- guild_id : :obj:`str` The guild ID to use for the widget. - style : :obj:`typing.Literal` [ ``"shield"``, ``"banner1"``, ``"banner2"``, ``"banner3"``, ``"banner4"`` ] + style : :obj:`str` If specified, the syle of the widget. Returns @@ -2643,7 +2643,7 @@ async def modify_current_user( Raises ------ :obj:`hikari.net.errors.BadRequestHTTPError` - If you pass username longer than the limit (2-32) or an invalid image. + If you pass username longer than the limit (``2-32``) or an invalid image. """ payload = {} transformations.put_if_specified(payload, "username", username) @@ -3021,7 +3021,7 @@ async def execute_webhook( :obj:`hikari.net.errors.BadRequestHTTPError` This can be raised if the file is too large; if the embed exceeds the defined limits; if the message content is specified only and - empty or greater than 2000 characters; if neither content, file + empty or greater than ``2000`` characters; if neither content, file or embed are specified; if there is a duplicate id in only of the fields in ``allowed_mentions``; if you specify to parse all users/roles mentions but also specify which users/roles to From 51a32a0d241942fcdd55dcb4c620d6083d723351 Mon Sep 17 00:00:00 2001 From: davfsa Date: Wed, 18 Mar 2020 21:21:19 +0100 Subject: [PATCH 015/922] Re-enabled Python39 related tasks and SAST --- gitlab/test.yml | 45 +++++++++++++++++++++++---------------------- 1 file changed, 23 insertions(+), 22 deletions(-) diff --git a/gitlab/test.yml b/gitlab/test.yml index a960dd6037..78d4883615 100644 --- a/gitlab/test.yml +++ b/gitlab/test.yml @@ -17,14 +17,15 @@ include: - template: "Dependency-Scanning.gitlab-ci.yml" + - template: "SAST.gitlab-ci.yml" - template: "License-Scanning.gitlab-ci.yml" -#sast: -# interruptible: true -# retry: 2 -# variables: -# SAST_BANDIT_EXCLUDED_PATHS: tests/*,docs/*,gitlab/*,insomnia/*,public/*,tasks/*,noxfile.py -# SAST_EXCLUDED_PATHS: tests/*,docs/*,gitlab/*,insomnia/*,public/*,tasks/*,noxfile.py +sast: + interruptible: true + retry: 2 + variables: + SAST_BANDIT_EXCLUDED_PATHS: tests/*,docs/*,gitlab/*,insomnia/*,public/*,tasks/*,noxfile.py + SAST_EXCLUDED_PATHS: tests/*,docs/*,gitlab/*,insomnia/*,public/*,tasks/*,noxfile.py license_scanning: # Overly slow to run and not very important, so skip normally to get 5 minutes back on @@ -76,10 +77,10 @@ dependency_scanning: image: python:3.8.1 extends: .venv -#.cpython-3.9-rc: -# interruptible: true -# image: python:3.9-rc -# extends: .venv +.cpython-3.9-rc: + interruptible: true + image: python:3.9-rc + extends: .venv .cpython-tool-alpine: interruptible: true @@ -136,10 +137,10 @@ pytest-c3.8.2: - .cpython-3.8.2 - .nox-test -#pytest-c3.9-rc: -# extends: -# - .cpython-3.9-rc -# - .nox-test +pytest-c3.9-rc: + extends: + - .cpython-3.9-rc + - .nox-test install-c3.8.0: extends: @@ -156,10 +157,10 @@ install-c3.8.2: - .cpython-3.8.2 - .nox-pip-install -#install-c3.9-rc: -# extends: -# - .cpython-3.9-rc -# - .nox-pip-install +install-c3.9-rc: + extends: + - .cpython-3.9-rc + - .nox-pip-install verify-c3.8.0-pypi: extends: @@ -176,10 +177,10 @@ verify-c3.8.2-pypi: - .cpython-3.8.2 - .nox-pip-install-showtime -#verify-c3.9-rc-pypi: -# extends: -# - .cpython-3.9-rc -# - .nox-pip-install-showtime +verify-c3.9-rc-pypi: + extends: + - .cpython-3.9-rc + - .nox-pip-install-showtime coverage-coalesce: extends: From 4a0c82ae6cbf1654fa2a1731e72b6a3bf45c9a2b Mon Sep 17 00:00:00 2001 From: Faster Speeding Date: Tue, 17 Mar 2020 21:11:25 +0000 Subject: [PATCH 016/922] Add user related models + tests and docs --- hikari/core/users.py | 148 ++++++++++++++++++++++++++++++-- tests/hikari/core/test_users.py | 105 ++++++++++++++++++++++ 2 files changed, 247 insertions(+), 6 deletions(-) create mode 100644 tests/hikari/core/test_users.py diff --git a/hikari/core/users.py b/hikari/core/users.py index 7e2e10854a..9e08bb205a 100644 --- a/hikari/core/users.py +++ b/hikari/core/users.py @@ -16,20 +16,156 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . -__all__ = ["User", "MyUser"] +"""Components and entities that are used to describe Users on Discord.""" +__all__ = ["User", "MyUser", "UserFlag"] -import attr +import enum +import urllib +import typing from hikari.core import entities from hikari.core import snowflakes from hikari.internal_utilities import marshaller -@marshaller.attrs() +@marshaller.attrs(slots=True) class User(snowflakes.UniqueEntity, entities.Deserializable): - ... + """Represents a user.""" + #: This user's discriminator. + #: + #: :type: :obj:`str` + discriminator: str = marshaller.attrib(deserializer=str) -@attr.s(slots=True) + #: This user's username. + #: + #: :type: :obj:`str` + username: str = marshaller.attrib(deserializer=str) + + #: This user's avatar hash, if set. + #: + #: :type: :obj:`str`, optional + avatar_hash: typing.Optional[str] = marshaller.attrib(raw_name="avatar", deserializer=str, if_none=None) + + #: Whether this user is a bot account. + #: + #: :type: :obj:`bool` + is_bot: bool = marshaller.attrib(raw_name="bot", deserializer=bool, if_undefined=lambda: False) + + #: Whether this user is a system account. + #: + #: :type: :obj:`bool` + is_system: bool = marshaller.attrib(raw_name="system", deserializer=bool, if_undefined=lambda: False) + + @property + def avatar_url(self) -> str: + """The url for this user's custom avatar if set, else default.""" + return self.format_avatar_url() + + def format_avatar_url(self, fmt: typing.Optional[str] = None, size: int = 2048) -> str: + """"Generate the avatar url for this user's custom avatar if set, + else their default avatar. + + Parameters + ---------- + fmt : :obj:`str` + The format to use for this url, defaults to ``png``. + Supports ``png``, ``jpeg``, ``webp`` and ``gif`` (when animated). + Will be ignored for default avatars which can only be ``png``. + size : :obj:`int` + The size to set for the url, defaults to ``2048``. + Can be any power of two between 16 and 2048. + Will be ignored for default avatars. + + Returns + ------- + :obj:`str` + The string url. + """ + + if not self.avatar_hash: + return f"https://cdn.discordapp.com/embed/avatars/{self.default_avatar}.png" + if fmt is None and self.avatar_hash.startswith("a_"): + fmt = "gif" + elif fmt is None: + fmt = "png" + return ( + f"https://cdn.discordapp.com/avatars/{self.id}/{self.avatar_hash}." + f"{urllib.parse.quote_plus(fmt)}?{urllib.parse.urlencode({'size': size})}" + ) + + @property + def default_avatar(self) -> int: + """Returns the number for this user's default avatar.""" + return int(self.discriminator) % 5 + + +@enum.unique +class UserFlag(enum.IntFlag): + """The known user flags that represent account badges.""" + + NONE = 0 + DISCORD_EMPLOYEE = 1 << 0 + DISCORD_PARTNER = 1 << 1 + HYPESQUAD_EVENTS = 1 << 2 + BUG_HUNTER_LEVEL_1 = 1 << 3 + HOUSE_BRAVERY = 1 << 6 + HOUSE_BRILLIANCE = 1 << 7 + HOUSE_BALANCE = 1 << 8 + EARLY_SUPPORTER = 1 << 9 + TEAM_USER = 1 << 10 + SYSTEM = 1 << 11 + BUG_HUNTER_LEVEL_2 = 1 << 12 + + +@enum.unique +class PremiumType(enum.IntEnum): + """The types of Nitro.""" + + #: No premium. + NONE = 0 + #: Premium including basic perks like animated emojis and avatars. + NITRO_CLASSIC = 1 + #: Premium including all perks (e.g. 2 server boosts). + NITRO = 2 + + +@marshaller.attrs(slots=True) class MyUser(User): - ... + """Represents a user with extended oauth2 information.""" + + #: Whether the user's account has 2fa enabled. + #: Requires the ``identify`` scope. + #: + #: :type: :obj:`bool`, optional + is_mfa_enabled: typing.Optional[bool] = marshaller.attrib( + raw_name="mfa_enabled", deserializer=bool, if_undefined=None + ) + + #: The user's set language, requires the ``identify`` scope. + #: + #: :type: :obj:`str`, optional + locale: typing.Optional[str] = marshaller.attrib(deserializer=str, if_undefined=None) + + #: Whether the email for this user's account has been verified. + #: Requires the ``email`` scope. + #: + #: :type: :obj:`bool`, optional + is_verified: typing.Optional[bool] = marshaller.attrib(raw_name="verified", deserializer=bool, if_undefined=None) + + #: The user's set email, requires the ``email`` scope. + #: This will always be ``None`` for bots. + #: + #: :type: :obj:`str`, optional + email: typing.Optional[str] = marshaller.attrib(deserializer=str, if_undefined=None, if_none=None) + + #: This user account's flags, requires the ``identify`` scope. + #: + #: :type: :obj:`UserFlag`, optional + flags: typing.Optional[UserFlag] = marshaller.attrib(deserializer=UserFlag, if_undefined=None) + + #: The type of Nitro Subscription this user account had. + #: Requires the ``identify`` scope and will always be ``None`` for bots. + #: + #: :type: :obj:`PremiumType`, optional + premium_type: typing.Optional[PremiumType] = marshaller.attrib(deserializer=PremiumType, if_undefined=None) diff --git a/tests/hikari/core/test_users.py b/tests/hikari/core/test_users.py new file mode 100644 index 0000000000..6ab1103d16 --- /dev/null +++ b/tests/hikari/core/test_users.py @@ -0,0 +1,105 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# Copyright © Nekoka.tt 2019-2020 +# +# This file is part of Hikari. +# +# Hikari is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Hikari is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along ith Hikari. If not, see . +from __future__ import annotations + +import pytest + +from hikari.core import users + + +@pytest.fixture() +def test_user_payload(): + return { + "id": "115590097100865541", + "username": "nyaa", + "avatar": "b3b24c6d7cbcdec129d5d537067061a8", + "discriminator": "6127", + } + + +@pytest.fixture() +def test_oauth_user_payload(): + return { + "id": "379953393319542784", + "username": "qt pi", + "avatar": "820d0e50543216e812ad94e6ab7", + "discriminator": "2880", + "email": "blahblah@blah.blah", + "verified": True, + "locale": "en-US", + "mfa_enabled": True, + "flags": int(users.UserFlag.DISCORD_PARTNER | users.UserFlag.DISCORD_EMPLOYEE), + "premium_type": 1, + } + + +class TestUser: + @pytest.fixture() + def user_obj(self, test_user_payload): + return users.User.deserialize(test_user_payload) + + def test_deserialize(self, user_obj): + assert user_obj.id == 115590097100865541 + assert user_obj.username == "nyaa" + assert user_obj.avatar_hash == "b3b24c6d7cbcdec129d5d537067061a8" + assert user_obj.discriminator == "6127" + + def test_avatar_url(self, user_obj): + url = user_obj.avatar_url + assert ( + url == "https://cdn.discordapp.com/avatars/115590097100865541" + "/b3b24c6d7cbcdec129d5d537067061a8.png?size=2048" + ) + + def test_default_avatar(self, user_obj): + assert user_obj.default_avatar == 2 + + def test_format_avatar_url_when_animated(self, user_obj): + user_obj.avatar_hash = "a_820d0e50543216e812ad94e6ab7" + url = user_obj.format_avatar_url(size=3232) + assert ( + url == "https://cdn.discordapp.com/avatars/115590097100865541/a_820d0e50543216e812ad94e6ab7.gif?size=3232" + ) + + def test_format_avatar_url_default(self, user_obj): + user_obj.avatar_hash = None + url = user_obj.format_avatar_url(size=3232) + assert url == "https://cdn.discordapp.com/embed/avatars/2.png" + + def test_format_avatar_url_when_format_specified(self, user_obj): + url = user_obj.format_avatar_url(fmt="nyaapeg", size=1024) + assert ( + url == "https://cdn.discordapp.com/avatars/115590097100865541" + "/b3b24c6d7cbcdec129d5d537067061a8.nyaapeg?size=1024" + ) + + +class TestMyUser: + def test_deserialize(self, test_oauth_user_payload): + my_user_obj = users.MyUser.deserialize(test_oauth_user_payload) + assert my_user_obj.id == 379953393319542784 + assert my_user_obj.username == "qt pi" + assert my_user_obj.avatar_hash == "820d0e50543216e812ad94e6ab7" + assert my_user_obj.discriminator == "2880" + assert my_user_obj.is_mfa_enabled is True + assert my_user_obj.locale == "en-US" + assert my_user_obj.is_verified is True + assert my_user_obj.email == "blahblah@blah.blah" + assert my_user_obj.flags == users.UserFlag.DISCORD_PARTNER | users.UserFlag.DISCORD_EMPLOYEE + assert my_user_obj.premium_type is users.PremiumType.NITRO_CLASSIC From 6d32b72716bd3ffcbe372b097d6dba9d59fdb4fa Mon Sep 17 00:00:00 2001 From: Faster Speeding Date: Tue, 17 Mar 2020 15:11:59 +0000 Subject: [PATCH 017/922] Add oauth2 related models + tests and docs --- hikari/core/oauth2.py | 271 +++++++++++++++++++++++- hikari/core/users.py | 1 + hikari/internal_utilities/marshaller.py | 4 +- tests/hikari/_helpers.py | 15 ++ tests/hikari/core/test_oauth2.py | 195 +++++++++++++++++ 5 files changed, 476 insertions(+), 10 deletions(-) create mode 100644 tests/hikari/core/test_oauth2.py diff --git a/hikari/core/oauth2.py b/hikari/core/oauth2.py index f748977ac4..ef98d16f5e 100644 --- a/hikari/core/oauth2.py +++ b/hikari/core/oauth2.py @@ -16,18 +16,273 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . -__all__ = ["Owner", "Team"] +"""Components and entities related to discord's Oauth2 flow.""" +__all__ = ["Application", "Owner", "Team", "TeamMember", "TeamMembershipState"] -import attr +import enum +import typing +import urllib +from hikari.core import entities from hikari.core import snowflakes +from hikari.core import users +from hikari.internal_utilities import marshaller -@attr.s(slots=True) -class Owner(snowflakes.UniqueEntity): - ... +@enum.unique +class TeamMembershipState(enum.IntEnum): + """Represents the state of a user's team membership.""" + #: Denotes the user has been invited to the team but has yet to accept. + INVITED = 1 -@attr.s(slots=True) -class Team(snowflakes.UniqueEntity): - ... + #: Denotes the user has accepted the invite and is now a member. + ACCEPTED = 2 + + +@marshaller.attrs(slots=True) +class TeamMember(entities.HikariEntity, entities.Deserializable): + """Represents a member of a Team.""" + + #: The state of this user's membership. + #: + #: :type: :obj:`TeamMembershipState` + membership_state: TeamMembershipState = marshaller.attrib(deserializer=TeamMembershipState) + + #: This member's permissions within a team. + #: Will always be ``["*"]`` until Discord starts using this. + #: + #: :type: :obj:`typing.Set` [ `str` ] + permissions: typing.Set[str] = marshaller.attrib( + deserializer=lambda permissions: {str(permission) for permission in permissions} + ) + + #: The ID of the team this member belongs to. + #: + #: :type: :obj:`hikari.core.snowflakes.Snowflake` + team_id: snowflakes.Snowflake = marshaller.attrib( + hash=True, eq=True, repr=True, deserializer=snowflakes.Snowflake, serializer=str, + ) + + #: The user object of this team member. + #: + #: :type: :obj:`TeamUser` + user: users.User = marshaller.attrib(deserializer=users.User.deserialize) + + +@marshaller.attrs(slots=True) +class Team(snowflakes.UniqueEntity, entities.Deserializable): + """This represents a Team and it's members.""" + + #: The hash of this team's icon, if set. + #: + #: :type: :obj:`str`, optional + icon_hash: typing.Optional[str] = marshaller.attrib(raw_name="icon", deserializer=str) + #: The member's that belong to this team. + #: + #: :type: :obj:`typing.Sequence` [ :obj:`TeamMember` ] + members: typing.Sequence[TeamMember] = marshaller.attrib( + deserializer=lambda members: {int(member["user"]["id"]): TeamMember.deserialize(member) for member in members} + ) + + #: The snowflake ID of this team's owner. + #: + #: :type: :obj:`hikari.core.snowflakes.Snowflake` + owner_user_id: snowflakes.Snowflake = marshaller.attrib(deserializer=snowflakes.Snowflake, serializer=str) + + @property + def icon_url(self) -> typing.Optional[str]: + """The url of this team's icon, if set.""" + return self.format_icon_url() + + def format_icon_url(self, fmt: str = "png", size: int = 2048) -> typing.Optional[str]: + """"Generate the icon url for this team if set. + + Parameters + ---------- + fmt : :obj:`str` + The format to use for this url, defaults to ``png``. + Supports ``png``, ``jpeg``, ``jpg`` and ``webp``. + size : :obj:`int` + The size to set for the url, defaults to ``2048``. + Can be any power of 2 in the range :math:`\left[16, 2048\right]`. + + Returns + ------- + :obj`str`, optional + The string url. + """ + if self.icon_hash: + return ( + f"https://cdn.discordapp.com/team-icons/{self.id}/{self.icon_hash}." + f"{urllib.parse.quote_plus(fmt)}?{urllib.parse.urlencode({'size': size})}" + ) + return None + + +@marshaller.attrs(slots=True) +class Owner(users.User): + """Represents the user who owns an application, may be a team user.""" + + #: This user's flags. + #: + #: :type: :obj:`int` + flags: int = marshaller.attrib(deserializer=int) + + @property + def is_team_user(self) -> bool: + """If this user is a Team user (the owner of an application that's owned by a team).""" + return bool((self.flags >> 10) & 1) + + +@marshaller.attrs(slots=True) +class Application(snowflakes.UniqueEntity, entities.Deserializable): + """Represents the information of an Oauth2 Application.""" + + #: The name of this application. + #: + #: :type: :obj:`str` + name: str = marshaller.attrib(deserializer=str) + + #: The description of this application, will be an empty string if unset. + #: + #: :type: :obj:`str` + description: str = marshaller.attrib(deserializer=str) + + #: Whether the bot associated with this application is public. + #: Will be ``None`` if this application doesn't have an associated bot. + #: + #: :type: :obj:`bool`, optional + is_bot_public: typing.Optional[bool] = marshaller.attrib( + raw_name="bot_public", deserializer=bool, if_undefined=None + ) + + #: Whether the bot associated with this application is requiring code grant + #: for invites. Will be ``None`` if this application doesn't have a bot. + #: + #: :type: :obj:`bool`, optional + is_bot_code_grant_required: typing.Optional[bool] = marshaller.attrib( + raw_name="bot_require_code_grant", deserializer=bool, if_undefined=None + ) + + #: The object of this application's owner. + #: This should always be ``None`` in application objects retrieved outside + #: Discord's oauth2 flow. + #: + #: :type: :obj:`Owner`, optional + owner: typing.Optional[Owner] = marshaller.attrib(deserializer=Owner.deserialize, if_undefined=None) + + #: A collection of this application's rpc origin urls, if rpc is enabled. + #: + #: :type: :obj:`typing.Set` [ :obj:`str` ], optional + rpc_origins: typing.Optional[typing.Set[str]] = marshaller.attrib( + deserializer=lambda origins: {str(origin) for origin in origins}, if_undefined=None + ) + + #: This summary for this application's primary SKU if it's sold on Discord. + #: Will be an empty string if unset. + #: + #: :type: :obj:`str` + summary: str = marshaller.attrib(deserializer=str) + + #: The base64 encoded key used for the GameSDK's ``GetTicket``. + #: + #: :type: :obj:`str` + verify_key: bytes = marshaller.attrib(deserializer=lambda key: bytes(key, "utf-8")) + + #: The hash of this application's icon if set. + #: + #: :type: :obj:`str`, optional + icon_hash: typing.Optional[str] = marshaller.attrib(raw_name="icon", deserializer=str, if_undefined=None) + + #: This application's team if it belongs to one. + #: + #: :type: :obj:`Team`, optional + team: typing.Optional[Team] = marshaller.attrib(deserializer=Team.deserialize, if_undefined=None, if_none=None) + + #: The ID of the guild this application is linked to + #: if it's sold on Discord. + #: + #: :type: :obj:`hikari.core.snowflakes.Snowflake`, optional + guild_id: typing.Optional[snowflakes.Snowflake] = marshaller.attrib( + deserializer=snowflakes.Snowflake, if_undefined=None + ) + + #: The ID of the primary "Game SKU" of a game that's sold on Discord. + #: + #: :type: :obj:`hikari.core.snowflakes.Snowflake`, optional + primary_sku_id: typing.Optional[snowflakes.Snowflake] = marshaller.attrib( + deserializer=snowflakes.Snowflake, if_undefined=None + ) + + #: The url slug that links to this application's store page + #: if it's sold on Discord. + #: + #: :type: :obj:`str`, optional + slug: typing.Optional[str] = marshaller.attrib(deserializer=str, if_undefined=None) + + #: The hash of this application's cover image on it's store, if set. + #: + #: :type: :obj:`str`, optional + cover_image_hash: typing.Optional[str] = marshaller.attrib( + raw_name="cover_image", deserializer=str, if_undefined=None + ) + + @property + def icon_url(self) -> typing.Optional[str]: + """The url for this team's icon, if set.""" + return self.format_icon_url() + + def format_icon_url(self, fmt: str = "png", size: int = 2048) -> typing.Optional[str]: + """"Generate the icon url for this application if set. + + Parameters + ---------- + fmt : :obj:`str` + The format to use for this url, defaults to ``png``. + Supports ``png``, ``jpeg``, ``jpg`` and ```webp``. + size : :obj:`int` + The size to set for the url, defaults to ``2048``. + Can be any power of two between 16 and 2048. + + Returns + ------- + :obj`str`, optional + The string url. + """ + if self.icon_hash: + return ( + f"https://cdn.discordapp.com/app-icons/{self.id}/{self.icon_hash}." + f"{urllib.parse.quote_plus(fmt)}?{urllib.parse.urlencode({'size': size})}" + ) + return None + + @property + def cover_image_url(self) -> typing.Optional[str]: + """The url for this icon's store cover image, if set.""" + return self.format_cover_image_url() + + def format_cover_image_url(self, fmt: str = "png", size: int = 2048) -> typing.Optional[str]: + """"Generate the url for this application's store page's cover image is + set and applicable. + + Parameters + ---------- + fmt : :obj:`str` + The format to use for this url, defaults to ``png``. + Supports ``png``, ``jpeg``, ``jpg`` and ``webp``. + size : :obj:`int` + The size to set for the url, defaults to ``2048``. + Can be any power of two between 16 and 2048. + + Returns + ------- + :obj`str`, optional + The string url. + """ + if self.cover_image_hash: + return ( + f"https://cdn.discordapp.com/app-assets/{self.id}/{self.cover_image_hash}" + f".{urllib.parse.quote_plus(fmt)}?{urllib.parse.urlencode({'size': size})}" + ) + return None diff --git a/hikari/core/users.py b/hikari/core/users.py index 9e08bb205a..1f2b1d4571 100644 --- a/hikari/core/users.py +++ b/hikari/core/users.py @@ -85,6 +85,7 @@ def format_avatar_url(self, fmt: typing.Optional[str] = None, size: int = 2048) if not self.avatar_hash: return f"https://cdn.discordapp.com/embed/avatars/{self.default_avatar}.png" + # pylint: disable=E1101: if fmt is None and self.avatar_hash.startswith("a_"): fmt = "gif" elif fmt is None: diff --git a/hikari/internal_utilities/marshaller.py b/hikari/internal_utilities/marshaller.py index be9fd75000..d1cf572439 100644 --- a/hikari/internal_utilities/marshaller.py +++ b/hikari/internal_utilities/marshaller.py @@ -259,12 +259,12 @@ def deserialize(self, raw_data: typing.Mapping[str, typing.Any], target_type: ty try: # Use the deserializer if it is there, otherwise use the constructor of the type of the field. kwargs[a.field_name] = a.deserializer(data) if a.deserializer else data - except Exception: + except Exception as exc: raise TypeError( "Failed to deserialize data to instance of " f"{target_type.__module__}.{target_type.__qualname__} because marshalling failed on " f"attribute {a.field_name}" - ) + ) from exc return target_type(**kwargs) diff --git a/tests/hikari/_helpers.py b/tests/hikari/_helpers.py index 4d86e72d42..cd9b4ae800 100644 --- a/tests/hikari/_helpers.py +++ b/tests/hikari/_helpers.py @@ -35,6 +35,8 @@ import cymock as mock import pytest +from hikari.internal_utilities import marshaller + _LOGGER = logging.getLogger(__name__) @@ -442,3 +444,16 @@ def stupid_windows_please_stop_breaking_my_tests(test): def create_autospec(spec, *args, **kwargs): return mock.create_autospec(spec, spec_set=True, *args, **kwargs) + + +def patch_marshal_attr(target_entity, field_name, *args, deserializer=None, **kwargs): + # noinspection PyProtectedMember + for attr in marshaller.HIKARI_ENTITY_MARSHALLER._registered_entities[target_entity].attribs: + if attr.field_name == field_name and (deserializer is None or attr.deserializer == deserializer): + target = attr + break + elif attr.field_name == field_name: + raise TypeError(f"Deserializer mismatch found on `{target_entity.__name__}.{attr.field_name}`") + else: + raise LookupError(f"Failed to find a `{field_name}` field on `{target_entity.__name__}`.") + return mock.patch.object(target, "deserializer", *args, **kwargs) diff --git a/tests/hikari/core/test_oauth2.py b/tests/hikari/core/test_oauth2.py new file mode 100644 index 0000000000..000984465f --- /dev/null +++ b/tests/hikari/core/test_oauth2.py @@ -0,0 +1,195 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# Copyright © Nekoka.tt 2019-2020 +# +# This file is part of Hikari. +# +# Hikari is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Hikari is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along ith Hikari. If not, see . +from __future__ import annotations + +import cymock as mock +import pytest + +from hikari.core import oauth2 +from hikari.core import users +from tests.hikari import _helpers + + +@pytest.fixture() +def owner_payload(): + return {"username": "agent 47", "avatar": "hashed", "discriminator": "4747", "id": "474747474", "flags": 1 << 10} + + +@pytest.fixture() +def team_user_payload(): + return {"username": "aka", "avatar": "I am an avatar", "discriminator": "2222", "id": "202292292"} + + +@pytest.fixture() +def member_payload(team_user_payload): + return {"membership_state": 1, "permissions": ["*"], "team_id": "209333111222", "user": team_user_payload} + + +@pytest.fixture() +def team_payload(member_payload): + return {"icon": "hashtag", "id": "202020202", "members": [member_payload], "owner_user_id": "393030292"} + + +@pytest.fixture() +def application_information_payload(owner_payload, team_payload): + return { + "id": "209333111222", + "name": "Dream Sweet in Sea Major", + "icon": "iwiwiwiwiw", + "description": "I am an app", + "rpc_origins": ["127.0.0.0"], + "bot_public": True, + "bot_require_code_grant": False, + "owner": owner_payload, + "summary": "", + "verify_key": "698c5d0859abb686be1f8a19e0e7634d8471e33817650f9fb29076de227bca90", + "team": team_payload, + "guild_id": "2020293939", + "primary_sku_id": "2020202002", + "slug": "192.168.1.254", + "cover_image": "hashmebaby", + } + + +class TestOwner: + @pytest.fixture() + def owner_obj(self, owner_payload): + return oauth2.Owner.deserialize(owner_payload) + + def test_deserialize(self, owner_obj): + assert owner_obj.username == "agent 47" + assert owner_obj.discriminator == "4747" + assert owner_obj.id == 474747474 + assert owner_obj.flags == users.UserFlag.TEAM_USER + assert owner_obj.avatar_hash == "hashed" + + def test_is_team_user(self, owner_obj): + owner_obj.flags = users.UserFlag.TEAM_USER | users.UserFlag.SYSTEM + assert owner_obj.is_team_user is True + owner_obj.flags = users.UserFlag.BUG_HUNTER_LEVEL_1 | users.UserFlag.HYPESQUAD_EVENTS + assert owner_obj.is_team_user is False + + +class TestTeamMember: + def test_deserialize(self, member_payload, team_user_payload): + mock_team_user = mock.MagicMock(users.User) + with _helpers.patch_marshal_attr( + oauth2.TeamMember, "user", deserializer=users.User.deserialize, return_value=mock_team_user + ) as patched_deserializer: + member_obj = oauth2.TeamMember.deserialize(member_payload) + patched_deserializer.assert_called_once_with(team_user_payload) + assert member_obj.user is mock_team_user + assert member_obj.membership_state is oauth2.TeamMembershipState.INVITED + assert member_obj.permissions == {"*"} + assert member_obj.team_id == 209333111222 + + +class TestTeam: + @pytest.fixture() + def team_obj(self, team_payload): + return oauth2.Team.deserialize(team_payload) + + def test_deserialize(self, team_payload, member_payload): + mock_members = {123: mock.MagicMock(oauth2.Team)} + with _helpers.patch_marshal_attr(oauth2.Team, "members", return_value=mock_members) as patched_deserializer: + team_obj = oauth2.Team.deserialize(team_payload) + patched_deserializer.assert_called_once_with([member_payload]) + assert team_obj.members is mock_members + assert team_obj.icon_hash == "hashtag" + assert team_obj.id == 202020202 + assert team_obj.owner_user_id == 393030292 + + def test_format_icon_url(self): + mock_team = _helpers.create_autospec(oauth2.Team, icon_hash="3o2o32o", id=22323) + url = oauth2.Team.format_icon_url(mock_team, fmt="jpg", size=64) + assert url == "https://cdn.discordapp.com/team-icons/22323/3o2o32o.jpg?size=64" + + def test_format_icon_url_returns_none(self): + mock_team = _helpers.create_autospec(oauth2.Team, icon_hash=None, id=22323) + url = oauth2.Team.format_icon_url(mock_team, fmt="jpg", size=64) + assert url is None + + def test_icon_url(self, team_obj): + url = team_obj.icon_url + assert url == "https://cdn.discordapp.com/team-icons/202020202/hashtag.png?size=2048" + + +class TestApplication: + @pytest.fixture() + def application_obj(self, application_information_payload): + return oauth2.Application.deserialize(application_information_payload) + + def test_deserialize(self, application_information_payload, team_payload, owner_payload): + mock_team = mock.MagicMock(oauth2.Team) + mock_owner = mock.MagicMock(oauth2.Owner) + with _helpers.patch_marshal_attr( + oauth2.Application, "team", deserializer=oauth2.Team.deserialize, return_value=mock_team + ) as patched_team_deserializer: + with _helpers.patch_marshal_attr( + oauth2.Application, "owner", deserializer=oauth2.Owner.deserialize, return_value=mock_owner + ) as patched_owner_deserializer: + application_obj = oauth2.Application.deserialize(application_information_payload) + patched_owner_deserializer.assert_called_once_with(owner_payload) + patched_team_deserializer.assert_called_once_with(team_payload) + assert application_obj.team is mock_team + assert application_obj.owner is mock_owner + assert application_obj.id == 209333111222 + assert application_obj.name == "Dream Sweet in Sea Major" + assert application_obj.icon_hash == "iwiwiwiwiw" + assert application_obj.description == "I am an app" + assert application_obj.rpc_origins == {"127.0.0.0"} + assert application_obj.is_bot_public is True + assert application_obj.is_bot_code_grant_required is False + assert application_obj.summary == "" + assert application_obj.verify_key == b"698c5d0859abb686be1f8a19e0e7634d8471e33817650f9fb29076de227bca90" + assert application_obj.guild_id == 2020293939 + assert application_obj.primary_sku_id == 2020202002 + assert application_obj.slug == "192.168.1.254" + assert application_obj.cover_image_hash == "hashmebaby" + + @pytest.fixture() + def mock_application(self): + return _helpers.create_autospec(oauth2.Application, id=22222) + + def test_icon_url(self, application_obj): + assert application_obj.icon_url == "https://cdn.discordapp.com/app-icons/209333111222/iwiwiwiwiw.png?size=2048" + + def test_format_icon_url(self, mock_application): + mock_application.icon_hash = "wosososoos" + url = oauth2.Application.format_icon_url(mock_application, fmt="jpg", size=4) + assert url == "https://cdn.discordapp.com/app-icons/22222/wosososoos.jpg?size=4" + + def test_format_icon_url_returns_none(self, mock_application): + mock_application.icon_hash = None + url = oauth2.Application.format_icon_url(mock_application, fmt="jpg", size=4) + assert url is None + + def test_cover_image_url(self, application_obj): + url = application_obj.cover_image_url + assert url == "https://cdn.discordapp.com/app-assets/209333111222/hashmebaby.png?size=2048" + + def test_format_cover_image_url(self, mock_application): + mock_application.cover_image_hash = "wowowowowo" + url = oauth2.Application.format_cover_image_url(mock_application, fmt="jpg", size=42) + assert url == "https://cdn.discordapp.com/app-assets/22222/wowowowowo.jpg?size=42" + + def test_format_cover_image_url_returns_none(self, mock_application): + mock_application.cover_image_hash = None + url = oauth2.Application.format_cover_image_url(mock_application, fmt="jpg", size=42) + assert url is None From 7c54b6ba24a1b89748250cc19ea06594f836b0ea Mon Sep 17 00:00:00 2001 From: Faster Speeding Date: Thu, 19 Mar 2020 19:29:14 +0000 Subject: [PATCH 018/922] Add invite related models + tests and docs and internal_utilities.cdn --- hikari/core/channels.py | 44 +++- hikari/core/guilds.py | 149 +++++++----- hikari/core/invites.py | 218 +++++++++++++++++- hikari/core/oauth2.py | 46 ++-- hikari/core/users.py | 18 +- hikari/internal_utilities/cdn.py | 54 +++++ hikari/net/http_client.py | 2 +- tests/hikari/_helpers.py | 5 +- tests/hikari/core/test_channels.py | 34 +++ tests/hikari/core/test_guilds.py | 73 +++++- tests/hikari/core/test_invites.py | 236 ++++++++++++++++++++ tests/hikari/core/test_oauth2.py | 75 +++++-- tests/hikari/core/test_users.py | 44 ++-- tests/hikari/internal_utilities/test_cdn.py | 29 +++ 14 files changed, 895 insertions(+), 132 deletions(-) create mode 100644 hikari/internal_utilities/cdn.py create mode 100644 tests/hikari/core/test_channels.py create mode 100644 tests/hikari/core/test_invites.py create mode 100644 tests/hikari/internal_utilities/test_cdn.py diff --git a/hikari/core/channels.py b/hikari/core/channels.py index b00f497501..6ddd58a58d 100644 --- a/hikari/core/channels.py +++ b/hikari/core/channels.py @@ -16,23 +16,55 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . -__all__ = ["Channel", "DMChannel", "GroupDMChannel"] +__all__ = ["Channel", "ChannelType", "DMChannel", "PartialChannel", "GroupDMChannel"] -import attr +import enum +from hikari.core import entities from hikari.core import snowflakes +from hikari.internal_utilities import marshaller -@attr.s(slots=True) -class Channel(snowflakes.UniqueEntity): +@enum.unique +class ChannelType(enum.IntEnum): + """The known channel types that are exposed to us by the api.""" + + GUILD_TEXT = 0 + DM = 1 + GUILD_VOICE = 2 + GROUP_DM = 3 + GUILD_CATEGORY = 4 + GUILD_NEWS = 5 + GUILD_STORE = 6 + + +@marshaller.attrs(slots=True) +class PartialChannel(snowflakes.UniqueEntity, entities.Deserializable): + """Represents a channel where we've only received it's basic information, + commonly received in rest responses. + """ + + #: This channel's name. + #: + #: :class: :obj:`str` + name: str = marshaller.attrib(deserializer=str) + + #: This channel's type. + #: + #: :class: :obj:`ChannelType` + type: ChannelType = marshaller.attrib(deserializer=ChannelType) + + +@marshaller.attrs(slots=True) +class Channel(PartialChannel): ... -@attr.s(slots=True) +@marshaller.attrs(slots=True) class DMChannel(Channel): ... -@attr.s(slots=True) +@marshaller.attrs(slots=True) class GroupDMChannel(DMChannel): ... diff --git a/hikari/core/guilds.py b/hikari/core/guilds.py index f5034ebf3d..5e5439cdb9 100644 --- a/hikari/core/guilds.py +++ b/hikari/core/guilds.py @@ -39,6 +39,7 @@ "GuildMemberPresence", "GuildIntegration", "GuildMemberBan", + "PartialGuild", ] import datetime @@ -51,6 +52,7 @@ from hikari.core import permissions as permissions_ from hikari.core import snowflakes from hikari.core import users +from hikari.internal_utilities import cdn from hikari.internal_utilities import dates from hikari.internal_utilities import marshaller from hikari.internal_utilities import transformations @@ -240,66 +242,112 @@ class GuildRole(snowflakes.UniqueEntity, entities.Deserializable): @marshaller.attrs(slots=True) -class Guild(snowflakes.UniqueEntity, entities.Deserializable): - """A representation of a guild on Discord. - - Note - ---- - If a guild object is considered to be unavailable, then the state of any - other fields other than the :attr:`is_unavailable` and :attr:`id` members - may be ``None``, outdated, or incorrect. If a guild is unavailable, then - the contents of any other fields should be ignored. +class PartialGuild(snowflakes.UniqueEntity, entities.Deserializable): + """This is a base object for any partial guild objects returned by the api + where we are only given limited information. """ #: The name of the guild. #: - #: :type: :obj`str` + #: :type: :obj:`str` name: str = marshaller.attrib(deserializer=str) #: The hash for the guild icon, if there is one. #: - #: :type: :obj`str`, optional + #: :type: :obj:`str`, optional icon_hash: typing.Optional[str] = marshaller.attrib(raw_name="icon", deserializer=str, if_none=None) + #: A set of the features in this guild. + #: + #: :type: :obj:`typing.Set` [ :obj:`GuildFeature` ] + features: typing.Set[GuildFeature] = marshaller.attrib( + deserializer=lambda features: {transformations.try_cast(f, GuildFeature, f) for f in features}, + ) + + def format_icon_url(self, fmt: typing.Optional[str] = None, size: int = 2048) -> typing.Optional[str]: + """"Generate the url for this guild's custom icon, if set. + + Parameters + ---------- + fmt : :obj:`str` + The format to use for this url, defaults to ``png`` or ``gif``. + Supports ``png``, ``jpeg``, `jpg`, ``webp`` and ``gif`` (when + animated). + size : :obj:`int` + The size to set for the url, defaults to ``2048``. + Can be any power of two between 16 and 2048. + + Returns + ------- + :obj:`str`, optional + The string url. + """ + if self.icon_hash: + # pylint: disable=E1101: + if fmt is None and self.icon_hash.startswith("a_"): + fmt = "gif" + elif fmt is None: + fmt = "png" + return cdn.generate_cdn_url("icons", str(self.id), self.icon_hash, fmt=fmt, size=size) + return None + + @property + def icon_url(self) -> typing.Optional[str]: + """The url for this guild's icon, if set.""" + return self.format_icon_url() + + +@marshaller.attrs(slots=True) +class Guild(PartialGuild): + """A representation of a guild on Discord. + + Note + ---- + If a guild object is considered to be unavailable, then the state of any + other fields other than the :attr:`is_unavailable` and :attr:`id` members + may be ``None``, outdated, or incorrect. If a guild is unavailable, then + the contents of any other fields should be ignored. + """ + #: The hash of the splash for the guild, if there is one. #: - #: :type: :obj`str`, optional + #: :type: :obj:`str`, optional splash_hash: typing.Optional[str] = marshaller.attrib(raw_name="splash", deserializer=str, if_none=None) #: The hash of the discovery splash for the guild, if there is one. #: - #: :type: :obj`str`, optional + #: :type: :obj:`str`, optional discovery_splash_hash: typing.Optional[str] = marshaller.attrib( raw_name="discovery_splash", deserializer=str, if_none=None ) #: The ID of the owner of this guild. #: - #: :type: :obj`snowflakes.Snowflake` + #: :type: :obj:`snowflakes.Snowflake` owner_id: snowflakes.Snowflake = marshaller.attrib(deserializer=snowflakes.Snowflake) #: The guild level permissions that apply to the bot user. #: - #: :type: :obj`hikari.core.permissions.Permission` + #: :type: :obj:`hikari.core.permissions.Permission` my_permissions: permissions_.Permission = marshaller.attrib( raw_name="permissions", deserializer=permissions_.Permission ) #: The voice region for the guild. #: - #: :type: :obj`str` + #: :type: :obj:`str` region: str = marshaller.attrib(deserializer=str) #: The ID for the channel that AFK voice users get sent to, if set for the #: guild. #: - #: :type: :obj`snowflakes.Snowflake`, optional + #: :type: :obj:`snowflakes.Snowflake`, optional afk_channel_id: typing.Optional[snowflakes.Snowflake] = marshaller.attrib(deserializer=str, if_none=None) #: How long a voice user has to be AFK for before they are classed as being #: AFK and are moved to the AFK channel (:attr:`afk_channel_id`). #: - #: :type: :obj`datetime.timedelta` + #: :type: :obj:`datetime.timedelta` afk_timeout: datetime.timedelta = marshaller.attrib( raw_name="afk_timeout", deserializer=lambda seconds: datetime.timedelta(seconds=seconds) ) @@ -309,7 +357,7 @@ class Guild(snowflakes.UniqueEntity, entities.Deserializable): #: Defines if the guild embed is enabled or not. This information may not #: be present, in which case, it will be ``None`` instead. #: - #: :type: :obj`bool`, optional + #: :type: :obj:`bool`, optional is_embed_enabled: typing.Optional[bool] = marshaller.attrib( raw_name="embed_enabled", if_undefined=lambda: False, deserializer=bool ) @@ -317,33 +365,33 @@ class Guild(snowflakes.UniqueEntity, entities.Deserializable): #: The channel ID that the guild embed will generate an invite to, if #: enabled for this guild. If not enabled, it will be ``None``. #: - #: :type: :obj`snowflakes.Snowflake`, optional + #: :type: :obj:`snowflakes.Snowflake`, optional embed_channel_id: typing.Optional[snowflakes.Snowflake] = marshaller.attrib( deserializer=snowflakes.Snowflake, if_none=None, if_undefined=None ) #: The verification level required for a user to participate in this guild. #: - #: :type: :obj`GuildVerificationLevel` + #: :type: :obj:`GuildVerificationLevel` verification_level: GuildVerificationLevel = marshaller.attrib(deserializer=GuildVerificationLevel) #: The default setting for message notifications in this guild. #: - #: :type: :obj`GuildMessageNotificationsLevel` + #: :type: :obj:`GuildMessageNotificationsLevel` default_message_notifications: GuildMessageNotificationsLevel = marshaller.attrib( deserializer=GuildMessageNotificationsLevel ) #: The setting for the explicit content filter in this guild. #: - #: :type: :obj`GuildExplicitContentFilterLevel` + #: :type: :obj:`GuildExplicitContentFilterLevel` explicit_content_filter: GuildExplicitContentFilterLevel = marshaller.attrib( deserializer=GuildExplicitContentFilterLevel ) #: The roles in this guild, represented as a mapping of ID to role object. #: - #: :type: :obj`typing.Mapping` [ :obj`snowflakes.Snowflake`, :obj`GuildRole` ] + #: :type: :obj:`typing.Mapping` [ :obj:`snowflakes.Snowflake`, :obj:`GuildRole` ] roles: typing.Mapping[snowflakes.Snowflake, GuildRole] = marshaller.attrib( deserializer=lambda roles: {r.id: r for r in map(GuildRole.deserialize, roles)}, ) @@ -351,27 +399,20 @@ class Guild(snowflakes.UniqueEntity, entities.Deserializable): #: The emojis that this guild provides, represented as a mapping of ID to #: emoji object. #: - #: :type: :obj`typing.Mapping` [ :obj`snowflakes.Snowflake`, :obj`GuildEmoji` ] + #: :type: :obj:`typing.Mapping` [ :obj:`snowflakes.Snowflake`, :obj:`GuildEmoji` ] emojis: typing.Mapping[snowflakes.Snowflake, GuildEmoji] = marshaller.attrib( deserializer=lambda emojis: {e.id: e for e in map(GuildEmoji.deserialize, emojis)}, ) - #: A set of the features in this guild. - #: - #: :type: :obj`typing.Set` [ :obj`GuildFeature` ] - features: typing.Set[GuildFeature] = marshaller.attrib( - deserializer=lambda features: {transformations.try_cast(f, GuildFeature, f) for f in features}, - ) - #: The required MFA level for users wishing to participate in this guild. #: - #: :type: :obj`GuildMFALevel` + #: :type: :obj:`GuildMFALevel` mfa_level: GuildMFALevel = marshaller.attrib(deserializer=GuildMFALevel) #: The ID of the application that created this guild, if it was created by #: a bot. If not, this is always ``None``. #: - #: :type: :obj`snowflakes.Snowflake`, optional + #: :type: :obj:`snowflakes.Snowflake`, optional application_id: typing.Optional[snowflakes.Snowflake] = marshaller.attrib( deserializer=snowflakes.Snowflake, if_none=None ) @@ -380,7 +421,7 @@ class Guild(snowflakes.UniqueEntity, entities.Deserializable): #: Describes whether the guild widget is enabled or not. If this information #: is not present, this will be ``None``. #: - #: :type: :obj`bool`, optional + #: :type: :obj:`bool`, optional is_widget_enabled: typing.Optional[bool] = marshaller.attrib( raw_name="widget_enabled", if_undefined=None, deserializer=bool ) @@ -388,7 +429,7 @@ class Guild(snowflakes.UniqueEntity, entities.Deserializable): #: The channel ID that the widget's generated invite will send the user to, #: if enabled. If this information is unavailable, this will be ``None``. #: - #: :type: :obj`snowflakes.Snowflake`, optional + #: :type: :obj:`snowflakes.Snowflake`, optional widget_channel_id: typing.Optional[snowflakes.Snowflake] = marshaller.attrib( if_undefined=None, if_none=None, deserializer=snowflakes.Snowflake ) @@ -396,7 +437,7 @@ class Guild(snowflakes.UniqueEntity, entities.Deserializable): #: The ID of the system channel (where welcome messages and Nitro boost #: messages are sent), or ``None`` if it is not enabled. #: - #: :type: :obj`snowflakes.Snowflake`, optional + #: :type: :obj:`snowflakes.Snowflake`, optional system_channel_id: typing.Optional[snowflakes.Snowflake] = marshaller.attrib( if_none=None, deserializer=snowflakes.Snowflake ) @@ -404,14 +445,14 @@ class Guild(snowflakes.UniqueEntity, entities.Deserializable): #: Flags for the guild system channel to describe which notification #: features are suppressed. #: - #: :type: :obj`GuildSystemChannelFlag` + #: :type: :obj:`GuildSystemChannelFlag` system_channel_flags: GuildSystemChannelFlag = marshaller.attrib(deserializer=GuildSystemChannelFlag) #: The ID of the channel where guilds with the :obj:`GuildFeature.PUBLIC` #: :attr:`features` display rules and guidelines. If the #: :obj:`GuildFeature.PUBLIC` feature is not defined, then this is ``None``. #: - #: :type: :obj`snowflakes.Snowflake`, optional + #: :type: :obj:`snowflakes.Snowflake`, optional rules_channel_id: typing.Optional[snowflakes.Snowflake] = marshaller.attrib( if_none=None, deserializer=snowflakes.Snowflake ) @@ -422,7 +463,7 @@ class Guild(snowflakes.UniqueEntity, entities.Deserializable): #: ``GUILD_CREATE`` event. If the guild is received from any other place, #: this will always be ``None``. #: - #: :type: :obj`datetime.datetime`, optional + #: :type: :obj:`datetime.datetime`, optional joined_at: typing.Optional[datetime.datetime] = marshaller.attrib( raw_name="joined_at", deserializer=dates.parse_iso_8601_ts, ) @@ -436,7 +477,7 @@ class Guild(snowflakes.UniqueEntity, entities.Deserializable): #: The implications of a large guild are that presence information will #: not be sent about members who are offline or invisible. #: - #: :type: :obj`bool`, optional + #: :type: :obj:`bool`, optional is_large: typing.Optional[bool] = marshaller.attrib(raw_name="large", if_undefined=None, deserializer=bool) #: Whether the guild is unavailable or not. @@ -457,7 +498,7 @@ class Guild(snowflakes.UniqueEntity, entities.Deserializable): #: `GUILD_CREATE` event. If the guild is received from any other place, #: this will always be ``None``. #: - #: :type: :obj`int`, optional + #: :type: :obj:`int`, optional member_count: typing.Optional[int] = marshaller.attrib(if_undefined=None, deserializer=int) #: A mapping of ID to the corresponding guild members in this guild. @@ -477,7 +518,7 @@ class Guild(snowflakes.UniqueEntity, entities.Deserializable): #: representation. If you need complete accurate information, you should #: query the members using the appropriate API call instead. #: - #: :type: :obj`typing.Mapping` [ :obj`snowflakes.Snowflake`, :obj`GuildMember` ], optional + #: :type: :obj:`typing.Mapping` [ :obj:`snowflakes.Snowflake`, :obj:`GuildMember` ], optional members: typing.Optional[typing.Mapping[snowflakes.Snowflake, GuildMember]] = marshaller.attrib( deserializer=lambda members: {m.user.id: m for m in map(GuildMember.deserialize, members)}, if_undefined=None, ) @@ -497,7 +538,7 @@ class Guild(snowflakes.UniqueEntity, entities.Deserializable): #: To retrieve a list of channels in any other case, you should make an #: appropriate API call to retrieve this information. #: - #: :type: :obj`typing.Mapping` [ :obj`snowflakes.Snowflake`, :obj`GuildChannel` ], optional + #: :type: :obj:`typing.Mapping` [ :obj:`snowflakes.Snowflake`, :obj:`GuildChannel` ], optional channels: typing.Optional[typing.Mapping[snowflakes.Snowflake, GuildChannel]] = marshaller.attrib( deserializer=lambda guild_channels: {c.id: c for c in map(parse_guild_channel, guild_channels)}, if_undefined=None, @@ -519,7 +560,7 @@ class Guild(snowflakes.UniqueEntity, entities.Deserializable): #: To retrieve a list of presences in any other case, you should make an #: appropriate API call to retrieve this information. #: - #: :type: :obj`typing.Mapping` [ :obj`snowflakes.Snowflake`, :obj`GuildMemberPresence` ], optional + #: :type: :obj:`typing.Mapping` [ :obj:`snowflakes.Snowflake`, :obj:`GuildMemberPresence` ], optional presences: typing.Optional[typing.Mapping[snowflakes.Snowflake, GuildMemberPresence]] = marshaller.attrib( deserializer=lambda presences: {p.user.id: p for p in map(GuildMemberPresence.deserialize, presences)}, if_undefined=None, @@ -528,21 +569,21 @@ class Guild(snowflakes.UniqueEntity, entities.Deserializable): #: The maximum number of presences for the guild. If this is ``None``, then #: the default value is used (currently 5000). #: - #: :type: :obj`int`, optional + #: :type: :obj:`int`, optional max_presences: typing.Optional[int] = marshaller.attrib(if_none=None, if_undefined=None, deserializer=int) #: The maximum number of members allowed in this guild. #: #: This information may not be present, in which case, it will be ``None``. #: - #: :type: :obj`int`, optional + #: :type: :obj:`int`, optional max_members: typing.Optional[int] = marshaller.attrib(if_undefined=None, deserializer=int) #: The vanity URL code for the guild's vanity URL. #: This is only present if :obj:`GuildFeatures.VANITY_URL` is in the #: :attr:`features` for this guild. If not, this will always be ``None``. #: - #: :type: :obj`str`, optional + #: :type: :obj:`str`, optional vanity_url_code: typing.Optional[str] = marshaller.attrib(if_none=None, deserializer=str) #: The guild's description. @@ -551,25 +592,25 @@ class Guild(snowflakes.UniqueEntity, entities.Deserializable): #: Otherwise, this will always be ``None``. For all other purposes, it is #: ``None``. #: - #: :type: :obj`str`, optional + #: :type: :obj:`str`, optional description: typing.Optional[str] = marshaller.attrib(if_none=None, deserializer=str) #: The hash for the guild's banner. #: This is only present if the guild has :obj:`GuildFeatures.BANNER` in the #: :attr:`features` for this guild. For all other purposes, it is ``None``. #: - #: :type: :obj`str`, optional + #: :type: :obj:`str`, optional banner_hash: typing.Optional[str] = marshaller.attrib(raw_name="banner", if_none=None, deserializer=str) #: The premium tier for this guild. #: - #: :type: :obj`GuildPremiumTier` + #: :type: :obj:`GuildPremiumTier` premium_tier: GuildPremiumTier = marshaller.attrib(deserializer=GuildPremiumTier) #: The number of nitro boosts that the server currently has. This #: information may not be present, in which case, it will be ``None``. #: - #: :type: :obj`int`, optional + #: :type: :obj:`int`, optional premium_subscription_count: typing.Optional[int] = marshaller.attrib(if_undefined=None, deserializer=int) #: The preferred locale to use for this guild. @@ -577,7 +618,7 @@ class Guild(snowflakes.UniqueEntity, entities.Deserializable): #: This can only be change if :obj:`GuildFeatures.PUBLIC` is in the #: :attr:`features` for this guild and will otherwise default to ``en-US```. #: - #: :type: :obj`str` + #: :type: :obj:`str` preferred_locale: str = marshaller.attrib(deserializer=str) #: The channel ID of the channel where admins and moderators receive notices @@ -587,7 +628,7 @@ class Guild(snowflakes.UniqueEntity, entities.Deserializable): #: :attr:`features` for this guild. For all other purposes, it should be #: considered to be ``None``. #: - #: :type: :obj`snowflakes.Snowflake`, optional + #: :type: :obj:`snowflakes.Snowflake`, optional public_updates_channel_id: typing.Optional[snowflakes.Snowflake] = marshaller.attrib( if_none=None, deserializer=snowflakes.Snowflake ) diff --git a/hikari/core/invites.py b/hikari/core/invites.py index 8047c612e9..7920a915c2 100644 --- a/hikari/core/invites.py +++ b/hikari/core/invites.py @@ -16,13 +16,221 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . -__all__ = ["Invite"] +"""Components and entities that are used to describe invites on Discord. +""" +__all__ = ["TargetUserType", "VanityUrl", "InviteGuild", "Invite", "InviteWithMetadata"] -import attr +import datetime +import enum +import typing +from hikari.core import channels from hikari.core import entities +from hikari.core import guilds +from hikari.core import users +from hikari.internal_utilities import cdn +from hikari.internal_utilities import dates +from hikari.internal_utilities import marshaller -@attr.s(slots=True) -class Invite(entities.HikariEntity): - ... +@enum.unique +class TargetUserType(enum.IntEnum): + """The reason a invite targets a user.""" + + #: This invite is targeting a "Go Live" stream. + STREAM = 1 + + +@marshaller.attrs(slots=True) +class VanityUrl(entities.HikariEntity, entities.Deserializable): + """A special case invite object, that represents a guild's vanity url.""" + + #: The code for this invite. + #: + #: :class: :obj:`hikari.core.snowflakes.Snowflake` + code: str = marshaller.attrib(deserializer=str) + + #: The amount of times this invite has been used. + #: + #: :class: :obj:`int` + uses: int = marshaller.attrib(deserializer=int) + + +@marshaller.attrs(slots=True) +class InviteGuild(guilds.PartialGuild): + """Represents the partial data of a guild that'll be attached to invites.""" + + #: The hash of the splash for the guild, if there is one. + #: + #: :type: :obj:`str`, optional + splash_hash: typing.Optional[str] = marshaller.attrib(raw_name="splash", deserializer=str, if_none=None) + + #: The hash for the guild's banner. + #: This is only present if the guild has :obj:`GuildFeatures.BANNER` in the + #: :attr:`features` for this guild. For all other purposes, it is ``None``. + #: + #: :type: :obj:`str`, optional + banner_hash: typing.Optional[str] = marshaller.attrib(raw_name="banner", if_none=None, deserializer=str) + + #: The guild's description. + #: + #: This is only present if certain :attr:`features` are set in this guild. + #: Otherwise, this will always be ``None``. For all other purposes, it is + #: ``None``. + #: + #: :type: :obj:`str`, optional + description: typing.Optional[str] = marshaller.attrib(if_none=None, deserializer=str) + + #: The verification level required for a user to participate in this guild. + #: + #: :type: :obj:`GuildVerificationLevel` + verification_level: guilds.GuildVerificationLevel = marshaller.attrib(deserializer=guilds.GuildVerificationLevel) + + #: The vanity URL code for the guild's vanity URL. + #: This is only present if :obj:`GuildFeatures.VANITY_URL` is in the + #: :attr:`features` for this guild. If not, this will always be ``None``. + #: + #: :type: :obj:`str`, optional + vanity_url_code: typing.Optional[str] = marshaller.attrib(if_none=None, deserializer=str) + + def format_splash_url(self, fmt: str = "png", size: int = 2048) -> typing.Optional[str]: + """"Generate the url for this guild's splash, if set. + + Parameters + ---------- + fmt : :obj:`str` + The format to use for this url, defaults to ``png``. + Supports ``png``, ``jpeg``, ``jpg` and ``webp``. + size : :obj:`int` + The size to set for the url, defaults to ``2048``. + Can be any power of two between 16 and 2048. + + Returns + ------- + :obj:`str`, optional + The string url. + """ + if self.splash_hash: + return cdn.generate_cdn_url("splashes", str(self.id), self.splash_hash, fmt=fmt, size=size) + return None + + @property + def splash_url(self) -> typing.Optional[str]: + """The url for this guild's splash, if set.""" + return self.format_splash_url() + + def format_banner_url(self, fmt: str = "png", size: int = 2048) -> typing.Optional[str]: + """"Generate the url for this guild's banner, if set. + + Parameters + ---------- + fmt : :obj:`str` + The format to use for this url, defaults to ``png``. + Supports ``png``, ``jpeg``, ``jpg`` and ``webp``. + size : :obj:`int` + The size to set for the url, defaults to ``2048``. + Can be any power of two between 16 and 2048. + + Returns + ------- + :obj:`str`, optional + The string url. + """ + if self.banner_hash: + return cdn.generate_cdn_url("banners", str(self.id), self.banner_hash, fmt=fmt, size=size) + return None + + @property + def banner_url(self) -> typing.Optional[str]: + """The url for this guild's banner, if set.""" + return self.format_banner_url() + + +@marshaller.attrs(slots=True) +class Invite(entities.HikariEntity, entities.Deserializable): + """Represents an invite that's used to add users to a guild or group dm.""" + + #: The code for this invite. + #: + #: :class: :obj:`hikari.core.snowflakes.Snowflake` + code: str = marshaller.attrib(deserializer=str) + + #: The partial object of the guild this dm belongs to. + #: Will be ``None`` for group dm invites. + #: + #: :class: :obj:`InviteGuild`, optional + guild: typing.Optional[InviteGuild] = marshaller.attrib(deserializer=InviteGuild.deserialize, if_undefined=None) + #: The partial object of the channel this invite targets. + #: + #: :class: :obj:`hikari.core.channels.PartialChannel` + channel: channels.PartialChannel = marshaller.attrib(deserializer=channels.PartialChannel.deserialize) + + #: The object of the user who created this invite. + #: + #: :class: :obj:`hikari.core.users.User`, optional + inviter: typing.Optional[users.User] = marshaller.attrib(deserializer=users.User.deserialize, if_undefined=None) + + #: The object of the user who this invite targets, if set. + #: + #: :class: :obj:`hikari.core.users.User`, optional + target_user: typing.Optional[users.User] = marshaller.attrib(deserializer=users.User.deserialize, if_undefined=None) + + #: The type of user target this invite is, if applicable. + #: + #: :class: :obj:`TargetUserType`, optional + target_user_type: typing.Optional[TargetUserType] = marshaller.attrib( + deserializer=TargetUserType, if_undefined=None + ) + + #: The approximate amount of presences in this invite's guild, only present + #: when ``with_counts`` is passed as ``True`` to the GET invites endpoint. + #: + #: :class: :obj:`int` + approximate_presence_count: typing.Optional[int] = marshaller.attrib(deserializer=int, if_undefined=None) + + #: The approximate amount of members in this invite's guild, only present + #: when ``with_counts`` is passed as ``True`` to the GET invites endpoint. + #: + #: :class: :obj:`int` + approximate_member_count: typing.Optional[int] = marshaller.attrib(deserializer=int, if_undefined=None) + + +@marshaller.attrs(slots=True) +class InviteWithMetadata(Invite): + """Extends the base ``Invite`` object with metadata that's only returned + when getting an invite with guild permissions, rather than it's code. + """ + + #: The amount of times this invite has been used. + #: + #: :class: :obj:`int` + uses: int = marshaller.attrib(deserializer=int) + + #: The limit for how many times this invite can be used before it expires. + #: If set to ``0`` then this is unlimited. + #: + #: :class: :obj:`int` + max_uses: int = marshaller.attrib(deserializer=int) + + #: The amount of time (in seconds) this invite will be valid for. + #: If set to ``0`` then this is unlimited. + #: + #: :class: :obj:`int` + max_age: int = marshaller.attrib(deserializer=int) + + #: Whether this invite grants temporary membership. + #: + #: :class: :obj:`bool` + is_temporary: bool = marshaller.attrib(raw_name="temporary", deserializer=bool) + + #: When this invite was created. + #: + #: :class: :obj:`datetime.datetime` + created_at: datetime.datetime = marshaller.attrib(deserializer=dates.parse_iso_8601_ts) + + @property + def expires_at(self) -> typing.Optional[datetime.datetime]: + """The :obj:`datetime` of when this invite should expire, if ``max_age`` is set.""" + if self.max_age: + return self.created_at + datetime.timedelta(seconds=self.max_age) + return None diff --git a/hikari/core/oauth2.py b/hikari/core/oauth2.py index ef98d16f5e..3cc33c5673 100644 --- a/hikari/core/oauth2.py +++ b/hikari/core/oauth2.py @@ -17,18 +17,39 @@ # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . """Components and entities related to discord's Oauth2 flow.""" -__all__ = ["Application", "Owner", "Team", "TeamMember", "TeamMembershipState"] +__all__ = ["Application", "Owner", "OwnGuild", "Team", "TeamMember", "TeamMembershipState"] import enum import typing -import urllib from hikari.core import entities +from hikari.core import guilds +from hikari.core import permissions from hikari.core import snowflakes from hikari.core import users +from hikari.internal_utilities import cdn from hikari.internal_utilities import marshaller +@marshaller.attrs(slots=True) +class OwnGuild(guilds.PartialGuild): + """Represents a user bound partial guild object, + returned by GET Current User Guilds. + """ + + #: Whether the current user owns this guild. + #: + #: :class: :obj:`bool` + is_owner: bool = marshaller.attrib(raw_name="owner", deserializer=bool) + + #: The guild level permissions that apply to the current user or bot. + #: + #: :type: :obj:`hikari.core.permissions.Permission` + my_permissions: permissions.Permission = marshaller.attrib( + raw_name="permissions", deserializer=permissions.Permission + ) + + @enum.unique class TeamMembershipState(enum.IntEnum): """Represents the state of a user's team membership.""" @@ -109,14 +130,11 @@ def format_icon_url(self, fmt: str = "png", size: int = 2048) -> typing.Optional Returns ------- - :obj`str`, optional + :obj:`str`, optional The string url. """ if self.icon_hash: - return ( - f"https://cdn.discordapp.com/team-icons/{self.id}/{self.icon_hash}." - f"{urllib.parse.quote_plus(fmt)}?{urllib.parse.urlencode({'size': size})}" - ) + return cdn.generate_cdn_url("team-icons", str(self.id), self.icon_hash, fmt=fmt, size=size) return None @@ -247,14 +265,11 @@ def format_icon_url(self, fmt: str = "png", size: int = 2048) -> typing.Optional Returns ------- - :obj`str`, optional + :obj:`str`, optional The string url. """ if self.icon_hash: - return ( - f"https://cdn.discordapp.com/app-icons/{self.id}/{self.icon_hash}." - f"{urllib.parse.quote_plus(fmt)}?{urllib.parse.urlencode({'size': size})}" - ) + return cdn.generate_cdn_url("app-icons", str(self.id), self.icon_hash, fmt=fmt, size=size) return None @property @@ -277,12 +292,9 @@ def format_cover_image_url(self, fmt: str = "png", size: int = 2048) -> typing.O Returns ------- - :obj`str`, optional + :obj:`str`, optional The string url. """ if self.cover_image_hash: - return ( - f"https://cdn.discordapp.com/app-assets/{self.id}/{self.cover_image_hash}" - f".{urllib.parse.quote_plus(fmt)}?{urllib.parse.urlencode({'size': size})}" - ) + return cdn.generate_cdn_url("app-assets", str(self.id), self.cover_image_hash, fmt=fmt, size=size) return None diff --git a/hikari/core/users.py b/hikari/core/users.py index 1f2b1d4571..b1c1c5e73a 100644 --- a/hikari/core/users.py +++ b/hikari/core/users.py @@ -20,11 +20,11 @@ __all__ = ["User", "MyUser", "UserFlag"] import enum -import urllib import typing from hikari.core import entities from hikari.core import snowflakes +from hikari.internal_utilities import cdn from hikari.internal_utilities import marshaller @@ -69,9 +69,10 @@ def format_avatar_url(self, fmt: typing.Optional[str] = None, size: int = 2048) Parameters ---------- fmt : :obj:`str` - The format to use for this url, defaults to ``png``. - Supports ``png``, ``jpeg``, ``webp`` and ``gif`` (when animated). - Will be ignored for default avatars which can only be ``png``. + The format to use for this url, defaults to ``png`` or ``gif``. + Supports ``png``, ``jpeg``, ``jpg``, ``webp`` and ``gif`` (when + animated). Will be ignored for default avatars which can only be + ``png``. size : :obj:`int` The size to set for the url, defaults to ``2048``. Can be any power of two between 16 and 2048. @@ -84,20 +85,17 @@ def format_avatar_url(self, fmt: typing.Optional[str] = None, size: int = 2048) """ if not self.avatar_hash: - return f"https://cdn.discordapp.com/embed/avatars/{self.default_avatar}.png" + return cdn.generate_cdn_url("embed/avatars", str(self.default_avatar), fmt="png", size=None) # pylint: disable=E1101: if fmt is None and self.avatar_hash.startswith("a_"): fmt = "gif" elif fmt is None: fmt = "png" - return ( - f"https://cdn.discordapp.com/avatars/{self.id}/{self.avatar_hash}." - f"{urllib.parse.quote_plus(fmt)}?{urllib.parse.urlencode({'size': size})}" - ) + return cdn.generate_cdn_url("avatars", str(self.id), self.avatar_hash, fmt=fmt, size=size) @property def default_avatar(self) -> int: - """Returns the number for this user's default avatar.""" + """The number representation of this user's default avatar.""" return int(self.discriminator) % 5 diff --git a/hikari/internal_utilities/cdn.py b/hikari/internal_utilities/cdn.py new file mode 100644 index 0000000000..b302d6448a --- /dev/null +++ b/hikari/internal_utilities/cdn.py @@ -0,0 +1,54 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# Copyright © Nekokatt 2019-2020 +# +# This file is part of Hikari. +# +# Hikari is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Hikari is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Hikari. If not, see . +"""Basic utilities for handling the cdn.""" +__all__ = [ + "generate_cdn_url", +] + +import typing +import urllib + + +BASE_CDN_URL = "https://cdn.discordapp.com" + + +def generate_cdn_url(*route_parts: str, fmt: str, size: typing.Optional[int]) -> str: + """Generate a link for a cdn entry. + + Parameters + ---------- + route_parts : :obj:`str` + The string route parts that will be used to form the link. + fmt : :obj:`str` + The format to use for the wanted cdn entity, will usually be one of + ``webp``, ``png``, ``jpeg``, ``jpg`` or ``gif`` (which will be invalid + if the target entity doesn't have an animated version available). + size : :obj:`typing.Union` [ :obj:`int`, :obj:`None` ] + The size to specify for the image in the query string if applicable, + should be passed through as ``None`` to avoid the param being set. + + Returns + ------- + :obj:`str` + The formed cdn url. + """ + path = "/".join(urllib.parse.unquote(part) for part in route_parts) + url = urllib.parse.urljoin(BASE_CDN_URL, "/" + path) + "." + str(fmt) + query = urllib.parse.urlencode({"size": size}) if size is not None else None + return f"{url}?{query}" if query else url diff --git a/hikari/net/http_client.py b/hikari/net/http_client.py index 4acaa4716a..8e8e00494e 100644 --- a/hikari/net/http_client.py +++ b/hikari/net/http_client.py @@ -921,7 +921,7 @@ async def edit_channel_permissions( If specified, the bitwise value of all permissions to set to be allowed. deny : :obj:`int` If specified, the bitwise value of all permissions to set to be denied. - type_ : :obj:`str`] + type_ : :obj:`str` If specified, the type of overwrite. ``"member"`` if it is for a member, or ``"role"`` if it is for a role. reason : :obj:`str` diff --git a/tests/hikari/_helpers.py b/tests/hikari/_helpers.py index cd9b4ae800..cc32a0e956 100644 --- a/tests/hikari/_helpers.py +++ b/tests/hikari/_helpers.py @@ -453,7 +453,10 @@ def patch_marshal_attr(target_entity, field_name, *args, deserializer=None, **kw target = attr break elif attr.field_name == field_name: - raise TypeError(f"Deserializer mismatch found on `{target_entity.__name__}.{attr.field_name}`") + raise TypeError( + f"Deserializer mismatch found on `{target_entity.__name__}.{attr.field_name}`; " + f"expected `{deserializer}` but got `{attr.deserializer}`." + ) else: raise LookupError(f"Failed to find a `{field_name}` field on `{target_entity.__name__}`.") return mock.patch.object(target, "deserializer", *args, **kwargs) diff --git a/tests/hikari/core/test_channels.py b/tests/hikari/core/test_channels.py new file mode 100644 index 0000000000..fac04af283 --- /dev/null +++ b/tests/hikari/core/test_channels.py @@ -0,0 +1,34 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# Copyright © Nekoka.tt 2019-2020 +# +# This file is part of Hikari. +# +# Hikari is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Hikari is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along ith Hikari. If not, see . +import pytest + +from hikari.core import channels + + +@pytest.fixture() +def test_partial_channel_payload(): + return {"id": "561884984214814750", "name": "general", "type": 0} + + +class TestPartialChannel: + def test_deserialize(self, test_partial_channel_payload): + partial_channel_obj = channels.PartialChannel.deserialize(test_partial_channel_payload) + assert partial_channel_obj.id == 561884984214814750 + assert partial_channel_obj.name == "general" + assert partial_channel_obj.type is channels.ChannelType.GUILD_TEXT diff --git a/tests/hikari/core/test_guilds.py b/tests/hikari/core/test_guilds.py index 25d804239f..b867870921 100644 --- a/tests/hikari/core/test_guilds.py +++ b/tests/hikari/core/test_guilds.py @@ -16,12 +16,12 @@ # # You should have received a copy of the GNU Lesser General Public License # along ith Hikari. If not, see . -from __future__ import annotations - -from timeit import timeit - +import cymock as mock import pytest +from hikari.core import guilds +from hikari.internal_utilities import cdn + @pytest.fixture def test_emoji_payload(): @@ -142,6 +142,16 @@ def test_voice_state_payload(): } +@pytest.fixture() +def test_partial_guild_payload(): + return { + "id": "152559372126519269", + "name": "Isopropyl", + "icon": "d4a983885dsaa7691ce8bcaaf945a", + "features": ["DISCOVERABLE"], + } + + @pytest.fixture def test_guild_payload( test_emoji_payload, test_roles_payloads, test_channel_payloads, test_member_payload, test_voice_state_payload @@ -182,3 +192,58 @@ def test_guild_payload( "system_channel_flags": 3, "rules_channel_id": "42042069", } + + +class TestPartialGuild: + @pytest.fixture() + def partial_guild_obj(self, test_partial_guild_payload): + return guilds.PartialGuild.deserialize(test_partial_guild_payload) + + def test_deserialize(self, partial_guild_obj): + assert partial_guild_obj.id == 152559372126519269 + assert partial_guild_obj.name == "Isopropyl" + assert partial_guild_obj.icon_hash == "d4a983885dsaa7691ce8bcaaf945a" + assert partial_guild_obj.features == {guilds.GuildFeature.DISCOVERABLE} + + def test_format_icon_url(self, partial_guild_obj): + mock_url = "https://cdn.discordapp.com/icons/152559372126519269/d4a983885dsaa7691ce8bcaaf945a.png?size=20" + with mock.patch.object(cdn, "generate_cdn_url", return_value=mock_url): + url = partial_guild_obj.format_icon_url(fmt="nyaapeg", size=42) + cdn.generate_cdn_url.assert_called_once_with( + "icons", "152559372126519269", "d4a983885dsaa7691ce8bcaaf945a", fmt="nyaapeg", size=42 + ) + assert url == mock_url + + def test_format_icon_url_animated_default(self, partial_guild_obj): + partial_guild_obj.icon_hash = "a_d4a983885dsaa7691ce8bcaaf945a" + mock_url = "https://cdn.discordapp.com/icons/152559372126519269/a_d4a983885dsaa7691ce8bcaaf945a.gif?size=20" + with mock.patch.object(cdn, "generate_cdn_url", return_value=mock_url): + url = partial_guild_obj.format_icon_url() + cdn.generate_cdn_url.assert_called_once_with( + "icons", "152559372126519269", "a_d4a983885dsaa7691ce8bcaaf945a", fmt="gif", size=2048 + ) + assert url == mock_url + + def test_format_icon_url_none_animated_default(self, partial_guild_obj): + partial_guild_obj.icon_hash = "d4a983885dsaa7691ce8bcaaf945a" + mock_url = "https://cdn.discordapp.com/icons/152559372126519269/d4a983885dsaa7691ce8bcaaf945a.png?size=20" + with mock.patch.object(cdn, "generate_cdn_url", return_value=mock_url): + url = partial_guild_obj.format_icon_url() + cdn.generate_cdn_url.assert_called_once_with( + "icons", "152559372126519269", "d4a983885dsaa7691ce8bcaaf945a", fmt="png", size=2048 + ) + assert url == mock_url + + def test_format_icon_url_returns_none(self, partial_guild_obj): + partial_guild_obj.icon_hash = None + with mock.patch.object(cdn, "generate_cdn_url", return_value=...): + url = partial_guild_obj.format_icon_url(fmt="nyaapeg", size=42) + cdn.generate_cdn_url.assert_not_called() + assert url is None + + def test_format_icon_url(self, partial_guild_obj): + mock_url = "https://cdn.discordapp.com/icons/152559372126519269/d4a983885dsaa7691ce8bcaaf945a.png?size=20" + with mock.patch.object(cdn, "generate_cdn_url", return_value=mock_url): + url = partial_guild_obj.icon_url + cdn.generate_cdn_url.assert_called_once() + assert url == mock_url diff --git a/tests/hikari/core/test_invites.py b/tests/hikari/core/test_invites.py new file mode 100644 index 0000000000..beede601fe --- /dev/null +++ b/tests/hikari/core/test_invites.py @@ -0,0 +1,236 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# Copyright © Nekoka.tt 2019-2020 +# +# This file is part of Hikari. +# +# Hikari is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Hikari is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along ith Hikari. If not, see . +import datetime + +import cymock as mock +import pytest + +from hikari.core import channels +from hikari.core import guilds +from hikari.core import invites +from hikari.core import users +from hikari.internal_utilities import cdn +from hikari.internal_utilities import dates +from tests.hikari import _helpers + + +@pytest.fixture() +def test_user_payload(): + return {"id": "2020202", "username": "bang", "discriminator": "2222", "avatar": None} + + +@pytest.fixture() +def test_2nd_user_payload(): + return {"id": "1231231", "username": "soad", "discriminator": "3333", "avatar": None} + + +@pytest.fixture() +def test_invite_guild_payload(): + return { + "id": "56188492224814744", + "name": "Testin' Your Scene", + "splash": "aSplashForSure", + "banner": "aBannerForSure", + "description": "Describe me cute kitty.", + "icon": "bb71f469c158984e265093a81b3397fb", + "features": [], + "verification_level": 2, + "vanity_url_code": "I-am-very-vain", + } + + +@pytest.fixture() +def test_partial_channel(): + return {"id": "303030", "name": "channel-time", "type": 3} + + +@pytest.fixture() +def test_invite_payload(test_user_payload, test_2nd_user_payload, test_invite_guild_payload, test_partial_channel): + return { + "code": "aCode", + "guild": test_invite_guild_payload, + "channel": test_partial_channel, + "inviter": test_user_payload, + "target_user": test_2nd_user_payload, + "target_user_type": 1, + "approximate_presence_count": 42, + "approximate_member_count": 84, + } + + +@pytest.fixture() +def test_invite_with_metadata_payload(test_invite_payload): + return { + **test_invite_payload, + "uses": 3, + "max_uses": 8, + "max_age": 239349393, + "temporary": True, + "created_at": "2015-04-26T06:26:56.936000+00:00", + } + + +class TestInviteGuild: + @pytest.fixture() + def invite_guild_obj(self, test_invite_guild_payload): + return invites.InviteGuild.deserialize(test_invite_guild_payload) + + def test_deserialize(self, invite_guild_obj): + assert invite_guild_obj.splash_hash == "aSplashForSure" + assert invite_guild_obj.banner_hash == "aBannerForSure" + assert invite_guild_obj.description == "Describe me cute kitty." + assert invite_guild_obj.verification_level is guilds.GuildVerificationLevel.MEDIUM + assert invite_guild_obj.vanity_url_code == "I-am-very-vain" + + def test_format_splash_url(self, invite_guild_obj): + mock_url = "https://not-al" + with mock.patch.object(cdn, "generate_cdn_url", return_value=mock_url): + url = invite_guild_obj.format_splash_url(fmt="nyaapeg", size=4000) + cdn.generate_cdn_url.assert_called_once_with( + "splashes", "56188492224814744", "aSplashForSure", fmt="nyaapeg", size=4000 + ) + assert url is mock_url + + def test_format_splash_url_returns_none(self, invite_guild_obj): + invite_guild_obj.splash_hash = None + with mock.patch.object(cdn, "generate_cdn_url", return_value=...): + url = invite_guild_obj.format_splash_url() + cdn.generate_cdn_url.assert_not_called() + assert url is None + + def test_splash_url(self, invite_guild_obj): + mock_url = "https://not-al" + with mock.patch.object(cdn, "generate_cdn_url", return_value=mock_url): + url = invite_guild_obj.splash_url + cdn.generate_cdn_url.assert_called_once() + assert url is mock_url + + def test_format_banner_url(self, invite_guild_obj): + mock_url = "https://not-al" + with mock.patch.object(cdn, "generate_cdn_url", return_value=mock_url): + url = invite_guild_obj.format_banner_url(fmt="nyaapeg", size=4000) + cdn.generate_cdn_url.assert_called_once_with( + "banners", "56188492224814744", "aBannerForSure", fmt="nyaapeg", size=4000 + ) + assert url is mock_url + + def test_format_banner_url_returns_none(self, invite_guild_obj): + invite_guild_obj.banner_hash = None + with mock.patch.object(cdn, "generate_cdn_url", return_value=...): + url = invite_guild_obj.format_banner_url() + cdn.generate_cdn_url.assert_not_called() + assert url is None + + def test_banner_url(self, invite_guild_obj): + mock_url = "https://not-al" + with mock.patch.object(cdn, "generate_cdn_url", return_value=mock_url): + url = invite_guild_obj.banner_url + cdn.generate_cdn_url.assert_called_once() + assert url is mock_url + + +class TestVanityUrl: + @pytest.fixture() + def vanity_url_payload(self): + return {"code": "iamacode", "uses": 42} + + def test_deserialize(self, vanity_url_payload): + vanity_url_obj = invites.VanityUrl.deserialize(vanity_url_payload) + assert vanity_url_obj.code == "iamacode" + assert vanity_url_obj.uses == 42 + + +class TestInvite: + def test_deserialize( + self, + test_invite_payload, + test_user_payload, + test_2nd_user_payload, + test_partial_channel, + test_invite_guild_payload, + ): + mock_guild = mock.MagicMock(invites.InviteGuild) + mock_channel = mock.MagicMock(channels.PartialChannel) + mock_user_1 = mock.MagicMock(users.User) + mock_user_2 = mock.MagicMock(users.User) + with _helpers.patch_marshal_attr( + invites.Invite, "guild", deserializer=invites.InviteGuild.deserialize, return_value=mock_guild + ) as mock_guld_deseralize: + with _helpers.patch_marshal_attr( + invites.Invite, "channel", deserializer=channels.PartialChannel.deserialize, return_value=mock_channel + ) as mock_channel_deseralize: + with _helpers.patch_marshal_attr( + invites.Invite, "inviter", deserializer=users.User.deserialize, return_value=mock_user_1 + ) as mock_inviter_deseralize: + with _helpers.patch_marshal_attr( + invites.Invite, "target_user", deserializer=users.User.deserialize, return_value=mock_user_2 + ) as mock_target_user_deseralize: + invite_obj = invites.Invite.deserialize(test_invite_payload) + mock_target_user_deseralize.assert_called_once_with(test_2nd_user_payload) + mock_inviter_deseralize.assert_called_once_with(test_user_payload) + mock_channel_deseralize.assert_called_once_with(test_partial_channel) + mock_guld_deseralize.assert_called_once_with(test_invite_guild_payload) + assert invite_obj.code == "aCode" + assert invite_obj.guild is mock_guild + assert invite_obj.channel is mock_channel + assert invite_obj.inviter is mock_user_1 + assert invite_obj.target_user is mock_user_2 + assert invite_obj.target_user_type is invites.TargetUserType.STREAM + assert invite_obj.approximate_member_count == 84 + assert invite_obj.approximate_presence_count == 42 + + +class TestInviteWithMetadata: + @pytest.fixture() + @_helpers.patch_marshal_attr(invites.InviteWithMetadata, "guild", deserializer=invites.InviteGuild.deserialize) + @_helpers.patch_marshal_attr( + invites.InviteWithMetadata, "channel", deserializer=channels.PartialChannel.deserialize + ) + @_helpers.patch_marshal_attr(invites.InviteWithMetadata, "inviter", deserializer=users.User.deserialize) + @_helpers.patch_marshal_attr(invites.InviteWithMetadata, "target_user", deserializer=users.User.deserialize) + def mock_invite_with_metadata(self, *args, test_invite_with_metadata_payload): + return invites.InviteWithMetadata.deserialize(test_invite_with_metadata_payload) + + @_helpers.patch_marshal_attr(invites.InviteWithMetadata, "guild", deserializer=invites.InviteGuild.deserialize) + @_helpers.patch_marshal_attr( + invites.InviteWithMetadata, "channel", deserializer=channels.PartialChannel.deserialize + ) + @_helpers.patch_marshal_attr(invites.InviteWithMetadata, "inviter", deserializer=users.User.deserialize) + @_helpers.patch_marshal_attr(invites.InviteWithMetadata, "target_user", deserializer=users.User.deserialize) + def test_deserialize(self, *deserializers, test_invite_with_metadata_payload): + mock_datetime = mock.MagicMock(datetime.datetime) + with _helpers.patch_marshal_attr( + invites.InviteWithMetadata, "created_at", deserializers=dates.parse_iso_8601_ts, return_value=mock_datetime + ) as mock_created_at_deserializer: + invite_with_metadata_obj = invites.InviteWithMetadata.deserialize(test_invite_with_metadata_payload) + mock_created_at_deserializer.assert_called_once_with("2015-04-26T06:26:56.936000+00:00") + assert invite_with_metadata_obj.uses == 3 + assert invite_with_metadata_obj.max_uses == 8 + assert invite_with_metadata_obj.max_age == 239349393 + assert invite_with_metadata_obj.is_temporary is True + assert invite_with_metadata_obj.created_at is mock_datetime + + def test_expires_at(self, mock_invite_with_metadata): + assert mock_invite_with_metadata.expires_at == datetime.datetime.fromisoformat( + "2022-11-25 12:23:29.936000+00:00" + ) + + def test_expires_at_returns_None(self, mock_invite_with_metadata): + mock_invite_with_metadata.max_age = None + assert mock_invite_with_metadata.expires_at is None diff --git a/tests/hikari/core/test_oauth2.py b/tests/hikari/core/test_oauth2.py index 000984465f..d46c201b0c 100644 --- a/tests/hikari/core/test_oauth2.py +++ b/tests/hikari/core/test_oauth2.py @@ -16,16 +16,27 @@ # # You should have received a copy of the GNU Lesser General Public License # along ith Hikari. If not, see . -from __future__ import annotations - import cymock as mock import pytest from hikari.core import oauth2 from hikari.core import users +from hikari.internal_utilities import cdn from tests.hikari import _helpers +@pytest.fixture() +def own_guild_payload(): + return { + "id": "152559372126519269", + "name": "Isopropyl", + "icon": "d4a983885dsaa7691ce8bcaaf945a", + "owner": False, + "permissions": 2147483647, + "features": ["DISCOVERABLE"], + } + + @pytest.fixture() def owner_payload(): return {"username": "agent 47", "avatar": "hashed", "discriminator": "4747", "id": "474747474", "flags": 1 << 10} @@ -67,6 +78,13 @@ def application_information_payload(owner_payload, team_payload): } +class TestOwnGuild: + def test_deserialize(self, own_guild_payload): + own_guild_obj = oauth2.OwnGuild.deserialize(own_guild_payload) + assert own_guild_obj.is_owner is False + assert own_guild_obj.my_permissions == 2147483647 + + class TestOwner: @pytest.fixture() def owner_obj(self, owner_payload): @@ -117,17 +135,25 @@ def test_deserialize(self, team_payload, member_payload): def test_format_icon_url(self): mock_team = _helpers.create_autospec(oauth2.Team, icon_hash="3o2o32o", id=22323) - url = oauth2.Team.format_icon_url(mock_team, fmt="jpg", size=64) - assert url == "https://cdn.discordapp.com/team-icons/22323/3o2o32o.jpg?size=64" + mock_url = "https://cdn.discordapp.com/team-icons/22323/3o2o32o.jpg?size=64" + with mock.patch.object(cdn, "generate_cdn_url", return_value=mock_url): + url = oauth2.Team.format_icon_url(mock_team, fmt="jpg", size=64) + cdn.generate_cdn_url.assert_called_once_with("team-icons", "22323", "3o2o32o", fmt="jpg", size=64) + assert url == mock_url def test_format_icon_url_returns_none(self): mock_team = _helpers.create_autospec(oauth2.Team, icon_hash=None, id=22323) - url = oauth2.Team.format_icon_url(mock_team, fmt="jpg", size=64) + with mock.patch.object(cdn, "generate_cdn_url", return_value=...): + url = oauth2.Team.format_icon_url(mock_team, fmt="jpg", size=64) + cdn.generate_cdn_url.assert_not_called() assert url is None def test_icon_url(self, team_obj): - url = team_obj.icon_url - assert url == "https://cdn.discordapp.com/team-icons/202020202/hashtag.png?size=2048" + mock_url = "https://cdn.discordapp.com/team-icons/202020202/hashtag.png?size=2048" + with mock.patch.object(cdn, "generate_cdn_url", return_value=mock_url): + url = team_obj.icon_url + cdn.generate_cdn_url.assert_called_once() + assert url == mock_url class TestApplication: @@ -168,28 +194,45 @@ def mock_application(self): return _helpers.create_autospec(oauth2.Application, id=22222) def test_icon_url(self, application_obj): - assert application_obj.icon_url == "https://cdn.discordapp.com/app-icons/209333111222/iwiwiwiwiw.png?size=2048" + mock_url = "https://cdn.discordapp.com/app-icons/209333111222/iwiwiwiwiw.png?size=2048" + with mock.patch.object(cdn, "generate_cdn_url", return_value=mock_url): + url = application_obj.icon_url + cdn.generate_cdn_url.assert_called_once() + assert url == "https://cdn.discordapp.com/app-icons/209333111222/iwiwiwiwiw.png?size=2048" def test_format_icon_url(self, mock_application): mock_application.icon_hash = "wosososoos" - url = oauth2.Application.format_icon_url(mock_application, fmt="jpg", size=4) - assert url == "https://cdn.discordapp.com/app-icons/22222/wosososoos.jpg?size=4" + mock_url = "https://cdn.discordapp.com/app-icons/22222/wosososoos.jpg?size=4" + with mock.patch.object(cdn, "generate_cdn_url", return_value=mock_url): + url = oauth2.Application.format_icon_url(mock_application, fmt="jpg", size=4) + cdn.generate_cdn_url.assert_called_once_with("app-icons", "22222", "wosososoos", fmt="jpg", size=4) + assert url == mock_url def test_format_icon_url_returns_none(self, mock_application): mock_application.icon_hash = None - url = oauth2.Application.format_icon_url(mock_application, fmt="jpg", size=4) + with mock.patch.object(cdn, "generate_cdn_url", return_value=...): + url = oauth2.Application.format_icon_url(mock_application, fmt="jpg", size=4) + cdn.generate_cdn_url.assert_not_called() assert url is None def test_cover_image_url(self, application_obj): - url = application_obj.cover_image_url - assert url == "https://cdn.discordapp.com/app-assets/209333111222/hashmebaby.png?size=2048" + mock_url = "https://cdn.discordapp.com/app-assets/209333111222/hashmebaby.png?size=2048" + with mock.patch.object(cdn, "generate_cdn_url", return_value=mock_url): + url = application_obj.cover_image_url + cdn.generate_cdn_url.assert_called_once() + assert url == mock_url def test_format_cover_image_url(self, mock_application): mock_application.cover_image_hash = "wowowowowo" - url = oauth2.Application.format_cover_image_url(mock_application, fmt="jpg", size=42) - assert url == "https://cdn.discordapp.com/app-assets/22222/wowowowowo.jpg?size=42" + mock_url = "https://cdn.discordapp.com/app-assets/22222/wowowowowo.jpg?size=42" + with mock.patch.object(cdn, "generate_cdn_url", return_value=mock_url): + url = oauth2.Application.format_cover_image_url(mock_application, fmt="jpg", size=42) + cdn.generate_cdn_url.assert_called_once_with("app-assets", "22222", "wowowowowo", fmt="jpg", size=42) + assert url == mock_url def test_format_cover_image_url_returns_none(self, mock_application): mock_application.cover_image_hash = None - url = oauth2.Application.format_cover_image_url(mock_application, fmt="jpg", size=42) + with mock.patch.object(cdn, "generate_cdn_url", return_value=...): + url = oauth2.Application.format_cover_image_url(mock_application, fmt="jpg", size=42) + cdn.generate_cdn_url.assert_not_called() assert url is None diff --git a/tests/hikari/core/test_users.py b/tests/hikari/core/test_users.py index 6ab1103d16..78bf79cade 100644 --- a/tests/hikari/core/test_users.py +++ b/tests/hikari/core/test_users.py @@ -16,11 +16,11 @@ # # You should have received a copy of the GNU Lesser General Public License # along ith Hikari. If not, see . -from __future__ import annotations - +import cymock as mock import pytest from hikari.core import users +from hikari.internal_utilities import cdn @pytest.fixture() @@ -61,33 +61,41 @@ def test_deserialize(self, user_obj): assert user_obj.discriminator == "6127" def test_avatar_url(self, user_obj): - url = user_obj.avatar_url - assert ( - url == "https://cdn.discordapp.com/avatars/115590097100865541" - "/b3b24c6d7cbcdec129d5d537067061a8.png?size=2048" - ) + mock_url = "https://cdn.discordapp.com/avatars/115590097100865541" + with mock.patch.object(cdn, "generate_cdn_url", return_value=mock_url): + url = user_obj.avatar_url + cdn.generate_cdn_url.assert_called_once() + assert url == mock_url def test_default_avatar(self, user_obj): assert user_obj.default_avatar == 2 def test_format_avatar_url_when_animated(self, user_obj): + mock_url = "https://cdn.discordapp.com/avatars/115590097100865541/a_820d0e50543216e812ad94e6ab7.gif?size=3232" user_obj.avatar_hash = "a_820d0e50543216e812ad94e6ab7" - url = user_obj.format_avatar_url(size=3232) - assert ( - url == "https://cdn.discordapp.com/avatars/115590097100865541/a_820d0e50543216e812ad94e6ab7.gif?size=3232" - ) + with mock.patch.object(cdn, "generate_cdn_url", return_value=mock_url): + url = user_obj.format_avatar_url(size=3232) + cdn.generate_cdn_url.assert_called_once_with( + "avatars", "115590097100865541", "a_820d0e50543216e812ad94e6ab7", fmt="gif", size=3232 + ) + assert url == mock_url def test_format_avatar_url_default(self, user_obj): user_obj.avatar_hash = None - url = user_obj.format_avatar_url(size=3232) - assert url == "https://cdn.discordapp.com/embed/avatars/2.png" + mock_url = "https://cdn.discordapp.com/embed/avatars/2.png" + with mock.patch.object(cdn, "generate_cdn_url", return_value=mock_url): + url = user_obj.format_avatar_url(size=3232) + cdn.generate_cdn_url("embed/avatars", "115590097100865541", fmt="png", size=None) + assert url == mock_url def test_format_avatar_url_when_format_specified(self, user_obj): - url = user_obj.format_avatar_url(fmt="nyaapeg", size=1024) - assert ( - url == "https://cdn.discordapp.com/avatars/115590097100865541" - "/b3b24c6d7cbcdec129d5d537067061a8.nyaapeg?size=1024" - ) + mock_url = "https://cdn.discordapp.com/avatars/115590097100865541/b3b24c6d7c37067061a8.nyaapeg?size=1024" + with mock.patch.object(cdn, "generate_cdn_url", return_value=mock_url): + url = user_obj.format_avatar_url(fmt="nyaapeg", size=1024) + cdn.generate_cdn_url.assert_called_once_with( + "avatars", "115590097100865541", "b3b24c6d7cbcdec129d5d537067061a8", fmt="nyaapeg", size=1024 + ) + assert url == mock_url class TestMyUser: diff --git a/tests/hikari/internal_utilities/test_cdn.py b/tests/hikari/internal_utilities/test_cdn.py new file mode 100644 index 0000000000..22334a9702 --- /dev/null +++ b/tests/hikari/internal_utilities/test_cdn.py @@ -0,0 +1,29 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# Copyright © Nekoka.tt 2019-2020 +# +# This file is part of Hikari. +# +# Hikari is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Hikari is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along ith Hikari. If not, see . +from hikari.internal_utilities import cdn + + +def test_generate_cdn_url(): + url = cdn.generate_cdn_url("not", "a", "path", fmt="neko", size=42) + assert url == "https://cdn.discordapp.com/not/a/path.neko?size=42" + + +def test_generate_cdn_url_with_size_set_to_none(): + url = cdn.generate_cdn_url("not", "a", "path", fmt="neko", size=None) + assert url == "https://cdn.discordapp.com/not/a/path.neko" From 9ba02453de91faa67a3463771fd3b0073b85b4db Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Fri, 20 Mar 2020 13:57:34 +0000 Subject: [PATCH 019/922] Implemented color datatype. --- hikari/core/colors.py | 320 ++++++++++++++++++ hikari/core/colours.py | 24 ++ hikari/internal_utilities/assertions.py | 8 + tests/hikari/core/test_colors.py | 186 ++++++++++ tests/hikari/core/test_colours.py | 27 ++ .../internal_utilities/test_assertions.py | 32 ++ 6 files changed, 597 insertions(+) create mode 100644 hikari/core/colors.py create mode 100644 hikari/core/colours.py create mode 100644 tests/hikari/core/test_colors.py create mode 100644 tests/hikari/core/test_colours.py diff --git a/hikari/core/colors.py b/hikari/core/colors.py new file mode 100644 index 0000000000..5cce323668 --- /dev/null +++ b/hikari/core/colors.py @@ -0,0 +1,320 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# Copyright © Nekokatt 2019-2020 +# +# This file is part of Hikari. +# +# Hikari is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Hikari is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Hikari. If not, see . +""" +Model that represents a common RGB color and provides simple conversions to other common color systems. +""" +__all__ = ["Color", "ColorCompatibleT"] + +import string +import typing + +from hikari.internal_utilities import assertions + + +class Color(int, typing.SupportsInt): + """ + Representation of a color. This value is immutable. + + This is a specialization of :class:`int` which provides alternative overrides for common methods and color system + conversions. + + This currently supports: + + - RGB + - RGB (float) + - 3-digit hex codes (e.g. 0xF1A -- web safe) + - 6-digit hex codes (e.g. 0xFF11AA) + - 3-digit RGB strings (e.g. #1A2 -- web safe) + - 6-digit RGB hash strings (e.g. #1A2B3C) + + Examples of conversions to given formats include + + >>> c = Color(0xFF051A) + Color(r=0xff, g=0x5, b=0x1a) + >>> hex(c) + 0xff051a + >>> c.hex_code + #FF051A + >>> str(c) + #FF051A + >>> int(c) + 16712986 + >>> c.rgb + (255, 5, 26) + >>> c.rgb_float + (1.0, 0.0196078431372549, 0.10196078431372549) + + Alternatively, if you have an arbitrary input in one of the above formats that you wish to become a color, you can + use the get-attribute operator on the class itself to automatically attempt to resolve the color + + >>> Color[0xFF051A] + Color(r=0xff, g=0x5, b=0x1a) + >>> Color[16712986] + Color(r=0xff, g=0x5, b=0x1a) + >>> c = Color[(255, 5, 26)] + Color(r=0xff, g=0x5, b=1xa) + >>> c = Color[[0xFF, 0x5, 0x1a]] + Color(r=0xff, g=0x5, b=1xa) + >>> c = Color["#1a2b3c"] + Color(r=0x1a, g=0x2b, b=0x3c) + >>> c = Color["#1AB"] + Color(r=0x11, g=0xaa, b=0xbb) + >>> c = Color[(1.0, 0.0196078431372549, 0.10196078431372549)] + Color(r=0xff, g=0x5, b=0x1a) + >>> c = Color[[1.0, 0.0196078431372549, 0.10196078431372549]] + Color(r=0xff, g=0x5, b=0x1a) + + Examples of initialization of Color objects from given formats include + + >>> c = Color(16712986) + Color(r=0xff, g=0x5, b=0x1a) + >>> c = Color.from_rgb(255, 5, 26) + Color(r=0xff, g=0x5, b=1xa) + >>> c = Color.from_hex_code("#1a2b3c") + Color(r=0x1a, g=0x2b, b=0x3c) + >>> c = Color.from_hex_code("#1AB") + Color(r=0x11, g=0xaa, b=0xbb) + >>> c = Color.from_rgb_float(1.0, 0.0196078431372549, 0.10196078431372549) + Color(r=0xff, g=0x5, b=0x1a) + """ + + __slots__ = () + + def __new__(cls: typing.Type["Color"], raw_rgb: typing.SupportsInt) -> "Color": + assertions.assert_in_range(raw_rgb, 0, 0xFF_FF_FF, "integer value") + return super(Color, cls).__new__(cls, raw_rgb) + + def __repr__(self) -> str: + r, g, b = self.rgb + return f"Color(r={hex(r)}, g={hex(g)}, b={hex(b)})" + + def __str__(self) -> str: + return self.hex_code + + def __int__(self): + # Binary-OR to make raw int. + return self | 0 + + @property + def rgb(self) -> typing.Tuple[int, int, int]: + """ + The RGB representation of this Color. Represented a tuple of R, G, B. Each value is in the range [0, 0xFF]. + """ + return (self >> 16) & 0xFF, (self >> 8) & 0xFF, self & 0xFF + + @property + def rgb_float(self) -> typing.Tuple[float, float, float]: + """ + Return the floating-point RGB representation of this Color. Represented as a tuple of R, G, B. Each value is in + the range [0, 1]. + """ + r, g, b = self.rgb + return r / 0xFF, g / 0xFF, b / 0xFF + + @property + def hex_code(self) -> str: + """ + The six-digit hexadecimal color code for this Color. This is prepended with a `#` symbol, and will be + in upper case. + + Example: + #1A2B3C + """ + return "#" + self.raw_hex_code + + @property + def raw_hex_code(self) -> str: + """ + The raw hex code. + + Example: + 1A2B3C + """ + components = self.rgb + return "".join(hex(c)[2:].zfill(2) for c in components).upper() + + @property + def is_web_safe(self) -> bool: + """ + True if this color is a web-safe color, False otherwise. + """ + hex_code = self.raw_hex_code + return all(_all_same(*c) for c in (hex_code[:2], hex_code[2:4], hex_code[4:])) + + @classmethod + def from_rgb(cls: typing.Type["Color"], red: int, green: int, blue: int) -> "Color": + """ + Convert the given RGB colorspace represented in values within the range [0, 255]: [0x0, 0xFF], to a Color + object. + + Args: + red: + Red channel. + green: + Green channel. + blue: + Blue channel. + + Returns: + A Color object. + + Raises: + ValueError: if red, green, or blue are outside the range [0x0, 0xFF] + """ + assertions.assert_in_range(red, 0, 0xFF, "red") + assertions.assert_in_range(green, 0, 0xFF, "green") + assertions.assert_in_range(blue, 0, 0xFF, "blue") + # noinspection PyTypeChecker + return cls((red << 16) | (green << 8) | blue) + + @classmethod + def from_rgb_float(cls: typing.Type["Color"], red_f: float, green_f: float, blue_f: float) -> "Color": + """ + Convert the given RGB colorspace represented using floats in the range [0, 1] to a Color object. + + Args: + red_f: + Red channel. + green_f: + Green channel. + blue_f: + Blue channel. + + Returns: + A Color object. + + Raises: + ValueError: if red, green or blue are outside the range [0, 1] + """ + assertions.assert_in_range(red_f, 0, 1, "red") + assertions.assert_in_range(green_f, 0, 1, "green") + assertions.assert_in_range(blue_f, 0, 1, "blue") + # noinspection PyTypeChecker + return cls.from_rgb(int(red_f * 0xFF), int(green_f * 0xFF), int(blue_f * 0xFF)) + + @classmethod + def from_hex_code(cls: typing.Type["Color"], hex_code: str) -> "Color": + """ + Consumes a string hexadecimal color code and produces a Color. + + The inputs may be of the following format (case insensitive): + `1a2`, `#1a2`, `0x1a2` (for websafe colors), or + `1a2b3c`, `#1a2b3c` `0x1a2b3c` (for regular 3-byte color-codes). + + Args: + hex_code: + A hexadecimal color code to parse. + + Returns: + A corresponding Color object. + + Raises: + Value error if the Color + """ + if hex_code.startswith("#"): + hex_code = hex_code[1:] + elif hex_code.startswith(("0x", "0X")): + hex_code = hex_code[2:] + + if not all(c in string.hexdigits for c in hex_code): + raise ValueError("Color code must be hexadecimal") + + if len(hex_code) == 3: + # Web-safe + components = (int(c, 16) for c in hex_code) + # noinspection PyTypeChecker + return cls.from_rgb(*[(c << 4 | c) for c in components]) + + if len(hex_code) == 6: + return cls.from_rgb(int(hex_code[:2], 16), int(hex_code[2:4], 16), int(hex_code[4:6], 16)) + + raise ValueError("Color code is invalid length. Must be 3 or 6 digits") + + @classmethod + def from_int(cls: typing.Type["Color"], i: typing.SupportsInt) -> "Color": + """ + Create a color from a raw integer that Discord can understand. + + Args: + i: + The raw color integer. + + Returns: + The Color object. + """ + return cls(i) + + # Partially chose to override these as the docstrings contain typos according to Sphinx. + @classmethod + def from_bytes(cls, bytes_: typing.Sequence[int], byteorder: str, *, signed: bool = True) -> "Color": + """Converts the color from bytes.""" + return Color(super().from_bytes(bytes_, byteorder, signed=signed)) + + def to_bytes(self, length: int, byteorder: str, *, signed: bool = True) -> bytes: + """Converts the color code to bytes.""" + return super().to_bytes(length, byteorder, signed=signed) + + @classmethod + def __class_getitem__(cls, color: "ColorCompatibleT") -> "Color": + if isinstance(color, cls): + return color + elif isinstance(color, int): + return cls.from_int(color) + elif isinstance(color, (list, tuple)): + assertions.assert_that( + len(color) == 3, f"color must be an RGB triplet if set to a {type(color).__name__} type" + ) + + if _all_same(*map(type, color)): + first = color[0] + if isinstance(first, float): + return cls.from_rgb_float(*color) + if isinstance(first, int): + return cls.from_rgb(*color) + + raise ValueError( + "all three channels must be all int or all float types if setting the color to an RGB triplet" + ) + if isinstance(color, str): + is_start_hash_or_hex_literal = color.casefold().startswith(("#", "0x")) + is_hex_digits = all(c in string.hexdigits for c in color) and len(color) in (3, 6) + if is_start_hash_or_hex_literal or is_hex_digits: + return cls.from_hex_code(color) + + raise ValueError(f"Could not transform {color!r} into a {cls.__qualname__} object") + + +def _all_same(first, *rest): + for r in rest: + if r != first: + return False + + return True + + +#: Any type that can be converted into a color object. +ColorCompatibleT = typing.Union[ + Color, + typing.SupportsInt, + typing.Tuple[typing.SupportsInt, typing.SupportsInt, typing.SupportsInt], + typing.Tuple[typing.SupportsFloat, typing.SupportsFloat, typing.SupportsFloat], + typing.Sequence[typing.SupportsInt], + typing.Sequence[typing.SupportsFloat], + str, +] diff --git a/hikari/core/colours.py b/hikari/core/colours.py new file mode 100644 index 0000000000..8de2122cf6 --- /dev/null +++ b/hikari/core/colours.py @@ -0,0 +1,24 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# Copyright © Nekokatt 2019-2020 +# +# This file is part of Hikari. +# +# Hikari is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Hikari is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Hikari. If not, see . +""" +Alias for the :mod:`hikari.orm.models.colors` module. +""" +__all__ = ["Colour"] + +from hikari.core.colors import Color as Colour diff --git a/hikari/internal_utilities/assertions.py b/hikari/internal_utilities/assertions.py index 745e5feedd..8772070a0b 100644 --- a/hikari/internal_utilities/assertions.py +++ b/hikari/internal_utilities/assertions.py @@ -23,6 +23,7 @@ __all__ = [ "assert_that", "assert_not_none", + "assert_in_range", ] import typing @@ -42,3 +43,10 @@ def assert_not_none(value: ValueT, message: typing.Optional[str] = None) -> Valu if value is None: raise ValueError(message or "value must not be None") return value + + +def assert_in_range(value, min_inclusive, max_inclusive, name: str = None): + """Raise a value error if a value is not in the range [min, max]""" + if not (min_inclusive <= value <= max_inclusive): + name = name or "The value" + raise ValueError(f"{name} must be in the inclusive range of {min_inclusive} and {max_inclusive}") diff --git a/tests/hikari/core/test_colors.py b/tests/hikari/core/test_colors.py new file mode 100644 index 0000000000..3c36ac1236 --- /dev/null +++ b/tests/hikari/core/test_colors.py @@ -0,0 +1,186 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# Copyright © Nekoka.tt 2019-2020 +# +# This file is part of Hikari. +# +# Hikari is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Hikari is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Hikari. If not, see . +import math + +import pytest + +from hikari.core import colors + + +@pytest.mark.model +class TestColor: + @pytest.mark.parametrize("i", [0, 0x1, 0x11, 0x111, 0x1111, 0x11111, 0xFFFFFF]) + def test_Color_validates_constructor_and_passes_for_valid_values(self, i): + assert colors.Color(i) is not None + + @pytest.mark.parametrize("i", [-1, 0x1000000]) + def test_Color_validates_constructor_and_fails_for_out_of_range_values(self, i): + try: + colors.Color(i) + assert False, "Failed to fail validation for bad values" + except ValueError: + pass + + @pytest.mark.parametrize("i", [0, 0x1, 0x11, 0x111, 0x1111, 0x11111, 0xFFFFFF]) + def test_Color_from_int_passes_for_valid_values(self, i): + assert colors.Color.from_int(i) is not None + + @pytest.mark.parametrize("i", [-1, 0x1000000]) + def test_Color_from_int_fails_for_out_of_range_values(self, i): + try: + colors.Color.from_int(i) + assert False, "Failed to fail validation for bad values" + except ValueError: + pass + + def test_equality_with_int(self): + assert colors.Color(0xFA) == 0xFA + + def test_cast_to_int(self): + assert int(colors.Color(0xFA)) == 0xFA + + @pytest.mark.parametrize( + ["i", "string"], [(0x1A2B3C, "Color(r=0x1a, g=0x2b, b=0x3c)"), (0x1A2, "Color(r=0x0, g=0x1, b=0xa2)")] + ) + def test_Color_repr_operator(self, i, string): + assert repr(colors.Color(i)) == string + + @pytest.mark.parametrize(["i", "string"], [(0x1A2B3C, "#1A2B3C"), (0x1A2, "#0001A2")]) + def test_Color_str_operator(self, i, string): + assert str(colors.Color(i)) == string + + @pytest.mark.parametrize(["i", "string"], [(0x1A2B3C, "#1A2B3C"), (0x1A2, "#0001A2")]) + def test_Color_hex_code(self, i, string): + assert colors.Color(i).hex_code == string + + @pytest.mark.parametrize(["i", "string"], [(0x1A2B3C, "1A2B3C"), (0x1A2, "0001A2")]) + def test_Color_raw_hex_code(self, i, string): + assert colors.Color(i).raw_hex_code == string + + @pytest.mark.parametrize( + ["i", "expected_outcome"], [(0x1A2B3C, False), (0x1AAA2B, False), (0x0, True), (0x11AA33, True)] + ) + def test_Color_is_web_safe(self, i, expected_outcome): + assert colors.Color(i).is_web_safe is expected_outcome + + @pytest.mark.parametrize(["r", "g", "b", "expected"], [(0x9, 0x18, 0x27, 0x91827), (0x55, 0x1A, 0xFF, 0x551AFF)]) + def test_Color_from_rgb(self, r, g, b, expected): + assert colors.Color.from_rgb(r, g, b) == expected + + @pytest.mark.parametrize( + ["r", "g", "b", "expected"], + [(0x09 / 0xFF, 0x18 / 0xFF, 0x27 / 0xFF, 0x91827), (0x55 / 0xFF, 0x1A / 0xFF, 0xFF / 0xFF, 0x551AFF)], + ) + def test_Color_from_rgb_float(self, r, g, b, expected): + assert math.isclose(colors.Color.from_rgb_float(r, g, b), expected, abs_tol=1) + + @pytest.mark.parametrize(["input", "r", "g", "b"], [(0x91827, 0x9, 0x18, 0x27), (0x551AFF, 0x55, 0x1A, 0xFF)]) + def test_Color_rgb(self, input, r, g, b): + assert colors.Color(input).rgb == (r, g, b) + + @pytest.mark.parametrize( + ["input", "r", "g", "b"], + [(0x91827, 0x09 / 0xFF, 0x18 / 0xFF, 0x27 / 0xFF), (0x551AFF, 0x55 / 0xFF, 0x1A / 0xFF, 0xFF / 0xFF)], + ) + def test_Color_rgb_float(self, input, r, g, b): + assert colors.Color(input).rgb_float == (r, g, b) + + @pytest.mark.parametrize("prefix", ["0x", "0X", "#", ""]) + @pytest.mark.parametrize( + ["expected", "string"], [(0x1A2B3C, "1A2B3C"), (0x1A2, "0001A2"), (0xAABBCC, "ABC"), (0x00AA00, "0A0")] + ) + def test_Color_from_hex_code(self, prefix, string, expected): + actual_string = prefix + string + assert colors.Color.from_hex_code(actual_string) == expected + + def test_Color_from_hex_code_ValueError_when_not_hex(self): + try: + colors.Color.from_hex_code("0xlmfao") + assert False, "No failure" + except ValueError: + pass + + def test_Color_from_hex_code_ValueError_when_not_6_or_3_in_size(self): + try: + colors.Color.from_hex_code("0x1111") + assert False, "No failure" + except ValueError: + pass + + def test_Color_from_bytes(self): + assert colors.Color(0xFFAAFF) == colors.Color.from_bytes(b"\xff\xaa\xff\x00\x00\x00\x00\x00\x00\x00", "little") + + def test_Color_to_bytes(self): + c = colors.Color(0xFFAAFF) + b = c.to_bytes(10, "little") + assert b == b"\xff\xaa\xff\x00\x00\x00\x00\x00\x00\x00" + + @pytest.mark.parametrize( + ["input", "expected_result"], + [ + (0xFF051A, colors.Color(0xFF051A)), + (16712986, colors.Color(0xFF051A)), + ((255, 5, 26), colors.Color(0xFF051A)), + ([0xFF, 0x5, 0x1A], colors.Color(0xFF051A)), + ("#1a2b3c", colors.Color(0x1A2B3C)), + ("#123", colors.Color(0x112233)), + ("0x1a2b3c", colors.Color(0x1A2B3C)), + ("0x123", colors.Color(0x112233)), + ("0X1a2b3c", colors.Color(0x1A2B3C)), + ("0X123", colors.Color(0x112233)), + ("1a2b3c", colors.Color(0x1A2B3C)), + ("123", colors.Color(0x112233)), + ((1.0, 0.0196078431372549, 0.10196078431372549), colors.Color(0xFF051A)), + ([1.0, 0.0196078431372549, 0.10196078431372549], colors.Color(0xFF051A)), + ], + ) + def test_Color__cls_getattr___happy_path(self, input, expected_result): + result = colors.Color[input] + assert result == expected_result, f"{input}" + result.__repr__() + + @pytest.mark.parametrize( + "input", + [ + "blah", + "0xfff1", + lambda: 22, + NotImplementedError, + NotImplemented, + (1, 1, 1, 1), + (1, "a", 1), + (1, 1.1, 1), + (), + {}, + [], + {1, 1, 1}, + set(), + b"1ff1ff", + ], + ) + def test_Color__cls_getattr___sad_path(self, input): + try: + result = colors.Color[input] + assert False, f"Expected ValueError, got {result} returned safely instead" + except ValueError: + pass + + @pytest.mark.model + def test_Color___repr__(self): + assert repr(colors.Color["#1a2b3c"]) == "Color(r=0x1a, g=0x2b, b=0x3c)" diff --git a/tests/hikari/core/test_colours.py b/tests/hikari/core/test_colours.py new file mode 100644 index 0000000000..496581e8c7 --- /dev/null +++ b/tests/hikari/core/test_colours.py @@ -0,0 +1,27 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# Copyright © Nekoka.tt 2019-2020 +# +# This file is part of Hikari. +# +# Hikari is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Hikari is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Hikari. If not, see . +import pytest + +from hikari.core import colors +from hikari.core import colours + + +@pytest.mark.model +def test_colours(): + assert colors.Color is colours.Colour diff --git a/tests/hikari/internal_utilities/test_assertions.py b/tests/hikari/internal_utilities/test_assertions.py index 0a113dc079..5c281851c8 100644 --- a/tests/hikari/internal_utilities/test_assertions.py +++ b/tests/hikari/internal_utilities/test_assertions.py @@ -41,3 +41,35 @@ def test_assert_not_none_when_none(): @_helpers.assert_does_not_raise(type_=ValueError) def test_assert_not_none_when_not_none(arg): assertions.assert_not_none(arg) + + +@pytest.mark.parametrize( + ["min_r", "max_r", "test"], + [ + (0, 10, 5), + (0, 10, 0), + (0, 10, 10), + (0, 0, 0), + (0.0, 10.0, 5.0), + (0.0, 10.0, 0.0), + (0.0, 10.0, 10.0), + (0.0, 0.0, 0.0), + (float("-inf"), 10, 10), + (10, float("inf"), 10), + ], +) +def test_in_range_when_in_range(min_r, max_r, test): + try: + assertions.assert_in_range(test, min_r, max_r, "blah") + except ValueError: + assert False, "should not have failed." + + +@pytest.mark.parametrize(["min_r", "max_r", "test"], [(0, 0, -1), (0, 10, 11), (10, 0, 5),]) +def test_in_range_when_not_in_range(min_r, max_r, test): + try: + assertions.assert_in_range(test, min_r, max_r, "blah") + except ValueError: + pass + else: + assert False, "should have failed." From 1859ed59d0323cceaf1c0d264b10da19be7e7f22 Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Sat, 21 Mar 2020 15:53:50 +0000 Subject: [PATCH 020/922] Started gateway client --- hikari/core/clients/__init__.py | 18 ++++++ hikari/core/clients/gateway.py | 74 +++++++++++++++++++++++++ hikari/internal_utilities/marshaller.py | 5 +- 3 files changed, 95 insertions(+), 2 deletions(-) create mode 100644 hikari/core/clients/__init__.py create mode 100644 hikari/core/clients/gateway.py diff --git a/hikari/core/clients/__init__.py b/hikari/core/clients/__init__.py new file mode 100644 index 0000000000..1c1502a5ca --- /dev/null +++ b/hikari/core/clients/__init__.py @@ -0,0 +1,18 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# Copyright © Nekoka.tt 2019-2020 +# +# This file is part of Hikari. +# +# Hikari is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Hikari is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Hikari. If not, see . diff --git a/hikari/core/clients/gateway.py b/hikari/core/clients/gateway.py new file mode 100644 index 0000000000..f2e20fd457 --- /dev/null +++ b/hikari/core/clients/gateway.py @@ -0,0 +1,74 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# Copyright © Nekoka.tt 2019-2020 +# +# This file is part of Hikari. +# +# Hikari is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Hikari is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Hikari. If not, see . +import typing + +from hikari.internal_utilities import marshaller + + +@marshaller.attrs() +class GatewayActivity: + #: The activity name. + #: + #: :type: :class:`str` + name: str = marshaller.attrib(deserializer=str, serializer=str) + + #: The activity url. Only valid for ``STREAMING`` activities. + #: + #: :type: :class:`str`, optional + url: typing.Optional[str] = marshaller.attrib(deserializer=str, serializer=str, if_none=None, if_undefined=None) + + # TODO: implement enum for this. + #: The activity type. + #: + #: :type: :class:`int` + type: int = marshaller.attrib(deserializer=int, serializer=int, if_undefined=0) + + +@marshaller.attrs() +class GatewayConfig: + #: Whether to enable debugging mode for the generated shards. Usually you + #: don't want to enable this. + #: + #: :type: :class:`bool` + debug: bool = marshaller.attrib(deserializer=bool, if_undefined=False) + + #: The initial activity to set all shards to when starting the gateway. If + #: ``None``, then no activity will be set. + #: + #: :type: :class:`GatewayActivity` + initial_activity: typing.Optional[GatewayActivity] = marshaller.attrib( + deserializer=bool, if_none=None, if_undefined=None + ) + + # TODO: implement enum for this + #: The initial status to set the shards to when starting the gateway. + #: + #: :type: :class:`str` + initial_status: str = marshaller.attrib(deserializer=str, if_undefined="online") + + #: Whether to use zlib compression on the gateway for inbound messages or + #: not. Usually you want this turned on. + #: + #: :type: :class:`bool` + use_compression: bool = marshaller.attrib(deserializer=bool, if_undefined=True) + + +class Gateway: + def __init__(self, gateway_config: GatewayConfig): + pass diff --git a/hikari/internal_utilities/marshaller.py b/hikari/internal_utilities/marshaller.py index d1cf572439..de293a4c66 100644 --- a/hikari/internal_utilities/marshaller.py +++ b/hikari/internal_utilities/marshaller.py @@ -95,14 +95,15 @@ class that this attribute is in will trigger a :class:`TypeError` The result of :func:`attr.ib` internally being called with additional metadata. """ - metadata = kwargs.setdefault("metadata", {}) + metadata = kwargs.pop("metadata", {}) metadata[_RAW_NAME_ATTR] = raw_name metadata[_SERIALIZER_ATTR] = serializer metadata[_DESERIALIZER_ATTR] = deserializer metadata[_IF_NONE] = if_none metadata[_IF_UNDEFINED] = if_undefined metadata[_TRANSIENT_ATTR] = transient - return attr.ib(**kwargs) + + return attr.ib(**kwargs, metadata=metadata) def _no_serialize(name): From ee2ea068c2c031daeb728b2dd3d167a75ef64621 Mon Sep 17 00:00:00 2001 From: davfsa Date: Sat, 21 Mar 2020 21:05:46 +0100 Subject: [PATCH 021/922] Docstring fixes, added enum.unique, add missing __all__ and consistency --- hikari/core/channels.py | 4 +- hikari/core/clients/gateway.py | 14 +- hikari/core/colors.py | 268 +++++++++++++----------- hikari/core/gateway_bot.py | 2 +- hikari/core/guilds.py | 25 ++- hikari/core/invites.py | 36 ++-- hikari/core/oauth2.py | 42 ++-- hikari/core/snowflakes.py | 4 +- hikari/core/users.py | 4 +- hikari/internal_utilities/marshaller.py | 24 +-- 10 files changed, 219 insertions(+), 204 deletions(-) diff --git a/hikari/core/channels.py b/hikari/core/channels.py index 6ddd58a58d..2aece2cc39 100644 --- a/hikari/core/channels.py +++ b/hikari/core/channels.py @@ -46,12 +46,12 @@ class PartialChannel(snowflakes.UniqueEntity, entities.Deserializable): #: This channel's name. #: - #: :class: :obj:`str` + #: :type: :obj:`str` name: str = marshaller.attrib(deserializer=str) #: This channel's type. #: - #: :class: :obj:`ChannelType` + #: :type: :obj:`ChannelType` type: ChannelType = marshaller.attrib(deserializer=ChannelType) diff --git a/hikari/core/clients/gateway.py b/hikari/core/clients/gateway.py index f2e20fd457..75fa068437 100644 --- a/hikari/core/clients/gateway.py +++ b/hikari/core/clients/gateway.py @@ -25,18 +25,18 @@ class GatewayActivity: #: The activity name. #: - #: :type: :class:`str` + #: :type: :obj:`str` name: str = marshaller.attrib(deserializer=str, serializer=str) #: The activity url. Only valid for ``STREAMING`` activities. #: - #: :type: :class:`str`, optional + #: :type: :obj:`str`, optional url: typing.Optional[str] = marshaller.attrib(deserializer=str, serializer=str, if_none=None, if_undefined=None) # TODO: implement enum for this. #: The activity type. #: - #: :type: :class:`int` + #: :type: :obj:`int` type: int = marshaller.attrib(deserializer=int, serializer=int, if_undefined=0) @@ -45,13 +45,13 @@ class GatewayConfig: #: Whether to enable debugging mode for the generated shards. Usually you #: don't want to enable this. #: - #: :type: :class:`bool` + #: :type: :obj:`bool` debug: bool = marshaller.attrib(deserializer=bool, if_undefined=False) #: The initial activity to set all shards to when starting the gateway. If #: ``None``, then no activity will be set. #: - #: :type: :class:`GatewayActivity` + #: :type: :obj:`GatewayActivity` initial_activity: typing.Optional[GatewayActivity] = marshaller.attrib( deserializer=bool, if_none=None, if_undefined=None ) @@ -59,13 +59,13 @@ class GatewayConfig: # TODO: implement enum for this #: The initial status to set the shards to when starting the gateway. #: - #: :type: :class:`str` + #: :type: :obj:`str` initial_status: str = marshaller.attrib(deserializer=str, if_undefined="online") #: Whether to use zlib compression on the gateway for inbound messages or #: not. Usually you want this turned on. #: - #: :type: :class:`bool` + #: :type: :obj:`bool` use_compression: bool = marshaller.attrib(deserializer=bool, if_undefined=True) diff --git a/hikari/core/colors.py b/hikari/core/colors.py index 5cce323668..d1e5a1a118 100644 --- a/hikari/core/colors.py +++ b/hikari/core/colors.py @@ -28,75 +28,81 @@ class Color(int, typing.SupportsInt): - """ - Representation of a color. This value is immutable. + """Representation of a color. + + This value is immutable. - This is a specialization of :class:`int` which provides alternative overrides for common methods and color system + This is a specialization of :obj:`int` which provides alternative overrides for common methods and color system conversions. This currently supports: - - RGB - - RGB (float) - - 3-digit hex codes (e.g. 0xF1A -- web safe) - - 6-digit hex codes (e.g. 0xFF11AA) - - 3-digit RGB strings (e.g. #1A2 -- web safe) - - 6-digit RGB hash strings (e.g. #1A2B3C) - - Examples of conversions to given formats include - - >>> c = Color(0xFF051A) - Color(r=0xff, g=0x5, b=0x1a) - >>> hex(c) - 0xff051a - >>> c.hex_code - #FF051A - >>> str(c) - #FF051A - >>> int(c) - 16712986 - >>> c.rgb - (255, 5, 26) - >>> c.rgb_float - (1.0, 0.0196078431372549, 0.10196078431372549) + * RGB + * RGB (float) + * 3-digit hex codes (e.g. 0xF1A -- web safe) + * 6-digit hex codes (e.g. 0xFF11AA) + * 3-digit RGB strings (e.g. #1A2 -- web safe) + * 6-digit RGB hash strings (e.g. #1A2B3C) + + Examples + -------- + Examples of conversions to given formats include: + .. code-block::python + + >>> c = Color(0xFF051A) + Color(r=0xff, g=0x5, b=0x1a) + >>> hex(c) + 0xff051a + >>> c.hex_code + #FF051A + >>> str(c) + #FF051A + >>> int(c) + 16712986 + >>> c.rgb + (255, 5, 26) + >>> c.rgb_float + (1.0, 0.0196078431372549, 0.10196078431372549) Alternatively, if you have an arbitrary input in one of the above formats that you wish to become a color, you can - use the get-attribute operator on the class itself to automatically attempt to resolve the color - - >>> Color[0xFF051A] - Color(r=0xff, g=0x5, b=0x1a) - >>> Color[16712986] - Color(r=0xff, g=0x5, b=0x1a) - >>> c = Color[(255, 5, 26)] - Color(r=0xff, g=0x5, b=1xa) - >>> c = Color[[0xFF, 0x5, 0x1a]] - Color(r=0xff, g=0x5, b=1xa) - >>> c = Color["#1a2b3c"] - Color(r=0x1a, g=0x2b, b=0x3c) - >>> c = Color["#1AB"] - Color(r=0x11, g=0xaa, b=0xbb) - >>> c = Color[(1.0, 0.0196078431372549, 0.10196078431372549)] - Color(r=0xff, g=0x5, b=0x1a) - >>> c = Color[[1.0, 0.0196078431372549, 0.10196078431372549]] - Color(r=0xff, g=0x5, b=0x1a) - - Examples of initialization of Color objects from given formats include - - >>> c = Color(16712986) - Color(r=0xff, g=0x5, b=0x1a) - >>> c = Color.from_rgb(255, 5, 26) - Color(r=0xff, g=0x5, b=1xa) - >>> c = Color.from_hex_code("#1a2b3c") - Color(r=0x1a, g=0x2b, b=0x3c) - >>> c = Color.from_hex_code("#1AB") - Color(r=0x11, g=0xaa, b=0xbb) - >>> c = Color.from_rgb_float(1.0, 0.0196078431372549, 0.10196078431372549) - Color(r=0xff, g=0x5, b=0x1a) + use the get-attribute operator on the class itself to automatically attempt to resolve the color: + .. code-block::python + + >>> Color[0xFF051A] + Color(r=0xff, g=0x5, b=0x1a) + >>> Color[16712986] + Color(r=0xff, g=0x5, b=0x1a) + >>> c = Color[(255, 5, 26)] + Color(r=0xff, g=0x5, b=1xa) + >>> c = Color[[0xFF, 0x5, 0x1a]] + Color(r=0xff, g=0x5, b=1xa) + >>> c = Color["#1a2b3c"] + Color(r=0x1a, g=0x2b, b=0x3c) + >>> c = Color["#1AB"] + Color(r=0x11, g=0xaa, b=0xbb) + >>> c = Color[(1.0, 0.0196078431372549, 0.10196078431372549)] + Color(r=0xff, g=0x5, b=0x1a) + >>> c = Color[[1.0, 0.0196078431372549, 0.10196078431372549]] + Color(r=0xff, g=0x5, b=0x1a) + + Examples of initialization of Color objects from given formats include: + .. code-block::python + + >>> c = Color(16712986) + Color(r=0xff, g=0x5, b=0x1a) + >>> c = Color.from_rgb(255, 5, 26) + Color(r=0xff, g=0x5, b=1xa) + >>> c = Color.from_hex_code("#1a2b3c") + Color(r=0x1a, g=0x2b, b=0x3c) + >>> c = Color.from_hex_code("#1AB") + Color(r=0x11, g=0xaa, b=0xbb) + >>> c = Color.from_rgb_float(1.0, 0.0196078431372549, 0.10196078431372549) + Color(r=0xff, g=0x5, b=0x1a) """ __slots__ = () - def __new__(cls: typing.Type["Color"], raw_rgb: typing.SupportsInt) -> "Color": + def __new__(cls, raw_rgb: typing.SupportsInt) -> "Color": assertions.assert_in_range(raw_rgb, 0, 0xFF_FF_FF, "integer value") return super(Color, cls).__new__(cls, raw_rgb) @@ -113,69 +119,68 @@ def __int__(self): @property def rgb(self) -> typing.Tuple[int, int, int]: - """ - The RGB representation of this Color. Represented a tuple of R, G, B. Each value is in the range [0, 0xFF]. - """ + """The RGB representation of this Color. Represented a tuple of R, G, B. Each value is in the + range [0, 0xFF].""" return (self >> 16) & 0xFF, (self >> 8) & 0xFF, self & 0xFF @property def rgb_float(self) -> typing.Tuple[float, float, float]: - """ - Return the floating-point RGB representation of this Color. Represented as a tuple of R, G, B. Each value is in - the range [0, 1]. - """ + """Return the floating-point RGB representation of this Color. Represented as a tuple of R, G, B. + Each value is in the range [0, 1].""" r, g, b = self.rgb return r / 0xFF, g / 0xFF, b / 0xFF @property def hex_code(self) -> str: - """ - The six-digit hexadecimal color code for this Color. This is prepended with a `#` symbol, and will be - in upper case. + """The six-digit hexadecimal color code for this Color. This is prepended with a ``#`` symbol, + and will bein upper case. - Example: - #1A2B3C + Example + ------- + ``#1A2B3C`` """ return "#" + self.raw_hex_code @property def raw_hex_code(self) -> str: - """ - The raw hex code. + """The raw hex code. - Example: - 1A2B3C + Example + ------- + ``1A2B3C`` """ components = self.rgb return "".join(hex(c)[2:].zfill(2) for c in components).upper() @property def is_web_safe(self) -> bool: - """ - True if this color is a web-safe color, False otherwise. - """ + """``True`` if this color is a web-safe color, ``False`` otherwise.""" hex_code = self.raw_hex_code return all(_all_same(*c) for c in (hex_code[:2], hex_code[2:4], hex_code[4:])) @classmethod - def from_rgb(cls: typing.Type["Color"], red: int, green: int, blue: int) -> "Color": - """ - Convert the given RGB colorspace represented in values within the range [0, 255]: [0x0, 0xFF], to a Color - object. - - Args: - red: - Red channel. - green: - Green channel. - blue: - Blue channel. - - Returns: + def from_rgb(cls, red: int, green: int, blue: int) -> "Color": + """Convert the given RGB colorspace represented in values within the range [0, 255]: [0x0, 0xFF], + to a :obj:`Color` object. + + Parameters + ---------- + red : :obj:`int` + Red channel. + green : :obj:`int` + Green channel. + blue : :obj:`int` + Blue channel. + + Returns + ------- + :obj:`Color` A Color object. - Raises: - ValueError: if red, green, or blue are outside the range [0x0, 0xFF] + Raises + ------ + :obj:`ValueError` + If red, green, or blue are outside the range [0x0, 0xFF]. """ assertions.assert_in_range(red, 0, 0xFF, "red") assertions.assert_in_range(green, 0, 0xFF, "green") @@ -184,23 +189,28 @@ def from_rgb(cls: typing.Type["Color"], red: int, green: int, blue: int) -> "Col return cls((red << 16) | (green << 8) | blue) @classmethod - def from_rgb_float(cls: typing.Type["Color"], red_f: float, green_f: float, blue_f: float) -> "Color": - """ - Convert the given RGB colorspace represented using floats in the range [0, 1] to a Color object. - - Args: - red_f: - Red channel. - green_f: - Green channel. - blue_f: - Blue channel. - - Returns: + def from_rgb_float(cls, red_f: float, green_f: float, blue_f: float) -> "Color": + """Convert the given RGB colorspace represented using floats in + the range [0, 1] to a :obj:`Color` object. + + Parameters + ---------- + red_f : :obj:`float` + Red channel. + green_f : :obj:`float` + Green channel. + blue_f : :obj:`float` + Blue channel. + + Returns + ------- + :obj:`Color` A Color object. - Raises: - ValueError: if red, green or blue are outside the range [0, 1] + Raises + ------ + :obj:`ValueError` + If red, green or blue are outside the range [0, 1]. """ assertions.assert_in_range(red_f, 0, 1, "red") assertions.assert_in_range(green_f, 0, 1, "green") @@ -209,23 +219,27 @@ def from_rgb_float(cls: typing.Type["Color"], red_f: float, green_f: float, blue return cls.from_rgb(int(red_f * 0xFF), int(green_f * 0xFF), int(blue_f * 0xFF)) @classmethod - def from_hex_code(cls: typing.Type["Color"], hex_code: str) -> "Color": - """ - Consumes a string hexadecimal color code and produces a Color. + def from_hex_code(cls, hex_code: str) -> "Color": + """Consumes a string hexadecimal color code and produces a :obj:`Color`. The inputs may be of the following format (case insensitive): - `1a2`, `#1a2`, `0x1a2` (for websafe colors), or - `1a2b3c`, `#1a2b3c` `0x1a2b3c` (for regular 3-byte color-codes). + ``1a2``, ``#1a2``, ``0x1a2`` (for websafe colors), or + ``1a2b3c``, ``#1a2b3c``, ``0x1a2b3c`` (for regular 3-byte color-codes). - Args: - hex_code: - A hexadecimal color code to parse. + Parameters + ---------- + hex_code : :obj:`str` + A hexadecimal color code to parse. - Returns: + Returns + ------- + :obj:`Color` A corresponding Color object. - Raises: - Value error if the Color + Raises + ------ + :obj:`ValueError` + If ``hex_code`` is not a hexadecimal or is a inalid length. """ if hex_code.startswith("#"): hex_code = hex_code[1:] @@ -247,15 +261,17 @@ def from_hex_code(cls: typing.Type["Color"], hex_code: str) -> "Color": raise ValueError("Color code is invalid length. Must be 3 or 6 digits") @classmethod - def from_int(cls: typing.Type["Color"], i: typing.SupportsInt) -> "Color": - """ - Create a color from a raw integer that Discord can understand. + def from_int(cls, i: typing.SupportsInt) -> "Color": + """Create a color from a raw integer that Discord can understand. - Args: - i: - The raw color integer. + Parameters + ---------- + i : :obj:`typing.SupportsInt` + The raw color integer. - Returns: + Returns + ------- + :obj:`Color` The Color object. """ return cls(i) diff --git a/hikari/core/gateway_bot.py b/hikari/core/gateway_bot.py index ee9164a218..5b0df27481 100644 --- a/hikari/core/gateway_bot.py +++ b/hikari/core/gateway_bot.py @@ -16,7 +16,7 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . -__all__ = ["GatewayBot"] +__all__ = ["GatewayBot", "SessionStartLimit"] import attr diff --git a/hikari/core/guilds.py b/hikari/core/guilds.py index 5e5439cdb9..438e505999 100644 --- a/hikari/core/guilds.py +++ b/hikari/core/guilds.py @@ -101,6 +101,7 @@ class Duff: return Duff() +@enum.unique class GuildExplicitContentFilterLevel(enum.IntEnum): """Represents the explicit content filter setting for a guild.""" @@ -114,6 +115,7 @@ class GuildExplicitContentFilterLevel(enum.IntEnum): ALL_MEMBERS = 2 +@enum.unique class GuildFeature(str, enum.Enum): """Features that a guild can provide.""" @@ -150,6 +152,7 @@ class GuildFeature(str, enum.Enum): VIP_REGIONS = "VIP_REGIONS" +@enum.unique class GuildMessageNotificationsLevel(enum.IntEnum): """Represents the default notification level for new messages in a guild.""" @@ -160,6 +163,7 @@ class GuildMessageNotificationsLevel(enum.IntEnum): ONLY_MENTIONS = 1 +@enum.unique class GuildMFALevel(enum.IntEnum): """Represents the multi-factor authorization requirement for a guild.""" @@ -170,6 +174,7 @@ class GuildMFALevel(enum.IntEnum): ELEVATED = 1 +@enum.unique class GuildPremiumTier(enum.IntEnum): """Tier for Discord Nitro boosting in a guild.""" @@ -195,6 +200,7 @@ class GuildSystemChannelFlag(enum.IntFlag): SUPPRESS_PREMIUM_SUBSCRIPTION = 2 +@enum.unique class GuildVerificationLevel(enum.IntEnum): """Represents the level of verification a user needs to provide for their account before being allowed to participate in a guild.""" @@ -244,8 +250,7 @@ class GuildRole(snowflakes.UniqueEntity, entities.Deserializable): @marshaller.attrs(slots=True) class PartialGuild(snowflakes.UniqueEntity, entities.Deserializable): """This is a base object for any partial guild objects returned by the api - where we are only given limited information. - """ + where we are only given limited information.""" #: The name of the guild. #: @@ -265,7 +270,7 @@ class PartialGuild(snowflakes.UniqueEntity, entities.Deserializable): ) def format_icon_url(self, fmt: typing.Optional[str] = None, size: int = 2048) -> typing.Optional[str]: - """"Generate the url for this guild's custom icon, if set. + """Generate the url for this guild's custom icon, if set. Parameters ---------- @@ -342,14 +347,16 @@ class Guild(PartialGuild): #: guild. #: #: :type: :obj:`snowflakes.Snowflake`, optional - afk_channel_id: typing.Optional[snowflakes.Snowflake] = marshaller.attrib(deserializer=str, if_none=None) + afk_channel_id: typing.Optional[snowflakes.Snowflake] = marshaller.attrib( + deserializer=snowflakes.Snowflake, if_none=None + ) #: How long a voice user has to be AFK for before they are classed as being #: AFK and are moved to the AFK channel (:attr:`afk_channel_id`). #: #: :type: :obj:`datetime.timedelta` afk_timeout: datetime.timedelta = marshaller.attrib( - raw_name="afk_timeout", deserializer=lambda seconds: datetime.timedelta(seconds=seconds) + deserializer=lambda seconds: datetime.timedelta(seconds=seconds) ) # TODO: document when this is not specified. @@ -463,10 +470,8 @@ class Guild(PartialGuild): #: ``GUILD_CREATE`` event. If the guild is received from any other place, #: this will always be ``None``. #: - #: :type: :obj:`datetime.datetime`, optional - joined_at: typing.Optional[datetime.datetime] = marshaller.attrib( - raw_name="joined_at", deserializer=dates.parse_iso_8601_ts, - ) + #: :type: :obj`datetime.datetime`, optional + joined_at: typing.Optional[datetime.datetime] = marshaller.attrib(deserializer=dates.parse_iso_8601_ts) #: Whether the guild is considered to be large or not. #: @@ -495,7 +500,7 @@ class Guild(PartialGuild): #: The number of members in this guild. #: #: This information is only available if the guild was sent via a - #: `GUILD_CREATE` event. If the guild is received from any other place, + #: ``GUILD_CREATE`` event. If the guild is received from any other place, #: this will always be ``None``. #: #: :type: :obj:`int`, optional diff --git a/hikari/core/invites.py b/hikari/core/invites.py index 7920a915c2..7f69cebd70 100644 --- a/hikari/core/invites.py +++ b/hikari/core/invites.py @@ -47,12 +47,12 @@ class VanityUrl(entities.HikariEntity, entities.Deserializable): #: The code for this invite. #: - #: :class: :obj:`hikari.core.snowflakes.Snowflake` + #: :type: :obj:`str` code: str = marshaller.attrib(deserializer=str) #: The amount of times this invite has been used. #: - #: :class: :obj:`int` + #: :type: :obj:`int` uses: int = marshaller.attrib(deserializer=int) @@ -94,7 +94,7 @@ class InviteGuild(guilds.PartialGuild): vanity_url_code: typing.Optional[str] = marshaller.attrib(if_none=None, deserializer=str) def format_splash_url(self, fmt: str = "png", size: int = 2048) -> typing.Optional[str]: - """"Generate the url for this guild's splash, if set. + """Generate the url for this guild's splash, if set. Parameters ---------- @@ -120,7 +120,7 @@ def splash_url(self) -> typing.Optional[str]: return self.format_splash_url() def format_banner_url(self, fmt: str = "png", size: int = 2048) -> typing.Optional[str]: - """"Generate the url for this guild's banner, if set. + """Generate the url for this guild's banner, if set. Parameters ---------- @@ -152,32 +152,32 @@ class Invite(entities.HikariEntity, entities.Deserializable): #: The code for this invite. #: - #: :class: :obj:`hikari.core.snowflakes.Snowflake` + #: :type: :obj:`str` code: str = marshaller.attrib(deserializer=str) #: The partial object of the guild this dm belongs to. #: Will be ``None`` for group dm invites. #: - #: :class: :obj:`InviteGuild`, optional + #: :type: :obj:`InviteGuild`, optional guild: typing.Optional[InviteGuild] = marshaller.attrib(deserializer=InviteGuild.deserialize, if_undefined=None) #: The partial object of the channel this invite targets. #: - #: :class: :obj:`hikari.core.channels.PartialChannel` + #: :type: :obj:`channels.PartialChannel` channel: channels.PartialChannel = marshaller.attrib(deserializer=channels.PartialChannel.deserialize) #: The object of the user who created this invite. #: - #: :class: :obj:`hikari.core.users.User`, optional + #: :type: :obj:`users.User`, optional inviter: typing.Optional[users.User] = marshaller.attrib(deserializer=users.User.deserialize, if_undefined=None) #: The object of the user who this invite targets, if set. #: - #: :class: :obj:`hikari.core.users.User`, optional + #: :type: :obj:`users.User`, optional target_user: typing.Optional[users.User] = marshaller.attrib(deserializer=users.User.deserialize, if_undefined=None) #: The type of user target this invite is, if applicable. #: - #: :class: :obj:`TargetUserType`, optional + #: :type: :obj:`TargetUserType`, optional target_user_type: typing.Optional[TargetUserType] = marshaller.attrib( deserializer=TargetUserType, if_undefined=None ) @@ -185,47 +185,47 @@ class Invite(entities.HikariEntity, entities.Deserializable): #: The approximate amount of presences in this invite's guild, only present #: when ``with_counts`` is passed as ``True`` to the GET invites endpoint. #: - #: :class: :obj:`int` + #: :type: :obj:`int` approximate_presence_count: typing.Optional[int] = marshaller.attrib(deserializer=int, if_undefined=None) #: The approximate amount of members in this invite's guild, only present #: when ``with_counts`` is passed as ``True`` to the GET invites endpoint. #: - #: :class: :obj:`int` + #: :type: :obj:`int` approximate_member_count: typing.Optional[int] = marshaller.attrib(deserializer=int, if_undefined=None) @marshaller.attrs(slots=True) class InviteWithMetadata(Invite): - """Extends the base ``Invite`` object with metadata that's only returned + """Extends the base :obj:`Invite` object with metadata that's only returned when getting an invite with guild permissions, rather than it's code. """ #: The amount of times this invite has been used. #: - #: :class: :obj:`int` + #: :type: :obj:`int` uses: int = marshaller.attrib(deserializer=int) #: The limit for how many times this invite can be used before it expires. #: If set to ``0`` then this is unlimited. #: - #: :class: :obj:`int` + #: :type: :obj:`int` max_uses: int = marshaller.attrib(deserializer=int) #: The amount of time (in seconds) this invite will be valid for. #: If set to ``0`` then this is unlimited. #: - #: :class: :obj:`int` + #: :type: :obj:`int` max_age: int = marshaller.attrib(deserializer=int) #: Whether this invite grants temporary membership. #: - #: :class: :obj:`bool` + #: :type: :obj:`bool` is_temporary: bool = marshaller.attrib(raw_name="temporary", deserializer=bool) #: When this invite was created. #: - #: :class: :obj:`datetime.datetime` + #: :type: :obj:`datetime.datetime` created_at: datetime.datetime = marshaller.attrib(deserializer=dates.parse_iso_8601_ts) @property diff --git a/hikari/core/oauth2.py b/hikari/core/oauth2.py index 3cc33c5673..1a3947054d 100644 --- a/hikari/core/oauth2.py +++ b/hikari/core/oauth2.py @@ -39,12 +39,12 @@ class OwnGuild(guilds.PartialGuild): #: Whether the current user owns this guild. #: - #: :class: :obj:`bool` + #: :type: :obj:`bool` is_owner: bool = marshaller.attrib(raw_name="owner", deserializer=bool) #: The guild level permissions that apply to the current user or bot. #: - #: :type: :obj:`hikari.core.permissions.Permission` + #: :type: :obj:`permissions.Permission` my_permissions: permissions.Permission = marshaller.attrib( raw_name="permissions", deserializer=permissions.Permission ) @@ -73,17 +73,13 @@ class TeamMember(entities.HikariEntity, entities.Deserializable): #: This member's permissions within a team. #: Will always be ``["*"]`` until Discord starts using this. #: - #: :type: :obj:`typing.Set` [ `str` ] - permissions: typing.Set[str] = marshaller.attrib( - deserializer=lambda permissions: {str(permission) for permission in permissions} - ) + #: :type: :obj:`typing.Set` [ :obj:`str` ] + permissions: typing.Set[str] = marshaller.attrib(deserializer=lambda permissions: {p for p in permissions}) #: The ID of the team this member belongs to. #: - #: :type: :obj:`hikari.core.snowflakes.Snowflake` - team_id: snowflakes.Snowflake = marshaller.attrib( - hash=True, eq=True, repr=True, deserializer=snowflakes.Snowflake, serializer=str, - ) + #: :type: :obj:`snowflakes.Snowflake` + team_id: snowflakes.Snowflake = marshaller.attrib(deserializer=snowflakes.Snowflake) #: The user object of this team member. #: @@ -101,15 +97,15 @@ class Team(snowflakes.UniqueEntity, entities.Deserializable): icon_hash: typing.Optional[str] = marshaller.attrib(raw_name="icon", deserializer=str) #: The member's that belong to this team. #: - #: :type: :obj:`typing.Sequence` [ :obj:`TeamMember` ] - members: typing.Sequence[TeamMember] = marshaller.attrib( - deserializer=lambda members: {int(member["user"]["id"]): TeamMember.deserialize(member) for member in members} + #: :type: :obj:`typing.Mapping` [ :obj:`snowflakes.Snowflake`, :obj:`TeamMember` ] + members: typing.Mapping[snowflakes.Snowflake, TeamMember] = marshaller.attrib( + deserializer=lambda members: {m.user.id: m for m in map(TeamMember.deserialize, members)} ) #: The snowflake ID of this team's owner. #: - #: :type: :obj:`hikari.core.snowflakes.Snowflake` - owner_user_id: snowflakes.Snowflake = marshaller.attrib(deserializer=snowflakes.Snowflake, serializer=str) + #: :type: :obj:`snowflakes.Snowflake` + owner_user_id: snowflakes.Snowflake = marshaller.attrib(deserializer=snowflakes.Snowflake) @property def icon_url(self) -> typing.Optional[str]: @@ -117,7 +113,7 @@ def icon_url(self) -> typing.Optional[str]: return self.format_icon_url() def format_icon_url(self, fmt: str = "png", size: int = 2048) -> typing.Optional[str]: - """"Generate the icon url for this team if set. + """Generate the icon url for this team if set. Parameters ---------- @@ -126,7 +122,7 @@ def format_icon_url(self, fmt: str = "png", size: int = 2048) -> typing.Optional Supports ``png``, ``jpeg``, ``jpg`` and ``webp``. size : :obj:`int` The size to set for the url, defaults to ``2048``. - Can be any power of 2 in the range :math:`\left[16, 2048\right]`. + Can be any power of two between 16 and 2048. Returns ------- @@ -193,9 +189,7 @@ class Application(snowflakes.UniqueEntity, entities.Deserializable): #: A collection of this application's rpc origin urls, if rpc is enabled. #: #: :type: :obj:`typing.Set` [ :obj:`str` ], optional - rpc_origins: typing.Optional[typing.Set[str]] = marshaller.attrib( - deserializer=lambda origins: {str(origin) for origin in origins}, if_undefined=None - ) + rpc_origins: typing.Optional[typing.Set[str]] = marshaller.attrib(deserializer=set, if_undefined=None) #: This summary for this application's primary SKU if it's sold on Discord. #: Will be an empty string if unset. @@ -221,14 +215,14 @@ class Application(snowflakes.UniqueEntity, entities.Deserializable): #: The ID of the guild this application is linked to #: if it's sold on Discord. #: - #: :type: :obj:`hikari.core.snowflakes.Snowflake`, optional + #: :type: :obj:`snowflakes.Snowflake`, optional guild_id: typing.Optional[snowflakes.Snowflake] = marshaller.attrib( deserializer=snowflakes.Snowflake, if_undefined=None ) #: The ID of the primary "Game SKU" of a game that's sold on Discord. #: - #: :type: :obj:`hikari.core.snowflakes.Snowflake`, optional + #: :type: :obj:`snowflakes.Snowflake`, optional primary_sku_id: typing.Optional[snowflakes.Snowflake] = marshaller.attrib( deserializer=snowflakes.Snowflake, if_undefined=None ) @@ -252,7 +246,7 @@ def icon_url(self) -> typing.Optional[str]: return self.format_icon_url() def format_icon_url(self, fmt: str = "png", size: int = 2048) -> typing.Optional[str]: - """"Generate the icon url for this application if set. + """Generate the icon url for this application if set. Parameters ---------- @@ -278,7 +272,7 @@ def cover_image_url(self) -> typing.Optional[str]: return self.format_cover_image_url() def format_cover_image_url(self, fmt: str = "png", size: int = 2048) -> typing.Optional[str]: - """"Generate the url for this application's store page's cover image is + """Generate the url for this application's store page's cover image is set and applicable. Parameters diff --git a/hikari/core/snowflakes.py b/hikari/core/snowflakes.py index 5dd51a457a..ad7464bd96 100644 --- a/hikari/core/snowflakes.py +++ b/hikari/core/snowflakes.py @@ -36,14 +36,14 @@ class Snowflake(entities.HikariEntity, typing.SupportsInt): """A concrete representation of a unique identifier for an object on Discord. - This object can be treated as a regular :class:`int` for most purposes. + This object can be treated as a regular :obj:`int` for most purposes. """ __slots__ = ("_value",) #: The integer value of this ID. #: - #: :type: :class:`int` + #: :type: :obj:`int` _value: int def __init__(self, value: typing.Union[int, str]) -> None: diff --git a/hikari/core/users.py b/hikari/core/users.py index b1c1c5e73a..6734c61095 100644 --- a/hikari/core/users.py +++ b/hikari/core/users.py @@ -17,7 +17,7 @@ # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . """Components and entities that are used to describe Users on Discord.""" -__all__ = ["User", "MyUser", "UserFlag"] +__all__ = ["User", "MyUser", "UserFlag", "PremiumType"] import enum import typing @@ -63,7 +63,7 @@ def avatar_url(self) -> str: return self.format_avatar_url() def format_avatar_url(self, fmt: typing.Optional[str] = None, size: int = 2048) -> str: - """"Generate the avatar url for this user's custom avatar if set, + """Generate the avatar url for this user's custom avatar if set, else their default avatar. Parameters diff --git a/hikari/internal_utilities/marshaller.py b/hikari/internal_utilities/marshaller.py index de293a4c66..5662e62e0c 100644 --- a/hikari/internal_utilities/marshaller.py +++ b/hikari/internal_utilities/marshaller.py @@ -70,21 +70,21 @@ def attrib( The raw name of the element in its raw serialized form. If not provided, then this will use the field's default name later. transient : :obj:`bool` - If True, the field is marked as transient, meaning it will not be - serialized. Defaults to False. + If ``True``, the field is marked as transient, meaning it will not be + serialized. Defaults to ``False``. if_none : :obj:`typing.Union` [ :obj:`typing.Callable` [ ... , :obj:`typing.Any` ], :obj:`None` ], optional Either a default factory function called to get the default for when - this field is `None` or `None` to specify that this should default - to `None`. Will raise an exception when `None` is received for this + this field is ``None`` or ``None`` to specify that this should default + to ``None``. Will raise an exception when ``None`` is received for this field later if this isn't specified. if_undefined : :obj:`typing.Union` [ :obj:`typing.Callable` [ ... , :obj:`typing.Any` ], :obj:`None` ], optional Either a default factory function called to get the default for when - this field isn't defined or `None` to specify that this should default - to `None`. Will raise an exception when this field is undefined later + this field isn't defined or ``None`` to specify that this should default + to ``None``. Will raise an exception when this field is undefined later on if this isn't specified. serializer : :obj:`typing.Callable` [ [ :obj:`typing.Any` ], :obj:`typing.Any` ], optional The serializer to use. If not specified, then serializing the entire - class that this attribute is in will trigger a :class:`TypeError` + class that this attribute is in will trigger a :obj:`TypeError` later. **kwargs : Any kwargs to pass to :func:`attr.ib`. @@ -274,17 +274,17 @@ def serialize(self, obj: typing.Optional[typing.Any]) -> typing.Optional[typing. Parameters ---------- - obj : :class:`typing.Any`, optional + obj : :obj:`typing.Any`, optional The entity to serialize. Returns ------- - :class:`typing.Mapping` [ :class:`str`, :class:`typing.Any` ], optional + :obj:`typing.Mapping` [ :obj:`str`, :obj:`typing.Any` ], optional The serialized raw data item. Raises ------ - :class:`LookupError` + :obj:`LookupError` If the entity is not registered. """ if obj is None: @@ -324,7 +324,7 @@ def attrs(**kwargs): Other Parameters ---------------- auto_attribs : :obj:`bool` - This must always be ``False`` if specified, or a :class:`ValueError` + This must always be ``False`` if specified, or a :obj:`ValueError` will be raised, as this feature is not compatible with this marshaller implementation. If not specified, it will default to ``False``. marshaller : :obj:`HikariEntityMarshaller` @@ -339,7 +339,7 @@ def attrs(**kwargs): Raises ------ - :class:`ValueError` + :obj:`ValueError` If you attempt to use the `auto_attribs` feature provided by :mod:`attr`. From cd81db7ddd98a349ad3ac6a13f1fd9302004be9d Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Sun, 22 Mar 2020 16:29:42 +0000 Subject: [PATCH 022/922] Adding more configuration bits and pieces. --- README.md | 19 -- doc-requirements.txt | 5 +- docs/.gitignore | 1 - docs/_static/style.css | 76 ++++-- docs/_templates/gendoc/index.rst | 21 +- docs/_templates/gendoc/module.rst | 28 ++- docs/conf.py | 22 +- docs/index.rst | 30 +++ examples/example_basic_config.json | 3 + examples/example_config.json | 24 ++ examples/example_config.yaml | 28 +++ hikari/core/__init__.py | 14 +- hikari/core/clients/gateway.py | 37 +-- hikari/core/configs/__init__.py | 33 +++ hikari/core/configs/app.py | 154 ++++++++++++ hikari/core/configs/gateway.py | 189 ++++++++++++++ hikari/core/configs/http.py | 65 +++++ hikari/core/configs/protocol.py | 132 ++++++++++ hikari/core/oauth2.py | 5 +- hikari/internal_utilities/marshaller.py | 60 ++++- hikari/net/gateway.py | 8 +- hikari/net/http_client.py | 5 +- insomnia/v7.yaml | 315 ++++++++++++++++++++++-- noxfile.py | 22 +- 24 files changed, 1121 insertions(+), 175 deletions(-) create mode 100644 docs/index.rst create mode 100644 examples/example_basic_config.json create mode 100644 examples/example_config.json create mode 100644 examples/example_config.yaml create mode 100644 hikari/core/configs/__init__.py create mode 100644 hikari/core/configs/app.py create mode 100644 hikari/core/configs/gateway.py create mode 100644 hikari/core/configs/http.py create mode 100644 hikari/core/configs/protocol.py diff --git a/README.md b/README.md index 99a6c7e6d1..164e69bb04 100644 --- a/README.md +++ b/README.md @@ -1,22 +1,3 @@ -[![](https://img.shields.io/discord/574921006817476608.svg?logo=Discord&logoColor=white&label=discord)](https://discord.gg/HMnGbsv) -[![](https://img.shields.io/lgtm/alerts/gitlab/nekokatt/hikari)](https://lgtm.com/projects/gl/nekokatt/hikari) -[![](https://img.shields.io/lgtm/grade/python/gitlab/nekokatt/hikari)](https://lgtm.com/projects/gl/nekokatt/hikari?mode=tree) -[![](https://gitlab.com/nekokatt/hikari/badges/master/coverage.svg)](https://gitlab.com/nekokatt/hikari/pipelines) -[![](https://img.shields.io/gitlab/pipeline/nekokatt/hikari/master?label=ci%20(master)&logo=gitlab)](https://gitlab.com/nekokatt/hikari/pipelines) -[![](https://img.shields.io/gitlab/pipeline/nekokatt/hikari/staging?label=ci%20(staging)&logo=gitlab)](https://gitlab.com/nekokatt/hikari/pipelines) -[![](https://img.shields.io/website/https/nekokatt.gitlab.io/hikari.svg?down_color=red&down_message=not%20building&label=docs%20(master)&logo=gitlab&logoColor=white&up_message=up-to-date)](https://nekokatt.gitlab.io/hikari) -[![](https://img.shields.io/website/https/nekokatt.gitlab.io/hikari/staging.svg?down_color=red&down_message=not%20building&label=docs%20(staging)&logo=gitlab&logoColor=white&up_message=up-to-date)](https://nekokatt.gitlab.io/hikari/staging/) -[![](https://badgen.net/pypi/v/hikari)](https://pypi.org/project/hikari) -[![](https://badgen.net/pypi/license/hikari)](?) -[![](https://img.shields.io/pypi/implementation/hikari.svg)](?) -[![](https://img.shields.io/pypi/format/hikari.svg)](?) -[![](https://img.shields.io/pypi/dm/hikari)](?) -[![](https://img.shields.io/pypi/status/hikari)](?) -[![](https://img.shields.io/pypi/pyversions/hikari)](?) -[![](https://img.shields.io/badge/code%20style-black-000000.svg)](?) -[![](https://img.shields.io/sourcegraph/rrc/gitlab.com/nekokatt/hikari)](https://sourcegraph.com/gitlab.com/nekokatt/hikari) -[![](https://img.shields.io/static/v1?label=sourcegraph&message=view%20now!&color=blueviolet&logo=sourcegraph)](https://sourcegraph.com/gitlab.com/nekokatt/hikari) - # hikari An opinionated Discord API for Python 3 and asyncio. diff --git a/doc-requirements.txt b/doc-requirements.txt index c3d9e0f2c2..5d3162cd0b 100644 --- a/doc-requirements.txt +++ b/doc-requirements.txt @@ -1,5 +1,4 @@ -requests Jinja2==2.11.1 sphinx==2.4.4 -sphinx-autodoc-typehints==1.10.3 -sphinx-bootstrap-theme==0.7.1 +# sphinx-bootstrap-theme==0.7.1 +https://github.com/ryan-roemer/sphinx-bootstrap-theme/zipball/v0.8.0 diff --git a/docs/.gitignore b/docs/.gitignore index d5fc47c6f4..1e33be2427 100644 --- a/docs/.gitignore +++ b/docs/.gitignore @@ -1,2 +1 @@ technical/ -index.rst \ No newline at end of file diff --git a/docs/_static/style.css b/docs/_static/style.css index eb9163ddb5..063a231123 100644 --- a/docs/_static/style.css +++ b/docs/_static/style.css @@ -1,4 +1,4 @@ -@import url('https://fonts.googleapis.com/css?family=Bangers|Montserrat:300|Lato&display=swap'); +@import url('https://fonts.googleapis.com/css?family=Baloo+2|Roboto&display=swap'); @import url('https://cdn.jsdelivr.net/gh/tonsky/FiraCode@1.207/distr/fira_code.css'); /* @@ -27,7 +27,7 @@ body { background-color: #222; color: #f8f9fa; padding-top: 0 !important; - font-family: 'Montserrat', sans-serif !important; + font-family: 'Roboto', sans-serif; } /* whyyyyy */ @@ -60,32 +60,41 @@ dt:target, span.highlighted { /* syntax highlighting: https://tmtheme-editor.herokuapp.com/#!/editor/theme/Monokai */ /* yes, i hate myself for spending this much time deciphering this. */ +.bp { color: #D6D2BC !important; font-style: italic; font-weight: bold; text-decoration: underline; } /* base parameter (self, cls, etc) */ .c1, .c2, .c3 { color: #8F8C7E !important; } /* single line comment */ +.fm { color: #D6D2BC !important; font-style: italic; font-weight: bold; } /* magic attribute? */ .gp { color: #F92672 !important; } /* >>> symbol */ -.nb { color: #FD971F !important; font-style: italic; } /* builtin name */ -.nn { color: #FD971F !important; font-style: italic; } /* namespace name */ -.nc { color: #A6E22E !important; text-decoration: underline; } /* class name */ -.ne { color: #FD971F !important; font-style: italic; } /* exception name */ -.nd { color: #FD971F !important; } /* decorator name */ -.nf { color: #A6E22E !important; } /* function name */ +.go { color: #FFFFFF !important; } /* output text */ .k { color: #F92672 !important; } /* keyword */ .kn { color: #F92672 !important; } /* keyword for start of import declaration? */ .kc { color: #FD971F !important; font-style: italic; font-weight: bold;} /* singleton literal (False, True, None, etc) */ -.bp { color: #D6D2BC !important; font-style: italic; font-weight: bold; text-decoration: underline; } /* base parameter (self, cls, etc) */ -.n { color: #D6D2BC !important; font-style: italic; } /* name */ -.o { color: #F92672 !important; } /* operator */ -.p { color: #75715E !important; } /* parenthesis */ -.go { color: #FFFFFF !important; } /* output text */ .mi { color: #AE81FF !important; } /* int literal */ .mf { color: #AE81FF !important; } /* float literal */ .mh { color: #AE81FF !important; } /* hex literal */ .mn { color: #AE81FF !important; } /* no clue, but it fixes MathJAX LaTeX rendering... */ .mo { color: #AE81FF !important; } /* octal literal */ .mb { color: #AE81FF !important; } /* bool literal */ +.n { color: #D6D2BC !important; font-style: italic; } /* name */ +.nb { color: #FD971F !important; font-style: italic; } /* builtin name */ +.nn { color: #FD971F !important; font-style: italic; } /* namespace name */ +.nc { color: #A6E22E !important; text-decoration: underline; } /* class name */ +.ne { color: #FD971F !important; font-style: italic; } /* exception name */ +.nd { color: #FD971F !important; } /* decorator name */ +.nf { color: #A6E22E !important; } /* function name */ +.nt { color: #A6E22E !important; } /* json key or something */ +.o { color: #F92672 !important; } /* operator */ +.p { color: #75715E !important; } /* parenthesis */ +.s1, .s2 { color: #E6DB74 !important; } /* string literal */ .sd { color: #8F8C7E !important; } /* multiple line docstring */ .vm { color: #D6D2BC !important; font-style: italic; font-weight: bold; } /* magic attribute? */ -.fm { color: #D6D2BC !important; font-style: italic; font-weight: bold; } /* magic attribute? */ -.s1, .s2 { color: #E6DB74 !important; } /* string literal */ + +input[type="text"] { + background-color: #555; + color: #fff; + + border-color: #777; + border-radius: 3px; +} .alert { border-color: transparent !important; @@ -106,7 +115,7 @@ dt:target, span.highlighted { } .reference { - color: #a6e22e !important; + color: #F92672 !important; } .external { @@ -128,7 +137,7 @@ nav, .alert, .admonition { h1, h2, h3, h4, h5, h6, .navbar-brand, .navbar-text .navbar-version { word-wrap: break-word; - font-family: 'Lato', sans-serif !important; + font-family: 'Baloo 2', cursive; } h1 { @@ -170,10 +179,20 @@ html { */ body { position: relative; + font-size: 1.7em; min-height: 100vh; height: 100%; } +.body { + width: 100% !important; +} + +div.body { + max-width: 100% !important; +} + + body > div.container { min-height: -webkit-fill-available; } @@ -271,7 +290,7 @@ code { } .navbar-brand, h1, h2, h3, h4, h5, h6, .navbar-text .navbar-version { - font-family: 'Indie Flower', cursive; + font-family: 'Baloo 2', cursive; } dl { @@ -287,7 +306,6 @@ dl.class { dl > dd { margin-left: 4em; - line-height: 2em; } .sig-name.descname { @@ -297,3 +315,23 @@ dl > dd { p { line-height: 2em; } + +a { + color: #F92672 !important; +} + +/* + * I cant get this to hide, Sphinx is a pain in the arse, so might as well just hide the element... + * this hides an erroneous default value for attrs usage. + */ +dl.attribute > dt > em.property { + display: none; +} + +/* + * Fixes alignment voodoo for type names. + */ +a.reference > code { + padding-top: 0; + padding-bottom: 0; +} diff --git a/docs/_templates/gendoc/index.rst b/docs/_templates/gendoc/index.rst index 98ffb55c88..6bb38d8768 100644 --- a/docs/_templates/gendoc/index.rst +++ b/docs/_templates/gendoc/index.rst @@ -2,34 +2,17 @@ .. image:: https://img.shields.io/discord/574921006817476608.svg?logo=Discord&logoColor=white&label=discord :target: https://discord.gg/HMnGbsv -.. image:: https://img.shields.io/lgtm/alerts/gitlab/nekokatt/hikari - :target: https://lgtm.com/projects/gl/nekokatt/hikari .. image:: https://img.shields.io/lgtm/grade/python/gitlab/nekokatt/hikari :target: https://lgtm.com/projects/gl/nekokatt/hikari?mode=tree .. image:: https://gitlab.com/nekokatt/hikari/badges/master/coverage.svg :target: https://gitlab.com/nekokatt/hikari/pipelines -.. image:: https://img.shields.io/gitlab/pipeline/nekokatt/hikari/master?label=ci%20(master)&logo=gitlab +.. image:: https://img.shields.io/gitlab/pipeline/nekokatt/hikari/master?label=pipelines&logo=gitlab :target: https://gitlab.com/nekokatt/hikari/pipelines -.. image:: https://img.shields.io/gitlab/pipeline/nekokatt/hikari/staging?label=ci%20(staging)&logo=gitlab - :target: https://gitlab.com/nekokatt/hikari/pipelines -.. image:: https://img.shields.io/website/https/nekokatt.gitlab.io/hikari.svg?down_color=red&down_message=not%20building&label=docs%20(master)&logo=gitlab&logoColor=white&up_message=up-to-date - :target: https://nekokatt.gitlab.io/hikari -.. image:: https://img.shields.io/website/https/nekokatt.gitlab.io/hikari/staging.svg?down_color=red&down_message=not%20building&label=docs%20(staging)&logo=gitlab&logoColor=white&up_message=up-to-date - :target: https://nekokatt.gitlab.io/hikari/staging .. image:: https://badgen.net/pypi/v/hikari :target: https://pypi.org/project/hikari -.. image:: https://img.shields.io/sourcegraph/rrc/gitlab.com/nekokatt/hikari - :target: https://sourcegraph.com/gitlab.com/nekokatt/hikari -.. image:: https://img.shields.io/static/v1?label=sourcegraph&message=view%20now!&color=blueviolet&logo=sourcegraph - :target: https://sourcegraph.com/gitlab.com/nekokatt/hikari - .. image:: https://badgen.net/pypi/license/hikari -.. image:: https://img.shields.io/pypi/implementation/hikari.svg .. image:: https://img.shields.io/pypi/format/hikari.svg -.. image:: https://img.shields.io/pypi/dm/hikari -.. image:: https://img.shields.io/pypi/status/hikari .. image:: https://img.shields.io/pypi/pyversions/hikari -.. image:: https://img.shields.io/badge/code%20style-black-000000.svg Hikari Technical Documentation ############################## @@ -49,4 +32,4 @@ Packages and submodules * :ref:`genindex` * :ref:`modindex` -* :ref:`search` \ No newline at end of file +* :ref:`search` diff --git a/docs/_templates/gendoc/module.rst b/docs/_templates/gendoc/module.rst index 6ff3d02551..878281b3a8 100644 --- a/docs/_templates/gendoc/module.rst +++ b/docs/_templates/gendoc/module.rst @@ -1,21 +1,29 @@ :orphan: -{{ module }} -{{ rule }} +.. currentmodule:: {{ module }} -Documentation -------------- - -.. automodule:: {{ module }} - :inherited-members: +{{ module | underline }} {% if submodules %} -Submodules ----------- - .. autosummary:: {% for m in submodules %}{{ m }} {% endfor %} {% endif %} +Overview +-------- + +.. autosummary:: + {{ module }} + :members: + + +Details +------- + +.. automodule:: {{ module }} + :show-inheritance: + :inherited-members: + + diff --git a/docs/conf.py b/docs/conf.py index 40dd2fbcd2..59744a705e 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -24,17 +24,15 @@ # This file only contains a selection of the most common options. For a full # list see the documentation: # http://www.sphinx-doc.org/en/master/config -import datetime import os import re import sys import textwrap -import traceback import types -import requests import sphinx_bootstrap_theme + sys.path.insert(0, os.path.abspath("..")) @@ -76,7 +74,6 @@ "sphinx.ext.viewcode", "sphinx.ext.intersphinx", "sphinx.ext.mathjax", - "sphinx_autodoc_typehints", ] templates_path = ["_templates"] @@ -163,6 +160,9 @@ "members": True, } +autodoc_typehints = "none" +autodoc_mock_imports = ["aiohttp"] + # -- Intersphinx options ----------------------------------------------------- intersphinx_mapping = { "python": ("https://docs.python.org/3", None), @@ -172,7 +172,8 @@ # -- Autosummary settings... --------------------------------------------- -autosummary_generate_overwrite = False +autosummary_generate = True +autosummary_generate_overwrite = True # -- Epilog to inject into each page... --------------------------------------------- @@ -201,14 +202,3 @@ def setup(app): app.add_stylesheet("style.css") - - # Little easteregg. - try: - if datetime.datetime.now().month == 12: - with requests.get("http://www.schillmania.com/projects/snowstorm/snowstorm.js") as resp: - resp.raise_for_status() - with open("docs/_static/snowstorm.js", "w") as fp: - fp.write(resp.text) - app.add_javascript("snowstorm.js") - except Exception: - traceback.print_exc() diff --git a/docs/index.rst b/docs/index.rst new file mode 100644 index 0000000000..b0509a53dc --- /dev/null +++ b/docs/index.rst @@ -0,0 +1,30 @@ +.. image:: https://img.shields.io/discord/574921006817476608.svg?logo=Discord&logoColor=white&label=discord + :target: https://discord.gg/HMnGbsv +.. image:: https://img.shields.io/lgtm/grade/python/gitlab/nekokatt/hikari + :target: https://lgtm.com/projects/gl/nekokatt/hikari?mode=tree +.. image:: https://gitlab.com/nekokatt/hikari/badges/master/coverage.svg + :target: https://gitlab.com/nekokatt/hikari/pipelines +.. image:: https://img.shields.io/gitlab/pipeline/nekokatt/hikari/master?label=pipelines&logo=gitlab + :target: https://gitlab.com/nekokatt/hikari/pipelines +.. image:: https://badgen.net/pypi/v/hikari + :target: https://pypi.org/project/hikari +.. image:: https://img.shields.io/pypi/pyversions/hikari + +Hikari Documentation +#################### + +This is for version |version|. |staging_link| + +Hikari is licensed under the GNU LGPLv3 https://www.gnu.org/licenses/lgpl-3.0.en.html + +Technical documentation +----------------------- + +.. autosummary:: + hikari + hikari.core + hikari.net + hikari.errors + +* :ref:`genindex` +* :ref:`search` diff --git a/examples/example_basic_config.json b/examples/example_basic_config.json new file mode 100644 index 0000000000..5e52a1dcee --- /dev/null +++ b/examples/example_basic_config.json @@ -0,0 +1,3 @@ +{ + "token": "ashjkdfkasjdhfajkhfdkajghak==" +} diff --git a/examples/example_config.json b/examples/example_config.json new file mode 100644 index 0000000000..07620e8016 --- /dev/null +++ b/examples/example_config.json @@ -0,0 +1,24 @@ +{ + "gateway": { + "allow_redirects": false, + "protocol": { + "proxy_auth": "Basic 1a2b3c4d==", + "proxy_url": "http://my.first.proxy.net:8080", + "request_timeout": 30.0, + "verify_ssl": true + }, + "version": 6 + }, + "http": { + "allow_redirects": true, + "protocol": { + "proxy_auth": "Basic 4d3c2b1a==", + "proxy_url": "http://my.other.proxy.net:8080", + "request_timeout": 30.0, + "ssl_context": "mybot.utils.ssl#MySSLContext", + "verify_ssl": true + }, + "version": 7 + }, + "token": "ashjkdfkasjdhfajkhfdkajghak==" +} diff --git a/examples/example_config.yaml b/examples/example_config.yaml new file mode 100644 index 0000000000..0a431aec1a --- /dev/null +++ b/examples/example_config.yaml @@ -0,0 +1,28 @@ +# You would want to use a library such as PyYAML to deserialize this file into +# a Python dict first in the same way you'd use the `json` library to do it if +# you were using JSON config files. + +gateway: + allow_redirects: false + + protocol: + proxy_auth: Basic 1a2b3c4d== + proxy_url: http://my.first.proxy.net:8080 + request_timeout: 30.0 + verify_ssl: true + + version: 6 + +http: + allow_redirects: true + + protocol: + proxy_auth: Basic 4d3c2b1a== + proxy_url: http://my.other.proxy.net:8080 + request_timeout: 30.0 + ssl_context: mybot.utils.ssl#MySSLContext + verify_ssl: true + + version: 7 + +token: ashjkdfkasjdhfajkhfdkajghak== diff --git a/hikari/core/__init__.py b/hikari/core/__init__.py index fc10b8f708..55f62c2736 100644 --- a/hikari/core/__init__.py +++ b/hikari/core/__init__.py @@ -17,18 +17,8 @@ # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . """The core API for interacting with Discord directly.""" -from hikari.core import channels -from hikari.core import entities -from hikari.core import events -from hikari.core import gateway_bot -from hikari.core import guilds -from hikari.core import invites -from hikari.core import messages -from hikari.core import oauth2 -from hikari.core import permissions -from hikari.core import snowflakes -from hikari.core import users - +from hikari.core.configs import * +from hikari.core.clients import * from hikari.core.channels import * from hikari.core.entities import * from hikari.core.events import * diff --git a/hikari/core/clients/gateway.py b/hikari/core/clients/gateway.py index 75fa068437..fb4adba8cb 100644 --- a/hikari/core/clients/gateway.py +++ b/hikari/core/clients/gateway.py @@ -18,11 +18,12 @@ # along with Hikari. If not, see . import typing +from hikari.core import entities from hikari.internal_utilities import marshaller @marshaller.attrs() -class GatewayActivity: +class GatewayActivity(entities.Deserializable): #: The activity name. #: #: :type: :obj:`str` @@ -38,37 +39,3 @@ class GatewayActivity: #: #: :type: :obj:`int` type: int = marshaller.attrib(deserializer=int, serializer=int, if_undefined=0) - - -@marshaller.attrs() -class GatewayConfig: - #: Whether to enable debugging mode for the generated shards. Usually you - #: don't want to enable this. - #: - #: :type: :obj:`bool` - debug: bool = marshaller.attrib(deserializer=bool, if_undefined=False) - - #: The initial activity to set all shards to when starting the gateway. If - #: ``None``, then no activity will be set. - #: - #: :type: :obj:`GatewayActivity` - initial_activity: typing.Optional[GatewayActivity] = marshaller.attrib( - deserializer=bool, if_none=None, if_undefined=None - ) - - # TODO: implement enum for this - #: The initial status to set the shards to when starting the gateway. - #: - #: :type: :obj:`str` - initial_status: str = marshaller.attrib(deserializer=str, if_undefined="online") - - #: Whether to use zlib compression on the gateway for inbound messages or - #: not. Usually you want this turned on. - #: - #: :type: :obj:`bool` - use_compression: bool = marshaller.attrib(deserializer=bool, if_undefined=True) - - -class Gateway: - def __init__(self, gateway_config: GatewayConfig): - pass diff --git a/hikari/core/configs/__init__.py b/hikari/core/configs/__init__.py new file mode 100644 index 0000000000..e635171193 --- /dev/null +++ b/hikari/core/configs/__init__.py @@ -0,0 +1,33 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# Copyright © Nekoka.tt 2019-2020 +# +# This file is part of Hikari. +# +# Hikari is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Hikari is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Hikari. If not, see . +"""Configuration data objects. These structure the settings a user can +initialise their application with, and optionally support being read +in from an external source, such as a JSON file, using the marshalling +functionality included in this library. +""" + +from hikari.core.configs import app +from hikari.core.configs import gateway +from hikari.core.configs import http +from hikari.core.configs import protocol + +from hikari.core.configs.app import * +from hikari.core.configs.gateway import * +from hikari.core.configs.http import * +from hikari.core.configs.protocol import * diff --git a/hikari/core/configs/app.py b/hikari/core/configs/app.py new file mode 100644 index 0000000000..df464a9aa6 --- /dev/null +++ b/hikari/core/configs/app.py @@ -0,0 +1,154 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# Copyright © Nekoka.tt 2019-2020 +# +# This file is part of Hikari. +# +# Hikari is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Hikari is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Hikari. If not, see . +"""Core application configuration objects and options.""" +__all__ = ["AppConfig"] + +import typing + +from hikari.core.configs import http as http_ +from hikari.core.configs import gateway as gateway_ +from hikari.internal_utilities import marshaller + + +@marshaller.attrs(kw_only=True) +class AppConfig: + """Root application configuration object. + + All fields are optional kwargs that can be passed to the constructor. + + "Deserialized" and "unspecified" defaults are only applicable if you + create the object using :meth:`deserialize`. + + Examples + -------- + + Initializing programatically: + .. code-block:: python + + # Basic config + config = AppConfig(token="1a2b3c4da9089288a.23rhagaa8==") + + .. code-block:: python + + # A more complicated config example + config = AppConfig( + gateway=GatewayConfig( + protocol=HTTPProtocolConfig( + allow_redirects=False, + proxy_auth=aiohttp.BasicAuth("username", "password"), + proxy_url="http://my.first.proxy.net:8080", + request_timeout=30.0, + verify_ssl=False, # heresy! do not do this! + ), + sharding=ShardConfig( + shard_ids=range(0, 10), + shard_count=10 + ), + version=6, + ), + http=HTTPConfig( + protocol=HTTPProtocolConfig( + allow_redirects=True, + proxy_auth=aiohttp.BasicAuth.decode("Basic dXNlcm5hbWU6cGFzc3dvcmQ="), + proxy_url="http://my.other.proxy.net:8080", + request_timeout=30.0, + ssl_context=mybot.utils.ssl.MySSLContext, + verify_ssl=True + ), + version=7, + ), + token="1a2b3c4da9089288a.23rhagaa8==", + ) + + Initializing from a file: + .. code-block:: python + + # loading a JSON file + with open("foo.json") as fp: + config = AppConfig.deserialize(json.load(fp)) + + .. code-block:: js + + /* basic config */ + { + "token": "1a2b3c4da9089288a.23rhagaa8==" + } + + .. code-block:: js + + /* a more complicated config example */ + { + "gateway": { + "protocol": { + "allow_redirects": false, + "proxy_auth": "Basic dXNlcm5hbWU6cGFzc3dvcmQ=", + "proxy_url": "http://my.first.proxy.net:8080", + "request_timeout": 30.0, + "verify_ssl": false // heresy, do not do this! + }, + "sharding": { + "shard_ids": "0..10", + "shard_count": 10 + }, + "version": 6 + }, + "http": { + "protocol": { + "allow_redirects": true, + "proxy_auth": "Basic dXNlcm5hbWU6cGFzc3dvcmQ=", + "proxy_url": "http://my.other.proxy.net:8080", + "request_timeout": 30.0, + "ssl_context": "mybot.utils.ssl#MySSLContext", + "verify_ssl": true + }, + "version": 7 + }, + "token": "1a2b3c4da9089288a.23rhagaa8==" + } + + Of course, comments are not valid in actual standard JSON; I added them + simply for reference for this example. + """ + + #: The HTTP configuration to use. + #: + #: If unspecified or None, then this will be a set of default values. + #: + #: :type: :obj:`hikari.core.configs.http.HTTPConfig`, optional + http: typing.Optional[http_.HTTPConfig] = marshaller.attrib( + deserializer=http_.HTTPConfig.deserialize, if_none=None, if_undefined=None, default=None + ) + + #: The gateway configuration to use. + #: + #: If unspecified or None, then this will be a set of default values. + #: + #: :type: :obj:`hikari.core.configs.gateway.GatewayConfig`, optional + gateway: typing.Optional[gateway_.GatewayConfig] = marshaller.attrib( + deserializer=gateway_.GatewayConfig.deserialize, if_none=None, if_undefined=None, default=None + ) + + #: The global token to use, if applicable. This can be overridden for each + #: component that requires it. + #: + #: Note that this should not start with ``Bot`` or ``Bearer``. This is + #: detected automatically. + #: + #: :type: :obj:`str`, optional + token: typing.Optional[str] = marshaller.attrib(deserializer=str, if_none=None, if_undefined=None, default=None) diff --git a/hikari/core/configs/gateway.py b/hikari/core/configs/gateway.py new file mode 100644 index 0000000000..41d357f65d --- /dev/null +++ b/hikari/core/configs/gateway.py @@ -0,0 +1,189 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# Copyright © Nekoka.tt 2019-2020 +# +# This file is part of Hikari. +# +# Hikari is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Hikari is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Hikari. If not, see . +"""Gateway and sharding configuration objects and options.""" +__all__ = ["GatewayConfig", "ShardConfig"] + +import datetime +import re +import typing + +from hikari.core import entities +from hikari.core.clients import gateway +from hikari.core.configs import protocol as protocol_ +from hikari.internal_utilities import assertions +from hikari.internal_utilities import dates +from hikari.internal_utilities import marshaller + + +def _parse_shard_info(payload): + range_matcher = re.search(r"(\d+)\s*(\.{2,3})\s*(\d+)", payload) + + if not range_matcher: + if isinstance(payload, str): + payload = int(payload) + + if isinstance(payload, int): + return [payload] + + raise ValueError('expected shard_ids to be one of int, list of int, or range string ("x..y")') + + minimum, range_mod, maximum = range_matcher.groups() + minimum, maximum = int(minimum), int(maximum) + if len(range_mod) == 3: + maximum += 1 + + return [*range(minimum, maximum)] + + +@marshaller.attrs(kw_only=True, init=False) +class ShardConfig(entities.Deserializable): + """Manual sharding configuration. + + All fields are optional kwargs that can be passed to the constructor. + + "Deserialized" and "unspecified" defaults are only applicable if you + create the object using :meth:`deserialize`. + """ + + #: The shard IDs to produce shard connections for. + #: + #: If being deserialized, this can be several formats. + #: ``12``, ``"12"``: + #: A specific shard ID. + #: ``[0, 1, 2, 3, 8, 9, 10]``, ``["0", "1", "2", "3", "8", "9", "10"]``: + #: A sequence of shard IDs. + #: ``"5..16"``: + #: A range string. Two periods indicate a range of ``[5, 16)`` + #: (inclusive beginning, exclusive end). + #: ``"5...16"``: + #: A range string. Three periods indicate a range of + #: ``[5, 17]`` (inclusive beginning, inclusive end). + #: ``None``: + #: The ``shard_count`` will be considered and that many shards will + #: be created for you. + #: + #: :type: :obj:`typing.Sequence` [ :obj:`int` ] + shard_ids: typing.Sequence[int] = marshaller.attrib( + deserializer=_parse_shard_info, if_none=None, if_undefined=None, + ) + + #: The number of shards the entire distributed application should consist + #: of. If you run multiple instances of the bot, you should ensure this + #: value is consistent. + #: + #: :type: :obj:`int` + shard_count: int = marshaller.attrib(deserializer=int) + + def __init__(self, *, shard_ids: typing.Optional[typing.Iterable[int]] = None, shard_count: int) -> None: + self.shard_ids = [*shard_ids] if shard_ids else [*range(shard_count)] + + for shard_id in self.shard_ids: + assertions.assert_that(shard_id < self.shard_count, "shard_count must be greater than any shard ids") + + self.shard_count = shard_count + + +@marshaller.attrs(kw_only=True) +class GatewayConfig(entities.Deserializable): + """Gateway and sharding configuration. + + All fields are optional kwargs that can be passed to the constructor. + + "Deserialized" and "unspecified" defaults are only applicable if you + create the object using :meth:`deserialize`. + """ + + #: Whether to enable debugging mode for the generated shards. Usually you + #: don't want to enable this. + #: + #: :type: :obj:`bool` + debug: bool = marshaller.attrib(deserializer=bool, if_undefined=lambda: False, default=False) + + #: The initial activity to set all shards to when starting the gateway. If + #: ``None``, then no activity will be set. + #: + #: :type: :obj:`GatewayActivity`, optional + initial_activity: typing.Optional[gateway.GatewayActivity] = marshaller.attrib( + deserializer=gateway.GatewayActivity.deserialize, if_none=None, if_undefined=None, default=None + ) + + # TODO: implement enum for this + #: The initial status to set the shards to when starting the gateway. + #: + #: :type: :obj:`str` + initial_status: str = marshaller.attrib(deserializer=str, if_undefined=lambda: "online", default="online") + + #: Whether to show up as AFK or not on sign-in. + #: + #: :type: :obj:`bool` + initial_afk: bool = marshaller.attrib(deserializer=bool, if_undefined=lambda: False, default=False) + + #: The idle time to show on signing in, or ``None`` to not show an idle + #: time. + #: + #: :type: :obj:`datetime.datetime`, optional + initial_idle_since: typing.Optional[datetime.datetime] = marshaller.attrib( + deserializer=dates.unix_epoch_to_ts, if_none=None, if_undefined=None, default=None + ) + + #: The large threshold to use. + large_threshold: int = marshaller.attrib(deserializer=int, if_undefined=lambda: 250, default=True) + + #: Low level protocol details, such as proxy configuration and SSL settings. + #: + #: This is only used while creating the HTTP connection that the websocket + #: is upgraded from. + #: + #: If unspecified, defaults are used. + #: + #: :type: :obj:`hikari.core.configs.protocol.HTTPProtocolConfig` + protocol: typing.Optional[protocol_.HTTPProtocolConfig] = marshaller.attrib( + deserializer=protocol_.HTTPProtocolConfig.deserialize, if_undefined=None, default=None, + ) + + #: Manual sharding configuration to use. If this is ``None``, or + #: unspecified, then auto sharding configuration will be performed for you + #: based on defaults suggested by Discord. + #: + #: :type: :obj:`ShardConfig`, optional + shard_config: typing.Optional[ShardConfig] = marshaller.attrib( + deserializer=ShardConfig.deserialize, if_undefined=None, default=None, + ) + + #: The token to use, if applicable. + #: + #: If ``None`` or not specified, whatever is in the global token field on + #: the config will be used. Note that you will have to specify this value + #: somewhere; you will not be able to connect without it. + #: + #: :type: :obj:`str`, optional + token: typing.Optional[str] = marshaller.attrib(deserializer=str, if_none=None, if_undefined=None, default=None) + + #: Whether to use zlib compression on the gateway for inbound messages or + #: not. Usually you want this turned on. + #: + #: :type: :obj:`bool` + use_compression: bool = marshaller.attrib(deserializer=bool, if_undefined=lambda: True, default=True) + + #: The gateway API version to use. + #: + #: If unspecified, then V6 is used. + #: + #: :type: :obj:`int` + version: int = marshaller.attrib(deserializer=int, if_undefined=lambda: 6, default=6) diff --git a/hikari/core/configs/http.py b/hikari/core/configs/http.py new file mode 100644 index 0000000000..eaf17ba5e5 --- /dev/null +++ b/hikari/core/configs/http.py @@ -0,0 +1,65 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# Copyright © Nekoka.tt 2019-2020 +# +# This file is part of Hikari. +# +# Hikari is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Hikari is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Hikari. If not, see . +"""HTTP (REST) API configuration objects and options.""" + +__all__ = ["HTTPConfig"] + +import typing + +from hikari.core import entities +from hikari.core.configs import protocol as protocol_ +from hikari.internal_utilities import marshaller + + +@marshaller.attrs(kw_only=True) +class HTTPConfig(entities.Deserializable): + """HTTP API configuration. + + All fields are optional kwargs that can be passed to the constructor. + + "Deserialized" and "unspecified" defaults are only applicable if you + create the object using :meth:`deserialize`. + """ + + #: Low level protocol details, such as proxy configuration and SSL settings. + #: + #: If unspecified, defaults are used. + #: + #: :type: :obj:`hikari.core.configs.protocol.HTTPProtocolConfig` + protocol: typing.Optional[protocol_.HTTPProtocolConfig] = marshaller.attrib( + deserializer=protocol_.HTTPProtocolConfig.deserialize, if_undefined=None, default=None, + ) + + #: The token to use, if applicable. + #: + #: Note that this should not start with ``Bot`` or ``Bearer``. This is + #: detected automatically. + #: + #: If ``None`` or not specified, whatever is in the global token field on + #: the config will be used. + #: + #: :type: :obj:`str`, optional + token: typing.Optional[str] = marshaller.attrib(deserializer=str, if_none=None, if_undefined=None, default=None) + + #: The HTTP API version to use. + #: + #: If unspecified, then V7 is used. + #: + #: :type: :obj:`int` + version: int = marshaller.attrib(deserializer=int, if_undefined=lambda: 7, default=7) diff --git a/hikari/core/configs/protocol.py b/hikari/core/configs/protocol.py new file mode 100644 index 0000000000..4249863ba2 --- /dev/null +++ b/hikari/core/configs/protocol.py @@ -0,0 +1,132 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# Copyright © Nekoka.tt 2019-2020 +# +# This file is part of Hikari. +# +# Hikari is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Hikari is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Hikari. If not, see . +"""Configuration objects for various low-level protocols such as HTTP +connections and SSL management, proxies, etc.""" +__all__ = ["HTTPProtocolConfig"] + +import ssl +import typing + +import aiohttp + +from hikari.core import entities +from hikari.internal_utilities import marshaller + + +@marshaller.attrs(kw_only=True) +class HTTPProtocolConfig(entities.Deserializable): + """A configuration class that can be deserialized from a :obj:`dict`. This + represents any HTTP-specific implementation and protocol details such as + how to manage redirects, how to manage SSL, and how to use a proxy if + needed. + + All fields are optional kwargs that can be passed to the constructor. + + "Deserialized" and "unspecified" defaults are only applicable if you + create the object using :meth:`deserialize`. + """ + + #: If ``True``, allow following redirects from ``3xx`` HTTP responses. + #: Generally you do not want to enable this unless you have a good reason + #: to. + #: + #: Defaults to ``False`` if unspecified during deserialization. + #: + #: :type: :obj:`bool` + allow_redirects: bool = marshaller.attrib(deserializer=bool, if_undefined=lambda: False, default=False) + + #: Either an implementation of :obj:`aiohttp.BaseConnector`. + #: + #: This may otherwise be ``None`` to use the default settings provided + #: by :mod:`aiohttp`. + #: + #: This is deserialized as an object reference in the format + #: ``package.module#object.attribute`` that is expected to point to the + #: desired value. + #: + #: Defaults to ``None`` if unspecified during deserialization. + #: + #: :type: :obj:`aiohttp.BaseConnector`, optional + connector: typing.Optional[aiohttp.BaseConnector] = marshaller.attrib( + deserializer=marshaller.dereference_handle, if_none=None, if_undefined=None, default=None + ) + + #: Optional proxy headers to provide in any HTTP requests. + #: + #: Defaults to ``None`` if unspecified during deserialization. + #: + #: :type: :obj:`typing.Dict` [ :obj:`str`, :obj:`str` ], optional + proxy_headers: typing.Optional[typing.Dict[str, str]] = marshaller.attrib( + deserializer=dict, if_none=None, if_undefined=None, default=None + ) + + #: Optional proxy authorization to provide in any HTTP requests. + #: + #: This is deserialized using the format ``"basic {{base 64 string here}}"``. + #: + #: Defaults to ``None`` if unspecified during deserialization. + #: + #: :type: :obj:`aiohttp.BasicAuth`, optional + proxy_auth: typing.Optional[aiohttp.BasicAuth] = marshaller.attrib( + deserializer=aiohttp.BasicAuth.decode, if_none=None, if_undefined=None, default=None + ) + + #: The optional URL of the proxy to send requests via. + #: + #: Defaults to ``None`` if unspecified during deserialization. + #: + #: :type: :obj:`str`, optional + proxy_url: typing.Optional[str] = marshaller.attrib(deserializer=str, if_undefined=None, if_none=None, default=None) + + #: Optional request timeout to use. If an HTTP request takes longer than + #: this, it will be aborted. + #: + #: If not ``None``, the value represents a number of seconds as a floating + #: point number. + #: + #: Defaults to ``None`` if unspecified during deserialization. + #: + #: :type: :obj:`float`, optional + request_timeout: typing.Optional[float] = marshaller.attrib( + deserializer=float, if_undefined=None, if_none=None, default=None + ) + + #: The optional SSL context to use. + #: + #: This is deserialized as an object reference in the format + #: ``package.module#object.attribute`` that is expected to point to the + #: desired value. + #: + #: Defaults to ``None`` if unspecified during deserialization. + #: + #: :type: :obj:`ssl.SSLContext`, optional + ssl_context: typing.Optional[ssl.SSLContext] = marshaller.attrib( + deserializer=marshaller.dereference_handle, if_none=None, if_undefined=None, default=None + ) + + #: If ``True``, then responses with invalid SSL certificates will be + #: rejected. Generally you want to keep this enabled unless you have a + #: problem with SSL and you know exactly what you are doing by disabling + #: this. Disabling SSL verification can have major security implications. + #: You turn this off at your own risk. + #: + #: Defaults to ``True`` if unspecified during deserialization. + #: + #: :type: :obj:`bool` + verify_ssl: bool = marshaller.attrib(deserializer=bool, if_undefined=lambda: True, default=True) diff --git a/hikari/core/oauth2.py b/hikari/core/oauth2.py index 1a3947054d..2501636208 100644 --- a/hikari/core/oauth2.py +++ b/hikari/core/oauth2.py @@ -95,6 +95,7 @@ class Team(snowflakes.UniqueEntity, entities.Deserializable): #: #: :type: :obj:`str`, optional icon_hash: typing.Optional[str] = marshaller.attrib(raw_name="icon", deserializer=str) + #: The member's that belong to this team. #: #: :type: :obj:`typing.Mapping` [ :obj:`snowflakes.Snowflake`, :obj:`TeamMember` ] @@ -121,8 +122,8 @@ def format_icon_url(self, fmt: str = "png", size: int = 2048) -> typing.Optional The format to use for this url, defaults to ``png``. Supports ``png``, ``jpeg``, ``jpg`` and ``webp``. size : :obj:`int` - The size to set for the url, defaults to ``2048``. - Can be any power of two between 16 and 2048. + The size to set for the url, defaults to ``2048``. Can be any power + of two between 16 and 2048 inclusive. Returns ------- diff --git a/hikari/internal_utilities/marshaller.py b/hikari/internal_utilities/marshaller.py index 5662e62e0c..e0ca43bca6 100644 --- a/hikari/internal_utilities/marshaller.py +++ b/hikari/internal_utilities/marshaller.py @@ -23,8 +23,18 @@ You should not change anything in this file, if you do, you will likely get unexpected behaviour elsewhere. """ - +__all__ = [ + "RAISE", + "dereference_handle", + "attrib", + "attrs", + "HIKARI_ENTITY_MARSHALLER", + "HikariEntityMarshaller", +] + +import importlib import typing +import weakref import attr @@ -44,6 +54,46 @@ EntityT = typing.TypeVar("EntityT", contravariant=True) +def dereference_handle(handle_string: str) -> typing.Any: + """Parse a given handle string into an object reference. + + Parameters + ---------- + handle_string : :obj:`str` + The handle to the object to refer to. This is in the format + ``fully.qualified.module.name#object.attribute``. If no ``#`` is + input, then the reference will be made to the module itself. + + Returns + ------- + :obj:`typing.Any` + The thing that is referred to from this reference. + + Examples + -------- + ``"collections#deque"``: + Refers to :obj:`collections.deque` + ``"asyncio.tasks#Task"``: + Refers to :obj:`asyncio.tasks.Task` + ``"hikari.net"``: + Refers to :obj:`hikari.net` + ``"foo.bar#baz.bork.qux"``: + Would refer to a theoretical ``qux`` attribute on a ``bork`` + attribute on a ``baz`` object in the ``foo.bar`` module. + """ + if "#" not in handle_string: + module, attribute_names = handle_string, () + else: + module, _, attribute_string = handle_string.partition("#") + attribute_names = attribute_string.split(".") + + obj = importlib.import_module(module) + for attr_name in attribute_names: + obj = getattr(obj, attr_name) + + return weakref.proxy(obj) + + def attrib( *, # Mandatory! We do not want to rely on type annotations alone, as they will @@ -95,6 +145,14 @@ class that this attribute is in will trigger a :obj:`TypeError` The result of :func:`attr.ib` internally being called with additional metadata. """ + # Sphinx decides to be really awkward and inject the wrong default values + # by default. Not helpful when it documents non-optional shit as defaulting + # to None. Hack to fix this seems to be to turn on autodoc's + # typing.TYPE_CHECKING mode, and then if that is enabled, always return + # some dummy class that has a repr that returns a literal "..." string. + if typing.TYPE_CHECKING: + return type("Literal", (), {"__repr__": lambda *_: "..."})() + metadata = kwargs.pop("metadata", {}) metadata[_RAW_NAME_ATTR] = raw_name metadata[_SERIALIZER_ATTR] = serializer diff --git a/hikari/net/gateway.py b/hikari/net/gateway.py index faa8307a3a..03c59b1b71 100644 --- a/hikari/net/gateway.py +++ b/hikari/net/gateway.py @@ -308,8 +308,8 @@ class GatewayClient: #: The API version to use on Discord. #: - #: :type: :obj:`hikari.net.versions.GatewayVersion` - version: versions.GatewayVersion + #: :type: :obj:`int` + version: int def __init__( self, @@ -334,7 +334,7 @@ def __init__( token: str, url: str, verify_ssl: bool = True, - version: versions.GatewayVersion = versions.GatewayVersion.STABLE, + version: typing.Union[int, versions.GatewayVersion] = versions.GatewayVersion.STABLE, ) -> None: # Sanitise the URL... scheme, netloc, path, params, query, fragment = urllib.parse.urlparse(url, allow_fragments=True) @@ -386,7 +386,7 @@ def __init__( self.shard_id: int = shard_id self.shard_count: int = shard_count self.status: GatewayStatus = GatewayStatus.OFFLINE - self.version: versions.GatewayVersion = version + self.version: int = int(version) self.logger.debug("using Gateway version %s", int(version)) @property diff --git a/hikari/net/http_client.py b/hikari/net/http_client.py index 8e8e00494e..9b71b0dbfb 100644 --- a/hikari/net/http_client.py +++ b/hikari/net/http_client.py @@ -62,7 +62,7 @@ def __init__( json_deserialize=json.loads, json_serialize=json.dumps, token, - version: versions.HTTPAPIVersion = versions.HTTPAPIVersion.STABLE, + version: typing.Union[int, versions.HTTPAPIVersion] = versions.HTTPAPIVersion.STABLE, ): super().__init__( allow_redirects=allow_redirects, @@ -75,7 +75,7 @@ def __init__( timeout=timeout, json_serialize=json_serialize, ) - self.version = version + self.version = int(version) self.base_url = base_url.format(self) self.global_ratelimiter = ratelimits.ManualRateLimiter() self.json_serialize = json_serialize @@ -87,6 +87,7 @@ def __init__( this_type = type(self).__name__ auth_schemes = " or ".join(self._AUTHENTICATION_SCHEMES) raise RuntimeError(f"Any token passed to {this_type} should begin with {auth_schemes}") + self.token = token async def close(self): diff --git a/insomnia/v7.yaml b/insomnia/v7.yaml index d591bcf43d..05ac2bd1e9 100644 --- a/insomnia/v7.yaml +++ b/insomnia/v7.yaml @@ -1,7 +1,7 @@ _type: export __export_format: 4 -__export_date: 2020-01-26T18:17:15.486Z -__export_source: insomnia.desktop.app:v7.0.6 +__export_date: 2020-03-22T13:28:17.667Z +__export_source: insomnia.desktop.app:v7.1.1 resources: - _id: req_3f79be849f9d4a76ad7fc0572894acb4 authentication: {} @@ -60,7 +60,7 @@ resources: description: "" environment: api_url: "{{base_url}}/api/v{{api_version}}" - api_version: "7" + api_version: "6" base_domain: discordapp.com base_url: "{{http_protocol}}://{{base_domain}}" http_protocol: https @@ -72,7 +72,7 @@ resources: - api_version - api_url metaSortKey: -1576849752963 - modified: 1576852464388 + modified: 1584009171511 name: Discord API parentId: wrk_ef4732eebfd640729709f9377913ae41 _type: request_group @@ -87,9 +87,9 @@ resources: authentication: {} body: mimeType: application/json - text: |- + text: >- { - "content": "hello" + "content": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" } created: 1579446582054 description: "" @@ -103,6 +103,7 @@ resources: isPrivate: false metaSortKey: -1579446582054 method: POST + modified: 1584009252159 name: Create Message (JSON) parameters: [] parentId: fld_02ddb24b4e5148469979a2364fcc01dc @@ -112,7 +113,8 @@ resources: settingRebuildPath: true settingSendCookies: true settingStoreCookies: true - url: "{{api_url}}/channels/{% prompt 'channel_id', 'channel_id', '', '', false, true %}/messages" + url: "{{api_url}}/channels/{% prompt 'channel_id', 'channel_id', '', '', false, + true %}/messages" _type: request - _id: req_25d53074d2cf44b399bddb805ddab89b authentication: {} @@ -312,7 +314,7 @@ resources: isPrivate: false metaSortKey: -1578277095877.5 method: GET - modified: 1579888297862 + modified: 1584567461195 name: Get Reactions parameters: [] parentId: fld_02ddb24b4e5148469979a2364fcc01dc @@ -326,6 +328,33 @@ resources: true %}/messages/{% prompt 'message_id', 'message_id', '', '', false, true %}/reactions/{% prompt 'emoji', 'emoji', '', '', false, true %}" _type: request + - _id: req_21e8d5a0163648ee8bc7b41ae1cf2d32 + authentication: {} + body: {} + created: 1584883559070 + description: "" + headers: + - name: Authorization + value: "{{authorization}}" + - name: Accept + value: application/json + isPrivate: false + metaSortKey: -1577692587588.75 + method: GET + modified: 1584883585709 + name: Get Message + parameters: [] + parentId: fld_02ddb24b4e5148469979a2364fcc01dc + settingDisableRenderRequestBody: false + settingEncodeUrl: true + settingFollowRedirects: global + settingRebuildPath: true + settingSendCookies: true + settingStoreCookies: true + url: "{{api_url}}/channels/{% prompt 'channel_id', 'channel_id', '', '', false, + true %}/messages/{% prompt 'message_id', 'message_id', '', '', false, true + %}" + _type: request - _id: req_b7d357e7d7884ab7ad6d775410a9bde0 authentication: {} body: {} @@ -337,7 +366,7 @@ resources: isPrivate: false metaSortKey: -1577108079300 method: POST - modified: 1579888455825 + modified: 1580509000696 name: Trigger Typing parameters: [] parentId: fld_02ddb24b4e5148469979a2364fcc01dc @@ -475,7 +504,7 @@ resources: isPrivate: false metaSortKey: -1579553628410 method: PATCH - modified: 1579553772380 + modified: 1580471224340 name: Modify Guild Member parameters: [] parentId: fld_1d2d60a473504c10b9e30848d3864d42 @@ -487,6 +516,83 @@ resources: settingStoreCookies: true url: "{{api_url}}/guilds/574921006817476608/members/215061635574792192" _type: request + - _id: req_877c0b9320e7418fa26bdfe4ad12f666 + authentication: {} + body: + mimeType: application/json + text: '{"nick": null}' + created: 1580470148250 + description: "" + headers: + - name: Authorization + value: "{{authorization}}" + - name: Content-Type + value: application/json + isPrivate: false + metaSortKey: -1579500105257 + method: PATCH + modified: 1580508925477 + name: Modify My Nickname + parameters: [] + parentId: fld_1d2d60a473504c10b9e30848d3864d42 + settingDisableRenderRequestBody: false + settingEncodeUrl: true + settingFollowRedirects: global + settingRebuildPath: true + settingSendCookies: true + settingStoreCookies: true + url: "{{api_url}}/guilds/574921006817476608/members/@me/nick" + _type: request + - _id: req_a4c668b16f0041c19271c19d5f70ece2 + authentication: {} + body: {} + created: 1580508932231 + description: "" + headers: + - name: Authorization + value: "{{authorization}}" + isPrivate: false + metaSortKey: -1579473343680.5 + method: PATCH + modified: 1580508974015 + name: Modify My Nickname (Bad Request - No Content-Type) + parameters: [] + parentId: fld_1d2d60a473504c10b9e30848d3864d42 + settingDisableRenderRequestBody: false + settingEncodeUrl: true + settingFollowRedirects: global + settingRebuildPath: true + settingSendCookies: true + settingStoreCookies: true + url: "{{api_url}}/guilds/574921006817476608/members/@me/nick" + _type: request + - _id: req_e977a9ad1b074a958e28ce793410d780 + authentication: {} + body: + mimeType: application/json + text: "" + created: 1580508951200 + description: "" + headers: + - name: Authorization + value: "{{authorization}}" + - name: Content-Type + value: application/json + isPrivate: false + metaSortKey: -1579459962892.25 + method: PATCH + modified: 1580508951200 + name: Modify My Nickname (Bad Request - No Body) + parameters: [] + parentId: fld_1d2d60a473504c10b9e30848d3864d42 + settingDisableRenderRequestBody: false + settingEncodeUrl: true + settingFollowRedirects: global + settingRebuildPath: true + settingSendCookies: true + settingStoreCookies: true + url: "{{api_url}}/guilds/574921006817476608/members/@me/nick" + _type: request - _id: req_39206beb9d5c49d1b8fc029ac38c7073 authentication: {} body: @@ -571,6 +677,48 @@ resources: settingStoreCookies: true url: "{{api_url}}/guilds/668468453199446018" _type: request + - _id: req_527753bc06bb4b4484c92a688321cb34 + authentication: {} + body: + mimeType: application/json + text: >- + { + "recipient_id": "{% prompt 'recipient_id', 'recipient_id', '', '', false, true %}" + } + created: 1582755723321 + description: "" + headers: + - name: Authorization + value: "{{authorization}}" + - name: Content-Type + value: application/json + - name: Accept + value: application/json + isPrivate: false + metaSortKey: -1582755723321 + method: POST + modified: 1582755781201 + name: Create DM + parameters: [] + parentId: fld_bcb657e64ee54f1396c650c9ee822fd1 + settingDisableRenderRequestBody: false + settingEncodeUrl: true + settingFollowRedirects: global + settingRebuildPath: true + settingSendCookies: true + settingStoreCookies: true + url: "{{api_url}}/users/@me/channels" + _type: request + - _id: fld_bcb657e64ee54f1396c650c9ee822fd1 + created: 1582755716765 + description: "" + environment: {} + environmentPropertyOrder: null + metaSortKey: -1576882545733.75 + modified: 1582755720417 + name: User + parentId: fld_fd85c020b1744299b6016ba69c25966b + _type: request_group - _id: req_812faf17a42349a1aee6ee3c16a94620 authentication: {} body: {} @@ -731,6 +879,115 @@ resources: settingStoreCookies: true url: "{{base_url}}/oauth2/applications/@me" _type: request + - _id: req_690ed8ea2b6d49f593753043bf76b3f8 + authentication: {} + body: + mimeType: application/json + text: >- + { + "name": "{% prompt 'webhook_name', 'webhook_name', '', '', false, true %}", + "avatar": "data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAASABIAAD/2wBDAAUDBAQEAwUEBAQFBQUGBwwIBwcHBw8LCwkMEQ8SEhEPERETFhwXExQaFRERGCEYGh0dHx8fExciJCIeJBweHx7/2wBDAQUFBQcGBw4ICA4eFBEUHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh7/wgARCAFzApQDASIAAhEBAxEB/8QAHAAAAQQDAQAAAAAAAAAAAAAAAAECAwQFBgcI/8QAGgEAAwEBAQEAAAAAAAAAAAAAAAECAwQFBv/aAAwDAQACEAMQAAAB7EqKMABRAHCKIABOJdt0pXxQmSPTrw2ITOrXvRVhYjrx3g6J7BE7JHMckd9mTp2K+t3bViXV4xt7VJTFMziorUkWlV68sky1jpG2xSxockkQNVsqTUc0lta1Aig2xDIKqyRigRo5wRua5VK5orkkZMtVljlnZ+y651ZLpKouvngAAiAogMAAFaCoAAACtcAAAAAAAACxvA4ZrXoTi2fdgILMK3rwWY658fK5bwSCeNxKjm3Bk6NmrbaxuWoyF7E4/Slx8V7GMnffa6tMTFkqiVVtohVknpSJO16GQT1USSq5kcVmuggsVUijkacwiiyQjmSKOaCNmaWJYjm5n17S2dNHKtpPRPK+zPlAK5gAYAAAAAAioAgAAAOa4ARQAAAAAUBHICcz6Zi1XnduZw+fpwRTR1lWe2W8KiqtZPjkbedmvPWdWb1Sa3Jg3kKTJVZm7mUxlXaruzapldbuULUDMfEmbxjHtzeGt4+OLNYykdyG3XqZLGST4/I4+JfHajDHzxzQoqmXqMSLIPTxqpLBbpLknrh71SzjrYt1O1FbHlAvgAAAAAAAAAQBFQFRUAAAABVa4Ec1wIAAqAKIAABi+M9n0xa8jiyGOOyKV0dYwDisWgXMkY4i3Wlxrpt2vLEzWYb+ttp1siiw6BdKmptfKu2KcdvL4SsyZfu2r5W6uQMdpS41l2FjYoLuUpcxOX0eJo2KWE5tYIKa5TDtlOa+PNl+sxa2K+ZxMa9A6z5tzan0OYLO3xgCAAAABFQFRWgAAAAoggBWIADhBCgMAAAAAApanvLW/OOF2DX49GaKzDphXaqPIRW1CTV5iCjK1Od9e81YrWcbV27MbmOhWuiS3E5jqsuMRM9HTGZjWLW3kdZEVylOiN+xavlc1mKDaUpXqeedpIZS6lqrbRJWt0lVxzJTW26pl41xfQ9W9BRCSBpxgCAAAEARUQAMAAASRRAFEVgIrFEAVUUAAAAAADB5zmxXMMZlMXHo3YZYdueqNe8mAOWqjDOvLDNmPuwT6VWsVbYSo2qyR7JlMjlxlXFJHLnBdp5GqEHOWUchjkWUS+tOpcg7TzTm6cVAknXyQQTxzMM1eyFa1VtFzY+/QmrlrHZFbLkses67J3nz9398rgDEAABAAQAAAAAEQCCFEAURWKIoADFEUFEAURQAAThvTuLHRimK6OtrUNuarLDMZwuY9yyOSupbPFaUz1poKLEjIgGQ3ES2I61WzLZLYcr5hIxXi7JY6/dIrUJbTexE2/6H2vLqdqG80ceridxY+vggRzXlUsQSois1ppu1Qv0HUlmu+NsgrZFrlO1cG69WW8CByAACooIgAAAAiAQQgIWogNVapm4QBwIxQAUQYoigC4oOec6yWGXoRSwvkjVGaYRTQyPOCWGZzHVsVlE1qEYyxFMh1JUCa4yKnNtR0vHpXl/UOJzWHcx2vFYnhkpuRI0VXwzo3DqOCzeHpOcqp850vs/H9Mo688O/DXcNgbNBMXbo3aRUj2Sxtkkikdv33QL06ejSvPp5rgEDQQAMARAgIEEKBApRAaiKSqtUhytUFEAUCgAA5nvfCXriJGSx2451ed5EcsF4sfFIs4ZoZXEEEqIs13tCeJ0IFiOZhsOeyGe2x29G3rPsb577TxKudXNXXkvtdE2+NYUR53B7RG2xY7K7Zn26Lkc8wmrgdpE+Rrk8Z08dWOWFYtngmLtUrlMqWaCzOtto90roJMujs+68O7hr56gLMAABAVEECCFoog1aAwaA5WgPGqJytUlwgSqoMUMAGkc2lsV1MwmUxcUWY2VFiraqVmyWGaZhljlqKUkMyEkiVCCDJdh1x2e3V9684Zia7Nr+B6TO+mci7bxHTmdYnrXlPG6Kk+tNXlZ7quKwuXobHq+jBO343AKLLS4XMJOxufp3njqs0WmEdivaVyVZoCpbNS4aJksLmXWPsq3K7Pd+Ab1U9kGurjARAgIEGlgqDEGjFEBRAARQVUAcIonKxROVoxvDNz5npTOjZ3IwcZ1ONZu3BapWshSyGOqWTQyZy17VuKU0Msg1drL1S4mZ5e7BPv0Vrb3fRdz059M3XcKtYN4j1flF5bdgt819aa21F0xM1g+r575PV+urGnJtF63zTLbEM6tjuH0Od7LuNRVf2Xjnd/R8nX+G9W5/vhg8hQv3GOkilmnXKdt3Vu0biu51LlnRHnzez2Hi0ad02vz33+sJEEWYgFoCDVBBoo0FQABAEVqjeNcJVaoOVqiXWMhplLWeuX5kLxrr3nFPFVN80suxSlisvV3lTVc1c5HNfUVHjlWW7nzfp+XVwzbN30Xg9HYcNlZ/C9nF61ueG7uS/wBGgse589pvC++8J0x7ThtjuLfz+s0GnI3vfCfROfVk1asTFxnt1edNEuQQeD7lzH2c5U4nb2a373i4DXur8gutWu1na81eVroa2YJnUNmGwru07uTqe7YXW+lTh5n6ht3EnXoYwmbSEEGqIDQVoAIMQQFEBoqA3KxxLmqoqeC2pRavtLXCVUUen61X6SHG9T2rVrqirEl2ZGlzAipA9yOqa0jJUdSyFDdse/EZdKhy4aDdrPN16VuNLGaTscGqvuV4v3HkF5dIlzcuPZzbn/oHlG3JrnbeLd+cwrffFY6zZqCuWddrs2rFYm8LE52/GVU4P2fiNqOpPDWUijlazRWG47MTh29j1jaQ6ZczGrrn2mKvcb1DJZyJEiYScMoCFAiAqIFAAwaACKCiKDlaCerVEoAnMdTJ5l1vn+/t8L1PY9YqqbZY4qw5rKlG2KyUr4JmoHtEda3LRtuz7HWddTLbJ492dc4fLZUeUciK8jjfZNTqMvVu4TPtuWcXl2cl7EtO+TMASlABAAGPYDY3VjXT+Sblpu2UatnQ1yOLlbYqjtbFguhUuebhre0o7Miiwwty6gkAKAQBFQY1UGAgwQBRAGq0LerVIVWgPGqJytUl2r7PqwZhmV5i1zLpOq92qvLbZEjVYXvedilbqsSSJ6lEcgbruXN+tZd1aPYZ4LCsfXKACRSmFyCpfHQ1vcdfXRQz2I2FaTyItciqISqAACAkboi2427odXoGPz2C0xjnhsqopEetJo5nMl6XzHqNRq1/LY5T1wGrMAAAAQQBAKQAaIAANAFAYrFNHiBKuaEvVoDlaonYPN1iZuBdG1uq3LcEFPnHC79oc7QWoJqSJDfc49UdI+OSNxf7DxXpk77tax9rPW1JQw5Gz4nA59FLJ5IcAgQrHhUMoNioqhUVoKIAMWIpInVTSvruVsPXmGrXsfWEkrJqTJY7i0jkrXHR1DmXTDPYNR3/AJ88uqAKAAAEARWjEVBiCDAaAAAAVErVLc5ikuVATlRSVVoDmO0sWjdj1nchuEUjnPIfRXndbQijotUZSI0u0yFRzQzF/WOorTbCth8eqxG3KTecswv05JliwROxppmXDOmpqG1mp0W96NBxQ+p1tA11T0XMch6QXm42RgtdzFqzB5bSFroLXLvxOmikLWaJFT5nwF3+nc06WRvfO+jYZ8mZAAABABoioAgFIioA0AACgQHEIpSq1wK5qkqoCcrEEmHydskABVaojz16F5gacqe5Fuxu76W8Z6b3OYh0aRmMQgdgOc9gx6bNiu8qzLV1ojM86p6rGq77z8vGTKYcDZ11cayWMUVTRIJybdp0ivutnn2/jWJ+NKj5blNa1ya9r6xHCrSSBspWWmj6i45f0TnPRB9FxGSpPiyIBQACADRFQERUKQABoAAhYNBxiKNQAc5qicqKSORSRRAcAAIAuHy4LzLk8rZXRs3Jeqas89Sa9o2xTwkPlSS4qbHhIIfdrXF+o49iaPd0WNYwR4KP2lrUXdT2JnDr3dlDglL0PXR5/XrOjK9fVQd7ofLtkK6Nz+ngdc1ci3zpJDOrcFdXJOl5abB2XnPTr5eHbdLCr3ienknzqAAACADRFQERUKQGgAFIitGCA4wBqqKDgBPVFEOa4gAAVFARUAADWdM6xzNW+/tkzjzWyxCtGMkY021S3O8sJjc9I51zKY3Y8NsZi1THqNpN/ek+YgtVmrle8Y1lBQkwOphNgw89Gp6F0fQp0xtispjfx+Yw+/NMObpMUzHxqMSZUZLG58vrGyxvrkg5z03HBdnQIcIDAAEABBBiCDGuQEQQtEAaCA2K1w1VFE4RRPVrhKrXEAAAAAAKxzQXD5dAo3q05XINF7pwoaNc0ItgwFwy77yrsoPneh7PomHYxzdxcYnZ9Qkquv3uPblN7tNjbRlZIWOLENTUzTZNBwdcptmJpnRj6vytXkY6fVBc1xnVOU781l8b52bKgtMht2q9XcbggPBysAeNcJRASiAKggwRBgNABCkarSlarRqIBG5qlOVFJVUcDnMcQrmuJAABUAABEAERWlIDSjz36D5gHN2PYS0WIz9HmmyLbStY2XW8Om12znvRrnU9F7lqlZc/jRZU2awI62+DVELs1xFCqkQnbzT6U6i5Z1jWJ15D0HQdlldg4d2LnvRy6O5jlclmpbV3O8806nWbhqEyIiiVWgnDXAOaAqAACDQQGMApEVo1aqAo0GxzVKeNcS5WqJ7o3kuEUhytUFQaDkbKOFlgCqWURXSag6s4F+mj5q1QzZG+Mnsiwujp0rXdmo479A2TC7FcktO68cDy/uVao4W7Z9XG9rFmlauzC1roO45AK6SxBHr+wa/PTz1mdxafU+VdW43ry4RzFskliuD7jmNZukZlcfZRO1JCWyPcKIsDUAIU5AEIiFuaiDEEGrQGIINABoACqAOUCHgA5AIVQAaAsjSAxjQI1GhIjgutexQJ8bQLhkQGfYIwz7NZaGPR0LMBpFa+D55JgJdzgKnQQHe29YCGNAiKIC2YAJ6tZgCb6RwANuKFQtSWgHt3WgUPcGcgAKoArwuLVAKFAN2oAIgDVoA0ApEAYAH/xAAzEAABBAEBBgUDBAIDAQEAAAABAAIDBAURBhASEyExFCIyM0EgI0AVNEJQJDAlNUM2Jv/aAAgBAQABBQL/AF7Yw6ZjlRrhABRT9dPKobnLbNO+QkooIIpgUDFK/my1o9AWrhOsjmwxyOMjmt1LY+BNYGiRyALySAh13OTem7X6XJ4+j5ch9DNNwQ3BBbOV+flfytraJuY4EOaUU5FFM0XqIRTEd1dvmlcWQV+rm6aBuqm4Yo5XmV7G6qGPhDQGh2r1pxuJAQG5ybud2Hfcdx7vHVq+U5fHwd7UO+4IILYytpF+UQCNoMcaNohFFFP9JB0d6T0ah2Kaox5Z38yWr3a5B7Wsu2DPIBxGrENHdtHSPmK9LQ3cew6ncUzee6KcE7o49iimoekJwQ3FDcEE1rnux1dtWn+Xerx2oMnQkozORRRT/Xpq+TuF8KNupkempnlazqb1jVNBUDdXRyANY2ORljhhZBH05a0WikPVjdNz+z+7Ro3RaL+RR9RUo6M6sHVr0zu1fPw3o6WPgPcRncEFshR8Re/N2ttTxJoIBRRRHnYvlHdF0Tu8Q1dqppOXGOrmKLoA/Uxu5UYJsTwsbpajcHcLi2Z2giZquFcKJ612mSR467h63dh3Tuqj6OATgj6uAhOQRCraSM0IPaUbq0ElqzjqkdKp+bfpw3a+VozY6w5FFfL+kaHdyav/ABCi7Kw8ve0Jq11Vdupsy8ySuS1Gy8pttys2GcEbTJIxoK4ONlk8IkPTHx6MeDrouXq2L3JejR3jaHKRnCZ2GNzfVwqaPyx+cyQEEd9ENQbAE0RGpjOo1WzGJ8BB+ffqQXa+Xw9vHHoQ7sBq6b3E3u7v8SnRoXZTv0aBqWdA3qmDVWZOBkQQWqLgE5xcYvKGyEBtsxNsS86RvmfCwNhkA3XdIq1Ou6RWQ5rmwyIDlyXWBOHHEw9ApNOGuftSSDwk40lO6nII3yxGGVnQ7KVIubFbqSn8+7cgpsu7RsLbWks0jhrW9z5Kbu+ZPcb6tVIdXMCcdS0IERxN1ke3pve7UxhaouUkhJ+KLdZeeSXydYSDJfm5tgThjLEjnP5sro1I7jhf1TFGdY5XnSkRr8TdYPjRcK9+o5urmtUWnKwsksuM/OtV4bMU2z1HTJxxx3NAFH0rj0lfCZ1frrIxSu8oXpZH0EbdTbk43xN03vcmjU6rVSvTF8w+Ss0rVcfCmHV5OjCxz2wnVj0132JeknaSE6B3UQ+pHrUaUN1OTlTWYuVPqtn8M+wh0H55PTIyOmtvT+lc+gobo+7O3xKesY1LvM4KR/KghbrvcdAE0abpHcI6udqmKToN0zvLH2f3o1+PEw93duujyCpO7T5lH6039uw9WdkOhd96lgYmzZwdB/QZix4XGP1bA/vY7v7FN9BTejB2Tj1HRjVGOJ07+bM1arVHqWBBOPC2RxcW6gKqC6Vzi5/mXmUxJczotNTjKrW4qyzkWQnbpOze57dpFH7LfVEeo3UZAyXBt5G0v9DtrOSy4fu95LPWeRO7D0u7H2E46N+e5ABUp5cMQQ3OdqWjd0AlkLi0dd1fywjfrq/VUA02I7VXlZ+ICzAdWybndhuf7ih9EfcFDrub3MnBID0/oMla8Tlna61xrYf1kkTu/wAOR9oqRNTNVG3rI7mSt3PdqmIJo6TSaqhQ46o3Bdod0h0axDvs5Ua5pr1yszQi8O06Pd2RR7jtJ61B2b6h2gPTdUAlZgpzZxP5+0NzwWMs/Zgcqn7od5F8/wAXJ3o+XpnZqkdwMYOgUjl3LBomBTyrB4d1lbVSCLG7m9TJ690xTO0LDJJQhEFUq03UX4OTZjPEw907u30yepQ+gd2pvRw6hROLXbMSAj8/aOw2xlJZDJI9Vf3AUndH0uT9zkxDREl7wnO0CjCYNVPLosDh+bu2uk4r26H3Nd8nrWzdbm2SUAns4m7Q1zwA8Mj0U9M7SdwmewEF3Gurd2ylngtfnZO02lSuvcyFPVX3mhS7inKTufT/ACYFIejAvhx1Q6qMaqWVbP4jmIbs3JzcsgoENxK7loJdhq/h6aBAXGFfjbIyzA6KVp4mFP7MUnqC7UxuC/8AHdSmMFmvIJYfzdprTZbU8hkkrxcamcHSVvdjTz0RRUnqf2b37DuWp51XzGziUsnTBUopHtkcvgnQPdzH7o+kW558rVimx+IlzUTUc2SWXshIo5sso7GQWZgfJGSOJ4TuzPVJ6lJ0ojcB5IWmSCM6hArZO1zqX5l+w2rVyc7i6CJ08tudpR9cHSeH0u7bj3m9cneNPTE46bo26p71gsE3S/s/osRbs1rJ6LJ2mNpDcF/47pOzO3AyCPH4UaNZWrMfkaoP6iCvHPTrkUrcjAyGw3VOTfU/3FY6Vhub7NJ3DLaaGysO7Zu14e8D+ZtNkdZY2vmktSsijadUOskf7uD23dtx7ze7J6o+3dN0DdU0IvWPiqMVa3fa6nLNLHkxBo3mTHOQNiwy01MrQyV3t7pO+Hp+Kmo0YKrL2TgrOkzkWr9oLikzOUev1LIJuWyIVjJT2I+MEO0K/l3kV7o8bovbqnSfTm04ng7oZDHJg7InqflZm34SlPI61YlkZVjmpTx0m9BW6zV/3Vf0fx3HvL7zu/ZiO9o03AuCqZW/VVLMQzyVnU5otrGlmEWMj51+T3H+nc71YKnyIdobzxIepAKOoPTQaFUcXcvKbA5SAfpd/kaEF3qHrYOJ9w8VobofajPmhfy78NbjycgdHItm73Il/Jc4Nbn778lf4hEsBs6I1tbd8XmSq/SOLpPB3/ivg+qb3R3dvAKADW8woyBY6ub1+5slcYLdO3Uk2XykYl20OmEWykXHkr8fKvOO/BVefcdPJXjo7PW7VjainVoQfL2OD+FCNzzTv3sWo9q5eHD2n3WbS06ktF/q+afvk6v3Rft2dp/cyLnCXKY6PN49wc11d/BJs7eFyl+ProtpbxFerHLadgsJBjmZy74HGd3O7Hy00zpZ3Ds5WPW31LRU8NNJUYPt0qMltlmjNXdyyq9aWaSpn8nRfjcucpZyWztOyM+LQ2bWyEPDW2pi5WW34OuIqLRo1bZQGTG0YPETBkVqJ2Hj461KvWVmuLU0MTjapQiCttbb45Ldc13fMXlrN9W5vSoz0ydp/NjNg7usW0ODZfZKySGXCZB1HItcHN/Gzd6KlDUxlzLW8dRgpQrbq7zLcsEkDXK35UFr5phpMgirPqbuxQIvVomyV8nUdRyFSPw7QWSRuxsPHwMgZkq3Li2OgO7bEa4FbPQ8rF7Z1v8AF3FY+LQL5sRNmhfVmxN+F0UzQnaK3KIotncU6I2JeWyhj5HybQ9Lim8ldnbc/wDZfDvar+ajj7UmPvVJ2Wq2cxEGThvVZ6VjY7Jc2L8XI369KOjjZr92KNkbFM9sUUTJMvl9tWiPLV2cyzO7jmj6v/8AGz7qCKsIIdTga8cjakjXNzmLZkoKs8lZwazVHhAEcuWlq1461crascWArs5k+LsRkZd9WStPC+CZRjWSs3z7tVYrw2I58BLFI2tl2rwmXkNDExwPuW4KzKd/xtkhZ5/FbiHE60/ikA3zfth3eOGLH9Tg6TcnFsk91QLKUK+QrZLH5DC2cTdiyFL8EnRSWWMVmzkJlQxDI5d+2FnkYTYWn024/wC9p+UHtH0kb6JOsKHcqXrGo/Xs/p4ewLHNq5doViClkoX4OxCvBZcKDDzSuhjigj1Uk0UZz1qGTEY1pfda1ug5TRtBWE0Kg9+G5NEv1OBfqdVDJ0im3aiZagcg7Vaq3HclDMHFxxQRQMkIa288vUXlB9W+dR+udYz93sWS3PZekZhjbfi66cxr2vxMlexFe5ZHb8bbiV01vFVm1KG2/wD3jvJR+XJ/SWPrUX8k7rWUfuYEF1eIBokp1ZiMTTaYYxHunuwRl96Ypz5pDHUkKfj3SQbNx8zKxV4g3yhPiikWfxfgpamhtjHUyW1azVyIFyIVJQrPTsTWcnYosQ/U4FDe3FZmTl420dZnHRrfos/uIfdlBLcedLuyQ02kVyrJBPUsR2Ytz2te11SWBVJrL3/iOPC2GM3tsFtu3izl4jnBP9L+oq9Xo7m9a6b0ds77LSp7Rilber8M9571JJLKIa0jxHSYE1rWjdi4fDbWns9wAY7UPjZNFmsW/GzUpWz1vrKKK2mm+0eru5buCibxSuPFLW9/DV/F06R/ytlf/pt09VzJYbAkH5FtwDNj4y+NZewLWfbDJOE/0jrAx3BPO3hld2UHp3bNu1a6doae5Ogggs2FWpxxfVcqtZnphopzxPru+1D2dG2VmOqtpM/0EpxW0k/HaXYRjrurdHMGja3SzsNHq18Xh8psuP8A9Nv+fyNoZjHRxlcVKGXsipjPbo4jGcvZkdiq/Vr+qm80aHpg9W7Bu1chDKRVjE1sfXNG2VknFwTaizVPkgTf9JTlcmENe87imXd0Hubu1UdVF+92C/b7QxcrP7Lf/SfmZBvPvrbWwZHY6oclmXRgw6Fu6E8DpG8MkHmrIKP1nusVJwGqzRNTQEPplswRltkvTOLR41FiA8daPQRN6D/QUSnFbSWtDdqeFrJire8N03RsHu1OtvYMf4e1kGs2yg1zv5lYceWmkbFFZsvnn2Yx3gaSzkPh8zul8zYHCOzKzgk/l8v7qkfuUZeZXYgUDuc5rWy5GAAG3bMFSOP6CAVy2haf6SiVcnZXho1XGTaOXmZB6Cre8ExvE+Yh9iHpHSH3thP2OdiElfY4a2vzK8XKO1Nn7Gz1EXru7biHl5Zfzj9uTtN9xju6+EAQzAWBLAwpqmmjhZJcmkTWOmlgosH4BRUjw0GHmSu7W5Oba7vVX3Aq/lMY0afLWo9thP2mRYZIthz978x2ul2V2Wy1GuyrV3bdwcVFH1dn9CKnnjd6vjdieW+WkZcbkWdRNO2CPzyPeHPkgjZEzVa/6uiknhjFnLxKB8jo9USinIrKTcqmegZuroKTpWV7yOpjy7ED/j3DVbK1XwZH8zai4+Ots3ihRh352v4rE/D08eWMprjHLcaGy/O4EgwCHL4+lG+KtzefZ58IWOjJfu66Tz3qramfisTPyHLDc7RK/W6ZRzNdSZyqE3KySrIZiau7Z+dmUk2ht03TY+w9rsdejuRkolEoqQraSbSA74PbCk6zQM457jg+1UWxH7BY4ff/AC5HtY2rSHifpzdbwmUk7IxPiTuob92p8O7bsReNKzcut5NKnxRMDWAIbrWRihfl73MdxHii2ilOKjc5jo8pfiX67fX69k9LN+9ZCjkfG9NJBx1p0jqlptqDc86CR40ydjxNoerdD7QIamDrUGimp/8AC0u+xB/xFjfT+U7i0ZH5/q23rJw6HUKetz9k0wmOVwGqbuIWJttr2YpGyM3BZG85771tsDXEk/643uY+hc0LS1zXHQPdqsxkOcU3vuj6Rv8ANI33D5KUtDTZ7GnVbD/t5OjMcNKv9Fmq4t40grIx6P2VjbZwLmOjch3K/kBqGjijWHyb6j4ZWyM1WUtGJl6dtSM9/pjjkem0Lrk+pbjRBB+ijP4efFT8iaWTVZfJczezsh2B0jiUY81KDxGYm6KSt4TN7D+xddw1YW8MX9HtNT8Jk4oPG7L7Cv1g2treHzSeU5OUHWSrpxSAhxCxeRlpOrXIZa9izwte8vdujY+R9XAXplX2cqsUGOowIAAabpoYphbwVKRXMVZr/RRldJVyOTfYAQ6l6ZuPtvOrR0WNA5mxkJfP0W01XS1sWdFb+5N/SbSUvG4/YuTSzgof07aLbWrzcbvcojpJwaSzjjZuxscksuRs+In3Y/EulFKvFXY3dotFotFonjpMOt+hDYU8T4JVXlME2RiEdlMUmgrs7J58kY1cT5YvtUNmoDXxKsxCaDCuNHaFjNJP6TuH1zjtpZasMlmxE2eCaJ8E29ywsUVyazBNTt0KHjk9rmPkHg8ZuwlNshhGqYEN2i0+hysgBW7MNZl+aa24HUIffxqaPt3D90bpfS3o0guHK8ReAAG6Ss11j+mylcWK1GQy1ltnV5WR3uWCk5eSy2Pr5GCBs+Gze0dOtJlMtLzbaYON8BfAaWVjL4pQQ1yC1+guViwGi9llwlztfLNHwSLGuAtx15PEWdYZnHWUbtNXqqNHbJwGTJf1ccfLetpavi8T8bnKkeG5XdxQWIop4jJG2wTqeyZi+LERu1DmtkFW3ZqKjla8yZIg5cS4lJK0C/ma7FZsT2yBpuJOv6RC3GyRuhlBLThI4JMjex54B9OmjNlYOVj/AOtOhGVreEyW4ph0fjHcVC/aigik1jwypxGe1AAwZnE80tPVPYx6gmt1jHnLUYZtDA5TZzVWZ7NotDWjc5y2dxpD5RqzaOoeJbOynnX2PkrZumKdrfWYHy1A+zLBGIov67batod/zj5bsmMuwR1MfkzpRWz1f7jujwFmcQ2wjxNcN51Wv0SSLBYx73RtEbSrEbZIpGOikxMhZVGSiY/bEMM+8nl1dlKvHY/p9VqtV1Wq137QRxy4kdQd38tmTridov2Oa6NawyPxUYFqVusUZDmLKYuG6LdaarKtV0R03xsklkxWBDEGNYN0vr2gh4LdUEYfRr49qWQx3tzRqXHmTYat4Sh9HEFxD+g1XmKEZXkC4wFzCuMoiNym4o1ILkj83R52Nb0O7+Wyv/T7QHpmvdwkfMyOHHlCrdButVYbcWTw1iouh3lYzD2bhpUq1SNO3ze7tCwOosZphG9G7QSczNbgsbIyO5FmaUjm3uMsdacuWShHGhouJca1aUWAogt/LKb1d2BJ+rurP2rWW6VXetFfy2V/6faD1Zvvs52xvSAKP9yghu2wrwRL4WzEMUpd5Qinb5vdz3/Xgf4nzbJN7fD0ZsrGyW2PKfrYu4f0P+3/xAAqEQACAgEDBAEEAgMBAAAAAAAAAQIRIQMQMQQSIEEwIjJRYRNABRRCcf/aAAgBAwEBPwHx02lLJ2rlEkSiNDW/6KHgSveiivgQkRRFGrLti/n6fVtUySJIcR7I9l0csURoryfihIiiKOonbpfPBpSTYpKatE0SQ90NiE6INcsdciyOIssa2ZQiihESCNTU7Ijzn+hpOSeC20SHu9kc7XYnQ53gjgsdWYFwPZMQlZA1NByy2NU6/oRk1wL7VZIY9n4WIb3ssnDtSe9iERFh2a+q1hf0YR7pIfBIfghMvbjZbwVujUjcbXrwREQlaNfL/o6EbdkuCRLZ+KI6bpt7Lfp43liiuCUXGVPdEREGdRCs/BXw6MajZOWCTyMfijS0ryzWdRrxhHsii3dnUx/6W6IiIOjUj3R+KvLShbHL0ibwSeSXlCD5ZCVo6l8Lw0I5tjnJ5R3P2xNNU2Th2utkITE8ifo1Y9smvGviiuxCkSYx+MYPlmSOpGOPZrStlYseyitOCT5ZbY2ZqyTUveyEI5IztmvG1fgvgoogqdslNvgjhD2e8YOXBoaH4R/Bqfgn08+SmnnknySjUE9unh3St+ib7mdPHuwj/RXs/wBC4s19OMJUhxtWJC2iStO0Q1FJUycO17Lyrxdsgs2XaHs9mQThCz/H60U6kdquzqnCEGSduySyTX00UdO6Te2jq/xyUiHW6Uldmt18IxqJf8krZOlGlskM/BL2cPA5dyz8L2rfhC4Qx70Kv41ZlO0Q6zViqslLU1XbFpJfc6JxipKnZ3p+jqI27SNGS7WmXH0YKQ40RSRqP0UUP0PlGouRbMXyMQ1grBQ1tD6oqh9OmuT6NP8AZPWcsLA3fI+UyHFmpHAoVx4xRN2xISKyT/JPKvyXg1u14PGD1suB7dLJKLsnqtqh70aTxRqOkN58Ei+1FP2UJEfbNTgu4/M1tFex5IZQ1RWRjR07zROItNvgcFFZY5elsnQ5N8+CIoUM2yTt7ehKkaqwyP2/G14VZL8LbS4okiWVezNNXxyJdzROSiqQ88na3wdtCg2dn5Y4peyldDhQokYjxF2JFDXol6NdYYpUq8L8a3ez3o03TGiPtFDIycXaIS7soaHBydIqOn/6SipZY0mdkRJLhFeyL9M7aIRNbUTwiIlkq2akqZrfbZ7+BeL8nJYZxIkh7Rm4uyM1NYF9K/Y3st62WTV1lVIRE/YvpVs1XklLuh4rwXi/KLxRebHkY0M6eNsnK3ZDTtdz4Jtehsss0eTWcY8CNSPaxMiJW6Jv0Sy7FKk14rdLdrd/An6GMcTTXbCyVvghqSiq9FqRKDO1iilyd9YQ/wAs07Sya0bVoREhyTlvRXxPZ/B+xjKtEsJIjhMljgu0LHs7mhu8ssSpZNNdyaJ5Q0IukN+iivGtkt62e1eDaR3fgU3+BSGyxkcpE45on+Bj/Qpb9yRbfJ0/I402jUrAhsstDkdx3I52SK2SK+B8EefB7MXCP+0av3DGMRIQjpuSXLNTlCGS8GQ8F4f/xAAoEQACAgIBAwQCAgMAAAAAAAAAAQIRECExAyBBEjBAUQRhBTITFSL/2gAIAQIBAT8BvtlwWJiLwhI/QoEqielt2KKRRQ0Vloa7XhkVvvv2JIQhH6KIqiO3Yp0rZbk7Iw1ocNjiPEihoY12NDVZivZrufGERFmOiUrdEdEZ0rITXLJU9ij6mSh6UJep0OJJEkel0JeCUT0/RQ9osor4DVlUR4FmTpESKLtljl4Iyol1HJUdKltjaY2m9mm2QaaaJPY3ojKkPk4ZNUyL9m/ZZERYiTti5JPwIbESkIutDkxMjO5CdMkXoRLwM5RFd7yu9j5oQuyJdsuhbG6WIclkmN0rISqRdjEIn4Ey6ZVP2771tiYhDYhvwWWLWyU7dYXBY3s6kvBbR05WhieJcLDE7XdftydaI84iJjEWWJeWdTqXpEOcJ6LxJ27x05Vol95l4wyLp1i/cvC+xLyxcngQhvKVckp+ENHT7JvRSKRxshJNZkJDWrGXaKKL9tDRJ2RRIR47JTvQ6NkFo8jw3b0aLL8EXRdnkk9keRbVDVEHar3kvStjd5lwIXDw3R1eqo7b0RnCW0yPUjwWvBDgvdYnKkLSOpKtn+ygtMj/ACMZy1wdOTkR+iK2NkeRMfJwy/cTobvnCR5JcYjw8T26PzoScLif5p9PSZ+HLqTkq4EqVEHoT3eOp4xOCnFxZ1/4yV3E/G/jZ3cuCK9KpEObI6vESK0x8J/Bj9iey/GIi5Jf2HTJ/jdOb2iEI9NUker6IP8A5Yk/JB+GdRPVG8WWNnT4vK4I8MXHweFjyNVojyLknps9Zt8CivOExiY5Wu6qVZfBHgX9fgv6x4JfYnvHVTbtCj2sXdFWyTt6FyMfKExcfAWGLehbQhktqxFlvwLNd3qrgWkR5PInezwP4DeHiL2PDHoS3v2LylbxEvyLixcL4z3hqySrF1s22RdCk0etjk2WMTvEI+cWN6orwLj5EUSj4GqP7PCRRWGscbIw8vsW2JlfE/fZEmr2ibpC0X4F2SIxbxB2iqW8Mjrfy7Ju2RXkcUzaLxYo/eJryjpvZKV4ZXj5di2foTxSKWWxsjp5S38e12sgxYsrtkSfBHsssv4LHlZZHgjxldsh+CGFyT57V7H/xABEEAABAwEFBQMIBwcEAwEBAAABAAIDEQQQEiExIEFRYXETIoEjMlJikaGxwRQwM0BCctEFUGNzguHwJDRTkkOi8RWT/9oACAEBAAY/Avq45XZtkjoK6VXmgKgF+SqXLCG1HsWJwoNw2wzxKJ3aBYuN1F/maxO8FzXF5XO7PTYp9TXap9XHvDTX732kY8rEcTVUbWM6DRYzoNNqp0GZ+Sp+ORUVLi5VOiruWKme7kqlHgqDRUH1pCps1+rfaiPOyH3uhzqnSMHkJDXodk0WaDQq7LWb3Gp6IncMgsV2IlYQaNVBosRVbuyZ4rCPFV2ifqa/U12RGzznmgUcIGg++OikGRRa8dz8J2QFRctgN4pzh0CoqKi7NpyC5lUVKBaYisI1KxuWIt8L6Kt1OKA4aofWm6vuQp5hzF2HY+lvHkocm8z9+EPYQOheKVfrU8FnTYqnO4bLncAgOGardQecfcqnYxO1KruQrosWqxUyVN6xG8uVNgDZIRF7q7igbzZ3anNnIqhyIQPG+Oyw5ySH2DeUyzxDutH34xTNqD7V2U3eYfs5PS/vstHHPZHM1VVVVOl1L6nQZqg0WSFMqLvAUVGCip7UGjJgRcNAqb1Tii/wF5O5VvLd+5UQduN9QvzZFECpbTW8UyIQtDRno/qjcABUnTmu2nFbTIM/VHD9wGC0MxNPtRNHTWfc8DTqsiDdQIjhkNkDlfS6qrd2Y13qt9buapXJHeFiwhvS4M33hu8qoWEhZgtHNDNpVUWF1KaIHhcU4+iQU7iq7jndTeqP8x+TkWHj7kRwQ/aVre1kbMocRpU8VhitETzwDv3AHzktB5ItssDnc3iiM8mBpPoig/uqBYuGazvrdTZwhURkPgqnrs1vpdiOgzVbhXRBoOQyWFg038FiJzWEvJF0fMUuIQNzwdCqblE7kR7ELszkq/8Aki15hVogq6EaEblA+bzy3PL7+Yp2B7Si6tpyzwh6fHHCWYcu8a/G5540CJ2SVW/mqlU9qwN0H1FFW4ne7K+qLrnvH4AK+N49Urlc5vC43H1ZELw7UHIotHmnNvQ3C02phEWrWceqp+4bVK7fIfjdGOJJ2TfS6i6I+k7RYj9TRBNZw2aK0P3kV9iIuoggUeexN1BR2MX4ovgrK17Q4Zuoen7inm3huXXRCurzW5reDQL63PPLZCDRpvKy00CpfU6Kt1VotM+q096GWQ5ommq3LcqVWtxFBmE5vonZGxP+X5o7AxeYcj0VmjdxI9xp+4rPYm6yOxHoFhGjRQeCpzTutNk8zTYoqqgyL/gq35bHJVve7ectpuIErB2rG5bzmhK0giQe9U4bQutA/hooHYsVv3xSBr/3FarZqyHuR3MHrInnst5uuoq3URO7dfQX1KoNFLbpxSCNpI9c8BsNb47RmkaDwWcMZ8E8xRBrm5iiDtx2hdP/AC0bqXyWU/8AlFByO5WeQ+dho7qMj+4JZQe+7us6lRWbf5z+tzPHZCZ43VvpvKrdh331KwjTehaLSMMG5u939lHZ2UAkeBQcBsU4ZX0uDBqTRNZcDTkpId1at6bTbp/5e0HDUGqmaPNcRIPEZ+/9wCGvkbIMb+bkZDqc7ujTfW9nRVv5KuxVclhCFqtbfJ/hZ6X9root0cdfE3hE7PauHdF9OKE4GcZoei9V2my260H1R8diM8W39kd+Xt/v9/ltLvwjLmhG81llPaTfIXBO/Idofl2MIvrdyWFuSFrtTe5+Eced9odwdT2ZXudwGyBvOSApmdgkjIihTrOdQat5rmNht0x4kDYaeDqKt0claUOabIN4+/Ngr5Gz9+T824J0jtSi92UbfOKqBQbhwT/yFDrs+AQvrdS7ksLcghNP2bwNI8YHtQ8g7lQgresXBOedXGt58Bs9rIWhjOJVBMP6BVUZ2xXk7JaT4qv0Sf8A7BUkscnsr8ChKIy17MxUHThmg8aHXYF35n34udFIwajvDwvouzce8z76+Y7tOqMVauJxScyVhG7MngEIYcom+88bnD1CvH5bVLqXU33Z6LAwZr6Rbm4juZ+q7exuMR9H9CvoVsoAcgXZZ8FnkrRgq7yZz8OOwOZ2OqZBE1stpOuVaIPtZNTngHzKyayIdFQGR/5WEru2W0u/pX+xtCwOZNGd1W6IllOyk9xWA6i8XQt41N7+oRprQoOZ9nIMTfmL8zkVX74Q0+ThNG+s/j4INaKuJX0aE5fjd6V1OaPMEI+Pw2jdVVVbqNQl/wD0eym3eTrRAx22yWsc8ig6WERnk6oWB0Qkcd1N3XcuzaTKR/1H6q0PcS91Bn48LqcclI0bnEJnjsd6vZsGfXch2bO9vdqVgxR1619wVRA6U88gvJRQReFT71/vJB+WgX+9n/7L/eSHrQ/FYJ2xu54aFDjuVd9wujZ6LQL5fD4oHmpIvxwnE3pvvDxuKArm34bvvZcHUecgeHPwQEYJGjQjDG7yp+0f8gha5wYxIaRNOruJ6KirzJTOad/m76ml2S0N2RK8naHU4HRBttxx8XMzqgLK9haOCm5lvxuhj4vT/wAxTOl9ExhHe853VOgZI5pGRaBRZ6rRUXn+5ecnCyx9ph1zAWKSyupyz+CEzbM97CK5ZlEUII1uqg3iU/rfL0HxVeaFdCaHoV9Ex4HEkR147h8kYpWGORhoRwuZiOVcDvl96JOgRgg7zBkvo9l8pM7IuA9wTbTb2h8mrYtzevEotaaxQdxvPj77pH8qJh4FSN5n57J6X12NF5qisoLGY3ancsVmmjm5HulYZ4ZIj01TLHamsofs5DqDwKpxlb87u03Rtqp4+EhQ6X9q4dyPPx3LsrPBJLanju5ZDmnSW57o21q46ucrNFZYQzETidqT4ocFmDyuo1pJVIXllTmxwyK79jZXk+i+kvAaSDkOuSltEjKStbk4Xhx0aC5Vvl8PjcDxaFDamZOwh46qH9o2WjbQWA9eIRjkbhe05gqlaYsvHcm1+0j7rh95MUcojxZF3Ab/ABX0SwRuofOdvPVY30fOdTwU0/4qUZ1OiJ1zuHrORPAJ/MgojYPTYktU9YwGktbvd+l1RQN4qj2+N2GNuevBCCby44Sa+1PhfA1seHQ51RdZ/wDTycvN9i7O1DvRTtAdXUZ53SzEec6nsRfulaHfLYibvd3jzqgOAuEw/wDE6vgckGg0oKrDNH3xrxCyf3fesTWlzuJVXijGjdxTYRmaqOIbmgHqo/2dGfWfy4IMOvZYjdK/jRg2JDzCCjPqKJ//ABvLT0UlgcfN77Om9dtBRlpGnB3VOhmYY5G6gpspr2b6CT9UHNNQfu+KU6+awHvSFdva6t9Tc0fJYImDmaXR2JpyiGN3U6f5zURkFO1Z2jemiCjj9Fqd+VYuLQfYU8c9gdAq3ROwgkGgBGp4LvaEKSzu0rVp4hBzalhAJHzVCGvaUaeadOSwQR94/wCZoTOdWUuzKktB0OV0vJzT77oWbzmmTgfZOwnof77ETeAF7opBVrxQo4wS0b+IWNhBF9a5nQL6XaWUedGnci7U7lJbLXnLISacFKODALo2ci8qt/V/yuiPI/FWhnCjlHaY9WH2jeFHPEe5IKhd4dnM3zJOH9l9GtbMLtx3OX0CZ3lIvM5t/t92xSuq7c3enftK2gtqe6DqAgxjcIFzpH5NaMR6AKmeK0SVPIf/ABRRtyayztA6Zpjeac7miOIKrwBCxcQDsM/LcApS9od3hRUB0yPVAZNlb5rl9Dto7ORmhOlFUAXcl9Hs7fItPlJNw6JsMYo0D23WnkAfems4lRxuAYaZZ5EJ0EjsZeKYW5lGKVtCLmjiQug2MErA4buSMlhtFORVHWVsn9QCoIYoRzdVCaeTt5teQ6LFNIGfNPDW90DLldaXfxKewJreKJGmg6bEQ4vJuDPRcQpWelGVabGThkDRJEeB3p1gmLw7F5rtx305G4wTsrwdvaeITZg4ljDVkzfmmWiMjTvD0TvH3LQnwXmTHpGV2dksb46/jkyX0i1v+kTa+qOmxKAe9KQweOvuUltcMz3WKnCJvzU03oig6m5nVSN4KJ3KmxEeVLh1Uh9dfSLLixjUDeFS1Nw+u3zfHeEO0DJRuc05jxX+jtvd9GQKmCznniX+vmGD0Izr1QiiYGNGgAupJIxp5lWqJhLiY8slG0arCQCKU0WWELt46F0evMXR/mHxXlI2yDiDQ+zRZslHgFn2nsX2lPBfbAdQu7NGf6gss7sMdqbAPVZV3vWO0WmWY79ywwxtYOQRdwFVU6uq4+Kc/gKDqgNiBvBtUOqP8xyA4ghYeMTgV9IhqJmZim9VOUjcnj/ONxa8BwORyX0j9my9iTrGfNK7O2xmzu9I+afFV3fd7NY2ZkZ+JyUUAHmtonfy2qNu95xHoqIHgU7gfmiPRNdhh4G5vVPA9P5KgCrJAxx37liha+E+o6lVTHI78zq3UxYz6Lc1SOJjB62Z9mi700juQNPcF3YA3rkpGSPGbCMhyUbTuBXeGLqsmNHgu+wfBdrEP9O7T1TwUI/iD4rEY+0PrElUFmiH9K+xi/6r7GP/AKhZNMf5DRfaT/8AavxVYLQ4H1v1C8x8g/MHV+aw2mF8B9YUF853luEdTkiNwyVOGuzT0WgJnUJzxulNfFRdVT1H3fTbGO9+Nm5yEkZ68Rfhc0OB3ELFYZcH8J+bT+iwT2QxesDUfdS47hVEuzbABXr/AIbg0fiY0LCNGDCq8bmO4tUkfpNN1biOBuBUv5/lcGNZjoO8q1cP6VSBpa30jqeipJIaeiDl/dd1mEc8lWQl59gXdaB4XzQcMdPjcXHQIFGORoc06ptphBdZ6g9DwKjlboQD7fqKUyvji4nGegRJ37LW8Snv4uKj6hftFu/si4da1+SiPrJ/5H/G/wCkWQ4X/ibucqPGB/A/eQDvOfQZlWn9oPHetEhI6XWq06xw90eGXxUrmjzGl7ulxXQ/FMfzTm8De8XyDjQokGp3ImuZVSaLE0dlGfxO1PgqkmR3F36bVjtmHzgY3HnTKtzYuOZ6Jp5XFj2hwIoQd4XYMLiypIqa05D6t4r6g+d1OKI5Xl/oNJujVt8B8UYvRlyUh9R3320FvnCLC3q40UNnA81tCp7RvYw067lme9K7EegVslezys8LiOlMr3N4t+F0cvFtD1F5va3EQHMp1oqKuBBj2nCwVc07zu+oLD/8VHDPfzTj/DyRHAn618p3DLruWue/reel7z6RDfmqIdT8Fav5g+CB3PIPsNFN+Q/H77Z7PuM2N3Rgr8SLrP8Asxhzecb+m5NhH2TNegRhGTS3Ci3eMrmHmsPgpI97O8NprvQfn0WIjO6tBXao54rwGa8nBIRxrQLvADxrcHciEBv3/WiAaDM8zuCjdL9vKcRHojhdVG+KPgMR8UOWaB9Vx9ytJ/ij4KzzDdKG+1Wh3L77aJP+JoYOpzPyTpH6NBJVo/aD/OmcWxdEHyDy0ubvkLrXFoO1xDoc7nBRy8Rn4JpOhyKczhf1uLD+IUUb+Lc9mrnBo5lUjrKeWg8VkMLOeQ/UrPvlZX5hV+sMj/DnyRtlpHlHGoFNEWbowGqlxuDRvNFI7dWg6DJSu4CntUh4RlWn+d8gncgHeIKmk4n77Ia1Mjy4r6LG6hfk48OP+c020OZ/poMoxxO6+OYaTR+8ZXFOj4GoVeCjl4ijut4uEo3FFu8Gt+KRwHxKpGOyb7Xf2VKGV++p0QdL5QjQbh9y5oSy500buC5KWX0nErpc7xudJ/xtJ8Vnqh65r7FK7+GVaj/F+SIHAj3K0N4AH5ffcsuaEFmxdnWgPIan2pkMYADb4LQBnFJQ9CP/AJcECq7lJDv85vXZdZZTRk2Q5HchHK2grnzHFVCxHOuQHErtJc3HduHRMgZkZDQngN6DIxl7/rNVV8rW+KwWWOS0yHSg7teZQMkYjdwrWm1K/kaXVulPI3Bm+R3uFzI/RZTxUv8AKKn/AJ3yF37QqwtaHUb7fvosVnqZ7RllqG7z8l2kgBmdry5bFohpmW1HUZ3BdEQhIgW+acxsAjI7k0vymZv4FMjeQSBRPk3NNBwCp2gRtDhSoo3kL8jRFzxFJGN5ND+i7FkDy86aLytmnZ1jKykYfFd2eDxev9xZR/Uv92z+htVSGC2zfljWExd7g6YH3CqfHapXMkGYYw0BHxK+jWOBvZxu7z9S89eCElgkwSDWB+h6IkN7OUecw6jZomQ+kalUvkN1NzMv1TWcSgeJqpf5ZU/875XWk+tT74XHRPtcwrK87+HDps0U8NO7WrehvaSO7K2reYrRURafOjz8N6rsB+ZjOTwgIH17QVLxuBTO27rBpH8ysLGho4DYMUY7WUa8G9Sj2z+0O5oyDViBwkHLkpbNaATOWUZI32ZrE1xad1Dmu7aHH84DvitLN/8AwaqMnZH+SJoKpPa5pBwLsvZcHRuLDTUXAjKiD2Ow2pn/ALhCRuVMiOB37Bc45BOf+HQdNjqViO7PxQ46lSSei3LqULdTMT08KU+Kk/llWkfxvldK7jIfvfcA/RY5HYnbuA24rY0eo75Kiod2SgtAFX2aRwPStwePFZaXUvb2wLoq58uaD2uBBFRz2DZLM7MfaP4LsIdVUnP6wOYaEaL6YzTITsHxQc01BzuPBdjCe4NTxuOwGeK8EOLzX2aJ9hp3uwxD8wzR5xlWr+aPgi7gEOZr+45oeLcuqwnXQqK0Ad20R4x10PvVosr9C4j2hOjdqw0PgqKhu6onguYN3ZyEuiPu5oPa4OBuEMX20mnILsIjWQ+c5VJz2u5G9/QVWVjn/wCi79mmH9JVCKHZxHOM5PCNke6sZ70J5cFwCMEBoze7jfW4oIvO9HiTRWezfhZSv9OZ96D+B9yns2jcy3oRkrX/ADB8E/mKJjeA/chIHk5u8OR3p+EVlschI/Kdf85K0x8HA+5PeBRkwxjrobq8L6cQQqHQ5FUN1DV0Z3cOi7ZrgWgVKfbZPOkyjHJFzjUm/BGxzyeAqqyYYB65qfYFWaWSY/8AULydli8RX4qgFL8MsTJB6wqiY2uiPI1Cq3yjfV/TYwV8tB3o+YXZx1ZHv2OlxQbxQC7Q6Mq4q0Wt27uDrqVRWa3N3HA/odFbY+DwfcooedT0/cpoPKM7zVaLO4ZPZWnTIq02L8L21Z01CFoaO9A6vgcjsg80/cwnIoSb9/Ua39njLYtZOiqPMGTReJJ6sG5u8/osMTGsHTX6ouHck4jf1RikFCLmzDdr0WKP7N/eb0uJWLe45dL6Iv8AAIqp1k+A/uoQR3njG7xuMZzqprK/ITDu9Roi86n9zRTAeSldTpXJRWlzT2sNcJ67uifC/NrxhPipIH+dG4tOy6yzebM0HodKqSx2kUdqHbjz8VLFDlOG4o66O4hFjwQ5uRQj0lmzPIX/AEiShaD3Rz5/WYpXdBvKEpAaB5reV5b+OA1HQ3cyaJsY0blfTiqIMGpNAobGzzaiP9UAMgBQXsnwMJbx3dP3ORvGYTHP88ZO6/3uZamjuzCh6j+2wFEf84rBJ3XjNjxq0qI21lGYvPGjhxH6KzSsIxy5vA0IG9Opo3IXYeGqx2Y4OIOhQbN5GT3H6k55IssvfPpbh+qxyuL3nisOSNwYfNlGAqmHR9D7UWBtTC7PgibxyuMx0jFR13J07sxC33n+37sJYaNOZHPldK0Cr2d9vUKuxEa/iCjdxaEYpo2yMOoOitU0YpDA3sohwRJuxta76Qe9TiOCz1CoVQeVj9E/JBuPA70X5bNSckWRkzP4M/VeWdhb6DdPFUAuwNzJNAOaETvtCO87n+idC/VpzQcNQaqTHGHYmCRi/bMhYQZZR2fxROyyHee+/wCS7X8Uzi7wGQ/d3FT2fcHVb0OwHcDVRHknNc8Yy0kN36Kp86V+d0ce6uaDOARtNlFJdS30kQciNbswvIWg09F2YQ7WAP8Aymi7zJG+FVSCF7ubsl5eY09FuioAtLqoWi0Nq/h6KKFsZoKB11jfyMZRbG2pqKJsbQKUp4jYFfNGbuiLh58zqN+CbEzRoAHh+74ba0fw3/LYKhbZnAV/FTRSuBc+V2TpHecVZmeN0biMyC75Jh50uNos4DZxqPSRa4EEajZ3exbr6f4ULRO3P8I4c1hFzoX6EEFOidqw0THilRNkuztbTA7jSrT4qzyxuDmvxnI9Nin4pMvBdqRlCKjru/dehWmxPHK9jKircRpmNFXYj6lBg1dIAoW8E2JuriApaDJgDQiBrqEHcbsQ7kw/Fx6oxTsLTu4HYyv7KFuN59yEtqOJ/BUaKXkc0yYD7QZ9QnP/AIlVQioIUccTQykeI05m+iHDd8lHGR3yMTuuzqP3Fku85cVpduWlEHBzXN57kaWqNke7A3NGcOMkkLiXlx/DyRbsM/MVZR/FQbTdVB26MVUr+LzcWeg4+y/sp2B7d3LojJDinh/9h1WWuwHSVhi4/iPTgsMEQb8dl3VNdvBTRxI+KpwCtB3NIYPAbEUs4d2YeCackBHNESd2KlfcqRxmQ+rmvso4xzNV35XHoKLzQ7rmvNb7LtFmsvv2W2V5Pug7lbwP+AleGwz85+Ks351AeRUx6I/mNz+g2e2jja15yqFVDopZZGBzmHu8lQZbTut1lHrM+N1pJ/5DsudI0OIBWAABvD7x/8QAKhABAAIBAwMEAgMBAQEBAAAAAQARITFBURBhcYGRobEgwTDR8OFA8VD/2gAIAQEAAT8h/G/yVkmBZAvS+0SbGPCmPSoBUg4YIJW028QzpW8CTRrep9Yw7iJdQeXo1QUXFcsZSJhC+MaY4dg5lqtcCYNIqRWS/SXxr9r+u0WM06OI9iKtCZgGheDsf3LS6tWagYOeZeMBqTAEDksA2I809YKW6xI5VAx1dILHtKDslU4m9dHHlMhqaLaxKgw4YAIFeIdQIYJkJlp5wfFs8flcv+Yly5fRDb0VlDUhC7J7QQQQQmqHtChxcTT+4KnYDApe7NUF5ZgTeoWFhO4mj1mVqTXggAO+IADgJoAuFxpzy8R42GhtEXBtENUejh/34jZsvOsWJgavEsug1YRXjmXtso4joo1gvL0dCtWZUlRJsjBlKuRpN62lUO+Ik0XxNVNBAEuoEIqbjtHOkIEE0wQNRFqbGMS5cuXL/wDM7xAidmZ4JCb23YfhHkjDmGoIIVJZqDJgGAHVhrAX/mDunTwazWVBOEN28SvRWv4SgX6qGkvSZi41oBq3pMSRg8cwgCofSwaHfmELNpgjT4ISmWnI3YgWoa+WI5e0qYWZyoAFSqzHfCYIc3x++j0Wa/WXZ3IM+ziCu0wTBpmYnGIbBFZ4xKIIY0mCCCyyAwQQ/LAZy4lQ4M9d/wD2G0cA1df7ftF+unz4C/i/TXoEEEF+oggGuh5YxqaMHiDeBQjGO1WZTvX0bwQVXo9YKBtrLCHUvxLGFrpvAB5JQbN3vAeVzM0NnjP9RoEMy9pe7qcf8mSoC8KNIwjRpKFjLK7R0DroiounvMzozq9RL9UFTL1QXL7VpFcPGZiPaHcizOYcV3iUXOZVw6kXWXsl6Sjb2dvJKwTWWpCCCM121UwuvtDT/wBrQ4F7rtQlV9wIXu1EPQJgX2gqzZjy/wDIl0lZqYkMsxdnPLiK+Ct66TMtCUC3SWf9C2/7EtoGsDq67QF0Si/SZe4CW483xHUA0A7xbSjjG3pMQ8lTAdW/EfEU0PMYaQ3aGkNi8uZWhNMyu0ShZlIaTMh3jDSorGVI7J8y4cQINs1DTF5Q4YaR5p9NJxmS4HSksfY8MVysUkXjBTBZcCF3hLrGonFSiZQXdd1/9wDjYawe01dEaMDYXYfMGa36BFgctZ+NCBq96hsTZBvBrkZeCO7eq36EFd6CHEzFCt9ppephDUl8Noen5DFRUGAmDoDuwgAuvHJLIKpTXMJZB2+oVbdtrgj4DBXdYkFWhRMbaW/EQJ1V+kJszXqbxDY9sUk0DWZIZCO2GLovC1Hu7lxZo5LNMx2e59TLaIIGzMoC6E7DUw8Jfd/9jpiONei5JRY+JskBmw0fWY7qBUMV3IoUF0QtWlVAQgl67lfXK7v89/y3L6iZNnFB5HZ7ygvuuJ4Bp5INwDkbmEUeste8Yg0p4CJRUOuKNIqHY/OYKoIYBLQusa12zCI41OyOqhE/JnEeravO0VFEIAsuNpzi9JqhpZWIcS2jBdmaTWMtFucIEt+Js7GfLAuIg6EQXGvC0kK1qFajoY1YBq8UowS31btTOGEx8+jCHkrTK37qYyWl2lhtRPEQ1mFCO4kBToB8OYqFgOvwYhGyqduGZZWyPK0ZVtKVnhmULAABh8Qwc32nZZqX2uWc/wAD/wCdeZUU1X0mWBUUD6MIm5YF8QK7xOZjLAWRgr9IZtkgr01LXibW1ZqCOxw/UFiOF7R2lzXuyh0ZRQ0gYdi7sTWvNv6gpBi1rFwOJvfToQ3lDOD5jYl1ZWAU29IO6OK76SxHEuA0yzEJRq5lDmFAdUAWB8VErhDmWjW/eGxS1XkzFRDUls/mOjxn0lwEvcUb9pncVir3gyGbL3WJd9yo2M+8bYR4RK9dNt4CCsJTTxvCLUa4FsrV2xA22quEs4dQlktW7Rc0Kdw/gf4L/Hb+I0NZE08O0AFgIsrW2c/MuX3WvS7Xd2jZvLq+YVnVj9sHoqmphqOcRNoLPNTsPQmNqU47wK3FHvUQ8s6RjG+VwSn4MrdhA53ly5sH/wBjUc6soFGhGKymu8N5eDzLukYJqZRRF0u/aKtVcwVhZh3jOZ3lSbs0WFLuqJqG2IKzNccinhgtN1xw1vGEzaHJyRpt9D6g9EuYBxczOlgGieR1xNaFDc6UAOdKs7xApInCzRgAAAMAFBx/+AAlSgt7TVIQZErE+COrqDvs+NCCvNmqZ32nePNxawa+WDVOZZR4moaaxOdi6hFSsQDDj7CagVuBRjod2ksturpKHdhpDR3i0bzvKADaC3unG65rlgxZVUcuIavzF6EtMyh4yI6bvBcWGNN/SAKshTfMIEIxQ6Lmaq8TCvN9BZ8fpQEHabHGIdAahl6eV/1C7BQLFE6b7MNADgqtD/wXL6X/AC7w7stmWqwPeENWlrq1j7YtENf20S5iDp1ty3MRhOB+8wJ2l0LxmWJnqHWGnktxldDOwBrmWbgcPBCFCU4jQuNqOwgmSlQK3tARaG8turYuYi3JpLeUXAqVcNo0zljMvh72P+SFVp4OYHB0HEuQLbG5dortr9R2989LxKORxDSkYdfDH8hFlKoOmQ8F9oOPtLEPFwSt4DZ4d5b+8RhoJ5v/AJP/AAX+Pp+J/A0egzfAfLfpAXVWq33e7Ao5Ej7PF6FRaev1HSmBIyMOyRtMjzNSQERHDIAxUPsi7RSW30mJcui3SYi1bRlpYCtEuw6SomjAcygrWDcNZ6BHrNEuLDJfM+0UBrMBfmIpYVqvR3lViqjZT/iRbGuMG8YbRF8J0w8HTLkJ+SbjeohXtr4jp0sQ4OqJwOF8kBCInJ/6D+BQ3lktP1jBXqrEkre7feXduH5iebb8zb4ZmDlhpm2abkfY6VVcA+ZqaXUHbbLLnBl7E4INDgJTBLlqzH3Mm+IKK3lX2I7cQ+YjkZdEijsWlsCGdXMOZyi0Wra9dIOJcvUxtMrfSZQXVMWN4J6I3+5dF9xRTVMVtfxKMeSC4RmRBTIrD2mp7MNJ8p9kxgLRFbtToa3E1xeuzlTaH79d+9R/8l/yFiq1O0e2sOQUl/ft7R1bDfYF7CxWmLPpFt9ibfE1xpfKBZI7WBgcw4uNT/rJVyMwO8ow8ntCwCAJZwQtetyEt6rY4X+2roYgCVgKAb+0/cuXiYBNQaVi+mn6phOkMsIICmszLSWiE1HMcw3Puz/yUqudHzMYdIavzHhH80HBFTf5kmKiirRwx5DonNAD4mI0r40h6L9f/wAB3W1zhND0ie2tjjgiwxZ1u/iPMXw/cM3yYsTtNU0Di/uwavgLHtBaHpN5wMviXJ6eCHaFdu6EXVVVi6pqqw+YRdfdNpp+wX17n9N95VABgoAo0+pTq0EcLb8EuDKpJon9yypurFixWu2IOAJZWNF7yzGgRH1gK+zwwPHY6r+n4hF5Oy9YKvtNUGjHdO8+9FgmPb/vGq+hPJqQqFUG25hm8unEqUosM7f0Hz+L/FcuXL/E/grpR0ctg7xaS7OtsLzFkbrFm4+qEg96ipvtAoDtFg8TVNBwJh5oS7JKi5ScC5fEqLghZa8xVbHB2htbpAOMDVlY427KqO5zZugxoGlAYxt26GErh+AfYZcU94v6ixFlBbByu7AQOgDmHTKrfWGuYNWbhwQ+DReRl5jaODpXpNa4Ehpq58EeUn3Oi/8AETfSM1QaJvDtZdRbKaMuClgxcC59tZW9RNcO/wDLcX8CH8bF3HeH5fNROLS/F/omf6x6ugd4ByZyaDSf7fEz/wA7RGyGIu80E1s0ySouVZm2CB2hdyCqJeppz3mtImrA35gGkNYrSF2RTRtsfMaJAUEIduYU2AXswHWQK+hce1WW+W+l0Q2zd+SDiXKn3xM1eJWZytYL6wOwAqlHukzueDH7mcE8g+ouqHZb7IbRnCh+QfUtQ/Ql3Cma1+IIqqw8P/ZVmHImt46camT/APWjoIFrNn1XC1Eg91WT1INPOSDAbW5HzfFV8Yf0+v8ABf4L+F9b/C5f4sToQ8rAe8QF9a/C8EIOALem4/1CtVc/M/UVrsQ2Dn9UGrh/eONo5JrEVek+o67QQ2x6DeDPiVKaotGI+tyXtLkQpoAywBsGX0O/J7RW68lvjY8OIFR6mtwtpnviIFoHLiKFkGuhaNWrmYAduma9ppVu/BBlx4HeB0iuwlbIizPfp7Gu87K2FvyPjSAwrwCWgp/9CoaTu9P3BnQ9CaVctZb0feMgmsB81m2dIclvQeYKUmImY9H3R8tRaQuZ9lvsjoWwOUzUw5Cq2X6NYlZZebI5ia6c6X8Z9ICARHRN/wCK+t9b6XL/ABvrcyK1O8bncDHm4rSyt+VeIj5I93HYlmwwQKb0EQ1aewEy8kVfRx0xaOx9R35vqHVFtCStY2ba6kscysqxtUuMHnKPUf1CNbKAH0wnpHZAw2PuCQqIuXY7n9EBtcKtefkd8sEmyFgLBgYOggOuDzMFYJ4GoqLyly49EBqm2hq1o/cr3ra2Rdbf9UVNqa2/se5Csg3r+5+YooDk+cpi0DoUPgJTr7+XdU9cX2tFgpMAeSyagQOTv2jGuNxHHqi5U0joP2iXNkGZdoUegV9cS4eqbd0DxLA3q4OF2IhmbfTf4iMsYOdVle38K9b6XLly+t/jcslRV1Xu9CFFDnaHL+2GuQadufBCUNJdG6NQG+qwUDbLDQtkemYrd5HuMzLz9o5MvM26Or6TK3Ns0O8psCO8NDfl/qOfSW3Qt0ogZN27UC8jUyRnspKwLsS/Yw2RWy+g7npNJBhXHkc3NAsK+bPRhAal8GY1Tdb5Y6Pz91l9LtQroBvFFWllarb0ItQLBTzvLKi12uJcKnkisSntM7Ivi0GojyRFDDdADtl1igU6tSe5YtkxhoLrIZMxeEFBKTyTegXijjbJXlhoNBB4CpshNXIwuN4KtXPZwfcUiFFoVqdmF1EWZRt4eYOU41lQXSF2dXsfEESxv81/G+l9bZcGDL/Bo61LlqKdDjGp43XfSCQnUcV3/sgdsRWaMiPh0JfdoBKQ5e6vBFRcNttV5WprfR+6hrjF8QZ6TIhqFT/7iZQhQ2INRYFR5jQ1dh/cuAW1BXJhdLPiPRCFKpq1y1obspQ+Cz3sfWUplytQdnRi0SUAOQVkduI/YNPd+pvMyC1lytR3xD29tSwHBOlzNFrVmHY/cV2RoxH5TkbyD2vTutkFpF2EGFRofTKPEQlpHJ2lquodetAyxykalsYyOnkpmp7Sqh+H7g0ISuDYvfAtuYR7YsrdUu/rBeGkBJJQZB6FxLGra+sITFe5h2nlYlLhQJzQxlbkTYJ/VweTSmlzL3HFxttVrh29ItCsAdjUver9YDOu4hMV6ad9Zf4XF/G/xuXL6kHogKoBqsXt5DXcHK0r1g0bEururY7QwQGUsTsQhUqi5xey7fELtFKtXLi198x5sFPe7yBX3CynYfkjB7e7D/ccdDRmuKk5PQ1gqwLpp30mG+65g34c3lhJDc92apFSmZhi6Bo+s7EO41bW0re9pUQCCiHuDX1uCh6cV263j4nmK82mzs8ktCuJaoB3a1errOY2zgeTV8xwSuwb1/JBvotEMPM1q/oQzQoCue0Nah6qm8aajxcTMoKm5p8wwcwAAQ1rtxGW7rGEvuQUUha0gOpW/p8wWnU0cQegWAagz9y8KxtG8jF9M+vaVhuQ4Vf66XyVzxbb8TNPEOnmR/bPeRXvUfFkO8PmDQmY9eV1TQPDTEji5VVG0Y75GbxuRUVU+LweW0LACxGx6XF/G5f4j0vpfW5czd5HjgcHLFyAuj0D+msHEtYKX6cdLgcRvW6noQd0Vl5UoXi6vwx6OWLB7hwuZmD/ADJMD/ywMqnZ/cY8R3PcSPq6BnQ1HcaWmkIApYm9Mfa2tmkaqmLZrRzExAyJYniXRpoP0guZTQ55Xb5g61VaZNCLCIFbOZVYhob/AK5+53YqBQ28stdL9Z/Ahp46aGBW4F7FsLhTaHoVPs4/7LA8aGj6PmtYJ5mNfHaKzDGNWJKKqLVexvHDnWXOV29+0OkwNNVdpYSKrZq350CU8Nfe2BqxUer3C9PiHLkwhFT8PtAVQ2mXaHtGhLwD4llOTV40E8lnaXzypwJ9jiHKAYDK4eYasIdwuR4+mKt8ra52ju9Fy/4bl9Bly+ly616dlAXLxxMogrnAAcAGnvrBxaIN/PPRRBNb5CzBwW2t7a+gqHGAMMAY3nAL4nHaq8GJ4hj2uDfN7Ij/AHDYH9qRm6aZnfkQUQ90amAWSTIm5xKr1dO4IZHLMY9e0bvxsBDv+9JjoXcm2pG9sAW5xBdIKp6C1WYsZDWVyzJxP8kgzgcx97fiAW5Q0FuP6YfRHmvYwU7ze2NN739enY9e6Qh0YpProNNwCUzOOrwyncdoURub0X10fWM4ruAgVyEK+IlIMFKq4b+rNJawavgS5Uv2fl3imKIYmiD4IfkYpGrD31hb0B9mJUB2gQNph/z1RDYDdqOg22/hna/wIcFYVrRD2dH3gmmyoK2hy1CXcqZXJVxE/wBe+IX4IrVmw2eRwy9uQVZ+gv4/C5cv8F/C/wAAyzOLQFtByP1MOvDEvNbedZkB9Tq9jq92FBvj3qHRK3ppzWXwMMH6smxq+qfESpbL7Tijb+gTC0Za3B7lSwzUL7YY6vm/R6ap2gwz1n9kIL8P7gDT/wCQlBVwf6CnfWEBCOTVPZqe8MhZjPKDJ4m7WsvfJ/U3TcEQcpvawOxFTCz+kD038xTidiEDcTBrBoVTdviVYiXPaosAtA2+G6m0fdAPeCczY5V/roVIxdXsl4K3+wL9lQUsLun7ieoebfuag/Jn1FGDcofqI0n2/sgC8HI3FVa4ipf0U95UehGNv1NJ8uX5jRiCzWiJoES8EYMieQogzXwax9TPgZ6ECGjlX2V/5Bj7fuLP+2kdL090IpOD04iQ52RCDT1PkhAHujz46CqlQhE4T9RD4ox2TTwieIo6Ump9hgiEBFFiOP4r6jL6HaW3dvxPH4ujQaG6A/coLVJ5d3zHTP8AoYMTS/oRyOGWKqdh9pQOw+tLJ3x6YTpoe9xg7gkqfAfcV6i9+yUQAb8xI0agbPpU5KMt8lw9Cd9JL4iBvyr1rB7ypQt18Cor13wnww71gn6XMZXJZquZUtLKcUS7KuFfxpExeOJG2w6KZe5rEdauWq2q7cQIbBH4TIeaVnzBvZRgentf6ju/5u0etbiv7afEOts8PoglY7FD4H7jxgmwHwWjL/I6p4c1HMEczFj7v9oAn/BADbl3MOVdaqbQJtK2OnqNWw2XP3QOFL40CgL6zyBnuMseB/M7MK4e6dB1o5fhlhI2OpwnVVqqsD5I5j7vL2RvCauF0Ve2/R6Mv8Lly3qMH8hQYReCDpYn2F/a9pYaaEa0WJXlJaaeFdsPzM73wcp6GHyYnapDyZnCzCu6CPaesj56PsyfcVH5/DpESooukvQHmZA/hQ+5Ksy1w+Bt5cy4Dzh6t1Ch34U9tWePK/QZfVleL4KHQw2mII5Qq+BKfESVjsIFrAAvJcDwtBNTf/7KySjq3WdhrDGYsbXAs/N8pAJYPBB+NIMURwgbhD7GPl+I3eRfeOhsZWGy/WOkGJ2iT3Ye1IPGhDZSrcEdhpH2ISLcfMNdr6nSjixxF5G8F42YURuY/wAMcv4P5XLl7db/ABGIVR7A+gmdsWmQL0PCBVuxpkebMut2rgOf3A2NoIYr2/QWTNGAX43lEaLXcdJmq2hkHSO27jKjjJtMj6PfK/UDjsDd/qJW2RVeX+odWHLpKUOkHB2/tPJrODxoIVVV4PxA2BQaJ+9pfiXKlTrVu6NPdiKNkPpiK4axYDCajMBVDEu47HQ/FYvQaU3YKp2DPyajgxEp3RZxP16BEP6C+ao+ZQjq594KTyPxLs5E0ebR1CjC7LZ8MZNofMn4UNkL5o+/4H8Ll9Ll9DoPQeup0qNcTXemVkKk5d/mO9rUuqK+STICah4Me7EqYIcJYiZl3SHCdovJod1f1BRri46jX7A+IliTMXFR8jK6OqkIOTSAqAvjvKYXNNEPs38g0Hsa95TnHjU/Let5YSW2JqtkhehMI0XMQLDn25l3AXxcNkMQc8Q/JiijbYuDvoHvHqUgy51PQMHGYLPv+oECVzQvi7fFQWpvglETQ+JSlu6HyY9FQJ5yP1B6X0v/ABDLly5cvofg6QPp+h/2xBxMbM07aB72wgsYvY/tYRogCtBEjcBFXfZqJcFXQq9zRlmtBf6TuIy8bntKmpOGKvN+4aZ3lQ3lAXyYfuAUVtB2ItLhFGBS1n/sUHoI6dGFM1yPYj9c6j9kSs7wL7THR1BssBs6x9x/Kq/UpEFQ/G4sXQT3gr1Gs3Mf43jD85boaeVtvio6XA0o1ZkXZ+oMwnmT5lj4IRt0svBmWQ2b7pTu0vY/3AFZYewE+T5nYtnuv9fxL+dwx+R0uD0sjJ5er+x7YooMF1QRAlAONBXp8ypTezIfpINZ4mMaEHo/hjAs7lkeE2PLBm+B+viI5ufiadiTkQauF+8dIYwVQ87RHXIHkFPzFfSSDHxpqgEdCbcV5lj2loTlyfAfJDxHIoB6B+7hjGBxjqJVlwRQB7EAH5rFi6FpzWAarYfuEyWqtbZfGgeszh8kq39RYBq4goCF9766G2sD1xBBo9OQexo8qpdxT8RFN5ApJMTSPVfUfD5z2L/f8L/EPQ/C4dLg5OqrmivQCNWxUDoC34D5isuQjQtT/szGgHbpQgo35X6JEgUXIMVq1/s/iC6tVcoPr6E09yYeDNMxRLcx6Rl1NUrwmYWXRC9B/wC9BYlCmtGLTgN2LnBFyemkKiZlX+Sth7RtZqE/+z5g1gEDG38J0xzLIsWKKXFF2BqyiNs7rXnuxglKGsdnNJ4vHxNXjD1lTU8H0gxEOaC+ij5SIFrRb5cwAryq8CvthvcL+8CN0Sexl5WfvJY1fuAX1/C9b/K+g9L6D0OrVEOsvQJZgOWytfqpfXiFlAEOf31yKWINKX2e6JMG5EipurYS3AyNOIrHZ6DNe0PpRgqOqEILXZyvXT17ROWpGibOYqMggibkYZyB67BGy5jGx4/tGFu+EavaUdA2rqeV5hBZ/Ck6U3VTeoZ0hpFGoPLKzvlJALioPeDERsDJo9WI9AohzVv1FKnUQvJa/cWWGhWqwmPZkIwDkP8Ajf6lVmKt2UdzbKg4/VKl5f1QyDtfyJGQoXCCCbOT+F/iHoPQYdb6EsMgeRubX+0VmMsbDQePmHUS7J9kfJDIXWrZgvvLFWuUJA5xCdzn9T9JTs0R6x6M0hSjZJtfhBn9w6ygGQm5t6xpDb3dA19XnaKMuthWOmwNM7j5gwiNlpKGt5kX9jZ33MF6agq02Gy2ZYMzgnuDLHHcLHxFlC7UQ5YfkD+5hRnYT6EZyDd8+VoJSLfVz6rT3nleyz1TIO1mIffvOomwrfK87RXIc2y3pfyUxwGC7J27ROhZFKjlBPW/TjQ94L8oFYgTHnIe7AtVFUNAJ3NXvOKhvwazYano2hv/AG2ixd306VcD7n/yHQ6HQ6IjhobviUrXi5INOwGh6tsU9DjpfRBHZv4ZStFWruBBi8JAs8wrPJKwC1eSC3lknOEg3dj01mohORtNrjKCYOO3J3NTnSOGjUD+w/E1QEVNWdjK8k7ZBAPYqLbGlXWkRzC7rebVEB/pPBmGxMt98I+1yxpssEKKiCcVxamxVIOprvAApqQHhEfmaUO4B8IBq7VDkv2iase4j1COudIe1RADFIdjX0xCC1FlNJT8Y8Qwaeg6sdJasRyPJAqA8dp7wqLZPXaP6ir0G7eFoAWqxTbph4G8zXY+5jiHMQP/ADUfWKDR3GhGDc0e4s0HlD2BEzGp1v8A0PmUaTWdw9g+5/qGsNv/AHv4n+C/xPwryDtbUU2h4VewfjfQazAqBqar3xLFAVNFW7bSrZEbrVnuxL0mhBp3EAjUbPETFTAVtjooiaJcf9AF6nYfZLCTAyDzBzUGJ2j0zAPvDv3jSnDLr7vP3vEaFbuf5G2qtGzBMc92Hb4+YTQwEcU6Q7GX0oAu3aLbhsT/ALXTWQzAveAHvbO0ht4mTrYyxAz+9MPnNdqo1lC1d7hXcH46Grvy+xL2mU9z/E/nfQfwNIfgfmpAVF7gzCLgPoOn3KoAKcaX2fMLi147U18wD6bfyqZQtt4EfFjvBTHEqXdmPOs8O/Zx9ysXvDDCtNV8P2Q+RWI3cITi8FPUY5oNbLbeYlsFZVz1106WcxWnuH+k+6C/ZHq7uh9Eua4kp6GNehFAKvejr7R2AqxvuHiJZWm7XQrWPdgcTnw7QbthHjuVhBm4ICHYzCpqFjxGTLoCEeW68dR5gUMvM/BI1FQl75/pNEz3A1AeuJ2sRO+v8T+V/mMOh1v8qEpL2b7x6MBx+BNIwILUBHtmCttF8NL+JTEG9Ypg9y/Um9wFprkeGIckOLvJmMJ6MeUx8xZxLWdl0fRioKuWZIJubN/dwii/R3PPE39d/bWPbmN2Rq9KYC2JMvYjIf8AqH7GVwhqH0mfmUCJN7XvaAAA2Cj2in/pKTRT1ilQ2H7QtT5D4huCOK1Rk86mJSjhNYExoyk05bfUTxUoWsNWX/h8wYy2sFARAJehMhIIqR4YwetPaEAaEQ9E9Fg+YJ1qHeWX5IiLZKplI8XYaWWfCwL2Gq3yj9QIOl6+E0/8D/Afhf43LlzC69TxtCw8291oeyTEdNddQ/Qp6TKYJtZ/UNM2iXiVRBZQ0mk54A+2swjkTYFLS+a0iUgbPBUPUpPWVEGMRXXDimW/9pEQKPsAxp0uMynZrvPDtCpRqFHydWB1ZX4gxQsSh4O7KMldhY8G/kzN4Ga37nTNarDZ0JEgYSnS23vCD0IBo+mQIDffpmDSXZpo8SpOBlAwJ/d7olJxBJlXZ7FHSrWiyzfUm5T5xaL9ke9QMm0ng2P4b/F6L0v8z+RChLG7Hhiai4QxTYfWnzCcEpugEVyrhfiY9hXxrCbzg706+3RjBkTDN3QRqI0dxrzMEDBGUwHsMPEO+KiVAOXhMU7VmOJRFKRNROSENkd3cPr56gjp3ON/DbmEr4xXiFRjpC9YdCpUalUgCmWM6t6GpwH70ggpaDJwXdYFhiBZXMKxmib8XvCINNEPMDYoFdtfnrTUOUBAEYUNm7ywuC2VOBLwmygDYqiD0A7Z0W2xdjHP8LL/AAZcuPW+p1Oh/FcpRedNdM133Jo1GDSmF8ao6THBbA0/vp0Y6Tc7x11NT2/4ZU1wG5VnJyRbeFuzF2/Ksyl+O8sevTvDWrz+2NeioGgW+DeBbNHMS4SVElrbfidCDqdd5ZHcBLlxYJKgANVcEdQD6vPivVBvWcq/aXRShtUSrSDlO+n/AGE10aD30feCpDUDu4VA5slrBGqe17xmNS33mghDJaaPPQhY0Ll4/vM/Ao+mPsXpfQZcJcv87/B/C4dCEOgw/hvq1ozKi8HTlF94oUMwZMlepBsA1jHSCzwwzVgPdr9y7Nx7k0Vtu/Ls99TaNSzIWhms96jILW18sUC8NTIbmBwGlNMHvFUSagyklsYH7/k8osg46x42YaXeIG8pEmu0VBBqrgiPkbB50e0Yu7iP/SEKATNWx4HFDVhD95vqLNHHhC5R5hrW8YCgB8NxpuoOy6+cyjUNJVgXTsv1HbyQhDGeiy9DwXo9sw1RVgdf8D69bl9Lly4szzL4l9Ll9Viy5cvrcIdCEP4XqvUoEponJoxKhFWGuY9rqMZolebXsbmabqr6LB1SnlUs1sXGlNUvLf0dDIXT5QSgA1iASlYz3nf72joDojioN66w6jeEwwJVEDFDatj4hRb96viHR4dkfazO5tND+4FGHZ1jnT5St+1wwRmsB3lLRLEYGx5d+COPtftAgXQTbhgicjLjXPfM0TKUIWZrDUPBRdeo/JCEuoSY5rgZ/wCQWWiQYLaHggeUddhR7vW+ly5cuD1uX1WLFj+F9DoQhCH5V+T+BYQcgeqfn4jGMckcRSyXMViZt37BCemLWm5ex20nBtW9n+5prEq2qTbR/cPBqfrp8kCE2gJYP+z5IUM1IUnpFbLZYlJFsCvSCCm/m0UcoPSDTmXRdQANy6Gv/EH5RtDBNxzwQ6ODV5YbE5IDl+uWPhhEZq78PtKtuWTBdPtiZGLhvwv7QjMWAmHh5hDpcDC0TY6e7DDXwrVlfC32gy5cv87l9blxYvRei9L6EOpBg9TrfRBER8pf/JFGqPJAMU56LkbQRrat1xFTyIOn6TI9g9sRK9hHriClMWPYCaVmL7zamn2Ar7j7ceBMwVaET11gRiwTBMdg3OHWV4Dauw5HeWhd4uocpahHVYDiUaeXEf38w0exsf6pUaIxgrjP7h0IU0MaDABrUejEA5DSCOOJiNgpC8HwS4SjtReWUAMMDisD/bzChoncZPTSXMjTr0vF01zP/vS7RPeXL7S70631vosuXPTqv4D0vpmGkGDBly40gYSvepqAIm0xsCTgD2m0i9ITlPszLEtLYe6zX1lqG1Mo8v8AcyH5DQFYVzFYFVk8RjM18RrZsPzKNm3TwSlhYY96lslp9eh8spV/zTRbpM31AeWSEKjW6bsUrlbPeOYI2LHgGp3PaFzbDbeabzvFRbg7wmt+jBwPs5gytmVLTzf71idDGfKzcE6fP/yIDq473Io4B7EItvwAA/N9LjoawuICAom0FdVvFWiAkk9oHswpH1Uw5yTL2KIfCCHvlg21nLV8ynQvA/qD2D4gNE+k2SvMGyqa0Wcku+rL6L+FxZf4nQ6H46JuMxDYRGrH8GUC0bTyA09psQe6x0z0Onw9Ik+q+oTkNsJyK+kJRxLRPf3+0Oo6+kEFtYNTxz3moWtw3buUzAaE+JAAIPwGM+Vn2wsTEx94jFv7WHQ/U2GMMqkhXpxp4hPAg/I6JhuTJrH4MY9H8v/aAAwDAQACAAMAAAAQoAAAAswc0kchFgoM8sI4U0WLF8i+tUA6I8AAAAAEd9pAAAAAgg4gUpc4QUocokkAEwcdV33wYOkQBAAAAV99pIAAAAEUUoEt0MwwckEggsktEoI0gEcUUBAAAAE9199ooAAAA8cY8/fMdI4dg0gw04YIUk0UwMwAAAAUU99x99NAAAAAIU88FsZABY005lMU4wZI8Y4EpAAAE999/wD5ffaQAAEIKPPEVyBHBQKVAURFGBWXPCBCQABPffeYwwffXTCAGGNBFEEQVbTPIgDacBJBefqPGDDPKPfeR/vrw+ffPMOJBBHDcdfFGCN+ZIAPCPQ+oOlBNAHfeQ3qgkr0/wD33jhhxT3ExA1gRDhSwRBgwR2hQASRj33kNqIM8MK4vWW3CShwV2ACzRDASgJywwzTWRQAjBT3kN6IeMNMNK47zw4CFQHERszKQggjjQQ+rCygAhS4CQ/qIOMMO5MMK1TJAhEAESzss+wRiGRuhN5CiEoAwHZXMIOMJ77K5fJvIISyxcWgDBzzDymAARriQhCQIAbioHZIMd7L4Nr4JMOBjbJxwyDySx4wDgT8P8OwipZzRgKPsMJ6IIOIMb6tZaQJhigGghL/ALy8gmnf7SIIw4oswH/DDeiCHzOCvG+Lsg800oA4EscUfus+XzGwMsIw00E/rDG+CH/ynuSDGCcDQGkBWgAcUe6+pQaEo4AuoIoga/DCWqCfqCLzqWKT2uqw2MCY8sgKsc8cMUwSwoEswVbzDCW+DfCO/DmSjGC+uoO2qoAckalAghkC4I+oeYkQzDDCWyH6G+XrWWrCCSyQnYqYMJWUg4wwIAkAAocweKDCCCeCTCqzC/eSLCCCXTiOqiRwAEUksYs+0gEo0cS++OOeunyeP/q2fKrCCDfGi+qG0iUQeQMe6UYqa8A4GCHayyCiavTyuWbqzCSGe8Kmi2wLIY00eP6Koss+SegwsCSDGe3aC+/i+eeCeg9DgcCeABACCc8iiigAc+AcDeiDgC+/CC++/8QAJBEAAAQGAgMBAQAAAAAAAAAAAAEgMRARITBAQVFhUHGhYIH/2gAIAQMBAT8QSQA0HigAAAAuQvwoAAAAEIAAAAAAAADYBIgaDCgAAA5AYQGGCAUE1gAOyB2BkgAUgDZfAJBYSBQBQHmAA0CSqBPe/BwgAABOgJkQkCAMblgUHwAIACAOjEGIgGwQGJAMMEAPYWwAKA8OECgAAAAQAAAJmw4JiFAKgCcSAYaZioeBTqrUAwAAAoGKAAOwgdXhIBMKwCCCEmIM3Mz8AAQAAAAgHIBzEUBcM5SCMXUAABjNAAAAISFoQAAKjOAgAAjpICGC4AEDjNAAQAAKiMBMgNBZAB0ATAwcEhHwA7jgAAkOCIEuxYAIEGEQqEAcUPgCYYYAVLAAAAB7SwBAAGhYEBB70wwagAkBaAADMUGAMB8HEFQBpAH8FYAAAYO6KCAVIAnBkgAmD2KASASYAep5FSgAgOHlgCgAVDlAA5kQKhEISHxwAA4WQAKpywA4ByAlDCAAAGDsAxLTiQZEcQQZeL//xAAkEQAABAYCAwEBAAAAAAAAAAABEBExACAhMEBBUFFhgbFgcf/aAAgBAgEBPxC6AAeMAAAAAAAAKgIGHCgAAA8XAAAAAAAqcSAAAAAIAAAOgpwCAALUywAAKEwAwYChOADRGzDA3mHGAAAAAAcYQeSDl8AAAADmgAIIDnEAAAAAAARaWwAAN0AAgHF8FP4YGXQAATMgAAgAAOghiUA1KyAABAHFoHW0hEsbhyMGwigOTAAQElRQ3HQUBFVsNgAAANshANiYBUBYoAdIBsQtaEFZDQgtgAAAOGcJMcKAAgAAUMoPpguIJgDeiBQJABhhB2nAHqsAAAIsFEAZGmcAALYA6AIX0Y2yYAelIAHAnYLAABrgAABLIANmSMMAAQlgAaVAUOKBEAAAaZg4N4AAHn8KAAAdDuWAB//EACoQAAEDBAEDAwQDAQAAAAAAABEBMDEAIEBQECFBYFFhcYGRobFwwdHw/9oACAEBAAE/EPEE6KIAEAACiAhEAgDIAAhEAAECAhFAGIIgAEAAIIQAAIQJBAIACoAIIAAIAAAAAAogAICAALwJyNKwAUAAAARQCMAAQBFgAAIIAAhgKkEUFAokAggRAggJCAAIQAFB4EAAAABAAAENAAQIQAEBSgbgHAJAAAAIoQQQCAAAAAkQAKSAgRAEJAIBCgUqQgBAiIgoQAIAAAwQAAAAAAAQIAIhkAoIzpmgJgQXQDq+/wDCYAAFFEAAAAVAMMABAIAIIAAwCgUQBRECEAAAhAYAAAQDQIEAQEAAAAACIAAIzBCAAoBDDoDpAAEQAIDAAABQBAAAgMMBEIBBAkClAAQAAQFGQEAKUBABgIASCCCiQEFAMQEgABUDAUSAgAAAAEA52CBvPB36MMkUEZAAACkAAECIBAACEAAAJASAoiCoCoAAICAAlAAKAwAAgyAAAVQChChCAAFgAACgAAEQAAEAomQAAgACqGAaAoAoIQAkQJgBYpFYNHFHUFGbIQAIIiQKQAEAAAAAAKAAAQAAAiCiCoQABUAIIIAACAQoAhAEAggIMAAlEAQgMJAEAAFABoACFEAIgAI4AAAABIFUAAoAE+MqBAAAQx8ALZQYjfQGuBQCgxUAQghIEAAAAEAAKICJAqgAEAFFBFAgCAxMBAECoBgQAAICoCCBQABAAAhUABAAAAAEBRhICAGKAAUAAAAwiACzEQAPggnDEACIOAAImgBQCoICAAACgAAAQEAoAAcCgIUAAEAAgBhABABAFQQAVABAADAUEEQAAQcAgDMYEgNEQKAFBAkCCAAFhNmIgCKYAACIAAAAAAEAAQAAEMEgAoABEICCiIUAFAAEIGAMAAAqAEQAAAAREAAACBMwUYABEALwDlAnLZBIAKmAABWAAAAKAAABBIKoEAigIAgQCACFACAIAgqAAqAAAIAECgAoUAEwECAAgAEgIAAQXgRJ4BPCwoLgAQSABQNQAAABAAAgoAKGGAAGAAACAiBCAoACAqiIEgECKOIAhAAAEdABAgYABAvABAAl4wAIDACAAUQqAAABIAAAAZAMQACAQQQUQQBSAAAAAUAAgABUAREISjCAAMCAAAAQWBIQAERBBK0QQ0ePqEls0AgTAAIAgAAAABAAEOIASAACCQDuIQBIBmAAAAAcZABQAQAYAAAMDAAAAEAUAIABVIBVtLhLRCwdPuExAFMAAEAAAASAAAAgIAFAABDAogWXtaDqAAACAIAACAIALABiYAAEARWQAmBAABACAAGBgAgiAAIMKkZAwAABEAgATABAAAJAAAQAAEBQAARCAIBdAAATwGe4AEABAAogBUQBABUAYFTOBAAAAgiAJEAQACAABUMA0XAdGC7xgFRAUEwKJABhAAAAQQAAEAQAEgSBnBm9AIAAA4AACgQAK4AFAAAAwgIBSEgAUUk+dM14AA4QAAAVGAAAUDAIACEwIBEidgAUAAAJgAB1BeLAAETuQQQCQFCQAFDAAAAAARBEAAgAAABAIkHwgiXIBVGcAAkAmVq0SAABAICAAAEAhAEpEQCIgEgwACIAoBYAIILABAAAIIgAACIAFgWAAAIGABEADaHEAqBmAAEYAUFIMBQBEYEB2zgAAgAAAAICCURECIDABRAKAQRkAAAbEAoCDAAEQIIgAgQOAIgrIYAUAAABEoIQIEQBCCX9MggKADoFEAoBAhDgAANQodgAgAApACGAAAG0DIXJNAAAYAFAQE8QMgAAgAIBBAIACEEAAAAQAAACoAhBgBBGIEATAG/o58CoEBQThgCAgAIgsQFACIAAAACAAEAC2tZkAAik0AUAYABREMAAFQAqEASwSAgAGYAEHeQCqE8AAApYAAAOiABRAxDIAFAYgKUARhAKBwAAgggAAAQAAEBAABdMQBQCAEOQQQABhDQAkYgAUAYIACB8gFCgBUOBQDRAQURmgIgmwAqBmQAQEQgBBDCBS0AAQADL0jAbNEiAAohAAAAAhACAIVJ8QAAAAAapNAA4FCECiMYAAgACICgABYG0HGACAYITYAARogEBYgDYIyAFAGAAEMAQAAIBACAAAQoZHTAAAASRBIEIQgQCZEEBAuHFABASEHbPSBAAGI4BELgAFDICoAAVQQACAIBAAYIAFBQZI4EAYGCqMBEMABCTAAFUgAIEAAg2BAgCAAgLgsQAEAEQCBcAVCwAXCgC2AsAbjATiAoACAEgRDS15K4ggUHAAAAIwBCDAKBC0AIA8sAOABFEIQjGyoCEAiIgEImERIAUDgAAAAgGAKGDtZB8WABQcABEGAAAhMBEAAASgjIKIRgBAAAghAAAmAAAYAEnioAAAgIgTCADBlgAwNwDVkYobQGMAcEAFimVtKESTmQAEZAKw1ADVeIQAGAP4AAIBEAACAAAYQyKBAAEAQAAkIhIBYKQJkAAACAAGTHAAKYAAwAUAAiIEQAoG0ASBiCkCAgcoABSIAAIBkAzHDCPBFEAgCAQNRACAQKACIAFQAAABAQCoOCnTwagACtpgYABRBiAQYIgAAMUR8GAtgNg6dQRCDDICgCSKAADRMgAAAwAABCjAAKAAAAgQAAACFBAAngKACAAERKIABEGJQRAYQDgzAAUBwEHMACCAAEQQCgggEwFEwo4Ei0AhACACBgQEBIYpEQFEDoAAiYAARBAEQABJBEABBK8GQAQHBFCgUAkZREgE6HwXYEYMYQCtdAAuwGMWAogAAABaQCEAKJhAJgBkADACkApg768AIBiIIKQCCJAFAChQAgICQBIBAIDQQGRAOYGkAASCCoBABgAQggEUMAQICRAEAiAHBSIIBAAQAEAKPAAFXgEBEAAAAx4BIqDQAAAGCBYNbMgBUCQQBMBUPIBgQAMAAHxcFEgIpiAAFQ4CHIAAIwAYAAgQBABAKAIAAQ4EAiAFAoAAHAEBAIAAAFBQAIogBQAEEMAoGCABBOAAEgAAKAMAUAIAADQChgASAQoBAALAAAJQQAYAFsXRoCA4BH2GwQBFNgAClULLwEBBoAAAAgAQEAgAAiQAUgBIESQGAACo9AAEKAAFQAAoMMAEAOQCGqAQEEAAAAgCjQAAMAImgiCACAYAIAlaBtEIxQIhgAoMIBU6uke7OA2DQIAAAEgAAQYAAAgRAwgAAACIAzgAAAUQIACiQEIIIAIQwAoJAApEAABAEKOXgEQEAIynOFQkAFgA2DQEBPjAAXb39EBVIiFUdgCkAANBoMAIwNR4AAgAIgZAIkABAAAAoEwAhkEhgwEAAUACAAAFC0AgoJgAAAEA+DLzQFQAEYDADAMMAQAAk0EAoAAoMAEAVQASkQABXgZCAJAhABuQFgAAJUECgAAQARAQEAIgUhwBBLACIgAAAQIqAkAqEMgBIAAAACESAgyABAAcCABEKAqAAuAKAsAFOewFMANUxkAAAQAAACgAAACAAAAFUACoAApCIAAgEsIIBIAIiQAAEODpQABAISgxBAAoMBAEAT6gCAABRgAAhEAEJQAKACyBKwCBJUHw0PIABACAEAgQAIFAFACDIAAAKIWAKIgwAAAAQEgCIoIBRgECAgQAACACAQoAcACiNAEADQEAAgAAKjACDABANAKAGCI4QAAhntUJZgGiMEAQDkAABAACCqBHwBREIKjMgFBiBIYiAEBCiBgAAAABANACAGIAX4gjgKgAgRrmEAAgDghAQCQQgDAA6gADMBQAQBLA1gBQCpCQMCIwAAAAAeMgAgBAQDKBBEngAgAAIAAAAFAXAULgAAC6rgAICsUkAAAAAAsAAIALBoCAgIBQCAQAIBIGCKhAIABAZAXAIAMsSAA3Q5QI4KgGBhvgFEN4AHGQwAACUEcgUCAAQZACgRIBUSAAAKQTABFRBDAIAgAA0ABFIABQgIERQYAMKIAJosCNSQAKIEEAABQAUAIBBGMAIIgFgQUgIIACDARgAAijCAoIAAAAaIAAKCAArgAK7ckEAsxppBoYACKIAFFAFRmEIA+ABUyAACABGiCIEKIgIEQQwCogCkiPMAAAABADCxCAEBI4BVEwAASiAgABAJiAAAIAAIIVAAgYgOxUjGBYSoBI0AGMALAhcNGjoQAFUDACFgQACOAIjIQAAAAAAQQAiNABQCAAChQEKBIwAEEAAFwUIgCAQgAgcFEGAEAAgYRRCAAogEAAWW2R5gUgBAAAEAAUAVBAQcgAQIAAADCwAAgYAAjAAMRGw4wAEC85AIDJyIAAIA0AVRywJgAIhAAAFEQEEqLARqiMwAIIiAACoIBaAwAiEBBPQUCYACiQQAAhAAEEALpmAAIoAEDdY8AABVgAAQAUQQEAAgjgAAKPACgqAQtACpaQABADEABPYA3iAAEQYYAAoAAUQmgIAhDKAwAAKIaAYjKQAgACOEABAFQAAAKCiQAofAAyIADrm/HsABB4CGAUGABUiAARKhCGSIgAASBwAcA3gAIBHYBUOBAIAAEcQQARBQEgoKAILwgAFQMUAKyQQ0ijGAoGAAKDgACAAYAVGCGqoYAAEOEsQAKEIAQCYAIENCAIAgMEEQDAAFHIwBAAIBTSAABAQAgRCRgAQACSCgABBCAAJEAkARSEFUEAAEGAKJgAgPoAAApAAUAb1AYCyEAAWAANgWgBDiTZCwNgCACgdiBDSBgAYAAIDAAACoAgGEAEKgCoFAaAAQBQIBIEBqIUAoABBCiAAAgAokIgCoYwOAAgDgAgIBVFEAAFoCqUgAOhwAGjwN4BYAJcngeUMUBVGAIZwIVAAIAEQjiAEAACBAIABKIhEFEQAIQAAQAQAQAACIRyACIAAVDAJR0AAIgAESCJgaQBCpaFggAAiAgAAABLBABACAIhyVUFggBATAAIcAhAwehSyACkAigAACAgCIIAAAIAECQSAAACEmAQAoAAKAEEQwASJwASwCAGCICEyAAAAICA0bwDaHgOsAAGAEgwMAkCYCRBACkCiAUNwQAQAAEhIAEEBAABIgFGAAAICABCiABIgQBEUQAUEgAJOiCuAUQBQCiZAAgAAM4AFAYUAUigINAtB9iB2QEAkMIAAOgokAGwLQIMH/ZV0WT4VOq2oUoBNEX7Sr/AE/Fe3gE+5cAB0pWp0BnHrXQP1tAj9nwf8aW0J0mqvfofQPvSd5d6pe+nrUjp3J+An2r0e5RGAJOhbt+AVDz62gEdFUnlHX4vRFexnCR+mHqlr3mv6islwgw/9k=" + } + created: 1582058281984 + description: "" + headers: + - description: "" + id: pair_314d74af59264ca4863e4f61e54e9484 + name: Authorization + value: "{{authorization}}" + - description: "" + id: pair_c1e909e6c80b48318872dab2e8d5b4ba + name: Content-Type + value: application/json + - description: "" + id: pair_f4fa3371ec2e4a50a1f6931515cc1b1c + name: Accept + value: application/json + isPrivate: false + metaSortKey: -1582058273761 + method: POST + modified: 1582058751132 + name: Create Webhook in Channel + parameters: [] + parentId: fld_de2969c9c2314e359cc340bc1ae19fb1 + settingDisableRenderRequestBody: false + settingEncodeUrl: true + settingFollowRedirects: global + settingRebuildPath: true + settingSendCookies: true + settingStoreCookies: true + url: "{{api_url}}/channels/{% prompt 'channel_id', 'channel_id', '', '', false, + true %}/webhooks" + _type: request + - _id: fld_de2969c9c2314e359cc340bc1ae19fb1 + created: 1582058239990 + description: "" + environment: {} + environmentPropertyOrder: null + metaSortKey: -1576850327804 + modified: 1582058256001 + name: Webhook + parentId: fld_fd85c020b1744299b6016ba69c25966b + _type: request_group + - _id: req_e550b6417a82446d9b0e6b30a81a79c7 + authentication: {} + body: + mimeType: application/json + text: "" + created: 1582058762936 + description: "" + headers: + - name: Authorization + value: "{{authorization}}" + - name: Accept + value: application/json + isPrivate: false + metaSortKey: -1580806035624 + method: GET + modified: 1582058774812 + name: Get Channel Webhooks + parameters: [] + parentId: fld_de2969c9c2314e359cc340bc1ae19fb1 + settingDisableRenderRequestBody: false + settingEncodeUrl: true + settingFollowRedirects: global + settingRebuildPath: true + settingSendCookies: true + settingStoreCookies: true + url: "{{api_url}}/channels/{% prompt 'channel_id', 'channel_id', '', '', false, + true %}/webhooks" + _type: request + - _id: req_b00ac8dcdc33452c95724656eeb1b4eb + authentication: {} + body: {} + created: 1582663055224 + description: "" + headers: [] + isPrivate: false + metaSortKey: -1576852151202 + method: GET + modified: 1582663078532 + name: Status + parameters: [] + parentId: fld_c375a668904e4e6a9277055481c17ab1 + settingDisableRenderRequestBody: false + settingEncodeUrl: true + settingFollowRedirects: global + settingRebuildPath: true + settingSendCookies: true + settingStoreCookies: true + url: https://status.discordapp.com/api/v2/status.json + _type: request + - _id: fld_c375a668904e4e6a9277055481c17ab1 + created: 1577108885767 + description: "" + environment: {} + environmentPropertyOrder: null + metaSortKey: -1576850327779 + modified: 1582058254195 + name: Uncategorised calls + parentId: fld_fd85c020b1744299b6016ba69c25966b + _type: request_group - _id: req_6ba6849bdad6462ea46ed8dc75b96514 authentication: {} body: {} @@ -796,16 +1053,6 @@ resources: settingStoreCookies: true url: "{{base_url}}/cdn-cgi/trace" _type: request - - _id: fld_c375a668904e4e6a9277055481c17ab1 - created: 1577108885767 - description: "" - environment: {} - environmentPropertyOrder: null - metaSortKey: -1576850327779 - modified: 1577108904373 - name: Uncategorised calls - parentId: fld_fd85c020b1744299b6016ba69c25966b - _type: request_group - _id: env_29ed27f9c562933de7fccb4654fee3698ef432f8 color: null created: 1572465096240 @@ -818,9 +1065,33 @@ resources: parentId: wrk_ef4732eebfd640729709f9377913ae41 _type: environment - _id: jar_29ed27f9c562933de7fccb4654fee3698ef432f8 - cookies: [] + cookies: + - creation: 2020-01-26T18:05:56.711Z + domain: discordapp.com + expires: 2020-03-26T20:37:45.000Z + extensions: + - SameSite=Lax + hostOnly: false + httpOnly: true + id: "8737133547435112" + key: __cfduid + lastAccessed: 2020-02-25T20:37:45.414Z + path: / + value: d5d0adf12c22116d6c39bf9879465e57b1582663065 + - creation: 2020-01-26T18:05:56.711Z + domain: discordapp.com + extensions: + - SameSite=None + hostOnly: false + httpOnly: true + id: "6248571442813258" + key: __cfruid + lastAccessed: 2020-03-12T10:25:09.692Z + path: / + secure: true + value: 8a85b3de134221943df192595d5c4eb1d8f2770f-1584008709 created: 1572465096242 - modified: 1579888936589 + modified: 1584008709692 name: Default Jar parentId: wrk_ef4732eebfd640729709f9377913ae41 _type: cookie_jar diff --git a/noxfile.py b/noxfile.py index 719356d956..25781c9115 100644 --- a/noxfile.py +++ b/noxfile.py @@ -81,20 +81,22 @@ def documentation(session) -> None: session.install("-r", "requirements.txt") session.install("-r", "dev-requirements.txt") session.install("-r", "doc-requirements.txt") + session.env["SPHINXOPTS"] = SPHINX_OPTS + tech_dir = pathify(DOCUMENTATION_DIR, TECHNICAL_DIR) shutil.rmtree(tech_dir, ignore_errors=True, onerror=lambda *_: None) os.mkdir(tech_dir) - session.env["SPHINX_IS_GENERATING_DOCUMENTATION"] = "true" - session.run( - "python", - GENDOC_PATH, - ".", - MAIN_PACKAGE, - pathify(DOCUMENTATION_DIR, "_templates", "gendoc"), - pathify(DOCUMENTATION_DIR, "index.rst"), - pathify(DOCUMENTATION_DIR, TECHNICAL_DIR), - ) + # session.run( + # "python", + # GENDOC_PATH, + # ".", + # MAIN_PACKAGE, + # pathify(DOCUMENTATION_DIR, "_templates", "gendoc"), + # pathify(DOCUMENTATION_DIR, "index.rst"), + # pathify(DOCUMENTATION_DIR, TECHNICAL_DIR), + # ) + session.run("sphinx-apidoc", "-e", "-o", tech_dir, MAIN_PACKAGE) session.run("python", "-m", "sphinx.cmd.build", DOCUMENTATION_DIR, ARTIFACT_DIR, "-b", "html") From d308959d636971b455a296fd3c1e540664240a95 Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Mon, 23 Mar 2020 11:24:09 +0000 Subject: [PATCH 023/922] Fixed docstring typo in ratelimits.py [skip deploy] --- hikari/net/ratelimits.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/hikari/net/ratelimits.py b/hikari/net/ratelimits.py index 39fe90107e..c0d791d6de 100644 --- a/hikari/net/ratelimits.py +++ b/hikari/net/ratelimits.py @@ -918,8 +918,8 @@ class ExponentialBackOff: single iteration before an :obj:`asyncio.TimeoutError` is raised. Defaults to ``64`` seconds. jitter_multiplier : :obj:`float` - The multiplier for the random jitter. Defaults to ``1``. Set to ``0`` to disable - jitter. + The multiplier for the random jitter. Defaults to ``1``. + Set to ``0`` to disable jitter. """ __slots__ = ("base", "increment", "maximum", "jitter_multiplier") @@ -940,8 +940,8 @@ class ExponentialBackOff: #: :type: :obj:`float`, optional maximum: typing.Optional[float] - #: The multiplier for the random jitter. Defaults to ``1`. Set to ``0``` to disable - #: jitter. + #: The multiplier for the random jitter. Defaults to ``1``. + #: Set to ``0`` to disable jitter. #: #: :type: :obj:`float` jitter_multiplier: float From b2f9add47550ef9374f6318c9a254ce5b019bea6 Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Mon, 23 Mar 2020 11:27:33 +0000 Subject: [PATCH 024/922] Update ratelimits.py to fix other typos in docstrings [skip deploy] --- hikari/net/ratelimits.py | 45 +++++++++++++++++++++------------------- 1 file changed, 24 insertions(+), 21 deletions(-) diff --git a/hikari/net/ratelimits.py b/hikari/net/ratelimits.py index c0d791d6de..320de86794 100644 --- a/hikari/net/ratelimits.py +++ b/hikari/net/ratelimits.py @@ -75,7 +75,7 @@ Initially acquiring time on a bucket ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Each time you :meth:`hikari.net.ratelimits.RateLimiter.acquire` a request +Each time you :meth:`IRateLimiter.acquire` a request timeslice for a given :obj:`hikari.net.ratelimits.CompiledRoute`, several things happen. The first is that we attempt to find the existing bucket for that route, if there is one, or get an unknown bucket otherwise. This is done @@ -87,14 +87,13 @@ provide the :obj:`RealBucketHash` we need to get the route's bucket object internally. -The :meth:`hikari.net.ratelimits.RateLimiter.acquire` method will take the -bucket and acquire a new timeslice on it. This takes the form of a -:obj:`asyncio.Future` which should be awaited by the caller and will complete -once the caller is allowed to make a request. Most of the time, this is done -instantly, but if the bucket has an active rate limit preventing requests being -sent, then the future will be paused until the rate limit is over. This may be -longer than the rate limit period if you have queued a large number of requests -during this limit, as it is first-come-first-served. +The :meth:`acquire` method will take the bucket and acquire a new timeslice on +it. This takes the form of a :obj:`asyncio.Future` which should be awaited by +the caller and will complete once the caller is allowed to make a request. Most +of the time, this is done instantly, but if the bucket has an active rate limit +preventing requests being sent, then the future will be paused until the rate +limit is over. This may be longer than the rate limit period if you have queued +a large number of requests during this limit, as it is first-come-first-served. Acquiring a rate limited bucket will start a bucket-wide task (if not already running) that will wait until the rate limit has completed before allowing more @@ -120,24 +119,28 @@ the vital rate limit headers manually and parse them to the correct data types. These headers are: -* ``Date``: the response date on the server. This should be parsed to a +* ``Date``: + the response date on the server. This should be parsed to a :obj:`datetime.datetime` using :func:`email.utils.parsedate_to_datetime`. -* ``X-RateLimit-Limit``: an :obj:`int` describing the max requests in the bucket +* ``X-RateLimit-Limit``: + an :obj:`int` describing the max requests in the bucket from empty to being rate limited. -* ``X-RateLimit-Remaining``: an :obj:`int` describing the remaining number of +* ``X-RateLimit-Remaining``: + an :obj:`int` describing the remaining number of requests before rate limiting occurs in the current window. -* ``X-RateLimit-Bucket``: a :obj:`str` containing the initial bucket hash. -* ``X-RateLimit-Reset``: a :obj:`float` containing the number of seconds since +* ``X-RateLimit-Bucket``: + a :obj:`str` containing the initial bucket hash. +* ``X-RateLimit-Reset``: + a :obj:`float` containing the number of seconds since 1st January 1970 at 0:00:00 UTC at which the current ratelimit window resets. This should be parsed to a :obj:`datetime` using :func:`datetime.datetime.fromtimestamp`, passing :obj:`datetime.timezone.utc` as a second parameter. Each of the above values should be passed to the -:meth:`hikari.net.ratelimits.RateLimiter.update_rate_limits` method -to ensure that the bucket you acquired time from is correctly updated should -Discord decide to alter their ratelimits on the fly without warning (including -timings and the bucket). +:meth:`update_rate_limits` method to ensure that the bucket you acquired time +from is correctly updated should Discord decide to alter their ratelimits on the +fly without warning (including timings and the bucket). This method will manage creating new buckets as needed and resetting vital information in each bucket you use. @@ -145,14 +148,14 @@ Tidying up ~~~~~~~~~~ -To prevent unused buckets cluttering up memory, each :obj:`RateLimiter` +To prevent unused buckets cluttering up memory, each :obj:`IRateLimiter` instance spins up a :obj:`asyncio.Task` that periodically locks the bucket list (not threadsafe, only using the concept of asyncio not yielding in regular functions) and disposes of any clearly stale buckets that are no longer needed. These will be recreated again in the future if they are needed. -When shutting down an application, one must remember to :meth:`close` the -:obj:`RateLimiter` that has been used. This will ensure the garbage collection +When shutting down an application, one must remember to :meth`close` the +:obj:`IRateLimiter` that has been used. This will ensure the garbage collection task is stopped, and will also ensure any remaining futures in any bucket queues have an :obj:`asyncio.CancelledError` set on them to prevent deadlocking ratelimited calls that may be waiting to be unlocked. From dd5b50629acb15abc24a7f1307f2d8fd82a95b3c Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Mon, 23 Mar 2020 12:26:36 +0000 Subject: [PATCH 025/922] Even more CSS fixes for documentation [skip deploy] --- docs/_static/style.css | 20 ++++++++++++++++++-- docs/conf.py | 4 ++-- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/docs/_static/style.css b/docs/_static/style.css index 063a231123..d4c56d51bc 100644 --- a/docs/_static/style.css +++ b/docs/_static/style.css @@ -91,11 +91,14 @@ dt:target, span.highlighted { input[type="text"] { background-color: #555; color: #fff; - - border-color: #777; + border-color: #333 !important; border-radius: 3px; } +form.navbar-form { + padding: 0 !important; +} + .alert { border-color: transparent !important; } @@ -249,6 +252,19 @@ html { color: #fff !important; } +.dropdown-menu { + background-color: #555 !important; +} + +.dropdown-menu>li>a:focus, .dropdown-menu>li>a:hover { + background-color: #666 !important; + background-image: none !important; +} + +.divider { + background-color: #333 !important; +} + .dropdown-menu .caption { padding-left: 1em; } diff --git a/docs/conf.py b/docs/conf.py index 59744a705e..be0785c636 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -106,7 +106,7 @@ # Render the next and previous page links in navbar. (Default: true) "navbar_sidebarrel": True, # Render the current pages TOC in the navbar. (Default: true) - "navbar_pagenav": False, + "navbar_pagenav": True, # Tab name for the current pages TOC. (Default: "Page") "navbar_pagenav_name": "This page", # Global TOC depth for "site" navbar tab. (Default: 1) @@ -125,7 +125,7 @@ "navbar_class": "navbar navbar-inverse", # Fix navigation bar to top of page? # Values: "true" (default) or "false" - "navbar_fixed_top": "true", + "navbar_fixed_top": "false", # Location of link to source. # Options are "nav" (default), "footer" or anything else to exclude. "source_link_position": "footer", From 853b16d7a6f6cb2e20764bd91bb780100ed71169 Mon Sep 17 00:00:00 2001 From: Faster Speeding Date: Sat, 28 Mar 2020 16:17:28 +0000 Subject: [PATCH 026/922] Fix un-awaited tasks/coroutines in mocks. --- hikari/net/ratelimits.py | 2 +- tests/hikari/net/test_http_client.py | 149 +++++++++++++-------------- tests/hikari/net/test_ratelimits.py | 17 ++- 3 files changed, 87 insertions(+), 81 deletions(-) diff --git a/hikari/net/ratelimits.py b/hikari/net/ratelimits.py index 320de86794..a648e6b0a8 100644 --- a/hikari/net/ratelimits.py +++ b/hikari/net/ratelimits.py @@ -943,7 +943,7 @@ class ExponentialBackOff: #: :type: :obj:`float`, optional maximum: typing.Optional[float] - #: The multiplier for the random jitter. Defaults to ``1``. + #: The multiplier for the random jitter. Defaults to ``1``. #: Set to ``0`` to disable jitter. #: #: :type: :obj:`float` diff --git a/tests/hikari/net/test_http_client.py b/tests/hikari/net/test_http_client.py index 5493c9b72b..36976ff79a 100644 --- a/tests/hikari/net/test_http_client.py +++ b/tests/hikari/net/test_http_client.py @@ -181,32 +181,38 @@ async def test_close(self, http_client_impl): mock_close.assert_called_once_with() http_client_impl.ratelimiter.close.assert_called_once_with() - @pytest.mark.asyncio - async def test__request_acquires_ratelimiter(self, compiled_route, exit_error): + @pytest.fixture() + @mock.patch.object(base_http_client.BaseHTTPClient, "__init__") + @mock.patch.object(ratelimits, "ManualRateLimiter") + @mock.patch.object(ratelimits, "HTTPBucketRateLimiterManager") + async def mock_http_client(self, *args): http_client_impl = http_client.HTTPClient(token="Bot token") - http_client_impl.logger = mock.MagicMock(debug=mock.MagicMock(side_effect=exit_error)) + http_client_impl.logger = mock.MagicMock(debug=mock.MagicMock()) http_client_impl.ratelimiter = mock.MagicMock() http_client_impl.global_ratelimiter = mock.MagicMock() + return http_client_impl + + @pytest.mark.asyncio + async def test__request_acquires_ratelimiter(self, compiled_route, exit_error, mock_http_client): + mock_http_client = await mock_http_client + mock_http_client.logger.debug.side_effect = exit_error with mock.patch("asyncio.gather", return_value=_helpers.AwaitableMock()): try: - await http_client_impl._request(compiled_route) + await mock_http_client._request(compiled_route) except exit_error: pass - http_client_impl.ratelimiter.acquire.asset_called_once_with(compiled_route) + mock_http_client.ratelimiter.acquire.asset_called_once_with(compiled_route) @pytest.mark.asyncio - async def test__request_sets_Authentication_if_token(self, compiled_route, exit_error): - http_client_impl = http_client.HTTPClient(token="Bot token") - http_client_impl.logger = mock.MagicMock(debug=mock.MagicMock(side_effect=[None, exit_error])) - http_client_impl.ratelimiter = mock.MagicMock() - http_client_impl.global_ratelimiter = mock.MagicMock() - + async def test__request_sets_Authentication_if_token(self, compiled_route, exit_error, mock_http_client): + mock_http_client = await mock_http_client + mock_http_client.logger.debug.side_effect = [None, exit_error] with mock.patch("asyncio.gather", return_value=_helpers.AwaitableMock()): with mock.patch.object(base_http_client.BaseHTTPClient, "_request") as mock_request: try: - await http_client_impl._request(compiled_route) + await mock_http_client._request(compiled_route) except exit_error: pass @@ -221,17 +227,15 @@ async def test__request_sets_Authentication_if_token(self, compiled_route, exit_ @pytest.mark.asyncio async def test__request_doesnt_set_Authentication_if_suppress_authorization_header( - self, compiled_route, exit_error + self, compiled_route, exit_error, mock_http_client ): - http_client_impl = http_client.HTTPClient(token="Bot token") - http_client_impl.logger = mock.MagicMock(debug=mock.MagicMock(side_effect=[None, exit_error])) - http_client_impl.ratelimiter = mock.MagicMock() - http_client_impl.global_ratelimiter = mock.MagicMock() + mock_http_client = await mock_http_client + mock_http_client.logger.debug.side_effect = [None, exit_error] with mock.patch("asyncio.gather", return_value=_helpers.AwaitableMock()): with mock.patch.object(base_http_client.BaseHTTPClient, "_request") as mock_request: try: - await http_client_impl._request(compiled_route, suppress_authorization_header=True) + await mock_http_client._request(compiled_route, suppress_authorization_header=True) except exit_error: pass @@ -245,39 +249,39 @@ async def test__request_doesnt_set_Authentication_if_suppress_authorization_head ) @pytest.mark.asyncio - async def test__request_sets_X_Audit_Log_Reason_if_reason(self, compiled_route, exit_error): - http_client_impl = http_client.HTTPClient(token=None) - http_client_impl.logger = mock.MagicMock(debug=mock.MagicMock(side_effect=[None, exit_error])) - http_client_impl.ratelimiter = mock.MagicMock() - http_client_impl.global_ratelimiter = mock.MagicMock() + async def test__request_sets_X_Audit_Log_Reason_if_reason(self, compiled_route, exit_error, mock_http_client): + mock_http_client = await mock_http_client + mock_http_client.logger.debug.side_effect = [None, exit_error] with mock.patch("asyncio.gather", return_value=_helpers.AwaitableMock()): with mock.patch.object(base_http_client.BaseHTTPClient, "_request") as mock_request: try: - await http_client_impl._request(compiled_route, reason="test reason") + await mock_http_client._request(compiled_route, reason="test reason") except exit_error: pass mock_request.assert_called_with( "get", "https://discordapp.com/api/v6/somewhere", - headers={"X-RateLimit-Precision": "millisecond", "X-Audit-Log-Reason": "test reason"}, + headers={ + "X-RateLimit-Precision": "millisecond", + "Authorization": "Bot token", + "X-Audit-Log-Reason": "test reason", + }, json=None, params=None, data=None, ) @pytest.mark.asyncio - async def test__request_updates_headers_with_provided_headers(self, compiled_route, exit_error): - http_client_impl = http_client.HTTPClient(token="Bot token") - http_client_impl.logger = mock.MagicMock(debug=mock.MagicMock(side_effect=[None, exit_error])) - http_client_impl.ratelimiter = mock.MagicMock() - http_client_impl.global_ratelimiter = mock.MagicMock() + async def test__request_updates_headers_with_provided_headers(self, compiled_route, exit_error, mock_http_client): + mock_http_client = await mock_http_client + mock_http_client.logger.debug.side_effect = [None, exit_error] with mock.patch("asyncio.gather", return_value=_helpers.AwaitableMock()): with mock.patch.object(base_http_client.BaseHTTPClient, "_request") as mock_request: try: - await http_client_impl._request( + await mock_http_client._request( compiled_route, headers={"X-RateLimit-Precision": "nanosecond", "Authorization": "Bearer token"} ) except exit_error: @@ -293,7 +297,7 @@ async def test__request_updates_headers_with_provided_headers(self, compiled_rou ) @pytest.mark.asyncio - async def test__request_resets_seek_on_seekable_resources(self, compiled_route, exit_error): + async def test__request_resets_seek_on_seekable_resources(self, compiled_route, exit_error, mock_http_client): class SeekableResource: seeked: bool = False @@ -303,15 +307,13 @@ def seek(self, _): def assert_seek_called(self): assert self.seeked - http_client_impl = http_client.HTTPClient(token=None) - http_client_impl.logger = mock.MagicMock(debug=mock.MagicMock(side_effect=exit_error)) - http_client_impl.ratelimiter = mock.MagicMock() - http_client_impl.global_ratelimiter = mock.MagicMock() + mock_http_client = await mock_http_client + mock_http_client.logger.debug.side_effect = exit_error seekable_resources = [SeekableResource(), SeekableResource(), SeekableResource()] with mock.patch("asyncio.gather", return_value=_helpers.AwaitableMock()): try: - await http_client_impl._request(compiled_route, re_seekable_resources=seekable_resources) + await mock_http_client._request(compiled_route, re_seekable_resources=seekable_resources) except exit_error: pass @@ -323,68 +325,65 @@ def assert_seek_called(self): "content_type", ["text/plain", "text/html"], ) async def test__request_handles_bad_response_when_content_type_is_plain_or_htlm( - self, content_type, exit_error, compiled_route, discord_response + self, content_type, exit_error, compiled_route, discord_response, mock_http_client ): discord_response.headers["Content-Type"] = content_type - http_client_impl = http_client.HTTPClient(token="Bot token") - http_client_impl.ratelimiter = mock.MagicMock() - http_client_impl.global_ratelimiter = mock.MagicMock() - http_client_impl._handle_bad_response = mock.AsyncMock(side_effect=[None, exit_error]) + mock_http_client = await mock_http_client + mock_http_client._handle_bad_response = mock.AsyncMock(side_effect=[None, exit_error]) with mock.patch("asyncio.gather", return_value=_helpers.AwaitableMock()): with mock.patch.object(base_http_client.BaseHTTPClient, "_request", return_value=discord_response): try: - await http_client_impl._request(compiled_route) + await mock_http_client._request(compiled_route) except exit_error: pass - http_client_impl._handle_bad_response.assert_called() + mock_http_client._handle_bad_response.assert_called() @pytest.mark.asyncio - async def test__request_when_invalid_content_type(self, compiled_route, discord_response): + async def test__request_when_invalid_content_type(self, compiled_route, discord_response, mock_http_client): discord_response.headers["Content-Type"] = "something/invalid" - http_client_impl = http_client.HTTPClient(token="Bot token") - http_client_impl.ratelimiter = mock.MagicMock() - http_client_impl.global_ratelimiter = mock.MagicMock() + mock_http_client = await mock_http_client with mock.patch("asyncio.gather", return_value=_helpers.AwaitableMock()): with mock.patch.object(base_http_client.BaseHTTPClient, "_request", return_value=discord_response): - assert await http_client_impl._request(compiled_route, json_body={}) is None + assert await mock_http_client._request(compiled_route, json_body={}) is None @pytest.mark.asyncio - async def test__request_when_TOO_MANY_REQUESTS_when_global(self, compiled_route, exit_error, discord_response): + async def test__request_when_TOO_MANY_REQUESTS_when_global( + self, compiled_route, exit_error, discord_response, mock_http_client + ): discord_response.status = 429 discord_response.raw_body = '{"retry_after": 1, "global": true}' - http_client_impl = http_client.HTTPClient(token="Bot token") - http_client_impl.ratelimiter = mock.MagicMock() - http_client_impl.global_ratelimiter = mock.MagicMock(throttle=mock.MagicMock(side_effect=[None, exit_error])) + mock_http_client = await mock_http_client + mock_http_client.global_ratelimiter.throttle = mock.MagicMock(side_effect=[None, exit_error]) with mock.patch("asyncio.gather", return_value=_helpers.AwaitableMock()): with mock.patch.object(base_http_client.BaseHTTPClient, "_request", return_value=discord_response): try: - await http_client_impl._request(compiled_route) + await mock_http_client._request(compiled_route) except exit_error: pass - http_client_impl.global_ratelimiter.throttle.assert_called_with(0.001) + mock_http_client.global_ratelimiter.throttle.assert_called_with(0.001) @pytest.mark.asyncio - async def test__request_when_TOO_MANY_REQUESTS_when_not_global(self, compiled_route, exit_error, discord_response): + async def test__request_when_TOO_MANY_REQUESTS_when_not_global( + self, compiled_route, exit_error, discord_response, mock_http_client + ): discord_response.status = 429 discord_response.raw_body = '{"retry_after": 1, "global": false}' - http_client_impl = http_client.HTTPClient(token="Bot token") - http_client_impl.ratelimiter = mock.MagicMock() - http_client_impl.global_ratelimiter = mock.MagicMock(throttle=mock.MagicMock()) - http_client_impl.logger = mock.MagicMock(debug=mock.MagicMock(side_effect=[None, exit_error])) + mock_http_client = await mock_http_client + mock_http_client.logger.debug.side_effect = [None, exit_error] with mock.patch("asyncio.gather", return_value=_helpers.AwaitableMock()): with mock.patch.object(base_http_client.BaseHTTPClient, "_request", return_value=discord_response): try: - await http_client_impl._request(compiled_route) + await mock_http_client._request(compiled_route) except exit_error: pass - http_client_impl.global_ratelimiter.throttle.assert_not_called() + mock_http_client.global_ratelimiter.throttle.assert_not_called() @pytest.mark.asyncio @pytest.mark.parametrize("api_version", [versions.HTTPAPIVersion.V6, versions.HTTPAPIVersion.V7]) @@ -398,13 +397,17 @@ async def test__request_when_TOO_MANY_REQUESTS_when_not_global(self, compiled_ro (405, errors.ClientHTTPError), ], ) + @mock.patch.object(base_http_client.BaseHTTPClient, "__init__") + @mock.patch.object(ratelimits, "ManualRateLimiter") + @mock.patch.object(ratelimits, "HTTPBucketRateLimiterManager") async def test__request_raises_appropriate_error_for_status_code( - self, status_code, error, compiled_route, discord_response, api_version + self, *patches, status_code, error, compiled_route, discord_response, api_version ): discord_response.status = status_code http_client_impl = http_client.HTTPClient(token="Bot token", version=api_version) http_client_impl.ratelimiter = mock.MagicMock() http_client_impl.global_ratelimiter = mock.MagicMock() + http_client_impl.logger = mock.MagicMock() with mock.patch("asyncio.gather", return_value=_helpers.AwaitableMock()): with mock.patch.object(base_http_client.BaseHTTPClient, "_request", return_value=discord_response): @@ -415,35 +418,31 @@ async def test__request_raises_appropriate_error_for_status_code( assert True @pytest.mark.asyncio - async def test__request_when_NO_CONTENT(self, compiled_route, discord_response): + async def test__request_when_NO_CONTENT(self, compiled_route, discord_response, mock_http_client): discord_response.status = 204 - http_client_impl = http_client.HTTPClient(token="Bot token") - http_client_impl.ratelimiter = mock.MagicMock() - http_client_impl.global_ratelimiter = mock.MagicMock() + mock_http_client = await mock_http_client with mock.patch("asyncio.gather", return_value=_helpers.AwaitableMock()): with mock.patch.object(base_http_client.BaseHTTPClient, "_request", return_value=discord_response): - assert await http_client_impl._request(compiled_route, form_body=aiohttp.FormData()) is None + assert await mock_http_client._request(compiled_route, form_body=aiohttp.FormData()) is None @pytest.mark.asyncio async def test__request_handles_bad_response_when_status_error_not_catched( - self, exit_error, compiled_route, discord_response + self, exit_error, compiled_route, discord_response, mock_http_client ): discord_response.raw_body = "{}" discord_response.status = 1000 - http_client_impl = http_client.HTTPClient(token="Bot token") - http_client_impl.ratelimiter = mock.MagicMock() - http_client_impl.global_ratelimiter = mock.MagicMock() - http_client_impl._handle_bad_response = mock.AsyncMock(side_effect=[None, exit_error]) + mock_http_client = await mock_http_client + mock_http_client._handle_bad_response = mock.AsyncMock(side_effect=[None, exit_error]) with mock.patch("asyncio.gather", return_value=_helpers.AwaitableMock()): with mock.patch.object(base_http_client.BaseHTTPClient, "_request", return_value=discord_response): try: - await http_client_impl._request(compiled_route) + await mock_http_client._request(compiled_route) except exit_error: pass - http_client_impl._handle_bad_response.assert_called() + mock_http_client._handle_bad_response.assert_called() @pytest.mark.asyncio async def test_handle_bad_response(self, http_client_impl): diff --git a/tests/hikari/net/test_ratelimits.py b/tests/hikari/net/test_ratelimits.py index 1006132e12..9dd545c933 100644 --- a/tests/hikari/net/test_ratelimits.py +++ b/tests/hikari/net/test_ratelimits.py @@ -429,8 +429,10 @@ async def test_start(self): @pytest.mark.asyncio async def test_exit_closes(self): with mock.patch("hikari.net.ratelimits.HTTPBucketRateLimiterManager.close") as close: - with ratelimits.HTTPBucketRateLimiterManager() as mgr: - mgr.start(0.01) + with mock.patch("hikari.net.ratelimits.HTTPBucketRateLimiterManager.gc") as gc: + with ratelimits.HTTPBucketRateLimiterManager() as mgr: + mgr.start(0.01) + gc.assert_called_once_with(0.01) close.assert_called() @pytest.mark.asyncio @@ -455,10 +457,13 @@ async def test_gc_polls_until_closed_event_set(self): @pytest.mark.asyncio async def test_gc_calls_do_pass(self): with _helpers.unslot_class(ratelimits.HTTPBucketRateLimiterManager)() as mgr: - mgr.do_gc_pass = mock.AsyncMock() + mgr.do_gc_pass = mock.MagicMock() mgr.start(0.01) - await asyncio.sleep(0.1) - mgr.do_gc_pass.assert_called() + try: + await asyncio.sleep(0.1) + mgr.do_gc_pass.assert_called() + finally: + mgr.gc_task.cancel() @pytest.mark.asyncio async def test_gc_calls_do_pass_and_ignores_exception(self): @@ -472,6 +477,8 @@ async def test_gc_calls_do_pass_and_ignores_exception(self): assert False except asyncio.InvalidStateError: pass + finally: + mgr.gc_task.cancel() @pytest.mark.asyncio async def test_do_gc_pass_any_buckets_that_are_empty_and_unknown_get_closed(self): From 8b73da524333ca7a468261e01be0d5fa72dee368 Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Sat, 28 Mar 2020 17:48:48 +0000 Subject: [PATCH 027/922] Fixes #256 to allow fields with leading underscores to be marshalled correctly. --- hikari/internal_utilities/marshaller.py | 41 ++++++++++++------- .../internal_utilities/test_marshaller.py | 18 ++++++++ .../test_marshaller_pep563.py | 18 ++++++++ 3 files changed, 63 insertions(+), 14 deletions(-) diff --git a/hikari/internal_utilities/marshaller.py b/hikari/internal_utilities/marshaller.py index e0ca43bca6..74994e504b 100644 --- a/hikari/internal_utilities/marshaller.py +++ b/hikari/internal_utilities/marshaller.py @@ -102,7 +102,7 @@ def attrib( # as an attr.ib() kwargs AND use type hints at the same time, and without # type hints, the library loses the ability to be type checked properly # anymore, so we have to pass this explicitly regardless. - deserializer: typing.Callable[[typing.Any], typing.Any], + deserializer: typing.Optional[typing.Callable[[typing.Any], typing.Any]] = None, if_none: typing.Union[typing.Callable[..., typing.Any], None, type(RAISE)] = RAISE, if_undefined: typing.Union[typing.Callable[..., typing.Any], None, type(RAISE)] = RAISE, raw_name: typing.Optional[str] = None, @@ -114,7 +114,7 @@ def attrib( Parameters ---------- - deserializer : :obj:`typing.Callable` [ [ :obj:`typing.Any` ], :obj:`typing.Any` ] + deserializer : :obj:`typing.Callable` [ [ :obj:`typing.Any` ], :obj:`typing.Any` ], optional The deserializer to use to deserialize raw elements. raw_name : :obj:`str`, optional The raw name of the element in its raw serialized form. If not provided, @@ -164,20 +164,23 @@ class that this attribute is in will trigger a :obj:`TypeError` return attr.ib(**kwargs, metadata=metadata) -def _no_serialize(name): +def _not_implemented(op, name): def error(*_, **__) -> typing.NoReturn: - raise TypeError(f"Field {name} does not support serialization") + raise NotImplementedError(f"Field {name} does not support operation {op}") return error class _AttributeDescriptor: - __slots__ = ("raw_name", "field_name", "if_none", "if_undefined", "is_transient", "deserializer", "serializer") + __slots__ = ( + "raw_name", "field_name", "constructor_name", "if_none", "if_undefined", "is_transient", "deserializer", + "serializer") def __init__( self, raw_name: str, field_name: str, + constructor_name: str, if_none: typing.Callable[..., typing.Any], if_undefined: typing.Callable[..., typing.Any], is_transient: bool, @@ -186,6 +189,7 @@ def __init__( ) -> None: self.raw_name = raw_name self.field_name = field_name + self.constructor_name = constructor_name self.if_none = if_none self.if_undefined = if_undefined self.is_transient = is_transient # Do not serialize @@ -196,7 +200,7 @@ def __init__( class _EntityDescriptor: __slots__ = ("entity_type", "attribs") - def __init__(self, entity_type: typing.Type, attribs: typing.Collection[_AttributeDescriptor],) -> None: + def __init__(self, entity_type: typing.Type, attribs: typing.Collection[_AttributeDescriptor], ) -> None: self.entity_type = entity_type self.attribs = tuple(attribs) @@ -205,14 +209,21 @@ def _construct_attribute_descriptor(field: attr.Attribute) -> _AttributeDescript raw_name = typing.cast(str, field.metadata.get(_RAW_NAME_ATTR) or field.name) field_name = typing.cast(str, field.name) + constructor_name = field_name + + # Attrs strips leading underscores for generated __init__ methods. + while constructor_name.startswith("_"): + constructor_name = constructor_name[1:] + return _AttributeDescriptor( raw_name=raw_name, field_name=field_name, + constructor_name=constructor_name, if_none=field.metadata[_IF_NONE], if_undefined=field.metadata[_IF_UNDEFINED], is_transient=field.metadata[_TRANSIENT_ATTR], - deserializer=field.metadata[_DESERIALIZER_ATTR], - serializer=field.metadata[_SERIALIZER_ATTR] or _no_serialize(field_name), + deserializer=field.metadata[_DESERIALIZER_ATTR] or _not_implemented("deserialize", field_name), + serializer=field.metadata[_SERIALIZER_ATTR] or _not_implemented("serialize", field_name), ) @@ -292,6 +303,8 @@ def deserialize(self, raw_data: typing.Mapping[str, typing.Any], target_type: ty kwargs = {} for a in descriptor.attribs: + kwarg_name = a.constructor_name + if a.raw_name not in raw_data: if a.if_undefined is RAISE: raise AttributeError( @@ -299,9 +312,9 @@ def deserialize(self, raw_data: typing.Mapping[str, typing.Any], target_type: ty f"payload\n\n{raw_data}" ) elif a.if_undefined: - kwargs[a.field_name] = a.if_undefined() + kwargs[kwarg_name] = a.if_undefined() else: - kwargs[a.field_name] = None + kwargs[kwarg_name] = None continue elif (data := raw_data[a.raw_name]) is None: if a.if_none is RAISE: @@ -310,19 +323,19 @@ def deserialize(self, raw_data: typing.Mapping[str, typing.Any], target_type: ty f"payload\n\n{raw_data}" ) elif a.if_none: - kwargs[a.field_name] = a.if_none() + kwargs[kwarg_name] = a.if_none() else: - kwargs[a.field_name] = None + kwargs[kwarg_name] = None continue try: # Use the deserializer if it is there, otherwise use the constructor of the type of the field. - kwargs[a.field_name] = a.deserializer(data) if a.deserializer else data + kwargs[kwarg_name] = a.deserializer(data) if a.deserializer else data except Exception as exc: raise TypeError( "Failed to deserialize data to instance of " f"{target_type.__module__}.{target_type.__qualname__} because marshalling failed on " - f"attribute {a.field_name}" + f"attribute {a.field_name} (passed to constructor as {kwarg_name}" ) from exc return target_type(**kwargs) diff --git a/tests/hikari/internal_utilities/test_marshaller.py b/tests/hikari/internal_utilities/test_marshaller.py index 42df7c1019..8b79a55a10 100644 --- a/tests/hikari/internal_utilities/test_marshaller.py +++ b/tests/hikari/internal_utilities/test_marshaller.py @@ -231,3 +231,21 @@ class Foo: f = Foo() marshaller_impl.serialize(f) + + def test_handling_underscores_correctly_during_deserialization(self, marshaller_impl): + @marshaller.attrs(marshaller=marshaller_impl) + class ClassWithUnderscores: + _foo = marshaller.attrib(deserializer=str) + + impl = marshaller_impl.deserialize({"_foo": 1234}, ClassWithUnderscores) + + assert impl._foo == "1234" + + def test_handling_underscores_correctly_during_serialization(self, marshaller_impl): + @marshaller.attrs(marshaller=marshaller_impl) + class ClassWithUnderscores: + _foo = marshaller.attrib(serializer=int) + + impl = ClassWithUnderscores(foo="1234") + + assert marshaller_impl.serialize(impl) == {"_foo": 1234} diff --git a/tests/hikari/internal_utilities/test_marshaller_pep563.py b/tests/hikari/internal_utilities/test_marshaller_pep563.py index 10392e61dd..19b0ec0223 100644 --- a/tests/hikari/internal_utilities/test_marshaller_pep563.py +++ b/tests/hikari/internal_utilities/test_marshaller_pep563.py @@ -238,3 +238,21 @@ class Foo: f = Foo() marshaller_impl.serialize(f) + + def test_handling_underscores_correctly_during_deserialization(self, marshaller_impl): + @marshaller.attrs(marshaller=marshaller_impl) + class ClassWithUnderscores: + _foo = marshaller.attrib(deserializer=str) + + impl = marshaller_impl.deserialize({"_foo": 1234}, ClassWithUnderscores) + + assert impl._foo == "1234" + + def test_handling_underscores_correctly_during_serialization(self, marshaller_impl): + @marshaller.attrs(marshaller=marshaller_impl) + class ClassWithUnderscores: + _foo = marshaller.attrib(serializer=int) + + impl = ClassWithUnderscores(foo="1234") + + assert marshaller_impl.serialize(impl) == {"_foo": 1234} From a7f734ce1bf876d1337f485a67b40253cb0e32e8 Mon Sep 17 00:00:00 2001 From: Faster Speeding Date: Mon, 23 Mar 2020 17:55:01 +0000 Subject: [PATCH 028/922] Add gateway bot models and tests. --- hikari/core/gateway_bot.py | 53 ++++++++++++++++++++---- tests/hikari/core/test_gateway_bot.py | 58 +++++++++++++++++++++++++++ 2 files changed, 103 insertions(+), 8 deletions(-) create mode 100644 tests/hikari/core/test_gateway_bot.py diff --git a/hikari/core/gateway_bot.py b/hikari/core/gateway_bot.py index 5b0df27481..f028fdb8db 100644 --- a/hikari/core/gateway_bot.py +++ b/hikari/core/gateway_bot.py @@ -16,18 +16,55 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . -__all__ = ["GatewayBot", "SessionStartLimit"] +""" +Components and entities that are used to describe Discord gateway information. +""" +__all__ = ["GatewayBot"] -import attr +import datetime from hikari.core import entities +from hikari.internal_utilities import marshaller -@attr.s(slots=True, auto_attribs=True) -class GatewayBot(entities.HikariEntity): - ... +@marshaller.attrs(slots=True) +class SessionStartLimit(entities.HikariEntity, entities.Deserializable): + """Used to represent information about the current session start limits.""" + #: The total number of session starts the current bot is allowed. + #: + #: :type: :obj:`int` + total: int = marshaller.attrib(deserializer=int) -@attr.s(slots=True, auto_attribs=True) -class SessionStartLimit(entities.HikariEntity): - ... + #: The remaining number of session starts this bot has. + #: + #: :type: :obj:`int` + remaining: int = marshaller.attrib(deserializer=int) + + #: The timedelta of when :attr:`remaining` will reset back to :attr:`total` + #: for the current bot. + #: + #: :type: :obj:`datetime.timedelta` + reset_after: datetime.timedelta = marshaller.attrib( + deserializer=lambda after: datetime.timedelta(milliseconds=after), + ) + + +@marshaller.attrs(slots=True) +class GatewayBot(entities.HikariEntity, entities.Deserializable): + """Used to represent gateway information for the connected bot.""" + + #: The WSS URL that can be used for connecting to the gateway. + #: + #: :type: :obj:`str` + url: str = marshaller.attrib(deserializer=str) + + #: The recommended number of shards to use when connecting to the gateway. + #: + #: :type: :obj:`int` + shard_count: int = marshaller.attrib(raw_name="shards", deserializer=int) + + #: Information about the bot's current session start limit. + #: + #: :type: :obj:`SessionStartLimit` + session_start_limit: int = marshaller.attrib(deserializer=SessionStartLimit.deserialize) diff --git a/tests/hikari/core/test_gateway_bot.py b/tests/hikari/core/test_gateway_bot.py new file mode 100644 index 0000000000..886c74e202 --- /dev/null +++ b/tests/hikari/core/test_gateway_bot.py @@ -0,0 +1,58 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# Copyright © Nekoka.tt 2019-2020 +# +# This file is part of Hikari. +# +# Hikari is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Hikari is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Hikari. If not, see . +import datetime + +import cymock as mock +import pytest + +from hikari.core import gateway_bot +from tests.hikari import _helpers + + +@pytest.fixture() +def test_session_start_limit_payload(): + return {"total": 1000, "remaining": 991, "reset_after": 14170186} + + +class TestSessionStartLimit: + def test_deserialize(self, test_session_start_limit_payload): + session_start_limit_obj = gateway_bot.SessionStartLimit.deserialize(test_session_start_limit_payload) + assert session_start_limit_obj.total == 1000 + assert session_start_limit_obj.remaining == 991 + assert session_start_limit_obj.reset_after == datetime.timedelta(milliseconds=14170186) + + +class TestGatewayBot: + @pytest.fixture() + def test_gateway_bot_payload(self, test_session_start_limit_payload): + return {"url": "wss://gateway.discord.gg", "shards": 1, "session_start_limit": test_session_start_limit_payload} + + def test_deserialize(self, test_gateway_bot_payload, test_session_start_limit_payload): + mock_session_start_limit = mock.MagicMock(gateway_bot.SessionStartLimit) + with _helpers.patch_marshal_attr( + gateway_bot.GatewayBot, + "session_start_limit", + deserializer=gateway_bot.SessionStartLimit.deserialize, + return_value=mock_session_start_limit, + ) as patched_start_limit_deserializer: + gateway_bot_obj = gateway_bot.GatewayBot.deserialize(test_gateway_bot_payload) + patched_start_limit_deserializer.assert_called_once_with(test_session_start_limit_payload) + assert gateway_bot_obj.session_start_limit is mock_session_start_limit + assert gateway_bot_obj.url == "wss://gateway.discord.gg" + assert gateway_bot_obj.shard_count == 1 From 431b4b018ee8a1f303dab9a31b29d1db09c52490 Mon Sep 17 00:00:00 2001 From: davfsa Date: Mon, 23 Mar 2020 20:56:42 +0100 Subject: [PATCH 029/922] Implemented messages, embeds, webhooks and emojis entities - Fix bug when serializing - Also fixes several issues I found --- hikari/core/__init__.py | 5 + hikari/core/colors.py | 5 +- hikari/core/colours.py | 2 +- hikari/core/embeds.py | 300 +++++++++++++++++++++ hikari/core/emojis.py | 100 +++++++ hikari/core/guilds.py | 22 +- hikari/core/messages.py | 338 ++++++++++++++++++++++-- hikari/core/oauth2.py | 8 +- hikari/core/webhooks.py | 86 ++++++ hikari/internal_utilities/marshaller.py | 3 +- tests/hikari/core/test_embeds.py | 257 ++++++++++++++++++ tests/hikari/core/test_emojis.py | 74 ++++++ tests/hikari/core/test_invites.py | 2 +- tests/hikari/core/test_messages.py | 196 ++++++++++++++ tests/hikari/core/test_webhook.py | 51 ++++ 15 files changed, 1407 insertions(+), 42 deletions(-) create mode 100644 hikari/core/embeds.py create mode 100644 hikari/core/emojis.py create mode 100644 hikari/core/webhooks.py create mode 100644 tests/hikari/core/test_embeds.py create mode 100644 tests/hikari/core/test_emojis.py create mode 100644 tests/hikari/core/test_messages.py create mode 100644 tests/hikari/core/test_webhook.py diff --git a/hikari/core/__init__.py b/hikari/core/__init__.py index 55f62c2736..a232d38cce 100644 --- a/hikari/core/__init__.py +++ b/hikari/core/__init__.py @@ -20,6 +20,10 @@ from hikari.core.configs import * from hikari.core.clients import * from hikari.core.channels import * +from hikari.core.colors import * +from hikari.core.colours import * +from hikari.core.embeds import * +from hikari.core.emojis import * from hikari.core.entities import * from hikari.core.events import * from hikari.core.gateway_bot import * @@ -30,3 +34,4 @@ from hikari.core.permissions import * from hikari.core.snowflakes import * from hikari.core.users import * +from hikari.core.webhooks import * diff --git a/hikari/core/colors.py b/hikari/core/colors.py index d1e5a1a118..6b19fcd3fb 100644 --- a/hikari/core/colors.py +++ b/hikari/core/colors.py @@ -16,9 +16,10 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . +"""Model that represents a common RGB color and provides +simple conversions to other common color systems. """ -Model that represents a common RGB color and provides simple conversions to other common color systems. -""" + __all__ = ["Color", "ColorCompatibleT"] import string diff --git a/hikari/core/colours.py b/hikari/core/colours.py index 8de2122cf6..97a4875007 100644 --- a/hikari/core/colours.py +++ b/hikari/core/colours.py @@ -17,7 +17,7 @@ # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . """ -Alias for the :mod:`hikari.orm.models.colors` module. +Alias for the :mod:`hikari.core.colors` module. """ __all__ = ["Colour"] diff --git a/hikari/core/embeds.py b/hikari/core/embeds.py new file mode 100644 index 0000000000..640f1cbf78 --- /dev/null +++ b/hikari/core/embeds.py @@ -0,0 +1,300 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# Copyright © Nekoka.tt 2019-2020 +# +# This file is part of Hikari. +# +# Hikari is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Hikari is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Hikari. If not, see . + +__all__ = [ + "Embed", + "EmbedType", + "EmbedThumbnail", + "EmbedVideo", + "EmbedImage", + "EmbedProvider", + "EmbedAuthor", + "EmbedFooter", + "EmbedField", +] + +import typing +import enum +import datetime + +from hikari.core import entities +from hikari.core import colors +from hikari.internal_utilities import marshaller +from hikari.internal_utilities import dates + + +@enum.unique +class EmbedType(str, enum.Enum): + """The type of a embed.""" + + #: Generic embed rendered from embed attributes. + RICH = "rich" + #: Image embed. + IMAGE = "image" + #: Video embed. + VIDEO = "video" + #: Animated gif image embed renderered as a video embed. + GFV = "gfv" + #: Article embed. + ARTICLE = "article" + #: Link embed. + LINK = "link" + + +@marshaller.attrs(slots=True) +class EmbedFooter(entities.HikariEntity, entities.Deserializable, entities.Serializable): + """Represents a embed footer.""" + + #: The footer text. + #: + #: :type: :obj:`str` + text: typing.Optional[str] = marshaller.attrib(deserializer=str, serializer=str) + + #: The url of the footer icon. + #: + #: :type: :obj:`str`, optional + icon_url: typing.Optional[str] = marshaller.attrib(deserializer=str, serializer=str, if_undefined=None) + + #: The proxied url of the footer icon. + #: + #: :type: :obj:`str`, optional + proxy_icon_url: typing.Optional[str] = marshaller.attrib(deserializer=str, serializer=str, if_undefined=None) + + +@marshaller.attrs(slots=True) +class EmbedImage(entities.HikariEntity, entities.Deserializable, entities.Serializable): + """Represents a embed image.""" + + #: The url of the image. + #: + #: :type: :obj:`str`, optional + url: typing.Optional[str] = marshaller.attrib(deserializer=str, serializer=str, if_undefined=None) + + #: The proxied url of the image. + #: + #: :type: :obj:`str`, optional + proxy_url: typing.Optional[str] = marshaller.attrib(deserializer=str, serializer=str, if_undefined=None) + + #: The height of the image. + #: + #: :type: :obj:`int`, optional + height: typing.Optional[int] = marshaller.attrib(deserializer=int, serializer=int, if_undefined=None) + + #: The width of the image. + #: + #: :type: :obj:`int`, optional + width: typing.Optional[int] = marshaller.attrib(deserializer=int, serializer=int, if_undefined=None) + + +@marshaller.attrs(slots=True) +class EmbedThumbnail(entities.HikariEntity, entities.Deserializable, entities.Serializable): + """Represents a embed thumbnail.""" + + #: The url of the thumbnail. + #: + #: :type: :obj:`str`, optional + url: typing.Optional[str] = marshaller.attrib(deserializer=str, serializer=str, if_undefined=None) + + #: The proxied url of the thumbnail. + #: + #: :type: :obj:`str`, optional + proxy_url: typing.Optional[str] = marshaller.attrib(deserializer=str, serializer=str, if_undefined=None) + + #: The height of the thumbnail. + #: + #: :type: :obj:`int`, optional + height: typing.Optional[int] = marshaller.attrib(deserializer=int, serializer=int, if_undefined=None) + + #: The width of the thumbnail. + #: + #: :type: :obj:`int`, optional + width: typing.Optional[int] = marshaller.attrib(deserializer=int, serializer=int, if_undefined=None) + + +@marshaller.attrs(slots=True) +class EmbedVideo(entities.HikariEntity, entities.Deserializable, entities.Serializable): + """Represents a embed video.""" + + #: The url of the video. + #: + #: :type: :obj:`str`, optional + url: typing.Optional[str] = marshaller.attrib(deserializer=str, serializer=str, if_undefined=None) + + #: The height of the video. + #: + #: :type: :obj:`int`, optional + height: typing.Optional[int] = marshaller.attrib(deserializer=int, serializer=int, if_undefined=None) + + #: The width of the video. + #: + #: :type: :obj:`int`, optional + width: typing.Optional[int] = marshaller.attrib(deserializer=int, serializer=int, if_undefined=None) + + +@marshaller.attrs(slots=True) +class EmbedProvider(entities.HikariEntity, entities.Deserializable, entities.Serializable): + """Represents a embed provider.""" + + #: The name of the provider. + #: + #: :type: :obj:`str`, optional + name: typing.Optional[str] = marshaller.attrib(deserializer=str, serializer=str, if_undefined=None) + + #: The url of the provider. + #: + #: :type: :obj:`str`, optional + url: typing.Optional[str] = marshaller.attrib(deserializer=str, serializer=str, if_undefined=None) + + +@marshaller.attrs(slots=True) +class EmbedAuthor(entities.HikariEntity, entities.Deserializable, entities.Serializable): + """Represents a embed author.""" + + #: The name of the author. + #: + #: :type: :obj:`str`, optional + name: typing.Optional[str] = marshaller.attrib(deserializer=str, serializer=str, if_undefined=None) + + #: The url of the author. + #: + #: :type: :obj:`str`, optional + url: typing.Optional[str] = marshaller.attrib(deserializer=str, serializer=str, if_undefined=None) + + #: The url of the author icon. + #: + #: :type: :obj:`str`, optional + icon_url: typing.Optional[str] = marshaller.attrib(deserializer=str, serializer=str, if_undefined=None) + + #: The proxied url of the author icon. + #: + #: :type: :obj:`str`, optional + proxy_icon_url: typing.Optional[str] = marshaller.attrib(deserializer=str, serializer=str, if_undefined=None) + + +@marshaller.attrs(slots=True) +class EmbedField(entities.HikariEntity, entities.Deserializable, entities.Serializable): + """Represents a field in a embed.""" + + #: The name of the field. + #: + #: :type: :obj:`str` + name: str = marshaller.attrib(deserializer=str, serializer=str) + + #: The value of the field. + #: + #: :type: :obj:`str` + value: str = marshaller.attrib(deserializer=str, serializer=str) + + #: Whether the field should display inline. Defaults to ``False``. + #: + #: :type: :obj:`bool` + is_inline: bool = marshaller.attrib( + raw_name="inline", deserializer=bool, serializer=bool, if_undefined=lambda: False + ) + + +@marshaller.attrs(slots=True) +class Embed(entities.HikariEntity, entities.Deserializable, entities.Serializable): + """Represents a embed.""" + + #: The title of the embed. + #: + #: :type: :obj:`str`, optional + title: typing.Optional[str] = marshaller.attrib(deserializer=str, serializer=str, if_undefined=None) + + #: The type of the embed. + #: + #: :type: :obj:`EmbedType` + type: EmbedType = marshaller.attrib( + deserializer=EmbedType, serializer=str.lower, if_undefined=lambda: EmbedType.RICH + ) + + #: The description of the embed. + #: + #: :type: :obj:`str`, optional + description: typing.Optional[str] = marshaller.attrib(deserializer=str, serializer=str, if_undefined=None) + + #: The url of the embed. + #: + #: :type: :obj:`str`, optional + url: typing.Optional[str] = marshaller.attrib(deserializer=str, serializer=str, if_undefined=None) + + #: The timestamp of the embed. + #: + #: :type: :obj:`datetime.datetime`, optional + timestamp: typing.Optional[datetime.datetime] = marshaller.attrib( + deserializer=dates.parse_iso_8601_ts, + serializer=lambda timestamp: timestamp.replace(tzinfo=datetime.timezone.utc).isoformat(), + if_undefined=None, + ) + + color: typing.Optional[colors.Color] = marshaller.attrib( + deserializer=colors.Color, serializer=int, if_undefined=None + ) + + #: The footer of the embed. + #: + #: :type: :obj:`EmbedFooter`, optional + footer: typing.Optional[EmbedFooter] = marshaller.attrib( + deserializer=EmbedFooter.deserialize, serializer=EmbedFooter.serialize, if_undefined=None + ) + + #: The image of the embed. + #: + #: :type: :obj:`EmbedImage`, optional + image: typing.Optional[EmbedImage] = marshaller.attrib( + deserializer=EmbedImage.deserialize, serializer=EmbedImage.serialize, if_undefined=None + ) + + #: The thumbnail of the embed. + #: + #: :type: :obj:`EmbedThumbnail`, optional + thumbnail: typing.Optional[EmbedThumbnail] = marshaller.attrib( + deserializer=EmbedThumbnail.deserialize, serializer=EmbedThumbnail.serialize, if_undefined=None + ) + + #: The video of the embed. + #: + #: :type: :obj:`EmbedVideo`, optional + video: typing.Optional[EmbedVideo] = marshaller.attrib( + deserializer=EmbedVideo.deserialize, serializer=EmbedVideo.serialize, if_undefined=None + ) + + #: The provider of the embed. + #: + #: :type: :obj:`EmbedProvider`, optional + provider: typing.Optional[EmbedProvider] = marshaller.attrib( + deserializer=EmbedProvider.deserialize, serializer=EmbedProvider.serialize, if_undefined=None + ) + + #: The author of the embed. + #: + #: :type: :obj:`EmbedAuthor`, optional + author: typing.Optional[EmbedAuthor] = marshaller.attrib( + deserializer=EmbedAuthor.deserialize, serializer=EmbedAuthor.serialize, if_undefined=None + ) + + #: The fields of the embed. + #: + #: :type: :obj:`typing.Sequence` [ :obj:`EmbedField` ], optional + fields: typing.Optional[typing.Sequence[EmbedField]] = marshaller.attrib( + deserializer=lambda fields: [f for f in map(EmbedField.deserialize, fields)], + serializer=lambda fields: [f for f in map(EmbedField.serialize, fields)], + if_undefined=None, + ) diff --git a/hikari/core/emojis.py b/hikari/core/emojis.py new file mode 100644 index 0000000000..b001223604 --- /dev/null +++ b/hikari/core/emojis.py @@ -0,0 +1,100 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# Copyright © Nekoka.tt 2019-2020 +# +# This file is part of Hikari. +# +# Hikari is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Hikari is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Hikari. If not, see . + +import typing + +from hikari.core import entities +from hikari.core import snowflakes +from hikari.core import users +from hikari.internal_utilities import marshaller + +__all__ = ["Emoji", "UnicodeEmoji", "UnknownEmoji", "GuildEmoji"] + + +@marshaller.attrs(slots=True) +class Emoji(entities.HikariEntity, entities.Deserializable): + """Base class for all emojis.""" + + +@marshaller.attrs(slots=True) +class UnicodeEmoji(Emoji): + """Represents a unicode emoji.""" + + #: The codepoints that form the emoji. + #: + #: :type: :obj:`str` + name: str = marshaller.attrib(deserializer=str) + + +@marshaller.attrs(slots=True) +class UnknownEmoji(Emoji, snowflakes.UniqueEntity): + """Represents a unknown emoji.""" + + #: The name of the emoji. + #: + #: :type: :obj:`str`, optional + name: typing.Optional[str] = marshaller.attrib(deserializer=str, if_none=None) + + #: Wheter the emoji is animated. + #: + #: :type: :obj:`bool` + is_animated: bool = marshaller.attrib(raw_name="animated", deserializer=bool, if_undefined=lambda: False) + + +@marshaller.attrs(slots=True) +class GuildEmoji(UnknownEmoji): + """Represents a guild emoji.""" + + #: The whitelisted role IDs to use this emoji. + #: + #: :type: :obj:`typing.Set` [ :obj:`snowflakes.Snowflake` ] + role_ids: typing.Set[snowflakes.Snowflake] = marshaller.attrib( + raw_name="roles", deserializer=lambda roles: {r for r in map(snowflakes.Snowflake, roles)}, if_undefined={} + ) + + #: The user that created the emoji. + #: + #: Note + #: ---- + #: This will be ``None`` if you are missing ``MANAGE_EMOJIS``` permission + #: in the server the emoji is from + #: () + #: + #: :type: :obj:`users.User`, optional + user: typing.Optional[users.User] = marshaller.attrib(deserializer=users.User, if_none=None, if_undefined=None) + + #: Whether this emoji must be wrapped in colons. + #: + #: :type: :obj:`bool`, optional + is_colons_required: typing.Optional[bool] = marshaller.attrib( + raw_name="require_colons", deserializer=bool, if_undefined=None + ) + + #: Wheter the emoji is managed by an integration. + #: + #: :type: :obj:`bool`, optional + is_managed: typing.Optional[bool] = marshaller.attrib(raw_name="managed", deserializer=bool, if_undefined=None) + + +def deserialize_reaction_emoji(payload: typing.Dict) -> typing.Union[UnicodeEmoji, UnknownEmoji]: + """Deserialize a reaction emoji into an emoji.""" + if payload.get("id"): + return UnknownEmoji.deserialize(payload) + + return UnicodeEmoji.deserialize(payload) diff --git a/hikari/core/guilds.py b/hikari/core/guilds.py index 438e505999..0fcdfb01f6 100644 --- a/hikari/core/guilds.py +++ b/hikari/core/guilds.py @@ -19,7 +19,6 @@ """Components and entities that are used to describe guilds on Discord. """ __all__ = [ - "GuildEmoji", "GuildChannel", "GuildTextChannel", "GuildNewsChannel", @@ -48,8 +47,8 @@ from hikari.core import channels from hikari.core import entities -from hikari.core import messages -from hikari.core import permissions as permissions_ +from hikari.core import emojis as _emojis +from hikari.core import permissions from hikari.core import snowflakes from hikari.core import users from hikari.internal_utilities import cdn @@ -58,11 +57,6 @@ from hikari.internal_utilities import transformations -@marshaller.attrs(slots=True) -class GuildEmoji(snowflakes.UniqueEntity, messages.Emoji, entities.Deserializable): - ... - - @marshaller.attrs(slots=True) class GuildChannel(channels.Channel, entities.Deserializable): """The base for anything that is a guild channel.""" @@ -333,9 +327,9 @@ class Guild(PartialGuild): #: The guild level permissions that apply to the bot user. #: - #: :type: :obj:`hikari.core.permissions.Permission` - my_permissions: permissions_.Permission = marshaller.attrib( - raw_name="permissions", deserializer=permissions_.Permission + #: :type: :obj:`permissions.Permission` + my_permissions: permissions.Permission = marshaller.attrib( + raw_name="permissions", deserializer=permissions.Permission ) #: The voice region for the guild. @@ -406,9 +400,9 @@ class Guild(PartialGuild): #: The emojis that this guild provides, represented as a mapping of ID to #: emoji object. #: - #: :type: :obj:`typing.Mapping` [ :obj:`snowflakes.Snowflake`, :obj:`GuildEmoji` ] - emojis: typing.Mapping[snowflakes.Snowflake, GuildEmoji] = marshaller.attrib( - deserializer=lambda emojis: {e.id: e for e in map(GuildEmoji.deserialize, emojis)}, + #: :type: :obj:`typing.Mapping` [ :obj:`snowflakes.Snowflake`, :obj:`_emojis.GuildEmoji` ] + emojis: typing.Mapping[snowflakes.Snowflake, _emojis.GuildEmoji] = marshaller.attrib( + deserializer=lambda emojis: {e.id: e for e in map(_emojis.GuildEmoji.deserialize, emojis)}, ) #: The required MFA level for users wishing to participate in this guild. diff --git a/hikari/core/messages.py b/hikari/core/messages.py index 7c8aac0121..f4737f3aac 100644 --- a/hikari/core/messages.py +++ b/hikari/core/messages.py @@ -17,39 +17,337 @@ # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . -__all__ = ["Message", "Attachment", "Embed", "Emoji", "UnicodeEmoji", "Reaction"] +__all__ = [ + "MessageType", + "Message", + "Attachment", + "Reaction", +] -import attr +import datetime +import enum +import typing from hikari.core import entities from hikari.core import snowflakes +from hikari.core import guilds +from hikari.core import users +from hikari.core import oauth2 +from hikari.core import embeds as _embeds +from hikari.core import emojis as _emojis +from hikari.internal_utilities import dates +from hikari.internal_utilities import marshaller -@attr.s(slots=True) -class Message(snowflakes.UniqueEntity): - ... +@enum.unique +class MessageType(enum.IntEnum): + """The type of a message.""" + #: A normal message. + DEFAULT = 0 + #: A message to denote a new recipient in a group. + RECIPIENT_ADD = 1 + #: A message to denote that a recipient left the group. + RECIPIENT_REMOVE = 2 + #: A message to denote a VoIP call. + CALL = 3 + #: A message to denote that the name of a channel changed. + CHANNEL_NAME_CHANGE = 4 + #: A message to denote that the icon of a channel changed. + CHANNEL_ICON_CHANGE = 5 + #: A message to denote that a message was pinned. + CHANNEL_PINNED_MESSAGE = 6 + #: A message to denote that a member joined the guild. + GUILD_MEMBER_JOIN = 7 + #: A message to denote a Nitro subscription. + USER_PREMIUM_GUILD_SUBSCRIPTION = 8 + #: A message to denote a tier 1 Nitro subscription. + USER_PREMIUM_GUILD_SUBSCRIPTION_TIER_1 = 9 + #: A message to denote a tier 2 Nitro subscription. + USER_PREMIUM_GUILD_SUBSCRIPTION_TIER_2 = 10 + #: A message to denote a tier 3 Nitro subscription. + USER_PREMIUM_GUILD_SUBSCRIPTION_TIER_3 = 11 + #: Channel follow add + CHANNEL_FOLLOW_ADD = 12 -@attr.s(slots=True) -class Attachment(snowflakes.UniqueEntity): - ... +@enum.unique +class MessageFlag(enum.IntEnum): + """Additional flags for message options.""" -@attr.s(slots=True) -class Embed(entities.HikariEntity): - ... + NONE = 0x0 + #: This message has been published to subscribed channels via channel following. + CROSSPOSTED = 0x1 + #: This message originated from a message in another channel via channel following. + IS_CROSSPOST = 0x2 + #: Any embeds on this message should be omitted when serializing the message. + SUPPRESS_EMBEDS = 0x4 + #: The message this crosspost originated from was deleted via channel following. + SOURCE_MESSAGE_DELETED = 0x8 + #: This message came from the urgent message system. + URGENT = 0x10 -@attr.s(slots=True) -class Emoji(entities.HikariEntity): - ... +@enum.unique +class MessageActivityType(enum.IntEnum): + """The type of a rich presence message activity.""" + NONE = 0 + #: Join an activity. + JOIN = 1 + #: Spectating something. + SPECTATE = 2 + #: Listening to something. + LISTEN = 3 + #: Request to join an activity. + JOIN_REQUEST = 5 -@attr.s(slots=True) -class UnicodeEmoji(Emoji): - ... +@marshaller.attrs(slots=True) +class Attachment(snowflakes.UniqueEntity, entities.Deserializable): + """Represents a file attached to a message""" -@attr.s(slots=True) -class Reaction(entities.HikariEntity): - ... + #: The name of the file. + #: + #: :type: :obj:`str` + filename: str = marshaller.attrib(deserializer=str) + + #: The size of the file in bytes. + #: + #: :type: :obj:`int` + size: int = marshaller.attrib(deserializer=int) + + #: The source url of file. + #: + #: :type: :obj:`str` + url: str = marshaller.attrib(deserializer=str) + + #: The proxied url of file. + #: + #: :type: :obj:`str` + proxy_url: str = marshaller.attrib(deserializer=str) + + #: The height of the image (if the file is an image). + #: + #: :type: :obj:`int`, optional + height: typing.Optional[int] = marshaller.attrib(deserializer=int, if_undefined=None) + + #: The width of the image (if the file is an image). + #: + #: :type: :obj:`int`, optional + width: typing.Optional[int] = marshaller.attrib(deserializer=int, if_undefined=None) + + +@marshaller.attrs(slots=True) +class Reaction(entities.HikariEntity, entities.Deserializable): + """Represents a reaction in a message.""" + + #: The amount of times the emoji has been used to react. + #: + #: :type: :obj:`int` + count: int = marshaller.attrib(deserializer=int) + + #: The emoji used to react. + #: + #: :type: :obj:`typing.Union` [ :obj:`_emojis.UnicodeEmoji`, :obj:`_emojis.UnknownEmoji`] + emoji: typing.Union[_emojis.UnicodeEmoji, _emojis.UnknownEmoji] = marshaller.attrib( + deserializer=_emojis.deserialize_reaction_emoji + ) + + #: Whether the current user reacted using this emoji. + #: + #: :type: :obj:`bool` + is_reacted_by_me: bool = marshaller.attrib(raw_name="me", deserializer=bool) + + +@marshaller.attrs(slots=True) +class MessageActivity(entities.HikariEntity, entities.Deserializable): + """Represents the activity of a rich presence-enabled message.""" + + #: The type of message activity. + #: + #: :type: :obj:`MessageActivityType` + type: MessageActivityType = marshaller.attrib(deserializer=MessageActivityType) + + #: The party ID of the message activity. + #: + #: :type: :obj:`str`, optional + party_id: typing.Optional[str] = marshaller.attrib(deserializer=str, if_undefined=None) + + +@marshaller.attrs(slots=True) +class MessageCrosspost(entities.HikariEntity, entities.Deserializable): + """Represents information about a cross-posted message and the origin of the original message.""" + + #: The ID of the original message. + #: + #: Warning + #: ------- + #: This may be ``None`` in some cases according to the Discord API + #: documentation, but the situations that cause this to occur are not currently documented. + #: + #: :type: :obj:`snowflakes.Snowflake`, optional + message_id: typing.Optional[snowflakes.Snowflake] = marshaller.attrib( + deserializer=snowflakes.Snowflake, if_undefined=None + ) + + #: The ID of the channel that the message originated from. + #: + #: :type: :obj:`snowflakes.Snowflake` + channel_id: snowflakes.Snowflake = marshaller.attrib(deserializer=snowflakes.Snowflake) + + #: The ID of the guild that the message originated from. + #: + #: Warning + #: ------- + #: This may be ``None`` in some cases according to the Discord API + #: documentation, but the situations that cause this to occur are not currently documented. + #: + #: :type: :obj:`snowflakes.Snowflake`, optional + guild_id: typing.Optional[snowflakes.Snowflake] = marshaller.attrib( + deserializer=snowflakes.Snowflake, if_undefined=None + ) + + +@marshaller.attrs(slots=True) +class Message(snowflakes.UniqueEntity, entities.Deserializable): + """Represents a message.""" + + #: The ID of the channel that the message was sent in. + #: + #: :type: :obj:`snowflakes.Snowflake` + channel_id: snowflakes.Snowflake = marshaller.attrib(deserializer=snowflakes.Snowflake) + + #: The ID of the guild that the message was sent in. + #: + #: :type: :obj:`snowflakes.Snowflake`, optional + guild_id: typing.Optional[snowflakes.Snowflake] = marshaller.attrib( + deserializer=snowflakes.Snowflake, if_undefined=None + ) + + #: The author of this message. + #: + #: :type: :obj:`users.User` + author: users.User = marshaller.attrib(deserializer=users.User.deserialize) + + #: The member properties for the message's author. + #: + #: :type: :obj:`guilds.GuildMember`, optional + member: typing.Optional[guilds.GuildMember] = marshaller.attrib( + deserializer=guilds.GuildMember.deserialize, if_undefined=None + ) + + #: The content of the message. + #: + #: :type: :obj:`str` + content: str = marshaller.attrib(deserializer=str) + + #: The timestamp that the message was sent at. + #: + #: :type: :obj:`datetime.datetime` + timestamp: datetime.datetime = marshaller.attrib(deserializer=dates.parse_iso_8601_ts) + + #: The timestamp that the message was last edited at, or ``None`` if not ever edited. + #: + #: :type: :obj:`datetime.datetime`, optional + edited_timestamp: typing.Optional[datetime.datetime] = marshaller.attrib( + deserializer=dates.parse_iso_8601_ts, if_none=None + ) + + #: Whether the message is a TTS message. + #: + #: :type: :obj:`bool` + is_tts: bool = marshaller.attrib(raw_name="tts", deserializer=bool) + + #: Whether the message mentions ``@everyone`` or ``@here``. + #: + #: :type: :obj:`bool` + is_mentioning_everyone: bool = marshaller.attrib(raw_name="mention_everyone", deserializer=bool) + + #: The users the message mentions. + #: + #: :type: :obj:`typing.Set` [ :obj:`snowflakes.Snowflake` ] + user_mentions: typing.Set[snowflakes.Snowflake] = marshaller.attrib( + raw_name="mentions", deserializer=lambda user_mentions: {snowflakes.Snowflake(u["id"]) for u in user_mentions} + ) + + #: The roles the message mentions. + #: + #: :type: :obj:`typing.Set` [ :obj:`snowflakes.Snowflake` ] + role_mentions: typing.Set[snowflakes.Snowflake] = marshaller.attrib( + raw_name="mention_roles", + deserializer=lambda role_mentions: {r for r in map(snowflakes.Snowflake, role_mentions)}, + ) + + #: The channels the message mentions. + #: + #: :type: :obj:`typing.Set` [ :obj:`snowflakes.Snowflake` ] + channel_mentions: typing.Set[snowflakes.Snowflake] = marshaller.attrib( + raw_name="mention_channels", + deserializer=lambda channel_mentions: {snowflakes.Snowflake(c["id"]) for c in channel_mentions}, + if_undefined={}, + ) + + #: The message attachments. + #: + #: :type: :obj:`typing.Sequence` [ :obj:`Attachment` ] + attachments: typing.Sequence[Attachment] = marshaller.attrib( + deserializer=lambda attachments: [a for a in map(Attachment.deserialize, attachments)] + ) + + #: The message embeds. + #: + #: :type: :obj:`typing.Sequence` [ :obj:`_embeds.Embed` ] + embeds: typing.Sequence[_embeds.Embed] = marshaller.attrib( + deserializer=lambda embeds: [e for e in map(_embeds.Embed.deserialize, embeds)] + ) + + #: The message reactions. + #: + #: :type: :obj:`typing.Sequence` [ :obj:`Reaction` ] + reactions: typing.Sequence[Reaction] = marshaller.attrib( + deserializer=lambda reactions: [r for r in map(Reaction.deserialize, reactions)], if_undefined={} + ) + + #: Whether the message is pinned. + #: + #: :type: :obj:`bool` + is_pinned: bool = marshaller.attrib(raw_name="pinned", deserializer=bool) + + #: If the message was generated by a webhook, the webhook's id. + #: + #: :type: :obj:`snowflakes.Snowflake`, optional + webhook_id: typing.Optional[snowflakes.Snowflake] = marshaller.attrib( + deserializer=snowflakes.Snowflake, if_undefined=None + ) + + #: The message type. + #: + #: :type: :obj:`MessageType` + type: MessageType = marshaller.attrib(deserializer=MessageType) + + #: The message activity. + #: + #: :type: :obj:`MessageActivity`, optional + activity: typing.Optional[MessageActivity] = marshaller.attrib( + deserializer=MessageActivity.deserialize, if_undefined=None + ) + + #: The message application. + #: + #: :type: :obj:`oauth2.Application`, optional + application: typing.Optional[oauth2.Application] = marshaller.attrib( + deserializer=oauth2.Application.deserialize, if_undefined=None + ) + + #: The message crossposted reference data. + #: + #: :type: :obj:`MessageCrosspost`, optional + message_reference: typing.Optional[MessageCrosspost] = marshaller.attrib( + deserializer=MessageCrosspost.deserialize, if_undefined=None + ) + + #: The message flags. + #: + #: :type: :obj:`MessageFlag`, optional + flags: typing.Optional[MessageFlag] = marshaller.attrib(deserializer=MessageFlag, if_undefined=None) diff --git a/hikari/core/oauth2.py b/hikari/core/oauth2.py index 2501636208..08ceb8dec7 100644 --- a/hikari/core/oauth2.py +++ b/hikari/core/oauth2.py @@ -74,7 +74,7 @@ class TeamMember(entities.HikariEntity, entities.Deserializable): #: Will always be ``["*"]`` until Discord starts using this. #: #: :type: :obj:`typing.Set` [ :obj:`str` ] - permissions: typing.Set[str] = marshaller.attrib(deserializer=lambda permissions: {p for p in permissions}) + permissions: typing.Set[str] = marshaller.attrib(deserializer=set) #: The ID of the team this member belongs to. #: @@ -200,8 +200,10 @@ class Application(snowflakes.UniqueEntity, entities.Deserializable): #: The base64 encoded key used for the GameSDK's ``GetTicket``. #: - #: :type: :obj:`str` - verify_key: bytes = marshaller.attrib(deserializer=lambda key: bytes(key, "utf-8")) + #: :type: :obj:`bytes`, optional + verify_key: typing.Optional[bytes] = marshaller.attrib( + deserializer=lambda key: bytes(key, "utf-8"), if_undefined=None + ) #: The hash of this application's icon if set. #: diff --git a/hikari/core/webhooks.py b/hikari/core/webhooks.py new file mode 100644 index 0000000000..8bd08acfb1 --- /dev/null +++ b/hikari/core/webhooks.py @@ -0,0 +1,86 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# Copyright © Nekoka.tt 2019-2020 +# +# This file is part of Hikari. +# +# Hikari is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Hikari is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Hikari. If not, see . + +__all__ = ["WebhookType", "Webhook"] + +import enum +import typing + +from hikari.core import entities +from hikari.core import snowflakes +from hikari.core import users +from hikari.internal_utilities import marshaller + + +@enum.unique +class WebhookType(enum.IntEnum): + #: Incoming webhook. + INCOMING = 1 + #: Channel Follower webhook. + CHANNEL_FOLLOWER = 2 + + +@marshaller.attrs(slots=True) +class Webhook(snowflakes.UniqueEntity, entities.Deserializable): + """Represents a webhook""" + + #: The type of the webhook. + #: + #: :type: :obj:`WebhookType` + type: WebhookType = marshaller.attrib(deserializer=WebhookType) + + #: The guild ID of the webhook. + #: + #: :type: :obj:`snowflakes.Snowflake`, optional + guild_id: typing.Optional[snowflakes.Snowflake] = marshaller.attrib( + deserializer=snowflakes.Snowflake, if_undefined=None + ) + + #: The channel ID this webhook is for. + #: + #: :type: :obj:`snowflakes.Snowflake` + channel_id: snowflakes.Snowflake = marshaller.attrib(deserializer=snowflakes.Snowflake) + + #: The user that created the webhook + #: + #: Note + #: ---- + #: This will be ``None`` when getting a webhook with a token + #: + #: :type: :obj:`users.User`, optional + user: typing.Optional[users.User] = marshaller.attrib(deserializer=users.User, if_undefined=None) + + #: The default name of the webhook. + #: + #: :type: :obj:`str`, optional + name: typing.Optional[str] = marshaller.attrib(deserializer=str, if_none=None) + + #: The default avatar hash of the webhook. + #: + #: :type: :obj:`str`, optional + avatar_hash: typing.Optional[str] = marshaller.attrib(raw_name="avatar", deserializer=str, if_none=None) + + #: The token of the webhook. + #: + #: Note + #: ---- + #: This is only available for Incoming webhooks. + #: + #: :type: :obj:`str`, optional + token: typing.Optional[str] = marshaller.attrib(deserializer=str, if_undefined=None) diff --git a/hikari/internal_utilities/marshaller.py b/hikari/internal_utilities/marshaller.py index 74994e504b..9634a6394b 100644 --- a/hikari/internal_utilities/marshaller.py +++ b/hikari/internal_utilities/marshaller.py @@ -374,7 +374,8 @@ def serialize(self, obj: typing.Optional[typing.Any]) -> typing.Optional[typing. if a.is_transient: continue value = getattr(obj, a.field_name) - raw_data[a.raw_name] = a.serializer(value) or repr(value) + if value is not None: + raw_data[a.raw_name] = a.serializer(value) return raw_data diff --git a/tests/hikari/core/test_embeds.py b/tests/hikari/core/test_embeds.py new file mode 100644 index 0000000000..8138f92360 --- /dev/null +++ b/tests/hikari/core/test_embeds.py @@ -0,0 +1,257 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# Copyright © Nekoka.tt 2019-2020 +# +# This file is part of Hikari. +# +# Hikari is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Hikari is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along ith Hikari. If not, see . +import datetime + +import cymock as mock +import pytest + +from hikari.core import embeds +from hikari.core import colors +from hikari.internal_utilities import dates +from tests.hikari import _helpers + + +@pytest.fixture +def test_footer_payload(): + return { + "text": "footer text", + "icon_url": "https://somewhere.com/footer.png", + "proxy_icon_url": "https://media.somewhere.com/footer.png", + } + + +@pytest.fixture +def test_image_payload(): + return { + "url": "https://somewhere.com/image.png", + "proxy_url": "https://media.somewhere.com/image.png", + "height": 122, + "width": 133, + } + + +@pytest.fixture +def test_thumbnail_payload(): + return { + "url": "https://somewhere.com/thumbnail.png", + "proxy_url": "https://media.somewhere.com/thumbnail.png", + "height": 123, + "width": 456, + } + + +@pytest.fixture +def test_video_payload(): + return { + "url": "https://somewhere.com/video.mp4", + "height": 1234, + "width": 4567, + } + + +@pytest.fixture +def test_provider_payload(): + return {"name": "some name", "url": "https://somewhere.com/provider"} + + +@pytest.fixture +def test_author_payload(): + return { + "name": "some name", + "url": "https://somewhere.com/author", + "icon_url": "https://somewhere.com/author.png", + "proxy_icon_url": "https://media.somewhere.com/author.png", + } + + +@pytest.fixture +def test_field_payload(): + return {"name": "title", "value": "some value", "inline": True} + + +@pytest.fixture +def test_embed_payload( + test_footer_payload, + test_image_payload, + test_thumbnail_payload, + test_video_payload, + test_provider_payload, + test_author_payload, + test_field_payload, +): + return { + "title": "embed title", + "type": "article", + "description": "embed description", + "url": "https://somewhere.com", + "timestamp": "2020-03-22T16:40:39.218000+00:00", + "color": 14014915, + "footer": test_footer_payload, + "image": test_image_payload, + "thumbnail": test_thumbnail_payload, + "video": test_video_payload, + "provider": test_provider_payload, + "image": test_image_payload, + "author": test_author_payload, + "fields": [test_field_payload], + } + + +class TestEmbedFooter: + def test_deserialize(self, test_footer_payload): + footer_obj = embeds.EmbedFooter.deserialize(test_footer_payload) + + assert footer_obj.text == "footer text" + assert footer_obj.icon_url == "https://somewhere.com/footer.png" + assert footer_obj.proxy_icon_url == "https://media.somewhere.com/footer.png" + + def test_serialize(self, test_footer_payload): + footer_obj = embeds.EmbedFooter.deserialize(test_footer_payload) + + assert footer_obj.serialize() == test_footer_payload + + +class TestEmbedImage: + def test_deserialize(self, test_image_payload): + image_obj = embeds.EmbedImage.deserialize(test_image_payload) + + assert image_obj.url == "https://somewhere.com/image.png" + assert image_obj.proxy_url == "https://media.somewhere.com/image.png" + assert image_obj.height == 122 + assert image_obj.width == 133 + + def test_serialize(self, test_image_payload): + image_obj = embeds.EmbedImage.deserialize(test_image_payload) + + assert image_obj.serialize() == test_image_payload + + +class TestEmbedThumbnail: + def test_deserialize(self, test_thumbnail_payload): + thumbnail_obj = embeds.EmbedThumbnail.deserialize(test_thumbnail_payload) + + assert thumbnail_obj.url == "https://somewhere.com/thumbnail.png" + assert thumbnail_obj.proxy_url == "https://media.somewhere.com/thumbnail.png" + assert thumbnail_obj.height == 123 + assert thumbnail_obj.width == 456 + + def test_serialize(self, test_thumbnail_payload): + thumbnail_obj = embeds.EmbedThumbnail.deserialize(test_thumbnail_payload) + + assert thumbnail_obj.serialize() == test_thumbnail_payload + + +class TestEmbedVideo: + def test_deserialize(self, test_video_payload): + video_obj = embeds.EmbedVideo.deserialize(test_video_payload) + + assert video_obj.url == "https://somewhere.com/video.mp4" + assert video_obj.height == 1234 + assert video_obj.width == 4567 + + def test_serialize(self, test_video_payload): + video_obj = embeds.EmbedVideo.deserialize(test_video_payload) + + assert video_obj.serialize() == test_video_payload + + +class TestEmbedProvider: + def test_deserialize(self, test_provider_payload): + provider_obj = embeds.EmbedProvider.deserialize(test_provider_payload) + + assert provider_obj.name == "some name" + assert provider_obj.url == "https://somewhere.com/provider" + + def test_serialize(self, test_provider_payload): + provider_obj = embeds.EmbedProvider.deserialize(test_provider_payload) + + assert provider_obj.serialize() == test_provider_payload + + +class TestEmbedAuthor: + def test_deserialize(self, test_author_payload): + author_obj = embeds.EmbedAuthor.deserialize(test_author_payload) + + assert author_obj.name == "some name" + assert author_obj.url == "https://somewhere.com/author" + assert author_obj.icon_url == "https://somewhere.com/author.png" + assert author_obj.proxy_icon_url == "https://media.somewhere.com/author.png" + + def test_serialize(self, test_author_payload): + author_obj = embeds.EmbedAuthor.deserialize(test_author_payload) + + assert author_obj.serialize() == test_author_payload + + +class TestEmbedField: + def test_deserialize(self): + field_obj = embeds.EmbedField.deserialize({"name": "title", "value": "some value"}) + + assert field_obj.name == "title" + assert field_obj.value == "some value" + assert field_obj.is_inline is False + + def test_serialize(self, test_field_payload): + field_obj = embeds.EmbedField.deserialize(test_field_payload) + + assert field_obj.serialize() == test_field_payload + + +class TestEmbed: + @pytest.fixture + def embed_deserialized(self, test_embed_payload): + mock_datetime = mock.MagicMock(datetime.datetime) + + with _helpers.patch_marshal_attr(embeds.Embed, "timestamp", return_value=mock_datetime): + return embeds.Embed.deserialize(test_embed_payload), mock_datetime + + def test_deserialize( + self, + test_embed_payload, + test_footer_payload, + test_image_payload, + test_thumbnail_payload, + test_video_payload, + test_provider_payload, + test_author_payload, + test_field_payload, + ): + mock_datetime = mock.MagicMock(datetime.datetime) + + with _helpers.patch_marshal_attr(embeds.Embed, "timestamp", return_value=mock_datetime): + embed_obj = embeds.Embed.deserialize(test_embed_payload) + + assert embed_obj.title == "embed title" + assert embed_obj.type == embeds.EmbedType.ARTICLE + assert embed_obj.description == "embed description" + assert embed_obj.url == "https://somewhere.com" + assert embed_obj.timestamp == mock_datetime + assert embed_obj.color == colors.Color(14014915) + assert embed_obj.footer == embeds.EmbedFooter.deserialize(test_footer_payload) + assert embed_obj.image == embeds.EmbedImage.deserialize(test_image_payload) + assert embed_obj.thumbnail == embeds.EmbedThumbnail.deserialize(test_thumbnail_payload) + assert embed_obj.video == embeds.EmbedVideo.deserialize(test_video_payload) + assert embed_obj.provider == embeds.EmbedProvider.deserialize(test_provider_payload) + assert embed_obj.author == embeds.EmbedAuthor.deserialize(test_author_payload) + assert embed_obj.fields == [embeds.EmbedField.deserialize(test_field_payload)] + + def test_serialize(self, test_embed_payload): + embed_obj = embeds.Embed.deserialize(test_embed_payload) + + assert embed_obj.serialize() == test_embed_payload diff --git a/tests/hikari/core/test_emojis.py b/tests/hikari/core/test_emojis.py new file mode 100644 index 0000000000..473b514017 --- /dev/null +++ b/tests/hikari/core/test_emojis.py @@ -0,0 +1,74 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# Copyright © Nekoka.tt 2019-2020 +# +# This file is part of Hikari. +# +# Hikari is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Hikari is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along ith Hikari. If not, see . +import cymock as mock +import pytest + +from hikari.core import emojis +from hikari.core import users +from tests.hikari import _helpers + + +class TestUnicodeEmoji: + def test_deserialize(self): + emoji_obj = emojis.UnicodeEmoji.deserialize({"name": "🤷"}) + + assert emoji_obj.name == "🤷" + + +class TestUnknownEmoji: + def test_deserialize(self): + emoji_obj = emojis.UnknownEmoji.deserialize({"id": "1234", "name": "test", "animated": True}) + + assert emoji_obj.id == 1234 + assert emoji_obj.name == "test" + assert emoji_obj.is_animated is True + + +class TestGuildEmoji: + def test_deserialize(self): + mock_user = mock.MagicMock(users.User) + + with _helpers.patch_marshal_attr(emojis.GuildEmoji, "user", return_value=mock_user): + emoji_obj = emojis.GuildEmoji.deserialize( + { + "id": "12345", + "name": "testing", + "animated": False, + "roles": ["123", "456"], + "user": {"id": "123456", "username": "hikari", "discriminator": "0000", "avatar": None}, + "require_colons": True, + "managed": False, + } + ) + + assert emoji_obj.id == 12345 + assert emoji_obj.name == "testing" + assert emoji_obj.is_animated is False + assert emoji_obj.role_ids == {123, 456} + assert emoji_obj.user == mock_user + assert emoji_obj.is_colons_required is True + assert emoji_obj.is_managed is False + + +@pytest.mark.parametrize( + ["payload", "expected_type"], + [({"name": "🤷"}, emojis.UnicodeEmoji), ({"id": "1234", "name": "test"}, emojis.UnknownEmoji)], +) +def test_deserialize_reaction_emoji_returns_expected_type(payload, expected_type): + assert isinstance(emojis.deserialize_reaction_emoji(payload), expected_type) diff --git a/tests/hikari/core/test_invites.py b/tests/hikari/core/test_invites.py index beede601fe..bd6e85e1b3 100644 --- a/tests/hikari/core/test_invites.py +++ b/tests/hikari/core/test_invites.py @@ -216,7 +216,7 @@ def mock_invite_with_metadata(self, *args, test_invite_with_metadata_payload): def test_deserialize(self, *deserializers, test_invite_with_metadata_payload): mock_datetime = mock.MagicMock(datetime.datetime) with _helpers.patch_marshal_attr( - invites.InviteWithMetadata, "created_at", deserializers=dates.parse_iso_8601_ts, return_value=mock_datetime + invites.InviteWithMetadata, "created_at", deserializer=dates.parse_iso_8601_ts, return_value=mock_datetime ) as mock_created_at_deserializer: invite_with_metadata_obj = invites.InviteWithMetadata.deserialize(test_invite_with_metadata_payload) mock_created_at_deserializer.assert_called_once_with("2015-04-26T06:26:56.936000+00:00") diff --git a/tests/hikari/core/test_messages.py b/tests/hikari/core/test_messages.py new file mode 100644 index 0000000000..8d62829cc9 --- /dev/null +++ b/tests/hikari/core/test_messages.py @@ -0,0 +1,196 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# Copyright © Nekoka.tt 2019-2020 +# +# This file is part of Hikari. +# +# Hikari is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Hikari is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along ith Hikari. If not, see . +import datetime + +import cymock as mock +import pytest + +from hikari.core import messages +from hikari.core import users +from hikari.core import guilds +from hikari.core import emojis +from hikari.core import embeds +from hikari.core import oauth2 +from hikari.internal_utilities import dates +from tests.hikari import _helpers + + +@pytest.fixture +def test_attachment_payload(): + return { + "id": "690922406474154014", + "filename": "IMG.jpg", + "size": 660521, + "url": "https://somewhere.com/attachments/123/456/IMG.jpg", + "proxy_url": "https://media.somewhere.com/attachments/123/456/IMG.jpg", + "width": 1844, + "height": 2638, + } + + +@pytest.fixture +def test_reaction_payload(): + return {"emoji": {"id": "691225175349395456", "name": "test"}, "count": 100, "me": True} + + +@pytest.fixture +def test_message_activity_payload(): + return {"type": 5, "party_id": "ae488379-351d-4a4f-ad32-2b9b01c91657"} + + +@pytest.fixture +def test_message_crosspost_payload(): + return {"channel_id": "278325129692446722", "guild_id": "278325129692446720", "message_id": "306588351130107906"} + + +@pytest.fixture +def test_message_payload( + test_attachment_payload, test_reaction_payload, test_message_activity_payload, test_message_crosspost_payload, +): + return { + "id": "123", + "channel_id": "456", + "guild_id": "678", + "author": { + "bot": True, + "id": "1234", + "username": "cool username", + "avatar": "6608709a3274e1812beb4e8de6631111", + "discriminator": "0000", + }, + "member": {"user": {}}, + "content": "some info", + "timestamp": "2020-03-21T21:20:16.510000+00:00", + "edited_timestamp": "2020-04-21T21:20:16.510000+00:00", + "tts": True, + "mention_everyone": True, + "mentions": [ + {"id": "5678", "username": "uncool username", "avatar": "129387dskjafhasf", "discriminator": "4532"} + ], + "mention_roles": ["987"], + "mention_channels": [{"id": "456", "guild_id": "678", "type": 1, "name": "hikari-testing"}], + "attachments": [test_attachment_payload], + "embeds": [{}], + "reactions": [test_reaction_payload], + "pinned": True, + "webhook_id": "1234", + "type": 0, + "activity": test_message_activity_payload, + "application": { + "id": "456", + "name": "hikari", + "description": "The best app", + "icon": "2658b3029e775a931ffb49380073fa63", + "cover_image": "58982a23790c4f22787b05d3be38a026", + }, + "message_reference": test_message_crosspost_payload, + "flags": 2, + } + + +class TestAttachment: + def test_deserialize(self, test_attachment_payload): + attachment_obj = messages.Attachment.deserialize(test_attachment_payload) + + assert attachment_obj.id == 690922406474154014 + assert attachment_obj.filename == "IMG.jpg" + assert attachment_obj.size == 660521 + assert attachment_obj.url == "https://somewhere.com/attachments/123/456/IMG.jpg" + assert attachment_obj.proxy_url == "https://media.somewhere.com/attachments/123/456/IMG.jpg" + assert attachment_obj.height == 2638 + assert attachment_obj.width == 1844 + + +class TestReaction: + def test_deserialize(self, test_reaction_payload): + mock_emoji = mock.MagicMock(emojis.UnknownEmoji) + + with _helpers.patch_marshal_attr(messages.Reaction, "emoji", return_value=mock_emoji): + reaction_obj = messages.Reaction.deserialize(test_reaction_payload) + + assert reaction_obj.count == 100 + assert reaction_obj.emoji == mock_emoji + assert reaction_obj.is_reacted_by_me is True + + +class TestMessageActivity: + def test_deserialize(self, test_message_activity_payload): + message_activity_obj = messages.MessageActivity.deserialize(test_message_activity_payload) + + assert message_activity_obj.type == messages.MessageActivityType.JOIN_REQUEST + assert message_activity_obj.party_id == "ae488379-351d-4a4f-ad32-2b9b01c91657" + + +class TestMessageCrosspost: + def test_deserialize(self, test_message_crosspost_payload): + message_crosspost_obj = messages.MessageCrosspost.deserialize(test_message_crosspost_payload) + + assert message_crosspost_obj.message_id == 306588351130107906 + assert message_crosspost_obj.channel_id == 278325129692446722 + assert message_crosspost_obj.guild_id == 278325129692446720 + + +class TestMessage: + def test_deserialize( + self, + test_message_payload, + test_attachment_payload, + test_reaction_payload, + test_message_activity_payload, + test_message_crosspost_payload, + ): + mock_user = mock.MagicMock(users.User) + mock_member = mock.MagicMock(guilds.GuildMember) + mock_datetime = mock.MagicMock(datetime.datetime) + mock_datetime2 = mock.MagicMock(datetime.datetime) + mock_emoji = mock.MagicMock(emojis.UnknownEmoji) + mock_app = mock.MagicMock(oauth2.Application) + + with _helpers.patch_marshal_attr(messages.Message, "author", return_value=mock_user): + with _helpers.patch_marshal_attr(messages.Message, "member", return_value=mock_member): + with _helpers.patch_marshal_attr(messages.Message, "timestamp", return_value=mock_datetime): + with _helpers.patch_marshal_attr(messages.Message, "edited_timestamp", return_value=mock_datetime2): + with _helpers.patch_marshal_attr(messages.Reaction, "emoji", return_value=mock_emoji): + with _helpers.patch_marshal_attr(messages.Message, "application", return_value=mock_app): + message_obj = messages.Message.deserialize(test_message_payload) + + assert message_obj.reactions == [messages.Reaction.deserialize(test_reaction_payload)] + + assert message_obj.id == 123 + assert message_obj.channel_id == 456 + assert message_obj.guild_id == 678 + assert message_obj.author == mock_user + assert message_obj.member == mock_member + assert message_obj.content == "some info" + assert message_obj.timestamp == mock_datetime + assert message_obj.edited_timestamp == mock_datetime2 + assert message_obj.is_tts is True + assert message_obj.is_mentioning_everyone is True + assert message_obj.user_mentions == {5678} + assert message_obj.role_mentions == {987} + assert message_obj.channel_mentions == {456} + assert message_obj.attachments == [messages.Attachment.deserialize(test_attachment_payload)] + assert message_obj.embeds == [embeds.Embed.deserialize({})] + assert message_obj.is_pinned is True + assert message_obj.webhook_id == 1234 + assert message_obj.type == messages.MessageType.DEFAULT + assert message_obj.activity == messages.MessageActivity.deserialize(test_message_activity_payload) + assert message_obj.application == mock_app + assert message_obj.message_reference == messages.MessageCrosspost.deserialize(test_message_crosspost_payload) + assert message_obj.flags == messages.MessageFlag.IS_CROSSPOST diff --git a/tests/hikari/core/test_webhook.py b/tests/hikari/core/test_webhook.py new file mode 100644 index 0000000000..64793a9b84 --- /dev/null +++ b/tests/hikari/core/test_webhook.py @@ -0,0 +1,51 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# Copyright © Nekoka.tt 2019-2020 +# +# This file is part of Hikari. +# +# Hikari is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Hikari is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along ith Hikari. If not, see . +import cymock as mock +import pytest + +from hikari.core import webhooks +from hikari.core import users +from tests.hikari import _helpers + + +class TestWebhook: + def test_deserialize(self): + payload = { + "id": "1234", + "type": 1, + "guild_id": "123", + "channel_id": "456", + "user": {"id": "123456", "username": "hikari", "discriminator": "0000", "avatar": None}, + "name": "hikari webhook", + "avatar": "bb71f469c158984e265093a81b3397fb", + "token": "ueoqrialsdfaKJLKfajslkdf", + } + mock_user = mock.MagicMock(users.User) + + with _helpers.patch_marshal_attr(webhooks.Webhook, "user", return_value=mock_user): + webhook_obj = webhooks.Webhook.deserialize(payload) + + assert webhook_obj.id == 1234 + assert webhook_obj.type == webhooks.WebhookType.INCOMING + assert webhook_obj.guild_id == 123 + assert webhook_obj.channel_id == 456 + assert webhook_obj.user == mock_user + assert webhook_obj.name == "hikari webhook" + assert webhook_obj.avatar_hash == "bb71f469c158984e265093a81b3397fb" + assert webhook_obj.token == "ueoqrialsdfaKJLKfajslkdf" From 18a13de489c82b79e0527ec3437f3d0f26646bb6 Mon Sep 17 00:00:00 2001 From: Faster Speeding Date: Tue, 24 Mar 2020 01:58:29 +0000 Subject: [PATCH 030/922] Fix broken marshaller defaults and add type validation. --- hikari/core/clients/gateway.py | 2 +- hikari/core/emojis.py | 4 +++- hikari/core/messages.py | 4 ++-- hikari/internal_utilities/marshaller.py | 17 +++++++++++++++-- .../internal_utilities/test_marshaller.py | 15 +++++++++++++++ .../test_marshaller_pep563.py | 15 +++++++++++++++ 6 files changed, 51 insertions(+), 6 deletions(-) diff --git a/hikari/core/clients/gateway.py b/hikari/core/clients/gateway.py index fb4adba8cb..0001a309d1 100644 --- a/hikari/core/clients/gateway.py +++ b/hikari/core/clients/gateway.py @@ -38,4 +38,4 @@ class GatewayActivity(entities.Deserializable): #: The activity type. #: #: :type: :obj:`int` - type: int = marshaller.attrib(deserializer=int, serializer=int, if_undefined=0) + type: int = marshaller.attrib(deserializer=int, serializer=int, if_undefined=lambda: 0) diff --git a/hikari/core/emojis.py b/hikari/core/emojis.py index b001223604..abb12dd0cd 100644 --- a/hikari/core/emojis.py +++ b/hikari/core/emojis.py @@ -65,7 +65,9 @@ class GuildEmoji(UnknownEmoji): #: #: :type: :obj:`typing.Set` [ :obj:`snowflakes.Snowflake` ] role_ids: typing.Set[snowflakes.Snowflake] = marshaller.attrib( - raw_name="roles", deserializer=lambda roles: {r for r in map(snowflakes.Snowflake, roles)}, if_undefined={} + raw_name="roles", + deserializer=lambda roles: {snowflakes.Snowflake.deserialize(r) for r in roles}, + if_undefined=dict, ) #: The user that created the emoji. diff --git a/hikari/core/messages.py b/hikari/core/messages.py index f4737f3aac..c07c064345 100644 --- a/hikari/core/messages.py +++ b/hikari/core/messages.py @@ -285,7 +285,7 @@ class Message(snowflakes.UniqueEntity, entities.Deserializable): channel_mentions: typing.Set[snowflakes.Snowflake] = marshaller.attrib( raw_name="mention_channels", deserializer=lambda channel_mentions: {snowflakes.Snowflake(c["id"]) for c in channel_mentions}, - if_undefined={}, + if_undefined=dict, ) #: The message attachments. @@ -306,7 +306,7 @@ class Message(snowflakes.UniqueEntity, entities.Deserializable): #: #: :type: :obj:`typing.Sequence` [ :obj:`Reaction` ] reactions: typing.Sequence[Reaction] = marshaller.attrib( - deserializer=lambda reactions: [r for r in map(Reaction.deserialize, reactions)], if_undefined={} + deserializer=lambda reactions: [Reaction.deserialize(r) for r in reactions], if_undefined=dict ) #: Whether the message is pinned. diff --git a/hikari/internal_utilities/marshaller.py b/hikari/internal_utilities/marshaller.py index 9634a6394b..613dafba45 100644 --- a/hikari/internal_utilities/marshaller.py +++ b/hikari/internal_utilities/marshaller.py @@ -171,6 +171,17 @@ def error(*_, **__) -> typing.NoReturn: return error +def _default_validator(value: typing.Any): + assertions.assert_that( + value is RAISE or value is None or callable(value), + message=( + "Invalid default factory passed for `if_undefined` or `if_none`; " + f"expected a callable or `None` but got {value}." + ), + error_type=RuntimeError, + ) + + class _AttributeDescriptor: __slots__ = ( "raw_name", "field_name", "constructor_name", "if_none", "if_undefined", "is_transient", "deserializer", @@ -181,12 +192,14 @@ def __init__( raw_name: str, field_name: str, constructor_name: str, - if_none: typing.Callable[..., typing.Any], - if_undefined: typing.Callable[..., typing.Any], + if_none: typing.Union[typing.Callable[..., typing.Any], None, type(RAISE)], + if_undefined: typing.Union[typing.Callable[..., typing.Any], None, type(RAISE)], is_transient: bool, deserializer: typing.Callable[[typing.Any], typing.Any], serializer: typing.Callable[[typing.Any], typing.Any], ) -> None: + _default_validator(if_undefined) + _default_validator(if_none) self.raw_name = raw_name self.field_name = field_name self.constructor_name = constructor_name diff --git a/tests/hikari/internal_utilities/test_marshaller.py b/tests/hikari/internal_utilities/test_marshaller.py index 8b79a55a10..00a63fe9ff 100644 --- a/tests/hikari/internal_utilities/test_marshaller.py +++ b/tests/hikari/internal_utilities/test_marshaller.py @@ -57,6 +57,21 @@ def test_invokes_attrs(self): ) +@pytest.mark.parametrize("data", [2, "d", bytes("ok", "utf-8"), [], {}, set()]) +@_helpers.assert_raises(type_=RuntimeError) +def test_default_validator_raises_runtime_error(data): + marshaller._default_validator(data) + + +def method_stub(value): + ... + + +@pytest.mark.parametrize("data", [lambda x: "ok", None, marshaller.RAISE, dict, method_stub]) +def test_default_validator(data): + marshaller._default_validator(data) + + class TestAttrs: def test_invokes_attrs(self): marshaller_mock = mock.create_autospec(marshaller.HikariEntityMarshaller, spec_set=True) diff --git a/tests/hikari/internal_utilities/test_marshaller_pep563.py b/tests/hikari/internal_utilities/test_marshaller_pep563.py index 19b0ec0223..259cbdaa5e 100644 --- a/tests/hikari/internal_utilities/test_marshaller_pep563.py +++ b/tests/hikari/internal_utilities/test_marshaller_pep563.py @@ -85,6 +85,21 @@ class Foo: marshaller_mock.register.assert_called_once_with(Foo) +@pytest.mark.parametrize("data", [2, "d", bytes("ok", "utf-8"), [], {}, set()]) +@_helpers.assert_raises(type_=RuntimeError) +def test_default_validator_raises_runtime_error(data): + marshaller._default_validator(data) + + +def method_stub(value): + ... + + +@pytest.mark.parametrize("data", [lambda x: "ok", None, marshaller.RAISE, dict, method_stub]) +def test_default_validator(data): + marshaller._default_validator(data) + + class TestMarshallerPep563: @pytest.fixture() def marshaller_impl(self): From 079d135b2f365df8a68a2373d5064c604b0c7f33 Mon Sep 17 00:00:00 2001 From: Faster Speeding Date: Fri, 27 Mar 2020 20:27:08 +0000 Subject: [PATCH 031/922] Remove "type" field from embed model and EventType enum --- hikari/core/embeds.py | 27 --------------------------- tests/hikari/core/test_embeds.py | 2 -- 2 files changed, 29 deletions(-) diff --git a/hikari/core/embeds.py b/hikari/core/embeds.py index 640f1cbf78..104b29856e 100644 --- a/hikari/core/embeds.py +++ b/hikari/core/embeds.py @@ -19,7 +19,6 @@ __all__ = [ "Embed", - "EmbedType", "EmbedThumbnail", "EmbedVideo", "EmbedImage", @@ -30,7 +29,6 @@ ] import typing -import enum import datetime from hikari.core import entities @@ -39,24 +37,6 @@ from hikari.internal_utilities import dates -@enum.unique -class EmbedType(str, enum.Enum): - """The type of a embed.""" - - #: Generic embed rendered from embed attributes. - RICH = "rich" - #: Image embed. - IMAGE = "image" - #: Video embed. - VIDEO = "video" - #: Animated gif image embed renderered as a video embed. - GFV = "gfv" - #: Article embed. - ARTICLE = "article" - #: Link embed. - LINK = "link" - - @marshaller.attrs(slots=True) class EmbedFooter(entities.HikariEntity, entities.Deserializable, entities.Serializable): """Represents a embed footer.""" @@ -218,13 +198,6 @@ class Embed(entities.HikariEntity, entities.Deserializable, entities.Serializabl #: :type: :obj:`str`, optional title: typing.Optional[str] = marshaller.attrib(deserializer=str, serializer=str, if_undefined=None) - #: The type of the embed. - #: - #: :type: :obj:`EmbedType` - type: EmbedType = marshaller.attrib( - deserializer=EmbedType, serializer=str.lower, if_undefined=lambda: EmbedType.RICH - ) - #: The description of the embed. #: #: :type: :obj:`str`, optional diff --git a/tests/hikari/core/test_embeds.py b/tests/hikari/core/test_embeds.py index 8138f92360..c0de32395b 100644 --- a/tests/hikari/core/test_embeds.py +++ b/tests/hikari/core/test_embeds.py @@ -97,7 +97,6 @@ def test_embed_payload( ): return { "title": "embed title", - "type": "article", "description": "embed description", "url": "https://somewhere.com", "timestamp": "2020-03-22T16:40:39.218000+00:00", @@ -238,7 +237,6 @@ def test_deserialize( embed_obj = embeds.Embed.deserialize(test_embed_payload) assert embed_obj.title == "embed title" - assert embed_obj.type == embeds.EmbedType.ARTICLE assert embed_obj.description == "embed description" assert embed_obj.url == "https://somewhere.com" assert embed_obj.timestamp == mock_datetime From 9aed23102b6dd53a453ec8df8b5afc86cfc56a0a Mon Sep 17 00:00:00 2001 From: Faster Speeding Date: Fri, 27 Mar 2020 20:04:05 +0000 Subject: [PATCH 032/922] Make `serializer` or `deserializer` kwarg mandatory for patch_marshal_attr and fix some broken attribute deserialisers. --- hikari/core/emojis.py | 4 +- hikari/core/webhooks.py | 2 +- tests/hikari/_helpers.py | 18 +++-- tests/hikari/core/test_embeds.py | 5 +- tests/hikari/core/test_emojis.py | 8 ++- tests/hikari/core/test_messages.py | 104 ++++++++++++++++++++++------- tests/hikari/core/test_oauth2.py | 8 +-- tests/hikari/core/test_webhook.py | 8 ++- 8 files changed, 116 insertions(+), 41 deletions(-) diff --git a/hikari/core/emojis.py b/hikari/core/emojis.py index abb12dd0cd..23cced6a0b 100644 --- a/hikari/core/emojis.py +++ b/hikari/core/emojis.py @@ -79,7 +79,9 @@ class GuildEmoji(UnknownEmoji): #: () #: #: :type: :obj:`users.User`, optional - user: typing.Optional[users.User] = marshaller.attrib(deserializer=users.User, if_none=None, if_undefined=None) + user: typing.Optional[users.User] = marshaller.attrib( + deserializer=users.User.deserialize, if_none=None, if_undefined=None + ) #: Whether this emoji must be wrapped in colons. #: diff --git a/hikari/core/webhooks.py b/hikari/core/webhooks.py index 8bd08acfb1..4735e36213 100644 --- a/hikari/core/webhooks.py +++ b/hikari/core/webhooks.py @@ -64,7 +64,7 @@ class Webhook(snowflakes.UniqueEntity, entities.Deserializable): #: This will be ``None`` when getting a webhook with a token #: #: :type: :obj:`users.User`, optional - user: typing.Optional[users.User] = marshaller.attrib(deserializer=users.User, if_undefined=None) + user: typing.Optional[users.User] = marshaller.attrib(deserializer=users.User.deserialize, if_undefined=None) #: The default name of the webhook. #: diff --git a/tests/hikari/_helpers.py b/tests/hikari/_helpers.py index cc32a0e956..af7b4ecefb 100644 --- a/tests/hikari/_helpers.py +++ b/tests/hikari/_helpers.py @@ -446,17 +446,25 @@ def create_autospec(spec, *args, **kwargs): return mock.create_autospec(spec, spec_set=True, *args, **kwargs) -def patch_marshal_attr(target_entity, field_name, *args, deserializer=None, **kwargs): +def patch_marshal_attr(target_entity, field_name, *args, deserializer=None, serializer=None, **kwargs): + if not (deserializer or serializer): + raise TypeError("patch_marshal_attr() Missing required keyword-only argument: 'deserializer' or 'serializer'") + if deserializer and serializer: + raise TypeError( + "patch_marshal_attr() Expected one of either keyword-arguments 'deserializer' or 'serializer', not both." + ) + + target_type = "deserializer" if deserializer else "serializer" # noinspection PyProtectedMember for attr in marshaller.HIKARI_ENTITY_MARSHALLER._registered_entities[target_entity].attribs: - if attr.field_name == field_name and (deserializer is None or attr.deserializer == deserializer): + if attr.field_name == field_name and (serializer or deserializer) == getattr(attr, target_type): target = attr break elif attr.field_name == field_name: raise TypeError( - f"Deserializer mismatch found on `{target_entity.__name__}.{attr.field_name}`; " - f"expected `{deserializer}` but got `{attr.deserializer}`." + f"{target_type.capitalize()} mismatch found on `{target_entity.__name__}" + f".{attr.field_name}`; expected `{deserializer or serializer}` but got `{getattr(attr, target_type)}`." ) else: raise LookupError(f"Failed to find a `{field_name}` field on `{target_entity.__name__}`.") - return mock.patch.object(target, "deserializer", *args, **kwargs) + return mock.patch.object(target, target_type, *args, **kwargs) diff --git a/tests/hikari/core/test_embeds.py b/tests/hikari/core/test_embeds.py index c0de32395b..dce48c00d8 100644 --- a/tests/hikari/core/test_embeds.py +++ b/tests/hikari/core/test_embeds.py @@ -233,8 +233,11 @@ def test_deserialize( ): mock_datetime = mock.MagicMock(datetime.datetime) - with _helpers.patch_marshal_attr(embeds.Embed, "timestamp", return_value=mock_datetime): + with _helpers.patch_marshal_attr( + embeds.Embed, "timestamp", deserializer=dates.parse_iso_8601_ts, return_value=mock_datetime + ) as patched_timestamp_deserializer: embed_obj = embeds.Embed.deserialize(test_embed_payload) + patched_timestamp_deserializer.assert_called_once_with("2020-03-22T16:40:39.218000+00:00") assert embed_obj.title == "embed title" assert embed_obj.description == "embed description" diff --git a/tests/hikari/core/test_emojis.py b/tests/hikari/core/test_emojis.py index 473b514017..97d30c1912 100644 --- a/tests/hikari/core/test_emojis.py +++ b/tests/hikari/core/test_emojis.py @@ -44,18 +44,22 @@ class TestGuildEmoji: def test_deserialize(self): mock_user = mock.MagicMock(users.User) - with _helpers.patch_marshal_attr(emojis.GuildEmoji, "user", return_value=mock_user): + test_user_payload = {"id": "123456", "username": "hikari", "discriminator": "0000", "avatar": None} + with _helpers.patch_marshal_attr( + emojis.GuildEmoji, "user", deserializer=users.User.deserialize, return_value=mock_user + ) as patched_user_deserializer: emoji_obj = emojis.GuildEmoji.deserialize( { "id": "12345", "name": "testing", "animated": False, "roles": ["123", "456"], - "user": {"id": "123456", "username": "hikari", "discriminator": "0000", "avatar": None}, + "user": test_user_payload, "require_colons": True, "managed": False, } ) + patched_user_deserializer.assert_called_once_with(test_user_payload) assert emoji_obj.id == 12345 assert emoji_obj.name == "testing" diff --git a/tests/hikari/core/test_messages.py b/tests/hikari/core/test_messages.py index 8d62829cc9..409d6de147 100644 --- a/tests/hikari/core/test_messages.py +++ b/tests/hikari/core/test_messages.py @@ -59,22 +59,49 @@ def test_message_crosspost_payload(): return {"channel_id": "278325129692446722", "guild_id": "278325129692446720", "message_id": "306588351130107906"} +@pytest.fixture() +def test_application_payload(): + return { + "id": "456", + "name": "hikari", + "description": "The best app", + "icon": "2658b3029e775a931ffb49380073fa63", + "cover_image": "58982a23790c4f22787b05d3be38a026", + } + + +@pytest.fixture() +def test_user_payload(): + return { + "bot": True, + "id": "1234", + "username": "cool username", + "avatar": "6608709a3274e1812beb4e8de6631111", + "discriminator": "0000", + } + + +@pytest.fixture() +def test_member_payload(test_user_payload): + return {"user": test_user_payload} + + @pytest.fixture def test_message_payload( - test_attachment_payload, test_reaction_payload, test_message_activity_payload, test_message_crosspost_payload, + test_application_payload, + test_attachment_payload, + test_reaction_payload, + test_user_payload, + test_member_payload, + test_message_activity_payload, + test_message_crosspost_payload, ): return { "id": "123", "channel_id": "456", "guild_id": "678", - "author": { - "bot": True, - "id": "1234", - "username": "cool username", - "avatar": "6608709a3274e1812beb4e8de6631111", - "discriminator": "0000", - }, - "member": {"user": {}}, + "author": test_user_payload, + "member": test_member_payload, "content": "some info", "timestamp": "2020-03-21T21:20:16.510000+00:00", "edited_timestamp": "2020-04-21T21:20:16.510000+00:00", @@ -92,13 +119,7 @@ def test_message_payload( "webhook_id": "1234", "type": 0, "activity": test_message_activity_payload, - "application": { - "id": "456", - "name": "hikari", - "description": "The best app", - "icon": "2658b3029e775a931ffb49380073fa63", - "cover_image": "58982a23790c4f22787b05d3be38a026", - }, + "application": test_application_payload, "message_reference": test_message_crosspost_payload, "flags": 2, } @@ -121,7 +142,9 @@ class TestReaction: def test_deserialize(self, test_reaction_payload): mock_emoji = mock.MagicMock(emojis.UnknownEmoji) - with _helpers.patch_marshal_attr(messages.Reaction, "emoji", return_value=mock_emoji): + with _helpers.patch_marshal_attr( + messages.Reaction, "emoji", return_value=mock_emoji, deserializer=emojis.deserialize_reaction_emoji + ): reaction_obj = messages.Reaction.deserialize(test_reaction_payload) assert reaction_obj.count == 100 @@ -150,8 +173,11 @@ class TestMessage: def test_deserialize( self, test_message_payload, + test_application_payload, test_attachment_payload, test_reaction_payload, + test_user_payload, + test_member_payload, test_message_activity_payload, test_message_crosspost_payload, ): @@ -159,18 +185,46 @@ def test_deserialize( mock_member = mock.MagicMock(guilds.GuildMember) mock_datetime = mock.MagicMock(datetime.datetime) mock_datetime2 = mock.MagicMock(datetime.datetime) - mock_emoji = mock.MagicMock(emojis.UnknownEmoji) + mock_emoji = mock.MagicMock(messages._emojis) mock_app = mock.MagicMock(oauth2.Application) - with _helpers.patch_marshal_attr(messages.Message, "author", return_value=mock_user): - with _helpers.patch_marshal_attr(messages.Message, "member", return_value=mock_member): - with _helpers.patch_marshal_attr(messages.Message, "timestamp", return_value=mock_datetime): - with _helpers.patch_marshal_attr(messages.Message, "edited_timestamp", return_value=mock_datetime2): - with _helpers.patch_marshal_attr(messages.Reaction, "emoji", return_value=mock_emoji): - with _helpers.patch_marshal_attr(messages.Message, "application", return_value=mock_app): + with _helpers.patch_marshal_attr( + messages.Message, "author", deserializer=users.User.deserialize, return_value=mock_user + ) as patched_author_deserializer: + with _helpers.patch_marshal_attr( + messages.Message, "member", deserializer=guilds.GuildMember.deserialize, return_value=mock_member + ) as patched_member_deserializer: + with _helpers.patch_marshal_attr( + messages.Message, "timestamp", deserializer=dates.parse_iso_8601_ts, return_value=mock_datetime + ) as patched_timestamp_deserializer: + with _helpers.patch_marshal_attr( + messages.Message, + "edited_timestamp", + deserializer=dates.parse_iso_8601_ts, + return_value=mock_datetime2, + ) as patched_edited_timestamp_deserializer: + with _helpers.patch_marshal_attr( + messages.Message, + "application", + deserializer=oauth2.Application.deserialize, + return_value=mock_app, + ) as patched_application_deserializer: + with _helpers.patch_marshal_attr( + messages.Reaction, + "emoji", + deserializer=emojis.deserialize_reaction_emoji, + return_value=mock_emoji, + ) as patched_emoji_deserializer: message_obj = messages.Message.deserialize(test_message_payload) - + patched_emoji_deserializer.assert_called_once_with(test_reaction_payload["emoji"]) assert message_obj.reactions == [messages.Reaction.deserialize(test_reaction_payload)] + patched_application_deserializer.assert_called_once_with(test_application_payload) + patched_edited_timestamp_deserializer.assert_called_once_with( + "2020-04-21T21:20:16.510000+00:00" + ) + patched_timestamp_deserializer.assert_called_once_with("2020-03-21T21:20:16.510000+00:00") + patched_member_deserializer.assert_called_once_with(test_member_payload) + patched_author_deserializer.assert_called_once_with(test_user_payload) assert message_obj.id == 123 assert message_obj.channel_id == 456 diff --git a/tests/hikari/core/test_oauth2.py b/tests/hikari/core/test_oauth2.py index d46c201b0c..a9faa17fbe 100644 --- a/tests/hikari/core/test_oauth2.py +++ b/tests/hikari/core/test_oauth2.py @@ -124,11 +124,11 @@ def team_obj(self, team_payload): return oauth2.Team.deserialize(team_payload) def test_deserialize(self, team_payload, member_payload): - mock_members = {123: mock.MagicMock(oauth2.Team)} - with _helpers.patch_marshal_attr(oauth2.Team, "members", return_value=mock_members) as patched_deserializer: + mock_member = mock.MagicMock(oauth2.Team, user=mock.MagicMock(id=123)) + with mock.patch.object(oauth2.TeamMember, "deserialize", return_value=mock_member): team_obj = oauth2.Team.deserialize(team_payload) - patched_deserializer.assert_called_once_with([member_payload]) - assert team_obj.members is mock_members + oauth2.TeamMember.deserialize.assert_called_once_with(member_payload) + assert team_obj.members == {123: mock_member} assert team_obj.icon_hash == "hashtag" assert team_obj.id == 202020202 assert team_obj.owner_user_id == 393030292 diff --git a/tests/hikari/core/test_webhook.py b/tests/hikari/core/test_webhook.py index 64793a9b84..77c612ac47 100644 --- a/tests/hikari/core/test_webhook.py +++ b/tests/hikari/core/test_webhook.py @@ -26,20 +26,24 @@ class TestWebhook: def test_deserialize(self): + test_user_payload = {"id": "123456", "username": "hikari", "discriminator": "0000", "avatar": None} payload = { "id": "1234", "type": 1, "guild_id": "123", "channel_id": "456", - "user": {"id": "123456", "username": "hikari", "discriminator": "0000", "avatar": None}, + "user": test_user_payload, "name": "hikari webhook", "avatar": "bb71f469c158984e265093a81b3397fb", "token": "ueoqrialsdfaKJLKfajslkdf", } mock_user = mock.MagicMock(users.User) - with _helpers.patch_marshal_attr(webhooks.Webhook, "user", return_value=mock_user): + with _helpers.patch_marshal_attr( + webhooks.Webhook, "user", deserializer=users.User.deserialize, return_value=mock_user + ) as mock_user_deserializer: webhook_obj = webhooks.Webhook.deserialize(payload) + mock_user_deserializer.assert_called_once_with(test_user_payload) assert webhook_obj.id == 1234 assert webhook_obj.type == webhooks.WebhookType.INCOMING From 0fd1d1b3d6c1db014f6002106d03d04b474eda08 Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Sat, 28 Mar 2020 18:27:13 +0000 Subject: [PATCH 033/922] Reformatted changes. --- hikari/internal_utilities/marshaller.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/hikari/internal_utilities/marshaller.py b/hikari/internal_utilities/marshaller.py index 613dafba45..0a9755a153 100644 --- a/hikari/internal_utilities/marshaller.py +++ b/hikari/internal_utilities/marshaller.py @@ -184,8 +184,15 @@ def _default_validator(value: typing.Any): class _AttributeDescriptor: __slots__ = ( - "raw_name", "field_name", "constructor_name", "if_none", "if_undefined", "is_transient", "deserializer", - "serializer") + "raw_name", + "field_name", + "constructor_name", + "if_none", + "if_undefined", + "is_transient", + "deserializer", + "serializer", + ) def __init__( self, @@ -213,7 +220,7 @@ def __init__( class _EntityDescriptor: __slots__ = ("entity_type", "attribs") - def __init__(self, entity_type: typing.Type, attribs: typing.Collection[_AttributeDescriptor], ) -> None: + def __init__(self, entity_type: typing.Type, attribs: typing.Collection[_AttributeDescriptor],) -> None: self.entity_type = entity_type self.attribs = tuple(attribs) From fb1ce4a5279d2d2ea2ec07e37ba059ce87907b8c Mon Sep 17 00:00:00 2001 From: Faster Speeding Date: Tue, 24 Mar 2020 03:50:06 +0000 Subject: [PATCH 034/922] Add event related models + tests --- hikari/core/__init__.py | 1 + hikari/core/channels.py | 58 +- hikari/core/configs/gateway.py | 6 +- hikari/core/configs/protocol.py | 4 +- hikari/core/embeds.py | 8 +- hikari/core/emojis.py | 6 +- hikari/core/entities.py | 19 +- hikari/core/events.py | 1107 ++++++++++++++--- hikari/core/guilds.py | 444 ++++++- hikari/core/invites.py | 16 +- hikari/core/messages.py | 13 +- hikari/core/users.py | 4 +- hikari/core/voices.py | 128 ++ hikari/core/webhooks.py | 2 +- hikari/internal_utilities/marshaller.py | 36 +- tests/hikari/core/test_channels.py | 26 +- tests/hikari/core/test_entities.py | 34 + tests/hikari/core/test_events.py | 907 ++++++++++++++ tests/hikari/core/test_guilds.py | 294 ++++- tests/hikari/core/test_invites.py | 6 +- tests/hikari/core/test_voices.py | 66 + .../internal_utilities/test_marshaller.py | 18 +- .../test_marshaller_pep563.py | 18 +- 23 files changed, 2973 insertions(+), 248 deletions(-) create mode 100644 hikari/core/voices.py create mode 100644 tests/hikari/core/test_entities.py create mode 100644 tests/hikari/core/test_events.py create mode 100644 tests/hikari/core/test_voices.py diff --git a/hikari/core/__init__.py b/hikari/core/__init__.py index a232d38cce..53aca2edc4 100644 --- a/hikari/core/__init__.py +++ b/hikari/core/__init__.py @@ -34,4 +34,5 @@ from hikari.core.permissions import * from hikari.core.snowflakes import * from hikari.core.users import * +from hikari.core.voices import * from hikari.core.webhooks import * diff --git a/hikari/core/channels.py b/hikari/core/channels.py index 2aece2cc39..d6495251c0 100644 --- a/hikari/core/channels.py +++ b/hikari/core/channels.py @@ -16,12 +16,26 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . -__all__ = ["Channel", "ChannelType", "DMChannel", "PartialChannel", "GroupDMChannel"] +"""Components and entities that are used to describe both DMs and guild +channels on Discord. +""" + +__all__ = [ + "Channel", + "ChannelType", + "DMChannel", + "PartialChannel", + "PermissionOverwrite", + "PermissionOverwriteType", + "GroupDMChannel", +] import enum +import typing from hikari.core import entities from hikari.core import snowflakes +from hikari.core import permissions from hikari.internal_utilities import marshaller @@ -29,12 +43,19 @@ class ChannelType(enum.IntEnum): """The known channel types that are exposed to us by the api.""" + #: A text channel in a guild. GUILD_TEXT = 0 + #: A direct channel between two users. DM = 1 + #: A voice channel in a guild. GUILD_VOICE = 2 + #: A direct channel between multiple users. GROUP_DM = 3 + #: An category used for organizing channels in a guild. GUILD_CATEGORY = 4 + #: A channel that can be followed and can crosspost. GUILD_NEWS = 5 + #: A channel that show's a game's store page. GUILD_STORE = 6 @@ -55,6 +76,41 @@ class PartialChannel(snowflakes.UniqueEntity, entities.Deserializable): type: ChannelType = marshaller.attrib(deserializer=ChannelType) +@enum.unique +class PermissionOverwriteType(str, enum.Enum): + """The type of entity a Permission Overwrite targets.""" + + #: A permission overwrite that targets all the members with a specific + #: guild role. + ROLE = "role" + #: A permission overwrite that targets a specific guild member. + MEMBER = "member" + + +@marshaller.attrs(slots=True) +class PermissionOverwrite(snowflakes.UniqueEntity, entities.Deserializable, entities.Serializable): + """Represents permission overwrites for a channel or role in a channel.""" + + #: The type of entity this overwrite targets. + #: + #: :type: :obj:`PermissionOverwriteType` + type: PermissionOverwriteType = marshaller.attrib(deserializer=PermissionOverwriteType) + + #: The permissions this overwrite allows. + #: + #: :type: :obj:`permissions.Permission` + allow: permissions.Permission = marshaller.attrib(deserializer=permissions.Permission) + + #: The permissions this overwrite denies. + #: + #: :type: :obj:`permissions.Permission` + deny: permissions.Permission = marshaller.attrib(deserializer=permissions.Permission) + + @property + def unset(self) -> permissions.Permission: + return typing.cast(permissions.Permission, (self.allow | self.deny)) + + @marshaller.attrs(slots=True) class Channel(PartialChannel): ... diff --git a/hikari/core/configs/gateway.py b/hikari/core/configs/gateway.py index 41d357f65d..d26ac5da8f 100644 --- a/hikari/core/configs/gateway.py +++ b/hikari/core/configs/gateway.py @@ -113,7 +113,7 @@ class GatewayConfig(entities.Deserializable): #: don't want to enable this. #: #: :type: :obj:`bool` - debug: bool = marshaller.attrib(deserializer=bool, if_undefined=lambda: False, default=False) + debug: bool = marshaller.attrib(deserializer=bool, if_undefined=False, default=False) #: The initial activity to set all shards to when starting the gateway. If #: ``None``, then no activity will be set. @@ -132,7 +132,7 @@ class GatewayConfig(entities.Deserializable): #: Whether to show up as AFK or not on sign-in. #: #: :type: :obj:`bool` - initial_afk: bool = marshaller.attrib(deserializer=bool, if_undefined=lambda: False, default=False) + initial_afk: bool = marshaller.attrib(deserializer=bool, if_undefined=False, default=False) #: The idle time to show on signing in, or ``None`` to not show an idle #: time. @@ -179,7 +179,7 @@ class GatewayConfig(entities.Deserializable): #: not. Usually you want this turned on. #: #: :type: :obj:`bool` - use_compression: bool = marshaller.attrib(deserializer=bool, if_undefined=lambda: True, default=True) + use_compression: bool = marshaller.attrib(deserializer=bool, if_undefined=True, default=True) #: The gateway API version to use. #: diff --git a/hikari/core/configs/protocol.py b/hikari/core/configs/protocol.py index 4249863ba2..0d410a6c59 100644 --- a/hikari/core/configs/protocol.py +++ b/hikari/core/configs/protocol.py @@ -49,7 +49,7 @@ class HTTPProtocolConfig(entities.Deserializable): #: Defaults to ``False`` if unspecified during deserialization. #: #: :type: :obj:`bool` - allow_redirects: bool = marshaller.attrib(deserializer=bool, if_undefined=lambda: False, default=False) + allow_redirects: bool = marshaller.attrib(deserializer=bool, if_undefined=False, default=False) #: Either an implementation of :obj:`aiohttp.BaseConnector`. #: @@ -129,4 +129,4 @@ class HTTPProtocolConfig(entities.Deserializable): #: Defaults to ``True`` if unspecified during deserialization. #: #: :type: :obj:`bool` - verify_ssl: bool = marshaller.attrib(deserializer=bool, if_undefined=lambda: True, default=True) + verify_ssl: bool = marshaller.attrib(deserializer=bool, if_undefined=True, default=True) diff --git a/hikari/core/embeds.py b/hikari/core/embeds.py index 104b29856e..16f1a5bf27 100644 --- a/hikari/core/embeds.py +++ b/hikari/core/embeds.py @@ -16,7 +16,9 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . - +""" +Components and entities that are used to describe message embeds on Discord. +""" __all__ = [ "Embed", "EmbedThumbnail", @@ -184,9 +186,7 @@ class EmbedField(entities.HikariEntity, entities.Deserializable, entities.Serial #: Whether the field should display inline. Defaults to ``False``. #: #: :type: :obj:`bool` - is_inline: bool = marshaller.attrib( - raw_name="inline", deserializer=bool, serializer=bool, if_undefined=lambda: False - ) + is_inline: bool = marshaller.attrib(raw_name="inline", deserializer=bool, serializer=bool, if_undefined=False) @marshaller.attrs(slots=True) diff --git a/hikari/core/emojis.py b/hikari/core/emojis.py index 23cced6a0b..c99836f792 100644 --- a/hikari/core/emojis.py +++ b/hikari/core/emojis.py @@ -16,7 +16,9 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . - +"""Components and entities that are used to describe both custom and Unicode +emojis on Discord. +""" import typing from hikari.core import entities @@ -54,7 +56,7 @@ class UnknownEmoji(Emoji, snowflakes.UniqueEntity): #: Wheter the emoji is animated. #: #: :type: :obj:`bool` - is_animated: bool = marshaller.attrib(raw_name="animated", deserializer=bool, if_undefined=lambda: False) + is_animated: bool = marshaller.attrib(raw_name="animated", deserializer=bool, if_undefined=False) @marshaller.attrs(slots=True) diff --git a/hikari/core/entities.py b/hikari/core/entities.py index 78f26dfa52..6a47ea2f9b 100644 --- a/hikari/core/entities.py +++ b/hikari/core/entities.py @@ -17,12 +17,14 @@ # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . """Datastructure bases.""" -__all__ = ["HikariEntity", "Serializable", "Deserializable", "RawEntityT"] +__all__ = ["HikariEntity", "Serializable", "Deserializable", "RawEntityT", "UNSET"] import abc import typing from hikari.internal_utilities import marshaller +from hikari.internal_utilities import singleton_meta + RawEntityT = typing.Union[ None, bool, int, float, str, bytes, typing.Sequence[typing.Any], typing.Mapping[str, typing.Any] @@ -32,6 +34,21 @@ T_co = typing.TypeVar("T_co", covariant=True) +class Unset(metaclass=singleton_meta.SingletonMeta): + def __bool__(self): + return False + + def __repr__(self): + return type(self).__name__.upper() + + __str__ = __repr__ + + +#: A variable used for certain update events where a field being unset will +#: mean that it's not being acted on, mostly just seen attached to event models. +UNSET = Unset() + + @marshaller.attrs(slots=True) class HikariEntity(metaclass=abc.ABCMeta): """The base for any entity used in this API.""" diff --git a/hikari/core/events.py b/hikari/core/events.py index 46ebd2fbaf..7492e849af 100644 --- a/hikari/core/events.py +++ b/hikari/core/events.py @@ -16,6 +16,8 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . +"""Components and entities that are used to describe Discord gateway events. +""" __all__ = [ "HikariEvent", @@ -30,11 +32,11 @@ "ChannelCreateEvent", "ChannelUpdateEvent", "ChannelDeleteEvent", - "ChannelPinAddEvent", - "ChannelPinRemoveEvent", + "ChannelPinUpdateEvent", "GuildCreateEvent", "GuildUpdateEvent", - "GuildDeleteEvent", + "GuildLeaveEvent", + "GuildUnavailableEvent", "GuildBanAddEvent", "GuildBanRemoveEvent", "GuildEmojisUpdateEvent", @@ -63,20 +65,32 @@ "WebhookUpdate", ] +import datetime import typing import attr +from hikari.core import channels from hikari.core import entities -from hikari.core import guilds as guilds_ +from hikari.core import embeds as _embeds +from hikari.core import emojis as _emojis +from hikari.core import guilds +from hikari.core import invites +from hikari.core import messages +from hikari.core import oauth2 +from hikari.core import snowflakes from hikari.core import users +from hikari.core import voices +from hikari.internal_utilities import dates +from hikari.internal_utilities import marshaller T_contra = typing.TypeVar("T_contra", contravariant=True) +# Base event, is not deserialized @attr.s(slots=True, auto_attribs=True) class HikariEvent(entities.HikariEntity): - ... + """The base class that all events inherit from.""" # Synthetic event, is not deserialized @@ -115,186 +129,975 @@ class StoppedEvent(HikariEvent): ... -@attr.s(slots=True, auto_attribs=True) -class ReadyEvent(HikariEvent): - v: int - user: users.User - guilds: guilds_.Guild - session_id: str - shard_id: int - shard_count: int - - -@attr.s(slots=True, auto_attribs=True) +@marshaller.attrs(slots=True) +class ReadyEvent(HikariEvent, entities.Deserializable): + """Used to represent the gateway ready event, received when identifying + with the gateway and on reconnect. + """ + + #: The gateway version this is currently connected to. + #: + #: :type: :obj:`int` + gateway_version: int = marshaller.attrib(raw_name="v", deserializer=int) + + #: The object of the current bot account this connection is for. + #: + #: :type: :obj:`users.MyUser` + my_user: users.User = marshaller.attrib(raw_name="user", deserializer=users.MyUser.deserialize) + + #: A mapping of the guilds this bot is currently in. All guilds will start + #: off "unavailable". + #: + #: :type: :obj:`typing.Mapping` [ :obj:`snowflake.Snowflake`, :obj:`guilds.UnavailableGuild` ] + unavailable_guilds: typing.Mapping[snowflakes.Snowflake, guilds.UnavailableGuild] = marshaller.attrib( + raw_name="guilds", + deserializer=lambda guilds_objs: {g.id: g for g in map(guilds.UnavailableGuild.deserialize, guilds_objs)}, + ) + + #: The id of the current gateway session, used for reconnecting. + #: + #: :type: :obj:`str` + session_id: str = marshaller.attrib(deserializer=str) + + #: Information about the current shard, only provided when identifying. + #: + #: :type: :obj:`typing.Tuple` [ :obj:`int`, :obj:`int` ], optional + _shard_information: typing.Optional[typing.Tuple[int, int]] = marshaller.attrib( + raw_name="shard", deserializer=tuple, if_undefined=None + ) + + @property + def shard_id(self) -> typing.Optional[int]: + """The zero-indexed id of the current shard, only available if this + ready event was received while identifying. + .""" + return self._shard_information and self._shard_information[0] or None + + @property + def shard_count(self) -> typing.Optional[int]: + """The total shard count for this bot, only available if this + ready event was received while identifying. + """ + return self._shard_information and self._shard_information[1] or None + + +@marshaller.attrs(slots=True) class ResumedEvent(HikariEvent): - ... - + """Represents a gateway Resume event.""" + + +@marshaller.attrs(slots=True) +class BaseChannelEvent(HikariEvent, snowflakes.UniqueEntity, entities.Deserializable): + """A base object that Channel events will inherit from.""" + + #: The channel's type. + #: + #: :type: :obj:`channels.ChannelType` + type: channels.ChannelType = marshaller.attrib(deserializer=channels.ChannelType) + + #: The ID of the guild this channel is in, will be ``None`` for DMs. + #: + #: :type: :obj:`snowflakes.Snowflake`, optional + guild_id: typing.Optional[snowflakes.Snowflake] = marshaller.attrib(deserializer=snowflakes.Snowflake, if_none=None) + + #: The sorting position of this channel, will be relative to the + #: :attr:`parent_id` if set. + #: + #: :type: :obj:`int`, optional + position: typing.Optional[int] = marshaller.attrib(deserializer=int, if_none=None) + + #: An mapping of the set permission overwrites for this channel, if applicable. + #: + #: :type: :obj:`typing.Mapping` [ :obj:`snowflakes.Snowflake`, :obj:`channels.PermissionOverwrite` ], optional + permission_overwrites: typing.Optional[ + typing.Mapping[snowflakes.Snowflake, channels.PermissionOverwrite] + ] = marshaller.attrib( + deserializer=lambda overwrites: {o.id: o for o in map(channels.PermissionOverwrite.deserialize, overwrites)}, + if_none=None, + ) + + #: The name of this channel, if applicable. + #: + #: :type: :obj:`str`, optional + name: typing.Optional[str] = marshaller.attrib(deserializer=str, if_undefined=None) + + #: The topic of this channel, if applicable and set. + #: + #: :type: :obj:`str`, optional + topic: typing.Optional[str] = marshaller.attrib(deserializer=str, if_undefined=None, if_none=None) + + #: Whether this channel is nsfw, will be ``None`` if not applicable. + #: + #: :type: :obj:`bool`, optional + is_nsfw: typing.Optional[bool] = marshaller.attrib(raw_name="nsfw", deserializer=bool, if_undefined=None) + + #: The ID of the last message sent, if it's a text type channel. + #: + #: :type: :obj:`snowflakes.Snowflake`, optional + last_message_id: typing.Optional[snowflakes.Snowflake] = marshaller.attrib( + deserializer=snowflakes.Snowflake.deserialize, if_none=None, if_undefined=None + ) + + #: The bitrate (in bits) of this channel, if it's a guild voice channel. + #: + #: :type: :obj:`bool`, optional + bitrate: typing.Optional[int] = marshaller.attrib(deserializer=int, if_undefined=None) + + #: The user limit for this channel if it's a guild voice channel. + #: + #: :type: :obj:`bool`, optional + user_limit: typing.Optional[int] = marshaller.attrib(deserializer=int, if_undefined=None) + + #: The rate limit a user has to wait before sending another message in this + #: channel, if it's a guild text like channel. + #: + #: :type: :obj:`datetime.timedelta`, optional + rate_limit_per_user: typing.Optional[datetime.timedelta] = marshaller.attrib( + deserializer=lambda delta: datetime.timedelta(seconds=delta), if_undefined=None, + ) + + #: A mapping of this channel's recipient users, if it's a DM or group DM. + #: + #: :type: :obj:`Typing.MutableMapping` [ :obj:`snowflakes.Snowflake`, :obj:`users.User` ], optional + recipients: typing.Optional[typing.MutableMapping[snowflakes.Snowflake, users.User]] = marshaller.attrib( + deserializer=lambda recipients: {user.id: user for user in map(users.User.deserialize, recipients)} + ) + + #: The hash of this channel's icon, if it's a group DM channel and is set. + #: + #: :type: :obj:`str`, optional + icon_hash: typing.Optional[str] = marshaller.attrib( + raw_name="icon", deserializer=str, if_undefined=None, if_none=None + ) + + #: The ID of this channel's creator, if it's a DM channel. + #: + #: :type: :obj:`snowflakes.Snowflake`, optional + owner_id: typing.Optional[snowflakes.Snowflake] = marshaller.attrib( + deserializer=snowflakes.Snowflake.deserialize, if_undefined=None + ) + + #: The ID of the application id of the group DM creator, if it's a + #: bot based group DM. + #: + #: :type: :obj:`snowflakes.Snowflake`, optional + application_id: typing.Optional[snowflakes.Snowflake] = marshaller.attrib( + deserializer=snowflakes.Snowflake.deserialize, if_undefined=None + ) + + #: The ID of this channels's parent category within guild, if set. + #: + #: :type: :obj:`snowflakes.Snowflake`, optional + parent_id: typing.Optional[snowflakes.Snowflake] = marshaller.attrib( + deserializer=snowflakes.Snowflake.deserialize, if_undefined=None, if_none=None + ) + + #: The datetime of when the last message was pinned in this channel, + #: if set and applicable. + #: + #: :type: :obj:`datetime.datetime`, optional + last_pin_timestamp: typing.Optional[datetime.datetime] = marshaller.attrib( + deserializer=dates.parse_iso_8601_ts, if_undefined=None + ) + + +@marshaller.attrs(slots=True) +class ChannelCreateEvent(BaseChannelEvent): + """Represents Channel Create gateway events. + + Will be sent when a guild channel is created and before all Create Message + events that originate from a DM channel. + """ + + +@marshaller.attrs(slots=True) +class ChannelUpdateEvent(BaseChannelEvent): + """Represents Channel Update gateway events.""" + + +@marshaller.attrs(slots=True) +class ChannelDeleteEvent(BaseChannelEvent): + """Represents Channel Delete gateway events.""" + + +@marshaller.attrs(slots=True) +class ChannelPinUpdateEvent(HikariEvent, entities.Deserializable): + """Used to represent the Channel Pins Update gateway event. + Sent when a message is pinned or unpinned in a channel but not + when a pinned message is deleted. + """ + + #: The ID of the guild where this event happened. + #: Will be ``None`` if this happened in a DM channel. + #: + #: :type: :obj:`snowflake.Snowflake`, optional + guild_id: typing.Optional[snowflakes.Snowflake] = marshaller.attrib( + deserializer=snowflakes.Snowflake.deserialize, if_undefined=None + ) + + #: The ID of the channel where the message was pinned or unpinned. + #: + #: :type: :obj:`snowflake.Snowflake` + channel_id: snowflakes.Snowflake = marshaller.attrib(deserializer=snowflakes.Snowflake.deserialize) + + #: The datetime of when the most recent message was pinned in this channel. + #: Will be ``None`` if there are no messages pinned after this change. + #: + #: :type: :obj:`datetime.datetime`, optional + last_pin_timestamp: typing.Optional[datetime.datetime] = marshaller.attrib( + deserializer=dates.parse_iso_8601_ts, if_undefined=None + ) -@attr.s(slots=True, auto_attribs=True) -class ChannelCreateEvent(HikariEvent): - ... +@marshaller.attrs(slots=True) +class GuildCreateEvent(HikariEvent, guilds.Guild): + """Used to represent Guild Create gateway events. + + Will be received when the bot joins a guild, and when a guild becomes + available to a guild (either due to outage or at startup). + """ -@attr.s(slots=True, auto_attribs=True) -class ChannelUpdateEvent(HikariEvent): - ... +@marshaller.attrs(slots=True) +class GuildUpdateEvent(HikariEvent, guilds.Guild): + """Used to represent Guild Update gateway events.""" -@attr.s(slots=True, auto_attribs=True) -class ChannelDeleteEvent(HikariEvent): - ... +@marshaller.attrs(slots=True) +class GuildLeaveEvent(HikariEvent, snowflakes.UniqueEntity, entities.Deserializable): + """Fired when the current user leaves the guild or is kicked/banned from it. -@attr.s(slots=True, auto_attribs=True) -class ChannelPinAddEvent(HikariEvent): - ... - - -@attr.s(slots=True, auto_attribs=True) -class ChannelPinRemoveEvent(HikariEvent): - ... - - -@attr.s(slots=True, auto_attribs=True) -class GuildCreateEvent(HikariEvent): - ... - - -@attr.s(slots=True, auto_attribs=True) -class GuildUpdateEvent(HikariEvent): - ... + Notes + ----- + This is fired based on Discord's Guild Delete gateway event. + """ -@attr.s(slots=True, auto_attribs=True) -class GuildDeleteEvent(HikariEvent): - ... +@marshaller.attrs(slots=True) +class GuildUnavailableEvent(HikariEvent, snowflakes.UniqueEntity, entities.Deserializable): + """Fired when a guild becomes temporarily unavailable due to an outage. + Notes + ----- + This is fired based on Discord's Guild Delete gateway event. + """ -@attr.s(slots=True, auto_attribs=True) -class GuildBanAddEvent(HikariEvent): - ... +@marshaller.attrs(slots=True) +class BaseGuildBanEvent(HikariEvent, entities.Deserializable): + """A base object that guild ban events will inherit from.""" -@attr.s(slots=True, auto_attribs=True) -class GuildBanRemoveEvent(HikariEvent): - ... + #: The ID of the guild this ban is in. + #: + #: :type: :obj:`snowflake.Snowflake` + guild_id: snowflakes.Snowflake = marshaller.attrib(deserializer=snowflakes.Snowflake.deserialize) + #: The object of the user this ban targets. + #: + #: :type: :obj:`users.User` + user: users.User = marshaller.attrib(deserializer=users.User.deserialize) -@attr.s(slots=True, auto_attribs=True) -class GuildEmojisUpdateEvent(HikariEvent): - ... +@marshaller.attrs(slots=True) +class GuildBanAddEvent(BaseGuildBanEvent): + """Used to represent a Guild Ban Add gateway event.""" -@attr.s(slots=True, auto_attribs=True) -class GuildIntegrationsUpdateEvent(HikariEvent): - ... +@marshaller.attrs(slots=True) +class GuildBanRemoveEvent(BaseGuildBanEvent): + """Used to represent a Guild Ban Remove gateway event.""" -@attr.s(slots=True, auto_attribs=True) -class GuildMemberAddEvent(HikariEvent): - ... +@marshaller.attrs(slots=True) +class GuildEmojisUpdateEvent(HikariEvent, entities.Deserializable): + """Represents a Guild Emoji Update gateway event.""" -@attr.s(slots=True, auto_attribs=True) -class GuildMemberRemoveEvent(HikariEvent): - ... + #: The ID of the guild this emoji was updated in. + #: + #: :type: :obj:`snowflakes.Snowflake` + guild_id: snowflakes.Snowflake = marshaller.attrib(deserializer=snowflakes.Snowflake.deserialize) + #: The updated mapping of emojis by their ID. + #: + #: :type: :obj:`typing.Mapping` [ :obj:`snowflakes.Snowflake`, :obj:`_emojis.GuildEmoji` ] + emojis: typing.Mapping[snowflakes.Snowflake, _emojis.GuildEmoji] = marshaller.attrib( + deserializer=lambda ems: {emoji.id: emoji for emoji in map(_emojis.GuildEmoji.deserialize, ems)} + ) -@attr.s(slots=True, auto_attribs=True) -class GuildMemberUpdateEvent(HikariEvent): - ... +@marshaller.attrs(slots=True) +class GuildIntegrationsUpdateEvent(HikariEvent, entities.Deserializable): + """Used to represent Guild Integration Update gateway events.""" -@attr.s(slots=True, auto_attribs=True) -class GuildRoleCreateEvent(HikariEvent): - ... + #: The ID of the guild the integration was updated in. + #: + #: :type: :obj:`snowflake.Snowflake` + guild_id: snowflakes.Snowflake = marshaller.attrib(deserializer=snowflakes.Snowflake.deserialize) -@attr.s(slots=True, auto_attribs=True) -class GuildRoleUpdateEvent(HikariEvent): - ... +@marshaller.attrs(slots=True) +class GuildMemberAddEvent(HikariEvent, guilds.GuildMember): + """Used to represent a Guild Member Add gateway event.""" + #: The ID of the guild where this member was added. + #: + #: :type: :obj:`snowflakes.Snowflake` + guild_id: snowflakes.Snowflake = marshaller.attrib(deserializer=snowflakes.Snowflake.deserialize) -@attr.s(slots=True, auto_attribs=True) -class GuildRoleDeleteEvent(HikariEvent): - ... +@marshaller.attrs(slots=True) +class GuildMemberRemoveEvent(HikariEvent, entities.Deserializable): + """Used to represent Guild Member Remove gateway events. + Sent when a member is kicked, banned or leaves a guild. + """ -@attr.s(slots=True, auto_attribs=True) -class InviteCreateEvent(HikariEvent): - ... + #: The ID of the guild this user was removed from. + #: + #: :type: :obj:`snowflakes.Snowflake` + guild_id: snowflakes.Snowflake = marshaller.attrib(deserializer=snowflakes.Snowflake.deserialize) + #: The object of the user who was removed from this guild. + #: + #: :type: :obj:`users.User` + user: users.User = marshaller.attrib(deserializer=users.User.deserialize) -@attr.s(slots=True, auto_attribs=True) -class InviteDeleteEvent(HikariEvent): - ... +@marshaller.attrs(slots=True) +class GuildMemberUpdateEvent(HikariEvent, entities.Deserializable): + """Used to represent a Guild Member Update gateway event. + Sent when a guild member or their inner user object is updated. + """ -@attr.s(slots=True, auto_attribs=True) -class MessageCreateEvent(HikariEvent): - ... - - -@attr.s(slots=True, auto_attribs=True) -class MessageUpdateEvent(HikariEvent): - ... - - -@attr.s(slots=True, auto_attribs=True) -class MessageDeleteEvent(HikariEvent): - ... - - -@attr.s(slots=True, auto_attribs=True) -class MessageDeleteBulkEvent(HikariEvent): - ... - - -@attr.s(slots=True, auto_attribs=True) -class MessageReactionAddEvent(HikariEvent): - ... - - -@attr.s(slots=True, auto_attribs=True) -class MessageReactionRemoveEvent(HikariEvent): - ... - - -@attr.s(slots=True, auto_attribs=True) -class MessageReactionRemoveAllEvent(HikariEvent): - ... - - -@attr.s(slots=True, auto_attribs=True) -class MessageReactionRemoveEmojiEvent(HikariEvent): - ... - - -@attr.s(slots=True, auto_attribs=True) -class PresenceUpdateEvent(HikariEvent): - ... - - -@attr.s(slots=True, auto_attribs=True) -class TypingStartEvent(HikariEvent): - ... - - -@attr.s(slots=True, auto_attribs=True) -class UserUpdateEvent(HikariEvent): - ... - - -@attr.s(slots=True, auto_attribs=True) -class VoiceStateUpdateEvent(HikariEvent): - ... - - -@attr.s(slots=True, auto_attribs=True) -class VoiceServerUpdateEvent(HikariEvent): - ... - - -@attr.s(slots=True, auto_attribs=True) -class WebhookUpdate(HikariEvent): - ... + #: The ID of the guild this member was updated in. + #: + #: :type: :obj:`snowflakes.Snowflake` + guild_id: snowflakes.Snowflake = marshaller.attrib(deserializer=snowflakes.Snowflake.deserialize) + + #: A sequence of the IDs of the member's current roles. + #: + #: :type: :obj:`typing.Sequence` [ :obj:`snowflakes.Snowflake` ] + role_ids: typing.Sequence[snowflakes.Snowflake] = marshaller.attrib( + raw_name="roles", deserializer=lambda role_ids: [snowflakes.Snowflake.deserialize(rid) for rid in role_ids], + ) + + #: The object of the user who was updated. + #: + #: :type: :obj:`users.User` + user: users.User = marshaller.attrib(deserializer=users.User.deserialize) + + #: This member's nickname. When set to :obj:`None`, this has been removed + #: and when set to :obj:`entities.UNSET` this hasn't been acted on. + #: + #: :type: :obj:`typing.Union` [ :obj:`str`, :obj:`entities.UNSET` ], optional + nickname: typing.Union[None, str, entities.Unset] = marshaller.attrib( + raw_name="nick", deserializer=str, if_none=None, if_undefined=entities.Unset, + ) + + #: The datetime of when this member started "boosting" this guild. + #: Will be ``None`` if they aren't boosting. + #: + #: :type: :obj:`typing.Union` [ :obj:`datetime.datetime`, :obj:`entities.UNSET` ], optional + premium_since: typing.Union[None, datetime.datetime, entities.Unset] = marshaller.attrib( + deserializer=dates.parse_iso_8601_ts, if_none=None, if_undefined=entities.Unset + ) + + +@marshaller.attrs(slots=True) +class GuildRoleCreateEvent(HikariEvent, entities.Deserializable): + """Used to represent a Guild Role Create gateway event.""" + + #: The ID of the guild where this role was created. + #: + #: :type: :obj:`snowflakes.Snowflake` + guild_id: snowflakes.Snowflake = marshaller.attrib(deserializer=snowflakes.Snowflake.deserialize) + + #: The object of the role that was created. + #: + #: :type: :obj:`guilds.GuildRole` + role: guilds.GuildRole = marshaller.attrib(deserializer=guilds.GuildRole.deserialize) + + +@marshaller.attrs(slots=True) +class GuildRoleUpdateEvent(HikariEvent, entities.Deserializable): + """Used to represent a Guild Role Create gateway event.""" + + #: The ID of the guild where this role was updated. + #: + #: :type: :obj:`snowflakes.Snowflake` + guild_id: snowflakes.Snowflake = marshaller.attrib(deserializer=snowflakes.Snowflake.deserialize) + + #: The updated role object. + #: + #: :type: :obj:`guilds.GuildRole` + role: guilds.GuildRole = marshaller.attrib(deserializer=guilds.GuildRole.deserialize) + + +@marshaller.attrs(slots=True) +class GuildRoleDeleteEvent(HikariEvent, entities.Deserializable): + """Represents a gateway Guild Role Delete Event.""" + + #: The ID of the guild where this role is being deleted. + #: + #: :type: :obj:`snowflakes.Snowflake` + guild_id: snowflakes.Snowflake = marshaller.attrib(deserializer=snowflakes.Snowflake.deserialize) + + #: The ID of the role being deleted. + #: + #: :type: :obj:`snowflakes.Snowflake` + role_id: snowflakes.Snowflake = marshaller.attrib(deserializer=snowflakes.Snowflake.deserialize) + + +@marshaller.attrs(slots=True) +class InviteCreateEvent(HikariEvent, entities.Deserializable): + """""" + + #: The ID of the channel this invite targets. + #: + #: :type: :obj:`snowflakes.Snowflake` + channel_id: snowflakes.Snowflake = marshaller.attrib(deserializer=snowflakes.Snowflake.deserialize) + + #: The code that identifies this invite + #: + #: :type: :obj:`str` + code: str = marshaller.attrib(deserializer=str) + + #: The datetime of when this invite was created. + #: + #: :type: :obj:`datetime.datetime` + created_at: datetime.datetime = marshaller.attrib(deserializer=dates.parse_iso_8601_ts) + + #: The ID of the guild this invite was created in, if applicable. + #: Will be ``None`` for group DM invites. + #: + #: :type: :obj:`snowflakes.Snowflake`, optional + guild_id: typing.Optional[snowflakes.Snowflake] = marshaller.attrib( + deserializer=snowflakes.Snowflake.deserialize, if_undefined=None + ) + + #: The object of the user who created this invite, if applicable. + #: + #: :type: :obj:`users.User`, optional + inviter: typing.Optional[users.User] = marshaller.attrib(deserializer=users.User.deserialize, if_undefined=None) + + #: The timedelta of how long this invite will be valid for. + #: If set to :obj:`None` then this is unlimited. + #: + #: :type: :obj:`datetime.timedelta`, optional + max_age: typing.Optional[datetime.timedelta] = marshaller.attrib( + deserializer=lambda age: datetime.timedelta(seconds=age) if age > 0 else None, + ) + + #: The limit for how many times this invite can be used before it expires. + #: If set to ``0`` then this is unlimited. + #: + #: :type: :obj:`typing.Union` [ :obj:`int`, :obj:`float(inf)` ] + max_uses: typing.Union[int, float] = marshaller.attrib(deserializer=lambda count: count or float("inf")) + + #: The object of the user who this invite targets, if set. + #: + #: :type: :obj:`users.User`, optional + target_user: typing.Optional[users.User] = marshaller.attrib(deserializer=users.User.deserialize) + + #: The type of user target this invite is, if applicable. + #: + #: :type: :obj:`invites.TargetUserType`, optional + target_user_type: typing.Optional[invites.TargetUserType] = marshaller.attrib(deserializer=invites.TargetUserType) + + #: Whether this invite grants temporary membership. + #: + #: :type: :obj:`bool` + is_temporary: bool = marshaller.attrib(raw_name="temporary", deserializer=bool) + + #: The amount of times this invite has been used. + #: + #: :type: :obj:`int` + uses: int = marshaller.attrib(deserializer=int) + + +@marshaller.attrs(slots=True) +class InviteDeleteEvent(HikariEvent, entities.Deserializable): + """Used to represent Invite Delete gateway events. + Sent when an invite is deleted for a channel we can access. + """ + + #: The ID of the channel this ID was attached to + #: + #: :type: :obj:`snowflakes.Snowflake` + channel_id: snowflakes.Snowflake = marshaller.attrib(deserializer=snowflakes.Snowflake.deserialize) + + #: The code of this invite. + #: + #: :type: :obj:`snowflakes.Snowflake` + code: str = marshaller.attrib(deserializer=str) + + #: The ID of the guild this invite was deleted in. + #: This will be ``None`` if this invite belonged to a DM channel. + #: + #: :type: :obj:`snowflakes.Snowflake`, optional + guild_id: typing.Optional[snowflakes.Snowflake] = marshaller.attrib( + deserializer=snowflakes.Snowflake.deserialize, if_undefined=None + ) + + +@marshaller.attrs(slots=True) +class MessageCreateEvent(HikariEvent, messages.Message): + """Used to represent Message Create gateway events.""" + + +# This is an arbitrarily partial version of `messages.Message` +@marshaller.attrs(slots=True) +class MessageUpdateEvent(HikariEvent, snowflakes.UniqueEntity, entities.Deserializable): + """ + Represents Message Update gateway events. + + Note + ---- + + All fields on this model except :attr:`channel_id` and :attr:`id` may be + set to :obj:`entities.UNSET` (a singleton defined in + ``hikari.core.entities``) if we've not received information about their + state from Discord alongside field nullability. + """ + + #: The ID of the channel that the message was sent in. + #: + #: :type: :obj:`snowflakes.Snowflake` + channel_id: snowflakes.Snowflake = marshaller.attrib(deserializer=snowflakes.Snowflake) + + #: The ID of the guild that the message was sent in. + #: + #: :type: :obj:`typing.Union` [ :obj:`snowflakes.Snowflake`, :obj:`entities.UNSET` ] + guild_id: typing.Union[snowflakes.Snowflake, entities.Unset] = marshaller.attrib( + deserializer=snowflakes.Snowflake, if_undefined=entities.Unset + ) + + #: The author of this message. + #: + #: :type: :obj:`typing.Union` [ :obj:`users.User`, :obj:`entities.UNSET` ] + author: typing.Union[users.User, entities.Unset] = marshaller.attrib( + deserializer=users.User.deserialize, if_undefined=entities.Unset + ) + + #: The member properties for the message's author. + #: + #: :type: :obj:`typing.Union` [ :obj:`guilds.GuildMember`, :obj:`entities.UNSET` ] + member: typing.Union[guilds.GuildMember, entities.Unset] = marshaller.attrib( + deserializer=guilds.GuildMember.deserialize, if_undefined=entities.Unset + ) + + #: The content of the message. + #: + #: :type: :obj:`typing.Union` [ :obj:`str`, :obj:`entities.UNSET` ] + content: typing.Union[str, entities.Unset] = marshaller.attrib(deserializer=str, if_undefined=entities.Unset) + + #: The timestamp that the message was sent at. + #: + #: :type: :obj:`typing.Union` [ :obj:`datetime.datetime`, :obj:`entities.UNSET` ] + timestamp: typing.Union[datetime.datetime, entities.Unset] = marshaller.attrib( + deserializer=dates.parse_iso_8601_ts, if_undefined=entities.Unset + ) + + #: The timestamp that the message was last edited at, or ``None`` if not ever edited. + #: + #: :type: :obj:`typing.Union` [ :obj:`datetime.datetime`, :obj:`entities.UNSET` ], optional + edited_timestamp: typing.Union[datetime.datetime, entities.Unset, None] = marshaller.attrib( + deserializer=dates.parse_iso_8601_ts, if_none=None, if_undefined=entities.Unset + ) + + #: Whether the message is a TTS message. + #: + #: :type: :obj:`typing.Union` [ :obj:`bool`, :obj:`entities.UNSET` ] + is_tts: typing.Union[bool, entities.Unset] = marshaller.attrib( + raw_name="tts", deserializer=bool, if_undefined=entities.Unset + ) + + #: Whether the message mentions ``@everyone`` or ``@here``. + #: + #: :type: :obj:`typing.Union` [ :obj:`bool`, :obj:`entities.UNSET` ] + is_mentioning_everyone: typing.Union[bool, entities.Unset] = marshaller.attrib( + raw_name="mention_everyone", deserializer=bool, if_undefined=entities.Unset + ) + + #: The users the message mentions. + #: + #: :type: :obj:`typing.Union` [ :obj:`typing.Set` [ :obj:`snowflakes.Snowflake` ], :obj:`entities.UNSET` ] + user_mentions: typing.Union[typing.Set[snowflakes.Snowflake], entities.Unset] = marshaller.attrib( + raw_name="mentions", + deserializer=lambda user_mentions: {snowflakes.Snowflake(u["id"]) for u in user_mentions}, + if_undefined=entities.Unset, + ) + + #: The roles the message mentions. + #: + #: :type: :obj:`typing.Union` [ :obj:`typing.Set` [ :obj:`snowflakes.Snowflake` ], :obj:`entities.UNSET` ] + role_mentions: typing.Union[typing.Set[snowflakes.Snowflake], entities.Unset] = marshaller.attrib( + raw_name="mention_roles", + deserializer=lambda role_mentions: {snowflakes.Snowflake(r) for r in role_mentions}, + if_undefined=entities.Unset, + ) + + #: The channels the message mentions. + #: + #: :type: :obj:`typing.Union` [ :obj:`typing.Set` [ :obj:`snowflakes.Snowflake` ], :obj:`entities.UNSET` ] + channel_mentions: typing.Union[typing.Set[snowflakes.Snowflake], entities.Unset] = marshaller.attrib( + raw_name="mention_channels", + deserializer=lambda channel_mentions: {snowflakes.Snowflake(c["id"]) for c in channel_mentions}, + if_undefined=entities.Unset, + ) + + #: The message attachments. + #: + #: :type: :obj:`typing.Union` [ :obj:`typing.Sequence` [ :obj:`messages.Attachment` ], :obj:`entities.UNSET` ] + attachments: typing.Union[typing.Sequence[messages.Attachment], entities.Unset] = marshaller.attrib( + deserializer=lambda attachments: [messages.Attachment.deserialize(a) for a in attachments], + if_undefined=entities.Unset, + ) + + #: The message's embeds. + #: + #: :type: :obj:`typing.Union` [ :obj:`typing.Sequence` [ :obj:`_embeds.Embed` ], :obj:`entities.UNSET` ] + embeds: typing.Union[typing.Sequence[_embeds.Embed], entities.Unset] = marshaller.attrib( + deserializer=lambda embed_objs: [_embeds.Embed.deserialize(e) for e in embed_objs], if_undefined=entities.Unset, + ) + + #: The message's reactions. + #: + #: :type: :obj:`typing.Union` [ :obj:`typing.Sequence` [ :obj:`messages.Reaction` ], :obj:`entities.UNSET` ] + reactions: typing.Union[typing.Sequence[messages.Reaction], entities.Unset] = marshaller.attrib( + deserializer=lambda reactions: [messages.Reaction.deserialize(r) for r in reactions], + if_undefined=entities.Unset, + ) + + #: Whether the message is pinned. + #: + #: :type: :obj:`typing.Union` [ :obj:`bool`, :obj:`entities.UNSET` ] + is_pinned: typing.Union[bool, entities.Unset] = marshaller.attrib( + raw_name="pinned", deserializer=bool, if_undefined=entities.Unset + ) + + #: If the message was generated by a webhook, the webhook's id. + #: + #: :type: :obj:`typing.Union` [ :obj:`snowflakes.Snowflake`, :obj:`entities.UNSET` ] + webhook_id: typing.Union[snowflakes.Snowflake, entities.Unset] = marshaller.attrib( + deserializer=snowflakes.Snowflake, if_undefined=entities.Unset + ) + + #: The message's type. + #: + #: :type: :obj:`typing.Union` [ :obj:`messages.MessageType`, :obj:`entities.UNSET` ] + type: typing.Union[messages.MessageType, entities.Unset] = marshaller.attrib( + deserializer=messages.MessageType, if_undefined=entities.Unset + ) + + #: The message's activity. + #: + #: :type: :obj:`typing.Union` [ :obj:`messages.MessageActivity`, :obj:`entities.UNSET` ] + activity: typing.Union[messages.MessageActivity, entities.Unset] = marshaller.attrib( + deserializer=messages.MessageActivity.deserialize, if_undefined=entities.Unset + ) + + #: The message's application. + #: + #: :type: :obj:`typing.Union` [ :obj:`oauth2.Application`, :obj:`entities.UNSET` ] + application: typing.Optional[oauth2.Application] = marshaller.attrib( + deserializer=oauth2.Application.deserialize, if_undefined=entities.Unset + ) + + #: The message's crossposted reference data. + #: + #: :type: :obj:`typing.Union` [ :obj:`MessageCrosspost`, :obj:`entities.UNSET` ] + message_reference: typing.Union[messages.MessageCrosspost, entities.Unset] = marshaller.attrib( + deserializer=messages.MessageCrosspost.deserialize, if_undefined=entities.Unset + ) + + #: The message's flags. + #: + #: :type: :obj:`typing.Union` [ :obj:`messages.MessageFlag`, :obj:`entities.UNSET` ] + flags: typing.Union[messages.MessageFlag, entities.Unset] = marshaller.attrib( + deserializer=messages.MessageFlag, if_undefined=entities.Unset + ) + + +@marshaller.attrs(slots=True) +class MessageDeleteEvent(HikariEvent, entities.Deserializable): + """Used to represent Message Delete gateway events. + Sent when a message is deleted in a channel we have access to. + """ + + #: The ID of the channel where this message was deleted. + #: + #: :type: :obj:`snowflakes.Snowflake` + channel_id: snowflakes.Snowflake = marshaller.attrib(deserializer=snowflakes.Snowflake.deserialize) + + #: The ID of the guild where this message was deleted. + #: Will be ``None`` if this message was deleted in a DM channel. + #: + #: :type: :obj:`snowflakes.Snowflake`, optional + guild_id: typing.Optional[snowflakes.Snowflake] = marshaller.attrib( + deserializer=snowflakes.Snowflake.deserialize, if_undefined=None + ) + #: The ID of the message that was deleted. + #: + #: :type: :obj:`snowflakes.Snowflake` + message_id: snowflakes.Snowflake = marshaller.attrib(raw_name="id", deserializer=snowflakes.Snowflake.deserialize) + + +@marshaller.attrs(slots=True) +class MessageDeleteBulkEvent(HikariEvent, entities.Deserializable): + """Used to represent Message Bulk Delete gateway events. + Sent when multiple messages are deleted in a channel at once. + """ + + #: The ID of the channel these messages have been deleted in. + #: + #: :type: :obj:`snowflakes.Snowflake` + channel_id: snowflakes.Snowflake = marshaller.attrib(deserializer=snowflakes.Snowflake.deserialize) + + #: The ID of the channel these messages have been deleted in. + #: Will be ``None`` if these messages were bulk deleted in a DM channel. + #: + #: :type: :obj:`snowflakes.Snowflake`, optional + guild_id: typing.Optional[snowflakes.Snowflake] = marshaller.attrib( + deserializer=snowflakes.Snowflake.deserialize, if_none=None + ) + + #: A collection of the IDs of the messages that were deleted. + #: + #: :type: :obj:`typing.Set` [ :obj:`snowflakes.Snowflake` ] + message_ids: typing.Set[snowflakes.Snowflake] = marshaller.attrib( + raw_name="ids", deserializer=lambda msgs: {snowflakes.Snowflake.deserialize(m) for m in msgs} + ) + + +@marshaller.attrs(slots=True) +class MessageReactionAddEvent(HikariEvent, entities.Deserializable): + """Used to represent Message Reaction Add gateway events.""" + + #: The ID of the user adding the reaction. + #: + #: :type: :obj:`snowflakes.Snowflake` + user_id: snowflakes.Snowflake = marshaller.attrib(deserializer=snowflakes.Snowflake.deserialize) + + #: The ID of the channel where this reaction is being added. + #: + #: :type: :obj:`snowflakes.Snowflake` + channel_id: snowflakes.Snowflake = marshaller.attrib(deserializer=snowflakes.Snowflake.deserialize) + + #: The ID of the message this reaction is being added to. + #: + #: :type: :obj:`snowflakes.Snowflake` + message_id: snowflakes.Snowflake = marshaller.attrib(deserializer=snowflakes.Snowflake.deserialize) + + #: The ID of the guild where this reaction is being added, unless this is + #: happening in a DM channel. + #: + #: :type: :obj:`snowflakes.Snowflake`, optional + guild_id: typing.Optional[snowflakes.Snowflake] = marshaller.attrib( + deserializer=snowflakes.Snowflake, if_undefined=None + ) + + #: The member object of the user who's adding this reaction, if this is + #: occurring in a guild. + #: + #: :type: :obj:`guilds.GuildMember`, optional + member: typing.Optional[guilds.GuildMember] = marshaller.attrib( + deserializer=guilds.GuildMember.deserialize, if_undefined=None + ) + + #: The object of the emoji being added. + #: + #: :type: :obj:`typing.Union` [ :obj:`_emojis.UnknownEmoji`, `_emojis.UnicodeEmoji` ] + emoji: typing.Union[_emojis.UnknownEmoji, _emojis.UnicodeEmoji] = marshaller.attrib( + deserializer=_emojis.deserialize_reaction_emoji, + ) + + +@marshaller.attrs(slots=True) +class MessageReactionRemoveEvent(HikariEvent, entities.Deserializable): + """Used to represent Message Reaction Remove gateway events.""" + + #: The ID of the user who is removing their reaction. + #: + #: :type: :obj:`snowflakes.Snowflake` + user_id: snowflakes.Snowflake = marshaller.attrib(deserializer=snowflakes.Snowflake.deserialize) + + #: The ID of the channel where this reaction is being removed. + #: + #: :type: :obj:`snowflakes.Snowflake` + channel_id: snowflakes.Snowflake = marshaller.attrib(deserializer=snowflakes.Snowflake.deserialize) + + #: The ID of the message this reaction is being removed from. + #: + #: :type: :obj:`snowflakes.Snowflake` + message_id: snowflakes.Snowflake = marshaller.attrib(deserializer=snowflakes.Snowflake.deserialize) + + #: The ID of the guild where this reaction is being removed, unless this is + #: happening in a DM channel. + #: + #: :type: :obj:`snowflakes.Snowflake`, optional + guild_id: typing.Optional[snowflakes.Snowflake] = marshaller.attrib( + deserializer=snowflakes.Snowflake, if_undefined=None + ) + + #: The object of the emoji being removed. + #: + #: :type: :obj:`typing.Union` [ :obj:`_emojis.UnknownEmoji`, `_emojis.UnicodeEmoji` ] + emoji: typing.Union[_emojis.UnicodeEmoji, _emojis.UnknownEmoji] = marshaller.attrib( + deserializer=_emojis.deserialize_reaction_emoji, + ) + + +@marshaller.attrs(slots=True) +class MessageReactionRemoveAllEvent(HikariEvent, entities.Deserializable): + """Used to represent Message Reaction Remove All gateway events. + Sent when all the reactions are removed from a message, regardless of emoji. + """ + + #: The ID of the channel where the targeted message is. + #: + #: :type: :obj:`snowflakes.Snowflake` + channel_id: snowflakes.Snowflake = marshaller.attrib(deserializer=snowflakes.Snowflake.deserialize) + + #: The ID of the message all reactions are being removed from. + #: + #: :type: :obj:`snowflakes.Snowflake` + message_id: snowflakes.Snowflake = marshaller.attrib(deserializer=snowflakes.Snowflake.deserialize) + + #: The ID of the guild where the targeted message is, if applicable. + #: + #: :type: :obj:`snowflakes.Snowflake` + guild_id: typing.Optional[snowflakes.Snowflake] = marshaller.attrib( + deserializer=snowflakes.Snowflake, if_undefined=None + ) + + +@marshaller.attrs(slots=True) +class MessageReactionRemoveEmojiEvent(HikariEvent, entities.Deserializable): + """Represents Message Reaction Remove Emoji events. + Sent when all the reactions for a single emoji are removed from a message. + """ + + #: The ID of the channel where the targeted message is. + #: + #: :type: :obj:`snowflakes.Snowflake` + channel_id: snowflakes.Snowflake = marshaller.attrib(deserializer=snowflakes.Snowflake.deserialize) + + #: The ID of the guild where the targeted message is, if applicable. + #: + #: :type: :obj:`snowflakes.Snowflake` + guild_id: typing.Optional[snowflakes.Snowflake] = marshaller.attrib( + deserializer=snowflakes.Snowflake, if_undefined=None + ) + + #: The ID of the message the reactions are being removed from. + #: + #: :type: :obj:`snowflakes.Snowflake` + message_id: snowflakes.Snowflake = marshaller.attrib(deserializer=snowflakes.Snowflake.deserialize) + + #: The object of the emoji that's being removed. + #: + #: :type: :obj:`typing.Union` [ :obj:`_emojis.UnknownEmoji`, `_emojis.UnicodeEmoji` ] + emoji: typing.Union[_emojis.UnicodeEmoji, _emojis.UnknownEmoji] = marshaller.attrib( + deserializer=_emojis.deserialize_reaction_emoji, + ) + + +@marshaller.attrs(slots=True) +class PresenceUpdateEvent(HikariEvent, guilds.GuildMemberPresence): + """Used to represent Presence Update gateway events. + Sent when a guild member changes their presence. + """ + + +@marshaller.attrs(slots=True) +class TypingStartEvent(HikariEvent, entities.Deserializable): + """Used to represent typing start gateway events. + Received when a user or bot starts "typing" in a channel. + """ + + #: The ID of the channel this typing event is occurring in. + #: + #: :type: :obj:`snowflakes.Snowflake` + channel_id: snowflakes.Snowflake = marshaller.attrib(deserializer=snowflakes.Snowflake.deserialize) + + #: The ID of the guild this typing event is occurring in. + #: Will be ``None`` if this event is happening in a DM channel. + #: + #: :type: :obj:`snowflakes.Snowflake`, optional + guild_id: typing.Optional[snowflakes.Snowflake] = marshaller.attrib( + deserializer=snowflakes.Snowflake.deserialize, if_undefined=None + ) + + #: The ID of the user who triggered this typing event. + #: + #: :type: :obj:`snowflakes.Snowflake` + user_id: snowflakes.Snowflake = marshaller.attrib(deserializer=snowflakes.Snowflake.deserialize) + + #: The datetime of when this typing event started. + #: + #: :type: :obj:`datetime.datetime` + timestamp: datetime.datetime = marshaller.attrib( + deserializer=lambda date: datetime.datetime.fromtimestamp(date, datetime.timezone.utc) + ) + + #: The member object of the user who triggered this typing event, + #: if this was triggered in a guild. + #: + #: :type: :obj:`guilds.GuildMember`, optional + member: typing.Optional[guilds.GuildMember] = marshaller.attrib( + deserializer=guilds.GuildMember.deserialize, if_undefined=None + ) + + +@marshaller.attrs(slots=True) +class UserUpdateEvent(HikariEvent, users.MyUser): + """Used to represent User Update gateway events. + Sent when the current user is updated. + """ + + +@marshaller.attrs(slots=True) +class VoiceStateUpdateEvent(HikariEvent, voices.VoiceState): + """Used to represent voice state update gateway events. + Sent when a user joins, leaves or moves voice channel(s). + """ + + +@marshaller.attrs(slots=True) +class VoiceServerUpdateEvent(HikariEvent, entities.Deserializable): + """Used to represent voice server update gateway events. + Sent when initially connecting to voice and when the current voice instance + falls over to a new server. + """ + + #: The voice connection's token + #: + #: :type: :obj:`str` + token: str = marshaller.attrib(deserializer=str) + + #: The ID of the guild this voice server update is for + #: + #: :type: :obj:`snowflakes.Snowflake` + guild_id: snowflakes.Snowflake = marshaller.attrib(deserializer=snowflakes.Snowflake.deserialize) + + #: The uri for this voice server host. + #: + #: :type: :obj:`str` + endpoint: str = marshaller.attrib(deserializer=str) + + +@marshaller.attrs(slots=True) +class WebhookUpdate(HikariEvent, entities.Deserializable): + """Used to represent webhook update gateway events. + Sent when a webhook is updated, created or deleted in a guild. + """ + + #: The ID of the guild this webhook is being updated in. + #: + #: :type: :obj:`snowflakes.Snowflake` + guild_id: snowflakes.Snowflake = marshaller.attrib(deserializer=snowflakes.Snowflake.deserialize) + + #: The ID of the channel this webhook is being updated in. + #: + #: :type: :obj:`snowflakes.Snowflake` + channel_id: snowflakes.Snowflake = marshaller.attrib(deserializer=snowflakes.Snowflake.deserialize) diff --git a/hikari/core/guilds.py b/hikari/core/guilds.py index 0fcdfb01f6..27b80d2cda 100644 --- a/hikari/core/guilds.py +++ b/hikari/core/guilds.py @@ -19,6 +19,8 @@ """Components and entities that are used to describe guilds on Discord. """ __all__ = [ + "ActivityFlag", + "ActivityType", "GuildChannel", "GuildTextChannel", "GuildNewsChannel", @@ -39,6 +41,7 @@ "GuildIntegration", "GuildMemberBan", "PartialGuild", + "PresenceStatus", ] import datetime @@ -217,13 +220,403 @@ class GuildVerificationLevel(enum.IntEnum): @marshaller.attrs(slots=True) class GuildMember(entities.HikariEntity, entities.Deserializable): + """Used to represent a guild bound member.""" + + #: This member's user object. + #: + #: :type: :obj:`users.User` user: users.User = marshaller.attrib(deserializer=users.User.deserialize) + #: This member's nickname, if set. + #: + #: :type: :obj:`str`, optional + nickname: typing.Optional[str] = marshaller.attrib(raw_name="nick", deserializer=str, if_none=None) + + #: A sequence of the IDs of the member's current roles. + #: + #: :type: :obj:`typing.Sequence` [ :obj:`snowflakes.Snowflake` ] + role_ids: typing.Sequence[snowflakes.Snowflake] = marshaller.attrib( + raw_name="roles", deserializer=lambda role_ids: [snowflakes.Snowflake.deserialize(rid) for rid in role_ids], + ) + + #: The datetime of when this member joined the guild they belong to. + #: + #: :type: :obj:`datetime.datetime` + joined_at: datetime.datetime = marshaller.attrib(deserializer=dates.parse_iso_8601_ts) + + #: The datetime of when this member started "boosting" this guild. + #: Will be ``None`` if they aren't boosting. + #: + #: :type: :obj:`datetime.datetime`, optional + premium_since: typing.Optional[datetime.datetime] = marshaller.attrib( + deserializer=dates.parse_iso_8601_ts, if_none=None, if_undefined=None, + ) + + #: Whether this member is deafened by this guild in it's voice channels. + #: + #: :type: :obj:`bool` + is_deaf: bool = marshaller.attrib(raw_name="deaf", deserializer=bool) + + #: Whether this member is muted by this guild in it's voice channels. + #: + #: :type: :obj:`bool` + is_mute: bool = marshaller.attrib(raw_name="mute", deserializer=bool) + -# Wait, so is Presence just an extension of Member? Should we subclass it? @marshaller.attrs(slots=True) -class GuildMemberPresence(entities.HikariEntity): - user: users.User = marshaller.attrib(deserializer=users.User.deserialize) +class GuildRole(snowflakes.UniqueEntity, entities.Deserializable): + ... + + +@enum.unique +class ActivityType(enum.IntEnum): + """ + The activity state. + """ + + #: Shows up as ``Playing `` + PLAYING = 0 + #: Shows up as ``Streaming ``. + #: + #: Warnings + #: -------- + #: Corresponding presences must be associated with VALID Twitch or YouTube + #: stream URLS! + STREAMING = 1 + #: Shows up as ``Listening to ``. + LISTENING = 2 + #: Shows up as ``Watching ``. Note that this is not officially + #: documented, so will be likely removed in the near future. + WATCHING = 3 + #: A custom status. + #: + #: To set an emoji with the status, place a unicode emoji or Discord emoji + #: (``:smiley:``) as the first part of the status activity name. + CUSTOM = 4 + + +@marshaller.attrs(slots=True) +class ActivityTimestamps(entities.HikariEntity, entities.Deserializable): + """The datetimes for the start and/or end of an activity session.""" + + #: When this activity's session was started, if applicable. + #: + #: :type: :obj:`datetime.datetime`, optional + start: typing.Optional[datetime.datetime] = marshaller.attrib( + deserializer=dates.unix_epoch_to_ts, if_undefined=None + ) + + #: When this activity's session will end, if applicable. + #: + #: :type: :obj:`datetime.datetime`, optional + end: typing.Optional[datetime.datetime] = marshaller.attrib(deserializer=dates.unix_epoch_to_ts, if_undefined=None) + + +@marshaller.attrs(slots=True) +class ActivityParty(entities.HikariEntity, entities.Deserializable): + """Used to represent activity groups of users.""" + + #: The string id of this party instance, if set. + #: + #: :type: :obj:`str`, optional + id: typing.Optional[str] = marshaller.attrib(deserializer=str, if_undefined=None) + + #: The size metadata of this party, if applicable. + #: + #: :type: :obj:`typing.Tuple` [ :obj:`int`, :obj:`int` ], optional + _size_information: typing.Optional[typing.Tuple[int, int]] = marshaller.attrib( + raw_name="size", deserializer=tuple, if_undefined=None, + ) + + @property + def current_size(self) -> typing.Optional[int]: + """The current size of this party, if applicable.""" + return self._size_information and self._size_information[0] or None + + @property + def max_size(self) -> typing.Optional[int]: + """The maximum size of this party, if applicable""" + return self._size_information and self._size_information[1] or None + + +@marshaller.attrs(slots=True) +class ActivityAssets(entities.HikariEntity, entities.Deserializable): + """Used to represent possible assets for an activity.""" + + #: The ID of the asset's large image, if set. + #: + #: :type: :obj:`str`, optional + large_image: typing.Optional[str] = marshaller.attrib(deserializer=str, if_undefined=None) + + #: The text that'll appear when hovering over the large image, if set. + #: + #: :type: :obj:`str`, optional + large_text: typing.Optional[str] = marshaller.attrib(deserializer=str, if_undefined=None) + + #: The ID of the asset's small image, if set. + #: + #: :type: :obj:`str`, optional + small_image: typing.Optional[str] = marshaller.attrib(deserializer=str, if_undefined=None) + + #: The text that'll appear when hovering over the small image, if set. + #: + #: :type: :obj:`str`, optional + small_text: typing.Optional[str] = marshaller.attrib(deserializer=str, if_undefined=None) + + +@marshaller.attrs(slots=True) +class ActivitySecret(entities.HikariEntity, entities.Deserializable): + """The secrets used for interacting with an activity party.""" + + #: The secret used for joining a party, if applicable. + #: + #: :type: :obj:`str`, optional + join: typing.Optional[str] = marshaller.attrib(deserializer=str, if_undefined=None) + + #: The secret used for spectating a party, if applicable. + #: + #: :type: :obj:`str`, optional + spectate: typing.Optional[str] = marshaller.attrib(deserializer=str, if_undefined=None) + + #: The secret used for joining a party, if applicable. + #: + #: :type: :obj:`str`, optional + match: typing.Optional[str] = marshaller.attrib(deserializer=str, if_undefined=None) + + +class ActivityFlag(enum.IntFlag): + """ + Flags that describe what an activity includes, + can be more than one using bitwise-combinations. + """ + + INSTANCE = 1 << 0 + JOIN = 1 << 1 + SPECTATE = 1 << 2 + JOIN_REQUEST = 1 << 3 + SYNC = 1 << 4 + PLAY = 1 << 5 + + +@marshaller.attrs(slots=True) +class PresenceActivity(entities.HikariEntity, entities.Deserializable): + """Represents an activity that'll be attached to a member's presence.""" + + #: The activity's name. + #: + #: :type: :obj:`str` + name: str = marshaller.attrib(deserializer=str) + + #: The activity's type. + #: + #: :type: :obj:`ActivityType` + type: ActivityType = marshaller.attrib(deserializer=ActivityType) + + #: The url for a ``STREAM` type activity, if applicable + #: + #: :type: :obj:`url`, optional + url: typing.Optional[str] = marshaller.attrib(deserializer=str, if_undefined=None, if_none=None) + + #: When this activity was added to the user's session. + #: + #: :type: :obj:`datetime.datetime` + created_at: datetime.datetime = marshaller.attrib(deserializer=dates.unix_epoch_to_ts) + + #: The timestamps for when this activity's current state will start and + #: end, if applicable. + #: + #: :type: :obj:`ActivityTimestamps`, optional + timestamps: ActivityTimestamps = marshaller.attrib(deserializer=ActivityTimestamps.deserialize) + + #: The ID of the application this activity is for, if applicable. + #: + #: :type: :obj:`snowflakes.Snowflake`, optional + application_id: typing.Optional[snowflakes.Snowflake] = marshaller.attrib( + deserializer=snowflakes.Snowflake.deserialize, if_undefined=None + ) + + #: The text that describes what the activity's target is doing, if set. + #: + #: :type: :obj:`str`, optional + details: typing.Optional[str] = marshaller.attrib(deserializer=str, if_undefined=None, if_none=None) + + #: The current status of this activity's target, if set. + #: + #: :type: :obj:`str`, optional + state: typing.Optional[str] = marshaller.attrib(deserializer=str, if_undefined=None, if_none=None) + + #: The emoji of this activity, if it is a custom status and set. + #: + #: :type: :obj:`typing.Union` [ :obj:`_emojis.UnicodeEmoji`, :obj:`_emojis.UnknownEmoji` ], optional + emoji: typing.Union[None, _emojis.UnicodeEmoji, _emojis.UnknownEmoji] = marshaller.attrib( + deserializer=_emojis.deserialize_reaction_emoji, if_undefined=None + ) + + #: Information about the party associated with this activity, if set. + #: + #: :type: :obj:`ActivityParty`, optional + party: typing.Optional[ActivityParty] = marshaller.attrib(deserializer=ActivityParty.deserialize, if_undefined=None) + + #: Images and their hover over text for the activity. + #: + #: :type: :obj:`ActivityAssets`, optional + assets: typing.Optional[ActivityAssets] = marshaller.attrib( + deserializer=ActivityAssets.deserialize, if_undefined=None + ) + + #: Secrets for Rich Presence joining and spectating. + #: + #: :type: :obj:`ActivitySecret`, optional + secrets: typing.Optional[ActivitySecret] = marshaller.attrib( + deserializer=ActivitySecret.deserialize, if_undefined=None + ) + + #: Whether this activity is an instanced game session. + #: + #: :type: :obj:`bool`, optional + is_instance: typing.Optional[bool] = marshaller.attrib(raw_name="instance", deserializer=bool, if_undefined=None) + + #: Flags that describe what the activity includes. + #: + #: :type: :obj:`ActivityFlag` + flags: ActivityFlag = marshaller.attrib(deserializer=ActivityFlag, if_undefined=None) + + +class PresenceStatus(enum.Enum): + """ + The status of a member. + """ + + #: Online/green. + ONLINE = "online" + #: Idle/yellow. + IDLE = "idle" + #: Do not disturb/red. + DND = "dnd" + #: An alias for :attr:`DND` + DO_NOT_DISTURB = DND + #: Offline or invisible/grey. + OFFLINE = "offline" + + +@marshaller.attrs(slots=True) +class ClientStatus(entities.HikariEntity, entities.Deserializable): + """The client statuses for this member.""" + + #: The status of the target user's desktop session. + #: + #: :type: :obj:`PresenceStatus` + desktop: PresenceStatus = marshaller.attrib( + deserializer=PresenceStatus, if_undefined=lambda: PresenceStatus.OFFLINE, + ) + + #: The status of the target user's mobile session. + #: + #: :type: :obj:`PresenceStatus` + mobile: PresenceStatus = marshaller.attrib(deserializer=PresenceStatus, if_undefined=lambda: PresenceStatus.OFFLINE) + + #: The status of the target user's web session. + #: + #: :type: :obj:`PresenceStatus` + web: PresenceStatus = marshaller.attrib(deserializer=PresenceStatus, if_undefined=lambda: PresenceStatus.OFFLINE) + + +@marshaller.attrs(slots=True) +class PresenceUser(users.User): + """A user representation specifically used for presence updates. + + Warnings + -------- + Every attribute except :attr:`id` may be received as :obj:`entities.UNSET` + unless it is specifically being modified for this update. + """ + + #: This user's discriminator. + #: + #: :type: :obj:`typing.Union` [ :obj:`str`, `entities.UNSET` ] + discriminator: typing.Union[str, entities.Unset] = marshaller.attrib(deserializer=str, if_undefined=entities.Unset) + + #: This user's username. + #: + #: :type: :obj:`typing.Union` [ :obj:`str`, `entities.UNSET` ] + username: typing.Union[str, entities.Unset] = marshaller.attrib(deserializer=str, if_undefined=entities.Unset) + + #: This user's avatar hash, if set. + #: + #: :type: :obj:`typing.Union` [ :obj:`str`, `entities.UNSET` ], optional + avatar_hash: typing.Union[None, str, entities.Unset] = marshaller.attrib( + raw_name="avatar", deserializer=str, if_none=None, if_undefined=entities.Unset + ) + + #: Whether this user is a bot account. + #: + #: :type: :obj:`typing.Union` [ :obj:`bool`, `entities.UNSET` ] + is_bot: typing.Union[bool, entities.Unset] = marshaller.attrib( + raw_name="bot", deserializer=bool, if_undefined=entities.Unset + ) + + #: Whether this user is a system account. + #: + #: :type: :obj:`typing.Union` [ :obj:`bool`, `entities.UNSET` ] + is_system: typing.Union[bool, entities.Unset] = marshaller.attrib( + raw_name="system", deserializer=bool, if_undefined=entities.Unset, + ) + + +@marshaller.attrs(slots=True) +class GuildMemberPresence(entities.HikariEntity, entities.Deserializable): + """Used to represent a guild member's presence.""" + + #: The object of the user who this presence is for, only `id` is guaranteed + #: for this partial object, with other attributes only being included when + #: when they are being changed in an event. + #: + #: :type: :obj:`PresenceUser` + user: PresenceUser = marshaller.attrib(deserializer=PresenceUser.deserialize) + + #: A sequence of the ids of the user's current roles in the guild this + #: presence belongs to. + #: + #: :type: :obj:`typing.Sequence` [ :obj:`snowflakes.Snowflake` ] + role_ids: typing.Sequence[snowflakes.Snowflake] = marshaller.attrib( + raw_name="roles", deserializer=lambda roles: [snowflakes.Snowflake.deserialize(rid) for rid in roles], + ) + + #: The ID of the guild this presence belongs to. + #: + #: :type: :obj:`snowflakes.Snowflake` + guild_id: snowflakes.Snowflake = marshaller.attrib(deserializer=snowflakes.Snowflake.deserialize) + + #: This user's current status being displayed by the client. + #: + #: :type: :obj:`PresenceStatus` + visible_status: PresenceStatus = marshaller.attrib(raw_name="status", deserializer=PresenceStatus) + + #: An array of the user's activities, with the top one will being + #: prioritised by the client. + #: + #: :type: :obj:`typing.Sequence` [ :obj:`PresenceActivity` ] + activities: typing.Sequence[PresenceActivity] = marshaller.attrib( + deserializer=lambda activities: [PresenceActivity.deserialize(a) for a in activities] + ) + + #: An object of the target user's client statuses. + #: + #: :type: :obj:`ClientStatus` + client_status: ClientStatus = marshaller.attrib(deserializer=ClientStatus.deserialize) + + #: The datetime of when this member started "boosting" this guild. + #: Will be ``None`` if they aren't boosting. + #: + #: :type: :obj:`datetime.datetime`, optional + premium_since: typing.Optional[datetime.datetime] = marshaller.attrib( + deserializer=dates.parse_iso_8601_ts, if_none=None, if_undefined=None, + ) + + #: This member's nickname, if set. + #: + #: :type: :obj:`str`, optional + nick: typing.Optional[str] = marshaller.attrib(raw_name="nick", deserializer=str, if_undefined=None, if_none=None) @marshaller.attrs(slots=True) @@ -237,8 +630,19 @@ class GuildMemberBan(entities.HikariEntity): @marshaller.attrs(slots=True) -class GuildRole(snowflakes.UniqueEntity, entities.Deserializable): - ... +class UnavailableGuild(snowflakes.UniqueEntity, entities.Deserializable): + """An unavailable guild object, received during gateway events such as + the "Ready". + An unavailable guild cannot be interacted with, and most information may + be outdated if that is the case. + """ + + @property + def is_unavailable(self) -> bool: + """ + Whether this guild is unavailable or not, should always be :obj:`True`. + """ + return True @marshaller.attrs(slots=True) @@ -304,8 +708,8 @@ class Guild(PartialGuild): ---- If a guild object is considered to be unavailable, then the state of any other fields other than the :attr:`is_unavailable` and :attr:`id` members - may be ``None``, outdated, or incorrect. If a guild is unavailable, then - the contents of any other fields should be ignored. + outdated, or incorrect. If a guild is unavailable, then the contents of any + other fields should be ignored. """ #: The hash of the splash for the guild, if there is one. @@ -360,7 +764,7 @@ class Guild(PartialGuild): #: #: :type: :obj:`bool`, optional is_embed_enabled: typing.Optional[bool] = marshaller.attrib( - raw_name="embed_enabled", if_undefined=lambda: False, deserializer=bool + raw_name="embed_enabled", if_undefined=False, deserializer=bool ) #: The channel ID that the guild embed will generate an invite to, if @@ -418,6 +822,18 @@ class Guild(PartialGuild): deserializer=snowflakes.Snowflake, if_none=None ) + #: Whether the guild is unavailable or not. + #: + #: This information is only available if the guild was sent via a + #: ``GUILD_CREATE`` event. If the guild is received from any other place, + #: this will always be ``None``. + #: + #: An unavailable guild cannot be interacted with, and most information may + #: be outdated if that is the case. + is_unavailable: typing.Optional[bool] = marshaller.attrib( + raw_name="unavailable", if_undefined=None, deserializer=bool + ) + # TODO: document in which cases this information is not available. #: Describes whether the guild widget is enabled or not. If this information #: is not present, this will be ``None``. @@ -479,18 +895,6 @@ class Guild(PartialGuild): #: :type: :obj:`bool`, optional is_large: typing.Optional[bool] = marshaller.attrib(raw_name="large", if_undefined=None, deserializer=bool) - #: Whether the guild is unavailable or not. - #: - #: This information is only available if the guild was sent via a - #: ``GUILD_CREATE`` event. If the guild is received from any other place, - #: this will always be ``None``. - #: - #: An unavailable guild cannot be interacted with, and most information may - #: be outdated or missing if that is the case. - is_unavailable: typing.Optional[bool] = marshaller.attrib( - raw_name="unavailable", if_undefined=None, deserializer=bool - ) - #: The number of members in this guild. #: #: This information is only available if the guild was sent via a diff --git a/hikari/core/invites.py b/hikari/core/invites.py index 7f69cebd70..49af829ddf 100644 --- a/hikari/core/invites.py +++ b/hikari/core/invites.py @@ -185,13 +185,13 @@ class Invite(entities.HikariEntity, entities.Deserializable): #: The approximate amount of presences in this invite's guild, only present #: when ``with_counts`` is passed as ``True`` to the GET invites endpoint. #: - #: :type: :obj:`int` + #: :type: :obj:`int`, optional approximate_presence_count: typing.Optional[int] = marshaller.attrib(deserializer=int, if_undefined=None) #: The approximate amount of members in this invite's guild, only present #: when ``with_counts`` is passed as ``True`` to the GET invites endpoint. #: - #: :type: :obj:`int` + #: :type: :obj:`int`, optional approximate_member_count: typing.Optional[int] = marshaller.attrib(deserializer=int, if_undefined=None) @@ -212,11 +212,13 @@ class InviteWithMetadata(Invite): #: :type: :obj:`int` max_uses: int = marshaller.attrib(deserializer=int) - #: The amount of time (in seconds) this invite will be valid for. - #: If set to ``0`` then this is unlimited. + #: The timedelta of how long this invite will be valid for. + #: If set to :obj:`None` then this is unlimited. #: - #: :type: :obj:`int` - max_age: int = marshaller.attrib(deserializer=int) + #: :type: :obj:`datetime.timedelta`, optional + max_age: typing.Optional[datetime.timedelta] = marshaller.attrib( + deserializer=lambda age: datetime.timedelta(seconds=age) if age > 0 else None + ) #: Whether this invite grants temporary membership. #: @@ -232,5 +234,5 @@ class InviteWithMetadata(Invite): def expires_at(self) -> typing.Optional[datetime.datetime]: """The :obj:`datetime` of when this invite should expire, if ``max_age`` is set.""" if self.max_age: - return self.created_at + datetime.timedelta(seconds=self.max_age) + return self.created_at + self.max_age return None diff --git a/hikari/core/messages.py b/hikari/core/messages.py index c07c064345..9e41d2c329 100644 --- a/hikari/core/messages.py +++ b/hikari/core/messages.py @@ -16,7 +16,9 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . - +""" +Components and entities that are used to describe messages on Discord. +""" __all__ = [ "MessageType", "Message", @@ -71,8 +73,7 @@ class MessageType(enum.IntEnum): CHANNEL_FOLLOW_ADD = 12 -@enum.unique -class MessageFlag(enum.IntEnum): +class MessageFlag(enum.IntFlag): """Additional flags for message options.""" NONE = 0x0 @@ -276,7 +277,7 @@ class Message(snowflakes.UniqueEntity, entities.Deserializable): #: :type: :obj:`typing.Set` [ :obj:`snowflakes.Snowflake` ] role_mentions: typing.Set[snowflakes.Snowflake] = marshaller.attrib( raw_name="mention_roles", - deserializer=lambda role_mentions: {r for r in map(snowflakes.Snowflake, role_mentions)}, + deserializer=lambda role_mentions: {snowflakes.Snowflake.deserialize(mention) for mention in role_mentions}, ) #: The channels the message mentions. @@ -292,14 +293,14 @@ class Message(snowflakes.UniqueEntity, entities.Deserializable): #: #: :type: :obj:`typing.Sequence` [ :obj:`Attachment` ] attachments: typing.Sequence[Attachment] = marshaller.attrib( - deserializer=lambda attachments: [a for a in map(Attachment.deserialize, attachments)] + deserializer=lambda attachments: [Attachment.deserialize(a) for a in attachments] ) #: The message embeds. #: #: :type: :obj:`typing.Sequence` [ :obj:`_embeds.Embed` ] embeds: typing.Sequence[_embeds.Embed] = marshaller.attrib( - deserializer=lambda embeds: [e for e in map(_embeds.Embed.deserialize, embeds)] + deserializer=lambda embeds: [_embeds.Embed.deserialize(e) for e in embeds] ) #: The message reactions. diff --git a/hikari/core/users.py b/hikari/core/users.py index 6734c61095..90489007a5 100644 --- a/hikari/core/users.py +++ b/hikari/core/users.py @@ -50,12 +50,12 @@ class User(snowflakes.UniqueEntity, entities.Deserializable): #: Whether this user is a bot account. #: #: :type: :obj:`bool` - is_bot: bool = marshaller.attrib(raw_name="bot", deserializer=bool, if_undefined=lambda: False) + is_bot: bool = marshaller.attrib(raw_name="bot", deserializer=bool, if_undefined=False) #: Whether this user is a system account. #: #: :type: :obj:`bool` - is_system: bool = marshaller.attrib(raw_name="system", deserializer=bool, if_undefined=lambda: False) + is_system: bool = marshaller.attrib(raw_name="system", deserializer=bool, if_undefined=False) @property def avatar_url(self) -> str: diff --git a/hikari/core/voices.py b/hikari/core/voices.py new file mode 100644 index 0000000000..9003091259 --- /dev/null +++ b/hikari/core/voices.py @@ -0,0 +1,128 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# Copyright © Nekoka.tt 2019-2020 +# +# This file is part of Hikari. +# +# Hikari is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Hikari is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Hikari. If not, see . +"""Components and entities that are used to describe voice states on Discord. +""" +__all__ = ["VoiceRegion", "VoiceState"] + +import typing + +from hikari.core import entities +from hikari.core import guilds +from hikari.core import snowflakes +from hikari.internal_utilities import marshaller + + +@marshaller.attrs(slots=True) +class VoiceState(entities.HikariEntity, entities.Deserializable): + """Represents a user's voice connection status.""" + + #: The ID of the guild this voice state is in, if applicable. + #: + #: :type: :obj:`snowflakes.Snowflake`, optional + guild_id: typing.Optional[snowflakes.Snowflake] = marshaller.attrib( + deserializer=snowflakes.Snowflake.deserialize, if_undefined=None + ) + + #: The ID of the channel this user is connected to. + #: + #: :type: :obj:`.core.snowflakes.Snowflake`, optional + channel_id: snowflakes.Snowflake = marshaller.attrib(deserializer=snowflakes.Snowflake.deserialize) + + #: The ID of the user this voice state is for. + #: + #: :type: :obj:`snowflakes.Snowflake` + user_id: snowflakes.Snowflake = marshaller.attrib(deserializer=snowflakes.Snowflake.deserialize) + + #: The guild member this voice state is for if the voice state is in a + #: guild. + #: + #: :type: :obj:`guilds.GuildMember`, optional + member: typing.Optional[guilds.GuildMember] = marshaller.attrib( + deserializer=guilds.GuildMember.deserialize, if_undefined=None + ) + + #: The ID of this voice state's session. + #: + #: :type: :obj:`str` + session_id: str = marshaller.attrib(deserializer=str) + + #: Whether this user is deafened by the guild. + #: + #: :type: :obj:`bool` + is_guild_deafened: bool = marshaller.attrib(raw_name="deaf", deserializer=bool) + + #: Whether this user is muted by the guild. + #: + #: :type: :obj:`bool` + is_guild_muted: bool = marshaller.attrib(raw_name="mute", deserializer=bool) + + #: Whether this user is deafened by their client. + #: + #: :type: :obj:`bool` + is_self_deafened: bool = marshaller.attrib(raw_name="self_deaf", deserializer=bool) + + #: Whether this user is muted by their client. + #: + #: :type: :obj:`bool` + is_self_muted: bool = marshaller.attrib(raw_name="self_mute", deserializer=bool) + + #: Whether this user is streaming using "Go Live". + #: + #: :type: :obj:`bool` + is_streaming: bool = marshaller.attrib(raw_name="self_stream", deserializer=bool, if_undefined=False) + + #: Whether this user is muted by the current user. + #: + #: :type: :obj:`bool` + is_suppressed: bool = marshaller.attrib(raw_name="suppress", deserializer=bool) + + +@marshaller.attrs(slots=True) +class VoiceRegion(entities.HikariEntity, entities.Deserializable): + """Represent's a voice region server.""" + + #: The ID of this region + #: + #: :type: :obj:`str` + id: str = marshaller.attrib(deserializer=str) + + #: The name of this region + #: + #: :type: :obj:`str` + name: str = marshaller.attrib(deserializer=str) + + #: Whether this region is vip-only. + #: + #: :type: :obj:`bool` + is_vip: bool = marshaller.attrib(raw_name="vip", deserializer=bool) + + #: Whether this region's server is closest to the current user's client. + #: + #: :type: :obj:`bool` + is_optimal_location: bool = marshaller.attrib(raw_name="optimal", deserializer=bool) + + #: Whether this region is deprecated. + #: + #: :type: :obj:`bool` + is_deprecated: bool = marshaller.attrib(raw_name="deprecated", deserializer=bool) + + #: Whether this region is custom (e.g. used for events). + #: + #: :type: :obj:`bool` + is_custom: bool = marshaller.attrib(raw_name="custom", deserializer=bool) diff --git a/hikari/core/webhooks.py b/hikari/core/webhooks.py index 4735e36213..cf11c5d2d8 100644 --- a/hikari/core/webhooks.py +++ b/hikari/core/webhooks.py @@ -16,7 +16,7 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . - +"""Components and entities that are used to describe webhooks on Discord.""" __all__ = ["WebhookType", "Webhook"] import enum diff --git a/hikari/internal_utilities/marshaller.py b/hikari/internal_utilities/marshaller.py index 0a9755a153..4c07ac1d7c 100644 --- a/hikari/internal_utilities/marshaller.py +++ b/hikari/internal_utilities/marshaller.py @@ -49,6 +49,8 @@ MARSHALLER_META_ATTR = "__hikari_marshaller_meta_attr__" +PASSED_THROUGH_SINGLETONS = (False, True, None) + RAISE = object() EntityT = typing.TypeVar("EntityT", contravariant=True) @@ -122,16 +124,18 @@ def attrib( transient : :obj:`bool` If ``True``, the field is marked as transient, meaning it will not be serialized. Defaults to ``False``. - if_none : :obj:`typing.Union` [ :obj:`typing.Callable` [ ... , :obj:`typing.Any` ], :obj:`None` ], optional + if_none Either a default factory function called to get the default for when - this field is ``None`` or ``None`` to specify that this should default - to ``None``. Will raise an exception when ``None`` is received for this - field later if this isn't specified. - if_undefined : :obj:`typing.Union` [ :obj:`typing.Callable` [ ... , :obj:`typing.Any` ], :obj:`None` ], optional + this field is ``None`` or one of ``None``, ``False`` or ``True`` to + specify that this should default to the given singleton. + Will raise an exception when ``None`` is received for this field later + if this isn't specified. + if_undefined Either a default factory function called to get the default for when - this field isn't defined or ``None`` to specify that this should default - to ``None``. Will raise an exception when this field is undefined later - on if this isn't specified. + this field isn't defined or one of ``None``, ``False`` or ``True`` to + specify that this should default to the given singleton. + Will raise an exception when this field is undefined later on if this + isn't specified. serializer : :obj:`typing.Callable` [ [ :obj:`typing.Any` ], :obj:`typing.Any` ], optional The serializer to use. If not specified, then serializing the entire class that this attribute is in will trigger a :obj:`TypeError` @@ -173,10 +177,10 @@ def error(*_, **__) -> typing.NoReturn: def _default_validator(value: typing.Any): assertions.assert_that( - value is RAISE or value is None or callable(value), + value is RAISE or value in PASSED_THROUGH_SINGLETONS or callable(value), message=( "Invalid default factory passed for `if_undefined` or `if_none`; " - f"expected a callable or `None` but got {value}." + f"expected a callable or one of the 'passed through singletons' but got {value}." ), error_type=RuntimeError, ) @@ -331,10 +335,10 @@ def deserialize(self, raw_data: typing.Mapping[str, typing.Any], target_type: ty f"Required field {a.field_name} (from raw {a.raw_name}) is not specified in the input " f"payload\n\n{raw_data}" ) - elif a.if_undefined: - kwargs[kwarg_name] = a.if_undefined() + if a.if_undefined in PASSED_THROUGH_SINGLETONS: + kwargs[kwarg_name] = a.if_undefined else: - kwargs[kwarg_name] = None + kwargs[kwarg_name] = a.if_undefined() continue elif (data := raw_data[a.raw_name]) is None: if a.if_none is RAISE: @@ -342,10 +346,10 @@ def deserialize(self, raw_data: typing.Mapping[str, typing.Any], target_type: ty f"Non-nullable field {a.field_name} (from raw {a.raw_name}) is `None` in the input " f"payload\n\n{raw_data}" ) - elif a.if_none: - kwargs[kwarg_name] = a.if_none() + if a.if_none in PASSED_THROUGH_SINGLETONS: + kwargs[kwarg_name] = a.if_none else: - kwargs[kwarg_name] = None + kwargs[kwarg_name] = a.if_none() continue try: diff --git a/tests/hikari/core/test_channels.py b/tests/hikari/core/test_channels.py index fac04af283..8f41bf3271 100644 --- a/tests/hikari/core/test_channels.py +++ b/tests/hikari/core/test_channels.py @@ -19,16 +19,32 @@ import pytest from hikari.core import channels - - -@pytest.fixture() -def test_partial_channel_payload(): - return {"id": "561884984214814750", "name": "general", "type": 0} +from hikari.core import permissions class TestPartialChannel: + @pytest.fixture() + def test_partial_channel_payload(self): + return {"id": "561884984214814750", "name": "general", "type": 0} + def test_deserialize(self, test_partial_channel_payload): partial_channel_obj = channels.PartialChannel.deserialize(test_partial_channel_payload) assert partial_channel_obj.id == 561884984214814750 assert partial_channel_obj.name == "general" assert partial_channel_obj.type is channels.ChannelType.GUILD_TEXT + + +class TestPermissionOverwrite: + @pytest.fixture() + def test_permission_overwrite_payload(self): + return {"id": "4242", "type": "member", "allow": 65, "deny": 49152} + + def test_deserialize(self, test_permission_overwrite_payload): + permission_overwrite_obj = channels.PermissionOverwrite.deserialize(test_permission_overwrite_payload) + assert ( + permission_overwrite_obj.allow + == permissions.Permission.CREATE_INSTANT_INVITE | permissions.Permission.ADD_REACTIONS + ) + assert permission_overwrite_obj.deny == permissions.Permission.EMBED_LINKS | permissions.Permission.ATTACH_FILES + assert permission_overwrite_obj.unset == permissions.Permission(49217) + assert isinstance(permission_overwrite_obj.unset, permissions.Permission) diff --git a/tests/hikari/core/test_entities.py b/tests/hikari/core/test_entities.py new file mode 100644 index 0000000000..23c417d31d --- /dev/null +++ b/tests/hikari/core/test_entities.py @@ -0,0 +1,34 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# Copyright © Nekoka.tt 2019-2020 +# +# This file is part of Hikari. +# +# Hikari is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Hikari is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along ith Hikari. If not, see . +from hikari.core import entities + + +class TestUnset: + def test_repr(self): + assert repr(entities.UNSET) == "UNSET" + + def test_str(self): + assert str(entities.UNSET) == "UNSET" + + def test_bool(self): + assert bool(entities.UNSET) is False + + def test_singleton_behaviour(self): + assert entities.Unset() is entities.Unset() + assert entities.UNSET is entities.Unset() diff --git a/tests/hikari/core/test_events.py b/tests/hikari/core/test_events.py new file mode 100644 index 0000000000..cf9d9425c2 --- /dev/null +++ b/tests/hikari/core/test_events.py @@ -0,0 +1,907 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# Copyright © Nekoka.tt 2019-2020 +# +# This file is part of Hikari. +# +# Hikari is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Hikari is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along ith Hikari. If not, see . +import datetime + +import cymock as mock +import pytest + +from hikari.core import channels +from hikari.core import embeds +from hikari.core import emojis +from hikari.core import entities +from hikari.core import events +from hikari.core import guilds +from hikari.core import invites +from hikari.core import messages +from hikari.core import oauth2 +from hikari.core import users +from hikari.internal_utilities import dates + +from tests.hikari import _helpers + + +@pytest.fixture() +def test_emoji_payload(): + return {"id": "4242", "name": "blahblah", "animated": True} + + +@pytest.fixture() +def test_user_payload(): + return {"id": "2929292", "username": "agent 69", "discriminator": "4444", "avatar": "9292929292929292"} + + +@pytest.fixture() +def test_guild_payload(): + return {"id": "40404040", "name": "electric guild boogaloo"} + + +@pytest.fixture() +def test_member_payload(test_user_payload): + return { + "user": test_user_payload, + "nick": "Agent 42", + "roles": [], + "joined_at": "2015-04-26T06:26:56.936000+00:00", + "premium_since": "2019-05-17T06:26:56.936000+00:00", + "deaf": True, + "mute": False, + } + + +@pytest.fixture() +def test_role_payload(): + return { + "id": "2929292929", + "name": "nyaa nyaa nyaa", + "color": 16735488, + "hoist": True, + "permissions": 2146959103, + "managed": False, + "mentionable": False, + } + + +@pytest.fixture() +def test_channel_payload(): + return {"id": "393939", "name": "a channel", "type": 2} + + +@pytest.fixture() +def test_overwrite_payload(): + return {"id": "292929", "type": "member", "allow": 49152, "deny": 0} + + +# Base event, is not deserialized +class TestHikariEvent: + ... + + +# Synthetic event, is not deserialized +class TestConnectedEvent: + ... + + +# Synthetic event, is not deserialized +class TestDisconnectedEvent: + ... + + +# Synthetic event, is not deserialized +class TestReconnectedEvent: + ... + + +# Synthetic event, is not deserialized +class TestStartedEvent: + ... + + +# Synthetic event, is not deserialized +class TestStoppingEvent: + ... + + +# Synthetic event, is not deserialized +class TestStoppedEvent: + ... + + +class TestReadyEvent: + @pytest.fixture() + def test_read_event_payload(self, test_guild_payload, test_user_payload): + return { + "v": 69420, + "user": test_user_payload, + "private_channels": [], + "guilds": [test_guild_payload], + "session_id": "osdkoiiodsaooeiwio9", + "shard": [42, 80], + } + + def test_deserialize(self, test_read_event_payload, test_guild_payload, test_user_payload): + mock_guild = mock.MagicMock(guilds.Guild, id=40404040) + mock_user = mock.MagicMock(users.MyUser) + with mock.patch.object(guilds.UnavailableGuild, "deserialize", return_value=mock_guild): + with _helpers.patch_marshal_attr( + events.ReadyEvent, "my_user", deserializer=users.MyUser.deserialize, return_value=mock_user + ) as patched_user_deserialize: + ready_obj = events.ReadyEvent.deserialize(test_read_event_payload) + patched_user_deserialize.assert_called_once_with(test_user_payload) + guilds.UnavailableGuild.deserialize.assert_called_once_with(test_guild_payload) + assert ready_obj.gateway_version == 69420 + assert ready_obj.my_user is mock_user + assert ready_obj.unavailable_guilds == {40404040: mock_guild} + assert ready_obj.session_id == "osdkoiiodsaooeiwio9" + assert ready_obj._shard_information == (42, 80) + + @pytest.fixture() + @mock.patch.object(guilds.UnavailableGuild, "deserialize") + @_helpers.patch_marshal_attr(events.ReadyEvent, "my_user", deserializer=users.MyUser.deserialize) + def mock_ready_event_obj(self, *args, test_read_event_payload): + return events.ReadyEvent.deserialize(test_read_event_payload) + + def test_shard_id_when_information_set(self, mock_ready_event_obj): + assert mock_ready_event_obj.shard_id == 42 + + def test_shard_count_when_information_set(self, mock_ready_event_obj): + assert mock_ready_event_obj.shard_count == 80 + + def test_shard_id_when_information_not_set(self, mock_ready_event_obj): + mock_ready_event_obj._shard_information = None + assert mock_ready_event_obj.shard_id is None + + def test_shard_count_when_information_not_set(self, mock_ready_event_obj): + mock_ready_event_obj._shard_information = None + assert mock_ready_event_obj.shard_count is None + + +# Doesn't have any fields. +class TestResumedEvent: + ... + + +class TestBaseChannelEvent: + @pytest.fixture() + def test_base_channel_payload(self, test_overwrite_payload, test_user_payload): + return { + "id": "424242", + "type": 2, + "guild_id": "69240", + "position": 7, + "permission_overwrites": [test_overwrite_payload], + "name": "Name", + "topic": "Topically drunk", + "nsfw": True, + "last_message_id": "22222222", + "bitrate": 96000, + "user_limit": 42, + "rate_limit_per_user": 2333, + "recipients": [test_user_payload], + "icon": "sdodsooioio2oi", + "owner_id": "32939393", + "application_id": "202020202", + "parent_id": "2030302939", + "last_pin_timestamp": "2019-05-17T06:26:56.936000+00:00", + } + + def test_deserialize(self, test_base_channel_payload, test_overwrite_payload, test_user_payload): + mock_timestamp = mock.MagicMock(datetime.datetime) + mock_user = mock.MagicMock(users.User, id=42) + mock_overwrite = mock.MagicMock(channels.PermissionOverwrite, id=64) + with _helpers.patch_marshal_attr( + events.BaseChannelEvent, + "last_pin_timestamp", + deserializer=dates.parse_iso_8601_ts, + return_value=mock_timestamp, + ) as patched_timestamp_deserializer: + with mock.patch.object(users.User, "deserialize", return_value=mock_user): + with mock.patch.object(channels.PermissionOverwrite, "deserialize", return_value=mock_overwrite): + base_channel_payload = events.BaseChannelEvent.deserialize(test_base_channel_payload) + channels.PermissionOverwrite.deserialize.assert_called_once_with(test_overwrite_payload) + users.User.deserialize.assert_called_once_with(test_user_payload) + patched_timestamp_deserializer.assert_called_once_with("2019-05-17T06:26:56.936000+00:00") + assert base_channel_payload.type is channels.ChannelType.GUILD_VOICE + assert base_channel_payload.guild_id == 69240 + assert base_channel_payload.position == 7 + assert base_channel_payload.permission_overwrites == {64: mock_overwrite} + assert base_channel_payload.name == "Name" + assert base_channel_payload.topic == "Topically drunk" + assert base_channel_payload.is_nsfw is True + assert base_channel_payload.last_message_id == 22222222 + assert base_channel_payload.bitrate == 96000 + assert base_channel_payload.user_limit == 42 + assert base_channel_payload.rate_limit_per_user == datetime.timedelta(seconds=2333) + assert base_channel_payload.recipients == {42: mock_user} + assert base_channel_payload.icon_hash == "sdodsooioio2oi" + assert base_channel_payload.owner_id == 32939393 + assert base_channel_payload.application_id == 202020202 + assert base_channel_payload.parent_id == 2030302939 + assert base_channel_payload.last_pin_timestamp is mock_timestamp + + +# Doesn't declare any new fields. +class TestChannelCreateEvent: + ... + + +# Doesn't declare any new fields. +class TestChannelUpdateEvent: + ... + + +# Doesn't declare any new fields. +class TestChannelDeleteEvent: + ... + + +class TestChannelPinUpdateEvent: + @pytest.fixture() + def test_chanel_pin_update_payload(self): + return { + "guild_id": "424242", + "channel_id": "29292929", + "last_pin_timestamp": "2020-03-20T16:08:25.412000+00:00", + } + + def test_deserialize(self, test_chanel_pin_update_payload): + mock_timestamp = mock.MagicMock(datetime.datetime) + with _helpers.patch_marshal_attr( + events.ChannelPinUpdateEvent, + "last_pin_timestamp", + deserializer=dates.parse_iso_8601_ts, + return_value=mock_timestamp, + ) as patched_iso_parser: + channel_pin_add_obj = events.ChannelPinUpdateEvent.deserialize(test_chanel_pin_update_payload) + patched_iso_parser.assert_called_once_with("2020-03-20T16:08:25.412000+00:00") + assert channel_pin_add_obj.guild_id == 424242 + assert channel_pin_add_obj.channel_id == 29292929 + assert channel_pin_add_obj.last_pin_timestamp is mock_timestamp + + +# Doesn't declare any new fields. +class TestGuildCreateEvent: + ... + + +# Doesn't declare any new fields. +class TestGuildUpdateEvent: + ... + + +# Doesn't declare any new fields. +class GuildLeaveEvent: + ... + + +# Doesn't declare any new fields. +class GuildUnavailableEvent: + ... + + +class TestBaseGuildBanEvent: + @pytest.fixture() + def test_guild_ban_payload(self, test_user_payload): + return {"user": test_user_payload, "guild_id": "5959"} + + def test_deserialize(self, test_guild_ban_payload, test_user_payload): + mock_user = mock.MagicMock(users.User) + with _helpers.patch_marshal_attr( + events.BaseGuildBanEvent, "user", deserializer=users.User.deserialize, return_value=mock_user + ) as patched_user_deserializer: + base_guild_ban_object = events.BaseGuildBanEvent.deserialize(test_guild_ban_payload) + patched_user_deserializer.assert_called_once_with(test_user_payload) + assert base_guild_ban_object.user is mock_user + assert base_guild_ban_object.guild_id == 5959 + + +# Doesn't declare any new fields. +class TestGuildBanAddEvent: + ... + + +# Doesn't declare any new fields. +class TestGuildBanRemoveEvent: + ... + + +class TestGuildEmojisUpdateEvent: + @pytest.fixture() + def test_guild_emojis_update_payload(self, test_emoji_payload): + return {"emojis": [test_emoji_payload], "guild_id": "696969"} + + def test_deserialize(self, test_guild_emojis_update_payload, test_emoji_payload): + mock_emoji = _helpers.mock_model(emojis.GuildEmoji, id=240) + with mock.patch.object(emojis.GuildEmoji, "deserialize", return_value=mock_emoji): + guild_emojis_update_obj = events.GuildEmojisUpdateEvent.deserialize(test_guild_emojis_update_payload) + emojis.GuildEmoji.deserialize.assert_called_once_with(test_emoji_payload) + assert guild_emojis_update_obj.emojis == {mock_emoji.id: mock_emoji} + assert guild_emojis_update_obj.guild_id == 696969 + + +class TestGuildIntegrationsUpdateEvent: + def test_deserialize(self): + assert events.GuildIntegrationsUpdateEvent.deserialize({"guild_id": "1234"}).guild_id == 1234 + + +class TestGuildMemberAddEvent: + @pytest.fixture() + def test_guild_member_add_payload(self, test_member_payload): + return {**test_member_payload, "guild_id": "292929"} + + def test_deserialize(self, test_guild_member_add_payload): + guild_member_add_obj = events.GuildMemberAddEvent.deserialize(test_guild_member_add_payload) + assert guild_member_add_obj.guild_id == 292929 + + +class TestGuildMemberRemoveEvent: + @pytest.fixture() + def test_guild_member_remove_payload(self, test_user_payload): + return {"guild_id": "9494949", "user": test_user_payload} + + def test_deserialize(self, test_guild_member_remove_payload, test_user_payload): + mock_user = mock.MagicMock(users.User) + with _helpers.patch_marshal_attr( + events.GuildMemberRemoveEvent, "user", deserializer=users.User.deserialize, return_value=mock_user + ) as patched_user_deseializer: + guild_member_remove_payload = events.GuildMemberRemoveEvent.deserialize(test_guild_member_remove_payload) + patched_user_deseializer.assert_called_once_with(test_user_payload) + assert guild_member_remove_payload.guild_id == 9494949 + assert guild_member_remove_payload.user is mock_user + + +class TestGuildMemberUpdateEvent: + @pytest.fixture() + def guild_member_update_payload(self, test_user_payload): + return { + "guild_id": "292929", + "roles": ["213", "412"], + "user": test_user_payload, + "nick": "konnichiwa", + "premium_since": "2019-05-17T06:26:56.936000+00:00", + } + + def test_deserialize(self, guild_member_update_payload, test_user_payload): + mock_user = mock.MagicMock(users.User) + mock_premium_since = mock.MagicMock(datetime.datetime) + with _helpers.patch_marshal_attr( + events.GuildMemberUpdateEvent, "user", deserializer=users.User.deserialize, return_value=mock_user + ) as patched_user_deserializer: + with _helpers.patch_marshal_attr( + events.GuildMemberUpdateEvent, + "premium_since", + deserializer=dates.parse_iso_8601_ts, + return_value=mock_premium_since, + ) as patched_premium_since_deserializer: + guild_member_update_obj = events.GuildMemberUpdateEvent.deserialize(guild_member_update_payload) + patched_premium_since_deserializer.assert_called_once_with("2019-05-17T06:26:56.936000+00:00") + patched_user_deserializer.assert_called_once_with(test_user_payload) + assert guild_member_update_obj.guild_id == 292929 + assert guild_member_update_obj.role_ids == [213, 412] + assert guild_member_update_obj.user is mock_user + assert guild_member_update_obj.nickname == "konnichiwa" + assert guild_member_update_obj.premium_since is mock_premium_since + + def test_partial_deserializer(self, guild_member_update_payload): + del guild_member_update_payload["nick"] + del guild_member_update_payload["premium_since"] + with _helpers.patch_marshal_attr(events.GuildMemberUpdateEvent, "user", deserializer=users.User.deserialize): + guild_member_update_obj = events.GuildMemberUpdateEvent.deserialize(guild_member_update_payload) + assert guild_member_update_obj.nickname is entities.UNSET + assert guild_member_update_obj.premium_since is entities.UNSET + + +@pytest.fixture() +def test_guild_role_create_update_payload(test_guild_payload): + return {"guild_id": "69240", "role": test_guild_payload} + + +class TestGuildRoleCreateEvent: + def test_deserialize(self, test_guild_role_create_update_payload, test_guild_payload): + mock_role = mock.MagicMock(guilds.GuildRole) + with _helpers.patch_marshal_attr( + events.GuildRoleCreateEvent, "role", deserializer=guilds.GuildRole.deserialize, return_value=mock_role + ) as patched_role_deserializer: + guild_role_create_obj = events.GuildRoleCreateEvent.deserialize(test_guild_role_create_update_payload) + patched_role_deserializer.assert_called_once_with(test_guild_payload) + assert guild_role_create_obj.role is mock_role + assert guild_role_create_obj.guild_id == 69240 + + +class TestGuildRoleUpdateEvent: + @pytest.fixture() + def test_guild_role_create_fixture(self, test_guild_payload): + return {"guild_id": "69240", "role": test_guild_payload} + + def test_deserialize(self, test_guild_role_create_update_payload, test_guild_payload): + mock_role = mock.MagicMock(guilds.GuildRole) + with _helpers.patch_marshal_attr( + events.GuildRoleUpdateEvent, "role", deserializer=guilds.GuildRole.deserialize, return_value=mock_role + ) as patched_role_deserializer: + guild_role_create_obj = events.GuildRoleUpdateEvent.deserialize(test_guild_role_create_update_payload) + patched_role_deserializer.assert_called_once_with(test_guild_payload) + assert guild_role_create_obj.role is mock_role + assert guild_role_create_obj.guild_id == 69240 + + +class TestGuildRoleDeleteEvent: + @pytest.fixture() + def test_guild_role_delete_payload(self): + return {"guild_id": "424242", "role_id": "94595959"} + + def test_deserialize(self, test_guild_role_delete_payload): + guild_role_delete_payload = events.GuildRoleDeleteEvent.deserialize(test_guild_role_delete_payload) + assert guild_role_delete_payload.guild_id == 424242 + assert guild_role_delete_payload.role_id == 94595959 + + +class TestInviteCreateEvent: + @pytest.fixture() + def test_invite_create_payload(self, test_user_payload): + return { + "channel_id": "939393", + "code": "owouwuowouwu", + "created_at": "2019-05-17T06:26:56.936000+00:00", + "guild_id": "45949", + "inviter": test_user_payload, + "max_age": 42, + "max_uses": 69, + "target_user": {"id": "420", "username": "blah", "discriminator": "4242", "avatar": "ha"}, + "target_user_type": 1, + "temporary": True, + "uses": 42, + } + + def test_deserialize(self, test_invite_create_payload, test_user_payload): + mock_inviter = mock.MagicMock(users.User) + mock_target = mock.MagicMock(users.User) + mock_created_at = mock.MagicMock(datetime.datetime) + with _helpers.patch_marshal_attr( + events.InviteCreateEvent, "inviter", deserializer=users.User.deserialize, return_value=mock_inviter + ) as patched_inviter_deserializer: + with _helpers.patch_marshal_attr( + events.InviteCreateEvent, "target_user", deserializer=users.User.deserialize, return_value=mock_target + ) as patched_target_deserializer: + with _helpers.patch_marshal_attr( + events.InviteCreateEvent, + "created_at", + deserializer=dates.parse_iso_8601_ts, + return_value=mock_created_at, + ) as patched_created_at_deserializer: + invite_create_obj = events.InviteCreateEvent.deserialize(test_invite_create_payload) + patched_created_at_deserializer.assert_called_once_with("2019-05-17T06:26:56.936000+00:00") + patched_target_deserializer.assert_called_once_with( + {"id": "420", "username": "blah", "discriminator": "4242", "avatar": "ha"} + ) + patched_inviter_deserializer.assert_called_once_with(test_user_payload) + assert invite_create_obj.channel_id == 939393 + assert invite_create_obj.code == "owouwuowouwu" + assert invite_create_obj.created_at is mock_created_at + assert invite_create_obj.guild_id == 45949 + assert invite_create_obj.inviter is mock_inviter + assert invite_create_obj.max_age == datetime.timedelta(seconds=42) + assert invite_create_obj.max_uses == 69 + assert invite_create_obj.target_user is mock_target + assert invite_create_obj.target_user_type is invites.TargetUserType.STREAM + assert invite_create_obj.is_temporary is True + assert invite_create_obj.uses == 42 + + def test_max_age_when_zero(self, test_invite_create_payload): + test_invite_create_payload["max_age"] = 0 + assert events.InviteCreateEvent.deserialize(test_invite_create_payload).max_age is None + + +class TestInviteDeleteEvent: + @pytest.fixture() + def test_invite_delete_payload(self): + return {"channel_id": "393939", "code": "blahblahblah", "guild_id": "3834833"} + + def test_deserialize(self, test_invite_delete_payload): + invite_delete_obj = events.InviteDeleteEvent.deserialize(test_invite_delete_payload) + assert invite_delete_obj.channel_id == 393939 + assert invite_delete_obj.code == "blahblahblah" + assert invite_delete_obj.guild_id == 3834833 + + +# Doesn't declare any new fields. +class TestMessageCreateEvent: + ... + + +class TestMessageUpdateEvent: + @pytest.fixture() + def test_attachment_payload(self): + return { + "id": "4242", + "filename": "nyaa.png", + "size": 1024, + "url": "heck.heck", + "proxy_url": "proxy.proxy?heck", + "height": 42, + "width": 84, + } + + @pytest.fixture() + def test_embed_payload(self): + return {"title": "42", "description": "blah blah blah"} + + @pytest.fixture() + def test_reaction_payload(self): + return {"count": 69, "me": True, "emoji": "🤣"} + + @pytest.fixture() + def test_activity_payload(self): + return {"type": 1, "party_id": "spotify:23123123"} + + @pytest.fixture() + def test_application_payload(self): + return {"id": "292929", "icon": None, "description": "descript", "name": "A name"} + + @pytest.fixture() + def test_reference_payload(self): + return {"channel_id": "432341231231"} + + @pytest.fixture() + def test_message_update_payload( + self, + test_user_payload, + test_member_payload, + test_attachment_payload, + test_embed_payload, + test_reaction_payload, + test_activity_payload, + test_application_payload, + test_reference_payload, + test_channel_payload, + ): + return { + "id": "3939399393", + "channel_id": "93939393939", + "guild_id": "66557744883399", + "author": test_user_payload, + "member": test_member_payload, + "content": "THIS IS A CONTENT", + "timestamp": "2019-05-17T06:26:56.936000+00:00", + "edited_timestamp": "2019-05-17T06:58:56.936000+00:00", + "tts": True, + "mention_everyone": True, + "mentions": [test_user_payload], + "mention_roles": ["123"], + "mention_channels": [test_channel_payload], + "attachments": [test_attachment_payload], + "embeds": [test_embed_payload], + "reactions": [test_reaction_payload], + "nonce": "6454345345345345", + "pinned": True, + "webhook_id": "212231231232123", + "type": 2, + "activity": test_activity_payload, + "application": test_application_payload, + "message_reference": test_reference_payload, + "flags": 3, + } + + def test_deserialize( + self, + test_message_update_payload, + test_user_payload, + test_member_payload, + test_activity_payload, + test_application_payload, + test_reference_payload, + test_attachment_payload, + test_embed_payload, + test_reaction_payload, + ): + mock_author = mock.MagicMock(users.User) + mock_member = mock.MagicMock(guilds.GuildMember) + mock_timestamp = mock.MagicMock(datetime.datetime) + mock_edited_timestamp = mock.MagicMock(datetime.datetime) + mock_attachment = mock.MagicMock(messages.Attachment) + mock_embed = mock.MagicMock(embeds.Embed) + mock_reaction = mock.MagicMock(messages.Reaction) + mock_activity = mock.MagicMock(messages.MessageActivity) + mock_application = mock.MagicMock(oauth2.Application) + mock_reference = mock.MagicMock(messages.MessageCrosspost) + with _helpers.patch_marshal_attr( + events.MessageUpdateEvent, "author", deserializer=users.User.deserialize, return_value=mock_author + ) as patched_author_deserializer: + with _helpers.patch_marshal_attr( + events.MessageUpdateEvent, + "member", + deserializer=guilds.GuildMember.deserialize, + return_value=mock_member, + ) as patched_member_deserializer: + with _helpers.patch_marshal_attr( + events.MessageUpdateEvent, + "timestamp", + deserializer=dates.parse_iso_8601_ts, + return_value=mock_timestamp, + ) as patched_timestamp_deserializer: + with _helpers.patch_marshal_attr( + events.MessageUpdateEvent, + "edited_timestamp", + deserializer=dates.parse_iso_8601_ts, + return_value=mock_edited_timestamp, + ) as patched_edit_deserializer: + with _helpers.patch_marshal_attr( + events.MessageUpdateEvent, + "activity", + deserializer=messages.MessageActivity.deserialize, + return_value=mock_activity, + ) as patched_activity_deserializer: + with _helpers.patch_marshal_attr( + events.MessageUpdateEvent, + "application", + deserializer=oauth2.Application.deserialize, + return_value=mock_application, + ) as patched_application_deserializer: + with _helpers.patch_marshal_attr( + events.MessageUpdateEvent, + "message_reference", + deserializer=messages.MessageCrosspost.deserialize, + return_value=mock_reference, + ) as patched_reference_deserializer: + with mock.patch.object( + messages.Attachment, "deserialize", return_value=mock_attachment + ): + with mock.patch.object(embeds.Embed, "deserialize", return_value=mock_embed): + with mock.patch.object( + messages.Reaction, "deserialize", return_value=mock_reaction + ): + message_update_payload = events.MessageUpdateEvent.deserialize( + test_message_update_payload + ) + messages.Reaction.deserialize.assert_called_once_with( + test_reaction_payload + ) + embeds.Embed.deserialize.assert_called_once_with(test_embed_payload) + messages.Attachment.deserialize.assert_called_once_with(test_attachment_payload) + patched_reference_deserializer.assert_called_once_with(test_reference_payload) + patched_application_deserializer.assert_called_once_with(test_application_payload) + patched_activity_deserializer.assert_called_once_with(test_activity_payload) + patched_edit_deserializer.assert_called_once_with("2019-05-17T06:58:56.936000+00:00") + patched_timestamp_deserializer.assert_called_once_with("2019-05-17T06:26:56.936000+00:00") + patched_member_deserializer.assert_called_once_with(test_member_payload) + patched_author_deserializer.assert_called_once_with(test_user_payload) + assert message_update_payload.channel_id == 93939393939 + assert message_update_payload.guild_id == 66557744883399 + assert message_update_payload.author is mock_author + assert message_update_payload.member is mock_member + assert message_update_payload.content == "THIS IS A CONTENT" + assert message_update_payload.timestamp is mock_timestamp + assert message_update_payload.edited_timestamp is mock_edited_timestamp + assert message_update_payload.is_tts is True + assert message_update_payload.is_mentioning_everyone is True + assert message_update_payload.user_mentions == {2929292} + assert message_update_payload.role_mentions == {123} + assert message_update_payload.channel_mentions == {393939} + assert message_update_payload.attachments == [mock_attachment] + assert message_update_payload.embeds == [mock_embed] + assert message_update_payload.reactions == [mock_reaction] + assert message_update_payload.is_pinned is True + assert message_update_payload.webhook_id == 212231231232123 + assert message_update_payload.type is messages.MessageType.RECIPIENT_REMOVE + assert message_update_payload.activity is mock_activity + assert message_update_payload.application is mock_application + assert message_update_payload.message_reference is mock_reference + assert message_update_payload.flags == messages.MessageFlag.CROSSPOSTED | messages.MessageFlag.IS_CROSSPOST + + def test_partial_message_update(self): + message_update_obj = events.MessageUpdateEvent.deserialize({"id": "393939", "channel_id": "434949"}) + for key in message_update_obj.__slots__: + if key in ("id", "channel_id"): + continue + assert getattr(message_update_obj, key) is entities.UNSET + assert message_update_obj.id == 393939 + assert message_update_obj.channel_id == 434949 + + +class TestMessageDeleteEvent: + @pytest.fixture() + def test_message_delete_payload(self): + return {"channel_id": "20202020", "id": "2929", "guild_id": "1010101"} + + def test_deserialize(self, test_message_delete_payload): + message_delete_obj = events.MessageDeleteEvent.deserialize(test_message_delete_payload) + assert message_delete_obj.channel_id == 20202020 + assert message_delete_obj.message_id == 2929 + assert message_delete_obj.guild_id == 1010101 + + +class TestMessageDeleteBulkEvent: + @pytest.fixture() + def test_message_delete_bulk_payload(self): + return {"channel_id": "20202020", "ids": ["2929", "4394"], "guild_id": "1010101"} + + def test_deserialize(self, test_message_delete_bulk_payload): + message_delete_bulk_obj = events.MessageDeleteBulkEvent.deserialize(test_message_delete_bulk_payload) + assert message_delete_bulk_obj.channel_id == 20202020 + assert message_delete_bulk_obj.guild_id == 1010101 + assert message_delete_bulk_obj.message_ids == {2929, 4394} + + +class TestMessageReactionAddEvent: + @pytest.fixture() + def test_message_reaction_add_payload(self, test_member_payload, test_emoji_payload): + return { + "user_id": "9494949", + "channel_id": "4393939", + "message_id": "2993993", + "guild_id": "49494949", + "member": test_member_payload, + "emoji": test_emoji_payload, + } + + def test_deserialize(self, test_message_reaction_add_payload, test_member_payload, test_emoji_payload): + mock_member = mock.MagicMock(guilds.GuildMember) + mock_emoji = mock.MagicMock(emojis.UnknownEmoji) + with _helpers.patch_marshal_attr( + events.MessageReactionAddEvent, + "member", + deserializer=guilds.GuildMember.deserialize, + return_value=mock_member, + ) as patched_member_deserializer: + with _helpers.patch_marshal_attr( + events.MessageReactionAddEvent, + "emoji", + deserializer=emojis.deserialize_reaction_emoji, + return_value=mock_emoji, + ) as patched_emoji_deserializer: + message_reaction_add_obj = events.MessageReactionAddEvent.deserialize(test_message_reaction_add_payload) + patched_emoji_deserializer.assert_called_once_with(test_emoji_payload) + patched_member_deserializer.assert_called_once_with(test_member_payload) + assert message_reaction_add_obj.user_id == 9494949 + assert message_reaction_add_obj.channel_id == 4393939 + assert message_reaction_add_obj.message_id == 2993993 + assert message_reaction_add_obj.guild_id == 49494949 + assert message_reaction_add_obj.member is mock_member + assert message_reaction_add_obj.emoji is mock_emoji + + +class TestMessageReactionRemoveEvent: + @pytest.fixture() + def test_message_reaction_remove_payload(self, test_emoji_payload): + return { + "user_id": "9494949", + "channel_id": "4393939", + "message_id": "2993993", + "guild_id": "49494949", + "emoji": test_emoji_payload, + } + + def test_deserialize(self, test_message_reaction_remove_payload, test_emoji_payload): + mock_emoji = mock.MagicMock(emojis.UnknownEmoji) + with _helpers.patch_marshal_attr( + events.MessageReactionRemoveEvent, + "emoji", + deserializer=emojis.deserialize_reaction_emoji, + return_value=mock_emoji, + ) as patched_emoji_deserializer: + message_reaction_remove_obj = events.MessageReactionRemoveEvent.deserialize( + test_message_reaction_remove_payload + ) + patched_emoji_deserializer.assert_called_once_with(test_emoji_payload) + assert message_reaction_remove_obj.user_id == 9494949 + assert message_reaction_remove_obj.channel_id == 4393939 + assert message_reaction_remove_obj.message_id == 2993993 + assert message_reaction_remove_obj.guild_id == 49494949 + assert message_reaction_remove_obj.emoji is mock_emoji + + +class TestMessageReactionRemoveAllEvent: + @pytest.fixture() + def test_reaction_remove_all_payload(self): + return {"channel_id": "3493939", "message_id": "944949", "guild_id": "49494949"} + + def test_deserialize(self, test_reaction_remove_all_payload): + message_reaction_remove_all_obj = events.MessageReactionRemoveAllEvent.deserialize( + test_reaction_remove_all_payload + ) + assert message_reaction_remove_all_obj.channel_id == 3493939 + assert message_reaction_remove_all_obj.message_id == 944949 + assert message_reaction_remove_all_obj.guild_id == 49494949 + + +class TestMessageReactionRemoveEmojiEvent: + @pytest.fixture() + def test_message_reaction_remove_emoji_payload(self, test_emoji_payload): + return {"channel_id": "4393939", "message_id": "2993993", "guild_id": "49494949", "emoji": test_emoji_payload} + + def test_deserialize(self, test_message_reaction_remove_emoji_payload, test_emoji_payload): + mock_emoji = mock.MagicMock(emojis.UnknownEmoji) + with _helpers.patch_marshal_attr( + events.MessageReactionRemoveEmojiEvent, + "emoji", + deserializer=emojis.deserialize_reaction_emoji, + return_value=mock_emoji, + ) as patched_emoji_deserializer: + message_reaction_remove_emoji_obj = events.MessageReactionRemoveEmojiEvent.deserialize( + test_message_reaction_remove_emoji_payload + ) + patched_emoji_deserializer.assert_called_once_with(test_emoji_payload) + assert message_reaction_remove_emoji_obj.channel_id == 4393939 + assert message_reaction_remove_emoji_obj.message_id == 2993993 + assert message_reaction_remove_emoji_obj.guild_id == 49494949 + assert message_reaction_remove_emoji_obj.emoji is mock_emoji + + +# Doesn't declare any new fields. +class TestPresenceUpdateEvent: + ... + + +class TestTypingStartEvent: + @pytest.fixture() + def test_typing_start_event_payload(self, test_member_payload): + return { + "channel_id": "123123123", + "guild_id": "33333333", + "user_id": "2020202", + "timestamp": 1231231231, + "member": test_member_payload, + } + + def test_deserialize(self, test_typing_start_event_payload, test_member_payload): + mock_member = mock.MagicMock(guilds.GuildMember) + mock_datetime = mock.MagicMock(datetime.datetime) + with _helpers.patch_marshal_attr( + events.TypingStartEvent, "member", deserializer=guilds.GuildMember.deserialize, return_value=mock_member + ) as mock_member_deserialize: + with mock.patch.object(datetime, "datetime", fromtimestamp=mock.MagicMock(return_value=mock_datetime)): + typing_start_event_obj = events.TypingStartEvent.deserialize(test_typing_start_event_payload) + datetime.datetime.fromtimestamp.assert_called_once_with(1231231231, datetime.timezone.utc) + mock_member_deserialize.assert_called_once_with(test_member_payload) + assert typing_start_event_obj.channel_id == 123123123 + assert typing_start_event_obj.guild_id == 33333333 + assert typing_start_event_obj.user_id == 2020202 + assert typing_start_event_obj.timestamp is mock_datetime + assert typing_start_event_obj.member is mock_member + + +# Doesn't declare any new fields. +class TestUserUpdateEvent: + ... + + +# Doesn't declare any new fields. +class TestVoiceStateUpdateEvent: + ... + + +class TestVoiceServerUpdateEvent: + @pytest.fixture() + def test_voice_server_update_payload(self): + return {"token": "a_token", "guild_id": "303030300303", "endpoint": "smart.loyal.discord.gg"} + + def test_deserialize(self, test_voice_server_update_payload): + voice_server_update_obj = events.VoiceServerUpdateEvent.deserialize(test_voice_server_update_payload) + assert voice_server_update_obj.token == "a_token" + assert voice_server_update_obj.guild_id == 303030300303 + assert voice_server_update_obj.endpoint == "smart.loyal.discord.gg" + + +class TestWebhookUpdate: + @pytest.fixture() + def test_webhook_update_payload(self): + return {"guild_id": "2929292", "channel_id": "94949494"} + + def test_deserialize(self, test_webhook_update_payload): + webhook_update_obj = events.WebhookUpdate.deserialize(test_webhook_update_payload) + assert webhook_update_obj.guild_id == 2929292 + assert webhook_update_obj.channel_id == 94949494 diff --git a/tests/hikari/core/test_guilds.py b/tests/hikari/core/test_guilds.py index b867870921..448bcd4826 100644 --- a/tests/hikari/core/test_guilds.py +++ b/tests/hikari/core/test_guilds.py @@ -16,11 +16,19 @@ # # You should have received a copy of the GNU Lesser General Public License # along ith Hikari. If not, see . +import datetime + import cymock as mock import pytest +from hikari.core import emojis +from hikari.core import entities from hikari.core import guilds +from hikari.core import users from hikari.internal_utilities import cdn +from hikari.internal_utilities import dates + +from tests.hikari import _helpers @pytest.fixture @@ -105,7 +113,21 @@ def test_channel_payloads(): @pytest.fixture -def test_member_payload(): +def test_user_payload(): + return { + "id": "123456", + "username": "Boris Johnson", + "discriminator": "6969", + "avatar": "1a2b3c4d", + "mfa_enabled": True, + "locale": "gb", + "flags": 0b00101101, + "premium_type": 0b1101101, + } + + +@pytest.fixture +def test_member_payload(test_user_payload): return { "nick": "foobarbaz", "roles": ["11111", "22222", "33333", "44444"], @@ -114,16 +136,7 @@ def test_member_payload(): # These should be completely ignored. "deaf": False, "mute": True, - "user": { - "id": "123456", - "username": "Boris Johnson", - "discriminator": "6969", - "avatar": "1a2b3c4d", - "mfa_enabled": True, - "locale": "gb", - "flags": 0b00101101, - "premium_type": 0b1101101, - }, + "user": test_user_payload, } @@ -194,6 +207,265 @@ def test_guild_payload( } +@pytest.fixture() +def test_activity_party_payload(): + return {"id": "spotify:3234234234", "size": [2, 5]} + + +@pytest.fixture() +def test_activity_timestamps_payload(): + return { + "start": 1584996792798, + "end": 1999999792798, + } + + +@pytest.fixture() +def test_activity_assets_payload(): + return { + "large_image": "34234234234243", + "large_text": "LARGE TEXT", + "small_image": "3939393", + "small_text": "small text", + } + + +@pytest.fixture() +def test_activity_secrets_payload(): + return {"join": "who's a good secret?", "spectate": "I'm a good secret", "match": "No."} + + +@pytest.fixture() +def test_presence_activity_payload( + test_activity_timestamps_payload, + test_emoji_payload, + test_activity_party_payload, + test_activity_assets_payload, + test_activity_secrets_payload, +): + return { + "name": "an activity", + "type": 1, + "url": "https://69.420.owouwunyaa", + "created_at": 1584996792798, + "timestamps": test_activity_timestamps_payload, + "application_id": "40404040404040", + "details": "They are doing stuff", + "state": "STATED", + "emoji": test_emoji_payload, + "party": test_activity_party_payload, + "assets": test_activity_assets_payload, + "secrets": test_activity_secrets_payload, + "instance": True, + "flags": 3, + } + + +class TestGuildMember: + def test_deserialize(self, test_member_payload, test_user_payload): + mock_user = mock.MagicMock(users.User) + mock_datetime_1 = mock.MagicMock(dates) + mock_datetime_2 = mock.MagicMock(dates) + with _helpers.patch_marshal_attr( + guilds.GuildMember, "user", deserializer=users.User.deserialize, return_value=mock_user + ) as patched_user_deserializer: + with _helpers.patch_marshal_attr( + guilds.GuildMember, "joined_at", deserializer=dates.parse_iso_8601_ts, return_value=mock_datetime_1 + ) as patched_joined_at_deserializer: + with _helpers.patch_marshal_attr( + guilds.GuildMember, + "premium_since", + deserializer=dates.parse_iso_8601_ts, + return_value=mock_datetime_2, + ) as patched_premium_since_deserializer: + guild_member_obj = guilds.GuildMember.deserialize(test_member_payload) + patched_premium_since_deserializer.assert_called_once_with("2019-05-17T06:26:56.936000+00:00") + patched_joined_at_deserializer.assert_called_once_with("2015-04-26T06:26:56.936000+00:00") + patched_user_deserializer.assert_called_once_with(test_user_payload) + assert guild_member_obj.user is mock_user + assert guild_member_obj.nickname == "foobarbaz" + assert guild_member_obj.role_ids == [11111, 22222, 33333, 44444] + assert guild_member_obj.joined_at is mock_datetime_1 + assert guild_member_obj.premium_since is mock_datetime_2 + assert guild_member_obj.is_deaf is False + assert guild_member_obj.is_mute is True + + +class TestActivityTimestamps: + def test_deserialize(self, test_activity_timestamps_payload): + mock_start_date = mock.MagicMock(datetime.datetime) + mock_end_date = mock.MagicMock(datetime.datetime) + with _helpers.patch_marshal_attr( + guilds.ActivityTimestamps, "start", deserializer=dates.unix_epoch_to_ts, return_value=mock_start_date + ) as patched_start_deserializer: + with _helpers.patch_marshal_attr( + guilds.ActivityTimestamps, "end", deserializer=dates.unix_epoch_to_ts, return_value=mock_end_date + ) as patched_end_deserializer: + activity_timestamps_obj = guilds.ActivityTimestamps.deserialize(test_activity_timestamps_payload) + patched_end_deserializer.assert_called_once_with(1999999792798) + patched_start_deserializer.assert_called_once_with(1584996792798) + assert activity_timestamps_obj.start is mock_start_date + assert activity_timestamps_obj.end is mock_end_date + + +class TestActivityParty: + @pytest.fixture() + def test_activity_party_obj(self, test_activity_party_payload): + return guilds.ActivityParty.deserialize(test_activity_party_payload) + + def test_deserialize(self, test_activity_party_obj): + assert test_activity_party_obj.id == "spotify:3234234234" + assert test_activity_party_obj._size_information == (2, 5) + + def test_current_size(self, test_activity_party_obj): + assert test_activity_party_obj.current_size == 2 + + def test_current_size_when_null(self, test_activity_party_obj): + test_activity_party_obj._size_information = None + assert test_activity_party_obj.current_size is None + + def test_max_size(self, test_activity_party_obj): + assert test_activity_party_obj.max_size == 5 + + def test_max_size_when_null(self, test_activity_party_obj): + test_activity_party_obj._size_information = None + assert test_activity_party_obj.max_size is None + + +class TestActivityAssets: + def test_deserialize(self, test_activity_assets_payload): + activity_assets_obj = guilds.ActivityAssets.deserialize(test_activity_assets_payload) + assert activity_assets_obj.large_image == "34234234234243" + assert activity_assets_obj.large_text == "LARGE TEXT" + assert activity_assets_obj.small_image == "3939393" + assert activity_assets_obj.small_text == "small text" + + +class TestActivitySecret: + def test_deserialize(self, test_activity_secrets_payload): + activity_secret_obj = guilds.ActivitySecret.deserialize(test_activity_secrets_payload) + assert activity_secret_obj.join == "who's a good secret?" + assert activity_secret_obj.spectate == "I'm a good secret" + assert activity_secret_obj.match == "No." + + +class TestPresenceActivity: + def test_deserialize( + self, + test_presence_activity_payload, + test_activity_secrets_payload, + test_activity_assets_payload, + test_activity_party_payload, + test_emoji_payload, + test_activity_timestamps_payload, + ): + mock_created_at = mock.MagicMock(datetime.datetime) + mock_emoji = mock.MagicMock(emojis.UnknownEmoji) + with _helpers.patch_marshal_attr( + guilds.PresenceActivity, "created_at", deserializer=dates.unix_epoch_to_ts, return_value=mock_created_at + ) as patched_created_at_deserializer: + with _helpers.patch_marshal_attr( + guilds.PresenceActivity, + "emoji", + deserializer=emojis.deserialize_reaction_emoji, + return_value=mock_emoji, + ) as patched_emoji_deserializer: + presence_activity_obj = guilds.PresenceActivity.deserialize(test_presence_activity_payload) + patched_emoji_deserializer.assert_called_once_with(test_emoji_payload) + patched_created_at_deserializer.assert_called_once_with(1584996792798) + assert presence_activity_obj.name == "an activity" + assert presence_activity_obj.type is guilds.ActivityType.STREAMING + assert presence_activity_obj.url == "https://69.420.owouwunyaa" + assert presence_activity_obj.created_at is mock_created_at + assert presence_activity_obj.timestamps == guilds.ActivityTimestamps.deserialize( + test_activity_timestamps_payload + ) + assert presence_activity_obj.application_id == 40404040404040 + assert presence_activity_obj.details == "They are doing stuff" + assert presence_activity_obj.state == "STATED" + assert presence_activity_obj.emoji is mock_emoji + assert presence_activity_obj.party == guilds.ActivityParty.deserialize(test_activity_party_payload) + assert presence_activity_obj.assets == guilds.ActivityAssets.deserialize(test_activity_assets_payload) + assert presence_activity_obj.secrets == guilds.ActivitySecret.deserialize(test_activity_secrets_payload) + assert presence_activity_obj.is_instance is True + assert presence_activity_obj.flags == guilds.ActivityFlag.INSTANCE | guilds.ActivityFlag.JOIN + + +@pytest.fixture() +def test_client_status_payload(): + return {"desktop": "online", "mobile": "idle"} + + +class TestClientStatus: + def test_deserialize(self, test_client_status_payload): + client_status_obj = guilds.ClientStatus.deserialize(test_client_status_payload) + assert client_status_obj.desktop is guilds.PresenceStatus.ONLINE + assert client_status_obj.mobile is guilds.PresenceStatus.IDLE + assert client_status_obj.web is guilds.PresenceStatus.OFFLINE + + +class TestPresenceUser: + def test_deserialize_filled_presence_user(self, test_user_payload): + presence_user_obj = guilds.PresenceUser.deserialize(test_user_payload) + assert presence_user_obj.username == "Boris Johnson" + assert presence_user_obj.discriminator == "6969" + assert presence_user_obj.avatar_hash == "1a2b3c4d" + + def test_deserialize_partial_presence_user(self): + presence_user_obj = guilds.PresenceUser.deserialize({"id": "115590097100865541"}) + assert presence_user_obj.id == 115590097100865541 + for attr in presence_user_obj.__slots__: + if attr != "id": + assert getattr(presence_user_obj, attr) is entities.UNSET + + +@pytest.fixture() +def test_guild_member_presence(test_user_payload, test_presence_activity_payload, test_client_status_payload): + return { + "user": test_user_payload, + "roles": ["49494949"], + "game": test_presence_activity_payload, + "guild_id": "44004040", + "status": "dnd", + "activities": [test_presence_activity_payload], + "client_status": test_client_status_payload, + "premium_since": "2015-04-26T06:26:56.936000+00:00", + "nick": "Nick", + } + + +class TestGuildMemberPresence: + def test_deserialize( + self, test_guild_member_presence, test_user_payload, test_presence_activity_payload, test_client_status_payload + ): + mock_since = mock.MagicMock(datetime.datetime) + with _helpers.patch_marshal_attr( + guilds.GuildMemberPresence, "premium_since", deserializer=dates.parse_iso_8601_ts, return_value=mock_since, + ) as patched_since_deserializer: + guild_member_presence_obj = guilds.GuildMemberPresence.deserialize(test_guild_member_presence) + patched_since_deserializer.assert_called_once_with("2015-04-26T06:26:56.936000+00:00") + assert guild_member_presence_obj.user == guilds.PresenceUser.deserialize(test_user_payload) + assert guild_member_presence_obj.role_ids == [49494949] + assert guild_member_presence_obj.guild_id == 44004040 + assert guild_member_presence_obj.visible_status is guilds.PresenceStatus.DND + assert guild_member_presence_obj.activities == [ + guilds.PresenceActivity.deserialize(test_presence_activity_payload) + ] + assert guild_member_presence_obj.client_status == guilds.ClientStatus.deserialize(test_client_status_payload) + assert guild_member_presence_obj.premium_since is mock_since + assert guild_member_presence_obj.nick == "Nick" + + +class TestUnavailableGuild: + def test_deserialize_when_unavailable_is_defined(self): + guild_delete_event_obj = guilds.UnavailableGuild.deserialize({"id": "293293939", "unavailable": True}) + assert guild_delete_event_obj.is_unavailable is True + + def test_deserialize_when_unavailable_is_undefined(self): + guild_delete_event_obj = guilds.UnavailableGuild.deserialize({"id": "293293939"}) + assert guild_delete_event_obj.is_unavailable is True + + class TestPartialGuild: @pytest.fixture() def partial_guild_obj(self, test_partial_guild_payload): diff --git a/tests/hikari/core/test_invites.py b/tests/hikari/core/test_invites.py index bd6e85e1b3..e4428dc862 100644 --- a/tests/hikari/core/test_invites.py +++ b/tests/hikari/core/test_invites.py @@ -222,10 +222,14 @@ def test_deserialize(self, *deserializers, test_invite_with_metadata_payload): mock_created_at_deserializer.assert_called_once_with("2015-04-26T06:26:56.936000+00:00") assert invite_with_metadata_obj.uses == 3 assert invite_with_metadata_obj.max_uses == 8 - assert invite_with_metadata_obj.max_age == 239349393 + assert invite_with_metadata_obj.max_age == datetime.timedelta(seconds=239349393) assert invite_with_metadata_obj.is_temporary is True assert invite_with_metadata_obj.created_at is mock_datetime + def test_max_age_when_zero(self, test_invite_with_metadata_payload): + test_invite_with_metadata_payload["max_age"] = 0 + assert invites.InviteWithMetadata.deserialize(test_invite_with_metadata_payload).max_age is None + def test_expires_at(self, mock_invite_with_metadata): assert mock_invite_with_metadata.expires_at == datetime.datetime.fromisoformat( "2022-11-25 12:23:29.936000+00:00" diff --git a/tests/hikari/core/test_voices.py b/tests/hikari/core/test_voices.py new file mode 100644 index 0000000000..acc565559a --- /dev/null +++ b/tests/hikari/core/test_voices.py @@ -0,0 +1,66 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# Copyright © Nekoka.tt 2019-2020 +# +# This file is part of Hikari. +# +# Hikari is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Hikari is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along ith Hikari. If not, see . +import pytest + +from hikari.core import voices + + +@pytest.fixture() +def voice_state_payload(): + return { + "guild_id": "929292929292992", + "channel_id": "157733188964188161", + "user_id": "80351110224678912", + "session_id": "90326bd25d71d39b9ef95b299e3872ff", + "deaf": True, + "mute": True, + "self_deaf": False, + "self_mute": True, + "suppress": False, + } + + +@pytest.fixture() +def voice_region_payload(): + return {"id": "london", "name": "LONDON", "vip": True, "optimal": False, "deprecated": True, "custom": False} + + +class TestVoiceState: + def test_deserialize(self, voice_state_payload): + voice_state_obj = voices.VoiceState.deserialize(voice_state_payload) + assert voice_state_obj.guild_id == 929292929292992 + assert voice_state_obj.channel_id == 157733188964188161 + assert voice_state_obj.user_id == 80351110224678912 + assert voice_state_obj.session_id == "90326bd25d71d39b9ef95b299e3872ff" + assert voice_state_obj.is_guild_deafened is True + assert voice_state_obj.is_guild_muted is True + assert voice_state_obj.is_self_deafened is False + assert voice_state_obj.is_self_muted is True + assert voice_state_obj.is_suppressed is False + + +class TestVoiceRegion: + def test_deserialize(self, voice_region_payload): + voice_region_obj = voices.VoiceRegion.deserialize(voice_region_payload) + assert voice_region_obj.id == "london" + assert voice_region_obj.name == "LONDON" + assert voice_region_obj.is_vip is True + assert voice_region_obj.is_optimal_location is False + assert voice_region_obj.is_deprecated is True + assert voice_region_obj.is_custom is False diff --git a/tests/hikari/internal_utilities/test_marshaller.py b/tests/hikari/internal_utilities/test_marshaller.py index 00a63fe9ff..bc487cec05 100644 --- a/tests/hikari/internal_utilities/test_marshaller.py +++ b/tests/hikari/internal_utilities/test_marshaller.py @@ -67,7 +67,9 @@ def method_stub(value): ... -@pytest.mark.parametrize("data", [lambda x: "ok", None, marshaller.RAISE, dict, method_stub]) +@pytest.mark.parametrize( + "data", [lambda x: "ok", *marshaller.PASSED_THROUGH_SINGLETONS, marshaller.RAISE, dict, method_stub] +) def test_default_validator(data): marshaller._default_validator(data) @@ -123,15 +125,16 @@ class User: assert isinstance(result, User) assert result.id == "12345" - def test_deserialize_not_required_success_if_not_specified(self, marshaller_impl): + @pytest.mark.parametrize("singleton", marshaller.PASSED_THROUGH_SINGLETONS) + def test_deserialize_not_required_success_if_not_specified(self, marshaller_impl, singleton): @marshaller.attrs(marshaller=marshaller_impl) class User: - id: int = marshaller.attrib(if_undefined=None, deserializer=str) + id: int = marshaller.attrib(if_undefined=singleton, deserializer=str) result = marshaller_impl.deserialize({}, User) assert isinstance(result, User) - assert result.id is None + assert result.id is singleton def test_deserialize_calls_if_undefined_if_not_none_and_field_not_present(self, marshaller_impl): mock_result = mock.MagicMock() @@ -165,15 +168,16 @@ class User: assert isinstance(result, User) assert result.id == "12345" - def test_deserialize_nullable_success_if_null(self, marshaller_impl): + @pytest.mark.parametrize("singleton", marshaller.PASSED_THROUGH_SINGLETONS) + def test_deserialize_nullable_success_if_null(self, marshaller_impl, singleton): @marshaller.attrs(marshaller=marshaller_impl) class User: - id: int = marshaller.attrib(if_none=None, deserializer=str) + id: int = marshaller.attrib(if_none=singleton, deserializer=str) result = marshaller_impl.deserialize({"id": None}, User) assert isinstance(result, User) - assert result.id is None + assert result.id is singleton def test_deserialize_calls_if_none_if_not_none_and_data_is_none(self, marshaller_impl): mock_result = mock.MagicMock() diff --git a/tests/hikari/internal_utilities/test_marshaller_pep563.py b/tests/hikari/internal_utilities/test_marshaller_pep563.py index 259cbdaa5e..8b3a1a187a 100644 --- a/tests/hikari/internal_utilities/test_marshaller_pep563.py +++ b/tests/hikari/internal_utilities/test_marshaller_pep563.py @@ -95,7 +95,9 @@ def method_stub(value): ... -@pytest.mark.parametrize("data", [lambda x: "ok", None, marshaller.RAISE, dict, method_stub]) +@pytest.mark.parametrize( + "data", [lambda x: "ok", *marshaller.PASSED_THROUGH_SINGLETONS, marshaller.RAISE, dict, method_stub] +) def test_default_validator(data): marshaller._default_validator(data) @@ -130,15 +132,16 @@ class User: assert isinstance(result, User) assert result.id == "12345" - def test_deserialize_not_required_success_if_not_specified(self, marshaller_impl): + @pytest.mark.parametrize("singleton", marshaller.PASSED_THROUGH_SINGLETONS) + def test_deserialize_not_required_success_if_not_specified(self, marshaller_impl, singleton): @marshaller.attrs(marshaller=marshaller_impl) class User: - id: int = marshaller.attrib(if_undefined=None, deserializer=str) + id: int = marshaller.attrib(if_undefined=singleton, deserializer=str) result = marshaller_impl.deserialize({}, User) assert isinstance(result, User) - assert result.id is None + assert result.id is singleton def test_deserialize_calls_if_undefined_if_not_none_and_field_not_present(self, marshaller_impl): mock_result = mock.MagicMock() @@ -172,15 +175,16 @@ class User: assert isinstance(result, User) assert result.id == "12345" - def test_deserialize_nullable_success_if_null(self, marshaller_impl): + @pytest.mark.parametrize("singleton", marshaller.PASSED_THROUGH_SINGLETONS) + def test_deserialize_nullable_success_if_null(self, marshaller_impl, singleton): @marshaller.attrs(marshaller=marshaller_impl) class User: - id: int = marshaller.attrib(if_none=None, deserializer=str) + id: int = marshaller.attrib(if_none=singleton, deserializer=str) result = marshaller_impl.deserialize({"id": None}, User) assert isinstance(result, User) - assert result.id is None + assert result.id is singleton def test_deserialize_calls_if_none_if_not_none_and_data_is_none(self, marshaller_impl): mock_result = mock.MagicMock() From ba14e3c2eafa42e5eb65925537b86cb015a57bbc Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Mon, 23 Mar 2020 14:44:50 +0000 Subject: [PATCH 035/922] Added initial sharding code, moved configs to 'core' namespace directly, renamed net.GatewayClient to net.ShardConnection --- hikari/__init__.py | 6 +- hikari/core/__init__.py | 47 ++- hikari/core/{configs/app.py => app_config.py} | 19 +- hikari/core/clients/__init__.py | 18 - hikari/core/clients/gateway.py | 41 -- hikari/core/configs/__init__.py | 33 -- hikari/core/entities.py | 5 + hikari/core/gateway.py | 379 ++++++++++++++++++ .../{configs/gateway.py => gateway_config.py} | 63 ++- .../{gateway_bot.py => gateway_entities.py} | 24 +- .../core/{configs/http.py => http_config.py} | 10 +- .../protocol.py => protocol_config.py} | 8 +- hikari/internal_utilities/marshaller.py | 12 + hikari/net/__init__.py | 10 +- hikari/net/{gateway.py => shard.py} | 30 +- .../net/{test_gateway.py => test_shard.py} | 42 +- 16 files changed, 589 insertions(+), 158 deletions(-) rename hikari/core/{configs/app.py => app_config.py} (89%) delete mode 100644 hikari/core/clients/__init__.py delete mode 100644 hikari/core/configs/__init__.py create mode 100644 hikari/core/gateway.py rename hikari/core/{configs/gateway.py => gateway_config.py} (73%) rename hikari/core/{gateway_bot.py => gateway_entities.py} (77%) rename hikari/core/{configs/http.py => http_config.py} (84%) rename hikari/core/{configs/protocol.py => protocol_config.py} (95%) rename hikari/net/{gateway.py => shard.py} (96%) rename tests/hikari/net/{test_gateway.py => test_shard.py} (95%) diff --git a/hikari/__init__.py b/hikari/__init__.py index a2eef25497..64f9398fc3 100644 --- a/hikari/__init__.py +++ b/hikari/__init__.py @@ -26,8 +26,4 @@ from hikari._about import __url__ from hikari._about import __version__ from hikari.errors import * -from hikari.net.codes import * -from hikari.net.errors import * -from hikari.net.gateway import * -from hikari.net.http_client import * -from hikari.net.versions import * +from hikari.net import * diff --git a/hikari/core/__init__.py b/hikari/core/__init__.py index 53aca2edc4..f0d4202f3d 100644 --- a/hikari/core/__init__.py +++ b/hikari/core/__init__.py @@ -17,8 +17,25 @@ # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . """The core API for interacting with Discord directly.""" -from hikari.core.configs import * -from hikari.core.clients import * +from hikari.core import app_config +from hikari.core import channels +from hikari.core import entities +from hikari.core import events +from hikari.core import gateway +from hikari.core import gateway_entities +from hikari.core import gateway_config +from hikari.core import guilds +from hikari.core import http_config +from hikari.core import invites +from hikari.core import messages +from hikari.core import oauth2 +from hikari.core import permissions +from hikari.core import protocol_config +from hikari.core import snowflakes +from hikari.core import users +from hikari.core import webhooks + +from hikari.core.app_config import * from hikari.core.channels import * from hikari.core.colors import * from hikari.core.colours import * @@ -26,13 +43,37 @@ from hikari.core.emojis import * from hikari.core.entities import * from hikari.core.events import * -from hikari.core.gateway_bot import * +from hikari.core.gateway import * +from hikari.core.gateway_entities import * +from hikari.core.gateway_config import * from hikari.core.guilds import * +from hikari.core.http_config import * from hikari.core.invites import * from hikari.core.messages import * from hikari.core.oauth2 import * from hikari.core.permissions import * +from hikari.core.protocol_config import * from hikari.core.snowflakes import * from hikari.core.users import * from hikari.core.voices import * from hikari.core.webhooks import * + +__all__ = [ + *app_config.__all__, + *channels.__all__, + *entities.__all__, + *events.__all__, + *gateway.__all__, + *gateway_entities.__all__, + *gateway_config.__all__, + *guilds.__all__, + *http_config.__all__, + *invites.__all__, + *messages.__all__, + *oauth2.__all__, + *permissions.__all__, + *protocol_config.__all__, + *snowflakes.__all__, + *users.__all__, + *webhooks.__all__, +] diff --git a/hikari/core/configs/app.py b/hikari/core/app_config.py similarity index 89% rename from hikari/core/configs/app.py rename to hikari/core/app_config.py index df464a9aa6..99dc4f71cc 100644 --- a/hikari/core/configs/app.py +++ b/hikari/core/app_config.py @@ -21,13 +21,14 @@ import typing -from hikari.core.configs import http as http_ -from hikari.core.configs import gateway as gateway_ +from hikari.core import entities +from hikari.core import http_config +from hikari.core import gateway_config from hikari.internal_utilities import marshaller @marshaller.attrs(kw_only=True) -class AppConfig: +class AppConfig(entities.HikariEntity, entities.Deserializable): """Root application configuration object. All fields are optional kwargs that can be passed to the constructor. @@ -130,18 +131,18 @@ class AppConfig: #: #: If unspecified or None, then this will be a set of default values. #: - #: :type: :obj:`hikari.core.configs.http.HTTPConfig`, optional - http: typing.Optional[http_.HTTPConfig] = marshaller.attrib( - deserializer=http_.HTTPConfig.deserialize, if_none=None, if_undefined=None, default=None + #: :type: :obj:`hikari.core.http_config.HTTPConfig`, optional + http: typing.Optional[http_config.HTTPConfig] = marshaller.attrib( + deserializer=http_config.HTTPConfig.deserialize, if_none=None, if_undefined=None, default=None ) #: The gateway configuration to use. #: #: If unspecified or None, then this will be a set of default values. #: - #: :type: :obj:`hikari.core.configs.gateway.GatewayConfig`, optional - gateway: typing.Optional[gateway_.GatewayConfig] = marshaller.attrib( - deserializer=gateway_.GatewayConfig.deserialize, if_none=None, if_undefined=None, default=None + #: :type: :obj:`hikari.core.gateway_config.GatewayConfig`, optional + gateway: typing.Optional[gateway_config.GatewayConfig] = marshaller.attrib( + deserializer=gateway_config.GatewayConfig.deserialize, if_none=None, if_undefined=None, default=None ) #: The global token to use, if applicable. This can be overridden for each diff --git a/hikari/core/clients/__init__.py b/hikari/core/clients/__init__.py deleted file mode 100644 index 1c1502a5ca..0000000000 --- a/hikari/core/clients/__init__.py +++ /dev/null @@ -1,18 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# Copyright © Nekoka.tt 2019-2020 -# -# This file is part of Hikari. -# -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . diff --git a/hikari/core/clients/gateway.py b/hikari/core/clients/gateway.py index 0001a309d1..e69de29bb2 100644 --- a/hikari/core/clients/gateway.py +++ b/hikari/core/clients/gateway.py @@ -1,41 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# Copyright © Nekoka.tt 2019-2020 -# -# This file is part of Hikari. -# -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . -import typing - -from hikari.core import entities -from hikari.internal_utilities import marshaller - - -@marshaller.attrs() -class GatewayActivity(entities.Deserializable): - #: The activity name. - #: - #: :type: :obj:`str` - name: str = marshaller.attrib(deserializer=str, serializer=str) - - #: The activity url. Only valid for ``STREAMING`` activities. - #: - #: :type: :obj:`str`, optional - url: typing.Optional[str] = marshaller.attrib(deserializer=str, serializer=str, if_none=None, if_undefined=None) - - # TODO: implement enum for this. - #: The activity type. - #: - #: :type: :obj:`int` - type: int = marshaller.attrib(deserializer=int, serializer=int, if_undefined=lambda: 0) diff --git a/hikari/core/configs/__init__.py b/hikari/core/configs/__init__.py deleted file mode 100644 index e635171193..0000000000 --- a/hikari/core/configs/__init__.py +++ /dev/null @@ -1,33 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# Copyright © Nekoka.tt 2019-2020 -# -# This file is part of Hikari. -# -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . -"""Configuration data objects. These structure the settings a user can -initialise their application with, and optionally support being read -in from an external source, such as a JSON file, using the marshalling -functionality included in this library. -""" - -from hikari.core.configs import app -from hikari.core.configs import gateway -from hikari.core.configs import http -from hikari.core.configs import protocol - -from hikari.core.configs.app import * -from hikari.core.configs.gateway import * -from hikari.core.configs.http import * -from hikari.core.configs.protocol import * diff --git a/hikari/core/entities.py b/hikari/core/entities.py index 6a47ea2f9b..63ef9d8c7d 100644 --- a/hikari/core/entities.py +++ b/hikari/core/entities.py @@ -55,6 +55,11 @@ class HikariEntity(metaclass=abc.ABCMeta): __slots__ = () + if typing.TYPE_CHECKING: + + def __init__(self, *args, **kwargs) -> None: + ... + class Deserializable: """A mixin for any type that allows deserialization from a raw value diff --git a/hikari/core/gateway.py b/hikari/core/gateway.py new file mode 100644 index 0000000000..dd09060a54 --- /dev/null +++ b/hikari/core/gateway.py @@ -0,0 +1,379 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# Copyright © Nekoka.tt 2019-2020 +# +# This file is part of Hikari. +# +# Hikari is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Hikari is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Hikari. If not, see . +__all__ = ["ShardState", "Shard", "GatewayClient"] + +import asyncio +import datetime +import enum +import time +import typing + +import aiohttp + +from hikari.core import events +from hikari.core import gateway_entities +from hikari.core import gateway_config +from hikari.internal_utilities import aio +from hikari.internal_utilities import loggers +from hikari.net import errors +from hikari.net import shard +from hikari.net import ratelimits + +_EventT = typing.TypeVar("_EventT", bound=events.HikariEvent) + + +@enum.unique +class ShardState(enum.IntEnum): + NOT_RUNNING = 0 + INITIALIZING = enum.auto() + WAITING_FOR_READY = enum.auto() + READY = enum.auto() + RECONNECTING = enum.auto() + STOPPING = enum.auto() + STOPPED = enum.auto() + + +class Shard: + """The primary interface for a single shard connection. This contains + several abstractions to enable usage of the low level gateway network + interface with the higher level constructs in :mod:`hikari.core`. + + Parameters + ---------- + shard_id : :obj:`int` + The ID of this specific shard. + config : :obj:`gateway_config.GatewayConfig` + The gateway configuration to use to initialize this shard. + low_level_dispatch : :obj:`typing.Callable` [ [ :obj:`Shard`, :obj:`str`, :obj:`typing.Any` ] ] + A function that is fed any low-level event payloads. This will consist + of three arguments: an :obj:`Shard` which is this shard instance, + a :obj:`str` of the raw event name, and any naive raw payload that was + passed with the event. The expectation is the function passed here + will pass the payload onto any event handling and state handling system + to be transformed into a higher-level representation. + url : :obj:`str` + The URL to connect the gateway to. + + Notes + ----- + Generally, you want to use :class:`GatewayClient` rather than this class + directly, as that will handle sharding where enabled and applicable, and + provides a few more bits and pieces that may be useful. If you want + to customize this, you can subclass it and simply override anything you + want. + """ + + __slots__ = ( + "logger", + "_dispatch", + "_client", + "_status", + "_activity", + "_idle_since", + "_is_afk", + "_task", + "_shard_state", + ) + + def __init__( + self, + shard_id: int, + shard_count: int, + config: gateway_config.GatewayConfig, + low_level_dispatch: typing.Callable[["Shard", str, typing.Any], None], + url: str, + ) -> None: + self.logger = loggers.get_named_logger(self, shard_id) + self._dispatch = low_level_dispatch + self._activity = config.initial_activity + self._idle_since = config.initial_idle_since + self._is_afk = config.initial_is_afk + self._status = config.initial_status + self._shard_state = ShardState.NOT_RUNNING + self._task = None + self._client = shard.ShardConnection( + compression=config.use_compression, + connector=config.protocol.connector if config.protocol is not None else None, + debug=config.debug, + dispatch=lambda _, event_name, pl: self._dispatch(self, event_name, pl), + initial_presence=self._create_presence_pl( + status=config.initial_status, + activity=config.initial_activity, + idle_since=config.initial_idle_since, + is_afk=config.initial_is_afk, + ), + intents=config.intents, + large_threshold=config.large_threshold, + proxy_auth=config.protocol.proxy_auth if config.protocol is not None else None, + proxy_headers=config.protocol.proxy_headers if config.protocol is not None else None, + proxy_url=config.protocol.proxy_url if config.protocol is not None else None, + session_id=None, + seq=None, + shard_id=shard_id, + shard_count=shard_count, + ssl_context=config.protocol.ssl_context if config.protocol is not None else None, + token=config.token, + url=url, + verify_ssl=config.protocol.verify_ssl if config.protocol is not None else None, + version=config.version, + ) + + @property + def client(self) -> shard.ShardConnection: + """ + Returns + ------- + :obj:`hikari.net.gateway.GatewayClient` + The low-level gateway client used for this shard. + """ + return self._client + + #: TODO: use enum + @property + def status(self) -> str: + """ + Returns + ------- + :obj:`str` + The current user status for this shard. + """ + return self._status + + @property + def activity(self) -> typing.Optional[gateway_entities.GatewayActivity]: + """ + Returns + ------- + :obj:`hikari.core.gateway_entities.GatewayActivity`, optional + The current activity for the user on this shard, or ``None`` if + there is no activity. + """ + return self._activity + + @property + def idle_since(self) -> typing.Optional[datetime.datetime]: + """ + Returns + ------- + :obj:`datetime.datetime`, optional + The timestamp when the user of this shard appeared to be idle, or + ``None`` if not applicable. + """ + return self._idle_since + + @property + def is_afk(self) -> bool: + """ + Returns + ------- + :obj:`bool` + ``True`` if the user is AFK, ``False`` otherwise. + """ + return self._is_afk + + async def start(self): + """Connect to the gateway on this shard and schedule tasks to keep this + connection alive. Wait for the shard to dispatch a ``READY`` event, and + then return. + """ + if self._shard_state not in (ShardState.NOT_RUNNING, ShardState.STOPPED): + raise RuntimeError("Cannot start a shard twice") + + self.logger.debug("starting shard") + self._shard_state = ShardState.INITIALIZING + self._task = asyncio.create_task(self._keep_alive()) + self.logger.info("waiting for READY") + await self._client.identify_event.wait() + self._shard_state = ShardState.WAITING_FOR_READY + await self._client.ready_event.wait() + self.logger.info("now READY") + self._shard_state = ShardState.READY + + async def join(self) -> None: + """Wait for the shard to shut down fully.""" + await self._task if self._task is not None else aio.completed_future() + + async def stop(self, wait: bool = True) -> None: + """Request that the shard shuts down. + + Parameters + ---------- + wait : :obj:`bool` + If ``True`` (default), then wait for the client to shut down fully. + If ``False``, only send the signal to shut down, but do not wait + for it explicitly. + """ + if self._shard_state != ShardState.STOPPING: + self._shard_state = ShardState.STOPPING + self.logger.debug("stopping shard") + await self._client.close() + if wait: + await self._task + + async def _keep_alive(self): + back_off = ratelimits.ExponentialBackOff(maximum=None) + last_start = time.perf_counter() + do_not_back_off = True + + while True: + try: + if not do_not_back_off and time.perf_counter() - last_start < 30: + next_backoff = next(back_off) + self.logger.info( + "restarted within 30 seconds, will backoff for %ss", next_backoff, + ) + await asyncio.sleep(next_backoff) + else: + back_off.reset() + + last_start = time.perf_counter() + + do_not_back_off = False + await self._client.connect() + self.logger.critical("shut down silently! this shouldn't happen!") + + except aiohttp.ClientConnectorError as ex: + self.logger.exception( + "failed to connect to Discord to initialize a websocket connection", exc_info=ex, + ) + except errors.GatewayZombiedError: + self.logger.warning("entered a zombie state and will be restarted") + except errors.GatewayInvalidSessionError as ex: + if ex.can_resume: + self.logger.warning("invalid session, so will attempt to resume") + else: + self.logger.warning("invalid session, so will attempt to reconnect") + + if not ex.can_resume: + self._client.seq = None + self._client.session_id = None + do_not_back_off = True + await asyncio.sleep(5) + except errors.GatewayMustReconnectError: + self.logger.warning("instructed by Discord to reconnect") + self._client.seq = None + self._client.session_id = None + do_not_back_off = True + await asyncio.sleep(5) + except errors.GatewayServerClosedConnectionError: + self.logger.warning("disconnected by Discord, will attempt to reconnect") + except errors.GatewayClientClosedError: + self.logger.warning("shut down because the client is closing") + return + except Exception as ex: + self.logger.debug("propagating unexpected exception %s", exc_info=ex) + raise ex + + async def update_presence( + self, + status: str = ..., # TODO: use enum for status + activity: typing.Optional[gateway_entities.GatewayActivity] = ..., + idle_since: typing.Optional[datetime.datetime] = ..., + is_afk: bool = ..., + ) -> None: + """Update the presence of the user for the shard. + + This will only update arguments that you explicitly specify a value for. + Any arguments that you do not explicitly provide some value for will + not be changed. + + Warnings + -------- + This will fail if the shard is not online. + + Parameters + ---------- + status : :obj:`str` + The new status to set. + activity : :obj:`hikari.core.gateway_entities.GatewayActivity`, optional + The new activity to set. + idle_since : :obj:`datetime.datetime`, optional + The time to show up as being idle since, or ``None`` if not + applicable. + is_afk : :obj:`bool` + ``True`` if the user should be marked as AFK, or ``False`` + otherwise. + """ + status = self._status if status is ... else status + activity = self._activity if activity is ... else activity + idle_since = self._idle_since if idle_since is ... else idle_since + is_afk = self._is_afk if is_afk is ... else is_afk + + presence = self._create_presence_pl(status=status, activity=activity, idle_since=idle_since, is_afk=is_afk) + await self._client.update_presence(presence) + + # If we get this far, the update succeeded probably, or the gateway just died. Whatever. + self._status = status + self._activity = activity + self._idle_since = idle_since + self._is_afk = is_afk + + @staticmethod + def _create_presence_pl( + status: str, # TODO: use enum for status + activity: typing.Optional[gateway_entities.GatewayActivity], + idle_since: typing.Optional[datetime.datetime], + is_afk: bool, + ) -> typing.Dict[str, typing.Any]: + return { + "status": status, + "idle_since": idle_since.timestamp() if idle_since is not None else None, + "game": activity.serialize() if activity is not None else None, + "afk": is_afk, + } + + +ShardT = typing.TypeVar("ShardT", bound=Shard) + + +class GatewayClient(typing.Generic[ShardT]): + def __init__( + self, config: gateway_config.GatewayConfig, url: str, shard_type: typing.Type[ShardT] = Shard, + ) -> None: + self.config = config + self.shards = { + shard_id: shard_type(shard_id, config, self._low_level_dispatch, url) + for shard_id in config.shard_config.shard_ids + } + + async def start(self) -> None: + raise NotImplementedError() + + async def join(self) -> None: + raise NotImplementedError() + + async def shutdown(self) -> None: + raise NotImplementedError() + + async def destroy(self) -> None: + raise NotImplementedError() + + @typing.overload + async def wait_for(self, event: _EventT, predicate: typing.Callable[[_EventT], bool], timeout: float) -> _EventT: + ... + + @typing.overload + async def wait_for(self, event: str, predicate: typing.Callable[[_EventT], bool], timeout: float) -> _EventT: + ... + + async def wait_for(self, event: ..., predicate: ..., timeout: float) -> _EventT: + raise NotImplementedError() + + def _low_level_dispatch(self, shard: ShardT, event_name: str, payload: typing.Any) -> None: + print(shard, event_name, payload) diff --git a/hikari/core/configs/gateway.py b/hikari/core/gateway_config.py similarity index 73% rename from hikari/core/configs/gateway.py rename to hikari/core/gateway_config.py index d26ac5da8f..9d0d9d87fb 100644 --- a/hikari/core/configs/gateway.py +++ b/hikari/core/gateway_config.py @@ -24,11 +24,12 @@ import typing from hikari.core import entities -from hikari.core.clients import gateway -from hikari.core.configs import protocol as protocol_ +from hikari.core import gateway_entities +from hikari.core import protocol_config from hikari.internal_utilities import assertions from hikari.internal_utilities import dates from hikari.internal_utilities import marshaller +from hikari.net import codes as net_codes def _parse_shard_info(payload): @@ -52,7 +53,7 @@ def _parse_shard_info(payload): @marshaller.attrs(kw_only=True, init=False) -class ShardConfig(entities.Deserializable): +class ShardConfig(entities.HikariEntity, entities.Deserializable): """Manual sharding configuration. All fields are optional kwargs that can be passed to the constructor. @@ -100,7 +101,7 @@ def __init__(self, *, shard_ids: typing.Optional[typing.Iterable[int]] = None, s @marshaller.attrs(kw_only=True) -class GatewayConfig(entities.Deserializable): +class GatewayConfig(entities.HikariEntity, entities.Deserializable): """Gateway and sharding configuration. All fields are optional kwargs that can be passed to the constructor. @@ -119,8 +120,8 @@ class GatewayConfig(entities.Deserializable): #: ``None``, then no activity will be set. #: #: :type: :obj:`GatewayActivity`, optional - initial_activity: typing.Optional[gateway.GatewayActivity] = marshaller.attrib( - deserializer=gateway.GatewayActivity.deserialize, if_none=None, if_undefined=None, default=None + initial_activity: typing.Optional[gateway_entities.GatewayActivity] = marshaller.attrib( + deserializer=gateway_entities.GatewayActivity.deserialize, if_none=None, if_undefined=None, default=None ) # TODO: implement enum for this @@ -132,7 +133,7 @@ class GatewayConfig(entities.Deserializable): #: Whether to show up as AFK or not on sign-in. #: #: :type: :obj:`bool` - initial_afk: bool = marshaller.attrib(deserializer=bool, if_undefined=False, default=False) + initial_is_afk: bool = marshaller.attrib(deserializer=bool, if_undefined=False, default=False) #: The idle time to show on signing in, or ``None`` to not show an idle #: time. @@ -142,6 +143,48 @@ class GatewayConfig(entities.Deserializable): deserializer=dates.unix_epoch_to_ts, if_none=None, if_undefined=None, default=None ) + #: The intents to use for the connection. + #: + #: If being deserialized, this can be an integer bitfield, or a sequence of + #: intent names. If + #: unspecified, this will be set to ``None``. + #: + #: :type: :obj:`hikari.net.codes.GatewayIntent`, optional + #: + #: Examples + #: -------- + #: + #: .. code-block:: python + #: + #: # Python example + #: GatewayIntent.GUILDS | GatewayIntent.GUILD_MESSAGES + #: + #: ..code-block:: js + #: + #: // JSON example, using explicit bitfield values + #: 513 + #: // JSON example, using an array of names + #: [ "GUILDS", "GUILD_MESSAGES" ] + #: + #: See :obj:`hikari.net.codes.GatewayIntent` for valid names of + #: intents you can use. Integer values are as documented on Discord's + #: developer portal. + #: + #: Warnings + #: -------- + #: If you are using the V7 gateway implementation, you will NEED to provide + #: explicit intent values for this field in order to get online. + #: Additionally, intents that are classed by Discord as being privileged + #: will require you to whitelist your application in order to use them. + #: + #: If you are using the V6 gateway implementation, setting this to ``None`` + #: will simply opt you into every event you can subscribe to. + intents: typing.Optional[net_codes.GatewayIntent] = marshaller.attrib( + deserializer=lambda value: marshaller.dereference_int_flag(net_codes.GatewayIntent, value), + if_undefined=None, + default=None, + ) + #: The large threshold to use. large_threshold: int = marshaller.attrib(deserializer=int, if_undefined=lambda: 250, default=True) @@ -152,9 +195,9 @@ class GatewayConfig(entities.Deserializable): #: #: If unspecified, defaults are used. #: - #: :type: :obj:`hikari.core.configs.protocol.HTTPProtocolConfig` - protocol: typing.Optional[protocol_.HTTPProtocolConfig] = marshaller.attrib( - deserializer=protocol_.HTTPProtocolConfig.deserialize, if_undefined=None, default=None, + #: :type: :obj:`hikari.core.protocol_config.HTTPProtocolConfig` + protocol: typing.Optional[protocol_config.HTTPProtocolConfig] = marshaller.attrib( + deserializer=protocol_config.HTTPProtocolConfig.deserialize, if_undefined=None, default=None, ) #: Manual sharding configuration to use. If this is ``None``, or diff --git a/hikari/core/gateway_bot.py b/hikari/core/gateway_entities.py similarity index 77% rename from hikari/core/gateway_bot.py rename to hikari/core/gateway_entities.py index f028fdb8db..1c729c2a42 100644 --- a/hikari/core/gateway_bot.py +++ b/hikari/core/gateway_entities.py @@ -16,10 +16,7 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . -""" -Components and entities that are used to describe Discord gateway information. -""" -__all__ = ["GatewayBot"] +__all__ = ["GatewayBot", "GatewayActivity"] import datetime @@ -68,3 +65,22 @@ class GatewayBot(entities.HikariEntity, entities.Deserializable): #: #: :type: :obj:`SessionStartLimit` session_start_limit: int = marshaller.attrib(deserializer=SessionStartLimit.deserialize) + + +@marshaller.attrs() +class GatewayActivity(entities.Deserializable, entities.Serializable): + #: The activity name. + #: + #: :type: :obj:`str` + name: str = marshaller.attrib(deserializer=str, serializer=str) + + #: The activity url. Only valid for ``STREAMING`` activities. + #: + #: :type: :obj:`str`, optional + url: typing.Optional[str] = marshaller.attrib(deserializer=str, serializer=str, if_none=None, if_undefined=None) + + # TODO: implement enum for this. + #: The activity type. + #: + #: :type: :obj:`int` + type: int = marshaller.attrib(deserializer=int, serializer=int, if_undefined=0) diff --git a/hikari/core/configs/http.py b/hikari/core/http_config.py similarity index 84% rename from hikari/core/configs/http.py rename to hikari/core/http_config.py index eaf17ba5e5..cb10b3a2aa 100644 --- a/hikari/core/configs/http.py +++ b/hikari/core/http_config.py @@ -23,12 +23,12 @@ import typing from hikari.core import entities -from hikari.core.configs import protocol as protocol_ +from hikari.core import protocol_config from hikari.internal_utilities import marshaller @marshaller.attrs(kw_only=True) -class HTTPConfig(entities.Deserializable): +class HTTPConfig(entities.HikariEntity, entities.Deserializable): """HTTP API configuration. All fields are optional kwargs that can be passed to the constructor. @@ -41,9 +41,9 @@ class HTTPConfig(entities.Deserializable): #: #: If unspecified, defaults are used. #: - #: :type: :obj:`hikari.core.configs.protocol.HTTPProtocolConfig` - protocol: typing.Optional[protocol_.HTTPProtocolConfig] = marshaller.attrib( - deserializer=protocol_.HTTPProtocolConfig.deserialize, if_undefined=None, default=None, + #: :type: :obj:`hikari.core.protocol_config.HTTPProtocolConfig` + protocol: typing.Optional[protocol_config.HTTPProtocolConfig] = marshaller.attrib( + deserializer=protocol_config.HTTPProtocolConfig.deserialize, if_undefined=None, default=None, ) #: The token to use, if applicable. diff --git a/hikari/core/configs/protocol.py b/hikari/core/protocol_config.py similarity index 95% rename from hikari/core/configs/protocol.py rename to hikari/core/protocol_config.py index 0d410a6c59..a4775c4d1f 100644 --- a/hikari/core/configs/protocol.py +++ b/hikari/core/protocol_config.py @@ -23,14 +23,14 @@ import ssl import typing -import aiohttp +import aiohttp.typedefs from hikari.core import entities from hikari.internal_utilities import marshaller @marshaller.attrs(kw_only=True) -class HTTPProtocolConfig(entities.Deserializable): +class HTTPProtocolConfig(entities.HikariEntity, entities.Deserializable): """A configuration class that can be deserialized from a :obj:`dict`. This represents any HTTP-specific implementation and protocol details such as how to manage redirects, how to manage SSL, and how to use a proxy if @@ -71,8 +71,8 @@ class HTTPProtocolConfig(entities.Deserializable): #: #: Defaults to ``None`` if unspecified during deserialization. #: - #: :type: :obj:`typing.Dict` [ :obj:`str`, :obj:`str` ], optional - proxy_headers: typing.Optional[typing.Dict[str, str]] = marshaller.attrib( + #: :type: :obj:`aiohttp.typedefs.LooseHeaders`, optional + proxy_headers: typing.Optional[aiohttp.typedefs.LooseHeaders] = marshaller.attrib( deserializer=dict, if_none=None, if_undefined=None, default=None ) diff --git a/hikari/internal_utilities/marshaller.py b/hikari/internal_utilities/marshaller.py index 4c07ac1d7c..75c51fc029 100644 --- a/hikari/internal_utilities/marshaller.py +++ b/hikari/internal_utilities/marshaller.py @@ -32,7 +32,9 @@ "HikariEntityMarshaller", ] +import functools import importlib +import operator import typing import weakref @@ -96,6 +98,16 @@ def dereference_handle(handle_string: str) -> typing.Any: return weakref.proxy(obj) +def dereference_int_flag(int_flag_type, raw_value) -> None: + if isinstance(raw_value, str) and raw_value.isdigit(): + raw_value = int(raw_value) + + if not isinstance(raw_value, int): + raw_value = functools.reduce(operator.or_, (int_flag_type[name.upper()] for name in raw_value)) + + return int_flag_type(raw_value) + + def attrib( *, # Mandatory! We do not want to rely on type annotations alone, as they will diff --git a/hikari/net/__init__.py b/hikari/net/__init__.py index d277291605..02cf308632 100644 --- a/hikari/net/__init__.py +++ b/hikari/net/__init__.py @@ -23,9 +23,17 @@ """ from hikari.net import base_http_client from hikari.net import codes -from hikari.net import gateway from hikari.net import http_client from hikari.net import ratelimits from hikari.net import routes +from hikari.net import shard from hikari.net import user_agent from hikari.net import versions + +from hikari.net.codes import * +from hikari.net.errors import * +from hikari.net.shard import * +from hikari.net.http_client import * +from hikari.net.versions import * + +__all__ = (codes.__all__ + errors.__all__ + shard.__all__ + http_client.__all__ + versions.__all__) diff --git a/hikari/net/gateway.py b/hikari/net/shard.py similarity index 96% rename from hikari/net/gateway.py rename to hikari/net/shard.py index 03c59b1b71..35a51cc5c8 100644 --- a/hikari/net/gateway.py +++ b/hikari/net/shard.py @@ -30,7 +30,7 @@ * Gateway documentation: https://discordapp.com/developers/docs/topics/gateway * Opcode documentation: https://discordapp.com/developers/docs/topics/opcodes-and-status-codes """ -__all__ = ["GatewayStatus", "GatewayClient"] +__all__ = ["GatewayStatus", "ShardConnection"] import asyncio import contextlib @@ -73,7 +73,7 @@ class GatewayStatus(str, enum.Enum): DispatchT = typing.Callable[["GatewayClient", str, typing.Dict], None] -class GatewayClient: +class ShardConnection: """Implementation of a client for the Discord Gateway. This is a websocket connection to Discord that is used to inform your application of events that occur, and to allow you to change your presence, @@ -195,6 +195,7 @@ class GatewayClient: "_proxy_headers", "_proxy_url", "_ratelimiter", + "ready_event", "requesting_close_event", "_session", "session_id", @@ -273,6 +274,21 @@ class GatewayClient: #: :type: :obj:`logging.Logger` logger: logging.Logger + #: An event that is triggered when a ``READY`` payload is received for the + #: shard. This indicates that it successfully started up and had a correct + #: sharding configuration. This is more appropriate to wait for than + #: :attr:`identify_event` since the former will still fire if starting + #: shards too closely together, for example. This would still lead to an + #: immediate invalid session being fired afterwards. + # + #: It is worth noting that this event is only set for the first ``READY`` + #: event after connecting with a fresh connection. For all other purposes, + #: you should wait for the event to be fired in the ``dispatch`` function + #: you provide. + #: + #: :type: :obj:`asyncio.Event` + ready_event: asyncio.Event + #: An event that is set when something requests that the connection #: should close somewhere. #: @@ -381,6 +397,7 @@ def __init__( self.last_message_received: float = float("nan") self.logger: logging.Logger = loggers.get_named_logger(self, shard_id) self.requesting_close_event: asyncio.Event = asyncio.Event() + self.ready_event: asyncio.Event = asyncio.Event() self.session_id: typing.Optional[str] = session_id self.seq: typing.Optional[int] = seq self.shard_id: int = shard_id @@ -574,6 +591,7 @@ async def connect(self, client_session_type=aiohttp.ClientSession) -> None: self.closed_event.clear() self.hello_event.clear() self.identify_event.clear() + self.ready_event.clear() self.requesting_close_event.clear() self._session = client_session_type(**self._cs_init_kwargs) @@ -692,7 +710,7 @@ async def _identify_or_resume_then_poll_events(self): # noinspection PyTypeChecker pl["d"]["presence"] = self._presence await self._send(pl) - self.logger.info("sent IDENTIFY, ready to listen to incoming events") + self.logger.info("sent IDENTIFY, now listening to incoming events") else: self.status = GatewayStatus.RESUMING self.logger.debug("sending RESUME") @@ -701,7 +719,7 @@ async def _identify_or_resume_then_poll_events(self): "d": {"token": self._token, "seq": self.seq, "session_id": self.session_id}, } await self._send(pl) - self.logger.info("sent RESUME, ready to listen to incoming events") + self.logger.info("sent RESUME, now listening to incoming events") self.identify_event.set() await self._poll_events() @@ -732,6 +750,10 @@ async def _poll_events(self): if op == codes.GatewayOpcode.DISPATCH: self.seq = next_pl["s"] event_name = next_pl["t"] + + if event_name == "READY": + self.ready_event.set() + self.dispatch(self, event_name, d) elif op == codes.GatewayOpcode.HEARTBEAT: await self._send({"op": codes.GatewayOpcode.HEARTBEAT_ACK}) diff --git a/tests/hikari/net/test_gateway.py b/tests/hikari/net/test_shard.py similarity index 95% rename from tests/hikari/net/test_gateway.py rename to tests/hikari/net/test_shard.py index 48b1ec057a..2979b090ca 100644 --- a/tests/hikari/net/test_gateway.py +++ b/tests/hikari/net/test_shard.py @@ -30,7 +30,7 @@ from hikari.internal_utilities import containers from hikari.net import errors -from hikari.net import gateway +from hikari.net import shard from hikari.net import user_agent from hikari.net import versions from tests.hikari import _helpers @@ -86,12 +86,12 @@ async def ws_connect(self, *args, **kwargs): class TestGatewayClientConstructor: async def test_init_sets_shard_numbers_correctly(self,): input_shard_id, input_shard_count, expected_shard_id, expected_shard_count = 1, 2, 1, 2 - client = gateway.GatewayClient(shard_id=input_shard_id, shard_count=input_shard_count, token="xxx", url="yyy") + client = shard.ShardConnection(shard_id=input_shard_id, shard_count=input_shard_count, token="xxx", url="yyy") assert client.shard_id == expected_shard_id assert client.shard_count == expected_shard_count async def test_dispatch_is_callable(self): - client = gateway.GatewayClient(token="xxx", url="yyy") + client = shard.ShardConnection(token="xxx", url="yyy") client.dispatch(client, "ping", "pong") @pytest.mark.parametrize( @@ -103,7 +103,7 @@ async def test_dispatch_is_callable(self): ) async def test_compression(self, compression, expected_url_query): url = "ws://baka-im-not-a-http-url:49620/locate/the/bloody/websocket?ayyyyy=lmao" - client = gateway.GatewayClient(token="xxx", url=url, compression=compression) + client = shard.ShardConnection(token="xxx", url=url, compression=compression) scheme, netloc, path, params, query, fragment = urllib.parse.urlparse(client._url) assert scheme == "ws" assert netloc == "baka-im-not-a-http-url:49620" @@ -114,13 +114,13 @@ async def test_compression(self, compression, expected_url_query): assert fragment == "" async def test_init_hearbeat_defaults_before_startup(self): - client = gateway.GatewayClient(token="xxx", url="yyy") + client = shard.ShardConnection(token="xxx", url="yyy") assert math.isnan(client.last_heartbeat_sent) assert math.isnan(client.heartbeat_latency) assert math.isnan(client.last_message_received) async def test_init_connected_at_is_nan(self): - client = gateway.GatewayClient(token="xxx", url="yyy") + client = shard.ShardConnection(token="xxx", url="yyy") assert math.isnan(client._connected_at) @@ -132,7 +132,7 @@ class TestGatewayClientUptimeProperty: ) async def test_uptime(self, connected_at, now, expected_uptime): with mock.patch("time.perf_counter", return_value=now): - client = gateway.GatewayClient(token="xxx", url="yyy") + client = shard.ShardConnection(token="xxx", url="yyy") client._connected_at = connected_at assert client.uptime == expected_uptime @@ -141,7 +141,7 @@ async def test_uptime(self, connected_at, now, expected_uptime): class TestGatewayClientIsConnectedProperty: @pytest.mark.parametrize(["connected_at", "is_connected"], [(float("nan"), False), (15, True), (2500.0, True),]) async def test_is_connected(self, connected_at, is_connected): - client = gateway.GatewayClient(token="xxx", url="yyy") + client = shard.ShardConnection(token="xxx", url="yyy") client._connected_at = connected_at assert client.is_connected is is_connected @@ -162,7 +162,7 @@ class TestGatewayReconnectCountProperty: ], ) async def test_value(self, disconnect_count, is_connected, expected_reconnect_count): - client = gateway.GatewayClient(token="xxx", url="yyy") + client = shard.ShardConnection(token="xxx", url="yyy") client.disconnect_count = disconnect_count client._connected_at = 420 if is_connected else float("nan") assert client.reconnect_count == expected_reconnect_count @@ -171,12 +171,12 @@ async def test_value(self, disconnect_count, is_connected, expected_reconnect_co @pytest.mark.asyncio class TestGatewayCurrentPresenceProperty: async def test_returns_presence(self): - client = gateway.GatewayClient(token="xxx", url="yyy") + client = shard.ShardConnection(token="xxx", url="yyy") client._presence = {"foo": "bar"} assert client.current_presence == {"foo": "bar"} async def test_returns_copy(self): - client = gateway.GatewayClient(token="xxx", url="yyy") + client = shard.ShardConnection(token="xxx", url="yyy") client._presence = {"foo": "bar"} assert client.current_presence is not client._presence @@ -186,7 +186,7 @@ class TestGatewayClientAiohttpClientSessionKwargsProperty: async def test_right_stuff_is_included(self): connector = mock.MagicMock() - client = gateway.GatewayClient(url="...", token="...", connector=connector,) + client = shard.ShardConnection(url="...", token="...", connector=connector, ) assert client._cs_init_kwargs == dict(connector=connector) @@ -201,7 +201,7 @@ async def test_right_stuff_is_included(self): verify_ssl = True ssl_context = mock.MagicMock() - client = gateway.GatewayClient( + client = shard.ShardConnection( url=url, token="...", proxy_url=proxy_url, @@ -248,7 +248,7 @@ def non_hello_payload(self): @pytest.fixture def client(self, event_loop): asyncio.set_event_loop(event_loop) - client = _helpers.unslot_class(gateway.GatewayClient)(url="ws://localhost", token="xxx") + client = _helpers.unslot_class(shard.ShardConnection)(url="ws://localhost", token="xxx") client = _helpers.mock_methods_on(client, except_=("connect",)) client._receive = mock.AsyncMock(return_value=self.hello_payload) return client @@ -504,7 +504,7 @@ class TestGatewayClientIdentifyOrResumeThenPollEvents: @pytest.fixture def client(self, event_loop): asyncio.set_event_loop(event_loop) - client = _helpers.unslot_class(gateway.GatewayClient)(token="1234", url="xxx") + client = _helpers.unslot_class(shard.ShardConnection)(token="1234", url="xxx") client = _helpers.mock_methods_on(client, except_=("_identify_or_resume_then_poll_events",)) def send(_): @@ -514,7 +514,7 @@ def poll_events(): client.poll_events_time = time.perf_counter() client._send = mock.AsyncMock(wraps=send) - client._poll_events = mock.AsyncMock(spec=gateway.GatewayClient._send, wraps=poll_events) + client._poll_events = mock.AsyncMock(spec=shard.ShardConnection._send, wraps=poll_events) return client async def test_no_session_id_sends_identify_then_polls_events(self, client): @@ -678,7 +678,7 @@ class TestHeartbeatKeepAlive: @pytest.fixture def client(self, event_loop): asyncio.set_event_loop(event_loop) - client = _helpers.unslot_class(gateway.GatewayClient)(token="1234", url="xxx") + client = _helpers.unslot_class(shard.ShardConnection)(token="1234", url="xxx") client = _helpers.mock_methods_on(client, except_=("_heartbeat_keep_alive",)) client._send = mock.AsyncMock() # This won't get set on the right event loop if we are not careful @@ -738,7 +738,7 @@ class TestClose: @pytest.fixture def client(self, event_loop): asyncio.set_event_loop(event_loop) - client = _helpers.unslot_class(gateway.GatewayClient)(token="1234", url="xxx") + client = _helpers.unslot_class(shard.ShardConnection)(token="1234", url="xxx") client = _helpers.mock_methods_on(client, except_=("close",)) client.ws = _helpers.create_autospec(aiohttp.ClientWebSocketResponse) client.session = _helpers.create_autospec(aiohttp.ClientSession) @@ -809,7 +809,7 @@ class TestPollEvents: @pytest.fixture def client(self, event_loop): asyncio.set_event_loop(event_loop) - client = _helpers.unslot_class(gateway.GatewayClient)(token="1234", url="xxx") + client = _helpers.unslot_class(shard.ShardConnection)(token="1234", url="xxx") client = _helpers.mock_methods_on(client, except_=("_poll_events",)) return client @@ -831,7 +831,7 @@ class TestRequestGuildMembers: @pytest.fixture def client(self, event_loop): asyncio.set_event_loop(event_loop) - client = _helpers.unslot_class(gateway.GatewayClient)(token="1234", url="xxx") + client = _helpers.unslot_class(shard.ShardConnection)(token="1234", url="xxx") client = _helpers.mock_methods_on(client, except_=("request_guild_members",)) return client @@ -868,7 +868,7 @@ class TestUpdatePresence: @pytest.fixture def client(self, event_loop): asyncio.set_event_loop(event_loop) - client = _helpers.unslot_class(gateway.GatewayClient)(token="1234", url="xxx") + client = _helpers.unslot_class(shard.ShardConnection)(token="1234", url="xxx") client = _helpers.mock_methods_on(client, except_=("update_presence",)) return client From 6a58439500c3081b2b5953dac04220053a4decc2 Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Mon, 23 Mar 2020 18:10:50 +0000 Subject: [PATCH 036/922] Renamed gateway core components a little. --- hikari/core/__init__.py | 11 +- hikari/core/app_config.py | 2 +- hikari/core/embeds.py | 6 +- hikari/core/{gateway.py => gateway_client.py} | 119 +++++++++++++++--- hikari/core/gateway_config.py | 4 +- hikari/core/gateway_entities.py | 1 + hikari/core/guilds.py | 2 +- hikari/core/messages.py | 8 +- hikari/core/state.py | 24 ++++ hikari/internal_utilities/aio.py | 2 +- hikari/net/__init__.py | 5 +- hikari/net/shard.py | 3 + ...ateway_bot.py => test_gateway_entities.py} | 12 +- tests/hikari/net/test_shard.py | 2 +- 14 files changed, 153 insertions(+), 48 deletions(-) rename hikari/core/{gateway.py => gateway_client.py} (78%) create mode 100644 hikari/core/state.py rename tests/hikari/core/{test_gateway_bot.py => test_gateway_entities.py} (82%) diff --git a/hikari/core/__init__.py b/hikari/core/__init__.py index f0d4202f3d..3612738c3a 100644 --- a/hikari/core/__init__.py +++ b/hikari/core/__init__.py @@ -21,9 +21,9 @@ from hikari.core import channels from hikari.core import entities from hikari.core import events -from hikari.core import gateway -from hikari.core import gateway_entities +from hikari.core import gateway_client from hikari.core import gateway_config +from hikari.core import gateway_entities from hikari.core import guilds from hikari.core import http_config from hikari.core import invites @@ -34,7 +34,6 @@ from hikari.core import snowflakes from hikari.core import users from hikari.core import webhooks - from hikari.core.app_config import * from hikari.core.channels import * from hikari.core.colors import * @@ -43,9 +42,9 @@ from hikari.core.emojis import * from hikari.core.entities import * from hikari.core.events import * -from hikari.core.gateway import * -from hikari.core.gateway_entities import * +from hikari.core.gateway_client import * from hikari.core.gateway_config import * +from hikari.core.gateway_entities import * from hikari.core.guilds import * from hikari.core.http_config import * from hikari.core.invites import * @@ -63,7 +62,7 @@ *channels.__all__, *entities.__all__, *events.__all__, - *gateway.__all__, + *gateway_client.__all__, *gateway_entities.__all__, *gateway_config.__all__, *guilds.__all__, diff --git a/hikari/core/app_config.py b/hikari/core/app_config.py index 99dc4f71cc..8e5aa1a2a6 100644 --- a/hikari/core/app_config.py +++ b/hikari/core/app_config.py @@ -22,8 +22,8 @@ import typing from hikari.core import entities -from hikari.core import http_config from hikari.core import gateway_config +from hikari.core import http_config from hikari.internal_utilities import marshaller diff --git a/hikari/core/embeds.py b/hikari/core/embeds.py index 16f1a5bf27..bfd29f7ffc 100644 --- a/hikari/core/embeds.py +++ b/hikari/core/embeds.py @@ -30,13 +30,13 @@ "EmbedField", ] -import typing import datetime +import typing -from hikari.core import entities from hikari.core import colors -from hikari.internal_utilities import marshaller +from hikari.core import entities from hikari.internal_utilities import dates +from hikari.internal_utilities import marshaller @marshaller.attrs(slots=True) diff --git a/hikari/core/gateway.py b/hikari/core/gateway_client.py similarity index 78% rename from hikari/core/gateway.py rename to hikari/core/gateway_client.py index dd09060a54..0222e32528 100644 --- a/hikari/core/gateway.py +++ b/hikari/core/gateway_client.py @@ -16,24 +16,28 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . -__all__ = ["ShardState", "Shard", "GatewayClient"] +__all__ = ["ShardState", "ShardClient", "GatewayClient"] +import abc import asyncio +import contextlib import datetime import enum +import logging +import signal import time import typing import aiohttp from hikari.core import events -from hikari.core import gateway_entities from hikari.core import gateway_config +from hikari.core import gateway_entities from hikari.internal_utilities import aio from hikari.internal_utilities import loggers from hikari.net import errors -from hikari.net import shard from hikari.net import ratelimits +from hikari.net import shard _EventT = typing.TypeVar("_EventT", bound=events.HikariEvent) @@ -49,7 +53,61 @@ class ShardState(enum.IntEnum): STOPPED = enum.auto() -class Shard: +class Startable(abc.ABC): + """Base for any socket-based communication medium to provide functionality + for more automated control given certain method constraints. + """ + + logger: logging.Logger + + @abc.abstractmethod + async def start(self): + ... + + @abc.abstractmethod + async def shutdown(self, wait: bool = True): + ... + + @abc.abstractmethod + async def join(self): + ... + + def run(self): + loop = asyncio.get_event_loop() + + def sigterm_handler(*_): + raise KeyboardInterrupt() + + ex = None + + try: + with contextlib.suppress(NotImplementedError): + # Not implemented on Windows + loop.add_signal_handler(signal.SIGTERM, sigterm_handler) + + loop.run_until_complete(self.start()) + try: + loop.run_until_complete(self.join()) + except errors.GatewayClientClosedError: + self.logger.info("client has shut down") + + except KeyboardInterrupt as _ex: + self.logger.info("received signal to shut down client") + loop.run_until_complete(self.shutdown()) + # Apparently you have to alias except clauses or you get an + # UnboundLocalError. + ex = _ex + finally: + loop.run_until_complete(self.shutdown(True)) + with contextlib.suppress(NotImplementedError): + # Not implemented on Windows + loop.remove_signal_handler(signal.SIGTERM) + + if ex: + raise ex from ex + + +class ShardClient(Startable): """The primary interface for a single shard connection. This contains several abstractions to enable usage of the low level gateway network interface with the higher level constructs in :mod:`hikari.core`. @@ -94,9 +152,8 @@ class Shard: def __init__( self, shard_id: int, - shard_count: int, config: gateway_config.GatewayConfig, - low_level_dispatch: typing.Callable[["Shard", str, typing.Any], None], + low_level_dispatch: typing.Callable[["ShardClient", str, typing.Any], None], url: str, ) -> None: self.logger = loggers.get_named_logger(self, shard_id) @@ -126,7 +183,7 @@ def __init__( session_id=None, seq=None, shard_id=shard_id, - shard_count=shard_count, + shard_count=config.shard_config.shard_count, ssl_context=config.protocol.ssl_context if config.protocol is not None else None, token=config.token, url=url, @@ -209,7 +266,7 @@ async def join(self) -> None: """Wait for the shard to shut down fully.""" await self._task if self._task is not None else aio.completed_future() - async def stop(self, wait: bool = True) -> None: + async def shutdown(self, wait: bool = True) -> None: """Request that the shard shuts down. Parameters @@ -225,6 +282,8 @@ async def stop(self, wait: bool = True) -> None: await self._client.close() if wait: await self._task + with contextlib.suppress(): + self._task.result() async def _keep_alive(self): back_off = ratelimits.ExponentialBackOff(maximum=None) @@ -274,7 +333,7 @@ async def _keep_alive(self): except errors.GatewayServerClosedConnectionError: self.logger.warning("disconnected by Discord, will attempt to reconnect") except errors.GatewayClientClosedError: - self.logger.warning("shut down because the client is closing") + self.logger.warning("shutting down") return except Exception as ex: self.logger.debug("propagating unexpected exception %s", exc_info=ex) @@ -339,30 +398,50 @@ def _create_presence_pl( } -ShardT = typing.TypeVar("ShardT", bound=Shard) +ShardT = typing.TypeVar("ShardT", bound=ShardClient) -class GatewayClient(typing.Generic[ShardT]): +class GatewayClient(typing.Generic[ShardT], Startable): def __init__( - self, config: gateway_config.GatewayConfig, url: str, shard_type: typing.Type[ShardT] = Shard, + self, config: gateway_config.GatewayConfig, url: str, shard_type: typing.Type[ShardT] = ShardClient, ) -> None: + self.logger = loggers.get_named_logger(self) self.config = config - self.shards = { + self.shards: typing.Dict[int, ShardT] = { shard_id: shard_type(shard_id, config, self._low_level_dispatch, url) for shard_id in config.shard_config.shard_ids } async def start(self) -> None: - raise NotImplementedError() + """Start all shards. - async def join(self) -> None: - raise NotImplementedError() + This safely starts all shards at the correct rate to prevent invalid + session spam. This involves starting each shard sequentially with a + 5 second pause between each. + """ + self.logger.info("starting %s shard(s)", len(self.shards)) + start_time = time.perf_counter() + for i, shard_id in enumerate(self.config.shard_config.shard_ids): + if i > 0: + await asyncio.sleep(5) - async def shutdown(self) -> None: - raise NotImplementedError() + shard_obj = self.shards[shard_id] + await shard_obj.start() + finish_time = time.perf_counter() - async def destroy(self) -> None: - raise NotImplementedError() + self.logger.info("started %s shard(s) in approx %.2fs", len(self.shards), finish_time - start_time) + + async def join(self) -> None: + await asyncio.gather(*(shard_obj.join() for shard_obj in self.shards.values())) + + async def shutdown(self, wait: bool = True) -> None: + self.logger.info("stopping %s shard(s)", len(self.shards)) + start_time = time.perf_counter() + try: + await asyncio.gather(*(shard_obj.shutdown(wait) for shard_obj in self.shards.values())) + finally: + finish_time = time.perf_counter() + self.logger.info("stopped %s shard(s) in approx %.2fs", len(self.shards), finish_time - start_time) @typing.overload async def wait_for(self, event: _EventT, predicate: typing.Callable[[_EventT], bool], timeout: float) -> _EventT: diff --git a/hikari/core/gateway_config.py b/hikari/core/gateway_config.py index 9d0d9d87fb..edac8cd0a1 100644 --- a/hikari/core/gateway_config.py +++ b/hikari/core/gateway_config.py @@ -94,8 +94,8 @@ class ShardConfig(entities.HikariEntity, entities.Deserializable): def __init__(self, *, shard_ids: typing.Optional[typing.Iterable[int]] = None, shard_count: int) -> None: self.shard_ids = [*shard_ids] if shard_ids else [*range(shard_count)] - for shard_id in self.shard_ids: - assertions.assert_that(shard_id < self.shard_count, "shard_count must be greater than any shard ids") + for shard_id in shard_ids: + assertions.assert_that(shard_id < shard_count, "shard_count must be greater than any shard ids") self.shard_count = shard_count diff --git a/hikari/core/gateway_entities.py b/hikari/core/gateway_entities.py index 1c729c2a42..1538e638af 100644 --- a/hikari/core/gateway_entities.py +++ b/hikari/core/gateway_entities.py @@ -19,6 +19,7 @@ __all__ = ["GatewayBot", "GatewayActivity"] import datetime +import typing from hikari.core import entities from hikari.internal_utilities import marshaller diff --git a/hikari/core/guilds.py b/hikari/core/guilds.py index 27b80d2cda..6248f44d19 100644 --- a/hikari/core/guilds.py +++ b/hikari/core/guilds.py @@ -49,8 +49,8 @@ import typing from hikari.core import channels -from hikari.core import entities from hikari.core import emojis as _emojis +from hikari.core import entities from hikari.core import permissions from hikari.core import snowflakes from hikari.core import users diff --git a/hikari/core/messages.py b/hikari/core/messages.py index 9e41d2c329..72d5716b7c 100644 --- a/hikari/core/messages.py +++ b/hikari/core/messages.py @@ -30,13 +30,13 @@ import enum import typing +from hikari.core import embeds as _embeds +from hikari.core import emojis as _emojis from hikari.core import entities -from hikari.core import snowflakes from hikari.core import guilds -from hikari.core import users from hikari.core import oauth2 -from hikari.core import embeds as _embeds -from hikari.core import emojis as _emojis +from hikari.core import snowflakes +from hikari.core import users from hikari.internal_utilities import dates from hikari.internal_utilities import marshaller diff --git a/hikari/core/state.py b/hikari/core/state.py new file mode 100644 index 0000000000..4731cfcc01 --- /dev/null +++ b/hikari/core/state.py @@ -0,0 +1,24 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# Copyright © Nekoka.tt 2019-2020 +# +# This file is part of Hikari. +# +# Hikari is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Hikari is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Hikari. If not, see . +"""State registry and event manager.""" + + +class StateManager: + def on_event(self, shard, event_name, payload) -> None: + pass diff --git a/hikari/internal_utilities/aio.py b/hikari/internal_utilities/aio.py index 95265620ef..1cf7bdffe8 100644 --- a/hikari/internal_utilities/aio.py +++ b/hikari/internal_utilities/aio.py @@ -27,8 +27,8 @@ import asyncio import dataclasses -import typing import logging +import typing import weakref from hikari.internal_utilities import assertions diff --git a/hikari/net/__init__.py b/hikari/net/__init__.py index 02cf308632..ab4c6e13f8 100644 --- a/hikari/net/__init__.py +++ b/hikari/net/__init__.py @@ -29,11 +29,10 @@ from hikari.net import shard from hikari.net import user_agent from hikari.net import versions - from hikari.net.codes import * from hikari.net.errors import * -from hikari.net.shard import * from hikari.net.http_client import * +from hikari.net.shard import * from hikari.net.versions import * -__all__ = (codes.__all__ + errors.__all__ + shard.__all__ + http_client.__all__ + versions.__all__) +__all__ = codes.__all__ + errors.__all__ + shard.__all__ + http_client.__all__ + versions.__all__ diff --git a/hikari/net/shard.py b/hikari/net/shard.py index 35a51cc5c8..f913a5df26 100644 --- a/hikari/net/shard.py +++ b/hikari/net/shard.py @@ -630,6 +630,9 @@ async def connect(self, client_session_type=aiohttp.ClientSession) -> None: # Kill other running tasks now. for pending_task in pending_tasks: pending_task.cancel() + with contextlib.suppress(Exception): + # Clear any pending exception to prevent a nasty console message. + pending_task.result() ex = completed.pop().exception() diff --git a/tests/hikari/core/test_gateway_bot.py b/tests/hikari/core/test_gateway_entities.py similarity index 82% rename from tests/hikari/core/test_gateway_bot.py rename to tests/hikari/core/test_gateway_entities.py index 886c74e202..c0f9dfaf2f 100644 --- a/tests/hikari/core/test_gateway_bot.py +++ b/tests/hikari/core/test_gateway_entities.py @@ -21,7 +21,7 @@ import cymock as mock import pytest -from hikari.core import gateway_bot +from hikari.core import gateway_entities from tests.hikari import _helpers @@ -32,7 +32,7 @@ def test_session_start_limit_payload(): class TestSessionStartLimit: def test_deserialize(self, test_session_start_limit_payload): - session_start_limit_obj = gateway_bot.SessionStartLimit.deserialize(test_session_start_limit_payload) + session_start_limit_obj = gateway_entities.SessionStartLimit.deserialize(test_session_start_limit_payload) assert session_start_limit_obj.total == 1000 assert session_start_limit_obj.remaining == 991 assert session_start_limit_obj.reset_after == datetime.timedelta(milliseconds=14170186) @@ -44,14 +44,14 @@ def test_gateway_bot_payload(self, test_session_start_limit_payload): return {"url": "wss://gateway.discord.gg", "shards": 1, "session_start_limit": test_session_start_limit_payload} def test_deserialize(self, test_gateway_bot_payload, test_session_start_limit_payload): - mock_session_start_limit = mock.MagicMock(gateway_bot.SessionStartLimit) + mock_session_start_limit = mock.MagicMock(gateway_entities.SessionStartLimit) with _helpers.patch_marshal_attr( - gateway_bot.GatewayBot, + gateway_entities.GatewayBot, "session_start_limit", - deserializer=gateway_bot.SessionStartLimit.deserialize, + deserializer=gateway_entities.SessionStartLimit.deserialize, return_value=mock_session_start_limit, ) as patched_start_limit_deserializer: - gateway_bot_obj = gateway_bot.GatewayBot.deserialize(test_gateway_bot_payload) + gateway_bot_obj = gateway_entities.GatewayBot.deserialize(test_gateway_bot_payload) patched_start_limit_deserializer.assert_called_once_with(test_session_start_limit_payload) assert gateway_bot_obj.session_start_limit is mock_session_start_limit assert gateway_bot_obj.url == "wss://gateway.discord.gg" diff --git a/tests/hikari/net/test_shard.py b/tests/hikari/net/test_shard.py index 2979b090ca..0fa5b5908d 100644 --- a/tests/hikari/net/test_shard.py +++ b/tests/hikari/net/test_shard.py @@ -186,7 +186,7 @@ class TestGatewayClientAiohttpClientSessionKwargsProperty: async def test_right_stuff_is_included(self): connector = mock.MagicMock() - client = shard.ShardConnection(url="...", token="...", connector=connector, ) + client = shard.ShardConnection(url="...", token="...", connector=connector,) assert client._cs_init_kwargs == dict(connector=connector) From 15774e1f9b5c948635fbfe16c1468ab0fce6e8e7 Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Tue, 24 Mar 2020 11:40:33 +0000 Subject: [PATCH 037/922] Began moving aio.EventDelegate to dispatcher core module and migrating format to use event objects. --- hikari/core/dispatcher.py | 247 ++++++++++++++ hikari/core/events.py | 23 ++ hikari/core/gateway_client.py | 1 + hikari/internal_utilities/aio.py | 269 --------------- tests/hikari/core/test_dispatcher.py | 353 ++++++++++++++++++++ tests/hikari/core/test_embeds.py | 3 +- tests/hikari/core/test_messages.py | 9 +- tests/hikari/core/test_webhook.py | 3 +- tests/hikari/internal_utilities/test_aio.py | 323 ------------------ 9 files changed, 630 insertions(+), 601 deletions(-) create mode 100644 hikari/core/dispatcher.py create mode 100644 tests/hikari/core/test_dispatcher.py diff --git a/hikari/core/dispatcher.py b/hikari/core/dispatcher.py new file mode 100644 index 0000000000..3edb0b002a --- /dev/null +++ b/hikari/core/dispatcher.py @@ -0,0 +1,247 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# Copyright © Nekoka.tt 2019-2020 +# +# This file is part of Hikari. +# +# Hikari is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Hikari is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Hikari. If not, see . +"""Event dispatcher implementation.""" +import asyncio +import logging + +import typing +import weakref + +from hikari.internal_utilities import aio +from hikari.internal_utilities import assertions +from hikari.internal_utilities import loggers + +from hikari.core import events + + +class EventDelegate: + """Handles storing and dispatching to event listeners and one-time event + waiters. + + Event listeners once registered will be stored until they are manually + removed. Each time an event is dispatched with a matching name, they will + be invoked on the event loop. + + One-time event waiters are futures that will be completed when a matching + event is fired. Once they are matched, they are removed from the listener + list. Each listener has a corresponding predicate that is invoked prior + to completing the waiter, with any event parameters being passed to the + predicate. If the predicate returns False, the waiter is not completed. This + allows filtering of certain events and conditions in a procedural way. + """ + + #: The logger used to write log messages. + #: + #: :type: :obj:`logging.Logger` + logger: logging.Logger + + __slots__ = ("exception_event", "logger", "_listeners", "_waiters") + + def __init__(self) -> None: + self._listeners = {} + self._waiters = {} + self.logger = loggers.get_named_logger(self) + + def add(self, event: typing.Type[events.HikariEvent], coroutine_function: aio.CoroutineFunctionT) -> None: + """Register a new event callback to a given event name. + + Parameters + ---------- + event : :obj:`typing.Type` [ :obj:`events.HikariEvent` ] + The event to register to. + coroutine_function + The event callback to invoke when this event is fired. + + Raises + ------ + :obj:`TypeError` + If ``coroutine_function`` is not a coroutine. + """ + assertions.assert_that( + asyncio.iscoroutinefunction(coroutine_function), "You must subscribe a coroutine function only", TypeError + ) + if event not in self._listeners: + self._listeners[event] = [] + self._listeners[event].append(coroutine_function) + + def remove(self, name: str, coroutine_function: aio.CoroutineFunctionT) -> None: + """Remove the given coroutine function from the handlers for the given event. + + The name is mandatory to enable supporting registering the same event callback for multiple event types. + + Parameters + ---------- + name : :obj:`str` + The event to remove from. + coroutine_function + The event callback to remove. + """ + if name in self._listeners and coroutine_function in self._listeners[name]: + if len(self._listeners[name]) - 1 == 0: + del self._listeners[name] + else: + self._listeners[name].remove(coroutine_function) + + # Do not add an annotation here, it will mess with type hints in PyCharm which can lead to + # confusing telepathy comments to the user. + def dispatch(self, event: events.HikariEvent): + """Dispatch a given event to all listeners and waiters that are + applicable. + + Parameters + ---------- + event : :obj:`events.HikariEvent` + The event to dispatch. + + Returns + ------- + :obj:`asyncio.Future` + This may be a gathering future of the callbacks to invoke, or it may + be a completed future object. Regardless, this result will be + scheduled on the event loop automatically, and does not need to be + awaited. Awaiting this future will await completion of all invoked + event handlers. + """ + event_t = type(event) + + if event_t in self._waiters: + for future, predicate in tuple(self._waiters[event_t].items()): + try: + if predicate(event): + future.set_result(event) + del self._waiters[event_t][future] + except Exception as ex: + future.set_exception(ex) + del self._waiters[event_t][future] + + if not self._waiters[event_t]: + del self._waiters[event_t] + + # Hack to stop PyCharm saying you need to await this function when you do not need to await + # it. + future: typing.Any + + if event_t in self._listeners: + coros = (self._catch(callback, event) for callback in self._listeners[event_t]) + future = asyncio.gather(*coros) + else: + future = aio.completed_future() + + return future + + async def _catch(self, callback, event): + try: + return await callback(event) + except Exception as ex: + # Pop the top-most frame to remove this _catch call. + # The user doesn't need it in their traceback. + ex.__traceback__ = ex.__traceback__.tb_next + self.handle_exception(ex, event, callback) + + def handle_exception( + self, exception: Exception, event: events.HikariEvent, callback: aio.CoroutineFunctionT + ) -> None: + """Function that is passed any exception. This allows users to override + this with a custom implementation if desired. + + This implementation will check to see if the event that triggered the + exception is an exception event. If this exceptino was caused by the + ``exception_event``, then nothing is dispatched (thus preventing + an exception handler recursively re-triggering itself). Otherwise, an + ``exception_event`` is dispatched with a + :obj:`EventExceptionContext` as the sole parameter. + + Parameters + ---------- + exception: :obj:`Exception` + The exception that triggered this call. + event: :obj:`events.Event` + The event that was being dispatched. + callback + The callback that threw the exception. + """ + # Do not recurse if a dodgy exception handler is added. + if not isinstance(event, events.ExceptionEvent): + self.logger.exception( + 'Exception occurred in handler for event "%s"', type(event).__name__, exc_info=exception) + self.dispatch(events.ExceptionEvent(exception=exception, event=event, callback=callback)) + else: + self.logger.exception( + 'Exception occurred in handler for event "%s", and the exception has been dropped', + type(event).__name__, + exc_info=exception, + ) + + def wait_for( + self, + event_type: typing.Type[events.HikariEvent], + *, + timeout: typing.Optional[float], + predicate: typing.Callable[..., bool], + ) -> asyncio.Future: + """Given an event name, wait for the event to occur once, then return + the arguments that accompanied the event as the result. + + Events can be filtered using a given predicate function. If unspecified, + the first event of the given name will be a match. + + Every event that matches the event name that the bot receives will be + checked. Thus, if you need to wait for events in a specific guild or + channel, or from a specific person, you want to give a predicate that + checks this. + + Parameters + ---------- + event_type : :obj:`typing.Type` [ :obj:`events.HikariEvent` ] + The name of the event to wait for. + timeout : :obj:`float`, optional + The timeout to wait for before cancelling and raising an + :obj:`asyncio.TimeoutError` instead. If this is `None`, this will + wait forever. Care must be taken if you use `None` as this may + leak memory if you do this from an event listener that gets + repeatedly called. If you want to do this, you should consider + using an event listener instead of this function. + predicate : :obj:`typing.Callable` [ ..., :obj:`bool` ] + A function that takes the arguments for the event and returns True + if it is a match, or False if it should be ignored. + This cannot be a coroutine function. + + Returns + ------- + :obj:`asyncio.Future` + A future that when awaited will provide a the arguments passed to the + first matching event. If no arguments are passed to the event, then + `None` is the result. If one argument is passed to the event, then + that argument is the result, otherwise a tuple of arguments is the + result instead. + + Note + ---- + Awaiting this result will raise an :obj:`asyncio.TimeoutError` if the timeout + is hit and no match is found. If the predicate throws any exception, + this is raised immediately. + """ + future = asyncio.get_event_loop().create_future() + if event_type not in self._waiters: + # This is used as a weakref dict to allow automatically tidying up + # any future that falls out of scope entirely. + self._waiters[event_type] = weakref.WeakKeyDictionary() + self._waiters[event_type][future] = predicate + # noinspection PyTypeChecker + return asyncio.ensure_future(asyncio.wait_for(future, timeout)) diff --git a/hikari/core/events.py b/hikari/core/events.py index 7492e849af..c14cc8ec30 100644 --- a/hikari/core/events.py +++ b/hikari/core/events.py @@ -21,6 +21,7 @@ __all__ = [ "HikariEvent", + "ExceptionEvent", "ConnectedEvent", "DisconnectedEvent", "ReconnectedEvent", @@ -81,6 +82,7 @@ from hikari.core import snowflakes from hikari.core import users from hikari.core import voices +from hikari.internal_utilities import aio from hikari.internal_utilities import dates from hikari.internal_utilities import marshaller @@ -93,6 +95,27 @@ class HikariEvent(entities.HikariEntity): """The base class that all events inherit from.""" +# Synthetic event, is not deserialized, and is produced by the dispatcher. +@attr.attrs(slots=True, auto_attribs=True) +class ExceptionEvent(HikariEvent): + """Descriptor for an exception thrown while processing an event.""" + + #: The exception that was raised. + #: + #: :type: :obj:`Exception` + exception: Exception + + #: The event that was being invoked when the exception occurred. + #: + #: :type: :obj:`HikariEvent` + event: HikariEvent + + #: The event that was being invoked when the exception occurred. + #: + #: :type: :obj`typing.Callable` [ [ :obj:`HikariEvent` ], ``None`` ] + callback: aio.CoroutineFunctionT + + # Synthetic event, is not deserialized @attr.s(slots=True, auto_attribs=True) class ConnectedEvent(HikariEvent): diff --git a/hikari/core/gateway_client.py b/hikari/core/gateway_client.py index 0222e32528..4161a90af1 100644 --- a/hikari/core/gateway_client.py +++ b/hikari/core/gateway_client.py @@ -407,6 +407,7 @@ def __init__( ) -> None: self.logger = loggers.get_named_logger(self) self.config = config + self.event_dispatcher = aio.EventDelegate() self.shards: typing.Dict[int, ShardT] = { shard_id: shard_type(shard_id, config, self._low_level_dispatch, url) for shard_id in config.shard_config.shard_ids diff --git a/hikari/internal_utilities/aio.py b/hikari/internal_utilities/aio.py index 1cf7bdffe8..9cd4afadc5 100644 --- a/hikari/internal_utilities/aio.py +++ b/hikari/internal_utilities/aio.py @@ -20,19 +20,11 @@ __all__ = [ "CoroutineFunctionT", "PartialCoroutineProtocolT", - "EventExceptionContext", - "EventDelegate", "completed_future", ] import asyncio -import dataclasses -import logging import typing -import weakref - -from hikari.internal_utilities import assertions -from hikari.internal_utilities import loggers ReturnT = typing.TypeVar("ReturnT", covariant=True) CoroutineFunctionT = typing.Callable[..., typing.Coroutine[typing.Any, typing.Any, ReturnT]] @@ -48,267 +40,6 @@ def __await__(self): ... -@dataclasses.dataclass(frozen=True) -class EventExceptionContext: - """A dataclass that contains information about where an exception was thrown from.""" - - __slots__ = ("event_name", "callback", "args", "exception") - - #: The name of the event that triggered the exception. - #: - #: :type: :obj:`str` - event_name: str - - #: The event handler that was being invoked. - #: - #: :type: :obj:`CoroutineFunctionT` - callback: CoroutineFunctionT - - #: The arguments passed to the event callback. - #: - #: :type: :obj:`typing.Sequence` [ :obj:`typing.Any` ] - args: typing.Sequence[typing.Any] - - #: The exception that was thrown. - #: - #: :type: :obj:`Exception` - exception: Exception - - -class EventDelegate: - """Handles storing and dispatching to event listeners and one-time event - waiters. - - Event listeners once registered will be stored until they are manually - removed. Each time an event is dispatched with a matching name, they will - be invoked on the event loop. - - One-time event waiters are futures that will be completed when a matching - event is fired. Once they are matched, they are removed from the listener - list. Each listener has a corresponding predicate that is invoked prior - to completing the waiter, with any event parameters being passed to the - predicate. If the predicate returns False, the waiter is not completed. This - allows filtering of certain events and conditions in a procedural way. - - Parameters - ---------- - exception_event : :obj:`str` - The event to invoke if an exception is caught. - """ - - #: The event to invoke if an exception is caught. - #: - #: :type: :obj:`str` - exception_event: str - - #: The logger used to write log messages. - #: - #: :type: :obj:`logging.Logger` - logger: logging.Logger - - __slots__ = ("exception_event", "logger", "_listeners", "_waiters") - - def __init__(self, exception_event: str) -> None: - self._listeners = {} - self._waiters = {} - self.logger = loggers.get_named_logger(self) - self.exception_event = exception_event - - def add(self, name: str, coroutine_function: CoroutineFunctionT) -> None: - """Register a new event callback to a given event name. - - Parameters - ---------- - name : :obj:`str` - The name of the event to register to. - coroutine_function - The event callback to invoke when this event is fired. - - Raises - ------ - :obj:`TypeError` - If ``coroutine_function`` is not a coroutine. - """ - assertions.assert_that( - asyncio.iscoroutinefunction(coroutine_function), "You must subscribe a coroutine function only", TypeError - ) - if name not in self._listeners: - self._listeners[name] = [] - self._listeners[name].append(coroutine_function) - - def remove(self, name: str, coroutine_function: CoroutineFunctionT) -> None: - """Remove the given coroutine function from the handlers for the given event. - - The name is mandatory to enable supporting registering the same event callback for multiple event types. - - Parameters - ---------- - name : :obj:`str` - The event to remove from. - coroutine_function - The event callback to remove. - """ - if name in self._listeners and coroutine_function in self._listeners[name]: - if len(self._listeners[name]) - 1 == 0: - del self._listeners[name] - else: - self._listeners[name].remove(coroutine_function) - - # Do not add an annotation here, it will mess with type hints in PyCharm which can lead to - # confusing telepathy comments to the user. - def dispatch(self, name: str, *args): - """Dispatch a given event to all listeners and waiters that are - applicable. - - Parameters - ---------- - name: :obj:`str` - The name of the event to dispatch. - *args - The parameters to pass to the event callback. - - Returns - ------- - :obj:`asyncio.Future` - This may be a gathering future of the callbacks to invoke, or it may - be a completed future object. Regardless, this result will be - scheduled on the event loop automatically, and does not need to be - awaited. Awaiting this future will await completion of all invoked - event handlers. - """ - if name in self._waiters: - # Unwrap single or no argument events. - if len(args) == 1: - waiter_result_args = args[0] - elif not args: - waiter_result_args = None - else: - waiter_result_args = args - - for future, predicate in tuple(self._waiters[name].items()): - try: - if predicate(*args): - future.set_result(waiter_result_args) - del self._waiters[name][future] - except Exception as ex: - future.set_exception(ex) - del self._waiters[name][future] - - if not self._waiters[name]: - del self._waiters[name] - - # Hack to stop PyCharm saying you need to await this function when you do not need to await - # it. - future: typing.Any - - if name in self._listeners: - coros = (self._catch(callback, name, args) for callback in self._listeners[name]) - future = asyncio.gather(*coros) - else: - future = completed_future() - - return future - - async def _catch(self, callback, name, args): - try: - return await callback(*args) - except Exception as ex: - # Pop the top-most frame to remove this _catch call. - # The user doesn't need it in their traceback. - ex.__traceback__ = ex.__traceback__.tb_next - self.handle_exception(ex, name, args, callback) - - def handle_exception( - self, exception: Exception, event_name: str, args: typing.Sequence[typing.Any], callback: CoroutineFunctionT - ) -> None: - """Function that is passed any exception. This allows users to override - this with a custom implementation if desired. - - This implementation will check to see if the event that triggered the - exception is an exception event. If this exceptino was caused by the - ``exception_event``, then nothing is dispatched (thus preventing - an exception handler recursively re-triggering itself). Otherwise, an - ``exception_event`` is dispatched with a - :obj:`EventExceptionContext` as the sole parameter. - - Parameters - ---------- - exception: :obj:`Exception` - The exception that triggered this call. - event_name: :obj:`str` - The name of the event that triggered the exception. - args: :obj:`typing.Sequence` [ :obj:`typing.Any` ] - The arguments passed to the event that threw an exception. - callback - The callback that threw the exception. - """ - # Do not recurse if a dodgy exception handler is added. - if event_name != self.exception_event: - self.logger.exception('Exception occurred in handler for event "%s"', event_name, exc_info=exception) - ctx = EventExceptionContext(event_name, callback, args, exception) - self.dispatch(self.exception_event, ctx) - else: - self.logger.exception( - 'Exception occurred in handler for event "%s", and the exception has been dropped', - event_name, - exc_info=exception, - ) - - def wait_for( - self, name: str, *, timeout: typing.Optional[float], predicate: typing.Callable[..., bool] - ) -> asyncio.Future: - """Given an event name, wait for the event to occur once, then return - the arguments that accompanied the event as the result. - - Events can be filtered using a given predicate function. If unspecified, - the first event of the given name will be a match. - - Every event that matches the event name that the bot receives will be - checked. Thus, if you need to wait for events in a specific guild or - channel, or from a specific person, you want to give a predicate that - checks this. - - Parameters - ---------- - name : :obj:`str` - The name of the event to wait for. - timeout : :obj:`float`, optional - The timeout to wait for before cancelling and raising an - :obj:`asyncio.TimeoutError` instead. If this is `None`, this will - wait forever. Care must be taken if you use `None` as this may - leak memory if you do this from an event listener that gets - repeatedly called. If you want to do this, you should consider - using an event listener instead of this function. - predicate : :obj:`typing.Callable` [ ..., :obj:`bool` ] - A function that takes the arguments for the event and returns True - if it is a match, or False if it should be ignored. - This cannot be a coroutine function. - - Returns - ------- - :obj:`asyncio.Future` - A future that when awaited will provide a the arguments passed to the - first matching event. If no arguments are passed to the event, then - `None` is the result. If one argument is passed to the event, then - that argument is the result, otherwise a tuple of arguments is the - result instead. - - Note - ---- - Awaiting this result will raise an :obj:`asyncio.TimeoutError` if the timeout - is hit and no match is found. If the predicate throws any exception, - this is raised immediately. - """ - future = asyncio.get_event_loop().create_future() - if name not in self._waiters: - # This is used as a weakref dict to allow automatically tidying up - # any future that falls out of scope entirely. - self._waiters[name] = weakref.WeakKeyDictionary() - self._waiters[name][future] = predicate - # noinspection PyTypeChecker - return asyncio.ensure_future(asyncio.wait_for(future, timeout)) - - def completed_future(result: typing.Any = None) -> asyncio.Future: """Create a future on the current running loop that is completed, then return it. diff --git a/tests/hikari/core/test_dispatcher.py b/tests/hikari/core/test_dispatcher.py new file mode 100644 index 0000000000..2446c9d071 --- /dev/null +++ b/tests/hikari/core/test_dispatcher.py @@ -0,0 +1,353 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# Copyright © Nekoka.tt 2019-2020 +# +# This file is part of Hikari. +# +# Hikari is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Hikari is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Hikari. If not, see . +import asyncio +from unittest import mock + +import pytest + +from hikari.core import dispatcher +from hikari.core import events +from hikari.internal_utilities import aio +from tests.hikari import _helpers + + +class TestEvent1(events.HikariEvent): + ... + + +class TestEvent2(events.HikariEvent): + ... + + +class TestEvent3(events.HikariEvent): + ... + + +@pytest.mark.skip("TODO: fixme") +class TestEventDelegate: + EXCEPTION_EVENT = "exception" + + @pytest.fixture + def delegate(self): + return _helpers.unslot_class(dispatcher.EventDelegate)() + + # noinspection PyTypeChecker + @_helpers.assert_raises(type_=TypeError) + def test_add_not_coroutine_function(self, delegate): + delegate.add("foo", lambda: None) + + def test_add_coroutine_function_when_no_others_with_name(self, delegate): + async def coro_fn(): + pass + + delegate.add("foo", coro_fn) + assert coro_fn in delegate._listeners["foo"] + + def test_add_coroutine_function_when_list_exists(self, delegate): + async def coro_fn1(): + pass + + async def coro_fn2(): + pass + + delegate.add("foo", coro_fn1) + delegate.add("foo", coro_fn2) + assert coro_fn1 in delegate._listeners["foo"] + assert coro_fn2 in delegate._listeners["foo"] + + def test_remove_non_existing_mux_list(self, delegate): + async def remove_this(): + pass + + # should not raise + delegate.remove("foo", remove_this) + + def test_remove_non_existing_mux(self, delegate): + delegate._listeners["foo"] = [] + + async def remove_this(): + pass + + # should not raise + delegate.remove("foo", remove_this) + + def test_remove_when_list_left_empty_removes_key(self, delegate): + async def remove_this(): + pass + + delegate._listeners["foo"] = [remove_this] + + delegate.remove("foo", remove_this) + + assert "foo" not in delegate._listeners + + def test_remove_when_list_not_left_empty_removes_coroutine_function(self, delegate): + async def remove_this(): + pass + + delegate._listeners["foo"] = [remove_this, remove_this] + + delegate.remove("foo", remove_this) + + assert delegate._listeners["foo"] == [remove_this] + + def test_dispatch_to_existing_muxes(self, delegate): + delegate._catch = mock.MagicMock() + mock_coro_fn1 = mock.MagicMock() + mock_coro_fn2 = mock.MagicMock() + mock_coro_fn3 = mock.MagicMock() + + delegate._listeners["foo"] = [mock_coro_fn1, mock_coro_fn2] + delegate._listeners["bar"] = [mock_coro_fn3] + + args = ("a", "b", "c") + + with mock.patch("asyncio.gather") as gather: + delegate.dispatch("foo", *args) + gather.assert_called_once_with( + delegate._catch(mock_coro_fn1, "foo", args), delegate._catch(mock_coro_fn2, "foo", args) + ) + + def test_dispatch_to_non_existant_muxes(self, delegate): + # Should not throw. + delegate.dispatch("foo", "a", "b", "c") + + @pytest.mark.asyncio + async def test_dispatch_is_awaitable_if_nothing_is_invoked(self, delegate): + coro_fn = mock.AsyncMock() + + delegate.add("foo", coro_fn) + await delegate.dispatch("bar") + + @pytest.mark.asyncio + async def test_dispatch_is_awaitable_if_something_is_invoked(self, delegate): + coro_fn = mock.AsyncMock() + + delegate.add("foo", coro_fn) + await delegate.dispatch("foo") + + @pytest.mark.asyncio + @pytest.mark.parametrize("predicate_return", (True, False)) + @pytest.mark.parametrize( + ("in_event_args", "expected_result"), [((), None,), ((12,), 12), ((12, 22, 33), (12, 22, 33))] + ) + @_helpers.timeout_after(1) + async def test_dispatch_awakens_matching_futures( + self, delegate, event_loop, predicate_return, in_event_args, expected_result + ): + future1 = event_loop.create_future() + future2 = event_loop.create_future() + future3 = event_loop.create_future() + future4 = event_loop.create_future() + + predicate1 = mock.MagicMock(return_value=predicate_return) + predicate2 = mock.MagicMock(return_value=predicate_return) + predicate3 = mock.MagicMock(return_value=True) + predicate4 = mock.MagicMock(return_value=False) + + delegate._waiters[TestEvent1] = {} + delegate._waiters[TestEvent2] = {future3: predicate3} + delegate._waiters[TestEvent1][future1] = predicate1 + delegate._waiters[TestEvent1][future2] = predicate2 + # Shouldn't be invoked, as the predicate is always false-returning. + delegate._waiters[TestEvent1][future4] = predicate4 + + event_ctx = TestEvent1() + await delegate.dispatch(event_ctx) + + assert future1.done() is predicate_return + predicate1.assert_called_once_with(event_ctx) + assert future2.done() is predicate_return + predicate2.assert_called_once_with(event_ctx) + assert future3.done() is False + predicate3.assert_not_called() + assert future4.done() is False + predicate4.assert_called_once_with(event_ctx) + + if predicate_return: + assert await future1 == expected_result + assert await future2 == expected_result + + @pytest.mark.asyncio + @pytest.mark.parametrize("predicate_return", (True, False)) + @pytest.mark.parametrize( + ("in_event_args", "expected_result"), [((), None,), ((12,), 12), ((12, 22, 33), (12, 22, 33))] + ) + @_helpers.timeout_after(1) + async def test_dispatch_removes_awoken_future( + self, delegate, event_loop, predicate_return, in_event_args, expected_result + ): + future = event_loop.create_future() + + predicate = mock.MagicMock() + + delegate._waiters["foobar"] = {} + delegate._waiters["foobar"][future] = predicate + # Add a second future that never gets hit so the weakref map is not dropped from being + # empty. + delegate._waiters["foobar"][event_loop.create_future()] = lambda *_: False + + await delegate.dispatch("foobar", *in_event_args) + predicate.assert_called_once_with(*in_event_args) + predicate.reset_mock() + + await delegate.dispatch("foobar", *in_event_args) + predicate.assert_not_called() + assert future not in delegate._waiters["foobar"] + + @pytest.mark.asyncio + @_helpers.timeout_after(1) + async def test_dispatch_returns_exception_to_caller(self, delegate, event_loop): + predicate1 = mock.MagicMock(side_effect=RuntimeError()) + predicate2 = mock.MagicMock(return_value=False) + + future1 = event_loop.create_future() + future2 = event_loop.create_future() + + delegate._waiters["foobar"] = {} + delegate._waiters["foobar"][future1] = predicate1 + delegate._waiters["foobar"][future2] = predicate2 + + await delegate.dispatch("foobar", object(), object(), object()) + + try: + await future1 + assert False, "No RuntimeError propagated :(" + except RuntimeError: + pass + + # Isn't done, should raise InvalidStateError. + try: + future2.exception() + assert False, "this future should still be running but isn't!" + except asyncio.InvalidStateError: + pass + + @pytest.mark.asyncio + async def test_waiter_map_deleted_if_already_empty(self, delegate): + delegate._waiters[TestEvent1] = {} + await delegate.dispatch(TestEvent1()) + assert TestEvent1 not in delegate._waiters + + @pytest.mark.asyncio + async def test_waiter_map_deleted_if_made_empty_during_this_dispatch(self, delegate): + delegate._waiters[TestEvent1] = {mock.MagicMock(): mock.MagicMock(return_value=True)} + await delegate.dispatch(TestEvent1()) + assert TestEvent1 not in delegate._waiters + + @pytest.mark.asyncio + async def test_waiter_map_not_deleted_if_not_empty(self, delegate): + delegate._waiters["foobar"] = {mock.MagicMock(): mock.MagicMock(return_value=False)} + await delegate.dispatch("foobar") + assert "foobar" in delegate._waiters + + @pytest.mark.asyncio + @_helpers.timeout_after(2) + async def test_wait_for_returns_event(self, delegate, in_event_args, expected_result): + predicate = mock.MagicMock(return_value=True) + future = delegate.wait_for(TestEvent1, timeout=5, predicate=predicate) + + event = TestEvent1() + await delegate.dispatch(event) + + await asyncio.sleep(0.5) + + assert future.done() + actual_result = await future + assert actual_result == expected_result + predicate.assert_called_once_with(*in_event_args) + + @pytest.mark.asyncio + @pytest.mark.parametrize( + ("in_event_args", "expected_result"), [((), None,), ((12,), 12), ((12, 22, 33), (12, 22, 33))] + ) + @_helpers.timeout_after(2) + async def test_wait_for_returns_matching_event_args_when_invoked_but_no_predicate_match( + self, delegate, in_event_args, expected_result + ): + predicate = mock.MagicMock(return_value=False) + future = delegate.wait_for("foobar", timeout=5, predicate=predicate) + await delegate.dispatch("foobar", *in_event_args) + + await asyncio.sleep(0.5) + + assert not future.done() + predicate.assert_called_once_with(*in_event_args) + + @pytest.mark.asyncio + @_helpers.assert_raises(type_=asyncio.TimeoutError) + async def test_wait_for_hits_timeout_and_raises(self, delegate): + predicate = mock.MagicMock(return_value=False) + await delegate.wait_for("foobar", timeout=1, predicate=predicate) + assert False, "event was marked as succeeded when it shouldn't have been" + + @pytest.mark.asyncio + @_helpers.timeout_after(2) + @_helpers.assert_raises(type_=RuntimeError) + async def test_wait_for_raises_predicate_errors(self, delegate): + predicate = mock.MagicMock(side_effect=RuntimeError) + future = delegate.wait_for("foobar", timeout=1, predicate=predicate) + await delegate.dispatch("foobar", object()) + await future + + @pytest.mark.asyncio + @pytest.mark.parametrize("predicate_side_effect", (True, False, RuntimeError())) + @_helpers.timeout_after(5) + @_helpers.assert_raises(type_=asyncio.TimeoutError) + async def test_other_events_in_same_waiter_event_name_do_not_awaken_us( + self, delegate, predicate_side_effect, event_loop + ): + delegate._waiters["foobar"] = {event_loop.create_future(): mock.MagicMock(side_effect=predicate_side_effect)} + + future = delegate.wait_for("foobar", timeout=1, predicate=mock.MagicMock(return_value=False)) + + await asyncio.gather(future, *(delegate.dispatch("foobar") for _ in range(5))) + + @pytest.mark.asyncio + async def test_catch_happy_path(self, delegate): + callback = mock.AsyncMock() + delegate.handle_exception = mock.MagicMock() + await delegate._catch(callback, "wubalubadubdub", ("blep1", "blep2", "blep3")) + callback.assert_awaited_once_with("blep1", "blep2", "blep3") + delegate.handle_exception.assert_not_called() + + @pytest.mark.asyncio + async def test_catch_sad_path(self, delegate): + ex = RuntimeError() + callback = mock.AsyncMock(side_effect=ex) + delegate.handle_exception = mock.MagicMock() + await delegate._catch(callback, "wubalubadubdub", ("blep1", "blep2", "blep3")) + delegate.handle_exception.assert_called_once_with(ex, "wubalubadubdub", ("blep1", "blep2", "blep3"), callback) + + def test_handle_exception_dispatches_exception_event_with_context(self, delegate): + delegate.dispatch = mock.MagicMock() + + ex = RuntimeError() + event = TestEvent1() + callback = mock.AsyncMock() + + delegate.handle_exception(ex, event, callback) + + expected_ctx = events.ExceptionEvent(..., ..., ...) + delegate.dispatch.assert_called_once_with(expected_ctx) + + def test_handle_exception_will_not_recursively_invoke_exception_handler_event(self, delegate): + delegate.dispatch = mock.MagicMock() + delegate.handle_exception(RuntimeError(), events.ExceptionEvent(..., ..., ...), mock.AsyncMock()) + delegate.dispatch.assert_not_called() diff --git a/tests/hikari/core/test_embeds.py b/tests/hikari/core/test_embeds.py index dce48c00d8..aeb376a7a4 100644 --- a/tests/hikari/core/test_embeds.py +++ b/tests/hikari/core/test_embeds.py @@ -21,9 +21,8 @@ import cymock as mock import pytest -from hikari.core import embeds from hikari.core import colors -from hikari.internal_utilities import dates +from hikari.core import embeds from tests.hikari import _helpers diff --git a/tests/hikari/core/test_messages.py b/tests/hikari/core/test_messages.py index 409d6de147..08b21c2567 100644 --- a/tests/hikari/core/test_messages.py +++ b/tests/hikari/core/test_messages.py @@ -21,13 +21,12 @@ import cymock as mock import pytest -from hikari.core import messages -from hikari.core import users -from hikari.core import guilds -from hikari.core import emojis from hikari.core import embeds +from hikari.core import emojis +from hikari.core import guilds +from hikari.core import messages from hikari.core import oauth2 -from hikari.internal_utilities import dates +from hikari.core import users from tests.hikari import _helpers diff --git a/tests/hikari/core/test_webhook.py b/tests/hikari/core/test_webhook.py index 77c612ac47..bda21283e7 100644 --- a/tests/hikari/core/test_webhook.py +++ b/tests/hikari/core/test_webhook.py @@ -17,10 +17,9 @@ # You should have received a copy of the GNU Lesser General Public License # along ith Hikari. If not, see . import cymock as mock -import pytest -from hikari.core import webhooks from hikari.core import users +from hikari.core import webhooks from tests.hikari import _helpers diff --git a/tests/hikari/internal_utilities/test_aio.py b/tests/hikari/internal_utilities/test_aio.py index be29843cd2..00bce93729 100644 --- a/tests/hikari/internal_utilities/test_aio.py +++ b/tests/hikari/internal_utilities/test_aio.py @@ -18,11 +18,9 @@ # along with Hikari. If not, see . import asyncio -import cymock as mock import pytest from hikari.internal_utilities import aio -from tests.hikari import _helpers class CoroutineStub: @@ -58,327 +56,6 @@ def test_coro_stub_neq(self): assert CoroutineStub(9, 18, x=27) != CoroutineStub(9, 18, x=36) -class TestEventDelegate: - EXCEPTION_EVENT = "exception" - - @pytest.fixture - def delegate(self): - return _helpers.unslot_class(aio.EventDelegate)(self.EXCEPTION_EVENT) - - # noinspection PyTypeChecker - @_helpers.assert_raises(type_=TypeError) - def test_add_not_coroutine_function(self, delegate): - delegate.add("foo", lambda: None) - - def test_add_coroutine_function_when_no_others_with_name(self, delegate): - async def coro_fn(): - pass - - delegate.add("foo", coro_fn) - assert coro_fn in delegate._listeners["foo"] - - def test_add_coroutine_function_when_list_exists(self, delegate): - async def coro_fn1(): - pass - - async def coro_fn2(): - pass - - delegate.add("foo", coro_fn1) - delegate.add("foo", coro_fn2) - assert coro_fn1 in delegate._listeners["foo"] - assert coro_fn2 in delegate._listeners["foo"] - - def test_remove_non_existing_mux_list(self, delegate): - async def remove_this(): - pass - - # should not raise - delegate.remove("foo", remove_this) - - def test_remove_non_existing_mux(self, delegate): - delegate._listeners["foo"] = [] - - async def remove_this(): - pass - - # should not raise - delegate.remove("foo", remove_this) - - def test_remove_when_list_left_empty_removes_key(self, delegate): - async def remove_this(): - pass - - delegate._listeners["foo"] = [remove_this] - - delegate.remove("foo", remove_this) - - assert "foo" not in delegate._listeners - - def test_remove_when_list_not_left_empty_removes_coroutine_function(self, delegate): - async def remove_this(): - pass - - delegate._listeners["foo"] = [remove_this, remove_this] - - delegate.remove("foo", remove_this) - - assert delegate._listeners["foo"] == [remove_this] - - def test_dispatch_to_existing_muxes(self, delegate): - delegate._catch = mock.MagicMock() - mock_coro_fn1 = mock.MagicMock() - mock_coro_fn2 = mock.MagicMock() - mock_coro_fn3 = mock.MagicMock() - - delegate._listeners["foo"] = [mock_coro_fn1, mock_coro_fn2] - delegate._listeners["bar"] = [mock_coro_fn3] - - args = ("a", "b", "c") - - with mock.patch("asyncio.gather") as gather: - delegate.dispatch("foo", *args) - gather.assert_called_once_with( - delegate._catch(mock_coro_fn1, "foo", args), delegate._catch(mock_coro_fn2, "foo", args) - ) - - def test_dispatch_to_non_existant_muxes(self, delegate): - # Should not throw. - delegate.dispatch("foo", "a", "b", "c") - - @pytest.mark.asyncio - async def test_dispatch_is_awaitable_if_nothing_is_invoked(self, delegate): - coro_fn = mock.AsyncMock() - - delegate.add("foo", coro_fn) - await delegate.dispatch("bar") - - @pytest.mark.asyncio - async def test_dispatch_is_awaitable_if_something_is_invoked(self, delegate): - coro_fn = mock.AsyncMock() - - delegate.add("foo", coro_fn) - await delegate.dispatch("foo") - - @pytest.mark.asyncio - @pytest.mark.parametrize("predicate_return", (True, False)) - @pytest.mark.parametrize( - ("in_event_args", "expected_result"), [((), None,), ((12,), 12), ((12, 22, 33), (12, 22, 33))] - ) - @_helpers.timeout_after(1) - async def test_dispatch_awakens_matching_futures( - self, delegate, event_loop, predicate_return, in_event_args, expected_result - ): - future1 = event_loop.create_future() - future2 = event_loop.create_future() - future3 = event_loop.create_future() - future4 = event_loop.create_future() - - predicate1 = mock.MagicMock(return_value=predicate_return) - predicate2 = mock.MagicMock(return_value=predicate_return) - predicate3 = mock.MagicMock(return_value=True) - predicate4 = mock.MagicMock(return_value=False) - - delegate._waiters["foobar"] = {} - delegate._waiters["barbaz"] = {future3: predicate3} - delegate._waiters["foobar"][future1] = predicate1 - delegate._waiters["foobar"][future2] = predicate2 - # Shouldn't be invoked, as the predicate is always false-returning. - delegate._waiters["foobar"][future4] = predicate4 - - await delegate.dispatch("foobar", *in_event_args) - - assert future1.done() is predicate_return - predicate1.assert_called_once_with(*in_event_args) - assert future2.done() is predicate_return - predicate2.assert_called_once_with(*in_event_args) - assert future3.done() is False - predicate3.assert_not_called() - assert future4.done() is False - predicate4.assert_called_once_with(*in_event_args) - - if predicate_return: - assert await future1 == expected_result - assert await future2 == expected_result - - @pytest.mark.asyncio - @pytest.mark.parametrize("predicate_return", (True, False)) - @pytest.mark.parametrize( - ("in_event_args", "expected_result"), [((), None,), ((12,), 12), ((12, 22, 33), (12, 22, 33))] - ) - @_helpers.timeout_after(1) - async def test_dispatch_removes_awoken_future( - self, delegate, event_loop, predicate_return, in_event_args, expected_result - ): - future = event_loop.create_future() - - predicate = mock.MagicMock() - - delegate._waiters["foobar"] = {} - delegate._waiters["foobar"][future] = predicate - # Add a second future that never gets hit so the weakref map is not dropped from being - # empty. - delegate._waiters["foobar"][event_loop.create_future()] = lambda *_: False - - await delegate.dispatch("foobar", *in_event_args) - predicate.assert_called_once_with(*in_event_args) - predicate.reset_mock() - - await delegate.dispatch("foobar", *in_event_args) - predicate.assert_not_called() - assert future not in delegate._waiters["foobar"] - - @pytest.mark.asyncio - @_helpers.timeout_after(1) - async def test_dispatch_returns_exception_to_caller(self, delegate, event_loop): - predicate1 = mock.MagicMock(side_effect=RuntimeError()) - predicate2 = mock.MagicMock(return_value=False) - - future1 = event_loop.create_future() - future2 = event_loop.create_future() - - delegate._waiters["foobar"] = {} - delegate._waiters["foobar"][future1] = predicate1 - delegate._waiters["foobar"][future2] = predicate2 - - await delegate.dispatch("foobar", object(), object(), object()) - - try: - await future1 - assert False, "No RuntimeError propagated :(" - except RuntimeError: - pass - - # Isn't done, should raise InvalidStateError. - try: - future2.exception() - assert False, "this future should still be running but isn't!" - except asyncio.InvalidStateError: - pass - - @pytest.mark.asyncio - async def test_waiter_map_deleted_if_already_empty(self, delegate): - delegate._waiters["foobar"] = {} - await delegate.dispatch("foobar") - assert "foobar" not in delegate._waiters - - @pytest.mark.asyncio - async def test_waiter_map_deleted_if_made_empty_during_this_dispatch(self, delegate): - delegate._waiters["foobar"] = {mock.MagicMock(): mock.MagicMock(return_value=True)} - await delegate.dispatch("foobar") - assert "foobar" not in delegate._waiters - - @pytest.mark.asyncio - async def test_waiter_map_not_deleted_if_not_empty(self, delegate): - delegate._waiters["foobar"] = {mock.MagicMock(): mock.MagicMock(return_value=False)} - await delegate.dispatch("foobar") - assert "foobar" in delegate._waiters - - @pytest.mark.asyncio - @pytest.mark.parametrize( - ("in_event_args", "expected_result"), [((), None,), ((12,), 12), ((12, 22, 33), (12, 22, 33))] - ) - @_helpers.timeout_after(2) - async def test_wait_for_returns_matching_event_args_when_invoked(self, delegate, in_event_args, expected_result): - predicate = mock.MagicMock(return_value=True) - future = delegate.wait_for("foobar", timeout=5, predicate=predicate) - - await delegate.dispatch("foobar", *in_event_args) - - await asyncio.sleep(0.5) - - assert future.done() - actual_result = await future - assert actual_result == expected_result - predicate.assert_called_once_with(*in_event_args) - - @pytest.mark.asyncio - @pytest.mark.parametrize( - ("in_event_args", "expected_result"), [((), None,), ((12,), 12), ((12, 22, 33), (12, 22, 33))] - ) - @_helpers.timeout_after(2) - async def test_wait_for_returns_matching_event_args_when_invoked_but_no_predicate_match( - self, delegate, in_event_args, expected_result - ): - predicate = mock.MagicMock(return_value=False) - future = delegate.wait_for("foobar", timeout=5, predicate=predicate) - await delegate.dispatch("foobar", *in_event_args) - - await asyncio.sleep(0.5) - - assert not future.done() - predicate.assert_called_once_with(*in_event_args) - - @pytest.mark.asyncio - @_helpers.assert_raises(type_=asyncio.TimeoutError) - async def test_wait_for_hits_timeout_and_raises(self, delegate): - predicate = mock.MagicMock(return_value=False) - await delegate.wait_for("foobar", timeout=1, predicate=predicate) - assert False, "event was marked as succeeded when it shouldn't have been" - - @pytest.mark.asyncio - @_helpers.timeout_after(2) - @_helpers.assert_raises(type_=RuntimeError) - async def test_wait_for_raises_predicate_errors(self, delegate): - predicate = mock.MagicMock(side_effect=RuntimeError) - future = delegate.wait_for("foobar", timeout=1, predicate=predicate) - await delegate.dispatch("foobar", object()) - await future - - @pytest.mark.asyncio - @pytest.mark.parametrize("predicate_side_effect", (True, False, RuntimeError())) - @_helpers.timeout_after(5) - @_helpers.assert_raises(type_=asyncio.TimeoutError) - async def test_other_events_in_same_waiter_event_name_do_not_awaken_us( - self, delegate, predicate_side_effect, event_loop - ): - delegate._waiters["foobar"] = {event_loop.create_future(): mock.MagicMock(side_effect=predicate_side_effect)} - - future = delegate.wait_for("foobar", timeout=1, predicate=mock.MagicMock(return_value=False)) - - await asyncio.gather(future, *(delegate.dispatch("foobar") for _ in range(5))) - - @pytest.mark.asyncio - async def test_catch_happy_path(self, delegate): - callback = mock.AsyncMock() - delegate.handle_exception = mock.MagicMock() - await delegate._catch(callback, "wubalubadubdub", ("blep1", "blep2", "blep3")) - callback.assert_awaited_once_with("blep1", "blep2", "blep3") - delegate.handle_exception.assert_not_called() - - @pytest.mark.asyncio - async def test_catch_sad_path(self, delegate): - ex = RuntimeError() - callback = mock.AsyncMock(side_effect=ex) - delegate.handle_exception = mock.MagicMock() - await delegate._catch(callback, "wubalubadubdub", ("blep1", "blep2", "blep3")) - delegate.handle_exception.assert_called_once_with(ex, "wubalubadubdub", ("blep1", "blep2", "blep3"), callback) - - def test_handle_exception_dispatches_exception_event_with_context(self, delegate): - delegate.dispatch = mock.MagicMock() - - ex = RuntimeError() - event_name = "foof" - args = ("aawwwww", "oooo", "ooooooo", "oo.") - callback = mock.AsyncMock() - - delegate.handle_exception(ex, event_name, args, callback) - - expected_ctx = aio.EventExceptionContext(event_name, callback, args, ex) - delegate.dispatch.assert_called_once_with(self.EXCEPTION_EVENT, expected_ctx) - - def test_handle_exception_will_not_recursively_invoke_exception_handler_event(self, delegate): - delegate.dispatch = mock.MagicMock() - - ex = RuntimeError() - event_name = self.EXCEPTION_EVENT - args = ("aawwwww", "oooo", "ooooooo", "oo.") - callback = mock.AsyncMock() - - delegate.handle_exception(ex, event_name, args, callback) - delegate.dispatch.assert_not_called() - - class TestCompletedFuture: @pytest.mark.asyncio @pytest.mark.parametrize("args", [(), (12,)]) From 2ead892362a4f47b190f00760df617973074cc33 Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Wed, 25 Mar 2020 10:28:56 +0000 Subject: [PATCH 038/922] Fixes #251: refactoring http_client/HTTPClient to rest/RestfulClient and removing base_http_client --- hikari/core/bot_client.py | 21 + hikari/core/dispatcher.py | 3 +- hikari/core/http_client.py | 18 + hikari/net/__init__.py | 8 +- hikari/net/base_http_client.py | 292 --- hikari/net/{http_client.py => rest.py} | 104 +- tests/hikari/net/test_base_http_client.py | 54 - .../net/{test_http_client.py => test_rest.py} | 1691 +++++++++-------- 8 files changed, 990 insertions(+), 1201 deletions(-) create mode 100644 hikari/core/bot_client.py create mode 100644 hikari/core/http_client.py delete mode 100644 hikari/net/base_http_client.py rename hikari/net/{http_client.py => rest.py} (97%) delete mode 100644 tests/hikari/net/test_base_http_client.py rename tests/hikari/net/{test_http_client.py => test_rest.py} (51%) diff --git a/hikari/core/bot_client.py b/hikari/core/bot_client.py new file mode 100644 index 0000000000..224a608a55 --- /dev/null +++ b/hikari/core/bot_client.py @@ -0,0 +1,21 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# Copyright © Nekoka.tt 2019-2020 +# +# This file is part of Hikari. +# +# Hikari is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Hikari is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Hikari. If not, see . +"""Application client. + +""" diff --git a/hikari/core/dispatcher.py b/hikari/core/dispatcher.py index 3edb0b002a..8eb072b70e 100644 --- a/hikari/core/dispatcher.py +++ b/hikari/core/dispatcher.py @@ -179,7 +179,8 @@ def handle_exception( # Do not recurse if a dodgy exception handler is added. if not isinstance(event, events.ExceptionEvent): self.logger.exception( - 'Exception occurred in handler for event "%s"', type(event).__name__, exc_info=exception) + 'Exception occurred in handler for event "%s"', type(event).__name__, exc_info=exception + ) self.dispatch(events.ExceptionEvent(exception=exception, event=event, callback=callback)) else: self.logger.exception( diff --git a/hikari/core/http_client.py b/hikari/core/http_client.py new file mode 100644 index 0000000000..1c1502a5ca --- /dev/null +++ b/hikari/core/http_client.py @@ -0,0 +1,18 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# Copyright © Nekoka.tt 2019-2020 +# +# This file is part of Hikari. +# +# Hikari is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Hikari is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Hikari. If not, see . diff --git a/hikari/net/__init__.py b/hikari/net/__init__.py index ab4c6e13f8..f380381ae3 100644 --- a/hikari/net/__init__.py +++ b/hikari/net/__init__.py @@ -21,18 +21,18 @@ These components describe the low level parts of Hikari. No model classes exist for these; the majority of communication is done via JSON arrays and objects. """ -from hikari.net import base_http_client from hikari.net import codes -from hikari.net import http_client from hikari.net import ratelimits +from hikari.net import rest from hikari.net import routes from hikari.net import shard from hikari.net import user_agent from hikari.net import versions + from hikari.net.codes import * from hikari.net.errors import * -from hikari.net.http_client import * +from hikari.net.rest import * from hikari.net.shard import * from hikari.net.versions import * -__all__ = codes.__all__ + errors.__all__ + shard.__all__ + http_client.__all__ + versions.__all__ +__all__ = codes.__all__ + errors.__all__ + shard.__all__ + rest.__all__ + versions.__all__ diff --git a/hikari/net/base_http_client.py b/hikari/net/base_http_client.py deleted file mode 100644 index c4e486e2d8..0000000000 --- a/hikari/net/base_http_client.py +++ /dev/null @@ -1,292 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# Copyright © Nekokatt 2019-2020 -# -# This file is part of Hikari. -# -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . -"""Provides a base utility class for any component needing an HTTP session -that supports proxying, SSL configuration, and a standard easy-to-use interface.""" -__all__ = ["BaseHTTPClient"] - -import abc -import asyncio -import contextlib -import json -import logging -import ssl -import typing - -import aiohttp.typedefs - -from hikari.internal_utilities import loggers -from hikari.net import user_agent - - -class BaseHTTPClient(abc.ABC): - """Base utility class for any component which uses an HTTP session. - - Each instance represents a session. This class handles consuming and managing - optional settings such as retries, proxies, and SSL customisation if desired. - - Examples - -------- - This can be used in a context manager: - - .. code-block:: python - - >>> class HTTPClientImpl(BaseHTTPClient): - ... def __init__(self, *args, **kwargs): - ... super().__init__(*args, **kwargs) - ... def request(self, *args, **kwargs): - ... return super()._request(*args, **kwargs) - - >>> async with HTTPClientImpl() as client: - ... async with client.request("GET", "https://some-websi.te") as resp: - ... resp.raise_for_status() - ... body = await resp.read() - - Warning - ------- - This must be initialized within a coroutine while an event loop is active - and registered to the current thread. - """ - - DELETE = "delete" - GET = "get" - PATCH = "patch" - POST = "post" - PUT = "put" - - __slots__ = ( - "allow_redirects", - "client_session", - "in_count", - "logger", - "proxy_auth", - "proxy_headers", - "proxy_url", - "ssl_context", - "timeout", - "user_agent", - "verify_ssl", - ) - - #: Whether to allow following of redirects or not. Generally you do not want this - #: as it poses a security risk. - #: - #: :type: :obj:`bool` - allow_redirects: bool - - #: The underlying client session used to make low level HTTP requests. - #: - #: :type: :obj:`aiohttp.ClientSession` - client_session: aiohttp.ClientSession - - #: The number of requests that have been made. This acts as a unique ID for each request. - #: - #: :type: :obj:`int` - in_count: int - - #: The logger used to write log messages. - #: - #: :type: :obj:`logging.Logger` - logger: logging.Logger - - #: The asyncio event loop being used. - #: - #: :type: :obj:`asyncio.AbstractEventLoop` - loop: asyncio.AbstractEventLoop - - #: Proxy authorization info. - #: - #: :type: :obj:`aiohttp.BasicAuth`, optional - proxy_auth: typing.Optional[aiohttp.BasicAuth] - - #: Proxy headers. - #: - #: :type: :obj:`aiohttp.typedefs.LooseHeaders`, optional - proxy_headers: typing.Optional[aiohttp.typedefs.LooseHeaders] - - #: Proxy URL to use. - #: - #: :type: :obj:`str`, optional - proxy_url: typing.Optional[str] - - #: SSL context to use. - #: - #: :type: :obj:`ssl.SSLContext`, optional - ssl_context: typing.Optional[ssl.SSLContext] - - #: Response timeout or``None`` if you are using the - #: default for :mod:`aiohttp`. - #: - #: :type: :obj:`float`, optional - timeout: typing.Optional[float] - - #: The user agent being used. - #: - #: Warning - #: ------- - #: Certain areas of the Discord API may enforce specific user agents - #: to be used for requests. You should not overwrite this generated value - #: unless you know what you are doing. Invalid useragents may lead to - #: bot account deauthorization. - #: - #: :type: :obj:`str` - user_agent: str - - #: Whether to verify SSL certificates or not. Generally you want this turned on - #: to prevent the risk of fake certificates being used to perform a - #: "man-in-the-middle" (MITM) attack on your application. However, if you are - #: stuck behind a proxy that cannot verify the certificates correctly, or are - #: having other SSL-related issues, you may wish to turn this off. - #: - #: :type: :obj:`bool` - verify_ssl: bool - - @abc.abstractmethod - def __init__( - self, - *, - allow_redirects: bool = False, - json_serialize: typing.Optional[typing.Callable] = None, - connector: typing.Optional[aiohttp.BaseConnector] = None, - proxy_headers: typing.Optional[aiohttp.typedefs.LooseHeaders] = None, - proxy_auth: typing.Optional[aiohttp.BasicAuth] = None, - proxy_url: typing.Optional[str] = None, - ssl_context: typing.Optional[ssl.SSLContext] = None, - verify_ssl: bool = True, - timeout: typing.Optional[float] = None, - ) -> None: - """ - Parameters - ---------- - allow_redirects : :obj:`bool` - If you find you are receiving multiple redirection responses causing - requests to fail, it is probably worth enabling this. Defaults to ``False`` - for security reasons. - json_serialize : :obj:`typing.Callable`, optional - A callable that consumes a Python object and returns a JSON-encoded string. - This defaults to :func:`json.dumps`. - connector : :obj:`aiohttp.BaseConnector`, optional - The :obj:`aiohttp.BaseConnector` to use for the client session, or ``None`` - if you wish to use the default instead. - proxy_headers : :obj:`aiohttp.typedefs.LooseHeaders`, optional - Proxy headers to pass. - proxy_auth : :obj:`aiohttp.BasicAuth`, optional - Proxy authentication to use. - proxy_url : :obj:`str`, optional - Proxy URL to use. - ssl_context : :obj:`ssl.SSLContext`, optional - SSL context to use. - verify_ssl : :obj:`bool` - Wheather to verify SSL. - timeout : :obj:`float`, optional - Timeout to apply to individual HTTP requests. - """ - - #: Whether to allow redirects or not. - #: - #: :type: :obj:`bool` - self.allow_redirects = allow_redirects - - #: The HTTP client session to use. - #: - #: :type: :obj:`aiohttp.ClientSession` - self.client_session = aiohttp.ClientSession( - connector=connector, version=aiohttp.HttpVersion11, json_serialize=json_serialize or json.dumps, - ) - - #: The logger to use for this object. - #: - #: :type: :obj:`logging.Logger` - self.logger = loggers.get_named_logger(self) - - #: User agent to use. - #: - #: :type: :obj:`str` - self.user_agent = user_agent.user_agent() - - #: If ``True``, this will enforce SSL signed certificate verification, otherwise it will - #: ignore potentially malicious SSL certificates. - #: - #: :type: :obj:`bool` - self.verify_ssl = verify_ssl - - #: Optional proxy URL to use for HTTP requests. - #: - #: :type: :obj:`str` - self.proxy_url = proxy_url - - #: Optional authorization to use if using a proxy. - #: - #: :type: :obj:`aiohttp.BasicAuth` - self.proxy_auth = proxy_auth - - #: Optional proxy headers to pass. - #: - #: :type: :obj:`aiohttp.typedefs.LooseHeaders` - self.proxy_headers = proxy_headers - - #: Optional SSL context to use. - #: - #: :type: :obj:`ssl.SSLContext` - self.ssl_context: ssl.SSLContext = ssl_context - - #: Optional timeout for HTTP requests. - #: - #: :type: :obj:`float` - self.timeout = timeout - - #: How many responses have been received. - #: - #: :type: :obj:`int` - self.in_count = 0 - - def _request(self, method, uri, **kwargs): - """Calls :func:`aiohttp.ClientSession.request` and returns the context manager result. - - Parameters - ---------- - method - The HTTP method to use. - uri - The URI to send to. - **kwargs - Any other parameters to pass to :func:`aiohttp.ClientSession.request` when invoking it. - """ - return self.client_session.request( - method, - uri, - allow_redirects=self.allow_redirects, - proxy=self.proxy_url, - proxy_auth=self.proxy_auth, - proxy_headers=self.proxy_headers, - verify_ssl=self.verify_ssl, - ssl_context=self.ssl_context, - timeout=self.timeout, - **kwargs, - ) - - async def __aenter__(self): - return self - - async def __aexit__(self, exc_type, exc_val, exc_tb): - await self.close() - - async def close(self): - self.logger.debug("Closing HTTPClient") - with contextlib.suppress(Exception): - await self.client_session.close() diff --git a/hikari/net/http_client.py b/hikari/net/rest.py similarity index 97% rename from hikari/net/http_client.py rename to hikari/net/rest.py index 9b71b0dbfb..409944734a 100644 --- a/hikari/net/http_client.py +++ b/hikari/net/rest.py @@ -17,7 +17,7 @@ # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . """Implementation of a basic HTTP client that uses aiohttp to interact with the Discord API.""" -__all__ = ["HTTPClient"] +__all__ = ["RestfulClient"] import asyncio import contextlib @@ -32,19 +32,28 @@ from hikari.internal_utilities import assertions from hikari.internal_utilities import containers +from hikari.internal_utilities import loggers from hikari.internal_utilities import storage from hikari.internal_utilities import transformations -from hikari.net import base_http_client from hikari.net import codes from hikari.net import errors from hikari.net import ratelimits from hikari.net import routes +from hikari.net import user_agent from hikari.net import versions -class HTTPClient(base_http_client.BaseHTTPClient): +class RestfulClient: """A RESTful client to allow you to interact with the Discord API.""" + GET = "get" + POST = "post" + PATCH = "patch" + PUT = "put" + HEAD = "head" + DELETE = "delete" + OPTIONS = "options" + _AUTHENTICATION_SCHEMES = ("Bearer", "Bot") def __init__( @@ -64,17 +73,64 @@ def __init__( token, version: typing.Union[int, versions.HTTPAPIVersion] = versions.HTTPAPIVersion.STABLE, ): - super().__init__( - allow_redirects=allow_redirects, - connector=connector, - proxy_headers=proxy_headers, - proxy_auth=proxy_auth, - proxy_url=proxy_url, - ssl_context=ssl_context, - verify_ssl=verify_ssl, - timeout=timeout, - json_serialize=json_serialize, + #: Whether to allow redirects or not. + #: + #: :type: :obj:`bool` + self.allow_redirects = allow_redirects + + #: The HTTP client session to use. + #: + #: :type: :obj:`aiohttp.ClientSession` + self.client_session = aiohttp.ClientSession( + connector=connector, version=aiohttp.HttpVersion11, json_serialize=json_serialize or json.dumps, ) + + #: The logger to use for this object. + #: + #: :type: :obj:`logging.Logger` + self.logger = loggers.get_named_logger(self) + + #: User agent to use. + #: + #: :type: :obj:`str` + self.user_agent = user_agent.user_agent() + + #: If ``True``, this will enforce SSL signed certificate verification, otherwise it will + #: ignore potentially malicious SSL certificates. + #: + #: :type: :obj:`bool` + self.verify_ssl = verify_ssl + + #: Optional proxy URL to use for HTTP requests. + #: + #: :type: :obj:`str` + self.proxy_url = proxy_url + + #: Optional authorization to use if using a proxy. + #: + #: :type: :obj:`aiohttp.BasicAuth` + self.proxy_auth = proxy_auth + + #: Optional proxy headers to pass. + #: + #: :type: :obj:`aiohttp.typedefs.LooseHeaders` + self.proxy_headers = proxy_headers + + #: Optional SSL context to use. + #: + #: :type: :obj:`ssl.SSLContext` + self.ssl_context: ssl.SSLContext = ssl_context + + #: Optional timeout for HTTP requests. + #: + #: :type: :obj:`float` + self.timeout = timeout + + #: How many responses have been received. + #: + #: :type: :obj:`int` + self.in_count = 0 + self.version = int(version) self.base_url = base_url.format(self) self.global_ratelimiter = ratelimits.ManualRateLimiter() @@ -93,7 +149,18 @@ def __init__( async def close(self): with contextlib.suppress(Exception): self.ratelimiter.close() - await super().close() + with contextlib.suppress(Exception): + self.logger.debug("Closing HTTPClient") + await self.client_session.close() + + def __enter__(self) -> typing.NoReturn: + raise RuntimeError(f"Please use 'async with' instead of 'with' for {type(self).__name__}") + + async def __aenter__(self): + return self + + async def __aexit__(self, exc_type, exc_val, exc_tb): + await self.close() async def _request( self, @@ -151,13 +218,20 @@ async def _request( json_body if json_body is not None else form_body, ) - async with super()._request( + async with self.client_session.request( compiled_route.method, compiled_route.create_url(self.base_url), headers=request_headers, json=json_body, params=query, data=form_body, + allow_redirects=self.allow_redirects, + proxy=self.proxy_url, + proxy_auth=self.proxy_auth, + proxy_headers=self.proxy_headers, + verify_ssl=self.verify_ssl, + ssl_context=self.ssl_context, + timeout=self.timeout, **kwargs, ) as resp: raw_body = await resp.read() diff --git a/tests/hikari/net/test_base_http_client.py b/tests/hikari/net/test_base_http_client.py deleted file mode 100644 index cfad93afc4..0000000000 --- a/tests/hikari/net/test_base_http_client.py +++ /dev/null @@ -1,54 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# Copyright © Nekoka.tt 2019-2020 -# -# This file is part of Hikari. -# -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . -import logging - -import cymock as mock -import pytest - -from hikari.net import base_http_client - - -@pytest.mark.asyncio -async def test_http_client___aenter___and___aexit__(): - class HTTPClientImpl(base_http_client.BaseHTTPClient): - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.close = mock.AsyncMock() - - inst = HTTPClientImpl() - - async with inst as client: - assert client is inst - - inst.close.assert_called_once_with() - - -@pytest.mark.asyncio -async def test_http_client_close_calls_client_session_close(): - class HTTPClientImpl(base_http_client.BaseHTTPClient): - def __init__(self, *args, **kwargs): - self.client_session = mock.MagicMock() - self.client_session.close = mock.AsyncMock() - self.logger = logging.getLogger(__name__) - - inst = HTTPClientImpl() - - await inst.close() - - inst.client_session.close.assert_called_with() diff --git a/tests/hikari/net/test_http_client.py b/tests/hikari/net/test_rest.py similarity index 51% rename from tests/hikari/net/test_http_client.py rename to tests/hikari/net/test_rest.py index 36976ff79a..e7036974e2 100644 --- a/tests/hikari/net/test_http_client.py +++ b/tests/hikari/net/test_rest.py @@ -17,8 +17,10 @@ # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . import asyncio +import contextlib import io import json +import logging import ssl import unittest.mock @@ -28,19 +30,18 @@ from hikari.internal_utilities import storage from hikari.internal_utilities import transformations -from hikari.net import base_http_client from hikari.net import errors -from hikari.net import http_client from hikari.net import ratelimits +from hikari.net import rest from hikari.net import routes from hikari.net import versions from tests.hikari import _helpers -class TestHTTPClient: +class TestRestfulClient: @pytest.fixture - def http_client_impl(self): - class HTTPClientImpl(http_client.HTTPClient): + def rest_impl(self): + class RestfulClientImpl(rest.RestfulClient): def __init__(self, *args, **kwargs): self.base_url = "https://discordapp.com/api/v6" self.client_session = mock.MagicMock(close=mock.AsyncMock()) @@ -48,7 +49,7 @@ def __init__(self, *args, **kwargs): self.ratelimiter = mock.MagicMock(close=mock.MagicMock()) self._request = mock.AsyncMock(return_value=...) - return HTTPClientImpl() + return RestfulClientImpl() @pytest.fixture def compiled_route(self): @@ -86,218 +87,270 @@ async def __aexit__(self, exc_type, exc_val, exc_tb): return Response() - @unittest.mock.patch.object(base_http_client.BaseHTTPClient, "__init__") - def test__init__with_bot_token_and_without_optionals(self, mock_init): + @pytest.mark.asyncio + async def test_rest___aenter___and___aexit__(self): + class RestfulClientImpl(rest.RestfulClient): + def __init__(self, *args, **kwargs): + kwargs.setdefault("token", "Bearer xxx") + super().__init__(*args, **kwargs) + self.close = mock.AsyncMock() + + inst = RestfulClientImpl() + + async with inst as client: + assert client is inst + + inst.close.assert_called_once_with() + + @pytest.mark.asyncio + async def test_rest_close_calls_client_session_close(self): + class RestfulClientImpl(rest.RestfulClient): + def __init__(self, *args, **kwargs): + self.client_session = mock.MagicMock() + self.client_session.close = mock.AsyncMock() + self.logger = logging.getLogger(__name__) + + inst = RestfulClientImpl() + + await inst.close() + + inst.client_session.close.assert_called_with() + + @pytest.mark.asyncio + async def test__init__with_bot_token_and_without_optionals(self): mock_manual_rate_limiter = mock.MagicMock() - mock_http_bucket_rate_limit_manager = mock.MagicMock() - with mock.patch.object(ratelimits, "ManualRateLimiter", return_value=mock_manual_rate_limiter): - with mock.patch.object( - ratelimits, "HTTPBucketRateLimiterManager", return_value=mock_http_bucket_rate_limit_manager - ): - client = http_client.HTTPClient(token="Bot token.otacon.a-token") - base_http_client.BaseHTTPClient.__init__.assert_called_once_with( - allow_redirects=False, - connector=None, - proxy_headers=None, - proxy_auth=None, - proxy_url=None, - ssl_context=None, - verify_ssl=True, - timeout=None, - json_serialize=json.dumps, - ) - assert client.base_url == f"https://discordapp.com/api/v{int(versions.HTTPAPIVersion.STABLE)}" - assert client.global_ratelimiter is mock_manual_rate_limiter - assert client.json_serialize is json.dumps - assert client.json_deserialize is json.loads - assert client.ratelimiter is mock_http_bucket_rate_limit_manager - assert client.token == "Bot token.otacon.a-token" - - @unittest.mock.patch.object(base_http_client.BaseHTTPClient, "__init__") - @unittest.mock.patch.object(ratelimits, "ManualRateLimiter") - @unittest.mock.patch.object(ratelimits, "HTTPBucketRateLimiterManager") - def test__init__with_bearer_token_and_without_optionals(self, *args): - client = http_client.HTTPClient(token="Bearer token.otacon.a-token") + buckets_mock = mock.MagicMock() + + stack = contextlib.ExitStack() + stack.enter_context(mock.patch.object(ratelimits, "ManualRateLimiter", return_value=mock_manual_rate_limiter)) + stack.enter_context(mock.patch.object(ratelimits, "HTTPBucketRateLimiterManager", return_value=buckets_mock)) + + with stack: + client = rest.RestfulClient(token="Bot token.otacon.a-token") + + assert client.base_url == f"https://discordapp.com/api/v{int(versions.HTTPAPIVersion.STABLE)}" + assert client.global_ratelimiter is mock_manual_rate_limiter + assert client.json_serialize is json.dumps + assert client.json_deserialize is json.loads + assert client.ratelimiter is buckets_mock + assert client.token == "Bot token.otacon.a-token" + + @pytest.mark.asyncio + async def test__init__with_bearer_token_and_without_optionals(self): + client = rest.RestfulClient(token="Bearer token.otacon.a-token") assert client.token == "Bearer token.otacon.a-token" - @unittest.mock.patch.object(base_http_client.BaseHTTPClient, "__init__") - def test__init__with_optionals(self, mock_init): + @pytest.mark.asyncio + async def test__init__with_optionals(self): mock_manual_rate_limiter = mock.MagicMock(ratelimits.ManualRateLimiter) mock_http_bucket_rate_limit_manager = mock.MagicMock(ratelimits.HTTPBucketRateLimiterManager) - mock_connetor = mock.MagicMock(aiohttp.BaseConnector) + mock_connector = mock.MagicMock(aiohttp.BaseConnector) mock_dumps = mock.MagicMock(json.dumps) mock_loads = mock.MagicMock(json.loads) mock_proxy_auth = mock.MagicMock(aiohttp.BasicAuth) mock_proxy_headers = {"User-Agent": "Agent 42"} mock_ssl_context = mock.MagicMock(ssl.SSLContext) - with mock.patch.object(ratelimits, "ManualRateLimiter", return_value=mock_manual_rate_limiter): - with mock.patch.object( + + stack = contextlib.ExitStack() + stack.enter_context(mock.patch.object(aiohttp, "ClientSession")) + stack.enter_context(mock.patch.object(ratelimits, "ManualRateLimiter", return_value=mock_manual_rate_limiter)) + stack.enter_context( + mock.patch.object( ratelimits, "HTTPBucketRateLimiterManager", return_value=mock_http_bucket_rate_limit_manager - ): - client = http_client.HTTPClient( - token="Bot token.otacon.a-token", - base_url="https://discordapp.com/api/v69420", - allow_redirects=True, - connector=mock_connetor, - proxy_headers=mock_proxy_headers, - proxy_auth=mock_proxy_auth, - proxy_url="a.proxy.url.today.nep", - ssl_context=mock_ssl_context, - verify_ssl=False, - timeout=30.53, - json_deserialize=mock_loads, - json_serialize=mock_dumps, - ) - base_http_client.BaseHTTPClient.__init__.assert_called_once_with( - allow_redirects=True, - connector=mock_connetor, - proxy_headers=mock_proxy_headers, - proxy_auth=mock_proxy_auth, - proxy_url="a.proxy.url.today.nep", - ssl_context=mock_ssl_context, - verify_ssl=False, - timeout=30.53, - json_serialize=mock_dumps, - ) - assert client.base_url == "https://discordapp.com/api/v69420" - assert client.global_ratelimiter is mock_manual_rate_limiter - assert client.json_serialize is mock_dumps - assert client.json_deserialize is mock_loads - assert client.ratelimiter is mock_http_bucket_rate_limit_manager - assert client.token == "Bot token.otacon.a-token" - - @mock.patch.object(base_http_client.BaseHTTPClient, "__init__") - @mock.patch.object(ratelimits, "ManualRateLimiter") - @mock.patch.object(ratelimits, "HTTPBucketRateLimiterManager") + ) + ) + + with stack: + client = rest.RestfulClient( + token="Bot token.otacon.a-token", + base_url="https://discordapp.com/api/v69420", + allow_redirects=True, + connector=mock_connector, + proxy_headers=mock_proxy_headers, + proxy_auth=mock_proxy_auth, + proxy_url="a.proxy.url.today.nep", + ssl_context=mock_ssl_context, + verify_ssl=False, + timeout=30.53, + json_deserialize=mock_loads, + json_serialize=mock_dumps, + ) + assert client.base_url == "https://discordapp.com/api/v69420" + assert client.global_ratelimiter is mock_manual_rate_limiter + assert client.json_serialize is mock_dumps + assert client.json_deserialize is mock_loads + assert client.ratelimiter is mock_http_bucket_rate_limit_manager + assert client.token == "Bot token.otacon.a-token" + + @pytest.mark.asyncio @_helpers.assert_raises(type_=RuntimeError) - def test__init__raises_runtime_error_with_invalid_token(self, *args): - with http_client.HTTPClient(token="An-invalid-TOKEN"): + async def test__init__raises_runtime_error_with_invalid_token(self, *_): + async with rest.RestfulClient(token="An-invalid-TOKEN"): pass @pytest.mark.asyncio - async def test_close(self, http_client_impl): - with mock.patch.object(base_http_client.BaseHTTPClient, "close") as mock_close: - await http_client_impl.close() - mock_close.assert_called_once_with() - http_client_impl.ratelimiter.close.assert_called_once_with() + async def test_close(self, rest_impl): + await rest_impl.close() + rest_impl.ratelimiter.close.assert_called_once_with() + rest_impl.client_session.close.assert_called_once_with() @pytest.fixture() - @mock.patch.object(base_http_client.BaseHTTPClient, "__init__") @mock.patch.object(ratelimits, "ManualRateLimiter") @mock.patch.object(ratelimits, "HTTPBucketRateLimiterManager") - async def mock_http_client(self, *args): - http_client_impl = http_client.HTTPClient(token="Bot token") - http_client_impl.logger = mock.MagicMock(debug=mock.MagicMock()) - http_client_impl.ratelimiter = mock.MagicMock() - http_client_impl.global_ratelimiter = mock.MagicMock() - return http_client_impl + async def mock_rest_impl(self, *args): + rest_impl = rest.RestfulClient(token="Bot token") + rest_impl.logger = mock.MagicMock(debug=mock.MagicMock()) + rest_impl.ratelimiter = mock.MagicMock() + rest_impl.global_ratelimiter = mock.MagicMock() + return rest_impl @pytest.mark.asyncio - async def test__request_acquires_ratelimiter(self, compiled_route, exit_error, mock_http_client): - mock_http_client = await mock_http_client - mock_http_client.logger.debug.side_effect = exit_error + async def test__request_acquires_ratelimiter(self, compiled_route, exit_error, mock_rest_impl): + rest_impl = await mock_rest_impl + rest_impl.logger.debug.side_effect = exit_error with mock.patch("asyncio.gather", return_value=_helpers.AwaitableMock()): try: - await mock_http_client._request(compiled_route) + await rest_impl._request(compiled_route) except exit_error: pass - mock_http_client.ratelimiter.acquire.asset_called_once_with(compiled_route) + rest_impl.ratelimiter.acquire.asset_called_once_with(compiled_route) @pytest.mark.asyncio - async def test__request_sets_Authentication_if_token(self, compiled_route, exit_error, mock_http_client): - mock_http_client = await mock_http_client - mock_http_client.logger.debug.side_effect = [None, exit_error] - with mock.patch("asyncio.gather", return_value=_helpers.AwaitableMock()): - with mock.patch.object(base_http_client.BaseHTTPClient, "_request") as mock_request: - try: - await mock_http_client._request(compiled_route) - except exit_error: - pass + async def test__request_sets_Authentication_if_token(self, compiled_route, exit_error, mock_rest_impl): + rest_impl = await mock_rest_impl + rest_impl.logger.debug.side_effect = [None, exit_error] - mock_request.assert_called_with( - "get", - "https://discordapp.com/api/v6/somewhere", - headers={"X-RateLimit-Precision": "millisecond", "Authorization": "Bot token"}, - json=None, - params=None, - data=None, - ) + stack = contextlib.ExitStack() + mock_request = stack.enter_context(mock.patch.object(aiohttp.ClientSession, "request")) + stack.enter_context(mock.patch("asyncio.gather", return_value=_helpers.AwaitableMock())) + + with stack: + try: + await rest_impl._request(compiled_route) + except exit_error: + pass + + mock_request.assert_called_with( + "get", + "https://discordapp.com/api/v6/somewhere", + headers={"X-RateLimit-Precision": "millisecond", "Authorization": "Bot token"}, + json=None, + params=None, + data=None, + allow_redirects=False, + proxy=None, + proxy_auth=None, + proxy_headers=None, + verify_ssl=True, + ssl_context=None, + timeout=None, + ) @pytest.mark.asyncio async def test__request_doesnt_set_Authentication_if_suppress_authorization_header( - self, compiled_route, exit_error, mock_http_client + self, compiled_route, exit_error, mock_rest_impl ): - mock_http_client = await mock_http_client - mock_http_client.logger.debug.side_effect = [None, exit_error] + rest_impl = await mock_rest_impl + rest_impl.logger.debug.side_effect = [None, exit_error] - with mock.patch("asyncio.gather", return_value=_helpers.AwaitableMock()): - with mock.patch.object(base_http_client.BaseHTTPClient, "_request") as mock_request: - try: - await mock_http_client._request(compiled_route, suppress_authorization_header=True) - except exit_error: - pass + stack = contextlib.ExitStack() + stack.enter_context(mock.patch("asyncio.gather", return_value=_helpers.AwaitableMock())) + mock_request = stack.enter_context(mock.patch.object(aiohttp.ClientSession, "request")) - mock_request.assert_called_with( - "get", - "https://discordapp.com/api/v6/somewhere", - headers={"X-RateLimit-Precision": "millisecond"}, - json=None, - params=None, - data=None, - ) + with stack: + try: + await rest_impl._request(compiled_route, suppress_authorization_header=True) + except exit_error: + pass + + mock_request.assert_called_with( + "get", + "https://discordapp.com/api/v6/somewhere", + headers={"X-RateLimit-Precision": "millisecond"}, + json=None, + params=None, + data=None, + allow_redirects=False, + proxy=None, + proxy_auth=None, + proxy_headers=None, + verify_ssl=True, + ssl_context=None, + timeout=None, + ) @pytest.mark.asyncio - async def test__request_sets_X_Audit_Log_Reason_if_reason(self, compiled_route, exit_error, mock_http_client): - mock_http_client = await mock_http_client - mock_http_client.logger.debug.side_effect = [None, exit_error] + async def test__request_sets_X_Audit_Log_Reason_if_reason(self, compiled_route, exit_error, mock_rest_impl): + rest_impl = await mock_rest_impl + rest_impl.logger.debug.side_effect = [None, exit_error] - with mock.patch("asyncio.gather", return_value=_helpers.AwaitableMock()): - with mock.patch.object(base_http_client.BaseHTTPClient, "_request") as mock_request: - try: - await mock_http_client._request(compiled_route, reason="test reason") - except exit_error: - pass + stack = contextlib.ExitStack() + stack.enter_context(mock.patch("asyncio.gather", return_value=_helpers.AwaitableMock())) + mock_request = stack.enter_context(mock.patch.object(aiohttp.ClientSession, "request")) + + with stack: + try: + await rest_impl._request(compiled_route, reason="test reason") + except exit_error: + pass - mock_request.assert_called_with( - "get", - "https://discordapp.com/api/v6/somewhere", - headers={ + mock_request.assert_called_with( + "get", + "https://discordapp.com/api/v6/somewhere", + headers={ "X-RateLimit-Precision": "millisecond", "Authorization": "Bot token", "X-Audit-Log-Reason": "test reason", }, - json=None, - params=None, - data=None, - ) + json=None, + params=None, + data=None, + allow_redirects=False, + proxy=None, + proxy_auth=None, + proxy_headers=None, + verify_ssl=True, + ssl_context=None, + timeout=None, + ) @pytest.mark.asyncio - async def test__request_updates_headers_with_provided_headers(self, compiled_route, exit_error, mock_http_client): - mock_http_client = await mock_http_client - mock_http_client.logger.debug.side_effect = [None, exit_error] + async def test__request_updates_headers_with_provided_headers(self, compiled_route, exit_error, mock_rest_impl): + rest_impl = await mock_rest_impl + rest_impl.logger.debug.side_effect = [None, exit_error] - with mock.patch("asyncio.gather", return_value=_helpers.AwaitableMock()): - with mock.patch.object(base_http_client.BaseHTTPClient, "_request") as mock_request: - try: - await mock_http_client._request( - compiled_route, headers={"X-RateLimit-Precision": "nanosecond", "Authorization": "Bearer token"} - ) - except exit_error: - pass + stack = contextlib.ExitStack() + stack.enter_context(mock.patch("asyncio.gather", return_value=_helpers.AwaitableMock())) + mock_request = stack.enter_context(mock.patch.object(aiohttp.ClientSession, "request")) - mock_request.assert_called_with( - "get", - "https://discordapp.com/api/v6/somewhere", - headers={"X-RateLimit-Precision": "nanosecond", "Authorization": "Bearer token"}, - json=None, - params=None, - data=None, + with stack: + try: + await rest_impl._request( + compiled_route, headers={"X-RateLimit-Precision": "nanosecond", "Authorization": "Bearer token"} ) + except exit_error: + pass + + mock_request.assert_called_with( + "get", + "https://discordapp.com/api/v6/somewhere", + headers={"X-RateLimit-Precision": "nanosecond", "Authorization": "Bearer token"}, + json=None, + params=None, + data=None, + allow_redirects=False, + proxy=None, + proxy_auth=None, + proxy_headers=None, + verify_ssl=True, + ssl_context=None, + timeout=None, + ) @pytest.mark.asyncio - async def test__request_resets_seek_on_seekable_resources(self, compiled_route, exit_error, mock_http_client): + async def test__request_resets_seek_on_seekable_resources(self, compiled_route, exit_error, mock_rest_impl): class SeekableResource: seeked: bool = False @@ -307,13 +360,13 @@ def seek(self, _): def assert_seek_called(self): assert self.seeked - mock_http_client = await mock_http_client - mock_http_client.logger.debug.side_effect = exit_error + rest_impl = await mock_rest_impl + rest_impl.logger.debug.side_effect = exit_error seekable_resources = [SeekableResource(), SeekableResource(), SeekableResource()] with mock.patch("asyncio.gather", return_value=_helpers.AwaitableMock()): try: - await mock_http_client._request(compiled_route, re_seekable_resources=seekable_resources) + await rest_impl._request(compiled_route, re_seekable_resources=seekable_resources) except exit_error: pass @@ -325,65 +378,71 @@ def assert_seek_called(self): "content_type", ["text/plain", "text/html"], ) async def test__request_handles_bad_response_when_content_type_is_plain_or_htlm( - self, content_type, exit_error, compiled_route, discord_response, mock_http_client + self, content_type, exit_error, compiled_route, discord_response, mock_rest_impl ): discord_response.headers["Content-Type"] = content_type - mock_http_client = await mock_http_client - mock_http_client._handle_bad_response = mock.AsyncMock(side_effect=[None, exit_error]) + rest_impl = await mock_rest_impl + rest_impl._handle_bad_response = mock.AsyncMock(side_effect=[None, exit_error]) + rest_impl.client_session.request = mock.MagicMock(return_value=discord_response) with mock.patch("asyncio.gather", return_value=_helpers.AwaitableMock()): - with mock.patch.object(base_http_client.BaseHTTPClient, "_request", return_value=discord_response): - try: - await mock_http_client._request(compiled_route) - except exit_error: - pass + try: + await rest_impl._request(compiled_route) + except exit_error: + pass - mock_http_client._handle_bad_response.assert_called() + rest_impl._handle_bad_response.assert_called() @pytest.mark.asyncio - async def test__request_when_invalid_content_type(self, compiled_route, discord_response, mock_http_client): + async def test__request_when_invalid_content_type(self, compiled_route, discord_response, mock_rest_impl): discord_response.headers["Content-Type"] = "something/invalid" - mock_http_client = await mock_http_client + rest_impl = await mock_rest_impl - with mock.patch("asyncio.gather", return_value=_helpers.AwaitableMock()): - with mock.patch.object(base_http_client.BaseHTTPClient, "_request", return_value=discord_response): - assert await mock_http_client._request(compiled_route, json_body={}) is None + stack = contextlib.ExitStack() + stack.enter_context(mock.patch("asyncio.gather", return_value=_helpers.AwaitableMock())) + stack.enter_context(mock.patch.object(aiohttp.ClientSession, "request", return_value=discord_response)) + + with stack: + assert await rest_impl._request(compiled_route, json_body={}) is None @pytest.mark.asyncio async def test__request_when_TOO_MANY_REQUESTS_when_global( - self, compiled_route, exit_error, discord_response, mock_http_client + self, compiled_route, exit_error, discord_response, mock_rest_impl ): discord_response.status = 429 discord_response.raw_body = '{"retry_after": 1, "global": true}' - mock_http_client = await mock_http_client - mock_http_client.global_ratelimiter.throttle = mock.MagicMock(side_effect=[None, exit_error]) + rest_impl = await mock_rest_impl + rest_impl.global_ratelimiter.throttle = mock.MagicMock(side_effect=[None, exit_error]) - with mock.patch("asyncio.gather", return_value=_helpers.AwaitableMock()): - with mock.patch.object(base_http_client.BaseHTTPClient, "_request", return_value=discord_response): - try: - await mock_http_client._request(compiled_route) - except exit_error: - pass + stack = contextlib.ExitStack() + stack.enter_context(mock.patch("asyncio.gather", return_value=_helpers.AwaitableMock())) + stack.enter_context(mock.patch.object(aiohttp.ClientSession, "request", return_value=discord_response)) - mock_http_client.global_ratelimiter.throttle.assert_called_with(0.001) + with stack: + try: + await rest_impl._request(compiled_route) + except exit_error: + pass + + rest_impl.global_ratelimiter.throttle.assert_called_with(0.001) @pytest.mark.asyncio async def test__request_when_TOO_MANY_REQUESTS_when_not_global( - self, compiled_route, exit_error, discord_response, mock_http_client + self, compiled_route, exit_error, discord_response, mock_rest_impl ): discord_response.status = 429 discord_response.raw_body = '{"retry_after": 1, "global": false}' - mock_http_client = await mock_http_client - mock_http_client.logger.debug.side_effect = [None, exit_error] + rest_impl = await mock_rest_impl + rest_impl.logger.debug.side_effect = [None, exit_error] with mock.patch("asyncio.gather", return_value=_helpers.AwaitableMock()): - with mock.patch.object(base_http_client.BaseHTTPClient, "_request", return_value=discord_response): + with mock.patch.object(rest.RestfulClient, "_request", return_value=discord_response): try: - await mock_http_client._request(compiled_route) + await rest_impl._request(compiled_route) except exit_error: pass - mock_http_client.global_ratelimiter.throttle.assert_not_called() + rest_impl.global_ratelimiter.throttle.assert_not_called() @pytest.mark.asyncio @pytest.mark.parametrize("api_version", [versions.HTTPAPIVersion.V6, versions.HTTPAPIVersion.V7]) @@ -397,65 +456,64 @@ async def test__request_when_TOO_MANY_REQUESTS_when_not_global( (405, errors.ClientHTTPError), ], ) - @mock.patch.object(base_http_client.BaseHTTPClient, "__init__") @mock.patch.object(ratelimits, "ManualRateLimiter") @mock.patch.object(ratelimits, "HTTPBucketRateLimiterManager") async def test__request_raises_appropriate_error_for_status_code( self, *patches, status_code, error, compiled_route, discord_response, api_version ): discord_response.status = status_code - http_client_impl = http_client.HTTPClient(token="Bot token", version=api_version) - http_client_impl.ratelimiter = mock.MagicMock() - http_client_impl.global_ratelimiter = mock.MagicMock() - http_client_impl.logger = mock.MagicMock() + rest_impl = rest.RestfulClient(token="Bot token", version=api_version) + rest_impl.ratelimiter = mock.MagicMock() + rest_impl.global_ratelimiter = mock.MagicMock() + rest_impl.client_session.request = mock.MagicMock(return_value=discord_response) with mock.patch("asyncio.gather", return_value=_helpers.AwaitableMock()): - with mock.patch.object(base_http_client.BaseHTTPClient, "_request", return_value=discord_response): - try: - await http_client_impl._request(compiled_route) - assert False - except error: - assert True + try: + await rest_impl._request(compiled_route) + assert False + except error: + assert True @pytest.mark.asyncio - async def test__request_when_NO_CONTENT(self, compiled_route, discord_response, mock_http_client): + async def test__request_when_NO_CONTENT(self, compiled_route, discord_response, mock_rest_impl): discord_response.status = 204 - mock_http_client = await mock_http_client + rest_impl = await mock_rest_impl + rest_impl.client_session.request = mock.MagicMock(return_value=discord_response) with mock.patch("asyncio.gather", return_value=_helpers.AwaitableMock()): - with mock.patch.object(base_http_client.BaseHTTPClient, "_request", return_value=discord_response): - assert await mock_http_client._request(compiled_route, form_body=aiohttp.FormData()) is None + assert await rest_impl._request(compiled_route, form_body=aiohttp.FormData()) is None @pytest.mark.asyncio - async def test__request_handles_bad_response_when_status_error_not_catched( - self, exit_error, compiled_route, discord_response, mock_http_client + async def test__request_handles_bad_response_when_error_results_in_retry( + self, exit_error, compiled_route, discord_response, mock_rest_impl ): discord_response.raw_body = "{}" discord_response.status = 1000 - mock_http_client = await mock_http_client - mock_http_client._handle_bad_response = mock.AsyncMock(side_effect=[None, exit_error]) + rest_impl = await mock_rest_impl + rest_impl._handle_bad_response = mock.AsyncMock(side_effect=[None, exit_error]) - with mock.patch("asyncio.gather", return_value=_helpers.AwaitableMock()): - with mock.patch.object(base_http_client.BaseHTTPClient, "_request", return_value=discord_response): - try: - await mock_http_client._request(compiled_route) - except exit_error: - pass + stack = contextlib.ExitStack() + stack.enter_context(mock.patch.object(aiohttp.ClientSession, "request", return_value=discord_response)) + stack.enter_context(mock.patch("asyncio.gather", return_value=_helpers.AwaitableMock())) + + with stack: + try: + await rest_impl._request(compiled_route) + except exit_error: + pass - mock_http_client._handle_bad_response.assert_called() + assert rest_impl._handle_bad_response.call_count == 2 @pytest.mark.asyncio - async def test_handle_bad_response(self, http_client_impl): + async def test_handle_bad_response(self, rest_impl): backoff = _helpers.create_autospec(ratelimits.ExponentialBackOff, __next__=mock.MagicMock(return_value=4)) mock_route = mock.MagicMock(routes.CompiledRoute) with mock.patch.object(asyncio, "sleep"): - await http_client_impl._handle_bad_response( - backoff, "Being spammy", mock_route, "You are being rate limited", 429 - ) + await rest_impl._handle_bad_response(backoff, "Being spammy", mock_route, "You are being rate limited", 429) asyncio.sleep.assert_called_once_with(4) @pytest.mark.asyncio - async def test_handle_bad_response_raises_server_http_error_on_timeout(self, http_client_impl): + async def test_handle_bad_response_raises_server_http_error_on_timeout(self, rest_impl): backoff = _helpers.create_autospec( ratelimits.ExponentialBackOff, __next__=mock.MagicMock(side_effect=asyncio.TimeoutError()) ) @@ -464,7 +522,7 @@ async def test_handle_bad_response_raises_server_http_error_on_timeout(self, htt excepted_exception = errors.ServerHTTPError with mock.patch.object(errors, "ServerHTTPError", side_effect=mock_exception): try: - await http_client_impl._handle_bad_response( + await rest_impl._handle_bad_response( backoff, "Being spammy", mock_route, "You are being rate limited", 429 ) except excepted_exception as e: @@ -476,78 +534,78 @@ async def test_handle_bad_response_raises_server_http_error_on_timeout(self, htt assert False, "Missing `ServerHTTPError`, should be raised on timeout." @pytest.mark.asyncio - async def test_get_gateway(self, http_client_impl): - http_client_impl._request.return_value = {"url": "discord.discord///"} + async def test_get_gateway(self, rest_impl): + rest_impl._request.return_value = {"url": "discord.discord///"} mock_route = mock.MagicMock(routes.GATEWAY) with mock.patch.object(routes, "GATEWAY", compile=mock.MagicMock(return_value=mock_route)): - assert await http_client_impl.get_gateway() == "discord.discord///" - routes.GATEWAY.compile.assert_called_once_with(http_client_impl.GET) - http_client_impl._request.assert_called_once_with(mock_route) + assert await rest_impl.get_gateway() == "discord.discord///" + routes.GATEWAY.compile.assert_called_once_with(rest_impl.GET) + rest_impl._request.assert_called_once_with(mock_route) @pytest.mark.asyncio - async def test_get_gateway_bot(self, http_client_impl): + async def test_get_gateway_bot(self, rest_impl): mock_response = {"url": "discord.discord///"} - http_client_impl._request.return_value = mock_response + rest_impl._request.return_value = mock_response mock_route = mock.MagicMock(routes.GATEWAY_BOT) with mock.patch.object(routes, "GATEWAY_BOT", compile=mock.MagicMock(return_value=mock_route)): - assert await http_client_impl.get_gateway_bot() is mock_response - routes.GATEWAY_BOT.compile.assert_called_once_with(http_client_impl.GET) - http_client_impl._request.assert_called_once_with(mock_route) + assert await rest_impl.get_gateway_bot() is mock_response + routes.GATEWAY_BOT.compile.assert_called_once_with(rest_impl.GET) + rest_impl._request.assert_called_once_with(mock_route) @pytest.mark.asyncio - async def test_get_guild_audit_log_without_optionals(self, http_client_impl): + async def test_get_guild_audit_log_without_optionals(self, rest_impl): mock_response = {"webhooks": [], "users": []} - http_client_impl._request.return_value = mock_response + rest_impl._request.return_value = mock_response mock_route = mock.MagicMock(routes.GUILD_AUDIT_LOGS) with mock.patch.object(routes, "GUILD_AUDIT_LOGS", compile=mock.MagicMock(return_value=mock_route)): - assert await http_client_impl.get_guild_audit_log("2929292929") is mock_response - routes.GUILD_AUDIT_LOGS.compile.assert_called_once_with(http_client_impl.GET, guild_id="2929292929") - http_client_impl._request.assert_called_once_with(mock_route, query={}) + assert await rest_impl.get_guild_audit_log("2929292929") is mock_response + routes.GUILD_AUDIT_LOGS.compile.assert_called_once_with(rest_impl.GET, guild_id="2929292929") + rest_impl._request.assert_called_once_with(mock_route, query={}) @pytest.mark.asyncio - async def test_get_guild_audit_log_with_optionals(self, http_client_impl): + async def test_get_guild_audit_log_with_optionals(self, rest_impl): mock_response = {"webhooks": [], "users": []} - http_client_impl._request.return_value = mock_response + rest_impl._request.return_value = mock_response mock_route = mock.MagicMock(routes.GUILD_AUDIT_LOGS) with mock.patch.object(routes, "GUILD_AUDIT_LOGS", compile=mock.MagicMock(return_value=mock_route)): assert ( - await http_client_impl.get_guild_audit_log( + await rest_impl.get_guild_audit_log( "2929292929", user_id="115590097100865541", action_type=42, limit=5, ) is mock_response ) - routes.GUILD_AUDIT_LOGS.compile.assert_called_once_with(http_client_impl.GET, guild_id="2929292929") - http_client_impl._request.assert_called_once_with( + routes.GUILD_AUDIT_LOGS.compile.assert_called_once_with(rest_impl.GET, guild_id="2929292929") + rest_impl._request.assert_called_once_with( mock_route, query={"user_id": "115590097100865541", "action_type": 42, "limit": 5} ) @pytest.mark.asyncio - async def test_get_channel(self, http_client_impl): + async def test_get_channel(self, rest_impl): mock_response = {"id": "20202020200202"} - http_client_impl._request.return_value = mock_response + rest_impl._request.return_value = mock_response mock_route = mock.MagicMock(routes.CHANNEL) with mock.patch.object(routes, "CHANNEL", compile=mock.MagicMock(return_value=mock_route)): - assert await http_client_impl.get_channel("20202020020202") is mock_response - routes.CHANNEL.compile.assert_called_once_with(http_client_impl.GET, channel_id="20202020020202") - http_client_impl._request.assert_called_once_with(mock_route) + assert await rest_impl.get_channel("20202020020202") is mock_response + routes.CHANNEL.compile.assert_called_once_with(rest_impl.GET, channel_id="20202020020202") + rest_impl._request.assert_called_once_with(mock_route) @pytest.mark.asyncio - async def test_modify_channel_without_optionals(self, http_client_impl): + async def test_modify_channel_without_optionals(self, rest_impl): mock_response = {"id": "20393939"} - http_client_impl._request.return_value = mock_response + rest_impl._request.return_value = mock_response mock_route = mock.MagicMock(routes.CHANNEL) with mock.patch.object(routes, "CHANNEL", compile=mock.MagicMock(return_value=mock_route)): - assert await http_client_impl.modify_channel("6942069420") is mock_response - routes.CHANNEL.compile.assert_called_once_with(http_client_impl.PATCH, channel_id="6942069420") - http_client_impl._request.assert_called_once_with(mock_route, json_body={}, reason=...) + assert await rest_impl.modify_channel("6942069420") is mock_response + routes.CHANNEL.compile.assert_called_once_with(rest_impl.PATCH, channel_id="6942069420") + rest_impl._request.assert_called_once_with(mock_route, json_body={}, reason=...) @pytest.mark.asyncio - async def test_modify_channel_with_optionals(self, http_client_impl): + async def test_modify_channel_with_optionals(self, rest_impl): mock_response = {"id": "20393939"} - http_client_impl._request.return_value = mock_response + rest_impl._request.return_value = mock_response mock_route = mock.MagicMock(routes.CHANNEL) with mock.patch.object(routes, "CHANNEL", compile=mock.MagicMock(return_value=mock_route)): - result = await http_client_impl.modify_channel( + result = await rest_impl.modify_channel( "6942069420", position=22, topic="HAHAHAHHAHAHA", @@ -560,8 +618,8 @@ async def test_modify_channel_with_optionals(self, http_client_impl): reason="Get channel'ed", ) assert result is mock_response - routes.CHANNEL.compile.assert_called_once_with(http_client_impl.PATCH, channel_id="6942069420") - http_client_impl._request.assert_called_once_with( + routes.CHANNEL.compile.assert_called_once_with(rest_impl.PATCH, channel_id="6942069420") + rest_impl._request.assert_called_once_with( mock_route, json_body={ "position": 22, @@ -577,66 +635,66 @@ async def test_modify_channel_with_optionals(self, http_client_impl): ) @pytest.mark.asyncio - async def test_delete_channel_close(self, http_client_impl): + async def test_delete_channel_close(self, rest_impl): mock_route = mock.MagicMock(routes.CHANNEL) with mock.patch.object(routes, "CHANNEL", compile=mock.MagicMock(return_value=mock_route)): - assert await http_client_impl.delete_close_channel("939392929") is None - routes.CHANNEL.compile.assert_called_once_with(http_client_impl.DELETE, channel_id="939392929") - http_client_impl._request.assert_called_once_with(mock_route) + assert await rest_impl.delete_close_channel("939392929") is None + routes.CHANNEL.compile.assert_called_once_with(rest_impl.DELETE, channel_id="939392929") + rest_impl._request.assert_called_once_with(mock_route) @pytest.mark.asyncio - async def test_get_channel_messages_without_optionals(self, http_client_impl): + async def test_get_channel_messages_without_optionals(self, rest_impl): mock_response = [{"id": "29492", "content": "Kon'nichiwa"}] - http_client_impl._request.return_value = mock_response + rest_impl._request.return_value = mock_response mock_route = mock.MagicMock(routes.CHANNEL_MESSAGES) with mock.patch.object(routes, "CHANNEL_MESSAGES", compile=mock.MagicMock(return_value=mock_route)): - assert await http_client_impl.get_channel_messages("9292929292") is mock_response - routes.CHANNEL_MESSAGES.compile.assert_called_once_with(http_client_impl.GET, channel_id="9292929292") - http_client_impl._request.assert_called_once_with(mock_route, query={}) + assert await rest_impl.get_channel_messages("9292929292") is mock_response + routes.CHANNEL_MESSAGES.compile.assert_called_once_with(rest_impl.GET, channel_id="9292929292") + rest_impl._request.assert_called_once_with(mock_route, query={}) @pytest.mark.asyncio - async def test_get_channel_messages_with_optionals(self, http_client_impl): + async def test_get_channel_messages_with_optionals(self, rest_impl): mock_response = [{"id": "29492", "content": "Kon'nichiwa"}] - http_client_impl._request.return_value = mock_response + rest_impl._request.return_value = mock_response mock_route = mock.MagicMock(routes.CHANNEL_MESSAGES) with mock.patch.object(routes, "CHANNEL_MESSAGES", compile=mock.MagicMock(return_value=mock_route)): assert ( - await http_client_impl.get_channel_messages( + await rest_impl.get_channel_messages( "9292929292", limit=42, after="293939393", before="4945959595", around="44444444", ) is mock_response ) - routes.CHANNEL_MESSAGES.compile.assert_called_once_with(http_client_impl.GET, channel_id="9292929292") - http_client_impl._request.assert_called_once_with( + routes.CHANNEL_MESSAGES.compile.assert_called_once_with(rest_impl.GET, channel_id="9292929292") + rest_impl._request.assert_called_once_with( mock_route, query={"limit": 42, "after": "293939393", "before": "4945959595", "around": "44444444",} ) @pytest.mark.asyncio - async def test_get_channel_message(self, http_client_impl): + async def test_get_channel_message(self, rest_impl): mock_response = {"content": "I'm really into networking with cute routers and modems."} - http_client_impl._request.return_value = mock_response + rest_impl._request.return_value = mock_response mock_route = mock.MagicMock(routes.CHANNEL_MESSAGE) with mock.patch.object(routes, "CHANNEL_MESSAGE", compile=mock.MagicMock(return_value=mock_route)): - assert await http_client_impl.get_channel_message("1111111111", "42424242") is mock_response + assert await rest_impl.get_channel_message("1111111111", "42424242") is mock_response routes.CHANNEL_MESSAGE.compile.assert_called_once_with( - http_client_impl.GET, channel_id="1111111111", message_id="42424242" + rest_impl.GET, channel_id="1111111111", message_id="42424242" ) - http_client_impl._request.assert_called_once_with(mock_route) + rest_impl._request.assert_called_once_with(mock_route) @pytest.mark.asyncio - async def test_create_message_without_optionals(self, http_client_impl): + async def test_create_message_without_optionals(self, rest_impl): mock_response = {"content": "nyaa, nyaa, nyaa."} - http_client_impl._request.return_value = mock_response + rest_impl._request.return_value = mock_response mock_route = mock.MagicMock(routes.CHANNEL_MESSAGE) mock_form = mock.MagicMock(spec_set=aiohttp.FormData, add_field=mock.MagicMock()) with mock.patch.object(routes, "CHANNEL_MESSAGES", compile=mock.MagicMock(return_value=mock_route)): with mock.patch.object(aiohttp, "FormData", autospec=True, return_value=mock_form): - assert await http_client_impl.create_message("22222222") is mock_response - routes.CHANNEL_MESSAGES.compile.assert_called_once_with(http_client_impl.POST, channel_id="22222222") + assert await rest_impl.create_message("22222222") is mock_response + routes.CHANNEL_MESSAGES.compile.assert_called_once_with(rest_impl.POST, channel_id="22222222") mock_form.add_field.assert_called_once_with( "payload_json", json.dumps({}), content_type="application/json" ) - http_client_impl._request.assert_called_once_with(mock_route, form_body=mock_form, re_seekable_resources=[]) + rest_impl._request.assert_called_once_with(mock_route, form_body=mock_form, re_seekable_resources=[]) @pytest.mark.asyncio @unittest.mock.patch.object(routes, "CHANNEL_MESSAGES") @@ -644,10 +702,10 @@ async def test_create_message_without_optionals(self, http_client_impl): @unittest.mock.patch.object(storage, "make_resource_seekable") @unittest.mock.patch.object(json, "dumps") async def test_create_message_with_optionals( - self, dumps, make_resource_seekable, FormData, CHANNEL_MESSAGES, http_client_impl + self, dumps, make_resource_seekable, FormData, CHANNEL_MESSAGES, rest_impl ): mock_response = {"content": "nyaa, nyaa, nyaa."} - http_client_impl._request.return_value = mock_response + rest_impl._request.return_value = mock_response mock_route = mock.MagicMock(routes.CHANNEL_MESSAGE) CHANNEL_MESSAGES.compile.return_value = mock_route mock_form = mock.MagicMock(spec_set=aiohttp.FormData, add_field=mock.MagicMock()) @@ -657,7 +715,7 @@ async def test_create_message_with_optionals( mock_json = '{"description": "I am a message", "tts": "True"}' dumps.return_value = mock_json - result = await http_client_impl.create_message( + result = await rest_impl.create_message( "22222222", content="I am a message", nonce="ag993okskm_cdolsio", @@ -667,7 +725,7 @@ async def test_create_message_with_optionals( allowed_mentions={"users": ["123"], "roles": ["456"]}, ) assert result is mock_response - CHANNEL_MESSAGES.compile.assert_called_once_with(http_client_impl.POST, channel_id="22222222") + CHANNEL_MESSAGES.compile.assert_called_once_with(rest_impl.POST, channel_id="22222222") make_resource_seekable.assert_called_once_with(b"okdsio9u8oij32") dumps.assert_called_once_with( { @@ -687,190 +745,188 @@ async def test_create_message_with_optionals( any_order=True, ) assert mock_form.add_field.call_count == 2 - http_client_impl._request.assert_called_once_with( - mock_route, form_body=mock_form, re_seekable_resources=[mock_file] - ) + rest_impl._request.assert_called_once_with(mock_route, form_body=mock_form, re_seekable_resources=[mock_file]) @pytest.mark.asyncio - async def test_create_reaction(self, http_client_impl): + async def test_create_reaction(self, rest_impl): mock_route = mock.MagicMock(routes.OWN_REACTION) with mock.patch.object(routes, "OWN_REACTION", compile=mock.MagicMock(return_value=mock_route)): - assert await http_client_impl.create_reaction("20202020", "8484848", "emoji:2929") is None + assert await rest_impl.create_reaction("20202020", "8484848", "emoji:2929") is None routes.OWN_REACTION.compile.assert_called_once_with( - http_client_impl.PUT, channel_id="20202020", message_id="8484848", emoji="emoji:2929" + rest_impl.PUT, channel_id="20202020", message_id="8484848", emoji="emoji:2929" ) - http_client_impl._request.assert_called_once_with(mock_route) + rest_impl._request.assert_called_once_with(mock_route) @pytest.mark.asyncio - async def test_delete_own_reaction(self, http_client_impl): + async def test_delete_own_reaction(self, rest_impl): mock_route = mock.MagicMock(routes.OWN_REACTION) with mock.patch.object(routes, "OWN_REACTION", compile=mock.MagicMock(return_value=mock_route)): - assert await http_client_impl.delete_own_reaction("20202020", "8484848", "emoji:2929") is None + assert await rest_impl.delete_own_reaction("20202020", "8484848", "emoji:2929") is None routes.OWN_REACTION.compile.assert_called_once_with( - http_client_impl.DELETE, channel_id="20202020", message_id="8484848", emoji="emoji:2929" + rest_impl.DELETE, channel_id="20202020", message_id="8484848", emoji="emoji:2929" ) - http_client_impl._request.assert_called_once_with(mock_route) + rest_impl._request.assert_called_once_with(mock_route) @pytest.mark.asyncio - async def test_delete_all_reactions_for_emoji(self, http_client_impl): + async def test_delete_all_reactions_for_emoji(self, rest_impl): mock_route = mock.MagicMock(routes.REACTION_EMOJI) with mock.patch.object(routes, "REACTION_EMOJI", compile=mock.MagicMock(return_value=mock_route)): - assert await http_client_impl.delete_all_reactions_for_emoji("222", "333", "222:owo") is None + assert await rest_impl.delete_all_reactions_for_emoji("222", "333", "222:owo") is None routes.REACTION_EMOJI.compile.assert_called_once_with( - http_client_impl.DELETE, channel_id="222", message_id="333", emoji="222:owo" + rest_impl.DELETE, channel_id="222", message_id="333", emoji="222:owo" ) @pytest.mark.asyncio - async def test_delete_user_reaction(self, http_client_impl): + async def test_delete_user_reaction(self, rest_impl): mock_route = mock.MagicMock(routes.REACTION_EMOJI_USER) with mock.patch.object(routes, "REACTION_EMOJI_USER", compile=mock.MagicMock(return_value=mock_route)): - assert await http_client_impl.delete_user_reaction("11111", "4444", "emoji:42", "29292992") is None + assert await rest_impl.delete_user_reaction("11111", "4444", "emoji:42", "29292992") is None routes.REACTION_EMOJI_USER.compile.assert_called_once_with( - http_client_impl.DELETE, channel_id="11111", message_id="4444", emoji="emoji:42", user_id="29292992" + rest_impl.DELETE, channel_id="11111", message_id="4444", emoji="emoji:42", user_id="29292992" ) - http_client_impl._request.assert_called_once_with(mock_route) + rest_impl._request.assert_called_once_with(mock_route) @pytest.mark.asyncio - async def test_get_reactions_without_optionals(self, http_client_impl): + async def test_get_reactions_without_optionals(self, rest_impl): mock_response = [{"id": "42"}] - http_client_impl._request.return_value = mock_response + rest_impl._request.return_value = mock_response mock_route = mock.MagicMock(routes.REACTIONS) with mock.patch.object(routes, "REACTIONS", compile=mock.MagicMock(return_value=mock_route)): - assert await http_client_impl.get_reactions("29292929", "48484848", "emoji:42") is mock_response + assert await rest_impl.get_reactions("29292929", "48484848", "emoji:42") is mock_response routes.REACTIONS.compile.assert_called_once_with( - http_client_impl.GET, channel_id="29292929", message_id="48484848", emoji="emoji:42" + rest_impl.GET, channel_id="29292929", message_id="48484848", emoji="emoji:42" ) - http_client_impl._request.assert_called_once_with(mock_route, query={}) + rest_impl._request.assert_called_once_with(mock_route, query={}) @pytest.mark.asyncio - async def test_get_reactions_with_optionals(self, http_client_impl): + async def test_get_reactions_with_optionals(self, rest_impl): mock_response = [{"id": "42"}] - http_client_impl._request.return_value = mock_response + rest_impl._request.return_value = mock_response mock_route = mock.MagicMock(routes.REACTIONS) with mock.patch.object(routes, "REACTIONS", compile=mock.MagicMock(return_value=mock_route)): assert ( - await http_client_impl.get_reactions("29292929", "48484848", "emoji:42", after="3333333", limit=40) + await rest_impl.get_reactions("29292929", "48484848", "emoji:42", after="3333333", limit=40) is mock_response ) routes.REACTIONS.compile.assert_called_once_with( - http_client_impl.GET, channel_id="29292929", message_id="48484848", emoji="emoji:42" + rest_impl.GET, channel_id="29292929", message_id="48484848", emoji="emoji:42" ) - http_client_impl._request.assert_called_once_with(mock_route, query={"after": "3333333", "limit": 40}) + rest_impl._request.assert_called_once_with(mock_route, query={"after": "3333333", "limit": 40}) @pytest.mark.asyncio - async def test_delete_all_reactions(self, http_client_impl): + async def test_delete_all_reactions(self, rest_impl): mock_route = mock.MagicMock(routes.ALL_REACTIONS) with mock.patch.object(routes, "ALL_REACTIONS", compile=mock.MagicMock(return_value=mock_route)): - assert await http_client_impl.delete_all_reactions("44444", "999999") is None + assert await rest_impl.delete_all_reactions("44444", "999999") is None routes.ALL_REACTIONS.compile.assert_called_once_with( - http_client_impl.DELETE, channel_id="44444", message_id="999999" + rest_impl.DELETE, channel_id="44444", message_id="999999" ) - http_client_impl._request.assert_called_once_with(mock_route) + rest_impl._request.assert_called_once_with(mock_route) @pytest.mark.asyncio - async def test_edit_message_without_optionals(self, http_client_impl): + async def test_edit_message_without_optionals(self, rest_impl): mock_response = {"flags": 3, "content": "edited for the win."} - http_client_impl._request.return_value = mock_response + rest_impl._request.return_value = mock_response mock_route = mock.MagicMock(routes.CHANNEL_MESSAGE) with mock.patch.object(routes, "CHANNEL_MESSAGE", compile=mock.MagicMock(return_value=mock_route)): - assert await http_client_impl.edit_message("9292929", "484848") is mock_response + assert await rest_impl.edit_message("9292929", "484848") is mock_response routes.CHANNEL_MESSAGE.compile.assert_called_once_with( - http_client_impl.PATCH, channel_id="9292929", message_id="484848" + rest_impl.PATCH, channel_id="9292929", message_id="484848" ) - http_client_impl._request.assert_called_once_with(mock_route, json_body={}) + rest_impl._request.assert_called_once_with(mock_route, json_body={}) @pytest.mark.asyncio - async def test_edit_message_with_optionals(self, http_client_impl): + async def test_edit_message_with_optionals(self, rest_impl): mock_response = {"flags": 3, "content": "edited for the win."} - http_client_impl._request.return_value = mock_response + rest_impl._request.return_value = mock_response mock_route = mock.MagicMock(routes.CHANNEL_MESSAGE) with mock.patch.object(routes, "CHANNEL_MESSAGE", compile=mock.MagicMock(return_value=mock_route)): assert ( - await http_client_impl.edit_message( + await rest_impl.edit_message( "9292929", "484848", content="42", embed={"content": "I AM AN EMBED"}, flags=2 ) is mock_response ) routes.CHANNEL_MESSAGE.compile.assert_called_once_with( - http_client_impl.PATCH, channel_id="9292929", message_id="484848" + rest_impl.PATCH, channel_id="9292929", message_id="484848" ) - http_client_impl._request.assert_called_once_with( + rest_impl._request.assert_called_once_with( mock_route, json_body={"content": "42", "embed": {"content": "I AM AN EMBED"}, "flags": 2} ) @pytest.mark.asyncio - async def test_delete_message(self, http_client_impl): + async def test_delete_message(self, rest_impl): mock_route = mock.MagicMock(routes.CHANNEL_MESSAGE) with mock.patch.object(routes, "CHANNEL_MESSAGE", compile=mock.MagicMock(return_value=mock_route)): - assert await http_client_impl.delete_message("20202", "484848") is None + assert await rest_impl.delete_message("20202", "484848") is None routes.CHANNEL_MESSAGE.compile.assert_called_once_with( - http_client_impl.DELETE, channel_id="20202", message_id="484848" + rest_impl.DELETE, channel_id="20202", message_id="484848" ) - http_client_impl._request.assert_called_once_with(mock_route) + rest_impl._request.assert_called_once_with(mock_route) @pytest.mark.asyncio - async def test_bulk_delete_messages(self, http_client_impl): + async def test_bulk_delete_messages(self, rest_impl): mock_route = mock.MagicMock(routes.CHANNEL_MESSAGES_BULK_DELETE) with mock.patch.object(routes, "CHANNEL_MESSAGES_BULK_DELETE", compile=mock.MagicMock(return_value=mock_route)): - assert await http_client_impl.bulk_delete_messages("111", ["222", "333"]) is None - routes.CHANNEL_MESSAGES_BULK_DELETE.compile.assert_called_once_with(http_client_impl.POST, channel_id="111") - http_client_impl._request.assert_called_once_with(mock_route, json_body={"messages": ["222", "333"]}) + assert await rest_impl.bulk_delete_messages("111", ["222", "333"]) is None + routes.CHANNEL_MESSAGES_BULK_DELETE.compile.assert_called_once_with(rest_impl.POST, channel_id="111") + rest_impl._request.assert_called_once_with(mock_route, json_body={"messages": ["222", "333"]}) @pytest.mark.asyncio - async def test_edit_channel_permissions_without_optionals(self, http_client_impl): + async def test_edit_channel_permissions_without_optionals(self, rest_impl): mock_route = mock.MagicMock(routes.CHANNEL_PERMISSIONS) with mock.patch.object(routes, "CHANNEL_PERMISSIONS", compile=mock.MagicMock(return_value=mock_route)): - assert await http_client_impl.edit_channel_permissions("101010101010", "100101010") is None + assert await rest_impl.edit_channel_permissions("101010101010", "100101010") is None routes.CHANNEL_PERMISSIONS.compile.assert_called_once_with( - http_client_impl.PATCH, channel_id="101010101010", overwrite_id="100101010" + rest_impl.PATCH, channel_id="101010101010", overwrite_id="100101010" ) - http_client_impl._request.assert_called_once_with(mock_route, json_body={}, reason=...) + rest_impl._request.assert_called_once_with(mock_route, json_body={}, reason=...) @pytest.mark.asyncio - async def test_edit_channel_permissions_with_optionals(self, http_client_impl): + async def test_edit_channel_permissions_with_optionals(self, rest_impl): mock_route = mock.MagicMock(routes.CHANNEL_PERMISSIONS) with mock.patch.object(routes, "CHANNEL_PERMISSIONS", compile=mock.MagicMock(return_value=mock_route)): assert ( - await http_client_impl.edit_channel_permissions( + await rest_impl.edit_channel_permissions( "101010101010", "100101010", allow=243, deny=333, type_="user", reason="get vectored" ) is None ) routes.CHANNEL_PERMISSIONS.compile.assert_called_once_with( - http_client_impl.PATCH, channel_id="101010101010", overwrite_id="100101010" + rest_impl.PATCH, channel_id="101010101010", overwrite_id="100101010" ) - http_client_impl._request.assert_called_once_with( + rest_impl._request.assert_called_once_with( mock_route, json_body={"allow": 243, "deny": 333, "type": "user"}, reason="get vectored" ) @pytest.mark.asyncio - async def test_get_channel_invites(self, http_client_impl): + async def test_get_channel_invites(self, rest_impl): mock_response = {"code": "dasd32"} - http_client_impl._request.return_value = mock_response + rest_impl._request.return_value = mock_response mock_route = mock.MagicMock(routes.CHANNEL_INVITES) with mock.patch.object(routes, "CHANNEL_INVITES", compile=mock.MagicMock(return_value=mock_route)): - assert await http_client_impl.get_channel_invites("999999999") is mock_response - routes.CHANNEL_INVITES.compile.assert_called_once_with(http_client_impl.GET, channel_id="999999999") - http_client_impl._request.assert_called_once_with(mock_route) + assert await rest_impl.get_channel_invites("999999999") is mock_response + routes.CHANNEL_INVITES.compile.assert_called_once_with(rest_impl.GET, channel_id="999999999") + rest_impl._request.assert_called_once_with(mock_route) @pytest.mark.asyncio - async def test_create_channel_invite_without_optionals(self, http_client_impl): + async def test_create_channel_invite_without_optionals(self, rest_impl): mock_response = {"code": "ro934jsd"} - http_client_impl._request.return_value = mock_response + rest_impl._request.return_value = mock_response mock_route = mock.MagicMock(routes.CHANNEL_INVITES) with mock.patch.object(routes, "CHANNEL_INVITES", compile=mock.MagicMock(return_value=mock_route)): - assert await http_client_impl.create_channel_invite("99992929") is mock_response - routes.CHANNEL_INVITES.compile.assert_called_once_with(http_client_impl.POST, channel_id="99992929") - http_client_impl._request.assert_called_once_with(mock_route, json_body={}, reason=...) + assert await rest_impl.create_channel_invite("99992929") is mock_response + routes.CHANNEL_INVITES.compile.assert_called_once_with(rest_impl.POST, channel_id="99992929") + rest_impl._request.assert_called_once_with(mock_route, json_body={}, reason=...) @pytest.mark.asyncio - async def test_create_channel_invite_with_optionals(self, http_client_impl): + async def test_create_channel_invite_with_optionals(self, rest_impl): mock_response = {"code": "ro934jsd"} - http_client_impl._request.return_value = mock_response + rest_impl._request.return_value = mock_response mock_route = mock.MagicMock(routes.CHANNEL_INVITES) with mock.patch.object(routes, "CHANNEL_INVITES", compile=mock.MagicMock(return_value=mock_route)): assert ( - await http_client_impl.create_channel_invite( + await rest_impl.create_channel_invite( "99992929", max_age=5, max_uses=7, @@ -882,8 +938,8 @@ async def test_create_channel_invite_with_optionals(self, http_client_impl): ) is mock_response ) - routes.CHANNEL_INVITES.compile.assert_called_once_with(http_client_impl.POST, channel_id="99992929") - http_client_impl._request.assert_called_once_with( + routes.CHANNEL_INVITES.compile.assert_called_once_with(rest_impl.POST, channel_id="99992929") + rest_impl._request.assert_called_once_with( mock_route, json_body={ "max_age": 5, @@ -897,167 +953,161 @@ async def test_create_channel_invite_with_optionals(self, http_client_impl): ) @pytest.mark.asyncio - async def test_delete_channel_permission(self, http_client_impl): + async def test_delete_channel_permission(self, rest_impl): mock_route = mock.MagicMock(routes.CHANNEL_PERMISSIONS) with mock.patch.object(routes, "CHANNEL_PERMISSIONS", compile=mock.MagicMock(return_value=mock_route)): - assert await http_client_impl.delete_channel_permission("9292929", "74574747") is None + assert await rest_impl.delete_channel_permission("9292929", "74574747") is None routes.CHANNEL_PERMISSIONS.compile.assert_called_once_with( - http_client_impl.DELETE, channel_id="9292929", overwrite_id="74574747" + rest_impl.DELETE, channel_id="9292929", overwrite_id="74574747" ) - http_client_impl._request.assert_called_once_with(mock_route) + rest_impl._request.assert_called_once_with(mock_route) @pytest.mark.asyncio - async def test_trigger_typing_indicator(self, http_client_impl): + async def test_trigger_typing_indicator(self, rest_impl): mock_route = mock.MagicMock(routes.CHANNEL_TYPING) with mock.patch.object(routes, "CHANNEL_TYPING", compile=mock.MagicMock(return_value=mock_route)): - assert await http_client_impl.trigger_typing_indicator("11111111111") is None - routes.CHANNEL_TYPING.compile.assert_called_once_with(http_client_impl.POST, channel_id="11111111111") - http_client_impl._request.assert_called_once_with(mock_route) + assert await rest_impl.trigger_typing_indicator("11111111111") is None + routes.CHANNEL_TYPING.compile.assert_called_once_with(rest_impl.POST, channel_id="11111111111") + rest_impl._request.assert_called_once_with(mock_route) @pytest.mark.asyncio - async def test_get_pinned_messages(self, http_client_impl): + async def test_get_pinned_messages(self, rest_impl): mock_response = [{"content": "no u", "id": "4212"}] - http_client_impl._request.return_value = mock_response + rest_impl._request.return_value = mock_response mock_route = mock.MagicMock(routes.CHANNEL_PINS) with mock.patch.object(routes, "CHANNEL_PINS", compile=mock.MagicMock(return_value=mock_route)): - assert await http_client_impl.get_pinned_messages("393939") is mock_response - routes.CHANNEL_PINS.compile.assert_called_once_with(http_client_impl.GET, channel_id="393939") - http_client_impl._request.assert_called_once_with(mock_route) + assert await rest_impl.get_pinned_messages("393939") is mock_response + routes.CHANNEL_PINS.compile.assert_called_once_with(rest_impl.GET, channel_id="393939") + rest_impl._request.assert_called_once_with(mock_route) @pytest.mark.asyncio - async def test_add_pinned_channel_message(self, http_client_impl): + async def test_add_pinned_channel_message(self, rest_impl): mock_route = mock.MagicMock(routes.CHANNEL_PIN) with mock.patch.object(routes, "CHANNEL_PINS", compile=mock.MagicMock(return_value=mock_route)): - assert await http_client_impl.add_pinned_channel_message("292929", "48458484") is None + assert await rest_impl.add_pinned_channel_message("292929", "48458484") is None routes.CHANNEL_PINS.compile.assert_called_once_with( - http_client_impl.PUT, channel_id="292929", message_id="48458484" + rest_impl.PUT, channel_id="292929", message_id="48458484" ) - http_client_impl._request.assert_called_once_with(mock_route) + rest_impl._request.assert_called_once_with(mock_route) @pytest.mark.asyncio - async def test_delete_pinned_channel_message(self, http_client_impl): + async def test_delete_pinned_channel_message(self, rest_impl): mock_route = mock.MagicMock(routes.CHANNEL_PIN) with mock.patch.object(routes, "CHANNEL_PIN", compile=mock.MagicMock(return_value=mock_route)): - assert await http_client_impl.delete_pinned_channel_message("929292", "292929") is None + assert await rest_impl.delete_pinned_channel_message("929292", "292929") is None routes.CHANNEL_PIN.compile.assert_called_once_with( - http_client_impl.DELETE, channel_id="929292", message_id="292929" + rest_impl.DELETE, channel_id="929292", message_id="292929" ) - http_client_impl._request.assert_called_once_with(mock_route) + rest_impl._request.assert_called_once_with(mock_route) @pytest.mark.asyncio - async def test_list_guild_emojis(self, http_client_impl): + async def test_list_guild_emojis(self, rest_impl): mock_response = [{"id": "444", "name": "nekonyan"}] - http_client_impl._request.return_value = mock_response + rest_impl._request.return_value = mock_response mock_route = mock.MagicMock(routes.GUILD_EMOJIS) with mock.patch.object(routes, "GUILD_EMOJIS", compile=mock.MagicMock(return_value=mock_route)): - assert await http_client_impl.list_guild_emojis("9929292") is mock_response - routes.GUILD_EMOJIS.compile.assert_called_once_with(http_client_impl.GET, guild_id="9929292") - http_client_impl._request.assert_called_once_with(mock_route) + assert await rest_impl.list_guild_emojis("9929292") is mock_response + routes.GUILD_EMOJIS.compile.assert_called_once_with(rest_impl.GET, guild_id="9929292") + rest_impl._request.assert_called_once_with(mock_route) @pytest.mark.asyncio - async def test_get_guild_emoji(self, http_client_impl): + async def test_get_guild_emoji(self, rest_impl): mock_response = {"id": "444", "name": "nekonyan"} - http_client_impl._request.return_value = mock_response + rest_impl._request.return_value = mock_response mock_route = mock.MagicMock(routes.GUILD_EMOJI) with mock.patch.object(routes, "GUILD_EMOJI", compile=mock.MagicMock(return_value=mock_route)): - assert await http_client_impl.get_guild_emoji("292929", "44848") is mock_response - routes.GUILD_EMOJI.compile.assert_called_once_with( - http_client_impl.GET, guild_id="292929", emoji_id="44848" - ) - http_client_impl._request.assert_called_once_with(mock_route) + assert await rest_impl.get_guild_emoji("292929", "44848") is mock_response + routes.GUILD_EMOJI.compile.assert_called_once_with(rest_impl.GET, guild_id="292929", emoji_id="44848") + rest_impl._request.assert_called_once_with(mock_route) @pytest.mark.asyncio - async def test_create_guild_emoji_without_optionals(self, http_client_impl): + async def test_create_guild_emoji_without_optionals(self, rest_impl): mock_response = {"id": "33", "name": "OwO"} - http_client_impl._request.return_value = mock_response + rest_impl._request.return_value = mock_response mock_route = mock.MagicMock(routes.GUILD_EMOJI) mock_image_data = "data:image/png;base64,iVBORw0KGgpibGFo" with mock.patch.object(routes, "GUILD_EMOJIS", compile=mock.MagicMock(return_value=mock_route)): with mock.patch.object(transformations, "image_bytes_to_image_data", return_value=mock_image_data): - result = await http_client_impl.create_guild_emoji("2222", "iEmoji", b"\211PNG\r\n\032\nblah") + result = await rest_impl.create_guild_emoji("2222", "iEmoji", b"\211PNG\r\n\032\nblah") assert result is mock_response transformations.image_bytes_to_image_data.assert_called_once_with(b"\211PNG\r\n\032\nblah") - routes.GUILD_EMOJIS.compile.assert_called_once_with(http_client_impl.POST, guild_id="2222") - http_client_impl._request.assert_called_once_with( + routes.GUILD_EMOJIS.compile.assert_called_once_with(rest_impl.POST, guild_id="2222") + rest_impl._request.assert_called_once_with( mock_route, json_body={"name": "iEmoji", "roles": [], "image": mock_image_data}, reason=..., ) @pytest.mark.asyncio - async def test_create_guild_emoji_with_optionals(self, http_client_impl): + async def test_create_guild_emoji_with_optionals(self, rest_impl): mock_response = {"id": "33", "name": "OwO"} - http_client_impl._request.return_value = mock_response + rest_impl._request.return_value = mock_response mock_route = mock.MagicMock(routes.GUILD_EMOJI) mock_image_data = "data:image/png;base64,iVBORw0KGgpibGFo" with mock.patch.object(routes, "GUILD_EMOJIS", compile=mock.MagicMock(return_value=mock_route)): with mock.patch.object(transformations, "image_bytes_to_image_data", return_value=mock_image_data): - result = await http_client_impl.create_guild_emoji( + result = await rest_impl.create_guild_emoji( "2222", "iEmoji", b"\211PNG\r\n\032\nblah", roles=["292929", "484884"], reason="uwu owo" ) assert result is mock_response transformations.image_bytes_to_image_data.assert_called_once_with(b"\211PNG\r\n\032\nblah") - routes.GUILD_EMOJIS.compile.assert_called_once_with(http_client_impl.POST, guild_id="2222") - http_client_impl._request.assert_called_once_with( + routes.GUILD_EMOJIS.compile.assert_called_once_with(rest_impl.POST, guild_id="2222") + rest_impl._request.assert_called_once_with( mock_route, json_body={"name": "iEmoji", "roles": ["292929", "484884"], "image": mock_image_data}, reason="uwu owo", ) @pytest.mark.asyncio - async def test_modify_guild_emoji_without_optionals(self, http_client_impl): + async def test_modify_guild_emoji_without_optionals(self, rest_impl): mock_response = {"id": "20202", "name": "jeje"} - http_client_impl._request.return_value = mock_response + rest_impl._request.return_value = mock_response mock_route = mock.MagicMock(routes.GUILD_EMOJI) with mock.patch.object(routes, "GUILD_EMOJI", compile=mock.MagicMock(return_value=mock_route)): - assert await http_client_impl.modify_guild_emoji("292929", "3484848") is mock_response - routes.GUILD_EMOJI.compile.assert_called_once_with( - http_client_impl.PATCH, guild_id="292929", emoji_id="3484848" - ) - http_client_impl._request.assert_called_once_with(mock_route, json_body={}, reason=...) + assert await rest_impl.modify_guild_emoji("292929", "3484848") is mock_response + routes.GUILD_EMOJI.compile.assert_called_once_with(rest_impl.PATCH, guild_id="292929", emoji_id="3484848") + rest_impl._request.assert_called_once_with(mock_route, json_body={}, reason=...) @pytest.mark.asyncio - async def test_modify_guild_emoji_with_optionals(self, http_client_impl): + async def test_modify_guild_emoji_with_optionals(self, rest_impl): mock_response = {"id": "20202", "name": "jeje"} - http_client_impl._request.return_value = mock_response + rest_impl._request.return_value = mock_response mock_route = mock.MagicMock(routes.GUILD_EMOJI) with mock.patch.object(routes, "GUILD_EMOJI", compile=mock.MagicMock(return_value=mock_route)): assert ( - await http_client_impl.modify_guild_emoji("292929", "3484848", name="ok", roles=["222", "111"]) + await rest_impl.modify_guild_emoji("292929", "3484848", name="ok", roles=["222", "111"]) is mock_response ) - routes.GUILD_EMOJI.compile.assert_called_once_with( - http_client_impl.PATCH, guild_id="292929", emoji_id="3484848" - ) - http_client_impl._request.assert_called_once_with( + routes.GUILD_EMOJI.compile.assert_called_once_with(rest_impl.PATCH, guild_id="292929", emoji_id="3484848") + rest_impl._request.assert_called_once_with( mock_route, json_body={"name": "ok", "roles": ["222", "111"]}, reason=... ) @pytest.mark.asyncio - async def test_delete_guild_emoji(self, http_client_impl): + async def test_delete_guild_emoji(self, rest_impl): mock_route = mock.MagicMock(routes.GUILD_EMOJI) with mock.patch.object(routes, "GUILD_EMOJI", compile=mock.MagicMock(return_value=mock_route)): - assert await http_client_impl.delete_guild_emoji("202", "4454") is None - routes.GUILD_EMOJI.compile.assert_called_once_with(http_client_impl.DELETE, guild_id="202", emoji_id="4454") - http_client_impl._request.assert_called_once_with(mock_route) + assert await rest_impl.delete_guild_emoji("202", "4454") is None + routes.GUILD_EMOJI.compile.assert_called_once_with(rest_impl.DELETE, guild_id="202", emoji_id="4454") + rest_impl._request.assert_called_once_with(mock_route) @pytest.mark.asyncio - async def test_create_guild_without_optionals(self, http_client_impl): + async def test_create_guild_without_optionals(self, rest_impl): mock_response = {"id": "99999", "name": "Guildith-Sama"} - http_client_impl._request.return_value = mock_response + rest_impl._request.return_value = mock_response mock_route = mock.MagicMock(routes.GUILD) with mock.patch.object(routes, "GUILDS", compile=mock.MagicMock(return_value=mock_route)): - assert await http_client_impl.create_guild("GUILD TIME") is mock_response - routes.GUILDS.compile.assert_called_once_with(http_client_impl.POST) - http_client_impl._request.assert_called_once_with(mock_route, json_body={"name": "GUILD TIME"}) + assert await rest_impl.create_guild("GUILD TIME") is mock_response + routes.GUILDS.compile.assert_called_once_with(rest_impl.POST) + rest_impl._request.assert_called_once_with(mock_route, json_body={"name": "GUILD TIME"}) @pytest.mark.asyncio - async def test_create_guild_with_optionals(self, http_client_impl): + async def test_create_guild_with_optionals(self, rest_impl): mock_response = {"id": "99999", "name": "Guildith-Sama"} - http_client_impl._request.return_value = mock_response + rest_impl._request.return_value = mock_response mock_route = mock.MagicMock(routes.GUILD) mock_image_data = "data:image/png;base64,iVBORw0KGgpibGFo" with mock.patch.object(routes, "GUILDS", compile=mock.MagicMock(return_value=mock_route)): with mock.patch.object(transformations, "image_bytes_to_image_data", return_value=mock_image_data): - result = await http_client_impl.create_guild( + result = await rest_impl.create_guild( "GUILD TIME", region="london", icon=b"\211PNG\r\n\032\nblah", @@ -1067,9 +1117,9 @@ async def test_create_guild_with_optionals(self, http_client_impl): channels=[{"type": 0, "name": "444"}], ) assert result is mock_response - routes.GUILDS.compile.assert_called_once_with(http_client_impl.POST) + routes.GUILDS.compile.assert_called_once_with(rest_impl.POST) transformations.image_bytes_to_image_data.assert_called_once_with(b"\211PNG\r\n\032\nblah") - http_client_impl._request.assert_called_once_with( + rest_impl._request.assert_called_once_with( mock_route, json_body={ "name": "GUILD TIME", @@ -1083,29 +1133,29 @@ async def test_create_guild_with_optionals(self, http_client_impl): ) @pytest.mark.asyncio - async def test_get_guild(self, http_client_impl): + async def test_get_guild(self, rest_impl): mock_response = {"id": "42", "name": "Hikari"} - http_client_impl._request.return_value = mock_response + rest_impl._request.return_value = mock_response mock_route = mock.MagicMock(routes.GUILD) with mock.patch.object(routes, "GUILD", compile=mock.MagicMock(return_value=mock_route)): - assert await http_client_impl.get_guild("3939393993939") is mock_response - routes.GUILD.compile.assert_called_once_with(http_client_impl.GET, guild_id="3939393993939") - http_client_impl._request.assert_called_once_with(mock_route) + assert await rest_impl.get_guild("3939393993939") is mock_response + routes.GUILD.compile.assert_called_once_with(rest_impl.GET, guild_id="3939393993939") + rest_impl._request.assert_called_once_with(mock_route) @pytest.mark.asyncio - async def test_modify_guild_without_optionals(self, http_client_impl): + async def test_modify_guild_without_optionals(self, rest_impl): mock_response = {"id": "42", "name": "Hikari"} - http_client_impl._request.return_value = mock_response + rest_impl._request.return_value = mock_response mock_route = mock.MagicMock(routes.GUILD) with mock.patch.object(routes, "GUILD", compile=mock.MagicMock(return_value=mock_route)): - assert await http_client_impl.modify_guild("49949495") is mock_response - routes.GUILD.compile.assert_called_once_with(http_client_impl.PATCH, guild_id="49949495") - http_client_impl._request.assert_called_once_with(mock_route, json_body={}, reason=...) + assert await rest_impl.modify_guild("49949495") is mock_response + routes.GUILD.compile.assert_called_once_with(rest_impl.PATCH, guild_id="49949495") + rest_impl._request.assert_called_once_with(mock_route, json_body={}, reason=...) @pytest.mark.asyncio - async def test_modify_guild_with_optionals(self, http_client_impl): + async def test_modify_guild_with_optionals(self, rest_impl): mock_response = {"id": "42", "name": "Hikari"} - http_client_impl._request.return_value = mock_response + rest_impl._request.return_value = mock_response mock_route = mock.MagicMock(routes.GUILD) mock_icon_data = "data:image/png;base64,iVBORw0KGgpibGFo" mock_splash_data = "data:image/png;base64,iVBORw0KGgpicnVo" @@ -1113,7 +1163,7 @@ async def test_modify_guild_with_optionals(self, http_client_impl): with mock.patch.object( transformations, "image_bytes_to_image_data", side_effect=(mock_icon_data, mock_splash_data) ): - result = await http_client_impl.modify_guild( + result = await rest_impl.modify_guild( "49949495", name="Deutschland", region="deutschland", @@ -1130,7 +1180,7 @@ async def test_modify_guild_with_optionals(self, http_client_impl): ) assert result is mock_response - routes.GUILD.compile.assert_called_once_with(http_client_impl.PATCH, guild_id="49949495") + routes.GUILD.compile.assert_called_once_with(rest_impl.PATCH, guild_id="49949495") assert transformations.image_bytes_to_image_data.call_count == 2 transformations.image_bytes_to_image_data.assert_has_calls( ( @@ -1140,7 +1190,7 @@ async def test_modify_guild_with_optionals(self, http_client_impl): mock.call(b"\211PNG\r\n\032\nbruh"), ) ) - http_client_impl._request.assert_called_once_with( + rest_impl._request.assert_called_once_with( mock_route, json_body={ "name": "Deutschland", @@ -1159,40 +1209,40 @@ async def test_modify_guild_with_optionals(self, http_client_impl): ) @pytest.mark.asyncio - async def test_delete_guild(self, http_client_impl): + async def test_delete_guild(self, rest_impl): mock_route = mock.MagicMock(routes.GUILD) with mock.patch.object(routes, "GUILD", compile=mock.MagicMock(return_value=mock_route)): - assert await http_client_impl.delete_guild("92847478") is None - routes.GUILD.compile.assert_called_once_with(http_client_impl.DELETE, guild_id="92847478") - http_client_impl._request.assert_called_once_with(mock_route) + assert await rest_impl.delete_guild("92847478") is None + routes.GUILD.compile.assert_called_once_with(rest_impl.DELETE, guild_id="92847478") + rest_impl._request.assert_called_once_with(mock_route) @pytest.mark.asyncio - async def test_get_guild_channels(self, http_client_impl): + async def test_get_guild_channels(self, rest_impl): mock_response = [{"type": 2, "id": "21", "name": "Watashi-wa-channel-desu"}] - http_client_impl._request.return_value = mock_response + rest_impl._request.return_value = mock_response mock_route = mock.MagicMock(routes.GUILD_CHANNELS) with mock.patch.object(routes, "GUILD_CHANNELS", compile=mock.MagicMock(return_value=mock_route)): - assert await http_client_impl.get_guild_channels("393939393") is mock_response - routes.GUILD_CHANNELS.compile.assert_called_once_with(http_client_impl.GET, guild_id="393939393") - http_client_impl._request.assert_called_once_with(mock_route) + assert await rest_impl.get_guild_channels("393939393") is mock_response + routes.GUILD_CHANNELS.compile.assert_called_once_with(rest_impl.GET, guild_id="393939393") + rest_impl._request.assert_called_once_with(mock_route) @pytest.mark.asyncio - async def test_create_guild_channel_without_optionals(self, http_client_impl): + async def test_create_guild_channel_without_optionals(self, rest_impl): mock_response = {"type": 2, "id": "3333"} - http_client_impl._request.return_value = mock_response + rest_impl._request.return_value = mock_response mock_route = mock.MagicMock(routes.GUILD_CHANNELS) with mock.patch.object(routes, "GUILD_CHANNELS", compile=mock.MagicMock(return_value=mock_route)): - assert await http_client_impl.create_guild_channel("292929", "I am a channel") is mock_response - routes.GUILD_CHANNELS.compile.assert_called_once_with(http_client_impl.POST, guild_id="292929") - http_client_impl._request.assert_called_once_with(mock_route, json_body={"name": "I am a channel"}, reason=...) + assert await rest_impl.create_guild_channel("292929", "I am a channel") is mock_response + routes.GUILD_CHANNELS.compile.assert_called_once_with(rest_impl.POST, guild_id="292929") + rest_impl._request.assert_called_once_with(mock_route, json_body={"name": "I am a channel"}, reason=...) @pytest.mark.asyncio - async def test_create_guild_channel_with_optionals(self, http_client_impl): + async def test_create_guild_channel_with_optionals(self, rest_impl): mock_response = {"type": 2, "id": "379953393319542784"} - http_client_impl._request.return_value = mock_response + rest_impl._request.return_value = mock_response mock_route = mock.MagicMock(routes.GUILD_CHANNELS) with mock.patch.object(routes, "GUILD_CHANNELS", compile=mock.MagicMock(return_value=mock_route)): - result = await http_client_impl.create_guild_channel( + result = await rest_impl.create_guild_channel( "292929", "I am a channel", type_=2, @@ -1208,8 +1258,8 @@ async def test_create_guild_channel_with_optionals(self, http_client_impl): ) assert result is mock_response - routes.GUILD_CHANNELS.compile.assert_called_once_with(http_client_impl.POST, guild_id="292929") - http_client_impl._request.assert_called_once_with( + routes.GUILD_CHANNELS.compile.assert_called_once_with(rest_impl.POST, guild_id="292929") + rest_impl._request.assert_called_once_with( mock_route, json_body={ "name": "I am a channel", @@ -1227,68 +1277,66 @@ async def test_create_guild_channel_with_optionals(self, http_client_impl): ) @pytest.mark.asyncio - async def test_modify_guild_channel_positions(self, http_client_impl): + async def test_modify_guild_channel_positions(self, rest_impl): mock_route = mock.MagicMock(routes.GUILD_CHANNELS) with mock.patch.object(routes, "GUILD_CHANNELS", compile=mock.MagicMock(return_value=mock_route)): assert ( - await http_client_impl.modify_guild_channel_positions("379953393319542784", ("29292", 0), ("3838", 1)) - is None + await rest_impl.modify_guild_channel_positions("379953393319542784", ("29292", 0), ("3838", 1)) is None ) - routes.GUILD_CHANNELS.compile.assert_called_once_with(http_client_impl.PATCH, guild_id="379953393319542784") - http_client_impl._request.assert_called_once_with( + routes.GUILD_CHANNELS.compile.assert_called_once_with(rest_impl.PATCH, guild_id="379953393319542784") + rest_impl._request.assert_called_once_with( mock_route, json_body=[{"id": "29292", "position": 0}, {"id": "3838", "position": 1}] ) @pytest.mark.asyncio - async def test_get_guild_member(self, http_client_impl): + async def test_get_guild_member(self, rest_impl): mock_response = {"id": "379953393319542784", "nick": "Big Moh"} - http_client_impl._request.return_value = mock_response + rest_impl._request.return_value = mock_response mock_route = mock.MagicMock(routes.GUILD_MEMBER) with mock.patch.object(routes, "GUILD_MEMBER", compile=mock.MagicMock(return_value=mock_route)): - assert await http_client_impl.get_guild_member("115590097100865541", "379953393319542784") is mock_response + assert await rest_impl.get_guild_member("115590097100865541", "379953393319542784") is mock_response routes.GUILD_MEMBER.compile.assert_called_once_with( - http_client_impl.GET, guild_id="115590097100865541", user_id="379953393319542784" + rest_impl.GET, guild_id="115590097100865541", user_id="379953393319542784" ) - http_client_impl._request.assert_called_once_with(mock_route) + rest_impl._request.assert_called_once_with(mock_route) @pytest.mark.asyncio - async def test_list_guild_members_without_optionals(self, http_client_impl): + async def test_list_guild_members_without_optionals(self, rest_impl): mock_response = [{"id": "379953393319542784", "nick": "Big Moh"}] - http_client_impl._request.return_value = mock_response + rest_impl._request.return_value = mock_response mock_route = mock.MagicMock(routes.GUILD_MEMBERS) with mock.patch.object(routes, "GUILD_MEMBERS", compile=mock.MagicMock(return_value=mock_route)): - assert await http_client_impl.list_guild_members("115590097100865541") is mock_response - routes.GUILD_MEMBERS.compile.assert_called_once_with(http_client_impl.GET, guild_id="115590097100865541") - http_client_impl._request.assert_called_once_with(mock_route, query={}) + assert await rest_impl.list_guild_members("115590097100865541") is mock_response + routes.GUILD_MEMBERS.compile.assert_called_once_with(rest_impl.GET, guild_id="115590097100865541") + rest_impl._request.assert_called_once_with(mock_route, query={}) @pytest.mark.asyncio - async def test_list_guild_members_with_optionals(self, http_client_impl): + async def test_list_guild_members_with_optionals(self, rest_impl): mock_response = [{"id": "379953393319542784", "nick": "Big Moh"}] - http_client_impl._request.return_value = mock_response + rest_impl._request.return_value = mock_response mock_route = mock.MagicMock(routes.GUILD_MEMBERS) with mock.patch.object(routes, "GUILD_MEMBERS", compile=mock.MagicMock(return_value=mock_route)): assert ( - await http_client_impl.list_guild_members("115590097100865541", limit=5, after="4444444444") - is mock_response + await rest_impl.list_guild_members("115590097100865541", limit=5, after="4444444444") is mock_response ) - routes.GUILD_MEMBERS.compile.assert_called_once_with(http_client_impl.GET, guild_id="115590097100865541") - http_client_impl._request.assert_called_once_with(mock_route, query={"limit": 5, "after": "4444444444"}) + routes.GUILD_MEMBERS.compile.assert_called_once_with(rest_impl.GET, guild_id="115590097100865541") + rest_impl._request.assert_called_once_with(mock_route, query={"limit": 5, "after": "4444444444"}) @pytest.mark.asyncio - async def test_modify_guild_member_without_optionals(self, http_client_impl): + async def test_modify_guild_member_without_optionals(self, rest_impl): mock_route = mock.MagicMock(routes.GUILD_MEMBER) with mock.patch.object(routes, "GUILD_MEMBER", compile=mock.MagicMock(return_value=mock_route)): - assert await http_client_impl.modify_guild_member("115590097100865541", "379953393319542784") is None + assert await rest_impl.modify_guild_member("115590097100865541", "379953393319542784") is None routes.GUILD_MEMBER.compile.assert_called_once_with( - http_client_impl.PATCH, guild_id="115590097100865541", user_id="379953393319542784" + rest_impl.PATCH, guild_id="115590097100865541", user_id="379953393319542784" ) - http_client_impl._request.assert_called_once_with(mock_route, json_body={}, reason=...) + rest_impl._request.assert_called_once_with(mock_route, json_body={}, reason=...) @pytest.mark.asyncio - async def test_modify_guild_member_with_optionals(self, http_client_impl): + async def test_modify_guild_member_with_optionals(self, rest_impl): mock_route = mock.MagicMock(routes.GUILD_MEMBER) with mock.patch.object(routes, "GUILD_MEMBER", compile=mock.MagicMock(return_value=mock_route)): - result = await http_client_impl.modify_guild_member( + result = await rest_impl.modify_guild_member( "115590097100865541", "379953393319542784", nick="QT", @@ -1301,223 +1349,211 @@ async def test_modify_guild_member_with_optionals(self, http_client_impl): assert result is None routes.GUILD_MEMBER.compile.assert_called_once_with( - http_client_impl.PATCH, guild_id="115590097100865541", user_id="379953393319542784" + rest_impl.PATCH, guild_id="115590097100865541", user_id="379953393319542784" ) - http_client_impl._request.assert_called_once_with( + rest_impl._request.assert_called_once_with( mock_route, json_body={"nick": "QT", "roles": ["222222222"], "mute": True, "deaf": True, "channel_id": "777"}, reason="I will drink your blood.", ) @pytest.mark.asyncio - async def test_modify_current_user_nick_without_reason(self, http_client_impl): + async def test_modify_current_user_nick_without_reason(self, rest_impl): mock_route = mock.MagicMock(routes.OWN_GUILD_NICKNAME) with mock.patch.object(routes, "OWN_GUILD_NICKNAME", compile=mock.MagicMock(return_value=mock_route)): - assert await http_client_impl.modify_current_user_nick("202020202", "Nickname me") is None - routes.OWN_GUILD_NICKNAME.compile.assert_called_once_with(http_client_impl.PATCH, guild_id="202020202") - http_client_impl._request.assert_called_once_with(mock_route, json_body={"nick": "Nickname me"}, reason=...) + assert await rest_impl.modify_current_user_nick("202020202", "Nickname me") is None + routes.OWN_GUILD_NICKNAME.compile.assert_called_once_with(rest_impl.PATCH, guild_id="202020202") + rest_impl._request.assert_called_once_with(mock_route, json_body={"nick": "Nickname me"}, reason=...) @pytest.mark.asyncio - async def test_modify_current_user_nick_with_reason(self, http_client_impl): + async def test_modify_current_user_nick_with_reason(self, rest_impl): mock_route = mock.MagicMock(routes.OWN_GUILD_NICKNAME) with mock.patch.object(routes, "OWN_GUILD_NICKNAME", compile=mock.MagicMock(return_value=mock_route)): - assert ( - await http_client_impl.modify_current_user_nick("202020202", "Nickname me", reason="Look at me") is None - ) - routes.OWN_GUILD_NICKNAME.compile.assert_called_once_with(http_client_impl.PATCH, guild_id="202020202") - http_client_impl._request.assert_called_once_with( - mock_route, json_body={"nick": "Nickname me"}, reason="Look at me" - ) + assert await rest_impl.modify_current_user_nick("202020202", "Nickname me", reason="Look at me") is None + routes.OWN_GUILD_NICKNAME.compile.assert_called_once_with(rest_impl.PATCH, guild_id="202020202") + rest_impl._request.assert_called_once_with(mock_route, json_body={"nick": "Nickname me"}, reason="Look at me") @pytest.mark.asyncio - async def test_add_guild_member_role_without_reason(self, http_client_impl): + async def test_add_guild_member_role_without_reason(self, rest_impl): mock_route = mock.MagicMock(routes.GUILD_MEMBER_ROLE) with mock.patch.object(routes, "GUILD_MEMBER_ROLE", compile=mock.MagicMock(return_value=mock_route)): - assert await http_client_impl.add_guild_member_role("3939393", "2838383", "84384848") is None + assert await rest_impl.add_guild_member_role("3939393", "2838383", "84384848") is None routes.GUILD_MEMBER_ROLE.compile.assert_called_once_with( - http_client_impl.PUT, guild_id="3939393", user_id="2838383", role_id="84384848" + rest_impl.PUT, guild_id="3939393", user_id="2838383", role_id="84384848" ) - http_client_impl._request.assert_called_once_with(mock_route, reason=...) + rest_impl._request.assert_called_once_with(mock_route, reason=...) @pytest.mark.asyncio - async def test_add_guild_member_role_with_reason(self, http_client_impl): + async def test_add_guild_member_role_with_reason(self, rest_impl): mock_route = mock.MagicMock(routes.GUILD_MEMBER_ROLE) with mock.patch.object(routes, "GUILD_MEMBER_ROLE", compile=mock.MagicMock(return_value=mock_route)): assert ( - await http_client_impl.add_guild_member_role( + await rest_impl.add_guild_member_role( "3939393", "2838383", "84384848", reason="A special role for a special somebody" ) is None ) routes.GUILD_MEMBER_ROLE.compile.assert_called_once_with( - http_client_impl.PUT, guild_id="3939393", user_id="2838383", role_id="84384848" + rest_impl.PUT, guild_id="3939393", user_id="2838383", role_id="84384848" ) - http_client_impl._request.assert_called_once_with(mock_route, reason="A special role for a special somebody") + rest_impl._request.assert_called_once_with(mock_route, reason="A special role for a special somebody") @pytest.mark.asyncio - async def test_remove_guild_member_role_without_reason(self, http_client_impl): + async def test_remove_guild_member_role_without_reason(self, rest_impl): mock_route = mock.MagicMock(routes.GUILD_MEMBER_ROLE) with mock.patch.object(routes, "GUILD_MEMBER_ROLE", compile=mock.MagicMock(return_value=mock_route)): - assert await http_client_impl.remove_guild_member_role("22222", "3333", "44444") is None + assert await rest_impl.remove_guild_member_role("22222", "3333", "44444") is None routes.GUILD_MEMBER_ROLE.compile.assert_called_once_with( - http_client_impl.DELETE, guild_id="22222", user_id="3333", role_id="44444" + rest_impl.DELETE, guild_id="22222", user_id="3333", role_id="44444" ) - http_client_impl._request.assert_called_once_with(mock_route, reason=...) + rest_impl._request.assert_called_once_with(mock_route, reason=...) @pytest.mark.asyncio - async def test_remove_guild_member_role_with_reason(self, http_client_impl): + async def test_remove_guild_member_role_with_reason(self, rest_impl): mock_route = mock.MagicMock(routes.GUILD_MEMBER_ROLE) with mock.patch.object(routes, "GUILD_MEMBER_ROLE", compile=mock.MagicMock(return_value=mock_route)): - assert await http_client_impl.remove_guild_member_role("22222", "3333", "44444", reason="bye") is None + assert await rest_impl.remove_guild_member_role("22222", "3333", "44444", reason="bye") is None routes.GUILD_MEMBER_ROLE.compile.assert_called_once_with( - http_client_impl.DELETE, guild_id="22222", user_id="3333", role_id="44444" + rest_impl.DELETE, guild_id="22222", user_id="3333", role_id="44444" ) - http_client_impl._request.assert_called_once_with(mock_route, reason="bye") + rest_impl._request.assert_called_once_with(mock_route, reason="bye") @pytest.mark.asyncio - async def test_remove_guild_member_without_reason(self, http_client_impl): + async def test_remove_guild_member_without_reason(self, rest_impl): mock_route = mock.MagicMock(routes.GUILD_MEMBER) with mock.patch.object(routes, "GUILD_MEMBER", compile=mock.MagicMock(return_value=mock_route)): - assert await http_client_impl.remove_guild_member("393939", "82828") is None - routes.GUILD_MEMBER.compile.assert_called_once_with( - http_client_impl.DELETE, guild_id="393939", user_id="82828" - ) - http_client_impl._request.assert_called_once_with(mock_route, reason=...) + assert await rest_impl.remove_guild_member("393939", "82828") is None + routes.GUILD_MEMBER.compile.assert_called_once_with(rest_impl.DELETE, guild_id="393939", user_id="82828") + rest_impl._request.assert_called_once_with(mock_route, reason=...) @pytest.mark.asyncio - async def test_remove_guild_member_with_reason(self, http_client_impl): + async def test_remove_guild_member_with_reason(self, rest_impl): mock_route = mock.MagicMock(routes.GUILD_MEMBER) with mock.patch.object(routes, "GUILD_MEMBER", compile=mock.MagicMock(return_value=mock_route)): - assert await http_client_impl.remove_guild_member("393939", "82828", reason="super bye") is None - routes.GUILD_MEMBER.compile.assert_called_once_with( - http_client_impl.DELETE, guild_id="393939", user_id="82828" - ) - http_client_impl._request.assert_called_once_with(mock_route, reason="super bye") + assert await rest_impl.remove_guild_member("393939", "82828", reason="super bye") is None + routes.GUILD_MEMBER.compile.assert_called_once_with(rest_impl.DELETE, guild_id="393939", user_id="82828") + rest_impl._request.assert_called_once_with(mock_route, reason="super bye") @pytest.mark.asyncio - async def test_get_guild_bans(self, http_client_impl): + async def test_get_guild_bans(self, rest_impl): mock_response = [{"id": "3939393"}] - http_client_impl._request.return_value = mock_response + rest_impl._request.return_value = mock_response mock_route = mock.MagicMock(routes.GUILD_BANS) with mock.patch.object(routes, "GUILD_BANS", compile=mock.MagicMock(return_value=mock_route)): - assert await http_client_impl.get_guild_bans("292929") is mock_response - routes.GUILD_BANS.compile.assert_called_once_with(http_client_impl.GET, guild_id="292929") - http_client_impl._request.assert_called_once_with(mock_route) + assert await rest_impl.get_guild_bans("292929") is mock_response + routes.GUILD_BANS.compile.assert_called_once_with(rest_impl.GET, guild_id="292929") + rest_impl._request.assert_called_once_with(mock_route) @pytest.mark.asyncio - async def test_get_guild_ban(self, http_client_impl): + async def test_get_guild_ban(self, rest_impl): mock_response = {"id": "3939393"} - http_client_impl._request.return_value = mock_response + rest_impl._request.return_value = mock_response mock_route = mock.MagicMock(routes.GUILD_BAN) with mock.patch.object(routes, "GUILD_BAN", compile=mock.MagicMock(return_value=mock_route)): - assert await http_client_impl.get_guild_ban("92929", "44848") is mock_response - routes.GUILD_BAN.compile.assert_called_once_with(http_client_impl.GET, guild_id="92929", user_id="44848") - http_client_impl._request.assert_called_once_with(mock_route) + assert await rest_impl.get_guild_ban("92929", "44848") is mock_response + routes.GUILD_BAN.compile.assert_called_once_with(rest_impl.GET, guild_id="92929", user_id="44848") + rest_impl._request.assert_called_once_with(mock_route) @pytest.mark.asyncio - async def test_create_guild_ban_without_optionals(self, http_client_impl): + async def test_create_guild_ban_without_optionals(self, rest_impl): mock_route = mock.MagicMock(routes.GUILD_BAN) with mock.patch.object(routes, "GUILD_BAN", compile=mock.MagicMock(return_value=mock_route)): - assert await http_client_impl.create_guild_ban("222", "444") is None - routes.GUILD_BAN.compile.assert_called_once_with(http_client_impl.PUT, guild_id="222", user_id="444") - http_client_impl._request.assert_called_once_with(mock_route, query={}) + assert await rest_impl.create_guild_ban("222", "444") is None + routes.GUILD_BAN.compile.assert_called_once_with(rest_impl.PUT, guild_id="222", user_id="444") + rest_impl._request.assert_called_once_with(mock_route, query={}) @pytest.mark.asyncio - async def test_create_guild_ban_with_optionals(self, http_client_impl): + async def test_create_guild_ban_with_optionals(self, rest_impl): mock_route = mock.MagicMock(routes.GUILD_BAN) with mock.patch.object(routes, "GUILD_BAN", compile=mock.MagicMock(return_value=mock_route)): - assert await http_client_impl.create_guild_ban("222", "444", delete_message_days=5, reason="TRUE") is None - routes.GUILD_BAN.compile.assert_called_once_with(http_client_impl.PUT, guild_id="222", user_id="444") - http_client_impl._request.assert_called_once_with( - mock_route, query={"delete-message-days": 5, "reason": "TRUE"} - ) + assert await rest_impl.create_guild_ban("222", "444", delete_message_days=5, reason="TRUE") is None + routes.GUILD_BAN.compile.assert_called_once_with(rest_impl.PUT, guild_id="222", user_id="444") + rest_impl._request.assert_called_once_with(mock_route, query={"delete-message-days": 5, "reason": "TRUE"}) @pytest.mark.asyncio - async def test_remove_guild_ban_without_reason(self, http_client_impl): + async def test_remove_guild_ban_without_reason(self, rest_impl): mock_route = mock.MagicMock(routes.GUILD_BAN) with mock.patch.object(routes, "GUILD_BAN", compile=mock.MagicMock(return_value=mock_route)): - assert await http_client_impl.remove_guild_ban("494949", "3737") is None - routes.GUILD_BAN.compile.assert_called_once_with(http_client_impl.DELETE, guild_id="494949", user_id="3737") - http_client_impl._request.assert_called_once_with(mock_route, reason=...) + assert await rest_impl.remove_guild_ban("494949", "3737") is None + routes.GUILD_BAN.compile.assert_called_once_with(rest_impl.DELETE, guild_id="494949", user_id="3737") + rest_impl._request.assert_called_once_with(mock_route, reason=...) @pytest.mark.asyncio - async def test_remove_guild_ban_with_reason(self, http_client_impl): + async def test_remove_guild_ban_with_reason(self, rest_impl): mock_route = mock.MagicMock(routes.GUILD_BAN) with mock.patch.object(routes, "GUILD_BAN", compile=mock.MagicMock(return_value=mock_route)): - assert await http_client_impl.remove_guild_ban("494949", "3737", reason="LMFAO") is None - routes.GUILD_BAN.compile.assert_called_once_with(http_client_impl.DELETE, guild_id="494949", user_id="3737") - http_client_impl._request.assert_called_once_with(mock_route, reason="LMFAO") + assert await rest_impl.remove_guild_ban("494949", "3737", reason="LMFAO") is None + routes.GUILD_BAN.compile.assert_called_once_with(rest_impl.DELETE, guild_id="494949", user_id="3737") + rest_impl._request.assert_called_once_with(mock_route, reason="LMFAO") @pytest.mark.asyncio - async def test_get_guild_roles(self, http_client_impl): + async def test_get_guild_roles(self, rest_impl): mock_response = [{"name": "role", "id": "4949494994"}] - http_client_impl._request.return_value = mock_response + rest_impl._request.return_value = mock_response mock_route = mock.MagicMock(routes.GUILD_ROLES) with mock.patch.object(routes, "GUILD_ROLES", compile=mock.MagicMock(return_value=mock_route)): - assert await http_client_impl.get_guild_roles("909") is mock_response - routes.GUILD_ROLES.compile.assert_called_once_with(http_client_impl.GET, guild_id="909") - http_client_impl._request.assert_called_once_with(mock_route) + assert await rest_impl.get_guild_roles("909") is mock_response + routes.GUILD_ROLES.compile.assert_called_once_with(rest_impl.GET, guild_id="909") + rest_impl._request.assert_called_once_with(mock_route) @pytest.mark.asyncio - async def test_create_guild_role_without_optionals(self, http_client_impl): + async def test_create_guild_role_without_optionals(self, rest_impl): mock_response = {"id": "42"} - http_client_impl._request.return_value = mock_response + rest_impl._request.return_value = mock_response mock_route = mock.MagicMock(routes.GUILD_ROLES) with mock.patch.object(routes, "GUILD_ROLES", compile=mock.MagicMock(return_value=mock_route)): - assert await http_client_impl.create_guild_role("9494") is mock_response - routes.GUILD_ROLES.compile.assert_called_once_with(http_client_impl.POST, guild_id="9494") - http_client_impl._request.assert_called_once_with(mock_route, json_body={}, reason=...) + assert await rest_impl.create_guild_role("9494") is mock_response + routes.GUILD_ROLES.compile.assert_called_once_with(rest_impl.POST, guild_id="9494") + rest_impl._request.assert_called_once_with(mock_route, json_body={}, reason=...) @pytest.mark.asyncio - async def test_create_guild_role_with_optionals(self, http_client_impl): + async def test_create_guild_role_with_optionals(self, rest_impl): mock_response = {"id": "42"} - http_client_impl._request.return_value = mock_response + rest_impl._request.return_value = mock_response mock_route = mock.MagicMock(routes.GUILD_ROLES) with mock.patch.object(routes, "GUILD_ROLES", compile=mock.MagicMock(return_value=mock_route)): assert ( - await http_client_impl.create_guild_role( + await rest_impl.create_guild_role( "9494", name="role sama", permissions=22, color=12, hoist=True, mentionable=True, reason="eat dirt" ) is mock_response ) - routes.GUILD_ROLES.compile.assert_called_once_with(http_client_impl.POST, guild_id="9494") - http_client_impl._request.assert_called_once_with( + routes.GUILD_ROLES.compile.assert_called_once_with(rest_impl.POST, guild_id="9494") + rest_impl._request.assert_called_once_with( mock_route, json_body={"name": "role sama", "permissions": 22, "color": 12, "hoist": True, "mentionable": True,}, reason="eat dirt", ) @pytest.mark.asyncio - async def test_modify_guild_role_positions(self, http_client_impl): + async def test_modify_guild_role_positions(self, rest_impl): mock_response = [{"id": "444", "position": 0}, {"id": "999", "position": 1}] - http_client_impl._request.return_value = mock_response + rest_impl._request.return_value = mock_response mock_route = mock.MagicMock(routes.GUILD_ROLES) with mock.patch.object(routes, "GUILD_ROLES", compile=mock.MagicMock(return_value=mock_route)): - assert await http_client_impl.modify_guild_role_positions("292929", ("444", 0), ("999", 1)) is mock_response - routes.GUILD_ROLES.compile.assert_called_once_with(http_client_impl.PATCH, guild_id="292929") - http_client_impl._request.assert_called_once_with( + assert await rest_impl.modify_guild_role_positions("292929", ("444", 0), ("999", 1)) is mock_response + routes.GUILD_ROLES.compile.assert_called_once_with(rest_impl.PATCH, guild_id="292929") + rest_impl._request.assert_called_once_with( mock_route, json_body=[{"id": "444", "position": 0}, {"id": "999", "position": 1}] ) @pytest.mark.asyncio - async def test_modify_guild_role_with_optionals(self, http_client_impl): + async def test_modify_guild_role_with_optionals(self, rest_impl): mock_response = {"id": "54234", "name": "roleio roleio"} - http_client_impl._request.return_value = mock_response + rest_impl._request.return_value = mock_response mock_route = mock.MagicMock(routes.GUILD_ROLE) with mock.patch.object(routes, "GUILD_ROLE", compile=mock.MagicMock(return_value=mock_route)): - assert await http_client_impl.modify_guild_role("999999", "54234") is mock_response - routes.GUILD_ROLE.compile.assert_called_once_with( - http_client_impl.PATCH, guild_id="999999", role_id="54234" - ) - http_client_impl._request.assert_called_once_with(mock_route, json_body={}, reason=...) + assert await rest_impl.modify_guild_role("999999", "54234") is mock_response + routes.GUILD_ROLE.compile.assert_called_once_with(rest_impl.PATCH, guild_id="999999", role_id="54234") + rest_impl._request.assert_called_once_with(mock_route, json_body={}, reason=...) @pytest.mark.asyncio - async def test_modify_guild_role_without_optionals(self, http_client_impl): + async def test_modify_guild_role_without_optionals(self, rest_impl): mock_response = {"id": "54234", "name": "roleio roleio"} - http_client_impl._request.return_value = mock_response + rest_impl._request.return_value = mock_response mock_route = mock.MagicMock(routes.GUILD_ROLE) with mock.patch.object(routes, "GUILD_ROLE", compile=mock.MagicMock(return_value=mock_route)): - result = await http_client_impl.modify_guild_role( + result = await rest_impl.modify_guild_role( "999999", "54234", name="HAHA", @@ -1528,128 +1564,119 @@ async def test_modify_guild_role_without_optionals(self, http_client_impl): reason="You are a pirate.", ) assert result is mock_response - routes.GUILD_ROLE.compile.assert_called_once_with( - http_client_impl.PATCH, guild_id="999999", role_id="54234" - ) - http_client_impl._request.assert_called_once_with( + routes.GUILD_ROLE.compile.assert_called_once_with(rest_impl.PATCH, guild_id="999999", role_id="54234") + rest_impl._request.assert_called_once_with( mock_route, json_body={"name": "HAHA", "permissions": 42, "color": 69, "hoist": True, "mentionable": False,}, reason="You are a pirate.", ) @pytest.mark.asyncio - async def test_delete_guild_role(self, http_client_impl): + async def test_delete_guild_role(self, rest_impl): mock_route = mock.MagicMock(routes.GUILD_ROLE) with mock.patch.object(routes, "GUILD_ROLE", compile=mock.MagicMock(return_value=mock_route)): - assert await http_client_impl.delete_guild_role("29292", "4848") is None - routes.GUILD_ROLE.compile.assert_called_once_with(http_client_impl.DELETE, guild_id="29292", role_id="4848") - http_client_impl._request.assert_called_once_with(mock_route) + assert await rest_impl.delete_guild_role("29292", "4848") is None + routes.GUILD_ROLE.compile.assert_called_once_with(rest_impl.DELETE, guild_id="29292", role_id="4848") + rest_impl._request.assert_called_once_with(mock_route) @pytest.mark.asyncio - async def test_get_guild_prune_count(self, http_client_impl): + async def test_get_guild_prune_count(self, rest_impl): mock_response = {"pruned": 7} - http_client_impl._request.return_value = mock_response + rest_impl._request.return_value = mock_response mock_route = mock.MagicMock(routes.GUILD_PRUNE) with mock.patch.object(routes, "GUILD_PRUNE", compile=mock.MagicMock(return_value=mock_route)): - assert await http_client_impl.get_guild_prune_count("29292", 14) == 7 - routes.GUILD_PRUNE.compile.assert_called_once_with(http_client_impl.GET, guild_id="29292") - http_client_impl._request.assert_called_once_with(mock_route, query={"days": 14}) + assert await rest_impl.get_guild_prune_count("29292", 14) == 7 + routes.GUILD_PRUNE.compile.assert_called_once_with(rest_impl.GET, guild_id="29292") + rest_impl._request.assert_called_once_with(mock_route, query={"days": 14}) @pytest.mark.asyncio @pytest.mark.parametrize("mock_response", ({"pruned": None}, {})) - async def test_begin_guild_prune_without_optionals_returns_none(self, http_client_impl, mock_response): - http_client_impl._request.return_value = mock_response + async def test_begin_guild_prune_without_optionals_returns_none(self, rest_impl, mock_response): + rest_impl._request.return_value = mock_response mock_route = mock.MagicMock(routes.GUILD_PRUNE) with mock.patch.object(routes, "GUILD_PRUNE", compile=mock.MagicMock(return_value=mock_route)): - assert await http_client_impl.begin_guild_prune("39393", 14) is None - http_client_impl._request.assert_called_once_with(mock_route, query={"days": 14}, reason=...) + assert await rest_impl.begin_guild_prune("39393", 14) is None + rest_impl._request.assert_called_once_with(mock_route, query={"days": 14}, reason=...) @pytest.mark.asyncio - async def test_begin_guild_prune_with_optionals(self, http_client_impl): - http_client_impl._request.return_value = {"pruned": 32} + async def test_begin_guild_prune_with_optionals(self, rest_impl): + rest_impl._request.return_value = {"pruned": 32} mock_route = mock.MagicMock(routes.GUILD_PRUNE) with mock.patch.object(routes, "GUILD_PRUNE", compile=mock.MagicMock(return_value=mock_route)): - assert ( - await http_client_impl.begin_guild_prune("39393", 14, compute_prune_count=True, reason="BYEBYE") == 32 - ) - http_client_impl._request.assert_called_once_with( + assert await rest_impl.begin_guild_prune("39393", 14, compute_prune_count=True, reason="BYEBYE") == 32 + rest_impl._request.assert_called_once_with( mock_route, query={"days": 14, "compute_prune_count": "True"}, reason="BYEBYE" ) @pytest.mark.asyncio - async def test_get_guild_voice_regions(self, http_client_impl): + async def test_get_guild_voice_regions(self, rest_impl): mock_response = [{"name": "london", "vip": True}] - http_client_impl._request.return_value = mock_response + rest_impl._request.return_value = mock_response mock_route = mock.MagicMock(routes.GUILD_VOICE_REGIONS) with mock.patch.object(routes, "GUILD_VOICE_REGIONS", compile=mock.MagicMock(return_value=mock_route)): - assert await http_client_impl.get_guild_voice_regions("2393939") is mock_response - routes.GUILD_VOICE_REGIONS.compile.assert_called_once_with(http_client_impl.GET, guild_id="2393939") - http_client_impl._request.assert_called_once_with(mock_route) + assert await rest_impl.get_guild_voice_regions("2393939") is mock_response + routes.GUILD_VOICE_REGIONS.compile.assert_called_once_with(rest_impl.GET, guild_id="2393939") + rest_impl._request.assert_called_once_with(mock_route) @pytest.mark.asyncio - async def test_get_guild_invites(self, http_client_impl): + async def test_get_guild_invites(self, rest_impl): mock_response = [{"code": "ewkkww"}] - http_client_impl._request.return_value = mock_response + rest_impl._request.return_value = mock_response mock_route = mock.MagicMock(routes.GUILD_INVITES) with mock.patch.object(routes, "GUILD_INVITES", compile=mock.MagicMock(return_value=mock_route)): - assert await http_client_impl.get_guild_invites("9292929") is mock_response - routes.GUILD_INVITES.compile.assert_called_once_with(http_client_impl.GET, guild_id="9292929") - http_client_impl._request.assert_called_once_with(mock_route) + assert await rest_impl.get_guild_invites("9292929") is mock_response + routes.GUILD_INVITES.compile.assert_called_once_with(rest_impl.GET, guild_id="9292929") + rest_impl._request.assert_called_once_with(mock_route) @pytest.mark.asyncio - async def test_get_guild_integrations(self, http_client_impl): + async def test_get_guild_integrations(self, rest_impl): mock_response = [{"id": "4242"}] - http_client_impl._request.return_value = mock_response + rest_impl._request.return_value = mock_response mock_route = mock.MagicMock(routes.GUILD_INTEGRATIONS) with mock.patch.object(routes, "GUILD_INTEGRATIONS", compile=mock.MagicMock(return_value=mock_route)): - assert await http_client_impl.get_guild_integrations("537340989808050216") is mock_response - routes.GUILD_INTEGRATIONS.compile.assert_called_once_with( - http_client_impl.GET, guild_id="537340989808050216" - ) - http_client_impl._request.assert_called_once_with(mock_route) + assert await rest_impl.get_guild_integrations("537340989808050216") is mock_response + routes.GUILD_INTEGRATIONS.compile.assert_called_once_with(rest_impl.GET, guild_id="537340989808050216") + rest_impl._request.assert_called_once_with(mock_route) @pytest.mark.asyncio - async def test_create_guild_integration_without_reason(self, http_client_impl): + async def test_create_guild_integration_without_reason(self, rest_impl): mock_response = {"id": "22222"} - http_client_impl._request.return_value = mock_response + rest_impl._request.return_value = mock_response mock_route = mock.MagicMock(routes.GUILD_INTEGRATIONS) with mock.patch.object(routes, "GUILD_INTEGRATIONS", compile=mock.MagicMock(return_value=mock_route)): - assert await http_client_impl.create_guild_integration("2222", "twitch", "443223") is mock_response - routes.GUILD_INTEGRATIONS.compile.assert_called_once_with(http_client_impl.POST, guild_id="2222") - http_client_impl._request.assert_called_once_with( - mock_route, json_body={"type": "twitch", "id": "443223"}, reason=... - ) + assert await rest_impl.create_guild_integration("2222", "twitch", "443223") is mock_response + routes.GUILD_INTEGRATIONS.compile.assert_called_once_with(rest_impl.POST, guild_id="2222") + rest_impl._request.assert_called_once_with(mock_route, json_body={"type": "twitch", "id": "443223"}, reason=...) @pytest.mark.asyncio - async def test_create_guild_integration_with_reason(self, http_client_impl): + async def test_create_guild_integration_with_reason(self, rest_impl): mock_response = {"id": "22222"} - http_client_impl._request.return_value = mock_response + rest_impl._request.return_value = mock_response mock_route = mock.MagicMock(routes.GUILD_INTEGRATIONS) with mock.patch.object(routes, "GUILD_INTEGRATIONS", compile=mock.MagicMock(return_value=mock_route)): assert ( - await http_client_impl.create_guild_integration("2222", "twitch", "443223", reason="NAH m8") - is mock_response + await rest_impl.create_guild_integration("2222", "twitch", "443223", reason="NAH m8") is mock_response ) - routes.GUILD_INTEGRATIONS.compile.assert_called_once_with(http_client_impl.POST, guild_id="2222") - http_client_impl._request.assert_called_once_with( + routes.GUILD_INTEGRATIONS.compile.assert_called_once_with(rest_impl.POST, guild_id="2222") + rest_impl._request.assert_called_once_with( mock_route, json_body={"type": "twitch", "id": "443223"}, reason="NAH m8" ) @pytest.mark.asyncio - async def test_modify_guild_integration_without_optionals(self, http_client_impl): + async def test_modify_guild_integration_without_optionals(self, rest_impl): mock_route = mock.MagicMock(routes.GUILD_INTEGRATION) with mock.patch.object(routes, "GUILD_INTEGRATION", compile=mock.MagicMock(return_value=mock_route)): - assert await http_client_impl.modify_guild_integration("292929", "747474") is None + assert await rest_impl.modify_guild_integration("292929", "747474") is None routes.GUILD_INTEGRATION.compile.assert_called_once_with( - http_client_impl.PATCH, guild_id="292929", integration_id="747474" + rest_impl.PATCH, guild_id="292929", integration_id="747474" ) - http_client_impl._request.assert_called_once_with(mock_route, json_body={}, reason=...) + rest_impl._request.assert_called_once_with(mock_route, json_body={}, reason=...) @pytest.mark.asyncio - async def test_modify_guild_integration_with_optionals(self, http_client_impl): + async def test_modify_guild_integration_with_optionals(self, rest_impl): mock_route = mock.MagicMock(routes.GUILD_INTEGRATION) with mock.patch.object(routes, "GUILD_INTEGRATION", compile=mock.MagicMock(return_value=mock_route)): - result = await http_client_impl.modify_guild_integration( + result = await rest_impl.modify_guild_integration( "292929", "747474", expire_behaviour=2, @@ -1660,340 +1687,334 @@ async def test_modify_guild_integration_with_optionals(self, http_client_impl): assert result is None routes.GUILD_INTEGRATION.compile.assert_called_once_with( - http_client_impl.PATCH, guild_id="292929", integration_id="747474" + rest_impl.PATCH, guild_id="292929", integration_id="747474" ) - http_client_impl._request.assert_called_once_with( + rest_impl._request.assert_called_once_with( mock_route, json_body={"expire_behaviour": 2, "expire_grace_period": 1, "enable_emoticons": True}, reason="This password is already taken by {redacted}", ) @pytest.mark.asyncio - async def test_delete_guild_integration_without_reason(self, http_client_impl): + async def test_delete_guild_integration_without_reason(self, rest_impl): mock_route = mock.MagicMock(routes.GUILD_INTEGRATION) with mock.patch.object(routes, "GUILD_INTEGRATION", compile=mock.MagicMock(return_value=mock_route)): - assert await http_client_impl.delete_guild_integration("23992", "7474") is None + assert await rest_impl.delete_guild_integration("23992", "7474") is None routes.GUILD_INTEGRATION.compile.assert_called_once_with( - http_client_impl.DELETE, guild_id="23992", integration_id="7474" + rest_impl.DELETE, guild_id="23992", integration_id="7474" ) - http_client_impl._request.assert_called_once_with(mock_route, reason=...) + rest_impl._request.assert_called_once_with(mock_route, reason=...) @pytest.mark.asyncio - async def test_delete_guild_integration_with_reason(self, http_client_impl): + async def test_delete_guild_integration_with_reason(self, rest_impl): mock_route = mock.MagicMock(routes.GUILD_INTEGRATION) with mock.patch.object(routes, "GUILD_INTEGRATION", compile=mock.MagicMock(return_value=mock_route)): - assert await http_client_impl.delete_guild_integration("23992", "7474", reason="HOT") is None + assert await rest_impl.delete_guild_integration("23992", "7474", reason="HOT") is None routes.GUILD_INTEGRATION.compile.assert_called_once_with( - http_client_impl.DELETE, guild_id="23992", integration_id="7474" + rest_impl.DELETE, guild_id="23992", integration_id="7474" ) - http_client_impl._request.assert_called_once_with(mock_route, reason="HOT") + rest_impl._request.assert_called_once_with(mock_route, reason="HOT") @pytest.mark.asyncio - async def test_sync_guild_integration(self, http_client_impl): + async def test_sync_guild_integration(self, rest_impl): mock_route = mock.MagicMock(routes.GUILD_INTEGRATION_SYNC) with mock.patch.object(routes, "GUILD_INTEGRATION_SYNC", compile=mock.MagicMock(return_value=mock_route)): - assert await http_client_impl.sync_guild_integration("3939439", "84884") is None + assert await rest_impl.sync_guild_integration("3939439", "84884") is None routes.GUILD_INTEGRATION_SYNC.compile.assert_called_once_with( - http_client_impl.POST, guild_id="3939439", integration_id="84884" + rest_impl.POST, guild_id="3939439", integration_id="84884" ) - http_client_impl._request.assert_called_once_with(mock_route) + rest_impl._request.assert_called_once_with(mock_route) @pytest.mark.asyncio - async def test_get_guild_embed(self, http_client_impl): + async def test_get_guild_embed(self, rest_impl): mock_response = {"channel_id": "4304040", "enabled": True} - http_client_impl._request.return_value = mock_response + rest_impl._request.return_value = mock_response mock_route = mock.MagicMock(routes.GUILD_EMBED) with mock.patch.object(routes, "GUILD_EMBED", compile=mock.MagicMock(return_value=mock_route)): - assert await http_client_impl.get_guild_embed("4949") is mock_response - routes.GUILD_EMBED.compile.assert_called_once_with(http_client_impl.GET, guild_id="4949") - http_client_impl._request.assert_called_once_with(mock_route) + assert await rest_impl.get_guild_embed("4949") is mock_response + routes.GUILD_EMBED.compile.assert_called_once_with(rest_impl.GET, guild_id="4949") + rest_impl._request.assert_called_once_with(mock_route) @pytest.mark.asyncio - async def test_modify_guild_embed_without_reason(self, http_client_impl): + async def test_modify_guild_embed_without_reason(self, rest_impl): mock_response = {"channel_id": "4444", "enabled": False} - http_client_impl._request.return_value = mock_response + rest_impl._request.return_value = mock_response mock_route = mock.MagicMock(routes.GUILD_EMBED) with mock.patch.object(routes, "GUILD_EMBED", compile=mock.MagicMock(return_value=mock_route)): embed_obj = {"channel_id": "222", "enabled": True} - assert await http_client_impl.modify_guild_embed("393939", embed_obj) is mock_response - routes.GUILD_EMBED.compile.assert_called_once_with(http_client_impl.PATCH, guild_id="393939") - http_client_impl._request.assert_called_once_with(mock_route, json_body=embed_obj, reason=...) + assert await rest_impl.modify_guild_embed("393939", embed_obj) is mock_response + routes.GUILD_EMBED.compile.assert_called_once_with(rest_impl.PATCH, guild_id="393939") + rest_impl._request.assert_called_once_with(mock_route, json_body=embed_obj, reason=...) @pytest.mark.asyncio - async def test_modify_guild_embed_with_reason(self, http_client_impl): + async def test_modify_guild_embed_with_reason(self, rest_impl): mock_response = {"channel_id": "4444", "enabled": False} - http_client_impl._request.return_value = mock_response + rest_impl._request.return_value = mock_response mock_route = mock.MagicMock(routes.GUILD_EMBED) with mock.patch.object(routes, "GUILD_EMBED", compile=mock.MagicMock(return_value=mock_route)): embed_obj = {"channel_id": "222", "enabled": True} - assert await http_client_impl.modify_guild_embed("393939", embed_obj, reason="OK") is mock_response - routes.GUILD_EMBED.compile.assert_called_once_with(http_client_impl.PATCH, guild_id="393939") - http_client_impl._request.assert_called_once_with(mock_route, json_body=embed_obj, reason="OK") + assert await rest_impl.modify_guild_embed("393939", embed_obj, reason="OK") is mock_response + routes.GUILD_EMBED.compile.assert_called_once_with(rest_impl.PATCH, guild_id="393939") + rest_impl._request.assert_called_once_with(mock_route, json_body=embed_obj, reason="OK") @pytest.mark.asyncio - async def test_get_guild_vanity_url(self, http_client_impl): + async def test_get_guild_vanity_url(self, rest_impl): mock_response = {"code": "dsidid"} - http_client_impl._request.return_value = mock_response + rest_impl._request.return_value = mock_response mock_route = mock.MagicMock(routes.GUILD_VANITY_URL) with mock.patch.object(routes, "GUILD_VANITY_URL", compile=mock.MagicMock(return_value=mock_route)): - assert await http_client_impl.get_guild_vanity_url("399393") is mock_response - routes.GUILD_VANITY_URL.compile.assert_called_once_with(http_client_impl.GET, guild_id="399393") - http_client_impl._request.assert_called_once_with(mock_route) + assert await rest_impl.get_guild_vanity_url("399393") is mock_response + routes.GUILD_VANITY_URL.compile.assert_called_once_with(rest_impl.GET, guild_id="399393") + rest_impl._request.assert_called_once_with(mock_route) - def test_get_guild_widget_image_url_without_style(self, http_client_impl): - url = http_client_impl.get_guild_widget_image_url("54949") + def test_get_guild_widget_image_url_without_style(self, rest_impl): + url = rest_impl.get_guild_widget_image_url("54949") assert url == "https://discordapp.com/api/v6/guilds/54949/widget.png" - def test_get_guild_widget_image_url_with_style(self, http_client_impl): - url = http_client_impl.get_guild_widget_image_url("54949", style="banner2") + def test_get_guild_widget_image_url_with_style(self, rest_impl): + url = rest_impl.get_guild_widget_image_url("54949", style="banner2") assert url == "https://discordapp.com/api/v6/guilds/54949/widget.png?style=banner2" @pytest.mark.asyncio - async def test_get_invite_without_counts(self, http_client_impl): + async def test_get_invite_without_counts(self, rest_impl): mock_response = {"code": "fesdfes"} - http_client_impl._request.return_value = mock_response + rest_impl._request.return_value = mock_response mock_route = mock.MagicMock(routes.INVITE) with mock.patch.object(routes, "INVITE", compile=mock.MagicMock(return_value=mock_route)): - assert await http_client_impl.get_invite("fesdfes") is mock_response - routes.INVITE.compile.assert_called_once_with(http_client_impl.GET, invite_code="fesdfes") - http_client_impl._request.assert_called_once_with(mock_route, query={}) + assert await rest_impl.get_invite("fesdfes") is mock_response + routes.INVITE.compile.assert_called_once_with(rest_impl.GET, invite_code="fesdfes") + rest_impl._request.assert_called_once_with(mock_route, query={}) @pytest.mark.asyncio - async def test_get_invite_with_counts(self, http_client_impl): + async def test_get_invite_with_counts(self, rest_impl): mock_response = {"code": "fesdfes"} - http_client_impl._request.return_value = mock_response + rest_impl._request.return_value = mock_response mock_route = mock.MagicMock(routes.INVITE) with mock.patch.object(routes, "INVITE", compile=mock.MagicMock(return_value=mock_route)): - assert await http_client_impl.get_invite("fesdfes", with_counts=True) is mock_response - routes.INVITE.compile.assert_called_once_with(http_client_impl.GET, invite_code="fesdfes") - http_client_impl._request.assert_called_once_with(mock_route, query={"with_counts": "True"}) + assert await rest_impl.get_invite("fesdfes", with_counts=True) is mock_response + routes.INVITE.compile.assert_called_once_with(rest_impl.GET, invite_code="fesdfes") + rest_impl._request.assert_called_once_with(mock_route, query={"with_counts": "True"}) @pytest.mark.asyncio - async def test_delete_invite(self, http_client_impl): + async def test_delete_invite(self, rest_impl): mock_response = {"code": "diidsk"} - http_client_impl._request.return_value = mock_response + rest_impl._request.return_value = mock_response mock_route = mock.MagicMock(routes.INVITE) with mock.patch.object(routes, "INVITE", compile=mock.MagicMock(return_value=mock_route)): - assert await http_client_impl.delete_invite("diidsk") is mock_response - routes.INVITE.compile.assert_called_once_with(http_client_impl.DELETE, invite_code="diidsk") - http_client_impl._request.assert_called_once_with(mock_route) + assert await rest_impl.delete_invite("diidsk") is mock_response + routes.INVITE.compile.assert_called_once_with(rest_impl.DELETE, invite_code="diidsk") + rest_impl._request.assert_called_once_with(mock_route) @pytest.mark.asyncio - async def test_get_current_application_info(self, http_client_impl): + async def test_get_current_application_info(self, rest_impl): mock_response = {"bot_public": True} - http_client_impl._request.return_value = mock_response + rest_impl._request.return_value = mock_response mock_route = mock.MagicMock(routes.OAUTH2_APPLICATIONS_ME) with mock.patch.object(routes, "OAUTH2_APPLICATIONS_ME", compile=mock.MagicMock(return_value=mock_route)): - assert await http_client_impl.get_current_application_info() is mock_response - routes.OAUTH2_APPLICATIONS_ME.compile.assert_called_once_with(http_client_impl.GET) - http_client_impl._request.assert_called_once_with(mock_route) + assert await rest_impl.get_current_application_info() is mock_response + routes.OAUTH2_APPLICATIONS_ME.compile.assert_called_once_with(rest_impl.GET) + rest_impl._request.assert_called_once_with(mock_route) @pytest.mark.asyncio - async def test_get_current_user(self, http_client_impl): + async def test_get_current_user(self, rest_impl): mock_response = {"id": "494949", "username": "A name"} - http_client_impl._request.return_value = mock_response + rest_impl._request.return_value = mock_response mock_route = mock.MagicMock(routes.OWN_USER) with mock.patch.object(routes, "OWN_USER", compile=mock.MagicMock(return_value=mock_route)): - assert await http_client_impl.get_current_user() is mock_response - routes.OWN_USER.compile.assert_called_once_with(http_client_impl.GET) - http_client_impl._request.assert_called_once_with(mock_route) + assert await rest_impl.get_current_user() is mock_response + routes.OWN_USER.compile.assert_called_once_with(rest_impl.GET) + rest_impl._request.assert_called_once_with(mock_route) @pytest.mark.asyncio - async def test_get_user(self, http_client_impl): + async def test_get_user(self, rest_impl): mock_response = {"id": "54959"} - http_client_impl._request.return_value = mock_response + rest_impl._request.return_value = mock_response mock_route = mock.MagicMock(routes.USER) with mock.patch.object(routes, "USER", compile=mock.MagicMock(return_value=mock_route)): - assert await http_client_impl.get_user("54959") is mock_response - routes.USER.compile.assert_called_once_with(http_client_impl.GET, user_id="54959") - http_client_impl._request.assert_called_once_with(mock_route) + assert await rest_impl.get_user("54959") is mock_response + routes.USER.compile.assert_called_once_with(rest_impl.GET, user_id="54959") + rest_impl._request.assert_called_once_with(mock_route) @pytest.mark.asyncio - async def test_modify_current_user_without_optionals(self, http_client_impl): + async def test_modify_current_user_without_optionals(self, rest_impl): mock_response = {"id": "44444", "username": "Watashi"} - http_client_impl._request.return_value = mock_response + rest_impl._request.return_value = mock_response mock_route = mock.MagicMock(routes.OWN_USER) with mock.patch.object(routes, "OWN_USER", compile=mock.MagicMock(return_value=mock_route)): - assert await http_client_impl.modify_current_user() is mock_response - routes.OWN_USER.compile.assert_called_once_with(http_client_impl.PATCH) - http_client_impl._request.assert_called_once_with(mock_route, json_body={}) + assert await rest_impl.modify_current_user() is mock_response + routes.OWN_USER.compile.assert_called_once_with(rest_impl.PATCH) + rest_impl._request.assert_called_once_with(mock_route, json_body={}) @pytest.mark.asyncio - async def test_modify_current_user_with_optionals(self, http_client_impl): + async def test_modify_current_user_with_optionals(self, rest_impl): mock_response = {"id": "44444", "username": "Watashi"} - http_client_impl._request.return_value = mock_response + rest_impl._request.return_value = mock_response mock_route = mock.MagicMock(routes.OWN_USER) mock_image_data = "data:image/png;base64,iVBORw0KGgpibGFo" with mock.patch.object(routes, "OWN_USER", compile=mock.MagicMock(return_value=mock_route)): with mock.patch.object(transformations, "image_bytes_to_image_data", return_value=mock_image_data): - result = await http_client_impl.modify_current_user( - username="Watashi 2", avatar=b"\211PNG\r\n\032\nblah" - ) + result = await rest_impl.modify_current_user(username="Watashi 2", avatar=b"\211PNG\r\n\032\nblah") assert result is mock_response - routes.OWN_USER.compile.assert_called_once_with(http_client_impl.PATCH) + routes.OWN_USER.compile.assert_called_once_with(rest_impl.PATCH) transformations.image_bytes_to_image_data.assert_called_once_with(b"\211PNG\r\n\032\nblah") - http_client_impl._request.assert_called_once_with( + rest_impl._request.assert_called_once_with( mock_route, json_body={"username": "Watashi 2", "avatar": mock_image_data} ) @pytest.mark.asyncio - async def test_get_current_user_connections(self, http_client_impl): + async def test_get_current_user_connections(self, rest_impl): mock_response = [{"id": "fspeed", "revoked": False}] - http_client_impl._request.return_value = mock_response + rest_impl._request.return_value = mock_response mock_route = mock.MagicMock(routes.OWN_CONNECTIONS) with mock.patch.object(routes, "OWN_CONNECTIONS", compile=mock.MagicMock(return_value=mock_route)): - assert await http_client_impl.get_current_user_connections() is mock_response - routes.OWN_CONNECTIONS.compile.assert_called_once_with(http_client_impl.GET) - http_client_impl._request.assert_called_once_with(mock_route) + assert await rest_impl.get_current_user_connections() is mock_response + routes.OWN_CONNECTIONS.compile.assert_called_once_with(rest_impl.GET) + rest_impl._request.assert_called_once_with(mock_route) @pytest.mark.asyncio - async def test_get_current_user_guilds_without_optionals(self, http_client_impl): + async def test_get_current_user_guilds_without_optionals(self, rest_impl): mock_response = [{"id": "452", "owner_id": "4949"}] - http_client_impl._request.return_value = mock_response + rest_impl._request.return_value = mock_response mock_route = mock.MagicMock(routes.OWN_GUILDS) with mock.patch.object(routes, "OWN_GUILDS", compile=mock.MagicMock(return_value=mock_route)): - assert await http_client_impl.get_current_user_guilds() is mock_response - routes.OWN_GUILDS.compile.assert_called_once_with(http_client_impl.GET) - http_client_impl._request.assert_called_once_with(mock_route, query={}) + assert await rest_impl.get_current_user_guilds() is mock_response + routes.OWN_GUILDS.compile.assert_called_once_with(rest_impl.GET) + rest_impl._request.assert_called_once_with(mock_route, query={}) @pytest.mark.asyncio - async def test_get_current_user_guilds_with_optionals(self, http_client_impl): + async def test_get_current_user_guilds_with_optionals(self, rest_impl): mock_response = [{"id": "452", "owner_id": "4949"}] - http_client_impl._request.return_value = mock_response + rest_impl._request.return_value = mock_response mock_route = mock.MagicMock(routes.OWN_GUILDS) with mock.patch.object(routes, "OWN_GUILDS", compile=mock.MagicMock(return_value=mock_route)): - assert ( - await http_client_impl.get_current_user_guilds(before="292929", after="22288", limit=5) is mock_response - ) - routes.OWN_GUILDS.compile.assert_called_once_with(http_client_impl.GET) - http_client_impl._request.assert_called_once_with( - mock_route, query={"before": "292929", "after": "22288", "limit": 5} - ) + assert await rest_impl.get_current_user_guilds(before="292929", after="22288", limit=5) is mock_response + routes.OWN_GUILDS.compile.assert_called_once_with(rest_impl.GET) + rest_impl._request.assert_called_once_with(mock_route, query={"before": "292929", "after": "22288", "limit": 5}) @pytest.mark.asyncio - async def test_leave_guild(self, http_client_impl): + async def test_leave_guild(self, rest_impl): mock_route = mock.MagicMock(routes.LEAVE_GUILD) with mock.patch.object(routes, "LEAVE_GUILD", compile=mock.MagicMock(return_value=mock_route)): - assert await http_client_impl.leave_guild("292929") is None - routes.LEAVE_GUILD.compile.assert_called_once_with(http_client_impl.DELETE, guild_id="292929") - http_client_impl._request.assert_called_once_with(mock_route) + assert await rest_impl.leave_guild("292929") is None + routes.LEAVE_GUILD.compile.assert_called_once_with(rest_impl.DELETE, guild_id="292929") + rest_impl._request.assert_called_once_with(mock_route) @pytest.mark.asyncio - async def test_create_dm(self, http_client_impl): + async def test_create_dm(self, rest_impl): mock_response = {"id": "404040", "recipients": []} - http_client_impl._request.return_value = mock_response + rest_impl._request.return_value = mock_response mock_route = mock.MagicMock(routes.OWN_DMS) with mock.patch.object(routes, "OWN_DMS", compile=mock.MagicMock(return_value=mock_route)): - assert await http_client_impl.create_dm("409491291156774923") is mock_response - routes.OWN_DMS.compile.assert_called_once_with(http_client_impl.POST) - http_client_impl._request.assert_called_once_with(mock_route, json_body={"recipient_id": "409491291156774923"}) + assert await rest_impl.create_dm("409491291156774923") is mock_response + routes.OWN_DMS.compile.assert_called_once_with(rest_impl.POST) + rest_impl._request.assert_called_once_with(mock_route, json_body={"recipient_id": "409491291156774923"}) @pytest.mark.asyncio - async def test_list_voice_regions(self, http_client_impl): + async def test_list_voice_regions(self, rest_impl): mock_response = [{"name": "neko-cafe"}] - http_client_impl._request.return_value = mock_response + rest_impl._request.return_value = mock_response mock_route = mock.MagicMock(routes.VOICE_REGIONS) with mock.patch.object(routes, "VOICE_REGIONS", compile=mock.MagicMock(return_value=mock_route)): - assert await http_client_impl.list_voice_regions() is mock_response - routes.VOICE_REGIONS.compile.assert_called_once_with(http_client_impl.GET) - http_client_impl._request.assert_called_once_with(mock_route) + assert await rest_impl.list_voice_regions() is mock_response + routes.VOICE_REGIONS.compile.assert_called_once_with(rest_impl.GET) + rest_impl._request.assert_called_once_with(mock_route) @pytest.mark.asyncio - async def test_create_webhook_without_optionals(self, http_client_impl): + async def test_create_webhook_without_optionals(self, rest_impl): mock_response = {"channel_id": "39393993", "id": "8383838"} - http_client_impl._request.return_value = mock_response + rest_impl._request.return_value = mock_response mock_route = mock.MagicMock(routes.CHANNEL_WEBHOOKS) with mock.patch.object(routes, "CHANNEL_WEBHOOKS", compile=mock.MagicMock(return_value=mock_route)): - assert await http_client_impl.create_webhook("39393939", "I am a webhook") is mock_response - routes.CHANNEL_WEBHOOKS.compile.assert_called_once_with(http_client_impl.POST, channel_id="39393939") - http_client_impl._request.assert_called_once_with(mock_route, json_body={"name": "I am a webhook"}, reason=...) + assert await rest_impl.create_webhook("39393939", "I am a webhook") is mock_response + routes.CHANNEL_WEBHOOKS.compile.assert_called_once_with(rest_impl.POST, channel_id="39393939") + rest_impl._request.assert_called_once_with(mock_route, json_body={"name": "I am a webhook"}, reason=...) @pytest.mark.asyncio - async def test_create_webhook_with_optionals(self, http_client_impl): + async def test_create_webhook_with_optionals(self, rest_impl): mock_response = {"channel_id": "39393993", "id": "8383838"} - http_client_impl._request.return_value = mock_response + rest_impl._request.return_value = mock_response mock_route = mock.MagicMock(routes.CHANNEL_WEBHOOKS) mock_image_data = "data:image/png;base64,iVBORw0KGgpibGFo" with mock.patch.object(routes, "CHANNEL_WEBHOOKS", compile=mock.MagicMock(return_value=mock_route)): with mock.patch.object(transformations, "image_bytes_to_image_data", return_value=mock_image_data): - result = await http_client_impl.create_webhook( + result = await rest_impl.create_webhook( "39393939", "I am a webhook", avatar=b"\211PNG\r\n\032\nblah", reason="get reasoned" ) assert result is mock_response - routes.CHANNEL_WEBHOOKS.compile.assert_called_once_with(http_client_impl.POST, channel_id="39393939") + routes.CHANNEL_WEBHOOKS.compile.assert_called_once_with(rest_impl.POST, channel_id="39393939") transformations.image_bytes_to_image_data.assert_called_once_with(b"\211PNG\r\n\032\nblah") - http_client_impl._request.assert_called_once_with( + rest_impl._request.assert_called_once_with( mock_route, json_body={"name": "I am a webhook", "avatar": mock_image_data}, reason="get reasoned", ) @pytest.mark.asyncio - async def test_get_channel_webhooks(self, http_client_impl): + async def test_get_channel_webhooks(self, rest_impl): mock_response = [{"channel_id": "39393993", "id": "8383838"}] - http_client_impl._request.return_value = mock_response + rest_impl._request.return_value = mock_response mock_route = mock.MagicMock(routes.CHANNEL_WEBHOOKS) with mock.patch.object(routes, "CHANNEL_WEBHOOKS", compile=mock.MagicMock(return_value=mock_route)): - assert await http_client_impl.get_channel_webhooks("9393939") is mock_response - routes.CHANNEL_WEBHOOKS.compile.assert_called_once_with(http_client_impl.GET, channel_id="9393939") - http_client_impl._request.assert_called_once_with(mock_route) + assert await rest_impl.get_channel_webhooks("9393939") is mock_response + routes.CHANNEL_WEBHOOKS.compile.assert_called_once_with(rest_impl.GET, channel_id="9393939") + rest_impl._request.assert_called_once_with(mock_route) @pytest.mark.asyncio - async def test_get_guild_webhooks(self, http_client_impl): + async def test_get_guild_webhooks(self, rest_impl): mock_response = [{"channel_id": "39393993", "id": "8383838"}] - http_client_impl._request.return_value = mock_response + rest_impl._request.return_value = mock_response mock_route = mock.MagicMock(routes.GUILD_WEBHOOKS) with mock.patch.object(routes, "GUILD_WEBHOOKS", compile=mock.MagicMock(return_value=mock_route)): - assert await http_client_impl.get_guild_webhooks("9393939") is mock_response - routes.GUILD_WEBHOOKS.compile.assert_called_once_with(http_client_impl.GET, guild_id="9393939") - http_client_impl._request.assert_called_once_with(mock_route) + assert await rest_impl.get_guild_webhooks("9393939") is mock_response + routes.GUILD_WEBHOOKS.compile.assert_called_once_with(rest_impl.GET, guild_id="9393939") + rest_impl._request.assert_called_once_with(mock_route) @pytest.mark.asyncio - async def test_get_webhook_without_token(self, http_client_impl): + async def test_get_webhook_without_token(self, rest_impl): mock_response = {"channel_id": "39393993", "id": "8383838"} - http_client_impl._request.return_value = mock_response + rest_impl._request.return_value = mock_response mock_route = mock.MagicMock(routes.WEBHOOK) with mock.patch.object(routes, "WEBHOOK", compile=mock.MagicMock(return_value=mock_route)): - assert await http_client_impl.get_webhook("9393939") is mock_response - routes.WEBHOOK.compile.assert_called_once_with(http_client_impl.GET, webhook_id="9393939") - http_client_impl._request.assert_called_once_with(mock_route, suppress_authorization_header=False) + assert await rest_impl.get_webhook("9393939") is mock_response + routes.WEBHOOK.compile.assert_called_once_with(rest_impl.GET, webhook_id="9393939") + rest_impl._request.assert_called_once_with(mock_route, suppress_authorization_header=False) @pytest.mark.asyncio - async def test_get_webhook_with_token(self, http_client_impl): + async def test_get_webhook_with_token(self, rest_impl): mock_response = {"channel_id": "39393993", "id": "8383838"} - http_client_impl._request.return_value = mock_response + rest_impl._request.return_value = mock_response mock_route = mock.MagicMock(routes.WEBHOOK_WITH_TOKEN) with mock.patch.object(routes, "WEBHOOK_WITH_TOKEN", compile=mock.MagicMock(return_value=mock_route)): - assert await http_client_impl.get_webhook("9393939", webhook_token="a_webhook_token") is mock_response + assert await rest_impl.get_webhook("9393939", webhook_token="a_webhook_token") is mock_response routes.WEBHOOK_WITH_TOKEN.compile.assert_called_once_with( - http_client_impl.GET, webhook_id="9393939", webhook_token="a_webhook_token" + rest_impl.GET, webhook_id="9393939", webhook_token="a_webhook_token" ) - http_client_impl._request.assert_called_once_with(mock_route, suppress_authorization_header=True) + rest_impl._request.assert_called_once_with(mock_route, suppress_authorization_header=True) @pytest.mark.asyncio - async def test_modify_webhook_without_optionals_without_token(self, http_client_impl): + async def test_modify_webhook_without_optionals_without_token(self, rest_impl): mock_response = {"channel_id": "39393993", "id": "8383838"} - http_client_impl._request.return_value = mock_response + rest_impl._request.return_value = mock_response mock_route = mock.MagicMock(routes.WEBHOOK) with mock.patch.object(routes, "WEBHOOK", compile=mock.MagicMock(return_value=mock_route)): - assert await http_client_impl.modify_webhook("929292") is mock_response - routes.WEBHOOK.compile.assert_called_once_with(http_client_impl.PATCH, webhook_id="929292") - http_client_impl._request.assert_called_once_with( + assert await rest_impl.modify_webhook("929292") is mock_response + routes.WEBHOOK.compile.assert_called_once_with(rest_impl.PATCH, webhook_id="929292") + rest_impl._request.assert_called_once_with( mock_route, json_body={}, reason=..., suppress_authorization_header=False ) @pytest.mark.asyncio - async def test_modify_webhook_with_optionals_without_token(self, http_client_impl): + async def test_modify_webhook_with_optionals_without_token(self, rest_impl): mock_response = {"channel_id": "39393993", "id": "8383838"} - http_client_impl._request.return_value = mock_response + rest_impl._request.return_value = mock_response mock_route = mock.MagicMock(routes.WEBHOOK) with mock.patch.object(routes, "WEBHOOK", compile=mock.MagicMock(return_value=mock_route)): assert ( - await http_client_impl.modify_webhook( + await rest_impl.modify_webhook( "929292", name="nyaa", avatar=b"\211PNG\r\n\032\nblah", channel_id="2929292929", reason="nuzzle", ) is mock_response ) - routes.WEBHOOK.compile.assert_called_once_with(http_client_impl.PATCH, webhook_id="929292") - http_client_impl._request.assert_called_once_with( + routes.WEBHOOK.compile.assert_called_once_with(rest_impl.PATCH, webhook_id="929292") + rest_impl._request.assert_called_once_with( mock_route, json_body={"name": "nyaa", "avatar": "data:image/png;base64,iVBORw0KGgpibGFo", "channel_id": "2929292929",}, reason="nuzzle", @@ -2001,53 +2022,53 @@ async def test_modify_webhook_with_optionals_without_token(self, http_client_imp ) @pytest.mark.asyncio - async def test_modify_webhook_without_optionals_with_token(self, http_client_impl): + async def test_modify_webhook_without_optionals_with_token(self, rest_impl): mock_response = {"channel_id": "39393993", "id": "8383838"} - http_client_impl._request.return_value = mock_response + rest_impl._request.return_value = mock_response mock_route = mock.MagicMock(routes.WEBHOOK_WITH_TOKEN) with mock.patch.object(routes, "WEBHOOK_WITH_TOKEN", compile=mock.MagicMock(return_value=mock_route)): - assert await http_client_impl.modify_webhook("929292", webhook_token="a_webhook_token") is mock_response + assert await rest_impl.modify_webhook("929292", webhook_token="a_webhook_token") is mock_response routes.WEBHOOK_WITH_TOKEN.compile.assert_called_once_with( - http_client_impl.PATCH, webhook_id="929292", webhook_token="a_webhook_token" + rest_impl.PATCH, webhook_id="929292", webhook_token="a_webhook_token" ) - http_client_impl._request.assert_called_once_with( + rest_impl._request.assert_called_once_with( mock_route, json_body={}, reason=..., suppress_authorization_header=True ) @pytest.mark.asyncio - async def test_delete_webhook_without_token(self, http_client_impl): + async def test_delete_webhook_without_token(self, rest_impl): mock_route = mock.MagicMock(routes.WEBHOOK) with mock.patch.object(routes, "WEBHOOK", compile=mock.MagicMock(return_value=mock_route)): - assert await http_client_impl.delete_webhook("9393939") is None - routes.WEBHOOK.compile.assert_called_once_with(http_client_impl.DELETE, webhook_id="9393939") - http_client_impl._request.assert_called_once_with(mock_route, suppress_authorization_header=False) + assert await rest_impl.delete_webhook("9393939") is None + routes.WEBHOOK.compile.assert_called_once_with(rest_impl.DELETE, webhook_id="9393939") + rest_impl._request.assert_called_once_with(mock_route, suppress_authorization_header=False) @pytest.mark.asyncio - async def test_delete_webhook_with_token(self, http_client_impl): + async def test_delete_webhook_with_token(self, rest_impl): mock_route = mock.MagicMock(routes.WEBHOOK_WITH_TOKEN) with mock.patch.object(routes, "WEBHOOK_WITH_TOKEN", compile=mock.MagicMock(return_value=mock_route)): - assert await http_client_impl.delete_webhook("9393939", webhook_token="a_webhook_token") is None + assert await rest_impl.delete_webhook("9393939", webhook_token="a_webhook_token") is None routes.WEBHOOK_WITH_TOKEN.compile.assert_called_once_with( - http_client_impl.DELETE, webhook_id="9393939", webhook_token="a_webhook_token" + rest_impl.DELETE, webhook_id="9393939", webhook_token="a_webhook_token" ) - http_client_impl._request.assert_called_once_with(mock_route, suppress_authorization_header=True) + rest_impl._request.assert_called_once_with(mock_route, suppress_authorization_header=True) @pytest.mark.asyncio - async def test_execute_webhook_without_optionals(self, http_client_impl): + async def test_execute_webhook_without_optionals(self, rest_impl): mock_form = mock.MagicMock(spec_set=aiohttp.FormData, add_field=mock.MagicMock()) mock_route = mock.MagicMock(routes.WEBHOOK_WITH_TOKEN) - http_client_impl._request.return_value = None + rest_impl._request.return_value = None mock_json = "{}" with mock.patch.object(aiohttp, "FormData", autospec=True, return_value=mock_form): with mock.patch.object(routes, "WEBHOOK_WITH_TOKEN", compile=mock.MagicMock(return_value=mock_route)): with mock.patch.object(json, "dumps", return_value=mock_json): - assert await http_client_impl.execute_webhook("9393939", "a_webhook_token") is None + assert await rest_impl.execute_webhook("9393939", "a_webhook_token") is None routes.WEBHOOK_WITH_TOKEN.compile.assert_called_once_with( - http_client_impl.POST, webhook_id="9393939", webhook_token="a_webhook_token" + rest_impl.POST, webhook_id="9393939", webhook_token="a_webhook_token" ) json.dumps.assert_called_once_with({}) mock_form.add_field.assert_called_once_with("payload_json", mock_json, content_type="application/json") - http_client_impl._request.assert_called_once_with( + rest_impl._request.assert_called_once_with( mock_route, form_body=mock_form, re_seekable_resources=[], query={}, suppress_authorization_header=True, ) @@ -2058,19 +2079,19 @@ async def test_execute_webhook_without_optionals(self, http_client_impl): @unittest.mock.patch.object(json, "dumps") @unittest.mock.patch.object(storage, "make_resource_seekable") async def test_execute_webhook_with_optionals( - self, make_resource_seekable, dumps, WEBHOOK_WITH_TOKEN, FormData, http_client_impl + self, make_resource_seekable, dumps, WEBHOOK_WITH_TOKEN, FormData, rest_impl ): mock_form = mock.MagicMock(spec_set=aiohttp.FormData, add_field=mock.MagicMock()) FormData.return_value = mock_form mock_route = mock.MagicMock(routes.WEBHOOK_WITH_TOKEN) WEBHOOK_WITH_TOKEN.compile.return_value = mock_route mock_response = {"id": "53", "content": "la"} - http_client_impl._request.return_value = mock_response + rest_impl._request.return_value = mock_response mock_bytes = mock.MagicMock(io.BytesIO) make_resource_seekable.return_value = mock_bytes mock_json = '{"content": "A messages", "username": "agent 42"}' dumps.return_value = mock_json - response = await http_client_impl.execute_webhook( + response = await rest_impl.execute_webhook( "9393939", "a_webhook_token", content="A message", @@ -2085,7 +2106,7 @@ async def test_execute_webhook_with_optionals( assert response is mock_response make_resource_seekable.assert_called_once_with(b"4444ididid") routes.WEBHOOK_WITH_TOKEN.compile.assert_called_once_with( - http_client_impl.POST, webhook_id="9393939", webhook_token="a_webhook_token" + rest_impl.POST, webhook_id="9393939", webhook_token="a_webhook_token" ) dumps.assert_called_once_with( { @@ -2107,7 +2128,7 @@ async def test_execute_webhook_with_optionals( any_order=True, ) - http_client_impl._request.assert_called_once_with( + rest_impl._request.assert_called_once_with( mock_route, form_body=mock_form, re_seekable_resources=[mock_bytes], From 5121bc9789ddfe8c679b35ea925afbf0fc1cc73f Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Wed, 25 Mar 2020 13:13:11 +0000 Subject: [PATCH 039/922] Fixed failing dispatcher tests and renamed EventDelegate to EventDispatcher, added support for async predicates in 'wait_for'. --- hikari/core/dispatcher.py | 94 ++++++++------ hikari/internal_utilities/cdn.py | 2 +- hikari/internal_utilities/containers.py | 15 +-- tests/hikari/core/test_dispatcher.py | 161 +++++++----------------- 4 files changed, 110 insertions(+), 162 deletions(-) diff --git a/hikari/core/dispatcher.py b/hikari/core/dispatcher.py index 8eb072b70e..5c629816c8 100644 --- a/hikari/core/dispatcher.py +++ b/hikari/core/dispatcher.py @@ -17,20 +17,20 @@ # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . """Event dispatcher implementation.""" +__all__ = ["EventDispatcher"] + import asyncio import logging - import typing import weakref +from hikari.core import events from hikari.internal_utilities import aio from hikari.internal_utilities import assertions from hikari.internal_utilities import loggers -from hikari.core import events - -class EventDelegate: +class EventDispatcher: """Handles storing and dispatching to event listeners and one-time event waiters. @@ -120,30 +120,49 @@ def dispatch(self, event: events.HikariEvent): """ event_t = type(event) - if event_t in self._waiters: - for future, predicate in tuple(self._waiters[event_t].items()): - try: - if predicate(event): - future.set_result(event) - del self._waiters[event_t][future] - except Exception as ex: - future.set_exception(ex) - del self._waiters[event_t][future] + awaitables = [] - if not self._waiters[event_t]: - del self._waiters[event_t] + if event_t in self._listeners: + for callback in self._listeners[event_t]: + awaitables.append(self._catch(callback, event)) - # Hack to stop PyCharm saying you need to await this function when you do not need to await - # it. - future: typing.Any + # Only try to awaken waiters when the waiter is registered as a valid + # event type and there is more than 0 waiters in that dict. + if waiters := self._waiters.get(event_t): + # Run this in the background as a coroutine so that any async predicates + # can be awaited concurrently. + awaitables.append(asyncio.create_task(self._awaken_waiters(waiters, event))) - if event_t in self._listeners: - coros = (self._catch(callback, event) for callback in self._listeners[event_t]) - future = asyncio.gather(*coros) - else: - future = aio.completed_future() + result = asyncio.gather(*awaitables) if awaitables else aio.completed_future() + + # Stop false positives from linters that now assume this is a coroutine function + result: typing.Any + + return result + + async def _awaken_waiters(self, waiters, event): + await asyncio.gather( + *(self._maybe_awaken_waiter(event, future, predicate) for future, predicate in tuple(waiters.items())) + ) + + async def _maybe_awaken_waiter(self, event, future, predicate): + delete_waiter = True + try: + result = predicate(event) + if result or asyncio.iscoroutine(result) and await result: + future.set_result(event) + else: + delete_waiter = False + except Exception as ex: + delete_waiter = True + future.set_exception(ex) + + event_t = type(event) - return future + if delete_waiter: + del self._waiters[event_t][future] + if not self._waiters[event_t]: + del self._waiters[event_t] async def _catch(self, callback, event): try: @@ -218,25 +237,26 @@ def wait_for( leak memory if you do this from an event listener that gets repeatedly called. If you want to do this, you should consider using an event listener instead of this function. - predicate : :obj:`typing.Callable` [ ..., :obj:`bool` ] + predicate : ``def predicate(event) -> bool`` or ``async def predicate(event) -> bool`` A function that takes the arguments for the event and returns True if it is a match, or False if it should be ignored. - This cannot be a coroutine function. + This can be a coroutine function that returns a boolean, or a + regular function. Returns ------- :obj:`asyncio.Future` - A future that when awaited will provide a the arguments passed to the - first matching event. If no arguments are passed to the event, then - `None` is the result. If one argument is passed to the event, then - that argument is the result, otherwise a tuple of arguments is the - result instead. - - Note - ---- - Awaiting this result will raise an :obj:`asyncio.TimeoutError` if the timeout - is hit and no match is found. If the predicate throws any exception, - this is raised immediately. + A future that when awaited will provide a the arguments passed to + the first matching event. If no arguments are passed to the event, + then `None` is the result. If one argument is passed to the event, + then that argument is the result, otherwise a tuple of arguments is + the result instead. + + Notes + ----- + Awaiting this result will raise an :obj:`asyncio.TimeoutError` if the + timeout is hit and no match is found. If the predicate throws any + exception, this is raised immediately. """ future = asyncio.get_event_loop().create_future() if event_type not in self._waiters: diff --git a/hikari/internal_utilities/cdn.py b/hikari/internal_utilities/cdn.py index b302d6448a..300e4f36d8 100644 --- a/hikari/internal_utilities/cdn.py +++ b/hikari/internal_utilities/cdn.py @@ -22,7 +22,7 @@ ] import typing -import urllib +import urllib.parse BASE_CDN_URL = "https://cdn.discordapp.com" diff --git a/hikari/internal_utilities/containers.py b/hikari/internal_utilities/containers.py index a6934c2f46..d7bb77e830 100644 --- a/hikari/internal_utilities/containers.py +++ b/hikari/internal_utilities/containers.py @@ -17,21 +17,11 @@ # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . """Custom data structures and constant values.""" -__all__ = [ - "EMPTY_SEQUENCE", - "EMPTY_SET", - "EMPTY_COLLECTION", - "EMPTY_DICT", -] +__all__ = ["EMPTY_SEQUENCE", "EMPTY_SET", "EMPTY_COLLECTION", "EMPTY_DICT", "EMPTY_GENERATOR_EXPRESSION"] import types import typing -# If more than one empty-definition is used in the same context, the type checker will probably whinge, so we have -# to keep separate types... -HashableT = typing.TypeVar("HashableT", bound=typing.Hashable) -ValueT = typing.TypeVar("ValueT") - #: An immutable indexable container of elements with zero size. EMPTY_SEQUENCE: typing.Sequence = tuple() #: An immutable unordered container of elements with zero size. @@ -40,3 +30,6 @@ EMPTY_COLLECTION: typing.Collection = tuple() #: An immutable ordered mapping of key elements to value elements with zero size. EMPTY_DICT: typing.Mapping = types.MappingProxyType({}) +#: An empty generator expression that can be used as a placeholder, but never +#: yields anything. +EMPTY_GENERATOR_EXPRESSION = (_ for _ in EMPTY_COLLECTION) diff --git a/tests/hikari/core/test_dispatcher.py b/tests/hikari/core/test_dispatcher.py index 2446c9d071..b6fe6f412c 100644 --- a/tests/hikari/core/test_dispatcher.py +++ b/tests/hikari/core/test_dispatcher.py @@ -23,7 +23,6 @@ from hikari.core import dispatcher from hikari.core import events -from hikari.internal_utilities import aio from tests.hikari import _helpers @@ -39,13 +38,12 @@ class TestEvent3(events.HikariEvent): ... -@pytest.mark.skip("TODO: fixme") -class TestEventDelegate: +class TestEventDispatcher: EXCEPTION_EVENT = "exception" @pytest.fixture def delegate(self): - return _helpers.unslot_class(dispatcher.EventDelegate)() + return _helpers.unslot_class(dispatcher.EventDispatcher)() # noinspection PyTypeChecker @_helpers.assert_raises(type_=TypeError) @@ -113,20 +111,22 @@ def test_dispatch_to_existing_muxes(self, delegate): mock_coro_fn2 = mock.MagicMock() mock_coro_fn3 = mock.MagicMock() - delegate._listeners["foo"] = [mock_coro_fn1, mock_coro_fn2] - delegate._listeners["bar"] = [mock_coro_fn3] + ctx = TestEvent1() - args = ("a", "b", "c") + delegate._listeners[TestEvent1] = [mock_coro_fn1, mock_coro_fn2] + delegate._listeners[TestEvent2] = [mock_coro_fn3] with mock.patch("asyncio.gather") as gather: - delegate.dispatch("foo", *args) - gather.assert_called_once_with( - delegate._catch(mock_coro_fn1, "foo", args), delegate._catch(mock_coro_fn2, "foo", args) - ) + delegate.dispatch(ctx) + gather.assert_called_once_with(delegate._catch(mock_coro_fn1, ctx), delegate._catch(mock_coro_fn2, ctx)) def test_dispatch_to_non_existant_muxes(self, delegate): # Should not throw. - delegate.dispatch("foo", "a", "b", "c") + delegate._waiters = {} + delegate._listeners = {} + delegate.dispatch(TestEvent1()) + assert delegate._waiters == {} + assert delegate._listeners == {} @pytest.mark.asyncio async def test_dispatch_is_awaitable_if_nothing_is_invoked(self, delegate): @@ -143,73 +143,9 @@ async def test_dispatch_is_awaitable_if_something_is_invoked(self, delegate): await delegate.dispatch("foo") @pytest.mark.asyncio - @pytest.mark.parametrize("predicate_return", (True, False)) - @pytest.mark.parametrize( - ("in_event_args", "expected_result"), [((), None,), ((12,), 12), ((12, 22, 33), (12, 22, 33))] - ) @_helpers.timeout_after(1) - async def test_dispatch_awakens_matching_futures( - self, delegate, event_loop, predicate_return, in_event_args, expected_result - ): - future1 = event_loop.create_future() - future2 = event_loop.create_future() - future3 = event_loop.create_future() - future4 = event_loop.create_future() - - predicate1 = mock.MagicMock(return_value=predicate_return) - predicate2 = mock.MagicMock(return_value=predicate_return) - predicate3 = mock.MagicMock(return_value=True) - predicate4 = mock.MagicMock(return_value=False) - - delegate._waiters[TestEvent1] = {} - delegate._waiters[TestEvent2] = {future3: predicate3} - delegate._waiters[TestEvent1][future1] = predicate1 - delegate._waiters[TestEvent1][future2] = predicate2 - # Shouldn't be invoked, as the predicate is always false-returning. - delegate._waiters[TestEvent1][future4] = predicate4 - - event_ctx = TestEvent1() - await delegate.dispatch(event_ctx) - - assert future1.done() is predicate_return - predicate1.assert_called_once_with(event_ctx) - assert future2.done() is predicate_return - predicate2.assert_called_once_with(event_ctx) - assert future3.done() is False - predicate3.assert_not_called() - assert future4.done() is False - predicate4.assert_called_once_with(event_ctx) - - if predicate_return: - assert await future1 == expected_result - assert await future2 == expected_result - - @pytest.mark.asyncio - @pytest.mark.parametrize("predicate_return", (True, False)) - @pytest.mark.parametrize( - ("in_event_args", "expected_result"), [((), None,), ((12,), 12), ((12, 22, 33), (12, 22, 33))] - ) - @_helpers.timeout_after(1) - async def test_dispatch_removes_awoken_future( - self, delegate, event_loop, predicate_return, in_event_args, expected_result - ): - future = event_loop.create_future() - - predicate = mock.MagicMock() - - delegate._waiters["foobar"] = {} - delegate._waiters["foobar"][future] = predicate - # Add a second future that never gets hit so the weakref map is not dropped from being - # empty. - delegate._waiters["foobar"][event_loop.create_future()] = lambda *_: False - - await delegate.dispatch("foobar", *in_event_args) - predicate.assert_called_once_with(*in_event_args) - predicate.reset_mock() - - await delegate.dispatch("foobar", *in_event_args) - predicate.assert_not_called() - assert future not in delegate._waiters["foobar"] + async def test_dispatch_invokes_future_waker_if_registered_with_futures(self, delegate, event_loop): + delegate._waiters[TestEvent1] = {event_loop.create_future(): lambda _: False} @pytest.mark.asyncio @_helpers.timeout_after(1) @@ -220,11 +156,13 @@ async def test_dispatch_returns_exception_to_caller(self, delegate, event_loop): future1 = event_loop.create_future() future2 = event_loop.create_future() - delegate._waiters["foobar"] = {} - delegate._waiters["foobar"][future1] = predicate1 - delegate._waiters["foobar"][future2] = predicate2 + ctx = TestEvent3() + + delegate._waiters[TestEvent3] = {} + delegate._waiters[TestEvent3][future1] = predicate1 + delegate._waiters[TestEvent3][future2] = predicate2 - await delegate.dispatch("foobar", object(), object(), object()) + await delegate.dispatch(ctx) try: await future1 @@ -240,55 +178,49 @@ async def test_dispatch_returns_exception_to_caller(self, delegate, event_loop): pass @pytest.mark.asyncio - async def test_waiter_map_deleted_if_already_empty(self, delegate): - delegate._waiters[TestEvent1] = {} - await delegate.dispatch(TestEvent1()) - assert TestEvent1 not in delegate._waiters - - @pytest.mark.asyncio + @_helpers.timeout_after(1) async def test_waiter_map_deleted_if_made_empty_during_this_dispatch(self, delegate): delegate._waiters[TestEvent1] = {mock.MagicMock(): mock.MagicMock(return_value=True)} - await delegate.dispatch(TestEvent1()) + delegate.dispatch(TestEvent1()) + await asyncio.sleep(0.1) assert TestEvent1 not in delegate._waiters @pytest.mark.asyncio + @_helpers.timeout_after(1) async def test_waiter_map_not_deleted_if_not_empty(self, delegate): delegate._waiters["foobar"] = {mock.MagicMock(): mock.MagicMock(return_value=False)} - await delegate.dispatch("foobar") + delegate.dispatch("foobar") + await asyncio.sleep(0.1) assert "foobar" in delegate._waiters @pytest.mark.asyncio @_helpers.timeout_after(2) - async def test_wait_for_returns_event(self, delegate, in_event_args, expected_result): + async def test_wait_for_returns_event(self, delegate): predicate = mock.MagicMock(return_value=True) future = delegate.wait_for(TestEvent1, timeout=5, predicate=predicate) - event = TestEvent1() - await delegate.dispatch(event) + ctx = TestEvent1() + await delegate.dispatch(ctx) - await asyncio.sleep(0.5) + await asyncio.sleep(0.1) assert future.done() actual_result = await future - assert actual_result == expected_result - predicate.assert_called_once_with(*in_event_args) + assert actual_result == ctx + predicate.assert_called_once_with(ctx) @pytest.mark.asyncio - @pytest.mark.parametrize( - ("in_event_args", "expected_result"), [((), None,), ((12,), 12), ((12, 22, 33), (12, 22, 33))] - ) @_helpers.timeout_after(2) - async def test_wait_for_returns_matching_event_args_when_invoked_but_no_predicate_match( - self, delegate, in_event_args, expected_result - ): + async def test_wait_for_returns_matching_event_args_when_invoked_but_no_predicate_match(self, delegate): predicate = mock.MagicMock(return_value=False) - future = delegate.wait_for("foobar", timeout=5, predicate=predicate) - await delegate.dispatch("foobar", *in_event_args) + ctx = TestEvent3() + future = delegate.wait_for(TestEvent3, timeout=5, predicate=predicate) + await delegate.dispatch(ctx) - await asyncio.sleep(0.5) + await asyncio.sleep(0.1) assert not future.done() - predicate.assert_called_once_with(*in_event_args) + predicate.assert_called_once_with(ctx) @pytest.mark.asyncio @_helpers.assert_raises(type_=asyncio.TimeoutError) @@ -302,8 +234,9 @@ async def test_wait_for_hits_timeout_and_raises(self, delegate): @_helpers.assert_raises(type_=RuntimeError) async def test_wait_for_raises_predicate_errors(self, delegate): predicate = mock.MagicMock(side_effect=RuntimeError) - future = delegate.wait_for("foobar", timeout=1, predicate=predicate) - await delegate.dispatch("foobar", object()) + ctx = TestEvent1() + future = delegate.wait_for(TestEvent1, timeout=1, predicate=predicate) + await delegate.dispatch(ctx) await future @pytest.mark.asyncio @@ -322,9 +255,10 @@ async def test_other_events_in_same_waiter_event_name_do_not_awaken_us( @pytest.mark.asyncio async def test_catch_happy_path(self, delegate): callback = mock.AsyncMock() + event = TestEvent1() delegate.handle_exception = mock.MagicMock() - await delegate._catch(callback, "wubalubadubdub", ("blep1", "blep2", "blep3")) - callback.assert_awaited_once_with("blep1", "blep2", "blep3") + await delegate._catch(callback, event) + callback.assert_awaited_once_with(event) delegate.handle_exception.assert_not_called() @pytest.mark.asyncio @@ -332,8 +266,9 @@ async def test_catch_sad_path(self, delegate): ex = RuntimeError() callback = mock.AsyncMock(side_effect=ex) delegate.handle_exception = mock.MagicMock() - await delegate._catch(callback, "wubalubadubdub", ("blep1", "blep2", "blep3")) - delegate.handle_exception.assert_called_once_with(ex, "wubalubadubdub", ("blep1", "blep2", "blep3"), callback) + ctx = TestEvent3() + await delegate._catch(callback, ctx) + delegate.handle_exception.assert_called_once_with(ex, ctx, callback) def test_handle_exception_dispatches_exception_event_with_context(self, delegate): delegate.dispatch = mock.MagicMock() @@ -344,7 +279,7 @@ def test_handle_exception_dispatches_exception_event_with_context(self, delegate delegate.handle_exception(ex, event, callback) - expected_ctx = events.ExceptionEvent(..., ..., ...) + expected_ctx = events.ExceptionEvent(ex, event, callback) delegate.dispatch.assert_called_once_with(expected_ctx) def test_handle_exception_will_not_recursively_invoke_exception_handler_event(self, delegate): From 1c2c371c803ea796bd723f090c483cf1f9098fb7 Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Wed, 25 Mar 2020 13:59:36 +0000 Subject: [PATCH 040/922] Made ABC for dispatcher, added close() to dispatcher impl and tested, let gateway take custom dispatcher type to init. --- hikari/core/dispatcher.py | 189 ++++++++++++++++--- hikari/core/gateway_client.py | 10 +- hikari/internal_utilities/containers.py | 25 ++- tests/hikari/core/test_dispatcher.py | 240 +++++++++++++++--------- 4 files changed, 338 insertions(+), 126 deletions(-) diff --git a/hikari/core/dispatcher.py b/hikari/core/dispatcher.py index 5c629816c8..cc76bdb670 100644 --- a/hikari/core/dispatcher.py +++ b/hikari/core/dispatcher.py @@ -17,8 +17,9 @@ # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . """Event dispatcher implementation.""" -__all__ = ["EventDispatcher"] +__all__ = ["EventDispatcher", "EventDispatcherImpl"] +import abc import asyncio import logging import typing @@ -27,10 +28,129 @@ from hikari.core import events from hikari.internal_utilities import aio from hikari.internal_utilities import assertions +from hikari.internal_utilities import containers from hikari.internal_utilities import loggers -class EventDispatcher: +EventT = typing.TypeVar("EventT", bound=events.HikariEvent) +PredicateT = typing.Callable[[EventT], typing.Union[bool, typing.Coroutine[None, None, bool]]] +EventCallbackT = typing.Callable[[EventT], typing.Coroutine[None, None, typing.Any]] + + +class EventDispatcher(abc.ABC): + """Base definition for a conforming event dispatcher implementation. + + This enables users to implement their own event dispatching strategies + if the base implementation is not suitable. This could be used to write + a distributed bot dispatcher, for example, or could handle dispatching + to a set of micro-interpreter instances to achieve greater concurrency. + """ + @abc.abstractmethod + def close(self): + """Cancel anything that is waiting for an event to be dispatched.""" + + @abc.abstractmethod + def add_listener( + self, + event_type: typing.Type[EventT], + callback: EventCallbackT + ) -> EventCallbackT: + """Register a new event callback to a given event name. + + Parameters + ---------- + event_type : :obj:`typing.Type` [ :obj:`events.HikariEvent` ] + The event to register to. + callback : ``async def callback(event: HikariEvent) -> ...`` + The event callback to invoke when this event is fired. + + Raises + ------ + :obj:`TypeError` + If ``coroutine_function`` is not a coroutine. + """ + + @abc.abstractmethod + def remove_listener( + self, + event_type: typing.Type[EventT], + callback: EventCallbackT, + ) -> EventCallbackT: + """Remove the given coroutine function from the handlers for the given event. + + The name is mandatory to enable supporting registering the same event callback for multiple event types. + + Parameters + ---------- + event_type : :obj:`typing.Type` [ :obj:`events.HikariEvent` ] + The type of event to remove the callback from. + callback : ``async def callback(event: HikariEvent) -> ...`` + The event callback to invoke when this event is fired. + """ + + @abc.abstractmethod + def wait_for( + self, + event_type: typing.Type[EventT], + *, + timeout: typing.Optional[float], + predicate: PredicateT + ) -> asyncio.Future: + """Wait for the given event type to occur. + + Parameters + ---------- + event_type : :obj:`typing.Type` [ :obj:`events.HikariEvent` ] + The name of the event to wait for. + timeout : :obj:`float`, optional + The timeout to wait for before cancelling and raising an + :obj:`asyncio.TimeoutError` instead. If this is `None`, this will + wait forever. Care must be taken if you use `None` as this may + leak memory if you do this from an event listener that gets + repeatedly called. If you want to do this, you should consider + using an event listener instead of this function. + predicate : ``def predicate(event) -> bool`` or ``async def predicate(event) -> bool`` + A function that takes the arguments for the event and returns True + if it is a match, or False if it should be ignored. + This can be a coroutine function that returns a boolean, or a + regular function. + + Returns + ------- + :obj:`asyncio.Future`: + A future to await. When the given event is matched, this will be + completed with the corresponding event body. + + If the predicate throws an exception, or the timeout is reached, + then this will be set as an exception on the returned future. + + Notes + ----- + The event type is not expected to be considered in a polymorphic + lookup, but can be implemented this way optionally if documented. + """ + + # Do not add an annotation here, it will mess with type hints in PyCharm which can lead to + # confusing telepathy comments to the user. + @abc.abstractmethod + def dispatch_event(self, event: events.HikariEvent) -> ...: + """Dispatch a given event to any listeners and waiters. + + Parameters + ---------- + event : :obj:`events.HikariEvent` + The event to dispatch. + + Returns + ------- + :obj:`asyncio.Future`: + a future that can be optionally awaited if you need to wait for all + listener callbacks and waiters to be processed. If this is not + awaited, the invocation is invoked soon on the current event loop. + """ + + +class EventDispatcherImpl(EventDispatcher): """Handles storing and dispatching to event listeners and one-time event waiters. @@ -54,18 +174,26 @@ class EventDispatcher: __slots__ = ("exception_event", "logger", "_listeners", "_waiters") def __init__(self) -> None: - self._listeners = {} - self._waiters = {} + self._listeners: typing.Dict[typing.Type[EventT], typing.List[EventCallbackT]] = {} + self._waiters: typing.Dict[typing.Type[EventT], containers.WeakKeyDictionary[asyncio.Future, PredicateT]] = {} self.logger = loggers.get_named_logger(self) - def add(self, event: typing.Type[events.HikariEvent], coroutine_function: aio.CoroutineFunctionT) -> None: + def close(self) -> None: + """Cancel anything that is waiting for an event to be dispatched.""" + self._listeners.clear() + for waiter in self._waiters.values(): + for future in waiter.keys(): + future.cancel() + self._waiters.clear() + + def add_listener(self, event_type: typing.Type[events.HikariEvent], callback: EventCallbackT) -> None: """Register a new event callback to a given event name. Parameters ---------- - event : :obj:`typing.Type` [ :obj:`events.HikariEvent` ] + event_type : :obj:`typing.Type` [ :obj:`events.HikariEvent` ] The event to register to. - coroutine_function + callback : ``async def callback(event: HikariEvent) -> ...`` The event callback to invoke when this event is fired. Raises @@ -74,33 +202,33 @@ def add(self, event: typing.Type[events.HikariEvent], coroutine_function: aio.Co If ``coroutine_function`` is not a coroutine. """ assertions.assert_that( - asyncio.iscoroutinefunction(coroutine_function), "You must subscribe a coroutine function only", TypeError + asyncio.iscoroutinefunction(callback), "You must subscribe a coroutine function only", TypeError ) - if event not in self._listeners: - self._listeners[event] = [] - self._listeners[event].append(coroutine_function) + if event_type not in self._listeners: + self._listeners[event_type] = [] + self._listeners[event_type].append(callback) - def remove(self, name: str, coroutine_function: aio.CoroutineFunctionT) -> None: + def remove_listener(self, event_type: typing.Type[EventT], callback: EventCallbackT) -> None: """Remove the given coroutine function from the handlers for the given event. The name is mandatory to enable supporting registering the same event callback for multiple event types. Parameters ---------- - name : :obj:`str` - The event to remove from. - coroutine_function + event_type : :obj:`typing.Type` [ :obj:`events.HikariEvent` ] + The type of event to remove the callback from. + callback : ``async def callback(event: HikariEvent) -> ...`` The event callback to remove. """ - if name in self._listeners and coroutine_function in self._listeners[name]: - if len(self._listeners[name]) - 1 == 0: - del self._listeners[name] + if event_type in self._listeners and callback in self._listeners[event_type]: + if len(self._listeners[event_type]) - 1 == 0: + del self._listeners[event_type] else: - self._listeners[name].remove(coroutine_function) + self._listeners[event_type].remove(callback) # Do not add an annotation here, it will mess with type hints in PyCharm which can lead to # confusing telepathy comments to the user. - def dispatch(self, event: events.HikariEvent): + def dispatch_event(self, event: events.HikariEvent): """Dispatch a given event to all listeners and waiters that are applicable. @@ -120,20 +248,20 @@ def dispatch(self, event: events.HikariEvent): """ event_t = type(event) - awaitables = [] + futs = [] if event_t in self._listeners: for callback in self._listeners[event_t]: - awaitables.append(self._catch(callback, event)) + futs.append(self._catch(callback, event)) # Only try to awaken waiters when the waiter is registered as a valid # event type and there is more than 0 waiters in that dict. if waiters := self._waiters.get(event_t): # Run this in the background as a coroutine so that any async predicates # can be awaited concurrently. - awaitables.append(asyncio.create_task(self._awaken_waiters(waiters, event))) + futs.append(asyncio.create_task(self._awaken_waiters(waiters, event))) - result = asyncio.gather(*awaitables) if awaitables else aio.completed_future() + result = asyncio.gather(*futs) if futs else aio.completed_future() # lgtm [py/unused-local-variable] # Stop false positives from linters that now assume this is a coroutine function result: typing.Any @@ -160,9 +288,10 @@ async def _maybe_awaken_waiter(self, event, future, predicate): event_t = type(event) if delete_waiter: - del self._waiters[event_t][future] - if not self._waiters[event_t]: - del self._waiters[event_t] + if not len(self._waiters[event_t]) - 1: + del self._waiters[event_t] + else: + del self._waiters[event_t][future] async def _catch(self, callback, event): try: @@ -200,7 +329,7 @@ def handle_exception( self.logger.exception( 'Exception occurred in handler for event "%s"', type(event).__name__, exc_info=exception ) - self.dispatch(events.ExceptionEvent(exception=exception, event=event, callback=callback)) + self.dispatch_event(events.ExceptionEvent(exception=exception, event=event, callback=callback)) else: self.logger.exception( 'Exception occurred in handler for event "%s", and the exception has been dropped', @@ -210,10 +339,10 @@ def handle_exception( def wait_for( self, - event_type: typing.Type[events.HikariEvent], + event_type: typing.Type[EventT], *, timeout: typing.Optional[float], - predicate: typing.Callable[..., bool], + predicate: PredicateT, ) -> asyncio.Future: """Given an event name, wait for the event to occur once, then return the arguments that accompanied the event as the result. diff --git a/hikari/core/gateway_client.py b/hikari/core/gateway_client.py index 4161a90af1..a3195d0b62 100644 --- a/hikari/core/gateway_client.py +++ b/hikari/core/gateway_client.py @@ -30,6 +30,7 @@ import aiohttp +from hikari.core import dispatcher from hikari.core import events from hikari.core import gateway_config from hikari.core import gateway_entities @@ -403,11 +404,16 @@ def _create_presence_pl( class GatewayClient(typing.Generic[ShardT], Startable): def __init__( - self, config: gateway_config.GatewayConfig, url: str, shard_type: typing.Type[ShardT] = ShardClient, + self, + config: gateway_config.GatewayConfig, + url: str, + shard_type: typing.Type[ShardT] = ShardClient, + ) -> None: self.logger = loggers.get_named_logger(self) self.config = config - self.event_dispatcher = aio.EventDelegate() + self.event_dispatcher = dispatcher.EventDispatcher() + self.state self.shards: typing.Dict[int, ShardT] = { shard_id: shard_type(shard_id, config, self._low_level_dispatch, url) for shard_id in config.shard_config.shard_ids diff --git a/hikari/internal_utilities/containers.py b/hikari/internal_utilities/containers.py index d7bb77e830..36dd5e81f3 100644 --- a/hikari/internal_utilities/containers.py +++ b/hikari/internal_utilities/containers.py @@ -17,12 +17,17 @@ # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . """Custom data structures and constant values.""" -__all__ = ["EMPTY_SEQUENCE", "EMPTY_SET", "EMPTY_COLLECTION", "EMPTY_DICT", "EMPTY_GENERATOR_EXPRESSION"] +__all__ = [ + "EMPTY_SEQUENCE", "EMPTY_SET", "EMPTY_COLLECTION", "EMPTY_DICT", "EMPTY_GENERATOR_EXPRESSION", + "WeakKeyDictionary" +] import types import typing #: An immutable indexable container of elements with zero size. +import weakref + EMPTY_SEQUENCE: typing.Sequence = tuple() #: An immutable unordered container of elements with zero size. EMPTY_SET: typing.AbstractSet = frozenset() @@ -33,3 +38,21 @@ #: An empty generator expression that can be used as a placeholder, but never #: yields anything. EMPTY_GENERATOR_EXPRESSION = (_ for _ in EMPTY_COLLECTION) + + +K = typing.TypeVar("K") +V = typing.TypeVar("V") + + +class WeakKeyDictionary(weakref.WeakKeyDictionary, typing.MutableMapping[K, V]): + """A dictionary that has weak references to the keys. + + This is a type-safe version of :obj:`weakref.WeakKeyDictionary`. + """ + + +class WeakValueDictionary(weakref.WeakValueDictionary, typing.MutableMapping[K, V]): + """A dictionary that has weak references to the values. + + This is a type-safe version of :obj:`weakref.WeakValueDictionary`. + """ diff --git a/tests/hikari/core/test_dispatcher.py b/tests/hikari/core/test_dispatcher.py index b6fe6f412c..80bf151b74 100644 --- a/tests/hikari/core/test_dispatcher.py +++ b/tests/hikari/core/test_dispatcher.py @@ -38,118 +38,172 @@ class TestEvent3(events.HikariEvent): ... -class TestEventDispatcher: - EXCEPTION_EVENT = "exception" - +class TestEventDispatcherImpl: @pytest.fixture - def delegate(self): - return _helpers.unslot_class(dispatcher.EventDispatcher)() + def dispatcher_inst(self): + return _helpers.unslot_class(dispatcher.EventDispatcherImpl)() # noinspection PyTypeChecker @_helpers.assert_raises(type_=TypeError) - def test_add_not_coroutine_function(self, delegate): - delegate.add("foo", lambda: None) - - def test_add_coroutine_function_when_no_others_with_name(self, delegate): + def test_add_not_coroutine_function(self, dispatcher_inst): + dispatcher_inst.add_listener("foo", lambda: None) + + def _coro_fn(self, lambda_ex): + async def wrap(*args, **kwargs): + return lambda_ex(*args, **kwargs) + + return wrap + + def test_close(self, dispatcher_inst, event_loop): + futures = [] + + def fut(): + futures.append(event_loop.create_future()) + return futures[-1] + + test_event_1_waiters = { + fut(): lambda _: False, + fut(): lambda _: False, + fut(): lambda _: False, + fut(): lambda _: False + } + + test_event_2_waiters = { + fut(): lambda _: False, + fut(): lambda _: False, + fut(): lambda _: False, + fut(): lambda _: False + } + + test_event_1_listeners = [ + self._coro_fn(lambda xxx: None) + ] + + test_event_2_listeners = [ + self._coro_fn(lambda xxx: None), + self._coro_fn(lambda xxx: None), + self._coro_fn(lambda xxx: None), + self._coro_fn(lambda xxx: None) + ] + + dispatcher_inst._waiters = (waiters := { + TestEvent1: test_event_1_waiters, + TestEvent2: test_event_2_waiters, + }) + + dispatcher_inst._listeners = (listeners := { + TestEvent1: test_event_1_listeners, + TestEvent2: test_event_2_listeners, + }) + + dispatcher_inst.close() + + assert not waiters + assert not listeners + + for i, f in enumerate(futures): + assert f.cancelled(), str(i) + + def test_add_coroutine_function_when_no_others_with_name(self, dispatcher_inst): async def coro_fn(): pass - delegate.add("foo", coro_fn) - assert coro_fn in delegate._listeners["foo"] + dispatcher_inst.add_listener("foo", coro_fn) + assert coro_fn in dispatcher_inst._listeners["foo"] - def test_add_coroutine_function_when_list_exists(self, delegate): + def test_add_coroutine_function_when_list_exists(self, dispatcher_inst): async def coro_fn1(): pass async def coro_fn2(): pass - delegate.add("foo", coro_fn1) - delegate.add("foo", coro_fn2) - assert coro_fn1 in delegate._listeners["foo"] - assert coro_fn2 in delegate._listeners["foo"] + dispatcher_inst.add_listener("foo", coro_fn1) + dispatcher_inst.add_listener("foo", coro_fn2) + assert coro_fn1 in dispatcher_inst._listeners["foo"] + assert coro_fn2 in dispatcher_inst._listeners["foo"] - def test_remove_non_existing_mux_list(self, delegate): + def test_remove_non_existing_mux_list(self, dispatcher_inst): async def remove_this(): pass # should not raise - delegate.remove("foo", remove_this) + dispatcher_inst.remove_listener("foo", remove_this) - def test_remove_non_existing_mux(self, delegate): - delegate._listeners["foo"] = [] + def test_remove_non_existing_mux(self, dispatcher_inst): + dispatcher_inst._listeners["foo"] = [] async def remove_this(): pass # should not raise - delegate.remove("foo", remove_this) + dispatcher_inst.remove_listener("foo", remove_this) - def test_remove_when_list_left_empty_removes_key(self, delegate): + def test_remove_when_list_left_empty_removes_key(self, dispatcher_inst): async def remove_this(): pass - delegate._listeners["foo"] = [remove_this] + dispatcher_inst._listeners["foo"] = [remove_this] - delegate.remove("foo", remove_this) + dispatcher_inst.remove_listener("foo", remove_this) - assert "foo" not in delegate._listeners + assert "foo" not in dispatcher_inst._listeners - def test_remove_when_list_not_left_empty_removes_coroutine_function(self, delegate): + def test_remove_when_list_not_left_empty_removes_coroutine_function(self, dispatcher_inst): async def remove_this(): pass - delegate._listeners["foo"] = [remove_this, remove_this] + dispatcher_inst._listeners["foo"] = [remove_this, remove_this] - delegate.remove("foo", remove_this) + dispatcher_inst.remove_listener("foo", remove_this) - assert delegate._listeners["foo"] == [remove_this] + assert dispatcher_inst._listeners["foo"] == [remove_this] - def test_dispatch_to_existing_muxes(self, delegate): - delegate._catch = mock.MagicMock() + def test_dispatch_to_existing_muxes(self, dispatcher_inst): + dispatcher_inst._catch = mock.MagicMock() mock_coro_fn1 = mock.MagicMock() mock_coro_fn2 = mock.MagicMock() mock_coro_fn3 = mock.MagicMock() ctx = TestEvent1() - delegate._listeners[TestEvent1] = [mock_coro_fn1, mock_coro_fn2] - delegate._listeners[TestEvent2] = [mock_coro_fn3] + dispatcher_inst._listeners[TestEvent1] = [mock_coro_fn1, mock_coro_fn2] + dispatcher_inst._listeners[TestEvent2] = [mock_coro_fn3] with mock.patch("asyncio.gather") as gather: - delegate.dispatch(ctx) - gather.assert_called_once_with(delegate._catch(mock_coro_fn1, ctx), delegate._catch(mock_coro_fn2, ctx)) + dispatcher_inst.dispatch_event(ctx) + gather.assert_called_once_with(dispatcher_inst._catch(mock_coro_fn1, ctx), dispatcher_inst._catch(mock_coro_fn2, ctx)) - def test_dispatch_to_non_existant_muxes(self, delegate): + def test_dispatch_to_non_existant_muxes(self, dispatcher_inst): # Should not throw. - delegate._waiters = {} - delegate._listeners = {} - delegate.dispatch(TestEvent1()) - assert delegate._waiters == {} - assert delegate._listeners == {} + dispatcher_inst._waiters = {} + dispatcher_inst._listeners = {} + dispatcher_inst.dispatch_event(TestEvent1()) + assert dispatcher_inst._waiters == {} + assert dispatcher_inst._listeners == {} @pytest.mark.asyncio - async def test_dispatch_is_awaitable_if_nothing_is_invoked(self, delegate): + async def test_dispatch_is_awaitable_if_nothing_is_invoked(self, dispatcher_inst): coro_fn = mock.AsyncMock() - delegate.add("foo", coro_fn) - await delegate.dispatch("bar") + dispatcher_inst.add_listener("foo", coro_fn) + await dispatcher_inst.dispatch_event("bar") @pytest.mark.asyncio - async def test_dispatch_is_awaitable_if_something_is_invoked(self, delegate): + async def test_dispatch_is_awaitable_if_something_is_invoked(self, dispatcher_inst): coro_fn = mock.AsyncMock() - delegate.add("foo", coro_fn) - await delegate.dispatch("foo") + dispatcher_inst.add_listener("foo", coro_fn) + await dispatcher_inst.dispatch_event("foo") @pytest.mark.asyncio @_helpers.timeout_after(1) - async def test_dispatch_invokes_future_waker_if_registered_with_futures(self, delegate, event_loop): - delegate._waiters[TestEvent1] = {event_loop.create_future(): lambda _: False} + async def test_dispatch_invokes_future_waker_if_registered_with_futures(self, dispatcher_inst, event_loop): + dispatcher_inst._waiters[TestEvent1] = {event_loop.create_future(): lambda _: False} @pytest.mark.asyncio @_helpers.timeout_after(1) - async def test_dispatch_returns_exception_to_caller(self, delegate, event_loop): + async def test_dispatch_returns_exception_to_caller(self, dispatcher_inst, event_loop): predicate1 = mock.MagicMock(side_effect=RuntimeError()) predicate2 = mock.MagicMock(return_value=False) @@ -158,11 +212,11 @@ async def test_dispatch_returns_exception_to_caller(self, delegate, event_loop): ctx = TestEvent3() - delegate._waiters[TestEvent3] = {} - delegate._waiters[TestEvent3][future1] = predicate1 - delegate._waiters[TestEvent3][future2] = predicate2 + dispatcher_inst._waiters[TestEvent3] = {} + dispatcher_inst._waiters[TestEvent3][future1] = predicate1 + dispatcher_inst._waiters[TestEvent3][future2] = predicate2 - await delegate.dispatch(ctx) + await dispatcher_inst.dispatch_event(ctx) try: await future1 @@ -179,28 +233,28 @@ async def test_dispatch_returns_exception_to_caller(self, delegate, event_loop): @pytest.mark.asyncio @_helpers.timeout_after(1) - async def test_waiter_map_deleted_if_made_empty_during_this_dispatch(self, delegate): - delegate._waiters[TestEvent1] = {mock.MagicMock(): mock.MagicMock(return_value=True)} - delegate.dispatch(TestEvent1()) + async def test_waiter_map_deleted_if_made_empty_during_this_dispatch(self, dispatcher_inst): + dispatcher_inst._waiters[TestEvent1] = {mock.MagicMock(): mock.MagicMock(return_value=True)} + dispatcher_inst.dispatch_event(TestEvent1()) await asyncio.sleep(0.1) - assert TestEvent1 not in delegate._waiters + assert TestEvent1 not in dispatcher_inst._waiters @pytest.mark.asyncio @_helpers.timeout_after(1) - async def test_waiter_map_not_deleted_if_not_empty(self, delegate): - delegate._waiters["foobar"] = {mock.MagicMock(): mock.MagicMock(return_value=False)} - delegate.dispatch("foobar") + async def test_waiter_map_not_deleted_if_not_empty(self, dispatcher_inst): + dispatcher_inst._waiters[TestEvent1] = {mock.MagicMock(): mock.MagicMock(return_value=False)} + dispatcher_inst.dispatch_event(TestEvent1()) await asyncio.sleep(0.1) - assert "foobar" in delegate._waiters + assert TestEvent1 in dispatcher_inst._waiters @pytest.mark.asyncio @_helpers.timeout_after(2) - async def test_wait_for_returns_event(self, delegate): + async def test_wait_for_returns_event(self, dispatcher_inst): predicate = mock.MagicMock(return_value=True) - future = delegate.wait_for(TestEvent1, timeout=5, predicate=predicate) + future = dispatcher_inst.wait_for(TestEvent1, timeout=5, predicate=predicate) ctx = TestEvent1() - await delegate.dispatch(ctx) + await dispatcher_inst.dispatch_event(ctx) await asyncio.sleep(0.1) @@ -211,11 +265,11 @@ async def test_wait_for_returns_event(self, delegate): @pytest.mark.asyncio @_helpers.timeout_after(2) - async def test_wait_for_returns_matching_event_args_when_invoked_but_no_predicate_match(self, delegate): + async def test_wait_for_returns_matching_event_args_when_invoked_but_no_predicate_match(self, dispatcher_inst): predicate = mock.MagicMock(return_value=False) ctx = TestEvent3() - future = delegate.wait_for(TestEvent3, timeout=5, predicate=predicate) - await delegate.dispatch(ctx) + future = dispatcher_inst.wait_for(TestEvent3, timeout=5, predicate=predicate) + await dispatcher_inst.dispatch_event(ctx) await asyncio.sleep(0.1) @@ -224,19 +278,19 @@ async def test_wait_for_returns_matching_event_args_when_invoked_but_no_predicat @pytest.mark.asyncio @_helpers.assert_raises(type_=asyncio.TimeoutError) - async def test_wait_for_hits_timeout_and_raises(self, delegate): + async def test_wait_for_hits_timeout_and_raises(self, dispatcher_inst): predicate = mock.MagicMock(return_value=False) - await delegate.wait_for("foobar", timeout=1, predicate=predicate) + await dispatcher_inst.wait_for("foobar", timeout=1, predicate=predicate) assert False, "event was marked as succeeded when it shouldn't have been" @pytest.mark.asyncio @_helpers.timeout_after(2) @_helpers.assert_raises(type_=RuntimeError) - async def test_wait_for_raises_predicate_errors(self, delegate): + async def test_wait_for_raises_predicate_errors(self, dispatcher_inst): predicate = mock.MagicMock(side_effect=RuntimeError) ctx = TestEvent1() - future = delegate.wait_for(TestEvent1, timeout=1, predicate=predicate) - await delegate.dispatch(ctx) + future = dispatcher_inst.wait_for(TestEvent1, timeout=1, predicate=predicate) + await dispatcher_inst.dispatch_event(ctx) await future @pytest.mark.asyncio @@ -244,45 +298,45 @@ async def test_wait_for_raises_predicate_errors(self, delegate): @_helpers.timeout_after(5) @_helpers.assert_raises(type_=asyncio.TimeoutError) async def test_other_events_in_same_waiter_event_name_do_not_awaken_us( - self, delegate, predicate_side_effect, event_loop + self, dispatcher_inst, predicate_side_effect, event_loop ): - delegate._waiters["foobar"] = {event_loop.create_future(): mock.MagicMock(side_effect=predicate_side_effect)} + dispatcher_inst._waiters["foobar"] = {event_loop.create_future(): mock.MagicMock(side_effect=predicate_side_effect)} - future = delegate.wait_for("foobar", timeout=1, predicate=mock.MagicMock(return_value=False)) + future = dispatcher_inst.wait_for("foobar", timeout=1, predicate=mock.MagicMock(return_value=False)) - await asyncio.gather(future, *(delegate.dispatch("foobar") for _ in range(5))) + await asyncio.gather(future, *(dispatcher_inst.dispatch_event("foobar") for _ in range(5))) @pytest.mark.asyncio - async def test_catch_happy_path(self, delegate): + async def test_catch_happy_path(self, dispatcher_inst): callback = mock.AsyncMock() event = TestEvent1() - delegate.handle_exception = mock.MagicMock() - await delegate._catch(callback, event) + dispatcher_inst.handle_exception = mock.MagicMock() + await dispatcher_inst._catch(callback, event) callback.assert_awaited_once_with(event) - delegate.handle_exception.assert_not_called() + dispatcher_inst.handle_exception.assert_not_called() @pytest.mark.asyncio - async def test_catch_sad_path(self, delegate): + async def test_catch_sad_path(self, dispatcher_inst): ex = RuntimeError() callback = mock.AsyncMock(side_effect=ex) - delegate.handle_exception = mock.MagicMock() + dispatcher_inst.handle_exception = mock.MagicMock() ctx = TestEvent3() - await delegate._catch(callback, ctx) - delegate.handle_exception.assert_called_once_with(ex, ctx, callback) + await dispatcher_inst._catch(callback, ctx) + dispatcher_inst.handle_exception.assert_called_once_with(ex, ctx, callback) - def test_handle_exception_dispatches_exception_event_with_context(self, delegate): - delegate.dispatch = mock.MagicMock() + def test_handle_exception_dispatches_exception_event_with_context(self, dispatcher_inst): + dispatcher_inst.dispatch_event = mock.MagicMock() ex = RuntimeError() event = TestEvent1() callback = mock.AsyncMock() - delegate.handle_exception(ex, event, callback) + dispatcher_inst.handle_exception(ex, event, callback) expected_ctx = events.ExceptionEvent(ex, event, callback) - delegate.dispatch.assert_called_once_with(expected_ctx) + dispatcher_inst.dispatch_event.assert_called_once_with(expected_ctx) - def test_handle_exception_will_not_recursively_invoke_exception_handler_event(self, delegate): - delegate.dispatch = mock.MagicMock() - delegate.handle_exception(RuntimeError(), events.ExceptionEvent(..., ..., ...), mock.AsyncMock()) - delegate.dispatch.assert_not_called() + def test_handle_exception_will_not_recursively_invoke_exception_handler_event(self, dispatcher_inst): + dispatcher_inst.dispatch_event = mock.MagicMock() + dispatcher_inst.handle_exception(RuntimeError(), events.ExceptionEvent(..., ..., ...), mock.AsyncMock()) + dispatcher_inst.dispatch_event.assert_not_called() From 29e46d99f2f692b832a89c06f7b4d9d319ad08a5 Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Wed, 25 Mar 2020 15:23:00 +0000 Subject: [PATCH 041/922] Added stuff to automatically bootstrap and convert raw events into the formatted payloads. --- hikari/core/dispatcher.py | 25 +---- hikari/core/events.py | 88 +++++++++++++---- hikari/core/gateway_client.py | 126 ++++++++++++++++++------ hikari/core/state.py | 27 ++++- hikari/internal_utilities/containers.py | 8 +- hikari/net/shard.py | 9 +- tests/hikari/core/test_dispatcher.py | 32 +++--- 7 files changed, 224 insertions(+), 91 deletions(-) diff --git a/hikari/core/dispatcher.py b/hikari/core/dispatcher.py index cc76bdb670..88dc9cc741 100644 --- a/hikari/core/dispatcher.py +++ b/hikari/core/dispatcher.py @@ -45,16 +45,13 @@ class EventDispatcher(abc.ABC): a distributed bot dispatcher, for example, or could handle dispatching to a set of micro-interpreter instances to achieve greater concurrency. """ + @abc.abstractmethod def close(self): """Cancel anything that is waiting for an event to be dispatched.""" @abc.abstractmethod - def add_listener( - self, - event_type: typing.Type[EventT], - callback: EventCallbackT - ) -> EventCallbackT: + def add_listener(self, event_type: typing.Type[EventT], callback: EventCallbackT) -> EventCallbackT: """Register a new event callback to a given event name. Parameters @@ -71,11 +68,7 @@ def add_listener( """ @abc.abstractmethod - def remove_listener( - self, - event_type: typing.Type[EventT], - callback: EventCallbackT, - ) -> EventCallbackT: + def remove_listener(self, event_type: typing.Type[EventT], callback: EventCallbackT,) -> EventCallbackT: """Remove the given coroutine function from the handlers for the given event. The name is mandatory to enable supporting registering the same event callback for multiple event types. @@ -90,11 +83,7 @@ def remove_listener( @abc.abstractmethod def wait_for( - self, - event_type: typing.Type[EventT], - *, - timeout: typing.Optional[float], - predicate: PredicateT + self, event_type: typing.Type[EventT], *, timeout: typing.Optional[float], predicate: PredicateT ) -> asyncio.Future: """Wait for the given event type to occur. @@ -338,11 +327,7 @@ def handle_exception( ) def wait_for( - self, - event_type: typing.Type[EventT], - *, - timeout: typing.Optional[float], - predicate: PredicateT, + self, event_type: typing.Type[EventT], *, timeout: typing.Optional[float], predicate: PredicateT, ) -> asyncio.Future: """Given an event name, wait for the event to occur once, then return the arguments that accompanied the event as the result. diff --git a/hikari/core/events.py b/hikari/core/events.py index c14cc8ec30..03ee9155a4 100644 --- a/hikari/core/events.py +++ b/hikari/core/events.py @@ -63,10 +63,11 @@ "UserUpdateEvent", "VoiceStateUpdateEvent", "VoiceServerUpdateEvent", - "WebhookUpdate", + "WebhookUpdateEvent", ] import datetime +import re import typing import attr @@ -83,6 +84,7 @@ from hikari.core import users from hikari.core import voices from hikari.internal_utilities import aio +from hikari.internal_utilities import assertions from hikari.internal_utilities import dates from hikari.internal_utilities import marshaller @@ -90,7 +92,7 @@ # Base event, is not deserialized -@attr.s(slots=True, auto_attribs=True) +@marshaller.attrs(slots=True) class HikariEvent(entities.HikariEntity): """The base class that all events inherit from.""" @@ -117,41 +119,54 @@ class ExceptionEvent(HikariEvent): # Synthetic event, is not deserialized -@attr.s(slots=True, auto_attribs=True) -class ConnectedEvent(HikariEvent): +@attr.attrs(slots=True, auto_attribs=True) +class StartedEvent(HikariEvent): ... # Synthetic event, is not deserialized -@attr.s(slots=True, auto_attribs=True) -class DisconnectedEvent(HikariEvent): +@attr.attrs(slots=True, auto_attribs=True) +class StoppingEvent(HikariEvent): ... # Synthetic event, is not deserialized -@attr.s(slots=True, auto_attribs=True) -class ReconnectedEvent(HikariEvent): +@attr.attrs(slots=True, auto_attribs=True) +class StoppedEvent(HikariEvent): ... -# Synthetic event, is not deserialized -@attr.s(slots=True, auto_attribs=True) -class StartedEvent(HikariEvent): +_websocket_name_break = re.compile(r"(?<=[a-z])(?=[A-Z])") + + +def mark_as_websocket_event(cls): + name = cls.__name__ + assertions.assert_that(name.endswith("Event"), "expected name to be Event") + name = name[: -len("Event")] + raw_name = _websocket_name_break.sub("_", name).upper() + cls.___raw_ws_event_name___ = raw_name + return cls + + +@mark_as_websocket_event +@marshaller.attrs(slots=True) +class ConnectedEvent(HikariEvent, entities.Deserializable): ... -# Synthetic event, is not deserialized -@attr.s(slots=True, auto_attribs=True) -class StoppingEvent(HikariEvent): +@mark_as_websocket_event +@marshaller.attrs(slots=True) +class DisconnectedEvent(HikariEvent, entities.Deserializable): ... -# Synthetic event, is not deserialized -@attr.s(slots=True, auto_attribs=True) -class StoppedEvent(HikariEvent): +@mark_as_websocket_event +@marshaller.attrs(slots=True) +class ReconnectedEvent(HikariEvent, entities.Deserializable): ... +@mark_as_websocket_event @marshaller.attrs(slots=True) class ReadyEvent(HikariEvent, entities.Deserializable): """Used to represent the gateway ready event, received when identifying @@ -204,6 +219,7 @@ def shard_count(self) -> typing.Optional[int]: return self._shard_information and self._shard_information[1] or None +@mark_as_websocket_event @marshaller.attrs(slots=True) class ResumedEvent(HikariEvent): """Represents a gateway Resume event.""" @@ -324,6 +340,7 @@ class BaseChannelEvent(HikariEvent, snowflakes.UniqueEntity, entities.Deserializ ) +@mark_as_websocket_event @marshaller.attrs(slots=True) class ChannelCreateEvent(BaseChannelEvent): """Represents Channel Create gateway events. @@ -333,16 +350,19 @@ class ChannelCreateEvent(BaseChannelEvent): """ +@mark_as_websocket_event @marshaller.attrs(slots=True) class ChannelUpdateEvent(BaseChannelEvent): """Represents Channel Update gateway events.""" +@mark_as_websocket_event @marshaller.attrs(slots=True) class ChannelDeleteEvent(BaseChannelEvent): """Represents Channel Delete gateway events.""" +@mark_as_websocket_event @marshaller.attrs(slots=True) class ChannelPinUpdateEvent(HikariEvent, entities.Deserializable): """Used to represent the Channel Pins Update gateway event. @@ -372,6 +392,7 @@ class ChannelPinUpdateEvent(HikariEvent, entities.Deserializable): ) +@mark_as_websocket_event @marshaller.attrs(slots=True) class GuildCreateEvent(HikariEvent, guilds.Guild): """Used to represent Guild Create gateway events. @@ -381,11 +402,13 @@ class GuildCreateEvent(HikariEvent, guilds.Guild): """ +@mark_as_websocket_event @marshaller.attrs(slots=True) class GuildUpdateEvent(HikariEvent, guilds.Guild): """Used to represent Guild Update gateway events.""" +@mark_as_websocket_event @marshaller.attrs(slots=True) class GuildLeaveEvent(HikariEvent, snowflakes.UniqueEntity, entities.Deserializable): """Fired when the current user leaves the guild or is kicked/banned from it. @@ -396,6 +419,7 @@ class GuildLeaveEvent(HikariEvent, snowflakes.UniqueEntity, entities.Deserializa """ +@mark_as_websocket_event @marshaller.attrs(slots=True) class GuildUnavailableEvent(HikariEvent, snowflakes.UniqueEntity, entities.Deserializable): """Fired when a guild becomes temporarily unavailable due to an outage. @@ -421,16 +445,19 @@ class BaseGuildBanEvent(HikariEvent, entities.Deserializable): user: users.User = marshaller.attrib(deserializer=users.User.deserialize) +@mark_as_websocket_event @marshaller.attrs(slots=True) class GuildBanAddEvent(BaseGuildBanEvent): """Used to represent a Guild Ban Add gateway event.""" +@mark_as_websocket_event @marshaller.attrs(slots=True) class GuildBanRemoveEvent(BaseGuildBanEvent): """Used to represent a Guild Ban Remove gateway event.""" +@mark_as_websocket_event @marshaller.attrs(slots=True) class GuildEmojisUpdateEvent(HikariEvent, entities.Deserializable): """Represents a Guild Emoji Update gateway event.""" @@ -448,6 +475,7 @@ class GuildEmojisUpdateEvent(HikariEvent, entities.Deserializable): ) +@mark_as_websocket_event @marshaller.attrs(slots=True) class GuildIntegrationsUpdateEvent(HikariEvent, entities.Deserializable): """Used to represent Guild Integration Update gateway events.""" @@ -458,6 +486,7 @@ class GuildIntegrationsUpdateEvent(HikariEvent, entities.Deserializable): guild_id: snowflakes.Snowflake = marshaller.attrib(deserializer=snowflakes.Snowflake.deserialize) +@mark_as_websocket_event @marshaller.attrs(slots=True) class GuildMemberAddEvent(HikariEvent, guilds.GuildMember): """Used to represent a Guild Member Add gateway event.""" @@ -468,6 +497,7 @@ class GuildMemberAddEvent(HikariEvent, guilds.GuildMember): guild_id: snowflakes.Snowflake = marshaller.attrib(deserializer=snowflakes.Snowflake.deserialize) +@mark_as_websocket_event @marshaller.attrs(slots=True) class GuildMemberRemoveEvent(HikariEvent, entities.Deserializable): """Used to represent Guild Member Remove gateway events. @@ -485,6 +515,7 @@ class GuildMemberRemoveEvent(HikariEvent, entities.Deserializable): user: users.User = marshaller.attrib(deserializer=users.User.deserialize) +@mark_as_websocket_event @marshaller.attrs(slots=True) class GuildMemberUpdateEvent(HikariEvent, entities.Deserializable): """Used to represent a Guild Member Update gateway event. @@ -525,6 +556,7 @@ class GuildMemberUpdateEvent(HikariEvent, entities.Deserializable): ) +@mark_as_websocket_event @marshaller.attrs(slots=True) class GuildRoleCreateEvent(HikariEvent, entities.Deserializable): """Used to represent a Guild Role Create gateway event.""" @@ -540,6 +572,7 @@ class GuildRoleCreateEvent(HikariEvent, entities.Deserializable): role: guilds.GuildRole = marshaller.attrib(deserializer=guilds.GuildRole.deserialize) +@mark_as_websocket_event @marshaller.attrs(slots=True) class GuildRoleUpdateEvent(HikariEvent, entities.Deserializable): """Used to represent a Guild Role Create gateway event.""" @@ -555,6 +588,7 @@ class GuildRoleUpdateEvent(HikariEvent, entities.Deserializable): role: guilds.GuildRole = marshaller.attrib(deserializer=guilds.GuildRole.deserialize) +@mark_as_websocket_event @marshaller.attrs(slots=True) class GuildRoleDeleteEvent(HikariEvent, entities.Deserializable): """Represents a gateway Guild Role Delete Event.""" @@ -570,9 +604,10 @@ class GuildRoleDeleteEvent(HikariEvent, entities.Deserializable): role_id: snowflakes.Snowflake = marshaller.attrib(deserializer=snowflakes.Snowflake.deserialize) +@mark_as_websocket_event @marshaller.attrs(slots=True) class InviteCreateEvent(HikariEvent, entities.Deserializable): - """""" + """Represents a gateway Invite Create event.""" #: The ID of the channel this invite targets. #: @@ -637,6 +672,7 @@ class InviteCreateEvent(HikariEvent, entities.Deserializable): uses: int = marshaller.attrib(deserializer=int) +@mark_as_websocket_event @marshaller.attrs(slots=True) class InviteDeleteEvent(HikariEvent, entities.Deserializable): """Used to represent Invite Delete gateway events. @@ -662,12 +698,14 @@ class InviteDeleteEvent(HikariEvent, entities.Deserializable): ) +@mark_as_websocket_event @marshaller.attrs(slots=True) class MessageCreateEvent(HikariEvent, messages.Message): """Used to represent Message Create gateway events.""" # This is an arbitrarily partial version of `messages.Message` +@mark_as_websocket_event @marshaller.attrs(slots=True) class MessageUpdateEvent(HikariEvent, snowflakes.UniqueEntity, entities.Deserializable): """ @@ -841,6 +879,7 @@ class MessageUpdateEvent(HikariEvent, snowflakes.UniqueEntity, entities.Deserial ) +@mark_as_websocket_event @marshaller.attrs(slots=True) class MessageDeleteEvent(HikariEvent, entities.Deserializable): """Used to represent Message Delete gateway events. @@ -865,6 +904,7 @@ class MessageDeleteEvent(HikariEvent, entities.Deserializable): message_id: snowflakes.Snowflake = marshaller.attrib(raw_name="id", deserializer=snowflakes.Snowflake.deserialize) +@mark_as_websocket_event @marshaller.attrs(slots=True) class MessageDeleteBulkEvent(HikariEvent, entities.Deserializable): """Used to represent Message Bulk Delete gateway events. @@ -892,6 +932,7 @@ class MessageDeleteBulkEvent(HikariEvent, entities.Deserializable): ) +@mark_as_websocket_event @marshaller.attrs(slots=True) class MessageReactionAddEvent(HikariEvent, entities.Deserializable): """Used to represent Message Reaction Add gateway events.""" @@ -935,6 +976,7 @@ class MessageReactionAddEvent(HikariEvent, entities.Deserializable): ) +@mark_as_websocket_event @marshaller.attrs(slots=True) class MessageReactionRemoveEvent(HikariEvent, entities.Deserializable): """Used to represent Message Reaction Remove gateway events.""" @@ -970,6 +1012,7 @@ class MessageReactionRemoveEvent(HikariEvent, entities.Deserializable): ) +@mark_as_websocket_event @marshaller.attrs(slots=True) class MessageReactionRemoveAllEvent(HikariEvent, entities.Deserializable): """Used to represent Message Reaction Remove All gateway events. @@ -994,6 +1037,7 @@ class MessageReactionRemoveAllEvent(HikariEvent, entities.Deserializable): ) +@mark_as_websocket_event @marshaller.attrs(slots=True) class MessageReactionRemoveEmojiEvent(HikariEvent, entities.Deserializable): """Represents Message Reaction Remove Emoji events. @@ -1025,6 +1069,7 @@ class MessageReactionRemoveEmojiEvent(HikariEvent, entities.Deserializable): ) +@mark_as_websocket_event @marshaller.attrs(slots=True) class PresenceUpdateEvent(HikariEvent, guilds.GuildMemberPresence): """Used to represent Presence Update gateway events. @@ -1032,6 +1077,7 @@ class PresenceUpdateEvent(HikariEvent, guilds.GuildMemberPresence): """ +@mark_as_websocket_event @marshaller.attrs(slots=True) class TypingStartEvent(HikariEvent, entities.Deserializable): """Used to represent typing start gateway events. @@ -1072,6 +1118,7 @@ class TypingStartEvent(HikariEvent, entities.Deserializable): ) +@mark_as_websocket_event @marshaller.attrs(slots=True) class UserUpdateEvent(HikariEvent, users.MyUser): """Used to represent User Update gateway events. @@ -1079,6 +1126,7 @@ class UserUpdateEvent(HikariEvent, users.MyUser): """ +@mark_as_websocket_event @marshaller.attrs(slots=True) class VoiceStateUpdateEvent(HikariEvent, voices.VoiceState): """Used to represent voice state update gateway events. @@ -1086,6 +1134,7 @@ class VoiceStateUpdateEvent(HikariEvent, voices.VoiceState): """ +@mark_as_websocket_event @marshaller.attrs(slots=True) class VoiceServerUpdateEvent(HikariEvent, entities.Deserializable): """Used to represent voice server update gateway events. @@ -1109,8 +1158,9 @@ class VoiceServerUpdateEvent(HikariEvent, entities.Deserializable): endpoint: str = marshaller.attrib(deserializer=str) +@mark_as_websocket_event @marshaller.attrs(slots=True) -class WebhookUpdate(HikariEvent, entities.Deserializable): +class WebhookUpdateEvent(HikariEvent, entities.Deserializable): """Used to represent webhook update gateway events. Sent when a webhook is updated, created or deleted in a guild. """ diff --git a/hikari/core/gateway_client.py b/hikari/core/gateway_client.py index a3195d0b62..b1af884118 100644 --- a/hikari/core/gateway_client.py +++ b/hikari/core/gateway_client.py @@ -23,6 +23,7 @@ import contextlib import datetime import enum +import inspect import logging import signal import time @@ -34,6 +35,7 @@ from hikari.core import events from hikari.core import gateway_config from hikari.core import gateway_entities +from hikari.core import state from hikari.internal_utilities import aio from hikari.internal_utilities import loggers from hikari.net import errors @@ -54,7 +56,7 @@ class ShardState(enum.IntEnum): STOPPED = enum.auto() -class Startable(abc.ABC): +class BaseClient(abc.ABC): """Base for any socket-based communication medium to provide functionality for more automated control given certain method constraints. """ @@ -87,10 +89,9 @@ def sigterm_handler(*_): loop.add_signal_handler(signal.SIGTERM, sigterm_handler) loop.run_until_complete(self.start()) - try: - loop.run_until_complete(self.join()) - except errors.GatewayClientClosedError: - self.logger.info("client has shut down") + loop.run_until_complete(self.join()) + + self.logger.info("client has shut down") except KeyboardInterrupt as _ex: self.logger.info("received signal to shut down client") @@ -108,7 +109,7 @@ def sigterm_handler(*_): raise ex from ex -class ShardClient(Startable): +class ShardClient(BaseClient): """The primary interface for a single shard connection. This contains several abstractions to enable usage of the low level gateway network interface with the higher level constructs in :mod:`hikari.core`. @@ -133,9 +134,9 @@ class ShardClient(Startable): ----- Generally, you want to use :class:`GatewayClient` rather than this class directly, as that will handle sharding where enabled and applicable, and - provides a few more bits and pieces that may be useful. If you want - to customize this, you can subclass it and simply override anything you - want. + provides a few more bits and pieces that may be useful such as state + management and event dispatcher integration. and If you want to customize + this, you can subclass it and simply override anything you want. """ __slots__ = ( @@ -169,7 +170,7 @@ def __init__( compression=config.use_compression, connector=config.protocol.connector if config.protocol is not None else None, debug=config.debug, - dispatch=lambda _, event_name, pl: self._dispatch(self, event_name, pl), + dispatch=self._dispatch, initial_presence=self._create_presence_pl( status=config.initial_status, activity=config.initial_activity, @@ -402,20 +403,23 @@ def _create_presence_pl( ShardT = typing.TypeVar("ShardT", bound=ShardClient) -class GatewayClient(typing.Generic[ShardT], Startable): +class GatewayClient(typing.Generic[ShardT], BaseClient): def __init__( self, config: gateway_config.GatewayConfig, url: str, + *, + dispatcher_impl: typing.Optional[dispatcher.EventDispatcher] = None, shard_type: typing.Type[ShardT] = ShardClient, - ) -> None: self.logger = loggers.get_named_logger(self) self.config = config - self.event_dispatcher = dispatcher.EventDispatcher() - self.state + self.event_dispatcher = dispatcher_impl if dispatcher_impl is not None else dispatcher.EventDispatcherImpl() + self._websocket_event_types = self._websocket_events() + self.state = state.StatefulStateManagerImpl() + self._is_running = False self.shards: typing.Dict[int, ShardT] = { - shard_id: shard_type(shard_id, config, self._low_level_dispatch, url) + shard_id: shard_type(shard_id, config, self._handle_websocket_event_later, url) for shard_id in config.shard_config.shard_ids } @@ -426,6 +430,7 @@ async def start(self) -> None: session spam. This involves starting each shard sequentially with a 5 second pause between each. """ + self._is_running = True self.logger.info("starting %s shard(s)", len(self.shards)) start_time = time.perf_counter() for i, shard_id in enumerate(self.config.shard_config.shard_ids): @@ -442,24 +447,83 @@ async def join(self) -> None: await asyncio.gather(*(shard_obj.join() for shard_obj in self.shards.values())) async def shutdown(self, wait: bool = True) -> None: - self.logger.info("stopping %s shard(s)", len(self.shards)) - start_time = time.perf_counter() + if self._is_running: + self.logger.info("stopping %s shard(s)", len(self.shards)) + start_time = time.perf_counter() + try: + await asyncio.gather(*(shard_obj.shutdown(wait) for shard_obj in self.shards.values())) + finally: + finish_time = time.perf_counter() + self.logger.info("stopped %s shard(s) in approx %.2fs", len(self.shards), finish_time - start_time) + self._is_running = False + + async def wait_for( + self, + event_type: typing.Type[dispatcher.EventT], + *, + predicate: dispatcher.PredicateT, + timeout: typing.Optional[float], + ) -> dispatcher.EventT: + """Wait for the given event type to occur. + + Parameters + ---------- + event_type : :obj:`typing.Type` [ :obj:`events.HikariEvent` ] + The name of the event to wait for. + timeout : :obj:`float`, optional + The timeout to wait for before cancelling and raising an + :obj:`asyncio.TimeoutError` instead. If this is `None`, this will + wait forever. Care must be taken if you use `None` as this may + leak memory if you do this from an event listener that gets + repeatedly called. If you want to do this, you should consider + using an event listener instead of this function. + predicate : ``def predicate(event) -> bool`` or ``async def predicate(event) -> bool`` + A function that takes the arguments for the event and returns True + if it is a match, or False if it should be ignored. + This can be a coroutine function that returns a boolean, or a + regular function. + + Returns + ------- + :obj:`asyncio.Future`: + A future to await. When the given event is matched, this will be + completed with the corresponding event body. + + If the predicate throws an exception, or the timeout is reached, + then this will be set as an exception on the returned future. + """ + return await self.event_dispatcher.wait_for(event_type, predicate=predicate, timeout=timeout) + + def _handle_websocket_event_later(self, conn: shard.ShardConnection, event_name: str, payload: typing.Any) -> None: + # Run this asynchronously so that we can allow awaiting stuff like state management. + asyncio.get_event_loop().create_task(self._handle_websocket_event(conn, event_name, payload)) + + async def _handle_websocket_event(self, _: shard.ShardConnection, event_name: str, payload: typing.Any) -> None: try: - await asyncio.gather(*(shard_obj.shutdown(wait) for shard_obj in self.shards.values())) - finally: - finish_time = time.perf_counter() - self.logger.info("stopped %s shard(s) in approx %.2fs", len(self.shards), finish_time - start_time) + event_type = self._websocket_event_types[event_name] + except KeyError: + pass + else: + event_payload = event_type.deserialize(payload) + await self.state.on_event(event_payload) + await self.event_dispatcher.dispatch_event(event_payload) - @typing.overload - async def wait_for(self, event: _EventT, predicate: typing.Callable[[_EventT], bool], timeout: float) -> _EventT: - ... + def _websocket_events(self): + # Look for anything that has the ___raw_ws_event_name___ class attribute + # to each corresponding class where appropriate to do so. This provides + # a quick and dirty event lookup mechanism that can be extended quickly + # and has O(k) lookup time. - @typing.overload - async def wait_for(self, event: str, predicate: typing.Callable[[_EventT], bool], timeout: float) -> _EventT: - ... + types = {} + + def predicate(member): + return inspect.isclass(member) and hasattr(member, "___raw_ws_event_name___") + + for name, cls in inspect.getmembers(events, predicate): + raw_name = cls.___raw_ws_event_name___ + types[raw_name] = cls + self.logger.debug("detected %s as a web socket event to listen for", name) - async def wait_for(self, event: ..., predicate: ..., timeout: float) -> _EventT: - raise NotImplementedError() + self.logger.debug("detected %s web socket events to register from %s", len(types), events.__name__) - def _low_level_dispatch(self, shard: ShardT, event_name: str, payload: typing.Any) -> None: - print(shard, event_name, payload) + return types diff --git a/hikari/core/state.py b/hikari/core/state.py index 4731cfcc01..40b52205e8 100644 --- a/hikari/core/state.py +++ b/hikari/core/state.py @@ -17,8 +17,31 @@ # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . """State registry and event manager.""" +__all__ = ["StateManager", "StatefulStateManagerImpl", "StatelessStateManagerImpl"] +import abc -class StateManager: - def on_event(self, shard, event_name, payload) -> None: +from hikari.core import events + + +class StateManager(abc.ABC): + """Base type for a state management implementation.""" + + @abc.abstractmethod + async def on_event(self, event_obj: events.HikariEvent) -> None: + ... + + +class StatelessStateManagerImpl(StateManager): + """Stubbed stateless event manager for implementing stateless bots.""" + + async def on_event(self, event_obj: events.HikariEvent) -> None: + pass + + +class StatefulStateManagerImpl(StateManager): + """A basic state event manager implementation.""" + + async def on_event(self, event_obj: events.HikariEvent) -> None: + # TODO: implement state management pass diff --git a/hikari/internal_utilities/containers.py b/hikari/internal_utilities/containers.py index 36dd5e81f3..bd33e7b9c1 100644 --- a/hikari/internal_utilities/containers.py +++ b/hikari/internal_utilities/containers.py @@ -18,8 +18,12 @@ # along with Hikari. If not, see . """Custom data structures and constant values.""" __all__ = [ - "EMPTY_SEQUENCE", "EMPTY_SET", "EMPTY_COLLECTION", "EMPTY_DICT", "EMPTY_GENERATOR_EXPRESSION", - "WeakKeyDictionary" + "EMPTY_SEQUENCE", + "EMPTY_SET", + "EMPTY_COLLECTION", + "EMPTY_DICT", + "EMPTY_GENERATOR_EXPRESSION", + "WeakKeyDictionary", ] import types diff --git a/hikari/net/shard.py b/hikari/net/shard.py index f913a5df26..661a1ee627 100644 --- a/hikari/net/shard.py +++ b/hikari/net/shard.py @@ -634,7 +634,14 @@ async def connect(self, client_session_type=aiohttp.ClientSession) -> None: # Clear any pending exception to prevent a nasty console message. pending_task.result() - ex = completed.pop().exception() + # If the heartbeat call closes normally, then we want to get the exception + # raised by the identify call if it raises anything. This prevents spammy + # exceptions being thrown if the client shuts down during the handshake, + # which becomes more and more likely when we consider bots may have many + # shards running, each taking min of 5s to start up after the first. + ex = None + while len(completed) > 0 and ex is None: + ex = completed.pop().exception() if ex is None: # If no exception occurred, we must have exited non-exceptionally, indicating diff --git a/tests/hikari/core/test_dispatcher.py b/tests/hikari/core/test_dispatcher.py index 80bf151b74..184aa7aa69 100644 --- a/tests/hikari/core/test_dispatcher.py +++ b/tests/hikari/core/test_dispatcher.py @@ -65,36 +65,32 @@ def fut(): fut(): lambda _: False, fut(): lambda _: False, fut(): lambda _: False, - fut(): lambda _: False + fut(): lambda _: False, } test_event_2_waiters = { fut(): lambda _: False, fut(): lambda _: False, fut(): lambda _: False, - fut(): lambda _: False + fut(): lambda _: False, } - test_event_1_listeners = [ - self._coro_fn(lambda xxx: None) - ] + test_event_1_listeners = [self._coro_fn(lambda xxx: None)] test_event_2_listeners = [ self._coro_fn(lambda xxx: None), self._coro_fn(lambda xxx: None), self._coro_fn(lambda xxx: None), - self._coro_fn(lambda xxx: None) + self._coro_fn(lambda xxx: None), ] - dispatcher_inst._waiters = (waiters := { - TestEvent1: test_event_1_waiters, - TestEvent2: test_event_2_waiters, - }) + dispatcher_inst._waiters = ( + waiters := {TestEvent1: test_event_1_waiters, TestEvent2: test_event_2_waiters,} + ) - dispatcher_inst._listeners = (listeners := { - TestEvent1: test_event_1_listeners, - TestEvent2: test_event_2_listeners, - }) + dispatcher_inst._listeners = ( + listeners := {TestEvent1: test_event_1_listeners, TestEvent2: test_event_2_listeners,} + ) dispatcher_inst.close() @@ -172,7 +168,9 @@ def test_dispatch_to_existing_muxes(self, dispatcher_inst): with mock.patch("asyncio.gather") as gather: dispatcher_inst.dispatch_event(ctx) - gather.assert_called_once_with(dispatcher_inst._catch(mock_coro_fn1, ctx), dispatcher_inst._catch(mock_coro_fn2, ctx)) + gather.assert_called_once_with( + dispatcher_inst._catch(mock_coro_fn1, ctx), dispatcher_inst._catch(mock_coro_fn2, ctx) + ) def test_dispatch_to_non_existant_muxes(self, dispatcher_inst): # Should not throw. @@ -300,7 +298,9 @@ async def test_wait_for_raises_predicate_errors(self, dispatcher_inst): async def test_other_events_in_same_waiter_event_name_do_not_awaken_us( self, dispatcher_inst, predicate_side_effect, event_loop ): - dispatcher_inst._waiters["foobar"] = {event_loop.create_future(): mock.MagicMock(side_effect=predicate_side_effect)} + dispatcher_inst._waiters["foobar"] = { + event_loop.create_future(): mock.MagicMock(side_effect=predicate_side_effect) + } future = dispatcher_inst.wait_for("foobar", timeout=1, predicate=mock.MagicMock(return_value=False)) From 8a44a472e31210d21310728dd8714bf882432947 Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Wed, 25 Mar 2020 15:54:19 +0000 Subject: [PATCH 042/922] Moved shard to separate file to gateway client --- hikari/core/__init__.py | 7 + hikari/core/gateway_client.py | 378 +------------------------------- hikari/core/shard_client.py | 397 ++++++++++++++++++++++++++++++++++ 3 files changed, 409 insertions(+), 373 deletions(-) create mode 100644 hikari/core/shard_client.py diff --git a/hikari/core/__init__.py b/hikari/core/__init__.py index 3612738c3a..a94159e174 100644 --- a/hikari/core/__init__.py +++ b/hikari/core/__init__.py @@ -17,6 +17,8 @@ # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . """The core API for interacting with Discord directly.""" + +# Do I need this? It still resolves without adding these in...? from hikari.core import app_config from hikari.core import channels from hikari.core import entities @@ -31,9 +33,12 @@ from hikari.core import oauth2 from hikari.core import permissions from hikari.core import protocol_config +from hikari.core import shard_client from hikari.core import snowflakes from hikari.core import users from hikari.core import webhooks + +# Import everything into this namespace. from hikari.core.app_config import * from hikari.core.channels import * from hikari.core.colors import * @@ -52,6 +57,7 @@ from hikari.core.oauth2 import * from hikari.core.permissions import * from hikari.core.protocol_config import * +from hikari.core.shard_client import * from hikari.core.snowflakes import * from hikari.core.users import * from hikari.core.voices import * @@ -72,6 +78,7 @@ *oauth2.__all__, *permissions.__all__, *protocol_config.__all__, + *shard_client.__all__, *snowflakes.__all__, *users.__all__, *webhooks.__all__, diff --git a/hikari/core/gateway_client.py b/hikari/core/gateway_client.py index b1af884118..37b7efe71f 100644 --- a/hikari/core/gateway_client.py +++ b/hikari/core/gateway_client.py @@ -16,401 +16,33 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . -__all__ = ["ShardState", "ShardClient", "GatewayClient"] +__all__ = ["GatewayClient"] -import abc import asyncio -import contextlib -import datetime -import enum import inspect -import logging -import signal import time import typing -import aiohttp - from hikari.core import dispatcher from hikari.core import events from hikari.core import gateway_config -from hikari.core import gateway_entities +from hikari.core import shard_client from hikari.core import state -from hikari.internal_utilities import aio from hikari.internal_utilities import loggers -from hikari.net import errors -from hikari.net import ratelimits from hikari.net import shard -_EventT = typing.TypeVar("_EventT", bound=events.HikariEvent) - - -@enum.unique -class ShardState(enum.IntEnum): - NOT_RUNNING = 0 - INITIALIZING = enum.auto() - WAITING_FOR_READY = enum.auto() - READY = enum.auto() - RECONNECTING = enum.auto() - STOPPING = enum.auto() - STOPPED = enum.auto() - - -class BaseClient(abc.ABC): - """Base for any socket-based communication medium to provide functionality - for more automated control given certain method constraints. - """ - - logger: logging.Logger - - @abc.abstractmethod - async def start(self): - ... - - @abc.abstractmethod - async def shutdown(self, wait: bool = True): - ... - - @abc.abstractmethod - async def join(self): - ... - - def run(self): - loop = asyncio.get_event_loop() - - def sigterm_handler(*_): - raise KeyboardInterrupt() - - ex = None - - try: - with contextlib.suppress(NotImplementedError): - # Not implemented on Windows - loop.add_signal_handler(signal.SIGTERM, sigterm_handler) - - loop.run_until_complete(self.start()) - loop.run_until_complete(self.join()) - - self.logger.info("client has shut down") - - except KeyboardInterrupt as _ex: - self.logger.info("received signal to shut down client") - loop.run_until_complete(self.shutdown()) - # Apparently you have to alias except clauses or you get an - # UnboundLocalError. - ex = _ex - finally: - loop.run_until_complete(self.shutdown(True)) - with contextlib.suppress(NotImplementedError): - # Not implemented on Windows - loop.remove_signal_handler(signal.SIGTERM) - - if ex: - raise ex from ex - - -class ShardClient(BaseClient): - """The primary interface for a single shard connection. This contains - several abstractions to enable usage of the low level gateway network - interface with the higher level constructs in :mod:`hikari.core`. - - Parameters - ---------- - shard_id : :obj:`int` - The ID of this specific shard. - config : :obj:`gateway_config.GatewayConfig` - The gateway configuration to use to initialize this shard. - low_level_dispatch : :obj:`typing.Callable` [ [ :obj:`Shard`, :obj:`str`, :obj:`typing.Any` ] ] - A function that is fed any low-level event payloads. This will consist - of three arguments: an :obj:`Shard` which is this shard instance, - a :obj:`str` of the raw event name, and any naive raw payload that was - passed with the event. The expectation is the function passed here - will pass the payload onto any event handling and state handling system - to be transformed into a higher-level representation. - url : :obj:`str` - The URL to connect the gateway to. - - Notes - ----- - Generally, you want to use :class:`GatewayClient` rather than this class - directly, as that will handle sharding where enabled and applicable, and - provides a few more bits and pieces that may be useful such as state - management and event dispatcher integration. and If you want to customize - this, you can subclass it and simply override anything you want. - """ - - __slots__ = ( - "logger", - "_dispatch", - "_client", - "_status", - "_activity", - "_idle_since", - "_is_afk", - "_task", - "_shard_state", - ) - - def __init__( - self, - shard_id: int, - config: gateway_config.GatewayConfig, - low_level_dispatch: typing.Callable[["ShardClient", str, typing.Any], None], - url: str, - ) -> None: - self.logger = loggers.get_named_logger(self, shard_id) - self._dispatch = low_level_dispatch - self._activity = config.initial_activity - self._idle_since = config.initial_idle_since - self._is_afk = config.initial_is_afk - self._status = config.initial_status - self._shard_state = ShardState.NOT_RUNNING - self._task = None - self._client = shard.ShardConnection( - compression=config.use_compression, - connector=config.protocol.connector if config.protocol is not None else None, - debug=config.debug, - dispatch=self._dispatch, - initial_presence=self._create_presence_pl( - status=config.initial_status, - activity=config.initial_activity, - idle_since=config.initial_idle_since, - is_afk=config.initial_is_afk, - ), - intents=config.intents, - large_threshold=config.large_threshold, - proxy_auth=config.protocol.proxy_auth if config.protocol is not None else None, - proxy_headers=config.protocol.proxy_headers if config.protocol is not None else None, - proxy_url=config.protocol.proxy_url if config.protocol is not None else None, - session_id=None, - seq=None, - shard_id=shard_id, - shard_count=config.shard_config.shard_count, - ssl_context=config.protocol.ssl_context if config.protocol is not None else None, - token=config.token, - url=url, - verify_ssl=config.protocol.verify_ssl if config.protocol is not None else None, - version=config.version, - ) - - @property - def client(self) -> shard.ShardConnection: - """ - Returns - ------- - :obj:`hikari.net.gateway.GatewayClient` - The low-level gateway client used for this shard. - """ - return self._client - - #: TODO: use enum - @property - def status(self) -> str: - """ - Returns - ------- - :obj:`str` - The current user status for this shard. - """ - return self._status - - @property - def activity(self) -> typing.Optional[gateway_entities.GatewayActivity]: - """ - Returns - ------- - :obj:`hikari.core.gateway_entities.GatewayActivity`, optional - The current activity for the user on this shard, or ``None`` if - there is no activity. - """ - return self._activity - - @property - def idle_since(self) -> typing.Optional[datetime.datetime]: - """ - Returns - ------- - :obj:`datetime.datetime`, optional - The timestamp when the user of this shard appeared to be idle, or - ``None`` if not applicable. - """ - return self._idle_since - - @property - def is_afk(self) -> bool: - """ - Returns - ------- - :obj:`bool` - ``True`` if the user is AFK, ``False`` otherwise. - """ - return self._is_afk - - async def start(self): - """Connect to the gateway on this shard and schedule tasks to keep this - connection alive. Wait for the shard to dispatch a ``READY`` event, and - then return. - """ - if self._shard_state not in (ShardState.NOT_RUNNING, ShardState.STOPPED): - raise RuntimeError("Cannot start a shard twice") - - self.logger.debug("starting shard") - self._shard_state = ShardState.INITIALIZING - self._task = asyncio.create_task(self._keep_alive()) - self.logger.info("waiting for READY") - await self._client.identify_event.wait() - self._shard_state = ShardState.WAITING_FOR_READY - await self._client.ready_event.wait() - self.logger.info("now READY") - self._shard_state = ShardState.READY - - async def join(self) -> None: - """Wait for the shard to shut down fully.""" - await self._task if self._task is not None else aio.completed_future() - - async def shutdown(self, wait: bool = True) -> None: - """Request that the shard shuts down. - - Parameters - ---------- - wait : :obj:`bool` - If ``True`` (default), then wait for the client to shut down fully. - If ``False``, only send the signal to shut down, but do not wait - for it explicitly. - """ - if self._shard_state != ShardState.STOPPING: - self._shard_state = ShardState.STOPPING - self.logger.debug("stopping shard") - await self._client.close() - if wait: - await self._task - with contextlib.suppress(): - self._task.result() - - async def _keep_alive(self): - back_off = ratelimits.ExponentialBackOff(maximum=None) - last_start = time.perf_counter() - do_not_back_off = True - - while True: - try: - if not do_not_back_off and time.perf_counter() - last_start < 30: - next_backoff = next(back_off) - self.logger.info( - "restarted within 30 seconds, will backoff for %ss", next_backoff, - ) - await asyncio.sleep(next_backoff) - else: - back_off.reset() - - last_start = time.perf_counter() - - do_not_back_off = False - await self._client.connect() - self.logger.critical("shut down silently! this shouldn't happen!") - - except aiohttp.ClientConnectorError as ex: - self.logger.exception( - "failed to connect to Discord to initialize a websocket connection", exc_info=ex, - ) - except errors.GatewayZombiedError: - self.logger.warning("entered a zombie state and will be restarted") - except errors.GatewayInvalidSessionError as ex: - if ex.can_resume: - self.logger.warning("invalid session, so will attempt to resume") - else: - self.logger.warning("invalid session, so will attempt to reconnect") - - if not ex.can_resume: - self._client.seq = None - self._client.session_id = None - do_not_back_off = True - await asyncio.sleep(5) - except errors.GatewayMustReconnectError: - self.logger.warning("instructed by Discord to reconnect") - self._client.seq = None - self._client.session_id = None - do_not_back_off = True - await asyncio.sleep(5) - except errors.GatewayServerClosedConnectionError: - self.logger.warning("disconnected by Discord, will attempt to reconnect") - except errors.GatewayClientClosedError: - self.logger.warning("shutting down") - return - except Exception as ex: - self.logger.debug("propagating unexpected exception %s", exc_info=ex) - raise ex - - async def update_presence( - self, - status: str = ..., # TODO: use enum for status - activity: typing.Optional[gateway_entities.GatewayActivity] = ..., - idle_since: typing.Optional[datetime.datetime] = ..., - is_afk: bool = ..., - ) -> None: - """Update the presence of the user for the shard. - - This will only update arguments that you explicitly specify a value for. - Any arguments that you do not explicitly provide some value for will - not be changed. - - Warnings - -------- - This will fail if the shard is not online. - - Parameters - ---------- - status : :obj:`str` - The new status to set. - activity : :obj:`hikari.core.gateway_entities.GatewayActivity`, optional - The new activity to set. - idle_since : :obj:`datetime.datetime`, optional - The time to show up as being idle since, or ``None`` if not - applicable. - is_afk : :obj:`bool` - ``True`` if the user should be marked as AFK, or ``False`` - otherwise. - """ - status = self._status if status is ... else status - activity = self._activity if activity is ... else activity - idle_since = self._idle_since if idle_since is ... else idle_since - is_afk = self._is_afk if is_afk is ... else is_afk - - presence = self._create_presence_pl(status=status, activity=activity, idle_since=idle_since, is_afk=is_afk) - await self._client.update_presence(presence) - - # If we get this far, the update succeeded probably, or the gateway just died. Whatever. - self._status = status - self._activity = activity - self._idle_since = idle_since - self._is_afk = is_afk - - @staticmethod - def _create_presence_pl( - status: str, # TODO: use enum for status - activity: typing.Optional[gateway_entities.GatewayActivity], - idle_since: typing.Optional[datetime.datetime], - is_afk: bool, - ) -> typing.Dict[str, typing.Any]: - return { - "status": status, - "idle_since": idle_since.timestamp() if idle_since is not None else None, - "game": activity.serialize() if activity is not None else None, - "afk": is_afk, - } - -ShardT = typing.TypeVar("ShardT", bound=ShardClient) +ShardT = typing.TypeVar("ShardT", bound=shard_client.ShardClient) -class GatewayClient(typing.Generic[ShardT], BaseClient): +class GatewayClient(typing.Generic[ShardT], shard_client.WebsocketClientBase): def __init__( self, config: gateway_config.GatewayConfig, url: str, *, dispatcher_impl: typing.Optional[dispatcher.EventDispatcher] = None, - shard_type: typing.Type[ShardT] = ShardClient, + shard_type: typing.Type[ShardT] = shard_client.ShardClient, ) -> None: self.logger = loggers.get_named_logger(self) self.config = config diff --git a/hikari/core/shard_client.py b/hikari/core/shard_client.py new file mode 100644 index 0000000000..5a411bae87 --- /dev/null +++ b/hikari/core/shard_client.py @@ -0,0 +1,397 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# Copyright © Nekoka.tt 2019-2020 +# +# This file is part of Hikari. +# +# Hikari is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Hikari is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Hikari. If not, see . +__all__ = ["ShardState", "ShardClient"] + +import abc +import asyncio +import contextlib +import datetime +import enum +import logging +import signal +import time +import typing + +import aiohttp + +from hikari.core import events +from hikari.core import gateway_config +from hikari.core import gateway_entities +from hikari.internal_utilities import aio +from hikari.internal_utilities import loggers +from hikari.net import errors +from hikari.net import ratelimits +from hikari.net import shard + +_EventT = typing.TypeVar("_EventT", bound=events.HikariEvent) + + +@enum.unique +class ShardState(enum.IntEnum): + NOT_RUNNING = 0 + INITIALIZING = enum.auto() + WAITING_FOR_READY = enum.auto() + READY = enum.auto() + RECONNECTING = enum.auto() + STOPPING = enum.auto() + STOPPED = enum.auto() + + +class WebsocketClientBase(abc.ABC): + """Base for any socket-based communication medium to provide functionality + for more automated control given certain method constraints. + """ + + logger: logging.Logger + + @abc.abstractmethod + async def start(self): + ... + + @abc.abstractmethod + async def shutdown(self, wait: bool = True): + ... + + @abc.abstractmethod + async def join(self): + ... + + def run(self): + loop = asyncio.get_event_loop() + + def sigterm_handler(*_): + raise KeyboardInterrupt() + + ex = None + + try: + with contextlib.suppress(NotImplementedError): + # Not implemented on Windows + loop.add_signal_handler(signal.SIGTERM, sigterm_handler) + + loop.run_until_complete(self.start()) + loop.run_until_complete(self.join()) + + self.logger.info("client has shut down") + + except KeyboardInterrupt as _ex: + self.logger.info("received signal to shut down client") + loop.run_until_complete(self.shutdown()) + # Apparently you have to alias except clauses or you get an + # UnboundLocalError. + ex = _ex + finally: + loop.run_until_complete(self.shutdown(True)) + with contextlib.suppress(NotImplementedError): + # Not implemented on Windows + loop.remove_signal_handler(signal.SIGTERM) + + if ex: + raise ex from ex + + +class ShardClient(WebsocketClientBase): + """The primary interface for a single shard connection. This contains + several abstractions to enable usage of the low level gateway network + interface with the higher level constructs in :mod:`hikari.core`. + + Parameters + ---------- + shard_id : :obj:`int` + The ID of this specific shard. + config : :obj:`gateway_config.GatewayConfig` + The gateway configuration to use to initialize this shard. + low_level_dispatch : :obj:`typing.Callable` [ [ :obj:`Shard`, :obj:`str`, :obj:`typing.Any` ] ] + A function that is fed any low-level event payloads. This will consist + of three arguments: an :obj:`Shard` which is this shard instance, + a :obj:`str` of the raw event name, and any naive raw payload that was + passed with the event. The expectation is the function passed here + will pass the payload onto any event handling and state handling system + to be transformed into a higher-level representation. + url : :obj:`str` + The URL to connect the gateway to. + + Notes + ----- + Generally, you want to use :class:`GatewayClient` rather than this class + directly, as that will handle sharding where enabled and applicable, and + provides a few more bits and pieces that may be useful such as state + management and event dispatcher integration. and If you want to customize + this, you can subclass it and simply override anything you want. + """ + + __slots__ = ( + "logger", + "_dispatch", + "_client", + "_status", + "_activity", + "_idle_since", + "_is_afk", + "_task", + "_shard_state", + ) + + def __init__( + self, + shard_id: int, + config: gateway_config.GatewayConfig, + low_level_dispatch: typing.Callable[["ShardClient", str, typing.Any], None], + url: str, + ) -> None: + self.logger = loggers.get_named_logger(self, shard_id) + self._dispatch = low_level_dispatch + self._activity = config.initial_activity + self._idle_since = config.initial_idle_since + self._is_afk = config.initial_is_afk + self._status = config.initial_status + self._shard_state = ShardState.NOT_RUNNING + self._task = None + self._client = shard.ShardConnection( + compression=config.use_compression, + connector=config.protocol.connector if config.protocol is not None else None, + debug=config.debug, + dispatch=self._dispatch, + initial_presence=self._create_presence_pl( + status=config.initial_status, + activity=config.initial_activity, + idle_since=config.initial_idle_since, + is_afk=config.initial_is_afk, + ), + intents=config.intents, + large_threshold=config.large_threshold, + proxy_auth=config.protocol.proxy_auth if config.protocol is not None else None, + proxy_headers=config.protocol.proxy_headers if config.protocol is not None else None, + proxy_url=config.protocol.proxy_url if config.protocol is not None else None, + session_id=None, + seq=None, + shard_id=shard_id, + shard_count=config.shard_config.shard_count, + ssl_context=config.protocol.ssl_context if config.protocol is not None else None, + token=config.token, + url=url, + verify_ssl=config.protocol.verify_ssl if config.protocol is not None else None, + version=config.version, + ) + + @property + def client(self) -> shard.ShardConnection: + """ + Returns + ------- + :obj:`hikari.net.gateway.GatewayClient` + The low-level gateway client used for this shard. + """ + return self._client + + #: TODO: use enum + @property + def status(self) -> str: + """ + Returns + ------- + :obj:`str` + The current user status for this shard. + """ + return self._status + + @property + def activity(self) -> typing.Optional[gateway_entities.GatewayActivity]: + """ + Returns + ------- + :obj:`hikari.core.gateway_entities.GatewayActivity`, optional + The current activity for the user on this shard, or ``None`` if + there is no activity. + """ + return self._activity + + @property + def idle_since(self) -> typing.Optional[datetime.datetime]: + """ + Returns + ------- + :obj:`datetime.datetime`, optional + The timestamp when the user of this shard appeared to be idle, or + ``None`` if not applicable. + """ + return self._idle_since + + @property + def is_afk(self) -> bool: + """ + Returns + ------- + :obj:`bool` + ``True`` if the user is AFK, ``False`` otherwise. + """ + return self._is_afk + + async def start(self): + """Connect to the gateway on this shard and schedule tasks to keep this + connection alive. Wait for the shard to dispatch a ``READY`` event, and + then return. + """ + if self._shard_state not in (ShardState.NOT_RUNNING, ShardState.STOPPED): + raise RuntimeError("Cannot start a shard twice") + + self.logger.debug("starting shard") + self._shard_state = ShardState.INITIALIZING + self._task = asyncio.create_task(self._keep_alive()) + self.logger.info("waiting for READY") + await self._client.identify_event.wait() + self._shard_state = ShardState.WAITING_FOR_READY + await self._client.ready_event.wait() + self.logger.info("now READY") + self._shard_state = ShardState.READY + + async def join(self) -> None: + """Wait for the shard to shut down fully.""" + await self._task if self._task is not None else aio.completed_future() + + async def shutdown(self, wait: bool = True) -> None: + """Request that the shard shuts down. + + Parameters + ---------- + wait : :obj:`bool` + If ``True`` (default), then wait for the client to shut down fully. + If ``False``, only send the signal to shut down, but do not wait + for it explicitly. + """ + if self._shard_state != ShardState.STOPPING: + self._shard_state = ShardState.STOPPING + self.logger.debug("stopping shard") + await self._client.close() + if wait: + await self._task + with contextlib.suppress(): + self._task.result() + + async def _keep_alive(self): + back_off = ratelimits.ExponentialBackOff(maximum=None) + last_start = time.perf_counter() + do_not_back_off = True + + while True: + try: + if not do_not_back_off and time.perf_counter() - last_start < 30: + next_backoff = next(back_off) + self.logger.info( + "restarted within 30 seconds, will backoff for %ss", next_backoff, + ) + await asyncio.sleep(next_backoff) + else: + back_off.reset() + + last_start = time.perf_counter() + + do_not_back_off = False + await self._client.connect() + self.logger.critical("shut down silently! this shouldn't happen!") + + except aiohttp.ClientConnectorError as ex: + self.logger.exception( + "failed to connect to Discord to initialize a websocket connection", exc_info=ex, + ) + except errors.GatewayZombiedError: + self.logger.warning("entered a zombie state and will be restarted") + except errors.GatewayInvalidSessionError as ex: + if ex.can_resume: + self.logger.warning("invalid session, so will attempt to resume") + else: + self.logger.warning("invalid session, so will attempt to reconnect") + + if not ex.can_resume: + self._client.seq = None + self._client.session_id = None + do_not_back_off = True + await asyncio.sleep(5) + except errors.GatewayMustReconnectError: + self.logger.warning("instructed by Discord to reconnect") + self._client.seq = None + self._client.session_id = None + do_not_back_off = True + await asyncio.sleep(5) + except errors.GatewayServerClosedConnectionError: + self.logger.warning("disconnected by Discord, will attempt to reconnect") + except errors.GatewayClientClosedError: + self.logger.warning("shutting down") + return + except Exception as ex: + self.logger.debug("propagating unexpected exception %s", exc_info=ex) + raise ex + + async def update_presence( + self, + status: str = ..., # TODO: use enum for status + activity: typing.Optional[gateway_entities.GatewayActivity] = ..., + idle_since: typing.Optional[datetime.datetime] = ..., + is_afk: bool = ..., + ) -> None: + """Update the presence of the user for the shard. + + This will only update arguments that you explicitly specify a value for. + Any arguments that you do not explicitly provide some value for will + not be changed. + + Warnings + -------- + This will fail if the shard is not online. + + Parameters + ---------- + status : :obj:`str` + The new status to set. + activity : :obj:`hikari.core.gateway_entities.GatewayActivity`, optional + The new activity to set. + idle_since : :obj:`datetime.datetime`, optional + The time to show up as being idle since, or ``None`` if not + applicable. + is_afk : :obj:`bool` + ``True`` if the user should be marked as AFK, or ``False`` + otherwise. + """ + status = self._status if status is ... else status + activity = self._activity if activity is ... else activity + idle_since = self._idle_since if idle_since is ... else idle_since + is_afk = self._is_afk if is_afk is ... else is_afk + + presence = self._create_presence_pl(status=status, activity=activity, idle_since=idle_since, is_afk=is_afk) + await self._client.update_presence(presence) + + # If we get this far, the update succeeded probably, or the gateway just died. Whatever. + self._status = status + self._activity = activity + self._idle_since = idle_since + self._is_afk = is_afk + + @staticmethod + def _create_presence_pl( + status: str, # TODO: use enum for status + activity: typing.Optional[gateway_entities.GatewayActivity], + idle_since: typing.Optional[datetime.datetime], + is_afk: bool, + ) -> typing.Dict[str, typing.Any]: + return { + "status": status, + "idle_since": idle_since.timestamp() if idle_since is not None else None, + "game": activity.serialize() if activity is not None else None, + "afk": is_afk, + } From 14362971b4ed4802a8971196f46008b88ccd0e18 Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Wed, 25 Mar 2020 16:15:46 +0000 Subject: [PATCH 043/922] Made gateway_client delegate to the event dispatcher; made dispatcher impl have an 'on' decorator. --- hikari/core/dispatcher.py | 17 +++++++++++++++++ hikari/core/gateway_client.py | 18 +++++++++++++++--- hikari/core/shard_client.py | 8 ++++---- 3 files changed, 36 insertions(+), 7 deletions(-) diff --git a/hikari/core/dispatcher.py b/hikari/core/dispatcher.py index 88dc9cc741..727b504dc0 100644 --- a/hikari/core/dispatcher.py +++ b/hikari/core/dispatcher.py @@ -119,6 +119,23 @@ def wait_for( lookup, but can be implemented this way optionally if documented. """ + def on(self, event_type: typing.Type[EventT]) -> typing.Callable[[EventCallbackT], EventCallbackT]: + """A decorator that is equivalent to invoking :meth:`add_listener`. + + Parameters + ---------- + event_type : :obj:`typing.Type` [ :obj:`events.HikariEvent` ] + The event type to register the produced decorator to. + + Returns + ------- + coroutine function decorator: + A decorator for a coroutine function that registers the given event. + """ + def decorator(callback: EventCallbackT) -> EventCallbackT: + return self.add_listener(event_type, callback) + return decorator + # Do not add an annotation here, it will mess with type hints in PyCharm which can lead to # confusing telepathy comments to the user. @abc.abstractmethod diff --git a/hikari/core/gateway_client.py b/hikari/core/gateway_client.py index 37b7efe71f..cd1b155e55 100644 --- a/hikari/core/gateway_client.py +++ b/hikari/core/gateway_client.py @@ -28,6 +28,9 @@ from hikari.core import gateway_config from hikari.core import shard_client from hikari.core import state +from hikari.core.dispatcher import EventCallbackT +from hikari.core.dispatcher import EventCallbackT +from hikari.core.dispatcher import EventT from hikari.internal_utilities import loggers from hikari.net import shard @@ -35,7 +38,7 @@ ShardT = typing.TypeVar("ShardT", bound=shard_client.ShardClient) -class GatewayClient(typing.Generic[ShardT], shard_client.WebsocketClientBase): +class GatewayClient(typing.Generic[ShardT], shard_client.WebsocketClientBase, dispatcher.EventDispatcher): def __init__( self, config: gateway_config.GatewayConfig, @@ -78,12 +81,12 @@ async def start(self) -> None: async def join(self) -> None: await asyncio.gather(*(shard_obj.join() for shard_obj in self.shards.values())) - async def shutdown(self, wait: bool = True) -> None: + async def close(self, wait: bool = True) -> None: if self._is_running: self.logger.info("stopping %s shard(s)", len(self.shards)) start_time = time.perf_counter() try: - await asyncio.gather(*(shard_obj.shutdown(wait) for shard_obj in self.shards.values())) + await asyncio.gather(*(shard_obj.close(wait) for shard_obj in self.shards.values())) finally: finish_time = time.perf_counter() self.logger.info("stopped %s shard(s) in approx %.2fs", len(self.shards), finish_time - start_time) @@ -159,3 +162,12 @@ def predicate(member): self.logger.debug("detected %s web socket events to register from %s", len(types), events.__name__) return types + + def add_listener(self, event_type: typing.Type[EventT], callback: EventCallbackT) -> EventCallbackT: + return self.event_dispatcher.add_listener(event_type, callback) + + def remove_listener(self, event_type: typing.Type[EventT], callback: EventCallbackT) -> EventCallbackT: + return self.event_dispatcher.remove_listener(event_type, callback) + + def dispatch_event(self, event: events.HikariEvent) -> ...: + return self.event_dispatcher.dispatch_event(event) diff --git a/hikari/core/shard_client.py b/hikari/core/shard_client.py index 5a411bae87..2e2d701671 100644 --- a/hikari/core/shard_client.py +++ b/hikari/core/shard_client.py @@ -65,7 +65,7 @@ async def start(self): ... @abc.abstractmethod - async def shutdown(self, wait: bool = True): + async def close(self, wait: bool = True): ... @abc.abstractmethod @@ -92,12 +92,12 @@ def sigterm_handler(*_): except KeyboardInterrupt as _ex: self.logger.info("received signal to shut down client") - loop.run_until_complete(self.shutdown()) + loop.run_until_complete(self.close()) # Apparently you have to alias except clauses or you get an # UnboundLocalError. ex = _ex finally: - loop.run_until_complete(self.shutdown(True)) + loop.run_until_complete(self.close(True)) with contextlib.suppress(NotImplementedError): # Not implemented on Windows loop.remove_signal_handler(signal.SIGTERM) @@ -265,7 +265,7 @@ async def join(self) -> None: """Wait for the shard to shut down fully.""" await self._task if self._task is not None else aio.completed_future() - async def shutdown(self, wait: bool = True) -> None: + async def close(self, wait: bool = True) -> None: """Request that the shard shuts down. Parameters From cdf675bfe167f9dd59120a506653c650e688c42b Mon Sep 17 00:00:00 2001 From: Faster Speeding Date: Sat, 28 Mar 2020 20:50:35 +0000 Subject: [PATCH 044/922] Fix broken tests and pylint warning --- hikari/core/dispatcher.py | 4 ++++ hikari/core/gateway_client.py | 1 - tests/hikari/core/test_embeds.py | 1 + tests/hikari/core/test_events.py | 4 ++-- tests/hikari/core/test_messages.py | 1 + tests/hikari/net/test_rest.py | 8 ++++---- 6 files changed, 12 insertions(+), 7 deletions(-) diff --git a/hikari/core/dispatcher.py b/hikari/core/dispatcher.py index 727b504dc0..3538e9f996 100644 --- a/hikari/core/dispatcher.py +++ b/hikari/core/dispatcher.py @@ -132,8 +132,10 @@ def on(self, event_type: typing.Type[EventT]) -> typing.Callable[[EventCallbackT coroutine function decorator: A decorator for a coroutine function that registers the given event. """ + def decorator(callback: EventCallbackT) -> EventCallbackT: return self.add_listener(event_type, callback) + return decorator # Do not add an annotation here, it will mess with type hints in PyCharm which can lead to @@ -181,7 +183,9 @@ class EventDispatcherImpl(EventDispatcher): def __init__(self) -> None: self._listeners: typing.Dict[typing.Type[EventT], typing.List[EventCallbackT]] = {} + # pylint: disable=E1136 self._waiters: typing.Dict[typing.Type[EventT], containers.WeakKeyDictionary[asyncio.Future, PredicateT]] = {} + # pylint: enable=E1136 self.logger = loggers.get_named_logger(self) def close(self) -> None: diff --git a/hikari/core/gateway_client.py b/hikari/core/gateway_client.py index cd1b155e55..895eb13971 100644 --- a/hikari/core/gateway_client.py +++ b/hikari/core/gateway_client.py @@ -29,7 +29,6 @@ from hikari.core import shard_client from hikari.core import state from hikari.core.dispatcher import EventCallbackT -from hikari.core.dispatcher import EventCallbackT from hikari.core.dispatcher import EventT from hikari.internal_utilities import loggers from hikari.net import shard diff --git a/tests/hikari/core/test_embeds.py b/tests/hikari/core/test_embeds.py index aeb376a7a4..a6e850499f 100644 --- a/tests/hikari/core/test_embeds.py +++ b/tests/hikari/core/test_embeds.py @@ -23,6 +23,7 @@ from hikari.core import colors from hikari.core import embeds +from hikari.internal_utilities import dates from tests.hikari import _helpers diff --git a/tests/hikari/core/test_events.py b/tests/hikari/core/test_events.py index cf9d9425c2..8c4d5d5a96 100644 --- a/tests/hikari/core/test_events.py +++ b/tests/hikari/core/test_events.py @@ -896,12 +896,12 @@ def test_deserialize(self, test_voice_server_update_payload): assert voice_server_update_obj.endpoint == "smart.loyal.discord.gg" -class TestWebhookUpdate: +class TestWebhookUpdateEvent: @pytest.fixture() def test_webhook_update_payload(self): return {"guild_id": "2929292", "channel_id": "94949494"} def test_deserialize(self, test_webhook_update_payload): - webhook_update_obj = events.WebhookUpdate.deserialize(test_webhook_update_payload) + webhook_update_obj = events.WebhookUpdateEvent.deserialize(test_webhook_update_payload) assert webhook_update_obj.guild_id == 2929292 assert webhook_update_obj.channel_id == 94949494 diff --git a/tests/hikari/core/test_messages.py b/tests/hikari/core/test_messages.py index 08b21c2567..cd8f5739c9 100644 --- a/tests/hikari/core/test_messages.py +++ b/tests/hikari/core/test_messages.py @@ -27,6 +27,7 @@ from hikari.core import messages from hikari.core import oauth2 from hikari.core import users +from hikari.internal_utilities import dates from tests.hikari import _helpers diff --git a/tests/hikari/net/test_rest.py b/tests/hikari/net/test_rest.py index e7036974e2..72f4ff6e22 100644 --- a/tests/hikari/net/test_rest.py +++ b/tests/hikari/net/test_rest.py @@ -300,10 +300,10 @@ async def test__request_sets_X_Audit_Log_Reason_if_reason(self, compiled_route, "get", "https://discordapp.com/api/v6/somewhere", headers={ - "X-RateLimit-Precision": "millisecond", - "Authorization": "Bot token", - "X-Audit-Log-Reason": "test reason", - }, + "X-RateLimit-Precision": "millisecond", + "Authorization": "Bot token", + "X-Audit-Log-Reason": "test reason", + }, json=None, params=None, data=None, From 5ccb76dbe365b0efb873e6cdeefae604f953df52 Mon Sep 17 00:00:00 2001 From: Faster Speeding Date: Sat, 28 Mar 2020 21:04:17 +0000 Subject: [PATCH 045/922] Add guild related models + tests --- hikari/core/guilds.py | 255 ++++++++++++++++++- tests/hikari/core/test_guilds.py | 423 ++++++++++++++++++++++++------- 2 files changed, 579 insertions(+), 99 deletions(-) diff --git a/hikari/core/guilds.py b/hikari/core/guilds.py index 6248f44d19..670a05d879 100644 --- a/hikari/core/guilds.py +++ b/hikari/core/guilds.py @@ -49,9 +49,10 @@ import typing from hikari.core import channels +from hikari.core import colors from hikari.core import emojis as _emojis from hikari.core import entities -from hikari.core import permissions +from hikari.core import permissions as _permissions from hikari.core import snowflakes from hikari.core import users from hikari.internal_utilities import cdn @@ -265,7 +266,47 @@ class GuildMember(entities.HikariEntity, entities.Deserializable): @marshaller.attrs(slots=True) class GuildRole(snowflakes.UniqueEntity, entities.Deserializable): - ... + """Represents a guild bound Role object.""" + + #: The role's name + #: + #: :type: :obj:`str` + name: str = marshaller.attrib(deserializer=str) + + #: The colour of this role, will be applied to a member's name in chat + #: if it's their top coloured role. + #: + #: :type: :obj:`colors.Color` + color: colors.Color = marshaller.attrib(deserializer=colors.Color) + + #: Whether this role is hoisting the members it's attached to in the member + #: list, members will be hoisted under their highest role where + #: :attr:`hoisted` is true. + #: + #: :type: :obj:`bool` + is_hoisted: bool = marshaller.attrib(raw_name="hoist", deserializer=bool) + + #: The position of this role in the role hierarchy. + #: + #: :type: :obj:`int` + position: int = marshaller.attrib(deserializer=int) + + #: The guild wide permissions this role gives to the members it's attached + #: to, may be overridden by channel overwrites. + #: + #: :type: :obj:`_permissions.Permission` + permissions: _permissions.Permission = marshaller.attrib(deserializer=_permissions.Permission) + + #: Whether this role is managed by an integration. + #: + #: :type: :obj:`bool` + is_managed: bool = marshaller.attrib(raw_name="managed", deserializer=bool) + + #: Whether this role can be mentioned by all, regardless of the + #: ``MENTION_EVERYONE`` permission. + #: + #: :type: :obj:`bool` + is_mentionable: bool = marshaller.attrib(raw_name="mentionable", deserializer=bool) @enum.unique @@ -619,14 +660,118 @@ class GuildMemberPresence(entities.HikariEntity, entities.Deserializable): nick: typing.Optional[str] = marshaller.attrib(raw_name="nick", deserializer=str, if_undefined=None, if_none=None) +@enum.unique +class IntegrationExpireBehaviour(enum.IntEnum): + """Behavior for expiring integration subscribers.""" + + #: Remove the role. + REMOVE_ROLE = 0 + #: Kick the subscriber. + KICK = 1 + + @marshaller.attrs(slots=True) -class GuildIntegration(snowflakes.UniqueEntity): - ... +class IntegrationAccount(entities.HikariEntity, entities.Deserializable): + """An account that's linked to an integration.""" + + #: The string ID of this (likely) third party account. + #: + #: :type: :obj:`str` + id: str = marshaller.attrib(deserializer=str) + + #: The name of this account. + #: + #: :type: :obj:`str` + name: str = marshaller.attrib(deserializer=str) @marshaller.attrs(slots=True) -class GuildMemberBan(entities.HikariEntity): - ... +class PartialGuildIntegration(snowflakes.UniqueEntity, entities.Deserializable): + """A partial representation of an integration, found in audit logs.""" + + #: The name of this integration. + #: + #: :type: :obj:`str` + name: str = marshaller.attrib(deserializer=str) + + #: The type of this integration. + #: + #: :type: :obj:`str` + type: str = marshaller.attrib(deserializer=str) + + #: The account connected to this integration. + #: + #: :type: :obj:`IntegrationAccount` + account: IntegrationAccount = marshaller.attrib(deserializer=IntegrationAccount.deserialize) + + +@marshaller.attrs(slots=True) +class GuildIntegration(snowflakes.UniqueEntity, entities.Deserializable): + """Represents a guild integration object.""" + + #: Whether this integration is enabled. + #: + #: :type: :obj:`bool` + is_enabled: bool = marshaller.attrib(raw_name="enabled", deserializer=bool) + + #: Whether this integration is syncing subscribers/emojis. + #: + #: :type: :obj:`bool` + is_syncing: bool = marshaller.attrib(raw_name="syncing", deserializer=bool) + + #: The ID of the managed role used for this integration's subscribers. + #: + #: :type: :obj:`snowflakes.Snowflake` + role_id: typing.Optional[snowflakes.Snowflake] = marshaller.attrib(deserializer=snowflakes.Snowflake.deserialize) + + #: Whether users under this integration are allowed to use it's custom + #: emojis. + #: + #: + is_emojis_enabled: typing.Optional[snowflakes.Snowflake] = marshaller.attrib( + raw_name="enable_emoticons", deserializer=bool, if_undefined=None, + ) + + #: How members should be treated after their connected subscription expires + #: This won't be enacted until after :attr:`expire_grace_period` passes. + #: + #: :type: :obj:`IntegrationExpireBehaviour` + expire_behavior: IntegrationExpireBehaviour = marshaller.attrib(deserializer=IntegrationExpireBehaviour) + + #: The time delta for how many days users with expired subscriptions are + #: given until :attr:`expire_behavior` is enacted out on them + #: + #: :type: :obj:`datetime.timedelta` + expire_grace_period: datetime.timedelta = marshaller.attrib( + deserializer=lambda delta: datetime.timedelta(days=delta), + ) + + #: The user this integration belongs to. + #: + #: :type: :obj:`users.User` + user: users.User = marshaller.attrib(deserializer=users.User.deserialize) + + #: The datetime of when this integration's subscribers were last synced. + #: + #: :type: :obj:`datetime.datetime` + last_synced_at: datetime.datetime = marshaller.attrib( + raw_name="synced_at", deserializer=dates.parse_iso_8601_ts, if_none=None + ) + + +@marshaller.attrs(slots=True) +class GuildMemberBan(entities.HikariEntity, entities.Deserializable): + """Used to represent guild bans.""" + + #: The reason for this ban, will be :obj:`None` if no reason was given. + #: + #: :type: :obj:`str`, optional + reason: str = marshaller.attrib(deserializer=str, if_none=None) + + #: The object of the user this ban targets. + #: + #: :type: :obj:`users.User` + user: users.User = marshaller.attrib(deserializer=users.User.deserialize) @marshaller.attrs(slots=True) @@ -729,11 +874,13 @@ class Guild(PartialGuild): #: :type: :obj:`snowflakes.Snowflake` owner_id: snowflakes.Snowflake = marshaller.attrib(deserializer=snowflakes.Snowflake) - #: The guild level permissions that apply to the bot user. + #: The guild level permissions that apply to the bot user, + #: Will be ``None`` when this object is retrieved through a REST request + #: rather than from the gateway. #: - #: :type: :obj:`permissions.Permission` - my_permissions: permissions.Permission = marshaller.attrib( - raw_name="permissions", deserializer=permissions.Permission + #: :type: :obj:`_permissions.Permission` + my_permissions: _permissions.Permission = marshaller.attrib( + raw_name="permissions", deserializer=_permissions.Permission, if_undefined=None ) #: The voice region for the guild. @@ -758,7 +905,6 @@ class Guild(PartialGuild): ) # TODO: document when this is not specified. - # FIXME: do we need a field for this, or can we infer it from the `embed_channel_id`? #: Defines if the guild embed is enabled or not. This information may not #: be present, in which case, it will be ``None`` instead. #: @@ -768,7 +914,8 @@ class Guild(PartialGuild): ) #: The channel ID that the guild embed will generate an invite to, if - #: enabled for this guild. If not enabled, it will be ``None``. + #: enabled for this guild. Will be ``None`` if invites are disable for this + #: guild's embed. #: #: :type: :obj:`snowflakes.Snowflake`, optional embed_channel_id: typing.Optional[snowflakes.Snowflake] = marshaller.attrib( @@ -881,7 +1028,9 @@ class Guild(PartialGuild): #: this will always be ``None``. #: #: :type: :obj`datetime.datetime`, optional - joined_at: typing.Optional[datetime.datetime] = marshaller.attrib(deserializer=dates.parse_iso_8601_ts) + joined_at: typing.Optional[datetime.datetime] = marshaller.attrib( + deserializer=dates.parse_iso_8601_ts, if_undefined=None + ) #: Whether the guild is considered to be large or not. #: @@ -1035,3 +1184,83 @@ class Guild(PartialGuild): public_updates_channel_id: typing.Optional[snowflakes.Snowflake] = marshaller.attrib( if_none=None, deserializer=snowflakes.Snowflake ) + + def format_splash_url(self, fmt: str = "png", size: int = 2048) -> typing.Optional[str]: + """Generate the url for this guild's splash image, if set. + + Parameters + ---------- + fmt : :obj:`str` + The format to use for this url, defaults to ``png``. + Supports ``png``, ``jpeg``, ``jpg`` and ``webp``. + size : :obj:`int` + The size to set for the url, defaults to ``2048``. + Can be any power of two between 16 and 2048. + + Returns + ------- + :obj:`str`, optional + The string url. + """ + if self.splash_hash: + return cdn.generate_cdn_url("splashes", str(self.id), self.splash_hash, fmt=fmt, size=size) + return None + + @property + def splash_url(self) -> typing.Optional[str]: + """The url for this guild's splash, if set.""" + return self.format_splash_url() + + def format_discovery_splash_url(self, fmt: str = "png", size: int = 2048) -> typing.Optional[str]: + """Generate the url for this guild's discovery splash image, if set. + + Parameters + ---------- + fmt : :obj:`str` + The format to use for this url, defaults to ``png``. + Supports ``png``, ``jpeg``, ``jpg`` and ``webp``. + size : :obj:`int` + The size to set for the url, defaults to ``2048``. + Can be any power of two between 16 and 2048. + + Returns + ------- + :obj:`str`, optional + The string url. + """ + if self.discovery_splash_hash: + return cdn.generate_cdn_url( + "discovery-splashes", str(self.id), self.discovery_splash_hash, fmt=fmt, size=size + ) + return None + + @property + def discovery_splash_url(self) -> typing.Optional[str]: + """The url for this guild's discovery splash, if set.""" + return self.format_discovery_splash_url() + + def format_banner_url(self, fmt: str = "png", size: int = 2048) -> typing.Optional[str]: + """Generate the url for this guild's banner image, if set. + + Parameters + ---------- + fmt : :obj:`str` + The format to use for this url, defaults to ``png``. + Supports ``png``, ``jpeg``, ``jpg`` and ``webp``. + size : :obj:`int` + The size to set for the url, defaults to ``2048``. + Can be any power of two between 16 and 2048. + + Returns + ------- + :obj:`str`, optional + The string url. + """ + if self.banner_hash: + return cdn.generate_cdn_url("banners", str(self.id), self.banner_hash, fmt=fmt, size=size) + return None + + @property + def banner_url(self) -> typing.Optional[str]: + """The url for this guild's banner, if set.""" + return self.format_banner_url() diff --git a/tests/hikari/core/test_guilds.py b/tests/hikari/core/test_guilds.py index 448bcd4826..15fa253cb5 100644 --- a/tests/hikari/core/test_guilds.py +++ b/tests/hikari/core/test_guilds.py @@ -76,40 +76,19 @@ def test_roles_payloads(): @pytest.fixture -def test_channel_payloads(): - return [ - { - "type": 0, - "id": "1234567", - "guild_id": "696969", - "position": 100, - "permission_overwrites": [], - "nsfw": True, - "parent_id": None, - "rate_limit_per_user": 420, - "topic": "nsfw stuff", - "name": "shh!", - }, - { - "type": 4, - "id": "123456", - "guild_id": "54321", - "position": 69, - "permission_overwrites": [], - "name": "dank category", - }, - { - "type": 2, - "id": "9292929", - "guild_id": "929", - "position": 66, - "permission_overwrites": [], - "name": "roy rodgers mc freely", - "bitrate": 999, - "user_limit": 0, - "parent_id": "42", - }, - ] +def test_channel_payload(): + return { + "type": 0, + "id": "1234567", + "guild_id": "696969", + "position": 100, + "permission_overwrites": [], + "nsfw": True, + "parent_id": None, + "rate_limit_per_user": 420, + "topic": "nsfw stuff", + "name": "shh!", + } @pytest.fixture @@ -155,58 +134,6 @@ def test_voice_state_payload(): } -@pytest.fixture() -def test_partial_guild_payload(): - return { - "id": "152559372126519269", - "name": "Isopropyl", - "icon": "d4a983885dsaa7691ce8bcaaf945a", - "features": ["DISCOVERABLE"], - } - - -@pytest.fixture -def test_guild_payload( - test_emoji_payload, test_roles_payloads, test_channel_payloads, test_member_payload, test_voice_state_payload -): - return { - "id": "265828729970753537", - "afk_channel_id": "99998888777766", - "owner_id": "6969696", - "region": "eu-central", - "system_channel_id": "19216801", - "application_id": "10987654321", - "name": "L33t guild", - "icon": "1a2b3c4d", - "splash": "0ff0ff0ff", - "afk_timeout": 1200, - "verification_level": 4, - "default_message_notifications": 1, - "explicit_content_filter": 2, - "roles": test_roles_payloads, - "emojis": [test_emoji_payload], - "features": ["ANIMATED_ICON", "MORE_EMOJI", "NEWS", "SOME_UNDOCUMENTED_FEATURE"], - "voice_states": [test_voice_state_payload], - "member_count": 14, - "mfa_level": 1, - "joined_at": "2019-05-17T06:26:56.936000+00:00", - "large": False, - "unavailable": False, - "permissions": 66_321_471, - "members": [test_member_payload], - "channels": test_channel_payloads, - "max_members": 25000, - "vanity_url_code": "loool", - "description": "This is a server I guess, its a bit crap though", - "banner": "1a2b3c", - "premium_tier": 2, - "premium_subscription_count": 1, - "preferred_locale": "en-GB", - "system_channel_flags": 3, - "rules_channel_id": "42042069", - } - - @pytest.fixture() def test_activity_party_payload(): return {"id": "spotify:3234234234", "size": [2, 5]} @@ -261,6 +188,71 @@ def test_presence_activity_payload( } +@pytest.fixture() +def test_partial_guild_payload(): + return { + "id": "152559372126519269", + "name": "Isopropyl", + "icon": "d4a983885dsaa7691ce8bcaaf945a", + "features": ["DISCOVERABLE"], + } + + +@pytest.fixture +def test_guild_payload( + test_emoji_payload, + test_roles_payloads, + test_channel_payload, + test_member_payload, + test_voice_state_payload, + test_guild_member_presence, +): + return { + "id": "265828729970753537", + "afk_channel_id": "99998888777766", + "owner_id": "6969696", + "region": "eu-central", + "system_channel_id": "19216801", + "application_id": "39494949", + "name": "L33t guild", + "icon": "1a2b3c4d", + "splash": "0ff0ff0ff", + "afk_timeout": 1200, + "verification_level": 4, + "default_message_notifications": 1, + "explicit_content_filter": 2, + "roles": test_roles_payloads, + "emojis": [test_emoji_payload], + "features": ["ANIMATED_ICON", "MORE_EMOJI", "NEWS", "SOME_UNDOCUMENTED_FEATURE"], + "voice_states": [test_voice_state_payload], + "member_count": 14, + "mfa_level": 1, + "joined_at": "2019-05-17T06:26:56.936000+00:00", + "large": False, + "unavailable": False, + "permissions": 66_321_471, + "members": [test_member_payload], + "channels": [test_channel_payload], + "max_members": 25000, + "vanity_url_code": "loool", + "description": "This is a server I guess, its a bit crap though", + "banner": "1a2b3c", + "premium_tier": 2, + "premium_subscription_count": 1, + "preferred_locale": "en-GB", + "system_channel_flags": 3, + "rules_channel_id": "42042069", + "discovery_splash": "famfamFAMFAMfam", + "embed_enabled": True, + "embed_channel_id": "9439394949", + "widget_enabled": True, + "widget_channel_id": "9439394949", + "public_updates_channel_id": "33333333", + "presences": [test_guild_member_presence], + "max_presences": 250, + } + + class TestGuildMember: def test_deserialize(self, test_member_payload, test_user_payload): mock_user = mock.MagicMock(users.User) @@ -291,6 +283,31 @@ def test_deserialize(self, test_member_payload, test_user_payload): assert guild_member_obj.is_mute is True +class TestGuildRole: + @pytest.fixture() + def test_guild_role_payload(self): + return { + "id": "41771983423143936", + "name": "WE DEM BOYZZ!!!!!!", + "color": 3_447_003, + "hoist": True, + "position": 0, + "permissions": 66_321_471, + "managed": False, + "mentionable": False, + } + + def test_deserialize(self, test_guild_role_payload): + guild_role_obj = guilds.GuildRole.deserialize(test_guild_role_payload) + assert guild_role_obj.name == "WE DEM BOYZZ!!!!!!" + assert guild_role_obj.color == 3_447_003 + assert guild_role_obj.is_hoisted is True + assert guild_role_obj.position == 0 + assert guild_role_obj.permissions == 66_321_471 + assert guild_role_obj.is_managed is False + assert guild_role_obj.is_mentionable is False + + class TestActivityTimestamps: def test_deserialize(self, test_activity_timestamps_payload): mock_start_date = mock.MagicMock(datetime.datetime) @@ -456,6 +473,97 @@ def test_deserialize( assert guild_member_presence_obj.nick == "Nick" +class TestGuildMemberBan: + @pytest.fixture() + def test_guild_member_ban_payload(self, test_user_payload): + return {"reason": "Get Nyaa'ed", "user": test_user_payload} + + def test_deserializer(self, test_guild_member_ban_payload, test_user_payload): + mock_user = mock.MagicMock(users.User) + with _helpers.patch_marshal_attr( + guilds.GuildMemberBan, "user", deserializer=users.User.deserialize, return_value=mock_user + ) as patched_user_deserializer: + guild_member_ban_obj = guilds.GuildMemberBan.deserialize(test_guild_member_ban_payload) + patched_user_deserializer.assert_called_once_with(test_user_payload) + assert guild_member_ban_obj.reason == "Get Nyaa'ed" + assert guild_member_ban_obj.user is mock_user + + +@pytest.fixture() +def test_integration_account_payload(): + return {"id": "543453", "name": "Blah Blah"} + + +class TestIntegrationAccount: + def test_deserializer(self, test_integration_account_payload): + integration_account_obj = guilds.IntegrationAccount.deserialize(test_integration_account_payload) + assert integration_account_obj.id == "543453" + assert integration_account_obj.name == "Blah Blah" + + +@pytest.fixture() +def test_partial_guild_integration_payload(test_integration_account_payload): + return { + "id": "4949494949", + "name": "Blah blah", + "type": "twitch", + "account": test_integration_account_payload, + } + + +class TestPartialGuildIntegration: + def test_deserialise(self, test_partial_guild_integration_payload, test_integration_account_payload): + partial_guild_integration_obj = guilds.PartialGuildIntegration.deserialize( + test_partial_guild_integration_payload + ) + assert partial_guild_integration_obj.name == "Blah blah" + assert partial_guild_integration_obj.type == "twitch" + assert partial_guild_integration_obj.account == guilds.IntegrationAccount.deserialize( + test_integration_account_payload + ) + + +class TestGuildIntegration: + @pytest.fixture() + def test_guild_integration_payload(self, test_user_payload, test_partial_guild_integration_payload): + return { + **test_partial_guild_integration_payload, + "enabled": True, + "syncing": False, + "role_id": "98494949", + "enable_emoticons": False, + "expire_behavior": 1, + "expire_grace_period": 7, + "user": test_user_payload, + "synced_at": "2015-04-26T06:26:56.936000+00:00", + } + + def test_deserialize(self, test_guild_integration_payload, test_user_payload, test_integration_account_payload): + mock_user = mock.MagicMock(users.User) + mock_sync_date = mock.MagicMock(datetime.datetime) + with _helpers.patch_marshal_attr( + guilds.GuildIntegration, + "last_synced_at", + deserializer=dates.parse_iso_8601_ts, + return_value=mock_sync_date, + ) as patched_sync_at_deserializer: + with _helpers.patch_marshal_attr( + guilds.GuildIntegration, "user", deserializer=users.User.deserialize, return_value=mock_user + ) as patched_user_deserializer: + guild_integration_obj = guilds.GuildIntegration.deserialize(test_guild_integration_payload) + patched_user_deserializer.assert_called_once_with(test_user_payload) + patched_sync_at_deserializer.assert_called_once_with("2015-04-26T06:26:56.936000+00:00") + + assert guild_integration_obj.is_enabled is True + assert guild_integration_obj.is_syncing is False + assert guild_integration_obj.role_id == 98494949 + assert guild_integration_obj.is_emojis_enabled is False + assert guild_integration_obj.expire_behavior is guilds.IntegrationExpireBehaviour.KICK + assert guild_integration_obj.expire_grace_period == datetime.timedelta(days=7) + assert guild_integration_obj.user is mock_user + assert guild_integration_obj.last_synced_at is mock_sync_date + + class TestUnavailableGuild: def test_deserialize_when_unavailable_is_defined(self): guild_delete_event_obj = guilds.UnavailableGuild.deserialize({"id": "293293939", "unavailable": True}) @@ -519,3 +627,146 @@ def test_format_icon_url(self, partial_guild_obj): url = partial_guild_obj.icon_url cdn.generate_cdn_url.assert_called_once() assert url == mock_url + + +class TestGuild: + def test_deserialize( + self, + test_guild_payload, + test_roles_payloads, + test_emoji_payload, + test_member_payload, + test_channel_payload, + test_guild_member_presence, + ): + mock_emoji = mock.MagicMock(emojis.GuildEmoji, id=42) + mock_user = mock.MagicMock(users.User, id=84) + mock_guild_channel = mock.MagicMock(guilds.GuildChannel, id=6969) + with mock.patch.object(emojis.GuildEmoji, "deserialize", return_value=mock_emoji): + with _helpers.patch_marshal_attr( + guilds.GuildMemberPresence, "user", deserializer=guilds.PresenceUser.deserialize, return_value=mock_user + ) as patched_user_deserializer: + with _helpers.patch_marshal_attr( + guilds.GuildMember, "user", deserializer=users.User.deserialize, return_value=mock_user + ) as patched_member_user_deserializer: + with mock.patch.object(guilds, "parse_guild_channel", return_value=mock_guild_channel): + guild_obj = guilds.Guild.deserialize(test_guild_payload) + guilds.parse_guild_channel.assert_called_once_with(test_channel_payload) + patched_member_user_deserializer.assert_called_once_with(test_member_payload["user"]) + assert guild_obj.members == {84: guilds.GuildMember.deserialize(test_member_payload)} + patched_user_deserializer.assert_called_once_with(test_member_payload["user"]) + assert guild_obj.presences == {84: guilds.GuildMemberPresence.deserialize(test_guild_member_presence)} + emojis.GuildEmoji.deserialize.assert_called_once_with(test_emoji_payload) + assert guild_obj.splash_hash == "0ff0ff0ff" + assert guild_obj.discovery_splash_hash == "famfamFAMFAMfam" + assert guild_obj.owner_id == 6969696 + assert guild_obj.my_permissions == 66_321_471 + assert guild_obj.region == "eu-central" + assert guild_obj.afk_channel_id == 99998888777766 + assert guild_obj.afk_timeout == datetime.timedelta(minutes=20) + assert guild_obj.is_embed_enabled is True + assert guild_obj.embed_channel_id == 9439394949 + assert guild_obj.is_widget_enabled is True + assert guild_obj.widget_channel_id == 9439394949 + assert guild_obj.verification_level is guilds.GuildVerificationLevel.VERY_HIGH + assert guild_obj.default_message_notifications is guilds.GuildMessageNotificationsLevel.ONLY_MENTIONS + assert guild_obj.explicit_content_filter is guilds.GuildExplicitContentFilterLevel.ALL_MEMBERS + assert guild_obj.roles == {r.id: r for r in map(guilds.GuildRole.deserialize, test_roles_payloads)} + assert guild_obj.emojis == {42: mock_emoji} + assert guild_obj.mfa_level is guilds.GuildMFALevel.ELEVATED + assert guild_obj.application_id == 39494949 + assert guild_obj.is_unavailable is False + assert guild_obj.system_channel_id == 19216801 + assert ( + guild_obj.system_channel_flags + == guilds.GuildSystemChannelFlag.SUPPRESS_PREMIUM_SUBSCRIPTION + | guilds.GuildSystemChannelFlag.SUPPRESS_USER_JOIN + ) + assert guild_obj.rules_channel_id == 42042069 + assert guild_obj.joined_at == dates.parse_iso_8601_ts("2019-05-17T06:26:56.936000+00:00") + assert guild_obj.is_large is False + assert guild_obj.member_count == 14 + assert guild_obj.channels == {6969: mock_guild_channel} + assert guild_obj.max_presences == 250 + assert guild_obj.max_members == 25000 + assert guild_obj.vanity_url_code == "loool" + assert guild_obj.description == "This is a server I guess, its a bit crap though" + assert guild_obj.banner_hash == "1a2b3c" + assert guild_obj.premium_tier is guilds.GuildPremiumTier.TIER_2 + assert guild_obj.premium_subscription_count == 1 + assert guild_obj.preferred_locale == "en-GB" + assert guild_obj.public_updates_channel_id == 33333333 + + @pytest.fixture() + @mock.patch.object(emojis.GuildEmoji, "deserialize") + def test_guild_obj(self, *patched_objs, test_guild_payload): + return guilds.Guild.deserialize(test_guild_payload) + + def test_format_banner_url(self, test_guild_obj): + mock_url = "https://not-al" + with mock.patch.object(cdn, "generate_cdn_url", return_value=mock_url): + url = test_guild_obj.format_banner_url(fmt="nyaapeg", size=4000) + cdn.generate_cdn_url.assert_called_once_with( + "banners", "265828729970753537", "1a2b3c", fmt="nyaapeg", size=4000 + ) + assert url is mock_url + + def test_format_banner_url_returns_none(self, test_guild_obj): + test_guild_obj.banner_hash = None + with mock.patch.object(cdn, "generate_cdn_url", return_value=...): + url = test_guild_obj.format_banner_url() + cdn.generate_cdn_url.assert_not_called() + assert url is None + + def test_banner_url(self, test_guild_obj): + mock_url = "https://not-al" + with mock.patch.object(cdn, "generate_cdn_url", return_value=mock_url): + url = test_guild_obj.banner_url + cdn.generate_cdn_url.assert_called_once() + assert url is mock_url + + def test_format_discovery_splash_url(self, test_guild_obj): + mock_url = "https://not-al" + with mock.patch.object(cdn, "generate_cdn_url", return_value=mock_url): + url = test_guild_obj.format_discovery_splash_url(fmt="nyaapeg", size=4000) + cdn.generate_cdn_url.assert_called_once_with( + "discovery-splashes", "265828729970753537", "famfamFAMFAMfam", fmt="nyaapeg", size=4000 + ) + assert url is mock_url + + def test_format_discovery_splash_returns_none(self, test_guild_obj): + test_guild_obj.discovery_splash_hash = None + with mock.patch.object(cdn, "generate_cdn_url", return_value=...): + url = test_guild_obj.format_discovery_splash_url() + cdn.generate_cdn_url.assert_not_called() + assert url is None + + def test_discover_splash_url(self, test_guild_obj): + mock_url = "https://not-al" + with mock.patch.object(cdn, "generate_cdn_url", return_value=mock_url): + url = test_guild_obj.discovery_splash_url + cdn.generate_cdn_url.assert_called_once() + assert url is mock_url + + def test_format_splash_url(self, test_guild_obj): + mock_url = "https://not-al" + with mock.patch.object(cdn, "generate_cdn_url", return_value=mock_url): + url = test_guild_obj.format_splash_url(fmt="nyaapeg", size=4000) + cdn.generate_cdn_url.assert_called_once_with( + "splashes", "265828729970753537", "0ff0ff0ff", fmt="nyaapeg", size=4000 + ) + assert url is mock_url + + def test_format_splash_returns_none(self, test_guild_obj): + test_guild_obj.splash_hash = None + with mock.patch.object(cdn, "generate_cdn_url", return_value=...): + url = test_guild_obj.format_splash_url() + cdn.generate_cdn_url.assert_not_called() + assert url is None + + def test_splash_url(self, test_guild_obj): + mock_url = "https://not-al" + with mock.patch.object(cdn, "generate_cdn_url", return_value=mock_url): + url = test_guild_obj.splash_url + cdn.generate_cdn_url.assert_called_once() + assert url is mock_url From 45385eb7a96289b9048ae7a91956328aae04bde8 Mon Sep 17 00:00:00 2001 From: davfsa Date: Sun, 29 Mar 2020 13:11:22 +0200 Subject: [PATCH 046/922] Fix snowflakes deserialization --- hikari/core/events.py | 26 ++++++++++++++------------ hikari/core/guilds.py | 20 ++++++++++---------- hikari/core/messages.py | 17 +++++++++-------- hikari/core/oauth2.py | 8 ++++---- hikari/core/webhooks.py | 4 ++-- tests/hikari/core/test_snowflake.py | 4 ++-- 6 files changed, 41 insertions(+), 38 deletions(-) diff --git a/hikari/core/events.py b/hikari/core/events.py index 03ee9155a4..adb5acae05 100644 --- a/hikari/core/events.py +++ b/hikari/core/events.py @@ -237,7 +237,9 @@ class BaseChannelEvent(HikariEvent, snowflakes.UniqueEntity, entities.Deserializ #: The ID of the guild this channel is in, will be ``None`` for DMs. #: #: :type: :obj:`snowflakes.Snowflake`, optional - guild_id: typing.Optional[snowflakes.Snowflake] = marshaller.attrib(deserializer=snowflakes.Snowflake, if_none=None) + guild_id: typing.Optional[snowflakes.Snowflake] = marshaller.attrib( + deserializer=snowflakes.Snowflake.deserialize, if_none=None + ) #: The sorting position of this channel, will be relative to the #: :attr:`parent_id` if set. @@ -686,7 +688,7 @@ class InviteDeleteEvent(HikariEvent, entities.Deserializable): #: The code of this invite. #: - #: :type: :obj:`snowflakes.Snowflake` + #: :type: :obj:`str` code: str = marshaller.attrib(deserializer=str) #: The ID of the guild this invite was deleted in. @@ -723,13 +725,13 @@ class MessageUpdateEvent(HikariEvent, snowflakes.UniqueEntity, entities.Deserial #: The ID of the channel that the message was sent in. #: #: :type: :obj:`snowflakes.Snowflake` - channel_id: snowflakes.Snowflake = marshaller.attrib(deserializer=snowflakes.Snowflake) + channel_id: snowflakes.Snowflake = marshaller.attrib(deserializer=snowflakes.Snowflake.deserialize) #: The ID of the guild that the message was sent in. #: #: :type: :obj:`typing.Union` [ :obj:`snowflakes.Snowflake`, :obj:`entities.UNSET` ] guild_id: typing.Union[snowflakes.Snowflake, entities.Unset] = marshaller.attrib( - deserializer=snowflakes.Snowflake, if_undefined=entities.Unset + deserializer=snowflakes.Snowflake.deserialize, if_undefined=entities.Unset ) #: The author of this message. @@ -784,7 +786,7 @@ class MessageUpdateEvent(HikariEvent, snowflakes.UniqueEntity, entities.Deserial #: :type: :obj:`typing.Union` [ :obj:`typing.Set` [ :obj:`snowflakes.Snowflake` ], :obj:`entities.UNSET` ] user_mentions: typing.Union[typing.Set[snowflakes.Snowflake], entities.Unset] = marshaller.attrib( raw_name="mentions", - deserializer=lambda user_mentions: {snowflakes.Snowflake(u["id"]) for u in user_mentions}, + deserializer=lambda user_mentions: {snowflakes.Snowflake.deserialize(u["id"]) for u in user_mentions}, if_undefined=entities.Unset, ) @@ -793,7 +795,7 @@ class MessageUpdateEvent(HikariEvent, snowflakes.UniqueEntity, entities.Deserial #: :type: :obj:`typing.Union` [ :obj:`typing.Set` [ :obj:`snowflakes.Snowflake` ], :obj:`entities.UNSET` ] role_mentions: typing.Union[typing.Set[snowflakes.Snowflake], entities.Unset] = marshaller.attrib( raw_name="mention_roles", - deserializer=lambda role_mentions: {snowflakes.Snowflake(r) for r in role_mentions}, + deserializer=lambda role_mentions: {snowflakes.Snowflake.deserialize(r) for r in role_mentions}, if_undefined=entities.Unset, ) @@ -802,7 +804,7 @@ class MessageUpdateEvent(HikariEvent, snowflakes.UniqueEntity, entities.Deserial #: :type: :obj:`typing.Union` [ :obj:`typing.Set` [ :obj:`snowflakes.Snowflake` ], :obj:`entities.UNSET` ] channel_mentions: typing.Union[typing.Set[snowflakes.Snowflake], entities.Unset] = marshaller.attrib( raw_name="mention_channels", - deserializer=lambda channel_mentions: {snowflakes.Snowflake(c["id"]) for c in channel_mentions}, + deserializer=lambda channel_mentions: {snowflakes.Snowflake.deserialize(c["id"]) for c in channel_mentions}, if_undefined=entities.Unset, ) @@ -840,7 +842,7 @@ class MessageUpdateEvent(HikariEvent, snowflakes.UniqueEntity, entities.Deserial #: #: :type: :obj:`typing.Union` [ :obj:`snowflakes.Snowflake`, :obj:`entities.UNSET` ] webhook_id: typing.Union[snowflakes.Snowflake, entities.Unset] = marshaller.attrib( - deserializer=snowflakes.Snowflake, if_undefined=entities.Unset + deserializer=snowflakes.Snowflake.deserialize, if_undefined=entities.Unset ) #: The message's type. @@ -957,7 +959,7 @@ class MessageReactionAddEvent(HikariEvent, entities.Deserializable): #: #: :type: :obj:`snowflakes.Snowflake`, optional guild_id: typing.Optional[snowflakes.Snowflake] = marshaller.attrib( - deserializer=snowflakes.Snowflake, if_undefined=None + deserializer=snowflakes.Snowflake.deserialize, if_undefined=None ) #: The member object of the user who's adding this reaction, if this is @@ -1001,7 +1003,7 @@ class MessageReactionRemoveEvent(HikariEvent, entities.Deserializable): #: #: :type: :obj:`snowflakes.Snowflake`, optional guild_id: typing.Optional[snowflakes.Snowflake] = marshaller.attrib( - deserializer=snowflakes.Snowflake, if_undefined=None + deserializer=snowflakes.Snowflake.deserialize, if_undefined=None ) #: The object of the emoji being removed. @@ -1033,7 +1035,7 @@ class MessageReactionRemoveAllEvent(HikariEvent, entities.Deserializable): #: #: :type: :obj:`snowflakes.Snowflake` guild_id: typing.Optional[snowflakes.Snowflake] = marshaller.attrib( - deserializer=snowflakes.Snowflake, if_undefined=None + deserializer=snowflakes.Snowflake.deserialize, if_undefined=None ) @@ -1053,7 +1055,7 @@ class MessageReactionRemoveEmojiEvent(HikariEvent, entities.Deserializable): #: #: :type: :obj:`snowflakes.Snowflake` guild_id: typing.Optional[snowflakes.Snowflake] = marshaller.attrib( - deserializer=snowflakes.Snowflake, if_undefined=None + deserializer=snowflakes.Snowflake.deserialize, if_undefined=None ) #: The ID of the message the reactions are being removed from. diff --git a/hikari/core/guilds.py b/hikari/core/guilds.py index 670a05d879..9bba57d2be 100644 --- a/hikari/core/guilds.py +++ b/hikari/core/guilds.py @@ -727,8 +727,8 @@ class GuildIntegration(snowflakes.UniqueEntity, entities.Deserializable): #: Whether users under this integration are allowed to use it's custom #: emojis. #: - #: - is_emojis_enabled: typing.Optional[snowflakes.Snowflake] = marshaller.attrib( + #: :type: :obj:`bool`, optional + is_emojis_enabled: typing.Optional[bool] = marshaller.attrib( raw_name="enable_emoticons", deserializer=bool, if_undefined=None, ) @@ -872,7 +872,7 @@ class Guild(PartialGuild): #: The ID of the owner of this guild. #: #: :type: :obj:`snowflakes.Snowflake` - owner_id: snowflakes.Snowflake = marshaller.attrib(deserializer=snowflakes.Snowflake) + owner_id: snowflakes.Snowflake = marshaller.attrib(deserializer=snowflakes.Snowflake.deserialize) #: The guild level permissions that apply to the bot user, #: Will be ``None`` when this object is retrieved through a REST request @@ -893,7 +893,7 @@ class Guild(PartialGuild): #: #: :type: :obj:`snowflakes.Snowflake`, optional afk_channel_id: typing.Optional[snowflakes.Snowflake] = marshaller.attrib( - deserializer=snowflakes.Snowflake, if_none=None + deserializer=snowflakes.Snowflake.deserialize, if_none=None ) #: How long a voice user has to be AFK for before they are classed as being @@ -919,7 +919,7 @@ class Guild(PartialGuild): #: #: :type: :obj:`snowflakes.Snowflake`, optional embed_channel_id: typing.Optional[snowflakes.Snowflake] = marshaller.attrib( - deserializer=snowflakes.Snowflake, if_none=None, if_undefined=None + deserializer=snowflakes.Snowflake.deserialize, if_none=None, if_undefined=None ) #: The verification level required for a user to participate in this guild. @@ -966,7 +966,7 @@ class Guild(PartialGuild): #: #: :type: :obj:`snowflakes.Snowflake`, optional application_id: typing.Optional[snowflakes.Snowflake] = marshaller.attrib( - deserializer=snowflakes.Snowflake, if_none=None + deserializer=snowflakes.Snowflake.deserialize, if_none=None ) #: Whether the guild is unavailable or not. @@ -995,7 +995,7 @@ class Guild(PartialGuild): #: #: :type: :obj:`snowflakes.Snowflake`, optional widget_channel_id: typing.Optional[snowflakes.Snowflake] = marshaller.attrib( - if_undefined=None, if_none=None, deserializer=snowflakes.Snowflake + if_undefined=None, if_none=None, deserializer=snowflakes.Snowflake.deserialize ) #: The ID of the system channel (where welcome messages and Nitro boost @@ -1003,7 +1003,7 @@ class Guild(PartialGuild): #: #: :type: :obj:`snowflakes.Snowflake`, optional system_channel_id: typing.Optional[snowflakes.Snowflake] = marshaller.attrib( - if_none=None, deserializer=snowflakes.Snowflake + if_none=None, deserializer=snowflakes.Snowflake.deserialize ) #: Flags for the guild system channel to describe which notification @@ -1018,7 +1018,7 @@ class Guild(PartialGuild): #: #: :type: :obj:`snowflakes.Snowflake`, optional rules_channel_id: typing.Optional[snowflakes.Snowflake] = marshaller.attrib( - if_none=None, deserializer=snowflakes.Snowflake + if_none=None, deserializer=snowflakes.Snowflake.deserialize ) #: The date and time that the bot user joined this guild. @@ -1182,7 +1182,7 @@ class Guild(PartialGuild): #: #: :type: :obj:`snowflakes.Snowflake`, optional public_updates_channel_id: typing.Optional[snowflakes.Snowflake] = marshaller.attrib( - if_none=None, deserializer=snowflakes.Snowflake + if_none=None, deserializer=snowflakes.Snowflake.deserialize ) def format_splash_url(self, fmt: str = "png", size: int = 2048) -> typing.Optional[str]: diff --git a/hikari/core/messages.py b/hikari/core/messages.py index 72d5716b7c..a037a32d9b 100644 --- a/hikari/core/messages.py +++ b/hikari/core/messages.py @@ -189,13 +189,13 @@ class MessageCrosspost(entities.HikariEntity, entities.Deserializable): #: #: :type: :obj:`snowflakes.Snowflake`, optional message_id: typing.Optional[snowflakes.Snowflake] = marshaller.attrib( - deserializer=snowflakes.Snowflake, if_undefined=None + deserializer=snowflakes.Snowflake.deserialize, if_undefined=None ) #: The ID of the channel that the message originated from. #: #: :type: :obj:`snowflakes.Snowflake` - channel_id: snowflakes.Snowflake = marshaller.attrib(deserializer=snowflakes.Snowflake) + channel_id: snowflakes.Snowflake = marshaller.attrib(deserializer=snowflakes.Snowflake.deserialize) #: The ID of the guild that the message originated from. #: @@ -206,7 +206,7 @@ class MessageCrosspost(entities.HikariEntity, entities.Deserializable): #: #: :type: :obj:`snowflakes.Snowflake`, optional guild_id: typing.Optional[snowflakes.Snowflake] = marshaller.attrib( - deserializer=snowflakes.Snowflake, if_undefined=None + deserializer=snowflakes.Snowflake.deserialize, if_undefined=None ) @@ -217,13 +217,13 @@ class Message(snowflakes.UniqueEntity, entities.Deserializable): #: The ID of the channel that the message was sent in. #: #: :type: :obj:`snowflakes.Snowflake` - channel_id: snowflakes.Snowflake = marshaller.attrib(deserializer=snowflakes.Snowflake) + channel_id: snowflakes.Snowflake = marshaller.attrib(deserializer=snowflakes.Snowflake.deserialize) #: The ID of the guild that the message was sent in. #: #: :type: :obj:`snowflakes.Snowflake`, optional guild_id: typing.Optional[snowflakes.Snowflake] = marshaller.attrib( - deserializer=snowflakes.Snowflake, if_undefined=None + deserializer=snowflakes.Snowflake.deserialize, if_undefined=None ) #: The author of this message. @@ -269,7 +269,8 @@ class Message(snowflakes.UniqueEntity, entities.Deserializable): #: #: :type: :obj:`typing.Set` [ :obj:`snowflakes.Snowflake` ] user_mentions: typing.Set[snowflakes.Snowflake] = marshaller.attrib( - raw_name="mentions", deserializer=lambda user_mentions: {snowflakes.Snowflake(u["id"]) for u in user_mentions} + raw_name="mentions", + deserializer=lambda user_mentions: {snowflakes.Snowflake.deserialize(u["id"]) for u in user_mentions}, ) #: The roles the message mentions. @@ -285,7 +286,7 @@ class Message(snowflakes.UniqueEntity, entities.Deserializable): #: :type: :obj:`typing.Set` [ :obj:`snowflakes.Snowflake` ] channel_mentions: typing.Set[snowflakes.Snowflake] = marshaller.attrib( raw_name="mention_channels", - deserializer=lambda channel_mentions: {snowflakes.Snowflake(c["id"]) for c in channel_mentions}, + deserializer=lambda channel_mentions: {snowflakes.Snowflake.deserialize(c["id"]) for c in channel_mentions}, if_undefined=dict, ) @@ -319,7 +320,7 @@ class Message(snowflakes.UniqueEntity, entities.Deserializable): #: #: :type: :obj:`snowflakes.Snowflake`, optional webhook_id: typing.Optional[snowflakes.Snowflake] = marshaller.attrib( - deserializer=snowflakes.Snowflake, if_undefined=None + deserializer=snowflakes.Snowflake.deserialize, if_undefined=None ) #: The message type. diff --git a/hikari/core/oauth2.py b/hikari/core/oauth2.py index 08ceb8dec7..25eb2a58b8 100644 --- a/hikari/core/oauth2.py +++ b/hikari/core/oauth2.py @@ -79,7 +79,7 @@ class TeamMember(entities.HikariEntity, entities.Deserializable): #: The ID of the team this member belongs to. #: #: :type: :obj:`snowflakes.Snowflake` - team_id: snowflakes.Snowflake = marshaller.attrib(deserializer=snowflakes.Snowflake) + team_id: snowflakes.Snowflake = marshaller.attrib(deserializer=snowflakes.Snowflake.deserialize) #: The user object of this team member. #: @@ -106,7 +106,7 @@ class Team(snowflakes.UniqueEntity, entities.Deserializable): #: The snowflake ID of this team's owner. #: #: :type: :obj:`snowflakes.Snowflake` - owner_user_id: snowflakes.Snowflake = marshaller.attrib(deserializer=snowflakes.Snowflake) + owner_user_id: snowflakes.Snowflake = marshaller.attrib(deserializer=snowflakes.Snowflake.deserialize) @property def icon_url(self) -> typing.Optional[str]: @@ -220,14 +220,14 @@ class Application(snowflakes.UniqueEntity, entities.Deserializable): #: #: :type: :obj:`snowflakes.Snowflake`, optional guild_id: typing.Optional[snowflakes.Snowflake] = marshaller.attrib( - deserializer=snowflakes.Snowflake, if_undefined=None + deserializer=snowflakes.Snowflake.deserialize, if_undefined=None ) #: The ID of the primary "Game SKU" of a game that's sold on Discord. #: #: :type: :obj:`snowflakes.Snowflake`, optional primary_sku_id: typing.Optional[snowflakes.Snowflake] = marshaller.attrib( - deserializer=snowflakes.Snowflake, if_undefined=None + deserializer=snowflakes.Snowflake.deserialize, if_undefined=None ) #: The url slug that links to this application's store page diff --git a/hikari/core/webhooks.py b/hikari/core/webhooks.py index cf11c5d2d8..d05ab52e26 100644 --- a/hikari/core/webhooks.py +++ b/hikari/core/webhooks.py @@ -49,13 +49,13 @@ class Webhook(snowflakes.UniqueEntity, entities.Deserializable): #: #: :type: :obj:`snowflakes.Snowflake`, optional guild_id: typing.Optional[snowflakes.Snowflake] = marshaller.attrib( - deserializer=snowflakes.Snowflake, if_undefined=None + deserializer=snowflakes.Snowflake.deserialize, if_undefined=None ) #: The channel ID this webhook is for. #: #: :type: :obj:`snowflakes.Snowflake` - channel_id: snowflakes.Snowflake = marshaller.attrib(deserializer=snowflakes.Snowflake) + channel_id: snowflakes.Snowflake = marshaller.attrib(deserializer=snowflakes.Snowflake.deserialize) #: The user that created the webhook #: diff --git a/tests/hikari/core/test_snowflake.py b/tests/hikari/core/test_snowflake.py index 918e18cb98..be6b89fb8b 100644 --- a/tests/hikari/core/test_snowflake.py +++ b/tests/hikari/core/test_snowflake.py @@ -30,7 +30,7 @@ def raw_id(self): @pytest.fixture() def neko_snowflake(self, raw_id): - return snowflakes.Snowflake(raw_id) + return snowflakes.Snowflake.deserialize(raw_id) def test_created_at(self, neko_snowflake): assert neko_snowflake.created_at == datetime.datetime(2019, 1, 22, 18, 41, 15, 283_000).replace( @@ -60,7 +60,7 @@ def test_repr_cast(self, neko_snowflake, raw_id): def test_eq(self, neko_snowflake, raw_id): assert neko_snowflake == raw_id - assert neko_snowflake == snowflakes.Snowflake(raw_id) + assert neko_snowflake == snowflakes.Snowflake.deserialize(raw_id) assert str(raw_id) != neko_snowflake def test_lt(self, neko_snowflake, raw_id): From 4070864456fa7f56a25f38e35033ceb5fbbbadd4 Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Sun, 29 Mar 2020 13:28:52 +0100 Subject: [PATCH 047/922] Added code to run a gateway client from command line. --- .coveragerc | 1 + hikari/__main__.py | 21 +++---- hikari/core/__init__.py | 21 +------ hikari/core/clients/__init__.py | 49 +++++++++++++++ hikari/core/clients/_run_gateway.py | 63 ++++++++++++++++++++ hikari/core/{ => clients}/app_config.py | 3 +- hikari/core/{ => clients}/bot_client.py | 4 +- hikari/core/clients/gateway.py | 0 hikari/core/{ => clients}/gateway_client.py | 15 ++--- hikari/core/{ => clients}/gateway_config.py | 4 +- hikari/core/{ => clients}/http_client.py | 1 + hikari/core/{ => clients}/http_config.py | 2 +- hikari/core/{ => clients}/protocol_config.py | 0 hikari/core/{ => clients}/shard_client.py | 35 +++++++++-- hikari/core/events.py | 4 +- hikari/core/guilds.py | 4 +- requirements.txt | 2 +- 17 files changed, 175 insertions(+), 54 deletions(-) create mode 100644 hikari/core/clients/__init__.py create mode 100644 hikari/core/clients/_run_gateway.py rename hikari/core/{ => clients}/app_config.py (98%) rename hikari/core/{ => clients}/bot_client.py (96%) delete mode 100644 hikari/core/clients/gateway.py rename hikari/core/{ => clients}/gateway_client.py (94%) rename hikari/core/{ => clients}/gateway_config.py (99%) rename hikari/core/{ => clients}/http_client.py (98%) rename hikari/core/{ => clients}/http_config.py (97%) rename hikari/core/{ => clients}/protocol_config.py (100%) rename hikari/core/{ => clients}/shard_client.py (91%) diff --git a/.coveragerc b/.coveragerc index 35bd6a4d11..8fdf800763 100644 --- a/.coveragerc +++ b/.coveragerc @@ -6,6 +6,7 @@ branch = true # timid = true omit = hikari/__main__.py + hikari/core/clients/_run_gateway.py [report] precision = 2 diff --git a/hikari/__main__.py b/hikari/__main__.py index d846847629..14cb76005c 100644 --- a/hikari/__main__.py +++ b/hikari/__main__.py @@ -16,19 +16,16 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . - -import argparse import platform +import click + import hikari -parser = argparse.ArgumentParser(prog=hikari.__name__, allow_abbrev=False) -parser.add_argument( - "-V", - "--version", - action="version", - version=f"{hikari.__name__} {hikari.__version__} from {hikari.__path__[0]} (python {platform.python_version()})", - help=f"show {hikari.__name__}'s version and exit", -) -parser.set_defaults(func=parser.print_help) -parser.parse_args().func() + +@click.command() +def main(): + print(f"{hikari.__name__} {hikari.__version__} from {hikari.__path__[0]} (python {platform.python_version()})") + + +main() diff --git a/hikari/core/__init__.py b/hikari/core/__init__.py index a94159e174..fed89f3a3d 100644 --- a/hikari/core/__init__.py +++ b/hikari/core/__init__.py @@ -19,66 +19,51 @@ """The core API for interacting with Discord directly.""" # Do I need this? It still resolves without adding these in...? -from hikari.core import app_config from hikari.core import channels +from hikari.core import clients from hikari.core import entities from hikari.core import events -from hikari.core import gateway_client -from hikari.core import gateway_config from hikari.core import gateway_entities from hikari.core import guilds -from hikari.core import http_config from hikari.core import invites from hikari.core import messages from hikari.core import oauth2 from hikari.core import permissions -from hikari.core import protocol_config -from hikari.core import shard_client from hikari.core import snowflakes from hikari.core import users from hikari.core import webhooks # Import everything into this namespace. -from hikari.core.app_config import * from hikari.core.channels import * +from hikari.core.clients import * from hikari.core.colors import * from hikari.core.colours import * from hikari.core.embeds import * from hikari.core.emojis import * from hikari.core.entities import * from hikari.core.events import * -from hikari.core.gateway_client import * -from hikari.core.gateway_config import * from hikari.core.gateway_entities import * from hikari.core.guilds import * -from hikari.core.http_config import * from hikari.core.invites import * from hikari.core.messages import * from hikari.core.oauth2 import * from hikari.core.permissions import * -from hikari.core.protocol_config import * -from hikari.core.shard_client import * from hikari.core.snowflakes import * from hikari.core.users import * from hikari.core.voices import * from hikari.core.webhooks import * __all__ = [ - *app_config.__all__, *channels.__all__, + *clients.__all__, *entities.__all__, *events.__all__, - *gateway_client.__all__, *gateway_entities.__all__, - *gateway_config.__all__, *guilds.__all__, - *http_config.__all__, *invites.__all__, *messages.__all__, *oauth2.__all__, *permissions.__all__, - *protocol_config.__all__, - *shard_client.__all__, *snowflakes.__all__, *users.__all__, *webhooks.__all__, diff --git a/hikari/core/clients/__init__.py b/hikari/core/clients/__init__.py new file mode 100644 index 0000000000..b5754ba30e --- /dev/null +++ b/hikari/core/clients/__init__.py @@ -0,0 +1,49 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# Copyright © Nekokatt 2019-2020 +# +# This file is part of Hikari. +# +# Hikari is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Hikari is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Hikari. If not, see . +"""The core API for interacting with Discord directly.""" + +from hikari.core.clients import app_config +from hikari.core.clients import bot_client +from hikari.core.clients import gateway_client +from hikari.core.clients import gateway_config +from hikari.core.clients import http_client +from hikari.core.clients import http_config +from hikari.core.clients import protocol_config +from hikari.core.clients import shard_client + +from hikari.core.clients.app_config import * +from hikari.core.clients.bot_client import * +from hikari.core.clients.gateway_client import * +from hikari.core.clients.gateway_config import * +from hikari.core.clients.http_client import * +from hikari.core.clients.http_config import * +from hikari.core.clients.protocol_config import * +from hikari.core.clients.shard_client import * + + +__all__ = [ + *app_config.__all__, + *bot_client.__all__, + *gateway_client.__all__, + *gateway_config.__all__, + *http_client.__all__, + *http_config.__all__, + *protocol_config.__all__, + *shard_client.__all__, +] diff --git a/hikari/core/clients/_run_gateway.py b/hikari/core/clients/_run_gateway.py new file mode 100644 index 0000000000..3d0329240c --- /dev/null +++ b/hikari/core/clients/_run_gateway.py @@ -0,0 +1,63 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# Copyright © Nekoka.tt 2019-2020 +# +# This file is part of Hikari. +# +# Hikari is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Hikari is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Hikari. If not, see . +"""An executable module to be used to test that the gateway works as intended. + +This is only for use by developers of this library, regular users do not need +to use this. +""" +import logging + + +import click + +from hikari.core.clients import gateway_client +from hikari.core.clients import gateway_config +from hikari.core.clients import protocol_config + +logger_levels = ("DEBUG", "INFO", "WARNING", "ERROR", "NOTSET") + + +@click.command() +@click.option("--compression", default=True, type=click.BOOL, help="Enable or disable gateway compression.") +@click.option("--debug", default=False, type=click.BOOL, help="Enable or disable debug mode.") +@click.option("--logger", default="INFO", type=click.Choice(logger_levels), help="Logger verbosity.") +@click.option("--shards", default=1, type=click.IntRange(min=1), help="The number of shards to explicitly use.") +@click.option("--token", required=True, envvar="TOKEN", help="The token to use to authenticate with Discord.") +@click.option("--url", default="wss://gateway.discord.gg/", help="The websocket URL to connect to.") +@click.option("--verify-ssl", default=True, type=click.BOOL, help="Enable or disable SSL verification.") +@click.option("--version", default=7, type=click.IntRange(min=6), help="Version of the gateway to use.") +def run_gateway(compression, debug, logger, shards, token, url, verify_ssl, version): + logging.basicConfig(level=logger) + + client = gateway_client.GatewayClient( + config=gateway_config.GatewayConfig( + debug=debug, + protocol=protocol_config.HTTPProtocolConfig(verify_ssl=verify_ssl), + shard_config=gateway_config.ShardConfig(shard_count=shards), + token=token, + use_compression=compression, + version=version, + ), + url=url, + ) + + client.run() + + +run_gateway() diff --git a/hikari/core/app_config.py b/hikari/core/clients/app_config.py similarity index 98% rename from hikari/core/app_config.py rename to hikari/core/clients/app_config.py index 8e5aa1a2a6..a2f0f7513a 100644 --- a/hikari/core/app_config.py +++ b/hikari/core/clients/app_config.py @@ -22,8 +22,7 @@ import typing from hikari.core import entities -from hikari.core import gateway_config -from hikari.core import http_config +from hikari.core.clients import http_config, gateway_config from hikari.internal_utilities import marshaller diff --git a/hikari/core/bot_client.py b/hikari/core/clients/bot_client.py similarity index 96% rename from hikari/core/bot_client.py rename to hikari/core/clients/bot_client.py index 224a608a55..2dc5f05140 100644 --- a/hikari/core/bot_client.py +++ b/hikari/core/clients/bot_client.py @@ -16,6 +16,4 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . -"""Application client. - -""" +__all__ = [] diff --git a/hikari/core/clients/gateway.py b/hikari/core/clients/gateway.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/hikari/core/gateway_client.py b/hikari/core/clients/gateway_client.py similarity index 94% rename from hikari/core/gateway_client.py rename to hikari/core/clients/gateway_client.py index 895eb13971..5a694dda65 100644 --- a/hikari/core/gateway_client.py +++ b/hikari/core/clients/gateway_client.py @@ -25,15 +25,12 @@ from hikari.core import dispatcher from hikari.core import events -from hikari.core import gateway_config -from hikari.core import shard_client +from hikari.core.clients import gateway_config +from hikari.core.clients import shard_client from hikari.core import state -from hikari.core.dispatcher import EventCallbackT -from hikari.core.dispatcher import EventT from hikari.internal_utilities import loggers from hikari.net import shard - ShardT = typing.TypeVar("ShardT", bound=shard_client.ShardClient) @@ -162,10 +159,14 @@ def predicate(member): return types - def add_listener(self, event_type: typing.Type[EventT], callback: EventCallbackT) -> EventCallbackT: + def add_listener( + self, event_type: typing.Type[dispatcher.EventT], callback: dispatcher.EventCallbackT + ) -> dispatcher.EventCallbackT: return self.event_dispatcher.add_listener(event_type, callback) - def remove_listener(self, event_type: typing.Type[EventT], callback: EventCallbackT) -> EventCallbackT: + def remove_listener( + self, event_type: typing.Type[dispatcher.EventT], callback: dispatcher.EventCallbackT + ) -> dispatcher.EventCallbackT: return self.event_dispatcher.remove_listener(event_type, callback) def dispatch_event(self, event: events.HikariEvent) -> ...: diff --git a/hikari/core/gateway_config.py b/hikari/core/clients/gateway_config.py similarity index 99% rename from hikari/core/gateway_config.py rename to hikari/core/clients/gateway_config.py index edac8cd0a1..cb12c66102 100644 --- a/hikari/core/gateway_config.py +++ b/hikari/core/clients/gateway_config.py @@ -25,7 +25,7 @@ from hikari.core import entities from hikari.core import gateway_entities -from hikari.core import protocol_config +from hikari.core.clients import protocol_config from hikari.internal_utilities import assertions from hikari.internal_utilities import dates from hikari.internal_utilities import marshaller @@ -94,7 +94,7 @@ class ShardConfig(entities.HikariEntity, entities.Deserializable): def __init__(self, *, shard_ids: typing.Optional[typing.Iterable[int]] = None, shard_count: int) -> None: self.shard_ids = [*shard_ids] if shard_ids else [*range(shard_count)] - for shard_id in shard_ids: + for shard_id in self.shard_ids: assertions.assert_that(shard_id < shard_count, "shard_count must be greater than any shard ids") self.shard_count = shard_count diff --git a/hikari/core/http_client.py b/hikari/core/clients/http_client.py similarity index 98% rename from hikari/core/http_client.py rename to hikari/core/clients/http_client.py index 1c1502a5ca..2dc5f05140 100644 --- a/hikari/core/http_client.py +++ b/hikari/core/clients/http_client.py @@ -16,3 +16,4 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . +__all__ = [] diff --git a/hikari/core/http_config.py b/hikari/core/clients/http_config.py similarity index 97% rename from hikari/core/http_config.py rename to hikari/core/clients/http_config.py index cb10b3a2aa..b2107c87ed 100644 --- a/hikari/core/http_config.py +++ b/hikari/core/clients/http_config.py @@ -23,7 +23,7 @@ import typing from hikari.core import entities -from hikari.core import protocol_config +from hikari.core.clients import protocol_config from hikari.internal_utilities import marshaller diff --git a/hikari/core/protocol_config.py b/hikari/core/clients/protocol_config.py similarity index 100% rename from hikari/core/protocol_config.py rename to hikari/core/clients/protocol_config.py diff --git a/hikari/core/shard_client.py b/hikari/core/clients/shard_client.py similarity index 91% rename from hikari/core/shard_client.py rename to hikari/core/clients/shard_client.py index 2e2d701671..1b7028551a 100644 --- a/hikari/core/shard_client.py +++ b/hikari/core/clients/shard_client.py @@ -31,10 +31,11 @@ import aiohttp from hikari.core import events -from hikari.core import gateway_config +from hikari.core.clients import gateway_config from hikari.core import gateway_entities from hikari.internal_utilities import aio from hikari.internal_utilities import loggers +from hikari.net import codes from hikari.net import errors from hikari.net import ratelimits from hikari.net import shard @@ -255,9 +256,22 @@ async def start(self): self._shard_state = ShardState.INITIALIZING self._task = asyncio.create_task(self._keep_alive()) self.logger.info("waiting for READY") - await self._client.identify_event.wait() + completed, _ = await asyncio.wait( + [self._task, self._client.identify_event.wait()], return_when=asyncio.FIRST_COMPLETED + ) + + if self._task in completed: + raise self._task.exception() + self._shard_state = ShardState.WAITING_FOR_READY - await self._client.ready_event.wait() + + completed, _ = await asyncio.wait( + [self._task, self._client.ready_event.wait()], return_when=asyncio.FIRST_COMPLETED + ) + + if self._task in completed: + raise self._task.exception() + self.logger.info("now READY") self._shard_state = ShardState.READY @@ -329,8 +343,19 @@ async def _keep_alive(self): self._client.session_id = None do_not_back_off = True await asyncio.sleep(5) - except errors.GatewayServerClosedConnectionError: - self.logger.warning("disconnected by Discord, will attempt to reconnect") + except errors.GatewayServerClosedConnectionError as ex: + if ex.close_code in ( + codes.GatewayCloseCode.RATE_LIMITED, + codes.GatewayCloseCode.SESSION_TIMEOUT, + codes.GatewayCloseCode.INVALID_SEQ, + codes.GatewayCloseCode.UNKNOWN_ERROR, + codes.GatewayCloseCode.SESSION_TIMEOUT, + codes.GatewayCloseCode.NORMAL_CLOSURE, + ): + self.logger.warning("disconnected by Discord, will attempt to reconnect") + else: + self.logger.error("disconnected by Discord, %s: %s", type(ex).__name__, ex.reason) + raise ex except errors.GatewayClientClosedError: self.logger.warning("shutting down") return diff --git a/hikari/core/events.py b/hikari/core/events.py index 03ee9155a4..3180f76b1b 100644 --- a/hikari/core/events.py +++ b/hikari/core/events.py @@ -394,7 +394,7 @@ class ChannelPinUpdateEvent(HikariEvent, entities.Deserializable): @mark_as_websocket_event @marshaller.attrs(slots=True) -class GuildCreateEvent(HikariEvent, guilds.Guild): +class GuildCreateEvent(HikariEvent, entities.Deserializable): # fixme """Used to represent Guild Create gateway events. Will be received when the bot joins a guild, and when a guild becomes @@ -404,7 +404,7 @@ class GuildCreateEvent(HikariEvent, guilds.Guild): @mark_as_websocket_event @marshaller.attrs(slots=True) -class GuildUpdateEvent(HikariEvent, guilds.Guild): +class GuildUpdateEvent(HikariEvent, entities.Deserializable): # fixme """Used to represent Guild Update gateway events.""" diff --git a/hikari/core/guilds.py b/hikari/core/guilds.py index 670a05d879..2b24264e81 100644 --- a/hikari/core/guilds.py +++ b/hikari/core/guilds.py @@ -467,7 +467,9 @@ class PresenceActivity(entities.HikariEntity, entities.Deserializable): #: end, if applicable. #: #: :type: :obj:`ActivityTimestamps`, optional - timestamps: ActivityTimestamps = marshaller.attrib(deserializer=ActivityTimestamps.deserialize) + timestamps: typing.Optional[ActivityTimestamps] = marshaller.attrib( + deserializer=ActivityTimestamps.deserialize, if_undefined=None + ) #: The ID of the application this activity is for, if applicable. #: diff --git a/requirements.txt b/requirements.txt index d83fb8060d..7a9f55048d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,3 @@ aiohttp==3.6.2 attrs==19.3.0 -typing_inspect==0.5.0 +click==7.1.1 From 9f1ac4c36164af0f3fa07ca22e47a4b0d029acf6 Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Sun, 29 Mar 2020 12:57:54 +0000 Subject: [PATCH 048/922] Update noxfile.py --- noxfile.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/noxfile.py b/noxfile.py index 25781c9115..b27e34630f 100644 --- a/noxfile.py +++ b/noxfile.py @@ -186,7 +186,7 @@ def pip_bdist_wheel(session: nox.sessions.Session): print(f"copying newest wheel found at {newest_wheel} and installing it in temp dir") shutil.copyfile(newest_wheel, newest_wheel_name) session.run("pip", "install", newest_wheel_name) - session.run("python", "-m", MAIN_PACKAGE, "-V") + session.run("python", "-m", MAIN_PACKAGE) print("Installed as wheel in temporary environment successfully!") @@ -223,7 +223,7 @@ def pip_sdist(session: nox.sessions.Session): print("installing sdist") with temp_chdir(session, newest_tarball_dir): session.run("python", "setup.py", "install") - session.run("python", "-m", MAIN_PACKAGE, "-V") + session.run("python", "-m", MAIN_PACKAGE) print("Installed as wheel in temporary environment successfully!") @@ -246,7 +246,7 @@ def pip_git(session: nox.sessions.Session): with temp_chdir(session, temp_dir) as project_dir: session.install(f"git+file://{project_dir}") session.install(MAIN_PACKAGE) - session.run("python", "-m", MAIN_PACKAGE, "-V") + session.run("python", "-m", MAIN_PACKAGE) print("Installed as git dir in temporary environment successfully!") From 5c36ea25229b923afe4652261a1ec4bec363ecc0 Mon Sep 17 00:00:00 2001 From: Faster Speeding Date: Sun, 29 Mar 2020 15:13:28 +0100 Subject: [PATCH 049/922] Fix some model deserialization. --- hikari/core/embeds.py | 2 +- hikari/core/events.py | 13 ++++++++----- hikari/core/guilds.py | 11 +++++++---- hikari/core/voices.py | 2 +- hikari/internal_utilities/marshaller.py | 12 +++++++----- 5 files changed, 24 insertions(+), 16 deletions(-) diff --git a/hikari/core/embeds.py b/hikari/core/embeds.py index bfd29f7ffc..5fe9a1097b 100644 --- a/hikari/core/embeds.py +++ b/hikari/core/embeds.py @@ -141,7 +141,7 @@ class EmbedProvider(entities.HikariEntity, entities.Deserializable, entities.Ser #: The url of the provider. #: #: :type: :obj:`str`, optional - url: typing.Optional[str] = marshaller.attrib(deserializer=str, serializer=str, if_undefined=None) + url: typing.Optional[str] = marshaller.attrib(deserializer=str, serializer=str, if_undefined=None, if_none=None) @marshaller.attrs(slots=True) diff --git a/hikari/core/events.py b/hikari/core/events.py index 7920123abf..31a9ccbe1f 100644 --- a/hikari/core/events.py +++ b/hikari/core/events.py @@ -301,7 +301,8 @@ class BaseChannelEvent(HikariEvent, snowflakes.UniqueEntity, entities.Deserializ #: #: :type: :obj:`Typing.MutableMapping` [ :obj:`snowflakes.Snowflake`, :obj:`users.User` ], optional recipients: typing.Optional[typing.MutableMapping[snowflakes.Snowflake, users.User]] = marshaller.attrib( - deserializer=lambda recipients: {user.id: user for user in map(users.User.deserialize, recipients)} + deserializer=lambda recipients: {user.id: user for user in map(users.User.deserialize, recipients)}, + if_undefined=None, ) #: The hash of this channel's icon, if it's a group DM channel and is set. @@ -396,7 +397,7 @@ class ChannelPinUpdateEvent(HikariEvent, entities.Deserializable): @mark_as_websocket_event @marshaller.attrs(slots=True) -class GuildCreateEvent(HikariEvent, entities.Deserializable): # fixme +class GuildCreateEvent(HikariEvent, entities.Deserializable): """Used to represent Guild Create gateway events. Will be received when the bot joins a guild, and when a guild becomes @@ -406,7 +407,7 @@ class GuildCreateEvent(HikariEvent, entities.Deserializable): # fixme @mark_as_websocket_event @marshaller.attrs(slots=True) -class GuildUpdateEvent(HikariEvent, entities.Deserializable): # fixme +class GuildUpdateEvent(HikariEvent, entities.Deserializable): """Used to represent Guild Update gateway events.""" @@ -656,12 +657,14 @@ class InviteCreateEvent(HikariEvent, entities.Deserializable): #: The object of the user who this invite targets, if set. #: #: :type: :obj:`users.User`, optional - target_user: typing.Optional[users.User] = marshaller.attrib(deserializer=users.User.deserialize) + target_user: typing.Optional[users.User] = marshaller.attrib(deserializer=users.User.deserialize, if_undefined=None) #: The type of user target this invite is, if applicable. #: #: :type: :obj:`invites.TargetUserType`, optional - target_user_type: typing.Optional[invites.TargetUserType] = marshaller.attrib(deserializer=invites.TargetUserType) + target_user_type: typing.Optional[invites.TargetUserType] = marshaller.attrib( + deserializer=invites.TargetUserType, if_undefined=None + ) #: Whether this invite grants temporary membership. #: diff --git a/hikari/core/guilds.py b/hikari/core/guilds.py index d5f296e251..fd8b8c069b 100644 --- a/hikari/core/guilds.py +++ b/hikari/core/guilds.py @@ -223,15 +223,18 @@ class GuildVerificationLevel(enum.IntEnum): class GuildMember(entities.HikariEntity, entities.Deserializable): """Used to represent a guild bound member.""" - #: This member's user object. + #: This member's user object, will be :obj:`None` when attached to Message + #: Create and Update gateway events. #: - #: :type: :obj:`users.User` - user: users.User = marshaller.attrib(deserializer=users.User.deserialize) + #: :type: :obj:`users.User`, optional + user: typing.Optional[users.User] = marshaller.attrib(deserializer=users.User.deserialize, if_undefined=None) #: This member's nickname, if set. #: #: :type: :obj:`str`, optional - nickname: typing.Optional[str] = marshaller.attrib(raw_name="nick", deserializer=str, if_none=None) + nickname: typing.Optional[str] = marshaller.attrib( + raw_name="nick", deserializer=str, if_none=None, if_undefined=None, + ) #: A sequence of the IDs of the member's current roles. #: diff --git a/hikari/core/voices.py b/hikari/core/voices.py index 9003091259..0961dbc1be 100644 --- a/hikari/core/voices.py +++ b/hikari/core/voices.py @@ -42,7 +42,7 @@ class VoiceState(entities.HikariEntity, entities.Deserializable): #: The ID of the channel this user is connected to. #: #: :type: :obj:`.core.snowflakes.Snowflake`, optional - channel_id: snowflakes.Snowflake = marshaller.attrib(deserializer=snowflakes.Snowflake.deserialize) + channel_id: snowflakes.Snowflake = marshaller.attrib(deserializer=snowflakes.Snowflake.deserialize, if_none=None) #: The ID of the user this voice state is for. #: diff --git a/hikari/internal_utilities/marshaller.py b/hikari/internal_utilities/marshaller.py index 75c51fc029..cfdccf0463 100644 --- a/hikari/internal_utilities/marshaller.py +++ b/hikari/internal_utilities/marshaller.py @@ -344,8 +344,9 @@ def deserialize(self, raw_data: typing.Mapping[str, typing.Any], target_type: ty if a.raw_name not in raw_data: if a.if_undefined is RAISE: raise AttributeError( - f"Required field {a.field_name} (from raw {a.raw_name}) is not specified in the input " - f"payload\n\n{raw_data}" + "Failed to deserialize data to instance of " + f"{target_type.__module__}.{target_type.__qualname__} due to required field {a.field_name} " + f"(from raw key {a.raw_name}) not being included in the input payload\n\n{raw_data}" ) if a.if_undefined in PASSED_THROUGH_SINGLETONS: kwargs[kwarg_name] = a.if_undefined @@ -355,8 +356,9 @@ def deserialize(self, raw_data: typing.Mapping[str, typing.Any], target_type: ty elif (data := raw_data[a.raw_name]) is None: if a.if_none is RAISE: raise AttributeError( - f"Non-nullable field {a.field_name} (from raw {a.raw_name}) is `None` in the input " - f"payload\n\n{raw_data}" + "Failed to deserialize data to instance of " + f"{target_type.__module__}.{target_type.__qualname__} due to non-nullable field {a.field_name}" + f" (from raw key {a.raw_name}) being `None` in the input payload\n\n{raw_data}" ) if a.if_none in PASSED_THROUGH_SINGLETONS: kwargs[kwarg_name] = a.if_none @@ -371,7 +373,7 @@ def deserialize(self, raw_data: typing.Mapping[str, typing.Any], target_type: ty raise TypeError( "Failed to deserialize data to instance of " f"{target_type.__module__}.{target_type.__qualname__} because marshalling failed on " - f"attribute {a.field_name} (passed to constructor as {kwarg_name}" + f"attribute {a.field_name} (passed to constructor as {kwarg_name})" ) from exc return target_type(**kwargs) From 49b323a8ec85779c40464bbc146b27575f102b9c Mon Sep 17 00:00:00 2001 From: davfsa Date: Sun, 29 Mar 2020 20:08:50 +0200 Subject: [PATCH 050/922] Added channel models + tests --- hikari/core/channels.py | 243 +++++++++++++++++++--- hikari/core/events.py | 6 +- hikari/core/guilds.py | 52 +---- hikari/internal_utilities/marshaller.py | 4 +- tests/hikari/core/test_channels.py | 255 +++++++++++++++++++++++- tests/hikari/core/test_embeds.py | 7 - tests/hikari/core/test_guilds.py | 8 +- 7 files changed, 484 insertions(+), 91 deletions(-) diff --git a/hikari/core/channels.py b/hikari/core/channels.py index d6495251c0..4d4a734161 100644 --- a/hikari/core/channels.py +++ b/hikari/core/channels.py @@ -23,11 +23,16 @@ __all__ = [ "Channel", "ChannelType", - "DMChannel", - "PartialChannel", "PermissionOverwrite", "PermissionOverwriteType", + "PartialChannel", + "DMChannel", "GroupDMChannel", + "GuildCategory", + "GuildTextChannel", + "GuildNewsChannel", + "GuildStoreChannel", + "GuildVoiceChannel", ] import enum @@ -36,6 +41,7 @@ from hikari.core import entities from hikari.core import snowflakes from hikari.core import permissions +from hikari.core import users from hikari.internal_utilities import marshaller @@ -59,23 +65,6 @@ class ChannelType(enum.IntEnum): GUILD_STORE = 6 -@marshaller.attrs(slots=True) -class PartialChannel(snowflakes.UniqueEntity, entities.Deserializable): - """Represents a channel where we've only received it's basic information, - commonly received in rest responses. - """ - - #: This channel's name. - #: - #: :type: :obj:`str` - name: str = marshaller.attrib(deserializer=str) - - #: This channel's type. - #: - #: :type: :obj:`ChannelType` - type: ChannelType = marshaller.attrib(deserializer=ChannelType) - - @enum.unique class PermissionOverwriteType(str, enum.Enum): """The type of entity a Permission Overwrite targets.""" @@ -112,15 +101,223 @@ def unset(self) -> permissions.Permission: @marshaller.attrs(slots=True) -class Channel(PartialChannel): - ... +class PartialChannel(snowflakes.UniqueEntity, entities.Deserializable): + """Represents a channel where we've only received it's basic information, + commonly received in rest responses. + """ + + #: The channel's name. + #: + #: :type: :obj:`str` + name: str = marshaller.attrib(deserializer=str) + + #: The channel's type. + #: + #: :type: :obj:`ChannelType` + type: ChannelType = marshaller.attrib(deserializer=ChannelType) + + +def register_channel_type(type: ChannelType): + def decorator(cls): + mapping = getattr(register_channel_type, "types", {}) + mapping[type] = cls + setattr(register_channel_type, "types", mapping) + return cls + + return decorator + + +@marshaller.attrs(slots=True) +class Channel(snowflakes.UniqueEntity, entities.Deserializable): + """Base class for all channels.""" + + #: The channel's type. + #: + #: :type: :obj:`ChannelType` + type: ChannelType = marshaller.attrib(deserializer=ChannelType) +@register_channel_type(ChannelType.DM) @marshaller.attrs(slots=True) class DMChannel(Channel): - ... + """Represents a DM channel""" + #: The ID of the last message sent in this channel. + #: + #: Note + #: ---- + #: This might point to an invalid or deleted message. + #: + #: :type: :obj:`snowflakes.Snowflake`, optional + last_message_id: snowflakes.Snowflake = marshaller.attrib( + deserializer=snowflakes.Snowflake.deserialize, if_none=None + ) + + #: The recipients of the DM. + #: + #: :type: :obj:`typing.Mapping` [ :obj:`snowflakes.Snowflake`, :obj:`users.User` ] + recipients: typing.Mapping[snowflakes.Snowflake, users.User] = marshaller.attrib( + deserializer=lambda recipients: {user.id: user for user in map(users.User.deserialize, recipients)} + ) + +@register_channel_type(ChannelType.GROUP_DM) @marshaller.attrs(slots=True) class GroupDMChannel(DMChannel): - ... + """Represents a DM group channel.""" + + #: The group's name. + #: + #: :type: :obj:`str` + name: str = marshaller.attrib(deserializer=str) + + #: The ID of the owner of the group. + #: + #: :type: :obj:`snowflakes.Snowflake` + owner_id: snowflakes.Snowflake = marshaller.attrib(deserializer=snowflakes.Snowflake.deserialize) + + #: The hash of the icon of the group. + #: + #: :type: :obj:`str`, optional + icon_hash: typing.Optional[str] = marshaller.attrib(raw_name="icon", deserializer=str, if_none=None) + + #: The ID of the application that created the group DM, if it's a + #: bot based group DM. + #: + #: :type: :obj:`snowflakes.Snowflake`, optional + application_id: typing.Optional[snowflakes.Snowflake] = marshaller.attrib( + deserializer=snowflakes.Snowflake.deserialize, if_undefined=None + ) + + +@marshaller.attrs(slots=True) +class GuildChannel(Channel): + """The base for anything that is a guild channel.""" + + #: The ID of the guild the channel belongs to. + #: + #: :type: :obj:`snowflakes.Snowflake` + guild_id: snowflakes.Snowflake = marshaller.attrib(deserializer=snowflakes.Snowflake.deserialize) + + #: The sorting position of the channel. + #: + #: :type: :obj:`int` + position: int = marshaller.attrib(deserializer=int) + + #: The permission overwrites for the channel. + #: + #: :type: :obj:`typing.Mapping` [ :obj:`snowflakes.Snowflake`, :obj:`PermissionOverwrite` ] + permission_overwrites: PermissionOverwrite = marshaller.attrib( + deserializer=lambda overwrites: {o.id: o for o in map(PermissionOverwrite.deserialize, overwrites)} + ) + + #: The name of the channel. + #: + #: :type: :obj:`str` + name: str = marshaller.attrib(deserializer=str) + + #: Wheter the channel is marked as NSFW. + #: + #: :type: :obj:`bool` + is_nsfw: bool = marshaller.attrib(raw_name="nsfw", deserializer=bool) + + #: The ID of the parent category the channel belongs to. + #: + #: :type: :obj:`snowflakes.Snowflake`, optional + parent_id: snowflakes.Snowflake = marshaller.attrib(deserializer=snowflakes.Snowflake.deserialize, if_none=None) + + +@register_channel_type(ChannelType.GUILD_CATEGORY) +@marshaller.attrs(slots=True) +class GuildCategory(GuildChannel): + """Represents a guild category.""" + + +@register_channel_type(type=ChannelType.GUILD_TEXT) +@marshaller.attrs(slots=True) +class GuildTextChannel(GuildChannel): + """Represents a guild text channel.""" + + #: The topic of the channel. + #: + #: :type: :obj:`str`, optional + topic: str = marshaller.attrib(deserializer=str, if_none=None) + + #: The ID of the last message sent in this channel. + #: + #: Note + #: ---- + #: This might point to an invalid or deleted message. + #: + #: :type: :obj:`snowflakes.Snowflake`, optional + last_message_id: snowflakes.Snowflake = marshaller.attrib( + deserializer=snowflakes.Snowflake.deserialize, if_none=None + ) + + #: The delay (in seconds) between a user can send a message + #: to this channel. + #: + #: Note + #: ---- + #: Bots, as well as users with ``MANAGE_MESSAGES`` or + #: ``MANAGE_CHANNEL``, are not afected by this. + #: + #: :type: :obj:`int` + rate_limit_per_user: int = marshaller.attrib(deserializer=int) + + +@register_channel_type(ChannelType.GUILD_NEWS) +@marshaller.attrs(slots=True) +class GuildNewsChannel(GuildChannel): + """Represents an news channel.""" + + #: The topic of the channel. + #: + #: :type: :obj:`str`, optional + topic: str = marshaller.attrib(deserializer=str, if_none=None) + + #: The ID of the last message sent in this channel. + #: + #: Note + #: ---- + #: This might point to an invalid or deleted message. + #: + #: :type: :obj:`snowflakes.Snowflake`, optional + last_message_id: snowflakes.Snowflake = marshaller.attrib( + deserializer=snowflakes.Snowflake.deserialize, if_none=None + ) + + +@register_channel_type(ChannelType.GUILD_STORE) +@marshaller.attrs(slots=True) +class GuildStoreChannel(GuildChannel): + """Represents a store channel.""" + + +@register_channel_type(ChannelType.GUILD_VOICE) +@marshaller.attrs(slots=True) +class GuildVoiceChannel(GuildChannel): + """Represents an voice channel.""" + + #: The bitrate for the voice channel (in bits). + #: + #: :type: :obj:`int` + bitrate: int = marshaller.attrib(deserializer=int) + + #: The user limit for the voice channel. + #: + #: :type: :obj:`int` + user_limit: int = marshaller.attrib(deserializer=int) + + +def deserialize_channel(payload: typing.Dict[str, typing.Any]) -> typing.Union[GuildChannel, DMChannel]: + """Deserialize a channel object into the corresponding class. + + Warning + ------- + This can only be used to deserialize full channel objects. To deserialize a + partial object, use :obj:`PartialChannel.deserialize` + """ + type_id = payload["type"] + channel_type = register_channel_type.types[type_id] + return channel_type.deserialize(payload) diff --git a/hikari/core/events.py b/hikari/core/events.py index 31a9ccbe1f..58512c32bd 100644 --- a/hikari/core/events.py +++ b/hikari/core/events.py @@ -299,8 +299,8 @@ class BaseChannelEvent(HikariEvent, snowflakes.UniqueEntity, entities.Deserializ #: A mapping of this channel's recipient users, if it's a DM or group DM. #: - #: :type: :obj:`Typing.MutableMapping` [ :obj:`snowflakes.Snowflake`, :obj:`users.User` ], optional - recipients: typing.Optional[typing.MutableMapping[snowflakes.Snowflake, users.User]] = marshaller.attrib( + #: :type: :obj:`typing.Mapping` [ :obj:`snowflakes.Snowflake`, :obj:`users.User` ], optional + recipients: typing.Optional[typing.Mapping[snowflakes.Snowflake, users.User]] = marshaller.attrib( deserializer=lambda recipients: {user.id: user for user in map(users.User.deserialize, recipients)}, if_undefined=None, ) @@ -319,7 +319,7 @@ class BaseChannelEvent(HikariEvent, snowflakes.UniqueEntity, entities.Deserializ deserializer=snowflakes.Snowflake.deserialize, if_undefined=None ) - #: The ID of the application id of the group DM creator, if it's a + #: The ID of the application that created the group DM, if it's a #: bot based group DM. #: #: :type: :obj:`snowflakes.Snowflake`, optional diff --git a/hikari/core/guilds.py b/hikari/core/guilds.py index fd8b8c069b..a9d82fc942 100644 --- a/hikari/core/guilds.py +++ b/hikari/core/guilds.py @@ -21,12 +21,6 @@ __all__ = [ "ActivityFlag", "ActivityType", - "GuildChannel", - "GuildTextChannel", - "GuildNewsChannel", - "GuildStoreChannel", - "GuildVoiceChannel", - "GuildCategory", "GuildRole", "GuildFeature", "GuildSystemChannelFlag", @@ -48,8 +42,8 @@ import enum import typing -from hikari.core import channels from hikari.core import colors +from hikari.core import channels as _channels from hikari.core import emojis as _emojis from hikari.core import entities from hikari.core import permissions as _permissions @@ -61,44 +55,6 @@ from hikari.internal_utilities import transformations -@marshaller.attrs(slots=True) -class GuildChannel(channels.Channel, entities.Deserializable): - """The base for anything that is a guild channel.""" - - -@marshaller.attrs(slots=True) -class GuildTextChannel(GuildChannel): - ... - - -@marshaller.attrs(slots=True) -class GuildVoiceChannel(GuildChannel): - ... - - -@marshaller.attrs(slots=True) -class GuildCategory(GuildChannel): - ... - - -@marshaller.attrs(slots=True) -class GuildStoreChannel(GuildChannel): - ... - - -@marshaller.attrs(slots=True) -class GuildNewsChannel(GuildChannel): - ... - - -def parse_guild_channel(payload) -> GuildChannel: - class Duff: - id = snowflakes.Snowflake(123) - - # FIXME: implement properly - return Duff() - - @enum.unique class GuildExplicitContentFilterLevel(enum.IntEnum): """Represents the explicit content filter setting for a guild.""" @@ -1095,9 +1051,9 @@ class Guild(PartialGuild): #: To retrieve a list of channels in any other case, you should make an #: appropriate API call to retrieve this information. #: - #: :type: :obj:`typing.Mapping` [ :obj:`snowflakes.Snowflake`, :obj:`GuildChannel` ], optional - channels: typing.Optional[typing.Mapping[snowflakes.Snowflake, GuildChannel]] = marshaller.attrib( - deserializer=lambda guild_channels: {c.id: c for c in map(parse_guild_channel, guild_channels)}, + #: :type: :obj:`typing.Mapping` [ :obj:`snowflakes.Snowflake`, :obj:`_channels.GuildChannel` ], optional + channels: typing.Optional[typing.Mapping[snowflakes.Snowflake, _channels.GuildChannel]] = marshaller.attrib( + deserializer=lambda guild_channels: {c.id: c for c in map(_channels.deserialize_channel, guild_channels)}, if_undefined=None, ) diff --git a/hikari/internal_utilities/marshaller.py b/hikari/internal_utilities/marshaller.py index cfdccf0463..328f153fdd 100644 --- a/hikari/internal_utilities/marshaller.py +++ b/hikari/internal_utilities/marshaller.py @@ -424,11 +424,9 @@ def serialize(self, obj: typing.Optional[typing.Any]) -> typing.Optional[typing. def attrs(**kwargs): """Creates a decorator for a class to make it into an :mod:`attrs` class. - This decorator will register the - Parameters ---------- - **kwargs : + **kwargs Any kwargs to pass to :func:`attr.s`. Other Parameters diff --git a/tests/hikari/core/test_channels.py b/tests/hikari/core/test_channels.py index 8f41bf3271..c5657fd19c 100644 --- a/tests/hikari/core/test_channels.py +++ b/tests/hikari/core/test_channels.py @@ -16,10 +16,124 @@ # # You should have received a copy of the GNU Lesser General Public License # along ith Hikari. If not, see . +import cymock as mock import pytest from hikari.core import channels from hikari.core import permissions +from hikari.core import users +from tests.hikari import _helpers + + +@pytest.fixture() +def test_recipient_payload(): + return {"username": "someone", "discriminator": "9999", "id": "987", "avatar": "qrqwefasfefd"} + + +@pytest.fixture() +def test_permission_overwrite_payload(): + return {"id": "4242", "type": "member", "allow": 65, "deny": 49152} + + +@pytest.fixture() +def test_dm_channel_payload(test_recipient_payload): + return { + "id": "123", + "last_message_id": "456", + "type": 1, + "recipients": [test_recipient_payload], + } + + +@pytest.fixture() +def test_group_dm_channel_payload(test_recipient_payload): + return { + "id": "123", + "name": "Secret Developer Group", + "icon": "123asdf123adsf", + "owner_id": "456", + "application_id": "123789", + "last_message_id": "456", + "type": 3, + "recipients": [test_recipient_payload], + } + + +@pytest.fixture() +def test_guild_category_payload(test_permission_overwrite_payload): + return { + "id": "123", + "permission_overwrites": [test_permission_overwrite_payload], + "name": "Test", + "parent_id": None, + "nsfw": True, + "position": 3, + "guild_id": "9876", + "type": 4, + } + + +@pytest.fixture() +def test_guild_text_channel_payload(test_permission_overwrite_payload): + return { + "id": "123", + "guild_id": "567", + "name": "general", + "type": 0, + "position": 6, + "permission_overwrites": [test_permission_overwrite_payload], + "rate_limit_per_user": 2, + "nsfw": True, + "topic": "¯\_(ツ)_/¯", + "last_message_id": "123456", + "parent_id": "987", + } + + +@pytest.fixture() +def test_guild_news_channel_payload(test_permission_overwrite_payload): + return { + "id": "567", + "guild_id": "123", + "name": "Important Announcements", + "type": 5, + "position": 0, + "permission_overwrites": [test_permission_overwrite_payload], + "nsfw": True, + "topic": "Super Important Announcements", + "last_message_id": "456", + "parent_id": "654", + } + + +@pytest.fixture() +def test_guild_store_channel_payload(test_permission_overwrite_payload): + return { + "id": "123", + "permission_overwrites": [test_permission_overwrite_payload], + "name": "Half Life 3", + "parent_id": "9876", + "nsfw": True, + "position": 2, + "guild_id": "1234", + "type": 6, + } + + +@pytest.fixture() +def test_guild_voice_channel_payload(test_permission_overwrite_payload): + return { + "id": "123", + "guild_id": "789", + "name": "Secret Developer Discussions", + "type": 2, + "nsfw": True, + "position": 4, + "permission_overwrites": [test_permission_overwrite_payload], + "bitrate": 64000, + "user_limit": 3, + "parent_id": "456", + } class TestPartialChannel: @@ -35,10 +149,6 @@ def test_deserialize(self, test_partial_channel_payload): class TestPermissionOverwrite: - @pytest.fixture() - def test_permission_overwrite_payload(self): - return {"id": "4242", "type": "member", "allow": 65, "deny": 49152} - def test_deserialize(self, test_permission_overwrite_payload): permission_overwrite_obj = channels.PermissionOverwrite.deserialize(test_permission_overwrite_payload) assert ( @@ -48,3 +158,140 @@ def test_deserialize(self, test_permission_overwrite_payload): assert permission_overwrite_obj.deny == permissions.Permission.EMBED_LINKS | permissions.Permission.ATTACH_FILES assert permission_overwrite_obj.unset == permissions.Permission(49217) assert isinstance(permission_overwrite_obj.unset, permissions.Permission) + + +class TestDMChannel: + def test_deserialize(self, test_dm_channel_payload, test_recipient_payload): + mock_user = mock.MagicMock(users.User, id=987) + + with mock.patch.object(users.User, "deserialize", return_value=mock_user) as patched_user_deserialize: + channel_obj = channels.DMChannel.deserialize(test_dm_channel_payload) + patched_user_deserialize.assert_called_once_with(test_recipient_payload) + + assert channel_obj.id == 123 + assert channel_obj.last_message_id == 456 + assert channel_obj.type == channels.ChannelType.DM + assert channel_obj.recipients == {987: mock_user} + + +class TestGroupDMChannel: + def test_deserialize(self, test_group_dm_channel_payload, test_recipient_payload): + mock_user = mock.MagicMock(users.User, id=987) + + with mock.patch.object(users.User, "deserialize", return_value=mock_user) as patched_user_deserialize: + channel_obj = channels.GroupDMChannel.deserialize(test_group_dm_channel_payload) + patched_user_deserialize.assert_called_once_with(test_recipient_payload) + + assert channel_obj.id == 123 + assert channel_obj.last_message_id == 456 + assert channel_obj.type == channels.ChannelType.GROUP_DM + assert channel_obj.recipients == {987: mock_user} + assert channel_obj.name == "Secret Developer Group" + assert channel_obj.icon_hash == "123asdf123adsf" + assert channel_obj.owner_id == 456 + assert channel_obj.application_id == 123789 + + +class TestGuildCategory: + def test_deserialize(self, test_guild_category_payload, test_permission_overwrite_payload): + channel_obj = channels.GuildCategory.deserialize(test_guild_category_payload) + + assert channel_obj.id == 123 + assert channel_obj.permission_overwrites == { + 4242: channels.PermissionOverwrite.deserialize(test_permission_overwrite_payload) + } + assert channel_obj.guild_id == 9876 + assert channel_obj.position == 3 + assert channel_obj.name == "Test" + assert channel_obj.is_nsfw is True + assert channel_obj.parent_id is None + assert channel_obj.type == channels.ChannelType.GUILD_CATEGORY + + +class TestGuildTextChannel: + def test_deserialize(self, test_guild_text_channel_payload, test_permission_overwrite_payload): + channel_obj = channels.GuildTextChannel.deserialize(test_guild_text_channel_payload) + + assert channel_obj.id == 123 + assert channel_obj.permission_overwrites == { + 4242: channels.PermissionOverwrite.deserialize(test_permission_overwrite_payload) + } + assert channel_obj.guild_id == 567 + assert channel_obj.position == 6 + assert channel_obj.name == "general" + assert channel_obj.topic == "¯\_(ツ)_/¯" + assert channel_obj.is_nsfw is True + assert channel_obj.parent_id == 987 + assert channel_obj.type == channels.ChannelType.GUILD_TEXT + assert channel_obj.last_message_id == 123456 + assert channel_obj.rate_limit_per_user == 2 + + +class TestGuildNewsChannel: + def test_deserialize(self, test_guild_news_channel_payload, test_permission_overwrite_payload): + channel_obj = channels.GuildNewsChannel.deserialize(test_guild_news_channel_payload) + + assert channel_obj.id == 567 + assert channel_obj.permission_overwrites == { + 4242: channels.PermissionOverwrite.deserialize(test_permission_overwrite_payload) + } + assert channel_obj.guild_id == 123 + assert channel_obj.position == 0 + assert channel_obj.name == "Important Announcements" + assert channel_obj.topic == "Super Important Announcements" + assert channel_obj.is_nsfw is True + assert channel_obj.parent_id == 654 + assert channel_obj.type == channels.ChannelType.GUILD_NEWS + assert channel_obj.last_message_id == 456 + + +class TestGuildStoreChannel: + def test_deserialize(self, test_guild_store_channel_payload, test_permission_overwrite_payload): + channel_obj = channels.GuildStoreChannel.deserialize(test_guild_store_channel_payload) + + assert channel_obj.id == 123 + assert channel_obj.permission_overwrites == { + 4242: channels.PermissionOverwrite.deserialize(test_permission_overwrite_payload) + } + assert channel_obj.guild_id == 1234 + assert channel_obj.position == 2 + assert channel_obj.name == "Half Life 3" + assert channel_obj.is_nsfw is True + assert channel_obj.parent_id == 9876 + assert channel_obj.type == channels.ChannelType.GUILD_STORE + + +class TestGuildVoiceChannell: + def test_deserialize(self, test_guild_voice_channel_payload, test_permission_overwrite_payload): + channel_obj = channels.GuildVoiceChannel.deserialize(test_guild_voice_channel_payload) + + assert channel_obj.id == 123 + assert channel_obj.permission_overwrites == { + 4242: channels.PermissionOverwrite.deserialize(test_permission_overwrite_payload) + } + assert channel_obj.guild_id == 789 + assert channel_obj.position == 4 + assert channel_obj.name == "Secret Developer Discussions" + assert channel_obj.is_nsfw is True + assert channel_obj.parent_id == 456 + assert channel_obj.type == channels.ChannelType.GUILD_VOICE + assert channel_obj.bitrate == 64000 + assert channel_obj.user_limit == 3 + + +def test_deserialize_channel_returns_correct_type( + test_dm_channel_payload, + test_group_dm_channel_payload, + test_guild_category_payload, + test_guild_text_channel_payload, + test_guild_news_channel_payload, + test_guild_store_channel_payload, + test_guild_voice_channel_payload, +): + assert isinstance(channels.deserialize_channel(test_dm_channel_payload), channels.DMChannel) + assert isinstance(channels.deserialize_channel(test_group_dm_channel_payload), channels.GroupDMChannel) + assert isinstance(channels.deserialize_channel(test_guild_category_payload), channels.GuildCategory) + assert isinstance(channels.deserialize_channel(test_guild_text_channel_payload), channels.GuildTextChannel) + assert isinstance(channels.deserialize_channel(test_guild_news_channel_payload), channels.GuildNewsChannel) + assert isinstance(channels.deserialize_channel(test_guild_store_channel_payload), channels.GuildStoreChannel) + assert isinstance(channels.deserialize_channel(test_guild_voice_channel_payload), channels.GuildVoiceChannel) diff --git a/tests/hikari/core/test_embeds.py b/tests/hikari/core/test_embeds.py index a6e850499f..8e6743a475 100644 --- a/tests/hikari/core/test_embeds.py +++ b/tests/hikari/core/test_embeds.py @@ -213,13 +213,6 @@ def test_serialize(self, test_field_payload): class TestEmbed: - @pytest.fixture - def embed_deserialized(self, test_embed_payload): - mock_datetime = mock.MagicMock(datetime.datetime) - - with _helpers.patch_marshal_attr(embeds.Embed, "timestamp", return_value=mock_datetime): - return embeds.Embed.deserialize(test_embed_payload), mock_datetime - def test_deserialize( self, test_embed_payload, diff --git a/tests/hikari/core/test_guilds.py b/tests/hikari/core/test_guilds.py index 15fa253cb5..fb6e0546ef 100644 --- a/tests/hikari/core/test_guilds.py +++ b/tests/hikari/core/test_guilds.py @@ -25,6 +25,7 @@ from hikari.core import entities from hikari.core import guilds from hikari.core import users +from hikari.core import channels from hikari.internal_utilities import cdn from hikari.internal_utilities import dates @@ -88,6 +89,7 @@ def test_channel_payload(): "rate_limit_per_user": 420, "topic": "nsfw stuff", "name": "shh!", + "last_message_id": "1234", } @@ -641,7 +643,7 @@ def test_deserialize( ): mock_emoji = mock.MagicMock(emojis.GuildEmoji, id=42) mock_user = mock.MagicMock(users.User, id=84) - mock_guild_channel = mock.MagicMock(guilds.GuildChannel, id=6969) + mock_guild_channel = mock.MagicMock(channels.GuildChannel, id=6969) with mock.patch.object(emojis.GuildEmoji, "deserialize", return_value=mock_emoji): with _helpers.patch_marshal_attr( guilds.GuildMemberPresence, "user", deserializer=guilds.PresenceUser.deserialize, return_value=mock_user @@ -649,9 +651,9 @@ def test_deserialize( with _helpers.patch_marshal_attr( guilds.GuildMember, "user", deserializer=users.User.deserialize, return_value=mock_user ) as patched_member_user_deserializer: - with mock.patch.object(guilds, "parse_guild_channel", return_value=mock_guild_channel): + with mock.patch.object(channels, "deserialize_channel", return_value=mock_guild_channel): guild_obj = guilds.Guild.deserialize(test_guild_payload) - guilds.parse_guild_channel.assert_called_once_with(test_channel_payload) + channels.deserialize_channel.assert_called_once_with(test_channel_payload) patched_member_user_deserializer.assert_called_once_with(test_member_payload["user"]) assert guild_obj.members == {84: guilds.GuildMember.deserialize(test_member_payload)} patched_user_deserializer.assert_called_once_with(test_member_payload["user"]) From a625ad532c24ad2baeae0f14d668a8600cc0b03d Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Sun, 29 Mar 2020 23:06:54 +0100 Subject: [PATCH 051/922] Major refactoring. - Fixed about 500 new pylint warnings that were occuring, and I've made pylint much stricter including about 100 new rules. - Added CI pipelines for Pydocstyle which enforces correct docstrings. - Refactored noxfile. - Removed a bunch of dead code, mostly out of the internal utilities. - Internal utilities is now `_internal` and private. - Coalesced a bunch of sparse utility files. - Deimplemented cache - Fixed loads of status-related TODOs and FIXMEs - Fixed loads of missing docstrings. - Made config files for CI have consistent names. - Defined typing facades for asyncio.Future, asyncio.Task, and Coroutine types. - Reworked user-agent to use singleton. --- .coveragerc => coverage.ini | 0 gitlab/test.yml | 11 + hikari/__main__.py | 4 + hikari/_about.py | 1 + .../__init__.py | 23 +- .../assertions.py | 8 +- .../{internal_utilities => _internal}/cdn.py | 0 hikari/_internal/conversions.py | 261 ++++ .../marshaller.py | 24 +- .../singleton_meta.py => _internal/meta.py} | 8 +- hikari/_internal/more_asyncio.py | 133 ++ .../more_collections.py} | 16 +- .../loggers.py => _internal/more_logging.py} | 0 hikari/core/channels.py | 31 +- hikari/core/clients/_run_gateway.py | 4 +- hikari/core/clients/app_config.py | 5 +- hikari/core/clients/bot_client.py | 1 + hikari/core/clients/gateway_client.py | 42 +- hikari/core/clients/gateway_config.py | 15 +- hikari/core/clients/http_client.py | 1 + hikari/core/clients/http_config.py | 2 +- hikari/core/clients/protocol_config.py | 2 +- hikari/core/clients/shard_client.py | 56 +- hikari/core/colors.py | 36 +- hikari/core/dispatcher.py | 27 +- hikari/core/embeds.py | 10 +- hikari/core/emojis.py | 2 +- hikari/core/entities.py | 17 +- hikari/core/events.py | 46 +- hikari/core/gateway_entities.py | 18 +- hikari/core/guilds.py | 36 +- hikari/core/invites.py | 8 +- hikari/core/messages.py | 8 +- hikari/core/oauth2.py | 4 +- hikari/core/snowflakes.py | 12 +- hikari/core/{state.py => state/__init__.py} | 13 +- hikari/core/users.py | 5 +- hikari/core/voices.py | 2 +- hikari/core/webhooks.py | 10 +- hikari/errors.py | 1 - hikari/internal_utilities/aio.py | 58 - hikari/internal_utilities/cache.py | 190 --- hikari/internal_utilities/dates.py | 115 -- hikari/internal_utilities/storage.py | 56 - hikari/internal_utilities/transformations.py | 132 -- hikari/net/errors.py | 2 + hikari/net/ratelimits.py | 86 +- hikari/net/rest.py | 1101 ++++++++--------- hikari/net/routes.py | 24 +- hikari/net/shard.py | 48 +- hikari/net/user_agent.py | 155 +-- noxfile.py | 95 +- pydocstyle.ini | 3 + pylintrc => pylint.ini | 465 ++----- tests/hikari/_helpers.py | 2 +- .../__init__.py | 0 .../test_assertions.py | 2 +- .../test_cdn.py | 2 +- tests/hikari/_internal/test_conversions.py | 195 +++ .../test_marshaller.py | 2 +- .../test_marshaller_pep563.py | 2 +- .../test_meta.py} | 4 +- .../test_more_asyncio.py} | 10 +- .../test_more_collections.py} | 40 +- .../test_more_logging.py} | 14 +- tests/hikari/core/test_embeds.py | 7 +- tests/hikari/core/test_events.py | 15 +- tests/hikari/core/test_guilds.py | 63 +- tests/hikari/core/test_invites.py | 9 +- tests/hikari/core/test_messages.py | 9 +- tests/hikari/core/test_oauth2.py | 2 +- tests/hikari/core/test_users.py | 2 +- tests/hikari/internal_utilities/test_cache.py | 363 ------ tests/hikari/internal_utilities/test_dates.py | 105 -- .../test_transformations.py | 96 -- tests/hikari/net/test_ratelimits.py | 17 +- tests/hikari/net/test_rest.py | 33 +- tests/hikari/net/test_shard.py | 30 +- tests/hikari/net/test_user_agent.py | 21 +- 79 files changed, 1870 insertions(+), 2608 deletions(-) rename .coveragerc => coverage.ini (100%) rename hikari/{internal_utilities => _internal}/__init__.py (64%) rename hikari/{internal_utilities => _internal}/assertions.py (88%) rename hikari/{internal_utilities => _internal}/cdn.py (100%) create mode 100644 hikari/_internal/conversions.py rename hikari/{internal_utilities => _internal}/marshaller.py (95%) rename hikari/{internal_utilities/singleton_meta.py => _internal/meta.py} (92%) create mode 100644 hikari/_internal/more_asyncio.py rename hikari/{internal_utilities/containers.py => _internal/more_collections.py} (69%) rename hikari/{internal_utilities/loggers.py => _internal/more_logging.py} (100%) rename hikari/core/{state.py => state/__init__.py} (76%) delete mode 100644 hikari/internal_utilities/aio.py delete mode 100644 hikari/internal_utilities/cache.py delete mode 100644 hikari/internal_utilities/dates.py delete mode 100644 hikari/internal_utilities/storage.py delete mode 100644 hikari/internal_utilities/transformations.py create mode 100644 pydocstyle.ini rename pylintrc => pylint.ini (65%) rename tests/hikari/{internal_utilities => _internal}/__init__.py (100%) rename tests/hikari/{internal_utilities => _internal}/test_assertions.py (97%) rename tests/hikari/{internal_utilities => _internal}/test_cdn.py (96%) create mode 100644 tests/hikari/_internal/test_conversions.py rename tests/hikari/{internal_utilities => _internal}/test_marshaller.py (99%) rename tests/hikari/{internal_utilities => _internal}/test_marshaller_pep563.py (99%) rename tests/hikari/{internal_utilities/test_singleton_meta.py => _internal/test_meta.py} (88%) rename tests/hikari/{internal_utilities/test_aio.py => _internal/test_more_asyncio.py} (88%) rename tests/hikari/{internal_utilities/test_storage.py => _internal/test_more_collections.py} (53%) rename tests/hikari/{internal_utilities/test_loggers.py => _internal/test_more_logging.py} (78%) delete mode 100644 tests/hikari/internal_utilities/test_cache.py delete mode 100644 tests/hikari/internal_utilities/test_dates.py delete mode 100644 tests/hikari/internal_utilities/test_transformations.py diff --git a/.coveragerc b/coverage.ini similarity index 100% rename from .coveragerc rename to coverage.ini diff --git a/gitlab/test.yml b/gitlab/test.yml index 78d4883615..1a02452f31 100644 --- a/gitlab/test.yml +++ b/gitlab/test.yml @@ -228,3 +228,14 @@ pylint: artifacts: reports: junit: public/pylint.xml + + +pydocstyle: + extends: + - .cpython-tool-buster + - .cache + script: + - bash tasks/retry_aborts.sh nox -s docstyle + artifacts: + reports: + junit: public/pylint.xml diff --git a/hikari/__main__.py b/hikari/__main__.py index 14cb76005c..3ff11eb34a 100644 --- a/hikari/__main__.py +++ b/hikari/__main__.py @@ -16,6 +16,9 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . +"""Provides a command-line entry point that shows the library version and then +exits. +""" import platform import click @@ -25,6 +28,7 @@ @click.command() def main(): + """Show the application version, then exit.""" print(f"{hikari.__name__} {hikari.__version__} from {hikari.__path__[0]} (python {platform.python_version()})") diff --git a/hikari/_about.py b/hikari/_about.py index e127e9db43..a255fa020c 100644 --- a/hikari/_about.py +++ b/hikari/_about.py @@ -16,6 +16,7 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . +"""Package metadata.""" __all__ = ["__author__", "__copyright__", "__email__", "__license__", "__version__", "__url__"] __author__ = "Nekokatt" diff --git a/hikari/internal_utilities/__init__.py b/hikari/_internal/__init__.py similarity index 64% rename from hikari/internal_utilities/__init__.py rename to hikari/_internal/__init__.py index 9f715f0f62..c8b749b260 100644 --- a/hikari/internal_utilities/__init__.py +++ b/hikari/_internal/__init__.py @@ -16,18 +16,17 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . -"""Various utilities used internally within this API. +"""Various utilities used internally within this API. -These are not bound to the versioning contact, and are considered to be -implementation detail that could change at any time, so should not be +These are not bound to the versioning contact, and are considered to be +implementation detail that could change at any time, so should not be used outside this library. """ -from hikari.internal_utilities import aio -from hikari.internal_utilities import assertions -from hikari.internal_utilities import cache -from hikari.internal_utilities import containers -from hikari.internal_utilities import dates -from hikari.internal_utilities import loggers -from hikari.internal_utilities import singleton_meta -from hikari.internal_utilities import storage -from hikari.internal_utilities import transformations +from hikari._internal import assertions +from hikari._internal import cdn +from hikari._internal import conversions +from hikari._internal import marshaller +from hikari._internal import meta +from hikari._internal import more_asyncio +from hikari._internal import more_collections +from hikari._internal import more_logging diff --git a/hikari/internal_utilities/assertions.py b/hikari/_internal/assertions.py similarity index 88% rename from hikari/internal_utilities/assertions.py rename to hikari/_internal/assertions.py index 8772070a0b..b453429b61 100644 --- a/hikari/internal_utilities/assertions.py +++ b/hikari/_internal/assertions.py @@ -16,9 +16,11 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . -"""Assertions of things. -These are functions that validate a value, expected to return the value on success but error -on any failure. +"""Assertions of things. + +These are functions that validate a value, and are expected to return the value +on success but error on any failure. This allows for quick checking of +conditions that might break the function or cause it to misbehave. """ __all__ = [ "assert_that", diff --git a/hikari/internal_utilities/cdn.py b/hikari/_internal/cdn.py similarity index 100% rename from hikari/internal_utilities/cdn.py rename to hikari/_internal/cdn.py diff --git a/hikari/_internal/conversions.py b/hikari/_internal/conversions.py new file mode 100644 index 0000000000..fd7591822e --- /dev/null +++ b/hikari/_internal/conversions.py @@ -0,0 +1,261 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# Copyright © Nekokatt 2019-2020 +# +# This file is part of Hikari. +# +# Hikari is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Hikari is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Hikari. If not, see . +"""Basic transformation utilities.""" +__all__ = [ + "CastInputT", + "CastOutputT", + "DefaultT", + "TypeCastT", + "ResultT", + "nullable_cast", + "try_cast", + "put_if_specified", + "image_bytes_to_image_data", +] + +import base64 +import contextlib +import datetime +import email.utils +import io +import re +import typing + +CastInputT = typing.TypeVar("CastInputT") +CastOutputT = typing.TypeVar("CastOutputT") +DefaultT = typing.TypeVar("DefaultT") +TypeCastT = typing.Callable[[CastInputT], CastOutputT] +ResultT = typing.Union[CastOutputT, DefaultT] + + +def nullable_cast(value: CastInputT, cast: TypeCastT) -> ResultT: + """Attempts to cast the given ``value`` with the given ``cast``, but only if the + ``value`` is not ``None``. If it is ``None``, then ``None`` is returned instead. + """ + if value is None: + return None + return cast(value) + + +def try_cast(value: CastInputT, cast: TypeCastT, default: DefaultT = None) -> ResultT: + """Try to cast the given value to the given cast. + + If it throws a :obj:`Exception` or derivative, it will return ``default`` instead + of the cast value instead. + """ + with contextlib.suppress(Exception): + return cast(value) + return default + + +def put_if_specified( + mapping: typing.Dict[typing.Hashable, typing.Any], + key: typing.Hashable, + value: typing.Any, + type_after: typing.Optional[TypeCastT] = None, +) -> None: + """Add a value to the mapping under the given key as long as the value is not :obj:`typing.Literal` + + Parameters + ---------- + mapping : :obj:`typing.Dict` [ :obj:`typing.Hashable`, :obj:`typing.Any` ] + The mapping to add to. + key : :obj:`typing.Hashable` + The key to add the value under. + value : :obj:`typing.Any` + The value to add. + type_after : :obj:`TypeCastT`, optional + Type to apply to the value when added. + """ + if value is not ...: + if type_after: + mapping[key] = type_after(value) + else: + mapping[key] = value + + +def image_bytes_to_image_data(img_bytes: typing.Optional[bytes] = None) -> typing.Optional[str]: + """Encode image bytes into an image data string. + + Parameters + ---------- + img_bytes : :obj:`bytes`, optional + The image bytes. + + Raises + ------ + :obj:`ValueError` + If the image type passed is not supported. + + Returns + ------- + :obj:`str`, optional + The ``image_bytes`` given encoded into an image data string or ``None``. + + Note + ---- + Supported image types: ``.png``, ``.jpeg``, ``.jfif``, ``.gif``, ``.webp`` + """ + if img_bytes is None: + return None + + if img_bytes[:8] == b"\211PNG\r\n\032\n": + img_type = "image/png" + elif img_bytes[6:10] in (b"Exif", b"JFIF"): + img_type = "image/jpeg" + elif img_bytes[:6] in (b"GIF87a", b"GIF89a"): + img_type = "image/gif" + elif img_bytes.startswith(b"RIFF") and img_bytes[8:12] == b"WEBP": + img_type = "image/webp" + else: + raise ValueError("Unsupported image type passed") + + image_data = base64.b64encode(img_bytes).decode() + + return f"data:{img_type};base64,{image_data}" + + +def try_cast_or_defer_unary_operator(type_): + """Returns a unary operator that will try to cast the given input to + whatever type is provided. + + Parameters + ---------- + type_: + The type to cast to. + """ + return lambda data: try_cast(data, type_, data) + + +def parse_http_date(date_str: str) -> datetime.datetime: + """Return the HTTP date as a datetime object. + + Parameters + ---------- + date_str : :obj:`str` + The RFC-2822 (section 3.3) compliant date string to parse. + + See also + -------- + ``_ + """ + return email.utils.parsedate_to_datetime(date_str) + + +ISO_8601_DATE_PART = re.compile(r"^(\d{4})-(\d{2})-(\d{2})") +ISO_8601_TIME_PART = re.compile(r"T(\d{2}):(\d{2}):(\d{2})(?:\.(\d{1,6}))?", re.I) +ISO_8601_TZ_PART = re.compile(r"([+-])(\d{2}):(\d{2})$") + + +def parse_iso_8601_ts(date_string: str) -> datetime.datetime: + """Parses an ISO 8601 date string into a datetime object + + Parameters + ---------- + date_string : :obj:`str` + The ISO 8601 compliant date string to parse. + + See also + -------- + ``_ + """ + year, month, day = map(int, ISO_8601_DATE_PART.findall(date_string)[0]) + + time_part = ISO_8601_TIME_PART.findall(date_string)[0] + hour, minute, second, partial = time_part + + # Pad the millisecond part if it is not in microseconds, otherwise Python will complain. + partial = partial + (6 - len(partial)) * "0" + hour, minute, second, partial = int(hour), int(minute), int(second), int(partial) + if date_string.endswith(("Z", "z")): + timezone = datetime.timezone.utc + else: + sign, tz_hour, tz_minute = ISO_8601_TZ_PART.findall(date_string)[0] + tz_hour, tz_minute = int(tz_hour), int(tz_minute) + offset = datetime.timedelta(hours=tz_hour, minutes=tz_minute) + if sign == "-": + offset = -offset + timezone = datetime.timezone(offset) + + return datetime.datetime(year, month, day, hour, minute, second, partial, timezone) + + +DISCORD_EPOCH = 1_420_070_400 + + +def discord_epoch_to_datetime(epoch: int) -> datetime.datetime: + """Parses a discord epoch into a datetime object + + Parameters + ---------- + epoch : :obj:`int` + Number of milliseconds since 1/1/2015 (UTC) + + Returns + ------- + :obj:`datetime.datetime` + Number of seconds since 1/1/1970 within a datetime object (UTC). + """ + return datetime.datetime.fromtimestamp(epoch / 1000 + DISCORD_EPOCH, datetime.timezone.utc) + + +def unix_epoch_to_ts(epoch: int) -> datetime.datetime: + """Parses a unix epoch to a datetime object + + Parameters + ---------- + epoch : :obj:`int` + Number of milliseconds since 1/1/1970 (UTC) + + Returns + ------- + :obj:`datetime.datetime` + Number of seconds since 1/1/1970 within a datetime object (UTC). + """ + return datetime.datetime.fromtimestamp(epoch / 1000, datetime.timezone.utc) + + +def make_resource_seekable(resource: typing.Any) -> typing.Union[io.BytesIO, io.StringIO]: + """Given some representation of data, make a seekable resource to use. + + This supports :obj:`bytes`, :obj:`bytearray`, :obj:`memoryview`, and :obj:`str`. + Anything else is just returned. + + Parameters + ---------- + resource : :obj:`typing.Any` + The resource to check. + + Returns + ------- + :obj:`typing.Union` [ :obj:`io.BytesIO`, :obj:`io.StringIO` ] + An stream-compatible resource where possible. + """ + if isinstance(resource, (bytes, bytearray)): + resource = io.BytesIO(resource) + elif isinstance(resource, memoryview): + resource = io.BytesIO(resource.tobytes()) + elif isinstance(resource, str): + resource = io.StringIO(resource) + + return resource + + +BytesLikeT = typing.Union[bytes, bytearray, memoryview, str, io.StringIO, io.BytesIO] +FileLikeT = typing.Union[BytesLikeT, io.BufferedRandom, io.BufferedReader, io.BufferedRWPair] diff --git a/hikari/internal_utilities/marshaller.py b/hikari/_internal/marshaller.py similarity index 95% rename from hikari/internal_utilities/marshaller.py rename to hikari/_internal/marshaller.py index 328f153fdd..d1bb871afb 100644 --- a/hikari/internal_utilities/marshaller.py +++ b/hikari/_internal/marshaller.py @@ -40,7 +40,7 @@ import attr -from hikari.internal_utilities import assertions +from hikari._internal import assertions _RAW_NAME_ATTR = __name__ + "_RAW_NAME" _SERIALIZER_ATTR = __name__ + "_SERIALIZER" @@ -98,7 +98,20 @@ def dereference_handle(handle_string: str) -> typing.Any: return weakref.proxy(obj) -def dereference_int_flag(int_flag_type, raw_value) -> None: +def dereference_int_flag(int_flag_type, raw_value) -> typing.SupportsInt: + """Given a type of :obj:`enum.IntFlag`, and a raw value, cast the raw value + to the int flag. + + This will support resolving bitfield integers as well as decoding a sequence + of case insensitive flag names into one combined value. + + Parameters + ---------- + int_flag_type: + The type of the int flag to check. + raw_value: + The raw value to convert. + """ if isinstance(raw_value, str) and raw_value.isdigit(): raw_value = int(raw_value) @@ -177,7 +190,9 @@ class that this attribute is in will trigger a :obj:`TypeError` metadata[_IF_UNDEFINED] = if_undefined metadata[_TRANSIENT_ATTR] = transient - return attr.ib(**kwargs, metadata=metadata) + attribute = attr.ib(**kwargs, metadata=metadata) + # Fool pylint into thinking this is any type. + return typing.cast(typing.Any, attribute) def _not_implemented(op, name): @@ -353,7 +368,8 @@ def deserialize(self, raw_data: typing.Mapping[str, typing.Any], target_type: ty else: kwargs[kwarg_name] = a.if_undefined() continue - elif (data := raw_data[a.raw_name]) is None: + + if (data := raw_data[a.raw_name]) is None: if a.if_none is RAISE: raise AttributeError( "Failed to deserialize data to instance of " diff --git a/hikari/internal_utilities/singleton_meta.py b/hikari/_internal/meta.py similarity index 92% rename from hikari/internal_utilities/singleton_meta.py rename to hikari/_internal/meta.py index f1e4f049f5..03f18f3842 100644 --- a/hikari/internal_utilities/singleton_meta.py +++ b/hikari/_internal/meta.py @@ -16,14 +16,14 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . -"""Singleton metaclass""" +"""Various functional types and metatypes.""" __all__ = ["SingletonMeta"] class SingletonMeta(type): - """Metaclass that makes the class a singleton. - - Once an instance has been defined at runtime, it will exist until the interpreter + """Metaclass that makes the class a singleton. + + Once an instance has been defined at runtime, it will exist until the interpreter that created it is terminated. Example diff --git a/hikari/_internal/more_asyncio.py b/hikari/_internal/more_asyncio.py new file mode 100644 index 0000000000..5ff94b0d4f --- /dev/null +++ b/hikari/_internal/more_asyncio.py @@ -0,0 +1,133 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# Copyright © Nekokatt 2019-2020 +# +# This file is part of Hikari. +# +# Hikari is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Hikari is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Hikari. If not, see . +"""Asyncio extensions and utilities.""" +from __future__ import annotations + +__all__ = ["Future", "Task", "completed_future"] + +import asyncio +import contextvars +import typing + +T = typing.TypeVar("T") +T_co = typing.TypeVar("T_co") + +try: + raise Exception +except Exception as ex: # pylint:disable=broad-except + tb = ex.__traceback__ + StackFrameType = type(tb.tb_frame) + + +# pylint:disable=unused-variable +@typing.runtime_checkable +class Future(typing.Protocol[T]): + """Typed protocol representation of an :obj:`asyncio.Future`. + + You should consult the documentation for :obj:`asyncio.Future` for usage. + """ + + def result(self) -> T: + """See :meth:`asyncio.Future.result`.""" + + def set_result(self, result: T, /) -> None: + """See :meth:`asyncio.Future.set_result`.""" + + def set_exception(self, exception: Exception, /) -> None: + """See :meth:`asyncio.Future.set_exception`.""" + + def done(self) -> bool: + """See :meth:`asyncio.Future.done`.""" + + def cancelled(self) -> bool: + """See :meth:`asyncio.Future.cancelled`.""" + + def add_done_callback( + self, callback: typing.Callable[[Future[T]], None], /, *, context: typing.Optional[contextvars.Context], + ) -> None: + """See :meth:`asyncio.Future.add_done_callback`.""" + + def remove_done_callback(self, callback: typing.Callable[[Future[T]], None], /) -> None: + """See :meth:`asyncio.Future.remove_done_callback`.""" + + def cancel(self) -> bool: + """See :meth:`asyncio.Future.cancel`.""" + + def exception(self) -> typing.Optional[Exception]: + """See :meth:`asyncio.Future.exception`.""" + + def get_loop(self) -> asyncio.AbstractEventLoop: + """See :meth:`asyncio.Future.get_loop`.""" + + def __await__(self) -> typing.Coroutine[None, None, T]: + ... + + +# pylint:enable=unused-variable + + +# pylint:disable=unused-variable +class Task(Future[T]): + """Typed protocol representation of an :obj:`asyncio.Task`. + + You should consult the documentation for :obj:`asyncio.Task` for usage. + """ + + def get_stack(self, *, limit: typing.Optional[int] = None) -> typing.Sequence[StackFrameType]: + """See :meth:`asyncio.Task.get_stack`.""" + + def print_stack(self, *, limit: typing.Optional[int] = None, file: typing.Optional[typing.IO] = None) -> None: + """See :meth:`asyncio.Task.print_stack`.""" + + def get_name(self) -> str: + """See :meth:`asyncio.Task.get_name`.""" + + def set_name(self, value: str, /) -> None: + """See :meth:`asyncio.Task.set_name`.""" + + +# pylint:enable=unused-variable + + +@typing.overload +def completed_future() -> Future[None]: + """Return a completed future with no result.""" + + +@typing.overload +def completed_future(result: T, /) -> Future[T]: + """Return a completed future with the given value as the result.""" + + +def completed_future(result=None, /): + """Create a future on the current running loop that is completed, then return it. + + Parameters + --------- + result : :obj:`typing.Any` + The value to set for the result of the future. + + Returns + ------- + :obj:`asyncio.Future` + The completed future. + """ + future = asyncio.get_event_loop().create_future() + future.set_result(result) + return future diff --git a/hikari/internal_utilities/containers.py b/hikari/_internal/more_collections.py similarity index 69% rename from hikari/internal_utilities/containers.py rename to hikari/_internal/more_collections.py index bd33e7b9c1..9a4032a949 100644 --- a/hikari/internal_utilities/containers.py +++ b/hikari/_internal/more_collections.py @@ -16,7 +16,7 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . -"""Custom data structures and constant values.""" +"""Special data structures and utilities.""" __all__ = [ "EMPTY_SEQUENCE", "EMPTY_SET", @@ -29,21 +29,14 @@ import types import typing -#: An immutable indexable container of elements with zero size. import weakref EMPTY_SEQUENCE: typing.Sequence = tuple() -#: An immutable unordered container of elements with zero size. EMPTY_SET: typing.AbstractSet = frozenset() -#: An immutable container of elements with zero size. EMPTY_COLLECTION: typing.Collection = tuple() -#: An immutable ordered mapping of key elements to value elements with zero size. EMPTY_DICT: typing.Mapping = types.MappingProxyType({}) -#: An empty generator expression that can be used as a placeholder, but never -#: yields anything. EMPTY_GENERATOR_EXPRESSION = (_ for _ in EMPTY_COLLECTION) - K = typing.TypeVar("K") V = typing.TypeVar("V") @@ -53,10 +46,3 @@ class WeakKeyDictionary(weakref.WeakKeyDictionary, typing.MutableMapping[K, V]): This is a type-safe version of :obj:`weakref.WeakKeyDictionary`. """ - - -class WeakValueDictionary(weakref.WeakValueDictionary, typing.MutableMapping[K, V]): - """A dictionary that has weak references to the values. - - This is a type-safe version of :obj:`weakref.WeakValueDictionary`. - """ diff --git a/hikari/internal_utilities/loggers.py b/hikari/_internal/more_logging.py similarity index 100% rename from hikari/internal_utilities/loggers.py rename to hikari/_internal/more_logging.py diff --git a/hikari/core/channels.py b/hikari/core/channels.py index 4d4a734161..f9cc4c881d 100644 --- a/hikari/core/channels.py +++ b/hikari/core/channels.py @@ -38,11 +38,11 @@ import enum import typing +from hikari._internal import marshaller from hikari.core import entities -from hikari.core import snowflakes from hikari.core import permissions from hikari.core import users -from hikari.internal_utilities import marshaller +from hikari.core import snowflakes @enum.unique @@ -97,6 +97,9 @@ class PermissionOverwrite(snowflakes.UniqueEntity, entities.Deserializable, enti @property def unset(self) -> permissions.Permission: + """Return a bitfield of all permissions not explicitly allowed or + denied by this overwrite. + """ return typing.cast(permissions.Permission, (self.allow | self.deny)) @@ -117,10 +120,24 @@ class PartialChannel(snowflakes.UniqueEntity, entities.Deserializable): type: ChannelType = marshaller.attrib(deserializer=ChannelType) -def register_channel_type(type: ChannelType): +def register_channel_type(type_: ChannelType) -> typing.Callable[[typing.Type["Channel"]], typing.Type["Channel"]]: + """Generates a decorator for channel classes defined in this library to use + to associate themselves with a given channel type. + + Parameters + ---------- + type_ : :obj:`ChannelType` + The channel type to associate with. + + Returns + ------- + ``decorator(cls: T) -> T`` + The decorator to decorate the class with. + """ + def decorator(cls): mapping = getattr(register_channel_type, "types", {}) - mapping[type] = cls + mapping[type_] = cls setattr(register_channel_type, "types", mapping) return cls @@ -233,7 +250,7 @@ class GuildCategory(GuildChannel): """Represents a guild category.""" -@register_channel_type(type=ChannelType.GUILD_TEXT) +@register_channel_type(ChannelType.GUILD_TEXT) @marshaller.attrs(slots=True) class GuildTextChannel(GuildChannel): """Represents a guild text channel.""" @@ -312,10 +329,10 @@ class GuildVoiceChannel(GuildChannel): def deserialize_channel(payload: typing.Dict[str, typing.Any]) -> typing.Union[GuildChannel, DMChannel]: """Deserialize a channel object into the corresponding class. - + Warning ------- - This can only be used to deserialize full channel objects. To deserialize a + This can only be used to deserialize full channel objects. To deserialize a partial object, use :obj:`PartialChannel.deserialize` """ type_id = payload["type"] diff --git a/hikari/core/clients/_run_gateway.py b/hikari/core/clients/_run_gateway.py index 3d0329240c..1a343a0793 100644 --- a/hikari/core/clients/_run_gateway.py +++ b/hikari/core/clients/_run_gateway.py @@ -23,7 +23,6 @@ """ import logging - import click from hikari.core.clients import gateway_client @@ -43,6 +42,7 @@ @click.option("--verify-ssl", default=True, type=click.BOOL, help="Enable or disable SSL verification.") @click.option("--version", default=7, type=click.IntRange(min=6), help="Version of the gateway to use.") def run_gateway(compression, debug, logger, shards, token, url, verify_ssl, version): + """Run the client.""" logging.basicConfig(level=logger) client = gateway_client.GatewayClient( @@ -60,4 +60,4 @@ def run_gateway(compression, debug, logger, shards, token, url, verify_ssl, vers client.run() -run_gateway() +run_gateway() # pylint:disable=no-value-for-parameter diff --git a/hikari/core/clients/app_config.py b/hikari/core/clients/app_config.py index a2f0f7513a..7421031610 100644 --- a/hikari/core/clients/app_config.py +++ b/hikari/core/clients/app_config.py @@ -21,9 +21,10 @@ import typing +from hikari._internal import marshaller from hikari.core import entities -from hikari.core.clients import http_config, gateway_config -from hikari.internal_utilities import marshaller +from hikari.core.clients import gateway_config +from hikari.core.clients import http_config @marshaller.attrs(kw_only=True) diff --git a/hikari/core/clients/bot_client.py b/hikari/core/clients/bot_client.py index 2dc5f05140..59042b1470 100644 --- a/hikari/core/clients/bot_client.py +++ b/hikari/core/clients/bot_client.py @@ -16,4 +16,5 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . +"""A bot client might go here... eventually...""" __all__ = [] diff --git a/hikari/core/clients/gateway_client.py b/hikari/core/clients/gateway_client.py index 5a694dda65..aa95cc240e 100644 --- a/hikari/core/clients/gateway_client.py +++ b/hikari/core/clients/gateway_client.py @@ -16,6 +16,10 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . +"""Defines a facade around :obj:`hikari.core.clients.shard_client.ShardClient` +which provides functionality such as keeping multiple shards alive +simultaneously. +""" __all__ = ["GatewayClient"] import asyncio @@ -23,18 +27,24 @@ import time import typing +from hikari._internal import more_logging from hikari.core import dispatcher from hikari.core import events +from hikari.core import state from hikari.core.clients import gateway_config from hikari.core.clients import shard_client -from hikari.core import state -from hikari.internal_utilities import loggers from hikari.net import shard ShardT = typing.TypeVar("ShardT", bound=shard_client.ShardClient) class GatewayClient(typing.Generic[ShardT], shard_client.WebsocketClientBase, dispatcher.EventDispatcher): + """Facades :obj:`shard_client.ShardClient` implementations to provide a + management layer for multiple-sharded bots. This also provides additional + conduit used to connect up shards to the rest of this framework to enable + management of dispatched events, etc. + """ + def __init__( self, config: gateway_config.GatewayConfig, @@ -43,7 +53,7 @@ def __init__( dispatcher_impl: typing.Optional[dispatcher.EventDispatcher] = None, shard_type: typing.Type[ShardT] = shard_client.ShardClient, ) -> None: - self.logger = loggers.get_named_logger(self) + self.logger = more_logging.get_named_logger(self) self.config = config self.event_dispatcher = dispatcher_impl if dispatcher_impl is not None else dispatcher.EventDispatcherImpl() self._websocket_event_types = self._websocket_events() @@ -75,9 +85,19 @@ async def start(self) -> None: self.logger.info("started %s shard(s) in approx %.2fs", len(self.shards), finish_time - start_time) async def join(self) -> None: + """Wait for all shards to finish executing, then return.""" await asyncio.gather(*(shard_obj.join() for shard_obj in self.shards.values())) async def close(self, wait: bool = True) -> None: + """Close all shards. + + Parameters + ---------- + wait : :obj:`bool` + If ``True`` (the default), then once called, this will wait until + all shards have shut down before returning. If ``False``, it will + only send the signal to shut down, but will return immediately. + """ if self._is_running: self.logger.info("stopping %s shard(s)", len(self.shards)) start_time = time.perf_counter() @@ -136,8 +156,7 @@ async def _handle_websocket_event(self, _: shard.ShardConnection, event_name: st pass else: event_payload = event_type.deserialize(payload) - await self.state.on_event(event_payload) - await self.event_dispatcher.dispatch_event(event_payload) + await self.state.handle_new_event(event_payload) def _websocket_events(self): # Look for anything that has the ___raw_ws_event_name___ class attribute @@ -158,16 +177,3 @@ def predicate(member): self.logger.debug("detected %s web socket events to register from %s", len(types), events.__name__) return types - - def add_listener( - self, event_type: typing.Type[dispatcher.EventT], callback: dispatcher.EventCallbackT - ) -> dispatcher.EventCallbackT: - return self.event_dispatcher.add_listener(event_type, callback) - - def remove_listener( - self, event_type: typing.Type[dispatcher.EventT], callback: dispatcher.EventCallbackT - ) -> dispatcher.EventCallbackT: - return self.event_dispatcher.remove_listener(event_type, callback) - - def dispatch_event(self, event: events.HikariEvent) -> ...: - return self.event_dispatcher.dispatch_event(event) diff --git a/hikari/core/clients/gateway_config.py b/hikari/core/clients/gateway_config.py index cb12c66102..b89c3c0b14 100644 --- a/hikari/core/clients/gateway_config.py +++ b/hikari/core/clients/gateway_config.py @@ -23,12 +23,13 @@ import re import typing +import hikari._internal.conversions +from hikari._internal import assertions +from hikari._internal import marshaller from hikari.core import entities from hikari.core import gateway_entities +from hikari.core import guilds from hikari.core.clients import protocol_config -from hikari.internal_utilities import assertions -from hikari.internal_utilities import dates -from hikari.internal_utilities import marshaller from hikari.net import codes as net_codes @@ -91,6 +92,7 @@ class ShardConfig(entities.HikariEntity, entities.Deserializable): #: :type: :obj:`int` shard_count: int = marshaller.attrib(deserializer=int) + # noinspection PyMissingConstructor def __init__(self, *, shard_ids: typing.Optional[typing.Iterable[int]] = None, shard_count: int) -> None: self.shard_ids = [*shard_ids] if shard_ids else [*range(shard_count)] @@ -124,11 +126,12 @@ class GatewayConfig(entities.HikariEntity, entities.Deserializable): deserializer=gateway_entities.GatewayActivity.deserialize, if_none=None, if_undefined=None, default=None ) - # TODO: implement enum for this #: The initial status to set the shards to when starting the gateway. #: #: :type: :obj:`str` - initial_status: str = marshaller.attrib(deserializer=str, if_undefined=lambda: "online", default="online") + initial_status: guilds.PresenceStatus = marshaller.attrib( + deserializer=guilds.PresenceStatus.__getitem__, if_undefined=lambda: "online", default="online", + ) #: Whether to show up as AFK or not on sign-in. #: @@ -140,7 +143,7 @@ class GatewayConfig(entities.HikariEntity, entities.Deserializable): #: #: :type: :obj:`datetime.datetime`, optional initial_idle_since: typing.Optional[datetime.datetime] = marshaller.attrib( - deserializer=dates.unix_epoch_to_ts, if_none=None, if_undefined=None, default=None + deserializer=hikari._internal.conversions.unix_epoch_to_ts, if_none=None, if_undefined=None, default=None ) #: The intents to use for the connection. diff --git a/hikari/core/clients/http_client.py b/hikari/core/clients/http_client.py index 2dc5f05140..b1664db498 100644 --- a/hikari/core/clients/http_client.py +++ b/hikari/core/clients/http_client.py @@ -16,4 +16,5 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . +"""May contain nuts.""" __all__ = [] diff --git a/hikari/core/clients/http_config.py b/hikari/core/clients/http_config.py index b2107c87ed..7528b98260 100644 --- a/hikari/core/clients/http_config.py +++ b/hikari/core/clients/http_config.py @@ -22,9 +22,9 @@ import typing +from hikari._internal import marshaller from hikari.core import entities from hikari.core.clients import protocol_config -from hikari.internal_utilities import marshaller @marshaller.attrs(kw_only=True) diff --git a/hikari/core/clients/protocol_config.py b/hikari/core/clients/protocol_config.py index a4775c4d1f..572cbfea9d 100644 --- a/hikari/core/clients/protocol_config.py +++ b/hikari/core/clients/protocol_config.py @@ -25,8 +25,8 @@ import aiohttp.typedefs +from hikari._internal import marshaller from hikari.core import entities -from hikari.internal_utilities import marshaller @marshaller.attrs(kw_only=True) diff --git a/hikari/core/clients/shard_client.py b/hikari/core/clients/shard_client.py index 1b7028551a..2187aac7a8 100644 --- a/hikari/core/clients/shard_client.py +++ b/hikari/core/clients/shard_client.py @@ -16,6 +16,13 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . +"""Provides a facade around the :obj:`hikari.net.shard.ShardConnection` +implementation which handles parsing and initializing the object from a +configuration, as well as restarting it if it disconnects. + +Additional functions and coroutines are provided to update the presence on the +shard using models defined in :mod:`hikari.core`. +""" __all__ = ["ShardState", "ShardClient"] import abc @@ -30,11 +37,12 @@ import aiohttp +from hikari._internal import more_asyncio +from hikari._internal import more_logging from hikari.core import events -from hikari.core.clients import gateway_config from hikari.core import gateway_entities -from hikari.internal_utilities import aio -from hikari.internal_utilities import loggers +from hikari.core import guilds +from hikari.core.clients import gateway_config from hikari.net import codes from hikari.net import errors from hikari.net import ratelimits @@ -45,12 +53,23 @@ @enum.unique class ShardState(enum.IntEnum): + """Describes the state of a shard.""" + + #: The shard is not running. NOT_RUNNING = 0 - INITIALIZING = enum.auto() + #: The shard is undergoing the initial connection handshake. + HANDSHAKE = enum.auto() + #: The initialization handshake has completed. We are waiting for the shard + #: to receive the ``READY`` event. WAITING_FOR_READY = enum.auto() + #: The shard is ``READY``. READY = enum.auto() + #: The shard has disconnected and is currently attempting to reconnect + #: again. RECONNECTING = enum.auto() + #: The shard is currently shutting down permanently. STOPPING = enum.auto() + #: The shard has shut down and is no longer connected. STOPPED = enum.auto() @@ -63,17 +82,24 @@ class WebsocketClientBase(abc.ABC): @abc.abstractmethod async def start(self): - ... + """Starts the component.""" @abc.abstractmethod async def close(self, wait: bool = True): - ... + """Shuts down the component.""" @abc.abstractmethod async def join(self): - ... + """Waits for the component to terminate.""" def run(self): + """Performs the same job as :meth:`start`, but provides additional + preparation such as registering OS signal handlers for interrupts, + and preparing the initial event loop. + + This enables the client to be run immediately without having to + set up the :mod:`asyncio` event loop manually first. + """ loop = asyncio.get_event_loop() def sigterm_handler(*_): @@ -156,7 +182,7 @@ def __init__( low_level_dispatch: typing.Callable[["ShardClient", str, typing.Any], None], url: str, ) -> None: - self.logger = loggers.get_named_logger(self, shard_id) + self.logger = more_logging.get_named_logger(self, shard_id) self._dispatch = low_level_dispatch self._activity = config.initial_activity self._idle_since = config.initial_idle_since @@ -203,11 +229,11 @@ def client(self) -> shard.ShardConnection: #: TODO: use enum @property - def status(self) -> str: + def status(self) -> guilds.PresenceStatus: """ Returns ------- - :obj:`str` + :obj:`guilds.PresenceStatus` The current user status for this shard. """ return self._status @@ -253,7 +279,7 @@ async def start(self): raise RuntimeError("Cannot start a shard twice") self.logger.debug("starting shard") - self._shard_state = ShardState.INITIALIZING + self._shard_state = ShardState.HANDSHAKE self._task = asyncio.create_task(self._keep_alive()) self.logger.info("waiting for READY") completed, _ = await asyncio.wait( @@ -277,7 +303,7 @@ async def start(self): async def join(self) -> None: """Wait for the shard to shut down fully.""" - await self._task if self._task is not None else aio.completed_future() + await self._task if self._task is not None else more_asyncio.completed_future() async def close(self, wait: bool = True) -> None: """Request that the shard shuts down. @@ -365,7 +391,7 @@ async def _keep_alive(self): async def update_presence( self, - status: str = ..., # TODO: use enum for status + status: guilds.PresenceStatus = ..., activity: typing.Optional[gateway_entities.GatewayActivity] = ..., idle_since: typing.Optional[datetime.datetime] = ..., is_afk: bool = ..., @@ -382,7 +408,7 @@ async def update_presence( Parameters ---------- - status : :obj:`str` + status : :obj:`guilds.PresenceStatus` The new status to set. activity : :obj:`hikari.core.gateway_entities.GatewayActivity`, optional The new activity to set. @@ -409,7 +435,7 @@ async def update_presence( @staticmethod def _create_presence_pl( - status: str, # TODO: use enum for status + status: guilds.PresenceStatus, activity: typing.Optional[gateway_entities.GatewayActivity], idle_since: typing.Optional[datetime.datetime], is_afk: bool, diff --git a/hikari/core/colors.py b/hikari/core/colors.py index 6b19fcd3fb..07aedddc2f 100644 --- a/hikari/core/colors.py +++ b/hikari/core/colors.py @@ -16,8 +16,8 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . -"""Model that represents a common RGB color and provides -simple conversions to other common color systems. +"""Model that represents a common RGB color and provides simple conversions to +other common color systems. """ __all__ = ["Color", "ColorCompatibleT"] @@ -25,12 +25,12 @@ import string import typing -from hikari.internal_utilities import assertions +from hikari._internal import assertions class Color(int, typing.SupportsInt): """Representation of a color. - + This value is immutable. This is a specialization of :obj:`int` which provides alternative overrides for common methods and color system @@ -120,21 +120,23 @@ def __int__(self): @property def rgb(self) -> typing.Tuple[int, int, int]: - """The RGB representation of this Color. Represented a tuple of R, G, B. Each value is in the - range [0, 0xFF].""" + """The RGB representation of this Color. Represented a tuple of R, G, B. + Each value is in the range [0, 0xFF]. + """ return (self >> 16) & 0xFF, (self >> 8) & 0xFF, self & 0xFF @property def rgb_float(self) -> typing.Tuple[float, float, float]: - """Return the floating-point RGB representation of this Color. Represented as a tuple of R, G, B. - Each value is in the range [0, 1].""" + """Return the floating-point RGB representation of this Color. + Represented as a tuple of R, G, B. Each value is in the range [0, 1]. + """ r, g, b = self.rgb return r / 0xFF, g / 0xFF, b / 0xFF @property def hex_code(self) -> str: - """The six-digit hexadecimal color code for this Color. This is prepended with a ``#`` symbol, - and will bein upper case. + """The six-digit hexadecimal color code for this Color. This is + prepended with a ``#`` symbol, and will be in upper case. Example ------- @@ -161,8 +163,8 @@ def is_web_safe(self) -> bool: @classmethod def from_rgb(cls, red: int, green: int, blue: int) -> "Color": - """Convert the given RGB colorspace represented in values within the range [0, 255]: [0x0, 0xFF], - to a :obj:`Color` object. + """Convert the given RGB colorspace represented in values within the + range [0, 255]: [0x0, 0xFF], to a :obj:`Color` object. Parameters ---------- @@ -191,8 +193,8 @@ def from_rgb(cls, red: int, green: int, blue: int) -> "Color": @classmethod def from_rgb_float(cls, red_f: float, green_f: float, blue_f: float) -> "Color": - """Convert the given RGB colorspace represented using floats in - the range [0, 1] to a :obj:`Color` object. + """Convert the given RGB colorspace represented using floats in the + range [0, 1] to a :obj:`Color` object. Parameters ---------- @@ -281,14 +283,14 @@ def from_int(cls, i: typing.SupportsInt) -> "Color": @classmethod def from_bytes(cls, bytes_: typing.Sequence[int], byteorder: str, *, signed: bool = True) -> "Color": """Converts the color from bytes.""" - return Color(super().from_bytes(bytes_, byteorder, signed=signed)) + return Color(int.from_bytes(bytes_, byteorder, signed=signed)) def to_bytes(self, length: int, byteorder: str, *, signed: bool = True) -> bytes: """Converts the color code to bytes.""" - return super().to_bytes(length, byteorder, signed=signed) + return int(self).to_bytes(length, byteorder, signed=signed) @classmethod - def __class_getitem__(cls, color: "ColorCompatibleT") -> "Color": + def __class_getitem__(cls, color: "ColorCompatibleT") -> "Color": # pylint:disable=arguments-differ if isinstance(color, cls): return color elif isinstance(color, int): diff --git a/hikari/core/dispatcher.py b/hikari/core/dispatcher.py index 3538e9f996..bd7934da3e 100644 --- a/hikari/core/dispatcher.py +++ b/hikari/core/dispatcher.py @@ -25,12 +25,11 @@ import typing import weakref +from hikari._internal import assertions +from hikari._internal import more_asyncio +from hikari._internal import more_collections +from hikari._internal import more_logging from hikari.core import events -from hikari.internal_utilities import aio -from hikari.internal_utilities import assertions -from hikari.internal_utilities import containers -from hikari.internal_utilities import loggers - EventT = typing.TypeVar("EventT", bound=events.HikariEvent) PredicateT = typing.Callable[[EventT], typing.Union[bool, typing.Coroutine[None, None, bool]]] @@ -84,7 +83,7 @@ def remove_listener(self, event_type: typing.Type[EventT], callback: EventCallba @abc.abstractmethod def wait_for( self, event_type: typing.Type[EventT], *, timeout: typing.Optional[float], predicate: PredicateT - ) -> asyncio.Future: + ) -> more_asyncio.Future: """Wait for the given event type to occur. Parameters @@ -184,9 +183,11 @@ class EventDispatcherImpl(EventDispatcher): def __init__(self) -> None: self._listeners: typing.Dict[typing.Type[EventT], typing.List[EventCallbackT]] = {} # pylint: disable=E1136 - self._waiters: typing.Dict[typing.Type[EventT], containers.WeakKeyDictionary[asyncio.Future, PredicateT]] = {} + self._waiters: typing.Dict[ + typing.Type[EventT], more_collections.WeakKeyDictionary[asyncio.Future, PredicateT] + ] = {} # pylint: enable=E1136 - self.logger = loggers.get_named_logger(self) + self.logger = more_logging.get_named_logger(self) def close(self) -> None: """Cancel anything that is waiting for an event to be dispatched.""" @@ -271,7 +272,7 @@ def dispatch_event(self, event: events.HikariEvent): # can be awaited concurrently. futs.append(asyncio.create_task(self._awaken_waiters(waiters, event))) - result = asyncio.gather(*futs) if futs else aio.completed_future() # lgtm [py/unused-local-variable] + result = asyncio.gather(*futs) if futs else more_asyncio.completed_future() # lgtm [py/unused-local-variable] # Stop false positives from linters that now assume this is a coroutine function result: typing.Any @@ -291,7 +292,7 @@ async def _maybe_awaken_waiter(self, event, future, predicate): future.set_result(event) else: delete_waiter = False - except Exception as ex: + except Exception as ex: # pylint:disable=broad-except delete_waiter = True future.set_exception(ex) @@ -306,14 +307,14 @@ async def _maybe_awaken_waiter(self, event, future, predicate): async def _catch(self, callback, event): try: return await callback(event) - except Exception as ex: + except Exception as ex: # pylint:disable=broad-except # Pop the top-most frame to remove this _catch call. # The user doesn't need it in their traceback. ex.__traceback__ = ex.__traceback__.tb_next self.handle_exception(ex, event, callback) def handle_exception( - self, exception: Exception, event: events.HikariEvent, callback: aio.CoroutineFunctionT + self, exception: Exception, event: events.HikariEvent, callback: typing.Callable[..., typing.Awaitable[None]] ) -> None: """Function that is passed any exception. This allows users to override this with a custom implementation if desired. @@ -349,7 +350,7 @@ def handle_exception( def wait_for( self, event_type: typing.Type[EventT], *, timeout: typing.Optional[float], predicate: PredicateT, - ) -> asyncio.Future: + ) -> more_asyncio.Future: """Given an event name, wait for the event to occur once, then return the arguments that accompanied the event as the result. diff --git a/hikari/core/embeds.py b/hikari/core/embeds.py index 5fe9a1097b..13649d3a92 100644 --- a/hikari/core/embeds.py +++ b/hikari/core/embeds.py @@ -33,10 +33,10 @@ import datetime import typing +import hikari._internal.conversions +from hikari._internal import marshaller from hikari.core import colors from hikari.core import entities -from hikari.internal_utilities import dates -from hikari.internal_utilities import marshaller @marshaller.attrs(slots=True) @@ -212,7 +212,7 @@ class Embed(entities.HikariEntity, entities.Deserializable, entities.Serializabl #: #: :type: :obj:`datetime.datetime`, optional timestamp: typing.Optional[datetime.datetime] = marshaller.attrib( - deserializer=dates.parse_iso_8601_ts, + deserializer=hikari._internal.conversions.parse_iso_8601_ts, serializer=lambda timestamp: timestamp.replace(tzinfo=datetime.timezone.utc).isoformat(), if_undefined=None, ) @@ -267,7 +267,7 @@ class Embed(entities.HikariEntity, entities.Deserializable, entities.Serializabl #: #: :type: :obj:`typing.Sequence` [ :obj:`EmbedField` ], optional fields: typing.Optional[typing.Sequence[EmbedField]] = marshaller.attrib( - deserializer=lambda fields: [f for f in map(EmbedField.deserialize, fields)], - serializer=lambda fields: [f for f in map(EmbedField.serialize, fields)], + deserializer=lambda fields: [EmbedField.deserialize(f) for f in fields], + serializer=lambda fields: [f.serialize() for f in fields], if_undefined=None, ) diff --git a/hikari/core/emojis.py b/hikari/core/emojis.py index c99836f792..7dbb7ca644 100644 --- a/hikari/core/emojis.py +++ b/hikari/core/emojis.py @@ -21,10 +21,10 @@ """ import typing +from hikari._internal import marshaller from hikari.core import entities from hikari.core import snowflakes from hikari.core import users -from hikari.internal_utilities import marshaller __all__ = ["Emoji", "UnicodeEmoji", "UnknownEmoji", "GuildEmoji"] diff --git a/hikari/core/entities.py b/hikari/core/entities.py index 63ef9d8c7d..cf784db139 100644 --- a/hikari/core/entities.py +++ b/hikari/core/entities.py @@ -22,9 +22,8 @@ import abc import typing -from hikari.internal_utilities import marshaller -from hikari.internal_utilities import singleton_meta - +from hikari._internal import marshaller +from hikari._internal import meta RawEntityT = typing.Union[ None, bool, int, float, str, bytes, typing.Sequence[typing.Any], typing.Mapping[str, typing.Any] @@ -34,7 +33,9 @@ T_co = typing.TypeVar("T_co", covariant=True) -class Unset(metaclass=singleton_meta.SingletonMeta): +class Unset(metaclass=meta.SingletonMeta): + """A singleton value that represents an unset field.""" + def __bool__(self): return False @@ -57,7 +58,7 @@ class HikariEntity(metaclass=abc.ABCMeta): if typing.TYPE_CHECKING: - def __init__(self, *args, **kwargs) -> None: + def __init__(self, *_, **__) -> None: ... @@ -70,6 +71,9 @@ class Deserializable: @classmethod def deserialize(cls: typing.Type[T_contra], payload: RawEntityT) -> T_contra: + """Deserialize the given payload into this type and return the + constructed object. + """ return marshaller.HIKARI_ENTITY_MARSHALLER.deserialize(payload, cls) @@ -81,4 +85,7 @@ class Serializable: __slots__ = () def serialize(self: T_co) -> RawEntityT: + """Serialize this instance into a naive value such as a + :obj:`dict` and return it. + """ return marshaller.HIKARI_ENTITY_MARSHALLER.serialize(self) diff --git a/hikari/core/events.py b/hikari/core/events.py index 58512c32bd..3a2630209f 100644 --- a/hikari/core/events.py +++ b/hikari/core/events.py @@ -72,10 +72,13 @@ import attr +import hikari._internal.conversions +from hikari._internal import assertions +from hikari._internal import marshaller from hikari.core import channels -from hikari.core import entities from hikari.core import embeds as _embeds from hikari.core import emojis as _emojis +from hikari.core import entities from hikari.core import guilds from hikari.core import invites from hikari.core import messages @@ -83,10 +86,6 @@ from hikari.core import snowflakes from hikari.core import users from hikari.core import voices -from hikari.internal_utilities import aio -from hikari.internal_utilities import assertions -from hikari.internal_utilities import dates -from hikari.internal_utilities import marshaller T_contra = typing.TypeVar("T_contra", contravariant=True) @@ -114,32 +113,37 @@ class ExceptionEvent(HikariEvent): #: The event that was being invoked when the exception occurred. #: - #: :type: :obj`typing.Callable` [ [ :obj:`HikariEvent` ], ``None`` ] - callback: aio.CoroutineFunctionT + #: :type: ``async def`` [ [ :obj:`HikariEvent` ], ``None`` ] + callback: typing.Callable[[HikariEvent], typing.Awaitable[None]] # Synthetic event, is not deserialized @attr.attrs(slots=True, auto_attribs=True) class StartedEvent(HikariEvent): - ... + """Event that is fired when the gateway client starts all shards.""" # Synthetic event, is not deserialized @attr.attrs(slots=True, auto_attribs=True) class StoppingEvent(HikariEvent): - ... + """Event that is fired when the gateway client is instructed to disconnect + all shards. + """ # Synthetic event, is not deserialized @attr.attrs(slots=True, auto_attribs=True) class StoppedEvent(HikariEvent): - ... + """Event that is fired when the gateway client has finished disconnecting + all shards. + """ _websocket_name_break = re.compile(r"(?<=[a-z])(?=[A-Z])") def mark_as_websocket_event(cls): + """Marks the event as being a websocket one. I'll probably delete this.""" name = cls.__name__ assertions.assert_that(name.endswith("Event"), "expected name to be Event") name = name[: -len("Event")] @@ -151,19 +155,19 @@ def mark_as_websocket_event(cls): @mark_as_websocket_event @marshaller.attrs(slots=True) class ConnectedEvent(HikariEvent, entities.Deserializable): - ... + """Event invoked each time a shard connects.""" @mark_as_websocket_event @marshaller.attrs(slots=True) class DisconnectedEvent(HikariEvent, entities.Deserializable): - ... + """Event invoked each time a shard disconnects.""" @mark_as_websocket_event @marshaller.attrs(slots=True) class ReconnectedEvent(HikariEvent, entities.Deserializable): - ... + """Event invoked each time a shard successfully reconnects.""" @mark_as_websocket_event @@ -209,14 +213,14 @@ def shard_id(self) -> typing.Optional[int]: """The zero-indexed id of the current shard, only available if this ready event was received while identifying. .""" - return self._shard_information and self._shard_information[0] or None + return self._shard_information[0] if self._shard_information else None @property def shard_count(self) -> typing.Optional[int]: """The total shard count for this bot, only available if this ready event was received while identifying. """ - return self._shard_information and self._shard_information[1] or None + return self._shard_information[1] if self._shard_information else None @mark_as_websocket_event @@ -339,7 +343,7 @@ class BaseChannelEvent(HikariEvent, snowflakes.UniqueEntity, entities.Deserializ #: #: :type: :obj:`datetime.datetime`, optional last_pin_timestamp: typing.Optional[datetime.datetime] = marshaller.attrib( - deserializer=dates.parse_iso_8601_ts, if_undefined=None + deserializer=hikari._internal.conversions.parse_iso_8601_ts, if_undefined=None ) @@ -391,7 +395,7 @@ class ChannelPinUpdateEvent(HikariEvent, entities.Deserializable): #: #: :type: :obj:`datetime.datetime`, optional last_pin_timestamp: typing.Optional[datetime.datetime] = marshaller.attrib( - deserializer=dates.parse_iso_8601_ts, if_undefined=None + deserializer=hikari._internal.conversions.parse_iso_8601_ts, if_undefined=None ) @@ -555,7 +559,7 @@ class GuildMemberUpdateEvent(HikariEvent, entities.Deserializable): #: #: :type: :obj:`typing.Union` [ :obj:`datetime.datetime`, :obj:`entities.UNSET` ], optional premium_since: typing.Union[None, datetime.datetime, entities.Unset] = marshaller.attrib( - deserializer=dates.parse_iso_8601_ts, if_none=None, if_undefined=entities.Unset + deserializer=hikari._internal.conversions.parse_iso_8601_ts, if_none=None, if_undefined=entities.Unset ) @@ -625,7 +629,7 @@ class InviteCreateEvent(HikariEvent, entities.Deserializable): #: The datetime of when this invite was created. #: #: :type: :obj:`datetime.datetime` - created_at: datetime.datetime = marshaller.attrib(deserializer=dates.parse_iso_8601_ts) + created_at: datetime.datetime = marshaller.attrib(deserializer=hikari._internal.conversions.parse_iso_8601_ts) #: The ID of the guild this invite was created in, if applicable. #: Will be ``None`` for group DM invites. @@ -760,14 +764,14 @@ class MessageUpdateEvent(HikariEvent, snowflakes.UniqueEntity, entities.Deserial #: #: :type: :obj:`typing.Union` [ :obj:`datetime.datetime`, :obj:`entities.UNSET` ] timestamp: typing.Union[datetime.datetime, entities.Unset] = marshaller.attrib( - deserializer=dates.parse_iso_8601_ts, if_undefined=entities.Unset + deserializer=hikari._internal.conversions.parse_iso_8601_ts, if_undefined=entities.Unset ) #: The timestamp that the message was last edited at, or ``None`` if not ever edited. #: #: :type: :obj:`typing.Union` [ :obj:`datetime.datetime`, :obj:`entities.UNSET` ], optional edited_timestamp: typing.Union[datetime.datetime, entities.Unset, None] = marshaller.attrib( - deserializer=dates.parse_iso_8601_ts, if_none=None, if_undefined=entities.Unset + deserializer=hikari._internal.conversions.parse_iso_8601_ts, if_none=None, if_undefined=entities.Unset ) #: Whether the message is a TTS message. diff --git a/hikari/core/gateway_entities.py b/hikari/core/gateway_entities.py index 1538e638af..1a2456e0f7 100644 --- a/hikari/core/gateway_entities.py +++ b/hikari/core/gateway_entities.py @@ -16,13 +16,15 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . +"""Entities directly related to creating and managing gateway shards.""" __all__ = ["GatewayBot", "GatewayActivity"] import datetime import typing +from hikari._internal import marshaller from hikari.core import entities -from hikari.internal_utilities import marshaller +from hikari.core import guilds @marshaller.attrs(slots=True) @@ -68,8 +70,13 @@ class GatewayBot(entities.HikariEntity, entities.Deserializable): session_start_limit: int = marshaller.attrib(deserializer=SessionStartLimit.deserialize) -@marshaller.attrs() +@marshaller.attrs(slots=True) class GatewayActivity(entities.Deserializable, entities.Serializable): + """An activity that the bot can set for one or more shards. + + This will show the activity as the bot's presence. + """ + #: The activity name. #: #: :type: :obj:`str` @@ -80,8 +87,9 @@ class GatewayActivity(entities.Deserializable, entities.Serializable): #: :type: :obj:`str`, optional url: typing.Optional[str] = marshaller.attrib(deserializer=str, serializer=str, if_none=None, if_undefined=None) - # TODO: implement enum for this. #: The activity type. #: - #: :type: :obj:`int` - type: int = marshaller.attrib(deserializer=int, serializer=int, if_undefined=0) + #: :type: :obj:`guilds.ActivityType` + type: guilds.ActivityType = marshaller.attrib( + deserializer=guilds.ActivityType, serializer=int, if_undefined=lambda: guilds.ActivityType.PLAYING + ) diff --git a/hikari/core/guilds.py b/hikari/core/guilds.py index a9d82fc942..b1673ece26 100644 --- a/hikari/core/guilds.py +++ b/hikari/core/guilds.py @@ -42,6 +42,9 @@ import enum import typing +from hikari._internal import cdn +from hikari._internal import conversions +from hikari._internal import marshaller from hikari.core import colors from hikari.core import channels as _channels from hikari.core import emojis as _emojis @@ -49,10 +52,6 @@ from hikari.core import permissions as _permissions from hikari.core import snowflakes from hikari.core import users -from hikari.internal_utilities import cdn -from hikari.internal_utilities import dates -from hikari.internal_utilities import marshaller -from hikari.internal_utilities import transformations @enum.unique @@ -202,14 +201,14 @@ class GuildMember(entities.HikariEntity, entities.Deserializable): #: The datetime of when this member joined the guild they belong to. #: #: :type: :obj:`datetime.datetime` - joined_at: datetime.datetime = marshaller.attrib(deserializer=dates.parse_iso_8601_ts) + joined_at: datetime.datetime = marshaller.attrib(deserializer=conversions.parse_iso_8601_ts) #: The datetime of when this member started "boosting" this guild. #: Will be ``None`` if they aren't boosting. #: #: :type: :obj:`datetime.datetime`, optional premium_since: typing.Optional[datetime.datetime] = marshaller.attrib( - deserializer=dates.parse_iso_8601_ts, if_none=None, if_undefined=None, + deserializer=conversions.parse_iso_8601_ts, if_none=None, if_undefined=None, ) #: Whether this member is deafened by this guild in it's voice channels. @@ -303,13 +302,15 @@ class ActivityTimestamps(entities.HikariEntity, entities.Deserializable): #: #: :type: :obj:`datetime.datetime`, optional start: typing.Optional[datetime.datetime] = marshaller.attrib( - deserializer=dates.unix_epoch_to_ts, if_undefined=None + deserializer=conversions.unix_epoch_to_ts, if_undefined=None ) #: When this activity's session will end, if applicable. #: #: :type: :obj:`datetime.datetime`, optional - end: typing.Optional[datetime.datetime] = marshaller.attrib(deserializer=dates.unix_epoch_to_ts, if_undefined=None) + end: typing.Optional[datetime.datetime] = marshaller.attrib( + deserializer=conversions.unix_epoch_to_ts, if_undefined=None + ) @marshaller.attrs(slots=True) @@ -331,12 +332,12 @@ class ActivityParty(entities.HikariEntity, entities.Deserializable): @property def current_size(self) -> typing.Optional[int]: """The current size of this party, if applicable.""" - return self._size_information and self._size_information[0] or None + return self._size_information[0] if self._size_information else None @property def max_size(self) -> typing.Optional[int]: """The maximum size of this party, if applicable""" - return self._size_information and self._size_information[1] or None + return self._size_information[1] if self._size_information else None @marshaller.attrs(slots=True) @@ -420,7 +421,7 @@ class PresenceActivity(entities.HikariEntity, entities.Deserializable): #: When this activity was added to the user's session. #: #: :type: :obj:`datetime.datetime` - created_at: datetime.datetime = marshaller.attrib(deserializer=dates.unix_epoch_to_ts) + created_at: datetime.datetime = marshaller.attrib(deserializer=conversions.unix_epoch_to_ts) #: The timestamps for when this activity's current state will start and #: end, if applicable. @@ -485,9 +486,7 @@ class PresenceActivity(entities.HikariEntity, entities.Deserializable): class PresenceStatus(enum.Enum): - """ - The status of a member. - """ + """The status of a member.""" #: Online/green. ONLINE = "online" @@ -612,7 +611,7 @@ class GuildMemberPresence(entities.HikariEntity, entities.Deserializable): #: #: :type: :obj:`datetime.datetime`, optional premium_since: typing.Optional[datetime.datetime] = marshaller.attrib( - deserializer=dates.parse_iso_8601_ts, if_none=None, if_undefined=None, + deserializer=conversions.parse_iso_8601_ts, if_none=None, if_undefined=None, ) #: This member's nickname, if set. @@ -716,7 +715,7 @@ class GuildIntegration(snowflakes.UniqueEntity, entities.Deserializable): #: #: :type: :obj:`datetime.datetime` last_synced_at: datetime.datetime = marshaller.attrib( - raw_name="synced_at", deserializer=dates.parse_iso_8601_ts, if_none=None + raw_name="synced_at", deserializer=conversions.parse_iso_8601_ts, if_none=None ) @@ -770,7 +769,7 @@ class PartialGuild(snowflakes.UniqueEntity, entities.Deserializable): #: #: :type: :obj:`typing.Set` [ :obj:`GuildFeature` ] features: typing.Set[GuildFeature] = marshaller.attrib( - deserializer=lambda features: {transformations.try_cast(f, GuildFeature, f) for f in features}, + deserializer=lambda features: {conversions.try_cast(f, GuildFeature, f) for f in features}, ) def format_icon_url(self, fmt: typing.Optional[str] = None, size: int = 2048) -> typing.Optional[str]: @@ -792,7 +791,6 @@ def format_icon_url(self, fmt: typing.Optional[str] = None, size: int = 2048) -> The string url. """ if self.icon_hash: - # pylint: disable=E1101: if fmt is None and self.icon_hash.startswith("a_"): fmt = "gif" elif fmt is None: @@ -990,7 +988,7 @@ class Guild(PartialGuild): #: #: :type: :obj`datetime.datetime`, optional joined_at: typing.Optional[datetime.datetime] = marshaller.attrib( - deserializer=dates.parse_iso_8601_ts, if_undefined=None + deserializer=conversions.parse_iso_8601_ts, if_undefined=None ) #: Whether the guild is considered to be large or not. diff --git a/hikari/core/invites.py b/hikari/core/invites.py index 49af829ddf..0f7a53cab0 100644 --- a/hikari/core/invites.py +++ b/hikari/core/invites.py @@ -24,13 +24,13 @@ import enum import typing +import hikari._internal.conversions +from hikari._internal import cdn +from hikari._internal import marshaller from hikari.core import channels from hikari.core import entities from hikari.core import guilds from hikari.core import users -from hikari.internal_utilities import cdn -from hikari.internal_utilities import dates -from hikari.internal_utilities import marshaller @enum.unique @@ -228,7 +228,7 @@ class InviteWithMetadata(Invite): #: When this invite was created. #: #: :type: :obj:`datetime.datetime` - created_at: datetime.datetime = marshaller.attrib(deserializer=dates.parse_iso_8601_ts) + created_at: datetime.datetime = marshaller.attrib(deserializer=hikari._internal.conversions.parse_iso_8601_ts) @property def expires_at(self) -> typing.Optional[datetime.datetime]: diff --git a/hikari/core/messages.py b/hikari/core/messages.py index a037a32d9b..8262cb2182 100644 --- a/hikari/core/messages.py +++ b/hikari/core/messages.py @@ -30,6 +30,8 @@ import enum import typing +import hikari._internal.conversions +from hikari._internal import marshaller from hikari.core import embeds as _embeds from hikari.core import emojis as _emojis from hikari.core import entities @@ -37,8 +39,6 @@ from hikari.core import oauth2 from hikari.core import snowflakes from hikari.core import users -from hikari.internal_utilities import dates -from hikari.internal_utilities import marshaller @enum.unique @@ -246,13 +246,13 @@ class Message(snowflakes.UniqueEntity, entities.Deserializable): #: The timestamp that the message was sent at. #: #: :type: :obj:`datetime.datetime` - timestamp: datetime.datetime = marshaller.attrib(deserializer=dates.parse_iso_8601_ts) + timestamp: datetime.datetime = marshaller.attrib(deserializer=hikari._internal.conversions.parse_iso_8601_ts) #: The timestamp that the message was last edited at, or ``None`` if not ever edited. #: #: :type: :obj:`datetime.datetime`, optional edited_timestamp: typing.Optional[datetime.datetime] = marshaller.attrib( - deserializer=dates.parse_iso_8601_ts, if_none=None + deserializer=hikari._internal.conversions.parse_iso_8601_ts, if_none=None ) #: Whether the message is a TTS message. diff --git a/hikari/core/oauth2.py b/hikari/core/oauth2.py index 25eb2a58b8..d54d587d5e 100644 --- a/hikari/core/oauth2.py +++ b/hikari/core/oauth2.py @@ -22,13 +22,13 @@ import enum import typing +from hikari._internal import cdn +from hikari._internal import marshaller from hikari.core import entities from hikari.core import guilds from hikari.core import permissions from hikari.core import snowflakes from hikari.core import users -from hikari.internal_utilities import cdn -from hikari.internal_utilities import marshaller @marshaller.attrs(slots=True) diff --git a/hikari/core/snowflakes.py b/hikari/core/snowflakes.py index ad7464bd96..2c6df63e48 100644 --- a/hikari/core/snowflakes.py +++ b/hikari/core/snowflakes.py @@ -26,9 +26,9 @@ import functools import typing +import hikari._internal.conversions +from hikari._internal import marshaller from hikari.core import entities -from hikari.internal_utilities import dates -from hikari.internal_utilities import marshaller @functools.total_ordering @@ -46,14 +46,15 @@ class Snowflake(entities.HikariEntity, typing.SupportsInt): #: :type: :obj:`int` _value: int - def __init__(self, value: typing.Union[int, str]) -> None: + # noinspection PyMissingConstructor + def __init__(self, value: typing.Union[int, str]) -> None: # pylint:disable=super-init-not-called self._value = int(value) @property def created_at(self) -> datetime.datetime: """When the object was created.""" epoch = self._value >> 22 - return dates.discord_epoch_to_datetime(epoch) + return hikari._internal.conversions.discord_epoch_to_datetime(epoch) @property def internal_worker_id(self) -> int: @@ -94,6 +95,9 @@ def serialize(self) -> str: @classmethod def deserialize(cls, value: str) -> "Snowflake": + """Take a serialized string ID and convert it into a Snowflake + object. + """ return cls(value) diff --git a/hikari/core/state.py b/hikari/core/state/__init__.py similarity index 76% rename from hikari/core/state.py rename to hikari/core/state/__init__.py index 40b52205e8..a9983cd84f 100644 --- a/hikari/core/state.py +++ b/hikari/core/state/__init__.py @@ -28,20 +28,19 @@ class StateManager(abc.ABC): """Base type for a state management implementation.""" @abc.abstractmethod - async def on_event(self, event_obj: events.HikariEvent) -> None: - ... + async def handle_new_event(self, event_obj: events.HikariEvent) -> None: + """This is abstract and this is a dummy string.""" class StatelessStateManagerImpl(StateManager): """Stubbed stateless event manager for implementing stateless bots.""" - async def on_event(self, event_obj: events.HikariEvent) -> None: - pass + async def handle_new_event(self, event_obj: events.HikariEvent) -> None: + """Gluten free.""" class StatefulStateManagerImpl(StateManager): """A basic state event manager implementation.""" - async def on_event(self, event_obj: events.HikariEvent) -> None: - # TODO: implement state management - pass + async def handle_new_event(self, event_obj: events.HikariEvent) -> None: + """Sourced from sustainable agricultural plots in Sweden.""" diff --git a/hikari/core/users.py b/hikari/core/users.py index 90489007a5..1fd5a0a33d 100644 --- a/hikari/core/users.py +++ b/hikari/core/users.py @@ -22,10 +22,10 @@ import enum import typing +from hikari._internal import cdn +from hikari._internal import marshaller from hikari.core import entities from hikari.core import snowflakes -from hikari.internal_utilities import cdn -from hikari.internal_utilities import marshaller @marshaller.attrs(slots=True) @@ -86,7 +86,6 @@ def format_avatar_url(self, fmt: typing.Optional[str] = None, size: int = 2048) if not self.avatar_hash: return cdn.generate_cdn_url("embed/avatars", str(self.default_avatar), fmt="png", size=None) - # pylint: disable=E1101: if fmt is None and self.avatar_hash.startswith("a_"): fmt = "gif" elif fmt is None: diff --git a/hikari/core/voices.py b/hikari/core/voices.py index 0961dbc1be..d8d54355e3 100644 --- a/hikari/core/voices.py +++ b/hikari/core/voices.py @@ -22,10 +22,10 @@ import typing +from hikari._internal import marshaller from hikari.core import entities from hikari.core import guilds from hikari.core import snowflakes -from hikari.internal_utilities import marshaller @marshaller.attrs(slots=True) diff --git a/hikari/core/webhooks.py b/hikari/core/webhooks.py index d05ab52e26..bab873c8de 100644 --- a/hikari/core/webhooks.py +++ b/hikari/core/webhooks.py @@ -22,14 +22,16 @@ import enum import typing +from hikari._internal import marshaller from hikari.core import entities from hikari.core import snowflakes from hikari.core import users -from hikari.internal_utilities import marshaller @enum.unique class WebhookType(enum.IntEnum): + """Types of webhook.""" + #: Incoming webhook. INCOMING = 1 #: Channel Follower webhook. @@ -38,7 +40,11 @@ class WebhookType(enum.IntEnum): @marshaller.attrs(slots=True) class Webhook(snowflakes.UniqueEntity, entities.Deserializable): - """Represents a webhook""" + """Represents a webhook object on Discord. This is an endpoint that can have + messages sent to it using standard HTTP requests, which enables external + services that are not bots to send informational messages to specific + channels. + """ #: The type of the webhook. #: diff --git a/hikari/errors.py b/hikari/errors.py index d2c9d9595c..55494b24c7 100644 --- a/hikari/errors.py +++ b/hikari/errors.py @@ -17,7 +17,6 @@ # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . """Core errors that may be raised by this API implementation.""" -__all__ = ["HikariError"] class HikariError(RuntimeError): diff --git a/hikari/internal_utilities/aio.py b/hikari/internal_utilities/aio.py deleted file mode 100644 index 9cd4afadc5..0000000000 --- a/hikari/internal_utilities/aio.py +++ /dev/null @@ -1,58 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# Copyright © Nekokatt 2019-2020 -# -# This file is part of Hikari. -# -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . -"""Asyncio extensions and utilities.""" -__all__ = [ - "CoroutineFunctionT", - "PartialCoroutineProtocolT", - "completed_future", -] - -import asyncio -import typing - -ReturnT = typing.TypeVar("ReturnT", covariant=True) -CoroutineFunctionT = typing.Callable[..., typing.Coroutine[typing.Any, typing.Any, ReturnT]] - - -class PartialCoroutineProtocolT(typing.Protocol[ReturnT]): - """Represents the type of a :obj:`functools.partial` wrapping an :mod:`asyncio` coroutine.""" - - def __call__(self, *args, **kwargs) -> typing.Coroutine[None, None, ReturnT]: - ... - - def __await__(self): - ... - - -def completed_future(result: typing.Any = None) -> asyncio.Future: - """Create a future on the current running loop that is completed, then return it. - - Parameters - --------- - result : :obj:`typing.Any` - The value to set for the result of the future. - - Returns - ------- - :obj:`asyncio.Future` - The completed future. - """ - future = asyncio.get_event_loop().create_future() - future.set_result(result) - return future diff --git a/hikari/internal_utilities/cache.py b/hikari/internal_utilities/cache.py deleted file mode 100644 index 2c5afcd6e3..0000000000 --- a/hikari/internal_utilities/cache.py +++ /dev/null @@ -1,190 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# Copyright © Nekokatt 2019-2020 -# -# This file is part of Hikari. -# -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . -"""Provides mechanisms to cache results of calls lazily.""" -__all__ = [ - "CachedFunctionT", - "CachedPropertyFunctionT", - "CachedFunction", - "CachedProperty", - "AsyncCachedProperty", - "cached_function", - "cached_property", -] - -import asyncio -import functools -import inspect -import os -import typing - -ReturnT = typing.TypeVar("ReturnT") -ClassT = typing.TypeVar("ClassT") -CallT = typing.Callable[..., ReturnT] -CachedFunctionT = typing.Callable[..., ReturnT] -CachedPropertyFunctionT = typing.Callable[[ClassT], ReturnT] - -# Hacky workaround to Sphinx being unable to document cached properties. We simply make the -# decorators return their inputs when this is True. -__is_sphinx = os.getenv("SPHINX_IS_GENERATING_DOCUMENTATION") is not None - - -def __noop_decorator(func): # pragma: no cover - return func - - -class CachedFunction: - """Wraps a call, some arguments, and some keyword arguments in a partial and stores the - result of the call for later invocations. - - Warning - ------- - This is not thread safe! - """ - - _sentinel = object() - __slots__ = ( - "_call", - "_value", - "__qualname__", # pylint: disable=class-variable-slots-conflict - "__dict__", - "__name__", - ) - - def __init__(self, call, args, kwargs): - self._value = self._sentinel - self.__qualname__ = getattr(call, "__qualname__", None) - self.__name__ = getattr(call, "__name__", None) - self.__dict__ = getattr(call, "__dict__", None) - is_coro = inspect.iscoroutinefunction(call) - call_wrapper = self._coroutine_fn_wrapper if is_coro else self._fn_wrapper - self._call = call_wrapper(call, args, kwargs) - - def __call__(self) -> ReturnT: - if self._value is self._sentinel: - self._call() - return self._value - - def _coroutine_fn_wrapper(self, call, args, kwargs): - def fn_wrapper(): - self._value = asyncio.create_task(call(*args, **kwargs), name="pending CachedFunction coroutine completion") - - return fn_wrapper - - def _fn_wrapper(self, call, args, kwargs): - def fn_wrapper(): - self._value = call(*args, **kwargs) - - return fn_wrapper - - -class CachedProperty: - """A get/delete descriptor to wrap a no-args method which can cache the result of the - call for future retrieval. Calling :func:`del` on the property will flush the cache. - - This will misbehave on class methods and static methods, and will not work on - non-instance functions. For general functions, you should consider :obj:`CachedFunction` - instead. - """ - - __slots__ = ( - "func", - "_cache_attr", - "__dict__", - "__name__", - "__qualname__", # pylint: disable=class-variable-slots-conflict - ) - - def __init__(self, func: CachedPropertyFunctionT, cache_attr: typing.Optional[str]) -> None: - self.func = func - self._cache_attr = cache_attr or "_cp_" + func.__name__ - self.__dict__ = getattr(self.func, "__dict__", None) - - def __get__(self, instance: typing.Optional[ClassT], owner: typing.Type[ClassT]) -> ReturnT: - if instance is None: - return typing.cast(ReturnT, self) - if not hasattr(instance, self._cache_attr): - setattr(instance, self._cache_attr, self.func(instance)) - return getattr(instance, self._cache_attr) - - def __delete__(self, instance: ClassT): - try: - delattr(instance, self._cache_attr) - except AttributeError: - pass - - -class AsyncCachedProperty(CachedProperty): - """Cached property implementation that supports awaitable coroutines.""" - - __slots__ = () - - def __get__(self, instance: typing.Optional[ClassT], owner: typing.Type[ClassT]) -> typing.Awaitable[ReturnT]: - if instance is None: - return typing.cast(ReturnT, self) - - if not hasattr(instance, self._cache_attr): - setattr( - instance, - self._cache_attr, - asyncio.create_task(self.func(instance), name="pending AsyncCachedProperty coroutine completion"), - ) - return getattr(instance, self._cache_attr) - - -def cached_function(*args, **kwargs) -> typing.Callable[[CachedFunctionT], typing.Callable[[], ReturnT]]: - """Create a wrapped cached call decorator. - - This remembers the last result of the given call forever until cleared. - - Parameters - ----------- - *args - Any arguments to call the call with. - **kwargs - Any kwargs to call the call with. - - Note - ---- - This is not useful for instance methods on classes, you should use - a :obj:`CachedProperty` instead for those. You should also not expect - thread safety here. Coroutines will be detected and dealt with as futures. - This is lazily evaluated. - """ - - def decorator(func): - return functools.wraps(func)(CachedFunction(func, args, kwargs)) - - return decorator if not __is_sphinx else __noop_decorator - - -def cached_property( - *, cache_name=None -) -> typing.Callable[[CachedPropertyFunctionT], typing.Union[CachedProperty, AsyncCachedProperty]]: - """Makes a slots-compatible cached property. - - If using slots, you should specify the ``cache_name`` directly. - """ - - def decorator(func: CachedPropertyFunctionT) -> typing.Union[CachedProperty, AsyncCachedProperty]: - cls = AsyncCachedProperty if asyncio.iscoroutinefunction(func) else CachedProperty - return typing.cast( - typing.Union[CachedProperty, AsyncCachedProperty], functools.wraps(func)(cls(func, cache_name)) - ) - - return decorator if not __is_sphinx else __noop_decorator diff --git a/hikari/internal_utilities/dates.py b/hikari/internal_utilities/dates.py deleted file mode 100644 index 686e7ad9f4..0000000000 --- a/hikari/internal_utilities/dates.py +++ /dev/null @@ -1,115 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# Copyright © Nekokatt 2019-2020 -# -# This file is part of Hikari. -# -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . -"""Date/Time utilities.""" -__all__ = ["parse_http_date", "parse_iso_8601_ts", "discord_epoch_to_datetime", "unix_epoch_to_ts"] - -import datetime -import email.utils -import re - - -def parse_http_date(date_str: str) -> datetime.datetime: - """Return the HTTP date as a datetime object. - - Parameters - ---------- - date_str : :obj:`str` - The RFC-2822 (section 3.3) compliant date string to parse. - - See also - -------- - ``_ - """ - return email.utils.parsedate_to_datetime(date_str) - - -ISO_8601_DATE_PART = re.compile(r"^(\d{4})-(\d{2})-(\d{2})") -# We don't always consistently get the subsecond part, it would seem. I have only -# observed this in raw_guild_members_chunk, however... -ISO_8601_TIME_PART = re.compile(r"T(\d{2}):(\d{2}):(\d{2})(?:\.(\d{1,6}))?", re.I) -ISO_8601_TZ_PART = re.compile(r"([+-])(\d{2}):(\d{2})$") - - -def parse_iso_8601_ts(date_string: str) -> datetime.datetime: - """Parses an ISO 8601 date string into a datetime object - - Parameters - ---------- - date_string : :obj:`str` - The ISO 8601 compliant date string to parse. - - See also - -------- - ``_ - """ - year, month, day = map(int, ISO_8601_DATE_PART.findall(date_string)[0]) - - time_part = ISO_8601_TIME_PART.findall(date_string)[0] - hour, minute, second, partial = time_part - - # Pad the millisecond part if it is not in microseconds, otherwise Python will complain. - partial = partial + (6 - len(partial)) * "0" - hour, minute, second, partial = int(hour), int(minute), int(second), int(partial) - if date_string.endswith(("Z", "z")): - timezone = datetime.timezone.utc - else: - sign, tz_hour, tz_minute = ISO_8601_TZ_PART.findall(date_string)[0] - tz_hour, tz_minute = int(tz_hour), int(tz_minute) - offset = datetime.timedelta(hours=tz_hour, minutes=tz_minute) - if sign == "-": - offset = -offset - timezone = datetime.timezone(offset) - - return datetime.datetime(year, month, day, hour, minute, second, partial, timezone) - - -#: This represents the 1st January 2015 as the number of seconds since 1st January 1970 (Discord epoch) -DISCORD_EPOCH = 1_420_070_400 - - -def discord_epoch_to_datetime(epoch: int) -> datetime.datetime: - """Parses a discord epoch into a datetime object - - Parameters - ---------- - epoch : :obj:`int` - Number of milliseconds since 1/1/2015 (UTC) - - Returns - ------- - :obj:`datetime.datetime` - Number of seconds since 1/1/1970 within a datetime object (UTC). - """ - return datetime.datetime.fromtimestamp(epoch / 1000 + DISCORD_EPOCH, datetime.timezone.utc) - - -def unix_epoch_to_ts(epoch: int) -> datetime.datetime: - """Parses a unix epoch to a datetime object - - Parameters - ---------- - epoch : :obj:`int` - Number of milliseconds since 1/1/1970 (UTC) - - Returns - ------- - :obj:`datetime.datetime` - Number of seconds since 1/1/1970 within a datetime object (UTC). - """ - return datetime.datetime.fromtimestamp(epoch / 1000, datetime.timezone.utc) diff --git a/hikari/internal_utilities/storage.py b/hikari/internal_utilities/storage.py deleted file mode 100644 index 97d833f587..0000000000 --- a/hikari/internal_utilities/storage.py +++ /dev/null @@ -1,56 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# Copyright © Nekokatt 2019-2020 -# -# This file is part of Hikari. -# -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . -"""IO utilities.""" -__all__ = ["make_resource_seekable", "FileLikeT", "BytesLikeT"] - -import io -import typing - - -def make_resource_seekable(resource: typing.Any) -> typing.Union[io.BytesIO, io.StringIO]: - """Given some representation of data, make a seekable resource to use. - - This supports :obj:`bytes`, :obj:`bytearray`, :obj:`memoryview`, and :obj:`str`. - Anything else is just returned. - - Parameters - ---------- - resource : :obj:`typing.Any` - The resource to check. - - Returns - ------- - :obj:`typing.Union` [ :obj:`io.BytesIO`, :obj:`io.StringIO` ] - An stream-compatible resource where possible. - """ - if isinstance(resource, (bytes, bytearray)): - resource = io.BytesIO(resource) - elif isinstance(resource, memoryview): - resource = io.BytesIO(resource.tobytes()) - elif isinstance(resource, str): - resource = io.StringIO(resource) - - return resource - - -#: A bytes-like object, such as a :obj:`str`, raw :obj:`bytes`, or view across a bytes-like object. -BytesLikeT = typing.Union[bytes, bytearray, memoryview, str, io.StringIO, io.BytesIO] - -#: Type description for any object that can be considered to be file-like. -FileLikeT = typing.Union[BytesLikeT, io.BufferedRandom, io.BufferedReader, io.BufferedRWPair] diff --git a/hikari/internal_utilities/transformations.py b/hikari/internal_utilities/transformations.py deleted file mode 100644 index eaf41c36d1..0000000000 --- a/hikari/internal_utilities/transformations.py +++ /dev/null @@ -1,132 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# Copyright © Nekokatt 2019-2020 -# -# This file is part of Hikari. -# -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . -"""Basic transformation utilities.""" -__all__ = [ - "CastInputT", - "CastOutputT", - "DefaultT", - "TypeCastT", - "ResultT", - "nullable_cast", - "try_cast", - "put_if_specified", - "image_bytes_to_image_data", -] - -import base64 -import contextlib -import typing - -CastInputT = typing.TypeVar("CastInputT") -CastOutputT = typing.TypeVar("CastOutputT") -DefaultT = typing.TypeVar("DefaultT") -TypeCastT = typing.Callable[[CastInputT], CastOutputT] -ResultT = typing.Union[CastOutputT, DefaultT] - - -def nullable_cast(value: CastInputT, cast: TypeCastT) -> ResultT: - """ - Attempts to cast the given ``value`` with the given ``cast``, but only if the - ``value`` is not ``None``. If it is ``None``, then ``None`` is returned instead. - """ - if value is None: - return None - return cast(value) - - -def try_cast(value: CastInputT, cast: TypeCastT, default: DefaultT = None) -> ResultT: - """Try to cast the given value to the given cast. - - If it throws a :obj:`Exception` or derivative, it will return ``default`` instead - of the cast value instead. - """ - with contextlib.suppress(Exception): - return cast(value) - return default - - -def put_if_specified( - mapping: typing.Dict[typing.Hashable, typing.Any], - key: typing.Hashable, - value: typing.Any, - type_after: typing.Optional[TypeCastT] = None, -) -> None: - """Add a value to the mapping under the given key as long as the value is not :obj:`typing.Literal` - - Parameters - ---------- - mapping : :obj:`typing.Dict` [ :obj:`typing.Hashable`, :obj:`typing.Any` ] - The mapping to add to. - key : :obj:`typing.Hashable` - The key to add the value under. - value : :obj:`typing.Any` - The value to add. - type_after : :obj:`TypeCastT`, optional - Type to apply to the value when added. - """ - if value is not ...: - if type_after: - mapping[key] = type_after(value) - else: - mapping[key] = value - - -def image_bytes_to_image_data(img_bytes: typing.Optional[bytes] = None) -> typing.Optional[str]: - """Encode image bytes into an image data string. - - Parameters - ---------- - img_bytes : :obj:`bytes`, optional - The image bytes. - - Raises - ------ - :obj:`ValueError` - If the image type passed is not supported. - - Returns - ------- - :obj:`str`, optional - The ``image_bytes`` given encoded into an image data string or ``None``. - - Note - ---- - Supported image types: ``.png``, ``.jpeg``, ``.jfif``, ``.gif``, ``.webp`` - """ - if img_bytes is None: - return None - - if img_bytes[:8] == b"\211PNG\r\n\032\n": - img_type = "image/png" - elif img_bytes[6:10] in (b"Exif", b"JFIF"): - img_type = "image/jpeg" - elif img_bytes[:6] in (b"GIF87a", b"GIF89a"): - img_type = "image/gif" - elif img_bytes.startswith(b"RIFF") and img_bytes[8:12] == b"WEBP": - img_type = "image/webp" - else: - raise ValueError("Unsupported image type passed") - - image_data = base64.b64encode(img_bytes).decode() - - return f"data:{img_type};base64,{image_data}" - - -def try_cast_or_defer_unary_operator(type_): - return lambda data: try_cast(data, type_, data) diff --git a/hikari/net/errors.py b/hikari/net/errors.py index 5f906647e6..ba41c479be 100644 --- a/hikari/net/errors.py +++ b/hikari/net/errors.py @@ -57,6 +57,7 @@ class GatewayError(errors.HikariError): reason: str def __init__(self, reason: str) -> None: + super().__init__() self.reason = reason def __str__(self) -> str: @@ -191,6 +192,7 @@ class HTTPError(errors.HikariError): reason: str def __init__(self, reason: str) -> None: + super().__init__() self.reason = reason def __str__(self) -> str: diff --git a/hikari/net/ratelimits.py b/hikari/net/ratelimits.py index a648e6b0a8..acb2087ad4 100644 --- a/hikari/net/ratelimits.py +++ b/hikari/net/ratelimits.py @@ -87,12 +87,12 @@ provide the :obj:`RealBucketHash` we need to get the route's bucket object internally. -The :meth:`acquire` method will take the bucket and acquire a new timeslice on -it. This takes the form of a :obj:`asyncio.Future` which should be awaited by +The :meth:`acquire` method will take the bucket and acquire a new timeslice on +it. This takes the form of a :obj:`asyncio.Future` which should be awaited by the caller and will complete once the caller is allowed to make a request. Most -of the time, this is done instantly, but if the bucket has an active rate limit -preventing requests being sent, then the future will be paused until the rate -limit is over. This may be longer than the rate limit period if you have queued +of the time, this is done instantly, but if the bucket has an active rate limit +preventing requests being sent, then the future will be paused until the rate +limit is over. This may be longer than the rate limit period if you have queued a large number of requests during this limit, as it is first-come-first-served. Acquiring a rate limited bucket will start a bucket-wide task (if not already @@ -119,18 +119,18 @@ the vital rate limit headers manually and parse them to the correct data types. These headers are: -* ``Date``: +* ``Date``: the response date on the server. This should be parsed to a :obj:`datetime.datetime` using :func:`email.utils.parsedate_to_datetime`. -* ``X-RateLimit-Limit``: +* ``X-RateLimit-Limit``: an :obj:`int` describing the max requests in the bucket from empty to being rate limited. -* ``X-RateLimit-Remaining``: +* ``X-RateLimit-Remaining``: an :obj:`int` describing the remaining number of requests before rate limiting occurs in the current window. -* ``X-RateLimit-Bucket``: +* ``X-RateLimit-Bucket``: a :obj:`str` containing the initial bucket hash. -* ``X-RateLimit-Reset``: +* ``X-RateLimit-Reset``: a :obj:`float` containing the number of seconds since 1st January 1970 at 0:00:00 UTC at which the current ratelimit window resets. This should be parsed to a :obj:`datetime` using @@ -138,8 +138,8 @@ :obj:`datetime.timezone.utc` as a second parameter. Each of the above values should be passed to the -:meth:`update_rate_limits` method to ensure that the bucket you acquired time -from is correctly updated should Discord decide to alter their ratelimits on the +:meth:`update_rate_limits` method to ensure that the bucket you acquired time +from is correctly updated should Discord decide to alter their ratelimits on the fly without warning (including timings and the bucket). This method will manage creating new buckets as needed and resetting vital @@ -161,7 +161,7 @@ ratelimited calls that may be waiting to be unlocked. """ __all__ = [ - "IRateLimiter", + "BaseRateLimiter", "BurstRateLimiter", "ManualRateLimiter", "WindowedBurstRateLimiter", @@ -176,17 +176,18 @@ import logging import random import time +import types import typing import weakref -from hikari.internal_utilities import aio -from hikari.internal_utilities import loggers +from hikari._internal import more_asyncio +from hikari._internal import more_logging from hikari.net import routes UNKNOWN_HASH = "UNKNOWN" -class IRateLimiter(abc.ABC): +class BaseRateLimiter(abc.ABC): """Base for any asyncio-based rate limiter being used. Supports being used as a synchronous context manager. @@ -199,7 +200,7 @@ class IRateLimiter(abc.ABC): __slots__ = () @abc.abstractmethod - def acquire(self) -> asyncio.Future: + def acquire(self) -> more_asyncio.Future[None]: """Acquire permission to perform a task that needs to have rate limit management enforced. @@ -221,7 +222,7 @@ def __exit__(self, exc_type, exc_val, exc_tb): self.close() -class BurstRateLimiter(IRateLimiter, abc.ABC): +class BurstRateLimiter(BaseRateLimiter, abc.ABC): """Base implementation for a burst-based rate limiter. This provides an internal queue and throttling placeholder, as well as @@ -238,26 +239,26 @@ class BurstRateLimiter(IRateLimiter, abc.ABC): #: The throttling task, or ``None``` if it isn't running. #: #: :type: :obj:`asyncio.Task`, optional - throttle_task: typing.Optional[asyncio.Task] + throttle_task: typing.Optional[more_asyncio.Task[None]] #: The queue of any futures under a rate limit. #: #: :type: :obj:`asyncio.Queue` [`asyncio.Future`] - queue: asyncio.Queue + queue: typing.List[more_asyncio.Future[None]] #: The logger used by this rate limiter. #: #: :type: :obj:`logging.Logger` logger: logging.Logger - def __init__(self, name): + def __init__(self, name: str) -> None: self.name = name - self.throttle_task: typing.Optional[asyncio.Task] = None + self.throttle_task = None self.queue = [] - self.logger: logging.Logger = loggers.get_named_logger(self) + self.logger: logging.Logger = more_logging.get_named_logger(self) @abc.abstractmethod - def acquire(self) -> asyncio.Future: + def acquire(self) -> more_asyncio.Future[None]: """Acquire time on this rate limiter. The implementation should define this. @@ -321,7 +322,7 @@ class ManualRateLimiter(BurstRateLimiter): def __init__(self) -> None: super().__init__("global HTTP") - def acquire(self) -> asyncio.Future: + def acquire(self) -> more_asyncio.Future[None]: """Acquire time on this rate limiter. Returns @@ -454,7 +455,7 @@ def __init__(self, name: str, period: float, limit: int) -> None: self.limit = limit self.period = period - def acquire(self) -> asyncio.Future: + def acquire(self) -> more_asyncio.Future[None]: """Acquire time on this rate limiter. Returns @@ -535,7 +536,7 @@ def is_rate_limited(self, now: float) -> bool: return self.remaining <= 0 - def drip(self): + def drip(self) -> None: """Decrements the remaining counter.""" self.remaining -= 1 @@ -612,7 +613,7 @@ def is_unknown(self) -> bool: """Return ``True`` if the bucket represents an ``UNKNOWN`` bucket.""" return self.name.startswith(UNKNOWN_HASH) - def acquire(self) -> asyncio.Future: + def acquire(self) -> more_asyncio.Future[None]: """Acquire time on this rate limiter. Returns @@ -626,7 +627,7 @@ def acquire(self) -> asyncio.Future: You should afterwards invoke :meth:`update_rate_limit` to update any rate limit information you are made aware of. """ - return aio.completed_future(None) if self.is_unknown else super().acquire() + return more_asyncio.completed_future(None) if self.is_unknown else super().acquire() def update_rate_limit(self, remaining: int, limit: int, reset_at: float) -> None: """Amend the rate limit. @@ -700,7 +701,7 @@ class HTTPBucketRateLimiterManager: #: The internal garbage collector task. #: #: :type: :obj:`asyncio.Task`, optional - gc_task: typing.Optional[asyncio.Task] + gc_task: typing.Optional[more_asyncio.Task[None]] #: The logger to use for this object. #: @@ -712,15 +713,15 @@ def __init__(self) -> None: self.real_hashes_to_buckets = {} self.closed_event: asyncio.Event = asyncio.Event() self.gc_task: typing.Optional[asyncio.Task] = None - self.logger: logging.Logger = loggers.get_named_logger(self) + self.logger: logging.Logger = more_logging.get_named_logger(self) - def __enter__(self): + def __enter__(self) -> "HTTPBucketRateLimiterManager": return self - def __exit__(self, exc_type, exc_val, exc_tb): + def __exit__(self, exc_type: typing.Type[Exception], exc_val: Exception, exc_tb: types.TracebackType) -> None: self.close() - def __del__(self): + def __del__(self) -> None: self.close() def start(self, poll_period: float = 20) -> None: @@ -777,14 +778,11 @@ async def gc(self, poll_period: float = 20) -> None: try: await asyncio.wait_for(self.closed_event.wait(), timeout=poll_period) except asyncio.TimeoutError: - try: - self.logger.debug("performing rate limit garbage collection pass") - self.do_gc_pass() - except Exception as ex: - self.logger.exception("ignoring garbage collection error for rate limits", exc_info=ex) + self.logger.debug("performing rate limit garbage collection pass") + self.do_gc_pass() self.gc_task = None - def do_gc_pass(self): + def do_gc_pass(self) -> None: """Perform a single garbage collection pass. This will assess any routes stored in the internal mappings of this @@ -814,7 +812,7 @@ def do_gc_pass(self): self.logger.debug("purged %s stale buckets", len(buckets_to_purge)) - def acquire(self, compiled_route: routes.CompiledRoute) -> asyncio.Future: + def acquire(self, compiled_route: routes.CompiledRoute) -> more_asyncio.Future: """Acquire a bucket for the given route. Parameters @@ -921,7 +919,7 @@ class ExponentialBackOff: single iteration before an :obj:`asyncio.TimeoutError` is raised. Defaults to ``64`` seconds. jitter_multiplier : :obj:`float` - The multiplier for the random jitter. Defaults to ``1``. + The multiplier for the random jitter. Defaults to ``1``. Set to ``0`` to disable jitter. """ @@ -967,10 +965,10 @@ def __next__(self) -> float: value += random.random() * self.jitter_multiplier # nosec return value - def __iter__(self): + def __iter__(self) -> "ExponentialBackOff": """Returns this object, as it is an iterator.""" return self - def reset(self): + def reset(self) -> None: """Resets the exponential back-off.""" self.increment = 0 diff --git a/hikari/net/rest.py b/hikari/net/rest.py index 409944734a..a7e171431b 100644 --- a/hikari/net/rest.py +++ b/hikari/net/rest.py @@ -30,11 +30,10 @@ import aiohttp.typedefs -from hikari.internal_utilities import assertions -from hikari.internal_utilities import containers -from hikari.internal_utilities import loggers -from hikari.internal_utilities import storage -from hikari.internal_utilities import transformations +from hikari._internal import assertions +from hikari._internal import conversions +from hikari._internal import more_collections +from hikari._internal import more_logging from hikari.net import codes from hikari.net import errors from hikari.net import ratelimits @@ -88,12 +87,12 @@ def __init__( #: The logger to use for this object. #: #: :type: :obj:`logging.Logger` - self.logger = loggers.get_named_logger(self) + self.logger = more_logging.get_named_logger(self) #: User agent to use. #: #: :type: :obj:`str` - self.user_agent = user_agent.user_agent() + self.user_agent = user_agent.UserAgent().user_agent #: If ``True``, this will enforce SSL signed certificate verification, otherwise it will #: ignore potentially malicious SSL certificates. @@ -147,10 +146,13 @@ def __init__( self.token = token async def close(self): + """Shut down the REST client safely, and terminate any rate limiters + executing in the background. + """ with contextlib.suppress(Exception): self.ratelimiter.close() with contextlib.suppress(Exception): - self.logger.debug("Closing HTTPClient") + self.logger.debug("Closing %s", type(self).__qualname__) await self.client_session.close() def __enter__(self) -> typing.NoReturn: @@ -169,12 +171,12 @@ async def _request( headers=None, query=None, form_body=None, - json_body: typing.Optional[typing.Union[typing.Dict, typing.Sequence[typing.Any]]] = None, - reason: typing.Union[typing.Literal[...], str] = ..., - re_seekable_resources: typing.Collection[typing.Any] = containers.EMPTY_COLLECTION, + json_body: typing.Optional[typing.Union[typing.Dict[str, typing.Any], typing.Sequence[typing.Any]]] = None, + reason: str = ..., + re_seekable_resources: typing.Collection[typing.Any] = more_collections.EMPTY_COLLECTION, suppress_authorization_header: bool = False, **kwargs, - ) -> typing.Union[typing.Dict, typing.Sequence[typing.Any], None]: + ) -> typing.Union[typing.Dict[str, typing.Any], typing.Sequence[typing.Any], None]: bucket_ratelimit_future = self.ratelimiter.acquire(compiled_route) request_headers = {"X-RateLimit-Precision": "millisecond"} @@ -264,7 +266,7 @@ async def _request( body = None elif content_type == "application/json": body = self.json_deserialize(raw_body) - elif content_type == "text/plain" or content_type == "text/html": + elif content_type in ("text/plain", "text/html"): await self._handle_bad_response( backoff, status, @@ -297,13 +299,13 @@ async def _request( if status == codes.HTTPStatusCode.BAD_REQUEST: raise errors.BadRequestHTTPError(compiled_route, message, code) - elif status == codes.HTTPStatusCode.UNAUTHORIZED: + if status == codes.HTTPStatusCode.UNAUTHORIZED: raise errors.UnauthorizedHTTPError(compiled_route, message, code) - elif status == codes.HTTPStatusCode.FORBIDDEN: + if status == codes.HTTPStatusCode.FORBIDDEN: raise errors.ForbiddenHTTPError(compiled_route, message, code) - elif status == codes.HTTPStatusCode.NOT_FOUND: + if status == codes.HTTPStatusCode.NOT_FOUND: raise errors.NotFoundHTTPError(compiled_route, message, code) - elif status < codes.HTTPStatusCode.INTERNAL_SERVER_ERROR: + if status < codes.HTTPStatusCode.INTERNAL_SERVER_ERROR: raise errors.ClientHTTPError(status, compiled_route, message, code) await self._handle_bad_response(backoff, status, compiled_route, message, code) @@ -340,11 +342,11 @@ async def get_gateway(self) -> str: result = await self._request(routes.GATEWAY.compile(self.GET)) return result["url"] - async def get_gateway_bot(self) -> typing.Dict: + async def get_gateway_bot(self) -> typing.Dict[str, typing.Any]: """ Returns ------- - :obj:`typing.Dict` + :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ]` An object containing a ``url`` to connect to, an :obj:`int` number of shards recommended to use for connecting, and a ``session_start_limit`` object. @@ -355,12 +357,7 @@ async def get_gateway_bot(self) -> typing.Dict: return await self._request(routes.GATEWAY_BOT.compile(self.GET)) async def get_guild_audit_log( - self, - guild_id: str, - *, - user_id: typing.Union[typing.Literal[...], str] = ..., - action_type: typing.Union[typing.Literal[...], int] = ..., - limit: typing.Union[typing.Literal[...], int] = ..., + self, guild_id: str, *, user_id: str = ..., action_type: int = ..., limit: int = ..., ) -> typing.Dict: """Get an audit log object for the given guild. @@ -373,12 +370,12 @@ async def get_guild_audit_log( action_type : :obj:`int` If specified, the action type to look up. limit : :obj:`int` - If specified, the limit to apply to the number of records. + If specified, the limit to apply to the number of records. Defaults to ``50``. Must be between ``1`` and ``100`` inclusive. Returns ------- - :obj:`typing.Dict` + :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ]` An audit log object. Raises @@ -389,13 +386,13 @@ async def get_guild_audit_log( If the guild does not exist. """ query = {} - transformations.put_if_specified(query, "user_id", user_id) - transformations.put_if_specified(query, "action_type", action_type) - transformations.put_if_specified(query, "limit", limit) + conversions.put_if_specified(query, "user_id", user_id) + conversions.put_if_specified(query, "action_type", action_type) + conversions.put_if_specified(query, "limit", limit) route = routes.GUILD_AUDIT_LOGS.compile(self.GET, guild_id=guild_id) return await self._request(route, query=query) - async def get_channel(self, channel_id: str) -> typing.Dict: + async def get_channel(self, channel_id: str) -> typing.Dict[str, typing.Any]: """Get a channel object from a given channel ID. Parameters @@ -405,7 +402,7 @@ async def get_channel(self, channel_id: str) -> typing.Dict: Returns ------- - :obj:`typing.Dict` + :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ]` The channel object that has been found. Raises @@ -422,17 +419,17 @@ async def modify_channel( # lgtm [py/similar-function] self, channel_id: str, *, - name: typing.Union[typing.Literal[...], str] = ..., - position: typing.Union[typing.Literal[...], int] = ..., - topic: typing.Union[typing.Literal[...], str] = ..., - nsfw: typing.Union[typing.Literal[...], bool] = ..., - rate_limit_per_user: typing.Union[typing.Literal[...], int] = ..., - bitrate: typing.Union[typing.Literal[...], int] = ..., - user_limit: typing.Union[typing.Literal[...], int] = ..., - permission_overwrites: typing.Union[typing.Literal[...], typing.Sequence[typing.Dict]] = ..., - parent_id: typing.Union[typing.Literal[...], str] = ..., - reason: typing.Union[typing.Literal[...], str] = ..., - ) -> typing.Dict: + name: str = ..., + position: int = ..., + topic: str = ..., + nsfw: bool = ..., + rate_limit_per_user: int = ..., + bitrate: int = ..., + user_limit: int = ..., + permission_overwrites: typing.Sequence[typing.Dict[str, typing.Any]] = ..., + parent_id: str = ..., + reason: str = ..., + ) -> typing.Dict[str, typing.Any]: """Update one or more aspects of a given channel ID. Parameters @@ -440,43 +437,43 @@ async def modify_channel( # lgtm [py/similar-function] channel_id : :obj:`str` The channel ID to update. name : :obj:`str` - If specified, the new name for the channel.This must be + If specified, the new name for the channel.This must be between ``2`` and ``100`` characters in length. position : :obj:`int` If specified, the position to change the channel to. topic : :obj:`str` - If specified, the topic to set. This is only applicable to - text channels. This must be between ``0`` and ``1024`` + If specified, the topic to set. This is only applicable to + text channels. This must be between ``0`` and ``1024`` characters in length. nsfw : :obj:`bool` - If specified, wheather the channel will be marked as NSFW. + If specified, wheather the channel will be marked as NSFW. Only applicable to text channels. rate_limit_per_user : :obj:`int` - If specified, the number of seconds the user has to wait before sending - another message. This will not apply to bots, or to members with - ``MANAGE_MESSAGES`` or ``MANAGE_CHANNEL`` permissions. This must + If specified, the number of seconds the user has to wait before sending + another message. This will not apply to bots, or to members with + ``MANAGE_MESSAGES`` or ``MANAGE_CHANNEL`` permissions. This must be between ``0`` and ``21600`` seconds. bitrate : :obj:`int` - If specified, the bitrate in bits per second allowable for the channel. - This only applies to voice channels and must be between ``8000`` - and ``96000`` for normal servers or ``8000`` and ``128000`` for + If specified, the bitrate in bits per second allowable for the channel. + This only applies to voice channels and must be between ``8000`` + and ``96000`` for normal servers or ``8000`` and ``128000`` for VIP servers. user_limit : :obj:`int` - If specified, the new max number of users to allow in a voice channel. - This must be between ``0`` and ``99`` inclusive, where + If specified, the new max number of users to allow in a voice channel. + This must be between ``0`` and ``99`` inclusive, where ``0`` implies no limit. - permission_overwrites : :obj:`typing.Sequence` [ :obj:`typing.Dict` ] - If specified, the new list of permission overwrites that are category + permission_overwrites : :obj:`typing.Sequence` [ :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ]` ] + If specified, the new list of permission overwrites that are category specific to replace the existing overwrites with. parent_id : :obj:`str` If specified, the new parent category ID to set for the channel. reason : :obj:`str` - If specified, the audit log reason explaining why the operation + If specified, the audit log reason explaining why the operation was performed. - + Returns ------- - :obj:`typing.Dict` + :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ]` The channel object that has been modified. Raises @@ -486,19 +483,19 @@ async def modify_channel( # lgtm [py/similar-function] :obj:`hikari.net.errors.ForbiddenHTTPError` If you lack the permission to make the change. :obj:`hikari.net.errors.BadRequestHTTPError` - If you provide incorrect options for the corresponding channel type + If you provide incorrect options for the corresponding channel type (e.g. a ``bitrate`` for a text channel). """ payload = {} - transformations.put_if_specified(payload, "name", name) - transformations.put_if_specified(payload, "position", position) - transformations.put_if_specified(payload, "topic", topic) - transformations.put_if_specified(payload, "nsfw", nsfw) - transformations.put_if_specified(payload, "rate_limit_per_user", rate_limit_per_user) - transformations.put_if_specified(payload, "bitrate", bitrate) - transformations.put_if_specified(payload, "user_limit", user_limit) - transformations.put_if_specified(payload, "permission_overwrites", permission_overwrites) - transformations.put_if_specified(payload, "parent_id", parent_id) + conversions.put_if_specified(payload, "name", name) + conversions.put_if_specified(payload, "position", position) + conversions.put_if_specified(payload, "topic", topic) + conversions.put_if_specified(payload, "nsfw", nsfw) + conversions.put_if_specified(payload, "rate_limit_per_user", rate_limit_per_user) + conversions.put_if_specified(payload, "bitrate", bitrate) + conversions.put_if_specified(payload, "user_limit", user_limit) + conversions.put_if_specified(payload, "permission_overwrites", permission_overwrites) + conversions.put_if_specified(payload, "parent_id", parent_id) route = routes.CHANNEL.compile(self.PATCH, channel_id=channel_id) return await self._request(route, json_body=payload, reason=reason) @@ -513,7 +510,7 @@ async def delete_close_channel(self, channel_id: str) -> None: Returns ------- ``None`` - Nothing, unlike what the API specifies. This is done to maintain + Nothing, unlike what the API specifies. This is done to maintain consistency with other calls of a similar nature in this API wrapper. Raises @@ -531,15 +528,9 @@ async def delete_close_channel(self, channel_id: str) -> None: await self._request(route) async def get_channel_messages( - self, - channel_id: str, - *, - limit: typing.Union[typing.Literal[...], int] = ..., - after: typing.Union[typing.Literal[...], str] = ..., - before: typing.Union[typing.Literal[...], str] = ..., - around: typing.Union[typing.Literal[...], str] = ..., - ) -> typing.Sequence[typing.Dict]: - """Retrieve message history for a given channel. + self, channel_id: str, *, limit: int = ..., after: str = ..., before: str = ..., around: str = ..., + ) -> typing.Sequence[typing.Dict[str, typing.Any]]: + """Retrieve message history for a given channel. If a user is provided, retrieve the DM history. Parameters @@ -547,8 +538,8 @@ async def get_channel_messages( channel_id : :obj:`str` The ID of the channel to retrieve the messages from. limit : :obj:`int` - If specified, the number of messages to return. Must be - between ``1`` and ``100`` inclusive.Defaults to ``50`` + If specified, the number of messages to return. Must be + between ``1`` and ``100`` inclusive.Defaults to ``50`` if unspecified. after : :obj:`str` A message ID. If specified, only return messages sent AFTER this message. @@ -556,10 +547,10 @@ async def get_channel_messages( A message ID. If specified, only return messages sent BEFORE this message. around : :obj:`str` A message ID. If specified, only return messages sent AROUND this message. - + Returns ------- - :obj:`typing.Sequence` [ :obj:`typing.Dict` ] + :obj:`typing.Sequence` [ :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ]` ] A list of message objects. Raises @@ -567,17 +558,17 @@ async def get_channel_messages( :obj:`hikari.net.errors.ForbiddenHTTPError` If you lack permission to read the channel. :obj:`hikari.net.errors.BadRequestHTTPError` - If your query is malformed, has an invalid value for ``limit``, + If your query is malformed, has an invalid value for ``limit``, or contains more than one of ``after``, ``before`` and ``around``. :obj:`hikari.net.errors.NotFoundHTTPError` - If the channel is not found, or the message + If the channel is not found, or the message provided for one of the filter arguments is not found. - + Note ---- - If you are missing the ``VIEW_CHANNEL`` permission, you will receive a - :obj:`hikari.net.errors.ForbiddenHTTPError`. If you are instead missing - the ``READ_MESSAGE_HISTORY`` permission, you will always receive + If you are missing the ``VIEW_CHANNEL`` permission, you will receive a + :obj:`hikari.net.errors.ForbiddenHTTPError`. If you are instead missing + the ``READ_MESSAGE_HISTORY`` permission, you will always receive zero results, and thus an empty list will be returned instead. Warning @@ -586,14 +577,14 @@ async def get_channel_messages( Specifying more than one will cause a :obj:`hikari.net.errors.BadRequestHTTPError` to be raised. """ query = {} - transformations.put_if_specified(query, "limit", limit) - transformations.put_if_specified(query, "before", before) - transformations.put_if_specified(query, "after", after) - transformations.put_if_specified(query, "around", around) + conversions.put_if_specified(query, "limit", limit) + conversions.put_if_specified(query, "before", before) + conversions.put_if_specified(query, "after", after) + conversions.put_if_specified(query, "around", around) route = routes.CHANNEL_MESSAGES.compile(self.GET, channel_id=channel_id) return await self._request(route, query=query) - async def get_channel_message(self, channel_id: str, message_id: str) -> typing.Dict: + async def get_channel_message(self, channel_id: str, message_id: str) -> typing.Dict[str, typing.Any]: """Get the message with the given message ID from the channel with the given channel ID. Parameters @@ -605,7 +596,7 @@ async def get_channel_message(self, channel_id: str, message_id: str) -> typing. Returns ------- - :obj:`typing.Dict` + :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ]` A message object. Note @@ -626,13 +617,13 @@ async def create_message( self, channel_id: str, *, - content: typing.Union[typing.Literal[...], str] = ..., - nonce: typing.Union[typing.Literal[...], str] = ..., - tts: typing.Union[typing.Literal[...], bool] = ..., - files: typing.Union[typing.Literal[...], typing.Sequence[typing.Tuple[str, storage.FileLikeT]]] = ..., - embed: typing.Union[typing.Literal[...], typing.Dict] = ..., - allowed_mentions: typing.Union[typing.Literal[...], typing.Dict] = ..., - ) -> typing.Dict: + content: str = ..., + nonce: str = ..., + tts: bool = ..., + files: typing.Sequence[typing.Tuple[str, conversions.FileLikeT]] = ..., + embed: typing.Dict[str, typing.Any] = ..., + allowed_mentions: typing.Dict[str, typing.Any] = ..., + ) -> typing.Dict[str, typing.Any]: """Create a message in the given channel or DM. Parameters @@ -642,25 +633,25 @@ async def create_message( content : :obj:`str` If specified, the message content to send with the message. nonce : :obj:`str` - If specified, an optional ID to send for opportunistic message - creation. This doesn't serve any real purpose for general use, + If specified, an optional ID to send for opportunistic message + creation. This doesn't serve any real purpose for general use, and can usually be ignored. tts : :obj:`bool` If specified, whether the message will be sent as a TTS message. files : :obj:`typing.Sequence` [ :obj:`typing.Tuple` [ :obj:`str`, :obj:`storage.FileLikeT` ] ] - If specified, this should be a list of between ``1`` and ``5`` tuples. - Each tuple should consist of the file name, and either - raw :obj:`bytes` or an :obj:`io.IOBase` derived object with + If specified, this should be a list of between ``1`` and ``5`` tuples. + Each tuple should consist of the file name, and either + raw :obj:`bytes` or an :obj:`io.IOBase` derived object with a seek that points to a buffer containing said file. - embed : :obj:`typing.Dict` + embed : :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ]` If specified, the embed to send with the message. - allowed_mentions : :obj:`typing.Dict` - If specified, the mentions to parse from the ``content``. + allowed_mentions : :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ]` + If specified, the mentions to parse from the ``content``. If not specified, will parse all mentions from the ``content``. Returns ------- - :obj:`typing.Dict` + :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ]` The created message object. Raises @@ -668,12 +659,12 @@ async def create_message( :obj:`hikari.net.errors.NotFoundHTTPError` If the channel is not found. :obj:`hikari.net.errors.BadRequestHTTPError` - This can be raised if the file is too large; if the embed exceeds - the defined limits; if the message content is specified only and - empty or greater than ``2000`` characters; if neither content, file + This can be raised if the file is too large; if the embed exceeds + the defined limits; if the message content is specified only and + empty or greater than ``2000`` characters; if neither content, file or embed are specified; if there is a duplicate id in only of the - fields in ``allowed_mentions``; if you specify to parse all - users/roles mentions but also specify which users/roles to + fields in ``allowed_mentions``; if you specify to parse all + users/roles mentions but also specify which users/roles to parse only. :obj:`hikari.net.errors.ForbiddenHTTPError` If you lack permissions to send to this channel. @@ -681,18 +672,18 @@ async def create_message( form = aiohttp.FormData() json_payload = {} - transformations.put_if_specified(json_payload, "content", content) - transformations.put_if_specified(json_payload, "nonce", nonce) - transformations.put_if_specified(json_payload, "tts", tts) - transformations.put_if_specified(json_payload, "embed", embed) - transformations.put_if_specified(json_payload, "allowed_mentions", allowed_mentions) + conversions.put_if_specified(json_payload, "content", content) + conversions.put_if_specified(json_payload, "nonce", nonce) + conversions.put_if_specified(json_payload, "tts", tts) + conversions.put_if_specified(json_payload, "embed", embed) + conversions.put_if_specified(json_payload, "allowed_mentions", allowed_mentions) form.add_field("payload_json", json.dumps(json_payload), content_type="application/json") re_seekable_resources = [] if files is not ...: for i, (file_name, file) in enumerate(files): - file = storage.make_resource_seekable(file) + file = conversions.make_resource_seekable(file) re_seekable_resources.append(file) form.add_field(f"file{i}", file, filename=file_name, content_type="application/octet-stream") @@ -709,15 +700,15 @@ async def create_reaction(self, channel_id: str, message_id: str, emoji: str) -> message_id : :obj:`str` The ID of the message to add the reaction in. emoji : :obj:`str` - The emoji to add. This can either be a series of unicode - characters making up a valid Discord emoji, or it can be a + The emoji to add. This can either be a series of unicode + characters making up a valid Discord emoji, or it can be a snowflake ID for a custom emoji. Raises ------ :obj:`hikari.net.errors.ForbiddenHTTPError` - If this is the first reaction using this specific emoji on this - message and you lack the ``ADD_REACTIONS`` permission. If you lack + If this is the first reaction using this specific emoji on this + message and you lack the ``ADD_REACTIONS`` permission. If you lack ``READ_MESSAGE_HISTORY``, this may also raise this error. :obj:`hikari.net.errors.NotFoundHTTPError` If the channel or message is not found, or if the emoji is not found. @@ -737,8 +728,8 @@ async def delete_own_reaction(self, channel_id: str, message_id: str, emoji: str message_id : :obj:`str` The ID of the message to delete the reaction from. emoji : :obj:`str` - The emoji to delete. This can either be a series of unicode - characters making up a valid Discord emoji, or it can be a + The emoji to delete. This can either be a series of unicode + characters making up a valid Discord emoji, or it can be a snowflake ID for a custom emoji. Raises @@ -761,8 +752,8 @@ async def delete_all_reactions_for_emoji(self, channel_id: str, message_id: str, message_id : :obj:`str` The ID of the message to delete the reactions from. emoji : :obj:`str` - The emoji to delete. This can either be a series of unicode - characters making up a valid Discord emoji, or it can be a + The emoji to delete. This can either be a series of unicode + characters making up a valid Discord emoji, or it can be a snowflake ID for a custom emoji. Raises @@ -785,8 +776,8 @@ async def delete_user_reaction(self, channel_id: str, message_id: str, emoji: st message_id : :obj:`str` The ID of the message to remove the reaction from. emoji : :obj:`str` - The emoji to delete. This can either be a series of unicode - characters making up a valid Discord emoji, or it can be a + The emoji to delete. This can either be a series of unicode + characters making up a valid Discord emoji, or it can be a snowflake ID for a custom emoji. user_id : :obj:`str` The ID of the user who made the reaction that you wish to remove. @@ -804,15 +795,9 @@ async def delete_user_reaction(self, channel_id: str, message_id: str, emoji: st await self._request(route) async def get_reactions( - self, - channel_id: str, - message_id: str, - emoji: str, - *, - after: typing.Union[typing.Literal[...], str] = ..., - limit: typing.Union[typing.Literal[...], int] = ..., - ) -> typing.Sequence[typing.Dict]: - """Get a list of users who reacted with the given emoji on + self, channel_id: str, message_id: str, emoji: str, *, after: str = ..., limit: int = ..., + ) -> typing.Sequence[typing.Dict[str, typing.Any]]: + """Get a list of users who reacted with the given emoji on the given message in the given channel or user DM. Parameters @@ -822,20 +807,20 @@ async def get_reactions( message_id : :obj:`str` The ID of the message to get the reactions from. emoji : :obj:`str` - The emoji to get. This can either be a series of unicode - characters making up a valid Discord emoji, or it can be a + The emoji to get. This can either be a series of unicode + characters making up a valid Discord emoji, or it can be a snowflake ID for a custom emoji. after : :obj:`str` - If specified, the user ID. If specified, only users with a snowflake + If specified, the user ID. If specified, only users with a snowflake that is lexicographically greater thanthe value will be returned. limit : :obj:`str` - If specified, the limit of the number of values to return. Must be - between ``1`` and ``100`` inclusive. If unspecified, + If specified, the limit of the number of values to return. Must be + between ``1`` and ``100`` inclusive. If unspecified, defaults to ``25``. Returns ------- - :obj:`typing.Sequence` [ :obj:`typing.Dict` ] + :obj:`typing.Sequence` [ :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ]` ] A list of user objects. Raises @@ -846,8 +831,8 @@ async def get_reactions( If the channel or message is not found. """ query = {} - transformations.put_if_specified(query, "after", after) - transformations.put_if_specified(query, "limit", limit) + conversions.put_if_specified(query, "after", after) + conversions.put_if_specified(query, "limit", limit) route = routes.REACTIONS.compile(self.GET, channel_id=channel_id, message_id=message_id, emoji=emoji) return await self._request(route, query=query) @@ -876,22 +861,22 @@ async def edit_message( channel_id: str, message_id: str, *, - content: typing.Optional[typing.Union[typing.Literal[...], str]] = ..., - embed: typing.Optional[typing.Union[typing.Literal[...], typing.Dict]] = ..., - flags: typing.Union[typing.Literal[...], int] = ..., - ) -> typing.Dict: + content: typing.Optional[str] = ..., + embed: typing.Optional[typing.Dict[str, typing.Any]] = ..., + flags: int = ..., + ) -> typing.Dict[str, typing.Any]: """Update the given message. Parameters ---------- channel_id : :obj:`str` The ID of the channel to get the message from. - message_id : :obj:`str` + message_id : :obj:`str` The ID of the message to edit. content : :obj:`str`, optional If specified, the string content to replace with in the message. If ``None``, the content will be removed from the message. - embed : :obj:`typing.Dict`, optional + embed : :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ]`, optional If specified, the embed to replace with in the message. If ``None``, the embed will be removed from the message. flags : :obj:`int` @@ -899,7 +884,7 @@ async def edit_message( Returns ------- - :obj:`typing.Dict` + :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ]` The edited message object. Raises @@ -907,9 +892,9 @@ async def edit_message( :obj:`hikari.net.errors.NotFoundHTTPError` If the channel or message is not found. :obj:`hikari.net.errors.BadRequestHTTPError` - This can be raised if the embed exceeds the defined limits; - if the message content is specified only and empty or greater - than ``2000`` characters; if neither content, file or embed + This can be raised if the embed exceeds the defined limits; + if the message content is specified only and empty or greater + than ``2000`` characters; if neither content, file or embed are specified. parse only. :obj:`hikari.net.errors.ForbiddenHTTPError` @@ -917,9 +902,9 @@ async def edit_message( on a message you did not author without the ``MANAGE_MESSAGES`` permission. """ payload = {} - transformations.put_if_specified(payload, "content", content) - transformations.put_if_specified(payload, "embed", embed) - transformations.put_if_specified(payload, "flags", flags) + conversions.put_if_specified(payload, "content", content) + conversions.put_if_specified(payload, "embed", embed) + conversions.put_if_specified(payload, "flags", flags) route = routes.CHANNEL_MESSAGE.compile(self.PATCH, channel_id=channel_id, message_id=message_id) return await self._request(route, json_body=payload) @@ -961,14 +946,16 @@ async def bulk_delete_messages(self, channel_id: str, messages: typing.Sequence[ :obj:`hikari.net.errors.ForbiddenHTTPError` If you lack the ``MANAGE_MESSAGES`` permission in the channel. :obj:`hikari.net.errors.BadRequestHTTPError` - If any of the messages passed are older than ``2`` weeks in age or any duplicate message IDs are passed. + If any of the messages passed are older than ``2`` weeks in age or + any duplicate message IDs are passed. Note ---- This can only be used on guild text channels. - Any message IDs that do not exist or are invalid still add towards the total ``100`` max messages to remove. - This can only delete messages that are newer than ``2`` weeks in age. If any of the messages are older than ``2`` weeks - then this call will fail. + Any message IDs that do not exist or are invalid still add towards the + total ``100`` max messages to remove. This can only delete messages that + are newer than ``2`` weeks in age. If any of the messages are older than + ``2`` weeks then this call will fail. """ payload = {"messages": messages} route = routes.CHANNEL_MESSAGES_BULK_DELETE.compile(self.POST, channel_id=channel_id) @@ -979,10 +966,10 @@ async def edit_channel_permissions( channel_id: str, overwrite_id: str, *, - allow: typing.Union[typing.Literal[...], int] = ..., - deny: typing.Union[typing.Literal[...], int] = ..., - type_: typing.Union[typing.Literal[...], str] = ..., - reason: typing.Union[typing.Literal[...], str] = ..., + allow: int = ..., + deny: int = ..., + type_: str = ..., + reason: str = ..., ) -> None: """Edit permissions for a given channel. @@ -997,10 +984,10 @@ async def edit_channel_permissions( deny : :obj:`int` If specified, the bitwise value of all permissions to set to be denied. type_ : :obj:`str` - If specified, the type of overwrite. ``"member"`` if it is for a member, + If specified, the type of overwrite. ``"member"`` if it is for a member, or ``"role"`` if it is for a role. reason : :obj:`str` - If specified, the audit log reason explaining why the operation + If specified, the audit log reason explaining why the operation was performed. Raises @@ -1011,13 +998,13 @@ async def edit_channel_permissions( If you lack permission to do this. """ payload = {} - transformations.put_if_specified(payload, "allow", allow) - transformations.put_if_specified(payload, "deny", deny) - transformations.put_if_specified(payload, "type", type_) + conversions.put_if_specified(payload, "allow", allow) + conversions.put_if_specified(payload, "deny", deny) + conversions.put_if_specified(payload, "type", type_) route = routes.CHANNEL_PERMISSIONS.compile(self.PATCH, channel_id=channel_id, overwrite_id=overwrite_id) await self._request(route, json_body=payload, reason=reason) - async def get_channel_invites(self, channel_id: str) -> typing.Sequence[typing.Dict]: + async def get_channel_invites(self, channel_id: str) -> typing.Sequence[typing.Dict[str, typing.Any]]: """Get invites for a given channel. Parameters @@ -1027,7 +1014,7 @@ async def get_channel_invites(self, channel_id: str) -> typing.Sequence[typing.D Returns ------- - :obj:`typing.Sequence` [ :obj:`typing.Dict` ] + :obj:`typing.Sequence` [ :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ]` ] A list of invite objects. Raises @@ -1044,14 +1031,14 @@ async def create_channel_invite( self, channel_id: str, *, - max_age: typing.Union[typing.Literal[...], int] = ..., - max_uses: typing.Union[typing.Literal[...], int] = ..., - temporary: typing.Union[typing.Literal[...], bool] = ..., - unique: typing.Union[typing.Literal[...], bool] = ..., - target_user: typing.Union[typing.Literal[...], str] = ..., - target_user_type: typing.Union[typing.Literal[...], int] = ..., - reason: typing.Union[typing.Literal[...], str] = ..., - ) -> typing.Dict: + max_age: int = ..., + max_uses: int = ..., + temporary: bool = ..., + unique: bool = ..., + target_user: str = ..., + target_user_type: int = ..., + reason: str = ..., + ) -> typing.Dict[str, typing.Any]: """Create a new invite for the given channel. Parameters @@ -1059,14 +1046,14 @@ async def create_channel_invite( channel_id : :obj:`str` The ID of the channel to create the invite for. max_age : :obj:`int` - If specified, the max age of the invite in seconds, defaults to - ``86400`` (``24`` hours). + If specified, the max age of the invite in seconds, defaults to + ``86400`` (``24`` hours). Set to ``0`` to never expire. max_uses : :obj:`int` - If specified, the max number of uses this invite can have, or ``0`` for + If specified, the max number of uses this invite can have, or ``0`` for unlimited (as per the default). temporary : :obj:`bool` - If specified, whether to grant temporary membership, meaning the user + If specified, whether to grant temporary membership, meaning the user is kicked when their session ends unless they are given a role. unique : :obj:`bool` If specified, whether to try to reuse a similar invite. @@ -1075,12 +1062,12 @@ async def create_channel_invite( target_user_type : :obj:`int` If specified, the type of target for this invite. reason : :obj:`str` - If specified, the audit log reason explaining why the operation + If specified, the audit log reason explaining why the operation was performed. Returns ------- - :obj:`typing.Dict` + :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ]` An invite object. Raises @@ -1093,12 +1080,12 @@ async def create_channel_invite( If the arguments provided are not valid (e.g. negative age, etc). """ payload = {} - transformations.put_if_specified(payload, "max_age", max_age) - transformations.put_if_specified(payload, "max_uses", max_uses) - transformations.put_if_specified(payload, "temporary", temporary) - transformations.put_if_specified(payload, "unique", unique) - transformations.put_if_specified(payload, "target_user", target_user) - transformations.put_if_specified(payload, "target_user_type", target_user_type) + conversions.put_if_specified(payload, "max_age", max_age) + conversions.put_if_specified(payload, "max_uses", max_uses) + conversions.put_if_specified(payload, "temporary", temporary) + conversions.put_if_specified(payload, "unique", unique) + conversions.put_if_specified(payload, "target_user", target_user) + conversions.put_if_specified(payload, "target_user_type", target_user_type) route = routes.CHANNEL_INVITES.compile(self.POST, channel_id=channel_id) return await self._request(route, json_body=payload, reason=reason) @@ -1128,7 +1115,7 @@ async def trigger_typing_indicator(self, channel_id: str) -> None: Parameters ---------- channel_id : :obj:`str` - The ID of the channel to appear to be typing in. This may be + The ID of the channel to appear to be typing in. This may be a user ID if you wish to appear to be typing in DMs. Raises @@ -1141,7 +1128,7 @@ async def trigger_typing_indicator(self, channel_id: str) -> None: route = routes.CHANNEL_TYPING.compile(self.POST, channel_id=channel_id) await self._request(route) - async def get_pinned_messages(self, channel_id: str) -> typing.Sequence[typing.Dict]: + async def get_pinned_messages(self, channel_id: str) -> typing.Sequence[typing.Dict[str, typing.Any]]: """Get pinned messages for a given channel. Parameters @@ -1151,7 +1138,7 @@ async def get_pinned_messages(self, channel_id: str) -> typing.Sequence[typing.D Returns ------- - :obj:`typing.Sequence` [ :obj:`typing.Dict` ] + :obj:`typing.Sequence` [ :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ]` ] A list of messages. Raises @@ -1163,7 +1150,7 @@ async def get_pinned_messages(self, channel_id: str) -> typing.Sequence[typing.D Note ---- - If you are not able to see the pinned message (eg. you are missing ``READ_MESSAGE_HISTORY`` + If you are not able to see the pinned message (eg. you are missing ``READ_MESSAGE_HISTORY`` and the pinned message is an old message), it will not be returned. """ route = routes.CHANNEL_PINS.compile(self.GET, channel_id=channel_id) @@ -1190,8 +1177,8 @@ async def add_pinned_channel_message(self, channel_id: str, message_id: str) -> await self._request(route) async def delete_pinned_channel_message(self, channel_id: str, message_id: str) -> None: - """Remove a pinned message from the channel. - + """Remove a pinned message from the channel. + This will only unpin the message, not delete it. Parameters @@ -1211,7 +1198,7 @@ async def delete_pinned_channel_message(self, channel_id: str, message_id: str) route = routes.CHANNEL_PIN.compile(self.DELETE, channel_id=channel_id, message_id=message_id) await self._request(route) - async def list_guild_emojis(self, guild_id: str) -> typing.Sequence[typing.Dict]: + async def list_guild_emojis(self, guild_id: str) -> typing.Sequence[typing.Dict[str, typing.Any]]: """Gets emojis for a given guild ID. Parameters @@ -1221,7 +1208,7 @@ async def list_guild_emojis(self, guild_id: str) -> typing.Sequence[typing.Dict] Returns ------- - :obj:`typing.Sequence` [ :obj:`typing.Dict` ] + :obj:`typing.Sequence` [ :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ]` ] A list of emoji objects. Raises @@ -1234,7 +1221,7 @@ async def list_guild_emojis(self, guild_id: str) -> typing.Sequence[typing.Dict] route = routes.GUILD_EMOJIS.compile(self.GET, guild_id=guild_id) return await self._request(route) - async def get_guild_emoji(self, guild_id: str, emoji_id: str) -> typing.Dict: + async def get_guild_emoji(self, guild_id: str, emoji_id: str) -> typing.Dict[str, typing.Any]: """Gets an emoji from a given guild and emoji IDs. Parameters @@ -1246,7 +1233,7 @@ async def get_guild_emoji(self, guild_id: str, emoji_id: str) -> typing.Dict: Returns ------- - :obj:`typing.Dict` + :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ]` An emoji object. Raises @@ -1260,14 +1247,8 @@ async def get_guild_emoji(self, guild_id: str, emoji_id: str) -> typing.Dict: return await self._request(route) async def create_guild_emoji( - self, - guild_id: str, - name: str, - image: bytes, - *, - roles: typing.Union[typing.Literal[...], typing.Sequence[str]] = ..., - reason: typing.Union[typing.Literal[...], str] = ..., - ) -> typing.Dict: + self, guild_id: str, name: str, image: bytes, *, roles: typing.Sequence[str] = ..., reason: str = ..., + ) -> typing.Dict[str, typing.Any]: """Creates a new emoji for a given guild. Parameters @@ -1279,15 +1260,15 @@ async def create_guild_emoji( image : :obj:`bytes` The ``128x128`` image in bytes form. roles : :obj:`typing.Sequence` [ :obj:`str` ] - If specified, a list of roles for which the emoji will be whitelisted. + If specified, a list of roles for which the emoji will be whitelisted. If empty, all roles are whitelisted. reason : :obj:`str` - If specified, the audit log reason explaining why the operation + If specified, the audit log reason explaining why the operation was performed. Returns ------- - :obj:`typing.Dict` + :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ]` The newly created emoji object. Raises @@ -1305,20 +1286,14 @@ async def create_guild_emoji( payload = { "name": name, "roles": [] if roles is ... else roles, - "image": transformations.image_bytes_to_image_data(image), + "image": conversions.image_bytes_to_image_data(image), } route = routes.GUILD_EMOJIS.compile(self.POST, guild_id=guild_id) return await self._request(route, json_body=payload, reason=reason) async def modify_guild_emoji( - self, - guild_id: str, - emoji_id: str, - *, - name: typing.Union[typing.Literal[...], str] = ..., - roles: typing.Union[typing.Literal[...], typing.Sequence[str]] = ..., - reason: typing.Union[typing.Literal[...], str] = ..., - ) -> typing.Dict: + self, guild_id: str, emoji_id: str, *, name: str = ..., roles: typing.Sequence[str] = ..., reason: str = ..., + ) -> typing.Dict[str, typing.Any]: """Edits an emoji of a given guild Parameters @@ -1334,12 +1309,12 @@ async def modify_guild_emoji( Set to an empty list to whitelist all roles. Keep unspecified to leave the same roles already set. reason : :obj:`str` - If specified, the audit log reason explaining why the operation + If specified, the audit log reason explaining why the operation was performed. Returns ------- - :obj:`typing.Dict` + :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ]` The updated emoji object. Raises @@ -1350,8 +1325,8 @@ async def modify_guild_emoji( If you either lack the ``MANAGE_EMOJIS`` permission or are not a member of the given guild. """ payload = {} - transformations.put_if_specified(payload, "name", name) - transformations.put_if_specified(payload, "roles", roles) + conversions.put_if_specified(payload, "name", name) + conversions.put_if_specified(payload, "roles", roles) route = routes.GUILD_EMOJI.compile(self.PATCH, guild_id=guild_id, emoji_id=emoji_id) return await self._request(route, json_body=payload, reason=reason) @@ -1379,14 +1354,14 @@ async def create_guild( self, name: str, *, - region: typing.Union[typing.Literal[...], str] = ..., - icon: typing.Union[typing.Literal[...], bytes] = ..., - verification_level: typing.Union[typing.Literal[...], int] = ..., - default_message_notifications: typing.Union[typing.Literal[...], int] = ..., - explicit_content_filter: typing.Union[typing.Literal[...], int] = ..., - roles: typing.Union[typing.Literal[...], typing.Sequence[typing.Dict]] = ..., - channels: typing.Union[typing.Literal[...], typing.Sequence[typing.Dict]] = ..., - ) -> typing.Dict: + region: str = ..., + icon: bytes = ..., + verification_level: int = ..., + default_message_notifications: int = ..., + explicit_content_filter: int = ..., + roles: typing.Sequence[typing.Dict[str, typing.Any]] = ..., + channels: typing.Sequence[typing.Dict[str, typing.Any]] = ..., + ) -> typing.Dict[str, typing.Any]: """Creates a new guild. Can only be used by bots in less than ``10`` guilds. Parameters @@ -1394,7 +1369,7 @@ async def create_guild( name : :obj:`str` The name string for the new guild (``2-100`` characters). region : :obj:`str` - If specified, the voice region ID for new guild. You can use + If specified, the voice region ID for new guild. You can use :meth:`list_voice_regions` to see which region IDs are available. icon : :obj:`bytes` If specified, the guild icon image in bytes form. @@ -1404,15 +1379,15 @@ async def create_guild( If specified, the default notification level integer (``0-1``). explicit_content_filter : :obj:`int` If specified, the explicit content filter integer (``0-2``). - roles : :obj:`typing.Sequence` [ :obj:`typing.Dict` ] - If specified, an array of role objects to be created alongside the + roles : :obj:`typing.Sequence` [ :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ]` ] + If specified, an array of role objects to be created alongside the guild. First element changes the ``@everyone`` role. - channels : :obj:`typing.Sequence` [ :obj:`typing.Dict` ] + channels : :obj:`typing.Sequence` [ :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ]` ] If specified, an array of channel objects to be created alongside the guild. Returns ------- - :obj:`typing.Dict` + :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ]` The newly created guild object. Raises @@ -1423,17 +1398,17 @@ async def create_guild( If you provide unsupported fields like ``parent_id`` in channel objects. """ payload = {"name": name} - transformations.put_if_specified(payload, "region", region) - transformations.put_if_specified(payload, "verification_level", verification_level) - transformations.put_if_specified(payload, "default_message_notifications", default_message_notifications) - transformations.put_if_specified(payload, "explicit_content_filter", explicit_content_filter) - transformations.put_if_specified(payload, "roles", roles) - transformations.put_if_specified(payload, "channels", channels) - transformations.put_if_specified(payload, "icon", icon, transformations.image_bytes_to_image_data) + conversions.put_if_specified(payload, "region", region) + conversions.put_if_specified(payload, "verification_level", verification_level) + conversions.put_if_specified(payload, "default_message_notifications", default_message_notifications) + conversions.put_if_specified(payload, "explicit_content_filter", explicit_content_filter) + conversions.put_if_specified(payload, "roles", roles) + conversions.put_if_specified(payload, "channels", channels) + conversions.put_if_specified(payload, "icon", icon, conversions.image_bytes_to_image_data) route = routes.GUILDS.compile(self.POST) return await self._request(route, json_body=payload) - async def get_guild(self, guild_id: str) -> typing.Dict: + async def get_guild(self, guild_id: str) -> typing.Dict[str, typing.Any]: """Gets a given guild's object. Parameters @@ -1443,7 +1418,7 @@ async def get_guild(self, guild_id: str) -> typing.Dict: Returns ------- - :obj:`typing.Dict` + :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ]` The requested guild object. Raises @@ -1461,19 +1436,19 @@ async def modify_guild( # lgtm [py/similar-function] self, guild_id: str, *, - name: typing.Union[typing.Literal[...], str] = ..., - region: typing.Union[typing.Literal[...], str] = ..., - verification_level: typing.Union[typing.Literal[...], int] = ..., - default_message_notifications: typing.Union[typing.Literal[...], int] = ..., - explicit_content_filter: typing.Union[typing.Literal[...], int] = ..., - afk_channel_id: typing.Union[typing.Literal[...], str] = ..., - afk_timeout: typing.Union[typing.Literal[...], int] = ..., - icon: typing.Union[typing.Literal[...], bytes] = ..., - owner_id: typing.Union[typing.Literal[...], str] = ..., - splash: typing.Union[typing.Literal[...], bytes] = ..., - system_channel_id: typing.Union[typing.Literal[...], str] = ..., - reason: typing.Union[typing.Literal[...], str] = ..., - ) -> typing.Dict: + name: str = ..., + region: str = ..., + verification_level: int = ..., + default_message_notifications: int = ..., + explicit_content_filter: int = ..., + afk_channel_id: str = ..., + afk_timeout: int = ..., + icon: bytes = ..., + owner_id: str = ..., + splash: bytes = ..., + system_channel_id: str = ..., + reason: str = ..., + ) -> typing.Dict[str, typing.Any]: """Edits a given guild. Parameters @@ -1483,7 +1458,7 @@ async def modify_guild( # lgtm [py/similar-function] name : :obj:`str` If specified, the new name string for the guild (``2-100`` characters). region : :obj:`str` - If specified, the new voice region ID for guild. You can use + If specified, the new voice region ID for guild. You can use :meth:`list_voice_regions` to see which region IDs are available. verification_level : :obj:`int` If specified, the new verification level integer (``0-5``). @@ -1504,12 +1479,12 @@ async def modify_guild( # lgtm [py/similar-function] system_channel_id : :obj:`str` If specified, the new ID of the new system channel. reason : :obj:`str` - If specified, the audit log reason explaining why the operation + If specified, the audit log reason explaining why the operation was performed. Returns ------- - :obj:`typing.Dict` + :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ]` The edited guild object. Raises @@ -1520,25 +1495,25 @@ async def modify_guild( # lgtm [py/similar-function] If you lack the ``MANAGE_GUILD`` permission or are not in the guild. """ payload = {} - transformations.put_if_specified(payload, "name", name) - transformations.put_if_specified(payload, "region", region) - transformations.put_if_specified(payload, "verification_level", verification_level) - transformations.put_if_specified(payload, "default_message_notifications", default_message_notifications) - transformations.put_if_specified(payload, "explicit_content_filter", explicit_content_filter) - transformations.put_if_specified(payload, "afk_channel_id", afk_channel_id) - transformations.put_if_specified(payload, "afk_timeout", afk_timeout) - transformations.put_if_specified(payload, "icon", icon, transformations.image_bytes_to_image_data) - transformations.put_if_specified(payload, "owner_id", owner_id) - transformations.put_if_specified(payload, "splash", splash, transformations.image_bytes_to_image_data) - transformations.put_if_specified(payload, "system_channel_id", system_channel_id) + conversions.put_if_specified(payload, "name", name) + conversions.put_if_specified(payload, "region", region) + conversions.put_if_specified(payload, "verification_level", verification_level) + conversions.put_if_specified(payload, "default_message_notifications", default_message_notifications) + conversions.put_if_specified(payload, "explicit_content_filter", explicit_content_filter) + conversions.put_if_specified(payload, "afk_channel_id", afk_channel_id) + conversions.put_if_specified(payload, "afk_timeout", afk_timeout) + conversions.put_if_specified(payload, "icon", icon, conversions.image_bytes_to_image_data) + conversions.put_if_specified(payload, "owner_id", owner_id) + conversions.put_if_specified(payload, "splash", splash, conversions.image_bytes_to_image_data) + conversions.put_if_specified(payload, "system_channel_id", system_channel_id) route = routes.GUILD.compile(self.PATCH, guild_id=guild_id) return await self._request(route, json_body=payload, reason=reason) # pylint: enable=too-many-locals async def delete_guild(self, guild_id: str) -> None: - """Permanently deletes the given guild. - + """Permanently deletes the given guild. + You must be owner of the guild to perform this action. Parameters @@ -1556,7 +1531,7 @@ async def delete_guild(self, guild_id: str) -> None: route = routes.GUILD.compile(self.DELETE, guild_id=guild_id) await self._request(route) - async def get_guild_channels(self, guild_id: str) -> typing.Sequence[typing.Dict]: + async def get_guild_channels(self, guild_id: str) -> typing.Sequence[typing.Dict[str, typing.Any]]: """Gets all the channels for a given guild. Parameters @@ -1566,7 +1541,7 @@ async def get_guild_channels(self, guild_id: str) -> typing.Sequence[typing.Dict Returns ------- - :obj:`typing.Sequence` [ :obj:`typing.Dict` ] + :obj:`typing.Sequence` [ :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ]` ] A list of channel objects. Raises @@ -1584,17 +1559,17 @@ async def create_guild_channel( guild_id: str, name: str, *, - type_: typing.Union[typing.Literal[...], int] = ..., - position: typing.Union[typing.Literal[...], int] = ..., - topic: typing.Union[typing.Literal[...], str] = ..., - nsfw: typing.Union[typing.Literal[...], bool] = ..., - rate_limit_per_user: typing.Union[typing.Literal[...], int] = ..., - bitrate: typing.Union[typing.Literal[...], int] = ..., - user_limit: typing.Union[typing.Literal[...], int] = ..., - permission_overwrites: typing.Union[typing.Literal[...], typing.Sequence[typing.Dict]] = ..., - parent_id: typing.Union[typing.Literal[...], str] = ..., - reason: typing.Union[typing.Literal[...], str] = ..., - ) -> typing.Dict: + type_: int = ..., + position: int = ..., + topic: str = ..., + nsfw: bool = ..., + rate_limit_per_user: int = ..., + bitrate: int = ..., + user_limit: int = ..., + permission_overwrites: typing.Sequence[typing.Dict[str, typing.Any]] = ..., + parent_id: str = ..., + reason: str = ..., + ) -> typing.Dict[str, typing.Any]: """Creates a channel in a given guild. Parameters @@ -1602,45 +1577,45 @@ async def create_guild_channel( guild_id: The ID of the guild to create the channel in. name : :obj:`str` - If specified, the name for the channel.This must be + If specified, the name for the channel.This must be between ``2`` and ``100`` characters in length. type_: :obj:`int` If specified, the channel type integer (``0-6``). position : :obj:`int` If specified, the position to change the channel to. topic : :obj:`str` - If specified, the topic to set. This is only applicable to - text channels. This must be between ``0`` and ``1024`` + If specified, the topic to set. This is only applicable to + text channels. This must be between ``0`` and ``1024`` characters in length. nsfw : :obj:`bool` - If specified, whether the channel will be marked as NSFW. + If specified, whether the channel will be marked as NSFW. Only applicable to text channels. rate_limit_per_user : :obj:`int` - If specified, the number of seconds the user has to wait before sending - another message. This will not apply to bots, or to members with - ``MANAGE_MESSAGES`` or ``MANAGE_CHANNEL`` permissions. This must + If specified, the number of seconds the user has to wait before sending + another message. This will not apply to bots, or to members with + ``MANAGE_MESSAGES`` or ``MANAGE_CHANNEL`` permissions. This must be between ``0`` and ``21600`` seconds. bitrate : :obj:`int` - If specified, the bitrate in bits per second allowable for the channel. - This only applies to voice channels and must be between ``8000`` - and ``96000`` for normal servers or ``8000`` and ``128000`` for + If specified, the bitrate in bits per second allowable for the channel. + This only applies to voice channels and must be between ``8000`` + and ``96000`` for normal servers or ``8000`` and ``128000`` for VIP servers. user_limit : :obj:`int` - If specified, the max number of users to allow in a voice channel. - This must be between ``0`` and ``99`` inclusive, where + If specified, the max number of users to allow in a voice channel. + This must be between ``0`` and ``99`` inclusive, where ``0`` implies no limit. - permission_overwrites : :obj:`typing.Sequence` [ :obj:`typing.Dict` ] - If specified, the list of permission overwrites that are category + permission_overwrites : :obj:`typing.Sequence` [ :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ]` ] + If specified, the list of permission overwrites that are category specific to replace the existing overwrites with. parent_id : :obj:`str` If specified, the parent category ID to set for the channel. reason : :obj:`str` - If specified, the audit log reason explaining why the operation + If specified, the audit log reason explaining why the operation was performed. Returns ------- - :obj:`typing.Dict` + :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ]` The newly created channel object. Raises @@ -1650,20 +1625,20 @@ async def create_guild_channel( :obj:`hikari.net.errors.ForbiddenHTTPError` If you lack the ``MANAGE_CHANNEL`` permission or are not in the guild. :obj:`hikari.net.errors.BadRequestHTTPError` - If you provide incorrect options for the corresponding channel type + If you provide incorrect options for the corresponding channel type (e.g. a ``bitrate`` for a text channel). """ payload = {} - transformations.put_if_specified(payload, "name", name) - transformations.put_if_specified(payload, "type", type_) - transformations.put_if_specified(payload, "position", position) - transformations.put_if_specified(payload, "topic", topic) - transformations.put_if_specified(payload, "nsfw", nsfw) - transformations.put_if_specified(payload, "rate_limit_per_user", rate_limit_per_user) - transformations.put_if_specified(payload, "bitrate", bitrate) - transformations.put_if_specified(payload, "user_limit", user_limit) - transformations.put_if_specified(payload, "permission_overwrites", permission_overwrites) - transformations.put_if_specified(payload, "parent_id", parent_id) + conversions.put_if_specified(payload, "name", name) + conversions.put_if_specified(payload, "type", type_) + conversions.put_if_specified(payload, "position", position) + conversions.put_if_specified(payload, "topic", topic) + conversions.put_if_specified(payload, "nsfw", nsfw) + conversions.put_if_specified(payload, "rate_limit_per_user", rate_limit_per_user) + conversions.put_if_specified(payload, "bitrate", bitrate) + conversions.put_if_specified(payload, "user_limit", user_limit) + conversions.put_if_specified(payload, "permission_overwrites", permission_overwrites) + conversions.put_if_specified(payload, "parent_id", parent_id) route = routes.GUILD_CHANNELS.compile(self.POST, guild_id=guild_id) return await self._request(route, json_body=payload, reason=reason) @@ -1696,7 +1671,7 @@ async def modify_guild_channel_positions( route = routes.GUILD_CHANNELS.compile(self.PATCH, guild_id=guild_id) await self._request(route, json_body=payload) - async def get_guild_member(self, guild_id: str, user_id: str) -> typing.Dict: + async def get_guild_member(self, guild_id: str, user_id: str) -> typing.Dict[str, typing.Any]: """Gets a given guild member. Parameters @@ -1705,10 +1680,10 @@ async def get_guild_member(self, guild_id: str, user_id: str) -> typing.Dict: The ID of the guild to get the member from. user_id : :obj:`str` The ID of the member to get. - + Returns ------- - :obj:`typing.Dict` + :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ]` The requested member object. Raises @@ -1722,12 +1697,8 @@ async def get_guild_member(self, guild_id: str, user_id: str) -> typing.Dict: return await self._request(route) async def list_guild_members( - self, - guild_id: str, - *, - limit: typing.Union[typing.Literal[...], int] = ..., - after: typing.Union[typing.Literal[...], str] = ..., - ) -> typing.Sequence[typing.Dict]: + self, guild_id: str, *, limit: int = ..., after: str = ..., + ) -> typing.Sequence[typing.Dict[str, typing.Any]]: """Lists all members of a given guild. Parameters @@ -1735,31 +1706,31 @@ async def list_guild_members( guild_id : :obj:`str` The ID of the guild to get the members from. limit : :obj:`int` - If specified, the maximum number of members to return. This has to be between + If specified, the maximum number of members to return. This has to be between ``1`` and ``1000`` inclusive. after : :obj:`str` - If specified, the highest ID in the previous page. This is used for retrieving more + If specified, the highest ID in the previous page. This is used for retrieving more than ``1000`` members in a server using consecutive requests. - + Example ------- .. code-block:: python - + members = [] last_id = 0 - + while True: next_members = await client.list_guild_members(1234567890, limit=1000, after=last_id) members += next_members - + if len(next_members) == 1000: last_id = next_members[-1] else: - break + break Returns ------- - :obj:`typing.Dict` + :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ]` A list of member objects. Raises @@ -1772,8 +1743,8 @@ async def list_guild_members( If you provide invalid values for the ``limit`` or `after`` fields. """ query = {} - transformations.put_if_specified(query, "limit", limit) - transformations.put_if_specified(query, "after", after) + conversions.put_if_specified(query, "limit", limit) + conversions.put_if_specified(query, "after", after) route = routes.GUILD_MEMBERS.compile(self.GET, guild_id=guild_id) return await self._request(route, query=query) @@ -1782,12 +1753,12 @@ async def modify_guild_member( # lgtm [py/similar-function] guild_id: str, user_id: str, *, - nick: typing.Optional[typing.Union[typing.Literal[...], str]] = ..., - roles: typing.Union[typing.Literal[...], typing.Sequence[str]] = ..., - mute: typing.Union[typing.Literal[...], bool] = ..., - deaf: typing.Union[typing.Literal[...], bool] = ..., - channel_id: typing.Optional[typing.Union[typing.Literal[...], str]] = ..., - reason: typing.Union[typing.Literal[...], str] = ..., + nick: typing.Optional[str] = ..., + roles: typing.Sequence[str] = ..., + mute: bool = ..., + deaf: bool = ..., + channel_id: typing.Optional[str] = ..., + reason: str = ..., ) -> None: """Edits a member of a given guild. @@ -1798,7 +1769,7 @@ async def modify_guild_member( # lgtm [py/similar-function] user_id : :obj:`str` The ID of the member to edit. nick : :obj:`str` - If specified, the new nickname string. Setting it to ``None`` explicitly + If specified, the new nickname string. Setting it to ``None`` explicitly will clear the nickname. roles : :obj:`str` If specified, a list of role IDs the member should have. @@ -1810,9 +1781,9 @@ async def modify_guild_member( # lgtm [py/similar-function] If specified, the ID of the channel to move the member to. Setting it to ``None`` explicitly will disconnect the user. reason : :obj:`str` - If specified, the audit log reason explaining why the operation + If specified, the audit log reason explaining why the operation was performed. - + Raises ------ :obj:`hikari.net.errors.NotFoundHTTPError` @@ -1826,17 +1797,15 @@ async def modify_guild_member( # lgtm [py/similar-function] If you pass ```mute``, ``deaf`` or ``channel_id`` while the member is not connected to a voice channel. """ payload = {} - transformations.put_if_specified(payload, "nick", nick) - transformations.put_if_specified(payload, "roles", roles) - transformations.put_if_specified(payload, "mute", mute) - transformations.put_if_specified(payload, "deaf", deaf) - transformations.put_if_specified(payload, "channel_id", channel_id) + conversions.put_if_specified(payload, "nick", nick) + conversions.put_if_specified(payload, "roles", roles) + conversions.put_if_specified(payload, "mute", mute) + conversions.put_if_specified(payload, "deaf", deaf) + conversions.put_if_specified(payload, "channel_id", channel_id) route = routes.GUILD_MEMBER.compile(self.PATCH, guild_id=guild_id, user_id=user_id) await self._request(route, json_body=payload, reason=reason) - async def modify_current_user_nick( - self, guild_id: str, nick: typing.Optional[str], *, reason: typing.Union[typing.Literal[...], str] = ..., - ) -> None: + async def modify_current_user_nick(self, guild_id: str, nick: typing.Optional[str], *, reason: str = ...,) -> None: """Edits the current user's nickname for a given guild. Parameters @@ -1846,9 +1815,9 @@ async def modify_current_user_nick( nick : :obj:`str`, optional The new nick string. Setting this to `None` clears the nickname. reason : :obj:`str` - If specified, the audit log reason explaining why the operation + If specified, the audit log reason explaining why the operation was performed. - + Raises ------ :obj:`hikari.net.errors.NotFoundHTTPError` @@ -1862,9 +1831,7 @@ async def modify_current_user_nick( route = routes.OWN_GUILD_NICKNAME.compile(self.PATCH, guild_id=guild_id) await self._request(route, json_body=payload, reason=reason) - async def add_guild_member_role( - self, guild_id: str, user_id: str, role_id: str, *, reason: typing.Union[typing.Literal[...], str] = ..., - ) -> None: + async def add_guild_member_role(self, guild_id: str, user_id: str, role_id: str, *, reason: str = ...,) -> None: """Adds a role to a given member. Parameters @@ -1876,7 +1843,7 @@ async def add_guild_member_role( role_id : :obj:`str` The ID of the role you want to add. reason : :obj:`str` - If specified, the audit log reason explaining why the operation + If specified, the audit log reason explaining why the operation was performed. Raises @@ -1889,9 +1856,7 @@ async def add_guild_member_role( route = routes.GUILD_MEMBER_ROLE.compile(self.PUT, guild_id=guild_id, user_id=user_id, role_id=role_id) await self._request(route, reason=reason) - async def remove_guild_member_role( - self, guild_id: str, user_id: str, role_id: str, *, reason: typing.Union[typing.Literal[...], str] = ..., - ) -> None: + async def remove_guild_member_role(self, guild_id: str, user_id: str, role_id: str, *, reason: str = ...,) -> None: """Removed a role from a given member. Parameters @@ -1903,7 +1868,7 @@ async def remove_guild_member_role( role_id : :obj:`str` The ID of the role you want to remove. reason : :obj:`str` - If specified, the audit log reason explaining why the operation + If specified, the audit log reason explaining why the operation was performed. Raises @@ -1916,9 +1881,7 @@ async def remove_guild_member_role( route = routes.GUILD_MEMBER_ROLE.compile(self.DELETE, guild_id=guild_id, user_id=user_id, role_id=role_id) await self._request(route, reason=reason) - async def remove_guild_member( - self, guild_id: str, user_id: str, *, reason: typing.Union[typing.Literal[...], str] = ..., - ) -> None: + async def remove_guild_member(self, guild_id: str, user_id: str, *, reason: str = ...,) -> None: """Kicks a user from a given guild. Parameters @@ -1928,7 +1891,7 @@ async def remove_guild_member( user_id: :obj:`str` The ID of the member you want to kick. reason : :obj:`str` - If specified, the audit log reason explaining why the operation + If specified, the audit log reason explaining why the operation was performed. Raises @@ -1941,7 +1904,7 @@ async def remove_guild_member( route = routes.GUILD_MEMBER.compile(self.DELETE, guild_id=guild_id, user_id=user_id) await self._request(route, reason=reason) - async def get_guild_bans(self, guild_id: str) -> typing.Sequence[typing.Dict]: + async def get_guild_bans(self, guild_id: str) -> typing.Sequence[typing.Dict[str, typing.Any]]: """Gets the bans for a given guild. Parameters @@ -1951,7 +1914,7 @@ async def get_guild_bans(self, guild_id: str) -> typing.Sequence[typing.Dict]: Returns ------- - :obj:`typing.Sequence` [ :obj:`typing.Dict` ] + :obj:`typing.Sequence` [ :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ]` ] A list of ban objects. Raises @@ -1964,7 +1927,7 @@ async def get_guild_bans(self, guild_id: str) -> typing.Sequence[typing.Dict]: route = routes.GUILD_BANS.compile(self.GET, guild_id=guild_id) return await self._request(route) - async def get_guild_ban(self, guild_id: str, user_id: str) -> typing.Dict: + async def get_guild_ban(self, guild_id: str, user_id: str) -> typing.Dict[str, typing.Any]: """Gets a ban from a given guild. Parameters @@ -1976,7 +1939,7 @@ async def get_guild_ban(self, guild_id: str, user_id: str) -> typing.Dict: Returns ------- - :obj:`typing.Dict` + :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ]` A ban object for the requested user. Raises @@ -1990,12 +1953,7 @@ async def get_guild_ban(self, guild_id: str, user_id: str) -> typing.Dict: return await self._request(route) async def create_guild_ban( - self, - guild_id: str, - user_id: str, - *, - delete_message_days: typing.Union[typing.Literal[...], int] = ..., - reason: typing.Union[typing.Literal[...], str] = ..., + self, guild_id: str, user_id: str, *, delete_message_days: int = ..., reason: str = ..., ) -> None: """Bans a user from a given guild. @@ -2006,10 +1964,10 @@ async def create_guild_ban( user_id : :obj:`str` The ID of the member you want to ban. delete_message_days : :obj:`str` - If specified, how many days of messages from the user should + If specified, how many days of messages from the user should be removed. reason : :obj:`str` - If specified, the audit log reason explaining why the operation + If specified, the audit log reason explaining why the operation was performed. Raises @@ -2020,14 +1978,12 @@ async def create_guild_ban( If you lack the ``BAN_MEMBERS`` permission or are not in the guild. """ query = {} - transformations.put_if_specified(query, "delete-message-days", delete_message_days) - transformations.put_if_specified(query, "reason", reason) + conversions.put_if_specified(query, "delete-message-days", delete_message_days) + conversions.put_if_specified(query, "reason", reason) route = routes.GUILD_BAN.compile(self.PUT, guild_id=guild_id, user_id=user_id) await self._request(route, query=query) - async def remove_guild_ban( - self, guild_id: str, user_id: str, *, reason: typing.Union[typing.Literal[...], str] = ..., - ) -> None: + async def remove_guild_ban(self, guild_id: str, user_id: str, *, reason: str = ...,) -> None: """Un-bans a user from a given guild. Parameters @@ -2037,7 +1993,7 @@ async def remove_guild_ban( user_id : :obj:`str` The ID of the user you want to un-ban. reason : :obj:`str` - If specified, the audit log reason explaining why the operation + If specified, the audit log reason explaining why the operation was performed. Raises @@ -2050,7 +2006,7 @@ async def remove_guild_ban( route = routes.GUILD_BAN.compile(self.DELETE, guild_id=guild_id, user_id=user_id) await self._request(route, reason=reason) - async def get_guild_roles(self, guild_id: str) -> typing.Sequence[typing.Dict]: + async def get_guild_roles(self, guild_id: str) -> typing.Sequence[typing.Dict[str, typing.Any]]: """Gets the roles for a given guild. Parameters @@ -2060,7 +2016,7 @@ async def get_guild_roles(self, guild_id: str) -> typing.Sequence[typing.Dict]: Returns ------- - :obj:`typing.Sequence` [ :obj:`typing.Dict` ] + :obj:`typing.Sequence` [ :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ]` ] A list of role objects. Raises @@ -2077,13 +2033,13 @@ async def create_guild_role( self, guild_id: str, *, - name: typing.Union[typing.Literal[...], str] = ..., - permissions: typing.Union[typing.Literal[...], int] = ..., - color: typing.Union[typing.Literal[...], int] = ..., - hoist: typing.Union[typing.Literal[...], bool] = ..., - mentionable: typing.Union[typing.Literal[...], bool] = ..., - reason: typing.Union[typing.Literal[...], str] = ..., - ) -> typing.Dict: + name: str = ..., + permissions: int = ..., + color: int = ..., + hoist: bool = ..., + mentionable: bool = ..., + reason: str = ..., + ) -> typing.Dict[str, typing.Any]: """Creates a new role for a given guild. Parameters @@ -2101,12 +2057,12 @@ async def create_guild_role( mentionable : :obj:`bool` If specified, whether the role will be able to be mentioned by any user. reason : :obj:`str` - If specified, the audit log reason explaining why the operation + If specified, the audit log reason explaining why the operation was performed. Returns ------- - :obj:`typing.Dict` + :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ]` The newly created role object. Raises @@ -2119,17 +2075,17 @@ async def create_guild_role( If you provide invalid values for the role attributes. """ payload = {} - transformations.put_if_specified(payload, "name", name) - transformations.put_if_specified(payload, "permissions", permissions) - transformations.put_if_specified(payload, "color", color) - transformations.put_if_specified(payload, "hoist", hoist) - transformations.put_if_specified(payload, "mentionable", mentionable) + conversions.put_if_specified(payload, "name", name) + conversions.put_if_specified(payload, "permissions", permissions) + conversions.put_if_specified(payload, "color", color) + conversions.put_if_specified(payload, "hoist", hoist) + conversions.put_if_specified(payload, "mentionable", mentionable) route = routes.GUILD_ROLES.compile(self.POST, guild_id=guild_id) return await self._request(route, json_body=payload, reason=reason) async def modify_guild_role_positions( self, guild_id: str, role: typing.Tuple[str, int], *roles: typing.Tuple[str, int] - ) -> typing.Sequence[typing.Dict]: + ) -> typing.Sequence[typing.Dict[str, typing.Any]]: """Edits the position of two or more roles in a given guild. Parameters @@ -2143,7 +2099,7 @@ async def modify_guild_role_positions( Returns ------- - :obj:`typing.Sequence` [ :obj:`typing.Dict` ] + :obj:`typing.Sequence` [ :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ]` ] A list of all the guild roles. Raises @@ -2164,13 +2120,13 @@ async def modify_guild_role( # lgtm [py/similar-function] guild_id: str, role_id: str, *, - name: typing.Union[typing.Literal[...], str] = ..., - permissions: typing.Union[typing.Literal[...], int] = ..., - color: typing.Union[typing.Literal[...], int] = ..., - hoist: typing.Union[typing.Literal[...], bool] = ..., - mentionable: typing.Union[typing.Literal[...], bool] = ..., - reason: typing.Union[typing.Literal[...], str] = ..., - ) -> typing.Dict: + name: str = ..., + permissions: int = ..., + color: int = ..., + hoist: bool = ..., + mentionable: bool = ..., + reason: str = ..., + ) -> typing.Dict[str, typing.Any]: """Edits a role in a given guild. Parameters @@ -2190,12 +2146,12 @@ async def modify_guild_role( # lgtm [py/similar-function] mentionable : :obj:`bool` If specified, whether the role should be mentionable or not. reason : :obj:`str` - If specified, the audit log reason explaining why the operation + If specified, the audit log reason explaining why the operation was performed. - + Returns ------- - :obj:`typing.Dict` + :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ]` The edited role object. Raises @@ -2208,11 +2164,11 @@ async def modify_guild_role( # lgtm [py/similar-function] If you provide invalid values for the role attributes. """ payload = {} - transformations.put_if_specified(payload, "name", name) - transformations.put_if_specified(payload, "permissions", permissions) - transformations.put_if_specified(payload, "color", color) - transformations.put_if_specified(payload, "hoist", hoist) - transformations.put_if_specified(payload, "mentionable", mentionable) + conversions.put_if_specified(payload, "name", name) + conversions.put_if_specified(payload, "permissions", permissions) + conversions.put_if_specified(payload, "color", color) + conversions.put_if_specified(payload, "hoist", hoist) + conversions.put_if_specified(payload, "mentionable", mentionable) route = routes.GUILD_ROLE.compile(self.PATCH, guild_id=guild_id, role_id=role_id) return await self._request(route, json_body=payload, reason=reason) @@ -2266,12 +2222,7 @@ async def get_guild_prune_count(self, guild_id: str, days: int) -> int: return int(result["pruned"]) async def begin_guild_prune( - self, - guild_id: str, - days: int, - *, - compute_prune_count: typing.Union[typing.Literal[...], bool] = ..., - reason: typing.Union[typing.Literal[...], str] = ..., + self, guild_id: str, days: int, *, compute_prune_count: bool = ..., reason: str = ..., ) -> typing.Optional[int]: """Prunes members of a given guild based on the number of inactive days. @@ -2282,16 +2233,16 @@ async def begin_guild_prune( days : :obj:`int` The number of inactivity days you want to use as filter. compute_prune_count : :obj:`bool` - Whether a count of pruned members is returned or not. + Whether a count of pruned members is returned or not. Discouraged for large guilds out of politeness. reason : :obj:`str` - If specified, the audit log reason explaining why the operation + If specified, the audit log reason explaining why the operation was performed. Returns ------- :obj:`int`, optional - The number of members who were kicked if ``compute_prune_count`` + The number of members who were kicked if ``compute_prune_count`` is ``True``, else ``None``. Raises @@ -2304,7 +2255,7 @@ async def begin_guild_prune( If you provide invalid values for the ``days`` or ``compute_prune_count`` fields. """ query = {"days": days} - transformations.put_if_specified(query, "compute_prune_count", compute_prune_count, str) + conversions.put_if_specified(query, "compute_prune_count", compute_prune_count, str) route = routes.GUILD_PRUNE.compile(self.POST, guild_id=guild_id) result = await self._request(route, query=query, reason=reason) @@ -2313,7 +2264,7 @@ async def begin_guild_prune( except (TypeError, KeyError): return None - async def get_guild_voice_regions(self, guild_id: str) -> typing.Sequence[typing.Dict]: + async def get_guild_voice_regions(self, guild_id: str) -> typing.Sequence[typing.Dict[str, typing.Any]]: """Gets the voice regions for a given guild. Parameters @@ -2323,7 +2274,7 @@ async def get_guild_voice_regions(self, guild_id: str) -> typing.Sequence[typing Returns ------- - :obj:`typing.Sequence` [ :obj:`typing.Dict` ] + :obj:`typing.Sequence` [ :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ]` ] A list of voice region objects. Raises @@ -2336,7 +2287,7 @@ async def get_guild_voice_regions(self, guild_id: str) -> typing.Sequence[typing route = routes.GUILD_VOICE_REGIONS.compile(self.GET, guild_id=guild_id) return await self._request(route) - async def get_guild_invites(self, guild_id: str) -> typing.Sequence[typing.Dict]: + async def get_guild_invites(self, guild_id: str) -> typing.Sequence[typing.Dict[str, typing.Any]]: """Gets the invites for a given guild. Parameters @@ -2346,7 +2297,7 @@ async def get_guild_invites(self, guild_id: str) -> typing.Sequence[typing.Dict] Returns ------- - :obj:`typing.Sequence` [ :obj:`typing.Dict` ] + :obj:`typing.Sequence` [ :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ]` ] A list of invite objects (with metadata). Raises @@ -2359,7 +2310,7 @@ async def get_guild_invites(self, guild_id: str) -> typing.Sequence[typing.Dict] route = routes.GUILD_INVITES.compile(self.GET, guild_id=guild_id) return await self._request(route) - async def get_guild_integrations(self, guild_id: str) -> typing.Sequence[typing.Dict]: + async def get_guild_integrations(self, guild_id: str) -> typing.Sequence[typing.Dict[str, typing.Any]]: """Gets the integrations for a given guild. Parameters @@ -2369,7 +2320,7 @@ async def get_guild_integrations(self, guild_id: str) -> typing.Sequence[typing. Returns ------- - :obj:`typing.Sequence` [ :obj:`typing.Dict` ] + :obj:`typing.Sequence` [ :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ]` ] A list of integration objects. Raises @@ -2383,8 +2334,8 @@ async def get_guild_integrations(self, guild_id: str) -> typing.Sequence[typing. return await self._request(route) async def create_guild_integration( - self, guild_id: str, type_: str, integration_id: str, *, reason: typing.Union[typing.Literal[...], str] = ..., - ) -> typing.Dict: + self, guild_id: str, type_: str, integration_id: str, *, reason: str = ..., + ) -> typing.Dict[str, typing.Any]: """Creates an integrations for a given guild. Parameters @@ -2396,12 +2347,12 @@ async def create_guild_integration( integration_id : :obj:`str` The ID for the new integration. reason : :obj:`str` - If specified, the audit log reason explaining why the operation + If specified, the audit log reason explaining why the operation was performed. Returns ------- - :obj:`typing.Dict` + :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ]` The newly created integration object. Raises @@ -2420,10 +2371,10 @@ async def modify_guild_integration( guild_id: str, integration_id: str, *, - expire_behaviour: typing.Union[typing.Literal[...], int] = ..., - expire_grace_period: typing.Union[typing.Literal[...], int] = ..., - enable_emojis: typing.Union[typing.Literal[...], bool] = ..., - reason: typing.Union[typing.Literal[...], str] = ..., + expire_behaviour: int = ..., + expire_grace_period: int = ..., + enable_emojis: bool = ..., + reason: str = ..., ) -> None: """Edits an integrations for a given guild. @@ -2434,15 +2385,15 @@ async def modify_guild_integration( integration_id : :obj:`str` The ID of the integration. expire_behaviour : :obj:`int` - If specified, the behaviour for when an integration subscription + If specified, the behaviour for when an integration subscription lapses. expire_grace_period : :obj:`int` - If specified, time interval in seconds in which the integration + If specified, time interval in seconds in which the integration will ignore lapsed subscriptions. enable_emojis : :obj:`bool` If specified, whether emojis should be synced for this integration. reason : :obj:`str` - If specified, the audit log reason explaining why the operation + If specified, the audit log reason explaining why the operation was performed. Raises @@ -2453,16 +2404,14 @@ async def modify_guild_integration( If you lack the ``MANAGE_GUILD`` permission or are not in the guild. """ payload = {} - transformations.put_if_specified(payload, "expire_behaviour", expire_behaviour) - transformations.put_if_specified(payload, "expire_grace_period", expire_grace_period) + conversions.put_if_specified(payload, "expire_behaviour", expire_behaviour) + conversions.put_if_specified(payload, "expire_grace_period", expire_grace_period) # This is inconsistently named in their API. - transformations.put_if_specified(payload, "enable_emoticons", enable_emojis) + conversions.put_if_specified(payload, "enable_emoticons", enable_emojis) route = routes.GUILD_INTEGRATION.compile(self.PATCH, guild_id=guild_id, integration_id=integration_id) await self._request(route, json_body=payload, reason=reason) - async def delete_guild_integration( - self, guild_id: str, integration_id: str, *, reason: typing.Union[typing.Literal[...], str] = ..., - ) -> None: + async def delete_guild_integration(self, guild_id: str, integration_id: str, *, reason: str = ...,) -> None: """Deletes an integration for the given guild. Parameters @@ -2472,7 +2421,7 @@ async def delete_guild_integration( integration_id : :obj:`str` The ID of the integration to delete. reason : :obj:`str` - If specified, the audit log reason explaining why the operation + If specified, the audit log reason explaining why the operation was performed. Raises @@ -2505,7 +2454,7 @@ async def sync_guild_integration(self, guild_id: str, integration_id: str) -> No route = routes.GUILD_INTEGRATION_SYNC.compile(self.POST, guild_id=guild_id, integration_id=integration_id) await self._request(route) - async def get_guild_embed(self, guild_id: str) -> typing.Dict: + async def get_guild_embed(self, guild_id: str) -> typing.Dict[str, typing.Any]: """Gets the embed for a given guild. Parameters @@ -2515,7 +2464,7 @@ async def get_guild_embed(self, guild_id: str) -> typing.Dict: Returns ------- - :obj:`typing.Dict` + :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ]` A guild embed object. Raises @@ -2529,23 +2478,23 @@ async def get_guild_embed(self, guild_id: str) -> typing.Dict: return await self._request(route) async def modify_guild_embed( - self, guild_id: str, embed: typing.Dict, *, reason: typing.Union[typing.Literal[...], str] = ..., - ) -> typing.Dict: + self, guild_id: str, embed: typing.Dict[str, typing.Any], *, reason: str = ..., + ) -> typing.Dict[str, typing.Any]: """Edits the embed for a given guild. Parameters ---------- guild_id : :obj:`str` The ID of the guild to edit the embed for. - embed : :obj:`typing.Dict` + embed : :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ]` The new embed object to be set. reason : :obj:`str` - If specified, the audit log reason explaining why the operation + If specified, the audit log reason explaining why the operation was performed. Returns ------- - :obj:`typing.Dict` + :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ]` The updated embed object. Raises @@ -2558,7 +2507,7 @@ async def modify_guild_embed( route = routes.GUILD_EMBED.compile(self.PATCH, guild_id=guild_id) return await self._request(route, json_body=embed, reason=reason) - async def get_guild_vanity_url(self, guild_id: str) -> typing.Dict: + async def get_guild_vanity_url(self, guild_id: str) -> typing.Dict[str, typing.Any]: """ Gets the vanity URL for a given guild. @@ -2569,7 +2518,7 @@ async def get_guild_vanity_url(self, guild_id: str) -> typing.Dict: Returns ------- - :obj:`typing.Dict` + :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ]` A partial invite object containing the vanity URL in the ``code`` field. Raises @@ -2582,7 +2531,7 @@ async def get_guild_vanity_url(self, guild_id: str) -> typing.Dict: route = routes.GUILD_VANITY_URL.compile(self.GET, guild_id=guild_id) return await self._request(route) - def get_guild_widget_image_url(self, guild_id: str, *, style: typing.Union[typing.Literal[...], str] = ...,) -> str: + def get_guild_widget_image_url(self, guild_id: str, *, style: str = ...,) -> str: """Get the URL for a guild widget. Parameters @@ -2599,7 +2548,7 @@ def get_guild_widget_image_url(self, guild_id: str, *, style: typing.Union[typin Note ---- - This does not actually make any form of request, and shouldn't be awaited. + This does not actually make any form of request, and shouldn't be awaited. Thus, it doesn't have rate limits either. Warning @@ -2609,9 +2558,7 @@ def get_guild_widget_image_url(self, guild_id: str, *, style: typing.Union[typin query = "" if style is ... else f"?style={style}" return f"{self.base_url}/guilds/{guild_id}/widget.png" + query - async def get_invite( - self, invite_code: str, *, with_counts: typing.Union[typing.Literal[...], bool] = ... - ) -> typing.Dict: + async def get_invite(self, invite_code: str, *, with_counts: bool = ...) -> typing.Dict[str, typing.Any]: """Gets the given invite. Parameters @@ -2619,12 +2566,12 @@ async def get_invite( invite_code : :str: The ID for wanted invite. with_counts : :bool: - If specified, wheter to attempt to count the number of + If specified, wheter to attempt to count the number of times the invite has been used. Returns ------- - :obj:`typing.Dict` + :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ]` The requested invite object. Raises @@ -2633,7 +2580,7 @@ async def get_invite( If the invite is not found. """ query = {} - transformations.put_if_specified(query, "with_counts", with_counts, str) + conversions.put_if_specified(query, "with_counts", with_counts, str) route = routes.INVITE.compile(self.GET, invite_code=invite_code) return await self._request(route, query=query) @@ -2648,7 +2595,7 @@ async def delete_invite(self, invite_code: str) -> None: Returns ------- ``None`` # Marker - Nothing, unlike what the API specifies. This is done to maintain + Nothing, unlike what the API specifies. This is done to maintain consistency with other calls of a similar nature in this API wrapper. Raises @@ -2656,24 +2603,24 @@ async def delete_invite(self, invite_code: str) -> None: :obj:`hikari.net.errors.NotFoundHTTPError` If the invite is not found. :obj:`hikari.net.errors.ForbiddenHTTPError` - If you lack either ``MANAGE_CHANNELS`` on the channel the invite + If you lack either ``MANAGE_CHANNELS`` on the channel the invite belongs to or ``MANAGE_GUILD`` for guild-global delete. """ route = routes.INVITE.compile(self.DELETE, invite_code=invite_code) return await self._request(route) - async def get_current_user(self) -> typing.Dict: + async def get_current_user(self) -> typing.Dict[str, typing.Any]: """Gets the current user that is represented by token given to the client. Returns ------- - :obj:`typing.Dict` + :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ]` The current user object. """ route = routes.OWN_USER.compile(self.GET) return await self._request(route) - async def get_user(self, user_id: str) -> typing.Dict: + async def get_user(self, user_id: str) -> typing.Dict[str, typing.Any]: """Gets a given user. Parameters @@ -2683,7 +2630,7 @@ async def get_user(self, user_id: str) -> typing.Dict: Returns ------- - :obj:`typing.Dict` + :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ]` The requested user object. Raises @@ -2695,11 +2642,8 @@ async def get_user(self, user_id: str) -> typing.Dict: return await self._request(route) async def modify_current_user( - self, - *, - username: typing.Union[typing.Literal[...], str] = ..., - avatar: typing.Optional[typing.Union[typing.Literal[...], bytes]] = ..., - ) -> typing.Dict: + self, *, username: str = ..., avatar: typing.Optional[bytes] = ..., + ) -> typing.Dict[str, typing.Any]: """Edits the current user. Parameters @@ -2707,12 +2651,12 @@ async def modify_current_user( username : :obj:`str` If specified, the new username string. avatar : :obj:`bytes` - If specified, the new avatar image in bytes form. + If specified, the new avatar image in bytes form. If it is ``None``, the avatar is removed. Returns ------- - :obj:`typing.Dict` + :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ]` The updated user object. Raises @@ -2721,33 +2665,29 @@ async def modify_current_user( If you pass username longer than the limit (``2-32``) or an invalid image. """ payload = {} - transformations.put_if_specified(payload, "username", username) - transformations.put_if_specified(payload, "avatar", avatar, transformations.image_bytes_to_image_data) + conversions.put_if_specified(payload, "username", username) + conversions.put_if_specified(payload, "avatar", avatar, conversions.image_bytes_to_image_data) route = routes.OWN_USER.compile(self.PATCH) return await self._request(route, json_body=payload) - async def get_current_user_connections(self) -> typing.Sequence[typing.Dict]: + async def get_current_user_connections(self) -> typing.Sequence[typing.Dict[str, typing.Any]]: """ - Gets the current user's connections. This endpoint can be - used with both ``Bearer`` and ``Bot`` tokens but will usually return an - empty list for bots (with there being some exceptions to this, like + Gets the current user's connections. This endpoint can be + used with both ``Bearer`` and ``Bot`` tokens but will usually return an + empty list for bots (with there being some exceptions to this, like user accounts that have been converted to bots). Returns ------- - :obj:`typing.Sequence` [ :obj:`typing.Dict` ] + :obj:`typing.Sequence` [ :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ]` ] A list of connection objects. """ route = routes.OWN_CONNECTIONS.compile(self.GET) return await self._request(route) async def get_current_user_guilds( - self, - *, - before: typing.Union[typing.Literal[...], str] = ..., - after: typing.Union[typing.Literal[...], str] = ..., - limit: typing.Union[typing.Literal[...], int] = ..., - ) -> typing.Sequence[typing.Dict]: + self, *, before: str = ..., after: str = ..., limit: int = ..., + ) -> typing.Sequence[typing.Dict[str, typing.Any]]: """Gets the guilds the current user is in. Parameters @@ -2764,19 +2704,19 @@ async def get_current_user_guilds( Returns ------- - :obj:`typing.Sequence` [ :obj:`typing.Dict` ] + :obj:`typing.Sequence` [ :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ]` ] A list of partial guild objects. Raises ------ :obj:`hikari.net.errors.BadRequestHTTPError` - If you pass both ``before`` and ``after`` or an + If you pass both ``before`` and ``after`` or an invalid value for ``limit``. """ query = {} - transformations.put_if_specified(query, "before", before) - transformations.put_if_specified(query, "after", after) - transformations.put_if_specified(query, "limit", limit) + conversions.put_if_specified(query, "before", before) + conversions.put_if_specified(query, "after", after) + conversions.put_if_specified(query, "limit", limit) route = routes.OWN_GUILDS.compile(self.GET) return await self._request(route, query=query) @@ -2796,7 +2736,7 @@ async def leave_guild(self, guild_id: str) -> None: route = routes.LEAVE_GUILD.compile(self.DELETE, guild_id=guild_id) await self._request(route) - async def create_dm(self, recipient_id: str) -> typing.Dict: + async def create_dm(self, recipient_id: str) -> typing.Dict[str, typing.Any]: """Creates a new DM channel with a given user. Parameters @@ -2806,7 +2746,7 @@ async def create_dm(self, recipient_id: str) -> typing.Dict: Returns ------- - :obj:`typing.Dict` + :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ]` The newly created DM channel object. Raises @@ -2818,12 +2758,12 @@ async def create_dm(self, recipient_id: str) -> typing.Dict: route = routes.OWN_DMS.compile(self.POST) return await self._request(route, json_body=payload) - async def list_voice_regions(self) -> typing.Sequence[typing.Dict]: + async def list_voice_regions(self) -> typing.Sequence[typing.Dict[str, typing.Any]]: """Get the voice regions that are available. Returns ------- - :obj:`typing.Sequence` [ :obj:`typing.Dict` ] + :obj:`typing.Sequence` [ :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ]` ] A list of voice regions available Note @@ -2834,13 +2774,8 @@ async def list_voice_regions(self) -> typing.Sequence[typing.Dict]: return await self._request(route) async def create_webhook( - self, - channel_id: str, - name: str, - *, - avatar: typing.Union[typing.Literal[...], bytes] = ..., - reason: typing.Union[typing.Literal[...], str] = ..., - ) -> typing.Dict: + self, channel_id: str, name: str, *, avatar: bytes = ..., reason: str = ..., + ) -> typing.Dict[str, typing.Any]: """ Creates a webhook for a given channel. @@ -2853,12 +2788,12 @@ async def create_webhook( avatar : :obj:`bytes` If specified, the avatar image in bytes form. reason : :obj:`str` - If specified, the audit log reason explaining why the operation + If specified, the audit log reason explaining why the operation was performed. Returns ------- - :obj:`typing.Dict` + :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ]` The newly created webhook object. Raises @@ -2866,17 +2801,17 @@ async def create_webhook( :obj:`hikari.net.errors.NotFoundHTTPError` If the channel is not found. :obj:`hikari.net.errors.ForbiddenHTTPError` - If you either lack the ``MANAGE_WEBHOOKS`` permission or + If you either lack the ``MANAGE_WEBHOOKS`` permission or can not see the given channel. :obj:`hikari.net.errors.BadRequestHTTPError` If the avatar image is too big or the format is invalid. """ payload = {"name": name} - transformations.put_if_specified(payload, "avatar", avatar, transformations.image_bytes_to_image_data) + conversions.put_if_specified(payload, "avatar", avatar, conversions.image_bytes_to_image_data) route = routes.CHANNEL_WEBHOOKS.compile(self.POST, channel_id=channel_id) return await self._request(route, json_body=payload, reason=reason) - async def get_channel_webhooks(self, channel_id: str) -> typing.Sequence[typing.Dict]: + async def get_channel_webhooks(self, channel_id: str) -> typing.Sequence[typing.Dict[str, typing.Any]]: """Gets all webhooks from a given channel. Parameters @@ -2886,7 +2821,7 @@ async def get_channel_webhooks(self, channel_id: str) -> typing.Sequence[typing. Returns ------- - :obj:`typing.Sequence` [ :obj:`typing.Dict` ] + :obj:`typing.Sequence` [ :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ]` ] A list of webhook objects for the give channel. Raises @@ -2894,13 +2829,13 @@ async def get_channel_webhooks(self, channel_id: str) -> typing.Sequence[typing. :obj:`hikari.net.errors.NotFoundHTTPError` If the channel is not found. :obj:`hikari.net.errors.ForbiddenHTTPError` - If you either lack the ``MANAGE_WEBHOOKS`` permission or + If you either lack the ``MANAGE_WEBHOOKS`` permission or can not see the given channel. """ route = routes.CHANNEL_WEBHOOKS.compile(self.GET, channel_id=channel_id) return await self._request(route) - async def get_guild_webhooks(self, guild_id: str) -> typing.Sequence[typing.Dict]: + async def get_guild_webhooks(self, guild_id: str) -> typing.Sequence[typing.Dict[str, typing.Any]]: """Gets all webhooks for a given guild. Parameters @@ -2910,7 +2845,7 @@ async def get_guild_webhooks(self, guild_id: str) -> typing.Sequence[typing.Dict Returns ------- - :obj:`typing.Sequence` [ :obj:`typing.Dict` ] + :obj:`typing.Sequence` [ :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ]` ] A list of webhook objects for the given guild. Raises @@ -2918,15 +2853,13 @@ async def get_guild_webhooks(self, guild_id: str) -> typing.Sequence[typing.Dict :obj:`hikari.net.errors.NotFoundHTTPError` If the guild is not found. :obj:`hikari.net.errors.ForbiddenHTTPError` - If you either lack the ``MANAGE_WEBHOOKS`` permission or + If you either lack the ``MANAGE_WEBHOOKS`` permission or aren't a member of the given guild. """ route = routes.GUILD_WEBHOOKS.compile(self.GET, guild_id=guild_id) return await self._request(route) - async def get_webhook( - self, webhook_id: str, *, webhook_token: typing.Union[typing.Literal[...], str] = ... - ) -> typing.Dict: + async def get_webhook(self, webhook_id: str, *, webhook_token: str = ...) -> typing.Dict[str, typing.Any]: """Gets a given webhook. Parameters @@ -2938,7 +2871,7 @@ async def get_webhook( Returns ------- - :obj:`typing.Dict` + :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ]` The requested webhook object. Raises @@ -2946,7 +2879,7 @@ async def get_webhook( :obj:`hikari.net.errors.NotFoundHTTPError` If the webhook is not found. :obj:`hikari.net.errors.ForbiddenHTTPError` - If you're not in the guild that owns this webhook or + If you're not in the guild that owns this webhook or lack the ``MANAGE_WEBHOOKS`` permission. :obj:`hikari.net.errors.UnauthorizedHTTPError` If you pass a token that's invalid for the target webhook. @@ -2961,12 +2894,12 @@ async def modify_webhook( self, webhook_id: str, *, - webhook_token: typing.Union[typing.Literal[...], str] = ..., - name: typing.Union[typing.Literal[...], str] = ..., - avatar: typing.Optional[typing.Union[typing.Literal[...], bytes]] = ..., - channel_id: typing.Union[typing.Literal[...], str] = ..., - reason: typing.Union[typing.Literal[...], str] = ..., - ) -> typing.Dict: + webhook_token: str = ..., + name: str = ..., + avatar: typing.Optional[bytes] = ..., + channel_id: str = ..., + reason: str = ..., + ) -> typing.Dict[str, typing.Any]: """Edits a given webhook. Parameters @@ -2981,15 +2914,15 @@ async def modify_webhook( If specified, the new avatar image in bytes form. If None, then it is removed. channel_id : :obj:`str` - If specified, the ID of the new channel the given + If specified, the ID of the new channel the given webhook should be moved to. reason : :obj:`str` - If specified, the audit log reason explaining why the operation + If specified, the audit log reason explaining why the operation was performed. Returns ------- - :obj:`typing.Dict` + :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ]` The updated webhook object. Raises @@ -2997,15 +2930,15 @@ async def modify_webhook( :obj:`hikari.net.errors.NotFoundHTTPError` If either the webhook or the channel aren't found. :obj:`hikari.net.errors.ForbiddenHTTPError` - If you either lack the ``MANAGE_WEBHOOKS`` permission or + If you either lack the ``MANAGE_WEBHOOKS`` permission or aren't a member of the guild this webhook belongs to. :obj:`hikari.net.errors.UnauthorizedHTTPError` If you pass a token that's invalid for the target webhook. """ payload = {} - transformations.put_if_specified(payload, "name", name) - transformations.put_if_specified(payload, "channel_id", channel_id) - transformations.put_if_specified(payload, "avatar", avatar, transformations.image_bytes_to_image_data) + conversions.put_if_specified(payload, "name", name) + conversions.put_if_specified(payload, "channel_id", channel_id) + conversions.put_if_specified(payload, "avatar", avatar, conversions.image_bytes_to_image_data) if webhook_token is ...: route = routes.WEBHOOK.compile(self.PATCH, webhook_id=webhook_id) else: @@ -3014,9 +2947,7 @@ async def modify_webhook( route, json_body=payload, reason=reason, suppress_authorization_header=webhook_token is not ..., ) - async def delete_webhook( - self, webhook_id: str, *, webhook_token: typing.Union[typing.Literal[...], str] = ... - ) -> None: + async def delete_webhook(self, webhook_id: str, *, webhook_token: str = ...) -> None: """Deletes a given webhook. Parameters @@ -3024,7 +2955,7 @@ async def delete_webhook( webhook_id : :obj:`str` The ID of the webhook to delete webhook_token : :obj:`str` - If specified, the webhook token to use to + If specified, the webhook token to use to delete it (bypassing bot authorization). Raises @@ -3032,7 +2963,7 @@ async def delete_webhook( :obj:`hikari.net.errors.NotFoundHTTPError` If the webhook is not found. :obj:`hikari.net.errors.ForbiddenHTTPError` - If you either lack the ``MANAGE_WEBHOOKS`` permission or + If you either lack the ``MANAGE_WEBHOOKS`` permission or aren't a member of the guild this webhook belongs to. :obj:`hikari.net.errors.UnauthorizedHTTPError` If you pass a token that's invalid for the target webhook. @@ -3048,15 +2979,15 @@ async def execute_webhook( webhook_id: str, webhook_token: str, *, - content: typing.Union[typing.Literal[...], str] = ..., - username: typing.Union[typing.Literal[...], str] = ..., - avatar_url: typing.Union[typing.Literal[...], str] = ..., - tts: typing.Union[typing.Literal[...], bool] = ..., - wait: typing.Union[typing.Literal[...], bool] = ..., - file: typing.Union[typing.Literal[...], typing.Tuple[str, storage.FileLikeT]] = ..., - embeds: typing.Union[typing.Literal[...], typing.Sequence[typing.Dict]] = ..., - allowed_mentions: typing.Union[typing.Literal[...], typing.Dict] = ..., - ) -> typing.Optional[typing.Dict]: + content: str = ..., + username: str = ..., + avatar_url: str = ..., + tts: bool = ..., + wait: bool = ..., + file: typing.Tuple[str, conversions.FileLikeT] = ..., + embeds: typing.Sequence[typing.Dict[str, typing.Any]] = ..., + allowed_mentions: typing.Dict[str, typing.Any] = ..., + ) -> typing.Optional[typing.Dict[str, typing.Any]]: """Create a message in the given channel or DM. Parameters @@ -3068,25 +2999,25 @@ async def execute_webhook( content : :obj:`str` If specified, the webhook message content to send. username : :obj:`str` - If specified, the username to override the webhook's username + If specified, the username to override the webhook's username for this request. avatar_url : :obj:`str` - If specified, the url of an image to override the webhook's + If specified, the url of an image to override the webhook's avatar with for this request. tts : :obj:`bool` If specified, whether this webhook should create a TTS message. wait : :obj:`bool` - If specified, whether this request should wait for the webhook + If specified, whether this request should wait for the webhook to be executed and return the resultant message object. file : :obj:`typing.Tuple` [ :obj:`str`, :obj:`storage.FileLikeT` ] - If specified, a tuple of the file name and either raw :obj:`bytes` - or a :obj:`io.IOBase` derived object that points to a buffer + If specified, a tuple of the file name and either raw :obj:`bytes` + or a :obj:`io.IOBase` derived object that points to a buffer containing said file. - embeds : :obj:`typing.Sequence` [:obj:`typing.Dict`] - If specified, the sequence of embed objects that will be sent + embeds : :obj:`typing.Sequence` [:obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ]`] + If specified, the sequence of embed objects that will be sent with this message. - allowed_mentions : :obj:`typing.Dict` - If specified, the mentions to parse from the ``content``. + allowed_mentions : :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ]` + If specified, the mentions to parse from the ``content``. If not specified, will parse all mentions from the ``content``. Raises @@ -3094,13 +3025,13 @@ async def execute_webhook( :obj:`hikari.net.errors.NotFoundHTTPError` If the channel ID or webhook ID is not found. :obj:`hikari.net.errors.BadRequestHTTPError` - This can be raised if the file is too large; if the embed exceeds - the defined limits; if the message content is specified only and - empty or greater than ``2000`` characters; if neither content, file + This can be raised if the file is too large; if the embed exceeds + the defined limits; if the message content is specified only and + empty or greater than ``2000`` characters; if neither content, file or embed are specified; if there is a duplicate id in only of the - fields in ``allowed_mentions``; if you specify to parse all - users/roles mentions but also specify which users/roles to - parse only. + fields in ``allowed_mentions``; if you specify to parse all + users/roles mentions but also specify which users/roles to parse + only. :obj:`hikari.net.errors.ForbiddenHTTPError` If you lack permissions to send to this channel. :obj:`hikari.net.errors.UnauthorizedHTTPError` @@ -3108,31 +3039,31 @@ async def execute_webhook( Returns ------- - :obj:`hikari.internal_utilities.typing.Dict`, optional + :obj:`hikari._internal.typing.Dict` [ :obj:`str`, :obj:`typing.Any` ]`, optional The created message object if ``wait`` is ``True``, else ``None``. """ form = aiohttp.FormData() json_payload = {} - transformations.put_if_specified(json_payload, "content", content) - transformations.put_if_specified(json_payload, "username", username) - transformations.put_if_specified(json_payload, "avatar_url", avatar_url) - transformations.put_if_specified(json_payload, "tts", tts) - transformations.put_if_specified(json_payload, "embeds", embeds) - transformations.put_if_specified(json_payload, "allowed_mentions", allowed_mentions) + conversions.put_if_specified(json_payload, "content", content) + conversions.put_if_specified(json_payload, "username", username) + conversions.put_if_specified(json_payload, "avatar_url", avatar_url) + conversions.put_if_specified(json_payload, "tts", tts) + conversions.put_if_specified(json_payload, "embeds", embeds) + conversions.put_if_specified(json_payload, "allowed_mentions", allowed_mentions) form.add_field("payload_json", json.dumps(json_payload), content_type="application/json") if file is not ...: file_name, file = file - file = storage.make_resource_seekable(file) + file = conversions.make_resource_seekable(file) re_seekable_resources = [file] form.add_field("file", file, filename=file_name, content_type="application/octet-stream") else: re_seekable_resources = [] query = {} - transformations.put_if_specified(query, "wait", wait, str) + conversions.put_if_specified(query, "wait", wait, str) route = routes.WEBHOOK_WITH_TOKEN.compile(self.POST, webhook_id=webhook_id, webhook_token=webhook_token) return await self._request( @@ -3147,12 +3078,12 @@ async def execute_webhook( # OAUTH2 # ########## - async def get_current_application_info(self) -> typing.Dict: + async def get_current_application_info(self) -> typing.Dict[str, typing.Any]: """Get the current application information. Returns ------- - :obj:`typing.Dict` + :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ]` An application info object. """ route = routes.OAUTH2_APPLICATIONS_ME.compile(self.GET) diff --git a/hikari/net/routes.py b/hikari/net/routes.py index 8bb80021d3..3bf862fb61 100644 --- a/hikari/net/routes.py +++ b/hikari/net/routes.py @@ -80,6 +80,23 @@ def create_url(self, base_url: str) -> str: return base_url + self.compiled_path def create_real_bucket_hash(self, initial_bucket_hash: str) -> str: + """Create a full bucket hash from a given initial hash. + + The result of this hash will be decided by the value of the major + parameters passed to the route during the compilation phase. + + Parameters + ---------- + initial_bucket_hash: :obj:`str` + The initial bucket hash provided by Discord in the HTTP headers + for a given response. + + Returns + ------- + :obj:`str` + The input hash amalgamated with a hash code produced by the + major parameters in this compiled route instance. + """ return initial_bucket_hash + ";" + self.major_params_hash def __hash__(self) -> int: @@ -105,7 +122,7 @@ def __str__(self) -> str: class RouteTemplate: """A template used to create compiled routes for specific parameters. - + These compiled routes are used to identify rate limit buckets. Parameters @@ -113,8 +130,9 @@ class RouteTemplate: path_template : :obj:`str` The template string for the path to use. major_params : :obj:`str` - A collection of major parameter names that appear in the template path. - If not specified, the default major parameter names are extracted and used in-place. + A collection of major parameter names that appear in the template path. + If not specified, the default major parameter names are extracted and + used in-place. """ __slots__ = ("path_template", "major_params") diff --git a/hikari/net/shard.py b/hikari/net/shard.py index 661a1ee627..efbc378cdf 100644 --- a/hikari/net/shard.py +++ b/hikari/net/shard.py @@ -47,8 +47,8 @@ import aiohttp.typedefs -from hikari.internal_utilities import containers -from hikari.internal_utilities import loggers +from hikari._internal import more_collections +from hikari._internal import more_logging from hikari.net import codes from hikari.net import errors from hikari.net import ratelimits @@ -353,7 +353,7 @@ def __init__( version: typing.Union[int, versions.GatewayVersion] = versions.GatewayVersion.STABLE, ) -> None: # Sanitise the URL... - scheme, netloc, path, params, query, fragment = urllib.parse.urlparse(url, allow_fragments=True) + scheme, netloc, path, params, _, _ = urllib.parse.urlparse(url, allow_fragments=True) new_query = dict(v=int(version), encoding="json") if compression: @@ -395,7 +395,7 @@ def __init__( self.identify_event: asyncio.Event = asyncio.Event() self.last_heartbeat_sent: float = float("nan") self.last_message_received: float = float("nan") - self.logger: logging.Logger = loggers.get_named_logger(self, shard_id) + self.logger: logging.Logger = more_logging.get_named_logger(self, shard_id) self.requesting_close_event: asyncio.Event = asyncio.Event() self.ready_event: asyncio.Event = asyncio.Event() self.session_id: typing.Optional[str] = session_id @@ -451,7 +451,7 @@ def intents(self) -> typing.Optional[codes.GatewayIntent]: def reconnect_count(self) -> int: """The ammount of times the gateway has reconnected since initialization. - This can be used as a debugging context, but is also used internally + This can be used as a debugging context, but is also used internally for exception management. Returns @@ -481,11 +481,15 @@ def current_presence(self) -> typing.Dict: @typing.overload async def request_guild_members(self, guild_id: str, *guild_ids: str, limit: int = 0, query: str = "") -> None: - ... + """Request guild members in the given guilds using a query string and + an optional limit. + """ @typing.overload async def request_guild_members(self, guild_id: str, *guild_ids: str, user_ids: typing.Collection[str]) -> None: - ... + """Request guild members in the given guilds using a set of user IDs + to resolve. + """ async def request_guild_members(self, guild_id, *guild_ids, **kwargs): """Requests the guild members for a guild or set of guilds. @@ -517,7 +521,7 @@ async def request_guild_members(self, guild_id, *guild_ids, **kwargs): Note ---- You may not specify ``user_ids`` at the same time as ``limit`` and - ``query``. Likewise, if you specify one of ``limit`` or ``query``, + ``query``. Likewise, if you specify one of ``limit`` or ``query``, the other must also be included. The default, if no optional arguments are specified, is to use a ``limit`` of ``0`` and a ``query`` of ``""`` (empty-string). @@ -618,7 +622,7 @@ async def connect(self, client_session_type=aiohttp.ClientSession) -> None: self.dispatch( self, "RECONNECT" if self.disconnect_count else "CONNECT", - typing.cast(typing.Dict, containers.EMPTY_DICT), + typing.cast(typing.Dict, more_collections.EMPTY_DICT), ) self.logger.info("received HELLO, interval is %ss", self.heartbeat_interval) @@ -670,7 +674,7 @@ async def connect(self, client_session_type=aiohttp.ClientSession) -> None: self._ws = None await self._session.close() self._session = None - self.dispatch(self, "DISCONNECT", typing.cast(typing.Dict, containers.EMPTY_DICT)) + self.dispatch(self, "DISCONNECT", typing.cast(typing.Dict, more_collections.EMPTY_DICT)) @property def _ws_connect_kwargs(self): @@ -694,17 +698,14 @@ async def _identify_or_resume_then_poll_events(self): if self.session_id is None: self.status = GatewayStatus.IDENTIFYING self.logger.debug("sending IDENTIFY") + pl = { "op": codes.GatewayOpcode.IDENTIFY, "d": { "token": self._token, "compress": False, "large_threshold": self._large_threshold, - "properties": { - "$os": user_agent.system_type(), - "$browser": user_agent.library_version(), - "$device": user_agent.python_version(), - }, + "properties": user_agent.UserAgent().websocket_triplet, "shard": [self.shard_id, self.shard_count], }, } @@ -825,7 +826,8 @@ async def _receive(self): packets, ) return obj - elif message.type == aiohttp.WSMsgType.CLOSE: + + if message.type == aiohttp.WSMsgType.CLOSE: close_code = self._ws.close_code try: meaning = codes.GatewayCloseCode(close_code) @@ -835,16 +837,18 @@ async def _receive(self): self.logger.debug("connection closed with code %s (%s)", close_code, meaning) if close_code == codes.GatewayCloseCode.AUTHENTICATION_FAILED: raise errors.GatewayInvalidTokenError() - elif close_code in (codes.GatewayCloseCode.SESSION_TIMEOUT, codes.GatewayCloseCode.INVALID_SEQ): + if close_code in (codes.GatewayCloseCode.SESSION_TIMEOUT, codes.GatewayCloseCode.INVALID_SEQ): raise errors.GatewayInvalidSessionError(False) - elif close_code == codes.GatewayCloseCode.SHARDING_REQUIRED: + if close_code == codes.GatewayCloseCode.SHARDING_REQUIRED: raise errors.GatewayNeedsShardingError() - else: - raise errors.GatewayServerClosedConnectionError(close_code) - elif message.type in (aiohttp.WSMsgType.CLOSING, aiohttp.WSMsgType.CLOSED): + + raise errors.GatewayServerClosedConnectionError(close_code) + + if message.type in (aiohttp.WSMsgType.CLOSING, aiohttp.WSMsgType.CLOSED): self.logger.debug("connection has been marked as closed") raise errors.GatewayClientClosedError() - elif message.type == aiohttp.WSMsgType.ERROR: + + if message.type == aiohttp.WSMsgType.ERROR: ex = self._ws.exception() self.logger.debug("connection encountered some error", exc_info=ex) raise errors.GatewayError("Unexpected exception occurred") from ex diff --git a/hikari/net/user_agent.py b/hikari/net/user_agent.py index 42dcdc1c0b..34b4910511 100644 --- a/hikari/net/user_agent.py +++ b/hikari/net/user_agent.py @@ -24,102 +24,73 @@ This information is provided to enable Discord to detect that you are using a valid bot and not attempting to abuse the API. """ -__all__ = ["library_version", "python_version", "system_type", "user_agent"] +__all__ = ["UserAgent"] -import platform +import typing -from hikari.internal_utilities import cache +from hikari._internal import meta -@cache.cached_function() -def library_version() -> str: - """The version of the library being used. +class UserAgent(metaclass=meta.SingletonMeta): + """Platform version info. - Returns - ------- - :obj:`str` - A string representing the version of this library. - - Example - ------- - .. code-block:: python - - >>> from hikari.net import user_agent - >>> print(user_agent.library_version()) - hikari 0.0.71 - """ - from hikari._about import __version__ - - return f"hikari {__version__}" - - -@cache.cached_function() -def python_version() -> str: - """The python version being used. - - Returns - ------- - :obj:`str` - A string representing the version of this release of Python. - - Example - ------- - .. code-block:: python - - >>> from hikari.net import user_agent - >>> print(user_agent.python_version()) - CPython 3.8.1 GCC 9.2.0 - """ - attrs = [ - platform.python_implementation(), - platform.python_version(), - platform.python_branch(), - platform.python_compiler(), - ] - return " ".join(a.strip() for a in attrs if a.strip()) - - -@cache.cached_function() -def system_type() -> str: - """The operating system being used. - - Returns - ------- - :obj:`str` - A string representing the system being used. - - Example - ------- - .. code-block:: python - - >>> from hikari.net import user_agent - >>> print(user_agent.system_type()) - Linux-5.4.15-2-MANJARO-x86_64-with-glibc2.2.5 - """ - # Might change this eventually to be more detailed, who knows. - return platform.platform() - - -@cache.cached_function() -def user_agent() -> str: - """The user agent of the bot - - Returns - ------- - :obj:`str` - The string to use for the library ``User-Agent`` HTTP header that is required - to be sent with every HTTP request. - - Example - ------- - .. code-block:: python - - >>> from hikari.net import user_agent - >>> print(user_agent.user_agent()) - DiscordBot (https://gitlab.com/nekokatt/hikari, 0.0.71) CPython 3.8.1 GCC 9.2.0 Linux + Notes + ----- + This is a singleton. """ - from hikari._about import __version__, __url__ - system = system_type() - python = python_version() - return f"DiscordBot ({__url__}, {__version__}) {python} {system}" + #: The version of the library. + #: + #: ``"hikari 1.0.1"`` + library_version: typing.Final[str] + + #: The platform version. + #: + #: ``"CPython 3.8.2 GCC 9.2.0"`` + platform_version: typing.Final[str] + + #: The operating system type. + #: + #: ``"Linux-5.4.15-2-MANJARO-x86_64-with-glibc2.2.5"`` + system_type: typing.Final[str] + + #: The Hikari-specific user-agent to use in HTTP connections to Discord. + #: + #: ``"DiscordBot (https://gitlab.com/nekokatt/hikari; 1.0.1; Nekokatt) CPython 3.8.2 GCC 9.2.0 Linux"`` + user_agent: typing.Final[str] + + def __init__(self): + from hikari._about import __author__, __url__, __version__ + from platform import python_implementation, python_version, python_branch, python_compiler, platform + + self.library_version = f"hikari {__version__}" + self.platform_version = self._join_strip( + python_implementation(), python_version(), python_branch(), python_compiler() + ) + self.system_type = platform() + self.user_agent = f"DiscordBot ({__url__}; {__version__}; {__author__}) {python_version()} {self.system_type}" + + def __attr__(_): + raise TypeError("cannot change attributes once set") + + self.__delattr__ = __attr__ + self.__setattr__ = __attr__ + + @staticmethod + def _join_strip(*args): + return " ".join((arg.strip() for arg in args if arg.strip())) + + @property + def websocket_triplet(self) -> typing.Dict[str, str]: + """ + Returns + ------- + :obj:`typing.Dict` [ :obj:`str`, :obj:`str` ]: + The object to send to Discord representing device info when + IDENTIFYing with the gateway. + """ + return { + "$os": self.system_type, + "$browser": self.library_version, + "$device": self.platform_version, + } diff --git a/noxfile.py b/noxfile.py index b27e34630f..528a461d43 100644 --- a/noxfile.py +++ b/noxfile.py @@ -35,36 +35,18 @@ def pathify(arg, *args, root=False): OWNER = "nekokatt" TECHNICAL_DIR = "technical" TEST_PATH = "tests/hikari" -PYLINT_VERSION = "2.4.4" -PYLINT_THRESHOLD = 8 -COVERAGE_RC = ".coveragerc" ARTIFACT_DIR = "public" DOCUMENTATION_DIR = "docs" CI_SCRIPT_DIR = "tasks" -SPHINX_OPTS = "-WTvvn" BLACK_PACKAGES = [MAIN_PACKAGE, TEST_PATH] -BLACK_PATHS = [m.replace(".", "/") for m in BLACK_PACKAGES] + [__file__, pathify(DOCUMENTATION_DIR, "conf.py")] +BLACK_PATHS = [m.replace(".", "/") for m in BLACK_PACKAGES] + [ + __file__, + pathify(DOCUMENTATION_DIR, "conf.py"), + "noxfile.py", +] BLACK_SHIM_PATH = pathify(CI_SCRIPT_DIR, "black.py") -GENDOC_PATH = pathify(CI_SCRIPT_DIR, "gendoc.py") MAIN_PACKAGE_PATH = MAIN_PACKAGE.replace(".", "/") REPOSITORY = f"https://gitlab.com/{OWNER}/{MAIN_PACKAGE}" -PYTEST_ARGS = [ - "-n", - "auto", - "--cov", - MAIN_PACKAGE, - "--cov-config", - COVERAGE_RC, - "--cov-report", - "term", - "--cov-report", - f"html:{ARTIFACT_DIR}/coverage/html", - "--cov-branch", - f"--junitxml={ARTIFACT_DIR}/tests.xml", - "--showlocals", - # "--testdox", - # "--force-testdox", -] @nox.session(reuse_venv=True) @@ -72,7 +54,34 @@ def test(session) -> None: """Run unit tests in Pytest.""" session.install("-r", "requirements.txt") session.install("-r", "dev-requirements.txt") - session.run("python", "-m", "pytest", *PYTEST_ARGS, *session.posargs, TEST_PATH) + + additional_opts = ["--pastebin=all"] if os.getenv("CI") else [] + + session.run( + "python", + "-m", + "pytest", + "-c", + "pytest.ini", + "-r", + "a", + *additional_opts, + "--full-trace", + "-n", + "auto", + "--cov", + MAIN_PACKAGE, + "--cov-config=coverage.ini", + "--cov-report", + "term", + "--cov-report", + f"html:{ARTIFACT_DIR}/coverage/html", + "--cov-branch", + f"--junitxml={ARTIFACT_DIR}/tests.xml", + "--showlocals", + *session.posargs, + TEST_PATH, + ) @nox.session(reuse_venv=True) @@ -82,20 +91,11 @@ def documentation(session) -> None: session.install("-r", "dev-requirements.txt") session.install("-r", "doc-requirements.txt") - session.env["SPHINXOPTS"] = SPHINX_OPTS + session.env["SPHINXOPTS"] = "-WTvvn" tech_dir = pathify(DOCUMENTATION_DIR, TECHNICAL_DIR) shutil.rmtree(tech_dir, ignore_errors=True, onerror=lambda *_: None) os.mkdir(tech_dir) - # session.run( - # "python", - # GENDOC_PATH, - # ".", - # MAIN_PACKAGE, - # pathify(DOCUMENTATION_DIR, "_templates", "gendoc"), - # pathify(DOCUMENTATION_DIR, "index.rst"), - # pathify(DOCUMENTATION_DIR, TECHNICAL_DIR), - # ) session.run("sphinx-apidoc", "-e", "-o", tech_dir, MAIN_PACKAGE) session.run("python", "-m", "sphinx.cmd.build", DOCUMENTATION_DIR, ARTIFACT_DIR, "-b", "html") @@ -124,20 +124,33 @@ def format(session) -> None: @nox.session(reuse_venv=True) +def docstyle(session) -> None: + """Reformat code with Black. Pass the '--check' flag to check formatting only.""" + session.install("pydocstyle") + session.chdir(MAIN_PACKAGE_PATH) + # add -e flag for explainations. + with contextlib.suppress(Exception): # TODO: remove this once these are being fixed. + session.run("pydocstyle", "--config=../pydocstyle.ini") + + +@nox.session(reuse_venv=True,) def lint(session) -> None: """Check formating with pylint""" session.install("-r", "requirements.txt") session.install("-r", "dev-requirements.txt") session.install("-r", "doc-requirements.txt") session.install("pylint-junit==0.2.0") - # TODO: Change code under this comment to the commented code when we update to pylint 2.5 - # session.run("pip", "install", f"pylint=={PYLINT_VERSION}" if PYLINT_VERSION else "pylint") - # frozen version of pylint 2.5 pre-release to make sure that nothing will break - session.install("git+https://github.com/davfsa/pylint") + session.install("pylint") pkg = MAIN_PACKAGE.split(".")[0] try: - session.run("pylint", pkg, "--rcfile=pylintrc", f"--fail-under={PYLINT_THRESHOLD}") + session.run( + "pylint", + pkg, + "--rcfile=pylint.ini", + "--spelling-private-dict-file=dict.txt", + success_codes=list(range(0, 256)) + ) finally: os.makedirs(ARTIFACT_DIR, exist_ok=True) @@ -145,10 +158,10 @@ def lint(session) -> None: session.run( "pylint", pkg, - "--rcfile=pylintrc", - f"--fail-under={PYLINT_THRESHOLD}", + "--rcfile=pylint.ini", "--output-format=pylint_junit.JUnitReporter", stdout=fp, + success_codes=list(range(0, 256)) ) diff --git a/pydocstyle.ini b/pydocstyle.ini new file mode 100644 index 0000000000..bb2b6db2e8 --- /dev/null +++ b/pydocstyle.ini @@ -0,0 +1,3 @@ +[pydocstyle] +convention=numpy +ignore= diff --git a/pylintrc b/pylint.ini similarity index 65% rename from pylintrc rename to pylint.ini index bfcb5ace68..365905a72d 100644 --- a/pylintrc +++ b/pylint.ini @@ -60,356 +60,82 @@ confidence= # --enable=similarities". If you want to run only the classes checker, but have # no Warning level messages displayed, use "--disable=all --enable=classes # --disable=W". -disable=blacklisted-name, - invalid-name, - empty-docstring, - unneeded-not, - missing-module-docstring, - missing-class-docstring, - missing-function-docstring, - singleton-comparison, - misplaced-comparison-constant, - unidiomatic-typecheck, - consider-using-enumerate, - consider-iterating-dictionary, - bad-classmethod-argument, - bad-mcs-method-argument, - bad-mcs-classmethod-argument, - single-string-used-for-slots, - line-too-long, - too-many-lines, - trailing-whitespace, - multiple-statements, - superfluous-parens, +disable=bad-continuation, bad-whitespace, - mixed-line-endings, - unexpected-line-ending-format, - bad-continuation, - wrong-import-position, - useless-import-alias, - import-outside-toplevel, - len-as-condition, - print-statement, - parameter-unpacking, - unpacking-in-except, - old-raise-syntax, - backtick, - long-suffix, - old-ne-operator, - old-octal-literal, - import-star-module-level, - non-ascii-bytes-literal, - raw-checker-failed, - bad-inline-option, - locally-disabled, - file-ignored, - suppressed-message, - useless-suppression, - deprecated-pragma, - use-symbolic-message-instead, + binary-op-exception, c-extension-no-member, - literal-comparison, - comparison-with-itself, - no-self-use, - no-classmethod-decorator, - no-staticmethod-decorator, - useless-object-inheritance, - property-with-parameters, - cyclic-import, - duplicate-code, - too-many-ancestors, - too-many-instance-attributes, - too-few-public-methods, - too-many-public-methods, - too-many-return-statements, - too-many-branches, - too-many-arguments, - too-many-locals, - too-many-statements, - too-many-boolean-expressions, - consider-merging-isinstance, - too-many-nested-blocks, - simplifiable-if-statement, - redefined-argument-from-local, - no-else-return, - consider-using-ternary, - trailing-comma-tuple, - stop-iteration-return, - simplify-boolean-expression, - inconsistent-return-statements, - useless-return, - consider-swap-variables, - consider-using-join, - consider-using-in, - consider-using-get, - chained-comparison, - consider-using-dict-comprehension, - consider-using-set-comprehension, - simplifiable-if-expression, - no-else-raise, - unnecessary-comprehension, + cell-var-from-loop, + cmp-method, consider-using-sys-exit, - no-else-break, - no-else-continue, - expression-not-assigned, - duplicate-key, - assign-to-new-keyword, - useless-else-on-loop, - exec-used, + cyclic-import, + deprecated-str-translate-call, + deprecated-types-field, + dict-iter-method, + dict-view-method, + div-method, eval-used, - confusing-with-statement, - using-constant-test, - self-assigning-variable, - redeclared-assigned-name, - comparison-with-callable, - assert-on-tuple, - attribute-defined-outside-init, - bad-staticmethod-argument, - protected-access, - arguments-differ, - signature-differs, - super-init-not-called, - no-init, - non-parent-init-called, - useless-super-delegation, - invalid-overridden-method, - bad-indentation, - mixed-indentation, - wildcard-import, - import-self, - preferred-module, - misplaced-future, - fixme, - global-variable-undefined, - global-variable-not-assigned, - global-statement, + exception-message-attribute, + exec-used, + expression-not-assigned, + filter-builtin-not-iterating, + format-combined-specification, global-at-module-level, - unused-import, - unused-variable, - unused-argument, - unused-wildcard-import, - redefined-outer-name, - redefined-builtin, - redefine-in-handler, - undefined-loop-variable, - unbalanced-tuple-unpacking, - cell-var-from-loop, - possibly-unused-variable, - self-cls-assignment, - bare-except, - broad-except, - duplicate-except, - try-except-raise, - binary-op-exception, - raising-format-tuple, - wrong-exception-operation, + global-statement, + global-variable-not-assigned, + global-variable-undefined, + hex-method, + idiv-method, + import-outside-toplevel, + import-star-module-level, + input-builtin, + invalid-name, keyword-arg-before-vararg, - arguments-out-of-order, - bad-format-string-key, - unused-format-string-key, - bad-format-string, - missing-format-argument-key, - unused-format-string-argument, - format-combined-specification, + long-suffix, + map-builtin-not-iterating, + metaclass-assignment, + misplaced-future, missing-format-attribute, - invalid-format-index, - duplicate-string-formatting-argument, - anomalous-backslash-in-string, - anomalous-unicode-escape-in-string, - implicit-str-concat-in-sequence, - bad-open-mode, - boolean-datetime, - redundant-unittest-assert, - deprecated-method, - bad-thread-instantiation, - shallow-copy-environ, - invalid-envvar-default, - subprocess-popen-preexec-fn, - subprocess-run-check, - apply-builtin, - basestring-builtin, - buffer-builtin, - cmp-builtin, - coerce-builtin, - execfile-builtin, - file-builtin, - long-builtin, - raw_input-builtin, - reduce-builtin, - standarderror-builtin, - unicode-builtin, - xrange-builtin, - coerce-method, - delslice-method, - getslice-method, - setslice-method, - no-absolute-import, - old-division, - dict-iter-method, - dict-view-method, next-method-called, - metaclass-assignment, - indexing-exception, - raising-string, - reload-builtin, - oct-method, - hex-method, + no-absolute-import, + no-else-return, + no-member, + no-self-use, + non-ascii-bytes-literal, nonzero-method, - cmp-method, - input-builtin, - round-builtin, - intern-builtin, - unichr-builtin, - map-builtin-not-iterating, - zip-builtin-not-iterating, + oct-method, + old-division, + old-octal-literal, + parameter-unpacking, + possibly-unused-variable, + preferred-module, + print-statement, range-builtin-not-iterating, - filter-builtin-not-iterating, - using-cmp-argument, - eq-without-hash, - div-method, - idiv-method, rdiv-method, - exception-message-attribute, - invalid-str-codec, - sys-max-int, - bad-python3-import, - deprecated-string-function, - deprecated-str-translate-call, - deprecated-itertools-function, - deprecated-types-field, - next-method-defined, - dict-items-not-iterating, - dict-keys-not-iterating, - dict-values-not-iterating, - deprecated-operator-function, - deprecated-urllib-function, + redeclared-assigned-name, + redefine-in-handler, + reload-builtin, + round-builtin, + shallow-copy-environ, + simplify-boolean-expression, + subprocess-popen-preexec-fn, + subprocess-run-check, + superfluous-parens, + super-init-not-called, + too-few-public-methods, + too-many-ancestors, + too-many-arguments, + too-many-branches, + too-many-function-args, + too-many-instance-attributes, + too-many-locals, + too-many-public-methods, + too-many-statements, + try-except-raise, + unused-import, + wildcard-import, + wrong-exception-operation, xreadlines-attribute, - deprecated-sys-function, - exception-escape, - comprehension-escape, - too-many-function-args - -# Enable the message, report, category or checker with the given id(s). You can -# either give multiple identifier separated by comma (,) or put this option -# multiple time (only on the command line, not in the configuration file where -# it should appear only once). See also the "--disable" option for examples. -enable=syntax-error, - unrecognized-inline-option, - bad-option-value, - init-is-generator, - return-in-init, - function-redefined, - not-in-loop, - return-outside-function, - yield-outside-function, - return-arg-in-generator, - nonexistent-operator, - duplicate-argument-name, - abstract-class-instantiated, - bad-reversed-sequence, - too-many-star-expressions, - invalid-star-assignment-target, - star-needs-assignment-target, - nonlocal-and-global, - continue-in-finally, - nonlocal-without-binding, - used-prior-global-declaration, - misplaced-format-function, - method-hidden, - access-member-before-definition, - no-method-argument, - no-self-argument, - invalid-slots-object, - assigning-non-slot, - invalid-slots, - inherit-non-class, - inconsistent-mro, - duplicate-bases, - class-variable-slots-conflict, - non-iterator-returned, - unexpected-special-method-signature, - invalid-length-returned, - import-error, - relative-beyond-top-level, - used-before-assignment, - undefined-variable, - undefined-all-variable, - invalid-all-object, - no-name-in-module, - unpacking-non-sequence, - bad-except-order, - raising-bad-type, - bad-exception-context, - misplaced-bare-raise, - raising-non-exception, - notimplemented-raised, - catching-non-exception, - bad-super-call, - no-member, - not-callable, - assignment-from-no-return, - no-value-for-parameter, - unexpected-keyword-arg, - redundant-keyword-arg, - missing-kwoa, - invalid-sequence-index, - invalid-slice-index, - assignment-from-none, - not-context-manager, - invalid-unary-operand-type, - unsupported-binary-operation, - repeated-keyword, - not-an-iterable, - not-a-mapping, - unsupported-membership-test, - unsubscriptable-object, - unsupported-assignment-operation, - unsupported-delete-operation, - invalid-metaclass, - unhashable-dict-key, - dict-iter-missing-items, - logging-unsupported-format, - logging-format-truncated, - logging-too-many-args, - logging-too-few-args, - logging-not-lazy, - logging-format-interpolation, - bad-format-character, - truncated-format-string, - mixed-format-string, - format-needs-mapping, - missing-format-string-key, - too-many-format-args, - too-few-format-args, - bad-string-format-type, - bad-str-strip-call, - invalid-envvar-value, - yield-inside-async-function, - not-async-context-manager, - fatal, - astroid-error, - parse-error, - method-check-failed, - deprecated-module, - reimported, - unnecessary-semicolon, - abstract-method, - lost-exception, - missing-parentheses-for-call-in-test, - unnecessary-lambda, - unnecessary-pass, - dangerous-default-value, - pointless-statement, - pointless-string-statement, - unreachable, - wrong-spelling-in-comment, - wrong-spelling-in-docstring, - invalid-characters-in-docstring, - multiple-imports, - wrong-import-order, - ungrouped-imports, - trailing-newlines, - missing-final-newline, + zip-builtin-not-iterating, [REPORTS] @@ -428,7 +154,7 @@ evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / stateme # Set the output format. Available formats are text, parseable, colorized, json # and msvs (visual studio). You can also give a reporter class, e.g. # mypackage.mymodule.MyReporterClass. -output-format=text +output-format=colorized # Tells whether to display a full report or only the messages. reports=yes @@ -446,7 +172,9 @@ max-nested-blocks=6 # inconsistent-return-statements if a never returning function is called then # it will be considered as an explicit return statement and no message will be # printed. -never-returning-functions=sys.exit +never-returning-functions=sys.exit, + exit, + quit [SPELLING] @@ -478,10 +206,10 @@ ignore-comments=yes ignore-docstrings=yes # Ignore imports when computing similarities. -ignore-imports=no +ignore-imports=yes # Minimum lines number of a similarity. -min-similarity-lines=4 +min-similarity-lines=5 [TYPECHECK] @@ -489,7 +217,8 @@ min-similarity-lines=4 # List of decorators that produce context managers, such as # contextlib.contextmanager. Add to this list to register other decorators that # produce valid context managers. -contextmanager-decorators=contextlib.contextmanager +contextmanager-decorators=contextlib.contextmanager, + contextlib.asynccontextmanager # List of members which are set dynamically and missed by pylint inference # system, and so shouldn't trigger E1101 when accessed. Python regular @@ -536,7 +265,9 @@ missing-member-hint-distance=1 missing-member-max-choices=1 # List of decorators that change the signature of a decorated function. -signature-mutators= +signature-mutators=attr.s, + marshaller.attrs, + functools.wraps [BASIC] @@ -600,8 +331,11 @@ good-names=i, j, k, ex, - Run, - _ + T, + U, + V, + _, + __ # Include a hint for the correct naming format with invalid-name. include-naming-hint=no @@ -638,7 +372,7 @@ no-docstring-rgx=^_ # List of decorators that produce properties, such as abc.abstractproperty. Add # to this list to register other decorators that produce valid properties. # These decorators are taken in consideration only for invalid-name. -property-classes=abc.abstractproperty +property-classes= # Naming style matching correct variable names. variable-naming-style=snake_case @@ -661,7 +395,11 @@ check-str-concat-over-line-jumps=no # List of note tags to take in consideration, separated by a comma. notes=FIXME, XXX, - TODO + TODO, + TO-DO, + FIX-ME, + BUG, + ???, [VARIABLES] @@ -697,23 +435,23 @@ redefining-builtins-modules=six.moves,past.builtins,future.builtins,builtins,io [FORMAT] # Expected format of line ending, e.g. empty (any line ending), LF or CRLF. -expected-line-ending-format= +expected-line-ending-format=LF # Regexp for a line that is allowed to be longer than the limit. ignore-long-lines=^\s*(# )??$ # Number of spaces of indent required inside a hanging or continued line. -indent-after-paren=4 +indent-after-paren=0 # String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 # tab). indent-string=' ' # Maximum number of characters on a single line. -max-line-length=100 +max-line-length=120 # Maximum number of lines in a module. -max-module-lines=1000 +max-module-lines=4000 # List of optional constructs for which whitespace checking is disabled. `dict- # separator` is used to allow tabulation in dicts, etc.: {1 : 1,\n222: 2}. @@ -756,7 +494,8 @@ exclude-protected=_asdict, _fields, _replace, _source, - _make + _make, + _internal, # List of valid names for the first argument in a class method. valid-classmethod-first-arg=cls @@ -772,7 +511,7 @@ valid-metaclass-classmethod-first-arg=cls allow-any-import-level= # Allow wildcard imports from modules that define __all__. -allow-wildcard-with-all=no +allow-wildcard-with-all=yes # Analyse import fallback blocks. This can be used to support both Python 2 and # 3 compatible code, which means that the block might have code that exists @@ -808,34 +547,34 @@ preferred-modules= [DESIGN] # Maximum number of arguments for function / method. -max-args=5 +max-args=999999999999 # Maximum number of attributes for a class (see R0902). -max-attributes=7 +max-attributes=999999999999 # Maximum number of boolean expressions in an if statement (see R0916). -max-bool-expr=5 +max-bool-expr=3 # Maximum number of branch for function / method body. -max-branches=12 +max-branches=999999999999 # Maximum number of locals for function / method body. -max-locals=15 +max-locals=999999999999 # Maximum number of parents for a class (see R0901). max-parents=7 # Maximum number of public methods for a class (see R0904). -max-public-methods=20 +max-public-methods=999999999999 # Maximum number of return / yield for function / method body. -max-returns=6 +max-returns=999999999999 # Maximum number of statements in function / method body. -max-statements=50 +max-statements=999999999999 # Minimum number of public methods for a class (see R0903). -min-public-methods=2 +min-public-methods=999999999999 [EXCEPTIONS] diff --git a/tests/hikari/_helpers.py b/tests/hikari/_helpers.py index af7b4ecefb..5727a582a0 100644 --- a/tests/hikari/_helpers.py +++ b/tests/hikari/_helpers.py @@ -35,7 +35,7 @@ import cymock as mock import pytest -from hikari.internal_utilities import marshaller +from hikari._internal import marshaller _LOGGER = logging.getLogger(__name__) diff --git a/tests/hikari/internal_utilities/__init__.py b/tests/hikari/_internal/__init__.py similarity index 100% rename from tests/hikari/internal_utilities/__init__.py rename to tests/hikari/_internal/__init__.py diff --git a/tests/hikari/internal_utilities/test_assertions.py b/tests/hikari/_internal/test_assertions.py similarity index 97% rename from tests/hikari/internal_utilities/test_assertions.py rename to tests/hikari/_internal/test_assertions.py index 5c281851c8..b733be8381 100644 --- a/tests/hikari/internal_utilities/test_assertions.py +++ b/tests/hikari/_internal/test_assertions.py @@ -18,7 +18,7 @@ # along with Hikari. If not, see . import pytest -from hikari.internal_utilities import assertions +from hikari._internal import assertions from tests.hikari import _helpers diff --git a/tests/hikari/internal_utilities/test_cdn.py b/tests/hikari/_internal/test_cdn.py similarity index 96% rename from tests/hikari/internal_utilities/test_cdn.py rename to tests/hikari/_internal/test_cdn.py index 22334a9702..6bb3f7f126 100644 --- a/tests/hikari/internal_utilities/test_cdn.py +++ b/tests/hikari/_internal/test_cdn.py @@ -16,7 +16,7 @@ # # You should have received a copy of the GNU Lesser General Public License # along ith Hikari. If not, see . -from hikari.internal_utilities import cdn +from hikari._internal import cdn def test_generate_cdn_url(): diff --git a/tests/hikari/_internal/test_conversions.py b/tests/hikari/_internal/test_conversions.py new file mode 100644 index 0000000000..f260b82b06 --- /dev/null +++ b/tests/hikari/_internal/test_conversions.py @@ -0,0 +1,195 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# Copyright © Nekoka.tt 2019-2020 +# +# This file is part of Hikari. +# +# Hikari is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Hikari is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Hikari. If not, see . +import datetime +import io + +import pytest + +from hikari._internal import conversions + + +@pytest.mark.parametrize( + ["value", "cast", "expect"], + [ + ("22", int, 22), + (None, int, None), + ("22", lambda a: float(a) / 10 + 7, 9.2), + (None, lambda a: float(a) / 10 + 7, None), + ], +) +def test_nullable_cast(value, cast, expect): + assert conversions.nullable_cast(value, cast) == expect + + +@pytest.mark.parametrize( + ["value", "cast", "default", "expect"], + [ + ("hello", int, "dead", "dead"), + ("22", int, "dead", 22), + ("22", lambda n: n + 4, ..., ...), + (22, lambda n: n + 4, ..., 26), + ], +) +def test_try_cast(value, cast, default, expect): + assert conversions.try_cast(value, cast, default) == expect + + +def test_put_if_specified_when_specified(): + d = {} + conversions.put_if_specified(d, "foo", 69) + conversions.put_if_specified(d, "bar", "hi") + conversions.put_if_specified(d, "bar", None) + assert d == {"foo": 69, "bar": None} + + +def test_put_if_specified_when_unspecified(): + d = {} + conversions.put_if_specified(d, "bar", ...) + assert d == {} + + +def test_put_if_specified_when_type_after_passed(): + d = {} + conversions.put_if_specified(d, "foo", 69, str) + conversions.put_if_specified(d, "bar", "69", int) + assert d == {"foo": "69", "bar": 69} + + +@pytest.mark.parametrize( + ["img_bytes", "expect"], + [ + (b"\211PNG\r\n\032\n", "data:image/png;base64,iVBORw0KGgo="), + (b" Exif", "data:image/jpeg;base64,ICAgICAgRXhpZg=="), + (b" JFIF", "data:image/jpeg;base64,ICAgICAgSkZJRg=="), + (b"GIF87a", "data:image/gif;base64,R0lGODdh"), + (b"GIF89a", "data:image/gif;base64,R0lGODlh"), + (b"RIFF WEBP", "data:image/webp;base64,UklGRiAgICBXRUJQ"), + ], +) +def test_image_bytes_to_image_data_img_types(img_bytes, expect): + assert conversions.image_bytes_to_image_data(img_bytes) == expect + + +def test_image_bytes_to_image_data_when_None_returns_None(): + assert conversions.image_bytes_to_image_data(None) is None + + +def test_image_bytes_to_image_data_when_unsupported_image_type_raises_value_error(): + try: + conversions.image_bytes_to_image_data(b"") + assert False + except ValueError: + assert True + + +def test_parse_iso_8601_date_with_negative_timezone(): + string = "2019-10-10T05:22:33.023456-02:30" + date = conversions.parse_iso_8601_ts(string) + assert date.year == 2019 + assert date.month == 10 + assert date.day == 10 + assert date.hour == 5 + assert date.minute == 22 + assert date.second == 33 + assert date.microsecond == 23456 + offset = date.tzinfo.utcoffset(None) + assert offset == datetime.timedelta(hours=-2, minutes=-30) + + +def test_parse_iso_8601_date_with_positive_timezone(): + string = "2019-10-10T05:22:33.023456+02:30" + date = conversions.parse_iso_8601_ts(string) + assert date.year == 2019 + assert date.month == 10 + assert date.day == 10 + assert date.hour == 5 + assert date.minute == 22 + assert date.second == 33 + assert date.microsecond == 23456 + offset = date.tzinfo.utcoffset(None) + assert offset == datetime.timedelta(hours=2, minutes=30) + + +def test_parse_iso_8601_date_with_zulu(): + string = "2019-10-10T05:22:33.023456Z" + date = conversions.parse_iso_8601_ts(string) + assert date.year == 2019 + assert date.month == 10 + assert date.day == 10 + assert date.hour == 5 + assert date.minute == 22 + assert date.second == 33 + assert date.microsecond == 23456 + offset = date.tzinfo.utcoffset(None) + assert offset == datetime.timedelta(seconds=0) + + +def test_parse_iso_8601_date_with_milliseconds_instead_of_microseconds(): + string = "2019-10-10T05:22:33.023Z" + date = conversions.parse_iso_8601_ts(string) + assert date.year == 2019 + assert date.month == 10 + assert date.day == 10 + assert date.hour == 5 + assert date.minute == 22 + assert date.second == 33 + assert date.microsecond == 23000 + + +def test_parse_iso_8601_date_with_no_fraction(): + string = "2019-10-10T05:22:33Z" + date = conversions.parse_iso_8601_ts(string) + assert date.year == 2019 + assert date.month == 10 + assert date.day == 10 + assert date.hour == 5 + assert date.minute == 22 + assert date.second == 33 + assert date.microsecond == 0 + + +def test_parse_http_date(): + rfc_timestamp = "Mon, 03 Jun 2019 17:54:26 GMT" + expected_timestamp = datetime.datetime(2019, 6, 3, 17, 54, 26, tzinfo=datetime.timezone.utc) + assert conversions.parse_http_date(rfc_timestamp) == expected_timestamp + + +def test_parse_discord_epoch_to_datetime(): + discord_timestamp = 37921278956 + expected_timestamp = datetime.datetime(2016, 3, 14, 21, 41, 18, 956000, tzinfo=datetime.timezone.utc) + assert conversions.discord_epoch_to_datetime(discord_timestamp) == expected_timestamp + + +def test_parse_unix_epoch_to_datetime(): + unix_timestamp = 1457991678956 + expected_timestamp = datetime.datetime(2016, 3, 14, 21, 41, 18, 956000, tzinfo=datetime.timezone.utc) + assert conversions.unix_epoch_to_ts(unix_timestamp) == expected_timestamp + + +@pytest.mark.parametrize( + ["input_arg", "expected_result_type"], + [ + ("hello", io.StringIO), + (b"hello", io.BytesIO), + (bytearray("hello", "utf-8"), io.BytesIO), + (memoryview(b"hello"), io.BytesIO), + ], +) +def test_make_resource_seekable(input_arg, expected_result_type): + assert isinstance(conversions.make_resource_seekable(input_arg), expected_result_type) diff --git a/tests/hikari/internal_utilities/test_marshaller.py b/tests/hikari/_internal/test_marshaller.py similarity index 99% rename from tests/hikari/internal_utilities/test_marshaller.py rename to tests/hikari/_internal/test_marshaller.py index bc487cec05..85ddc7d925 100644 --- a/tests/hikari/internal_utilities/test_marshaller.py +++ b/tests/hikari/_internal/test_marshaller.py @@ -19,7 +19,7 @@ import cymock as mock import pytest -from hikari.internal_utilities import marshaller +from hikari._internal import marshaller from tests.hikari import _helpers diff --git a/tests/hikari/internal_utilities/test_marshaller_pep563.py b/tests/hikari/_internal/test_marshaller_pep563.py similarity index 99% rename from tests/hikari/internal_utilities/test_marshaller_pep563.py rename to tests/hikari/_internal/test_marshaller_pep563.py index 8b3a1a187a..76df58c845 100644 --- a/tests/hikari/internal_utilities/test_marshaller_pep563.py +++ b/tests/hikari/_internal/test_marshaller_pep563.py @@ -26,7 +26,7 @@ import cymock as mock import pytest -from hikari.internal_utilities import marshaller +from hikari._internal import marshaller from tests.hikari import _helpers diff --git a/tests/hikari/internal_utilities/test_singleton_meta.py b/tests/hikari/_internal/test_meta.py similarity index 88% rename from tests/hikari/internal_utilities/test_singleton_meta.py rename to tests/hikari/_internal/test_meta.py index 6bfd9b3367..0f227ff489 100644 --- a/tests/hikari/internal_utilities/test_singleton_meta.py +++ b/tests/hikari/_internal/test_meta.py @@ -16,11 +16,11 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . -from hikari.internal_utilities import singleton_meta +from hikari._internal import meta def test_SingletonMeta(): - class Test(metaclass=singleton_meta.SingletonMeta): + class Test(metaclass=meta.SingletonMeta): pass assert Test() is Test() diff --git a/tests/hikari/internal_utilities/test_aio.py b/tests/hikari/_internal/test_more_asyncio.py similarity index 88% rename from tests/hikari/internal_utilities/test_aio.py rename to tests/hikari/_internal/test_more_asyncio.py index 00bce93729..1036314faf 100644 --- a/tests/hikari/internal_utilities/test_aio.py +++ b/tests/hikari/_internal/test_more_asyncio.py @@ -20,7 +20,7 @@ import pytest -from hikari.internal_utilities import aio +from hikari._internal import more_asyncio class CoroutineStub: @@ -60,18 +60,18 @@ class TestCompletedFuture: @pytest.mark.asyncio @pytest.mark.parametrize("args", [(), (12,)]) async def test_is_awaitable(self, args): - await aio.completed_future(*args) + await more_asyncio.completed_future(*args) @pytest.mark.asyncio @pytest.mark.parametrize("args", [(), (12,)]) async def test_is_completed(self, args): - future = aio.completed_future(*args) + future = more_asyncio.completed_future(*args) assert future.done() @pytest.mark.asyncio async def test_default_result_is_none(self): - assert aio.completed_future().result() is None + assert more_asyncio.completed_future().result() is None @pytest.mark.asyncio async def test_non_default_result(self): - assert aio.completed_future(...).result() is ... + assert more_asyncio.completed_future(...).result() is ... diff --git a/tests/hikari/internal_utilities/test_storage.py b/tests/hikari/_internal/test_more_collections.py similarity index 53% rename from tests/hikari/internal_utilities/test_storage.py rename to tests/hikari/_internal/test_more_collections.py index 2ec3db3113..3346bc4677 100644 --- a/tests/hikari/internal_utilities/test_storage.py +++ b/tests/hikari/_internal/test_more_collections.py @@ -16,21 +16,33 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . -import io -import pytest +from hikari._internal import more_collections -from hikari.internal_utilities import storage +class TestWeakKeyDictionary: + def test_is_weak(self): + class Key: + pass -@pytest.mark.parametrize( - ["input", "expected_result_type"], - [ - ("hello", io.StringIO), - (b"hello", io.BytesIO), - (bytearray("hello", "utf-8"), io.BytesIO), - (memoryview(b"hello"), io.BytesIO), - ], -) -def test_make_resource_seekable(input, expected_result_type): - assert isinstance(storage.make_resource_seekable(input), expected_result_type) + class Value: + pass + + d: more_collections.WeakKeyDictionary[Key, Value] = more_collections.WeakKeyDictionary() + + key1 = Key() + key2 = Key() + value1 = Value() + value2 = Value() + + d[key1] = value1 + d[key2] = value2 + + assert key1 in d + assert key2 in d + assert value1 in d.values() + assert value2 in d.values() + del key2 + assert len([*d.keys()]) == 1 + assert value1 in d.values() + assert value2 not in d.values() diff --git a/tests/hikari/internal_utilities/test_loggers.py b/tests/hikari/_internal/test_more_logging.py similarity index 78% rename from tests/hikari/internal_utilities/test_loggers.py rename to tests/hikari/_internal/test_more_logging.py index 01d41cbe41..3b5659519c 100644 --- a/tests/hikari/internal_utilities/test_loggers.py +++ b/tests/hikari/_internal/test_more_logging.py @@ -17,7 +17,7 @@ # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . -from hikari.internal_utilities import loggers +from hikari._internal import more_logging package_name = __name__ @@ -28,30 +28,30 @@ class NestedDummy: def test_get_named_logger_with_global_class(): - logger = loggers.get_named_logger(Dummy) + logger = more_logging.get_named_logger(Dummy) assert logger.name == package_name + ".Dummy" def test_get_named_logger_with_nested_class(): - logger = loggers.get_named_logger(Dummy.NestedDummy) + logger = more_logging.get_named_logger(Dummy.NestedDummy) assert logger.name == package_name + ".Dummy.NestedDummy" def test_get_named_logger_with_global_class_instance(): - logger = loggers.get_named_logger(Dummy()) + logger = more_logging.get_named_logger(Dummy()) assert logger.name == package_name + ".Dummy" def test_get_named_logger_with_nested_class_instance(): - logger = loggers.get_named_logger(Dummy.NestedDummy()) + logger = more_logging.get_named_logger(Dummy.NestedDummy()) assert logger.name == package_name + ".Dummy.NestedDummy" def test_get_named_logger_with_string(): - logger = loggers.get_named_logger("potato") + logger = more_logging.get_named_logger("potato") assert logger.name == "potato" def test_get_named_logger_with_extras(): - logger = loggers.get_named_logger("potato", "foo", "bar", "baz") + logger = more_logging.get_named_logger("potato", "foo", "bar", "baz") assert logger.name == "potato[foo, bar, baz]" diff --git a/tests/hikari/core/test_embeds.py b/tests/hikari/core/test_embeds.py index 8e6743a475..d8015e2756 100644 --- a/tests/hikari/core/test_embeds.py +++ b/tests/hikari/core/test_embeds.py @@ -21,9 +21,9 @@ import cymock as mock import pytest +import hikari._internal.conversions from hikari.core import colors from hikari.core import embeds -from hikari.internal_utilities import dates from tests.hikari import _helpers @@ -227,7 +227,10 @@ def test_deserialize( mock_datetime = mock.MagicMock(datetime.datetime) with _helpers.patch_marshal_attr( - embeds.Embed, "timestamp", deserializer=dates.parse_iso_8601_ts, return_value=mock_datetime + embeds.Embed, + "timestamp", + deserializer=hikari._internal.conversions.parse_iso_8601_ts, + return_value=mock_datetime, ) as patched_timestamp_deserializer: embed_obj = embeds.Embed.deserialize(test_embed_payload) patched_timestamp_deserializer.assert_called_once_with("2020-03-22T16:40:39.218000+00:00") diff --git a/tests/hikari/core/test_events.py b/tests/hikari/core/test_events.py index 8c4d5d5a96..18883af69d 100644 --- a/tests/hikari/core/test_events.py +++ b/tests/hikari/core/test_events.py @@ -21,6 +21,7 @@ import cymock as mock import pytest +import hikari._internal.conversions from hikari.core import channels from hikari.core import embeds from hikari.core import emojis @@ -31,8 +32,6 @@ from hikari.core import messages from hikari.core import oauth2 from hikari.core import users -from hikari.internal_utilities import dates - from tests.hikari import _helpers @@ -207,7 +206,7 @@ def test_deserialize(self, test_base_channel_payload, test_overwrite_payload, te with _helpers.patch_marshal_attr( events.BaseChannelEvent, "last_pin_timestamp", - deserializer=dates.parse_iso_8601_ts, + deserializer=hikari._internal.conversions.parse_iso_8601_ts, return_value=mock_timestamp, ) as patched_timestamp_deserializer: with mock.patch.object(users.User, "deserialize", return_value=mock_user): @@ -264,7 +263,7 @@ def test_deserialize(self, test_chanel_pin_update_payload): with _helpers.patch_marshal_attr( events.ChannelPinUpdateEvent, "last_pin_timestamp", - deserializer=dates.parse_iso_8601_ts, + deserializer=hikari._internal.conversions.parse_iso_8601_ts, return_value=mock_timestamp, ) as patched_iso_parser: channel_pin_add_obj = events.ChannelPinUpdateEvent.deserialize(test_chanel_pin_update_payload) @@ -385,7 +384,7 @@ def test_deserialize(self, guild_member_update_payload, test_user_payload): with _helpers.patch_marshal_attr( events.GuildMemberUpdateEvent, "premium_since", - deserializer=dates.parse_iso_8601_ts, + deserializer=hikari._internal.conversions.parse_iso_8601_ts, return_value=mock_premium_since, ) as patched_premium_since_deserializer: guild_member_update_obj = events.GuildMemberUpdateEvent.deserialize(guild_member_update_payload) @@ -480,7 +479,7 @@ def test_deserialize(self, test_invite_create_payload, test_user_payload): with _helpers.patch_marshal_attr( events.InviteCreateEvent, "created_at", - deserializer=dates.parse_iso_8601_ts, + deserializer=hikari._internal.conversions.parse_iso_8601_ts, return_value=mock_created_at, ) as patched_created_at_deserializer: invite_create_obj = events.InviteCreateEvent.deserialize(test_invite_create_payload) @@ -630,13 +629,13 @@ def test_deserialize( with _helpers.patch_marshal_attr( events.MessageUpdateEvent, "timestamp", - deserializer=dates.parse_iso_8601_ts, + deserializer=hikari._internal.conversions.parse_iso_8601_ts, return_value=mock_timestamp, ) as patched_timestamp_deserializer: with _helpers.patch_marshal_attr( events.MessageUpdateEvent, "edited_timestamp", - deserializer=dates.parse_iso_8601_ts, + deserializer=hikari._internal.conversions.parse_iso_8601_ts, return_value=mock_edited_timestamp, ) as patched_edit_deserializer: with _helpers.patch_marshal_attr( diff --git a/tests/hikari/core/test_guilds.py b/tests/hikari/core/test_guilds.py index fb6e0546ef..5bec5c4472 100644 --- a/tests/hikari/core/test_guilds.py +++ b/tests/hikari/core/test_guilds.py @@ -21,13 +21,13 @@ import cymock as mock import pytest +import hikari._internal.conversions +from hikari._internal import cdn from hikari.core import emojis from hikari.core import entities from hikari.core import guilds from hikari.core import users from hikari.core import channels -from hikari.internal_utilities import cdn -from hikari.internal_utilities import dates from tests.hikari import _helpers @@ -257,19 +257,22 @@ def test_guild_payload( class TestGuildMember: def test_deserialize(self, test_member_payload, test_user_payload): - mock_user = mock.MagicMock(users.User) - mock_datetime_1 = mock.MagicMock(dates) - mock_datetime_2 = mock.MagicMock(dates) + mock_user = mock.create_autospec(users.User) + mock_datetime_1 = mock.create_autospec(datetime.datetime) + mock_datetime_2 = mock.create_autospec(datetime.datetime) with _helpers.patch_marshal_attr( guilds.GuildMember, "user", deserializer=users.User.deserialize, return_value=mock_user ) as patched_user_deserializer: with _helpers.patch_marshal_attr( - guilds.GuildMember, "joined_at", deserializer=dates.parse_iso_8601_ts, return_value=mock_datetime_1 + guilds.GuildMember, + "joined_at", + deserializer=hikari._internal.conversions.parse_iso_8601_ts, + return_value=mock_datetime_1, ) as patched_joined_at_deserializer: with _helpers.patch_marshal_attr( guilds.GuildMember, "premium_since", - deserializer=dates.parse_iso_8601_ts, + deserializer=hikari._internal.conversions.parse_iso_8601_ts, return_value=mock_datetime_2, ) as patched_premium_since_deserializer: guild_member_obj = guilds.GuildMember.deserialize(test_member_payload) @@ -312,13 +315,19 @@ def test_deserialize(self, test_guild_role_payload): class TestActivityTimestamps: def test_deserialize(self, test_activity_timestamps_payload): - mock_start_date = mock.MagicMock(datetime.datetime) - mock_end_date = mock.MagicMock(datetime.datetime) + mock_start_date = mock.create_autospec(datetime.datetime) + mock_end_date = mock.create_autospec(datetime.datetime) with _helpers.patch_marshal_attr( - guilds.ActivityTimestamps, "start", deserializer=dates.unix_epoch_to_ts, return_value=mock_start_date + guilds.ActivityTimestamps, + "start", + deserializer=hikari._internal.conversions.unix_epoch_to_ts, + return_value=mock_start_date, ) as patched_start_deserializer: with _helpers.patch_marshal_attr( - guilds.ActivityTimestamps, "end", deserializer=dates.unix_epoch_to_ts, return_value=mock_end_date + guilds.ActivityTimestamps, + "end", + deserializer=hikari._internal.conversions.unix_epoch_to_ts, + return_value=mock_end_date, ) as patched_end_deserializer: activity_timestamps_obj = guilds.ActivityTimestamps.deserialize(test_activity_timestamps_payload) patched_end_deserializer.assert_called_once_with(1999999792798) @@ -378,10 +387,13 @@ def test_deserialize( test_emoji_payload, test_activity_timestamps_payload, ): - mock_created_at = mock.MagicMock(datetime.datetime) - mock_emoji = mock.MagicMock(emojis.UnknownEmoji) + mock_created_at = mock.create_autospec(datetime.datetime) + mock_emoji = mock.create_autospec(emojis.UnknownEmoji) with _helpers.patch_marshal_attr( - guilds.PresenceActivity, "created_at", deserializer=dates.unix_epoch_to_ts, return_value=mock_created_at + guilds.PresenceActivity, + "created_at", + deserializer=hikari._internal.conversions.unix_epoch_to_ts, + return_value=mock_created_at, ) as patched_created_at_deserializer: with _helpers.patch_marshal_attr( guilds.PresenceActivity, @@ -457,9 +469,12 @@ class TestGuildMemberPresence: def test_deserialize( self, test_guild_member_presence, test_user_payload, test_presence_activity_payload, test_client_status_payload ): - mock_since = mock.MagicMock(datetime.datetime) + mock_since = mock.create_autospec(datetime.datetime) with _helpers.patch_marshal_attr( - guilds.GuildMemberPresence, "premium_since", deserializer=dates.parse_iso_8601_ts, return_value=mock_since, + guilds.GuildMemberPresence, + "premium_since", + deserializer=hikari._internal.conversions.parse_iso_8601_ts, + return_value=mock_since, ) as patched_since_deserializer: guild_member_presence_obj = guilds.GuildMemberPresence.deserialize(test_guild_member_presence) patched_since_deserializer.assert_called_once_with("2015-04-26T06:26:56.936000+00:00") @@ -481,7 +496,7 @@ def test_guild_member_ban_payload(self, test_user_payload): return {"reason": "Get Nyaa'ed", "user": test_user_payload} def test_deserializer(self, test_guild_member_ban_payload, test_user_payload): - mock_user = mock.MagicMock(users.User) + mock_user = mock.create_autospec(users.User) with _helpers.patch_marshal_attr( guilds.GuildMemberBan, "user", deserializer=users.User.deserialize, return_value=mock_user ) as patched_user_deserializer: @@ -541,12 +556,12 @@ def test_guild_integration_payload(self, test_user_payload, test_partial_guild_i } def test_deserialize(self, test_guild_integration_payload, test_user_payload, test_integration_account_payload): - mock_user = mock.MagicMock(users.User) - mock_sync_date = mock.MagicMock(datetime.datetime) + mock_user = mock.create_autospec(users.User) + mock_sync_date = mock.create_autospec(datetime.datetime) with _helpers.patch_marshal_attr( guilds.GuildIntegration, "last_synced_at", - deserializer=dates.parse_iso_8601_ts, + deserializer=hikari._internal.conversions.parse_iso_8601_ts, return_value=mock_sync_date, ) as patched_sync_at_deserializer: with _helpers.patch_marshal_attr( @@ -641,9 +656,9 @@ def test_deserialize( test_channel_payload, test_guild_member_presence, ): - mock_emoji = mock.MagicMock(emojis.GuildEmoji, id=42) - mock_user = mock.MagicMock(users.User, id=84) - mock_guild_channel = mock.MagicMock(channels.GuildChannel, id=6969) + mock_emoji = mock.create_autospec(emojis.GuildEmoji, id=42) + mock_user = mock.create_autospec(users.User, id=84) + mock_guild_channel = mock.create_autospec(channels.GuildChannel, id=6969) with mock.patch.object(emojis.GuildEmoji, "deserialize", return_value=mock_emoji): with _helpers.patch_marshal_attr( guilds.GuildMemberPresence, "user", deserializer=guilds.PresenceUser.deserialize, return_value=mock_user @@ -685,7 +700,7 @@ def test_deserialize( | guilds.GuildSystemChannelFlag.SUPPRESS_USER_JOIN ) assert guild_obj.rules_channel_id == 42042069 - assert guild_obj.joined_at == dates.parse_iso_8601_ts("2019-05-17T06:26:56.936000+00:00") + assert guild_obj.joined_at == hikari._internal.conversions.parse_iso_8601_ts("2019-05-17T06:26:56.936000+00:00") assert guild_obj.is_large is False assert guild_obj.member_count == 14 assert guild_obj.channels == {6969: mock_guild_channel} diff --git a/tests/hikari/core/test_invites.py b/tests/hikari/core/test_invites.py index e4428dc862..60ebb56085 100644 --- a/tests/hikari/core/test_invites.py +++ b/tests/hikari/core/test_invites.py @@ -21,12 +21,12 @@ import cymock as mock import pytest +import hikari._internal.conversions +from hikari._internal import cdn from hikari.core import channels from hikari.core import guilds from hikari.core import invites from hikari.core import users -from hikari.internal_utilities import cdn -from hikari.internal_utilities import dates from tests.hikari import _helpers @@ -216,7 +216,10 @@ def mock_invite_with_metadata(self, *args, test_invite_with_metadata_payload): def test_deserialize(self, *deserializers, test_invite_with_metadata_payload): mock_datetime = mock.MagicMock(datetime.datetime) with _helpers.patch_marshal_attr( - invites.InviteWithMetadata, "created_at", deserializer=dates.parse_iso_8601_ts, return_value=mock_datetime + invites.InviteWithMetadata, + "created_at", + deserializer=hikari._internal.conversions.parse_iso_8601_ts, + return_value=mock_datetime, ) as mock_created_at_deserializer: invite_with_metadata_obj = invites.InviteWithMetadata.deserialize(test_invite_with_metadata_payload) mock_created_at_deserializer.assert_called_once_with("2015-04-26T06:26:56.936000+00:00") diff --git a/tests/hikari/core/test_messages.py b/tests/hikari/core/test_messages.py index cd8f5739c9..817c276cc8 100644 --- a/tests/hikari/core/test_messages.py +++ b/tests/hikari/core/test_messages.py @@ -21,13 +21,13 @@ import cymock as mock import pytest +import hikari._internal.conversions from hikari.core import embeds from hikari.core import emojis from hikari.core import guilds from hikari.core import messages from hikari.core import oauth2 from hikari.core import users -from hikari.internal_utilities import dates from tests.hikari import _helpers @@ -195,12 +195,15 @@ def test_deserialize( messages.Message, "member", deserializer=guilds.GuildMember.deserialize, return_value=mock_member ) as patched_member_deserializer: with _helpers.patch_marshal_attr( - messages.Message, "timestamp", deserializer=dates.parse_iso_8601_ts, return_value=mock_datetime + messages.Message, + "timestamp", + deserializer=hikari._internal.conversions.parse_iso_8601_ts, + return_value=mock_datetime, ) as patched_timestamp_deserializer: with _helpers.patch_marshal_attr( messages.Message, "edited_timestamp", - deserializer=dates.parse_iso_8601_ts, + deserializer=hikari._internal.conversions.parse_iso_8601_ts, return_value=mock_datetime2, ) as patched_edited_timestamp_deserializer: with _helpers.patch_marshal_attr( diff --git a/tests/hikari/core/test_oauth2.py b/tests/hikari/core/test_oauth2.py index a9faa17fbe..cb3817dfc7 100644 --- a/tests/hikari/core/test_oauth2.py +++ b/tests/hikari/core/test_oauth2.py @@ -19,9 +19,9 @@ import cymock as mock import pytest +from hikari._internal import cdn from hikari.core import oauth2 from hikari.core import users -from hikari.internal_utilities import cdn from tests.hikari import _helpers diff --git a/tests/hikari/core/test_users.py b/tests/hikari/core/test_users.py index 78bf79cade..83a8dde081 100644 --- a/tests/hikari/core/test_users.py +++ b/tests/hikari/core/test_users.py @@ -19,8 +19,8 @@ import cymock as mock import pytest +from hikari._internal import cdn from hikari.core import users -from hikari.internal_utilities import cdn @pytest.fixture() diff --git a/tests/hikari/internal_utilities/test_cache.py b/tests/hikari/internal_utilities/test_cache.py deleted file mode 100644 index 79130f4954..0000000000 --- a/tests/hikari/internal_utilities/test_cache.py +++ /dev/null @@ -1,363 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# Copyright © Nekoka.tt 2019-2020 -# -# This file is part of Hikari. -# -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . -import asyncio - -import cymock as mock -import pytest - -from hikari.internal_utilities import cache - - -def test_init_CachedFunction_sets_value_to_sentinel(): - def call(): - pass - - cached_call = cache.CachedFunction(call, [], {}) - assert cached_call._value is cached_call._sentinel - - -def test_call_CachedFunction_first_time_sets_value(): - call = mock.MagicMock(return_value=27) - - cached_call = cache.CachedFunction(call, [9], {"k": 18}) - - cached_call() - - call.assert_called_with(9, k=18) - - assert cached_call._value == 27 - - -def test_call_CachedFunction_first_time_returns_value(): - call = mock.MagicMock(return_value=27) - - cached_call = cache.CachedFunction(call, [9], {"k": 18}) - - assert cached_call() == 27 - - -def test_call_CachedFunction_second_time_does_not_reset_value(): - call = mock.MagicMock(return_value=27) - cached_call = cache.CachedFunction(call, [9], {"k": 18}) - - cached_call() - sentinel = object() - cached_call._value = sentinel - cached_call() - call.assert_called_once() - - -def test_call_CachedFunction_second_time_returns_value(): - call = mock.MagicMock(return_value=27) - cached_call = cache.CachedFunction(call, [9], {"k": 18}) - - cached_call() - sentinel = object() - cached_call._value = sentinel - assert cached_call() is sentinel - - -@pytest.mark.asyncio -async def test_async_init_CachedFunction_sets_value_for_sentinel(): - async def call(): - pass - - cached_call = cache.CachedFunction(call, [], {}) - assert cached_call._value is cached_call._sentinel - - -@pytest.mark.asyncio -async def test_async_call_CachedFunction_first_time_sets_value(): - async def call(*args, **kwargs): - assert len(args) == 1 - assert args[0] == 9 - assert len(kwargs) == 1 - assert kwargs["k"] == 18 - return 27 - - cached_call = cache.CachedFunction(call, [9], {"k": 18}) - - await cached_call() - - assert await cached_call._value == 27 - - -@pytest.mark.asyncio -async def test_async_call_CachedFunction_first_time_returns_value(): - call = mock.AsyncMock(return_value=27) - - cached_call = cache.CachedFunction(call, [9], {"k": 18}) - - assert await cached_call() == 27 - - -@pytest.mark.asyncio -async def test_async_call_CachedFunction_second_time_does_not_reset_value(): - call = mock.AsyncMock(return_value=27) - cached_call = cache.CachedFunction(call, [9], {"k": 18}) - - async def sentinel_test_value(): - return 22 - - await cached_call() - cached_call._value = asyncio.create_task(sentinel_test_value()) - await cached_call() - call.assert_called_once() - - -@pytest.mark.asyncio -async def test_async_call_CachedFunction_second_time_returns_value(): - call = mock.AsyncMock(return_value=27) - cached_call = cache.CachedFunction(call, [9], {"k": 18}) - - await cached_call() - - async def sentinel_test_value(): - return 22 - - cached_call._value = asyncio.create_task(sentinel_test_value()) - assert await cached_call() == 22 - - -def test_cached_function(): - spy = mock.MagicMock() - sentinel = object() - - @cache.cached_function(9, 18, 27, name="nekokatt") - def test(a, b, c, *, name): - spy(a, b, c, name=name) - return sentinel - - assert test() is sentinel - assert test() is sentinel - spy.assert_called_once_with(9, 18, 27, name="nekokatt") - - -@pytest.mark.asyncio -async def test_cached_function_coro(): - spy = mock.MagicMock() - sentinel = object() - - @cache.cached_function(9, 18, 27, name="nekokatt") - async def test(a, b, c, *, name): - spy(a, b, c, name=name) - return sentinel - - assert await test() is sentinel - assert await test() is sentinel - spy.assert_called_once_with(9, 18, 27, name="nekokatt") - - -def test_CachedFunction___qualname__(): - def potato(): - pass - - cached_call = cache.CachedFunction(potato, [], {}) - - assert cached_call.__qualname__ == "test_CachedFunction___qualname__..potato" - - -@pytest.fixture -def cached_property_usage(): - class CachedPropertyUsage: - call_count = 0 - - @cache.cached_property() - def function(self): - self.call_count += 1 - return self.call_count - - return CachedPropertyUsage() - - -@pytest.fixture -def async_cached_property_usage(): - class CachedPropertyUsage: - call_count = 0 - - @cache.cached_property() - async def function(self): - self.call_count += 1 - return self.call_count - - return CachedPropertyUsage() - - -def test_cached_property_makes_property_that_caches_result(cached_property_usage): - assert cached_property_usage.function == 1 - assert cached_property_usage.function == 1 - assert cached_property_usage.function == 1 - assert cached_property_usage.call_count == 1 - cached_property_usage.call_count = 2 - assert cached_property_usage.function == 1 - - -def test_cached_property_makes_property_that_can_have_cache_cleared(cached_property_usage): - _ = cached_property_usage.function - del cached_property_usage.function - assert cached_property_usage.function == 2 - assert cached_property_usage.function == 2 - del cached_property_usage.function - del cached_property_usage.function - assert cached_property_usage.function == 3 - - -@pytest.mark.asyncio -async def test_async_cached_property_makes_property_that_caches_result(async_cached_property_usage): - assert await async_cached_property_usage.function == 1 - assert await async_cached_property_usage.function == 1 - assert await async_cached_property_usage.function == 1 - assert async_cached_property_usage.call_count == 1 - async_cached_property_usage.call_count = 2 - assert await async_cached_property_usage.function == 1 - - -@pytest.mark.asyncio -async def test_async_cached_property_reuses_future(async_cached_property_usage): - f1 = async_cached_property_usage.function - assert isinstance(f1, asyncio.Future) - f2 = async_cached_property_usage.function - assert f1 is f2 - await f1 - f3 = async_cached_property_usage.function - assert f3 is f1 - assert await f1 == await f3 - - -@pytest.mark.asyncio -async def test_async_cached_property_makes_property_that_can_have_cache_cleared(async_cached_property_usage): - _ = await async_cached_property_usage.function - del async_cached_property_usage.function - assert await async_cached_property_usage.function == 2 - assert await async_cached_property_usage.function == 2 - del async_cached_property_usage.function - del async_cached_property_usage.function - assert await async_cached_property_usage.function == 3 - - -def test_cached_property_on_class_returns_self(): - class Class: - @cache.cached_property() - def foo(self): - return 123 - - # noinspection PyTypeHints - assert isinstance(Class.foo, cache.CachedProperty) - - -def test_async_cached_property_on_class_returns_self(): - class Class: - @cache.cached_property() - async def foo(self): - return 123 - - # noinspection PyTypeHints - assert isinstance(Class.foo, cache.CachedProperty) - - -def test_cached_property_works_on_slots_for_call(): - value = 0 - - class Slotted: - __slots__ = ("_cp_foo",) - - @cache.cached_property() - def foo(self): - nonlocal value - value += 1 - return value - - s = Slotted() - assert value == 0 - assert s.foo == 1 - assert value == 1 - assert s.foo == 1 - assert s.foo == 1 - assert value == 1 - - -@pytest.mark.asyncio -async def test_async_cached_property_works_on_slots_for_call(): - value = 0 - - class Slotted: - __slots__ = ("_cp_foo",) - - @cache.cached_property() - async def foo(self): - nonlocal value - value += 1 - return value - - s = Slotted() - assert value == 0 - assert await s.foo == 1 - assert value == 1 - assert await s.foo == 1 - assert await s.foo == 1 - assert value == 1 - - -def test_cached_property_works_on_slots_for_del(): - value = 0 - - class Slotted: - __slots__ = ("_cp_foo",) - - @cache.cached_property() - def foo(self): - nonlocal value - value += 1 - return value - - s = Slotted() - _ = s.foo - del s.foo - assert s.foo == 2 - assert s.foo == 2 - assert value == 2 - del s.foo - del s.foo - assert s.foo == 3 - assert value == 3 - - -@pytest.mark.asyncio -async def test_async_cached_property_works_on_slots_for_del(): - value = 0 - - class Slotted: - __slots__ = ("_cp_foo",) - - @cache.cached_property() - async def foo(self): - nonlocal value - value += 1 - return value - - s = Slotted() - _ = await s.foo - del s.foo - assert await s.foo == 2 - assert await s.foo == 2 - assert value == 2 - del s.foo - del s.foo - assert await s.foo == 3 - assert value == 3 diff --git a/tests/hikari/internal_utilities/test_dates.py b/tests/hikari/internal_utilities/test_dates.py deleted file mode 100644 index fb0e68b32a..0000000000 --- a/tests/hikari/internal_utilities/test_dates.py +++ /dev/null @@ -1,105 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# Copyright © Nekoka.tt 2019-2020 -# -# This file is part of Hikari. -# -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . -import datetime - -from hikari.internal_utilities import dates - - -def test_parse_iso_8601_date_with_negative_timezone(): - string = "2019-10-10T05:22:33.023456-02:30" - date = dates.parse_iso_8601_ts(string) - assert date.year == 2019 - assert date.month == 10 - assert date.day == 10 - assert date.hour == 5 - assert date.minute == 22 - assert date.second == 33 - assert date.microsecond == 23456 - offset = date.tzinfo.utcoffset(None) - assert offset == datetime.timedelta(hours=-2, minutes=-30) - - -def test_parse_iso_8601_date_with_positive_timezone(): - string = "2019-10-10T05:22:33.023456+02:30" - date = dates.parse_iso_8601_ts(string) - assert date.year == 2019 - assert date.month == 10 - assert date.day == 10 - assert date.hour == 5 - assert date.minute == 22 - assert date.second == 33 - assert date.microsecond == 23456 - offset = date.tzinfo.utcoffset(None) - assert offset == datetime.timedelta(hours=2, minutes=30) - - -def test_parse_iso_8601_date_with_zulu(): - string = "2019-10-10T05:22:33.023456Z" - date = dates.parse_iso_8601_ts(string) - assert date.year == 2019 - assert date.month == 10 - assert date.day == 10 - assert date.hour == 5 - assert date.minute == 22 - assert date.second == 33 - assert date.microsecond == 23456 - offset = date.tzinfo.utcoffset(None) - assert offset == datetime.timedelta(seconds=0) - - -def test_parse_iso_8601_date_with_milliseconds_instead_of_microseconds(): - string = "2019-10-10T05:22:33.023Z" - date = dates.parse_iso_8601_ts(string) - assert date.year == 2019 - assert date.month == 10 - assert date.day == 10 - assert date.hour == 5 - assert date.minute == 22 - assert date.second == 33 - assert date.microsecond == 23000 - - -def test_parse_iso_8601_date_with_no_fraction(): - string = "2019-10-10T05:22:33Z" - date = dates.parse_iso_8601_ts(string) - assert date.year == 2019 - assert date.month == 10 - assert date.day == 10 - assert date.hour == 5 - assert date.minute == 22 - assert date.second == 33 - assert date.microsecond == 0 - - -def test_parse_http_date(): - rfc_timestamp = "Mon, 03 Jun 2019 17:54:26 GMT" - expected_timestamp = datetime.datetime(2019, 6, 3, 17, 54, 26, tzinfo=datetime.timezone.utc) - assert dates.parse_http_date(rfc_timestamp) == expected_timestamp - - -def test_parse_discord_epoch_to_datetime(): - discord_timestamp = 37921278956 - expected_timestamp = datetime.datetime(2016, 3, 14, 21, 41, 18, 956000, tzinfo=datetime.timezone.utc) - assert dates.discord_epoch_to_datetime(discord_timestamp) == expected_timestamp - - -def test_parse_unix_epoch_to_datetime(): - unix_timestamp = 1457991678956 - expected_timestamp = datetime.datetime(2016, 3, 14, 21, 41, 18, 956000, tzinfo=datetime.timezone.utc) - assert dates.unix_epoch_to_ts(unix_timestamp) == expected_timestamp diff --git a/tests/hikari/internal_utilities/test_transformations.py b/tests/hikari/internal_utilities/test_transformations.py deleted file mode 100644 index 60e7d6a4fa..0000000000 --- a/tests/hikari/internal_utilities/test_transformations.py +++ /dev/null @@ -1,96 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# Copyright © Nekoka.tt 2019-2020 -# -# This file is part of Hikari. -# -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . - -import pytest - -from hikari.internal_utilities import transformations - - -@pytest.mark.parametrize( - ["value", "cast", "expect"], - [ - ("22", int, 22), - (None, int, None), - ("22", lambda a: float(a) / 10 + 7, 9.2), - (None, lambda a: float(a) / 10 + 7, None), - ], -) -def test_nullable_cast(value, cast, expect): - assert transformations.nullable_cast(value, cast) == expect - - -@pytest.mark.parametrize( - ["value", "cast", "default", "expect"], - [ - ("hello", int, "dead", "dead"), - ("22", int, "dead", 22), - ("22", lambda n: n + 4, ..., ...), - (22, lambda n: n + 4, ..., 26), - ], -) -def test_try_cast(value, cast, default, expect): - assert transformations.try_cast(value, cast, default) == expect - - -def test_put_if_specified_when_specified(): - d = {} - transformations.put_if_specified(d, "foo", 69) - transformations.put_if_specified(d, "bar", "hi") - transformations.put_if_specified(d, "bar", None) - assert d == {"foo": 69, "bar": None} - - -def test_put_if_specified_when_unspecified(): - d = {} - transformations.put_if_specified(d, "bar", ...) - assert d == {} - - -def test_put_if_specified_when_type_after_passed(): - d = {} - transformations.put_if_specified(d, "foo", 69, str) - transformations.put_if_specified(d, "bar", "69", int) - assert d == {"foo": "69", "bar": 69} - - -@pytest.mark.parametrize( - ["img_bytes", "expect"], - [ - (b"\211PNG\r\n\032\n", "data:image/png;base64,iVBORw0KGgo="), - (b" Exif", "data:image/jpeg;base64,ICAgICAgRXhpZg=="), - (b" JFIF", "data:image/jpeg;base64,ICAgICAgSkZJRg=="), - (b"GIF87a", "data:image/gif;base64,R0lGODdh"), - (b"GIF89a", "data:image/gif;base64,R0lGODlh"), - (b"RIFF WEBP", "data:image/webp;base64,UklGRiAgICBXRUJQ"), - ], -) -def test_image_bytes_to_image_data_img_types(img_bytes, expect): - assert transformations.image_bytes_to_image_data(img_bytes) == expect - - -def test_image_bytes_to_image_data_when_None_returns_None(): - assert transformations.image_bytes_to_image_data(None) is None - - -def test_image_bytes_to_image_data_when_unsuported_image_type_raises_value_error(): - try: - transformations.image_bytes_to_image_data(b"") - assert False - except ValueError: - assert True diff --git a/tests/hikari/net/test_ratelimits.py b/tests/hikari/net/test_ratelimits.py index 9dd545c933..8661f39bea 100644 --- a/tests/hikari/net/test_ratelimits.py +++ b/tests/hikari/net/test_ratelimits.py @@ -34,7 +34,7 @@ class TestBaseRateLimiter: def test_context_management(self): - class MockedBaseRateLimiter(ratelimits.IRateLimiter): + class MockedBaseRateLimiter(ratelimits.BaseRateLimiter): close = mock.MagicMock() acquire = NotImplemented @@ -465,21 +465,6 @@ async def test_gc_calls_do_pass(self): finally: mgr.gc_task.cancel() - @pytest.mark.asyncio - async def test_gc_calls_do_pass_and_ignores_exception(self): - with _helpers.unslot_class(ratelimits.HTTPBucketRateLimiterManager)() as mgr: - mgr.do_gc_pass = mock.MagicMock(side_effect=RuntimeError) - mgr.start(0.01) - await asyncio.sleep(0.1) - mgr.do_gc_pass.assert_called() - try: - mgr.gc_task.exception() - assert False - except asyncio.InvalidStateError: - pass - finally: - mgr.gc_task.cancel() - @pytest.mark.asyncio async def test_do_gc_pass_any_buckets_that_are_empty_and_unknown_get_closed(self): with _helpers.unslot_class(ratelimits.HTTPBucketRateLimiterManager)() as mgr: diff --git a/tests/hikari/net/test_rest.py b/tests/hikari/net/test_rest.py index 72f4ff6e22..eeb1b4f866 100644 --- a/tests/hikari/net/test_rest.py +++ b/tests/hikari/net/test_rest.py @@ -28,8 +28,7 @@ import cymock as mock import pytest -from hikari.internal_utilities import storage -from hikari.internal_utilities import transformations +from hikari._internal import conversions from hikari.net import errors from hikari.net import ratelimits from hikari.net import rest @@ -699,7 +698,7 @@ async def test_create_message_without_optionals(self, rest_impl): @pytest.mark.asyncio @unittest.mock.patch.object(routes, "CHANNEL_MESSAGES") @unittest.mock.patch.object(aiohttp, "FormData", autospec=True) - @unittest.mock.patch.object(storage, "make_resource_seekable") + @unittest.mock.patch.object(conversions, "make_resource_seekable") @unittest.mock.patch.object(json, "dumps") async def test_create_message_with_optionals( self, dumps, make_resource_seekable, FormData, CHANNEL_MESSAGES, rest_impl @@ -1027,10 +1026,10 @@ async def test_create_guild_emoji_without_optionals(self, rest_impl): mock_route = mock.MagicMock(routes.GUILD_EMOJI) mock_image_data = "data:image/png;base64,iVBORw0KGgpibGFo" with mock.patch.object(routes, "GUILD_EMOJIS", compile=mock.MagicMock(return_value=mock_route)): - with mock.patch.object(transformations, "image_bytes_to_image_data", return_value=mock_image_data): + with mock.patch.object(conversions, "image_bytes_to_image_data", return_value=mock_image_data): result = await rest_impl.create_guild_emoji("2222", "iEmoji", b"\211PNG\r\n\032\nblah") assert result is mock_response - transformations.image_bytes_to_image_data.assert_called_once_with(b"\211PNG\r\n\032\nblah") + conversions.image_bytes_to_image_data.assert_called_once_with(b"\211PNG\r\n\032\nblah") routes.GUILD_EMOJIS.compile.assert_called_once_with(rest_impl.POST, guild_id="2222") rest_impl._request.assert_called_once_with( mock_route, json_body={"name": "iEmoji", "roles": [], "image": mock_image_data}, reason=..., @@ -1043,12 +1042,12 @@ async def test_create_guild_emoji_with_optionals(self, rest_impl): mock_route = mock.MagicMock(routes.GUILD_EMOJI) mock_image_data = "data:image/png;base64,iVBORw0KGgpibGFo" with mock.patch.object(routes, "GUILD_EMOJIS", compile=mock.MagicMock(return_value=mock_route)): - with mock.patch.object(transformations, "image_bytes_to_image_data", return_value=mock_image_data): + with mock.patch.object(conversions, "image_bytes_to_image_data", return_value=mock_image_data): result = await rest_impl.create_guild_emoji( "2222", "iEmoji", b"\211PNG\r\n\032\nblah", roles=["292929", "484884"], reason="uwu owo" ) assert result is mock_response - transformations.image_bytes_to_image_data.assert_called_once_with(b"\211PNG\r\n\032\nblah") + conversions.image_bytes_to_image_data.assert_called_once_with(b"\211PNG\r\n\032\nblah") routes.GUILD_EMOJIS.compile.assert_called_once_with(rest_impl.POST, guild_id="2222") rest_impl._request.assert_called_once_with( mock_route, @@ -1106,7 +1105,7 @@ async def test_create_guild_with_optionals(self, rest_impl): mock_route = mock.MagicMock(routes.GUILD) mock_image_data = "data:image/png;base64,iVBORw0KGgpibGFo" with mock.patch.object(routes, "GUILDS", compile=mock.MagicMock(return_value=mock_route)): - with mock.patch.object(transformations, "image_bytes_to_image_data", return_value=mock_image_data): + with mock.patch.object(conversions, "image_bytes_to_image_data", return_value=mock_image_data): result = await rest_impl.create_guild( "GUILD TIME", region="london", @@ -1118,7 +1117,7 @@ async def test_create_guild_with_optionals(self, rest_impl): ) assert result is mock_response routes.GUILDS.compile.assert_called_once_with(rest_impl.POST) - transformations.image_bytes_to_image_data.assert_called_once_with(b"\211PNG\r\n\032\nblah") + conversions.image_bytes_to_image_data.assert_called_once_with(b"\211PNG\r\n\032\nblah") rest_impl._request.assert_called_once_with( mock_route, json_body={ @@ -1161,7 +1160,7 @@ async def test_modify_guild_with_optionals(self, rest_impl): mock_splash_data = "data:image/png;base64,iVBORw0KGgpicnVo" with mock.patch.object(routes, "GUILD", compile=mock.MagicMock(return_value=mock_route)): with mock.patch.object( - transformations, "image_bytes_to_image_data", side_effect=(mock_icon_data, mock_splash_data) + conversions, "image_bytes_to_image_data", side_effect=(mock_icon_data, mock_splash_data) ): result = await rest_impl.modify_guild( "49949495", @@ -1181,8 +1180,8 @@ async def test_modify_guild_with_optionals(self, rest_impl): assert result is mock_response routes.GUILD.compile.assert_called_once_with(rest_impl.PATCH, guild_id="49949495") - assert transformations.image_bytes_to_image_data.call_count == 2 - transformations.image_bytes_to_image_data.assert_has_calls( + assert conversions.image_bytes_to_image_data.call_count == 2 + conversions.image_bytes_to_image_data.assert_has_calls( ( mock.call.__bool__(), mock.call(b"\211PNG\r\n\032\nblah"), @@ -1852,11 +1851,11 @@ async def test_modify_current_user_with_optionals(self, rest_impl): mock_route = mock.MagicMock(routes.OWN_USER) mock_image_data = "data:image/png;base64,iVBORw0KGgpibGFo" with mock.patch.object(routes, "OWN_USER", compile=mock.MagicMock(return_value=mock_route)): - with mock.patch.object(transformations, "image_bytes_to_image_data", return_value=mock_image_data): + with mock.patch.object(conversions, "image_bytes_to_image_data", return_value=mock_image_data): result = await rest_impl.modify_current_user(username="Watashi 2", avatar=b"\211PNG\r\n\032\nblah") assert result is mock_response routes.OWN_USER.compile.assert_called_once_with(rest_impl.PATCH) - transformations.image_bytes_to_image_data.assert_called_once_with(b"\211PNG\r\n\032\nblah") + conversions.image_bytes_to_image_data.assert_called_once_with(b"\211PNG\r\n\032\nblah") rest_impl._request.assert_called_once_with( mock_route, json_body={"username": "Watashi 2", "avatar": mock_image_data} ) @@ -1936,13 +1935,13 @@ async def test_create_webhook_with_optionals(self, rest_impl): mock_route = mock.MagicMock(routes.CHANNEL_WEBHOOKS) mock_image_data = "data:image/png;base64,iVBORw0KGgpibGFo" with mock.patch.object(routes, "CHANNEL_WEBHOOKS", compile=mock.MagicMock(return_value=mock_route)): - with mock.patch.object(transformations, "image_bytes_to_image_data", return_value=mock_image_data): + with mock.patch.object(conversions, "image_bytes_to_image_data", return_value=mock_image_data): result = await rest_impl.create_webhook( "39393939", "I am a webhook", avatar=b"\211PNG\r\n\032\nblah", reason="get reasoned" ) assert result is mock_response routes.CHANNEL_WEBHOOKS.compile.assert_called_once_with(rest_impl.POST, channel_id="39393939") - transformations.image_bytes_to_image_data.assert_called_once_with(b"\211PNG\r\n\032\nblah") + conversions.image_bytes_to_image_data.assert_called_once_with(b"\211PNG\r\n\032\nblah") rest_impl._request.assert_called_once_with( mock_route, json_body={"name": "I am a webhook", "avatar": mock_image_data}, reason="get reasoned", ) @@ -2077,7 +2076,7 @@ async def test_execute_webhook_without_optionals(self, rest_impl): @unittest.mock.patch.object(aiohttp, "FormData", autospec=True) @unittest.mock.patch.object(routes, "WEBHOOK_WITH_TOKEN") @unittest.mock.patch.object(json, "dumps") - @unittest.mock.patch.object(storage, "make_resource_seekable") + @unittest.mock.patch.object(conversions, "make_resource_seekable") async def test_execute_webhook_with_optionals( self, make_resource_seekable, dumps, WEBHOOK_WITH_TOKEN, FormData, rest_impl ): diff --git a/tests/hikari/net/test_shard.py b/tests/hikari/net/test_shard.py index 0fa5b5908d..a5b97ff2a1 100644 --- a/tests/hikari/net/test_shard.py +++ b/tests/hikari/net/test_shard.py @@ -28,7 +28,7 @@ import cymock as mock import pytest -from hikari.internal_utilities import containers +from hikari._internal import more_collections from hikari.net import errors from hikari.net import shard from hikari.net import user_agent @@ -350,7 +350,7 @@ async def test_disconnecting_dispatches_DISCONNECT(self, client, client_session_ client.dispatch = mock.MagicMock() with self.suppress_closure(): await client.connect(client_session_t) - client.dispatch.assert_called_with(client, "DISCONNECT", containers.EMPTY_DICT) + client.dispatch.assert_called_with(client, "DISCONNECT", more_collections.EMPTY_DICT) @_helpers.timeout_after(10.0) async def test_new_zlib_each_time(self, client, client_session_t): @@ -374,7 +374,7 @@ async def test_hello(self, client, client_session_t): @_helpers.timeout_after(10.0) @_helpers.assert_raises(type_=errors.GatewayError) - async def test_no_hello_throws_RuntimeError(self, client, client_session_t): + async def test_no_hello_throws_GatewayError(self, client, client_session_t): client._receive = mock.AsyncMock(return_value=self.non_hello_payload) await client.connect(client_session_t) @@ -559,11 +559,7 @@ async def test_identify_payload_no_intents_no_presence(self, client): "token": "aaaa", "compress": False, "large_threshold": 420, - "properties": { - "$os": user_agent.system_type(), - "$browser": user_agent.library_version(), - "$device": user_agent.python_version(), - }, + "properties": user_agent.UserAgent().websocket_triplet, "shard": [69, 96], }, } @@ -588,11 +584,7 @@ async def test_identify_payload_with_presence(self, client): "token": "aaaa", "compress": False, "large_threshold": 420, - "properties": { - "$os": user_agent.system_type(), - "$browser": user_agent.library_version(), - "$device": user_agent.python_version(), - }, + "properties": user_agent.UserAgent().websocket_triplet, "shard": [69, 96], "presence": presence, }, @@ -618,11 +610,7 @@ async def test_identify_payload_with_intents(self, client): "token": "aaaa", "compress": False, "large_threshold": 420, - "properties": { - "$os": user_agent.system_type(), - "$browser": user_agent.library_version(), - "$device": user_agent.python_version(), - }, + "properties": user_agent.UserAgent().websocket_triplet, "shard": [69, 96], "intents": intents, }, @@ -649,11 +637,7 @@ async def test_identify_payload_with_intents_and_presence(self, client): "token": "aaaa", "compress": False, "large_threshold": 420, - "properties": { - "$os": user_agent.system_type(), - "$browser": user_agent.library_version(), - "$device": user_agent.python_version(), - }, + "properties": user_agent.UserAgent().websocket_triplet, "shard": [69, 96], "intents": intents, "presence": presence, diff --git a/tests/hikari/net/test_user_agent.py b/tests/hikari/net/test_user_agent.py index 94a2aed744..fda87c48da 100644 --- a/tests/hikari/net/test_user_agent.py +++ b/tests/hikari/net/test_user_agent.py @@ -16,19 +16,24 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . -import hikari.net.user_agent +from hikari.net import user_agent def test_library_version_is_callable_and_produces_string(): - result = hikari.net.user_agent.library_version() - assert result.startswith("hikari ") + assert isinstance(user_agent.UserAgent().library_version, str) -def test_python_version_is_callable_and_produces_string(): - result = hikari.net.user_agent.python_version() - assert isinstance(result, str) and len(result.strip()) > 0 +def test_platform_version_is_callable_and_produces_string(): + assert isinstance(user_agent.UserAgent().platform_version, str) def test_system_type_produces_string(): - result = hikari.net.user_agent.system_type() - assert isinstance(result, str) and len(result.strip()) > 0 + assert isinstance(user_agent.UserAgent().system_type, str) + + +def test_websocket_triplet_produces_trio(): + assert user_agent.UserAgent().websocket_triplet == { + "$os": user_agent.UserAgent().system_type, + "$browser": user_agent.UserAgent().library_version, + "$device": user_agent.UserAgent().platform_version, + } From 9fdf5e610d0b7891a61d6405ebfe519b7abc15a9 Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Sun, 29 Mar 2020 23:33:13 +0100 Subject: [PATCH 052/922] Amended typehints for marshaller#attrib. --- hikari/_internal/marshaller.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/hikari/_internal/marshaller.py b/hikari/_internal/marshaller.py index d1bb871afb..473e2ee884 100644 --- a/hikari/_internal/marshaller.py +++ b/hikari/_internal/marshaller.py @@ -130,8 +130,8 @@ def attrib( # type hints, the library loses the ability to be type checked properly # anymore, so we have to pass this explicitly regardless. deserializer: typing.Optional[typing.Callable[[typing.Any], typing.Any]] = None, - if_none: typing.Union[typing.Callable[..., typing.Any], None, type(RAISE)] = RAISE, - if_undefined: typing.Union[typing.Callable[..., typing.Any], None, type(RAISE)] = RAISE, + if_none: typing.Union[typing.Callable[[], typing.Any], None, type(RAISE)] = RAISE, + if_undefined: typing.Union[typing.Callable[[], typing.Any], None, type(RAISE)] = RAISE, raw_name: typing.Optional[str] = None, transient: bool = False, serializer: typing.Optional[typing.Callable[[typing.Any], typing.Any]] = None, From cc4d0210e623f01290448008b357d1c360bcc435 Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Sun, 29 Mar 2020 23:59:54 +0100 Subject: [PATCH 053/922] Fixed noxfile formatting and similar code warning [skip deploy] --- noxfile.py | 4 ++-- pylint.ini | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/noxfile.py b/noxfile.py index 528a461d43..088fa50566 100644 --- a/noxfile.py +++ b/noxfile.py @@ -149,7 +149,7 @@ def lint(session) -> None: pkg, "--rcfile=pylint.ini", "--spelling-private-dict-file=dict.txt", - success_codes=list(range(0, 256)) + success_codes=list(range(0, 256)), ) finally: os.makedirs(ARTIFACT_DIR, exist_ok=True) @@ -161,7 +161,7 @@ def lint(session) -> None: "--rcfile=pylint.ini", "--output-format=pylint_junit.JUnitReporter", stdout=fp, - success_codes=list(range(0, 256)) + success_codes=list(range(0, 256)), ) diff --git a/pylint.ini b/pylint.ini index 365905a72d..e0083bb4dc 100644 --- a/pylint.ini +++ b/pylint.ini @@ -209,7 +209,7 @@ ignore-docstrings=yes ignore-imports=yes # Minimum lines number of a similarity. -min-similarity-lines=5 +min-similarity-lines=10 [TYPECHECK] From 40d4e49ba28f1c8f2042c6adba23ef9f09a3197b Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Tue, 31 Mar 2020 18:31:26 +0100 Subject: [PATCH 054/922] Updated documentation style. - Changed theme and formatting a bit. - Moved hikari._internal to hikari.internal to fix documentation. - Reformatted index page. - Added inheritance diagrams to some modules. - Made Channel a base class for partial channel. --- docs/.gitignore | 3 +- docs/_static/style.css | 54 ++++++--------- docs/_templates/gendoc/index.rst | 35 ---------- docs/_templates/gendoc/module.rst | 29 -------- docs/conf.py | 67 +++++++++++++------ docs/index.rst | 30 +++++++-- gitlab/pages.yml | 5 +- hikari/core/channels.py | 40 ++++++----- hikari/core/clients/app_config.py | 2 +- hikari/core/clients/gateway_client.py | 2 +- hikari/core/clients/gateway_config.py | 8 +-- hikari/core/clients/http_config.py | 2 +- hikari/core/clients/protocol_config.py | 2 +- hikari/core/clients/shard_client.py | 4 +- hikari/core/colors.py | 7 +- hikari/core/colours.py | 8 ++- hikari/core/dispatcher.py | 8 +-- hikari/core/embeds.py | 6 +- hikari/core/emojis.py | 2 +- hikari/core/entities.py | 4 +- hikari/core/events.py | 18 ++--- hikari/core/gateway_entities.py | 2 +- hikari/core/guilds.py | 6 +- hikari/core/invites.py | 8 +-- hikari/core/messages.py | 8 +-- hikari/core/oauth2.py | 4 +- hikari/core/snowflakes.py | 6 +- hikari/core/users.py | 4 +- hikari/core/voices.py | 2 +- hikari/core/webhooks.py | 2 +- hikari/{_internal => internal}/__init__.py | 20 +++--- hikari/{_internal => internal}/assertions.py | 2 + hikari/{_internal => internal}/cdn.py | 6 +- hikari/{_internal => internal}/conversions.py | 0 hikari/{_internal => internal}/marshaller.py | 4 +- hikari/{_internal => internal}/meta.py | 46 +++++++++++-- .../{_internal => internal}/more_asyncio.py | 5 +- .../more_collections.py | 6 +- .../{_internal => internal}/more_logging.py | 6 +- hikari/net/ratelimits.py | 4 +- hikari/net/rest.py | 10 +-- hikari/net/shard.py | 4 +- hikari/net/user_agent.py | 2 +- noxfile.py | 15 +++-- tests/hikari/_helpers.py | 2 +- tests/hikari/_internal/test_assertions.py | 2 +- tests/hikari/_internal/test_cdn.py | 2 +- tests/hikari/_internal/test_conversions.py | 2 +- tests/hikari/_internal/test_marshaller.py | 2 +- .../_internal/test_marshaller_pep563.py | 2 +- tests/hikari/_internal/test_meta.py | 2 +- tests/hikari/_internal/test_more_asyncio.py | 2 +- .../hikari/_internal/test_more_collections.py | 2 +- tests/hikari/_internal/test_more_logging.py | 2 +- tests/hikari/core/test_embeds.py | 4 +- tests/hikari/core/test_events.py | 14 ++-- tests/hikari/core/test_guilds.py | 20 +++--- tests/hikari/core/test_invites.py | 6 +- tests/hikari/core/test_messages.py | 6 +- tests/hikari/core/test_oauth2.py | 2 +- tests/hikari/core/test_users.py | 2 +- tests/hikari/net/test_rest.py | 2 +- tests/hikari/net/test_shard.py | 2 +- 63 files changed, 313 insertions(+), 273 deletions(-) delete mode 100644 docs/_templates/gendoc/index.rst delete mode 100644 docs/_templates/gendoc/module.rst rename hikari/{_internal => internal}/__init__.py (62%) rename hikari/{_internal => internal}/assertions.py (99%) rename hikari/{_internal => internal}/cdn.py (97%) rename hikari/{_internal => internal}/conversions.py (100%) rename hikari/{_internal => internal}/marshaller.py (99%) rename hikari/{_internal => internal}/meta.py (58%) rename hikari/{_internal => internal}/more_asyncio.py (98%) rename hikari/{_internal => internal}/more_collections.py (96%) rename hikari/{_internal => internal}/more_logging.py (95%) diff --git a/docs/.gitignore b/docs/.gitignore index 1e33be2427..1a73054c6e 100644 --- a/docs/.gitignore +++ b/docs/.gitignore @@ -1 +1,2 @@ -technical/ +hikari.*rst +modules.*rst diff --git a/docs/_static/style.css b/docs/_static/style.css index d4c56d51bc..7152545bad 100644 --- a/docs/_static/style.css +++ b/docs/_static/style.css @@ -30,10 +30,24 @@ body { font-family: 'Roboto', sans-serif; } +.xref { + background-color: #272822 !important; + font-weight: lighter; +} + +.go { + color: #999 !important; +} + +.c1 { + color: #aa3344 !important; + background-color: transparent !important; +} + /* whyyyyy */ code, .literal, .literal > span.pre, .highlight { color: #F92672 !important; - background-color: #272822 !important; + background-color: #272822; border-color: transparent !important; } @@ -58,36 +72,6 @@ dt:target, span.highlighted { text-decoration-color: #F92672; } -/* syntax highlighting: https://tmtheme-editor.herokuapp.com/#!/editor/theme/Monokai */ -/* yes, i hate myself for spending this much time deciphering this. */ -.bp { color: #D6D2BC !important; font-style: italic; font-weight: bold; text-decoration: underline; } /* base parameter (self, cls, etc) */ -.c1, .c2, .c3 { color: #8F8C7E !important; } /* single line comment */ -.fm { color: #D6D2BC !important; font-style: italic; font-weight: bold; } /* magic attribute? */ -.gp { color: #F92672 !important; } /* >>> symbol */ -.go { color: #FFFFFF !important; } /* output text */ -.k { color: #F92672 !important; } /* keyword */ -.kn { color: #F92672 !important; } /* keyword for start of import declaration? */ -.kc { color: #FD971F !important; font-style: italic; font-weight: bold;} /* singleton literal (False, True, None, etc) */ -.mi { color: #AE81FF !important; } /* int literal */ -.mf { color: #AE81FF !important; } /* float literal */ -.mh { color: #AE81FF !important; } /* hex literal */ -.mn { color: #AE81FF !important; } /* no clue, but it fixes MathJAX LaTeX rendering... */ -.mo { color: #AE81FF !important; } /* octal literal */ -.mb { color: #AE81FF !important; } /* bool literal */ -.n { color: #D6D2BC !important; font-style: italic; } /* name */ -.nb { color: #FD971F !important; font-style: italic; } /* builtin name */ -.nn { color: #FD971F !important; font-style: italic; } /* namespace name */ -.nc { color: #A6E22E !important; text-decoration: underline; } /* class name */ -.ne { color: #FD971F !important; font-style: italic; } /* exception name */ -.nd { color: #FD971F !important; } /* decorator name */ -.nf { color: #A6E22E !important; } /* function name */ -.nt { color: #A6E22E !important; } /* json key or something */ -.o { color: #F92672 !important; } /* operator */ -.p { color: #75715E !important; } /* parenthesis */ -.s1, .s2 { color: #E6DB74 !important; } /* string literal */ -.sd { color: #8F8C7E !important; } /* multiple line docstring */ -.vm { color: #D6D2BC !important; font-style: italic; font-weight: bold; } /* magic attribute? */ - input[type="text"] { background-color: #555; color: #fff; @@ -104,13 +88,13 @@ form.navbar-form { } .alert-info { - background-color: #31708f; + background-color: #505050; color: #d9edf7; } .alert-warning { - background-color: #8a6d3b; - color: #fcf8e3; + background-color: #f8d7da; + color: #721c24; } .viewcode-link, .headerlink { @@ -122,7 +106,7 @@ form.navbar-form { } .external { - color: #fd971f !important; + color: #F92672 !important } nav, .alert, .admonition { diff --git a/docs/_templates/gendoc/index.rst b/docs/_templates/gendoc/index.rst deleted file mode 100644 index 6bb38d8768..0000000000 --- a/docs/_templates/gendoc/index.rst +++ /dev/null @@ -1,35 +0,0 @@ -:orphan: - -.. image:: https://img.shields.io/discord/574921006817476608.svg?logo=Discord&logoColor=white&label=discord - :target: https://discord.gg/HMnGbsv -.. image:: https://img.shields.io/lgtm/grade/python/gitlab/nekokatt/hikari - :target: https://lgtm.com/projects/gl/nekokatt/hikari?mode=tree -.. image:: https://gitlab.com/nekokatt/hikari/badges/master/coverage.svg - :target: https://gitlab.com/nekokatt/hikari/pipelines -.. image:: https://img.shields.io/gitlab/pipeline/nekokatt/hikari/master?label=pipelines&logo=gitlab - :target: https://gitlab.com/nekokatt/hikari/pipelines -.. image:: https://badgen.net/pypi/v/hikari - :target: https://pypi.org/project/hikari -.. image:: https://badgen.net/pypi/license/hikari -.. image:: https://img.shields.io/pypi/format/hikari.svg -.. image:: https://img.shields.io/pypi/pyversions/hikari - -Hikari Technical Documentation -############################## - -This is for version |version|. |staging_link| - -Hikari is licensed under the GNU LGPLv3 https://www.gnu.org/licenses/lgpl-3.0.en.html - -Packages and submodules ------------------------ - -.. autosummary:: - :toctree: {{documentation_path}} - - {% for m in modules %}{{ m }} - {% endfor %} - -* :ref:`genindex` -* :ref:`modindex` -* :ref:`search` diff --git a/docs/_templates/gendoc/module.rst b/docs/_templates/gendoc/module.rst deleted file mode 100644 index 878281b3a8..0000000000 --- a/docs/_templates/gendoc/module.rst +++ /dev/null @@ -1,29 +0,0 @@ -:orphan: - -.. currentmodule:: {{ module }} - -{{ module | underline }} - -{% if submodules %} - -.. autosummary:: - {% for m in submodules %}{{ m }} - {% endfor %} -{% endif %} - -Overview --------- - -.. autosummary:: - {{ module }} - :members: - - -Details -------- - -.. automodule:: {{ module }} - :show-inheritance: - :inherited-members: - - diff --git a/docs/conf.py b/docs/conf.py index be0785c636..89af6d191f 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -26,6 +26,7 @@ # http://www.sphinx-doc.org/en/master/config import os import re +import shutil import sys import textwrap import types @@ -76,9 +77,17 @@ "sphinx.ext.mathjax", ] +if shutil.which("dot"): + print("Inheritance diagram enabled") + extensions += ["sphinx.ext.graphviz", "sphinx.ext.inheritance_diagram"] + templates_path = ["_templates"] exclude_patterns = [] +# -- Pygments style ---------------------------------------------------------- +pygments_style = "fruity" + + # -- Options for HTML output ------------------------------------------------- html_theme = "bootstrap" html_theme_path = sphinx_bootstrap_theme.get_html_theme_path() @@ -99,14 +108,13 @@ # Note the "1" or "True" value above as the third argument to indicate # an arbitrary url. "navbar_links": [ - ("Repository", "http://gitlab.com/nekokatt/hikari", True), - ("Wiki", "http://gitlab.com/nekokatt/hikari/wikis", True), - ("CI", "http://gitlab.com/nekokatt/hikari/pipelines", True), + ("Source", "http://gitlab.com/nekokatt/hikari", True), + ("Builds", "http://gitlab.com/nekokatt/hikari/pipelines", True), ], # Render the next and previous page links in navbar. (Default: true) - "navbar_sidebarrel": True, + "navbar_sidebarrel": False, # Render the current pages TOC in the navbar. (Default: true) - "navbar_pagenav": True, + "navbar_pagenav": False, # Tab name for the current pages TOC. (Default: "Page") "navbar_pagenav_name": "This page", # Global TOC depth for "site" navbar tab. (Default: 1) @@ -119,7 +127,7 @@ # will break. # # Values: "true" (default) or "false" - "globaltoc_includehidden": "true", + "globaltoc_includehidden": "false", # HTML navbar class (Default: "navbar") to attach to
element. # For black navbar, do "navbar navbar-inverse" "navbar_class": "navbar navbar-inverse", @@ -156,6 +164,8 @@ "undoc-members": False, "exclude-members": "__weakref__", "show_inheritance": True, + "imported_members": False, + "ignore-module-all": True, "inherited_members": True, "members": True, } @@ -170,33 +180,50 @@ "websockets": ("https://websockets.readthedocs.io/en/stable/", None), } -# -- Autosummary settings... --------------------------------------------- - -autosummary_generate = True -autosummary_generate_overwrite = True +# -- Inheritance diagram options... ------------------------------------------------- + +inheritance_graph_attrs = dict( + bgcolor="transparent", rankdir="TD", ratio="auto", fontsize=10, splines="line", size='"20 50"', +) + +inheritance_node_attrs = dict( + fontsize=10, fontname='"monospace"', color='"#505050"', style='"filled,rounded"', fontcolor='"#FFFFFF"' +) +inheritance_edge_attrs = dict( + color='"#505050"', + arrowtail="oempty", + arrowhead="none", + arrowsize=1, + dir="both", + fontcolor='"#FFFFFF"', + style='"filled"', +) +graphviz_output_format = "svg" # -- Epilog to inject into each page... --------------------------------------------- rst_epilog = """ -.. |rawEvent| replace:: This is is a raw event. This means that it is fired with the raw data sent by Discord's gateway - without any form of pre-processing or validation. Corresponding information may be incorrect, - sent multiple times, or refer to information that is not cached. The implementation specifics - of this are documented on the developer portal for Discord at - https://discordapp.com/developers/docs/topics/gateway#commands-and-events - -.. |selfHealing| replace:: You do not have to do anything in this situation. The gateway client in Hikari will attempt - to resolve these issues for you. +.. |internal| replace:: + These components are part of the hikari.internal module. + This means that anything located here is designed **only to be used internally by Hikari**, + and **you should not use it directly in your applications**. Changes to these files will occur + **without** warning or a deprecation period. It is only documented to ensure a complete reference + for application developers wishing to either contribute to or extend this library. """ if not is_staging: rst_epilog += textwrap.dedent( """.. |staging_link| replace:: If you want the latest staging documentation instead, please visit - `this page `__.""" + `this page `__. + + """ ) else: rst_epilog += textwrap.dedent( - """.. |staging_link| replace:: This is the documentation for the development release""" + """.. |staging_link| replace:: This is the documentation for the development release. + + """ ) diff --git a/docs/index.rst b/docs/index.rst index b0509a53dc..a829680ca4 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -20,11 +20,33 @@ Hikari is licensed under the GNU LGPLv3 https://www.gnu.org/licenses/lgpl-3.0.en Technical documentation ----------------------- -.. autosummary:: +.. toctree:: + :titlesonly: + hikari - hikari.core - hikari.net - hikari.errors + + +Internal components +................... + +|internal| + +.. toctree:: + :maxdepth: 2 + :titlesonly: + + hikari.internal + +Other resources +--------------- + +* `Our Discord server `_ +* `Source code `_ +* `Pipelines and builds `_ +* `CI success statistics for nerds `_ + +Search for a topic +------------------ * :ref:`genindex` * :ref:`search` diff --git a/gitlab/pages.yml b/gitlab/pages.yml index a41dcac4cf..25455efc6e 100644 --- a/gitlab/pages.yml +++ b/gitlab/pages.yml @@ -33,6 +33,8 @@ image: python:3.8.2 stage: test script: + - apt-get update + - apt-get install -qy graphviz - pip install requests - source tasks/deploy.sh - bash tasks/retry_aborts.sh pip install nox @@ -45,7 +47,6 @@ - master - staging - master-pages: extends: .generate-pages-from-specific-branch variables: @@ -60,6 +61,8 @@ test-pages: extends: .deploy-pages-activation image: python:3.8.2 script: + - apt-get update + - apt-get install -qy graphviz - pip install requests nox - bash tasks/retry_aborts.sh nox -s documentation only: diff --git a/hikari/core/channels.py b/hikari/core/channels.py index f9cc4c881d..bd31aa9f7a 100644 --- a/hikari/core/channels.py +++ b/hikari/core/channels.py @@ -18,6 +18,15 @@ # along with Hikari. If not, see . """Components and entities that are used to describe both DMs and guild channels on Discord. + + +.. inheritance-diagram:: + hikari.core.channels + enum.IntEnum + hikari.core.entities.HikariEntity + hikari.core.entities.Deserializable + hikari.core.entities.Serializable + :parts: 1 """ __all__ = [ @@ -38,7 +47,7 @@ import enum import typing -from hikari._internal import marshaller +from hikari.internal import marshaller from hikari.core import entities from hikari.core import permissions from hikari.core import users @@ -103,23 +112,6 @@ def unset(self) -> permissions.Permission: return typing.cast(permissions.Permission, (self.allow | self.deny)) -@marshaller.attrs(slots=True) -class PartialChannel(snowflakes.UniqueEntity, entities.Deserializable): - """Represents a channel where we've only received it's basic information, - commonly received in rest responses. - """ - - #: The channel's name. - #: - #: :type: :obj:`str` - name: str = marshaller.attrib(deserializer=str) - - #: The channel's type. - #: - #: :type: :obj:`ChannelType` - type: ChannelType = marshaller.attrib(deserializer=ChannelType) - - def register_channel_type(type_: ChannelType) -> typing.Callable[[typing.Type["Channel"]], typing.Type["Channel"]]: """Generates a decorator for channel classes defined in this library to use to associate themselves with a given channel type. @@ -154,6 +146,18 @@ class Channel(snowflakes.UniqueEntity, entities.Deserializable): type: ChannelType = marshaller.attrib(deserializer=ChannelType) +@marshaller.attrs(slots=True) +class PartialChannel(Channel): + """Represents a channel where we've only received it's basic information, + commonly received in rest responses. + """ + + #: The channel's name. + #: + #: :type: :obj:`str` + name: str = marshaller.attrib(deserializer=str) + + @register_channel_type(ChannelType.DM) @marshaller.attrs(slots=True) class DMChannel(Channel): diff --git a/hikari/core/clients/app_config.py b/hikari/core/clients/app_config.py index 7421031610..68e73723c2 100644 --- a/hikari/core/clients/app_config.py +++ b/hikari/core/clients/app_config.py @@ -21,7 +21,7 @@ import typing -from hikari._internal import marshaller +from hikari.internal import marshaller from hikari.core import entities from hikari.core.clients import gateway_config from hikari.core.clients import http_config diff --git a/hikari/core/clients/gateway_client.py b/hikari/core/clients/gateway_client.py index aa95cc240e..98634b1b5e 100644 --- a/hikari/core/clients/gateway_client.py +++ b/hikari/core/clients/gateway_client.py @@ -27,7 +27,7 @@ import time import typing -from hikari._internal import more_logging +from hikari.internal import more_logging from hikari.core import dispatcher from hikari.core import events from hikari.core import state diff --git a/hikari/core/clients/gateway_config.py b/hikari/core/clients/gateway_config.py index b89c3c0b14..ad5cb22515 100644 --- a/hikari/core/clients/gateway_config.py +++ b/hikari/core/clients/gateway_config.py @@ -23,9 +23,9 @@ import re import typing -import hikari._internal.conversions -from hikari._internal import assertions -from hikari._internal import marshaller +import hikari.internal.conversions +from hikari.internal import assertions +from hikari.internal import marshaller from hikari.core import entities from hikari.core import gateway_entities from hikari.core import guilds @@ -143,7 +143,7 @@ class GatewayConfig(entities.HikariEntity, entities.Deserializable): #: #: :type: :obj:`datetime.datetime`, optional initial_idle_since: typing.Optional[datetime.datetime] = marshaller.attrib( - deserializer=hikari._internal.conversions.unix_epoch_to_ts, if_none=None, if_undefined=None, default=None + deserializer=hikari.internal.conversions.unix_epoch_to_ts, if_none=None, if_undefined=None, default=None ) #: The intents to use for the connection. diff --git a/hikari/core/clients/http_config.py b/hikari/core/clients/http_config.py index 7528b98260..d2902932a6 100644 --- a/hikari/core/clients/http_config.py +++ b/hikari/core/clients/http_config.py @@ -22,7 +22,7 @@ import typing -from hikari._internal import marshaller +from hikari.internal import marshaller from hikari.core import entities from hikari.core.clients import protocol_config diff --git a/hikari/core/clients/protocol_config.py b/hikari/core/clients/protocol_config.py index 572cbfea9d..0a54126d31 100644 --- a/hikari/core/clients/protocol_config.py +++ b/hikari/core/clients/protocol_config.py @@ -25,7 +25,7 @@ import aiohttp.typedefs -from hikari._internal import marshaller +from hikari.internal import marshaller from hikari.core import entities diff --git a/hikari/core/clients/shard_client.py b/hikari/core/clients/shard_client.py index 2187aac7a8..a72d5978e4 100644 --- a/hikari/core/clients/shard_client.py +++ b/hikari/core/clients/shard_client.py @@ -37,8 +37,8 @@ import aiohttp -from hikari._internal import more_asyncio -from hikari._internal import more_logging +from hikari.internal import more_asyncio +from hikari.internal import more_logging from hikari.core import events from hikari.core import gateway_entities from hikari.core import guilds diff --git a/hikari/core/colors.py b/hikari/core/colors.py index 07aedddc2f..0cf343152b 100644 --- a/hikari/core/colors.py +++ b/hikari/core/colors.py @@ -18,6 +18,11 @@ # along with Hikari. If not, see . """Model that represents a common RGB color and provides simple conversions to other common color systems. + +.. inheritance-diagram:: + builtins.int + hikari.core.colors + :parts: 1 """ __all__ = ["Color", "ColorCompatibleT"] @@ -25,7 +30,7 @@ import string import typing -from hikari._internal import assertions +from hikari.internal import assertions class Color(int, typing.SupportsInt): diff --git a/hikari/core/colours.py b/hikari/core/colours.py index 97a4875007..98d501684f 100644 --- a/hikari/core/colours.py +++ b/hikari/core/colours.py @@ -16,8 +16,12 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . -""" -Alias for the :mod:`hikari.core.colors` module. +"""Alias for the :mod:`hikari.core.colors` module. + +.. inheritance-diagram:: + builtins.int + hikari.core.colours + :parts: 1 """ __all__ = ["Colour"] diff --git a/hikari/core/dispatcher.py b/hikari/core/dispatcher.py index bd7934da3e..9150a0c2b4 100644 --- a/hikari/core/dispatcher.py +++ b/hikari/core/dispatcher.py @@ -25,10 +25,10 @@ import typing import weakref -from hikari._internal import assertions -from hikari._internal import more_asyncio -from hikari._internal import more_collections -from hikari._internal import more_logging +from hikari.internal import assertions +from hikari.internal import more_asyncio +from hikari.internal import more_collections +from hikari.internal import more_logging from hikari.core import events EventT = typing.TypeVar("EventT", bound=events.HikariEvent) diff --git a/hikari/core/embeds.py b/hikari/core/embeds.py index 13649d3a92..826d7c7440 100644 --- a/hikari/core/embeds.py +++ b/hikari/core/embeds.py @@ -33,8 +33,8 @@ import datetime import typing -import hikari._internal.conversions -from hikari._internal import marshaller +import hikari.internal.conversions +from hikari.internal import marshaller from hikari.core import colors from hikari.core import entities @@ -212,7 +212,7 @@ class Embed(entities.HikariEntity, entities.Deserializable, entities.Serializabl #: #: :type: :obj:`datetime.datetime`, optional timestamp: typing.Optional[datetime.datetime] = marshaller.attrib( - deserializer=hikari._internal.conversions.parse_iso_8601_ts, + deserializer=hikari.internal.conversions.parse_iso_8601_ts, serializer=lambda timestamp: timestamp.replace(tzinfo=datetime.timezone.utc).isoformat(), if_undefined=None, ) diff --git a/hikari/core/emojis.py b/hikari/core/emojis.py index 7dbb7ca644..bc0249b3f8 100644 --- a/hikari/core/emojis.py +++ b/hikari/core/emojis.py @@ -21,7 +21,7 @@ """ import typing -from hikari._internal import marshaller +from hikari.internal import marshaller from hikari.core import entities from hikari.core import snowflakes from hikari.core import users diff --git a/hikari/core/entities.py b/hikari/core/entities.py index cf784db139..49b46018bd 100644 --- a/hikari/core/entities.py +++ b/hikari/core/entities.py @@ -22,8 +22,8 @@ import abc import typing -from hikari._internal import marshaller -from hikari._internal import meta +from hikari.internal import marshaller +from hikari.internal import meta RawEntityT = typing.Union[ None, bool, int, float, str, bytes, typing.Sequence[typing.Any], typing.Mapping[str, typing.Any] diff --git a/hikari/core/events.py b/hikari/core/events.py index 3a2630209f..0e93c4ffe1 100644 --- a/hikari/core/events.py +++ b/hikari/core/events.py @@ -72,9 +72,9 @@ import attr -import hikari._internal.conversions -from hikari._internal import assertions -from hikari._internal import marshaller +import hikari.internal.conversions +from hikari.internal import assertions +from hikari.internal import marshaller from hikari.core import channels from hikari.core import embeds as _embeds from hikari.core import emojis as _emojis @@ -343,7 +343,7 @@ class BaseChannelEvent(HikariEvent, snowflakes.UniqueEntity, entities.Deserializ #: #: :type: :obj:`datetime.datetime`, optional last_pin_timestamp: typing.Optional[datetime.datetime] = marshaller.attrib( - deserializer=hikari._internal.conversions.parse_iso_8601_ts, if_undefined=None + deserializer=hikari.internal.conversions.parse_iso_8601_ts, if_undefined=None ) @@ -395,7 +395,7 @@ class ChannelPinUpdateEvent(HikariEvent, entities.Deserializable): #: #: :type: :obj:`datetime.datetime`, optional last_pin_timestamp: typing.Optional[datetime.datetime] = marshaller.attrib( - deserializer=hikari._internal.conversions.parse_iso_8601_ts, if_undefined=None + deserializer=hikari.internal.conversions.parse_iso_8601_ts, if_undefined=None ) @@ -559,7 +559,7 @@ class GuildMemberUpdateEvent(HikariEvent, entities.Deserializable): #: #: :type: :obj:`typing.Union` [ :obj:`datetime.datetime`, :obj:`entities.UNSET` ], optional premium_since: typing.Union[None, datetime.datetime, entities.Unset] = marshaller.attrib( - deserializer=hikari._internal.conversions.parse_iso_8601_ts, if_none=None, if_undefined=entities.Unset + deserializer=hikari.internal.conversions.parse_iso_8601_ts, if_none=None, if_undefined=entities.Unset ) @@ -629,7 +629,7 @@ class InviteCreateEvent(HikariEvent, entities.Deserializable): #: The datetime of when this invite was created. #: #: :type: :obj:`datetime.datetime` - created_at: datetime.datetime = marshaller.attrib(deserializer=hikari._internal.conversions.parse_iso_8601_ts) + created_at: datetime.datetime = marshaller.attrib(deserializer=hikari.internal.conversions.parse_iso_8601_ts) #: The ID of the guild this invite was created in, if applicable. #: Will be ``None`` for group DM invites. @@ -764,14 +764,14 @@ class MessageUpdateEvent(HikariEvent, snowflakes.UniqueEntity, entities.Deserial #: #: :type: :obj:`typing.Union` [ :obj:`datetime.datetime`, :obj:`entities.UNSET` ] timestamp: typing.Union[datetime.datetime, entities.Unset] = marshaller.attrib( - deserializer=hikari._internal.conversions.parse_iso_8601_ts, if_undefined=entities.Unset + deserializer=hikari.internal.conversions.parse_iso_8601_ts, if_undefined=entities.Unset ) #: The timestamp that the message was last edited at, or ``None`` if not ever edited. #: #: :type: :obj:`typing.Union` [ :obj:`datetime.datetime`, :obj:`entities.UNSET` ], optional edited_timestamp: typing.Union[datetime.datetime, entities.Unset, None] = marshaller.attrib( - deserializer=hikari._internal.conversions.parse_iso_8601_ts, if_none=None, if_undefined=entities.Unset + deserializer=hikari.internal.conversions.parse_iso_8601_ts, if_none=None, if_undefined=entities.Unset ) #: Whether the message is a TTS message. diff --git a/hikari/core/gateway_entities.py b/hikari/core/gateway_entities.py index 1a2456e0f7..d5523e7b1a 100644 --- a/hikari/core/gateway_entities.py +++ b/hikari/core/gateway_entities.py @@ -22,7 +22,7 @@ import datetime import typing -from hikari._internal import marshaller +from hikari.internal import marshaller from hikari.core import entities from hikari.core import guilds diff --git a/hikari/core/guilds.py b/hikari/core/guilds.py index b1673ece26..8ead6ca8f5 100644 --- a/hikari/core/guilds.py +++ b/hikari/core/guilds.py @@ -42,9 +42,9 @@ import enum import typing -from hikari._internal import cdn -from hikari._internal import conversions -from hikari._internal import marshaller +from hikari.internal import cdn +from hikari.internal import conversions +from hikari.internal import marshaller from hikari.core import colors from hikari.core import channels as _channels from hikari.core import emojis as _emojis diff --git a/hikari/core/invites.py b/hikari/core/invites.py index 0f7a53cab0..ace8e1d011 100644 --- a/hikari/core/invites.py +++ b/hikari/core/invites.py @@ -24,9 +24,9 @@ import enum import typing -import hikari._internal.conversions -from hikari._internal import cdn -from hikari._internal import marshaller +import hikari.internal.conversions +from hikari.internal import cdn +from hikari.internal import marshaller from hikari.core import channels from hikari.core import entities from hikari.core import guilds @@ -228,7 +228,7 @@ class InviteWithMetadata(Invite): #: When this invite was created. #: #: :type: :obj:`datetime.datetime` - created_at: datetime.datetime = marshaller.attrib(deserializer=hikari._internal.conversions.parse_iso_8601_ts) + created_at: datetime.datetime = marshaller.attrib(deserializer=hikari.internal.conversions.parse_iso_8601_ts) @property def expires_at(self) -> typing.Optional[datetime.datetime]: diff --git a/hikari/core/messages.py b/hikari/core/messages.py index 8262cb2182..744655a05c 100644 --- a/hikari/core/messages.py +++ b/hikari/core/messages.py @@ -30,8 +30,8 @@ import enum import typing -import hikari._internal.conversions -from hikari._internal import marshaller +import hikari.internal.conversions +from hikari.internal import marshaller from hikari.core import embeds as _embeds from hikari.core import emojis as _emojis from hikari.core import entities @@ -246,13 +246,13 @@ class Message(snowflakes.UniqueEntity, entities.Deserializable): #: The timestamp that the message was sent at. #: #: :type: :obj:`datetime.datetime` - timestamp: datetime.datetime = marshaller.attrib(deserializer=hikari._internal.conversions.parse_iso_8601_ts) + timestamp: datetime.datetime = marshaller.attrib(deserializer=hikari.internal.conversions.parse_iso_8601_ts) #: The timestamp that the message was last edited at, or ``None`` if not ever edited. #: #: :type: :obj:`datetime.datetime`, optional edited_timestamp: typing.Optional[datetime.datetime] = marshaller.attrib( - deserializer=hikari._internal.conversions.parse_iso_8601_ts, if_none=None + deserializer=hikari.internal.conversions.parse_iso_8601_ts, if_none=None ) #: Whether the message is a TTS message. diff --git a/hikari/core/oauth2.py b/hikari/core/oauth2.py index d54d587d5e..a7d3d59207 100644 --- a/hikari/core/oauth2.py +++ b/hikari/core/oauth2.py @@ -22,8 +22,8 @@ import enum import typing -from hikari._internal import cdn -from hikari._internal import marshaller +from hikari.internal import cdn +from hikari.internal import marshaller from hikari.core import entities from hikari.core import guilds from hikari.core import permissions diff --git a/hikari/core/snowflakes.py b/hikari/core/snowflakes.py index 2c6df63e48..158457da8a 100644 --- a/hikari/core/snowflakes.py +++ b/hikari/core/snowflakes.py @@ -26,8 +26,8 @@ import functools import typing -import hikari._internal.conversions -from hikari._internal import marshaller +import hikari.internal.conversions +from hikari.internal import marshaller from hikari.core import entities @@ -54,7 +54,7 @@ def __init__(self, value: typing.Union[int, str]) -> None: # pylint:disable=sup def created_at(self) -> datetime.datetime: """When the object was created.""" epoch = self._value >> 22 - return hikari._internal.conversions.discord_epoch_to_datetime(epoch) + return hikari.internal.conversions.discord_epoch_to_datetime(epoch) @property def internal_worker_id(self) -> int: diff --git a/hikari/core/users.py b/hikari/core/users.py index 1fd5a0a33d..d90ac83700 100644 --- a/hikari/core/users.py +++ b/hikari/core/users.py @@ -22,8 +22,8 @@ import enum import typing -from hikari._internal import cdn -from hikari._internal import marshaller +from hikari.internal import cdn +from hikari.internal import marshaller from hikari.core import entities from hikari.core import snowflakes diff --git a/hikari/core/voices.py b/hikari/core/voices.py index d8d54355e3..a2f2897d18 100644 --- a/hikari/core/voices.py +++ b/hikari/core/voices.py @@ -22,7 +22,7 @@ import typing -from hikari._internal import marshaller +from hikari.internal import marshaller from hikari.core import entities from hikari.core import guilds from hikari.core import snowflakes diff --git a/hikari/core/webhooks.py b/hikari/core/webhooks.py index bab873c8de..3811f4336d 100644 --- a/hikari/core/webhooks.py +++ b/hikari/core/webhooks.py @@ -22,7 +22,7 @@ import enum import typing -from hikari._internal import marshaller +from hikari.internal import marshaller from hikari.core import entities from hikari.core import snowflakes from hikari.core import users diff --git a/hikari/_internal/__init__.py b/hikari/internal/__init__.py similarity index 62% rename from hikari/_internal/__init__.py rename to hikari/internal/__init__.py index c8b749b260..c02f609023 100644 --- a/hikari/_internal/__init__.py +++ b/hikari/internal/__init__.py @@ -18,15 +18,13 @@ # along with Hikari. If not, see . """Various utilities used internally within this API. -These are not bound to the versioning contact, and are considered to be -implementation detail that could change at any time, so should not be -used outside this library. +|internal| """ -from hikari._internal import assertions -from hikari._internal import cdn -from hikari._internal import conversions -from hikari._internal import marshaller -from hikari._internal import meta -from hikari._internal import more_asyncio -from hikari._internal import more_collections -from hikari._internal import more_logging +from hikari.internal import assertions +from hikari.internal import cdn +from hikari.internal import conversions +from hikari.internal import marshaller +from hikari.internal import meta +from hikari.internal import more_asyncio +from hikari.internal import more_collections +from hikari.internal import more_logging diff --git a/hikari/_internal/assertions.py b/hikari/internal/assertions.py similarity index 99% rename from hikari/_internal/assertions.py rename to hikari/internal/assertions.py index b453429b61..5bf7fc7304 100644 --- a/hikari/_internal/assertions.py +++ b/hikari/internal/assertions.py @@ -21,6 +21,8 @@ These are functions that validate a value, and are expected to return the value on success but error on any failure. This allows for quick checking of conditions that might break the function or cause it to misbehave. + +|internal| """ __all__ = [ "assert_that", diff --git a/hikari/_internal/cdn.py b/hikari/internal/cdn.py similarity index 97% rename from hikari/_internal/cdn.py rename to hikari/internal/cdn.py index 300e4f36d8..9c43714697 100644 --- a/hikari/_internal/cdn.py +++ b/hikari/internal/cdn.py @@ -16,7 +16,11 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . -"""Basic utilities for handling the cdn.""" +"""Basic utilities for handling the cdn. + +|internal| +""" + __all__ = [ "generate_cdn_url", ] diff --git a/hikari/_internal/conversions.py b/hikari/internal/conversions.py similarity index 100% rename from hikari/_internal/conversions.py rename to hikari/internal/conversions.py diff --git a/hikari/_internal/marshaller.py b/hikari/internal/marshaller.py similarity index 99% rename from hikari/_internal/marshaller.py rename to hikari/internal/marshaller.py index 473e2ee884..6de64cc1c3 100644 --- a/hikari/_internal/marshaller.py +++ b/hikari/internal/marshaller.py @@ -22,6 +22,8 @@ -------- You should not change anything in this file, if you do, you will likely get unexpected behaviour elsewhere. + +|internal| """ __all__ = [ "RAISE", @@ -40,7 +42,7 @@ import attr -from hikari._internal import assertions +from hikari.internal import assertions _RAW_NAME_ATTR = __name__ + "_RAW_NAME" _SERIALIZER_ATTR = __name__ + "_SERIALIZER" diff --git a/hikari/_internal/meta.py b/hikari/internal/meta.py similarity index 58% rename from hikari/_internal/meta.py rename to hikari/internal/meta.py index 03f18f3842..7d90fe641a 100644 --- a/hikari/_internal/meta.py +++ b/hikari/internal/meta.py @@ -16,15 +16,18 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . -"""Various functional types and metatypes.""" -__all__ = ["SingletonMeta"] +"""Various functional types and metatypes. + +|internal| +""" +__all__ = ["SingletonMeta", "Singleton"] class SingletonMeta(type): """Metaclass that makes the class a singleton. - Once an instance has been defined at runtime, it will exist until the interpreter - that created it is terminated. + Once an instance has been defined at runtime, it will exist until the + interpreter that created it is terminated. Example -------- @@ -33,15 +36,18 @@ class SingletonMeta(type): >>> class Unknown(metaclass=SingletonMeta): ... def __init__(self): ... print("Initialized an Unknown!") + >>> Unknown() is Unknown() # True Note ---- - The constructors of these classes must not take any arguments other than ``self``. + The constructors of instances of this metaclass must not take any arguments + other than ``self``. Warning ------- - This is not thread safe. + Constructing instances of class instances of this metaclass may not be + thread safe. """ ___instances___ = {} @@ -51,3 +57,31 @@ def __call__(cls): if cls not in SingletonMeta.___instances___: SingletonMeta.___instances___[cls] = super().__call__() return SingletonMeta.___instances___[cls] + + +class Singleton(metaclass=SingletonMeta): + """Base type for anything implementing the :obj:`SingletonMeta` metaclass. + + Once an instance has been defined at runtime, it will exist until the + interpreter that created it is terminated. + + Example + ------- + + .. code-block:: python + + >>> class MySingleton(Singleton): + ... pass + + >>> assert MySingleton() is MySingleton() + + Note + ---- + The constructors of child classes must not take any arguments other than + ``self``. + + Warning + ------- + Constructing instances of this class or derived classes may not be thread + safe. + """ diff --git a/hikari/_internal/more_asyncio.py b/hikari/internal/more_asyncio.py similarity index 98% rename from hikari/_internal/more_asyncio.py rename to hikari/internal/more_asyncio.py index 5ff94b0d4f..0b7f2dff4b 100644 --- a/hikari/_internal/more_asyncio.py +++ b/hikari/internal/more_asyncio.py @@ -16,7 +16,10 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . -"""Asyncio extensions and utilities.""" +"""Asyncio extensions and utilities. + +|internal| +""" from __future__ import annotations __all__ = ["Future", "Task", "completed_future"] diff --git a/hikari/_internal/more_collections.py b/hikari/internal/more_collections.py similarity index 96% rename from hikari/_internal/more_collections.py rename to hikari/internal/more_collections.py index 9a4032a949..19925781db 100644 --- a/hikari/_internal/more_collections.py +++ b/hikari/internal/more_collections.py @@ -16,7 +16,11 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . -"""Special data structures and utilities.""" +"""Special data structures and utilities. + +|internal| +""" + __all__ = [ "EMPTY_SEQUENCE", "EMPTY_SET", diff --git a/hikari/_internal/more_logging.py b/hikari/internal/more_logging.py similarity index 95% rename from hikari/_internal/more_logging.py rename to hikari/internal/more_logging.py index 4b29ae5162..46c24ad191 100644 --- a/hikari/_internal/more_logging.py +++ b/hikari/internal/more_logging.py @@ -16,7 +16,11 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . -"""Utilities for creating and naming loggers in this library in a consistent way.""" +"""Utilities for creating and naming loggers with consistent names. + +|internal| +""" + __all__ = ["get_named_logger"] import logging diff --git a/hikari/net/ratelimits.py b/hikari/net/ratelimits.py index acb2087ad4..47a909a1c8 100644 --- a/hikari/net/ratelimits.py +++ b/hikari/net/ratelimits.py @@ -180,8 +180,8 @@ import typing import weakref -from hikari._internal import more_asyncio -from hikari._internal import more_logging +from hikari.internal import more_asyncio +from hikari.internal import more_logging from hikari.net import routes UNKNOWN_HASH = "UNKNOWN" diff --git a/hikari/net/rest.py b/hikari/net/rest.py index a7e171431b..e6bb2915bf 100644 --- a/hikari/net/rest.py +++ b/hikari/net/rest.py @@ -30,10 +30,10 @@ import aiohttp.typedefs -from hikari._internal import assertions -from hikari._internal import conversions -from hikari._internal import more_collections -from hikari._internal import more_logging +from hikari.internal import assertions +from hikari.internal import conversions +from hikari.internal import more_collections +from hikari.internal import more_logging from hikari.net import codes from hikari.net import errors from hikari.net import ratelimits @@ -3039,7 +3039,7 @@ async def execute_webhook( Returns ------- - :obj:`hikari._internal.typing.Dict` [ :obj:`str`, :obj:`typing.Any` ]`, optional + :obj:`hikari.internal.typing.Dict` [ :obj:`str`, :obj:`typing.Any` ]`, optional The created message object if ``wait`` is ``True``, else ``None``. """ form = aiohttp.FormData() diff --git a/hikari/net/shard.py b/hikari/net/shard.py index efbc378cdf..c65f21de9e 100644 --- a/hikari/net/shard.py +++ b/hikari/net/shard.py @@ -47,8 +47,8 @@ import aiohttp.typedefs -from hikari._internal import more_collections -from hikari._internal import more_logging +from hikari.internal import more_collections +from hikari.internal import more_logging from hikari.net import codes from hikari.net import errors from hikari.net import ratelimits diff --git a/hikari/net/user_agent.py b/hikari/net/user_agent.py index 34b4910511..84aa65153e 100644 --- a/hikari/net/user_agent.py +++ b/hikari/net/user_agent.py @@ -28,7 +28,7 @@ import typing -from hikari._internal import meta +from hikari.internal import meta class UserAgent(metaclass=meta.SingletonMeta): diff --git a/noxfile.py b/noxfile.py index 088fa50566..29cc6054aa 100644 --- a/noxfile.py +++ b/noxfile.py @@ -17,7 +17,9 @@ # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . import contextlib +import fnmatch import os +import re import shutil import subprocess import tarfile @@ -92,12 +94,13 @@ def documentation(session) -> None: session.install("-r", "doc-requirements.txt") session.env["SPHINXOPTS"] = "-WTvvn" - - tech_dir = pathify(DOCUMENTATION_DIR, TECHNICAL_DIR) - shutil.rmtree(tech_dir, ignore_errors=True, onerror=lambda *_: None) - os.mkdir(tech_dir) - session.run("sphinx-apidoc", "-e", "-o", tech_dir, MAIN_PACKAGE) - session.run("python", "-m", "sphinx.cmd.build", DOCUMENTATION_DIR, ARTIFACT_DIR, "-b", "html") + session.run("sphinx-apidoc", "-e", "-o", DOCUMENTATION_DIR, MAIN_PACKAGE) + session.run( + "python", "-m", "sphinx.cmd.build", "-a", "-b", "html", "-j", "auto", "-n", DOCUMENTATION_DIR, ARTIFACT_DIR + ) + for f in os.listdir(DOCUMENTATION_DIR): + if f in ("hikari.rst", "modules.rst") or re.match(r"hikari\.(\w|\.)+\.rst", f): + os.unlink(pathify(DOCUMENTATION_DIR, f)) @nox.session(reuse_venv=True) diff --git a/tests/hikari/_helpers.py b/tests/hikari/_helpers.py index 5727a582a0..58bb866939 100644 --- a/tests/hikari/_helpers.py +++ b/tests/hikari/_helpers.py @@ -35,7 +35,7 @@ import cymock as mock import pytest -from hikari._internal import marshaller +from hikari.internal import marshaller _LOGGER = logging.getLogger(__name__) diff --git a/tests/hikari/_internal/test_assertions.py b/tests/hikari/_internal/test_assertions.py index b733be8381..2ff3d1b7bd 100644 --- a/tests/hikari/_internal/test_assertions.py +++ b/tests/hikari/_internal/test_assertions.py @@ -18,7 +18,7 @@ # along with Hikari. If not, see . import pytest -from hikari._internal import assertions +from hikari.internal import assertions from tests.hikari import _helpers diff --git a/tests/hikari/_internal/test_cdn.py b/tests/hikari/_internal/test_cdn.py index 6bb3f7f126..1f89fa6fe9 100644 --- a/tests/hikari/_internal/test_cdn.py +++ b/tests/hikari/_internal/test_cdn.py @@ -16,7 +16,7 @@ # # You should have received a copy of the GNU Lesser General Public License # along ith Hikari. If not, see . -from hikari._internal import cdn +from hikari.internal import cdn def test_generate_cdn_url(): diff --git a/tests/hikari/_internal/test_conversions.py b/tests/hikari/_internal/test_conversions.py index f260b82b06..073e189496 100644 --- a/tests/hikari/_internal/test_conversions.py +++ b/tests/hikari/_internal/test_conversions.py @@ -21,7 +21,7 @@ import pytest -from hikari._internal import conversions +from hikari.internal import conversions @pytest.mark.parametrize( diff --git a/tests/hikari/_internal/test_marshaller.py b/tests/hikari/_internal/test_marshaller.py index 85ddc7d925..0f47b4fd4b 100644 --- a/tests/hikari/_internal/test_marshaller.py +++ b/tests/hikari/_internal/test_marshaller.py @@ -19,7 +19,7 @@ import cymock as mock import pytest -from hikari._internal import marshaller +from hikari.internal import marshaller from tests.hikari import _helpers diff --git a/tests/hikari/_internal/test_marshaller_pep563.py b/tests/hikari/_internal/test_marshaller_pep563.py index 76df58c845..e17756181b 100644 --- a/tests/hikari/_internal/test_marshaller_pep563.py +++ b/tests/hikari/_internal/test_marshaller_pep563.py @@ -26,7 +26,7 @@ import cymock as mock import pytest -from hikari._internal import marshaller +from hikari.internal import marshaller from tests.hikari import _helpers diff --git a/tests/hikari/_internal/test_meta.py b/tests/hikari/_internal/test_meta.py index 0f227ff489..db02c8ecbe 100644 --- a/tests/hikari/_internal/test_meta.py +++ b/tests/hikari/_internal/test_meta.py @@ -16,7 +16,7 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . -from hikari._internal import meta +from hikari.internal import meta def test_SingletonMeta(): diff --git a/tests/hikari/_internal/test_more_asyncio.py b/tests/hikari/_internal/test_more_asyncio.py index 1036314faf..7fb634a304 100644 --- a/tests/hikari/_internal/test_more_asyncio.py +++ b/tests/hikari/_internal/test_more_asyncio.py @@ -20,7 +20,7 @@ import pytest -from hikari._internal import more_asyncio +from hikari.internal import more_asyncio class CoroutineStub: diff --git a/tests/hikari/_internal/test_more_collections.py b/tests/hikari/_internal/test_more_collections.py index 3346bc4677..cd7d0501da 100644 --- a/tests/hikari/_internal/test_more_collections.py +++ b/tests/hikari/_internal/test_more_collections.py @@ -17,7 +17,7 @@ # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . -from hikari._internal import more_collections +from hikari.internal import more_collections class TestWeakKeyDictionary: diff --git a/tests/hikari/_internal/test_more_logging.py b/tests/hikari/_internal/test_more_logging.py index 3b5659519c..d5317ca09c 100644 --- a/tests/hikari/_internal/test_more_logging.py +++ b/tests/hikari/_internal/test_more_logging.py @@ -17,7 +17,7 @@ # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . -from hikari._internal import more_logging +from hikari.internal import more_logging package_name = __name__ diff --git a/tests/hikari/core/test_embeds.py b/tests/hikari/core/test_embeds.py index d8015e2756..3152bd2e3e 100644 --- a/tests/hikari/core/test_embeds.py +++ b/tests/hikari/core/test_embeds.py @@ -21,7 +21,7 @@ import cymock as mock import pytest -import hikari._internal.conversions +import hikari.internal.conversions from hikari.core import colors from hikari.core import embeds from tests.hikari import _helpers @@ -229,7 +229,7 @@ def test_deserialize( with _helpers.patch_marshal_attr( embeds.Embed, "timestamp", - deserializer=hikari._internal.conversions.parse_iso_8601_ts, + deserializer=hikari.internal.conversions.parse_iso_8601_ts, return_value=mock_datetime, ) as patched_timestamp_deserializer: embed_obj = embeds.Embed.deserialize(test_embed_payload) diff --git a/tests/hikari/core/test_events.py b/tests/hikari/core/test_events.py index 18883af69d..721f3e2e90 100644 --- a/tests/hikari/core/test_events.py +++ b/tests/hikari/core/test_events.py @@ -21,7 +21,7 @@ import cymock as mock import pytest -import hikari._internal.conversions +import hikari.internal.conversions from hikari.core import channels from hikari.core import embeds from hikari.core import emojis @@ -206,7 +206,7 @@ def test_deserialize(self, test_base_channel_payload, test_overwrite_payload, te with _helpers.patch_marshal_attr( events.BaseChannelEvent, "last_pin_timestamp", - deserializer=hikari._internal.conversions.parse_iso_8601_ts, + deserializer=hikari.internal.conversions.parse_iso_8601_ts, return_value=mock_timestamp, ) as patched_timestamp_deserializer: with mock.patch.object(users.User, "deserialize", return_value=mock_user): @@ -263,7 +263,7 @@ def test_deserialize(self, test_chanel_pin_update_payload): with _helpers.patch_marshal_attr( events.ChannelPinUpdateEvent, "last_pin_timestamp", - deserializer=hikari._internal.conversions.parse_iso_8601_ts, + deserializer=hikari.internal.conversions.parse_iso_8601_ts, return_value=mock_timestamp, ) as patched_iso_parser: channel_pin_add_obj = events.ChannelPinUpdateEvent.deserialize(test_chanel_pin_update_payload) @@ -384,7 +384,7 @@ def test_deserialize(self, guild_member_update_payload, test_user_payload): with _helpers.patch_marshal_attr( events.GuildMemberUpdateEvent, "premium_since", - deserializer=hikari._internal.conversions.parse_iso_8601_ts, + deserializer=hikari.internal.conversions.parse_iso_8601_ts, return_value=mock_premium_since, ) as patched_premium_since_deserializer: guild_member_update_obj = events.GuildMemberUpdateEvent.deserialize(guild_member_update_payload) @@ -479,7 +479,7 @@ def test_deserialize(self, test_invite_create_payload, test_user_payload): with _helpers.patch_marshal_attr( events.InviteCreateEvent, "created_at", - deserializer=hikari._internal.conversions.parse_iso_8601_ts, + deserializer=hikari.internal.conversions.parse_iso_8601_ts, return_value=mock_created_at, ) as patched_created_at_deserializer: invite_create_obj = events.InviteCreateEvent.deserialize(test_invite_create_payload) @@ -629,13 +629,13 @@ def test_deserialize( with _helpers.patch_marshal_attr( events.MessageUpdateEvent, "timestamp", - deserializer=hikari._internal.conversions.parse_iso_8601_ts, + deserializer=hikari.internal.conversions.parse_iso_8601_ts, return_value=mock_timestamp, ) as patched_timestamp_deserializer: with _helpers.patch_marshal_attr( events.MessageUpdateEvent, "edited_timestamp", - deserializer=hikari._internal.conversions.parse_iso_8601_ts, + deserializer=hikari.internal.conversions.parse_iso_8601_ts, return_value=mock_edited_timestamp, ) as patched_edit_deserializer: with _helpers.patch_marshal_attr( diff --git a/tests/hikari/core/test_guilds.py b/tests/hikari/core/test_guilds.py index 5bec5c4472..0dfef2bbcc 100644 --- a/tests/hikari/core/test_guilds.py +++ b/tests/hikari/core/test_guilds.py @@ -21,8 +21,8 @@ import cymock as mock import pytest -import hikari._internal.conversions -from hikari._internal import cdn +import hikari.internal.conversions +from hikari.internal import cdn from hikari.core import emojis from hikari.core import entities from hikari.core import guilds @@ -266,13 +266,13 @@ def test_deserialize(self, test_member_payload, test_user_payload): with _helpers.patch_marshal_attr( guilds.GuildMember, "joined_at", - deserializer=hikari._internal.conversions.parse_iso_8601_ts, + deserializer=hikari.internal.conversions.parse_iso_8601_ts, return_value=mock_datetime_1, ) as patched_joined_at_deserializer: with _helpers.patch_marshal_attr( guilds.GuildMember, "premium_since", - deserializer=hikari._internal.conversions.parse_iso_8601_ts, + deserializer=hikari.internal.conversions.parse_iso_8601_ts, return_value=mock_datetime_2, ) as patched_premium_since_deserializer: guild_member_obj = guilds.GuildMember.deserialize(test_member_payload) @@ -320,13 +320,13 @@ def test_deserialize(self, test_activity_timestamps_payload): with _helpers.patch_marshal_attr( guilds.ActivityTimestamps, "start", - deserializer=hikari._internal.conversions.unix_epoch_to_ts, + deserializer=hikari.internal.conversions.unix_epoch_to_ts, return_value=mock_start_date, ) as patched_start_deserializer: with _helpers.patch_marshal_attr( guilds.ActivityTimestamps, "end", - deserializer=hikari._internal.conversions.unix_epoch_to_ts, + deserializer=hikari.internal.conversions.unix_epoch_to_ts, return_value=mock_end_date, ) as patched_end_deserializer: activity_timestamps_obj = guilds.ActivityTimestamps.deserialize(test_activity_timestamps_payload) @@ -392,7 +392,7 @@ def test_deserialize( with _helpers.patch_marshal_attr( guilds.PresenceActivity, "created_at", - deserializer=hikari._internal.conversions.unix_epoch_to_ts, + deserializer=hikari.internal.conversions.unix_epoch_to_ts, return_value=mock_created_at, ) as patched_created_at_deserializer: with _helpers.patch_marshal_attr( @@ -473,7 +473,7 @@ def test_deserialize( with _helpers.patch_marshal_attr( guilds.GuildMemberPresence, "premium_since", - deserializer=hikari._internal.conversions.parse_iso_8601_ts, + deserializer=hikari.internal.conversions.parse_iso_8601_ts, return_value=mock_since, ) as patched_since_deserializer: guild_member_presence_obj = guilds.GuildMemberPresence.deserialize(test_guild_member_presence) @@ -561,7 +561,7 @@ def test_deserialize(self, test_guild_integration_payload, test_user_payload, te with _helpers.patch_marshal_attr( guilds.GuildIntegration, "last_synced_at", - deserializer=hikari._internal.conversions.parse_iso_8601_ts, + deserializer=hikari.internal.conversions.parse_iso_8601_ts, return_value=mock_sync_date, ) as patched_sync_at_deserializer: with _helpers.patch_marshal_attr( @@ -700,7 +700,7 @@ def test_deserialize( | guilds.GuildSystemChannelFlag.SUPPRESS_USER_JOIN ) assert guild_obj.rules_channel_id == 42042069 - assert guild_obj.joined_at == hikari._internal.conversions.parse_iso_8601_ts("2019-05-17T06:26:56.936000+00:00") + assert guild_obj.joined_at == hikari.internal.conversions.parse_iso_8601_ts("2019-05-17T06:26:56.936000+00:00") assert guild_obj.is_large is False assert guild_obj.member_count == 14 assert guild_obj.channels == {6969: mock_guild_channel} diff --git a/tests/hikari/core/test_invites.py b/tests/hikari/core/test_invites.py index 60ebb56085..1cc225144b 100644 --- a/tests/hikari/core/test_invites.py +++ b/tests/hikari/core/test_invites.py @@ -21,8 +21,8 @@ import cymock as mock import pytest -import hikari._internal.conversions -from hikari._internal import cdn +import hikari.internal.conversions +from hikari.internal import cdn from hikari.core import channels from hikari.core import guilds from hikari.core import invites @@ -218,7 +218,7 @@ def test_deserialize(self, *deserializers, test_invite_with_metadata_payload): with _helpers.patch_marshal_attr( invites.InviteWithMetadata, "created_at", - deserializer=hikari._internal.conversions.parse_iso_8601_ts, + deserializer=hikari.internal.conversions.parse_iso_8601_ts, return_value=mock_datetime, ) as mock_created_at_deserializer: invite_with_metadata_obj = invites.InviteWithMetadata.deserialize(test_invite_with_metadata_payload) diff --git a/tests/hikari/core/test_messages.py b/tests/hikari/core/test_messages.py index 817c276cc8..4b0b85907d 100644 --- a/tests/hikari/core/test_messages.py +++ b/tests/hikari/core/test_messages.py @@ -21,7 +21,7 @@ import cymock as mock import pytest -import hikari._internal.conversions +import hikari.internal.conversions from hikari.core import embeds from hikari.core import emojis from hikari.core import guilds @@ -197,13 +197,13 @@ def test_deserialize( with _helpers.patch_marshal_attr( messages.Message, "timestamp", - deserializer=hikari._internal.conversions.parse_iso_8601_ts, + deserializer=hikari.internal.conversions.parse_iso_8601_ts, return_value=mock_datetime, ) as patched_timestamp_deserializer: with _helpers.patch_marshal_attr( messages.Message, "edited_timestamp", - deserializer=hikari._internal.conversions.parse_iso_8601_ts, + deserializer=hikari.internal.conversions.parse_iso_8601_ts, return_value=mock_datetime2, ) as patched_edited_timestamp_deserializer: with _helpers.patch_marshal_attr( diff --git a/tests/hikari/core/test_oauth2.py b/tests/hikari/core/test_oauth2.py index cb3817dfc7..8dec8e6fa4 100644 --- a/tests/hikari/core/test_oauth2.py +++ b/tests/hikari/core/test_oauth2.py @@ -19,7 +19,7 @@ import cymock as mock import pytest -from hikari._internal import cdn +from hikari.internal import cdn from hikari.core import oauth2 from hikari.core import users from tests.hikari import _helpers diff --git a/tests/hikari/core/test_users.py b/tests/hikari/core/test_users.py index 83a8dde081..935da60470 100644 --- a/tests/hikari/core/test_users.py +++ b/tests/hikari/core/test_users.py @@ -19,7 +19,7 @@ import cymock as mock import pytest -from hikari._internal import cdn +from hikari.internal import cdn from hikari.core import users diff --git a/tests/hikari/net/test_rest.py b/tests/hikari/net/test_rest.py index eeb1b4f866..841ff1a0a5 100644 --- a/tests/hikari/net/test_rest.py +++ b/tests/hikari/net/test_rest.py @@ -28,7 +28,7 @@ import cymock as mock import pytest -from hikari._internal import conversions +from hikari.internal import conversions from hikari.net import errors from hikari.net import ratelimits from hikari.net import rest diff --git a/tests/hikari/net/test_shard.py b/tests/hikari/net/test_shard.py index a5b97ff2a1..b6373bffe3 100644 --- a/tests/hikari/net/test_shard.py +++ b/tests/hikari/net/test_shard.py @@ -28,7 +28,7 @@ import cymock as mock import pytest -from hikari._internal import more_collections +from hikari.internal import more_collections from hikari.net import errors from hikari.net import shard from hikari.net import user_agent From 3c42f10b10488cc7d77c84e2ae479ea763b575cb Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Fri, 3 Apr 2020 19:41:45 +0000 Subject: [PATCH 055/922] Update pages.yml to only rebuild pages on staging [skip deploy] --- gitlab/pages.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/gitlab/pages.yml b/gitlab/pages.yml index 25455efc6e..6a4076b2e2 100644 --- a/gitlab/pages.yml +++ b/gitlab/pages.yml @@ -44,7 +44,6 @@ - rm ../public -rf && mkdir ../public - mv public "../public/${TARGET_BRANCH}" only: - - master - staging master-pages: From 8373d8b4093db6b3549d4a650d099ebc9973b2f5 Mon Sep 17 00:00:00 2001 From: tandemdude Date: Fri, 3 Apr 2020 21:25:08 +0100 Subject: [PATCH 056/922] Fixes #250; Update docstring for RestfulClient to match the format. --- hikari/net/rest.py | 54 +++++++++++++++++++++++++++++++++++++--------- 1 file changed, 44 insertions(+), 10 deletions(-) diff --git a/hikari/net/rest.py b/hikari/net/rest.py index e6bb2915bf..4b9fc82701 100644 --- a/hikari/net/rest.py +++ b/hikari/net/rest.py @@ -43,7 +43,41 @@ class RestfulClient: - """A RESTful client to allow you to interact with the Discord API.""" + """A RESTful client to allow you to interact with the Discord API. + + Parameters + ---------- + base_url: :obj:`str` + The base URL and route for the discord API + allow_redirects: :obj:`bool` + Whether to allow redirects or not. + connector: :obj:`aiohttp.BaseConnector`, optional + Optional aiohttp connector info for making an HTTP connection + proxy_headers: :obj:`aiohttp.typedefs.LooseHeaders`, optional + Optional proxy headers to pass to HTTP requests. + proxy_auth: :obj:`aiohttp.BasicAuth`, optional + Optional authorization to be used if using a proxy. + proxy_url: :obj:`str`, optional + Optional proxy URL to use for HTTP requests. + ssl_context: :obj:`ssl.SSLContext`, optional + The optional SSL context to be used. + verify_ssl: :obj:`bool` + Whether or not the client should enforce SSL signed certificate + verification. If ``False`` it will ignore potentially malicious + SSL certificates. + timeout: :obj:`float`, optional + The optional timeout for all HTTP requests. + json_deserialize: deserialization function + A custom JSON deserializer function to use. Defaults to + :func:`json.loads`. + json_serialize: serialization function + A custom JSON serializer function to use. Defaults to + :func:`json.dumps`. + token: :obj:`string`, optional + The bot token for the client to use. + version: :obj:`typing.Union` [ :obj:`int`, :obj:`hikari.net.versions.HTTPAPIVersion` ] + The version of the API to use. Defaults to the most recent stable version. + """ GET = "get" POST = "post" @@ -60,16 +94,16 @@ def __init__( *, base_url="https://discordapp.com/api/v{0.version}", allow_redirects: bool = False, - connector: aiohttp.BaseConnector = None, - proxy_headers: aiohttp.typedefs.LooseHeaders = None, - proxy_auth: aiohttp.BasicAuth = None, - proxy_url: str = None, - ssl_context: ssl.SSLContext = None, + connector: typing.Optional[aiohttp.BaseConnector] = None, + proxy_headers: typing.Optional[aiohttp.typedefs.LooseHeaders] = None, + proxy_auth: typing.Optional[aiohttp.BasicAuth] = None, + proxy_url: typing.Optional[str] = None, + ssl_context: typing.Optional[ssl.SSLContext] = None, verify_ssl: bool = True, - timeout: float = None, - json_deserialize=json.loads, - json_serialize=json.dumps, - token, + timeout: typing.Optional[float] = None, + json_deserialize: typing.Callable[[typing.AnyStr], typing.Dict] = json.loads, + json_serialize: typing.Callable[[typing.Dict], typing.AnyStr] = json.dumps, + token: typing.Optional[str], version: typing.Union[int, versions.HTTPAPIVersion] = versions.HTTPAPIVersion.STABLE, ): #: Whether to allow redirects or not. From b00fd1597e3a5b4bcf356a469090bc4f084dd74f Mon Sep 17 00:00:00 2001 From: Faster Speeding Date: Wed, 1 Apr 2020 23:30:48 +0100 Subject: [PATCH 057/922] Add user connection and audit log entities + tests and docs. --- hikari/core/audit_logs.py | 517 +++++++++++++++++++++++++++ hikari/core/channels.py | 7 +- hikari/core/guilds.py | 13 +- hikari/core/oauth2.py | 98 ++++- tests/hikari/core/test_audit_logs.py | 421 ++++++++++++++++++++++ tests/hikari/core/test_channels.py | 5 +- tests/hikari/core/test_guilds.py | 14 +- tests/hikari/core/test_oauth2.py | 43 +++ 8 files changed, 1102 insertions(+), 16 deletions(-) create mode 100644 hikari/core/audit_logs.py create mode 100644 tests/hikari/core/test_audit_logs.py diff --git a/hikari/core/audit_logs.py b/hikari/core/audit_logs.py new file mode 100644 index 0000000000..74f3f469b1 --- /dev/null +++ b/hikari/core/audit_logs.py @@ -0,0 +1,517 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# Copyright © Nekoka.tt 2019-2020 +# +# This file is part of Hikari. +# +# Hikari is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Hikari is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Hikari. If not, see . +"""Components and entities that are used to describe audit logs on Discord. + + +.. inheritance-diagram:: + hikari.core.audit_logs + :parts: 1 +""" +__all__ = [ + "AuditLog", + "AuditLogChange", + "AuditLogChangeKey", + "AuditLogEntry", + "AuditLogEventType", + "BaseAuditLogEntryInfo", + "ChannelOverwriteEntryInfo", + "get_entry_info_entity", + "MemberDisconnectEntryInfo", + "MemberMoveEntryInfo", + "MemberPruneEntryInfo", + "MessageBulkDeleteEntryInfo", + "MessageDeleteEntryInfo", + "MessagePinEntryInfo", +] + +import abc +import datetime +import enum +import typing + +from hikari.core import channels +from hikari.core import colors +from hikari.core import entities +from hikari.core import guilds +from hikari.core import permissions +from hikari.core import snowflakes +from hikari.core import users as _users +from hikari.core import webhooks as _webhooks +from hikari.internal import conversions +from hikari.internal import marshaller +from hikari.internal import more_collections + + +class AuditLogChangeKey(str, enum.Enum): + """Commonly known and documented keys for audit log change objects. + + Others may exist. These should be expected to default to the raw string + Discord provided us. + """ + + NAME = "name" + ICON_HASH = "icon_hash" + SPLASH_HASH = "splash_hash" + OWNER_ID = "owner_id" + REGION = "region" + AFK_CHANNEL_ID = "afk_channel_id" + AFK_TIMEOUT = "afk_timeout" + MFA_LEVEL = "mfa_level" + VERIFICATION_LEVEL = "verification_level" + EXPLICIT_CONTENT_FILTER = "explicit_content_filter" + DEFAULT_MESSAGE_NOTIFICATIONS = "default_message_notifications" + VANITY_URL_CODE = "vanity_url_code" + ADD_ROLE_TO_MEMBER = "$add" + REMOVE_ROLE_FROM_MEMBER = "$remove" + PRUNE_DELETE_DAYS = "prune_delete_days" + WIDGET_ENABLED = "widget_enabled" + WIDGET_CHANNEL_ID = "widget_channel_id" + POSITION = "position" + TOPIC = "topic" + BITRATE = "bitrate" + PERMISSION_OVERWRITES = "permission_overwrites" + NSFW = "nsfw" + APPLICATION_ID = "application_id" + PERMISSIONS = "permissions" + COLOR = "color" + HOIST = "hoist" + MENTIONABLE = "mentionable" + ALLOW = "allow" + DENY = "deny" + INVITE_CODE = "code" + CHANNEL_ID = "channel_id" + INVITER_ID = "inviter_id" + MAX_USES = "max_uses" + USES = "uses" + MAX_AGE = "max_age" + TEMPORARY = "temporary" + DEAF = "deaf" + MUTE = "mute" + NICK = "nick" + AVATAR_HASH = "avatar_hash" + ID = "id" + TYPE = "type" + ENABLE_EMOTICONS = "enable_emoticons" + EXPIRE_BEHAVIOR = "expire_behavior" + EXPIRE_GRACE_PERIOD = "expire_grace_period" + RATE_LIMIT_PER_USER = "rate_limit_per_user" + SYSTEM_CHANNEL_ID = "system_channel_id" + + #: Alias for "COLOR" + COLOUR = COLOR + + def __str__(self) -> str: + return self.name + + __repr__ = __str__ + + +AUDIT_LOG_ENTRY_CONVERTERS = { + AuditLogChangeKey.OWNER_ID: snowflakes.Snowflake.deserialize, + AuditLogChangeKey.AFK_CHANNEL_ID: snowflakes.Snowflake.deserialize, + AuditLogChangeKey.AFK_TIMEOUT: lambda payload: datetime.timedelta(seconds=payload), + AuditLogChangeKey.MFA_LEVEL: guilds.GuildMFALevel, + AuditLogChangeKey.VERIFICATION_LEVEL: guilds.GuildVerificationLevel, + AuditLogChangeKey.EXPLICIT_CONTENT_FILTER: guilds.GuildExplicitContentFilterLevel, + AuditLogChangeKey.DEFAULT_MESSAGE_NOTIFICATIONS: guilds.GuildMessageNotificationsLevel, + AuditLogChangeKey.ADD_ROLE_TO_MEMBER: lambda payload: { + role.id: role for role in map(guilds.PartialGuildRole.deserialize, payload) + }, + AuditLogChangeKey.REMOVE_ROLE_FROM_MEMBER: lambda payload: { + role.id: role for role in map(guilds.PartialGuildRole.deserialize, payload) + }, + AuditLogChangeKey.PRUNE_DELETE_DAYS: lambda payload: datetime.timedelta(days=int(payload)), + AuditLogChangeKey.WIDGET_CHANNEL_ID: snowflakes.Snowflake.deserialize, + AuditLogChangeKey.POSITION: int, + AuditLogChangeKey.BITRATE: int, + AuditLogChangeKey.PERMISSION_OVERWRITES: lambda payload: { + overwrite.id: overwrite for overwrite in map(channels.PermissionOverwrite.deserialize, payload) + }, + AuditLogChangeKey.APPLICATION_ID: snowflakes.Snowflake.deserialize, + AuditLogChangeKey.PERMISSIONS: permissions.Permission, + AuditLogChangeKey.COLOR: colors.Color, + AuditLogChangeKey.ALLOW: permissions.Permission, + AuditLogChangeKey.DENY: permissions.Permission, + AuditLogChangeKey.CHANNEL_ID: snowflakes.Snowflake.deserialize, + AuditLogChangeKey.INVITER_ID: snowflakes.Snowflake.deserialize, + AuditLogChangeKey.MAX_USES: lambda payload: int(payload) if payload > 0 else float("inf"), + AuditLogChangeKey.USES: int, + AuditLogChangeKey.MAX_AGE: lambda payload: datetime.timedelta(seconds=payload) if payload > 0 else None, + AuditLogChangeKey.ID: snowflakes.Snowflake.deserialize, + AuditLogChangeKey.TYPE: str, + AuditLogChangeKey.ENABLE_EMOTICONS: bool, + AuditLogChangeKey.EXPIRE_BEHAVIOR: guilds.IntegrationExpireBehaviour, + AuditLogChangeKey.EXPIRE_GRACE_PERIOD: lambda payload: datetime.timedelta(days=payload), + AuditLogChangeKey.RATE_LIMIT_PER_USER: lambda payload: datetime.timedelta(seconds=payload), + AuditLogChangeKey.SYSTEM_CHANNEL_ID: snowflakes.Snowflake.deserialize, +} + + +@marshaller.attrs(slots=True) +class AuditLogChange(entities.HikariEntity, entities.Deserializable): + """Represents a change made to an audit log entry's target entity.""" + + #: The new value of the key, if something was added or changed. + #: + #: :type: :obj:`typing.Any`, optional + new_value: typing.Optional[typing.Any] = marshaller.attrib() + + #: The old value of the key, if something was removed or changed. + #: + #: :type: :obj:`typing.Any`, optional + old_value: typing.Optional[typing.Any] = marshaller.attrib() + + #: The name of the audit log change's key. + #: + #: :type: :obj:`typing.Union` [ :obj:`AuditLogChangeKey`, :obj:`str` ] + key: typing.Union[AuditLogChangeKey, str] = marshaller.attrib() + + @classmethod + def deserialize(cls, payload: entities.RawEntityT) -> "AuditLogChange": + """Deserialize this model from a raw payload.""" + key = conversions.try_cast(payload["key"], AuditLogChangeKey, payload["key"]) + new_value = payload.get("new_value") + old_value = payload.get("old_value") + if value_converter := AUDIT_LOG_ENTRY_CONVERTERS.get(key): + new_value = value_converter(new_value) if new_value is not None else None + old_value = value_converter(old_value) if old_value is not None else None + + return cls(key=key, new_value=new_value, old_value=old_value) + + +@enum.unique +class AuditLogEventType(enum.IntEnum): + """The type of event that occurred.""" + + GUILD_UPDATE = 1 + CHANNEL_CREATE = 10 + CHANNEL_UPDATE = 11 + CHANNEL_DELETE = 12 + CHANNEL_OVERWRITE_CREATE = 13 + CHANNEL_OVERWRITE_UPDATE = 14 + CHANNEL_OVERWRITE_DELETE = 15 + MEMBER_KICK = 20 + MEMBER_PRUNE = 21 + MEMBER_BAN_ADD = 22 + MEMBER_BAN_REMOVE = 23 + MEMBER_UPDATE = 24 + MEMBER_ROLE_UPDATE = 25 + MEMBER_MOVE = 26 + MEMBER_DISCONNECT = 27 + BOT_ADD = 28 + ROLE_CREATE = 30 + ROLE_UPDATE = 31 + ROLE_DELETE = 32 + INVITE_CREATE = 40 + INVITE_UPDATE = 41 + INVITE_DELETE = 42 + WEBHOOK_CREATE = 50 + WEBHOOK_UPDATE = 51 + WEBHOOK_DELETE = 52 + EMOJI_CREATE = 60 + EMOJI_UPDATE = 61 + EMOJI_DELETE = 62 + MESSAGE_DELETE = 72 + MESSAGE_BULK_DELETE = 73 + MESSAGE_PIN = 74 + MESSAGE_UNPIN = 75 + INTEGRATION_CREATE = 80 + INTEGRATION_UPDATE = 81 + INTEGRATION_DELETE = 82 + + +def register_audit_log_entry_info( + type_: AuditLogEventType, *additional_types: AuditLogEventType +) -> typing.Callable[[typing.Type["BaseAuditLogEntryInfo"]], typing.Type["BaseAuditLogEntryInfo"]]: + """Generates a decorator for defined audit log entry info entities. + + Allows them to be associated with given entry type(s). + + Parameters + ---------- + type_ : :obj:`AuditLogEventType` + An entry types to associate the entity with. + *additional_types : :obj:`AuditLogEventType` + Extra entry types to associate the entity with. + + Returns + ------- + ``decorator(cls: T) -> T`` + The decorator to decorate the class with. + """ + + def decorator(cls): + mapping = getattr(register_audit_log_entry_info, "types", {}) + for t in [type_, *additional_types]: + mapping[t] = cls + setattr(register_audit_log_entry_info, "types", mapping) + return cls + + return decorator + + +@marshaller.attrs(slots=True) +class BaseAuditLogEntryInfo(abc.ABC, entities.HikariEntity, entities.Deserializable): + """A base object that all audit log entry info objects will inherit from.""" + + +@register_audit_log_entry_info( + AuditLogEventType.CHANNEL_OVERWRITE_CREATE, + AuditLogEventType.CHANNEL_OVERWRITE_UPDATE, + AuditLogEventType.CHANNEL_OVERWRITE_DELETE, +) +@marshaller.attrs(slots=True) +class ChannelOverwriteEntryInfo(BaseAuditLogEntryInfo): + """Represents the extra information for overwrite related audit log entries. + + Will be attached to the overwrite create, update and delete audit log + entries. + """ + + #: The ID of the overwrite being updated, added or removed (and the entity + #: it targets). + #: + #: :type: :obj:`hikari.core.snowflakes.Snowflake` + id: snowflakes.Snowflake = marshaller.attrib(deserializer=snowflakes.Snowflake.deserialize) + + #: The type of entity this overwrite targets. + #: + #: :type: :obj:`hikari.core.channels.PermissionOverwriteType` + type: channels.PermissionOverwriteType = marshaller.attrib(deserializer=channels.PermissionOverwriteType) + + #: The name of the role this overwrite targets, if it targets a role. + #: + #: :type: :obj:`str`, optional + role_name: typing.Optional[str] = marshaller.attrib(deserializer=str, if_undefined=None) + + +@register_audit_log_entry_info(AuditLogEventType.MESSAGE_PIN, AuditLogEventType.MESSAGE_UNPIN) +@marshaller.attrs(slots=True) +class MessagePinEntryInfo(BaseAuditLogEntryInfo): + """The extra information for message pin related audit log entries. + + Will be attached to the message pin and message unpin audit log entries. + """ + + #: The ID of the guild text based channel where this pinned message is + #: being added or removed. + #: + #: :type: :obj:`hikari.core.snowflakes.Snowflake` + channel_id: snowflakes.Snowflake = marshaller.attrib(deserializer=snowflakes.Snowflake.deserialize) + + #: The ID of the message that's being pinned or unpinned. + #: + #: :type: :obj:`hikari.core.snowflakes.Snowflake` + message_id: snowflakes.Snowflake = marshaller.attrib(deserializer=snowflakes.Snowflake.deserialize) + + +@register_audit_log_entry_info(AuditLogEventType.MEMBER_PRUNE) +@marshaller.attrs(slots=True) +class MemberPruneEntryInfo(BaseAuditLogEntryInfo): + """Represents the extra information attached to guild prune log entries.""" + + #: The timedelta of how many days members were pruned for inactivity based + #: on. + #: + #: :type: :obj:`datetime.timedelta` + delete_member_days: datetime.timedelta = marshaller.attrib( + deserializer=lambda payload: datetime.timedelta(days=int(payload)) + ) + + #: The number of members who were removed by this prune. + #: + #: :type: :obj:`int` + members_removed: int = marshaller.attrib(deserializer=int) + + +@register_audit_log_entry_info(AuditLogEventType.MESSAGE_BULK_DELETE) +@marshaller.attrs(slots=True) +class MessageBulkDeleteEntryInfo(BaseAuditLogEntryInfo): + """Represents extra information for the message bulk delete audit entry. + """ + + #: The amount of messages that were deleted. + #: + #: :type: :obj:`int` + count: int = marshaller.attrib(deserializer=int) + + +@register_audit_log_entry_info(AuditLogEventType.MESSAGE_DELETE) +@marshaller.attrs(slots=True) +class MessageDeleteEntryInfo(MessageBulkDeleteEntryInfo): + """Represents extra information attached to the message delete audit entry. + """ + + #: The guild text based channel where these message(s) were deleted. + #: + #: :type: :obj:`hikari.core.snowflakes.Snowflake` + channel_id: snowflakes.Snowflake = marshaller.attrib(deserializer=snowflakes.Snowflake.deserialize) + + +@register_audit_log_entry_info(AuditLogEventType.MEMBER_DISCONNECT) +@marshaller.attrs(slots=True) +class MemberDisconnectEntryInfo(BaseAuditLogEntryInfo): + """Represents extra information for the voice chat member disconnect entry. + """ + + #: The amount of members who were disconnected from voice in this entry. + #: + #: :type: :obj:`int` + count: int = marshaller.attrib(deserializer=int) + + +@register_audit_log_entry_info(AuditLogEventType.MEMBER_MOVE) +@marshaller.attrs(slots=True) +class MemberMoveEntryInfo(MemberDisconnectEntryInfo): + """Represents extra information for the voice chat based member move entry. + """ + + #: The channel these member(s) were moved to. + #: + #: :type: :obj:`hikari.core.snowflakes.Snowflake` + channel_id: snowflakes.Snowflake = marshaller.attrib(deserializer=snowflakes.Snowflake.deserialize) + + +class UnrecognisedAuditLogEntryInfo(BaseAuditLogEntryInfo): + """Represents any audit log entry options that haven't been implemented.""" + + def __init__(self, payload: entities.RawEntityT) -> None: + self.__dict__.update(payload) + + @classmethod + def deserialize(cls, payload: entities.RawEntityT) -> "UnrecognisedAuditLogEntryInfo": + return cls(payload) + + +def get_entry_info_entity(type_: int) -> typing.Type[BaseAuditLogEntryInfo]: + """Get the entity that's registered for an entry's options. + + Parameters + ---------- + :obj:`int` + The int + + Returns + ------- + :obj:`typing.Type` [ :obj:`BaseAuditLogEntryInfo` ] + The associated options entity. If not implemented then this will be + :obj:`UnrecognisedAuditLogEntryInfo` + """ + return register_audit_log_entry_info.types.get(type_) or UnrecognisedAuditLogEntryInfo + + +@marshaller.attrs(slots=True) +class AuditLogEntry(snowflakes.UniqueEntity, entities.Deserializable): + """Represents an entry in a guild's audit log.""" + + #: The ID of the entity affected by this change, if applicable. + #: + #: :type: :obj:`hikari.core.snowflakes.Snowflake`, optional + target_id: typing.Optional[snowflakes.Snowflake] = marshaller.attrib() + + #: A sequence of the changes made to :attr:`target_id` + #: + #: :type: :obj:`typing.Sequence` [ :obj:`AuditLogChange` ] + changes: typing.Sequence[AuditLogChange] = marshaller.attrib() + + #: The ID of the user who made this change. + #: + #: :type: :obj:`hikari.core.snowflakes.Snowflake` + user_id: snowflakes.Snowflake = marshaller.attrib() + + #: The ID of this entry. + #: + #: :type: :obj:`hikari.core.snowflakes.Snowflake` + id: snowflakes.Snowflake = marshaller.attrib() + + #: The type of action this entry represents. + #: + #: :type: :obj:`typing.Union` [ :obj:`AuditLogEventType`, :obj:`str` ] + action_type: typing.Union[AuditLogEventType, str] = marshaller.attrib() + + #: Extra information about this entry. Will only be provided for certain + #: :attr:`action_type`. + #: + #: :type: :obj:`BaseAuditLogEntryInfo`, optional + options: typing.Optional[BaseAuditLogEntryInfo] = marshaller.attrib() + + #: The reason for this change, if set (between 0-512 characters). + #: + #: :type: :obj:`str` + reason: typing.Optional[str] = marshaller.attrib() + + @classmethod + def deserialize(cls, payload: entities.RawEntityT) -> "AuditLogEntry": + """Deserialize this model from a raw payload.""" + action_type = conversions.try_cast(payload["action_type"], AuditLogEventType, payload["action_type"]) + if target_id := payload.get("target_id"): + target_id = snowflakes.Snowflake.deserialize(target_id) + + if (options := payload.get("options")) is not None: + if option_converter := get_entry_info_entity(action_type): + options = option_converter.deserialize(options) + + return cls( + target_id=target_id, + changes=[ + AuditLogChange.deserialize(payload) + for payload in payload.get("changes", more_collections.EMPTY_SEQUENCE) + ], + user_id=snowflakes.Snowflake.deserialize(payload["user_id"]), + id=snowflakes.Snowflake.deserialize(payload["id"]), + action_type=action_type, + options=options, + reason=payload.get("reason"), + ) + + +@marshaller.attrs(slots=True) +class AuditLog(entities.HikariEntity, entities.Deserializable): + """Represents a guilds audit log.""" + + #: A sequence of the audit log's entries. + #: + #: :type: :obj:`typing.Mapping` [ :obj:`hikari.core.snowflakes.Snowflake`, :obj:`AuditLogEntry` ] + entries: typing.Mapping[snowflakes.Snowflake, AuditLogEntry] = marshaller.attrib( + raw_name="audit_log_entries", + deserializer=lambda payload: {entry.id: entry for entry in map(AuditLogEntry.deserialize, payload)}, + ) + + #: A mapping of the partial objects of integrations found in this audit log. + #: + #: :type: :obj:`typing.Mapping` [ :obj:`hikari.core.snowflakes.Snowflake`, :obj:`hikari.core.guilds.GuildIntegration` ] + integrations: typing.Mapping[snowflakes.Snowflake, guilds.GuildIntegration] = marshaller.attrib( + deserializer=lambda payload: { + integration.id: integration for integration in map(guilds.PartialGuildIntegration.deserialize, payload) + } + ) + + #: A mapping of the objects of users found in this audit log. + #: + #: :type: :obj:`typing.Mapping` [ :obj:`hikari.core.snowflakes.Snowflake`, :obj:`hikari.core.users.User` ] + users: typing.Mapping[snowflakes.Snowflake, _users.User] = marshaller.attrib( + deserializer=lambda payload: {user.id: user for user in map(_users.User.deserialize, payload)} + ) + + #: A mapping of the objects of webhooks found in this audit log. + #: + #: :type: :obj:`typing.Mapping` [ :obj:`hikari.core.snowflakes.Snowflake`, :obj:`hikari.core.webhooks.Webhook` ] + webhooks: typing.Mapping[snowflakes.Snowflake, _webhooks.Webhook] = marshaller.attrib( + deserializer=lambda payload: {webhook.id: webhook for webhook in map(_webhooks.Webhook.deserialize, payload)} + ) diff --git a/hikari/core/channels.py b/hikari/core/channels.py index bd31aa9f7a..a8d83c1277 100644 --- a/hikari/core/channels.py +++ b/hikari/core/channels.py @@ -44,6 +44,7 @@ "GuildVoiceChannel", ] +import datetime import enum import typing @@ -283,8 +284,10 @@ class GuildTextChannel(GuildChannel): #: Bots, as well as users with ``MANAGE_MESSAGES`` or #: ``MANAGE_CHANNEL``, are not afected by this. #: - #: :type: :obj:`int` - rate_limit_per_user: int = marshaller.attrib(deserializer=int) + #: :type: :obj:`datetime.timedelta` + rate_limit_per_user: datetime.timedelta = marshaller.attrib( + deserializer=lambda payload: datetime.timedelta(seconds=payload) + ) @register_channel_type(ChannelType.GUILD_NEWS) diff --git a/hikari/core/guilds.py b/hikari/core/guilds.py index 8ead6ca8f5..eabf3833f3 100644 --- a/hikari/core/guilds.py +++ b/hikari/core/guilds.py @@ -35,6 +35,8 @@ "GuildIntegration", "GuildMemberBan", "PartialGuild", + "PartialGuildIntegration", + "PartialGuildRole", "PresenceStatus", ] @@ -223,14 +225,19 @@ class GuildMember(entities.HikariEntity, entities.Deserializable): @marshaller.attrs(slots=True) -class GuildRole(snowflakes.UniqueEntity, entities.Deserializable): - """Represents a guild bound Role object.""" +class PartialGuildRole(snowflakes.UniqueEntity, entities.Deserializable): + """Represents a partial guild bound Role object.""" - #: The role's name + #: The role's name. #: #: :type: :obj:`str` name: str = marshaller.attrib(deserializer=str) + +@marshaller.attrs(slots=True) +class GuildRole(PartialGuildRole): + """Represents a guild bound Role object.""" + #: The colour of this role, will be applied to a member's name in chat #: if it's their top coloured role. #: diff --git a/hikari/core/oauth2.py b/hikari/core/oauth2.py index a7d3d59207..99ea358b74 100644 --- a/hikari/core/oauth2.py +++ b/hikari/core/oauth2.py @@ -17,7 +17,16 @@ # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . """Components and entities related to discord's Oauth2 flow.""" -__all__ = ["Application", "Owner", "OwnGuild", "Team", "TeamMember", "TeamMembershipState"] +__all__ = [ + "Application", + "ConnectionVisibility", + "Owner", + "OwnConnection", + "OwnGuild", + "Team", + "TeamMember", + "TeamMembershipState", +] import enum import typing @@ -31,10 +40,81 @@ from hikari.core import users +@enum.unique +class ConnectionVisibility(enum.IntEnum): + NONE = 0 + EVERYONE = 1 + + +@marshaller.attrs(slots=True) +class OwnConnection(entities.HikariEntity, entities.Deserializable): + """Represents a user's connection with a third party account. + + Returned by the ``GET Current User Connections`` endpoint. + """ + + #: The string ID of the third party connected account. + #: + #: Warning + #: ------- + #: Seeing as this is a third party ID, it will not be a snowflake. + #: + #: + #: :type: :obj:`str` + id: str = marshaller.attrib(deserializer=str) + + #: The username of the connected account. + #: + #: :type: :obj:`str` + name: str = marshaller.attrib(deserializer=str) + + #: The type of service this connection is for. + #: + #: :type: :obj:`str` + type: str = marshaller.attrib(deserializer=str) + + #: Whether the connection has been revoked. + #: + #: :type: :obj:`bool` + is_revoked: bool = marshaller.attrib(raw_name="revoked", deserializer=bool, if_undefined=False) + + #: A sequence of the partial guild integration objects this connection has. + #: + #: :type: :obj:`typing.Sequence` [ :obj:`guilds.PartialGuildIntegration` ] + integrations: typing.Sequence[guilds.PartialGuildIntegration] = marshaller.attrib( + deserializer=lambda payload: [ + guilds.PartialGuildIntegration.deserialize(integration) for integration in payload + ], + if_undefined=list, + ) + + #: Whether the connection has been verified. + #: + #: :type: :obj:`bool` + is_verified: bool = marshaller.attrib(raw_name="verified", deserializer=bool) + + #: Whether friends should be added based on this connection. + #: + #: :type: :obj:`bool` + is_friend_syncing: bool = marshaller.attrib(raw_name="friend_sync", deserializer=bool) + + #: Whether activities related to this connection will be shown in the + #: user's presence updates. + #: + #: :type: :obj:`bool` + is_showing_activity: bool = marshaller.attrib(raw_name="show_activity", deserializer=bool) + + #: The visibility of the connection. + #: + #: :type: :obj:`ConnectionVisibility` + visibility: ConnectionVisibility = marshaller.attrib(deserializer=ConnectionVisibility) + + @marshaller.attrs(slots=True) class OwnGuild(guilds.PartialGuild): - """Represents a user bound partial guild object, - returned by GET Current User Guilds. + """Represents a user bound partial guild object. + + Returned by the ``GET Current User Guilds`` endpoint. """ #: Whether the current user owns this guild. @@ -128,7 +208,7 @@ def format_icon_url(self, fmt: str = "png", size: int = 2048) -> typing.Optional Returns ------- :obj:`str`, optional - The string url. + The string url, will be :obj:`None` if not set. """ if self.icon_hash: return cdn.generate_cdn_url("team-icons", str(self.id), self.icon_hash, fmt=fmt, size=size) @@ -141,12 +221,14 @@ class Owner(users.User): #: This user's flags. #: - #: :type: :obj:`int` - flags: int = marshaller.attrib(deserializer=int) + #: :type: :obj:`hikari.core.users.UserFlag` + flags: int = marshaller.attrib(deserializer=users.UserFlag) @property def is_team_user(self) -> bool: - """If this user is a Team user (the owner of an application that's owned by a team).""" + """If this user is a Team user (the owner of an application that's + owned by a team). + """ return bool((self.flags >> 10) & 1) @@ -205,7 +287,7 @@ class Application(snowflakes.UniqueEntity, entities.Deserializable): deserializer=lambda key: bytes(key, "utf-8"), if_undefined=None ) - #: The hash of this application's icon if set. + #: The hash of this application's icon, if set. #: #: :type: :obj:`str`, optional icon_hash: typing.Optional[str] = marshaller.attrib(raw_name="icon", deserializer=str, if_undefined=None) diff --git a/tests/hikari/core/test_audit_logs.py b/tests/hikari/core/test_audit_logs.py new file mode 100644 index 0000000000..25e6df6df1 --- /dev/null +++ b/tests/hikari/core/test_audit_logs.py @@ -0,0 +1,421 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# Copyright © Nekoka.tt 2019-2020 +# +# This file is part of Hikari. +# +# Hikari is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Hikari is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along ith Hikari. If not, see . +import datetime +import contextlib + +import cymock as mock +import pytest + +from hikari.core import audit_logs +from hikari.core import channels +from hikari.core import guilds +from hikari.core import users +from hikari.core import webhooks + + +class TestAuditLogChangeKey: + def test___str__(self): + assert str(audit_logs.AuditLogChangeKey.ID) == "ID" + + def test___repr__(self): + assert repr(audit_logs.AuditLogChangeKey.ID) == "ID" + + +def test_audit_log_afk_timeout_entry_lambda(): + result = audit_logs.AUDIT_LOG_ENTRY_CONVERTERS[audit_logs.AuditLogChangeKey.AFK_TIMEOUT](30) + assert result == datetime.timedelta(seconds=30) + + +def test_audit_log_add_role_to_member_entry_lambda(): + test_role_payloads = [ + {"id": "24", "name": "roleA", "hoisted": True}, + {"id": "24", "name": "roleA", "hoisted": True}, + ] + mock_role_objs = [mock.MagicMock(guilds.PartialGuildRole, id=24), mock.MagicMock(guilds.PartialGuildRole, id=48)] + with mock.patch.object(guilds.PartialGuildRole, "deserialize", side_effect=mock_role_objs): + result = audit_logs.AUDIT_LOG_ENTRY_CONVERTERS[audit_logs.AuditLogChangeKey.ADD_ROLE_TO_MEMBER]( + test_role_payloads + ) + assert result == {24: mock_role_objs[0], 48: mock_role_objs[1]} + guilds.PartialGuildRole.deserialize.assert_has_calls( + [mock.call(test_role_payloads[0]), mock.call(test_role_payloads[1])] + ) + + +def test_audit_log_remove_role_from_member_entry_lambda(): + test_role_payloads = [ + {"id": "24", "name": "roleA", "hoisted": True}, + {"id": "24", "name": "roleA", "hoisted": True}, + ] + mock_role_objs = [mock.MagicMock(guilds.PartialGuildRole, id=24), mock.MagicMock(guilds.PartialGuildRole, id=48)] + with mock.patch.object(guilds.PartialGuildRole, "deserialize", side_effect=mock_role_objs): + result = audit_logs.AUDIT_LOG_ENTRY_CONVERTERS[audit_logs.AuditLogChangeKey.REMOVE_ROLE_FROM_MEMBER]( + test_role_payloads + ) + assert result == {24: mock_role_objs[0], 48: mock_role_objs[1]} + guilds.PartialGuildRole.deserialize.assert_has_calls( + [mock.call(test_role_payloads[0]), mock.call(test_role_payloads[1])] + ) + + +def test_audit_log_prune_delete_days_entry_lambda(): + result = audit_logs.AUDIT_LOG_ENTRY_CONVERTERS[audit_logs.AuditLogChangeKey.PRUNE_DELETE_DAYS]("4") + assert result == datetime.timedelta(days=4) + + +def test_audit_log_permission_overwrites_entry_lambda(): + test_overwrite_payloads = [{"id": "24", "allow": 21, "deny": 0}, {"id": "24", "deny": 42, "allow": 0}] + mock_overwrite_objs = [ + mock.MagicMock(guilds.PartialGuildRole, id=24), + mock.MagicMock(guilds.PartialGuildRole, id=48), + ] + with mock.patch.object(channels.PermissionOverwrite, "deserialize", side_effect=mock_overwrite_objs): + result = audit_logs.AUDIT_LOG_ENTRY_CONVERTERS[audit_logs.AuditLogChangeKey.PERMISSION_OVERWRITES]( + test_overwrite_payloads + ) + assert result == {24: mock_overwrite_objs[0], 48: mock_overwrite_objs[1]} + channels.PermissionOverwrite.deserialize.assert_has_calls( + [mock.call(test_overwrite_payloads[0]), mock.call(test_overwrite_payloads[1])] + ) + + +def test_audit_log_max_uses_entry_lambda_returns_int(): + assert audit_logs.AUDIT_LOG_ENTRY_CONVERTERS[audit_logs.AuditLogChangeKey.MAX_USES](120) == 120 + + +def test_audit_log_max_uses_entry_lambda_returns_infinity(): + assert audit_logs.AUDIT_LOG_ENTRY_CONVERTERS[audit_logs.AuditLogChangeKey.MAX_USES](0) == float("inf") + + +def test_audit_log_max_age_entry_lambda_returns_timedelta(): + result = audit_logs.AUDIT_LOG_ENTRY_CONVERTERS[audit_logs.AuditLogChangeKey.MAX_AGE](120) + assert result == datetime.timedelta(seconds=120) + + +def test_audit_log_max_age_entry_lambda_returns_null(): + assert audit_logs.AUDIT_LOG_ENTRY_CONVERTERS[audit_logs.AuditLogChangeKey.MAX_AGE](0) is None + + +def test_audit_log_expire_grace_period_entry_lambda(): + result = audit_logs.AUDIT_LOG_ENTRY_CONVERTERS[audit_logs.AuditLogChangeKey.EXPIRE_GRACE_PERIOD](7) + assert result == datetime.timedelta(days=7) + + +def test_audit_log_rate_limit_per_user_entry_lambda(): + result = audit_logs.AUDIT_LOG_ENTRY_CONVERTERS[audit_logs.AuditLogChangeKey.RATE_LIMIT_PER_USER](3600) + assert result == datetime.timedelta(seconds=3600) + + +class TestAuditLogChange: + @pytest.fixture() + def test_audit_log_change_payload(self): + return { + "key": "$add", + "old_value": [{"id": "568651298858074123", "name": "Casual"}], + "new_value": [{"id": "123123123312312", "name": "aRole"}], + } + + def test_deserialize_with_known_converter_and_values(self, test_audit_log_change_payload): + mock_role_zero = mock.MagicMock(guilds.PartialGuildRole, id=123123) + mock_role_one = mock.MagicMock(guilds.PartialGuildRole, id=234234) + with mock.patch.object(guilds.PartialGuildRole, "deserialize", side_effect=[mock_role_zero, mock_role_one]): + audit_log_change_obj = audit_logs.AuditLogChange.deserialize(test_audit_log_change_payload) + guilds.PartialGuildRole.deserialize.assert_has_calls( + [ + mock.call({"id": "123123123312312", "name": "aRole"}), + mock.call({"id": "568651298858074123", "name": "Casual"}), + ] + ) + assert audit_log_change_obj.key is audit_logs.AuditLogChangeKey.ADD_ROLE_TO_MEMBER + assert audit_log_change_obj.old_value == {234234: mock_role_one} + assert audit_log_change_obj.new_value == {123123: mock_role_zero} + + def test_deserialize_with_known_converter_and_no_values(self, test_audit_log_change_payload): + del test_audit_log_change_payload["old_value"] + del test_audit_log_change_payload["new_value"] + with mock.patch.object(guilds.PartialGuildRole, "deserialize"): + audit_log_change_obj = audit_logs.AuditLogChange.deserialize(test_audit_log_change_payload) + guilds.PartialGuildRole.deserialize.assert_not_called() + assert audit_log_change_obj.key is audit_logs.AuditLogChangeKey.ADD_ROLE_TO_MEMBER + assert audit_log_change_obj.old_value is None + assert audit_log_change_obj.new_value is None + + def test_deserialize_with_unknown_converter_and_values(self, test_audit_log_change_payload): + test_audit_log_change_payload["key"] = "aUnknownKey" + audit_log_change_obj = audit_logs.AuditLogChange.deserialize(test_audit_log_change_payload) + assert audit_log_change_obj.key == "aUnknownKey" + assert audit_log_change_obj.old_value == test_audit_log_change_payload["old_value"] + assert audit_log_change_obj.new_value == test_audit_log_change_payload["new_value"] + + def test_deserialize_with_unknown_converter_and_no_values(self, test_audit_log_change_payload): + test_audit_log_change_payload["key"] = "aUnknownKey" + del test_audit_log_change_payload["old_value"] + del test_audit_log_change_payload["new_value"] + audit_log_change_obj = audit_logs.AuditLogChange.deserialize(test_audit_log_change_payload) + assert audit_log_change_obj.key == "aUnknownKey" + assert audit_log_change_obj.old_value is None + assert audit_log_change_obj.new_value is None + + +class TestChannelOverwriteEntryInfo: + @pytest.fixture() + def test_overwrite_info_payload(self): + return {"id": "123123123", "type": "role", "role_name": "aRole"} + + def test_deserialize(self, test_overwrite_info_payload): + overwrite_entry_info = audit_logs.ChannelOverwriteEntryInfo.deserialize(test_overwrite_info_payload) + assert overwrite_entry_info.id == 123123123 + assert overwrite_entry_info.type is channels.PermissionOverwriteType.ROLE + assert overwrite_entry_info.role_name == "aRole" + + +class TestMessagePinEntryInfo: + @pytest.fixture() + def test_message_pin_info_payload(self): + return { + "channel_id": "123123123", + "message_id": "69696969", + } + + def test_deserialize(self, test_message_pin_info_payload): + message_pin_info_obj = audit_logs.MessagePinEntryInfo.deserialize(test_message_pin_info_payload) + assert message_pin_info_obj.channel_id == 123123123 + assert message_pin_info_obj.message_id == 69696969 + + +class TestMemberPruneEntryInfo: + @pytest.fixture() + def test_member_prune_info_payload(self): + return { + "delete_member_days": "7", + "members_removed": "1", + } + + def test_deserialize(self, test_member_prune_info_payload): + member_prune_info_obj = audit_logs.MemberPruneEntryInfo.deserialize(test_member_prune_info_payload) + assert member_prune_info_obj.delete_member_days == datetime.timedelta(days=7) + assert member_prune_info_obj.members_removed == 1 + + +class TestMessageDeleteEntryInfo: + @pytest.fixture() + def test_message_delete_info_payload(self): + return {"count": "42", "channel_id": "4206942069"} + + def test_deserialize(self, test_message_delete_info_payload): + assert audit_logs.MessageDeleteEntryInfo.deserialize(test_message_delete_info_payload).channel_id == 4206942069 + + +class TestMessageBulkDeleteEntryInfo: + @pytest.fixture() + def test_message_bulk_delete_info_payload(self): + return {"count": "42"} + + def test_deserialize(self, test_message_bulk_delete_info_payload): + assert audit_logs.MessageBulkDeleteEntryInfo.deserialize(test_message_bulk_delete_info_payload).count == 42 + + +class TestMemberDisconnectEntryInfo: + @pytest.fixture() + def test_member_disconnect_info_payload(self): + return {"count": "42"} + + def test_deserialize(self, test_member_disconnect_info_payload): + assert audit_logs.MemberDisconnectEntryInfo.deserialize(test_member_disconnect_info_payload).count == 42 + + +class TestMemberMoveEntryInfo: + @pytest.fixture() + def test_member_move_info_payload(self): + return {"count": "42", "channel_id": "22222222"} + + def test_deserialize(self, test_member_move_info_payload): + assert audit_logs.MemberMoveEntryInfo.deserialize(test_member_move_info_payload).channel_id == 22222222 + + +class TestUnrecognisedAuditLogEntryInfo: + @pytest.fixture() + def test_unrecognised_audit_log_entry(self): + return {"count": "5412", "action": "nyaa'd"} + + def test_deserialize(self, test_unrecognised_audit_log_entry): + unrecognised_info_obj = audit_logs.UnrecognisedAuditLogEntryInfo(test_unrecognised_audit_log_entry) + assert unrecognised_info_obj.count == "5412" + assert unrecognised_info_obj.action == "nyaa'd" + + +@pytest.mark.parametrize( + ("type_", "expected_entity"), + [ + (13, audit_logs.ChannelOverwriteEntryInfo), + (14, audit_logs.ChannelOverwriteEntryInfo), + (15, audit_logs.ChannelOverwriteEntryInfo), + (74, audit_logs.MessagePinEntryInfo), + (75, audit_logs.MessagePinEntryInfo), + (21, audit_logs.MemberPruneEntryInfo), + (72, audit_logs.MessageDeleteEntryInfo), + (73, audit_logs.MessageBulkDeleteEntryInfo), + (27, audit_logs.MemberDisconnectEntryInfo), + (26, audit_logs.MemberMoveEntryInfo), + ], +) +def test_get_audit_log_entry_info_entity(type_, expected_entity): + assert audit_logs.get_entry_info_entity(type_) is expected_entity + + +@pytest.fixture() +def test_audit_log_change_payload(): + return {"key": "deny", "new_value": 0, "old_value": 42} + + +@pytest.fixture() +def test_audit_log_option_payload(): + return { + "id": "115590097100865541", + "type": "member", + } + + +@pytest.fixture() +def test_audit_log_entry_payload(test_audit_log_change_payload, test_audit_log_option_payload): + return { + "action_type": 14, + "changes": [test_audit_log_change_payload], + "id": "694026906592477214", + "options": test_audit_log_option_payload, + "target_id": "115590097100865541", + "user_id": "560984860634644482", + "reason": "An artificial insanity.", + } + + +class TestAuditLogEntry: + def test_deserialize_with_options_and_target_id_and_known_type( + self, test_audit_log_entry_payload, test_audit_log_option_payload, test_audit_log_change_payload + ): + audit_log_entry_obj = audit_logs.AuditLogEntry.deserialize(test_audit_log_entry_payload) + assert audit_log_entry_obj.changes == [audit_logs.AuditLogChange.deserialize(test_audit_log_change_payload)] + assert audit_log_entry_obj.options == audit_logs.ChannelOverwriteEntryInfo.deserialize( + test_audit_log_option_payload + ) + assert audit_log_entry_obj.target_id == 115590097100865541 + assert audit_log_entry_obj.user_id == 560984860634644482 + assert audit_log_entry_obj.id == 694026906592477214 + assert audit_log_entry_obj.action_type is audit_logs.AuditLogEventType.CHANNEL_OVERWRITE_UPDATE + assert audit_log_entry_obj.reason == "An artificial insanity." + + def test_deserialize_with_known_type_without_options_or_target_( + self, test_audit_log_entry_payload, test_audit_log_change_payload + ): + del test_audit_log_entry_payload["options"] + del test_audit_log_entry_payload["target_id"] + audit_log_entry_obj = audit_logs.AuditLogEntry.deserialize(test_audit_log_entry_payload) + assert audit_log_entry_obj.changes == [audit_logs.AuditLogChange.deserialize(test_audit_log_change_payload)] + assert audit_log_entry_obj.options is None + assert audit_log_entry_obj.target_id is None + assert audit_log_entry_obj.user_id == 560984860634644482 + assert audit_log_entry_obj.id == 694026906592477214 + assert audit_log_entry_obj.action_type is audit_logs.AuditLogEventType.CHANNEL_OVERWRITE_UPDATE + assert audit_log_entry_obj.reason == "An artificial insanity." + + def test_deserialize_with_options_and_target_id_and_unknown_type( + self, test_audit_log_entry_payload, test_audit_log_option_payload, test_audit_log_change_payload + ): + test_audit_log_entry_payload["action_type"] = 123123123 + audit_log_entry_obj = audit_logs.AuditLogEntry.deserialize(test_audit_log_entry_payload) + assert audit_log_entry_obj.changes == [audit_logs.AuditLogChange.deserialize(test_audit_log_change_payload)] + assert audit_log_entry_obj.options == audit_logs.UnrecognisedAuditLogEntryInfo.deserialize( + test_audit_log_option_payload + ) + assert audit_log_entry_obj.target_id == 115590097100865541 + assert audit_log_entry_obj.user_id == 560984860634644482 + assert audit_log_entry_obj.id == 694026906592477214 + assert audit_log_entry_obj.action_type == 123123123 + assert audit_log_entry_obj.reason == "An artificial insanity." + + def test_deserialize_without_options_or_target_id_and_unknown_type( + self, test_audit_log_entry_payload, test_audit_log_option_payload, test_audit_log_change_payload + ): + del test_audit_log_entry_payload["options"] + del test_audit_log_entry_payload["target_id"] + test_audit_log_entry_payload["action_type"] = 123123123 + audit_log_entry_obj = audit_logs.AuditLogEntry.deserialize(test_audit_log_entry_payload) + assert audit_log_entry_obj.changes == [audit_logs.AuditLogChange.deserialize(test_audit_log_change_payload)] + assert audit_log_entry_obj.options is None + assert audit_log_entry_obj.target_id is None + assert audit_log_entry_obj.user_id == 560984860634644482 + assert audit_log_entry_obj.id == 694026906592477214 + assert audit_log_entry_obj.action_type == 123123123 + assert audit_log_entry_obj.reason == "An artificial insanity." + + +class TestAuditLog: + @pytest.fixture() + def test_integration_payload(self): + return {"id": 33590653072239123, "name": "A Name", "type": "twitch", "account": {}} + + @pytest.fixture() + def test_user_payload(self): + return {"id": "92929292", "username": "A USER", "discriminator": "6969", "avatar": None} + + @pytest.fixture() + def test_webhook_payload(self): + return {"id": "424242", "type": 1, "channel_id": "2020202"} + + @pytest.fixture() + def test_audit_log_payload( + self, test_audit_log_entry_payload, test_integration_payload, test_user_payload, test_webhook_payload + ): + return { + "audit_log_entries": [test_audit_log_entry_payload], + "integrations": [test_integration_payload], + "users": [test_user_payload], + "webhooks": [test_webhook_payload], + } + + def test_deserialize( + self, + test_audit_log_payload, + test_audit_log_entry_payload, + test_integration_payload, + test_user_payload, + test_webhook_payload, + ): + mock_webhook_obj = mock.MagicMock(webhooks.Webhook, id=123) + mock_user_obj = mock.MagicMock(users.User, id=345) + mock_integration_obj = mock.MagicMock(guilds.PartialGuildIntegration, id=234) + + stack = contextlib.ExitStack() + stack.enter_context(mock.patch.object(webhooks.Webhook, "deserialize", return_value=mock_webhook_obj)) + stack.enter_context(mock.patch.object(users.User, "deserialize", return_value=mock_user_obj)) + stack.enter_context( + mock.patch.object(guilds.PartialGuildIntegration, "deserialize", return_value=mock_integration_obj) + ) + + with stack: + audit_log_obj = audit_logs.AuditLog.deserialize(test_audit_log_payload) + webhooks.Webhook.deserialize.assert_called_once_with(test_webhook_payload) + users.User.deserialize.assert_called_once_with(test_user_payload) + guilds.PartialGuildIntegration.deserialize.assert_called_once_with(test_integration_payload) + assert audit_log_obj.entries == { + 694026906592477214: audit_logs.AuditLogEntry.deserialize(test_audit_log_entry_payload) + } + assert audit_log_obj.webhooks == {123: mock_webhook_obj} + assert audit_log_obj.users == {345: mock_user_obj} + assert audit_log_obj.integrations == {234: mock_integration_obj} diff --git a/tests/hikari/core/test_channels.py b/tests/hikari/core/test_channels.py index c5657fd19c..319444aa36 100644 --- a/tests/hikari/core/test_channels.py +++ b/tests/hikari/core/test_channels.py @@ -16,13 +16,14 @@ # # You should have received a copy of the GNU Lesser General Public License # along ith Hikari. If not, see . +import datetime + import cymock as mock import pytest from hikari.core import channels from hikari.core import permissions from hikari.core import users -from tests.hikari import _helpers @pytest.fixture() @@ -224,7 +225,7 @@ def test_deserialize(self, test_guild_text_channel_payload, test_permission_over assert channel_obj.parent_id == 987 assert channel_obj.type == channels.ChannelType.GUILD_TEXT assert channel_obj.last_message_id == 123456 - assert channel_obj.rate_limit_per_user == 2 + assert channel_obj.rate_limit_per_user == datetime.timedelta(seconds=2) class TestGuildNewsChannel: diff --git a/tests/hikari/core/test_guilds.py b/tests/hikari/core/test_guilds.py index 0dfef2bbcc..6dcc3d753c 100644 --- a/tests/hikari/core/test_guilds.py +++ b/tests/hikari/core/test_guilds.py @@ -288,6 +288,19 @@ def test_deserialize(self, test_member_payload, test_user_payload): assert guild_member_obj.is_mute is True +class TestPartialGuildRole: + @pytest.fixture() + def test_partial_guild_role_payload(self): + return { + "id": "41771983423143936", + "name": "WE DEM BOYZZ!!!!!!", + } + + def test_deserialize(self, test_partial_guild_role_payload): + partial_guild_role_obj = guilds.PartialGuildRole.deserialize(test_partial_guild_role_payload) + assert partial_guild_role_obj.name == "WE DEM BOYZZ!!!!!!" + + class TestGuildRole: @pytest.fixture() def test_guild_role_payload(self): @@ -304,7 +317,6 @@ def test_guild_role_payload(self): def test_deserialize(self, test_guild_role_payload): guild_role_obj = guilds.GuildRole.deserialize(test_guild_role_payload) - assert guild_role_obj.name == "WE DEM BOYZZ!!!!!!" assert guild_role_obj.color == 3_447_003 assert guild_role_obj.is_hoisted is True assert guild_role_obj.position == 0 diff --git a/tests/hikari/core/test_oauth2.py b/tests/hikari/core/test_oauth2.py index 8dec8e6fa4..1d0f9bd70e 100644 --- a/tests/hikari/core/test_oauth2.py +++ b/tests/hikari/core/test_oauth2.py @@ -20,11 +20,37 @@ import pytest from hikari.internal import cdn +from hikari.core import guilds from hikari.core import oauth2 from hikari.core import users from tests.hikari import _helpers +@pytest.fixture() +def test_partial_integration(): + return { + "id": "123123123123123", + "name": "A Name", + "type": "twitch", + "account": {"name": "twitchUsername", "id": "123123"}, + } + + +@pytest.fixture() +def own_connection_payload(test_partial_integration): + return { + "friend_sync": False, + "id": "2513849648", + "integrations": [test_partial_integration], + "name": "FS", + "revoked": False, + "show_activity": True, + "type": "twitter", + "verified": True, + "visibility": 0, + } + + @pytest.fixture() def own_guild_payload(): return { @@ -78,6 +104,23 @@ def application_information_payload(owner_payload, team_payload): } +class TestOwnConnection: + def test_deserialize(self, own_connection_payload, test_partial_integration): + mock_integration_obj = mock.MagicMock(guilds.PartialGuildIntegration) + with mock.patch.object(guilds.PartialGuildIntegration, "deserialize", return_value=mock_integration_obj): + connection_obj = oauth2.OwnConnection.deserialize(own_connection_payload) + guilds.PartialGuildIntegration.deserialize.assert_called_once_with(test_partial_integration) + assert connection_obj.id == "2513849648" + assert connection_obj.name == "FS" + assert connection_obj.type == "twitter" + assert connection_obj.is_revoked is False + assert connection_obj.integrations == [mock_integration_obj] + assert connection_obj.is_verified is True + assert connection_obj.is_friend_syncing is False + assert connection_obj.is_showing_activity is True + assert connection_obj.visibility is oauth2.ConnectionVisibility.NONE + + class TestOwnGuild: def test_deserialize(self, own_guild_payload): own_guild_obj = oauth2.OwnGuild.deserialize(own_guild_payload) From a8a9324cc9269b6604d437a15db360086238587a Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Fri, 3 Apr 2020 22:06:35 +0100 Subject: [PATCH 058/922] Added prototype for working event dispatcher and state registry integration with gateway. --- hikari/core/clients/_run_gateway.py | 19 +++- hikari/core/clients/gateway_client.py | 47 +++------- hikari/core/state/__init__.py | 24 +---- hikari/core/state/base_state.py | 121 ++++++++++++++++++++++++++ hikari/core/state/default_state.py | 38 ++++++++ hikari/core/{ => state}/dispatcher.py | 0 tests/hikari/core/test_dispatcher.py | 2 +- 7 files changed, 189 insertions(+), 62 deletions(-) create mode 100644 hikari/core/state/base_state.py create mode 100644 hikari/core/state/default_state.py rename hikari/core/{ => state}/dispatcher.py (100%) diff --git a/hikari/core/clients/_run_gateway.py b/hikari/core/clients/_run_gateway.py index 1a343a0793..306a829f30 100644 --- a/hikari/core/clients/_run_gateway.py +++ b/hikari/core/clients/_run_gateway.py @@ -25,9 +25,12 @@ import click +from hikari.core import events from hikari.core.clients import gateway_client from hikari.core.clients import gateway_config from hikari.core.clients import protocol_config +from hikari.core.state import default_state +from hikari.core.state import dispatcher logger_levels = ("DEBUG", "INFO", "WARNING", "ERROR", "NOTSET") @@ -41,10 +44,12 @@ @click.option("--url", default="wss://gateway.discord.gg/", help="The websocket URL to connect to.") @click.option("--verify-ssl", default=True, type=click.BOOL, help="Enable or disable SSL verification.") @click.option("--version", default=7, type=click.IntRange(min=6), help="Version of the gateway to use.") -def run_gateway(compression, debug, logger, shards, token, url, verify_ssl, version): +def run_gateway(compression, debug, logger, shards, token, url, verify_ssl, version) -> None: """Run the client.""" logging.basicConfig(level=logger) + event_dispatcher = dispatcher.EventDispatcherImpl() + state = default_state.DefaultState(event_dispatcher) client = gateway_client.GatewayClient( config=gateway_config.GatewayConfig( debug=debug, @@ -55,8 +60,20 @@ def run_gateway(compression, debug, logger, shards, token, url, verify_ssl, vers version=version, ), url=url, + state_impl=state, ) + @event_dispatcher.on(events.MessageCreateEvent) + async def on_message(message: events.MessageCreateEvent) -> None: + logging.info( + "Received message from @%s#%s in %s (guild: %s) with content %s", + message.author.username, + message.author.discriminator, + message.channel_id, + message.guild_id, + message.content, + ) + client.run() diff --git a/hikari/core/clients/gateway_client.py b/hikari/core/clients/gateway_client.py index 98634b1b5e..d5c78f45c6 100644 --- a/hikari/core/clients/gateway_client.py +++ b/hikari/core/clients/gateway_client.py @@ -23,14 +23,12 @@ __all__ = ["GatewayClient"] import asyncio -import inspect import time import typing +from hikari.core.state import base_state from hikari.internal import more_logging -from hikari.core import dispatcher -from hikari.core import events -from hikari.core import state +from hikari.core.state import dispatcher from hikari.core.clients import gateway_config from hikari.core.clients import shard_client from hikari.net import shard @@ -38,7 +36,7 @@ ShardT = typing.TypeVar("ShardT", bound=shard_client.ShardClient) -class GatewayClient(typing.Generic[ShardT], shard_client.WebsocketClientBase, dispatcher.EventDispatcher): +class GatewayClient(typing.Generic[ShardT], shard_client.WebsocketClientBase): """Facades :obj:`shard_client.ShardClient` implementations to provide a management layer for multiple-sharded bots. This also provides additional conduit used to connect up shards to the rest of this framework to enable @@ -50,14 +48,12 @@ def __init__( config: gateway_config.GatewayConfig, url: str, *, - dispatcher_impl: typing.Optional[dispatcher.EventDispatcher] = None, + state_impl: base_state.BaseState, shard_type: typing.Type[ShardT] = shard_client.ShardClient, ) -> None: self.logger = more_logging.get_named_logger(self) self.config = config - self.event_dispatcher = dispatcher_impl if dispatcher_impl is not None else dispatcher.EventDispatcherImpl() - self._websocket_event_types = self._websocket_events() - self.state = state.StatefulStateManagerImpl() + self._state = state_impl self._is_running = False self.shards: typing.Dict[int, ShardT] = { shard_id: shard_type(shard_id, config, self._handle_websocket_event_later, url) @@ -149,31 +145,8 @@ def _handle_websocket_event_later(self, conn: shard.ShardConnection, event_name: # Run this asynchronously so that we can allow awaiting stuff like state management. asyncio.get_event_loop().create_task(self._handle_websocket_event(conn, event_name, payload)) - async def _handle_websocket_event(self, _: shard.ShardConnection, event_name: str, payload: typing.Any) -> None: - try: - event_type = self._websocket_event_types[event_name] - except KeyError: - pass - else: - event_payload = event_type.deserialize(payload) - await self.state.handle_new_event(event_payload) - - def _websocket_events(self): - # Look for anything that has the ___raw_ws_event_name___ class attribute - # to each corresponding class where appropriate to do so. This provides - # a quick and dirty event lookup mechanism that can be extended quickly - # and has O(k) lookup time. - - types = {} - - def predicate(member): - return inspect.isclass(member) and hasattr(member, "___raw_ws_event_name___") - - for name, cls in inspect.getmembers(events, predicate): - raw_name = cls.___raw_ws_event_name___ - types[raw_name] = cls - self.logger.debug("detected %s as a web socket event to listen for", name) - - self.logger.debug("detected %s web socket events to register from %s", len(types), events.__name__) - - return types + async def _handle_websocket_event( + self, shard_obj: shard.ShardConnection, event_name: str, payload: typing.Any + ) -> None: + shard_client_obj = self.shards[shard_obj.shard_id] + await self._state.process_raw_event(shard_client_obj, event_name, payload) diff --git a/hikari/core/state/__init__.py b/hikari/core/state/__init__.py index a9983cd84f..5dc33a6d3b 100644 --- a/hikari/core/state/__init__.py +++ b/hikari/core/state/__init__.py @@ -17,30 +17,8 @@ # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . """State registry and event manager.""" -__all__ = ["StateManager", "StatefulStateManagerImpl", "StatelessStateManagerImpl"] + import abc from hikari.core import events - - -class StateManager(abc.ABC): - """Base type for a state management implementation.""" - - @abc.abstractmethod - async def handle_new_event(self, event_obj: events.HikariEvent) -> None: - """This is abstract and this is a dummy string.""" - - -class StatelessStateManagerImpl(StateManager): - """Stubbed stateless event manager for implementing stateless bots.""" - - async def handle_new_event(self, event_obj: events.HikariEvent) -> None: - """Gluten free.""" - - -class StatefulStateManagerImpl(StateManager): - """A basic state event manager implementation.""" - - async def handle_new_event(self, event_obj: events.HikariEvent) -> None: - """Sourced from sustainable agricultural plots in Sweden.""" diff --git a/hikari/core/state/base_state.py b/hikari/core/state/base_state.py new file mode 100644 index 0000000000..ca74067cbb --- /dev/null +++ b/hikari/core/state/base_state.py @@ -0,0 +1,121 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# Copyright © Nekoka.tt 2019-2020 +# +# This file is part of Hikari. +# +# Hikari is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Hikari is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Hikari. If not, see . +"""Definition of the interface a compliant state implementation should provide. + +State object handle decoding events and managing application state. +""" +__all__ = ["BaseState"] + +import abc +import asyncio +import inspect +import typing + +from hikari.core import entities +from hikari.core import events +from hikari.core.clients import shard_client +from hikari.internal import assertions +from hikari.internal import more_logging + +EVENT_MARKER_ATTR = "___event_name___" + +EventConsumerT = typing.Callable[[str, entities.RawEntityT], typing.Awaitable[None]] + + +def register_state_event_handler(name: str) -> typing.Callable[[EventConsumerT], EventConsumerT]: + """Create a decorator for a coroutine function to register it as an event handler. + + Parameters + ---------- + name: str + The case sensitive name of the event to associate the annotated method + with. + + Returns + ------- + ``decorator(callable) -> callable`` + A decorator for a method. + + """ + + def decorator(callable_item: EventConsumerT) -> EventConsumerT: + assertions.assert_that(inspect.isfunction(callable_item), "Annotated element must be a function") + setattr(callable_item, EVENT_MARKER_ATTR, name) + return callable_item + + return decorator + + +def _has_event_marker(obj: typing.Any) -> bool: + return hasattr(obj, EVENT_MARKER_ATTR) + + +def _get_event_marker(obj: typing.Any) -> str: + return getattr(obj, EVENT_MARKER_ATTR) + + +class BaseState(abc.ABC): + """Abstract definition of a state manager. + + This is designed to manage any state-related operations in an application by + consuming raw events from a low level gateway connection, transforming them + to object-based event types, and tracking overall application state. + + Any methods marked with the :obj:`register_state_event_handler` decorator + will be detected and registered as event handlers by the constructor. + """ + + @abc.abstractmethod + def __init__(self): + self.logger = more_logging.get_named_logger(self) + self._event_mapping = {} + + # Look for events and register them. + for _, member in inspect.getmembers(self, _has_event_marker): + event = _get_event_marker(member) + self._event_mapping[event] = member + + async def process_raw_event( + self, shard_client_obj: shard_client.ShardClient, name: str, payload: entities.RawEntityT, + ) -> None: + """Process a low level event. + + This will update the internal state, perform processing where necessary, + and then dispatch the event to any listeners. + + Parameters + ---------- + shard_client_obj: :obj:`hikari.core.clients.shard_client.ShardClient` + The shard that triggered this event. + name : :obj:`str` + The raw event name. + payload : :obj:`dict` + The payload that was sent. + """ + try: + handler = self._event_mapping[name] + except KeyError: + self.logger.debug("No handler for event %s is registered", name) + else: + event = await handler(shard_client_obj, payload) + self.dispatch(event) + + @abc.abstractmethod + def dispatch(self, event: events.HikariEvent) -> None: + """Dispatch the given event somewhere.""" diff --git a/hikari/core/state/default_state.py b/hikari/core/state/default_state.py new file mode 100644 index 0000000000..0dd79fe70c --- /dev/null +++ b/hikari/core/state/default_state.py @@ -0,0 +1,38 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# Copyright © Nekoka.tt 2019-2020 +# +# This file is part of Hikari. +# +# Hikari is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Hikari is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Hikari. If not, see . +"""Basic single-application state manager.""" +__all__ = ["DefaultState"] + +from hikari.core.state import dispatcher +from hikari.core import entities +from hikari.core import events +from hikari.core.state import base_state + + +class DefaultState(base_state.BaseState): + def __init__(self, event_dispatcher: dispatcher.EventDispatcher): + super().__init__() + self.event_dispatcher: dispatcher.EventDispatcher = event_dispatcher + + @base_state.register_state_event_handler("MESSAGE_CREATE") + async def _on_message_create(self, _, payload: entities.RawEntityT) -> None: + self.dispatch(events.MessageCreateEvent.deserialize(payload)) + + def dispatch(self, event: events.HikariEvent) -> None: + self.event_dispatcher.dispatch_event(event) diff --git a/hikari/core/dispatcher.py b/hikari/core/state/dispatcher.py similarity index 100% rename from hikari/core/dispatcher.py rename to hikari/core/state/dispatcher.py diff --git a/tests/hikari/core/test_dispatcher.py b/tests/hikari/core/test_dispatcher.py index 184aa7aa69..da58c07045 100644 --- a/tests/hikari/core/test_dispatcher.py +++ b/tests/hikari/core/test_dispatcher.py @@ -21,7 +21,7 @@ import pytest -from hikari.core import dispatcher +from hikari.core.state import dispatcher from hikari.core import events from tests.hikari import _helpers From 55e60ac797e19c8310c39d1fa4c64920fe84fc6e Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Sat, 4 Apr 2020 07:41:53 +0000 Subject: [PATCH 059/922] Changed shard client to attempt to resume on reconnect --- hikari/core/clients/shard_client.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/hikari/core/clients/shard_client.py b/hikari/core/clients/shard_client.py index a72d5978e4..f75bbd7c3c 100644 --- a/hikari/core/clients/shard_client.py +++ b/hikari/core/clients/shard_client.py @@ -365,8 +365,6 @@ async def _keep_alive(self): await asyncio.sleep(5) except errors.GatewayMustReconnectError: self.logger.warning("instructed by Discord to reconnect") - self._client.seq = None - self._client.session_id = None do_not_back_off = True await asyncio.sleep(5) except errors.GatewayServerClosedConnectionError as ex: From a75b8d2808fd690feef86f419c9ee0991748d844 Mon Sep 17 00:00:00 2001 From: davfsa Date: Sat, 4 Apr 2020 17:14:09 +0200 Subject: [PATCH 060/922] Fix docstrings and enable docstyle in CI --- hikari/__main__.py | 4 +- hikari/core/audit_logs.py | 16 +- hikari/core/channels.py | 49 ++--- hikari/core/clients/app_config.py | 11 +- hikari/core/clients/gateway_client.py | 21 +- hikari/core/clients/gateway_config.py | 2 +- hikari/core/clients/protocol_config.py | 15 +- hikari/core/clients/shard_client.py | 73 ++++--- hikari/core/colors.py | 82 ++++++-- hikari/core/embeds.py | 28 ++- hikari/core/emojis.py | 14 +- hikari/core/entities.py | 20 +- hikari/core/events.py | 243 ++++++++++++----------- hikari/core/gateway_entities.py | 6 +- hikari/core/guilds.py | 154 +++++++-------- hikari/core/invites.py | 57 +++--- hikari/core/messages.py | 45 +++-- hikari/core/oauth2.py | 79 ++++---- hikari/core/permissions.py | 3 +- hikari/core/snowflakes.py | 19 +- hikari/core/state/default_state.py | 2 +- hikari/core/state/dispatcher.py | 53 +++-- hikari/core/users.py | 14 +- hikari/core/voices.py | 11 +- hikari/core/webhooks.py | 17 +- hikari/errors.py | 1 + hikari/internal/assertions.py | 6 +- hikari/internal/cdn.py | 2 +- hikari/internal/conversions.py | 35 ++-- hikari/internal/marshaller.py | 28 +-- hikari/internal/meta.py | 3 +- hikari/internal/more_asyncio.py | 2 +- hikari/internal/more_logging.py | 2 +- hikari/net/codes.py | 4 +- hikari/net/errors.py | 46 ++--- hikari/net/ratelimits.py | 67 +++---- hikari/net/rest.py | 262 +++++++++++++------------ hikari/net/routes.py | 14 +- hikari/net/shard.py | 48 +++-- hikari/net/user_agent.py | 24 ++- noxfile.py | 7 +- pydocstyle.ini | 3 +- pylint.ini | 2 +- tests/hikari/core/test_oauth2.py | 8 +- 44 files changed, 835 insertions(+), 767 deletions(-) diff --git a/hikari/__main__.py b/hikari/__main__.py index 3ff11eb34a..93f1c340d1 100644 --- a/hikari/__main__.py +++ b/hikari/__main__.py @@ -16,9 +16,7 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . -"""Provides a command-line entry point that shows the library version and then -exits. -""" +"""Provides a command-line entry point that shows the library version and then exits.""" import platform import click diff --git a/hikari/core/audit_logs.py b/hikari/core/audit_logs.py index 74f3f469b1..f7e9efe54f 100644 --- a/hikari/core/audit_logs.py +++ b/hikari/core/audit_logs.py @@ -18,7 +18,6 @@ # along with Hikari. If not, see . """Components and entities that are used to describe audit logs on Discord. - .. inheritance-diagram:: hikari.core.audit_logs :parts: 1 @@ -236,9 +235,10 @@ class AuditLogEventType(enum.IntEnum): INTEGRATION_DELETE = 82 +# Ignore docstring not starting in an imperative mood def register_audit_log_entry_info( type_: AuditLogEventType, *additional_types: AuditLogEventType -) -> typing.Callable[[typing.Type["BaseAuditLogEntryInfo"]], typing.Type["BaseAuditLogEntryInfo"]]: +) -> typing.Callable[[typing.Type["BaseAuditLogEntryInfo"]], typing.Type["BaseAuditLogEntryInfo"]]: # noqa: D401 """Generates a decorator for defined audit log entry info entities. Allows them to be associated with given entry type(s). @@ -343,8 +343,7 @@ class MemberPruneEntryInfo(BaseAuditLogEntryInfo): @register_audit_log_entry_info(AuditLogEventType.MESSAGE_BULK_DELETE) @marshaller.attrs(slots=True) class MessageBulkDeleteEntryInfo(BaseAuditLogEntryInfo): - """Represents extra information for the message bulk delete audit entry. - """ + """Represents extra information for the message bulk delete audit entry.""" #: The amount of messages that were deleted. #: @@ -355,8 +354,7 @@ class MessageBulkDeleteEntryInfo(BaseAuditLogEntryInfo): @register_audit_log_entry_info(AuditLogEventType.MESSAGE_DELETE) @marshaller.attrs(slots=True) class MessageDeleteEntryInfo(MessageBulkDeleteEntryInfo): - """Represents extra information attached to the message delete audit entry. - """ + """Represents extra information attached to the message delete audit entry.""" #: The guild text based channel where these message(s) were deleted. #: @@ -367,8 +365,7 @@ class MessageDeleteEntryInfo(MessageBulkDeleteEntryInfo): @register_audit_log_entry_info(AuditLogEventType.MEMBER_DISCONNECT) @marshaller.attrs(slots=True) class MemberDisconnectEntryInfo(BaseAuditLogEntryInfo): - """Represents extra information for the voice chat member disconnect entry. - """ + """Represents extra information for the voice chat member disconnect entry.""" #: The amount of members who were disconnected from voice in this entry. #: @@ -379,8 +376,7 @@ class MemberDisconnectEntryInfo(BaseAuditLogEntryInfo): @register_audit_log_entry_info(AuditLogEventType.MEMBER_MOVE) @marshaller.attrs(slots=True) class MemberMoveEntryInfo(MemberDisconnectEntryInfo): - """Represents extra information for the voice chat based member move entry. - """ + """Represents extra information for the voice chat based member move entry.""" #: The channel these member(s) were moved to. #: diff --git a/hikari/core/channels.py b/hikari/core/channels.py index a8d83c1277..354db94703 100644 --- a/hikari/core/channels.py +++ b/hikari/core/channels.py @@ -16,9 +16,7 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . -"""Components and entities that are used to describe both DMs and guild -channels on Discord. - +"""Components and entities that are used to describe both DMs and guild channels on Discord. .. inheritance-diagram:: hikari.core.channels @@ -97,25 +95,24 @@ class PermissionOverwrite(snowflakes.UniqueEntity, entities.Deserializable, enti #: The permissions this overwrite allows. #: - #: :type: :obj:`permissions.Permission` + #: :type: :obj:`hikari.core.permissions.Permission` allow: permissions.Permission = marshaller.attrib(deserializer=permissions.Permission) #: The permissions this overwrite denies. #: - #: :type: :obj:`permissions.Permission` + #: :type: :obj:`hikari.core.permissions.Permission` deny: permissions.Permission = marshaller.attrib(deserializer=permissions.Permission) @property def unset(self) -> permissions.Permission: - """Return a bitfield of all permissions not explicitly allowed or - denied by this overwrite. - """ + """Bitfield of all permissions not explicitly allowed or denied by this overwrite.""" return typing.cast(permissions.Permission, (self.allow | self.deny)) def register_channel_type(type_: ChannelType) -> typing.Callable[[typing.Type["Channel"]], typing.Type["Channel"]]: - """Generates a decorator for channel classes defined in this library to use - to associate themselves with a given channel type. + """Generate a decorator for channel classes defined in this library. + + This allows them to associate themselves with a given channel type. Parameters ---------- @@ -124,7 +121,7 @@ def register_channel_type(type_: ChannelType) -> typing.Callable[[typing.Type["C Returns ------- - ``decorator(cls: T) -> T`` + ``decorator`` The decorator to decorate the class with. """ @@ -149,8 +146,9 @@ class Channel(snowflakes.UniqueEntity, entities.Deserializable): @marshaller.attrs(slots=True) class PartialChannel(Channel): - """Represents a channel where we've only received it's basic information, - commonly received in rest responses. + """Represents a channel where we've only received it's basic information. + + This is commonly received in REST responses. """ #: The channel's name. @@ -162,7 +160,7 @@ class PartialChannel(Channel): @register_channel_type(ChannelType.DM) @marshaller.attrs(slots=True) class DMChannel(Channel): - """Represents a DM channel""" + """Represents a DM channel.""" #: The ID of the last message sent in this channel. #: @@ -170,14 +168,15 @@ class DMChannel(Channel): #: ---- #: This might point to an invalid or deleted message. #: - #: :type: :obj:`snowflakes.Snowflake`, optional + #: + #: :type: :obj:`hikari.core.snowflakes.Snowflake`, optional last_message_id: snowflakes.Snowflake = marshaller.attrib( deserializer=snowflakes.Snowflake.deserialize, if_none=None ) #: The recipients of the DM. #: - #: :type: :obj:`typing.Mapping` [ :obj:`snowflakes.Snowflake`, :obj:`users.User` ] + #: :type: :obj:`typing.Mapping` [ :obj:`hikari.core.snowflakes.Snowflake`, :obj:`hikari.core.users.User` ] recipients: typing.Mapping[snowflakes.Snowflake, users.User] = marshaller.attrib( deserializer=lambda recipients: {user.id: user for user in map(users.User.deserialize, recipients)} ) @@ -195,7 +194,7 @@ class GroupDMChannel(DMChannel): #: The ID of the owner of the group. #: - #: :type: :obj:`snowflakes.Snowflake` + #: :type: :obj:`hikari.core.snowflakes.Snowflake` owner_id: snowflakes.Snowflake = marshaller.attrib(deserializer=snowflakes.Snowflake.deserialize) #: The hash of the icon of the group. @@ -206,7 +205,7 @@ class GroupDMChannel(DMChannel): #: The ID of the application that created the group DM, if it's a #: bot based group DM. #: - #: :type: :obj:`snowflakes.Snowflake`, optional + #: :type: :obj:`hikari.core.snowflakes.Snowflake`, optional application_id: typing.Optional[snowflakes.Snowflake] = marshaller.attrib( deserializer=snowflakes.Snowflake.deserialize, if_undefined=None ) @@ -218,7 +217,7 @@ class GuildChannel(Channel): #: The ID of the guild the channel belongs to. #: - #: :type: :obj:`snowflakes.Snowflake` + #: :type: :obj:`hikari.core.snowflakes.Snowflake` guild_id: snowflakes.Snowflake = marshaller.attrib(deserializer=snowflakes.Snowflake.deserialize) #: The sorting position of the channel. @@ -228,7 +227,7 @@ class GuildChannel(Channel): #: The permission overwrites for the channel. #: - #: :type: :obj:`typing.Mapping` [ :obj:`snowflakes.Snowflake`, :obj:`PermissionOverwrite` ] + #: :type: :obj:`typing.Mapping` [ :obj:`hikari.core.snowflakes.Snowflake`, :obj:`PermissionOverwrite` ] permission_overwrites: PermissionOverwrite = marshaller.attrib( deserializer=lambda overwrites: {o.id: o for o in map(PermissionOverwrite.deserialize, overwrites)} ) @@ -245,7 +244,7 @@ class GuildChannel(Channel): #: The ID of the parent category the channel belongs to. #: - #: :type: :obj:`snowflakes.Snowflake`, optional + #: :type: :obj:`hikari.core.snowflakes.Snowflake`, optional parent_id: snowflakes.Snowflake = marshaller.attrib(deserializer=snowflakes.Snowflake.deserialize, if_none=None) @@ -271,7 +270,8 @@ class GuildTextChannel(GuildChannel): #: ---- #: This might point to an invalid or deleted message. #: - #: :type: :obj:`snowflakes.Snowflake`, optional + #: + #: :type: :obj:`hikari.core.snowflakes.Snowflake`, optional last_message_id: snowflakes.Snowflake = marshaller.attrib( deserializer=snowflakes.Snowflake.deserialize, if_none=None ) @@ -306,7 +306,8 @@ class GuildNewsChannel(GuildChannel): #: ---- #: This might point to an invalid or deleted message. #: - #: :type: :obj:`snowflakes.Snowflake`, optional + #: + #: :type: :obj:`hikari.core.snowflakes.Snowflake`, optional last_message_id: snowflakes.Snowflake = marshaller.attrib( deserializer=snowflakes.Snowflake.deserialize, if_none=None ) @@ -340,7 +341,7 @@ def deserialize_channel(payload: typing.Dict[str, typing.Any]) -> typing.Union[G Warning ------- This can only be used to deserialize full channel objects. To deserialize a - partial object, use :obj:`PartialChannel.deserialize` + partial object, use ``PartialChannel.deserialize()``. """ type_id = payload["type"] channel_type = register_channel_type.types[type_id] diff --git a/hikari/core/clients/app_config.py b/hikari/core/clients/app_config.py index 68e73723c2..7de2ded62f 100644 --- a/hikari/core/clients/app_config.py +++ b/hikari/core/clients/app_config.py @@ -34,11 +34,10 @@ class AppConfig(entities.HikariEntity, entities.Deserializable): All fields are optional kwargs that can be passed to the constructor. "Deserialized" and "unspecified" defaults are only applicable if you - create the object using :meth:`deserialize`. + create the object using ``deserialize()``. Examples -------- - Initializing programatically: .. code-block:: python @@ -129,18 +128,18 @@ class AppConfig(entities.HikariEntity, entities.Deserializable): #: The HTTP configuration to use. #: - #: If unspecified or None, then this will be a set of default values. + #: If unspecified or ``None```, then this will be a set of default values. #: - #: :type: :obj:`hikari.core.http_config.HTTPConfig`, optional + #: :type: :obj:`hikari.core.clients.http_config.HTTPConfig`, optional http: typing.Optional[http_config.HTTPConfig] = marshaller.attrib( deserializer=http_config.HTTPConfig.deserialize, if_none=None, if_undefined=None, default=None ) #: The gateway configuration to use. #: - #: If unspecified or None, then this will be a set of default values. + #: If unspecified or ``None```, then this will be a set of default values. #: - #: :type: :obj:`hikari.core.gateway_config.GatewayConfig`, optional + #: :type: :obj:`hikari.core.clients.gateway_config.GatewayConfig`, optional gateway: typing.Optional[gateway_config.GatewayConfig] = marshaller.attrib( deserializer=gateway_config.GatewayConfig.deserialize, if_none=None, if_undefined=None, default=None ) diff --git a/hikari/core/clients/gateway_client.py b/hikari/core/clients/gateway_client.py index d5c78f45c6..a599d7155e 100644 --- a/hikari/core/clients/gateway_client.py +++ b/hikari/core/clients/gateway_client.py @@ -16,8 +16,9 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . -"""Defines a facade around :obj:`hikari.core.clients.shard_client.ShardClient` -which provides functionality such as keeping multiple shards alive +"""Defines a facade around :obj:`hikari.core.clients.shard_client.ShardClient`. + +This provides functionality such as keeping multiple shards alive simultaneously. """ __all__ = ["GatewayClient"] @@ -36,11 +37,11 @@ ShardT = typing.TypeVar("ShardT", bound=shard_client.ShardClient) -class GatewayClient(typing.Generic[ShardT], shard_client.WebsocketClientBase): - """Facades :obj:`shard_client.ShardClient` implementations to provide a - management layer for multiple-sharded bots. This also provides additional - conduit used to connect up shards to the rest of this framework to enable - management of dispatched events, etc. +class GatewayClient(typing.Generic[ShardT], shard_client.WebsocketClientBase, dispatcher.EventDispatcher): + """Provides a management layer for multiple-sharded bots. + + This also provides additional conduit used to connect up shards to the + rest of this framework to enable management of dispatched events, etc. """ def __init__( @@ -115,12 +116,12 @@ async def wait_for( Parameters ---------- - event_type : :obj:`typing.Type` [ :obj:`events.HikariEvent` ] + event_type : :obj:`typing.Type` [ :obj:`hikari.core.events.HikariEvent` ] The name of the event to wait for. timeout : :obj:`float`, optional The timeout to wait for before cancelling and raising an - :obj:`asyncio.TimeoutError` instead. If this is `None`, this will - wait forever. Care must be taken if you use `None` as this may + :obj:`asyncio.TimeoutError` instead. If this is ``None``, this will + wait forever. Care must be taken if you use ``None`` as this may leak memory if you do this from an event listener that gets repeatedly called. If you want to do this, you should consider using an event listener instead of this function. diff --git a/hikari/core/clients/gateway_config.py b/hikari/core/clients/gateway_config.py index ad5cb22515..5ce5899ea7 100644 --- a/hikari/core/clients/gateway_config.py +++ b/hikari/core/clients/gateway_config.py @@ -121,7 +121,7 @@ class GatewayConfig(entities.HikariEntity, entities.Deserializable): #: The initial activity to set all shards to when starting the gateway. If #: ``None``, then no activity will be set. #: - #: :type: :obj:`GatewayActivity`, optional + #: :type: :obj:`hikari.core.gateway_entities.GatewayActivity`, optional initial_activity: typing.Optional[gateway_entities.GatewayActivity] = marshaller.attrib( deserializer=gateway_entities.GatewayActivity.deserialize, if_none=None, if_undefined=None, default=None ) diff --git a/hikari/core/clients/protocol_config.py b/hikari/core/clients/protocol_config.py index 0a54126d31..46d50232ee 100644 --- a/hikari/core/clients/protocol_config.py +++ b/hikari/core/clients/protocol_config.py @@ -16,8 +16,10 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . -"""Configuration objects for various low-level protocols such as HTTP -connections and SSL management, proxies, etc.""" +"""Configuration objects for various low-level protocols. + +These include HTTP connections and SSL management, proxies, etc. +""" __all__ = ["HTTPProtocolConfig"] import ssl @@ -31,10 +33,11 @@ @marshaller.attrs(kw_only=True) class HTTPProtocolConfig(entities.HikariEntity, entities.Deserializable): - """A configuration class that can be deserialized from a :obj:`dict`. This - represents any HTTP-specific implementation and protocol details such as - how to manage redirects, how to manage SSL, and how to use a proxy if - needed. + """A configuration class that can be deserialized from a :obj:`typing.Dict`. + + This represents any HTTP-specific implementation and protocol details + such as how to manage redirects, how to manage SSL, and how to use a + proxy if needed. All fields are optional kwargs that can be passed to the constructor. diff --git a/hikari/core/clients/shard_client.py b/hikari/core/clients/shard_client.py index f75bbd7c3c..25b8ab697a 100644 --- a/hikari/core/clients/shard_client.py +++ b/hikari/core/clients/shard_client.py @@ -16,9 +16,10 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . -"""Provides a facade around the :obj:`hikari.net.shard.ShardConnection` -implementation which handles parsing and initializing the object from a -configuration, as well as restarting it if it disconnects. +"""Provides a facade around :obj:`hikari.net.shard.ShardConnection`. + +This handles parsing and initializing the object from a configuration, as +well as restarting it if it disconnects. Additional functions and coroutines are provided to update the presence on the shard using models defined in :mod:`hikari.core`. @@ -74,27 +75,30 @@ class ShardState(enum.IntEnum): class WebsocketClientBase(abc.ABC): - """Base for any socket-based communication medium to provide functionality - for more automated control given certain method constraints. + """Base for any socket-based communication medium to provide more functionality. + + This includes more automated control given certain method constraints. """ logger: logging.Logger + # Ignore docstring not starting in an imperative mood @abc.abstractmethod - async def start(self): + async def start(self): # noqa: D401 """Starts the component.""" @abc.abstractmethod async def close(self, wait: bool = True): - """Shuts down the component.""" + """Shut down the component.""" @abc.abstractmethod async def join(self): - """Waits for the component to terminate.""" + """Wait for the component to terminate.""" def run(self): - """Performs the same job as :meth:`start`, but provides additional - preparation such as registering OS signal handlers for interrupts, + """Perform the same job as :meth:`start`, but with additional preparation. + + Additional preparation includes: registering OS signal handlers for interrupts and preparing the initial event loop. This enables the client to be run immediately without having to @@ -134,19 +138,21 @@ def sigterm_handler(*_): class ShardClient(WebsocketClientBase): - """The primary interface for a single shard connection. This contains - several abstractions to enable usage of the low level gateway network - interface with the higher level constructs in :mod:`hikari.core`. + """The primary interface for a single shard connection. + + This contains several abstractions to enable usage of the low + level gateway network interface with the higher level constructs + in :mod:`hikari.core`. Parameters ---------- shard_id : :obj:`int` The ID of this specific shard. - config : :obj:`gateway_config.GatewayConfig` + config : :obj:`hikari.core.gateway_config.GatewayConfig` The gateway configuration to use to initialize this shard. - low_level_dispatch : :obj:`typing.Callable` [ [ :obj:`Shard`, :obj:`str`, :obj:`typing.Any` ] ] + low_level_dispatch : :obj:`typing.Callable` [ [ :obj:`ShardClient`, :obj:`str`, :obj:`typing.Any` ] ] A function that is fed any low-level event payloads. This will consist - of three arguments: an :obj:`Shard` which is this shard instance, + of three arguments: an :obj:`ShardClient` which is this shard instance, a :obj:`str` of the raw event name, and any naive raw payload that was passed with the event. The expectation is the function passed here will pass the payload onto any event handling and state handling system @@ -219,28 +225,33 @@ def __init__( @property def client(self) -> shard.ShardConnection: - """ + """Low-level gateway client used for this shard. + Returns ------- - :obj:`hikari.net.gateway.GatewayClient` + :obj:`hikari.net.shard.ShardConnection` The low-level gateway client used for this shard. """ return self._client #: TODO: use enum + # Ignore docstring not starting in an imperative mood @property - def status(self) -> guilds.PresenceStatus: - """ + def status(self) -> guilds.PresenceStatus: # noqa: D401 + """Current user status for this shard. + Returns ------- - :obj:`guilds.PresenceStatus` + :obj:`hikari.core.guilds.PresenceStatus` The current user status for this shard. """ return self._status + # Ignore docstring not starting in an imperative mood @property - def activity(self) -> typing.Optional[gateway_entities.GatewayActivity]: - """ + def activity(self) -> typing.Optional[gateway_entities.GatewayActivity]: # noqa: D401 + """Current activity for the user status for this shard. + Returns ------- :obj:`hikari.core.gateway_entities.GatewayActivity`, optional @@ -251,7 +262,8 @@ def activity(self) -> typing.Optional[gateway_entities.GatewayActivity]: @property def idle_since(self) -> typing.Optional[datetime.datetime]: - """ + """Timestamp when the user of this shard appeared to be idle. + Returns ------- :obj:`datetime.datetime`, optional @@ -260,9 +272,11 @@ def idle_since(self) -> typing.Optional[datetime.datetime]: """ return self._idle_since + # Ignore docstring not starting in an imperative mood @property - def is_afk(self) -> bool: - """ + def is_afk(self) -> bool: # noqa: D401 + """``True`` if the user is AFK, ``False`` otherwise. + Returns ------- :obj:`bool` @@ -271,8 +285,9 @@ def is_afk(self) -> bool: return self._is_afk async def start(self): - """Connect to the gateway on this shard and schedule tasks to keep this - connection alive. Wait for the shard to dispatch a ``READY`` event, and + """Connect to the gateway on this shard and keep the connection alive. + + This will wait for the shard to dispatch a ``READY`` event, and then return. """ if self._shard_state not in (ShardState.NOT_RUNNING, ShardState.STOPPED): @@ -406,7 +421,7 @@ async def update_presence( Parameters ---------- - status : :obj:`guilds.PresenceStatus` + status : :obj:`hikari.core.guilds.PresenceStatus` The new status to set. activity : :obj:`hikari.core.gateway_entities.GatewayActivity`, optional The new activity to set. diff --git a/hikari/core/colors.py b/hikari/core/colors.py index 0cf343152b..3fe6ca25a2 100644 --- a/hikari/core/colors.py +++ b/hikari/core/colors.py @@ -16,8 +16,7 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . -"""Model that represents a common RGB color and provides simple conversions to -other common color systems. +"""Model that represents a common RGB color and provides simple conversions to other common color systems. .. inheritance-diagram:: builtins.int @@ -123,16 +122,20 @@ def __int__(self): # Binary-OR to make raw int. return self | 0 + # Ignore docstring not starting in an imperative mood @property - def rgb(self) -> typing.Tuple[int, int, int]: - """The RGB representation of this Color. Represented a tuple of R, G, B. - Each value is in the range [0, 0xFF]. + def rgb(self) -> typing.Tuple[int, int, int]: # noqa: D401 + """The RGB representation of this Color. + + Represented as a tuple of R, G, B. Each value is + in the range [0, 0xFF]. """ return (self >> 16) & 0xFF, (self >> 8) & 0xFF, self & 0xFF @property def rgb_float(self) -> typing.Tuple[float, float, float]: """Return the floating-point RGB representation of this Color. + Represented as a tuple of R, G, B. Each value is in the range [0, 1]. """ r, g, b = self.rgb @@ -140,8 +143,9 @@ def rgb_float(self) -> typing.Tuple[float, float, float]: @property def hex_code(self) -> str: - """The six-digit hexadecimal color code for this Color. This is - prepended with a ``#`` symbol, and will be in upper case. + """Six-digit hexadecimal color code for this Color. + + This is prepended with a ``#`` symbol, and will be in upper case. Example ------- @@ -151,7 +155,7 @@ def hex_code(self) -> str: @property def raw_hex_code(self) -> str: - """The raw hex code. + """Raw hex code. Example ------- @@ -160,16 +164,18 @@ def raw_hex_code(self) -> str: components = self.rgb return "".join(hex(c)[2:].zfill(2) for c in components).upper() + # Ignore docstring not starting in an imperative mood @property - def is_web_safe(self) -> bool: - """``True`` if this color is a web-safe color, ``False`` otherwise.""" + def is_web_safe(self) -> bool: # noqa: D401 + """``True`` if the color is web safe, ``False`` otherwise.""" hex_code = self.raw_hex_code return all(_all_same(*c) for c in (hex_code[:2], hex_code[2:4], hex_code[4:])) @classmethod def from_rgb(cls, red: int, green: int, blue: int) -> "Color": - """Convert the given RGB colorspace represented in values within the - range [0, 255]: [0x0, 0xFF], to a :obj:`Color` object. + """Convert the given RGB to a :obj:`Color` object. + + Each channel must be withing the range [0, 255] (0x0, 0xFF). Parameters ---------- @@ -198,8 +204,10 @@ def from_rgb(cls, red: int, green: int, blue: int) -> "Color": @classmethod def from_rgb_float(cls, red_f: float, green_f: float, blue_f: float) -> "Color": - """Convert the given RGB colorspace represented using floats in the - range [0, 1] to a :obj:`Color` object. + """Convert the given RGB to a :obj:`Color` object. + + The colorspace represented values have to be within the + range [0, 1]. Parameters ---------- @@ -228,7 +236,7 @@ def from_rgb_float(cls, red_f: float, green_f: float, blue_f: float) -> "Color": @classmethod def from_hex_code(cls, hex_code: str) -> "Color": - """Consumes a string hexadecimal color code and produces a :obj:`Color`. + """Convert the given hexadecimal color code to a :obj:`Color`. The inputs may be of the following format (case insensitive): ``1a2``, ``#1a2``, ``0x1a2`` (for websafe colors), or @@ -270,7 +278,7 @@ def from_hex_code(cls, hex_code: str) -> "Color": @classmethod def from_int(cls, i: typing.SupportsInt) -> "Color": - """Create a color from a raw integer that Discord can understand. + """Convert the given :obj:`typing.SupportsInt` to a :obj:`Color`. Parameters ---------- @@ -286,12 +294,48 @@ def from_int(cls, i: typing.SupportsInt) -> "Color": # Partially chose to override these as the docstrings contain typos according to Sphinx. @classmethod - def from_bytes(cls, bytes_: typing.Sequence[int], byteorder: str, *, signed: bool = True) -> "Color": - """Converts the color from bytes.""" + def from_bytes(cls, bytes_: typing.Iterable[int], byteorder: str, *, signed: bool = True) -> "Color": + """Convert the bytes to a :obj:`Color`. + + Parameters + ---------- + bytes_ : :obj:`typing.Iterable` [ :obj:`int` ] + A iterable of :obj:`int` byte values. + + byteorder : :obj:str` + The endianess of the value represented by the bytes. + Can be ``"big"`` endian or ``"little"`` endian. + + signed : :obj:`bool` + Whether the value is signed or unsigned. + + Returns + ------- + :obj:`Color` + The Color object. + """ return Color(int.from_bytes(bytes_, byteorder, signed=signed)) def to_bytes(self, length: int, byteorder: str, *, signed: bool = True) -> bytes: - """Converts the color code to bytes.""" + """Convert the color code to bytes. + + Parameters + ---------- + length : :obj:`int` + The number of bytes to produce. Should be around ``3``, but not less. + + byteorder : :obj:str` + The endianess of the value represented by the bytes. + Can be ``"big"`` endian or ``"little"`` endian. + + signed : :obj:`bool` + Whether the value is signed or unsigned. + + Returns + ------- + :obj:`bytes` + The bytes represntation of the Color. + """ return int(self).to_bytes(length, byteorder, signed=signed) @classmethod diff --git a/hikari/core/embeds.py b/hikari/core/embeds.py index 826d7c7440..e8201c047d 100644 --- a/hikari/core/embeds.py +++ b/hikari/core/embeds.py @@ -16,9 +16,7 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . -""" -Components and entities that are used to describe message embeds on Discord. -""" +"""Components and entities that are used to describe message embeds on Discord.""" __all__ = [ "Embed", "EmbedThumbnail", @@ -48,12 +46,12 @@ class EmbedFooter(entities.HikariEntity, entities.Deserializable, entities.Seria #: :type: :obj:`str` text: typing.Optional[str] = marshaller.attrib(deserializer=str, serializer=str) - #: The url of the footer icon. + #: The URL of the footer icon. #: #: :type: :obj:`str`, optional icon_url: typing.Optional[str] = marshaller.attrib(deserializer=str, serializer=str, if_undefined=None) - #: The proxied url of the footer icon. + #: The proxied URL of the footer icon. #: #: :type: :obj:`str`, optional proxy_icon_url: typing.Optional[str] = marshaller.attrib(deserializer=str, serializer=str, if_undefined=None) @@ -63,12 +61,12 @@ class EmbedFooter(entities.HikariEntity, entities.Deserializable, entities.Seria class EmbedImage(entities.HikariEntity, entities.Deserializable, entities.Serializable): """Represents a embed image.""" - #: The url of the image. + #: The URL of the image. #: #: :type: :obj:`str`, optional url: typing.Optional[str] = marshaller.attrib(deserializer=str, serializer=str, if_undefined=None) - #: The proxied url of the image. + #: The proxied URL of the image. #: #: :type: :obj:`str`, optional proxy_url: typing.Optional[str] = marshaller.attrib(deserializer=str, serializer=str, if_undefined=None) @@ -88,12 +86,12 @@ class EmbedImage(entities.HikariEntity, entities.Deserializable, entities.Serial class EmbedThumbnail(entities.HikariEntity, entities.Deserializable, entities.Serializable): """Represents a embed thumbnail.""" - #: The url of the thumbnail. + #: The URL of the thumbnail. #: #: :type: :obj:`str`, optional url: typing.Optional[str] = marshaller.attrib(deserializer=str, serializer=str, if_undefined=None) - #: The proxied url of the thumbnail. + #: The proxied URL of the thumbnail. #: #: :type: :obj:`str`, optional proxy_url: typing.Optional[str] = marshaller.attrib(deserializer=str, serializer=str, if_undefined=None) @@ -113,7 +111,7 @@ class EmbedThumbnail(entities.HikariEntity, entities.Deserializable, entities.Se class EmbedVideo(entities.HikariEntity, entities.Deserializable, entities.Serializable): """Represents a embed video.""" - #: The url of the video. + #: The URL of the video. #: #: :type: :obj:`str`, optional url: typing.Optional[str] = marshaller.attrib(deserializer=str, serializer=str, if_undefined=None) @@ -138,7 +136,7 @@ class EmbedProvider(entities.HikariEntity, entities.Deserializable, entities.Ser #: :type: :obj:`str`, optional name: typing.Optional[str] = marshaller.attrib(deserializer=str, serializer=str, if_undefined=None) - #: The url of the provider. + #: The URL of the provider. #: #: :type: :obj:`str`, optional url: typing.Optional[str] = marshaller.attrib(deserializer=str, serializer=str, if_undefined=None, if_none=None) @@ -153,17 +151,17 @@ class EmbedAuthor(entities.HikariEntity, entities.Deserializable, entities.Seria #: :type: :obj:`str`, optional name: typing.Optional[str] = marshaller.attrib(deserializer=str, serializer=str, if_undefined=None) - #: The url of the author. + #: The URL of the author. #: #: :type: :obj:`str`, optional url: typing.Optional[str] = marshaller.attrib(deserializer=str, serializer=str, if_undefined=None) - #: The url of the author icon. + #: The URL of the author icon. #: #: :type: :obj:`str`, optional icon_url: typing.Optional[str] = marshaller.attrib(deserializer=str, serializer=str, if_undefined=None) - #: The proxied url of the author icon. + #: The proxied URL of the author icon. #: #: :type: :obj:`str`, optional proxy_icon_url: typing.Optional[str] = marshaller.attrib(deserializer=str, serializer=str, if_undefined=None) @@ -203,7 +201,7 @@ class Embed(entities.HikariEntity, entities.Deserializable, entities.Serializabl #: :type: :obj:`str`, optional description: typing.Optional[str] = marshaller.attrib(deserializer=str, serializer=str, if_undefined=None) - #: The url of the embed. + #: The URL of the embed. #: #: :type: :obj:`str`, optional url: typing.Optional[str] = marshaller.attrib(deserializer=str, serializer=str, if_undefined=None) diff --git a/hikari/core/emojis.py b/hikari/core/emojis.py index bc0249b3f8..2bfb400498 100644 --- a/hikari/core/emojis.py +++ b/hikari/core/emojis.py @@ -16,9 +16,7 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . -"""Components and entities that are used to describe both custom and Unicode -emojis on Discord. -""" +"""Components and entities that are used to describe both custom and Unicode emojis on Discord.""" import typing from hikari.internal import marshaller @@ -65,7 +63,7 @@ class GuildEmoji(UnknownEmoji): #: The whitelisted role IDs to use this emoji. #: - #: :type: :obj:`typing.Set` [ :obj:`snowflakes.Snowflake` ] + #: :type: :obj:`typing.Set` [ :obj:`hikari.core.snowflakes.Snowflake` ] role_ids: typing.Set[snowflakes.Snowflake] = marshaller.attrib( raw_name="roles", deserializer=lambda roles: {snowflakes.Snowflake.deserialize(r) for r in roles}, @@ -76,11 +74,11 @@ class GuildEmoji(UnknownEmoji): #: #: Note #: ---- - #: This will be ``None`` if you are missing ``MANAGE_EMOJIS``` permission - #: in the server the emoji is from - #: () + #: This will be ``None`` if you are missing the ``MANAGE_EMOJIS`` permission + #: in the server the emoji is from. #: - #: :type: :obj:`users.User`, optional + #: + #: :type: :obj:`hikari.core.users.User`, optional user: typing.Optional[users.User] = marshaller.attrib( deserializer=users.User.deserialize, if_none=None, if_undefined=None ) diff --git a/hikari/core/entities.py b/hikari/core/entities.py index 49b46018bd..b4ad779404 100644 --- a/hikari/core/entities.py +++ b/hikari/core/entities.py @@ -63,29 +63,27 @@ def __init__(self, *_, **__) -> None: class Deserializable: - """A mixin for any type that allows deserialization from a raw value - into a Hikari entity. - """ + """A mixin for any type that allows deserialization from a raw value into a Hikari entity.""" __slots__ = () @classmethod def deserialize(cls: typing.Type[T_contra], payload: RawEntityT) -> T_contra: - """Deserialize the given payload into this type and return the - constructed object. + """Deserialize the given payload into the object. + + Parameters + ---------- + payload + The payload to deserialize into the object. """ return marshaller.HIKARI_ENTITY_MARSHALLER.deserialize(payload, cls) class Serializable: - """A mixin for any type that allows serialization from a Hikari entity - into a raw value. - """ + """A mixin for any type that allows serialization from a Hikari entity into a raw value.""" __slots__ = () def serialize(self: T_co) -> RawEntityT: - """Serialize this instance into a naive value such as a - :obj:`dict` and return it. - """ + """Serialize this instance into a naive value.""" return marshaller.HIKARI_ENTITY_MARSHALLER.serialize(self) diff --git a/hikari/core/events.py b/hikari/core/events.py index 0e93c4ffe1..0a2057d18d 100644 --- a/hikari/core/events.py +++ b/hikari/core/events.py @@ -16,8 +16,7 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . -"""Components and entities that are used to describe Discord gateway events. -""" +"""Components and entities that are used to describe Discord gateway events.""" __all__ = [ "HikariEvent", @@ -126,24 +125,20 @@ class StartedEvent(HikariEvent): # Synthetic event, is not deserialized @attr.attrs(slots=True, auto_attribs=True) class StoppingEvent(HikariEvent): - """Event that is fired when the gateway client is instructed to disconnect - all shards. - """ + """Event that is fired when the gateway client is instructed to disconnect all shards.""" # Synthetic event, is not deserialized @attr.attrs(slots=True, auto_attribs=True) class StoppedEvent(HikariEvent): - """Event that is fired when the gateway client has finished disconnecting - all shards. - """ + """Event that is fired when the gateway client has finished disconnecting all shards.""" _websocket_name_break = re.compile(r"(?<=[a-z])(?=[A-Z])") def mark_as_websocket_event(cls): - """Marks the event as being a websocket one. I'll probably delete this.""" + """Mark the event as being a websocket one.""" name = cls.__name__ assertions.assert_that(name.endswith("Event"), "expected name to be Event") name = name[: -len("Event")] @@ -173,8 +168,9 @@ class ReconnectedEvent(HikariEvent, entities.Deserializable): @mark_as_websocket_event @marshaller.attrs(slots=True) class ReadyEvent(HikariEvent, entities.Deserializable): - """Used to represent the gateway ready event, received when identifying - with the gateway and on reconnect. + """Used to represent the gateway ready event. + + This is received when IDENTIFYing with the gateway and on reconnect. """ #: The gateway version this is currently connected to. @@ -184,13 +180,13 @@ class ReadyEvent(HikariEvent, entities.Deserializable): #: The object of the current bot account this connection is for. #: - #: :type: :obj:`users.MyUser` - my_user: users.User = marshaller.attrib(raw_name="user", deserializer=users.MyUser.deserialize) + #: :type: :obj:`hikari.core.users.MyUser` + my_user: users.MyUser = marshaller.attrib(raw_name="user", deserializer=users.MyUser.deserialize) #: A mapping of the guilds this bot is currently in. All guilds will start #: off "unavailable". #: - #: :type: :obj:`typing.Mapping` [ :obj:`snowflake.Snowflake`, :obj:`guilds.UnavailableGuild` ] + #: :type: :obj:`typing.Mapping` [ :obj:`hikari.core.snowflake.Snowflake`, :obj:`hikari.core.guilds.UnavailableGuild` ] unavailable_guilds: typing.Mapping[snowflakes.Snowflake, guilds.UnavailableGuild] = marshaller.attrib( raw_name="guilds", deserializer=lambda guilds_objs: {g.id: g for g in map(guilds.UnavailableGuild.deserialize, guilds_objs)}, @@ -201,7 +197,7 @@ class ReadyEvent(HikariEvent, entities.Deserializable): #: :type: :obj:`str` session_id: str = marshaller.attrib(deserializer=str) - #: Information about the current shard, only provided when identifying. + #: Information about the current shard, only provided when IDENTIFYing. #: #: :type: :obj:`typing.Tuple` [ :obj:`int`, :obj:`int` ], optional _shard_information: typing.Optional[typing.Tuple[int, int]] = marshaller.attrib( @@ -210,15 +206,17 @@ class ReadyEvent(HikariEvent, entities.Deserializable): @property def shard_id(self) -> typing.Optional[int]: - """The zero-indexed id of the current shard, only available if this - ready event was received while identifying. - .""" + """Zero-indexed ID of the current shard. + + This is only available if this ready event was received while IDENTIFYing. + """ return self._shard_information[0] if self._shard_information else None @property def shard_count(self) -> typing.Optional[int]: - """The total shard count for this bot, only available if this - ready event was received while identifying. + """Total shard count for this bot. + + This is only available if this ready event was received while IDENTIFYing. """ return self._shard_information[1] if self._shard_information else None @@ -235,12 +233,12 @@ class BaseChannelEvent(HikariEvent, snowflakes.UniqueEntity, entities.Deserializ #: The channel's type. #: - #: :type: :obj:`channels.ChannelType` + #: :type: :obj:`hikari.core.channels.ChannelType` type: channels.ChannelType = marshaller.attrib(deserializer=channels.ChannelType) #: The ID of the guild this channel is in, will be ``None`` for DMs. #: - #: :type: :obj:`snowflakes.Snowflake`, optional + #: :type: :obj:`hikari.core.snowflakes.Snowflake`, optional guild_id: typing.Optional[snowflakes.Snowflake] = marshaller.attrib( deserializer=snowflakes.Snowflake.deserialize, if_none=None ) @@ -253,7 +251,7 @@ class BaseChannelEvent(HikariEvent, snowflakes.UniqueEntity, entities.Deserializ #: An mapping of the set permission overwrites for this channel, if applicable. #: - #: :type: :obj:`typing.Mapping` [ :obj:`snowflakes.Snowflake`, :obj:`channels.PermissionOverwrite` ], optional + #: :type: :obj:`typing.Mapping` [ :obj:`hikari.core.snowflakes.Snowflake`, :obj:`hikari.core.channels.PermissionOverwrite` ], optional permission_overwrites: typing.Optional[ typing.Mapping[snowflakes.Snowflake, channels.PermissionOverwrite] ] = marshaller.attrib( @@ -278,7 +276,7 @@ class BaseChannelEvent(HikariEvent, snowflakes.UniqueEntity, entities.Deserializ #: The ID of the last message sent, if it's a text type channel. #: - #: :type: :obj:`snowflakes.Snowflake`, optional + #: :type: :obj:`hikari.core.snowflakes.Snowflake`, optional last_message_id: typing.Optional[snowflakes.Snowflake] = marshaller.attrib( deserializer=snowflakes.Snowflake.deserialize, if_none=None, if_undefined=None ) @@ -303,7 +301,7 @@ class BaseChannelEvent(HikariEvent, snowflakes.UniqueEntity, entities.Deserializ #: A mapping of this channel's recipient users, if it's a DM or group DM. #: - #: :type: :obj:`typing.Mapping` [ :obj:`snowflakes.Snowflake`, :obj:`users.User` ], optional + #: :type: :obj:`typing.Mapping` [ :obj:`hikari.core.snowflakes.Snowflake`, :obj:`hikari.core.users.User` ], optional recipients: typing.Optional[typing.Mapping[snowflakes.Snowflake, users.User]] = marshaller.attrib( deserializer=lambda recipients: {user.id: user for user in map(users.User.deserialize, recipients)}, if_undefined=None, @@ -318,7 +316,7 @@ class BaseChannelEvent(HikariEvent, snowflakes.UniqueEntity, entities.Deserializ #: The ID of this channel's creator, if it's a DM channel. #: - #: :type: :obj:`snowflakes.Snowflake`, optional + #: :type: :obj:`hikari.core.snowflakes.Snowflake`, optional owner_id: typing.Optional[snowflakes.Snowflake] = marshaller.attrib( deserializer=snowflakes.Snowflake.deserialize, if_undefined=None ) @@ -326,14 +324,14 @@ class BaseChannelEvent(HikariEvent, snowflakes.UniqueEntity, entities.Deserializ #: The ID of the application that created the group DM, if it's a #: bot based group DM. #: - #: :type: :obj:`snowflakes.Snowflake`, optional + #: :type: :obj:`hikari.core.snowflakes.Snowflake`, optional application_id: typing.Optional[snowflakes.Snowflake] = marshaller.attrib( deserializer=snowflakes.Snowflake.deserialize, if_undefined=None ) #: The ID of this channels's parent category within guild, if set. #: - #: :type: :obj:`snowflakes.Snowflake`, optional + #: :type: :obj:`hikari.core.snowflakes.Snowflake`, optional parent_id: typing.Optional[snowflakes.Snowflake] = marshaller.attrib( deserializer=snowflakes.Snowflake.deserialize, if_undefined=None, if_none=None ) @@ -373,6 +371,7 @@ class ChannelDeleteEvent(BaseChannelEvent): @marshaller.attrs(slots=True) class ChannelPinUpdateEvent(HikariEvent, entities.Deserializable): """Used to represent the Channel Pins Update gateway event. + Sent when a message is pinned or unpinned in a channel but not when a pinned message is deleted. """ @@ -380,14 +379,14 @@ class ChannelPinUpdateEvent(HikariEvent, entities.Deserializable): #: The ID of the guild where this event happened. #: Will be ``None`` if this happened in a DM channel. #: - #: :type: :obj:`snowflake.Snowflake`, optional + #: :type: :obj:`hikari.core.snowflake.Snowflake`, optional guild_id: typing.Optional[snowflakes.Snowflake] = marshaller.attrib( deserializer=snowflakes.Snowflake.deserialize, if_undefined=None ) #: The ID of the channel where the message was pinned or unpinned. #: - #: :type: :obj:`snowflake.Snowflake` + #: :type: :obj:`hikari.core.snowflake.Snowflake` channel_id: snowflakes.Snowflake = marshaller.attrib(deserializer=snowflakes.Snowflake.deserialize) #: The datetime of when the most recent message was pinned in this channel. @@ -443,12 +442,12 @@ class BaseGuildBanEvent(HikariEvent, entities.Deserializable): #: The ID of the guild this ban is in. #: - #: :type: :obj:`snowflake.Snowflake` + #: :type: :obj:`hikari.core.snowflake.Snowflake` guild_id: snowflakes.Snowflake = marshaller.attrib(deserializer=snowflakes.Snowflake.deserialize) #: The object of the user this ban targets. #: - #: :type: :obj:`users.User` + #: :type: :obj:`hikari.core.users.User` user: users.User = marshaller.attrib(deserializer=users.User.deserialize) @@ -471,12 +470,12 @@ class GuildEmojisUpdateEvent(HikariEvent, entities.Deserializable): #: The ID of the guild this emoji was updated in. #: - #: :type: :obj:`snowflakes.Snowflake` + #: :type: :obj:`hikari.core.snowflakes.Snowflake` guild_id: snowflakes.Snowflake = marshaller.attrib(deserializer=snowflakes.Snowflake.deserialize) #: The updated mapping of emojis by their ID. #: - #: :type: :obj:`typing.Mapping` [ :obj:`snowflakes.Snowflake`, :obj:`_emojis.GuildEmoji` ] + #: :type: :obj:`typing.Mapping` [ :obj:`hikari.core.snowflakes.Snowflake`, :obj:`hikari.core.emojis.GuildEmoji` ] emojis: typing.Mapping[snowflakes.Snowflake, _emojis.GuildEmoji] = marshaller.attrib( deserializer=lambda ems: {emoji.id: emoji for emoji in map(_emojis.GuildEmoji.deserialize, ems)} ) @@ -489,7 +488,7 @@ class GuildIntegrationsUpdateEvent(HikariEvent, entities.Deserializable): #: The ID of the guild the integration was updated in. #: - #: :type: :obj:`snowflake.Snowflake` + #: :type: :obj:`hikari.core.snowflake.Snowflake` guild_id: snowflakes.Snowflake = marshaller.attrib(deserializer=snowflakes.Snowflake.deserialize) @@ -500,7 +499,7 @@ class GuildMemberAddEvent(HikariEvent, guilds.GuildMember): #: The ID of the guild where this member was added. #: - #: :type: :obj:`snowflakes.Snowflake` + #: :type: :obj:`hikari.core.snowflakes.Snowflake` guild_id: snowflakes.Snowflake = marshaller.attrib(deserializer=snowflakes.Snowflake.deserialize) @@ -508,17 +507,18 @@ class GuildMemberAddEvent(HikariEvent, guilds.GuildMember): @marshaller.attrs(slots=True) class GuildMemberRemoveEvent(HikariEvent, entities.Deserializable): """Used to represent Guild Member Remove gateway events. + Sent when a member is kicked, banned or leaves a guild. """ #: The ID of the guild this user was removed from. #: - #: :type: :obj:`snowflakes.Snowflake` + #: :type: :obj:`hikari.core.snowflakes.Snowflake` guild_id: snowflakes.Snowflake = marshaller.attrib(deserializer=snowflakes.Snowflake.deserialize) #: The object of the user who was removed from this guild. #: - #: :type: :obj:`users.User` + #: :type: :obj:`hikari.core.users.User` user: users.User = marshaller.attrib(deserializer=users.User.deserialize) @@ -526,30 +526,31 @@ class GuildMemberRemoveEvent(HikariEvent, entities.Deserializable): @marshaller.attrs(slots=True) class GuildMemberUpdateEvent(HikariEvent, entities.Deserializable): """Used to represent a Guild Member Update gateway event. + Sent when a guild member or their inner user object is updated. """ #: The ID of the guild this member was updated in. #: - #: :type: :obj:`snowflakes.Snowflake` + #: :type: :obj:`hikari.core.snowflakes.Snowflake` guild_id: snowflakes.Snowflake = marshaller.attrib(deserializer=snowflakes.Snowflake.deserialize) #: A sequence of the IDs of the member's current roles. #: - #: :type: :obj:`typing.Sequence` [ :obj:`snowflakes.Snowflake` ] + #: :type: :obj:`typing.Sequence` [ :obj:`hikari.core.snowflakes.Snowflake` ] role_ids: typing.Sequence[snowflakes.Snowflake] = marshaller.attrib( raw_name="roles", deserializer=lambda role_ids: [snowflakes.Snowflake.deserialize(rid) for rid in role_ids], ) #: The object of the user who was updated. #: - #: :type: :obj:`users.User` + #: :type: :obj:`hikari.core.users.User` user: users.User = marshaller.attrib(deserializer=users.User.deserialize) - #: This member's nickname. When set to :obj:`None`, this has been removed - #: and when set to :obj:`entities.UNSET` this hasn't been acted on. + #: This member's nickname. When set to ``None``, this has been removed + #: and when set to :obj:`hikari.core.entities.UNSET` this hasn't been acted on. #: - #: :type: :obj:`typing.Union` [ :obj:`str`, :obj:`entities.UNSET` ], optional + #: :type: :obj:`typing.Union` [ :obj:`str`, :obj:`hikari.core.entities.UNSET` ], optional nickname: typing.Union[None, str, entities.Unset] = marshaller.attrib( raw_name="nick", deserializer=str, if_none=None, if_undefined=entities.Unset, ) @@ -557,7 +558,7 @@ class GuildMemberUpdateEvent(HikariEvent, entities.Deserializable): #: The datetime of when this member started "boosting" this guild. #: Will be ``None`` if they aren't boosting. #: - #: :type: :obj:`typing.Union` [ :obj:`datetime.datetime`, :obj:`entities.UNSET` ], optional + #: :type: :obj:`typing.Union` [ :obj:`datetime.datetime`, :obj:`hikari.core.entities.UNSET` ], optional premium_since: typing.Union[None, datetime.datetime, entities.Unset] = marshaller.attrib( deserializer=hikari.internal.conversions.parse_iso_8601_ts, if_none=None, if_undefined=entities.Unset ) @@ -570,12 +571,12 @@ class GuildRoleCreateEvent(HikariEvent, entities.Deserializable): #: The ID of the guild where this role was created. #: - #: :type: :obj:`snowflakes.Snowflake` + #: :type: :obj:`hikari.core.snowflakes.Snowflake` guild_id: snowflakes.Snowflake = marshaller.attrib(deserializer=snowflakes.Snowflake.deserialize) #: The object of the role that was created. #: - #: :type: :obj:`guilds.GuildRole` + #: :type: :obj:`hikari.core.guilds.GuildRole` role: guilds.GuildRole = marshaller.attrib(deserializer=guilds.GuildRole.deserialize) @@ -586,12 +587,12 @@ class GuildRoleUpdateEvent(HikariEvent, entities.Deserializable): #: The ID of the guild where this role was updated. #: - #: :type: :obj:`snowflakes.Snowflake` + #: :type: :obj:`hikari.core.snowflakes.Snowflake` guild_id: snowflakes.Snowflake = marshaller.attrib(deserializer=snowflakes.Snowflake.deserialize) #: The updated role object. #: - #: :type: :obj:`guilds.GuildRole` + #: :type: :obj:`hikari.core.guilds.GuildRole` role: guilds.GuildRole = marshaller.attrib(deserializer=guilds.GuildRole.deserialize) @@ -602,12 +603,12 @@ class GuildRoleDeleteEvent(HikariEvent, entities.Deserializable): #: The ID of the guild where this role is being deleted. #: - #: :type: :obj:`snowflakes.Snowflake` + #: :type: :obj:`hikari.core.snowflakes.Snowflake` guild_id: snowflakes.Snowflake = marshaller.attrib(deserializer=snowflakes.Snowflake.deserialize) #: The ID of the role being deleted. #: - #: :type: :obj:`snowflakes.Snowflake` + #: :type: :obj:`hikari.core.snowflakes.Snowflake` role_id: snowflakes.Snowflake = marshaller.attrib(deserializer=snowflakes.Snowflake.deserialize) @@ -618,7 +619,7 @@ class InviteCreateEvent(HikariEvent, entities.Deserializable): #: The ID of the channel this invite targets. #: - #: :type: :obj:`snowflakes.Snowflake` + #: :type: :obj:`hikari.core.snowflakes.Snowflake` channel_id: snowflakes.Snowflake = marshaller.attrib(deserializer=snowflakes.Snowflake.deserialize) #: The code that identifies this invite @@ -634,18 +635,18 @@ class InviteCreateEvent(HikariEvent, entities.Deserializable): #: The ID of the guild this invite was created in, if applicable. #: Will be ``None`` for group DM invites. #: - #: :type: :obj:`snowflakes.Snowflake`, optional + #: :type: :obj:`hikari.core.snowflakes.Snowflake`, optional guild_id: typing.Optional[snowflakes.Snowflake] = marshaller.attrib( deserializer=snowflakes.Snowflake.deserialize, if_undefined=None ) #: The object of the user who created this invite, if applicable. #: - #: :type: :obj:`users.User`, optional + #: :type: :obj:`hikari.core.users.User`, optional inviter: typing.Optional[users.User] = marshaller.attrib(deserializer=users.User.deserialize, if_undefined=None) #: The timedelta of how long this invite will be valid for. - #: If set to :obj:`None` then this is unlimited. + #: If set to ``None`` then this is unlimited. #: #: :type: :obj:`datetime.timedelta`, optional max_age: typing.Optional[datetime.timedelta] = marshaller.attrib( @@ -660,12 +661,12 @@ class InviteCreateEvent(HikariEvent, entities.Deserializable): #: The object of the user who this invite targets, if set. #: - #: :type: :obj:`users.User`, optional + #: :type: :obj:`hikari.core.users.User`, optional target_user: typing.Optional[users.User] = marshaller.attrib(deserializer=users.User.deserialize, if_undefined=None) #: The type of user target this invite is, if applicable. #: - #: :type: :obj:`invites.TargetUserType`, optional + #: :type: :obj:`hikari.core.invites.TargetUserType`, optional target_user_type: typing.Optional[invites.TargetUserType] = marshaller.attrib( deserializer=invites.TargetUserType, if_undefined=None ) @@ -685,12 +686,13 @@ class InviteCreateEvent(HikariEvent, entities.Deserializable): @marshaller.attrs(slots=True) class InviteDeleteEvent(HikariEvent, entities.Deserializable): """Used to represent Invite Delete gateway events. + Sent when an invite is deleted for a channel we can access. """ #: The ID of the channel this ID was attached to #: - #: :type: :obj:`snowflakes.Snowflake` + #: :type: :obj:`hikari.core.snowflakes.Snowflake` channel_id: snowflakes.Snowflake = marshaller.attrib(deserializer=snowflakes.Snowflake.deserialize) #: The code of this invite. @@ -701,7 +703,7 @@ class InviteDeleteEvent(HikariEvent, entities.Deserializable): #: The ID of the guild this invite was deleted in. #: This will be ``None`` if this invite belonged to a DM channel. #: - #: :type: :obj:`snowflakes.Snowflake`, optional + #: :type: :obj:`hikari.core.snowflakes.Snowflake`, optional guild_id: typing.Optional[snowflakes.Snowflake] = marshaller.attrib( deserializer=snowflakes.Snowflake.deserialize, if_undefined=None ) @@ -717,80 +719,77 @@ class MessageCreateEvent(HikariEvent, messages.Message): @mark_as_websocket_event @marshaller.attrs(slots=True) class MessageUpdateEvent(HikariEvent, snowflakes.UniqueEntity, entities.Deserializable): - """ - Represents Message Update gateway events. + """Represents Message Update gateway events. Note ---- - All fields on this model except :attr:`channel_id` and :attr:`id` may be - set to :obj:`entities.UNSET` (a singleton defined in - ``hikari.core.entities``) if we've not received information about their - state from Discord alongside field nullability. + set to :obj:`hikari.core.entities.UNSET` if we have not received information + about their state from Discord alongside field nullability. """ #: The ID of the channel that the message was sent in. #: - #: :type: :obj:`snowflakes.Snowflake` + #: :type: :obj:`hikari.core.snowflakes.Snowflake` channel_id: snowflakes.Snowflake = marshaller.attrib(deserializer=snowflakes.Snowflake.deserialize) #: The ID of the guild that the message was sent in. #: - #: :type: :obj:`typing.Union` [ :obj:`snowflakes.Snowflake`, :obj:`entities.UNSET` ] + #: :type: :obj:`typing.Union` [ :obj:`hikari.core.snowflakes.Snowflake`, :obj:`hikari.core.entities.UNSET` ] guild_id: typing.Union[snowflakes.Snowflake, entities.Unset] = marshaller.attrib( deserializer=snowflakes.Snowflake.deserialize, if_undefined=entities.Unset ) #: The author of this message. #: - #: :type: :obj:`typing.Union` [ :obj:`users.User`, :obj:`entities.UNSET` ] + #: :type: :obj:`typing.Union` [ :obj:`hikari.core.users.User`, :obj:`hikari.core.entities.UNSET` ] author: typing.Union[users.User, entities.Unset] = marshaller.attrib( deserializer=users.User.deserialize, if_undefined=entities.Unset ) #: The member properties for the message's author. #: - #: :type: :obj:`typing.Union` [ :obj:`guilds.GuildMember`, :obj:`entities.UNSET` ] + #: :type: :obj:`typing.Union` [ :obj:`hikari.core.guilds.GuildMember`, :obj:`hikari.core.entities.UNSET` ] member: typing.Union[guilds.GuildMember, entities.Unset] = marshaller.attrib( deserializer=guilds.GuildMember.deserialize, if_undefined=entities.Unset ) #: The content of the message. #: - #: :type: :obj:`typing.Union` [ :obj:`str`, :obj:`entities.UNSET` ] + #: :type: :obj:`typing.Union` [ :obj:`str`, :obj:`hikari.core.entities.UNSET` ] content: typing.Union[str, entities.Unset] = marshaller.attrib(deserializer=str, if_undefined=entities.Unset) #: The timestamp that the message was sent at. #: - #: :type: :obj:`typing.Union` [ :obj:`datetime.datetime`, :obj:`entities.UNSET` ] + #: :type: :obj:`typing.Union` [ :obj:`datetime.datetime`, :obj:`hikari.core.entities.UNSET` ] timestamp: typing.Union[datetime.datetime, entities.Unset] = marshaller.attrib( deserializer=hikari.internal.conversions.parse_iso_8601_ts, if_undefined=entities.Unset ) #: The timestamp that the message was last edited at, or ``None`` if not ever edited. #: - #: :type: :obj:`typing.Union` [ :obj:`datetime.datetime`, :obj:`entities.UNSET` ], optional + #: :type: :obj:`typing.Union` [ :obj:`datetime.datetime`, :obj:`hikari.core.entities.UNSET` ], optional edited_timestamp: typing.Union[datetime.datetime, entities.Unset, None] = marshaller.attrib( deserializer=hikari.internal.conversions.parse_iso_8601_ts, if_none=None, if_undefined=entities.Unset ) #: Whether the message is a TTS message. #: - #: :type: :obj:`typing.Union` [ :obj:`bool`, :obj:`entities.UNSET` ] + #: :type: :obj:`typing.Union` [ :obj:`bool`, :obj:`hikari.core.entities.UNSET` ] is_tts: typing.Union[bool, entities.Unset] = marshaller.attrib( raw_name="tts", deserializer=bool, if_undefined=entities.Unset ) #: Whether the message mentions ``@everyone`` or ``@here``. #: - #: :type: :obj:`typing.Union` [ :obj:`bool`, :obj:`entities.UNSET` ] + #: :type: :obj:`typing.Union` [ :obj:`bool`, :obj:`hikari.core.entities.UNSET` ] is_mentioning_everyone: typing.Union[bool, entities.Unset] = marshaller.attrib( raw_name="mention_everyone", deserializer=bool, if_undefined=entities.Unset ) #: The users the message mentions. #: - #: :type: :obj:`typing.Union` [ :obj:`typing.Set` [ :obj:`snowflakes.Snowflake` ], :obj:`entities.UNSET` ] + #: :type: :obj:`typing.Union` [ :obj:`typing.Set` [ :obj:`hikari.core.snowflakes.Snowflake` ], :obj:`hikari.core.entities.UNSET` ] user_mentions: typing.Union[typing.Set[snowflakes.Snowflake], entities.Unset] = marshaller.attrib( raw_name="mentions", deserializer=lambda user_mentions: {snowflakes.Snowflake.deserialize(u["id"]) for u in user_mentions}, @@ -799,7 +798,7 @@ class MessageUpdateEvent(HikariEvent, snowflakes.UniqueEntity, entities.Deserial #: The roles the message mentions. #: - #: :type: :obj:`typing.Union` [ :obj:`typing.Set` [ :obj:`snowflakes.Snowflake` ], :obj:`entities.UNSET` ] + #: :type: :obj:`typing.Union` [ :obj:`typing.Set` [ :obj:`hikari.core.snowflakes.Snowflake` ], :obj:`hikari.core.entities.UNSET` ] role_mentions: typing.Union[typing.Set[snowflakes.Snowflake], entities.Unset] = marshaller.attrib( raw_name="mention_roles", deserializer=lambda role_mentions: {snowflakes.Snowflake.deserialize(r) for r in role_mentions}, @@ -817,7 +816,7 @@ class MessageUpdateEvent(HikariEvent, snowflakes.UniqueEntity, entities.Deserial #: The message attachments. #: - #: :type: :obj:`typing.Union` [ :obj:`typing.Sequence` [ :obj:`messages.Attachment` ], :obj:`entities.UNSET` ] + #: :type: :obj:`typing.Union` [ :obj:`typing.Sequence` [ :obj:`hikari.core.messages.Attachment` ], :obj:`hikari.core.entities.UNSET` ] attachments: typing.Union[typing.Sequence[messages.Attachment], entities.Unset] = marshaller.attrib( deserializer=lambda attachments: [messages.Attachment.deserialize(a) for a in attachments], if_undefined=entities.Unset, @@ -825,14 +824,14 @@ class MessageUpdateEvent(HikariEvent, snowflakes.UniqueEntity, entities.Deserial #: The message's embeds. #: - #: :type: :obj:`typing.Union` [ :obj:`typing.Sequence` [ :obj:`_embeds.Embed` ], :obj:`entities.UNSET` ] + #: :type: :obj:`typing.Union` [ :obj:`typing.Sequence` [ :obj:`hikari.core.embeds.Embed` ], :obj:`hikari.core.entities.UNSET` ] embeds: typing.Union[typing.Sequence[_embeds.Embed], entities.Unset] = marshaller.attrib( deserializer=lambda embed_objs: [_embeds.Embed.deserialize(e) for e in embed_objs], if_undefined=entities.Unset, ) #: The message's reactions. #: - #: :type: :obj:`typing.Union` [ :obj:`typing.Sequence` [ :obj:`messages.Reaction` ], :obj:`entities.UNSET` ] + #: :type: :obj:`typing.Union` [ :obj:`typing.Sequence` [ :obj:`hikari.core.messages.Reaction` ], :obj:`hikari.core.entities.UNSET` ] reactions: typing.Union[typing.Sequence[messages.Reaction], entities.Unset] = marshaller.attrib( deserializer=lambda reactions: [messages.Reaction.deserialize(r) for r in reactions], if_undefined=entities.Unset, @@ -840,49 +839,49 @@ class MessageUpdateEvent(HikariEvent, snowflakes.UniqueEntity, entities.Deserial #: Whether the message is pinned. #: - #: :type: :obj:`typing.Union` [ :obj:`bool`, :obj:`entities.UNSET` ] + #: :type: :obj:`typing.Union` [ :obj:`bool`, :obj:`hikari.core.entities.UNSET` ] is_pinned: typing.Union[bool, entities.Unset] = marshaller.attrib( raw_name="pinned", deserializer=bool, if_undefined=entities.Unset ) #: If the message was generated by a webhook, the webhook's id. #: - #: :type: :obj:`typing.Union` [ :obj:`snowflakes.Snowflake`, :obj:`entities.UNSET` ] + #: :type: :obj:`typing.Union` [ :obj:`hikari.core.snowflakes.Snowflake`, :obj:`hikari.core.entities.UNSET` ] webhook_id: typing.Union[snowflakes.Snowflake, entities.Unset] = marshaller.attrib( deserializer=snowflakes.Snowflake.deserialize, if_undefined=entities.Unset ) #: The message's type. #: - #: :type: :obj:`typing.Union` [ :obj:`messages.MessageType`, :obj:`entities.UNSET` ] + #: :type: :obj:`typing.Union` [ :obj:`hikari.core.messages.MessageType`, :obj:`hikari.core.entities.UNSET` ] type: typing.Union[messages.MessageType, entities.Unset] = marshaller.attrib( deserializer=messages.MessageType, if_undefined=entities.Unset ) #: The message's activity. #: - #: :type: :obj:`typing.Union` [ :obj:`messages.MessageActivity`, :obj:`entities.UNSET` ] + #: :type: :obj:`typing.Union` [ :obj:`hikari.core.messages.MessageActivity`, :obj:`hikari.core.entities.UNSET` ] activity: typing.Union[messages.MessageActivity, entities.Unset] = marshaller.attrib( deserializer=messages.MessageActivity.deserialize, if_undefined=entities.Unset ) #: The message's application. #: - #: :type: :obj:`typing.Union` [ :obj:`oauth2.Application`, :obj:`entities.UNSET` ] + #: :type: :obj:`typing.Union` [ :obj:`hikari.core.oauth2.Application`, :obj:`hikari.core.entities.UNSET` ] application: typing.Optional[oauth2.Application] = marshaller.attrib( deserializer=oauth2.Application.deserialize, if_undefined=entities.Unset ) #: The message's crossposted reference data. #: - #: :type: :obj:`typing.Union` [ :obj:`MessageCrosspost`, :obj:`entities.UNSET` ] + #: :type: :obj:`typing.Union` [ :obj:`hikari.core.messages.MessageCrosspost`, :obj:`hikari.core.entities.UNSET` ] message_reference: typing.Union[messages.MessageCrosspost, entities.Unset] = marshaller.attrib( deserializer=messages.MessageCrosspost.deserialize, if_undefined=entities.Unset ) #: The message's flags. #: - #: :type: :obj:`typing.Union` [ :obj:`messages.MessageFlag`, :obj:`entities.UNSET` ] + #: :type: :obj:`typing.Union` [ :obj:`hikari.core.messages.MessageFlag`, :obj:`hikari.core.entities.UNSET` ] flags: typing.Union[messages.MessageFlag, entities.Unset] = marshaller.attrib( deserializer=messages.MessageFlag, if_undefined=entities.Unset ) @@ -892,24 +891,25 @@ class MessageUpdateEvent(HikariEvent, snowflakes.UniqueEntity, entities.Deserial @marshaller.attrs(slots=True) class MessageDeleteEvent(HikariEvent, entities.Deserializable): """Used to represent Message Delete gateway events. + Sent when a message is deleted in a channel we have access to. """ #: The ID of the channel where this message was deleted. #: - #: :type: :obj:`snowflakes.Snowflake` + #: :type: :obj:`hikari.core.snowflakes.Snowflake` channel_id: snowflakes.Snowflake = marshaller.attrib(deserializer=snowflakes.Snowflake.deserialize) #: The ID of the guild where this message was deleted. #: Will be ``None`` if this message was deleted in a DM channel. #: - #: :type: :obj:`snowflakes.Snowflake`, optional + #: :type: :obj:`hikari.core.snowflakes.Snowflake`, optional guild_id: typing.Optional[snowflakes.Snowflake] = marshaller.attrib( deserializer=snowflakes.Snowflake.deserialize, if_undefined=None ) #: The ID of the message that was deleted. #: - #: :type: :obj:`snowflakes.Snowflake` + #: :type: :obj:`hikari.core.snowflakes.Snowflake` message_id: snowflakes.Snowflake = marshaller.attrib(raw_name="id", deserializer=snowflakes.Snowflake.deserialize) @@ -917,25 +917,26 @@ class MessageDeleteEvent(HikariEvent, entities.Deserializable): @marshaller.attrs(slots=True) class MessageDeleteBulkEvent(HikariEvent, entities.Deserializable): """Used to represent Message Bulk Delete gateway events. + Sent when multiple messages are deleted in a channel at once. """ #: The ID of the channel these messages have been deleted in. #: - #: :type: :obj:`snowflakes.Snowflake` + #: :type: :obj:`hikari.core.snowflakes.Snowflake` channel_id: snowflakes.Snowflake = marshaller.attrib(deserializer=snowflakes.Snowflake.deserialize) #: The ID of the channel these messages have been deleted in. #: Will be ``None`` if these messages were bulk deleted in a DM channel. #: - #: :type: :obj:`snowflakes.Snowflake`, optional + #: :type: :obj:`hikari.core.snowflakes.Snowflake`, optional guild_id: typing.Optional[snowflakes.Snowflake] = marshaller.attrib( deserializer=snowflakes.Snowflake.deserialize, if_none=None ) #: A collection of the IDs of the messages that were deleted. #: - #: :type: :obj:`typing.Set` [ :obj:`snowflakes.Snowflake` ] + #: :type: :obj:`typing.Set` [ :obj:`hikari.core.snowflakes.Snowflake` ] message_ids: typing.Set[snowflakes.Snowflake] = marshaller.attrib( raw_name="ids", deserializer=lambda msgs: {snowflakes.Snowflake.deserialize(m) for m in msgs} ) @@ -948,23 +949,23 @@ class MessageReactionAddEvent(HikariEvent, entities.Deserializable): #: The ID of the user adding the reaction. #: - #: :type: :obj:`snowflakes.Snowflake` + #: :type: :obj:`hikari.core.snowflakes.Snowflake` user_id: snowflakes.Snowflake = marshaller.attrib(deserializer=snowflakes.Snowflake.deserialize) #: The ID of the channel where this reaction is being added. #: - #: :type: :obj:`snowflakes.Snowflake` + #: :type: :obj:`hikari.core.snowflakes.Snowflake` channel_id: snowflakes.Snowflake = marshaller.attrib(deserializer=snowflakes.Snowflake.deserialize) #: The ID of the message this reaction is being added to. #: - #: :type: :obj:`snowflakes.Snowflake` + #: :type: :obj:`hikari.core.snowflakes.Snowflake` message_id: snowflakes.Snowflake = marshaller.attrib(deserializer=snowflakes.Snowflake.deserialize) #: The ID of the guild where this reaction is being added, unless this is #: happening in a DM channel. #: - #: :type: :obj:`snowflakes.Snowflake`, optional + #: :type: :obj:`hikari.core.snowflakes.Snowflake`, optional guild_id: typing.Optional[snowflakes.Snowflake] = marshaller.attrib( deserializer=snowflakes.Snowflake.deserialize, if_undefined=None ) @@ -972,14 +973,14 @@ class MessageReactionAddEvent(HikariEvent, entities.Deserializable): #: The member object of the user who's adding this reaction, if this is #: occurring in a guild. #: - #: :type: :obj:`guilds.GuildMember`, optional + #: :type: :obj:`hikari.core.guilds.GuildMember`, optional member: typing.Optional[guilds.GuildMember] = marshaller.attrib( deserializer=guilds.GuildMember.deserialize, if_undefined=None ) #: The object of the emoji being added. #: - #: :type: :obj:`typing.Union` [ :obj:`_emojis.UnknownEmoji`, `_emojis.UnicodeEmoji` ] + #: :type: :obj:`typing.Union` [ :obj:`hikari.core.emojis.UnknownEmoji`, :obj:`hikari.core.emojis.UnicodeEmoji` ] emoji: typing.Union[_emojis.UnknownEmoji, _emojis.UnicodeEmoji] = marshaller.attrib( deserializer=_emojis.deserialize_reaction_emoji, ) @@ -992,30 +993,30 @@ class MessageReactionRemoveEvent(HikariEvent, entities.Deserializable): #: The ID of the user who is removing their reaction. #: - #: :type: :obj:`snowflakes.Snowflake` + #: :type: :obj:`hikari.core.snowflakes.Snowflake` user_id: snowflakes.Snowflake = marshaller.attrib(deserializer=snowflakes.Snowflake.deserialize) #: The ID of the channel where this reaction is being removed. #: - #: :type: :obj:`snowflakes.Snowflake` + #: :type: :obj:`hikari.core.snowflakes.Snowflake` channel_id: snowflakes.Snowflake = marshaller.attrib(deserializer=snowflakes.Snowflake.deserialize) #: The ID of the message this reaction is being removed from. #: - #: :type: :obj:`snowflakes.Snowflake` + #: :type: :obj:`hikari.core.snowflakes.Snowflake` message_id: snowflakes.Snowflake = marshaller.attrib(deserializer=snowflakes.Snowflake.deserialize) #: The ID of the guild where this reaction is being removed, unless this is #: happening in a DM channel. #: - #: :type: :obj:`snowflakes.Snowflake`, optional + #: :type: :obj:`hikari.core.snowflakes.Snowflake`, optional guild_id: typing.Optional[snowflakes.Snowflake] = marshaller.attrib( deserializer=snowflakes.Snowflake.deserialize, if_undefined=None ) #: The object of the emoji being removed. #: - #: :type: :obj:`typing.Union` [ :obj:`_emojis.UnknownEmoji`, `_emojis.UnicodeEmoji` ] + #: :type: :obj:`typing.Union` [ :obj:`hikari.core.emojis.UnknownEmoji`, :obj:`hikari.core.emojis.UnicodeEmoji` ] emoji: typing.Union[_emojis.UnicodeEmoji, _emojis.UnknownEmoji] = marshaller.attrib( deserializer=_emojis.deserialize_reaction_emoji, ) @@ -1025,22 +1026,23 @@ class MessageReactionRemoveEvent(HikariEvent, entities.Deserializable): @marshaller.attrs(slots=True) class MessageReactionRemoveAllEvent(HikariEvent, entities.Deserializable): """Used to represent Message Reaction Remove All gateway events. + Sent when all the reactions are removed from a message, regardless of emoji. """ #: The ID of the channel where the targeted message is. #: - #: :type: :obj:`snowflakes.Snowflake` + #: :type: :obj:`hikari.core.snowflakes.Snowflake` channel_id: snowflakes.Snowflake = marshaller.attrib(deserializer=snowflakes.Snowflake.deserialize) #: The ID of the message all reactions are being removed from. #: - #: :type: :obj:`snowflakes.Snowflake` + #: :type: :obj:`hikari.core.snowflakes.Snowflake` message_id: snowflakes.Snowflake = marshaller.attrib(deserializer=snowflakes.Snowflake.deserialize) #: The ID of the guild where the targeted message is, if applicable. #: - #: :type: :obj:`snowflakes.Snowflake` + #: :type: :obj:`hikari.core.snowflakes.Snowflake` guild_id: typing.Optional[snowflakes.Snowflake] = marshaller.attrib( deserializer=snowflakes.Snowflake.deserialize, if_undefined=None ) @@ -1050,29 +1052,30 @@ class MessageReactionRemoveAllEvent(HikariEvent, entities.Deserializable): @marshaller.attrs(slots=True) class MessageReactionRemoveEmojiEvent(HikariEvent, entities.Deserializable): """Represents Message Reaction Remove Emoji events. + Sent when all the reactions for a single emoji are removed from a message. """ #: The ID of the channel where the targeted message is. #: - #: :type: :obj:`snowflakes.Snowflake` + #: :type: :obj:`hikari.core.snowflakes.Snowflake` channel_id: snowflakes.Snowflake = marshaller.attrib(deserializer=snowflakes.Snowflake.deserialize) #: The ID of the guild where the targeted message is, if applicable. #: - #: :type: :obj:`snowflakes.Snowflake` + #: :type: :obj:`hikari.core.snowflakes.Snowflake` guild_id: typing.Optional[snowflakes.Snowflake] = marshaller.attrib( deserializer=snowflakes.Snowflake.deserialize, if_undefined=None ) #: The ID of the message the reactions are being removed from. #: - #: :type: :obj:`snowflakes.Snowflake` + #: :type: :obj:`hikari.core.snowflakes.Snowflake` message_id: snowflakes.Snowflake = marshaller.attrib(deserializer=snowflakes.Snowflake.deserialize) #: The object of the emoji that's being removed. #: - #: :type: :obj:`typing.Union` [ :obj:`_emojis.UnknownEmoji`, `_emojis.UnicodeEmoji` ] + #: :type: :obj:`typing.Union` [ :obj:`hikari.core.emojis.UnknownEmoji`, :obj:`emojis.UnicodeEmoji` ] emoji: typing.Union[_emojis.UnicodeEmoji, _emojis.UnknownEmoji] = marshaller.attrib( deserializer=_emojis.deserialize_reaction_emoji, ) @@ -1082,6 +1085,7 @@ class MessageReactionRemoveEmojiEvent(HikariEvent, entities.Deserializable): @marshaller.attrs(slots=True) class PresenceUpdateEvent(HikariEvent, guilds.GuildMemberPresence): """Used to represent Presence Update gateway events. + Sent when a guild member changes their presence. """ @@ -1090,25 +1094,26 @@ class PresenceUpdateEvent(HikariEvent, guilds.GuildMemberPresence): @marshaller.attrs(slots=True) class TypingStartEvent(HikariEvent, entities.Deserializable): """Used to represent typing start gateway events. + Received when a user or bot starts "typing" in a channel. """ #: The ID of the channel this typing event is occurring in. #: - #: :type: :obj:`snowflakes.Snowflake` + #: :type: :obj:`hikari.core.snowflakes.Snowflake` channel_id: snowflakes.Snowflake = marshaller.attrib(deserializer=snowflakes.Snowflake.deserialize) #: The ID of the guild this typing event is occurring in. #: Will be ``None`` if this event is happening in a DM channel. #: - #: :type: :obj:`snowflakes.Snowflake`, optional + #: :type: :obj:`hikari.core.snowflakes.Snowflake`, optional guild_id: typing.Optional[snowflakes.Snowflake] = marshaller.attrib( deserializer=snowflakes.Snowflake.deserialize, if_undefined=None ) #: The ID of the user who triggered this typing event. #: - #: :type: :obj:`snowflakes.Snowflake` + #: :type: :obj:`hikari.core.snowflakes.Snowflake` user_id: snowflakes.Snowflake = marshaller.attrib(deserializer=snowflakes.Snowflake.deserialize) #: The datetime of when this typing event started. @@ -1121,7 +1126,7 @@ class TypingStartEvent(HikariEvent, entities.Deserializable): #: The member object of the user who triggered this typing event, #: if this was triggered in a guild. #: - #: :type: :obj:`guilds.GuildMember`, optional + #: :type: :obj:`hikari.core.guilds.GuildMember`, optional member: typing.Optional[guilds.GuildMember] = marshaller.attrib( deserializer=guilds.GuildMember.deserialize, if_undefined=None ) @@ -1131,6 +1136,7 @@ class TypingStartEvent(HikariEvent, entities.Deserializable): @marshaller.attrs(slots=True) class UserUpdateEvent(HikariEvent, users.MyUser): """Used to represent User Update gateway events. + Sent when the current user is updated. """ @@ -1139,6 +1145,7 @@ class UserUpdateEvent(HikariEvent, users.MyUser): @marshaller.attrs(slots=True) class VoiceStateUpdateEvent(HikariEvent, voices.VoiceState): """Used to represent voice state update gateway events. + Sent when a user joins, leaves or moves voice channel(s). """ @@ -1147,6 +1154,7 @@ class VoiceStateUpdateEvent(HikariEvent, voices.VoiceState): @marshaller.attrs(slots=True) class VoiceServerUpdateEvent(HikariEvent, entities.Deserializable): """Used to represent voice server update gateway events. + Sent when initially connecting to voice and when the current voice instance falls over to a new server. """ @@ -1158,7 +1166,7 @@ class VoiceServerUpdateEvent(HikariEvent, entities.Deserializable): #: The ID of the guild this voice server update is for #: - #: :type: :obj:`snowflakes.Snowflake` + #: :type: :obj:`hikari.core.snowflakes.Snowflake` guild_id: snowflakes.Snowflake = marshaller.attrib(deserializer=snowflakes.Snowflake.deserialize) #: The uri for this voice server host. @@ -1171,15 +1179,16 @@ class VoiceServerUpdateEvent(HikariEvent, entities.Deserializable): @marshaller.attrs(slots=True) class WebhookUpdateEvent(HikariEvent, entities.Deserializable): """Used to represent webhook update gateway events. + Sent when a webhook is updated, created or deleted in a guild. """ #: The ID of the guild this webhook is being updated in. #: - #: :type: :obj:`snowflakes.Snowflake` + #: :type: :obj:`hikari.core.snowflakes.Snowflake` guild_id: snowflakes.Snowflake = marshaller.attrib(deserializer=snowflakes.Snowflake.deserialize) #: The ID of the channel this webhook is being updated in. #: - #: :type: :obj:`snowflakes.Snowflake` + #: :type: :obj:`hikari.core.snowflakes.Snowflake` channel_id: snowflakes.Snowflake = marshaller.attrib(deserializer=snowflakes.Snowflake.deserialize) diff --git a/hikari/core/gateway_entities.py b/hikari/core/gateway_entities.py index d5523e7b1a..0a81439508 100644 --- a/hikari/core/gateway_entities.py +++ b/hikari/core/gateway_entities.py @@ -67,7 +67,7 @@ class GatewayBot(entities.HikariEntity, entities.Deserializable): #: Information about the bot's current session start limit. #: #: :type: :obj:`SessionStartLimit` - session_start_limit: int = marshaller.attrib(deserializer=SessionStartLimit.deserialize) + session_start_limit: SessionStartLimit = marshaller.attrib(deserializer=SessionStartLimit.deserialize) @marshaller.attrs(slots=True) @@ -82,14 +82,14 @@ class GatewayActivity(entities.Deserializable, entities.Serializable): #: :type: :obj:`str` name: str = marshaller.attrib(deserializer=str, serializer=str) - #: The activity url. Only valid for ``STREAMING`` activities. + #: The activity URL. Only valid for ``STREAMING`` activities. #: #: :type: :obj:`str`, optional url: typing.Optional[str] = marshaller.attrib(deserializer=str, serializer=str, if_none=None, if_undefined=None) #: The activity type. #: - #: :type: :obj:`guilds.ActivityType` + #: :type: :obj:`hikari.core.guilds.ActivityType` type: guilds.ActivityType = marshaller.attrib( deserializer=guilds.ActivityType, serializer=int, if_undefined=lambda: guilds.ActivityType.PLAYING ) diff --git a/hikari/core/guilds.py b/hikari/core/guilds.py index eabf3833f3..2ddb7185f6 100644 --- a/hikari/core/guilds.py +++ b/hikari/core/guilds.py @@ -16,8 +16,7 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . -"""Components and entities that are used to describe guilds on Discord. -""" +"""Components and entities that are used to describe guilds on Discord.""" __all__ = [ "ActivityFlag", "ActivityType", @@ -157,8 +156,7 @@ class GuildSystemChannelFlag(enum.IntFlag): @enum.unique class GuildVerificationLevel(enum.IntEnum): - """Represents the level of verification a user needs to provide for their - account before being allowed to participate in a guild.""" + """Represents the level of verification of a guild.""" #: Unrestricted NONE = 0 @@ -180,10 +178,10 @@ class GuildVerificationLevel(enum.IntEnum): class GuildMember(entities.HikariEntity, entities.Deserializable): """Used to represent a guild bound member.""" - #: This member's user object, will be :obj:`None` when attached to Message + #: This member's user object, will be ``None`` when attached to Message #: Create and Update gateway events. #: - #: :type: :obj:`users.User`, optional + #: :type: :obj:`hikari.core.users.User`, optional user: typing.Optional[users.User] = marshaller.attrib(deserializer=users.User.deserialize, if_undefined=None) #: This member's nickname, if set. @@ -195,7 +193,7 @@ class GuildMember(entities.HikariEntity, entities.Deserializable): #: A sequence of the IDs of the member's current roles. #: - #: :type: :obj:`typing.Sequence` [ :obj:`snowflakes.Snowflake` ] + #: :type: :obj:`typing.Sequence` [ :obj:`hikari.core.snowflakes.Snowflake` ] role_ids: typing.Sequence[snowflakes.Snowflake] = marshaller.attrib( raw_name="roles", deserializer=lambda role_ids: [snowflakes.Snowflake.deserialize(rid) for rid in role_ids], ) @@ -241,7 +239,7 @@ class GuildRole(PartialGuildRole): #: The colour of this role, will be applied to a member's name in chat #: if it's their top coloured role. #: - #: :type: :obj:`colors.Color` + #: :type: :obj:`hikari.core.colors.Color` color: colors.Color = marshaller.attrib(deserializer=colors.Color) #: Whether this role is hoisting the members it's attached to in the member @@ -259,7 +257,7 @@ class GuildRole(PartialGuildRole): #: The guild wide permissions this role gives to the members it's attached #: to, may be overridden by channel overwrites. #: - #: :type: :obj:`_permissions.Permission` + #: :type: :obj:`hikari.core.permissions.Permission` permissions: _permissions.Permission = marshaller.attrib(deserializer=_permissions.Permission) #: Whether this role is managed by an integration. @@ -276,16 +274,14 @@ class GuildRole(PartialGuildRole): @enum.unique class ActivityType(enum.IntEnum): - """ - The activity state. - """ + """The activity type.""" #: Shows up as ``Playing `` PLAYING = 0 #: Shows up as ``Streaming ``. #: - #: Warnings - #: -------- + #: Warning + #: ------- #: Corresponding presences must be associated with VALID Twitch or YouTube #: stream URLS! STREAMING = 1 @@ -336,14 +332,15 @@ class ActivityParty(entities.HikariEntity, entities.Deserializable): raw_name="size", deserializer=tuple, if_undefined=None, ) + # Ignore docstring not starting in an imperative mood @property - def current_size(self) -> typing.Optional[int]: - """The current size of this party, if applicable.""" + def current_size(self) -> typing.Optional[int]: # noqa: D401 + """Current size of this party, if applicable.""" return self._size_information[0] if self._size_information else None @property def max_size(self) -> typing.Optional[int]: - """The maximum size of this party, if applicable""" + """Maximum size of this party, if applicable.""" return self._size_information[1] if self._size_information else None @@ -393,9 +390,9 @@ class ActivitySecret(entities.HikariEntity, entities.Deserializable): class ActivityFlag(enum.IntFlag): - """ - Flags that describe what an activity includes, - can be more than one using bitwise-combinations. + """Flags that describe what an activity includes. + + This can be more than one using bitwise-combinations. """ INSTANCE = 1 << 0 @@ -420,9 +417,9 @@ class PresenceActivity(entities.HikariEntity, entities.Deserializable): #: :type: :obj:`ActivityType` type: ActivityType = marshaller.attrib(deserializer=ActivityType) - #: The url for a ``STREAM` type activity, if applicable + #: The URL for a ``STREAM`` type activity, if applicable. #: - #: :type: :obj:`url`, optional + #: :type: :obj:`str`, optional url: typing.Optional[str] = marshaller.attrib(deserializer=str, if_undefined=None, if_none=None) #: When this activity was added to the user's session. @@ -440,7 +437,7 @@ class PresenceActivity(entities.HikariEntity, entities.Deserializable): #: The ID of the application this activity is for, if applicable. #: - #: :type: :obj:`snowflakes.Snowflake`, optional + #: :type: :obj:`hikari.core.snowflakes.Snowflake`, optional application_id: typing.Optional[snowflakes.Snowflake] = marshaller.attrib( deserializer=snowflakes.Snowflake.deserialize, if_undefined=None ) @@ -457,7 +454,7 @@ class PresenceActivity(entities.HikariEntity, entities.Deserializable): #: The emoji of this activity, if it is a custom status and set. #: - #: :type: :obj:`typing.Union` [ :obj:`_emojis.UnicodeEmoji`, :obj:`_emojis.UnknownEmoji` ], optional + #: :type: :obj:`typing.Union` [ :obj:`hikari.core.emojis.UnicodeEmoji`, :obj:`hikari.core.emojis.UnknownEmoji` ], optional emoji: typing.Union[None, _emojis.UnicodeEmoji, _emojis.UnknownEmoji] = marshaller.attrib( deserializer=_emojis.deserialize_reaction_emoji, if_undefined=None ) @@ -535,37 +532,37 @@ class PresenceUser(users.User): Warnings -------- - Every attribute except :attr:`id` may be received as :obj:`entities.UNSET` + Every attribute except :attr:`id` may be received as :obj:`hikari.core.entities.UNSET` unless it is specifically being modified for this update. """ #: This user's discriminator. #: - #: :type: :obj:`typing.Union` [ :obj:`str`, `entities.UNSET` ] + #: :type: :obj:`typing.Union` [ :obj:`str`, :obj:`hikari.core.entities.UNSET` ] discriminator: typing.Union[str, entities.Unset] = marshaller.attrib(deserializer=str, if_undefined=entities.Unset) #: This user's username. #: - #: :type: :obj:`typing.Union` [ :obj:`str`, `entities.UNSET` ] + #: :type: :obj:`typing.Union` [ :obj:`str`, :obj:`hikari.core.entities.UNSET` ] username: typing.Union[str, entities.Unset] = marshaller.attrib(deserializer=str, if_undefined=entities.Unset) #: This user's avatar hash, if set. #: - #: :type: :obj:`typing.Union` [ :obj:`str`, `entities.UNSET` ], optional + #: :type: :obj:`typing.Union` [ :obj:`str`, :obj:`hikari.core.entities.UNSET` ], optional avatar_hash: typing.Union[None, str, entities.Unset] = marshaller.attrib( raw_name="avatar", deserializer=str, if_none=None, if_undefined=entities.Unset ) #: Whether this user is a bot account. #: - #: :type: :obj:`typing.Union` [ :obj:`bool`, `entities.UNSET` ] + #: :type: :obj:`typing.Union` [ :obj:`bool`, :obj:`hikari.core.entities.UNSET` ] is_bot: typing.Union[bool, entities.Unset] = marshaller.attrib( raw_name="bot", deserializer=bool, if_undefined=entities.Unset ) #: Whether this user is a system account. #: - #: :type: :obj:`typing.Union` [ :obj:`bool`, `entities.UNSET` ] + #: :type: :obj:`typing.Union` [ :obj:`bool`, :obj:`hikari.core.entities.UNSET` ] is_system: typing.Union[bool, entities.Unset] = marshaller.attrib( raw_name="system", deserializer=bool, if_undefined=entities.Unset, ) @@ -585,14 +582,14 @@ class GuildMemberPresence(entities.HikariEntity, entities.Deserializable): #: A sequence of the ids of the user's current roles in the guild this #: presence belongs to. #: - #: :type: :obj:`typing.Sequence` [ :obj:`snowflakes.Snowflake` ] + #: :type: :obj:`typing.Sequence` [ :obj:`hikari.core.snowflakes.Snowflake` ] role_ids: typing.Sequence[snowflakes.Snowflake] = marshaller.attrib( raw_name="roles", deserializer=lambda roles: [snowflakes.Snowflake.deserialize(rid) for rid in roles], ) #: The ID of the guild this presence belongs to. #: - #: :type: :obj:`snowflakes.Snowflake` + #: :type: :obj:`hikari.core.snowflakes.Snowflake` guild_id: snowflakes.Snowflake = marshaller.attrib(deserializer=snowflakes.Snowflake.deserialize) #: This user's current status being displayed by the client. @@ -688,7 +685,7 @@ class GuildIntegration(snowflakes.UniqueEntity, entities.Deserializable): #: The ID of the managed role used for this integration's subscribers. #: - #: :type: :obj:`snowflakes.Snowflake` + #: :type: :obj:`hikari.core.snowflakes.Snowflake` role_id: typing.Optional[snowflakes.Snowflake] = marshaller.attrib(deserializer=snowflakes.Snowflake.deserialize) #: Whether users under this integration are allowed to use it's custom @@ -737,30 +734,31 @@ class GuildMemberBan(entities.HikariEntity, entities.Deserializable): #: The object of the user this ban targets. #: - #: :type: :obj:`users.User` + #: :type: :obj:`hikari.core.users.User` user: users.User = marshaller.attrib(deserializer=users.User.deserialize) @marshaller.attrs(slots=True) class UnavailableGuild(snowflakes.UniqueEntity, entities.Deserializable): - """An unavailable guild object, received during gateway events such as - the "Ready". + """An unavailable guild object, received during gateway events such as READY. + An unavailable guild cannot be interacted with, and most information may be outdated if that is the case. """ + # Ignore docstring not starting in an imperative mood @property - def is_unavailable(self) -> bool: - """ - Whether this guild is unavailable or not, should always be :obj:`True`. + def is_unavailable(self) -> bool: # noqa: D401 + """``True`` if this guild is unavailable, or ``False`` if it is available. + + This value is always ``True``, and is only provided for consistency. """ return True @marshaller.attrs(slots=True) class PartialGuild(snowflakes.UniqueEntity, entities.Deserializable): - """This is a base object for any partial guild objects returned by the api - where we are only given limited information.""" + """Base object for any partial guild objects.""" #: The name of the guild. #: @@ -780,22 +778,22 @@ class PartialGuild(snowflakes.UniqueEntity, entities.Deserializable): ) def format_icon_url(self, fmt: typing.Optional[str] = None, size: int = 2048) -> typing.Optional[str]: - """Generate the url for this guild's custom icon, if set. + """Generate the URL for this guild's custom icon, if set. Parameters ---------- fmt : :obj:`str` - The format to use for this url, defaults to ``png`` or ``gif``. + The format to use for this URL, defaults to ``png`` or ``gif``. Supports ``png``, ``jpeg``, `jpg`, ``webp`` and ``gif`` (when animated). size : :obj:`int` - The size to set for the url, defaults to ``2048``. + The size to set for the URL, defaults to ``2048``. Can be any power of two between 16 and 2048. Returns ------- :obj:`str`, optional - The string url. + The string URL. """ if self.icon_hash: if fmt is None and self.icon_hash.startswith("a_"): @@ -807,7 +805,7 @@ def format_icon_url(self, fmt: typing.Optional[str] = None, size: int = 2048) -> @property def icon_url(self) -> typing.Optional[str]: - """The url for this guild's icon, if set.""" + """URL for this guild's icon, if set.""" return self.format_icon_url() @@ -837,14 +835,14 @@ class Guild(PartialGuild): #: The ID of the owner of this guild. #: - #: :type: :obj:`snowflakes.Snowflake` + #: :type: :obj:`hikari.core.snowflakes.Snowflake` owner_id: snowflakes.Snowflake = marshaller.attrib(deserializer=snowflakes.Snowflake.deserialize) #: The guild level permissions that apply to the bot user, #: Will be ``None`` when this object is retrieved through a REST request #: rather than from the gateway. #: - #: :type: :obj:`_permissions.Permission` + #: :type: :obj:`hikari.core.permissions.Permission` my_permissions: _permissions.Permission = marshaller.attrib( raw_name="permissions", deserializer=_permissions.Permission, if_undefined=None ) @@ -857,7 +855,7 @@ class Guild(PartialGuild): #: The ID for the channel that AFK voice users get sent to, if set for the #: guild. #: - #: :type: :obj:`snowflakes.Snowflake`, optional + #: :type: :obj:`hikari.core.snowflakes.Snowflake`, optional afk_channel_id: typing.Optional[snowflakes.Snowflake] = marshaller.attrib( deserializer=snowflakes.Snowflake.deserialize, if_none=None ) @@ -883,7 +881,7 @@ class Guild(PartialGuild): #: enabled for this guild. Will be ``None`` if invites are disable for this #: guild's embed. #: - #: :type: :obj:`snowflakes.Snowflake`, optional + #: :type: :obj:`hikari.core.snowflakes.Snowflake`, optional embed_channel_id: typing.Optional[snowflakes.Snowflake] = marshaller.attrib( deserializer=snowflakes.Snowflake.deserialize, if_none=None, if_undefined=None ) @@ -909,7 +907,7 @@ class Guild(PartialGuild): #: The roles in this guild, represented as a mapping of ID to role object. #: - #: :type: :obj:`typing.Mapping` [ :obj:`snowflakes.Snowflake`, :obj:`GuildRole` ] + #: :type: :obj:`typing.Mapping` [ :obj:`hikari.core.snowflakes.Snowflake`, :obj:`GuildRole` ] roles: typing.Mapping[snowflakes.Snowflake, GuildRole] = marshaller.attrib( deserializer=lambda roles: {r.id: r for r in map(GuildRole.deserialize, roles)}, ) @@ -917,7 +915,7 @@ class Guild(PartialGuild): #: The emojis that this guild provides, represented as a mapping of ID to #: emoji object. #: - #: :type: :obj:`typing.Mapping` [ :obj:`snowflakes.Snowflake`, :obj:`_emojis.GuildEmoji` ] + #: :type: :obj:`typing.Mapping` [ :obj:`hikari.core.snowflakes.Snowflake`, :obj:`hikari.core.emojis.GuildEmoji` ] emojis: typing.Mapping[snowflakes.Snowflake, _emojis.GuildEmoji] = marshaller.attrib( deserializer=lambda emojis: {e.id: e for e in map(_emojis.GuildEmoji.deserialize, emojis)}, ) @@ -930,7 +928,7 @@ class Guild(PartialGuild): #: The ID of the application that created this guild, if it was created by #: a bot. If not, this is always ``None``. #: - #: :type: :obj:`snowflakes.Snowflake`, optional + #: :type: :obj:`hikari.core.snowflakes.Snowflake`, optional application_id: typing.Optional[snowflakes.Snowflake] = marshaller.attrib( deserializer=snowflakes.Snowflake.deserialize, if_none=None ) @@ -959,7 +957,7 @@ class Guild(PartialGuild): #: The channel ID that the widget's generated invite will send the user to, #: if enabled. If this information is unavailable, this will be ``None``. #: - #: :type: :obj:`snowflakes.Snowflake`, optional + #: :type: :obj:`hikari.core.snowflakes.Snowflake`, optional widget_channel_id: typing.Optional[snowflakes.Snowflake] = marshaller.attrib( if_undefined=None, if_none=None, deserializer=snowflakes.Snowflake.deserialize ) @@ -967,7 +965,7 @@ class Guild(PartialGuild): #: The ID of the system channel (where welcome messages and Nitro boost #: messages are sent), or ``None`` if it is not enabled. #: - #: :type: :obj:`snowflakes.Snowflake`, optional + #: :type: :obj:`hikari.core.snowflakes.Snowflake`, optional system_channel_id: typing.Optional[snowflakes.Snowflake] = marshaller.attrib( if_none=None, deserializer=snowflakes.Snowflake.deserialize ) @@ -982,7 +980,7 @@ class Guild(PartialGuild): #: :attr:`features` display rules and guidelines. If the #: :obj:`GuildFeature.PUBLIC` feature is not defined, then this is ``None``. #: - #: :type: :obj:`snowflakes.Snowflake`, optional + #: :type: :obj:`hikari.core.snowflakes.Snowflake`, optional rules_channel_id: typing.Optional[snowflakes.Snowflake] = marshaller.attrib( if_none=None, deserializer=snowflakes.Snowflake.deserialize ) @@ -993,7 +991,7 @@ class Guild(PartialGuild): #: ``GUILD_CREATE`` event. If the guild is received from any other place, #: this will always be ``None``. #: - #: :type: :obj`datetime.datetime`, optional + #: :type: :obj:`datetime.datetime`, optional joined_at: typing.Optional[datetime.datetime] = marshaller.attrib( deserializer=conversions.parse_iso_8601_ts, if_undefined=None ) @@ -1036,7 +1034,7 @@ class Guild(PartialGuild): #: representation. If you need complete accurate information, you should #: query the members using the appropriate API call instead. #: - #: :type: :obj:`typing.Mapping` [ :obj:`snowflakes.Snowflake`, :obj:`GuildMember` ], optional + #: :type: :obj:`typing.Mapping` [ :obj:`hikari.core.snowflakes.Snowflake`, :obj:`GuildMember` ], optional members: typing.Optional[typing.Mapping[snowflakes.Snowflake, GuildMember]] = marshaller.attrib( deserializer=lambda members: {m.user.id: m for m in map(GuildMember.deserialize, members)}, if_undefined=None, ) @@ -1056,7 +1054,7 @@ class Guild(PartialGuild): #: To retrieve a list of channels in any other case, you should make an #: appropriate API call to retrieve this information. #: - #: :type: :obj:`typing.Mapping` [ :obj:`snowflakes.Snowflake`, :obj:`_channels.GuildChannel` ], optional + #: :type: :obj:`typing.Mapping` [ :obj:`hikari.core.snowflakes.Snowflake`, :obj:`hikari.core.channels.GuildChannel` ], optional channels: typing.Optional[typing.Mapping[snowflakes.Snowflake, _channels.GuildChannel]] = marshaller.attrib( deserializer=lambda guild_channels: {c.id: c for c in map(_channels.deserialize_channel, guild_channels)}, if_undefined=None, @@ -1078,7 +1076,7 @@ class Guild(PartialGuild): #: To retrieve a list of presences in any other case, you should make an #: appropriate API call to retrieve this information. #: - #: :type: :obj:`typing.Mapping` [ :obj:`snowflakes.Snowflake`, :obj:`GuildMemberPresence` ], optional + #: :type: :obj:`typing.Mapping` [ :obj:`hikari.core.snowflakes.Snowflake`, :obj:`GuildMemberPresence` ], optional presences: typing.Optional[typing.Mapping[snowflakes.Snowflake, GuildMemberPresence]] = marshaller.attrib( deserializer=lambda presences: {p.user.id: p for p in map(GuildMemberPresence.deserialize, presences)}, if_undefined=None, @@ -1098,7 +1096,7 @@ class Guild(PartialGuild): max_members: typing.Optional[int] = marshaller.attrib(if_undefined=None, deserializer=int) #: The vanity URL code for the guild's vanity URL. - #: This is only present if :obj:`GuildFeatures.VANITY_URL` is in the + #: This is only present if :obj:`GuildFeature.VANITY_URL` is in the #: :attr:`features` for this guild. If not, this will always be ``None``. #: #: :type: :obj:`str`, optional @@ -1114,7 +1112,7 @@ class Guild(PartialGuild): description: typing.Optional[str] = marshaller.attrib(if_none=None, deserializer=str) #: The hash for the guild's banner. - #: This is only present if the guild has :obj:`GuildFeatures.BANNER` in the + #: This is only present if the guild has :obj:`GuildFeature.BANNER` in the #: :attr:`features` for this guild. For all other purposes, it is ``None``. #: #: :type: :obj:`str`, optional @@ -1146,27 +1144,27 @@ class Guild(PartialGuild): #: :attr:`features` for this guild. For all other purposes, it should be #: considered to be ``None``. #: - #: :type: :obj:`snowflakes.Snowflake`, optional + #: :type: :obj:`hikari.core.snowflakes.Snowflake`, optional public_updates_channel_id: typing.Optional[snowflakes.Snowflake] = marshaller.attrib( if_none=None, deserializer=snowflakes.Snowflake.deserialize ) def format_splash_url(self, fmt: str = "png", size: int = 2048) -> typing.Optional[str]: - """Generate the url for this guild's splash image, if set. + """Generate the URL for this guild's splash image, if set. Parameters ---------- fmt : :obj:`str` - The format to use for this url, defaults to ``png``. + The format to use for this URL, defaults to ``png``. Supports ``png``, ``jpeg``, ``jpg`` and ``webp``. size : :obj:`int` - The size to set for the url, defaults to ``2048``. + The size to set for the URL, defaults to ``2048``. Can be any power of two between 16 and 2048. Returns ------- :obj:`str`, optional - The string url. + The string URL. """ if self.splash_hash: return cdn.generate_cdn_url("splashes", str(self.id), self.splash_hash, fmt=fmt, size=size) @@ -1174,25 +1172,25 @@ def format_splash_url(self, fmt: str = "png", size: int = 2048) -> typing.Option @property def splash_url(self) -> typing.Optional[str]: - """The url for this guild's splash, if set.""" + """URL for this guild's splash, if set.""" return self.format_splash_url() def format_discovery_splash_url(self, fmt: str = "png", size: int = 2048) -> typing.Optional[str]: - """Generate the url for this guild's discovery splash image, if set. + """Generate the URL for this guild's discovery splash image, if set. Parameters ---------- fmt : :obj:`str` - The format to use for this url, defaults to ``png``. + The format to use for this URL, defaults to ``png``. Supports ``png``, ``jpeg``, ``jpg`` and ``webp``. size : :obj:`int` - The size to set for the url, defaults to ``2048``. + The size to set for the URL, defaults to ``2048``. Can be any power of two between 16 and 2048. Returns ------- :obj:`str`, optional - The string url. + The string URL. """ if self.discovery_splash_hash: return cdn.generate_cdn_url( @@ -1202,25 +1200,25 @@ def format_discovery_splash_url(self, fmt: str = "png", size: int = 2048) -> typ @property def discovery_splash_url(self) -> typing.Optional[str]: - """The url for this guild's discovery splash, if set.""" + """URL for this guild's discovery splash, if set.""" return self.format_discovery_splash_url() def format_banner_url(self, fmt: str = "png", size: int = 2048) -> typing.Optional[str]: - """Generate the url for this guild's banner image, if set. + """Generate the URL for this guild's banner image, if set. Parameters ---------- fmt : :obj:`str` - The format to use for this url, defaults to ``png``. + The format to use for this URL, defaults to ``png``. Supports ``png``, ``jpeg``, ``jpg`` and ``webp``. size : :obj:`int` - The size to set for the url, defaults to ``2048``. + The size to set for the URL, defaults to ``2048``. Can be any power of two between 16 and 2048. Returns ------- :obj:`str`, optional - The string url. + The string URL. """ if self.banner_hash: return cdn.generate_cdn_url("banners", str(self.id), self.banner_hash, fmt=fmt, size=size) @@ -1228,5 +1226,5 @@ def format_banner_url(self, fmt: str = "png", size: int = 2048) -> typing.Option @property def banner_url(self) -> typing.Optional[str]: - """The url for this guild's banner, if set.""" + """URL for this guild's banner, if set.""" return self.format_banner_url() diff --git a/hikari/core/invites.py b/hikari/core/invites.py index ace8e1d011..a571c39d68 100644 --- a/hikari/core/invites.py +++ b/hikari/core/invites.py @@ -16,8 +16,7 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . -"""Components and entities that are used to describe invites on Discord. -""" +"""Components and entities that are used to describe invites on Discord.""" __all__ = ["TargetUserType", "VanityUrl", "InviteGuild", "Invite", "InviteWithMetadata"] import datetime @@ -66,15 +65,16 @@ class InviteGuild(guilds.PartialGuild): splash_hash: typing.Optional[str] = marshaller.attrib(raw_name="splash", deserializer=str, if_none=None) #: The hash for the guild's banner. - #: This is only present if the guild has :obj:`GuildFeatures.BANNER` in the - #: :attr:`features` for this guild. For all other purposes, it is ``None``. + #: + #: This is only present if :obj:`hikari.core.guild.GuildFeature.BANNER` + #: is in the ``features`` for this guild. For all other purposes, it is ``None``. #: #: :type: :obj:`str`, optional banner_hash: typing.Optional[str] = marshaller.attrib(raw_name="banner", if_none=None, deserializer=str) #: The guild's description. #: - #: This is only present if certain :attr:`features` are set in this guild. + #: This is only present if certain ``features`` are set in this guild. #: Otherwise, this will always be ``None``. For all other purposes, it is #: ``None``. #: @@ -83,32 +83,33 @@ class InviteGuild(guilds.PartialGuild): #: The verification level required for a user to participate in this guild. #: - #: :type: :obj:`GuildVerificationLevel` + #: :type: :obj:`hikari.core.guilds.GuildVerificationLevel` verification_level: guilds.GuildVerificationLevel = marshaller.attrib(deserializer=guilds.GuildVerificationLevel) #: The vanity URL code for the guild's vanity URL. - #: This is only present if :obj:`GuildFeatures.VANITY_URL` is in the - #: :attr:`features` for this guild. If not, this will always be ``None``. + #: + #: This is only present if :obj:`hikari.core.guilds.GuildFeature.VANITY_URL` + #: is in the ``features`` for this guild. If not, this will always be ``None``. #: #: :type: :obj:`str`, optional vanity_url_code: typing.Optional[str] = marshaller.attrib(if_none=None, deserializer=str) def format_splash_url(self, fmt: str = "png", size: int = 2048) -> typing.Optional[str]: - """Generate the url for this guild's splash, if set. + """Generate the URL for this guild's splash, if set. Parameters ---------- fmt : :obj:`str` - The format to use for this url, defaults to ``png``. + The format to use for this URL, defaults to ``png``. Supports ``png``, ``jpeg``, ``jpg` and ``webp``. size : :obj:`int` - The size to set for the url, defaults to ``2048``. + The size to set for the URL, defaults to ``2048``. Can be any power of two between 16 and 2048. Returns ------- :obj:`str`, optional - The string url. + The string URL. """ if self.splash_hash: return cdn.generate_cdn_url("splashes", str(self.id), self.splash_hash, fmt=fmt, size=size) @@ -116,25 +117,25 @@ def format_splash_url(self, fmt: str = "png", size: int = 2048) -> typing.Option @property def splash_url(self) -> typing.Optional[str]: - """The url for this guild's splash, if set.""" + """URL for this guild's splash, if set.""" return self.format_splash_url() def format_banner_url(self, fmt: str = "png", size: int = 2048) -> typing.Optional[str]: - """Generate the url for this guild's banner, if set. + """Generate the URL for this guild's banner, if set. Parameters ---------- fmt : :obj:`str` - The format to use for this url, defaults to ``png``. + The format to use for this URL, defaults to ``png``. Supports ``png``, ``jpeg``, ``jpg`` and ``webp``. size : :obj:`int` - The size to set for the url, defaults to ``2048``. + The size to set for the URL, defaults to ``2048``. Can be any power of two between 16 and 2048. Returns ------- :obj:`str`, optional - The string url. + The string URL. """ if self.banner_hash: return cdn.generate_cdn_url("banners", str(self.id), self.banner_hash, fmt=fmt, size=size) @@ -142,7 +143,7 @@ def format_banner_url(self, fmt: str = "png", size: int = 2048) -> typing.Option @property def banner_url(self) -> typing.Optional[str]: - """The url for this guild's banner, if set.""" + """URL for this guild's banner, if set.""" return self.format_banner_url() @@ -162,17 +163,17 @@ class Invite(entities.HikariEntity, entities.Deserializable): guild: typing.Optional[InviteGuild] = marshaller.attrib(deserializer=InviteGuild.deserialize, if_undefined=None) #: The partial object of the channel this invite targets. #: - #: :type: :obj:`channels.PartialChannel` + #: :type: :obj:`hikari.core.channels.PartialChannel` channel: channels.PartialChannel = marshaller.attrib(deserializer=channels.PartialChannel.deserialize) #: The object of the user who created this invite. #: - #: :type: :obj:`users.User`, optional + #: :type: :obj:`hikari.core.users.User`, optional inviter: typing.Optional[users.User] = marshaller.attrib(deserializer=users.User.deserialize, if_undefined=None) #: The object of the user who this invite targets, if set. #: - #: :type: :obj:`users.User`, optional + #: :type: :obj:`hikari.core.users.User`, optional target_user: typing.Optional[users.User] = marshaller.attrib(deserializer=users.User.deserialize, if_undefined=None) #: The type of user target this invite is, if applicable. @@ -183,13 +184,13 @@ class Invite(entities.HikariEntity, entities.Deserializable): ) #: The approximate amount of presences in this invite's guild, only present - #: when ``with_counts`` is passed as ``True`` to the GET invites endpoint. + #: when ``with_counts`` is passed as ``True`` to the GET Invites endpoint. #: #: :type: :obj:`int`, optional approximate_presence_count: typing.Optional[int] = marshaller.attrib(deserializer=int, if_undefined=None) #: The approximate amount of members in this invite's guild, only present - #: when ``with_counts`` is passed as ``True`` to the GET invites endpoint. + #: when ``with_counts`` is passed as ``True`` to the GET Invites endpoint. #: #: :type: :obj:`int`, optional approximate_member_count: typing.Optional[int] = marshaller.attrib(deserializer=int, if_undefined=None) @@ -197,8 +198,10 @@ class Invite(entities.HikariEntity, entities.Deserializable): @marshaller.attrs(slots=True) class InviteWithMetadata(Invite): - """Extends the base :obj:`Invite` object with metadata that's only returned - when getting an invite with guild permissions, rather than it's code. + """Extends the base :obj:`Invite` object with metadata. + + The metadata is only returned when getting an invite with + guild permissions, rather than it's code. """ #: The amount of times this invite has been used. @@ -213,7 +216,7 @@ class InviteWithMetadata(Invite): max_uses: int = marshaller.attrib(deserializer=int) #: The timedelta of how long this invite will be valid for. - #: If set to :obj:`None` then this is unlimited. + #: If set to ``None`` then this is unlimited. #: #: :type: :obj:`datetime.timedelta`, optional max_age: typing.Optional[datetime.timedelta] = marshaller.attrib( @@ -232,7 +235,7 @@ class InviteWithMetadata(Invite): @property def expires_at(self) -> typing.Optional[datetime.datetime]: - """The :obj:`datetime` of when this invite should expire, if ``max_age`` is set.""" + """When this invite should expire, if ``max_age`` is set. Else ``None``.""" if self.max_age: return self.created_at + self.max_age return None diff --git a/hikari/core/messages.py b/hikari/core/messages.py index 744655a05c..204dded919 100644 --- a/hikari/core/messages.py +++ b/hikari/core/messages.py @@ -16,14 +16,16 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . -""" -Components and entities that are used to describe messages on Discord. -""" +"""Components and entities that are used to describe messages on Discord.""" __all__ = [ "MessageType", - "Message", + "MessageFlag", + "MessageActivityType", "Attachment", "Reaction", + "MessageActivity", + "MessageCrosspost", + "Message", ] import datetime @@ -106,7 +108,7 @@ class MessageActivityType(enum.IntEnum): @marshaller.attrs(slots=True) class Attachment(snowflakes.UniqueEntity, entities.Deserializable): - """Represents a file attached to a message""" + """Represents a file attached to a message.""" #: The name of the file. #: @@ -118,12 +120,12 @@ class Attachment(snowflakes.UniqueEntity, entities.Deserializable): #: :type: :obj:`int` size: int = marshaller.attrib(deserializer=int) - #: The source url of file. + #: The source URL of file. #: #: :type: :obj:`str` url: str = marshaller.attrib(deserializer=str) - #: The proxied url of file. + #: The proxied URL of file. #: #: :type: :obj:`str` proxy_url: str = marshaller.attrib(deserializer=str) @@ -150,7 +152,7 @@ class Reaction(entities.HikariEntity, entities.Deserializable): #: The emoji used to react. #: - #: :type: :obj:`typing.Union` [ :obj:`_emojis.UnicodeEmoji`, :obj:`_emojis.UnknownEmoji`] + #: :type: :obj:`typing.Union` [ :obj:`hikari.core.emojis.UnicodeEmoji`, :obj:`hikari.core.emojis.UnknownEmoji`] emoji: typing.Union[_emojis.UnicodeEmoji, _emojis.UnknownEmoji] = marshaller.attrib( deserializer=_emojis.deserialize_reaction_emoji ) @@ -187,14 +189,15 @@ class MessageCrosspost(entities.HikariEntity, entities.Deserializable): #: This may be ``None`` in some cases according to the Discord API #: documentation, but the situations that cause this to occur are not currently documented. #: - #: :type: :obj:`snowflakes.Snowflake`, optional + #: + #: :type: :obj:`hikari.core.snowflakes.Snowflake`, optional message_id: typing.Optional[snowflakes.Snowflake] = marshaller.attrib( deserializer=snowflakes.Snowflake.deserialize, if_undefined=None ) #: The ID of the channel that the message originated from. #: - #: :type: :obj:`snowflakes.Snowflake` + #: :type: :obj:`hikari.core.snowflakes.Snowflake` channel_id: snowflakes.Snowflake = marshaller.attrib(deserializer=snowflakes.Snowflake.deserialize) #: The ID of the guild that the message originated from. @@ -204,7 +207,7 @@ class MessageCrosspost(entities.HikariEntity, entities.Deserializable): #: This may be ``None`` in some cases according to the Discord API #: documentation, but the situations that cause this to occur are not currently documented. #: - #: :type: :obj:`snowflakes.Snowflake`, optional + #: :type: :obj:`hikari.core.snowflakes.Snowflake`, optional guild_id: typing.Optional[snowflakes.Snowflake] = marshaller.attrib( deserializer=snowflakes.Snowflake.deserialize, if_undefined=None ) @@ -216,24 +219,24 @@ class Message(snowflakes.UniqueEntity, entities.Deserializable): #: The ID of the channel that the message was sent in. #: - #: :type: :obj:`snowflakes.Snowflake` + #: :type: :obj:`hikari.core.snowflakes.Snowflake` channel_id: snowflakes.Snowflake = marshaller.attrib(deserializer=snowflakes.Snowflake.deserialize) #: The ID of the guild that the message was sent in. #: - #: :type: :obj:`snowflakes.Snowflake`, optional + #: :type: :obj:`hikari.core.snowflakes.Snowflake`, optional guild_id: typing.Optional[snowflakes.Snowflake] = marshaller.attrib( deserializer=snowflakes.Snowflake.deserialize, if_undefined=None ) #: The author of this message. #: - #: :type: :obj:`users.User` + #: :type: :obj:`hikari.core.users.User` author: users.User = marshaller.attrib(deserializer=users.User.deserialize) #: The member properties for the message's author. #: - #: :type: :obj:`guilds.GuildMember`, optional + #: :type: :obj:`hikari.core.guilds.GuildMember`, optional member: typing.Optional[guilds.GuildMember] = marshaller.attrib( deserializer=guilds.GuildMember.deserialize, if_undefined=None ) @@ -267,7 +270,7 @@ class Message(snowflakes.UniqueEntity, entities.Deserializable): #: The users the message mentions. #: - #: :type: :obj:`typing.Set` [ :obj:`snowflakes.Snowflake` ] + #: :type: :obj:`typing.Set` [ :obj:`hikari.core.snowflakes.Snowflake` ] user_mentions: typing.Set[snowflakes.Snowflake] = marshaller.attrib( raw_name="mentions", deserializer=lambda user_mentions: {snowflakes.Snowflake.deserialize(u["id"]) for u in user_mentions}, @@ -275,7 +278,7 @@ class Message(snowflakes.UniqueEntity, entities.Deserializable): #: The roles the message mentions. #: - #: :type: :obj:`typing.Set` [ :obj:`snowflakes.Snowflake` ] + #: :type: :obj:`typing.Set` [ :obj:`hikari.core.snowflakes.Snowflake` ] role_mentions: typing.Set[snowflakes.Snowflake] = marshaller.attrib( raw_name="mention_roles", deserializer=lambda role_mentions: {snowflakes.Snowflake.deserialize(mention) for mention in role_mentions}, @@ -283,7 +286,7 @@ class Message(snowflakes.UniqueEntity, entities.Deserializable): #: The channels the message mentions. #: - #: :type: :obj:`typing.Set` [ :obj:`snowflakes.Snowflake` ] + #: :type: :obj:`typing.Set` [ :obj:`hikari.core.snowflakes.Snowflake` ] channel_mentions: typing.Set[snowflakes.Snowflake] = marshaller.attrib( raw_name="mention_channels", deserializer=lambda channel_mentions: {snowflakes.Snowflake.deserialize(c["id"]) for c in channel_mentions}, @@ -299,7 +302,7 @@ class Message(snowflakes.UniqueEntity, entities.Deserializable): #: The message embeds. #: - #: :type: :obj:`typing.Sequence` [ :obj:`_embeds.Embed` ] + #: :type: :obj:`typing.Sequence` [ :obj:`hikari.core.embeds.Embed` ] embeds: typing.Sequence[_embeds.Embed] = marshaller.attrib( deserializer=lambda embeds: [_embeds.Embed.deserialize(e) for e in embeds] ) @@ -318,7 +321,7 @@ class Message(snowflakes.UniqueEntity, entities.Deserializable): #: If the message was generated by a webhook, the webhook's id. #: - #: :type: :obj:`snowflakes.Snowflake`, optional + #: :type: :obj:`hikari.core.snowflakes.Snowflake`, optional webhook_id: typing.Optional[snowflakes.Snowflake] = marshaller.attrib( deserializer=snowflakes.Snowflake.deserialize, if_undefined=None ) @@ -337,7 +340,7 @@ class Message(snowflakes.UniqueEntity, entities.Deserializable): #: The message application. #: - #: :type: :obj:`oauth2.Application`, optional + #: :type: :obj:`hikari.core.oauth2.Application`, optional application: typing.Optional[oauth2.Application] = marshaller.attrib( deserializer=oauth2.Application.deserialize, if_undefined=None ) diff --git a/hikari/core/oauth2.py b/hikari/core/oauth2.py index 99ea358b74..aac4d30c46 100644 --- a/hikari/core/oauth2.py +++ b/hikari/core/oauth2.py @@ -17,16 +17,7 @@ # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . """Components and entities related to discord's Oauth2 flow.""" -__all__ = [ - "Application", - "ConnectionVisibility", - "Owner", - "OwnConnection", - "OwnGuild", - "Team", - "TeamMember", - "TeamMembershipState", -] +__all__ = ["Application", "ApplicationOwner", "OwnGuild", "Team", "TeamMember", "TeamMembershipState"] import enum import typing @@ -112,10 +103,7 @@ class OwnConnection(entities.HikariEntity, entities.Deserializable): @marshaller.attrs(slots=True) class OwnGuild(guilds.PartialGuild): - """Represents a user bound partial guild object. - - Returned by the ``GET Current User Guilds`` endpoint. - """ + """Represents a user bound partial guild object.""" #: Whether the current user owns this guild. #: @@ -124,7 +112,7 @@ class OwnGuild(guilds.PartialGuild): #: The guild level permissions that apply to the current user or bot. #: - #: :type: :obj:`permissions.Permission` + #: :type: :obj:`hikari.core.permissions.Permission` my_permissions: permissions.Permission = marshaller.attrib( raw_name="permissions", deserializer=permissions.Permission ) @@ -158,7 +146,7 @@ class TeamMember(entities.HikariEntity, entities.Deserializable): #: The ID of the team this member belongs to. #: - #: :type: :obj:`snowflakes.Snowflake` + #: :type: :obj:`hikari.core.snowflakes.Snowflake` team_id: snowflakes.Snowflake = marshaller.attrib(deserializer=snowflakes.Snowflake.deserialize) #: The user object of this team member. @@ -169,7 +157,7 @@ class TeamMember(entities.HikariEntity, entities.Deserializable): @marshaller.attrs(slots=True) class Team(snowflakes.UniqueEntity, entities.Deserializable): - """This represents a Team and it's members.""" + """Represents a development team, along with all its members.""" #: The hash of this team's icon, if set. #: @@ -178,37 +166,37 @@ class Team(snowflakes.UniqueEntity, entities.Deserializable): #: The member's that belong to this team. #: - #: :type: :obj:`typing.Mapping` [ :obj:`snowflakes.Snowflake`, :obj:`TeamMember` ] + #: :type: :obj:`typing.Mapping` [ :obj:`hikari.core.snowflakes.Snowflake`, :obj:`TeamMember` ] members: typing.Mapping[snowflakes.Snowflake, TeamMember] = marshaller.attrib( deserializer=lambda members: {m.user.id: m for m in map(TeamMember.deserialize, members)} ) - #: The snowflake ID of this team's owner. + #: The ID of this team's owner. #: - #: :type: :obj:`snowflakes.Snowflake` + #: :type: :obj:`hikari.core.snowflakes.Snowflake` owner_user_id: snowflakes.Snowflake = marshaller.attrib(deserializer=snowflakes.Snowflake.deserialize) @property def icon_url(self) -> typing.Optional[str]: - """The url of this team's icon, if set.""" + """URL of this team's icon, if set.""" return self.format_icon_url() def format_icon_url(self, fmt: str = "png", size: int = 2048) -> typing.Optional[str]: - """Generate the icon url for this team if set. + """Generate the icon URL for this team if set. Parameters ---------- fmt : :obj:`str` - The format to use for this url, defaults to ``png``. + The format to use for this URL, defaults to ``png``. Supports ``png``, ``jpeg``, ``jpg`` and ``webp``. size : :obj:`int` - The size to set for the url, defaults to ``2048``. Can be any power + The size to set for the URL, defaults to ``2048``. Can be any power of two between 16 and 2048 inclusive. Returns ------- :obj:`str`, optional - The string url, will be :obj:`None` if not set. + The string URL. """ if self.icon_hash: return cdn.generate_cdn_url("team-icons", str(self.id), self.icon_hash, fmt=fmt, size=size) @@ -216,7 +204,7 @@ def format_icon_url(self, fmt: str = "png", size: int = 2048) -> typing.Optional @marshaller.attrs(slots=True) -class Owner(users.User): +class ApplicationOwner(users.User): """Represents the user who owns an application, may be a team user.""" #: This user's flags. @@ -226,9 +214,7 @@ class Owner(users.User): @property def is_team_user(self) -> bool: - """If this user is a Team user (the owner of an application that's - owned by a team). - """ + """If this user is a Team user (the owner of an application that's owned by a team).""" return bool((self.flags >> 10) & 1) @@ -266,10 +252,12 @@ class Application(snowflakes.UniqueEntity, entities.Deserializable): #: This should always be ``None`` in application objects retrieved outside #: Discord's oauth2 flow. #: - #: :type: :obj:`Owner`, optional - owner: typing.Optional[Owner] = marshaller.attrib(deserializer=Owner.deserialize, if_undefined=None) + #: :type: :obj:`ApplicationOwner`, optional + owner: typing.Optional[ApplicationOwner] = marshaller.attrib( + deserializer=ApplicationOwner.deserialize, if_undefined=None + ) - #: A collection of this application's rpc origin urls, if rpc is enabled. + #: A collection of this application's rpc origin URLs, if rpc is enabled. #: #: :type: :obj:`typing.Set` [ :obj:`str` ], optional rpc_origins: typing.Optional[typing.Set[str]] = marshaller.attrib(deserializer=set, if_undefined=None) @@ -300,19 +288,19 @@ class Application(snowflakes.UniqueEntity, entities.Deserializable): #: The ID of the guild this application is linked to #: if it's sold on Discord. #: - #: :type: :obj:`snowflakes.Snowflake`, optional + #: :type: :obj:`hikari.core.snowflakes.Snowflake`, optional guild_id: typing.Optional[snowflakes.Snowflake] = marshaller.attrib( deserializer=snowflakes.Snowflake.deserialize, if_undefined=None ) #: The ID of the primary "Game SKU" of a game that's sold on Discord. #: - #: :type: :obj:`snowflakes.Snowflake`, optional + #: :type: :obj:`hikari.core.snowflakes.Snowflake`, optional primary_sku_id: typing.Optional[snowflakes.Snowflake] = marshaller.attrib( deserializer=snowflakes.Snowflake.deserialize, if_undefined=None ) - #: The url slug that links to this application's store page + #: The URL slug that links to this application's store page #: if it's sold on Discord. #: #: :type: :obj:`str`, optional @@ -327,25 +315,25 @@ class Application(snowflakes.UniqueEntity, entities.Deserializable): @property def icon_url(self) -> typing.Optional[str]: - """The url for this team's icon, if set.""" + """URL for this team's icon, if set.""" return self.format_icon_url() def format_icon_url(self, fmt: str = "png", size: int = 2048) -> typing.Optional[str]: - """Generate the icon url for this application if set. + """Generate the icon URL for this application if set. Parameters ---------- fmt : :obj:`str` - The format to use for this url, defaults to ``png``. + The format to use for this URL, defaults to ``png``. Supports ``png``, ``jpeg``, ``jpg`` and ```webp``. size : :obj:`int` - The size to set for the url, defaults to ``2048``. + The size to set for the URL, defaults to ``2048``. Can be any power of two between 16 and 2048. Returns ------- :obj:`str`, optional - The string url. + The string URL. """ if self.icon_hash: return cdn.generate_cdn_url("app-icons", str(self.id), self.icon_hash, fmt=fmt, size=size) @@ -353,26 +341,25 @@ def format_icon_url(self, fmt: str = "png", size: int = 2048) -> typing.Optional @property def cover_image_url(self) -> typing.Optional[str]: - """The url for this icon's store cover image, if set.""" + """URL for this icon's store cover image, if set.""" return self.format_cover_image_url() def format_cover_image_url(self, fmt: str = "png", size: int = 2048) -> typing.Optional[str]: - """Generate the url for this application's store page's cover image is - set and applicable. + """Generate the URL for this application's store page's cover image is set and applicable. Parameters ---------- fmt : :obj:`str` - The format to use for this url, defaults to ``png``. + The format to use for this URL, defaults to ``png``. Supports ``png``, ``jpeg``, ``jpg`` and ``webp``. size : :obj:`int` - The size to set for the url, defaults to ``2048``. + The size to set for the URL, defaults to ``2048``. Can be any power of two between 16 and 2048. Returns ------- :obj:`str`, optional - The string url. + The string URL. """ if self.cover_image_hash: return cdn.generate_cdn_url("app-assets", str(self.id), self.cover_image_hash, fmt=fmt, size=size) diff --git a/hikari/core/permissions.py b/hikari/core/permissions.py index 9f0f965784..2a0fc63140 100644 --- a/hikari/core/permissions.py +++ b/hikari/core/permissions.py @@ -16,8 +16,7 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . -"""Bitfield of permissions. -""" +"""Bitfield of permissions.""" __all__ = ["Permission"] import enum diff --git a/hikari/core/snowflakes.py b/hikari/core/snowflakes.py index 158457da8a..471bea33da 100644 --- a/hikari/core/snowflakes.py +++ b/hikari/core/snowflakes.py @@ -16,8 +16,10 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . -"""A representation of the Discord Snowflake datatype, which is a 64-bit integer -used to uniquely identify entities on the server. +"""A representation of the Discord Snowflake datatype. + +Each Snowflake integer used to uniquely identify entities +on the server. """ __all__ = ["Snowflake", "UniqueEntity"] @@ -33,8 +35,7 @@ @functools.total_ordering class Snowflake(entities.HikariEntity, typing.SupportsInt): - """A concrete representation of a unique identifier for an object on - Discord. + """A concrete representation of a unique identifier for an object on Discord. This object can be treated as a regular :obj:`int` for most purposes. """ @@ -58,17 +59,17 @@ def created_at(self) -> datetime.datetime: @property def internal_worker_id(self) -> int: - """The internal worker ID that created this object on Discord.""" + """ID of the worker that created this snowflake on Discord's systems.""" return (self._value & 0x3E0_000) >> 17 @property def internal_process_id(self) -> int: - """The internal process ID that created this object on Discord.""" + """ID of the process that created this snowflake on Discord's systems.""" return (self._value & 0x1F_000) >> 12 @property def increment(self) -> int: - """The increment of Discord's system when this object was made.""" + """Increment of Discord's system when this object was made.""" return self._value & 0xFFF def __hash__(self): @@ -95,9 +96,7 @@ def serialize(self) -> str: @classmethod def deserialize(cls, value: str) -> "Snowflake": - """Take a serialized string ID and convert it into a Snowflake - object. - """ + """Take a :obj:`str` ID and convert it into a Snowflake object.""" return cls(value) diff --git a/hikari/core/state/default_state.py b/hikari/core/state/default_state.py index 0dd79fe70c..dba0d1712f 100644 --- a/hikari/core/state/default_state.py +++ b/hikari/core/state/default_state.py @@ -25,7 +25,7 @@ from hikari.core.state import base_state -class DefaultState(base_state.BaseState): +class DefaultState(base_state.BaseState): # noqa: D101 def __init__(self, event_dispatcher: dispatcher.EventDispatcher): super().__init__() self.event_dispatcher: dispatcher.EventDispatcher = event_dispatcher diff --git a/hikari/core/state/dispatcher.py b/hikari/core/state/dispatcher.py index 9150a0c2b4..8b077833c9 100644 --- a/hikari/core/state/dispatcher.py +++ b/hikari/core/state/dispatcher.py @@ -55,7 +55,7 @@ def add_listener(self, event_type: typing.Type[EventT], callback: EventCallbackT Parameters ---------- - event_type : :obj:`typing.Type` [ :obj:`events.HikariEvent` ] + event_type : :obj:`typing.Type` [ :obj:`hikari.core.events.HikariEvent` ] The event to register to. callback : ``async def callback(event: HikariEvent) -> ...`` The event callback to invoke when this event is fired. @@ -74,7 +74,7 @@ def remove_listener(self, event_type: typing.Type[EventT], callback: EventCallba Parameters ---------- - event_type : :obj:`typing.Type` [ :obj:`events.HikariEvent` ] + event_type : :obj:`typing.Type` [ :obj:`hikari.core.events.HikariEvent` ] The type of event to remove the callback from. callback : ``async def callback(event: HikariEvent) -> ...`` The event callback to invoke when this event is fired. @@ -88,12 +88,12 @@ def wait_for( Parameters ---------- - event_type : :obj:`typing.Type` [ :obj:`events.HikariEvent` ] + event_type : :obj:`typing.Type` [ :obj:`hikari.core.events.HikariEvent` ] The name of the event to wait for. timeout : :obj:`float`, optional The timeout to wait for before cancelling and raising an - :obj:`asyncio.TimeoutError` instead. If this is `None`, this will - wait forever. Care must be taken if you use `None` as this may + :obj:`asyncio.TimeoutError` instead. If this is ``None``, this will + wait forever. Care must be taken if you use ``None`` as this may leak memory if you do this from an event listener that gets repeatedly called. If you want to do this, you should consider using an event listener instead of this function. @@ -118,12 +118,13 @@ def wait_for( lookup, but can be implemented this way optionally if documented. """ - def on(self, event_type: typing.Type[EventT]) -> typing.Callable[[EventCallbackT], EventCallbackT]: - """A decorator that is equivalent to invoking :meth:`add_listener`. + # Ignore docstring not starting in an imperative mood + def on(self, event_type: typing.Type[EventT]) -> typing.Callable[[EventCallbackT], EventCallbackT]: # noqa: D401 + """Returns a decorator that is equivalent to invoking :meth:`add_listener`. Parameters ---------- - event_type : :obj:`typing.Type` [ :obj:`events.HikariEvent` ] + event_type : :obj:`typing.Type` [ :obj:`hikari.core.events.HikariEvent` ] The event type to register the produced decorator to. Returns @@ -145,7 +146,7 @@ def dispatch_event(self, event: events.HikariEvent) -> ...: Parameters ---------- - event : :obj:`events.HikariEvent` + event : :obj:`hikari.core.events.HikariEvent` The event to dispatch. Returns @@ -158,8 +159,7 @@ def dispatch_event(self, event: events.HikariEvent) -> ...: class EventDispatcherImpl(EventDispatcher): - """Handles storing and dispatching to event listeners and one-time event - waiters. + """Handles storing and dispatching to event listeners and one-time event waiters. Event listeners once registered will be stored until they are manually removed. Each time an event is dispatched with a matching name, they will @@ -202,7 +202,7 @@ def add_listener(self, event_type: typing.Type[events.HikariEvent], callback: Ev Parameters ---------- - event_type : :obj:`typing.Type` [ :obj:`events.HikariEvent` ] + event_type : :obj:`typing.Type` [ :obj:`hikari.core.events.HikariEvent` ] The event to register to. callback : ``async def callback(event: HikariEvent) -> ...`` The event callback to invoke when this event is fired. @@ -226,7 +226,7 @@ def remove_listener(self, event_type: typing.Type[EventT], callback: EventCallba Parameters ---------- - event_type : :obj:`typing.Type` [ :obj:`events.HikariEvent` ] + event_type : :obj:`typing.Type` [ :obj:`hikari.core.events.HikariEvent` ] The type of event to remove the callback from. callback : ``async def callback(event: HikariEvent) -> ...`` The event callback to remove. @@ -240,12 +240,11 @@ def remove_listener(self, event_type: typing.Type[EventT], callback: EventCallba # Do not add an annotation here, it will mess with type hints in PyCharm which can lead to # confusing telepathy comments to the user. def dispatch_event(self, event: events.HikariEvent): - """Dispatch a given event to all listeners and waiters that are - applicable. + """Dispatch a given event to all listeners and waiters that are applicable. Parameters ---------- - event : :obj:`events.HikariEvent` + event : :obj:`hikari.core.events.HikariEvent` The event to dispatch. Returns @@ -316,21 +315,22 @@ async def _catch(self, callback, event): def handle_exception( self, exception: Exception, event: events.HikariEvent, callback: typing.Callable[..., typing.Awaitable[None]] ) -> None: - """Function that is passed any exception. This allows users to override - this with a custom implementation if desired. + """Handle raised exception. + + This allows users to override this with a custom implementation if desired. This implementation will check to see if the event that triggered the - exception is an exception event. If this exceptino was caused by the - ``exception_event``, then nothing is dispatched (thus preventing - an exception handler recursively re-triggering itself). Otherwise, an - ``exception_event`` is dispatched with a - :obj:`EventExceptionContext` as the sole parameter. + exception is an :obj:`hikari.core.events.ExceptionEvent`. If this + exception was caused by the :obj:`hikari.core.events.ExceptionEvent`, + then nothing is dispatched (thus preventing an exception handler recursively + re-triggering itself). Otherwise, an :obj:`hikari.core.events.ExceptionEvent` + is dispatched. Parameters ---------- exception: :obj:`Exception` The exception that triggered this call. - event: :obj:`events.Event` + event: :obj:`hikari.core.events.HikariEvent` The event that was being dispatched. callback The callback that threw the exception. @@ -351,8 +351,7 @@ def handle_exception( def wait_for( self, event_type: typing.Type[EventT], *, timeout: typing.Optional[float], predicate: PredicateT, ) -> more_asyncio.Future: - """Given an event name, wait for the event to occur once, then return - the arguments that accompanied the event as the result. + """Wait for a event to occur once and then return the arguments the event was called with. Events can be filtered using a given predicate function. If unspecified, the first event of the given name will be a match. @@ -364,7 +363,7 @@ def wait_for( Parameters ---------- - event_type : :obj:`typing.Type` [ :obj:`events.HikariEvent` ] + event_type : :obj:`typing.Type` [ :obj:`hikari.core.events.HikariEvent` ] The name of the event to wait for. timeout : :obj:`float`, optional The timeout to wait for before cancelling and raising an diff --git a/hikari/core/users.py b/hikari/core/users.py index d90ac83700..aee2c9d8e0 100644 --- a/hikari/core/users.py +++ b/hikari/core/users.py @@ -59,31 +59,29 @@ class User(snowflakes.UniqueEntity, entities.Deserializable): @property def avatar_url(self) -> str: - """The url for this user's custom avatar if set, else default.""" + """URL for this user's custom avatar if set, else default.""" return self.format_avatar_url() def format_avatar_url(self, fmt: typing.Optional[str] = None, size: int = 2048) -> str: - """Generate the avatar url for this user's custom avatar if set, - else their default avatar. + """Generate the avatar URL for this user's custom avatar if set, else their default avatar. Parameters ---------- fmt : :obj:`str` - The format to use for this url, defaults to ``png`` or ``gif``. + The format to use for this URL, defaults to ``png`` or ``gif``. Supports ``png``, ``jpeg``, ``jpg``, ``webp`` and ``gif`` (when animated). Will be ignored for default avatars which can only be ``png``. size : :obj:`int` - The size to set for the url, defaults to ``2048``. + The size to set for the URL, defaults to ``2048``. Can be any power of two between 16 and 2048. Will be ignored for default avatars. Returns ------- :obj:`str` - The string url. + The string URL. """ - if not self.avatar_hash: return cdn.generate_cdn_url("embed/avatars", str(self.default_avatar), fmt="png", size=None) if fmt is None and self.avatar_hash.startswith("a_"): @@ -94,7 +92,7 @@ def format_avatar_url(self, fmt: typing.Optional[str] = None, size: int = 2048) @property def default_avatar(self) -> int: - """The number representation of this user's default avatar.""" + """Integer representation of this user's default avatar.""" return int(self.discriminator) % 5 diff --git a/hikari/core/voices.py b/hikari/core/voices.py index a2f2897d18..d3603871d9 100644 --- a/hikari/core/voices.py +++ b/hikari/core/voices.py @@ -16,8 +16,7 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . -"""Components and entities that are used to describe voice states on Discord. -""" +"""Components and entities that are used to describe voice states on Discord.""" __all__ = ["VoiceRegion", "VoiceState"] import typing @@ -34,25 +33,25 @@ class VoiceState(entities.HikariEntity, entities.Deserializable): #: The ID of the guild this voice state is in, if applicable. #: - #: :type: :obj:`snowflakes.Snowflake`, optional + #: :type: :obj:`hikari.core.snowflakes.Snowflake`, optional guild_id: typing.Optional[snowflakes.Snowflake] = marshaller.attrib( deserializer=snowflakes.Snowflake.deserialize, if_undefined=None ) #: The ID of the channel this user is connected to. #: - #: :type: :obj:`.core.snowflakes.Snowflake`, optional + #: :type: :obj:`hikari.core.snowflakes.Snowflake`, optional channel_id: snowflakes.Snowflake = marshaller.attrib(deserializer=snowflakes.Snowflake.deserialize, if_none=None) #: The ID of the user this voice state is for. #: - #: :type: :obj:`snowflakes.Snowflake` + #: :type: :obj:`hikari.core.snowflakes.Snowflake` user_id: snowflakes.Snowflake = marshaller.attrib(deserializer=snowflakes.Snowflake.deserialize) #: The guild member this voice state is for if the voice state is in a #: guild. #: - #: :type: :obj:`guilds.GuildMember`, optional + #: :type: :obj:`hikari.core.guilds.GuildMember`, optional member: typing.Optional[guilds.GuildMember] = marshaller.attrib( deserializer=guilds.GuildMember.deserialize, if_undefined=None ) diff --git a/hikari/core/webhooks.py b/hikari/core/webhooks.py index 3811f4336d..9026f2ed6f 100644 --- a/hikari/core/webhooks.py +++ b/hikari/core/webhooks.py @@ -40,10 +40,11 @@ class WebhookType(enum.IntEnum): @marshaller.attrs(slots=True) class Webhook(snowflakes.UniqueEntity, entities.Deserializable): - """Represents a webhook object on Discord. This is an endpoint that can have - messages sent to it using standard HTTP requests, which enables external - services that are not bots to send informational messages to specific - channels. + """Represents a webhook object on Discord. + + This is an endpoint that can have messages sent to it using standard + HTTP requests, which enables external services that are not bots to + send informational messages to specific channels. """ #: The type of the webhook. @@ -53,14 +54,14 @@ class Webhook(snowflakes.UniqueEntity, entities.Deserializable): #: The guild ID of the webhook. #: - #: :type: :obj:`snowflakes.Snowflake`, optional + #: :type: :obj:`hikari.core.snowflakes.Snowflake`, optional guild_id: typing.Optional[snowflakes.Snowflake] = marshaller.attrib( deserializer=snowflakes.Snowflake.deserialize, if_undefined=None ) #: The channel ID this webhook is for. #: - #: :type: :obj:`snowflakes.Snowflake` + #: :type: :obj:`hikari.core.snowflakes.Snowflake` channel_id: snowflakes.Snowflake = marshaller.attrib(deserializer=snowflakes.Snowflake.deserialize) #: The user that created the webhook @@ -69,7 +70,8 @@ class Webhook(snowflakes.UniqueEntity, entities.Deserializable): #: ---- #: This will be ``None`` when getting a webhook with a token #: - #: :type: :obj:`users.User`, optional + #: + #: :type: :obj:`hikari.core.users.User`, optional user: typing.Optional[users.User] = marshaller.attrib(deserializer=users.User.deserialize, if_undefined=None) #: The default name of the webhook. @@ -88,5 +90,6 @@ class Webhook(snowflakes.UniqueEntity, entities.Deserializable): #: ---- #: This is only available for Incoming webhooks. #: + #: #: :type: :obj:`str`, optional token: typing.Optional[str] = marshaller.attrib(deserializer=str, if_undefined=None) diff --git a/hikari/errors.py b/hikari/errors.py index 55494b24c7..d2c9d9595c 100644 --- a/hikari/errors.py +++ b/hikari/errors.py @@ -17,6 +17,7 @@ # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . """Core errors that may be raised by this API implementation.""" +__all__ = ["HikariError"] class HikariError(RuntimeError): diff --git a/hikari/internal/assertions.py b/hikari/internal/assertions.py index 5bf7fc7304..73d3826c70 100644 --- a/hikari/internal/assertions.py +++ b/hikari/internal/assertions.py @@ -37,20 +37,20 @@ def assert_that(condition: bool, message: str = None, error_type: type = ValueError) -> None: - """Raises a :obj:`ValueError` with the optional description if the given condition is falsified.""" + """If the given condition is falsified, raise a :obj:`ValueError` with the optional description provided.""" if not condition: raise error_type(message or "condition must not be False") def assert_not_none(value: ValueT, message: typing.Optional[str] = None) -> ValueT: - """Raises a :obj:`ValueError` with the optional description if the given value is ``None``.""" + """If the given value is ``None``, raise a :obj:`ValueError` with the optional description provided.""" if value is None: raise ValueError(message or "value must not be None") return value def assert_in_range(value, min_inclusive, max_inclusive, name: str = None): - """Raise a value error if a value is not in the range [min, max]""" + """If a value is not in the range [min, max], raise a :obj:`ValueError`.""" if not (min_inclusive <= value <= max_inclusive): name = name or "The value" raise ValueError(f"{name} must be in the inclusive range of {min_inclusive} and {max_inclusive}") diff --git a/hikari/internal/cdn.py b/hikari/internal/cdn.py index 9c43714697..5c0251b980 100644 --- a/hikari/internal/cdn.py +++ b/hikari/internal/cdn.py @@ -43,7 +43,7 @@ def generate_cdn_url(*route_parts: str, fmt: str, size: typing.Optional[int]) -> The format to use for the wanted cdn entity, will usually be one of ``webp``, ``png``, ``jpeg``, ``jpg`` or ``gif`` (which will be invalid if the target entity doesn't have an animated version available). - size : :obj:`typing.Union` [ :obj:`int`, :obj:`None` ] + size : :obj:`int`, optional The size to specify for the image in the query string if applicable, should be passed through as ``None`` to avoid the param being set. diff --git a/hikari/internal/conversions.py b/hikari/internal/conversions.py index fd7591822e..3f3d0b31ae 100644 --- a/hikari/internal/conversions.py +++ b/hikari/internal/conversions.py @@ -45,8 +45,10 @@ def nullable_cast(value: CastInputT, cast: TypeCastT) -> ResultT: - """Attempts to cast the given ``value`` with the given ``cast``, but only if the - ``value`` is not ``None``. If it is ``None``, then ``None`` is returned instead. + """Attempt to cast the given ``value`` with the given ``cast``. + + This will only succeed if ``value`` is not ``None``. If it is ``None``, + then ``None`` is returned instead. """ if value is None: return None @@ -70,7 +72,7 @@ def put_if_specified( value: typing.Any, type_after: typing.Optional[TypeCastT] = None, ) -> None: - """Add a value to the mapping under the given key as long as the value is not :obj:`typing.Literal` + """Add a value to the mapping under the given key as long as the value is not ``...``. Parameters ---------- @@ -132,12 +134,11 @@ def image_bytes_to_image_data(img_bytes: typing.Optional[bytes] = None) -> typin def try_cast_or_defer_unary_operator(type_): - """Returns a unary operator that will try to cast the given input to - whatever type is provided. + """Return a unary operator that will try to cast the given input to the type provided. Parameters ---------- - type_: + type_ : :obj:`typing.Callable` [ ..., :obj:`T` ] The type to cast to. """ return lambda data: try_cast(data, type_, data) @@ -151,7 +152,12 @@ def parse_http_date(date_str: str) -> datetime.datetime: date_str : :obj:`str` The RFC-2822 (section 3.3) compliant date string to parse. - See also + Returns + ------- + :obj:`datetime.datetime` + The HTTP date as a datetime object. + + See Also -------- ``_ """ @@ -164,14 +170,19 @@ def parse_http_date(date_str: str) -> datetime.datetime: def parse_iso_8601_ts(date_string: str) -> datetime.datetime: - """Parses an ISO 8601 date string into a datetime object + """Parse an ISO 8601 date string into a :obj:`datetime.datetime` object. Parameters ---------- date_string : :obj:`str` The ISO 8601 compliant date string to parse. - See also + Returns + ------- + :obj:`datetime.datetime` + The ISO 8601 date string as a datetime object. + + See Also -------- ``_ """ @@ -200,7 +211,7 @@ def parse_iso_8601_ts(date_string: str) -> datetime.datetime: def discord_epoch_to_datetime(epoch: int) -> datetime.datetime: - """Parses a discord epoch into a datetime object + """Parse a Discord epoch into a :obj:`datetime.datetime` object. Parameters ---------- @@ -216,7 +227,7 @@ def discord_epoch_to_datetime(epoch: int) -> datetime.datetime: def unix_epoch_to_ts(epoch: int) -> datetime.datetime: - """Parses a unix epoch to a datetime object + """Parse a UNIX epoch to a :obj:`datetime.datetime` object. Parameters ---------- @@ -232,7 +243,7 @@ def unix_epoch_to_ts(epoch: int) -> datetime.datetime: def make_resource_seekable(resource: typing.Any) -> typing.Union[io.BytesIO, io.StringIO]: - """Given some representation of data, make a seekable resource to use. + """Make a seekable resource to use off some representation of data. This supports :obj:`bytes`, :obj:`bytearray`, :obj:`memoryview`, and :obj:`str`. Anything else is just returned. diff --git a/hikari/internal/marshaller.py b/hikari/internal/marshaller.py index 6de64cc1c3..80874f3297 100644 --- a/hikari/internal/marshaller.py +++ b/hikari/internal/marshaller.py @@ -16,10 +16,10 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . -"""This is an internal marshalling utility used by internal API components. +"""An internal marshalling utility used by internal API components. -Warnings --------- +Warning +------- You should not change anything in this file, if you do, you will likely get unexpected behaviour elsewhere. @@ -101,17 +101,16 @@ def dereference_handle(handle_string: str) -> typing.Any: def dereference_int_flag(int_flag_type, raw_value) -> typing.SupportsInt: - """Given a type of :obj:`enum.IntFlag`, and a raw value, cast the raw value - to the int flag. + """Cast to the provided :obj:`enum.IntFlag` type. - This will support resolving bitfield integers as well as decoding a sequence + This supports resolving bitfield integers as well as decoding a sequence of case insensitive flag names into one combined value. Parameters ---------- - int_flag_type: + int_flag_type : :obj:`typing.Type` [ :obj:`enum.IntFlag` ] The type of the int flag to check. - raw_value: + raw_value The raw value to convert. """ if isinstance(raw_value, str) and raw_value.isdigit(): @@ -291,17 +290,18 @@ def _construct_entity_descriptor(entity: typing.Any): class HikariEntityMarshaller: - """This is a global marshaller helper that can help to deserialize and - serialize any internal components that are decorated with the - :obj:`attrs` decorator, and that are :mod:`attr` classes using fields - with the :obj:`attrib` function call descriptor. + """A global marshaller helper that helps deserialize and serialize any internal components. + + It can deserialize and serialize any internal componentsthat that are + decorated with the :obj:`attrs` decorator, and that are :mod:`attr` + classes using fields with the :obj:`attrib` function call descriptor. """ def __init__(self) -> None: self._registered_entities: typing.MutableMapping[typing.Type, _EntityDescriptor] = {} def register(self, cls: typing.Type[EntityT]) -> typing.Type[EntityT]: - """Registers an attrs type for fast future deserialization. + """Register an attrs type for fast future deserialization. Parameters ---------- @@ -440,7 +440,7 @@ def serialize(self, obj: typing.Optional[typing.Any]) -> typing.Optional[typing. def attrs(**kwargs): - """Creates a decorator for a class to make it into an :mod:`attrs` class. + """Create a decorator for a class to make it into an :mod:`attrs` class. Parameters ---------- diff --git a/hikari/internal/meta.py b/hikari/internal/meta.py index 7d90fe641a..544e5059cf 100644 --- a/hikari/internal/meta.py +++ b/hikari/internal/meta.py @@ -30,7 +30,7 @@ class SingletonMeta(type): interpreter that created it is terminated. Example - -------- + ------- .. code-block:: python >>> class Unknown(metaclass=SingletonMeta): @@ -67,7 +67,6 @@ class Singleton(metaclass=SingletonMeta): Example ------- - .. code-block:: python >>> class MySingleton(Singleton): diff --git a/hikari/internal/more_asyncio.py b/hikari/internal/more_asyncio.py index 0b7f2dff4b..bf968e4c42 100644 --- a/hikari/internal/more_asyncio.py +++ b/hikari/internal/more_asyncio.py @@ -122,7 +122,7 @@ def completed_future(result=None, /): """Create a future on the current running loop that is completed, then return it. Parameters - --------- + ---------- result : :obj:`typing.Any` The value to set for the result of the future. diff --git a/hikari/internal/more_logging.py b/hikari/internal/more_logging.py index 46c24ad191..e1a3995a4e 100644 --- a/hikari/internal/more_logging.py +++ b/hikari/internal/more_logging.py @@ -28,7 +28,7 @@ def get_named_logger(obj: typing.Any, *extra_objs: typing.Any) -> logging.Logger: - """Builds an appropriately named logger. + """Build an appropriately named logger. If the passed object is an instance of a class, the class is used instead. diff --git a/hikari/net/codes.py b/hikari/net/codes.py index 9b2338288c..7fe9b318be 100644 --- a/hikari/net/codes.py +++ b/hikari/net/codes.py @@ -80,7 +80,7 @@ class GatewayCloseCode(enum.IntEnum): UNKNOWN_OPCODE = 4001 #: You sent an invalid payload to Discord. Don't do that! DECODE_ERROR = 4002 - #: You sent Discord a payload prior to identifying. + #: You sent Discord a payload prior to IDENTIFYing. NOT_AUTHENTICATED = 4003 #: The account token sent with your identify payload is incorrect. AUTHENTICATION_FAILED = 4004 @@ -92,7 +92,7 @@ class GatewayCloseCode(enum.IntEnum): RATE_LIMITED = 4008 #: Your session timed out. Reconnect and start a new one. SESSION_TIMEOUT = 4009 - #: You sent Discord an invalid shard when identifying. + #: You sent Discord an invalid shard when IDENTIFYing. INVALID_SHARD = 4010 #: The session would have handled too many guilds - you are required to shard your connection in order to connect. SHARDING_REQUIRED = 4011 diff --git a/hikari/net/errors.py b/hikari/net/errors.py index ba41c479be..d463636a9c 100644 --- a/hikari/net/errors.py +++ b/hikari/net/errors.py @@ -65,8 +65,7 @@ def __str__(self) -> str: class GatewayClientClosedError(GatewayError): - """An exception raised when you programmatically shut down the bot - client-side. + """An exception raised when you programmatically shut down the bot client-side. Parameters ---------- @@ -109,9 +108,7 @@ def __init__( class GatewayInvalidTokenError(GatewayServerClosedConnectionError): - """An exception that is raised if you failed to authenticate with a valid - token to the Gateway. - """ + """An exception that is raised if you failed to authenticate with a valid token to the Gateway.""" def __init__(self) -> None: super().__init__( @@ -126,11 +123,11 @@ class GatewayInvalidSessionError(GatewayServerClosedConnectionError): Parameters ---------- can_resume : :obj:`bool` - True if the connection will be able to RESUME next time it starts rather - than re-IDENTIFYing, or False if you need to IDENTIFY again instead. + ``True`` if the connection will be able to RESUME next time it starts rather + than re-IDENTIFYing, or ``False`` if you need to IDENTIFY again instead. """ - #: True if the next reconnection can be RESUMED. False if it has to be + #: ``True``` if the next reconnection can be RESUMED. ``False``` if it has to be #: coordinated by re-IDENFITYing. #: #: :type: :obj:`bool` @@ -143,9 +140,9 @@ def __init__(self, can_resume: bool) -> None: class GatewayMustReconnectError(GatewayServerClosedConnectionError): - """An exception raised when the Gateway has to re-connect with a new session - (thus re-IDENTIFYing in the process). + """An exception raised when the Gateway has to re-connect with a new session. + This will cause a re-IDENTIFY. """ def __init__(self) -> None: @@ -153,8 +150,7 @@ def __init__(self) -> None: class GatewayNeedsShardingError(GatewayServerClosedConnectionError): - """An exception raised if you have too many guilds on one of the current - Gateway shards. + """An exception raised if you have too many guilds on one of the current Gateway shards. This is a sign you need to increase the number of shards that your bot is running with in order to connect to Discord. @@ -254,8 +250,7 @@ def __str__(self) -> str: class ServerHTTPError(CodedHTTPError): - """An exception raised if a server-side error occurs when interacting with - the HTTP API. + """An exception raised if a server-side error occurs when interacting with the REST API. If you get these, DO NOT PANIC! Your bot is working perfectly fine. Discord have probably broken something again. @@ -263,8 +258,7 @@ class ServerHTTPError(CodedHTTPError): class ClientHTTPError(CodedHTTPError): - """An exception raised if a client-side error occurs when interacting with - the HTTP API. + """An exception raised if a server-side error occurs when interacting with the REST API. If you get one of these, you most likely have a mistake in your code, or have found a bug with this library. @@ -275,9 +269,10 @@ class ClientHTTPError(CodedHTTPError): class BadRequestHTTPError(CodedHTTPError): - """A specific case of :obj:`ClientHTTPError` that occurs when you send - Discord information in an unexpected format, miss required information out, - or give bad values for stuff. + """A specific case of :obj:`CodedHTTPError`. + + This can occur hat occurs when you send Discord information in an unexpected + format, miss required information out, or give bad values for stuff. An example might be sending a message without any content, or an embed with more than 6000 characters. @@ -302,8 +297,10 @@ def __init__( class UnauthorizedHTTPError(ClientHTTPError): - """A specific case of :obj:`ClientHTTPError` that occurs when you have - invalid authorization details to access the given resource. + """A specific case of :obj:`ClientHTTPError`. + + This occurs when you have invalid authorization details to access + the given resource. This usually means that you have an incorrect token. @@ -327,8 +324,7 @@ def __init__( class ForbiddenHTTPError(ClientHTTPError): - """A specific case of :obj:`ClientHTTPError` that occurs when you - are not allowed to view a given resource. + """A specific case of :obj:`ClientHTTPError`. This occurs when you are missing permissions, or are using an endpoint that your account is not allowed to see without being whitelisted. @@ -355,9 +351,9 @@ def __init__( class NotFoundHTTPError(ClientHTTPError): - """A specific case of :obj:`ClientHTTPError` that occurs when you try to - refer to something that doesn't exist on Discord. + """A specific case of :obj:`ClientHTTPError`. + This occurs when you try to refer to something that doesn't exist on Discord. This might be referring to a user ID, channel ID, guild ID, etc that does not exist, or it might be attempting to use an HTTP endpoint that is not found. diff --git a/hikari/net/ratelimits.py b/hikari/net/ratelimits.py index 47a909a1c8..d869937a19 100644 --- a/hikari/net/ratelimits.py +++ b/hikari/net/ratelimits.py @@ -16,7 +16,8 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . -""" +"""Complex rate limiting mechanisms. + Provides an implementation for the complex rate limiting mechanisms that Discord requires for rate limit handling that conforms to the passed bucket headers correctly. @@ -55,8 +56,7 @@ Rate limits, on the other hand, apply to a bucket and are specific to the major parameters of the compiled route. This means that ``POST /channels/123/messages`` and ``POST /channels/456/messages`` do not share the same real bucket, despite -Discord providing the same bucket hash. A -:obj:`hikari.net.ratelimits.RealBucketHash`, therefore, is the :obj:`str` +Discord providing the same bucket hash. A real bucket hash is the :obj:`str` hash of the bucket that Discord sends us in a response concatenated to the corresponding major parameters. This is used for quick bucket indexing internally in this module. @@ -75,19 +75,18 @@ Initially acquiring time on a bucket ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Each time you :meth:`IRateLimiter.acquire` a request -timeslice for a given :obj:`hikari.net.ratelimits.CompiledRoute`, several +Each time you ``BaseRateLimiter.acquire()`` a request +timeslice for a given :obj:`hikari.net.routes.CompiledRoute`, several things happen. The first is that we attempt to find the existing bucket for that route, if there is one, or get an unknown bucket otherwise. This is done -by creating a :obj:`hikari.net.ratelimits.RealBucketHash` from the compiled -route. The initial hash is calculated using a lookup table that maps -:obj:`hikari.net.ratelimits.CompiledRoute` objects to their corresponding -initial hash codes, or to the unknown bucket hash code if not yet known. This -initial hash is processed by the :class`hikari.net.ratelimits.CompiledRoute` to -provide the :obj:`RealBucketHash` we need to get the route's bucket object -internally. - -The :meth:`acquire` method will take the bucket and acquire a new timeslice on +by creating a real bucket hash` from the compiled route. The initial hash +is calculated using a lookup table that maps :obj:`hikari.net.routes.CompiledRoute` +objects to their corresponding initial hash codes, or to the unknown bucket +hash code if not yet known. This initial hash is processed by the +:obj:`hikari.net.routes.CompiledRoute` to provide the real bucket hash we +need to get the route's bucket object internally. + +The ``acquire`` method will take the bucket and acquire a new timeslice on it. This takes the form of a :obj:`asyncio.Future` which should be awaited by the caller and will complete once the caller is allowed to make a request. Most of the time, this is done instantly, but if the bucket has an active rate limit @@ -102,15 +101,15 @@ tidies itself up and disposes of itself. This task will complete once the queue becomes empty. -The result of :meth:`hikari.net.ratelimits.RateLimiter.acquire` is a tuple of a +The result of ``RateLimiter.acquire()`` is a tuple of a :obj:`asyncio.Future` to await on which completes when you are allowed to -proceed with making a request, and a :class`RealBucketHash` which should be +proceed with making a request, and a real bucket hash which should be stored temporarily. This will be explained in the next section. When you make your response, you should be sure to set the -``X-RateLimit-Precision`` header to `millisecond` to ensure a much greater +``X-RateLimit-Precision`` header to ``millisecond`` to ensure a much greater accuracy against rounding errors for rate limits (reduces the error margin from -`1` second to `1` millisecond). +``1`` second to ``1`` millisecond). Handling the rate limit headers of a response ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -133,7 +132,7 @@ * ``X-RateLimit-Reset``: a :obj:`float` containing the number of seconds since 1st January 1970 at 0:00:00 UTC at which the current ratelimit window - resets. This should be parsed to a :obj:`datetime` using + resets. This should be parsed to a :obj:`datetime.datetime` using :func:`datetime.datetime.fromtimestamp`, passing :obj:`datetime.timezone.utc` as a second parameter. @@ -148,14 +147,14 @@ Tidying up ~~~~~~~~~~ -To prevent unused buckets cluttering up memory, each :obj:`IRateLimiter` +To prevent unused buckets cluttering up memory, each :obj:`BaseRateLimiter` instance spins up a :obj:`asyncio.Task` that periodically locks the bucket list (not threadsafe, only using the concept of asyncio not yielding in regular functions) and disposes of any clearly stale buckets that are no longer needed. These will be recreated again in the future if they are needed. -When shutting down an application, one must remember to :meth`close` the -:obj:`IRateLimiter` that has been used. This will ensure the garbage collection +When shutting down an application, one must remember to ``close()`` the +:obj:`BaseRateLimiter` that has been used. This will ensure the garbage collection task is stopped, and will also ensure any remaining futures in any bucket queues have an :obj:`asyncio.CancelledError` set on them to prevent deadlocking ratelimited calls that may be waiting to be unlocked. @@ -201,8 +200,7 @@ class BaseRateLimiter(abc.ABC): @abc.abstractmethod def acquire(self) -> more_asyncio.Future[None]: - """Acquire permission to perform a task that needs to have rate limit - management enforced. + """Acquire permission to perform a task that needs to have rate limit management enforced. Returns ------- @@ -388,7 +386,6 @@ async def unlock_later(self, retry_after: float) -> None: expected to set the :attr:`throttle_task` to ``None``. This means you can check if throttling is occurring by checking if :attr:`throttle_task` is not ``None``. - """ self.logger.warning("you are being globally rate limited for %ss", retry_after) await asyncio.sleep(retry_after) @@ -399,7 +396,9 @@ async def unlock_later(self, retry_after: float) -> None: class WindowedBurstRateLimiter(BurstRateLimiter): - """Rate limiter for rate limits that last fixed periods of time with a + """Windowed burst rate limiter. + + Rate limiter for rate limits that last fixed periods of time with a fixed number of times it can be used in that time frame. To use this, you should call :meth:`acquire` and await the result @@ -734,8 +733,8 @@ def start(self, poll_period: float = 20) -> None: Parameters ---------- poll_period : :obj:`float` - The period to poll the garbage collector at. This defaults to 20 - seconds. + Period to poll the garbage collector at in seconds. Defaults + to ``20`` seconds. """ if not self.gc_task: self.gc_task = asyncio.get_running_loop().create_task(self.gc(poll_period)) @@ -752,7 +751,8 @@ def close(self) -> None: self.real_hashes_to_buckets.clear() self.routes_to_hashes.clear() - async def gc(self, poll_period: float = 20) -> None: + # Ignore docstring not starting in an imperative mood + async def gc(self, poll_period: float = 20) -> None: # noqa: D401 """The garbage collector loop. This is designed to run in the background and manage removing unused @@ -865,7 +865,6 @@ def update_rate_limits( Parameters ---------- - compiled_route : :obj:`hikari.net.routes.CompiledRoute` The compiled route to get the bucket for. bucket_header : :obj:`str`, optional @@ -897,8 +896,7 @@ def update_rate_limits( class ExponentialBackOff: - """Implementation of an asyncio-compatible exponential back-off algorithm - with random jitter. + r"""Implementation of an asyncio-compatible exponential back-off algorithm with random jitter. .. math:: @@ -911,7 +909,6 @@ class ExponentialBackOff: Parameters ---------- - base : :obj:`float` The base to use. Defaults to ``2``. maximum : :obj:`float`, optional @@ -966,9 +963,9 @@ def __next__(self) -> float: return value def __iter__(self) -> "ExponentialBackOff": - """Returns this object, as it is an iterator.""" + """Return this object, as it is an iterator.""" return self def reset(self) -> None: - """Resets the exponential back-off.""" + """Reset the exponential back-off.""" self.increment = 0 diff --git a/hikari/net/rest.py b/hikari/net/rest.py index 4b9fc82701..0bc3db85e2 100644 --- a/hikari/net/rest.py +++ b/hikari/net/rest.py @@ -180,9 +180,7 @@ def __init__( self.token = token async def close(self): - """Shut down the REST client safely, and terminate any rate limiters - executing in the background. - """ + """Shut down the REST client safely, and terminate any rate limiters executing in the background.""" with contextlib.suppress(Exception): self.ratelimiter.close() with contextlib.suppress(Exception): @@ -363,7 +361,8 @@ async def _handle_bad_response( raise errors.ServerHTTPError(status, route, message, code) async def get_gateway(self) -> str: - """ + """Get the URL to use to connect to the gateway with. + Returns ------- :obj:`str` @@ -377,10 +376,11 @@ async def get_gateway(self) -> str: return result["url"] async def get_gateway_bot(self) -> typing.Dict[str, typing.Any]: - """ + """Get the gateway info for the bot. + Returns ------- - :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ]` + :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ] An object containing a ``url`` to connect to, an :obj:`int` number of shards recommended to use for connecting, and a ``session_start_limit`` object. @@ -409,7 +409,7 @@ async def get_guild_audit_log( Returns ------- - :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ]` + :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ] An audit log object. Raises @@ -436,7 +436,7 @@ async def get_channel(self, channel_id: str) -> typing.Dict[str, typing.Any]: Returns ------- - :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ]` + :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ] The channel object that has been found. Raises @@ -496,7 +496,7 @@ async def modify_channel( # lgtm [py/similar-function] If specified, the new max number of users to allow in a voice channel. This must be between ``0`` and ``99`` inclusive, where ``0`` implies no limit. - permission_overwrites : :obj:`typing.Sequence` [ :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ]` ] + permission_overwrites : :obj:`typing.Sequence` [ :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ] ] If specified, the new list of permission overwrites that are category specific to replace the existing overwrites with. parent_id : :obj:`str` @@ -507,7 +507,7 @@ async def modify_channel( # lgtm [py/similar-function] Returns ------- - :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ]` + :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ] The channel object that has been modified. Raises @@ -565,6 +565,7 @@ async def get_channel_messages( self, channel_id: str, *, limit: int = ..., after: str = ..., before: str = ..., around: str = ..., ) -> typing.Sequence[typing.Dict[str, typing.Any]]: """Retrieve message history for a given channel. + If a user is provided, retrieve the DM history. Parameters @@ -584,7 +585,7 @@ async def get_channel_messages( Returns ------- - :obj:`typing.Sequence` [ :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ]` ] + :obj:`typing.Sequence` [ :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ] ] A list of message objects. Raises @@ -630,7 +631,7 @@ async def get_channel_message(self, channel_id: str, message_id: str) -> typing. Returns ------- - :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ]` + :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ] A message object. Note @@ -677,15 +678,15 @@ async def create_message( Each tuple should consist of the file name, and either raw :obj:`bytes` or an :obj:`io.IOBase` derived object with a seek that points to a buffer containing said file. - embed : :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ]` + embed : :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ] If specified, the embed to send with the message. - allowed_mentions : :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ]` + allowed_mentions : :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ] If specified, the mentions to parse from the ``content``. If not specified, will parse all mentions from the ``content``. Returns ------- - :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ]` + :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ] The created message object. Raises @@ -831,8 +832,7 @@ async def delete_user_reaction(self, channel_id: str, message_id: str, emoji: st async def get_reactions( self, channel_id: str, message_id: str, emoji: str, *, after: str = ..., limit: int = ..., ) -> typing.Sequence[typing.Dict[str, typing.Any]]: - """Get a list of users who reacted with the given emoji on - the given message in the given channel or user DM. + """Get a list of users who reacted with the given emoji on the given message. Parameters ---------- @@ -854,7 +854,7 @@ async def get_reactions( Returns ------- - :obj:`typing.Sequence` [ :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ]` ] + :obj:`typing.Sequence` [ :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ] ] A list of user objects. Raises @@ -871,7 +871,7 @@ async def get_reactions( return await self._request(route, query=query) async def delete_all_reactions(self, channel_id: str, message_id: str) -> None: - """Deletes all reactions from a given message in a given channel. + """Delete all reactions from a given message in a given channel. Parameters ---------- @@ -910,7 +910,7 @@ async def edit_message( content : :obj:`str`, optional If specified, the string content to replace with in the message. If ``None``, the content will be removed from the message. - embed : :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ]`, optional + embed : :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ], optional If specified, the embed to replace with in the message. If ``None``, the embed will be removed from the message. flags : :obj:`int` @@ -918,7 +918,7 @@ async def edit_message( Returns ------- - :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ]` + :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ] The edited message object. Raises @@ -1048,7 +1048,7 @@ async def get_channel_invites(self, channel_id: str) -> typing.Sequence[typing.D Returns ------- - :obj:`typing.Sequence` [ :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ]` ] + :obj:`typing.Sequence` [ :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ] ] A list of invite objects. Raises @@ -1101,7 +1101,7 @@ async def create_channel_invite( Returns ------- - :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ]` + :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ] An invite object. Raises @@ -1172,7 +1172,7 @@ async def get_pinned_messages(self, channel_id: str) -> typing.Sequence[typing.D Returns ------- - :obj:`typing.Sequence` [ :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ]` ] + :obj:`typing.Sequence` [ :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ] ] A list of messages. Raises @@ -1233,7 +1233,7 @@ async def delete_pinned_channel_message(self, channel_id: str, message_id: str) await self._request(route) async def list_guild_emojis(self, guild_id: str) -> typing.Sequence[typing.Dict[str, typing.Any]]: - """Gets emojis for a given guild ID. + """Get a list of the emojis for a given guild ID. Parameters ---------- @@ -1242,7 +1242,7 @@ async def list_guild_emojis(self, guild_id: str) -> typing.Sequence[typing.Dict[ Returns ------- - :obj:`typing.Sequence` [ :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ]` ] + :obj:`typing.Sequence` [ :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ] ] A list of emoji objects. Raises @@ -1256,7 +1256,7 @@ async def list_guild_emojis(self, guild_id: str) -> typing.Sequence[typing.Dict[ return await self._request(route) async def get_guild_emoji(self, guild_id: str, emoji_id: str) -> typing.Dict[str, typing.Any]: - """Gets an emoji from a given guild and emoji IDs. + """Get an emoji from a given guild and emoji IDs. Parameters ---------- @@ -1267,7 +1267,7 @@ async def get_guild_emoji(self, guild_id: str, emoji_id: str) -> typing.Dict[str Returns ------- - :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ]` + :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ] An emoji object. Raises @@ -1283,7 +1283,7 @@ async def get_guild_emoji(self, guild_id: str, emoji_id: str) -> typing.Dict[str async def create_guild_emoji( self, guild_id: str, name: str, image: bytes, *, roles: typing.Sequence[str] = ..., reason: str = ..., ) -> typing.Dict[str, typing.Any]: - """Creates a new emoji for a given guild. + """Create a new emoji for a given guild. Parameters ---------- @@ -1302,7 +1302,7 @@ async def create_guild_emoji( Returns ------- - :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ]` + :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ] The newly created emoji object. Raises @@ -1328,7 +1328,7 @@ async def create_guild_emoji( async def modify_guild_emoji( self, guild_id: str, emoji_id: str, *, name: str = ..., roles: typing.Sequence[str] = ..., reason: str = ..., ) -> typing.Dict[str, typing.Any]: - """Edits an emoji of a given guild + """Edit an emoji of a given guild. Parameters ---------- @@ -1348,7 +1348,7 @@ async def modify_guild_emoji( Returns ------- - :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ]` + :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ] The updated emoji object. Raises @@ -1365,7 +1365,7 @@ async def modify_guild_emoji( return await self._request(route, json_body=payload, reason=reason) async def delete_guild_emoji(self, guild_id: str, emoji_id: str) -> None: - """Deletes an emoji from a given guild + """Delete an emoji from a given guild. Parameters ---------- @@ -1396,7 +1396,11 @@ async def create_guild( roles: typing.Sequence[typing.Dict[str, typing.Any]] = ..., channels: typing.Sequence[typing.Dict[str, typing.Any]] = ..., ) -> typing.Dict[str, typing.Any]: - """Creates a new guild. Can only be used by bots in less than ``10`` guilds. + """Create a new guild. + + Warning + ------- + Can only be used by bots in less than ``10`` guilds. Parameters ---------- @@ -1413,15 +1417,15 @@ async def create_guild( If specified, the default notification level integer (``0-1``). explicit_content_filter : :obj:`int` If specified, the explicit content filter integer (``0-2``). - roles : :obj:`typing.Sequence` [ :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ]` ] + roles : :obj:`typing.Sequence` [ :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ] ] If specified, an array of role objects to be created alongside the guild. First element changes the ``@everyone`` role. - channels : :obj:`typing.Sequence` [ :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ]` ] + channels : :obj:`typing.Sequence` [ :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ] ] If specified, an array of channel objects to be created alongside the guild. Returns ------- - :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ]` + :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ] The newly created guild object. Raises @@ -1443,7 +1447,7 @@ async def create_guild( return await self._request(route, json_body=payload) async def get_guild(self, guild_id: str) -> typing.Dict[str, typing.Any]: - """Gets a given guild's object. + """Get a given guild's object. Parameters ---------- @@ -1452,7 +1456,7 @@ async def get_guild(self, guild_id: str) -> typing.Dict[str, typing.Any]: Returns ------- - :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ]` + :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ] The requested guild object. Raises @@ -1483,7 +1487,7 @@ async def modify_guild( # lgtm [py/similar-function] system_channel_id: str = ..., reason: str = ..., ) -> typing.Dict[str, typing.Any]: - """Edits a given guild. + """Edit a given guild. Parameters ---------- @@ -1518,7 +1522,7 @@ async def modify_guild( # lgtm [py/similar-function] Returns ------- - :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ]` + :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ] The edited guild object. Raises @@ -1546,7 +1550,7 @@ async def modify_guild( # lgtm [py/similar-function] # pylint: enable=too-many-locals async def delete_guild(self, guild_id: str) -> None: - """Permanently deletes the given guild. + """Permanently delete the given guild. You must be owner of the guild to perform this action. @@ -1566,7 +1570,7 @@ async def delete_guild(self, guild_id: str) -> None: await self._request(route) async def get_guild_channels(self, guild_id: str) -> typing.Sequence[typing.Dict[str, typing.Any]]: - """Gets all the channels for a given guild. + """Get all the channels for a given guild. Parameters ---------- @@ -1575,7 +1579,7 @@ async def get_guild_channels(self, guild_id: str) -> typing.Sequence[typing.Dict Returns ------- - :obj:`typing.Sequence` [ :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ]` ] + :obj:`typing.Sequence` [ :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ] ] A list of channel objects. Raises @@ -1604,7 +1608,7 @@ async def create_guild_channel( parent_id: str = ..., reason: str = ..., ) -> typing.Dict[str, typing.Any]: - """Creates a channel in a given guild. + """Create a channel in a given guild. Parameters ---------- @@ -1638,7 +1642,7 @@ async def create_guild_channel( If specified, the max number of users to allow in a voice channel. This must be between ``0`` and ``99`` inclusive, where ``0`` implies no limit. - permission_overwrites : :obj:`typing.Sequence` [ :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ]` ] + permission_overwrites : :obj:`typing.Sequence` [ :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ] ] If specified, the list of permission overwrites that are category specific to replace the existing overwrites with. parent_id : :obj:`str` @@ -1649,7 +1653,7 @@ async def create_guild_channel( Returns ------- - :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ]` + :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ] The newly created channel object. Raises @@ -1679,7 +1683,7 @@ async def create_guild_channel( async def modify_guild_channel_positions( self, guild_id: str, channel: typing.Tuple[str, int], *channels: typing.Tuple[str, int] ) -> None: - """Edits the position of one or more given channels. + """Edit the position of one or more given channels. Parameters ---------- @@ -1706,7 +1710,7 @@ async def modify_guild_channel_positions( await self._request(route, json_body=payload) async def get_guild_member(self, guild_id: str, user_id: str) -> typing.Dict[str, typing.Any]: - """Gets a given guild member. + """Get a given guild member. Parameters ---------- @@ -1717,7 +1721,7 @@ async def get_guild_member(self, guild_id: str, user_id: str) -> typing.Dict[str Returns ------- - :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ]` + :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ] The requested member object. Raises @@ -1733,7 +1737,7 @@ async def get_guild_member(self, guild_id: str, user_id: str) -> typing.Dict[str async def list_guild_members( self, guild_id: str, *, limit: int = ..., after: str = ..., ) -> typing.Sequence[typing.Dict[str, typing.Any]]: - """Lists all members of a given guild. + """List all members of a given guild. Parameters ---------- @@ -1764,7 +1768,7 @@ async def list_guild_members( Returns ------- - :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ]` + :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ] A list of member objects. Raises @@ -1794,7 +1798,7 @@ async def modify_guild_member( # lgtm [py/similar-function] channel_id: typing.Optional[str] = ..., reason: str = ..., ) -> None: - """Edits a member of a given guild. + """Edit a member of a given guild. Parameters ---------- @@ -1840,7 +1844,7 @@ async def modify_guild_member( # lgtm [py/similar-function] await self._request(route, json_body=payload, reason=reason) async def modify_current_user_nick(self, guild_id: str, nick: typing.Optional[str], *, reason: str = ...,) -> None: - """Edits the current user's nickname for a given guild. + """Edit the current user's nickname for a given guild. Parameters ---------- @@ -1866,7 +1870,7 @@ async def modify_current_user_nick(self, guild_id: str, nick: typing.Optional[st await self._request(route, json_body=payload, reason=reason) async def add_guild_member_role(self, guild_id: str, user_id: str, role_id: str, *, reason: str = ...,) -> None: - """Adds a role to a given member. + """Add a role to a given member. Parameters ---------- @@ -1891,7 +1895,7 @@ async def add_guild_member_role(self, guild_id: str, user_id: str, role_id: str, await self._request(route, reason=reason) async def remove_guild_member_role(self, guild_id: str, user_id: str, role_id: str, *, reason: str = ...,) -> None: - """Removed a role from a given member. + """Remove a role from a given member. Parameters ---------- @@ -1916,7 +1920,7 @@ async def remove_guild_member_role(self, guild_id: str, user_id: str, role_id: s await self._request(route, reason=reason) async def remove_guild_member(self, guild_id: str, user_id: str, *, reason: str = ...,) -> None: - """Kicks a user from a given guild. + """Kick a user from a given guild. Parameters ---------- @@ -1939,7 +1943,7 @@ async def remove_guild_member(self, guild_id: str, user_id: str, *, reason: str await self._request(route, reason=reason) async def get_guild_bans(self, guild_id: str) -> typing.Sequence[typing.Dict[str, typing.Any]]: - """Gets the bans for a given guild. + """Get the bans for a given guild. Parameters ---------- @@ -1948,7 +1952,7 @@ async def get_guild_bans(self, guild_id: str) -> typing.Sequence[typing.Dict[str Returns ------- - :obj:`typing.Sequence` [ :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ]` ] + :obj:`typing.Sequence` [ :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ] ] A list of ban objects. Raises @@ -1962,7 +1966,7 @@ async def get_guild_bans(self, guild_id: str) -> typing.Sequence[typing.Dict[str return await self._request(route) async def get_guild_ban(self, guild_id: str, user_id: str) -> typing.Dict[str, typing.Any]: - """Gets a ban from a given guild. + """Get a ban from a given guild. Parameters ---------- @@ -1973,7 +1977,7 @@ async def get_guild_ban(self, guild_id: str, user_id: str) -> typing.Dict[str, t Returns ------- - :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ]` + :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ] A ban object for the requested user. Raises @@ -1989,7 +1993,7 @@ async def get_guild_ban(self, guild_id: str, user_id: str) -> typing.Dict[str, t async def create_guild_ban( self, guild_id: str, user_id: str, *, delete_message_days: int = ..., reason: str = ..., ) -> None: - """Bans a user from a given guild. + """Ban a user from a given guild. Parameters ---------- @@ -2041,7 +2045,7 @@ async def remove_guild_ban(self, guild_id: str, user_id: str, *, reason: str = . await self._request(route, reason=reason) async def get_guild_roles(self, guild_id: str) -> typing.Sequence[typing.Dict[str, typing.Any]]: - """Gets the roles for a given guild. + """Get the roles for a given guild. Parameters ---------- @@ -2050,7 +2054,7 @@ async def get_guild_roles(self, guild_id: str) -> typing.Sequence[typing.Dict[st Returns ------- - :obj:`typing.Sequence` [ :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ]` ] + :obj:`typing.Sequence` [ :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ] ] A list of role objects. Raises @@ -2074,7 +2078,7 @@ async def create_guild_role( mentionable: bool = ..., reason: str = ..., ) -> typing.Dict[str, typing.Any]: - """Creates a new role for a given guild. + """Create a new role for a given guild. Parameters ---------- @@ -2096,7 +2100,7 @@ async def create_guild_role( Returns ------- - :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ]` + :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ] The newly created role object. Raises @@ -2120,7 +2124,7 @@ async def create_guild_role( async def modify_guild_role_positions( self, guild_id: str, role: typing.Tuple[str, int], *roles: typing.Tuple[str, int] ) -> typing.Sequence[typing.Dict[str, typing.Any]]: - """Edits the position of two or more roles in a given guild. + """Edit the position of two or more roles in a given guild. Parameters ---------- @@ -2133,7 +2137,7 @@ async def modify_guild_role_positions( Returns ------- - :obj:`typing.Sequence` [ :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ]` ] + :obj:`typing.Sequence` [ :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ] ] A list of all the guild roles. Raises @@ -2185,7 +2189,7 @@ async def modify_guild_role( # lgtm [py/similar-function] Returns ------- - :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ]` + :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ] The edited role object. Raises @@ -2207,7 +2211,7 @@ async def modify_guild_role( # lgtm [py/similar-function] return await self._request(route, json_body=payload, reason=reason) async def delete_guild_role(self, guild_id: str, role_id: str) -> None: - """Deletes a role from a given guild. + """Delete a role from a given guild. Parameters ---------- @@ -2227,7 +2231,7 @@ async def delete_guild_role(self, guild_id: str, role_id: str) -> None: await self._request(route) async def get_guild_prune_count(self, guild_id: str, days: int) -> int: - """Gets the estimated prune count for a given guild. + """Get the estimated prune count for a given guild. Parameters ---------- @@ -2258,7 +2262,7 @@ async def get_guild_prune_count(self, guild_id: str, days: int) -> int: async def begin_guild_prune( self, guild_id: str, days: int, *, compute_prune_count: bool = ..., reason: str = ..., ) -> typing.Optional[int]: - """Prunes members of a given guild based on the number of inactive days. + """Prune members of a given guild based on the number of inactive days. Parameters ---------- @@ -2299,7 +2303,7 @@ async def begin_guild_prune( return None async def get_guild_voice_regions(self, guild_id: str) -> typing.Sequence[typing.Dict[str, typing.Any]]: - """Gets the voice regions for a given guild. + """Get the voice regions for a given guild. Parameters ---------- @@ -2308,7 +2312,7 @@ async def get_guild_voice_regions(self, guild_id: str) -> typing.Sequence[typing Returns ------- - :obj:`typing.Sequence` [ :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ]` ] + :obj:`typing.Sequence` [ :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ] ] A list of voice region objects. Raises @@ -2322,7 +2326,7 @@ async def get_guild_voice_regions(self, guild_id: str) -> typing.Sequence[typing return await self._request(route) async def get_guild_invites(self, guild_id: str) -> typing.Sequence[typing.Dict[str, typing.Any]]: - """Gets the invites for a given guild. + """Get the invites for a given guild. Parameters ---------- @@ -2331,7 +2335,7 @@ async def get_guild_invites(self, guild_id: str) -> typing.Sequence[typing.Dict[ Returns ------- - :obj:`typing.Sequence` [ :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ]` ] + :obj:`typing.Sequence` [ :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ] ] A list of invite objects (with metadata). Raises @@ -2345,7 +2349,7 @@ async def get_guild_invites(self, guild_id: str) -> typing.Sequence[typing.Dict[ return await self._request(route) async def get_guild_integrations(self, guild_id: str) -> typing.Sequence[typing.Dict[str, typing.Any]]: - """Gets the integrations for a given guild. + """Get the integrations for a given guild. Parameters ---------- @@ -2354,7 +2358,7 @@ async def get_guild_integrations(self, guild_id: str) -> typing.Sequence[typing. Returns ------- - :obj:`typing.Sequence` [ :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ]` ] + :obj:`typing.Sequence` [ :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ] ] A list of integration objects. Raises @@ -2370,7 +2374,7 @@ async def get_guild_integrations(self, guild_id: str) -> typing.Sequence[typing. async def create_guild_integration( self, guild_id: str, type_: str, integration_id: str, *, reason: str = ..., ) -> typing.Dict[str, typing.Any]: - """Creates an integrations for a given guild. + """Create an integrations for a given guild. Parameters ---------- @@ -2386,7 +2390,7 @@ async def create_guild_integration( Returns ------- - :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ]` + :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ] The newly created integration object. Raises @@ -2410,7 +2414,7 @@ async def modify_guild_integration( enable_emojis: bool = ..., reason: str = ..., ) -> None: - """Edits an integrations for a given guild. + """Edit an integrations for a given guild. Parameters ---------- @@ -2446,7 +2450,7 @@ async def modify_guild_integration( await self._request(route, json_body=payload, reason=reason) async def delete_guild_integration(self, guild_id: str, integration_id: str, *, reason: str = ...,) -> None: - """Deletes an integration for the given guild. + """Delete an integration for the given guild. Parameters ---------- @@ -2469,7 +2473,7 @@ async def delete_guild_integration(self, guild_id: str, integration_id: str, *, await self._request(route, reason=reason) async def sync_guild_integration(self, guild_id: str, integration_id: str) -> None: - """Syncs the given integration. + """Sync the given integration. Parameters ---------- @@ -2489,7 +2493,7 @@ async def sync_guild_integration(self, guild_id: str, integration_id: str) -> No await self._request(route) async def get_guild_embed(self, guild_id: str) -> typing.Dict[str, typing.Any]: - """Gets the embed for a given guild. + """Get the embed for a given guild. Parameters ---------- @@ -2498,7 +2502,7 @@ async def get_guild_embed(self, guild_id: str) -> typing.Dict[str, typing.Any]: Returns ------- - :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ]` + :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ] A guild embed object. Raises @@ -2514,13 +2518,13 @@ async def get_guild_embed(self, guild_id: str) -> typing.Dict[str, typing.Any]: async def modify_guild_embed( self, guild_id: str, embed: typing.Dict[str, typing.Any], *, reason: str = ..., ) -> typing.Dict[str, typing.Any]: - """Edits the embed for a given guild. + """Edit the embed for a given guild. Parameters ---------- guild_id : :obj:`str` The ID of the guild to edit the embed for. - embed : :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ]` + embed : :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ] The new embed object to be set. reason : :obj:`str` If specified, the audit log reason explaining why the operation @@ -2528,7 +2532,7 @@ async def modify_guild_embed( Returns ------- - :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ]` + :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ] The updated embed object. Raises @@ -2542,8 +2546,7 @@ async def modify_guild_embed( return await self._request(route, json_body=embed, reason=reason) async def get_guild_vanity_url(self, guild_id: str) -> typing.Dict[str, typing.Any]: - """ - Gets the vanity URL for a given guild. + """Get the vanity URL for a given guild. Parameters ---------- @@ -2552,7 +2555,7 @@ async def get_guild_vanity_url(self, guild_id: str) -> typing.Dict[str, typing.A Returns ------- - :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ]` + :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ] A partial invite object containing the vanity URL in the ``code`` field. Raises @@ -2593,7 +2596,7 @@ def get_guild_widget_image_url(self, guild_id: str, *, style: str = ...,) -> str return f"{self.base_url}/guilds/{guild_id}/widget.png" + query async def get_invite(self, invite_code: str, *, with_counts: bool = ...) -> typing.Dict[str, typing.Any]: - """Gets the given invite. + """Getsthe given invite. Parameters ---------- @@ -2605,7 +2608,7 @@ async def get_invite(self, invite_code: str, *, with_counts: bool = ...) -> typi Returns ------- - :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ]` + :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ] The requested invite object. Raises @@ -2619,7 +2622,7 @@ async def get_invite(self, invite_code: str, *, with_counts: bool = ...) -> typi return await self._request(route, query=query) async def delete_invite(self, invite_code: str) -> None: - """Deletes a given invite. + """Delete a given invite. Parameters ---------- @@ -2644,18 +2647,18 @@ async def delete_invite(self, invite_code: str) -> None: return await self._request(route) async def get_current_user(self) -> typing.Dict[str, typing.Any]: - """Gets the current user that is represented by token given to the client. + """Get the current user that is represented by token given to the client. Returns ------- - :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ]` + :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ] The current user object. """ route = routes.OWN_USER.compile(self.GET) return await self._request(route) async def get_user(self, user_id: str) -> typing.Dict[str, typing.Any]: - """Gets a given user. + """Get a given user. Parameters ---------- @@ -2664,7 +2667,7 @@ async def get_user(self, user_id: str) -> typing.Dict[str, typing.Any]: Returns ------- - :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ]` + :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ] The requested user object. Raises @@ -2678,7 +2681,7 @@ async def get_user(self, user_id: str) -> typing.Dict[str, typing.Any]: async def modify_current_user( self, *, username: str = ..., avatar: typing.Optional[bytes] = ..., ) -> typing.Dict[str, typing.Any]: - """Edits the current user. + """Edit the current user. Parameters ---------- @@ -2690,7 +2693,7 @@ async def modify_current_user( Returns ------- - :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ]` + :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ] The updated user object. Raises @@ -2705,15 +2708,15 @@ async def modify_current_user( return await self._request(route, json_body=payload) async def get_current_user_connections(self) -> typing.Sequence[typing.Dict[str, typing.Any]]: - """ - Gets the current user's connections. This endpoint can be - used with both ``Bearer`` and ``Bot`` tokens but will usually return an - empty list for bots (with there being some exceptions to this, like - user accounts that have been converted to bots). + """Get the current user's connections. + + This endpoint can be used with both ``Bearer`` and ``Bot`` tokens but + will usually return an empty list for bots (with there being some exceptions + to this, like user accounts that have been converted to bots). Returns ------- - :obj:`typing.Sequence` [ :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ]` ] + :obj:`typing.Sequence` [ :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ] ] A list of connection objects. """ route = routes.OWN_CONNECTIONS.compile(self.GET) @@ -2722,7 +2725,7 @@ async def get_current_user_connections(self) -> typing.Sequence[typing.Dict[str, async def get_current_user_guilds( self, *, before: str = ..., after: str = ..., limit: int = ..., ) -> typing.Sequence[typing.Dict[str, typing.Any]]: - """Gets the guilds the current user is in. + """Get the guilds the current user is in. Parameters ---------- @@ -2738,7 +2741,7 @@ async def get_current_user_guilds( Returns ------- - :obj:`typing.Sequence` [ :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ]` ] + :obj:`typing.Sequence` [ :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ] ] A list of partial guild objects. Raises @@ -2755,7 +2758,7 @@ async def get_current_user_guilds( return await self._request(route, query=query) async def leave_guild(self, guild_id: str) -> None: - """Makes the current user leave a given guild. + """Make the current user leave a given guild. Parameters ---------- @@ -2771,7 +2774,7 @@ async def leave_guild(self, guild_id: str) -> None: await self._request(route) async def create_dm(self, recipient_id: str) -> typing.Dict[str, typing.Any]: - """Creates a new DM channel with a given user. + """Create a new DM channel with a given user. Parameters ---------- @@ -2780,7 +2783,7 @@ async def create_dm(self, recipient_id: str) -> typing.Dict[str, typing.Any]: Returns ------- - :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ]` + :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ] The newly created DM channel object. Raises @@ -2797,7 +2800,7 @@ async def list_voice_regions(self) -> typing.Sequence[typing.Dict[str, typing.An Returns ------- - :obj:`typing.Sequence` [ :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ]` ] + :obj:`typing.Sequence` [ :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ] ] A list of voice regions available Note @@ -2810,8 +2813,7 @@ async def list_voice_regions(self) -> typing.Sequence[typing.Dict[str, typing.An async def create_webhook( self, channel_id: str, name: str, *, avatar: bytes = ..., reason: str = ..., ) -> typing.Dict[str, typing.Any]: - """ - Creates a webhook for a given channel. + """Create a webhook for a given channel. Parameters ---------- @@ -2827,7 +2829,7 @@ async def create_webhook( Returns ------- - :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ]` + :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ] The newly created webhook object. Raises @@ -2846,7 +2848,7 @@ async def create_webhook( return await self._request(route, json_body=payload, reason=reason) async def get_channel_webhooks(self, channel_id: str) -> typing.Sequence[typing.Dict[str, typing.Any]]: - """Gets all webhooks from a given channel. + """Get all webhooks from a given channel. Parameters ---------- @@ -2855,7 +2857,7 @@ async def get_channel_webhooks(self, channel_id: str) -> typing.Sequence[typing. Returns ------- - :obj:`typing.Sequence` [ :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ]` ] + :obj:`typing.Sequence` [ :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ] ] A list of webhook objects for the give channel. Raises @@ -2870,7 +2872,7 @@ async def get_channel_webhooks(self, channel_id: str) -> typing.Sequence[typing. return await self._request(route) async def get_guild_webhooks(self, guild_id: str) -> typing.Sequence[typing.Dict[str, typing.Any]]: - """Gets all webhooks for a given guild. + """Get all webhooks for a given guild. Parameters ---------- @@ -2879,7 +2881,7 @@ async def get_guild_webhooks(self, guild_id: str) -> typing.Sequence[typing.Dict Returns ------- - :obj:`typing.Sequence` [ :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ]` ] + :obj:`typing.Sequence` [ :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ] ] A list of webhook objects for the given guild. Raises @@ -2894,7 +2896,7 @@ async def get_guild_webhooks(self, guild_id: str) -> typing.Sequence[typing.Dict return await self._request(route) async def get_webhook(self, webhook_id: str, *, webhook_token: str = ...) -> typing.Dict[str, typing.Any]: - """Gets a given webhook. + """Get a given webhook. Parameters ---------- @@ -2905,7 +2907,7 @@ async def get_webhook(self, webhook_id: str, *, webhook_token: str = ...) -> typ Returns ------- - :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ]` + :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ] The requested webhook object. Raises @@ -2934,7 +2936,7 @@ async def modify_webhook( channel_id: str = ..., reason: str = ..., ) -> typing.Dict[str, typing.Any]: - """Edits a given webhook. + """Edit a given webhook. Parameters ---------- @@ -2956,7 +2958,7 @@ async def modify_webhook( Returns ------- - :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ]` + :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ] The updated webhook object. Raises @@ -2982,7 +2984,7 @@ async def modify_webhook( ) async def delete_webhook(self, webhook_id: str, *, webhook_token: str = ...) -> None: - """Deletes a given webhook. + """Delete a given webhook. Parameters ---------- @@ -3000,7 +3002,7 @@ async def delete_webhook(self, webhook_id: str, *, webhook_token: str = ...) -> If you either lack the ``MANAGE_WEBHOOKS`` permission or aren't a member of the guild this webhook belongs to. :obj:`hikari.net.errors.UnauthorizedHTTPError` - If you pass a token that's invalid for the target webhook. + If you pass a token that's invalid for the target webhook. """ if webhook_token is ...: route = routes.WEBHOOK.compile(self.DELETE, webhook_id=webhook_id) @@ -3043,14 +3045,14 @@ async def execute_webhook( wait : :obj:`bool` If specified, whether this request should wait for the webhook to be executed and return the resultant message object. - file : :obj:`typing.Tuple` [ :obj:`str`, :obj:`storage.FileLikeT` ] + file : :obj:`typing.Tuple` [ :obj:`str`, :obj:`hikari.internal.conversions.FileLikeT` ] If specified, a tuple of the file name and either raw :obj:`bytes` or a :obj:`io.IOBase` derived object that points to a buffer containing said file. - embeds : :obj:`typing.Sequence` [:obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ]`] + embeds : :obj:`typing.Sequence` [:obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ]] If specified, the sequence of embed objects that will be sent with this message. - allowed_mentions : :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ]` + allowed_mentions : :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ] If specified, the mentions to parse from the ``content``. If not specified, will parse all mentions from the ``content``. @@ -3073,7 +3075,7 @@ async def execute_webhook( Returns ------- - :obj:`hikari.internal.typing.Dict` [ :obj:`str`, :obj:`typing.Any` ]`, optional + :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ], optional The created message object if ``wait`` is ``True``, else ``None``. """ form = aiohttp.FormData() @@ -3117,7 +3119,7 @@ async def get_current_application_info(self) -> typing.Dict[str, typing.Any]: Returns ------- - :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ]` + :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ] An application info object. """ route = routes.OAUTH2_APPLICATIONS_ME.compile(self.GET) diff --git a/hikari/net/routes.py b/hikari/net/routes.py index 3bf862fb61..83b0653fa8 100644 --- a/hikari/net/routes.py +++ b/hikari/net/routes.py @@ -16,8 +16,7 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . -"""Provides the valid routes that can be used on the API, as well as mechanisms to aid -with rate limit bucketing.""" +"""Provides the valid routes that can be used on the API, as well as mechanisms to aid with rate limit bucketing.""" __all__ = ["CompiledRoute", "RouteTemplate"] import typing @@ -26,7 +25,7 @@ class CompiledRoute: - """A compiled representation of a route ready to be made into a full URL and to be used for a request. + """A compiled representation of a route ready to be made into a full e and to be used for a request. Parameters ---------- @@ -64,7 +63,7 @@ def __init__(self, method: str, path_template: str, path: str, major_params_hash self.hash_code = hash((path_template, major_params_hash)) def create_url(self, base_url: str) -> str: - """Creates the full URL with which you can make a request. + """Create the full URL with which you can make a request. Parameters ---------- @@ -76,7 +75,6 @@ def create_url(self, base_url: str) -> str: :obj:`str` The full URL for the route. """ - return base_url + self.compiled_path def create_real_bucket_hash(self, initial_bucket_hash: str) -> str: @@ -154,8 +152,10 @@ def __init__(self, path_template: str, major_params: typing.Collection[str] = No self.major_params = frozenset(major_params) def compile(self, method: str, /, **kwargs: typing.Any) -> CompiledRoute: - """Generate a formatted :obj:`CompiledRoute` for this route, taking into account any URL parameters that have - been passed, and extracting the :attr:major_params" for bucket hash operations accordingly. + """Generate a formatted :obj:`CompiledRoute` for this route template. + + This takes into account any URL parameters that have been passed, and extracting + the :attr:major_params" for bucket hash operations accordingly. Parameters ---------- diff --git a/hikari/net/shard.py b/hikari/net/shard.py index c65f21de9e..f594e25af4 100644 --- a/hikari/net/shard.py +++ b/hikari/net/shard.py @@ -24,7 +24,7 @@ Can be used as the main gateway connection for a single-sharded bot, or the gateway connection for a specific shard in a swarm of shards making up a larger bot. -See also +See Also -------- * IANA WS closure code standards: https://www.iana.org/assignments/websocket/websocket.xhtml * Gateway documentation: https://discordapp.com/developers/docs/topics/gateway @@ -70,16 +70,17 @@ class GatewayStatus(str, enum.Enum): #: The signature for an event dispatch callback. -DispatchT = typing.Callable[["GatewayClient", str, typing.Dict], None] +DispatchT = typing.Callable[["ShardConnection", str, typing.Dict], None] class ShardConnection: """Implementation of a client for the Discord Gateway. + This is a websocket connection to Discord that is used to inform your application of events that occur, and to allow you to change your presence, amongst other real-time applications. - Each :obj:`GatewayClient` represents a single shard. + Each :obj:`ShardConnection` represents a single shard. Expected events that may be passed to the event dispatcher are documented in the `gateway event reference `_. @@ -89,7 +90,7 @@ class ShardConnection: * ``CONNECT`` - fired on initial connection to Discord. * ``RECONNECT`` - fired if we have previously been connected to Discord - but are making a new connection on an existing :obj:`GatewayClient` instance. + but are making a new connection on an existing :obj:`ShardConnection` instance. * ``DISCONNECT`` - fired when the connection is closed for any reason. Parameters @@ -110,7 +111,7 @@ class ShardConnection: dispatch: dispatch function The function to invoke with any dispatched events. This must not be a coroutine function, and must take three arguments only. The first is - the reference to this :obj:`GatewayClient` The second is the + the reference to this :obj:`ShardConnection` The second is the event name. initial_presence: :obj:`typing.Dict`, optional A raw JSON object as a :obj:`typing.Dict` that should be set as the initial @@ -408,7 +409,7 @@ def __init__( @property def uptime(self) -> datetime.timedelta: - """The amount of time the connection has been running for. + """Amount of time the connection has been running for. Returns ------- @@ -433,7 +434,7 @@ def is_connected(self) -> bool: @property def intents(self) -> typing.Optional[codes.GatewayIntent]: - """The intents being used. + """Intents being used. If this is ``None``, no intent usage was being used on this shard. On V6 this would be regular usage as prior to @@ -449,7 +450,7 @@ def intents(self) -> typing.Optional[codes.GatewayIntent]: @property def reconnect_count(self) -> int: - """The ammount of times the gateway has reconnected since initialization. + """Amount of times the gateway has reconnected since initialization. This can be used as a debugging context, but is also used internally for exception management. @@ -457,7 +458,7 @@ def reconnect_count(self) -> int: Returns ------- :obj:`int` - The ammount of times the gateway has reconnected since initialization. + The amount of times the gateway has reconnected since initialization. """ # 0 disconnects + not is_connected => 0 # 0 disconnects + is_connected => 0 @@ -467,9 +468,10 @@ def reconnect_count(self) -> int: # 2 disconnects + is_connected = 2 return max(0, self.disconnect_count - int(not self.is_connected)) + # Ignore docstring not starting in an imperative mood @property - def current_presence(self) -> typing.Dict: - """The current presence of the gateway. + def current_presence(self) -> typing.Dict: # noqa: D401 + """Current presence for the gateway. Returns ------- @@ -481,18 +483,14 @@ def current_presence(self) -> typing.Dict: @typing.overload async def request_guild_members(self, guild_id: str, *guild_ids: str, limit: int = 0, query: str = "") -> None: - """Request guild members in the given guilds using a query string and - an optional limit. - """ + """Request guild members in the given guilds using a query string and an optional limit.""" @typing.overload - async def request_guild_members(self, guild_id: str, *guild_ids: str, user_ids: typing.Collection[str]) -> None: - """Request guild members in the given guilds using a set of user IDs - to resolve. - """ + async def request_guild_members(self, guild_id: str, *guild_ids: str, user_ids: typing.Sequence[str]) -> None: + """Request guild members in the given guilds using a set of user IDs to resolve.""" async def request_guild_members(self, guild_id, *guild_ids, **kwargs): - """Requests the guild members for a guild or set of guilds. + """Request the guild members for a guild or set of guilds. These guilds must be being served by this shard, and the results will be provided to the dispatcher with ``GUILD_MEMBER_CHUNK`` events. @@ -503,7 +501,7 @@ async def request_guild_members(self, guild_id, *guild_ids, **kwargs): The first guild to request members for. *guild_ids : :obj:`str` Additional guilds to request members for. - **kwargs : + **kwargs Optional arguments. Keyword Args @@ -515,16 +513,15 @@ async def request_guild_members(self, guild_id, *guild_ids, **kwargs): An optional string to filter members with. If specified, only members who have a username starting with this string will be returned. - user_ids : :obj:`typing.Itinerable` [ :obj:`str` ] - An optional list of user IDs to return member info about. + user_ids : :obj:`typing.Sequence` [ :obj:`str` ] + An optional list of user IDs to return member info about. Note ---- You may not specify ``user_ids`` at the same time as ``limit`` and ``query``. Likewise, if you specify one of ``limit`` or ``query``, the other must also be included. The default, if no optional arguments - are specified, is to use a ``limit`` of ``0`` and a ``query`` of - ``""`` (empty-string). + are specified, is to use a ``limit = 0`` and a ``query = ""`` (empty-string). """ guilds = [guild_id, *guild_ids] constraints = {} @@ -580,8 +577,7 @@ async def close(self, close_code: int = 1000) -> None: self.closed_event.set() async def connect(self, client_session_type=aiohttp.ClientSession) -> None: - """Connect to the gateway and return when it closes (usually with some - form of exception documented in :mod:`hikari.net.errors`). + """Connect to the gateway and return when it closes. Parameters ---------- diff --git a/hikari/net/user_agent.py b/hikari/net/user_agent.py index 84aa65153e..b320c0b5dd 100644 --- a/hikari/net/user_agent.py +++ b/hikari/net/user_agent.py @@ -41,22 +41,38 @@ class UserAgent(metaclass=meta.SingletonMeta): #: The version of the library. #: + #: Example + #: ------- #: ``"hikari 1.0.1"`` + #: + #: :type: :obj:`typing.Final` [ :obj:`str` ] library_version: typing.Final[str] #: The platform version. #: + #: Example + #: ------- #: ``"CPython 3.8.2 GCC 9.2.0"`` + #: + #: :type: :obj:`typing.Final` [ :obj:`str` ] platform_version: typing.Final[str] #: The operating system type. #: + #: Example + #: ------- #: ``"Linux-5.4.15-2-MANJARO-x86_64-with-glibc2.2.5"`` + #: + #: :type: :obj:`typing.Final` [ :obj:`str` ] system_type: typing.Final[str] #: The Hikari-specific user-agent to use in HTTP connections to Discord. #: + #: Example + #: ------- #: ``"DiscordBot (https://gitlab.com/nekokatt/hikari; 1.0.1; Nekokatt) CPython 3.8.2 GCC 9.2.0 Linux"`` + #: + #: :type: :obj:`typing.Final` [ :obj:`str` ] user_agent: typing.Final[str] def __init__(self): @@ -80,12 +96,14 @@ def __attr__(_): def _join_strip(*args): return " ".join((arg.strip() for arg in args if arg.strip())) + # Ignore docstring not starting in an imperative mood @property - def websocket_triplet(self) -> typing.Dict[str, str]: - """ + def websocket_triplet(self) -> typing.Dict[str, str]: # noqa: D401 + """A dict representing device and library info. + Returns ------- - :obj:`typing.Dict` [ :obj:`str`, :obj:`str` ]: + :obj:`typing.Dict` [ :obj:`str`, :obj:`str` ] The object to send to Discord representing device info when IDENTIFYing with the gateway. """ diff --git a/noxfile.py b/noxfile.py index 29cc6054aa..1836699ffd 100644 --- a/noxfile.py +++ b/noxfile.py @@ -128,15 +128,14 @@ def format(session) -> None: @nox.session(reuse_venv=True) def docstyle(session) -> None: - """Reformat code with Black. Pass the '--check' flag to check formatting only.""" + """Check docstrings with pydocstyle.""" session.install("pydocstyle") session.chdir(MAIN_PACKAGE_PATH) # add -e flag for explainations. - with contextlib.suppress(Exception): # TODO: remove this once these are being fixed. - session.run("pydocstyle", "--config=../pydocstyle.ini") + session.run("pydocstyle", "--config=../pydocstyle.ini") -@nox.session(reuse_venv=True,) +@nox.session(reuse_venv=True) def lint(session) -> None: """Check formating with pylint""" session.install("-r", "requirements.txt") diff --git a/pydocstyle.ini b/pydocstyle.ini index bb2b6db2e8..819e2dad1b 100644 --- a/pydocstyle.ini +++ b/pydocstyle.ini @@ -1,3 +1,4 @@ [pydocstyle] convention=numpy -ignore= +add-ignore=D105, # Magic methods not having a docstring + D102, # Missing docstring in public method diff --git a/pylint.ini b/pylint.ini index e0083bb4dc..ee8b5c7743 100644 --- a/pylint.ini +++ b/pylint.ini @@ -438,7 +438,7 @@ redefining-builtins-modules=six.moves,past.builtins,future.builtins,builtins,io expected-line-ending-format=LF # Regexp for a line that is allowed to be longer than the limit. -ignore-long-lines=^\s*(# )??$ +ignore-long-lines=^\s*?(#(:\s+:type:|:?\s+https?://).*?) # Number of spaces of indent required inside a hanging or continued line. indent-after-paren=0 diff --git a/tests/hikari/core/test_oauth2.py b/tests/hikari/core/test_oauth2.py index 1d0f9bd70e..fde5c9e294 100644 --- a/tests/hikari/core/test_oauth2.py +++ b/tests/hikari/core/test_oauth2.py @@ -128,10 +128,10 @@ def test_deserialize(self, own_guild_payload): assert own_guild_obj.my_permissions == 2147483647 -class TestOwner: +class TestApplicationOwner: @pytest.fixture() def owner_obj(self, owner_payload): - return oauth2.Owner.deserialize(owner_payload) + return oauth2.ApplicationOwner.deserialize(owner_payload) def test_deserialize(self, owner_obj): assert owner_obj.username == "agent 47" @@ -206,12 +206,12 @@ def application_obj(self, application_information_payload): def test_deserialize(self, application_information_payload, team_payload, owner_payload): mock_team = mock.MagicMock(oauth2.Team) - mock_owner = mock.MagicMock(oauth2.Owner) + mock_owner = mock.MagicMock(oauth2.ApplicationOwner) with _helpers.patch_marshal_attr( oauth2.Application, "team", deserializer=oauth2.Team.deserialize, return_value=mock_team ) as patched_team_deserializer: with _helpers.patch_marshal_attr( - oauth2.Application, "owner", deserializer=oauth2.Owner.deserialize, return_value=mock_owner + oauth2.Application, "owner", deserializer=oauth2.ApplicationOwner.deserialize, return_value=mock_owner ) as patched_owner_deserializer: application_obj = oauth2.Application.deserialize(application_information_payload) patched_owner_deserializer.assert_called_once_with(owner_payload) From 0da6037c374ce3ce62f07896bf0c74a13b9a6e37 Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Sat, 4 Apr 2020 20:11:51 +0100 Subject: [PATCH 061/922] Reshuffled for the last time to flatten the repository out, and rejigged states again. --- coverage.ini | 3 +- docs/conf.py | 3 +- docs/index.rst | 9 +- hikari/__init__.py | 73 +++- hikari/{core => }/audit_logs.py | 44 +- hikari/{core => }/channels.py | 38 +- hikari/{core => }/clients/__init__.py | 36 +- hikari/{core => }/clients/app_config.py | 10 +- hikari/{core => }/clients/bot_client.py | 0 hikari/{core => }/clients/gateway_client.py | 66 +-- hikari/{core => }/clients/gateway_config.py | 96 +---- .../gateway_runner.py} | 67 ++-- hikari/{core => }/clients/http_client.py | 0 hikari/{core => }/clients/http_config.py | 6 +- hikari/{core => }/clients/protocol_config.py | 8 +- hikari/{core => }/clients/shard_client.py | 111 +----- hikari/clients/shard_config.py | 97 +++++ hikari/clients/websocket_client.py | 87 ++++ hikari/{core => }/colors.py | 2 +- hikari/{core => }/colours.py | 10 +- hikari/core/__init__.py | 70 ---- hikari/core/state/base_state.py | 121 ------ hikari/core/state/default_state.py | 38 -- hikari/{core => }/embeds.py | 4 +- hikari/{core => }/emojis.py | 10 +- hikari/{core => }/entities.py | 0 hikari/errors.py | 344 +++++++++++++++- hikari/{core => }/events.py | 215 +++++----- hikari/{core => }/gateway_entities.py | 6 +- hikari/{core => }/guilds.py | 82 ++-- hikari/internal/conversions.py | 16 +- hikari/internal/marshaller.py | 6 +- hikari/{core => }/invites.py | 20 +- hikari/{core => }/messages.py | 42 +- hikari/net/__init__.py | 3 +- hikari/net/errors.py | 377 ------------------ hikari/net/rest.py | 354 ++++++++-------- hikari/net/shard.py | 2 +- hikari/{core => }/oauth2.py | 28 +- hikari/{core => }/permissions.py | 0 hikari/{core => }/snowflakes.py | 2 +- hikari/state/__init__.py | 35 ++ .../event_dispatcher.py} | 33 +- hikari/state/event_manager.py | 169 ++++++++ hikari/state/raw_event_consumer.py | 58 +++ hikari/state/stateless_event_manager_impl.py | 31 ++ hikari/{core => }/users.py | 4 +- hikari/{core => }/voices.py | 14 +- hikari/{core => }/webhooks.py | 12 +- insomnia/v7.yaml | 146 +++---- pylint.ini | 4 +- tests/hikari/{core => clients}/__init__.py | 0 .../{_internal => internal}/__init__.py | 0 .../test_assertions.py | 0 .../{_internal => internal}/test_cdn.py | 0 .../test_conversions.py | 0 .../test_marshaller.py | 0 .../test_marshaller_pep563.py | 0 .../{_internal => internal}/test_meta.py | 0 .../test_more_asyncio.py | 0 .../test_more_collections.py | 0 .../test_more_logging.py | 0 tests/hikari/net/test_errors.py | 2 +- tests/hikari/net/test_rest.py | 2 +- tests/hikari/net/test_shard.py | 2 +- .../core => tests/hikari}/state/__init__.py | 6 - .../test_event_dispatcher.py} | 6 +- tests/hikari/{core => }/test_audit_logs.py | 10 +- tests/hikari/{core => }/test_channels.py | 6 +- tests/hikari/{core => }/test_colors.py | 2 +- tests/hikari/{core => }/test_colours.py | 3 +- tests/hikari/{core => }/test_embeds.py | 3 +- tests/hikari/{core => }/test_emojis.py | 4 +- tests/hikari/{core => }/test_entities.py | 2 +- tests/hikari/{core => }/test_events.py | 20 +- .../{core => }/test_gateway_entities.py | 2 +- tests/hikari/{core => }/test_guilds.py | 10 +- tests/hikari/{core => }/test_invites.py | 8 +- tests/hikari/{core => }/test_messages.py | 9 +- tests/hikari/{core => }/test_oauth2.py | 6 +- tests/hikari/{core => }/test_snowflake.py | 2 +- tests/hikari/{core => }/test_users.py | 2 +- tests/hikari/{core => }/test_voices.py | 2 +- tests/hikari/{core => }/test_webhook.py | 4 +- 84 files changed, 1616 insertions(+), 1509 deletions(-) rename hikari/{core => }/audit_logs.py (93%) rename hikari/{core => }/channels.py (90%) rename hikari/{core => }/clients/__init__.py (55%) rename hikari/{core => }/clients/app_config.py (95%) rename hikari/{core => }/clients/bot_client.py (100%) rename hikari/{core => }/clients/gateway_client.py (58%) rename hikari/{core => }/clients/gateway_config.py (63%) rename hikari/{core/clients/_run_gateway.py => clients/gateway_runner.py} (56%) rename hikari/{core => }/clients/http_client.py (100%) rename hikari/{core => }/clients/http_config.py (93%) rename hikari/{core => }/clients/protocol_config.py (95%) rename hikari/{core => }/clients/shard_client.py (79%) create mode 100644 hikari/clients/shard_config.py create mode 100644 hikari/clients/websocket_client.py rename hikari/{core => }/colors.py (99%) rename hikari/{core => }/colours.py (80%) delete mode 100644 hikari/core/__init__.py delete mode 100644 hikari/core/state/base_state.py delete mode 100644 hikari/core/state/default_state.py rename hikari/{core => }/embeds.py (99%) rename hikari/{core => }/emojis.py (93%) rename hikari/{core => }/entities.py (100%) rename hikari/{core => }/events.py (84%) rename hikari/{core => }/gateway_entities.py (96%) rename hikari/{core => }/guilds.py (94%) rename hikari/{core => }/invites.py (94%) rename hikari/{core => }/messages.py (90%) delete mode 100644 hikari/net/errors.py rename hikari/{core => }/oauth2.py (94%) rename hikari/{core => }/permissions.py (100%) rename hikari/{core => }/snowflakes.py (99%) create mode 100644 hikari/state/__init__.py rename hikari/{core/state/dispatcher.py => state/event_dispatcher.py} (93%) create mode 100644 hikari/state/event_manager.py create mode 100644 hikari/state/raw_event_consumer.py create mode 100644 hikari/state/stateless_event_manager_impl.py rename hikari/{core => }/users.py (98%) rename hikari/{core => }/voices.py (92%) rename hikari/{core => }/webhooks.py (91%) rename tests/hikari/{core => clients}/__init__.py (100%) rename tests/hikari/{_internal => internal}/__init__.py (100%) rename tests/hikari/{_internal => internal}/test_assertions.py (100%) rename tests/hikari/{_internal => internal}/test_cdn.py (100%) rename tests/hikari/{_internal => internal}/test_conversions.py (100%) rename tests/hikari/{_internal => internal}/test_marshaller.py (100%) rename tests/hikari/{_internal => internal}/test_marshaller_pep563.py (100%) rename tests/hikari/{_internal => internal}/test_meta.py (100%) rename tests/hikari/{_internal => internal}/test_more_asyncio.py (100%) rename tests/hikari/{_internal => internal}/test_more_collections.py (100%) rename tests/hikari/{_internal => internal}/test_more_logging.py (100%) rename {hikari/core => tests/hikari}/state/__init__.py (89%) rename tests/hikari/{core/test_dispatcher.py => state/test_event_dispatcher.py} (98%) rename tests/hikari/{core => }/test_audit_logs.py (99%) rename tests/hikari/{core => }/test_channels.py (99%) rename tests/hikari/{core => }/test_colors.py (99%) rename tests/hikari/{core => }/test_colours.py (93%) rename tests/hikari/{core => }/test_embeds.py (99%) rename tests/hikari/{core => }/test_emojis.py (97%) rename tests/hikari/{core => }/test_entities.py (97%) rename tests/hikari/{core => }/test_events.py (99%) rename tests/hikari/{core => }/test_gateway_entities.py (98%) rename tests/hikari/{core => }/test_guilds.py (99%) rename tests/hikari/{core => }/test_invites.py (98%) rename tests/hikari/{core => }/test_messages.py (98%) rename tests/hikari/{core => }/test_oauth2.py (99%) rename tests/hikari/{core => }/test_snowflake.py (98%) rename tests/hikari/{core => }/test_users.py (99%) rename tests/hikari/{core => }/test_voices.py (98%) rename tests/hikari/{core => }/test_webhook.py (97%) diff --git a/coverage.ini b/coverage.ini index 8fdf800763..df9aab851c 100644 --- a/coverage.ini +++ b/coverage.ini @@ -6,7 +6,8 @@ branch = true # timid = true omit = hikari/__main__.py - hikari/core/clients/_run_gateway.py + hikari/_about.py + hikari/clients/gateway_runner.py [report] precision = 2 diff --git a/docs/conf.py b/docs/conf.py index 89af6d191f..66567f57fd 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -177,7 +177,8 @@ intersphinx_mapping = { "python": ("https://docs.python.org/3", None), "aiohttp": ("https://aiohttp.readthedocs.io/en/stable/", None), - "websockets": ("https://websockets.readthedocs.io/en/stable/", None), + "attrs": ("https://www.attrs.org/en/stable/", None), + "click": ("https://click.palletsprojects.com/en/7.x/", None), } # -- Inheritance diagram options... ------------------------------------------------- diff --git a/docs/index.rst b/docs/index.rst index a829680ca4..2523984ad6 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -26,16 +26,10 @@ Technical documentation hikari -Internal components -................... - -|internal| - .. toctree:: - :maxdepth: 2 :titlesonly: - hikari.internal + modules Other resources --------------- @@ -50,3 +44,4 @@ Search for a topic * :ref:`genindex` * :ref:`search` +* :ref:`modindex` diff --git a/hikari/__init__.py b/hikari/__init__.py index 64f9398fc3..bb0fa18ebe 100644 --- a/hikari/__init__.py +++ b/hikari/__init__.py @@ -16,14 +16,79 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . -"""Hikari's core framework for writing Discord bots in Python.""" -from hikari import errors -from hikari import net +"""Hikari's models framework for writing Discord bots in Python.""" from hikari._about import __author__ from hikari._about import __copyright__ from hikari._about import __email__ from hikari._about import __license__ from hikari._about import __url__ from hikari._about import __version__ -from hikari.errors import * +from hikari import audit_logs +from hikari import channels +from hikari import clients +from hikari import colors +from hikari import colours +from hikari import embeds +from hikari import emojis +from hikari import entities +from hikari import errors +from hikari import events +from hikari import gateway_entities +from hikari import guilds +from hikari import invites +from hikari import messages +from hikari import net +from hikari import oauth2 +from hikari import permissions +from hikari import snowflakes +from hikari import state +from hikari import users +from hikari import voices +from hikari import webhooks +from hikari.audit_logs import * +from hikari.channels import * +from hikari.clients import * +from hikari.colors import * +from hikari.colours import * +from hikari.embeds import * +from hikari.emojis import * +from hikari.entities import * +from hikari.events import * +from hikari.gateway_entities import * +from hikari.guilds import * +from hikari.invites import * +from hikari.messages import * from hikari.net import * +from hikari.oauth2 import * +from hikari.permissions import * +from hikari.snowflakes import * +from hikari.state import * +from hikari.users import * +from hikari.voices import * +from hikari.webhooks import * + +# Import everything into this namespace. + +__all__ = [ + *audit_logs.__all__, + *channels.__all__, + *clients.__all__, + *colors.__all__, + *colours.__all__, + *embeds.__all__, + *emojis.__all__, + *entities.__all__, + *events.__all__, + *gateway_entities.__all__, + *guilds.__all__, + *invites.__all__, + *messages.__all__, + *net.__all__, + *oauth2.__all__, + *permissions.__all__, + *snowflakes.__all__, + *state.__all__, + *users.__all__, + *voices.__all__, + *webhooks.__all__, +] diff --git a/hikari/core/audit_logs.py b/hikari/audit_logs.py similarity index 93% rename from hikari/core/audit_logs.py rename to hikari/audit_logs.py index f7e9efe54f..fe15a49d26 100644 --- a/hikari/core/audit_logs.py +++ b/hikari/audit_logs.py @@ -19,7 +19,7 @@ """Components and entities that are used to describe audit logs on Discord. .. inheritance-diagram:: - hikari.core.audit_logs + hikari.audit_logs :parts: 1 """ __all__ = [ @@ -44,14 +44,14 @@ import enum import typing -from hikari.core import channels -from hikari.core import colors -from hikari.core import entities -from hikari.core import guilds -from hikari.core import permissions -from hikari.core import snowflakes -from hikari.core import users as _users -from hikari.core import webhooks as _webhooks +from hikari import channels +from hikari import colors +from hikari import entities +from hikari import guilds +from hikari import permissions +from hikari import snowflakes +from hikari import users as _users +from hikari import webhooks as _webhooks from hikari.internal import conversions from hikari.internal import marshaller from hikari.internal import more_collections @@ -287,12 +287,12 @@ class ChannelOverwriteEntryInfo(BaseAuditLogEntryInfo): #: The ID of the overwrite being updated, added or removed (and the entity #: it targets). #: - #: :type: :obj:`hikari.core.snowflakes.Snowflake` + #: :type: :obj:`hikari.snowflakes.Snowflake` id: snowflakes.Snowflake = marshaller.attrib(deserializer=snowflakes.Snowflake.deserialize) #: The type of entity this overwrite targets. #: - #: :type: :obj:`hikari.core.channels.PermissionOverwriteType` + #: :type: :obj:`hikari.channels.PermissionOverwriteType` type: channels.PermissionOverwriteType = marshaller.attrib(deserializer=channels.PermissionOverwriteType) #: The name of the role this overwrite targets, if it targets a role. @@ -312,12 +312,12 @@ class MessagePinEntryInfo(BaseAuditLogEntryInfo): #: The ID of the guild text based channel where this pinned message is #: being added or removed. #: - #: :type: :obj:`hikari.core.snowflakes.Snowflake` + #: :type: :obj:`hikari.snowflakes.Snowflake` channel_id: snowflakes.Snowflake = marshaller.attrib(deserializer=snowflakes.Snowflake.deserialize) #: The ID of the message that's being pinned or unpinned. #: - #: :type: :obj:`hikari.core.snowflakes.Snowflake` + #: :type: :obj:`hikari.snowflakes.Snowflake` message_id: snowflakes.Snowflake = marshaller.attrib(deserializer=snowflakes.Snowflake.deserialize) @@ -358,7 +358,7 @@ class MessageDeleteEntryInfo(MessageBulkDeleteEntryInfo): #: The guild text based channel where these message(s) were deleted. #: - #: :type: :obj:`hikari.core.snowflakes.Snowflake` + #: :type: :obj:`hikari.snowflakes.Snowflake` channel_id: snowflakes.Snowflake = marshaller.attrib(deserializer=snowflakes.Snowflake.deserialize) @@ -380,7 +380,7 @@ class MemberMoveEntryInfo(MemberDisconnectEntryInfo): #: The channel these member(s) were moved to. #: - #: :type: :obj:`hikari.core.snowflakes.Snowflake` + #: :type: :obj:`hikari.snowflakes.Snowflake` channel_id: snowflakes.Snowflake = marshaller.attrib(deserializer=snowflakes.Snowflake.deserialize) @@ -418,7 +418,7 @@ class AuditLogEntry(snowflakes.UniqueEntity, entities.Deserializable): #: The ID of the entity affected by this change, if applicable. #: - #: :type: :obj:`hikari.core.snowflakes.Snowflake`, optional + #: :type: :obj:`hikari.snowflakes.Snowflake`, optional target_id: typing.Optional[snowflakes.Snowflake] = marshaller.attrib() #: A sequence of the changes made to :attr:`target_id` @@ -428,12 +428,12 @@ class AuditLogEntry(snowflakes.UniqueEntity, entities.Deserializable): #: The ID of the user who made this change. #: - #: :type: :obj:`hikari.core.snowflakes.Snowflake` + #: :type: :obj:`hikari.snowflakes.Snowflake` user_id: snowflakes.Snowflake = marshaller.attrib() #: The ID of this entry. #: - #: :type: :obj:`hikari.core.snowflakes.Snowflake` + #: :type: :obj:`hikari.snowflakes.Snowflake` id: snowflakes.Snowflake = marshaller.attrib() #: The type of action this entry represents. @@ -483,7 +483,7 @@ class AuditLog(entities.HikariEntity, entities.Deserializable): #: A sequence of the audit log's entries. #: - #: :type: :obj:`typing.Mapping` [ :obj:`hikari.core.snowflakes.Snowflake`, :obj:`AuditLogEntry` ] + #: :type: :obj:`typing.Mapping` [ :obj:`hikari.snowflakes.Snowflake`, :obj:`AuditLogEntry` ] entries: typing.Mapping[snowflakes.Snowflake, AuditLogEntry] = marshaller.attrib( raw_name="audit_log_entries", deserializer=lambda payload: {entry.id: entry for entry in map(AuditLogEntry.deserialize, payload)}, @@ -491,7 +491,7 @@ class AuditLog(entities.HikariEntity, entities.Deserializable): #: A mapping of the partial objects of integrations found in this audit log. #: - #: :type: :obj:`typing.Mapping` [ :obj:`hikari.core.snowflakes.Snowflake`, :obj:`hikari.core.guilds.GuildIntegration` ] + #: :type: :obj:`typing.Mapping` [ :obj:`hikari.snowflakes.Snowflake`, :obj:`hikari.guilds.GuildIntegration` ] integrations: typing.Mapping[snowflakes.Snowflake, guilds.GuildIntegration] = marshaller.attrib( deserializer=lambda payload: { integration.id: integration for integration in map(guilds.PartialGuildIntegration.deserialize, payload) @@ -500,14 +500,14 @@ class AuditLog(entities.HikariEntity, entities.Deserializable): #: A mapping of the objects of users found in this audit log. #: - #: :type: :obj:`typing.Mapping` [ :obj:`hikari.core.snowflakes.Snowflake`, :obj:`hikari.core.users.User` ] + #: :type: :obj:`typing.Mapping` [ :obj:`hikari.snowflakes.Snowflake`, :obj:`hikari.users.User` ] users: typing.Mapping[snowflakes.Snowflake, _users.User] = marshaller.attrib( deserializer=lambda payload: {user.id: user for user in map(_users.User.deserialize, payload)} ) #: A mapping of the objects of webhooks found in this audit log. #: - #: :type: :obj:`typing.Mapping` [ :obj:`hikari.core.snowflakes.Snowflake`, :obj:`hikari.core.webhooks.Webhook` ] + #: :type: :obj:`typing.Mapping` [ :obj:`hikari.snowflakes.Snowflake`, :obj:`hikari.webhooks.Webhook` ] webhooks: typing.Mapping[snowflakes.Snowflake, _webhooks.Webhook] = marshaller.attrib( deserializer=lambda payload: {webhook.id: webhook for webhook in map(_webhooks.Webhook.deserialize, payload)} ) diff --git a/hikari/core/channels.py b/hikari/channels.py similarity index 90% rename from hikari/core/channels.py rename to hikari/channels.py index 354db94703..c624323526 100644 --- a/hikari/core/channels.py +++ b/hikari/channels.py @@ -19,11 +19,11 @@ """Components and entities that are used to describe both DMs and guild channels on Discord. .. inheritance-diagram:: - hikari.core.channels + hikari.channels enum.IntEnum - hikari.core.entities.HikariEntity - hikari.core.entities.Deserializable - hikari.core.entities.Serializable + hikari.entities.HikariEntity + hikari.entities.Deserializable + hikari.entities.Serializable :parts: 1 """ @@ -47,10 +47,10 @@ import typing from hikari.internal import marshaller -from hikari.core import entities -from hikari.core import permissions -from hikari.core import users -from hikari.core import snowflakes +from hikari import entities +from hikari import permissions +from hikari import users +from hikari import snowflakes @enum.unique @@ -95,12 +95,12 @@ class PermissionOverwrite(snowflakes.UniqueEntity, entities.Deserializable, enti #: The permissions this overwrite allows. #: - #: :type: :obj:`hikari.core.permissions.Permission` + #: :type: :obj:`hikari.permissions.Permission` allow: permissions.Permission = marshaller.attrib(deserializer=permissions.Permission) #: The permissions this overwrite denies. #: - #: :type: :obj:`hikari.core.permissions.Permission` + #: :type: :obj:`hikari.permissions.Permission` deny: permissions.Permission = marshaller.attrib(deserializer=permissions.Permission) @property @@ -169,14 +169,14 @@ class DMChannel(Channel): #: This might point to an invalid or deleted message. #: #: - #: :type: :obj:`hikari.core.snowflakes.Snowflake`, optional + #: :type: :obj:`hikari.snowflakes.Snowflake`, optional last_message_id: snowflakes.Snowflake = marshaller.attrib( deserializer=snowflakes.Snowflake.deserialize, if_none=None ) #: The recipients of the DM. #: - #: :type: :obj:`typing.Mapping` [ :obj:`hikari.core.snowflakes.Snowflake`, :obj:`hikari.core.users.User` ] + #: :type: :obj:`typing.Mapping` [ :obj:`hikari.snowflakes.Snowflake`, :obj:`hikari.users.User` ] recipients: typing.Mapping[snowflakes.Snowflake, users.User] = marshaller.attrib( deserializer=lambda recipients: {user.id: user for user in map(users.User.deserialize, recipients)} ) @@ -194,7 +194,7 @@ class GroupDMChannel(DMChannel): #: The ID of the owner of the group. #: - #: :type: :obj:`hikari.core.snowflakes.Snowflake` + #: :type: :obj:`hikari.snowflakes.Snowflake` owner_id: snowflakes.Snowflake = marshaller.attrib(deserializer=snowflakes.Snowflake.deserialize) #: The hash of the icon of the group. @@ -205,7 +205,7 @@ class GroupDMChannel(DMChannel): #: The ID of the application that created the group DM, if it's a #: bot based group DM. #: - #: :type: :obj:`hikari.core.snowflakes.Snowflake`, optional + #: :type: :obj:`hikari.snowflakes.Snowflake`, optional application_id: typing.Optional[snowflakes.Snowflake] = marshaller.attrib( deserializer=snowflakes.Snowflake.deserialize, if_undefined=None ) @@ -217,7 +217,7 @@ class GuildChannel(Channel): #: The ID of the guild the channel belongs to. #: - #: :type: :obj:`hikari.core.snowflakes.Snowflake` + #: :type: :obj:`hikari.snowflakes.Snowflake` guild_id: snowflakes.Snowflake = marshaller.attrib(deserializer=snowflakes.Snowflake.deserialize) #: The sorting position of the channel. @@ -227,7 +227,7 @@ class GuildChannel(Channel): #: The permission overwrites for the channel. #: - #: :type: :obj:`typing.Mapping` [ :obj:`hikari.core.snowflakes.Snowflake`, :obj:`PermissionOverwrite` ] + #: :type: :obj:`typing.Mapping` [ :obj:`hikari.snowflakes.Snowflake`, :obj:`PermissionOverwrite` ] permission_overwrites: PermissionOverwrite = marshaller.attrib( deserializer=lambda overwrites: {o.id: o for o in map(PermissionOverwrite.deserialize, overwrites)} ) @@ -244,7 +244,7 @@ class GuildChannel(Channel): #: The ID of the parent category the channel belongs to. #: - #: :type: :obj:`hikari.core.snowflakes.Snowflake`, optional + #: :type: :obj:`hikari.snowflakes.Snowflake`, optional parent_id: snowflakes.Snowflake = marshaller.attrib(deserializer=snowflakes.Snowflake.deserialize, if_none=None) @@ -271,7 +271,7 @@ class GuildTextChannel(GuildChannel): #: This might point to an invalid or deleted message. #: #: - #: :type: :obj:`hikari.core.snowflakes.Snowflake`, optional + #: :type: :obj:`hikari.snowflakes.Snowflake`, optional last_message_id: snowflakes.Snowflake = marshaller.attrib( deserializer=snowflakes.Snowflake.deserialize, if_none=None ) @@ -307,7 +307,7 @@ class GuildNewsChannel(GuildChannel): #: This might point to an invalid or deleted message. #: #: - #: :type: :obj:`hikari.core.snowflakes.Snowflake`, optional + #: :type: :obj:`hikari.snowflakes.Snowflake`, optional last_message_id: snowflakes.Snowflake = marshaller.attrib( deserializer=snowflakes.Snowflake.deserialize, if_none=None ) diff --git a/hikari/core/clients/__init__.py b/hikari/clients/__init__.py similarity index 55% rename from hikari/core/clients/__init__.py rename to hikari/clients/__init__.py index b5754ba30e..1e74f58720 100644 --- a/hikari/core/clients/__init__.py +++ b/hikari/clients/__init__.py @@ -16,25 +16,26 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . -"""The core API for interacting with Discord directly.""" +"""The models API for interacting with Discord directly.""" -from hikari.core.clients import app_config -from hikari.core.clients import bot_client -from hikari.core.clients import gateway_client -from hikari.core.clients import gateway_config -from hikari.core.clients import http_client -from hikari.core.clients import http_config -from hikari.core.clients import protocol_config -from hikari.core.clients import shard_client +from hikari.clients import app_config +from hikari.clients import bot_client +from hikari.clients import gateway_client +from hikari.clients import gateway_config +from hikari.clients import http_client +from hikari.clients import http_config +from hikari.clients import protocol_config +from hikari.clients import websocket_client -from hikari.core.clients.app_config import * -from hikari.core.clients.bot_client import * -from hikari.core.clients.gateway_client import * -from hikari.core.clients.gateway_config import * -from hikari.core.clients.http_client import * -from hikari.core.clients.http_config import * -from hikari.core.clients.protocol_config import * -from hikari.core.clients.shard_client import * +from hikari.clients.app_config import * +from hikari.clients.bot_client import * +from hikari.clients.gateway_client import * +from hikari.clients.gateway_config import * +from hikari.clients.http_client import * +from hikari.clients.http_config import * +from hikari.clients.protocol_config import * +from hikari.clients.shard_client import * +from hikari.clients.websocket_client import * __all__ = [ @@ -46,4 +47,5 @@ *http_config.__all__, *protocol_config.__all__, *shard_client.__all__, + *websocket_client.__all__, ] diff --git a/hikari/core/clients/app_config.py b/hikari/clients/app_config.py similarity index 95% rename from hikari/core/clients/app_config.py rename to hikari/clients/app_config.py index 7de2ded62f..0130ca280d 100644 --- a/hikari/core/clients/app_config.py +++ b/hikari/clients/app_config.py @@ -22,9 +22,9 @@ import typing from hikari.internal import marshaller -from hikari.core import entities -from hikari.core.clients import gateway_config -from hikari.core.clients import http_config +from hikari import entities +from hikari.clients import gateway_config +from hikari.clients import http_config @marshaller.attrs(kw_only=True) @@ -130,7 +130,7 @@ class AppConfig(entities.HikariEntity, entities.Deserializable): #: #: If unspecified or ``None```, then this will be a set of default values. #: - #: :type: :obj:`hikari.core.clients.http_config.HTTPConfig`, optional + #: :type: :obj:`hikari.clients.http_config.HTTPConfig`, optional http: typing.Optional[http_config.HTTPConfig] = marshaller.attrib( deserializer=http_config.HTTPConfig.deserialize, if_none=None, if_undefined=None, default=None ) @@ -139,7 +139,7 @@ class AppConfig(entities.HikariEntity, entities.Deserializable): #: #: If unspecified or ``None```, then this will be a set of default values. #: - #: :type: :obj:`hikari.core.clients.gateway_config.GatewayConfig`, optional + #: :type: :obj:`hikari.clients.gateway_config.GatewayConfig`, optional gateway: typing.Optional[gateway_config.GatewayConfig] = marshaller.attrib( deserializer=gateway_config.GatewayConfig.deserialize, if_none=None, if_undefined=None, default=None ) diff --git a/hikari/core/clients/bot_client.py b/hikari/clients/bot_client.py similarity index 100% rename from hikari/core/clients/bot_client.py rename to hikari/clients/bot_client.py diff --git a/hikari/core/clients/gateway_client.py b/hikari/clients/gateway_client.py similarity index 58% rename from hikari/core/clients/gateway_client.py rename to hikari/clients/gateway_client.py index a599d7155e..daf2f3734d 100644 --- a/hikari/core/clients/gateway_client.py +++ b/hikari/clients/gateway_client.py @@ -16,32 +16,33 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . -"""Defines a facade around :obj:`hikari.core.clients.shard_client.ShardClient`. +"""Defines a facade around :obj:`hikari.clients.shard_client.ShardClient`. This provides functionality such as keeping multiple shards alive -simultaneously. """ + __all__ = ["GatewayClient"] import asyncio import time import typing -from hikari.core.state import base_state +from hikari.clients import websocket_client from hikari.internal import more_logging -from hikari.core.state import dispatcher -from hikari.core.clients import gateway_config -from hikari.core.clients import shard_client +from hikari.clients import gateway_config +from hikari.clients import shard_client from hikari.net import shard +from hikari.state import raw_event_consumer ShardT = typing.TypeVar("ShardT", bound=shard_client.ShardClient) -class GatewayClient(typing.Generic[ShardT], shard_client.WebsocketClientBase, dispatcher.EventDispatcher): +class GatewayClient(typing.Generic[ShardT], websocket_client.WebsocketClient): """Provides a management layer for multiple-sharded bots. This also provides additional conduit used to connect up shards to the rest of this framework to enable management of dispatched events, etc. + """ def __init__( @@ -49,12 +50,12 @@ def __init__( config: gateway_config.GatewayConfig, url: str, *, - state_impl: base_state.BaseState, + raw_event_consumer_impl: raw_event_consumer.RawEventConsumer, shard_type: typing.Type[ShardT] = shard_client.ShardClient, ) -> None: self.logger = more_logging.get_named_logger(self) self.config = config - self._state = state_impl + self.raw_event_consumer = raw_event_consumer_impl self._is_running = False self.shards: typing.Dict[int, ShardT] = { shard_id: shard_type(shard_id, config, self._handle_websocket_event_later, url) @@ -105,49 +106,6 @@ async def close(self, wait: bool = True) -> None: self.logger.info("stopped %s shard(s) in approx %.2fs", len(self.shards), finish_time - start_time) self._is_running = False - async def wait_for( - self, - event_type: typing.Type[dispatcher.EventT], - *, - predicate: dispatcher.PredicateT, - timeout: typing.Optional[float], - ) -> dispatcher.EventT: - """Wait for the given event type to occur. - - Parameters - ---------- - event_type : :obj:`typing.Type` [ :obj:`hikari.core.events.HikariEvent` ] - The name of the event to wait for. - timeout : :obj:`float`, optional - The timeout to wait for before cancelling and raising an - :obj:`asyncio.TimeoutError` instead. If this is ``None``, this will - wait forever. Care must be taken if you use ``None`` as this may - leak memory if you do this from an event listener that gets - repeatedly called. If you want to do this, you should consider - using an event listener instead of this function. - predicate : ``def predicate(event) -> bool`` or ``async def predicate(event) -> bool`` - A function that takes the arguments for the event and returns True - if it is a match, or False if it should be ignored. - This can be a coroutine function that returns a boolean, or a - regular function. - - Returns - ------- - :obj:`asyncio.Future`: - A future to await. When the given event is matched, this will be - completed with the corresponding event body. - - If the predicate throws an exception, or the timeout is reached, - then this will be set as an exception on the returned future. - """ - return await self.event_dispatcher.wait_for(event_type, predicate=predicate, timeout=timeout) - def _handle_websocket_event_later(self, conn: shard.ShardConnection, event_name: str, payload: typing.Any) -> None: - # Run this asynchronously so that we can allow awaiting stuff like state management. - asyncio.get_event_loop().create_task(self._handle_websocket_event(conn, event_name, payload)) - - async def _handle_websocket_event( - self, shard_obj: shard.ShardConnection, event_name: str, payload: typing.Any - ) -> None: - shard_client_obj = self.shards[shard_obj.shard_id] - await self._state.process_raw_event(shard_client_obj, event_name, payload) + shard_client_obj = self.shards[conn.shard_id] + self.raw_event_consumer.process_raw_event(shard_client_obj, event_name, payload) diff --git a/hikari/core/clients/gateway_config.py b/hikari/clients/gateway_config.py similarity index 63% rename from hikari/core/clients/gateway_config.py rename to hikari/clients/gateway_config.py index 5ce5899ea7..ce543d587d 100644 --- a/hikari/core/clients/gateway_config.py +++ b/hikari/clients/gateway_config.py @@ -17,89 +17,19 @@ # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . """Gateway and sharding configuration objects and options.""" -__all__ = ["GatewayConfig", "ShardConfig"] +__all__ = ["GatewayConfig"] import datetime -import re import typing -import hikari.internal.conversions -from hikari.internal import assertions +from hikari.internal import conversions +from hikari import entities +from hikari import gateway_entities +from hikari import guilds +from hikari.clients import protocol_config from hikari.internal import marshaller -from hikari.core import entities -from hikari.core import gateway_entities -from hikari.core import guilds -from hikari.core.clients import protocol_config from hikari.net import codes as net_codes - - -def _parse_shard_info(payload): - range_matcher = re.search(r"(\d+)\s*(\.{2,3})\s*(\d+)", payload) - - if not range_matcher: - if isinstance(payload, str): - payload = int(payload) - - if isinstance(payload, int): - return [payload] - - raise ValueError('expected shard_ids to be one of int, list of int, or range string ("x..y")') - - minimum, range_mod, maximum = range_matcher.groups() - minimum, maximum = int(minimum), int(maximum) - if len(range_mod) == 3: - maximum += 1 - - return [*range(minimum, maximum)] - - -@marshaller.attrs(kw_only=True, init=False) -class ShardConfig(entities.HikariEntity, entities.Deserializable): - """Manual sharding configuration. - - All fields are optional kwargs that can be passed to the constructor. - - "Deserialized" and "unspecified" defaults are only applicable if you - create the object using :meth:`deserialize`. - """ - - #: The shard IDs to produce shard connections for. - #: - #: If being deserialized, this can be several formats. - #: ``12``, ``"12"``: - #: A specific shard ID. - #: ``[0, 1, 2, 3, 8, 9, 10]``, ``["0", "1", "2", "3", "8", "9", "10"]``: - #: A sequence of shard IDs. - #: ``"5..16"``: - #: A range string. Two periods indicate a range of ``[5, 16)`` - #: (inclusive beginning, exclusive end). - #: ``"5...16"``: - #: A range string. Three periods indicate a range of - #: ``[5, 17]`` (inclusive beginning, inclusive end). - #: ``None``: - #: The ``shard_count`` will be considered and that many shards will - #: be created for you. - #: - #: :type: :obj:`typing.Sequence` [ :obj:`int` ] - shard_ids: typing.Sequence[int] = marshaller.attrib( - deserializer=_parse_shard_info, if_none=None, if_undefined=None, - ) - - #: The number of shards the entire distributed application should consist - #: of. If you run multiple instances of the bot, you should ensure this - #: value is consistent. - #: - #: :type: :obj:`int` - shard_count: int = marshaller.attrib(deserializer=int) - - # noinspection PyMissingConstructor - def __init__(self, *, shard_ids: typing.Optional[typing.Iterable[int]] = None, shard_count: int) -> None: - self.shard_ids = [*shard_ids] if shard_ids else [*range(shard_count)] - - for shard_id in self.shard_ids: - assertions.assert_that(shard_id < shard_count, "shard_count must be greater than any shard ids") - - self.shard_count = shard_count +from hikari.clients import shard_config as _shard_config @marshaller.attrs(kw_only=True) @@ -121,7 +51,7 @@ class GatewayConfig(entities.HikariEntity, entities.Deserializable): #: The initial activity to set all shards to when starting the gateway. If #: ``None``, then no activity will be set. #: - #: :type: :obj:`hikari.core.gateway_entities.GatewayActivity`, optional + #: :type: :obj:`hikari.gateway_entities.GatewayActivity`, optional initial_activity: typing.Optional[gateway_entities.GatewayActivity] = marshaller.attrib( deserializer=gateway_entities.GatewayActivity.deserialize, if_none=None, if_undefined=None, default=None ) @@ -143,7 +73,7 @@ class GatewayConfig(entities.HikariEntity, entities.Deserializable): #: #: :type: :obj:`datetime.datetime`, optional initial_idle_since: typing.Optional[datetime.datetime] = marshaller.attrib( - deserializer=hikari.internal.conversions.unix_epoch_to_ts, if_none=None, if_undefined=None, default=None + deserializer=conversions.unix_epoch_to_ts, if_none=None, if_undefined=None, default=None ) #: The intents to use for the connection. @@ -198,7 +128,7 @@ class GatewayConfig(entities.HikariEntity, entities.Deserializable): #: #: If unspecified, defaults are used. #: - #: :type: :obj:`hikari.core.protocol_config.HTTPProtocolConfig` + #: :type: :obj:`hikari.clients.protocol_config.HTTPProtocolConfig` protocol: typing.Optional[protocol_config.HTTPProtocolConfig] = marshaller.attrib( deserializer=protocol_config.HTTPProtocolConfig.deserialize, if_undefined=None, default=None, ) @@ -207,9 +137,9 @@ class GatewayConfig(entities.HikariEntity, entities.Deserializable): #: unspecified, then auto sharding configuration will be performed for you #: based on defaults suggested by Discord. #: - #: :type: :obj:`ShardConfig`, optional - shard_config: typing.Optional[ShardConfig] = marshaller.attrib( - deserializer=ShardConfig.deserialize, if_undefined=None, default=None, + #: :type: :obj:`hikari.clients.shard_config.ShardConfig`, optional + shard_config: typing.Optional[_shard_config.ShardConfig] = marshaller.attrib( + deserializer=_shard_config.ShardConfig.deserialize, if_undefined=None, default=None, ) #: The token to use, if applicable. diff --git a/hikari/core/clients/_run_gateway.py b/hikari/clients/gateway_runner.py similarity index 56% rename from hikari/core/clients/_run_gateway.py rename to hikari/clients/gateway_runner.py index 306a829f30..097a004a16 100644 --- a/hikari/core/clients/_run_gateway.py +++ b/hikari/clients/gateway_runner.py @@ -22,21 +22,40 @@ to use this. """ import logging +import os +import sys import click -from hikari.core import events -from hikari.core.clients import gateway_client -from hikari.core.clients import gateway_config -from hikari.core.clients import protocol_config -from hikari.core.state import default_state -from hikari.core.state import dispatcher +from hikari.clients import shard_client +from hikari import entities + +from hikari.clients import gateway_client +from hikari.clients import gateway_config +from hikari.clients import protocol_config +from hikari.state import raw_event_consumer logger_levels = ("DEBUG", "INFO", "WARNING", "ERROR", "NOTSET") +def _supports_color(): + plat = sys.platform + supported_platform = plat != "Pocket PC" and (plat != "win32" or "ANSICON" in os.environ) + # isatty is not always implemented, #6223. + is_a_tty = hasattr(sys.stdout, "isatty") and sys.stdout.isatty() + return supported_platform and is_a_tty + + +_color_format = ( + "\033[1;35m%(levelname)1.1s \033[0;31m%(asctime)23.23s \033[1;34m%(module)-15.15s " + "\033[1;32m#%(lineno)-4d \033[0m:: \033[0;33m%(message)s\033[0m" +) +_regular_format = "%(levelname)1.1s %(asctime)23.23s %(module)-15.15s #%(lineno)-4d :: %(message)s" + + @click.command() @click.option("--compression", default=True, type=click.BOOL, help="Enable or disable gateway compression.") +@click.option("--color", default=_supports_color(), type=click.BOOL, help="Whether to enable or disable color.") @click.option("--debug", default=False, type=click.BOOL, help="Enable or disable debug mode.") @click.option("--logger", default="INFO", type=click.Choice(logger_levels), help="Logger verbosity.") @click.option("--shards", default=1, type=click.IntRange(min=1), help="The number of shards to explicitly use.") @@ -44,12 +63,22 @@ @click.option("--url", default="wss://gateway.discord.gg/", help="The websocket URL to connect to.") @click.option("--verify-ssl", default=True, type=click.BOOL, help="Enable or disable SSL verification.") @click.option("--version", default=7, type=click.IntRange(min=6), help="Version of the gateway to use.") -def run_gateway(compression, debug, logger, shards, token, url, verify_ssl, version) -> None: - """Run the client.""" - logging.basicConfig(level=logger) +def run_gateway(compression, color, debug, logger, shards, token, url, verify_ssl, version) -> None: + """A :mod:`click` command line client for running a test gateway connection. + + This is provided for internal testing purposes for benchmarking API + stabiltiy, etc. + """ + logging.captureWarnings(True) + + logging.basicConfig(level=logger, format=_color_format if color else _regular_format, stream=sys.stdout) + + class _DummyConsumer(raw_event_consumer.RawEventConsumer): + def process_raw_event( + self, _client: shard_client.ShardClient, _name: str, _payload: entities.RawEntityT + ) -> None: + pass - event_dispatcher = dispatcher.EventDispatcherImpl() - state = default_state.DefaultState(event_dispatcher) client = gateway_client.GatewayClient( config=gateway_config.GatewayConfig( debug=debug, @@ -60,21 +89,11 @@ def run_gateway(compression, debug, logger, shards, token, url, verify_ssl, vers version=version, ), url=url, - state_impl=state, + raw_event_consumer_impl=_DummyConsumer(), ) - @event_dispatcher.on(events.MessageCreateEvent) - async def on_message(message: events.MessageCreateEvent) -> None: - logging.info( - "Received message from @%s#%s in %s (guild: %s) with content %s", - message.author.username, - message.author.discriminator, - message.channel_id, - message.guild_id, - message.content, - ) - client.run() -run_gateway() # pylint:disable=no-value-for-parameter +if __name__ == "__main__": + run_gateway() # pylint:disable=no-value-for-parameter diff --git a/hikari/core/clients/http_client.py b/hikari/clients/http_client.py similarity index 100% rename from hikari/core/clients/http_client.py rename to hikari/clients/http_client.py diff --git a/hikari/core/clients/http_config.py b/hikari/clients/http_config.py similarity index 93% rename from hikari/core/clients/http_config.py rename to hikari/clients/http_config.py index d2902932a6..e61a3fe3ee 100644 --- a/hikari/core/clients/http_config.py +++ b/hikari/clients/http_config.py @@ -23,8 +23,8 @@ import typing from hikari.internal import marshaller -from hikari.core import entities -from hikari.core.clients import protocol_config +from hikari import entities +from hikari.clients import protocol_config @marshaller.attrs(kw_only=True) @@ -41,7 +41,7 @@ class HTTPConfig(entities.HikariEntity, entities.Deserializable): #: #: If unspecified, defaults are used. #: - #: :type: :obj:`hikari.core.protocol_config.HTTPProtocolConfig` + #: :type: :obj:`hikari.protocol_config.HTTPProtocolConfig` protocol: typing.Optional[protocol_config.HTTPProtocolConfig] = marshaller.attrib( deserializer=protocol_config.HTTPProtocolConfig.deserialize, if_undefined=None, default=None, ) diff --git a/hikari/core/clients/protocol_config.py b/hikari/clients/protocol_config.py similarity index 95% rename from hikari/core/clients/protocol_config.py rename to hikari/clients/protocol_config.py index 46d50232ee..8324179930 100644 --- a/hikari/core/clients/protocol_config.py +++ b/hikari/clients/protocol_config.py @@ -28,7 +28,7 @@ import aiohttp.typedefs from hikari.internal import marshaller -from hikari.core import entities +from hikari import entities @marshaller.attrs(kw_only=True) @@ -42,7 +42,7 @@ class HTTPProtocolConfig(entities.HikariEntity, entities.Deserializable): All fields are optional kwargs that can be passed to the constructor. "Deserialized" and "unspecified" defaults are only applicable if you - create the object using :meth:`deserialize`. + create the object using :meth:`hikari.entities.Deserializable.deserialize`. """ #: If ``True``, allow following redirects from ``3xx`` HTTP responses. @@ -74,8 +74,8 @@ class HTTPProtocolConfig(entities.HikariEntity, entities.Deserializable): #: #: Defaults to ``None`` if unspecified during deserialization. #: - #: :type: :obj:`aiohttp.typedefs.LooseHeaders`, optional - proxy_headers: typing.Optional[aiohttp.typedefs.LooseHeaders] = marshaller.attrib( + #: :type: :obj:`typing.Mapping` [ :obj:`str`, :obj:`str` ], optional + proxy_headers: typing.Optional[typing.Mapping[str, str]] = marshaller.attrib( deserializer=dict, if_none=None, if_undefined=None, default=None ) diff --git a/hikari/core/clients/shard_client.py b/hikari/clients/shard_client.py similarity index 79% rename from hikari/core/clients/shard_client.py rename to hikari/clients/shard_client.py index 25b8ab697a..e43d73b362 100644 --- a/hikari/core/clients/shard_client.py +++ b/hikari/clients/shard_client.py @@ -22,32 +22,31 @@ well as restarting it if it disconnects. Additional functions and coroutines are provided to update the presence on the -shard using models defined in :mod:`hikari.core`. +shard using models defined in :mod:`hikari`. """ __all__ = ["ShardState", "ShardClient"] -import abc import asyncio import contextlib import datetime import enum -import logging -import signal import time import typing import aiohttp +from hikari.clients import gateway_config +from hikari.clients import websocket_client from hikari.internal import more_asyncio from hikari.internal import more_logging -from hikari.core import events -from hikari.core import gateway_entities -from hikari.core import guilds -from hikari.core.clients import gateway_config +from hikari import events +from hikari import gateway_entities +from hikari import guilds from hikari.net import codes -from hikari.net import errors +from hikari import errors from hikari.net import ratelimits from hikari.net import shard +from hikari.state import raw_event_consumer _EventT = typing.TypeVar("_EventT", bound=events.HikariEvent) @@ -74,89 +73,21 @@ class ShardState(enum.IntEnum): STOPPED = enum.auto() -class WebsocketClientBase(abc.ABC): - """Base for any socket-based communication medium to provide more functionality. - - This includes more automated control given certain method constraints. - """ - - logger: logging.Logger - - # Ignore docstring not starting in an imperative mood - @abc.abstractmethod - async def start(self): # noqa: D401 - """Starts the component.""" - - @abc.abstractmethod - async def close(self, wait: bool = True): - """Shut down the component.""" - - @abc.abstractmethod - async def join(self): - """Wait for the component to terminate.""" - - def run(self): - """Perform the same job as :meth:`start`, but with additional preparation. - - Additional preparation includes: registering OS signal handlers for interrupts - and preparing the initial event loop. - - This enables the client to be run immediately without having to - set up the :mod:`asyncio` event loop manually first. - """ - loop = asyncio.get_event_loop() - - def sigterm_handler(*_): - raise KeyboardInterrupt() - - ex = None - - try: - with contextlib.suppress(NotImplementedError): - # Not implemented on Windows - loop.add_signal_handler(signal.SIGTERM, sigterm_handler) - - loop.run_until_complete(self.start()) - loop.run_until_complete(self.join()) - - self.logger.info("client has shut down") - - except KeyboardInterrupt as _ex: - self.logger.info("received signal to shut down client") - loop.run_until_complete(self.close()) - # Apparently you have to alias except clauses or you get an - # UnboundLocalError. - ex = _ex - finally: - loop.run_until_complete(self.close(True)) - with contextlib.suppress(NotImplementedError): - # Not implemented on Windows - loop.remove_signal_handler(signal.SIGTERM) - - if ex: - raise ex from ex - - -class ShardClient(WebsocketClientBase): +class ShardClient(websocket_client.WebsocketClient): """The primary interface for a single shard connection. This contains several abstractions to enable usage of the low level gateway network interface with the higher level constructs - in :mod:`hikari.core`. + in :mod:`hikari`. Parameters ---------- shard_id : :obj:`int` The ID of this specific shard. - config : :obj:`hikari.core.gateway_config.GatewayConfig` + config : :obj:`hikari.clients.gateway_config.GatewayConfig` The gateway configuration to use to initialize this shard. - low_level_dispatch : :obj:`typing.Callable` [ [ :obj:`ShardClient`, :obj:`str`, :obj:`typing.Any` ] ] - A function that is fed any low-level event payloads. This will consist - of three arguments: an :obj:`ShardClient` which is this shard instance, - a :obj:`str` of the raw event name, and any naive raw payload that was - passed with the event. The expectation is the function passed here - will pass the payload onto any event handling and state handling system - to be transformed into a higher-level representation. + raw_event_consumer_impl : :obj:`hikari.state.raw_event_consumer.RawEventConsumer` + The consumer of a raw event. url : :obj:`str` The URL to connect the gateway to. @@ -171,7 +102,7 @@ class ShardClient(WebsocketClientBase): __slots__ = ( "logger", - "_dispatch", + "_raw_event_consumer", "_client", "_status", "_activity", @@ -185,11 +116,11 @@ def __init__( self, shard_id: int, config: gateway_config.GatewayConfig, - low_level_dispatch: typing.Callable[["ShardClient", str, typing.Any], None], + raw_event_consumer_impl: raw_event_consumer.RawEventConsumer, url: str, ) -> None: self.logger = more_logging.get_named_logger(self, shard_id) - self._dispatch = low_level_dispatch + self._raw_event_consumer = raw_event_consumer_impl self._activity = config.initial_activity self._idle_since = config.initial_idle_since self._is_afk = config.initial_is_afk @@ -200,7 +131,7 @@ def __init__( compression=config.use_compression, connector=config.protocol.connector if config.protocol is not None else None, debug=config.debug, - dispatch=self._dispatch, + dispatch=lambda c, n, pl: raw_event_consumer_impl.process_raw_event(self, n, pl), initial_presence=self._create_presence_pl( status=config.initial_status, activity=config.initial_activity, @@ -242,7 +173,7 @@ def status(self) -> guilds.PresenceStatus: # noqa: D401 Returns ------- - :obj:`hikari.core.guilds.PresenceStatus` + :obj:`hikari.guilds.PresenceStatus` The current user status for this shard. """ return self._status @@ -254,7 +185,7 @@ def activity(self) -> typing.Optional[gateway_entities.GatewayActivity]: # noqa Returns ------- - :obj:`hikari.core.gateway_entities.GatewayActivity`, optional + :obj:`hikari.gateway_entities.GatewayActivity`, optional The current activity for the user on this shard, or ``None`` if there is no activity. """ @@ -421,9 +352,9 @@ async def update_presence( Parameters ---------- - status : :obj:`hikari.core.guilds.PresenceStatus` + status : :obj:`hikari.guilds.PresenceStatus` The new status to set. - activity : :obj:`hikari.core.gateway_entities.GatewayActivity`, optional + activity : :obj:`hikari.gateway_entities.GatewayActivity`, optional The new activity to set. idle_since : :obj:`datetime.datetime`, optional The time to show up as being idle since, or ``None`` if not diff --git a/hikari/clients/shard_config.py b/hikari/clients/shard_config.py new file mode 100644 index 0000000000..d3b468d224 --- /dev/null +++ b/hikari/clients/shard_config.py @@ -0,0 +1,97 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# Copyright © Nekoka.tt 2019-2020 +# +# This file is part of Hikari. +# +# Hikari is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Hikari is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Hikari. If not, see . +"""Configuration for sharding.""" + +__all__ = ["ShardConfig"] + +import re +import typing + +from hikari import entities +from hikari.internal import assertions +from hikari.internal import marshaller + + +def _parse_shard_info(payload): + range_matcher = re.search(r"(\d+)\s*(\.{2,3})\s*(\d+)", payload) + + if not range_matcher: + if isinstance(payload, str): + payload = int(payload) + + if isinstance(payload, int): + return [payload] + + raise ValueError('expected shard_ids to be one of int, list of int, or range string ("x..y")') + + minimum, range_mod, maximum = range_matcher.groups() + minimum, maximum = int(minimum), int(maximum) + if len(range_mod) == 3: + maximum += 1 + + return [*range(minimum, maximum)] + + +@marshaller.attrs(kw_only=True, init=False) +class ShardConfig(entities.HikariEntity, entities.Deserializable): + """Manual sharding configuration. + + All fields are optional kwargs that can be passed to the constructor. + + "Deserialized" and "unspecified" defaults are only applicable if you + create the object using :meth:`deserialize`. + """ + + #: The shard IDs to produce shard connections for. + #: + #: If being deserialized, this can be several formats. + #: ``12``, ``"12"``: + #: A specific shard ID. + #: ``[0, 1, 2, 3, 8, 9, 10]``, ``["0", "1", "2", "3", "8", "9", "10"]``: + #: A sequence of shard IDs. + #: ``"5..16"``: + #: A range string. Two periods indicate a range of ``[5, 16)`` + #: (inclusive beginning, exclusive end). + #: ``"5...16"``: + #: A range string. Three periods indicate a range of + #: ``[5, 17]`` (inclusive beginning, inclusive end). + #: ``None``: + #: The ``shard_count`` will be considered and that many shards will + #: be created for you. + #: + #: :type: :obj:`typing.Sequence` [ :obj:`int` ] + shard_ids: typing.Sequence[int] = marshaller.attrib( + deserializer=_parse_shard_info, if_none=None, if_undefined=None, + ) + + #: The number of shards the entire distributed application should consist + #: of. If you run multiple instances of the bot, you should ensure this + #: value is consistent. + #: + #: :type: :obj:`int` + shard_count: int = marshaller.attrib(deserializer=int) + + # noinspection PyMissingConstructor + def __init__(self, *, shard_ids: typing.Optional[typing.Iterable[int]] = None, shard_count: int) -> None: + self.shard_ids = [*shard_ids] if shard_ids else [*range(shard_count)] + + for shard_id in self.shard_ids: + assertions.assert_that(shard_id < shard_count, "shard_count must be greater than any shard ids") + + self.shard_count = shard_count diff --git a/hikari/clients/websocket_client.py b/hikari/clients/websocket_client.py new file mode 100644 index 0000000000..b3e9287aca --- /dev/null +++ b/hikari/clients/websocket_client.py @@ -0,0 +1,87 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# Copyright © Nekoka.tt 2019-2020 +# +# This file is part of Hikari. +# +# Hikari is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Hikari is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Hikari. If not, see . +"""Provides a base for any type of websocket client.""" + +__all__ = ["WebsocketClient"] + +import abc +import asyncio +import contextlib +import logging +import signal + + +class WebsocketClient(abc.ABC): + """Base for any websocket client that must be kept alive.""" + + logger: logging.Logger + + @abc.abstractmethod + async def start(self): # noqa: D401 + """Start the component.""" + + @abc.abstractmethod + async def close(self, wait: bool = True): + """Shut down the component.""" + + @abc.abstractmethod + async def join(self): + """Wait for the component to terminate.""" + + def run(self): + """Execute this component on an event loop. + + Performs the same job as :meth:`start`, but provides additional + preparation such as registering OS signal handlers for interrupts, + and preparing the initial event loop. + + This enables the client to be run immediately without having to + set up the :mod:`asyncio` event loop manually first. + """ + loop = asyncio.get_event_loop() + + def sigterm_handler(*_): + raise KeyboardInterrupt() + + ex = None + + try: + with contextlib.suppress(NotImplementedError): + # Not implemented on Windows + loop.add_signal_handler(signal.SIGTERM, sigterm_handler) + + loop.run_until_complete(self.start()) + loop.run_until_complete(self.join()) + + self.logger.info("client has shut down") + + except KeyboardInterrupt as _ex: + self.logger.info("received signal to shut down client") + loop.run_until_complete(self.close()) + # Apparently you have to alias except clauses or you get an + # UnboundLocalError. + ex = _ex + finally: + loop.run_until_complete(self.close(True)) + with contextlib.suppress(NotImplementedError): + # Not implemented on Windows + loop.remove_signal_handler(signal.SIGTERM) + + if ex: + raise ex from ex diff --git a/hikari/core/colors.py b/hikari/colors.py similarity index 99% rename from hikari/core/colors.py rename to hikari/colors.py index 3fe6ca25a2..00d6e93ee0 100644 --- a/hikari/core/colors.py +++ b/hikari/colors.py @@ -20,7 +20,7 @@ .. inheritance-diagram:: builtins.int - hikari.core.colors + hikari.colors :parts: 1 """ diff --git a/hikari/core/colours.py b/hikari/colours.py similarity index 80% rename from hikari/core/colours.py rename to hikari/colours.py index 98d501684f..0b319e9e36 100644 --- a/hikari/core/colours.py +++ b/hikari/colours.py @@ -16,13 +16,7 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . -"""Alias for the :mod:`hikari.core.colors` module. - -.. inheritance-diagram:: - builtins.int - hikari.core.colours - :parts: 1 -""" +"""Alias for the :mod:`hikari.colors` module.""" __all__ = ["Colour"] -from hikari.core.colors import Color as Colour +from hikari.colors import Color as Colour diff --git a/hikari/core/__init__.py b/hikari/core/__init__.py deleted file mode 100644 index fed89f3a3d..0000000000 --- a/hikari/core/__init__.py +++ /dev/null @@ -1,70 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# Copyright © Nekokatt 2019-2020 -# -# This file is part of Hikari. -# -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . -"""The core API for interacting with Discord directly.""" - -# Do I need this? It still resolves without adding these in...? -from hikari.core import channels -from hikari.core import clients -from hikari.core import entities -from hikari.core import events -from hikari.core import gateway_entities -from hikari.core import guilds -from hikari.core import invites -from hikari.core import messages -from hikari.core import oauth2 -from hikari.core import permissions -from hikari.core import snowflakes -from hikari.core import users -from hikari.core import webhooks - -# Import everything into this namespace. -from hikari.core.channels import * -from hikari.core.clients import * -from hikari.core.colors import * -from hikari.core.colours import * -from hikari.core.embeds import * -from hikari.core.emojis import * -from hikari.core.entities import * -from hikari.core.events import * -from hikari.core.gateway_entities import * -from hikari.core.guilds import * -from hikari.core.invites import * -from hikari.core.messages import * -from hikari.core.oauth2 import * -from hikari.core.permissions import * -from hikari.core.snowflakes import * -from hikari.core.users import * -from hikari.core.voices import * -from hikari.core.webhooks import * - -__all__ = [ - *channels.__all__, - *clients.__all__, - *entities.__all__, - *events.__all__, - *gateway_entities.__all__, - *guilds.__all__, - *invites.__all__, - *messages.__all__, - *oauth2.__all__, - *permissions.__all__, - *snowflakes.__all__, - *users.__all__, - *webhooks.__all__, -] diff --git a/hikari/core/state/base_state.py b/hikari/core/state/base_state.py deleted file mode 100644 index ca74067cbb..0000000000 --- a/hikari/core/state/base_state.py +++ /dev/null @@ -1,121 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# Copyright © Nekoka.tt 2019-2020 -# -# This file is part of Hikari. -# -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . -"""Definition of the interface a compliant state implementation should provide. - -State object handle decoding events and managing application state. -""" -__all__ = ["BaseState"] - -import abc -import asyncio -import inspect -import typing - -from hikari.core import entities -from hikari.core import events -from hikari.core.clients import shard_client -from hikari.internal import assertions -from hikari.internal import more_logging - -EVENT_MARKER_ATTR = "___event_name___" - -EventConsumerT = typing.Callable[[str, entities.RawEntityT], typing.Awaitable[None]] - - -def register_state_event_handler(name: str) -> typing.Callable[[EventConsumerT], EventConsumerT]: - """Create a decorator for a coroutine function to register it as an event handler. - - Parameters - ---------- - name: str - The case sensitive name of the event to associate the annotated method - with. - - Returns - ------- - ``decorator(callable) -> callable`` - A decorator for a method. - - """ - - def decorator(callable_item: EventConsumerT) -> EventConsumerT: - assertions.assert_that(inspect.isfunction(callable_item), "Annotated element must be a function") - setattr(callable_item, EVENT_MARKER_ATTR, name) - return callable_item - - return decorator - - -def _has_event_marker(obj: typing.Any) -> bool: - return hasattr(obj, EVENT_MARKER_ATTR) - - -def _get_event_marker(obj: typing.Any) -> str: - return getattr(obj, EVENT_MARKER_ATTR) - - -class BaseState(abc.ABC): - """Abstract definition of a state manager. - - This is designed to manage any state-related operations in an application by - consuming raw events from a low level gateway connection, transforming them - to object-based event types, and tracking overall application state. - - Any methods marked with the :obj:`register_state_event_handler` decorator - will be detected and registered as event handlers by the constructor. - """ - - @abc.abstractmethod - def __init__(self): - self.logger = more_logging.get_named_logger(self) - self._event_mapping = {} - - # Look for events and register them. - for _, member in inspect.getmembers(self, _has_event_marker): - event = _get_event_marker(member) - self._event_mapping[event] = member - - async def process_raw_event( - self, shard_client_obj: shard_client.ShardClient, name: str, payload: entities.RawEntityT, - ) -> None: - """Process a low level event. - - This will update the internal state, perform processing where necessary, - and then dispatch the event to any listeners. - - Parameters - ---------- - shard_client_obj: :obj:`hikari.core.clients.shard_client.ShardClient` - The shard that triggered this event. - name : :obj:`str` - The raw event name. - payload : :obj:`dict` - The payload that was sent. - """ - try: - handler = self._event_mapping[name] - except KeyError: - self.logger.debug("No handler for event %s is registered", name) - else: - event = await handler(shard_client_obj, payload) - self.dispatch(event) - - @abc.abstractmethod - def dispatch(self, event: events.HikariEvent) -> None: - """Dispatch the given event somewhere.""" diff --git a/hikari/core/state/default_state.py b/hikari/core/state/default_state.py deleted file mode 100644 index dba0d1712f..0000000000 --- a/hikari/core/state/default_state.py +++ /dev/null @@ -1,38 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# Copyright © Nekoka.tt 2019-2020 -# -# This file is part of Hikari. -# -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . -"""Basic single-application state manager.""" -__all__ = ["DefaultState"] - -from hikari.core.state import dispatcher -from hikari.core import entities -from hikari.core import events -from hikari.core.state import base_state - - -class DefaultState(base_state.BaseState): # noqa: D101 - def __init__(self, event_dispatcher: dispatcher.EventDispatcher): - super().__init__() - self.event_dispatcher: dispatcher.EventDispatcher = event_dispatcher - - @base_state.register_state_event_handler("MESSAGE_CREATE") - async def _on_message_create(self, _, payload: entities.RawEntityT) -> None: - self.dispatch(events.MessageCreateEvent.deserialize(payload)) - - def dispatch(self, event: events.HikariEvent) -> None: - self.event_dispatcher.dispatch_event(event) diff --git a/hikari/core/embeds.py b/hikari/embeds.py similarity index 99% rename from hikari/core/embeds.py rename to hikari/embeds.py index e8201c047d..5b37ba69fd 100644 --- a/hikari/core/embeds.py +++ b/hikari/embeds.py @@ -33,8 +33,8 @@ import hikari.internal.conversions from hikari.internal import marshaller -from hikari.core import colors -from hikari.core import entities +from hikari import colors +from hikari import entities @marshaller.attrs(slots=True) diff --git a/hikari/core/emojis.py b/hikari/emojis.py similarity index 93% rename from hikari/core/emojis.py rename to hikari/emojis.py index 2bfb400498..b02904b071 100644 --- a/hikari/core/emojis.py +++ b/hikari/emojis.py @@ -20,9 +20,9 @@ import typing from hikari.internal import marshaller -from hikari.core import entities -from hikari.core import snowflakes -from hikari.core import users +from hikari import entities +from hikari import snowflakes +from hikari import users __all__ = ["Emoji", "UnicodeEmoji", "UnknownEmoji", "GuildEmoji"] @@ -63,7 +63,7 @@ class GuildEmoji(UnknownEmoji): #: The whitelisted role IDs to use this emoji. #: - #: :type: :obj:`typing.Set` [ :obj:`hikari.core.snowflakes.Snowflake` ] + #: :type: :obj:`typing.Set` [ :obj:`hikari.snowflakes.Snowflake` ] role_ids: typing.Set[snowflakes.Snowflake] = marshaller.attrib( raw_name="roles", deserializer=lambda roles: {snowflakes.Snowflake.deserialize(r) for r in roles}, @@ -78,7 +78,7 @@ class GuildEmoji(UnknownEmoji): #: in the server the emoji is from. #: #: - #: :type: :obj:`hikari.core.users.User`, optional + #: :type: :obj:`hikari.users.User`, optional user: typing.Optional[users.User] = marshaller.attrib( deserializer=users.User.deserialize, if_none=None, if_undefined=None ) diff --git a/hikari/core/entities.py b/hikari/entities.py similarity index 100% rename from hikari/core/entities.py rename to hikari/entities.py diff --git a/hikari/errors.py b/hikari/errors.py index d2c9d9595c..cbcf0914cb 100644 --- a/hikari/errors.py +++ b/hikari/errors.py @@ -16,14 +16,19 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . -"""Core errors that may be raised by this API implementation.""" +"""Core that may be raised by this API implementation.""" __all__ = ["HikariError"] +import typing + +from hikari.net import codes +from hikari.net import routes + class HikariError(RuntimeError): """Base for an error raised by this API. - Any errors should derive from this. + Any should derive from this. Note ---- @@ -31,3 +36,338 @@ class HikariError(RuntimeError): """ __slots__ = () + + +class GatewayError(HikariError): + """A base exception type for anything that can be thrown by the Gateway. + + Parameters + ---------- + reason : :obj:`str` + A string explaining the issue. + """ + + #: A string to explain the issue. + #: + #: :type: :obj:`str` + reason: str + + def __init__(self, reason: str) -> None: + super().__init__() + self.reason = reason + + def __str__(self) -> str: + return self.reason + + +class GatewayClientClosedError(GatewayError): + """An exception raised when you programmatically shut down the bot client-side. + + Parameters + ---------- + reason : :obj:`str` + A string explaining the issue. + """ + + def __init__(self, reason: str = "The gateway client has been closed") -> None: + super().__init__(reason) + + +class GatewayServerClosedConnectionError(GatewayError): + """An exception raised when the server closes the connection. + + Parameters + ---------- + close_code : :obj:`hikari.net.codes.GatewayCloseCode`, :obj:`int`, optional + The close code provided by the server, if there was one. + reason : :obj:`str`, optional + A string explaining the issue. + """ + + close_code: typing.Union[codes.GatewayCloseCode, int, None] + + def __init__( + self, + close_code: typing.Optional[typing.Union[codes.GatewayCloseCode, int]] = None, + reason: typing.Optional[str] = None, + ) -> None: + if reason is None: + try: + name = close_code.name + except AttributeError: + name = str(close_code) if close_code is not None else "no reason" + + reason = f"Gateway connection closed by server ({name})" + + self.close_code = close_code + super().__init__(reason) + + +class GatewayInvalidTokenError(GatewayServerClosedConnectionError): + """An exception that is raised if you failed to authenticate with a valid token to the Gateway.""" + + def __init__(self) -> None: + super().__init__( + codes.GatewayCloseCode.AUTHENTICATION_FAILED, + "The account token specified is invalid for the gateway connection", + ) + + +class GatewayInvalidSessionError(GatewayServerClosedConnectionError): + """An exception raised if a Gateway session becomes invalid. + + Parameters + ---------- + can_resume : :obj:`bool` + ``True`` if the connection will be able to RESUME next time it starts rather + than re-IDENTIFYing, or ``False`` if you need to IDENTIFY again instead. + """ + + #: ``True``` if the next reconnection can be RESUMED. ``False``` if it has to be + #: coordinated by re-IDENFITYing. + #: + #: :type: :obj:`bool` + can_resume: bool + + def __init__(self, can_resume: bool) -> None: + self.can_resume = can_resume + instruction = "restart the shard and RESUME" if can_resume else "restart the shard with a fresh session" + super().__init__(reason=f"The session has been invalidated; {instruction}") + + +class GatewayMustReconnectError(GatewayServerClosedConnectionError): + """An exception raised when the Gateway has to re-connect with a new session. + + This will cause a re-IDENTIFY. + """ + + def __init__(self) -> None: + super().__init__(reason="The gateway server has requested that the client reconnects with a new session") + + +class GatewayNeedsShardingError(GatewayServerClosedConnectionError): + """An exception raised if you have too many guilds on one of the current Gateway shards. + + This is a sign you need to increase the number of shards that your bot is + running with in order to connect to Discord. + """ + + def __init__(self) -> None: + super().__init__( + codes.GatewayCloseCode.SHARDING_REQUIRED, "You are in too many guilds. Shard the bot to connect", + ) + + +class GatewayZombiedError(GatewayClientClosedError): + """An exception raised if a shard becomes zombied. + + This means that Discord is no longer responding to us, and we have + disconnected due to a timeout. + """ + + def __init__(self) -> None: + super().__init__("No heartbeat was received, the connection has been closed") + + +class HTTPError(HikariError): + """Base exception raised if an HTTP error occurs. + + Parameters + ---------- + reason : :obj:`str` + A meaningful explanation of the problem. + """ + + #: A meaningful explanation of the problem. + #: + #: :type: :obj:`str` + reason: str + + def __init__(self, reason: str) -> None: + super().__init__() + self.reason = reason + + def __str__(self) -> str: + return self.reason + + +class CodedHTTPError(HTTPError): + """An HTTP exception that has contextual response information with it. + + Parameters + ---------- + status : :obj:`int` or :obj:`hikari.net.codes.HTTPStatusCode` + The HTTP status code that was returned by the server. + route : :obj:`hikari.net.routes.CompiledRoute` + The HTTP route that was being invoked when this exception occurred. + message : :obj:`str`, optional + An optional message if provided in the response payload. + json_code : :obj:`hikari.net.codes.JSONErrorCode`, :obj:`int`, optional + An optional error code the server provided us. + """ + + #: The HTTP status code that was returned by the server. + #: + #: :type: :obj:`int` or :obj:`hikari.net.codes.HTTPStatusCode` + status: typing.Union[int, codes.HTTPStatusCode] + + #: The HTTP route that was being invoked when this exception occurred. + #: + #: :type: :obj:`hikari.net.routes.CompiledRoute` + route: routes.CompiledRoute + + #: An optional contextual message the server provided us with in the + #: response body. + # + #: :type: :obj:`str`, optional + message: typing.Optional[str] + + #: An optional contextual error code the server provided us with in the + #: response body. + # + #: :type: :obj:`hikari.net.codes.JSONErrorCode` or :obj:`int`, optional + json_code: typing.Optional[typing.Union[codes.JSONErrorCode, int]] + + def __init__( + self, + status: typing.Union[int, codes.HTTPStatusCode], + route: routes.CompiledRoute, + message: typing.Optional[str], + json_code: typing.Union[codes.JSONErrorCode, int, None], + ) -> None: + super().__init__(str(status)) + self.status = status + self.route = route + self.message = message + self.json_code = json_code + + def __str__(self) -> str: + return f"{self.reason}: ({self.json_code}) {self.message}" + + +class ServerHTTPError(CodedHTTPError): + """An exception raised if a server-side error occurs when interacting with the REST API. + + If you get these, DO NOT PANIC! Your bot is working perfectly fine. Discord + have probably broken something again. + """ + + +class ClientHTTPError(CodedHTTPError): + """An exception raised if a server-side error occurs when interacting with the REST API. + + If you get one of these, you most likely have a mistake in your code, or + have found a bug with this library. + + If you are sure that your code is correct, please register a bug at + https://gitlab.com/nekokatt/hikari/issues and we will take a look for you. + """ + + +class BadRequestHTTPError(CodedHTTPError): + """A specific case of :obj:`CodedHTTPError`. + + This can occur hat occurs when you send Discord information in an unexpected + format, miss required information out, or give bad values for stuff. + + An example might be sending a message without any content, or an embed with + more than 6000 characters. + + Parameters + ---------- + route : :obj:`hikari.net.routes.CompiledRoute` + The HTTP route that was being invoked when this exception occurred. + message : :obj:`str`, optional + An optional message if provided in the response payload. + json_code : :obj:`hikari.net.codes.JSONErrorCode`, :obj:`int`, optional + An optional error code the server provided us. + """ + + def __init__( + self, + route: routes.CompiledRoute, + message: typing.Optional[str], + json_code: typing.Optional[typing.Union[codes.JSONErrorCode, int]], + ) -> None: + super().__init__(codes.HTTPStatusCode.BAD_REQUEST, route, message, json_code) + + +class UnauthorizedHTTPError(ClientHTTPError): + """A specific case of :obj:`ClientHTTPError`. + + This occurs when you have invalid authorization details to access + the given resource. + + This usually means that you have an incorrect token. + + Parameters + ---------- + route : :obj:`hikari.net.routes.CompiledRoute` + The HTTP route that was being invoked when this exception occurred. + message : :obj:`str`, optional + An optional message if provided in the response payload. + json_code : :obj:`hikari.net.codes.JSONErrorCode`, :obj:`int`, optional + An optional error code the server provided us. + """ + + def __init__( + self, + route: routes.CompiledRoute, + message: typing.Optional[str], + json_code: typing.Optional[typing.Union[codes.JSONErrorCode, int]], + ) -> None: + super().__init__(codes.HTTPStatusCode.UNAUTHORIZED, route, message, json_code) + + +class ForbiddenHTTPError(ClientHTTPError): + """A specific case of :obj:`ClientHTTPError`. + + This occurs when you are missing permissions, or are using an endpoint that + your account is not allowed to see without being whitelisted. + + This will not occur if your token is invalid. + + Parameters + ---------- + route : :obj:`hikari.net.routes.CompiledRoute` + The HTTP route that was being invoked when this exception occurred. + message : :obj:`str`, optional + An optional message if provided in the response payload. + json_code : :obj:`hikari.net.codes.JSONErrorCode`, :obj:`int`, optional + An optional error code the server provided us. + """ + + def __init__( + self, + route: routes.CompiledRoute, + message: typing.Optional[str], + json_code: typing.Optional[typing.Union[codes.JSONErrorCode, int]], + ) -> None: + super().__init__(codes.HTTPStatusCode.FORBIDDEN, route, message, json_code) + + +class NotFoundHTTPError(ClientHTTPError): + """A specific case of :obj:`ClientHTTPError`. + + This occurs when you try to refer to something that doesn't exist on Discord. + This might be referring to a user ID, channel ID, guild ID, etc that does + not exist, or it might be attempting to use an HTTP endpoint that is not + found. + + Parameters + ---------- + route : :obj:`hikari.net.routes.CompiledRoute` + The HTTP route that was being invoked when this exception occurred. + message : :obj:`str`, optional + An optional message if provided in the response payload. + json_code : :obj:`hikari.net.codes.JSONErrorCode`, :obj:`int`, optional + An optional error code the server provided us. + """ + + def __init__( + self, + route: routes.CompiledRoute, + message: typing.Optional[str], + json_code: typing.Optional[typing.Union[codes.JSONErrorCode, int]], + ) -> None: + super().__init__(codes.HTTPStatusCode.NOT_FOUND, route, message, json_code) diff --git a/hikari/core/events.py b/hikari/events.py similarity index 84% rename from hikari/core/events.py rename to hikari/events.py index 0a2057d18d..8266c2727a 100644 --- a/hikari/core/events.py +++ b/hikari/events.py @@ -74,17 +74,17 @@ import hikari.internal.conversions from hikari.internal import assertions from hikari.internal import marshaller -from hikari.core import channels -from hikari.core import embeds as _embeds -from hikari.core import emojis as _emojis -from hikari.core import entities -from hikari.core import guilds -from hikari.core import invites -from hikari.core import messages -from hikari.core import oauth2 -from hikari.core import snowflakes -from hikari.core import users -from hikari.core import voices +from hikari import channels +from hikari import embeds as _embeds +from hikari import emojis as _emojis +from hikari import entities +from hikari import guilds +from hikari import invites +from hikari import messages +from hikari import oauth2 +from hikari import snowflakes +from hikari import users +from hikari import voices T_contra = typing.TypeVar("T_contra", contravariant=True) @@ -112,7 +112,7 @@ class ExceptionEvent(HikariEvent): #: The event that was being invoked when the exception occurred. #: - #: :type: ``async def`` [ [ :obj:`HikariEvent` ], ``None`` ] + #: :type: coroutine function ( :obj:`HikariEvent` ) -> ``None`` callback: typing.Callable[[HikariEvent], typing.Awaitable[None]] @@ -180,13 +180,13 @@ class ReadyEvent(HikariEvent, entities.Deserializable): #: The object of the current bot account this connection is for. #: - #: :type: :obj:`hikari.core.users.MyUser` + #: :type: :obj:`hikari.users.MyUser` my_user: users.MyUser = marshaller.attrib(raw_name="user", deserializer=users.MyUser.deserialize) #: A mapping of the guilds this bot is currently in. All guilds will start #: off "unavailable". #: - #: :type: :obj:`typing.Mapping` [ :obj:`hikari.core.snowflake.Snowflake`, :obj:`hikari.core.guilds.UnavailableGuild` ] + #: :type: :obj:`typing.Mapping` [ :obj:`hikari.snowflakes.Snowflake`, :obj:`hikari.guilds.UnavailableGuild` ] unavailable_guilds: typing.Mapping[snowflakes.Snowflake, guilds.UnavailableGuild] = marshaller.attrib( raw_name="guilds", deserializer=lambda guilds_objs: {g.id: g for g in map(guilds.UnavailableGuild.deserialize, guilds_objs)}, @@ -233,12 +233,12 @@ class BaseChannelEvent(HikariEvent, snowflakes.UniqueEntity, entities.Deserializ #: The channel's type. #: - #: :type: :obj:`hikari.core.channels.ChannelType` + #: :type: :obj:`hikari.channels.ChannelType` type: channels.ChannelType = marshaller.attrib(deserializer=channels.ChannelType) #: The ID of the guild this channel is in, will be ``None`` for DMs. #: - #: :type: :obj:`hikari.core.snowflakes.Snowflake`, optional + #: :type: :obj:`hikari.snowflakes.Snowflake`, optional guild_id: typing.Optional[snowflakes.Snowflake] = marshaller.attrib( deserializer=snowflakes.Snowflake.deserialize, if_none=None ) @@ -251,7 +251,7 @@ class BaseChannelEvent(HikariEvent, snowflakes.UniqueEntity, entities.Deserializ #: An mapping of the set permission overwrites for this channel, if applicable. #: - #: :type: :obj:`typing.Mapping` [ :obj:`hikari.core.snowflakes.Snowflake`, :obj:`hikari.core.channels.PermissionOverwrite` ], optional + #: :type: :obj:`typing.Mapping` [ :obj:`hikari.snowflakes.Snowflake`, :obj:`hikari.channels.PermissionOverwrite` ], optional permission_overwrites: typing.Optional[ typing.Mapping[snowflakes.Snowflake, channels.PermissionOverwrite] ] = marshaller.attrib( @@ -276,7 +276,7 @@ class BaseChannelEvent(HikariEvent, snowflakes.UniqueEntity, entities.Deserializ #: The ID of the last message sent, if it's a text type channel. #: - #: :type: :obj:`hikari.core.snowflakes.Snowflake`, optional + #: :type: :obj:`hikari.snowflakes.Snowflake`, optional last_message_id: typing.Optional[snowflakes.Snowflake] = marshaller.attrib( deserializer=snowflakes.Snowflake.deserialize, if_none=None, if_undefined=None ) @@ -301,7 +301,7 @@ class BaseChannelEvent(HikariEvent, snowflakes.UniqueEntity, entities.Deserializ #: A mapping of this channel's recipient users, if it's a DM or group DM. #: - #: :type: :obj:`typing.Mapping` [ :obj:`hikari.core.snowflakes.Snowflake`, :obj:`hikari.core.users.User` ], optional + #: :type: :obj:`typing.Mapping` [ :obj:`hikari.snowflakes.Snowflake`, :obj:`hikari.users.User` ], optional recipients: typing.Optional[typing.Mapping[snowflakes.Snowflake, users.User]] = marshaller.attrib( deserializer=lambda recipients: {user.id: user for user in map(users.User.deserialize, recipients)}, if_undefined=None, @@ -316,7 +316,7 @@ class BaseChannelEvent(HikariEvent, snowflakes.UniqueEntity, entities.Deserializ #: The ID of this channel's creator, if it's a DM channel. #: - #: :type: :obj:`hikari.core.snowflakes.Snowflake`, optional + #: :type: :obj:`hikari.snowflakes.Snowflake`, optional owner_id: typing.Optional[snowflakes.Snowflake] = marshaller.attrib( deserializer=snowflakes.Snowflake.deserialize, if_undefined=None ) @@ -324,14 +324,14 @@ class BaseChannelEvent(HikariEvent, snowflakes.UniqueEntity, entities.Deserializ #: The ID of the application that created the group DM, if it's a #: bot based group DM. #: - #: :type: :obj:`hikari.core.snowflakes.Snowflake`, optional + #: :type: :obj:`hikari.snowflakes.Snowflake`, optional application_id: typing.Optional[snowflakes.Snowflake] = marshaller.attrib( deserializer=snowflakes.Snowflake.deserialize, if_undefined=None ) #: The ID of this channels's parent category within guild, if set. #: - #: :type: :obj:`hikari.core.snowflakes.Snowflake`, optional + #: :type: :obj:`hikari.snowflakes.Snowflake`, optional parent_id: typing.Optional[snowflakes.Snowflake] = marshaller.attrib( deserializer=snowflakes.Snowflake.deserialize, if_undefined=None, if_none=None ) @@ -379,14 +379,14 @@ class ChannelPinUpdateEvent(HikariEvent, entities.Deserializable): #: The ID of the guild where this event happened. #: Will be ``None`` if this happened in a DM channel. #: - #: :type: :obj:`hikari.core.snowflake.Snowflake`, optional + #: :type: :obj:`hikari.snowflakes.Snowflake`, optional guild_id: typing.Optional[snowflakes.Snowflake] = marshaller.attrib( deserializer=snowflakes.Snowflake.deserialize, if_undefined=None ) #: The ID of the channel where the message was pinned or unpinned. #: - #: :type: :obj:`hikari.core.snowflake.Snowflake` + #: :type: :obj:`hikari.snowflakes.Snowflake` channel_id: snowflakes.Snowflake = marshaller.attrib(deserializer=snowflakes.Snowflake.deserialize) #: The datetime of when the most recent message was pinned in this channel. @@ -442,12 +442,12 @@ class BaseGuildBanEvent(HikariEvent, entities.Deserializable): #: The ID of the guild this ban is in. #: - #: :type: :obj:`hikari.core.snowflake.Snowflake` + #: :type: :obj:`hikari.snowflakes.Snowflake` guild_id: snowflakes.Snowflake = marshaller.attrib(deserializer=snowflakes.Snowflake.deserialize) #: The object of the user this ban targets. #: - #: :type: :obj:`hikari.core.users.User` + #: :type: :obj:`hikari.users.User` user: users.User = marshaller.attrib(deserializer=users.User.deserialize) @@ -470,12 +470,12 @@ class GuildEmojisUpdateEvent(HikariEvent, entities.Deserializable): #: The ID of the guild this emoji was updated in. #: - #: :type: :obj:`hikari.core.snowflakes.Snowflake` + #: :type: :obj:`hikari.snowflakes.Snowflake` guild_id: snowflakes.Snowflake = marshaller.attrib(deserializer=snowflakes.Snowflake.deserialize) #: The updated mapping of emojis by their ID. #: - #: :type: :obj:`typing.Mapping` [ :obj:`hikari.core.snowflakes.Snowflake`, :obj:`hikari.core.emojis.GuildEmoji` ] + #: :type: :obj:`typing.Mapping` [ :obj:`hikari.snowflakes.Snowflake`, :obj:`hikari.emojis.GuildEmoji` ] emojis: typing.Mapping[snowflakes.Snowflake, _emojis.GuildEmoji] = marshaller.attrib( deserializer=lambda ems: {emoji.id: emoji for emoji in map(_emojis.GuildEmoji.deserialize, ems)} ) @@ -488,7 +488,7 @@ class GuildIntegrationsUpdateEvent(HikariEvent, entities.Deserializable): #: The ID of the guild the integration was updated in. #: - #: :type: :obj:`hikari.core.snowflake.Snowflake` + #: :type: :obj:`hikari.snowflakes.Snowflake` guild_id: snowflakes.Snowflake = marshaller.attrib(deserializer=snowflakes.Snowflake.deserialize) @@ -499,7 +499,7 @@ class GuildMemberAddEvent(HikariEvent, guilds.GuildMember): #: The ID of the guild where this member was added. #: - #: :type: :obj:`hikari.core.snowflakes.Snowflake` + #: :type: :obj:`hikari.snowflakes.Snowflake` guild_id: snowflakes.Snowflake = marshaller.attrib(deserializer=snowflakes.Snowflake.deserialize) @@ -513,12 +513,12 @@ class GuildMemberRemoveEvent(HikariEvent, entities.Deserializable): #: The ID of the guild this user was removed from. #: - #: :type: :obj:`hikari.core.snowflakes.Snowflake` + #: :type: :obj:`hikari.snowflakes.Snowflake` guild_id: snowflakes.Snowflake = marshaller.attrib(deserializer=snowflakes.Snowflake.deserialize) #: The object of the user who was removed from this guild. #: - #: :type: :obj:`hikari.core.users.User` + #: :type: :obj:`hikari.users.User` user: users.User = marshaller.attrib(deserializer=users.User.deserialize) @@ -532,25 +532,25 @@ class GuildMemberUpdateEvent(HikariEvent, entities.Deserializable): #: The ID of the guild this member was updated in. #: - #: :type: :obj:`hikari.core.snowflakes.Snowflake` + #: :type: :obj:`hikari.snowflakes.Snowflake` guild_id: snowflakes.Snowflake = marshaller.attrib(deserializer=snowflakes.Snowflake.deserialize) #: A sequence of the IDs of the member's current roles. #: - #: :type: :obj:`typing.Sequence` [ :obj:`hikari.core.snowflakes.Snowflake` ] + #: :type: :obj:`typing.Sequence` [ :obj:`hikari.snowflakes.Snowflake` ] role_ids: typing.Sequence[snowflakes.Snowflake] = marshaller.attrib( raw_name="roles", deserializer=lambda role_ids: [snowflakes.Snowflake.deserialize(rid) for rid in role_ids], ) #: The object of the user who was updated. #: - #: :type: :obj:`hikari.core.users.User` + #: :type: :obj:`hikari.users.User` user: users.User = marshaller.attrib(deserializer=users.User.deserialize) #: This member's nickname. When set to ``None``, this has been removed - #: and when set to :obj:`hikari.core.entities.UNSET` this hasn't been acted on. + #: and when set to :obj:`hikari.entities.UNSET` this hasn't been acted on. #: - #: :type: :obj:`typing.Union` [ :obj:`str`, :obj:`hikari.core.entities.UNSET` ], optional + #: :type: :obj:`typing.Union` [ :obj:`str`, :obj:`hikari.entities.UNSET` ], optional nickname: typing.Union[None, str, entities.Unset] = marshaller.attrib( raw_name="nick", deserializer=str, if_none=None, if_undefined=entities.Unset, ) @@ -558,7 +558,7 @@ class GuildMemberUpdateEvent(HikariEvent, entities.Deserializable): #: The datetime of when this member started "boosting" this guild. #: Will be ``None`` if they aren't boosting. #: - #: :type: :obj:`typing.Union` [ :obj:`datetime.datetime`, :obj:`hikari.core.entities.UNSET` ], optional + #: :type: :obj:`typing.Union` [ :obj:`datetime.datetime`, :obj:`hikari.entities.UNSET` ], optional premium_since: typing.Union[None, datetime.datetime, entities.Unset] = marshaller.attrib( deserializer=hikari.internal.conversions.parse_iso_8601_ts, if_none=None, if_undefined=entities.Unset ) @@ -571,12 +571,12 @@ class GuildRoleCreateEvent(HikariEvent, entities.Deserializable): #: The ID of the guild where this role was created. #: - #: :type: :obj:`hikari.core.snowflakes.Snowflake` + #: :type: :obj:`hikari.snowflakes.Snowflake` guild_id: snowflakes.Snowflake = marshaller.attrib(deserializer=snowflakes.Snowflake.deserialize) #: The object of the role that was created. #: - #: :type: :obj:`hikari.core.guilds.GuildRole` + #: :type: :obj:`hikari.guilds.GuildRole` role: guilds.GuildRole = marshaller.attrib(deserializer=guilds.GuildRole.deserialize) @@ -587,12 +587,12 @@ class GuildRoleUpdateEvent(HikariEvent, entities.Deserializable): #: The ID of the guild where this role was updated. #: - #: :type: :obj:`hikari.core.snowflakes.Snowflake` + #: :type: :obj:`hikari.snowflakes.Snowflake` guild_id: snowflakes.Snowflake = marshaller.attrib(deserializer=snowflakes.Snowflake.deserialize) #: The updated role object. #: - #: :type: :obj:`hikari.core.guilds.GuildRole` + #: :type: :obj:`hikari.guilds.GuildRole` role: guilds.GuildRole = marshaller.attrib(deserializer=guilds.GuildRole.deserialize) @@ -603,12 +603,12 @@ class GuildRoleDeleteEvent(HikariEvent, entities.Deserializable): #: The ID of the guild where this role is being deleted. #: - #: :type: :obj:`hikari.core.snowflakes.Snowflake` + #: :type: :obj:`hikari.snowflakes.Snowflake` guild_id: snowflakes.Snowflake = marshaller.attrib(deserializer=snowflakes.Snowflake.deserialize) #: The ID of the role being deleted. #: - #: :type: :obj:`hikari.core.snowflakes.Snowflake` + #: :type: :obj:`hikari.snowflakes.Snowflake` role_id: snowflakes.Snowflake = marshaller.attrib(deserializer=snowflakes.Snowflake.deserialize) @@ -619,7 +619,7 @@ class InviteCreateEvent(HikariEvent, entities.Deserializable): #: The ID of the channel this invite targets. #: - #: :type: :obj:`hikari.core.snowflakes.Snowflake` + #: :type: :obj:`hikari.snowflakes.Snowflake` channel_id: snowflakes.Snowflake = marshaller.attrib(deserializer=snowflakes.Snowflake.deserialize) #: The code that identifies this invite @@ -635,14 +635,14 @@ class InviteCreateEvent(HikariEvent, entities.Deserializable): #: The ID of the guild this invite was created in, if applicable. #: Will be ``None`` for group DM invites. #: - #: :type: :obj:`hikari.core.snowflakes.Snowflake`, optional + #: :type: :obj:`hikari.snowflakes.Snowflake`, optional guild_id: typing.Optional[snowflakes.Snowflake] = marshaller.attrib( deserializer=snowflakes.Snowflake.deserialize, if_undefined=None ) #: The object of the user who created this invite, if applicable. #: - #: :type: :obj:`hikari.core.users.User`, optional + #: :type: :obj:`hikari.users.User`, optional inviter: typing.Optional[users.User] = marshaller.attrib(deserializer=users.User.deserialize, if_undefined=None) #: The timedelta of how long this invite will be valid for. @@ -661,12 +661,12 @@ class InviteCreateEvent(HikariEvent, entities.Deserializable): #: The object of the user who this invite targets, if set. #: - #: :type: :obj:`hikari.core.users.User`, optional + #: :type: :obj:`hikari.users.User`, optional target_user: typing.Optional[users.User] = marshaller.attrib(deserializer=users.User.deserialize, if_undefined=None) #: The type of user target this invite is, if applicable. #: - #: :type: :obj:`hikari.core.invites.TargetUserType`, optional + #: :type: :obj:`hikari.invites.TargetUserType`, optional target_user_type: typing.Optional[invites.TargetUserType] = marshaller.attrib( deserializer=invites.TargetUserType, if_undefined=None ) @@ -692,7 +692,7 @@ class InviteDeleteEvent(HikariEvent, entities.Deserializable): #: The ID of the channel this ID was attached to #: - #: :type: :obj:`hikari.core.snowflakes.Snowflake` + #: :type: :obj:`hikari.snowflakes.Snowflake` channel_id: snowflakes.Snowflake = marshaller.attrib(deserializer=snowflakes.Snowflake.deserialize) #: The code of this invite. @@ -703,7 +703,7 @@ class InviteDeleteEvent(HikariEvent, entities.Deserializable): #: The ID of the guild this invite was deleted in. #: This will be ``None`` if this invite belonged to a DM channel. #: - #: :type: :obj:`hikari.core.snowflakes.Snowflake`, optional + #: :type: :obj:`hikari.snowflakes.Snowflake`, optional guild_id: typing.Optional[snowflakes.Snowflake] = marshaller.attrib( deserializer=snowflakes.Snowflake.deserialize, if_undefined=None ) @@ -723,73 +723,74 @@ class MessageUpdateEvent(HikariEvent, snowflakes.UniqueEntity, entities.Deserial Note ---- - All fields on this model except :attr:`channel_id` and :attr:`id` may be - set to :obj:`hikari.core.entities.UNSET` if we have not received information - about their state from Discord alongside field nullability. + All fields on this model except :attr:`channel_id` and :obj:``HikariEvent.id`` may be + set to :obj:`hikari.entities.UNSET` (a singleton defined in + ``hikari.entities``) if we have not received information about their + state from Discord alongside field nullability. """ #: The ID of the channel that the message was sent in. #: - #: :type: :obj:`hikari.core.snowflakes.Snowflake` + #: :type: :obj:`hikari.snowflakes.Snowflake` channel_id: snowflakes.Snowflake = marshaller.attrib(deserializer=snowflakes.Snowflake.deserialize) #: The ID of the guild that the message was sent in. #: - #: :type: :obj:`typing.Union` [ :obj:`hikari.core.snowflakes.Snowflake`, :obj:`hikari.core.entities.UNSET` ] + #: :type: :obj:`typing.Union` [ :obj:`hikari.snowflakes.Snowflake`, :obj:`hikari.entities.UNSET` ] guild_id: typing.Union[snowflakes.Snowflake, entities.Unset] = marshaller.attrib( deserializer=snowflakes.Snowflake.deserialize, if_undefined=entities.Unset ) #: The author of this message. #: - #: :type: :obj:`typing.Union` [ :obj:`hikari.core.users.User`, :obj:`hikari.core.entities.UNSET` ] + #: :type: :obj:`typing.Union` [ :obj:`hikari.users.User`, :obj:`hikari.entities.UNSET` ] author: typing.Union[users.User, entities.Unset] = marshaller.attrib( deserializer=users.User.deserialize, if_undefined=entities.Unset ) #: The member properties for the message's author. #: - #: :type: :obj:`typing.Union` [ :obj:`hikari.core.guilds.GuildMember`, :obj:`hikari.core.entities.UNSET` ] + #: :type: :obj:`typing.Union` [ :obj:`hikari.guilds.GuildMember`, :obj:`hikari.entities.UNSET` ] member: typing.Union[guilds.GuildMember, entities.Unset] = marshaller.attrib( deserializer=guilds.GuildMember.deserialize, if_undefined=entities.Unset ) #: The content of the message. #: - #: :type: :obj:`typing.Union` [ :obj:`str`, :obj:`hikari.core.entities.UNSET` ] + #: :type: :obj:`typing.Union` [ :obj:`str`, :obj:`hikari.entities.UNSET` ] content: typing.Union[str, entities.Unset] = marshaller.attrib(deserializer=str, if_undefined=entities.Unset) #: The timestamp that the message was sent at. #: - #: :type: :obj:`typing.Union` [ :obj:`datetime.datetime`, :obj:`hikari.core.entities.UNSET` ] + #: :type: :obj:`typing.Union` [ :obj:`datetime.datetime`, :obj:`hikari.entities.UNSET` ] timestamp: typing.Union[datetime.datetime, entities.Unset] = marshaller.attrib( deserializer=hikari.internal.conversions.parse_iso_8601_ts, if_undefined=entities.Unset ) #: The timestamp that the message was last edited at, or ``None`` if not ever edited. #: - #: :type: :obj:`typing.Union` [ :obj:`datetime.datetime`, :obj:`hikari.core.entities.UNSET` ], optional + #: :type: :obj:`typing.Union` [ :obj:`datetime.datetime`, :obj:`hikari.entities.UNSET` ], optional edited_timestamp: typing.Union[datetime.datetime, entities.Unset, None] = marshaller.attrib( deserializer=hikari.internal.conversions.parse_iso_8601_ts, if_none=None, if_undefined=entities.Unset ) #: Whether the message is a TTS message. #: - #: :type: :obj:`typing.Union` [ :obj:`bool`, :obj:`hikari.core.entities.UNSET` ] + #: :type: :obj:`typing.Union` [ :obj:`bool`, :obj:`hikari.entities.UNSET` ] is_tts: typing.Union[bool, entities.Unset] = marshaller.attrib( raw_name="tts", deserializer=bool, if_undefined=entities.Unset ) #: Whether the message mentions ``@everyone`` or ``@here``. #: - #: :type: :obj:`typing.Union` [ :obj:`bool`, :obj:`hikari.core.entities.UNSET` ] + #: :type: :obj:`typing.Union` [ :obj:`bool`, :obj:`hikari.entities.UNSET` ] is_mentioning_everyone: typing.Union[bool, entities.Unset] = marshaller.attrib( raw_name="mention_everyone", deserializer=bool, if_undefined=entities.Unset ) #: The users the message mentions. #: - #: :type: :obj:`typing.Union` [ :obj:`typing.Set` [ :obj:`hikari.core.snowflakes.Snowflake` ], :obj:`hikari.core.entities.UNSET` ] + #: :type: :obj:`typing.Union` [ :obj:`typing.Set` [ :obj:`hikari.snowflakes.Snowflake` ], :obj:`hikari.entities.UNSET` ] user_mentions: typing.Union[typing.Set[snowflakes.Snowflake], entities.Unset] = marshaller.attrib( raw_name="mentions", deserializer=lambda user_mentions: {snowflakes.Snowflake.deserialize(u["id"]) for u in user_mentions}, @@ -798,7 +799,7 @@ class MessageUpdateEvent(HikariEvent, snowflakes.UniqueEntity, entities.Deserial #: The roles the message mentions. #: - #: :type: :obj:`typing.Union` [ :obj:`typing.Set` [ :obj:`hikari.core.snowflakes.Snowflake` ], :obj:`hikari.core.entities.UNSET` ] + #: :type: :obj:`typing.Union` [ :obj:`typing.Set` [ :obj:`hikari.snowflakes.Snowflake` ], :obj:`hikari.entities.UNSET` ] role_mentions: typing.Union[typing.Set[snowflakes.Snowflake], entities.Unset] = marshaller.attrib( raw_name="mention_roles", deserializer=lambda role_mentions: {snowflakes.Snowflake.deserialize(r) for r in role_mentions}, @@ -807,7 +808,7 @@ class MessageUpdateEvent(HikariEvent, snowflakes.UniqueEntity, entities.Deserial #: The channels the message mentions. #: - #: :type: :obj:`typing.Union` [ :obj:`typing.Set` [ :obj:`snowflakes.Snowflake` ], :obj:`entities.UNSET` ] + #: :type: :obj:`typing.Union` [ :obj:`typing.Set` [ :obj:`hikari.snowflakes.Snowflake` ], :obj:`entities.UNSET` ] channel_mentions: typing.Union[typing.Set[snowflakes.Snowflake], entities.Unset] = marshaller.attrib( raw_name="mention_channels", deserializer=lambda channel_mentions: {snowflakes.Snowflake.deserialize(c["id"]) for c in channel_mentions}, @@ -816,7 +817,7 @@ class MessageUpdateEvent(HikariEvent, snowflakes.UniqueEntity, entities.Deserial #: The message attachments. #: - #: :type: :obj:`typing.Union` [ :obj:`typing.Sequence` [ :obj:`hikari.core.messages.Attachment` ], :obj:`hikari.core.entities.UNSET` ] + #: :type: :obj:`typing.Union` [ :obj:`typing.Sequence` [ :obj:`hikari.messages.Attachment` ], :obj:`hikari.entities.UNSET` ] attachments: typing.Union[typing.Sequence[messages.Attachment], entities.Unset] = marshaller.attrib( deserializer=lambda attachments: [messages.Attachment.deserialize(a) for a in attachments], if_undefined=entities.Unset, @@ -824,14 +825,14 @@ class MessageUpdateEvent(HikariEvent, snowflakes.UniqueEntity, entities.Deserial #: The message's embeds. #: - #: :type: :obj:`typing.Union` [ :obj:`typing.Sequence` [ :obj:`hikari.core.embeds.Embed` ], :obj:`hikari.core.entities.UNSET` ] + #: :type: :obj:`typing.Union` [ :obj:`typing.Sequence` [ :obj:`hikari.embeds.Embed` ], :obj:`hikari.entities.UNSET` ] embeds: typing.Union[typing.Sequence[_embeds.Embed], entities.Unset] = marshaller.attrib( deserializer=lambda embed_objs: [_embeds.Embed.deserialize(e) for e in embed_objs], if_undefined=entities.Unset, ) #: The message's reactions. #: - #: :type: :obj:`typing.Union` [ :obj:`typing.Sequence` [ :obj:`hikari.core.messages.Reaction` ], :obj:`hikari.core.entities.UNSET` ] + #: :type: :obj:`typing.Union` [ :obj:`typing.Sequence` [ :obj:`hikari.messages.Reaction` ], :obj:`hikari.entities.UNSET` ] reactions: typing.Union[typing.Sequence[messages.Reaction], entities.Unset] = marshaller.attrib( deserializer=lambda reactions: [messages.Reaction.deserialize(r) for r in reactions], if_undefined=entities.Unset, @@ -839,49 +840,49 @@ class MessageUpdateEvent(HikariEvent, snowflakes.UniqueEntity, entities.Deserial #: Whether the message is pinned. #: - #: :type: :obj:`typing.Union` [ :obj:`bool`, :obj:`hikari.core.entities.UNSET` ] + #: :type: :obj:`typing.Union` [ :obj:`bool`, :obj:`hikari.entities.UNSET` ] is_pinned: typing.Union[bool, entities.Unset] = marshaller.attrib( raw_name="pinned", deserializer=bool, if_undefined=entities.Unset ) #: If the message was generated by a webhook, the webhook's id. #: - #: :type: :obj:`typing.Union` [ :obj:`hikari.core.snowflakes.Snowflake`, :obj:`hikari.core.entities.UNSET` ] + #: :type: :obj:`typing.Union` [ :obj:`hikari.snowflakes.Snowflake`, :obj:`hikari.entities.UNSET` ] webhook_id: typing.Union[snowflakes.Snowflake, entities.Unset] = marshaller.attrib( deserializer=snowflakes.Snowflake.deserialize, if_undefined=entities.Unset ) #: The message's type. #: - #: :type: :obj:`typing.Union` [ :obj:`hikari.core.messages.MessageType`, :obj:`hikari.core.entities.UNSET` ] + #: :type: :obj:`typing.Union` [ :obj:`hikari.messages.MessageType`, :obj:`hikari.entities.UNSET` ] type: typing.Union[messages.MessageType, entities.Unset] = marshaller.attrib( deserializer=messages.MessageType, if_undefined=entities.Unset ) #: The message's activity. #: - #: :type: :obj:`typing.Union` [ :obj:`hikari.core.messages.MessageActivity`, :obj:`hikari.core.entities.UNSET` ] + #: :type: :obj:`typing.Union` [ :obj:`hikari.messages.MessageActivity`, :obj:`hikari.entities.UNSET` ] activity: typing.Union[messages.MessageActivity, entities.Unset] = marshaller.attrib( deserializer=messages.MessageActivity.deserialize, if_undefined=entities.Unset ) #: The message's application. #: - #: :type: :obj:`typing.Union` [ :obj:`hikari.core.oauth2.Application`, :obj:`hikari.core.entities.UNSET` ] + #: :type: :obj:`typing.Union` [ :obj:`hikari.oauth2.Application`, :obj:`hikari.entities.UNSET` ] application: typing.Optional[oauth2.Application] = marshaller.attrib( deserializer=oauth2.Application.deserialize, if_undefined=entities.Unset ) #: The message's crossposted reference data. #: - #: :type: :obj:`typing.Union` [ :obj:`hikari.core.messages.MessageCrosspost`, :obj:`hikari.core.entities.UNSET` ] + #: :type: :obj:`typing.Union` [ :obj:`hikari.messages.MessageCrosspost`, :obj:`hikari.entities.UNSET` ] message_reference: typing.Union[messages.MessageCrosspost, entities.Unset] = marshaller.attrib( deserializer=messages.MessageCrosspost.deserialize, if_undefined=entities.Unset ) #: The message's flags. #: - #: :type: :obj:`typing.Union` [ :obj:`hikari.core.messages.MessageFlag`, :obj:`hikari.core.entities.UNSET` ] + #: :type: :obj:`typing.Union` [ :obj:`hikari.messages.MessageFlag`, :obj:`hikari.entities.UNSET` ] flags: typing.Union[messages.MessageFlag, entities.Unset] = marshaller.attrib( deserializer=messages.MessageFlag, if_undefined=entities.Unset ) @@ -897,19 +898,19 @@ class MessageDeleteEvent(HikariEvent, entities.Deserializable): #: The ID of the channel where this message was deleted. #: - #: :type: :obj:`hikari.core.snowflakes.Snowflake` + #: :type: :obj:`hikari.snowflakes.Snowflake` channel_id: snowflakes.Snowflake = marshaller.attrib(deserializer=snowflakes.Snowflake.deserialize) #: The ID of the guild where this message was deleted. #: Will be ``None`` if this message was deleted in a DM channel. #: - #: :type: :obj:`hikari.core.snowflakes.Snowflake`, optional + #: :type: :obj:`hikari.snowflakes.Snowflake`, optional guild_id: typing.Optional[snowflakes.Snowflake] = marshaller.attrib( deserializer=snowflakes.Snowflake.deserialize, if_undefined=None ) #: The ID of the message that was deleted. #: - #: :type: :obj:`hikari.core.snowflakes.Snowflake` + #: :type: :obj:`hikari.snowflakes.Snowflake` message_id: snowflakes.Snowflake = marshaller.attrib(raw_name="id", deserializer=snowflakes.Snowflake.deserialize) @@ -923,20 +924,20 @@ class MessageDeleteBulkEvent(HikariEvent, entities.Deserializable): #: The ID of the channel these messages have been deleted in. #: - #: :type: :obj:`hikari.core.snowflakes.Snowflake` + #: :type: :obj:`hikari.snowflakes.Snowflake` channel_id: snowflakes.Snowflake = marshaller.attrib(deserializer=snowflakes.Snowflake.deserialize) #: The ID of the channel these messages have been deleted in. #: Will be ``None`` if these messages were bulk deleted in a DM channel. #: - #: :type: :obj:`hikari.core.snowflakes.Snowflake`, optional + #: :type: :obj:`hikari.snowflakes.Snowflake`, optional guild_id: typing.Optional[snowflakes.Snowflake] = marshaller.attrib( deserializer=snowflakes.Snowflake.deserialize, if_none=None ) #: A collection of the IDs of the messages that were deleted. #: - #: :type: :obj:`typing.Set` [ :obj:`hikari.core.snowflakes.Snowflake` ] + #: :type: :obj:`typing.Set` [ :obj:`hikari.snowflakes.Snowflake` ] message_ids: typing.Set[snowflakes.Snowflake] = marshaller.attrib( raw_name="ids", deserializer=lambda msgs: {snowflakes.Snowflake.deserialize(m) for m in msgs} ) @@ -949,23 +950,23 @@ class MessageReactionAddEvent(HikariEvent, entities.Deserializable): #: The ID of the user adding the reaction. #: - #: :type: :obj:`hikari.core.snowflakes.Snowflake` + #: :type: :obj:`hikari.snowflakes.Snowflake` user_id: snowflakes.Snowflake = marshaller.attrib(deserializer=snowflakes.Snowflake.deserialize) #: The ID of the channel where this reaction is being added. #: - #: :type: :obj:`hikari.core.snowflakes.Snowflake` + #: :type: :obj:`hikari.snowflakes.Snowflake` channel_id: snowflakes.Snowflake = marshaller.attrib(deserializer=snowflakes.Snowflake.deserialize) #: The ID of the message this reaction is being added to. #: - #: :type: :obj:`hikari.core.snowflakes.Snowflake` + #: :type: :obj:`hikari.snowflakes.Snowflake` message_id: snowflakes.Snowflake = marshaller.attrib(deserializer=snowflakes.Snowflake.deserialize) #: The ID of the guild where this reaction is being added, unless this is #: happening in a DM channel. #: - #: :type: :obj:`hikari.core.snowflakes.Snowflake`, optional + #: :type: :obj:`hikari.snowflakes.Snowflake`, optional guild_id: typing.Optional[snowflakes.Snowflake] = marshaller.attrib( deserializer=snowflakes.Snowflake.deserialize, if_undefined=None ) @@ -973,14 +974,14 @@ class MessageReactionAddEvent(HikariEvent, entities.Deserializable): #: The member object of the user who's adding this reaction, if this is #: occurring in a guild. #: - #: :type: :obj:`hikari.core.guilds.GuildMember`, optional + #: :type: :obj:`hikari.guilds.GuildMember`, optional member: typing.Optional[guilds.GuildMember] = marshaller.attrib( deserializer=guilds.GuildMember.deserialize, if_undefined=None ) #: The object of the emoji being added. #: - #: :type: :obj:`typing.Union` [ :obj:`hikari.core.emojis.UnknownEmoji`, :obj:`hikari.core.emojis.UnicodeEmoji` ] + #: :type: :obj:`typing.Union` [ :obj:`hikari.emojis.UnknownEmoji`, :obj:`hikari.emojis.UnicodeEmoji` ] emoji: typing.Union[_emojis.UnknownEmoji, _emojis.UnicodeEmoji] = marshaller.attrib( deserializer=_emojis.deserialize_reaction_emoji, ) @@ -993,30 +994,30 @@ class MessageReactionRemoveEvent(HikariEvent, entities.Deserializable): #: The ID of the user who is removing their reaction. #: - #: :type: :obj:`hikari.core.snowflakes.Snowflake` + #: :type: :obj:`hikari.snowflakes.Snowflake` user_id: snowflakes.Snowflake = marshaller.attrib(deserializer=snowflakes.Snowflake.deserialize) #: The ID of the channel where this reaction is being removed. #: - #: :type: :obj:`hikari.core.snowflakes.Snowflake` + #: :type: :obj:`hikari.snowflakes.Snowflake` channel_id: snowflakes.Snowflake = marshaller.attrib(deserializer=snowflakes.Snowflake.deserialize) #: The ID of the message this reaction is being removed from. #: - #: :type: :obj:`hikari.core.snowflakes.Snowflake` + #: :type: :obj:`hikari.snowflakes.Snowflake` message_id: snowflakes.Snowflake = marshaller.attrib(deserializer=snowflakes.Snowflake.deserialize) #: The ID of the guild where this reaction is being removed, unless this is #: happening in a DM channel. #: - #: :type: :obj:`hikari.core.snowflakes.Snowflake`, optional + #: :type: :obj:`hikari.snowflakes.Snowflake`, optional guild_id: typing.Optional[snowflakes.Snowflake] = marshaller.attrib( deserializer=snowflakes.Snowflake.deserialize, if_undefined=None ) #: The object of the emoji being removed. #: - #: :type: :obj:`typing.Union` [ :obj:`hikari.core.emojis.UnknownEmoji`, :obj:`hikari.core.emojis.UnicodeEmoji` ] + #: :type: :obj:`typing.Union` [ :obj:`hikari.emojis.UnknownEmoji`, :obj:`hikari.emojis.UnicodeEmoji` ] emoji: typing.Union[_emojis.UnicodeEmoji, _emojis.UnknownEmoji] = marshaller.attrib( deserializer=_emojis.deserialize_reaction_emoji, ) @@ -1032,17 +1033,17 @@ class MessageReactionRemoveAllEvent(HikariEvent, entities.Deserializable): #: The ID of the channel where the targeted message is. #: - #: :type: :obj:`hikari.core.snowflakes.Snowflake` + #: :type: :obj:`hikari.snowflakes.Snowflake` channel_id: snowflakes.Snowflake = marshaller.attrib(deserializer=snowflakes.Snowflake.deserialize) #: The ID of the message all reactions are being removed from. #: - #: :type: :obj:`hikari.core.snowflakes.Snowflake` + #: :type: :obj:`hikari.snowflakes.Snowflake` message_id: snowflakes.Snowflake = marshaller.attrib(deserializer=snowflakes.Snowflake.deserialize) #: The ID of the guild where the targeted message is, if applicable. #: - #: :type: :obj:`hikari.core.snowflakes.Snowflake` + #: :type: :obj:`hikari.snowflakes.Snowflake` guild_id: typing.Optional[snowflakes.Snowflake] = marshaller.attrib( deserializer=snowflakes.Snowflake.deserialize, if_undefined=None ) @@ -1058,24 +1059,24 @@ class MessageReactionRemoveEmojiEvent(HikariEvent, entities.Deserializable): #: The ID of the channel where the targeted message is. #: - #: :type: :obj:`hikari.core.snowflakes.Snowflake` + #: :type: :obj:`hikari.snowflakes.Snowflake` channel_id: snowflakes.Snowflake = marshaller.attrib(deserializer=snowflakes.Snowflake.deserialize) #: The ID of the guild where the targeted message is, if applicable. #: - #: :type: :obj:`hikari.core.snowflakes.Snowflake` + #: :type: :obj:`hikari.snowflakes.Snowflake` guild_id: typing.Optional[snowflakes.Snowflake] = marshaller.attrib( deserializer=snowflakes.Snowflake.deserialize, if_undefined=None ) #: The ID of the message the reactions are being removed from. #: - #: :type: :obj:`hikari.core.snowflakes.Snowflake` + #: :type: :obj:`hikari.snowflakes.Snowflake` message_id: snowflakes.Snowflake = marshaller.attrib(deserializer=snowflakes.Snowflake.deserialize) #: The object of the emoji that's being removed. #: - #: :type: :obj:`typing.Union` [ :obj:`hikari.core.emojis.UnknownEmoji`, :obj:`emojis.UnicodeEmoji` ] + #: :type: :obj:`typing.Union` [ :obj:`hikari.emojis.UnknownEmoji`, :obj:`emojis.UnicodeEmoji` ] emoji: typing.Union[_emojis.UnicodeEmoji, _emojis.UnknownEmoji] = marshaller.attrib( deserializer=_emojis.deserialize_reaction_emoji, ) @@ -1100,20 +1101,20 @@ class TypingStartEvent(HikariEvent, entities.Deserializable): #: The ID of the channel this typing event is occurring in. #: - #: :type: :obj:`hikari.core.snowflakes.Snowflake` + #: :type: :obj:`hikari.snowflakes.Snowflake` channel_id: snowflakes.Snowflake = marshaller.attrib(deserializer=snowflakes.Snowflake.deserialize) #: The ID of the guild this typing event is occurring in. #: Will be ``None`` if this event is happening in a DM channel. #: - #: :type: :obj:`hikari.core.snowflakes.Snowflake`, optional + #: :type: :obj:`hikari.snowflakes.Snowflake`, optional guild_id: typing.Optional[snowflakes.Snowflake] = marshaller.attrib( deserializer=snowflakes.Snowflake.deserialize, if_undefined=None ) #: The ID of the user who triggered this typing event. #: - #: :type: :obj:`hikari.core.snowflakes.Snowflake` + #: :type: :obj:`hikari.snowflakes.Snowflake` user_id: snowflakes.Snowflake = marshaller.attrib(deserializer=snowflakes.Snowflake.deserialize) #: The datetime of when this typing event started. @@ -1126,7 +1127,7 @@ class TypingStartEvent(HikariEvent, entities.Deserializable): #: The member object of the user who triggered this typing event, #: if this was triggered in a guild. #: - #: :type: :obj:`hikari.core.guilds.GuildMember`, optional + #: :type: :obj:`hikari.guilds.GuildMember`, optional member: typing.Optional[guilds.GuildMember] = marshaller.attrib( deserializer=guilds.GuildMember.deserialize, if_undefined=None ) @@ -1166,7 +1167,7 @@ class VoiceServerUpdateEvent(HikariEvent, entities.Deserializable): #: The ID of the guild this voice server update is for #: - #: :type: :obj:`hikari.core.snowflakes.Snowflake` + #: :type: :obj:`hikari.snowflakes.Snowflake` guild_id: snowflakes.Snowflake = marshaller.attrib(deserializer=snowflakes.Snowflake.deserialize) #: The uri for this voice server host. @@ -1185,10 +1186,10 @@ class WebhookUpdateEvent(HikariEvent, entities.Deserializable): #: The ID of the guild this webhook is being updated in. #: - #: :type: :obj:`hikari.core.snowflakes.Snowflake` + #: :type: :obj:`hikari.snowflakes.Snowflake` guild_id: snowflakes.Snowflake = marshaller.attrib(deserializer=snowflakes.Snowflake.deserialize) #: The ID of the channel this webhook is being updated in. #: - #: :type: :obj:`hikari.core.snowflakes.Snowflake` + #: :type: :obj:`hikari.snowflakes.Snowflake` channel_id: snowflakes.Snowflake = marshaller.attrib(deserializer=snowflakes.Snowflake.deserialize) diff --git a/hikari/core/gateway_entities.py b/hikari/gateway_entities.py similarity index 96% rename from hikari/core/gateway_entities.py rename to hikari/gateway_entities.py index 0a81439508..f808208966 100644 --- a/hikari/core/gateway_entities.py +++ b/hikari/gateway_entities.py @@ -23,8 +23,8 @@ import typing from hikari.internal import marshaller -from hikari.core import entities -from hikari.core import guilds +from hikari import entities +from hikari import guilds @marshaller.attrs(slots=True) @@ -89,7 +89,7 @@ class GatewayActivity(entities.Deserializable, entities.Serializable): #: The activity type. #: - #: :type: :obj:`hikari.core.guilds.ActivityType` + #: :type: :obj:`hikari.guilds.ActivityType` type: guilds.ActivityType = marshaller.attrib( deserializer=guilds.ActivityType, serializer=int, if_undefined=lambda: guilds.ActivityType.PLAYING ) diff --git a/hikari/core/guilds.py b/hikari/guilds.py similarity index 94% rename from hikari/core/guilds.py rename to hikari/guilds.py index 2ddb7185f6..bdea441af4 100644 --- a/hikari/core/guilds.py +++ b/hikari/guilds.py @@ -46,13 +46,13 @@ from hikari.internal import cdn from hikari.internal import conversions from hikari.internal import marshaller -from hikari.core import colors -from hikari.core import channels as _channels -from hikari.core import emojis as _emojis -from hikari.core import entities -from hikari.core import permissions as _permissions -from hikari.core import snowflakes -from hikari.core import users +from hikari import colors +from hikari import channels as _channels +from hikari import emojis as _emojis +from hikari import entities +from hikari import permissions as _permissions +from hikari import snowflakes +from hikari import users @enum.unique @@ -181,7 +181,7 @@ class GuildMember(entities.HikariEntity, entities.Deserializable): #: This member's user object, will be ``None`` when attached to Message #: Create and Update gateway events. #: - #: :type: :obj:`hikari.core.users.User`, optional + #: :type: :obj:`hikari.users.User`, optional user: typing.Optional[users.User] = marshaller.attrib(deserializer=users.User.deserialize, if_undefined=None) #: This member's nickname, if set. @@ -193,7 +193,7 @@ class GuildMember(entities.HikariEntity, entities.Deserializable): #: A sequence of the IDs of the member's current roles. #: - #: :type: :obj:`typing.Sequence` [ :obj:`hikari.core.snowflakes.Snowflake` ] + #: :type: :obj:`typing.Sequence` [ :obj:`hikari.snowflakes.Snowflake` ] role_ids: typing.Sequence[snowflakes.Snowflake] = marshaller.attrib( raw_name="roles", deserializer=lambda role_ids: [snowflakes.Snowflake.deserialize(rid) for rid in role_ids], ) @@ -239,7 +239,7 @@ class GuildRole(PartialGuildRole): #: The colour of this role, will be applied to a member's name in chat #: if it's their top coloured role. #: - #: :type: :obj:`hikari.core.colors.Color` + #: :type: :obj:`hikari.colors.Color` color: colors.Color = marshaller.attrib(deserializer=colors.Color) #: Whether this role is hoisting the members it's attached to in the member @@ -257,7 +257,7 @@ class GuildRole(PartialGuildRole): #: The guild wide permissions this role gives to the members it's attached #: to, may be overridden by channel overwrites. #: - #: :type: :obj:`hikari.core.permissions.Permission` + #: :type: :obj:`hikari.permissions.Permission` permissions: _permissions.Permission = marshaller.attrib(deserializer=_permissions.Permission) #: Whether this role is managed by an integration. @@ -405,7 +405,7 @@ class ActivityFlag(enum.IntFlag): @marshaller.attrs(slots=True) class PresenceActivity(entities.HikariEntity, entities.Deserializable): - """Represents an activity that'll be attached to a member's presence.""" + """Represents an activity that will be attached to a member's presence.""" #: The activity's name. #: @@ -437,7 +437,7 @@ class PresenceActivity(entities.HikariEntity, entities.Deserializable): #: The ID of the application this activity is for, if applicable. #: - #: :type: :obj:`hikari.core.snowflakes.Snowflake`, optional + #: :type: :obj:`hikari.snowflakes.Snowflake`, optional application_id: typing.Optional[snowflakes.Snowflake] = marshaller.attrib( deserializer=snowflakes.Snowflake.deserialize, if_undefined=None ) @@ -454,7 +454,7 @@ class PresenceActivity(entities.HikariEntity, entities.Deserializable): #: The emoji of this activity, if it is a custom status and set. #: - #: :type: :obj:`typing.Union` [ :obj:`hikari.core.emojis.UnicodeEmoji`, :obj:`hikari.core.emojis.UnknownEmoji` ], optional + #: :type: :obj:`typing.Union` [ :obj:`hikari.emojis.UnicodeEmoji`, :obj:`hikari.emojis.UnknownEmoji` ], optional emoji: typing.Union[None, _emojis.UnicodeEmoji, _emojis.UnknownEmoji] = marshaller.attrib( deserializer=_emojis.deserialize_reaction_emoji, if_undefined=None ) @@ -532,37 +532,37 @@ class PresenceUser(users.User): Warnings -------- - Every attribute except :attr:`id` may be received as :obj:`hikari.core.entities.UNSET` + Every attribute except :attr:`id` may be received as :obj:`hikari.entities.UNSET` unless it is specifically being modified for this update. """ #: This user's discriminator. #: - #: :type: :obj:`typing.Union` [ :obj:`str`, :obj:`hikari.core.entities.UNSET` ] + #: :type: :obj:`typing.Union` [ :obj:`str`, :obj:`hikari.entities.UNSET` ] discriminator: typing.Union[str, entities.Unset] = marshaller.attrib(deserializer=str, if_undefined=entities.Unset) #: This user's username. #: - #: :type: :obj:`typing.Union` [ :obj:`str`, :obj:`hikari.core.entities.UNSET` ] + #: :type: :obj:`typing.Union` [ :obj:`str`, :obj:`hikari.entities.UNSET` ] username: typing.Union[str, entities.Unset] = marshaller.attrib(deserializer=str, if_undefined=entities.Unset) #: This user's avatar hash, if set. #: - #: :type: :obj:`typing.Union` [ :obj:`str`, :obj:`hikari.core.entities.UNSET` ], optional + #: :type: :obj:`typing.Union` [ :obj:`str`, :obj:`hikari.entities.UNSET` ], optional avatar_hash: typing.Union[None, str, entities.Unset] = marshaller.attrib( raw_name="avatar", deserializer=str, if_none=None, if_undefined=entities.Unset ) #: Whether this user is a bot account. #: - #: :type: :obj:`typing.Union` [ :obj:`bool`, :obj:`hikari.core.entities.UNSET` ] + #: :type: :obj:`typing.Union` [ :obj:`bool`, :obj:`hikari.entities.UNSET` ] is_bot: typing.Union[bool, entities.Unset] = marshaller.attrib( raw_name="bot", deserializer=bool, if_undefined=entities.Unset ) #: Whether this user is a system account. #: - #: :type: :obj:`typing.Union` [ :obj:`bool`, :obj:`hikari.core.entities.UNSET` ] + #: :type: :obj:`typing.Union` [ :obj:`bool`, :obj:`hikari.entities.UNSET` ] is_system: typing.Union[bool, entities.Unset] = marshaller.attrib( raw_name="system", deserializer=bool, if_undefined=entities.Unset, ) @@ -582,14 +582,14 @@ class GuildMemberPresence(entities.HikariEntity, entities.Deserializable): #: A sequence of the ids of the user's current roles in the guild this #: presence belongs to. #: - #: :type: :obj:`typing.Sequence` [ :obj:`hikari.core.snowflakes.Snowflake` ] + #: :type: :obj:`typing.Sequence` [ :obj:`hikari.snowflakes.Snowflake` ] role_ids: typing.Sequence[snowflakes.Snowflake] = marshaller.attrib( raw_name="roles", deserializer=lambda roles: [snowflakes.Snowflake.deserialize(rid) for rid in roles], ) #: The ID of the guild this presence belongs to. #: - #: :type: :obj:`hikari.core.snowflakes.Snowflake` + #: :type: :obj:`hikari.snowflakes.Snowflake` guild_id: snowflakes.Snowflake = marshaller.attrib(deserializer=snowflakes.Snowflake.deserialize) #: This user's current status being displayed by the client. @@ -685,7 +685,7 @@ class GuildIntegration(snowflakes.UniqueEntity, entities.Deserializable): #: The ID of the managed role used for this integration's subscribers. #: - #: :type: :obj:`hikari.core.snowflakes.Snowflake` + #: :type: :obj:`hikari.snowflakes.Snowflake` role_id: typing.Optional[snowflakes.Snowflake] = marshaller.attrib(deserializer=snowflakes.Snowflake.deserialize) #: Whether users under this integration are allowed to use it's custom @@ -734,7 +734,7 @@ class GuildMemberBan(entities.HikariEntity, entities.Deserializable): #: The object of the user this ban targets. #: - #: :type: :obj:`hikari.core.users.User` + #: :type: :obj:`hikari.users.User` user: users.User = marshaller.attrib(deserializer=users.User.deserialize) @@ -835,14 +835,14 @@ class Guild(PartialGuild): #: The ID of the owner of this guild. #: - #: :type: :obj:`hikari.core.snowflakes.Snowflake` + #: :type: :obj:`hikari.snowflakes.Snowflake` owner_id: snowflakes.Snowflake = marshaller.attrib(deserializer=snowflakes.Snowflake.deserialize) #: The guild level permissions that apply to the bot user, #: Will be ``None`` when this object is retrieved through a REST request #: rather than from the gateway. #: - #: :type: :obj:`hikari.core.permissions.Permission` + #: :type: :obj:`hikari.permissions.Permission` my_permissions: _permissions.Permission = marshaller.attrib( raw_name="permissions", deserializer=_permissions.Permission, if_undefined=None ) @@ -855,7 +855,7 @@ class Guild(PartialGuild): #: The ID for the channel that AFK voice users get sent to, if set for the #: guild. #: - #: :type: :obj:`hikari.core.snowflakes.Snowflake`, optional + #: :type: :obj:`hikari.snowflakes.Snowflake`, optional afk_channel_id: typing.Optional[snowflakes.Snowflake] = marshaller.attrib( deserializer=snowflakes.Snowflake.deserialize, if_none=None ) @@ -881,7 +881,7 @@ class Guild(PartialGuild): #: enabled for this guild. Will be ``None`` if invites are disable for this #: guild's embed. #: - #: :type: :obj:`hikari.core.snowflakes.Snowflake`, optional + #: :type: :obj:`hikari.snowflakes.Snowflake`, optional embed_channel_id: typing.Optional[snowflakes.Snowflake] = marshaller.attrib( deserializer=snowflakes.Snowflake.deserialize, if_none=None, if_undefined=None ) @@ -907,7 +907,7 @@ class Guild(PartialGuild): #: The roles in this guild, represented as a mapping of ID to role object. #: - #: :type: :obj:`typing.Mapping` [ :obj:`hikari.core.snowflakes.Snowflake`, :obj:`GuildRole` ] + #: :type: :obj:`typing.Mapping` [ :obj:`hikari.snowflakes.Snowflake`, :obj:`GuildRole` ] roles: typing.Mapping[snowflakes.Snowflake, GuildRole] = marshaller.attrib( deserializer=lambda roles: {r.id: r for r in map(GuildRole.deserialize, roles)}, ) @@ -915,7 +915,7 @@ class Guild(PartialGuild): #: The emojis that this guild provides, represented as a mapping of ID to #: emoji object. #: - #: :type: :obj:`typing.Mapping` [ :obj:`hikari.core.snowflakes.Snowflake`, :obj:`hikari.core.emojis.GuildEmoji` ] + #: :type: :obj:`typing.Mapping` [ :obj:`hikari.snowflakes.Snowflake`, :obj:`hikari.emojis.GuildEmoji` ] emojis: typing.Mapping[snowflakes.Snowflake, _emojis.GuildEmoji] = marshaller.attrib( deserializer=lambda emojis: {e.id: e for e in map(_emojis.GuildEmoji.deserialize, emojis)}, ) @@ -928,7 +928,7 @@ class Guild(PartialGuild): #: The ID of the application that created this guild, if it was created by #: a bot. If not, this is always ``None``. #: - #: :type: :obj:`hikari.core.snowflakes.Snowflake`, optional + #: :type: :obj:`hikari.snowflakes.Snowflake`, optional application_id: typing.Optional[snowflakes.Snowflake] = marshaller.attrib( deserializer=snowflakes.Snowflake.deserialize, if_none=None ) @@ -957,7 +957,7 @@ class Guild(PartialGuild): #: The channel ID that the widget's generated invite will send the user to, #: if enabled. If this information is unavailable, this will be ``None``. #: - #: :type: :obj:`hikari.core.snowflakes.Snowflake`, optional + #: :type: :obj:`hikari.snowflakes.Snowflake`, optional widget_channel_id: typing.Optional[snowflakes.Snowflake] = marshaller.attrib( if_undefined=None, if_none=None, deserializer=snowflakes.Snowflake.deserialize ) @@ -965,7 +965,7 @@ class Guild(PartialGuild): #: The ID of the system channel (where welcome messages and Nitro boost #: messages are sent), or ``None`` if it is not enabled. #: - #: :type: :obj:`hikari.core.snowflakes.Snowflake`, optional + #: :type: :obj:`hikari.snowflakes.Snowflake`, optional system_channel_id: typing.Optional[snowflakes.Snowflake] = marshaller.attrib( if_none=None, deserializer=snowflakes.Snowflake.deserialize ) @@ -980,7 +980,7 @@ class Guild(PartialGuild): #: :attr:`features` display rules and guidelines. If the #: :obj:`GuildFeature.PUBLIC` feature is not defined, then this is ``None``. #: - #: :type: :obj:`hikari.core.snowflakes.Snowflake`, optional + #: :type: :obj:`hikari.snowflakes.Snowflake`, optional rules_channel_id: typing.Optional[snowflakes.Snowflake] = marshaller.attrib( if_none=None, deserializer=snowflakes.Snowflake.deserialize ) @@ -1034,7 +1034,7 @@ class Guild(PartialGuild): #: representation. If you need complete accurate information, you should #: query the members using the appropriate API call instead. #: - #: :type: :obj:`typing.Mapping` [ :obj:`hikari.core.snowflakes.Snowflake`, :obj:`GuildMember` ], optional + #: :type: :obj:`typing.Mapping` [ :obj:`hikari.snowflakes.Snowflake`, :obj:`GuildMember` ], optional members: typing.Optional[typing.Mapping[snowflakes.Snowflake, GuildMember]] = marshaller.attrib( deserializer=lambda members: {m.user.id: m for m in map(GuildMember.deserialize, members)}, if_undefined=None, ) @@ -1054,7 +1054,7 @@ class Guild(PartialGuild): #: To retrieve a list of channels in any other case, you should make an #: appropriate API call to retrieve this information. #: - #: :type: :obj:`typing.Mapping` [ :obj:`hikari.core.snowflakes.Snowflake`, :obj:`hikari.core.channels.GuildChannel` ], optional + #: :type: :obj:`typing.Mapping` [ :obj:`hikari.snowflakes.Snowflake`, :obj:`hikari.channels.GuildChannel` ], optional channels: typing.Optional[typing.Mapping[snowflakes.Snowflake, _channels.GuildChannel]] = marshaller.attrib( deserializer=lambda guild_channels: {c.id: c for c in map(_channels.deserialize_channel, guild_channels)}, if_undefined=None, @@ -1076,7 +1076,7 @@ class Guild(PartialGuild): #: To retrieve a list of presences in any other case, you should make an #: appropriate API call to retrieve this information. #: - #: :type: :obj:`typing.Mapping` [ :obj:`hikari.core.snowflakes.Snowflake`, :obj:`GuildMemberPresence` ], optional + #: :type: :obj:`typing.Mapping` [ :obj:`hikari.snowflakes.Snowflake`, :obj:`GuildMemberPresence` ], optional presences: typing.Optional[typing.Mapping[snowflakes.Snowflake, GuildMemberPresence]] = marshaller.attrib( deserializer=lambda presences: {p.user.id: p for p in map(GuildMemberPresence.deserialize, presences)}, if_undefined=None, @@ -1104,9 +1104,9 @@ class Guild(PartialGuild): #: The guild's description. #: - #: This is only present if certain :attr:`features` are set in this guild. - #: Otherwise, this will always be ``None``. For all other purposes, it is - #: ``None``. + #: This is only present if certain :attr:`Guild.features` are set in this + #: guild. Otherwise, this will always be ``None``. For all other purposes, + #: it is ``None``. #: #: :type: :obj:`str`, optional description: typing.Optional[str] = marshaller.attrib(if_none=None, deserializer=str) @@ -1144,7 +1144,7 @@ class Guild(PartialGuild): #: :attr:`features` for this guild. For all other purposes, it should be #: considered to be ``None``. #: - #: :type: :obj:`hikari.core.snowflakes.Snowflake`, optional + #: :type: :obj:`hikari.snowflakes.Snowflake`, optional public_updates_channel_id: typing.Optional[snowflakes.Snowflake] = marshaller.attrib( if_none=None, deserializer=snowflakes.Snowflake.deserialize ) diff --git a/hikari/internal/conversions.py b/hikari/internal/conversions.py index 3f3d0b31ae..0a0314670b 100644 --- a/hikari/internal/conversions.py +++ b/hikari/internal/conversions.py @@ -47,8 +47,8 @@ def nullable_cast(value: CastInputT, cast: TypeCastT) -> ResultT: """Attempt to cast the given ``value`` with the given ``cast``. - This will only succeed if ``value`` is not ``None``. If it is ``None``, - then ``None`` is returned instead. + This will only succeed if ``value`` is not ``None``. If it is ``None``, then + ``None`` is returned instead. """ if value is None: return None @@ -58,8 +58,8 @@ def nullable_cast(value: CastInputT, cast: TypeCastT) -> ResultT: def try_cast(value: CastInputT, cast: TypeCastT, default: DefaultT = None) -> ResultT: """Try to cast the given value to the given cast. - If it throws a :obj:`Exception` or derivative, it will return ``default`` instead - of the cast value instead. + If it throws a :obj:`Exception` or derivative, it will return ``default`` + instead of the cast value instead. """ with contextlib.suppress(Exception): return cast(value) @@ -82,7 +82,7 @@ def put_if_specified( The key to add the value under. value : :obj:`typing.Any` The value to add. - type_after : :obj:`TypeCastT`, optional + type_after : :obj:`typing.Callable` [ [ ``input type`` ], ``output type`` ], optional Type to apply to the value when added. """ if value is not ...: @@ -138,7 +138,7 @@ def try_cast_or_defer_unary_operator(type_): Parameters ---------- - type_ : :obj:`typing.Callable` [ ..., :obj:`T` ] + type_ : :obj:`typing.Callable` [ ..., ``output type`` ] The type to cast to. """ return lambda data: try_cast(data, type_, data) @@ -245,8 +245,8 @@ def unix_epoch_to_ts(epoch: int) -> datetime.datetime: def make_resource_seekable(resource: typing.Any) -> typing.Union[io.BytesIO, io.StringIO]: """Make a seekable resource to use off some representation of data. - This supports :obj:`bytes`, :obj:`bytearray`, :obj:`memoryview`, and :obj:`str`. - Anything else is just returned. + This supports :obj:`bytes`, :obj:`bytearray`, :obj:`memoryview`, and + :obj:`str`. Anything else is just returned. Parameters ---------- diff --git a/hikari/internal/marshaller.py b/hikari/internal/marshaller.py index 80874f3297..847dfc0354 100644 --- a/hikari/internal/marshaller.py +++ b/hikari/internal/marshaller.py @@ -440,12 +440,12 @@ def serialize(self, obj: typing.Optional[typing.Any]) -> typing.Optional[typing. def attrs(**kwargs): - """Create a decorator for a class to make it into an :mod:`attrs` class. + """Create a decorator for a class to make it into an :obj:`attr.s` class. Parameters ---------- **kwargs - Any kwargs to pass to :func:`attr.s`. + Any kwargs to pass to :obj:`attr.s`. Other Parameters ---------------- @@ -467,7 +467,7 @@ def attrs(**kwargs): ------ :obj:`ValueError` If you attempt to use the `auto_attribs` feature provided by - :mod:`attr`. + :obj:`attr.s`. Example ------- diff --git a/hikari/core/invites.py b/hikari/invites.py similarity index 94% rename from hikari/core/invites.py rename to hikari/invites.py index a571c39d68..51e6a1191f 100644 --- a/hikari/core/invites.py +++ b/hikari/invites.py @@ -26,10 +26,10 @@ import hikari.internal.conversions from hikari.internal import cdn from hikari.internal import marshaller -from hikari.core import channels -from hikari.core import entities -from hikari.core import guilds -from hikari.core import users +from hikari import channels +from hikari import entities +from hikari import guilds +from hikari import users @enum.unique @@ -66,7 +66,7 @@ class InviteGuild(guilds.PartialGuild): #: The hash for the guild's banner. #: - #: This is only present if :obj:`hikari.core.guild.GuildFeature.BANNER` + #: This is only present if :obj:`hikari.guild.GuildFeature.BANNER` #: is in the ``features`` for this guild. For all other purposes, it is ``None``. #: #: :type: :obj:`str`, optional @@ -83,12 +83,12 @@ class InviteGuild(guilds.PartialGuild): #: The verification level required for a user to participate in this guild. #: - #: :type: :obj:`hikari.core.guilds.GuildVerificationLevel` + #: :type: :obj:`hikari.guilds.GuildVerificationLevel` verification_level: guilds.GuildVerificationLevel = marshaller.attrib(deserializer=guilds.GuildVerificationLevel) #: The vanity URL code for the guild's vanity URL. #: - #: This is only present if :obj:`hikari.core.guilds.GuildFeature.VANITY_URL` + #: This is only present if :obj:`hikari.guilds.GuildFeature.VANITY_URL` #: is in the ``features`` for this guild. If not, this will always be ``None``. #: #: :type: :obj:`str`, optional @@ -163,17 +163,17 @@ class Invite(entities.HikariEntity, entities.Deserializable): guild: typing.Optional[InviteGuild] = marshaller.attrib(deserializer=InviteGuild.deserialize, if_undefined=None) #: The partial object of the channel this invite targets. #: - #: :type: :obj:`hikari.core.channels.PartialChannel` + #: :type: :obj:`hikari.channels.PartialChannel` channel: channels.PartialChannel = marshaller.attrib(deserializer=channels.PartialChannel.deserialize) #: The object of the user who created this invite. #: - #: :type: :obj:`hikari.core.users.User`, optional + #: :type: :obj:`hikari.users.User`, optional inviter: typing.Optional[users.User] = marshaller.attrib(deserializer=users.User.deserialize, if_undefined=None) #: The object of the user who this invite targets, if set. #: - #: :type: :obj:`hikari.core.users.User`, optional + #: :type: :obj:`hikari.users.User`, optional target_user: typing.Optional[users.User] = marshaller.attrib(deserializer=users.User.deserialize, if_undefined=None) #: The type of user target this invite is, if applicable. diff --git a/hikari/core/messages.py b/hikari/messages.py similarity index 90% rename from hikari/core/messages.py rename to hikari/messages.py index 204dded919..f87d865f52 100644 --- a/hikari/core/messages.py +++ b/hikari/messages.py @@ -34,13 +34,13 @@ import hikari.internal.conversions from hikari.internal import marshaller -from hikari.core import embeds as _embeds -from hikari.core import emojis as _emojis -from hikari.core import entities -from hikari.core import guilds -from hikari.core import oauth2 -from hikari.core import snowflakes -from hikari.core import users +from hikari import embeds as _embeds +from hikari import emojis as _emojis +from hikari import entities +from hikari import guilds +from hikari import oauth2 +from hikari import snowflakes +from hikari import users @enum.unique @@ -152,7 +152,7 @@ class Reaction(entities.HikariEntity, entities.Deserializable): #: The emoji used to react. #: - #: :type: :obj:`typing.Union` [ :obj:`hikari.core.emojis.UnicodeEmoji`, :obj:`hikari.core.emojis.UnknownEmoji`] + #: :type: :obj:`typing.Union` [ :obj:`hikari.emojis.UnicodeEmoji`, :obj:`hikari.emojis.UnknownEmoji`] emoji: typing.Union[_emojis.UnicodeEmoji, _emojis.UnknownEmoji] = marshaller.attrib( deserializer=_emojis.deserialize_reaction_emoji ) @@ -190,14 +190,14 @@ class MessageCrosspost(entities.HikariEntity, entities.Deserializable): #: documentation, but the situations that cause this to occur are not currently documented. #: #: - #: :type: :obj:`hikari.core.snowflakes.Snowflake`, optional + #: :type: :obj:`hikari.snowflakes.Snowflake`, optional message_id: typing.Optional[snowflakes.Snowflake] = marshaller.attrib( deserializer=snowflakes.Snowflake.deserialize, if_undefined=None ) #: The ID of the channel that the message originated from. #: - #: :type: :obj:`hikari.core.snowflakes.Snowflake` + #: :type: :obj:`hikari.snowflakes.Snowflake` channel_id: snowflakes.Snowflake = marshaller.attrib(deserializer=snowflakes.Snowflake.deserialize) #: The ID of the guild that the message originated from. @@ -207,7 +207,7 @@ class MessageCrosspost(entities.HikariEntity, entities.Deserializable): #: This may be ``None`` in some cases according to the Discord API #: documentation, but the situations that cause this to occur are not currently documented. #: - #: :type: :obj:`hikari.core.snowflakes.Snowflake`, optional + #: :type: :obj:`hikari.snowflakes.Snowflake`, optional guild_id: typing.Optional[snowflakes.Snowflake] = marshaller.attrib( deserializer=snowflakes.Snowflake.deserialize, if_undefined=None ) @@ -219,24 +219,24 @@ class Message(snowflakes.UniqueEntity, entities.Deserializable): #: The ID of the channel that the message was sent in. #: - #: :type: :obj:`hikari.core.snowflakes.Snowflake` + #: :type: :obj:`hikari.snowflakes.Snowflake` channel_id: snowflakes.Snowflake = marshaller.attrib(deserializer=snowflakes.Snowflake.deserialize) #: The ID of the guild that the message was sent in. #: - #: :type: :obj:`hikari.core.snowflakes.Snowflake`, optional + #: :type: :obj:`hikari.snowflakes.Snowflake`, optional guild_id: typing.Optional[snowflakes.Snowflake] = marshaller.attrib( deserializer=snowflakes.Snowflake.deserialize, if_undefined=None ) #: The author of this message. #: - #: :type: :obj:`hikari.core.users.User` + #: :type: :obj:`hikari.users.User` author: users.User = marshaller.attrib(deserializer=users.User.deserialize) #: The member properties for the message's author. #: - #: :type: :obj:`hikari.core.guilds.GuildMember`, optional + #: :type: :obj:`hikari.guilds.GuildMember`, optional member: typing.Optional[guilds.GuildMember] = marshaller.attrib( deserializer=guilds.GuildMember.deserialize, if_undefined=None ) @@ -270,7 +270,7 @@ class Message(snowflakes.UniqueEntity, entities.Deserializable): #: The users the message mentions. #: - #: :type: :obj:`typing.Set` [ :obj:`hikari.core.snowflakes.Snowflake` ] + #: :type: :obj:`typing.Set` [ :obj:`hikari.snowflakes.Snowflake` ] user_mentions: typing.Set[snowflakes.Snowflake] = marshaller.attrib( raw_name="mentions", deserializer=lambda user_mentions: {snowflakes.Snowflake.deserialize(u["id"]) for u in user_mentions}, @@ -278,7 +278,7 @@ class Message(snowflakes.UniqueEntity, entities.Deserializable): #: The roles the message mentions. #: - #: :type: :obj:`typing.Set` [ :obj:`hikari.core.snowflakes.Snowflake` ] + #: :type: :obj:`typing.Set` [ :obj:`hikari.snowflakes.Snowflake` ] role_mentions: typing.Set[snowflakes.Snowflake] = marshaller.attrib( raw_name="mention_roles", deserializer=lambda role_mentions: {snowflakes.Snowflake.deserialize(mention) for mention in role_mentions}, @@ -286,7 +286,7 @@ class Message(snowflakes.UniqueEntity, entities.Deserializable): #: The channels the message mentions. #: - #: :type: :obj:`typing.Set` [ :obj:`hikari.core.snowflakes.Snowflake` ] + #: :type: :obj:`typing.Set` [ :obj:`hikari.snowflakes.Snowflake` ] channel_mentions: typing.Set[snowflakes.Snowflake] = marshaller.attrib( raw_name="mention_channels", deserializer=lambda channel_mentions: {snowflakes.Snowflake.deserialize(c["id"]) for c in channel_mentions}, @@ -302,7 +302,7 @@ class Message(snowflakes.UniqueEntity, entities.Deserializable): #: The message embeds. #: - #: :type: :obj:`typing.Sequence` [ :obj:`hikari.core.embeds.Embed` ] + #: :type: :obj:`typing.Sequence` [ :obj:`hikari.embeds.Embed` ] embeds: typing.Sequence[_embeds.Embed] = marshaller.attrib( deserializer=lambda embeds: [_embeds.Embed.deserialize(e) for e in embeds] ) @@ -321,7 +321,7 @@ class Message(snowflakes.UniqueEntity, entities.Deserializable): #: If the message was generated by a webhook, the webhook's id. #: - #: :type: :obj:`hikari.core.snowflakes.Snowflake`, optional + #: :type: :obj:`hikari.snowflakes.Snowflake`, optional webhook_id: typing.Optional[snowflakes.Snowflake] = marshaller.attrib( deserializer=snowflakes.Snowflake.deserialize, if_undefined=None ) @@ -340,7 +340,7 @@ class Message(snowflakes.UniqueEntity, entities.Deserializable): #: The message application. #: - #: :type: :obj:`hikari.core.oauth2.Application`, optional + #: :type: :obj:`hikari.oauth2.Application`, optional application: typing.Optional[oauth2.Application] = marshaller.attrib( deserializer=oauth2.Application.deserialize, if_undefined=None ) diff --git a/hikari/net/__init__.py b/hikari/net/__init__.py index f380381ae3..7c49fa7d80 100644 --- a/hikari/net/__init__.py +++ b/hikari/net/__init__.py @@ -30,9 +30,8 @@ from hikari.net import versions from hikari.net.codes import * -from hikari.net.errors import * from hikari.net.rest import * from hikari.net.shard import * from hikari.net.versions import * -__all__ = codes.__all__ + errors.__all__ + shard.__all__ + rest.__all__ + versions.__all__ +__all__ = codes.__all__ + shard.__all__ + rest.__all__ + versions.__all__ diff --git a/hikari/net/errors.py b/hikari/net/errors.py deleted file mode 100644 index d463636a9c..0000000000 --- a/hikari/net/errors.py +++ /dev/null @@ -1,377 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# Copyright © Nekokatt 2019-2020 -# -# This file is part of Hikari. -# -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . -"""Errors that can be raised by networking components.""" -__all__ = [ - "GatewayError", - "GatewayClientClosedError", - "GatewayServerClosedConnectionError", - "GatewayInvalidTokenError", - "GatewayInvalidSessionError", - "GatewayMustReconnectError", - "GatewayNeedsShardingError", - "GatewayZombiedError", - "HTTPError", - "ServerHTTPError", - "ClientHTTPError", - "BadRequestHTTPError", - "UnauthorizedHTTPError", - "ForbiddenHTTPError", - "NotFoundHTTPError", -] - -import typing - -from hikari import errors -from hikari.net import codes -from hikari.net import routes - - -class GatewayError(errors.HikariError): - """A base exception type for anything that can be thrown by the Gateway. - - Parameters - ---------- - reason : :obj:`str` - A string explaining the issue. - """ - - #: A string to explain the issue. - #: - #: :type: :obj:`str` - reason: str - - def __init__(self, reason: str) -> None: - super().__init__() - self.reason = reason - - def __str__(self) -> str: - return self.reason - - -class GatewayClientClosedError(GatewayError): - """An exception raised when you programmatically shut down the bot client-side. - - Parameters - ---------- - reason : :obj:`str` - A string explaining the issue. - """ - - def __init__(self, reason: str = "The gateway client has been closed") -> None: - super().__init__(reason) - - -class GatewayServerClosedConnectionError(GatewayError): - """An exception raised when the server closes the connection. - - Parameters - ---------- - close_code : :obj:`hikari.net.codes.GatewayCloseCode`, :obj:`int`, optional - The close code provided by the server, if there was one. - reason : :obj:`str`, optional - A string explaining the issue. - """ - - close_code: typing.Union[codes.GatewayCloseCode, int, None] - - def __init__( - self, - close_code: typing.Optional[typing.Union[codes.GatewayCloseCode, int]] = None, - reason: typing.Optional[str] = None, - ) -> None: - if reason is None: - try: - name = close_code.name - except AttributeError: - name = str(close_code) if close_code is not None else "no reason" - - reason = f"Gateway connection closed by server ({name})" - - self.close_code = close_code - super().__init__(reason) - - -class GatewayInvalidTokenError(GatewayServerClosedConnectionError): - """An exception that is raised if you failed to authenticate with a valid token to the Gateway.""" - - def __init__(self) -> None: - super().__init__( - codes.GatewayCloseCode.AUTHENTICATION_FAILED, - "The account token specified is invalid for the gateway connection", - ) - - -class GatewayInvalidSessionError(GatewayServerClosedConnectionError): - """An exception raised if a Gateway session becomes invalid. - - Parameters - ---------- - can_resume : :obj:`bool` - ``True`` if the connection will be able to RESUME next time it starts rather - than re-IDENTIFYing, or ``False`` if you need to IDENTIFY again instead. - """ - - #: ``True``` if the next reconnection can be RESUMED. ``False``` if it has to be - #: coordinated by re-IDENFITYing. - #: - #: :type: :obj:`bool` - can_resume: bool - - def __init__(self, can_resume: bool) -> None: - self.can_resume = can_resume - instruction = "restart the shard and RESUME" if can_resume else "restart the shard with a fresh session" - super().__init__(reason=f"The session has been invalidated; {instruction}") - - -class GatewayMustReconnectError(GatewayServerClosedConnectionError): - """An exception raised when the Gateway has to re-connect with a new session. - - This will cause a re-IDENTIFY. - """ - - def __init__(self) -> None: - super().__init__(reason="The gateway server has requested that the client reconnects with a new session") - - -class GatewayNeedsShardingError(GatewayServerClosedConnectionError): - """An exception raised if you have too many guilds on one of the current Gateway shards. - - This is a sign you need to increase the number of shards that your bot is - running with in order to connect to Discord. - """ - - def __init__(self) -> None: - super().__init__( - codes.GatewayCloseCode.SHARDING_REQUIRED, "You are in too many guilds. Shard the bot to connect", - ) - - -class GatewayZombiedError(GatewayClientClosedError): - """An exception raised if a shard becomes zombied. - - This means that Discord is no longer responding to us, and we have - disconnected due to a timeout. - """ - - def __init__(self) -> None: - super().__init__("No heartbeat was received, the connection has been closed") - - -class HTTPError(errors.HikariError): - """Base exception raised if an HTTP error occurs. - - Parameters - ---------- - reason : :obj:`str` - A meaningful explanation of the problem. - """ - - #: A meaningful explanation of the problem. - #: - #: :type: :obj:`str` - reason: str - - def __init__(self, reason: str) -> None: - super().__init__() - self.reason = reason - - def __str__(self) -> str: - return self.reason - - -class CodedHTTPError(HTTPError): - """An HTTP exception that has contextual response information with it. - - Parameters - ---------- - status : :obj:`int` or :obj:`hikari.net.codes.HTTPStatusCode` - The HTTP status code that was returned by the server. - route : :obj:`hikari.net.routes.CompiledRoute` - The HTTP route that was being invoked when this exception occurred. - message : :obj:`str`, optional - An optional message if provided in the response payload. - json_code : :obj:`hikari.net.codes.JSONErrorCode`, :obj:`int`, optional - An optional error code the server provided us. - """ - - #: The HTTP status code that was returned by the server. - #: - #: :type: :obj:`int` or :obj:`hikari.net.codes.HTTPStatusCode` - status: typing.Union[int, codes.HTTPStatusCode] - - #: The HTTP route that was being invoked when this exception occurred. - #: - #: :type: :obj:`hikari.net.routes.CompiledRoute` - route: routes.CompiledRoute - - #: An optional contextual message the server provided us with in the - #: response body. - # - #: :type: :obj:`str`, optional - message: typing.Optional[str] - - #: An optional contextual error code the server provided us with in the - #: response body. - # - #: :type: :obj:`hikari.net.codes.JSONErrorCode` or :obj:`int`, optional - json_code: typing.Optional[typing.Union[codes.JSONErrorCode, int]] - - def __init__( - self, - status: typing.Union[int, codes.HTTPStatusCode], - route: routes.CompiledRoute, - message: typing.Optional[str], - json_code: typing.Union[codes.JSONErrorCode, int, None], - ) -> None: - super().__init__(str(status)) - self.status = status - self.route = route - self.message = message - self.json_code = json_code - - def __str__(self) -> str: - return f"{self.reason}: ({self.json_code}) {self.message}" - - -class ServerHTTPError(CodedHTTPError): - """An exception raised if a server-side error occurs when interacting with the REST API. - - If you get these, DO NOT PANIC! Your bot is working perfectly fine. Discord - have probably broken something again. - """ - - -class ClientHTTPError(CodedHTTPError): - """An exception raised if a server-side error occurs when interacting with the REST API. - - If you get one of these, you most likely have a mistake in your code, or - have found a bug with this library. - - If you are sure that your code is correct, please register a bug at - https://gitlab.com/nekokatt/hikari/issues and we will take a look for you. - """ - - -class BadRequestHTTPError(CodedHTTPError): - """A specific case of :obj:`CodedHTTPError`. - - This can occur hat occurs when you send Discord information in an unexpected - format, miss required information out, or give bad values for stuff. - - An example might be sending a message without any content, or an embed with - more than 6000 characters. - - Parameters - ---------- - route : :obj:`hikari.net.routes.CompiledRoute` - The HTTP route that was being invoked when this exception occurred. - message : :obj:`str`, optional - An optional message if provided in the response payload. - json_code : :obj:`hikari.net.codes.JSONErrorCode`, :obj:`int`, optional - An optional error code the server provided us. - """ - - def __init__( - self, - route: routes.CompiledRoute, - message: typing.Optional[str], - json_code: typing.Optional[typing.Union[codes.JSONErrorCode, int]], - ) -> None: - super().__init__(codes.HTTPStatusCode.BAD_REQUEST, route, message, json_code) - - -class UnauthorizedHTTPError(ClientHTTPError): - """A specific case of :obj:`ClientHTTPError`. - - This occurs when you have invalid authorization details to access - the given resource. - - This usually means that you have an incorrect token. - - Parameters - ---------- - route : :obj:`hikari.net.routes.CompiledRoute` - The HTTP route that was being invoked when this exception occurred. - message : :obj:`str`, optional - An optional message if provided in the response payload. - json_code : :obj:`hikari.net.codes.JSONErrorCode`, :obj:`int`, optional - An optional error code the server provided us. - """ - - def __init__( - self, - route: routes.CompiledRoute, - message: typing.Optional[str], - json_code: typing.Optional[typing.Union[codes.JSONErrorCode, int]], - ) -> None: - super().__init__(codes.HTTPStatusCode.UNAUTHORIZED, route, message, json_code) - - -class ForbiddenHTTPError(ClientHTTPError): - """A specific case of :obj:`ClientHTTPError`. - - This occurs when you are missing permissions, or are using an endpoint that - your account is not allowed to see without being whitelisted. - - This will not occur if your token is invalid. - - Parameters - ---------- - route : :obj:`hikari.net.routes.CompiledRoute` - The HTTP route that was being invoked when this exception occurred. - message : :obj:`str`, optional - An optional message if provided in the response payload. - json_code : :obj:`hikari.net.codes.JSONErrorCode`, :obj:`int`, optional - An optional error code the server provided us. - """ - - def __init__( - self, - route: routes.CompiledRoute, - message: typing.Optional[str], - json_code: typing.Optional[typing.Union[codes.JSONErrorCode, int]], - ) -> None: - super().__init__(codes.HTTPStatusCode.FORBIDDEN, route, message, json_code) - - -class NotFoundHTTPError(ClientHTTPError): - """A specific case of :obj:`ClientHTTPError`. - - This occurs when you try to refer to something that doesn't exist on Discord. - This might be referring to a user ID, channel ID, guild ID, etc that does - not exist, or it might be attempting to use an HTTP endpoint that is not - found. - - Parameters - ---------- - route : :obj:`hikari.net.routes.CompiledRoute` - The HTTP route that was being invoked when this exception occurred. - message : :obj:`str`, optional - An optional message if provided in the response payload. - json_code : :obj:`hikari.net.codes.JSONErrorCode`, :obj:`int`, optional - An optional error code the server provided us. - """ - - def __init__( - self, - route: routes.CompiledRoute, - message: typing.Optional[str], - json_code: typing.Optional[typing.Union[codes.JSONErrorCode, int]], - ) -> None: - super().__init__(codes.HTTPStatusCode.NOT_FOUND, route, message, json_code) diff --git a/hikari/net/rest.py b/hikari/net/rest.py index 0bc3db85e2..508d233db0 100644 --- a/hikari/net/rest.py +++ b/hikari/net/rest.py @@ -30,12 +30,12 @@ import aiohttp.typedefs +from hikari import errors from hikari.internal import assertions from hikari.internal import conversions from hikari.internal import more_collections from hikari.internal import more_logging from hikari.net import codes -from hikari.net import errors from hikari.net import ratelimits from hikari.net import routes from hikari.net import user_agent @@ -414,9 +414,9 @@ async def get_guild_audit_log( Raises ------ - :obj:`hikari.net.errors.ForbiddenHTTPError` + :obj:`hikari.errors.ForbiddenHTTPError` If you lack the given permissions to view an audit log. - :obj:`hikari.net.errors.NotFoundHTTPError` + :obj:`hikari.errors.NotFoundHTTPError` If the guild does not exist. """ query = {} @@ -441,9 +441,9 @@ async def get_channel(self, channel_id: str) -> typing.Dict[str, typing.Any]: Raises ------ - :obj:`hikari.net.errors.ForbiddenHTTPError` + :obj:`hikari.errors.ForbiddenHTTPError` If you don't have access to the channel. - :obj:`hikari.net.errors.NotFoundHTTPError` + :obj:`hikari.errors.NotFoundHTTPError` If the channel does not exist. """ route = routes.CHANNEL.compile(self.GET, channel_id=channel_id) @@ -512,11 +512,11 @@ async def modify_channel( # lgtm [py/similar-function] Raises ------ - :obj:`hikari.net.errors.NotFoundHTTPError` + :obj:`hikari.errors.NotFoundHTTPError` If the channel does not exist. - :obj:`hikari.net.errors.ForbiddenHTTPError` + :obj:`hikari.errors.ForbiddenHTTPError` If you lack the permission to make the change. - :obj:`hikari.net.errors.BadRequestHTTPError` + :obj:`hikari.errors.BadRequestHTTPError` If you provide incorrect options for the corresponding channel type (e.g. a ``bitrate`` for a text channel). """ @@ -549,9 +549,9 @@ async def delete_close_channel(self, channel_id: str) -> None: Raises ------ - :obj:`hikari.net.errors.NotFoundHTTPError` + :obj:`hikari.errors.NotFoundHTTPError` If the channel does not exist. - :obj:`hikari.net.errors.ForbiddenHTTPError` + :obj:`hikari.errors.ForbiddenHTTPError` If you do not have permission to delete the channel. Warning @@ -590,26 +590,26 @@ async def get_channel_messages( Raises ------ - :obj:`hikari.net.errors.ForbiddenHTTPError` + :obj:`hikari.errors.ForbiddenHTTPError` If you lack permission to read the channel. - :obj:`hikari.net.errors.BadRequestHTTPError` + :obj:`hikari.errors.BadRequestHTTPError` If your query is malformed, has an invalid value for ``limit``, or contains more than one of ``after``, ``before`` and ``around``. - :obj:`hikari.net.errors.NotFoundHTTPError` + :obj:`hikari.errors.NotFoundHTTPError` If the channel is not found, or the message provided for one of the filter arguments is not found. Note ---- If you are missing the ``VIEW_CHANNEL`` permission, you will receive a - :obj:`hikari.net.errors.ForbiddenHTTPError`. If you are instead missing + :obj:`hikari.errors.ForbiddenHTTPError`. If you are instead missing the ``READ_MESSAGE_HISTORY`` permission, you will always receive zero results, and thus an empty list will be returned instead. Warning ------- You can only specify a maximum of one from ``before``, ``after``, and ``around``. - Specifying more than one will cause a :obj:`hikari.net.errors.BadRequestHTTPError` to be raised. + Specifying more than one will cause a :obj:`hikari.errors.BadRequestHTTPError` to be raised. """ query = {} conversions.put_if_specified(query, "limit", limit) @@ -640,9 +640,9 @@ async def get_channel_message(self, channel_id: str, message_id: str) -> typing. Raises ------ - :obj:`hikari.net.errors.ForbiddenHTTPError` + :obj:`hikari.errors.ForbiddenHTTPError` If you lack permission to see the message. - :obj:`hikari.net.errors.NotFoundHTTPError` + :obj:`hikari.errors.NotFoundHTTPError` If the channel or message is not found. """ route = routes.CHANNEL_MESSAGE.compile(self.GET, channel_id=channel_id, message_id=message_id) @@ -691,9 +691,9 @@ async def create_message( Raises ------ - :obj:`hikari.net.errors.NotFoundHTTPError` + :obj:`hikari.errors.NotFoundHTTPError` If the channel is not found. - :obj:`hikari.net.errors.BadRequestHTTPError` + :obj:`hikari.errors.BadRequestHTTPError` This can be raised if the file is too large; if the embed exceeds the defined limits; if the message content is specified only and empty or greater than ``2000`` characters; if neither content, file @@ -701,7 +701,7 @@ async def create_message( fields in ``allowed_mentions``; if you specify to parse all users/roles mentions but also specify which users/roles to parse only. - :obj:`hikari.net.errors.ForbiddenHTTPError` + :obj:`hikari.errors.ForbiddenHTTPError` If you lack permissions to send to this channel. """ form = aiohttp.FormData() @@ -741,13 +741,13 @@ async def create_reaction(self, channel_id: str, message_id: str, emoji: str) -> Raises ------ - :obj:`hikari.net.errors.ForbiddenHTTPError` + :obj:`hikari.errors.ForbiddenHTTPError` If this is the first reaction using this specific emoji on this message and you lack the ``ADD_REACTIONS`` permission. If you lack ``READ_MESSAGE_HISTORY``, this may also raise this error. - :obj:`hikari.net.errors.NotFoundHTTPError` + :obj:`hikari.errors.NotFoundHTTPError` If the channel or message is not found, or if the emoji is not found. - :obj:`hikari.net.errors.BadRequestHTTPError` + :obj:`hikari.errors.BadRequestHTTPError` If the emoji is not valid, unknown, or formatted incorrectly. """ route = routes.OWN_REACTION.compile(self.PUT, channel_id=channel_id, message_id=message_id, emoji=emoji) @@ -769,9 +769,9 @@ async def delete_own_reaction(self, channel_id: str, message_id: str, emoji: str Raises ------ - :obj:`hikari.net.errors.ForbiddenHTTPError` + :obj:`hikari.errors.ForbiddenHTTPError` If you lack permission to do this. - :obj:`hikari.net.errors.NotFoundHTTPError` + :obj:`hikari.errors.NotFoundHTTPError` If the channel or message or emoji is not found. """ route = routes.OWN_REACTION.compile(self.DELETE, channel_id=channel_id, message_id=message_id, emoji=emoji) @@ -793,9 +793,9 @@ async def delete_all_reactions_for_emoji(self, channel_id: str, message_id: str, Raises ------ - :obj:`hikari.net.errors.NotFoundError` + :obj:`hikari.errors.NotFoundError` If the channel or message or emoji or user is not found. - :obj:`hikari.net.errors.ForbiddenError` + :obj:`hikari.errors.ForbiddenError` If you lack the ``MANAGE_MESSAGES`` permission, or are in DMs. """ route = routes.REACTION_EMOJI.compile(self.DELETE, channel_id=channel_id, message_id=message_id, emoji=emoji) @@ -819,9 +819,9 @@ async def delete_user_reaction(self, channel_id: str, message_id: str, emoji: st Raises ------ - :obj:`hikari.net.errors.NotFoundHTTPError` + :obj:`hikari.errors.NotFoundHTTPError` If the channel or message or emoji or user is not found. - :obj:`hikari.net.errors.ForbiddenHTTPError` + :obj:`hikari.errors.ForbiddenHTTPError` If you lack the ``MANAGE_MESSAGES`` permission, or are in DMs. """ route = routes.REACTION_EMOJI_USER.compile( @@ -859,9 +859,9 @@ async def get_reactions( Raises ------ - :obj:`hikari.net.errors.ForbiddenHTTPError` + :obj:`hikari.errors.ForbiddenHTTPError` If you lack access to the message. - :obj:`hikari.net.errors.NotFoundHTTPError` + :obj:`hikari.errors.NotFoundHTTPError` If the channel or message is not found. """ query = {} @@ -882,9 +882,9 @@ async def delete_all_reactions(self, channel_id: str, message_id: str) -> None: Raises ------ - :obj:`hikari.net.errors.NotFoundHTTPError` + :obj:`hikari.errors.NotFoundHTTPError` If the channel or message is not found. - :obj:`hikari.net.errors.ForbiddenHTTPError` + :obj:`hikari.errors.ForbiddenHTTPError` If you lack the ``MANAGE_MESSAGES`` permission. """ route = routes.ALL_REACTIONS.compile(self.DELETE, channel_id=channel_id, message_id=message_id) @@ -923,15 +923,15 @@ async def edit_message( Raises ------ - :obj:`hikari.net.errors.NotFoundHTTPError` + :obj:`hikari.errors.NotFoundHTTPError` If the channel or message is not found. - :obj:`hikari.net.errors.BadRequestHTTPError` + :obj:`hikari.errors.BadRequestHTTPError` This can be raised if the embed exceeds the defined limits; if the message content is specified only and empty or greater than ``2000`` characters; if neither content, file or embed are specified. parse only. - :obj:`hikari.net.errors.ForbiddenHTTPError` + :obj:`hikari.errors.ForbiddenHTTPError` If you try to edit content or embed on a message you did not author or try to edit the flags on a message you did not author without the ``MANAGE_MESSAGES`` permission. """ @@ -954,10 +954,10 @@ async def delete_message(self, channel_id: str, message_id: str) -> None: Raises ------ - :obj:`hikari.net.errors.ForbiddenHTTPError` + :obj:`hikari.errors.ForbiddenHTTPError` If you did not author the message and are in a DM, or if you did not author the message and lack the ``MANAGE_MESSAGES`` permission in a guild channel. - :obj:`hikari.net.errors.NotFoundHTTPError` + :obj:`hikari.errors.NotFoundHTTPError` If the channel or message is not found. """ route = routes.CHANNEL_MESSAGE.compile(self.DELETE, channel_id=channel_id, message_id=message_id) @@ -975,11 +975,11 @@ async def bulk_delete_messages(self, channel_id: str, messages: typing.Sequence[ Raises ------ - :obj:`hikari.net.errors.NotFoundHTTPError` + :obj:`hikari.errors.NotFoundHTTPError` If the channel is not found. - :obj:`hikari.net.errors.ForbiddenHTTPError` + :obj:`hikari.errors.ForbiddenHTTPError` If you lack the ``MANAGE_MESSAGES`` permission in the channel. - :obj:`hikari.net.errors.BadRequestHTTPError` + :obj:`hikari.errors.BadRequestHTTPError` If any of the messages passed are older than ``2`` weeks in age or any duplicate message IDs are passed. @@ -1026,9 +1026,9 @@ async def edit_channel_permissions( Raises ------ - :obj:`hikari.net.errors.NotFoundHTTPError` + :obj:`hikari.errors.NotFoundHTTPError` If the target channel or overwrite doesn't exist. - :obj:`hikari.net.errors.ForbiddenHTTPError` + :obj:`hikari.errors.ForbiddenHTTPError` If you lack permission to do this. """ payload = {} @@ -1053,9 +1053,9 @@ async def get_channel_invites(self, channel_id: str) -> typing.Sequence[typing.D Raises ------ - :obj:`hikari.net.errors.ForbiddenHTTPError` + :obj:`hikari.errors.ForbiddenHTTPError` If you lack the ``MANAGE_CHANNELS`` permission. - :obj:`hikari.net.errors.NotFoundHTTPError` + :obj:`hikari.errors.NotFoundHTTPError` If the channel does not exist. """ route = routes.CHANNEL_INVITES.compile(self.GET, channel_id=channel_id) @@ -1106,11 +1106,11 @@ async def create_channel_invite( Raises ------ - :obj:`hikari.net.errors.ForbiddenHTTPError` + :obj:`hikari.errors.ForbiddenHTTPError` If you lack the ``CREATE_INSTANT_MESSAGES`` permission. - :obj:`hikari.net.errors.NotFoundHTTPError` + :obj:`hikari.errors.NotFoundHTTPError` If the channel does not exist. - :obj:`hikari.net.errors.BadRequestHTTPError` + :obj:`hikari.errors.BadRequestHTTPError` If the arguments provided are not valid (e.g. negative age, etc). """ payload = {} @@ -1135,9 +1135,9 @@ async def delete_channel_permission(self, channel_id: str, overwrite_id: str) -> Raises ------ - :obj:`hikari.net.errors.NotFoundHTTPError` + :obj:`hikari.errors.NotFoundHTTPError` If the overwrite or channel do not exist. - :obj:`hikari.net.errors.ForbiddenHTTPError` + :obj:`hikari.errors.ForbiddenHTTPError` If you lack the ``MANAGE_ROLES`` permission for that channel. """ route = routes.CHANNEL_PERMISSIONS.compile(self.DELETE, channel_id=channel_id, overwrite_id=overwrite_id) @@ -1154,9 +1154,9 @@ async def trigger_typing_indicator(self, channel_id: str) -> None: Raises ------ - :obj:`hikari.net.errors.NotFoundHTTPError` + :obj:`hikari.errors.NotFoundHTTPError` If the channel is not found. - :obj:`hikari.net.errors.ForbiddenHTTPError` + :obj:`hikari.errors.ForbiddenHTTPError` If you are not able to type in the channel. """ route = routes.CHANNEL_TYPING.compile(self.POST, channel_id=channel_id) @@ -1177,9 +1177,9 @@ async def get_pinned_messages(self, channel_id: str) -> typing.Sequence[typing.D Raises ------ - :obj:`hikari.net.errors.NotFoundHTTPError` + :obj:`hikari.errors.NotFoundHTTPError` If the channel is not found. - :obj:`hikari.net.errors.ForbiddenHTTPError` + :obj:`hikari.errors.ForbiddenHTTPError` If you are not able to see the channel. Note @@ -1202,9 +1202,9 @@ async def add_pinned_channel_message(self, channel_id: str, message_id: str) -> Raises ------ - :obj:`hikari.net.errors.ForbiddenHTTPError` + :obj:`hikari.errors.ForbiddenHTTPError` If you lack the ``MANAGE_MESSAGES`` permission. - :obj:`hikari.net.errors.NotFoundHTTPError` + :obj:`hikari.errors.NotFoundHTTPError` If the message or channel do not exist. """ route = routes.CHANNEL_PINS.compile(self.PUT, channel_id=channel_id, message_id=message_id) @@ -1224,9 +1224,9 @@ async def delete_pinned_channel_message(self, channel_id: str, message_id: str) Raises ------ - :obj:`hikari.net.errors.ForbiddenHTTPError` + :obj:`hikari.errors.ForbiddenHTTPError` If you lack the ``MANAGE_MESSAGES`` permission. - :obj:`hikari.net.errors.NotFoundHTTPError` + :obj:`hikari.errors.NotFoundHTTPError` If the message or channel do not exist. """ route = routes.CHANNEL_PIN.compile(self.DELETE, channel_id=channel_id, message_id=message_id) @@ -1247,9 +1247,9 @@ async def list_guild_emojis(self, guild_id: str) -> typing.Sequence[typing.Dict[ Raises ------ - :obj:`hikari.net.errors.NotFoundHTTPError` + :obj:`hikari.errors.NotFoundHTTPError` If the guild is not found. - :obj:`hikari.net.errors.ForbiddenHTTPError` + :obj:`hikari.errors.ForbiddenHTTPError` If you aren't a member of the guild. """ route = routes.GUILD_EMOJIS.compile(self.GET, guild_id=guild_id) @@ -1272,9 +1272,9 @@ async def get_guild_emoji(self, guild_id: str, emoji_id: str) -> typing.Dict[str Raises ------ - :obj:`hikari.net.errors.NotFoundHTTPError` + :obj:`hikari.errors.NotFoundHTTPError` If either the guild or the emoji aren't found. - :obj:`hikari.net.errors.ForbiddenHTTPError` + :obj:`hikari.errors.ForbiddenHTTPError` If you aren't a member of said guild. """ route = routes.GUILD_EMOJI.compile(self.GET, guild_id=guild_id, emoji_id=emoji_id) @@ -1309,11 +1309,11 @@ async def create_guild_emoji( ------ :obj:`ValueError` If ``image`` is ``None``. - :obj:`hikari.net.errors.NotFoundHTTPError` + :obj:`hikari.errors.NotFoundHTTPError` If the guild is not found. - :obj:`hikari.net.errors.ForbiddenHTTPError` + :obj:`hikari.errors.ForbiddenHTTPError` If you either lack the ``MANAGE_EMOJIS`` permission or aren't a member of said guild. - :obj:`hikari.net.errors.BadRequestHTTPError` + :obj:`hikari.errors.BadRequestHTTPError` If you attempt to upload an image larger than ``256kb``, an empty image or an invalid image format. """ assertions.assert_not_none(image, "image must be a valid image") @@ -1353,9 +1353,9 @@ async def modify_guild_emoji( Raises ------ - :obj:`hikari.net.errors.NotFoundHTTPError` + :obj:`hikari.errors.NotFoundHTTPError` If either the guild or the emoji aren't found. - :obj:`hikari.net.errors.ForbiddenHTTPError` + :obj:`hikari.errors.ForbiddenHTTPError` If you either lack the ``MANAGE_EMOJIS`` permission or are not a member of the given guild. """ payload = {} @@ -1376,9 +1376,9 @@ async def delete_guild_emoji(self, guild_id: str, emoji_id: str) -> None: Raises ------ - :obj:`hikari.net.errors.NotFoundHTTPError` + :obj:`hikari.errors.NotFoundHTTPError` If either the guild or the emoji aren't found. - :obj:`hikari.net.errors.ForbiddenHTTPError` + :obj:`hikari.errors.ForbiddenHTTPError` If you either lack the ``MANAGE_EMOJIS`` permission or aren't a member of said guild. """ route = routes.GUILD_EMOJI.compile(self.DELETE, guild_id=guild_id, emoji_id=emoji_id) @@ -1430,9 +1430,9 @@ async def create_guild( Raises ------ - :obj:`hikari.net.errors.ForbiddenHTTPError` + :obj:`hikari.errors.ForbiddenHTTPError` If you are on ``10`` or more guilds. - :obj:`hikari.net.errors.BadRequestHTTPError` + :obj:`hikari.errors.BadRequestHTTPError` If you provide unsupported fields like ``parent_id`` in channel objects. """ payload = {"name": name} @@ -1461,9 +1461,9 @@ async def get_guild(self, guild_id: str) -> typing.Dict[str, typing.Any]: Raises ------ - :obj:`hikari.net.errors.NotFoundHTTPError` + :obj:`hikari.errors.NotFoundHTTPError` If the guild is not found. - :obj:`hikari.net.errors.ForbiddenHTTPError` + :obj:`hikari.errors.ForbiddenHTTPError` If you don't have access to the guild. """ route = routes.GUILD.compile(self.GET, guild_id=guild_id) @@ -1527,9 +1527,9 @@ async def modify_guild( # lgtm [py/similar-function] Raises ------ - :obj:`hikari.net.errors.NotFoundHTTPError` + :obj:`hikari.errors.NotFoundHTTPError` If the guild is not found. - :obj:`hikari.net.errors.ForbiddenHTTPError` + :obj:`hikari.errors.ForbiddenHTTPError` If you lack the ``MANAGE_GUILD`` permission or are not in the guild. """ payload = {} @@ -1561,9 +1561,9 @@ async def delete_guild(self, guild_id: str) -> None: Raises ------ - :obj:`hikari.net.errors.NotFoundHTTPError` + :obj:`hikari.errors.NotFoundHTTPError` If the guild is not found. - :obj:`hikari.net.errors.ForbiddenHTTPError` + :obj:`hikari.errors.ForbiddenHTTPError` If you are not the guild owner. """ route = routes.GUILD.compile(self.DELETE, guild_id=guild_id) @@ -1584,9 +1584,9 @@ async def get_guild_channels(self, guild_id: str) -> typing.Sequence[typing.Dict Raises ------ - :obj:`hikari.net.errors.NotFoundHTTPError` + :obj:`hikari.errors.NotFoundHTTPError` If the guild is not found. - :obj:`hikari.net.errors.ForbiddenHTTPError` + :obj:`hikari.errors.ForbiddenHTTPError` If you are not in the guild. """ route = routes.GUILD_CHANNELS.compile(self.GET, guild_id=guild_id) @@ -1658,11 +1658,11 @@ async def create_guild_channel( Raises ------ - :obj:`hikari.net.errors.NotFoundHTTPError` + :obj:`hikari.errors.NotFoundHTTPError` If the guild is not found. - :obj:`hikari.net.errors.ForbiddenHTTPError` + :obj:`hikari.errors.ForbiddenHTTPError` If you lack the ``MANAGE_CHANNEL`` permission or are not in the guild. - :obj:`hikari.net.errors.BadRequestHTTPError` + :obj:`hikari.errors.BadRequestHTTPError` If you provide incorrect options for the corresponding channel type (e.g. a ``bitrate`` for a text channel). """ @@ -1697,12 +1697,12 @@ async def modify_guild_channel_positions( Raises ------ - :obj:`hikari.net.errors.NotFoundHTTPError` + :obj:`hikari.errors.NotFoundHTTPError` If either the guild or any of the channels aren't found. - :obj:`hikari.net.errors.ForbiddenHTTPError` + :obj:`hikari.errors.ForbiddenHTTPError` If you either lack the ``MANAGE_CHANNELS`` permission or are not a member of said guild or are not in the guild. - :obj:`hikari.net.errors.BadRequestHTTPError` + :obj:`hikari.errors.BadRequestHTTPError` If you provide anything other than the ``id`` and ``position`` fields for the channels. """ payload = [{"id": ch[0], "position": ch[1]} for ch in (channel, *channels)] @@ -1726,9 +1726,9 @@ async def get_guild_member(self, guild_id: str, user_id: str) -> typing.Dict[str Raises ------ - :obj:`hikari.net.errors.NotFoundHTTPError` + :obj:`hikari.errors.NotFoundHTTPError` If either the guild or the member aren't found. - :obj:`hikari.net.errors.ForbiddenHTTPError` + :obj:`hikari.errors.ForbiddenHTTPError` If you don't have access to the target guild. """ route = routes.GUILD_MEMBER.compile(self.GET, guild_id=guild_id, user_id=user_id) @@ -1773,11 +1773,11 @@ async def list_guild_members( Raises ------ - :obj:`hikari.net.errors.NotFoundHTTPError` + :obj:`hikari.errors.NotFoundHTTPError` If the guild is not found. - :obj:`hikari.net.errors.ForbiddenHTTPError` + :obj:`hikari.errors.ForbiddenHTTPError` If you are not in the guild. - :obj:`hikari.net.errors.BadRequestHTTPError` + :obj:`hikari.errors.BadRequestHTTPError` If you provide invalid values for the ``limit`` or `after`` fields. """ query = {} @@ -1824,14 +1824,14 @@ async def modify_guild_member( # lgtm [py/similar-function] Raises ------ - :obj:`hikari.net.errors.NotFoundHTTPError` + :obj:`hikari.errors.NotFoundHTTPError` If either the guild, user, channel or any of the roles aren't found. - :obj:`hikari.net.errors.ForbiddenHTTPError` + :obj:`hikari.errors.ForbiddenHTTPError` If you lack any of the applicable permissions (``MANAGE_NICKNAMES``, ``MANAGE_ROLES``, ``MUTE_MEMBERS``, ``DEAFEN_MEMBERS`` or ``MOVE_MEMBERS``). Note that to move a member you must also have permission to connect to the end channel. This will also be raised if you're not in the guild. - :obj:`hikari.net.errors.BadRequestHTTPError` + :obj:`hikari.errors.BadRequestHTTPError` If you pass ```mute``, ``deaf`` or ``channel_id`` while the member is not connected to a voice channel. """ payload = {} @@ -1858,11 +1858,11 @@ async def modify_current_user_nick(self, guild_id: str, nick: typing.Optional[st Raises ------ - :obj:`hikari.net.errors.NotFoundHTTPError` + :obj:`hikari.errors.NotFoundHTTPError` If the guild is not found. - :obj:`hikari.net.errors.ForbiddenHTTPError` + :obj:`hikari.errors.ForbiddenHTTPError` If you lack the ``CHANGE_NICKNAME`` permission or are not in the guild. - :obj:`hikari.net.errors.BadRequestHTTPError` + :obj:`hikari.errors.BadRequestHTTPError` If you provide a disallowed nickname, one that is too long, or one that is empty. """ payload = {"nick": nick} @@ -1886,9 +1886,9 @@ async def add_guild_member_role(self, guild_id: str, user_id: str, role_id: str, Raises ------ - :obj:`hikari.net.errors.NotFoundHTTPError` + :obj:`hikari.errors.NotFoundHTTPError` If either the guild, member or role aren't found. - :obj:`hikari.net.errors.ForbiddenHTTPError` + :obj:`hikari.errors.ForbiddenHTTPError` If you lack the ``MANAGE_ROLES`` permission or are not in the guild. """ route = routes.GUILD_MEMBER_ROLE.compile(self.PUT, guild_id=guild_id, user_id=user_id, role_id=role_id) @@ -1911,9 +1911,9 @@ async def remove_guild_member_role(self, guild_id: str, user_id: str, role_id: s Raises ------ - :obj:`hikari.net.errors.NotFoundHTTPError` + :obj:`hikari.errors.NotFoundHTTPError` If either the guild, member or role aren't found. - :obj:`hikari.net.errors.ForbiddenHTTPError` + :obj:`hikari.errors.ForbiddenHTTPError` If you lack the ``MANAGE_ROLES`` permission or are not in the guild. """ route = routes.GUILD_MEMBER_ROLE.compile(self.DELETE, guild_id=guild_id, user_id=user_id, role_id=role_id) @@ -1934,9 +1934,9 @@ async def remove_guild_member(self, guild_id: str, user_id: str, *, reason: str Raises ------ - :obj:`hikari.net.errors.NotFoundHTTPError` + :obj:`hikari.errors.NotFoundHTTPError` If either the guild or member aren't found. - :obj:`hikari.net.errors.ForbiddenHTTPError` + :obj:`hikari.errors.ForbiddenHTTPError` If you lack the ``KICK_MEMBERS`` permission or are not in the guild. """ route = routes.GUILD_MEMBER.compile(self.DELETE, guild_id=guild_id, user_id=user_id) @@ -1957,9 +1957,9 @@ async def get_guild_bans(self, guild_id: str) -> typing.Sequence[typing.Dict[str Raises ------ - :obj:`hikari.net.errors.NotFoundHTTPError` + :obj:`hikari.errors.NotFoundHTTPError` If the guild is not found. - :obj:`hikari.net.errors.ForbiddenHTTPError` + :obj:`hikari.errors.ForbiddenHTTPError` If you lack the ``BAN_MEMBERS`` permission or are not in the guild. """ route = routes.GUILD_BANS.compile(self.GET, guild_id=guild_id) @@ -1982,9 +1982,9 @@ async def get_guild_ban(self, guild_id: str, user_id: str) -> typing.Dict[str, t Raises ------ - :obj:`hikari.net.errors.NotFoundHTTPError` + :obj:`hikari.errors.NotFoundHTTPError` If either the guild or the user aren't found, or if the user is not banned. - :obj:`hikari.net.errors.ForbiddenHTTPError` + :obj:`hikari.errors.ForbiddenHTTPError` If you lack the ``BAN_MEMBERS`` permission or are not in the guild. """ route = routes.GUILD_BAN.compile(self.GET, guild_id=guild_id, user_id=user_id) @@ -2010,9 +2010,9 @@ async def create_guild_ban( Raises ------ - :obj:`hikari.net.errors.NotFoundHTTPError` + :obj:`hikari.errors.NotFoundHTTPError` If either the guild or member aren't found. - :obj:`hikari.net.errors.ForbiddenHTTPError` + :obj:`hikari.errors.ForbiddenHTTPError` If you lack the ``BAN_MEMBERS`` permission or are not in the guild. """ query = {} @@ -2036,9 +2036,9 @@ async def remove_guild_ban(self, guild_id: str, user_id: str, *, reason: str = . Raises ------ - :obj:`hikari.net.errors.NotFoundHTTPError` + :obj:`hikari.errors.NotFoundHTTPError` If either the guild or member aren't found, or the member is not banned. - :obj:`hikari.net.errors.ForbiddenHTTPError` + :obj:`hikari.errors.ForbiddenHTTPError` If you lack the ``BAN_MEMBERS`` permission or are not a in the guild. """ route = routes.GUILD_BAN.compile(self.DELETE, guild_id=guild_id, user_id=user_id) @@ -2059,9 +2059,9 @@ async def get_guild_roles(self, guild_id: str) -> typing.Sequence[typing.Dict[st Raises ------ - :obj:`hikari.net.errors.NotFoundHTTPError` + :obj:`hikari.errors.NotFoundHTTPError` If the guild is not found. - :obj:`hikari.net.errors.ForbiddenHTTPError` + :obj:`hikari.errors.ForbiddenHTTPError` If you're not in the guild. """ route = routes.GUILD_ROLES.compile(self.GET, guild_id=guild_id) @@ -2105,11 +2105,11 @@ async def create_guild_role( Raises ------ - :obj:`hikari.net.errors.NotFoundHTTPError` + :obj:`hikari.errors.NotFoundHTTPError` If the guild is not found. - :obj:`hikari.net.errors.ForbiddenHTTPError` + :obj:`hikari.errors.ForbiddenHTTPError` If you lack the ``MANAGE_ROLES`` permission or you're not in the guild. - :obj:`hikari.net.errors.BadRequestHTTPError` + :obj:`hikari.errors.BadRequestHTTPError` If you provide invalid values for the role attributes. """ payload = {} @@ -2142,11 +2142,11 @@ async def modify_guild_role_positions( Raises ------ - :obj:`hikari.net.errors.NotFoundHTTPError` + :obj:`hikari.errors.NotFoundHTTPError` If either the guild or any of the roles aren't found. - :obj:`hikari.net.errors.ForbiddenHTTPError` + :obj:`hikari.errors.ForbiddenHTTPError` If you lack the ``MANAGE_ROLES`` permission or you're not in the guild. - :obj:`hikari.net.errors.BadRequestHTTPError` + :obj:`hikari.errors.BadRequestHTTPError` If you provide invalid values for the `position` fields. """ payload = [{"id": r[0], "position": r[1]} for r in (role, *roles)] @@ -2194,11 +2194,11 @@ async def modify_guild_role( # lgtm [py/similar-function] Raises ------ - :obj:`hikari.net.errors.NotFoundHTTPError` + :obj:`hikari.errors.NotFoundHTTPError` If either the guild or role aren't found. - :obj:`hikari.net.errors.ForbiddenHTTPError` + :obj:`hikari.errors.ForbiddenHTTPError` If you lack the ``MANAGE_ROLES`` permission or you're not in the guild. - :obj:`hikari.net.errors.BadRequestHTTPError` + :obj:`hikari.errors.BadRequestHTTPError` If you provide invalid values for the role attributes. """ payload = {} @@ -2222,9 +2222,9 @@ async def delete_guild_role(self, guild_id: str, role_id: str) -> None: Raises ------ - :obj:`hikari.net.errors.NotFoundHTTPError` + :obj:`hikari.errors.NotFoundHTTPError` If either the guild or the role aren't found. - :obj:`hikari.net.errors.ForbiddenHTTPError` + :obj:`hikari.errors.ForbiddenHTTPError` If you lack the ``MANAGE_ROLES`` permission or are not in the guild. """ route = routes.GUILD_ROLE.compile(self.DELETE, guild_id=guild_id, role_id=role_id) @@ -2247,11 +2247,11 @@ async def get_guild_prune_count(self, guild_id: str, days: int) -> int: Raises ------ - :obj:`hikari.net.errors.NotFoundHTTPError` + :obj:`hikari.errors.NotFoundHTTPError` If the guild is not found. - :obj:`hikari.net.errors.ForbiddenHTTPError` + :obj:`hikari.errors.ForbiddenHTTPError` If you lack the ``KICK_MEMBERS`` or you are not in the guild. - :obj:`hikari.net.errors.BadRequestHTTPError` + :obj:`hikari.errors.BadRequestHTTPError` If you pass an invalid amount of days. """ payload = {"days": days} @@ -2285,11 +2285,11 @@ async def begin_guild_prune( Raises ------ - :obj:`hikari.net.errors.NotFoundHTTPError` + :obj:`hikari.errors.NotFoundHTTPError` If the guild is not found: - :obj:`hikari.net.errors.ForbiddenHTTPError` + :obj:`hikari.errors.ForbiddenHTTPError` If you lack the ``KICK_MEMBER`` permission or are not in the guild. - :obj:`hikari.net.errors.BadRequestHTTPError` + :obj:`hikari.errors.BadRequestHTTPError` If you provide invalid values for the ``days`` or ``compute_prune_count`` fields. """ query = {"days": days} @@ -2317,9 +2317,9 @@ async def get_guild_voice_regions(self, guild_id: str) -> typing.Sequence[typing Raises ------ - :obj:`hikari.net.errors.NotFoundHTTPError` + :obj:`hikari.errors.NotFoundHTTPError` If the guild is not found. - :obj:`hikari.net.errors.ForbiddenHTTPError` + :obj:`hikari.errors.ForbiddenHTTPError` If you are not in the guild. """ route = routes.GUILD_VOICE_REGIONS.compile(self.GET, guild_id=guild_id) @@ -2340,9 +2340,9 @@ async def get_guild_invites(self, guild_id: str) -> typing.Sequence[typing.Dict[ Raises ------ - :obj:`hikari.net.errors.NotFoundHTTPError` + :obj:`hikari.errors.NotFoundHTTPError` If the guild is not found. - :obj:`hikari.net.errors.ForbiddenHTTPError` + :obj:`hikari.errors.ForbiddenHTTPError` If you lack the ``MANAGE_GUILD`` permission or are not in the guild. """ route = routes.GUILD_INVITES.compile(self.GET, guild_id=guild_id) @@ -2363,9 +2363,9 @@ async def get_guild_integrations(self, guild_id: str) -> typing.Sequence[typing. Raises ------ - :obj:`hikari.net.errors.NotFoundHTTPError` + :obj:`hikari.errors.NotFoundHTTPError` If the guild is not found. - :obj:`hikari.net.errors.ForbiddenHTTPError` + :obj:`hikari.errors.ForbiddenHTTPError` If you lack the ``MANAGE_GUILD`` permission or are not in the guild. """ route = routes.GUILD_INTEGRATIONS.compile(self.GET, guild_id=guild_id) @@ -2395,9 +2395,9 @@ async def create_guild_integration( Raises ------ - :obj:`hikari.net.errors.NotFoundHTTPError` + :obj:`hikari.errors.NotFoundHTTPError` If the guild is not found. - :obj:`hikari.net.errors.ForbiddenHTTPError` + :obj:`hikari.errors.ForbiddenHTTPError` If you lack the ``MANAGE_GUILD`` permission or are not in the guild. """ payload = {"type": type_, "id": integration_id} @@ -2436,9 +2436,9 @@ async def modify_guild_integration( Raises ------ - :obj:`hikari.net.errors.NotFoundHTTPError` + :obj:`hikari.errors.NotFoundHTTPError` If either the guild or the integration aren't found. - :obj:`hikari.net.errors.ForbiddenHTTPError` + :obj:`hikari.errors.ForbiddenHTTPError` If you lack the ``MANAGE_GUILD`` permission or are not in the guild. """ payload = {} @@ -2464,9 +2464,9 @@ async def delete_guild_integration(self, guild_id: str, integration_id: str, *, Raises ------ - :obj:`hikari.net.errors.NotFoundHTTPError` + :obj:`hikari.errors.NotFoundHTTPError` If either the guild or the integration aren't found. - :obj:`hikari.net.errors.ForbiddenHTTPError` + :obj:`hikari.errors.ForbiddenHTTPError` If you lack the `MANAGE_GUILD` permission or are not in the guild. """ route = routes.GUILD_INTEGRATION.compile(self.DELETE, guild_id=guild_id, integration_id=integration_id) @@ -2484,9 +2484,9 @@ async def sync_guild_integration(self, guild_id: str, integration_id: str) -> No Raises ------ - :obj:`hikari.net.errors.NotFoundHTTPError` + :obj:`hikari.errors.NotFoundHTTPError` If either the guild or the integration aren't found. - :obj:`hikari.net.errors.ForbiddenHTTPError` + :obj:`hikari.errors.ForbiddenHTTPError` If you lack the ``MANAGE_GUILD`` permission or are not in the guild. """ route = routes.GUILD_INTEGRATION_SYNC.compile(self.POST, guild_id=guild_id, integration_id=integration_id) @@ -2507,9 +2507,9 @@ async def get_guild_embed(self, guild_id: str) -> typing.Dict[str, typing.Any]: Raises ------ - :obj:`hikari.net.errors.NotFoundHTTPError` + :obj:`hikari.errors.NotFoundHTTPError` If the guild is not found. - :obj:`hikari.net.errors.ForbiddenHTTPError` + :obj:`hikari.errors.ForbiddenHTTPError` If you either lack the ``MANAGE_GUILD`` permission or are not in the guild. """ route = routes.GUILD_EMBED.compile(self.GET, guild_id=guild_id) @@ -2537,9 +2537,9 @@ async def modify_guild_embed( Raises ------ - :obj:`hikari.net.errors.NotFoundHTTPError` + :obj:`hikari.errors.NotFoundHTTPError` If the guild is not found. - :obj:`hikari.net.errors.ForbiddenHTTPError` + :obj:`hikari.errors.ForbiddenHTTPError` If you either lack the ``MANAGE_GUILD`` permission or are not in the guild. """ route = routes.GUILD_EMBED.compile(self.PATCH, guild_id=guild_id) @@ -2560,9 +2560,9 @@ async def get_guild_vanity_url(self, guild_id: str) -> typing.Dict[str, typing.A Raises ------ - :obj:`hikari.net.errors.NotFoundHTTPError` + :obj:`hikari.errors.NotFoundHTTPError` If the guild is not found. - :obj:`hikari.net.errors.ForbiddenHTTPError` + :obj:`hikari.errors.ForbiddenHTTPError` If you either lack the ``MANAGE_GUILD`` permission or are not in the guild. """ route = routes.GUILD_VANITY_URL.compile(self.GET, guild_id=guild_id) @@ -2613,7 +2613,7 @@ async def get_invite(self, invite_code: str, *, with_counts: bool = ...) -> typi Raises ------ - :obj:`hikari.net.errors.NotFoundHTTPError` + :obj:`hikari.errors.NotFoundHTTPError` If the invite is not found. """ query = {} @@ -2637,9 +2637,9 @@ async def delete_invite(self, invite_code: str) -> None: Raises ------ - :obj:`hikari.net.errors.NotFoundHTTPError` + :obj:`hikari.errors.NotFoundHTTPError` If the invite is not found. - :obj:`hikari.net.errors.ForbiddenHTTPError` + :obj:`hikari.errors.ForbiddenHTTPError` If you lack either ``MANAGE_CHANNELS`` on the channel the invite belongs to or ``MANAGE_GUILD`` for guild-global delete. """ @@ -2672,7 +2672,7 @@ async def get_user(self, user_id: str) -> typing.Dict[str, typing.Any]: Raises ------ - :obj:`hikari.net.errors.NotFoundHTTPError` + :obj:`hikari.errors.NotFoundHTTPError` If the user is not found. """ route = routes.USER.compile(self.GET, user_id=user_id) @@ -2698,7 +2698,7 @@ async def modify_current_user( Raises ------ - :obj:`hikari.net.errors.BadRequestHTTPError` + :obj:`hikari.errors.BadRequestHTTPError` If you pass username longer than the limit (``2-32``) or an invalid image. """ payload = {} @@ -2746,7 +2746,7 @@ async def get_current_user_guilds( Raises ------ - :obj:`hikari.net.errors.BadRequestHTTPError` + :obj:`hikari.errors.BadRequestHTTPError` If you pass both ``before`` and ``after`` or an invalid value for ``limit``. """ @@ -2767,7 +2767,7 @@ async def leave_guild(self, guild_id: str) -> None: Raises ------ - :obj:`hikari.net.errors.NotFoundHTTPError` + :obj:`hikari.errors.NotFoundHTTPError` If the guild is not found. """ route = routes.LEAVE_GUILD.compile(self.DELETE, guild_id=guild_id) @@ -2788,7 +2788,7 @@ async def create_dm(self, recipient_id: str) -> typing.Dict[str, typing.Any]: Raises ------ - :obj:`hikari.net.errors.NotFoundHTTPError` + :obj:`hikari.errors.NotFoundHTTPError` If the recipient is not found. """ payload = {"recipient_id": recipient_id} @@ -2834,12 +2834,12 @@ async def create_webhook( Raises ------ - :obj:`hikari.net.errors.NotFoundHTTPError` + :obj:`hikari.errors.NotFoundHTTPError` If the channel is not found. - :obj:`hikari.net.errors.ForbiddenHTTPError` + :obj:`hikari.errors.ForbiddenHTTPError` If you either lack the ``MANAGE_WEBHOOKS`` permission or can not see the given channel. - :obj:`hikari.net.errors.BadRequestHTTPError` + :obj:`hikari.errors.BadRequestHTTPError` If the avatar image is too big or the format is invalid. """ payload = {"name": name} @@ -2862,9 +2862,9 @@ async def get_channel_webhooks(self, channel_id: str) -> typing.Sequence[typing. Raises ------ - :obj:`hikari.net.errors.NotFoundHTTPError` + :obj:`hikari.errors.NotFoundHTTPError` If the channel is not found. - :obj:`hikari.net.errors.ForbiddenHTTPError` + :obj:`hikari.errors.ForbiddenHTTPError` If you either lack the ``MANAGE_WEBHOOKS`` permission or can not see the given channel. """ @@ -2886,9 +2886,9 @@ async def get_guild_webhooks(self, guild_id: str) -> typing.Sequence[typing.Dict Raises ------ - :obj:`hikari.net.errors.NotFoundHTTPError` + :obj:`hikari.errors.NotFoundHTTPError` If the guild is not found. - :obj:`hikari.net.errors.ForbiddenHTTPError` + :obj:`hikari.errors.ForbiddenHTTPError` If you either lack the ``MANAGE_WEBHOOKS`` permission or aren't a member of the given guild. """ @@ -2912,12 +2912,12 @@ async def get_webhook(self, webhook_id: str, *, webhook_token: str = ...) -> typ Raises ------ - :obj:`hikari.net.errors.NotFoundHTTPError` + :obj:`hikari.errors.NotFoundHTTPError` If the webhook is not found. - :obj:`hikari.net.errors.ForbiddenHTTPError` + :obj:`hikari.errors.ForbiddenHTTPError` If you're not in the guild that owns this webhook or lack the ``MANAGE_WEBHOOKS`` permission. - :obj:`hikari.net.errors.UnauthorizedHTTPError` + :obj:`hikari.errors.UnauthorizedHTTPError` If you pass a token that's invalid for the target webhook. """ if webhook_token is ...: @@ -2963,12 +2963,12 @@ async def modify_webhook( Raises ------ - :obj:`hikari.net.errors.NotFoundHTTPError` + :obj:`hikari.errors.NotFoundHTTPError` If either the webhook or the channel aren't found. - :obj:`hikari.net.errors.ForbiddenHTTPError` + :obj:`hikari.errors.ForbiddenHTTPError` If you either lack the ``MANAGE_WEBHOOKS`` permission or aren't a member of the guild this webhook belongs to. - :obj:`hikari.net.errors.UnauthorizedHTTPError` + :obj:`hikari.errors.UnauthorizedHTTPError` If you pass a token that's invalid for the target webhook. """ payload = {} @@ -2996,12 +2996,12 @@ async def delete_webhook(self, webhook_id: str, *, webhook_token: str = ...) -> Raises ------ - :obj:`hikari.net.errors.NotFoundHTTPError` + :obj:`hikari.errors.NotFoundHTTPError` If the webhook is not found. - :obj:`hikari.net.errors.ForbiddenHTTPError` + :obj:`hikari.errors.ForbiddenHTTPError` If you either lack the ``MANAGE_WEBHOOKS`` permission or aren't a member of the guild this webhook belongs to. - :obj:`hikari.net.errors.UnauthorizedHTTPError` + :obj:`hikari.errors.UnauthorizedHTTPError` If you pass a token that's invalid for the target webhook. """ if webhook_token is ...: @@ -3058,9 +3058,9 @@ async def execute_webhook( Raises ------ - :obj:`hikari.net.errors.NotFoundHTTPError` + :obj:`hikari.errors.NotFoundHTTPError` If the channel ID or webhook ID is not found. - :obj:`hikari.net.errors.BadRequestHTTPError` + :obj:`hikari.errors.BadRequestHTTPError` This can be raised if the file is too large; if the embed exceeds the defined limits; if the message content is specified only and empty or greater than ``2000`` characters; if neither content, file @@ -3068,9 +3068,9 @@ async def execute_webhook( fields in ``allowed_mentions``; if you specify to parse all users/roles mentions but also specify which users/roles to parse only. - :obj:`hikari.net.errors.ForbiddenHTTPError` + :obj:`hikari.errors.ForbiddenHTTPError` If you lack permissions to send to this channel. - :obj:`hikari.net.errors.UnauthorizedHTTPError` + :obj:`hikari.errors.UnauthorizedHTTPError` If you pass a token that's invalid for the target webhook. Returns diff --git a/hikari/net/shard.py b/hikari/net/shard.py index f594e25af4..4cdf676c6b 100644 --- a/hikari/net/shard.py +++ b/hikari/net/shard.py @@ -47,10 +47,10 @@ import aiohttp.typedefs +from hikari import errors from hikari.internal import more_collections from hikari.internal import more_logging from hikari.net import codes -from hikari.net import errors from hikari.net import ratelimits from hikari.net import user_agent from hikari.net import versions diff --git a/hikari/core/oauth2.py b/hikari/oauth2.py similarity index 94% rename from hikari/core/oauth2.py rename to hikari/oauth2.py index aac4d30c46..7acbe9d968 100644 --- a/hikari/core/oauth2.py +++ b/hikari/oauth2.py @@ -24,16 +24,20 @@ from hikari.internal import cdn from hikari.internal import marshaller -from hikari.core import entities -from hikari.core import guilds -from hikari.core import permissions -from hikari.core import snowflakes -from hikari.core import users +from hikari import entities +from hikari import guilds +from hikari import permissions +from hikari import snowflakes +from hikari import users @enum.unique class ConnectionVisibility(enum.IntEnum): + """Describes who can see a connection with a third party account.""" + + #: Only you can see the connection. NONE = 0 + #: Everyone can see the connection. EVERYONE = 1 @@ -112,7 +116,7 @@ class OwnGuild(guilds.PartialGuild): #: The guild level permissions that apply to the current user or bot. #: - #: :type: :obj:`hikari.core.permissions.Permission` + #: :type: :obj:`hikari.permissions.Permission` my_permissions: permissions.Permission = marshaller.attrib( raw_name="permissions", deserializer=permissions.Permission ) @@ -146,7 +150,7 @@ class TeamMember(entities.HikariEntity, entities.Deserializable): #: The ID of the team this member belongs to. #: - #: :type: :obj:`hikari.core.snowflakes.Snowflake` + #: :type: :obj:`hikari.snowflakes.Snowflake` team_id: snowflakes.Snowflake = marshaller.attrib(deserializer=snowflakes.Snowflake.deserialize) #: The user object of this team member. @@ -166,14 +170,14 @@ class Team(snowflakes.UniqueEntity, entities.Deserializable): #: The member's that belong to this team. #: - #: :type: :obj:`typing.Mapping` [ :obj:`hikari.core.snowflakes.Snowflake`, :obj:`TeamMember` ] + #: :type: :obj:`typing.Mapping` [ :obj:`hikari.snowflakes.Snowflake`, :obj:`TeamMember` ] members: typing.Mapping[snowflakes.Snowflake, TeamMember] = marshaller.attrib( deserializer=lambda members: {m.user.id: m for m in map(TeamMember.deserialize, members)} ) #: The ID of this team's owner. #: - #: :type: :obj:`hikari.core.snowflakes.Snowflake` + #: :type: :obj:`hikari.snowflakes.Snowflake` owner_user_id: snowflakes.Snowflake = marshaller.attrib(deserializer=snowflakes.Snowflake.deserialize) @property @@ -209,7 +213,7 @@ class ApplicationOwner(users.User): #: This user's flags. #: - #: :type: :obj:`hikari.core.users.UserFlag` + #: :type: :obj:`hikari.users.UserFlag` flags: int = marshaller.attrib(deserializer=users.UserFlag) @property @@ -288,14 +292,14 @@ class Application(snowflakes.UniqueEntity, entities.Deserializable): #: The ID of the guild this application is linked to #: if it's sold on Discord. #: - #: :type: :obj:`hikari.core.snowflakes.Snowflake`, optional + #: :type: :obj:`hikari.snowflakes.Snowflake`, optional guild_id: typing.Optional[snowflakes.Snowflake] = marshaller.attrib( deserializer=snowflakes.Snowflake.deserialize, if_undefined=None ) #: The ID of the primary "Game SKU" of a game that's sold on Discord. #: - #: :type: :obj:`hikari.core.snowflakes.Snowflake`, optional + #: :type: :obj:`hikari.snowflakes.Snowflake`, optional primary_sku_id: typing.Optional[snowflakes.Snowflake] = marshaller.attrib( deserializer=snowflakes.Snowflake.deserialize, if_undefined=None ) diff --git a/hikari/core/permissions.py b/hikari/permissions.py similarity index 100% rename from hikari/core/permissions.py rename to hikari/permissions.py diff --git a/hikari/core/snowflakes.py b/hikari/snowflakes.py similarity index 99% rename from hikari/core/snowflakes.py rename to hikari/snowflakes.py index 471bea33da..a14222dd7b 100644 --- a/hikari/core/snowflakes.py +++ b/hikari/snowflakes.py @@ -30,7 +30,7 @@ import hikari.internal.conversions from hikari.internal import marshaller -from hikari.core import entities +from hikari import entities @functools.total_ordering diff --git a/hikari/state/__init__.py b/hikari/state/__init__.py new file mode 100644 index 0000000000..c9cb77fadb --- /dev/null +++ b/hikari/state/__init__.py @@ -0,0 +1,35 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# Copyright © Nekoka.tt 2019-2020 +# +# This file is part of Hikari. +# +# Hikari is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Hikari is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Hikari. If not, see . +"""Provides the internal framework for processing the lifetime of a bot. + +The API for this part of the framework has been split into groups of +abstract base classes, and corresponding implementations. This allows +several key components to be implemented separately, in case you have a +specific use case you want to provide (such as placing stuff on a message +queue if you distribute your bot). + +The overall structure is as follows: + +.. inheritance-diagram:: + hikari.state.event_dispatcher + hikari.state.raw_event_consumer + hikari.state.event_manager + hikari.state.stateless_event_manager_impl +""" +__all__ = [] diff --git a/hikari/core/state/dispatcher.py b/hikari/state/event_dispatcher.py similarity index 93% rename from hikari/core/state/dispatcher.py rename to hikari/state/event_dispatcher.py index 8b077833c9..09cc866099 100644 --- a/hikari/core/state/dispatcher.py +++ b/hikari/state/event_dispatcher.py @@ -23,13 +23,12 @@ import asyncio import logging import typing -import weakref from hikari.internal import assertions from hikari.internal import more_asyncio from hikari.internal import more_collections from hikari.internal import more_logging -from hikari.core import events +from hikari import events EventT = typing.TypeVar("EventT", bound=events.HikariEvent) PredicateT = typing.Callable[[EventT], typing.Union[bool, typing.Coroutine[None, None, bool]]] @@ -55,7 +54,7 @@ def add_listener(self, event_type: typing.Type[EventT], callback: EventCallbackT Parameters ---------- - event_type : :obj:`typing.Type` [ :obj:`hikari.core.events.HikariEvent` ] + event_type : :obj:`typing.Type` [ :obj:`hikari.events.HikariEvent` ] The event to register to. callback : ``async def callback(event: HikariEvent) -> ...`` The event callback to invoke when this event is fired. @@ -74,7 +73,7 @@ def remove_listener(self, event_type: typing.Type[EventT], callback: EventCallba Parameters ---------- - event_type : :obj:`typing.Type` [ :obj:`hikari.core.events.HikariEvent` ] + event_type : :obj:`typing.Type` [ :obj:`hikari.events.HikariEvent` ] The type of event to remove the callback from. callback : ``async def callback(event: HikariEvent) -> ...`` The event callback to invoke when this event is fired. @@ -88,7 +87,7 @@ def wait_for( Parameters ---------- - event_type : :obj:`typing.Type` [ :obj:`hikari.core.events.HikariEvent` ] + event_type : :obj:`typing.Type` [ :obj:`hikari.events.HikariEvent` ] The name of the event to wait for. timeout : :obj:`float`, optional The timeout to wait for before cancelling and raising an @@ -124,7 +123,7 @@ def on(self, event_type: typing.Type[EventT]) -> typing.Callable[[EventCallbackT Parameters ---------- - event_type : :obj:`typing.Type` [ :obj:`hikari.core.events.HikariEvent` ] + event_type : :obj:`typing.Type` [ :obj:`hikari.events.HikariEvent` ] The event type to register the produced decorator to. Returns @@ -146,7 +145,7 @@ def dispatch_event(self, event: events.HikariEvent) -> ...: Parameters ---------- - event : :obj:`hikari.core.events.HikariEvent` + event : :obj:`hikari.events.HikariEvent` The event to dispatch. Returns @@ -178,8 +177,6 @@ class EventDispatcherImpl(EventDispatcher): #: :type: :obj:`logging.Logger` logger: logging.Logger - __slots__ = ("exception_event", "logger", "_listeners", "_waiters") - def __init__(self) -> None: self._listeners: typing.Dict[typing.Type[EventT], typing.List[EventCallbackT]] = {} # pylint: disable=E1136 @@ -202,7 +199,7 @@ def add_listener(self, event_type: typing.Type[events.HikariEvent], callback: Ev Parameters ---------- - event_type : :obj:`typing.Type` [ :obj:`hikari.core.events.HikariEvent` ] + event_type : :obj:`typing.Type` [ :obj:`hikari.events.HikariEvent` ] The event to register to. callback : ``async def callback(event: HikariEvent) -> ...`` The event callback to invoke when this event is fired. @@ -226,7 +223,7 @@ def remove_listener(self, event_type: typing.Type[EventT], callback: EventCallba Parameters ---------- - event_type : :obj:`typing.Type` [ :obj:`hikari.core.events.HikariEvent` ] + event_type : :obj:`typing.Type` [ :obj:`hikari.events.HikariEvent` ] The type of event to remove the callback from. callback : ``async def callback(event: HikariEvent) -> ...`` The event callback to remove. @@ -244,7 +241,7 @@ def dispatch_event(self, event: events.HikariEvent): Parameters ---------- - event : :obj:`hikari.core.events.HikariEvent` + event : :obj:`hikari.events.HikariEvent` The event to dispatch. Returns @@ -320,17 +317,17 @@ def handle_exception( This allows users to override this with a custom implementation if desired. This implementation will check to see if the event that triggered the - exception is an :obj:`hikari.core.events.ExceptionEvent`. If this - exception was caused by the :obj:`hikari.core.events.ExceptionEvent`, + exception is an :obj:`hikari.events.ExceptionEvent`. If this + exception was caused by the :obj:`hikari.events.ExceptionEvent`, then nothing is dispatched (thus preventing an exception handler recursively - re-triggering itself). Otherwise, an :obj:`hikari.core.events.ExceptionEvent` + re-triggering itself). Otherwise, an :obj:`hikari.events.ExceptionEvent` is dispatched. Parameters ---------- exception: :obj:`Exception` The exception that triggered this call. - event: :obj:`hikari.core.events.HikariEvent` + event: :obj:`hikari.events.HikariEvent` The event that was being dispatched. callback The callback that threw the exception. @@ -363,7 +360,7 @@ def wait_for( Parameters ---------- - event_type : :obj:`typing.Type` [ :obj:`hikari.core.events.HikariEvent` ] + event_type : :obj:`typing.Type` [ :obj:`hikari.events.HikariEvent` ] The name of the event to wait for. timeout : :obj:`float`, optional The timeout to wait for before cancelling and raising an @@ -397,7 +394,7 @@ def wait_for( if event_type not in self._waiters: # This is used as a weakref dict to allow automatically tidying up # any future that falls out of scope entirely. - self._waiters[event_type] = weakref.WeakKeyDictionary() + self._waiters[event_type] = more_collections.WeakKeyDictionary() self._waiters[event_type][future] = predicate # noinspection PyTypeChecker return asyncio.ensure_future(asyncio.wait_for(future, timeout)) diff --git a/hikari/state/event_manager.py b/hikari/state/event_manager.py new file mode 100644 index 0000000000..02deed9952 --- /dev/null +++ b/hikari/state/event_manager.py @@ -0,0 +1,169 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# Copyright © Nekoka.tt 2019-2020 +# +# This file is part of Hikari. +# +# Hikari is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Hikari is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Hikari. If not, see . +"""Basic single-application weaving manager.""" +__all__ = ["raw_event_mapper", "EventManager"] + +import inspect + +import typing + +from hikari.clients import shard_client +from hikari.state import event_dispatcher +from hikari import entities +from hikari.state import raw_event_consumer +from hikari.internal import assertions +from hikari.internal import more_logging + +EVENT_MARKER_ATTR = "___event_name___" + +EventConsumerT = typing.Callable[[str, entities.RawEntityT], typing.Awaitable[None]] + + +def raw_event_mapper(name: str) -> typing.Callable[[EventConsumerT], EventConsumerT]: + """Create a decorator for a coroutine function to register it as an event handler. + + Parameters + ---------- + name: str + The case sensitive name of the event to associate the annotated method + with. + + Returns + ------- + ``decorator(callable) -> callable`` + A decorator for a method. + + """ + + def decorator(callable_item: EventConsumerT) -> EventConsumerT: + assertions.assert_that(inspect.isfunction(callable_item), "Annotated element must be a function") + event_set = getattr(callable_item, EVENT_MARKER_ATTR, set()) + event_set.add(name) + setattr(callable_item, EVENT_MARKER_ATTR, event_set) + return callable_item + + return decorator + + +def _has_event_marker(obj: typing.Any) -> bool: + return hasattr(obj, EVENT_MARKER_ATTR) + + +def _get_event_marker(obj: typing.Any) -> typing.Set[str]: + return getattr(obj, EVENT_MARKER_ATTR) + + +class EventManager(raw_event_consumer.RawEventConsumer): + """Abstract definition of the components for an event system for a bot. + + The class itself inherits from + :obj:`hikari.state.raw_event_consumer.RawEventConsumer` (which allows + it to provide the ability to transform a raw payload into an event object). + + This is designed as a basis to enable transformation of raw incoming events + from the websocket into more usable native Python objects, and to then + dispatch them to a given event dispatcher. It does not provide the logic for + how to specifically parse each event however. + + Parameters + ---------- + event_dispatcher_impl: :obj:`hikari.state.event_dispatcher.EventDispatcher` + An implementation of event dispatcher that will store individual events + and manage dispatching them after this object creates them. + + Notes + ----- + This object will detect internal event mapper functions by looking for + coroutine functions wrapped with :obj:`raw_event_mapper`. + + These methods are expected to have the following parameters: + + shard_obj: :obj:`hikari.clients.shard_client.ShardClient` + The shard client that emitted the event. + payload: :obj:`typing.Any` + The received payload. This is expected to be a JSON-compatible type. + + For example, if you want to provide an implementation that can consume + and handle ``MESSAGE_CREATE`` events, you can do the following. + + .. code-block:: python + + class MyMappingEventConsumer(MappingEventConsumer): + @event_mapper("MESSAGE_CREATE") + def _process_message_create(self, shard, payload) -> MessageCreateEvent: + return MessageCreateEvent.deserialize(payload) + + The decorator can be stacked if you wish to provide one mapper + + ... it is pretty simple. This is exposed in this way to enable you to write + code that may use a distributed system instead of a single-process bot. + + Writing to a message queue is pretty simple using this mechanism, as you can + choose when and how to place the event on a queue to be consumed by other + application components. + + For the sake of simplicity, Hikari only provides implementations for single + process bots, since most of what you will need will be fairly bespoke if you + want to implement anything more complicated; regardless, the tools are here + for you to use as you see fit. + + Warnings + -------- + This class provides the scaffold for making an event consumer, but does not + physically implement the logic to deserialize and process specific events. + + To provide this, use one of the provided implementations of this class, or + create your own as needed. + """ + + def __init__(self, event_dispatcher_impl: event_dispatcher.EventDispatcher) -> None: + self.logger = more_logging.get_named_logger(self) + self.event_dispatcher = event_dispatcher_impl + self.raw_event_mappers = {} + + # Look for events and register them. + for _, member in inspect.getmembers(self, _has_event_marker): + event_names = _get_event_marker(member) + for event_name in event_names: + self.raw_event_mappers[event_name] = member + + def process_raw_event( + self, shard_client_obj: shard_client.ShardClient, name: str, payload: entities.RawEntityT, + ) -> None: + """Process a low level event. + + This will update the internal weaving, perform processing where necessary, + and then dispatch the event to any listeners. + + Parameters + ---------- + shard_client_obj: :obj:`hikari.clients.shard_client.ShardClient` + The shard that triggered this event. + name : :obj:`str` + The raw event name. + payload : :obj:`dict` + The payload that was sent. + """ + try: + handler = self.raw_event_mappers[name] + except KeyError: + self.logger.debug("No handler for event %s is registered", name) + else: + event = handler(shard_client_obj, payload) + self.event_dispatcher.dispatch_event(event) diff --git a/hikari/state/raw_event_consumer.py b/hikari/state/raw_event_consumer.py new file mode 100644 index 0000000000..1cf8e852bd --- /dev/null +++ b/hikari/state/raw_event_consumer.py @@ -0,0 +1,58 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# Copyright © Nekoka.tt 2019-2020 +# +# This file is part of Hikari. +# +# Hikari is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Hikari is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Hikari. If not, see . +"""Definition of the interface a compliant weaving implementation should provide. + +State object handle decoding events and managing application state. +""" +from __future__ import annotations + +__all__ = ["RawEventConsumer"] + +import abc + +from hikari import entities +from hikari.clients import shard_client + + +class RawEventConsumer(abc.ABC): + """Consumer of raw events from Discord. + + RawEventConsumer describes an object that takes any event payloads that + Discord dispatches over a websocket and decides how to process it further. + This is used as the core base for any form of event manager type. + + This base may also be used by users to dispatch the event to a completely + different medium, such as a message queue for distributed applications. + """ + + @abc.abstractmethod + def process_raw_event( + self, shard_client_obj: shard_client.ShardClient, name: str, payload: entities.RawEntityT, + ) -> None: + """Consume a raw event that was received from a shard connection. + + Parameters + ---------- + shard_client_obj : :obj:`hikari.clients.ShardClient` + The client for the shard that received the event. + name : :obj:`str` + The raw event name. + payload : :obj:`typing.Any` + The raw event payload. Will be a JSON-compatible type. + """ diff --git a/hikari/state/stateless_event_manager_impl.py b/hikari/state/stateless_event_manager_impl.py new file mode 100644 index 0000000000..80a0059771 --- /dev/null +++ b/hikari/state/stateless_event_manager_impl.py @@ -0,0 +1,31 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# Copyright © Nekoka.tt 2019-2020 +# +# This file is part of Hikari. +# +# Hikari is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Hikari is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Hikari. If not, see . +"""Provides an implementation of a stateless event manager.""" +__all__ = ["StatelessEventManagerImpl"] + +from hikari.state import event_manager + + +class StatelessEventManagerImpl(event_manager.EventManager): + """Stateless event manager implementation for stateless bots. + + This is an implementation that does not rely on querying prior information to + operate. The implementation details of this are much simpler than a stateful + version, and are not immediately affected by the use of intents. + """ diff --git a/hikari/core/users.py b/hikari/users.py similarity index 98% rename from hikari/core/users.py rename to hikari/users.py index aee2c9d8e0..217baa9160 100644 --- a/hikari/core/users.py +++ b/hikari/users.py @@ -24,8 +24,8 @@ from hikari.internal import cdn from hikari.internal import marshaller -from hikari.core import entities -from hikari.core import snowflakes +from hikari import entities +from hikari import snowflakes @marshaller.attrs(slots=True) diff --git a/hikari/core/voices.py b/hikari/voices.py similarity index 92% rename from hikari/core/voices.py rename to hikari/voices.py index d3603871d9..a47e29bc31 100644 --- a/hikari/core/voices.py +++ b/hikari/voices.py @@ -22,9 +22,9 @@ import typing from hikari.internal import marshaller -from hikari.core import entities -from hikari.core import guilds -from hikari.core import snowflakes +from hikari import entities +from hikari import guilds +from hikari import snowflakes @marshaller.attrs(slots=True) @@ -33,25 +33,25 @@ class VoiceState(entities.HikariEntity, entities.Deserializable): #: The ID of the guild this voice state is in, if applicable. #: - #: :type: :obj:`hikari.core.snowflakes.Snowflake`, optional + #: :type: :obj:`hikari.snowflakes.Snowflake`, optional guild_id: typing.Optional[snowflakes.Snowflake] = marshaller.attrib( deserializer=snowflakes.Snowflake.deserialize, if_undefined=None ) #: The ID of the channel this user is connected to. #: - #: :type: :obj:`hikari.core.snowflakes.Snowflake`, optional + #: :type: :obj:`hikari.snowflakes.Snowflake`, optional channel_id: snowflakes.Snowflake = marshaller.attrib(deserializer=snowflakes.Snowflake.deserialize, if_none=None) #: The ID of the user this voice state is for. #: - #: :type: :obj:`hikari.core.snowflakes.Snowflake` + #: :type: :obj:`hikari.snowflakes.Snowflake` user_id: snowflakes.Snowflake = marshaller.attrib(deserializer=snowflakes.Snowflake.deserialize) #: The guild member this voice state is for if the voice state is in a #: guild. #: - #: :type: :obj:`hikari.core.guilds.GuildMember`, optional + #: :type: :obj:`hikari.guilds.GuildMember`, optional member: typing.Optional[guilds.GuildMember] = marshaller.attrib( deserializer=guilds.GuildMember.deserialize, if_undefined=None ) diff --git a/hikari/core/webhooks.py b/hikari/webhooks.py similarity index 91% rename from hikari/core/webhooks.py rename to hikari/webhooks.py index 9026f2ed6f..c95751db7d 100644 --- a/hikari/core/webhooks.py +++ b/hikari/webhooks.py @@ -23,9 +23,9 @@ import typing from hikari.internal import marshaller -from hikari.core import entities -from hikari.core import snowflakes -from hikari.core import users +from hikari import entities +from hikari import snowflakes +from hikari import users @enum.unique @@ -54,14 +54,14 @@ class Webhook(snowflakes.UniqueEntity, entities.Deserializable): #: The guild ID of the webhook. #: - #: :type: :obj:`hikari.core.snowflakes.Snowflake`, optional + #: :type: :obj:`hikari.snowflakes.Snowflake`, optional guild_id: typing.Optional[snowflakes.Snowflake] = marshaller.attrib( deserializer=snowflakes.Snowflake.deserialize, if_undefined=None ) #: The channel ID this webhook is for. #: - #: :type: :obj:`hikari.core.snowflakes.Snowflake` + #: :type: :obj:`hikari.snowflakes.Snowflake` channel_id: snowflakes.Snowflake = marshaller.attrib(deserializer=snowflakes.Snowflake.deserialize) #: The user that created the webhook @@ -71,7 +71,7 @@ class Webhook(snowflakes.UniqueEntity, entities.Deserializable): #: This will be ``None`` when getting a webhook with a token #: #: - #: :type: :obj:`hikari.core.users.User`, optional + #: :type: :obj:`hikari.users.User`, optional user: typing.Optional[users.User] = marshaller.attrib(deserializer=users.User.deserialize, if_undefined=None) #: The default name of the webhook. diff --git a/insomnia/v7.yaml b/insomnia/v7.yaml index 05ac2bd1e9..3fbeda9f5b 100644 --- a/insomnia/v7.yaml +++ b/insomnia/v7.yaml @@ -1,6 +1,6 @@ _type: export __export_format: 4 -__export_date: 2020-03-22T13:28:17.667Z +__export_date: 2020-04-04T18:36:08.338Z __export_source: insomnia.desktop.app:v7.1.1 resources: - _id: req_3f79be849f9d4a76ad7fc0572894acb4 @@ -32,7 +32,7 @@ resources: isPrivate: false metaSortKey: -1579446582104 method: POST - modified: 1579888478034 + modified: 1586025134857 name: Create Channel Invite parameters: [] parentId: fld_02ddb24b4e5148469979a2364fcc01dc @@ -42,8 +42,7 @@ resources: settingRebuildPath: true settingSendCookies: true settingStoreCookies: true - url: "{{api_url}}/channels/{% prompt 'channel_id', 'channel_id', '', '', false, - true %}/invites" + url: "{{api_url}}/channels/{channel_id}/invites" _type: request - _id: fld_02ddb24b4e5148469979a2364fcc01dc created: 1577108071067 @@ -94,16 +93,19 @@ resources: created: 1579446582054 description: "" headers: - - name: Authorization + - id: pair_6175f5722d6741689ddc2ca3648ec135 + name: Authorization value: "{{authorization}}" - - name: Content-Type + - id: pair_504b780773ab4b3e96b06b8eb5c9c4c9 + name: Content-Type value: application/json - - name: Accept + - id: pair_cca685e5ba2c42f182dd68062866d1ca + name: Accept value: application/json isPrivate: false metaSortKey: -1579446582054 method: POST - modified: 1584009252159 + modified: 1586025146155 name: Create Message (JSON) parameters: [] parentId: fld_02ddb24b4e5148469979a2364fcc01dc @@ -113,8 +115,7 @@ resources: settingRebuildPath: true settingSendCookies: true settingStoreCookies: true - url: "{{api_url}}/channels/{% prompt 'channel_id', 'channel_id', '', '', false, - true %}/messages" + url: "{{api_url}}/channels/{channel_id}/messages" _type: request - _id: req_25d53074d2cf44b399bddb805ddab89b authentication: {} @@ -124,16 +125,19 @@ resources: created: 1579887586462 description: "" headers: - - name: Authorization + - id: pair_05feeec703f24fb28eb975f3c81d9647 + name: Authorization value: "{{authorization}}" - - name: Content-Type + - id: pair_91d84368319f46bea7c7897c31abbf9b + name: Content-Type value: application/json - - name: Accept + - id: pair_9cd6bd15a20f4b0caa757d7250a08d42 + name: Accept value: application/json isPrivate: false metaSortKey: -1579446347254.5 method: PUT - modified: 1579888368767 + modified: 1586025171372 name: Create Reaction parameters: [] parentId: fld_02ddb24b4e5148469979a2364fcc01dc @@ -143,9 +147,8 @@ resources: settingRebuildPath: true settingSendCookies: true settingStoreCookies: true - url: "{{api_url}}/channels/{% prompt 'channel_id', 'channel_id', '', '', false, - true %}/messages/{% prompt 'message_id', 'message_id', '', '', false, true - %}/reactions/{% prompt 'emoji', 'emoji', '', '', false, true %}/@me" + url: "{{api_url}}/channels/{channel_id}/messages/{message_id}/reactions/{emoji\ + }/@me" _type: request - _id: req_8ed678ae359d4170b5720081960696db authentication: {} @@ -155,16 +158,19 @@ resources: created: 1579888166303 description: "" headers: - - name: Authorization + - id: pair_38b217632cfc4a99ab70eda0a2b1a3e4 + name: Authorization value: "{{authorization}}" - - name: Content-Type + - id: pair_8c5c4760590f437aa7fd4537d2e9714d + name: Content-Type value: application/json - - name: Accept + - id: pair_60902223d98e4cfbb5afe66c72924daf + name: Accept value: application/json isPrivate: false metaSortKey: -1579446288554.625 method: DELETE - modified: 1579888898059 + modified: 1586025188293 name: Delete All Reactions parameters: [] parentId: fld_02ddb24b4e5148469979a2364fcc01dc @@ -174,9 +180,7 @@ resources: settingRebuildPath: true settingSendCookies: true settingStoreCookies: true - url: "{{api_url}}/channels/{% prompt 'channel_id', 'channel_id', '', '', false, - true %}/messages/{% prompt 'message_id', 'message_id', '', '', false, true - %}/reactions" + url: "{{api_url}}/channels/{channel_id}/messages/{message_id}/reactions" _type: request - _id: req_ab8e42a9978d411aa290e79897d1bcb6 authentication: {} @@ -186,16 +190,19 @@ resources: created: 1579888568770 description: "" headers: - - name: Authorization + - id: pair_081411a07584409dbf42949a9d43b0d0 + name: Authorization value: "{{authorization}}" - - name: Content-Type + - id: pair_af6051069b814acaa87a433738ce7ec0 + name: Content-Type value: application/json - - name: Accept + - id: pair_222248024d9e454884c2bf42577451ba + name: Accept value: application/json isPrivate: false metaSortKey: -1579446259204.6875 method: DELETE - modified: 1579888899729 + modified: 1586025275845 name: Delete All Reactions for Emoji parameters: [] parentId: fld_02ddb24b4e5148469979a2364fcc01dc @@ -205,9 +212,7 @@ resources: settingRebuildPath: true settingSendCookies: true settingStoreCookies: true - url: "{{api_url}}/channels/{% prompt 'channel_id', 'channel_id', '', '', false, - true %}/messages/{% prompt 'message_id', 'message_id', '', '', false, true - %}/reactions/{% prompt 'emoji', 'emoji', '', '', false, true %}" + url: "{{api_url}}/channels/{channel_id}/messages/{message_id}/reactions/{emoji}" _type: request - _id: req_82394bee04d94ea5b171da6239d12700 authentication: {} @@ -217,16 +222,19 @@ resources: created: 1579887965188 description: "" headers: - - name: Authorization + - id: pair_6d81d2b754024e71a06a74e5a82d73b9 + name: Authorization value: "{{authorization}}" - - name: Content-Type + - id: pair_0065e0fd87394fd8a9dfda386301b497 + name: Content-Type value: application/json - - name: Accept + - id: pair_0ecf8672926340048d4076134ebf68e7 + name: Accept value: application/json isPrivate: false metaSortKey: -1579446229854.75 method: DELETE - modified: 1579888327919 + modified: 1586025268656 name: Delete Own Reaction parameters: [] parentId: fld_02ddb24b4e5148469979a2364fcc01dc @@ -236,9 +244,8 @@ resources: settingRebuildPath: true settingSendCookies: true settingStoreCookies: true - url: "{{api_url}}/channels/{% prompt 'channel_id', 'channel_id', '', '', false, - true %}/messages/{% prompt 'message_id', 'message_id', '', '', false, true - %}/reactions/{% prompt 'emoji', 'emoji', '', '', false, true %}/@me" + url: "{{api_url}}/channels/{channel_id}/messages/{message_id}/reactions/{emoji\ + }/@me" _type: request - _id: req_e16bac177c144773a63b51f12cb2e76a authentication: {} @@ -248,16 +255,19 @@ resources: created: 1579888064162 description: "" headers: - - name: Authorization + - id: pair_8d99cb13b67f4bf1b5e6dcf801b56c07 + name: Authorization value: "{{authorization}}" - - name: Content-Type + - id: pair_feb3ec76f5324e54b852e2ec04b5665b + name: Content-Type value: application/json - - name: Accept + - id: pair_275a8ebe66ce4cfdb35b0bc31b7fc69c + name: Accept value: application/json isPrivate: false metaSortKey: -1579446171154.875 method: DELETE - modified: 1579888320847 + modified: 1586025304354 name: Delete User Reaction parameters: [] parentId: fld_02ddb24b4e5148469979a2364fcc01dc @@ -267,10 +277,8 @@ resources: settingRebuildPath: true settingSendCookies: true settingStoreCookies: true - url: "{{api_url}}/channels/{% prompt 'channel_id', 'channel_id', '', '', false, - true %}/messages/{% prompt 'message_id', 'message_id', '', '', false, true - %}/reactions/{% prompt 'emoji', 'emoji', '', '', false, true %}/{% prompt - 'user_id', 'user_id', '', '', false, true %}" + url: "{{api_url}}/gateway{{api_url}}/channels/{channel_id}/messages/{message_i\ + d}" _type: request - _id: req_6711d4d8b3754abb99a3134fd709aefa authentication: {} @@ -280,12 +288,13 @@ resources: created: 1579886260172 description: "" headers: - - name: Authorization + - id: pair_6aaa67c400414c78b5f5d65d94db56e5 + name: Authorization value: "{{authorization}}" isPrivate: false metaSortKey: -1579446112455 method: DELETE - modified: 1579888018462 + modified: 1586025243516 name: Delete Message parameters: [] parentId: fld_02ddb24b4e5148469979a2364fcc01dc @@ -295,9 +304,7 @@ resources: settingRebuildPath: true settingSendCookies: true settingStoreCookies: true - url: "{{api_url}}/channels/{% prompt 'channel_id', 'channel_id', '', '', false, - true %}/messages/{% prompt 'message_id', 'message_id', '', '', false, true - %}" + url: "{{api_url}}/channels/{channel_id}/messages/{message_id}" _type: request - _id: req_e545c73cba2d404c8b81eb1b08089ed7 authentication: {} @@ -305,16 +312,19 @@ resources: created: 1579888209925 description: "" headers: - - name: Authorization + - id: pair_822a5e5ff7694595b913d77c313e82e3 + name: Authorization value: "{{authorization}}" - - name: Content-Type + - id: pair_e81f882d39fc4ba29fc17f85f6fe9d89 + name: Content-Type value: application/json - - name: Accept + - id: pair_3699d419135c4d898353382075ecf141 + name: Accept value: application/json isPrivate: false metaSortKey: -1578277095877.5 method: GET - modified: 1584567461195 + modified: 1586025318800 name: Get Reactions parameters: [] parentId: fld_02ddb24b4e5148469979a2364fcc01dc @@ -324,9 +334,7 @@ resources: settingRebuildPath: true settingSendCookies: true settingStoreCookies: true - url: "{{api_url}}/channels/{% prompt 'channel_id', 'channel_id', '', '', false, - true %}/messages/{% prompt 'message_id', 'message_id', '', '', false, true - %}/reactions/{% prompt 'emoji', 'emoji', '', '', false, true %}" + url: "{{api_url}}/channels/{channel_id}/messages/{message_id}/reactions/{emoji}" _type: request - _id: req_21e8d5a0163648ee8bc7b41ae1cf2d32 authentication: {} @@ -334,14 +342,16 @@ resources: created: 1584883559070 description: "" headers: - - name: Authorization + - id: pair_4d84e72871814adc9bd306c3f779bfc9 + name: Authorization value: "{{authorization}}" - - name: Accept + - id: pair_9e0e687481ac462d99850a70baeb976a + name: Accept value: application/json isPrivate: false metaSortKey: -1577692587588.75 method: GET - modified: 1584883585709 + modified: 1586025300976 name: Get Message parameters: [] parentId: fld_02ddb24b4e5148469979a2364fcc01dc @@ -351,9 +361,7 @@ resources: settingRebuildPath: true settingSendCookies: true settingStoreCookies: true - url: "{{api_url}}/channels/{% prompt 'channel_id', 'channel_id', '', '', false, - true %}/messages/{% prompt 'message_id', 'message_id', '', '', false, true - %}" + url: "{{api_url}}/channels/{channel_id}/messages/{message_id}" _type: request - _id: req_b7d357e7d7884ab7ad6d775410a9bde0 authentication: {} @@ -361,12 +369,13 @@ resources: created: 1577108809820 description: "" headers: - - name: Authorization + - id: pair_3e74098acac2415f84a380d9c5dc07a5 + name: Authorization value: "{{authorization}}" isPrivate: false metaSortKey: -1577108079300 method: POST - modified: 1580509000696 + modified: 1586025344109 name: Trigger Typing parameters: [] parentId: fld_02ddb24b4e5148469979a2364fcc01dc @@ -376,8 +385,7 @@ resources: settingRebuildPath: true settingSendCookies: true settingStoreCookies: true - url: "{{api_url}}/channels/{% prompt 'channel_id', 'channel_id', '', '', false, - true %}/typing" + url: "{{api_url}}/channels/{channel_id}/typing" _type: request - _id: req_cec8e07486b345eb976d2f7a3dc7c80c authentication: {} @@ -839,7 +847,7 @@ resources: value: identify guilds.join - description: "" id: pair_596801dda92243f4950c8cba79e8999a - name: state + name: weaving value: "1234" parentId: fld_86100ce074544d8d810050ef04f111ee settingDisableRenderRequestBody: false diff --git a/pylint.ini b/pylint.ini index ee8b5c7743..49c1bfadf2 100644 --- a/pylint.ini +++ b/pylint.ini @@ -267,7 +267,9 @@ missing-member-max-choices=1 # List of decorators that change the signature of a decorated function. signature-mutators=attr.s, marshaller.attrs, - functools.wraps + functools.wraps, + click.command, + click.option [BASIC] diff --git a/tests/hikari/core/__init__.py b/tests/hikari/clients/__init__.py similarity index 100% rename from tests/hikari/core/__init__.py rename to tests/hikari/clients/__init__.py diff --git a/tests/hikari/_internal/__init__.py b/tests/hikari/internal/__init__.py similarity index 100% rename from tests/hikari/_internal/__init__.py rename to tests/hikari/internal/__init__.py diff --git a/tests/hikari/_internal/test_assertions.py b/tests/hikari/internal/test_assertions.py similarity index 100% rename from tests/hikari/_internal/test_assertions.py rename to tests/hikari/internal/test_assertions.py diff --git a/tests/hikari/_internal/test_cdn.py b/tests/hikari/internal/test_cdn.py similarity index 100% rename from tests/hikari/_internal/test_cdn.py rename to tests/hikari/internal/test_cdn.py diff --git a/tests/hikari/_internal/test_conversions.py b/tests/hikari/internal/test_conversions.py similarity index 100% rename from tests/hikari/_internal/test_conversions.py rename to tests/hikari/internal/test_conversions.py diff --git a/tests/hikari/_internal/test_marshaller.py b/tests/hikari/internal/test_marshaller.py similarity index 100% rename from tests/hikari/_internal/test_marshaller.py rename to tests/hikari/internal/test_marshaller.py diff --git a/tests/hikari/_internal/test_marshaller_pep563.py b/tests/hikari/internal/test_marshaller_pep563.py similarity index 100% rename from tests/hikari/_internal/test_marshaller_pep563.py rename to tests/hikari/internal/test_marshaller_pep563.py diff --git a/tests/hikari/_internal/test_meta.py b/tests/hikari/internal/test_meta.py similarity index 100% rename from tests/hikari/_internal/test_meta.py rename to tests/hikari/internal/test_meta.py diff --git a/tests/hikari/_internal/test_more_asyncio.py b/tests/hikari/internal/test_more_asyncio.py similarity index 100% rename from tests/hikari/_internal/test_more_asyncio.py rename to tests/hikari/internal/test_more_asyncio.py diff --git a/tests/hikari/_internal/test_more_collections.py b/tests/hikari/internal/test_more_collections.py similarity index 100% rename from tests/hikari/_internal/test_more_collections.py rename to tests/hikari/internal/test_more_collections.py diff --git a/tests/hikari/_internal/test_more_logging.py b/tests/hikari/internal/test_more_logging.py similarity index 100% rename from tests/hikari/_internal/test_more_logging.py rename to tests/hikari/internal/test_more_logging.py diff --git a/tests/hikari/net/test_errors.py b/tests/hikari/net/test_errors.py index 141a79ffaa..1e13167898 100644 --- a/tests/hikari/net/test_errors.py +++ b/tests/hikari/net/test_errors.py @@ -18,8 +18,8 @@ # along with Hikari. If not, see . import pytest +from hikari import errors from hikari.net import codes -from hikari.net import errors from hikari.net import routes diff --git a/tests/hikari/net/test_rest.py b/tests/hikari/net/test_rest.py index 841ff1a0a5..e97d5e36dc 100644 --- a/tests/hikari/net/test_rest.py +++ b/tests/hikari/net/test_rest.py @@ -29,7 +29,7 @@ import pytest from hikari.internal import conversions -from hikari.net import errors +from hikari import errors from hikari.net import ratelimits from hikari.net import rest from hikari.net import routes diff --git a/tests/hikari/net/test_shard.py b/tests/hikari/net/test_shard.py index b6373bffe3..4e785199e0 100644 --- a/tests/hikari/net/test_shard.py +++ b/tests/hikari/net/test_shard.py @@ -29,7 +29,7 @@ import pytest from hikari.internal import more_collections -from hikari.net import errors +from hikari import errors from hikari.net import shard from hikari.net import user_agent from hikari.net import versions diff --git a/hikari/core/state/__init__.py b/tests/hikari/state/__init__.py similarity index 89% rename from hikari/core/state/__init__.py rename to tests/hikari/state/__init__.py index 5dc33a6d3b..1c1502a5ca 100644 --- a/hikari/core/state/__init__.py +++ b/tests/hikari/state/__init__.py @@ -16,9 +16,3 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . -"""State registry and event manager.""" - - -import abc - -from hikari.core import events diff --git a/tests/hikari/core/test_dispatcher.py b/tests/hikari/state/test_event_dispatcher.py similarity index 98% rename from tests/hikari/core/test_dispatcher.py rename to tests/hikari/state/test_event_dispatcher.py index da58c07045..9fe89b1c9a 100644 --- a/tests/hikari/core/test_dispatcher.py +++ b/tests/hikari/state/test_event_dispatcher.py @@ -21,8 +21,8 @@ import pytest -from hikari.core.state import dispatcher -from hikari.core import events +from hikari.state import event_dispatcher +from hikari import events from tests.hikari import _helpers @@ -41,7 +41,7 @@ class TestEvent3(events.HikariEvent): class TestEventDispatcherImpl: @pytest.fixture def dispatcher_inst(self): - return _helpers.unslot_class(dispatcher.EventDispatcherImpl)() + return _helpers.unslot_class(event_dispatcher.EventDispatcherImpl)() # noinspection PyTypeChecker @_helpers.assert_raises(type_=TypeError) diff --git a/tests/hikari/core/test_audit_logs.py b/tests/hikari/test_audit_logs.py similarity index 99% rename from tests/hikari/core/test_audit_logs.py rename to tests/hikari/test_audit_logs.py index 25e6df6df1..2f8b93bd73 100644 --- a/tests/hikari/core/test_audit_logs.py +++ b/tests/hikari/test_audit_logs.py @@ -22,11 +22,11 @@ import cymock as mock import pytest -from hikari.core import audit_logs -from hikari.core import channels -from hikari.core import guilds -from hikari.core import users -from hikari.core import webhooks +from hikari import audit_logs +from hikari import channels +from hikari import guilds +from hikari import users +from hikari import webhooks class TestAuditLogChangeKey: diff --git a/tests/hikari/core/test_channels.py b/tests/hikari/test_channels.py similarity index 99% rename from tests/hikari/core/test_channels.py rename to tests/hikari/test_channels.py index 319444aa36..6e73f47224 100644 --- a/tests/hikari/core/test_channels.py +++ b/tests/hikari/test_channels.py @@ -21,9 +21,9 @@ import cymock as mock import pytest -from hikari.core import channels -from hikari.core import permissions -from hikari.core import users +from hikari import channels +from hikari import permissions +from hikari import users @pytest.fixture() diff --git a/tests/hikari/core/test_colors.py b/tests/hikari/test_colors.py similarity index 99% rename from tests/hikari/core/test_colors.py rename to tests/hikari/test_colors.py index 3c36ac1236..bdc1cba7d5 100644 --- a/tests/hikari/core/test_colors.py +++ b/tests/hikari/test_colors.py @@ -20,7 +20,7 @@ import pytest -from hikari.core import colors +from hikari import colors @pytest.mark.model diff --git a/tests/hikari/core/test_colours.py b/tests/hikari/test_colours.py similarity index 93% rename from tests/hikari/core/test_colours.py rename to tests/hikari/test_colours.py index 496581e8c7..a281474d9a 100644 --- a/tests/hikari/core/test_colours.py +++ b/tests/hikari/test_colours.py @@ -18,8 +18,7 @@ # along with Hikari. If not, see . import pytest -from hikari.core import colors -from hikari.core import colours +from hikari import colours, colors @pytest.mark.model diff --git a/tests/hikari/core/test_embeds.py b/tests/hikari/test_embeds.py similarity index 99% rename from tests/hikari/core/test_embeds.py rename to tests/hikari/test_embeds.py index 3152bd2e3e..7b0eb2b5c7 100644 --- a/tests/hikari/core/test_embeds.py +++ b/tests/hikari/test_embeds.py @@ -22,8 +22,7 @@ import pytest import hikari.internal.conversions -from hikari.core import colors -from hikari.core import embeds +from hikari import embeds, colors from tests.hikari import _helpers diff --git a/tests/hikari/core/test_emojis.py b/tests/hikari/test_emojis.py similarity index 97% rename from tests/hikari/core/test_emojis.py rename to tests/hikari/test_emojis.py index 97d30c1912..8cacf1fc5e 100644 --- a/tests/hikari/core/test_emojis.py +++ b/tests/hikari/test_emojis.py @@ -19,8 +19,8 @@ import cymock as mock import pytest -from hikari.core import emojis -from hikari.core import users +from hikari import emojis +from hikari import users from tests.hikari import _helpers diff --git a/tests/hikari/core/test_entities.py b/tests/hikari/test_entities.py similarity index 97% rename from tests/hikari/core/test_entities.py rename to tests/hikari/test_entities.py index 23c417d31d..e2ef6586d1 100644 --- a/tests/hikari/core/test_entities.py +++ b/tests/hikari/test_entities.py @@ -16,7 +16,7 @@ # # You should have received a copy of the GNU Lesser General Public License # along ith Hikari. If not, see . -from hikari.core import entities +from hikari import entities class TestUnset: diff --git a/tests/hikari/core/test_events.py b/tests/hikari/test_events.py similarity index 99% rename from tests/hikari/core/test_events.py rename to tests/hikari/test_events.py index 721f3e2e90..16179243fe 100644 --- a/tests/hikari/core/test_events.py +++ b/tests/hikari/test_events.py @@ -22,16 +22,16 @@ import pytest import hikari.internal.conversions -from hikari.core import channels -from hikari.core import embeds -from hikari.core import emojis -from hikari.core import entities -from hikari.core import events -from hikari.core import guilds -from hikari.core import invites -from hikari.core import messages -from hikari.core import oauth2 -from hikari.core import users +from hikari import channels +from hikari import embeds +from hikari import emojis +from hikari import entities +from hikari import events +from hikari import guilds +from hikari import invites +from hikari import messages +from hikari import oauth2 +from hikari import users from tests.hikari import _helpers diff --git a/tests/hikari/core/test_gateway_entities.py b/tests/hikari/test_gateway_entities.py similarity index 98% rename from tests/hikari/core/test_gateway_entities.py rename to tests/hikari/test_gateway_entities.py index c0f9dfaf2f..b89ee57677 100644 --- a/tests/hikari/core/test_gateway_entities.py +++ b/tests/hikari/test_gateway_entities.py @@ -21,7 +21,7 @@ import cymock as mock import pytest -from hikari.core import gateway_entities +from hikari import gateway_entities from tests.hikari import _helpers diff --git a/tests/hikari/core/test_guilds.py b/tests/hikari/test_guilds.py similarity index 99% rename from tests/hikari/core/test_guilds.py rename to tests/hikari/test_guilds.py index 6dcc3d753c..99c4c61fe2 100644 --- a/tests/hikari/core/test_guilds.py +++ b/tests/hikari/test_guilds.py @@ -23,11 +23,11 @@ import hikari.internal.conversions from hikari.internal import cdn -from hikari.core import emojis -from hikari.core import entities -from hikari.core import guilds -from hikari.core import users -from hikari.core import channels +from hikari import emojis +from hikari import entities +from hikari import guilds +from hikari import users +from hikari import channels from tests.hikari import _helpers diff --git a/tests/hikari/core/test_invites.py b/tests/hikari/test_invites.py similarity index 98% rename from tests/hikari/core/test_invites.py rename to tests/hikari/test_invites.py index 1cc225144b..decc508a45 100644 --- a/tests/hikari/core/test_invites.py +++ b/tests/hikari/test_invites.py @@ -23,10 +23,10 @@ import hikari.internal.conversions from hikari.internal import cdn -from hikari.core import channels -from hikari.core import guilds -from hikari.core import invites -from hikari.core import users +from hikari import channels +from hikari import guilds +from hikari import invites +from hikari import users from tests.hikari import _helpers diff --git a/tests/hikari/core/test_messages.py b/tests/hikari/test_messages.py similarity index 98% rename from tests/hikari/core/test_messages.py rename to tests/hikari/test_messages.py index 4b0b85907d..20ee85888a 100644 --- a/tests/hikari/core/test_messages.py +++ b/tests/hikari/test_messages.py @@ -22,12 +22,9 @@ import pytest import hikari.internal.conversions -from hikari.core import embeds -from hikari.core import emojis -from hikari.core import guilds -from hikari.core import messages -from hikari.core import oauth2 -from hikari.core import users +from hikari import guilds +from hikari import oauth2, emojis, embeds, messages +from hikari import users from tests.hikari import _helpers diff --git a/tests/hikari/core/test_oauth2.py b/tests/hikari/test_oauth2.py similarity index 99% rename from tests/hikari/core/test_oauth2.py rename to tests/hikari/test_oauth2.py index fde5c9e294..991fbc6db3 100644 --- a/tests/hikari/core/test_oauth2.py +++ b/tests/hikari/test_oauth2.py @@ -20,9 +20,9 @@ import pytest from hikari.internal import cdn -from hikari.core import guilds -from hikari.core import oauth2 -from hikari.core import users +from hikari import guilds +from hikari import oauth2 +from hikari import users from tests.hikari import _helpers diff --git a/tests/hikari/core/test_snowflake.py b/tests/hikari/test_snowflake.py similarity index 98% rename from tests/hikari/core/test_snowflake.py rename to tests/hikari/test_snowflake.py index be6b89fb8b..b6067ad33d 100644 --- a/tests/hikari/core/test_snowflake.py +++ b/tests/hikari/test_snowflake.py @@ -20,7 +20,7 @@ import pytest -from hikari.core import snowflakes +from hikari import snowflakes class TestSnowflake: diff --git a/tests/hikari/core/test_users.py b/tests/hikari/test_users.py similarity index 99% rename from tests/hikari/core/test_users.py rename to tests/hikari/test_users.py index 935da60470..54841c22b6 100644 --- a/tests/hikari/core/test_users.py +++ b/tests/hikari/test_users.py @@ -20,7 +20,7 @@ import pytest from hikari.internal import cdn -from hikari.core import users +from hikari import users @pytest.fixture() diff --git a/tests/hikari/core/test_voices.py b/tests/hikari/test_voices.py similarity index 98% rename from tests/hikari/core/test_voices.py rename to tests/hikari/test_voices.py index acc565559a..60797587a5 100644 --- a/tests/hikari/core/test_voices.py +++ b/tests/hikari/test_voices.py @@ -18,7 +18,7 @@ # along ith Hikari. If not, see . import pytest -from hikari.core import voices +from hikari import voices @pytest.fixture() diff --git a/tests/hikari/core/test_webhook.py b/tests/hikari/test_webhook.py similarity index 97% rename from tests/hikari/core/test_webhook.py rename to tests/hikari/test_webhook.py index bda21283e7..ed0226d253 100644 --- a/tests/hikari/core/test_webhook.py +++ b/tests/hikari/test_webhook.py @@ -18,8 +18,8 @@ # along ith Hikari. If not, see . import cymock as mock -from hikari.core import users -from hikari.core import webhooks +from hikari import users +from hikari import webhooks from tests.hikari import _helpers From 20313b801ce96db4f092a6ab5e367168a6725d40 Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Sat, 4 Apr 2020 21:16:36 +0100 Subject: [PATCH 062/922] More documentation. --- examples/example_basic_config.json | 3 --- examples/example_config.json | 24 ------------------------ examples/example_config.yaml | 28 ---------------------------- hikari/channels.py | 1 + hikari/clients/app_config.py | 12 ++++++++++++ hikari/clients/gateway_config.py | 2 +- hikari/clients/gateway_runner.py | 2 +- hikari/clients/http_config.py | 4 ++-- hikari/clients/shard_client.py | 3 ++- hikari/clients/shard_config.py | 2 +- hikari/colors.py | 21 +++++++++++---------- hikari/colours.py | 3 ++- hikari/events.py | 8 ++++---- hikari/guilds.py | 18 ++++++++++++------ 14 files changed, 49 insertions(+), 82 deletions(-) delete mode 100644 examples/example_basic_config.json delete mode 100644 examples/example_config.json delete mode 100644 examples/example_config.yaml diff --git a/examples/example_basic_config.json b/examples/example_basic_config.json deleted file mode 100644 index 5e52a1dcee..0000000000 --- a/examples/example_basic_config.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "token": "ashjkdfkasjdhfajkhfdkajghak==" -} diff --git a/examples/example_config.json b/examples/example_config.json deleted file mode 100644 index 07620e8016..0000000000 --- a/examples/example_config.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "gateway": { - "allow_redirects": false, - "protocol": { - "proxy_auth": "Basic 1a2b3c4d==", - "proxy_url": "http://my.first.proxy.net:8080", - "request_timeout": 30.0, - "verify_ssl": true - }, - "version": 6 - }, - "http": { - "allow_redirects": true, - "protocol": { - "proxy_auth": "Basic 4d3c2b1a==", - "proxy_url": "http://my.other.proxy.net:8080", - "request_timeout": 30.0, - "ssl_context": "mybot.utils.ssl#MySSLContext", - "verify_ssl": true - }, - "version": 7 - }, - "token": "ashjkdfkasjdhfajkhfdkajghak==" -} diff --git a/examples/example_config.yaml b/examples/example_config.yaml deleted file mode 100644 index 0a431aec1a..0000000000 --- a/examples/example_config.yaml +++ /dev/null @@ -1,28 +0,0 @@ -# You would want to use a library such as PyYAML to deserialize this file into -# a Python dict first in the same way you'd use the `json` library to do it if -# you were using JSON config files. - -gateway: - allow_redirects: false - - protocol: - proxy_auth: Basic 1a2b3c4d== - proxy_url: http://my.first.proxy.net:8080 - request_timeout: 30.0 - verify_ssl: true - - version: 6 - -http: - allow_redirects: true - - protocol: - proxy_auth: Basic 4d3c2b1a== - proxy_url: http://my.other.proxy.net:8080 - request_timeout: 30.0 - ssl_context: mybot.utils.ssl#MySSLContext - verify_ssl: true - - version: 7 - -token: ashjkdfkasjdhfajkhfdkajghak== diff --git a/hikari/channels.py b/hikari/channels.py index c624323526..c0d2096c5c 100644 --- a/hikari/channels.py +++ b/hikari/channels.py @@ -24,6 +24,7 @@ hikari.entities.HikariEntity hikari.entities.Deserializable hikari.entities.Serializable + hikari.snowflakes.UniqueEntity :parts: 1 """ diff --git a/hikari/clients/app_config.py b/hikari/clients/app_config.py index 0130ca280d..1115dcefa6 100644 --- a/hikari/clients/app_config.py +++ b/hikari/clients/app_config.py @@ -21,6 +21,7 @@ import typing +from hikari.clients import protocol_config from hikari.internal import marshaller from hikari import entities from hikari.clients import gateway_config @@ -126,6 +127,17 @@ class AppConfig(entities.HikariEntity, entities.Deserializable): simply for reference for this example. """ + #: Low level protocol details, such as proxy configuration and SSL settings. + #: + #: If specified, this will be the default for gateway and HTTP settings, + #: unless they also specifically override this. The component-specific + #: settings will always take precedence over this setting where specified. + #: + #: :type: :obj:`hikari.clients.protocol_config.HTTPProtocolConfig` + protocol: typing.Optional[protocol_config.HTTPProtocolConfig] = marshaller.attrib( + deserializer=protocol_config.HTTPProtocolConfig.deserialize, if_undefined=None, default=None, + ) + #: The HTTP configuration to use. #: #: If unspecified or ``None```, then this will be a set of default values. diff --git a/hikari/clients/gateway_config.py b/hikari/clients/gateway_config.py index ce543d587d..1df9cb9635 100644 --- a/hikari/clients/gateway_config.py +++ b/hikari/clients/gateway_config.py @@ -39,7 +39,7 @@ class GatewayConfig(entities.HikariEntity, entities.Deserializable): All fields are optional kwargs that can be passed to the constructor. "Deserialized" and "unspecified" defaults are only applicable if you - create the object using :meth:`deserialize`. + create the object using :meth:`hikari.entities.Deserializable.deserialize`. """ #: Whether to enable debugging mode for the generated shards. Usually you diff --git a/hikari/clients/gateway_runner.py b/hikari/clients/gateway_runner.py index 097a004a16..9f473930eb 100644 --- a/hikari/clients/gateway_runner.py +++ b/hikari/clients/gateway_runner.py @@ -64,7 +64,7 @@ def _supports_color(): @click.option("--verify-ssl", default=True, type=click.BOOL, help="Enable or disable SSL verification.") @click.option("--version", default=7, type=click.IntRange(min=6), help="Version of the gateway to use.") def run_gateway(compression, color, debug, logger, shards, token, url, verify_ssl, version) -> None: - """A :mod:`click` command line client for running a test gateway connection. + """:mod:`click` command line client for running a test gateway connection. This is provided for internal testing purposes for benchmarking API stabiltiy, etc. diff --git a/hikari/clients/http_config.py b/hikari/clients/http_config.py index e61a3fe3ee..889a7a8de8 100644 --- a/hikari/clients/http_config.py +++ b/hikari/clients/http_config.py @@ -34,14 +34,14 @@ class HTTPConfig(entities.HikariEntity, entities.Deserializable): All fields are optional kwargs that can be passed to the constructor. "Deserialized" and "unspecified" defaults are only applicable if you - create the object using :meth:`deserialize`. + create the object using :meth:`hikari.entities.Deserializable.deserialize`. """ #: Low level protocol details, such as proxy configuration and SSL settings. #: #: If unspecified, defaults are used. #: - #: :type: :obj:`hikari.protocol_config.HTTPProtocolConfig` + #: :type: :obj:`hikari.clients.protocol_config.HTTPProtocolConfig` protocol: typing.Optional[protocol_config.HTTPProtocolConfig] = marshaller.attrib( deserializer=protocol_config.HTTPProtocolConfig.deserialize, if_undefined=None, default=None, ) diff --git a/hikari/clients/shard_client.py b/hikari/clients/shard_client.py index e43d73b362..890f99b180 100644 --- a/hikari/clients/shard_client.py +++ b/hikari/clients/shard_client.py @@ -93,7 +93,8 @@ class ShardClient(websocket_client.WebsocketClient): Notes ----- - Generally, you want to use :class:`GatewayClient` rather than this class + Generally, you want to use + :obj:`hikari.clients.gateway_client.GatewayClient` rather than this class directly, as that will handle sharding where enabled and applicable, and provides a few more bits and pieces that may be useful such as state management and event dispatcher integration. and If you want to customize diff --git a/hikari/clients/shard_config.py b/hikari/clients/shard_config.py index d3b468d224..749ec2deb8 100644 --- a/hikari/clients/shard_config.py +++ b/hikari/clients/shard_config.py @@ -55,7 +55,7 @@ class ShardConfig(entities.HikariEntity, entities.Deserializable): All fields are optional kwargs that can be passed to the constructor. "Deserialized" and "unspecified" defaults are only applicable if you - create the object using :meth:`deserialize`. + create the object using :meth:`hikari.entities.Deserializable.deserialize`. """ #: The shard IDs to produce shard connections for. diff --git a/hikari/colors.py b/hikari/colors.py index 00d6e93ee0..2308ec433d 100644 --- a/hikari/colors.py +++ b/hikari/colors.py @@ -23,6 +23,7 @@ hikari.colors :parts: 1 """ +from __future__ import annotations __all__ = ["Color", "ColorCompatibleT"] @@ -107,7 +108,7 @@ class Color(int, typing.SupportsInt): __slots__ = () - def __new__(cls, raw_rgb: typing.SupportsInt) -> "Color": + def __new__(cls, raw_rgb: typing.SupportsInt) -> Color: assertions.assert_in_range(raw_rgb, 0, 0xFF_FF_FF, "integer value") return super(Color, cls).__new__(cls, raw_rgb) @@ -172,7 +173,7 @@ def is_web_safe(self) -> bool: # noqa: D401 return all(_all_same(*c) for c in (hex_code[:2], hex_code[2:4], hex_code[4:])) @classmethod - def from_rgb(cls, red: int, green: int, blue: int) -> "Color": + def from_rgb(cls, red: int, green: int, blue: int) -> Color: """Convert the given RGB to a :obj:`Color` object. Each channel must be withing the range [0, 255] (0x0, 0xFF). @@ -203,7 +204,7 @@ def from_rgb(cls, red: int, green: int, blue: int) -> "Color": return cls((red << 16) | (green << 8) | blue) @classmethod - def from_rgb_float(cls, red_f: float, green_f: float, blue_f: float) -> "Color": + def from_rgb_float(cls, red_f: float, green_f: float, blue_f: float) -> Color: """Convert the given RGB to a :obj:`Color` object. The colorspace represented values have to be within the @@ -235,7 +236,7 @@ def from_rgb_float(cls, red_f: float, green_f: float, blue_f: float) -> "Color": return cls.from_rgb(int(red_f * 0xFF), int(green_f * 0xFF), int(blue_f * 0xFF)) @classmethod - def from_hex_code(cls, hex_code: str) -> "Color": + def from_hex_code(cls, hex_code: str) -> Color: """Convert the given hexadecimal color code to a :obj:`Color`. The inputs may be of the following format (case insensitive): @@ -277,7 +278,7 @@ def from_hex_code(cls, hex_code: str) -> "Color": raise ValueError("Color code is invalid length. Must be 3 or 6 digits") @classmethod - def from_int(cls, i: typing.SupportsInt) -> "Color": + def from_int(cls, i: typing.SupportsInt) -> Color: """Convert the given :obj:`typing.SupportsInt` to a :obj:`Color`. Parameters @@ -294,7 +295,7 @@ def from_int(cls, i: typing.SupportsInt) -> "Color": # Partially chose to override these as the docstrings contain typos according to Sphinx. @classmethod - def from_bytes(cls, bytes_: typing.Iterable[int], byteorder: str, *, signed: bool = True) -> "Color": + def from_bytes(cls, bytes_: typing.Sequence[int], byteorder: str, *, signed: bool = True) -> Color: """Convert the bytes to a :obj:`Color`. Parameters @@ -302,7 +303,7 @@ def from_bytes(cls, bytes_: typing.Iterable[int], byteorder: str, *, signed: boo bytes_ : :obj:`typing.Iterable` [ :obj:`int` ] A iterable of :obj:`int` byte values. - byteorder : :obj:str` + byteorder : :obj:`str` The endianess of the value represented by the bytes. Can be ``"big"`` endian or ``"little"`` endian. @@ -324,7 +325,7 @@ def to_bytes(self, length: int, byteorder: str, *, signed: bool = True) -> bytes length : :obj:`int` The number of bytes to produce. Should be around ``3``, but not less. - byteorder : :obj:str` + byteorder : :obj:`str` The endianess of the value represented by the bytes. Can be ``"big"`` endian or ``"little"`` endian. @@ -334,12 +335,12 @@ def to_bytes(self, length: int, byteorder: str, *, signed: bool = True) -> bytes Returns ------- :obj:`bytes` - The bytes represntation of the Color. + The bytes representation of the Color. """ return int(self).to_bytes(length, byteorder, signed=signed) @classmethod - def __class_getitem__(cls, color: "ColorCompatibleT") -> "Color": # pylint:disable=arguments-differ + def __class_getitem__(cls, color: ColorCompatibleT) -> Color: # pylint:disable=arguments-differ if isinstance(color, cls): return color elif isinstance(color, int): diff --git a/hikari/colours.py b/hikari/colours.py index 0b319e9e36..6e1e19bace 100644 --- a/hikari/colours.py +++ b/hikari/colours.py @@ -17,6 +17,7 @@ # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . """Alias for the :mod:`hikari.colors` module.""" -__all__ = ["Colour"] +__all__ = ["Colour", "ColourCompatibleT"] from hikari.colors import Color as Colour +from hikari.colors import ColorCompatibleT as ColourCompatibleT diff --git a/hikari/events.py b/hikari/events.py index 8266c2727a..266f656c76 100644 --- a/hikari/events.py +++ b/hikari/events.py @@ -654,9 +654,9 @@ class InviteCreateEvent(HikariEvent, entities.Deserializable): ) #: The limit for how many times this invite can be used before it expires. - #: If set to ``0`` then this is unlimited. + #: If set to ``0``, or infinity (``float("inf")``) then this is unlimited. #: - #: :type: :obj:`typing.Union` [ :obj:`int`, :obj:`float(inf)` ] + #: :type: :obj:`typing.Union` [ :obj:`int`, :obj:`float` ( ``"inf"`` ) ] max_uses: typing.Union[int, float] = marshaller.attrib(deserializer=lambda count: count or float("inf")) #: The object of the user who this invite targets, if set. @@ -808,7 +808,7 @@ class MessageUpdateEvent(HikariEvent, snowflakes.UniqueEntity, entities.Deserial #: The channels the message mentions. #: - #: :type: :obj:`typing.Union` [ :obj:`typing.Set` [ :obj:`hikari.snowflakes.Snowflake` ], :obj:`entities.UNSET` ] + #: :type: :obj:`typing.Union` [ :obj:`typing.Set` [ :obj:`hikari.snowflakes.Snowflake` ], :obj:`hikari.entities.UNSET` ] channel_mentions: typing.Union[typing.Set[snowflakes.Snowflake], entities.Unset] = marshaller.attrib( raw_name="mention_channels", deserializer=lambda channel_mentions: {snowflakes.Snowflake.deserialize(c["id"]) for c in channel_mentions}, @@ -1076,7 +1076,7 @@ class MessageReactionRemoveEmojiEvent(HikariEvent, entities.Deserializable): #: The object of the emoji that's being removed. #: - #: :type: :obj:`typing.Union` [ :obj:`hikari.emojis.UnknownEmoji`, :obj:`emojis.UnicodeEmoji` ] + #: :type: :obj:`typing.Union` [ :obj:`hikari.emojis.UnknownEmoji`, :obj:`hikari.emojis.UnicodeEmoji` ] emoji: typing.Union[_emojis.UnicodeEmoji, _emojis.UnknownEmoji] = marshaller.attrib( deserializer=_emojis.deserialize_reaction_emoji, ) diff --git a/hikari/guilds.py b/hikari/guilds.py index bdea441af4..a0a71a0b03 100644 --- a/hikari/guilds.py +++ b/hikari/guilds.py @@ -16,7 +16,11 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . -"""Components and entities that are used to describe guilds on Discord.""" +"""Components and entities that are used to describe guilds on Discord. + +.. inheritance-diagram:: + hikari.guilds +""" __all__ = [ "ActivityFlag", "ActivityType", @@ -816,9 +820,10 @@ class Guild(PartialGuild): Note ---- If a guild object is considered to be unavailable, then the state of any - other fields other than the :attr:`is_unavailable` and :attr:`id` members - outdated, or incorrect. If a guild is unavailable, then the contents of any - other fields should be ignored. + other fields other than the :attr:`is_unavailable` and + :obj:`hikari.entities.UniqueEntity.id` members outdated, or incorrect. + If a guild is unavailable, then the contents of any other fields should be + ignored. """ #: The hash of the splash for the guild, if there is one. @@ -1104,7 +1109,7 @@ class Guild(PartialGuild): #: The guild's description. #: - #: This is only present if certain :attr:`Guild.features` are set in this + #: This is only present if certain :obj:`Guild.features` are set in this #: guild. Otherwise, this will always be ``None``. For all other purposes, #: it is ``None``. #: @@ -1113,7 +1118,8 @@ class Guild(PartialGuild): #: The hash for the guild's banner. #: This is only present if the guild has :obj:`GuildFeature.BANNER` in the - #: :attr:`features` for this guild. For all other purposes, it is ``None``. + #: :obj:`Guild.features` for this guild. For all other purposes, it is + # ``None``. #: #: :type: :obj:`str`, optional banner_hash: typing.Optional[str] = marshaller.attrib(raw_name="banner", if_none=None, deserializer=str) From 17d0b13dcf8d0741ca5e9233bde8a4282bfc912a Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Sat, 4 Apr 2020 21:48:05 +0100 Subject: [PATCH 063/922] Fixed a couple of bugs I missed earlier; added dockerfile --- Dockerfile | 3 +++ docker-compose.yml | 6 ++++++ hikari/clients/gateway_client.py | 5 +---- hikari/clients/gateway_runner.py | 3 ++- 4 files changed, 12 insertions(+), 5 deletions(-) create mode 100644 Dockerfile create mode 100644 docker-compose.yml diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000000..af5a925783 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,3 @@ +FROM python:3 +RUN pip install --pre hikari>=1.0.0 +ENTRYPOINT python -m hikari.clients.gateway_runner diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000000..0c1446af1e --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,6 @@ +version: "3" + +services: + gateway-test: + build: . + restart: always diff --git a/hikari/clients/gateway_client.py b/hikari/clients/gateway_client.py index daf2f3734d..1a21cf5090 100644 --- a/hikari/clients/gateway_client.py +++ b/hikari/clients/gateway_client.py @@ -58,7 +58,7 @@ def __init__( self.raw_event_consumer = raw_event_consumer_impl self._is_running = False self.shards: typing.Dict[int, ShardT] = { - shard_id: shard_type(shard_id, config, self._handle_websocket_event_later, url) + shard_id: shard_type(shard_id, config, raw_event_consumer_impl, url) for shard_id in config.shard_config.shard_ids } @@ -106,6 +106,3 @@ async def close(self, wait: bool = True) -> None: self.logger.info("stopped %s shard(s) in approx %.2fs", len(self.shards), finish_time - start_time) self._is_running = False - def _handle_websocket_event_later(self, conn: shard.ShardConnection, event_name: str, payload: typing.Any) -> None: - shard_client_obj = self.shards[conn.shard_id] - self.raw_event_consumer.process_raw_event(shard_client_obj, event_name, payload) diff --git a/hikari/clients/gateway_runner.py b/hikari/clients/gateway_runner.py index 9f473930eb..759501987d 100644 --- a/hikari/clients/gateway_runner.py +++ b/hikari/clients/gateway_runner.py @@ -33,6 +33,7 @@ from hikari.clients import gateway_client from hikari.clients import gateway_config from hikari.clients import protocol_config +from hikari.clients import shard_config from hikari.state import raw_event_consumer logger_levels = ("DEBUG", "INFO", "WARNING", "ERROR", "NOTSET") @@ -83,7 +84,7 @@ def process_raw_event( config=gateway_config.GatewayConfig( debug=debug, protocol=protocol_config.HTTPProtocolConfig(verify_ssl=verify_ssl), - shard_config=gateway_config.ShardConfig(shard_count=shards), + shard_config=shard_config.ShardConfig(shard_count=shards), token=token, use_compression=compression, version=version, From 8f5bc9db7d8b0107e1d87a04dba64394ffeea546 Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Sat, 4 Apr 2020 21:58:23 +0100 Subject: [PATCH 064/922] Fixed docker env_file schenanigans [skip ci] --- .gitignore | 3 +++ Dockerfile | 1 - docker-compose.yml | 3 +++ 3 files changed, 6 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index a9d37f8556..f64e0dd3b5 100644 --- a/.gitignore +++ b/.gitignore @@ -47,3 +47,6 @@ poetry.lock # Container logs **.container.log + +# Docker secrets +credentials.env diff --git a/Dockerfile b/Dockerfile index af5a925783..81e835e6fb 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,3 +1,2 @@ FROM python:3 RUN pip install --pre hikari>=1.0.0 -ENTRYPOINT python -m hikari.clients.gateway_runner diff --git a/docker-compose.yml b/docker-compose.yml index 0c1446af1e..c7915c89d5 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -4,3 +4,6 @@ services: gateway-test: build: . restart: always + entrypoint: python -m hikari + env_file: + - credentials.env From 257c88903b841480b63837010ae8c5838e1cbb50 Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Sat, 4 Apr 2020 22:00:04 +0100 Subject: [PATCH 065/922] Helps if I actually add the right thing in the file. [skip ci] --- docker-compose.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker-compose.yml b/docker-compose.yml index c7915c89d5..dc08683837 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -4,6 +4,6 @@ services: gateway-test: build: . restart: always - entrypoint: python -m hikari + entrypoint: python -m hikari.client.gateway_runner env_file: - credentials.env From 97214bf6e79f0101005d76035c7ad93c03b8a627 Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Sat, 4 Apr 2020 22:03:45 +0100 Subject: [PATCH 066/922] Maybe it is time to go to bed [skip ci] --- docker-compose.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker-compose.yml b/docker-compose.yml index dc08683837..68e27a514d 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -4,6 +4,6 @@ services: gateway-test: build: . restart: always - entrypoint: python -m hikari.client.gateway_runner + entrypoint: python -m hikari.clients.gateway_runner env_file: - credentials.env From 00d1f3cd8782b6c95db1257d0b5e043ba21ff48d Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Sat, 4 Apr 2020 21:41:42 +0000 Subject: [PATCH 067/922] Add debug flag [skip ci] --- docker-compose.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 68e27a514d..5013ddb19b 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -3,7 +3,7 @@ version: "3" services: gateway-test: build: . - restart: always - entrypoint: python -m hikari.clients.gateway_runner + entrypoint: python -m hikari.clients.gateway_runner --logger=DEBUG env_file: - credentials.env + restart: always From 29f1a129f897f49162e96fa04f985dc901857fce Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Sun, 5 Apr 2020 17:18:33 +0100 Subject: [PATCH 068/922] Coalesced and flattened configs, added stateless client base, logging and naming changes. - Made logging for shards consistent. ShardConnection only logs debugging messages now, ShardClient will monitor the full handshake to report the status tidily. - GatewayClient is now GatewayManager - Made EventManager a generic type - Turned constructor typechecking off for marshaller.attrs classes. - Logging helper now only produces class name. - Changed gateway_runner logging style slightly - Pluralised some module names. - Client configs rewritten to be one flat object using mixins to make the interface clearer. --- hikari/clients/__init__.py | 33 +- hikari/clients/app_config.py | 166 --------- hikari/clients/bot_client.py | 145 +++++++- hikari/clients/configs.py | 343 ++++++++++++++++++ hikari/clients/gateway_config.py | 165 --------- .../{gateway_client.py => gateway_manager.py} | 32 +- hikari/clients/gateway_runner.py | 36 +- hikari/clients/http_client.py | 20 - hikari/clients/http_config.py | 65 ---- hikari/clients/protocol_config.py | 135 ------- hikari/clients/rest_client.py | 47 +++ .../{websocket_client.py => runnable.py} | 21 +- hikari/clients/shard_client.py | 99 +++-- hikari/clients/shard_config.py | 97 ----- hikari/entities.py | 10 +- hikari/errors.py | 10 +- hikari/internal/conversions.py | 6 + hikari/internal/marshaller.py | 13 +- hikari/internal/more_logging.py | 4 +- hikari/net/shard.py | 14 +- ...ent_dispatcher.py => event_dispatchers.py} | 6 +- .../{event_manager.py => event_managers.py} | 28 +- ...ent_consumer.py => raw_event_consumers.py} | 0 hikari/state/stateless_event_manager_impl.py | 31 -- tests/hikari/internal/test_more_logging.py | 10 +- tests/hikari/state/test_event_dispatcher.py | 4 +- 26 files changed, 724 insertions(+), 816 deletions(-) delete mode 100644 hikari/clients/app_config.py create mode 100644 hikari/clients/configs.py delete mode 100644 hikari/clients/gateway_config.py rename hikari/clients/{gateway_client.py => gateway_manager.py} (81%) delete mode 100644 hikari/clients/http_client.py delete mode 100644 hikari/clients/http_config.py delete mode 100644 hikari/clients/protocol_config.py create mode 100644 hikari/clients/rest_client.py rename hikari/clients/{websocket_client.py => runnable.py} (85%) delete mode 100644 hikari/clients/shard_config.py rename hikari/state/{event_dispatcher.py => event_dispatchers.py} (98%) rename hikari/state/{event_manager.py => event_managers.py} (86%) rename hikari/state/{raw_event_consumer.py => raw_event_consumers.py} (100%) delete mode 100644 hikari/state/stateless_event_manager_impl.py diff --git a/hikari/clients/__init__.py b/hikari/clients/__init__.py index 1e74f58720..270cec9184 100644 --- a/hikari/clients/__init__.py +++ b/hikari/clients/__init__.py @@ -18,34 +18,25 @@ # along with Hikari. If not, see . """The models API for interacting with Discord directly.""" -from hikari.clients import app_config from hikari.clients import bot_client -from hikari.clients import gateway_client -from hikari.clients import gateway_config -from hikari.clients import http_client -from hikari.clients import http_config -from hikari.clients import protocol_config -from hikari.clients import websocket_client +from hikari.clients import configs +from hikari.clients import gateway_manager +from hikari.clients import rest_client +from hikari.clients import runnable -from hikari.clients.app_config import * from hikari.clients.bot_client import * -from hikari.clients.gateway_client import * -from hikari.clients.gateway_config import * -from hikari.clients.http_client import * -from hikari.clients.http_config import * -from hikari.clients.protocol_config import * +from hikari.clients.configs import * +from hikari.clients.gateway_manager import * +from hikari.clients.rest_client import * from hikari.clients.shard_client import * -from hikari.clients.websocket_client import * +from hikari.clients.runnable import * __all__ = [ - *app_config.__all__, *bot_client.__all__, - *gateway_client.__all__, - *gateway_config.__all__, - *http_client.__all__, - *http_config.__all__, - *protocol_config.__all__, + *configs.__all__, + *gateway_manager.__all__, + *rest_client.__all__, *shard_client.__all__, - *websocket_client.__all__, + *runnable.__all__, ] diff --git a/hikari/clients/app_config.py b/hikari/clients/app_config.py deleted file mode 100644 index 1115dcefa6..0000000000 --- a/hikari/clients/app_config.py +++ /dev/null @@ -1,166 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# Copyright © Nekoka.tt 2019-2020 -# -# This file is part of Hikari. -# -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . -"""Core application configuration objects and options.""" -__all__ = ["AppConfig"] - -import typing - -from hikari.clients import protocol_config -from hikari.internal import marshaller -from hikari import entities -from hikari.clients import gateway_config -from hikari.clients import http_config - - -@marshaller.attrs(kw_only=True) -class AppConfig(entities.HikariEntity, entities.Deserializable): - """Root application configuration object. - - All fields are optional kwargs that can be passed to the constructor. - - "Deserialized" and "unspecified" defaults are only applicable if you - create the object using ``deserialize()``. - - Examples - -------- - Initializing programatically: - .. code-block:: python - - # Basic config - config = AppConfig(token="1a2b3c4da9089288a.23rhagaa8==") - - .. code-block:: python - - # A more complicated config example - config = AppConfig( - gateway=GatewayConfig( - protocol=HTTPProtocolConfig( - allow_redirects=False, - proxy_auth=aiohttp.BasicAuth("username", "password"), - proxy_url="http://my.first.proxy.net:8080", - request_timeout=30.0, - verify_ssl=False, # heresy! do not do this! - ), - sharding=ShardConfig( - shard_ids=range(0, 10), - shard_count=10 - ), - version=6, - ), - http=HTTPConfig( - protocol=HTTPProtocolConfig( - allow_redirects=True, - proxy_auth=aiohttp.BasicAuth.decode("Basic dXNlcm5hbWU6cGFzc3dvcmQ="), - proxy_url="http://my.other.proxy.net:8080", - request_timeout=30.0, - ssl_context=mybot.utils.ssl.MySSLContext, - verify_ssl=True - ), - version=7, - ), - token="1a2b3c4da9089288a.23rhagaa8==", - ) - - Initializing from a file: - .. code-block:: python - - # loading a JSON file - with open("foo.json") as fp: - config = AppConfig.deserialize(json.load(fp)) - - .. code-block:: js - - /* basic config */ - { - "token": "1a2b3c4da9089288a.23rhagaa8==" - } - - .. code-block:: js - - /* a more complicated config example */ - { - "gateway": { - "protocol": { - "allow_redirects": false, - "proxy_auth": "Basic dXNlcm5hbWU6cGFzc3dvcmQ=", - "proxy_url": "http://my.first.proxy.net:8080", - "request_timeout": 30.0, - "verify_ssl": false // heresy, do not do this! - }, - "sharding": { - "shard_ids": "0..10", - "shard_count": 10 - }, - "version": 6 - }, - "http": { - "protocol": { - "allow_redirects": true, - "proxy_auth": "Basic dXNlcm5hbWU6cGFzc3dvcmQ=", - "proxy_url": "http://my.other.proxy.net:8080", - "request_timeout": 30.0, - "ssl_context": "mybot.utils.ssl#MySSLContext", - "verify_ssl": true - }, - "version": 7 - }, - "token": "1a2b3c4da9089288a.23rhagaa8==" - } - - Of course, comments are not valid in actual standard JSON; I added them - simply for reference for this example. - """ - - #: Low level protocol details, such as proxy configuration and SSL settings. - #: - #: If specified, this will be the default for gateway and HTTP settings, - #: unless they also specifically override this. The component-specific - #: settings will always take precedence over this setting where specified. - #: - #: :type: :obj:`hikari.clients.protocol_config.HTTPProtocolConfig` - protocol: typing.Optional[protocol_config.HTTPProtocolConfig] = marshaller.attrib( - deserializer=protocol_config.HTTPProtocolConfig.deserialize, if_undefined=None, default=None, - ) - - #: The HTTP configuration to use. - #: - #: If unspecified or ``None```, then this will be a set of default values. - #: - #: :type: :obj:`hikari.clients.http_config.HTTPConfig`, optional - http: typing.Optional[http_config.HTTPConfig] = marshaller.attrib( - deserializer=http_config.HTTPConfig.deserialize, if_none=None, if_undefined=None, default=None - ) - - #: The gateway configuration to use. - #: - #: If unspecified or ``None```, then this will be a set of default values. - #: - #: :type: :obj:`hikari.clients.gateway_config.GatewayConfig`, optional - gateway: typing.Optional[gateway_config.GatewayConfig] = marshaller.attrib( - deserializer=gateway_config.GatewayConfig.deserialize, if_none=None, if_undefined=None, default=None - ) - - #: The global token to use, if applicable. This can be overridden for each - #: component that requires it. - #: - #: Note that this should not start with ``Bot`` or ``Bearer``. This is - #: detected automatically. - #: - #: :type: :obj:`str`, optional - token: typing.Optional[str] = marshaller.attrib(deserializer=str, if_none=None, if_undefined=None, default=None) diff --git a/hikari/clients/bot_client.py b/hikari/clients/bot_client.py index 59042b1470..000cc1660c 100644 --- a/hikari/clients/bot_client.py +++ b/hikari/clients/bot_client.py @@ -17,4 +17,147 @@ # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . """A bot client might go here... eventually...""" -__all__ = [] +__all__ = ["BotBase", "StatelessBot"] + +import abc +import asyncio +import datetime +import logging +import typing + +from hikari import events +from hikari.clients import configs +from hikari.clients import gateway_manager +from hikari.clients import rest_client +from hikari.clients import runnable +from hikari.clients import shard_client +from hikari.internal import more_asyncio +from hikari.internal import more_logging +from hikari.state import event_dispatchers +from hikari.state import event_managers + + +class BotBase(runnable.RunnableClient, event_dispatchers.EventDispatcher): + """An abstract base class for a bot implementation. + + Parameters + ---------- + config : :obj:`hikari.clients.configs.BotConfig` + The config object to use. + event_manager : ``EventManagerT`` + The event manager to use. This must be a subclass of + :obj:`hikari.state.event_managers.EventManager` + """ + + #: The config for this bot. + #: + #: :type: :obj:`hikari.clients.configs.BotConfig` + config: configs.BotConfig + + #: The event manager for this bot. + #: + #: :type: a subclass of :obj:`hikari.state.event_managers.EventManager` + event_manager: event_managers.EventManager + + #: The gateway for this bot. + #: + #: :type: :obj:`hikari.clients.gateway_client.GatewayClient` + gateway: gateway_manager.GatewayManager[shard_client.ShardClient] + + #: The logger to use for this bot. + #: + #: :type: :obj:`logging.Logger` + logger: logging.Logger + + #: The REST HTTP client to use for this bot. + #: + #: :type: :obj:`hikari.clients.rest_client.RESTClient` + rest: rest_client.RESTClient + + @abc.abstractmethod + def __init__(self, config: configs.BotConfig, event_manager: event_managers.EventManager) -> None: + super().__init__(more_logging.get_named_logger(self)) + self.config = config + self.event_manager = event_manager + self.gateway = NotImplemented + self.rest = rest_client.RESTClient(self.config) + + async def start(self): + while (gateway_bot := await self.rest.fetch_gateway_bot()).session_start_limit.remaining <= 0: + resume_at = datetime.datetime.now() + gateway_bot.session_start_limit.reset_after + + self.logger.critical( + "You have reached the max identify limit for this time window (%s). " + "To prevent your token being reset, I will wait for %s (until approx %s) " + "and then continue signing in. Press CTRL-C to shut down.", + gateway_bot.session_start_limit.total, + gateway_bot.session_start_limit.reset_after, + resume_at, + ) + + await asyncio.sleep(60) + while (now := datetime.datetime.now()) < resume_at: + self.logger.info("Still waiting, %s to go...", resume_at - now) + await asyncio.sleep(60) + + self.logger.info( + "You have sent an IDENTIFY %s time(s) before now, and have %s remaining. This will reset at %s.", + gateway_bot.session_start_limit.total - gateway_bot.session_start_limit.remaining, + gateway_bot.session_start_limit.remaining, + datetime.datetime.now() + gateway_bot.session_start_limit.reset_after, + ) + + shard_count = self.config.shard_count if self.config.shard_count else gateway_bot.shard_count + shard_ids = self.config.shard_ids if self.config.shard_ids else [*range(shard_count)] + + self.gateway = gateway_manager.GatewayManager( + config=self.config, + url=gateway_bot.url, + raw_event_consumer_impl=self.event_manager, + shard_ids=shard_ids, + shard_count=shard_count, + ) + + await self.gateway.start() + + async def close(self, wait: bool = True): + await self.gateway.close(wait) + self.event_manager.event_dispatcher.close() + await self.rest.close() + + async def join(self) -> None: + await self.gateway.join() + + def add_listener( + self, event_type: typing.Type[event_dispatchers.EventT], callback: event_dispatchers.EventCallbackT + ) -> event_dispatchers.EventCallbackT: + return self.event_manager.event_dispatcher.remove_listener(event_type, callback) + + def remove_listener( + self, event_type: typing.Type[event_dispatchers.EventT], callback: event_dispatchers.EventCallbackT + ) -> event_dispatchers.EventCallbackT: + return self.event_manager.event_dispatcher.remove_listener(event_type, callback) + + def wait_for( + self, + event_type: typing.Type[event_dispatchers.EventT], + *, + timeout: typing.Optional[float], + predicate: event_dispatchers.PredicateT, + ) -> more_asyncio.Future: + return self.event_manager.event_dispatcher.wait_for(event_type, timeout=timeout, predicate=predicate) + + def dispatch_event(self, event: events.HikariEvent) -> more_asyncio.Future[typing.Any]: + return self.event_manager.event_dispatcher.dispatch_event(event) + + +class StatelessBot(BotBase): + """Bot client without any state internals. + + Parameters + ---------- + config : :obj:`hikari.clients.configs.BotConfig` + The config object to use. + """ + def __init__(self, config: configs.BotConfig) -> None: + super().__init__(config, event_managers.StatelessEventManagerImpl()) diff --git a/hikari/clients/configs.py b/hikari/clients/configs.py new file mode 100644 index 0000000000..bdd7354d95 --- /dev/null +++ b/hikari/clients/configs.py @@ -0,0 +1,343 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# Copyright © Nekoka.tt 2019-2020 +# +# This file is part of Hikari. +# +# Hikari is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Hikari is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Hikari. If not, see . +"""Configuration data classes.""" + +__all__ = [ + "generate_config_attrs", + "BaseConfig", + "DebugConfig", + "AIOHTTPConfig", + "TokenConfig", + "WebsocketConfig", + "ShardConfig", + "RESTConfig", + "BotConfig", +] + +import datetime +import re +import ssl +import typing + +import aiohttp + +from hikari import entities +from hikari import gateway_entities +from hikari import guilds +from hikari.internal import conversions +from hikari.internal import marshaller +from hikari.net import codes + + +class BaseConfig(entities.Deserializable): + """Base class for any configuration data class.""" + if typing.TYPE_CHECKING: + # pylint:disable=unused-argument + # Screws up PyCharm and makes annoying warnings everywhere, so just + # mute this. We can always make dummy constructors later, or find + # another way around this perhaps. + # This only ever takes kwargs. + @typing.no_type_check + def __init__(self, **kwargs) -> None: + ... + + # pylint:enable=unused-argument + + +#: Decorator for :obj:`attr.s` classes that use the +#: :obj:`hikari.internal.marshaller` protocol. +generate_config_attrs = marshaller.attrs(kw_only=True) + + +@generate_config_attrs +class DebugConfig(BaseConfig): + """Configuration for anything with a debugging mode.""" + #: Whether to enable debugging mode. Usually you don't want to enable this. + #: + #: :type: :obj:`bool` + debug: bool = marshaller.attrib(deserializer=bool, if_undefined=False, default=False) + + +@generate_config_attrs +class AIOHTTPConfig(BaseConfig): + """Config for components that use AIOHTTP somewhere.""" + + #: If ``True``, allow following redirects from ``3xx`` HTTP responses. + #: Generally you do not want to enable this unless you have a good reason + #: to. + #: + #: Defaults to ``False`` if unspecified during deserialization. + #: + #: :type: :obj:`bool` + allow_redirects: bool = marshaller.attrib(deserializer=bool, if_undefined=False, default=False) + + #: Either an implementation of :obj:`aiohttp.TCPConnector`. + #: + #: This may otherwise be ``None`` to use the default settings provided + #: by :mod:`aiohttp`. + #: + #: This is deserialized as an object reference in the format + #: ``package.module#object.attribute`` that is expected to point to the + #: desired value. + #: + #: Defaults to ``None`` if unspecified during deserialization. + #: + #: :type: :obj:`aiohttp.TCPConnector`, optional + tcp_connector: typing.Optional[aiohttp.TCPConnector] = marshaller.attrib( + deserializer=marshaller.dereference_handle, if_none=None, if_undefined=None, default=None + ) + + #: Optional proxy headers to provide in any HTTP requests. + #: + #: Defaults to ``None`` if unspecified during deserialization. + #: + #: :type: :obj:`typing.Mapping` [ :obj:`str`, :obj:`str` ], optional + proxy_headers: typing.Optional[typing.Mapping[str, str]] = marshaller.attrib( + deserializer=dict, if_none=None, if_undefined=None, default=None + ) + + #: Optional proxy authorization to provide in any HTTP requests. + #: + #: This is deserialized using the format ``"basic {{base 64 string here}}"``. + #: + #: Defaults to ``None`` if unspecified during deserialization. + #: + #: :type: :obj:`aiohttp.BasicAuth`, optional + proxy_auth: typing.Optional[aiohttp.BasicAuth] = marshaller.attrib( + deserializer=aiohttp.BasicAuth.decode, if_none=None, if_undefined=None, default=None + ) + + #: The optional URL of the proxy to send requests via. + #: + #: Defaults to ``None`` if unspecified during deserialization. + #: + #: :type: :obj:`str`, optional + proxy_url: typing.Optional[str] = marshaller.attrib(deserializer=str, if_undefined=None, if_none=None, default=None) + + #: Optional request timeout to use. If an HTTP request takes longer than + #: this, it will be aborted. + #: + #: If not ``None``, the value represents a number of seconds as a floating + #: point number. + #: + #: Defaults to ``None`` if unspecified during deserialization. + #: + #: :type: :obj:`float`, optional + request_timeout: typing.Optional[float] = marshaller.attrib( + deserializer=float, if_undefined=None, if_none=None, default=None + ) + + #: The optional SSL context to use. + #: + #: This is deserialized as an object reference in the format + #: ``package.module#object.attribute`` that is expected to point to the + #: desired value. + #: + #: Defaults to ``None`` if unspecified during deserialization. + #: + #: :type: :obj:`ssl.SSLContext`, optional + ssl_context: typing.Optional[ssl.SSLContext] = marshaller.attrib( + deserializer=marshaller.dereference_handle, if_none=None, if_undefined=None, default=None + ) + + #: If ``True``, then responses with invalid SSL certificates will be + #: rejected. Generally you want to keep this enabled unless you have a + #: problem with SSL and you know exactly what you are doing by disabling + #: this. Disabling SSL verification can have major security implications. + #: You turn this off at your own risk. + #: + #: Defaults to ``True`` if unspecified during deserialization. + #: + #: :type: :obj:`bool` + verify_ssl: bool = marshaller.attrib(deserializer=bool, if_undefined=True, default=True) + + +@generate_config_attrs +class TokenConfig(BaseConfig): + """Token config options.""" + + #: The token to use. + #: + #: :type: :obj:`str`, optional + token: typing.Optional[str] = marshaller.attrib(deserializer=str, if_none=None, if_undefined=None, default=None) + + +@generate_config_attrs +class WebsocketConfig(AIOHTTPConfig, TokenConfig, DebugConfig): + """Single-websocket specific configuration options.""" + + #: Whether to use zlib compression on the gateway for inbound messages or + #: not. Usually you want this turned on. + #: + #: :type: :obj:`bool` + gateway_use_compression: bool = marshaller.attrib(deserializer=bool, if_undefined=True, default=True) + + #: The gateway API version to use. + #: + #: If unspecified, then V6 is used. + #: + #: :type: :obj:`int` + gateway_version: int = marshaller.attrib(deserializer=int, if_undefined=lambda: 6, default=6) + + #: The initial activity to set all shards to when starting the gateway. If + #: ``None``, then no activity will be set. + #: + #: :type: :obj:`hikari.gateway_entities.GatewayActivity`, optional + initial_activity: typing.Optional[gateway_entities.GatewayActivity] = marshaller.attrib( + deserializer=gateway_entities.GatewayActivity.deserialize, if_none=None, if_undefined=None, default=None + ) + + #: The initial status to set the shards to when starting the gateway. + #: + #: :type: :obj:`str` + initial_status: guilds.PresenceStatus = marshaller.attrib( + deserializer=guilds.PresenceStatus.__getitem__, if_undefined=lambda: "online", default="online", + ) + + #: Whether to show up as AFK or not on sign-in. + #: + #: :type: :obj:`bool` + initial_is_afk: bool = marshaller.attrib(deserializer=bool, if_undefined=False, default=False) + + #: The idle time to show on signing in, or ``None`` to not show an idle + #: time. + #: + #: :type: :obj:`datetime.datetime`, optional + initial_idle_since: typing.Optional[datetime.datetime] = marshaller.attrib( + deserializer=conversions.unix_epoch_to_ts, if_none=None, if_undefined=None, default=None + ) + + #: The intents to use for the connection. + #: + #: If being deserialized, this can be an integer bitfield, or a sequence of + #: intent names. If + #: unspecified, this will be set to ``None``. + #: + #: :type: :obj:`hikari.net.codes.GatewayIntent`, optional + #: + #: Examples + #: -------- + #: + #: .. code-block:: python + #: + #: # Python example + #: GatewayIntent.GUILDS | GatewayIntent.GUILD_MESSAGES + #: + #: ..code-block:: js + #: + #: // JSON example, using explicit bitfield values + #: 513 + #: // JSON example, using an array of names + #: [ "GUILDS", "GUILD_MESSAGES" ] + #: + #: See :obj:`hikari.net.codes.GatewayIntent` for valid names of + #: intents you can use. Integer values are as documented on Discord's + #: developer portal. + #: + #: Warnings + #: -------- + #: If you are using the V7 gateway implementation, you will NEED to provide + #: explicit intent values for this field in order to get online. + #: Additionally, intents that are classed by Discord as being privileged + #: will require you to whitelist your application in order to use them. + #: + #: If you are using the V6 gateway implementation, setting this to ``None`` + #: will simply opt you into every event you can subscribe to. + intents: typing.Optional[codes.GatewayIntent] = marshaller.attrib( + deserializer=lambda value: marshaller.dereference_int_flag(codes.GatewayIntent, value), + if_undefined=None, + default=None, + ) + + #: The large threshold to use. + large_threshold: int = marshaller.attrib(deserializer=int, if_undefined=lambda: 250, default=True) + + +def _parse_shard_info(payload): + range_matcher = re.search(r"(\d+)\s*(\.{2,3})\s*(\d+)", payload) + + if not range_matcher: + if isinstance(payload, str): + payload = int(payload) + + if isinstance(payload, int): + return [payload] + + raise ValueError('expected shard_ids to be one of int, list of int, or range string ("x..y")') + + minimum, range_mod, maximum = range_matcher.groups() + minimum, maximum = int(minimum), int(maximum) + if len(range_mod) == 3: + maximum += 1 + + return [*range(minimum, maximum)] + + +@generate_config_attrs +class ShardConfig(BaseConfig): + """Definition of shard management configuration settings.""" + + #: The shard IDs to produce shard connections for. + #: + #: If being deserialized, this can be several formats. + #: ``12``, ``"12"``: + #: A specific shard ID. + #: ``[0, 1, 2, 3, 8, 9, 10]``, ``["0", "1", "2", "3", "8", "9", "10"]``: + #: A sequence of shard IDs. + #: ``"5..16"``: + #: A range string. Two periods indicate a range of ``[5, 16)`` + #: (inclusive beginning, exclusive end). + #: ``"5...16"``: + #: A range string. Three periods indicate a range of + #: ``[5, 17]`` (inclusive beginning, inclusive end). + #: ``None``: + #: The ``shard_count`` will be considered and that many shards will + #: be created for you. If the ``shard_count`` is also ``None``, then + #: auto-sharding will be performed for you. + #: + #: :type: :obj:`typing.Sequence` [ :obj:`int` ], optional + shard_ids: typing.Sequence[int] = marshaller.attrib( + deserializer=_parse_shard_info, if_none=None, if_undefined=None, default=None + ) + + #: The number of shards the entire distributed application should consist + #: of. If you run multiple distributed instances of the bot, you should + #: ensure this value is consistent. + #: + #: This can be set to `None` to enable auto-sharding. This is the default. + #: + #: :type: :obj:`int`, optional. + shard_count: typing.Optional[int] = marshaller.attrib(deserializer=int, if_undefined=None, default=None) + + +@generate_config_attrs +class RESTConfig(AIOHTTPConfig, TokenConfig): + """REST-specific configuration details.""" + + #: The HTTP API version to use. + #: + #: If unspecified, then V7 is used. + #: + #: :type: :obj:`int` + rest_version: int = marshaller.attrib(deserializer=int, if_undefined=lambda: 7, default=7) + + +@generate_config_attrs +class BotConfig(RESTConfig, ShardConfig, WebsocketConfig): + """Configuration for a standard bot.""" diff --git a/hikari/clients/gateway_config.py b/hikari/clients/gateway_config.py deleted file mode 100644 index 1df9cb9635..0000000000 --- a/hikari/clients/gateway_config.py +++ /dev/null @@ -1,165 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# Copyright © Nekoka.tt 2019-2020 -# -# This file is part of Hikari. -# -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . -"""Gateway and sharding configuration objects and options.""" -__all__ = ["GatewayConfig"] - -import datetime -import typing - -from hikari.internal import conversions -from hikari import entities -from hikari import gateway_entities -from hikari import guilds -from hikari.clients import protocol_config -from hikari.internal import marshaller -from hikari.net import codes as net_codes -from hikari.clients import shard_config as _shard_config - - -@marshaller.attrs(kw_only=True) -class GatewayConfig(entities.HikariEntity, entities.Deserializable): - """Gateway and sharding configuration. - - All fields are optional kwargs that can be passed to the constructor. - - "Deserialized" and "unspecified" defaults are only applicable if you - create the object using :meth:`hikari.entities.Deserializable.deserialize`. - """ - - #: Whether to enable debugging mode for the generated shards. Usually you - #: don't want to enable this. - #: - #: :type: :obj:`bool` - debug: bool = marshaller.attrib(deserializer=bool, if_undefined=False, default=False) - - #: The initial activity to set all shards to when starting the gateway. If - #: ``None``, then no activity will be set. - #: - #: :type: :obj:`hikari.gateway_entities.GatewayActivity`, optional - initial_activity: typing.Optional[gateway_entities.GatewayActivity] = marshaller.attrib( - deserializer=gateway_entities.GatewayActivity.deserialize, if_none=None, if_undefined=None, default=None - ) - - #: The initial status to set the shards to when starting the gateway. - #: - #: :type: :obj:`str` - initial_status: guilds.PresenceStatus = marshaller.attrib( - deserializer=guilds.PresenceStatus.__getitem__, if_undefined=lambda: "online", default="online", - ) - - #: Whether to show up as AFK or not on sign-in. - #: - #: :type: :obj:`bool` - initial_is_afk: bool = marshaller.attrib(deserializer=bool, if_undefined=False, default=False) - - #: The idle time to show on signing in, or ``None`` to not show an idle - #: time. - #: - #: :type: :obj:`datetime.datetime`, optional - initial_idle_since: typing.Optional[datetime.datetime] = marshaller.attrib( - deserializer=conversions.unix_epoch_to_ts, if_none=None, if_undefined=None, default=None - ) - - #: The intents to use for the connection. - #: - #: If being deserialized, this can be an integer bitfield, or a sequence of - #: intent names. If - #: unspecified, this will be set to ``None``. - #: - #: :type: :obj:`hikari.net.codes.GatewayIntent`, optional - #: - #: Examples - #: -------- - #: - #: .. code-block:: python - #: - #: # Python example - #: GatewayIntent.GUILDS | GatewayIntent.GUILD_MESSAGES - #: - #: ..code-block:: js - #: - #: // JSON example, using explicit bitfield values - #: 513 - #: // JSON example, using an array of names - #: [ "GUILDS", "GUILD_MESSAGES" ] - #: - #: See :obj:`hikari.net.codes.GatewayIntent` for valid names of - #: intents you can use. Integer values are as documented on Discord's - #: developer portal. - #: - #: Warnings - #: -------- - #: If you are using the V7 gateway implementation, you will NEED to provide - #: explicit intent values for this field in order to get online. - #: Additionally, intents that are classed by Discord as being privileged - #: will require you to whitelist your application in order to use them. - #: - #: If you are using the V6 gateway implementation, setting this to ``None`` - #: will simply opt you into every event you can subscribe to. - intents: typing.Optional[net_codes.GatewayIntent] = marshaller.attrib( - deserializer=lambda value: marshaller.dereference_int_flag(net_codes.GatewayIntent, value), - if_undefined=None, - default=None, - ) - - #: The large threshold to use. - large_threshold: int = marshaller.attrib(deserializer=int, if_undefined=lambda: 250, default=True) - - #: Low level protocol details, such as proxy configuration and SSL settings. - #: - #: This is only used while creating the HTTP connection that the websocket - #: is upgraded from. - #: - #: If unspecified, defaults are used. - #: - #: :type: :obj:`hikari.clients.protocol_config.HTTPProtocolConfig` - protocol: typing.Optional[protocol_config.HTTPProtocolConfig] = marshaller.attrib( - deserializer=protocol_config.HTTPProtocolConfig.deserialize, if_undefined=None, default=None, - ) - - #: Manual sharding configuration to use. If this is ``None``, or - #: unspecified, then auto sharding configuration will be performed for you - #: based on defaults suggested by Discord. - #: - #: :type: :obj:`hikari.clients.shard_config.ShardConfig`, optional - shard_config: typing.Optional[_shard_config.ShardConfig] = marshaller.attrib( - deserializer=_shard_config.ShardConfig.deserialize, if_undefined=None, default=None, - ) - - #: The token to use, if applicable. - #: - #: If ``None`` or not specified, whatever is in the global token field on - #: the config will be used. Note that you will have to specify this value - #: somewhere; you will not be able to connect without it. - #: - #: :type: :obj:`str`, optional - token: typing.Optional[str] = marshaller.attrib(deserializer=str, if_none=None, if_undefined=None, default=None) - - #: Whether to use zlib compression on the gateway for inbound messages or - #: not. Usually you want this turned on. - #: - #: :type: :obj:`bool` - use_compression: bool = marshaller.attrib(deserializer=bool, if_undefined=True, default=True) - - #: The gateway API version to use. - #: - #: If unspecified, then V6 is used. - #: - #: :type: :obj:`int` - version: int = marshaller.attrib(deserializer=int, if_undefined=lambda: 6, default=6) diff --git a/hikari/clients/gateway_client.py b/hikari/clients/gateway_manager.py similarity index 81% rename from hikari/clients/gateway_client.py rename to hikari/clients/gateway_manager.py index 1a21cf5090..295b29da07 100644 --- a/hikari/clients/gateway_client.py +++ b/hikari/clients/gateway_manager.py @@ -21,46 +21,47 @@ This provides functionality such as keeping multiple shards alive """ -__all__ = ["GatewayClient"] +__all__ = ["GatewayManager"] import asyncio import time import typing -from hikari.clients import websocket_client +from hikari.clients import configs +from hikari.clients import runnable +from hikari.internal import conversions from hikari.internal import more_logging -from hikari.clients import gateway_config from hikari.clients import shard_client -from hikari.net import shard -from hikari.state import raw_event_consumer +from hikari.state import raw_event_consumers ShardT = typing.TypeVar("ShardT", bound=shard_client.ShardClient) -class GatewayClient(typing.Generic[ShardT], websocket_client.WebsocketClient): +class GatewayManager(typing.Generic[ShardT], runnable.RunnableClient): """Provides a management layer for multiple-sharded bots. This also provides additional conduit used to connect up shards to the rest of this framework to enable management of dispatched events, etc. - """ def __init__( self, - config: gateway_config.GatewayConfig, - url: str, *, - raw_event_consumer_impl: raw_event_consumer.RawEventConsumer, + shard_ids: typing.Sequence[int], + shard_count: int, + config: configs.WebsocketConfig, + url: str, + raw_event_consumer_impl: raw_event_consumers.RawEventConsumer, shard_type: typing.Type[ShardT] = shard_client.ShardClient, ) -> None: - self.logger = more_logging.get_named_logger(self) + super().__init__(more_logging.get_named_logger(self, conversions.pluralize(shard_count, "shard"))) + self._is_running = False self.config = config self.raw_event_consumer = raw_event_consumer_impl - self._is_running = False self.shards: typing.Dict[int, ShardT] = { - shard_id: shard_type(shard_id, config, raw_event_consumer_impl, url) - for shard_id in config.shard_config.shard_ids + shard_id: shard_type(shard_id, shard_count, config, raw_event_consumer_impl, url) for shard_id in shard_ids } + self.shard_ids = shard_ids async def start(self) -> None: """Start all shards. @@ -72,7 +73,7 @@ async def start(self) -> None: self._is_running = True self.logger.info("starting %s shard(s)", len(self.shards)) start_time = time.perf_counter() - for i, shard_id in enumerate(self.config.shard_config.shard_ids): + for i, shard_id in enumerate(self.shard_ids): if i > 0: await asyncio.sleep(5) @@ -105,4 +106,3 @@ async def close(self, wait: bool = True) -> None: finish_time = time.perf_counter() self.logger.info("stopped %s shard(s) in approx %.2fs", len(self.shards), finish_time - start_time) self._is_running = False - diff --git a/hikari/clients/gateway_runner.py b/hikari/clients/gateway_runner.py index 759501987d..42fb6021f7 100644 --- a/hikari/clients/gateway_runner.py +++ b/hikari/clients/gateway_runner.py @@ -27,14 +27,11 @@ import click -from hikari.clients import shard_client from hikari import entities - -from hikari.clients import gateway_client -from hikari.clients import gateway_config -from hikari.clients import protocol_config -from hikari.clients import shard_config -from hikari.state import raw_event_consumer +from hikari.clients import configs +from hikari.clients import gateway_manager +from hikari.clients import shard_client +from hikari.state import raw_event_consumers logger_levels = ("DEBUG", "INFO", "WARNING", "ERROR", "NOTSET") @@ -48,17 +45,17 @@ def _supports_color(): _color_format = ( - "\033[1;35m%(levelname)1.1s \033[0;31m%(asctime)23.23s \033[1;34m%(module)-15.15s " + "\033[1;35m%(levelname)1.1s \033[0;37m%(name)25.25s \033[0;31m%(asctime)23.23s \033[1;34m%(module)-15.15s " "\033[1;32m#%(lineno)-4d \033[0m:: \033[0;33m%(message)s\033[0m" ) -_regular_format = "%(levelname)1.1s %(asctime)23.23s %(module)-15.15s #%(lineno)-4d :: %(message)s" +_regular_format = "%(levelname)1.1s %(name)25.25s %(asctime)23.23s %(module)-15.15s #%(lineno)-4d :: %(message)s" @click.command() @click.option("--compression", default=True, type=click.BOOL, help="Enable or disable gateway compression.") @click.option("--color", default=_supports_color(), type=click.BOOL, help="Whether to enable or disable color.") @click.option("--debug", default=False, type=click.BOOL, help="Enable or disable debug mode.") -@click.option("--logger", default="INFO", type=click.Choice(logger_levels), help="Logger verbosity.") +@click.option("--logger", envvar="LOGGER", default="INFO", type=click.Choice(logger_levels), help="Logger verbosity.") @click.option("--shards", default=1, type=click.IntRange(min=1), help="The number of shards to explicitly use.") @click.option("--token", required=True, envvar="TOKEN", help="The token to use to authenticate with Discord.") @click.option("--url", default="wss://gateway.discord.gg/", help="The websocket URL to connect to.") @@ -68,26 +65,27 @@ def run_gateway(compression, color, debug, logger, shards, token, url, verify_ss """:mod:`click` command line client for running a test gateway connection. This is provided for internal testing purposes for benchmarking API - stabiltiy, etc. + stability, etc. """ logging.captureWarnings(True) logging.basicConfig(level=logger, format=_color_format if color else _regular_format, stream=sys.stdout) - class _DummyConsumer(raw_event_consumer.RawEventConsumer): + class _DummyConsumer(raw_event_consumers.RawEventConsumer): def process_raw_event( self, _client: shard_client.ShardClient, _name: str, _payload: entities.RawEntityT ) -> None: pass - client = gateway_client.GatewayClient( - config=gateway_config.GatewayConfig( - debug=debug, - protocol=protocol_config.HTTPProtocolConfig(verify_ssl=verify_ssl), - shard_config=shard_config.ShardConfig(shard_count=shards), + client = gateway_manager.GatewayManager( + shard_ids=[*range(shards)], + shard_count=shards, + config=configs.WebsocketConfig( token=token, - use_compression=compression, - version=version, + gateway_version=version, + debug=debug, + gateway_use_compression=compression, + verify_ssl=verify_ssl, ), url=url, raw_event_consumer_impl=_DummyConsumer(), diff --git a/hikari/clients/http_client.py b/hikari/clients/http_client.py deleted file mode 100644 index b1664db498..0000000000 --- a/hikari/clients/http_client.py +++ /dev/null @@ -1,20 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# Copyright © Nekoka.tt 2019-2020 -# -# This file is part of Hikari. -# -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . -"""May contain nuts.""" -__all__ = [] diff --git a/hikari/clients/http_config.py b/hikari/clients/http_config.py deleted file mode 100644 index 889a7a8de8..0000000000 --- a/hikari/clients/http_config.py +++ /dev/null @@ -1,65 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# Copyright © Nekoka.tt 2019-2020 -# -# This file is part of Hikari. -# -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . -"""HTTP (REST) API configuration objects and options.""" - -__all__ = ["HTTPConfig"] - -import typing - -from hikari.internal import marshaller -from hikari import entities -from hikari.clients import protocol_config - - -@marshaller.attrs(kw_only=True) -class HTTPConfig(entities.HikariEntity, entities.Deserializable): - """HTTP API configuration. - - All fields are optional kwargs that can be passed to the constructor. - - "Deserialized" and "unspecified" defaults are only applicable if you - create the object using :meth:`hikari.entities.Deserializable.deserialize`. - """ - - #: Low level protocol details, such as proxy configuration and SSL settings. - #: - #: If unspecified, defaults are used. - #: - #: :type: :obj:`hikari.clients.protocol_config.HTTPProtocolConfig` - protocol: typing.Optional[protocol_config.HTTPProtocolConfig] = marshaller.attrib( - deserializer=protocol_config.HTTPProtocolConfig.deserialize, if_undefined=None, default=None, - ) - - #: The token to use, if applicable. - #: - #: Note that this should not start with ``Bot`` or ``Bearer``. This is - #: detected automatically. - #: - #: If ``None`` or not specified, whatever is in the global token field on - #: the config will be used. - #: - #: :type: :obj:`str`, optional - token: typing.Optional[str] = marshaller.attrib(deserializer=str, if_none=None, if_undefined=None, default=None) - - #: The HTTP API version to use. - #: - #: If unspecified, then V7 is used. - #: - #: :type: :obj:`int` - version: int = marshaller.attrib(deserializer=int, if_undefined=lambda: 7, default=7) diff --git a/hikari/clients/protocol_config.py b/hikari/clients/protocol_config.py deleted file mode 100644 index 8324179930..0000000000 --- a/hikari/clients/protocol_config.py +++ /dev/null @@ -1,135 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# Copyright © Nekoka.tt 2019-2020 -# -# This file is part of Hikari. -# -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . -"""Configuration objects for various low-level protocols. - -These include HTTP connections and SSL management, proxies, etc. -""" -__all__ = ["HTTPProtocolConfig"] - -import ssl -import typing - -import aiohttp.typedefs - -from hikari.internal import marshaller -from hikari import entities - - -@marshaller.attrs(kw_only=True) -class HTTPProtocolConfig(entities.HikariEntity, entities.Deserializable): - """A configuration class that can be deserialized from a :obj:`typing.Dict`. - - This represents any HTTP-specific implementation and protocol details - such as how to manage redirects, how to manage SSL, and how to use a - proxy if needed. - - All fields are optional kwargs that can be passed to the constructor. - - "Deserialized" and "unspecified" defaults are only applicable if you - create the object using :meth:`hikari.entities.Deserializable.deserialize`. - """ - - #: If ``True``, allow following redirects from ``3xx`` HTTP responses. - #: Generally you do not want to enable this unless you have a good reason - #: to. - #: - #: Defaults to ``False`` if unspecified during deserialization. - #: - #: :type: :obj:`bool` - allow_redirects: bool = marshaller.attrib(deserializer=bool, if_undefined=False, default=False) - - #: Either an implementation of :obj:`aiohttp.BaseConnector`. - #: - #: This may otherwise be ``None`` to use the default settings provided - #: by :mod:`aiohttp`. - #: - #: This is deserialized as an object reference in the format - #: ``package.module#object.attribute`` that is expected to point to the - #: desired value. - #: - #: Defaults to ``None`` if unspecified during deserialization. - #: - #: :type: :obj:`aiohttp.BaseConnector`, optional - connector: typing.Optional[aiohttp.BaseConnector] = marshaller.attrib( - deserializer=marshaller.dereference_handle, if_none=None, if_undefined=None, default=None - ) - - #: Optional proxy headers to provide in any HTTP requests. - #: - #: Defaults to ``None`` if unspecified during deserialization. - #: - #: :type: :obj:`typing.Mapping` [ :obj:`str`, :obj:`str` ], optional - proxy_headers: typing.Optional[typing.Mapping[str, str]] = marshaller.attrib( - deserializer=dict, if_none=None, if_undefined=None, default=None - ) - - #: Optional proxy authorization to provide in any HTTP requests. - #: - #: This is deserialized using the format ``"basic {{base 64 string here}}"``. - #: - #: Defaults to ``None`` if unspecified during deserialization. - #: - #: :type: :obj:`aiohttp.BasicAuth`, optional - proxy_auth: typing.Optional[aiohttp.BasicAuth] = marshaller.attrib( - deserializer=aiohttp.BasicAuth.decode, if_none=None, if_undefined=None, default=None - ) - - #: The optional URL of the proxy to send requests via. - #: - #: Defaults to ``None`` if unspecified during deserialization. - #: - #: :type: :obj:`str`, optional - proxy_url: typing.Optional[str] = marshaller.attrib(deserializer=str, if_undefined=None, if_none=None, default=None) - - #: Optional request timeout to use. If an HTTP request takes longer than - #: this, it will be aborted. - #: - #: If not ``None``, the value represents a number of seconds as a floating - #: point number. - #: - #: Defaults to ``None`` if unspecified during deserialization. - #: - #: :type: :obj:`float`, optional - request_timeout: typing.Optional[float] = marshaller.attrib( - deserializer=float, if_undefined=None, if_none=None, default=None - ) - - #: The optional SSL context to use. - #: - #: This is deserialized as an object reference in the format - #: ``package.module#object.attribute`` that is expected to point to the - #: desired value. - #: - #: Defaults to ``None`` if unspecified during deserialization. - #: - #: :type: :obj:`ssl.SSLContext`, optional - ssl_context: typing.Optional[ssl.SSLContext] = marshaller.attrib( - deserializer=marshaller.dereference_handle, if_none=None, if_undefined=None, default=None - ) - - #: If ``True``, then responses with invalid SSL certificates will be - #: rejected. Generally you want to keep this enabled unless you have a - #: problem with SSL and you know exactly what you are doing by disabling - #: this. Disabling SSL verification can have major security implications. - #: You turn this off at your own risk. - #: - #: Defaults to ``True`` if unspecified during deserialization. - #: - #: :type: :obj:`bool` - verify_ssl: bool = marshaller.attrib(deserializer=bool, if_undefined=True, default=True) diff --git a/hikari/clients/rest_client.py b/hikari/clients/rest_client.py new file mode 100644 index 0000000000..3d4220cd30 --- /dev/null +++ b/hikari/clients/rest_client.py @@ -0,0 +1,47 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# Copyright © Nekoka.tt 2019-2020 +# +# This file is part of Hikari. +# +# Hikari is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Hikari is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Hikari. If not, see . +"""May contain nuts.""" +__all__ = ["RESTClient"] + +import datetime + +from hikari import gateway_entities +from hikari.clients import configs + + +class RESTClient: + """Stuff will go here when this is implemented...""" + + # TODO: FasterSpeeding: update this. + def __init__(self, _: configs.RESTConfig) -> None: + ... + + async def close(self): + ... + + async def fetch_gateway_bot(self) -> gateway_entities.GatewayBot: + # Stubbed placeholder. + # TODO: replace with actual implementation. + return gateway_entities.GatewayBot( + url="wss://gateway.discord.gg", + shard_count=1, + session_start_limit=gateway_entities.SessionStartLimit( + total=1000, remaining=999, reset_after=datetime.timedelta(days=0.9) + ), + ) diff --git a/hikari/clients/websocket_client.py b/hikari/clients/runnable.py similarity index 85% rename from hikari/clients/websocket_client.py rename to hikari/clients/runnable.py index b3e9287aca..c5166d9f59 100644 --- a/hikari/clients/websocket_client.py +++ b/hikari/clients/runnable.py @@ -18,7 +18,7 @@ # along with Hikari. If not, see . """Provides a base for any type of websocket client.""" -__all__ = ["WebsocketClient"] +__all__ = ["RunnableClient"] import abc import asyncio @@ -27,24 +27,33 @@ import signal -class WebsocketClient(abc.ABC): +class RunnableClient(abc.ABC): """Base for any websocket client that must be kept alive.""" + __slots__ = ("logger",) + + #: The logger to use for this client. + #: + #: :type: :obj:`logging.Logger` logger: logging.Logger @abc.abstractmethod - async def start(self): # noqa: D401 + def __init__(self, logger: logging.Logger) -> None: + self.logger = logger + + @abc.abstractmethod + async def start(self) -> None: # noqa: D401 """Start the component.""" @abc.abstractmethod - async def close(self, wait: bool = True): + async def close(self, wait: bool = True) -> None: """Shut down the component.""" @abc.abstractmethod - async def join(self): + async def join(self) -> None: """Wait for the component to terminate.""" - def run(self): + def run(self) -> None: """Execute this component on an event loop. Performs the same job as :meth:`start`, but provides additional diff --git a/hikari/clients/shard_client.py b/hikari/clients/shard_client.py index 890f99b180..05d1e9b13e 100644 --- a/hikari/clients/shard_client.py +++ b/hikari/clients/shard_client.py @@ -35,8 +35,8 @@ import aiohttp -from hikari.clients import gateway_config -from hikari.clients import websocket_client +from hikari.clients import configs +from hikari.clients import runnable from hikari.internal import more_asyncio from hikari.internal import more_logging from hikari import events @@ -46,7 +46,7 @@ from hikari import errors from hikari.net import ratelimits from hikari.net import shard -from hikari.state import raw_event_consumer +from hikari.state import raw_event_consumers _EventT = typing.TypeVar("_EventT", bound=events.HikariEvent) @@ -73,7 +73,7 @@ class ShardState(enum.IntEnum): STOPPED = enum.auto() -class ShardClient(websocket_client.WebsocketClient): +class ShardClient(runnable.RunnableClient): """The primary interface for a single shard connection. This contains several abstractions to enable usage of the low @@ -84,7 +84,9 @@ class ShardClient(websocket_client.WebsocketClient): ---------- shard_id : :obj:`int` The ID of this specific shard. - config : :obj:`hikari.clients.gateway_config.GatewayConfig` + shard_id : :obj:`int` + The number of shards that make up this distributed application. + config : :obj:`hikari.clients.configs.WebsocketConfig` The gateway configuration to use to initialize this shard. raw_event_consumer_impl : :obj:`hikari.state.raw_event_consumer.RawEventConsumer` The consumer of a raw event. @@ -116,11 +118,12 @@ class ShardClient(websocket_client.WebsocketClient): def __init__( self, shard_id: int, - config: gateway_config.GatewayConfig, - raw_event_consumer_impl: raw_event_consumer.RawEventConsumer, + shard_count: int, + config: configs.WebsocketConfig, + raw_event_consumer_impl: raw_event_consumers.RawEventConsumer, url: str, ) -> None: - self.logger = more_logging.get_named_logger(self, shard_id) + super().__init__(more_logging.get_named_logger(self, f"#{shard_id}")) self._raw_event_consumer = raw_event_consumer_impl self._activity = config.initial_activity self._idle_since = config.initial_idle_since @@ -129,8 +132,8 @@ def __init__( self._shard_state = ShardState.NOT_RUNNING self._task = None self._client = shard.ShardConnection( - compression=config.use_compression, - connector=config.protocol.connector if config.protocol is not None else None, + compression=config.gateway_use_compression, + connector=config.tcp_connector, debug=config.debug, dispatch=lambda c, n, pl: raw_event_consumer_impl.process_raw_event(self, n, pl), initial_presence=self._create_presence_pl( @@ -141,18 +144,18 @@ def __init__( ), intents=config.intents, large_threshold=config.large_threshold, - proxy_auth=config.protocol.proxy_auth if config.protocol is not None else None, - proxy_headers=config.protocol.proxy_headers if config.protocol is not None else None, - proxy_url=config.protocol.proxy_url if config.protocol is not None else None, + proxy_auth=config.proxy_auth, + proxy_headers=config.proxy_headers, + proxy_url=config.proxy_url, session_id=None, seq=None, shard_id=shard_id, - shard_count=config.shard_config.shard_count, - ssl_context=config.protocol.ssl_context if config.protocol is not None else None, + shard_count=shard_count, + ssl_context=config.ssl_context, token=config.token, url=url, - verify_ssl=config.protocol.verify_ssl if config.protocol is not None else None, - version=config.version, + verify_ssl=config.verify_ssl, + version=config.gateway_version, ) @property @@ -166,8 +169,6 @@ def client(self) -> shard.ShardConnection: """ return self._client - #: TODO: use enum - # Ignore docstring not starting in an imperative mood @property def status(self) -> guilds.PresenceStatus: # noqa: D401 """Current user status for this shard. @@ -225,18 +226,7 @@ async def start(self): if self._shard_state not in (ShardState.NOT_RUNNING, ShardState.STOPPED): raise RuntimeError("Cannot start a shard twice") - self.logger.debug("starting shard") - self._shard_state = ShardState.HANDSHAKE - self._task = asyncio.create_task(self._keep_alive()) - self.logger.info("waiting for READY") - completed, _ = await asyncio.wait( - [self._task, self._client.identify_event.wait()], return_when=asyncio.FIRST_COMPLETED - ) - - if self._task in completed: - raise self._task.exception() - - self._shard_state = ShardState.WAITING_FOR_READY + self._task = asyncio.create_task(self._keep_alive(), name="ShardClient#keep_alive") completed, _ = await asyncio.wait( [self._task, self._client.ready_event.wait()], return_when=asyncio.FIRST_COMPLETED @@ -245,9 +235,6 @@ async def start(self): if self._task in completed: raise self._task.exception() - self.logger.info("now READY") - self._shard_state = ShardState.READY - async def join(self) -> None: """Wait for the shard to shut down fully.""" await self._task if self._task is not None else more_asyncio.completed_future() @@ -288,9 +275,10 @@ async def _keep_alive(self): back_off.reset() last_start = time.perf_counter() - do_not_back_off = False - await self._client.connect() + + connect_task = await self._spin_up() + await connect_task self.logger.critical("shut down silently! this shouldn't happen!") except aiohttp.ClientConnectorError as ex: @@ -334,6 +322,45 @@ async def _keep_alive(self): self.logger.debug("propagating unexpected exception %s", exc_info=ex) raise ex + async def _spin_up(self) -> asyncio.Task: + self.logger.debug("initializing shard") + self._shard_state = ShardState.HANDSHAKE + + is_resume = self._client.seq is not None and self._client.session_id is not None + + connect_task = asyncio.create_task(self._client.connect(), name="ShardConnection#connect") + + completed, _ = await asyncio.wait( + [connect_task, self._client.hello_event.wait()], return_when=asyncio.FIRST_COMPLETED + ) + + if connect_task in completed: + raise connect_task.exception() + + self.logger.info("received HELLO, interval is %ss", self.client.heartbeat_interval) + + completed, _ = await asyncio.wait( + [connect_task, self._client.identify_event.wait()], return_when=asyncio.FIRST_COMPLETED + ) + + if connect_task in completed: + raise connect_task.exception() + + self.logger.info("sent %s, waiting for READY event", "RESUME" if is_resume else "IDENTIFY") + self._shard_state = ShardState.WAITING_FOR_READY + + self.logger.info("now READY") + self._shard_state = ShardState.READY + + completed, _ = await asyncio.wait( + [connect_task, self._client.ready_event.wait()], return_when=asyncio.FIRST_COMPLETED + ) + + if connect_task in completed: + raise connect_task.exception() + + return connect_task + async def update_presence( self, status: guilds.PresenceStatus = ..., diff --git a/hikari/clients/shard_config.py b/hikari/clients/shard_config.py deleted file mode 100644 index 749ec2deb8..0000000000 --- a/hikari/clients/shard_config.py +++ /dev/null @@ -1,97 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# Copyright © Nekoka.tt 2019-2020 -# -# This file is part of Hikari. -# -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . -"""Configuration for sharding.""" - -__all__ = ["ShardConfig"] - -import re -import typing - -from hikari import entities -from hikari.internal import assertions -from hikari.internal import marshaller - - -def _parse_shard_info(payload): - range_matcher = re.search(r"(\d+)\s*(\.{2,3})\s*(\d+)", payload) - - if not range_matcher: - if isinstance(payload, str): - payload = int(payload) - - if isinstance(payload, int): - return [payload] - - raise ValueError('expected shard_ids to be one of int, list of int, or range string ("x..y")') - - minimum, range_mod, maximum = range_matcher.groups() - minimum, maximum = int(minimum), int(maximum) - if len(range_mod) == 3: - maximum += 1 - - return [*range(minimum, maximum)] - - -@marshaller.attrs(kw_only=True, init=False) -class ShardConfig(entities.HikariEntity, entities.Deserializable): - """Manual sharding configuration. - - All fields are optional kwargs that can be passed to the constructor. - - "Deserialized" and "unspecified" defaults are only applicable if you - create the object using :meth:`hikari.entities.Deserializable.deserialize`. - """ - - #: The shard IDs to produce shard connections for. - #: - #: If being deserialized, this can be several formats. - #: ``12``, ``"12"``: - #: A specific shard ID. - #: ``[0, 1, 2, 3, 8, 9, 10]``, ``["0", "1", "2", "3", "8", "9", "10"]``: - #: A sequence of shard IDs. - #: ``"5..16"``: - #: A range string. Two periods indicate a range of ``[5, 16)`` - #: (inclusive beginning, exclusive end). - #: ``"5...16"``: - #: A range string. Three periods indicate a range of - #: ``[5, 17]`` (inclusive beginning, inclusive end). - #: ``None``: - #: The ``shard_count`` will be considered and that many shards will - #: be created for you. - #: - #: :type: :obj:`typing.Sequence` [ :obj:`int` ] - shard_ids: typing.Sequence[int] = marshaller.attrib( - deserializer=_parse_shard_info, if_none=None, if_undefined=None, - ) - - #: The number of shards the entire distributed application should consist - #: of. If you run multiple instances of the bot, you should ensure this - #: value is consistent. - #: - #: :type: :obj:`int` - shard_count: int = marshaller.attrib(deserializer=int) - - # noinspection PyMissingConstructor - def __init__(self, *, shard_ids: typing.Optional[typing.Iterable[int]] = None, shard_count: int) -> None: - self.shard_ids = [*shard_ids] if shard_ids else [*range(shard_count)] - - for shard_id in self.shard_ids: - assertions.assert_that(shard_id < shard_count, "shard_count must be greater than any shard ids") - - self.shard_count = shard_count diff --git a/hikari/entities.py b/hikari/entities.py index b4ad779404..a9a78524af 100644 --- a/hikari/entities.py +++ b/hikari/entities.py @@ -57,10 +57,16 @@ class HikariEntity(metaclass=abc.ABCMeta): __slots__ = () if typing.TYPE_CHECKING: - - def __init__(self, *_, **__) -> None: + # pylint:disable=unused-argument + # Screws up PyCharm and makes annoying warnings everywhere, so just + # mute this. We can always make dummy constructors later, or find + # another way around this perhaps. + @typing.no_type_check + def __init__(self, *args, **kwargs) -> None: ... + # pylint:enable=unused-argument + class Deserializable: """A mixin for any type that allows deserialization from a raw value into a Hikari entity.""" diff --git a/hikari/errors.py b/hikari/errors.py index cbcf0914cb..a5b6748e66 100644 --- a/hikari/errors.py +++ b/hikari/errors.py @@ -91,12 +91,12 @@ def __init__( close_code: typing.Optional[typing.Union[codes.GatewayCloseCode, int]] = None, reason: typing.Optional[str] = None, ) -> None: - if reason is None: - try: - name = close_code.name - except AttributeError: - name = str(close_code) if close_code is not None else "no reason" + try: + name = close_code.name + except AttributeError: + name = str(close_code) if close_code is not None else "no reason" + if reason is None: reason = f"Gateway connection closed by server ({name})" self.close_code = close_code diff --git a/hikari/internal/conversions.py b/hikari/internal/conversions.py index 0a0314670b..178ea9ae0c 100644 --- a/hikari/internal/conversions.py +++ b/hikari/internal/conversions.py @@ -27,6 +27,7 @@ "try_cast", "put_if_specified", "image_bytes_to_image_data", + "pluralize", ] import base64 @@ -268,5 +269,10 @@ def make_resource_seekable(resource: typing.Any) -> typing.Union[io.BytesIO, io. return resource +def pluralize(count: int, name: str, suffix: str = "s") -> str: + """Pluralizes a word.""" + return f"{count} {name + suffix}" if count - 1 else f"{count} {name}" + + BytesLikeT = typing.Union[bytes, bytearray, memoryview, str, io.StringIO, io.BytesIO] FileLikeT = typing.Union[BytesLikeT, io.BufferedRandom, io.BufferedReader, io.BufferedRWPair] diff --git a/hikari/internal/marshaller.py b/hikari/internal/marshaller.py index 847dfc0354..a60912f175 100644 --- a/hikari/internal/marshaller.py +++ b/hikari/internal/marshaller.py @@ -451,13 +451,14 @@ def attrs(**kwargs): ---------------- auto_attribs : :obj:`bool` This must always be ``False`` if specified, or a :obj:`ValueError` - will be raised, as this feature is not compatible with this marshaller - implementation. If not specified, it will default to ``False``. + will be raised, as this feature is not compatible with this + marshaller implementation. If not specified, it will default to + ``False``. marshaller : :obj:`HikariEntityMarshaller` If specified, this should be an instance of a marshaller to use. For most internal purposes, you want to not specify this, since it will - then default to the hikari-global marshaller instead. This is useful, - however, for testing and for external usage. + then default to the hikari-global marshaller instead. This is + useful, however, for testing and for external usage. Returns ------- @@ -484,3 +485,7 @@ class MyEntity: assertions.assert_that(not kwargs.get("auto_attribs"), "Cannot use auto attribs here") kwargs["auto_attribs"] = False return lambda cls: kwargs.pop("marshaller", HIKARI_ENTITY_MARSHALLER).register(attr.s(**kwargs)(cls)) + + +if typing.TYPE_CHECKING: + attrs = attr.s diff --git a/hikari/internal/more_logging.py b/hikari/internal/more_logging.py index e1a3995a4e..e579ba97f9 100644 --- a/hikari/internal/more_logging.py +++ b/hikari/internal/more_logging.py @@ -32,7 +32,7 @@ def get_named_logger(obj: typing.Any, *extra_objs: typing.Any) -> logging.Logger If the passed object is an instance of a class, the class is used instead. - If a class is provided/used, then the fully qualified package and class name is used to name the logger. + If a class is provided/used, then the class name is used to name the logger. If a string is provided, then the string is used as the name. This is not recommended. @@ -52,7 +52,7 @@ def get_named_logger(obj: typing.Any, *extra_objs: typing.Any) -> logging.Logger if not isinstance(obj, type): obj = type(obj) - obj = f"{obj.__module__}.{obj.__qualname__}" + obj = obj.__qualname__ if extra_objs: extras = ", ".join(map(str, extra_objs)) diff --git a/hikari/net/shard.py b/hikari/net/shard.py index 4cdf676c6b..f54664792b 100644 --- a/hikari/net/shard.py +++ b/hikari/net/shard.py @@ -620,7 +620,7 @@ async def connect(self, client_session_type=aiohttp.ClientSession) -> None: "RECONNECT" if self.disconnect_count else "CONNECT", typing.cast(typing.Dict, more_collections.EMPTY_DICT), ) - self.logger.info("received HELLO, interval is %ss", self.heartbeat_interval) + self.logger.debug("received HELLO, interval is %ss", self.heartbeat_interval) completed, pending_tasks = await asyncio.wait( [self._heartbeat_keep_alive(self.heartbeat_interval), self._identify_or_resume_then_poll_events()], @@ -717,7 +717,7 @@ async def _identify_or_resume_then_poll_events(self): # noinspection PyTypeChecker pl["d"]["presence"] = self._presence await self._send(pl) - self.logger.info("sent IDENTIFY, now listening to incoming events") + self.logger.debug("sent IDENTIFY, now listening to incoming events") else: self.status = GatewayStatus.RESUMING self.logger.debug("sending RESUME") @@ -726,7 +726,7 @@ async def _identify_or_resume_then_poll_events(self): "d": {"token": self._token, "seq": self.seq, "session_id": self.session_id}, } await self._send(pl) - self.logger.info("sent RESUME, now listening to incoming events") + self.logger.debug("sent RESUME, now listening to incoming events") self.identify_event.set() await self._poll_events() @@ -769,7 +769,7 @@ async def _poll_events(self): raise errors.GatewayMustReconnectError() elif op == codes.GatewayOpcode.INVALID_SESSION: can_resume = bool(d) - self.logger.info( + self.logger.debug( "instructed by gateway server to %s session", "resume" if can_resume else "restart", ) raise errors.GatewayInvalidSessionError(can_resume) @@ -826,11 +826,11 @@ async def _receive(self): if message.type == aiohttp.WSMsgType.CLOSE: close_code = self._ws.close_code try: - meaning = codes.GatewayCloseCode(close_code) + close_code = codes.GatewayCloseCode(close_code) except ValueError: - meaning = "???" + pass - self.logger.debug("connection closed with code %s (%s)", close_code, meaning) + self.logger.debug("connection closed with code %s", close_code) if close_code == codes.GatewayCloseCode.AUTHENTICATION_FAILED: raise errors.GatewayInvalidTokenError() if close_code in (codes.GatewayCloseCode.SESSION_TIMEOUT, codes.GatewayCloseCode.INVALID_SEQ): diff --git a/hikari/state/event_dispatcher.py b/hikari/state/event_dispatchers.py similarity index 98% rename from hikari/state/event_dispatcher.py rename to hikari/state/event_dispatchers.py index 09cc866099..3a6e85dd20 100644 --- a/hikari/state/event_dispatcher.py +++ b/hikari/state/event_dispatchers.py @@ -44,9 +44,7 @@ class EventDispatcher(abc.ABC): to a set of micro-interpreter instances to achieve greater concurrency. """ - @abc.abstractmethod - def close(self): - """Cancel anything that is waiting for an event to be dispatched.""" + __slots__ = () @abc.abstractmethod def add_listener(self, event_type: typing.Type[EventT], callback: EventCallbackT) -> EventCallbackT: @@ -140,7 +138,7 @@ def decorator(callback: EventCallbackT) -> EventCallbackT: # Do not add an annotation here, it will mess with type hints in PyCharm which can lead to # confusing telepathy comments to the user. @abc.abstractmethod - def dispatch_event(self, event: events.HikariEvent) -> ...: + def dispatch_event(self, event: events.HikariEvent) -> more_asyncio.Future[typing.Any]: """Dispatch a given event to any listeners and waiters. Parameters diff --git a/hikari/state/event_manager.py b/hikari/state/event_managers.py similarity index 86% rename from hikari/state/event_manager.py rename to hikari/state/event_managers.py index 02deed9952..6d3bd754cb 100644 --- a/hikari/state/event_manager.py +++ b/hikari/state/event_managers.py @@ -24,9 +24,9 @@ import typing from hikari.clients import shard_client -from hikari.state import event_dispatcher +from hikari.state import event_dispatchers from hikari import entities -from hikari.state import raw_event_consumer +from hikari.state import raw_event_consumers from hikari.internal import assertions from hikari.internal import more_logging @@ -69,7 +69,10 @@ def _get_event_marker(obj: typing.Any) -> typing.Set[str]: return getattr(obj, EVENT_MARKER_ATTR) -class EventManager(raw_event_consumer.RawEventConsumer): +EventDispatcherT = typing.TypeVar("EventDispatcherT", bound=event_dispatchers.EventDispatcher) + + +class EventManager(typing.Generic[EventDispatcherT], raw_event_consumers.RawEventConsumer): """Abstract definition of the components for an event system for a bot. The class itself inherits from @@ -83,9 +86,10 @@ class EventManager(raw_event_consumer.RawEventConsumer): Parameters ---------- - event_dispatcher_impl: :obj:`hikari.state.event_dispatcher.EventDispatcher` + event_dispatcher_impl: :obj:`hikari.state.event_dispatcher.EventDispatcher`, optional An implementation of event dispatcher that will store individual events - and manage dispatching them after this object creates them. + and manage dispatching them after this object creates them. If ``None``, + then a default implementation is chosen. Notes ----- @@ -132,7 +136,10 @@ def _process_message_create(self, shard, payload) -> MessageCreateEvent: create your own as needed. """ - def __init__(self, event_dispatcher_impl: event_dispatcher.EventDispatcher) -> None: + def __init__(self, event_dispatcher_impl: typing.Optional[EventDispatcherT] = None) -> None: + if event_dispatcher_impl is None: + event_dispatcher_impl = event_dispatchers.EventDispatcherImpl() + self.logger = more_logging.get_named_logger(self) self.event_dispatcher = event_dispatcher_impl self.raw_event_mappers = {} @@ -167,3 +174,12 @@ def process_raw_event( else: event = handler(shard_client_obj, payload) self.event_dispatcher.dispatch_event(event) + + +class StatelessEventManagerImpl(EventManager[event_dispatchers.EventDispatcher]): + """Stateless event manager implementation for stateless bots. + + This is an implementation that does not rely on querying prior information to + operate. The implementation details of this are much simpler than a stateful + version, and are not immediately affected by the use of intents. + """ diff --git a/hikari/state/raw_event_consumer.py b/hikari/state/raw_event_consumers.py similarity index 100% rename from hikari/state/raw_event_consumer.py rename to hikari/state/raw_event_consumers.py diff --git a/hikari/state/stateless_event_manager_impl.py b/hikari/state/stateless_event_manager_impl.py deleted file mode 100644 index 80a0059771..0000000000 --- a/hikari/state/stateless_event_manager_impl.py +++ /dev/null @@ -1,31 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# Copyright © Nekoka.tt 2019-2020 -# -# This file is part of Hikari. -# -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . -"""Provides an implementation of a stateless event manager.""" -__all__ = ["StatelessEventManagerImpl"] - -from hikari.state import event_manager - - -class StatelessEventManagerImpl(event_manager.EventManager): - """Stateless event manager implementation for stateless bots. - - This is an implementation that does not rely on querying prior information to - operate. The implementation details of this are much simpler than a stateful - version, and are not immediately affected by the use of intents. - """ diff --git a/tests/hikari/internal/test_more_logging.py b/tests/hikari/internal/test_more_logging.py index d5317ca09c..fc685c38f6 100644 --- a/tests/hikari/internal/test_more_logging.py +++ b/tests/hikari/internal/test_more_logging.py @@ -19,8 +19,6 @@ from hikari.internal import more_logging -package_name = __name__ - class Dummy: class NestedDummy: @@ -29,22 +27,22 @@ class NestedDummy: def test_get_named_logger_with_global_class(): logger = more_logging.get_named_logger(Dummy) - assert logger.name == package_name + ".Dummy" + assert logger.name == "Dummy" def test_get_named_logger_with_nested_class(): logger = more_logging.get_named_logger(Dummy.NestedDummy) - assert logger.name == package_name + ".Dummy.NestedDummy" + assert logger.name == "Dummy.NestedDummy" def test_get_named_logger_with_global_class_instance(): logger = more_logging.get_named_logger(Dummy()) - assert logger.name == package_name + ".Dummy" + assert logger.name == "Dummy" def test_get_named_logger_with_nested_class_instance(): logger = more_logging.get_named_logger(Dummy.NestedDummy()) - assert logger.name == package_name + ".Dummy.NestedDummy" + assert logger.name == "Dummy.NestedDummy" def test_get_named_logger_with_string(): diff --git a/tests/hikari/state/test_event_dispatcher.py b/tests/hikari/state/test_event_dispatcher.py index 9fe89b1c9a..4b8c5ad5df 100644 --- a/tests/hikari/state/test_event_dispatcher.py +++ b/tests/hikari/state/test_event_dispatcher.py @@ -21,7 +21,7 @@ import pytest -from hikari.state import event_dispatcher +from hikari.state import event_dispatchers from hikari import events from tests.hikari import _helpers @@ -41,7 +41,7 @@ class TestEvent3(events.HikariEvent): class TestEventDispatcherImpl: @pytest.fixture def dispatcher_inst(self): - return _helpers.unslot_class(event_dispatcher.EventDispatcherImpl)() + return _helpers.unslot_class(event_dispatchers.EventDispatcherImpl)() # noinspection PyTypeChecker @_helpers.assert_raises(type_=TypeError) From 2260eda8322c0dd5d3993cc6ff547cf845fafefe Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Sun, 5 Apr 2020 18:28:43 +0100 Subject: [PATCH 069/922] Fixed an issue where the session_id never got stored properly, thus preventing a RESUME being possible. This is in response to checks made after https://github.com/discord/discord-api-docs/issues/1476. --- gitlab/test.yml | 1 + hikari/clients/bot_client.py | 1 + hikari/clients/configs.py | 2 ++ hikari/clients/gateway_runner.py | 9 +++++++++ hikari/clients/shard_client.py | 10 +++++----- hikari/net/shard.py | 28 ++++++++++++++++++---------- tests/hikari/net/test_shard.py | 18 ++++++++++++++++++ 7 files changed, 54 insertions(+), 15 deletions(-) diff --git a/gitlab/test.yml b/gitlab/test.yml index 1a02452f31..db56ebb445 100644 --- a/gitlab/test.yml +++ b/gitlab/test.yml @@ -231,6 +231,7 @@ pylint: pydocstyle: + allow_failure: true extends: - .cpython-tool-buster - .cache diff --git a/hikari/clients/bot_client.py b/hikari/clients/bot_client.py index 000cc1660c..7fb3af5a3a 100644 --- a/hikari/clients/bot_client.py +++ b/hikari/clients/bot_client.py @@ -159,5 +159,6 @@ class StatelessBot(BotBase): config : :obj:`hikari.clients.configs.BotConfig` The config object to use. """ + def __init__(self, config: configs.BotConfig) -> None: super().__init__(config, event_managers.StatelessEventManagerImpl()) diff --git a/hikari/clients/configs.py b/hikari/clients/configs.py index bdd7354d95..040e898fd8 100644 --- a/hikari/clients/configs.py +++ b/hikari/clients/configs.py @@ -47,6 +47,7 @@ class BaseConfig(entities.Deserializable): """Base class for any configuration data class.""" + if typing.TYPE_CHECKING: # pylint:disable=unused-argument # Screws up PyCharm and makes annoying warnings everywhere, so just @@ -68,6 +69,7 @@ def __init__(self, **kwargs) -> None: @generate_config_attrs class DebugConfig(BaseConfig): """Configuration for anything with a debugging mode.""" + #: Whether to enable debugging mode. Usually you don't want to enable this. #: #: :type: :obj:`bool` diff --git a/hikari/clients/gateway_runner.py b/hikari/clients/gateway_runner.py index 42fb6021f7..1e136fab4d 100644 --- a/hikari/clients/gateway_runner.py +++ b/hikari/clients/gateway_runner.py @@ -21,6 +21,7 @@ This is only for use by developers of this library, regular users do not need to use this. """ +import asyncio import logging import os import sys @@ -91,6 +92,14 @@ def process_raw_event( raw_event_consumer_impl=_DummyConsumer(), ) + async def _restart_in_a_bit(): + conn = client.shards[0].connection + + async def fake_recv(): + return + + await asyncio.sleep(15) + client.run() diff --git a/hikari/clients/shard_client.py b/hikari/clients/shard_client.py index 05d1e9b13e..d867520639 100644 --- a/hikari/clients/shard_client.py +++ b/hikari/clients/shard_client.py @@ -159,7 +159,7 @@ def __init__( ) @property - def client(self) -> shard.ShardConnection: + def connection(self) -> shard.ShardConnection: """Low-level gateway client used for this shard. Returns @@ -337,7 +337,7 @@ async def _spin_up(self) -> asyncio.Task: if connect_task in completed: raise connect_task.exception() - self.logger.info("received HELLO, interval is %ss", self.client.heartbeat_interval) + self.logger.info("received HELLO, interval is %ss", self.connection.heartbeat_interval) completed, _ = await asyncio.wait( [connect_task, self._client.identify_event.wait()], return_when=asyncio.FIRST_COMPLETED @@ -349,13 +349,13 @@ async def _spin_up(self) -> asyncio.Task: self.logger.info("sent %s, waiting for READY event", "RESUME" if is_resume else "IDENTIFY") self._shard_state = ShardState.WAITING_FOR_READY - self.logger.info("now READY") - self._shard_state = ShardState.READY - completed, _ = await asyncio.wait( [connect_task, self._client.ready_event.wait()], return_when=asyncio.FIRST_COMPLETED ) + self.logger.info("now READY") + self._shard_state = ShardState.READY + if connect_task in completed: raise connect_task.exception() diff --git a/hikari/net/shard.py b/hikari/net/shard.py index f54664792b..d57cec00e4 100644 --- a/hikari/net/shard.py +++ b/hikari/net/shard.py @@ -396,16 +396,16 @@ def __init__( self.identify_event: asyncio.Event = asyncio.Event() self.last_heartbeat_sent: float = float("nan") self.last_message_received: float = float("nan") - self.logger: logging.Logger = more_logging.get_named_logger(self, shard_id) self.requesting_close_event: asyncio.Event = asyncio.Event() self.ready_event: asyncio.Event = asyncio.Event() - self.session_id: typing.Optional[str] = session_id + self.session_id = session_id self.seq: typing.Optional[int] = seq self.shard_id: int = shard_id self.shard_count: int = shard_count self.status: GatewayStatus = GatewayStatus.OFFLINE self.version: int = int(version) - self.logger.debug("using Gateway version %s", int(version)) + + self.logger: logging.Logger = more_logging.get_named_logger(self, f"#{shard_id}", f"v{self.version}") @property def uptime(self) -> datetime.timedelta: @@ -422,7 +422,7 @@ def uptime(self) -> datetime.timedelta: @property def is_connected(self) -> bool: - """Wether the gateway is connecter or not. + """Whether the gateway is connecter or not. Returns ------- @@ -450,7 +450,7 @@ def intents(self) -> typing.Optional[codes.GatewayIntent]: @property def reconnect_count(self) -> int: - """Amount of times the gateway has reconnected since initialization. + """Number of times the gateway has reconnected since initialization. This can be used as a debugging context, but is also used internally for exception management. @@ -620,7 +620,7 @@ async def connect(self, client_session_type=aiohttp.ClientSession) -> None: "RECONNECT" if self.disconnect_count else "CONNECT", typing.cast(typing.Dict, more_collections.EMPTY_DICT), ) - self.logger.debug("received HELLO, interval is %ss", self.heartbeat_interval) + self.logger.debug("received HELLO (interval:%ss)", self.heartbeat_interval) completed, pending_tasks = await asyncio.wait( [self._heartbeat_keep_alive(self.heartbeat_interval), self._identify_or_resume_then_poll_events()], @@ -693,7 +693,7 @@ def _cs_init_kwargs(self): async def _identify_or_resume_then_poll_events(self): if self.session_id is None: self.status = GatewayStatus.IDENTIFYING - self.logger.debug("sending IDENTIFY") + self.logger.debug("preparing to send IDENTIFY") pl = { "op": codes.GatewayOpcode.IDENTIFY, @@ -719,8 +719,8 @@ async def _identify_or_resume_then_poll_events(self): await self._send(pl) self.logger.debug("sent IDENTIFY, now listening to incoming events") else: + self.logger.debug("preparing to send RESUME") self.status = GatewayStatus.RESUMING - self.logger.debug("sending RESUME") pl = { "op": codes.GatewayOpcode.RESUME, "d": {"token": self._token, "seq": self.seq, "session_id": self.session_id}, @@ -737,7 +737,7 @@ async def _heartbeat_keep_alive(self, heartbeat_interval): raise asyncio.TimeoutError( f"{self.shard_id}: connection is a zombie, haven't received HEARTBEAT ACK for too long" ) - self.logger.debug("sending heartbeat") + self.logger.debug("preparing to send HEARTBEAT (s:%s, interval:%ss)", self.seq, self.heartbeat_interval) await self._send({"op": codes.GatewayOpcode.HEARTBEAT, "d": self.seq}) self.last_heartbeat_sent = time.perf_counter() try: @@ -759,10 +759,18 @@ async def _poll_events(self): event_name = next_pl["t"] if event_name == "READY": + self.session_id = d["session_id"] + version = d["v"] + + self.logger.debug( + "connection is READY (session:%s, version:%s)", self.session_id, version, + ) + self.ready_event.set() self.dispatch(self, event_name, d) elif op == codes.GatewayOpcode.HEARTBEAT: + self.logger.debug("received HEARTBEAT, preparing to send HEARTBEAT ACK to server in response") await self._send({"op": codes.GatewayOpcode.HEARTBEAT_ACK}) elif op == codes.GatewayOpcode.RECONNECT: self.logger.debug("instructed by gateway server to restart connection") @@ -776,7 +784,7 @@ async def _poll_events(self): elif op == codes.GatewayOpcode.HEARTBEAT_ACK: now = time.perf_counter() self.heartbeat_latency = now - self.last_heartbeat_sent - self.logger.debug("received HEARTBEAT ACK in %ss", self.heartbeat_latency) + self.logger.debug("received HEARTBEAT ACK (latency:%ss)", self.heartbeat_latency) else: self.logger.debug("ignoring opcode %s with data %r", op, d) diff --git a/tests/hikari/net/test_shard.py b/tests/hikari/net/test_shard.py index 4e785199e0..ab2e714e55 100644 --- a/tests/hikari/net/test_shard.py +++ b/tests/hikari/net/test_shard.py @@ -809,6 +809,24 @@ def receive(): client.dispatch.assert_called_with(client, "MESSAGE_CREATE", {"content": "whatever"}) + @_helpers.timeout_after(5.0) + async def test_opcode_0_resume_sets_session_id(self, client): + client.seq = None + client.session_id = None + + def receive(): + client.requesting_close_event.set() + return {"op": 0, "d": {"v": 69, "session_id": "1a2b3c4d"}, "t": "READY", "s": 123} + + client._receive = mock.AsyncMock(wraps=receive) + + await client._poll_events() + + client.dispatch.assert_called_with(client, "READY", {"v": 69, "session_id": "1a2b3c4d"}) + + assert client.session_id == "1a2b3c4d" + assert client.seq == 123 + @pytest.mark.asyncio class TestRequestGuildMembers: From 650bc1583331b01bfd71a2d28b5043dd6dc82a04 Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Sun, 5 Apr 2020 18:34:27 +0100 Subject: [PATCH 070/922] Removed dead code --- hikari/clients/gateway_runner.py | 9 --------- 1 file changed, 9 deletions(-) diff --git a/hikari/clients/gateway_runner.py b/hikari/clients/gateway_runner.py index 1e136fab4d..42fb6021f7 100644 --- a/hikari/clients/gateway_runner.py +++ b/hikari/clients/gateway_runner.py @@ -21,7 +21,6 @@ This is only for use by developers of this library, regular users do not need to use this. """ -import asyncio import logging import os import sys @@ -92,14 +91,6 @@ def process_raw_event( raw_event_consumer_impl=_DummyConsumer(), ) - async def _restart_in_a_bit(): - conn = client.shards[0].connection - - async def fake_recv(): - return - - await asyncio.sleep(15) - client.run() From 68a772da8f2e5b23ac36727b277ffe711446a6c9 Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Sun, 5 Apr 2020 18:38:40 +0100 Subject: [PATCH 071/922] Fixed docstyle complaint about use of imperative mood. [skip deploy] --- hikari/net/shard.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hikari/net/shard.py b/hikari/net/shard.py index d57cec00e4..5e4b0d9830 100644 --- a/hikari/net/shard.py +++ b/hikari/net/shard.py @@ -450,7 +450,7 @@ def intents(self) -> typing.Optional[codes.GatewayIntent]: @property def reconnect_count(self) -> int: - """Number of times the gateway has reconnected since initialization. + """Reconnection count for this shard connection instance. This can be used as a debugging context, but is also used internally for exception management. From 4118a387a2802084a1c944828d72d62a9fd9c835 Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Mon, 6 Apr 2020 03:01:10 +0000 Subject: [PATCH 072/922] Update Dockerfile to use the repo for running stuff rather than pypi for obtaining the code [skip deploy] --- Dockerfile | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 81e835e6fb..68db195ce5 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,2 +1,3 @@ FROM python:3 -RUN pip install --pre hikari>=1.0.0 +COPY . . +RUN pip install -r requirements.txt From ffbd30c396a3ad4a6d3f61cddfe8bc28591eb17b Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Mon, 6 Apr 2020 19:23:52 +0100 Subject: [PATCH 073/922] Fixes #270, stops waiting for READY on RESUME events. --- hikari/clients/shard_client.py | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/hikari/clients/shard_client.py b/hikari/clients/shard_client.py index d867520639..2121b76d32 100644 --- a/hikari/clients/shard_client.py +++ b/hikari/clients/shard_client.py @@ -346,18 +346,23 @@ async def _spin_up(self) -> asyncio.Task: if connect_task in completed: raise connect_task.exception() - self.logger.info("sent %s, waiting for READY event", "RESUME" if is_resume else "IDENTIFY") - self._shard_state = ShardState.WAITING_FOR_READY + if is_resume: + self.logger.info("sent RESUME") - completed, _ = await asyncio.wait( - [connect_task, self._client.ready_event.wait()], return_when=asyncio.FIRST_COMPLETED - ) + else: + self.logger.info("sent IDENTIFY, waiting for READY event") - self.logger.info("now READY") - self._shard_state = ShardState.READY + self._shard_state = ShardState.WAITING_FOR_READY - if connect_task in completed: - raise connect_task.exception() + completed, _ = await asyncio.wait( + [connect_task, self._client.ready_event.wait()], return_when=asyncio.FIRST_COMPLETED + ) + + self.logger.info("now READY") + self._shard_state = ShardState.READY + + if connect_task in completed: + raise connect_task.exception() return connect_task From 642eb49811e730d83bb413d535bb55e3f5aa7b8a Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Mon, 6 Apr 2020 20:14:28 +0100 Subject: [PATCH 074/922] Fixed a second bug in RESUME handling; added some utility properties and functions to ShardClient and GatewayManager --- hikari/clients/bot_client.py | 17 +------ hikari/clients/gateway_manager.py | 75 +++++++++++++++++++++++++++++++ hikari/clients/shard_client.py | 53 +++++++++++++++++++++- 3 files changed, 127 insertions(+), 18 deletions(-) diff --git a/hikari/clients/bot_client.py b/hikari/clients/bot_client.py index 7fb3af5a3a..9d1d6d72f7 100644 --- a/hikari/clients/bot_client.py +++ b/hikari/clients/bot_client.py @@ -83,22 +83,7 @@ def __init__(self, config: configs.BotConfig, event_manager: event_managers.Even self.rest = rest_client.RESTClient(self.config) async def start(self): - while (gateway_bot := await self.rest.fetch_gateway_bot()).session_start_limit.remaining <= 0: - resume_at = datetime.datetime.now() + gateway_bot.session_start_limit.reset_after - - self.logger.critical( - "You have reached the max identify limit for this time window (%s). " - "To prevent your token being reset, I will wait for %s (until approx %s) " - "and then continue signing in. Press CTRL-C to shut down.", - gateway_bot.session_start_limit.total, - gateway_bot.session_start_limit.reset_after, - resume_at, - ) - - await asyncio.sleep(60) - while (now := datetime.datetime.now()) < resume_at: - self.logger.info("Still waiting, %s to go...", resume_at - now) - await asyncio.sleep(60) + gateway_bot = await self.rest.fetch_gateway_bot() self.logger.info( "You have sent an IDENTIFY %s time(s) before now, and have %s remaining. This will reset at %s.", diff --git a/hikari/clients/gateway_manager.py b/hikari/clients/gateway_manager.py index 295b29da07..5e2028c668 100644 --- a/hikari/clients/gateway_manager.py +++ b/hikari/clients/gateway_manager.py @@ -24,9 +24,13 @@ __all__ = ["GatewayManager"] import asyncio +import datetime +import math import time import typing +from hikari import gateway_entities +from hikari import guilds from hikari.clients import configs from hikari.clients import runnable from hikari.internal import conversions @@ -63,6 +67,32 @@ def __init__( } self.shard_ids = shard_ids + @property + def latency(self) -> float: + """Average heartbeat latency for all valid shards. + + This will return a mean of all the heartbeat intervals for all shards + with a valid heartbeat latency that are in the + :obj:`hikari.clients.shard_client.ShardState.READY` state. + + If no shards are in this state, this will return ``float('nan')`` + instead. + + Returns + ------- + :obj:`float` + The mean latency for all ``READY`` shards that have sent at least + one acknowledged ``HEARTBEAT`` payload. If there is not at least + one shard that meets this criteria, this will instead return + ``float('nan')``. + """ + latencies = [] + for shard in self.shards.values(): + if shard.connection_state == shard_client.ShardState.READY and not math.isnan(shard.latency): + latencies.append(shard.latency) + + return sum(latencies) / len(latencies) if latencies else float("nan") + async def start(self) -> None: """Start all shards. @@ -106,3 +136,48 @@ async def close(self, wait: bool = True) -> None: finish_time = time.perf_counter() self.logger.info("stopped %s shard(s) in approx %.2fs", len(self.shards), finish_time - start_time) self._is_running = False + + async def update_presence( + self, + *, + status: guilds.PresenceStatus = ..., + activity: typing.Optional[gateway_entities.GatewayActivity] = ..., + idle_since: typing.Optional[datetime.datetime] = ..., + is_afk: bool = ..., + ) -> None: + """Update the presence of the user for all shards. + + This will only update arguments that you explicitly specify a value for. + Any arguments that you do not explicitly provide some value for will + not be changed. + + Warnings + -------- + This will only apply to connected shards. + + Notes + ----- + If you wish to update a presence for a specific shard, you can do this + by using the :attr:`GatewayManager.shards` :obj:`typing.Mapping` to + find the shard you wish to update. + + Parameters + ---------- + status : :obj:`hikari.guilds.PresenceStatus` + The new status to set. + activity : :obj:`hikari.gateway_entities.GatewayActivity`, optional + The new activity to set. + idle_since : :obj:`datetime.datetime`, optional + The time to show up as being idle since, or ``None`` if not + applicable. + is_afk : :obj:`bool` + ``True`` if the user should be marked as AFK, or ``False`` + otherwise. + """ + await asyncio.gather( + *( + shard.update_presence(status=status, activity=activity, idle_since=idle_since, is_afk=is_afk) + for shard in self.shards.values() + if shard.connection_state in (shard_client.ShardState.WAITING_FOR_READY, shard_client.ShardState.READY) + ) + ) diff --git a/hikari/clients/shard_client.py b/hikari/clients/shard_client.py index 2121b76d32..bbd074f926 100644 --- a/hikari/clients/shard_client.py +++ b/hikari/clients/shard_client.py @@ -217,6 +217,54 @@ def is_afk(self) -> bool: # noqa: D401 """ return self._is_afk + @property + def latency(self) -> float: + """Latency between sending a HEARTBEAT and receiving an ACK. + + Returns + ------- + :obj:`float` + The heartbeat latency in seconds. This will be ``float('nan')`` + until the first heartbeat is performed. + """ + return self._client.heartbeat_latency + + @property + def heartbeat_interval(self) -> float: + """Time period to wait between sending HEARTBEAT payloads. + + Returns + ------- + :obj:`float` + The heartbeat interval in seconds. This will be ``float('nan')`` + until the connection has received a ``HELLO`` payload. + """ + return self._client.heartbeat_interval + + @property + def reconnect_count(self) -> float: + """Count of number of times the internal connection has reconnected. + + This includes RESUME and re-IDENTIFY events. + + Returns + ------- + :obj:`int` + The number of reconnects this shard has performed. + """ + return self._client.reconnect_count + + @property + def connection_state(self) -> ShardState: + """State of this shard. + + Returns + ------- + :obj:`ShardState` + The state of this shard. + """ + return self._shard_state + async def start(self): """Connect to the gateway on this shard and keep the connection alive. @@ -348,7 +396,6 @@ async def _spin_up(self) -> asyncio.Task: if is_resume: self.logger.info("sent RESUME") - else: self.logger.info("sent IDENTIFY, waiting for READY event") @@ -359,15 +406,17 @@ async def _spin_up(self) -> asyncio.Task: ) self.logger.info("now READY") - self._shard_state = ShardState.READY if connect_task in completed: raise connect_task.exception() + self._shard_state = ShardState.READY + return connect_task async def update_presence( self, + *, status: guilds.PresenceStatus = ..., activity: typing.Optional[gateway_entities.GatewayActivity] = ..., idle_since: typing.Optional[datetime.datetime] = ..., From 8a625cf883196dffdb8a02ae0bcaddde11a58b70 Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Mon, 6 Apr 2020 21:41:31 +0100 Subject: [PATCH 075/922] Removed RECONNECT event and fixed RESUME to wait for RESUMED event. - Removed RECONNECT as it will always be followed by a CONNECT or RESUME on that shard. Reconnecting is not relevant to multiple sharded bots if you perform actions on the whole bot as a result so I am discouraging this. - Added some missing test cases. - On resume, we now listen for the RESUMED event. - CONNECT and DISCONNECT are now CONNECTED and DISCONNECTED. - Optimised imports... --- hikari/__init__.py | 12 ++--- hikari/channels.py | 4 +- hikari/clients/__init__.py | 4 +- hikari/clients/bot_client.py | 4 +- hikari/clients/gateway_manager.py | 2 +- hikari/clients/gateway_runner.py | 4 +- hikari/clients/shard_client.py | 34 ++++++++----- hikari/embeds.py | 2 +- hikari/emojis.py | 2 +- hikari/events.py | 11 +---- hikari/gateway_entities.py | 2 +- hikari/guilds.py | 8 +-- hikari/invites.py | 4 +- hikari/messages.py | 2 +- hikari/net/shard.py | 62 ++++++++---------------- hikari/oauth2.py | 4 +- hikari/snowflakes.py | 2 +- hikari/state/event_dispatchers.py | 2 +- hikari/state/event_managers.py | 16 ++---- hikari/state/stateless_event_managers.py | 35 +++++++++++++ hikari/users.py | 4 +- hikari/voices.py | 2 +- hikari/webhooks.py | 2 +- tests/hikari/net/test_shard.py | 49 ++++++++++++++++--- 24 files changed, 155 insertions(+), 118 deletions(-) create mode 100644 hikari/state/stateless_event_managers.py diff --git a/hikari/__init__.py b/hikari/__init__.py index bb0fa18ebe..1181796085 100644 --- a/hikari/__init__.py +++ b/hikari/__init__.py @@ -17,12 +17,6 @@ # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . """Hikari's models framework for writing Discord bots in Python.""" -from hikari._about import __author__ -from hikari._about import __copyright__ -from hikari._about import __email__ -from hikari._about import __license__ -from hikari._about import __url__ -from hikari._about import __version__ from hikari import audit_logs from hikari import channels from hikari import clients @@ -45,6 +39,12 @@ from hikari import users from hikari import voices from hikari import webhooks +from hikari._about import __author__ +from hikari._about import __copyright__ +from hikari._about import __email__ +from hikari._about import __license__ +from hikari._about import __url__ +from hikari._about import __version__ from hikari.audit_logs import * from hikari.channels import * from hikari.clients import * diff --git a/hikari/channels.py b/hikari/channels.py index c0d2096c5c..8d8ff24f94 100644 --- a/hikari/channels.py +++ b/hikari/channels.py @@ -47,11 +47,11 @@ import enum import typing -from hikari.internal import marshaller from hikari import entities from hikari import permissions -from hikari import users from hikari import snowflakes +from hikari import users +from hikari.internal import marshaller @enum.unique diff --git a/hikari/clients/__init__.py b/hikari/clients/__init__.py index 270cec9184..ea7f1e7025 100644 --- a/hikari/clients/__init__.py +++ b/hikari/clients/__init__.py @@ -23,14 +23,12 @@ from hikari.clients import gateway_manager from hikari.clients import rest_client from hikari.clients import runnable - from hikari.clients.bot_client import * from hikari.clients.configs import * from hikari.clients.gateway_manager import * from hikari.clients.rest_client import * -from hikari.clients.shard_client import * from hikari.clients.runnable import * - +from hikari.clients.shard_client import * __all__ = [ *bot_client.__all__, diff --git a/hikari/clients/bot_client.py b/hikari/clients/bot_client.py index 9d1d6d72f7..7f2786a6fc 100644 --- a/hikari/clients/bot_client.py +++ b/hikari/clients/bot_client.py @@ -20,7 +20,6 @@ __all__ = ["BotBase", "StatelessBot"] import abc -import asyncio import datetime import logging import typing @@ -35,6 +34,7 @@ from hikari.internal import more_logging from hikari.state import event_dispatchers from hikari.state import event_managers +from hikari.state import stateless_event_managers class BotBase(runnable.RunnableClient, event_dispatchers.EventDispatcher): @@ -146,4 +146,4 @@ class StatelessBot(BotBase): """ def __init__(self, config: configs.BotConfig) -> None: - super().__init__(config, event_managers.StatelessEventManagerImpl()) + super().__init__(config, stateless_event_managers.StatelessEventManagerImpl()) diff --git a/hikari/clients/gateway_manager.py b/hikari/clients/gateway_manager.py index 5e2028c668..d5d93eaed3 100644 --- a/hikari/clients/gateway_manager.py +++ b/hikari/clients/gateway_manager.py @@ -33,9 +33,9 @@ from hikari import guilds from hikari.clients import configs from hikari.clients import runnable +from hikari.clients import shard_client from hikari.internal import conversions from hikari.internal import more_logging -from hikari.clients import shard_client from hikari.state import raw_event_consumers ShardT = typing.TypeVar("ShardT", bound=shard_client.ShardClient) diff --git a/hikari/clients/gateway_runner.py b/hikari/clients/gateway_runner.py index 42fb6021f7..d9588d122c 100644 --- a/hikari/clients/gateway_runner.py +++ b/hikari/clients/gateway_runner.py @@ -73,9 +73,9 @@ def run_gateway(compression, color, debug, logger, shards, token, url, verify_ss class _DummyConsumer(raw_event_consumers.RawEventConsumer): def process_raw_event( - self, _client: shard_client.ShardClient, _name: str, _payload: entities.RawEntityT + self, _client: shard_client.ShardClient, name: str, payload: entities.RawEntityT ) -> None: - pass + logging.debug("dispatched %s with body [%-100.100s]", name, payload) client = gateway_manager.GatewayManager( shard_ids=[*range(shards)], diff --git a/hikari/clients/shard_client.py b/hikari/clients/shard_client.py index bbd074f926..c72fef8e40 100644 --- a/hikari/clients/shard_client.py +++ b/hikari/clients/shard_client.py @@ -35,15 +35,15 @@ import aiohttp +from hikari import errors +from hikari import events +from hikari import gateway_entities +from hikari import guilds from hikari.clients import configs from hikari.clients import runnable from hikari.internal import more_asyncio from hikari.internal import more_logging -from hikari import events -from hikari import gateway_entities -from hikari import guilds from hikari.net import codes -from hikari import errors from hikari.net import ratelimits from hikari.net import shard from hikari.state import raw_event_consumers @@ -58,15 +58,14 @@ class ShardState(enum.IntEnum): #: The shard is not running. NOT_RUNNING = 0 #: The shard is undergoing the initial connection handshake. - HANDSHAKE = enum.auto() + CONNECTING = enum.auto() #: The initialization handshake has completed. We are waiting for the shard #: to receive the ``READY`` event. WAITING_FOR_READY = enum.auto() #: The shard is ``READY``. READY = enum.auto() - #: The shard has disconnected and is currently attempting to reconnect - #: again. - RECONNECTING = enum.auto() + #: The shard has sent a request to ``RESUME`` and is waiting for a response. + RESUMING = enum.auto() #: The shard is currently shutting down permanently. STOPPING = enum.auto() #: The shard has shut down and is no longer connected. @@ -372,7 +371,7 @@ async def _keep_alive(self): async def _spin_up(self) -> asyncio.Task: self.logger.debug("initializing shard") - self._shard_state = ShardState.HANDSHAKE + self._shard_state = ShardState.CONNECTING is_resume = self._client.seq is not None and self._client.session_id is not None @@ -395,7 +394,18 @@ async def _spin_up(self) -> asyncio.Task: raise connect_task.exception() if is_resume: - self.logger.info("sent RESUME") + self.logger.info("sent RESUME, waiting for RESUMED event") + self._shard_state = ShardState.RESUMING + + completed, _ = await asyncio.wait( + [connect_task, self._client.resumed_event.wait()], return_when=asyncio.FIRST_COMPLETED + ) + + if connect_task in completed: + raise connect_task.exception() + + self.logger.info("now RESUMED") + else: self.logger.info("sent IDENTIFY, waiting for READY event") @@ -405,11 +415,11 @@ async def _spin_up(self) -> asyncio.Task: [connect_task, self._client.ready_event.wait()], return_when=asyncio.FIRST_COMPLETED ) - self.logger.info("now READY") - if connect_task in completed: raise connect_task.exception() + self.logger.info("now READY") + self._shard_state = ShardState.READY return connect_task diff --git a/hikari/embeds.py b/hikari/embeds.py index 5b37ba69fd..defb5349d7 100644 --- a/hikari/embeds.py +++ b/hikari/embeds.py @@ -32,9 +32,9 @@ import typing import hikari.internal.conversions -from hikari.internal import marshaller from hikari import colors from hikari import entities +from hikari.internal import marshaller @marshaller.attrs(slots=True) diff --git a/hikari/emojis.py b/hikari/emojis.py index b02904b071..7ce4444330 100644 --- a/hikari/emojis.py +++ b/hikari/emojis.py @@ -19,10 +19,10 @@ """Components and entities that are used to describe both custom and Unicode emojis on Discord.""" import typing -from hikari.internal import marshaller from hikari import entities from hikari import snowflakes from hikari import users +from hikari.internal import marshaller __all__ = ["Emoji", "UnicodeEmoji", "UnknownEmoji", "GuildEmoji"] diff --git a/hikari/events.py b/hikari/events.py index 266f656c76..5ef313dd69 100644 --- a/hikari/events.py +++ b/hikari/events.py @@ -23,7 +23,6 @@ "ExceptionEvent", "ConnectedEvent", "DisconnectedEvent", - "ReconnectedEvent", "StartedEvent", "StoppingEvent", "StoppedEvent", @@ -72,8 +71,6 @@ import attr import hikari.internal.conversions -from hikari.internal import assertions -from hikari.internal import marshaller from hikari import channels from hikari import embeds as _embeds from hikari import emojis as _emojis @@ -85,6 +82,8 @@ from hikari import snowflakes from hikari import users from hikari import voices +from hikari.internal import assertions +from hikari.internal import marshaller T_contra = typing.TypeVar("T_contra", contravariant=True) @@ -159,12 +158,6 @@ class DisconnectedEvent(HikariEvent, entities.Deserializable): """Event invoked each time a shard disconnects.""" -@mark_as_websocket_event -@marshaller.attrs(slots=True) -class ReconnectedEvent(HikariEvent, entities.Deserializable): - """Event invoked each time a shard successfully reconnects.""" - - @mark_as_websocket_event @marshaller.attrs(slots=True) class ReadyEvent(HikariEvent, entities.Deserializable): diff --git a/hikari/gateway_entities.py b/hikari/gateway_entities.py index f808208966..4077719191 100644 --- a/hikari/gateway_entities.py +++ b/hikari/gateway_entities.py @@ -22,9 +22,9 @@ import datetime import typing -from hikari.internal import marshaller from hikari import entities from hikari import guilds +from hikari.internal import marshaller @marshaller.attrs(slots=True) diff --git a/hikari/guilds.py b/hikari/guilds.py index a0a71a0b03..deebfda61b 100644 --- a/hikari/guilds.py +++ b/hikari/guilds.py @@ -47,16 +47,16 @@ import enum import typing -from hikari.internal import cdn -from hikari.internal import conversions -from hikari.internal import marshaller -from hikari import colors from hikari import channels as _channels +from hikari import colors from hikari import emojis as _emojis from hikari import entities from hikari import permissions as _permissions from hikari import snowflakes from hikari import users +from hikari.internal import cdn +from hikari.internal import conversions +from hikari.internal import marshaller @enum.unique diff --git a/hikari/invites.py b/hikari/invites.py index 51e6a1191f..45422c1504 100644 --- a/hikari/invites.py +++ b/hikari/invites.py @@ -24,12 +24,12 @@ import typing import hikari.internal.conversions -from hikari.internal import cdn -from hikari.internal import marshaller from hikari import channels from hikari import entities from hikari import guilds from hikari import users +from hikari.internal import cdn +from hikari.internal import marshaller @enum.unique diff --git a/hikari/messages.py b/hikari/messages.py index f87d865f52..777602e200 100644 --- a/hikari/messages.py +++ b/hikari/messages.py @@ -33,7 +33,6 @@ import typing import hikari.internal.conversions -from hikari.internal import marshaller from hikari import embeds as _embeds from hikari import emojis as _emojis from hikari import entities @@ -41,6 +40,7 @@ from hikari import oauth2 from hikari import snowflakes from hikari import users +from hikari.internal import marshaller @enum.unique diff --git a/hikari/net/shard.py b/hikari/net/shard.py index 5e4b0d9830..2a62569ab9 100644 --- a/hikari/net/shard.py +++ b/hikari/net/shard.py @@ -30,12 +30,11 @@ * Gateway documentation: https://discordapp.com/developers/docs/topics/gateway * Opcode documentation: https://discordapp.com/developers/docs/topics/opcodes-and-status-codes """ -__all__ = ["GatewayStatus", "ShardConnection"] +__all__ = ["ShardConnection"] import asyncio import contextlib import datetime -import enum import json import logging import math @@ -48,27 +47,12 @@ import aiohttp.typedefs from hikari import errors -from hikari.internal import more_collections from hikari.internal import more_logging from hikari.net import codes from hikari.net import ratelimits from hikari.net import user_agent from hikari.net import versions - -class GatewayStatus(str, enum.Enum): - """Various states that a gateway connection can be in.""" - - OFFLINE = "offline" - CONNECTING = "connecting" - WAITING_FOR_HELLO = "waiting for HELLO" - IDENTIFYING = "identifying" - RESUMING = "resuming" - SHUTTING_DOWN = "shutting down" - WAITING_FOR_MESSAGES = "waiting for messages" - PROCESSING_NEW_MESSAGE = "processing message" - - #: The signature for an event dispatch callback. DispatchT = typing.Callable[["ShardConnection", str, typing.Dict], None] @@ -85,13 +69,11 @@ class ShardConnection: Expected events that may be passed to the event dispatcher are documented in the `gateway event reference `_. No normalization of the gateway event names occurs. In addition to this, - a few internal events can also be triggered to notify you of changes to + some internal events can also be triggered to notify you of changes to the connection state. - * ``CONNECT`` - fired on initial connection to Discord. - * ``RECONNECT`` - fired if we have previously been connected to Discord - but are making a new connection on an existing :obj:`ShardConnection` instance. - * ``DISCONNECT`` - fired when the connection is closed for any reason. + * ``CONNECTED`` - fired on initial connection to Discord. + * ``DISCONNECTED`` - fired when the connection is closed for any reason. Parameters ---------- @@ -197,6 +179,7 @@ class ShardConnection: "_proxy_url", "_ratelimiter", "ready_event", + "resumed_event", "requesting_close_event", "_session", "session_id", @@ -290,6 +273,11 @@ class ShardConnection: #: :type: :obj:`asyncio.Event` ready_event: asyncio.Event + #: An event that is triggered when a resume has succeeded on the gateway. + #: + #: :type: :obj:`asyncio.Event` + resumed_event: asyncio.Event + #: An event that is set when something requests that the connection #: should close somewhere. #: @@ -317,12 +305,6 @@ class ShardConnection: #: :type: :obj:`int` shard_count: int - #: The current status of the gateway. This can be used to print out - #: informative context for large sharded bots. - #: - #: :type: :obj:`GatewayStatus` - status: GatewayStatus - #: The API version to use on Discord. #: #: :type: :obj:`int` @@ -398,11 +380,11 @@ def __init__( self.last_message_received: float = float("nan") self.requesting_close_event: asyncio.Event = asyncio.Event() self.ready_event: asyncio.Event = asyncio.Event() + self.resumed_event: asyncio.Event = asyncio.Event() self.session_id = session_id self.seq: typing.Optional[int] = seq self.shard_id: int = shard_id self.shard_count: int = shard_count - self.status: GatewayStatus = GatewayStatus.OFFLINE self.version: int = int(version) self.logger: logging.Logger = more_logging.get_named_logger(self, f"#{shard_id}", f"v{self.version}") @@ -567,7 +549,6 @@ async def close(self, close_code: int = 1000) -> None: The close code to use. Defaults to ``1000`` (normal closure). """ if not self.requesting_close_event.is_set(): - self.status = GatewayStatus.SHUTTING_DOWN self.requesting_close_event.set() # These will attribute error if they are not set; in this case we don't care, just ignore it. with contextlib.suppress(asyncio.TimeoutError, AttributeError): @@ -593,14 +574,13 @@ async def connect(self, client_session_type=aiohttp.ClientSession) -> None: self.identify_event.clear() self.ready_event.clear() self.requesting_close_event.clear() + self.resumed_event.clear() self._session = client_session_type(**self._cs_init_kwargs) close_code = codes.GatewayCloseCode.ABNORMAL_CLOSURE try: - self.status = GatewayStatus.CONNECTING self._ws = await self._session.ws_connect(**self._ws_connect_kwargs) - self.status = GatewayStatus.WAITING_FOR_HELLO self._connected_at = time.perf_counter() self._zlib = zlib.decompressobj() @@ -615,11 +595,7 @@ async def connect(self, client_session_type=aiohttp.ClientSession) -> None: self.hello_event.set() - self.dispatch( - self, - "RECONNECT" if self.disconnect_count else "CONNECT", - typing.cast(typing.Dict, more_collections.EMPTY_DICT), - ) + self.dispatch(self, "CONNECTED", {}) self.logger.debug("received HELLO (interval:%ss)", self.heartbeat_interval) completed, pending_tasks = await asyncio.wait( @@ -661,7 +637,6 @@ async def connect(self, client_session_type=aiohttp.ClientSession) -> None: finally: await self.close(close_code) self.closed_event.set() - self.status = GatewayStatus.OFFLINE self._connected_at = float("nan") self.last_heartbeat_sent = float("nan") self.heartbeat_latency = float("nan") @@ -670,7 +645,7 @@ async def connect(self, client_session_type=aiohttp.ClientSession) -> None: self._ws = None await self._session.close() self._session = None - self.dispatch(self, "DISCONNECT", typing.cast(typing.Dict, more_collections.EMPTY_DICT)) + self.dispatch(self, "DISCONNECTED", {}) @property def _ws_connect_kwargs(self): @@ -692,7 +667,6 @@ def _cs_init_kwargs(self): async def _identify_or_resume_then_poll_events(self): if self.session_id is None: - self.status = GatewayStatus.IDENTIFYING self.logger.debug("preparing to send IDENTIFY") pl = { @@ -720,7 +694,6 @@ async def _identify_or_resume_then_poll_events(self): self.logger.debug("sent IDENTIFY, now listening to incoming events") else: self.logger.debug("preparing to send RESUME") - self.status = GatewayStatus.RESUMING pl = { "op": codes.GatewayOpcode.RESUME, "d": {"token": self._token, "seq": self.seq, "session_id": self.session_id}, @@ -747,9 +720,7 @@ async def _heartbeat_keep_alive(self, heartbeat_interval): async def _poll_events(self): while not self.requesting_close_event.is_set(): - self.status = GatewayStatus.WAITING_FOR_MESSAGES next_pl = await self._receive() - self.status = GatewayStatus.PROCESSING_NEW_MESSAGE op = next_pl["op"] d = next_pl["d"] @@ -768,6 +739,11 @@ async def _poll_events(self): self.ready_event.set() + elif event_name == "RESUMED": + self.resumed_event.set() + + self.logger.debug("connection has RESUMED (session:%s, s:%s)", self.session_id, self.seq) + self.dispatch(self, event_name, d) elif op == codes.GatewayOpcode.HEARTBEAT: self.logger.debug("received HEARTBEAT, preparing to send HEARTBEAT ACK to server in response") diff --git a/hikari/oauth2.py b/hikari/oauth2.py index 7acbe9d968..44e887cc1d 100644 --- a/hikari/oauth2.py +++ b/hikari/oauth2.py @@ -22,13 +22,13 @@ import enum import typing -from hikari.internal import cdn -from hikari.internal import marshaller from hikari import entities from hikari import guilds from hikari import permissions from hikari import snowflakes from hikari import users +from hikari.internal import cdn +from hikari.internal import marshaller @enum.unique diff --git a/hikari/snowflakes.py b/hikari/snowflakes.py index a14222dd7b..ea7dee120b 100644 --- a/hikari/snowflakes.py +++ b/hikari/snowflakes.py @@ -29,8 +29,8 @@ import typing import hikari.internal.conversions -from hikari.internal import marshaller from hikari import entities +from hikari.internal import marshaller @functools.total_ordering diff --git a/hikari/state/event_dispatchers.py b/hikari/state/event_dispatchers.py index 3a6e85dd20..7a3953cb4b 100644 --- a/hikari/state/event_dispatchers.py +++ b/hikari/state/event_dispatchers.py @@ -24,11 +24,11 @@ import logging import typing +from hikari import events from hikari.internal import assertions from hikari.internal import more_asyncio from hikari.internal import more_collections from hikari.internal import more_logging -from hikari import events EventT = typing.TypeVar("EventT", bound=events.HikariEvent) PredicateT = typing.Callable[[EventT], typing.Union[bool, typing.Coroutine[None, None, bool]]] diff --git a/hikari/state/event_managers.py b/hikari/state/event_managers.py index 6d3bd754cb..7c2b902cf3 100644 --- a/hikari/state/event_managers.py +++ b/hikari/state/event_managers.py @@ -20,15 +20,14 @@ __all__ = ["raw_event_mapper", "EventManager"] import inspect - import typing -from hikari.clients import shard_client -from hikari.state import event_dispatchers from hikari import entities -from hikari.state import raw_event_consumers +from hikari.clients import shard_client from hikari.internal import assertions from hikari.internal import more_logging +from hikari.state import event_dispatchers +from hikari.state import raw_event_consumers EVENT_MARKER_ATTR = "___event_name___" @@ -174,12 +173,3 @@ def process_raw_event( else: event = handler(shard_client_obj, payload) self.event_dispatcher.dispatch_event(event) - - -class StatelessEventManagerImpl(EventManager[event_dispatchers.EventDispatcher]): - """Stateless event manager implementation for stateless bots. - - This is an implementation that does not rely on querying prior information to - operate. The implementation details of this are much simpler than a stateful - version, and are not immediately affected by the use of intents. - """ diff --git a/hikari/state/stateless_event_managers.py b/hikari/state/stateless_event_managers.py new file mode 100644 index 0000000000..1f511a4803 --- /dev/null +++ b/hikari/state/stateless_event_managers.py @@ -0,0 +1,35 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# Copyright © Nekoka.tt 2019-2020 +# +# This file is part of Hikari. +# +# Hikari is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Hikari is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Hikari. If not, see . +"""Event management for stateless bots.""" + +__all__ = ["StatelessEventManagerImpl"] + +from hikari.state import event_dispatchers +from hikari.state import event_managers + + +class StatelessEventManagerImpl(event_managers.EventManager[event_dispatchers.EventDispatcher]): + """Stateless event manager implementation for stateless bots. + + This is an implementation that does not rely on querying prior information to + operate. The implementation details of this are much simpler than a stateful + version, and are not immediately affected by the use of intents. + """ + + # @event_managers.raw_event_mapper("CONNECT") diff --git a/hikari/users.py b/hikari/users.py index 217baa9160..4811c7ea99 100644 --- a/hikari/users.py +++ b/hikari/users.py @@ -22,10 +22,10 @@ import enum import typing -from hikari.internal import cdn -from hikari.internal import marshaller from hikari import entities from hikari import snowflakes +from hikari.internal import cdn +from hikari.internal import marshaller @marshaller.attrs(slots=True) diff --git a/hikari/voices.py b/hikari/voices.py index a47e29bc31..b56b337913 100644 --- a/hikari/voices.py +++ b/hikari/voices.py @@ -21,10 +21,10 @@ import typing -from hikari.internal import marshaller from hikari import entities from hikari import guilds from hikari import snowflakes +from hikari.internal import marshaller @marshaller.attrs(slots=True) diff --git a/hikari/webhooks.py b/hikari/webhooks.py index c95751db7d..591b525016 100644 --- a/hikari/webhooks.py +++ b/hikari/webhooks.py @@ -22,10 +22,10 @@ import enum import typing -from hikari.internal import marshaller from hikari import entities from hikari import snowflakes from hikari import users +from hikari.internal import marshaller @enum.unique diff --git a/tests/hikari/net/test_shard.py b/tests/hikari/net/test_shard.py index ab2e714e55..167f2f4f43 100644 --- a/tests/hikari/net/test_shard.py +++ b/tests/hikari/net/test_shard.py @@ -25,7 +25,7 @@ import aiohttp import async_timeout -import cymock as mock +from unittest import mock import pytest from hikari.internal import more_collections @@ -272,14 +272,35 @@ async def test_RuntimeError_if_already_connected(self, client): client._identify_or_resume_then_poll_events.assert_not_called() client._heartbeat_keep_alive.assert_not_called() + @pytest.mark.parametrize( + "event_attr", ["closed_event", "identify_event", "ready_event", "requesting_close_event", "resumed_event"] + ) @_helpers.timeout_after(10.0) - async def test_closed_event_unset_on_open(self, client, client_session_t): - client.closed_event.set() + async def test_events_unset_on_open(self, client, client_session_t, event_attr): + getattr(client, event_attr).set() with self.suppress_closure(): task = asyncio.create_task(client.connect(client_session_t)) + # Wait until the first main event object is set. By then we expect + # the event we are testing to have been unset again if it is + # working properly. await client.hello_event.wait() - assert not client.closed_event.is_set() + assert not getattr(client, event_attr).is_set() await task + + async def test_hello_event_unset_on_open(self, client, client_session_t): + client.hello_event = mock.MagicMock() + + with self.suppress_closure(): + await client.connect(client_session_t) + + client.hello_event.clear.assert_called_once() + client.hello_event.set.assert_called_once() + + @_helpers.timeout_after(10.0) + async def test_closed_event_set_on_connect_terminate(self, client, client_session_t): + with self.suppress_closure(): + await asyncio.create_task(client.connect(client_session_t)) + assert client.closed_event.is_set() @_helpers.timeout_after(10.0) @@ -346,11 +367,25 @@ async def test_disconnecting_increments_disconnect_count(self, client, client_se assert client.disconnect_count == 70 @_helpers.timeout_after(10.0) - async def test_disconnecting_dispatches_DISCONNECT(self, client, client_session_t): + async def test_connecting_dispatches_CONNECTED(self, client, client_session_t): client.dispatch = mock.MagicMock() with self.suppress_closure(): - await client.connect(client_session_t) - client.dispatch.assert_called_with(client, "DISCONNECT", more_collections.EMPTY_DICT) + task = asyncio.create_task(client.connect(client_session_t)) + await client.hello_event.wait() + # sanity check for the DISCONNECTED test + assert mock.call(client, "CONNECTED", more_collections.EMPTY_DICT) in client.dispatch.call_args_list + client.dispatch.assert_called_with(client, "CONNECTED", more_collections.EMPTY_DICT) + await task + + @_helpers.timeout_after(10.0) + async def test_disconnecting_dispatches_DISCONNECTED(self, client, client_session_t): + client.dispatch = mock.MagicMock() + with self.suppress_closure(): + task = asyncio.create_task(client.connect(client_session_t)) + await client.hello_event.wait() + assert mock.call(client, "DISCONNECTED", more_collections.EMPTY_DICT) not in client.dispatch.call_args_list + await task + client.dispatch.assert_called_with(client, "DISCONNECTED", more_collections.EMPTY_DICT) @_helpers.timeout_after(10.0) async def test_new_zlib_each_time(self, client, client_session_t): From ad2dc2f7438d0c75c5bc1ebde57e2a97afbe7a4e Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Mon, 6 Apr 2020 22:12:08 +0100 Subject: [PATCH 076/922] Updated shard reconnect logic to whitelist all codes as resumable apart from known cases. This prevents undocumented opcodes like 1001 which I have received a few times today causing big errors unnecesarilly. This change also reduces the verbosity of critical failures by not chaining unnecesarry tracebacks. --- hikari/clients/shard_client.py | 23 ++++++++++++----------- hikari/net/codes.py | 4 +--- hikari/net/shard.py | 5 ++++- 3 files changed, 17 insertions(+), 15 deletions(-) diff --git a/hikari/clients/shard_client.py b/hikari/clients/shard_client.py index c72fef8e40..f06debd172 100644 --- a/hikari/clients/shard_client.py +++ b/hikari/clients/shard_client.py @@ -339,10 +339,9 @@ async def _keep_alive(self): self.logger.warning("invalid session, so will attempt to resume") else: self.logger.warning("invalid session, so will attempt to reconnect") - - if not ex.can_resume: self._client.seq = None self._client.session_id = None + do_not_back_off = True await asyncio.sleep(5) except errors.GatewayMustReconnectError: @@ -351,17 +350,19 @@ async def _keep_alive(self): await asyncio.sleep(5) except errors.GatewayServerClosedConnectionError as ex: if ex.close_code in ( - codes.GatewayCloseCode.RATE_LIMITED, - codes.GatewayCloseCode.SESSION_TIMEOUT, - codes.GatewayCloseCode.INVALID_SEQ, - codes.GatewayCloseCode.UNKNOWN_ERROR, - codes.GatewayCloseCode.SESSION_TIMEOUT, - codes.GatewayCloseCode.NORMAL_CLOSURE, + codes.GatewayCloseCode.NOT_AUTHENTICATED, + codes.GatewayCloseCode.AUTHENTICATION_FAILED, + codes.GatewayCloseCode.ALREADY_AUTHENTICATED, + codes.GatewayCloseCode.SHARDING_REQUIRED, + codes.GatewayCloseCode.INVALID_VERSION, + codes.GatewayCloseCode.INVALID_INTENT, + codes.GatewayCloseCode.DISALLOWED_INTENT, ): - self.logger.warning("disconnected by Discord, will attempt to reconnect") - else: self.logger.error("disconnected by Discord, %s: %s", type(ex).__name__, ex.reason) - raise ex + raise ex from None + + self.logger.warning("disconnected by Discord, will attempt to reconnect") + except errors.GatewayClientClosedError: self.logger.warning("shutting down") return diff --git a/hikari/net/codes.py b/hikari/net/codes.py index 7fe9b318be..a8e6b7d5f8 100644 --- a/hikari/net/codes.py +++ b/hikari/net/codes.py @@ -70,10 +70,8 @@ class GatewayCloseCode(enum.IntEnum): between `1000` and `1999` inclusive are generally client-side codes. """ - #: You closed your bot manually. + #: The application running closed. NORMAL_CLOSURE = 1000 - #: Your bot stopped working and shut down. - ABNORMAL_CLOSURE = 1006 #: Discord is not sure what went wrong. Try reconnecting? UNKNOWN_ERROR = 4000 #: You sent an invalid Gateway opcode or an invalid payload for an opcode. Don't do that! diff --git a/hikari/net/shard.py b/hikari/net/shard.py index 2a62569ab9..5b86f99d03 100644 --- a/hikari/net/shard.py +++ b/hikari/net/shard.py @@ -577,7 +577,10 @@ async def connect(self, client_session_type=aiohttp.ClientSession) -> None: self.resumed_event.clear() self._session = client_session_type(**self._cs_init_kwargs) - close_code = codes.GatewayCloseCode.ABNORMAL_CLOSURE + + # 1000 and 1001 will invalidate sessions, 1006 (used here before) + # is a sketchy area as to the intent. 4000 is known to work normally. + close_code = codes.GatewayCloseCode.UNKNOWN_ERROR try: self._ws = await self._session.ws_connect(**self._ws_connect_kwargs) From f48506bfb9af893a4073784067ced18d50bf9654 Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Mon, 6 Apr 2020 22:16:53 +0100 Subject: [PATCH 077/922] Fixed failing UT I missed --- tests/hikari/net/test_codes.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/hikari/net/test_codes.py b/tests/hikari/net/test_codes.py index d3ea8426fb..207c975771 100644 --- a/tests/hikari/net/test_codes.py +++ b/tests/hikari/net/test_codes.py @@ -26,7 +26,7 @@ def test_str_HTTPStatusCode(): def test_str_GatewayCloseCode(): - assert str(codes.GatewayCloseCode.ABNORMAL_CLOSURE) == "1006 Abnormal Closure" + assert str(codes.GatewayCloseCode.UNKNOWN_ERROR) == "4000 Unknown Error" def test_str_GatewayOpcode(): From 72a6b41cc23529efc80d11364c209a98de27bf61 Mon Sep 17 00:00:00 2001 From: davfsa Date: Tue, 7 Apr 2020 09:05:50 +0200 Subject: [PATCH 078/922] Fix internal documentation references --- hikari/audit_logs.py | 2 +- hikari/channels.py | 2 +- hikari/clients/bot_client.py | 2 +- hikari/clients/shard_client.py | 4 +- hikari/errors.py | 21 +++++++++- hikari/guilds.py | 60 ++++++++++++++++------------- hikari/internal/marshaller.py | 13 ++++--- hikari/invites.py | 2 +- hikari/net/ratelimits.py | 22 +++++------ hikari/net/rest.py | 16 ++++---- hikari/net/shard.py | 15 ++++---- hikari/oauth2.py | 4 +- hikari/state/event_dispatchers.py | 2 +- hikari/state/event_managers.py | 14 +++---- hikari/state/raw_event_consumers.py | 2 +- 15 files changed, 103 insertions(+), 78 deletions(-) diff --git a/hikari/audit_logs.py b/hikari/audit_logs.py index fe15a49d26..2e4fa8ff1b 100644 --- a/hikari/audit_logs.py +++ b/hikari/audit_logs.py @@ -252,7 +252,7 @@ def register_audit_log_entry_info( Returns ------- - ``decorator(cls: T) -> T`` + ``decorator(T) -> T`` The decorator to decorate the class with. """ diff --git a/hikari/channels.py b/hikari/channels.py index c0d2096c5c..c4041c5ec2 100644 --- a/hikari/channels.py +++ b/hikari/channels.py @@ -122,7 +122,7 @@ def register_channel_type(type_: ChannelType) -> typing.Callable[[typing.Type["C Returns ------- - ``decorator`` + ``decorator(T) -> T`` The decorator to decorate the class with. """ diff --git a/hikari/clients/bot_client.py b/hikari/clients/bot_client.py index 7fb3af5a3a..5fcbbe856a 100644 --- a/hikari/clients/bot_client.py +++ b/hikari/clients/bot_client.py @@ -61,7 +61,7 @@ class BotBase(runnable.RunnableClient, event_dispatchers.EventDispatcher): #: The gateway for this bot. #: - #: :type: :obj:`hikari.clients.gateway_client.GatewayClient` + #: :type: :obj:`hikari.clients.gateway_manager.GatewayManager` [ :obj:`hikari.clients.shard_client.ShardClient` ] gateway: gateway_manager.GatewayManager[shard_client.ShardClient] #: The logger to use for this bot. diff --git a/hikari/clients/shard_client.py b/hikari/clients/shard_client.py index d867520639..8734e63b50 100644 --- a/hikari/clients/shard_client.py +++ b/hikari/clients/shard_client.py @@ -88,7 +88,7 @@ class ShardClient(runnable.RunnableClient): The number of shards that make up this distributed application. config : :obj:`hikari.clients.configs.WebsocketConfig` The gateway configuration to use to initialize this shard. - raw_event_consumer_impl : :obj:`hikari.state.raw_event_consumer.RawEventConsumer` + raw_event_consumer_impl : :obj:`hikari.state.raw_event_consumers.RawEventConsumer` The consumer of a raw event. url : :obj:`str` The URL to connect the gateway to. @@ -96,7 +96,7 @@ class ShardClient(runnable.RunnableClient): Notes ----- Generally, you want to use - :obj:`hikari.clients.gateway_client.GatewayClient` rather than this class + :obj:`hikari.clients.gateway_manager.GatewayManager` rather than this class directly, as that will handle sharding where enabled and applicable, and provides a few more bits and pieces that may be useful such as state management and event dispatcher integration. and If you want to customize diff --git a/hikari/errors.py b/hikari/errors.py index a5b6748e66..40794d262d 100644 --- a/hikari/errors.py +++ b/hikari/errors.py @@ -16,8 +16,25 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . -"""Core that may be raised by this API implementation.""" -__all__ = ["HikariError"] +"""Core errors that may be raised by this API implementation.""" +__all__ = [ + "HikariError", + "NotFoundHTTPError", + "UnauthorizedHTTPError", + "BadRequestHTTPError", + "ClientHTTPError", + "ServerHTTPError", + "CodedHTTPError", + "HTTPError", + "GatewayZombiedError", + "GatewayNeedsShardingError", + "GatewayMustReconnectError", + "GatewayInvalidSessionError", + "GatewayInvalidTokenError", + "GatewayServerClosedConnectionError", + "GatewayClientClosedError", + "GatewayError", +] import typing diff --git a/hikari/guilds.py b/hikari/guilds.py index a0a71a0b03..933dd71a47 100644 --- a/hikari/guilds.py +++ b/hikari/guilds.py @@ -248,7 +248,7 @@ class GuildRole(PartialGuildRole): #: Whether this role is hoisting the members it's attached to in the member #: list, members will be hoisted under their highest role where - #: :attr:`hoisted` is true. + #: :attr:`is_hoisted` is true. #: #: :type: :obj:`bool` is_hoisted: bool = marshaller.attrib(raw_name="hoist", deserializer=bool) @@ -536,7 +536,7 @@ class PresenceUser(users.User): Warnings -------- - Every attribute except :attr:`id` may be received as :obj:`hikari.entities.UNSET` + Every attribute except ``id`` may be received as :obj:`hikari.entities.UNSET` unless it is specifically being modified for this update. """ @@ -716,7 +716,7 @@ class GuildIntegration(snowflakes.UniqueEntity, entities.Deserializable): #: The user this integration belongs to. #: - #: :type: :obj:`users.User` + #: :type: :obj:`hikari.users.User` user: users.User = marshaller.attrib(deserializer=users.User.deserialize) #: The datetime of when this integration's subscribers were last synced. @@ -820,10 +820,9 @@ class Guild(PartialGuild): Note ---- If a guild object is considered to be unavailable, then the state of any - other fields other than the :attr:`is_unavailable` and - :obj:`hikari.entities.UniqueEntity.id` members outdated, or incorrect. - If a guild is unavailable, then the contents of any other fields should be - ignored. + other fields other than the :attr:`is_unavailable` and ``id`` are outdated + or incorrect. If a guild is unavailable, then the contents of any other + fields should be ignored. """ #: The hash of the splash for the guild, if there is one. @@ -874,8 +873,10 @@ class Guild(PartialGuild): ) # TODO: document when this is not specified. - #: Defines if the guild embed is enabled or not. This information may not - #: be present, in which case, it will be ``None`` instead. + #: Defines if the guild embed is enabled or not. + #: + #: This information may not be present, in which case, + #: it will be ``None`` instead. #: #: :type: :obj:`bool`, optional is_embed_enabled: typing.Optional[bool] = marshaller.attrib( @@ -883,8 +884,9 @@ class Guild(PartialGuild): ) #: The channel ID that the guild embed will generate an invite to, if - #: enabled for this guild. Will be ``None`` if invites are disable for this - #: guild's embed. + #: enabled for this guild. + #: + #: Will be ``None`` if invites are disabled for this guild's embed. #: #: :type: :obj:`hikari.snowflakes.Snowflake`, optional embed_channel_id: typing.Optional[snowflakes.Snowflake] = marshaller.attrib( @@ -946,6 +948,8 @@ class Guild(PartialGuild): #: #: An unavailable guild cannot be interacted with, and most information may #: be outdated if that is the case. + #: + #: :type: :obj:`bool`, optional is_unavailable: typing.Optional[bool] = marshaller.attrib( raw_name="unavailable", if_undefined=None, deserializer=bool ) @@ -982,8 +986,9 @@ class Guild(PartialGuild): system_channel_flags: GuildSystemChannelFlag = marshaller.attrib(deserializer=GuildSystemChannelFlag) #: The ID of the channel where guilds with the :obj:`GuildFeature.PUBLIC` - #: :attr:`features` display rules and guidelines. If the - #: :obj:`GuildFeature.PUBLIC` feature is not defined, then this is ``None``. + #: ``features`` display rules and guidelines. + #: + #: If the :obj:`GuildFeature.PUBLIC` feature is not defined, then this is ``None``. #: #: :type: :obj:`hikari.snowflakes.Snowflake`, optional rules_channel_id: typing.Optional[snowflakes.Snowflake] = marshaller.attrib( @@ -1087,8 +1092,9 @@ class Guild(PartialGuild): if_undefined=None, ) - #: The maximum number of presences for the guild. If this is ``None``, then - #: the default value is used (currently 5000). + #: The maximum number of presences for the guild. + #: + #: If this is ``None``, then the default value is used (currently 5000). #: #: :type: :obj:`int`, optional max_presences: typing.Optional[int] = marshaller.attrib(if_none=None, if_undefined=None, deserializer=int) @@ -1101,24 +1107,25 @@ class Guild(PartialGuild): max_members: typing.Optional[int] = marshaller.attrib(if_undefined=None, deserializer=int) #: The vanity URL code for the guild's vanity URL. + #: #: This is only present if :obj:`GuildFeature.VANITY_URL` is in the - #: :attr:`features` for this guild. If not, this will always be ``None``. + #: ``features`` for this guild. If not, this will always be ``None``. #: #: :type: :obj:`str`, optional vanity_url_code: typing.Optional[str] = marshaller.attrib(if_none=None, deserializer=str) #: The guild's description. #: - #: This is only present if certain :obj:`Guild.features` are set in this - #: guild. Otherwise, this will always be ``None``. For all other purposes, - #: it is ``None``. + #: This is only present if certain :obj:`GuildFeature`'s are set in the + #: ``features`` for this guild. Otherwise, this will always be ``None``. #: #: :type: :obj:`str`, optional description: typing.Optional[str] = marshaller.attrib(if_none=None, deserializer=str) #: The hash for the guild's banner. + #: #: This is only present if the guild has :obj:`GuildFeature.BANNER` in the - #: :obj:`Guild.features` for this guild. For all other purposes, it is + #: ``features`` for this guild. For all other purposes, it is # ``None``. #: #: :type: :obj:`str`, optional @@ -1129,16 +1136,17 @@ class Guild(PartialGuild): #: :type: :obj:`GuildPremiumTier` premium_tier: GuildPremiumTier = marshaller.attrib(deserializer=GuildPremiumTier) - #: The number of nitro boosts that the server currently has. This - #: information may not be present, in which case, it will be ``None``. + #: The number of nitro boosts that the server currently has. + #: + #: This information may not be present, in which case, it will be ``None``. #: #: :type: :obj:`int`, optional premium_subscription_count: typing.Optional[int] = marshaller.attrib(if_undefined=None, deserializer=int) #: The preferred locale to use for this guild. #: - #: This can only be change if :obj:`GuildFeatures.PUBLIC` is in the - #: :attr:`features` for this guild and will otherwise default to ``en-US```. + #: This can only be change if :obj:`GuildFeature.PUBLIC` is in the + #: ``features`` for this guild and will otherwise default to ``en-US```. #: #: :type: :obj:`str` preferred_locale: str = marshaller.attrib(deserializer=str) @@ -1146,8 +1154,8 @@ class Guild(PartialGuild): #: The channel ID of the channel where admins and moderators receive notices #: from Discord. #: - #: This is only present if :obj:`GuildFeatures.PUBLIC` is in the - #: :attr:`features` for this guild. For all other purposes, it should be + #: This is only present if :obj:`GuildFeature.PUBLIC` is in the + #: ``features`` for this guild. For all other purposes, it should be #: considered to be ``None``. #: #: :type: :obj:`hikari.snowflakes.Snowflake`, optional diff --git a/hikari/internal/marshaller.py b/hikari/internal/marshaller.py index a60912f175..f4b594b3e2 100644 --- a/hikari/internal/marshaller.py +++ b/hikari/internal/marshaller.py @@ -80,7 +80,7 @@ def dereference_handle(handle_string: str) -> typing.Any: ``"collections#deque"``: Refers to :obj:`collections.deque` ``"asyncio.tasks#Task"``: - Refers to :obj:`asyncio.tasks.Task` + Refers to ``asyncio.tasks.Task`` ``"hikari.net"``: Refers to :obj:`hikari.net` ``"foo.bar#baz.bork.qux"``: @@ -293,7 +293,7 @@ class HikariEntityMarshaller: """A global marshaller helper that helps deserialize and serialize any internal components. It can deserialize and serialize any internal componentsthat that are - decorated with the :obj:`attrs` decorator, and that are :mod:`attr` + decorated with the :obj:`attrs` decorator, and that are :obj:`attr.s` classes using fields with the :obj:`attrib` function call descriptor. """ @@ -317,7 +317,7 @@ def register(self, cls: typing.Type[EntityT]) -> typing.Type[EntityT]: Raises ------ :obj:`TypeError` - If the class is not an :mod:`attrs` class. + If the class is not an :obj:`attr.s` class. """ entity_descriptor = _construct_entity_descriptor(cls) self._registered_entities[cls] = entity_descriptor @@ -344,7 +344,7 @@ def deserialize(self, raw_data: typing.Mapping[str, typing.Any], target_type: ty If the entity is not registered. :obj:`AttributeError` If the field is not optional, but the field was not present in the - raw payload, or it was present, but it was assigned `None`. + raw payload, or it was present, but it was assigned ``None``. :obj:`TypeError` If the deserialization call failed for some reason. """ @@ -462,12 +462,13 @@ def attrs(**kwargs): Returns ------- - A decorator to decorate a class with. + ``decorator(T) -> T`` + A decorator to decorate a class with. Raises ------ :obj:`ValueError` - If you attempt to use the `auto_attribs` feature provided by + If you attempt to use the ``auto_attribs`` feature provided by :obj:`attr.s`. Example diff --git a/hikari/invites.py b/hikari/invites.py index 51e6a1191f..599d235339 100644 --- a/hikari/invites.py +++ b/hikari/invites.py @@ -66,7 +66,7 @@ class InviteGuild(guilds.PartialGuild): #: The hash for the guild's banner. #: - #: This is only present if :obj:`hikari.guild.GuildFeature.BANNER` + #: This is only present if :obj:`hikari.guilds.GuildFeature.BANNER` #: is in the ``features`` for this guild. For all other purposes, it is ``None``. #: #: :type: :obj:`str`, optional diff --git a/hikari/net/ratelimits.py b/hikari/net/ratelimits.py index d869937a19..63a8440230 100644 --- a/hikari/net/ratelimits.py +++ b/hikari/net/ratelimits.py @@ -133,11 +133,11 @@ a :obj:`float` containing the number of seconds since 1st January 1970 at 0:00:00 UTC at which the current ratelimit window resets. This should be parsed to a :obj:`datetime.datetime` using - :func:`datetime.datetime.fromtimestamp`, passing - :obj:`datetime.timezone.utc` as a second parameter. + :meth:`datetime.datetime.fromtimestamp`, passing :obj:`datetime.timezone.utc` + as ``tz``. Each of the above values should be passed to the -:meth:`update_rate_limits` method to ensure that the bucket you acquired time +``update_rate_limits`` method to ensure that the bucket you acquired time from is correctly updated should Discord decide to alter their ratelimits on the fly without warning (including timings and the bucket). @@ -354,8 +354,8 @@ def throttle(self, retry_after: float) -> None: (it will not await it to finish) When the :meth:`unlock_later` coroutine function completes, it should be - expected to set the :attr:`throttle_task` to ``None``. This means you can - check if throttling is occurring by checking if :attr:`throttle_task` + expected to set the `throttle_task`` to ``None``. This means you can + check if throttling is occurring by checking if ``throttle_task`` is not ``None``. If this is invoked while another throttle is in progress, that one is @@ -383,8 +383,8 @@ async def unlock_later(self, retry_after: float) -> None: instead. When the :meth:`unlock_later` coroutine function completes, it should be - expected to set the :attr:`throttle_task` to ``None``. This means you can - check if throttling is occurring by checking if :attr:`throttle_task` + expected to set the ``throttle_task`` to ``None``. This means you can + check if throttling is occurring by checking if ``throttle_task`` is not ``None``. """ self.logger.warning("you are being globally rate limited for %ss", retry_after) @@ -549,11 +549,11 @@ async def throttle(self) -> None: ---- You should usually not need to invoke this directly, but if you do, ensure to call it using :func:`asyncio.create_task`, and store the - task immediately in :attr:`throttle_task`. + task immediately in ``throttle_task``. When this coroutine function completes, it will set the - :attr:`throttle_task` to ``None``. This means you can check if throttling - is occurring by checking if :attr:`throttle_task` is not ``None``. + ``throttle_task`` to ``None``. This means you can check if throttling + is occurring by checking if ``throttle_task`` is not ``None``. """ self.logger.debug( "you are being rate limited on bucket %s, backing off for %ss", @@ -642,7 +642,7 @@ def update_rate_limit(self, remaining: int, limit: int, reset_at: float) -> None Note ---- - The :attr:`reset_at` epoch is expected to be a :func:`time.perf_counter` + The ``reset_at`` epoch is expected to be a :func:`time.perf_counter` monotonic epoch, rather than a :func:`time.time` date-based epoch. """ self.remaining = remaining diff --git a/hikari/net/rest.py b/hikari/net/rest.py index 508d233db0..276376e270 100644 --- a/hikari/net/rest.py +++ b/hikari/net/rest.py @@ -53,7 +53,7 @@ class RestfulClient: Whether to allow redirects or not. connector: :obj:`aiohttp.BaseConnector`, optional Optional aiohttp connector info for making an HTTP connection - proxy_headers: :obj:`aiohttp.typedefs.LooseHeaders`, optional + proxy_headers: :obj:`typing.Mapping` [ :obj:`str`, :obj:`str` ], optional Optional proxy headers to pass to HTTP requests. proxy_auth: :obj:`aiohttp.BasicAuth`, optional Optional authorization to be used if using a proxy. @@ -67,10 +67,10 @@ class RestfulClient: SSL certificates. timeout: :obj:`float`, optional The optional timeout for all HTTP requests. - json_deserialize: deserialization function + json_deserialize: ``deserialization function`` A custom JSON deserializer function to use. Defaults to :func:`json.loads`. - json_serialize: serialization function + json_serialize: ``serialization function`` A custom JSON serializer function to use. Defaults to :func:`json.dumps`. token: :obj:`string`, optional @@ -146,7 +146,7 @@ def __init__( #: Optional proxy headers to pass. #: - #: :type: :obj:`aiohttp.typedefs.LooseHeaders` + #: :type: :obj:`typing.Mapping` [ :obj:`str`, :obj:`str` ] self.proxy_headers = proxy_headers #: Optional SSL context to use. @@ -673,7 +673,7 @@ async def create_message( and can usually be ignored. tts : :obj:`bool` If specified, whether the message will be sent as a TTS message. - files : :obj:`typing.Sequence` [ :obj:`typing.Tuple` [ :obj:`str`, :obj:`storage.FileLikeT` ] ] + files : :obj:`typing.Sequence` [ :obj:`typing.Tuple` [ :obj:`str`, :obj:`io.IOBase` ] ] If specified, this should be a list of between ``1`` and ``5`` tuples. Each tuple should consist of the file name, and either raw :obj:`bytes` or an :obj:`io.IOBase` derived object with @@ -793,9 +793,9 @@ async def delete_all_reactions_for_emoji(self, channel_id: str, message_id: str, Raises ------ - :obj:`hikari.errors.NotFoundError` + :obj:`hikari.errors.NotFoundHTTPError` If the channel or message or emoji or user is not found. - :obj:`hikari.errors.ForbiddenError` + :obj:`hikari.errors.ForbiddenHTTPError` If you lack the ``MANAGE_MESSAGES`` permission, or are in DMs. """ route = routes.REACTION_EMOJI.compile(self.DELETE, channel_id=channel_id, message_id=message_id, emoji=emoji) @@ -3045,7 +3045,7 @@ async def execute_webhook( wait : :obj:`bool` If specified, whether this request should wait for the webhook to be executed and return the resultant message object. - file : :obj:`typing.Tuple` [ :obj:`str`, :obj:`hikari.internal.conversions.FileLikeT` ] + file : :obj:`typing.Tuple` [ :obj:`str`, :obj:`io.IOBase` ] If specified, a tuple of the file name and either raw :obj:`bytes` or a :obj:`io.IOBase` derived object that points to a buffer containing said file. diff --git a/hikari/net/shard.py b/hikari/net/shard.py index 5e4b0d9830..ecafa2a2a4 100644 --- a/hikari/net/shard.py +++ b/hikari/net/shard.py @@ -108,7 +108,7 @@ class ShardConnection: information to use when debugging this library or extending it. This includes logging every payload that is sent or received to the logger as debug entries. Generally it is best to keep this disabled. - dispatch: dispatch function + dispatch: ``dispatch function`` The function to invoke with any dispatched events. This must not be a coroutine function, and must take three arguments only. The first is the reference to this :obj:`ShardConnection` The second is the @@ -121,25 +121,24 @@ class ShardConnection: intents: :obj:`hikari.net.codes.GatewayIntent`, optional Bitfield of intents to use. If you use the V7 API, this is mandatory. This field will determine what events you will receive. - json_deserialize: deserialization function + json_deserialize: ``deserialization function`` A custom JSON deserializer function to use. Defaults to :func:`json.loads`. - json_serialize: serialization function + json_serialize: ``serialization function`` A custom JSON serializer function to use. Defaults to :func:`json.dumps`. large_threshold: :obj:`int` The number of members that have to be in a guild for it to be considered to be "large". Large guilds will not have member information sent automatically, and must manually request that member chunks be - sent using :meth:`request_member_chunks`. + sent using :meth:`request_guild_members`. proxy_auth: :obj:`aiohttp.BasicAuth`, optional Optional :obj:`aiohttp.BasicAuth` object that can be provided to allow authenticating with a proxy if you use one. Leave ``None`` to ignore. - proxy_headers: :obj:`aiohttp.typedefs.LooseHeaders`, optional - Optional :obj:`aiohttp.typedefs.LooseHeaders` to provide as headers - to allow the connection through a proxy if you use one. Leave ``None`` - to ignore. + proxy_headers: :obj:`typing.Mapping` [ :obj:`str`, :obj:`str` ], optional + Optional :obj:`typing.Mapping` to provide as headers to allow the + connection through a proxy if you use one. Leave ``None`` to ignore. proxy_url: :obj:`str`, optional Optional :obj:`str` to use for a proxy server. If ``None``, then it is ignored. diff --git a/hikari/oauth2.py b/hikari/oauth2.py index 7acbe9d968..21ee809a1d 100644 --- a/hikari/oauth2.py +++ b/hikari/oauth2.py @@ -75,7 +75,7 @@ class OwnConnection(entities.HikariEntity, entities.Deserializable): #: A sequence of the partial guild integration objects this connection has. #: - #: :type: :obj:`typing.Sequence` [ :obj:`guilds.PartialGuildIntegration` ] + #: :type: :obj:`typing.Sequence` [ :obj:`hikari.guilds.PartialGuildIntegration` ] integrations: typing.Sequence[guilds.PartialGuildIntegration] = marshaller.attrib( deserializer=lambda payload: [ guilds.PartialGuildIntegration.deserialize(integration) for integration in payload @@ -155,7 +155,7 @@ class TeamMember(entities.HikariEntity, entities.Deserializable): #: The user object of this team member. #: - #: :type: :obj:`TeamUser` + #: :type: :obj:`hikari.users.User` user: users.User = marshaller.attrib(deserializer=users.User.deserialize) diff --git a/hikari/state/event_dispatchers.py b/hikari/state/event_dispatchers.py index 3a6e85dd20..751241d6e7 100644 --- a/hikari/state/event_dispatchers.py +++ b/hikari/state/event_dispatchers.py @@ -126,7 +126,7 @@ def on(self, event_type: typing.Type[EventT]) -> typing.Callable[[EventCallbackT Returns ------- - coroutine function decorator: + ``decorator(T) -> T`` A decorator for a coroutine function that registers the given event. """ diff --git a/hikari/state/event_managers.py b/hikari/state/event_managers.py index 6d3bd754cb..c1c6589096 100644 --- a/hikari/state/event_managers.py +++ b/hikari/state/event_managers.py @@ -46,7 +46,7 @@ def raw_event_mapper(name: str) -> typing.Callable[[EventConsumerT], EventConsum Returns ------- - ``decorator(callable) -> callable`` + ``decorator(T) -> T`` A decorator for a method. """ @@ -76,7 +76,7 @@ class EventManager(typing.Generic[EventDispatcherT], raw_event_consumers.RawEven """Abstract definition of the components for an event system for a bot. The class itself inherits from - :obj:`hikari.state.raw_event_consumer.RawEventConsumer` (which allows + :obj:`hikari.state.raw_event_consumers.RawEventConsumer` (which allows it to provide the ability to transform a raw payload into an event object). This is designed as a basis to enable transformation of raw incoming events @@ -86,7 +86,7 @@ class EventManager(typing.Generic[EventDispatcherT], raw_event_consumers.RawEven Parameters ---------- - event_dispatcher_impl: :obj:`hikari.state.event_dispatcher.EventDispatcher`, optional + event_dispatcher_impl: :obj:`hikari.state.event_dispatchers.EventDispatcher`, optional An implementation of event dispatcher that will store individual events and manage dispatching them after this object creates them. If ``None``, then a default implementation is chosen. @@ -98,10 +98,10 @@ class EventManager(typing.Generic[EventDispatcherT], raw_event_consumers.RawEven These methods are expected to have the following parameters: - shard_obj: :obj:`hikari.clients.shard_client.ShardClient` - The shard client that emitted the event. - payload: :obj:`typing.Any` - The received payload. This is expected to be a JSON-compatible type. + shard_obj: :obj:`hikari.clients.shard_client.ShardClient` + The shard client that emitted the event. + payload: :obj:`typing.Any` + The received payload. This is expected to be a JSON-compatible type. For example, if you want to provide an implementation that can consume and handle ``MESSAGE_CREATE`` events, you can do the following. diff --git a/hikari/state/raw_event_consumers.py b/hikari/state/raw_event_consumers.py index 1cf8e852bd..00c1e77aa7 100644 --- a/hikari/state/raw_event_consumers.py +++ b/hikari/state/raw_event_consumers.py @@ -49,7 +49,7 @@ def process_raw_event( Parameters ---------- - shard_client_obj : :obj:`hikari.clients.ShardClient` + shard_client_obj : :obj:`hikari.clients.shard_client.ShardClient` The client for the shard that received the event. name : :obj:`str` The raw event name. From 0fa7307f573d774bff50f37df8e30396452a019d Mon Sep 17 00:00:00 2001 From: davfsa Date: Tue, 7 Apr 2020 12:48:05 +0200 Subject: [PATCH 079/922] Add tests for gateway_manager --- hikari/clients/gateway_manager.py | 22 +- hikari/clients/gateway_runner.py | 4 +- tests/hikari/clients/test_gateway_manager.py | 204 +++++++++++++++++++ 3 files changed, 216 insertions(+), 14 deletions(-) create mode 100644 tests/hikari/clients/test_gateway_manager.py diff --git a/hikari/clients/gateway_manager.py b/hikari/clients/gateway_manager.py index d5d93eaed3..608f0a734f 100644 --- a/hikari/clients/gateway_manager.py +++ b/hikari/clients/gateway_manager.py @@ -88,7 +88,7 @@ def latency(self) -> float: """ latencies = [] for shard in self.shards.values(): - if shard.connection_state == shard_client.ShardState.READY and not math.isnan(shard.latency): + if not math.isnan(shard.latency): latencies.append(shard.latency) return sum(latencies) / len(latencies) if latencies else float("nan") @@ -151,28 +151,28 @@ async def update_presence( Any arguments that you do not explicitly provide some value for will not be changed. - Warnings - -------- + Warning + ------- This will only apply to connected shards. Notes ----- If you wish to update a presence for a specific shard, you can do this - by using the :attr:`GatewayManager.shards` :obj:`typing.Mapping` to - find the shard you wish to update. + by using the ``shards`` :obj:`typing.Mapping` to find the shard you + wish to update. Parameters ---------- status : :obj:`hikari.guilds.PresenceStatus` - The new status to set. + If specified, the new status to set. activity : :obj:`hikari.gateway_entities.GatewayActivity`, optional - The new activity to set. + If specified, the new activity to set. idle_since : :obj:`datetime.datetime`, optional - The time to show up as being idle since, or ``None`` if not - applicable. + If specified, the time to show up as being idle since, + or ``None`` if not applicable. is_afk : :obj:`bool` - ``True`` if the user should be marked as AFK, or ``False`` - otherwise. + If specified, ``True`` if the user should be marked as AFK, + or ``False`` otherwise. """ await asyncio.gather( *( diff --git a/hikari/clients/gateway_runner.py b/hikari/clients/gateway_runner.py index d9588d122c..de4e34d4fb 100644 --- a/hikari/clients/gateway_runner.py +++ b/hikari/clients/gateway_runner.py @@ -72,9 +72,7 @@ def run_gateway(compression, color, debug, logger, shards, token, url, verify_ss logging.basicConfig(level=logger, format=_color_format if color else _regular_format, stream=sys.stdout) class _DummyConsumer(raw_event_consumers.RawEventConsumer): - def process_raw_event( - self, _client: shard_client.ShardClient, name: str, payload: entities.RawEntityT - ) -> None: + def process_raw_event(self, _client: shard_client.ShardClient, name: str, payload: entities.RawEntityT) -> None: logging.debug("dispatched %s with body [%-100.100s]", name, payload) client = gateway_manager.GatewayManager( diff --git a/tests/hikari/clients/test_gateway_manager.py b/tests/hikari/clients/test_gateway_manager.py new file mode 100644 index 0000000000..d91c1988fb --- /dev/null +++ b/tests/hikari/clients/test_gateway_manager.py @@ -0,0 +1,204 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# Copyright © Nekoka.tt 2019-2020 +# +# This file is part of Hikari. +# +# Hikari is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Hikari is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along ith Hikari. If not, see . +import math + +import cymock as mock +import pytest + +from hikari.clients import gateway_manager +from hikari.clients import shard_client +from tests.hikari import _helpers + + +class TestGatewayManager: + def test_latency(self): + shard1 = mock.MagicMock(shard_client.ShardClient, latency=20) + shard2 = mock.MagicMock(shard_client.ShardClient, latency=30) + shard3 = mock.MagicMock(shard_client.ShardClient, latency=40) + + with mock.patch("hikari.clients.shard_client.ShardClient", side_effect=[shard1, shard2, shard3]): + gateway_manager_obj = gateway_manager.GatewayManager( + shard_ids=[1, 2, 3], + shard_count=3, + config=None, + url="some_url", + raw_event_consumer_impl=None, + shard_type=shard_client.ShardClient, + ) + + assert gateway_manager_obj.latency == 30 + + def test_latency_doesnt_take_into_a_count_shards_with_no_latency(self): + shard1 = mock.MagicMock(shard_client.ShardClient, latency=20) + shard2 = mock.MagicMock(shard_client.ShardClient, latency=30) + shard3 = mock.MagicMock(shard_client.ShardClient, latency=float("nan")) + + with mock.patch("hikari.clients.shard_client.ShardClient", side_effect=[shard1, shard2, shard3]): + gateway_manager_obj = gateway_manager.GatewayManager( + shard_ids=[1, 2, 3], + shard_count=3, + config=None, + url="some_url", + raw_event_consumer_impl=None, + shard_type=shard_client.ShardClient, + ) + + assert gateway_manager_obj.latency == 25 + + def test_latency_returns_nan_if_all_shards_have_no_latency(self): + shard1 = mock.MagicMock(shard_client.ShardClient, latency=float("nan")) + shard2 = mock.MagicMock(shard_client.ShardClient, latency=float("nan")) + shard3 = mock.MagicMock(shard_client.ShardClient, latency=float("nan")) + + with mock.patch("hikari.clients.shard_client.ShardClient", side_effect=[shard1, shard2, shard3]): + gateway_manager_obj = gateway_manager.GatewayManager( + shard_ids=[1, 2, 3], + shard_count=3, + config=None, + url="some_url", + raw_event_consumer_impl=None, + shard_type=shard_client.ShardClient, + ) + + assert math.isnan(gateway_manager_obj.latency) + + @pytest.mark.asyncio + async def test_start_waits_five_seconds_between_shard_startup(self): + shard1 = mock.MagicMock(shard_client.ShardClient, start=mock.AsyncMock()) + shard2 = mock.MagicMock(shard_client.ShardClient, start=mock.AsyncMock()) + shard3 = mock.MagicMock(shard_client.ShardClient, start=mock.AsyncMock()) + mock_sleep = mock.AsyncMock() + + with mock.patch("hikari.clients.shard_client.ShardClient", side_effect=[shard1, shard2, shard3]): + with mock.patch("asyncio.sleep", wraps=mock_sleep): + gateway_manager_obj = gateway_manager.GatewayManager( + shard_ids=[1, 2, 3], + shard_count=3, + config=None, + url="some_url", + raw_event_consumer_impl=None, + shard_type=shard_client.ShardClient, + ) + await gateway_manager_obj.start() + + assert len(mock_sleep.mock_calls) == 2 + mock_sleep.assert_awaited_with(5) + shard1.start.assert_awaited_once() + shard2.start.assert_awaited_once() + shard3.start.assert_awaited_once() + + @pytest.mark.asyncio + async def test_join_calls_join_on_all_shards(self): + shard1 = mock.MagicMock(shard_client.ShardClient, join=mock.MagicMock()) + shard2 = mock.MagicMock(shard_client.ShardClient, join=mock.MagicMock()) + shard3 = mock.MagicMock(shard_client.ShardClient, join=mock.MagicMock()) + + with mock.patch("hikari.clients.shard_client.ShardClient", side_effect=[shard1, shard2, shard3]): + with mock.patch("asyncio.gather", return_value=_helpers.AwaitableMock()): + gateway_manager_obj = gateway_manager.GatewayManager( + shard_ids=[1, 2, 3], + shard_count=3, + config=None, + url="some_url", + raw_event_consumer_impl=None, + shard_type=shard_client.ShardClient, + ) + await gateway_manager_obj.join() + + shard1.join.assert_called_once() + shard2.join.assert_called_once() + shard3.join.assert_called_once() + + @pytest.mark.asyncio + async def test_close_closes_all_shards(self): + shard1 = mock.MagicMock(shard_client.ShardClient, close=mock.MagicMock()) + shard2 = mock.MagicMock(shard_client.ShardClient, close=mock.MagicMock()) + shard3 = mock.MagicMock(shard_client.ShardClient, close=mock.MagicMock()) + + with mock.patch("hikari.clients.shard_client.ShardClient", side_effect=[shard1, shard2, shard3]): + with mock.patch("asyncio.gather", return_value=_helpers.AwaitableMock()): + gateway_manager_obj = gateway_manager.GatewayManager( + shard_ids=[1, 2, 3], + shard_count=3, + config=None, + url="some_url", + raw_event_consumer_impl=None, + shard_type=shard_client.ShardClient, + ) + gateway_manager_obj._is_running = True + await gateway_manager_obj.close(wait=False) + + shard1.close.assert_called_once_with(False) + shard2.close.assert_called_once_with(False) + shard3.close.assert_called_once_with(False) + + @pytest.mark.asyncio + async def test_close_does_nothing_if_not_running(self): + shard1 = mock.MagicMock(shard_client.ShardClient, close=mock.MagicMock()) + shard2 = mock.MagicMock(shard_client.ShardClient, close=mock.MagicMock()) + shard3 = mock.MagicMock(shard_client.ShardClient, close=mock.MagicMock()) + + with mock.patch("hikari.clients.shard_client.ShardClient", side_effect=[shard1, shard2, shard3]): + with mock.patch("asyncio.gather", return_value=_helpers.AwaitableMock()): + gateway_manager_obj = gateway_manager.GatewayManager( + shard_ids=[1, 2, 3], + shard_count=3, + config=None, + url="some_url", + raw_event_consumer_impl=None, + shard_type=shard_client.ShardClient, + ) + gateway_manager_obj._is_running = False + await gateway_manager_obj.close() + + shard1.close.assert_not_called() + shard2.close.assert_not_called() + shard3.close.assert_not_called() + + @pytest.mark.asyncio + async def test_update_presence_updates_presence_in_all_ready_or_waiting_for_ready_shards(self): + shard1 = mock.MagicMock( + shard_client.ShardClient, update_presence=mock.MagicMock(), connection_state=shard_client.ShardState.READY, + ) + shard2 = mock.MagicMock( + shard_client.ShardClient, + update_presence=mock.MagicMock(), + connection_state=shard_client.ShardState.WAITING_FOR_READY, + ) + shard3 = mock.MagicMock( + shard_client.ShardClient, + update_presence=mock.MagicMock(), + connection_state=shard_client.ShardState.CONNECTING, + ) + + with mock.patch("hikari.clients.shard_client.ShardClient", side_effect=[shard1, shard2, shard3]): + with mock.patch("asyncio.gather", return_value=_helpers.AwaitableMock()): + gateway_manager_obj = gateway_manager.GatewayManager( + shard_ids=[1, 2, 3], + shard_count=3, + config=None, + url="some_url", + raw_event_consumer_impl=None, + shard_type=shard_client.ShardClient, + ) + await gateway_manager_obj.update_presence(status=None, activity=None, idle_since=None, is_afk=True) + + shard1.update_presence.assert_called_once_with(status=None, activity=None, idle_since=None, is_afk=True) + shard2.update_presence.assert_called_once_with(status=None, activity=None, idle_since=None, is_afk=True) + shard3.update_presence.assert_not_called() From d18f27452a5526327122636a14fdb8f4027dbe1b Mon Sep 17 00:00:00 2001 From: davfsa Date: Wed, 8 Apr 2020 08:34:40 +0200 Subject: [PATCH 080/922] Improved gateway_manager tests --- tests/hikari/clients/test_gateway_manager.py | 57 ++++++++++++-------- 1 file changed, 36 insertions(+), 21 deletions(-) diff --git a/tests/hikari/clients/test_gateway_manager.py b/tests/hikari/clients/test_gateway_manager.py index d91c1988fb..02bd260c57 100644 --- a/tests/hikari/clients/test_gateway_manager.py +++ b/tests/hikari/clients/test_gateway_manager.py @@ -34,7 +34,7 @@ def test_latency(self): with mock.patch("hikari.clients.shard_client.ShardClient", side_effect=[shard1, shard2, shard3]): gateway_manager_obj = gateway_manager.GatewayManager( - shard_ids=[1, 2, 3], + shard_ids=[0, 1, 2], shard_count=3, config=None, url="some_url", @@ -51,7 +51,7 @@ def test_latency_doesnt_take_into_a_count_shards_with_no_latency(self): with mock.patch("hikari.clients.shard_client.ShardClient", side_effect=[shard1, shard2, shard3]): gateway_manager_obj = gateway_manager.GatewayManager( - shard_ids=[1, 2, 3], + shard_ids=[0, 1, 2], shard_count=3, config=None, url="some_url", @@ -68,7 +68,7 @@ def test_latency_returns_nan_if_all_shards_have_no_latency(self): with mock.patch("hikari.clients.shard_client.ShardClient", side_effect=[shard1, shard2, shard3]): gateway_manager_obj = gateway_manager.GatewayManager( - shard_ids=[1, 2, 3], + shard_ids=[0, 1, 2], shard_count=3, config=None, url="some_url", @@ -80,15 +80,30 @@ def test_latency_returns_nan_if_all_shards_have_no_latency(self): @pytest.mark.asyncio async def test_start_waits_five_seconds_between_shard_startup(self): - shard1 = mock.MagicMock(shard_client.ShardClient, start=mock.AsyncMock()) - shard2 = mock.MagicMock(shard_client.ShardClient, start=mock.AsyncMock()) - shard3 = mock.MagicMock(shard_client.ShardClient, start=mock.AsyncMock()) mock_sleep = mock.AsyncMock() + class MockStart(mock.AsyncMock): + def __init__(self, condition): + super().__init__() + self.condition = condition + + def __call__(self): + if self.condition: + mock_sleep.assert_called_once_with(5) + mock_sleep.reset_mock() + else: + mock_sleep.assert_not_called() + + return super().__call__() + + shard1 = mock.MagicMock(shard_client.ShardClient, start=MockStart(condition=False)) + shard2 = mock.MagicMock(shard_client.ShardClient, start=MockStart(condition=True)) + shard3 = mock.MagicMock(shard_client.ShardClient, start=MockStart(condition=True)) + with mock.patch("hikari.clients.shard_client.ShardClient", side_effect=[shard1, shard2, shard3]): with mock.patch("asyncio.sleep", wraps=mock_sleep): gateway_manager_obj = gateway_manager.GatewayManager( - shard_ids=[1, 2, 3], + shard_ids=[0, 1, 2], shard_count=3, config=None, url="some_url", @@ -96,12 +111,11 @@ async def test_start_waits_five_seconds_between_shard_startup(self): shard_type=shard_client.ShardClient, ) await gateway_manager_obj.start() + mock_sleep.assert_not_called() - assert len(mock_sleep.mock_calls) == 2 - mock_sleep.assert_awaited_with(5) - shard1.start.assert_awaited_once() - shard2.start.assert_awaited_once() - shard3.start.assert_awaited_once() + shard1.start.assert_called_once() + shard2.start.assert_called_once() + shard3.start.assert_called_once() @pytest.mark.asyncio async def test_join_calls_join_on_all_shards(self): @@ -112,7 +126,7 @@ async def test_join_calls_join_on_all_shards(self): with mock.patch("hikari.clients.shard_client.ShardClient", side_effect=[shard1, shard2, shard3]): with mock.patch("asyncio.gather", return_value=_helpers.AwaitableMock()): gateway_manager_obj = gateway_manager.GatewayManager( - shard_ids=[1, 2, 3], + shard_ids=[0, 1, 2], shard_count=3, config=None, url="some_url", @@ -126,7 +140,8 @@ async def test_join_calls_join_on_all_shards(self): shard3.join.assert_called_once() @pytest.mark.asyncio - async def test_close_closes_all_shards(self): + @pytest.mark.parametrize("wait", [True, False]) + async def test_close_closes_all_shards(self, wait): shard1 = mock.MagicMock(shard_client.ShardClient, close=mock.MagicMock()) shard2 = mock.MagicMock(shard_client.ShardClient, close=mock.MagicMock()) shard3 = mock.MagicMock(shard_client.ShardClient, close=mock.MagicMock()) @@ -134,7 +149,7 @@ async def test_close_closes_all_shards(self): with mock.patch("hikari.clients.shard_client.ShardClient", side_effect=[shard1, shard2, shard3]): with mock.patch("asyncio.gather", return_value=_helpers.AwaitableMock()): gateway_manager_obj = gateway_manager.GatewayManager( - shard_ids=[1, 2, 3], + shard_ids=[0, 1, 2], shard_count=3, config=None, url="some_url", @@ -142,11 +157,11 @@ async def test_close_closes_all_shards(self): shard_type=shard_client.ShardClient, ) gateway_manager_obj._is_running = True - await gateway_manager_obj.close(wait=False) + await gateway_manager_obj.close(wait=wait) - shard1.close.assert_called_once_with(False) - shard2.close.assert_called_once_with(False) - shard3.close.assert_called_once_with(False) + shard1.close.assert_called_once_with(wait) + shard2.close.assert_called_once_with(wait) + shard3.close.assert_called_once_with(wait) @pytest.mark.asyncio async def test_close_does_nothing_if_not_running(self): @@ -157,7 +172,7 @@ async def test_close_does_nothing_if_not_running(self): with mock.patch("hikari.clients.shard_client.ShardClient", side_effect=[shard1, shard2, shard3]): with mock.patch("asyncio.gather", return_value=_helpers.AwaitableMock()): gateway_manager_obj = gateway_manager.GatewayManager( - shard_ids=[1, 2, 3], + shard_ids=[0, 1, 2], shard_count=3, config=None, url="some_url", @@ -190,7 +205,7 @@ async def test_update_presence_updates_presence_in_all_ready_or_waiting_for_read with mock.patch("hikari.clients.shard_client.ShardClient", side_effect=[shard1, shard2, shard3]): with mock.patch("asyncio.gather", return_value=_helpers.AwaitableMock()): gateway_manager_obj = gateway_manager.GatewayManager( - shard_ids=[1, 2, 3], + shard_ids=[0, 1, 2], shard_count=3, config=None, url="some_url", From afb3f6d57996c829b10e9f0bcd1bfba5642c101e Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Fri, 10 Apr 2020 14:17:58 +0100 Subject: [PATCH 081/922] Fixed some docstrings [skip ci] --- docs/index.rst | 1 + hikari/clients/bot_client.py | 5 ++--- hikari/clients/shard_client.py | 3 --- 3 files changed, 3 insertions(+), 6 deletions(-) diff --git a/docs/index.rst b/docs/index.rst index 2523984ad6..f6c89e05d9 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -38,6 +38,7 @@ Other resources * `Source code `_ * `Pipelines and builds `_ * `CI success statistics for nerds `_ +* `Discord API documentation `_ Search for a topic ------------------ diff --git a/hikari/clients/bot_client.py b/hikari/clients/bot_client.py index 5034673956..d0d1c99b24 100644 --- a/hikari/clients/bot_client.py +++ b/hikari/clients/bot_client.py @@ -44,9 +44,8 @@ class BotBase(runnable.RunnableClient, event_dispatchers.EventDispatcher): ---------- config : :obj:`hikari.clients.configs.BotConfig` The config object to use. - event_manager : ``EventManagerT`` - The event manager to use. This must be a subclass of - :obj:`hikari.state.event_managers.EventManager` + event_manager : ``hikari.state.event_managers.EventManager`` + The event manager to use. """ #: The config for this bot. diff --git a/hikari/clients/shard_client.py b/hikari/clients/shard_client.py index a618a981b5..c189de0bc8 100644 --- a/hikari/clients/shard_client.py +++ b/hikari/clients/shard_client.py @@ -36,7 +36,6 @@ import aiohttp from hikari import errors -from hikari import events from hikari import gateway_entities from hikari import guilds from hikari.clients import configs @@ -48,8 +47,6 @@ from hikari.net import shard from hikari.state import raw_event_consumers -_EventT = typing.TypeVar("_EventT", bound=events.HikariEvent) - @enum.unique class ShardState(enum.IntEnum): From a628189fe4cb12670e9d7719ee6cdc05913d2975 Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Fri, 10 Apr 2020 12:44:48 +0100 Subject: [PATCH 082/922] Type hint event support, renaming modules for consistency, handling startup/shutdown events. Fixes #275: - Implemented StartingEvent, StartedEvent, StoppingEvent, StoppedEvent for GatewayManager. - Implemented StartingEvent, StartedEvent, StoppingEvent, StoppedEvent for ShardClient. - Fixed circular import caused as a result of this. Fixes #276: - Implemented ability to get event type optionally from just the event type hint on the event object within EventDispatcher.on - Added tests for conversions#pluralize - Added tests for conversions#snoop_typehint_from_scope Fixes #277: - Renames inconsistent client modules to pluralised names. --- hikari/clients/__init__.py | 22 ++-- .../clients/{bot_client.py => bot_clients.py} | 25 ++-- ...gateway_manager.py => gateway_managers.py} | 41 +++--- hikari/clients/gateway_runner.py | 15 +-- .../{rest_client.py => rest_clients.py} | 0 hikari/clients/runnable.py | 4 +- .../{shard_client.py => shard_clients.py} | 112 ++++++++++++----- hikari/events.py | 36 ++++-- hikari/internal/conversions.py | 69 +++++++++++ hikari/state/__init__.py | 8 +- hikari/state/event_dispatchers.py | 99 +++++++++++++-- hikari/state/event_managers.py | 8 +- hikari/state/raw_event_consumers.py | 6 +- hikari/state/stateless_event_managers.py | 13 +- tests/hikari/clients/test_gateway_manager.py | 117 +++++++++--------- tests/hikari/internal/test_conversions.py | 82 ++++++++++++ 16 files changed, 494 insertions(+), 163 deletions(-) rename hikari/clients/{bot_client.py => bot_clients.py} (86%) rename hikari/clients/{gateway_manager.py => gateway_managers.py} (81%) rename hikari/clients/{rest_client.py => rest_clients.py} (100%) rename hikari/clients/{shard_client.py => shard_clients.py} (82%) diff --git a/hikari/clients/__init__.py b/hikari/clients/__init__.py index ea7f1e7025..f7dada76ec 100644 --- a/hikari/clients/__init__.py +++ b/hikari/clients/__init__.py @@ -18,23 +18,23 @@ # along with Hikari. If not, see . """The models API for interacting with Discord directly.""" -from hikari.clients import bot_client +from hikari.clients import bot_clients from hikari.clients import configs -from hikari.clients import gateway_manager -from hikari.clients import rest_client +from hikari.clients import gateway_managers +from hikari.clients import rest_clients from hikari.clients import runnable -from hikari.clients.bot_client import * +from hikari.clients.bot_clients import * from hikari.clients.configs import * -from hikari.clients.gateway_manager import * -from hikari.clients.rest_client import * +from hikari.clients.gateway_managers import * +from hikari.clients.rest_clients import * from hikari.clients.runnable import * -from hikari.clients.shard_client import * +from hikari.clients.shard_clients import * __all__ = [ - *bot_client.__all__, + *bot_clients.__all__, *configs.__all__, - *gateway_manager.__all__, - *rest_client.__all__, - *shard_client.__all__, + *gateway_managers.__all__, + *rest_clients.__all__, + *shard_clients.__all__, *runnable.__all__, ] diff --git a/hikari/clients/bot_client.py b/hikari/clients/bot_clients.py similarity index 86% rename from hikari/clients/bot_client.py rename to hikari/clients/bot_clients.py index d0d1c99b24..87747ed5ed 100644 --- a/hikari/clients/bot_client.py +++ b/hikari/clients/bot_clients.py @@ -26,10 +26,10 @@ from hikari import events from hikari.clients import configs -from hikari.clients import gateway_manager -from hikari.clients import rest_client +from hikari.clients import gateway_managers +from hikari.clients import rest_clients from hikari.clients import runnable -from hikari.clients import shard_client +from hikari.clients import shard_clients from hikari.internal import more_asyncio from hikari.internal import more_logging from hikari.state import event_dispatchers @@ -60,8 +60,8 @@ class BotBase(runnable.RunnableClient, event_dispatchers.EventDispatcher): #: The gateway for this bot. #: - #: :type: :obj:`hikari.clients.gateway_manager.GatewayManager` [ :obj:`hikari.clients.shard_client.ShardClient` ] - gateway: gateway_manager.GatewayManager[shard_client.ShardClient] + #: :type: :obj:`hikari.clients.gateway_managers.GatewayManager` [ :obj:`hikari.clients.shard_clients.ShardClient` ] + gateway: gateway_managers.GatewayManager[shard_clients.ShardClient] #: The logger to use for this bot. #: @@ -70,8 +70,8 @@ class BotBase(runnable.RunnableClient, event_dispatchers.EventDispatcher): #: The REST HTTP client to use for this bot. #: - #: :type: :obj:`hikari.clients.rest_client.RESTClient` - rest: rest_client.RESTClient + #: :type: :obj:`hikari.clients.rest_clients.RESTClient` + rest: rest_clients.RESTClient @abc.abstractmethod def __init__(self, config: configs.BotConfig, event_manager: event_managers.EventManager) -> None: @@ -79,7 +79,7 @@ def __init__(self, config: configs.BotConfig, event_manager: event_managers.Even self.config = config self.event_manager = event_manager self.gateway = NotImplemented - self.rest = rest_client.RESTClient(self.config) + self.rest = rest_clients.RESTClient(self.config) async def start(self): gateway_bot = await self.rest.fetch_gateway_bot() @@ -94,18 +94,19 @@ async def start(self): shard_count = self.config.shard_count if self.config.shard_count else gateway_bot.shard_count shard_ids = self.config.shard_ids if self.config.shard_ids else [*range(shard_count)] - self.gateway = gateway_manager.GatewayManager( + self.gateway = gateway_managers.GatewayManager( config=self.config, url=gateway_bot.url, raw_event_consumer_impl=self.event_manager, shard_ids=shard_ids, shard_count=shard_count, + dispatcher=self.event_manager.event_dispatcher.dispatch_event, ) await self.gateway.start() - async def close(self, wait: bool = True): - await self.gateway.close(wait) + async def close(self): + await self.gateway.close() self.event_manager.event_dispatcher.close() await self.rest.close() @@ -115,7 +116,7 @@ async def join(self) -> None: def add_listener( self, event_type: typing.Type[event_dispatchers.EventT], callback: event_dispatchers.EventCallbackT ) -> event_dispatchers.EventCallbackT: - return self.event_manager.event_dispatcher.remove_listener(event_type, callback) + return self.event_manager.event_dispatcher.add_listener(event_type, callback) def remove_listener( self, event_type: typing.Type[event_dispatchers.EventT], callback: event_dispatchers.EventCallbackT diff --git a/hikari/clients/gateway_manager.py b/hikari/clients/gateway_managers.py similarity index 81% rename from hikari/clients/gateway_manager.py rename to hikari/clients/gateway_managers.py index 608f0a734f..59e8916654 100644 --- a/hikari/clients/gateway_manager.py +++ b/hikari/clients/gateway_managers.py @@ -16,7 +16,7 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . -"""Defines a facade around :obj:`hikari.clients.shard_client.ShardClient`. +"""Defines a facade around :obj:`hikari.clients.shard_clients.ShardClient`. This provides functionality such as keeping multiple shards alive """ @@ -29,16 +29,18 @@ import time import typing +from hikari import events from hikari import gateway_entities from hikari import guilds from hikari.clients import configs from hikari.clients import runnable -from hikari.clients import shard_client +from hikari.clients import shard_clients from hikari.internal import conversions from hikari.internal import more_logging +from hikari.state import event_dispatchers from hikari.state import raw_event_consumers -ShardT = typing.TypeVar("ShardT", bound=shard_client.ShardClient) +ShardT = typing.TypeVar("ShardT", bound=shard_clients.ShardClient) class GatewayManager(typing.Generic[ShardT], runnable.RunnableClient): @@ -56,12 +58,14 @@ def __init__( config: configs.WebsocketConfig, url: str, raw_event_consumer_impl: raw_event_consumers.RawEventConsumer, - shard_type: typing.Type[ShardT] = shard_client.ShardClient, + shard_type: typing.Type[ShardT] = shard_clients.ShardClient, + dispatcher: typing.Optional[event_dispatchers.EventDispatcher] = None, ) -> None: super().__init__(more_logging.get_named_logger(self, conversions.pluralize(shard_count, "shard"))) self._is_running = False self.config = config self.raw_event_consumer = raw_event_consumer_impl + self._dispatcher = dispatcher self.shards: typing.Dict[int, ShardT] = { shard_id: shard_type(shard_id, shard_count, config, raw_event_consumer_impl, url) for shard_id in shard_ids } @@ -73,7 +77,7 @@ def latency(self) -> float: This will return a mean of all the heartbeat intervals for all shards with a valid heartbeat latency that are in the - :obj:`hikari.clients.shard_client.ShardState.READY` state. + :obj:`hikari.clients.shard_clients.ShardState.READY` state. If no shards are in this state, this will return ``float('nan')`` instead. @@ -100,6 +104,12 @@ async def start(self) -> None: session spam. This involves starting each shard sequentially with a 5 second pause between each. """ + if self._is_running: + raise RuntimeError("Cannot start a client twice.") + + if self._dispatcher is not None: + await self._dispatcher.dispatch_event(events.StartingEvent()) + self._is_running = True self.logger.info("starting %s shard(s)", len(self.shards)) start_time = time.perf_counter() @@ -113,29 +123,31 @@ async def start(self) -> None: self.logger.info("started %s shard(s) in approx %.2fs", len(self.shards), finish_time - start_time) + if self._dispatcher is not None: + await self._dispatcher.dispatch_event(events.StartedEvent()) + async def join(self) -> None: """Wait for all shards to finish executing, then return.""" await asyncio.gather(*(shard_obj.join() for shard_obj in self.shards.values())) - async def close(self, wait: bool = True) -> None: + async def close(self) -> None: """Close all shards. - Parameters - ---------- - wait : :obj:`bool` - If ``True`` (the default), then once called, this will wait until - all shards have shut down before returning. If ``False``, it will - only send the signal to shut down, but will return immediately. + Waits for all shards to shut down before returning. """ if self._is_running: self.logger.info("stopping %s shard(s)", len(self.shards)) start_time = time.perf_counter() try: - await asyncio.gather(*(shard_obj.close(wait) for shard_obj in self.shards.values())) + if self._dispatcher is not None: + await self._dispatcher.dispatch_event(events.StoppingEvent()) + await asyncio.gather(*(shard_obj.close() for shard_obj in self.shards.values())) finally: finish_time = time.perf_counter() self.logger.info("stopped %s shard(s) in approx %.2fs", len(self.shards), finish_time - start_time) self._is_running = False + if self._dispatcher is not None: + await self._dispatcher.dispatch_event(events.StoppedEvent()) async def update_presence( self, @@ -178,6 +190,7 @@ async def update_presence( *( shard.update_presence(status=status, activity=activity, idle_since=idle_since, is_afk=is_afk) for shard in self.shards.values() - if shard.connection_state in (shard_client.ShardState.WAITING_FOR_READY, shard_client.ShardState.READY) + if shard.connection_state + in (shard_clients.ShardState.WAITING_FOR_READY, shard_clients.ShardState.READY) ) ) diff --git a/hikari/clients/gateway_runner.py b/hikari/clients/gateway_runner.py index de4e34d4fb..0ca1ee1a92 100644 --- a/hikari/clients/gateway_runner.py +++ b/hikari/clients/gateway_runner.py @@ -27,11 +27,9 @@ import click -from hikari import entities from hikari.clients import configs -from hikari.clients import gateway_manager -from hikari.clients import shard_client -from hikari.state import raw_event_consumers +from hikari.clients import gateway_managers +from hikari.state import stateless_event_managers logger_levels = ("DEBUG", "INFO", "WARNING", "ERROR", "NOTSET") @@ -71,11 +69,9 @@ def run_gateway(compression, color, debug, logger, shards, token, url, verify_ss logging.basicConfig(level=logger, format=_color_format if color else _regular_format, stream=sys.stdout) - class _DummyConsumer(raw_event_consumers.RawEventConsumer): - def process_raw_event(self, _client: shard_client.ShardClient, name: str, payload: entities.RawEntityT) -> None: - logging.debug("dispatched %s with body [%-100.100s]", name, payload) + manager = stateless_event_managers.StatelessEventManagerImpl() - client = gateway_manager.GatewayManager( + client = gateway_managers.GatewayManager( shard_ids=[*range(shards)], shard_count=shards, config=configs.WebsocketConfig( @@ -86,7 +82,8 @@ def process_raw_event(self, _client: shard_client.ShardClient, name: str, payloa verify_ssl=verify_ssl, ), url=url, - raw_event_consumer_impl=_DummyConsumer(), + raw_event_consumer_impl=manager, + dispatcher=manager.event_dispatcher, ) client.run() diff --git a/hikari/clients/rest_client.py b/hikari/clients/rest_clients.py similarity index 100% rename from hikari/clients/rest_client.py rename to hikari/clients/rest_clients.py diff --git a/hikari/clients/runnable.py b/hikari/clients/runnable.py index c5166d9f59..d8337e335e 100644 --- a/hikari/clients/runnable.py +++ b/hikari/clients/runnable.py @@ -46,7 +46,7 @@ async def start(self) -> None: # noqa: D401 """Start the component.""" @abc.abstractmethod - async def close(self, wait: bool = True) -> None: + async def close(self) -> None: """Shut down the component.""" @abc.abstractmethod @@ -87,7 +87,7 @@ def sigterm_handler(*_): # UnboundLocalError. ex = _ex finally: - loop.run_until_complete(self.close(True)) + loop.run_until_complete(self.close()) with contextlib.suppress(NotImplementedError): # Not implemented on Windows loop.remove_signal_handler(signal.SIGTERM) diff --git a/hikari/clients/shard_client.py b/hikari/clients/shard_clients.py similarity index 82% rename from hikari/clients/shard_client.py rename to hikari/clients/shard_clients.py index c189de0bc8..67983e3cdd 100644 --- a/hikari/clients/shard_client.py +++ b/hikari/clients/shard_clients.py @@ -24,6 +24,8 @@ Additional functions and coroutines are provided to update the presence on the shard using models defined in :mod:`hikari`. """ +from __future__ import annotations + __all__ = ["ShardState", "ShardClient"] import asyncio @@ -36,6 +38,7 @@ import aiohttp from hikari import errors +from hikari import events from hikari import gateway_entities from hikari import guilds from hikari.clients import configs @@ -45,6 +48,7 @@ from hikari.net import codes from hikari.net import ratelimits from hikari.net import shard +from hikari.state import event_dispatchers from hikari.state import raw_event_consumers @@ -88,11 +92,16 @@ class ShardClient(runnable.RunnableClient): The consumer of a raw event. url : :obj:`str` The URL to connect the gateway to. + dispatcher : :obj:`hikari.state.event_dispatchers.EventDispatcher`, optional + The high level event dispatcher to use for dispatching start and stop + events. Set this to ``None`` to disable that functionality (useful if + you use a gateway manager to orchestrate multiple shards instead and + provide this functionality there). Defaults to ``None`` if unspecified. Notes ----- Generally, you want to use - :obj:`hikari.clients.gateway_manager.GatewayManager` rather than this class + :obj:`hikari.clients.gateway_managers.GatewayManager` rather than this class directly, as that will handle sharding where enabled and applicable, and provides a few more bits and pieces that may be useful such as state management and event dispatcher integration. and If you want to customize @@ -102,13 +111,14 @@ class ShardClient(runnable.RunnableClient): __slots__ = ( "logger", "_raw_event_consumer", - "_client", + "_connection", "_status", "_activity", "_idle_since", "_is_afk", "_task", "_shard_state", + "_dispatcher", ) def __init__( @@ -118,6 +128,7 @@ def __init__( config: configs.WebsocketConfig, raw_event_consumer_impl: raw_event_consumers.RawEventConsumer, url: str, + dispatcher: typing.Optional[event_dispatchers.EventDispatcher] = None, ) -> None: super().__init__(more_logging.get_named_logger(self, f"#{shard_id}")) self._raw_event_consumer = raw_event_consumer_impl @@ -127,7 +138,8 @@ def __init__( self._status = config.initial_status self._shard_state = ShardState.NOT_RUNNING self._task = None - self._client = shard.ShardConnection( + self._dispatcher = dispatcher + self._connection = shard.ShardConnection( compression=config.gateway_use_compression, connector=config.tcp_connector, debug=config.debug, @@ -163,7 +175,29 @@ def connection(self) -> shard.ShardConnection: :obj:`hikari.net.shard.ShardConnection` The low-level gateway client used for this shard. """ - return self._client + return self._connection + + @property + def shard_id(self) -> int: + """Shard ID. + + Returns + ------- + :obj:`int` + The 0-indexed shard ID. + """ + return self._connection.shard_id + + @property + def shard_count(self) -> int: + """Shard count. + + Returns + ------- + :obj:`int` + The number of shards that make up this bot. + """ + return self._connection.shard_count @property def status(self) -> guilds.PresenceStatus: # noqa: D401 @@ -223,7 +257,7 @@ def latency(self) -> float: The heartbeat latency in seconds. This will be ``float('nan')`` until the first heartbeat is performed. """ - return self._client.heartbeat_latency + return self._connection.heartbeat_latency @property def heartbeat_interval(self) -> float: @@ -235,7 +269,7 @@ def heartbeat_interval(self) -> float: The heartbeat interval in seconds. This will be ``float('nan')`` until the connection has received a ``HELLO`` payload. """ - return self._client.heartbeat_interval + return self._connection.heartbeat_interval @property def reconnect_count(self) -> float: @@ -248,7 +282,7 @@ def reconnect_count(self) -> float: :obj:`int` The number of reconnects this shard has performed. """ - return self._client.reconnect_count + return self._connection.reconnect_count @property def connection_state(self) -> ShardState: @@ -273,7 +307,7 @@ async def start(self): self._task = asyncio.create_task(self._keep_alive(), name="ShardClient#keep_alive") completed, _ = await asyncio.wait( - [self._task, self._client.ready_event.wait()], return_when=asyncio.FIRST_COMPLETED + [self._task, self._connection.ready_event.wait()], return_when=asyncio.FIRST_COMPLETED ) if self._task in completed: @@ -283,30 +317,34 @@ async def join(self) -> None: """Wait for the shard to shut down fully.""" await self._task if self._task is not None else more_asyncio.completed_future() - async def close(self, wait: bool = True) -> None: + async def close(self) -> None: """Request that the shard shuts down. - Parameters - ---------- - wait : :obj:`bool` - If ``True`` (default), then wait for the client to shut down fully. - If ``False``, only send the signal to shut down, but do not wait - for it explicitly. + This will wait for the client to shut down before returning. """ if self._shard_state != ShardState.STOPPING: self._shard_state = ShardState.STOPPING self.logger.debug("stopping shard") - await self._client.close() - if wait: - await self._task + + if self._dispatcher is not None: + await self._dispatcher.dispatch_event(events.StoppingEvent()) + + await self._connection.close() + with contextlib.suppress(): - self._task.result() + await self._task + + if self._dispatcher is not None: + await self._dispatcher.dispatch_event(events.StoppedEvent()) async def _keep_alive(self): back_off = ratelimits.ExponentialBackOff(maximum=None) last_start = time.perf_counter() do_not_back_off = True + if self._dispatcher is not None: + await self._dispatcher.dispatch_event(events.StartingEvent()) + while True: try: if not do_not_back_off and time.perf_counter() - last_start < 30: @@ -322,6 +360,11 @@ async def _keep_alive(self): do_not_back_off = False connect_task = await self._spin_up() + + if self._dispatcher is not None and self.reconnect_count == 0: + # Only dispatch this on initial connect, not on reconnect. + await self._dispatcher.dispatch_event(events.StartedEvent()) + await connect_task self.logger.critical("shut down silently! this shouldn't happen!") @@ -336,8 +379,8 @@ async def _keep_alive(self): self.logger.warning("invalid session, so will attempt to resume") else: self.logger.warning("invalid session, so will attempt to reconnect") - self._client.seq = None - self._client.session_id = None + self._connection.seq = None + self._connection.session_id = None do_not_back_off = True await asyncio.sleep(5) @@ -371,12 +414,12 @@ async def _spin_up(self) -> asyncio.Task: self.logger.debug("initializing shard") self._shard_state = ShardState.CONNECTING - is_resume = self._client.seq is not None and self._client.session_id is not None + is_resume = self._connection.seq is not None and self._connection.session_id is not None - connect_task = asyncio.create_task(self._client.connect(), name="ShardConnection#connect") + connect_task = asyncio.create_task(self._connection.connect(), name="ShardConnection#connect") completed, _ = await asyncio.wait( - [connect_task, self._client.hello_event.wait()], return_when=asyncio.FIRST_COMPLETED + [connect_task, self._connection.hello_event.wait()], return_when=asyncio.FIRST_COMPLETED ) if connect_task in completed: @@ -385,7 +428,7 @@ async def _spin_up(self) -> asyncio.Task: self.logger.info("received HELLO, interval is %ss", self.connection.heartbeat_interval) completed, _ = await asyncio.wait( - [connect_task, self._client.identify_event.wait()], return_when=asyncio.FIRST_COMPLETED + [connect_task, self._connection.identify_event.wait()], return_when=asyncio.FIRST_COMPLETED ) if connect_task in completed: @@ -396,7 +439,7 @@ async def _spin_up(self) -> asyncio.Task: self._shard_state = ShardState.RESUMING completed, _ = await asyncio.wait( - [connect_task, self._client.resumed_event.wait()], return_when=asyncio.FIRST_COMPLETED + [connect_task, self._connection.resumed_event.wait()], return_when=asyncio.FIRST_COMPLETED ) if connect_task in completed: @@ -410,7 +453,7 @@ async def _spin_up(self) -> asyncio.Task: self._shard_state = ShardState.WAITING_FOR_READY completed, _ = await asyncio.wait( - [connect_task, self._client.ready_event.wait()], return_when=asyncio.FIRST_COMPLETED + [connect_task, self._connection.ready_event.wait()], return_when=asyncio.FIRST_COMPLETED ) if connect_task in completed: @@ -459,7 +502,7 @@ async def update_presence( is_afk = self._is_afk if is_afk is ... else is_afk presence = self._create_presence_pl(status=status, activity=activity, idle_since=idle_since, is_afk=is_afk) - await self._client.update_presence(presence) + await self._connection.update_presence(presence) # If we get this far, the update succeeded probably, or the gateway just died. Whatever. self._status = status @@ -480,3 +523,16 @@ def _create_presence_pl( "game": activity.serialize() if activity is not None else None, "afk": is_afk, } + + def __str__(self) -> str: + return f"Shard {self.connection.shard_id} in pool of {self.connection.shard_count} shards" + + def __repr__(self) -> str: + return ( + "ShardClient(" + + ", ".join( + f"{k}={getattr(self, k)!r}" + for k in ("shard_id", "shard_count", "connection_state", "heartbeat_interval", "latency") + ) + + ")" + ) diff --git a/hikari/events.py b/hikari/events.py index 5ef313dd69..03be0f7cbd 100644 --- a/hikari/events.py +++ b/hikari/events.py @@ -23,6 +23,7 @@ "ExceptionEvent", "ConnectedEvent", "DisconnectedEvent", + "StartingEvent", "StartedEvent", "StoppingEvent", "StoppedEvent", @@ -70,7 +71,6 @@ import attr -import hikari.internal.conversions from hikari import channels from hikari import embeds as _embeds from hikari import emojis as _emojis @@ -82,7 +82,9 @@ from hikari import snowflakes from hikari import users from hikari import voices +from hikari.clients import shard_clients from hikari.internal import assertions +from hikari.internal import conversions from hikari.internal import marshaller T_contra = typing.TypeVar("T_contra", contravariant=True) @@ -115,6 +117,11 @@ class ExceptionEvent(HikariEvent): callback: typing.Callable[[HikariEvent], typing.Awaitable[None]] +@attr.attrs(slots=True, auto_attribs=True) +class StartingEvent(HikariEvent): + """Event that is fired before the gateway client starts all shards.""" + + # Synthetic event, is not deserialized @attr.attrs(slots=True, auto_attribs=True) class StartedEvent(HikariEvent): @@ -136,6 +143,7 @@ class StoppedEvent(HikariEvent): _websocket_name_break = re.compile(r"(?<=[a-z])(?=[A-Z])") +# TODO: remove this, it is unused. def mark_as_websocket_event(cls): """Mark the event as being a websocket one.""" name = cls.__name__ @@ -147,16 +155,26 @@ def mark_as_websocket_event(cls): @mark_as_websocket_event -@marshaller.attrs(slots=True) +@attr.s(slots=True, kw_only=True, auto_attribs=True) class ConnectedEvent(HikariEvent, entities.Deserializable): """Event invoked each time a shard connects.""" + #: The shard that connected. + #: + #: :type: :obj:`hikari.clients.shard_clients.ShardClient` + shard: shard_clients.ShardClient + @mark_as_websocket_event -@marshaller.attrs(slots=True) +@attr.s(slots=True, kw_only=True, auto_attribs=True) class DisconnectedEvent(HikariEvent, entities.Deserializable): """Event invoked each time a shard disconnects.""" + #: The shard that disconnected. + #: + #: :type: :obj:`hikari.clients.shard_clients.ShardClient` + shard: shard_clients.ShardClient + @mark_as_websocket_event @marshaller.attrs(slots=True) @@ -334,7 +352,7 @@ class BaseChannelEvent(HikariEvent, snowflakes.UniqueEntity, entities.Deserializ #: #: :type: :obj:`datetime.datetime`, optional last_pin_timestamp: typing.Optional[datetime.datetime] = marshaller.attrib( - deserializer=hikari.internal.conversions.parse_iso_8601_ts, if_undefined=None + deserializer=conversions.parse_iso_8601_ts, if_undefined=None ) @@ -387,7 +405,7 @@ class ChannelPinUpdateEvent(HikariEvent, entities.Deserializable): #: #: :type: :obj:`datetime.datetime`, optional last_pin_timestamp: typing.Optional[datetime.datetime] = marshaller.attrib( - deserializer=hikari.internal.conversions.parse_iso_8601_ts, if_undefined=None + deserializer=conversions.parse_iso_8601_ts, if_undefined=None ) @@ -553,7 +571,7 @@ class GuildMemberUpdateEvent(HikariEvent, entities.Deserializable): #: #: :type: :obj:`typing.Union` [ :obj:`datetime.datetime`, :obj:`hikari.entities.UNSET` ], optional premium_since: typing.Union[None, datetime.datetime, entities.Unset] = marshaller.attrib( - deserializer=hikari.internal.conversions.parse_iso_8601_ts, if_none=None, if_undefined=entities.Unset + deserializer=conversions.parse_iso_8601_ts, if_none=None, if_undefined=entities.Unset ) @@ -623,7 +641,7 @@ class InviteCreateEvent(HikariEvent, entities.Deserializable): #: The datetime of when this invite was created. #: #: :type: :obj:`datetime.datetime` - created_at: datetime.datetime = marshaller.attrib(deserializer=hikari.internal.conversions.parse_iso_8601_ts) + created_at: datetime.datetime = marshaller.attrib(deserializer=conversions.parse_iso_8601_ts) #: The ID of the guild this invite was created in, if applicable. #: Will be ``None`` for group DM invites. @@ -757,14 +775,14 @@ class MessageUpdateEvent(HikariEvent, snowflakes.UniqueEntity, entities.Deserial #: #: :type: :obj:`typing.Union` [ :obj:`datetime.datetime`, :obj:`hikari.entities.UNSET` ] timestamp: typing.Union[datetime.datetime, entities.Unset] = marshaller.attrib( - deserializer=hikari.internal.conversions.parse_iso_8601_ts, if_undefined=entities.Unset + deserializer=conversions.parse_iso_8601_ts, if_undefined=entities.Unset ) #: The timestamp that the message was last edited at, or ``None`` if not ever edited. #: #: :type: :obj:`typing.Union` [ :obj:`datetime.datetime`, :obj:`hikari.entities.UNSET` ], optional edited_timestamp: typing.Union[datetime.datetime, entities.Unset, None] = marshaller.attrib( - deserializer=hikari.internal.conversions.parse_iso_8601_ts, if_none=None, if_undefined=entities.Unset + deserializer=conversions.parse_iso_8601_ts, if_none=None, if_undefined=entities.Unset ) #: Whether the message is a TTS message. diff --git a/hikari/internal/conversions.py b/hikari/internal/conversions.py index 178ea9ae0c..58642ba0d8 100644 --- a/hikari/internal/conversions.py +++ b/hikari/internal/conversions.py @@ -25,9 +25,11 @@ "ResultT", "nullable_cast", "try_cast", + "try_cast_or_defer_unary_operator", "put_if_specified", "image_bytes_to_image_data", "pluralize", + "snoop_typehint_from_scope", ] import base64 @@ -36,6 +38,7 @@ import email.utils import io import re +import types import typing CastInputT = typing.TypeVar("CastInputT") @@ -274,5 +277,71 @@ def pluralize(count: int, name: str, suffix: str = "s") -> str: return f"{count} {name + suffix}" if count - 1 else f"{count} {name}" +def snoop_typehint_from_scope(frame: types.FrameType, typehint: typing.Union[str, typing.Any]) -> typing.Any: + """Resolve a string type hint from a given stack frame. + + This snoops around the local and global scope for the given frame to find + the given attribute name, taking into account nested function calls. The + reason to do this is that if a string type hint is used, or the + ``from __future__ import annotations`` directive is used, the physical thing + that the type hint represents will no longer be evaluated by the + interpreter. This is an implementation that does not require the use of + :obj:`eval`, and thus reduces the risk of arbitrary code execution as a + result. + + Nested parameters such as :obj:`typing.Sequence` should also be able to be + resolved correctly. + + Parameters + ---------- + frame : :obj:`types.FrameType` + The stack frame that the element with the typehint was defined in. + This is retrieved using :obj:`inspect.stack` ``(frame_no)[0][0]``, + where ``frame_no`` is the number of frames from this invocation that + you want to snoop the scope at. + typehint : :obj:`typing.Union` [ :obj:`str`, :obj:`typing.Any` ] + The type hint to resolve. If a non-:obj:`str` is passed, then this is + returned immediately as the result. + + Returns + ------- + :obj:`typing.Any` + The physical representation of the given type hint. + + Raises + ------ + :obj:`NameError` + If the attribute was not found. + + Warnings + -------- + The input frame must be manually dereferenced using the ``del`` keyword + after use. Any functions that are decorated and wrapped when using this + lookup must use :obj:`functools.wraps` to ensure that the correct scope is + identified on the stack. + + This is incredibly unpythonic and baremetal, but due to + `PEP 563 ` there is no other + consistent way of making this work correctly. + """ + if not isinstance(typehint, str): + return typehint + + fragments = typehint.split(".") + + try: + for scope in (frame.f_locals, frame.f_globals): + try: + scope = scope[fragments[0]] + for attr in fragments[1:]: + scope = getattr(scope, attr) + return scope + except (AttributeError, KeyError): + pass + raise NameError(f"No attribute {typehint} was found in enclosing scope") + finally: + del frame, scope # lgtm [py/unnecessary-delete] + + BytesLikeT = typing.Union[bytes, bytearray, memoryview, str, io.StringIO, io.BytesIO] FileLikeT = typing.Union[BytesLikeT, io.BufferedRandom, io.BufferedReader, io.BufferedRWPair] diff --git a/hikari/state/__init__.py b/hikari/state/__init__.py index c9cb77fadb..608c413269 100644 --- a/hikari/state/__init__.py +++ b/hikari/state/__init__.py @@ -27,9 +27,9 @@ The overall structure is as follows: .. inheritance-diagram:: - hikari.state.event_dispatcher - hikari.state.raw_event_consumer - hikari.state.event_manager - hikari.state.stateless_event_manager_impl + hikari.state.event_dispatchers + hikari.state.raw_event_consumers + hikari.state.event_managers + hikari.state.stateless_event_managers """ __all__ = [] diff --git a/hikari/state/event_dispatchers.py b/hikari/state/event_dispatchers.py index ec414dec86..986b6a3c77 100644 --- a/hikari/state/event_dispatchers.py +++ b/hikari/state/event_dispatchers.py @@ -17,22 +17,33 @@ # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . """Event dispatcher implementation.""" + +from __future__ import annotations + __all__ = ["EventDispatcher", "EventDispatcherImpl"] import abc import asyncio +import inspect import logging import typing from hikari import events from hikari.internal import assertions +from hikari.internal import conversions from hikari.internal import more_asyncio from hikari.internal import more_collections from hikari.internal import more_logging -EventT = typing.TypeVar("EventT", bound=events.HikariEvent) -PredicateT = typing.Callable[[EventT], typing.Union[bool, typing.Coroutine[None, None, bool]]] -EventCallbackT = typing.Callable[[EventT], typing.Coroutine[None, None, typing.Any]] +# Prevents a circular reference that prevents importing correctly. +if typing.TYPE_CHECKING: + EventT = typing.TypeVar("EventT", bound=events.HikariEvent) + PredicateT = typing.Callable[[EventT], typing.Union[bool, typing.Coroutine[None, None, bool]]] + EventCallbackT = typing.Callable[[EventT], typing.Coroutine[None, None, typing.Any]] +else: + EventT = typing.TypeVar("EventT") + PredicateT = typing.TypeVar("PredicateT") + EventCallbackT = typing.TypeVar("EventCallbackT") class EventDispatcher(abc.ABC): @@ -115,14 +126,60 @@ def wait_for( lookup, but can be implemented this way optionally if documented. """ - # Ignore docstring not starting in an imperative mood - def on(self, event_type: typing.Type[EventT]) -> typing.Callable[[EventCallbackT], EventCallbackT]: # noqa: D401 - """Returns a decorator that is equivalent to invoking :meth:`add_listener`. + @typing.overload + def on(self) -> typing.Callable[[EventCallbackT], EventCallbackT]: + """Return a decorator to create an event listener. + + This considers the type hint on the signature to get the event type. + + Example + ------- + .. code-block:: python + + @bot.on() + async def on_message(event: hikari.MessageCreatedEvent): + print(event.content) + """ + + @typing.overload + def on(self, event_type: typing.Type[EventCallbackT]) -> typing.Callable[[EventCallbackT], EventCallbackT]: + """Return a decorator to create an event listener. + + This considers the type given to the decorator. + + Example + ------- + .. code-block:: python + + @bot.on(hikari.MessageCreatedEvent) + async def on_message(event): + print(event.content) + """ + + def on(self, event_type=None): + """Return a decorator equivalent to invoking :meth:`add_listener`. Parameters ---------- - event_type : :obj:`typing.Type` [ :obj:`hikari.events.HikariEvent` ] - The event type to register the produced decorator to. + event_type : :obj:`typing.Type` [ :obj:`hikari.events.HikariEvent` ], optional + The event type to register the produced decorator to. If this is not + specified, then the given function is used instead and the type hint + of the first argument is considered. If no type hint is present + there either, then the call will fail. + + Examples + -------- + .. code-block:: python + + # Type-hinted format. + @bot.on() + async def on_message(event: hikari.MessageCreatedEvent): + print(event.content) + + # Explicit format. + @bot.on(hikari.MessageCreatedEvent) + async def on_message(event): + print(event.content) Returns ------- @@ -131,6 +188,32 @@ def on(self, event_type: typing.Type[EventT]) -> typing.Callable[[EventCallbackT """ def decorator(callback: EventCallbackT) -> EventCallbackT: + if event_type is None: + signature = inspect.signature(callback) + parameters = list(signature.parameters.values()) + + if len(parameters) == 2 and parameters[0].annotation is inspect.Parameter.empty: + event_param = parameters[1] + elif len(parameters) == 1: + event_param = parameters[0] + + if event_param.annotation is inspect.Parameter.empty: + raise TypeError(f"No typehint given for parameter: async def {callback}({signature}): ...") + else: + raise TypeError(f"Invalid signature for event: async def {callback.__name__}({signature}): ...") + + frame, *_, = inspect.stack(2)[0] + + try: + resolved_type = conversions.snoop_typehint_from_scope(frame, event_param.annotation) + + if not issubclass(resolved_type, events.HikariEvent): + raise TypeError("Event typehints should subclass hikari.events.HikariEvent to be valid") + + return self.add_listener(typing.cast(typing.Type[events.HikariEvent], resolved_type), callback) + finally: + del frame, _ + return self.add_listener(event_type, callback) return decorator diff --git a/hikari/state/event_managers.py b/hikari/state/event_managers.py index efcdd1cbc0..d9d50c5709 100644 --- a/hikari/state/event_managers.py +++ b/hikari/state/event_managers.py @@ -23,7 +23,7 @@ import typing from hikari import entities -from hikari.clients import shard_client +from hikari.clients import shard_clients from hikari.internal import assertions from hikari.internal import more_logging from hikari.state import event_dispatchers @@ -97,7 +97,7 @@ class EventManager(typing.Generic[EventDispatcherT], raw_event_consumers.RawEven These methods are expected to have the following parameters: - shard_obj: :obj:`hikari.clients.shard_client.ShardClient` + shard_obj: :obj:`hikari.clients.shard_clients.ShardClient` The shard client that emitted the event. payload: :obj:`typing.Any` The received payload. This is expected to be a JSON-compatible type. @@ -150,7 +150,7 @@ def __init__(self, event_dispatcher_impl: typing.Optional[EventDispatcherT] = No self.raw_event_mappers[event_name] = member def process_raw_event( - self, shard_client_obj: shard_client.ShardClient, name: str, payload: entities.RawEntityT, + self, shard_client_obj: shard_clients.ShardClient, name: str, payload: entities.RawEntityT, ) -> None: """Process a low level event. @@ -159,7 +159,7 @@ def process_raw_event( Parameters ---------- - shard_client_obj: :obj:`hikari.clients.shard_client.ShardClient` + shard_client_obj: :obj:`hikari.clients.shard_clients.ShardClient` The shard that triggered this event. name : :obj:`str` The raw event name. diff --git a/hikari/state/raw_event_consumers.py b/hikari/state/raw_event_consumers.py index 00c1e77aa7..28ae67273a 100644 --- a/hikari/state/raw_event_consumers.py +++ b/hikari/state/raw_event_consumers.py @@ -27,7 +27,7 @@ import abc from hikari import entities -from hikari.clients import shard_client +from hikari.clients import shard_clients class RawEventConsumer(abc.ABC): @@ -43,13 +43,13 @@ class RawEventConsumer(abc.ABC): @abc.abstractmethod def process_raw_event( - self, shard_client_obj: shard_client.ShardClient, name: str, payload: entities.RawEntityT, + self, shard_client_obj: shard_clients.ShardClient, name: str, payload: entities.RawEntityT, ) -> None: """Consume a raw event that was received from a shard connection. Parameters ---------- - shard_client_obj : :obj:`hikari.clients.shard_client.ShardClient` + shard_client_obj : :obj:`hikari.clients.shard_clients.ShardClient` The client for the shard that received the event. name : :obj:`str` The raw event name. diff --git a/hikari/state/stateless_event_managers.py b/hikari/state/stateless_event_managers.py index 1f511a4803..b8e3584308 100644 --- a/hikari/state/stateless_event_managers.py +++ b/hikari/state/stateless_event_managers.py @@ -20,6 +20,7 @@ __all__ = ["StatelessEventManagerImpl"] +from hikari import events from hikari.state import event_dispatchers from hikari.state import event_managers @@ -32,4 +33,14 @@ class StatelessEventManagerImpl(event_managers.EventManager[event_dispatchers.Ev version, and are not immediately affected by the use of intents. """ - # @event_managers.raw_event_mapper("CONNECT") + @event_managers.raw_event_mapper("CONNECTED") + def on_connect(self, shard, _): + """Handle CONNECTED events.""" + event = events.ConnectedEvent(shard=shard) + self.event_dispatcher.dispatch_event(event) + + @event_managers.raw_event_mapper("DISCONNECTED") + def on_disconnect(self, shard, _): + """Handle DISCONNECTED events.""" + event = events.DisconnectedEvent(shard=shard) + self.event_dispatcher.dispatch_event(event) diff --git a/tests/hikari/clients/test_gateway_manager.py b/tests/hikari/clients/test_gateway_manager.py index 02bd260c57..cfcfbc6a39 100644 --- a/tests/hikari/clients/test_gateway_manager.py +++ b/tests/hikari/clients/test_gateway_manager.py @@ -21,59 +21,59 @@ import cymock as mock import pytest -from hikari.clients import gateway_manager -from hikari.clients import shard_client +from hikari.clients import gateway_managers +from hikari.clients import shard_clients from tests.hikari import _helpers class TestGatewayManager: def test_latency(self): - shard1 = mock.MagicMock(shard_client.ShardClient, latency=20) - shard2 = mock.MagicMock(shard_client.ShardClient, latency=30) - shard3 = mock.MagicMock(shard_client.ShardClient, latency=40) + shard1 = mock.MagicMock(shard_clients.ShardClient, latency=20) + shard2 = mock.MagicMock(shard_clients.ShardClient, latency=30) + shard3 = mock.MagicMock(shard_clients.ShardClient, latency=40) - with mock.patch("hikari.clients.shard_client.ShardClient", side_effect=[shard1, shard2, shard3]): - gateway_manager_obj = gateway_manager.GatewayManager( + with mock.patch("hikari.clients.shard_clients.ShardClient", side_effect=[shard1, shard2, shard3]): + gateway_manager_obj = gateway_managers.GatewayManager( shard_ids=[0, 1, 2], shard_count=3, config=None, url="some_url", raw_event_consumer_impl=None, - shard_type=shard_client.ShardClient, + shard_type=shard_clients.ShardClient, ) assert gateway_manager_obj.latency == 30 def test_latency_doesnt_take_into_a_count_shards_with_no_latency(self): - shard1 = mock.MagicMock(shard_client.ShardClient, latency=20) - shard2 = mock.MagicMock(shard_client.ShardClient, latency=30) - shard3 = mock.MagicMock(shard_client.ShardClient, latency=float("nan")) + shard1 = mock.MagicMock(shard_clients.ShardClient, latency=20) + shard2 = mock.MagicMock(shard_clients.ShardClient, latency=30) + shard3 = mock.MagicMock(shard_clients.ShardClient, latency=float("nan")) - with mock.patch("hikari.clients.shard_client.ShardClient", side_effect=[shard1, shard2, shard3]): - gateway_manager_obj = gateway_manager.GatewayManager( + with mock.patch("hikari.clients.shard_clients.ShardClient", side_effect=[shard1, shard2, shard3]): + gateway_manager_obj = gateway_managers.GatewayManager( shard_ids=[0, 1, 2], shard_count=3, config=None, url="some_url", raw_event_consumer_impl=None, - shard_type=shard_client.ShardClient, + shard_type=shard_clients.ShardClient, ) assert gateway_manager_obj.latency == 25 def test_latency_returns_nan_if_all_shards_have_no_latency(self): - shard1 = mock.MagicMock(shard_client.ShardClient, latency=float("nan")) - shard2 = mock.MagicMock(shard_client.ShardClient, latency=float("nan")) - shard3 = mock.MagicMock(shard_client.ShardClient, latency=float("nan")) + shard1 = mock.MagicMock(shard_clients.ShardClient, latency=float("nan")) + shard2 = mock.MagicMock(shard_clients.ShardClient, latency=float("nan")) + shard3 = mock.MagicMock(shard_clients.ShardClient, latency=float("nan")) - with mock.patch("hikari.clients.shard_client.ShardClient", side_effect=[shard1, shard2, shard3]): - gateway_manager_obj = gateway_manager.GatewayManager( + with mock.patch("hikari.clients.shard_clients.ShardClient", side_effect=[shard1, shard2, shard3]): + gateway_manager_obj = gateway_managers.GatewayManager( shard_ids=[0, 1, 2], shard_count=3, config=None, url="some_url", raw_event_consumer_impl=None, - shard_type=shard_client.ShardClient, + shard_type=shard_clients.ShardClient, ) assert math.isnan(gateway_manager_obj.latency) @@ -96,19 +96,19 @@ def __call__(self): return super().__call__() - shard1 = mock.MagicMock(shard_client.ShardClient, start=MockStart(condition=False)) - shard2 = mock.MagicMock(shard_client.ShardClient, start=MockStart(condition=True)) - shard3 = mock.MagicMock(shard_client.ShardClient, start=MockStart(condition=True)) + shard1 = mock.MagicMock(shard_clients.ShardClient, start=MockStart(condition=False)) + shard2 = mock.MagicMock(shard_clients.ShardClient, start=MockStart(condition=True)) + shard3 = mock.MagicMock(shard_clients.ShardClient, start=MockStart(condition=True)) - with mock.patch("hikari.clients.shard_client.ShardClient", side_effect=[shard1, shard2, shard3]): + with mock.patch("hikari.clients.shard_clients.ShardClient", side_effect=[shard1, shard2, shard3]): with mock.patch("asyncio.sleep", wraps=mock_sleep): - gateway_manager_obj = gateway_manager.GatewayManager( + gateway_manager_obj = gateway_managers.GatewayManager( shard_ids=[0, 1, 2], shard_count=3, config=None, url="some_url", raw_event_consumer_impl=None, - shard_type=shard_client.ShardClient, + shard_type=shard_clients.ShardClient, ) await gateway_manager_obj.start() mock_sleep.assert_not_called() @@ -119,19 +119,19 @@ def __call__(self): @pytest.mark.asyncio async def test_join_calls_join_on_all_shards(self): - shard1 = mock.MagicMock(shard_client.ShardClient, join=mock.MagicMock()) - shard2 = mock.MagicMock(shard_client.ShardClient, join=mock.MagicMock()) - shard3 = mock.MagicMock(shard_client.ShardClient, join=mock.MagicMock()) + shard1 = mock.MagicMock(shard_clients.ShardClient, join=mock.MagicMock()) + shard2 = mock.MagicMock(shard_clients.ShardClient, join=mock.MagicMock()) + shard3 = mock.MagicMock(shard_clients.ShardClient, join=mock.MagicMock()) - with mock.patch("hikari.clients.shard_client.ShardClient", side_effect=[shard1, shard2, shard3]): + with mock.patch("hikari.clients.shard_clients.ShardClient", side_effect=[shard1, shard2, shard3]): with mock.patch("asyncio.gather", return_value=_helpers.AwaitableMock()): - gateway_manager_obj = gateway_manager.GatewayManager( + gateway_manager_obj = gateway_managers.GatewayManager( shard_ids=[0, 1, 2], shard_count=3, config=None, url="some_url", raw_event_consumer_impl=None, - shard_type=shard_client.ShardClient, + shard_type=shard_clients.ShardClient, ) await gateway_manager_obj.join() @@ -140,44 +140,43 @@ async def test_join_calls_join_on_all_shards(self): shard3.join.assert_called_once() @pytest.mark.asyncio - @pytest.mark.parametrize("wait", [True, False]) - async def test_close_closes_all_shards(self, wait): - shard1 = mock.MagicMock(shard_client.ShardClient, close=mock.MagicMock()) - shard2 = mock.MagicMock(shard_client.ShardClient, close=mock.MagicMock()) - shard3 = mock.MagicMock(shard_client.ShardClient, close=mock.MagicMock()) + async def test_close_closes_all_shards(self): + shard1 = mock.MagicMock(shard_clients.ShardClient, close=mock.MagicMock()) + shard2 = mock.MagicMock(shard_clients.ShardClient, close=mock.MagicMock()) + shard3 = mock.MagicMock(shard_clients.ShardClient, close=mock.MagicMock()) - with mock.patch("hikari.clients.shard_client.ShardClient", side_effect=[shard1, shard2, shard3]): + with mock.patch("hikari.clients.shard_clients.ShardClient", side_effect=[shard1, shard2, shard3]): with mock.patch("asyncio.gather", return_value=_helpers.AwaitableMock()): - gateway_manager_obj = gateway_manager.GatewayManager( + gateway_manager_obj = gateway_managers.GatewayManager( shard_ids=[0, 1, 2], shard_count=3, config=None, url="some_url", raw_event_consumer_impl=None, - shard_type=shard_client.ShardClient, + shard_type=shard_clients.ShardClient, ) gateway_manager_obj._is_running = True - await gateway_manager_obj.close(wait=wait) + await gateway_manager_obj.close() - shard1.close.assert_called_once_with(wait) - shard2.close.assert_called_once_with(wait) - shard3.close.assert_called_once_with(wait) + shard1.close.assert_called_once_with() + shard2.close.assert_called_once_with() + shard3.close.assert_called_once_with() @pytest.mark.asyncio async def test_close_does_nothing_if_not_running(self): - shard1 = mock.MagicMock(shard_client.ShardClient, close=mock.MagicMock()) - shard2 = mock.MagicMock(shard_client.ShardClient, close=mock.MagicMock()) - shard3 = mock.MagicMock(shard_client.ShardClient, close=mock.MagicMock()) + shard1 = mock.MagicMock(shard_clients.ShardClient, close=mock.MagicMock()) + shard2 = mock.MagicMock(shard_clients.ShardClient, close=mock.MagicMock()) + shard3 = mock.MagicMock(shard_clients.ShardClient, close=mock.MagicMock()) - with mock.patch("hikari.clients.shard_client.ShardClient", side_effect=[shard1, shard2, shard3]): + with mock.patch("hikari.clients.shard_clients.ShardClient", side_effect=[shard1, shard2, shard3]): with mock.patch("asyncio.gather", return_value=_helpers.AwaitableMock()): - gateway_manager_obj = gateway_manager.GatewayManager( + gateway_manager_obj = gateway_managers.GatewayManager( shard_ids=[0, 1, 2], shard_count=3, config=None, url="some_url", raw_event_consumer_impl=None, - shard_type=shard_client.ShardClient, + shard_type=shard_clients.ShardClient, ) gateway_manager_obj._is_running = False await gateway_manager_obj.close() @@ -189,28 +188,30 @@ async def test_close_does_nothing_if_not_running(self): @pytest.mark.asyncio async def test_update_presence_updates_presence_in_all_ready_or_waiting_for_ready_shards(self): shard1 = mock.MagicMock( - shard_client.ShardClient, update_presence=mock.MagicMock(), connection_state=shard_client.ShardState.READY, + shard_clients.ShardClient, + update_presence=mock.MagicMock(), + connection_state=shard_clients.ShardState.READY, ) shard2 = mock.MagicMock( - shard_client.ShardClient, + shard_clients.ShardClient, update_presence=mock.MagicMock(), - connection_state=shard_client.ShardState.WAITING_FOR_READY, + connection_state=shard_clients.ShardState.WAITING_FOR_READY, ) shard3 = mock.MagicMock( - shard_client.ShardClient, + shard_clients.ShardClient, update_presence=mock.MagicMock(), - connection_state=shard_client.ShardState.CONNECTING, + connection_state=shard_clients.ShardState.CONNECTING, ) - with mock.patch("hikari.clients.shard_client.ShardClient", side_effect=[shard1, shard2, shard3]): + with mock.patch("hikari.clients.shard_clients.ShardClient", side_effect=[shard1, shard2, shard3]): with mock.patch("asyncio.gather", return_value=_helpers.AwaitableMock()): - gateway_manager_obj = gateway_manager.GatewayManager( + gateway_manager_obj = gateway_managers.GatewayManager( shard_ids=[0, 1, 2], shard_count=3, config=None, url="some_url", raw_event_consumer_impl=None, - shard_type=shard_client.ShardClient, + shard_type=shard_clients.ShardClient, ) await gateway_manager_obj.update_presence(status=None, activity=None, idle_since=None, is_afk=True) diff --git a/tests/hikari/internal/test_conversions.py b/tests/hikari/internal/test_conversions.py index 073e189496..ee79cbd47a 100644 --- a/tests/hikari/internal/test_conversions.py +++ b/tests/hikari/internal/test_conversions.py @@ -16,12 +16,16 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . +import concurrent.futures import datetime +import inspect import io import pytest +import typing from hikari.internal import conversions +from tests.hikari import _helpers @pytest.mark.parametrize( @@ -193,3 +197,81 @@ def test_parse_unix_epoch_to_datetime(): ) def test_make_resource_seekable(input_arg, expected_result_type): assert isinstance(conversions.make_resource_seekable(input_arg), expected_result_type) + + +@pytest.mark.parametrize( + ["count", "name", "kwargs", "expect"], + [ + (0, "foo", {}, "0 foos"), + (1, "foo", {}, "1 foo"), + (2, "foo", {}, "2 foos"), + (0, "foo", dict(suffix="es"), "0 fooes"), + (1, "foo", dict(suffix="es"), "1 foo"), + (2, "foo", dict(suffix="es"), "2 fooes"), + ], +) +def test_pluralize(count, name, kwargs, expect): + assert conversions.pluralize(count, name, **kwargs) == expect + + +class TestSnoopTypeHints: + def test_snoop_simple_local_scope(self): + x = object() + + frame = inspect.stack(1)[0][0] + try: + assert conversions.snoop_typehint_from_scope(frame, "x") is x + finally: + del frame + + def test_snoop_simple_global_scope(self): + frame = inspect.stack(1)[0][0] + try: + assert conversions.snoop_typehint_from_scope(frame, "pytest") is pytest + finally: + del frame + + # noinspection PyUnusedLocal + def test_snoop_nested_local_scope(self): + expected = object() + + class Foo: + class Bar: + class Baz: + class Bork: + qux = expected + + frame = inspect.stack(1)[0][0] + try: + assert conversions.snoop_typehint_from_scope(frame, "Foo.Bar.Baz.Bork.qux") is expected + finally: + del frame + + def test_snoop_nested_global_scope(self): + frame = inspect.stack(1)[0][0] + try: + assert ( + conversions.snoop_typehint_from_scope(frame, "concurrent.futures.as_completed") + is concurrent.futures.as_completed + ) + finally: + del frame + + def test_snoop_on_resolved_typehint_does_nothing(self): + frame = inspect.stack(1)[0][0] + try: + assert conversions.snoop_typehint_from_scope(frame, typing.Sequence) is typing.Sequence + finally: + del frame + + @_helpers.assert_raises(type_=NameError) + def test_not_resolved_is_failure(self): + attr = "this_is_not_an_attribute" + assert attr not in locals(), "change this attribute name to something else so the test can run" + assert attr not in globals(), "change this attribute name to something else so the test can run" + + frame = inspect.stack(1)[0][0] + try: + conversions.snoop_typehint_from_scope(frame, attr) + finally: + del frame From f827e713f0d7b3e6726fd9a275d9bca282b5e397 Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Fri, 10 Apr 2020 18:11:34 +0100 Subject: [PATCH 083/922] Updated entities docstrings to be < 80 chars, fixed deletion warning. --- hikari/entities.py | 4 ++-- hikari/internal/conversions.py | 21 +++++++++------------ 2 files changed, 11 insertions(+), 14 deletions(-) diff --git a/hikari/entities.py b/hikari/entities.py index a9a78524af..aa0568587a 100644 --- a/hikari/entities.py +++ b/hikari/entities.py @@ -69,7 +69,7 @@ def __init__(self, *args, **kwargs) -> None: class Deserializable: - """A mixin for any type that allows deserialization from a raw value into a Hikari entity.""" + """Mixin that enables the class to be deserialized from a raw entity.""" __slots__ = () @@ -86,7 +86,7 @@ def deserialize(cls: typing.Type[T_contra], payload: RawEntityT) -> T_contra: class Serializable: - """A mixin for any type that allows serialization from a Hikari entity into a raw value.""" + """Mixin that enables an instance of the class to be serialized.""" __slots__ = () diff --git a/hikari/internal/conversions.py b/hikari/internal/conversions.py index 58642ba0d8..b7d558e64c 100644 --- a/hikari/internal/conversions.py +++ b/hikari/internal/conversions.py @@ -329,18 +329,15 @@ def snoop_typehint_from_scope(frame: types.FrameType, typehint: typing.Union[str fragments = typehint.split(".") - try: - for scope in (frame.f_locals, frame.f_globals): - try: - scope = scope[fragments[0]] - for attr in fragments[1:]: - scope = getattr(scope, attr) - return scope - except (AttributeError, KeyError): - pass - raise NameError(f"No attribute {typehint} was found in enclosing scope") - finally: - del frame, scope # lgtm [py/unnecessary-delete] + for scope in (frame.f_locals, frame.f_globals): + try: + scope = scope[fragments[0]] + for attr in fragments[1:]: + scope = getattr(scope, attr) + return scope + except (AttributeError, KeyError): + pass + raise NameError(f"No attribute {typehint} was found in enclosing scope") BytesLikeT = typing.Union[bytes, bytearray, memoryview, str, io.StringIO, io.BytesIO] From 359b42f1e4b97e46e7f3089cb139a4391e711b4c Mon Sep 17 00:00:00 2001 From: davfsa Date: Fri, 10 Apr 2020 22:52:57 +0200 Subject: [PATCH 084/922] Add tests for shard_client --- hikari/clients/configs.py | 6 +- hikari/clients/shard_clients.py | 18 +- hikari/guilds.py | 2 +- tests/hikari/_helpers.py | 5 + ...ay_manager.py => test_gateway_managers.py} | 0 tests/hikari/clients/test_shard_clients.py | 336 ++++++++++++++++++ 6 files changed, 355 insertions(+), 12 deletions(-) rename tests/hikari/clients/{test_gateway_manager.py => test_gateway_managers.py} (100%) create mode 100644 tests/hikari/clients/test_shard_clients.py diff --git a/hikari/clients/configs.py b/hikari/clients/configs.py index 040e898fd8..57dc45773e 100644 --- a/hikari/clients/configs.py +++ b/hikari/clients/configs.py @@ -207,9 +207,11 @@ class WebsocketConfig(AIOHTTPConfig, TokenConfig, DebugConfig): #: The initial status to set the shards to when starting the gateway. #: - #: :type: :obj:`str` + #: :type: :obj:`hikari.guilds.PresenceStatus` initial_status: guilds.PresenceStatus = marshaller.attrib( - deserializer=guilds.PresenceStatus.__getitem__, if_undefined=lambda: "online", default="online", + deserializer=guilds.PresenceStatus.__getitem__, + if_undefined=lambda: guilds.PresenceStatus.ONLINE, + default=guilds.PresenceStatus.ONLINE, ) #: Whether to show up as AFK or not on sign-in. diff --git a/hikari/clients/shard_clients.py b/hikari/clients/shard_clients.py index 67983e3cdd..21d4a00cf8 100644 --- a/hikari/clients/shard_clients.py +++ b/hikari/clients/shard_clients.py @@ -43,7 +43,6 @@ from hikari import guilds from hikari.clients import configs from hikari.clients import runnable -from hikari.internal import more_asyncio from hikari.internal import more_logging from hikari.net import codes from hikari.net import ratelimits @@ -199,6 +198,7 @@ def shard_count(self) -> int: """ return self._connection.shard_count + # Ignore docstring not starting in an imperative mood @property def status(self) -> guilds.PresenceStatus: # noqa: D401 """Current user status for this shard. @@ -272,7 +272,7 @@ def heartbeat_interval(self) -> float: return self._connection.heartbeat_interval @property - def reconnect_count(self) -> float: + def reconnect_count(self) -> int: """Count of number of times the internal connection has reconnected. This includes RESUME and re-IDENTIFY events. @@ -315,7 +315,8 @@ async def start(self): async def join(self) -> None: """Wait for the shard to shut down fully.""" - await self._task if self._task is not None else more_asyncio.completed_future() + if self._task: + await self._task async def close(self) -> None: """Request that the shard shuts down. @@ -486,15 +487,14 @@ async def update_presence( Parameters ---------- status : :obj:`hikari.guilds.PresenceStatus` - The new status to set. + If specified, the new status to set. activity : :obj:`hikari.gateway_entities.GatewayActivity`, optional - The new activity to set. + If specified, the new activity to set. idle_since : :obj:`datetime.datetime`, optional - The time to show up as being idle since, or ``None`` if not - applicable. + If specified, the time to show up as being idle since, or + ``None`` if not applicable. is_afk : :obj:`bool` - ``True`` if the user should be marked as AFK, or ``False`` - otherwise. + If specified, whether the user should be marked as AFK. """ status = self._status if status is ... else status activity = self._activity if activity is ... else activity diff --git a/hikari/guilds.py b/hikari/guilds.py index bca33e241a..93ec9378b7 100644 --- a/hikari/guilds.py +++ b/hikari/guilds.py @@ -493,7 +493,7 @@ class PresenceActivity(entities.HikariEntity, entities.Deserializable): flags: ActivityFlag = marshaller.attrib(deserializer=ActivityFlag, if_undefined=None) -class PresenceStatus(enum.Enum): +class PresenceStatus(str, enum.Enum): """The status of a member.""" #: Online/green. diff --git a/tests/hikari/_helpers.py b/tests/hikari/_helpers.py index 58bb866939..5e185310e3 100644 --- a/tests/hikari/_helpers.py +++ b/tests/hikari/_helpers.py @@ -353,10 +353,15 @@ def __init__(self, return_value=None): self.await_count = 0 self.return_value = return_value + def _is_exception(self, obj): + return isinstance(obj, BaseException) or isinstance(obj, type) and issubclass(obj, BaseException) + def __await__(self): if False: yield self.await_count += 1 + if self._is_exception(self.return_value): + raise self.return_value return self.return_value def assert_awaited_once(self): diff --git a/tests/hikari/clients/test_gateway_manager.py b/tests/hikari/clients/test_gateway_managers.py similarity index 100% rename from tests/hikari/clients/test_gateway_manager.py rename to tests/hikari/clients/test_gateway_managers.py diff --git a/tests/hikari/clients/test_shard_clients.py b/tests/hikari/clients/test_shard_clients.py new file mode 100644 index 0000000000..43b352401a --- /dev/null +++ b/tests/hikari/clients/test_shard_clients.py @@ -0,0 +1,336 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# Copyright © Nekoka.tt 2019-2020 +# +# This file is part of Hikari. +# +# Hikari is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Hikari is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along ith Hikari. If not, see . +import datetime +import math +import asyncio +import aiohttp + +import cymock as mock +import pytest + +from hikari import guilds +from hikari import errors +from hikari.net import shard +from hikari.net import codes +from hikari.state import raw_event_consumers +from hikari.clients import shard_clients +from hikari.clients import configs +from tests.hikari import _helpers + + +class TestShardClient: + @pytest.fixture + def shard_client_obj(self): + mock_shard_connection = mock.MagicMock( + shard.ShardConnection, + heartbeat_latency=float("nan"), + heartbeat_interval=float("nan"), + reconnect_count=0, + seq=None, + session_id=None, + ) + with mock.patch("hikari.net.shard.ShardConnection", return_value=mock_shard_connection): + return _helpers.unslot_class(shard_clients.ShardClient)(0, 1, configs.WebsocketConfig(), None, "some_url") + + def _generate_mock_task(self, exception=None): + class Task(mock.MagicMock): + def __init__(self, exception): + super().__init__() + self._exception = exception + + def exception(self): + return self._exception + + return Task(exception) + + def test_raw_event_consumer_in_shardclient(self): + class DummyConsumer(raw_event_consumers.RawEventConsumer): + def process_raw_event(self, _client, name, payload): + return "ASSERT TRUE" + + shard_client_obj = shard_clients.ShardClient(0, 1, configs.WebsocketConfig(), DummyConsumer(), "some_url") + + assert shard_client_obj._connection.dispatch(shard_client_obj, "TEST", {}) == "ASSERT TRUE" + + def test_connection(self, shard_client_obj): + mock_shard_connection = mock.MagicMock(shard.ShardConnection) + + with mock.patch("hikari.net.shard.ShardConnection", return_value=mock_shard_connection): + shard_client_obj = shard_clients.ShardClient(0, 1, configs.WebsocketConfig(), None, "some_url") + + assert shard_client_obj.connection == mock_shard_connection + + def test_status(self, shard_client_obj): + assert shard_client_obj.status == guilds.PresenceStatus.ONLINE + + def test_activity(self, shard_client_obj): + assert shard_client_obj.activity is None + + def test_idle_since(self, shard_client_obj): + assert shard_client_obj.idle_since is None + + def test_is_afk(self, shard_client_obj): + assert shard_client_obj.is_afk is False + + def test_latency(self, shard_client_obj): + assert math.isnan(shard_client_obj.latency) + + def test_heartbeat_interval(self, shard_client_obj): + assert math.isnan(shard_client_obj.heartbeat_interval) + + def test_reconnect_count(self, shard_client_obj): + assert shard_client_obj.reconnect_count == 0 + + def test_connection_state(self, shard_client_obj): + assert shard_client_obj.connection_state == shard_clients.ShardState.NOT_RUNNING + + @pytest.mark.asyncio + async def test_start_when_ready_event_completes_first(self, shard_client_obj): + shard_client_obj._keep_alive = mock.AsyncMock() + task_mock = self._generate_mock_task() + + with mock.patch("asyncio.create_task", return_value=task_mock): + with mock.patch("asyncio.wait", return_value=([], None)): + await shard_client_obj.start() + + @_helpers.assert_raises(type_=RuntimeError) + @pytest.mark.asyncio + async def test_start_when_task_completes(self, shard_client_obj): + shard_client_obj._keep_alive = mock.AsyncMock() + task_mock = self._generate_mock_task(RuntimeError) + + with mock.patch("asyncio.create_task", return_value=task_mock): + with mock.patch("asyncio.wait", return_value=([task_mock], None)): + await shard_client_obj.start() + + @_helpers.assert_raises(type_=RuntimeError) + @pytest.mark.asyncio + async def test_start_when_already_started(self, shard_client_obj): + shard_client_obj._shard_state = shard_clients.ShardState.READY + + await shard_client_obj.start() + + @pytest.mark.asyncio + async def test_join_when__task(self, shard_client_obj): + shard_client_obj._task = _helpers.AwaitableMock() + + await shard_client_obj.join() + + shard_client_obj._task.assert_awaited_once() + + @pytest.mark.asyncio + async def test_join_when_not__task(self, shard_client_obj): + shard_client_obj._task = None + + await shard_client_obj.join() + + @pytest.mark.asyncio + async def test_close(self, shard_client_obj): + shard_client_obj._dispatch = _helpers.AwaitableMock() + shard_client_obj._task = _helpers.AwaitableMock() + + await shard_client_obj.close() + + shard_client_obj._connection.close.assert_called_once() + shard_client_obj._task.assert_awaited_once() + + @pytest.mark.asyncio + async def test_close_when_already_stopping(self, shard_client_obj): + shard_client_obj._shard_state = shard_clients.ShardState.STOPPING + + await shard_client_obj.close() + + shard_client_obj._connection.close.assert_not_called() + + @_helpers.assert_raises(type_=RuntimeError) + @pytest.mark.parametrize( + "error", + [ + None, + aiohttp.ClientConnectorError(mock.MagicMock(), mock.MagicMock()), + errors.GatewayZombiedError, + errors.GatewayInvalidSessionError(False), + errors.GatewayInvalidSessionError(True), + errors.GatewayMustReconnectError, + ], + ) + @pytest.mark.asyncio + async def test__keep_alive_handles_errors(self, error, shard_client_obj): + should_return = False + + def side_effect(*args): + nonlocal should_return + if should_return: + return _helpers.AwaitableMock(return_value=RuntimeError) + + should_return = True + return _helpers.AwaitableMock(return_value=error) + + shard_client_obj._spin_up = mock.AsyncMock(side_effect=side_effect) + + with mock.patch("asyncio.sleep", new=mock.AsyncMock()): + await shard_client_obj._keep_alive() + + @pytest.mark.asyncio + async def test__keep_alive_shuts_down_when_GatewayClientClosedError(self, shard_client_obj): + shard_client_obj._spin_up = mock.AsyncMock( + return_value=_helpers.AwaitableMock(return_value=errors.GatewayClientClosedError) + ) + + with mock.patch("asyncio.sleep", new=mock.AsyncMock()): + await shard_client_obj._keep_alive() + + @_helpers.assert_raises(type_=errors.GatewayServerClosedConnectionError) + @pytest.mark.parametrize( + "code", + [ + codes.GatewayCloseCode.NOT_AUTHENTICATED, + codes.GatewayCloseCode.AUTHENTICATION_FAILED, + codes.GatewayCloseCode.ALREADY_AUTHENTICATED, + codes.GatewayCloseCode.SHARDING_REQUIRED, + codes.GatewayCloseCode.INVALID_VERSION, + codes.GatewayCloseCode.INVALID_INTENT, + codes.GatewayCloseCode.DISALLOWED_INTENT, + ], + ) + @pytest.mark.asyncio + async def test__keep_alive_shuts_down_when_GatewayServerClosedConnectionError(self, code, shard_client_obj): + shard_client_obj._spin_up = mock.AsyncMock( + return_value=_helpers.AwaitableMock(return_value=errors.GatewayServerClosedConnectionError(code)) + ) + + with mock.patch("asyncio.sleep", new=mock.AsyncMock()): + await shard_client_obj._keep_alive() + + @_helpers.assert_raises(type_=RuntimeError) + @pytest.mark.asyncio + async def test__keep_alive_ignores_when_GatewayServerClosedConnectionError_with_other_code(self, shard_client_obj): + should_return = False + + def side_effect(*args): + nonlocal should_return + if should_return: + return _helpers.AwaitableMock(return_value=RuntimeError) + + should_return = True + return _helpers.AwaitableMock( + return_value=errors.GatewayServerClosedConnectionError(codes.GatewayCloseCode.NORMAL_CLOSURE) + ) + + shard_client_obj._spin_up = mock.AsyncMock(side_effect=side_effect) + + with mock.patch("asyncio.sleep", new=mock.AsyncMock()): + await shard_client_obj._keep_alive() + + @_helpers.assert_raises(type_=RuntimeError) + @pytest.mark.asyncio + async def test__spin_up_if_connect_task_is_completed_raises_exception_during_hello_event(self, shard_client_obj): + task_mock = self._generate_mock_task(RuntimeError) + + with mock.patch("asyncio.create_task", return_value=task_mock): + with mock.patch("asyncio.wait", return_value=([task_mock], None)): + await shard_client_obj._spin_up() + + @_helpers.assert_raises(type_=RuntimeError) + @pytest.mark.asyncio + async def test__spin_up_if_connect_task_is_completed_raises_exception_during_identify_event(self, shard_client_obj): + task_mock = self._generate_mock_task(RuntimeError) + + with mock.patch("asyncio.create_task", return_value=task_mock): + with mock.patch("asyncio.wait", side_effect=[([], None), ([task_mock], None)]): + await shard_client_obj._spin_up() + + @pytest.mark.asyncio + async def test__spin_up_when_resuming(self, shard_client_obj): + shard_client_obj._connection.seq = 123 + shard_client_obj._connection.session_id = 123 + task_mock = self._generate_mock_task() + + with mock.patch("asyncio.create_task", return_value=task_mock): + with mock.patch("asyncio.wait", side_effect=[([], None), ([], None), ([], None)]): + assert await shard_client_obj._spin_up() == task_mock + + @_helpers.assert_raises(type_=RuntimeError) + @pytest.mark.asyncio + async def test__spin_up_if_connect_task_is_completed_raises_exception_during_resumed_event(self, shard_client_obj): + shard_client_obj._connection.seq = 123 + shard_client_obj._connection.session_id = 123 + task_mock = self._generate_mock_task(RuntimeError) + + with mock.patch("asyncio.create_task", return_value=task_mock): + with mock.patch("asyncio.wait", side_effect=[([], None), ([], None), ([task_mock], None)]): + await shard_client_obj._spin_up() + + @pytest.mark.asyncio + async def test__spin_up_when_not_resuming(self, shard_client_obj): + task_mock = self._generate_mock_task() + + with mock.patch("asyncio.create_task", return_value=task_mock): + with mock.patch("asyncio.wait", side_effect=[([], None), ([], None), ([], None)]): + assert await shard_client_obj._spin_up() == task_mock + + @_helpers.assert_raises(type_=RuntimeError) + @pytest.mark.asyncio + async def test__spin_up_if_connect_task_is_completed_raises_exception_during_ready_event(self, shard_client_obj): + task_mock = self._generate_mock_task(RuntimeError) + + with mock.patch("asyncio.create_task", return_value=task_mock): + with mock.patch("asyncio.wait", side_effect=[([], None), ([], None), ([task_mock], None)]): + await shard_client_obj._spin_up() + + @pytest.mark.asyncio + async def test_update_presence(self, shard_client_obj): + await shard_client_obj.update_presence() + + shard_client_obj._connection.update_presence.assert_called_once_with( + {"status": "online", "game": None, "idle_since": None, "afk": False} + ) + + assert shard_client_obj._status == guilds.PresenceStatus.ONLINE + assert shard_client_obj._activity == None + assert shard_client_obj._idle_since == None + assert shard_client_obj._is_afk is False + + @pytest.mark.asyncio + async def test_update_presence_with_optionals(self, shard_client_obj): + datetime_obj = datetime.datetime.now() + + await shard_client_obj.update_presence( + status=guilds.PresenceStatus.DND, activity=None, idle_since=datetime_obj, is_afk=True + ) + + shard_client_obj._connection.update_presence.assert_called_once_with( + {"status": "dnd", "game": None, "idle_since": datetime_obj.timestamp(), "afk": True} + ) + + assert shard_client_obj._status == guilds.PresenceStatus.DND + assert shard_client_obj._activity == None + assert shard_client_obj._idle_since == datetime_obj + assert shard_client_obj._is_afk is True + + def test__create_presence_pl(self, shard_client_obj): + datetime_obj = datetime.datetime.now() + returned = shard_client_obj._create_presence_pl(guilds.PresenceStatus.DND, None, datetime_obj, True) + + assert returned == { + "status": "dnd", + "game": None, + "idle_since": datetime_obj.timestamp(), + "afk": True, + } From a8a983fcaa233cd67130d85dea4ba33985c717f6 Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Sat, 11 Apr 2020 17:35:24 +0100 Subject: [PATCH 085/922] Removed need for typeshed hacks for PyCharm to show the correct signature. This just makes the @attr.s decorator a requirement for all models. Pycharm is dumb enough to think that this is a magical fix for everything and that it is sunshine and roses and suddenly my autocomplete isn't fundementally fubared, so yay for me, I guess. --- hikari/audit_logs.py | 35 ++++-- hikari/channels.py | 35 ++++-- hikari/clients/configs.py | 42 +++---- hikari/embeds.py | 26 ++-- hikari/emojis.py | 14 ++- hikari/entities.py | 18 +-- hikari/events.py | 117 ++++++++++++------ hikari/gateway_entities.py | 11 +- hikari/guilds.py | 56 ++++++--- hikari/internal/marshaller.py | 24 +--- hikari/internal/more_collections.py | 1 + hikari/invites.py | 18 ++- hikari/messages.py | 23 ++-- hikari/oauth2.py | 20 ++- hikari/snowflakes.py | 9 +- hikari/users.py | 8 +- hikari/voices.py | 8 +- hikari/webhooks.py | 5 +- tests/hikari/internal/test_marshaller.py | 61 +++++---- .../hikari/internal/test_marshaller_pep563.py | 61 +++++---- tests/hikari/net/test_rest.py | 18 ++- 21 files changed, 379 insertions(+), 231 deletions(-) diff --git a/hikari/audit_logs.py b/hikari/audit_logs.py index 2e4fa8ff1b..43cde2b121 100644 --- a/hikari/audit_logs.py +++ b/hikari/audit_logs.py @@ -44,6 +44,8 @@ import enum import typing +import attr + from hikari import channels from hikari import colors from hikari import entities @@ -162,7 +164,8 @@ def __str__(self) -> str: } -@marshaller.attrs(slots=True) +@marshaller.marshallable() +@attr.s(slots=True) class AuditLogChange(entities.HikariEntity, entities.Deserializable): """Represents a change made to an audit log entry's target entity.""" @@ -266,7 +269,8 @@ def decorator(cls): return decorator -@marshaller.attrs(slots=True) +@marshaller.marshallable() +@attr.s(slots=True) class BaseAuditLogEntryInfo(abc.ABC, entities.HikariEntity, entities.Deserializable): """A base object that all audit log entry info objects will inherit from.""" @@ -276,7 +280,8 @@ class BaseAuditLogEntryInfo(abc.ABC, entities.HikariEntity, entities.Deserializa AuditLogEventType.CHANNEL_OVERWRITE_UPDATE, AuditLogEventType.CHANNEL_OVERWRITE_DELETE, ) -@marshaller.attrs(slots=True) +@marshaller.marshallable() +@attr.s(slots=True) class ChannelOverwriteEntryInfo(BaseAuditLogEntryInfo): """Represents the extra information for overwrite related audit log entries. @@ -302,7 +307,8 @@ class ChannelOverwriteEntryInfo(BaseAuditLogEntryInfo): @register_audit_log_entry_info(AuditLogEventType.MESSAGE_PIN, AuditLogEventType.MESSAGE_UNPIN) -@marshaller.attrs(slots=True) +@marshaller.marshallable() +@attr.s(slots=True) class MessagePinEntryInfo(BaseAuditLogEntryInfo): """The extra information for message pin related audit log entries. @@ -322,7 +328,8 @@ class MessagePinEntryInfo(BaseAuditLogEntryInfo): @register_audit_log_entry_info(AuditLogEventType.MEMBER_PRUNE) -@marshaller.attrs(slots=True) +@marshaller.marshallable() +@attr.s(slots=True) class MemberPruneEntryInfo(BaseAuditLogEntryInfo): """Represents the extra information attached to guild prune log entries.""" @@ -341,7 +348,8 @@ class MemberPruneEntryInfo(BaseAuditLogEntryInfo): @register_audit_log_entry_info(AuditLogEventType.MESSAGE_BULK_DELETE) -@marshaller.attrs(slots=True) +@marshaller.marshallable() +@attr.s(slots=True) class MessageBulkDeleteEntryInfo(BaseAuditLogEntryInfo): """Represents extra information for the message bulk delete audit entry.""" @@ -352,7 +360,8 @@ class MessageBulkDeleteEntryInfo(BaseAuditLogEntryInfo): @register_audit_log_entry_info(AuditLogEventType.MESSAGE_DELETE) -@marshaller.attrs(slots=True) +@marshaller.marshallable() +@attr.s(slots=True) class MessageDeleteEntryInfo(MessageBulkDeleteEntryInfo): """Represents extra information attached to the message delete audit entry.""" @@ -363,7 +372,8 @@ class MessageDeleteEntryInfo(MessageBulkDeleteEntryInfo): @register_audit_log_entry_info(AuditLogEventType.MEMBER_DISCONNECT) -@marshaller.attrs(slots=True) +@marshaller.marshallable() +@attr.s(slots=True) class MemberDisconnectEntryInfo(BaseAuditLogEntryInfo): """Represents extra information for the voice chat member disconnect entry.""" @@ -374,7 +384,8 @@ class MemberDisconnectEntryInfo(BaseAuditLogEntryInfo): @register_audit_log_entry_info(AuditLogEventType.MEMBER_MOVE) -@marshaller.attrs(slots=True) +@marshaller.marshallable() +@attr.s(slots=True) class MemberMoveEntryInfo(MemberDisconnectEntryInfo): """Represents extra information for the voice chat based member move entry.""" @@ -412,7 +423,8 @@ def get_entry_info_entity(type_: int) -> typing.Type[BaseAuditLogEntryInfo]: return register_audit_log_entry_info.types.get(type_) or UnrecognisedAuditLogEntryInfo -@marshaller.attrs(slots=True) +@marshaller.marshallable() +@attr.s(slots=True) class AuditLogEntry(snowflakes.UniqueEntity, entities.Deserializable): """Represents an entry in a guild's audit log.""" @@ -477,7 +489,8 @@ def deserialize(cls, payload: entities.RawEntityT) -> "AuditLogEntry": ) -@marshaller.attrs(slots=True) +@marshaller.marshallable() +@attr.s(slots=True) class AuditLog(entities.HikariEntity, entities.Deserializable): """Represents a guilds audit log.""" diff --git a/hikari/channels.py b/hikari/channels.py index 2b798bb314..208821e971 100644 --- a/hikari/channels.py +++ b/hikari/channels.py @@ -47,6 +47,8 @@ import enum import typing +import attr + from hikari import entities from hikari import permissions from hikari import snowflakes @@ -85,7 +87,8 @@ class PermissionOverwriteType(str, enum.Enum): MEMBER = "member" -@marshaller.attrs(slots=True) +@marshaller.marshallable() +@attr.s(slots=True) class PermissionOverwrite(snowflakes.UniqueEntity, entities.Deserializable, entities.Serializable): """Represents permission overwrites for a channel or role in a channel.""" @@ -135,7 +138,8 @@ def decorator(cls): return decorator -@marshaller.attrs(slots=True) +@marshaller.marshallable() +@attr.s(slots=True) class Channel(snowflakes.UniqueEntity, entities.Deserializable): """Base class for all channels.""" @@ -145,7 +149,8 @@ class Channel(snowflakes.UniqueEntity, entities.Deserializable): type: ChannelType = marshaller.attrib(deserializer=ChannelType) -@marshaller.attrs(slots=True) +@marshaller.marshallable() +@attr.s(slots=True) class PartialChannel(Channel): """Represents a channel where we've only received it's basic information. @@ -159,7 +164,8 @@ class PartialChannel(Channel): @register_channel_type(ChannelType.DM) -@marshaller.attrs(slots=True) +@marshaller.marshallable() +@attr.s(slots=True) class DMChannel(Channel): """Represents a DM channel.""" @@ -184,7 +190,8 @@ class DMChannel(Channel): @register_channel_type(ChannelType.GROUP_DM) -@marshaller.attrs(slots=True) +@marshaller.marshallable() +@attr.s(slots=True) class GroupDMChannel(DMChannel): """Represents a DM group channel.""" @@ -212,7 +219,8 @@ class GroupDMChannel(DMChannel): ) -@marshaller.attrs(slots=True) +@marshaller.marshallable() +@attr.s(slots=True) class GuildChannel(Channel): """The base for anything that is a guild channel.""" @@ -250,13 +258,15 @@ class GuildChannel(Channel): @register_channel_type(ChannelType.GUILD_CATEGORY) -@marshaller.attrs(slots=True) +@marshaller.marshallable() +@attr.s(slots=True) class GuildCategory(GuildChannel): """Represents a guild category.""" @register_channel_type(ChannelType.GUILD_TEXT) -@marshaller.attrs(slots=True) +@marshaller.marshallable() +@attr.s(slots=True) class GuildTextChannel(GuildChannel): """Represents a guild text channel.""" @@ -292,7 +302,8 @@ class GuildTextChannel(GuildChannel): @register_channel_type(ChannelType.GUILD_NEWS) -@marshaller.attrs(slots=True) +@marshaller.marshallable() +@attr.s(slots=True) class GuildNewsChannel(GuildChannel): """Represents an news channel.""" @@ -315,13 +326,15 @@ class GuildNewsChannel(GuildChannel): @register_channel_type(ChannelType.GUILD_STORE) -@marshaller.attrs(slots=True) +@marshaller.marshallable() +@attr.s(slots=True) class GuildStoreChannel(GuildChannel): """Represents a store channel.""" @register_channel_type(ChannelType.GUILD_VOICE) -@marshaller.attrs(slots=True) +@marshaller.marshallable() +@attr.s(slots=True) class GuildVoiceChannel(GuildChannel): """Represents an voice channel.""" diff --git a/hikari/clients/configs.py b/hikari/clients/configs.py index 57dc45773e..3576f15581 100644 --- a/hikari/clients/configs.py +++ b/hikari/clients/configs.py @@ -19,7 +19,6 @@ """Configuration data classes.""" __all__ = [ - "generate_config_attrs", "BaseConfig", "DebugConfig", "AIOHTTPConfig", @@ -36,6 +35,7 @@ import typing import aiohttp +import attr from hikari import entities from hikari import gateway_entities @@ -45,28 +45,14 @@ from hikari.net import codes +@marshaller.marshallable() +@attr.s(kw_only=True) class BaseConfig(entities.Deserializable): """Base class for any configuration data class.""" - if typing.TYPE_CHECKING: - # pylint:disable=unused-argument - # Screws up PyCharm and makes annoying warnings everywhere, so just - # mute this. We can always make dummy constructors later, or find - # another way around this perhaps. - # This only ever takes kwargs. - @typing.no_type_check - def __init__(self, **kwargs) -> None: - ... - # pylint:enable=unused-argument - - -#: Decorator for :obj:`attr.s` classes that use the -#: :obj:`hikari.internal.marshaller` protocol. -generate_config_attrs = marshaller.attrs(kw_only=True) - - -@generate_config_attrs +@marshaller.marshallable() +@attr.s(kw_only=True) class DebugConfig(BaseConfig): """Configuration for anything with a debugging mode.""" @@ -76,7 +62,8 @@ class DebugConfig(BaseConfig): debug: bool = marshaller.attrib(deserializer=bool, if_undefined=False, default=False) -@generate_config_attrs +@marshaller.marshallable() +@attr.s(kw_only=True) class AIOHTTPConfig(BaseConfig): """Config for components that use AIOHTTP somewhere.""" @@ -170,7 +157,8 @@ class AIOHTTPConfig(BaseConfig): verify_ssl: bool = marshaller.attrib(deserializer=bool, if_undefined=True, default=True) -@generate_config_attrs +@marshaller.marshallable() +@attr.s(kw_only=True) class TokenConfig(BaseConfig): """Token config options.""" @@ -180,7 +168,8 @@ class TokenConfig(BaseConfig): token: typing.Optional[str] = marshaller.attrib(deserializer=str, if_none=None, if_undefined=None, default=None) -@generate_config_attrs +@marshaller.marshallable() +@attr.s(kw_only=True) class WebsocketConfig(AIOHTTPConfig, TokenConfig, DebugConfig): """Single-websocket specific configuration options.""" @@ -293,7 +282,8 @@ def _parse_shard_info(payload): return [*range(minimum, maximum)] -@generate_config_attrs +@marshaller.marshallable() +@attr.s(kw_only=True) class ShardConfig(BaseConfig): """Definition of shard management configuration settings.""" @@ -330,7 +320,8 @@ class ShardConfig(BaseConfig): shard_count: typing.Optional[int] = marshaller.attrib(deserializer=int, if_undefined=None, default=None) -@generate_config_attrs +@marshaller.marshallable() +@attr.s(kw_only=True) class RESTConfig(AIOHTTPConfig, TokenConfig): """REST-specific configuration details.""" @@ -342,6 +333,7 @@ class RESTConfig(AIOHTTPConfig, TokenConfig): rest_version: int = marshaller.attrib(deserializer=int, if_undefined=lambda: 7, default=7) -@generate_config_attrs +@marshaller.marshallable() +@attr.s(kw_only=True) class BotConfig(RESTConfig, ShardConfig, WebsocketConfig): """Configuration for a standard bot.""" diff --git a/hikari/embeds.py b/hikari/embeds.py index defb5349d7..249a73789d 100644 --- a/hikari/embeds.py +++ b/hikari/embeds.py @@ -31,13 +31,16 @@ import datetime import typing +import attr + import hikari.internal.conversions from hikari import colors from hikari import entities from hikari.internal import marshaller -@marshaller.attrs(slots=True) +@marshaller.marshallable() +@attr.s(slots=True) class EmbedFooter(entities.HikariEntity, entities.Deserializable, entities.Serializable): """Represents a embed footer.""" @@ -57,7 +60,8 @@ class EmbedFooter(entities.HikariEntity, entities.Deserializable, entities.Seria proxy_icon_url: typing.Optional[str] = marshaller.attrib(deserializer=str, serializer=str, if_undefined=None) -@marshaller.attrs(slots=True) +@marshaller.marshallable() +@attr.s(slots=True) class EmbedImage(entities.HikariEntity, entities.Deserializable, entities.Serializable): """Represents a embed image.""" @@ -82,7 +86,8 @@ class EmbedImage(entities.HikariEntity, entities.Deserializable, entities.Serial width: typing.Optional[int] = marshaller.attrib(deserializer=int, serializer=int, if_undefined=None) -@marshaller.attrs(slots=True) +@marshaller.marshallable() +@attr.s(slots=True) class EmbedThumbnail(entities.HikariEntity, entities.Deserializable, entities.Serializable): """Represents a embed thumbnail.""" @@ -107,7 +112,8 @@ class EmbedThumbnail(entities.HikariEntity, entities.Deserializable, entities.Se width: typing.Optional[int] = marshaller.attrib(deserializer=int, serializer=int, if_undefined=None) -@marshaller.attrs(slots=True) +@marshaller.marshallable() +@attr.s(slots=True) class EmbedVideo(entities.HikariEntity, entities.Deserializable, entities.Serializable): """Represents a embed video.""" @@ -127,7 +133,8 @@ class EmbedVideo(entities.HikariEntity, entities.Deserializable, entities.Serial width: typing.Optional[int] = marshaller.attrib(deserializer=int, serializer=int, if_undefined=None) -@marshaller.attrs(slots=True) +@marshaller.marshallable() +@attr.s(slots=True) class EmbedProvider(entities.HikariEntity, entities.Deserializable, entities.Serializable): """Represents a embed provider.""" @@ -142,7 +149,8 @@ class EmbedProvider(entities.HikariEntity, entities.Deserializable, entities.Ser url: typing.Optional[str] = marshaller.attrib(deserializer=str, serializer=str, if_undefined=None, if_none=None) -@marshaller.attrs(slots=True) +@marshaller.marshallable() +@attr.s(slots=True) class EmbedAuthor(entities.HikariEntity, entities.Deserializable, entities.Serializable): """Represents a embed author.""" @@ -167,7 +175,8 @@ class EmbedAuthor(entities.HikariEntity, entities.Deserializable, entities.Seria proxy_icon_url: typing.Optional[str] = marshaller.attrib(deserializer=str, serializer=str, if_undefined=None) -@marshaller.attrs(slots=True) +@marshaller.marshallable() +@attr.s(slots=True) class EmbedField(entities.HikariEntity, entities.Deserializable, entities.Serializable): """Represents a field in a embed.""" @@ -187,7 +196,8 @@ class EmbedField(entities.HikariEntity, entities.Deserializable, entities.Serial is_inline: bool = marshaller.attrib(raw_name="inline", deserializer=bool, serializer=bool, if_undefined=False) -@marshaller.attrs(slots=True) +@marshaller.marshallable() +@attr.s(slots=True) class Embed(entities.HikariEntity, entities.Deserializable, entities.Serializable): """Represents a embed.""" diff --git a/hikari/emojis.py b/hikari/emojis.py index 7ce4444330..0e531b16c3 100644 --- a/hikari/emojis.py +++ b/hikari/emojis.py @@ -19,6 +19,8 @@ """Components and entities that are used to describe both custom and Unicode emojis on Discord.""" import typing +import attr + from hikari import entities from hikari import snowflakes from hikari import users @@ -27,12 +29,14 @@ __all__ = ["Emoji", "UnicodeEmoji", "UnknownEmoji", "GuildEmoji"] -@marshaller.attrs(slots=True) +@marshaller.marshallable() +@attr.s(slots=True) class Emoji(entities.HikariEntity, entities.Deserializable): """Base class for all emojis.""" -@marshaller.attrs(slots=True) +@marshaller.marshallable() +@attr.s(slots=True) class UnicodeEmoji(Emoji): """Represents a unicode emoji.""" @@ -42,7 +46,8 @@ class UnicodeEmoji(Emoji): name: str = marshaller.attrib(deserializer=str) -@marshaller.attrs(slots=True) +@marshaller.marshallable() +@attr.s(slots=True) class UnknownEmoji(Emoji, snowflakes.UniqueEntity): """Represents a unknown emoji.""" @@ -57,7 +62,8 @@ class UnknownEmoji(Emoji, snowflakes.UniqueEntity): is_animated: bool = marshaller.attrib(raw_name="animated", deserializer=bool, if_undefined=False) -@marshaller.attrs(slots=True) +@marshaller.marshallable() +@attr.s(slots=True) class GuildEmoji(UnknownEmoji): """Represents a guild emoji.""" diff --git a/hikari/entities.py b/hikari/entities.py index aa0568587a..77c261a753 100644 --- a/hikari/entities.py +++ b/hikari/entities.py @@ -22,6 +22,8 @@ import abc import typing +import attr + from hikari.internal import marshaller from hikari.internal import meta @@ -50,23 +52,11 @@ def __repr__(self): UNSET = Unset() -@marshaller.attrs(slots=True) +@marshaller.marshallable() +@attr.s(slots=True) class HikariEntity(metaclass=abc.ABCMeta): """The base for any entity used in this API.""" - __slots__ = () - - if typing.TYPE_CHECKING: - # pylint:disable=unused-argument - # Screws up PyCharm and makes annoying warnings everywhere, so just - # mute this. We can always make dummy constructors later, or find - # another way around this perhaps. - @typing.no_type_check - def __init__(self, *args, **kwargs) -> None: - ... - - # pylint:enable=unused-argument - class Deserializable: """Mixin that enables the class to be deserialized from a raw entity.""" diff --git a/hikari/events.py b/hikari/events.py index 03be0f7cbd..9199fbeee2 100644 --- a/hikari/events.py +++ b/hikari/events.py @@ -91,7 +91,8 @@ # Base event, is not deserialized -@marshaller.attrs(slots=True) +@marshaller.marshallable() +@attr.s(slots=True) class HikariEvent(entities.HikariEntity): """The base class that all events inherit from.""" @@ -177,7 +178,8 @@ class DisconnectedEvent(HikariEvent, entities.Deserializable): @mark_as_websocket_event -@marshaller.attrs(slots=True) +@marshaller.marshallable() +@attr.s(slots=True) class ReadyEvent(HikariEvent, entities.Deserializable): """Used to represent the gateway ready event. @@ -233,12 +235,14 @@ def shard_count(self) -> typing.Optional[int]: @mark_as_websocket_event -@marshaller.attrs(slots=True) +@marshaller.marshallable() +@attr.s(slots=True) class ResumedEvent(HikariEvent): """Represents a gateway Resume event.""" -@marshaller.attrs(slots=True) +@marshaller.marshallable() +@attr.s(slots=True) class BaseChannelEvent(HikariEvent, snowflakes.UniqueEntity, entities.Deserializable): """A base object that Channel events will inherit from.""" @@ -357,7 +361,8 @@ class BaseChannelEvent(HikariEvent, snowflakes.UniqueEntity, entities.Deserializ @mark_as_websocket_event -@marshaller.attrs(slots=True) +@marshaller.marshallable() +@attr.s(slots=True) class ChannelCreateEvent(BaseChannelEvent): """Represents Channel Create gateway events. @@ -367,19 +372,22 @@ class ChannelCreateEvent(BaseChannelEvent): @mark_as_websocket_event -@marshaller.attrs(slots=True) +@marshaller.marshallable() +@attr.s(slots=True) class ChannelUpdateEvent(BaseChannelEvent): """Represents Channel Update gateway events.""" @mark_as_websocket_event -@marshaller.attrs(slots=True) +@marshaller.marshallable() +@attr.s(slots=True) class ChannelDeleteEvent(BaseChannelEvent): """Represents Channel Delete gateway events.""" @mark_as_websocket_event -@marshaller.attrs(slots=True) +@marshaller.marshallable() +@attr.s(slots=True) class ChannelPinUpdateEvent(HikariEvent, entities.Deserializable): """Used to represent the Channel Pins Update gateway event. @@ -410,7 +418,8 @@ class ChannelPinUpdateEvent(HikariEvent, entities.Deserializable): @mark_as_websocket_event -@marshaller.attrs(slots=True) +@marshaller.marshallable() +@attr.s(slots=True) class GuildCreateEvent(HikariEvent, entities.Deserializable): """Used to represent Guild Create gateway events. @@ -420,13 +429,15 @@ class GuildCreateEvent(HikariEvent, entities.Deserializable): @mark_as_websocket_event -@marshaller.attrs(slots=True) +@marshaller.marshallable() +@attr.s(slots=True) class GuildUpdateEvent(HikariEvent, entities.Deserializable): """Used to represent Guild Update gateway events.""" @mark_as_websocket_event -@marshaller.attrs(slots=True) +@marshaller.marshallable() +@attr.s(slots=True) class GuildLeaveEvent(HikariEvent, snowflakes.UniqueEntity, entities.Deserializable): """Fired when the current user leaves the guild or is kicked/banned from it. @@ -437,7 +448,8 @@ class GuildLeaveEvent(HikariEvent, snowflakes.UniqueEntity, entities.Deserializa @mark_as_websocket_event -@marshaller.attrs(slots=True) +@marshaller.marshallable() +@attr.s(slots=True) class GuildUnavailableEvent(HikariEvent, snowflakes.UniqueEntity, entities.Deserializable): """Fired when a guild becomes temporarily unavailable due to an outage. @@ -447,7 +459,8 @@ class GuildUnavailableEvent(HikariEvent, snowflakes.UniqueEntity, entities.Deser """ -@marshaller.attrs(slots=True) +@marshaller.marshallable() +@attr.s(slots=True) class BaseGuildBanEvent(HikariEvent, entities.Deserializable): """A base object that guild ban events will inherit from.""" @@ -463,19 +476,22 @@ class BaseGuildBanEvent(HikariEvent, entities.Deserializable): @mark_as_websocket_event -@marshaller.attrs(slots=True) +@marshaller.marshallable() +@attr.s(slots=True) class GuildBanAddEvent(BaseGuildBanEvent): """Used to represent a Guild Ban Add gateway event.""" @mark_as_websocket_event -@marshaller.attrs(slots=True) +@marshaller.marshallable() +@attr.s(slots=True) class GuildBanRemoveEvent(BaseGuildBanEvent): """Used to represent a Guild Ban Remove gateway event.""" @mark_as_websocket_event -@marshaller.attrs(slots=True) +@marshaller.marshallable() +@attr.s(slots=True) class GuildEmojisUpdateEvent(HikariEvent, entities.Deserializable): """Represents a Guild Emoji Update gateway event.""" @@ -493,7 +509,8 @@ class GuildEmojisUpdateEvent(HikariEvent, entities.Deserializable): @mark_as_websocket_event -@marshaller.attrs(slots=True) +@marshaller.marshallable() +@attr.s(slots=True) class GuildIntegrationsUpdateEvent(HikariEvent, entities.Deserializable): """Used to represent Guild Integration Update gateway events.""" @@ -504,7 +521,8 @@ class GuildIntegrationsUpdateEvent(HikariEvent, entities.Deserializable): @mark_as_websocket_event -@marshaller.attrs(slots=True) +@marshaller.marshallable() +@attr.s(slots=True) class GuildMemberAddEvent(HikariEvent, guilds.GuildMember): """Used to represent a Guild Member Add gateway event.""" @@ -515,7 +533,8 @@ class GuildMemberAddEvent(HikariEvent, guilds.GuildMember): @mark_as_websocket_event -@marshaller.attrs(slots=True) +@marshaller.marshallable() +@attr.s(slots=True) class GuildMemberRemoveEvent(HikariEvent, entities.Deserializable): """Used to represent Guild Member Remove gateway events. @@ -534,7 +553,8 @@ class GuildMemberRemoveEvent(HikariEvent, entities.Deserializable): @mark_as_websocket_event -@marshaller.attrs(slots=True) +@marshaller.marshallable() +@attr.s(slots=True) class GuildMemberUpdateEvent(HikariEvent, entities.Deserializable): """Used to represent a Guild Member Update gateway event. @@ -576,7 +596,8 @@ class GuildMemberUpdateEvent(HikariEvent, entities.Deserializable): @mark_as_websocket_event -@marshaller.attrs(slots=True) +@marshaller.marshallable() +@attr.s(slots=True) class GuildRoleCreateEvent(HikariEvent, entities.Deserializable): """Used to represent a Guild Role Create gateway event.""" @@ -592,7 +613,8 @@ class GuildRoleCreateEvent(HikariEvent, entities.Deserializable): @mark_as_websocket_event -@marshaller.attrs(slots=True) +@marshaller.marshallable() +@attr.s(slots=True) class GuildRoleUpdateEvent(HikariEvent, entities.Deserializable): """Used to represent a Guild Role Create gateway event.""" @@ -608,7 +630,8 @@ class GuildRoleUpdateEvent(HikariEvent, entities.Deserializable): @mark_as_websocket_event -@marshaller.attrs(slots=True) +@marshaller.marshallable() +@attr.s(slots=True) class GuildRoleDeleteEvent(HikariEvent, entities.Deserializable): """Represents a gateway Guild Role Delete Event.""" @@ -624,7 +647,8 @@ class GuildRoleDeleteEvent(HikariEvent, entities.Deserializable): @mark_as_websocket_event -@marshaller.attrs(slots=True) +@marshaller.marshallable() +@attr.s(slots=True) class InviteCreateEvent(HikariEvent, entities.Deserializable): """Represents a gateway Invite Create event.""" @@ -694,7 +718,8 @@ class InviteCreateEvent(HikariEvent, entities.Deserializable): @mark_as_websocket_event -@marshaller.attrs(slots=True) +@marshaller.marshallable() +@attr.s(slots=True) class InviteDeleteEvent(HikariEvent, entities.Deserializable): """Used to represent Invite Delete gateway events. @@ -721,14 +746,16 @@ class InviteDeleteEvent(HikariEvent, entities.Deserializable): @mark_as_websocket_event -@marshaller.attrs(slots=True) +@marshaller.marshallable() +@attr.s(slots=True) class MessageCreateEvent(HikariEvent, messages.Message): """Used to represent Message Create gateway events.""" # This is an arbitrarily partial version of `messages.Message` @mark_as_websocket_event -@marshaller.attrs(slots=True) +@marshaller.marshallable() +@attr.s(slots=True) class MessageUpdateEvent(HikariEvent, snowflakes.UniqueEntity, entities.Deserializable): """Represents Message Update gateway events. @@ -900,7 +927,8 @@ class MessageUpdateEvent(HikariEvent, snowflakes.UniqueEntity, entities.Deserial @mark_as_websocket_event -@marshaller.attrs(slots=True) +@marshaller.marshallable() +@attr.s(slots=True) class MessageDeleteEvent(HikariEvent, entities.Deserializable): """Used to represent Message Delete gateway events. @@ -926,7 +954,8 @@ class MessageDeleteEvent(HikariEvent, entities.Deserializable): @mark_as_websocket_event -@marshaller.attrs(slots=True) +@marshaller.marshallable() +@attr.s(slots=True) class MessageDeleteBulkEvent(HikariEvent, entities.Deserializable): """Used to represent Message Bulk Delete gateway events. @@ -955,7 +984,8 @@ class MessageDeleteBulkEvent(HikariEvent, entities.Deserializable): @mark_as_websocket_event -@marshaller.attrs(slots=True) +@marshaller.marshallable() +@attr.s(slots=True) class MessageReactionAddEvent(HikariEvent, entities.Deserializable): """Used to represent Message Reaction Add gateway events.""" @@ -999,7 +1029,8 @@ class MessageReactionAddEvent(HikariEvent, entities.Deserializable): @mark_as_websocket_event -@marshaller.attrs(slots=True) +@marshaller.marshallable() +@attr.s(slots=True) class MessageReactionRemoveEvent(HikariEvent, entities.Deserializable): """Used to represent Message Reaction Remove gateway events.""" @@ -1035,7 +1066,8 @@ class MessageReactionRemoveEvent(HikariEvent, entities.Deserializable): @mark_as_websocket_event -@marshaller.attrs(slots=True) +@marshaller.marshallable() +@attr.s(slots=True) class MessageReactionRemoveAllEvent(HikariEvent, entities.Deserializable): """Used to represent Message Reaction Remove All gateway events. @@ -1061,7 +1093,8 @@ class MessageReactionRemoveAllEvent(HikariEvent, entities.Deserializable): @mark_as_websocket_event -@marshaller.attrs(slots=True) +@marshaller.marshallable() +@attr.s(slots=True) class MessageReactionRemoveEmojiEvent(HikariEvent, entities.Deserializable): """Represents Message Reaction Remove Emoji events. @@ -1094,7 +1127,8 @@ class MessageReactionRemoveEmojiEvent(HikariEvent, entities.Deserializable): @mark_as_websocket_event -@marshaller.attrs(slots=True) +@marshaller.marshallable() +@attr.s(slots=True) class PresenceUpdateEvent(HikariEvent, guilds.GuildMemberPresence): """Used to represent Presence Update gateway events. @@ -1103,7 +1137,8 @@ class PresenceUpdateEvent(HikariEvent, guilds.GuildMemberPresence): @mark_as_websocket_event -@marshaller.attrs(slots=True) +@marshaller.marshallable() +@attr.s(slots=True) class TypingStartEvent(HikariEvent, entities.Deserializable): """Used to represent typing start gateway events. @@ -1145,7 +1180,8 @@ class TypingStartEvent(HikariEvent, entities.Deserializable): @mark_as_websocket_event -@marshaller.attrs(slots=True) +@marshaller.marshallable() +@attr.s(slots=True) class UserUpdateEvent(HikariEvent, users.MyUser): """Used to represent User Update gateway events. @@ -1154,7 +1190,8 @@ class UserUpdateEvent(HikariEvent, users.MyUser): @mark_as_websocket_event -@marshaller.attrs(slots=True) +@marshaller.marshallable() +@attr.s(slots=True) class VoiceStateUpdateEvent(HikariEvent, voices.VoiceState): """Used to represent voice state update gateway events. @@ -1163,7 +1200,8 @@ class VoiceStateUpdateEvent(HikariEvent, voices.VoiceState): @mark_as_websocket_event -@marshaller.attrs(slots=True) +@marshaller.marshallable() +@attr.s(slots=True) class VoiceServerUpdateEvent(HikariEvent, entities.Deserializable): """Used to represent voice server update gateway events. @@ -1188,7 +1226,8 @@ class VoiceServerUpdateEvent(HikariEvent, entities.Deserializable): @mark_as_websocket_event -@marshaller.attrs(slots=True) +@marshaller.marshallable() +@attr.s(slots=True) class WebhookUpdateEvent(HikariEvent, entities.Deserializable): """Used to represent webhook update gateway events. diff --git a/hikari/gateway_entities.py b/hikari/gateway_entities.py index 4077719191..1d0f09917f 100644 --- a/hikari/gateway_entities.py +++ b/hikari/gateway_entities.py @@ -22,12 +22,15 @@ import datetime import typing +import attr + from hikari import entities from hikari import guilds from hikari.internal import marshaller -@marshaller.attrs(slots=True) +@marshaller.marshallable() +@attr.s(slots=True) class SessionStartLimit(entities.HikariEntity, entities.Deserializable): """Used to represent information about the current session start limits.""" @@ -50,7 +53,8 @@ class SessionStartLimit(entities.HikariEntity, entities.Deserializable): ) -@marshaller.attrs(slots=True) +@marshaller.marshallable() +@attr.s(slots=True) class GatewayBot(entities.HikariEntity, entities.Deserializable): """Used to represent gateway information for the connected bot.""" @@ -70,7 +74,8 @@ class GatewayBot(entities.HikariEntity, entities.Deserializable): session_start_limit: SessionStartLimit = marshaller.attrib(deserializer=SessionStartLimit.deserialize) -@marshaller.attrs(slots=True) +@marshaller.marshallable() +@attr.s(slots=True) class GatewayActivity(entities.Deserializable, entities.Serializable): """An activity that the bot can set for one or more shards. diff --git a/hikari/guilds.py b/hikari/guilds.py index 93ec9378b7..389164f135 100644 --- a/hikari/guilds.py +++ b/hikari/guilds.py @@ -47,6 +47,8 @@ import enum import typing +import attr + from hikari import channels as _channels from hikari import colors from hikari import emojis as _emojis @@ -178,7 +180,8 @@ class GuildVerificationLevel(enum.IntEnum): VERY_HIGH = 4 -@marshaller.attrs(slots=True) +@marshaller.marshallable() +@attr.s(slots=True) class GuildMember(entities.HikariEntity, entities.Deserializable): """Used to represent a guild bound member.""" @@ -226,7 +229,8 @@ class GuildMember(entities.HikariEntity, entities.Deserializable): is_mute: bool = marshaller.attrib(raw_name="mute", deserializer=bool) -@marshaller.attrs(slots=True) +@marshaller.marshallable() +@attr.s(slots=True) class PartialGuildRole(snowflakes.UniqueEntity, entities.Deserializable): """Represents a partial guild bound Role object.""" @@ -236,7 +240,8 @@ class PartialGuildRole(snowflakes.UniqueEntity, entities.Deserializable): name: str = marshaller.attrib(deserializer=str) -@marshaller.attrs(slots=True) +@marshaller.marshallable() +@attr.s(slots=True) class GuildRole(PartialGuildRole): """Represents a guild bound Role object.""" @@ -301,7 +306,8 @@ class ActivityType(enum.IntEnum): CUSTOM = 4 -@marshaller.attrs(slots=True) +@marshaller.marshallable() +@attr.s(slots=True) class ActivityTimestamps(entities.HikariEntity, entities.Deserializable): """The datetimes for the start and/or end of an activity session.""" @@ -320,7 +326,8 @@ class ActivityTimestamps(entities.HikariEntity, entities.Deserializable): ) -@marshaller.attrs(slots=True) +@marshaller.marshallable() +@attr.s(slots=True) class ActivityParty(entities.HikariEntity, entities.Deserializable): """Used to represent activity groups of users.""" @@ -348,7 +355,8 @@ def max_size(self) -> typing.Optional[int]: return self._size_information[1] if self._size_information else None -@marshaller.attrs(slots=True) +@marshaller.marshallable() +@attr.s(slots=True) class ActivityAssets(entities.HikariEntity, entities.Deserializable): """Used to represent possible assets for an activity.""" @@ -373,7 +381,8 @@ class ActivityAssets(entities.HikariEntity, entities.Deserializable): small_text: typing.Optional[str] = marshaller.attrib(deserializer=str, if_undefined=None) -@marshaller.attrs(slots=True) +@marshaller.marshallable() +@attr.s(slots=True) class ActivitySecret(entities.HikariEntity, entities.Deserializable): """The secrets used for interacting with an activity party.""" @@ -407,7 +416,8 @@ class ActivityFlag(enum.IntFlag): PLAY = 1 << 5 -@marshaller.attrs(slots=True) +@marshaller.marshallable() +@attr.s(slots=True) class PresenceActivity(entities.HikariEntity, entities.Deserializable): """Represents an activity that will be attached to a member's presence.""" @@ -508,7 +518,8 @@ class PresenceStatus(str, enum.Enum): OFFLINE = "offline" -@marshaller.attrs(slots=True) +@marshaller.marshallable() +@attr.s(slots=True) class ClientStatus(entities.HikariEntity, entities.Deserializable): """The client statuses for this member.""" @@ -530,7 +541,8 @@ class ClientStatus(entities.HikariEntity, entities.Deserializable): web: PresenceStatus = marshaller.attrib(deserializer=PresenceStatus, if_undefined=lambda: PresenceStatus.OFFLINE) -@marshaller.attrs(slots=True) +@marshaller.marshallable() +@attr.s(slots=True) class PresenceUser(users.User): """A user representation specifically used for presence updates. @@ -572,7 +584,8 @@ class PresenceUser(users.User): ) -@marshaller.attrs(slots=True) +@marshaller.marshallable() +@attr.s(slots=True) class GuildMemberPresence(entities.HikariEntity, entities.Deserializable): """Used to represent a guild member's presence.""" @@ -638,7 +651,8 @@ class IntegrationExpireBehaviour(enum.IntEnum): KICK = 1 -@marshaller.attrs(slots=True) +@marshaller.marshallable() +@attr.s(slots=True) class IntegrationAccount(entities.HikariEntity, entities.Deserializable): """An account that's linked to an integration.""" @@ -653,7 +667,8 @@ class IntegrationAccount(entities.HikariEntity, entities.Deserializable): name: str = marshaller.attrib(deserializer=str) -@marshaller.attrs(slots=True) +@marshaller.marshallable() +@attr.s(slots=True) class PartialGuildIntegration(snowflakes.UniqueEntity, entities.Deserializable): """A partial representation of an integration, found in audit logs.""" @@ -673,7 +688,8 @@ class PartialGuildIntegration(snowflakes.UniqueEntity, entities.Deserializable): account: IntegrationAccount = marshaller.attrib(deserializer=IntegrationAccount.deserialize) -@marshaller.attrs(slots=True) +@marshaller.marshallable() +@attr.s(slots=True) class GuildIntegration(snowflakes.UniqueEntity, entities.Deserializable): """Represents a guild integration object.""" @@ -727,7 +743,8 @@ class GuildIntegration(snowflakes.UniqueEntity, entities.Deserializable): ) -@marshaller.attrs(slots=True) +@marshaller.marshallable() +@attr.s(slots=True) class GuildMemberBan(entities.HikariEntity, entities.Deserializable): """Used to represent guild bans.""" @@ -742,7 +759,8 @@ class GuildMemberBan(entities.HikariEntity, entities.Deserializable): user: users.User = marshaller.attrib(deserializer=users.User.deserialize) -@marshaller.attrs(slots=True) +@marshaller.marshallable() +@attr.s(slots=True) class UnavailableGuild(snowflakes.UniqueEntity, entities.Deserializable): """An unavailable guild object, received during gateway events such as READY. @@ -760,7 +778,8 @@ def is_unavailable(self) -> bool: # noqa: D401 return True -@marshaller.attrs(slots=True) +@marshaller.marshallable() +@attr.s(slots=True) class PartialGuild(snowflakes.UniqueEntity, entities.Deserializable): """Base object for any partial guild objects.""" @@ -813,7 +832,8 @@ def icon_url(self) -> typing.Optional[str]: return self.format_icon_url() -@marshaller.attrs(slots=True) +@marshaller.marshallable() +@attr.s(slots=True) class Guild(PartialGuild): """A representation of a guild on Discord. diff --git a/hikari/internal/marshaller.py b/hikari/internal/marshaller.py index f4b594b3e2..c86293b51b 100644 --- a/hikari/internal/marshaller.py +++ b/hikari/internal/marshaller.py @@ -29,7 +29,7 @@ "RAISE", "dereference_handle", "attrib", - "attrs", + "marshallable", "HIKARI_ENTITY_MARSHALLER", "HikariEntityMarshaller", ] @@ -439,21 +439,11 @@ def serialize(self, obj: typing.Optional[typing.Any]) -> typing.Optional[typing. HIKARI_ENTITY_MARSHALLER = HikariEntityMarshaller() -def attrs(**kwargs): +def marshallable(*, marshaller: HikariEntityMarshaller = HIKARI_ENTITY_MARSHALLER): """Create a decorator for a class to make it into an :obj:`attr.s` class. Parameters ---------- - **kwargs - Any kwargs to pass to :obj:`attr.s`. - - Other Parameters - ---------------- - auto_attribs : :obj:`bool` - This must always be ``False`` if specified, or a :obj:`ValueError` - will be raised, as this feature is not compatible with this - marshaller implementation. If not specified, it will default to - ``False``. marshaller : :obj:`HikariEntityMarshaller` If specified, this should be an instance of a marshaller to use. For most internal purposes, you want to not specify this, since it will @@ -483,10 +473,6 @@ class MyEntity: ... """ - assertions.assert_that(not kwargs.get("auto_attribs"), "Cannot use auto attribs here") - kwargs["auto_attribs"] = False - return lambda cls: kwargs.pop("marshaller", HIKARI_ENTITY_MARSHALLER).register(attr.s(**kwargs)(cls)) - - -if typing.TYPE_CHECKING: - attrs = attr.s + def decorator(cls): + return marshaller.register(cls) + return decorator diff --git a/hikari/internal/more_collections.py b/hikari/internal/more_collections.py index 19925781db..6dbe7bcaf3 100644 --- a/hikari/internal/more_collections.py +++ b/hikari/internal/more_collections.py @@ -50,3 +50,4 @@ class WeakKeyDictionary(weakref.WeakKeyDictionary, typing.MutableMapping[K, V]): This is a type-safe version of :obj:`weakref.WeakKeyDictionary`. """ + __slots__ = () diff --git a/hikari/invites.py b/hikari/invites.py index 8afa8a8b41..2fcb6774e8 100644 --- a/hikari/invites.py +++ b/hikari/invites.py @@ -23,12 +23,14 @@ import enum import typing -import hikari.internal.conversions +import attr + from hikari import channels from hikari import entities from hikari import guilds from hikari import users from hikari.internal import cdn +from hikari.internal import conversions from hikari.internal import marshaller @@ -40,7 +42,8 @@ class TargetUserType(enum.IntEnum): STREAM = 1 -@marshaller.attrs(slots=True) +@marshaller.marshallable() +@attr.s(slots=True) class VanityUrl(entities.HikariEntity, entities.Deserializable): """A special case invite object, that represents a guild's vanity url.""" @@ -55,7 +58,8 @@ class VanityUrl(entities.HikariEntity, entities.Deserializable): uses: int = marshaller.attrib(deserializer=int) -@marshaller.attrs(slots=True) +@marshaller.marshallable() +@attr.s(slots=True) class InviteGuild(guilds.PartialGuild): """Represents the partial data of a guild that'll be attached to invites.""" @@ -147,7 +151,8 @@ def banner_url(self) -> typing.Optional[str]: return self.format_banner_url() -@marshaller.attrs(slots=True) +@marshaller.marshallable() +@attr.s(slots=True) class Invite(entities.HikariEntity, entities.Deserializable): """Represents an invite that's used to add users to a guild or group dm.""" @@ -196,7 +201,8 @@ class Invite(entities.HikariEntity, entities.Deserializable): approximate_member_count: typing.Optional[int] = marshaller.attrib(deserializer=int, if_undefined=None) -@marshaller.attrs(slots=True) +@marshaller.marshallable() +@attr.s(slots=True) class InviteWithMetadata(Invite): """Extends the base :obj:`Invite` object with metadata. @@ -231,7 +237,7 @@ class InviteWithMetadata(Invite): #: When this invite was created. #: #: :type: :obj:`datetime.datetime` - created_at: datetime.datetime = marshaller.attrib(deserializer=hikari.internal.conversions.parse_iso_8601_ts) + created_at: datetime.datetime = marshaller.attrib(deserializer=conversions.parse_iso_8601_ts) @property def expires_at(self) -> typing.Optional[datetime.datetime]: diff --git a/hikari/messages.py b/hikari/messages.py index 777602e200..5064eb4d76 100644 --- a/hikari/messages.py +++ b/hikari/messages.py @@ -32,7 +32,8 @@ import enum import typing -import hikari.internal.conversions +import attr + from hikari import embeds as _embeds from hikari import emojis as _emojis from hikari import entities @@ -40,6 +41,7 @@ from hikari import oauth2 from hikari import snowflakes from hikari import users +from hikari.internal import conversions from hikari.internal import marshaller @@ -106,7 +108,8 @@ class MessageActivityType(enum.IntEnum): JOIN_REQUEST = 5 -@marshaller.attrs(slots=True) +@marshaller.marshallable() +@attr.s(slots=True) class Attachment(snowflakes.UniqueEntity, entities.Deserializable): """Represents a file attached to a message.""" @@ -141,7 +144,8 @@ class Attachment(snowflakes.UniqueEntity, entities.Deserializable): width: typing.Optional[int] = marshaller.attrib(deserializer=int, if_undefined=None) -@marshaller.attrs(slots=True) +@marshaller.marshallable() +@attr.s(slots=True) class Reaction(entities.HikariEntity, entities.Deserializable): """Represents a reaction in a message.""" @@ -163,7 +167,8 @@ class Reaction(entities.HikariEntity, entities.Deserializable): is_reacted_by_me: bool = marshaller.attrib(raw_name="me", deserializer=bool) -@marshaller.attrs(slots=True) +@marshaller.marshallable() +@attr.s(slots=True) class MessageActivity(entities.HikariEntity, entities.Deserializable): """Represents the activity of a rich presence-enabled message.""" @@ -178,7 +183,8 @@ class MessageActivity(entities.HikariEntity, entities.Deserializable): party_id: typing.Optional[str] = marshaller.attrib(deserializer=str, if_undefined=None) -@marshaller.attrs(slots=True) +@marshaller.marshallable() +@attr.s(slots=True) class MessageCrosspost(entities.HikariEntity, entities.Deserializable): """Represents information about a cross-posted message and the origin of the original message.""" @@ -213,7 +219,8 @@ class MessageCrosspost(entities.HikariEntity, entities.Deserializable): ) -@marshaller.attrs(slots=True) +@marshaller.marshallable() +@attr.s(slots=True) class Message(snowflakes.UniqueEntity, entities.Deserializable): """Represents a message.""" @@ -249,13 +256,13 @@ class Message(snowflakes.UniqueEntity, entities.Deserializable): #: The timestamp that the message was sent at. #: #: :type: :obj:`datetime.datetime` - timestamp: datetime.datetime = marshaller.attrib(deserializer=hikari.internal.conversions.parse_iso_8601_ts) + timestamp: datetime.datetime = marshaller.attrib(deserializer=conversions.parse_iso_8601_ts) #: The timestamp that the message was last edited at, or ``None`` if not ever edited. #: #: :type: :obj:`datetime.datetime`, optional edited_timestamp: typing.Optional[datetime.datetime] = marshaller.attrib( - deserializer=hikari.internal.conversions.parse_iso_8601_ts, if_none=None + deserializer=conversions.parse_iso_8601_ts, if_none=None ) #: Whether the message is a TTS message. diff --git a/hikari/oauth2.py b/hikari/oauth2.py index 4385d5a6e1..1ffe45a479 100644 --- a/hikari/oauth2.py +++ b/hikari/oauth2.py @@ -22,6 +22,8 @@ import enum import typing +import attr + from hikari import entities from hikari import guilds from hikari import permissions @@ -41,7 +43,8 @@ class ConnectionVisibility(enum.IntEnum): EVERYONE = 1 -@marshaller.attrs(slots=True) +@marshaller.marshallable() +@attr.s(slots=True) class OwnConnection(entities.HikariEntity, entities.Deserializable): """Represents a user's connection with a third party account. @@ -105,7 +108,8 @@ class OwnConnection(entities.HikariEntity, entities.Deserializable): visibility: ConnectionVisibility = marshaller.attrib(deserializer=ConnectionVisibility) -@marshaller.attrs(slots=True) +@marshaller.marshallable() +@attr.s(slots=True) class OwnGuild(guilds.PartialGuild): """Represents a user bound partial guild object.""" @@ -133,7 +137,8 @@ class TeamMembershipState(enum.IntEnum): ACCEPTED = 2 -@marshaller.attrs(slots=True) +@marshaller.marshallable() +@attr.s(slots=True) class TeamMember(entities.HikariEntity, entities.Deserializable): """Represents a member of a Team.""" @@ -159,7 +164,8 @@ class TeamMember(entities.HikariEntity, entities.Deserializable): user: users.User = marshaller.attrib(deserializer=users.User.deserialize) -@marshaller.attrs(slots=True) +@marshaller.marshallable() +@attr.s(slots=True) class Team(snowflakes.UniqueEntity, entities.Deserializable): """Represents a development team, along with all its members.""" @@ -207,7 +213,8 @@ def format_icon_url(self, fmt: str = "png", size: int = 2048) -> typing.Optional return None -@marshaller.attrs(slots=True) +@marshaller.marshallable() +@attr.s(slots=True) class ApplicationOwner(users.User): """Represents the user who owns an application, may be a team user.""" @@ -222,7 +229,8 @@ def is_team_user(self) -> bool: return bool((self.flags >> 10) & 1) -@marshaller.attrs(slots=True) +@marshaller.marshallable() +@attr.s(slots=True) class Application(snowflakes.UniqueEntity, entities.Deserializable): """Represents the information of an Oauth2 Application.""" diff --git a/hikari/snowflakes.py b/hikari/snowflakes.py index ea7dee120b..c18296a03d 100644 --- a/hikari/snowflakes.py +++ b/hikari/snowflakes.py @@ -28,8 +28,10 @@ import functools import typing -import hikari.internal.conversions +import attr + from hikari import entities +from hikari.internal import conversions from hikari.internal import marshaller @@ -55,7 +57,7 @@ def __init__(self, value: typing.Union[int, str]) -> None: # pylint:disable=sup def created_at(self) -> datetime.datetime: """When the object was created.""" epoch = self._value >> 22 - return hikari.internal.conversions.discord_epoch_to_datetime(epoch) + return conversions.discord_epoch_to_datetime(epoch) @property def internal_worker_id(self) -> int: @@ -100,7 +102,8 @@ def deserialize(cls, value: str) -> "Snowflake": return cls(value) -@marshaller.attrs(slots=True) +@marshaller.marshallable() +@attr.s(slots=True) class UniqueEntity(entities.HikariEntity): """An entity that has an integer ID of some sort.""" diff --git a/hikari/users.py b/hikari/users.py index 4811c7ea99..876ce3fe7a 100644 --- a/hikari/users.py +++ b/hikari/users.py @@ -22,13 +22,16 @@ import enum import typing +import attr + from hikari import entities from hikari import snowflakes from hikari.internal import cdn from hikari.internal import marshaller -@marshaller.attrs(slots=True) +@marshaller.marshallable() +@attr.s(slots=True) class User(snowflakes.UniqueEntity, entities.Deserializable): """Represents a user.""" @@ -126,7 +129,8 @@ class PremiumType(enum.IntEnum): NITRO = 2 -@marshaller.attrs(slots=True) +@marshaller.marshallable() +@attr.s(slots=True) class MyUser(User): """Represents a user with extended oauth2 information.""" diff --git a/hikari/voices.py b/hikari/voices.py index b56b337913..2f8cb28545 100644 --- a/hikari/voices.py +++ b/hikari/voices.py @@ -21,13 +21,16 @@ import typing +import attr + from hikari import entities from hikari import guilds from hikari import snowflakes from hikari.internal import marshaller -@marshaller.attrs(slots=True) +@marshaller.marshallable() +@attr.s(slots=True) class VoiceState(entities.HikariEntity, entities.Deserializable): """Represents a user's voice connection status.""" @@ -92,7 +95,8 @@ class VoiceState(entities.HikariEntity, entities.Deserializable): is_suppressed: bool = marshaller.attrib(raw_name="suppress", deserializer=bool) -@marshaller.attrs(slots=True) +@marshaller.marshallable() +@attr.s(slots=True) class VoiceRegion(entities.HikariEntity, entities.Deserializable): """Represent's a voice region server.""" diff --git a/hikari/webhooks.py b/hikari/webhooks.py index 591b525016..14e52be3ae 100644 --- a/hikari/webhooks.py +++ b/hikari/webhooks.py @@ -22,6 +22,8 @@ import enum import typing +import attr + from hikari import entities from hikari import snowflakes from hikari import users @@ -38,7 +40,8 @@ class WebhookType(enum.IntEnum): CHANNEL_FOLLOWER = 2 -@marshaller.attrs(slots=True) +@marshaller.marshallable() +@attr.s(slots=True) class Webhook(snowflakes.UniqueEntity, entities.Deserializable): """Represents a webhook object on Discord. diff --git a/tests/hikari/internal/test_marshaller.py b/tests/hikari/internal/test_marshaller.py index 0f47b4fd4b..a35625ff7b 100644 --- a/tests/hikari/internal/test_marshaller.py +++ b/tests/hikari/internal/test_marshaller.py @@ -16,6 +16,7 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . +import attr import cymock as mock import pytest @@ -78,21 +79,19 @@ class TestAttrs: def test_invokes_attrs(self): marshaller_mock = mock.create_autospec(marshaller.HikariEntityMarshaller, spec_set=True) - kwargs = {"foo": 9, "bar": "lol", "marshaller": marshaller_mock} + kwargs = {"marshaller": marshaller_mock} marshaller_mock.register = mock.MagicMock(wraps=lambda c: c) - with mock.patch("attr.s", return_value=lambda c: c) as attrs: - - @marshaller.attrs(**kwargs) - class Foo: - bar = 69 + @marshaller.marshallable(**kwargs) + @attr.s() + class Foo: + bar = 69 - assert Foo is not None - assert Foo.bar == 69 + assert Foo is not None + assert Foo.bar == 69 - attrs.assert_called_once_with(foo=9, bar="lol", auto_attribs=False) - marshaller_mock.register.assert_called_once_with(Foo) + marshaller_mock.register.assert_called_once_with(Foo) class TestMarshaller: @@ -104,7 +103,8 @@ def test_deserialize(self, marshaller_impl): deserialized_id = mock.MagicMock() id_deserializer = mock.MagicMock(return_value=deserialized_id) - @marshaller.attrs(marshaller=marshaller_impl) + @marshaller.marshallable(marshaller=marshaller_impl) + @attr.s() class User: id: int = marshaller.attrib(deserializer=id_deserializer) some_list: list = marshaller.attrib(deserializer=lambda items: [str(i) for i in items]) @@ -116,7 +116,8 @@ class User: assert result.some_list == ["True", "False", "foo", "12", "3.4"] def test_deserialize_not_required_success_if_specified(self, marshaller_impl): - @marshaller.attrs(marshaller=marshaller_impl) + @marshaller.marshallable(marshaller=marshaller_impl) + @attr.s() class User: id: int = marshaller.attrib(if_undefined=None, deserializer=str) @@ -127,7 +128,8 @@ class User: @pytest.mark.parametrize("singleton", marshaller.PASSED_THROUGH_SINGLETONS) def test_deserialize_not_required_success_if_not_specified(self, marshaller_impl, singleton): - @marshaller.attrs(marshaller=marshaller_impl) + @marshaller.marshallable(marshaller=marshaller_impl) + @attr.s() class User: id: int = marshaller.attrib(if_undefined=singleton, deserializer=str) @@ -140,7 +142,8 @@ def test_deserialize_calls_if_undefined_if_not_none_and_field_not_present(self, mock_result = mock.MagicMock() mock_callable = mock.MagicMock(return_value=mock_result) - @marshaller.attrs(marshaller=marshaller_impl) + @marshaller.marshallable(marshaller=marshaller_impl) + @attr.s() class User: id: int = marshaller.attrib(if_undefined=mock_callable, deserializer=str) @@ -152,14 +155,16 @@ class User: @_helpers.assert_raises(type_=AttributeError) def test_deserialize_fail_on_unspecified_if_required(self, marshaller_impl): - @marshaller.attrs(marshaller=marshaller_impl) + @marshaller.marshallable(marshaller=marshaller_impl) + @attr.s() class User: id: int = marshaller.attrib(deserializer=str) marshaller_impl.deserialize({}, User) def test_deserialize_nullable_success_if_not_null(self, marshaller_impl): - @marshaller.attrs(marshaller=marshaller_impl) + @marshaller.marshallable(marshaller=marshaller_impl) + @attr.s() class User: id: int = marshaller.attrib(if_none=None, deserializer=str) @@ -170,7 +175,8 @@ class User: @pytest.mark.parametrize("singleton", marshaller.PASSED_THROUGH_SINGLETONS) def test_deserialize_nullable_success_if_null(self, marshaller_impl, singleton): - @marshaller.attrs(marshaller=marshaller_impl) + @marshaller.marshallable(marshaller=marshaller_impl) + @attr.s() class User: id: int = marshaller.attrib(if_none=singleton, deserializer=str) @@ -183,7 +189,8 @@ def test_deserialize_calls_if_none_if_not_none_and_data_is_none(self, marshaller mock_result = mock.MagicMock() mock_callable = mock.MagicMock(return_value=mock_result) - @marshaller.attrs(marshaller=marshaller_impl) + @marshaller.marshallable(marshaller=marshaller_impl) + @attr.s() class User: id: int = marshaller.attrib(if_none=mock_callable, deserializer=str) @@ -195,7 +202,8 @@ class User: @_helpers.assert_raises(type_=AttributeError) def test_deserialize_fail_on_None_if_not_nullable(self, marshaller_impl): - @marshaller.attrs(marshaller=marshaller_impl) + @marshaller.marshallable(marshaller=marshaller_impl) + @attr.s() class User: id: int = marshaller.attrib(deserializer=str) @@ -205,14 +213,16 @@ class User: def test_deserialize_fail_on_Error(self, marshaller_impl): die = mock.MagicMock(side_effect=RuntimeError) - @marshaller.attrs(marshaller=marshaller_impl) + @marshaller.marshallable(marshaller=marshaller_impl) + @attr.s() class User: id: int = marshaller.attrib(deserializer=die) marshaller_impl.deserialize({"id": 123,}, User) def test_serialize(self, marshaller_impl): - @marshaller.attrs(marshaller=marshaller_impl) + @marshaller.marshallable(marshaller=marshaller_impl) + @attr.s() class User: id: int = marshaller.attrib(deserializer=..., serializer=str) some_list: list = marshaller.attrib(deserializer=..., serializer=lambda i: list(map(int, i))) @@ -222,7 +232,8 @@ class User: assert marshaller_impl.serialize(u) == {"id": "12", "some_list": [9, 18, 27, 36]} def test_serialize_transient(self, marshaller_impl): - @marshaller.attrs(marshaller=marshaller_impl) + @marshaller.marshallable(marshaller=marshaller_impl) + @attr.s() class User: id: int = marshaller.attrib(deserializer=..., serializer=str) some_list: list = marshaller.attrib( @@ -252,7 +263,8 @@ class Foo: marshaller_impl.serialize(f) def test_handling_underscores_correctly_during_deserialization(self, marshaller_impl): - @marshaller.attrs(marshaller=marshaller_impl) + @marshaller.marshallable(marshaller=marshaller_impl) + @attr.s() class ClassWithUnderscores: _foo = marshaller.attrib(deserializer=str) @@ -261,7 +273,8 @@ class ClassWithUnderscores: assert impl._foo == "1234" def test_handling_underscores_correctly_during_serialization(self, marshaller_impl): - @marshaller.attrs(marshaller=marshaller_impl) + @marshaller.marshallable(marshaller=marshaller_impl) + @attr.s() class ClassWithUnderscores: _foo = marshaller.attrib(serializer=int) diff --git a/tests/hikari/internal/test_marshaller_pep563.py b/tests/hikari/internal/test_marshaller_pep563.py index e17756181b..d9d8e17787 100644 --- a/tests/hikari/internal/test_marshaller_pep563.py +++ b/tests/hikari/internal/test_marshaller_pep563.py @@ -23,6 +23,7 @@ """ from __future__ import annotations +import attr import cymock as mock import pytest @@ -68,21 +69,19 @@ class TestAttrsPep563: def test_invokes_attrs(self): marshaller_mock = mock.create_autospec(marshaller.HikariEntityMarshaller, spec_set=True) - kwargs = {"foo": 9, "bar": "lol", "marshaller": marshaller_mock} + kwargs = {"marshaller": marshaller_mock} marshaller_mock.register = mock.MagicMock(wraps=lambda c: c) - with mock.patch("attr.s", return_value=lambda c: c) as attrs: - - @marshaller.attrs(**kwargs) - class Foo: - bar = 69 + @marshaller.marshallable(**kwargs) + @attr.s() + class Foo: + bar = 69 - assert Foo is not None - assert Foo.bar == 69 + assert Foo is not None + assert Foo.bar == 69 - attrs.assert_called_once_with(foo=9, bar="lol", auto_attribs=False) - marshaller_mock.register.assert_called_once_with(Foo) + marshaller_mock.register.assert_called_once_with(Foo) @pytest.mark.parametrize("data", [2, "d", bytes("ok", "utf-8"), [], {}, set()]) @@ -111,7 +110,8 @@ def test_deserialize(self, marshaller_impl): deserialized_id = mock.MagicMock() id_deserializer = mock.MagicMock(return_value=deserialized_id) - @marshaller.attrs(marshaller=marshaller_impl) + @marshaller.marshallable(marshaller=marshaller_impl) + @attr.s() class User: id: int = marshaller.attrib(deserializer=id_deserializer) some_list: list = marshaller.attrib(deserializer=lambda items: [str(i) for i in items]) @@ -123,7 +123,8 @@ class User: assert result.some_list == ["True", "False", "foo", "12", "3.4"] def test_deserialize_not_required_success_if_specified(self, marshaller_impl): - @marshaller.attrs(marshaller=marshaller_impl) + @marshaller.marshallable(marshaller=marshaller_impl) + @attr.s() class User: id: int = marshaller.attrib(if_undefined=None, deserializer=str) @@ -134,7 +135,8 @@ class User: @pytest.mark.parametrize("singleton", marshaller.PASSED_THROUGH_SINGLETONS) def test_deserialize_not_required_success_if_not_specified(self, marshaller_impl, singleton): - @marshaller.attrs(marshaller=marshaller_impl) + @marshaller.marshallable(marshaller=marshaller_impl) + @attr.s() class User: id: int = marshaller.attrib(if_undefined=singleton, deserializer=str) @@ -147,7 +149,8 @@ def test_deserialize_calls_if_undefined_if_not_none_and_field_not_present(self, mock_result = mock.MagicMock() mock_callable = mock.MagicMock(return_value=mock_result) - @marshaller.attrs(marshaller=marshaller_impl) + @marshaller.marshallable(marshaller=marshaller_impl) + @attr.s() class User: id: int = marshaller.attrib(if_undefined=mock_callable, deserializer=str) @@ -159,14 +162,16 @@ class User: @_helpers.assert_raises(type_=AttributeError) def test_deserialize_fail_on_unspecified_if_required(self, marshaller_impl): - @marshaller.attrs(marshaller=marshaller_impl) + @marshaller.marshallable(marshaller=marshaller_impl) + @attr.s() class User: id: int = marshaller.attrib(deserializer=str) marshaller_impl.deserialize({}, User) def test_deserialize_nullable_success_if_not_null(self, marshaller_impl): - @marshaller.attrs(marshaller=marshaller_impl) + @marshaller.marshallable(marshaller=marshaller_impl) + @attr.s() class User: id: int = marshaller.attrib(if_none=None, deserializer=str) @@ -177,7 +182,8 @@ class User: @pytest.mark.parametrize("singleton", marshaller.PASSED_THROUGH_SINGLETONS) def test_deserialize_nullable_success_if_null(self, marshaller_impl, singleton): - @marshaller.attrs(marshaller=marshaller_impl) + @marshaller.marshallable(marshaller=marshaller_impl) + @attr.s() class User: id: int = marshaller.attrib(if_none=singleton, deserializer=str) @@ -190,7 +196,8 @@ def test_deserialize_calls_if_none_if_not_none_and_data_is_none(self, marshaller mock_result = mock.MagicMock() mock_callable = mock.MagicMock(return_value=mock_result) - @marshaller.attrs(marshaller=marshaller_impl) + @marshaller.marshallable(marshaller=marshaller_impl) + @attr.s() class User: id: int = marshaller.attrib(if_none=mock_callable, deserializer=str) @@ -202,7 +209,8 @@ class User: @_helpers.assert_raises(type_=AttributeError) def test_deserialize_fail_on_None_if_not_nullable(self, marshaller_impl): - @marshaller.attrs(marshaller=marshaller_impl) + @marshaller.marshallable(marshaller=marshaller_impl) + @attr.s() class User: id: int = marshaller.attrib(deserializer=str) @@ -212,14 +220,16 @@ class User: def test_deserialize_fail_on_Error(self, marshaller_impl): die = mock.MagicMock(side_effect=RuntimeError) - @marshaller.attrs(marshaller=marshaller_impl) + @marshaller.marshallable(marshaller=marshaller_impl) + @attr.s() class User: id: int = marshaller.attrib(deserializer=die) marshaller_impl.deserialize({"id": 123,}, User) def test_serialize(self, marshaller_impl): - @marshaller.attrs(marshaller=marshaller_impl) + @marshaller.marshallable(marshaller=marshaller_impl) + @attr.s() class User: id: int = marshaller.attrib(deserializer=..., serializer=str) some_list: list = marshaller.attrib(deserializer=..., serializer=lambda i: list(map(int, i))) @@ -229,7 +239,8 @@ class User: assert marshaller_impl.serialize(u) == {"id": "12", "some_list": [9, 18, 27, 36]} def test_serialize_transient(self, marshaller_impl): - @marshaller.attrs(marshaller=marshaller_impl) + @marshaller.marshallable(marshaller=marshaller_impl) + @attr.s() class User: id: int = marshaller.attrib(deserializer=..., serializer=str) some_list: list = marshaller.attrib( @@ -259,7 +270,8 @@ class Foo: marshaller_impl.serialize(f) def test_handling_underscores_correctly_during_deserialization(self, marshaller_impl): - @marshaller.attrs(marshaller=marshaller_impl) + @marshaller.marshallable(marshaller=marshaller_impl) + @attr.s() class ClassWithUnderscores: _foo = marshaller.attrib(deserializer=str) @@ -268,7 +280,8 @@ class ClassWithUnderscores: assert impl._foo == "1234" def test_handling_underscores_correctly_during_serialization(self, marshaller_impl): - @marshaller.attrs(marshaller=marshaller_impl) + @marshaller.marshallable(marshaller=marshaller_impl) + @attr.s() class ClassWithUnderscores: _foo = marshaller.attrib(serializer=int) diff --git a/tests/hikari/net/test_rest.py b/tests/hikari/net/test_rest.py index e97d5e36dc..22648e5ae6 100644 --- a/tests/hikari/net/test_rest.py +++ b/tests/hikari/net/test_rest.py @@ -351,17 +351,28 @@ async def test__request_updates_headers_with_provided_headers(self, compiled_rou @pytest.mark.asyncio async def test__request_resets_seek_on_seekable_resources(self, compiled_route, exit_error, mock_rest_impl): class SeekableResource: - seeked: bool = False + seeked: bool + pos: int + initial_pos: int - def seek(self, _): + def __init__(self, pos): + self.pos = pos + self.initial_pos = pos + self.seeked = False + + def seek(self, pos): self.seeked = True + self.pos = pos + + def tellg(self): + return self.pos def assert_seek_called(self): assert self.seeked rest_impl = await mock_rest_impl rest_impl.logger.debug.side_effect = exit_error - seekable_resources = [SeekableResource(), SeekableResource(), SeekableResource()] + seekable_resources = [SeekableResource(5), SeekableResource(37), SeekableResource(16)] with mock.patch("asyncio.gather", return_value=_helpers.AwaitableMock()): try: @@ -371,6 +382,7 @@ def assert_seek_called(self): for resource in seekable_resources: resource.assert_seek_called() + assert resource.pos == resource.initial_pos @pytest.mark.asyncio @pytest.mark.parametrize( From e9e43e8ba95ea569ab2282163a41d9f62e9af0cb Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Sat, 11 Apr 2020 19:52:34 +0100 Subject: [PATCH 086/922] Fixes #274, provides support for IO objects not at the start-of-stream in HTTP requests. --- hikari/guilds.py | 10 +- hikari/internal/__init__.py | 2 +- hikari/internal/conversions.py | 71 ++++++---- hikari/internal/more_collections.py | 1 + hikari/internal/{cdn.py => urls.py} | 11 +- hikari/invites.py | 6 +- hikari/net/rest.py | 204 +++++++++++++++++++--------- hikari/oauth2.py | 8 +- hikari/users.py | 6 +- tests/hikari/internal/test_cdn.py | 6 +- tests/hikari/net/test_rest.py | 8 +- tests/hikari/test_guilds.py | 58 ++++---- tests/hikari/test_invites.py | 26 ++-- tests/hikari/test_oauth2.py | 38 +++--- tests/hikari/test_users.py | 18 +-- 15 files changed, 289 insertions(+), 184 deletions(-) rename hikari/internal/{cdn.py => urls.py} (83%) diff --git a/hikari/guilds.py b/hikari/guilds.py index 389164f135..82b760f32d 100644 --- a/hikari/guilds.py +++ b/hikari/guilds.py @@ -56,7 +56,7 @@ from hikari import permissions as _permissions from hikari import snowflakes from hikari import users -from hikari.internal import cdn +from hikari.internal import urls from hikari.internal import conversions from hikari.internal import marshaller @@ -823,7 +823,7 @@ def format_icon_url(self, fmt: typing.Optional[str] = None, size: int = 2048) -> fmt = "gif" elif fmt is None: fmt = "png" - return cdn.generate_cdn_url("icons", str(self.id), self.icon_hash, fmt=fmt, size=size) + return urls.generate_cdn_url("icons", str(self.id), self.icon_hash, fmt=fmt, size=size) return None @property @@ -1201,7 +1201,7 @@ def format_splash_url(self, fmt: str = "png", size: int = 2048) -> typing.Option The string URL. """ if self.splash_hash: - return cdn.generate_cdn_url("splashes", str(self.id), self.splash_hash, fmt=fmt, size=size) + return urls.generate_cdn_url("splashes", str(self.id), self.splash_hash, fmt=fmt, size=size) return None @property @@ -1227,7 +1227,7 @@ def format_discovery_splash_url(self, fmt: str = "png", size: int = 2048) -> typ The string URL. """ if self.discovery_splash_hash: - return cdn.generate_cdn_url( + return urls.generate_cdn_url( "discovery-splashes", str(self.id), self.discovery_splash_hash, fmt=fmt, size=size ) return None @@ -1255,7 +1255,7 @@ def format_banner_url(self, fmt: str = "png", size: int = 2048) -> typing.Option The string URL. """ if self.banner_hash: - return cdn.generate_cdn_url("banners", str(self.id), self.banner_hash, fmt=fmt, size=size) + return urls.generate_cdn_url("banners", str(self.id), self.banner_hash, fmt=fmt, size=size) return None @property diff --git a/hikari/internal/__init__.py b/hikari/internal/__init__.py index c02f609023..770a13acb1 100644 --- a/hikari/internal/__init__.py +++ b/hikari/internal/__init__.py @@ -21,7 +21,7 @@ |internal| """ from hikari.internal import assertions -from hikari.internal import cdn +from hikari.internal import urls from hikari.internal import conversions from hikari.internal import marshaller from hikari.internal import meta diff --git a/hikari/internal/conversions.py b/hikari/internal/conversions.py index b7d558e64c..6fa595f829 100644 --- a/hikari/internal/conversions.py +++ b/hikari/internal/conversions.py @@ -18,16 +18,17 @@ # along with Hikari. If not, see . """Basic transformation utilities.""" __all__ = [ - "CastInputT", - "CastOutputT", - "DefaultT", - "TypeCastT", - "ResultT", "nullable_cast", "try_cast", "try_cast_or_defer_unary_operator", "put_if_specified", "image_bytes_to_image_data", + "parse_http_date", + "parse_iso_8601_ts", + "discord_epoch_to_datetime", + "unix_epoch_to_ts", + "Seekable", + "make_resource_seekable", "pluralize", "snoop_typehint_from_scope", ] @@ -41,11 +42,19 @@ import types import typing + +DISCORD_EPOCH: typing.Final[int] = 1_420_070_400 +ISO_8601_DATE_PART: typing.Final[typing.Pattern] = re.compile(r"^(\d{4})-(\d{2})-(\d{2})") +ISO_8601_TIME_PART: typing.Final[typing.Pattern] = re.compile(r"T(\d{2}):(\d{2}):(\d{2})(?:\.(\d{1,6}))?", re.I) +ISO_8601_TZ_PART: typing.Final[typing.Pattern] = re.compile(r"([+-])(\d{2}):(\d{2})$") + CastInputT = typing.TypeVar("CastInputT") CastOutputT = typing.TypeVar("CastOutputT") DefaultT = typing.TypeVar("DefaultT") TypeCastT = typing.Callable[[CastInputT], CastOutputT] ResultT = typing.Union[CastOutputT, DefaultT] +BytesLikeT = typing.Union[bytes, bytearray, memoryview, str, io.StringIO, io.BytesIO] +FileLikeT = typing.Union[BytesLikeT, io.BufferedRandom, io.BufferedReader, io.BufferedRWPair] def nullable_cast(value: CastInputT, cast: TypeCastT) -> ResultT: @@ -70,6 +79,17 @@ def try_cast(value: CastInputT, cast: TypeCastT, default: DefaultT = None) -> Re return default +def try_cast_or_defer_unary_operator(type_): + """Return a unary operator that will try to cast the given input to the type provided. + + Parameters + ---------- + type_ : :obj:`typing.Callable` [ ..., ``output type`` ] + The type to cast to. + """ + return lambda data: try_cast(data, type_, data) + + def put_if_specified( mapping: typing.Dict[typing.Hashable, typing.Any], key: typing.Hashable, @@ -137,17 +157,6 @@ def image_bytes_to_image_data(img_bytes: typing.Optional[bytes] = None) -> typin return f"data:{img_type};base64,{image_data}" -def try_cast_or_defer_unary_operator(type_): - """Return a unary operator that will try to cast the given input to the type provided. - - Parameters - ---------- - type_ : :obj:`typing.Callable` [ ..., ``output type`` ] - The type to cast to. - """ - return lambda data: try_cast(data, type_, data) - - def parse_http_date(date_str: str) -> datetime.datetime: """Return the HTTP date as a datetime object. @@ -168,11 +177,6 @@ def parse_http_date(date_str: str) -> datetime.datetime: return email.utils.parsedate_to_datetime(date_str) -ISO_8601_DATE_PART = re.compile(r"^(\d{4})-(\d{2})-(\d{2})") -ISO_8601_TIME_PART = re.compile(r"T(\d{2}):(\d{2}):(\d{2})(?:\.(\d{1,6}))?", re.I) -ISO_8601_TZ_PART = re.compile(r"([+-])(\d{2}):(\d{2})$") - - def parse_iso_8601_ts(date_string: str) -> datetime.datetime: """Parse an ISO 8601 date string into a :obj:`datetime.datetime` object. @@ -211,9 +215,6 @@ def parse_iso_8601_ts(date_string: str) -> datetime.datetime: return datetime.datetime(year, month, day, hour, minute, second, partial, timezone) -DISCORD_EPOCH = 1_420_070_400 - - def discord_epoch_to_datetime(epoch: int) -> datetime.datetime: """Parse a Discord epoch into a :obj:`datetime.datetime` object. @@ -246,7 +247,23 @@ def unix_epoch_to_ts(epoch: int) -> datetime.datetime: return datetime.datetime.fromtimestamp(epoch / 1000, datetime.timezone.utc) -def make_resource_seekable(resource: typing.Any) -> typing.Union[io.BytesIO, io.StringIO]: +class Seekable(typing.Protocol[typing.AnyStr]): + """Structural type for an IO object that supports seek operations.""" + + def seek(self, pos: int) -> None: + ... + + def tell(self) -> int: + ... + + def read(self) -> typing.AnyStr: + ... + + def close(self) -> None: + ... + + +def make_resource_seekable(resource: typing.Any) -> Seekable: """Make a seekable resource to use off some representation of data. This supports :obj:`bytes`, :obj:`bytearray`, :obj:`memoryview`, and @@ -338,7 +355,3 @@ def snoop_typehint_from_scope(frame: types.FrameType, typehint: typing.Union[str except (AttributeError, KeyError): pass raise NameError(f"No attribute {typehint} was found in enclosing scope") - - -BytesLikeT = typing.Union[bytes, bytearray, memoryview, str, io.StringIO, io.BytesIO] -FileLikeT = typing.Union[BytesLikeT, io.BufferedRandom, io.BufferedReader, io.BufferedRWPair] diff --git a/hikari/internal/more_collections.py b/hikari/internal/more_collections.py index 6dbe7bcaf3..084147c0c0 100644 --- a/hikari/internal/more_collections.py +++ b/hikari/internal/more_collections.py @@ -50,4 +50,5 @@ class WeakKeyDictionary(weakref.WeakKeyDictionary, typing.MutableMapping[K, V]): This is a type-safe version of :obj:`weakref.WeakKeyDictionary`. """ + __slots__ = () diff --git a/hikari/internal/cdn.py b/hikari/internal/urls.py similarity index 83% rename from hikari/internal/cdn.py rename to hikari/internal/urls.py index 5c0251b980..6c919ceec5 100644 --- a/hikari/internal/cdn.py +++ b/hikari/internal/urls.py @@ -16,7 +16,7 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . -"""Basic utilities for handling the cdn. +"""Discord-specific URIs that have to be hard-coded. |internal| """ @@ -30,10 +30,15 @@ BASE_CDN_URL = "https://cdn.discordapp.com" +#: The URL for the REST API. This contains a version number parameter that +#: should be interpolated. +#: +#: :type: :obj:`str` +REST_API_URL: typing.Final[str] = "https://discordapp.com/api/v{0.version}" def generate_cdn_url(*route_parts: str, fmt: str, size: typing.Optional[int]) -> str: - """Generate a link for a cdn entry. + """Generate a link for a Discord CDN media resource. Parameters ---------- @@ -50,7 +55,7 @@ def generate_cdn_url(*route_parts: str, fmt: str, size: typing.Optional[int]) -> Returns ------- :obj:`str` - The formed cdn url. + The URL to the resource on the Discord CDN. """ path = "/".join(urllib.parse.unquote(part) for part in route_parts) url = urllib.parse.urljoin(BASE_CDN_URL, "/" + path) + "." + str(fmt) diff --git a/hikari/invites.py b/hikari/invites.py index 2fcb6774e8..c1bfe5ea37 100644 --- a/hikari/invites.py +++ b/hikari/invites.py @@ -29,7 +29,7 @@ from hikari import entities from hikari import guilds from hikari import users -from hikari.internal import cdn +from hikari.internal import urls from hikari.internal import conversions from hikari.internal import marshaller @@ -116,7 +116,7 @@ def format_splash_url(self, fmt: str = "png", size: int = 2048) -> typing.Option The string URL. """ if self.splash_hash: - return cdn.generate_cdn_url("splashes", str(self.id), self.splash_hash, fmt=fmt, size=size) + return urls.generate_cdn_url("splashes", str(self.id), self.splash_hash, fmt=fmt, size=size) return None @property @@ -142,7 +142,7 @@ def format_banner_url(self, fmt: str = "png", size: int = 2048) -> typing.Option The string URL. """ if self.banner_hash: - return cdn.generate_cdn_url("banners", str(self.id), self.banner_hash, fmt=fmt, size=size) + return urls.generate_cdn_url("banners", str(self.id), self.banner_hash, fmt=fmt, size=size) return None @property diff --git a/hikari/net/rest.py b/hikari/net/rest.py index 276376e270..adc09889e0 100644 --- a/hikari/net/rest.py +++ b/hikari/net/rest.py @@ -24,6 +24,7 @@ import datetime import email.utils import json +import logging import ssl import typing import uuid @@ -35,6 +36,7 @@ from hikari.internal import conversions from hikari.internal import more_collections from hikari.internal import more_logging +from hikari.internal import urls from hikari.net import codes from hikari.net import ratelimits from hikari.net import routes @@ -74,9 +76,15 @@ class RestfulClient: A custom JSON serializer function to use. Defaults to :func:`json.dumps`. token: :obj:`string`, optional - The bot token for the client to use. + The bot token for the client to use. You may start this with + a prefix of either ``Bot`` or ``Bearer`` to force the token type, or + not provide this information if you want to have it auto-detected. + If this is passed as :obj:`None`, then no token is used. + This will be passed as the ``Authorization`` header if not :obj:`None` + for each request. version: :obj:`typing.Union` [ :obj:`int`, :obj:`hikari.net.versions.HTTPAPIVersion` ] - The version of the API to use. Defaults to the most recent stable version. + The version of the API to use. Defaults to the most recent stable + version. """ GET = "get" @@ -89,10 +97,125 @@ class RestfulClient: _AUTHENTICATION_SCHEMES = ("Bearer", "Bot") + #: ``True`` if HTTP redirects are enabled, or ``False`` otherwise. + #: + #: :type: :obj:`bool` + allow_redirects: bool + + #: The base URL to send requests to. + #: + #: :type: :obj:`str` + base_url: str + + #: The :mod:`aiohttp` client session to make requests with. + #: + #: :type: :obj:`aiohttp.ClientSession` + client_session: aiohttp.ClientSession + + #: The internal correlation ID for the number of requests sent. This will + #: increase each time a REST request is sent. + #: + #: :type: :obj:`int` + in_count: int + + #: The global ratelimiter. This is used if Discord declares a ratelimit + #: across the entire API, regardless of the endpoint. If this is set, then + #: any HTTP operation using this session will be paused. + #: + #: :type: :obj:`hikari.net.ratelimits.ManualRateLimiter` + global_ratelimiter: ratelimits.ManualRateLimiter + + #: The logger to use for this object. + #: + #: :type: :obj:`logging.Logger` + logger: logging.Logger + + #: The JSON deserialization function. This consumes a JSON string and + #: produces some object. + json_deserialize: typing.Callable[[typing.AnyStr], typing.Any] + + #: The JSON deserialization function. This consumes an object and + #: produces some JSON string. + json_serialize: typing.Callable[[typing.Any], typing.AnyStr] + + #: Proxy authorization to use. + #: + #: :type: :obj:`aiohttp.BasicAuth`, optional + proxy_auth: typing.Optional[aiohttp.BasicAuth] + + #: A set of headers to provide to a proxy server. + #: + #: :type: :obj:`typing.Mapping` [ :obj:`str`, :obj:`str` ], optional + proxy_headers: typing.Optional[typing.Mapping[str, str]] + + #: An optional proxy URL to send requests to. + #: + #: :type: :obj:`str`, optional + proxy_url: typing.Optional[str] + + #: The per-route ratelimit manager. This handles tracking any ratelimits + #: for routes that have recently been used or are in active use, as well + #: as keeping memory usage to a minimum where possible for large numbers + #: of varying requests. This encapsulates a lot of complex rate limiting + #: rules to reduce the number of active ``429`` responses this client gets, + #: and thus reducing your chances of an API ban by Discord. + #: + #: You should not ever need to touch this implementation. + #: + #: :type: :obj:`hikari.net.ratelimits.HTTPBucketRateLimiterManager` + ratelimiter: ratelimits.HTTPBucketRateLimiterManager + + #: The custom SSL context to use. + #: + #: :type: :obj:`ssl.SSLContext` + ssl_context: typing.Optional[ssl.SSLContext] + + #: The HTTP request timeout to abort requests after. + #: + #: :type: :obj:`float` + timeout: typing.Optional[float] + + #: The bot token. This will be prefixed with either ``"Bearer "`` or + #: ``"Bot"`` depending on the format of the token passed to the constructor. + #: + #: This value will be used for the ``Authorization`` HTTP header on each + #: API request. + #: + #: If no token is set, then the value will be :obj:`None`. In this case, + #: no ``Authorization`` header will be sent. + #: + #: :type: :obj:`str`, optional + token: typing.Optional[str] + + #: The ``User-Agent`` header to send to Discord. + #: + #: Warning + #: ------- + #: Changing this value may lead to undesirable results, as Discord document + #: that they can actively IP ban any client that does not have a valid + #: ``User-Agent`` header that conforms to specific requirements. + #: Your mileage may vary (YMMV). + #: + #: :type: :obj:`str` + user_agent: str + + #: If ``True``, SSL certificates are verified for each request, and + #: invalid SSL certificates are rejected, causing an exception. If + #: ``False``, then unrecognised certificates that may be illegitimate + #: are accepted and ignored. + #: + #: :type: :obj:`bool` + verify_ssl: bool + + #: The API version number that is being used. + #: + #: :type: :obj:`int` + version: int + def __init__( self, *, - base_url="https://discordapp.com/api/v{0.version}", + base_url: str = urls.REST_API_URL, allow_redirects: bool = False, connector: typing.Optional[aiohttp.BaseConnector] = None, proxy_headers: typing.Optional[aiohttp.typedefs.LooseHeaders] = None, @@ -106,64 +229,19 @@ def __init__( token: typing.Optional[str], version: typing.Union[int, versions.HTTPAPIVersion] = versions.HTTPAPIVersion.STABLE, ): - #: Whether to allow redirects or not. - #: - #: :type: :obj:`bool` self.allow_redirects = allow_redirects - - #: The HTTP client session to use. - #: - #: :type: :obj:`aiohttp.ClientSession` self.client_session = aiohttp.ClientSession( connector=connector, version=aiohttp.HttpVersion11, json_serialize=json_serialize or json.dumps, ) - - #: The logger to use for this object. - #: - #: :type: :obj:`logging.Logger` self.logger = more_logging.get_named_logger(self) - - #: User agent to use. - #: - #: :type: :obj:`str` self.user_agent = user_agent.UserAgent().user_agent - - #: If ``True``, this will enforce SSL signed certificate verification, otherwise it will - #: ignore potentially malicious SSL certificates. - #: - #: :type: :obj:`bool` self.verify_ssl = verify_ssl - - #: Optional proxy URL to use for HTTP requests. - #: - #: :type: :obj:`str` self.proxy_url = proxy_url - - #: Optional authorization to use if using a proxy. - #: - #: :type: :obj:`aiohttp.BasicAuth` self.proxy_auth = proxy_auth - - #: Optional proxy headers to pass. - #: - #: :type: :obj:`typing.Mapping` [ :obj:`str`, :obj:`str` ] self.proxy_headers = proxy_headers - - #: Optional SSL context to use. - #: - #: :type: :obj:`ssl.SSLContext` self.ssl_context: ssl.SSLContext = ssl_context - - #: Optional timeout for HTTP requests. - #: - #: :type: :obj:`float` self.timeout = timeout - - #: How many responses have been received. - #: - #: :type: :obj:`int` self.in_count = 0 - self.version = int(version) self.base_url = base_url.format(self) self.global_ratelimiter = ratelimits.ManualRateLimiter() @@ -198,14 +276,14 @@ async def __aexit__(self, exc_type, exc_val, exc_tb): async def _request( self, - compiled_route, + compiled_route: routes.CompiledRoute, *, - headers=None, - query=None, - form_body=None, + headers: typing.Optional[typing.Dict[str, str]] = None, + query: typing.Optional[typing.Dict[str, typing.Any]] = None, + form_body: typing.Optional[aiohttp.FormData] = None, json_body: typing.Optional[typing.Union[typing.Dict[str, typing.Any], typing.Sequence[typing.Any]]] = None, reason: str = ..., - re_seekable_resources: typing.Collection[typing.Any] = more_collections.EMPTY_COLLECTION, + re_seekable_resources: typing.Collection[conversions.Seekable] = more_collections.EMPTY_COLLECTION, suppress_authorization_header: bool = False, **kwargs, ) -> typing.Union[typing.Dict[str, typing.Any], typing.Sequence[typing.Any], None]: @@ -223,11 +301,13 @@ async def _request( backoff = ratelimits.ExponentialBackOff() + seeks = {r.tell(): r for r in re_seekable_resources} + while True: # If we are uploading files with io objects in a form body, we need to reset the seeks to 0 to ensure # we can re-read the buffer. - for resource in re_seekable_resources: - resource.seek(0) + for pos, r in seeks.items(): + r.seek(pos) # Aids logging when lots of entries are being logged at once by matching a unique UUID # between the request and response @@ -1843,7 +1923,7 @@ async def modify_guild_member( # lgtm [py/similar-function] route = routes.GUILD_MEMBER.compile(self.PATCH, guild_id=guild_id, user_id=user_id) await self._request(route, json_body=payload, reason=reason) - async def modify_current_user_nick(self, guild_id: str, nick: typing.Optional[str], *, reason: str = ...,) -> None: + async def modify_current_user_nick(self, guild_id: str, nick: typing.Optional[str], *, reason: str = ..., ) -> None: """Edit the current user's nickname for a given guild. Parameters @@ -1869,7 +1949,7 @@ async def modify_current_user_nick(self, guild_id: str, nick: typing.Optional[st route = routes.OWN_GUILD_NICKNAME.compile(self.PATCH, guild_id=guild_id) await self._request(route, json_body=payload, reason=reason) - async def add_guild_member_role(self, guild_id: str, user_id: str, role_id: str, *, reason: str = ...,) -> None: + async def add_guild_member_role(self, guild_id: str, user_id: str, role_id: str, *, reason: str = ..., ) -> None: """Add a role to a given member. Parameters @@ -1894,7 +1974,7 @@ async def add_guild_member_role(self, guild_id: str, user_id: str, role_id: str, route = routes.GUILD_MEMBER_ROLE.compile(self.PUT, guild_id=guild_id, user_id=user_id, role_id=role_id) await self._request(route, reason=reason) - async def remove_guild_member_role(self, guild_id: str, user_id: str, role_id: str, *, reason: str = ...,) -> None: + async def remove_guild_member_role(self, guild_id: str, user_id: str, role_id: str, *, reason: str = ..., ) -> None: """Remove a role from a given member. Parameters @@ -1919,7 +1999,7 @@ async def remove_guild_member_role(self, guild_id: str, user_id: str, role_id: s route = routes.GUILD_MEMBER_ROLE.compile(self.DELETE, guild_id=guild_id, user_id=user_id, role_id=role_id) await self._request(route, reason=reason) - async def remove_guild_member(self, guild_id: str, user_id: str, *, reason: str = ...,) -> None: + async def remove_guild_member(self, guild_id: str, user_id: str, *, reason: str = ..., ) -> None: """Kick a user from a given guild. Parameters @@ -2021,7 +2101,7 @@ async def create_guild_ban( route = routes.GUILD_BAN.compile(self.PUT, guild_id=guild_id, user_id=user_id) await self._request(route, query=query) - async def remove_guild_ban(self, guild_id: str, user_id: str, *, reason: str = ...,) -> None: + async def remove_guild_ban(self, guild_id: str, user_id: str, *, reason: str = ..., ) -> None: """Un-bans a user from a given guild. Parameters @@ -2449,7 +2529,7 @@ async def modify_guild_integration( route = routes.GUILD_INTEGRATION.compile(self.PATCH, guild_id=guild_id, integration_id=integration_id) await self._request(route, json_body=payload, reason=reason) - async def delete_guild_integration(self, guild_id: str, integration_id: str, *, reason: str = ...,) -> None: + async def delete_guild_integration(self, guild_id: str, integration_id: str, *, reason: str = ..., ) -> None: """Delete an integration for the given guild. Parameters @@ -2568,7 +2648,7 @@ async def get_guild_vanity_url(self, guild_id: str) -> typing.Dict[str, typing.A route = routes.GUILD_VANITY_URL.compile(self.GET, guild_id=guild_id) return await self._request(route) - def get_guild_widget_image_url(self, guild_id: str, *, style: str = ...,) -> str: + def get_guild_widget_image_url(self, guild_id: str, *, style: str = ..., ) -> str: """Get the URL for a guild widget. Parameters diff --git a/hikari/oauth2.py b/hikari/oauth2.py index 1ffe45a479..06bedfc134 100644 --- a/hikari/oauth2.py +++ b/hikari/oauth2.py @@ -29,7 +29,7 @@ from hikari import permissions from hikari import snowflakes from hikari import users -from hikari.internal import cdn +from hikari.internal import urls from hikari.internal import marshaller @@ -209,7 +209,7 @@ def format_icon_url(self, fmt: str = "png", size: int = 2048) -> typing.Optional The string URL. """ if self.icon_hash: - return cdn.generate_cdn_url("team-icons", str(self.id), self.icon_hash, fmt=fmt, size=size) + return urls.generate_cdn_url("team-icons", str(self.id), self.icon_hash, fmt=fmt, size=size) return None @@ -348,7 +348,7 @@ def format_icon_url(self, fmt: str = "png", size: int = 2048) -> typing.Optional The string URL. """ if self.icon_hash: - return cdn.generate_cdn_url("app-icons", str(self.id), self.icon_hash, fmt=fmt, size=size) + return urls.generate_cdn_url("app-icons", str(self.id), self.icon_hash, fmt=fmt, size=size) return None @property @@ -374,5 +374,5 @@ def format_cover_image_url(self, fmt: str = "png", size: int = 2048) -> typing.O The string URL. """ if self.cover_image_hash: - return cdn.generate_cdn_url("app-assets", str(self.id), self.cover_image_hash, fmt=fmt, size=size) + return urls.generate_cdn_url("app-assets", str(self.id), self.cover_image_hash, fmt=fmt, size=size) return None diff --git a/hikari/users.py b/hikari/users.py index 876ce3fe7a..0895cbd131 100644 --- a/hikari/users.py +++ b/hikari/users.py @@ -26,7 +26,7 @@ from hikari import entities from hikari import snowflakes -from hikari.internal import cdn +from hikari.internal import urls from hikari.internal import marshaller @@ -86,12 +86,12 @@ def format_avatar_url(self, fmt: typing.Optional[str] = None, size: int = 2048) The string URL. """ if not self.avatar_hash: - return cdn.generate_cdn_url("embed/avatars", str(self.default_avatar), fmt="png", size=None) + return urls.generate_cdn_url("embed/avatars", str(self.default_avatar), fmt="png", size=None) if fmt is None and self.avatar_hash.startswith("a_"): fmt = "gif" elif fmt is None: fmt = "png" - return cdn.generate_cdn_url("avatars", str(self.id), self.avatar_hash, fmt=fmt, size=size) + return urls.generate_cdn_url("avatars", str(self.id), self.avatar_hash, fmt=fmt, size=size) @property def default_avatar(self) -> int: diff --git a/tests/hikari/internal/test_cdn.py b/tests/hikari/internal/test_cdn.py index 1f89fa6fe9..5ce6bff8aa 100644 --- a/tests/hikari/internal/test_cdn.py +++ b/tests/hikari/internal/test_cdn.py @@ -16,14 +16,14 @@ # # You should have received a copy of the GNU Lesser General Public License # along ith Hikari. If not, see . -from hikari.internal import cdn +from hikari.internal import urls def test_generate_cdn_url(): - url = cdn.generate_cdn_url("not", "a", "path", fmt="neko", size=42) + url = urls.generate_cdn_url("not", "a", "path", fmt="neko", size=42) assert url == "https://cdn.discordapp.com/not/a/path.neko?size=42" def test_generate_cdn_url_with_size_set_to_none(): - url = cdn.generate_cdn_url("not", "a", "path", fmt="neko", size=None) + url = urls.generate_cdn_url("not", "a", "path", fmt="neko", size=None) assert url == "https://cdn.discordapp.com/not/a/path.neko" diff --git a/tests/hikari/net/test_rest.py b/tests/hikari/net/test_rest.py index 22648e5ae6..28c57502a8 100644 --- a/tests/hikari/net/test_rest.py +++ b/tests/hikari/net/test_rest.py @@ -364,12 +364,18 @@ def seek(self, pos): self.seeked = True self.pos = pos - def tellg(self): + def tell(self): return self.pos def assert_seek_called(self): assert self.seeked + def read(self): + ... + + def close(self): + ... + rest_impl = await mock_rest_impl rest_impl.logger.debug.side_effect = exit_error seekable_resources = [SeekableResource(5), SeekableResource(37), SeekableResource(16)] diff --git a/tests/hikari/test_guilds.py b/tests/hikari/test_guilds.py index 99c4c61fe2..144d1acf8e 100644 --- a/tests/hikari/test_guilds.py +++ b/tests/hikari/test_guilds.py @@ -22,7 +22,7 @@ import pytest import hikari.internal.conversions -from hikari.internal import cdn +from hikari.internal import urls from hikari import emojis from hikari import entities from hikari import guilds @@ -616,9 +616,9 @@ def test_deserialize(self, partial_guild_obj): def test_format_icon_url(self, partial_guild_obj): mock_url = "https://cdn.discordapp.com/icons/152559372126519269/d4a983885dsaa7691ce8bcaaf945a.png?size=20" - with mock.patch.object(cdn, "generate_cdn_url", return_value=mock_url): + with mock.patch.object(urls, "generate_cdn_url", return_value=mock_url): url = partial_guild_obj.format_icon_url(fmt="nyaapeg", size=42) - cdn.generate_cdn_url.assert_called_once_with( + urls.generate_cdn_url.assert_called_once_with( "icons", "152559372126519269", "d4a983885dsaa7691ce8bcaaf945a", fmt="nyaapeg", size=42 ) assert url == mock_url @@ -626,9 +626,9 @@ def test_format_icon_url(self, partial_guild_obj): def test_format_icon_url_animated_default(self, partial_guild_obj): partial_guild_obj.icon_hash = "a_d4a983885dsaa7691ce8bcaaf945a" mock_url = "https://cdn.discordapp.com/icons/152559372126519269/a_d4a983885dsaa7691ce8bcaaf945a.gif?size=20" - with mock.patch.object(cdn, "generate_cdn_url", return_value=mock_url): + with mock.patch.object(urls, "generate_cdn_url", return_value=mock_url): url = partial_guild_obj.format_icon_url() - cdn.generate_cdn_url.assert_called_once_with( + urls.generate_cdn_url.assert_called_once_with( "icons", "152559372126519269", "a_d4a983885dsaa7691ce8bcaaf945a", fmt="gif", size=2048 ) assert url == mock_url @@ -636,25 +636,25 @@ def test_format_icon_url_animated_default(self, partial_guild_obj): def test_format_icon_url_none_animated_default(self, partial_guild_obj): partial_guild_obj.icon_hash = "d4a983885dsaa7691ce8bcaaf945a" mock_url = "https://cdn.discordapp.com/icons/152559372126519269/d4a983885dsaa7691ce8bcaaf945a.png?size=20" - with mock.patch.object(cdn, "generate_cdn_url", return_value=mock_url): + with mock.patch.object(urls, "generate_cdn_url", return_value=mock_url): url = partial_guild_obj.format_icon_url() - cdn.generate_cdn_url.assert_called_once_with( + urls.generate_cdn_url.assert_called_once_with( "icons", "152559372126519269", "d4a983885dsaa7691ce8bcaaf945a", fmt="png", size=2048 ) assert url == mock_url def test_format_icon_url_returns_none(self, partial_guild_obj): partial_guild_obj.icon_hash = None - with mock.patch.object(cdn, "generate_cdn_url", return_value=...): + with mock.patch.object(urls, "generate_cdn_url", return_value=...): url = partial_guild_obj.format_icon_url(fmt="nyaapeg", size=42) - cdn.generate_cdn_url.assert_not_called() + urls.generate_cdn_url.assert_not_called() assert url is None def test_format_icon_url(self, partial_guild_obj): mock_url = "https://cdn.discordapp.com/icons/152559372126519269/d4a983885dsaa7691ce8bcaaf945a.png?size=20" - with mock.patch.object(cdn, "generate_cdn_url", return_value=mock_url): + with mock.patch.object(urls, "generate_cdn_url", return_value=mock_url): url = partial_guild_obj.icon_url - cdn.generate_cdn_url.assert_called_once() + urls.generate_cdn_url.assert_called_once() assert url == mock_url @@ -733,69 +733,69 @@ def test_guild_obj(self, *patched_objs, test_guild_payload): def test_format_banner_url(self, test_guild_obj): mock_url = "https://not-al" - with mock.patch.object(cdn, "generate_cdn_url", return_value=mock_url): + with mock.patch.object(urls, "generate_cdn_url", return_value=mock_url): url = test_guild_obj.format_banner_url(fmt="nyaapeg", size=4000) - cdn.generate_cdn_url.assert_called_once_with( + urls.generate_cdn_url.assert_called_once_with( "banners", "265828729970753537", "1a2b3c", fmt="nyaapeg", size=4000 ) assert url is mock_url def test_format_banner_url_returns_none(self, test_guild_obj): test_guild_obj.banner_hash = None - with mock.patch.object(cdn, "generate_cdn_url", return_value=...): + with mock.patch.object(urls, "generate_cdn_url", return_value=...): url = test_guild_obj.format_banner_url() - cdn.generate_cdn_url.assert_not_called() + urls.generate_cdn_url.assert_not_called() assert url is None def test_banner_url(self, test_guild_obj): mock_url = "https://not-al" - with mock.patch.object(cdn, "generate_cdn_url", return_value=mock_url): + with mock.patch.object(urls, "generate_cdn_url", return_value=mock_url): url = test_guild_obj.banner_url - cdn.generate_cdn_url.assert_called_once() + urls.generate_cdn_url.assert_called_once() assert url is mock_url def test_format_discovery_splash_url(self, test_guild_obj): mock_url = "https://not-al" - with mock.patch.object(cdn, "generate_cdn_url", return_value=mock_url): + with mock.patch.object(urls, "generate_cdn_url", return_value=mock_url): url = test_guild_obj.format_discovery_splash_url(fmt="nyaapeg", size=4000) - cdn.generate_cdn_url.assert_called_once_with( + urls.generate_cdn_url.assert_called_once_with( "discovery-splashes", "265828729970753537", "famfamFAMFAMfam", fmt="nyaapeg", size=4000 ) assert url is mock_url def test_format_discovery_splash_returns_none(self, test_guild_obj): test_guild_obj.discovery_splash_hash = None - with mock.patch.object(cdn, "generate_cdn_url", return_value=...): + with mock.patch.object(urls, "generate_cdn_url", return_value=...): url = test_guild_obj.format_discovery_splash_url() - cdn.generate_cdn_url.assert_not_called() + urls.generate_cdn_url.assert_not_called() assert url is None def test_discover_splash_url(self, test_guild_obj): mock_url = "https://not-al" - with mock.patch.object(cdn, "generate_cdn_url", return_value=mock_url): + with mock.patch.object(urls, "generate_cdn_url", return_value=mock_url): url = test_guild_obj.discovery_splash_url - cdn.generate_cdn_url.assert_called_once() + urls.generate_cdn_url.assert_called_once() assert url is mock_url def test_format_splash_url(self, test_guild_obj): mock_url = "https://not-al" - with mock.patch.object(cdn, "generate_cdn_url", return_value=mock_url): + with mock.patch.object(urls, "generate_cdn_url", return_value=mock_url): url = test_guild_obj.format_splash_url(fmt="nyaapeg", size=4000) - cdn.generate_cdn_url.assert_called_once_with( + urls.generate_cdn_url.assert_called_once_with( "splashes", "265828729970753537", "0ff0ff0ff", fmt="nyaapeg", size=4000 ) assert url is mock_url def test_format_splash_returns_none(self, test_guild_obj): test_guild_obj.splash_hash = None - with mock.patch.object(cdn, "generate_cdn_url", return_value=...): + with mock.patch.object(urls, "generate_cdn_url", return_value=...): url = test_guild_obj.format_splash_url() - cdn.generate_cdn_url.assert_not_called() + urls.generate_cdn_url.assert_not_called() assert url is None def test_splash_url(self, test_guild_obj): mock_url = "https://not-al" - with mock.patch.object(cdn, "generate_cdn_url", return_value=mock_url): + with mock.patch.object(urls, "generate_cdn_url", return_value=mock_url): url = test_guild_obj.splash_url - cdn.generate_cdn_url.assert_called_once() + urls.generate_cdn_url.assert_called_once() assert url is mock_url diff --git a/tests/hikari/test_invites.py b/tests/hikari/test_invites.py index decc508a45..3a9ed77f5a 100644 --- a/tests/hikari/test_invites.py +++ b/tests/hikari/test_invites.py @@ -22,7 +22,7 @@ import pytest import hikari.internal.conversions -from hikari.internal import cdn +from hikari.internal import urls from hikari import channels from hikari import guilds from hikari import invites @@ -100,48 +100,48 @@ def test_deserialize(self, invite_guild_obj): def test_format_splash_url(self, invite_guild_obj): mock_url = "https://not-al" - with mock.patch.object(cdn, "generate_cdn_url", return_value=mock_url): + with mock.patch.object(urls, "generate_cdn_url", return_value=mock_url): url = invite_guild_obj.format_splash_url(fmt="nyaapeg", size=4000) - cdn.generate_cdn_url.assert_called_once_with( + urls.generate_cdn_url.assert_called_once_with( "splashes", "56188492224814744", "aSplashForSure", fmt="nyaapeg", size=4000 ) assert url is mock_url def test_format_splash_url_returns_none(self, invite_guild_obj): invite_guild_obj.splash_hash = None - with mock.patch.object(cdn, "generate_cdn_url", return_value=...): + with mock.patch.object(urls, "generate_cdn_url", return_value=...): url = invite_guild_obj.format_splash_url() - cdn.generate_cdn_url.assert_not_called() + urls.generate_cdn_url.assert_not_called() assert url is None def test_splash_url(self, invite_guild_obj): mock_url = "https://not-al" - with mock.patch.object(cdn, "generate_cdn_url", return_value=mock_url): + with mock.patch.object(urls, "generate_cdn_url", return_value=mock_url): url = invite_guild_obj.splash_url - cdn.generate_cdn_url.assert_called_once() + urls.generate_cdn_url.assert_called_once() assert url is mock_url def test_format_banner_url(self, invite_guild_obj): mock_url = "https://not-al" - with mock.patch.object(cdn, "generate_cdn_url", return_value=mock_url): + with mock.patch.object(urls, "generate_cdn_url", return_value=mock_url): url = invite_guild_obj.format_banner_url(fmt="nyaapeg", size=4000) - cdn.generate_cdn_url.assert_called_once_with( + urls.generate_cdn_url.assert_called_once_with( "banners", "56188492224814744", "aBannerForSure", fmt="nyaapeg", size=4000 ) assert url is mock_url def test_format_banner_url_returns_none(self, invite_guild_obj): invite_guild_obj.banner_hash = None - with mock.patch.object(cdn, "generate_cdn_url", return_value=...): + with mock.patch.object(urls, "generate_cdn_url", return_value=...): url = invite_guild_obj.format_banner_url() - cdn.generate_cdn_url.assert_not_called() + urls.generate_cdn_url.assert_not_called() assert url is None def test_banner_url(self, invite_guild_obj): mock_url = "https://not-al" - with mock.patch.object(cdn, "generate_cdn_url", return_value=mock_url): + with mock.patch.object(urls, "generate_cdn_url", return_value=mock_url): url = invite_guild_obj.banner_url - cdn.generate_cdn_url.assert_called_once() + urls.generate_cdn_url.assert_called_once() assert url is mock_url diff --git a/tests/hikari/test_oauth2.py b/tests/hikari/test_oauth2.py index 991fbc6db3..a763ec7496 100644 --- a/tests/hikari/test_oauth2.py +++ b/tests/hikari/test_oauth2.py @@ -19,7 +19,7 @@ import cymock as mock import pytest -from hikari.internal import cdn +from hikari.internal import urls from hikari import guilds from hikari import oauth2 from hikari import users @@ -179,23 +179,23 @@ def test_deserialize(self, team_payload, member_payload): def test_format_icon_url(self): mock_team = _helpers.create_autospec(oauth2.Team, icon_hash="3o2o32o", id=22323) mock_url = "https://cdn.discordapp.com/team-icons/22323/3o2o32o.jpg?size=64" - with mock.patch.object(cdn, "generate_cdn_url", return_value=mock_url): + with mock.patch.object(urls, "generate_cdn_url", return_value=mock_url): url = oauth2.Team.format_icon_url(mock_team, fmt="jpg", size=64) - cdn.generate_cdn_url.assert_called_once_with("team-icons", "22323", "3o2o32o", fmt="jpg", size=64) + urls.generate_cdn_url.assert_called_once_with("team-icons", "22323", "3o2o32o", fmt="jpg", size=64) assert url == mock_url def test_format_icon_url_returns_none(self): mock_team = _helpers.create_autospec(oauth2.Team, icon_hash=None, id=22323) - with mock.patch.object(cdn, "generate_cdn_url", return_value=...): + with mock.patch.object(urls, "generate_cdn_url", return_value=...): url = oauth2.Team.format_icon_url(mock_team, fmt="jpg", size=64) - cdn.generate_cdn_url.assert_not_called() + urls.generate_cdn_url.assert_not_called() assert url is None def test_icon_url(self, team_obj): mock_url = "https://cdn.discordapp.com/team-icons/202020202/hashtag.png?size=2048" - with mock.patch.object(cdn, "generate_cdn_url", return_value=mock_url): + with mock.patch.object(urls, "generate_cdn_url", return_value=mock_url): url = team_obj.icon_url - cdn.generate_cdn_url.assert_called_once() + urls.generate_cdn_url.assert_called_once() assert url == mock_url @@ -238,44 +238,44 @@ def mock_application(self): def test_icon_url(self, application_obj): mock_url = "https://cdn.discordapp.com/app-icons/209333111222/iwiwiwiwiw.png?size=2048" - with mock.patch.object(cdn, "generate_cdn_url", return_value=mock_url): + with mock.patch.object(urls, "generate_cdn_url", return_value=mock_url): url = application_obj.icon_url - cdn.generate_cdn_url.assert_called_once() + urls.generate_cdn_url.assert_called_once() assert url == "https://cdn.discordapp.com/app-icons/209333111222/iwiwiwiwiw.png?size=2048" def test_format_icon_url(self, mock_application): mock_application.icon_hash = "wosososoos" mock_url = "https://cdn.discordapp.com/app-icons/22222/wosososoos.jpg?size=4" - with mock.patch.object(cdn, "generate_cdn_url", return_value=mock_url): + with mock.patch.object(urls, "generate_cdn_url", return_value=mock_url): url = oauth2.Application.format_icon_url(mock_application, fmt="jpg", size=4) - cdn.generate_cdn_url.assert_called_once_with("app-icons", "22222", "wosososoos", fmt="jpg", size=4) + urls.generate_cdn_url.assert_called_once_with("app-icons", "22222", "wosososoos", fmt="jpg", size=4) assert url == mock_url def test_format_icon_url_returns_none(self, mock_application): mock_application.icon_hash = None - with mock.patch.object(cdn, "generate_cdn_url", return_value=...): + with mock.patch.object(urls, "generate_cdn_url", return_value=...): url = oauth2.Application.format_icon_url(mock_application, fmt="jpg", size=4) - cdn.generate_cdn_url.assert_not_called() + urls.generate_cdn_url.assert_not_called() assert url is None def test_cover_image_url(self, application_obj): mock_url = "https://cdn.discordapp.com/app-assets/209333111222/hashmebaby.png?size=2048" - with mock.patch.object(cdn, "generate_cdn_url", return_value=mock_url): + with mock.patch.object(urls, "generate_cdn_url", return_value=mock_url): url = application_obj.cover_image_url - cdn.generate_cdn_url.assert_called_once() + urls.generate_cdn_url.assert_called_once() assert url == mock_url def test_format_cover_image_url(self, mock_application): mock_application.cover_image_hash = "wowowowowo" mock_url = "https://cdn.discordapp.com/app-assets/22222/wowowowowo.jpg?size=42" - with mock.patch.object(cdn, "generate_cdn_url", return_value=mock_url): + with mock.patch.object(urls, "generate_cdn_url", return_value=mock_url): url = oauth2.Application.format_cover_image_url(mock_application, fmt="jpg", size=42) - cdn.generate_cdn_url.assert_called_once_with("app-assets", "22222", "wowowowowo", fmt="jpg", size=42) + urls.generate_cdn_url.assert_called_once_with("app-assets", "22222", "wowowowowo", fmt="jpg", size=42) assert url == mock_url def test_format_cover_image_url_returns_none(self, mock_application): mock_application.cover_image_hash = None - with mock.patch.object(cdn, "generate_cdn_url", return_value=...): + with mock.patch.object(urls, "generate_cdn_url", return_value=...): url = oauth2.Application.format_cover_image_url(mock_application, fmt="jpg", size=42) - cdn.generate_cdn_url.assert_not_called() + urls.generate_cdn_url.assert_not_called() assert url is None diff --git a/tests/hikari/test_users.py b/tests/hikari/test_users.py index 54841c22b6..f05e50e9ad 100644 --- a/tests/hikari/test_users.py +++ b/tests/hikari/test_users.py @@ -19,7 +19,7 @@ import cymock as mock import pytest -from hikari.internal import cdn +from hikari.internal import urls from hikari import users @@ -62,9 +62,9 @@ def test_deserialize(self, user_obj): def test_avatar_url(self, user_obj): mock_url = "https://cdn.discordapp.com/avatars/115590097100865541" - with mock.patch.object(cdn, "generate_cdn_url", return_value=mock_url): + with mock.patch.object(urls, "generate_cdn_url", return_value=mock_url): url = user_obj.avatar_url - cdn.generate_cdn_url.assert_called_once() + urls.generate_cdn_url.assert_called_once() assert url == mock_url def test_default_avatar(self, user_obj): @@ -73,9 +73,9 @@ def test_default_avatar(self, user_obj): def test_format_avatar_url_when_animated(self, user_obj): mock_url = "https://cdn.discordapp.com/avatars/115590097100865541/a_820d0e50543216e812ad94e6ab7.gif?size=3232" user_obj.avatar_hash = "a_820d0e50543216e812ad94e6ab7" - with mock.patch.object(cdn, "generate_cdn_url", return_value=mock_url): + with mock.patch.object(urls, "generate_cdn_url", return_value=mock_url): url = user_obj.format_avatar_url(size=3232) - cdn.generate_cdn_url.assert_called_once_with( + urls.generate_cdn_url.assert_called_once_with( "avatars", "115590097100865541", "a_820d0e50543216e812ad94e6ab7", fmt="gif", size=3232 ) assert url == mock_url @@ -83,16 +83,16 @@ def test_format_avatar_url_when_animated(self, user_obj): def test_format_avatar_url_default(self, user_obj): user_obj.avatar_hash = None mock_url = "https://cdn.discordapp.com/embed/avatars/2.png" - with mock.patch.object(cdn, "generate_cdn_url", return_value=mock_url): + with mock.patch.object(urls, "generate_cdn_url", return_value=mock_url): url = user_obj.format_avatar_url(size=3232) - cdn.generate_cdn_url("embed/avatars", "115590097100865541", fmt="png", size=None) + urls.generate_cdn_url("embed/avatars", "115590097100865541", fmt="png", size=None) assert url == mock_url def test_format_avatar_url_when_format_specified(self, user_obj): mock_url = "https://cdn.discordapp.com/avatars/115590097100865541/b3b24c6d7c37067061a8.nyaapeg?size=1024" - with mock.patch.object(cdn, "generate_cdn_url", return_value=mock_url): + with mock.patch.object(urls, "generate_cdn_url", return_value=mock_url): url = user_obj.format_avatar_url(fmt="nyaapeg", size=1024) - cdn.generate_cdn_url.assert_called_once_with( + urls.generate_cdn_url.assert_called_once_with( "avatars", "115590097100865541", "b3b24c6d7cbcdec129d5d537067061a8", fmt="nyaapeg", size=1024 ) assert url == mock_url From a2fb41d2d74f3aee320dca0cab3564d4e747ea90 Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Sat, 11 Apr 2020 20:36:08 +0100 Subject: [PATCH 087/922] Added typing.Final typehints and fixed some lint/docstyle issues. Added '/' operators to conversions module. --- hikari/clients/gateway_runner.py | 17 ++++-- hikari/internal/conversions.py | 57 ++++++++++++++----- hikari/internal/marshaller.py | 55 +++++++++++------- hikari/internal/more_collections.py | 15 ++--- hikari/internal/urls.py | 6 +- hikari/net/ratelimits.py | 5 +- hikari/net/rest.py | 14 ++--- pylint.ini | 8 ++- tests/hikari/internal/test_marshaller.py | 6 +- .../hikari/internal/test_marshaller_pep563.py | 6 +- 10 files changed, 124 insertions(+), 65 deletions(-) diff --git a/hikari/clients/gateway_runner.py b/hikari/clients/gateway_runner.py index 0ca1ee1a92..dc0d16cf71 100644 --- a/hikari/clients/gateway_runner.py +++ b/hikari/clients/gateway_runner.py @@ -24,6 +24,7 @@ import logging import os import sys +import typing import click @@ -31,7 +32,8 @@ from hikari.clients import gateway_managers from hikari.state import stateless_event_managers -logger_levels = ("DEBUG", "INFO", "WARNING", "ERROR", "NOTSET") + +_LOGGER_LEVELS: typing.Final[typing.Sequence[str]] = ["DEBUG", "INFO", "WARNING", "ERROR", "NOTSET"] def _supports_color(): @@ -42,23 +44,26 @@ def _supports_color(): return supported_platform and is_a_tty -_color_format = ( +_COLOR_FORMAT: typing.Final[str] = ( "\033[1;35m%(levelname)1.1s \033[0;37m%(name)25.25s \033[0;31m%(asctime)23.23s \033[1;34m%(module)-15.15s " "\033[1;32m#%(lineno)-4d \033[0m:: \033[0;33m%(message)s\033[0m" ) -_regular_format = "%(levelname)1.1s %(name)25.25s %(asctime)23.23s %(module)-15.15s #%(lineno)-4d :: %(message)s" + +_REGULAR_FORMAT: typing.Final[str] = ( + "%(levelname)1.1s %(name)25.25s %(asctime)23.23s %(module)-15.15s #%(lineno)-4d :: %(message)s" +) @click.command() @click.option("--compression", default=True, type=click.BOOL, help="Enable or disable gateway compression.") @click.option("--color", default=_supports_color(), type=click.BOOL, help="Whether to enable or disable color.") @click.option("--debug", default=False, type=click.BOOL, help="Enable or disable debug mode.") -@click.option("--logger", envvar="LOGGER", default="INFO", type=click.Choice(logger_levels), help="Logger verbosity.") +@click.option("--logger", envvar="LOGGER", default="INFO", type=click.Choice(_LOGGER_LEVELS), help="Logger verbosity.") @click.option("--shards", default=1, type=click.IntRange(min=1), help="The number of shards to explicitly use.") @click.option("--token", required=True, envvar="TOKEN", help="The token to use to authenticate with Discord.") @click.option("--url", default="wss://gateway.discord.gg/", help="The websocket URL to connect to.") @click.option("--verify-ssl", default=True, type=click.BOOL, help="Enable or disable SSL verification.") -@click.option("--version", default=7, type=click.IntRange(min=6), help="Version of the gateway to use.") +@click.option("--version", default=6, type=click.IntRange(min=6), help="Version of the gateway to use.") def run_gateway(compression, color, debug, logger, shards, token, url, verify_ssl, version) -> None: """:mod:`click` command line client for running a test gateway connection. @@ -67,7 +72,7 @@ def run_gateway(compression, color, debug, logger, shards, token, url, verify_ss """ logging.captureWarnings(True) - logging.basicConfig(level=logger, format=_color_format if color else _regular_format, stream=sys.stdout) + logging.basicConfig(level=logger, format=_COLOR_FORMAT if color else _REGULAR_FORMAT, stream=sys.stdout) manager = stateless_event_managers.StatelessEventManagerImpl() diff --git a/hikari/internal/conversions.py b/hikari/internal/conversions.py index 6fa595f829..e03cddc175 100644 --- a/hikari/internal/conversions.py +++ b/hikari/internal/conversions.py @@ -42,7 +42,6 @@ import types import typing - DISCORD_EPOCH: typing.Final[int] = 1_420_070_400 ISO_8601_DATE_PART: typing.Final[typing.Pattern] = re.compile(r"^(\d{4})-(\d{2})-(\d{2})") ISO_8601_TIME_PART: typing.Final[typing.Pattern] = re.compile(r"T(\d{2}):(\d{2}):(\d{2})(?:\.(\d{1,6}))?", re.I) @@ -57,7 +56,7 @@ FileLikeT = typing.Union[BytesLikeT, io.BufferedRandom, io.BufferedReader, io.BufferedRWPair] -def nullable_cast(value: CastInputT, cast: TypeCastT) -> ResultT: +def nullable_cast(value: CastInputT, cast: TypeCastT, /) -> ResultT: """Attempt to cast the given ``value`` with the given ``cast``. This will only succeed if ``value`` is not ``None``. If it is ``None``, then @@ -68,7 +67,7 @@ def nullable_cast(value: CastInputT, cast: TypeCastT) -> ResultT: return cast(value) -def try_cast(value: CastInputT, cast: TypeCastT, default: DefaultT = None) -> ResultT: +def try_cast(value: CastInputT, cast: TypeCastT, default: DefaultT = None, /) -> ResultT: """Try to cast the given value to the given cast. If it throws a :obj:`Exception` or derivative, it will return ``default`` @@ -79,7 +78,7 @@ def try_cast(value: CastInputT, cast: TypeCastT, default: DefaultT = None) -> Re return default -def try_cast_or_defer_unary_operator(type_): +def try_cast_or_defer_unary_operator(type_: typing.Type, /): """Return a unary operator that will try to cast the given input to the type provided. Parameters @@ -95,6 +94,7 @@ def put_if_specified( key: typing.Hashable, value: typing.Any, type_after: typing.Optional[TypeCastT] = None, + / ) -> None: """Add a value to the mapping under the given key as long as the value is not ``...``. @@ -116,7 +116,7 @@ def put_if_specified( mapping[key] = value -def image_bytes_to_image_data(img_bytes: typing.Optional[bytes] = None) -> typing.Optional[str]: +def image_bytes_to_image_data(img_bytes: typing.Optional[bytes] = None, /) -> typing.Optional[str]: """Encode image bytes into an image data string. Parameters @@ -157,7 +157,7 @@ def image_bytes_to_image_data(img_bytes: typing.Optional[bytes] = None) -> typin return f"data:{img_type};base64,{image_data}" -def parse_http_date(date_str: str) -> datetime.datetime: +def parse_http_date(date_str: str, /) -> datetime.datetime: """Return the HTTP date as a datetime object. Parameters @@ -177,7 +177,7 @@ def parse_http_date(date_str: str) -> datetime.datetime: return email.utils.parsedate_to_datetime(date_str) -def parse_iso_8601_ts(date_string: str) -> datetime.datetime: +def parse_iso_8601_ts(date_string: str, /) -> datetime.datetime: """Parse an ISO 8601 date string into a :obj:`datetime.datetime` object. Parameters @@ -215,7 +215,7 @@ def parse_iso_8601_ts(date_string: str) -> datetime.datetime: return datetime.datetime(year, month, day, hour, minute, second, partial, timezone) -def discord_epoch_to_datetime(epoch: int) -> datetime.datetime: +def discord_epoch_to_datetime(epoch: int, /) -> datetime.datetime: """Parse a Discord epoch into a :obj:`datetime.datetime` object. Parameters @@ -231,7 +231,7 @@ def discord_epoch_to_datetime(epoch: int) -> datetime.datetime: return datetime.datetime.fromtimestamp(epoch / 1000 + DISCORD_EPOCH, datetime.timezone.utc) -def unix_epoch_to_ts(epoch: int) -> datetime.datetime: +def unix_epoch_to_ts(epoch: int, /) -> datetime.datetime: """Parse a UNIX epoch to a :obj:`datetime.datetime` object. Parameters @@ -250,20 +250,47 @@ def unix_epoch_to_ts(epoch: int) -> datetime.datetime: class Seekable(typing.Protocol[typing.AnyStr]): """Structural type for an IO object that supports seek operations.""" - def seek(self, pos: int) -> None: - ... + def seek( + self, + offset: int, + whence: typing.Union[typing.Literal[0], typing.Literal[1], typing.Literal[2]] = 0, + / + ) -> None: + """Seek to the given offset. + + Parameters + ---------- + offset : :obj:`int` + The offset to seek to. + whence : :obj:`int` + If ``0``, as the default, then use absolute file positioning. + If ``1``, then seek to the current position. + If ``2``, then seek relative to the end of the file. + """ def tell(self) -> int: - ... + """Return the stream position. + + Returns + ------- + :obj:`int` + The stream position. + """ def read(self) -> typing.AnyStr: - ... + """Read part of a string. + + Returns + ------- + :obj:`str` + The string that was read. + """ def close(self) -> None: - ... + """Close the stream.""" -def make_resource_seekable(resource: typing.Any) -> Seekable: +def make_resource_seekable(resource: typing.Any, /) -> Seekable: """Make a seekable resource to use off some representation of data. This supports :obj:`bytes`, :obj:`bytearray`, :obj:`memoryview`, and diff --git a/hikari/internal/marshaller.py b/hikari/internal/marshaller.py index c86293b51b..1bcdd9b749 100644 --- a/hikari/internal/marshaller.py +++ b/hikari/internal/marshaller.py @@ -34,6 +34,7 @@ "HikariEntityMarshaller", ] +import enum import functools import importlib import operator @@ -44,19 +45,17 @@ from hikari.internal import assertions -_RAW_NAME_ATTR = __name__ + "_RAW_NAME" -_SERIALIZER_ATTR = __name__ + "_SERIALIZER" -_DESERIALIZER_ATTR = __name__ + "_DESERIALIZER" -_TRANSIENT_ATTR = __name__ + "_TRANSIENT" -_IF_UNDEFINED = __name__ + "IF_UNDEFINED" -_IF_NONE = __name__ + "_IF_NONE" - -MARSHALLER_META_ATTR = "__hikari_marshaller_meta_attr__" - -PASSED_THROUGH_SINGLETONS = (False, True, None) - -RAISE = object() - +_RAW_NAME_ATTR: typing.Final[str] = __name__ + "_RAW_NAME" +_SERIALIZER_ATTR: typing.Final[str] = __name__ + "_SERIALIZER" +_DESERIALIZER_ATTR: typing.Final[str] = __name__ + "_DESERIALIZER" +_TRANSIENT_ATTR: typing.Final[str] = __name__ + "_TRANSIENT" +_IF_UNDEFINED: typing.Final[str] = __name__ + "IF_UNDEFINED" +_IF_NONE: typing.Final[str] = __name__ + "_IF_NONE" +_PASSED_THROUGH_SINGLETONS: typing.Final[typing.Sequence[bool]] = [False, True, None] +RAISE: typing.Final[typing.Any] = object() + +IntFlagT = typing.TypeVar("IntFlagT", bound=enum.IntFlag) +RawIntFlagValueT = typing.Union[typing.AnyStr, typing.SupportsInt, int] EntityT = typing.TypeVar("EntityT", contravariant=True) @@ -100,7 +99,10 @@ def dereference_handle(handle_string: str) -> typing.Any: return weakref.proxy(obj) -def dereference_int_flag(int_flag_type, raw_value) -> typing.SupportsInt: +def dereference_int_flag( + int_flag_type: typing.Type[IntFlagT], + raw_value: typing.Union[RawIntFlagValueT, typing.Collection[RawIntFlagValueT]], +) -> IntFlagT: """Cast to the provided :obj:`enum.IntFlag` type. This supports resolving bitfield integers as well as decoding a sequence @@ -110,8 +112,21 @@ def dereference_int_flag(int_flag_type, raw_value) -> typing.SupportsInt: ---------- int_flag_type : :obj:`typing.Type` [ :obj:`enum.IntFlag` ] The type of the int flag to check. - raw_value + raw_value : ``Castable Value`` The raw value to convert. + + Returns + ------- + The cast value as a flag. + + Notes + ----- + Types that are a ``Castable Value`` include: + - :obj:`str` + - :obj:`int` + - :obj:`typing.SupportsInt` + - :obj:`typing.Collection` [ ``Castable Value`` ] - values will be combined + using functional reduction via the :obj:operator.or_` operator. """ if isinstance(raw_value, str) and raw_value.isdigit(): raw_value = int(raw_value) @@ -137,7 +152,7 @@ def attrib( transient: bool = False, serializer: typing.Optional[typing.Callable[[typing.Any], typing.Any]] = None, **kwargs, -) -> typing.Any: +) -> attr.Attribute: """Create an :func:`attr.ib` with marshaller metadata attached. Parameters @@ -205,7 +220,7 @@ def error(*_, **__) -> typing.NoReturn: def _default_validator(value: typing.Any): assertions.assert_that( - value is RAISE or value in PASSED_THROUGH_SINGLETONS or callable(value), + value is RAISE or value in _PASSED_THROUGH_SINGLETONS or callable(value), message=( "Invalid default factory passed for `if_undefined` or `if_none`; " f"expected a callable or one of the 'passed through singletons' but got {value}." @@ -365,7 +380,7 @@ def deserialize(self, raw_data: typing.Mapping[str, typing.Any], target_type: ty f"{target_type.__module__}.{target_type.__qualname__} due to required field {a.field_name} " f"(from raw key {a.raw_name}) not being included in the input payload\n\n{raw_data}" ) - if a.if_undefined in PASSED_THROUGH_SINGLETONS: + if a.if_undefined in _PASSED_THROUGH_SINGLETONS: kwargs[kwarg_name] = a.if_undefined else: kwargs[kwarg_name] = a.if_undefined() @@ -378,7 +393,7 @@ def deserialize(self, raw_data: typing.Mapping[str, typing.Any], target_type: ty f"{target_type.__module__}.{target_type.__qualname__} due to non-nullable field {a.field_name}" f" (from raw key {a.raw_name}) being `None` in the input payload\n\n{raw_data}" ) - if a.if_none in PASSED_THROUGH_SINGLETONS: + if a.if_none in _PASSED_THROUGH_SINGLETONS: kwargs[kwarg_name] = a.if_none else: kwargs[kwarg_name] = a.if_none() @@ -473,6 +488,8 @@ class MyEntity: ... """ + def decorator(cls): return marshaller.register(cls) + return decorator diff --git a/hikari/internal/more_collections.py b/hikari/internal/more_collections.py index 084147c0c0..3dc5d7fee2 100644 --- a/hikari/internal/more_collections.py +++ b/hikari/internal/more_collections.py @@ -35,15 +35,16 @@ import weakref -EMPTY_SEQUENCE: typing.Sequence = tuple() -EMPTY_SET: typing.AbstractSet = frozenset() -EMPTY_COLLECTION: typing.Collection = tuple() -EMPTY_DICT: typing.Mapping = types.MappingProxyType({}) -EMPTY_GENERATOR_EXPRESSION = (_ for _ in EMPTY_COLLECTION) - -K = typing.TypeVar("K") +T = typing.TypeVar("T") +K = typing.TypeVar("K", bound=typing.Hashable) V = typing.TypeVar("V") +EMPTY_SEQUENCE: typing.Final[typing.Sequence[T]] = tuple() +EMPTY_SET: typing.Final[typing.AbstractSet[T]] = frozenset() +EMPTY_COLLECTION: typing.Final[typing.Collection[T]] = tuple() +EMPTY_DICT: typing.Final[typing.Mapping[K, V]] = types.MappingProxyType({}) +EMPTY_GENERATOR_EXPRESSION: typing.Final[typing.Iterator[T]] = (_ for _ in EMPTY_COLLECTION) + class WeakKeyDictionary(weakref.WeakKeyDictionary, typing.MutableMapping[K, V]): """A dictionary that has weak references to the keys. diff --git a/hikari/internal/urls.py b/hikari/internal/urls.py index 6c919ceec5..2e65db3a86 100644 --- a/hikari/internal/urls.py +++ b/hikari/internal/urls.py @@ -29,7 +29,11 @@ import urllib.parse -BASE_CDN_URL = "https://cdn.discordapp.com" +#: The URL for the CDN. +#: +#: :type: :obj:`str` +BASE_CDN_URL: typing.Final[str] = "https://cdn.discordapp.com" + #: The URL for the REST API. This contains a version number parameter that #: should be interpolated. #: diff --git a/hikari/net/ratelimits.py b/hikari/net/ratelimits.py index 63a8440230..411a7b22ba 100644 --- a/hikari/net/ratelimits.py +++ b/hikari/net/ratelimits.py @@ -183,7 +183,10 @@ from hikari.internal import more_logging from hikari.net import routes -UNKNOWN_HASH = "UNKNOWN" +#: The hash used for an unknown bucket that has not yet been resolved. +#: +#: :type: :obj:`str` +UNKNOWN_HASH: typing.Final[str] = "UNKNOWN" class BaseRateLimiter(abc.ABC): diff --git a/hikari/net/rest.py b/hikari/net/rest.py index adc09889e0..87504bd73e 100644 --- a/hikari/net/rest.py +++ b/hikari/net/rest.py @@ -1923,7 +1923,7 @@ async def modify_guild_member( # lgtm [py/similar-function] route = routes.GUILD_MEMBER.compile(self.PATCH, guild_id=guild_id, user_id=user_id) await self._request(route, json_body=payload, reason=reason) - async def modify_current_user_nick(self, guild_id: str, nick: typing.Optional[str], *, reason: str = ..., ) -> None: + async def modify_current_user_nick(self, guild_id: str, nick: typing.Optional[str], *, reason: str = ...,) -> None: """Edit the current user's nickname for a given guild. Parameters @@ -1949,7 +1949,7 @@ async def modify_current_user_nick(self, guild_id: str, nick: typing.Optional[st route = routes.OWN_GUILD_NICKNAME.compile(self.PATCH, guild_id=guild_id) await self._request(route, json_body=payload, reason=reason) - async def add_guild_member_role(self, guild_id: str, user_id: str, role_id: str, *, reason: str = ..., ) -> None: + async def add_guild_member_role(self, guild_id: str, user_id: str, role_id: str, *, reason: str = ...,) -> None: """Add a role to a given member. Parameters @@ -1974,7 +1974,7 @@ async def add_guild_member_role(self, guild_id: str, user_id: str, role_id: str, route = routes.GUILD_MEMBER_ROLE.compile(self.PUT, guild_id=guild_id, user_id=user_id, role_id=role_id) await self._request(route, reason=reason) - async def remove_guild_member_role(self, guild_id: str, user_id: str, role_id: str, *, reason: str = ..., ) -> None: + async def remove_guild_member_role(self, guild_id: str, user_id: str, role_id: str, *, reason: str = ...,) -> None: """Remove a role from a given member. Parameters @@ -1999,7 +1999,7 @@ async def remove_guild_member_role(self, guild_id: str, user_id: str, role_id: s route = routes.GUILD_MEMBER_ROLE.compile(self.DELETE, guild_id=guild_id, user_id=user_id, role_id=role_id) await self._request(route, reason=reason) - async def remove_guild_member(self, guild_id: str, user_id: str, *, reason: str = ..., ) -> None: + async def remove_guild_member(self, guild_id: str, user_id: str, *, reason: str = ...,) -> None: """Kick a user from a given guild. Parameters @@ -2101,7 +2101,7 @@ async def create_guild_ban( route = routes.GUILD_BAN.compile(self.PUT, guild_id=guild_id, user_id=user_id) await self._request(route, query=query) - async def remove_guild_ban(self, guild_id: str, user_id: str, *, reason: str = ..., ) -> None: + async def remove_guild_ban(self, guild_id: str, user_id: str, *, reason: str = ...,) -> None: """Un-bans a user from a given guild. Parameters @@ -2529,7 +2529,7 @@ async def modify_guild_integration( route = routes.GUILD_INTEGRATION.compile(self.PATCH, guild_id=guild_id, integration_id=integration_id) await self._request(route, json_body=payload, reason=reason) - async def delete_guild_integration(self, guild_id: str, integration_id: str, *, reason: str = ..., ) -> None: + async def delete_guild_integration(self, guild_id: str, integration_id: str, *, reason: str = ...,) -> None: """Delete an integration for the given guild. Parameters @@ -2648,7 +2648,7 @@ async def get_guild_vanity_url(self, guild_id: str) -> typing.Dict[str, typing.A route = routes.GUILD_VANITY_URL.compile(self.GET, guild_id=guild_id) return await self._request(route) - def get_guild_widget_image_url(self, guild_id: str, *, style: str = ..., ) -> str: + def get_guild_widget_image_url(self, guild_id: str, *, style: str = ...,) -> str: """Get the URL for a guild widget. Parameters diff --git a/pylint.ini b/pylint.ini index 49c1bfadf2..9d546463a4 100644 --- a/pylint.ini +++ b/pylint.ini @@ -31,7 +31,7 @@ limit-inference-results=100 load-plugins=pylint_junit # Pickle collected data for later comparisons. -persistent=no +persistent=yes # Specify a configuration file. #rcfile= @@ -521,7 +521,9 @@ allow-wildcard-with-all=yes analyse-fallback-blocks=no # Deprecated modules which should not be used, separated by a comma. -deprecated-modules=optparse,tkinter.tix +deprecated-modules=optparse, + tkinter.tix, + collections.abc # Create a graph of external dependencies in the given file (report RP0402 must # not be disabled). @@ -540,7 +542,7 @@ int-import-graph= known-standard-library= # Force import order to recognize a module as part of a third party library. -known-third-party=enchant +known-third-party= # Couples of modules and preferred modules, separated by a comma. preferred-modules= diff --git a/tests/hikari/internal/test_marshaller.py b/tests/hikari/internal/test_marshaller.py index a35625ff7b..758159282c 100644 --- a/tests/hikari/internal/test_marshaller.py +++ b/tests/hikari/internal/test_marshaller.py @@ -69,7 +69,7 @@ def method_stub(value): @pytest.mark.parametrize( - "data", [lambda x: "ok", *marshaller.PASSED_THROUGH_SINGLETONS, marshaller.RAISE, dict, method_stub] + "data", [lambda x: "ok", *marshaller._PASSED_THROUGH_SINGLETONS, marshaller.RAISE, dict, method_stub] ) def test_default_validator(data): marshaller._default_validator(data) @@ -126,7 +126,7 @@ class User: assert isinstance(result, User) assert result.id == "12345" - @pytest.mark.parametrize("singleton", marshaller.PASSED_THROUGH_SINGLETONS) + @pytest.mark.parametrize("singleton", marshaller._PASSED_THROUGH_SINGLETONS) def test_deserialize_not_required_success_if_not_specified(self, marshaller_impl, singleton): @marshaller.marshallable(marshaller=marshaller_impl) @attr.s() @@ -173,7 +173,7 @@ class User: assert isinstance(result, User) assert result.id == "12345" - @pytest.mark.parametrize("singleton", marshaller.PASSED_THROUGH_SINGLETONS) + @pytest.mark.parametrize("singleton", marshaller._PASSED_THROUGH_SINGLETONS) def test_deserialize_nullable_success_if_null(self, marshaller_impl, singleton): @marshaller.marshallable(marshaller=marshaller_impl) @attr.s() diff --git a/tests/hikari/internal/test_marshaller_pep563.py b/tests/hikari/internal/test_marshaller_pep563.py index d9d8e17787..f0a1247b41 100644 --- a/tests/hikari/internal/test_marshaller_pep563.py +++ b/tests/hikari/internal/test_marshaller_pep563.py @@ -95,7 +95,7 @@ def method_stub(value): @pytest.mark.parametrize( - "data", [lambda x: "ok", *marshaller.PASSED_THROUGH_SINGLETONS, marshaller.RAISE, dict, method_stub] + "data", [lambda x: "ok", *marshaller._PASSED_THROUGH_SINGLETONS, marshaller.RAISE, dict, method_stub] ) def test_default_validator(data): marshaller._default_validator(data) @@ -133,7 +133,7 @@ class User: assert isinstance(result, User) assert result.id == "12345" - @pytest.mark.parametrize("singleton", marshaller.PASSED_THROUGH_SINGLETONS) + @pytest.mark.parametrize("singleton", marshaller._PASSED_THROUGH_SINGLETONS) def test_deserialize_not_required_success_if_not_specified(self, marshaller_impl, singleton): @marshaller.marshallable(marshaller=marshaller_impl) @attr.s() @@ -180,7 +180,7 @@ class User: assert isinstance(result, User) assert result.id == "12345" - @pytest.mark.parametrize("singleton", marshaller.PASSED_THROUGH_SINGLETONS) + @pytest.mark.parametrize("singleton", marshaller._PASSED_THROUGH_SINGLETONS) def test_deserialize_nullable_success_if_null(self, marshaller_impl, singleton): @marshaller.marshallable(marshaller=marshaller_impl) @attr.s() From dbdca8e2fd6689133a5c2c935178fbe5d0779f8d Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Sat, 11 Apr 2020 22:04:55 +0100 Subject: [PATCH 088/922] Added more missing Finals to files, and fixed sphinx warnings. --- hikari/audit_logs.py | 6 +- hikari/channels.py | 4 +- hikari/internal/marshaller.py | 27 ++++--- hikari/net/ratelimits.py | 22 +++--- hikari/net/routes.py | 136 +++++++++++++++++---------------- hikari/net/shard.py | 20 ++--- hikari/state/event_managers.py | 4 +- 7 files changed, 117 insertions(+), 102 deletions(-) diff --git a/hikari/audit_logs.py b/hikari/audit_logs.py index 43cde2b121..2d9c2ad6e8 100644 --- a/hikari/audit_logs.py +++ b/hikari/audit_logs.py @@ -194,6 +194,7 @@ def deserialize(cls, payload: entities.RawEntityT) -> "AuditLogChange": new_value = value_converter(new_value) if new_value is not None else None old_value = value_converter(old_value) if old_value is not None else None + # noinspection PyArgumentList return cls(key=key, new_value=new_value, old_value=old_value) @@ -420,7 +421,9 @@ def get_entry_info_entity(type_: int) -> typing.Type[BaseAuditLogEntryInfo]: The associated options entity. If not implemented then this will be :obj:`UnrecognisedAuditLogEntryInfo` """ - return register_audit_log_entry_info.types.get(type_) or UnrecognisedAuditLogEntryInfo + types = getattr(register_audit_log_entry_info, "types", more_collections.EMPTY_DICT) + entry_type = types.get(type_) + return entry_type if entry_type is not None else UnrecognisedAuditLogEntryInfo @marshaller.marshallable() @@ -475,6 +478,7 @@ def deserialize(cls, payload: entities.RawEntityT) -> "AuditLogEntry": if option_converter := get_entry_info_entity(action_type): options = option_converter.deserialize(options) + # noinspection PyArgumentList return cls( target_id=target_id, changes=[ diff --git a/hikari/channels.py b/hikari/channels.py index 208821e971..678c6d4c67 100644 --- a/hikari/channels.py +++ b/hikari/channels.py @@ -54,6 +54,7 @@ from hikari import snowflakes from hikari import users from hikari.internal import marshaller +from hikari.internal import more_collections @enum.unique @@ -358,5 +359,6 @@ def deserialize_channel(payload: typing.Dict[str, typing.Any]) -> typing.Union[G partial object, use ``PartialChannel.deserialize()``. """ type_id = payload["type"] - channel_type = register_channel_type.types[type_id] + types = getattr(register_channel_type, "types", more_collections.EMPTY_DICT) + channel_type = types[type_id] return channel_type.deserialize(payload) diff --git a/hikari/internal/marshaller.py b/hikari/internal/marshaller.py index 1bcdd9b749..7f086521a5 100644 --- a/hikari/internal/marshaller.py +++ b/hikari/internal/marshaller.py @@ -117,7 +117,8 @@ def dereference_int_flag( Returns ------- - The cast value as a flag. + :obj:`enum.IntFlag` + The cast value as a flag. Notes ----- @@ -125,8 +126,10 @@ def dereference_int_flag( - :obj:`str` - :obj:`int` - :obj:`typing.SupportsInt` - - :obj:`typing.Collection` [ ``Castable Value`` ] - values will be combined - using functional reduction via the :obj:operator.or_` operator. + - :obj:`typing.Collection` [ ``Castable Value`` ] + + When a collection is passed, values will be combined using functional + reduction via the :obj:operator.or_` operator. """ if isinstance(raw_value, str) and raw_value.isdigit(): raw_value = int(raw_value) @@ -305,11 +308,12 @@ def _construct_entity_descriptor(entity: typing.Any): class HikariEntityMarshaller: - """A global marshaller helper that helps deserialize and serialize any internal components. + """Hikari's utility to manage automated serialization and deserialization. - It can deserialize and serialize any internal componentsthat that are - decorated with the :obj:`attrs` decorator, and that are :obj:`attr.s` - classes using fields with the :obj:`attrib` function call descriptor. + It can deserialize and serialize any internal components that that are + decorated with the :obj:`marshallable` decorator, and that are + :func:`attr.s` classes using fields with the :obj:`attrib` function call + descriptor. """ def __init__(self) -> None: @@ -470,11 +474,10 @@ def marshallable(*, marshaller: HikariEntityMarshaller = HIKARI_ENTITY_MARSHALLE ``decorator(T) -> T`` A decorator to decorate a class with. - Raises - ------ - :obj:`ValueError` - If you attempt to use the ``auto_attribs`` feature provided by - :obj:`attr.s`. + Notes + ----- + The ``auto_attribs`` functionality provided by :obj:`attr.s` is not + supported by this marshaller utility. Do not attempt to use it! Example ------- diff --git a/hikari/net/ratelimits.py b/hikari/net/ratelimits.py index 411a7b22ba..d6b6b9f512 100644 --- a/hikari/net/ratelimits.py +++ b/hikari/net/ratelimits.py @@ -235,7 +235,7 @@ class BurstRateLimiter(BaseRateLimiter, abc.ABC): #: The name of the rate limiter. #: #: :type: :obj:`str` - name: str + name: typing.Final[str] #: The throttling task, or ``None``` if it isn't running. #: @@ -245,12 +245,12 @@ class BurstRateLimiter(BaseRateLimiter, abc.ABC): #: The queue of any futures under a rate limit. #: #: :type: :obj:`asyncio.Queue` [`asyncio.Future`] - queue: typing.List[more_asyncio.Future[None]] + queue: typing.Final[typing.List[more_asyncio.Future[None]]] #: The logger used by this rate limiter. #: #: :type: :obj:`logging.Logger` - logger: logging.Logger + logger: typing.Final[logging.Logger] def __init__(self, name: str) -> None: self.name = name @@ -602,7 +602,7 @@ class HTTPBucketRateLimiter(WindowedBurstRateLimiter): #: The compiled route that this rate limit is covering. #: #: :type: :obj:`hikari.net.routes.CompiledRoute` - compiled_route: routes.CompiledRoute + compiled_route: typing.Final[routes.CompiledRoute] def __init__(self, name: str, compiled_route: routes.CompiledRoute) -> None: super().__init__(name, 1, 1) @@ -686,19 +686,19 @@ class HTTPBucketRateLimiterManager: #: Maps compiled routes to their ``X-RateLimit-Bucket`` header being used. #: #: :type: :obj:`typing.MutableMapping` [ :obj:`hikari.net.routes.CompiledRoute`, :obj:`str` ] - routes_to_hashes: typing.MutableMapping[routes.CompiledRoute, str] + routes_to_hashes: typing.Final[typing.MutableMapping[routes.CompiledRoute, str]] #: Maps full bucket hashes (``X-RateLimit-Bucket`` appended with a hash of #: major parameters used in that compiled route) to their corresponding rate #: limiters. #: #: :type: :obj:`typing.MutableMapping` [ :obj:`str`, :obj:`HTTPBucketRateLimiter` ] - real_hashes_to_buckets: typing.MutableMapping[str, HTTPBucketRateLimiter] + real_hashes_to_buckets: typing.Final[typing.MutableMapping[str, HTTPBucketRateLimiter]] #: An internal event that is set when the object is shut down. #: #: :type: :obj:`asyncio.Event` - closed_event: asyncio.Event + closed_event: typing.Final[asyncio.Event] #: The internal garbage collector task. #: @@ -708,7 +708,7 @@ class HTTPBucketRateLimiterManager: #: The logger to use for this object. #: #: :type: :obj:`logging.Logger` - logger: logging.Logger + logger: typing.Final[logging.Logger] def __init__(self) -> None: self.routes_to_hashes = weakref.WeakKeyDictionary() @@ -743,7 +743,7 @@ def start(self, poll_period: float = 20) -> None: self.gc_task = asyncio.get_running_loop().create_task(self.gc(poll_period)) def close(self) -> None: - """Close the garbage collector and kill any tasks waiting on rate limits. + """Close the garbage collector and kill any tasks waiting on ratelimits. Once this has been called, this object is considered to be effectively dead. To reuse it, one should create a new instance. @@ -928,7 +928,7 @@ class ExponentialBackOff: #: The base to use. Defaults to 2. #: #: :type: :obj:`float` - base: float + base: typing.Final[float] #: The current increment. #: @@ -945,7 +945,7 @@ class ExponentialBackOff: #: Set to ``0`` to disable jitter. #: #: :type: :obj:`float` - jitter_multiplier: float + jitter_multiplier: typing.Final[float] def __init__(self, base: float = 2, maximum: typing.Optional[float] = 64, jitter_multiplier: float = 1) -> None: self.base = base diff --git a/hikari/net/routes.py b/hikari/net/routes.py index 83b0653fa8..43f39564ce 100644 --- a/hikari/net/routes.py +++ b/hikari/net/routes.py @@ -42,19 +42,22 @@ class CompiledRoute: #: The method to use on the route. #: #: :type: :obj:`str` - method: str + method: typing.Final[str] + #: The major parameters in a bucket hash-compatible representation. #: #: :type: :obj:`str` - major_params_hash: str + major_params_hash: typing.Final[str] + #: The compiled route path to use #: #: :type: :obj:`str` - compiled_path: str + compiled_path: typing.Final[str] + #: The hash code #: #: :type: :obj:`int` - hash_code: int + hash_code: typing.Final[int] def __init__(self, method: str, path_template: str, path: str, major_params_hash: str) -> None: self.method = method @@ -138,11 +141,12 @@ class RouteTemplate: #: The template string used for the path. #: #: :type: :obj:`str` - path_template: str + path_template: typing.Final[str] + #: Major parameter names that appear in the template path. #: #: :type: :obj:`typing.FrozenSet` [ :obj:`str` ] - major_params: typing.FrozenSet[str] + major_params: typing.Final[typing.FrozenSet[str]] def __init__(self, path_template: str, major_params: typing.Collection[str] = None) -> None: self.path_template = path_template @@ -182,81 +186,83 @@ def __str__(self) -> str: return self.path_template +_RT = typing.Final[RouteTemplate] + # Channels -CHANNEL = RouteTemplate("/channels/{channel_id}") -CHANNEL_DM_RECIPIENTS = RouteTemplate("/channels/{channel_id}/recipients/{user_id}") -CHANNEL_INVITES = RouteTemplate("/channels/{channel_id}/invites") -CHANNEL_MESSAGE = RouteTemplate("/channels/{channel_id}/messages/{message_id}") -CHANNEL_MESSAGES = RouteTemplate("/channels/{channel_id}/messages") -CHANNEL_MESSAGES_BULK_DELETE = RouteTemplate("/channels/{channel_id}/messages") -CHANNEL_PERMISSIONS = RouteTemplate("/channels/{channel_id}/permissions/{overwrite_id}") -CHANNEL_PIN = RouteTemplate("/channels/{channel_id}/pins/{message_id}") -CHANNEL_PINS = RouteTemplate("/channels/{channel_id}/pins") -CHANNEL_TYPING = RouteTemplate("/channels/{channel_id}/typing") -CHANNEL_WEBHOOKS = RouteTemplate("/channels/{channel_id}/webhooks") +CHANNEL: _RT = RouteTemplate("/channels/{channel_id}") +CHANNEL_DM_RECIPIENTS: _RT = RouteTemplate("/channels/{channel_id}/recipients/{user_id}") +CHANNEL_INVITES: _RT = RouteTemplate("/channels/{channel_id}/invites") +CHANNEL_MESSAGE: _RT = RouteTemplate("/channels/{channel_id}/messages/{message_id}") +CHANNEL_MESSAGES: _RT = RouteTemplate("/channels/{channel_id}/messages") +CHANNEL_MESSAGES_BULK_DELETE: _RT = RouteTemplate("/channels/{channel_id}/messages") +CHANNEL_PERMISSIONS: _RT = RouteTemplate("/channels/{channel_id}/permissions/{overwrite_id}") +CHANNEL_PIN: _RT = RouteTemplate("/channels/{channel_id}/pins/{message_id}") +CHANNEL_PINS: _RT = RouteTemplate("/channels/{channel_id}/pins") +CHANNEL_TYPING: _RT = RouteTemplate("/channels/{channel_id}/typing") +CHANNEL_WEBHOOKS: _RT = RouteTemplate("/channels/{channel_id}/webhooks") # Reactions -ALL_REACTIONS = RouteTemplate("/channels/{channel_id}/messages/{message_id}/reactions") -REACTION_EMOJI = RouteTemplate("/channels/{channel_id}/messages/{message_id}/reactions/{emoji}") -REACTION_EMOJI_USER = RouteTemplate("/channels/{channel_id}/messages/{message_id}/reactions/{emoji}/{used_id}") -REACTIONS = RouteTemplate("/channels/{channel_id}/messages/{message_id}/reactions/{emoji}") +ALL_REACTIONS: _RT = RouteTemplate("/channels/{channel_id}/messages/{message_id}/reactions") +REACTION_EMOJI: _RT = RouteTemplate("/channels/{channel_id}/messages/{message_id}/reactions/{emoji}") +REACTION_EMOJI_USER: _RT = RouteTemplate("/channels/{channel_id}/messages/{message_id}/reactions/{emoji}/{used_id}") +REACTIONS: _RT = RouteTemplate("/channels/{channel_id}/messages/{message_id}/reactions/{emoji}") # Guilds -GUILD = RouteTemplate("/guilds/{guild_id}") -GUILDS = RouteTemplate("/guilds") -GUILD_AUDIT_LOGS = RouteTemplate("/guilds/{guild_id}/audit-logs") -GUILD_BAN = RouteTemplate("/guilds/{guild_id}/bans/{user_id}") -GUILD_BANS = RouteTemplate("/guilds/{guild_id}/bans") -GUILD_CHANNELS = RouteTemplate("/guilds/{guild_id}/channels") -GUILD_EMBED = RouteTemplate("/guilds/{guild_id}/embed") -GUILD_EMOJI = RouteTemplate("/guilds/{guild_id}/emojis/{emoji_id}") -GUILD_EMOJIS = RouteTemplate("/guilds/{guild_id}/emojis") -GUILD_INTEGRATION = RouteTemplate("/guilds/{guild_id}/integrations/{integration_id}") -GUILD_INTEGRATIONS = RouteTemplate("/guilds/{guild_id}/integrations") -GUILD_INTEGRATION_SYNC = RouteTemplate("/guilds/{guild_id}/integrations/{integration_id}") -GUILD_INVITES = RouteTemplate("/guilds/{guild_id}/invites") -GUILD_MEMBERS = RouteTemplate("/guilds/{guild_id}/members") -GUILD_MEMBER = RouteTemplate("/guilds/{guild_id}/members/{user_id}") -GUILD_MEMBER_ROLE = RouteTemplate("/guilds/{guild_id}/members/{user_id}/roles/{role_id}") -GUILD_PRUNE = RouteTemplate("/guilds/{guild_id}/prune") -GUILD_ROLE = RouteTemplate("/guilds/{guild_id}/roles/{role_id}") -GUILD_ROLES = RouteTemplate("/guilds/{guild_id}/roles") -GUILD_VANITY_URL = RouteTemplate("/guilds/{guild_id}/vanity-url") -GUILD_VOICE_REGIONS = RouteTemplate("/guilds/{guild_id}/regions") -GUILD_WIDGET_IMAGE = RouteTemplate("/guilds/{guild_id}/widget.png") -GUILD_WEBHOOKS = RouteTemplate("/guilds/{guild_id}/webhooks") +GUILD: _RT = RouteTemplate("/guilds/{guild_id}") +GUILDS: _RT = RouteTemplate("/guilds") +GUILD_AUDIT_LOGS: _RT = RouteTemplate("/guilds/{guild_id}/audit-logs") +GUILD_BAN: _RT = RouteTemplate("/guilds/{guild_id}/bans/{user_id}") +GUILD_BANS: _RT = RouteTemplate("/guilds/{guild_id}/bans") +GUILD_CHANNELS: _RT = RouteTemplate("/guilds/{guild_id}/channels") +GUILD_EMBED: _RT = RouteTemplate("/guilds/{guild_id}/embed") +GUILD_EMOJI: _RT = RouteTemplate("/guilds/{guild_id}/emojis/{emoji_id}") +GUILD_EMOJIS: _RT = RouteTemplate("/guilds/{guild_id}/emojis") +GUILD_INTEGRATION: _RT = RouteTemplate("/guilds/{guild_id}/integrations/{integration_id}") +GUILD_INTEGRATIONS: _RT = RouteTemplate("/guilds/{guild_id}/integrations") +GUILD_INTEGRATION_SYNC: _RT = RouteTemplate("/guilds/{guild_id}/integrations/{integration_id}") +GUILD_INVITES: _RT = RouteTemplate("/guilds/{guild_id}/invites") +GUILD_MEMBERS: _RT = RouteTemplate("/guilds/{guild_id}/members") +GUILD_MEMBER: _RT = RouteTemplate("/guilds/{guild_id}/members/{user_id}") +GUILD_MEMBER_ROLE: _RT = RouteTemplate("/guilds/{guild_id}/members/{user_id}/roles/{role_id}") +GUILD_PRUNE: _RT = RouteTemplate("/guilds/{guild_id}/prune") +GUILD_ROLE: _RT = RouteTemplate("/guilds/{guild_id}/roles/{role_id}") +GUILD_ROLES: _RT = RouteTemplate("/guilds/{guild_id}/roles") +GUILD_VANITY_URL: _RT = RouteTemplate("/guilds/{guild_id}/vanity-url") +GUILD_VOICE_REGIONS: _RT = RouteTemplate("/guilds/{guild_id}/regions") +GUILD_WIDGET_IMAGE: _RT = RouteTemplate("/guilds/{guild_id}/widget.png") +GUILD_WEBHOOKS: _RT = RouteTemplate("/guilds/{guild_id}/webhooks") # Invites -INVITE = RouteTemplate("/invites/{invite_code}") +INVITE: _RT = RouteTemplate("/invites/{invite_code}") # Users -USER = RouteTemplate("/users/{user_id}") +USER: _RT = RouteTemplate("/users/{user_id}") # @me -LEAVE_GUILD = RouteTemplate("/users/@me/guilds/{guild_id}") -OWN_CONNECTIONS = RouteTemplate("/users/@me/connections") # OAuth2 only -OWN_DMS = RouteTemplate("/users/@me/channels") -OWN_GUILDS = RouteTemplate("/users/@me/guilds") -OWN_GUILD_NICKNAME = RouteTemplate("/guilds/{guild_id}/members/@me/nick") -OWN_USER = RouteTemplate("/users/@me") -OWN_REACTION = RouteTemplate("/channels/{channel_id}/messages/{message_id}/reactions/{emoji}/@me") +LEAVE_GUILD: _RT = RouteTemplate("/users/@me/guilds/{guild_id}") +OWN_CONNECTIONS: _RT = RouteTemplate("/users/@me/connections") # OAuth2 only +OWN_DMS: _RT = RouteTemplate("/users/@me/channels") +OWN_GUILDS: _RT = RouteTemplate("/users/@me/guilds") +OWN_GUILD_NICKNAME: _RT = RouteTemplate("/guilds/{guild_id}/members/@me/nick") +OWN_USER: _RT = RouteTemplate("/users/@me") +OWN_REACTION: _RT = RouteTemplate("/channels/{channel_id}/messages/{message_id}/reactions/{emoji}/@me") # Voice -VOICE_REGIONS = RouteTemplate("/voice/regions") +VOICE_REGIONS: _RT = RouteTemplate("/voice/regions") # Webhooks -WEBHOOK = RouteTemplate("/webhooks/{webhook_id}") -WEBHOOK_WITH_TOKEN = RouteTemplate("/webhooks/{webhook_id}/{webhook_token}") -WEBHOOK_WITH_TOKEN_GITHUB = RouteTemplate("/webhooks/{webhook_id}/{webhook_token}/github") -WEBHOOK_WITH_TOKEN_SLACK = RouteTemplate("/webhooks/{webhook_id}/{webhook_token}/slack") +WEBHOOK: _RT = RouteTemplate("/webhooks/{webhook_id}") +WEBHOOK_WITH_TOKEN: _RT = RouteTemplate("/webhooks/{webhook_id}/{webhook_token}") +WEBHOOK_WITH_TOKEN_GITHUB: _RT = RouteTemplate("/webhooks/{webhook_id}/{webhook_token}/github") +WEBHOOK_WITH_TOKEN_SLACK: _RT = RouteTemplate("/webhooks/{webhook_id}/{webhook_token}/slack") # OAuth2 API -OAUTH2_APPLICATIONS = RouteTemplate("/oauth2/applications") -OAUTH2_APPLICATIONS_ME = RouteTemplate("/oauth2/applications/@me") -OAUTH2_AUTHORIZE = RouteTemplate("/oauth2/authorize") -OAUTH2_TOKEN = RouteTemplate("/oauth2/token") -OAUTH2_TOKEN_REVOKE = RouteTemplate("/oauth2/token/revoke") +OAUTH2_APPLICATIONS: _RT = RouteTemplate("/oauth2/applications") +OAUTH2_APPLICATIONS_ME: _RT = RouteTemplate("/oauth2/applications/@me") +OAUTH2_AUTHORIZE: _RT = RouteTemplate("/oauth2/authorize") +OAUTH2_TOKEN: _RT = RouteTemplate("/oauth2/token") +OAUTH2_TOKEN_REVOKE: _RT = RouteTemplate("/oauth2/token/revoke") # Gateway -GATEWAY = RouteTemplate("/gateway") -GATEWAY_BOT = RouteTemplate("/gateway/bot") +GATEWAY: _RT = RouteTemplate("/gateway") +GATEWAY_BOT: _RT = RouteTemplate("/gateway/bot") diff --git a/hikari/net/shard.py b/hikari/net/shard.py index b2d598f1e2..bab5aa7602 100644 --- a/hikari/net/shard.py +++ b/hikari/net/shard.py @@ -198,7 +198,7 @@ class ShardConnection: #: An event that is set when the connection closes. #: #: :type: :obj:`asyncio.Event` - closed_event: asyncio.Event + closed_event: typing.Final[asyncio.Event] #: The number of times we have disconnected from the gateway on this #: client instance. @@ -228,7 +228,7 @@ class ShardConnection: #: indicates some sort of connection has successfully been made. #: #: :type: :obj:`asyncio.Event` - hello_event: asyncio.Event + hello_event: typing.Final[asyncio.Event] #: An event that is set when the client has successfully ``IDENTIFY``ed #: or ``RESUMED`` with the gateway. This indicates regular communication @@ -236,7 +236,7 @@ class ShardConnection: #: be received. #: #: :type: :obj:`asyncio.Event` - identify_event: asyncio.Event + identify_event: typing.Final[asyncio.Event] #: The monotonic timestamp that the last ``HEARTBEAT`` was sent at, or #: ``nan`` if no ``HEARTBEAT`` has yet been sent. @@ -255,7 +255,7 @@ class ShardConnection: #: The logger used for dumping information about what this client is doing. #: #: :type: :obj:`logging.Logger` - logger: logging.Logger + logger: typing.Final[logging.Logger] #: An event that is triggered when a ``READY`` payload is received for the #: shard. This indicates that it successfully started up and had a correct @@ -270,18 +270,18 @@ class ShardConnection: #: you provide. #: #: :type: :obj:`asyncio.Event` - ready_event: asyncio.Event + ready_event: typing.Final[asyncio.Event] #: An event that is triggered when a resume has succeeded on the gateway. #: #: :type: :obj:`asyncio.Event` - resumed_event: asyncio.Event + resumed_event: typing.Final[asyncio.Event] #: An event that is set when something requests that the connection #: should close somewhere. #: #: :type: :obj:`asyncio.Event` - requesting_close_event: asyncio.Event + requesting_close_event: typing.Final[asyncio.Event] #: The current session ID, if known. #: @@ -297,17 +297,17 @@ class ShardConnection: #: The shard ID. #: #: :type: :obj:`int` - shard_id: int + shard_id: typing.Final[int] #: The number of shards in use for the bot. #: #: :type: :obj:`int` - shard_count: int + shard_count: typing.Final[int] #: The API version to use on Discord. #: #: :type: :obj:`int` - version: int + version: typing.Final[int] def __init__( self, diff --git a/hikari/state/event_managers.py b/hikari/state/event_managers.py index d9d50c5709..9ab9a13958 100644 --- a/hikari/state/event_managers.py +++ b/hikari/state/event_managers.py @@ -29,7 +29,7 @@ from hikari.state import event_dispatchers from hikari.state import raw_event_consumers -EVENT_MARKER_ATTR = "___event_name___" +EVENT_MARKER_ATTR: typing.Final[str] = "___event_name___" EventConsumerT = typing.Callable[[str, entities.RawEntityT], typing.Awaitable[None]] @@ -169,7 +169,7 @@ def process_raw_event( try: handler = self.raw_event_mappers[name] except KeyError: - self.logger.debug("No handler for event %s is registered", name) + self.logger.debug("no handler for event %s is registered", name) else: event = handler(shard_client_obj, payload) self.event_dispatcher.dispatch_event(event) From 6b9ff55c7a18cd5742722970bacf69d703da3b9b Mon Sep 17 00:00:00 2001 From: davfsa Date: Sat, 11 Apr 2020 23:14:26 +0200 Subject: [PATCH 089/922] Rename `unix_epoch_to_ts` --- hikari/guilds.py | 6 +++--- hikari/internal/conversions.py | 11 ++++------- tests/hikari/internal/test_conversions.py | 2 +- tests/hikari/test_guilds.py | 6 +++--- 4 files changed, 11 insertions(+), 14 deletions(-) diff --git a/hikari/guilds.py b/hikari/guilds.py index 82b760f32d..50bab37c15 100644 --- a/hikari/guilds.py +++ b/hikari/guilds.py @@ -315,14 +315,14 @@ class ActivityTimestamps(entities.HikariEntity, entities.Deserializable): #: #: :type: :obj:`datetime.datetime`, optional start: typing.Optional[datetime.datetime] = marshaller.attrib( - deserializer=conversions.unix_epoch_to_ts, if_undefined=None + deserializer=conversions.unix_epoch_to_datetime, if_undefined=None ) #: When this activity's session will end, if applicable. #: #: :type: :obj:`datetime.datetime`, optional end: typing.Optional[datetime.datetime] = marshaller.attrib( - deserializer=conversions.unix_epoch_to_ts, if_undefined=None + deserializer=conversions.unix_epoch_to_datetime, if_undefined=None ) @@ -439,7 +439,7 @@ class PresenceActivity(entities.HikariEntity, entities.Deserializable): #: When this activity was added to the user's session. #: #: :type: :obj:`datetime.datetime` - created_at: datetime.datetime = marshaller.attrib(deserializer=conversions.unix_epoch_to_ts) + created_at: datetime.datetime = marshaller.attrib(deserializer=conversions.unix_epoch_to_datetime) #: The timestamps for when this activity's current state will start and #: end, if applicable. diff --git a/hikari/internal/conversions.py b/hikari/internal/conversions.py index e03cddc175..62cfd4aa10 100644 --- a/hikari/internal/conversions.py +++ b/hikari/internal/conversions.py @@ -26,7 +26,7 @@ "parse_http_date", "parse_iso_8601_ts", "discord_epoch_to_datetime", - "unix_epoch_to_ts", + "unix_epoch_to_datetime", "Seekable", "make_resource_seekable", "pluralize", @@ -94,7 +94,7 @@ def put_if_specified( key: typing.Hashable, value: typing.Any, type_after: typing.Optional[TypeCastT] = None, - / + /, ) -> None: """Add a value to the mapping under the given key as long as the value is not ``...``. @@ -231,7 +231,7 @@ def discord_epoch_to_datetime(epoch: int, /) -> datetime.datetime: return datetime.datetime.fromtimestamp(epoch / 1000 + DISCORD_EPOCH, datetime.timezone.utc) -def unix_epoch_to_ts(epoch: int, /) -> datetime.datetime: +def unix_epoch_to_datetime(epoch: int, /) -> datetime.datetime: """Parse a UNIX epoch to a :obj:`datetime.datetime` object. Parameters @@ -251,10 +251,7 @@ class Seekable(typing.Protocol[typing.AnyStr]): """Structural type for an IO object that supports seek operations.""" def seek( - self, - offset: int, - whence: typing.Union[typing.Literal[0], typing.Literal[1], typing.Literal[2]] = 0, - / + self, offset: int, whence: typing.Union[typing.Literal[0], typing.Literal[1], typing.Literal[2]] = 0, / ) -> None: """Seek to the given offset. diff --git a/tests/hikari/internal/test_conversions.py b/tests/hikari/internal/test_conversions.py index ee79cbd47a..5bc2491096 100644 --- a/tests/hikari/internal/test_conversions.py +++ b/tests/hikari/internal/test_conversions.py @@ -183,7 +183,7 @@ def test_parse_discord_epoch_to_datetime(): def test_parse_unix_epoch_to_datetime(): unix_timestamp = 1457991678956 expected_timestamp = datetime.datetime(2016, 3, 14, 21, 41, 18, 956000, tzinfo=datetime.timezone.utc) - assert conversions.unix_epoch_to_ts(unix_timestamp) == expected_timestamp + assert conversions.unix_epoch_to_datetime(unix_timestamp) == expected_timestamp @pytest.mark.parametrize( diff --git a/tests/hikari/test_guilds.py b/tests/hikari/test_guilds.py index 144d1acf8e..fd9f640965 100644 --- a/tests/hikari/test_guilds.py +++ b/tests/hikari/test_guilds.py @@ -332,13 +332,13 @@ def test_deserialize(self, test_activity_timestamps_payload): with _helpers.patch_marshal_attr( guilds.ActivityTimestamps, "start", - deserializer=hikari.internal.conversions.unix_epoch_to_ts, + deserializer=hikari.internal.conversions.unix_epoch_to_datetime, return_value=mock_start_date, ) as patched_start_deserializer: with _helpers.patch_marshal_attr( guilds.ActivityTimestamps, "end", - deserializer=hikari.internal.conversions.unix_epoch_to_ts, + deserializer=hikari.internal.conversions.unix_epoch_to_datetime, return_value=mock_end_date, ) as patched_end_deserializer: activity_timestamps_obj = guilds.ActivityTimestamps.deserialize(test_activity_timestamps_payload) @@ -404,7 +404,7 @@ def test_deserialize( with _helpers.patch_marshal_attr( guilds.PresenceActivity, "created_at", - deserializer=hikari.internal.conversions.unix_epoch_to_ts, + deserializer=hikari.internal.conversions.unix_epoch_to_datetime, return_value=mock_created_at, ) as patched_created_at_deserializer: with _helpers.patch_marshal_attr( From 704f58171c2f264b40decaa9ff254f3ca969261c Mon Sep 17 00:00:00 2001 From: davfsa Date: Sun, 12 Apr 2020 09:00:07 +0200 Subject: [PATCH 090/922] Fix incorrect time format --- hikari/clients/shard_clients.py | 2 +- tests/hikari/clients/test_shard_clients.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/hikari/clients/shard_clients.py b/hikari/clients/shard_clients.py index 21d4a00cf8..72ac29c164 100644 --- a/hikari/clients/shard_clients.py +++ b/hikari/clients/shard_clients.py @@ -519,7 +519,7 @@ def _create_presence_pl( ) -> typing.Dict[str, typing.Any]: return { "status": status, - "idle_since": idle_since.timestamp() if idle_since is not None else None, + "idle_since": idle_since.timestamp() * 1000 if idle_since is not None else None, "game": activity.serialize() if activity is not None else None, "afk": is_afk, } diff --git a/tests/hikari/clients/test_shard_clients.py b/tests/hikari/clients/test_shard_clients.py index 43b352401a..e42ed39a0f 100644 --- a/tests/hikari/clients/test_shard_clients.py +++ b/tests/hikari/clients/test_shard_clients.py @@ -316,7 +316,7 @@ async def test_update_presence_with_optionals(self, shard_client_obj): ) shard_client_obj._connection.update_presence.assert_called_once_with( - {"status": "dnd", "game": None, "idle_since": datetime_obj.timestamp(), "afk": True} + {"status": "dnd", "game": None, "idle_since": datetime_obj.timestamp() * 1000, "afk": True} ) assert shard_client_obj._status == guilds.PresenceStatus.DND @@ -331,6 +331,6 @@ def test__create_presence_pl(self, shard_client_obj): assert returned == { "status": "dnd", "game": None, - "idle_since": datetime_obj.timestamp(), + "idle_since": datetime_obj.timestamp() * 1000, "afk": True, } From 3b065470e4b831274780e61162f9e6b5cb4e5cb2 Mon Sep 17 00:00:00 2001 From: davfsa Date: Sun, 12 Apr 2020 11:01:54 +0200 Subject: [PATCH 091/922] Add tests for configs --- hikari/clients/configs.py | 30 +-- tests/hikari/clients/test_configs.py | 308 +++++++++++++++++++++++++++ 2 files changed, 324 insertions(+), 14 deletions(-) create mode 100644 tests/hikari/clients/test_configs.py diff --git a/hikari/clients/configs.py b/hikari/clients/configs.py index 3576f15581..4c1d97a69d 100644 --- a/hikari/clients/configs.py +++ b/hikari/clients/configs.py @@ -40,7 +40,6 @@ from hikari import entities from hikari import gateway_entities from hikari import guilds -from hikari.internal import conversions from hikari.internal import marshaller from hikari.net import codes @@ -198,7 +197,7 @@ class WebsocketConfig(AIOHTTPConfig, TokenConfig, DebugConfig): #: #: :type: :obj:`hikari.guilds.PresenceStatus` initial_status: guilds.PresenceStatus = marshaller.attrib( - deserializer=guilds.PresenceStatus.__getitem__, + deserializer=guilds.PresenceStatus, if_undefined=lambda: guilds.PresenceStatus.ONLINE, default=guilds.PresenceStatus.ONLINE, ) @@ -213,7 +212,7 @@ class WebsocketConfig(AIOHTTPConfig, TokenConfig, DebugConfig): #: #: :type: :obj:`datetime.datetime`, optional initial_idle_since: typing.Optional[datetime.datetime] = marshaller.attrib( - deserializer=conversions.unix_epoch_to_ts, if_none=None, if_undefined=None, default=None + deserializer=datetime.datetime.fromtimestamp, if_none=None, if_undefined=None, default=None ) #: The intents to use for the connection. @@ -222,8 +221,6 @@ class WebsocketConfig(AIOHTTPConfig, TokenConfig, DebugConfig): #: intent names. If #: unspecified, this will be set to ``None``. #: - #: :type: :obj:`hikari.net.codes.GatewayIntent`, optional - #: #: Examples #: -------- #: @@ -232,7 +229,7 @@ class WebsocketConfig(AIOHTTPConfig, TokenConfig, DebugConfig): #: # Python example #: GatewayIntent.GUILDS | GatewayIntent.GUILD_MESSAGES #: - #: ..code-block:: js + #: .. code-block:: js #: #: // JSON example, using explicit bitfield values #: 513 @@ -252,6 +249,9 @@ class WebsocketConfig(AIOHTTPConfig, TokenConfig, DebugConfig): #: #: If you are using the V6 gateway implementation, setting this to ``None`` #: will simply opt you into every event you can subscribe to. + #: + #: + #: :type: :obj:`hikari.net.codes.GatewayIntent`, optional intents: typing.Optional[codes.GatewayIntent] = marshaller.attrib( deserializer=lambda value: marshaller.dereference_int_flag(codes.GatewayIntent, value), if_undefined=None, @@ -259,20 +259,22 @@ class WebsocketConfig(AIOHTTPConfig, TokenConfig, DebugConfig): ) #: The large threshold to use. + #: + #: :type: :obj:`int` large_threshold: int = marshaller.attrib(deserializer=int, if_undefined=lambda: 250, default=True) def _parse_shard_info(payload): - range_matcher = re.search(r"(\d+)\s*(\.{2,3})\s*(\d+)", payload) + range_matcher = re.search(r"(\d+)\s*(\.{2,3})\s*(\d+)", payload) if isinstance(payload, str) else None if not range_matcher: - if isinstance(payload, str): - payload = int(payload) - if isinstance(payload, int): return [payload] - raise ValueError('expected shard_ids to be one of int, list of int, or range string ("x..y")') + if isinstance(payload, list): + return payload + + raise ValueError('expected shard_ids to be one of int, list of int, or range string ("x..y" or "x...y")') minimum, range_mod, maximum = range_matcher.groups() minimum, maximum = int(minimum), int(maximum) @@ -290,9 +292,9 @@ class ShardConfig(BaseConfig): #: The shard IDs to produce shard connections for. #: #: If being deserialized, this can be several formats. - #: ``12``, ``"12"``: + #: ``12``: #: A specific shard ID. - #: ``[0, 1, 2, 3, 8, 9, 10]``, ``["0", "1", "2", "3", "8", "9", "10"]``: + #: ``[0, 1, 2, 3, 8, 9, 10]``: #: A sequence of shard IDs. #: ``"5..16"``: #: A range string. Two periods indicate a range of ``[5, 16)`` @@ -306,7 +308,7 @@ class ShardConfig(BaseConfig): #: auto-sharding will be performed for you. #: #: :type: :obj:`typing.Sequence` [ :obj:`int` ], optional - shard_ids: typing.Sequence[int] = marshaller.attrib( + shard_ids: typing.Optional[typing.Sequence[int]] = marshaller.attrib( deserializer=_parse_shard_info, if_none=None, if_undefined=None, default=None ) diff --git a/tests/hikari/clients/test_configs.py b/tests/hikari/clients/test_configs.py new file mode 100644 index 0000000000..f16e8e2545 --- /dev/null +++ b/tests/hikari/clients/test_configs.py @@ -0,0 +1,308 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# Copyright © Nekoka.tt 2019-2020 +# +# This file is part of Hikari. +# +# Hikari is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Hikari is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along ith Hikari. If not, see . +import aiohttp +import ssl +import datetime + +import cymock as mock +import pytest + +from hikari import gateway_entities +from hikari import guilds +from hikari.net import codes +from hikari.clients import configs +from tests.hikari import _helpers + + +@pytest.fixture +def test_debug_config(): + return {"debug": True} + + +@pytest.fixture +def test_aiohttp_config(): + return { + "allow_redirects": True, + "tcp_connector": "aiohttp#TCPConnector", + "proxy_headers": {"Some-Header": "headercontent"}, + "proxy_auth": "basic Tm90aGluZyB0byBzZWUgaGVyZSA6IGpvaW4gZGlzY29yZC5nZy9IS0dQRTlRIDopIH5kYXZmc2E=", + "proxy_url": "proxy_url", + "request_timeout": 100, + "ssl_context": "ssl#SSLContext", + "verify_ssl": False, + } + + +@pytest.fixture +def test_token_config(): + return {"token": "token"} + + +@pytest.fixture +def test_websocket_config(test_debug_config, test_aiohttp_config, test_token_config): + return { + "gateway_use_compression": False, + "gateway_version": 7, + "initial_activity": {"name": "test", "url": "some_url", "type": 0}, + "initial_status": "dnd", + "initial_is_afk": True, + "initial_idle_since": None, # Set in test + "intents": 513, + "large_threshold": 1000, + **test_debug_config, + **test_aiohttp_config, + **test_token_config, + } + + +@pytest.fixture +def test_shard_config(): + return {"shard_ids": "5...10", "shard_count": "17"} + + +@pytest.fixture +def test_rest_config(test_aiohttp_config, test_token_config): + return {"rest_version": 6, **test_aiohttp_config, **test_token_config} + + +@pytest.fixture +def test_bot_config(test_rest_config, test_shard_config, test_websocket_config): + return {**test_rest_config, **test_shard_config, **test_websocket_config} + + +class TestDebugConfig: + def test_deserialize(self, test_debug_config): + debug_config_obj = configs.DebugConfig.deserialize(test_debug_config) + + assert debug_config_obj.debug is True + + def test_empty_deserialize(self): + debug_config_obj = configs.DebugConfig.deserialize({}) + + assert debug_config_obj.debug is False + + +class TestAIOHTTPConfig: + def test_deserialize(self, test_aiohttp_config): + aiohttp_config_obj = configs.AIOHTTPConfig.deserialize(test_aiohttp_config) + + assert aiohttp_config_obj.allow_redirects is True + assert aiohttp_config_obj.tcp_connector == aiohttp.TCPConnector + assert aiohttp_config_obj.proxy_headers == {"Some-Header": "headercontent"} + assert aiohttp_config_obj.proxy_auth == aiohttp.BasicAuth.decode( + "basic Tm90aGluZyB0byBzZWUgaGVyZSA6IGpvaW4gZGlzY29yZC5nZy9IS0dQRTlRIDopIH5kYXZmc2E=" + ) + assert aiohttp_config_obj.proxy_url == "proxy_url" + assert aiohttp_config_obj.request_timeout == 100 + assert aiohttp_config_obj.ssl_context == ssl.SSLContext + assert aiohttp_config_obj.verify_ssl is False + + def test_empty_deserialize(self): + aiohttp_config_obj = configs.AIOHTTPConfig.deserialize({}) + + assert aiohttp_config_obj.allow_redirects is False + assert aiohttp_config_obj.tcp_connector is None + assert aiohttp_config_obj.proxy_headers is None + assert aiohttp_config_obj.proxy_auth is None + assert aiohttp_config_obj.proxy_url is None + assert aiohttp_config_obj.request_timeout is None + assert aiohttp_config_obj.ssl_context is None + assert aiohttp_config_obj.verify_ssl is True + + +class TestTokenConfig: + def test_deserialize(self, test_token_config): + token_config_obj = configs.TokenConfig.deserialize(test_token_config) + + assert token_config_obj.token == "token" + + def test_empty_deserialize(self): + token_config_obj = configs.TokenConfig.deserialize({}) + + assert token_config_obj.token is None + + +class TestWebsocketConfig: + def test_deserialize(self, test_websocket_config): + datetime_obj = datetime.datetime.now() + test_websocket_config["initial_idle_since"] = datetime_obj.timestamp() + websocket_config_obj = configs.WebsocketConfig.deserialize(test_websocket_config) + + assert websocket_config_obj.gateway_use_compression is False + assert websocket_config_obj.gateway_version == 7 + assert websocket_config_obj.initial_activity == gateway_entities.GatewayActivity.deserialize( + {"name": "test", "url": "some_url", "type": 0} + ) + assert websocket_config_obj.initial_status == guilds.PresenceStatus.DND + assert websocket_config_obj.initial_idle_since == datetime_obj + assert websocket_config_obj.intents == codes.GatewayIntent.GUILD_MESSAGES | codes.GatewayIntent.GUILDS + assert websocket_config_obj.large_threshold == 1000 + assert websocket_config_obj.debug is True + assert websocket_config_obj.allow_redirects is True + assert websocket_config_obj.tcp_connector == aiohttp.TCPConnector + assert websocket_config_obj.proxy_headers == {"Some-Header": "headercontent"} + assert websocket_config_obj.proxy_auth == aiohttp.BasicAuth.decode( + "basic Tm90aGluZyB0byBzZWUgaGVyZSA6IGpvaW4gZGlzY29yZC5nZy9IS0dQRTlRIDopIH5kYXZmc2E=" + ) + assert websocket_config_obj.proxy_url == "proxy_url" + assert websocket_config_obj.request_timeout == 100 + assert websocket_config_obj.ssl_context == ssl.SSLContext + assert websocket_config_obj.verify_ssl is False + assert websocket_config_obj.token == "token" + + def test_empty_deserialize(self): + websocket_config_obj = configs.WebsocketConfig.deserialize({}) + + assert websocket_config_obj.gateway_use_compression is True + assert websocket_config_obj.gateway_version == 6 + assert websocket_config_obj.initial_activity is None + assert websocket_config_obj.initial_status == guilds.PresenceStatus.ONLINE + assert websocket_config_obj.initial_idle_since is None + assert websocket_config_obj.intents is None + assert websocket_config_obj.large_threshold == 250 + assert websocket_config_obj.debug is False + assert websocket_config_obj.allow_redirects is False + assert websocket_config_obj.tcp_connector is None + assert websocket_config_obj.proxy_headers is None + assert websocket_config_obj.proxy_auth is None + assert websocket_config_obj.proxy_url is None + assert websocket_config_obj.request_timeout is None + assert websocket_config_obj.ssl_context is None + assert websocket_config_obj.verify_ssl is True + assert websocket_config_obj.token is None + + +class TestParseShardInfo: + def test__parse_shard_info_when_exclusive_range(self): + assert configs._parse_shard_info("0..2") == [0, 1] + + def test__parse_shard_info_when_inclusive_range(self): + assert configs._parse_shard_info("0...2") == [0, 1, 2] + + def test__parse_shard_info_when_specific_id(self): + assert configs._parse_shard_info(2) == [2] + + def test__parse_shard_info_when_list(self): + assert configs._parse_shard_info([2, 5, 6]) == [2, 5, 6] + + @_helpers.assert_raises(type_=ValueError) + def test__parse_shard_info_when_invalid(self): + configs._parse_shard_info("something invalid") + + +class TestShardConfig: + def test_deserialize(self, test_shard_config): + shard_config_obj = configs.ShardConfig.deserialize(test_shard_config) + + assert shard_config_obj.shard_ids == [5, 6, 7, 8, 9, 10] + assert shard_config_obj.shard_count == 17 + + def test_empty_deserialize(self): + shard_config_obj = configs.ShardConfig.deserialize({}) + + assert shard_config_obj.shard_ids is None + assert shard_config_obj.shard_count is None + + +class TestRESTConfig: + def test_deserialize(self, test_rest_config): + rest_config_obj = configs.RESTConfig.deserialize(test_rest_config) + + assert rest_config_obj.rest_version == 6 + assert rest_config_obj.allow_redirects is True + assert rest_config_obj.tcp_connector == aiohttp.TCPConnector + assert rest_config_obj.proxy_headers == {"Some-Header": "headercontent"} + assert rest_config_obj.proxy_auth == aiohttp.BasicAuth.decode( + "basic Tm90aGluZyB0byBzZWUgaGVyZSA6IGpvaW4gZGlzY29yZC5nZy9IS0dQRTlRIDopIH5kYXZmc2E=" + ) + assert rest_config_obj.proxy_url == "proxy_url" + assert rest_config_obj.request_timeout == 100 + assert rest_config_obj.ssl_context == ssl.SSLContext + assert rest_config_obj.verify_ssl is False + assert rest_config_obj.token == "token" + + def test_empty_deserialize(self): + rest_config_obj = configs.RESTConfig.deserialize({}) + + assert rest_config_obj.rest_version == 7 + assert rest_config_obj.allow_redirects is False + assert rest_config_obj.tcp_connector is None + assert rest_config_obj.proxy_headers is None + assert rest_config_obj.proxy_auth is None + assert rest_config_obj.proxy_url is None + assert rest_config_obj.request_timeout is None + assert rest_config_obj.ssl_context is None + assert rest_config_obj.verify_ssl is True + assert rest_config_obj.token is None + + +class TestBotConfig: # TODO: Talk to esp about this because rest already has aiohttp and token, but websocket too. + def test_deserialize(self, test_bot_config): + datetime_obj = datetime.datetime.now() + test_bot_config["initial_idle_since"] = datetime_obj.timestamp() + bot_config_obj = configs.BotConfig.deserialize(test_bot_config) + + assert bot_config_obj.rest_version == 6 + assert bot_config_obj.allow_redirects is True + assert bot_config_obj.tcp_connector == aiohttp.TCPConnector + assert bot_config_obj.proxy_headers == {"Some-Header": "headercontent"} + assert bot_config_obj.proxy_auth == aiohttp.BasicAuth.decode( + "basic Tm90aGluZyB0byBzZWUgaGVyZSA6IGpvaW4gZGlzY29yZC5nZy9IS0dQRTlRIDopIH5kYXZmc2E=" + ) + assert bot_config_obj.proxy_url == "proxy_url" + assert bot_config_obj.request_timeout == 100 + assert bot_config_obj.ssl_context == ssl.SSLContext + assert bot_config_obj.verify_ssl is False + assert bot_config_obj.token == "token" + assert bot_config_obj.shard_ids == [5, 6, 7, 8, 9, 10] + assert bot_config_obj.shard_count == 17 + assert bot_config_obj.gateway_use_compression is False + assert bot_config_obj.gateway_version == 7 + assert bot_config_obj.initial_activity == gateway_entities.GatewayActivity.deserialize( + {"name": "test", "url": "some_url", "type": 0} + ) + assert bot_config_obj.initial_status == guilds.PresenceStatus.DND + assert bot_config_obj.initial_idle_since == datetime_obj + assert bot_config_obj.intents == codes.GatewayIntent.GUILD_MESSAGES | codes.GatewayIntent.GUILDS + assert bot_config_obj.large_threshold == 1000 + assert bot_config_obj.debug is True + + def test_empty_deserialize(self): + bot_config_obj = configs.BotConfig.deserialize({}) + + assert bot_config_obj.rest_version == 7 + assert bot_config_obj.allow_redirects is False + assert bot_config_obj.tcp_connector is None + assert bot_config_obj.proxy_headers is None + assert bot_config_obj.proxy_auth is None + assert bot_config_obj.proxy_url is None + assert bot_config_obj.request_timeout is None + assert bot_config_obj.ssl_context is None + assert bot_config_obj.verify_ssl is True + assert bot_config_obj.token is None + assert bot_config_obj.shard_ids is None + assert bot_config_obj.shard_count is None + assert bot_config_obj.gateway_use_compression is True + assert bot_config_obj.gateway_version == 6 + assert bot_config_obj.initial_activity is None + assert bot_config_obj.initial_status == guilds.PresenceStatus.ONLINE + assert bot_config_obj.initial_idle_since is None + assert bot_config_obj.intents is None + assert bot_config_obj.large_threshold == 250 + assert bot_config_obj.debug is False From 299fd43b5269a53ec63b31fcc7e3a3fc3817e97c Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Sun, 12 Apr 2020 10:39:33 +0100 Subject: [PATCH 092/922] Added wiki submodule [skip ci] --- .gitmodules | 3 +++ wiki | 1 + 2 files changed, 4 insertions(+) create mode 100644 .gitmodules create mode 160000 wiki diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000000..82fddd047b --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "wiki"] + path = wiki + url = https://gitlab.com/nekokatt/hikari.wiki.git diff --git a/wiki b/wiki new file mode 160000 index 0000000000..494442bb39 --- /dev/null +++ b/wiki @@ -0,0 +1 @@ +Subproject commit 494442bb3984338990f23572a0d03c4cbf3ff93b From 8741acb3f0efa687db487980ea2cab65123ff1ac Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Sun, 12 Apr 2020 10:46:49 +0100 Subject: [PATCH 093/922] Added default sessions to nox. Users can run the pipeline minus the installation shellchecking scripts by running 'nox' from their terminal without arguments. [skip deploy] --- noxfile.py | 95 +++++++++++++++++++++++++++++++----------------------- 1 file changed, 54 insertions(+), 41 deletions(-) diff --git a/noxfile.py b/noxfile.py index 1836699ffd..a05cf3d7c8 100644 --- a/noxfile.py +++ b/noxfile.py @@ -17,7 +17,6 @@ # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . import contextlib -import fnmatch import os import re import shutil @@ -27,6 +26,13 @@ import nox.sessions +nox.options.sessions = [] + + +def default_session(func): + nox.options.sessions.append(func.__name__) + return func + def pathify(arg, *args, root=False): return os.path.abspath(os.path.join(os.path.dirname(os.path.abspath(__file__)) if not root else "/", arg, *args)) @@ -51,6 +57,15 @@ def pathify(arg, *args, root=False): REPOSITORY = f"https://gitlab.com/{OWNER}/{MAIN_PACKAGE}" +@default_session +@nox.session(reuse_venv=True) +def format(session) -> None: + """Reformat code with Black. Pass the '--check' flag to check formatting only.""" + session.install("black") + session.run("python", BLACK_SHIM_PATH, *BLACK_PATHS, *session.posargs) + + +@default_session @nox.session(reuse_venv=True) def test(session) -> None: """Run unit tests in Pytest.""" @@ -86,46 +101,7 @@ def test(session) -> None: ) -@nox.session(reuse_venv=True) -def documentation(session) -> None: - """Generate documentation using Sphinx for the current branch.""" - session.install("-r", "requirements.txt") - session.install("-r", "dev-requirements.txt") - session.install("-r", "doc-requirements.txt") - - session.env["SPHINXOPTS"] = "-WTvvn" - session.run("sphinx-apidoc", "-e", "-o", DOCUMENTATION_DIR, MAIN_PACKAGE) - session.run( - "python", "-m", "sphinx.cmd.build", "-a", "-b", "html", "-j", "auto", "-n", DOCUMENTATION_DIR, ARTIFACT_DIR - ) - for f in os.listdir(DOCUMENTATION_DIR): - if f in ("hikari.rst", "modules.rst") or re.match(r"hikari\.(\w|\.)+\.rst", f): - os.unlink(pathify(DOCUMENTATION_DIR, f)) - - -@nox.session(reuse_venv=True) -def sast(session) -> None: - """Run static application security testing with Bandit.""" - session.install("bandit") - pkg = MAIN_PACKAGE.split(".")[0] - session.run("bandit", pkg, "-r") - - -@nox.session(reuse_venv=True) -def safety(session) -> None: - """Run safety checks against a vulnerability database using Safety.""" - session.install("-r", "requirements.txt") - session.install("safety") - session.run("safety", "check") - - -@nox.session(reuse_venv=True) -def format(session) -> None: - """Reformat code with Black. Pass the '--check' flag to check formatting only.""" - session.install("black") - session.run("python", BLACK_SHIM_PATH, *BLACK_PATHS, *session.posargs) - - +@default_session @nox.session(reuse_venv=True) def docstyle(session) -> None: """Check docstrings with pydocstyle.""" @@ -135,6 +111,7 @@ def docstyle(session) -> None: session.run("pydocstyle", "--config=../pydocstyle.ini") +@default_session @nox.session(reuse_venv=True) def lint(session) -> None: """Check formating with pylint""" @@ -167,6 +144,42 @@ def lint(session) -> None: ) +@default_session +@nox.session(reuse_venv=True) +def sast(session) -> None: + """Run static application security testing with Bandit.""" + session.install("bandit") + pkg = MAIN_PACKAGE.split(".")[0] + session.run("bandit", pkg, "-r") + + +@default_session +@nox.session(reuse_venv=True) +def safety(session) -> None: + """Run safety checks against a vulnerability database using Safety.""" + session.install("-r", "requirements.txt") + session.install("safety") + session.run("safety", "check") + + +@default_session +@nox.session(reuse_venv=True) +def documentation(session) -> None: + """Generate documentation using Sphinx for the current branch.""" + session.install("-r", "requirements.txt") + session.install("-r", "dev-requirements.txt") + session.install("-r", "doc-requirements.txt") + + session.env["SPHINXOPTS"] = "-WTvvn" + session.run("sphinx-apidoc", "-e", "-o", DOCUMENTATION_DIR, MAIN_PACKAGE) + session.run( + "python", "-m", "sphinx.cmd.build", "-a", "-b", "html", "-j", "auto", "-n", DOCUMENTATION_DIR, ARTIFACT_DIR + ) + for f in os.listdir(DOCUMENTATION_DIR): + if f in ("hikari.rst", "modules.rst") or re.match(r"hikari\.(\w|\.)+\.rst", f): + os.unlink(pathify(DOCUMENTATION_DIR, f)) + + if os.getenv("CI"): @nox.session(reuse_venv=False) From 9eab93987e78f3d7edabed919144ed09da227194 Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Sun, 12 Apr 2020 10:51:58 +0100 Subject: [PATCH 094/922] Fixed a coverage issue in noxfile.py [skip deploy] --- .gitignore | 3 --- noxfile.py | 9 +++++++-- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/.gitignore b/.gitignore index f64e0dd3b5..76c0aa6bcc 100644 --- a/.gitignore +++ b/.gitignore @@ -39,9 +39,6 @@ pip-wheel-metadata/ .pytest/ .pytest_cache/ -# Pip stuff -pip-wheel-metadata/ - # Poetry lockfile poetry.lock diff --git a/noxfile.py b/noxfile.py index a05cf3d7c8..8aab34cf8e 100644 --- a/noxfile.py +++ b/noxfile.py @@ -58,8 +58,8 @@ def pathify(arg, *args, root=False): @default_session -@nox.session(reuse_venv=True) -def format(session) -> None: +@nox.session(reuse_venv=True, name="format") +def format_(session) -> None: """Reformat code with Black. Pass the '--check' flag to check formatting only.""" session.install("black") session.run("python", BLACK_SHIM_PATH, *BLACK_PATHS, *session.posargs) @@ -100,6 +100,11 @@ def test(session) -> None: TEST_PATH, ) + # Apparently coverage doesn't replace this, leading to "no coverage was + # detected" which is helpful. + with contextlib.suppress(Exception): + shutil.move(".coverage", ".coverage.old") + @default_session @nox.session(reuse_venv=True) From 853de21f84ee2e83e5f2104acbf4060a13726422 Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Sun, 12 Apr 2020 11:05:59 +0100 Subject: [PATCH 095/922] Fixed bug breaking CI jobs on GitLab [skip deploy] --- noxfile.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/noxfile.py b/noxfile.py index 8aab34cf8e..c40a21dcd6 100644 --- a/noxfile.py +++ b/noxfile.py @@ -74,6 +74,11 @@ def test(session) -> None: additional_opts = ["--pastebin=all"] if os.getenv("CI") else [] + # Apparently coverage doesn't replace this, leading to "no coverage was + # detected" which is helpful. + with contextlib.suppress(Exception): + shutil.move(".coverage", ".coverage.old") + session.run( "python", "-m", @@ -100,11 +105,6 @@ def test(session) -> None: TEST_PATH, ) - # Apparently coverage doesn't replace this, leading to "no coverage was - # detected" which is helpful. - with contextlib.suppress(Exception): - shutil.move(".coverage", ".coverage.old") - @default_session @nox.session(reuse_venv=True) From e0a95d56d3e41fac55354dda2602d395c7846ec2 Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Sun, 12 Apr 2020 11:19:25 +0100 Subject: [PATCH 096/922] Updated pytest, moved Sphinx to v3 [skip deploy] --- dev-requirements.txt | 8 +++----- doc-requirements.txt | 2 +- wiki | 2 +- 3 files changed, 5 insertions(+), 7 deletions(-) diff --git a/dev-requirements.txt b/dev-requirements.txt index 626ebb198f..081c8267a2 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -1,14 +1,12 @@ async-timeout==3.0.1 -coverage==5.0.3 +coverage==5.0.4 nox -pytest==5.3.5 +pytest==5.4.1 pytest-asyncio==0.10.0 pytest-cov==2.8.1 -pytest-html==2.0.1 +pytest-html==2.1.1 pytest-xdist==1.31.0 # My Cythonized mock module cymock==2020.2.26.2 -# We use this to get pretty output, but for now I have disabled it to see how it impacts test execution speed. -# pytest-testdox==1.2.1 diff --git a/doc-requirements.txt b/doc-requirements.txt index 5d3162cd0b..6745559103 100644 --- a/doc-requirements.txt +++ b/doc-requirements.txt @@ -1,4 +1,4 @@ Jinja2==2.11.1 -sphinx==2.4.4 +sphinx==3.0.1 # sphinx-bootstrap-theme==0.7.1 https://github.com/ryan-roemer/sphinx-bootstrap-theme/zipball/v0.8.0 diff --git a/wiki b/wiki index 494442bb39..dfd6bac2f2 160000 --- a/wiki +++ b/wiki @@ -1 +1 @@ -Subproject commit 494442bb3984338990f23572a0d03c4cbf3ff93b +Subproject commit dfd6bac2f2d47de73e9a2548e7d07d13e108bd8c From 81a3ccf5dbbd4e3436c20320de9aebd788a54061 Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Sun, 12 Apr 2020 16:04:32 +0100 Subject: [PATCH 097/922] Removed submodule breaking builds [skip deploy] --- .gitmodules | 3 --- noxfile.py | 4 ++-- wiki | 1 - 3 files changed, 2 insertions(+), 6 deletions(-) delete mode 160000 wiki diff --git a/.gitmodules b/.gitmodules index 82fddd047b..e69de29bb2 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +0,0 @@ -[submodule "wiki"] - path = wiki - url = https://gitlab.com/nekokatt/hikari.wiki.git diff --git a/noxfile.py b/noxfile.py index c40a21dcd6..9f2f543e4f 100644 --- a/noxfile.py +++ b/noxfile.py @@ -58,8 +58,8 @@ def pathify(arg, *args, root=False): @default_session -@nox.session(reuse_venv=True, name="format") -def format_(session) -> None: +@nox.session(reuse_venv=True) +def format(session) -> None: """Reformat code with Black. Pass the '--check' flag to check formatting only.""" session.install("black") session.run("python", BLACK_SHIM_PATH, *BLACK_PATHS, *session.posargs) diff --git a/wiki b/wiki deleted file mode 160000 index dfd6bac2f2..0000000000 --- a/wiki +++ /dev/null @@ -1 +0,0 @@ -Subproject commit dfd6bac2f2d47de73e9a2548e7d07d13e108bd8c From 9699962310f15929925f907b7f7b2d554e7e5275 Mon Sep 17 00:00:00 2001 From: Faster Speeding Date: Mon, 6 Apr 2020 21:43:28 +0100 Subject: [PATCH 098/922] Implement rest_client.RESTClient + related changes. --- hikari/audit_logs.py | 114 + hikari/channels.py | 2 +- hikari/clients/rest_clients.py | 3954 ++++++++++++++++++++- hikari/emojis.py | 24 +- hikari/guilds.py | 20 +- hikari/internal/conversions.py | 30 + hikari/media.py | 47 + hikari/net/rest.py | 219 +- hikari/snowflakes.py | 23 +- hikari/voices.py | 7 +- pylint.ini | 1 + requirements.txt | 1 + tests/hikari/_helpers.py | 13 +- tests/hikari/clients/test_rest_client.py | 2743 ++++++++++++++ tests/hikari/internal/test_conversions.py | 19 + tests/hikari/net/test_rest.py | 173 +- tests/hikari/test_audit_logs.py | 188 + tests/hikari/test_emojis.py | 30 + tests/hikari/test_guilds.py | 11 + tests/hikari/test_snowflake.py | 44 +- 20 files changed, 7430 insertions(+), 233 deletions(-) create mode 100644 hikari/media.py create mode 100644 tests/hikari/clients/test_rest_client.py diff --git a/hikari/audit_logs.py b/hikari/audit_logs.py index 2d9c2ad6e8..1ca832678b 100644 --- a/hikari/audit_logs.py +++ b/hikari/audit_logs.py @@ -40,6 +40,7 @@ ] import abc +import copy import datetime import enum import typing @@ -528,3 +529,116 @@ class AuditLog(entities.HikariEntity, entities.Deserializable): webhooks: typing.Mapping[snowflakes.Snowflake, _webhooks.Webhook] = marshaller.attrib( deserializer=lambda payload: {webhook.id: webhook for webhook in map(_webhooks.Webhook.deserialize, payload)} ) + + +class AuditLogIterator(typing.AsyncIterator[AuditLogEntry]): + """An async iterator used for iterating through a guild's audit log entries. + + This returns the audit log entries created before a given entry object/ID or + from the newest audit log entry to the oldest. + + Parameters + ---------- + guild_id : :obj:`str` + The guild ID to look up. + request : :obj:`typing.Callable` [ ``...``, :obj:`typing.Coroutine` [ :obj:`typing.Any`, :obj:`typing.Any`, :obj:`typing.Any` ] ] + The session bound function that this iterator should use for making + Get Guild Audit Log requests. + user_id : :obj:`str` + If specified, the user ID to filter by. + action_type : :obj:`int` + If specified, the action type to look up. + limit : :obj:`int` + If specified, the limit to how many entries this iterator should return + else unlimited. + before : :obj:`str` + If specified, an entry ID to specify where this iterator's returned + audit log entries should start . + + Note + ---- + This iterator's attributes :attr:`integrations`, :attr:`users` and + :attr:`webhooks` will be filled up as this iterator makes requests to the + Get Guild Audit Log endpoint with the relevant objects for entities + referenced by returned entries. + """ + + __slots__ = ( + "_buffer", + "_front", + "_kwargs", + "_limit", + "_request", + "integrations", + "users", + "webhooks", + ) + + #: A mapping of the partial objects of integrations found in this audit log + #: so far. + #: + #: :type: :obj:`typing.Mapping` [ :obj:`hikari.snowflakes.Snowflake`, :obj:`hikari.guilds.GuildIntegration` ] + integrations: typing.Mapping[snowflakes.Snowflake, guilds.GuildIntegration] + + #: A mapping of the objects of users found in this audit log so far. + #: + #: :type: :obj:`typing.Mapping` [ :obj:`hikari.snowflakes.Snowflake`, :obj:`hikari.users.User` ] + users: typing.Mapping[snowflakes.Snowflake, _users.User] + + #: A mapping of the objects of webhooks found in this audit log so far. + #: + #: :type: :obj:`typing.Mapping` [ :obj:`hikari.snowflakes.Snowflake`, :obj:`hikari.webhooks.Webhook` ] + webhooks: typing.Mapping[snowflakes.Snowflake, _webhooks.Webhook] + + def __init__( + self, + guild_id: str, + request: typing.Callable[..., typing.Coroutine[typing.Any, typing.Any, typing.Any]], + before: typing.Optional[str] = None, + user_id: str = ..., + action_type: int = ..., + limit: typing.Optional[int] = None, + ) -> None: + self._kwargs = {"guild_id": guild_id, "user_id": user_id, "action_type": action_type} + self._limit = limit + self._buffer = [] + self._request = request + self._front = before + self.users = {} + self.webhooks = {} + self.integrations = {} + + def __aiter__(self) -> "AuditLogIterator": + return self + + async def __anext__(self) -> AuditLogEntry: + if not self._buffer and self._limit != 0: + await self._fill() + try: + entry = AuditLogEntry.deserialize(self._buffer.pop()) + self._front = str(entry.id) + return entry + except IndexError: + raise StopAsyncIteration + + async def _fill(self) -> None: + """Retrieve entries before :attr:`_front` and add to :attr:`_buffer`.""" + payload = await self._request( + **self._kwargs, + before=self._front if self._front is not None else ..., + limit=100 if self._limit is None or self._limit > 100 else self._limit, + ) + if self._limit is not None: + self._limit -= len(payload["audit_log_entries"]) + + payload["audit_log_entries"].reverse() + self._buffer.extend(payload["audit_log_entries"]) + if users := payload.get("users"): + self.users = copy.copy(self.users) + self.users.update({u.id: u for u in map(_users.User.deserialize, users)}) + if webhooks := payload.get("webhooks"): + self.webhooks = copy.copy(self.webhooks) + self.webhooks.update({w.id: w for w in map(_webhooks.Webhook.deserialize, webhooks)}) + if integrations := payload.get("integrations"): + self.integrations = copy.copy(self.integrations) + self.integrations.update({i.id: i for i in map(guilds.PartialGuildIntegration.deserialize, integrations)}) diff --git a/hikari/channels.py b/hikari/channels.py index 678c6d4c67..9bd90f29b1 100644 --- a/hikari/channels.py +++ b/hikari/channels.py @@ -222,7 +222,7 @@ class GroupDMChannel(DMChannel): @marshaller.marshallable() @attr.s(slots=True) -class GuildChannel(Channel): +class GuildChannel(Channel, entities.Serializable): """The base for anything that is a guild channel.""" #: The ID of the guild the channel belongs to. diff --git a/hikari/clients/rest_clients.py b/hikari/clients/rest_clients.py index 3d4220cd30..85b05f2d4d 100644 --- a/hikari/clients/rest_clients.py +++ b/hikari/clients/rest_clients.py @@ -16,32 +16,3956 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . -"""May contain nuts.""" +"""Marshall wrappings for the REST implementation in :mod:`hikari.net.rest`. + +This provides an object-oriented interface for interacting with discord's REST +API. +""" + __all__ = ["RESTClient"] +import asyncio import datetime +import types + +import typing -from hikari import gateway_entities from hikari.clients import configs +from hikari.internal import assertions +from hikari.internal import conversions +from hikari.internal import more_collections +from hikari.net import rest +from hikari import audit_logs +from hikari import channels as _channels +from hikari import colors +from hikari import embeds as _embeds +from hikari import emojis +from hikari import gateway_entities +from hikari import guilds +from hikari import invites +from hikari import media +from hikari import messages as _messages +from hikari import oauth2 +from hikari import permissions as _permissions +from hikari import snowflakes +from hikari import users +from hikari import voices +from hikari import webhooks + + +def _get_member_id(member: guilds.GuildMember) -> str: + return str(member.user.id) class RESTClient: - """Stuff will go here when this is implemented...""" + """ + A marshalling object-oriented HTTP API. + + This component bridges the basic HTTP API exposed by + :obj:`hikari.net.rest.LowLevelRestfulClient` and wraps it in a unit of + processing that can handle parsing API objects into Hikari entity objects. + + Parameters + ---------- + config : :obj:`hikari.clients.configs.RESTConfig` + A HTTP configuration object. + + Note + ---- + For all endpoints where a ``reason`` argument is provided, this may be a + string inclusively between ``0`` and ``512`` characters length, with any + additional characters being cut off. + """ + + def __init__(self, config: configs.RESTConfig) -> None: + self._session = rest.LowLevelRestfulClient( + allow_redirects=config.allow_redirects, + connector=config.tcp_connector, + proxy_headers=config.proxy_headers, + proxy_auth=config.proxy_auth, + ssl_context=config.ssl_context, + verify_ssl=config.verify_ssl, + timeout=config.request_timeout, + token=config.token, + version=config.rest_version, + ) + + async def close(self) -> None: + """Shut down the REST client safely.""" + await self._session.close() + + async def __aenter__(self) -> "RESTClient": + return self + + async def __aexit__( + self, exc_type: typing.Type[BaseException], exc_val: BaseException, exc_tb: types.TracebackType + ) -> None: + await self.close() - # TODO: FasterSpeeding: update this. - def __init__(self, _: configs.RESTConfig) -> None: - ... + async def fetch_gateway_url(self) -> str: + """Get a generic url used for establishing a Discord gateway connection. - async def close(self): - ... + Returns + ------- + :obj:`str` + A static URL to use to connect to the gateway with. + + Note + ---- + Users are expected to attempt to cache this result. + """ + return await self._session.get_gateway() async def fetch_gateway_bot(self) -> gateway_entities.GatewayBot: - # Stubbed placeholder. - # TODO: replace with actual implementation. - return gateway_entities.GatewayBot( - url="wss://gateway.discord.gg", - shard_count=1, - session_start_limit=gateway_entities.SessionStartLimit( - total=1000, remaining=999, reset_after=datetime.timedelta(days=0.9) + """Get bot specific gateway information. + + Returns + ------- + :obj:`hikari.gateway_entities.GatewayBot` + The bot specific gateway information object. + + Note + ---- + Unlike :meth:`fetch_gateway_url`, this requires a valid token to work. + """ + payload = await self._session.get_gateway_bot() + return gateway_entities.GatewayBot.deserialize(payload) + + async def fetch_audit_log( + self, + guild: snowflakes.HashableT[guilds.Guild], + *, + user: snowflakes.HashableT[users.User] = ..., + action_type: typing.Union[audit_logs.AuditLogEventType, int] = ..., + limit: int = ..., + before: typing.Union[datetime.datetime, snowflakes.HashableT[audit_logs.AuditLogEntry]] = ..., + ) -> audit_logs.AuditLog: + """Get an audit log object for the given guild. + + Parameters + ---------- + guild : :obj:`typing.Union` [ :obj:`hikari.guilds.Guild`, :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ] + The object or ID of the guild to get the audit logs for. + user : :obj:`typing.Union` [ :obj:`hikari.users.User`, :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ] + If specified, the object or ID of the user to filter by. + action_type : :obj:`typing.Union` [ :obj:`hikari.audit_logs.AuditLogEventType`, :obj:`int` ] + If specified, the action type to look up. Passing a raw integer + for this may lead to unexpected behaviour. + limit : :obj:`int` + If specified, the limit to apply to the number of records. + Defaults to ``50``. Must be between ``1`` and ``100`` inclusive. + before : :obj:`typing.Union` [ :obj:`datetime.datetime`, :obj:`hikari.audit_logs.AuditLogEntry`, :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ] + If specified, the object or ID of the entry that all retrieved + entries should have occurred befor. + + Returns + ------- + :obj:`hikari.audit_logs.AuditLog` + An audit log object. + + Raises + ------ + :obj:`hikari.errors.BadRequestHTTPError` + If any invalid snowflake IDs are passed; a snowflake may be invalid + due to it being outside of the range of UINT64. + :obj:`hikari.errors.ForbiddenHTTPError` + If you lack the given permissions to view an audit log. + :obj:`hikari.errors.NotFoundHTTPError` + If the guild does not exist. + """ + if isinstance(before, datetime.datetime): + before = str(snowflakes.Snowflake.from_datetime(before)) + elif before is not ...: + before = str(before.id if isinstance(before, snowflakes.UniqueEntity) else int(before)) + payload = await self._session.get_guild_audit_log( + guild_id=str(guild.id if isinstance(guilds, snowflakes.UniqueEntity) else int(guild)), + user_id=( + str(user.id if isinstance(user, snowflakes.UniqueEntity) else int(user)) if user is not ... else ... + ), + action_type=action_type, + limit=limit, + before=before, + ) + return audit_logs.AuditLog.deserialize(payload) + + def fetch_audit_log_entries_before( + self, + guild: snowflakes.HashableT[guilds.Guild], + *, + before: typing.Union[datetime.datetime, snowflakes.HashableT[audit_logs.AuditLogEntry], None] = None, + user: snowflakes.HashableT[users.User] = ..., + action_type: typing.Union[audit_logs.AuditLogEventType, int] = ..., + limit: typing.Optional[int] = None, + ) -> audit_logs.AuditLogIterator: + """Return an async iterator that retrieves a guild's audit log entries. + + This will return the audit log entries before a given entry object/ID or + from the first guild audit log entry. + + Parameters + ---------- + guild : :obj:`typing.Union` [ :obj:`hikari.guilds.Guild`, :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ] + The ID or object of the guild to get audit log entries for + before : :obj:`typing.Union` [ :obj:`datetime.datetime`, :obj:`hikari.audit_logs.AuditLogEntry`, :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ], optional + If specified, the ID or object of the entry or datetime to get + entries that happened before otherwise this will start from the + newest entry. + user : :obj:`typing.Union` [ :obj:`hikari.users.User`, :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ] + If specified, the object or ID of the user to filter by. + action_type : :obj:`typing.Union` [ :obj:`hikari.audit_logs.AuditLogEventType`, :obj:`int` ] + If specified, the action type to look up. Passing a raw integer + for this may lead to unexpected behaviour. + limit : :obj:`int`, optional + If specified, the limit for how many entries this iterator should + return, defaults to unlimited. + + Example + ------- + .. code-block:: python + + audit_log_entries = client.fetch_audit_log_entries_before(guild, before=9876543, limit=6969) + async for entry in audit_log_entries: + if (user := audit_log_entries.users[entry.user_id]).is_bot: + await client.ban_member(guild, user) + + Note + ---- + The returned iterator has the attributes ``users``, ``members`` and + ``integrations`` which are mappings of snowflake IDs to objects for the + relevant entities that are referenced by the retrieved audit log + entries. These will be filled over time as more audit log entries are + fetched by the iterator. + + Returns + ------- + :obj:`hikari.errors.BadRequestHTTPError` + If any invalid snowflake IDs are passed; a snowflake may be invalid + due to it being outside of the range of UINT64. + :obj:`hikari.audit_logs.AuditLogIterator` + An async iterator of the audit log entries in a guild (from newest + to oldest). + """ + if isinstance(before, datetime.datetime): + before = str(snowflakes.Snowflake.from_datetime(before)) + elif before is not None: + before = str(before.id if isinstance(before, snowflakes.UniqueEntity) else int(before)) + return audit_logs.AuditLogIterator( + guild_id=str(guild.id if isinstance(guild, snowflakes.UniqueEntity) else int(guild)), + request=self._session.get_guild_audit_log, + before=before, + user_id=( + str(user.id if isinstance(user, snowflakes.UniqueEntity) else int(user)) if user is not ... else ... + ), + action_type=action_type, + limit=limit, + ) + + async def fetch_channel(self, channel: snowflakes.HashableT[_channels.Channel]) -> _channels.Channel: + """Get an up to date channel object from a given channel object or ID. + + Parameters + ---------- + channel : :obj:`typing.Union` [ :obj:`hikari.channels.Channel`, :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ] + The object ID of the channel to look up. + + Returns + ------- + :obj:`hikari.channels.Channel` + The channel object that has been found. + + Raises + ------ + :obj:`hikari.errors.BadRequestHTTPError` + If any invalid snowflake IDs are passed; a snowflake may be invalid + due to it being outside of the range of UINT64. + :obj:`hikari.errors.ForbiddenHTTPError` + If you don't have access to the channel. + :obj:`hikari.errors.NotFoundHTTPError` + If the channel does not exist. + """ + payload = await self._session.get_channel( + channel_id=str(channel.id if isinstance(channel, snowflakes.UniqueEntity) else int(channel)) + ) + return _channels.deserialize_channel(payload) + + async def update_channel( + self, + channel: snowflakes.HashableT[_channels.Channel], + *, + name: str = ..., + position: int = ..., + topic: str = ..., + nsfw: bool = ..., + bitrate: int = ..., + user_limit: int = ..., + rate_limit_per_user: typing.Union[int, datetime.timedelta] = ..., + permission_overwrites: typing.Sequence[_channels.PermissionOverwrite] = ..., + parent_category: typing.Optional[snowflakes.HashableT[_channels.GuildCategory]] = ..., + reason: str = ..., + ) -> _channels.Channel: + """Update one or more aspects of a given channel ID. + + Parameters + ---------- + channel : :obj:`typing.Union` [ :obj:`hikari.channels.Channel`, :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ] + The channel ID to update. + name : :obj:`str` + If specified, the new name for the channel. This must be + inclusively between ``1`` and ``100`` characters in length. + position : :obj:`int` + If specified, the position to change the channel to. + topic : :obj:`str` + If specified, the topic to set. This is only applicable to + text channels. This must be inclusively between ``0`` and ``1024`` + characters in length. + nsfw : :obj:`bool` + Mark the channel as being not safe for work (NSFW) if :obj:`True`. + If ``False`` or unspecified, then the channel is not marked as NSFW. + Will have no visiable effect for non-text guild channels. + rate_limit_per_user : :obj:`typing.Union` [ :obj:`int`, :obj:`datetime.timedelta` ] + If specified, the time delta of seconds the user has to wait + before sending another message. This will not apply to bots, or to + members with ``MANAGE_MESSAGES`` or ``MANAGE_CHANNEL`` permissions. + This must be inclusively between ``0`` and ``21600`` seconds. + bitrate : :obj:`int` + If specified, the bitrate in bits per second allowable for the + channel. This only applies to voice channels and must be inclusively + between ``8000`` and ``96000`` for normal servers or ``8000`` and + ``128000`` for VIP servers. + user_limit : :obj:`int` + If specified, the new max number of users to allow in a voice + channel. This must be between ``0`` and ``99`` inclusive, where + ``0`` implies no limit. + permission_overwrites : :obj:`typing.Sequence` [ :obj:`hikari.channels.PermissionOverwrite` ] + If specified, the new list of permission overwrites that are + category specific to replace the existing overwrites with. + parent_category : :obj:`typing.Union` [ :obj:`hikari.channels.Channel`, :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ], optional + If specified, the new parent category ID to set for the channel, + pass :obj:`None` to unset. + reason : :obj:`str` + If specified, the audit log reason explaining why the operation + was performed. + + Returns + ------- + :obj:`hikari.channels.Channel` + The channel object that has been modified. + + Raises + ------ + :obj:`hikari.errors.NotFoundHTTPError` + If the channel does not exist. + :obj:`hikari.errors.ForbiddenHTTPError` + If you lack the permission to make the change. + :obj:`hikari.errors.BadRequestHTTPError` + If you provide incorrect options for the corresponding channel type + (e.g. a ``bitrate`` for a text channel). + If any invalid snowflake IDs are passed; a snowflake may be invalid + due to it being outside of the range of UINT64. + """ + payload = await self._session.modify_channel( + channel_id=str(channel.id if isinstance(channel, snowflakes.UniqueEntity) else int(channel)), + name=name, + position=position, + topic=topic, + nsfw=nsfw, + bitrate=bitrate, + user_limit=user_limit, + rate_limit_per_user=( + int(rate_limit_per_user.total_seconds()) + if isinstance(rate_limit_per_user, datetime.timedelta) + else rate_limit_per_user + ), + permission_overwrites=( + [po.serialize() for po in permission_overwrites] if permission_overwrites is not ... else ... + ), + parent_id=( + str( + parent_category.id if isinstance(parent_category, snowflakes.UniqueEntity) else int(parent_category) + ) + if parent_category is not ... and parent_category is not None + else parent_category + ), + reason=reason, + ) + return _channels.deserialize_channel(payload) + + async def delete_channel(self, channel: snowflakes.HashableT[_channels.Channel]) -> None: + """Delete the given channel ID, or if it is a DM, close it. + + Parameters + ---------- + channel : :obj:`typing.Union` [ :obj:`hikari.channels.Channel`, :obj:`hikari.snowflakes.Snowflake` :obj:`str` ] + The object or ID of the channel to delete. + + Returns + ------- + :obj:`None` + Nothing, unlike what the API specifies. This is done to maintain + consistency with other calls of a similar nature in this API + wrapper. + + Raises + ------ + :obj:`hikari.errors.BadRequestHTTPError` + If any invalid snowflake IDs are passed; a snowflake may be invalid + due to it being outside of the range of UINT64. + :obj:`hikari.errors.NotFoundHTTPError` + If the channel does not exist. + :obj:`hikari.errors.ForbiddenHTTPError` + If you do not have permission to delete the channel. + + Note + ---- + Closing a DM channel won't raise an exception but will have no effect + and "closed" DM channels will not have to be reopened to send messages + in theme. + + Warning + ------- + Deleted channels cannot be un-deleted. + """ + await self._session.delete_close_channel( + channel_id=str(channel.id if isinstance(channel, snowflakes.UniqueEntity) else int(channel)) + ) + + def fetch_messages_after( + self, + channel: snowflakes.HashableT[_channels.Channel], + *, + after: typing.Union[datetime.datetime, snowflakes.HashableT[_messages.Message]] = 0, + limit: typing.Optional[int] = None, + ) -> typing.AsyncIterator[_messages.Message]: + """Return an async iterator that retrieves a channel's message history. + + This will return the message created after a given message object/ID or + from the first message in the channel. + + Parameters + ---------- + channel : :obj:`typing.Union` [ :obj:`hikari.channels.Channel`, :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ] + The ID of the channel to retrieve the messages from. + limit : :obj:`int` + If specified, the maximum number of how many messages this iterator + should return. + after : :obj:`typing.Union` [ :obj:`datetime.datetime`, :obj:`hikari.channels.Channel`, :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ] + A object or ID message. Only return messages sent AFTER this + message if it's specified else this will return every message after + (and including) the first message in the channel. + + Example + ------- + .. code-block:: python + + async for message in client.fetch_messages_after(channel, after=9876543, limit=3232): + if message.author.id in BLACKLISTED_USERS: + await client.ban_member(channel.guild_id, message.author) + + Returns + ------- + :obj:`typing.AsyncIterator` [ :obj:`hikari.messages.Message` ] + An async iterator that retrieves the channel's message objects. + + Raises + ------ + :obj:`hikari.errors.BadRequestHTTPError` + If any invalid snowflake IDs are passed; a snowflake may be invalid + due to it being outside of the range of UINT64. + :obj:`hikari.errors.ForbiddenHTTPError` + If you lack permission to read the channel. + :obj:`hikari.errors.NotFoundHTTPError` + If the channel is not found, or the message + provided for one of the filter arguments is not found. + + Note + ---- + If you are missing the ``VIEW_CHANNEL`` permission, you will receive a + :obj:`hikari.errors.ForbiddenHTTPError`. If you are instead missing + the ``READ_MESSAGE_HISTORY`` permission, you will always receive + zero results, and thus an empty list will be returned instead. + """ + if isinstance(after, datetime.datetime): + after = str(snowflakes.Snowflake.from_datetime(after)) + else: + after = str(after.id if isinstance(after, snowflakes.UniqueEntity) else int(after)) + return self._pagination_handler( + channel_id=str(channel.id if isinstance(channel, snowflakes.UniqueEntity) else int(channel)), + deserializer=_messages.Message.deserialize, + direction="after", + start=after, + request=self._session.get_channel_messages, + reversing=True, # This is the only known endpoint where reversing is needed. + limit=limit, + ) + + def fetch_messages_before( + self, + channel: snowflakes.HashableT[_channels.Channel], + *, + before: typing.Union[datetime.datetime, snowflakes.HashableT[_messages.Message], None] = None, + limit: typing.Optional[int] = None, + ) -> typing.AsyncIterator[_messages.Message]: + """Return an async iterator that retrieves a channel's message history. + + This returns the message created after a given message object/ID or + from the first message in the channel. + + Parameters + ---------- + channel : :obj:`typing.Union` [ :obj:`hikari.channels.Channel`, :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ] + The ID of the channel to retrieve the messages from. + limit : :obj:`int` + If specified, the maximum number of how many messages this iterator + should return. + before : :obj:`typing.Union` [ :obj:`datetime.datetime`, :obj:`hikari.channels.Channel`, :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ] + A message object or ID. Only return messages sent BEFORE + this message if this is specified else this will return every + message before (and including) the most recent message in the + channel. + + Example + ------- + .. code-block:: python + + async for message in client.fetch_messages_before(channel, before=9876543, limit=1231): + if message.content.lower().contains("delete this"): + await client.delete_message(channel, message) + + Returns + ------- + :obj:`typing.AsyncIterator` [ :obj:`hikari.messages.Message` ] + An async iterator that retrieves the channel's message objects. + + Raises + ------ + :obj:`hikari.errors.BadRequestHTTPError` + If any invalid snowflake IDs are passed; a snowflake may be invalid + due to it being outside of the range of UINT64. + :obj:`hikari.errors.ForbiddenHTTPError` + If you lack permission to read the channel. + :obj:`hikari.errors.NotFoundHTTPError` + If the channel is not found, or the message + provided for one of the filter arguments is not found. + + Note + ---- + If you are missing the ``VIEW_CHANNEL`` permission, you will receive a + :obj:`hikari.errors.ForbiddenHTTPError`. If you are instead missing + the ``READ_MESSAGE_HISTORY`` permission, you will always receive + zero results, and thus an empty list will be returned instead. + """ + if isinstance(before, datetime.datetime): + before = str(snowflakes.Snowflake.from_datetime(before)) + elif before is not None: + before = str(before.id if isinstance(before, snowflakes.UniqueEntity) else int(before)) + return self._pagination_handler( + channel_id=str(channel.id if isinstance(channel, snowflakes.UniqueEntity) else int(channel)), + deserializer=_messages.Message.deserialize, + direction="before", + start=before, + request=self._session.get_channel_messages, + reversing=False, + limit=limit, + ) + + async def fetch_messages_around( + self, + channel: snowflakes.HashableT[_channels.Channel], + around: typing.Union[datetime.datetime, snowflakes.HashableT[_messages.Message]], + *, + limit: int = ..., + ) -> typing.AsyncIterator[_messages.Message]: + """Return an async iterator that retrieves up to 100 messages. + + This will return messages in order from newest to oldest, is based + around the creation time of the supplied message object/ID and will + include the given message if it still exists. + + Parameters + ---------- + channel : :obj:`typing.Union` [ :obj:`hikari.channels.Channel`, :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ] + The ID of the channel to retrieve the messages from. + around : :obj:`typing.Union` [ :obj:`datetime.datetime`, :obj:`hikari.channels.Channel`, :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ] + The object or ID of the message to get messages that were sent + AROUND it in the provided channel, unlike ``before`` and ``after``, + this argument is required and the provided message will also be + returned if it still exists. + limit : :obj:`int` + If specified, the maximum number of how many messages this iterator + should return, cannot be more than `100` + + Example + ------- + .. code-block:: python + + async for message in client.fetch_messages_around(channel, around=9876543, limit=42): + if message.embeds and not message.author.is_bot: + await client.delete_message(channel, message) + + Returns + ------- + :obj:`typing.AsyncIterator` [ :obj:`hikari.messages.Message` ] + An async iterator that retrieves the found message objects. + + Raises + ------ + :obj:`hikari.errors.BadRequestHTTPError` + If any invalid snowflake IDs are passed; a snowflake may be invalid + due to it being outside of the range of UINT64. + :obj:`hikari.errors.ForbiddenHTTPError` + If you lack permission to read the channel. + :obj:`hikari.errors.NotFoundHTTPError` + If the channel is not found, or the message + provided for one of the filter arguments is not found. + + Note + ---- + If you are missing the ``VIEW_CHANNEL`` permission, you will receive a + :obj:`hikari.errors.ForbiddenHTTPError`. If you are instead missing + the ``READ_MESSAGE_HISTORY`` permission, you will always receive + zero results, and thus an empty list will be returned instead. + """ + if isinstance(around, datetime.datetime): + around = str(snowflakes.Snowflake.from_datetime(around)) + else: + around = str(around.id if isinstance(around, snowflakes.UniqueEntity) else int(around)) + for payload in await self._session.get_channel_messages( + channel_id=str(channel.id if isinstance(channel, snowflakes.UniqueEntity) else int(channel)), + limit=limit, + around=around, + ): + yield _messages.Message.deserialize(payload) + + @staticmethod + async def _pagination_handler( + deserializer: typing.Callable[[typing.Any], typing.Any], + direction: typing.Union[typing.Literal["before"], typing.Literal["after"]], + request: typing.Callable[..., typing.Coroutine[typing.Any, typing.Any, typing.Any]], + reversing: bool, + start: typing.Union[str, None], + limit: typing.Optional[int] = None, + id_getter: typing.Callable[[typing.Any], str] = lambda entity: str(entity.id), + **kwargs, + ) -> typing.AsyncIterator[typing.Any]: + """Generate an async iterator for handling paginated endpoints. + + This will handle Discord's ``before`` and ``after`` pagination. + + Parameters + ---------- + deserializer : :obj:`typing.Callable` [ [ :obj:`typing.Any` ], :obj:`typing.Any` ] + The deserializer to use to deserialize raw elements. + direction : :obj:`typing.Union` [ ``"before"``, ``"after"`` ] + The direction that this paginator should go in. + request : :obj:`typing.Callable` [ ``...``, :obj:`typing.Coroutine` [ :obj:`typing.Any`, :obj:`typing.Any`, :obj:`typing.Any` ] ] + The function on :attr:`_session` that should be called to make + requests for this paginator. + reversing : :obj:`bool` + Whether the retrieved array of objects should be reversed before + iterating through it, this is needed for certain endpoints like + ``fetch_messages_before`` where the order is static regardless of + if you're using ``before`` or ``after``. + start : :obj:`int`, optional + The snowflake ID that this paginator should start at, ``0`` may be + passed for ``forward`` pagination to start at the first created + entity and :obj:`None` may be passed for ``before`` pagination to + start at the newest entity (based on when it's snowflake timestamp). + limit : :obj:`int`, optional + The amount of deserialized entities that the iterator should return + total, will be unlimited if set to :obj:`None`. + id_getter : :obj:`typing.Callable` [ [ :obj:`typing.Any` ], :obj:`str` ] + **kwargs + Kwargs to pass through to ``request`` for every request made along + with the current decided limit and direction snowflake. + + Returns + ------- + :obj:`typing.AsyncIterator` [ :obj:`typing.Any` ] + An async iterator of the found deserialized found objects. + + """ + while payloads := await request( + limit=100 if limit is None or limit > 100 else limit, + **{direction: start if start is not None else ...}, + **kwargs, + ): + if reversing: + payloads.reverse() + if limit is not None: + limit -= len(payloads) + + for payload in payloads: + entity = deserializer(payload) + yield entity + if limit == 0: + break + start = id_getter(entity) + + async def fetch_message( + self, channel: snowflakes.HashableT[_channels.Channel], message: snowflakes.HashableT[_messages.Message], + ) -> _messages.Message: + """Get a message from known channel that we can access. + + Parameters + ---------- + channel : :obj:`typing.Union` [ :obj:`hikari.channels.Channel`, :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ] + The object or ID of the channel to get the message from. + message : :obj:`typing.Union` [ :obj:`hikari.messages.Message`, :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ] + The object or ID of the message to retrieve. + + Returns + ------- + :obj:`hikari.messages.Message` + The found message object. + + Note + ---- + This requires the ``READ_MESSAGE_HISTORY`` permission. + + Raises + ------ + :obj:`hikari.errors.BadRequestHTTPError` + If any invalid snowflake IDs are passed; a snowflake may be invalid + due to it being outside of the range of UINT64. + :obj:`hikari.errors.ForbiddenHTTPError` + If you lack permission to see the message. + :obj:`hikari.errors.NotFoundHTTPError` + If the channel or message is not found. + """ + payload = await self._session.get_channel_message( + channel_id=str(channel.id if isinstance(channel, snowflakes.UniqueEntity) else int(channel)), + message_id=str(message.id if isinstance(message, snowflakes.UniqueEntity) else int(message)), + ) + return _messages.Message.deserialize(payload) + + @staticmethod + def _generate_allowed_mentions( + mentions_everyone: bool, + user_mentions: typing.Union[typing.Collection[snowflakes.HashableT[users.User]], bool], + role_mentions: typing.Union[typing.Collection[snowflakes.HashableT[guilds.GuildRole]], bool], + ) -> typing.Dict[str, typing.Sequence[str]]: + """Generate an allowed mentions object based on input mention rules. + + Parameters + ---------- + mentions_everyone : :obj:`bool` + Whether ``@everyone`` and ``@here`` mentions should be resolved by + discord and lead to actual pings. + user_mentions : :obj:`typing.Union` [ :obj:`typing.Collection` [ :obj:`typing.Union` [ :obj:`hikari.users.User`, :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ], :obj:`bool` ] + Either an array of user objects/IDs to allow mentions for, + :obj:`True` to allow all user mentions or ``False`` to block all + user mentions from resolving. + role_mentions : :obj:`typing.Union` [ :obj:`typing.Collection` [ :obj:`typing.Union` [ :obj:`hikari.guilds.GuildRole`, :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ] ], :obj:`bool` ] + Either an array of guild role objects/IDs to allow mentions for, + :obj:`True` to allow all role mentions or ``False`` to block all + role mentions from resolving. + + Returns + ------- + :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Sequence` [ :obj:`str` ] ] + The resulting allowed mentions dict object. + """ + parsed_mentions = [] + allowed_mentions = {} + if mentions_everyone is True: + parsed_mentions.append("everyone") + if user_mentions is True: + parsed_mentions.append("users") + # This covers both `False` and an array of IDs/objs by using `user_mentions or EMPTY_SEQUENCE`, where a + # resultant empty list will mean that all user mentions are blacklisted. + else: + allowed_mentions["users"] = list( + # dict.fromkeys is used to remove duplicate entries that would cause discord to return an error. + dict.fromkeys( + str(user.id if isinstance(user, snowflakes.UniqueEntity) else int(user)) + for user in user_mentions or more_collections.EMPTY_SEQUENCE + ) + ) + assertions.assert_that(len(allowed_mentions["users"]) <= 100, "Only up to 100 users can be provided.") + if role_mentions is True: + parsed_mentions.append("roles") + # This covers both `False` and an array of IDs/objs by using `user_mentions or EMPTY_SEQUENCE`, where a + # resultant empty list will mean that all role mentions are blacklisted. + else: + allowed_mentions["roles"] = list( + # dict.fromkeys is used to remove duplicate entries that would cause discord to return an error. + dict.fromkeys( + str(role.id if isinstance(role, snowflakes.UniqueEntity) else int(role)) + for role in role_mentions or more_collections.EMPTY_SEQUENCE + ) + ) + assertions.assert_that(len(allowed_mentions["roles"]) <= 100, "Only up to 100 roles can be provided.") + allowed_mentions["parse"] = parsed_mentions + # As a note, discord will also treat an empty `allowed_mentions` object as if it wasn't passed at all, so we + # want to use empty lists for blacklisting elements rather than just not including blacklisted elements. + return allowed_mentions + + async def create_message( + self, + channel: snowflakes.HashableT[_channels.Channel], + *, + content: str = ..., + nonce: str = ..., + tts: bool = ..., + files: typing.Collection[media.IO] = ..., + embed: _embeds.Embed = ..., + mentions_everyone: bool = True, + user_mentions: typing.Union[typing.Collection[snowflakes.HashableT[users.User]], bool] = True, + role_mentions: typing.Union[typing.Collection[snowflakes.HashableT[guilds.GuildRole]], bool] = True, + ) -> _messages.Message: + """Create a message in the given channel. + + Parameters + ---------- + channel : :obj:`typing.Union` [ :obj:`hikari.channels.Channel`, :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ] + The channel or ID of the channel to send to. + content : :obj:`str` + If specified, the message content to send with the message. + nonce : :obj:`str` + If specified, an optional ID to send for opportunistic message + creation. This doesn't serve any real purpose for general use, + and can usually be ignored. + tts : :obj:`bool` + If specified, whether the message will be sent as a TTS message. + files : :obj:`typing.Collection` [ :obj:`hikari.media.IO` ] + If specified, this should be a list of inclusively between ``1`` and + ``5`` IO like media objects, as defined in :mod:`hikari.media`. + embed : :obj:`hikari.embeds.Embed` + If specified, the embed object to send with the message. + mentions_everyone : :obj:`bool` + Whether ``@everyone`` and ``@here`` mentions should be resolved by + discord and lead to actual pings, defaults to :obj:`True`. + user_mentions : :obj:`typing.Union` [ :obj:`typing.Collection` [ :obj:`typing.Union` [ :obj:`hikari.users.User`, :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ], :obj:`bool` ] + Either an array of user objects/IDs to allow mentions for, + :obj:`True` to allow all user mentions or ``False`` to block all + user mentions from resolving, defaults to :obj:`True`. + role_mentions : :obj:`typing.Union` [ :obj:`typing.Collection` [ :obj:`typing.Union` [ :obj:`hikari.guilds.GuildRole`, :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ] ], :obj:`bool` ] + Either an array of guild role objects/IDs to allow mentions for, + :obj:`True` to allow all role mentions or ``False`` to block all + role mentions from resolving, defaults to :obj:`True`. + + Returns + ------- + :obj:`hikari.messages.Message` + The created message object. + + Raises + ------ + :obj:`hikari.errors.NotFoundHTTPError` + If the channel is not found. + :obj:`hikari.errors.BadRequestHTTPError` + This can be raised if the file is too large; if the embed exceeds + the defined limits; if the message content is specified only and + empty or greater than ``2000`` characters; if neither content, files + or embed are specified. + If any invalid snowflake IDs are passed; a snowflake may be invalid + due to it being outside of the range of UINT64. + :obj:`hikari.errors.ForbiddenHTTPError` + If you lack permissions to send to this channel. + """ + payload = await self._session.create_message( + channel_id=str(channel.id if isinstance(channel, snowflakes.UniqueEntity) else int(channel)), + content=content, + nonce=nonce, + tts=tts, + files=await asyncio.gather(*(media.safe_read_file(file) for file in files)) if files is not ... else ..., + embed=embed.serialize() if embed is not ... else ..., + allowed_mentions=self._generate_allowed_mentions( + mentions_everyone=mentions_everyone, user_mentions=user_mentions, role_mentions=role_mentions + ), + ) + return _messages.Message.deserialize(payload) + + def safe_create_message( + self, + channel: snowflakes.HashableT[_channels.Channel], + *, + content: str = ..., + nonce: str = ..., + tts: bool = ..., + files: typing.Collection[media.IO] = ..., + embed: _embeds.Embed = ..., + mentions_everyone: bool = False, + user_mentions: typing.Union[typing.Collection[snowflakes.HashableT[users.User]], bool] = False, + role_mentions: typing.Union[typing.Collection[snowflakes.HashableT[guilds.GuildRole]], bool] = False, + ) -> typing.Coroutine[typing.Any, typing.Any, _messages.Message]: + """Create a message in the given channel with mention safety. + + This endpoint has the same signature as :attr:`create_message` with + the only difference being that ``mentions_everyone``, + ``user_mentions`` and ``role_mentions`` default to ``False``. + """ + return self.create_message( + channel=channel, + content=content, + nonce=nonce, + tts=tts, + files=files, + embed=embed, + mentions_everyone=mentions_everyone, + user_mentions=user_mentions, + role_mentions=role_mentions, + ) + + async def create_reaction( + self, + channel: snowflakes.HashableT[_channels.Channel], + message: snowflakes.HashableT[_messages.Message], + emoji: typing.Union[emojis.Emoji, str], + ) -> None: + """Add a reaction to the given message in the given channel. + + Parameters + ---------- + channel : :obj:`typing.Union` [ :obj:`hikari.channels.Channel`, :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ] + The object or ID of the channel to add this reaction in. + message : :obj:`typing.Union` [ :obj:`hikari.messages.Message`, :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ] + The object or ID of the message to add the reaction in. + emoji : :obj:`typing.Union` [ :obj:`hikari.emojis.Emoji`, :obj:`str` ] + The emoji to add. This can either be an emoji object or a string + representation of an emoji. The string representation will be either + ``"name:id"`` for custom emojis else it's unicode character(s) (can + be UTF-32). + + Raises + ------ + :obj:`hikari.errors.ForbiddenHTTPError` + If this is the first reaction using this specific emoji on this + message and you lack the ``ADD_REACTIONS`` permission. If you lack + ``READ_MESSAGE_HISTORY``, this may also raise this error. + :obj:`hikari.errors.NotFoundHTTPError` + If the channel or message is not found, or if the emoji is not found. + :obj:`hikari.errors.BadRequestHTTPError` + If the emoji is not valid, unknown, or formatted incorrectly. + If any invalid snowflake IDs are passed; a snowflake may be invalid + due to it being outside of the range of UINT64. + """ + await self._session.create_reaction( + channel_id=str(channel.id if isinstance(channel, snowflakes.UniqueEntity) else int(channel)), + message_id=str(message.id if isinstance(message, snowflakes.UniqueEntity) else int(message)), + emoji=str(getattr(emoji, "url_name", emoji)), + ) + + async def delete_reaction( + self, + channel: snowflakes.HashableT[_channels.Channel], + message: snowflakes.HashableT[_messages.Message], + emoji: typing.Union[emojis.Emoji, str], + ) -> None: + """Remove your own reaction from the given message in the given channel. + + Parameters + ---------- + channel : :obj:`typing.Union` [ :obj:`hikari.channels.Channel`, :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ] + The object or ID of the channel to add this reaction in. + message : :obj:`typing.Union` [ :obj:`hikari.messages.Message`, :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ] + The object or ID of the message to add the reaction in. + emoji : :obj:`typing.Union` [ :obj:`hikari.emojis.Emoji`, :obj:`str` ] + The emoji to add. This can either be an emoji object or a + string representation of an emoji. The string representation will be + either ``"name:id"`` for custom emojis else it's unicode + character(s) (can be UTF-32). + + Raises + ------ + :obj:`hikari.errors.ForbiddenHTTPError` + If this is the first reaction using this specific emoji on this + message and you lack the ``ADD_REACTIONS`` permission. If you lack + ``READ_MESSAGE_HISTORY``, this may also raise this error. + :obj:`hikari.errors.NotFoundHTTPError` + If the channel or message is not found, or if the emoji is not + found. + :obj:`hikari.errors.BadRequestHTTPError` + If the emoji is not valid, unknown, or formatted incorrectly. + If any invalid snowflake IDs are passed; a snowflake may be invalid + due to it being outside of the range of UINT64. + """ + await self._session.delete_own_reaction( + channel_id=str(channel.id if isinstance(channel, snowflakes.UniqueEntity) else int(channel)), + message_id=str(message.id if isinstance(message, snowflakes.UniqueEntity) else int(message)), + emoji=str(getattr(emoji, "url_name", emoji)), + ) + + async def delete_all_reactions( + self, channel: snowflakes.HashableT[_channels.Channel], message: snowflakes.HashableT[_messages.Message], + ) -> None: + """Delete all reactions from a given message in a given channel. + + Parameters + ---------- + channel : :obj:`typing.Union` [ :obj:`hikari.channels.Channel`, :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ] + The object or ID of the channel to get the message from. + message : :obj:`typing.Union` [ :obj:`hikari.messages.Message`, :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ] + The object or ID of the message to remove all reactions from. + + Raises + ------ + :obj:`hikari.errors.BadRequestHTTPError` + If any invalid snowflake IDs are passed; a snowflake may be invalid + due to it being outside of the range of UINT64. + :obj:`hikari.errors.NotFoundHTTPError` + If the channel or message is not found. + :obj:`hikari.errors.ForbiddenHTTPError` + If you lack the ``MANAGE_MESSAGES`` permission. + """ + await self._session.delete_all_reactions( + channel_id=str(channel.id if isinstance(channel, snowflakes.UniqueEntity) else int(channel)), + message_id=str(message.id if isinstance(message, snowflakes.UniqueEntity) else int(message)), + ) + + async def delete_all_reactions_for_emoji( + self, + channel: snowflakes.HashableT[_channels.Channel], + message: snowflakes.HashableT[_messages.Message], + emoji: typing.Union[emojis.Emoji, str], + ) -> None: + """Remove all reactions for a single given emoji on a given message. + + Parameters + ---------- + channel : :obj:`typing.Union` [ :obj:`hikari.channels.Channel`, :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ] + The object or ID of the channel to get the message from. + message : :obj:`typing.Union` [ :obj:`hikari.messages.Message`, :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ] + The object or ID of the message to delete the reactions from. + emoji : :obj:`typing.Union` [ :obj:`hikari.emojis.Emoji`, :obj:`str` ] + The object or string representatiom of the emoji to delete. The + string representation will be either ``"name:id"`` for custom emojis + else it's unicode character(s) (can be UTF-32). + + Raises + ------ + :obj:`hikari.errors.BadRequestHTTPError` + If any invalid snowflake IDs are passed; a snowflake may be invalid + due to it being outside of the range of UINT64. + :obj:`hikari.errors.NotFoundHTTPError` + If the channel or message or emoji or user is not found. + :obj:`hikari.errors.ForbiddenHTTPError` + If you lack the ``MANAGE_MESSAGES`` permission, or the channel is a + DM channel. + """ + await self._session.delete_all_reactions_for_emoji( + channel_id=str(channel.id if isinstance(channel, snowflakes.UniqueEntity) else int(channel)), + message_id=str(message.id if isinstance(message, snowflakes.UniqueEntity) else int(message)), + emoji=str(getattr(emoji, "url_name", emoji)), + ) + + def fetch_reactors_after( + self, + channel: snowflakes.HashableT[_channels.Channel], + message: snowflakes.HashableT[_messages.Message], + emoji: typing.Union[emojis.Emoji, str], + *, + after: typing.Union[datetime.datetime, snowflakes.HashableT[users.User]] = 0, + limit: typing.Optional[int] = None, + ) -> typing.AsyncIterator[users.User]: + """Get an async iterator of the users who reacted to a message. + + This returns the users created after a given user object/ID or from the + oldest user who reacted. + + Parameters + ---------- + channel : :obj:`typing.Union` [ :obj:`hikari.channels.Channel`, :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ] + The object or ID of the channel to get the message from. + message : :obj:`typing.Union` [ :obj:`hikari.messages.Message`, :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ] + The object or ID of the message to get the reactions from. + emoji : :obj:`typing.Union` [ :obj:`hikari.emojis.Emoji`, :obj:`str` ] + The emoji to get. This can either be it's object or the string + representation of the emoji. The string representation will be + either ``"name:id"`` for custom emojis else it's unicode + character(s) (can be UTF-32). + after : :obj:`typing.Union` [ :obj:`datetime.datetime`, :obj:`hikari.users.User`, :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ] + If specified, a object or ID user. If specified, only users with a + snowflake that is lexicographically greater than the value will be + returned. + limit : :obj:`str` + If specified, the limit of the number of users this iterator should + return. + + Example + ------- + .. code-block:: python + + async for user in client.fetch_reactors_after(channel, message, emoji, after=9876543, limit=1231): + if user.is_bot: + await client.kick_member(channel.guild_id, user) + + Returns + ------- + :obj:`typing.AsyncIterator` [ :obj:`hikari.users.User` ] + An async iterator of user objects. + + Raises + ------ + :obj:`hikari.errors.BadRequestHTTPError` + If any invalid snowflake IDs are passed; a snowflake may be invalid + due to it being outside of the range of UINT64. + :obj:`hikari.errors.ForbiddenHTTPError` + If you lack access to the message. + :obj:`hikari.errors.NotFoundHTTPError` + If the channel or message is not found. + """ + if isinstance(after, datetime.datetime): + after = str(snowflakes.Snowflake.from_datetime(after)) + else: + after = str(after.id if isinstance(after, snowflakes.UniqueEntity) else int(after)) + return self._pagination_handler( + channel_id=str(channel.id if isinstance(channel, snowflakes.UniqueEntity) else int(channel)), + message_id=str(message.id if isinstance(message, snowflakes.UniqueEntity) else int(message)), + emoji=getattr(emoji, "url_name", emoji), + deserializer=users.User.deserialize, + direction="after", + request=self._session.get_reactions, + reversing=False, + start=after, + limit=limit, + ) + + async def update_message( + self, + message: snowflakes.HashableT[_messages.Message], + channel: snowflakes.HashableT[_channels.Channel], + *, + content: typing.Optional[str] = ..., + embed: typing.Optional[_embeds.Embed] = ..., + flags: int = ..., + mentions_everyone: bool = True, + user_mentions: typing.Union[typing.Collection[snowflakes.HashableT[users.User]], bool] = True, + role_mentions: typing.Union[typing.Collection[snowflakes.HashableT[guilds.GuildRole]], bool] = True, + ) -> _messages.Message: + """Update the given message. + + Parameters + ---------- + channel : :obj:`typing.Union` [ :obj:`hikari.channels.Channel`, :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ] + The object or ID of the channel to get the message from. + message : :obj:`typing.Union` [ :obj:`hikari.messages.Message`, :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ] + The object or ID of the message to edit. + content : :obj:`str`, optional + If specified, the string content to replace with in the message. + If :obj:`None`, the content will be removed from the message. + embed : :obj:`hikari.embeds.Embed`, optional + If specified, the embed to replace with in the message. + If :obj:`None`, the embed will be removed from the message. + flags : :obj:`hikari.messages.MessageFlag` + If specified, the new flags for this message, while a raw int may + be passed for this, this can lead to unexpected behaviour if it's + outside the range of the MessageFlag int flag. + mentions_everyone : :obj:`bool` + Whether ``@everyone`` and ``@here`` mentions should be resolved by + discord and lead to actual pings, defaults to :obj:`True`. + user_mentions : :obj:`typing.Union` [ :obj:`typing.Collection` [ :obj:`typing.Union` [ :obj:`hikari.users.User`, :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ], :obj:`bool` ] + Either an array of user objects/IDs to allow mentions for, + :obj:`True` to allow all user mentions or ``False`` to block all + user mentions from resolving, defaults to :obj:`True`. + role_mentions : :obj:`typing.Union` [ :obj:`typing.Collection` [ :obj:`typing.Union` [ :obj:`hikari.guilds.GuildRole`, :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ] ], :obj:`bool` ] + Either an array of guild role objects/IDs to allow mentions for, + :obj:`True` to allow all role mentions or ``False`` to block all + role mentions from resolving, defaults to :obj:`True`. + + Returns + ------- + :obj:`hikari.messages.Message` + The edited message object. + + Raises + ------ + :obj:`hikari.errors.NotFoundHTTPError` + If the channel or message is not found. + :obj:`hikari.errors.BadRequestHTTPError` + This can be raised if the embed exceeds the defined limits; + if the message content is specified only and empty or greater + than ``2000`` characters; if neither content, file or embed + are specified. + If any invalid snowflake IDs are passed; a snowflake may be invalid + due to it being outside of the range of UINT64. + :obj:`hikari.errors.ForbiddenHTTPError` + If you try to edit ``content`` or ``embed`` or ``allowed_mentions` + on a message you did not author. + If you try to edit the flags on a message you did not author without + the ``MANAGE_MESSAGES`` permission. + """ + payload = await self._session.edit_message( + channel_id=str(channel.id if isinstance(channel, snowflakes.UniqueEntity) else int(channel)), + message_id=str(message.id if isinstance(message, snowflakes.UniqueEntity) else int(message)), + content=content, + embed=embed.serialize() if embed is not ... and embed is not None else embed, + flags=flags, + allowed_mentions=self._generate_allowed_mentions( + mentions_everyone=mentions_everyone, user_mentions=user_mentions, role_mentions=role_mentions, + ), + ) + return _messages.Message.deserialize(payload) + + def safe_update_message( + self, + message: snowflakes.HashableT[_messages.Message], + channel: snowflakes.HashableT[_channels.Channel], + *, + content: typing.Optional[str] = ..., + embed: typing.Optional[_embeds.Embed] = ..., + flags: int = ..., + mentions_everyone: bool = False, + user_mentions: typing.Union[typing.Collection[snowflakes.HashableT[users.User]], bool] = False, + role_mentions: typing.Union[typing.Collection[snowflakes.HashableT[guilds.GuildRole]], bool] = False, + ) -> typing.Coroutine[typing.Any, typing.Any, _messages.Message]: + """Update a message in the given channel with mention safety. + + This endpoint has the same signature as :attr:`execute_webhook` with + the only difference being that ``mentions_everyone``, + ``user_mentions`` and ``role_mentions`` default to ``False``. + """ + return self.update_message( + message=message, + channel=channel, + content=content, + embed=embed, + flags=flags, + mentions_everyone=mentions_everyone, + user_mentions=user_mentions, + role_mentions=role_mentions, + ) + + async def delete_messages( + self, + channel: snowflakes.HashableT[_channels.Channel], + message: snowflakes.HashableT[_messages.Message], + *additional_messages: snowflakes.HashableT[_messages.Message], + ) -> None: + """Delete a message in a given channel. + + Parameters + ---------- + channel : :obj:`typing.Union` [ :obj:`hikari.channels.Channel`, :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ] + The object or ID of the channel to get the message from. + message : :obj:`typing.Union` [ :obj:`hikari.messages.Message`, :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ] + The object or ID of the message to delete. + *additional_messages : :obj:`typing.Union` [ :obj:`hikari.messages.Message`, :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ] + Objects and/or IDs of additional messages to delete in the same + channel, in total you can delete up to 100 messages in a request. + + Raises + ------ + :obj:`hikari.errors.BadRequestHTTPError` + If any invalid snowflake IDs are passed; a snowflake may be invalid + due to it being outside of the range of UINT64. + :obj:`hikari.errors.ForbiddenHTTPError` + If you did not author the message and are in a DM, or if you did + not author the message and lack the ``MANAGE_MESSAGES`` + permission in a guild channel. + :obj:`hikari.errors.NotFoundHTTPError` + If the channel or message is not found. + :obj:`ValueError` + If you try to delete over ``100`` messages in a single request. + + Note + ---- + This can only be used on guild text channels. + Any message IDs that do not exist or are invalid still add towards the + total ``100`` max messages to remove. This can only delete messages + that are newer than ``2`` weeks in age. If any of the messages ar + older than ``2`` weeks then this call will fail. + """ + if additional_messages: + messages = list( + # dict.fromkeys is used to remove duplicate entries that would cause discord to return an error. + dict.fromkeys( + str(m.id if isinstance(m, snowflakes.UniqueEntity) else int(m)) + for m in (message, *additional_messages) + ) + ) + assertions.assert_that( + len(messages) <= 100, "Only up to 100 messages can be bulk deleted in a single request." + ) + + if len(messages) > 1: + await self._session.bulk_delete_messages( + channel_id=str(channel.id if isinstance(channel, snowflakes.UniqueEntity) else int(channel)), + messages=messages, + ) + return None + + await self._session.delete_message( + channel_id=str(channel.id if isinstance(channel, snowflakes.UniqueEntity) else int(channel)), + message_id=str(message.id if isinstance(message, snowflakes.UniqueEntity) else int(message)), + ) + + async def update_channel_overwrite( + self, + channel: snowflakes.HashableT[_messages.Message], + overwrite: typing.Union[_channels.PermissionOverwrite, users.User, guilds.GuildRole, snowflakes.Snowflake, int], + target_type: typing.Union[_channels.PermissionOverwriteType, str], + *, + allow: typing.Union[_permissions.Permission, int] = ..., + deny: typing.Union[_permissions.Permission, int] = ..., + reason: str = ..., + ) -> None: + """Edit permissions for a given channel. + + Parameters + ---------- + channel : :obj:`typing.Union` [ :obj:`hikari.messages.Message`, :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ] + The object or ID of the channel to edit permissions for. + overwrite : :obj:`typing.Union` [ :obj:`hikari.channels.PermissionOverwrite`, :obj:`hikari.guilds.GuildRole`, :obj:`hikari.users.User`, :obj:`hikari.snowflakes.Snowflake` , :obj:`int` ] + The object or ID of the target member or role to edit/create the + overwrite for. + target_type : :obj:`typing.Union` [ :obj:`hikari.channels.PermissionOverwriteType`, :obj:`int` ] + The type of overwrite, passing a raw string that's outside of the + enum's range for this may lead to unexpected behaviour. + allow : :obj:`typing.Union` [ :obj:`hikari.permissions.Permission`, :obj:`int` ] + If specified, the value of all permissions to set to be allowed, + passing a raw integer for this may lead to unexpected behaviour. + deny : :obj:`typing.Union` [ :obj:`hikari.permissions.Permission`, :obj:`int` ] + If specified, the value of all permissions to set to be denied, + passing a raw integer for this may lead to unexpected behaviour. + reason : :obj:`str` + If specified, the audit log reason explaining why the operation + was performed. + + Raises + ------ + :obj:`hikari.errors.BadRequestHTTPError` + If any invalid snowflake IDs are passed; a snowflake may be invalid + due to it being outside of the range of UINT64. + :obj:`hikari.errors.NotFoundHTTPError` + If the target channel or overwrite doesn't exist. + :obj:`hikari.errors.ForbiddenHTTPError` + If you lack permission to do this. + """ + await self._session.edit_channel_permissions( + channel_id=str(channel.id if isinstance(channel, snowflakes.UniqueEntity) else int(channel)), + overwrite_id=str(overwrite.id if isinstance(overwrite, snowflakes.UniqueEntity) else int(overwrite)), + type_=target_type, + allow=allow, + deny=deny, + reason=reason, + ) + + async def fetch_invites_for_channel( + self, channel: snowflakes.HashableT[_channels.Channel] + ) -> typing.Sequence[invites.InviteWithMetadata]: + """Get invites for a given channel. + + Parameters + ---------- + channel : :obj:`typing.Union` [ :obj:`hikari.channels.Channel`, :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ] + The object or ID of the channel to get invites for. + + Returns + ------- + :obj:`typing.Sequence` [ :obj:`hikari.invites.InviteWithMetadata` ] + A list of invite objects. + + Raises + ------ + :obj:`hikari.errors.BadRequestHTTPError` + If any invalid snowflake IDs are passed; a snowflake may be invalid + due to it being outside of the range of UINT64. + :obj:`hikari.errors.ForbiddenHTTPError` + If you lack the ``MANAGE_CHANNELS`` permission. + :obj:`hikari.errors.NotFoundHTTPError` + If the channel does not exist. + """ + payload = await self._session.get_channel_invites( + channel_id=str(channel.id if isinstance(channel, snowflakes.UniqueEntity) else int(channel)) + ) + return [invites.InviteWithMetadata.deserialize(invite) for invite in payload] + + async def create_invite_for_channel( + self, + channel: snowflakes.HashableT[_channels.Channel], + *, + max_age: typing.Union[int, datetime.timedelta] = ..., + max_uses: int = ..., + temporary: bool = ..., + unique: bool = ..., + target_user: snowflakes.HashableT[users.User] = ..., + target_user_type: typing.Union[invites.TargetUserType, int] = ..., + reason: str = ..., + ) -> invites.InviteWithMetadata: + """Create a new invite for the given channel. + + Parameters + ---------- + channel : :obj:`typing.Union` [ :obj:`datetime.timedelta`, :obj:`str` ] + The object or ID of the channel to create the invite for. + max_age : :obj:`int` + If specified, the seconds time delta for the max age of the invite, + defaults to ``86400`` seconds (``24`` hours). + Set to ``0`` seconds to never expire. + max_uses : :obj:`int` + If specified, the max number of uses this invite can have, or ``0`` + for unlimited (as per the default). + temporary : :obj:`bool` + If specified, whether to grant temporary membership, meaning the + user is kicked when their session ends unless they are given a role. + unique : :obj:`bool` + If specified, whether to try to reuse a similar invite. + target_user : :obj:`typing.Union` [ :obj:`hikari.users.User`, :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ] + If specified, the object or ID of the user this invite should + target. + target_user_type : :obj:`typing.Union` [ :obj:`hikari.invites.TargetUserType`, :obj:`int` ] + If specified, the type of target for this invite, passing a raw + integer for this may lead to unexpected results. + reason : :obj:`str` + If specified, the audit log reason explaining why the operation + was performed. + + Returns + ------- + :obj:`hikari.invites.InviteWithMetadata` + The created invite object. + + Raises + ------ + :obj:`hikari.errors.ForbiddenHTTPError` + If you lack the ``CREATE_INSTANT_MESSAGES`` permission. + :obj:`hikari.errors.NotFoundHTTPError` + If the channel does not exist. + :obj:`hikari.errors.BadRequestHTTPError` + If the arguments provided are not valid (e.g. negative age, etc). + If any invalid snowflake IDs are passed; a snowflake may be invalid + due to it being outside of the range of UINT64. + """ + payload = await self._session.create_channel_invite( + channel_id=str(channel.id if isinstance(channel, snowflakes.UniqueEntity) else int(channel)), + max_age=int(max_age.total_seconds()) if isinstance(max_age, datetime.timedelta) else max_age, + max_uses=max_uses, + temporary=temporary, + unique=unique, + target_user=( + str(target_user.id if isinstance(target_user, snowflakes.UniqueEntity) else int(target_user)) + if target_user is not ... + else ... + ), + target_user_type=target_user_type, + reason=reason, + ) + return invites.InviteWithMetadata.deserialize(payload) + + async def delete_channel_overwrite( + self, + channel: snowflakes.HashableT[_channels.Channel], + overwrite: typing.Union[_channels.PermissionOverwrite, guilds.GuildRole, users.User, snowflakes.Snowflake, int], + ) -> None: + """Delete a channel permission overwrite for a user or a role. + + Parameters + ---------- + channel : :obj:`typing.Union` [ :obj:`hikari.channels.Channel`, :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ] + The object or ID of the channel to delete the overwrite from. + overwrite : :obj:`typing.Union` [ :obj:`hikari.channels.PermissionOverwrite`, :obj:`hikari.guilds.GuildRole`, :obj:`hikari.users.User`, :obj:`hikari.snowflakes.Snowflake`, :obj:int ] + The ID of the entity this overwrite targets. + + Raises + ------ + :obj:`hikari.errors.BadRequestHTTPError` + If any invalid snowflake IDs are passed; a snowflake may be invalid + due to it being outside of the range of UINT64. + :obj:`hikari.errors.NotFoundHTTPError` + If the overwrite or channel do not exist. + :obj:`hikari.errors.ForbiddenHTTPError` + If you lack the ``MANAGE_ROLES`` permission for that channel. + """ + await self._session.delete_channel_permission( + channel_id=str(channel.id if isinstance(channel, snowflakes.UniqueEntity) else int(channel)), + overwrite_id=str(overwrite.id if isinstance(overwrite, snowflakes.UniqueEntity) else int(overwrite)), + ) + + async def trigger_typing(self, channel: snowflakes.HashableT[_channels.Channel]) -> None: + """Trigger the typing indicator for ``10`` seconds in a channel. + + Parameters + ---------- + channel : :obj:`typing.Union` [ :obj:`hikari.channels.Channel`, :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ] + The object or ID of the channel to appear to be typing in. + + Raises + ------ + :obj:`hikari.errors.BadRequestHTTPError` + If any invalid snowflake IDs are passed; a snowflake may be invalid + due to it being outside of the range of UINT64. + :obj:`hikari.errors.NotFoundHTTPError` + If the channel is not found. + :obj:`hikari.errors.ForbiddenHTTPError` + If you are not able to type in the channel. + """ + await self._session.trigger_typing_indicator( + channel_id=str(channel.id if isinstance(channel, snowflakes.UniqueEntity) else int(channel)) + ) + + async def fetch_pins( + self, channel: snowflakes.HashableT[_channels.Channel] + ) -> typing.Mapping[snowflakes.Snowflake, _messages.Message]: + """Get pinned messages for a given channel. + + Parameters + ---------- + channel : :obj:`typing.Union` [ :obj:`hikari.channels.Channel`, :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ] + The object or ID of the channel to get messages from. + + Returns + ------- + :obj:`typing.Mapping` [ :obj:`hikari.snowflakes.Snowflake`, :obj:`hikari.messages.Message` ] + A list of message objects. + + Raises + ------ + :obj:`hikari.errors.BadRequestHTTPError` + If any invalid snowflake IDs are passed; a snowflake may be invalid + due to it being outside of the range of UINT64. + :obj:`hikari.errors.NotFoundHTTPError` + If the channel is not found. + :obj:`hikari.errors.ForbiddenHTTPError` + If you are not able to see the channel. + + Note + ---- + If you are not able to see the pinned message (eg. you are missing + ``READ_MESSAGE_HISTORY`` and the pinned message is an old message), it + will not be returned. + """ + payload = await self._session.get_pinned_messages( + channel_id=str(channel.id if isinstance(channel, snowflakes.UniqueEntity) else int(channel)) + ) + return {message.id: message for message in map(_messages.Message.deserialize, payload)} + + async def pin_message( + self, channel: snowflakes.HashableT[_channels.Channel], message: snowflakes.HashableT[_messages.Message], + ) -> None: + """Add a pinned message to the channel. + + Parameters + ---------- + channel : :obj:`typing.Union` [ :obj:`hikari.channels.Channel`, :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ] + The object or ID of the channel to pin a message to. + message : :obj:`typing.Union` [ :obj:`hikari.messages.Message`, :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ] + The object or ID of the message to pin. + + Raises + ------ + :obj:`hikari.errors.BadRequestHTTPError` + If any invalid snowflake IDs are passed; a snowflake may be invalid + due to it being outside of the range of UINT64. + :obj:`hikari.errors.ForbiddenHTTPError` + If you lack the ``MANAGE_MESSAGES`` permission. + :obj:`hikari.errors.NotFoundHTTPError` + If the message or channel do not exist. + """ + await self._session.add_pinned_channel_message( + channel_id=str(channel.id if isinstance(channel, snowflakes.UniqueEntity) else int(channel)), + message_id=str(message.id if isinstance(message, snowflakes.UniqueEntity) else int(message)), + ) + + async def unpin_message( + self, channel: snowflakes.HashableT[_channels.Channel], message: snowflakes.HashableT[_messages.Message], + ) -> None: + """Remove a pinned message from the channel. + + This will only unpin the message, not delete it. + + Parameters + ---------- + channel : :obj:`typing.Union` [ :obj:`hikari.channels.Channel`, :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ] + The ID of the channel to remove a pin from. + message : :obj:`typing.Union` [ :obj:`hikari.messages.Message`, :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ] + The object or ID of the message to unpin. + + Raises + ------ + :obj:`hikari.errors.BadRequestHTTPError` + If any invalid snowflake IDs are passed; a snowflake may be invalid + due to it being outside of the range of UINT64. + :obj:`hikari.errors.ForbiddenHTTPError` + If you lack the ``MANAGE_MESSAGES`` permission. + :obj:`hikari.errors.NotFoundHTTPError` + If the message or channel do not exist. + """ + await self._session.delete_pinned_channel_message( + channel_id=str(channel.id if isinstance(channel, snowflakes.UniqueEntity) else int(channel)), + message_id=str(message.id if isinstance(message, snowflakes.UniqueEntity) else int(message)), + ) + + async def fetch_guild_emoji( + self, guild: snowflakes.HashableT[guilds.Guild], emoji: snowflakes.HashableT[emojis.GuildEmoji], + ) -> emojis.GuildEmoji: + """Get an updated emoji object from a specific guild. + + Parameters + ---------- + guild : :obj:`typing.Union` [ :obj:`hikari.guilds.Guild`, :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ] + The object or ID of the guild to get the emoji from. + emoji : :obj:`typing.Union` [ :obj:`hikari.emojis.GuildEmoji`, :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ] + The object or ID of the emoji to get. + + Returns + ------- + :obj:`hikari.emojis.GuildEmoji` + A guild emoji object. + + Raises + ------ + :obj:`hikari.errors.BadRequestHTTPError` + If any invalid snowflake IDs are passed; a snowflake may be invalid + due to it being outside of the range of UINT64. + :obj:`hikari.errors.NotFoundHTTPError` + If either the guild or the emoji aren't found. + :obj:`hikari.errors.ForbiddenHTTPError` + If you aren't a member of said guild. + """ + payload = await self._session.get_guild_emoji( + guild_id=str(guild.id if isinstance(guild, snowflakes.UniqueEntity) else int(guild)), + emoji_id=str(emoji.id if isinstance(emoji, snowflakes.UniqueEntity) else int(emoji)), + ) + return emojis.GuildEmoji.deserialize(payload) + + async def fetch_guild_emojis(self, guild: snowflakes.HashableT[guilds.Guild]) -> typing.Sequence[emojis.GuildEmoji]: + """Get emojis for a given guild object or ID. + + Parameters + ---------- + guild : :obj:`typing.Union` [ :obj:`hikari.guilds.Guild`, :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ] + The object or ID of the guild to get the emojis for. + + Returns + ------- + :obj:`typing.Sequence` [ :obj:`hikari.emojis.GuildEmoji` ] + A list of guild emoji objects. + + Raises + ------ + :obj:`hikari.errors.BadRequestHTTPError` + If any invalid snowflake IDs are passed; a snowflake may be invalid + due to it being outside of the range of UINT64. + :obj:`hikari.errors.NotFoundHTTPError` + If the guild is not found. + :obj:`hikari.errors.ForbiddenHTTPError` + If you aren't a member of the guild. + """ + payload = await self._session.list_guild_emojis( + guild_id=str(guild.id if isinstance(guild, snowflakes.UniqueEntity) else int(guild)) + ) + return [emojis.GuildEmoji.deserialize(emoji) for emoji in payload] + + async def create_guild_emoji( + self, + guild: snowflakes.HashableT[guilds.GuildRole], + name: str, + image_data: conversions.FileLikeT, + *, + roles: typing.Sequence[snowflakes.HashableT[guilds.GuildRole]] = ..., + reason: str = ..., + ) -> emojis.GuildEmoji: + """Create a new emoji for a given guild. + + Parameters + ---------- + guild : :obj:`typing.Union` [ :obj:`hikari.guilds.GuildRole`, :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ] + The object or ID of the guild to create the emoji in. + name : :obj:`str` + The new emoji's name. + image_data : :obj:`hikari.internal.conversions.FileLikeT` + The ``128x128`` image data. + roles : :obj:`typing.Sequence` [ :obj:`typing.Union` [ :obj:`hikari.guilds.GuildRole`, :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ] ] + If specified, a list of role objects or IDs for which the emoji + will be whitelisted. If empty, all roles are whitelisted. + reason : :obj:`str` + If specified, the audit log reason explaining why the operation + was performed. + + Returns + ------- + :obj:`hikari.emojis.GuildEmoji` + The newly created emoji object. + + Raises + ------ + :obj:`ValueError` + If ``image`` is :obj:`None`. + :obj:`hikari.errors.NotFoundHTTPError` + If the guild is not found. + :obj:`hikari.errors.ForbiddenHTTPError` + If you either lack the ``MANAGE_EMOJIS`` permission or aren't a + member of said guild. + :obj:`hikari.errors.BadRequestHTTPError` + If you attempt to upload an image larger than ``256kb``, an empty + image or an invalid image format. + If any invalid snowflake IDs are passed; a snowflake may be invalid + due to it being outside of the range of UINT64. + """ + payload = await self._session.create_guild_emoji( + guild_id=str(guild.id if isinstance(guild, snowflakes.UniqueEntity) else int(guild)), + name=name, + image=conversions.get_bytes_from_resource(image_data), + roles=[str(role.id if isinstance(role, snowflakes.UniqueEntity) else int(role)) for role in roles] + if roles is not ... + else ..., + reason=reason, + ) + return emojis.GuildEmoji.deserialize(payload) + + async def update_guild_emoji( + self, + guild: snowflakes.HashableT[guilds.Guild], + emoji: snowflakes.HashableT[emojis.GuildEmoji], + *, + name: str = ..., + roles: typing.Sequence[snowflakes.HashableT[guilds.GuildRole]] = ..., + reason: str = ..., + ) -> emojis.GuildEmoji: + """Edits an emoji of a given guild. + + Parameters + ---------- + guild : :obj:`typing.Union` [ :obj:`hikari.guilds.Guild`, :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ] + The object or ID of the guild to which the emoji to edit belongs to. + emoji : :obj:`typing.Union` [ :obj:`hikari.emojis.GuildEmoji`, :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ] + The object or ID of the emoji to edit. + name : :obj:`str` + If specified, a new emoji name string. Keep unspecified to leave the + name unchanged. + roles : :obj:`typing.Sequence` [ :obj:`typing.Union` [ :obj:`hikari.guilds.GuildRole`, :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ] ] + If specified, a list of objects or IDs for the new whitelisted + roles. Set to an empty list to whitelist all roles. + Keep unspecified to leave the same roles already set. + reason : :obj:`str` + If specified, the audit log reason explaining why the operation + was performed. + + Returns + ------- + :obj:`hikari.emojis.GuildEmoji` + The updated emoji object. + + Raises + ------ + :obj:`hikari.errors.BadRequestHTTPError` + If any invalid snowflake IDs are passed; a snowflake may be invalid + due to it being outside of the range of UINT64. + :obj:`hikari.errors.NotFoundHTTPError` + If either the guild or the emoji aren't found. + :obj:`hikari.errors.ForbiddenHTTPError` + If you either lack the ``MANAGE_EMOJIS`` permission or are not a + member of the given guild. + """ + payload = await self._session.modify_guild_emoji( + guild_id=str(guild.id if isinstance(guild, snowflakes.UniqueEntity) else int(guild)), + emoji_id=str(emoji.id if isinstance(emoji, snowflakes.UniqueEntity) else int(emoji)), + name=name, + roles=[str(role.id if isinstance(role, snowflakes.UniqueEntity) else int(role)) for role in roles] + if roles is not ... + else ..., + reason=reason, + ) + return emojis.GuildEmoji.deserialize(payload) + + async def delete_guild_emoji( + self, guild: snowflakes.HashableT[guilds.Guild], emoji: snowflakes.HashableT[emojis.GuildEmoji], + ) -> None: + """Delete an emoji from a given guild. + + Parameters + ---------- + guild : :obj:`typing.Union` [ :obj:`hikari.guilds.Guild`, :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ] + The object or ID of the guild to delete the emoji from. + emoji : :obj:`typing.Union` [ :obj:`hikari.emojis.GuildEmoji`, :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ] + The object or ID of the guild emoji to be deleted. + + Raises + ------ + :obj:`hikari.errors.BadRequestHTTPError` + If any invalid snowflake IDs are passed; a snowflake may be invalid + due to it being outside of the range of UINT64. + :obj:`hikari.errors.NotFoundHTTPError` + If either the guild or the emoji aren't found. + :obj:`hikari.errors.ForbiddenHTTPError` + If you either lack the ``MANAGE_EMOJIS`` permission or aren't a + member of said guild. + """ + await self._session.delete_guild_emoji( + guild_id=str(guild.id if isinstance(guild, snowflakes.UniqueEntity) else int(guild)), + emoji_id=str(emoji.id if isinstance(emoji, snowflakes.UniqueEntity) else int(emoji)), + ) + + async def create_guild( + self, + name: str, + *, + region: typing.Union[voices.VoiceRegion, str] = ..., + icon_data: conversions.FileLikeT = ..., + verification_level: typing.Union[guilds.GuildVerificationLevel, int] = ..., + default_message_notifications: typing.Union[guilds.GuildMessageNotificationsLevel, int] = ..., + explicit_content_filter: typing.Union[guilds.GuildExplicitContentFilterLevel, int] = ..., + roles: typing.Sequence[guilds.GuildRole] = ..., + channels: typing.Sequence[_channels.GuildChannel] = ..., + ) -> guilds.Guild: + """Create a new guild. + + Warning + ------- + Can only be used by bots in less than ``10`` guilds. + + Parameters + ---------- + name : :obj:`str` + The name string for the new guild (``2-100`` characters). + region : :obj:`str` + If specified, the voice region ID for new guild. You can use + :meth:`fetch_guild_voice_regions` to see which region IDs are + available. + icon_data : :obj:`hikari.internal.conversions.FileLikeT` + If specified, the guild icon image data. + verification_level : :obj:`typing.Union` [ :obj:`hikari.guilds.GuildVerificationLevel`, :obj:`int` ] + If specified, the verification level. Passing a raw int for this + may lead to unexpected behaviour. + default_message_notifications : :obj:`typing.Union` [ :obj:`hikari.guilds.GuildMessageNotificationsLevel`, :obj:`int` ] + If specified, the default notification level. Passing a raw int for + this may lead to unexpected behaviour. + explicit_content_filter : :obj:`typing.Union` [ :obj:`hikari.guilds.GuildExplicitContentFilterLevel`, :obj:`int` ] + If specified, the explicit content filter. Passing a raw int for + this may lead to unexpected behaviour. + roles : :obj:`typing.Sequence` [ :obj:`hikari.guilds.GuildRole` ] + If specified, an array of role objects to be created alongside the + guild. First element changes the ``@everyone`` role. + channels : :obj:`typing.Sequence` [ :obj:`hikari.channels.GuildChannel` ] + If specified, an array of guild channel derived objects to be + created within the guild. + + Returns + ------- + :obj:`hikari.guilds.Guild` + The newly created guild object. + + Raises + ------ + :obj:`hikari.errors.ForbiddenHTTPError` + If you are in ``10`` or more guilds. + :obj:`hikari.errors.BadRequestHTTPError` + If you provide unsupported fields like ``parent_id`` in channel + objects. + If any invalid snowflake IDs are passed; a snowflake may be invalid + due to it being outside of the range of UINT64. + """ + payload = await self._session.create_guild( + name=name, + region=getattr(region, "id", region), + icon=conversions.get_bytes_from_resource(icon_data), + verification_level=verification_level, + default_message_notifications=default_message_notifications, + explicit_content_filter=explicit_content_filter, + roles=[role.serialize() for role in roles] if roles is not ... else ..., + channels=[channel.serialize() for channel in channels] if channels is not ... else ..., + ) + return guilds.Guild.deserialize(payload) + + async def fetch_guild(self, guild: snowflakes.HashableT[guilds.Guild]) -> guilds.Guild: + """Get a given guild's object. + + Parameters + ---------- + guild : :obj:`typing.Union` [ :obj:`hikari.guilds.Guild`, :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ] + The object or ID of the guild to get. + + Returns + ------- + :obj:`hikari.guilds.Guild` + The requested guild object. + + Raises + ------ + :obj:`hikari.errors.BadRequestHTTPError` + If any invalid snowflake IDs are passed; a snowflake may be invalid + due to it being outside of the range of UINT64. + :obj:`hikari.errors.NotFoundHTTPError` + If the guild is not found. + :obj:`hikari.errors.ForbiddenHTTPError` + If you don't have access to the guild. + """ + payload = await self._session.get_guild( + guild_id=str(guild.id if isinstance(guild, snowflakes.UniqueEntity) else int(guild)) + ) + return guilds.Guild.deserialize(payload) + + async def update_guild( + self, + guild: snowflakes.HashableT[guilds.Guild], + *, + name: str = ..., + region: typing.Union[voices.VoiceRegion, str] = ..., + verification_level: typing.Union[guilds.GuildVerificationLevel, int] = ..., + default_message_notifications: typing.Union[guilds.GuildMessageNotificationsLevel, int] = ..., + explicit_content_filter: typing.Union[guilds.GuildExplicitContentFilterLevel, int] = ..., + afk_channel: snowflakes.HashableT[_channels.GuildVoiceChannel] = ..., + afk_timeout: typing.Union[datetime.timedelta, int] = ..., + icon_data: conversions.FileLikeT = ..., + owner: snowflakes.HashableT[users.User] = ..., + splash_data: conversions.FileLikeT = ..., + system_channel: snowflakes.HashableT[_channels.Channel] = ..., + reason: str = ..., + ) -> guilds.Guild: + """Edit a given guild. + + Parameters + ---------- + guild : :obj:`typing.Union` [ :obj:`hikari.guilds.Guild`, :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ] + The object or ID of the guild to be edited. + name : :obj:`str` + If specified, the new name string for the guild (``2-100`` characters). + region : :obj:`str` + If specified, the new voice region ID for guild. You can use + :meth:`fetch_guild_voice_regions` to see which region IDs are + available. + verification_level : :obj:`typing.Union` [ :obj:`hikari.guilds.GuildVerificationLevel`, :obj:`int` ] + If specified, the new verification level. Passing a raw int for this + may lead to unexpected behaviour. + default_message_notifications : :obj:`typing.Union` [ :obj:`hikari.guilds.GuildMessageNotificationsLevel`, :obj:`int` ] + If specified, the new default notification level. Passing a raw int + for this may lead to unexpected behaviour. + explicit_content_filter : :obj:`typing.Union` [ :obj:`hikari.guilds.GuildExplicitContentFilterLevel`, :obj:`int` ] + If specified, the new explicit content filter. Passing a raw int for + this may lead to unexpected behaviour. + afk_channel : :obj:`typing.Union` [ :obj:`hikari.channels.GuildVoiceChannel`, :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ] + If specified, the object or ID for the new AFK voice channel. + afk_timeout : :obj:`typing.Union` [ :obj:`datetime.timedelta`, :obj:`int` ] + If specified, the new AFK timeout seconds timedelta. + icon_data : :obj:`hikari.internal.conversions.FileLikeT` + If specified, the new guild icon image file data. + owner : :obj:`typing.Union` [ :obj:`hikari.users.User`, :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ] + If specified, the object or ID of the new guild owner. + splash_data : :obj:`hikari.internal.conversions.FileLikeT` + If specified, the new new splash image file data. + system_channel : :obj:`typing.Union` [ :obj:`hikari.channels.GuildVoiceChannel`, :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ] + If specified, the object or ID of the new system channel. + reason : :obj:`str` + If specified, the audit log reason explaining why the operation + was performed. + + Returns + ------- + :obj:`hikari.guilds.Guild` + The edited guild object. + + Raises + ------ + :obj:`hikari.errors.BadRequestHTTPError` + If any invalid snowflake IDs are passed; a snowflake may be invalid + due to it being outside of the range of UINT64. + :obj:`hikari.errors.NotFoundHTTPError` + If the guild is not found. + :obj:`hikari.errors.ForbiddenHTTPError` + If you lack the ``MANAGE_GUILD`` permission or are not in the guild. + """ + payload = await self._session.modify_guild( + guild_id=str(guild.id if isinstance(guild, snowflakes.UniqueEntity) else int(guild)), + name=name, + region=getattr(region, "id", region) if region is not ... else ..., + verification_level=verification_level, + default_message_notifications=default_message_notifications, + explicit_content_filter=explicit_content_filter, + afk_timeout=afk_timeout.total_seconds() if isinstance(afk_timeout, datetime.timedelta) else afk_timeout, + afk_channel_id=( + str(afk_channel.id if isinstance(afk_channel, snowflakes.UniqueEntity) else int(afk_channel)) + if afk_channel is not ... + else ... + ), + icon=conversions.get_bytes_from_resource(icon_data) if icon_data is not ... else ..., + owner_id=( + str(owner.id if isinstance(owner, snowflakes.UniqueEntity) else int(owner)) if owner is not ... else ... + ), + splash=conversions.get_bytes_from_resource(splash_data) if splash_data is not ... else ..., + system_channel_id=( + str(system_channel.id if isinstance(system_channel, snowflakes.UniqueEntity) else int(system_channel)) + if system_channel is not ... + else ... + ), + reason=reason, + ) + return guilds.Guild.deserialize(payload) + + async def delete_guild(self, guild: snowflakes.HashableT[guilds.Guild]) -> None: + """Permanently deletes the given guild. + + You must be owner of the guild to perform this action. + + Parameters + ---------- + guild : :obj:`typing.Union` [ :obj:`hikari.guilds.Guild`, :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ] + The object or ID of the guild to be deleted. + + Raises + ------ + :obj:`hikari.errors.BadRequestHTTPError` + If any invalid snowflake IDs are passed; a snowflake may be invalid + due to it being outside of the range of UINT64. + :obj:`hikari.errors.NotFoundHTTPError` + If the guild is not found. + :obj:`hikari.errors.ForbiddenHTTPError` + If you are not the guild owner. + """ + await self._session.delete_guild( + guild_id=str(guild.id if isinstance(guild, snowflakes.UniqueEntity) else int(guild)) + ) + + async def fetch_guild_channels( + self, guild: snowflakes.HashableT[guilds.Guild] + ) -> typing.Sequence[_channels.GuildChannel]: + """Get all the channels for a given guild. + + Parameters + ---------- + guild : :obj:`typing.Union` [ :obj:`hikari.guilds.Guild`, :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ] + The object or ID of the guild to get the channels from. + + Returns + ------- + :obj:`typing.Sequence` [ :obj:`hikari.channels.GuildChannel` ] + A list of guild channel objects. + + Raises + ------ + :obj:`hikari.errors.BadRequestHTTPError` + If any invalid snowflake IDs are passed; a snowflake may be invalid + due to it being outside of the range of UINT64. + :obj:`hikari.errors.NotFoundHTTPError` + If the guild is not found. + :obj:`hikari.errors.ForbiddenHTTPError` + If you are not in the guild. + """ + payload = await self._session.list_guild_channels( + guild_id=str(guild.id if isinstance(guild, snowflakes.UniqueEntity) else int(guild)) + ) + return [_channels.deserialize_channel(channel) for channel in payload] + + async def create_guild_channel( + self, + guild: snowflakes.HashableT[guilds.Guild], + name: str, + channel_type: typing.Union[_channels.ChannelType, int] = ..., + position: int = ..., + topic: str = ..., + nsfw: bool = ..., + rate_limit_per_user: typing.Union[datetime.timedelta, int] = ..., + bitrate: int = ..., + user_limit: int = ..., + permission_overwrites: typing.Sequence[_channels.PermissionOverwrite] = ..., + parent_category: snowflakes.HashableT[_channels.GuildCategory] = ..., + reason: str = ..., + ) -> _channels.GuildChannel: + """Create a channel in a given guild. + + Parameters + ---------- + guild : :obj:`typing.Union` [ :obj:`hikari.guilds.Guild`, :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ] + The object or ID of the guild to create the channel in. + name : :obj:`str` + If specified, the name for the channel. This must be + inclusively between ``1` and ``100`` characters in length. + channel_type: :obj:`typing.Union` [ :obj:`hikari.channels.ChannelType`, :obj:`int` ] + If specified, the channel type, passing through a raw integer here + may lead to unexpected behaviour. + position : :obj:`int` + If specified, the position to change the channel to. + topic : :obj:`str` + If specified, the topic to set. This is only applicable to + text channels. This must be inclusively between ``0`` and ``1024`` + characters in length. + nsfw : :obj:`bool` + If specified, whether the channel will be marked as NSFW. + Only applicable for text channels. + rate_limit_per_user : :obj:`typing.Union` [ :obj:`datetime.timedelta`, :obj:`int` ] + If specified, the second time delta the user has to wait before + sending another message. This will not apply to bots, or to + members with ``MANAGE_MESSAGES`` or ``MANAGE_CHANNEL`` permissions. + This must be inclusively between ``0`` and ``21600`` seconds. + bitrate : :obj:`int` + If specified, the bitrate in bits per second allowable for the + channel. This only applies to voice channels and must be inclusively + between ``8000`` and ``96000`` for normal servers or ``8000`` and + ``128000`` for VIP servers. + user_limit : :obj:`int` + If specified, the max number of users to allow in a voice channel. + This must be between ``0`` and ``99`` inclusive, where + ``0`` implies no limit. + permission_overwrites : :obj:`typing.Sequence` [ :obj:`hikari.channels.PermissionOverwrite` ] + If specified, the list of permission overwrite objects that are + category specific to replace the existing overwrites with. + parent_category : :obj:`typing.Union` [ :obj:`hikari.channels.GuildCategory`, :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ] + If specified, the object or ID of the parent category to set for + the channel. + reason : :obj:`str` + If specified, the audit log reason explaining why the operation + was performed. + + Returns + ------- + :obj:`hikari.channels.GuildChannel` + The newly created channel object. + + Raises + ------ + :obj:`hikari.errors.NotFoundHTTPError` + If the guild is not found. + :obj:`hikari.errors.ForbiddenHTTPError` + If you lack the ``MANAGE_CHANNEL`` permission or are not in the + guild. + :obj:`hikari.errors.BadRequestHTTPError` + If you provide incorrect options for the corresponding channel type + (e.g. a ``bitrate`` for a text channel). + If any invalid snowflake IDs are passed; a snowflake may be invalid + due to it being outside of the range of UINT64. + """ + payload = await self._session.create_guild_channel( + guild_id=str(guild.id if isinstance(guild, snowflakes.UniqueEntity) else int(guild)), + name=name, + type_=channel_type, + position=position, + topic=topic, + nsfw=nsfw, + rate_limit_per_user=( + int(rate_limit_per_user.total_seconds()) + if isinstance(rate_limit_per_user, datetime.timedelta) + else rate_limit_per_user + ), + bitrate=bitrate, + user_limit=user_limit, + permission_overwrites=( + [po.serialize() for po in permission_overwrites] if permission_overwrites is not ... else ... + ), + parent_id=( + str( + parent_category.id if isinstance(parent_category, snowflakes.UniqueEntity) else int(parent_category) + ) + if parent_category is not ... + else ... + ), + reason=reason, + ) + return _channels.deserialize_channel(payload) + + async def reposition_guild_channels( + self, + guild: snowflakes.HashableT[guilds.Guild], + channel: typing.Tuple[int, snowflakes.HashableT[_channels.GuildChannel]], + *additional_channels: typing.Tuple[int, snowflakes.HashableT[_channels.GuildChannel]], + ) -> None: + """Edits the position of one or more given channels. + + Parameters + ---------- + guild : :obj:`typing.Union` [ :obj:`hikari.guilds.Guild`, :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ] + The object or ID of the guild in which to edit the channels. + channel : :obj:`typing.Tuple` [ :obj:`int` , :obj:`typing.Union` [ :obj:`hikari.channels.GuildChannel`, :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ] ] + The first channel to change the position of. This is a tuple of the + integer position the channel object or ID. + *additional_channels : :obj:`typing.Tuple` [ :obj:`int`, :obj:`typing.Union` [ :obj:`hikari.channels.GuildChannel`, :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ] ] + Optional additional channels to change the position of. These must + be tuples of integer positions to change to and the channel object + or ID and the. + + Raises + ------ + :obj:`hikari.errors.NotFoundHTTPError` + If either the guild or any of the channels aren't found. + :obj:`hikari.errors.ForbiddenHTTPError` + If you either lack the ``MANAGE_CHANNELS`` permission or are not a + member of said guild or are not in the guild. + :obj:`hikari.errors.BadRequestHTTPError` + If you provide anything other than the ``id`` and ``position`` + fields for the channels. + If any invalid snowflake IDs are passed; a snowflake may be invalid + due to it being outside of the range of UINT64. + """ + await self._session.modify_guild_channel_positions( + str(guild.id if isinstance(guild, snowflakes.UniqueEntity) else int(guild)), + *[ + (str(channel.id if isinstance(channel, snowflakes.UniqueEntity) else int(channel)), position) + for position, channel in [channel, *additional_channels] + ], + ) + + async def fetch_member( + self, guild: snowflakes.HashableT[guilds.Guild], user: snowflakes.HashableT[users.User], + ) -> guilds.GuildMember: + """Get a given guild member. + + Parameters + ---------- + guild : :obj:`typing.Union` [ :obj:`hikari.guilds.Guild`, :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ] + The object or ID of the guild to get the member from. + user : :obj:`typing.Union` [ :obj:`hikari.users.User`, :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ] + The object or ID of the member to get. + + Returns + ------- + :obj:`hikari.guilds.GuildMember` + The requested member object. + + Raises + ------ + :obj:`hikari.errors.BadRequestHTTPError` + If any invalid snowflake IDs are passed; a snowflake may be invalid + due to it being outside of the range of UINT64. + :obj:`hikari.errors.NotFoundHTTPError` + If either the guild or the member aren't found. + :obj:`hikari.errors.ForbiddenHTTPError` + If you don't have access to the target guild. + """ + payload = await self._session.get_guild_member( + guild_id=str(guild.id if isinstance(guild, snowflakes.UniqueEntity) else int(guild)), + user_id=str(user.id if isinstance(user, snowflakes.UniqueEntity) else int(user)), + ) + return guilds.GuildMember.deserialize(payload) + + def fetch_members_after( + self, + guild: snowflakes.HashableT[guilds.Guild], + *, + after: typing.Union[datetime.datetime, snowflakes.HashableT[users.User]] = 0, + limit: typing.Optional[int] = None, + ) -> typing.AsyncIterator[guilds.GuildMember]: + """Get an async iterator of all the members in a given guild. + + This returns the member objects with a user object/ID that was created + after the given user object/ID or from the member object or the oldest + user. + + Parameters + ---------- + guild : :obj:`typing.Union` [ :obj:`hikari.guilds.Guild`, :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ] + The object or ID of the guild to get the members from. + limit : :obj:`int` + If specified, the maximum number of members this iterator + should return. + after : :obj:`typing.Union` [ :obj:`datetime.datetime`, :obj:`hikari.users.User`, :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ] + The object or ID of the user this iterator should start + after if specified, else this will start at the oldest user. + + Example + ------- + .. code-block:: python + + async for user in client.fetch_members_after(guild, after=9876543, limit=1231): + if member.user.username[0] in HOIST_BLACKLIST: + await client.update_member(member, nickname="💩") + + Returns + ------- + :obj:`typing.AsyncIterator` [ :obj:`hikari.guilds.GuildMember` ] + An async iterator of member objects. + + Raises + ------ + :obj:`hikari.errors.BadRequestHTTPError` + If any invalid snowflake IDs are passed; a snowflake may be invalid + due to it being outside of the range of UINT64. + :obj:`hikari.errors.NotFoundHTTPError` + If the guild is not found. + :obj:`hikari.errors.ForbiddenHTTPError` + If you are not in the guild. + """ + if isinstance(after, datetime.datetime): + after = str(snowflakes.Snowflake.from_datetime(after)) + else: + after = str(after.id if isinstance(after, snowflakes.UniqueEntity) else int(after)) + return self._pagination_handler( + guild_id=str(guild.id if isinstance(guild, snowflakes.UniqueEntity) else int(guild)), + deserializer=guilds.GuildMember.deserialize, + direction="after", + request=self._session.list_guild_members, + reversing=False, + start=after, + limit=limit, + id_getter=_get_member_id, + ) + + async def update_member( + self, + guild: snowflakes.HashableT[guilds.Guild], + user: snowflakes.HashableT[users.User], + nickname: typing.Optional[str] = ..., + roles: typing.Sequence[snowflakes.HashableT[guilds.GuildRole]] = ..., + mute: bool = ..., + deaf: bool = ..., + voice_channel: typing.Optional[snowflakes.HashableT[_channels.GuildVoiceChannel]] = ..., + reason: str = ..., + ) -> None: + """Edits a guild's member, any unspecified fields will not be changed. + + Parameters + ---------- + guild : :obj:`typing.Union` [ :obj:`hikari.guilds.Guild`, :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ] + The object or ID of the guild to edit the member from. + user : :obj:`typing.Union` [ :obj:`hikari.guilds.GuildMember`, :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ] + The object or ID of the member to edit. + nickname : :obj:`str`, optional + If specified, the new nickname string. Setting it to :obj:`None` + explicitly will clear the nickname. + roles : :obj:`typing.Sequence` [ :obj:`typing.Union` [ :obj:`hikari.guilds.GuildRole`, :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ] ] + If specified, a list of role IDs the member should have. + mute : :obj:`bool` + If specified, whether the user should be muted in the voice channel + or not. + deaf : :obj:`bool` + If specified, whether the user should be deafen in the voice + channel or not. + voice_channel : :obj:`typing.Union` [ :obj:`hikari.channels.GuildVoiceChannel`, :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ], optional + If specified, the ID of the channel to move the member to. Setting + it to :obj:`None` explicitly will disconnect the user. + reason : :obj:`str` + If specified, the audit log reason explaining why the operation + was performed. + + Raises + ------ + :obj:`hikari.errors.NotFoundHTTPError` + If either the guild, user, channel or any of the roles aren't found. + :obj:`hikari.errors.ForbiddenHTTPError` + If you lack any of the applicable permissions + (``MANAGE_NICKNAMES``, ``MANAGE_ROLES``, ``MUTE_MEMBERS``, ``DEAFEN_MEMBERS`` or ``MOVE_MEMBERS``). + Note that to move a member you must also have permission to connect + to the end channel. This will also be raised if you're not in the + guild. + :obj:`hikari.errors.BadRequestHTTPError` + If you pass ``mute``, ``deaf`` or ``channel_id`` while the member + is not connected to a voice channel. + If any invalid snowflake IDs are passed; a snowflake may be invalid + due to it being outside of the range of UINT64. + """ + await self._session.modify_guild_member( + guild_id=str(guild.id if isinstance(guild, snowflakes.UniqueEntity) else int(guild)), + user_id=str(user.id if isinstance(user, snowflakes.UniqueEntity) else int(user)), + nick=nickname, + roles=( + [str(role.id if isinstance(role, snowflakes.UniqueEntity) else int(role)) for role in roles] + if roles is not ... + else ... + ), + mute=mute, + deaf=deaf, + channel_id=( + str(voice_channel.id if isinstance(voice_channel, snowflakes.UniqueEntity) else int(voice_channel)) + if voice_channel is not ... + else ... + ), + reason=reason, + ) + + async def update_my_member_nickname( + self, guild: snowflakes.HashableT[guilds.Guild], nickname: typing.Optional[str], *, reason: str = ..., + ) -> None: + """Edits the current user's nickname for a given guild. + + Parameters + ---------- + guild : :obj:`typing.Union` [ :obj:`hikari.guilds.Guild`, :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ] + The object or ID of the guild you want to change the nick on. + nickname : :obj:`str`, optional + The new nick string. Setting this to `None` clears the nickname. + reason : :obj:`str` + If specified, the audit log reason explaining why the operation + was performed. + + Raises + ------ + :obj:`hikari.errors.NotFoundHTTPError` + If the guild is not found. + :obj:`hikari.errors.ForbiddenHTTPError` + If you lack the ``CHANGE_NICKNAME`` permission or are not in the + guild. + :obj:`hikari.errors.BadRequestHTTPError` + If you provide a disallowed nickname, one that is too long, or one + that is empty. + If any invalid snowflake IDs are passed; a snowflake may be invalid + due to it being outside of the range of UINT64. + """ + await self._session.modify_current_user_nick( + guild_id=str(guild.id if isinstance(guild, snowflakes.UniqueEntity) else int(guild)), + nick=nickname, + reason=reason, + ) + + async def add_role_to_member( + self, + guild: snowflakes.HashableT[guilds.Guild], + user: snowflakes.HashableT[users.User], + role: snowflakes.HashableT[guilds.GuildRole], + *, + reason: str = ..., + ) -> None: + """Add a role to a given member. + + Parameters + ---------- + guild : :obj:`typing.Union` [ :obj:`hikari.guilds.Guild`, :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ] + The object or ID of the guild the member belongs to. + user : :obj:`typing.Union` [ :obj:`hikari.users.User`, :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ] + The object or ID of the member you want to add the role to. + role : :obj:`typing.Union` [ :obj:`hikari.guilds.GuildRole`, :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ] + The object or ID of the role you want to add. + reason : :obj:`str` + If specified, the audit log reason explaining why the operation + was performed. + + Raises + ------ + :obj:`hikari.errors.BadRequestHTTPError` + If any invalid snowflake IDs are passed; a snowflake may be invalid + due to it being outside of the range of UINT64. + :obj:`hikari.errors.NotFoundHTTPError` + If either the guild, member or role aren't found. + :obj:`hikari.errors.ForbiddenHTTPError` + If you lack the ``MANAGE_ROLES`` permission or are not in the guild. + """ + await self._session.add_guild_member_role( + guild_id=str(guild.id if isinstance(guild, snowflakes.UniqueEntity) else int(guild)), + user_id=str(user.id if isinstance(user, snowflakes.UniqueEntity) else int(user)), + role_id=str(role.id if isinstance(role, snowflakes.UniqueEntity) else int(role)), + reason=reason, + ) + + async def remove_role_from_member( + self, + guild: snowflakes.HashableT[guilds.Guild], + user: snowflakes.HashableT[users.User], + role: snowflakes.HashableT[guilds.GuildRole], + *, + reason: str = ..., + ) -> None: + """Remove a role from a given member. + + Parameters + ---------- + guild : :obj:`typing.Union` [ :obj:`hikari.guilds.Guild`, :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ] + The object or ID of the guild the member belongs to. + user : :obj:`typing.Union` [ :obj:`hikari.users.User`, :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ] + The object or ID of the member you want to remove the role from. + role : :obj:`typing.Union` [ :obj:`hikari.guilds.GuildRole`, :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ] + The object or ID of the role you want to remove. + reason : :obj:`str` + If specified, the audit log reason explaining why the operation + was performed. + + Raises + ------ + :obj:`hikari.errors.BadRequestHTTPError` + If any invalid snowflake IDs are passed; a snowflake may be invalid + due to it being outside of the range of UINT64. + :obj:`hikari.errors.NotFoundHTTPError` + If either the guild, member or role aren't found. + :obj:`hikari.errors.ForbiddenHTTPError` + If you lack the ``MANAGE_ROLES`` permission or are not in the guild. + """ + await self._session.remove_guild_member_role( + guild_id=str(guild.id if isinstance(guild, snowflakes.UniqueEntity) else int(guild)), + user_id=str(user.id if isinstance(user, snowflakes.UniqueEntity) else int(user)), + role_id=str(role.id if isinstance(role, snowflakes.UniqueEntity) else int(role)), + reason=reason, + ) + + async def kick_member( + self, guild: snowflakes.HashableT[guilds.Guild], user: snowflakes.HashableT[users.User], *, reason: str = ..., + ) -> None: + """Kicks a user from a given guild. + + Parameters + ---------- + guild : :obj:`typing.Union` [ :obj:`hikari.guilds.Guild`, :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ] + The object or ID of the guild the member belongs to. + user : :obj:`typing.Union` [ :obj:`hikari.users.User`, :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ] + The object or ID of the member you want to kick. + reason : :obj:`str` + If specified, the audit log reason explaining why the operation + was performed. + + Raises + ------ + :obj:`hikari.errors.BadRequestHTTPError` + If any invalid snowflake IDs are passed; a snowflake may be invalid + due to it being outside of the range of UINT64. + :obj:`hikari.errors.NotFoundHTTPError` + If either the guild or member aren't found. + :obj:`hikari.errors.ForbiddenHTTPError` + If you lack the ``KICK_MEMBERS`` permission or are not in the guild. + """ + await self._session.remove_guild_member( + guild_id=str(guild.id if isinstance(guild, snowflakes.UniqueEntity) else int(guild)), + user_id=str(user.id if isinstance(user, snowflakes.UniqueEntity) else int(user)), + reason=reason, + ) + + async def fetch_ban( + self, guild: snowflakes.HashableT[guilds.Guild], user: snowflakes.HashableT[users.User], + ) -> guilds.GuildMemberBan: + """Get a ban from a given guild. + + Parameters + ---------- + guild : :obj:`typing.Union` [ :obj:`hikari.guilds.Guild`, :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ] + The object or ID of the guild you want to get the ban from. + user : :obj:`typing.Union` [ :obj:`hikari.users.User`, :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ] + The object or ID of the user to get the ban information for. + + Returns + ------- + :obj:`hikari.guilds.GuildMemberBan` + A ban object for the requested user. + + Raises + ------ + :obj:`hikari.errors.BadRequestHTTPError` + If any invalid snowflake IDs are passed; a snowflake may be invalid + due to it being outside of the range of UINT64. + :obj:`hikari.errors.NotFoundHTTPError` + If either the guild or the user aren't found, or if the user is not + banned. + :obj:`hikari.errors.ForbiddenHTTPError` + If you lack the ``BAN_MEMBERS`` permission or are not in the guild. + """ + payload = await self._session.get_guild_ban( + guild_id=str(guild.id if isinstance(guild, snowflakes.UniqueEntity) else int(guild)), + user_id=str(user.id if isinstance(user, snowflakes.UniqueEntity) else int(user)), + ) + return guilds.GuildMemberBan.deserialize(payload) + + async def fetch_bans(self, guild: snowflakes.HashableT[guilds.Guild],) -> typing.Sequence[guilds.GuildMemberBan]: + """Get the bans for a given guild. + + Parameters + ---------- + guild : :obj:`typing.Union` [ :obj:`hikari.guilds.Guild`, :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ] + The object or ID of the guild you want to get the bans from. + + Returns + ------- + :obj:`typing.Sequence` [ :obj:`hikari.guilds.GuildMemberBan` ] + A list of ban objects. + + Raises + ------ + :obj:`hikari.errors.BadRequestHTTPError` + If any invalid snowflake IDs are passed; a snowflake may be invalid + due to it being outside of the range of UINT64. + :obj:`hikari.errors.NotFoundHTTPError` + If the guild is not found. + :obj:`hikari.errors.ForbiddenHTTPError` + If you lack the ``BAN_MEMBERS`` permission or are not in the guild. + """ + payload = await self._session.get_guild_bans( + guild_id=str(guild.id if isinstance(guild, snowflakes.UniqueEntity) else int(guild)) + ) + return [guilds.GuildMemberBan.deserialize(ban) for ban in payload] + + async def ban_member( + self, + guild: snowflakes.HashableT[guilds.Guild], + user: snowflakes.HashableT[users.User], + *, + delete_message_days: typing.Union[datetime.timedelta, int] = ..., + reason: str = ..., + ) -> None: + """Bans a user from a given guild. + + Parameters + ---------- + guild : :obj:`typing.Union` [ :obj:`hikari.guilds.Guild`, :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ] + The object or ID of the guild the member belongs to. + user : :obj:`typing.Union` [ :obj:`hikari.users.User`, :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ] + The object or ID of the member you want to ban. + delete_message_days : :obj:`typing.Union` [ :obj:`datetime.timedelta`, :obj:`int` ] + If specified, the tim delta of how many days of messages from the + user should be removed. + reason : :obj:`str` + If specified, the audit log reason explaining why the operation + was performed. + + Raises + ------ + :obj:`hikari.errors.BadRequestHTTPError` + If any invalid snowflake IDs are passed; a snowflake may be invalid + due to it being outside of the range of UINT64. + :obj:`hikari.errors.NotFoundHTTPError` + If either the guild or member aren't found. + :obj:`hikari.errors.ForbiddenHTTPError` + If you lack the ``BAN_MEMBERS`` permission or are not in the guild. + """ + await self._session.create_guild_ban( + guild_id=str(guild.id if isinstance(guild, snowflakes.UniqueEntity) else int(guild)), + user_id=str(user.id if isinstance(user, snowflakes.UniqueEntity) else int(user)), + delete_message_days=getattr(delete_message_days, "days", delete_message_days), + reason=reason, + ) + + async def unban_member( + self, guild: snowflakes.HashableT[guilds.Guild], user: snowflakes.HashableT[users.User], *, reason: str = ..., + ) -> None: + """Un-bans a user from a given guild. + + Parameters + ---------- + guild : :obj:`typing.Union` [ :obj:`hikari.guilds.Guild`, :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ] + The object or ID of the guild to un-ban the user from. + user : :obj:`typing.Union` [ :obj:`hikari.users.User`, :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ] + The ID of the user you want to un-ban. + reason : :obj:`str` + If specified, the audit log reason explaining why the operation + was performed. + + Raises + ------ + :obj:`hikari.errors.BadRequestHTTPError` + If any invalid snowflake IDs are passed; a snowflake may be invalid + due to it being outside of the range of UINT64. + :obj:`hikari.errors.NotFoundHTTPError` + If either the guild or member aren't found, or the member is not + banned. + :obj:`hikari.errors.ForbiddenHTTPError` + If you lack the ``BAN_MEMBERS`` permission or are not a in the + guild. + """ + await self._session.remove_guild_ban( + guild_id=str(guild.id if isinstance(guild, snowflakes.UniqueEntity) else int(guild)), + user_id=str(user.id if isinstance(user, snowflakes.UniqueEntity) else int(user)), + reason=reason, + ) + + async def fetch_roles( + self, guild: snowflakes.HashableT[guilds.Guild], + ) -> typing.Mapping[snowflakes.Snowflake, guilds.GuildRole]: + """Get the roles for a given guild. + + Parameters + ---------- + guild : :obj:`typing.Union` [ :obj:`hikari.guilds.Guild`, :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ] + The object or ID of the guild you want to get the roles from. + + Returns + ------- + :obj:`typing.Mapping` [ :obj:`hikari.snowflakes.Snowflake`, :obj:`hikari.guilds.GuildRole` ] + A list of role objects. + + Raises + ------ + :obj:`hikari.errors.BadRequestHTTPError` + If any invalid snowflake IDs are passed; a snowflake may be invalid + due to it being outside of the range of UINT64. + :obj:`hikari.errors.NotFoundHTTPError` + If the guild is not found. + :obj:`hikari.errors.ForbiddenHTTPError` + If you're not in the guild. + """ + payload = await self._session.get_guild_roles( + guild_id=str(guild.id if isinstance(guild, snowflakes.UniqueEntity) else int(guild)) + ) + return {role.id: role for role in map(guilds.GuildRole.deserialize, payload)} + + async def create_role( + self, + guild: snowflakes.HashableT[guilds.Guild], + *, + name: str = ..., + permissions: typing.Union[_permissions.Permission, int] = ..., + color: typing.Union[colors.Color, int] = ..., + hoist: bool = ..., + mentionable: bool = ..., + reason: str = ..., + ) -> guilds.GuildRole: + """Create a new role for a given guild. + + Parameters + ---------- + guild : :obj:`typing.Union` [ :obj:`hikari.guilds.Guild`, :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ] + The object or ID of the guild you want to create the role on. + name : :obj:`str` + If specified, the new role name string. + permissions : :obj:`typing.Union` [ :obj:`hikari.permissions.Permission`, :obj:`int` ] + If specified, the permissions integer for the role, passing a raw + integer rather than the int flag may lead to unexpected results. + color : :obj:`typing.Union` [ :obj:`hikari.colors.Color`, :obj:`int` ] + If specified, the color for the role. + hoist : :obj:`bool` + If specified, whether the role will be hoisted. + mentionable : :obj:`bool` + If specified, whether the role will be able to be mentioned by any + user. + reason : :obj:`str` + If specified, the audit log reason explaining why the operation + was performed. + + Returns + ------- + :obj:`hikari.guilds.GuildRole` + The newly created role object. + + Raises + ------ + :obj:`hikari.errors.NotFoundHTTPError` + If the guild is not found. + :obj:`hikari.errors.ForbiddenHTTPError` + If you lack the ``MANAGE_ROLES`` permission or you're not in the + guild. + :obj:`hikari.errors.BadRequestHTTPError` + If you provide invalid values for the role attributes. + If any invalid snowflake IDs are passed; a snowflake may be invalid + due to it being outside of the range of UINT64. + """ + payload = await self._session.create_guild_role( + guild_id=str(guild.id if isinstance(guild, snowflakes.UniqueEntity) else int(guild)), + name=name, + permissions=permissions, + color=color, + hoist=hoist, + mentionable=mentionable, + reason=reason, + ) + return guilds.GuildRole.deserialize(payload) + + async def reposition_roles( + self, + guild: snowflakes.HashableT[guilds.Guild], + role: typing.Tuple[int, snowflakes.HashableT[guilds.GuildRole]], + *additional_roles: typing.Tuple[int, snowflakes.HashableT[guilds.GuildRole]], + ) -> typing.Sequence[guilds.GuildRole]: + """Edits the position of two or more roles in a given guild. + + Parameters + ---------- + guild : :obj:`typing.Union` [ :obj:`hikari.guilds.Guild`, :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ] + The ID of the guild the roles belong to. + role : :obj:`typing.Tuple` [ :obj:`int`, :obj:`typing.Union` [ :obj:`hikari.guilds.GuildRole`, :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ] ] + The first role to move. This is a tuple of the integer position and + the role object or ID. + *additional_roles : :obj:`typing.Tuple` [ :obj:`int`, :obj:`typing.Union` [ :obj:`hikari.guilds.GuildRole`, :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ] ] + Optional extra roles to move. These must be tuples of the integer + position and the role object or ID. + + Returns + ------- + :obj:`typing.Sequence` [ :obj:`hikari.guilds.GuildRole` ] + A list of all the guild roles. + + Raises + ------ + :obj:`hikari.errors.NotFoundHTTPError` + If either the guild or any of the roles aren't found. + :obj:`hikari.errors.ForbiddenHTTPError` + If you lack the ``MANAGE_ROLES`` permission or you're not in the + guild. + :obj:`hikari.errors.BadRequestHTTPError` + If you provide invalid values for the `position` fields. + If any invalid snowflake IDs are passed; a snowflake may be invalid + due to it being outside of the range of UINT64. + """ + payload = await self._session.modify_guild_role_positions( + str(guild.id if isinstance(guild, snowflakes.UniqueEntity) else int(guild)), + *[ + (str(channel.id if isinstance(channel, snowflakes.UniqueEntity) else int(channel)), position) + for position, channel in [role, *additional_roles] + ], + ) + return [guilds.GuildRole.deserialize(role) for role in payload] + + async def update_role( + self, + guild: snowflakes.HashableT[guilds.Guild], + role: snowflakes.HashableT[guilds.GuildRole], + *, + name: str = ..., + permissions: typing.Union[_permissions.Permission, int] = ..., + color: typing.Union[colors.Color, int] = ..., + hoist: bool = ..., + mentionable: bool = ..., + reason: str = ..., + ) -> guilds.GuildRole: + """Edits a role in a given guild. + + Parameters + ---------- + guild : :obj:`typing.Union` [ :obj:`hikari.guilds.Guild`, :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ] + The object or ID of the guild the role belong to. + role : :obj:`typing.Union` [ :obj:`hikari.guilds.GuildRole`, :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ] + The object or ID of the role you want to edit. + name : :obj:`str` + If specified, the new role's name string. + permissions : :obj:`typing.Union` [ :obj:`hikari.permissions.Permission`, :obj:`int` ] + If specified, the new permissions integer for the role, passing a + raw integer for this may lead to unexpected behaviour. + color : :obj:`typing.Union` [ :obj:`hikari.colors.Color`, :obj:`int` ] + If specified, the new color for the new role passing a raw integer + for this may lead to unexpected behaviour. + hoist : :obj:`bool` + If specified, whether the role should hoist or not. + mentionable : :obj:`bool` + If specified, whether the role should be mentionable or not. + reason : :obj:`str` + If specified, the audit log reason explaining why the operation + was performed. + + Returns + ------- + :obj:`hikari.guilds.GuildRole` + The edited role object. + + Raises + ------ + :obj:`hikari.errors.NotFoundHTTPError` + If either the guild or role aren't found. + :obj:`hikari.errors.ForbiddenHTTPError` + If you lack the ``MANAGE_ROLES`` permission or you're not in the + guild. + :obj:`hikari.errors.BadRequestHTTPError` + If you provide invalid values for the role attributes. + If any invalid snowflake IDs are passed; a snowflake may be invalid + due to it being outside of the range of UINT64. + """ + payload = await self._session.modify_guild_role( + guild_id=str(guild.id if isinstance(guild, snowflakes.UniqueEntity) else int(guild)), + role_id=str(role.id if isinstance(role, snowflakes.UniqueEntity) else int(role)), + name=name, + permissions=permissions, + color=color, + hoist=hoist, + mentionable=mentionable, + reason=reason, + ) + return guilds.GuildRole.deserialize(payload) + + async def delete_role( + self, guild: snowflakes.HashableT[guilds.Guild], role: snowflakes.HashableT[guilds.GuildRole], + ) -> None: + """Delete a role from a given guild. + + Parameters + ---------- + guild : :obj:`typing.Union` [ :obj:`hikari.guilds.Guild`, :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ] + The object or ID of the guild you want to remove the role from. + role : :obj:`typing.Union` [ :obj:`hikari.guilds.GuildRole`, :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ] + The object or ID of the role you want to delete. + + Raises + ------ + :obj:`hikari.errors.BadRequestHTTPError` + If any invalid snowflake IDs are passed; a snowflake may be invalid + due to it being outside of the range of UINT64. + :obj:`hikari.errors.NotFoundHTTPError` + If either the guild or the role aren't found. + :obj:`hikari.errors.ForbiddenHTTPError` + If you lack the ``MANAGE_ROLES`` permission or are not in the guild. + """ + await self._session.delete_guild_role( + guild_id=str(guild.id if isinstance(guild, snowflakes.UniqueEntity) else int(guild)), + role_id=str(role.id if isinstance(role, snowflakes.UniqueEntity) else int(role)), + ) + + async def estimate_guild_prune_count( + self, guild: snowflakes.HashableT[guilds.Guild], days: typing.Union[datetime.timedelta, int], + ) -> int: + """Get the estimated prune count for a given guild. + + Parameters + ---------- + guild : :obj:`typing.Union` [ :obj:`hikari.guilds.Guild`, :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ] + The object or ID of the guild you want to get the count for. + days : :obj:`typing.Union` [ :obj:`datetime.timedelta`, :obj:`int` ] + The time delta of days to count prune for (at least ``1``). + + Returns + ------- + :obj:`int` + The number of members estimated to be pruned. + + Raises + ------ + :obj:`hikari.errors.NotFoundHTTPError` + If the guild is not found. + :obj:`hikari.errors.ForbiddenHTTPError` + If you lack the ``KICK_MEMBERS`` or you are not in the guild. + :obj:`hikari.errors.BadRequestHTTPError` + If you pass an invalid amount of days. + If any invalid snowflake IDs are passed; a snowflake may be invalid + due to it being outside of the range of UINT64. + """ + return await self._session.get_guild_prune_count( + guild_id=str(guild.id if isinstance(guild, snowflakes.UniqueEntity) else int(guild)), + days=getattr(days, "days", days), + ) + + async def begin_guild_prune( + self, + guild: snowflakes.HashableT[guilds.Guild], + days: typing.Union[datetime.timedelta, int], + *, + compute_prune_count: bool = ..., + reason: str = ..., + ) -> int: + """Prunes members of a given guild based on the number of inactive days. + + Parameters + ---------- + guild : :obj:`typing.Union` [ :obj:`hikari.guilds.Guild`, :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ] + The object or ID of the guild you want to prune member of. + days : :obj:`typing.Union` [ :obj:`datetime.timedelta`, :obj:`int` ] + The time delta of inactivity days you want to use as filter. + compute_prune_count : :obj:`bool` + Whether a count of pruned members is returned or not. + Discouraged for large guilds out of politeness. + reason : :obj:`str` + If specified, the audit log reason explaining why the operation + was performed. + + Returns + ------- + :obj:`int`, optional + The number of members who were kicked if ``compute_prune_count`` + is :obj:`True`, else :obj:`None`. + + Raises + ------ + :obj:`hikari.errors.NotFoundHTTPError` + If the guild is not found: + :obj:`hikari.errors.ForbiddenHTTPError` + If you lack the ``KICK_MEMBER`` permission or are not in the guild. + :obj:`hikari.errors.BadRequestHTTPError` + If you provide invalid values for the ``days`` or + ``compute_prune_count`` fields. + If any invalid snowflake IDs are passed; a snowflake may be invalid + due to it being outside of the range of UINT64. + """ + return await self._session.begin_guild_prune( + guild_id=str(guild.id if isinstance(guild, snowflakes.UniqueEntity) else int(guild)), + days=getattr(days, "days", days), + compute_prune_count=compute_prune_count, + reason=reason, + ) + + async def fetch_guild_voice_regions( + self, guild: snowflakes.HashableT[guilds.Guild], + ) -> typing.Sequence[voices.VoiceRegion]: + """Get the voice regions for a given guild. + + Parameters + ---------- + guild : :obj:`typing.Union` [ :obj:`hikari.guilds.Guild`, :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ] + The object or ID of the guild to get the voice regions for. + + Returns + ------- + :obj:`typing.Sequence` [ :obj:`hikari.voices.VoiceRegion` ] + A list of voice region objects. + + Raises + ------ + :obj:`hikari.errors.BadRequestHTTPError` + If any invalid snowflake IDs are passed; a snowflake may be invalid + due to it being outside of the range of UINT64. + :obj:`hikari.errors.NotFoundHTTPError` + If the guild is not found. + :obj:`hikari.errors.ForbiddenHTTPError` + If you are not in the guild. + """ + payload = await self._session.get_guild_voice_regions( + guild_id=str(guild.id if isinstance(guild, snowflakes.UniqueEntity) else int(guild)) + ) + return [voices.VoiceRegion.deserialize(region) for region in payload] + + async def fetch_guild_invites( + self, guild: snowflakes.HashableT[guilds.Guild], + ) -> typing.Sequence[invites.InviteWithMetadata]: + """Get the invites for a given guild. + + Parameters + ---------- + guild : :obj:`typing.Union` [ :obj:`hikari.guilds.Guild`, :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ] + The object or ID of the guild to get the invites for. + + Returns + ------- + :obj:`typing.Sequence` [ :obj:`hikari.invites.InviteWithMetadata` ] + A list of invite objects (with metadata). + + Raises + ------ + :obj:`hikari.errors.BadRequestHTTPError` + If any invalid snowflake IDs are passed; a snowflake may be invalid + due to it being outside of the range of UINT64. + :obj:`hikari.errors.NotFoundHTTPError` + If the guild is not found. + :obj:`hikari.errors.ForbiddenHTTPError` + If you lack the ``MANAGE_GUILD`` permission or are not in the guild. + """ + payload = await self._session.get_guild_invites( + guild_id=str(guild.id if isinstance(guild, snowflakes.UniqueEntity) else int(guild)) + ) + return [invites.InviteWithMetadata.deserialize(invite) for invite in payload] + + async def fetch_integrations( + self, guild: snowflakes.HashableT[guilds.Guild] + ) -> typing.Sequence[guilds.GuildIntegration]: + """Get the integrations for a given guild. + + Parameters + ---------- + guild : :obj:`typing.Union` [ :obj:`hikari.guilds.Guild`, :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ] + The object or ID of the guild to get the integrations for. + + Returns + ------- + :obj:`typing.Sequence` [ :obj:`hikari.guilds.GuildIntegration` ] + A list of integration objects. + + Raises + ------ + :obj:`hikari.errors.BadRequestHTTPError` + If any invalid snowflake IDs are passed; a snowflake may be invalid + due to it being outside of the range of UINT64. + :obj:`hikari.errors.NotFoundHTTPError` + If the guild is not found. + :obj:`hikari.errors.ForbiddenHTTPError` + If you lack the ``MANAGE_GUILD`` permission or are not in the guild. + """ + payload = await self._session.get_guild_integrations( + guild_id=str(guild.id if isinstance(guild, snowflakes.UniqueEntity) else int(guild)) + ) + return [guilds.GuildIntegration.deserialize(integration) for integration in payload] + + async def update_integration( + self, + guild: snowflakes.HashableT[guilds.Guild], + integration: snowflakes.HashableT[guilds.GuildIntegration], + *, + expire_behaviour: typing.Union[guilds.IntegrationExpireBehaviour, int] = ..., + expire_grace_period: typing.Union[datetime.timedelta, int] = ..., + enable_emojis: bool = ..., + reason: str = ..., + ) -> None: + """Edits an integrations for a given guild. + + Parameters + ---------- + guild : :obj:`typing.Union` [ :obj:`hikari.guilds.Guild`, :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ] + The object or ID of the guild to which the integration belongs to. + integration : :obj:`typing.Union` [ :obj:`hikari.guilds.GuildIntegration`, :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ] + The object or ID of the integration to update. + expire_behaviour : :obj:`typing.Union` [ :obj:`hikari.guilds.IntegrationExpireBehaviour`, :obj:`int` ] + If specified, the behaviour for when an integration subscription + expires (passing a raw integer for this may lead to unexpected + behaviour). + expire_grace_period : :obj:`typing.Union` [ :obj:`datetime.timedelta`, :obj:`int` ] + If specified, time time delta of how many days the integration will + ignore lapsed subscriptions for. + enable_emojis : :obj:`bool` + If specified, whether emojis should be synced for this integration. + reason : :obj:`str` + If specified, the audit log reason explaining why the operation + was performed. + + Raises + ------ + :obj:`hikari.errors.BadRequestHTTPError` + If any invalid snowflake IDs are passed; a snowflake may be invalid + due to it being outside of the range of UINT64. + :obj:`hikari.errors.NotFoundHTTPError` + If either the guild or the integration aren't found. + :obj:`hikari.errors.ForbiddenHTTPError` + If you lack the ``MANAGE_GUILD`` permission or are not in the guild. + """ + await self._session.modify_guild_integration( + guild_id=str(guild.id if isinstance(guild, snowflakes.UniqueEntity) else int(guild)), + integration_id=str( + integration.id if isinstance(integration, snowflakes.UniqueEntity) else int(integration) + ), + expire_behaviour=expire_behaviour, + expire_grace_period=getattr(expire_grace_period, "days", expire_grace_period), + enable_emojis=enable_emojis, + reason=reason, + ) + + async def delete_integration( + self, + guild: snowflakes.HashableT[guilds.Guild], + integration: snowflakes.HashableT[guilds.GuildIntegration], + *, + reason: str = ..., + ) -> None: + """Delete an integration for the given guild. + + Parameters + ---------- + guild : :obj:`typing.Union` [ :obj:`hikari.guilds.Guild`, :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ] + The object or ID of the guild to which the integration belongs to. + integration : :obj:`typing.Union` [ :obj:`hikari.guilds.GuildIntegration`, :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ] + The object or ID of the integration to delete. + reason : :obj:`str` + If specified, the audit log reason explaining why the operation + was performed. + + Raises + ------ + :obj:`hikari.errors.BadRequestHTTPError` + If any invalid snowflake IDs are passed; a snowflake may be invalid + due to it being outside of the range of UINT64. + :obj:`hikari.errors.NotFoundHTTPError` + If either the guild or the integration aren't found. + :obj:`hikari.errors.ForbiddenHTTPError` + If you lack the `MANAGE_GUILD` permission or are not in the guild. + """ + await self._session.delete_guild_integration( + guild_id=str(guild.id if isinstance(guild, snowflakes.UniqueEntity) else int(guild)), + integration_id=str( + integration.id if isinstance(integration, snowflakes.UniqueEntity) else int(integration) ), + reason=reason, + ) + + async def sync_guild_integration( + self, guild: snowflakes.HashableT[guilds.Guild], integration: snowflakes.HashableT[guilds.GuildIntegration], + ) -> None: + """Sync the given integration's subscribers/emojis. + + Parameters + ---------- + guild : :obj:`typing.Union` [ :obj:`hikari.guilds.Guild`, :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ] + The object or ID of the guild to which the integration belongs to. + integration : :obj:`typing.Union` [ :obj:`hikari.guilds.GuildIntegration`, :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ] + The ID of the integration to sync. + + Raises + ------ + :obj:`hikari.errors.BadRequestHTTPError` + If any invalid snowflake IDs are passed; a snowflake may be invalid + due to it being outside of the range of UINT64. + :obj:`hikari.errors.NotFoundHTTPError` + If either the guild or the integration aren't found. + :obj:`hikari.errors.ForbiddenHTTPError` + If you lack the ``MANAGE_GUILD`` permission or are not in the guild. + """ + await self._session.sync_guild_integration( + guild_id=str(guild.id if isinstance(guild, snowflakes.UniqueEntity) else int(guild)), + integration_id=str( + integration.id if isinstance(integration, snowflakes.UniqueEntity) else int(integration) + ), + ) + + async def fetch_guild_embed(self, guild: snowflakes.HashableT[guilds.Guild],) -> guilds.GuildEmbed: + """Get the embed for a given guild. + + Parameters + ---------- + guild : :obj:`typing.Union` [ :obj:`hikari.guilds.Guild`, :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ] + The object or ID of the guild to get the embed for. + + Returns + ------- + :obj:`hikari.guilds.GuildEmbed` + A guild embed object. + + Raises + ------ + :obj:`hikari.errors.BadRequestHTTPError` + If any invalid snowflake IDs are passed; a snowflake may be invalid + due to it being outside of the range of UINT64. + :obj:`hikari.errors.NotFoundHTTPError` + If the guild is not found. + :obj:`hikari.errors.ForbiddenHTTPError` + If you either lack the ``MANAGE_GUILD`` permission or are not in + the guild. + """ + payload = await self._session.get_guild_embed( + guild_id=str(guild.id if isinstance(guild, snowflakes.UniqueEntity) else int(guild)) + ) + return guilds.GuildEmbed.deserialize(payload) + + async def update_guild_embed( + self, + guild: snowflakes.HashableT[guilds.Guild], + *, + channel: snowflakes.HashableT[_channels.GuildChannel] = ..., + enabled: bool = ..., + reason: str = ..., + ) -> guilds.GuildEmbed: + """Edits the embed for a given guild. + + Parameters + ---------- + guild : :obj:`typing.Union` [ :obj:`hikari.guilds.Guild`, :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ] + The object or ID of the guild to edit the embed for. + channel : :obj:`typing.Union` [ :obj:`hikari.channels.GuildChannel`, :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ], optional + If specified, the object or ID of the channel that this embed's + invite should target. Set to :obj:`None` to disable invites for this + embed. + enabled : :obj:`bool` + If specified, whether this embed should be enabled. + reason : :obj:`str` + If specified, the audit log reason explaining why the operation + was performed. + + Returns + ------- + :obj:`hikari.guilds.GuildEmbed` + The updated embed object. + + Raises + ------ + :obj:`hikari.errors.BadRequestHTTPError` + If any invalid snowflake IDs are passed; a snowflake may be invalid + due to it being outside of the range of a 64 bit integer. + :obj:`hikari.errors.NotFoundHTTPError` + If the guild is not found. + :obj:`hikari.errors.ForbiddenHTTPError` + If you either lack the ``MANAGE_GUILD`` permission or are not in + the guild. + """ + payload = await self._session.modify_guild_embed( + guild_id=str(guild.id if isinstance(guild, snowflakes.UniqueEntity) else int(guild)), + channel_id=( + str(channel.id if isinstance(channel, snowflakes.UniqueEntity) else int(channel)) + if channel is not ... + else ... + ), + enabled=enabled, + reason=reason, + ) + return guilds.GuildEmbed.deserialize(payload) + + async def fetch_guild_vanity_url(self, guild: snowflakes.HashableT[guilds.Guild],) -> invites.VanityUrl: + """ + Get the vanity URL for a given guild. + + Parameters + ---------- + guild : :obj:`typing.Union` [ :obj:`hikari.guilds.Guild`, :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ] + The object or ID of the guild to get the vanity URL for. + + Returns + ------- + :obj:`hikari.invites.VanityUrl` + A partial invite object containing the vanity URL in the ``code`` + field. + + Raises + ------ + :obj:`hikari.errors.BadRequestHTTPError` + If any invalid snowflake IDs are passed; a snowflake may be invalid + due to it being outside of the range of a 64 bit integer. + :obj:`hikari.errors.NotFoundHTTPError` + If the guild is not found. + :obj:`hikari.errors.ForbiddenHTTPError` + If you either lack the ``MANAGE_GUILD`` permission or are not in + the guild. + """ + payload = await self._session.get_guild_vanity_url( + guild_id=str(guild.id if isinstance(guild, snowflakes.UniqueEntity) else int(guild)) + ) + return invites.VanityUrl.deserialize(payload) + + def format_guild_widget_image(self, guild: snowflakes.HashableT[guilds.Guild], *, style: str = ...) -> str: + """Get the URL for a guild widget. + + Parameters + ---------- + guild : :obj:`typing.Union` [ :obj:`hikari.guilds.Guild`, :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ] + The object or ID of the guild to form the widget. + style : :obj:`str` + If specified, the syle of the widget. + + Returns + ------- + :obj:`str` + A URL to retrieve a PNG widget for your guild. + + Note + ---- + This does not actually make any form of request, and shouldn't be + awaited. Thus, it doesn't have rate limits either. + + Warning + ------- + The guild must have the widget enabled in the guild settings for this + to be valid. + """ + return self._session.get_guild_widget_image_url( + guild_id=str(guild.id if isinstance(guild, snowflakes.UniqueEntity) else int(guild)), style=style + ) + + async def fetch_invite( + self, invite: typing.Union[invites.Invite, str], *, with_counts: bool = ... + ) -> invites.Invite: + """Get the given invite. + + Parameters + ---------- + invite : :obj:`typing.Union` [ :obj:`hikari.invites.Invite`, :obj:`str` ] + The object or code of the wanted invite. + with_counts : :bool: + If specified, whether to attempt to count the number of + times the invite has been used. + + Returns + ------- + :obj:`hikari.invites.Invite` + The requested invite object. + + Raises + ------ + :obj:`hikari.errors.BadRequestHTTPError` + If any invalid snowflake IDs are passed; a snowflake may be invalid + due to it being outside of the range of a 64 bit integer. + :obj:`hikari.errors.NotFoundHTTPError` + If the invite is not found. + """ + payload = await self._session.get_invite(invite_code=getattr(invite, "code", invite), with_counts=with_counts) + return invites.Invite.deserialize(payload) + + async def delete_invite(self, invite: typing.Union[invites.Invite, str]) -> None: + """Delete a given invite. + + Parameters + ---------- + invite : :obj:`typing.Union` [ :obj:`hikari.invites.Invite`, :obj:`str` ] + The object or ID for the invite to be deleted. + + Returns + ------- + :obj:`None` + Nothing, unlike what the API specifies. This is done to maintain + consistency with other calls of a similar nature in this API wrapper. + + Raises + ------ + :obj:`hikari.errors.NotFoundHTTPError` + If the invite is not found. + :obj:`hikari.errors.ForbiddenHTTPError` + If you lack either ``MANAGE_CHANNELS`` on the channel the invite + belongs to or ``MANAGE_GUILD`` for guild-global delete. + """ + await self._session.delete_invite(invite_code=getattr(invite, "code", invite)) + + async def fetch_user(self, user: snowflakes.HashableT[users.User]) -> users.User: + """Get a given user. + + Parameters + ---------- + user : :obj:`typing.Union` [ :obj:`hikari.users.User`, :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ] + The object or ID of the user to get. + + Returns + ------- + :obj:`hikari.users.User` + The requested user object. + + Raises + ------ + :obj:`hikari.errors.BadRequestHTTPError` + If any invalid snowflake IDs are passed; a snowflake may be invalid + due to it being outside of the range of a 64 bit integer. + :obj:`hikari.errors.NotFoundHTTPError` + If the user is not found. + """ + payload = await self._session.get_user( + user_id=str(user.id if isinstance(user, snowflakes.UniqueEntity) else int(user)) + ) + return users.User.deserialize(payload) + + async def fetch_my_application_info(self) -> oauth2.Application: + """Get the current application information. + + Returns + ------- + :obj:`hikari.oauth2.Application` + An application info object. + """ + payload = await self._session.get_current_application_info() + return oauth2.Application.deserialize(payload) + + async def fetch_me(self) -> users.MyUser: + """Get the current user that of the token given to the client. + + Returns + ------- + :obj:`hikari.users.MyUser` + The current user object. + """ + payload = await self._session.get_current_user() + return users.MyUser.deserialize(payload) + + async def update_me( + self, *, username: str = ..., avatar_data: typing.Optional[conversions.FileLikeT] = ..., + ) -> users.MyUser: + """Edit the current user. + + Parameters + ---------- + username : :obj:`str` + If specified, the new username string. + avatar_data : :obj:`hikari.internal.conversions.FileLikeT`, optional + If specified, the new avatar image data. + If it is :obj:`None`, the avatar is removed. + + Returns + ------- + :obj:`hikari.users.MyUser` + The updated user object. + + Raises + ------ + :obj:`hikari.errors.BadRequestHTTPError` + If you pass username longer than the limit (``2-32``) or an invalid image. + """ + payload = await self._session.modify_current_user( + username=username, + avatar=conversions.get_bytes_from_resource(avatar_data) if avatar_data is not ... else ..., + ) + return users.MyUser.deserialize(payload) + + async def fetch_my_connections(self) -> typing.Sequence[oauth2.OwnConnection]: + """ + Get the current user's connections. + + Note + ---- + This endpoint can be used with both ``Bearer`` and ``Bot`` tokens but + will usually return an empty list for bots (with there being some + exceptions to this, like user accounts that have been converted to bots). + + Returns + ------- + :obj:`typing.Sequence` [ :obj:`hikari.oauth2.OwnConnection` ] + A list of connection objects. + """ + payload = await self._session.get_current_user_connections() + return [oauth2.OwnConnection.deserialize(connection) for connection in payload] + + def fetch_my_guilds_after( + self, + *, + after: typing.Union[datetime.datetime, snowflakes.HashableT[guilds.Guild]] = 0, + limit: typing.Optional[int] = None, + ) -> typing.AsyncIterator[oauth2.OwnGuild]: + """Get an async iterator of the guilds the current user is in. + + This returns the guilds created after a given guild object/ID or from + the oldest guild. + + Parameters + ---------- + after : :obj:`typing.Union` [ :obj:`datetime.datetime`, :obj:`hikari.guilds.Guild`, :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ] + The object or ID of a guild to get guilds that were created after + it if specified, else this will start at the oldest guild. + limit : :obj:`int` + If specified, the maximum amount of guilds that this paginator + should return. + + Example + ------- + .. code-block:: python + + async for user in client.fetch_my_guilds_after(after=9876543, limit=1231): + await client.leave_guild(guild) + + Returns + ------- + :obj:`typing.AsyncIterator` [ :obj:`hikari.oauth2.OwnGuild` ] + An async iterator of partial guild objects. + + Raises + ------ + :obj:`hikari.errors.NotFoundHTTPError` + If the guild is not found. + :obj:`hikari.errors.BadRequestHTTPError` + If any invalid snowflake IDs are passed; a snowflake may be invalid + due to it being outside of the range of a 64 bit integer. + """ + if isinstance(after, datetime.datetime): + after = str(snowflakes.Snowflake.from_datetime(after)) + else: + after = str(after.id if isinstance(after, snowflakes.UniqueEntity) else int(after)) + return self._pagination_handler( + deserializer=oauth2.OwnGuild.deserialize, + direction="after", + request=self._session.get_current_user_guilds, + reversing=False, + start=after, + limit=limit, + ) + + def fetch_my_guilds_before( + self, + *, + before: typing.Union[datetime.datetime, snowflakes.HashableT[guilds.Guild], None] = None, + limit: typing.Optional[int] = None, + ) -> typing.AsyncIterator[oauth2.OwnGuild]: + """Get an async iterator of the guilds the current user is in. + + This returns the guilds that were created before a given user object/ID + or from the newest guild. + + Parameters + ---------- + before : :obj:`typing.Union` [ :obj:`datetime.datetime`, :obj:`hikari.guilds.Guild`, :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ] + The object or ID of a guild to get guilds that were created + before it if specified, else this will start at the newest guild. + limit : :obj:`int` + If specified, the maximum amount of guilds that this paginator + should return. + + Returns + ------- + :obj:`typing.AsyncIterator` [ :obj:`hikari.oauth2.OwnGuild` ] + An async iterator of partial guild objects. + + Raises + ------ + :obj:`hikari.errors.NotFoundHTTPError` + If the guild is not found. + :obj:`hikari.errors.BadRequestHTTPError` + If any invalid snowflake IDs are passed; a snowflake may be invalid + due to it being outside of the range of a 64 bit integer. + """ + if isinstance(before, datetime.datetime): + before = str(snowflakes.Snowflake.from_datetime(before)) + elif before is not None: + before = str(before.id if isinstance(before, snowflakes.UniqueEntity) else int(before)) + return self._pagination_handler( + deserializer=oauth2.OwnGuild.deserialize, + direction="before", + request=self._session.get_current_user_guilds, + reversing=False, + start=before, + limit=limit, + ) + + async def leave_guild(self, guild: snowflakes.HashableT[guilds.Guild]) -> None: + """Make the current user leave a given guild. + + Parameters + ---------- + guild : :obj:`typing.Union` [ :obj:`hikari.guilds.Guild`, :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ] + The object or ID of the guild to leave. + + Raises + ------ + :obj:`hikari.errors.NotFoundHTTPError` + If the guild is not found. + :obj:`hikari.errors.BadRequestHTTPError` + If any invalid snowflake IDs are passed; a snowflake may be invalid + due to it being outside of the range of a 64 bit integer. + """ + await self._session.leave_guild( + guild_id=str(guild.id if isinstance(guild, snowflakes.UniqueEntity) else int(guild)) + ) + + async def create_dm_channel(self, recipient: snowflakes.HashableT[users.User]) -> _channels.DMChannel: + """Create a new DM channel with a given user. + + Parameters + ---------- + recipient : :obj:`typing.Union` [ :obj:`hikari.users.User`, :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ] + The object or ID of the user to create the new DM channel with. + + Returns + ------- + :obj:`hikari.channels.DMChannel` + The newly created DM channel object. + + Raises + ------ + :obj:`hikari.errors.NotFoundHTTPError` + If the recipient is not found. + :obj:`hikari.errors.BadRequestHTTPError` + If any invalid snowflake IDs are passed; a snowflake may be invalid + due to it being outside of the range of a 64 bit integer. + """ + payload = await self._session.create_dm( + recipient_id=str(recipient.id if isinstance(recipient, snowflakes.UniqueEntity) else int(recipient)) + ) + return _channels.DMChannel.deserialize(payload) + + async def fetch_voice_regions(self) -> typing.Sequence[voices.VoiceRegion]: + """Get the voice regions that are available. + + Returns + ------- + :obj:`typing.Sequence` [ :obj:`hikari.voices.VoiceRegion` ] + A list of voice regions available + + Note + ---- + This does not include VIP servers. + """ + payload = await self._session.list_voice_regions() + return [voices.VoiceRegion.deserialize(region) for region in payload] + + async def create_webhook( + self, + channel: snowflakes.HashableT[_channels.GuildChannel], + name: str, + *, + avatar_data: conversions.FileLikeT = ..., + reason: str = ..., + ) -> webhooks.Webhook: + """Create a webhook for a given channel. + + Parameters + ---------- + channel : :obj:`typing.Union` [ :obj:`hikari.channels.GuildChannel`, :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ] + The object or ID of the channel for webhook to be created in. + name : :obj:`str` + The webhook's name string. + avatar_data : :obj:`hikari.internal.conversions.FileLikeT` + If specified, the avatar image data. + reason : :obj:`str` + If specified, the audit log reason explaining why the operation + was performed. + + Returns + ------- + :obj:`hikari.webhooks.Webhook` + The newly created webhook object. + + Raises + ------ + :obj:`hikari.errors.NotFoundHTTPError` + If the channel is not found. + :obj:`hikari.errors.ForbiddenHTTPError` + If you either lack the ``MANAGE_WEBHOOKS`` permission or + can not see the given channel. + :obj:`hikari.errors.BadRequestHTTPError` + If the avatar image is too big or the format is invalid. + If any invalid snowflake IDs are passed; a snowflake may be invalid + due to it being outside of the range of a 64 bit integer. + """ + payload = await self._session.create_webhook( + channel_id=str(channel.id if isinstance(channel, snowflakes.UniqueEntity) else int(channel)), + name=name, + avatar=conversions.get_bytes_from_resource(avatar_data) if avatar_data is not ... else ..., + reason=reason, + ) + return webhooks.Webhook.deserialize(payload) + + async def fetch_channel_webhooks( + self, channel: snowflakes.HashableT[_channels.GuildChannel] + ) -> typing.Sequence[webhooks.Webhook]: + """Get all webhooks from a given channel. + + Parameters + ---------- + channel : :obj:`typing.Union` [ :obj:`hikari.channels.GuildChannel`, :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ] + The object or ID of the guild channel to get the webhooks from. + + Returns + ------- + :obj:`typing.Sequence` [ :obj:`hikari.webhooks.Webhook` ] + A list of webhook objects for the give channel. + + Raises + ------ + :obj:`hikari.errors.BadRequestHTTPError` + If any invalid snowflake IDs are passed; a snowflake may be invalid + due to it being outside of the range of a 64 bit integer. + :obj:`hikari.errors.NotFoundHTTPError` + If the channel is not found. + :obj:`hikari.errors.ForbiddenHTTPError` + If you either lack the ``MANAGE_WEBHOOKS`` permission or + can not see the given channel. + """ + payload = await self._session.get_channel_webhooks( + channel_id=str(channel.id if isinstance(channel, snowflakes.UniqueEntity) else int(channel)) + ) + return [webhooks.Webhook.deserialize(webhook) for webhook in payload] + + async def fetch_guild_webhooks( + self, guild: snowflakes.HashableT[guilds.Guild] + ) -> typing.Sequence[webhooks.Webhook]: + """Get all webhooks for a given guild. + + Parameters + ---------- + guild : :obj:`typing.Union` [ :obj:`hikari.guilds.Guild`, :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ] + The object or ID for the guild to get the webhooks from. + + Returns + ------- + :obj:`typing.Sequence` [ :obj:`hikari.webhooks.Webhook` ] + A list of webhook objects for the given guild. + + Raises + ------ + :obj:`hikari.errors.BadRequestHTTPError` + If any invalid snowflake IDs are passed; a snowflake may be invalid + due to it being outside of the range of a 64 bit integer. + :obj:`hikari.errors.NotFoundHTTPError` + If the guild is not found. + :obj:`hikari.errors.ForbiddenHTTPError` + If you either lack the ``MANAGE_WEBHOOKS`` permission or + aren't a member of the given guild. + """ + payload = await self._session.get_guild_webhooks( + guild_id=str(guild.id if isinstance(guild, snowflakes.UniqueEntity) else int(guild)) + ) + return [webhooks.Webhook.deserialize(webhook) for webhook in payload] + + async def fetch_webhook( + self, webhook: snowflakes.HashableT[webhooks.Webhook], *, webhook_token: str = ... + ) -> webhooks.Webhook: + """Get a given webhook. + + Parameters + ---------- + webhook : :obj:`typing.Union` [ :obj:`hikari.webhooks.Webhook`, :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ] + The object or ID of the webhook to get. + webhook_token : :obj:`str` + If specified, the webhook token to use to get it (bypassing this + session's provided authorization ``token``). + + Returns + ------- + :obj:`hikari.webhooks.Webhook` + The requested webhook object. + + Raises + ------ + :obj:`hikari.errors.BadRequestHTTPError` + If any invalid snowflake IDs are passed; a snowflake may be invalid + due to it being outside of the range of a 64 bit integer. + :obj:`hikari.errors.NotFoundHTTPError` + If the webhook is not found. + :obj:`hikari.errors.ForbiddenHTTPError` + If you're not in the guild that owns this webhook or + lack the ``MANAGE_WEBHOOKS`` permission. + :obj:`hikari.errors.UnauthorizedHTTPError` + If you pass a token that's invalid for the target webhook. + """ + payload = await self._session.get_webhook( + webhook_id=str(webhook.id if isinstance(webhook, snowflakes.UniqueEntity) else int(webhook)), + webhook_token=webhook_token, + ) + return webhooks.Webhook.deserialize(payload) + + async def update_webhook( + self, + webhook: snowflakes.HashableT[webhooks.Webhook], + *, + webhook_token: str = ..., + name: str = ..., + avatar_data: typing.Optional[conversions.FileLikeT] = ..., + channel: snowflakes.HashableT[_channels.GuildChannel] = ..., + reason: str = ..., + ) -> webhooks.Webhook: + """Edit a given webhook. + + Parameters + ---------- + webhook : :obj:`typing.Union` [ :obj:`hikari.webhooks.Webhook`, :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ] + The object or ID of the webhook to edit. + webhook_token : :obj:`str` + If specified, the webhook token to use to modify it (bypassing this + session's provided authorization ``token``). + name : :obj:`str` + If specified, the new name string. + avatar_data : :obj:`hikari.internal.conversions.FileLikeT`, optional + If specified, the new avatar image file object. If :obj:`None`, then + it is removed. + channel : :obj:`typing.Union` [ :obj:`hikari.channels.GuildChannel`, :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ] + If specified, the object or ID of the new channel the given + webhook should be moved to. + reason : :obj:`str` + If specified, the audit log reason explaining why the operation + was performed. + + Returns + ------- + :obj:`hikari.webhooks.Webhook` + The updated webhook object. + + Raises + ------ + :obj:`hikari.errors.BadRequestHTTPError` + If any invalid snowflake IDs are passed; a snowflake may be invalid + due to it being outside of the range of a 64 bit integer. + :obj:`hikari.errors.NotFoundHTTPError` + If either the webhook or the channel aren't found. + :obj:`hikari.errors.ForbiddenHTTPError` + If you either lack the ``MANAGE_WEBHOOKS`` permission or + aren't a member of the guild this webhook belongs to. + :obj:`hikari.errors.UnauthorizedHTTPError` + If you pass a token that's invalid for the target webhook. + """ + payload = await self._session.modify_webhook( + webhook_id=str(webhook.id if isinstance(webhook, snowflakes.UniqueEntity) else int(webhook)), + webhook_token=webhook_token, + name=name, + avatar=( + conversions.get_bytes_from_resource(avatar_data) + if avatar_data and avatar_data is not ... + else avatar_data + ), + channel_id=( + str(channel.id if isinstance(channel, snowflakes.UniqueEntity) else int(channel)) + if channel and channel is not ... + else channel + ), + reason=reason, + ) + return webhooks.Webhook.deserialize(payload) + + async def delete_webhook( + self, webhook: snowflakes.HashableT[webhooks.Webhook], *, webhook_token: str = ... + ) -> None: + """Delete a given webhook. + + Parameters + ---------- + webhook : :obj:`typing.Union` [ :obj:`hikari.webhooks.Webhook`, :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ] + The object or ID of the webhook to delete + webhook_token : :obj:`str` + If specified, the webhook token to use to delete it (bypassing this + session's provided authorization ``token``). + + Raises + ------ + :obj:`hikari.errors.BadRequestHTTPError` + If any invalid snowflake IDs are passed; a snowflake may be invalid + due to it being outside of the range of a 64 bit integer. + :obj:`hikari.errors.NotFoundHTTPError` + If the webhook is not found. + :obj:`hikari.errors.ForbiddenHTTPError` + If you either lack the ``MANAGE_WEBHOOKS`` permission or + aren't a member of the guild this webhook belongs to. + :obj:`hikari.errors.UnauthorizedHTTPError` + If you pass a token that's invalid for the target webhook. + """ + await self._session.delete_webhook( + webhook_id=str(webhook.id if isinstance(webhook, snowflakes.UniqueEntity) else int(webhook)), + webhook_token=webhook_token, + ) + + async def execute_webhook( + self, + webhook: snowflakes.HashableT[webhooks.Webhook], + webhook_token: str, + *, + content: str = ..., + username: str = ..., + avatar_url: str = ..., + tts: bool = ..., + wait: bool = False, + file: media.IO = ..., + embeds: typing.Sequence[_embeds.Embed] = ..., + mentions_everyone: bool = True, + user_mentions: typing.Union[typing.Collection[snowflakes.HashableT[users.User]], bool] = True, + role_mentions: typing.Union[typing.Collection[snowflakes.HashableT[guilds.GuildRole]], bool] = True, + ) -> typing.Optional[_messages.Message]: + """Execute a webhook to create a message. + + Parameters + ---------- + webhook : :obj:`typing.Union` [ :obj:`hikari.webhooks.Webhook`, :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ] + The object or ID of the webhook to execute. + webhook_token : :obj:`str` + The token of the webhook to execute. + content : :obj:`str` + If specified, the message content to send with the message. + username : :obj:`str` + If specified, the username to override the webhook's username + for this request. + avatar_url : :obj:`str` + If specified, the url of an image to override the webhook's + avatar with for this request. + tts : :obj:`bool` + If specified, whether the message will be sent as a TTS message. + wait : :obj:`bool` + If specified, whether this request should wait for the webhook + to be executed and return the resultant message object. + file : :obj:`hikari.media.IO` + If specified, this is a file object to send along with the webhook + as defined in :mod:`hikari.media`. + embeds : :obj:`typing.Sequence` [ :obj:`hikari.embeds.Embed` ] + If specified, a sequence of ``1`` to ``10`` embed objects to send + with the embed. + mentions_everyone : :obj:`bool` + Whether ``@everyone`` and ``@here`` mentions should be resolved by + discord and lead to actual pings, defaults to :obj:`True`. + user_mentions : :obj:`typing.Union` [ :obj:`typing.Collection` [ :obj:`typing.Union` [ :obj:`hikari.users.User`, :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ], :obj:`bool` ] + Either an array of user objects/IDs to allow mentions for, + :obj:`True` to allow all user mentions or :obj:`False` to block all + user mentions from resolving, defaults to :obj:`True`. + role_mentions : :obj:`typing.Union` [ :obj:`typing.Collection` [ :obj:`typing.Union` [ :obj:`hikari.guilds.GuildRole`, :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ] ], :obj:`bool` ] + Either an array of guild role objects/IDs to allow mentions for, + :obj:`True` to allow all role mentions or :obj:`False` to block all + role mentions from resolving, defaults to :obj:`True`. + + Returns + ------- + :obj:`hikari.messages.Message`, optional + The created message object, if ``wait`` is :obj:`True`, else + :obj:`None`. + + Raises + ------ + :obj:`hikari.errors.NotFoundHTTPError` + If the channel ID or webhook ID is not found. + :obj:`hikari.errors.BadRequestHTTPError` + This can be raised if the file is too large; if the embed exceeds + the defined limits; if the message content is specified only and + empty or greater than ``2000`` characters; if neither content, file + or embeds are specified. + If any invalid snowflake IDs are passed; a snowflake may be invalid + due to it being outside of the range of a 64 bit integer. + :obj:`hikari.errors.ForbiddenHTTPError` + If you lack permissions to send to this channel. + :obj:`hikari.errors.UnauthorizedHTTPError` + If you pass a token that's invalid for the target webhook. + """ + payload = await self._session.execute_webhook( + webhook_id=str(webhook.id if isinstance(webhook, snowflakes.UniqueEntity) else int(webhook)), + webhook_token=webhook_token, + content=content, + username=username, + avatar_url=avatar_url, + tts=tts, + wait=wait, + file=await media.safe_read_file(file) if file is not ... else ..., + embeds=[embed.serialize() for embed in embeds] if embeds is not ... else ..., + allowed_mentions=self._generate_allowed_mentions( + mentions_everyone=mentions_everyone, user_mentions=user_mentions, role_mentions=role_mentions + ), + ) + if wait is True: + return _messages.Message.deserialize(payload) + return None + + def safe_webhook_execute( + self, + webhook: snowflakes.HashableT[webhooks.Webhook], + webhook_token: str, + *, + content: str = ..., + username: str = ..., + avatar_url: str = ..., + tts: bool = ..., + wait: bool = False, + file: media.IO = ..., + embeds: typing.Sequence[_embeds.Embed] = ..., + mentions_everyone: bool = False, + user_mentions: typing.Union[typing.Collection[snowflakes.HashableT[users.User]], bool] = False, + role_mentions: typing.Union[typing.Collection[snowflakes.HashableT[guilds.GuildRole]], bool] = False, + ) -> typing.Coroutine[typing.Any, typing.Any, typing.Optional[_messages.Message]]: + """Execute a webhook to create a message with mention safety. + + This endpoint has the same signature as :attr:`execute_webhook` with + the only difference being that ``mentions_everyone``, + ``user_mentions`` and ``role_mentions`` default to :obj:`False`. + """ + return self.execute_webhook( + webhook=webhook, + webhook_token=webhook_token, + content=content, + username=username, + avatar_url=avatar_url, + tts=tts, + wait=wait, + file=file, + embeds=embeds, + mentions_everyone=mentions_everyone, + user_mentions=user_mentions, + role_mentions=role_mentions, ) diff --git a/hikari/emojis.py b/hikari/emojis.py index 0e531b16c3..8e7e68f88e 100644 --- a/hikari/emojis.py +++ b/hikari/emojis.py @@ -45,6 +45,16 @@ class UnicodeEmoji(Emoji): #: :type: :obj:`str` name: str = marshaller.attrib(deserializer=str) + @property + def url_name(self) -> str: + """Get the format of this emoji used in request routes.""" + return self.name + + @property + def mention(self) -> str: + """Get the format of this emoji used for sending it in a channel.""" + return self.name + @marshaller.marshallable() @attr.s(slots=True) @@ -56,11 +66,16 @@ class UnknownEmoji(Emoji, snowflakes.UniqueEntity): #: :type: :obj:`str`, optional name: typing.Optional[str] = marshaller.attrib(deserializer=str, if_none=None) - #: Wheter the emoji is animated. + #: Whether the emoji is animated. #: #: :type: :obj:`bool` is_animated: bool = marshaller.attrib(raw_name="animated", deserializer=bool, if_undefined=False) + @property + def url_name(self) -> str: + """Get the format of this emoji used in request routes.""" + return f"{self.name}:{self.id}" + @marshaller.marshallable() @attr.s(slots=True) @@ -96,11 +111,16 @@ class GuildEmoji(UnknownEmoji): raw_name="require_colons", deserializer=bool, if_undefined=None ) - #: Wheter the emoji is managed by an integration. + #: Whether the emoji is managed by an integration. #: #: :type: :obj:`bool`, optional is_managed: typing.Optional[bool] = marshaller.attrib(raw_name="managed", deserializer=bool, if_undefined=None) + @property + def mention(self) -> str: + """Get the format of this emoji used for sending it in a channel.""" + return f"<{'a' if self.is_animated else ''}:{self.url_name}>" + def deserialize_reaction_emoji(payload: typing.Dict) -> typing.Union[UnicodeEmoji, UnknownEmoji]: """Deserialize a reaction emoji into an emoji.""" diff --git a/hikari/guilds.py b/hikari/guilds.py index 50bab37c15..76c2339dec 100644 --- a/hikari/guilds.py +++ b/hikari/guilds.py @@ -180,6 +180,24 @@ class GuildVerificationLevel(enum.IntEnum): VERY_HIGH = 4 +@marshaller.marshallable() +@attr.s(slots=True) +class GuildEmbed(entities.HikariEntity, entities.Deserializable): + """Represents a guild embed.""" + + #: The ID of the channel the invite for this embed targets, if enabled + #: + #: :type: :obj:`hikari.snowflakes.Snowflake`, optional + channel_id: typing.Optional[snowflakes.Snowflake] = marshaller.attrib( + deserializer=snowflakes.Snowflake.deserialize, serializer=str, if_none=None + ) + + #: Whether this embed is enabled. + #: + #: :type: :obj:`bool` + is_enabled: bool = marshaller.attrib(raw_name="enabled", deserializer=bool, serializer=bool) + + @marshaller.marshallable() @attr.s(slots=True) class GuildMember(entities.HikariEntity, entities.Deserializable): @@ -242,7 +260,7 @@ class PartialGuildRole(snowflakes.UniqueEntity, entities.Deserializable): @marshaller.marshallable() @attr.s(slots=True) -class GuildRole(PartialGuildRole): +class GuildRole(PartialGuildRole, entities.Serializable): """Represents a guild bound Role object.""" #: The colour of this role, will be applied to a member's name in chat diff --git a/hikari/internal/conversions.py b/hikari/internal/conversions.py index 62cfd4aa10..9a04519146 100644 --- a/hikari/internal/conversions.py +++ b/hikari/internal/conversions.py @@ -31,6 +31,8 @@ "make_resource_seekable", "pluralize", "snoop_typehint_from_scope", + "FileLikeT", + "BytesLikeT", ] import base64 @@ -313,6 +315,34 @@ def make_resource_seekable(resource: typing.Any, /) -> Seekable: return resource +def get_bytes_from_resource(resource: typing.Any) -> bytes: + """ + Take in any file-like object and return the raw bytes data from it. + + Supports any :obj:`FileLikeT` type that isn't string based. + Anything else is just returned. + + Parameters + ---------- + resource : :obj:`FileLikeT` + The resource to get bytes from. + + Returns + ------- + :obj:`bytes` + The resulting bytes. + """ + if isinstance(resource, bytearray): + resource = bytes(resource) + elif isinstance(resource, memoryview): + resource = resource.tobytes() + # Targets the io types found in FileLikeT and BytesLikeT + elif hasattr(resource, "read"): + resource = resource.read() + + return resource + + def pluralize(count: int, name: str, suffix: str = "s") -> str: """Pluralizes a word.""" return f"{count} {name + suffix}" if count - 1 else f"{count} {name}" diff --git a/hikari/media.py b/hikari/media.py new file mode 100644 index 0000000000..4d4eb0e92c --- /dev/null +++ b/hikari/media.py @@ -0,0 +1,47 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# Copyright © Nekoka.tt 2019-2020 +# +# This file is part of Hikari. +# +# Hikari is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Hikari is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Hikari. If not, see . +"""Represents various forms of media such as images.""" +__all__ = ["TextIO", "BytesIO", "IO", "safe_read_file"] + +import io +import typing + +import aiofiles + +from hikari.internal import conversions + +TextIO = typing.Union[aiofiles.threadpool.text.AsyncTextIOWrapper, typing.TextIO, io.StringIO, str] + +BytesIO = typing.Union[ + aiofiles.threadpool.binary.AsyncBufferedIOBase, + aiofiles.threadpool.binary.AsyncBufferedReader, + aiofiles.threadpool.binary.AsyncFileIO, + typing.BinaryIO, + io.BytesIO, + bytes, + bytearray, + memoryview, +] + +IO = typing.Union[TextIO, BytesIO] + + +async def safe_read_file(file: IO) -> typing.Tuple[str, conversions.FileLikeT]: + """Safely read an :obj:`IO` like object.""" + raise NotImplementedError # TODO: Nekokatt: update this. diff --git a/hikari/net/rest.py b/hikari/net/rest.py index 87504bd73e..f4c4381471 100644 --- a/hikari/net/rest.py +++ b/hikari/net/rest.py @@ -17,7 +17,7 @@ # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . """Implementation of a basic HTTP client that uses aiohttp to interact with the Discord API.""" -__all__ = ["RestfulClient"] +__all__ = ["LowLevelRestfulClient"] import asyncio import contextlib @@ -26,6 +26,7 @@ import json import logging import ssl +import types import typing import uuid @@ -44,7 +45,7 @@ from hikari.net import versions -class RestfulClient: +class LowLevelRestfulClient: """A RESTful client to allow you to interact with the Discord API. Parameters @@ -228,7 +229,7 @@ def __init__( json_serialize: typing.Callable[[typing.Dict], typing.AnyStr] = json.dumps, token: typing.Optional[str], version: typing.Union[int, versions.HTTPAPIVersion] = versions.HTTPAPIVersion.STABLE, - ): + ) -> None: self.allow_redirects = allow_redirects self.client_session = aiohttp.ClientSession( connector=connector, version=aiohttp.HttpVersion11, json_serialize=json_serialize or json.dumps, @@ -257,7 +258,7 @@ def __init__( self.token = token - async def close(self): + async def close(self) -> None: """Shut down the REST client safely, and terminate any rate limiters executing in the background.""" with contextlib.suppress(Exception): self.ratelimiter.close() @@ -265,13 +266,12 @@ async def close(self): self.logger.debug("Closing %s", type(self).__qualname__) await self.client_session.close() - def __enter__(self) -> typing.NoReturn: - raise RuntimeError(f"Please use 'async with' instead of 'with' for {type(self).__name__}") - - async def __aenter__(self): + async def __aenter__(self) -> "LowLevelRestfulClient": return self - async def __aexit__(self, exc_type, exc_val, exc_tb): + async def __aexit__( + self, exc_type: typing.Type[BaseException], exc_val: BaseException, exc_tb: types.TracebackType + ) -> None: await self.close() async def _request( @@ -471,7 +471,7 @@ async def get_gateway_bot(self) -> typing.Dict[str, typing.Any]: return await self._request(routes.GATEWAY_BOT.compile(self.GET)) async def get_guild_audit_log( - self, guild_id: str, *, user_id: str = ..., action_type: int = ..., limit: int = ..., + self, guild_id: str, *, user_id: str = ..., action_type: int = ..., limit: int = ..., before: str = ... ) -> typing.Dict: """Get an audit log object for the given guild. @@ -486,6 +486,9 @@ async def get_guild_audit_log( limit : :obj:`int` If specified, the limit to apply to the number of records. Defaults to ``50``. Must be between ``1`` and ``100`` inclusive. + before : :obj:`str` + If specified, the ID of the entry that all retrieved entries will + have occurred before. Returns ------- @@ -503,6 +506,7 @@ async def get_guild_audit_log( conversions.put_if_specified(query, "user_id", user_id) conversions.put_if_specified(query, "action_type", action_type) conversions.put_if_specified(query, "limit", limit) + conversions.put_if_specified(query, "before", before) route = routes.GUILD_AUDIT_LOGS.compile(self.GET, guild_id=guild_id) return await self._request(route, query=query) @@ -551,7 +555,7 @@ async def modify_channel( # lgtm [py/similar-function] channel_id : :obj:`str` The channel ID to update. name : :obj:`str` - If specified, the new name for the channel.This must be + If specified, the new name for the channel. This must be between ``2`` and ``100`` characters in length. position : :obj:`int` If specified, the position to change the channel to. @@ -560,7 +564,7 @@ async def modify_channel( # lgtm [py/similar-function] text channels. This must be between ``0`` and ``1024`` characters in length. nsfw : :obj:`bool` - If specified, wheather the channel will be marked as NSFW. + If specified, whether the channel will be marked as NSFW. Only applicable to text channels. rate_limit_per_user : :obj:`int` If specified, the number of seconds the user has to wait before sending @@ -579,8 +583,9 @@ async def modify_channel( # lgtm [py/similar-function] permission_overwrites : :obj:`typing.Sequence` [ :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ] ] If specified, the new list of permission overwrites that are category specific to replace the existing overwrites with. - parent_id : :obj:`str` - If specified, the new parent category ID to set for the channel. + parent_id : :obj:`str`, optional + If specified, the new parent category ID to set for the channel., + pass :obj:`None` to unset. reason : :obj:`str` If specified, the audit log reason explaining why the operation was performed. @@ -619,7 +624,7 @@ async def delete_close_channel(self, channel_id: str) -> None: Parameters ---------- channel_id : :obj:`str` - The channel ID to delete, or the user ID of the direct message to close. + The channel ID to delete, or direct message channel to close. Returns ------- @@ -646,8 +651,6 @@ async def get_channel_messages( ) -> typing.Sequence[typing.Dict[str, typing.Any]]: """Retrieve message history for a given channel. - If a user is provided, retrieve the DM history. - Parameters ---------- channel_id : :obj:`str` @@ -661,7 +664,8 @@ async def get_channel_messages( before : :obj:`str` A message ID. If specified, only return messages sent BEFORE this message. around : :obj:`str` - A message ID. If specified, only return messages sent AROUND this message. + A message ID. If specified, only return messages sent AROUND and + including (if it still exists) this message. Returns ------- @@ -744,7 +748,7 @@ async def create_message( Parameters ---------- channel_id : :obj:`str` - The channel or user ID to send to. + The ID of the channel to send to. content : :obj:`str` If specified, the message content to send with the message. nonce : :obj:`str` @@ -806,18 +810,18 @@ async def create_message( return await self._request(route, form_body=form, re_seekable_resources=re_seekable_resources) async def create_reaction(self, channel_id: str, message_id: str, emoji: str) -> None: - """Add a reaction to the given message in the given channel or user DM. + """Add a reaction to the given message in the given channel. Parameters ---------- channel_id : :obj:`str` - The ID of the channel to get the message from. + The ID of the channel to add this reaction in. message_id : :obj:`str` The ID of the message to add the reaction in. emoji : :obj:`str` The emoji to add. This can either be a series of unicode - characters making up a valid Discord emoji, or it can be a - snowflake ID for a custom emoji. + characters making up a valid Discord emoji, or it can be a the url + representation of a custom emoji ``<{emoji.name}:{emoji.id}>``. Raises ------ @@ -834,7 +838,7 @@ async def create_reaction(self, channel_id: str, message_id: str, emoji: str) -> await self._request(route) async def delete_own_reaction(self, channel_id: str, message_id: str, emoji: str) -> None: - """Remove a reaction you made using a given emoji from a given message in a given channel or user DM. + """Remove your own reaction from the given message in the given channel. Parameters ---------- @@ -858,7 +862,7 @@ async def delete_own_reaction(self, channel_id: str, message_id: str, emoji: str await self._request(route) async def delete_all_reactions_for_emoji(self, channel_id: str, message_id: str, emoji: str) -> None: - """Remove all reactions for a single given emoji on a given message in a given channel or user DM. + """Remove all reactions for a single given emoji on a given message in a given channel. Parameters ---------- @@ -882,7 +886,7 @@ async def delete_all_reactions_for_emoji(self, channel_id: str, message_id: str, await self._request(route) async def delete_user_reaction(self, channel_id: str, message_id: str, emoji: str, user_id: str) -> None: - """Remove a reaction made by a given user using a given emoji on a given message in a given channel or user DM. + """Remove a reaction made by a given user using a given emoji on a given message in a given channel. Parameters ---------- @@ -926,7 +930,7 @@ async def get_reactions( snowflake ID for a custom emoji. after : :obj:`str` If specified, the user ID. If specified, only users with a snowflake - that is lexicographically greater thanthe value will be returned. + that is lexicographically greater than the value will be returned. limit : :obj:`str` If specified, the limit of the number of values to return. Must be between ``1`` and ``100`` inclusive. If unspecified, @@ -957,7 +961,7 @@ async def delete_all_reactions(self, channel_id: str, message_id: str) -> None: ---------- channel_id : :obj:`str` The ID of the channel to get the message from. - message_id: + message_id : :obj:`str` The ID of the message to remove all reactions from. Raises @@ -978,6 +982,7 @@ async def edit_message( content: typing.Optional[str] = ..., embed: typing.Optional[typing.Dict[str, typing.Any]] = ..., flags: int = ..., + allowed_mentions: typing.Dict[str, typing.Any] = ..., ) -> typing.Dict[str, typing.Any]: """Update the given message. @@ -995,6 +1000,9 @@ async def edit_message( If ``None``, the embed will be removed from the message. flags : :obj:`int` If specified, the integer to replace the message's current flags. + allowed_mentions : :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ] + If specified, the mentions to parse from the ``content``. + If not specified, will parse all mentions from the ``content``. Returns ------- @@ -1012,13 +1020,16 @@ async def edit_message( are specified. parse only. :obj:`hikari.errors.ForbiddenHTTPError` - If you try to edit content or embed on a message you did not author or try to edit the flags - on a message you did not author without the ``MANAGE_MESSAGES`` permission. + If you try to edit ``content`` or ``embed`` or ``allowed_mentions` + on a message you did not author or try to edit the flags on a + message you did not author without the ``MANAGE_MESSAGES`` + permission. """ payload = {} conversions.put_if_specified(payload, "content", content) conversions.put_if_specified(payload, "embed", embed) conversions.put_if_specified(payload, "flags", flags) + conversions.put_if_specified(payload, "allowed_mentions", allowed_mentions) route = routes.CHANNEL_MESSAGE.compile(self.PATCH, channel_id=channel_id, message_id=message_id) return await self._request(route, json_body=payload) @@ -1076,14 +1087,7 @@ async def bulk_delete_messages(self, channel_id: str, messages: typing.Sequence[ await self._request(route, json_body=payload) async def edit_channel_permissions( - self, - channel_id: str, - overwrite_id: str, - *, - allow: int = ..., - deny: int = ..., - type_: str = ..., - reason: str = ..., + self, channel_id: str, overwrite_id: str, type_: str, *, allow: int = ..., deny: int = ..., reason: str = ..., ) -> None: """Edit permissions for a given channel. @@ -1093,13 +1097,13 @@ async def edit_channel_permissions( The ID of the channel to edit permissions for. overwrite_id : :obj:`str` The overwrite ID to edit. + type_ : :obj:`str` + The type of overwrite. ``"member"`` if it is for a member, + or ``"role"`` if it is for a role. allow : :obj:`int` If specified, the bitwise value of all permissions to set to be allowed. deny : :obj:`int` If specified, the bitwise value of all permissions to set to be denied. - type_ : :obj:`str` - If specified, the type of overwrite. ``"member"`` if it is for a member, - or ``"role"`` if it is for a role. reason : :obj:`str` If specified, the audit log reason explaining why the operation was performed. @@ -1111,10 +1115,9 @@ async def edit_channel_permissions( :obj:`hikari.errors.ForbiddenHTTPError` If you lack permission to do this. """ - payload = {} + payload = {"type": type_} conversions.put_if_specified(payload, "allow", allow) conversions.put_if_specified(payload, "deny", deny) - conversions.put_if_specified(payload, "type", type_) route = routes.CHANNEL_PERMISSIONS.compile(self.PATCH, channel_id=channel_id, overwrite_id=overwrite_id) await self._request(route, json_body=payload, reason=reason) @@ -1164,11 +1167,11 @@ async def create_channel_invite( ``86400`` (``24`` hours). Set to ``0`` to never expire. max_uses : :obj:`int` - If specified, the max number of uses this invite can have, or ``0`` for - unlimited (as per the default). + If specified, the max number of uses this invite can have, or ``0`` + for unlimited (as per the default). temporary : :obj:`bool` - If specified, whether to grant temporary membership, meaning the user - is kicked when their session ends unless they are given a role. + If specified, whether to grant temporary membership, meaning the + user is kicked when their session ends unless they are given a role. unique : :obj:`bool` If specified, whether to try to reuse a similar invite. target_user : :obj:`str` @@ -1209,7 +1212,7 @@ async def delete_channel_permission(self, channel_id: str, overwrite_id: str) -> Parameters ---------- channel_id : :obj:`str` - The ID of the channel to delete the overwire from. + The ID of the channel to delete the overwrite from. overwrite_id : :obj:`str` The ID of the overwrite to remove. @@ -1229,8 +1232,7 @@ async def trigger_typing_indicator(self, channel_id: str) -> None: Parameters ---------- channel_id : :obj:`str` - The ID of the channel to appear to be typing in. This may be - a user ID if you wish to appear to be typing in DMs. + The ID of the channel to appear to be typing in. Raises ------ @@ -1413,9 +1415,9 @@ async def modify_guild_emoji( Parameters ---------- guild_id : :obj:`str` - The ID of the guild to which the edited emoji belongs to. + The ID of the guild to which the emoji to update belongs to. emoji_id : :obj:`str` - The ID of the edited emoji. + The ID of the emoji to update. name : :obj:`str` If specified, a new emoji name string. Keep unspecified to keep the name the same. roles : :obj:`typing.Sequence` [ :obj:`str` ] @@ -1649,7 +1651,7 @@ async def delete_guild(self, guild_id: str) -> None: route = routes.GUILD.compile(self.DELETE, guild_id=guild_id) await self._request(route) - async def get_guild_channels(self, guild_id: str) -> typing.Sequence[typing.Dict[str, typing.Any]]: + async def list_guild_channels(self, guild_id: str) -> typing.Sequence[typing.Dict[str, typing.Any]]: """Get all the channels for a given guild. Parameters @@ -1692,7 +1694,7 @@ async def create_guild_channel( Parameters ---------- - guild_id: + guild_id : :obj:`str` The ID of the guild to create the channel in. name : :obj:`str` If specified, the name for the channel.This must be @@ -1832,19 +1834,19 @@ async def list_guild_members( Example ------- - .. code-block:: python + .. code-block:: python - members = [] - last_id = 0 + members = [] + last_id = 0 - while True: - next_members = await client.list_guild_members(1234567890, limit=1000, after=last_id) - members += next_members + while True: + next_members = await client.list_guild_members(1234567890, limit=1000, after=last_id) + members += next_members - if len(next_members) == 1000: - last_id = next_members[-1] - else: - break + if len(next_members) == 1000: + last_id = next_members[-1]["user"]["id"] + else: + break Returns ------- @@ -1886,10 +1888,10 @@ async def modify_guild_member( # lgtm [py/similar-function] The ID of the guild to edit the member from. user_id : :obj:`str` The ID of the member to edit. - nick : :obj:`str` + nick : :obj:`str`, optional If specified, the new nickname string. Setting it to ``None`` explicitly will clear the nickname. - roles : :obj:`str` + roles : :obj:`typing.Sequence` [ :obj:`str` ] If specified, a list of role IDs the member should have. mute : :obj:`bool` If specified, whether the user should be muted in the voice channel or not. @@ -2208,12 +2210,14 @@ async def modify_guild_role_positions( Parameters ---------- - guild_id: + guild_id : str The ID of the guild the roles belong to. - role: - The first role to move. - *roles: - Optional extra roles to move. + role : :obj:`typing.Tuple` [ :obj:`str`, :obj:`int` ] + The first role to move. This is a tuple of the role ID and the + integer position. + *roles : :obj:`typing.Tuple` [ :obj:`str`, :obj:`int` ] + Optional extra roles to move. These must be tuples of the role ID + and the integer position. Returns ------- @@ -2433,7 +2437,7 @@ async def get_guild_integrations(self, guild_id: str) -> typing.Sequence[typing. Parameters ---------- - guild_id: + guild_id : :obj:`int` The ID of the guild to get the integrations for. Returns @@ -2451,39 +2455,6 @@ async def get_guild_integrations(self, guild_id: str) -> typing.Sequence[typing. route = routes.GUILD_INTEGRATIONS.compile(self.GET, guild_id=guild_id) return await self._request(route) - async def create_guild_integration( - self, guild_id: str, type_: str, integration_id: str, *, reason: str = ..., - ) -> typing.Dict[str, typing.Any]: - """Create an integrations for a given guild. - - Parameters - ---------- - guild_id : :obj:`str` - The ID of the guild to create the integrations in. - type_ : :obj:`str` - The integration type string (e.g. "twitch" or "youtube"). - integration_id : :obj:`str` - The ID for the new integration. - reason : :obj:`str` - If specified, the audit log reason explaining why the operation - was performed. - - Returns - ------- - :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ] - The newly created integration object. - - Raises - ------ - :obj:`hikari.errors.NotFoundHTTPError` - If the guild is not found. - :obj:`hikari.errors.ForbiddenHTTPError` - If you lack the ``MANAGE_GUILD`` permission or are not in the guild. - """ - payload = {"type": type_, "id": integration_id} - route = routes.GUILD_INTEGRATIONS.compile(self.POST, guild_id=guild_id) - return await self._request(route, json_body=payload, reason=reason) - async def modify_guild_integration( self, guild_id: str, @@ -2529,7 +2500,7 @@ async def modify_guild_integration( route = routes.GUILD_INTEGRATION.compile(self.PATCH, guild_id=guild_id, integration_id=integration_id) await self._request(route, json_body=payload, reason=reason) - async def delete_guild_integration(self, guild_id: str, integration_id: str, *, reason: str = ...,) -> None: + async def delete_guild_integration(self, guild_id: str, integration_id: str, *, reason: str = ...) -> None: """Delete an integration for the given guild. Parameters @@ -2596,7 +2567,7 @@ async def get_guild_embed(self, guild_id: str) -> typing.Dict[str, typing.Any]: return await self._request(route) async def modify_guild_embed( - self, guild_id: str, embed: typing.Dict[str, typing.Any], *, reason: str = ..., + self, guild_id: str, *, channel_id: typing.Optional[str] = ..., enabled: bool = ..., reason: str = ..., ) -> typing.Dict[str, typing.Any]: """Edit the embed for a given guild. @@ -2604,8 +2575,11 @@ async def modify_guild_embed( ---------- guild_id : :obj:`str` The ID of the guild to edit the embed for. - embed : :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ] - The new embed object to be set. + channel_id : :obj:`str`, optional + If specified, the channel that this embed's invite should target. + Set to ``None`` to disable invites for this embed. + enabled : :obj:`bool` + If specified, whether this embed should be enabled. reason : :obj:`str` If specified, the audit log reason explaining why the operation was performed. @@ -2622,8 +2596,11 @@ async def modify_guild_embed( :obj:`hikari.errors.ForbiddenHTTPError` If you either lack the ``MANAGE_GUILD`` permission or are not in the guild. """ + payload = {} + conversions.put_if_specified(payload, "channel_id", channel_id) + conversions.put_if_specified(payload, "enabled", enabled) route = routes.GUILD_EMBED.compile(self.PATCH, guild_id=guild_id) - return await self._request(route, json_body=embed, reason=reason) + return await self._request(route, json_body=payload, reason=reason) async def get_guild_vanity_url(self, guild_id: str) -> typing.Dict[str, typing.Any]: """Get the vanity URL for a given guild. @@ -2680,10 +2657,10 @@ async def get_invite(self, invite_code: str, *, with_counts: bool = ...) -> typi Parameters ---------- - invite_code : :str: + invite_code : :obj:`str` The ID for wanted invite. - with_counts : :bool: - If specified, wheter to attempt to count the number of + with_counts : :obj:`bool` + If specified, whether to attempt to count the number of times the invite has been used. Returns @@ -2767,9 +2744,9 @@ async def modify_current_user( ---------- username : :obj:`str` If specified, the new username string. - avatar : :obj:`bytes` + avatar : :obj:`bytes`, optional If specified, the new avatar image in bytes form. - If it is ``None``, the avatar is removed. + If it is :obj:`None`, the avatar is removed. Returns ------- @@ -3104,7 +3081,7 @@ async def execute_webhook( embeds: typing.Sequence[typing.Dict[str, typing.Any]] = ..., allowed_mentions: typing.Dict[str, typing.Any] = ..., ) -> typing.Optional[typing.Dict[str, typing.Any]]: - """Create a message in the given channel or DM. + """Execute a webhook to create a message in its channel. Parameters ---------- @@ -3136,6 +3113,11 @@ async def execute_webhook( If specified, the mentions to parse from the ``content``. If not specified, will parse all mentions from the ``content``. + Returns + ------- + :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ], optional + The created message object if ``wait`` is ``True``, else ``None``. + Raises ------ :obj:`hikari.errors.NotFoundHTTPError` @@ -3152,11 +3134,6 @@ async def execute_webhook( If you lack permissions to send to this channel. :obj:`hikari.errors.UnauthorizedHTTPError` If you pass a token that's invalid for the target webhook. - - Returns - ------- - :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ], optional - The created message object if ``wait`` is ``True``, else ``None``. """ form = aiohttp.FormData() diff --git a/hikari/snowflakes.py b/hikari/snowflakes.py index c18296a03d..44b866c76a 100644 --- a/hikari/snowflakes.py +++ b/hikari/snowflakes.py @@ -22,7 +22,7 @@ on the server. """ -__all__ = ["Snowflake", "UniqueEntity"] +__all__ = ["Snowflake", "UniqueEntity", "HashableT"] import datetime import functools @@ -30,9 +30,9 @@ import attr -from hikari import entities from hikari.internal import conversions from hikari.internal import marshaller +from hikari import entities @functools.total_ordering @@ -101,13 +101,30 @@ def deserialize(cls, value: str) -> "Snowflake": """Take a :obj:`str` ID and convert it into a Snowflake object.""" return cls(value) + @classmethod + def from_datetime(cls, date: datetime.datetime) -> "Snowflake": + """Get a snowflake object from a datetime object.""" + return cls.from_timestamp(date.timestamp()) + + @classmethod + def from_timestamp(cls, timestamp: float) -> "Snowflake": + """Get a snowflake object from a seconds timestamp.""" + return cls(int(timestamp - conversions.DISCORD_EPOCH) * 1000 << 22) + @marshaller.marshallable() @attr.s(slots=True) -class UniqueEntity(entities.HikariEntity): +class UniqueEntity(entities.HikariEntity, typing.SupportsInt): """An entity that has an integer ID of some sort.""" #: The ID of this entity. #: #: :type: :obj:`Snowflake` id: Snowflake = marshaller.attrib(hash=True, eq=True, repr=True, deserializer=Snowflake, serializer=str) + + def __int__(self): + return int(self.id) + + +T = typing.TypeVar("T", bound=UniqueEntity) +HashableT = typing.Union[Snowflake, int, T] diff --git a/hikari/voices.py b/hikari/voices.py index 2f8cb28545..e02cb9e8d4 100644 --- a/hikari/voices.py +++ b/hikari/voices.py @@ -41,10 +41,13 @@ class VoiceState(entities.HikariEntity, entities.Deserializable): deserializer=snowflakes.Snowflake.deserialize, if_undefined=None ) - #: The ID of the channel this user is connected to. + #: The ID of the channel this user is connected to, will be :obj:`None` if + #: they are leaving voice. #: #: :type: :obj:`hikari.snowflakes.Snowflake`, optional - channel_id: snowflakes.Snowflake = marshaller.attrib(deserializer=snowflakes.Snowflake.deserialize, if_none=None) + channel_id: typing.Optional[snowflakes.Snowflake] = marshaller.attrib( + deserializer=snowflakes.Snowflake.deserialize, if_none=None + ) #: The ID of the user this voice state is for. #: diff --git a/pylint.ini b/pylint.ini index 9d546463a4..67eed34fa8 100644 --- a/pylint.ini +++ b/pylint.ini @@ -90,6 +90,7 @@ disable=bad-continuation, input-builtin, invalid-name, keyword-arg-before-vararg, + line-too-long, long-suffix, map-builtin-not-iterating, metaclass-assignment, diff --git a/requirements.txt b/requirements.txt index 7a9f55048d..b35792b9b3 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,4 @@ +aiofiles==0.4.0 aiohttp==3.6.2 attrs==19.3.0 click==7.1.1 diff --git a/tests/hikari/_helpers.py b/tests/hikari/_helpers.py index 5e185310e3..e9ef08332d 100644 --- a/tests/hikari/_helpers.py +++ b/tests/hikari/_helpers.py @@ -35,6 +35,7 @@ import cymock as mock import pytest +from hikari import snowflakes from hikari.internal import marshaller _LOGGER = logging.getLogger(__name__) @@ -253,8 +254,8 @@ def parametrize_valid_id_formats_for_models(param_name, id, model_type1, *model_ ... "guild", ... [ ... 1234, - ... "1234", - ... mock_model(guilds.Guild, id=1234, unavailable=False) + ... snowflakes.Snowflake(1234), + ... mock_model(guilds.Guild, id=snowflakes.Snowflake(1234), unavailable=False) ... ], ... id=lambda ...: ... ... ) @@ -266,13 +267,13 @@ def parametrize_valid_id_formats_for_models(param_name, id, model_type1, *model_ def decorator(func): mock_models = [] for model_type in model_types: - assert "SnowflakeMixin" in map( + assert "UniqueEntity" in map( lambda mro: mro.__name__, model_type.mro() - ), "model must be an SnowflakeMixin derivative" - mock_models.append(mock_model(model_type, id=int(id), **kwargs)) + ), "model must be an UniqueEntity derivative" + mock_models.append(mock_model(model_type, id=snowflakes.Snowflake(id), **kwargs)) return pytest.mark.parametrize( - param_name, [str(id), int(id), *mock_models], ids=_parameterize_ids_id(param_name) + param_name, [int(id), snowflakes.Snowflake(id), *mock_models], ids=_parameterize_ids_id(param_name) )(func) return decorator diff --git a/tests/hikari/clients/test_rest_client.py b/tests/hikari/clients/test_rest_client.py new file mode 100644 index 0000000000..d9f44d0975 --- /dev/null +++ b/tests/hikari/clients/test_rest_client.py @@ -0,0 +1,2743 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# Copyright © Nekoka.tt 2019-2020 +# +# This file is part of Hikari. +# +# Hikari is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Hikari is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along ith Hikari. If not, see . +import datetime +import io + +import cymock as mock +import datetime +import pytest + + +from hikari.internal import conversions +from hikari.clients import configs +from hikari.clients import rest_clients +from hikari.net import rest +from hikari import audit_logs +from hikari import channels +from hikari import colors +from hikari import embeds +from hikari import emojis +from hikari import gateway_entities +from hikari import guilds +from hikari import invites +from hikari import media +from hikari import messages +from hikari import oauth2 +from hikari import permissions +from hikari import snowflakes +from hikari import users +from hikari import voices +from hikari import webhooks + +from tests.hikari import _helpers + + +def test__get_member_id(): + member = mock.create_autospec( + guilds.GuildMember, user=mock.create_autospec(users.User, id=123123123, __int__=users.User.__int__) + ) + assert rest_clients._get_member_id(member) == "123123123" + + +class TestRESTClient: + @pytest.fixture() + def mock_config(self): + # Mocking the Configs leads to attribute errors regardless of spec set. + return configs.RESTConfig(token="blah.blah.blah") + + def test_init(self, mock_config): + mock_low_level_rest_clients = mock.MagicMock(rest.LowLevelRestfulClient) + with mock.patch.object(rest, "LowLevelRestfulClient", return_value=mock_low_level_rest_clients) as patched_init: + cli = rest_clients.RESTClient(mock_config) + patched_init.assert_called_once_with( + allow_redirects=mock_config.allow_redirects, + connector=mock_config.tcp_connector, + proxy_headers=mock_config.proxy_headers, + proxy_auth=mock_config.proxy_auth, + ssl_context=mock_config.ssl_context, + verify_ssl=mock_config.verify_ssl, + timeout=mock_config.request_timeout, + token=mock_config.token, + version=mock_config.rest_version, + ) + assert cli._session is mock_low_level_rest_clients + + @pytest.fixture() + def low_level_rest_impl(self) -> rest.LowLevelRestfulClient: + return mock.create_autospec(rest.LowLevelRestfulClient, auto_spec=True) + + @pytest.fixture() + def rest_clients_impl(self, low_level_rest_impl) -> rest_clients.RESTClient: + class RESTClient(rest_clients.RESTClient): + def __init__(self): + self._session: rest.LowLevelRestfulClient = low_level_rest_impl + + return RESTClient() + + @pytest.mark.asyncio + async def test_close_awaits_session_close(self, rest_clients_impl): + await rest_clients_impl.close() + rest_clients_impl._session.close.assert_called_once() + + @pytest.mark.asyncio + async def test___aenter___and___aexit__(self, rest_clients_impl): + rest_clients_impl.close = mock.AsyncMock() + async with rest_clients_impl as client: + assert client is rest_clients_impl + rest_clients_impl.close.assert_called_once_with() + + @pytest.mark.asyncio + async def test_fetch_gateway_url(self, rest_clients_impl): + mock_url = "wss://gateway.discord.gg/" + rest_clients_impl._session.get_gateway.return_value = mock_url + assert await rest_clients_impl.fetch_gateway_url() == mock_url + rest_clients_impl._session.get_gateway.assert_called_once() + + @pytest.mark.asyncio + async def test_fetch_gateway_bot(self, rest_clients_impl): + mock_payload = {"url": "wss://gateway.discord.gg/", "shards": 9, "session_start_limit": {}} + mock_gateway_bot_obj = mock.MagicMock(gateway_entities.GatewayBot) + rest_clients_impl._session.get_gateway_bot.return_value = mock_payload + with mock.patch.object(gateway_entities.GatewayBot, "deserialize", return_value=mock_gateway_bot_obj): + assert await rest_clients_impl.fetch_gateway_bot() is mock_gateway_bot_obj + rest_clients_impl._session.get_gateway_bot.assert_called_once() + gateway_entities.GatewayBot.deserialize.assert_called_once_with(mock_payload) + + @_helpers.parametrize_valid_id_formats_for_models("guild", 379953393319542784, guilds.Guild) + @_helpers.parametrize_valid_id_formats_for_models("user", 22222222, users.User) + @_helpers.parametrize_valid_id_formats_for_models("before", 123123123123, audit_logs.AuditLogEntry) + def test_fetch_audit_log_entries_before_with_optionals(self, rest_clients_impl, guild, before, user): + mock_audit_log_iterator = mock.MagicMock(audit_logs.AuditLogIterator) + with mock.patch.object(audit_logs, "AuditLogIterator", return_value=mock_audit_log_iterator): + result = rest_clients_impl.fetch_audit_log_entries_before( + guild, before=before, user=user, action_type=audit_logs.AuditLogEventType.MEMBER_MOVE, limit=42, + ) + assert result is mock_audit_log_iterator + audit_logs.AuditLogIterator.assert_called_once_with( + guild_id="379953393319542784", + request=rest_clients_impl._session.get_guild_audit_log, + before="123123123123", + user_id="22222222", + action_type=26, + limit=42, + ) + + @_helpers.parametrize_valid_id_formats_for_models("guild", 379953393319542784, guilds.Guild) + def test_fetch_audit_log_entries_before_without_optionals(self, rest_clients_impl, guild): + mock_audit_log_iterator = mock.MagicMock(audit_logs.AuditLogIterator) + with mock.patch.object(audit_logs, "AuditLogIterator", return_value=mock_audit_log_iterator): + assert rest_clients_impl.fetch_audit_log_entries_before(guild) is mock_audit_log_iterator + audit_logs.AuditLogIterator.assert_called_once_with( + guild_id="379953393319542784", + request=rest_clients_impl._session.get_guild_audit_log, + before=None, + user_id=..., + action_type=..., + limit=None, + ) + + def test_fetch_audit_log_entries_before_with_datetime_object(self, rest_clients_impl): + mock_audit_log_iterator = mock.MagicMock(audit_logs.AuditLogIterator) + with mock.patch.object(audit_logs, "AuditLogIterator", return_value=mock_audit_log_iterator): + date = datetime.datetime(2019, 1, 22, 18, 41, 15, 283_000, tzinfo=datetime.timezone.utc) + result = rest_clients_impl.fetch_audit_log_entries_before(123123123, before=date) + assert result is mock_audit_log_iterator + audit_logs.AuditLogIterator.assert_called_once_with( + guild_id="123123123", + request=rest_clients_impl._session.get_guild_audit_log, + before="537340988620800000", + user_id=..., + action_type=..., + limit=None, + ) + + @pytest.mark.asyncio + @_helpers.parametrize_valid_id_formats_for_models("guild", 379953393319542784, guilds.Guild) + @_helpers.parametrize_valid_id_formats_for_models("user", 115590097100865541, users.User) + @_helpers.parametrize_valid_id_formats_for_models("before", 1231231123, audit_logs.AuditLogEntry) + async def test_fetch_audit_log_with_optionals(self, rest_clients_impl, guild, user, before): + mock_audit_log_payload = {"entries": [], "integrations": [], "webhooks": [], "users": []} + mock_audit_log_obj = mock.MagicMock(audit_logs.AuditLog) + rest_clients_impl._session.get_guild_audit_log.return_value = mock_audit_log_payload + with mock.patch.object(audit_logs.AuditLog, "deserialize", return_value=mock_audit_log_obj): + result = await rest_clients_impl.fetch_audit_log( + guild, user=user, action_type=audit_logs.AuditLogEventType.MEMBER_MOVE, limit=100, before=before, + ) + assert result is mock_audit_log_obj + rest_clients_impl._session.get_guild_audit_log.assert_called_once_with( + guild_id="379953393319542784", + user_id="115590097100865541", + action_type=26, + limit=100, + before="1231231123", + ) + + @pytest.mark.asyncio + @_helpers.parametrize_valid_id_formats_for_models("guild", 379953393319542784, guilds.Guild) + async def test_fetch_audit_log_without_optionals(self, rest_clients_impl, guild): + mock_audit_log_payload = {"entries": [], "integrations": [], "webhooks": [], "users": []} + mock_audit_log_obj = mock.MagicMock(audit_logs.AuditLog) + rest_clients_impl._session.get_guild_audit_log.return_value = mock_audit_log_payload + with mock.patch.object(audit_logs.AuditLog, "deserialize", return_value=mock_audit_log_obj): + assert await rest_clients_impl.fetch_audit_log(guild) is mock_audit_log_obj + rest_clients_impl._session.get_guild_audit_log.assert_called_once_with( + guild_id="379953393319542784", user_id=..., action_type=..., limit=..., before=... + ) + + @pytest.mark.asyncio + @_helpers.parametrize_valid_id_formats_for_models("guild", 379953393319542784, guilds.Guild) + async def test_fetch_audit_log_handles_datetime_object(self, rest_clients_impl, guild): + mock_audit_log_payload = {"entries": [], "integrations": [], "webhooks": [], "users": []} + mock_audit_log_obj = mock.MagicMock(audit_logs.AuditLog) + rest_clients_impl._session.get_guild_audit_log.return_value = mock_audit_log_payload + date = datetime.datetime(2019, 1, 22, 18, 41, 15, 283_000, tzinfo=datetime.timezone.utc) + with mock.patch.object(audit_logs.AuditLog, "deserialize", return_value=mock_audit_log_obj): + assert await rest_clients_impl.fetch_audit_log(guild, before=date) is mock_audit_log_obj + rest_clients_impl._session.get_guild_audit_log.assert_called_once_with( + guild_id="379953393319542784", user_id=..., action_type=..., limit=..., before="537340988620800000" + ) + + @pytest.mark.asyncio + @_helpers.parametrize_valid_id_formats_for_models("channel", 1234, channels.Channel) + async def test_fetch_channel(self, rest_clients_impl, channel): + mock_payload = {"id": "49494994", "type": 3} + mock_channel_obj = mock.MagicMock(channels.Channel) + rest_clients_impl._session.get_channel.return_value = mock_payload + with mock.patch.object(channels, "deserialize_channel", return_value=mock_channel_obj): + assert await rest_clients_impl.fetch_channel(channel) is mock_channel_obj + rest_clients_impl._session.get_channel.assert_called_once_with(channel_id="1234") + channels.deserialize_channel.assert_called_once_with(mock_payload) + + @pytest.mark.asyncio + @_helpers.parametrize_valid_id_formats_for_models("channel", 379953393319542784, channels.Channel) + @_helpers.parametrize_valid_id_formats_for_models("parent_channel", 115590097100865541, channels.Channel) + @pytest.mark.parametrize("rate_limit_per_user", [42, datetime.timedelta(seconds=42)]) + async def test_update_channel_with_optionals(self, rest_clients_impl, channel, parent_channel, rate_limit_per_user): + mock_payload = {"name": "Qts", "type": 2} + mock_channel_obj = mock.MagicMock(channels.Channel) + mock_overwrite_payload = {"type": "user", "id": 543543543} + mock_overwrite_obj = mock.create_autospec( + channels.PermissionOverwrite, serialize=mock.MagicMock(return_value=mock_overwrite_payload) + ) + rest_clients_impl._session.modify_channel.return_value = mock_payload + with mock.patch.object(channels, "deserialize_channel", return_value=mock_channel_obj): + result = await rest_clients_impl.update_channel( + channel=channel, + name="ohNo", + position=7, + topic="camelsAreGreat", + nsfw=True, + bitrate=32000, + user_limit=42, + rate_limit_per_user=rate_limit_per_user, + permission_overwrites=[mock_overwrite_obj], + parent_category=parent_channel, + reason="Get Nyaa'd.", + ) + assert result is mock_channel_obj + rest_clients_impl._session.modify_channel.assert_called_once_with( + channel_id="379953393319542784", + name="ohNo", + position=7, + topic="camelsAreGreat", + nsfw=True, + rate_limit_per_user=42, + bitrate=32000, + user_limit=42, + permission_overwrites=[mock_overwrite_payload], + parent_id="115590097100865541", + reason="Get Nyaa'd.", + ) + mock_overwrite_obj.serialize.assert_called_once() + channels.deserialize_channel.assert_called_once_with(mock_payload) + + @pytest.mark.asyncio + @_helpers.parametrize_valid_id_formats_for_models("channel", 379953393319542784, channels.Channel) + async def test_update_channel_without_optionals( + self, rest_clients_impl, channel, + ): + mock_payload = {"name": "Qts", "type": 2} + mock_channel_obj = mock.MagicMock(channels.Channel) + rest_clients_impl._session.modify_channel.return_value = mock_payload + with mock.patch.object(channels, "deserialize_channel", return_value=mock_channel_obj): + result = await rest_clients_impl.update_channel(channel=channel,) + assert result is mock_channel_obj + rest_clients_impl._session.modify_channel.assert_called_once_with( + channel_id="379953393319542784", + name=..., + position=..., + topic=..., + nsfw=..., + rate_limit_per_user=..., + bitrate=..., + user_limit=..., + permission_overwrites=..., + parent_id=..., + reason=..., + ) + channels.deserialize_channel.assert_called_once_with(mock_payload) + + @pytest.mark.asyncio + @_helpers.parametrize_valid_id_formats_for_models("channel", 55555, channels.Channel) + async def test_delete_channel(self, rest_clients_impl, channel): + rest_clients_impl._session.delete_close_channel.return_value = ... + assert await rest_clients_impl.delete_channel(channel) is None + rest_clients_impl._session.delete_close_channel.assert_called_once_with(channel_id="55555") + + @_helpers.parametrize_valid_id_formats_for_models("channel", 123123123, channels.Channel) + @_helpers.parametrize_valid_id_formats_for_models("message", 777777777, messages.Message) + def test_fetch_messages_after_with_optionals(self, rest_clients_impl, channel, message): + mock_generator = mock.AsyncMock() + rest_clients_impl._pagination_handler = mock.MagicMock(return_value=mock_generator) + result = rest_clients_impl.fetch_messages_after(channel=channel, after=message, limit=52) + assert result is mock_generator + rest_clients_impl._pagination_handler.assert_called_once_with( + channel_id="123123123", + deserializer=messages.Message.deserialize, + direction="after", + start="777777777", + request=rest_clients_impl._session.get_channel_messages, + reversing=True, + limit=52, + ) + + @_helpers.parametrize_valid_id_formats_for_models("channel", 123123123, channels.Channel) + def test_fetch_messages_after_without_optionals(self, rest_clients_impl, channel): + mock_generator = mock.AsyncMock() + rest_clients_impl._pagination_handler = mock.MagicMock(return_value=mock_generator) + assert rest_clients_impl.fetch_messages_after(channel=channel) is mock_generator + rest_clients_impl._pagination_handler.assert_called_once_with( + channel_id="123123123", + deserializer=messages.Message.deserialize, + direction="after", + start="0", + request=rest_clients_impl._session.get_channel_messages, + reversing=True, + limit=None, + ) + + def test_fetch_messages_after_with_datetime_object(self, rest_clients_impl): + mock_generator = mock.AsyncMock() + rest_clients_impl._pagination_handler = mock.MagicMock(return_value=mock_generator) + date = datetime.datetime(2019, 1, 22, 18, 41, 15, 283_000, tzinfo=datetime.timezone.utc) + assert rest_clients_impl.fetch_messages_after(channel=123123123, after=date) is mock_generator + rest_clients_impl._pagination_handler.assert_called_once_with( + channel_id="123123123", + deserializer=messages.Message.deserialize, + direction="after", + start="537340988620800000", + request=rest_clients_impl._session.get_channel_messages, + reversing=True, + limit=None, + ) + + @_helpers.parametrize_valid_id_formats_for_models("channel", 123123123, channels.Channel) + @_helpers.parametrize_valid_id_formats_for_models("message", 777777777, messages.Message) + def test_fetch_messages_before_with_optionals(self, rest_clients_impl, channel, message): + mock_generator = mock.AsyncMock() + rest_clients_impl._pagination_handler = mock.MagicMock(return_value=mock_generator) + result = rest_clients_impl.fetch_messages_before(channel=channel, before=message, limit=52) + assert result is mock_generator + rest_clients_impl._pagination_handler.assert_called_once_with( + channel_id="123123123", + deserializer=messages.Message.deserialize, + direction="before", + start="777777777", + request=rest_clients_impl._session.get_channel_messages, + reversing=False, + limit=52, + ) + + @_helpers.parametrize_valid_id_formats_for_models("channel", 123123123, channels.Channel) + def test_fetch_messages_before_without_optionals(self, rest_clients_impl, channel): + mock_generator = mock.AsyncMock() + rest_clients_impl._pagination_handler = mock.MagicMock(return_value=mock_generator) + assert rest_clients_impl.fetch_messages_before(channel=channel) is mock_generator + rest_clients_impl._pagination_handler.assert_called_once_with( + channel_id="123123123", + deserializer=messages.Message.deserialize, + direction="before", + start=None, + request=rest_clients_impl._session.get_channel_messages, + reversing=False, + limit=None, + ) + + def test_fetch_messages_before_with_datetime_object(self, rest_clients_impl): + mock_generator = mock.AsyncMock() + rest_clients_impl._pagination_handler = mock.MagicMock(return_value=mock_generator) + date = datetime.datetime(2019, 1, 22, 18, 41, 15, 283_000, tzinfo=datetime.timezone.utc) + assert rest_clients_impl.fetch_messages_before(channel=123123123, before=date) is mock_generator + rest_clients_impl._pagination_handler.assert_called_once_with( + channel_id="123123123", + deserializer=messages.Message.deserialize, + direction="before", + start="537340988620800000", + request=rest_clients_impl._session.get_channel_messages, + reversing=False, + limit=None, + ) + + @pytest.mark.asyncio + @_helpers.parametrize_valid_id_formats_for_models("channel", 123123123, channels.Channel) + @_helpers.parametrize_valid_id_formats_for_models("message", 777777777, messages.Message) + async def test_fetch_messages_around_with_limit(self, rest_clients_impl, channel, message): + mock_message_payloads = [{"id": "202020", "content": "Nyaa"}, {"id": "2020222", "content": "Nyaa 2"}] + mock_message_objects = [mock.MagicMock(messages.Message), mock.MagicMock(messages.Message)] + rest_clients_impl._session.get_channel_messages.return_value = mock_message_payloads + with mock.patch.object(messages.Message, "deserialize", side_effect=mock_message_objects): + results = [] + async for result in rest_clients_impl.fetch_messages_around(channel, message, limit=2): + results.append(result) + assert results == mock_message_objects + messages.Message.deserialize.assert_has_calls( + [mock.call(mock_message_payloads[0]), mock.call(mock_message_payloads[1])] + ) + rest_clients_impl._session.get_channel_messages.assert_called_once_with( + channel_id="123123123", around="777777777", limit=2 + ) + + @pytest.mark.asyncio + @_helpers.parametrize_valid_id_formats_for_models("channel", 123123123, channels.Channel) + @_helpers.parametrize_valid_id_formats_for_models("message", 777777777, messages.Message) + async def test_fetch_messages_around_without_limit(self, rest_clients_impl, channel, message): + mock_message_payloads = [{"id": "202020", "content": "Nyaa"}, {"id": "2020222", "content": "Nyaa 2"}] + mock_message_objects = [mock.MagicMock(messages.Message), mock.MagicMock(messages.Message)] + rest_clients_impl._session.get_channel_messages.return_value = mock_message_payloads + with mock.patch.object(messages.Message, "deserialize", side_effect=mock_message_objects): + results = [] + async for result in rest_clients_impl.fetch_messages_around(channel, message): + results.append(result) + assert results == mock_message_objects + messages.Message.deserialize.assert_has_calls( + [mock.call(mock_message_payloads[0]), mock.call(mock_message_payloads[1])] + ) + rest_clients_impl._session.get_channel_messages.assert_called_once_with( + channel_id="123123123", around="777777777", limit=... + ) + + @pytest.mark.asyncio + async def test_fetch_messages_around_with_datetime_object(self, rest_clients_impl): + mock_message_payloads = [{"id": "202020", "content": "Nyaa"}, {"id": "2020222", "content": "Nyaa 2"}] + mock_message_objects = [mock.MagicMock(messages.Message), mock.MagicMock(messages.Message)] + rest_clients_impl._session.get_channel_messages.return_value = mock_message_payloads + date = datetime.datetime(2019, 1, 22, 18, 41, 15, 283_000, tzinfo=datetime.timezone.utc) + with mock.patch.object(messages.Message, "deserialize", side_effect=mock_message_objects): + results = [] + async for result in rest_clients_impl.fetch_messages_around(123123123, date): + results.append(result) + assert results == mock_message_objects + messages.Message.deserialize.assert_has_calls( + [mock.call(mock_message_payloads[0]), mock.call(mock_message_payloads[1])] + ) + rest_clients_impl._session.get_channel_messages.assert_called_once_with( + channel_id="123123123", around="537340988620800000", limit=... + ) + + @pytest.mark.asyncio + async def test__pagination_handler_ends_handles_empty_resource(self, rest_clients_impl): + mock_deserialize = mock.MagicMock() + mock_request = mock.AsyncMock(side_effect=[[]]) + async for _ in rest_clients_impl._pagination_handler( + random_kwarg="test", + deserializer=mock_deserialize, + direction="before", + request=mock_request, + reversing=True, + start="123123123", + limit=42, + ): + assert False, "Async generator shouldn't have yielded anything." + mock_request.assert_called_once_with( + limit=42, before="123123123", random_kwarg="test", + ) + mock_deserialize.assert_not_called() + + @pytest.mark.asyncio + async def test__pagination_handler_ends_without_limit_with_start(self, rest_clients_impl): + mock_payloads = [{"id": "312312312"}, {"id": "31231231"}, {"id": "20202020"}] + mock_models = [mock.MagicMock(), mock.MagicMock(), mock.MagicMock(id=20202020)] + mock_deserialize = mock.MagicMock(side_effect=mock_models) + mock_request = mock.AsyncMock(side_effect=[mock_payloads, []]) + results = [] + async for result in rest_clients_impl._pagination_handler( + random_kwarg="test", + deserializer=mock_deserialize, + direction="before", + request=mock_request, + reversing=True, + start="123123123", + limit=None, + ): + results.append(result) + assert results == mock_models + mock_request.assert_has_calls( + [ + mock.call(limit=100, before="123123123", random_kwarg="test"), + mock.call(limit=100, before="20202020", random_kwarg="test"), + ], + ) + mock_deserialize.assert_has_calls( + [mock.call({"id": "20202020"}), mock.call({"id": "31231231"}), mock.call({"id": "312312312"})] + ) + + @pytest.mark.asyncio + async def test__pagination_handler_ends_without_limit_without_start(self, rest_clients_impl): + mock_payloads = [{"id": "312312312"}, {"id": "31231231"}, {"id": "20202020"}] + mock_models = [mock.MagicMock(), mock.MagicMock(), mock.MagicMock(id=20202020)] + mock_deserialize = mock.MagicMock(side_effect=mock_models) + mock_request = mock.AsyncMock(side_effect=[mock_payloads, []]) + results = [] + async for result in rest_clients_impl._pagination_handler( + random_kwarg="test", + deserializer=mock_deserialize, + direction="before", + request=mock_request, + reversing=True, + start=None, + limit=None, + ): + results.append(result) + assert results == mock_models + mock_request.assert_has_calls( + [ + mock.call(limit=100, before=..., random_kwarg="test"), + mock.call(limit=100, before="20202020", random_kwarg="test"), + ], + ) + mock_deserialize.assert_has_calls( + [mock.call({"id": "20202020"}), mock.call({"id": "31231231"}), mock.call({"id": "312312312"})] + ) + + @pytest.mark.asyncio + async def test__pagination_handler_tracks_ends_when_hits_limit(self, rest_clients_impl): + mock_payloads = [{"id": "312312312"}, {"id": "31231231"}] + mock_models = [mock.MagicMock(), mock.MagicMock(id=20202020)] + mock_deserialize = mock.MagicMock(side_effect=mock_models) + mock_request = mock.AsyncMock(side_effect=[mock_payloads, []]) + results = [] + async for result in rest_clients_impl._pagination_handler( + random_kwarg="test", + deserializer=mock_deserialize, + direction="before", + request=mock_request, + reversing=False, + start=None, + limit=2, + ): + results.append(result) + assert results == mock_models + mock_request.assert_called_once_with(limit=2, before=..., random_kwarg="test") + mock_deserialize.assert_has_calls([mock.call({"id": "312312312"}), mock.call({"id": "31231231"})]) + + @pytest.mark.asyncio + async def test__pagination_handler_tracks_ends_when_limit_set_but_exhausts_requested_data(self, rest_clients_impl): + mock_payloads = [{"id": "312312312"}, {"id": "31231231"}, {"id": "20202020"}] + mock_models = [mock.MagicMock(), mock.MagicMock(), mock.MagicMock(id=20202020)] + mock_deserialize = mock.MagicMock(side_effect=mock_models) + mock_request = mock.AsyncMock(side_effect=[mock_payloads, []]) + results = [] + async for result in rest_clients_impl._pagination_handler( + random_kwarg="test", + deserializer=mock_deserialize, + direction="before", + request=mock_request, + reversing=False, + start=None, + limit=42, + ): + results.append(result) + assert results == mock_models + mock_request.assert_has_calls( + [ + mock.call(limit=42, before=..., random_kwarg="test"), + mock.call(limit=39, before="20202020", random_kwarg="test"), + ], + ) + mock_deserialize.assert_has_calls( + [mock.call({"id": "312312312"}), mock.call({"id": "31231231"}), mock.call({"id": "20202020"})] + ) + + @pytest.mark.asyncio + async def test__pagination_handler_reverses_data_when_reverse_is_true(self, rest_clients_impl): + mock_payloads = [{"id": "312312312"}, {"id": "31231231"}, {"id": "20202020"}] + mock_models = [mock.MagicMock(), mock.MagicMock(), mock.MagicMock(id=20202020)] + mock_deserialize = mock.MagicMock(side_effect=mock_models) + mock_request = mock.AsyncMock(side_effect=[mock_payloads, []]) + results = [] + async for result in rest_clients_impl._pagination_handler( + random_kwarg="test", + deserializer=mock_deserialize, + direction="before", + request=mock_request, + reversing=True, + start=None, + limit=None, + ): + results.append(result) + assert results == mock_models + mock_request.assert_has_calls( + [ + mock.call(limit=100, before=..., random_kwarg="test"), + mock.call(limit=100, before="20202020", random_kwarg="test"), + ], + ) + mock_deserialize.assert_has_calls( + [mock.call({"id": "20202020"}), mock.call({"id": "31231231"}), mock.call({"id": "312312312"})] + ) + + @pytest.mark.asyncio + async def test__pagination_handler_id_getter(self, rest_clients_impl): + mock_payloads = [{"id": "312312312"}, {"id": "20202020"}] + mock_models = [mock.MagicMock(), mock.MagicMock(user=mock.MagicMock(__int__=lambda x: 20202020))] + mock_deserialize = mock.MagicMock(side_effect=mock_models) + mock_request = mock.AsyncMock(side_effect=[mock_payloads, []]) + results = [] + async for result in rest_clients_impl._pagination_handler( + random_kwarg="test", + deserializer=mock_deserialize, + direction="before", + request=mock_request, + reversing=False, + start=None, + id_getter=lambda entity: str(int(entity.user)), + limit=None, + ): + results.append(result) + assert results == mock_models + mock_request.assert_has_calls( + [ + mock.call(limit=100, before=..., random_kwarg="test"), + mock.call(limit=100, before="20202020", random_kwarg="test"), + ], + ) + mock_deserialize.assert_has_calls([mock.call({"id": "312312312"}), mock.call({"id": "20202020"})]) + + @pytest.mark.asyncio + async def test__pagination_handler_handles_no_initial_data(self, rest_clients_impl): + mock_deserialize = mock.MagicMock() + mock_request = mock.AsyncMock(side_effect=[[]]) + async for _ in rest_clients_impl._pagination_handler( + random_kwarg="test", + deserializer=mock_deserialize, + direction="before", + request=mock_request, + reversing=True, + start=None, + limit=None, + ): + assert False, "Async generator shouldn't have yielded anything." + mock_request.assert_called_once_with( + limit=100, before=..., random_kwarg="test", + ) + mock_deserialize.assert_not_called() + + @pytest.mark.asyncio + @_helpers.parametrize_valid_id_formats_for_models("channel", 55555, channels.Channel) + @_helpers.parametrize_valid_id_formats_for_models("message", 565656, messages.Message) + async def test_fetch_message(self, rest_clients_impl, channel, message): + mock_payload = {"id": "9409404", "content": "I AM A MESSAGE!"} + mock_message_obj = mock.MagicMock(messages.Message) + rest_clients_impl._session.get_channel_message.return_value = mock_payload + with mock.patch.object(messages.Message, "deserialize", return_value=mock_message_obj): + assert await rest_clients_impl.fetch_message(channel=channel, message=message) is mock_message_obj + rest_clients_impl._session.get_channel_message.assert_called_once_with( + channel_id="55555", message_id="565656", + ) + messages.Message.deserialize.assert_called_once_with(mock_payload) + + @pytest.mark.parametrize( + ("kwargs", "expected_result"), + [ + ( + {"mentions_everyone": True, "user_mentions": True, "role_mentions": True}, + {"parse": ["everyone", "users", "roles"]}, + ), + ( + {"mentions_everyone": False, "user_mentions": False, "role_mentions": False}, + {"parse": [], "users": [], "roles": []}, + ), + ( + {"mentions_everyone": True, "user_mentions": ["1123123"], "role_mentions": True}, + {"parse": ["everyone", "roles"], "users": ["1123123"]}, + ), + ( + {"mentions_everyone": True, "user_mentions": True, "role_mentions": ["1231123"]}, + {"parse": ["everyone", "users"], "roles": ["1231123"]}, + ), + ( + {"mentions_everyone": False, "user_mentions": ["1123123"], "role_mentions": True}, + {"parse": ["roles"], "users": ["1123123"]}, + ), + ( + {"mentions_everyone": False, "user_mentions": True, "role_mentions": ["1231123"]}, + {"parse": ["users"], "roles": ["1231123"]}, + ), + ( + {"mentions_everyone": False, "user_mentions": ["1123123"], "role_mentions": False}, + {"parse": [], "roles": [], "users": ["1123123"]}, + ), + ( + {"mentions_everyone": False, "user_mentions": False, "role_mentions": ["1231123"]}, + {"parse": [], "roles": ["1231123"], "users": []}, + ), + ( + {"mentions_everyone": False, "user_mentions": ["22222"], "role_mentions": ["1231123"]}, + {"parse": [], "users": ["22222"], "roles": ["1231123"]}, + ), + ( + {"mentions_everyone": True, "user_mentions": ["22222"], "role_mentions": ["1231123"]}, + {"parse": ["everyone"], "users": ["22222"], "roles": ["1231123"]}, + ), + ], + ) + def test_generate_allowed_mentions(self, rest_clients_impl, kwargs, expected_result): + assert rest_clients_impl._generate_allowed_mentions(**kwargs) == expected_result + + @_helpers.parametrize_valid_id_formats_for_models("role", 3, guilds.GuildRole) + def test_generate_allowed_mentions_removes_duplicate_role_ids(self, rest_clients_impl, role): + result = rest_clients_impl._generate_allowed_mentions( + role_mentions=["1", "2", "1", "3", "5", "7", "2", role], user_mentions=True, mentions_everyone=True + ) + assert result == {"roles": ["1", "2", "3", "5", "7"], "parse": ["everyone", "users"]} + + @_helpers.parametrize_valid_id_formats_for_models("user", 3, users.User) + def test_generate_allowed_mentions_removes_duplicate_user_ids(self, rest_clients_impl, user): + result = rest_clients_impl._generate_allowed_mentions( + role_mentions=True, user_mentions=["1", "2", "1", "3", "5", "7", "2", user], mentions_everyone=True + ) + assert result == {"users": ["1", "2", "3", "5", "7"], "parse": ["everyone", "roles"]} + + @_helpers.parametrize_valid_id_formats_for_models("role", 190007233919057920, guilds.GuildRole) + def test_generate_allowed_mentions_handles_all_role_formats(self, rest_clients_impl, role): + result = rest_clients_impl._generate_allowed_mentions( + role_mentions=[role], user_mentions=True, mentions_everyone=True + ) + assert result == {"roles": ["190007233919057920"], "parse": ["everyone", "users"]} + + @_helpers.parametrize_valid_id_formats_for_models("user", 190007233919057920, users.User) + def test_generate_allowed_mentions_handles_all_user_formats(self, rest_clients_impl, user): + result = rest_clients_impl._generate_allowed_mentions( + role_mentions=True, user_mentions=[user], mentions_everyone=True + ) + assert result == {"users": ["190007233919057920"], "parse": ["everyone", "roles"]} + + @_helpers.assert_raises(type_=ValueError) + def test_generate_allowed_mentions_raises_error_on_too_many_roles(self, rest_clients_impl): + rest_clients_impl._generate_allowed_mentions( + user_mentions=False, role_mentions=list(range(101)), mentions_everyone=False + ) + + @_helpers.assert_raises(type_=ValueError) + def test_generate_allowed_mentions_raises_error_on_too_many_users(self, rest_clients_impl): + rest_clients_impl._generate_allowed_mentions( + user_mentions=list(range(101)), role_mentions=False, mentions_everyone=False + ) + + @pytest.mark.asyncio + @_helpers.parametrize_valid_id_formats_for_models("channel", 694463529998352394, channels.Channel) + async def test_create_message_with_optionals(self, rest_clients_impl, channel): + mock_message_obj = mock.MagicMock(messages.Message) + mock_message_payload = {"id": "2929292992", "content": "222922"} + rest_clients_impl._session.create_message.return_value = mock_message_payload + mock_allowed_mentions_payload = {"parse": ["everyone", "users", "roles"]} + rest_clients_impl._generate_allowed_mentions = mock.MagicMock(return_value=mock_allowed_mentions_payload) + mock_embed_payload = {"description": "424242"} + mock_embed_obj = mock.create_autospec( + embeds.Embed, auto_spec=True, serialize=mock.MagicMock(return_value=mock_embed_payload) + ) + mock_media_obj = mock.MagicMock() + mock_media_payload = ("aName.png", mock.MagicMock()) + with mock.patch.object(messages.Message, "deserialize", return_value=mock_message_obj): + with mock.patch.object(media, "safe_read_file", return_value=mock_media_payload): + result = await rest_clients_impl.create_message( + channel, + content="A CONTENT", + nonce="69696969696969", + tts=True, + files=[mock_media_obj], + embed=mock_embed_obj, + mentions_everyone=False, + user_mentions=False, + role_mentions=False, + ) + assert result is mock_message_obj + media.safe_read_file.assert_called_once_with(mock_media_obj) + messages.Message.deserialize.assert_called_once_with(mock_message_payload) + rest_clients_impl._session.create_message.assert_called_once_with( + channel_id="694463529998352394", + content="A CONTENT", + nonce="69696969696969", + tts=True, + files=[mock_media_payload], + embed=mock_embed_payload, + allowed_mentions=mock_allowed_mentions_payload, + ) + mock_embed_obj.serialize.assert_called_once() + rest_clients_impl._generate_allowed_mentions.assert_called_once_with( + mentions_everyone=False, user_mentions=False, role_mentions=False + ) + + @pytest.mark.asyncio + @_helpers.parametrize_valid_id_formats_for_models("channel", 694463529998352394, channels.Channel) + async def test_create_message_without_optionals(self, rest_clients_impl, channel): + mock_message_obj = mock.MagicMock(messages.Message) + mock_message_payload = {"id": "2929292992", "content": "222922"} + rest_clients_impl._session.create_message.return_value = mock_message_payload + mock_allowed_mentions_payload = {"parse": ["everyone", "users", "roles"]} + rest_clients_impl._generate_allowed_mentions = mock.MagicMock(return_value=mock_allowed_mentions_payload) + with mock.patch.object(messages.Message, "deserialize", return_value=mock_message_obj): + assert await rest_clients_impl.create_message(channel) is mock_message_obj + messages.Message.deserialize.assert_called_once_with(mock_message_payload) + rest_clients_impl._session.create_message.assert_called_once_with( + channel_id="694463529998352394", + content=..., + nonce=..., + tts=..., + files=..., + embed=..., + allowed_mentions=mock_allowed_mentions_payload, + ) + rest_clients_impl._generate_allowed_mentions.assert_called_once_with( + mentions_everyone=True, user_mentions=True, role_mentions=True + ) + + @pytest.mark.asyncio + async def test_safe_create_message_without_optionals(self, rest_clients_impl): + channel = mock.MagicMock(channels.Channel) + mock_message_obj = mock.MagicMock(messages.Message) + rest_clients_impl.create_message = mock.AsyncMock(return_value=mock_message_obj) + result = await rest_clients_impl.safe_create_message(channel,) + assert result is mock_message_obj + rest_clients_impl.create_message.assert_called_once_with( + channel=channel, + content=..., + nonce=..., + tts=..., + files=..., + embed=..., + mentions_everyone=False, + user_mentions=False, + role_mentions=False, + ) + + @pytest.mark.asyncio + async def test_safe_create_message_with_optionals(self, rest_clients_impl): + channel = mock.MagicMock(channels.Channel) + mock_embed_obj = mock.create_autospec(embeds.Embed) + mock_message_obj = mock.MagicMock(messages.Message) + mock_media_obj = mock.MagicMock(bytes) + rest_clients_impl.create_message = mock.AsyncMock(return_value=mock_message_obj) + result = await rest_clients_impl.safe_create_message( + channel=channel, + content="A CONTENT", + nonce="69696969696969", + tts=True, + files=[mock_media_obj], + embed=mock_embed_obj, + mentions_everyone=True, + user_mentions=True, + role_mentions=True, + ) + assert result is mock_message_obj + rest_clients_impl.create_message.assert_called_once_with( + channel=channel, + content="A CONTENT", + nonce="69696969696969", + tts=True, + files=[mock_media_obj], + embed=mock_embed_obj, + mentions_everyone=True, + user_mentions=True, + role_mentions=True, + ) + + @pytest.mark.asyncio + @_helpers.parametrize_valid_id_formats_for_models("channel", 213123, channels.Channel) + @_helpers.parametrize_valid_id_formats_for_models("message", 987654321, messages.Message) + @pytest.mark.parametrize("emoji", ["blah:123", emojis.UnknownEmoji(name="blah", id=123, is_animated=False)]) + async def test_create_reaction(self, rest_clients_impl, channel, message, emoji): + rest_clients_impl._session.create_reaction.return_value = ... + assert await rest_clients_impl.create_reaction(channel=channel, message=message, emoji=emoji) is None + rest_clients_impl._session.create_reaction.assert_called_once_with( + channel_id="213123", message_id="987654321", emoji="blah:123", + ) + + @pytest.mark.asyncio + @_helpers.parametrize_valid_id_formats_for_models("channel", 213123, channels.Channel) + @_helpers.parametrize_valid_id_formats_for_models("message", 987654321, messages.Message) + @pytest.mark.parametrize("emoji", ["blah:123", emojis.UnknownEmoji(name="blah", id=123, is_animated=False)]) + async def test_delete_reaction(self, rest_clients_impl, channel, message, emoji): + rest_clients_impl._session.delete_own_reaction.return_value = ... + assert await rest_clients_impl.delete_reaction(channel=channel, message=message, emoji=emoji) is None + rest_clients_impl._session.delete_own_reaction.assert_called_once_with( + channel_id="213123", message_id="987654321", emoji="blah:123", + ) + + @pytest.mark.asyncio + @_helpers.parametrize_valid_id_formats_for_models("channel", 213123, channels.Channel) + @_helpers.parametrize_valid_id_formats_for_models("message", 987654321, messages.Message) + async def test_delete_all_reactions(self, rest_clients_impl, channel, message): + rest_clients_impl._session.delete_all_reactions.return_value = ... + assert await rest_clients_impl.delete_all_reactions(channel=channel, message=message) is None + rest_clients_impl._session.delete_all_reactions.assert_called_once_with( + channel_id="213123", message_id="987654321", + ) + + @pytest.mark.asyncio + @_helpers.parametrize_valid_id_formats_for_models("channel", 213123, channels.Channel) + @_helpers.parametrize_valid_id_formats_for_models("message", 987654321, messages.Message) + @pytest.mark.parametrize("emoji", ["blah:123", emojis.UnknownEmoji(name="blah", id=123, is_animated=False)]) + async def test_delete_all_reactions_for_emoji(self, rest_clients_impl, channel, message, emoji): + rest_clients_impl._session.delete_all_reactions_for_emoji.return_value = ... + assert ( + await rest_clients_impl.delete_all_reactions_for_emoji(channel=channel, message=message, emoji=emoji) + is None + ) + rest_clients_impl._session.delete_all_reactions_for_emoji.assert_called_once_with( + channel_id="213123", message_id="987654321", emoji="blah:123", + ) + + @_helpers.parametrize_valid_id_formats_for_models("message", 432, messages.Message) + @_helpers.parametrize_valid_id_formats_for_models("channel", 123, channels.Channel) + @pytest.mark.parametrize( + "emoji", ["tutu1:456371206225002499", mock.MagicMock(emojis.GuildEmoji, url_name="tutu1:456371206225002499")] + ) + @_helpers.parametrize_valid_id_formats_for_models("user", 140502780547694592, users.User) + def test_fetch_reactors_after_with_optionals(self, rest_clients_impl, message, channel, emoji, user): + mock_generator = mock.AsyncMock() + rest_clients_impl._pagination_handler = mock.MagicMock(return_value=mock_generator) + result = rest_clients_impl.fetch_reactors_after(channel, message, emoji, after=user, limit=47) + assert result is mock_generator + rest_clients_impl._pagination_handler.assert_called_once_with( + channel_id="123", + message_id="432", + emoji="tutu1:456371206225002499", + deserializer=users.User.deserialize, + direction="after", + request=rest_clients_impl._session.get_reactions, + reversing=False, + start="140502780547694592", + limit=47, + ) + + @_helpers.parametrize_valid_id_formats_for_models("message", 432, messages.Message) + @_helpers.parametrize_valid_id_formats_for_models("channel", 123, channels.Channel) + @pytest.mark.parametrize( + "emoji", ["tutu1:456371206225002499", mock.MagicMock(emojis.GuildEmoji, url_name="tutu1:456371206225002499")] + ) + def test_fetch_reactors_after_without_optionals(self, rest_clients_impl, message, channel, emoji): + mock_generator = mock.AsyncMock() + rest_clients_impl._pagination_handler = mock.MagicMock(return_value=mock_generator) + assert rest_clients_impl.fetch_reactors_after(channel, message, emoji) is mock_generator + rest_clients_impl._pagination_handler.assert_called_once_with( + channel_id="123", + message_id="432", + emoji="tutu1:456371206225002499", + deserializer=users.User.deserialize, + direction="after", + request=rest_clients_impl._session.get_reactions, + reversing=False, + start="0", + limit=None, + ) + + def test_fetch_reactors_after_with_datetime_object(self, rest_clients_impl): + mock_generator = mock.AsyncMock() + rest_clients_impl._pagination_handler = mock.MagicMock(return_value=mock_generator) + date = datetime.datetime(2019, 1, 22, 18, 41, 15, 283_000, tzinfo=datetime.timezone.utc) + result = rest_clients_impl.fetch_reactors_after(123, 432, "tutu1:456371206225002499", after=date) + assert result is mock_generator + rest_clients_impl._pagination_handler.assert_called_once_with( + channel_id="123", + message_id="432", + emoji="tutu1:456371206225002499", + deserializer=users.User.deserialize, + direction="after", + request=rest_clients_impl._session.get_reactions, + reversing=False, + start="537340988620800000", + limit=None, + ) + + @pytest.mark.asyncio + @_helpers.parametrize_valid_id_formats_for_models("message", 432, messages.Message) + @_helpers.parametrize_valid_id_formats_for_models("channel", 123, channels.Channel) + async def test_update_message_with_optionals(self, rest_clients_impl, message, channel): + mock_payload = {"id": "4242", "content": "I HAVE BEEN UPDATED!"} + mock_message_obj = mock.MagicMock(messages.Message) + mock_embed_payload = {"description": "blahblah"} + mock_embed = mock.create_autospec( + embeds.Embed, auto_spec=True, serialize=mock.MagicMock(return_value=mock_embed_payload) + ) + mock_allowed_mentions_payload = {"parse": [], "users": ["123"]} + rest_clients_impl._generate_allowed_mentions = mock.MagicMock(return_value=mock_allowed_mentions_payload) + rest_clients_impl._session.edit_message.return_value = mock_payload + with mock.patch.object(messages.Message, "deserialize", return_value=mock_message_obj): + result = await rest_clients_impl.update_message( + message=message, + channel=channel, + content="C O N T E N T", + embed=mock_embed, + flags=messages.MessageFlag.IS_CROSSPOST | messages.MessageFlag.SUPPRESS_EMBEDS, + mentions_everyone=False, + role_mentions=False, + user_mentions=[123123123], + ) + assert result is mock_message_obj + rest_clients_impl._session.edit_message.assert_called_once_with( + channel_id="123", + message_id="432", + content="C O N T E N T", + embed=mock_embed_payload, + flags=6, + allowed_mentions=mock_allowed_mentions_payload, + ) + mock_embed.serialize.assert_called_once() + messages.Message.deserialize.assert_called_once_with(mock_payload) + rest_clients_impl._generate_allowed_mentions.assert_called_once_with( + mentions_everyone=False, role_mentions=False, user_mentions=[123123123] + ) + + @pytest.mark.asyncio + @_helpers.parametrize_valid_id_formats_for_models("message", 432, messages.Message) + @_helpers.parametrize_valid_id_formats_for_models("channel", 123, channels.Channel) + async def test_update_message_without_optionals(self, rest_clients_impl, message, channel): + mock_payload = {"id": "4242", "content": "I HAVE BEEN UPDATED!"} + mock_message_obj = mock.MagicMock(messages.Message) + mock_allowed_mentions_payload = {"parse": ["everyone", "users", "roles"]} + rest_clients_impl._generate_allowed_mentions = mock.MagicMock(return_value=mock_allowed_mentions_payload) + rest_clients_impl._session.edit_message.return_value = mock_payload + with mock.patch.object(messages.Message, "deserialize", return_value=mock_message_obj): + assert await rest_clients_impl.update_message(message=message, channel=channel) is mock_message_obj + rest_clients_impl._session.edit_message.assert_called_once_with( + channel_id="123", + message_id="432", + content=..., + embed=..., + flags=..., + allowed_mentions=mock_allowed_mentions_payload, + ) + messages.Message.deserialize.assert_called_once_with(mock_payload) + rest_clients_impl._generate_allowed_mentions.assert_called_once_with( + mentions_everyone=True, user_mentions=True, role_mentions=True + ) + + @pytest.mark.asyncio + async def test_safe_update_message_without_optionals(self, rest_clients_impl): + message = mock.MagicMock(messages.Message) + channel = mock.MagicMock(channels.Channel) + mock_message_obj = mock.MagicMock(messages.Message) + rest_clients_impl.update_message = mock.AsyncMock(return_value=mock_message_obj) + result = await rest_clients_impl.safe_update_message(message=message, channel=channel,) + assert result is mock_message_obj + rest_clients_impl.update_message.safe_update_message( + message=message, + channel=channel, + content=..., + embed=..., + flags=..., + mentions_everyone=False, + role_mentions=False, + user_mentions=False, + ) + + @pytest.mark.asyncio + async def test_safe_update_message_with_optionals(self, rest_clients_impl): + message = mock.MagicMock(messages.Message) + channel = mock.MagicMock(channels.Channel) + mock_embed = mock.MagicMock(embeds.Embed) + mock_message_obj = mock.MagicMock(messages.Message) + rest_clients_impl.update_message = mock.AsyncMock(return_value=mock_message_obj) + result = await rest_clients_impl.safe_update_message( + message=message, + channel=channel, + content="C O N T E N T", + embed=mock_embed, + flags=messages.MessageFlag.IS_CROSSPOST | messages.MessageFlag.SUPPRESS_EMBEDS, + mentions_everyone=True, + role_mentions=True, + user_mentions=True, + ) + assert result is mock_message_obj + rest_clients_impl.update_message.assert_called_once_with( + message=message, + channel=channel, + content="C O N T E N T", + embed=mock_embed, + flags=messages.MessageFlag.IS_CROSSPOST | messages.MessageFlag.SUPPRESS_EMBEDS, + mentions_everyone=True, + role_mentions=True, + user_mentions=True, + ) + + @pytest.mark.asyncio + @_helpers.parametrize_valid_id_formats_for_models("channel", 379953393319542784, channels.Channel) + @_helpers.parametrize_valid_id_formats_for_models("message", 115590097100865541, messages.Message) + async def test_delete_messages_singular(self, rest_clients_impl, channel, message): + rest_clients_impl._session.delete_message.return_value = ... + assert await rest_clients_impl.delete_messages(channel, message) is None + rest_clients_impl._session.delete_message.assert_called_once_with( + channel_id="379953393319542784", message_id="115590097100865541", + ) + + @pytest.mark.asyncio + @_helpers.parametrize_valid_id_formats_for_models("channel", 379953393319542784, channels.Channel) + @_helpers.parametrize_valid_id_formats_for_models("message", 115590097100865541, messages.Message) + @_helpers.parametrize_valid_id_formats_for_models("additional_message", 115590097100865541, messages.Message) + async def test_delete_messages_singular_after_duplicate_removal( + self, rest_clients_impl, channel, message, additional_message + ): + rest_clients_impl._session.delete_message.return_value = ... + assert await rest_clients_impl.delete_messages(channel, message, additional_message) is None + rest_clients_impl._session.delete_message.assert_called_once_with( + channel_id="379953393319542784", message_id="115590097100865541", + ) + + @pytest.mark.asyncio + @_helpers.parametrize_valid_id_formats_for_models("channel", 379953393319542784, channels.Channel) + @_helpers.parametrize_valid_id_formats_for_models("message", 115590097100865541, messages.Message) + @_helpers.parametrize_valid_id_formats_for_models("additional_message", 572144340277919754, messages.Message) + async def test_delete_messages_bulk_removes_duplicates( + self, rest_clients_impl, channel, message, additional_message + ): + rest_clients_impl._session.bulk_delete_messages.return_value = ... + assert await rest_clients_impl.delete_messages(channel, message, additional_message, 115590097100865541) is None + rest_clients_impl._session.bulk_delete_messages.assert_called_once_with( + channel_id="379953393319542784", messages=["115590097100865541", "572144340277919754"], + ) + rest_clients_impl._session.delete_message.assert_not_called() + + @pytest.mark.asyncio + @_helpers.assert_raises(type_=ValueError) + async def test_delete_messages_raises_value_error_on_over_100_messages(self, rest_clients_impl): + rest_clients_impl._session.bulk_delete_messages.return_value = ... + assert await rest_clients_impl.delete_messages(123123, *list(range(0, 111))) is None + + @pytest.mark.asyncio + @_helpers.parametrize_valid_id_formats_for_models("channel", 4123123, channels.Channel) + @_helpers.parametrize_valid_id_formats_for_models("overwrite", 9999, channels.PermissionOverwrite) + async def test_update_channel_overwrite_with_optionals(self, rest_clients_impl, channel, overwrite): + rest_clients_impl._session.edit_channel_permissions.return_value = ... + result = await rest_clients_impl.update_channel_overwrite( + channel=channel, + overwrite=overwrite, + target_type="member", + allow=messages.MessageFlag.IS_CROSSPOST | messages.MessageFlag.SUPPRESS_EMBEDS, + deny=21, + reason="get Nyaa'd", + ) + assert result is None + rest_clients_impl._session.edit_channel_permissions.assert_called_once_with( + channel_id="4123123", overwrite_id="9999", type_="member", allow=6, deny=21, reason="get Nyaa'd", + ) + + @pytest.mark.asyncio + @_helpers.parametrize_valid_id_formats_for_models("channel", 4123123, channels.Channel) + @_helpers.parametrize_valid_id_formats_for_models("overwrite", 9999, channels.PermissionOverwrite) + async def test_update_channel_overwrite_without_optionals(self, rest_clients_impl, channel, overwrite): + rest_clients_impl._session.edit_channel_permissions.return_value = ... + result = await rest_clients_impl.update_channel_overwrite( + channel=channel, overwrite=overwrite, target_type="member" + ) + assert result is None + rest_clients_impl._session.edit_channel_permissions.assert_called_once_with( + channel_id="4123123", overwrite_id="9999", type_="member", allow=..., deny=..., reason=..., + ) + + @pytest.mark.asyncio + @pytest.mark.parametrize( + "target", + [ + mock.MagicMock(guilds.GuildRole, id=snowflakes.Snowflake(9999), __int__=guilds.GuildRole.__int__), + mock.MagicMock(users.User, id=snowflakes.Snowflake(9999), __int__=users.User.__int__), + ], + ) + async def test_update_channel_overwrite_with_alternative_target_object(self, rest_clients_impl, target): + rest_clients_impl._session.edit_channel_permissions.return_value = ... + result = await rest_clients_impl.update_channel_overwrite( + channel=4123123, overwrite=target, target_type="member" + ) + assert result is None + rest_clients_impl._session.edit_channel_permissions.assert_called_once_with( + channel_id="4123123", overwrite_id="9999", type_="member", allow=..., deny=..., reason=..., + ) + + @pytest.mark.asyncio + @_helpers.parametrize_valid_id_formats_for_models("channel", 123123123, channels.Channel) + async def test_fetch_invites_for_channel(self, rest_clients_impl, channel): + mock_invite_payload = {"code": "ogogogogogogogo", "guild_id": "123123123"} + mock_invite_obj = mock.MagicMock(invites.InviteWithMetadata) + rest_clients_impl._session.get_channel_invites.return_value = [mock_invite_payload] + with mock.patch.object(invites.InviteWithMetadata, "deserialize", return_value=mock_invite_obj): + assert await rest_clients_impl.fetch_invites_for_channel(channel=channel) == [mock_invite_obj] + rest_clients_impl._session.get_channel_invites.assert_called_once_with(channel_id="123123123") + invites.InviteWithMetadata.deserialize.assert_called_once_with(mock_invite_payload) + + @pytest.mark.asyncio + @_helpers.parametrize_valid_id_formats_for_models("channel", 234123, channels.Channel) + @_helpers.parametrize_valid_id_formats_for_models("user", 333333, users.User) + @pytest.mark.parametrize("max_age", [4444, datetime.timedelta(seconds=4444)]) + async def test_create_invite_for_channel_with_optionals(self, rest_clients_impl, channel, user, max_age): + mock_invite_payload = {"code": "ogogogogogogogo", "guild_id": "123123123"} + mock_invite_obj = mock.MagicMock(invites.InviteWithMetadata) + rest_clients_impl._session.create_channel_invite.return_value = mock_invite_payload + with mock.patch.object(invites.InviteWithMetadata, "deserialize", return_value=mock_invite_obj): + result = await rest_clients_impl.create_invite_for_channel( + channel, + max_age=max_age, + max_uses=444, + temporary=True, + unique=False, + target_user=user, + target_user_type=invites.TargetUserType.STREAM, + reason="Hello there.", + ) + assert result is mock_invite_obj + rest_clients_impl._session.create_channel_invite.assert_called_once_with( + channel_id="234123", + max_age=4444, + max_uses=444, + temporary=True, + unique=False, + target_user="333333", + target_user_type=1, + reason="Hello there.", + ) + invites.InviteWithMetadata.deserialize.assert_called_once_with(mock_invite_payload) + + @pytest.mark.asyncio + @_helpers.parametrize_valid_id_formats_for_models("channel", 234123, channels.Channel) + async def test_create_invite_for_channel_without_optionals(self, rest_clients_impl, channel): + mock_invite_payload = {"code": "ogogogogogogogo", "guild_id": "123123123"} + mock_invite_obj = mock.MagicMock(invites.InviteWithMetadata) + rest_clients_impl._session.create_channel_invite.return_value = mock_invite_payload + with mock.patch.object(invites.InviteWithMetadata, "deserialize", return_value=mock_invite_obj): + assert await rest_clients_impl.create_invite_for_channel(channel) is mock_invite_obj + rest_clients_impl._session.create_channel_invite.assert_called_once_with( + channel_id="234123", + max_age=..., + max_uses=..., + temporary=..., + unique=..., + target_user=..., + target_user_type=..., + reason=..., + ) + invites.InviteWithMetadata.deserialize.assert_called_once_with(mock_invite_payload) + + @pytest.mark.asyncio + @_helpers.parametrize_valid_id_formats_for_models("channel", 379953393319542784, channels.Channel) + @_helpers.parametrize_valid_id_formats_for_models("overwrite", 123123123, channels.PermissionOverwrite) + async def test_delete_channel_overwrite(self, rest_clients_impl, channel, overwrite): + rest_clients_impl._session.delete_channel_permission.return_value = ... + assert await rest_clients_impl.delete_channel_overwrite(channel=channel, overwrite=overwrite) is None + rest_clients_impl._session.delete_channel_permission.assert_called_once_with( + channel_id="379953393319542784", overwrite_id="123123123", + ) + + @pytest.mark.asyncio + @pytest.mark.parametrize( + "target", + [ + mock.MagicMock(guilds.GuildRole, id=snowflakes.Snowflake(123123123), __int__=guilds.GuildRole.__int__), + mock.MagicMock(users.User, id=snowflakes.Snowflake(123123123), __int__=users.User.__int__), + ], + ) + async def test_delete_channel_overwrite_with_alternative_target_objects(self, rest_clients_impl, target): + rest_clients_impl._session.delete_channel_permission.return_value = ... + assert await rest_clients_impl.delete_channel_overwrite(channel=379953393319542784, overwrite=target) is None + rest_clients_impl._session.delete_channel_permission.assert_called_once_with( + channel_id="379953393319542784", overwrite_id="123123123", + ) + + @pytest.mark.asyncio + @_helpers.parametrize_valid_id_formats_for_models("channel", 379953393319542784, channels.PermissionOverwrite) + async def test_trigger_typing(self, rest_clients_impl, channel): + rest_clients_impl._session.trigger_typing_indicator.return_value = ... + assert await rest_clients_impl.trigger_typing(channel) is None + rest_clients_impl._session.trigger_typing_indicator.assert_called_once_with(channel_id="379953393319542784") + + @pytest.mark.asyncio + @_helpers.parametrize_valid_id_formats_for_models("channel", 123123123, channels.Channel) + async def test_fetch_pins(self, rest_clients_impl, channel): + mock_message_payload = {"id": "21232", "content": "CONTENT"} + mock_message_obj = mock.MagicMock(messages.Message, id=21232) + rest_clients_impl._session.get_pinned_messages.return_value = [mock_message_payload] + with mock.patch.object(messages.Message, "deserialize", return_value=mock_message_obj): + assert await rest_clients_impl.fetch_pins(channel) == {21232: mock_message_obj} + rest_clients_impl._session.get_pinned_messages.assert_called_once_with(channel_id="123123123") + messages.Message.deserialize.assert_called_once_with(mock_message_payload) + + @pytest.mark.asyncio + @_helpers.parametrize_valid_id_formats_for_models("channel", 292929, channels.Channel) + @_helpers.parametrize_valid_id_formats_for_models("message", 123123, messages.Message) + async def test_pin_message(self, rest_clients_impl, channel, message): + rest_clients_impl._session.add_pinned_channel_message.return_value = ... + assert await rest_clients_impl.pin_message(channel, message) is None + rest_clients_impl._session.add_pinned_channel_message.assert_called_once_with( + channel_id="292929", message_id="123123" + ) + + @pytest.mark.asyncio + @_helpers.parametrize_valid_id_formats_for_models("channel", 292929, channels.Channel) + @_helpers.parametrize_valid_id_formats_for_models("message", 123123, messages.Message) + async def test_unpin_message(self, rest_clients_impl, channel, message): + rest_clients_impl._session.delete_pinned_channel_message.return_value = ... + assert await rest_clients_impl.unpin_message(channel, message) is None + rest_clients_impl._session.delete_pinned_channel_message.assert_called_once_with( + channel_id="292929", message_id="123123" + ) + + @pytest.mark.asyncio + @_helpers.parametrize_valid_id_formats_for_models("guild", 93443949, guilds.Guild) + @_helpers.parametrize_valid_id_formats_for_models("emoji", 40404040404, emojis.GuildEmoji) + async def test_fetch_guild_emoji(self, rest_clients_impl, guild, emoji): + mock_emoji_payload = {"id": "92929", "name": "nyaa", "animated": True} + mock_emoji_obj = mock.MagicMock(emojis.GuildEmoji) + rest_clients_impl._session.get_guild_emoji.return_value = mock_emoji_payload + with mock.patch.object(emojis.GuildEmoji, "deserialize", return_value=mock_emoji_obj): + assert await rest_clients_impl.fetch_guild_emoji(guild=guild, emoji=emoji) is mock_emoji_obj + rest_clients_impl._session.get_guild_emoji.assert_called_once_with( + guild_id="93443949", emoji_id="40404040404", + ) + emojis.GuildEmoji.deserialize.assert_called_once_with(mock_emoji_payload) + + @pytest.mark.asyncio + @_helpers.parametrize_valid_id_formats_for_models("guild", 93443949, guilds.Guild) + async def test_fetch_guild_emojis(self, rest_clients_impl, guild): + mock_emoji_payload = {"id": "92929", "name": "nyaa", "animated": True} + mock_emoji_obj = mock.MagicMock(emojis.GuildEmoji) + rest_clients_impl._session.list_guild_emojis.return_value = [mock_emoji_payload] + with mock.patch.object(emojis.GuildEmoji, "deserialize", return_value=mock_emoji_obj): + assert await rest_clients_impl.fetch_guild_emojis(guild=guild) == [mock_emoji_obj] + rest_clients_impl._session.list_guild_emojis.assert_called_once_with(guild_id="93443949",) + emojis.GuildEmoji.deserialize.assert_called_once_with(mock_emoji_payload) + + @pytest.mark.asyncio + @_helpers.parametrize_valid_id_formats_for_models("guild", 93443949, guilds.Guild) + @_helpers.parametrize_valid_id_formats_for_models("role", 537340989808050216, guilds.GuildRole) + async def test_create_guild_emoji_with_optionals(self, rest_clients_impl, guild, role): + mock_emoji_payload = {"id": "229292929", "animated": True} + mock_emoji_obj = mock.MagicMock(emojis.GuildEmoji) + rest_clients_impl._session.create_guild_emoji.return_value = mock_emoji_payload + mock_image_obj = mock.MagicMock(io.BytesIO) + mock_image_data = mock.MagicMock(bytes) + with mock.patch.object(conversions, "get_bytes_from_resource", return_value=mock_image_data): + with mock.patch.object(emojis.GuildEmoji, "deserialize", return_value=mock_emoji_obj): + result = await rest_clients_impl.create_guild_emoji( + guild=guild, name="fairEmoji", image_data=mock_image_obj, roles=[role], reason="hello", + ) + assert result is mock_emoji_obj + emojis.GuildEmoji.deserialize.assert_called_once_with(mock_emoji_payload) + conversions.get_bytes_from_resource.assert_called_once_with(mock_image_obj) + rest_clients_impl._session.create_guild_emoji.assert_called_once_with( + guild_id="93443949", name="fairEmoji", image=mock_image_data, roles=["537340989808050216"], reason="hello", + ) + + @pytest.mark.asyncio + @_helpers.parametrize_valid_id_formats_for_models("guild", 93443949, guilds.Guild) + async def test_create_guild_emoji_without_optionals(self, rest_clients_impl, guild): + mock_emoji_payload = {"id": "229292929", "animated": True} + mock_emoji_obj = mock.MagicMock(emojis.GuildEmoji) + rest_clients_impl._session.create_guild_emoji.return_value = mock_emoji_payload + mock_image_obj = mock.MagicMock(io.BytesIO) + mock_image_data = mock.MagicMock(bytes) + with mock.patch.object(conversions, "get_bytes_from_resource", return_value=mock_image_data): + with mock.patch.object(emojis.GuildEmoji, "deserialize", return_value=mock_emoji_obj): + result = await rest_clients_impl.create_guild_emoji( + guild=guild, name="fairEmoji", image_data=mock_image_obj, + ) + assert result is mock_emoji_obj + emojis.GuildEmoji.deserialize.assert_called_once_with(mock_emoji_payload) + conversions.get_bytes_from_resource.assert_called_once_with(mock_image_obj) + rest_clients_impl._session.create_guild_emoji.assert_called_once_with( + guild_id="93443949", name="fairEmoji", image=mock_image_data, roles=..., reason=..., + ) + + @pytest.mark.asyncio + @_helpers.parametrize_valid_id_formats_for_models("guild", 93443949, guilds.Guild) + @_helpers.parametrize_valid_id_formats_for_models("emoji", 4123321, emojis.GuildEmoji) + async def test_update_guild_emoji_without_optionals(self, rest_clients_impl, guild, emoji): + mock_emoji_payload = {"id": "202020", "name": "Nyaa", "animated": True} + mock_emoji_obj = mock.MagicMock(emojis.GuildEmoji) + rest_clients_impl._session.modify_guild_emoji.return_value = mock_emoji_payload + with mock.patch.object(emojis.GuildEmoji, "deserialize", return_value=mock_emoji_obj): + assert await rest_clients_impl.update_guild_emoji(guild, emoji) is mock_emoji_obj + rest_clients_impl._session.modify_guild_emoji.assert_called_once_with( + guild_id="93443949", emoji_id="4123321", name=..., roles=..., reason=..., + ) + emojis.GuildEmoji.deserialize.assert_called_once_with(mock_emoji_payload) + + @pytest.mark.asyncio + @_helpers.parametrize_valid_id_formats_for_models("guild", 93443949, guilds.Guild) + @_helpers.parametrize_valid_id_formats_for_models("emoji", 4123321, emojis.GuildEmoji) + @_helpers.parametrize_valid_id_formats_for_models("role", 123123123, guilds.GuildRole) + async def test_update_guild_emoji_with_optionals(self, rest_clients_impl, guild, emoji, role): + mock_emoji_payload = {"id": "202020", "name": "Nyaa", "animated": True} + mock_emoji_obj = mock.MagicMock(emojis.GuildEmoji) + rest_clients_impl._session.modify_guild_emoji.return_value = mock_emoji_payload + with mock.patch.object(emojis.GuildEmoji, "deserialize", return_value=mock_emoji_obj): + result = await rest_clients_impl.update_guild_emoji( + guild, emoji, name="Nyaa", roles=[role], reason="Agent 42" + ) + assert result is mock_emoji_obj + rest_clients_impl._session.modify_guild_emoji.assert_called_once_with( + guild_id="93443949", emoji_id="4123321", name="Nyaa", roles=["123123123"], reason="Agent 42", + ) + emojis.GuildEmoji.deserialize.assert_called_once_with(mock_emoji_payload) + + @pytest.mark.asyncio + @_helpers.parametrize_valid_id_formats_for_models("guild", 93443949, guilds.Guild) + @_helpers.parametrize_valid_id_formats_for_models("emoji", 4123321, emojis.GuildEmoji) + async def test_delete_guild_emoji(self, rest_clients_impl, guild, emoji): + rest_clients_impl._session.delete_guild_emoji.return_value = ... + assert await rest_clients_impl.delete_guild_emoji(guild, emoji) is None + rest_clients_impl._session.delete_guild_emoji.assert_called_once_with(guild_id="93443949", emoji_id="4123321") + + @pytest.mark.asyncio + @pytest.mark.parametrize("region", [mock.MagicMock(voices.VoiceRegion, id="LONDON"), "LONDON"]) + async def test_create_guild_with_optionals(self, rest_clients_impl, region): + mock_guild_payload = {"id": "299292929292992", "region": "LONDON"} + mock_guild_obj = mock.MagicMock(guilds.Guild) + rest_clients_impl._session.create_guild.return_value = mock_guild_payload + mock_image_obj = mock.MagicMock(io.BytesIO) + mock_image_data = mock.MagicMock(bytes) + mock_role_payload = {"permissions": 123123} + mock_role_obj = mock.create_autospec( + guilds.GuildRole, spec_set=True, serialize=mock.MagicMock(return_value=mock_role_payload) + ) + mock_channel_payload = {"type": 2, "name": "aChannel"} + mock_channel_obj = mock.create_autospec( + channels.GuildChannel, spec_set=True, serialize=mock.MagicMock(return_value=mock_channel_payload) + ) + with mock.patch.object(guilds.Guild, "deserialize", return_value=mock_guild_obj): + with mock.patch.object(conversions, "get_bytes_from_resource", return_value=mock_image_data): + result = await rest_clients_impl.create_guild( + name="OK", + region=region, + icon_data=mock_image_obj, + verification_level=guilds.GuildVerificationLevel.NONE, + default_message_notifications=guilds.GuildMessageNotificationsLevel.ONLY_MENTIONS, + explicit_content_filter=guilds.GuildExplicitContentFilterLevel.MEMBERS_WITHOUT_ROLES, + roles=[mock_role_obj], + channels=[mock_channel_obj], + ) + assert result is mock_guild_obj + conversions.get_bytes_from_resource.assert_called_once_with(mock_image_obj) + guilds.Guild.deserialize.assert_called_once_with(mock_guild_payload) + mock_channel_obj.serialize.assert_called_once() + mock_role_obj.serialize.assert_called_once() + rest_clients_impl._session.create_guild.assert_called_once_with( + name="OK", + region="LONDON", + icon=mock_image_data, + verification_level=0, + default_message_notifications=1, + explicit_content_filter=1, + roles=[mock_role_payload], + channels=[mock_channel_payload], + ) + + @pytest.mark.asyncio + async def test_create_guild_without_optionals(self, rest_clients_impl): + mock_guild_payload = {"id": "299292929292992", "region": "LONDON"} + mock_guild_obj = mock.MagicMock(guilds.Guild) + rest_clients_impl._session.create_guild.return_value = mock_guild_payload + with mock.patch.object(guilds.Guild, "deserialize", return_value=mock_guild_obj): + assert await rest_clients_impl.create_guild(name="OK") is mock_guild_obj + guilds.Guild.deserialize.assert_called_once_with(mock_guild_payload) + rest_clients_impl._session.create_guild.assert_called_once_with( + name="OK", + region=..., + icon=..., + verification_level=..., + default_message_notifications=..., + explicit_content_filter=..., + roles=..., + channels=..., + ) + + @pytest.mark.asyncio + @_helpers.parametrize_valid_id_formats_for_models("guild", 379953393319542784, guilds.Guild) + async def test_fetch_guild(self, rest_clients_impl, guild): + mock_guild_payload = {"id": "94949494", "name": "A guild", "roles": []} + mock_guild_obj = mock.MagicMock(guilds.Guild) + rest_clients_impl._session.get_guild.return_value = mock_guild_payload + with mock.patch.object(guilds.Guild, "deserialize", return_value=mock_guild_obj): + assert await rest_clients_impl.fetch_guild(guild) is mock_guild_obj + rest_clients_impl._session.get_guild.assert_called_once_with(guild_id="379953393319542784") + guilds.Guild.deserialize.assert_called_once_with(mock_guild_payload) + + @pytest.mark.asyncio + @_helpers.parametrize_valid_id_formats_for_models("guild", 379953393319542784, guilds.Guild) + @_helpers.parametrize_valid_id_formats_for_models("afk_channel", 669517187031105607, guilds.Guild) + @_helpers.parametrize_valid_id_formats_for_models("owner", 379953393319542784, users.User) + @_helpers.parametrize_valid_id_formats_for_models("system_channel", 537340989808050216, users.User) + @pytest.mark.parametrize("region", ["LONDON", mock.MagicMock(voices.VoiceRegion, id="LONDON")]) + @pytest.mark.parametrize("afk_timeout", [300, datetime.timedelta(seconds=300)]) + async def test_update_guild_with_optionals( + self, rest_clients_impl, guild, region, afk_channel, afk_timeout, owner, system_channel + ): + mock_guild_payload = {"id": "424242", "splash": "2lmKmklsdlksalkd"} + mock_guild_obj = mock.MagicMock(guilds.Guild) + rest_clients_impl._session.modify_guild.return_value = mock_guild_payload + mock_icon_data = mock.MagicMock(bytes) + mock_icon_obj = mock.MagicMock(io.BytesIO) + mock_splash_data = mock.MagicMock(bytes) + mock_splash_obj = mock.MagicMock(io.BytesIO) + with mock.patch.object(guilds.Guild, "deserialize", return_value=mock_guild_obj): + with mock.patch.object( + conversions, "get_bytes_from_resource", side_effect=[mock_icon_data, mock_splash_data] + ): + result = await rest_clients_impl.update_guild( + guild, + name="aNewName", + region=region, + verification_level=guilds.GuildVerificationLevel.LOW, + default_message_notifications=guilds.GuildMessageNotificationsLevel.ONLY_MENTIONS, + explicit_content_filter=guilds.GuildExplicitContentFilterLevel.ALL_MEMBERS, + afk_channel=afk_channel, + afk_timeout=afk_timeout, + icon_data=mock_icon_obj, + owner=owner, + splash_data=mock_splash_obj, + system_channel=system_channel, + reason="A good reason", + ) + assert result is mock_guild_obj + rest_clients_impl._session.modify_guild.assert_called_once_with( + guild_id="379953393319542784", + name="aNewName", + region="LONDON", + verification_level=1, + default_message_notifications=1, + explicit_content_filter=2, + afk_channel_id="669517187031105607", + afk_timeout=300, + icon=mock_icon_data, + owner_id="379953393319542784", + splash=mock_splash_data, + system_channel_id="537340989808050216", + reason="A good reason", + ) + + @pytest.mark.asyncio + @_helpers.parametrize_valid_id_formats_for_models("guild", 379953393319542784, guilds.Guild) + async def test_update_guild_without_optionals(self, rest_clients_impl, guild): + mock_guild_payload = {"id": "424242", "splash": "2lmKmklsdlksalkd"} + mock_guild_obj = mock.MagicMock(guilds.Guild) + rest_clients_impl._session.modify_guild.return_value = mock_guild_payload + with mock.patch.object(guilds.Guild, "deserialize", return_value=mock_guild_obj): + assert await rest_clients_impl.update_guild(guild) is mock_guild_obj + rest_clients_impl._session.modify_guild.assert_called_once_with( + guild_id="379953393319542784", + name=..., + region=..., + verification_level=..., + default_message_notifications=..., + explicit_content_filter=..., + afk_channel_id=..., + afk_timeout=..., + icon=..., + owner_id=..., + splash=..., + system_channel_id=..., + reason=..., + ) + + @pytest.mark.asyncio + @_helpers.parametrize_valid_id_formats_for_models("guild", 379953393319542784, guilds.Guild) + async def test_delete_guild(self, rest_clients_impl, guild): + rest_clients_impl._session.delete_guild.return_value = ... + assert await rest_clients_impl.delete_guild(guild) is None + rest_clients_impl._session.delete_guild.assert_called_once_with(guild_id="379953393319542784") + + @pytest.mark.asyncio + @_helpers.parametrize_valid_id_formats_for_models("guild", 379953393319542784, guilds.Guild) + async def test_fetch_guild_channels(self, rest_clients_impl, guild): + mock_channel_payload = {"id": "292929", "type": 1, "description": "A CHANNEL"} + mock_channel_obj = mock.MagicMock(channels.GuildChannel) + rest_clients_impl._session.list_guild_channels.return_value = [mock_channel_payload] + with mock.patch.object(channels, "deserialize_channel", return_value=mock_channel_obj): + assert await rest_clients_impl.fetch_guild_channels(guild) == [mock_channel_obj] + rest_clients_impl._session.list_guild_channels.assert_called_once_with(guild_id="379953393319542784") + channels.deserialize_channel.assert_called_once_with(mock_channel_payload) + + @pytest.mark.asyncio + @_helpers.parametrize_valid_id_formats_for_models("guild", 123123123, guilds.Guild) + @_helpers.parametrize_valid_id_formats_for_models("category", 5555, channels.GuildCategory) + @pytest.mark.parametrize("rate_limit_per_user", [500, datetime.timedelta(seconds=500)]) + async def test_create_guild_channel_with_optionals(self, rest_clients_impl, guild, category, rate_limit_per_user): + mock_channel_payload = {"id": "22929292", "type": "5", "description": "A C H A N N E L"} + mock_channel_obj = mock.MagicMock(channels.GuildChannel) + mock_overwrite_payload = {"type": "member", "id": "30303030"} + mock_overwrite_obj = mock.MagicMock( + channels.PermissionOverwrite, serialize=mock.MagicMock(return_value=mock_overwrite_payload) + ) + rest_clients_impl._session.create_guild_channel.return_value = mock_channel_payload + with mock.patch.object(channels, "deserialize_channel", return_value=mock_channel_obj): + result = await rest_clients_impl.create_guild_channel( + guild, + "Hi-i-am-a-name", + channel_type=channels.ChannelType.GUILD_VOICE, + position=42, + topic="A TOPIC", + nsfw=True, + rate_limit_per_user=rate_limit_per_user, + bitrate=36000, + user_limit=5, + permission_overwrites=[mock_overwrite_obj], + parent_category=category, + reason="A GOOD REASON!", + ) + assert result is mock_channel_obj + mock_overwrite_obj.serialize.assert_called_once() + rest_clients_impl._session.create_guild_channel.assert_called_once_with( + guild_id="123123123", + name="Hi-i-am-a-name", + type_=2, + position=42, + topic="A TOPIC", + nsfw=True, + rate_limit_per_user=500, + bitrate=36000, + user_limit=5, + permission_overwrites=[mock_overwrite_payload], + parent_id="5555", + reason="A GOOD REASON!", + ) + channels.deserialize_channel.assert_called_once_with(mock_channel_payload) + + @pytest.mark.asyncio + @_helpers.parametrize_valid_id_formats_for_models("guild", 123123123, guilds.Guild) + async def test_create_guild_channel_without_optionals(self, rest_clients_impl, guild): + mock_channel_payload = {"id": "22929292", "type": "5", "description": "A C H A N N E L"} + mock_channel_obj = mock.MagicMock(channels.GuildChannel) + rest_clients_impl._session.create_guild_channel.return_value = mock_channel_payload + with mock.patch.object(channels, "deserialize_channel", return_value=mock_channel_obj): + assert await rest_clients_impl.create_guild_channel(guild, "Hi-i-am-a-name") is mock_channel_obj + rest_clients_impl._session.create_guild_channel.assert_called_once_with( + guild_id="123123123", + name="Hi-i-am-a-name", + type_=..., + position=..., + topic=..., + nsfw=..., + rate_limit_per_user=..., + bitrate=..., + user_limit=..., + permission_overwrites=..., + parent_id=..., + reason=..., + ) + channels.deserialize_channel.assert_called_once_with(mock_channel_payload) + + @pytest.mark.asyncio + @_helpers.parametrize_valid_id_formats_for_models("guild", 123123123, guilds.Guild) + @_helpers.parametrize_valid_id_formats_for_models("channel", 379953393319542784, channels.GuildChannel) + @_helpers.parametrize_valid_id_formats_for_models("second_channel", 115590097100865541, channels.GuildChannel) + async def test_reposition_guild_channels(self, rest_clients_impl, guild, channel, second_channel): + rest_clients_impl._session.modify_guild_channel_positions.return_value = ... + assert await rest_clients_impl.reposition_guild_channels(guild, (1, channel), (2, second_channel)) is None + rest_clients_impl._session.modify_guild_channel_positions.assert_called_once_with( + "123123123", ("379953393319542784", 1), ("115590097100865541", 2) + ) + + @pytest.mark.asyncio + @_helpers.parametrize_valid_id_formats_for_models("guild", 444444, guilds.Guild) + @_helpers.parametrize_valid_id_formats_for_models("user", 123123123123, users.User) + async def test_fetch_member(self, rest_clients_impl, guild, user): + mock_member_payload = {"user": {}, "nick": "! Agent 47"} + mock_member_obj = mock.MagicMock(guilds.GuildMember) + rest_clients_impl._session.get_guild_member.return_value = mock_member_payload + with mock.patch.object(guilds.GuildMember, "deserialize", return_value=mock_member_obj): + assert await rest_clients_impl.fetch_member(guild, user) is mock_member_obj + rest_clients_impl._session.get_guild_member.assert_called_once_with( + guild_id="444444", user_id="123123123123" + ) + guilds.GuildMember.deserialize.assert_called_once_with(mock_member_payload) + + @_helpers.parametrize_valid_id_formats_for_models("guild", 574921006817476608, guilds.Guild) + @_helpers.parametrize_valid_id_formats_for_models("user", 115590097100865541, users.User) + def test_fetch_members_after_with_optionals(self, rest_clients_impl, guild, user): + mock_generator = mock.AsyncMock() + rest_clients_impl._pagination_handler = mock.MagicMock(return_value=mock_generator) + assert rest_clients_impl.fetch_members_after(guild, after=user, limit=34) is mock_generator + rest_clients_impl._pagination_handler.assert_called_once_with( + guild_id="574921006817476608", + deserializer=guilds.GuildMember.deserialize, + direction="after", + request=rest_clients_impl._session.list_guild_members, + reversing=False, + start="115590097100865541", + limit=34, + id_getter=rest_clients._get_member_id, + ) + + @_helpers.parametrize_valid_id_formats_for_models("guild", 574921006817476608, guilds.Guild) + def test_fetch_members_after_without_optionals(self, rest_clients_impl, guild): + mock_generator = mock.AsyncMock() + rest_clients_impl._pagination_handler = mock.MagicMock(return_value=mock_generator) + assert rest_clients_impl.fetch_members_after(guild) is mock_generator + rest_clients_impl._pagination_handler.assert_called_once_with( + guild_id="574921006817476608", + deserializer=guilds.GuildMember.deserialize, + direction="after", + request=rest_clients_impl._session.list_guild_members, + reversing=False, + start="0", + limit=None, + id_getter=rest_clients._get_member_id, + ) + + def test_fetch_members_after_with_datetime_object(self, rest_clients_impl): + mock_generator = mock.AsyncMock() + rest_clients_impl._pagination_handler = mock.MagicMock(return_value=mock_generator) + date = datetime.datetime(2019, 1, 22, 18, 41, 15, 283_000, tzinfo=datetime.timezone.utc) + assert rest_clients_impl.fetch_members_after(574921006817476608, after=date) is mock_generator + rest_clients_impl._pagination_handler.assert_called_once_with( + guild_id="574921006817476608", + deserializer=guilds.GuildMember.deserialize, + direction="after", + request=rest_clients_impl._session.list_guild_members, + reversing=False, + start="537340988620800000", + limit=None, + id_getter=rest_clients._get_member_id, + ) + + @pytest.mark.asyncio + @_helpers.parametrize_valid_id_formats_for_models("guild", 229292992, guilds.Guild) + @_helpers.parametrize_valid_id_formats_for_models("user", 1010101010, users.User) + @_helpers.parametrize_valid_id_formats_for_models("role", 11100010, guilds.GuildRole) + @_helpers.parametrize_valid_id_formats_for_models("channel", 33333333, channels.GuildVoiceChannel) + async def test_update_member_with_optionals(self, rest_clients_impl, guild, user, role, channel): + rest_clients_impl._session.modify_guild_member.return_value = ... + result = await rest_clients_impl.update_member( + guild, + user, + nickname="Nick's Name", + roles=[role], + mute=True, + deaf=False, + voice_channel=channel, + reason="Get Tagged.", + ) + assert result is None + rest_clients_impl._session.modify_guild_member.assert_called_once_with( + guild_id="229292992", + user_id="1010101010", + nick="Nick's Name", + roles=["11100010"], + mute=True, + deaf=False, + channel_id="33333333", + reason="Get Tagged.", + ) + + @pytest.mark.asyncio + @_helpers.parametrize_valid_id_formats_for_models("guild", 229292992, guilds.Guild) + @_helpers.parametrize_valid_id_formats_for_models("user", 1010101010, users.User) + async def test_update_member_without_optionals(self, rest_clients_impl, guild, user): + rest_clients_impl._session.modify_guild_member.return_value = ... + assert await rest_clients_impl.update_member(guild, user) is None + rest_clients_impl._session.modify_guild_member.assert_called_once_with( + guild_id="229292992", + user_id="1010101010", + nick=..., + roles=..., + mute=..., + deaf=..., + channel_id=..., + reason=..., + ) + + @pytest.mark.asyncio + @_helpers.parametrize_valid_id_formats_for_models("guild", 229292992, guilds.Guild) + async def test_update_my_member_nickname_with_reason(self, rest_clients_impl, guild): + rest_clients_impl._session.modify_current_user_nick.return_value = ... + result = await rest_clients_impl.update_my_member_nickname( + guild, "Nick's nick", reason="I want to drink your blood." + ) + assert result is None + rest_clients_impl._session.modify_current_user_nick.assert_called_once_with( + guild_id="229292992", nick="Nick's nick", reason="I want to drink your blood." + ) + + @pytest.mark.asyncio + @_helpers.parametrize_valid_id_formats_for_models("guild", 229292992, guilds.Guild) + async def test_update_my_member_nickname_without_reason(self, rest_clients_impl, guild): + rest_clients_impl._session.modify_current_user_nick.return_value = ... + assert await rest_clients_impl.update_my_member_nickname(guild, "Nick's nick") is None + rest_clients_impl._session.modify_current_user_nick.assert_called_once_with( + guild_id="229292992", nick="Nick's nick", reason=... + ) + + @pytest.mark.asyncio + @_helpers.parametrize_valid_id_formats_for_models("guild", 123123123, guilds.Guild) + @_helpers.parametrize_valid_id_formats_for_models("user", 4444444, users.User) + @_helpers.parametrize_valid_id_formats_for_models("role", 101010101, guilds.GuildRole) + async def test_add_role_to_member_with_reason(self, rest_clients_impl, guild, user, role): + rest_clients_impl._session.add_guild_member_role.return_value = ... + assert await rest_clients_impl.add_role_to_member(guild, user, role, reason="Get role'd") is None + rest_clients_impl._session.add_guild_member_role.assert_called_once_with( + guild_id="123123123", user_id="4444444", role_id="101010101", reason="Get role'd" + ) + + @pytest.mark.asyncio + @_helpers.parametrize_valid_id_formats_for_models("guild", 123123123, guilds.Guild) + @_helpers.parametrize_valid_id_formats_for_models("user", 4444444, users.User) + @_helpers.parametrize_valid_id_formats_for_models("role", 101010101, guilds.GuildRole) + async def test_add_role_to_member_without_reason(self, rest_clients_impl, guild, user, role): + rest_clients_impl._session.add_guild_member_role.return_value = ... + assert await rest_clients_impl.add_role_to_member(guild, user, role) is None + rest_clients_impl._session.add_guild_member_role.assert_called_once_with( + guild_id="123123123", user_id="4444444", role_id="101010101", reason=... + ) + + @pytest.mark.asyncio + @_helpers.parametrize_valid_id_formats_for_models("guild", 123123123, guilds.Guild) + @_helpers.parametrize_valid_id_formats_for_models("user", 4444444, users.User) + @_helpers.parametrize_valid_id_formats_for_models("role", 101010101, guilds.GuildRole) + async def test_remove_role_from_member_with_reason(self, rest_clients_impl, guild, user, role): + rest_clients_impl._session.remove_guild_member_role.return_value = ... + assert await rest_clients_impl.remove_role_from_member(guild, user, role, reason="Get role'd") is None + rest_clients_impl._session.remove_guild_member_role.assert_called_once_with( + guild_id="123123123", user_id="4444444", role_id="101010101", reason="Get role'd" + ) + + @pytest.mark.asyncio + @_helpers.parametrize_valid_id_formats_for_models("guild", 123123123, guilds.Guild) + @_helpers.parametrize_valid_id_formats_for_models("user", 4444444, users.User) + @_helpers.parametrize_valid_id_formats_for_models("role", 101010101, guilds.GuildRole) + async def test_remove_role_from_member_without_reason(self, rest_clients_impl, guild, user, role): + rest_clients_impl._session.remove_guild_member_role.return_value = ... + assert await rest_clients_impl.remove_role_from_member(guild, user, role) is None + rest_clients_impl._session.remove_guild_member_role.assert_called_once_with( + guild_id="123123123", user_id="4444444", role_id="101010101", reason=... + ) + + @pytest.mark.asyncio + @_helpers.parametrize_valid_id_formats_for_models("guild", 123123123, guilds.Guild) + @_helpers.parametrize_valid_id_formats_for_models("user", 4444444, users.User) + async def test_kick_member_with_reason(self, rest_clients_impl, guild, user): + rest_clients_impl._session.remove_guild_member.return_value = ... + assert await rest_clients_impl.kick_member(guild, user, reason="TO DO") is None + rest_clients_impl._session.remove_guild_member.assert_called_once_with( + guild_id="123123123", user_id="4444444", reason="TO DO" + ) + + @pytest.mark.asyncio + @_helpers.parametrize_valid_id_formats_for_models("guild", 123123123, guilds.Guild) + @_helpers.parametrize_valid_id_formats_for_models("user", 4444444, users.User) + async def test_kick_member_without_reason(self, rest_clients_impl, guild, user): + rest_clients_impl._session.remove_guild_member.return_value = ... + assert await rest_clients_impl.kick_member(guild, user) is None + rest_clients_impl._session.remove_guild_member.assert_called_once_with( + guild_id="123123123", user_id="4444444", reason=..., + ) + + @pytest.mark.asyncio + @_helpers.parametrize_valid_id_formats_for_models("guild", 123123123, guilds.Guild) + @_helpers.parametrize_valid_id_formats_for_models("user", 4444444, users.User) + async def test_fetch_ban(self, rest_clients_impl, guild, user): + mock_ban_payload = {"reason": "42'd", "user": {}} + mock_ban_obj = mock.MagicMock(guilds.GuildMemberBan) + rest_clients_impl._session.get_guild_ban.return_value = mock_ban_payload + with mock.patch.object(guilds.GuildMemberBan, "deserialize", return_value=mock_ban_obj): + assert await rest_clients_impl.fetch_ban(guild, user) is mock_ban_obj + rest_clients_impl._session.get_guild_ban.assert_called_once_with(guild_id="123123123", user_id="4444444") + guilds.GuildMemberBan.deserialize.assert_called_once_with(mock_ban_payload) + + @pytest.mark.asyncio + @_helpers.parametrize_valid_id_formats_for_models("guild", 123123123, guilds.Guild) + async def test_fetch_bans(self, rest_clients_impl, guild): + mock_ban_payload = {"reason": "42'd", "user": {}} + mock_ban_obj = mock.MagicMock(guilds.GuildMemberBan) + rest_clients_impl._session.get_guild_bans.return_value = [mock_ban_payload] + with mock.patch.object(guilds.GuildMemberBan, "deserialize", return_value=mock_ban_obj): + assert await rest_clients_impl.fetch_bans(guild) == [mock_ban_obj] + rest_clients_impl._session.get_guild_bans.assert_called_once_with(guild_id="123123123") + guilds.GuildMemberBan.deserialize.assert_called_once_with(mock_ban_payload) + + @pytest.mark.asyncio + @_helpers.parametrize_valid_id_formats_for_models("guild", 123123123, guilds.Guild) + @_helpers.parametrize_valid_id_formats_for_models("user", 4444444, users.User) + @pytest.mark.parametrize("delete_message_days", [datetime.timedelta(days=12), 12]) + async def test_ban_member_with_optionals(self, rest_clients_impl, guild, user, delete_message_days): + rest_clients_impl._session.create_guild_ban.return_value = ... + result = await rest_clients_impl.ban_member(guild, user, delete_message_days=delete_message_days, reason="bye") + assert result is None + rest_clients_impl._session.create_guild_ban.assert_called_once_with( + guild_id="123123123", user_id="4444444", delete_message_days=12, reason="bye" + ) + + @pytest.mark.asyncio + @_helpers.parametrize_valid_id_formats_for_models("guild", 123123123, guilds.Guild) + @_helpers.parametrize_valid_id_formats_for_models("user", 4444444, users.User) + async def test_ban_member_without_optionals(self, rest_clients_impl, guild, user): + rest_clients_impl._session.create_guild_ban.return_value = ... + assert await rest_clients_impl.ban_member(guild, user) is None + rest_clients_impl._session.create_guild_ban.assert_called_once_with( + guild_id="123123123", user_id="4444444", delete_message_days=..., reason=... + ) + + @pytest.mark.asyncio + @_helpers.parametrize_valid_id_formats_for_models("guild", 123123123, guilds.Guild) + @_helpers.parametrize_valid_id_formats_for_models("user", 4444444, users.User) + async def test_unban_member_with_reason(self, rest_clients_impl, guild, user): + rest_clients_impl._session.remove_guild_ban.return_value = ... + result = await rest_clients_impl.unban_member(guild, user, reason="bye") + assert result is None + rest_clients_impl._session.remove_guild_ban.assert_called_once_with( + guild_id="123123123", user_id="4444444", reason="bye" + ) + + @pytest.mark.asyncio + @_helpers.parametrize_valid_id_formats_for_models("guild", 123123123, guilds.Guild) + @_helpers.parametrize_valid_id_formats_for_models("user", 4444444, users.User) + async def test_unban_member_without_reason(self, rest_clients_impl, guild, user): + rest_clients_impl._session.remove_guild_ban.return_value = ... + assert await rest_clients_impl.unban_member(guild, user) is None + rest_clients_impl._session.remove_guild_ban.assert_called_once_with( + guild_id="123123123", user_id="4444444", reason=... + ) + + @pytest.mark.asyncio + @_helpers.parametrize_valid_id_formats_for_models("guild", 574921006817476608, guilds.Guild) + async def test_fetch_roles(self, rest_clients_impl, guild): + mock_role_payload = {"id": "33030", "permissions": 333, "name": "ROlE"} + mock_role_obj = mock.MagicMock(guilds.GuildRole, id=33030) + rest_clients_impl._session.get_guild_roles.return_value = [mock_role_payload] + with mock.patch.object(guilds.GuildRole, "deserialize", return_value=mock_role_obj): + assert await rest_clients_impl.fetch_roles(guild) == {33030: mock_role_obj} + rest_clients_impl._session.get_guild_roles.assert_called_once_with(guild_id="574921006817476608") + guilds.GuildRole.deserialize.assert_called_once_with(mock_role_payload) + + @pytest.mark.asyncio + @_helpers.parametrize_valid_id_formats_for_models("guild", 574921006817476608, guilds.Guild) + async def test_create_role_with_optionals(self, rest_clients_impl, guild): + mock_role_payload = {"id": "033030", "permissions": 333, "name": "ROlE"} + mock_role_obj = mock.MagicMock(guilds.GuildRole) + rest_clients_impl._session.create_guild_role.return_value = mock_role_payload + with mock.patch.object(guilds.GuildRole, "deserialize", return_value=mock_role_obj): + result = await rest_clients_impl.create_role( + guild, + name="Roleington", + permissions=permissions.Permission.STREAM | permissions.Permission.EMBED_LINKS, + color=colors.Color(21312), + hoist=True, + mentionable=False, + reason="And then there was a role.", + ) + assert result is mock_role_obj + rest_clients_impl._session.create_guild_role.assert_called_once_with( + guild_id="574921006817476608", + name="Roleington", + permissions=16896, + color=21312, + hoist=True, + mentionable=False, + reason="And then there was a role.", + ) + guilds.GuildRole.deserialize.assert_called_once_with(mock_role_payload) + + @pytest.mark.asyncio + @_helpers.parametrize_valid_id_formats_for_models("guild", 574921006817476608, guilds.Guild) + async def test_create_role_without_optionals(self, rest_clients_impl, guild): + mock_role_payload = {"id": "033030", "permissions": 333, "name": "ROlE"} + mock_role_obj = mock.MagicMock(guilds.GuildRole) + rest_clients_impl._session.create_guild_role.return_value = mock_role_payload + with mock.patch.object(guilds.GuildRole, "deserialize", return_value=mock_role_obj): + result = await rest_clients_impl.create_role(guild) + assert result is mock_role_obj + rest_clients_impl._session.create_guild_role.assert_called_once_with( + guild_id="574921006817476608", + name=..., + permissions=..., + color=..., + hoist=..., + mentionable=..., + reason=..., + ) + guilds.GuildRole.deserialize.assert_called_once_with(mock_role_payload) + + @pytest.mark.asyncio + @_helpers.parametrize_valid_id_formats_for_models("guild", 574921006817476608, guilds.Guild) + @_helpers.parametrize_valid_id_formats_for_models("role", 123123, guilds.GuildRole) + @_helpers.parametrize_valid_id_formats_for_models("additional_role", 123456, guilds.GuildRole) + async def test_reposition_roles(self, rest_clients_impl, guild, role, additional_role): + mock_role_payload = {"id": "033030", "permissions": 333, "name": "ROlE"} + mock_role_obj = mock.MagicMock(guilds.GuildRole) + rest_clients_impl._session.modify_guild_role_positions.return_value = [mock_role_payload] + with mock.patch.object(guilds.GuildRole, "deserialize", return_value=mock_role_obj): + result = await rest_clients_impl.reposition_roles(guild, (1, role), (2, additional_role)) + assert result == [mock_role_obj] + rest_clients_impl._session.modify_guild_role_positions.assert_called_once_with( + "574921006817476608", ("123123", 1), ("123456", 2) + ) + guilds.GuildRole.deserialize.assert_called_once_with(mock_role_payload) + + @pytest.mark.asyncio + @_helpers.parametrize_valid_id_formats_for_models("guild", 574921006817476608, guilds.Guild) + @_helpers.parametrize_valid_id_formats_for_models("role", 123123, guilds.GuildRole) + async def test_update_role_with_optionals(self, rest_clients_impl, guild, role): + mock_role_payload = {"id": "033030", "permissions": 333, "name": "ROlE"} + mock_role_obj = mock.MagicMock(guilds.GuildRole) + rest_clients_impl._session.modify_guild_role.return_value = mock_role_payload + with mock.patch.object(guilds.GuildRole, "deserialize", return_value=mock_role_obj): + result = await rest_clients_impl.update_role( + guild, + role, + name="ROLE", + permissions=permissions.Permission.STREAM | permissions.Permission.EMBED_LINKS, + color=colors.Color(12312), + hoist=True, + mentionable=False, + reason="Why not?", + ) + assert result is mock_role_obj + rest_clients_impl._session.modify_guild_role.assert_called_once_with( + guild_id="574921006817476608", + role_id="123123", + name="ROLE", + permissions=16896, + color=12312, + hoist=True, + mentionable=False, + reason="Why not?", + ) + guilds.GuildRole.deserialize.assert_called_once_with(mock_role_payload) + + @pytest.mark.asyncio + @_helpers.parametrize_valid_id_formats_for_models("guild", 574921006817476608, guilds.Guild) + @_helpers.parametrize_valid_id_formats_for_models("role", 123123, guilds.GuildRole) + async def test_update_role_without_optionals(self, rest_clients_impl, guild, role): + mock_role_payload = {"id": "033030", "permissions": 333, "name": "ROlE"} + mock_role_obj = mock.MagicMock(guilds.GuildRole) + rest_clients_impl._session.modify_guild_role.return_value = mock_role_payload + with mock.patch.object(guilds.GuildRole, "deserialize", return_value=mock_role_obj): + assert await rest_clients_impl.update_role(guild, role) is mock_role_obj + rest_clients_impl._session.modify_guild_role.assert_called_once_with( + guild_id="574921006817476608", + role_id="123123", + name=..., + permissions=..., + color=..., + hoist=..., + mentionable=..., + reason=..., + ) + guilds.GuildRole.deserialize.assert_called_once_with(mock_role_payload) + + @pytest.mark.asyncio + @_helpers.parametrize_valid_id_formats_for_models("guild", 574921006817476608, guilds.Guild) + @_helpers.parametrize_valid_id_formats_for_models("role", 123123, guilds.GuildRole) + async def test_delete_role(self, rest_clients_impl, guild, role): + rest_clients_impl._session.delete_guild_role.return_value = ... + assert await rest_clients_impl.delete_role(guild, role) is None + rest_clients_impl._session.delete_guild_role.assert_called_once_with( + guild_id="574921006817476608", role_id="123123" + ) + + @pytest.mark.asyncio + @_helpers.parametrize_valid_id_formats_for_models("guild", 574921006817476608, guilds.Guild) + @pytest.mark.parametrize("days", [7, datetime.timedelta(days=7)]) + async def test_estimate_guild_prune_count(self, rest_clients_impl, guild, days): + rest_clients_impl._session.get_guild_prune_count.return_value = 42 + assert await rest_clients_impl.estimate_guild_prune_count(guild, days) == 42 + rest_clients_impl._session.get_guild_prune_count.assert_called_once_with(guild_id="574921006817476608", days=7) + + @pytest.mark.asyncio + @_helpers.parametrize_valid_id_formats_for_models("guild", 574921006817476608, guilds.Guild) + @pytest.mark.parametrize("days", [7, datetime.timedelta(days=7)]) + async def test_estimate_guild_with_optionals(self, rest_clients_impl, guild, days): + rest_clients_impl._session.begin_guild_prune.return_value = None + assert await rest_clients_impl.begin_guild_prune(guild, days, compute_prune_count=True, reason="nah m8") is None + rest_clients_impl._session.begin_guild_prune.assert_called_once_with( + guild_id="574921006817476608", days=7, compute_prune_count=True, reason="nah m8" + ) + + @pytest.mark.asyncio + @_helpers.parametrize_valid_id_formats_for_models("guild", 574921006817476608, guilds.Guild) + @pytest.mark.parametrize("days", [7, datetime.timedelta(days=7)]) + async def test_estimate_guild_without_optionals(self, rest_clients_impl, guild, days): + rest_clients_impl._session.begin_guild_prune.return_value = 42 + assert await rest_clients_impl.begin_guild_prune(guild, days) == 42 + rest_clients_impl._session.begin_guild_prune.assert_called_once_with( + guild_id="574921006817476608", days=7, compute_prune_count=..., reason=... + ) + + @pytest.mark.asyncio + @_helpers.parametrize_valid_id_formats_for_models("guild", 574921006817476608, guilds.Guild) + async def test_fetch_guild_voice_regions(self, rest_clients_impl, guild): + mock_voice_payload = {"name": "london", "id": "LONDON"} + mock_voice_obj = mock.MagicMock(voices.VoiceRegion) + rest_clients_impl._session.get_guild_voice_regions.return_value = [mock_voice_payload] + with mock.patch.object(voices.VoiceRegion, "deserialize", return_value=mock_voice_obj): + assert await rest_clients_impl.fetch_guild_voice_regions(guild) == [mock_voice_obj] + rest_clients_impl._session.get_guild_voice_regions.assert_called_once_with(guild_id="574921006817476608") + voices.VoiceRegion.deserialize.assert_called_once_with(mock_voice_payload) + + @pytest.mark.asyncio + @_helpers.parametrize_valid_id_formats_for_models("guild", 574921006817476608, guilds.Guild) + async def test_fetch_guild_invites(self, rest_clients_impl, guild): + mock_invite_payload = {"code": "dododo"} + mock_invite_obj = mock.MagicMock(invites.InviteWithMetadata) + rest_clients_impl._session.get_guild_invites.return_value = [mock_invite_payload] + with mock.patch.object(invites.InviteWithMetadata, "deserialize", return_value=mock_invite_obj): + assert await rest_clients_impl.fetch_guild_invites(guild) == [mock_invite_obj] + invites.InviteWithMetadata.deserialize.assert_called_once_with(mock_invite_payload) + + @pytest.mark.asyncio + @_helpers.parametrize_valid_id_formats_for_models("guild", 574921006817476608, guilds.Guild) + async def test_fetch_integrations(self, rest_clients_impl, guild): + mock_integration_payload = {"id": "123123", "name": "Integrated", "type": "twitch"} + mock_integration_obj = mock.MagicMock(guilds.GuildIntegration) + rest_clients_impl._session.get_guild_integrations.return_value = [mock_integration_payload] + with mock.patch.object(guilds.GuildIntegration, "deserialize", return_value=mock_integration_obj): + assert await rest_clients_impl.fetch_integrations(guild) == [mock_integration_obj] + rest_clients_impl._session.get_guild_integrations.assert_called_once_with(guild_id="574921006817476608") + guilds.GuildIntegration.deserialize.assert_called_once_with(mock_integration_payload) + + @pytest.mark.asyncio + @_helpers.parametrize_valid_id_formats_for_models("guild", 574921006817476608, guilds.Guild) + @_helpers.parametrize_valid_id_formats_for_models("integration", 379953393319542784, guilds.GuildIntegration) + @pytest.mark.parametrize("period", [datetime.timedelta(days=7), 7]) + async def test_update_integration_with_optionals(self, rest_clients_impl, guild, integration, period): + rest_clients_impl._session.modify_guild_integration.return_value = ... + result = await rest_clients_impl.update_integration( + guild, + integration, + expire_behaviour=guilds.IntegrationExpireBehaviour.KICK, + expire_grace_period=period, + enable_emojis=True, + reason="GET YEET'D", + ) + assert result is None + rest_clients_impl._session.modify_guild_integration.assert_called_once_with( + guild_id="574921006817476608", + integration_id="379953393319542784", + expire_behaviour=1, + expire_grace_period=7, + enable_emojis=True, + reason="GET YEET'D", + ) + + @pytest.mark.asyncio + @_helpers.parametrize_valid_id_formats_for_models("guild", 574921006817476608, guilds.Guild) + @_helpers.parametrize_valid_id_formats_for_models("integration", 379953393319542784, guilds.GuildIntegration) + async def test_update_integration_without_optionals(self, rest_clients_impl, guild, integration): + rest_clients_impl._session.modify_guild_integration.return_value = ... + assert await rest_clients_impl.update_integration(guild, integration) is None + rest_clients_impl._session.modify_guild_integration.assert_called_once_with( + guild_id="574921006817476608", + integration_id="379953393319542784", + expire_behaviour=..., + expire_grace_period=..., + enable_emojis=..., + reason=..., + ) + + @pytest.mark.asyncio + @_helpers.parametrize_valid_id_formats_for_models("guild", 574921006817476608, guilds.Guild) + @_helpers.parametrize_valid_id_formats_for_models("integration", 379953393319542784, guilds.GuildIntegration) + async def test_delete_integration_with_reason(self, rest_clients_impl, guild, integration): + rest_clients_impl._session.delete_guild_integration.return_value = ... + assert await rest_clients_impl.delete_integration(guild, integration, reason="B Y E") is None + rest_clients_impl._session.delete_guild_integration.assert_called_once_with( + guild_id="574921006817476608", integration_id="379953393319542784", reason="B Y E" + ) + + @pytest.mark.asyncio + @_helpers.parametrize_valid_id_formats_for_models("guild", 574921006817476608, guilds.Guild) + @_helpers.parametrize_valid_id_formats_for_models("integration", 379953393319542784, guilds.GuildIntegration) + async def test_delete_integration_without_reason(self, rest_clients_impl, guild, integration): + rest_clients_impl._session.delete_guild_integration.return_value = ... + assert await rest_clients_impl.delete_integration(guild, integration) is None + rest_clients_impl._session.delete_guild_integration.assert_called_once_with( + guild_id="574921006817476608", integration_id="379953393319542784", reason=... + ) + + @pytest.mark.asyncio + @_helpers.parametrize_valid_id_formats_for_models("guild", 574921006817476608, guilds.Guild) + @_helpers.parametrize_valid_id_formats_for_models("integration", 379953393319542784, guilds.GuildIntegration) + async def test_sync_guild_integration(self, rest_clients_impl, guild, integration): + rest_clients_impl._session.sync_guild_integration.return_value = ... + assert await rest_clients_impl.sync_guild_integration(guild, integration) is None + rest_clients_impl._session.sync_guild_integration.assert_called_once_with( + guild_id="574921006817476608", integration_id="379953393319542784", + ) + + @pytest.mark.asyncio + @_helpers.parametrize_valid_id_formats_for_models("guild", 574921006817476608, guilds.Guild) + async def test_fetch_guild_embed(self, rest_clients_impl, guild): + mock_embed_payload = {"enabled": True, "channel_id": "2020202"} + mock_embed_obj = mock.MagicMock(guilds.GuildEmbed) + rest_clients_impl._session.get_guild_embed.return_value = mock_embed_payload + with mock.patch.object(guilds.GuildEmbed, "deserialize", return_value=mock_embed_obj): + assert await rest_clients_impl.fetch_guild_embed(guild) is mock_embed_obj + rest_clients_impl._session.get_guild_embed.assert_called_once_with(guild_id="574921006817476608") + + @pytest.mark.asyncio + @_helpers.parametrize_valid_id_formats_for_models("guild", 574921006817476608, guilds.Guild) + @_helpers.parametrize_valid_id_formats_for_models("channel", 123123, channels.GuildChannel) + async def test_update_guild_embed_with_optionnal(self, rest_clients_impl, guild, channel): + mock_embed_payload = {"enabled": True, "channel_id": "2020202"} + mock_embed_obj = mock.MagicMock(guilds.GuildEmbed) + rest_clients_impl._session.modify_guild_embed.return_value = mock_embed_payload + with mock.patch.object(guilds.GuildEmbed, "deserialize", return_value=mock_embed_obj): + result = await rest_clients_impl.update_guild_embed(guild, channel=channel, enabled=True, reason="Nyaa!!!") + assert result is mock_embed_obj + rest_clients_impl._session.modify_guild_embed.assert_called_once_with( + guild_id="574921006817476608", channel_id="123123", enabled=True, reason="Nyaa!!!" + ) + + @pytest.mark.asyncio + @_helpers.parametrize_valid_id_formats_for_models("guild", 574921006817476608, guilds.Guild) + async def test_update_guild_embed_without_optionnal(self, rest_clients_impl, guild): + mock_embed_payload = {"enabled": True, "channel_id": "2020202"} + mock_embed_obj = mock.MagicMock(guilds.GuildEmbed) + rest_clients_impl._session.modify_guild_embed.return_value = mock_embed_payload + with mock.patch.object(guilds.GuildEmbed, "deserialize", return_value=mock_embed_obj): + assert await rest_clients_impl.update_guild_embed(guild) is mock_embed_obj + rest_clients_impl._session.modify_guild_embed.assert_called_once_with( + guild_id="574921006817476608", channel_id=..., enabled=..., reason=... + ) + + @pytest.mark.asyncio + @_helpers.parametrize_valid_id_formats_for_models("guild", 574921006817476608, guilds.Guild) + async def test_fetch_guild_vanity_url(self, rest_clients_impl, guild): + mock_vanity_payload = {"code": "akfdk", "uses": 5} + mock_vanity_obj = mock.MagicMock(invites.VanityUrl) + rest_clients_impl._session.get_guild_vanity_url.return_value = mock_vanity_payload + with mock.patch.object(invites.VanityUrl, "deserialize", return_value=mock_vanity_obj): + assert await rest_clients_impl.fetch_guild_vanity_url(guild) is mock_vanity_obj + rest_clients_impl._session.get_guild_vanity_url.assert_called_once_with(guild_id="574921006817476608") + invites.VanityUrl.deserialize.assert_called_once_with(mock_vanity_payload) + + @_helpers.parametrize_valid_id_formats_for_models("guild", 574921006817476608, guilds.Guild) + def test_fetch_guild_widget_image_with_style(self, rest_clients_impl, guild): + mock_url = "not/a/url" + rest_clients_impl._session.get_guild_widget_image_url.return_value = mock_url + assert rest_clients_impl.format_guild_widget_image(guild, style="notAStyle") == mock_url + rest_clients_impl._session.get_guild_widget_image_url.assert_called_once_with( + guild_id="574921006817476608", style="notAStyle", + ) + + @_helpers.parametrize_valid_id_formats_for_models("guild", 574921006817476608, guilds.Guild) + def test_fetch_guild_widget_image_without_style(self, rest_clients_impl, guild): + mock_url = "not/a/url" + rest_clients_impl._session.get_guild_widget_image_url.return_value = mock_url + assert rest_clients_impl.format_guild_widget_image(guild) == mock_url + rest_clients_impl._session.get_guild_widget_image_url.assert_called_once_with( + guild_id="574921006817476608", style=..., + ) + + @pytest.mark.asyncio + @pytest.mark.parametrize("invite", [mock.MagicMock(invites.Invite, code="AAAAAAAAAAAAAAAA"), "AAAAAAAAAAAAAAAA"]) + async def test_fetch_invite_with_counts(self, rest_clients_impl, invite): + mock_invite_payload = {"code": "AAAAAAAAAAAAAAAA", "guild": {}, "channel": {}} + mock_invite_obj = mock.MagicMock(invites.Invite) + rest_clients_impl._session.get_invite.return_value = mock_invite_payload + with mock.patch.object(invites.Invite, "deserialize", return_value=mock_invite_obj): + assert await rest_clients_impl.fetch_invite(invite, with_counts=True) is mock_invite_obj + rest_clients_impl._session.get_invite.assert_called_once_with( + invite_code="AAAAAAAAAAAAAAAA", with_counts=True, + ) + invites.Invite.deserialize.assert_called_once_with(mock_invite_payload) + + @pytest.mark.asyncio + @pytest.mark.parametrize("invite", [mock.MagicMock(invites.Invite, code="AAAAAAAAAAAAAAAA"), "AAAAAAAAAAAAAAAA"]) + async def test_fetch_invite_without_counts(self, rest_clients_impl, invite): + mock_invite_payload = {"code": "AAAAAAAAAAAAAAAA", "guild": {}, "channel": {}} + mock_invite_obj = mock.MagicMock(invites.Invite) + rest_clients_impl._session.get_invite.return_value = mock_invite_payload + with mock.patch.object(invites.Invite, "deserialize", return_value=mock_invite_obj): + assert await rest_clients_impl.fetch_invite(invite) is mock_invite_obj + rest_clients_impl._session.get_invite.assert_called_once_with( + invite_code="AAAAAAAAAAAAAAAA", with_counts=..., + ) + invites.Invite.deserialize.assert_called_once_with(mock_invite_payload) + + @pytest.mark.asyncio + @pytest.mark.parametrize("invite", [mock.MagicMock(invites.Invite, code="AAAAAAAAAAAAAAAA"), "AAAAAAAAAAAAAAAA"]) + async def test_delete_invite(self, rest_clients_impl, invite): + rest_clients_impl._session.delete_invite.return_value = ... + assert await rest_clients_impl.delete_invite(invite) is None + rest_clients_impl._session.delete_invite.assert_called_once_with(invite_code="AAAAAAAAAAAAAAAA") + + @pytest.mark.asyncio + @_helpers.parametrize_valid_id_formats_for_models("user", 123123123, users.User) + async def test_fetch_user(self, rest_clients_impl, user): + mock_user_payload = {"id": "123", "username": "userName"} + mock_user_obj = mock.MagicMock(users.User) + rest_clients_impl._session.get_user.return_value = mock_user_payload + with mock.patch.object(users.User, "deserialize", return_value=mock_user_obj): + assert await rest_clients_impl.fetch_user(user) is mock_user_obj + rest_clients_impl._session.get_user.assert_called_once_with(user_id="123123123") + users.User.deserialize.assert_called_once_with(mock_user_payload) + + @pytest.mark.asyncio + async def test_fetch_application_info(self, rest_clients_impl): + mock_application_payload = {"id": "2929292", "name": "blah blah", "description": "an app"} + mock_application_obj = mock.MagicMock(oauth2.Application) + rest_clients_impl._session.get_current_application_info.return_value = mock_application_payload + with mock.patch.object(oauth2.Application, "deserialize", return_value=mock_application_obj): + assert await rest_clients_impl.fetch_my_application_info() is mock_application_obj + rest_clients_impl._session.get_current_application_info.assert_called_once_with() + oauth2.Application.deserialize.assert_called_once_with(mock_application_payload) + + @pytest.mark.asyncio + async def test_fetch_me(self, rest_clients_impl): + mock_user_payload = {"username": "A User", "id": "202020200202"} + mock_user_obj = mock.MagicMock(users.MyUser) + rest_clients_impl._session.get_current_user.return_value = mock_user_payload + with mock.patch.object(users.MyUser, "deserialize", return_value=mock_user_obj): + assert await rest_clients_impl.fetch_me() is mock_user_obj + rest_clients_impl._session.get_current_user.assert_called_once() + users.MyUser.deserialize.assert_called_once_with(mock_user_payload) + + @pytest.mark.asyncio + async def test_update_me_with_optionals(self, rest_clients_impl): + mock_user_payload = {"id": "424242", "flags": "420", "discriminator": "6969"} + mock_user_obj = mock.MagicMock(users.MyUser) + rest_clients_impl._session.modify_current_user.return_value = mock_user_payload + mock_avatar_obj = mock.MagicMock(io.BytesIO) + mock_avatar_data = mock.MagicMock(bytes) + with mock.patch.object(users.MyUser, "deserialize", return_value=mock_user_obj): + with mock.patch.object(conversions, "get_bytes_from_resource", return_value=mock_avatar_data): + assert ( + await rest_clients_impl.update_me(username="aNewName", avatar_data=mock_avatar_obj) is mock_user_obj + ) + rest_clients_impl._session.modify_current_user.assert_called_once_with( + username="aNewName", avatar=mock_avatar_data + ) + conversions.get_bytes_from_resource.assert_called_once_with(mock_avatar_obj) + users.MyUser.deserialize.assert_called_once_with(mock_user_payload) + + @pytest.mark.asyncio + async def test_update_me_without_optionals(self, rest_clients_impl): + mock_user_payload = {"id": "424242", "flags": "420", "discriminator": "6969"} + mock_user_obj = mock.MagicMock(users.MyUser) + rest_clients_impl._session.modify_current_user.return_value = mock_user_payload + with mock.patch.object(users.MyUser, "deserialize", return_value=mock_user_obj): + assert await rest_clients_impl.update_me() is mock_user_obj + rest_clients_impl._session.modify_current_user.assert_called_once_with(username=..., avatar=...) + users.MyUser.deserialize.assert_called_once_with(mock_user_payload) + + @pytest.mark.asyncio + async def test_fetch_my_connections(self, rest_clients_impl): + mock_connection_payload = {"id": "odnkwu", "type": "twitch", "name": "eric"} + mock_connection_obj = mock.MagicMock(oauth2.OwnConnection) + rest_clients_impl._session.get_current_user_connections.return_value = [mock_connection_payload] + with mock.patch.object(oauth2.OwnConnection, "deserialize", return_value=mock_connection_obj): + assert await rest_clients_impl.fetch_my_connections() == [mock_connection_obj] + rest_clients_impl._session.get_current_user_connections.assert_called_once() + oauth2.OwnConnection.deserialize.assert_called_once_with(mock_connection_payload) + + @_helpers.parametrize_valid_id_formats_for_models("guild", 574921006817476608, guilds.Guild) + def test_fetch_my_guilds_after_with_optionals(self, rest_clients_impl, guild): + mock_generator = mock.AsyncMock() + rest_clients_impl._pagination_handler = mock.MagicMock(return_value=mock_generator) + assert rest_clients_impl.fetch_my_guilds_after(after=guild, limit=50) is mock_generator + rest_clients_impl._pagination_handler.assert_called_once_with( + deserializer=oauth2.OwnGuild.deserialize, + direction="after", + request=rest_clients_impl._session.get_current_user_guilds, + reversing=False, + start="574921006817476608", + limit=50, + ) + + def test_fetch_my_guilds_after_without_optionals(self, rest_clients_impl): + mock_generator = mock.AsyncMock() + rest_clients_impl._pagination_handler = mock.MagicMock(return_value=mock_generator) + assert rest_clients_impl.fetch_my_guilds_after() is mock_generator + rest_clients_impl._pagination_handler.assert_called_once_with( + deserializer=oauth2.OwnGuild.deserialize, + direction="after", + request=rest_clients_impl._session.get_current_user_guilds, + reversing=False, + start="0", + limit=None, + ) + + def test_fetch_my_guilds_after_with_datetime_object(self, rest_clients_impl): + mock_generator = mock.AsyncMock() + rest_clients_impl._pagination_handler = mock.MagicMock(return_value=mock_generator) + date = datetime.datetime(2019, 1, 22, 18, 41, 15, 283_000, tzinfo=datetime.timezone.utc) + assert rest_clients_impl.fetch_my_guilds_after(after=date) is mock_generator + rest_clients_impl._pagination_handler.assert_called_once_with( + deserializer=oauth2.OwnGuild.deserialize, + direction="after", + request=rest_clients_impl._session.get_current_user_guilds, + reversing=False, + start="537340988620800000", + limit=None, + ) + + @_helpers.parametrize_valid_id_formats_for_models("guild", 574921006817476608, guilds.Guild) + def test_fetch_my_guilds_before_with_optionals(self, rest_clients_impl, guild): + mock_generator = mock.AsyncMock() + rest_clients_impl._pagination_handler = mock.MagicMock(return_value=mock_generator) + assert rest_clients_impl.fetch_my_guilds_before(before=guild, limit=50) is mock_generator + rest_clients_impl._pagination_handler.assert_called_once_with( + deserializer=oauth2.OwnGuild.deserialize, + direction="before", + request=rest_clients_impl._session.get_current_user_guilds, + reversing=False, + start="574921006817476608", + limit=50, + ) + + def test_fetch_my_guilds_before_without_optionals(self, rest_clients_impl): + mock_generator = mock.AsyncMock() + rest_clients_impl._pagination_handler = mock.MagicMock(return_value=mock_generator) + assert rest_clients_impl.fetch_my_guilds_before() is mock_generator + rest_clients_impl._pagination_handler.assert_called_once_with( + deserializer=oauth2.OwnGuild.deserialize, + direction="before", + request=rest_clients_impl._session.get_current_user_guilds, + reversing=False, + start=None, + limit=None, + ) + + def test_fetch_my_guilds_before_with_datetime_object(self, rest_clients_impl): + mock_generator = mock.AsyncMock() + rest_clients_impl._pagination_handler = mock.MagicMock(return_value=mock_generator) + date = datetime.datetime(2019, 1, 22, 18, 41, 15, 283_000, tzinfo=datetime.timezone.utc) + assert rest_clients_impl.fetch_my_guilds_before(before=date) is mock_generator + rest_clients_impl._pagination_handler.assert_called_once_with( + deserializer=oauth2.OwnGuild.deserialize, + direction="before", + request=rest_clients_impl._session.get_current_user_guilds, + reversing=False, + start="537340988620800000", + limit=None, + ) + + @pytest.mark.asyncio + @_helpers.parametrize_valid_id_formats_for_models("guild", 574921006817476608, guilds.Guild) + async def test_leave_guild(self, rest_clients_impl, guild): + rest_clients_impl._session.leave_guild.return_value = ... + assert await rest_clients_impl.leave_guild(guild) is None + rest_clients_impl._session.leave_guild.assert_called_once_with(guild_id="574921006817476608") + + @pytest.mark.asyncio + @_helpers.parametrize_valid_id_formats_for_models("recipient", 115590097100865541, users.User) + async def test_create_dm_channel(self, rest_clients_impl, recipient): + mock_dm_payload = {"id": "2202020", "type": 2, "recipients": []} + mock_dm_obj = mock.MagicMock(channels.DMChannel) + rest_clients_impl._session.create_dm.return_value = mock_dm_payload + with mock.patch.object(channels.DMChannel, "deserialize", return_value=mock_dm_obj): + assert await rest_clients_impl.create_dm_channel(recipient) is mock_dm_obj + rest_clients_impl._session.create_dm.assert_called_once_with(recipient_id="115590097100865541") + channels.DMChannel.deserialize.assert_called_once_with(mock_dm_payload) + + @pytest.mark.asyncio + async def test_fetch_voice_regions(self, rest_clients_impl): + mock_voice_payload = {"id": "LONDON", "name": "london"} + mock_voice_obj = mock.MagicMock(voices.VoiceRegion) + rest_clients_impl._session.list_voice_regions.return_value = [mock_voice_payload] + with mock.patch.object(voices.VoiceRegion, "deserialize", return_value=mock_voice_obj): + assert await rest_clients_impl.fetch_voice_regions() == [mock_voice_obj] + rest_clients_impl._session.list_voice_regions.assert_called_once() + voices.VoiceRegion.deserialize.assert_called_once_with(mock_voice_payload) + + @pytest.mark.asyncio + @_helpers.parametrize_valid_id_formats_for_models("channel", 115590097100865541, channels.Channel) + async def test_create_webhook_with_optionals(self, rest_clients_impl, channel): + mock_webhook_payload = {"id": "29292929", "channel_id": "2292992"} + mock_webhook_obj = mock.MagicMock(webhooks.Webhook) + rest_clients_impl._session.create_webhook.return_value = mock_webhook_payload + mock_image_obj = mock.MagicMock(io.BytesIO) + mock_image_data = mock.MagicMock(bytes) + with mock.patch.object(webhooks.Webhook, "deserialize", return_value=mock_webhook_obj): + with mock.patch.object(conversions, "get_bytes_from_resource", return_value=mock_image_data): + result = await rest_clients_impl.create_webhook( + channel=channel, name="aWebhook", avatar_data=mock_image_obj, reason="And a webhook is born." + ) + assert result is mock_webhook_obj + conversions.get_bytes_from_resource.assert_called_once_with(mock_image_obj) + webhooks.Webhook.deserialize.assert_called_once_with(mock_webhook_payload) + rest_clients_impl._session.create_webhook.assert_called_once_with( + channel_id="115590097100865541", name="aWebhook", avatar=mock_image_data, reason="And a webhook is born." + ) + + @pytest.mark.asyncio + @_helpers.parametrize_valid_id_formats_for_models("channel", 115590097100865541, channels.Channel) + async def test_create_webhook_without_optionals(self, rest_clients_impl, channel): + mock_webhook_payload = {"id": "29292929", "channel_id": "2292992"} + mock_webhook_obj = mock.MagicMock(webhooks.Webhook) + rest_clients_impl._session.create_webhook.return_value = mock_webhook_payload + with mock.patch.object(webhooks.Webhook, "deserialize", return_value=mock_webhook_obj): + assert await rest_clients_impl.create_webhook(channel=channel, name="aWebhook") is mock_webhook_obj + webhooks.Webhook.deserialize.assert_called_once_with(mock_webhook_payload) + rest_clients_impl._session.create_webhook.assert_called_once_with( + channel_id="115590097100865541", name="aWebhook", avatar=..., reason=... + ) + + @pytest.mark.asyncio + @_helpers.parametrize_valid_id_formats_for_models("channel", 115590097100865541, channels.GuildChannel) + async def test_fetch_channel_webhooks(self, rest_clients_impl, channel): + mock_webhook_payload = {"id": "29292929", "channel_id": "2292992"} + mock_webhook_obj = mock.MagicMock(webhooks.Webhook) + rest_clients_impl._session.get_channel_webhooks.return_value = [mock_webhook_payload] + with mock.patch.object(webhooks.Webhook, "deserialize", return_value=mock_webhook_obj): + assert await rest_clients_impl.fetch_channel_webhooks(channel) == [mock_webhook_obj] + rest_clients_impl._session.get_channel_webhooks.assert_called_once_with(channel_id="115590097100865541") + webhooks.Webhook.deserialize.assert_called_once_with(mock_webhook_payload) + + @pytest.mark.asyncio + @_helpers.parametrize_valid_id_formats_for_models("channel", 115590097100865541, channels.GuildChannel) + async def test_fetch_guild_webhooks(self, rest_clients_impl, channel): + mock_webhook_payload = {"id": "29292929", "channel_id": "2292992"} + mock_webhook_obj = mock.MagicMock(webhooks.Webhook) + rest_clients_impl._session.get_guild_webhooks.return_value = [mock_webhook_payload] + with mock.patch.object(webhooks.Webhook, "deserialize", return_value=mock_webhook_obj): + assert await rest_clients_impl.fetch_guild_webhooks(channel) == [mock_webhook_obj] + rest_clients_impl._session.get_guild_webhooks.assert_called_once_with(guild_id="115590097100865541") + webhooks.Webhook.deserialize.assert_called_once_with(mock_webhook_payload) + + @pytest.mark.asyncio + @_helpers.parametrize_valid_id_formats_for_models("webhook", 379953393319542784, webhooks.Webhook) + async def test_fetch_webhook_with_webhook_token(self, rest_clients_impl, webhook): + mock_webhook_payload = {"id": "29292929", "channel_id": "2292992"} + mock_webhook_obj = mock.MagicMock(webhooks.Webhook) + rest_clients_impl._session.get_webhook.return_value = mock_webhook_payload + with mock.patch.object(webhooks.Webhook, "deserialize", return_value=mock_webhook_obj): + assert await rest_clients_impl.fetch_webhook(webhook, webhook_token="dsawqoepql.kmsdao") is mock_webhook_obj + rest_clients_impl._session.get_webhook.assert_called_once_with( + webhook_id="379953393319542784", webhook_token="dsawqoepql.kmsdao", + ) + webhooks.Webhook.deserialize.assert_called_once_with(mock_webhook_payload) + + @pytest.mark.asyncio + @_helpers.parametrize_valid_id_formats_for_models("webhook", 379953393319542784, webhooks.Webhook) + async def test_fetch_webhook_without_webhook_token(self, rest_clients_impl, webhook): + mock_webhook_payload = {"id": "29292929", "channel_id": "2292992"} + mock_webhook_obj = mock.MagicMock(webhooks.Webhook) + rest_clients_impl._session.get_webhook.return_value = mock_webhook_payload + with mock.patch.object(webhooks.Webhook, "deserialize", return_value=mock_webhook_obj): + assert await rest_clients_impl.fetch_webhook(webhook) is mock_webhook_obj + rest_clients_impl._session.get_webhook.assert_called_once_with( + webhook_id="379953393319542784", webhook_token=..., + ) + webhooks.Webhook.deserialize.assert_called_once_with(mock_webhook_payload) + + @pytest.mark.asyncio + @_helpers.parametrize_valid_id_formats_for_models("webhook", 379953393319542784, webhooks.Webhook) + @_helpers.parametrize_valid_id_formats_for_models("channel", 115590097100865541, webhooks.Webhook) + async def test_update_webhook_with_optionals(self, rest_clients_impl, webhook, channel): + mock_webhook_obj = mock.MagicMock(webhooks.Webhook) + mock_webhook_payload = {"id": "123123", "avatar": "1wedoklpasdoiksdoka"} + rest_clients_impl._session.modify_webhook.return_value = mock_webhook_payload + mock_image_obj = mock.MagicMock(io.BytesIO) + mock_image_data = mock.MagicMock(bytes) + with mock.patch.object(webhooks.Webhook, "deserialize", return_value=mock_webhook_obj): + with mock.patch.object(conversions, "get_bytes_from_resource", return_value=mock_image_data): + result = await rest_clients_impl.update_webhook( + webhook, + webhook_token="a.wEbHoOk.ToKeN", + name="blah_blah_blah", + avatar_data=mock_image_obj, + channel=channel, + reason="A reason", + ) + assert result is mock_webhook_obj + rest_clients_impl._session.modify_webhook.assert_called_once_with( + webhook_id="379953393319542784", + webhook_token="a.wEbHoOk.ToKeN", + name="blah_blah_blah", + avatar=mock_image_data, + channel_id="115590097100865541", + reason="A reason", + ) + webhooks.Webhook.deserialize.assert_called_once_with(mock_webhook_payload) + + @pytest.mark.asyncio + @_helpers.parametrize_valid_id_formats_for_models("webhook", 379953393319542784, webhooks.Webhook) + async def test_update_webhook_without_optionals(self, rest_clients_impl, webhook): + mock_webhook_obj = mock.MagicMock(webhooks.Webhook) + mock_webhook_payload = {"id": "123123", "avatar": "1wedoklpasdoiksdoka"} + rest_clients_impl._session.modify_webhook.return_value = mock_webhook_payload + with mock.patch.object(webhooks.Webhook, "deserialize", return_value=mock_webhook_obj): + assert await rest_clients_impl.update_webhook(webhook) is mock_webhook_obj + rest_clients_impl._session.modify_webhook.assert_called_once_with( + webhook_id="379953393319542784", webhook_token=..., name=..., avatar=..., channel_id=..., reason=..., + ) + webhooks.Webhook.deserialize.assert_called_once_with(mock_webhook_payload) + + @pytest.mark.asyncio + @_helpers.parametrize_valid_id_formats_for_models("webhook", 379953393319542784, webhooks.Webhook) + async def test_delete_webhook_with_webhook_token(self, rest_clients_impl, webhook): + rest_clients_impl._session.delete_webhook.return_value = ... + assert await rest_clients_impl.delete_webhook(webhook, webhook_token="dsawqoepql.kmsdao") is None + rest_clients_impl._session.delete_webhook.assert_called_once_with( + webhook_id="379953393319542784", webhook_token="dsawqoepql.kmsdao" + ) + + @pytest.mark.asyncio + @_helpers.parametrize_valid_id_formats_for_models("webhook", 379953393319542784, webhooks.Webhook) + async def test_delete_webhook_without_webhook_token(self, rest_clients_impl, webhook): + rest_clients_impl._session.delete_webhook.return_value = ... + assert await rest_clients_impl.delete_webhook(webhook) is None + rest_clients_impl._session.delete_webhook.assert_called_once_with( + webhook_id="379953393319542784", webhook_token=... + ) + + @pytest.mark.asyncio + @_helpers.parametrize_valid_id_formats_for_models("webhook", 379953393319542784, webhooks.Webhook) + async def test_execute_webhook_without_optionals(self, rest_clients_impl, webhook): + rest_clients_impl._session.execute_webhook.return_value = ... + mock_allowed_mentions_payload = {"parse": ["everyone", "users", "roles"]} + rest_clients_impl._generate_allowed_mentions = mock.MagicMock(return_value=mock_allowed_mentions_payload) + assert await rest_clients_impl.execute_webhook(webhook, "a.webhook.token") is None + rest_clients_impl._session.execute_webhook.assert_called_once_with( + webhook_id="379953393319542784", + webhook_token="a.webhook.token", + content=..., + username=..., + avatar_url=..., + tts=..., + wait=False, + file=..., + embeds=..., + allowed_mentions=mock_allowed_mentions_payload, + ) + + @pytest.mark.asyncio + @_helpers.parametrize_valid_id_formats_for_models("webhook", 379953393319542784, webhooks.Webhook) + async def test_execute_webhook_with_optionals(self, rest_clients_impl, webhook): + rest_clients_impl._session.execute_webhook.return_value = ... + mock_allowed_mentions_payload = {"parse": ["everyone", "users", "roles"]} + rest_clients_impl._generate_allowed_mentions = mock.MagicMock(return_value=mock_allowed_mentions_payload) + mock_embed_payload = {"description": "424242"} + mock_embed_obj = mock.create_autospec( + embeds.Embed, auto_spec=True, serialize=mock.MagicMock(return_value=mock_embed_payload) + ) + mock_media_obj = mock.MagicMock() + mock_media_payload = ("aName.png", mock.MagicMock()) + with mock.patch.object(media, "safe_read_file", return_value=mock_media_payload): + with mock.patch.object(messages.Message, "deserialize"): + await rest_clients_impl.execute_webhook( + webhook, + "a.webhook.token", + content="THE TRUTH", + username="User 97", + avatar_url="httttttt/L//", + tts=True, + wait=True, + file=mock_media_obj, + embeds=[mock_embed_obj], + mentions_everyone=False, + role_mentions=False, + user_mentions=False, + ) + media.safe_read_file.assert_called_once_with(mock_media_obj) + rest_clients_impl._session.execute_webhook.assert_called_once_with( + webhook_id="379953393319542784", + webhook_token="a.webhook.token", + content="THE TRUTH", + username="User 97", + avatar_url="httttttt/L//", + tts=True, + wait=True, + file=mock_media_payload, + embeds=[mock_embed_payload], + allowed_mentions=mock_allowed_mentions_payload, + ) + mock_embed_obj.serialize.assert_called_once() + rest_clients_impl._generate_allowed_mentions.assert_called_once_with( + mentions_everyone=False, user_mentions=False, role_mentions=False + ) + + @pytest.mark.asyncio + @_helpers.parametrize_valid_id_formats_for_models("webhook", 379953393319542784, webhooks.Webhook) + async def test_execute_webhook_returns_message_when_wait_is_true(self, rest_clients_impl, webhook): + mock_message_payload = {"id": "6796959949034", "content": "Nyaa Nyaa"} + mock_message_obj = mock.MagicMock(messages.Message) + rest_clients_impl._session.execute_webhook.return_value = mock_message_payload + mock_allowed_mentions_payload = {"parse": ["everyone", "users", "roles"]} + rest_clients_impl._generate_allowed_mentions = mock.MagicMock(return_value=mock_allowed_mentions_payload) + with mock.patch.object(messages.Message, "deserialize", return_value=mock_message_obj): + assert await rest_clients_impl.execute_webhook(webhook, "a.webhook.token", wait=True) is mock_message_obj + messages.Message.deserialize.assert_called_once_with(mock_message_payload) + + @pytest.mark.asyncio + async def test_safe_execute_webhook_without_optionals(self, rest_clients_impl): + webhook = mock.MagicMock(webhooks.Webhook) + mock_message_obj = mock.MagicMock(messages.Message) + rest_clients_impl.execute_webhook = mock.AsyncMock(return_value=mock_message_obj) + result = await rest_clients_impl.safe_webhook_execute(webhook, "a.webhook.token",) + assert result is mock_message_obj + rest_clients_impl.execute_webhook.assert_called_once_with( + webhook=webhook, + webhook_token="a.webhook.token", + content=..., + username=..., + avatar_url=..., + tts=..., + wait=False, + file=..., + embeds=..., + mentions_everyone=False, + user_mentions=False, + role_mentions=False, + ) + + @pytest.mark.asyncio + async def test_safe_execute_webhook_with_optionals(self, rest_clients_impl): + webhook = mock.MagicMock(webhooks.Webhook) + mock_media_obj = mock.MagicMock(bytes) + mock_embed_obj = mock.MagicMock(embeds.Embed) + mock_message_obj = mock.MagicMock(messages.Message) + rest_clients_impl.execute_webhook = mock.AsyncMock(return_value=mock_message_obj) + result = await rest_clients_impl.safe_webhook_execute( + webhook, + "a.webhook.token", + content="THE TRUTH", + username="User 97", + avatar_url="httttttt/L//", + tts=True, + wait=True, + file=mock_media_obj, + embeds=[mock_embed_obj], + mentions_everyone=False, + role_mentions=False, + user_mentions=False, + ) + assert result is mock_message_obj + rest_clients_impl.execute_webhook.assert_called_once_with( + webhook=webhook, + webhook_token="a.webhook.token", + content="THE TRUTH", + username="User 97", + avatar_url="httttttt/L//", + tts=True, + wait=True, + file=mock_media_obj, + embeds=[mock_embed_obj], + mentions_everyone=False, + role_mentions=False, + user_mentions=False, + ) diff --git a/tests/hikari/internal/test_conversions.py b/tests/hikari/internal/test_conversions.py index 5bc2491096..1f2179ee12 100644 --- a/tests/hikari/internal/test_conversions.py +++ b/tests/hikari/internal/test_conversions.py @@ -21,6 +21,7 @@ import inspect import io +import cymock as mock import pytest import typing @@ -275,3 +276,21 @@ def test_not_resolved_is_failure(self): conversions.snoop_typehint_from_scope(frame, attr) finally: del frame + + +@pytest.mark.parametrize( + "input", + [ + b"hello", + bytearray("hello", "utf-8"), + memoryview(b"hello"), + io.BytesIO(b"hello"), + mock.MagicMock(io.BufferedRandom, read=mock.MagicMock(return_value=b"hello")), + mock.MagicMock(io.BufferedReader, read=mock.MagicMock(return_value=b"hello")), + mock.MagicMock(io.BufferedRWPair, read=mock.MagicMock(return_value=b"hello")), + ], +) +def test_get_bytes_from_resource(input): + assert conversions.get_bytes_from_resource(input) == b"hello" + if isinstance(input, mock.MagicMock): + input.read.assert_called_once() diff --git a/tests/hikari/net/test_rest.py b/tests/hikari/net/test_rest.py index 28c57502a8..38930d8384 100644 --- a/tests/hikari/net/test_rest.py +++ b/tests/hikari/net/test_rest.py @@ -37,18 +37,26 @@ from tests.hikari import _helpers -class TestRestfulClient: +class TestLowLevelRestfulClient: @pytest.fixture def rest_impl(self): - class RestfulClientImpl(rest.RestfulClient): + class LowLevelRestfulClientImpl(rest.LowLevelRestfulClient): def __init__(self, *args, **kwargs): self.base_url = "https://discordapp.com/api/v6" self.client_session = mock.MagicMock(close=mock.AsyncMock()) self.logger = mock.MagicMock() - self.ratelimiter = mock.MagicMock(close=mock.MagicMock()) + self.ratelimiter = mock.create_autospec( + ratelimits.HTTPBucketRateLimiterManager, + auto_spec=True, + acquire=mock.MagicMock(), + update_rate_limits=mock.MagicMock(), + ) + self.global_ratelimiter = mock.create_autospec( + ratelimits.ManualRateLimiter, auto_spec=True, acquire=mock.MagicMock(), throttle=mock.MagicMock() + ) self._request = mock.AsyncMock(return_value=...) - return RestfulClientImpl() + return LowLevelRestfulClientImpl() @pytest.fixture def compiled_route(self): @@ -88,13 +96,13 @@ async def __aexit__(self, exc_type, exc_val, exc_tb): @pytest.mark.asyncio async def test_rest___aenter___and___aexit__(self): - class RestfulClientImpl(rest.RestfulClient): + class LowLevelRestfulClientImpl(rest.LowLevelRestfulClient): def __init__(self, *args, **kwargs): kwargs.setdefault("token", "Bearer xxx") super().__init__(*args, **kwargs) self.close = mock.AsyncMock() - inst = RestfulClientImpl() + inst = LowLevelRestfulClientImpl() async with inst as client: assert client is inst @@ -103,13 +111,13 @@ def __init__(self, *args, **kwargs): @pytest.mark.asyncio async def test_rest_close_calls_client_session_close(self): - class RestfulClientImpl(rest.RestfulClient): + class LowLevelRestfulClientImpl(rest.LowLevelRestfulClient): def __init__(self, *args, **kwargs): self.client_session = mock.MagicMock() self.client_session.close = mock.AsyncMock() self.logger = logging.getLogger(__name__) - inst = RestfulClientImpl() + inst = LowLevelRestfulClientImpl() await inst.close() @@ -125,7 +133,7 @@ async def test__init__with_bot_token_and_without_optionals(self): stack.enter_context(mock.patch.object(ratelimits, "HTTPBucketRateLimiterManager", return_value=buckets_mock)) with stack: - client = rest.RestfulClient(token="Bot token.otacon.a-token") + client = rest.LowLevelRestfulClient(token="Bot token.otacon.a-token") assert client.base_url == f"https://discordapp.com/api/v{int(versions.HTTPAPIVersion.STABLE)}" assert client.global_ratelimiter is mock_manual_rate_limiter @@ -136,7 +144,7 @@ async def test__init__with_bot_token_and_without_optionals(self): @pytest.mark.asyncio async def test__init__with_bearer_token_and_without_optionals(self): - client = rest.RestfulClient(token="Bearer token.otacon.a-token") + client = rest.LowLevelRestfulClient(token="Bearer token.otacon.a-token") assert client.token == "Bearer token.otacon.a-token" @pytest.mark.asyncio @@ -160,7 +168,7 @@ async def test__init__with_optionals(self): ) with stack: - client = rest.RestfulClient( + client = rest.LowLevelRestfulClient( token="Bot token.otacon.a-token", base_url="https://discordapp.com/api/v69420", allow_redirects=True, @@ -184,7 +192,7 @@ async def test__init__with_optionals(self): @pytest.mark.asyncio @_helpers.assert_raises(type_=RuntimeError) async def test__init__raises_runtime_error_with_invalid_token(self, *_): - async with rest.RestfulClient(token="An-invalid-TOKEN"): + async with rest.LowLevelRestfulClient(token="An-invalid-TOKEN"): pass @pytest.mark.asyncio @@ -196,16 +204,23 @@ async def test_close(self, rest_impl): @pytest.fixture() @mock.patch.object(ratelimits, "ManualRateLimiter") @mock.patch.object(ratelimits, "HTTPBucketRateLimiterManager") - async def mock_rest_impl(self, *args): - rest_impl = rest.RestfulClient(token="Bot token") + async def rest_impl_with__request(self, *args): + rest_impl = rest.LowLevelRestfulClient(token="Bot token") rest_impl.logger = mock.MagicMock(debug=mock.MagicMock()) - rest_impl.ratelimiter = mock.MagicMock() - rest_impl.global_ratelimiter = mock.MagicMock() + rest_impl.ratelimiter = mock.create_autospec( + ratelimits.HTTPBucketRateLimiterManager, + auto_spec=True, + acquire=mock.MagicMock(), + update_rate_limits=mock.MagicMock(), + ) + rest_impl.global_ratelimiter = mock.create_autospec( + ratelimits.ManualRateLimiter, auto_spec=True, acquire=mock.MagicMock(), throttle=mock.MagicMock() + ) return rest_impl @pytest.mark.asyncio - async def test__request_acquires_ratelimiter(self, compiled_route, exit_error, mock_rest_impl): - rest_impl = await mock_rest_impl + async def test__request_acquires_ratelimiter(self, compiled_route, exit_error, rest_impl_with__request): + rest_impl = await rest_impl_with__request rest_impl.logger.debug.side_effect = exit_error with mock.patch("asyncio.gather", return_value=_helpers.AwaitableMock()): @@ -217,8 +232,8 @@ async def test__request_acquires_ratelimiter(self, compiled_route, exit_error, m rest_impl.ratelimiter.acquire.asset_called_once_with(compiled_route) @pytest.mark.asyncio - async def test__request_sets_Authentication_if_token(self, compiled_route, exit_error, mock_rest_impl): - rest_impl = await mock_rest_impl + async def test__request_sets_Authentication_if_token(self, compiled_route, exit_error, rest_impl_with__request): + rest_impl = await rest_impl_with__request rest_impl.logger.debug.side_effect = [None, exit_error] stack = contextlib.ExitStack() @@ -249,9 +264,9 @@ async def test__request_sets_Authentication_if_token(self, compiled_route, exit_ @pytest.mark.asyncio async def test__request_doesnt_set_Authentication_if_suppress_authorization_header( - self, compiled_route, exit_error, mock_rest_impl + self, compiled_route, exit_error, rest_impl_with__request ): - rest_impl = await mock_rest_impl + rest_impl = await rest_impl_with__request rest_impl.logger.debug.side_effect = [None, exit_error] stack = contextlib.ExitStack() @@ -281,8 +296,10 @@ async def test__request_doesnt_set_Authentication_if_suppress_authorization_head ) @pytest.mark.asyncio - async def test__request_sets_X_Audit_Log_Reason_if_reason(self, compiled_route, exit_error, mock_rest_impl): - rest_impl = await mock_rest_impl + async def test__request_sets_X_Audit_Log_Reason_if_reason( + self, compiled_route, exit_error, rest_impl_with__request + ): + rest_impl = await rest_impl_with__request rest_impl.logger.debug.side_effect = [None, exit_error] stack = contextlib.ExitStack() @@ -316,8 +333,10 @@ async def test__request_sets_X_Audit_Log_Reason_if_reason(self, compiled_route, ) @pytest.mark.asyncio - async def test__request_updates_headers_with_provided_headers(self, compiled_route, exit_error, mock_rest_impl): - rest_impl = await mock_rest_impl + async def test__request_updates_headers_with_provided_headers( + self, compiled_route, exit_error, rest_impl_with__request + ): + rest_impl = await rest_impl_with__request rest_impl.logger.debug.side_effect = [None, exit_error] stack = contextlib.ExitStack() @@ -349,7 +368,9 @@ async def test__request_updates_headers_with_provided_headers(self, compiled_rou ) @pytest.mark.asyncio - async def test__request_resets_seek_on_seekable_resources(self, compiled_route, exit_error, mock_rest_impl): + async def test__request_resets_seek_on_seekable_resources( + self, compiled_route, exit_error, rest_impl_with__request + ): class SeekableResource: seeked: bool pos: int @@ -376,7 +397,7 @@ def read(self): def close(self): ... - rest_impl = await mock_rest_impl + rest_impl = await rest_impl_with__request rest_impl.logger.debug.side_effect = exit_error seekable_resources = [SeekableResource(5), SeekableResource(37), SeekableResource(16)] @@ -394,11 +415,11 @@ def close(self): @pytest.mark.parametrize( "content_type", ["text/plain", "text/html"], ) - async def test__request_handles_bad_response_when_content_type_is_plain_or_htlm( - self, content_type, exit_error, compiled_route, discord_response, mock_rest_impl + async def test__request_handles_bad_response_when_content_type_is_plain_or_html( + self, content_type, exit_error, compiled_route, discord_response, rest_impl_with__request ): discord_response.headers["Content-Type"] = content_type - rest_impl = await mock_rest_impl + rest_impl = await rest_impl_with__request rest_impl._handle_bad_response = mock.AsyncMock(side_effect=[None, exit_error]) rest_impl.client_session.request = mock.MagicMock(return_value=discord_response) @@ -411,9 +432,9 @@ async def test__request_handles_bad_response_when_content_type_is_plain_or_htlm( rest_impl._handle_bad_response.assert_called() @pytest.mark.asyncio - async def test__request_when_invalid_content_type(self, compiled_route, discord_response, mock_rest_impl): + async def test__request_when_invalid_content_type(self, compiled_route, discord_response, rest_impl_with__request): discord_response.headers["Content-Type"] = "something/invalid" - rest_impl = await mock_rest_impl + rest_impl = await rest_impl_with__request stack = contextlib.ExitStack() stack.enter_context(mock.patch("asyncio.gather", return_value=_helpers.AwaitableMock())) @@ -424,11 +445,11 @@ async def test__request_when_invalid_content_type(self, compiled_route, discord_ @pytest.mark.asyncio async def test__request_when_TOO_MANY_REQUESTS_when_global( - self, compiled_route, exit_error, discord_response, mock_rest_impl + self, compiled_route, exit_error, discord_response, rest_impl_with__request ): discord_response.status = 429 discord_response.raw_body = '{"retry_after": 1, "global": true}' - rest_impl = await mock_rest_impl + rest_impl = await rest_impl_with__request rest_impl.global_ratelimiter.throttle = mock.MagicMock(side_effect=[None, exit_error]) stack = contextlib.ExitStack() @@ -445,15 +466,15 @@ async def test__request_when_TOO_MANY_REQUESTS_when_global( @pytest.mark.asyncio async def test__request_when_TOO_MANY_REQUESTS_when_not_global( - self, compiled_route, exit_error, discord_response, mock_rest_impl + self, compiled_route, exit_error, discord_response, rest_impl_with__request ): discord_response.status = 429 discord_response.raw_body = '{"retry_after": 1, "global": false}' - rest_impl = await mock_rest_impl + rest_impl = await rest_impl_with__request rest_impl.logger.debug.side_effect = [None, exit_error] with mock.patch("asyncio.gather", return_value=_helpers.AwaitableMock()): - with mock.patch.object(rest.RestfulClient, "_request", return_value=discord_response): + with mock.patch.object(rest.LowLevelRestfulClient, "_request", return_value=discord_response): try: await rest_impl._request(compiled_route) except exit_error: @@ -479,7 +500,7 @@ async def test__request_raises_appropriate_error_for_status_code( self, *patches, status_code, error, compiled_route, discord_response, api_version ): discord_response.status = status_code - rest_impl = rest.RestfulClient(token="Bot token", version=api_version) + rest_impl = rest.LowLevelRestfulClient(token="Bot token", version=api_version) rest_impl.ratelimiter = mock.MagicMock() rest_impl.global_ratelimiter = mock.MagicMock() rest_impl.client_session.request = mock.MagicMock(return_value=discord_response) @@ -492,9 +513,9 @@ async def test__request_raises_appropriate_error_for_status_code( assert True @pytest.mark.asyncio - async def test__request_when_NO_CONTENT(self, compiled_route, discord_response, mock_rest_impl): + async def test__request_when_NO_CONTENT(self, compiled_route, discord_response, rest_impl_with__request): discord_response.status = 204 - rest_impl = await mock_rest_impl + rest_impl = await rest_impl_with__request rest_impl.client_session.request = mock.MagicMock(return_value=discord_response) with mock.patch("asyncio.gather", return_value=_helpers.AwaitableMock()): @@ -502,11 +523,11 @@ async def test__request_when_NO_CONTENT(self, compiled_route, discord_response, @pytest.mark.asyncio async def test__request_handles_bad_response_when_error_results_in_retry( - self, exit_error, compiled_route, discord_response, mock_rest_impl + self, exit_error, compiled_route, discord_response, rest_impl_with__request ): discord_response.raw_body = "{}" discord_response.status = 1000 - rest_impl = await mock_rest_impl + rest_impl = await rest_impl_with__request rest_impl._handle_bad_response = mock.AsyncMock(side_effect=[None, exit_error]) stack = contextlib.ExitStack() @@ -587,13 +608,13 @@ async def test_get_guild_audit_log_with_optionals(self, rest_impl): with mock.patch.object(routes, "GUILD_AUDIT_LOGS", compile=mock.MagicMock(return_value=mock_route)): assert ( await rest_impl.get_guild_audit_log( - "2929292929", user_id="115590097100865541", action_type=42, limit=5, + "2929292929", user_id="115590097100865541", action_type=42, limit=5, before="123123123" ) is mock_response ) routes.GUILD_AUDIT_LOGS.compile.assert_called_once_with(rest_impl.GET, guild_id="2929292929") rest_impl._request.assert_called_once_with( - mock_route, query={"user_id": "115590097100865541", "action_type": 42, "limit": 5} + mock_route, query={"user_id": "115590097100865541", "action_type": 42, "limit": 5, "before": "123123123"} ) @pytest.mark.asyncio @@ -860,7 +881,12 @@ async def test_edit_message_with_optionals(self, rest_impl): with mock.patch.object(routes, "CHANNEL_MESSAGE", compile=mock.MagicMock(return_value=mock_route)): assert ( await rest_impl.edit_message( - "9292929", "484848", content="42", embed={"content": "I AM AN EMBED"}, flags=2 + "9292929", + "484848", + content="42", + embed={"content": "I AM AN EMBED"}, + flags=2, + allowed_mentions={"parse": ["everyone", "users"]}, ) is mock_response ) @@ -868,7 +894,13 @@ async def test_edit_message_with_optionals(self, rest_impl): rest_impl.PATCH, channel_id="9292929", message_id="484848" ) rest_impl._request.assert_called_once_with( - mock_route, json_body={"content": "42", "embed": {"content": "I AM AN EMBED"}, "flags": 2} + mock_route, + json_body={ + "content": "42", + "embed": {"content": "I AM AN EMBED"}, + "flags": 2, + "allowed_mentions": {"parse": ["everyone", "users"]}, + }, ) @pytest.mark.asyncio @@ -893,11 +925,11 @@ async def test_bulk_delete_messages(self, rest_impl): async def test_edit_channel_permissions_without_optionals(self, rest_impl): mock_route = mock.MagicMock(routes.CHANNEL_PERMISSIONS) with mock.patch.object(routes, "CHANNEL_PERMISSIONS", compile=mock.MagicMock(return_value=mock_route)): - assert await rest_impl.edit_channel_permissions("101010101010", "100101010") is None + assert await rest_impl.edit_channel_permissions("101010101010", "100101010", type_="user") is None routes.CHANNEL_PERMISSIONS.compile.assert_called_once_with( rest_impl.PATCH, channel_id="101010101010", overwrite_id="100101010" ) - rest_impl._request.assert_called_once_with(mock_route, json_body={}, reason=...) + rest_impl._request.assert_called_once_with(mock_route, json_body={"type": "user"}, reason=...) @pytest.mark.asyncio async def test_edit_channel_permissions_with_optionals(self, rest_impl): @@ -1239,7 +1271,7 @@ async def test_get_guild_channels(self, rest_impl): rest_impl._request.return_value = mock_response mock_route = mock.MagicMock(routes.GUILD_CHANNELS) with mock.patch.object(routes, "GUILD_CHANNELS", compile=mock.MagicMock(return_value=mock_route)): - assert await rest_impl.get_guild_channels("393939393") is mock_response + assert await rest_impl.list_guild_channels("393939393") is mock_response routes.GUILD_CHANNELS.compile.assert_called_once_with(rest_impl.GET, guild_id="393939393") rest_impl._request.assert_called_once_with(mock_route) @@ -1655,30 +1687,6 @@ async def test_get_guild_integrations(self, rest_impl): routes.GUILD_INTEGRATIONS.compile.assert_called_once_with(rest_impl.GET, guild_id="537340989808050216") rest_impl._request.assert_called_once_with(mock_route) - @pytest.mark.asyncio - async def test_create_guild_integration_without_reason(self, rest_impl): - mock_response = {"id": "22222"} - rest_impl._request.return_value = mock_response - mock_route = mock.MagicMock(routes.GUILD_INTEGRATIONS) - with mock.patch.object(routes, "GUILD_INTEGRATIONS", compile=mock.MagicMock(return_value=mock_route)): - assert await rest_impl.create_guild_integration("2222", "twitch", "443223") is mock_response - routes.GUILD_INTEGRATIONS.compile.assert_called_once_with(rest_impl.POST, guild_id="2222") - rest_impl._request.assert_called_once_with(mock_route, json_body={"type": "twitch", "id": "443223"}, reason=...) - - @pytest.mark.asyncio - async def test_create_guild_integration_with_reason(self, rest_impl): - mock_response = {"id": "22222"} - rest_impl._request.return_value = mock_response - mock_route = mock.MagicMock(routes.GUILD_INTEGRATIONS) - with mock.patch.object(routes, "GUILD_INTEGRATIONS", compile=mock.MagicMock(return_value=mock_route)): - assert ( - await rest_impl.create_guild_integration("2222", "twitch", "443223", reason="NAH m8") is mock_response - ) - routes.GUILD_INTEGRATIONS.compile.assert_called_once_with(rest_impl.POST, guild_id="2222") - rest_impl._request.assert_called_once_with( - mock_route, json_body={"type": "twitch", "id": "443223"}, reason="NAH m8" - ) - @pytest.mark.asyncio async def test_modify_guild_integration_without_optionals(self, rest_impl): mock_route = mock.MagicMock(routes.GUILD_INTEGRATION) @@ -1758,10 +1766,11 @@ async def test_modify_guild_embed_without_reason(self, rest_impl): rest_impl._request.return_value = mock_response mock_route = mock.MagicMock(routes.GUILD_EMBED) with mock.patch.object(routes, "GUILD_EMBED", compile=mock.MagicMock(return_value=mock_route)): - embed_obj = {"channel_id": "222", "enabled": True} - assert await rest_impl.modify_guild_embed("393939", embed_obj) is mock_response + assert await rest_impl.modify_guild_embed("393939", channel_id="222", enabled=True) is mock_response routes.GUILD_EMBED.compile.assert_called_once_with(rest_impl.PATCH, guild_id="393939") - rest_impl._request.assert_called_once_with(mock_route, json_body=embed_obj, reason=...) + rest_impl._request.assert_called_once_with( + mock_route, json_body={"channel_id": "222", "enabled": True}, reason=... + ) @pytest.mark.asyncio async def test_modify_guild_embed_with_reason(self, rest_impl): @@ -1769,10 +1778,14 @@ async def test_modify_guild_embed_with_reason(self, rest_impl): rest_impl._request.return_value = mock_response mock_route = mock.MagicMock(routes.GUILD_EMBED) with mock.patch.object(routes, "GUILD_EMBED", compile=mock.MagicMock(return_value=mock_route)): - embed_obj = {"channel_id": "222", "enabled": True} - assert await rest_impl.modify_guild_embed("393939", embed_obj, reason="OK") is mock_response + assert ( + await rest_impl.modify_guild_embed("393939", channel_id="222", enabled=True, reason="OK") + is mock_response + ) routes.GUILD_EMBED.compile.assert_called_once_with(rest_impl.PATCH, guild_id="393939") - rest_impl._request.assert_called_once_with(mock_route, json_body=embed_obj, reason="OK") + rest_impl._request.assert_called_once_with( + mock_route, json_body={"channel_id": "222", "enabled": True}, reason="OK" + ) @pytest.mark.asyncio async def test_get_guild_vanity_url(self, rest_impl): diff --git a/tests/hikari/test_audit_logs.py b/tests/hikari/test_audit_logs.py index 2f8b93bd73..fe50735b99 100644 --- a/tests/hikari/test_audit_logs.py +++ b/tests/hikari/test_audit_logs.py @@ -419,3 +419,191 @@ def test_deserialize( assert audit_log_obj.webhooks == {123: mock_webhook_obj} assert audit_log_obj.users == {345: mock_user_obj} assert audit_log_obj.integrations == {234: mock_integration_obj} + + +class TestAuditLogIterator: + @pytest.mark.asyncio + async def test__fill_when_entities_returned(self): + mock_webhook_payload = {"id": "43242", "channel_id": "292393993"} + mock_webhook_obj = mock.MagicMock(webhooks.Webhook, id=292393993) + mock_user_payload = {"id": "929292", "public_flags": "22222"} + mock_user_obj = mock.MagicMock(users.User, id=929292) + mock_audit_log_entry_payload = {"target_id": "202020", "id": "222"} + mock_integration_payload = {"id": "123123123", "account": {}} + mock_integration_obj = mock.MagicMock(guilds.PartialGuildIntegration, id=123123123) + mock_request = mock.AsyncMock( + return_value={ + "webhooks": [mock_webhook_payload], + "users": [mock_user_payload], + "audit_log_entries": [mock_audit_log_entry_payload], + "integrations": [mock_integration_payload], + } + ) + audit_log_iterator = audit_logs.AuditLogIterator( + guild_id="123123", request=mock_request, before=None, user_id="11111", action_type=..., limit=None, + ) + stack = contextlib.ExitStack() + stack.enter_context(mock.patch.object(users.User, "deserialize", return_value=mock_user_obj)) + stack.enter_context(mock.patch.object(webhooks.Webhook, "deserialize", return_value=mock_webhook_obj)) + stack.enter_context( + mock.patch.object(guilds.PartialGuildIntegration, "deserialize", return_value=mock_integration_obj) + ) + + with stack: + assert await audit_log_iterator._fill() is None + users.User.deserialize.assert_called_once_with(mock_user_payload) + webhooks.Webhook.deserialize.assert_called_once_with(mock_webhook_payload) + guilds.PartialGuildIntegration.deserialize.assert_called_once_with(mock_integration_payload) + assert audit_log_iterator.webhooks == {292393993: mock_webhook_obj} + assert audit_log_iterator.users == {929292: mock_user_obj} + assert audit_log_iterator.integrations == {123123123: mock_integration_obj} + assert audit_log_iterator._buffer == [mock_audit_log_entry_payload] + mock_request.assert_called_once_with( + guild_id="123123", user_id="11111", action_type=..., before=..., limit=100, + ) + + @pytest.mark.asyncio + async def test__fill_when_resource_exhausted(self): + mock_request = mock.AsyncMock( + return_value={"webhooks": [], "users": [], "audit_log_entries": [], "integrations": []} + ) + audit_log_iterator = audit_logs.AuditLogIterator( + guild_id="123123", request=mock_request, before="222222222", user_id="11111", action_type=..., limit=None, + ) + stack = contextlib.ExitStack() + stack.enter_context(mock.patch.object(users.User, "deserialize", return_value=...)) + stack.enter_context(mock.patch.object(webhooks.Webhook, "deserialize", return_value=...)) + stack.enter_context(mock.patch.object(guilds.PartialGuildIntegration, "deserialize", return_value=...)) + + with stack: + assert await audit_log_iterator._fill() is None + users.User.deserialize.assert_not_called() + webhooks.Webhook.deserialize.assert_not_called() + guilds.PartialGuildIntegration.deserialize.assert_not_called() + assert audit_log_iterator.webhooks == {} + assert audit_log_iterator.users == {} + assert audit_log_iterator.integrations == {} + assert audit_log_iterator._buffer == [] + mock_request.assert_called_once_with( + guild_id="123123", user_id="11111", action_type=..., before="222222222", limit=100, + ) + + @pytest.mark.asyncio + async def test__fill_when_before_and_limit_not_set(self): + mock_request = mock.AsyncMock( + return_value={ + "webhooks": [], + "users": [], + "audit_log_entries": [{"id": "123123123"}, {"id": "123123123"}], + "integrations": [], + } + ) + audit_log_iterator = audit_logs.AuditLogIterator( + guild_id="123123", request=mock_request, before=None, user_id="11111", action_type=..., limit=None, + ) + stack = contextlib.ExitStack() + stack.enter_context(mock.patch.object(users.User, "deserialize", return_value=...)) + stack.enter_context(mock.patch.object(webhooks.Webhook, "deserialize", return_value=...)) + stack.enter_context(mock.patch.object(guilds.PartialGuildIntegration, "deserialize", return_value=...)) + + with stack: + assert await audit_log_iterator._fill() is None + mock_request.assert_called_once_with( + guild_id="123123", user_id="11111", action_type=..., before=..., limit=100, + ) + assert audit_log_iterator._limit is None + + @pytest.mark.asyncio + async def test__fill_when_before_and_limit_set(self): + mock_request = mock.AsyncMock( + return_value={ + "webhooks": [], + "users": [], + "audit_log_entries": [{"id": "123123123"}, {"id": "123123123"}], + "integrations": [], + } + ) + audit_log_iterator = audit_logs.AuditLogIterator( + guild_id="123123", + request=mock_request, + before="222222222", + user_id="11111", + action_type=audit_logs.AuditLogEventType.MEMBER_MOVE, + limit=44, + ) + stack = contextlib.ExitStack() + stack.enter_context(mock.patch.object(users.User, "deserialize", return_value=...)) + stack.enter_context(mock.patch.object(webhooks.Webhook, "deserialize", return_value=...)) + stack.enter_context(mock.patch.object(guilds.PartialGuildIntegration, "deserialize", return_value=...)) + + with stack: + assert await audit_log_iterator._fill() is None + mock_request.assert_called_once_with( + guild_id="123123", user_id="11111", action_type=26, before="222222222", limit=44, + ) + assert audit_log_iterator._limit == 42 + + @pytest.mark.asyncio + async def test___anext___when_not_filled_and_resource_is_exhausted(self): + mock_request = mock.AsyncMock( + return_value={"webhooks": [], "users": [], "audit_log_entries": [], "integrations": []} + ) + iterator = audit_logs.AuditLogIterator( + guild_id="123123", request=mock_request, before=None, user_id=..., action_type=..., limit=None + ) + with mock.patch.object(audit_logs.AuditLogEntry, "deserialize", return_value=...): + async for _ in iterator: + assert False, "Iterator shouldn't have yielded anything." + audit_logs.AuditLogEntry.deserialize.assert_not_called() + assert iterator._front is None + + @pytest.mark.asyncio + async def test___anext___when_not_filled(self): + mock_request = mock.AsyncMock( + side_effect=[{"webhooks": [], "users": [], "audit_log_entries": [{"id": "666666"}], "integrations": []},] + ) + mock_audit_log_entry = mock.MagicMock(audit_logs.AuditLogEntry, id=666666) + iterator = audit_logs.AuditLogIterator( + guild_id="123123", request=mock_request, before=None, user_id=..., action_type=..., limit=None + ) + with mock.patch.object(audit_logs.AuditLogEntry, "deserialize", side_effect=[mock_audit_log_entry]): + async for result in iterator: + assert result is mock_audit_log_entry + break + audit_logs.AuditLogEntry.deserialize.assert_called_once_with({"id": "666666"}) + mock_request.assert_called_once_with( + guild_id="123123", user_id=..., action_type=..., before=..., limit=100, + ) + assert iterator._front == "666666" + + @pytest.mark.asyncio + async def test___anext___when_not_filled_and_limit_exhausted(self): + mock_request = mock.AsyncMock(side_effect=[]) + mock_audit_log_entry = mock.MagicMock(audit_logs.AuditLogEntry, id=666666) + iterator = audit_logs.AuditLogIterator( + guild_id="123123", request=mock_request, before=None, user_id=..., action_type=..., limit=None + ) + with mock.patch.object(audit_logs.AuditLogEntry, "deserialize", side_effect=[mock_audit_log_entry]): + async for _ in iterator: + assert False, "Iterator shouldn't have yielded anything." + audit_logs.AuditLogEntry.deserialize.assert_not_called() + mock_request.assert_called_once_with( + guild_id="123123", user_id=..., action_type=..., before=..., limit=100, + ) + assert iterator._front is None + + @pytest.mark.asyncio + async def test___anext___when_filled(self): + mock_request = mock.AsyncMock(side_effect=[]) + mock_audit_log_entry = mock.MagicMock(audit_logs.AuditLogEntry, id=4242) + iterator = audit_logs.AuditLogIterator( + guild_id="123123", request=mock_request, before=None, user_id=..., action_type=..., limit=None + ) + iterator._buffer = [{"id": "123123"}] + with mock.patch.object(audit_logs.AuditLogEntry, "deserialize", side_effect=[mock_audit_log_entry]): + async for result in iterator: + assert result is mock_audit_log_entry + break + audit_logs.AuditLogEntry.deserialize.assert_called_once_with({"id": "123123"}) + mock_request.assert_not_called() + assert iterator._front == "4242" diff --git a/tests/hikari/test_emojis.py b/tests/hikari/test_emojis.py index 8cacf1fc5e..16b017eaee 100644 --- a/tests/hikari/test_emojis.py +++ b/tests/hikari/test_emojis.py @@ -30,6 +30,12 @@ def test_deserialize(self): assert emoji_obj.name == "🤷" + def test_url_name(self): + assert emojis.UnicodeEmoji(name="🤷").url_name == "🤷" + + def test_mention(self): + assert emojis.UnicodeEmoji(name="🤷").mention == "🤷" + class TestUnknownEmoji: def test_deserialize(self): @@ -39,6 +45,10 @@ def test_deserialize(self): assert emoji_obj.name == "test" assert emoji_obj.is_animated is True + def test_url_name(self): + name = emojis.UnknownEmoji(is_animated=True, id=650573534627758100, name="nyaa").url_name + assert name == "nyaa:650573534627758100" + class TestGuildEmoji: def test_deserialize(self): @@ -69,6 +79,26 @@ def test_deserialize(self): assert emoji_obj.is_colons_required is True assert emoji_obj.is_managed is False + @pytest.fixture() + def mock_guild_emoji_obj(self): + return emojis.GuildEmoji( + is_animated=False, + id=650573534627758100, + name="nyaa", + role_ids=[], + is_colons_required=True, + is_managed=False, + user=mock.MagicMock(users.User), + ) + + def test_mention_when_animated(self, mock_guild_emoji_obj): + mock_guild_emoji_obj.is_animated = True + assert mock_guild_emoji_obj.mention == "" + + def test_mention_when_not_animated(self, mock_guild_emoji_obj): + mock_guild_emoji_obj.is_animated = False + assert mock_guild_emoji_obj.mention == "<:nyaa:650573534627758100>" + @pytest.mark.parametrize( ["payload", "expected_type"], diff --git a/tests/hikari/test_guilds.py b/tests/hikari/test_guilds.py index fd9f640965..dce9673879 100644 --- a/tests/hikari/test_guilds.py +++ b/tests/hikari/test_guilds.py @@ -255,6 +255,17 @@ def test_guild_payload( } +class TestGuildEmbed: + @pytest.fixture() + def test_guild_embed_payload(self): + return {"channel_id": "123123123", "enabled": True} + + def test_deserialize(self, test_guild_embed_payload): + guild_embed_obj = guilds.GuildEmbed.deserialize(test_guild_embed_payload) + assert guild_embed_obj.channel_id == 123123123 + assert guild_embed_obj.is_enabled is True + + class TestGuildMember: def test_deserialize(self, test_member_payload, test_user_payload): mock_user = mock.create_autospec(users.User) diff --git a/tests/hikari/test_snowflake.py b/tests/hikari/test_snowflake.py index b6067ad33d..7ae4bcc62a 100644 --- a/tests/hikari/test_snowflake.py +++ b/tests/hikari/test_snowflake.py @@ -18,9 +18,12 @@ # along with Hikari. If not, see . import datetime +import attr import pytest +from hikari import entities from hikari import snowflakes +from hikari.internal import marshaller class TestSnowflake: @@ -33,8 +36,8 @@ def neko_snowflake(self, raw_id): return snowflakes.Snowflake.deserialize(raw_id) def test_created_at(self, neko_snowflake): - assert neko_snowflake.created_at == datetime.datetime(2019, 1, 22, 18, 41, 15, 283_000).replace( - tzinfo=datetime.timezone.utc + assert neko_snowflake.created_at == datetime.datetime( + 2019, 1, 22, 18, 41, 15, 283_000, tzinfo=datetime.timezone.utc ) def test_increment(self, neko_snowflake): @@ -65,3 +68,40 @@ def test_eq(self, neko_snowflake, raw_id): def test_lt(self, neko_snowflake, raw_id): assert neko_snowflake < raw_id + 1 + + def test_deserialize(self, neko_snowflake, raw_id): + assert neko_snowflake == snowflakes.Snowflake.deserialize(raw_id) + + def test_from_datetime(self): + result = snowflakes.Snowflake.from_datetime( + datetime.datetime(2019, 1, 22, 18, 41, 15, 283_000, tzinfo=datetime.timezone.utc) + ) + assert result == 537340988620800000 + assert isinstance(result, snowflakes.Snowflake) + + def test_from_timestamp(self): + result = snowflakes.Snowflake.from_timestamp(1548182475.283) + assert result == 537340988620800000 + assert isinstance(result, snowflakes.Snowflake) + + +class TestUniqueEntity: + def test_int(self): + assert int(snowflakes.UniqueEntity(id=snowflakes.Snowflake.deserialize("2333333"))) == 2333333 + + @pytest.fixture() + def stud_marshal_entity(self): + @marshaller.marshallable() + @attr.s(slots=True) + class StudEntity(snowflakes.UniqueEntity, entities.Deserializable, entities.Serializable): + ... + + return StudEntity + + def test_deserialize(self, stud_marshal_entity): + unique_entity = stud_marshal_entity.deserialize({"id": "5445"}) + assert unique_entity.id == snowflakes.Snowflake("5445") + assert isinstance(unique_entity.id, snowflakes.Snowflake) + + def test_serialize(self, stud_marshal_entity): + assert stud_marshal_entity(id=snowflakes.Snowflake(5445)).serialize() == {"id": "5445"} From e2505f19306b76e7c07c8c80fbb74ef34e928439 Mon Sep 17 00:00:00 2001 From: davfsa Date: Sun, 12 Apr 2020 18:00:52 +0200 Subject: [PATCH 099/922] Update image size --- hikari/guilds.py | 44 ++++++++++++++----- hikari/internal/assertions.py | 8 ++++ hikari/internal/urls.py | 11 +++++ hikari/invites.py | 22 +++++++--- hikari/oauth2.py | 33 ++++++++++---- hikari/users.py | 11 +++-- tests/hikari/internal/test_assertions.py | 26 ++++++----- .../internal/{test_cdn.py => test_urls.py} | 15 ++++++- tests/hikari/test_guilds.py | 4 +- tests/hikari/test_oauth2.py | 8 ++-- 10 files changed, 134 insertions(+), 48 deletions(-) rename tests/hikari/internal/{test_cdn.py => test_urls.py} (74%) diff --git a/hikari/guilds.py b/hikari/guilds.py index 76c2339dec..5afb3f492f 100644 --- a/hikari/guilds.py +++ b/hikari/guilds.py @@ -818,7 +818,7 @@ class PartialGuild(snowflakes.UniqueEntity, entities.Deserializable): deserializer=lambda features: {conversions.try_cast(f, GuildFeature, f) for f in features}, ) - def format_icon_url(self, fmt: typing.Optional[str] = None, size: int = 2048) -> typing.Optional[str]: + def format_icon_url(self, fmt: typing.Optional[str] = None, size: int = 4096) -> typing.Optional[str]: """Generate the URL for this guild's custom icon, if set. Parameters @@ -828,13 +828,18 @@ def format_icon_url(self, fmt: typing.Optional[str] = None, size: int = 2048) -> Supports ``png``, ``jpeg``, `jpg`, ``webp`` and ``gif`` (when animated). size : :obj:`int` - The size to set for the URL, defaults to ``2048``. - Can be any power of two between 16 and 2048. + The size to set for the URL, defaults to ``4096``. + Can be any power of two between 16 and 4096. Returns ------- :obj:`str`, optional The string URL. + + Raises + ------ + :obj:`ValueError` + If ``size`` is not a power of two or not between 16 and 4096. """ if self.icon_hash: if fmt is None and self.icon_hash.startswith("a_"): @@ -1201,7 +1206,7 @@ class Guild(PartialGuild): if_none=None, deserializer=snowflakes.Snowflake.deserialize ) - def format_splash_url(self, fmt: str = "png", size: int = 2048) -> typing.Optional[str]: + def format_splash_url(self, fmt: str = "png", size: int = 4096) -> typing.Optional[str]: """Generate the URL for this guild's splash image, if set. Parameters @@ -1210,13 +1215,18 @@ def format_splash_url(self, fmt: str = "png", size: int = 2048) -> typing.Option The format to use for this URL, defaults to ``png``. Supports ``png``, ``jpeg``, ``jpg`` and ``webp``. size : :obj:`int` - The size to set for the URL, defaults to ``2048``. - Can be any power of two between 16 and 2048. + The size to set for the URL, defaults to ``4096``. + Can be any power of two between 16 and 4096. Returns ------- :obj:`str`, optional The string URL. + + Raises + ------ + :obj:`ValueError` + If ``size`` is not a power of two or not between 16 and 4096. """ if self.splash_hash: return urls.generate_cdn_url("splashes", str(self.id), self.splash_hash, fmt=fmt, size=size) @@ -1227,7 +1237,7 @@ def splash_url(self) -> typing.Optional[str]: """URL for this guild's splash, if set.""" return self.format_splash_url() - def format_discovery_splash_url(self, fmt: str = "png", size: int = 2048) -> typing.Optional[str]: + def format_discovery_splash_url(self, fmt: str = "png", size: int = 4096) -> typing.Optional[str]: """Generate the URL for this guild's discovery splash image, if set. Parameters @@ -1236,13 +1246,18 @@ def format_discovery_splash_url(self, fmt: str = "png", size: int = 2048) -> typ The format to use for this URL, defaults to ``png``. Supports ``png``, ``jpeg``, ``jpg`` and ``webp``. size : :obj:`int` - The size to set for the URL, defaults to ``2048``. - Can be any power of two between 16 and 2048. + The size to set for the URL, defaults to ``4096``. + Can be any power of two between 16 and 4096. Returns ------- :obj:`str`, optional The string URL. + + Raises + ------ + :obj:`ValueError` + If ``size`` is not a power of two or not between 16 and 4096. """ if self.discovery_splash_hash: return urls.generate_cdn_url( @@ -1255,7 +1270,7 @@ def discovery_splash_url(self) -> typing.Optional[str]: """URL for this guild's discovery splash, if set.""" return self.format_discovery_splash_url() - def format_banner_url(self, fmt: str = "png", size: int = 2048) -> typing.Optional[str]: + def format_banner_url(self, fmt: str = "png", size: int = 4096) -> typing.Optional[str]: """Generate the URL for this guild's banner image, if set. Parameters @@ -1264,13 +1279,18 @@ def format_banner_url(self, fmt: str = "png", size: int = 2048) -> typing.Option The format to use for this URL, defaults to ``png``. Supports ``png``, ``jpeg``, ``jpg`` and ``webp``. size : :obj:`int` - The size to set for the URL, defaults to ``2048``. - Can be any power of two between 16 and 2048. + The size to set for the URL, defaults to ``4096``. + Can be any power of two between 16 and 4096. Returns ------- :obj:`str`, optional The string URL. + + Raises + ------ + :obj:`ValueError` + If ``size`` is not a power of two or not between 16 and 4096. """ if self.banner_hash: return urls.generate_cdn_url("banners", str(self.id), self.banner_hash, fmt=fmt, size=size) diff --git a/hikari/internal/assertions.py b/hikari/internal/assertions.py index 73d3826c70..074a1241bf 100644 --- a/hikari/internal/assertions.py +++ b/hikari/internal/assertions.py @@ -28,8 +28,10 @@ "assert_that", "assert_not_none", "assert_in_range", + "assert_is_int_power", ] +import math import typing ValueT = typing.TypeVar("ValueT") @@ -54,3 +56,9 @@ def assert_in_range(value, min_inclusive, max_inclusive, name: str = None): if not (min_inclusive <= value <= max_inclusive): name = name or "The value" raise ValueError(f"{name} must be in the inclusive range of {min_inclusive} and {max_inclusive}") + + +def assert_is_int_power(value: int, power: int) -> bool: + """If a value is not a power the given int, raise :obj:`ValueError`.""" + logarithm = math.log(value, power) + assert_that(logarithm.is_integer(), f"value must be an integer power of {power}") diff --git a/hikari/internal/urls.py b/hikari/internal/urls.py index 2e65db3a86..2ded5ea0fd 100644 --- a/hikari/internal/urls.py +++ b/hikari/internal/urls.py @@ -28,6 +28,7 @@ import typing import urllib.parse +from hikari.internal import assertions #: The URL for the CDN. #: @@ -55,12 +56,22 @@ def generate_cdn_url(*route_parts: str, fmt: str, size: typing.Optional[int]) -> size : :obj:`int`, optional The size to specify for the image in the query string if applicable, should be passed through as ``None`` to avoid the param being set. + Must be any power of two between 16 and 4096. Returns ------- :obj:`str` The URL to the resource on the Discord CDN. + + Raises + ------ + :obj:`ValueError` + If ``size`` is not a power of two or not between 16 and 4096. """ + if size: + assertions.assert_in_range(size, 16, 4096) + assertions.assert_is_int_power(size, 2) + path = "/".join(urllib.parse.unquote(part) for part in route_parts) url = urllib.parse.urljoin(BASE_CDN_URL, "/" + path) + "." + str(fmt) query = urllib.parse.urlencode({"size": size}) if size is not None else None diff --git a/hikari/invites.py b/hikari/invites.py index c1bfe5ea37..1a89e6d188 100644 --- a/hikari/invites.py +++ b/hikari/invites.py @@ -98,7 +98,7 @@ class InviteGuild(guilds.PartialGuild): #: :type: :obj:`str`, optional vanity_url_code: typing.Optional[str] = marshaller.attrib(if_none=None, deserializer=str) - def format_splash_url(self, fmt: str = "png", size: int = 2048) -> typing.Optional[str]: + def format_splash_url(self, fmt: str = "png", size: int = 4096) -> typing.Optional[str]: """Generate the URL for this guild's splash, if set. Parameters @@ -107,13 +107,18 @@ def format_splash_url(self, fmt: str = "png", size: int = 2048) -> typing.Option The format to use for this URL, defaults to ``png``. Supports ``png``, ``jpeg``, ``jpg` and ``webp``. size : :obj:`int` - The size to set for the URL, defaults to ``2048``. - Can be any power of two between 16 and 2048. + The size to set for the URL, defaults to ``4096``. + Can be any power of two between 16 and 4096. Returns ------- :obj:`str`, optional The string URL. + + Raises + ------ + :obj:`ValueError` + If ``size`` is not a power of two or not between 16 and 4096. """ if self.splash_hash: return urls.generate_cdn_url("splashes", str(self.id), self.splash_hash, fmt=fmt, size=size) @@ -124,7 +129,7 @@ def splash_url(self) -> typing.Optional[str]: """URL for this guild's splash, if set.""" return self.format_splash_url() - def format_banner_url(self, fmt: str = "png", size: int = 2048) -> typing.Optional[str]: + def format_banner_url(self, fmt: str = "png", size: int = 4096) -> typing.Optional[str]: """Generate the URL for this guild's banner, if set. Parameters @@ -133,13 +138,18 @@ def format_banner_url(self, fmt: str = "png", size: int = 2048) -> typing.Option The format to use for this URL, defaults to ``png``. Supports ``png``, ``jpeg``, ``jpg`` and ``webp``. size : :obj:`int` - The size to set for the URL, defaults to ``2048``. - Can be any power of two between 16 and 2048. + The size to set for the URL, defaults to ``4096``. + Can be any power of two between 16 and 4096. Returns ------- :obj:`str`, optional The string URL. + + Raises + ------ + :obj:`ValueError` + If ``size`` is not a power of two or not between 16 and 4096. """ if self.banner_hash: return urls.generate_cdn_url("banners", str(self.id), self.banner_hash, fmt=fmt, size=size) diff --git a/hikari/oauth2.py b/hikari/oauth2.py index 06bedfc134..8f5f5737f1 100644 --- a/hikari/oauth2.py +++ b/hikari/oauth2.py @@ -191,7 +191,7 @@ def icon_url(self) -> typing.Optional[str]: """URL of this team's icon, if set.""" return self.format_icon_url() - def format_icon_url(self, fmt: str = "png", size: int = 2048) -> typing.Optional[str]: + def format_icon_url(self, fmt: str = "png", size: int = 4096) -> typing.Optional[str]: """Generate the icon URL for this team if set. Parameters @@ -200,13 +200,18 @@ def format_icon_url(self, fmt: str = "png", size: int = 2048) -> typing.Optional The format to use for this URL, defaults to ``png``. Supports ``png``, ``jpeg``, ``jpg`` and ``webp``. size : :obj:`int` - The size to set for the URL, defaults to ``2048``. Can be any power - of two between 16 and 2048 inclusive. + The size to set for the URL, defaults to ``4096``. Can be any power + of two between 16 and 4096 inclusive. Returns ------- :obj:`str`, optional The string URL. + + Raises + ------ + :obj:`ValueError` + If ``size`` is not a power of two or not between 16 and 4096. """ if self.icon_hash: return urls.generate_cdn_url("team-icons", str(self.id), self.icon_hash, fmt=fmt, size=size) @@ -330,7 +335,7 @@ def icon_url(self) -> typing.Optional[str]: """URL for this team's icon, if set.""" return self.format_icon_url() - def format_icon_url(self, fmt: str = "png", size: int = 2048) -> typing.Optional[str]: + def format_icon_url(self, fmt: str = "png", size: int = 4096) -> typing.Optional[str]: """Generate the icon URL for this application if set. Parameters @@ -339,13 +344,18 @@ def format_icon_url(self, fmt: str = "png", size: int = 2048) -> typing.Optional The format to use for this URL, defaults to ``png``. Supports ``png``, ``jpeg``, ``jpg`` and ```webp``. size : :obj:`int` - The size to set for the URL, defaults to ``2048``. - Can be any power of two between 16 and 2048. + The size to set for the URL, defaults to ``4096``. + Can be any power of two between 16 and 4096. Returns ------- :obj:`str`, optional The string URL. + + Raises + ------ + :obj:`ValueError` + If ``size`` is not a power of two or not between 16 and 4096. """ if self.icon_hash: return urls.generate_cdn_url("app-icons", str(self.id), self.icon_hash, fmt=fmt, size=size) @@ -356,7 +366,7 @@ def cover_image_url(self) -> typing.Optional[str]: """URL for this icon's store cover image, if set.""" return self.format_cover_image_url() - def format_cover_image_url(self, fmt: str = "png", size: int = 2048) -> typing.Optional[str]: + def format_cover_image_url(self, fmt: str = "png", size: int = 4096) -> typing.Optional[str]: """Generate the URL for this application's store page's cover image is set and applicable. Parameters @@ -365,13 +375,18 @@ def format_cover_image_url(self, fmt: str = "png", size: int = 2048) -> typing.O The format to use for this URL, defaults to ``png``. Supports ``png``, ``jpeg``, ``jpg`` and ``webp``. size : :obj:`int` - The size to set for the URL, defaults to ``2048``. - Can be any power of two between 16 and 2048. + The size to set for the URL, defaults to ``4096``. + Can be any power of two between 16 and 4096. Returns ------- :obj:`str`, optional The string URL. + + Raises + ------ + :obj:`ValueError` + If ``size`` is not a power of two or not between 16 and 4096. """ if self.cover_image_hash: return urls.generate_cdn_url("app-assets", str(self.id), self.cover_image_hash, fmt=fmt, size=size) diff --git a/hikari/users.py b/hikari/users.py index 0895cbd131..3f274bcdec 100644 --- a/hikari/users.py +++ b/hikari/users.py @@ -65,7 +65,7 @@ def avatar_url(self) -> str: """URL for this user's custom avatar if set, else default.""" return self.format_avatar_url() - def format_avatar_url(self, fmt: typing.Optional[str] = None, size: int = 2048) -> str: + def format_avatar_url(self, fmt: typing.Optional[str] = None, size: int = 4096) -> str: """Generate the avatar URL for this user's custom avatar if set, else their default avatar. Parameters @@ -76,14 +76,19 @@ def format_avatar_url(self, fmt: typing.Optional[str] = None, size: int = 2048) animated). Will be ignored for default avatars which can only be ``png``. size : :obj:`int` - The size to set for the URL, defaults to ``2048``. - Can be any power of two between 16 and 2048. + The size to set for the URL, defaults to ``4096``. + Can be any power of two between 16 and 4096. Will be ignored for default avatars. Returns ------- :obj:`str` The string URL. + + Raises + ------ + :obj:`ValueError` + If ``size`` is not a power of two or not between 16 and 4096. """ if not self.avatar_hash: return urls.generate_cdn_url("embed/avatars", str(self.default_avatar), fmt="png", size=None) diff --git a/tests/hikari/internal/test_assertions.py b/tests/hikari/internal/test_assertions.py index 2ff3d1b7bd..0e6b87f1fb 100644 --- a/tests/hikari/internal/test_assertions.py +++ b/tests/hikari/internal/test_assertions.py @@ -43,6 +43,7 @@ def test_assert_not_none_when_not_none(arg): assertions.assert_not_none(arg) +@_helpers.assert_does_not_raise(type_=ValueError) @pytest.mark.parametrize( ["min_r", "max_r", "test"], [ @@ -59,17 +60,22 @@ def test_assert_not_none_when_not_none(arg): ], ) def test_in_range_when_in_range(min_r, max_r, test): - try: - assertions.assert_in_range(test, min_r, max_r, "blah") - except ValueError: - assert False, "should not have failed." + assertions.assert_in_range(test, min_r, max_r, "blah") +@_helpers.assert_raises(type_=ValueError) @pytest.mark.parametrize(["min_r", "max_r", "test"], [(0, 0, -1), (0, 10, 11), (10, 0, 5),]) def test_in_range_when_not_in_range(min_r, max_r, test): - try: - assertions.assert_in_range(test, min_r, max_r, "blah") - except ValueError: - pass - else: - assert False, "should have failed." + assertions.assert_in_range(test, min_r, max_r, "blah") + + +@_helpers.assert_does_not_raise(type_=ValueError) +@pytest.mark.parametrize(["value", "power"], [(16, 2), (9, 3), (16, 4)]) +def test_assert_is_int_power_when_is_power(value, power): + assertions.assert_is_int_power(value, power) + + +@_helpers.assert_raises(type_=ValueError) +@pytest.mark.parametrize(["value", "power"], [(11, 2), (10, 3), (101, 4)]) +def test_assert_is_int_power_when_is_not_power(value, power): + assertions.assert_is_int_power(value, power) diff --git a/tests/hikari/internal/test_cdn.py b/tests/hikari/internal/test_urls.py similarity index 74% rename from tests/hikari/internal/test_cdn.py rename to tests/hikari/internal/test_urls.py index 5ce6bff8aa..ca62552e83 100644 --- a/tests/hikari/internal/test_cdn.py +++ b/tests/hikari/internal/test_urls.py @@ -17,13 +17,24 @@ # You should have received a copy of the GNU Lesser General Public License # along ith Hikari. If not, see . from hikari.internal import urls +from tests.hikari import _helpers def test_generate_cdn_url(): - url = urls.generate_cdn_url("not", "a", "path", fmt="neko", size=42) - assert url == "https://cdn.discordapp.com/not/a/path.neko?size=42" + url = urls.generate_cdn_url("not", "a", "path", fmt="neko", size=16) + assert url == "https://cdn.discordapp.com/not/a/path.neko?size=16" def test_generate_cdn_url_with_size_set_to_none(): url = urls.generate_cdn_url("not", "a", "path", fmt="neko", size=None) assert url == "https://cdn.discordapp.com/not/a/path.neko" + + +@_helpers.assert_raises(type_=ValueError) +def test_generate_cdn_url_with_invalid_size_out_of_limits(): + urls.generate_cdn_url("not", "a", "path", fmt="neko", size=11) + + +@_helpers.assert_raises(type_=ValueError) +def test_generate_cdn_url_with_invalid_size_now_power_of_two(): + urls.generate_cdn_url("not", "a", "path", fmt="neko", size=111) diff --git a/tests/hikari/test_guilds.py b/tests/hikari/test_guilds.py index dce9673879..b1278e32a6 100644 --- a/tests/hikari/test_guilds.py +++ b/tests/hikari/test_guilds.py @@ -640,7 +640,7 @@ def test_format_icon_url_animated_default(self, partial_guild_obj): with mock.patch.object(urls, "generate_cdn_url", return_value=mock_url): url = partial_guild_obj.format_icon_url() urls.generate_cdn_url.assert_called_once_with( - "icons", "152559372126519269", "a_d4a983885dsaa7691ce8bcaaf945a", fmt="gif", size=2048 + "icons", "152559372126519269", "a_d4a983885dsaa7691ce8bcaaf945a", fmt="gif", size=4096 ) assert url == mock_url @@ -650,7 +650,7 @@ def test_format_icon_url_none_animated_default(self, partial_guild_obj): with mock.patch.object(urls, "generate_cdn_url", return_value=mock_url): url = partial_guild_obj.format_icon_url() urls.generate_cdn_url.assert_called_once_with( - "icons", "152559372126519269", "d4a983885dsaa7691ce8bcaaf945a", fmt="png", size=2048 + "icons", "152559372126519269", "d4a983885dsaa7691ce8bcaaf945a", fmt="png", size=4096 ) assert url == mock_url diff --git a/tests/hikari/test_oauth2.py b/tests/hikari/test_oauth2.py index a763ec7496..169a975995 100644 --- a/tests/hikari/test_oauth2.py +++ b/tests/hikari/test_oauth2.py @@ -192,7 +192,7 @@ def test_format_icon_url_returns_none(self): assert url is None def test_icon_url(self, team_obj): - mock_url = "https://cdn.discordapp.com/team-icons/202020202/hashtag.png?size=2048" + mock_url = "https://cdn.discordapp.com/team-icons/202020202/hashtag.png?size=4096" with mock.patch.object(urls, "generate_cdn_url", return_value=mock_url): url = team_obj.icon_url urls.generate_cdn_url.assert_called_once() @@ -237,11 +237,11 @@ def mock_application(self): return _helpers.create_autospec(oauth2.Application, id=22222) def test_icon_url(self, application_obj): - mock_url = "https://cdn.discordapp.com/app-icons/209333111222/iwiwiwiwiw.png?size=2048" + mock_url = "https://cdn.discordapp.com/app-icons/209333111222/iwiwiwiwiw.png?size=4096" with mock.patch.object(urls, "generate_cdn_url", return_value=mock_url): url = application_obj.icon_url urls.generate_cdn_url.assert_called_once() - assert url == "https://cdn.discordapp.com/app-icons/209333111222/iwiwiwiwiw.png?size=2048" + assert url == "https://cdn.discordapp.com/app-icons/209333111222/iwiwiwiwiw.png?size=4096" def test_format_icon_url(self, mock_application): mock_application.icon_hash = "wosososoos" @@ -259,7 +259,7 @@ def test_format_icon_url_returns_none(self, mock_application): assert url is None def test_cover_image_url(self, application_obj): - mock_url = "https://cdn.discordapp.com/app-assets/209333111222/hashmebaby.png?size=2048" + mock_url = "https://cdn.discordapp.com/app-assets/209333111222/hashmebaby.png?size=4096" with mock.patch.object(urls, "generate_cdn_url", return_value=mock_url): url = application_obj.cover_image_url urls.generate_cdn_url.assert_called_once() From 9e907e009bbc031ce96b2d76cd5f4429aeab5329 Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Sun, 12 Apr 2020 17:05:09 +0100 Subject: [PATCH 100/922] Removed cymock support and exposed a load of bugs. --- dev-requirements.txt | 4 ---- tests/hikari/_helpers.py | 2 +- tests/hikari/clients/test_configs.py | 7 +++---- tests/hikari/clients/test_gateway_managers.py | 2 +- tests/hikari/clients/test_rest_client.py | 13 +++++-------- tests/hikari/clients/test_shard_clients.py | 13 ++++++------- tests/hikari/internal/test_conversions.py | 4 ++-- tests/hikari/internal/test_marshaller.py | 3 ++- tests/hikari/internal/test_marshaller_pep563.py | 3 ++- tests/hikari/net/test_ratelimits.py | 2 +- tests/hikari/net/test_rest.py | 4 ++-- tests/hikari/net/test_shard.py | 4 ++-- tests/hikari/state/test_event_dispatcher.py | 2 +- tests/hikari/test_audit_logs.py | 4 ++-- tests/hikari/test_channels.py | 2 +- tests/hikari/test_colours.py | 3 ++- tests/hikari/test_embeds.py | 5 +++-- tests/hikari/test_emojis.py | 3 ++- tests/hikari/test_events.py | 2 +- tests/hikari/test_gateway_entities.py | 2 +- tests/hikari/test_guilds.py | 7 +++---- tests/hikari/test_invites.py | 4 ++-- tests/hikari/test_messages.py | 7 +++++-- tests/hikari/test_oauth2.py | 5 +++-- tests/hikari/test_users.py | 5 +++-- tests/hikari/test_webhook.py | 2 +- 26 files changed, 57 insertions(+), 57 deletions(-) diff --git a/dev-requirements.txt b/dev-requirements.txt index 081c8267a2..83a9b093eb 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -6,7 +6,3 @@ pytest-asyncio==0.10.0 pytest-cov==2.8.1 pytest-html==2.1.1 pytest-xdist==1.31.0 - -# My Cythonized mock module -cymock==2020.2.26.2 - diff --git a/tests/hikari/_helpers.py b/tests/hikari/_helpers.py index e9ef08332d..24f3691c88 100644 --- a/tests/hikari/_helpers.py +++ b/tests/hikari/_helpers.py @@ -30,9 +30,9 @@ import typing import warnings import weakref +from unittest import mock import async_timeout -import cymock as mock import pytest from hikari import snowflakes diff --git a/tests/hikari/clients/test_configs.py b/tests/hikari/clients/test_configs.py index f16e8e2545..41d80ae943 100644 --- a/tests/hikari/clients/test_configs.py +++ b/tests/hikari/clients/test_configs.py @@ -16,17 +16,16 @@ # # You should have received a copy of the GNU Lesser General Public License # along ith Hikari. If not, see . -import aiohttp -import ssl import datetime +import ssl -import cymock as mock +import aiohttp import pytest from hikari import gateway_entities from hikari import guilds -from hikari.net import codes from hikari.clients import configs +from hikari.net import codes from tests.hikari import _helpers diff --git a/tests/hikari/clients/test_gateway_managers.py b/tests/hikari/clients/test_gateway_managers.py index cfcfbc6a39..cb080f0695 100644 --- a/tests/hikari/clients/test_gateway_managers.py +++ b/tests/hikari/clients/test_gateway_managers.py @@ -17,8 +17,8 @@ # You should have received a copy of the GNU Lesser General Public License # along ith Hikari. If not, see . import math +from unittest import mock -import cymock as mock import pytest from hikari.clients import gateway_managers diff --git a/tests/hikari/clients/test_rest_client.py b/tests/hikari/clients/test_rest_client.py index d9f44d0975..382e4f1ffd 100644 --- a/tests/hikari/clients/test_rest_client.py +++ b/tests/hikari/clients/test_rest_client.py @@ -18,16 +18,10 @@ # along ith Hikari. If not, see . import datetime import io +from unittest import mock -import cymock as mock -import datetime import pytest - -from hikari.internal import conversions -from hikari.clients import configs -from hikari.clients import rest_clients -from hikari.net import rest from hikari import audit_logs from hikari import channels from hikari import colors @@ -44,7 +38,10 @@ from hikari import users from hikari import voices from hikari import webhooks - +from hikari.clients import configs +from hikari.clients import rest_clients +from hikari.internal import conversions +from hikari.net import rest from tests.hikari import _helpers diff --git a/tests/hikari/clients/test_shard_clients.py b/tests/hikari/clients/test_shard_clients.py index e42ed39a0f..5282d8cd64 100644 --- a/tests/hikari/clients/test_shard_clients.py +++ b/tests/hikari/clients/test_shard_clients.py @@ -18,19 +18,18 @@ # along ith Hikari. If not, see . import datetime import math -import asyncio -import aiohttp +from unittest import mock -import cymock as mock +import aiohttp import pytest -from hikari import guilds from hikari import errors -from hikari.net import shard +from hikari import guilds +from hikari.clients import configs +from hikari.clients import shard_clients from hikari.net import codes +from hikari.net import shard from hikari.state import raw_event_consumers -from hikari.clients import shard_clients -from hikari.clients import configs from tests.hikari import _helpers diff --git a/tests/hikari/internal/test_conversions.py b/tests/hikari/internal/test_conversions.py index 1f2179ee12..ecefd9ad32 100644 --- a/tests/hikari/internal/test_conversions.py +++ b/tests/hikari/internal/test_conversions.py @@ -20,10 +20,10 @@ import datetime import inspect import io +import typing +from unittest import mock -import cymock as mock import pytest -import typing from hikari.internal import conversions from tests.hikari import _helpers diff --git a/tests/hikari/internal/test_marshaller.py b/tests/hikari/internal/test_marshaller.py index 758159282c..6a867b7d17 100644 --- a/tests/hikari/internal/test_marshaller.py +++ b/tests/hikari/internal/test_marshaller.py @@ -16,8 +16,9 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . +from unittest import mock + import attr -import cymock as mock import pytest from hikari.internal import marshaller diff --git a/tests/hikari/internal/test_marshaller_pep563.py b/tests/hikari/internal/test_marshaller_pep563.py index f0a1247b41..4e2d7b04fb 100644 --- a/tests/hikari/internal/test_marshaller_pep563.py +++ b/tests/hikari/internal/test_marshaller_pep563.py @@ -23,8 +23,9 @@ """ from __future__ import annotations +from unittest import mock + import attr -import cymock as mock import pytest from hikari.internal import marshaller diff --git a/tests/hikari/net/test_ratelimits.py b/tests/hikari/net/test_ratelimits.py index 8661f39bea..29f3844be6 100644 --- a/tests/hikari/net/test_ratelimits.py +++ b/tests/hikari/net/test_ratelimits.py @@ -23,8 +23,8 @@ import math import statistics import time +from unittest import mock -import cymock as mock import pytest from hikari.net import ratelimits diff --git a/tests/hikari/net/test_rest.py b/tests/hikari/net/test_rest.py index 38930d8384..8817f3d3f4 100644 --- a/tests/hikari/net/test_rest.py +++ b/tests/hikari/net/test_rest.py @@ -23,13 +23,13 @@ import logging import ssl import unittest.mock +from unittest import mock import aiohttp -import cymock as mock import pytest -from hikari.internal import conversions from hikari import errors +from hikari.internal import conversions from hikari.net import ratelimits from hikari.net import rest from hikari.net import routes diff --git a/tests/hikari/net/test_shard.py b/tests/hikari/net/test_shard.py index 167f2f4f43..3a11c98f33 100644 --- a/tests/hikari/net/test_shard.py +++ b/tests/hikari/net/test_shard.py @@ -22,14 +22,14 @@ import math import time import urllib.parse +from unittest import mock import aiohttp import async_timeout -from unittest import mock import pytest -from hikari.internal import more_collections from hikari import errors +from hikari.internal import more_collections from hikari.net import shard from hikari.net import user_agent from hikari.net import versions diff --git a/tests/hikari/state/test_event_dispatcher.py b/tests/hikari/state/test_event_dispatcher.py index 4b8c5ad5df..e6a8fff44a 100644 --- a/tests/hikari/state/test_event_dispatcher.py +++ b/tests/hikari/state/test_event_dispatcher.py @@ -21,8 +21,8 @@ import pytest -from hikari.state import event_dispatchers from hikari import events +from hikari.state import event_dispatchers from tests.hikari import _helpers diff --git a/tests/hikari/test_audit_logs.py b/tests/hikari/test_audit_logs.py index fe50735b99..d319121985 100644 --- a/tests/hikari/test_audit_logs.py +++ b/tests/hikari/test_audit_logs.py @@ -16,10 +16,10 @@ # # You should have received a copy of the GNU Lesser General Public License # along ith Hikari. If not, see . -import datetime import contextlib +import datetime +from unittest import mock -import cymock as mock import pytest from hikari import audit_logs diff --git a/tests/hikari/test_channels.py b/tests/hikari/test_channels.py index 6e73f47224..8fcf8dea52 100644 --- a/tests/hikari/test_channels.py +++ b/tests/hikari/test_channels.py @@ -17,8 +17,8 @@ # You should have received a copy of the GNU Lesser General Public License # along ith Hikari. If not, see . import datetime +from unittest import mock -import cymock as mock import pytest from hikari import channels diff --git a/tests/hikari/test_colours.py b/tests/hikari/test_colours.py index a281474d9a..0fe10e273a 100644 --- a/tests/hikari/test_colours.py +++ b/tests/hikari/test_colours.py @@ -18,7 +18,8 @@ # along with Hikari. If not, see . import pytest -from hikari import colours, colors +from hikari import colors +from hikari import colours @pytest.mark.model diff --git a/tests/hikari/test_embeds.py b/tests/hikari/test_embeds.py index 7b0eb2b5c7..5ccaa25a03 100644 --- a/tests/hikari/test_embeds.py +++ b/tests/hikari/test_embeds.py @@ -17,12 +17,13 @@ # You should have received a copy of the GNU Lesser General Public License # along ith Hikari. If not, see . import datetime +from unittest import mock -import cymock as mock import pytest import hikari.internal.conversions -from hikari import embeds, colors +from hikari import colors +from hikari import embeds from tests.hikari import _helpers diff --git a/tests/hikari/test_emojis.py b/tests/hikari/test_emojis.py index 16b017eaee..54b4a0cf40 100644 --- a/tests/hikari/test_emojis.py +++ b/tests/hikari/test_emojis.py @@ -16,7 +16,8 @@ # # You should have received a copy of the GNU Lesser General Public License # along ith Hikari. If not, see . -import cymock as mock +from unittest import mock + import pytest from hikari import emojis diff --git a/tests/hikari/test_events.py b/tests/hikari/test_events.py index 16179243fe..e8cc705247 100644 --- a/tests/hikari/test_events.py +++ b/tests/hikari/test_events.py @@ -17,8 +17,8 @@ # You should have received a copy of the GNU Lesser General Public License # along ith Hikari. If not, see . import datetime +from unittest import mock -import cymock as mock import pytest import hikari.internal.conversions diff --git a/tests/hikari/test_gateway_entities.py b/tests/hikari/test_gateway_entities.py index b89ee57677..0d31719d38 100644 --- a/tests/hikari/test_gateway_entities.py +++ b/tests/hikari/test_gateway_entities.py @@ -17,8 +17,8 @@ # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . import datetime +from unittest import mock -import cymock as mock import pytest from hikari import gateway_entities diff --git a/tests/hikari/test_guilds.py b/tests/hikari/test_guilds.py index dce9673879..f809ebc7c7 100644 --- a/tests/hikari/test_guilds.py +++ b/tests/hikari/test_guilds.py @@ -17,18 +17,17 @@ # You should have received a copy of the GNU Lesser General Public License # along ith Hikari. If not, see . import datetime +from unittest import mock -import cymock as mock import pytest import hikari.internal.conversions -from hikari.internal import urls +from hikari import channels from hikari import emojis from hikari import entities from hikari import guilds from hikari import users -from hikari import channels - +from hikari.internal import urls from tests.hikari import _helpers diff --git a/tests/hikari/test_invites.py b/tests/hikari/test_invites.py index 3a9ed77f5a..a2a64cd66c 100644 --- a/tests/hikari/test_invites.py +++ b/tests/hikari/test_invites.py @@ -17,16 +17,16 @@ # You should have received a copy of the GNU Lesser General Public License # along ith Hikari. If not, see . import datetime +from unittest import mock -import cymock as mock import pytest import hikari.internal.conversions -from hikari.internal import urls from hikari import channels from hikari import guilds from hikari import invites from hikari import users +from hikari.internal import urls from tests.hikari import _helpers diff --git a/tests/hikari/test_messages.py b/tests/hikari/test_messages.py index 20ee85888a..1b942fd5e5 100644 --- a/tests/hikari/test_messages.py +++ b/tests/hikari/test_messages.py @@ -17,13 +17,16 @@ # You should have received a copy of the GNU Lesser General Public License # along ith Hikari. If not, see . import datetime +from unittest import mock -import cymock as mock import pytest import hikari.internal.conversions +from hikari import embeds +from hikari import emojis from hikari import guilds -from hikari import oauth2, emojis, embeds, messages +from hikari import messages +from hikari import oauth2 from hikari import users from tests.hikari import _helpers diff --git a/tests/hikari/test_oauth2.py b/tests/hikari/test_oauth2.py index a763ec7496..51c27a8a95 100644 --- a/tests/hikari/test_oauth2.py +++ b/tests/hikari/test_oauth2.py @@ -16,13 +16,14 @@ # # You should have received a copy of the GNU Lesser General Public License # along ith Hikari. If not, see . -import cymock as mock +from unittest import mock + import pytest -from hikari.internal import urls from hikari import guilds from hikari import oauth2 from hikari import users +from hikari.internal import urls from tests.hikari import _helpers diff --git a/tests/hikari/test_users.py b/tests/hikari/test_users.py index f05e50e9ad..971b9ad46d 100644 --- a/tests/hikari/test_users.py +++ b/tests/hikari/test_users.py @@ -16,11 +16,12 @@ # # You should have received a copy of the GNU Lesser General Public License # along ith Hikari. If not, see . -import cymock as mock +from unittest import mock + import pytest -from hikari.internal import urls from hikari import users +from hikari.internal import urls @pytest.fixture() diff --git a/tests/hikari/test_webhook.py b/tests/hikari/test_webhook.py index ed0226d253..f87a1a6a6e 100644 --- a/tests/hikari/test_webhook.py +++ b/tests/hikari/test_webhook.py @@ -16,7 +16,7 @@ # # You should have received a copy of the GNU Lesser General Public License # along ith Hikari. If not, see . -import cymock as mock +from unittest import mock from hikari import users from hikari import webhooks From 4ffefadd66b826a6ce8de28b414350dc98969484 Mon Sep 17 00:00:00 2001 From: Faster Speeding Date: Sun, 12 Apr 2020 20:44:58 +0100 Subject: [PATCH 101/922] Fix broken tests + test beautification. --- hikari/audit_logs.py | 1 + tests/hikari/clients/test_rest_client.py | 304 +++++++++++---------- tests/hikari/clients/test_shard_clients.py | 1 - tests/hikari/net/test_rest.py | 158 +++++------ tests/hikari/test_audit_logs.py | 6 +- tests/hikari/test_channels.py | 9 +- tests/hikari/test_events.py | 273 ++++++++++-------- tests/hikari/test_guilds.py | 207 +++++++++----- tests/hikari/test_invites.py | 134 ++++++--- tests/hikari/test_messages.py | 83 +++--- tests/hikari/test_oauth2.py | 48 ++-- tests/hikari/test_users.py | 22 +- 12 files changed, 718 insertions(+), 528 deletions(-) diff --git a/hikari/audit_logs.py b/hikari/audit_logs.py index 1ca832678b..b4b4ec74cf 100644 --- a/hikari/audit_logs.py +++ b/hikari/audit_logs.py @@ -631,6 +631,7 @@ async def _fill(self) -> None: if self._limit is not None: self._limit -= len(payload["audit_log_entries"]) + # Once the resources has been exhausted, discord will return empty lists. payload["audit_log_entries"].reverse() self._buffer.extend(payload["audit_log_entries"]) if users := payload.get("users"): diff --git a/tests/hikari/clients/test_rest_client.py b/tests/hikari/clients/test_rest_client.py index 382e4f1ffd..adfc01515e 100644 --- a/tests/hikari/clients/test_rest_client.py +++ b/tests/hikari/clients/test_rest_client.py @@ -16,6 +16,7 @@ # # You should have received a copy of the GNU Lesser General Public License # along ith Hikari. If not, see . +import contextlib import datetime import io from unittest import mock @@ -229,9 +230,8 @@ async def test_update_channel_with_optionals(self, rest_clients_impl, channel, p mock_payload = {"name": "Qts", "type": 2} mock_channel_obj = mock.MagicMock(channels.Channel) mock_overwrite_payload = {"type": "user", "id": 543543543} - mock_overwrite_obj = mock.create_autospec( - channels.PermissionOverwrite, serialize=mock.MagicMock(return_value=mock_overwrite_payload) - ) + mock_overwrite_obj = mock.create_autospec(channels.PermissionOverwrite) + mock_overwrite_obj.serialize = mock.MagicMock(return_value=mock_overwrite_payload) rest_clients_impl._session.modify_channel.return_value = mock_payload with mock.patch.object(channels, "deserialize_channel", return_value=mock_channel_obj): result = await rest_clients_impl.update_channel( @@ -756,27 +756,28 @@ async def test_create_message_with_optionals(self, rest_clients_impl, channel): mock_allowed_mentions_payload = {"parse": ["everyone", "users", "roles"]} rest_clients_impl._generate_allowed_mentions = mock.MagicMock(return_value=mock_allowed_mentions_payload) mock_embed_payload = {"description": "424242"} - mock_embed_obj = mock.create_autospec( - embeds.Embed, auto_spec=True, serialize=mock.MagicMock(return_value=mock_embed_payload) - ) + mock_embed_obj = mock.create_autospec(embeds.Embed, auto_spec=True) + mock_embed_obj.serialize = mock.MagicMock(return_value=mock_embed_payload) mock_media_obj = mock.MagicMock() mock_media_payload = ("aName.png", mock.MagicMock()) - with mock.patch.object(messages.Message, "deserialize", return_value=mock_message_obj): - with mock.patch.object(media, "safe_read_file", return_value=mock_media_payload): - result = await rest_clients_impl.create_message( - channel, - content="A CONTENT", - nonce="69696969696969", - tts=True, - files=[mock_media_obj], - embed=mock_embed_obj, - mentions_everyone=False, - user_mentions=False, - role_mentions=False, - ) - assert result is mock_message_obj - media.safe_read_file.assert_called_once_with(mock_media_obj) - messages.Message.deserialize.assert_called_once_with(mock_message_payload) + stack = contextlib.ExitStack() + stack.enter_context(mock.patch.object(messages.Message, "deserialize", return_value=mock_message_obj)) + stack.enter_context(mock.patch.object(media, "safe_read_file", return_value=mock_media_payload)) + with stack: + result = await rest_clients_impl.create_message( + channel, + content="A CONTENT", + nonce="69696969696969", + tts=True, + files=[mock_media_obj], + embed=mock_embed_obj, + mentions_everyone=False, + user_mentions=False, + role_mentions=False, + ) + assert result is mock_message_obj + media.safe_read_file.assert_called_once_with(mock_media_obj) + messages.Message.deserialize.assert_called_once_with(mock_message_payload) rest_clients_impl._session.create_message.assert_called_once_with( channel_id="694463529998352394", content="A CONTENT", @@ -980,9 +981,8 @@ async def test_update_message_with_optionals(self, rest_clients_impl, message, c mock_payload = {"id": "4242", "content": "I HAVE BEEN UPDATED!"} mock_message_obj = mock.MagicMock(messages.Message) mock_embed_payload = {"description": "blahblah"} - mock_embed = mock.create_autospec( - embeds.Embed, auto_spec=True, serialize=mock.MagicMock(return_value=mock_embed_payload) - ) + mock_embed = mock.create_autospec(embeds.Embed, auto_spec=True) + mock_embed.serialize = mock.MagicMock(return_value=mock_embed_payload) mock_allowed_mentions_payload = {"parse": [], "users": ["123"]} rest_clients_impl._generate_allowed_mentions = mock.MagicMock(return_value=mock_allowed_mentions_payload) rest_clients_impl._session.edit_message.return_value = mock_payload @@ -1336,13 +1336,15 @@ async def test_create_guild_emoji_with_optionals(self, rest_clients_impl, guild, rest_clients_impl._session.create_guild_emoji.return_value = mock_emoji_payload mock_image_obj = mock.MagicMock(io.BytesIO) mock_image_data = mock.MagicMock(bytes) - with mock.patch.object(conversions, "get_bytes_from_resource", return_value=mock_image_data): - with mock.patch.object(emojis.GuildEmoji, "deserialize", return_value=mock_emoji_obj): - result = await rest_clients_impl.create_guild_emoji( - guild=guild, name="fairEmoji", image_data=mock_image_obj, roles=[role], reason="hello", - ) - assert result is mock_emoji_obj - emojis.GuildEmoji.deserialize.assert_called_once_with(mock_emoji_payload) + stack = contextlib.ExitStack() + stack.enter_context(mock.patch.object(conversions, "get_bytes_from_resource", return_value=mock_image_data)) + stack.enter_context(mock.patch.object(emojis.GuildEmoji, "deserialize", return_value=mock_emoji_obj)) + with stack: + result = await rest_clients_impl.create_guild_emoji( + guild=guild, name="fairEmoji", image_data=mock_image_obj, roles=[role], reason="hello", + ) + assert result is mock_emoji_obj + emojis.GuildEmoji.deserialize.assert_called_once_with(mock_emoji_payload) conversions.get_bytes_from_resource.assert_called_once_with(mock_image_obj) rest_clients_impl._session.create_guild_emoji.assert_called_once_with( guild_id="93443949", name="fairEmoji", image=mock_image_data, roles=["537340989808050216"], reason="hello", @@ -1356,13 +1358,15 @@ async def test_create_guild_emoji_without_optionals(self, rest_clients_impl, gui rest_clients_impl._session.create_guild_emoji.return_value = mock_emoji_payload mock_image_obj = mock.MagicMock(io.BytesIO) mock_image_data = mock.MagicMock(bytes) - with mock.patch.object(conversions, "get_bytes_from_resource", return_value=mock_image_data): - with mock.patch.object(emojis.GuildEmoji, "deserialize", return_value=mock_emoji_obj): - result = await rest_clients_impl.create_guild_emoji( - guild=guild, name="fairEmoji", image_data=mock_image_obj, - ) - assert result is mock_emoji_obj - emojis.GuildEmoji.deserialize.assert_called_once_with(mock_emoji_payload) + stack = contextlib.ExitStack() + stack.enter_context(mock.patch.object(conversions, "get_bytes_from_resource", return_value=mock_image_data)) + stack.enter_context(mock.patch.object(emojis.GuildEmoji, "deserialize", return_value=mock_emoji_obj)) + with stack: + result = await rest_clients_impl.create_guild_emoji( + guild=guild, name="fairEmoji", image_data=mock_image_obj, + ) + assert result is mock_emoji_obj + emojis.GuildEmoji.deserialize.assert_called_once_with(mock_emoji_payload) conversions.get_bytes_from_resource.assert_called_once_with(mock_image_obj) rest_clients_impl._session.create_guild_emoji.assert_called_once_with( guild_id="93443949", name="fairEmoji", image=mock_image_data, roles=..., reason=..., @@ -1417,27 +1421,27 @@ async def test_create_guild_with_optionals(self, rest_clients_impl, region): mock_image_obj = mock.MagicMock(io.BytesIO) mock_image_data = mock.MagicMock(bytes) mock_role_payload = {"permissions": 123123} - mock_role_obj = mock.create_autospec( - guilds.GuildRole, spec_set=True, serialize=mock.MagicMock(return_value=mock_role_payload) - ) + mock_role_obj = mock.create_autospec(guilds.GuildRole, spec_set=True) + mock_role_obj.serialize = mock.MagicMock(return_value=mock_role_payload) mock_channel_payload = {"type": 2, "name": "aChannel"} - mock_channel_obj = mock.create_autospec( - channels.GuildChannel, spec_set=True, serialize=mock.MagicMock(return_value=mock_channel_payload) - ) - with mock.patch.object(guilds.Guild, "deserialize", return_value=mock_guild_obj): - with mock.patch.object(conversions, "get_bytes_from_resource", return_value=mock_image_data): - result = await rest_clients_impl.create_guild( - name="OK", - region=region, - icon_data=mock_image_obj, - verification_level=guilds.GuildVerificationLevel.NONE, - default_message_notifications=guilds.GuildMessageNotificationsLevel.ONLY_MENTIONS, - explicit_content_filter=guilds.GuildExplicitContentFilterLevel.MEMBERS_WITHOUT_ROLES, - roles=[mock_role_obj], - channels=[mock_channel_obj], - ) - assert result is mock_guild_obj - conversions.get_bytes_from_resource.assert_called_once_with(mock_image_obj) + mock_channel_obj = mock.create_autospec(channels.GuildChannel, spec_set=True) + mock_channel_obj.serialize = mock.MagicMock(return_value=mock_channel_payload) + stack = contextlib.ExitStack() + stack.enter_context(mock.patch.object(guilds.Guild, "deserialize", return_value=mock_guild_obj)) + stack.enter_context(mock.patch.object(conversions, "get_bytes_from_resource", return_value=mock_image_data)) + with stack: + result = await rest_clients_impl.create_guild( + name="OK", + region=region, + icon_data=mock_image_obj, + verification_level=guilds.GuildVerificationLevel.NONE, + default_message_notifications=guilds.GuildMessageNotificationsLevel.ONLY_MENTIONS, + explicit_content_filter=guilds.GuildExplicitContentFilterLevel.MEMBERS_WITHOUT_ROLES, + roles=[mock_role_obj], + channels=[mock_channel_obj], + ) + assert result is mock_guild_obj + conversions.get_bytes_from_resource.assert_called_once_with(mock_image_obj) guilds.Guild.deserialize.assert_called_once_with(mock_guild_payload) mock_channel_obj.serialize.assert_called_once() mock_role_obj.serialize.assert_called_once() @@ -1499,41 +1503,45 @@ async def test_update_guild_with_optionals( mock_icon_obj = mock.MagicMock(io.BytesIO) mock_splash_data = mock.MagicMock(bytes) mock_splash_obj = mock.MagicMock(io.BytesIO) - with mock.patch.object(guilds.Guild, "deserialize", return_value=mock_guild_obj): - with mock.patch.object( - conversions, "get_bytes_from_resource", side_effect=[mock_icon_data, mock_splash_data] - ): - result = await rest_clients_impl.update_guild( - guild, - name="aNewName", - region=region, - verification_level=guilds.GuildVerificationLevel.LOW, - default_message_notifications=guilds.GuildMessageNotificationsLevel.ONLY_MENTIONS, - explicit_content_filter=guilds.GuildExplicitContentFilterLevel.ALL_MEMBERS, - afk_channel=afk_channel, - afk_timeout=afk_timeout, - icon_data=mock_icon_obj, - owner=owner, - splash_data=mock_splash_obj, - system_channel=system_channel, - reason="A good reason", - ) - assert result is mock_guild_obj - rest_clients_impl._session.modify_guild.assert_called_once_with( - guild_id="379953393319542784", + stack = contextlib.ExitStack() + stack.enter_context(mock.patch.object(guilds.Guild, "deserialize", return_value=mock_guild_obj)) + stack.enter_context( + mock.patch.object(conversions, "get_bytes_from_resource", side_effect=[mock_icon_data, mock_splash_data]) + ) + with stack: + result = await rest_clients_impl.update_guild( + guild, name="aNewName", - region="LONDON", - verification_level=1, - default_message_notifications=1, - explicit_content_filter=2, - afk_channel_id="669517187031105607", - afk_timeout=300, - icon=mock_icon_data, - owner_id="379953393319542784", - splash=mock_splash_data, - system_channel_id="537340989808050216", + region=region, + verification_level=guilds.GuildVerificationLevel.LOW, + default_message_notifications=guilds.GuildMessageNotificationsLevel.ONLY_MENTIONS, + explicit_content_filter=guilds.GuildExplicitContentFilterLevel.ALL_MEMBERS, + afk_channel=afk_channel, + afk_timeout=afk_timeout, + icon_data=mock_icon_obj, + owner=owner, + splash_data=mock_splash_obj, + system_channel=system_channel, reason="A good reason", ) + assert result is mock_guild_obj + guilds.Guild.deserialize.assert_called_once_with(mock_guild_payload) + conversions.get_bytes_from_resource.has_calls(mock.call(mock_icon_obj), mock.call(mock_splash_obj)) + rest_clients_impl._session.modify_guild.assert_called_once_with( + guild_id="379953393319542784", + name="aNewName", + region="LONDON", + verification_level=1, + default_message_notifications=1, + explicit_content_filter=2, + afk_channel_id="669517187031105607", + afk_timeout=300, + icon=mock_icon_data, + owner_id="379953393319542784", + splash=mock_splash_data, + system_channel_id="537340989808050216", + reason="A good reason", + ) @pytest.mark.asyncio @_helpers.parametrize_valid_id_formats_for_models("guild", 379953393319542784, guilds.Guild) @@ -2319,15 +2327,15 @@ async def test_update_me_with_optionals(self, rest_clients_impl): rest_clients_impl._session.modify_current_user.return_value = mock_user_payload mock_avatar_obj = mock.MagicMock(io.BytesIO) mock_avatar_data = mock.MagicMock(bytes) - with mock.patch.object(users.MyUser, "deserialize", return_value=mock_user_obj): - with mock.patch.object(conversions, "get_bytes_from_resource", return_value=mock_avatar_data): - assert ( - await rest_clients_impl.update_me(username="aNewName", avatar_data=mock_avatar_obj) is mock_user_obj - ) - rest_clients_impl._session.modify_current_user.assert_called_once_with( - username="aNewName", avatar=mock_avatar_data - ) - conversions.get_bytes_from_resource.assert_called_once_with(mock_avatar_obj) + stack = contextlib.ExitStack() + stack.enter_context(mock.patch.object(users.MyUser, "deserialize", return_value=mock_user_obj)) + stack.enter_context(mock.patch.object(conversions, "get_bytes_from_resource", return_value=mock_avatar_data)) + with stack: + assert await rest_clients_impl.update_me(username="aNewName", avatar_data=mock_avatar_obj) is mock_user_obj + rest_clients_impl._session.modify_current_user.assert_called_once_with( + username="aNewName", avatar=mock_avatar_data + ) + conversions.get_bytes_from_resource.assert_called_once_with(mock_avatar_obj) users.MyUser.deserialize.assert_called_once_with(mock_user_payload) @pytest.mark.asyncio @@ -2468,13 +2476,15 @@ async def test_create_webhook_with_optionals(self, rest_clients_impl, channel): rest_clients_impl._session.create_webhook.return_value = mock_webhook_payload mock_image_obj = mock.MagicMock(io.BytesIO) mock_image_data = mock.MagicMock(bytes) - with mock.patch.object(webhooks.Webhook, "deserialize", return_value=mock_webhook_obj): - with mock.patch.object(conversions, "get_bytes_from_resource", return_value=mock_image_data): - result = await rest_clients_impl.create_webhook( - channel=channel, name="aWebhook", avatar_data=mock_image_obj, reason="And a webhook is born." - ) - assert result is mock_webhook_obj - conversions.get_bytes_from_resource.assert_called_once_with(mock_image_obj) + stack = contextlib.ExitStack() + stack.enter_context(mock.patch.object(webhooks.Webhook, "deserialize", return_value=mock_webhook_obj)) + stack.enter_context(mock.patch.object(conversions, "get_bytes_from_resource", return_value=mock_image_data)) + with stack: + result = await rest_clients_impl.create_webhook( + channel=channel, name="aWebhook", avatar_data=mock_image_obj, reason="And a webhook is born." + ) + assert result is mock_webhook_obj + conversions.get_bytes_from_resource.assert_called_once_with(mock_image_obj) webhooks.Webhook.deserialize.assert_called_once_with(mock_webhook_payload) rest_clients_impl._session.create_webhook.assert_called_once_with( channel_id="115590097100865541", name="aWebhook", avatar=mock_image_data, reason="And a webhook is born." @@ -2550,26 +2560,29 @@ async def test_update_webhook_with_optionals(self, rest_clients_impl, webhook, c rest_clients_impl._session.modify_webhook.return_value = mock_webhook_payload mock_image_obj = mock.MagicMock(io.BytesIO) mock_image_data = mock.MagicMock(bytes) - with mock.patch.object(webhooks.Webhook, "deserialize", return_value=mock_webhook_obj): - with mock.patch.object(conversions, "get_bytes_from_resource", return_value=mock_image_data): - result = await rest_clients_impl.update_webhook( - webhook, - webhook_token="a.wEbHoOk.ToKeN", - name="blah_blah_blah", - avatar_data=mock_image_obj, - channel=channel, - reason="A reason", - ) - assert result is mock_webhook_obj - rest_clients_impl._session.modify_webhook.assert_called_once_with( - webhook_id="379953393319542784", - webhook_token="a.wEbHoOk.ToKeN", - name="blah_blah_blah", - avatar=mock_image_data, - channel_id="115590097100865541", - reason="A reason", - ) + stack = contextlib.ExitStack() + stack.enter_context(mock.patch.object(webhooks.Webhook, "deserialize", return_value=mock_webhook_obj)) + stack.enter_context(mock.patch.object(conversions, "get_bytes_from_resource", return_value=mock_image_data)) + with stack: + result = await rest_clients_impl.update_webhook( + webhook, + webhook_token="a.wEbHoOk.ToKeN", + name="blah_blah_blah", + avatar_data=mock_image_obj, + channel=channel, + reason="A reason", + ) + assert result is mock_webhook_obj + rest_clients_impl._session.modify_webhook.assert_called_once_with( + webhook_id="379953393319542784", + webhook_token="a.wEbHoOk.ToKeN", + name="blah_blah_blah", + avatar=mock_image_data, + channel_id="115590097100865541", + reason="A reason", + ) webhooks.Webhook.deserialize.assert_called_once_with(mock_webhook_payload) + conversions.get_bytes_from_resource.assert_called_once_with(mock_image_obj) @pytest.mark.asyncio @_helpers.parametrize_valid_id_formats_for_models("webhook", 379953393319542784, webhooks.Webhook) @@ -2629,27 +2642,28 @@ async def test_execute_webhook_with_optionals(self, rest_clients_impl, webhook): mock_allowed_mentions_payload = {"parse": ["everyone", "users", "roles"]} rest_clients_impl._generate_allowed_mentions = mock.MagicMock(return_value=mock_allowed_mentions_payload) mock_embed_payload = {"description": "424242"} - mock_embed_obj = mock.create_autospec( - embeds.Embed, auto_spec=True, serialize=mock.MagicMock(return_value=mock_embed_payload) - ) + mock_embed_obj = mock.create_autospec(embeds.Embed, auto_spec=True) + mock_embed_obj.serialize = mock.MagicMock(return_value=mock_embed_payload) mock_media_obj = mock.MagicMock() mock_media_payload = ("aName.png", mock.MagicMock()) - with mock.patch.object(media, "safe_read_file", return_value=mock_media_payload): - with mock.patch.object(messages.Message, "deserialize"): - await rest_clients_impl.execute_webhook( - webhook, - "a.webhook.token", - content="THE TRUTH", - username="User 97", - avatar_url="httttttt/L//", - tts=True, - wait=True, - file=mock_media_obj, - embeds=[mock_embed_obj], - mentions_everyone=False, - role_mentions=False, - user_mentions=False, - ) + stack = contextlib.ExitStack() + stack.enter_context(mock.patch.object(media, "safe_read_file", return_value=mock_media_payload)) + stack.enter_context(mock.patch.object(messages.Message, "deserialize")) + with stack: + await rest_clients_impl.execute_webhook( + webhook, + "a.webhook.token", + content="THE TRUTH", + username="User 97", + avatar_url="httttttt/L//", + tts=True, + wait=True, + file=mock_media_obj, + embeds=[mock_embed_obj], + mentions_everyone=False, + role_mentions=False, + user_mentions=False, + ) media.safe_read_file.assert_called_once_with(mock_media_obj) rest_clients_impl._session.execute_webhook.assert_called_once_with( webhook_id="379953393319542784", diff --git a/tests/hikari/clients/test_shard_clients.py b/tests/hikari/clients/test_shard_clients.py index 5282d8cd64..a8e969e1e1 100644 --- a/tests/hikari/clients/test_shard_clients.py +++ b/tests/hikari/clients/test_shard_clients.py @@ -161,7 +161,6 @@ async def test_close_when_already_stopping(self, shard_client_obj): @pytest.mark.parametrize( "error", [ - None, aiohttp.ClientConnectorError(mock.MagicMock(), mock.MagicMock()), errors.GatewayZombiedError, errors.GatewayInvalidSessionError(False), diff --git a/tests/hikari/net/test_rest.py b/tests/hikari/net/test_rest.py index 8817f3d3f4..a36b1ca24e 100644 --- a/tests/hikari/net/test_rest.py +++ b/tests/hikari/net/test_rest.py @@ -127,15 +127,18 @@ def __init__(self, *args, **kwargs): async def test__init__with_bot_token_and_without_optionals(self): mock_manual_rate_limiter = mock.MagicMock() buckets_mock = mock.MagicMock() + mock_client_session = mock.MagicMock(aiohttp.ClientSession) stack = contextlib.ExitStack() stack.enter_context(mock.patch.object(ratelimits, "ManualRateLimiter", return_value=mock_manual_rate_limiter)) stack.enter_context(mock.patch.object(ratelimits, "HTTPBucketRateLimiterManager", return_value=buckets_mock)) + stack.enter_context(mock.patch.object(aiohttp, "ClientSession", return_value=mock_client_session)) with stack: client = rest.LowLevelRestfulClient(token="Bot token.otacon.a-token") assert client.base_url == f"https://discordapp.com/api/v{int(versions.HTTPAPIVersion.STABLE)}" + assert client.client_session is mock_client_session assert client.global_ratelimiter is mock_manual_rate_limiter assert client.json_serialize is json.dumps assert client.json_deserialize is json.loads @@ -144,8 +147,13 @@ async def test__init__with_bot_token_and_without_optionals(self): @pytest.mark.asyncio async def test__init__with_bearer_token_and_without_optionals(self): - client = rest.LowLevelRestfulClient(token="Bearer token.otacon.a-token") - assert client.token == "Bearer token.otacon.a-token" + stack = contextlib.ExitStack() + stack.enter_context(mock.patch.object(ratelimits, "ManualRateLimiter")) + stack.enter_context(mock.patch.object(ratelimits, "HTTPBucketRateLimiterManager")) + stack.enter_context(mock.patch.object(aiohttp, "ClientSession")) + with stack: + client = rest.LowLevelRestfulClient(token="Bearer token.otacon.a-token") + assert client.token == "Bearer token.otacon.a-token" @pytest.mark.asyncio async def test__init__with_optionals(self): @@ -192,8 +200,13 @@ async def test__init__with_optionals(self): @pytest.mark.asyncio @_helpers.assert_raises(type_=RuntimeError) async def test__init__raises_runtime_error_with_invalid_token(self, *_): - async with rest.LowLevelRestfulClient(token="An-invalid-TOKEN"): - pass + stack = contextlib.ExitStack() + stack.enter_context(mock.patch.object(ratelimits, "ManualRateLimiter")) + stack.enter_context(mock.patch.object(ratelimits, "HTTPBucketRateLimiterManager")) + stack.enter_context(mock.patch.object(aiohttp, "ClientSession")) + with stack: + async with rest.LowLevelRestfulClient(token="An-invalid-TOKEN"): + pass @pytest.mark.asyncio async def test_close(self, rest_impl): @@ -204,7 +217,8 @@ async def test_close(self, rest_impl): @pytest.fixture() @mock.patch.object(ratelimits, "ManualRateLimiter") @mock.patch.object(ratelimits, "HTTPBucketRateLimiterManager") - async def rest_impl_with__request(self, *args): + @mock.patch.object(aiohttp, "ClientSession") + def rest_impl_with__request(self, *args): rest_impl = rest.LowLevelRestfulClient(token="Bot token") rest_impl.logger = mock.MagicMock(debug=mock.MagicMock()) rest_impl.ratelimiter = mock.create_autospec( @@ -220,33 +234,27 @@ async def rest_impl_with__request(self, *args): @pytest.mark.asyncio async def test__request_acquires_ratelimiter(self, compiled_route, exit_error, rest_impl_with__request): - rest_impl = await rest_impl_with__request - rest_impl.logger.debug.side_effect = exit_error + rest_impl_with__request.logger.debug.side_effect = exit_error with mock.patch("asyncio.gather", return_value=_helpers.AwaitableMock()): try: - await rest_impl._request(compiled_route) + await rest_impl_with__request._request(compiled_route) except exit_error: pass - rest_impl.ratelimiter.acquire.asset_called_once_with(compiled_route) + rest_impl_with__request.ratelimiter.acquire.asset_called_once_with(compiled_route) @pytest.mark.asyncio async def test__request_sets_Authentication_if_token(self, compiled_route, exit_error, rest_impl_with__request): - rest_impl = await rest_impl_with__request - rest_impl.logger.debug.side_effect = [None, exit_error] - - stack = contextlib.ExitStack() - mock_request = stack.enter_context(mock.patch.object(aiohttp.ClientSession, "request")) - stack.enter_context(mock.patch("asyncio.gather", return_value=_helpers.AwaitableMock())) + rest_impl_with__request.logger.debug.side_effect = [None, exit_error] - with stack: + with mock.patch("asyncio.gather", return_value=_helpers.AwaitableMock()): try: - await rest_impl._request(compiled_route) + await rest_impl_with__request._request(compiled_route) except exit_error: pass - mock_request.assert_called_with( + rest_impl_with__request.client_session.request.assert_called_with( "get", "https://discordapp.com/api/v6/somewhere", headers={"X-RateLimit-Precision": "millisecond", "Authorization": "Bot token"}, @@ -266,20 +274,15 @@ async def test__request_sets_Authentication_if_token(self, compiled_route, exit_ async def test__request_doesnt_set_Authentication_if_suppress_authorization_header( self, compiled_route, exit_error, rest_impl_with__request ): - rest_impl = await rest_impl_with__request - rest_impl.logger.debug.side_effect = [None, exit_error] - - stack = contextlib.ExitStack() - stack.enter_context(mock.patch("asyncio.gather", return_value=_helpers.AwaitableMock())) - mock_request = stack.enter_context(mock.patch.object(aiohttp.ClientSession, "request")) + rest_impl_with__request.logger.debug.side_effect = [None, exit_error] - with stack: + with mock.patch("asyncio.gather", return_value=_helpers.AwaitableMock()): try: - await rest_impl._request(compiled_route, suppress_authorization_header=True) + await rest_impl_with__request._request(compiled_route, suppress_authorization_header=True) except exit_error: pass - mock_request.assert_called_with( + rest_impl_with__request.client_session.request.assert_called_with( "get", "https://discordapp.com/api/v6/somewhere", headers={"X-RateLimit-Precision": "millisecond"}, @@ -299,20 +302,15 @@ async def test__request_doesnt_set_Authentication_if_suppress_authorization_head async def test__request_sets_X_Audit_Log_Reason_if_reason( self, compiled_route, exit_error, rest_impl_with__request ): - rest_impl = await rest_impl_with__request - rest_impl.logger.debug.side_effect = [None, exit_error] - - stack = contextlib.ExitStack() - stack.enter_context(mock.patch("asyncio.gather", return_value=_helpers.AwaitableMock())) - mock_request = stack.enter_context(mock.patch.object(aiohttp.ClientSession, "request")) + rest_impl_with__request.logger.debug.side_effect = [None, exit_error] - with stack: + with mock.patch("asyncio.gather", return_value=_helpers.AwaitableMock()): try: - await rest_impl._request(compiled_route, reason="test reason") + await rest_impl_with__request._request(compiled_route, reason="test reason") except exit_error: pass - mock_request.assert_called_with( + rest_impl_with__request.client_session.request.assert_called_with( "get", "https://discordapp.com/api/v6/somewhere", headers={ @@ -336,22 +334,17 @@ async def test__request_sets_X_Audit_Log_Reason_if_reason( async def test__request_updates_headers_with_provided_headers( self, compiled_route, exit_error, rest_impl_with__request ): - rest_impl = await rest_impl_with__request - rest_impl.logger.debug.side_effect = [None, exit_error] + rest_impl_with__request.logger.debug.side_effect = [None, exit_error] - stack = contextlib.ExitStack() - stack.enter_context(mock.patch("asyncio.gather", return_value=_helpers.AwaitableMock())) - mock_request = stack.enter_context(mock.patch.object(aiohttp.ClientSession, "request")) - - with stack: + with mock.patch("asyncio.gather", return_value=_helpers.AwaitableMock()): try: - await rest_impl._request( + await rest_impl_with__request._request( compiled_route, headers={"X-RateLimit-Precision": "nanosecond", "Authorization": "Bearer token"} ) except exit_error: pass - mock_request.assert_called_with( + rest_impl_with__request.client_session.request.assert_called_with( "get", "https://discordapp.com/api/v6/somewhere", headers={"X-RateLimit-Precision": "nanosecond", "Authorization": "Bearer token"}, @@ -397,13 +390,12 @@ def read(self): def close(self): ... - rest_impl = await rest_impl_with__request - rest_impl.logger.debug.side_effect = exit_error + rest_impl_with__request.logger.debug.side_effect = exit_error seekable_resources = [SeekableResource(5), SeekableResource(37), SeekableResource(16)] with mock.patch("asyncio.gather", return_value=_helpers.AwaitableMock()): try: - await rest_impl._request(compiled_route, re_seekable_resources=seekable_resources) + await rest_impl_with__request._request(compiled_route, re_seekable_resources=seekable_resources) except exit_error: pass @@ -419,29 +411,24 @@ async def test__request_handles_bad_response_when_content_type_is_plain_or_html( self, content_type, exit_error, compiled_route, discord_response, rest_impl_with__request ): discord_response.headers["Content-Type"] = content_type - rest_impl = await rest_impl_with__request - rest_impl._handle_bad_response = mock.AsyncMock(side_effect=[None, exit_error]) + rest_impl_with__request._handle_bad_response = mock.AsyncMock(side_effect=[None, exit_error]) - rest_impl.client_session.request = mock.MagicMock(return_value=discord_response) + rest_impl_with__request.client_session.request = mock.MagicMock(return_value=discord_response) with mock.patch("asyncio.gather", return_value=_helpers.AwaitableMock()): try: - await rest_impl._request(compiled_route) + await rest_impl_with__request._request(compiled_route) except exit_error: pass - rest_impl._handle_bad_response.assert_called() + rest_impl_with__request._handle_bad_response.assert_called() @pytest.mark.asyncio async def test__request_when_invalid_content_type(self, compiled_route, discord_response, rest_impl_with__request): discord_response.headers["Content-Type"] = "something/invalid" - rest_impl = await rest_impl_with__request + rest_impl_with__request.client_session.request.return_value = discord_response - stack = contextlib.ExitStack() - stack.enter_context(mock.patch("asyncio.gather", return_value=_helpers.AwaitableMock())) - stack.enter_context(mock.patch.object(aiohttp.ClientSession, "request", return_value=discord_response)) - - with stack: - assert await rest_impl._request(compiled_route, json_body={}) is None + with mock.patch("asyncio.gather", return_value=_helpers.AwaitableMock()): + assert await rest_impl_with__request._request(compiled_route, json_body={}) is None @pytest.mark.asyncio async def test__request_when_TOO_MANY_REQUESTS_when_global( @@ -449,20 +436,16 @@ async def test__request_when_TOO_MANY_REQUESTS_when_global( ): discord_response.status = 429 discord_response.raw_body = '{"retry_after": 1, "global": true}' - rest_impl = await rest_impl_with__request - rest_impl.global_ratelimiter.throttle = mock.MagicMock(side_effect=[None, exit_error]) - - stack = contextlib.ExitStack() - stack.enter_context(mock.patch("asyncio.gather", return_value=_helpers.AwaitableMock())) - stack.enter_context(mock.patch.object(aiohttp.ClientSession, "request", return_value=discord_response)) + rest_impl_with__request.global_ratelimiter.throttle = mock.MagicMock(side_effect=[None, exit_error]) + rest_impl_with__request.client_session.request.return_value = discord_response - with stack: + with mock.patch("asyncio.gather", return_value=_helpers.AwaitableMock()): try: - await rest_impl._request(compiled_route) + await rest_impl_with__request._request(compiled_route) except exit_error: pass - rest_impl.global_ratelimiter.throttle.assert_called_with(0.001) + rest_impl_with__request.global_ratelimiter.throttle.assert_called_with(0.001) @pytest.mark.asyncio async def test__request_when_TOO_MANY_REQUESTS_when_not_global( @@ -470,17 +453,16 @@ async def test__request_when_TOO_MANY_REQUESTS_when_not_global( ): discord_response.status = 429 discord_response.raw_body = '{"retry_after": 1, "global": false}' - rest_impl = await rest_impl_with__request - rest_impl.logger.debug.side_effect = [None, exit_error] + rest_impl_with__request.logger.debug.side_effect = [None, exit_error] with mock.patch("asyncio.gather", return_value=_helpers.AwaitableMock()): with mock.patch.object(rest.LowLevelRestfulClient, "_request", return_value=discord_response): try: - await rest_impl._request(compiled_route) + await rest_impl_with__request._request(compiled_route) except exit_error: pass - rest_impl.global_ratelimiter.throttle.assert_not_called() + rest_impl_with__request.global_ratelimiter.throttle.assert_not_called() @pytest.mark.asyncio @pytest.mark.parametrize("api_version", [versions.HTTPAPIVersion.V6, versions.HTTPAPIVersion.V7]) @@ -494,13 +476,16 @@ async def test__request_when_TOO_MANY_REQUESTS_when_not_global( (405, errors.ClientHTTPError), ], ) - @mock.patch.object(ratelimits, "ManualRateLimiter") - @mock.patch.object(ratelimits, "HTTPBucketRateLimiterManager") async def test__request_raises_appropriate_error_for_status_code( - self, *patches, status_code, error, compiled_route, discord_response, api_version + self, status_code, error, compiled_route, discord_response, api_version ): + stack = contextlib.ExitStack() + stack.enter_context(mock.patch.object(ratelimits, "ManualRateLimiter")) + stack.enter_context(mock.patch.object(ratelimits, "HTTPBucketRateLimiterManager")) + stack.enter_context(mock.patch.object(aiohttp, "ClientSession")) discord_response.status = status_code - rest_impl = rest.LowLevelRestfulClient(token="Bot token", version=api_version) + with stack: + rest_impl = rest.LowLevelRestfulClient(token="Bot token", version=api_version) rest_impl.ratelimiter = mock.MagicMock() rest_impl.global_ratelimiter = mock.MagicMock() rest_impl.client_session.request = mock.MagicMock(return_value=discord_response) @@ -515,11 +500,10 @@ async def test__request_raises_appropriate_error_for_status_code( @pytest.mark.asyncio async def test__request_when_NO_CONTENT(self, compiled_route, discord_response, rest_impl_with__request): discord_response.status = 204 - rest_impl = await rest_impl_with__request - rest_impl.client_session.request = mock.MagicMock(return_value=discord_response) + rest_impl_with__request.client_session.request = mock.MagicMock(return_value=discord_response) with mock.patch("asyncio.gather", return_value=_helpers.AwaitableMock()): - assert await rest_impl._request(compiled_route, form_body=aiohttp.FormData()) is None + assert await rest_impl_with__request._request(compiled_route, form_body=aiohttp.FormData()) is None @pytest.mark.asyncio async def test__request_handles_bad_response_when_error_results_in_retry( @@ -527,20 +511,16 @@ async def test__request_handles_bad_response_when_error_results_in_retry( ): discord_response.raw_body = "{}" discord_response.status = 1000 - rest_impl = await rest_impl_with__request - rest_impl._handle_bad_response = mock.AsyncMock(side_effect=[None, exit_error]) + rest_impl_with__request._handle_bad_response = mock.AsyncMock(side_effect=[None, exit_error]) + rest_impl_with__request.client_session.request.return_value = discord_response - stack = contextlib.ExitStack() - stack.enter_context(mock.patch.object(aiohttp.ClientSession, "request", return_value=discord_response)) - stack.enter_context(mock.patch("asyncio.gather", return_value=_helpers.AwaitableMock())) - - with stack: + with mock.patch("asyncio.gather", return_value=_helpers.AwaitableMock()): try: - await rest_impl._request(compiled_route) + await rest_impl_with__request._request(compiled_route) except exit_error: pass - assert rest_impl._handle_bad_response.call_count == 2 + assert rest_impl_with__request._handle_bad_response.call_count == 2 @pytest.mark.asyncio async def test_handle_bad_response(self, rest_impl): diff --git a/tests/hikari/test_audit_logs.py b/tests/hikari/test_audit_logs.py index d319121985..52451cdf00 100644 --- a/tests/hikari/test_audit_logs.py +++ b/tests/hikari/test_audit_logs.py @@ -560,7 +560,7 @@ async def test___anext___when_not_filled_and_resource_is_exhausted(self): @pytest.mark.asyncio async def test___anext___when_not_filled(self): mock_request = mock.AsyncMock( - side_effect=[{"webhooks": [], "users": [], "audit_log_entries": [{"id": "666666"}], "integrations": []},] + side_effect=[{"webhooks": [], "users": [], "audit_log_entries": [{"id": "666666"}], "integrations": []}] ) mock_audit_log_entry = mock.MagicMock(audit_logs.AuditLogEntry, id=666666) iterator = audit_logs.AuditLogIterator( @@ -578,7 +578,9 @@ async def test___anext___when_not_filled(self): @pytest.mark.asyncio async def test___anext___when_not_filled_and_limit_exhausted(self): - mock_request = mock.AsyncMock(side_effect=[]) + mock_request = mock.AsyncMock( + side_effect=[{"webhooks": [], "users": [], "audit_log_entries": [], "integrations": []}] + ) mock_audit_log_entry = mock.MagicMock(audit_logs.AuditLogEntry, id=666666) iterator = audit_logs.AuditLogIterator( guild_id="123123", request=mock_request, before=None, user_id=..., action_type=..., limit=None diff --git a/tests/hikari/test_channels.py b/tests/hikari/test_channels.py index 8fcf8dea52..e8879890ec 100644 --- a/tests/hikari/test_channels.py +++ b/tests/hikari/test_channels.py @@ -85,7 +85,7 @@ def test_guild_text_channel_payload(test_permission_overwrite_payload): "permission_overwrites": [test_permission_overwrite_payload], "rate_limit_per_user": 2, "nsfw": True, - "topic": "¯\_(ツ)_/¯", + "topic": "¯\\_(ツ)_/¯", "last_message_id": "123456", "parent_id": "987", } @@ -157,6 +157,11 @@ def test_deserialize(self, test_permission_overwrite_payload): == permissions.Permission.CREATE_INSTANT_INVITE | permissions.Permission.ADD_REACTIONS ) assert permission_overwrite_obj.deny == permissions.Permission.EMBED_LINKS | permissions.Permission.ATTACH_FILES + + def test_unset(self): + permission_overwrite_obj = channels.PermissionOverwrite( + id=None, type=None, allow=permissions.Permission(65), deny=permissions.Permission(49152) + ) assert permission_overwrite_obj.unset == permissions.Permission(49217) assert isinstance(permission_overwrite_obj.unset, permissions.Permission) @@ -220,7 +225,7 @@ def test_deserialize(self, test_guild_text_channel_payload, test_permission_over assert channel_obj.guild_id == 567 assert channel_obj.position == 6 assert channel_obj.name == "general" - assert channel_obj.topic == "¯\_(ツ)_/¯" + assert channel_obj.topic == "¯\\_(ツ)_/¯" assert channel_obj.is_nsfw is True assert channel_obj.parent_id == 987 assert channel_obj.type == channels.ChannelType.GUILD_TEXT diff --git a/tests/hikari/test_events.py b/tests/hikari/test_events.py index e8cc705247..ce09cce5ab 100644 --- a/tests/hikari/test_events.py +++ b/tests/hikari/test_events.py @@ -16,6 +16,7 @@ # # You should have received a copy of the GNU Lesser General Public License # along ith Hikari. If not, see . +import contextlib import datetime from unittest import mock @@ -136,12 +137,16 @@ def test_read_event_payload(self, test_guild_payload, test_user_payload): def test_deserialize(self, test_read_event_payload, test_guild_payload, test_user_payload): mock_guild = mock.MagicMock(guilds.Guild, id=40404040) mock_user = mock.MagicMock(users.MyUser) - with mock.patch.object(guilds.UnavailableGuild, "deserialize", return_value=mock_guild): - with _helpers.patch_marshal_attr( + stack = contextlib.ExitStack() + stack.enter_context(mock.patch.object(guilds.UnavailableGuild, "deserialize", return_value=mock_guild)) + patched_user_deserialize = stack.enter_context( + _helpers.patch_marshal_attr( events.ReadyEvent, "my_user", deserializer=users.MyUser.deserialize, return_value=mock_user - ) as patched_user_deserialize: - ready_obj = events.ReadyEvent.deserialize(test_read_event_payload) - patched_user_deserialize.assert_called_once_with(test_user_payload) + ) + ) + with stack: + ready_obj = events.ReadyEvent.deserialize(test_read_event_payload) + patched_user_deserialize.assert_called_once_with(test_user_payload) guilds.UnavailableGuild.deserialize.assert_called_once_with(test_guild_payload) assert ready_obj.gateway_version == 69420 assert ready_obj.my_user is mock_user @@ -150,10 +155,10 @@ def test_deserialize(self, test_read_event_payload, test_guild_payload, test_use assert ready_obj._shard_information == (42, 80) @pytest.fixture() - @mock.patch.object(guilds.UnavailableGuild, "deserialize") - @_helpers.patch_marshal_attr(events.ReadyEvent, "my_user", deserializer=users.MyUser.deserialize) - def mock_ready_event_obj(self, *args, test_read_event_payload): - return events.ReadyEvent.deserialize(test_read_event_payload) + def mock_ready_event_obj(self): + return events.ReadyEvent( + gateway_version=None, my_user=None, unavailable_guilds=None, session_id=None, shard_information=(42, 80) + ) def test_shard_id_when_information_set(self, mock_ready_event_obj): assert mock_ready_event_obj.shard_id == 42 @@ -203,17 +208,21 @@ def test_deserialize(self, test_base_channel_payload, test_overwrite_payload, te mock_timestamp = mock.MagicMock(datetime.datetime) mock_user = mock.MagicMock(users.User, id=42) mock_overwrite = mock.MagicMock(channels.PermissionOverwrite, id=64) - with _helpers.patch_marshal_attr( - events.BaseChannelEvent, - "last_pin_timestamp", - deserializer=hikari.internal.conversions.parse_iso_8601_ts, - return_value=mock_timestamp, - ) as patched_timestamp_deserializer: - with mock.patch.object(users.User, "deserialize", return_value=mock_user): - with mock.patch.object(channels.PermissionOverwrite, "deserialize", return_value=mock_overwrite): - base_channel_payload = events.BaseChannelEvent.deserialize(test_base_channel_payload) - channels.PermissionOverwrite.deserialize.assert_called_once_with(test_overwrite_payload) - users.User.deserialize.assert_called_once_with(test_user_payload) + stack = contextlib.ExitStack() + patched_timestamp_deserializer = stack.enter_context( + _helpers.patch_marshal_attr( + events.BaseChannelEvent, + "last_pin_timestamp", + deserializer=hikari.internal.conversions.parse_iso_8601_ts, + return_value=mock_timestamp, + ) + ) + stack.enter_context(mock.patch.object(users.User, "deserialize", return_value=mock_user)) + stack.enter_context(mock.patch.object(channels.PermissionOverwrite, "deserialize", return_value=mock_overwrite)) + with stack: + base_channel_payload = events.BaseChannelEvent.deserialize(test_base_channel_payload) + channels.PermissionOverwrite.deserialize.assert_called_once_with(test_overwrite_payload) + users.User.deserialize.assert_called_once_with(test_user_payload) patched_timestamp_deserializer.assert_called_once_with("2019-05-17T06:26:56.936000+00:00") assert base_channel_payload.type is channels.ChannelType.GUILD_VOICE assert base_channel_payload.guild_id == 69240 @@ -378,17 +387,23 @@ def guild_member_update_payload(self, test_user_payload): def test_deserialize(self, guild_member_update_payload, test_user_payload): mock_user = mock.MagicMock(users.User) mock_premium_since = mock.MagicMock(datetime.datetime) - with _helpers.patch_marshal_attr( - events.GuildMemberUpdateEvent, "user", deserializer=users.User.deserialize, return_value=mock_user - ) as patched_user_deserializer: - with _helpers.patch_marshal_attr( + stack = contextlib.ExitStack() + patched_user_deserializer = stack.enter_context( + _helpers.patch_marshal_attr( + events.GuildMemberUpdateEvent, "user", deserializer=users.User.deserialize, return_value=mock_user + ) + ) + patched_premium_since_deserializer = stack.enter_context( + _helpers.patch_marshal_attr( events.GuildMemberUpdateEvent, "premium_since", deserializer=hikari.internal.conversions.parse_iso_8601_ts, return_value=mock_premium_since, - ) as patched_premium_since_deserializer: - guild_member_update_obj = events.GuildMemberUpdateEvent.deserialize(guild_member_update_payload) - patched_premium_since_deserializer.assert_called_once_with("2019-05-17T06:26:56.936000+00:00") + ) + ) + with stack: + guild_member_update_obj = events.GuildMemberUpdateEvent.deserialize(guild_member_update_payload) + patched_premium_since_deserializer.assert_called_once_with("2019-05-17T06:26:56.936000+00:00") patched_user_deserializer.assert_called_once_with(test_user_payload) assert guild_member_update_obj.guild_id == 292929 assert guild_member_update_obj.role_ids == [213, 412] @@ -470,23 +485,31 @@ def test_deserialize(self, test_invite_create_payload, test_user_payload): mock_inviter = mock.MagicMock(users.User) mock_target = mock.MagicMock(users.User) mock_created_at = mock.MagicMock(datetime.datetime) - with _helpers.patch_marshal_attr( - events.InviteCreateEvent, "inviter", deserializer=users.User.deserialize, return_value=mock_inviter - ) as patched_inviter_deserializer: - with _helpers.patch_marshal_attr( + stack = contextlib.ExitStack() + patched_inviter_deserializer = stack.enter_context( + _helpers.patch_marshal_attr( + events.InviteCreateEvent, "inviter", deserializer=users.User.deserialize, return_value=mock_inviter + ) + ) + patched_target_deserializer = stack.enter_context( + _helpers.patch_marshal_attr( events.InviteCreateEvent, "target_user", deserializer=users.User.deserialize, return_value=mock_target - ) as patched_target_deserializer: - with _helpers.patch_marshal_attr( - events.InviteCreateEvent, - "created_at", - deserializer=hikari.internal.conversions.parse_iso_8601_ts, - return_value=mock_created_at, - ) as patched_created_at_deserializer: - invite_create_obj = events.InviteCreateEvent.deserialize(test_invite_create_payload) - patched_created_at_deserializer.assert_called_once_with("2019-05-17T06:26:56.936000+00:00") - patched_target_deserializer.assert_called_once_with( - {"id": "420", "username": "blah", "discriminator": "4242", "avatar": "ha"} - ) + ) + ) + patched_created_at_deserializer = stack.enter_context( + _helpers.patch_marshal_attr( + events.InviteCreateEvent, + "created_at", + deserializer=hikari.internal.conversions.parse_iso_8601_ts, + return_value=mock_created_at, + ) + ) + with stack: + invite_create_obj = events.InviteCreateEvent.deserialize(test_invite_create_payload) + patched_created_at_deserializer.assert_called_once_with("2019-05-17T06:26:56.936000+00:00") + patched_target_deserializer.assert_called_once_with( + {"id": "420", "username": "blah", "discriminator": "4242", "avatar": "ha"} + ) patched_inviter_deserializer.assert_called_once_with(test_user_payload) assert invite_create_obj.channel_id == 939393 assert invite_create_obj.code == "owouwuowouwu" @@ -617,66 +640,74 @@ def test_deserialize( mock_activity = mock.MagicMock(messages.MessageActivity) mock_application = mock.MagicMock(oauth2.Application) mock_reference = mock.MagicMock(messages.MessageCrosspost) - with _helpers.patch_marshal_attr( - events.MessageUpdateEvent, "author", deserializer=users.User.deserialize, return_value=mock_author - ) as patched_author_deserializer: - with _helpers.patch_marshal_attr( + stack = contextlib.ExitStack() + patched_author_deserializer = stack.enter_context( + _helpers.patch_marshal_attr( + events.MessageUpdateEvent, "author", deserializer=users.User.deserialize, return_value=mock_author + ) + ) + patched_member_deserializer = stack.enter_context( + _helpers.patch_marshal_attr( events.MessageUpdateEvent, "member", deserializer=guilds.GuildMember.deserialize, return_value=mock_member, - ) as patched_member_deserializer: - with _helpers.patch_marshal_attr( - events.MessageUpdateEvent, - "timestamp", - deserializer=hikari.internal.conversions.parse_iso_8601_ts, - return_value=mock_timestamp, - ) as patched_timestamp_deserializer: - with _helpers.patch_marshal_attr( - events.MessageUpdateEvent, - "edited_timestamp", - deserializer=hikari.internal.conversions.parse_iso_8601_ts, - return_value=mock_edited_timestamp, - ) as patched_edit_deserializer: - with _helpers.patch_marshal_attr( - events.MessageUpdateEvent, - "activity", - deserializer=messages.MessageActivity.deserialize, - return_value=mock_activity, - ) as patched_activity_deserializer: - with _helpers.patch_marshal_attr( - events.MessageUpdateEvent, - "application", - deserializer=oauth2.Application.deserialize, - return_value=mock_application, - ) as patched_application_deserializer: - with _helpers.patch_marshal_attr( - events.MessageUpdateEvent, - "message_reference", - deserializer=messages.MessageCrosspost.deserialize, - return_value=mock_reference, - ) as patched_reference_deserializer: - with mock.patch.object( - messages.Attachment, "deserialize", return_value=mock_attachment - ): - with mock.patch.object(embeds.Embed, "deserialize", return_value=mock_embed): - with mock.patch.object( - messages.Reaction, "deserialize", return_value=mock_reaction - ): - message_update_payload = events.MessageUpdateEvent.deserialize( - test_message_update_payload - ) - messages.Reaction.deserialize.assert_called_once_with( - test_reaction_payload - ) - embeds.Embed.deserialize.assert_called_once_with(test_embed_payload) - messages.Attachment.deserialize.assert_called_once_with(test_attachment_payload) - patched_reference_deserializer.assert_called_once_with(test_reference_payload) - patched_application_deserializer.assert_called_once_with(test_application_payload) - patched_activity_deserializer.assert_called_once_with(test_activity_payload) - patched_edit_deserializer.assert_called_once_with("2019-05-17T06:58:56.936000+00:00") - patched_timestamp_deserializer.assert_called_once_with("2019-05-17T06:26:56.936000+00:00") - patched_member_deserializer.assert_called_once_with(test_member_payload) + ) + ) + patched_timestamp_deserializer = stack.enter_context( + _helpers.patch_marshal_attr( + events.MessageUpdateEvent, + "timestamp", + deserializer=hikari.internal.conversions.parse_iso_8601_ts, + return_value=mock_timestamp, + ) + ) + patched_edit_deserializer = stack.enter_context( + _helpers.patch_marshal_attr( + events.MessageUpdateEvent, + "edited_timestamp", + deserializer=hikari.internal.conversions.parse_iso_8601_ts, + return_value=mock_edited_timestamp, + ) + ) + patched_activity_deserializer = stack.enter_context( + _helpers.patch_marshal_attr( + events.MessageUpdateEvent, + "activity", + deserializer=messages.MessageActivity.deserialize, + return_value=mock_activity, + ) + ) + patched_application_deserializer = stack.enter_context( + _helpers.patch_marshal_attr( + events.MessageUpdateEvent, + "application", + deserializer=oauth2.Application.deserialize, + return_value=mock_application, + ) + ) + patched_reference_deserializer = stack.enter_context( + _helpers.patch_marshal_attr( + events.MessageUpdateEvent, + "message_reference", + deserializer=messages.MessageCrosspost.deserialize, + return_value=mock_reference, + ) + ) + stack.enter_context(mock.patch.object(messages.Attachment, "deserialize", return_value=mock_attachment)) + stack.enter_context(mock.patch.object(embeds.Embed, "deserialize", return_value=mock_embed)) + stack.enter_context(mock.patch.object(messages.Reaction, "deserialize", return_value=mock_reaction)) + with stack: + message_update_payload = events.MessageUpdateEvent.deserialize(test_message_update_payload) + messages.Reaction.deserialize.assert_called_once_with(test_reaction_payload) + embeds.Embed.deserialize.assert_called_once_with(test_embed_payload) + messages.Attachment.deserialize.assert_called_once_with(test_attachment_payload) + patched_reference_deserializer.assert_called_once_with(test_reference_payload) + patched_application_deserializer.assert_called_once_with(test_application_payload) + patched_activity_deserializer.assert_called_once_with(test_activity_payload) + patched_edit_deserializer.assert_called_once_with("2019-05-17T06:58:56.936000+00:00") + patched_timestamp_deserializer.assert_called_once_with("2019-05-17T06:26:56.936000+00:00") + patched_member_deserializer.assert_called_once_with(test_member_payload) patched_author_deserializer.assert_called_once_with(test_user_payload) assert message_update_payload.channel_id == 93939393939 assert message_update_payload.guild_id == 66557744883399 @@ -750,20 +781,26 @@ def test_message_reaction_add_payload(self, test_member_payload, test_emoji_payl def test_deserialize(self, test_message_reaction_add_payload, test_member_payload, test_emoji_payload): mock_member = mock.MagicMock(guilds.GuildMember) mock_emoji = mock.MagicMock(emojis.UnknownEmoji) - with _helpers.patch_marshal_attr( - events.MessageReactionAddEvent, - "member", - deserializer=guilds.GuildMember.deserialize, - return_value=mock_member, - ) as patched_member_deserializer: - with _helpers.patch_marshal_attr( + stack = contextlib.ExitStack() + patched_member_deserializer = stack.enter_context( + _helpers.patch_marshal_attr( + events.MessageReactionAddEvent, + "member", + deserializer=guilds.GuildMember.deserialize, + return_value=mock_member, + ) + ) + patched_emoji_deserializer = stack.enter_context( + _helpers.patch_marshal_attr( events.MessageReactionAddEvent, "emoji", deserializer=emojis.deserialize_reaction_emoji, return_value=mock_emoji, - ) as patched_emoji_deserializer: - message_reaction_add_obj = events.MessageReactionAddEvent.deserialize(test_message_reaction_add_payload) - patched_emoji_deserializer.assert_called_once_with(test_emoji_payload) + ) + ) + with stack: + message_reaction_add_obj = events.MessageReactionAddEvent.deserialize(test_message_reaction_add_payload) + patched_emoji_deserializer.assert_called_once_with(test_emoji_payload) patched_member_deserializer.assert_called_once_with(test_member_payload) assert message_reaction_add_obj.user_id == 9494949 assert message_reaction_add_obj.channel_id == 4393939 @@ -859,12 +896,18 @@ def test_typing_start_event_payload(self, test_member_payload): def test_deserialize(self, test_typing_start_event_payload, test_member_payload): mock_member = mock.MagicMock(guilds.GuildMember) mock_datetime = mock.MagicMock(datetime.datetime) - with _helpers.patch_marshal_attr( - events.TypingStartEvent, "member", deserializer=guilds.GuildMember.deserialize, return_value=mock_member - ) as mock_member_deserialize: - with mock.patch.object(datetime, "datetime", fromtimestamp=mock.MagicMock(return_value=mock_datetime)): - typing_start_event_obj = events.TypingStartEvent.deserialize(test_typing_start_event_payload) - datetime.datetime.fromtimestamp.assert_called_once_with(1231231231, datetime.timezone.utc) + stack = contextlib.ExitStack() + mock_member_deserialize = stack.enter_context( + _helpers.patch_marshal_attr( + events.TypingStartEvent, "member", deserializer=guilds.GuildMember.deserialize, return_value=mock_member + ) + ) + stack.enter_context( + mock.patch.object(datetime, "datetime", fromtimestamp=mock.MagicMock(return_value=mock_datetime)) + ) + with stack: + typing_start_event_obj = events.TypingStartEvent.deserialize(test_typing_start_event_payload) + datetime.datetime.fromtimestamp.assert_called_once_with(1231231231, datetime.timezone.utc) mock_member_deserialize.assert_called_once_with(test_member_payload) assert typing_start_event_obj.channel_id == 123123123 assert typing_start_event_obj.guild_id == 33333333 diff --git a/tests/hikari/test_guilds.py b/tests/hikari/test_guilds.py index f809ebc7c7..15cc3a4767 100644 --- a/tests/hikari/test_guilds.py +++ b/tests/hikari/test_guilds.py @@ -16,6 +16,7 @@ # # You should have received a copy of the GNU Lesser General Public License # along ith Hikari. If not, see . +import contextlib import datetime from unittest import mock @@ -270,24 +271,32 @@ def test_deserialize(self, test_member_payload, test_user_payload): mock_user = mock.create_autospec(users.User) mock_datetime_1 = mock.create_autospec(datetime.datetime) mock_datetime_2 = mock.create_autospec(datetime.datetime) - with _helpers.patch_marshal_attr( - guilds.GuildMember, "user", deserializer=users.User.deserialize, return_value=mock_user - ) as patched_user_deserializer: - with _helpers.patch_marshal_attr( + stack = contextlib.ExitStack() + patched_user_deserializer = stack.enter_context( + _helpers.patch_marshal_attr( + guilds.GuildMember, "user", deserializer=users.User.deserialize, return_value=mock_user + ) + ) + patched_joined_at_deserializer = stack.enter_context( + _helpers.patch_marshal_attr( guilds.GuildMember, "joined_at", deserializer=hikari.internal.conversions.parse_iso_8601_ts, return_value=mock_datetime_1, - ) as patched_joined_at_deserializer: - with _helpers.patch_marshal_attr( - guilds.GuildMember, - "premium_since", - deserializer=hikari.internal.conversions.parse_iso_8601_ts, - return_value=mock_datetime_2, - ) as patched_premium_since_deserializer: - guild_member_obj = guilds.GuildMember.deserialize(test_member_payload) - patched_premium_since_deserializer.assert_called_once_with("2019-05-17T06:26:56.936000+00:00") - patched_joined_at_deserializer.assert_called_once_with("2015-04-26T06:26:56.936000+00:00") + ) + ) + patched_premium_since_deserializer = stack.enter_context( + _helpers.patch_marshal_attr( + guilds.GuildMember, + "premium_since", + deserializer=hikari.internal.conversions.parse_iso_8601_ts, + return_value=mock_datetime_2, + ) + ) + with stack: + guild_member_obj = guilds.GuildMember.deserialize(test_member_payload) + patched_premium_since_deserializer.assert_called_once_with("2019-05-17T06:26:56.936000+00:00") + patched_joined_at_deserializer.assert_called_once_with("2015-04-26T06:26:56.936000+00:00") patched_user_deserializer.assert_called_once_with(test_user_payload) assert guild_member_obj.user is mock_user assert guild_member_obj.nickname == "foobarbaz" @@ -339,20 +348,26 @@ class TestActivityTimestamps: def test_deserialize(self, test_activity_timestamps_payload): mock_start_date = mock.create_autospec(datetime.datetime) mock_end_date = mock.create_autospec(datetime.datetime) - with _helpers.patch_marshal_attr( - guilds.ActivityTimestamps, - "start", - deserializer=hikari.internal.conversions.unix_epoch_to_datetime, - return_value=mock_start_date, - ) as patched_start_deserializer: - with _helpers.patch_marshal_attr( + stack = contextlib.ExitStack() + patched_start_deserializer = stack.enter_context( + _helpers.patch_marshal_attr( + guilds.ActivityTimestamps, + "start", + deserializer=hikari.internal.conversions.unix_epoch_to_datetime, + return_value=mock_start_date, + ) + ) + patched_end_deserializer = stack.enter_context( + _helpers.patch_marshal_attr( guilds.ActivityTimestamps, "end", deserializer=hikari.internal.conversions.unix_epoch_to_datetime, return_value=mock_end_date, - ) as patched_end_deserializer: - activity_timestamps_obj = guilds.ActivityTimestamps.deserialize(test_activity_timestamps_payload) - patched_end_deserializer.assert_called_once_with(1999999792798) + ) + ) + with stack: + activity_timestamps_obj = guilds.ActivityTimestamps.deserialize(test_activity_timestamps_payload) + patched_end_deserializer.assert_called_once_with(1999999792798) patched_start_deserializer.assert_called_once_with(1584996792798) assert activity_timestamps_obj.start is mock_start_date assert activity_timestamps_obj.end is mock_end_date @@ -411,20 +426,26 @@ def test_deserialize( ): mock_created_at = mock.create_autospec(datetime.datetime) mock_emoji = mock.create_autospec(emojis.UnknownEmoji) - with _helpers.patch_marshal_attr( - guilds.PresenceActivity, - "created_at", - deserializer=hikari.internal.conversions.unix_epoch_to_datetime, - return_value=mock_created_at, - ) as patched_created_at_deserializer: - with _helpers.patch_marshal_attr( + stack = contextlib.ExitStack() + patched_created_at_deserializer = stack.enter_context( + _helpers.patch_marshal_attr( + guilds.PresenceActivity, + "created_at", + deserializer=hikari.internal.conversions.unix_epoch_to_datetime, + return_value=mock_created_at, + ) + ) + patched_emoji_deserializer = stack.enter_context( + _helpers.patch_marshal_attr( guilds.PresenceActivity, "emoji", deserializer=emojis.deserialize_reaction_emoji, return_value=mock_emoji, - ) as patched_emoji_deserializer: - presence_activity_obj = guilds.PresenceActivity.deserialize(test_presence_activity_payload) - patched_emoji_deserializer.assert_called_once_with(test_emoji_payload) + ) + ) + with stack: + presence_activity_obj = guilds.PresenceActivity.deserialize(test_presence_activity_payload) + patched_emoji_deserializer.assert_called_once_with(test_emoji_payload) patched_created_at_deserializer.assert_called_once_with(1584996792798) assert presence_activity_obj.name == "an activity" assert presence_activity_obj.type is guilds.ActivityType.STREAMING @@ -580,17 +601,23 @@ def test_guild_integration_payload(self, test_user_payload, test_partial_guild_i def test_deserialize(self, test_guild_integration_payload, test_user_payload, test_integration_account_payload): mock_user = mock.create_autospec(users.User) mock_sync_date = mock.create_autospec(datetime.datetime) - with _helpers.patch_marshal_attr( - guilds.GuildIntegration, - "last_synced_at", - deserializer=hikari.internal.conversions.parse_iso_8601_ts, - return_value=mock_sync_date, - ) as patched_sync_at_deserializer: - with _helpers.patch_marshal_attr( + stack = contextlib.ExitStack() + patched_sync_at_deserializer = stack.enter_context( + _helpers.patch_marshal_attr( + guilds.GuildIntegration, + "last_synced_at", + deserializer=hikari.internal.conversions.parse_iso_8601_ts, + return_value=mock_sync_date, + ) + ) + patched_user_deserializer = stack.enter_context( + _helpers.patch_marshal_attr( guilds.GuildIntegration, "user", deserializer=users.User.deserialize, return_value=mock_user - ) as patched_user_deserializer: - guild_integration_obj = guilds.GuildIntegration.deserialize(test_guild_integration_payload) - patched_user_deserializer.assert_called_once_with(test_user_payload) + ) + ) + with stack: + guild_integration_obj = guilds.GuildIntegration.deserialize(test_guild_integration_payload) + patched_user_deserializer.assert_called_once_with(test_user_payload) patched_sync_at_deserializer.assert_called_once_with("2015-04-26T06:26:56.936000+00:00") assert guild_integration_obj.is_enabled is True @@ -614,16 +641,19 @@ def test_deserialize_when_unavailable_is_undefined(self): class TestPartialGuild: - @pytest.fixture() - def partial_guild_obj(self, test_partial_guild_payload): - return guilds.PartialGuild.deserialize(test_partial_guild_payload) - - def test_deserialize(self, partial_guild_obj): + def test_deserialize(self, test_partial_guild_payload): + partial_guild_obj = guilds.PartialGuild.deserialize(test_partial_guild_payload) assert partial_guild_obj.id == 152559372126519269 assert partial_guild_obj.name == "Isopropyl" assert partial_guild_obj.icon_hash == "d4a983885dsaa7691ce8bcaaf945a" assert partial_guild_obj.features == {guilds.GuildFeature.DISCOVERABLE} + @pytest.fixture() + def partial_guild_obj(self, test_partial_guild_payload): + return guilds.PartialGuild( + id=152559372126519269, icon_hash="d4a983885dsaa7691ce8bcaaf945a", name=None, features=None, + ) + def test_format_icon_url(self, partial_guild_obj): mock_url = "https://cdn.discordapp.com/icons/152559372126519269/d4a983885dsaa7691ce8bcaaf945a.png?size=20" with mock.patch.object(urls, "generate_cdn_url", return_value=mock_url): @@ -681,20 +711,26 @@ def test_deserialize( mock_emoji = mock.create_autospec(emojis.GuildEmoji, id=42) mock_user = mock.create_autospec(users.User, id=84) mock_guild_channel = mock.create_autospec(channels.GuildChannel, id=6969) - with mock.patch.object(emojis.GuildEmoji, "deserialize", return_value=mock_emoji): - with _helpers.patch_marshal_attr( + stack = contextlib.ExitStack() + stack.enter_context(mock.patch.object(emojis.GuildEmoji, "deserialize", return_value=mock_emoji)) + patched_user_deserializer = stack.enter_context( + _helpers.patch_marshal_attr( guilds.GuildMemberPresence, "user", deserializer=guilds.PresenceUser.deserialize, return_value=mock_user - ) as patched_user_deserializer: - with _helpers.patch_marshal_attr( - guilds.GuildMember, "user", deserializer=users.User.deserialize, return_value=mock_user - ) as patched_member_user_deserializer: - with mock.patch.object(channels, "deserialize_channel", return_value=mock_guild_channel): - guild_obj = guilds.Guild.deserialize(test_guild_payload) - channels.deserialize_channel.assert_called_once_with(test_channel_payload) - patched_member_user_deserializer.assert_called_once_with(test_member_payload["user"]) - assert guild_obj.members == {84: guilds.GuildMember.deserialize(test_member_payload)} - patched_user_deserializer.assert_called_once_with(test_member_payload["user"]) - assert guild_obj.presences == {84: guilds.GuildMemberPresence.deserialize(test_guild_member_presence)} + ) + ) + patched_member_user_deserializer = stack.enter_context( + _helpers.patch_marshal_attr( + guilds.GuildMember, "user", deserializer=users.User.deserialize, return_value=mock_user + ) + ) + stack.enter_context(mock.patch.object(channels, "deserialize_channel", return_value=mock_guild_channel)) + with stack: + guild_obj = guilds.Guild.deserialize(test_guild_payload) + channels.deserialize_channel.assert_called_once_with(test_channel_payload) + patched_member_user_deserializer.assert_called_once_with(test_member_payload["user"]) + assert guild_obj.members == {84: guilds.GuildMember.deserialize(test_member_payload)} + patched_user_deserializer.assert_called_once_with(test_member_payload["user"]) + assert guild_obj.presences == {84: guilds.GuildMemberPresence.deserialize(test_guild_member_presence)} emojis.GuildEmoji.deserialize.assert_called_once_with(test_emoji_payload) assert guild_obj.splash_hash == "0ff0ff0ff" assert guild_obj.discovery_splash_hash == "famfamFAMFAMfam" @@ -737,9 +773,50 @@ def test_deserialize( assert guild_obj.public_updates_channel_id == 33333333 @pytest.fixture() - @mock.patch.object(emojis.GuildEmoji, "deserialize") - def test_guild_obj(self, *patched_objs, test_guild_payload): - return guilds.Guild.deserialize(test_guild_payload) + def test_guild_obj(self): + return guilds.Guild( + id=265828729970753537, + icon_hash=None, + name=None, + features=None, + splash_hash="0ff0ff0ff", + banner_hash="1a2b3c", + discovery_splash_hash="famfamFAMFAMfam", + owner_id=None, + my_permissions=None, + region=None, + afk_channel_id=None, + afk_timeout=None, + is_embed_enabled=None, + embed_channel_id=None, + verification_level=None, + default_message_notifications=None, + explicit_content_filter=None, + roles=None, + emojis=None, + mfa_level=None, + application_id=None, + is_unavailable=None, + is_widget_enabled=None, + widget_channel_id=None, + system_channel_id=None, + system_channel_flags=None, + rules_channel_id=None, + joined_at=None, + is_large=None, + member_count=None, + members=None, + channels=None, + presences=None, + max_presences=None, + max_members=None, + vanity_url_code=None, + description=None, + premium_tier=None, + premium_subscription_count=None, + preferred_locale=None, + public_updates_channel_id=None, + ) def test_format_banner_url(self, test_guild_obj): mock_url = "https://not-al" diff --git a/tests/hikari/test_invites.py b/tests/hikari/test_invites.py index a2a64cd66c..3c5c5e8371 100644 --- a/tests/hikari/test_invites.py +++ b/tests/hikari/test_invites.py @@ -16,16 +16,17 @@ # # You should have received a copy of the GNU Lesser General Public License # along ith Hikari. If not, see . +import contextlib import datetime from unittest import mock import pytest -import hikari.internal.conversions from hikari import channels from hikari import guilds from hikari import invites from hikari import users +from hikari.internal import conversions from hikari.internal import urls from tests.hikari import _helpers @@ -87,17 +88,28 @@ def test_invite_with_metadata_payload(test_invite_payload): class TestInviteGuild: - @pytest.fixture() - def invite_guild_obj(self, test_invite_guild_payload): - return invites.InviteGuild.deserialize(test_invite_guild_payload) - - def test_deserialize(self, invite_guild_obj): + def test_deserialize(self, test_invite_guild_payload): + invite_guild_obj = invites.InviteGuild.deserialize(test_invite_guild_payload) assert invite_guild_obj.splash_hash == "aSplashForSure" assert invite_guild_obj.banner_hash == "aBannerForSure" assert invite_guild_obj.description == "Describe me cute kitty." assert invite_guild_obj.verification_level is guilds.GuildVerificationLevel.MEDIUM assert invite_guild_obj.vanity_url_code == "I-am-very-vain" + @pytest.fixture() + def invite_guild_obj(self): + return invites.InviteGuild( + id="56188492224814744", + name=None, + icon_hash=None, + features=None, + splash_hash="aSplashForSure", + banner_hash="aBannerForSure", + description=None, + verification_level=None, + vanity_url_code=None, + ) + def test_format_splash_url(self, invite_guild_obj): mock_url = "https://not-al" with mock.patch.object(urls, "generate_cdn_url", return_value=mock_url): @@ -169,22 +181,32 @@ def test_deserialize( mock_channel = mock.MagicMock(channels.PartialChannel) mock_user_1 = mock.MagicMock(users.User) mock_user_2 = mock.MagicMock(users.User) - with _helpers.patch_marshal_attr( - invites.Invite, "guild", deserializer=invites.InviteGuild.deserialize, return_value=mock_guild - ) as mock_guld_deseralize: - with _helpers.patch_marshal_attr( + stack = contextlib.ExitStack() + mock_guld_deseralize = stack.enter_context( + _helpers.patch_marshal_attr( + invites.Invite, "guild", deserializer=invites.InviteGuild.deserialize, return_value=mock_guild + ) + ) + mock_channel_deseralize = stack.enter_context( + _helpers.patch_marshal_attr( invites.Invite, "channel", deserializer=channels.PartialChannel.deserialize, return_value=mock_channel - ) as mock_channel_deseralize: - with _helpers.patch_marshal_attr( - invites.Invite, "inviter", deserializer=users.User.deserialize, return_value=mock_user_1 - ) as mock_inviter_deseralize: - with _helpers.patch_marshal_attr( - invites.Invite, "target_user", deserializer=users.User.deserialize, return_value=mock_user_2 - ) as mock_target_user_deseralize: - invite_obj = invites.Invite.deserialize(test_invite_payload) - mock_target_user_deseralize.assert_called_once_with(test_2nd_user_payload) - mock_inviter_deseralize.assert_called_once_with(test_user_payload) - mock_channel_deseralize.assert_called_once_with(test_partial_channel) + ) + ) + mock_inviter_deseralize = stack.enter_context( + _helpers.patch_marshal_attr( + invites.Invite, "inviter", deserializer=users.User.deserialize, return_value=mock_user_1 + ) + ) + mock_target_user_deseralize = stack.enter_context( + _helpers.patch_marshal_attr( + invites.Invite, "target_user", deserializer=users.User.deserialize, return_value=mock_user_2 + ) + ) + with stack: + invite_obj = invites.Invite.deserialize(test_invite_payload) + mock_target_user_deseralize.assert_called_once_with(test_2nd_user_payload) + mock_inviter_deseralize.assert_called_once_with(test_user_payload) + mock_channel_deseralize.assert_called_once_with(test_partial_channel) mock_guld_deseralize.assert_called_once_with(test_invite_guild_payload) assert invite_obj.code == "aCode" assert invite_obj.guild is mock_guild @@ -197,30 +219,34 @@ def test_deserialize( class TestInviteWithMetadata: - @pytest.fixture() - @_helpers.patch_marshal_attr(invites.InviteWithMetadata, "guild", deserializer=invites.InviteGuild.deserialize) - @_helpers.patch_marshal_attr( - invites.InviteWithMetadata, "channel", deserializer=channels.PartialChannel.deserialize - ) - @_helpers.patch_marshal_attr(invites.InviteWithMetadata, "inviter", deserializer=users.User.deserialize) - @_helpers.patch_marshal_attr(invites.InviteWithMetadata, "target_user", deserializer=users.User.deserialize) - def mock_invite_with_metadata(self, *args, test_invite_with_metadata_payload): - return invites.InviteWithMetadata.deserialize(test_invite_with_metadata_payload) - - @_helpers.patch_marshal_attr(invites.InviteWithMetadata, "guild", deserializer=invites.InviteGuild.deserialize) - @_helpers.patch_marshal_attr( - invites.InviteWithMetadata, "channel", deserializer=channels.PartialChannel.deserialize - ) - @_helpers.patch_marshal_attr(invites.InviteWithMetadata, "inviter", deserializer=users.User.deserialize) - @_helpers.patch_marshal_attr(invites.InviteWithMetadata, "target_user", deserializer=users.User.deserialize) - def test_deserialize(self, *deserializers, test_invite_with_metadata_payload): + def test_deserialize(self, test_invite_with_metadata_payload): mock_datetime = mock.MagicMock(datetime.datetime) - with _helpers.patch_marshal_attr( - invites.InviteWithMetadata, - "created_at", - deserializer=hikari.internal.conversions.parse_iso_8601_ts, - return_value=mock_datetime, - ) as mock_created_at_deserializer: + stack = contextlib.ExitStack() + stack.enter_context( + _helpers.patch_marshal_attr( + invites.InviteWithMetadata, "guild", deserializer=invites.InviteGuild.deserialize + ) + ) + stack.enter_context( + _helpers.patch_marshal_attr( + invites.InviteWithMetadata, "channel", deserializer=channels.PartialChannel.deserialize + ) + ) + stack.enter_context( + _helpers.patch_marshal_attr(invites.InviteWithMetadata, "inviter", deserializer=users.User.deserialize) + ) + stack.enter_context( + _helpers.patch_marshal_attr(invites.InviteWithMetadata, "target_user", deserializer=users.User.deserialize) + ) + mock_created_at_deserializer = stack.enter_context( + _helpers.patch_marshal_attr( + invites.InviteWithMetadata, + "created_at", + deserializer=conversions.parse_iso_8601_ts, + return_value=mock_datetime, + ) + ) + with stack: invite_with_metadata_obj = invites.InviteWithMetadata.deserialize(test_invite_with_metadata_payload) mock_created_at_deserializer.assert_called_once_with("2015-04-26T06:26:56.936000+00:00") assert invite_with_metadata_obj.uses == 3 @@ -229,6 +255,24 @@ def test_deserialize(self, *deserializers, test_invite_with_metadata_payload): assert invite_with_metadata_obj.is_temporary is True assert invite_with_metadata_obj.created_at is mock_datetime + @pytest.fixture() + def mock_invite_with_metadata(self, test_invite_with_metadata_payload): + return invites.InviteWithMetadata( + code=None, + guild=None, + channel=None, + inviter=None, + target_user=None, + target_user_type=None, + approximate_presence_count=None, + approximate_member_count=None, + uses=None, + max_uses=None, + max_age=datetime.timedelta(seconds=239349393), + is_temporary=None, + created_at=conversions.parse_iso_8601_ts("2015-04-26T06:26:56.936000+00:00"), + ) + def test_max_age_when_zero(self, test_invite_with_metadata_payload): test_invite_with_metadata_payload["max_age"] = 0 assert invites.InviteWithMetadata.deserialize(test_invite_with_metadata_payload).max_age is None @@ -238,6 +282,6 @@ def test_expires_at(self, mock_invite_with_metadata): "2022-11-25 12:23:29.936000+00:00" ) - def test_expires_at_returns_None(self, mock_invite_with_metadata): + def test_expires_at_returns_none(self, mock_invite_with_metadata): mock_invite_with_metadata.max_age = None assert mock_invite_with_metadata.expires_at is None diff --git a/tests/hikari/test_messages.py b/tests/hikari/test_messages.py index 1b942fd5e5..2079a7fa84 100644 --- a/tests/hikari/test_messages.py +++ b/tests/hikari/test_messages.py @@ -16,6 +16,7 @@ # # You should have received a copy of the GNU Lesser General Public License # along ith Hikari. If not, see . +import contextlib import datetime from unittest import mock @@ -188,45 +189,51 @@ def test_deserialize( mock_emoji = mock.MagicMock(messages._emojis) mock_app = mock.MagicMock(oauth2.Application) - with _helpers.patch_marshal_attr( - messages.Message, "author", deserializer=users.User.deserialize, return_value=mock_user - ) as patched_author_deserializer: - with _helpers.patch_marshal_attr( + stack = contextlib.ExitStack() + patched_author_deserializer = stack.enter_context( + _helpers.patch_marshal_attr( + messages.Message, "author", deserializer=users.User.deserialize, return_value=mock_user + ) + ) + patched_member_deserializer = stack.enter_context( + _helpers.patch_marshal_attr( messages.Message, "member", deserializer=guilds.GuildMember.deserialize, return_value=mock_member - ) as patched_member_deserializer: - with _helpers.patch_marshal_attr( - messages.Message, - "timestamp", - deserializer=hikari.internal.conversions.parse_iso_8601_ts, - return_value=mock_datetime, - ) as patched_timestamp_deserializer: - with _helpers.patch_marshal_attr( - messages.Message, - "edited_timestamp", - deserializer=hikari.internal.conversions.parse_iso_8601_ts, - return_value=mock_datetime2, - ) as patched_edited_timestamp_deserializer: - with _helpers.patch_marshal_attr( - messages.Message, - "application", - deserializer=oauth2.Application.deserialize, - return_value=mock_app, - ) as patched_application_deserializer: - with _helpers.patch_marshal_attr( - messages.Reaction, - "emoji", - deserializer=emojis.deserialize_reaction_emoji, - return_value=mock_emoji, - ) as patched_emoji_deserializer: - message_obj = messages.Message.deserialize(test_message_payload) - patched_emoji_deserializer.assert_called_once_with(test_reaction_payload["emoji"]) - assert message_obj.reactions == [messages.Reaction.deserialize(test_reaction_payload)] - patched_application_deserializer.assert_called_once_with(test_application_payload) - patched_edited_timestamp_deserializer.assert_called_once_with( - "2020-04-21T21:20:16.510000+00:00" - ) - patched_timestamp_deserializer.assert_called_once_with("2020-03-21T21:20:16.510000+00:00") - patched_member_deserializer.assert_called_once_with(test_member_payload) + ) + ) + patched_timestamp_deserializer = stack.enter_context( + _helpers.patch_marshal_attr( + messages.Message, + "timestamp", + deserializer=hikari.internal.conversions.parse_iso_8601_ts, + return_value=mock_datetime, + ) + ) + patched_edited_timestamp_deserializer = stack.enter_context( + _helpers.patch_marshal_attr( + messages.Message, + "edited_timestamp", + deserializer=hikari.internal.conversions.parse_iso_8601_ts, + return_value=mock_datetime2, + ) + ) + patched_application_deserializer = stack.enter_context( + _helpers.patch_marshal_attr( + messages.Message, "application", deserializer=oauth2.Application.deserialize, return_value=mock_app, + ) + ) + patched_emoji_deserializer = stack.enter_context( + _helpers.patch_marshal_attr( + messages.Reaction, "emoji", deserializer=emojis.deserialize_reaction_emoji, return_value=mock_emoji, + ) + ) + with stack: + message_obj = messages.Message.deserialize(test_message_payload) + patched_emoji_deserializer.assert_called_once_with(test_reaction_payload["emoji"]) + assert message_obj.reactions == [messages.Reaction.deserialize(test_reaction_payload)] + patched_application_deserializer.assert_called_once_with(test_application_payload) + patched_edited_timestamp_deserializer.assert_called_once_with("2020-04-21T21:20:16.510000+00:00") + patched_timestamp_deserializer.assert_called_once_with("2020-03-21T21:20:16.510000+00:00") + patched_member_deserializer.assert_called_once_with(test_member_payload) patched_author_deserializer.assert_called_once_with(test_user_payload) assert message_obj.id == 123 diff --git a/tests/hikari/test_oauth2.py b/tests/hikari/test_oauth2.py index 51c27a8a95..9eab178a8f 100644 --- a/tests/hikari/test_oauth2.py +++ b/tests/hikari/test_oauth2.py @@ -163,10 +163,6 @@ def test_deserialize(self, member_payload, team_user_payload): class TestTeam: - @pytest.fixture() - def team_obj(self, team_payload): - return oauth2.Team.deserialize(team_payload) - def test_deserialize(self, team_payload, member_payload): mock_member = mock.MagicMock(oauth2.Team, user=mock.MagicMock(id=123)) with mock.patch.object(oauth2.TeamMember, "deserialize", return_value=mock_member): @@ -177,6 +173,10 @@ def test_deserialize(self, team_payload, member_payload): assert team_obj.id == 202020202 assert team_obj.owner_user_id == 393030292 + @pytest.fixture() + def team_obj(self, team_payload): + return oauth2.Team(id=None, icon_hash="3o2o32o", members=None, owner_user_id=None,) + def test_format_icon_url(self): mock_team = _helpers.create_autospec(oauth2.Team, icon_hash="3o2o32o", id=22323) mock_url = "https://cdn.discordapp.com/team-icons/22323/3o2o32o.jpg?size=64" @@ -201,24 +201,10 @@ def test_icon_url(self, team_obj): class TestApplication: - @pytest.fixture() - def application_obj(self, application_information_payload): - return oauth2.Application.deserialize(application_information_payload) - def test_deserialize(self, application_information_payload, team_payload, owner_payload): - mock_team = mock.MagicMock(oauth2.Team) - mock_owner = mock.MagicMock(oauth2.ApplicationOwner) - with _helpers.patch_marshal_attr( - oauth2.Application, "team", deserializer=oauth2.Team.deserialize, return_value=mock_team - ) as patched_team_deserializer: - with _helpers.patch_marshal_attr( - oauth2.Application, "owner", deserializer=oauth2.ApplicationOwner.deserialize, return_value=mock_owner - ) as patched_owner_deserializer: - application_obj = oauth2.Application.deserialize(application_information_payload) - patched_owner_deserializer.assert_called_once_with(owner_payload) - patched_team_deserializer.assert_called_once_with(team_payload) - assert application_obj.team is mock_team - assert application_obj.owner is mock_owner + application_obj = oauth2.Application.deserialize(application_information_payload) + assert application_obj.team == oauth2.Team.deserialize(team_payload) + assert application_obj.owner == oauth2.ApplicationOwner.deserialize(owner_payload) assert application_obj.id == 209333111222 assert application_obj.name == "Dream Sweet in Sea Major" assert application_obj.icon_hash == "iwiwiwiwiw" @@ -233,6 +219,26 @@ def test_deserialize(self, application_information_payload, team_payload, owner_ assert application_obj.slug == "192.168.1.254" assert application_obj.cover_image_hash == "hashmebaby" + @pytest.fixture() + def application_obj(self, application_information_payload): + return oauth2.Application( + team=None, + owner=None, + id=209333111222, + name=None, + icon_hash="iwiwiwiwiw", + description=None, + rpc_origins=None, + is_bot_public=None, + is_bot_code_grant_required=None, + summary=None, + verify_key=None, + guild_id=None, + primary_sku_id=None, + slug=None, + cover_image_hash="hashmebaby", + ) + @pytest.fixture() def mock_application(self): return _helpers.create_autospec(oauth2.Application, id=22222) diff --git a/tests/hikari/test_users.py b/tests/hikari/test_users.py index 971b9ad46d..b045847d11 100644 --- a/tests/hikari/test_users.py +++ b/tests/hikari/test_users.py @@ -31,6 +31,8 @@ def test_user_payload(): "username": "nyaa", "avatar": "b3b24c6d7cbcdec129d5d537067061a8", "discriminator": "6127", + "bot": True, + "system": True, } @@ -51,15 +53,25 @@ def test_oauth_user_payload(): class TestUser: - @pytest.fixture() - def user_obj(self, test_user_payload): - return users.User.deserialize(test_user_payload) - - def test_deserialize(self, user_obj): + def test_deserialize(self, test_user_payload): + user_obj = users.User.deserialize(test_user_payload) assert user_obj.id == 115590097100865541 assert user_obj.username == "nyaa" assert user_obj.avatar_hash == "b3b24c6d7cbcdec129d5d537067061a8" assert user_obj.discriminator == "6127" + assert user_obj.is_bot is True + assert user_obj.is_system is True + + @pytest.fixture() + def user_obj(self, test_user_payload): + return users.User( + id="115590097100865541", + username=None, + avatar_hash="b3b24c6d7cbcdec129d5d537067061a8", + discriminator="6127", + is_bot=None, + is_system=None, + ) def test_avatar_url(self, user_obj): mock_url = "https://cdn.discordapp.com/avatars/115590097100865541" From 50929e089861e7fefa97da9745ddd8f1984f5266 Mon Sep 17 00:00:00 2001 From: davfsa Date: Mon, 13 Apr 2020 10:00:08 +0200 Subject: [PATCH 102/922] Freeze nox and roll back pytest to 5.3.5 --- dev-requirements.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dev-requirements.txt b/dev-requirements.txt index 83a9b093eb..8b155f0f5e 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -1,7 +1,7 @@ async-timeout==3.0.1 coverage==5.0.4 -nox -pytest==5.4.1 +nox==2019.11.9 +pytest==5.3.5 pytest-asyncio==0.10.0 pytest-cov==2.8.1 pytest-html==2.1.1 From 967b7c03c7e77f80e3801c9f4b6774bce7ee3b97 Mon Sep 17 00:00:00 2001 From: davfsa Date: Mon, 13 Apr 2020 10:10:50 +0200 Subject: [PATCH 103/922] Fix TOC and fix documentation warnings --- docs/conf.py | 2 +- docs/index.rst | 6 ------ hikari/clients/rest_clients.py | 18 +++++++++--------- hikari/internal/conversions.py | 7 +++---- hikari/media.py | 2 +- 5 files changed, 14 insertions(+), 21 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index 66567f57fd..3f7203be2e 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -229,4 +229,4 @@ def setup(app): - app.add_stylesheet("style.css") + app.add_css_file("style.css") diff --git a/docs/index.rst b/docs/index.rst index f6c89e05d9..c84ab8adae 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -20,12 +20,6 @@ Hikari is licensed under the GNU LGPLv3 https://www.gnu.org/licenses/lgpl-3.0.en Technical documentation ----------------------- -.. toctree:: - :titlesonly: - - hikari - - .. toctree:: :titlesonly: diff --git a/hikari/clients/rest_clients.py b/hikari/clients/rest_clients.py index 85b05f2d4d..157530a70d 100644 --- a/hikari/clients/rest_clients.py +++ b/hikari/clients/rest_clients.py @@ -827,7 +827,7 @@ async def create_message( and can usually be ignored. tts : :obj:`bool` If specified, whether the message will be sent as a TTS message. - files : :obj:`typing.Collection` [ :obj:`hikari.media.IO` ] + files : :obj:`typing.Collection` [ ``hikari.media.IO`` ] If specified, this should be a list of inclusively between ``1`` and ``5`` IO like media objects, as defined in :mod:`hikari.media`. embed : :obj:`hikari.embeds.Embed` @@ -1664,7 +1664,7 @@ async def create_guild_emoji( The object or ID of the guild to create the emoji in. name : :obj:`str` The new emoji's name. - image_data : :obj:`hikari.internal.conversions.FileLikeT` + image_data : ``hikari.internal.conversions.FileLikeT`` The ``128x128`` image data. roles : :obj:`typing.Sequence` [ :obj:`typing.Union` [ :obj:`hikari.guilds.GuildRole`, :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ] ] If specified, a list of role objects or IDs for which the emoji @@ -1813,7 +1813,7 @@ async def create_guild( If specified, the voice region ID for new guild. You can use :meth:`fetch_guild_voice_regions` to see which region IDs are available. - icon_data : :obj:`hikari.internal.conversions.FileLikeT` + icon_data : ``hikari.internal.conversions.FileLikeT`` If specified, the guild icon image data. verification_level : :obj:`typing.Union` [ :obj:`hikari.guilds.GuildVerificationLevel`, :obj:`int` ] If specified, the verification level. Passing a raw int for this @@ -1928,11 +1928,11 @@ async def update_guild( If specified, the object or ID for the new AFK voice channel. afk_timeout : :obj:`typing.Union` [ :obj:`datetime.timedelta`, :obj:`int` ] If specified, the new AFK timeout seconds timedelta. - icon_data : :obj:`hikari.internal.conversions.FileLikeT` + icon_data : ``hikari.internal.conversions.FileLikeT`` If specified, the new guild icon image file data. owner : :obj:`typing.Union` [ :obj:`hikari.users.User`, :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ] If specified, the object or ID of the new guild owner. - splash_data : :obj:`hikari.internal.conversions.FileLikeT` + splash_data : ``hikari.internal.conversions.FileLikeT`` If specified, the new new splash image file data. system_channel : :obj:`typing.Union` [ :obj:`hikari.channels.GuildVoiceChannel`, :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ] If specified, the object or ID of the new system channel. @@ -3396,7 +3396,7 @@ async def update_me( ---------- username : :obj:`str` If specified, the new username string. - avatar_data : :obj:`hikari.internal.conversions.FileLikeT`, optional + avatar_data : ``hikari.internal.conversions.FileLikeT``, optional If specified, the new avatar image data. If it is :obj:`None`, the avatar is removed. @@ -3610,7 +3610,7 @@ async def create_webhook( The object or ID of the channel for webhook to be created in. name : :obj:`str` The webhook's name string. - avatar_data : :obj:`hikari.internal.conversions.FileLikeT` + avatar_data : ``hikari.internal.conversions.FileLikeT`` If specified, the avatar image data. reason : :obj:`str` If specified, the audit log reason explaining why the operation @@ -3761,7 +3761,7 @@ async def update_webhook( session's provided authorization ``token``). name : :obj:`str` If specified, the new name string. - avatar_data : :obj:`hikari.internal.conversions.FileLikeT`, optional + avatar_data : ``hikari.internal.conversions.FileLikeT``, optional If specified, the new avatar image file object. If :obj:`None`, then it is removed. channel : :obj:`typing.Union` [ :obj:`hikari.channels.GuildChannel`, :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ] @@ -3875,7 +3875,7 @@ async def execute_webhook( wait : :obj:`bool` If specified, whether this request should wait for the webhook to be executed and return the resultant message object. - file : :obj:`hikari.media.IO` + file : ``hikari.media.IO`` If specified, this is a file object to send along with the webhook as defined in :mod:`hikari.media`. embeds : :obj:`typing.Sequence` [ :obj:`hikari.embeds.Embed` ] diff --git a/hikari/internal/conversions.py b/hikari/internal/conversions.py index 9a04519146..193535e46a 100644 --- a/hikari/internal/conversions.py +++ b/hikari/internal/conversions.py @@ -316,15 +316,14 @@ def make_resource_seekable(resource: typing.Any, /) -> Seekable: def get_bytes_from_resource(resource: typing.Any) -> bytes: - """ - Take in any file-like object and return the raw bytes data from it. + """Take in any file-like object and return the raw bytes data from it. - Supports any :obj:`FileLikeT` type that isn't string based. + Supports any ``FileLikeT`` type that isn't string based. Anything else is just returned. Parameters ---------- - resource : :obj:`FileLikeT` + resource : ``FileLikeT`` The resource to get bytes from. Returns diff --git a/hikari/media.py b/hikari/media.py index 4d4eb0e92c..6dceecb966 100644 --- a/hikari/media.py +++ b/hikari/media.py @@ -43,5 +43,5 @@ async def safe_read_file(file: IO) -> typing.Tuple[str, conversions.FileLikeT]: - """Safely read an :obj:`IO` like object.""" + """Safely read an ``IO`` like object.""" raise NotImplementedError # TODO: Nekokatt: update this. From 76a76149f9aa5b5f8fe8d914cad6336d7c7c2f2d Mon Sep 17 00:00:00 2001 From: Faster Speeding Date: Mon, 13 Apr 2020 13:24:51 +0100 Subject: [PATCH 104/922] Implement guild preview entity and route. --- hikari/clients/rest_clients.py | 31 +++++++ hikari/guilds.py | 107 +++++++++++++++++++++++ hikari/net/rest.py | 26 ++++++ hikari/net/routes.py | 1 + tests/hikari/clients/test_rest_client.py | 11 +++ tests/hikari/net/test_rest.py | 10 +++ tests/hikari/test_guilds.py | 103 ++++++++++++++++++++-- tests/hikari/test_invites.py | 8 +- 8 files changed, 287 insertions(+), 10 deletions(-) diff --git a/hikari/clients/rest_clients.py b/hikari/clients/rest_clients.py index 157530a70d..da30b7c0ba 100644 --- a/hikari/clients/rest_clients.py +++ b/hikari/clients/rest_clients.py @@ -1886,6 +1886,37 @@ async def fetch_guild(self, guild: snowflakes.HashableT[guilds.Guild]) -> guilds ) return guilds.Guild.deserialize(payload) + async def fetch_guild_preview(self, guild: snowflakes.HashableT[guilds.Guild]) -> guilds.GuildPreview: + """Get a given guild's object. + + Parameters + ---------- + guild : :obj:`typing.Union` [ :obj:`hikari.guilds.Guild`, :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ] + The object or ID of the guild to get the preview object for. + + Returns + ------- + :obj:`hikari.guilds.GuildPreview` + The requested guild preview object. + + Note + ---- + Unlike other guild endpoints, the bot doesn't have to be in the target + guild to get it's preview. + + Raises + ------ + :obj:`hikari.errors.BadRequestHTTPError` + If any invalid snowflake IDs are passed; a snowflake may be invalid + due to it being outside of the range of UINT64. + :obj:`hikari.errors.NotFoundHTTPError` + If the guild is not found or it isn't ``PUBLIC``. + """ + payload = await self._session.get_guild_preview( + guild_id=str(guild.id if isinstance(guild, snowflakes.UniqueEntity) else int(guild)) + ) + return guilds.GuildPreview.deserialize(payload) + async def update_guild( self, guild: snowflakes.HashableT[guilds.Guild], diff --git a/hikari/guilds.py b/hikari/guilds.py index 5afb3f492f..d40d569ada 100644 --- a/hikari/guilds.py +++ b/hikari/guilds.py @@ -855,6 +855,113 @@ def icon_url(self) -> typing.Optional[str]: return self.format_icon_url() +@marshaller.marshallable() +@attr.s(slots=True) +class GuildPreview(PartialGuild): + """A preview of a guild with the :obj:`GuildFeature.PUBLIC` feature.""" + + #: The hash of the splash for the guild, if there is one. + #: + #: :type: :obj:`str`, optional + splash_hash: typing.Optional[str] = marshaller.attrib(raw_name="splash", deserializer=str, if_none=None) + + #: The hash of the discovery splash for the guild, if there is one. + #: + #: :type: :obj:`str`, optional + discovery_splash_hash: typing.Optional[str] = marshaller.attrib( + raw_name="discovery_splash", deserializer=str, if_none=None + ) + + #: The emojis that this guild provides, represented as a mapping of ID to + #: emoji object. + #: + #: :type: :obj:`typing.Mapping` [ :obj:`hikari.snowflakes.Snowflake`, :obj:`hikari.emojis.GuildEmoji` ] + emojis: typing.Mapping[snowflakes.Snowflake, _emojis.GuildEmoji] = marshaller.attrib( + deserializer=lambda emojis: {e.id: e for e in map(_emojis.GuildEmoji.deserialize, emojis)}, + ) + + #: The approximate amount of presences in this invite's guild, only present + #: when ``with_counts`` is passed as ``True`` to the GET Invites endpoint. + #: + #: :type: :obj:`int`, optional + approximate_presence_count: typing.Optional[int] = marshaller.attrib(deserializer=int) + + #: The approximate amount of members in this invite's guild, only present + #: when ``with_counts`` is passed as ``True`` to the GET Invites endpoint. + #: + #: :type: :obj:`int`, optional + approximate_member_count: typing.Optional[int] = marshaller.attrib(deserializer=int) + + #: The guild's description, if set. + #: + #: :type: :obj:`str`, optional + description: typing.Optional[str] = marshaller.attrib(if_none=None, deserializer=str) + + def format_splash_url(self, fmt: str = "png", size: int = 4096) -> typing.Optional[str]: + """Generate the URL for this guild's splash image, if set. + + Parameters + ---------- + fmt : :obj:`str` + The format to use for this URL, defaults to ``png``. + Supports ``png``, ``jpeg``, ``jpg`` and ``webp``. + size : :obj:`int` + The size to set for the URL, defaults to ``4096``. + Can be any power of two between 16 and 4096. + + Returns + ------- + :obj:`str`, optional + The string URL. + + Raises + ------ + :obj:`ValueError` + If ``size`` is not a power of two or not between 16 and 4096. + """ + if self.splash_hash: + return urls.generate_cdn_url("splashes", str(self.id), self.splash_hash, fmt=fmt, size=size) + return None + + @property + def splash_url(self) -> typing.Optional[str]: + """URL for this guild's splash, if set.""" + return self.format_splash_url() + + def format_discovery_splash_url(self, fmt: str = "png", size: int = 4096) -> typing.Optional[str]: + """Generate the URL for this guild's discovery splash image, if set. + + Parameters + ---------- + fmt : :obj:`str` + The format to use for this URL, defaults to ``png``. + Supports ``png``, ``jpeg``, ``jpg`` and ``webp``. + size : :obj:`int` + The size to set for the URL, defaults to ``4096``. + Can be any power of two between 16 and 4096. + + Returns + ------- + :obj:`str`, optional + The string URL. + + Raises + ------ + :obj:`ValueError` + If ``size`` is not a power of two or not between 16 and 4096. + """ + if self.discovery_splash_hash: + return urls.generate_cdn_url( + "discovery-splashes", str(self.id), self.discovery_splash_hash, fmt=fmt, size=size + ) + return None + + @property + def discovery_splash_url(self) -> typing.Optional[str]: + """URL for this guild's discovery splash, if set.""" + return self.format_discovery_splash_url() + + @marshaller.marshallable() @attr.s(slots=True) class Guild(PartialGuild): diff --git a/hikari/net/rest.py b/hikari/net/rest.py index f4c4381471..6773bba640 100644 --- a/hikari/net/rest.py +++ b/hikari/net/rest.py @@ -1551,6 +1551,32 @@ async def get_guild(self, guild_id: str) -> typing.Dict[str, typing.Any]: route = routes.GUILD.compile(self.GET, guild_id=guild_id) return await self._request(route) + async def get_guild_preview(self, guild_id: str) -> typing.Dict[str, typing.Any]: + """Get a public guild's preview object. + + Parameters + ---------- + guild_id : :obj:`str` + The ID of the guild to get the preview object of. + + Returns + ------- + :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ] + The requested guild preview object. + + Note + ---- + Unlike other guild endpoints, the bot doesn't have to be in the target + guild to get it's preview. + + Raises + ------ + :obj:`hikari.errors.NotFoundHTTPError` + If the guild is not found or it isn't ``PUBLIC``. + """ + route = routes.GUILD_PREVIEW.compile(self.GET, guild_id=guild_id) + return await self._request(route) + # pylint: disable=too-many-locals async def modify_guild( # lgtm [py/similar-function] self, diff --git a/hikari/net/routes.py b/hikari/net/routes.py index 43f39564ce..55bd8e8dcd 100644 --- a/hikari/net/routes.py +++ b/hikari/net/routes.py @@ -224,6 +224,7 @@ def __str__(self) -> str: GUILD_MEMBERS: _RT = RouteTemplate("/guilds/{guild_id}/members") GUILD_MEMBER: _RT = RouteTemplate("/guilds/{guild_id}/members/{user_id}") GUILD_MEMBER_ROLE: _RT = RouteTemplate("/guilds/{guild_id}/members/{user_id}/roles/{role_id}") +GUILD_PREVIEW: _RT = RouteTemplate("/guilds/{guild_id}/preview") GUILD_PRUNE: _RT = RouteTemplate("/guilds/{guild_id}/prune") GUILD_ROLE: _RT = RouteTemplate("/guilds/{guild_id}/roles/{role_id}") GUILD_ROLES: _RT = RouteTemplate("/guilds/{guild_id}/roles") diff --git a/tests/hikari/clients/test_rest_client.py b/tests/hikari/clients/test_rest_client.py index adfc01515e..0604c2ac49 100644 --- a/tests/hikari/clients/test_rest_client.py +++ b/tests/hikari/clients/test_rest_client.py @@ -1486,6 +1486,17 @@ async def test_fetch_guild(self, rest_clients_impl, guild): rest_clients_impl._session.get_guild.assert_called_once_with(guild_id="379953393319542784") guilds.Guild.deserialize.assert_called_once_with(mock_guild_payload) + @pytest.mark.asyncio + @_helpers.parametrize_valid_id_formats_for_models("guild", 379953393319542784, guilds.Guild) + async def test_fetch_guild_preview(self, rest_clients_impl, guild): + mock_guild_preview_payload = {"id": "94949494", "name": "A guild", "emojis": []} + mock_guild_preview_obj = mock.MagicMock(guilds.GuildPreview) + rest_clients_impl._session.get_guild_preview.return_value = mock_guild_preview_payload + with mock.patch.object(guilds.GuildPreview, "deserialize", return_value=mock_guild_preview_obj): + assert await rest_clients_impl.fetch_guild_preview(guild) is mock_guild_preview_obj + rest_clients_impl._session.get_guild_preview.assert_called_once_with(guild_id="379953393319542784") + guilds.GuildPreview.deserialize.assert_called_once_with(mock_guild_preview_payload) + @pytest.mark.asyncio @_helpers.parametrize_valid_id_formats_for_models("guild", 379953393319542784, guilds.Guild) @_helpers.parametrize_valid_id_formats_for_models("afk_channel", 669517187031105607, guilds.Guild) diff --git a/tests/hikari/net/test_rest.py b/tests/hikari/net/test_rest.py index a36b1ca24e..39472abd50 100644 --- a/tests/hikari/net/test_rest.py +++ b/tests/hikari/net/test_rest.py @@ -1171,6 +1171,16 @@ async def test_get_guild(self, rest_impl): routes.GUILD.compile.assert_called_once_with(rest_impl.GET, guild_id="3939393993939") rest_impl._request.assert_called_once_with(mock_route) + @pytest.mark.asyncio + async def test_get_guild_preview(self, rest_impl): + mock_response = {"id": "42", "name": "Hikari"} + rest_impl._request.return_value = mock_response + mock_route = mock.MagicMock(routes.GUILD) + with mock.patch.object(routes, "GUILD_PREVIEW", compile=mock.MagicMock(return_value=mock_route)): + assert await rest_impl.get_guild_preview("3939393993939") is mock_response + routes.GUILD_PREVIEW.compile.assert_called_once_with(rest_impl.GET, guild_id="3939393993939") + rest_impl._request.assert_called_once_with(mock_route) + @pytest.mark.asyncio async def test_modify_guild_without_optionals(self, rest_impl): mock_response = {"id": "42", "name": "Hikari"} diff --git a/tests/hikari/test_guilds.py b/tests/hikari/test_guilds.py index dcce5b3250..002bec9e25 100644 --- a/tests/hikari/test_guilds.py +++ b/tests/hikari/test_guilds.py @@ -200,6 +200,22 @@ def test_partial_guild_payload(): } +@pytest.fixture() +def test_guild_preview_payload(test_emoji_payload): + return { + "id": "152559372126519269", + "name": "Isopropyl", + "icon": "d4a983885dsaa7691ce8bcaaf945a", + "splash": "dsa345tfcdg54b", + "discovery_splash": "lkodwaidi09239uid", + "emojis": [test_emoji_payload], + "features": ["DISCOVERABLE"], + "approximate_member_count": 69, + "approximate_presence_count": 42, + "description": "A DESCRIPTION.", + } + + @pytest.fixture def test_guild_payload( test_emoji_payload, @@ -698,6 +714,81 @@ def test_format_icon_url(self, partial_guild_obj): assert url == mock_url +class TestGuildPreview: + def test_deserialize(self, test_guild_preview_payload, test_emoji_payload): + mock_emoji = mock.MagicMock(emojis.GuildEmoji, id=76767676) + with mock.patch.object(emojis.GuildEmoji, "deserialize", return_value=mock_emoji): + guild_preview_obj = guilds.GuildPreview.deserialize(test_guild_preview_payload) + emojis.GuildEmoji.deserialize.assert_called_once_with(test_emoji_payload) + assert guild_preview_obj.splash_hash == "dsa345tfcdg54b" + assert guild_preview_obj.discovery_splash_hash == "lkodwaidi09239uid" + assert guild_preview_obj.emojis == {76767676: mock_emoji} + assert guild_preview_obj.approximate_presence_count == 42 + assert guild_preview_obj.approximate_member_count == 69 + assert guild_preview_obj.description == "A DESCRIPTION." + + @pytest.fixture() + def test_guild_preview_obj(self): + return guilds.GuildPreview( + id="23123123123", + name=None, + icon_hash=None, + features=None, + splash_hash="dsa345tfcdg54b", + discovery_splash_hash="lkodwaidi09239uid", + emojis=None, + approximate_presence_count=None, + approximate_member_count=None, + description=None, + ) + + def test_format_discovery_splash_url(self, test_guild_preview_obj): + mock_url = "https://not-al" + with mock.patch.object(urls, "generate_cdn_url", return_value=mock_url): + url = test_guild_preview_obj.format_discovery_splash_url(fmt="nyaapeg", size=4000) + urls.generate_cdn_url.assert_called_once_with( + "discovery-splashes", "23123123123", "lkodwaidi09239uid", fmt="nyaapeg", size=4000 + ) + assert url == mock_url + + def test_format_discovery_splash_returns_none(self, test_guild_preview_obj): + test_guild_preview_obj.discovery_splash_hash = None + with mock.patch.object(urls, "generate_cdn_url", return_value=...): + url = test_guild_preview_obj.format_discovery_splash_url() + urls.generate_cdn_url.assert_not_called() + assert url is None + + def test_discover_splash_url(self, test_guild_preview_obj): + mock_url = "https://not-al" + with mock.patch.object(urls, "generate_cdn_url", return_value=mock_url): + url = test_guild_preview_obj.discovery_splash_url + urls.generate_cdn_url.assert_called_once() + assert url == mock_url + + def test_format_splash_url(self, test_guild_preview_obj): + mock_url = "https://not-al" + with mock.patch.object(urls, "generate_cdn_url", return_value=mock_url): + url = test_guild_preview_obj.format_splash_url(fmt="nyaapeg", size=4000) + urls.generate_cdn_url.assert_called_once_with( + "splashes", "23123123123", "dsa345tfcdg54b", fmt="nyaapeg", size=4000 + ) + assert url == mock_url + + def test_format_splash_returns_none(self, test_guild_preview_obj): + test_guild_preview_obj.splash_hash = None + with mock.patch.object(urls, "generate_cdn_url", return_value=...): + url = test_guild_preview_obj.format_splash_url() + urls.generate_cdn_url.assert_not_called() + assert url is None + + def test_splash_url(self, test_guild_preview_obj): + mock_url = "https://not-al" + with mock.patch.object(urls, "generate_cdn_url", return_value=mock_url): + url = test_guild_preview_obj.splash_url + urls.generate_cdn_url.assert_called_once() + assert url == mock_url + + class TestGuild: def test_deserialize( self, @@ -825,7 +916,7 @@ def test_format_banner_url(self, test_guild_obj): urls.generate_cdn_url.assert_called_once_with( "banners", "265828729970753537", "1a2b3c", fmt="nyaapeg", size=4000 ) - assert url is mock_url + assert url == mock_url def test_format_banner_url_returns_none(self, test_guild_obj): test_guild_obj.banner_hash = None @@ -839,7 +930,7 @@ def test_banner_url(self, test_guild_obj): with mock.patch.object(urls, "generate_cdn_url", return_value=mock_url): url = test_guild_obj.banner_url urls.generate_cdn_url.assert_called_once() - assert url is mock_url + assert url == mock_url def test_format_discovery_splash_url(self, test_guild_obj): mock_url = "https://not-al" @@ -848,7 +939,7 @@ def test_format_discovery_splash_url(self, test_guild_obj): urls.generate_cdn_url.assert_called_once_with( "discovery-splashes", "265828729970753537", "famfamFAMFAMfam", fmt="nyaapeg", size=4000 ) - assert url is mock_url + assert url == mock_url def test_format_discovery_splash_returns_none(self, test_guild_obj): test_guild_obj.discovery_splash_hash = None @@ -862,7 +953,7 @@ def test_discover_splash_url(self, test_guild_obj): with mock.patch.object(urls, "generate_cdn_url", return_value=mock_url): url = test_guild_obj.discovery_splash_url urls.generate_cdn_url.assert_called_once() - assert url is mock_url + assert url == mock_url def test_format_splash_url(self, test_guild_obj): mock_url = "https://not-al" @@ -871,7 +962,7 @@ def test_format_splash_url(self, test_guild_obj): urls.generate_cdn_url.assert_called_once_with( "splashes", "265828729970753537", "0ff0ff0ff", fmt="nyaapeg", size=4000 ) - assert url is mock_url + assert url == mock_url def test_format_splash_returns_none(self, test_guild_obj): test_guild_obj.splash_hash = None @@ -885,4 +976,4 @@ def test_splash_url(self, test_guild_obj): with mock.patch.object(urls, "generate_cdn_url", return_value=mock_url): url = test_guild_obj.splash_url urls.generate_cdn_url.assert_called_once() - assert url is mock_url + assert url == mock_url diff --git a/tests/hikari/test_invites.py b/tests/hikari/test_invites.py index 3c5c5e8371..15c2807b3d 100644 --- a/tests/hikari/test_invites.py +++ b/tests/hikari/test_invites.py @@ -117,7 +117,7 @@ def test_format_splash_url(self, invite_guild_obj): urls.generate_cdn_url.assert_called_once_with( "splashes", "56188492224814744", "aSplashForSure", fmt="nyaapeg", size=4000 ) - assert url is mock_url + assert url == mock_url def test_format_splash_url_returns_none(self, invite_guild_obj): invite_guild_obj.splash_hash = None @@ -131,7 +131,7 @@ def test_splash_url(self, invite_guild_obj): with mock.patch.object(urls, "generate_cdn_url", return_value=mock_url): url = invite_guild_obj.splash_url urls.generate_cdn_url.assert_called_once() - assert url is mock_url + assert url == mock_url def test_format_banner_url(self, invite_guild_obj): mock_url = "https://not-al" @@ -140,7 +140,7 @@ def test_format_banner_url(self, invite_guild_obj): urls.generate_cdn_url.assert_called_once_with( "banners", "56188492224814744", "aBannerForSure", fmt="nyaapeg", size=4000 ) - assert url is mock_url + assert url == mock_url def test_format_banner_url_returns_none(self, invite_guild_obj): invite_guild_obj.banner_hash = None @@ -154,7 +154,7 @@ def test_banner_url(self, invite_guild_obj): with mock.patch.object(urls, "generate_cdn_url", return_value=mock_url): url = invite_guild_obj.banner_url urls.generate_cdn_url.assert_called_once() - assert url is mock_url + assert url == mock_url class TestVanityUrl: From bee403d99e04a964231f765d7ef3179c9f1e2278 Mon Sep 17 00:00:00 2001 From: davfsa Date: Mon, 13 Apr 2020 20:21:40 +0200 Subject: [PATCH 105/922] Replace create_autospec with MagicMock --- tests/hikari/_helpers.py | 8 ++---- tests/hikari/clients/test_rest_client.py | 20 ++++++------- tests/hikari/internal/test_marshaller.py | 2 +- .../hikari/internal/test_marshaller_pep563.py | 2 +- tests/hikari/net/test_rest.py | 26 ++++++++--------- tests/hikari/net/test_shard.py | 4 +-- tests/hikari/test_guilds.py | 28 +++++++++---------- tests/hikari/test_oauth2.py | 6 ++-- 8 files changed, 45 insertions(+), 51 deletions(-) diff --git a/tests/hikari/_helpers.py b/tests/hikari/_helpers.py index 24f3691c88..f45f1e2c18 100644 --- a/tests/hikari/_helpers.py +++ b/tests/hikari/_helpers.py @@ -169,7 +169,7 @@ def _can_weakref(spec_set): def mock_model(spec_set: typing.Type[T] = object, hash_code_provider=None, **kwargs) -> T: # Enables type hinting for my own reference, and quick attribute setting. - obj = create_autospec(spec_set) + obj = mock.MagicMock(spec_set) for name, value in kwargs.items(): setattr(obj, name, value) @@ -293,7 +293,7 @@ def __init__(self, *, pattern=r".*", category=Warning): def __enter__(self): self.old_warning = warnings.warn_explicit - self.mocked_warning = create_autospec(warnings.warn) + self.mocked_warning = mock.MagicMock(warnings.warn) self.context = mock.patch("warnings.warn", new=self.mocked_warning) self.context.__enter__() return self @@ -448,10 +448,6 @@ def stupid_windows_please_stop_breaking_my_tests(test): return pytest.mark.skipif(os.name == "nt", reason="This test will not pass on Windows :(")(test) -def create_autospec(spec, *args, **kwargs): - return mock.create_autospec(spec, spec_set=True, *args, **kwargs) - - def patch_marshal_attr(target_entity, field_name, *args, deserializer=None, serializer=None, **kwargs): if not (deserializer or serializer): raise TypeError("patch_marshal_attr() Missing required keyword-only argument: 'deserializer' or 'serializer'") diff --git a/tests/hikari/clients/test_rest_client.py b/tests/hikari/clients/test_rest_client.py index adfc01515e..37c8f4dd5c 100644 --- a/tests/hikari/clients/test_rest_client.py +++ b/tests/hikari/clients/test_rest_client.py @@ -47,8 +47,8 @@ def test__get_member_id(): - member = mock.create_autospec( - guilds.GuildMember, user=mock.create_autospec(users.User, id=123123123, __int__=users.User.__int__) + member = mock.MagicMock( + guilds.GuildMember, user=mock.MagicMock(users.User, id=123123123, __int__=users.User.__int__) ) assert rest_clients._get_member_id(member) == "123123123" @@ -78,7 +78,7 @@ def test_init(self, mock_config): @pytest.fixture() def low_level_rest_impl(self) -> rest.LowLevelRestfulClient: - return mock.create_autospec(rest.LowLevelRestfulClient, auto_spec=True) + return mock.MagicMock(rest.LowLevelRestfulClient) @pytest.fixture() def rest_clients_impl(self, low_level_rest_impl) -> rest_clients.RESTClient: @@ -230,7 +230,7 @@ async def test_update_channel_with_optionals(self, rest_clients_impl, channel, p mock_payload = {"name": "Qts", "type": 2} mock_channel_obj = mock.MagicMock(channels.Channel) mock_overwrite_payload = {"type": "user", "id": 543543543} - mock_overwrite_obj = mock.create_autospec(channels.PermissionOverwrite) + mock_overwrite_obj = mock.MagicMock(channels.PermissionOverwrite) mock_overwrite_obj.serialize = mock.MagicMock(return_value=mock_overwrite_payload) rest_clients_impl._session.modify_channel.return_value = mock_payload with mock.patch.object(channels, "deserialize_channel", return_value=mock_channel_obj): @@ -756,7 +756,7 @@ async def test_create_message_with_optionals(self, rest_clients_impl, channel): mock_allowed_mentions_payload = {"parse": ["everyone", "users", "roles"]} rest_clients_impl._generate_allowed_mentions = mock.MagicMock(return_value=mock_allowed_mentions_payload) mock_embed_payload = {"description": "424242"} - mock_embed_obj = mock.create_autospec(embeds.Embed, auto_spec=True) + mock_embed_obj = mock.MagicMock(embeds.Embed) mock_embed_obj.serialize = mock.MagicMock(return_value=mock_embed_payload) mock_media_obj = mock.MagicMock() mock_media_payload = ("aName.png", mock.MagicMock()) @@ -838,7 +838,7 @@ async def test_safe_create_message_without_optionals(self, rest_clients_impl): @pytest.mark.asyncio async def test_safe_create_message_with_optionals(self, rest_clients_impl): channel = mock.MagicMock(channels.Channel) - mock_embed_obj = mock.create_autospec(embeds.Embed) + mock_embed_obj = mock.MagicMock(embeds.Embed) mock_message_obj = mock.MagicMock(messages.Message) mock_media_obj = mock.MagicMock(bytes) rest_clients_impl.create_message = mock.AsyncMock(return_value=mock_message_obj) @@ -981,7 +981,7 @@ async def test_update_message_with_optionals(self, rest_clients_impl, message, c mock_payload = {"id": "4242", "content": "I HAVE BEEN UPDATED!"} mock_message_obj = mock.MagicMock(messages.Message) mock_embed_payload = {"description": "blahblah"} - mock_embed = mock.create_autospec(embeds.Embed, auto_spec=True) + mock_embed = mock.MagicMock(embeds.Embed) mock_embed.serialize = mock.MagicMock(return_value=mock_embed_payload) mock_allowed_mentions_payload = {"parse": [], "users": ["123"]} rest_clients_impl._generate_allowed_mentions = mock.MagicMock(return_value=mock_allowed_mentions_payload) @@ -1421,10 +1421,10 @@ async def test_create_guild_with_optionals(self, rest_clients_impl, region): mock_image_obj = mock.MagicMock(io.BytesIO) mock_image_data = mock.MagicMock(bytes) mock_role_payload = {"permissions": 123123} - mock_role_obj = mock.create_autospec(guilds.GuildRole, spec_set=True) + mock_role_obj = mock.MagicMock(guilds.GuildRole) mock_role_obj.serialize = mock.MagicMock(return_value=mock_role_payload) mock_channel_payload = {"type": 2, "name": "aChannel"} - mock_channel_obj = mock.create_autospec(channels.GuildChannel, spec_set=True) + mock_channel_obj = mock.MagicMock(channels.GuildNewsChannel) mock_channel_obj.serialize = mock.MagicMock(return_value=mock_channel_payload) stack = contextlib.ExitStack() stack.enter_context(mock.patch.object(guilds.Guild, "deserialize", return_value=mock_guild_obj)) @@ -2642,7 +2642,7 @@ async def test_execute_webhook_with_optionals(self, rest_clients_impl, webhook): mock_allowed_mentions_payload = {"parse": ["everyone", "users", "roles"]} rest_clients_impl._generate_allowed_mentions = mock.MagicMock(return_value=mock_allowed_mentions_payload) mock_embed_payload = {"description": "424242"} - mock_embed_obj = mock.create_autospec(embeds.Embed, auto_spec=True) + mock_embed_obj = mock.MagicMock(embeds.Embed) mock_embed_obj.serialize = mock.MagicMock(return_value=mock_embed_payload) mock_media_obj = mock.MagicMock() mock_media_payload = ("aName.png", mock.MagicMock()) diff --git a/tests/hikari/internal/test_marshaller.py b/tests/hikari/internal/test_marshaller.py index 6a867b7d17..47d2b856e3 100644 --- a/tests/hikari/internal/test_marshaller.py +++ b/tests/hikari/internal/test_marshaller.py @@ -78,7 +78,7 @@ def test_default_validator(data): class TestAttrs: def test_invokes_attrs(self): - marshaller_mock = mock.create_autospec(marshaller.HikariEntityMarshaller, spec_set=True) + marshaller_mock = mock.MagicMock(marshaller.HikariEntityMarshaller) kwargs = {"marshaller": marshaller_mock} diff --git a/tests/hikari/internal/test_marshaller_pep563.py b/tests/hikari/internal/test_marshaller_pep563.py index 4e2d7b04fb..bb60e49161 100644 --- a/tests/hikari/internal/test_marshaller_pep563.py +++ b/tests/hikari/internal/test_marshaller_pep563.py @@ -68,7 +68,7 @@ def test_invokes_attrs(self): class TestAttrsPep563: def test_invokes_attrs(self): - marshaller_mock = mock.create_autospec(marshaller.HikariEntityMarshaller, spec_set=True) + marshaller_mock = mock.MagicMock(marshaller.HikariEntityMarshaller) kwargs = {"marshaller": marshaller_mock} diff --git a/tests/hikari/net/test_rest.py b/tests/hikari/net/test_rest.py index a36b1ca24e..e854199450 100644 --- a/tests/hikari/net/test_rest.py +++ b/tests/hikari/net/test_rest.py @@ -45,14 +45,13 @@ def __init__(self, *args, **kwargs): self.base_url = "https://discordapp.com/api/v6" self.client_session = mock.MagicMock(close=mock.AsyncMock()) self.logger = mock.MagicMock() - self.ratelimiter = mock.create_autospec( + self.ratelimiter = mock.MagicMock( ratelimits.HTTPBucketRateLimiterManager, - auto_spec=True, acquire=mock.MagicMock(), update_rate_limits=mock.MagicMock(), ) - self.global_ratelimiter = mock.create_autospec( - ratelimits.ManualRateLimiter, auto_spec=True, acquire=mock.MagicMock(), throttle=mock.MagicMock() + self.global_ratelimiter = mock.MagicMock( + ratelimits.ManualRateLimiter, acquire=mock.MagicMock(), throttle=mock.MagicMock() ) self._request = mock.AsyncMock(return_value=...) @@ -221,14 +220,13 @@ async def test_close(self, rest_impl): def rest_impl_with__request(self, *args): rest_impl = rest.LowLevelRestfulClient(token="Bot token") rest_impl.logger = mock.MagicMock(debug=mock.MagicMock()) - rest_impl.ratelimiter = mock.create_autospec( + rest_impl.ratelimiter = mock.MagicMock( ratelimits.HTTPBucketRateLimiterManager, - auto_spec=True, acquire=mock.MagicMock(), update_rate_limits=mock.MagicMock(), ) - rest_impl.global_ratelimiter = mock.create_autospec( - ratelimits.ManualRateLimiter, auto_spec=True, acquire=mock.MagicMock(), throttle=mock.MagicMock() + rest_impl.global_ratelimiter = mock.MagicMock( + ratelimits.ManualRateLimiter, acquire=mock.MagicMock(), throttle=mock.MagicMock() ) return rest_impl @@ -524,7 +522,7 @@ async def test__request_handles_bad_response_when_error_results_in_retry( @pytest.mark.asyncio async def test_handle_bad_response(self, rest_impl): - backoff = _helpers.create_autospec(ratelimits.ExponentialBackOff, __next__=mock.MagicMock(return_value=4)) + backoff = mock.MagicMock(ratelimits.ExponentialBackOff, __next__=mock.MagicMock(return_value=4)) mock_route = mock.MagicMock(routes.CompiledRoute) with mock.patch.object(asyncio, "sleep"): await rest_impl._handle_bad_response(backoff, "Being spammy", mock_route, "You are being rate limited", 429) @@ -532,7 +530,7 @@ async def test_handle_bad_response(self, rest_impl): @pytest.mark.asyncio async def test_handle_bad_response_raises_server_http_error_on_timeout(self, rest_impl): - backoff = _helpers.create_autospec( + backoff = mock.MagicMock( ratelimits.ExponentialBackOff, __next__=mock.MagicMock(side_effect=asyncio.TimeoutError()) ) mock_route = mock.MagicMock(routes.CompiledRoute) @@ -704,7 +702,7 @@ async def test_create_message_without_optionals(self, rest_impl): mock_response = {"content": "nyaa, nyaa, nyaa."} rest_impl._request.return_value = mock_response mock_route = mock.MagicMock(routes.CHANNEL_MESSAGE) - mock_form = mock.MagicMock(spec_set=aiohttp.FormData, add_field=mock.MagicMock()) + mock_form = mock.MagicMock(aiohttp.FormData, add_field=mock.MagicMock()) with mock.patch.object(routes, "CHANNEL_MESSAGES", compile=mock.MagicMock(return_value=mock_route)): with mock.patch.object(aiohttp, "FormData", autospec=True, return_value=mock_form): assert await rest_impl.create_message("22222222") is mock_response @@ -726,7 +724,7 @@ async def test_create_message_with_optionals( rest_impl._request.return_value = mock_response mock_route = mock.MagicMock(routes.CHANNEL_MESSAGE) CHANNEL_MESSAGES.compile.return_value = mock_route - mock_form = mock.MagicMock(spec_set=aiohttp.FormData, add_field=mock.MagicMock()) + mock_form = mock.MagicMock(aiohttp.FormData, add_field=mock.MagicMock()) FormData.return_value = mock_form mock_file = mock.MagicMock(io.BytesIO) make_resource_seekable.return_value = mock_file @@ -2065,7 +2063,7 @@ async def test_delete_webhook_with_token(self, rest_impl): @pytest.mark.asyncio async def test_execute_webhook_without_optionals(self, rest_impl): - mock_form = mock.MagicMock(spec_set=aiohttp.FormData, add_field=mock.MagicMock()) + mock_form = mock.MagicMock(aiohttp.FormData, add_field=mock.MagicMock()) mock_route = mock.MagicMock(routes.WEBHOOK_WITH_TOKEN) rest_impl._request.return_value = None mock_json = "{}" @@ -2091,7 +2089,7 @@ async def test_execute_webhook_without_optionals(self, rest_impl): async def test_execute_webhook_with_optionals( self, make_resource_seekable, dumps, WEBHOOK_WITH_TOKEN, FormData, rest_impl ): - mock_form = mock.MagicMock(spec_set=aiohttp.FormData, add_field=mock.MagicMock()) + mock_form = mock.MagicMock(aiohttp.FormData, add_field=mock.MagicMock()) FormData.return_value = mock_form mock_route = mock.MagicMock(routes.WEBHOOK_WITH_TOKEN) WEBHOOK_WITH_TOKEN.compile.return_value = mock_route diff --git a/tests/hikari/net/test_shard.py b/tests/hikari/net/test_shard.py index 3a11c98f33..0eab0878de 100644 --- a/tests/hikari/net/test_shard.py +++ b/tests/hikari/net/test_shard.py @@ -759,8 +759,8 @@ def client(self, event_loop): asyncio.set_event_loop(event_loop) client = _helpers.unslot_class(shard.ShardConnection)(token="1234", url="xxx") client = _helpers.mock_methods_on(client, except_=("close",)) - client.ws = _helpers.create_autospec(aiohttp.ClientWebSocketResponse) - client.session = _helpers.create_autospec(aiohttp.ClientSession) + client.ws = mock.MagicMock(aiohttp.ClientWebSocketResponse) + client.session = mock.MagicMock(aiohttp.ClientSession) client.closed_event = asyncio.Event() client._presence = {} return client diff --git a/tests/hikari/test_guilds.py b/tests/hikari/test_guilds.py index dcce5b3250..1b415f8364 100644 --- a/tests/hikari/test_guilds.py +++ b/tests/hikari/test_guilds.py @@ -268,9 +268,9 @@ def test_deserialize(self, test_guild_embed_payload): class TestGuildMember: def test_deserialize(self, test_member_payload, test_user_payload): - mock_user = mock.create_autospec(users.User) - mock_datetime_1 = mock.create_autospec(datetime.datetime) - mock_datetime_2 = mock.create_autospec(datetime.datetime) + mock_user = mock.MagicMock(users.User) + mock_datetime_1 = mock.MagicMock(datetime.datetime) + mock_datetime_2 = mock.MagicMock(datetime.datetime) stack = contextlib.ExitStack() patched_user_deserializer = stack.enter_context( _helpers.patch_marshal_attr( @@ -346,8 +346,8 @@ def test_deserialize(self, test_guild_role_payload): class TestActivityTimestamps: def test_deserialize(self, test_activity_timestamps_payload): - mock_start_date = mock.create_autospec(datetime.datetime) - mock_end_date = mock.create_autospec(datetime.datetime) + mock_start_date = mock.MagicMock(datetime.datetime) + mock_end_date = mock.MagicMock(datetime.datetime) stack = contextlib.ExitStack() patched_start_deserializer = stack.enter_context( _helpers.patch_marshal_attr( @@ -424,8 +424,8 @@ def test_deserialize( test_emoji_payload, test_activity_timestamps_payload, ): - mock_created_at = mock.create_autospec(datetime.datetime) - mock_emoji = mock.create_autospec(emojis.UnknownEmoji) + mock_created_at = mock.MagicMock(datetime.datetime) + mock_emoji = mock.MagicMock(emojis.UnknownEmoji) stack = contextlib.ExitStack() patched_created_at_deserializer = stack.enter_context( _helpers.patch_marshal_attr( @@ -512,7 +512,7 @@ class TestGuildMemberPresence: def test_deserialize( self, test_guild_member_presence, test_user_payload, test_presence_activity_payload, test_client_status_payload ): - mock_since = mock.create_autospec(datetime.datetime) + mock_since = mock.MagicMock(datetime.datetime) with _helpers.patch_marshal_attr( guilds.GuildMemberPresence, "premium_since", @@ -539,7 +539,7 @@ def test_guild_member_ban_payload(self, test_user_payload): return {"reason": "Get Nyaa'ed", "user": test_user_payload} def test_deserializer(self, test_guild_member_ban_payload, test_user_payload): - mock_user = mock.create_autospec(users.User) + mock_user = mock.MagicMock(users.User) with _helpers.patch_marshal_attr( guilds.GuildMemberBan, "user", deserializer=users.User.deserialize, return_value=mock_user ) as patched_user_deserializer: @@ -599,8 +599,8 @@ def test_guild_integration_payload(self, test_user_payload, test_partial_guild_i } def test_deserialize(self, test_guild_integration_payload, test_user_payload, test_integration_account_payload): - mock_user = mock.create_autospec(users.User) - mock_sync_date = mock.create_autospec(datetime.datetime) + mock_user = mock.MagicMock(users.User) + mock_sync_date = mock.MagicMock(datetime.datetime) stack = contextlib.ExitStack() patched_sync_at_deserializer = stack.enter_context( _helpers.patch_marshal_attr( @@ -708,9 +708,9 @@ def test_deserialize( test_channel_payload, test_guild_member_presence, ): - mock_emoji = mock.create_autospec(emojis.GuildEmoji, id=42) - mock_user = mock.create_autospec(users.User, id=84) - mock_guild_channel = mock.create_autospec(channels.GuildChannel, id=6969) + mock_emoji = mock.MagicMock(emojis.GuildEmoji, id=42) + mock_user = mock.MagicMock(users.User, id=84) + mock_guild_channel = mock.MagicMock(channels.GuildChannel, id=6969) stack = contextlib.ExitStack() stack.enter_context(mock.patch.object(emojis.GuildEmoji, "deserialize", return_value=mock_emoji)) patched_user_deserializer = stack.enter_context( diff --git a/tests/hikari/test_oauth2.py b/tests/hikari/test_oauth2.py index c64d633607..a85d19ac55 100644 --- a/tests/hikari/test_oauth2.py +++ b/tests/hikari/test_oauth2.py @@ -178,7 +178,7 @@ def team_obj(self, team_payload): return oauth2.Team(id=None, icon_hash="3o2o32o", members=None, owner_user_id=None,) def test_format_icon_url(self): - mock_team = _helpers.create_autospec(oauth2.Team, icon_hash="3o2o32o", id=22323) + mock_team = mock.MagicMock(oauth2.Team, icon_hash="3o2o32o", id=22323) mock_url = "https://cdn.discordapp.com/team-icons/22323/3o2o32o.jpg?size=64" with mock.patch.object(urls, "generate_cdn_url", return_value=mock_url): url = oauth2.Team.format_icon_url(mock_team, fmt="jpg", size=64) @@ -186,7 +186,7 @@ def test_format_icon_url(self): assert url == mock_url def test_format_icon_url_returns_none(self): - mock_team = _helpers.create_autospec(oauth2.Team, icon_hash=None, id=22323) + mock_team = mock.MagicMock(oauth2.Team, icon_hash=None, id=22323) with mock.patch.object(urls, "generate_cdn_url", return_value=...): url = oauth2.Team.format_icon_url(mock_team, fmt="jpg", size=64) urls.generate_cdn_url.assert_not_called() @@ -241,7 +241,7 @@ def application_obj(self, application_information_payload): @pytest.fixture() def mock_application(self): - return _helpers.create_autospec(oauth2.Application, id=22222) + return mock.MagicMock(oauth2.Application, id=22222) def test_icon_url(self, application_obj): mock_url = "https://cdn.discordapp.com/app-icons/209333111222/iwiwiwiwiw.png?size=4096" From 145eff9cd8845ec142b6f9d991582deffc37e4eb Mon Sep 17 00:00:00 2001 From: Faster Speeding Date: Mon, 13 Apr 2020 19:17:01 +0100 Subject: [PATCH 106/922] Closes #281, #278: consistent use of literals in docs + switch to "64 bit integer" --- hikari/clients/configs.py | 42 ++++----- hikari/clients/gateway_managers.py | 6 +- hikari/clients/rest_clients.py | 144 ++++++++++++++--------------- hikari/clients/shard_clients.py | 15 +-- hikari/colors.py | 2 +- hikari/embeds.py | 2 +- hikari/emojis.py | 4 +- hikari/errors.py | 9 +- hikari/events.py | 29 +++--- hikari/guilds.py | 55 +++++------ hikari/internal/assertions.py | 10 +- hikari/internal/conversions.py | 7 +- hikari/internal/marshaller.py | 18 ++-- hikari/internal/urls.py | 2 +- hikari/invites.py | 23 +++-- hikari/messages.py | 13 ++- hikari/net/ratelimits.py | 26 +++--- hikari/net/rest.py | 39 ++++---- hikari/net/shard.py | 21 +++-- hikari/oauth2.py | 8 +- hikari/state/event_dispatchers.py | 6 +- hikari/state/event_managers.py | 4 +- hikari/users.py | 4 +- hikari/webhooks.py | 3 +- 24 files changed, 258 insertions(+), 234 deletions(-) diff --git a/hikari/clients/configs.py b/hikari/clients/configs.py index 4c1d97a69d..4cc6857d31 100644 --- a/hikari/clients/configs.py +++ b/hikari/clients/configs.py @@ -66,25 +66,25 @@ class DebugConfig(BaseConfig): class AIOHTTPConfig(BaseConfig): """Config for components that use AIOHTTP somewhere.""" - #: If ``True``, allow following redirects from ``3xx`` HTTP responses. + #: If :obj:`True`, allow following redirects from ``3xx`` HTTP responses. #: Generally you do not want to enable this unless you have a good reason #: to. #: - #: Defaults to ``False`` if unspecified during deserialization. + #: Defaults to :obj:`False` if unspecified during deserialization. #: #: :type: :obj:`bool` allow_redirects: bool = marshaller.attrib(deserializer=bool, if_undefined=False, default=False) #: Either an implementation of :obj:`aiohttp.TCPConnector`. #: - #: This may otherwise be ``None`` to use the default settings provided + #: This may otherwise be :obj:`None` to use the default settings provided #: by :mod:`aiohttp`. #: #: This is deserialized as an object reference in the format #: ``package.module#object.attribute`` that is expected to point to the #: desired value. #: - #: Defaults to ``None`` if unspecified during deserialization. + #: Defaults to :obj:`None` if unspecified during deserialization. #: #: :type: :obj:`aiohttp.TCPConnector`, optional tcp_connector: typing.Optional[aiohttp.TCPConnector] = marshaller.attrib( @@ -93,7 +93,7 @@ class AIOHTTPConfig(BaseConfig): #: Optional proxy headers to provide in any HTTP requests. #: - #: Defaults to ``None`` if unspecified during deserialization. + #: Defaults to :obj:`None` if unspecified during deserialization. #: #: :type: :obj:`typing.Mapping` [ :obj:`str`, :obj:`str` ], optional proxy_headers: typing.Optional[typing.Mapping[str, str]] = marshaller.attrib( @@ -104,7 +104,7 @@ class AIOHTTPConfig(BaseConfig): #: #: This is deserialized using the format ``"basic {{base 64 string here}}"``. #: - #: Defaults to ``None`` if unspecified during deserialization. + #: Defaults to :obj:`None` if unspecified during deserialization. #: #: :type: :obj:`aiohttp.BasicAuth`, optional proxy_auth: typing.Optional[aiohttp.BasicAuth] = marshaller.attrib( @@ -113,7 +113,7 @@ class AIOHTTPConfig(BaseConfig): #: The optional URL of the proxy to send requests via. #: - #: Defaults to ``None`` if unspecified during deserialization. + #: Defaults to :obj:`None` if unspecified during deserialization. #: #: :type: :obj:`str`, optional proxy_url: typing.Optional[str] = marshaller.attrib(deserializer=str, if_undefined=None, if_none=None, default=None) @@ -121,10 +121,10 @@ class AIOHTTPConfig(BaseConfig): #: Optional request timeout to use. If an HTTP request takes longer than #: this, it will be aborted. #: - #: If not ``None``, the value represents a number of seconds as a floating - #: point number. + #: If not :obj:`None`, the value represents a number of seconds as a + #: floating point number. #: - #: Defaults to ``None`` if unspecified during deserialization. + #: Defaults to :obj:`None` if unspecified during deserialization. #: #: :type: :obj:`float`, optional request_timeout: typing.Optional[float] = marshaller.attrib( @@ -137,20 +137,20 @@ class AIOHTTPConfig(BaseConfig): #: ``package.module#object.attribute`` that is expected to point to the #: desired value. #: - #: Defaults to ``None`` if unspecified during deserialization. + #: Defaults to :obj:`None` if unspecified during deserialization. #: #: :type: :obj:`ssl.SSLContext`, optional ssl_context: typing.Optional[ssl.SSLContext] = marshaller.attrib( deserializer=marshaller.dereference_handle, if_none=None, if_undefined=None, default=None ) - #: If ``True``, then responses with invalid SSL certificates will be + #: If :obj:`True`, then responses with invalid SSL certificates will be #: rejected. Generally you want to keep this enabled unless you have a #: problem with SSL and you know exactly what you are doing by disabling #: this. Disabling SSL verification can have major security implications. #: You turn this off at your own risk. #: - #: Defaults to ``True`` if unspecified during deserialization. + #: Defaults to :obj:`True` if unspecified during deserialization. #: #: :type: :obj:`bool` verify_ssl: bool = marshaller.attrib(deserializer=bool, if_undefined=True, default=True) @@ -186,7 +186,7 @@ class WebsocketConfig(AIOHTTPConfig, TokenConfig, DebugConfig): gateway_version: int = marshaller.attrib(deserializer=int, if_undefined=lambda: 6, default=6) #: The initial activity to set all shards to when starting the gateway. If - #: ``None``, then no activity will be set. + #: :obj:`None`, then no activity will be set. #: #: :type: :obj:`hikari.gateway_entities.GatewayActivity`, optional initial_activity: typing.Optional[gateway_entities.GatewayActivity] = marshaller.attrib( @@ -207,7 +207,7 @@ class WebsocketConfig(AIOHTTPConfig, TokenConfig, DebugConfig): #: :type: :obj:`bool` initial_is_afk: bool = marshaller.attrib(deserializer=bool, if_undefined=False, default=False) - #: The idle time to show on signing in, or ``None`` to not show an idle + #: The idle time to show on signing in, or :obj:`None` to not show an idle #: time. #: #: :type: :obj:`datetime.datetime`, optional @@ -219,7 +219,7 @@ class WebsocketConfig(AIOHTTPConfig, TokenConfig, DebugConfig): #: #: If being deserialized, this can be an integer bitfield, or a sequence of #: intent names. If - #: unspecified, this will be set to ``None``. + #: unspecified, this will be set to :obj:`None`. #: #: Examples #: -------- @@ -247,8 +247,8 @@ class WebsocketConfig(AIOHTTPConfig, TokenConfig, DebugConfig): #: Additionally, intents that are classed by Discord as being privileged #: will require you to whitelist your application in order to use them. #: - #: If you are using the V6 gateway implementation, setting this to ``None`` - #: will simply opt you into every event you can subscribe to. + #: If you are using the V6 gateway implementation, setting this to + #: :obj:`None` will simply opt you into every event you can subscribe to. #: #: #: :type: :obj:`hikari.net.codes.GatewayIntent`, optional @@ -302,10 +302,10 @@ class ShardConfig(BaseConfig): #: ``"5...16"``: #: A range string. Three periods indicate a range of #: ``[5, 17]`` (inclusive beginning, inclusive end). - #: ``None``: + #: :obj:`None`: #: The ``shard_count`` will be considered and that many shards will - #: be created for you. If the ``shard_count`` is also ``None``, then - #: auto-sharding will be performed for you. + #: be created for you. If the ``shard_count`` is also :obj:`None`, + #: then auto-sharding will be performed for you. #: #: :type: :obj:`typing.Sequence` [ :obj:`int` ], optional shard_ids: typing.Optional[typing.Sequence[int]] = marshaller.attrib( diff --git a/hikari/clients/gateway_managers.py b/hikari/clients/gateway_managers.py index 59e8916654..7c7ac35339 100644 --- a/hikari/clients/gateway_managers.py +++ b/hikari/clients/gateway_managers.py @@ -181,10 +181,10 @@ async def update_presence( If specified, the new activity to set. idle_since : :obj:`datetime.datetime`, optional If specified, the time to show up as being idle since, - or ``None`` if not applicable. + or :obj:`None` if not applicable. is_afk : :obj:`bool` - If specified, ``True`` if the user should be marked as AFK, - or ``False`` otherwise. + If specified, :obj:`True` if the user should be marked as AFK, + or :obj:`False` otherwise. """ await asyncio.gather( *( diff --git a/hikari/clients/rest_clients.py b/hikari/clients/rest_clients.py index 157530a70d..a2483e32eb 100644 --- a/hikari/clients/rest_clients.py +++ b/hikari/clients/rest_clients.py @@ -167,7 +167,7 @@ async def fetch_audit_log( ------ :obj:`hikari.errors.BadRequestHTTPError` If any invalid snowflake IDs are passed; a snowflake may be invalid - due to it being outside of the range of UINT64. + due to it being outside of the range of a 64 bit integer. :obj:`hikari.errors.ForbiddenHTTPError` If you lack the given permissions to view an audit log. :obj:`hikari.errors.NotFoundHTTPError` @@ -240,7 +240,7 @@ def fetch_audit_log_entries_before( ------- :obj:`hikari.errors.BadRequestHTTPError` If any invalid snowflake IDs are passed; a snowflake may be invalid - due to it being outside of the range of UINT64. + due to it being outside of the range of a 64 bit integer. :obj:`hikari.audit_logs.AuditLogIterator` An async iterator of the audit log entries in a guild (from newest to oldest). @@ -277,7 +277,7 @@ async def fetch_channel(self, channel: snowflakes.HashableT[_channels.Channel]) ------ :obj:`hikari.errors.BadRequestHTTPError` If any invalid snowflake IDs are passed; a snowflake may be invalid - due to it being outside of the range of UINT64. + due to it being outside of the range of a 64 bit integer. :obj:`hikari.errors.ForbiddenHTTPError` If you don't have access to the channel. :obj:`hikari.errors.NotFoundHTTPError` @@ -320,8 +320,8 @@ async def update_channel( characters in length. nsfw : :obj:`bool` Mark the channel as being not safe for work (NSFW) if :obj:`True`. - If ``False`` or unspecified, then the channel is not marked as NSFW. - Will have no visiable effect for non-text guild channels. + If :obj:`False` or unspecified, then the channel is not marked as + NSFW. Will have no visible effect for non-text guild channels. rate_limit_per_user : :obj:`typing.Union` [ :obj:`int`, :obj:`datetime.timedelta` ] If specified, the time delta of seconds the user has to wait before sending another message. This will not apply to bots, or to @@ -361,7 +361,7 @@ async def update_channel( If you provide incorrect options for the corresponding channel type (e.g. a ``bitrate`` for a text channel). If any invalid snowflake IDs are passed; a snowflake may be invalid - due to it being outside of the range of UINT64. + due to it being outside of the range of a 64 bit integer. """ payload = await self._session.modify_channel( channel_id=str(channel.id if isinstance(channel, snowflakes.UniqueEntity) else int(channel)), @@ -409,7 +409,7 @@ async def delete_channel(self, channel: snowflakes.HashableT[_channels.Channel]) ------ :obj:`hikari.errors.BadRequestHTTPError` If any invalid snowflake IDs are passed; a snowflake may be invalid - due to it being outside of the range of UINT64. + due to it being outside of the range of a 64 bit integer. :obj:`hikari.errors.NotFoundHTTPError` If the channel does not exist. :obj:`hikari.errors.ForbiddenHTTPError` @@ -470,7 +470,7 @@ def fetch_messages_after( ------ :obj:`hikari.errors.BadRequestHTTPError` If any invalid snowflake IDs are passed; a snowflake may be invalid - due to it being outside of the range of UINT64. + due to it being outside of the range of a 64 bit integer. :obj:`hikari.errors.ForbiddenHTTPError` If you lack permission to read the channel. :obj:`hikari.errors.NotFoundHTTPError` @@ -540,7 +540,7 @@ def fetch_messages_before( ------ :obj:`hikari.errors.BadRequestHTTPError` If any invalid snowflake IDs are passed; a snowflake may be invalid - due to it being outside of the range of UINT64. + due to it being outside of the range of a 64 bit integer. :obj:`hikari.errors.ForbiddenHTTPError` If you lack permission to read the channel. :obj:`hikari.errors.NotFoundHTTPError` @@ -611,7 +611,7 @@ async def fetch_messages_around( ------ :obj:`hikari.errors.BadRequestHTTPError` If any invalid snowflake IDs are passed; a snowflake may be invalid - due to it being outside of the range of UINT64. + due to it being outside of the range of a 64 bit integer. :obj:`hikari.errors.ForbiddenHTTPError` If you lack permission to read the channel. :obj:`hikari.errors.NotFoundHTTPError` @@ -726,7 +726,7 @@ async def fetch_message( ------ :obj:`hikari.errors.BadRequestHTTPError` If any invalid snowflake IDs are passed; a snowflake may be invalid - due to it being outside of the range of UINT64. + due to it being outside of the range of a 64 bit integer. :obj:`hikari.errors.ForbiddenHTTPError` If you lack permission to see the message. :obj:`hikari.errors.NotFoundHTTPError` @@ -753,11 +753,11 @@ def _generate_allowed_mentions( discord and lead to actual pings. user_mentions : :obj:`typing.Union` [ :obj:`typing.Collection` [ :obj:`typing.Union` [ :obj:`hikari.users.User`, :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ], :obj:`bool` ] Either an array of user objects/IDs to allow mentions for, - :obj:`True` to allow all user mentions or ``False`` to block all + :obj:`True` to allow all user mentions or :obj:`False` to block all user mentions from resolving. role_mentions : :obj:`typing.Union` [ :obj:`typing.Collection` [ :obj:`typing.Union` [ :obj:`hikari.guilds.GuildRole`, :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ] ], :obj:`bool` ] Either an array of guild role objects/IDs to allow mentions for, - :obj:`True` to allow all role mentions or ``False`` to block all + :obj:`True` to allow all role mentions or :obj:`False` to block all role mentions from resolving. Returns @@ -837,11 +837,11 @@ async def create_message( discord and lead to actual pings, defaults to :obj:`True`. user_mentions : :obj:`typing.Union` [ :obj:`typing.Collection` [ :obj:`typing.Union` [ :obj:`hikari.users.User`, :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ], :obj:`bool` ] Either an array of user objects/IDs to allow mentions for, - :obj:`True` to allow all user mentions or ``False`` to block all + :obj:`True` to allow all user mentions or :obj:`False` to block all user mentions from resolving, defaults to :obj:`True`. role_mentions : :obj:`typing.Union` [ :obj:`typing.Collection` [ :obj:`typing.Union` [ :obj:`hikari.guilds.GuildRole`, :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ] ], :obj:`bool` ] Either an array of guild role objects/IDs to allow mentions for, - :obj:`True` to allow all role mentions or ``False`` to block all + :obj:`True` to allow all role mentions or :obj:`False` to block all role mentions from resolving, defaults to :obj:`True`. Returns @@ -859,7 +859,7 @@ async def create_message( empty or greater than ``2000`` characters; if neither content, files or embed are specified. If any invalid snowflake IDs are passed; a snowflake may be invalid - due to it being outside of the range of UINT64. + due to it being outside of the range of a 64 bit integer. :obj:`hikari.errors.ForbiddenHTTPError` If you lack permissions to send to this channel. """ @@ -893,7 +893,7 @@ def safe_create_message( This endpoint has the same signature as :attr:`create_message` with the only difference being that ``mentions_everyone``, - ``user_mentions`` and ``role_mentions`` default to ``False``. + ``user_mentions`` and ``role_mentions`` default to :obj:`False`. """ return self.create_message( channel=channel, @@ -938,7 +938,7 @@ async def create_reaction( :obj:`hikari.errors.BadRequestHTTPError` If the emoji is not valid, unknown, or formatted incorrectly. If any invalid snowflake IDs are passed; a snowflake may be invalid - due to it being outside of the range of UINT64. + due to it being outside of the range of a 64 bit integer. """ await self._session.create_reaction( channel_id=str(channel.id if isinstance(channel, snowflakes.UniqueEntity) else int(channel)), @@ -978,7 +978,7 @@ async def delete_reaction( :obj:`hikari.errors.BadRequestHTTPError` If the emoji is not valid, unknown, or formatted incorrectly. If any invalid snowflake IDs are passed; a snowflake may be invalid - due to it being outside of the range of UINT64. + due to it being outside of the range of a 64 bit integer. """ await self._session.delete_own_reaction( channel_id=str(channel.id if isinstance(channel, snowflakes.UniqueEntity) else int(channel)), @@ -1002,7 +1002,7 @@ async def delete_all_reactions( ------ :obj:`hikari.errors.BadRequestHTTPError` If any invalid snowflake IDs are passed; a snowflake may be invalid - due to it being outside of the range of UINT64. + due to it being outside of the range of a 64 bit integer. :obj:`hikari.errors.NotFoundHTTPError` If the channel or message is not found. :obj:`hikari.errors.ForbiddenHTTPError` @@ -1036,7 +1036,7 @@ async def delete_all_reactions_for_emoji( ------ :obj:`hikari.errors.BadRequestHTTPError` If any invalid snowflake IDs are passed; a snowflake may be invalid - due to it being outside of the range of UINT64. + due to it being outside of the range of a 64 bit integer. :obj:`hikari.errors.NotFoundHTTPError` If the channel or message or emoji or user is not found. :obj:`hikari.errors.ForbiddenHTTPError` @@ -1099,7 +1099,7 @@ def fetch_reactors_after( ------ :obj:`hikari.errors.BadRequestHTTPError` If any invalid snowflake IDs are passed; a snowflake may be invalid - due to it being outside of the range of UINT64. + due to it being outside of the range of a 64 bit integer. :obj:`hikari.errors.ForbiddenHTTPError` If you lack access to the message. :obj:`hikari.errors.NotFoundHTTPError` @@ -1156,11 +1156,11 @@ async def update_message( discord and lead to actual pings, defaults to :obj:`True`. user_mentions : :obj:`typing.Union` [ :obj:`typing.Collection` [ :obj:`typing.Union` [ :obj:`hikari.users.User`, :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ], :obj:`bool` ] Either an array of user objects/IDs to allow mentions for, - :obj:`True` to allow all user mentions or ``False`` to block all + :obj:`True` to allow all user mentions or :obj:`False` to block all user mentions from resolving, defaults to :obj:`True`. role_mentions : :obj:`typing.Union` [ :obj:`typing.Collection` [ :obj:`typing.Union` [ :obj:`hikari.guilds.GuildRole`, :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ] ], :obj:`bool` ] Either an array of guild role objects/IDs to allow mentions for, - :obj:`True` to allow all role mentions or ``False`` to block all + :obj:`True` to allow all role mentions or :obj:`False` to block all role mentions from resolving, defaults to :obj:`True`. Returns @@ -1178,7 +1178,7 @@ async def update_message( than ``2000`` characters; if neither content, file or embed are specified. If any invalid snowflake IDs are passed; a snowflake may be invalid - due to it being outside of the range of UINT64. + due to it being outside of the range of a 64 bit integer. :obj:`hikari.errors.ForbiddenHTTPError` If you try to edit ``content`` or ``embed`` or ``allowed_mentions` on a message you did not author. @@ -1213,7 +1213,7 @@ def safe_update_message( This endpoint has the same signature as :attr:`execute_webhook` with the only difference being that ``mentions_everyone``, - ``user_mentions`` and ``role_mentions`` default to ``False``. + ``user_mentions`` and ``role_mentions`` default to :obj:`False`. """ return self.update_message( message=message, @@ -1248,7 +1248,7 @@ async def delete_messages( ------ :obj:`hikari.errors.BadRequestHTTPError` If any invalid snowflake IDs are passed; a snowflake may be invalid - due to it being outside of the range of UINT64. + due to it being outside of the range of a 64 bit integer. :obj:`hikari.errors.ForbiddenHTTPError` If you did not author the message and are in a DM, or if you did not author the message and lack the ``MANAGE_MESSAGES`` @@ -1326,7 +1326,7 @@ async def update_channel_overwrite( ------ :obj:`hikari.errors.BadRequestHTTPError` If any invalid snowflake IDs are passed; a snowflake may be invalid - due to it being outside of the range of UINT64. + due to it being outside of the range of a 64 bit integer. :obj:`hikari.errors.NotFoundHTTPError` If the target channel or overwrite doesn't exist. :obj:`hikari.errors.ForbiddenHTTPError` @@ -1360,7 +1360,7 @@ async def fetch_invites_for_channel( ------ :obj:`hikari.errors.BadRequestHTTPError` If any invalid snowflake IDs are passed; a snowflake may be invalid - due to it being outside of the range of UINT64. + due to it being outside of the range of a 64 bit integer. :obj:`hikari.errors.ForbiddenHTTPError` If you lack the ``MANAGE_CHANNELS`` permission. :obj:`hikari.errors.NotFoundHTTPError` @@ -1425,7 +1425,7 @@ async def create_invite_for_channel( :obj:`hikari.errors.BadRequestHTTPError` If the arguments provided are not valid (e.g. negative age, etc). If any invalid snowflake IDs are passed; a snowflake may be invalid - due to it being outside of the range of UINT64. + due to it being outside of the range of a 64 bit integer. """ payload = await self._session.create_channel_invite( channel_id=str(channel.id if isinstance(channel, snowflakes.UniqueEntity) else int(channel)), @@ -1461,7 +1461,7 @@ async def delete_channel_overwrite( ------ :obj:`hikari.errors.BadRequestHTTPError` If any invalid snowflake IDs are passed; a snowflake may be invalid - due to it being outside of the range of UINT64. + due to it being outside of the range of a 64 bit integer. :obj:`hikari.errors.NotFoundHTTPError` If the overwrite or channel do not exist. :obj:`hikari.errors.ForbiddenHTTPError` @@ -1484,7 +1484,7 @@ async def trigger_typing(self, channel: snowflakes.HashableT[_channels.Channel]) ------ :obj:`hikari.errors.BadRequestHTTPError` If any invalid snowflake IDs are passed; a snowflake may be invalid - due to it being outside of the range of UINT64. + due to it being outside of the range of a 64 bit integer. :obj:`hikari.errors.NotFoundHTTPError` If the channel is not found. :obj:`hikari.errors.ForbiddenHTTPError` @@ -1513,7 +1513,7 @@ async def fetch_pins( ------ :obj:`hikari.errors.BadRequestHTTPError` If any invalid snowflake IDs are passed; a snowflake may be invalid - due to it being outside of the range of UINT64. + due to it being outside of the range of a 64 bit integer. :obj:`hikari.errors.NotFoundHTTPError` If the channel is not found. :obj:`hikari.errors.ForbiddenHTTPError` @@ -1546,7 +1546,7 @@ async def pin_message( ------ :obj:`hikari.errors.BadRequestHTTPError` If any invalid snowflake IDs are passed; a snowflake may be invalid - due to it being outside of the range of UINT64. + due to it being outside of the range of a 64 bit integer. :obj:`hikari.errors.ForbiddenHTTPError` If you lack the ``MANAGE_MESSAGES`` permission. :obj:`hikari.errors.NotFoundHTTPError` @@ -1575,7 +1575,7 @@ async def unpin_message( ------ :obj:`hikari.errors.BadRequestHTTPError` If any invalid snowflake IDs are passed; a snowflake may be invalid - due to it being outside of the range of UINT64. + due to it being outside of the range of a 64 bit integer. :obj:`hikari.errors.ForbiddenHTTPError` If you lack the ``MANAGE_MESSAGES`` permission. :obj:`hikari.errors.NotFoundHTTPError` @@ -1607,7 +1607,7 @@ async def fetch_guild_emoji( ------ :obj:`hikari.errors.BadRequestHTTPError` If any invalid snowflake IDs are passed; a snowflake may be invalid - due to it being outside of the range of UINT64. + due to it being outside of the range of a 64 bit integer. :obj:`hikari.errors.NotFoundHTTPError` If either the guild or the emoji aren't found. :obj:`hikari.errors.ForbiddenHTTPError` @@ -1636,7 +1636,7 @@ async def fetch_guild_emojis(self, guild: snowflakes.HashableT[guilds.Guild]) -> ------ :obj:`hikari.errors.BadRequestHTTPError` If any invalid snowflake IDs are passed; a snowflake may be invalid - due to it being outside of the range of UINT64. + due to it being outside of the range of a 64 bit integer. :obj:`hikari.errors.NotFoundHTTPError` If the guild is not found. :obj:`hikari.errors.ForbiddenHTTPError` @@ -1691,7 +1691,7 @@ async def create_guild_emoji( If you attempt to upload an image larger than ``256kb``, an empty image or an invalid image format. If any invalid snowflake IDs are passed; a snowflake may be invalid - due to it being outside of the range of UINT64. + due to it being outside of the range of a 64 bit integer. """ payload = await self._session.create_guild_emoji( guild_id=str(guild.id if isinstance(guild, snowflakes.UniqueEntity) else int(guild)), @@ -1741,7 +1741,7 @@ async def update_guild_emoji( ------ :obj:`hikari.errors.BadRequestHTTPError` If any invalid snowflake IDs are passed; a snowflake may be invalid - due to it being outside of the range of UINT64. + due to it being outside of the range of a 64 bit integer. :obj:`hikari.errors.NotFoundHTTPError` If either the guild or the emoji aren't found. :obj:`hikari.errors.ForbiddenHTTPError` @@ -1775,7 +1775,7 @@ async def delete_guild_emoji( ------ :obj:`hikari.errors.BadRequestHTTPError` If any invalid snowflake IDs are passed; a snowflake may be invalid - due to it being outside of the range of UINT64. + due to it being outside of the range of a 64 bit integer. :obj:`hikari.errors.NotFoundHTTPError` If either the guild or the emoji aren't found. :obj:`hikari.errors.ForbiddenHTTPError` @@ -1844,7 +1844,7 @@ async def create_guild( If you provide unsupported fields like ``parent_id`` in channel objects. If any invalid snowflake IDs are passed; a snowflake may be invalid - due to it being outside of the range of UINT64. + due to it being outside of the range of a 64 bit integer. """ payload = await self._session.create_guild( name=name, @@ -1875,7 +1875,7 @@ async def fetch_guild(self, guild: snowflakes.HashableT[guilds.Guild]) -> guilds ------ :obj:`hikari.errors.BadRequestHTTPError` If any invalid snowflake IDs are passed; a snowflake may be invalid - due to it being outside of the range of UINT64. + due to it being outside of the range of a 64 bit integer. :obj:`hikari.errors.NotFoundHTTPError` If the guild is not found. :obj:`hikari.errors.ForbiddenHTTPError` @@ -1949,7 +1949,7 @@ async def update_guild( ------ :obj:`hikari.errors.BadRequestHTTPError` If any invalid snowflake IDs are passed; a snowflake may be invalid - due to it being outside of the range of UINT64. + due to it being outside of the range of a 64 bit integer. :obj:`hikari.errors.NotFoundHTTPError` If the guild is not found. :obj:`hikari.errors.ForbiddenHTTPError` @@ -1996,7 +1996,7 @@ async def delete_guild(self, guild: snowflakes.HashableT[guilds.Guild]) -> None: ------ :obj:`hikari.errors.BadRequestHTTPError` If any invalid snowflake IDs are passed; a snowflake may be invalid - due to it being outside of the range of UINT64. + due to it being outside of the range of a 64 bit integer. :obj:`hikari.errors.NotFoundHTTPError` If the guild is not found. :obj:`hikari.errors.ForbiddenHTTPError` @@ -2025,7 +2025,7 @@ async def fetch_guild_channels( ------ :obj:`hikari.errors.BadRequestHTTPError` If any invalid snowflake IDs are passed; a snowflake may be invalid - due to it being outside of the range of UINT64. + due to it being outside of the range of a 64 bit integer. :obj:`hikari.errors.NotFoundHTTPError` If the guild is not found. :obj:`hikari.errors.ForbiddenHTTPError` @@ -2112,7 +2112,7 @@ async def create_guild_channel( If you provide incorrect options for the corresponding channel type (e.g. a ``bitrate`` for a text channel). If any invalid snowflake IDs are passed; a snowflake may be invalid - due to it being outside of the range of UINT64. + due to it being outside of the range of a 64 bit integer. """ payload = await self._session.create_guild_channel( guild_id=str(guild.id if isinstance(guild, snowflakes.UniqueEntity) else int(guild)), @@ -2173,7 +2173,7 @@ async def reposition_guild_channels( If you provide anything other than the ``id`` and ``position`` fields for the channels. If any invalid snowflake IDs are passed; a snowflake may be invalid - due to it being outside of the range of UINT64. + due to it being outside of the range of a 64 bit integer. """ await self._session.modify_guild_channel_positions( str(guild.id if isinstance(guild, snowflakes.UniqueEntity) else int(guild)), @@ -2204,7 +2204,7 @@ async def fetch_member( ------ :obj:`hikari.errors.BadRequestHTTPError` If any invalid snowflake IDs are passed; a snowflake may be invalid - due to it being outside of the range of UINT64. + due to it being outside of the range of a 64 bit integer. :obj:`hikari.errors.NotFoundHTTPError` If either the guild or the member aren't found. :obj:`hikari.errors.ForbiddenHTTPError` @@ -2257,7 +2257,7 @@ def fetch_members_after( ------ :obj:`hikari.errors.BadRequestHTTPError` If any invalid snowflake IDs are passed; a snowflake may be invalid - due to it being outside of the range of UINT64. + due to it being outside of the range of a 64 bit integer. :obj:`hikari.errors.NotFoundHTTPError` If the guild is not found. :obj:`hikari.errors.ForbiddenHTTPError` @@ -2329,7 +2329,7 @@ async def update_member( If you pass ``mute``, ``deaf`` or ``channel_id`` while the member is not connected to a voice channel. If any invalid snowflake IDs are passed; a snowflake may be invalid - due to it being outside of the range of UINT64. + due to it being outside of the range of a 64 bit integer. """ await self._session.modify_guild_member( guild_id=str(guild.id if isinstance(guild, snowflakes.UniqueEntity) else int(guild)), @@ -2376,7 +2376,7 @@ async def update_my_member_nickname( If you provide a disallowed nickname, one that is too long, or one that is empty. If any invalid snowflake IDs are passed; a snowflake may be invalid - due to it being outside of the range of UINT64. + due to it being outside of the range of a 64 bit integer. """ await self._session.modify_current_user_nick( guild_id=str(guild.id if isinstance(guild, snowflakes.UniqueEntity) else int(guild)), @@ -2410,7 +2410,7 @@ async def add_role_to_member( ------ :obj:`hikari.errors.BadRequestHTTPError` If any invalid snowflake IDs are passed; a snowflake may be invalid - due to it being outside of the range of UINT64. + due to it being outside of the range of a 64 bit integer. :obj:`hikari.errors.NotFoundHTTPError` If either the guild, member or role aren't found. :obj:`hikari.errors.ForbiddenHTTPError` @@ -2449,7 +2449,7 @@ async def remove_role_from_member( ------ :obj:`hikari.errors.BadRequestHTTPError` If any invalid snowflake IDs are passed; a snowflake may be invalid - due to it being outside of the range of UINT64. + due to it being outside of the range of a 64 bit integer. :obj:`hikari.errors.NotFoundHTTPError` If either the guild, member or role aren't found. :obj:`hikari.errors.ForbiddenHTTPError` @@ -2481,7 +2481,7 @@ async def kick_member( ------ :obj:`hikari.errors.BadRequestHTTPError` If any invalid snowflake IDs are passed; a snowflake may be invalid - due to it being outside of the range of UINT64. + due to it being outside of the range of a 64 bit integer. :obj:`hikari.errors.NotFoundHTTPError` If either the guild or member aren't found. :obj:`hikari.errors.ForbiddenHTTPError` @@ -2514,7 +2514,7 @@ async def fetch_ban( ------ :obj:`hikari.errors.BadRequestHTTPError` If any invalid snowflake IDs are passed; a snowflake may be invalid - due to it being outside of the range of UINT64. + due to it being outside of the range of a 64 bit integer. :obj:`hikari.errors.NotFoundHTTPError` If either the guild or the user aren't found, or if the user is not banned. @@ -2544,7 +2544,7 @@ async def fetch_bans(self, guild: snowflakes.HashableT[guilds.Guild],) -> typing ------ :obj:`hikari.errors.BadRequestHTTPError` If any invalid snowflake IDs are passed; a snowflake may be invalid - due to it being outside of the range of UINT64. + due to it being outside of the range of a 64 bit integer. :obj:`hikari.errors.NotFoundHTTPError` If the guild is not found. :obj:`hikari.errors.ForbiddenHTTPError` @@ -2582,7 +2582,7 @@ async def ban_member( ------ :obj:`hikari.errors.BadRequestHTTPError` If any invalid snowflake IDs are passed; a snowflake may be invalid - due to it being outside of the range of UINT64. + due to it being outside of the range of a 64 bit integer. :obj:`hikari.errors.NotFoundHTTPError` If either the guild or member aren't found. :obj:`hikari.errors.ForbiddenHTTPError` @@ -2614,7 +2614,7 @@ async def unban_member( ------ :obj:`hikari.errors.BadRequestHTTPError` If any invalid snowflake IDs are passed; a snowflake may be invalid - due to it being outside of the range of UINT64. + due to it being outside of the range of a 64 bit integer. :obj:`hikari.errors.NotFoundHTTPError` If either the guild or member aren't found, or the member is not banned. @@ -2647,7 +2647,7 @@ async def fetch_roles( ------ :obj:`hikari.errors.BadRequestHTTPError` If any invalid snowflake IDs are passed; a snowflake may be invalid - due to it being outside of the range of UINT64. + due to it being outside of the range of a 64 bit integer. :obj:`hikari.errors.NotFoundHTTPError` If the guild is not found. :obj:`hikari.errors.ForbiddenHTTPError` @@ -2706,7 +2706,7 @@ async def create_role( :obj:`hikari.errors.BadRequestHTTPError` If you provide invalid values for the role attributes. If any invalid snowflake IDs are passed; a snowflake may be invalid - due to it being outside of the range of UINT64. + due to it being outside of the range of a 64 bit integer. """ payload = await self._session.create_guild_role( guild_id=str(guild.id if isinstance(guild, snowflakes.UniqueEntity) else int(guild)), @@ -2753,7 +2753,7 @@ async def reposition_roles( :obj:`hikari.errors.BadRequestHTTPError` If you provide invalid values for the `position` fields. If any invalid snowflake IDs are passed; a snowflake may be invalid - due to it being outside of the range of UINT64. + due to it being outside of the range of a 64 bit integer. """ payload = await self._session.modify_guild_role_positions( str(guild.id if isinstance(guild, snowflakes.UniqueEntity) else int(guild)), @@ -2815,7 +2815,7 @@ async def update_role( :obj:`hikari.errors.BadRequestHTTPError` If you provide invalid values for the role attributes. If any invalid snowflake IDs are passed; a snowflake may be invalid - due to it being outside of the range of UINT64. + due to it being outside of the range of a 64 bit integer. """ payload = await self._session.modify_guild_role( guild_id=str(guild.id if isinstance(guild, snowflakes.UniqueEntity) else int(guild)), @@ -2845,7 +2845,7 @@ async def delete_role( ------ :obj:`hikari.errors.BadRequestHTTPError` If any invalid snowflake IDs are passed; a snowflake may be invalid - due to it being outside of the range of UINT64. + due to it being outside of the range of a 64 bit integer. :obj:`hikari.errors.NotFoundHTTPError` If either the guild or the role aren't found. :obj:`hikari.errors.ForbiddenHTTPError` @@ -2882,7 +2882,7 @@ async def estimate_guild_prune_count( :obj:`hikari.errors.BadRequestHTTPError` If you pass an invalid amount of days. If any invalid snowflake IDs are passed; a snowflake may be invalid - due to it being outside of the range of UINT64. + due to it being outside of the range of a 64 bit integer. """ return await self._session.get_guild_prune_count( guild_id=str(guild.id if isinstance(guild, snowflakes.UniqueEntity) else int(guild)), @@ -2928,7 +2928,7 @@ async def begin_guild_prune( If you provide invalid values for the ``days`` or ``compute_prune_count`` fields. If any invalid snowflake IDs are passed; a snowflake may be invalid - due to it being outside of the range of UINT64. + due to it being outside of the range of a 64 bit integer. """ return await self._session.begin_guild_prune( guild_id=str(guild.id if isinstance(guild, snowflakes.UniqueEntity) else int(guild)), @@ -2956,7 +2956,7 @@ async def fetch_guild_voice_regions( ------ :obj:`hikari.errors.BadRequestHTTPError` If any invalid snowflake IDs are passed; a snowflake may be invalid - due to it being outside of the range of UINT64. + due to it being outside of the range of a 64 bit integer. :obj:`hikari.errors.NotFoundHTTPError` If the guild is not found. :obj:`hikari.errors.ForbiddenHTTPError` @@ -2986,7 +2986,7 @@ async def fetch_guild_invites( ------ :obj:`hikari.errors.BadRequestHTTPError` If any invalid snowflake IDs are passed; a snowflake may be invalid - due to it being outside of the range of UINT64. + due to it being outside of the range of a 64 bit integer. :obj:`hikari.errors.NotFoundHTTPError` If the guild is not found. :obj:`hikari.errors.ForbiddenHTTPError` @@ -3016,7 +3016,7 @@ async def fetch_integrations( ------ :obj:`hikari.errors.BadRequestHTTPError` If any invalid snowflake IDs are passed; a snowflake may be invalid - due to it being outside of the range of UINT64. + due to it being outside of the range of a 64 bit integer. :obj:`hikari.errors.NotFoundHTTPError` If the guild is not found. :obj:`hikari.errors.ForbiddenHTTPError` @@ -3062,7 +3062,7 @@ async def update_integration( ------ :obj:`hikari.errors.BadRequestHTTPError` If any invalid snowflake IDs are passed; a snowflake may be invalid - due to it being outside of the range of UINT64. + due to it being outside of the range of a 64 bit integer. :obj:`hikari.errors.NotFoundHTTPError` If either the guild or the integration aren't found. :obj:`hikari.errors.ForbiddenHTTPError` @@ -3102,7 +3102,7 @@ async def delete_integration( ------ :obj:`hikari.errors.BadRequestHTTPError` If any invalid snowflake IDs are passed; a snowflake may be invalid - due to it being outside of the range of UINT64. + due to it being outside of the range of a 64 bit integer. :obj:`hikari.errors.NotFoundHTTPError` If either the guild or the integration aren't found. :obj:`hikari.errors.ForbiddenHTTPError` @@ -3132,7 +3132,7 @@ async def sync_guild_integration( ------ :obj:`hikari.errors.BadRequestHTTPError` If any invalid snowflake IDs are passed; a snowflake may be invalid - due to it being outside of the range of UINT64. + due to it being outside of the range of a 64 bit integer. :obj:`hikari.errors.NotFoundHTTPError` If either the guild or the integration aren't found. :obj:`hikari.errors.ForbiddenHTTPError` @@ -3162,7 +3162,7 @@ async def fetch_guild_embed(self, guild: snowflakes.HashableT[guilds.Guild],) -> ------ :obj:`hikari.errors.BadRequestHTTPError` If any invalid snowflake IDs are passed; a snowflake may be invalid - due to it being outside of the range of UINT64. + due to it being outside of the range of a 64 bit integer. :obj:`hikari.errors.NotFoundHTTPError` If the guild is not found. :obj:`hikari.errors.ForbiddenHTTPError` diff --git a/hikari/clients/shard_clients.py b/hikari/clients/shard_clients.py index 72ac29c164..4d1e597b9c 100644 --- a/hikari/clients/shard_clients.py +++ b/hikari/clients/shard_clients.py @@ -93,9 +93,10 @@ class ShardClient(runnable.RunnableClient): The URL to connect the gateway to. dispatcher : :obj:`hikari.state.event_dispatchers.EventDispatcher`, optional The high level event dispatcher to use for dispatching start and stop - events. Set this to ``None`` to disable that functionality (useful if + events. Set this to :obj:`None` to disable that functionality (useful if you use a gateway manager to orchestrate multiple shards instead and - provide this functionality there). Defaults to ``None`` if unspecified. + provide this functionality there). Defaults to :obj:`None` if + unspecified. Notes ----- @@ -218,7 +219,7 @@ def activity(self) -> typing.Optional[gateway_entities.GatewayActivity]: # noqa Returns ------- :obj:`hikari.gateway_entities.GatewayActivity`, optional - The current activity for the user on this shard, or ``None`` if + The current activity for the user on this shard, or :obj:`None` if there is no activity. """ return self._activity @@ -231,19 +232,19 @@ def idle_since(self) -> typing.Optional[datetime.datetime]: ------- :obj:`datetime.datetime`, optional The timestamp when the user of this shard appeared to be idle, or - ``None`` if not applicable. + :obj:`None` if not applicable. """ return self._idle_since # Ignore docstring not starting in an imperative mood @property def is_afk(self) -> bool: # noqa: D401 - """``True`` if the user is AFK, ``False`` otherwise. + """:obj:`True` if the user is AFK, :obj:`False` otherwise. Returns ------- :obj:`bool` - ``True`` if the user is AFK, ``False`` otherwise. + :obj:`True` if the user is AFK, :obj:`False` otherwise. """ return self._is_afk @@ -492,7 +493,7 @@ async def update_presence( If specified, the new activity to set. idle_since : :obj:`datetime.datetime`, optional If specified, the time to show up as being idle since, or - ``None`` if not applicable. + :obj:`None` if not applicable. is_afk : :obj:`bool` If specified, whether the user should be marked as AFK. """ diff --git a/hikari/colors.py b/hikari/colors.py index 2308ec433d..6e97734e1c 100644 --- a/hikari/colors.py +++ b/hikari/colors.py @@ -168,7 +168,7 @@ def raw_hex_code(self) -> str: # Ignore docstring not starting in an imperative mood @property def is_web_safe(self) -> bool: # noqa: D401 - """``True`` if the color is web safe, ``False`` otherwise.""" + """:obj:`True` if the color is web safe, :obj:`False` otherwise.""" hex_code = self.raw_hex_code return all(_all_same(*c) for c in (hex_code[:2], hex_code[2:4], hex_code[4:])) diff --git a/hikari/embeds.py b/hikari/embeds.py index 249a73789d..f9e14d5dbd 100644 --- a/hikari/embeds.py +++ b/hikari/embeds.py @@ -190,7 +190,7 @@ class EmbedField(entities.HikariEntity, entities.Deserializable, entities.Serial #: :type: :obj:`str` value: str = marshaller.attrib(deserializer=str, serializer=str) - #: Whether the field should display inline. Defaults to ``False``. + #: Whether the field should display inline. Defaults to :obj:`False`. #: #: :type: :obj:`bool` is_inline: bool = marshaller.attrib(raw_name="inline", deserializer=bool, serializer=bool, if_undefined=False) diff --git a/hikari/emojis.py b/hikari/emojis.py index 8e7e68f88e..37f81beba9 100644 --- a/hikari/emojis.py +++ b/hikari/emojis.py @@ -95,8 +95,8 @@ class GuildEmoji(UnknownEmoji): #: #: Note #: ---- - #: This will be ``None`` if you are missing the ``MANAGE_EMOJIS`` permission - #: in the server the emoji is from. + #: This will be :obj:`None` if you are missing the ``MANAGE_EMOJIS`` + #: permission in the server the emoji is from. #: #: #: :type: :obj:`hikari.users.User`, optional diff --git a/hikari/errors.py b/hikari/errors.py index 40794d262d..512496ccd2 100644 --- a/hikari/errors.py +++ b/hikari/errors.py @@ -136,12 +136,13 @@ class GatewayInvalidSessionError(GatewayServerClosedConnectionError): Parameters ---------- can_resume : :obj:`bool` - ``True`` if the connection will be able to RESUME next time it starts rather - than re-IDENTIFYing, or ``False`` if you need to IDENTIFY again instead. + :obj:`True` if the connection will be able to RESUME next time it starts + rather than re-IDENTIFYing, or :obj:`False` if you need to IDENTIFY + again instead. """ - #: ``True``` if the next reconnection can be RESUMED. ``False``` if it has to be - #: coordinated by re-IDENFITYing. + #: :obj:`True` if the next reconnection can be RESUMED. :obj:`False` if it + #: has to be coordinated by re-IDENFITYing. #: #: :type: :obj:`bool` can_resume: bool diff --git a/hikari/events.py b/hikari/events.py index 9199fbeee2..56cf737cb4 100644 --- a/hikari/events.py +++ b/hikari/events.py @@ -114,7 +114,7 @@ class ExceptionEvent(HikariEvent): #: The event that was being invoked when the exception occurred. #: - #: :type: coroutine function ( :obj:`HikariEvent` ) -> ``None`` + #: :type: coroutine function ( :obj:`HikariEvent` ) -> :obj:`None` callback: typing.Callable[[HikariEvent], typing.Awaitable[None]] @@ -251,7 +251,7 @@ class BaseChannelEvent(HikariEvent, snowflakes.UniqueEntity, entities.Deserializ #: :type: :obj:`hikari.channels.ChannelType` type: channels.ChannelType = marshaller.attrib(deserializer=channels.ChannelType) - #: The ID of the guild this channel is in, will be ``None`` for DMs. + #: The ID of the guild this channel is in, will be :obj:`None` for DMs. #: #: :type: :obj:`hikari.snowflakes.Snowflake`, optional guild_id: typing.Optional[snowflakes.Snowflake] = marshaller.attrib( @@ -284,7 +284,7 @@ class BaseChannelEvent(HikariEvent, snowflakes.UniqueEntity, entities.Deserializ #: :type: :obj:`str`, optional topic: typing.Optional[str] = marshaller.attrib(deserializer=str, if_undefined=None, if_none=None) - #: Whether this channel is nsfw, will be ``None`` if not applicable. + #: Whether this channel is nsfw, will be :obj:`None` if not applicable. #: #: :type: :obj:`bool`, optional is_nsfw: typing.Optional[bool] = marshaller.attrib(raw_name="nsfw", deserializer=bool, if_undefined=None) @@ -396,7 +396,7 @@ class ChannelPinUpdateEvent(HikariEvent, entities.Deserializable): """ #: The ID of the guild where this event happened. - #: Will be ``None`` if this happened in a DM channel. + #: Will be :obj:`None` if this happened in a DM channel. #: #: :type: :obj:`hikari.snowflakes.Snowflake`, optional guild_id: typing.Optional[snowflakes.Snowflake] = marshaller.attrib( @@ -409,7 +409,7 @@ class ChannelPinUpdateEvent(HikariEvent, entities.Deserializable): channel_id: snowflakes.Snowflake = marshaller.attrib(deserializer=snowflakes.Snowflake.deserialize) #: The datetime of when the most recent message was pinned in this channel. - #: Will be ``None`` if there are no messages pinned after this change. + #: Will be :obj:`None` if there are no messages pinned after this change. #: #: :type: :obj:`datetime.datetime`, optional last_pin_timestamp: typing.Optional[datetime.datetime] = marshaller.attrib( @@ -578,7 +578,7 @@ class GuildMemberUpdateEvent(HikariEvent, entities.Deserializable): #: :type: :obj:`hikari.users.User` user: users.User = marshaller.attrib(deserializer=users.User.deserialize) - #: This member's nickname. When set to ``None``, this has been removed + #: This member's nickname. When set to :obj:`None`, this has been removed #: and when set to :obj:`hikari.entities.UNSET` this hasn't been acted on. #: #: :type: :obj:`typing.Union` [ :obj:`str`, :obj:`hikari.entities.UNSET` ], optional @@ -587,7 +587,7 @@ class GuildMemberUpdateEvent(HikariEvent, entities.Deserializable): ) #: The datetime of when this member started "boosting" this guild. - #: Will be ``None`` if they aren't boosting. + #: Will be :obj:`None` if they aren't boosting. #: #: :type: :obj:`typing.Union` [ :obj:`datetime.datetime`, :obj:`hikari.entities.UNSET` ], optional premium_since: typing.Union[None, datetime.datetime, entities.Unset] = marshaller.attrib( @@ -668,7 +668,7 @@ class InviteCreateEvent(HikariEvent, entities.Deserializable): created_at: datetime.datetime = marshaller.attrib(deserializer=conversions.parse_iso_8601_ts) #: The ID of the guild this invite was created in, if applicable. - #: Will be ``None`` for group DM invites. + #: Will be :obj:`None` for group DM invites. #: #: :type: :obj:`hikari.snowflakes.Snowflake`, optional guild_id: typing.Optional[snowflakes.Snowflake] = marshaller.attrib( @@ -681,7 +681,7 @@ class InviteCreateEvent(HikariEvent, entities.Deserializable): inviter: typing.Optional[users.User] = marshaller.attrib(deserializer=users.User.deserialize, if_undefined=None) #: The timedelta of how long this invite will be valid for. - #: If set to ``None`` then this is unlimited. + #: If set to :obj:`None` then this is unlimited. #: #: :type: :obj:`datetime.timedelta`, optional max_age: typing.Optional[datetime.timedelta] = marshaller.attrib( @@ -737,7 +737,7 @@ class InviteDeleteEvent(HikariEvent, entities.Deserializable): code: str = marshaller.attrib(deserializer=str) #: The ID of the guild this invite was deleted in. - #: This will be ``None`` if this invite belonged to a DM channel. + #: This will be :obj:`None` if this invite belonged to a DM channel. #: #: :type: :obj:`hikari.snowflakes.Snowflake`, optional guild_id: typing.Optional[snowflakes.Snowflake] = marshaller.attrib( @@ -805,7 +805,8 @@ class MessageUpdateEvent(HikariEvent, snowflakes.UniqueEntity, entities.Deserial deserializer=conversions.parse_iso_8601_ts, if_undefined=entities.Unset ) - #: The timestamp that the message was last edited at, or ``None`` if not ever edited. + #: The timestamp that the message was last edited at, or :obj:`None` if + #: it wasn't ever edited. #: #: :type: :obj:`typing.Union` [ :obj:`datetime.datetime`, :obj:`hikari.entities.UNSET` ], optional edited_timestamp: typing.Union[datetime.datetime, entities.Unset, None] = marshaller.attrib( @@ -941,7 +942,7 @@ class MessageDeleteEvent(HikariEvent, entities.Deserializable): channel_id: snowflakes.Snowflake = marshaller.attrib(deserializer=snowflakes.Snowflake.deserialize) #: The ID of the guild where this message was deleted. - #: Will be ``None`` if this message was deleted in a DM channel. + #: Will be :obj:`None` if this message was deleted in a DM channel. #: #: :type: :obj:`hikari.snowflakes.Snowflake`, optional guild_id: typing.Optional[snowflakes.Snowflake] = marshaller.attrib( @@ -968,7 +969,7 @@ class MessageDeleteBulkEvent(HikariEvent, entities.Deserializable): channel_id: snowflakes.Snowflake = marshaller.attrib(deserializer=snowflakes.Snowflake.deserialize) #: The ID of the channel these messages have been deleted in. - #: Will be ``None`` if these messages were bulk deleted in a DM channel. + #: Will be :obj:`None` if these messages were bulk deleted in a DM channel. #: #: :type: :obj:`hikari.snowflakes.Snowflake`, optional guild_id: typing.Optional[snowflakes.Snowflake] = marshaller.attrib( @@ -1151,7 +1152,7 @@ class TypingStartEvent(HikariEvent, entities.Deserializable): channel_id: snowflakes.Snowflake = marshaller.attrib(deserializer=snowflakes.Snowflake.deserialize) #: The ID of the guild this typing event is occurring in. - #: Will be ``None`` if this event is happening in a DM channel. + #: Will be :obj:`None` if this event is happening in a DM channel. #: #: :type: :obj:`hikari.snowflakes.Snowflake`, optional guild_id: typing.Optional[snowflakes.Snowflake] = marshaller.attrib( diff --git a/hikari/guilds.py b/hikari/guilds.py index 5afb3f492f..b9136193a1 100644 --- a/hikari/guilds.py +++ b/hikari/guilds.py @@ -203,7 +203,7 @@ class GuildEmbed(entities.HikariEntity, entities.Deserializable): class GuildMember(entities.HikariEntity, entities.Deserializable): """Used to represent a guild bound member.""" - #: This member's user object, will be ``None`` when attached to Message + #: This member's user object, will be :obj:`None` when attached to Message #: Create and Update gateway events. #: #: :type: :obj:`hikari.users.User`, optional @@ -229,7 +229,7 @@ class GuildMember(entities.HikariEntity, entities.Deserializable): joined_at: datetime.datetime = marshaller.attrib(deserializer=conversions.parse_iso_8601_ts) #: The datetime of when this member started "boosting" this guild. - #: Will be ``None`` if they aren't boosting. + #: Will be :obj:`None` if they aren't boosting. #: #: :type: :obj:`datetime.datetime`, optional premium_since: typing.Optional[datetime.datetime] = marshaller.attrib( @@ -646,7 +646,7 @@ class GuildMemberPresence(entities.HikariEntity, entities.Deserializable): client_status: ClientStatus = marshaller.attrib(deserializer=ClientStatus.deserialize) #: The datetime of when this member started "boosting" this guild. - #: Will be ``None`` if they aren't boosting. + #: Will be :obj:`None` if they aren't boosting. #: #: :type: :obj:`datetime.datetime`, optional premium_since: typing.Optional[datetime.datetime] = marshaller.attrib( @@ -789,9 +789,9 @@ class UnavailableGuild(snowflakes.UniqueEntity, entities.Deserializable): # Ignore docstring not starting in an imperative mood @property def is_unavailable(self) -> bool: # noqa: D401 - """``True`` if this guild is unavailable, or ``False`` if it is available. + """:obj:`True` if this guild is unavailable, else :obj:`False`. - This value is always ``True``, and is only provided for consistency. + This value is always :obj:`True`, and is only provided for consistency. """ return True @@ -886,7 +886,7 @@ class Guild(PartialGuild): owner_id: snowflakes.Snowflake = marshaller.attrib(deserializer=snowflakes.Snowflake.deserialize) #: The guild level permissions that apply to the bot user, - #: Will be ``None`` when this object is retrieved through a REST request + #: Will be :obj:`None` when this object is retrieved through a REST request #: rather than from the gateway. #: #: :type: :obj:`hikari.permissions.Permission` @@ -919,7 +919,7 @@ class Guild(PartialGuild): #: Defines if the guild embed is enabled or not. #: #: This information may not be present, in which case, - #: it will be ``None`` instead. + #: it will be :obj:`None` instead. #: #: :type: :obj:`bool`, optional is_embed_enabled: typing.Optional[bool] = marshaller.attrib( @@ -929,7 +929,7 @@ class Guild(PartialGuild): #: The channel ID that the guild embed will generate an invite to, if #: enabled for this guild. #: - #: Will be ``None`` if invites are disabled for this guild's embed. + #: Will be :obj:`None` if invites are disabled for this guild's embed. #: #: :type: :obj:`hikari.snowflakes.Snowflake`, optional embed_channel_id: typing.Optional[snowflakes.Snowflake] = marshaller.attrib( @@ -976,7 +976,7 @@ class Guild(PartialGuild): mfa_level: GuildMFALevel = marshaller.attrib(deserializer=GuildMFALevel) #: The ID of the application that created this guild, if it was created by - #: a bot. If not, this is always ``None``. + #: a bot. If not, this is always :obj:`None`. #: #: :type: :obj:`hikari.snowflakes.Snowflake`, optional application_id: typing.Optional[snowflakes.Snowflake] = marshaller.attrib( @@ -987,7 +987,7 @@ class Guild(PartialGuild): #: #: This information is only available if the guild was sent via a #: ``GUILD_CREATE`` event. If the guild is received from any other place, - #: this will always be ``None``. + #: this will always be :obj:`None`. #: #: An unavailable guild cannot be interacted with, and most information may #: be outdated if that is the case. @@ -999,7 +999,7 @@ class Guild(PartialGuild): # TODO: document in which cases this information is not available. #: Describes whether the guild widget is enabled or not. If this information - #: is not present, this will be ``None``. + #: is not present, this will be :obj:`None`. #: #: :type: :obj:`bool`, optional is_widget_enabled: typing.Optional[bool] = marshaller.attrib( @@ -1007,7 +1007,7 @@ class Guild(PartialGuild): ) #: The channel ID that the widget's generated invite will send the user to, - #: if enabled. If this information is unavailable, this will be ``None``. + #: if enabled. If this information is unavailable, this will be :obj:`None`. #: #: :type: :obj:`hikari.snowflakes.Snowflake`, optional widget_channel_id: typing.Optional[snowflakes.Snowflake] = marshaller.attrib( @@ -1015,7 +1015,7 @@ class Guild(PartialGuild): ) #: The ID of the system channel (where welcome messages and Nitro boost - #: messages are sent), or ``None`` if it is not enabled. + #: messages are sent), or :obj:`None` if it is not enabled. #: #: :type: :obj:`hikari.snowflakes.Snowflake`, optional system_channel_id: typing.Optional[snowflakes.Snowflake] = marshaller.attrib( @@ -1031,7 +1031,8 @@ class Guild(PartialGuild): #: The ID of the channel where guilds with the :obj:`GuildFeature.PUBLIC` #: ``features`` display rules and guidelines. #: - #: If the :obj:`GuildFeature.PUBLIC` feature is not defined, then this is ``None``. + #: If the :obj:`GuildFeature.PUBLIC` feature is not defined, then this is + #: :obj:`None`. #: #: :type: :obj:`hikari.snowflakes.Snowflake`, optional rules_channel_id: typing.Optional[snowflakes.Snowflake] = marshaller.attrib( @@ -1042,7 +1043,7 @@ class Guild(PartialGuild): #: #: This information is only available if the guild was sent via a #: ``GUILD_CREATE`` event. If the guild is received from any other place, - #: this will always be ``None``. + #: this will always be :obj:`None`. #: #: :type: :obj:`datetime.datetime`, optional joined_at: typing.Optional[datetime.datetime] = marshaller.attrib( @@ -1053,7 +1054,7 @@ class Guild(PartialGuild): #: #: This information is only available if the guild was sent via a #: ``GUILD_CREATE`` event. If the guild is received from any other place, - #: this will always be ``None``. + #: this will always be :obj:`None`. #: #: The implications of a large guild are that presence information will #: not be sent about members who are offline or invisible. @@ -1065,7 +1066,7 @@ class Guild(PartialGuild): #: #: This information is only available if the guild was sent via a #: ``GUILD_CREATE`` event. If the guild is received from any other place, - #: this will always be ``None``. + #: this will always be :obj:`None`. #: #: :type: :obj:`int`, optional member_count: typing.Optional[int] = marshaller.attrib(if_undefined=None, deserializer=int) @@ -1074,7 +1075,7 @@ class Guild(PartialGuild): #: #: This information is only available if the guild was sent via a #: ``GUILD_CREATE`` event. If the guild is received from any other place, - #: this will always be ``None``. + #: this will always be :obj:`None`. #: #: Additionally, any offline members may not be included here, especially #: if there are more members than the large threshold set for the gateway @@ -1096,7 +1097,7 @@ class Guild(PartialGuild): #: #: This information is only available if the guild was sent via a #: ``GUILD_CREATE`` event. If the guild is received from any other place, - #: this will always be ``None``. + #: this will always be :obj:`None`. #: #: Additionally, any channels that you lack permissions to see will not be #: defined here. @@ -1118,7 +1119,7 @@ class Guild(PartialGuild): #: #: This information is only available if the guild was sent via a #: ``GUILD_CREATE`` event. If the guild is received from any other place, - #: this will always be ``None``. + #: this will always be :obj:`None`. #: #: Additionally, any channels that you lack permissions to see will not be #: defined here. @@ -1137,14 +1138,14 @@ class Guild(PartialGuild): #: The maximum number of presences for the guild. #: - #: If this is ``None``, then the default value is used (currently 5000). + #: If this is :obj:`None`, then the default value is used (currently 5000). #: #: :type: :obj:`int`, optional max_presences: typing.Optional[int] = marshaller.attrib(if_none=None, if_undefined=None, deserializer=int) #: The maximum number of members allowed in this guild. #: - #: This information may not be present, in which case, it will be ``None``. + #: This information may not be present, in which case, it will be :obj:`None`. #: #: :type: :obj:`int`, optional max_members: typing.Optional[int] = marshaller.attrib(if_undefined=None, deserializer=int) @@ -1152,7 +1153,7 @@ class Guild(PartialGuild): #: The vanity URL code for the guild's vanity URL. #: #: This is only present if :obj:`GuildFeature.VANITY_URL` is in the - #: ``features`` for this guild. If not, this will always be ``None``. + #: ``features`` for this guild. If not, this will always be :obj:`None`. #: #: :type: :obj:`str`, optional vanity_url_code: typing.Optional[str] = marshaller.attrib(if_none=None, deserializer=str) @@ -1160,7 +1161,7 @@ class Guild(PartialGuild): #: The guild's description. #: #: This is only present if certain :obj:`GuildFeature`'s are set in the - #: ``features`` for this guild. Otherwise, this will always be ``None``. + #: ``features`` for this guild. Otherwise, this will always be :obj:`None`. #: #: :type: :obj:`str`, optional description: typing.Optional[str] = marshaller.attrib(if_none=None, deserializer=str) @@ -1169,7 +1170,7 @@ class Guild(PartialGuild): #: #: This is only present if the guild has :obj:`GuildFeature.BANNER` in the #: ``features`` for this guild. For all other purposes, it is - # ``None``. + # :obj:`None`. #: #: :type: :obj:`str`, optional banner_hash: typing.Optional[str] = marshaller.attrib(raw_name="banner", if_none=None, deserializer=str) @@ -1181,7 +1182,7 @@ class Guild(PartialGuild): #: The number of nitro boosts that the server currently has. #: - #: This information may not be present, in which case, it will be ``None``. + #: This information may not be present, in which case, it will be :obj:`None`. #: #: :type: :obj:`int`, optional premium_subscription_count: typing.Optional[int] = marshaller.attrib(if_undefined=None, deserializer=int) @@ -1199,7 +1200,7 @@ class Guild(PartialGuild): #: #: This is only present if :obj:`GuildFeature.PUBLIC` is in the #: ``features`` for this guild. For all other purposes, it should be - #: considered to be ``None``. + #: considered to be :obj:`None`. #: #: :type: :obj:`hikari.snowflakes.Snowflake`, optional public_updates_channel_id: typing.Optional[snowflakes.Snowflake] = marshaller.attrib( diff --git a/hikari/internal/assertions.py b/hikari/internal/assertions.py index 074a1241bf..7d17095d4a 100644 --- a/hikari/internal/assertions.py +++ b/hikari/internal/assertions.py @@ -39,13 +39,19 @@ def assert_that(condition: bool, message: str = None, error_type: type = ValueError) -> None: - """If the given condition is falsified, raise a :obj:`ValueError` with the optional description provided.""" + """If the given condition is falsified, raise a :obj:`ValueError`. + + Will be raised with the optional description if provided. + """ if not condition: raise error_type(message or "condition must not be False") def assert_not_none(value: ValueT, message: typing.Optional[str] = None) -> ValueT: - """If the given value is ``None``, raise a :obj:`ValueError` with the optional description provided.""" + """If the given value is :obj:`None`, raise a :obj:`ValueError`. + + Will be raised with the optional description if provided. + """ if value is None: raise ValueError(message or "value must not be None") return value diff --git a/hikari/internal/conversions.py b/hikari/internal/conversions.py index 193535e46a..80747fda9a 100644 --- a/hikari/internal/conversions.py +++ b/hikari/internal/conversions.py @@ -61,8 +61,8 @@ def nullable_cast(value: CastInputT, cast: TypeCastT, /) -> ResultT: """Attempt to cast the given ``value`` with the given ``cast``. - This will only succeed if ``value`` is not ``None``. If it is ``None``, then - ``None`` is returned instead. + This will only succeed if ``value`` is not :obj:`None`. If it is + :obj:`None`, then :obj:`None` is returned instead. """ if value is None: return None @@ -134,7 +134,8 @@ def image_bytes_to_image_data(img_bytes: typing.Optional[bytes] = None, /) -> ty Returns ------- :obj:`str`, optional - The ``image_bytes`` given encoded into an image data string or ``None``. + The ``image_bytes`` given encoded into an image data string or + :obj:`None`. Note ---- diff --git a/hikari/internal/marshaller.py b/hikari/internal/marshaller.py index 7f086521a5..241caae76a 100644 --- a/hikari/internal/marshaller.py +++ b/hikari/internal/marshaller.py @@ -166,18 +166,18 @@ def attrib( The raw name of the element in its raw serialized form. If not provided, then this will use the field's default name later. transient : :obj:`bool` - If ``True``, the field is marked as transient, meaning it will not be - serialized. Defaults to ``False``. + If :obj:`True`, the field is marked as transient, meaning it will not be + serialized. Defaults to :obj:`False`. if_none Either a default factory function called to get the default for when - this field is ``None`` or one of ``None``, ``False`` or ``True`` to - specify that this should default to the given singleton. - Will raise an exception when ``None`` is received for this field later - if this isn't specified. + this field is :obj:`None` or one of :obj:`None`, :obj:`False` or + :obj:`True` to specify that this should default to the given singleton. + Will raise an exception when :obj:`None` is received for this field + later if this isn't specified. if_undefined Either a default factory function called to get the default for when - this field isn't defined or one of ``None``, ``False`` or ``True`` to - specify that this should default to the given singleton. + this field isn't defined or one of :obj:`None`, :obj:`False` or + :obj:`True` to specify that this should default to the given singleton. Will raise an exception when this field is undefined later on if this isn't specified. serializer : :obj:`typing.Callable` [ [ :obj:`typing.Any` ], :obj:`typing.Any` ], optional @@ -363,7 +363,7 @@ def deserialize(self, raw_data: typing.Mapping[str, typing.Any], target_type: ty If the entity is not registered. :obj:`AttributeError` If the field is not optional, but the field was not present in the - raw payload, or it was present, but it was assigned ``None``. + raw payload, or it was present, but it was assigned :obj:`None`. :obj:`TypeError` If the deserialization call failed for some reason. """ diff --git a/hikari/internal/urls.py b/hikari/internal/urls.py index 2ded5ea0fd..69e29e11e2 100644 --- a/hikari/internal/urls.py +++ b/hikari/internal/urls.py @@ -55,7 +55,7 @@ def generate_cdn_url(*route_parts: str, fmt: str, size: typing.Optional[int]) -> if the target entity doesn't have an animated version available). size : :obj:`int`, optional The size to specify for the image in the query string if applicable, - should be passed through as ``None`` to avoid the param being set. + should be passed through as :obj:`None` to avoid the param being set. Must be any power of two between 16 and 4096. Returns diff --git a/hikari/invites.py b/hikari/invites.py index 1a89e6d188..669b04df23 100644 --- a/hikari/invites.py +++ b/hikari/invites.py @@ -71,7 +71,8 @@ class InviteGuild(guilds.PartialGuild): #: The hash for the guild's banner. #: #: This is only present if :obj:`hikari.guilds.GuildFeature.BANNER` - #: is in the ``features`` for this guild. For all other purposes, it is ``None``. + #: is in the ``features`` for this guild. For all other purposes, it is + #: :obj:`None`. #: #: :type: :obj:`str`, optional banner_hash: typing.Optional[str] = marshaller.attrib(raw_name="banner", if_none=None, deserializer=str) @@ -79,8 +80,8 @@ class InviteGuild(guilds.PartialGuild): #: The guild's description. #: #: This is only present if certain ``features`` are set in this guild. - #: Otherwise, this will always be ``None``. For all other purposes, it is - #: ``None``. + #: Otherwise, this will always be :obj:`None`. For all other purposes, it is + #: :obj:`None`. #: #: :type: :obj:`str`, optional description: typing.Optional[str] = marshaller.attrib(if_none=None, deserializer=str) @@ -93,7 +94,8 @@ class InviteGuild(guilds.PartialGuild): #: The vanity URL code for the guild's vanity URL. #: #: This is only present if :obj:`hikari.guilds.GuildFeature.VANITY_URL` - #: is in the ``features`` for this guild. If not, this will always be ``None``. + #: is in the ``features`` for this guild. If not, this will always be + #: :obj:`None`. #: #: :type: :obj:`str`, optional vanity_url_code: typing.Optional[str] = marshaller.attrib(if_none=None, deserializer=str) @@ -172,7 +174,7 @@ class Invite(entities.HikariEntity, entities.Deserializable): code: str = marshaller.attrib(deserializer=str) #: The partial object of the guild this dm belongs to. - #: Will be ``None`` for group dm invites. + #: Will be :obj:`None` for group dm invites. #: #: :type: :obj:`InviteGuild`, optional guild: typing.Optional[InviteGuild] = marshaller.attrib(deserializer=InviteGuild.deserialize, if_undefined=None) @@ -199,13 +201,13 @@ class Invite(entities.HikariEntity, entities.Deserializable): ) #: The approximate amount of presences in this invite's guild, only present - #: when ``with_counts`` is passed as ``True`` to the GET Invites endpoint. + #: when ``with_counts`` is passed as :obj:`True` to the GET Invites endpoint. #: #: :type: :obj:`int`, optional approximate_presence_count: typing.Optional[int] = marshaller.attrib(deserializer=int, if_undefined=None) #: The approximate amount of members in this invite's guild, only present - #: when ``with_counts`` is passed as ``True`` to the GET Invites endpoint. + #: when ``with_counts`` is passed as :obj:`True` to the GET Invites endpoint. #: #: :type: :obj:`int`, optional approximate_member_count: typing.Optional[int] = marshaller.attrib(deserializer=int, if_undefined=None) @@ -232,7 +234,7 @@ class InviteWithMetadata(Invite): max_uses: int = marshaller.attrib(deserializer=int) #: The timedelta of how long this invite will be valid for. - #: If set to ``None`` then this is unlimited. + #: If set to :obj:`None` then this is unlimited. #: #: :type: :obj:`datetime.timedelta`, optional max_age: typing.Optional[datetime.timedelta] = marshaller.attrib( @@ -251,7 +253,10 @@ class InviteWithMetadata(Invite): @property def expires_at(self) -> typing.Optional[datetime.datetime]: - """When this invite should expire, if ``max_age`` is set. Else ``None``.""" + """When this invite should expire, if ``max_age`` is set. + + If this invite doesn't have a set expiry then this will be :obj:`None`. + """ if self.max_age: return self.created_at + self.max_age return None diff --git a/hikari/messages.py b/hikari/messages.py index 5064eb4d76..4f586b2165 100644 --- a/hikari/messages.py +++ b/hikari/messages.py @@ -192,8 +192,9 @@ class MessageCrosspost(entities.HikariEntity, entities.Deserializable): #: #: Warning #: ------- - #: This may be ``None`` in some cases according to the Discord API - #: documentation, but the situations that cause this to occur are not currently documented. + #: This may be :obj:`None` in some cases according to the Discord API + #: documentation, but the situations that cause this to occur are not + #: currently documented. #: #: #: :type: :obj:`hikari.snowflakes.Snowflake`, optional @@ -210,8 +211,9 @@ class MessageCrosspost(entities.HikariEntity, entities.Deserializable): #: #: Warning #: ------- - #: This may be ``None`` in some cases according to the Discord API - #: documentation, but the situations that cause this to occur are not currently documented. + #: This may be :obj:`None` in some cases according to the Discord API + #: documentation, but the situations that cause this to occur are not + #: currently documented. #: #: :type: :obj:`hikari.snowflakes.Snowflake`, optional guild_id: typing.Optional[snowflakes.Snowflake] = marshaller.attrib( @@ -258,7 +260,8 @@ class Message(snowflakes.UniqueEntity, entities.Deserializable): #: :type: :obj:`datetime.datetime` timestamp: datetime.datetime = marshaller.attrib(deserializer=conversions.parse_iso_8601_ts) - #: The timestamp that the message was last edited at, or ``None`` if not ever edited. + #: The timestamp that the message was last edited at, or :obj:`None` if it + #: wasn't ever edited. #: #: :type: :obj:`datetime.datetime`, optional edited_timestamp: typing.Optional[datetime.datetime] = marshaller.attrib( diff --git a/hikari/net/ratelimits.py b/hikari/net/ratelimits.py index d6b6b9f512..2651325d55 100644 --- a/hikari/net/ratelimits.py +++ b/hikari/net/ratelimits.py @@ -237,7 +237,7 @@ class BurstRateLimiter(BaseRateLimiter, abc.ABC): #: :type: :obj:`str` name: typing.Final[str] - #: The throttling task, or ``None``` if it isn't running. + #: The throttling task, or :obj:`None` if it isn't running. #: #: :type: :obj:`asyncio.Task`, optional throttle_task: typing.Optional[more_asyncio.Task[None]] @@ -294,7 +294,7 @@ def close(self) -> None: @property def is_empty(self) -> bool: - """Return ``True`` if no futures are on the queue being rate limited.""" + """Return :obj:`True` if no futures are on the queue being rate limited.""" return len(self.queue) == 0 @@ -357,9 +357,9 @@ def throttle(self, retry_after: float) -> None: (it will not await it to finish) When the :meth:`unlock_later` coroutine function completes, it should be - expected to set the `throttle_task`` to ``None``. This means you can + expected to set the `throttle_task`` to :obj:`None`. This means you can check if throttling is occurring by checking if ``throttle_task`` - is not ``None``. + is not :obj:`None`. If this is invoked while another throttle is in progress, that one is cancelled and a new one is started. This enables new rate limits to @@ -386,9 +386,9 @@ async def unlock_later(self, retry_after: float) -> None: instead. When the :meth:`unlock_later` coroutine function completes, it should be - expected to set the ``throttle_task`` to ``None``. This means you can + expected to set the ``throttle_task`` to :obj:`None`. This means you can check if throttling is occurring by checking if ``throttle_task`` - is not ``None``. + is not :obj:`None`. """ self.logger.warning("you are being globally rate limited for %ss", retry_after) await asyncio.sleep(retry_after) @@ -521,7 +521,7 @@ def is_rate_limited(self, now: float) -> bool: Returns ------- :obj:`bool` - ``True`` if we are being rate limited. ``False`` if we are not. + :obj:`True` if we are being rate limited. :obj:`False` if we are not. Warning ------- @@ -555,8 +555,8 @@ async def throttle(self) -> None: task immediately in ``throttle_task``. When this coroutine function completes, it will set the - ``throttle_task`` to ``None``. This means you can check if throttling - is occurring by checking if ``throttle_task`` is not ``None``. + ``throttle_task`` to :obj:`None`. This means you can check if throttling + is occurring by checking if ``throttle_task`` is not :obj:`None`. """ self.logger.debug( "you are being rate limited on bucket %s, backing off for %ss", @@ -612,7 +612,7 @@ def __init__(self, name: str, compiled_route: routes.CompiledRoute) -> None: @property def is_unknown(self) -> bool: - """Return ``True`` if the bucket represents an ``UNKNOWN`` bucket.""" + """Return :obj:`True` if the bucket represents an ``UNKNOWN`` bucket.""" return self.name.startswith(UNKNOWN_HASH) def acquire(self) -> more_asyncio.Future[None]: @@ -872,7 +872,7 @@ def update_rate_limits( The compiled route to get the bucket for. bucket_header : :obj:`str`, optional The ``X-RateLimit-Bucket`` header that was provided in the response, - or ``None`` if not present. + or :obj:`None` if not present. remaining_header : :obj:`int` The ``X-RateLimit-Remaining`` header cast to an :obj:`int`. limit_header : :obj:`int` @@ -915,7 +915,7 @@ class ExponentialBackOff: base : :obj:`float` The base to use. Defaults to ``2``. maximum : :obj:`float`, optional - If not ``None``, then this is the max value the backoff can be in a + If not :obj:`None`, then this is the max value the backoff can be in a single iteration before an :obj:`asyncio.TimeoutError` is raised. Defaults to ``64`` seconds. jitter_multiplier : :obj:`float` @@ -935,7 +935,7 @@ class ExponentialBackOff: #: :type: :obj:`int` increment: int - #: If not ``None```, then this is the max value the backoff can be in a + #: If not :obj:`None`, then this is the max value the backoff can be in a #: single iteration before an :obj:`asyncio.TimeoutError` is raised. #: #: :type: :obj:`float`, optional diff --git a/hikari/net/rest.py b/hikari/net/rest.py index f4c4381471..f6518b592b 100644 --- a/hikari/net/rest.py +++ b/hikari/net/rest.py @@ -66,7 +66,7 @@ class LowLevelRestfulClient: The optional SSL context to be used. verify_ssl: :obj:`bool` Whether or not the client should enforce SSL signed certificate - verification. If ``False`` it will ignore potentially malicious + verification. If :obj:`False` it will ignore potentially malicious SSL certificates. timeout: :obj:`float`, optional The optional timeout for all HTTP requests. @@ -98,7 +98,7 @@ class LowLevelRestfulClient: _AUTHENTICATION_SCHEMES = ("Bearer", "Bot") - #: ``True`` if HTTP redirects are enabled, or ``False`` otherwise. + #: :obj:`True` if HTTP redirects are enabled, or :obj:`False` otherwise. #: #: :type: :obj:`bool` allow_redirects: bool @@ -200,9 +200,9 @@ class LowLevelRestfulClient: #: :type: :obj:`str` user_agent: str - #: If ``True``, SSL certificates are verified for each request, and + #: If :obj:`True`, SSL certificates are verified for each request, and #: invalid SSL certificates are rejected, causing an exception. If - #: ``False``, then unrecognised certificates that may be illegitimate + #: :obj:`False`, then unrecognised certificates that may be illegitimate #: are accepted and ignored. #: #: :type: :obj:`bool` @@ -628,7 +628,7 @@ async def delete_close_channel(self, channel_id: str) -> None: Returns ------- - ``None`` + :obj:`None` Nothing, unlike what the API specifies. This is done to maintain consistency with other calls of a similar nature in this API wrapper. @@ -994,10 +994,10 @@ async def edit_message( The ID of the message to edit. content : :obj:`str`, optional If specified, the string content to replace with in the message. - If ``None``, the content will be removed from the message. + If :obj:`None`, the content will be removed from the message. embed : :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ], optional If specified, the embed to replace with in the message. - If ``None``, the embed will be removed from the message. + If :obj:`None`, the embed will be removed from the message. flags : :obj:`int` If specified, the integer to replace the message's current flags. allowed_mentions : :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ] @@ -1390,7 +1390,7 @@ async def create_guild_emoji( Raises ------ :obj:`ValueError` - If ``image`` is ``None``. + If ``image`` is :obj:`None`. :obj:`hikari.errors.NotFoundHTTPError` If the guild is not found. :obj:`hikari.errors.ForbiddenHTTPError` @@ -1889,17 +1889,19 @@ async def modify_guild_member( # lgtm [py/similar-function] user_id : :obj:`str` The ID of the member to edit. nick : :obj:`str`, optional - If specified, the new nickname string. Setting it to ``None`` explicitly - will clear the nickname. + If specified, the new nickname string. Setting it to :obj:`None` + explicitly will clear the nickname. roles : :obj:`typing.Sequence` [ :obj:`str` ] If specified, a list of role IDs the member should have. mute : :obj:`bool` - If specified, whether the user should be muted in the voice channel or not. + If specified, whether the user should be muted in the voice channel + or not. deaf : :obj:`bool` - If specified, whether the user should be deafen in the voice channel or not. + If specified, whether the user should be deafen in the voice channel + or not. channel_id : :obj:`str` - If specified, the ID of the channel to move the member to. Setting it to - ``None`` explicitly will disconnect the user. + If specified, the ID of the channel to move the member to. Setting + it to :obj:`None` explicitly will disconnect the user. reason : :obj:`str` If specified, the audit log reason explaining why the operation was performed. @@ -2365,7 +2367,7 @@ async def begin_guild_prune( ------- :obj:`int`, optional The number of members who were kicked if ``compute_prune_count`` - is ``True``, else ``None``. + is :obj:`True`, else :obj:`None`. Raises ------ @@ -2577,7 +2579,7 @@ async def modify_guild_embed( The ID of the guild to edit the embed for. channel_id : :obj:`str`, optional If specified, the channel that this embed's invite should target. - Set to ``None`` to disable invites for this embed. + Set to :obj:`None` to disable invites for this embed. enabled : :obj:`bool` If specified, whether this embed should be enabled. reason : :obj:`str` @@ -2688,7 +2690,7 @@ async def delete_invite(self, invite_code: str) -> None: Returns ------- - ``None`` # Marker + :obj:`None` # Marker Nothing, unlike what the API specifies. This is done to maintain consistency with other calls of a similar nature in this API wrapper. @@ -3116,7 +3118,8 @@ async def execute_webhook( Returns ------- :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ], optional - The created message object if ``wait`` is ``True``, else ``None``. + The created message object if ``wait`` is :obj:`True`, else + :obj:`None`. Raises ------ diff --git a/hikari/net/shard.py b/hikari/net/shard.py index bab5aa7602..e9e59ea1ee 100644 --- a/hikari/net/shard.py +++ b/hikari/net/shard.py @@ -96,10 +96,10 @@ class ShardConnection: the reference to this :obj:`ShardConnection` The second is the event name. initial_presence: :obj:`typing.Dict`, optional - A raw JSON object as a :obj:`typing.Dict` that should be set as the initial - presence of the bot user once online. If ``None``, then it will be set to - the default, which is showing up as online without a custom status - message. + A raw JSON object as a :obj:`typing.Dict` that should be set as the + initial presence of the bot user once online. If :obj:`None`, then it + will be set to the default, which is showing up as online without a + custom status message. intents: :obj:`hikari.net.codes.GatewayIntent`, optional Bitfield of intents to use. If you use the V7 API, this is mandatory. This field will determine what events you will receive. @@ -116,13 +116,13 @@ class ShardConnection: sent using :meth:`request_guild_members`. proxy_auth: :obj:`aiohttp.BasicAuth`, optional Optional :obj:`aiohttp.BasicAuth` object that can be provided to - allow authenticating with a proxy if you use one. Leave ``None`` to + allow authenticating with a proxy if you use one. Leave :obj:`None` to ignore. proxy_headers: :obj:`typing.Mapping` [ :obj:`str`, :obj:`str` ], optional Optional :obj:`typing.Mapping` to provide as headers to allow the - connection through a proxy if you use one. Leave ``None`` to ignore. + connection through a proxy if you use one. Leave :obj:`None` to ignore. proxy_url: :obj:`str`, optional - Optional :obj:`str` to use for a proxy server. If ``None``, then it + Optional :obj:`str` to use for a proxy server. If :obj:`None`, then it is ignored. session_id: :obj:`str`, optional The session ID to use. If specified along with ``seq``, then the @@ -146,8 +146,9 @@ class ShardConnection: url: :obj:`str` The websocket URL to use. verify_ssl: :obj:`bool` - If ``True``, SSL verification is enabled, which is generally what you want. - If you get SSL issues, you can try turning this off at your own risk. + If :obj:`True`, SSL verification is enabled, which is generally what you + want. If you get SSL issues, you can try turning this off at your own + risk. version: :obj:`hikari.net.versions.GatewayVersion` The version of the gateway API to use. Defaults to the most recent stable documented version. @@ -417,7 +418,7 @@ def is_connected(self) -> bool: def intents(self) -> typing.Optional[codes.GatewayIntent]: """Intents being used. - If this is ``None``, no intent usage was being + If this is :obj:`None`, no intent usage was being used on this shard. On V6 this would be regular usage as prior to the intents change in January 2020. If on V7, you just won't be able to connect at all to the gateway. diff --git a/hikari/oauth2.py b/hikari/oauth2.py index 8f5f5737f1..63329a9bb8 100644 --- a/hikari/oauth2.py +++ b/hikari/oauth2.py @@ -250,7 +250,7 @@ class Application(snowflakes.UniqueEntity, entities.Deserializable): description: str = marshaller.attrib(deserializer=str) #: Whether the bot associated with this application is public. - #: Will be ``None`` if this application doesn't have an associated bot. + #: Will be :obj:`None` if this application doesn't have an associated bot. #: #: :type: :obj:`bool`, optional is_bot_public: typing.Optional[bool] = marshaller.attrib( @@ -258,7 +258,7 @@ class Application(snowflakes.UniqueEntity, entities.Deserializable): ) #: Whether the bot associated with this application is requiring code grant - #: for invites. Will be ``None`` if this application doesn't have a bot. + #: for invites. Will be :obj:`None` if this application doesn't have a bot. #: #: :type: :obj:`bool`, optional is_bot_code_grant_required: typing.Optional[bool] = marshaller.attrib( @@ -266,8 +266,8 @@ class Application(snowflakes.UniqueEntity, entities.Deserializable): ) #: The object of this application's owner. - #: This should always be ``None`` in application objects retrieved outside - #: Discord's oauth2 flow. + #: This should always be :obj:`None` in application objects retrieved + #: outside Discord's oauth2 flow. #: #: :type: :obj:`ApplicationOwner`, optional owner: typing.Optional[ApplicationOwner] = marshaller.attrib( diff --git a/hikari/state/event_dispatchers.py b/hikari/state/event_dispatchers.py index 986b6a3c77..be680d3fee 100644 --- a/hikari/state/event_dispatchers.py +++ b/hikari/state/event_dispatchers.py @@ -100,9 +100,9 @@ def wait_for( The name of the event to wait for. timeout : :obj:`float`, optional The timeout to wait for before cancelling and raising an - :obj:`asyncio.TimeoutError` instead. If this is ``None``, this will - wait forever. Care must be taken if you use ``None`` as this may - leak memory if you do this from an event listener that gets + :obj:`asyncio.TimeoutError` instead. If this is :obj:`None`, this + will wait forever. Care must be taken if you use :obj:`None` as this + may leak memory if you do this from an event listener that gets repeatedly called. If you want to do this, you should consider using an event listener instead of this function. predicate : ``def predicate(event) -> bool`` or ``async def predicate(event) -> bool`` diff --git a/hikari/state/event_managers.py b/hikari/state/event_managers.py index 9ab9a13958..97a99142da 100644 --- a/hikari/state/event_managers.py +++ b/hikari/state/event_managers.py @@ -87,8 +87,8 @@ class EventManager(typing.Generic[EventDispatcherT], raw_event_consumers.RawEven ---------- event_dispatcher_impl: :obj:`hikari.state.event_dispatchers.EventDispatcher`, optional An implementation of event dispatcher that will store individual events - and manage dispatching them after this object creates them. If ``None``, - then a default implementation is chosen. + and manage dispatching them after this object creates them. If + :obj:`None`, then a default implementation is chosen. Notes ----- diff --git a/hikari/users.py b/hikari/users.py index 3f274bcdec..8ad5fbffd2 100644 --- a/hikari/users.py +++ b/hikari/users.py @@ -159,7 +159,7 @@ class MyUser(User): is_verified: typing.Optional[bool] = marshaller.attrib(raw_name="verified", deserializer=bool, if_undefined=None) #: The user's set email, requires the ``email`` scope. - #: This will always be ``None`` for bots. + #: This will always be :obj:`None` for bots. #: #: :type: :obj:`str`, optional email: typing.Optional[str] = marshaller.attrib(deserializer=str, if_undefined=None, if_none=None) @@ -170,7 +170,7 @@ class MyUser(User): flags: typing.Optional[UserFlag] = marshaller.attrib(deserializer=UserFlag, if_undefined=None) #: The type of Nitro Subscription this user account had. - #: Requires the ``identify`` scope and will always be ``None`` for bots. + #: Requires the ``identify`` scope and will always be :obj:`None` for bots. #: #: :type: :obj:`PremiumType`, optional premium_type: typing.Optional[PremiumType] = marshaller.attrib(deserializer=PremiumType, if_undefined=None) diff --git a/hikari/webhooks.py b/hikari/webhooks.py index 14e52be3ae..586f49df7f 100644 --- a/hikari/webhooks.py +++ b/hikari/webhooks.py @@ -71,7 +71,8 @@ class Webhook(snowflakes.UniqueEntity, entities.Deserializable): #: #: Note #: ---- - #: This will be ``None`` when getting a webhook with a token + #: This will be :obj:`None` when getting a webhook with bot authorization + #: rather than the webhook's token. #: #: #: :type: :obj:`hikari.users.User`, optional From 00bde04eb5c894a5a8da2a1708af05689dc9937d Mon Sep 17 00:00:00 2001 From: Faster Speeding Date: Mon, 13 Apr 2020 17:57:04 +0100 Subject: [PATCH 107/922] Ensure objects are properly serializable for #272 + add test coverage. --- hikari/channels.py | 155 ++++++++++++++++++++- hikari/clients/rest_clients.py | 6 +- hikari/embeds.py | 170 +++++++++++++++++------ hikari/gateway_entities.py | 9 +- hikari/guilds.py | 16 ++- tests/hikari/clients/test_rest_client.py | 2 +- tests/hikari/test_channels.py | 92 ++++++++++++ tests/hikari/test_embeds.py | 88 ++++++++---- tests/hikari/test_gateway_entities.py | 36 +++++ tests/hikari/test_guilds.py | 33 +++++ 10 files changed, 522 insertions(+), 85 deletions(-) diff --git a/hikari/channels.py b/hikari/channels.py index 9bd90f29b1..c25ccad59a 100644 --- a/hikari/channels.py +++ b/hikari/channels.py @@ -41,6 +41,8 @@ "GuildNewsChannel", "GuildStoreChannel", "GuildVoiceChannel", + "GuildChannelBuilder", + "deserialize_channel", ] import datetime @@ -87,6 +89,9 @@ class PermissionOverwriteType(str, enum.Enum): #: A permission overwrite that targets a specific guild member. MEMBER = "member" + def __str__(self) -> str: + return self.value + @marshaller.marshallable() @attr.s(slots=True) @@ -96,17 +101,21 @@ class PermissionOverwrite(snowflakes.UniqueEntity, entities.Deserializable, enti #: The type of entity this overwrite targets. #: #: :type: :obj:`PermissionOverwriteType` - type: PermissionOverwriteType = marshaller.attrib(deserializer=PermissionOverwriteType) + type: PermissionOverwriteType = marshaller.attrib(deserializer=PermissionOverwriteType, serializer=str) #: The permissions this overwrite allows. #: #: :type: :obj:`hikari.permissions.Permission` - allow: permissions.Permission = marshaller.attrib(deserializer=permissions.Permission) + allow: permissions.Permission = marshaller.attrib( + deserializer=permissions.Permission, serializer=int, default=permissions.Permission(0) + ) #: The permissions this overwrite denies. #: #: :type: :obj:`hikari.permissions.Permission` - deny: permissions.Permission = marshaller.attrib(deserializer=permissions.Permission) + deny: permissions.Permission = marshaller.attrib( + deserializer=permissions.Permission, serializer=int, default=permissions.Permission(0) + ) @property def unset(self) -> permissions.Permission: @@ -222,7 +231,7 @@ class GroupDMChannel(DMChannel): @marshaller.marshallable() @attr.s(slots=True) -class GuildChannel(Channel, entities.Serializable): +class GuildChannel(Channel): """The base for anything that is a guild channel.""" #: The ID of the guild the channel belongs to. @@ -350,6 +359,144 @@ class GuildVoiceChannel(GuildChannel): user_limit: int = marshaller.attrib(deserializer=int) +class GuildChannelBuilder(entities.Serializable): + """Used to create channel objects to send in guild create requests. + + Parameters + ---------- + channel_name : str + The name to set for the channel. + channel_type : :obj:`ChannelType` + The type of channel this should build. + + Example + ------- + .. code-block:: python + + channel_obj = ( + channels.GuildChannelBuilder("Catgirl-appreciation", channels.ChannelType.GUILD_TEXT) + .is_nsfw(True) + .with_topic("Here we men of culture appreciate the way of the neko.") + .with_rate_limit_per_user(datetime.timedelta(seconds=5)) + .with_permission_overwrites([overwrite_obj]) + .with_id(1) + ) + """ + + __slots__ = ("_payload",) + + def __init__(self, channel_name: str, channel_type: ChannelType) -> None: + self._payload: entities.RawEntityT = { + "type": channel_type, + "name": channel_name, + } + + def serialize(self: entities.T_co) -> entities.RawEntityT: + """Serialize this instance into a naive value.""" + return self._payload + + def is_nsfw(self) -> "GuildChannelBuilder": + """Mark this channel as NSFW.""" + self._payload["nsfw"] = True + return self + + def with_permission_overwrites(self, overwrites: typing.Sequence[PermissionOverwrite]) -> "GuildChannelBuilder": + """Set the permission overwrites for this channel. + + Note + ---- + Calling this multiple times will overwrite any previously added + overwrites. + + Parameters + ---------- + overwrites : :obj:`typing.Sequence` [ :obj:`PermissionOverwrite` ] + A sequence of overwrite objects to add, where the first overwrite + object + """ + self._payload["permission_overwrites"] = [o.serialize() for o in overwrites] + return self + + def with_topic(self, topic: str) -> "GuildChannelBuilder": + """Set the topic for this channel. + + Parameters + ---------- + topic : :obj:`str` + The string topic to set. + """ + self._payload["topic"] = topic + return self + + def with_bitrate(self, bitrate: int) -> "GuildChannelBuilder": + """Set the bitrate for this channel. + + Parameters + ---------- + bitrate : :obj:`int` + The bitrate to set in bits. + """ + self._payload["bitrate"] = int(bitrate) + return self + + def with_user_limit(self, user_limit: int) -> "GuildChannelBuilder": + """Set the limit for how many users can be in this channel at once. + + Parameters + ---------- + user_limit : :obj:`int` + The user limit to set. + """ + self._payload["user_limit"] = int(user_limit) + return self + + def with_rate_limit_per_user( + self, rate_limit_per_user: typing.Union[datetime.timedelta, int] + ) -> "GuildChannelBuilder": + """Set the rate limit for users sending messages in this channel. + + Parameters + ---------- + rate_limit_per_user : :obj:`typing.Union` [ :obj:`datetime.timedelta`, :obj:`int` ] + The amount of seconds users will have to wait before sending another + message in the channel to set. + """ + self._payload["rate_limit_per_user"] = int( + rate_limit_per_user.total_seconds() + if isinstance(rate_limit_per_user, datetime.timedelta) + else rate_limit_per_user + ) + return self + + def with_parent_category(self, category: typing.Union[snowflakes.Snowflake, int]) -> "GuildChannelBuilder": + """Set the parent category for this channel. + + Parameters + ---------- + category : :obj:`typing.Union` [ :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ] + The placeholder ID of the category channel that should be this + channel's parent. + """ + self._payload["parent_id"] = str(int(category)) + return self + + def with_id(self, channel_id: typing.Union[snowflakes.Snowflake, int]) -> "GuildChannelBuilder": + """Set the placeholder ID for this channel. + + Notes + ----- + This ID is purely a place holder used for setting parent category + channels and will have no effect on the created channel's ID. + + Parameters + ---------- + channel_id : :obj:`typing.Union` [ :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ] + The placeholder ID to use. + """ + self._payload["id"] = str(int(channel_id)) + return self + + def deserialize_channel(payload: typing.Dict[str, typing.Any]) -> typing.Union[GuildChannel, DMChannel]: """Deserialize a channel object into the corresponding class. diff --git a/hikari/clients/rest_clients.py b/hikari/clients/rest_clients.py index e34a87eaca..21de40d83c 100644 --- a/hikari/clients/rest_clients.py +++ b/hikari/clients/rest_clients.py @@ -1797,7 +1797,7 @@ async def create_guild( default_message_notifications: typing.Union[guilds.GuildMessageNotificationsLevel, int] = ..., explicit_content_filter: typing.Union[guilds.GuildExplicitContentFilterLevel, int] = ..., roles: typing.Sequence[guilds.GuildRole] = ..., - channels: typing.Sequence[_channels.GuildChannel] = ..., + channels: typing.Sequence[_channels.GuildChannelBuilder] = ..., ) -> guilds.Guild: """Create a new guild. @@ -1827,8 +1827,8 @@ async def create_guild( roles : :obj:`typing.Sequence` [ :obj:`hikari.guilds.GuildRole` ] If specified, an array of role objects to be created alongside the guild. First element changes the ``@everyone`` role. - channels : :obj:`typing.Sequence` [ :obj:`hikari.channels.GuildChannel` ] - If specified, an array of guild channel derived objects to be + channels : :obj:`typing.Sequence` [ :obj:`hikari.channels.GuildChannelBuilder` ] + If specified, an array of guild channel builder objects to be created within the guild. Returns diff --git a/hikari/embeds.py b/hikari/embeds.py index f9e14d5dbd..194ae6fe66 100644 --- a/hikari/embeds.py +++ b/hikari/embeds.py @@ -42,137 +42,209 @@ @marshaller.marshallable() @attr.s(slots=True) class EmbedFooter(entities.HikariEntity, entities.Deserializable, entities.Serializable): - """Represents a embed footer.""" + """Represents an embed footer.""" #: The footer text. #: #: :type: :obj:`str` - text: typing.Optional[str] = marshaller.attrib(deserializer=str, serializer=str) + text: str = marshaller.attrib(deserializer=str, serializer=str) #: The URL of the footer icon. #: #: :type: :obj:`str`, optional - icon_url: typing.Optional[str] = marshaller.attrib(deserializer=str, serializer=str, if_undefined=None) + icon_url: typing.Optional[str] = marshaller.attrib( + deserializer=str, serializer=str, if_undefined=None, default=None + ) #: The proxied URL of the footer icon. #: + #: Note + #: ---- + #: This field cannot be set by bots or webhooks while sending an embed and + #: will be ignored during serialization. + #: + #: #: :type: :obj:`str`, optional - proxy_icon_url: typing.Optional[str] = marshaller.attrib(deserializer=str, serializer=str, if_undefined=None) + proxy_icon_url: typing.Optional[str] = marshaller.attrib( + deserializer=str, transient=True, if_undefined=None, default=None + ) @marshaller.marshallable() @attr.s(slots=True) class EmbedImage(entities.HikariEntity, entities.Deserializable, entities.Serializable): - """Represents a embed image.""" + """Represents an embed image.""" #: The URL of the image. #: #: :type: :obj:`str`, optional - url: typing.Optional[str] = marshaller.attrib(deserializer=str, serializer=str, if_undefined=None) + url: typing.Optional[str] = marshaller.attrib(deserializer=str, serializer=str, if_undefined=None, default=None) #: The proxied URL of the image. #: + #: Note + #: ---- + #: This field cannot be set by bots or webhooks while sending an embed and + #: will be ignored during serialization. + #: + #: #: :type: :obj:`str`, optional - proxy_url: typing.Optional[str] = marshaller.attrib(deserializer=str, serializer=str, if_undefined=None) + proxy_url: typing.Optional[str] = marshaller.attrib( + deserializer=str, transient=True, if_undefined=None, default=None + ) #: The height of the image. #: + #: Note + #: ---- + #: This field cannot be set by bots or webhooks while sending an embed and + #: will be ignored during serialization. + #: + #: #: :type: :obj:`int`, optional - height: typing.Optional[int] = marshaller.attrib(deserializer=int, serializer=int, if_undefined=None) + height: typing.Optional[int] = marshaller.attrib(deserializer=int, transient=True, if_undefined=None, default=None) #: The width of the image. #: + #: Note + #: ---- + #: This field cannot be set by bots or webhooks while sending an embed and + #: will be ignored during serialization. + #: + #: #: :type: :obj:`int`, optional - width: typing.Optional[int] = marshaller.attrib(deserializer=int, serializer=int, if_undefined=None) + width: typing.Optional[int] = marshaller.attrib(deserializer=int, transient=True, if_undefined=None, default=None) @marshaller.marshallable() @attr.s(slots=True) class EmbedThumbnail(entities.HikariEntity, entities.Deserializable, entities.Serializable): - """Represents a embed thumbnail.""" + """Represents an embed thumbnail.""" #: The URL of the thumbnail. #: #: :type: :obj:`str`, optional - url: typing.Optional[str] = marshaller.attrib(deserializer=str, serializer=str, if_undefined=None) + url: typing.Optional[str] = marshaller.attrib(deserializer=str, serializer=str, if_undefined=None, default=None) #: The proxied URL of the thumbnail. #: + #: Note + #: ---- + #: This field cannot be set by bots or webhooks while sending an embed and + #: will be ignored during serialization. + #: + #: #: :type: :obj:`str`, optional - proxy_url: typing.Optional[str] = marshaller.attrib(deserializer=str, serializer=str, if_undefined=None) + proxy_url: typing.Optional[str] = marshaller.attrib( + deserializer=str, transient=True, if_undefined=None, default=None + ) #: The height of the thumbnail. #: + #: Note + #: ---- + #: This field cannot be set by bots or webhooks while sending an embed and + #: will be ignored during serialization. + #: + #: #: :type: :obj:`int`, optional - height: typing.Optional[int] = marshaller.attrib(deserializer=int, serializer=int, if_undefined=None) + height: typing.Optional[int] = marshaller.attrib(deserializer=int, transient=True, if_undefined=None, default=None) #: The width of the thumbnail. #: + #: Note + #: ---- + #: This field cannot be set by bots or webhooks while sending an embed and + #: will be ignored during serialization. + #: + #: #: :type: :obj:`int`, optional - width: typing.Optional[int] = marshaller.attrib(deserializer=int, serializer=int, if_undefined=None) + width: typing.Optional[int] = marshaller.attrib(deserializer=int, transient=True, if_undefined=None, default=None) @marshaller.marshallable() @attr.s(slots=True) -class EmbedVideo(entities.HikariEntity, entities.Deserializable, entities.Serializable): - """Represents a embed video.""" +class EmbedVideo(entities.HikariEntity, entities.Deserializable): + """Represents an embed video. + + Note + ---- + This embed attached object cannot be sent by bots or webhooks while sending + an embed and therefore shouldn't be initiated like the other embed objects. + """ #: The URL of the video. #: #: :type: :obj:`str`, optional - url: typing.Optional[str] = marshaller.attrib(deserializer=str, serializer=str, if_undefined=None) + url: typing.Optional[str] = marshaller.attrib(deserializer=str, if_undefined=None) #: The height of the video. #: #: :type: :obj:`int`, optional - height: typing.Optional[int] = marshaller.attrib(deserializer=int, serializer=int, if_undefined=None) + height: typing.Optional[int] = marshaller.attrib(deserializer=int, if_undefined=None) #: The width of the video. #: #: :type: :obj:`int`, optional - width: typing.Optional[int] = marshaller.attrib(deserializer=int, serializer=int, if_undefined=None) + width: typing.Optional[int] = marshaller.attrib(deserializer=int, if_undefined=None) @marshaller.marshallable() @attr.s(slots=True) -class EmbedProvider(entities.HikariEntity, entities.Deserializable, entities.Serializable): - """Represents a embed provider.""" +class EmbedProvider(entities.HikariEntity, entities.Deserializable): + """Represents an embed provider. + + Note + ---- + This embed attached object cannot be sent by bots or webhooks while sending + an embed and therefore shouldn't be initiated like the other embed objects. + """ #: The name of the provider. #: #: :type: :obj:`str`, optional - name: typing.Optional[str] = marshaller.attrib(deserializer=str, serializer=str, if_undefined=None) + name: typing.Optional[str] = marshaller.attrib(deserializer=str, if_undefined=None) #: The URL of the provider. #: #: :type: :obj:`str`, optional - url: typing.Optional[str] = marshaller.attrib(deserializer=str, serializer=str, if_undefined=None, if_none=None) + url: typing.Optional[str] = marshaller.attrib(deserializer=str, if_undefined=None, if_none=None) @marshaller.marshallable() @attr.s(slots=True) class EmbedAuthor(entities.HikariEntity, entities.Deserializable, entities.Serializable): - """Represents a embed author.""" + """Represents an embed author.""" #: The name of the author. #: #: :type: :obj:`str`, optional - name: typing.Optional[str] = marshaller.attrib(deserializer=str, serializer=str, if_undefined=None) + name: typing.Optional[str] = marshaller.attrib(deserializer=str, serializer=str, if_undefined=None, default=None) #: The URL of the author. #: #: :type: :obj:`str`, optional - url: typing.Optional[str] = marshaller.attrib(deserializer=str, serializer=str, if_undefined=None) + url: typing.Optional[str] = marshaller.attrib(deserializer=str, serializer=str, if_undefined=None, default=None) #: The URL of the author icon. #: #: :type: :obj:`str`, optional - icon_url: typing.Optional[str] = marshaller.attrib(deserializer=str, serializer=str, if_undefined=None) + icon_url: typing.Optional[str] = marshaller.attrib( + deserializer=str, serializer=str, if_undefined=None, default=None + ) #: The proxied URL of the author icon. #: + #: Note + #: ---- + #: This field cannot be set by bots or webhooks while sending an embed and + #: will be ignored during serialization. + #: + #: #: :type: :obj:`str`, optional - proxy_icon_url: typing.Optional[str] = marshaller.attrib(deserializer=str, serializer=str, if_undefined=None) + proxy_icon_url: typing.Optional[str] = marshaller.attrib( + deserializer=str, transient=True, if_undefined=None, default=None + ) @marshaller.marshallable() @@ -193,28 +265,32 @@ class EmbedField(entities.HikariEntity, entities.Deserializable, entities.Serial #: Whether the field should display inline. Defaults to :obj:`False`. #: #: :type: :obj:`bool` - is_inline: bool = marshaller.attrib(raw_name="inline", deserializer=bool, serializer=bool, if_undefined=False) + is_inline: bool = marshaller.attrib( + raw_name="inline", deserializer=bool, serializer=bool, if_undefined=False, default=True + ) @marshaller.marshallable() @attr.s(slots=True) class Embed(entities.HikariEntity, entities.Deserializable, entities.Serializable): - """Represents a embed.""" + """Represents an embed.""" #: The title of the embed. #: #: :type: :obj:`str`, optional - title: typing.Optional[str] = marshaller.attrib(deserializer=str, serializer=str, if_undefined=None) + title: typing.Optional[str] = marshaller.attrib(deserializer=str, serializer=str, if_undefined=None, default=None) #: The description of the embed. #: #: :type: :obj:`str`, optional - description: typing.Optional[str] = marshaller.attrib(deserializer=str, serializer=str, if_undefined=None) + description: typing.Optional[str] = marshaller.attrib( + deserializer=str, serializer=str, if_undefined=None, default=None + ) #: The URL of the embed. #: #: :type: :obj:`str`, optional - url: typing.Optional[str] = marshaller.attrib(deserializer=str, serializer=str, if_undefined=None) + url: typing.Optional[str] = marshaller.attrib(deserializer=str, serializer=str, if_undefined=None, default=None) #: The timestamp of the embed. #: @@ -223,52 +299,65 @@ class Embed(entities.HikariEntity, entities.Deserializable, entities.Serializabl deserializer=hikari.internal.conversions.parse_iso_8601_ts, serializer=lambda timestamp: timestamp.replace(tzinfo=datetime.timezone.utc).isoformat(), if_undefined=None, + default=None, ) color: typing.Optional[colors.Color] = marshaller.attrib( - deserializer=colors.Color, serializer=int, if_undefined=None + deserializer=colors.Color, serializer=int, if_undefined=None, default=None ) #: The footer of the embed. #: #: :type: :obj:`EmbedFooter`, optional footer: typing.Optional[EmbedFooter] = marshaller.attrib( - deserializer=EmbedFooter.deserialize, serializer=EmbedFooter.serialize, if_undefined=None + deserializer=EmbedFooter.deserialize, serializer=EmbedFooter.serialize, if_undefined=None, default=None ) #: The image of the embed. #: #: :type: :obj:`EmbedImage`, optional image: typing.Optional[EmbedImage] = marshaller.attrib( - deserializer=EmbedImage.deserialize, serializer=EmbedImage.serialize, if_undefined=None + deserializer=EmbedImage.deserialize, serializer=EmbedImage.serialize, if_undefined=None, default=None ) #: The thumbnail of the embed. #: #: :type: :obj:`EmbedThumbnail`, optional thumbnail: typing.Optional[EmbedThumbnail] = marshaller.attrib( - deserializer=EmbedThumbnail.deserialize, serializer=EmbedThumbnail.serialize, if_undefined=None + deserializer=EmbedThumbnail.deserialize, serializer=EmbedThumbnail.serialize, if_undefined=None, default=None ) #: The video of the embed. #: + #: Note + #: ---- + #: This field cannot be set by bots or webhooks while sending an embed and + #: will be ignored during serialization. + #: + #: #: :type: :obj:`EmbedVideo`, optional video: typing.Optional[EmbedVideo] = marshaller.attrib( - deserializer=EmbedVideo.deserialize, serializer=EmbedVideo.serialize, if_undefined=None + deserializer=EmbedVideo.deserialize, transient=True, if_undefined=None, default=None, ) #: The provider of the embed. #: + #: Note + #: ---- + #: This field cannot be set by bots or webhooks while sending an embed and + #: will be ignored during serialization. + #: + #: #: :type: :obj:`EmbedProvider`, optional provider: typing.Optional[EmbedProvider] = marshaller.attrib( - deserializer=EmbedProvider.deserialize, serializer=EmbedProvider.serialize, if_undefined=None + deserializer=EmbedProvider.deserialize, transient=True, if_undefined=None, default=None ) #: The author of the embed. #: #: :type: :obj:`EmbedAuthor`, optional author: typing.Optional[EmbedAuthor] = marshaller.attrib( - deserializer=EmbedAuthor.deserialize, serializer=EmbedAuthor.serialize, if_undefined=None + deserializer=EmbedAuthor.deserialize, serializer=EmbedAuthor.serialize, if_undefined=None, default=None ) #: The fields of the embed. @@ -278,4 +367,5 @@ class Embed(entities.HikariEntity, entities.Deserializable, entities.Serializabl deserializer=lambda fields: [EmbedField.deserialize(f) for f in fields], serializer=lambda fields: [f.serialize() for f in fields], if_undefined=None, + default=None, ) diff --git a/hikari/gateway_entities.py b/hikari/gateway_entities.py index 1d0f09917f..81092aab72 100644 --- a/hikari/gateway_entities.py +++ b/hikari/gateway_entities.py @@ -90,11 +90,16 @@ class GatewayActivity(entities.Deserializable, entities.Serializable): #: The activity URL. Only valid for ``STREAMING`` activities. #: #: :type: :obj:`str`, optional - url: typing.Optional[str] = marshaller.attrib(deserializer=str, serializer=str, if_none=None, if_undefined=None) + url: typing.Optional[str] = marshaller.attrib( + deserializer=str, serializer=str, if_none=None, if_undefined=None, default=None + ) #: The activity type. #: #: :type: :obj:`hikari.guilds.ActivityType` type: guilds.ActivityType = marshaller.attrib( - deserializer=guilds.ActivityType, serializer=int, if_undefined=lambda: guilds.ActivityType.PLAYING + deserializer=guilds.ActivityType, + serializer=int, + if_undefined=lambda: guilds.ActivityType.PLAYING, + default=guilds.ActivityType.PLAYING, ) diff --git a/hikari/guilds.py b/hikari/guilds.py index 834f4f6b94..c679ee2791 100644 --- a/hikari/guilds.py +++ b/hikari/guilds.py @@ -255,7 +255,7 @@ class PartialGuildRole(snowflakes.UniqueEntity, entities.Deserializable): #: The role's name. #: #: :type: :obj:`str` - name: str = marshaller.attrib(deserializer=str) + name: str = marshaller.attrib(deserializer=str, serializer=str) @marshaller.marshallable() @@ -267,36 +267,38 @@ class GuildRole(PartialGuildRole, entities.Serializable): #: if it's their top coloured role. #: #: :type: :obj:`hikari.colors.Color` - color: colors.Color = marshaller.attrib(deserializer=colors.Color) + color: colors.Color = marshaller.attrib(deserializer=colors.Color, serializer=int, default=colors.Color(0)) #: Whether this role is hoisting the members it's attached to in the member #: list, members will be hoisted under their highest role where #: :attr:`is_hoisted` is true. #: #: :type: :obj:`bool` - is_hoisted: bool = marshaller.attrib(raw_name="hoist", deserializer=bool) + is_hoisted: bool = marshaller.attrib(raw_name="hoist", deserializer=bool, serializer=bool, default=False) #: The position of this role in the role hierarchy. #: #: :type: :obj:`int` - position: int = marshaller.attrib(deserializer=int) + position: int = marshaller.attrib(deserializer=int, serializer=int, default=None) #: The guild wide permissions this role gives to the members it's attached #: to, may be overridden by channel overwrites. #: #: :type: :obj:`hikari.permissions.Permission` - permissions: _permissions.Permission = marshaller.attrib(deserializer=_permissions.Permission) + permissions: _permissions.Permission = marshaller.attrib( + deserializer=_permissions.Permission, serializer=int, default=_permissions.Permission(0) + ) #: Whether this role is managed by an integration. #: #: :type: :obj:`bool` - is_managed: bool = marshaller.attrib(raw_name="managed", deserializer=bool) + is_managed: bool = marshaller.attrib(raw_name="managed", deserializer=bool, transient=True, default=None) #: Whether this role can be mentioned by all, regardless of the #: ``MENTION_EVERYONE`` permission. #: #: :type: :obj:`bool` - is_mentionable: bool = marshaller.attrib(raw_name="mentionable", deserializer=bool) + is_mentionable: bool = marshaller.attrib(raw_name="mentionable", deserializer=bool, serializer=bool, default=False) @enum.unique diff --git a/tests/hikari/clients/test_rest_client.py b/tests/hikari/clients/test_rest_client.py index dea776077a..96d012669e 100644 --- a/tests/hikari/clients/test_rest_client.py +++ b/tests/hikari/clients/test_rest_client.py @@ -1424,7 +1424,7 @@ async def test_create_guild_with_optionals(self, rest_clients_impl, region): mock_role_obj = mock.MagicMock(guilds.GuildRole) mock_role_obj.serialize = mock.MagicMock(return_value=mock_role_payload) mock_channel_payload = {"type": 2, "name": "aChannel"} - mock_channel_obj = mock.MagicMock(channels.GuildNewsChannel) + mock_channel_obj = mock.MagicMock(channels.GuildChannelBuilder) mock_channel_obj.serialize = mock.MagicMock(return_value=mock_channel_payload) stack = contextlib.ExitStack() stack.enter_context(mock.patch.object(guilds.Guild, "deserialize", return_value=mock_guild_obj)) diff --git a/tests/hikari/test_channels.py b/tests/hikari/test_channels.py index e8879890ec..4ab6fb10a5 100644 --- a/tests/hikari/test_channels.py +++ b/tests/hikari/test_channels.py @@ -23,6 +23,7 @@ from hikari import channels from hikari import permissions +from hikari import snowflakes from hikari import users @@ -149,6 +150,11 @@ def test_deserialize(self, test_partial_channel_payload): assert partial_channel_obj.type is channels.ChannelType.GUILD_TEXT +class TestPermissionOverwriteType: + def test___int__(self): + assert str(channels.PermissionOverwriteType.ROLE) == "role" + + class TestPermissionOverwrite: def test_deserialize(self, test_permission_overwrite_payload): permission_overwrite_obj = channels.PermissionOverwrite.deserialize(test_permission_overwrite_payload) @@ -158,6 +164,21 @@ def test_deserialize(self, test_permission_overwrite_payload): ) assert permission_overwrite_obj.deny == permissions.Permission.EMBED_LINKS | permissions.Permission.ATTACH_FILES + def test_serialize_full_overwrite(self): + permission_overwrite_obj = channels.PermissionOverwrite( + id=snowflakes.Snowflake(11111111), + type=channels.PermissionOverwriteType.ROLE, + allow=permissions.Permission(1321), + deny=permissions.Permission(39939), + ) + assert permission_overwrite_obj.serialize() == {"id": "11111111", "type": "role", "allow": 1321, "deny": 39939} + + def test_serialize_partial_overwrite(self): + permission_overwrite_obj = channels.PermissionOverwrite( + id=snowflakes.Snowflake(11111111), type=channels.PermissionOverwriteType.ROLE, + ) + assert permission_overwrite_obj.serialize() == {"id": "11111111", "type": "role", "allow": 0, "deny": 0} + def test_unset(self): permission_overwrite_obj = channels.PermissionOverwrite( id=None, type=None, allow=permissions.Permission(65), deny=permissions.Permission(49152) @@ -285,6 +306,77 @@ def test_deserialize(self, test_guild_voice_channel_payload, test_permission_ove assert channel_obj.user_limit == 3 +class TestGuildChannelBuilder: + def test___init__(self): + channel_builder_obj = channels.GuildChannelBuilder( + channel_name="A channel", channel_type=channels.ChannelType.GUILD_TEXT + ) + assert channel_builder_obj._payload == {"type": 0, "name": "A channel"} + + def test_is_sfw(self): + channel_builder_obj = channels.GuildChannelBuilder("A channel", channels.ChannelType.GUILD_TEXT).is_nsfw() + assert channel_builder_obj._payload == {"type": 0, "name": "A channel", "nsfw": True} + + def test_with_permission_overwrites(self): + channel_builder_obj = channels.GuildChannelBuilder( + "A channel", channels.ChannelType.GUILD_TEXT + ).with_permission_overwrites( + [channels.PermissionOverwrite(id=1231, type=channels.PermissionOverwriteType.MEMBER)] + ) + assert channel_builder_obj._payload == { + "type": 0, + "name": "A channel", + "permission_overwrites": [{"type": "member", "id": "1231", "allow": 0, "deny": 0}], + } + + def test_with_topic(self): + channel_builder_obj = channels.GuildChannelBuilder("A channel", channels.ChannelType.GUILD_TEXT).with_topic( + "A TOPIC" + ) + assert channel_builder_obj._payload == {"type": 0, "name": "A channel", "topic": "A TOPIC"} + + def test_with_bitrate(self): + channel_builder_obj = channels.GuildChannelBuilder("A channel", channels.ChannelType.GUILD_TEXT).with_bitrate( + 123123 + ) + assert channel_builder_obj._payload == {"type": 0, "name": "A channel", "bitrate": 123123} + + def test_with_user_limit(self): + channel_builder_obj = channels.GuildChannelBuilder( + "A channel", channels.ChannelType.GUILD_TEXT + ).with_user_limit(123123) + assert channel_builder_obj._payload == {"type": 0, "name": "A channel", "user_limit": 123123} + + @pytest.mark.parametrize("rate_limit", [3232, datetime.timedelta(seconds=3232)]) + def test_with_rate_limit_per_user(self, rate_limit): + channel_builder_obj = channels.GuildChannelBuilder( + "A channel", channels.ChannelType.GUILD_TEXT + ).with_rate_limit_per_user(rate_limit) + assert channel_builder_obj._payload == {"type": 0, "name": "A channel", "rate_limit_per_user": 3232} + + @pytest.mark.parametrize( + "category", [54321, snowflakes.Snowflake(54321)], + ) + def test_with_parent_category(self, category): + channel_builder_obj = channels.GuildChannelBuilder( + "A channel", channels.ChannelType.GUILD_TEXT + ).with_parent_category(category) + assert channel_builder_obj._payload == {"type": 0, "name": "A channel", "parent_id": "54321"} + + @pytest.mark.parametrize("placeholder_id", [444444, snowflakes.Snowflake(444444)]) + def test_with_user_limit(self, placeholder_id): + channel_builder_obj = channels.GuildChannelBuilder("A channel", channels.ChannelType.GUILD_TEXT).with_id( + placeholder_id + ) + assert channel_builder_obj._payload == {"type": 0, "name": "A channel", "id": "444444"} + + def test_serialize(self): + mock_payload = {"id": "424242", "name": "aChannel", "type": 4, "nsfw": True} + channel_builder_obj = channels.GuildChannelBuilder("A channel", channels.ChannelType.GUILD_TEXT) + channel_builder_obj._payload = mock_payload + assert channel_builder_obj.serialize() == mock_payload + + def test_deserialize_channel_returns_correct_type( test_dm_channel_payload, test_group_dm_channel_payload, diff --git a/tests/hikari/test_embeds.py b/tests/hikari/test_embeds.py index 5ccaa25a03..effc6ebc02 100644 --- a/tests/hikari/test_embeds.py +++ b/tests/hikari/test_embeds.py @@ -120,10 +120,15 @@ def test_deserialize(self, test_footer_payload): assert footer_obj.icon_url == "https://somewhere.com/footer.png" assert footer_obj.proxy_icon_url == "https://media.somewhere.com/footer.png" - def test_serialize(self, test_footer_payload): - footer_obj = embeds.EmbedFooter.deserialize(test_footer_payload) + def test_serialize_full_footer(self): + footer_obj = embeds.EmbedFooter(text="OK", icon_url="https:////////////",) + + assert footer_obj.serialize() == {"text": "OK", "icon_url": "https:////////////"} - assert footer_obj.serialize() == test_footer_payload + def test_serialize_partial_footer(self): + footer_obj = embeds.EmbedFooter(text="OK",) + + assert footer_obj.serialize() == {"text": "OK"} class TestEmbedImage: @@ -135,10 +140,13 @@ def test_deserialize(self, test_image_payload): assert image_obj.height == 122 assert image_obj.width == 133 - def test_serialize(self, test_image_payload): - image_obj = embeds.EmbedImage.deserialize(test_image_payload) + def test_serialize_full_image(self): + image_obj = embeds.EmbedImage(url="https://///////",) - assert image_obj.serialize() == test_image_payload + assert image_obj.serialize() == {"url": "https://///////"} + + def test_serialize_empty_image(self): + assert embeds.EmbedImage().serialize() == {} class TestEmbedThumbnail: @@ -150,10 +158,13 @@ def test_deserialize(self, test_thumbnail_payload): assert thumbnail_obj.height == 123 assert thumbnail_obj.width == 456 - def test_serialize(self, test_thumbnail_payload): - thumbnail_obj = embeds.EmbedThumbnail.deserialize(test_thumbnail_payload) + def test_serialize_full_thumbnail(self): + thumbnail_obj = embeds.EmbedThumbnail(url="https://somewhere.com/thumbnail.png") - assert thumbnail_obj.serialize() == test_thumbnail_payload + assert thumbnail_obj.serialize() == {"url": "https://somewhere.com/thumbnail.png"} + + def test_serialize_empty_thumbnail(self): + assert embeds.EmbedThumbnail().serialize() == {} class TestEmbedVideo: @@ -164,11 +175,6 @@ def test_deserialize(self, test_video_payload): assert video_obj.height == 1234 assert video_obj.width == 4567 - def test_serialize(self, test_video_payload): - video_obj = embeds.EmbedVideo.deserialize(test_video_payload) - - assert video_obj.serialize() == test_video_payload - class TestEmbedProvider: def test_deserialize(self, test_provider_payload): @@ -177,11 +183,6 @@ def test_deserialize(self, test_provider_payload): assert provider_obj.name == "some name" assert provider_obj.url == "https://somewhere.com/provider" - def test_serialize(self, test_provider_payload): - provider_obj = embeds.EmbedProvider.deserialize(test_provider_payload) - - assert provider_obj.serialize() == test_provider_payload - class TestEmbedAuthor: def test_deserialize(self, test_author_payload): @@ -192,10 +193,19 @@ def test_deserialize(self, test_author_payload): assert author_obj.icon_url == "https://somewhere.com/author.png" assert author_obj.proxy_icon_url == "https://media.somewhere.com/author.png" - def test_serialize(self, test_author_payload): - author_obj = embeds.EmbedAuthor.deserialize(test_author_payload) + def test_serialize_full_author(self): + author_obj = embeds.EmbedAuthor( + name="Author 187", url="https://nyaanyaanyaa", icon_url="https://a-proper-domain" + ) - assert author_obj.serialize() == test_author_payload + assert author_obj.serialize() == { + "name": "Author 187", + "url": "https://nyaanyaanyaa", + "icon_url": "https://a-proper-domain", + } + + def test_serialize_empty_author(self): + assert embeds.EmbedAuthor().serialize() == {} class TestEmbedField: @@ -207,9 +217,9 @@ def test_deserialize(self): assert field_obj.is_inline is False def test_serialize(self, test_field_payload): - field_obj = embeds.EmbedField.deserialize(test_field_payload) + field_obj = embeds.EmbedField(name="NAME", value="nyaa nyaa nyaa", is_inline=True) - assert field_obj.serialize() == test_field_payload + assert field_obj.serialize() == {"name": "NAME", "value": "nyaa nyaa nyaa", "inline": True} class TestEmbed: @@ -248,7 +258,29 @@ def test_deserialize( assert embed_obj.author == embeds.EmbedAuthor.deserialize(test_author_payload) assert embed_obj.fields == [embeds.EmbedField.deserialize(test_field_payload)] - def test_serialize(self, test_embed_payload): - embed_obj = embeds.Embed.deserialize(test_embed_payload) - - assert embed_obj.serialize() == test_embed_payload + def test_serialize(self): + embed_obj = embeds.Embed( + title="Nyaa me pls >////<", + description="Nyan >////<", + url="https://a-url-now", + timestamp=datetime.datetime.fromisoformat("2020-03-22T16:40:39.218000+00:00"), + color=colors.Color(123123), + footer=embeds.EmbedFooter(text="HI"), + image=embeds.EmbedImage(url="https://not-a-url"), + thumbnail=embeds.EmbedThumbnail(url="https://url-a-not"), + author=embeds.EmbedAuthor(name="a name", url="https://a-man"), + fields=[embeds.EmbedField(name="aField", value="agent69", is_inline=True)], + ) + + assert embed_obj.serialize() == { + "title": "Nyaa me pls >////<", + "description": "Nyan >////<", + "url": "https://a-url-now", + "timestamp": "2020-03-22T16:40:39.218000+00:00", + "color": 123123, + "footer": {"text": "HI"}, + "image": {"url": "https://not-a-url"}, + "thumbnail": {"url": "https://url-a-not"}, + "author": {"name": "a name", "url": "https://a-man"}, + "fields": [{"name": "aField", "value": "agent69", "inline": True}], + } diff --git a/tests/hikari/test_gateway_entities.py b/tests/hikari/test_gateway_entities.py index 0d31719d38..825d043dbb 100644 --- a/tests/hikari/test_gateway_entities.py +++ b/tests/hikari/test_gateway_entities.py @@ -22,6 +22,7 @@ import pytest from hikari import gateway_entities +from hikari import guilds from tests.hikari import _helpers @@ -56,3 +57,38 @@ def test_deserialize(self, test_gateway_bot_payload, test_session_start_limit_pa assert gateway_bot_obj.session_start_limit is mock_session_start_limit assert gateway_bot_obj.url == "wss://gateway.discord.gg" assert gateway_bot_obj.shard_count == 1 + + +class TestGatewayActivity: + @pytest.fixture() + def test_gateway_activity_config(self): + return {"name": "Presence me baby", "url": "http://a-url-name", "type": 1} + + def test_deserialize_full_config(self, test_gateway_activity_config): + gateway_activity_obj = gateway_entities.GatewayActivity.deserialize(test_gateway_activity_config) + assert gateway_activity_obj.name == "Presence me baby" + assert gateway_activity_obj.url == "http://a-url-name" + assert gateway_activity_obj.type is guilds.ActivityType.STREAMING + + def test_deserialize_partial_config(self): + gateway_activity_obj = gateway_entities.GatewayActivity.deserialize({"name": "Presence me baby"}) + assert gateway_activity_obj.name == "Presence me baby" + assert gateway_activity_obj.url == None + assert gateway_activity_obj.type is guilds.ActivityType.PLAYING + + def test_serialize_full_activity(self): + gateway_activity_obj = gateway_entities.GatewayActivity( + name="Presence me baby", url="http://a-url-name", type=guilds.ActivityType.STREAMING + ) + assert gateway_activity_obj.serialize() == { + "name": "Presence me baby", + "url": "http://a-url-name", + "type": 1, + } + + def test_serialize_partial_activity(self): + gateway_activity_obj = gateway_entities.GatewayActivity(name="Presence me baby",) + assert gateway_activity_obj.serialize() == { + "name": "Presence me baby", + "type": 0, + } diff --git a/tests/hikari/test_guilds.py b/tests/hikari/test_guilds.py index 768241e499..14ee13c534 100644 --- a/tests/hikari/test_guilds.py +++ b/tests/hikari/test_guilds.py @@ -24,9 +24,11 @@ import hikari.internal.conversions from hikari import channels +from hikari import colors from hikari import emojis from hikari import entities from hikari import guilds +from hikari import permissions from hikari import users from hikari.internal import urls from tests.hikari import _helpers @@ -359,6 +361,37 @@ def test_deserialize(self, test_guild_role_payload): assert guild_role_obj.is_managed is False assert guild_role_obj.is_mentionable is False + def test_serialize_full_role(self): + guild_role_obj = guilds.GuildRole( + name="aRole", + color=colors.Color(444), + is_hoisted=True, + position=42, + permissions=permissions.Permission(69), + is_mentionable=True, + id=123, + ) + assert guild_role_obj.serialize() == { + "name": "aRole", + "color": 444, + "hoist": True, + "position": 42, + "permissions": 69, + "mentionable": True, + "id": "123", + } + + def test_serialize_partial_role(self): + guild_role_obj = guilds.GuildRole(name="aRole", id=123) + assert guild_role_obj.serialize() == { + "name": "aRole", + "color": 0, + "hoist": False, + "permissions": 0, + "mentionable": False, + "id": "123", + } + class TestActivityTimestamps: def test_deserialize(self, test_activity_timestamps_payload): From 1e6d33a8d46d5ebb67898e7efc2198ce87739808 Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Mon, 13 Apr 2020 22:13:21 +0100 Subject: [PATCH 108/922] Closes #290, updates intents docs. --- hikari/net/codes.py | 52 +++++++++++++++++++++++++++++---------------- 1 file changed, 34 insertions(+), 18 deletions(-) diff --git a/hikari/net/codes.py b/hikari/net/codes.py index a8e6b7d5f8..1141924cc0 100644 --- a/hikari/net/codes.py +++ b/hikari/net/codes.py @@ -25,7 +25,7 @@ # Doesnt work correctly with enums, so since this file is all enums, ignore # pylint: disable=no-member class HTTPStatusCode(enum.IntEnum): - """HTTP status codes that a conforming HTTP server should give us on Discord.""" + """HTTP response codes expected from RESTful components.""" CONTINUE = 100 @@ -74,7 +74,8 @@ class GatewayCloseCode(enum.IntEnum): NORMAL_CLOSURE = 1000 #: Discord is not sure what went wrong. Try reconnecting? UNKNOWN_ERROR = 4000 - #: You sent an invalid Gateway opcode or an invalid payload for an opcode. Don't do that! + #: You sent an invalid Gateway opcode or an invalid payload for an opcode. + #: Don't do that! UNKNOWN_OPCODE = 4001 #: You sent an invalid payload to Discord. Don't do that! DECODE_ERROR = 4002 @@ -84,7 +85,8 @@ class GatewayCloseCode(enum.IntEnum): AUTHENTICATION_FAILED = 4004 #: You sent more than one identify payload. Don't do that! ALREADY_AUTHENTICATED = 4005 - #: The sequence sent when resuming the session was invalid. Reconnect and start a new session. + #: The sequence sent when resuming the session was invalid. Reconnect and + #: start a new session. INVALID_SEQ = 4007 #: Woah nelly! You're sending payloads to Discord too quickly. Slow it down! RATE_LIMITED = 4008 @@ -92,14 +94,16 @@ class GatewayCloseCode(enum.IntEnum): SESSION_TIMEOUT = 4009 #: You sent Discord an invalid shard when IDENTIFYing. INVALID_SHARD = 4010 - #: The session would have handled too many guilds - you are required to shard your connection in order to connect. + #: The session would have handled too many guilds - you are required to + #: shard your connection in order to connect. SHARDING_REQUIRED = 4011 #: You sent an invalid version for the gateway. INVALID_VERSION = 4012 - #: You sent an invalid intent for a Gateway Intent. You may have incorrectly calculated the bitwise value. + #: You sent an invalid intent for a Gateway Intent. You may have incorrectly + #: calculated the bitwise value. INVALID_INTENT = 4013 - #: You sent a disallowed intent for a Gateway Intent. You may have tried to specify an intent that you - #: have not enabled or are not whitelisted for. + #: You sent a disallowed intent for a Gateway Intent. You may have tried to + #: specify an intent that you have not enabled or are not whitelisted for. DISALLOWED_INTENT = 4014 def __str__(self) -> str: @@ -137,7 +141,8 @@ class GatewayOpcode(enum.IntEnum): #: Used to notify client they have an invalid session id. INVALID_SESSION = 9 - #: Sent immediately after connecting, contains heartbeat and server debug information. + #: Sent immediately after connecting, contains heartbeat and server debug + #: information. HELLO = 10 #: Sent immediately following a client heartbeat that was received. @@ -220,7 +225,9 @@ class JSONErrorCode(enum.IntEnum): #: #: Note #: ---- - #: You should never expect to receive this in normal API usage as this only applies to user accounts. + #: You should never expect to receive this in normal API usage as this only + #: applies to user accounts. + #: #: This is unlimited for bot accounts. MAX_GUILDS_REACHED = 30_001 @@ -228,7 +235,9 @@ class JSONErrorCode(enum.IntEnum): #: #: Note #: ---- - #: You should never expect to receive this in normal API usage as this only applies to user accounts. + #: You should never expect to receive this in normal API usage as this only + #: applies to user accounts. + #: #: Bots cannot have friends :( . MAX_FRIENDS_REACHED = 30_002 @@ -292,7 +301,8 @@ class JSONErrorCode(enum.IntEnum): #: Note is too long NOTE_IS_TOO_LONG = 50_015 - #: Provided too few or too many messages to delete. Must provide at least 2 and fewer than 100 messages to delete. + #: Provided too few or too many messages to delete. Must provide at least 2 + #: and fewer than 100 messages to delete. INVALID_NUMBER_OF_MESSAGES_TO_DELETE = 50_016 #: A message can only be pinned to the channel it was sent in @@ -336,25 +346,30 @@ class GatewayIntent(enum.IntFlag): This is a bitfield representation of all the categories of event that you wish to receive. - Any events not in an intent category will be fired regardless of what intents you provide. + Any events not in an intent category will be fired regardless of what + intents you provide. Warnings -------- - If you are using the V7 Gateway, you will be REQUIRED to provide some form of intent value when - you connect. Failure to do so may result in immediate termination of the session server-side. + If you are using the V7 Gateway, you will be REQUIRED to provide some form + of intent value when you connect. Failure to do so may result in immediate + termination of the session server-side. Notes ----- - Discord now places limits on certain events you can receive without whitelisting your bot first. On the - ``Bot`` tab in the developer's portal for your bot, you should now have the option to enable functionality + Discord now places limits on certain events you can receive without + whitelisting your bot first. On the ``Bot`` tab in the developer's portal + for your bot, you should now have the option to enable functionality for receiving these events. - If you attempt to request an intent type that you have not whitelisted your bot for, you will be - disconnected on startup with a ``4014`` closure code. + If you attempt to request an intent type that you have not whitelisted + your bot for, you will be disconnected on startup with a ``4014`` closure + code. """ #: Subscribes to the following events: #: * GUILD_CREATE + #: * GUILD_UPDATE #: * GUILD_DELETE #: * GUILD_ROLE_CREATE #: * GUILD_ROLE_UPDATE @@ -413,6 +428,7 @@ class GatewayIntent(enum.IntFlag): #: * MESSAGE_CREATE #: * MESSAGE_UPDATE #: * MESSAGE_DELETE + #: * MESSAGE_BULK GUILD_MESSAGES = 1 << 9 #: Subscribes to the following events: From 30443b3fd6f617b807d99e658549f5a95066cd46 Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Mon, 13 Apr 2020 22:27:45 +0100 Subject: [PATCH 109/922] Closes #293, fixes incorrect user flags and adds VERIFIED_BOT + VERIFIED_BOT_DEVELOPER --- hikari/users.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/hikari/users.py b/hikari/users.py index 8ad5fbffd2..3b750beb05 100644 --- a/hikari/users.py +++ b/hikari/users.py @@ -118,8 +118,10 @@ class UserFlag(enum.IntFlag): HOUSE_BALANCE = 1 << 8 EARLY_SUPPORTER = 1 << 9 TEAM_USER = 1 << 10 - SYSTEM = 1 << 11 - BUG_HUNTER_LEVEL_2 = 1 << 12 + SYSTEM = 1 << 12 + BUG_HUNTER_LEVEL_2 = 1 << 14 + VERIFIED_BOT = 1 << 16 + VERIFIED_BOT_DEVELOPER = 1 << 17 @enum.unique From caaa887bc1a55fe79947e630cf388734650d71f4 Mon Sep 17 00:00:00 2001 From: davfsa Date: Tue, 14 Apr 2020 10:11:51 +0200 Subject: [PATCH 110/922] Implement stateless_event_manager.py --- hikari/events.py | 113 ++--- hikari/state/stateless_event_managers.py | 210 +++++++++ .../state/test_stateless_event_managers.py | 415 ++++++++++++++++++ tests/hikari/test_events.py | 2 +- 4 files changed, 658 insertions(+), 82 deletions(-) create mode 100644 tests/hikari/state/test_stateless_event_managers.py diff --git a/hikari/events.py b/hikari/events.py index 56cf737cb4..9009cb2002 100644 --- a/hikari/events.py +++ b/hikari/events.py @@ -66,7 +66,6 @@ ] import datetime -import re import typing import attr @@ -83,7 +82,6 @@ from hikari import users from hikari import voices from hikari.clients import shard_clients -from hikari.internal import assertions from hikari.internal import conversions from hikari.internal import marshaller @@ -118,6 +116,7 @@ class ExceptionEvent(HikariEvent): callback: typing.Callable[[HikariEvent], typing.Awaitable[None]] +# Synthetic event, is not deserialized @attr.attrs(slots=True, auto_attribs=True) class StartingEvent(HikariEvent): """Event that is fired before the gateway client starts all shards.""" @@ -141,21 +140,6 @@ class StoppedEvent(HikariEvent): """Event that is fired when the gateway client has finished disconnecting all shards.""" -_websocket_name_break = re.compile(r"(?<=[a-z])(?=[A-Z])") - - -# TODO: remove this, it is unused. -def mark_as_websocket_event(cls): - """Mark the event as being a websocket one.""" - name = cls.__name__ - assertions.assert_that(name.endswith("Event"), "expected name to be Event") - name = name[: -len("Event")] - raw_name = _websocket_name_break.sub("_", name).upper() - cls.___raw_ws_event_name___ = raw_name - return cls - - -@mark_as_websocket_event @attr.s(slots=True, kw_only=True, auto_attribs=True) class ConnectedEvent(HikariEvent, entities.Deserializable): """Event invoked each time a shard connects.""" @@ -166,7 +150,6 @@ class ConnectedEvent(HikariEvent, entities.Deserializable): shard: shard_clients.ShardClient -@mark_as_websocket_event @attr.s(slots=True, kw_only=True, auto_attribs=True) class DisconnectedEvent(HikariEvent, entities.Deserializable): """Event invoked each time a shard disconnects.""" @@ -177,13 +160,22 @@ class DisconnectedEvent(HikariEvent, entities.Deserializable): shard: shard_clients.ShardClient -@mark_as_websocket_event +@attr.s(slots=True, kw_only=True, auto_attribs=True) +class ResumedEvent(HikariEvent): + """Represents a gateway Resume event.""" + + #: The shard that reconnected. + #: + #: :type: :obj:`hikari.clients.shard_clients.ShardClient` + shard: shard_clients.ShardClient + + @marshaller.marshallable() @attr.s(slots=True) class ReadyEvent(HikariEvent, entities.Deserializable): - """Used to represent the gateway ready event. + """Represents the gateway Ready event. - This is received when IDENTIFYing with the gateway and on reconnect. + This is received only when IDENTIFYing with the gateway. """ #: The gateway version this is currently connected to. @@ -234,13 +226,6 @@ def shard_count(self) -> typing.Optional[int]: return self._shard_information[1] if self._shard_information else None -@mark_as_websocket_event -@marshaller.marshallable() -@attr.s(slots=True) -class ResumedEvent(HikariEvent): - """Represents a gateway Resume event.""" - - @marshaller.marshallable() @attr.s(slots=True) class BaseChannelEvent(HikariEvent, snowflakes.UniqueEntity, entities.Deserializable): @@ -360,7 +345,6 @@ class BaseChannelEvent(HikariEvent, snowflakes.UniqueEntity, entities.Deserializ ) -@mark_as_websocket_event @marshaller.marshallable() @attr.s(slots=True) class ChannelCreateEvent(BaseChannelEvent): @@ -371,21 +355,18 @@ class ChannelCreateEvent(BaseChannelEvent): """ -@mark_as_websocket_event @marshaller.marshallable() @attr.s(slots=True) class ChannelUpdateEvent(BaseChannelEvent): """Represents Channel Update gateway events.""" -@mark_as_websocket_event @marshaller.marshallable() @attr.s(slots=True) class ChannelDeleteEvent(BaseChannelEvent): """Represents Channel Delete gateway events.""" -@mark_as_websocket_event @marshaller.marshallable() @attr.s(slots=True) class ChannelPinUpdateEvent(HikariEvent, entities.Deserializable): @@ -417,7 +398,6 @@ class ChannelPinUpdateEvent(HikariEvent, entities.Deserializable): ) -@mark_as_websocket_event @marshaller.marshallable() @attr.s(slots=True) class GuildCreateEvent(HikariEvent, entities.Deserializable): @@ -428,14 +408,12 @@ class GuildCreateEvent(HikariEvent, entities.Deserializable): """ -@mark_as_websocket_event @marshaller.marshallable() @attr.s(slots=True) class GuildUpdateEvent(HikariEvent, entities.Deserializable): """Used to represent Guild Update gateway events.""" -@mark_as_websocket_event @marshaller.marshallable() @attr.s(slots=True) class GuildLeaveEvent(HikariEvent, snowflakes.UniqueEntity, entities.Deserializable): @@ -447,7 +425,6 @@ class GuildLeaveEvent(HikariEvent, snowflakes.UniqueEntity, entities.Deserializa """ -@mark_as_websocket_event @marshaller.marshallable() @attr.s(slots=True) class GuildUnavailableEvent(HikariEvent, snowflakes.UniqueEntity, entities.Deserializable): @@ -475,21 +452,18 @@ class BaseGuildBanEvent(HikariEvent, entities.Deserializable): user: users.User = marshaller.attrib(deserializer=users.User.deserialize) -@mark_as_websocket_event @marshaller.marshallable() @attr.s(slots=True) class GuildBanAddEvent(BaseGuildBanEvent): """Used to represent a Guild Ban Add gateway event.""" -@mark_as_websocket_event @marshaller.marshallable() @attr.s(slots=True) class GuildBanRemoveEvent(BaseGuildBanEvent): """Used to represent a Guild Ban Remove gateway event.""" -@mark_as_websocket_event @marshaller.marshallable() @attr.s(slots=True) class GuildEmojisUpdateEvent(HikariEvent, entities.Deserializable): @@ -508,7 +482,6 @@ class GuildEmojisUpdateEvent(HikariEvent, entities.Deserializable): ) -@mark_as_websocket_event @marshaller.marshallable() @attr.s(slots=True) class GuildIntegrationsUpdateEvent(HikariEvent, entities.Deserializable): @@ -520,7 +493,6 @@ class GuildIntegrationsUpdateEvent(HikariEvent, entities.Deserializable): guild_id: snowflakes.Snowflake = marshaller.attrib(deserializer=snowflakes.Snowflake.deserialize) -@mark_as_websocket_event @marshaller.marshallable() @attr.s(slots=True) class GuildMemberAddEvent(HikariEvent, guilds.GuildMember): @@ -532,27 +504,6 @@ class GuildMemberAddEvent(HikariEvent, guilds.GuildMember): guild_id: snowflakes.Snowflake = marshaller.attrib(deserializer=snowflakes.Snowflake.deserialize) -@mark_as_websocket_event -@marshaller.marshallable() -@attr.s(slots=True) -class GuildMemberRemoveEvent(HikariEvent, entities.Deserializable): - """Used to represent Guild Member Remove gateway events. - - Sent when a member is kicked, banned or leaves a guild. - """ - - #: The ID of the guild this user was removed from. - #: - #: :type: :obj:`hikari.snowflakes.Snowflake` - guild_id: snowflakes.Snowflake = marshaller.attrib(deserializer=snowflakes.Snowflake.deserialize) - - #: The object of the user who was removed from this guild. - #: - #: :type: :obj:`hikari.users.User` - user: users.User = marshaller.attrib(deserializer=users.User.deserialize) - - -@mark_as_websocket_event @marshaller.marshallable() @attr.s(slots=True) class GuildMemberUpdateEvent(HikariEvent, entities.Deserializable): @@ -595,7 +546,25 @@ class GuildMemberUpdateEvent(HikariEvent, entities.Deserializable): ) -@mark_as_websocket_event +@marshaller.marshallable() +@attr.s(slots=True) +class GuildMemberRemoveEvent(HikariEvent, entities.Deserializable): + """Used to represent Guild Member Remove gateway events. + + Sent when a member is kicked, banned or leaves a guild. + """ + + #: The ID of the guild this user was removed from. + #: + #: :type: :obj:`hikari.snowflakes.Snowflake` + guild_id: snowflakes.Snowflake = marshaller.attrib(deserializer=snowflakes.Snowflake.deserialize) + + #: The object of the user who was removed from this guild. + #: + #: :type: :obj:`hikari.users.User` + user: users.User = marshaller.attrib(deserializer=users.User.deserialize) + + @marshaller.marshallable() @attr.s(slots=True) class GuildRoleCreateEvent(HikariEvent, entities.Deserializable): @@ -612,7 +581,6 @@ class GuildRoleCreateEvent(HikariEvent, entities.Deserializable): role: guilds.GuildRole = marshaller.attrib(deserializer=guilds.GuildRole.deserialize) -@mark_as_websocket_event @marshaller.marshallable() @attr.s(slots=True) class GuildRoleUpdateEvent(HikariEvent, entities.Deserializable): @@ -629,7 +597,6 @@ class GuildRoleUpdateEvent(HikariEvent, entities.Deserializable): role: guilds.GuildRole = marshaller.attrib(deserializer=guilds.GuildRole.deserialize) -@mark_as_websocket_event @marshaller.marshallable() @attr.s(slots=True) class GuildRoleDeleteEvent(HikariEvent, entities.Deserializable): @@ -646,7 +613,6 @@ class GuildRoleDeleteEvent(HikariEvent, entities.Deserializable): role_id: snowflakes.Snowflake = marshaller.attrib(deserializer=snowflakes.Snowflake.deserialize) -@mark_as_websocket_event @marshaller.marshallable() @attr.s(slots=True) class InviteCreateEvent(HikariEvent, entities.Deserializable): @@ -717,7 +683,6 @@ class InviteCreateEvent(HikariEvent, entities.Deserializable): uses: int = marshaller.attrib(deserializer=int) -@mark_as_websocket_event @marshaller.marshallable() @attr.s(slots=True) class InviteDeleteEvent(HikariEvent, entities.Deserializable): @@ -745,7 +710,6 @@ class InviteDeleteEvent(HikariEvent, entities.Deserializable): ) -@mark_as_websocket_event @marshaller.marshallable() @attr.s(slots=True) class MessageCreateEvent(HikariEvent, messages.Message): @@ -753,7 +717,6 @@ class MessageCreateEvent(HikariEvent, messages.Message): # This is an arbitrarily partial version of `messages.Message` -@mark_as_websocket_event @marshaller.marshallable() @attr.s(slots=True) class MessageUpdateEvent(HikariEvent, snowflakes.UniqueEntity, entities.Deserializable): @@ -927,7 +890,6 @@ class MessageUpdateEvent(HikariEvent, snowflakes.UniqueEntity, entities.Deserial ) -@mark_as_websocket_event @marshaller.marshallable() @attr.s(slots=True) class MessageDeleteEvent(HikariEvent, entities.Deserializable): @@ -954,7 +916,6 @@ class MessageDeleteEvent(HikariEvent, entities.Deserializable): message_id: snowflakes.Snowflake = marshaller.attrib(raw_name="id", deserializer=snowflakes.Snowflake.deserialize) -@mark_as_websocket_event @marshaller.marshallable() @attr.s(slots=True) class MessageDeleteBulkEvent(HikariEvent, entities.Deserializable): @@ -984,7 +945,6 @@ class MessageDeleteBulkEvent(HikariEvent, entities.Deserializable): ) -@mark_as_websocket_event @marshaller.marshallable() @attr.s(slots=True) class MessageReactionAddEvent(HikariEvent, entities.Deserializable): @@ -1029,7 +989,6 @@ class MessageReactionAddEvent(HikariEvent, entities.Deserializable): ) -@mark_as_websocket_event @marshaller.marshallable() @attr.s(slots=True) class MessageReactionRemoveEvent(HikariEvent, entities.Deserializable): @@ -1066,7 +1025,6 @@ class MessageReactionRemoveEvent(HikariEvent, entities.Deserializable): ) -@mark_as_websocket_event @marshaller.marshallable() @attr.s(slots=True) class MessageReactionRemoveAllEvent(HikariEvent, entities.Deserializable): @@ -1093,7 +1051,6 @@ class MessageReactionRemoveAllEvent(HikariEvent, entities.Deserializable): ) -@mark_as_websocket_event @marshaller.marshallable() @attr.s(slots=True) class MessageReactionRemoveEmojiEvent(HikariEvent, entities.Deserializable): @@ -1127,7 +1084,6 @@ class MessageReactionRemoveEmojiEvent(HikariEvent, entities.Deserializable): ) -@mark_as_websocket_event @marshaller.marshallable() @attr.s(slots=True) class PresenceUpdateEvent(HikariEvent, guilds.GuildMemberPresence): @@ -1137,7 +1093,6 @@ class PresenceUpdateEvent(HikariEvent, guilds.GuildMemberPresence): """ -@mark_as_websocket_event @marshaller.marshallable() @attr.s(slots=True) class TypingStartEvent(HikariEvent, entities.Deserializable): @@ -1180,7 +1135,6 @@ class TypingStartEvent(HikariEvent, entities.Deserializable): ) -@mark_as_websocket_event @marshaller.marshallable() @attr.s(slots=True) class UserUpdateEvent(HikariEvent, users.MyUser): @@ -1190,7 +1144,6 @@ class UserUpdateEvent(HikariEvent, users.MyUser): """ -@mark_as_websocket_event @marshaller.marshallable() @attr.s(slots=True) class VoiceStateUpdateEvent(HikariEvent, voices.VoiceState): @@ -1200,7 +1153,6 @@ class VoiceStateUpdateEvent(HikariEvent, voices.VoiceState): """ -@mark_as_websocket_event @marshaller.marshallable() @attr.s(slots=True) class VoiceServerUpdateEvent(HikariEvent, entities.Deserializable): @@ -1226,7 +1178,6 @@ class VoiceServerUpdateEvent(HikariEvent, entities.Deserializable): endpoint: str = marshaller.attrib(deserializer=str) -@mark_as_websocket_event @marshaller.marshallable() @attr.s(slots=True) class WebhookUpdateEvent(HikariEvent, entities.Deserializable): diff --git a/hikari/state/stateless_event_managers.py b/hikari/state/stateless_event_managers.py index b8e3584308..d2e06e144c 100644 --- a/hikari/state/stateless_event_managers.py +++ b/hikari/state/stateless_event_managers.py @@ -44,3 +44,213 @@ def on_disconnect(self, shard, _): """Handle DISCONNECTED events.""" event = events.DisconnectedEvent(shard=shard) self.event_dispatcher.dispatch_event(event) + + @event_managers.raw_event_mapper("RESUME") + def on_resume(self, shard, _): + """Handle RESUME events.""" + event = events.ResumedEvent(shard=shard) + self.event_dispatcher.dispatch_event(event) + + @event_managers.raw_event_mapper("READY") + def on_ready(self, _, payload): + """Handle READY events.""" + event = events.ReadyEvent.deserialize(payload) + self.event_dispatcher.dispatch_event(event) + + @event_managers.raw_event_mapper("CHANNEL_CREATE") + def on_channel_create(self, _, payload): + """Handle CHANNEL_CREATE events.""" + event = events.ChannelCreateEvent.deserialize(payload) + self.event_dispatcher.dispatch_event(event) + + @event_managers.raw_event_mapper("CHANNEL_UPDATE") + def on_channel_update(self, _, payload): + """Handle CHANNEL_UPDATE events.""" + event = events.ChannelUpdateEvent.deserialize(payload) + self.event_dispatcher.dispatch_event(event) + + @event_managers.raw_event_mapper("CHANNEL_DELETE") + def on_channel_delete(self, _, payload): + """Handle CHANNEL_DELETE events.""" + event = events.ChannelDeleteEvent.deserialize(payload) + self.event_dispatcher.dispatch_event(event) + + @event_managers.raw_event_mapper("CHANNEL_PIN_UPDATE") + def on_channel_pin_update(self, _, payload): + """Handle CHANNEL_PIN_UPDATE events.""" + event = events.ChannelPinUpdateEvent.deserialize(payload) + self.event_dispatcher.dispatch_event(event) + + @event_managers.raw_event_mapper("GUILD_CREATE") + def on_guild_create(self, _, payload): + """Handle GUILD_CREATE events.""" + event = events.GuildCreateEvent.deserialize(payload) + self.event_dispatcher.dispatch_event(event) + + @event_managers.raw_event_mapper("GUILD_UPDATE") + def on_guild_update(self, _, payload): + """Handle GUILD_UPDATE events.""" + event = events.GuildUpdateEvent.deserialize(payload) + self.event_dispatcher.dispatch_event(event) + + @event_managers.raw_event_mapper("GUILD_LEAVE") + def on_guild_leave(self, _, payload): + """Handle GUILD_LEAVE events.""" + event = events.GuildLeaveEvent.deserialize(payload) + self.event_dispatcher.dispatch_event(event) + + @event_managers.raw_event_mapper("GUILD_UNAVAILABLE") + def on_guild_unavailable(self, _, payload): + """Handle GUILD_UNAVAILABLE events.""" + event = events.GuildUnavailableEvent.deserialize(payload) + self.event_dispatcher.dispatch_event(event) + + @event_managers.raw_event_mapper("GUILD_BAN_ADD") + def on_guild_ban_add(self, _, payload): + """Handle GUILD_BAN_ADD events.""" + event = events.GuildBanAddEvent.deserialize(payload) + self.event_dispatcher.dispatch_event(event) + + @event_managers.raw_event_mapper("GUILD_BAN_REMOVE") + def on_guild_ban_remove(self, _, payload): + """Handle GUILD_BAN_REMOVE events.""" + event = events.GuildBanRemoveEvent.deserialize(payload) + self.event_dispatcher.dispatch_event(event) + + @event_managers.raw_event_mapper("GUILD_EMOJIS_UPDATE") + def on_guild_emojis_update(self, _, payload): + """Handle GUILD_EMOJIS_UPDATE events.""" + event = events.GuildEmojisUpdateEvent.deserialize(payload) + self.event_dispatcher.dispatch_event(event) + + @event_managers.raw_event_mapper("GUILD_INTEGRATIONS_UPDATE") + def on_guild_integrations_update(self, _, payload): + """Handle GUILD_INTEGRATIONS_UPDATE events.""" + event = events.GuildIntegrationsUpdateEvent.deserialize(payload) + self.event_dispatcher.dispatch_event(event) + + @event_managers.raw_event_mapper("GUILD_MEMBER_ADD") + def on_guild_member_add(self, _, payload): + """Handle GUILD_MEMBER_ADD events.""" + event = events.GuildMemberAddEvent.deserialize(payload) + self.event_dispatcher.dispatch_event(event) + + @event_managers.raw_event_mapper("GUILD_MEMBER_UPDATE") + def on_guild_member_update(self, _, payload): + """Handle GUILD_MEMBER_UPDATE events.""" + event = events.GuildMemberUpdateEvent.deserialize(payload) + self.event_dispatcher.dispatch_event(event) + + @event_managers.raw_event_mapper("GUILD_MEMBER_REMOVE") + def on_guild_member_remove(self, _, payload): + """Handle GUILD_MEMBER_REMOVE events.""" + event = events.GuildMemberRemoveEvent.deserialize(payload) + self.event_dispatcher.dispatch_event(event) + + @event_managers.raw_event_mapper("GUILD_ROLE_CREATE") + def on_guild_role_create(self, _, payload): + """Handle GUILD_ROLE_CREATE events.""" + event = events.GuildRoleCreateEvent.deserialize(payload) + self.event_dispatcher.dispatch_event(event) + + @event_managers.raw_event_mapper("GUILD_ROLE_UPDATE") + def on_guild_role_update(self, _, payload): + """Handle GUILD_ROLE_UPDATE events.""" + event = events.GuildRoleUpdateEvent.deserialize(payload) + self.event_dispatcher.dispatch_event(event) + + @event_managers.raw_event_mapper("GUILD_ROLE_DELETE") + def on_guild_role_delete(self, _, payload): + """Handle GUILD_ROLE_DELETE events.""" + event = events.GuildRoleDeleteEvent.deserialize(payload) + self.event_dispatcher.dispatch_event(event) + + @event_managers.raw_event_mapper("INVITE_CREATE") + def on_invite_create(self, _, payload): + """Handle INVITE_CREATE events.""" + event = events.InviteCreateEvent.deserialize(payload) + self.event_dispatcher.dispatch_event(event) + + @event_managers.raw_event_mapper("INVITE_DELETE") + def on_invite_delete(self, _, payload): + """Handle INVITE_DELETE events.""" + event = events.InviteDeleteEvent.deserialize(payload) + self.event_dispatcher.dispatch_event(event) + + @event_managers.raw_event_mapper("MESSAGE_CREATE") + def on_message_create(self, _, payload): + """Handle MESSAGE_CREATE events.""" + event = events.MessageCreateEvent.deserialize(payload) + self.event_dispatcher.dispatch_event(event) + + @event_managers.raw_event_mapper("MESSAGE_UPDATE") + def on_message_update(self, _, payload): + """Handle MESSAGE_UPDATE events.""" + event = events.MessageUpdateEvent.deserialize(payload) + self.event_dispatcher.dispatch_event(event) + + @event_managers.raw_event_mapper("MESSAGE_DELETE") + def on_message_delete(self, _, payload): + """Handle MESSAGE_DELETE events.""" + event = events.MessageDeleteEvent.deserialize(payload) + self.event_dispatcher.dispatch_event(event) + + @event_managers.raw_event_mapper("MESSAGE_DELETE_BULK") + def on_message_delete_bulk(self, _, payload): + """Handle MESSAGE_DELETE_BULK events.""" + event = events.MessageDeleteBulkEvent.deserialize(payload) + self.event_dispatcher.dispatch_event(event) + + @event_managers.raw_event_mapper("MESSAGE_REACTION_ADD") + def on_message_reaction_add(self, _, payload): + """Handle MESSAGE_REACTION_ADD events.""" + event = events.MessageReactionAddEvent.deserialize(payload) + self.event_dispatcher.dispatch_event(event) + + @event_managers.raw_event_mapper("MESSAGE_REACTION_REMOVE") + def on_message_reaction_remove(self, _, payload): + """Handle MESSAGE_REACTION_REMOVE events.""" + event = events.MessageReactionRemoveEvent.deserialize(payload) + self.event_dispatcher.dispatch_event(event) + + @event_managers.raw_event_mapper("MESSAGE_REACTION_REMOVE_EMOJI") + def on_message_reaction_remove_emoji(self, _, payload): + """Handle MESSAGE_REACTION_REMOVE_EMOJI events.""" + event = events.MessageReactionRemoveEmojiEvent.deserialize(payload) + self.event_dispatcher.dispatch_event(event) + + @event_managers.raw_event_mapper("PRESENCE_UPDATE") + def on_presence_update(self, _, payload): + """Handle PRESENCE_UPDATE events.""" + event = events.PresenceUpdateEvent.deserialize(payload) + self.event_dispatcher.dispatch_event(event) + + @event_managers.raw_event_mapper("TYPING_START") + def on_typing_start(self, _, payload): + """Handle TYPING_START events.""" + event = events.TypingStartEvent.deserialize(payload) + self.event_dispatcher.dispatch_event(event) + + @event_managers.raw_event_mapper("USER_UPDATE") + def on_user_update(self, _, payload): + """Handle USER_UPDATE events.""" + event = events.UserUpdateEvent.deserialize(payload) + self.event_dispatcher.dispatch_event(event) + + @event_managers.raw_event_mapper("VOICE_STATE_UPDATE") + def on_voice_state_update(self, _, payload): + """Handle VOICE_STATE_UPDATE events.""" + event = events.VoiceStateUpdateEvent.deserialize(payload) + self.event_dispatcher.dispatch_event(event) + + @event_managers.raw_event_mapper("VOICE_SERVER_UPDATE") + def on_voice_server_update(self, _, payload): + """Handle VOICE_SERVER_UPDATE events.""" + event = events.VoiceStateUpdateEvent.deserialize(payload) + self.event_dispatcher.dispatch_event(event) + + @event_managers.raw_event_mapper("WEBHOOK_UPDATE") + def on_webhook_update(self, _, payload): + """Handle WEBHOOK_UPDATE events.""" + event = events.WebhookUpdateEvent.deserialize(payload) + self.event_dispatcher.dispatch_event(event) diff --git a/tests/hikari/state/test_stateless_event_managers.py b/tests/hikari/state/test_stateless_event_managers.py new file mode 100644 index 0000000000..b7fdc3cdad --- /dev/null +++ b/tests/hikari/state/test_stateless_event_managers.py @@ -0,0 +1,415 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# Copyright © Nekoka.tt 2019-2020 +# +# This file is part of Hikari. +# +# Hikari is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Hikari is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Hikari. If not, see . +from unittest import mock + +import pytest + +from hikari import events +from hikari.clients import shard_clients +from hikari.state import stateless_event_managers +from tests.hikari import _helpers + + +class TestStatelessEventManagerImpl: + @pytest.fixture + def mock_payload(self): + return {} + + @pytest.fixture + def event_manager_impl(self): + class MockDispatcher: + dispatch_event = mock.MagicMock() + + return stateless_event_managers.StatelessEventManagerImpl(event_dispatcher_impl=MockDispatcher()) + + @pytest.fixture + def mock_shard(self): + return mock.MagicMock(shard_clients.ShardClient) + + def test_on_connect(self, event_manager_impl, mock_shard): + mock_event = mock.MagicMock(events.ConnectedEvent) + + with mock.patch("hikari.events.ConnectedEvent", return_value=mock_event) as event: + event_manager_impl.on_connect(mock_shard, {}) + + assert event_manager_impl.on_connect.___event_name___ == {"CONNECTED"} + event.assert_called_once_with(shard=mock_shard) + event_manager_impl.event_dispatcher.dispatch_event.assert_called_once_with(mock_event) + + def test_on_disconnect(self, event_manager_impl, mock_shard): + mock_event = mock.MagicMock(events.DisconnectedEvent) + + with mock.patch("hikari.events.DisconnectedEvent", return_value=mock_event) as event: + event_manager_impl.on_disconnect(mock_shard, {}) + + assert event_manager_impl.on_disconnect.___event_name___ == {"DISCONNECTED"} + event.assert_called_once_with(shard=mock_shard) + event_manager_impl.event_dispatcher.dispatch_event.assert_called_once_with(mock_event) + + def test_on_resume(self, event_manager_impl, mock_shard): + mock_event = mock.MagicMock(events.ResumedEvent) + + with mock.patch("hikari.events.ResumedEvent", return_value=mock_event) as event: + event_manager_impl.on_resume(mock_shard, {}) + + assert event_manager_impl.on_resume.___event_name___ == {"RESUME"} + event.assert_called_once_with(shard=mock_shard) + event_manager_impl.event_dispatcher.dispatch_event.assert_called_once_with(mock_event) + + def test_on_ready(self, event_manager_impl, mock_payload): + mock_event = mock.MagicMock(events.ReadyEvent) + + with mock.patch("hikari.events.ReadyEvent.deserialize", return_value=mock_event) as event: + event_manager_impl.on_ready(None, mock_payload) + + assert event_manager_impl.on_ready.___event_name___ == {"READY"} + event.assert_called_once_with(mock_payload) + event_manager_impl.event_dispatcher.dispatch_event.assert_called_once_with(mock_event) + + def test_on_channel_create(self, event_manager_impl, mock_payload): + mock_event = mock.MagicMock(events.ChannelCreateEvent) + + with mock.patch("hikari.events.ChannelCreateEvent.deserialize", return_value=mock_event) as event: + event_manager_impl.on_channel_create(None, mock_payload) + + assert event_manager_impl.on_channel_create.___event_name___ == {"CHANNEL_CREATE"} + event.assert_called_once_with(mock_payload) + event_manager_impl.event_dispatcher.dispatch_event.assert_called_once_with(mock_event) + + def test_on_channel_update(self, event_manager_impl, mock_payload): + mock_event = mock.MagicMock(events.ChannelUpdateEvent) + + with mock.patch("hikari.events.ChannelUpdateEvent.deserialize", return_value=mock_event) as event: + event_manager_impl.on_channel_update(None, mock_payload) + + assert event_manager_impl.on_channel_update.___event_name___ == {"CHANNEL_UPDATE"} + event.assert_called_once_with(mock_payload) + event_manager_impl.event_dispatcher.dispatch_event.assert_called_once_with(mock_event) + + def test_on_channel_delete(self, event_manager_impl, mock_payload): + mock_event = mock.MagicMock(events.ChannelDeleteEvent) + + with mock.patch("hikari.events.ChannelDeleteEvent.deserialize", return_value=mock_event) as event: + event_manager_impl.on_channel_delete(None, mock_payload) + + assert event_manager_impl.on_channel_delete.___event_name___ == {"CHANNEL_DELETE"} + event.assert_called_once_with(mock_payload) + event_manager_impl.event_dispatcher.dispatch_event.assert_called_once_with(mock_event) + + def test_on_channel_pin_update(self, event_manager_impl, mock_payload): + mock_event = mock.MagicMock(events.ChannelPinUpdateEvent) + + with mock.patch("hikari.events.ChannelPinUpdateEvent.deserialize", return_value=mock_event) as event: + event_manager_impl.on_channel_pin_update(None, mock_payload) + + assert event_manager_impl.on_channel_pin_update.___event_name___ == {"CHANNEL_PIN_UPDATE"} + event.assert_called_once_with(mock_payload) + event_manager_impl.event_dispatcher.dispatch_event.assert_called_once_with(mock_event) + + def test_on_guild_create(self, event_manager_impl, mock_payload): + mock_event = mock.MagicMock(events.GuildCreateEvent) + + with mock.patch("hikari.events.GuildCreateEvent.deserialize", return_value=mock_event) as event: + event_manager_impl.on_guild_create(None, mock_payload) + + assert event_manager_impl.on_guild_create.___event_name___ == {"GUILD_CREATE"} + event.assert_called_once_with(mock_payload) + event_manager_impl.event_dispatcher.dispatch_event.assert_called_once_with(mock_event) + + def test_on_guild_update(self, event_manager_impl, mock_payload): + mock_event = mock.MagicMock(events.GuildUpdateEvent) + + with mock.patch("hikari.events.GuildUpdateEvent.deserialize", return_value=mock_event) as event: + event_manager_impl.on_guild_update(None, mock_payload) + + assert event_manager_impl.on_guild_update.___event_name___ == {"GUILD_UPDATE"} + event.assert_called_once_with(mock_payload) + event_manager_impl.event_dispatcher.dispatch_event.assert_called_once_with(mock_event) + + def test_on_guild_leave(self, event_manager_impl, mock_payload): + mock_event = mock.MagicMock(events.GuildLeaveEvent) + + with mock.patch("hikari.events.GuildLeaveEvent.deserialize", return_value=mock_event) as event: + event_manager_impl.on_guild_leave(None, mock_payload) + + assert event_manager_impl.on_guild_leave.___event_name___ == {"GUILD_LEAVE"} + event.assert_called_once_with(mock_payload) + event_manager_impl.event_dispatcher.dispatch_event.assert_called_once_with(mock_event) + + def test_on_guild_unavailable(self, event_manager_impl, mock_payload): + mock_event = mock.MagicMock(events.GuildUnavailableEvent) + + with mock.patch("hikari.events.GuildUnavailableEvent.deserialize", return_value=mock_event) as event: + event_manager_impl.on_guild_unavailable(None, mock_payload) + + assert event_manager_impl.on_guild_unavailable.___event_name___ == {"GUILD_UNAVAILABLE"} + event.assert_called_once_with(mock_payload) + event_manager_impl.event_dispatcher.dispatch_event.assert_called_once_with(mock_event) + + def test_on_guild_ban_add(self, event_manager_impl, mock_payload): + mock_event = mock.MagicMock(events.GuildBanAddEvent) + + with mock.patch("hikari.events.GuildBanAddEvent.deserialize", return_value=mock_event) as event: + event_manager_impl.on_guild_ban_add(None, mock_payload) + + assert event_manager_impl.on_guild_ban_add.___event_name___ == {"GUILD_BAN_ADD"} + event.assert_called_once_with(mock_payload) + event_manager_impl.event_dispatcher.dispatch_event.assert_called_once_with(mock_event) + + def test_on_guild_ban_remove(self, event_manager_impl, mock_payload): + mock_event = mock.MagicMock(events.GuildBanRemoveEvent) + + with mock.patch("hikari.events.GuildBanRemoveEvent.deserialize", return_value=mock_event) as event: + event_manager_impl.on_guild_ban_remove(None, mock_payload) + + assert event_manager_impl.on_guild_ban_remove.___event_name___ == {"GUILD_BAN_REMOVE"} + event.assert_called_once_with(mock_payload) + event_manager_impl.event_dispatcher.dispatch_event.assert_called_once_with(mock_event) + + def test_on_guild_emojis_update(self, event_manager_impl, mock_payload): + mock_event = mock.MagicMock(events.GuildEmojisUpdateEvent) + + with mock.patch("hikari.events.GuildEmojisUpdateEvent.deserialize", return_value=mock_event) as event: + event_manager_impl.on_guild_emojis_update(None, mock_payload) + + assert event_manager_impl.on_guild_emojis_update.___event_name___ == {"GUILD_EMOJIS_UPDATE"} + event.assert_called_once_with(mock_payload) + event_manager_impl.event_dispatcher.dispatch_event.assert_called_once_with(mock_event) + + def test_on_guild_integrations_update(self, event_manager_impl, mock_payload): + mock_event = mock.MagicMock(events.GuildIntegrationsUpdateEvent) + + with mock.patch("hikari.events.GuildIntegrationsUpdateEvent.deserialize", return_value=mock_event) as event: + event_manager_impl.on_guild_integrations_update(None, mock_payload) + + assert event_manager_impl.on_guild_integrations_update.___event_name___ == {"GUILD_INTEGRATIONS_UPDATE"} + event.assert_called_once_with(mock_payload) + event_manager_impl.event_dispatcher.dispatch_event.assert_called_once_with(mock_event) + + def test_on_guild_member_add(self, event_manager_impl, mock_payload): + mock_event = mock.MagicMock(events.GuildMemberAddEvent) + + with mock.patch("hikari.events.GuildMemberAddEvent.deserialize", return_value=mock_event) as event: + event_manager_impl.on_guild_member_add(None, mock_payload) + + assert event_manager_impl.on_guild_member_add.___event_name___ == {"GUILD_MEMBER_ADD"} + event.assert_called_once_with(mock_payload) + event_manager_impl.event_dispatcher.dispatch_event.assert_called_once_with(mock_event) + + def test_on_guild_member_update(self, event_manager_impl, mock_payload): + mock_event = mock.MagicMock(events.GuildMemberUpdateEvent) + + with mock.patch("hikari.events.GuildMemberUpdateEvent.deserialize", return_value=mock_event) as event: + event_manager_impl.on_guild_member_update(None, mock_payload) + + assert event_manager_impl.on_guild_member_update.___event_name___ == {"GUILD_MEMBER_UPDATE"} + event.assert_called_once_with(mock_payload) + event_manager_impl.event_dispatcher.dispatch_event.assert_called_once_with(mock_event) + + def test_on_guild_member_remove(self, event_manager_impl, mock_payload): + mock_event = mock.MagicMock(events.GuildMemberRemoveEvent) + + with mock.patch("hikari.events.GuildMemberRemoveEvent.deserialize", return_value=mock_event) as event: + event_manager_impl.on_guild_member_remove(None, mock_payload) + + assert event_manager_impl.on_guild_member_remove.___event_name___ == {"GUILD_MEMBER_REMOVE"} + event.assert_called_once_with(mock_payload) + event_manager_impl.event_dispatcher.dispatch_event.assert_called_once_with(mock_event) + + def test_on_guild_role_create(self, event_manager_impl, mock_payload): + mock_event = mock.MagicMock(events.GuildRoleCreateEvent) + + with mock.patch("hikari.events.GuildRoleCreateEvent.deserialize", return_value=mock_event) as event: + event_manager_impl.on_guild_role_create(None, mock_payload) + + assert event_manager_impl.on_guild_role_create.___event_name___ == {"GUILD_ROLE_CREATE"} + event.assert_called_once_with(mock_payload) + event_manager_impl.event_dispatcher.dispatch_event.assert_called_once_with(mock_event) + + def test_on_guild_role_update(self, event_manager_impl, mock_payload): + mock_event = mock.MagicMock(events.GuildRoleUpdateEvent) + + with mock.patch("hikari.events.GuildRoleUpdateEvent.deserialize", return_value=mock_event) as event: + event_manager_impl.on_guild_role_update(None, mock_payload) + + assert event_manager_impl.on_guild_role_update.___event_name___ == {"GUILD_ROLE_UPDATE"} + event.assert_called_once_with(mock_payload) + event_manager_impl.event_dispatcher.dispatch_event.assert_called_once_with(mock_event) + + def test_on_guild_role_delete(self, event_manager_impl, mock_payload): + mock_event = mock.MagicMock(events.GuildRoleDeleteEvent) + + with mock.patch("hikari.events.GuildRoleDeleteEvent.deserialize", return_value=mock_event) as event: + event_manager_impl.on_guild_role_delete(None, mock_payload) + + assert event_manager_impl.on_guild_role_delete.___event_name___ == {"GUILD_ROLE_DELETE"} + event.assert_called_once_with(mock_payload) + event_manager_impl.event_dispatcher.dispatch_event.assert_called_once_with(mock_event) + + def test_on_invite_create(self, event_manager_impl, mock_payload): + mock_event = mock.MagicMock(events.InviteCreateEvent) + + with mock.patch("hikari.events.InviteCreateEvent.deserialize", return_value=mock_event) as event: + event_manager_impl.on_invite_create(None, mock_payload) + + assert event_manager_impl.on_invite_create.___event_name___ == {"INVITE_CREATE"} + event.assert_called_once_with(mock_payload) + event_manager_impl.event_dispatcher.dispatch_event.assert_called_once_with(mock_event) + + def test_on_invite_delete(self, event_manager_impl, mock_payload): + mock_event = mock.MagicMock(events.InviteDeleteEvent) + + with mock.patch("hikari.events.InviteDeleteEvent.deserialize", return_value=mock_event) as event: + event_manager_impl.on_invite_delete(None, mock_payload) + + assert event_manager_impl.on_invite_delete.___event_name___ == {"INVITE_DELETE"} + event.assert_called_once_with(mock_payload) + event_manager_impl.event_dispatcher.dispatch_event.assert_called_once_with(mock_event) + + def test_on_message_create(self, event_manager_impl, mock_payload): + mock_event = mock.MagicMock(events.MessageCreateEvent) + + with mock.patch("hikari.events.MessageCreateEvent.deserialize", return_value=mock_event) as event: + event_manager_impl.on_message_create(None, mock_payload) + + assert event_manager_impl.on_message_create.___event_name___ == {"MESSAGE_CREATE"} + event.assert_called_once_with(mock_payload) + event_manager_impl.event_dispatcher.dispatch_event.assert_called_once_with(mock_event) + + def test_on_message_update(self, event_manager_impl, mock_payload): + mock_event = mock.MagicMock(events.MessageUpdateEvent) + + with mock.patch("hikari.events.MessageUpdateEvent.deserialize", return_value=mock_event) as event: + event_manager_impl.on_message_update(None, mock_payload) + + assert event_manager_impl.on_message_update.___event_name___ == {"MESSAGE_UPDATE"} + event.assert_called_once_with(mock_payload) + event_manager_impl.event_dispatcher.dispatch_event.assert_called_once_with(mock_event) + + def test_on_message_delete(self, event_manager_impl, mock_payload): + mock_event = mock.MagicMock(events.MessageDeleteEvent) + + with mock.patch("hikari.events.MessageDeleteEvent.deserialize", return_value=mock_event) as event: + event_manager_impl.on_message_delete(None, mock_payload) + + assert event_manager_impl.on_message_delete.___event_name___ == {"MESSAGE_DELETE"} + event.assert_called_once_with(mock_payload) + event_manager_impl.event_dispatcher.dispatch_event.assert_called_once_with(mock_event) + + def test_on_message_delete_bulk(self, event_manager_impl, mock_payload): + mock_event = mock.MagicMock(events.MessageDeleteBulkEvent) + + with mock.patch("hikari.events.MessageDeleteBulkEvent.deserialize", return_value=mock_event) as event: + event_manager_impl.on_message_delete_bulk(None, mock_payload) + + assert event_manager_impl.on_message_delete_bulk.___event_name___ == {"MESSAGE_DELETE_BULK"} + event.assert_called_once_with(mock_payload) + event_manager_impl.event_dispatcher.dispatch_event.assert_called_once_with(mock_event) + + def test_on_message_reaction_add(self, event_manager_impl, mock_payload): + mock_event = mock.MagicMock(events.MessageReactionAddEvent) + + with mock.patch("hikari.events.MessageReactionAddEvent.deserialize", return_value=mock_event) as event: + event_manager_impl.on_message_reaction_add(None, mock_payload) + + assert event_manager_impl.on_message_reaction_add.___event_name___ == {"MESSAGE_REACTION_ADD"} + event.assert_called_once_with(mock_payload) + event_manager_impl.event_dispatcher.dispatch_event.assert_called_once_with(mock_event) + + def test_on_message_reaction_remove(self, event_manager_impl, mock_payload): + mock_event = mock.MagicMock(events.MessageReactionRemoveEvent) + + with mock.patch("hikari.events.MessageReactionRemoveEvent.deserialize", return_value=mock_event) as event: + event_manager_impl.on_message_reaction_remove(None, mock_payload) + + assert event_manager_impl.on_message_reaction_remove.___event_name___ == {"MESSAGE_REACTION_REMOVE"} + event.assert_called_once_with(mock_payload) + event_manager_impl.event_dispatcher.dispatch_event.assert_called_once_with(mock_event) + + def test_on_message_reaction_remove_emoji(self, event_manager_impl, mock_payload): + mock_event = mock.MagicMock(events.MessageReactionRemoveEmojiEvent) + + with mock.patch("hikari.events.MessageReactionRemoveEmojiEvent.deserialize", return_value=mock_event) as event: + event_manager_impl.on_message_reaction_remove_emoji(None, mock_payload) + + assert event_manager_impl.on_message_reaction_remove_emoji.___event_name___ == { + "MESSAGE_REACTION_REMOVE_EMOJI" + } + event.assert_called_once_with(mock_payload) + event_manager_impl.event_dispatcher.dispatch_event.assert_called_once_with(mock_event) + + def test_on_presence_update(self, event_manager_impl, mock_payload): + mock_event = mock.MagicMock(events.PresenceUpdateEvent) + + with mock.patch("hikari.events.PresenceUpdateEvent.deserialize", return_value=mock_event) as event: + event_manager_impl.on_presence_update(None, mock_payload) + + assert event_manager_impl.on_presence_update.___event_name___ == {"PRESENCE_UPDATE"} + event.assert_called_once_with(mock_payload) + event_manager_impl.event_dispatcher.dispatch_event.assert_called_once_with(mock_event) + + def test_on_typing_start(self, event_manager_impl, mock_payload): + mock_event = mock.MagicMock(events.TypingStartEvent) + + with mock.patch("hikari.events.TypingStartEvent.deserialize", return_value=mock_event) as event: + event_manager_impl.on_typing_start(None, mock_payload) + + assert event_manager_impl.on_typing_start.___event_name___ == {"TYPING_START"} + event.assert_called_once_with(mock_payload) + event_manager_impl.event_dispatcher.dispatch_event.assert_called_once_with(mock_event) + + def test_on_user_update(self, event_manager_impl, mock_payload): + mock_event = mock.MagicMock(events.UserUpdateEvent) + + with mock.patch("hikari.events.UserUpdateEvent.deserialize", return_value=mock_event) as event: + event_manager_impl.on_user_update(None, mock_payload) + + assert event_manager_impl.on_user_update.___event_name___ == {"USER_UPDATE"} + event.assert_called_once_with(mock_payload) + event_manager_impl.event_dispatcher.dispatch_event.assert_called_once_with(mock_event) + + def test_on_voice_state_update(self, event_manager_impl, mock_payload): + mock_event = mock.MagicMock(events.VoiceStateUpdateEvent) + + with mock.patch("hikari.events.VoiceStateUpdateEvent.deserialize", return_value=mock_event) as event: + event_manager_impl.on_voice_state_update(None, mock_payload) + + assert event_manager_impl.on_voice_state_update.___event_name___ == {"VOICE_STATE_UPDATE"} + event.assert_called_once_with(mock_payload) + event_manager_impl.event_dispatcher.dispatch_event.assert_called_once_with(mock_event) + + def test_on_voice_server_update(self, event_manager_impl, mock_payload): + mock_event = mock.MagicMock(events.VoiceStateUpdateEvent) + + with mock.patch("hikari.events.VoiceStateUpdateEvent.deserialize", return_value=mock_event) as event: + event_manager_impl.on_voice_server_update(None, mock_payload) + + assert event_manager_impl.on_voice_server_update.___event_name___ == {"VOICE_SERVER_UPDATE"} + event.assert_called_once_with(mock_payload) + event_manager_impl.event_dispatcher.dispatch_event.assert_called_once_with(mock_event) + + def test_on_webhook_update(self, event_manager_impl, mock_payload): + mock_event = mock.MagicMock(events.WebhookUpdateEvent) + + with mock.patch("hikari.events.WebhookUpdateEvent.deserialize", return_value=mock_event) as event: + event_manager_impl.on_webhook_update(None, mock_payload) + + assert event_manager_impl.on_webhook_update.___event_name___ == {"WEBHOOK_UPDATE"} + event.assert_called_once_with(mock_payload) + event_manager_impl.event_dispatcher.dispatch_event.assert_called_once_with(mock_event) diff --git a/tests/hikari/test_events.py b/tests/hikari/test_events.py index ce09cce5ab..2c09266715 100644 --- a/tests/hikari/test_events.py +++ b/tests/hikari/test_events.py @@ -175,7 +175,7 @@ def test_shard_count_when_information_not_set(self, mock_ready_event_obj): assert mock_ready_event_obj.shard_count is None -# Doesn't have any fields. +# Synthetic event, is not deserialized class TestResumedEvent: ... From 09a78e6473ad0d4e069434467de37960bf238b6a Mon Sep 17 00:00:00 2001 From: davfsa Date: Tue, 14 Apr 2020 11:52:18 +0200 Subject: [PATCH 111/922] Fix enum inconsistencies --- hikari/channels.py | 9 +++- hikari/clients/shard_clients.py | 6 +++ hikari/guilds.py | 30 ++++++++++- hikari/messages.py | 34 +++++++++--- hikari/net/codes.py | 19 +++++++ hikari/oauth2.py | 1 + hikari/permissions.py | 93 ++++++++++++++++++++++----------- hikari/users.py | 2 + hikari/webhooks.py | 1 + 9 files changed, 155 insertions(+), 40 deletions(-) diff --git a/hikari/channels.py b/hikari/channels.py index c25ccad59a..bd54d84301 100644 --- a/hikari/channels.py +++ b/hikari/channels.py @@ -61,20 +61,26 @@ @enum.unique class ChannelType(enum.IntEnum): - """The known channel types that are exposed to us by the api.""" + """The known channel types that are exposed to us by the API.""" #: A text channel in a guild. GUILD_TEXT = 0 + #: A direct channel between two users. DM = 1 + #: A voice channel in a guild. GUILD_VOICE = 2 + #: A direct channel between multiple users. GROUP_DM = 3 + #: An category used for organizing channels in a guild. GUILD_CATEGORY = 4 + #: A channel that can be followed and can crosspost. GUILD_NEWS = 5 + #: A channel that show's a game's store page. GUILD_STORE = 6 @@ -86,6 +92,7 @@ class PermissionOverwriteType(str, enum.Enum): #: A permission overwrite that targets all the members with a specific #: guild role. ROLE = "role" + #: A permission overwrite that targets a specific guild member. MEMBER = "member" diff --git a/hikari/clients/shard_clients.py b/hikari/clients/shard_clients.py index 4d1e597b9c..353a583b32 100644 --- a/hikari/clients/shard_clients.py +++ b/hikari/clients/shard_clients.py @@ -57,17 +57,23 @@ class ShardState(enum.IntEnum): #: The shard is not running. NOT_RUNNING = 0 + #: The shard is undergoing the initial connection handshake. CONNECTING = enum.auto() + #: The initialization handshake has completed. We are waiting for the shard #: to receive the ``READY`` event. WAITING_FOR_READY = enum.auto() + #: The shard is ``READY``. READY = enum.auto() + #: The shard has sent a request to ``RESUME`` and is waiting for a response. RESUMING = enum.auto() + #: The shard is currently shutting down permanently. STOPPING = enum.auto() + #: The shard has shut down and is no longer connected. STOPPED = enum.auto() diff --git a/hikari/guilds.py b/hikari/guilds.py index c679ee2791..db06f87b5f 100644 --- a/hikari/guilds.py +++ b/hikari/guilds.py @@ -81,32 +81,46 @@ class GuildFeature(str, enum.Enum): #: Guild has access to set an animated guild icon. ANIMATED_ICON = "ANIMATED_ICON" + #: Guild has access to set a guild banner image. BANNER = "BANNER" + #: Guild has access to use commerce features (i.e. create store channels). COMMERCE = "COMMERCE" + #: Guild is able to be discovered in the directory. DISCOVERABLE = "DISCOVERABLE" + #: Guild is able to be featured in the directory. FEATURABLE = "FEATURABLE" + #: Guild has access to set an invite splash background. INVITE_SPLASH = "INVITE_SPLASH" + #: More emojis can be hosted in this guild than normal. MORE_EMOJI = "MORE_EMOJI" + #: Guild has access to create news channels. NEWS = "NEWS" + #: People can view channels in this guild without joining. LURKABLE = "LURKABLE" + #: Guild is partnered. PARTNERED = "PARTNERED" + #: Guild is public, go figure. PUBLIC = "PUBLIC" + #: Guild cannot be public. Who would have guessed? PUBLIC_DISABLED = "PUBLIC_DISABLED" + #: Guild has access to set a vanity URL. VANITY_URL = "VANITY_URL" + #: Guild is verified. VERIFIED = "VERIFIED" + #: Guild has access to set 384kbps bitrate in voice (previously #: VIP voice servers). VIP_REGIONS = "VIP_REGIONS" @@ -151,13 +165,15 @@ class GuildPremiumTier(enum.IntEnum): TIER_3 = 3 +@enum.unique class GuildSystemChannelFlag(enum.IntFlag): """Defines which features are suppressed in the system channel.""" #: Display a message about new users joining. - SUPPRESS_USER_JOIN = 1 + SUPPRESS_USER_JOIN = 1 << 0 + #: Display a message when the guild is Nitro boosted. - SUPPRESS_PREMIUM_SUBSCRIPTION = 2 + SUPPRESS_PREMIUM_SUBSCRIPTION = 1 << 1 @enum.unique @@ -307,6 +323,7 @@ class ActivityType(enum.IntEnum): #: Shows up as ``Playing `` PLAYING = 0 + #: Shows up as ``Streaming ``. #: #: Warning @@ -314,11 +331,14 @@ class ActivityType(enum.IntEnum): #: Corresponding presences must be associated with VALID Twitch or YouTube #: stream URLS! STREAMING = 1 + #: Shows up as ``Listening to ``. LISTENING = 2 + #: Shows up as ``Watching ``. Note that this is not officially #: documented, so will be likely removed in the near future. WATCHING = 3 + #: A custom status. #: #: To set an emoji with the status, place a unicode emoji or Discord emoji @@ -422,6 +442,7 @@ class ActivitySecret(entities.HikariEntity, entities.Deserializable): match: typing.Optional[str] = marshaller.attrib(deserializer=str, if_undefined=None) +@enum.unique class ActivityFlag(enum.IntFlag): """Flags that describe what an activity includes. @@ -528,12 +549,16 @@ class PresenceStatus(str, enum.Enum): #: Online/green. ONLINE = "online" + #: Idle/yellow. IDLE = "idle" + #: Do not disturb/red. DND = "dnd" + #: An alias for :attr:`DND` DO_NOT_DISTURB = DND + #: Offline or invisible/grey. OFFLINE = "offline" @@ -667,6 +692,7 @@ class IntegrationExpireBehaviour(enum.IntEnum): #: Remove the role. REMOVE_ROLE = 0 + #: Kick the subscriber. KICK = 1 diff --git a/hikari/messages.py b/hikari/messages.py index 4f586b2165..ddee2bdb47 100644 --- a/hikari/messages.py +++ b/hikari/messages.py @@ -51,46 +51,64 @@ class MessageType(enum.IntEnum): #: A normal message. DEFAULT = 0 + #: A message to denote a new recipient in a group. RECIPIENT_ADD = 1 + #: A message to denote that a recipient left the group. RECIPIENT_REMOVE = 2 + #: A message to denote a VoIP call. CALL = 3 + #: A message to denote that the name of a channel changed. CHANNEL_NAME_CHANGE = 4 + #: A message to denote that the icon of a channel changed. CHANNEL_ICON_CHANGE = 5 + #: A message to denote that a message was pinned. CHANNEL_PINNED_MESSAGE = 6 + #: A message to denote that a member joined the guild. GUILD_MEMBER_JOIN = 7 + #: A message to denote a Nitro subscription. USER_PREMIUM_GUILD_SUBSCRIPTION = 8 + #: A message to denote a tier 1 Nitro subscription. USER_PREMIUM_GUILD_SUBSCRIPTION_TIER_1 = 9 + #: A message to denote a tier 2 Nitro subscription. USER_PREMIUM_GUILD_SUBSCRIPTION_TIER_2 = 10 + #: A message to denote a tier 3 Nitro subscription. USER_PREMIUM_GUILD_SUBSCRIPTION_TIER_3 = 11 + #: Channel follow add CHANNEL_FOLLOW_ADD = 12 +@enum.unique class MessageFlag(enum.IntFlag): """Additional flags for message options.""" - NONE = 0x0 + NONE = 0 + #: This message has been published to subscribed channels via channel following. - CROSSPOSTED = 0x1 + CROSSPOSTED = 1 << 0 + #: This message originated from a message in another channel via channel following. - IS_CROSSPOST = 0x2 + IS_CROSSPOST = 1 << 1 + #: Any embeds on this message should be omitted when serializing the message. - SUPPRESS_EMBEDS = 0x4 + SUPPRESS_EMBEDS = 1 << 2 + #: The message this crosspost originated from was deleted via channel following. - SOURCE_MESSAGE_DELETED = 0x8 + SOURCE_MESSAGE_DELETED = 1 << 3 + #: This message came from the urgent message system. - URGENT = 0x10 + URGENT = 1 << 4 @enum.unique @@ -98,12 +116,16 @@ class MessageActivityType(enum.IntEnum): """The type of a rich presence message activity.""" NONE = 0 + #: Join an activity. JOIN = 1 + #: Spectating something. SPECTATE = 2 + #: Listening to something. LISTEN = 3 + #: Request to join an activity. JOIN_REQUEST = 5 diff --git a/hikari/net/codes.py b/hikari/net/codes.py index 1141924cc0..9b81c5062c 100644 --- a/hikari/net/codes.py +++ b/hikari/net/codes.py @@ -24,6 +24,7 @@ # Doesnt work correctly with enums, so since this file is all enums, ignore # pylint: disable=no-member +@enum.unique class HTTPStatusCode(enum.IntEnum): """HTTP response codes expected from RESTful components.""" @@ -61,6 +62,7 @@ def __str__(self) -> str: return f"{self.value} {name}" +@enum.unique class GatewayCloseCode(enum.IntEnum): """Reasons for closing a gateway connection. @@ -72,36 +74,50 @@ class GatewayCloseCode(enum.IntEnum): #: The application running closed. NORMAL_CLOSURE = 1000 + #: Discord is not sure what went wrong. Try reconnecting? UNKNOWN_ERROR = 4000 + #: You sent an invalid Gateway opcode or an invalid payload for an opcode. #: Don't do that! UNKNOWN_OPCODE = 4001 + #: You sent an invalid payload to Discord. Don't do that! DECODE_ERROR = 4002 + #: You sent Discord a payload prior to IDENTIFYing. NOT_AUTHENTICATED = 4003 + #: The account token sent with your identify payload is incorrect. AUTHENTICATION_FAILED = 4004 + #: You sent more than one identify payload. Don't do that! ALREADY_AUTHENTICATED = 4005 + #: The sequence sent when resuming the session was invalid. Reconnect and #: start a new session. INVALID_SEQ = 4007 + #: Woah nelly! You're sending payloads to Discord too quickly. Slow it down! RATE_LIMITED = 4008 + #: Your session timed out. Reconnect and start a new one. SESSION_TIMEOUT = 4009 + #: You sent Discord an invalid shard when IDENTIFYing. INVALID_SHARD = 4010 + #: The session would have handled too many guilds - you are required to #: shard your connection in order to connect. SHARDING_REQUIRED = 4011 + #: You sent an invalid version for the gateway. INVALID_VERSION = 4012 + #: You sent an invalid intent for a Gateway Intent. You may have incorrectly #: calculated the bitwise value. INVALID_INTENT = 4013 + #: You sent a disallowed intent for a Gateway Intent. You may have tried to #: specify an intent that you have not enabled or are not whitelisted for. DISALLOWED_INTENT = 4014 @@ -111,6 +127,7 @@ def __str__(self) -> str: return f"{self.value} {name}" +@enum.unique class GatewayOpcode(enum.IntEnum): """Opcodes that the gateway uses internally.""" @@ -156,6 +173,7 @@ def __str__(self) -> str: return f"{self.value} {name}" +@enum.unique class JSONErrorCode(enum.IntEnum): """Error codes that can be returned by the REST API.""" @@ -340,6 +358,7 @@ def __str__(self) -> str: return f"{self.value} {name}" +@enum.unique class GatewayIntent(enum.IntFlag): """Represents an intent on the gateway. diff --git a/hikari/oauth2.py b/hikari/oauth2.py index 63329a9bb8..f30171e4dd 100644 --- a/hikari/oauth2.py +++ b/hikari/oauth2.py @@ -39,6 +39,7 @@ class ConnectionVisibility(enum.IntEnum): #: Only you can see the connection. NONE = 0 + #: Everyone can see the connection. EVERYONE = 1 diff --git a/hikari/permissions.py b/hikari/permissions.py index 2a0fc63140..340b2ffab6 100644 --- a/hikari/permissions.py +++ b/hikari/permissions.py @@ -22,6 +22,7 @@ import enum +@enum.unique class Permission(enum.IntFlag): """Represents the permissions available in a given channel or guild. @@ -86,66 +87,96 @@ class Permission(enum.IntFlag): """ #: Empty permission. - NONE = 0x0 + NONE = 0 + #: Allows creation of instant invites. - CREATE_INSTANT_INVITE = 0x1 + CREATE_INSTANT_INVITE = 1 << 0 + #: Allows kicking members - KICK_MEMBERS = 0x2 + KICK_MEMBERS = 1 << 1 + #: Allows banning members. - BAN_MEMBERS = 0x4 + BAN_MEMBERS = 1 << 2 + #: Allows all permissions and bypasses channel permission overwrites. - ADMINISTRATOR = 0x8 + ADMINISTRATOR = 1 << 3 + #: Allows management and editing of channels. - MANAGE_CHANNELS = 0x10 + MANAGE_CHANNELS = 1 << 4 + #: Allows management and editing of the guild. - MANAGE_GUILD = 0x20 + MANAGE_GUILD = 1 << 5 + #: Allows for the addition of reactions to messages. - ADD_REACTIONS = 0x40 + ADD_REACTIONS = 1 << 6 + #: Allows for viewing of audit logs. - VIEW_AUDIT_LOG = 0x80 + VIEW_AUDIT_LOG = 1 << 7 + #: Allows for using priority speaker in a voice channel. - PRIORITY_SPEAKER = 0x1_00 + PRIORITY_SPEAKER = 1 << 8 + #: Allows the user to go live. - STREAM = 0x2_00 + STREAM = 1 << 9 + #: Allows guild members to view a channel, which includes reading messages in text channels. - VIEW_CHANNEL = 0x4_00 + VIEW_CHANNEL = 1 << 10 + #: Allows for sending messages in a channel. - SEND_MESSAGES = 0x8_00 + SEND_MESSAGES = 1 << 11 + #: Allows for sending of ``/tts`` messages. - SEND_TTS_MESSAGES = 0x10_00 + SEND_TTS_MESSAGES = 1 << 12 + #: Allows for deletion of other users messages. - MANAGE_MESSAGES = 0x20_00 + MANAGE_MESSAGES = 1 << 13 + #: Links sent by users with this permission will be auto-embedded. - EMBED_LINKS = 0x40_00 + EMBED_LINKS = 1 << 14 + #: Allows for uploading images and files - ATTACH_FILES = 0x80_00 + ATTACH_FILES = 1 << 15 + #: Allows for reading of message history. - READ_MESSAGE_HISTORY = 0x1_00_00 + READ_MESSAGE_HISTORY = 1 << 16 + #: Allows for using the ``@everyone`` tag to notify all users in a channel, and the #: ``@here`` tag to notify all online users in a channel, and the ``@role`` tag (even #: if the role is not mentionable) to notify all users with that role in a channel. - MENTION_EVERYONE = 0x2_00_00 + MENTION_EVERYONE = 1 << 17 + #: Allows the usage of custom emojis from other servers. - USE_EXTERNAL_EMOJIS = 0x4_00_00 + USE_EXTERNAL_EMOJIS = 1 << 18 + #: Allows for joining of a voice channel. - CONNECT = 0x10_00_00 + CONNECT = 1 << 20 + #: Allows for speaking in a voice channel. - SPEAK = 0x20_00_00 + SPEAK = 1 << 21 + #: Allows for muting members in a voice channel. - MUTE_MEMBERS = 0x40_00_00 + MUTE_MEMBERS = 1 << 22 + #: Allows for deafening of members in a voice channel. - DEAFEN_MEMBERS = 0x80_00_00 + DEAFEN_MEMBERS = 1 << 23 + #: Allows for moving of members between voice channels. - MOVE_MEMBERS = 0x1_00_00_00 + MOVE_MEMBERS = 1 << 24 + #: Allows for using voice-activity-detection in a voice channel. - USE_VAD = 0x2_00_00_00 + USE_VAD = 1 << 25 + #: Allows for modification of own nickname. - CHANGE_NICKNAME = 0x4_00_00_00 + CHANGE_NICKNAME = 1 << 26 + #: Allows for modification of other users nicknames. - MANAGE_NICKNAMES = 0x8_00_00_00 + MANAGE_NICKNAMES = 1 << 27 + #: Allows management and editing of roles. - MANAGE_ROLES = 0x10_00_00_00 + MANAGE_ROLES = 1 << 28 + #: Allows management and editing of webhooks. - MANAGE_WEBHOOKS = 0x20_00_00_00 + MANAGE_WEBHOOKS = 1 << 29 + #: Allows management and editing of emojis. - MANAGE_EMOJIS = 0x40_00_00_00 + MANAGE_EMOJIS = 1 << 30 diff --git a/hikari/users.py b/hikari/users.py index 3b750beb05..6f9c430d3d 100644 --- a/hikari/users.py +++ b/hikari/users.py @@ -130,8 +130,10 @@ class PremiumType(enum.IntEnum): #: No premium. NONE = 0 + #: Premium including basic perks like animated emojis and avatars. NITRO_CLASSIC = 1 + #: Premium including all perks (e.g. 2 server boosts). NITRO = 2 diff --git a/hikari/webhooks.py b/hikari/webhooks.py index 586f49df7f..b642a7f2ff 100644 --- a/hikari/webhooks.py +++ b/hikari/webhooks.py @@ -36,6 +36,7 @@ class WebhookType(enum.IntEnum): #: Incoming webhook. INCOMING = 1 + #: Channel Follower webhook. CHANNEL_FOLLOWER = 2 From 4b967832dc479401d1956c5142c06d503c9360b2 Mon Sep 17 00:00:00 2001 From: davfsa Date: Tue, 14 Apr 2020 12:53:35 +0200 Subject: [PATCH 112/922] Document missing JSON Error Codes --- hikari/net/codes.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/hikari/net/codes.py b/hikari/net/codes.py index 1141924cc0..19e4d671ca 100644 --- a/hikari/net/codes.py +++ b/hikari/net/codes.py @@ -247,15 +247,33 @@ class JSONErrorCode(enum.IntEnum): #: Maximum number of guild roles reached (250) MAX_GUILD_ROLES_REACHED = 30_005 + #: Maximum number of webhooks reached (10) + MAX_WEBHOOKS_REACHED = 30_007 + #: Maximum number of reactions reached (20) MAX_REACTIONS_REACHED = 30_010 #: Maximum number of guild channels reached (500) MAX_GUILD_CHANNELS_REACHED = 30_013 + #: Maximun number of attachments in a message reached (10) + MAX_MESSAGE_ATTACHMENTS_REACHED = 30_015 + + #: Maximun number of invites reached (10000) + MAX_INVITES_REACHED = 30_016 + #: Unauthorized UNAUTHORIZED = 40_001 + #: Request entity too large. Try sending something smaller in size + TOO_LARGE = 40_005 + + #: This feature has been temporarily disabled server-side + DISABLED_TEMPORARILY = 40_006 + + #: The user is banned from this guild + USER_BANNED = 40_007 + #: Missing access MISSING_ACCESS = 50_001 From 6ff870f0983eef81f34417344de9dd752025ac68 Mon Sep 17 00:00:00 2001 From: davfsa Date: Tue, 14 Apr 2020 16:43:52 +0200 Subject: [PATCH 113/922] Added `nonce` attr to messages --- hikari/events.py | 6 ++++++ hikari/messages.py | 6 ++++++ 2 files changed, 12 insertions(+) diff --git a/hikari/events.py b/hikari/events.py index 9009cb2002..948199705f 100644 --- a/hikari/events.py +++ b/hikari/events.py @@ -889,6 +889,12 @@ class MessageUpdateEvent(HikariEvent, snowflakes.UniqueEntity, entities.Deserial deserializer=messages.MessageFlag, if_undefined=entities.Unset ) + #: The message nonce. This is a string used for validating + #: a message was sent. + #: + #: :type: :obj:`typing.Union` [ :obj:`hikari.messages.MessageFlag`, :obj:`hikari.entities.UNSET` ] + nonce: typing.Union[str, entities.Unset] = marshaller.attrib(deserializer=str, if_undefined=entities.Unset) + @marshaller.marshallable() @attr.s(slots=True) diff --git a/hikari/messages.py b/hikari/messages.py index ddee2bdb47..a1959be27c 100644 --- a/hikari/messages.py +++ b/hikari/messages.py @@ -388,3 +388,9 @@ class Message(snowflakes.UniqueEntity, entities.Deserializable): #: #: :type: :obj:`MessageFlag`, optional flags: typing.Optional[MessageFlag] = marshaller.attrib(deserializer=MessageFlag, if_undefined=None) + + #: The message nonce. This is a string used for validating + #: a message was sent. + #: + #: :type: :obj:`str`, optional + nonce: typing.Optional[str] = marshaller.attrib(deserializer=str, if_undefined=None) From 1ed23c1f21e218cba81e6ac39bf35613dc966501 Mon Sep 17 00:00:00 2001 From: davfsa Date: Tue, 14 Apr 2020 18:13:08 +0200 Subject: [PATCH 114/922] Add public_flags attr --- hikari/guilds.py | 11 ++++ hikari/messages.py | 3 +- hikari/net/codes.py | 51 ++++++++++++++++-- hikari/users.py | 105 +++++++++++++++++++++++++------------ tests/hikari/test_users.py | 3 ++ 5 files changed, 134 insertions(+), 39 deletions(-) diff --git a/hikari/guilds.py b/hikari/guilds.py index db06f87b5f..60ffba915a 100644 --- a/hikari/guilds.py +++ b/hikari/guilds.py @@ -449,11 +449,22 @@ class ActivityFlag(enum.IntFlag): This can be more than one using bitwise-combinations. """ + #: Instance INSTANCE = 1 << 0 + + #: Join JOIN = 1 << 1 + + #: Spectate SPECTATE = 1 << 2 + + #: Join Request JOIN_REQUEST = 1 << 3 + + #: Sync SYNC = 1 << 4 + + #: Play PLAY = 1 << 5 diff --git a/hikari/messages.py b/hikari/messages.py index ddee2bdb47..78a450aed9 100644 --- a/hikari/messages.py +++ b/hikari/messages.py @@ -85,7 +85,7 @@ class MessageType(enum.IntEnum): #: A message to denote a tier 3 Nitro subscription. USER_PREMIUM_GUILD_SUBSCRIPTION_TIER_3 = 11 - #: Channel follow add + #: Channel follow add. CHANNEL_FOLLOW_ADD = 12 @@ -93,6 +93,7 @@ class MessageType(enum.IntEnum): class MessageFlag(enum.IntFlag): """Additional flags for message options.""" + #: None NONE = 0 #: This message has been published to subscribed channels via channel following. diff --git a/hikari/net/codes.py b/hikari/net/codes.py index 5013c206b7..a9d1fa7cc9 100644 --- a/hikari/net/codes.py +++ b/hikari/net/codes.py @@ -28,33 +28,76 @@ class HTTPStatusCode(enum.IntEnum): """HTTP response codes expected from RESTful components.""" + #: Continue CONTINUE = 100 + #: OK OK = 200 + + #: Created CREATED = 201 + + #: Accepted ACCEPTED = 202 + + #: No content NO_CONTENT = 204 + #: Moved permanently MOVED_PERMANENTLY = 301 + #: Bad request BAD_REQUEST = 400 + + #: Unauthorized UNAUTHORIZED = 401 + + #: Forbidden FORBIDDEN = 403 + + #: Not found NOT_FOUND = 404 + + #: Method not allowed METHOD_NOT_ALLOWED = 405 + + #: Not acceptable NOT_ACCEPTABLE = 406 + + #: Proxy authentication required PROXY_AUTHENTICATION_REQUIRED = 407 + + #: Request entitiy too large REQUEST_ENTITY_TOO_LARGE = 413 + + #: Request URI too long REQUEST_URI_TOO_LONG = 414 + + #: Unsupported media type UNSUPPORTED_MEDIA_TYPE = 415 + + #: Im a teapot IM_A_TEAPOT = 418 + + #: Too many requests TOO_MANY_REQUESTS = 429 + #: Internal server error INTERNAL_SERVER_ERROR = 500 + + #: Not implemented NOT_IMPLEMENTED = 501 + + #: Bad gateway BAD_GATEWAY = 502 + + #: Service unavailable SERVICE_UNAVAILABLE = 503 + + #: Gateway timeout GATEWAY_TIMEOUT = 504 + + #: HTTP Version not supported HTTP_VERSION_NOT_SUPPORTED = 505 def __str__(self) -> str: @@ -66,10 +109,10 @@ def __str__(self) -> str: class GatewayCloseCode(enum.IntEnum): """Reasons for closing a gateway connection. - Notes - ----- - Any codes greater than or equal to `4000` are server-side codes. Any codes - between `1000` and `1999` inclusive are generally client-side codes. + Note + ---- + Any codes greater than or equal to ``4000`` are server-side codes. Any codes + between ``1000`` and ``1999`` inclusive are generally client-side codes. """ #: The application running closed. diff --git a/hikari/users.py b/hikari/users.py index 6f9c430d3d..4bbb8c4c8d 100644 --- a/hikari/users.py +++ b/hikari/users.py @@ -30,6 +30,67 @@ from hikari.internal import marshaller +@enum.unique +class UserFlag(enum.IntFlag): + """The known user flags that represent account badges.""" + + #: None + NONE = 0 + + #: Discord Empoloyee + DISCORD_EMPLOYEE = 1 << 0 + + #: Discord Partner + DISCORD_PARTNER = 1 << 1 + + #: HypeSquad Events + HYPESQUAD_EVENTS = 1 << 2 + + #: Bug Hunter Level 1 + BUG_HUNTER_LEVEL_1 = 1 << 3 + + #: House of Bravery + HOUSE_BRAVERY = 1 << 6 + + #: House of Brilliance + HOUSE_BRILLIANCE = 1 << 7 + + #: House of Balance + HOUSE_BALANCE = 1 << 8 + + #: Early Supporter + EARLY_SUPPORTER = 1 << 9 + + #: Team user + TEAM_USER = 1 << 10 + + #: System + SYSTEM = 1 << 12 + + #: Bug Hunter Level 2 + BUG_HUNTER_LEVEL_2 = 1 << 14 + + #: Verified Bot + VERIFIED_BOT = 1 << 16 + + #: Verified Bot Developer + VERIFIED_BOT_DEVELOPER = 1 << 17 + + +@enum.unique +class PremiumType(enum.IntEnum): + """The types of Nitro.""" + + #: No premium. + NONE = 0 + + #: Premium including basic perks like animated emojis and avatars. + NITRO_CLASSIC = 1 + + #: Premium including all perks (e.g. 2 server boosts). + NITRO = 2 + + @marshaller.marshallable() @attr.s(slots=True) class User(snowflakes.UniqueEntity, entities.Deserializable): @@ -60,6 +121,16 @@ class User(snowflakes.UniqueEntity, entities.Deserializable): #: :type: :obj:`bool` is_system: bool = marshaller.attrib(raw_name="system", deserializer=bool, if_undefined=False) + #: The public flags for this user. + #: + #: Note + #: ---- + #: This will be :obj:`None` if it's a webhook user. + #: + #: + #: :type: :obj:`UserFlag`, optional + public_flags: typing.Optional[UserFlag] = marshaller.attrib(deserializer=UserFlag, if_undefined=None) + @property def avatar_url(self) -> str: """URL for this user's custom avatar if set, else default.""" @@ -104,40 +175,6 @@ def default_avatar(self) -> int: return int(self.discriminator) % 5 -@enum.unique -class UserFlag(enum.IntFlag): - """The known user flags that represent account badges.""" - - NONE = 0 - DISCORD_EMPLOYEE = 1 << 0 - DISCORD_PARTNER = 1 << 1 - HYPESQUAD_EVENTS = 1 << 2 - BUG_HUNTER_LEVEL_1 = 1 << 3 - HOUSE_BRAVERY = 1 << 6 - HOUSE_BRILLIANCE = 1 << 7 - HOUSE_BALANCE = 1 << 8 - EARLY_SUPPORTER = 1 << 9 - TEAM_USER = 1 << 10 - SYSTEM = 1 << 12 - BUG_HUNTER_LEVEL_2 = 1 << 14 - VERIFIED_BOT = 1 << 16 - VERIFIED_BOT_DEVELOPER = 1 << 17 - - -@enum.unique -class PremiumType(enum.IntEnum): - """The types of Nitro.""" - - #: No premium. - NONE = 0 - - #: Premium including basic perks like animated emojis and avatars. - NITRO_CLASSIC = 1 - - #: Premium including all perks (e.g. 2 server boosts). - NITRO = 2 - - @marshaller.marshallable() @attr.s(slots=True) class MyUser(User): diff --git a/tests/hikari/test_users.py b/tests/hikari/test_users.py index b045847d11..1fb1dd0aa3 100644 --- a/tests/hikari/test_users.py +++ b/tests/hikari/test_users.py @@ -33,6 +33,7 @@ def test_user_payload(): "discriminator": "6127", "bot": True, "system": True, + "public_flags": int(users.UserFlag.VERIFIED_BOT_DEVELOPER), } @@ -61,6 +62,7 @@ def test_deserialize(self, test_user_payload): assert user_obj.discriminator == "6127" assert user_obj.is_bot is True assert user_obj.is_system is True + assert user_obj.public_flags == users.UserFlag.VERIFIED_BOT_DEVELOPER @pytest.fixture() def user_obj(self, test_user_payload): @@ -71,6 +73,7 @@ def user_obj(self, test_user_payload): discriminator="6127", is_bot=None, is_system=None, + public_flags=None, ) def test_avatar_url(self, user_obj): From fb3c15a72cd3134dc0c448c5841c97fb2783173c Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Mon, 13 Apr 2020 16:52:29 +0100 Subject: [PATCH 115/922] Rewrote CI config scripts. --- .gitlab-ci.yml | 16 +- ci/__init__.py | 18 ++ ci/bases.gitlab-ci.yml | 68 +++++ ci/installations.gitlab-ci.yml | 69 +++++ ci/integrations.gitlab-ci.yml | 133 ++++++++++ ci/linting.gitlab-ci.yml | 89 +++++++ ci/linting.py | 18 ++ ci/pages.gitlab-ci.yml | 101 ++++++++ .../deploy.yml => ci/releases.gitlab-ci.yml | 88 ++++--- ci/tests.gitlab-ci.yml | 94 +++++++ gitlab/pages.yml | 91 ------- gitlab/test.yml | 242 ------------------ hikari/_about.py | 2 +- tasks/make_version_string.py | 2 +- 14 files changed, 661 insertions(+), 370 deletions(-) create mode 100644 ci/__init__.py create mode 100644 ci/bases.gitlab-ci.yml create mode 100644 ci/installations.gitlab-ci.yml create mode 100644 ci/integrations.gitlab-ci.yml create mode 100644 ci/linting.gitlab-ci.yml create mode 100644 ci/linting.py create mode 100644 ci/pages.gitlab-ci.yml rename gitlab/deploy.yml => ci/releases.gitlab-ci.yml (55%) create mode 100644 ci/tests.gitlab-ci.yml delete mode 100644 gitlab/pages.yml delete mode 100644 gitlab/test.yml diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 9bf472d050..bfe5acd895 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -17,14 +17,20 @@ stages: - test - - results + - install + - lint + - report - deploy - - post-deploy + - verify variables: DOCKER_DRIVER: "overlay2" include: - - local: "/gitlab/test.yml" - - local: "/gitlab/pages.yml" - - local: "/gitlab/deploy.yml" + - local: "/ci/bases.gitlab-ci.yml" + - local: "/ci/installations.gitlab-ci.yml" + - local: "/ci/integrations.gitlab-ci.yml" + - local: "/ci/linting.gitlab-ci.yml" + - local: "/ci/pages.gitlab-ci.yml" + - local: "/ci/releases.gitlab-ci.yml" + - local: "/ci/tests.gitlab-ci.yml" diff --git a/ci/__init__.py b/ci/__init__.py new file mode 100644 index 0000000000..1c1502a5ca --- /dev/null +++ b/ci/__init__.py @@ -0,0 +1,18 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# Copyright © Nekoka.tt 2019-2020 +# +# This file is part of Hikari. +# +# Hikari is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Hikari is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Hikari. If not, see . diff --git a/ci/bases.gitlab-ci.yml b/ci/bases.gitlab-ci.yml new file mode 100644 index 0000000000..fd1013bc1e --- /dev/null +++ b/ci/bases.gitlab-ci.yml @@ -0,0 +1,68 @@ +# Copyright © Nekoka.tt 2019-2020 +# +# This file is part of Hikari. +# +# Hikari is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Hikari is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Hikari. If not, see . + +### +### Mark a job as having reactive defaults, such as firing on merge requests, +### new tags, new branches, schedules, new commits; all by default. +### +.reactive-job: + only: + - branches + - merge_requests + - tags + - schedules + +### +### Busybox 1.3.1 with musl libc +### +### Base to provide a tiny busybox install with nothing in it. +### Used by some stages to move files produced by other stages into +### the correct directory structure. +### +.busybox-musl: + image: busybox:1.31.1-musl + +### +### CPython 3.8.0 configuration. +### +.cpython380: + image: python:3.8.0 + +### +### CPython 3.8.1 configuration. +### +.cpython381: + image: python:3.8.1 + +### +### CPython 3.8.2 configuration. +### +.cpython382: + image: python:3.8.2 + +### +### CPython 3.9.0 configuration. +### +.cpython390: + image: python:3.9-rc + +### +### Most recent stable CPython release. +### +.cpython-stable: + extends: .cpython382 + diff --git a/ci/installations.gitlab-ci.yml b/ci/installations.gitlab-ci.yml new file mode 100644 index 0000000000..25a5b737c1 --- /dev/null +++ b/ci/installations.gitlab-ci.yml @@ -0,0 +1,69 @@ +# Copyright © Nekoka.tt 2019-2020 +# +# This file is part of Hikari. +# +# Hikari is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Hikari is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Hikari. If not, see . + +### +### Base for testing that the library setup script is valid. +### +.nox-pip-install: + extends: .reactive-job + script: + - |+ + set -e + set -x + apt install git -y + pip install nox + nox --sessions pip pip_sdist pip_bdist_wheel pip_git + stage: install +### +### Setup script tests for CPython 3.8.0 +### +### Test locally installing the library via pip in several formats +### +"install 3.8.0": + extends: + - .cpython380 + - .nox-pip-install + +### +### Setup script tests for CPython 3.8.1 +### +### Test locally installing the library via pip in several formats +### +"install 3.8.1": + extends: + - .cpython381 + - .nox-pip-install + +### +### Setup script tests for CPython 3.8.2 +### +### Test locally installing the library via pip in several formats +### +"install 3.8.2": + extends: + - .cpython382 + - .nox-pip-install + +### +### Setup script tests for CPython 3.9.0 +### +### Test locally installing the library via pip in several formats +### +"install 3.9.0": + extends: + - .cpython390 + - .nox-pip-install diff --git a/ci/integrations.gitlab-ci.yml b/ci/integrations.gitlab-ci.yml new file mode 100644 index 0000000000..7672735eee --- /dev/null +++ b/ci/integrations.gitlab-ci.yml @@ -0,0 +1,133 @@ +# Copyright © Nekoka.tt 2019-2020 +# +# This file is part of Hikari. +# +# Hikari is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Hikari is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Hikari. If not, see . + +include: + - template: "Dependency-Scanning.gitlab-ci.yml" + - template: "License-Scanning.gitlab-ci.yml" + - template: "SAST.gitlab-ci.yml" + +### +### Detect overall test coverage across all test jobs. +### +### Collect unit test coverage results and coalesce them, outputting the +### format so that GitLab can display it on the badge, on the pipeline stats, +### and on any active merge requests. +### +"coverage results": + coverage: /^TOTAL[\s\d%]+\s(\d+(?:\.\d+)?\%)$/ + extends: + - .cpython-stable + - .reactive-job + script: + - |+ + set -e + set -x + pip install coverage + coverage combine "public/*.coverage" + coverage report -i + stage: report + +### +### GitLab's dependency scanning template. +### +### This gives dependency statistics on any MRs, and beefs out the security +### dashboard. +### +### This must use a specific name. Do not rename it for consistency. +### +dependency_scanning: + interruptible: true + only: + # Overly slow to run and not very important, so skip normally to get 5 + # minutes back on + # each pipeline. + - merge_requests + - schedules + retry: 1 + stage: lint + variables: + DS_DEFAULT_ANALYZERS: "gemnasium-python" + + +### +### GitLab's license scanning template. +### +### This checks for license conflicts on any MRs and periodically, and beefs out +### the security dashboard. +### +### This must use a specific name. Do not rename it for consistency. +### +license_scanning: + interruptible: true + only: + # Overly slow to run and not very important, so skip normally to get 5 + # minutes back on + # each pipeline. + - merge_requests + - schedules + retry: 1 + stage: lint + +### +### Deploy the pages to GitLab. +### +### This pulls the results of generate-master-pages and generate-stating-pages +### and places them in the correct +### +### This must use a specific name. Do not rename it for consistency. +### +pages: + dependencies: + - master pages + - staging pages + extends: .busybox-musl + only: + - master + - staging + script: + - |+ + set -e + set -x + git log -n 1 "${CI_COMMIT_SHA}" --format="%B" \ + | grep -iqE "\[\s*(skip|no|don'?t|do\s+not)\s+pages?\s*\]" \ + && echo "SKIPPING ${CI_JOB_STAGE} STAGE JOB" \ + && exit 0 + mkdir public || true + cd public/master + find . -maxdepth 1 -exec mv {} .. \; + cd ../.. + rm public/master -Rvf + stage: deploy + +### +### GitLab's Static Application Security Testing suite. +### +### This checks for security issues on every commit and MR. This is reported +### to the security dashboard. +### +### This must use a specific name. Do not rename it for consistency. +### +sast: + interruptible: true + retry: 1 + stage: lint + variables: + SAST_BANDIT_EXCLUDED_PATHS: tests/*,docs/*,ci/*,insomnia/*,public/*,tasks/*,noxfile.py + SAST_DEFAULT_ANALYZERS: "bandit, flawfinder, secrets, kubesec, gitleaks, trufflehog" + SAST_EXCLUDED_PATHS: tests/*,docs/*,ci/*,insomnia/*,public/*,tasks/*,noxfile.py + + diff --git a/ci/linting.gitlab-ci.yml b/ci/linting.gitlab-ci.yml new file mode 100644 index 0000000000..ac2c4a178e --- /dev/null +++ b/ci/linting.gitlab-ci.yml @@ -0,0 +1,89 @@ +# Copyright © Nekoka.tt 2019-2020 +# +# This file is part of Hikari. +# +# Hikari is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Hikari is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Hikari. If not, see . + +### +### Base for a generic linting pipeline that is invoked via Nox. +### +.lint: + allow_failure: false + before_script: + - pip install nox + extends: + - .cpython-stable + - .reactive-job + interruptible: true + retry: 2 + stage: lint + +### +### Code linting. +### +### Looks for code style problems, code smells, and general bad things. +### Failures are added as warnings to the pipeline. +### +"pylint": + allow_failure: true + artifacts: + reports: + junit: public/pylint.xml + extends: .lint + script: + - nox -s lint + +### +### Documentation linting. +### +### If the documentation is clear and formatted correctly. Any failures +### become warnings on the pipeline. +### +"pydocstyle": + allow_failure: true + extends: .lint + script: + - nox -s docstyle + +### +### Code formatting. +### +### If the code style is not the Black code style, fail the pipeline. +### +"black": + extends: .lint + script: + - nox -s format -- --check --diff + +### +### Security checks #1 +### +### This runs static application security tests in addition to those that +### GitLab provides. Any issues are reported and the pipeline is aborted. +### +"safety": + extends: .lint + script: + - nox -s safety + +### +### Security checks #2. +### +### This runs static application security tests in addition to those that +### GitLab provides. Any issues are reported and the pipeline is aborted. +### +"bandit": + extends: .lint + script: + - nox -s sast diff --git a/ci/linting.py b/ci/linting.py new file mode 100644 index 0000000000..1c1502a5ca --- /dev/null +++ b/ci/linting.py @@ -0,0 +1,18 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# Copyright © Nekoka.tt 2019-2020 +# +# This file is part of Hikari. +# +# Hikari is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Hikari is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Hikari. If not, see . diff --git a/ci/pages.gitlab-ci.yml b/ci/pages.gitlab-ci.yml new file mode 100644 index 0000000000..853c12a995 --- /dev/null +++ b/ci/pages.gitlab-ci.yml @@ -0,0 +1,101 @@ +# Copyright © Nekoka.tt 2019-2020 +# +# This file is part of Hikari. +# +# Hikari is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Hikari is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Hikari. If not, see . + + +### +### Base for generating documentation ready to be published. +### +.generate-pages: + except: + - merge_requests + - tags + extends: .pages + only: + - master + - staging + retry: + max: 2 + script: |+ + set -x + apt-get update + apt-get install -qy graphviz + pip install requests + source tasks/deploy.sh + bash tasks/retry_aborts.sh pip install nox + git clone ${CI_REPOSITORY_URL} -b ${TARGET_BRANCH} hikari_docs_${TARGET_BRANCH} --depth=1 + cd "hikari_docs_${TARGET_BRANCH}" + bash tasks/retry_aborts.sh nox -s documentation + rm ../public -rf && mkdir ../public + mv public "../public/${TARGET_BRANCH}" + +### +### Base for generating documentation. +### +.pages: + artifacts: + paths: + - public/ + before_script: + - |+ + set -e + set -x + git log -n 1 "${CI_COMMIT_SHA}" --format="%B" \ + | grep -iqE "\[\s*(skip|no|don'?t|do\s+not)\s+pages?\s*\]" \ + && echo "SKIPPING ${CI_JOB_STAGE} STAGE JOB" \ + && exit 0 + mkdir public || true + extends: .cpython-stable + stage: report + +### +### Generate pages for master branch and export them. +### +"master pages": + extends: .generate-pages + variables: + TARGET_BRANCH: master + +### +### Generate pages for staging branch and export them. +### +"staging pages": + extends: .generate-pages + variables: + TARGET_BRANCH: staging + +### +### Generate pages for the current branch as a test. +### +"dry-run pages": + extends: + - .pages + - .reactive-job + script: + - |+ + set -e + set -x + apt-get update + apt-get install -qy graphviz + pip install requests nox + bash tasks/retry_aborts.sh nox -s documentation + except: + - master + - staging + only: + - merge_requests + - branches + diff --git a/gitlab/deploy.yml b/ci/releases.gitlab-ci.yml similarity index 55% rename from gitlab/deploy.yml rename to ci/releases.gitlab-ci.yml index 132052e146..344650635a 100644 --- a/gitlab/deploy.yml +++ b/ci/releases.gitlab-ci.yml @@ -15,52 +15,80 @@ # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . +### +### Base for making a release to PyPI. +### .deploy: - resource_group: deploy - retry: 2 allow_failure: false - stage: deploy - image: python:3.8.1 before_script: - - git log -n 1 "${CI_COMMIT_SHA}" --format="%B" | grep -iqE "\[\s*(skip|no|don'?t|do\s+not)\s+deploy(ments?)?\s*\]" && echo "SKIPPING ${CI_JOB_STAGE} STAGE JOB" && exit 0 - - source tasks/config.sh - - apt-get update - - apt-get install curl openssh-client -qy - - bash tasks/retry_aborts.sh pip install twine wheel - - bash tasks/retry_aborts.sh pip install -e . + - |+ + set -e + set -x + git log -n 1 "${CI_COMMIT_SHA}" --format="%B" \ + | grep -iqE "\[\s*(skip|no|don'?t|do\s+not)\s+deploy(ments?)?\s*\]" \ + && echo "SKIPPING ${CI_JOB_STAGE} STAGE JOB" \ + && exit 0 + source tasks/config.sh + apt-get update + apt-get install curl openssh-client -qy + pip install twine wheel + pip install -e . + extends: .cpython-stable + interruptible: false + resource_group: deploy + retry: 1 + stage: deploy -prerelease: - extends: .deploy - only: - - staging +### +### Make a dev release. +### +### Upload the current code to PyPI as a .dev release. +### This will only work on the staging branch. +### +"dev release": + environment: + name: staging + url: https://nekokatt.ci.io/hikari/staging except: - tags - merge_requests - schedules + extends: .deploy + only: + - staging + script: + - |+ + set -e + set -x + source tasks/deploy.sh + do-deployment variables: RELEASE_WEBHOOK_NAME: "New development snapshot of the next release has been published" RELEASE_WEBHOOK_COLOUR: "0x2C2F33" - script: - - source tasks/deploy.sh - - do-deployment - environment: - name: staging - url: https://nekokatt.gitlab.io/hikari/staging -release: - extends: .deploy - only: - - master +### +### Make a production release. +### +### Upload the current code to PyPI as a production release. This will only +### work on the master branch. +### +"prod release": + environment: + name: prod + url: https://nekokatt.ci.io/hikari except: - tags - merge_requests - schedules + extends: .deploy + only: + - master + script: + - |+ + set -e + set -x + source tasks/deploy.sh + do-deployment variables: RELEASE_WEBHOOK_NAME: "NEW VERSION OF HIKARI HAS BEEN RELEASED!" RELEASE_WEBHOOK_COLOUR: "0x7289DA" - script: - - source tasks/deploy.sh - - do-deployment - environment: - name: prod - url: https://nekokatt.gitlab.io/hikari diff --git a/ci/tests.gitlab-ci.yml b/ci/tests.gitlab-ci.yml new file mode 100644 index 0000000000..f948845136 --- /dev/null +++ b/ci/tests.gitlab-ci.yml @@ -0,0 +1,94 @@ +# Copyright © Nekoka.tt 2019-2020 +# +# This file is part of Hikari. +# +# Hikari is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Hikari is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Hikari. If not, see . + +### +### Base for running Pytest jobs with Nox, exporting coverage to be coalesced +### by the "coverage" job later. +### +.nox-test: + artifacts: + paths: + - public/coverage + - public/*.coverage + reports: + junit: public/tests.xml + extends: .reactive-job + interruptible: true + script: + - |+ + set -e + set -x + apt-get install -qy git gcc g++ make + pip install nox + nox -stest + mv .coverage "public/${CI_JOB_NAME}.coverage" + stage: test + +### +### Run CPython 3.8.0 unit tests and collect test coverage stats. +### +### Coverage is exported as an artifact with a name matching the job name. +### +"test 3.8.0": + extends: + - .cpython380 + - .nox-test + +### +### Run CPython 3.8.1 unit tests and collect test coverage stats. +### +### Coverage is exported as an artifact with a name matching the job name. +### +"test 3.8.1": + extends: + - .cpython381 + - .nox-test + +### +### Run CPython 3.8.2 unit tests and collect test coverage stats. +### +### Coverage is exported as an artifact with a name matching the job name. +### +"test 3.8.2": + extends: + - .cpython382 + - .nox-test + +### +### Run CPython 3.9.0 unit tests and collect test coverage stats. +### +### Coverage is exported as an artifact with a name matching the job name. +### +"test 3.9.0": + extends: + - .cpython390 + - .nox-test + +### +### Run the tests for the version updating scripts. +### +"test versioning": + extends: + - .cpython-stable + - .reactive-job + stage: test + script: + - |+ + set -e + set -x + pip install requests + bash tasks/test_make_version_string.sh diff --git a/gitlab/pages.yml b/gitlab/pages.yml deleted file mode 100644 index 6a4076b2e2..0000000000 --- a/gitlab/pages.yml +++ /dev/null @@ -1,91 +0,0 @@ -# Copyright © Nekoka.tt 2019-2020 -# -# This file is part of Hikari. -# -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . - -.deploy-pages-activation: - retry: - max: 2 - allow_failure: true - artifacts: - paths: - - public/ - only: - - branches - except: - - merge_requests - - tags - -.generate-pages-from-specific-branch: - extends: .deploy-pages-activation - image: python:3.8.2 - stage: test - script: - - apt-get update - - apt-get install -qy graphviz - - pip install requests - - source tasks/deploy.sh - - bash tasks/retry_aborts.sh pip install nox - - git clone ${CI_REPOSITORY_URL} -b ${TARGET_BRANCH} hikari_docs_${TARGET_BRANCH} --depth=1 - - cd hikari_docs_${TARGET_BRANCH} - - bash tasks/retry_aborts.sh nox -s documentation - - rm ../public -rf && mkdir ../public - - mv public "../public/${TARGET_BRANCH}" - only: - - staging - -master-pages: - extends: .generate-pages-from-specific-branch - variables: - TARGET_BRANCH: master - -staging-pages: - extends: .generate-pages-from-specific-branch - variables: - TARGET_BRANCH: staging - -test-pages: - extends: .deploy-pages-activation - image: python:3.8.2 - script: - - apt-get update - - apt-get install -qy graphviz - - pip install requests nox - - bash tasks/retry_aborts.sh nox -s documentation - only: - - merge_requests - - branches - except: - - master - - staging - -pages: # deploys the generated artifacts as pages - extends: .deploy-pages-activation - stage: results - image: busybox:1.31.1-musl - before_script: - - 'mkdir public || true' - - git log -n 1 "${CI_COMMIT_SHA}" --format="%B" | grep -iqE "\[\s*(skip|no|don'?t|do\s+not)\s+pages?\s*\]" && echo "SKIPPING ${CI_JOB_STAGE} STAGE JOB" && exit 0 - dependencies: - - staging-pages - - master-pages - script: - - cd public/master - - 'find . -maxdepth 1 -exec mv {} .. \;' - - cd ../.. - - rm public/master -Rvf - only: - - master - - staging diff --git a/gitlab/test.yml b/gitlab/test.yml deleted file mode 100644 index db56ebb445..0000000000 --- a/gitlab/test.yml +++ /dev/null @@ -1,242 +0,0 @@ -# Copyright © Nekoka.tt 2019-2020 -# -# This file is part of Hikari. -# -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . - -include: - - template: "Dependency-Scanning.gitlab-ci.yml" - - template: "SAST.gitlab-ci.yml" - - template: "License-Scanning.gitlab-ci.yml" - -sast: - interruptible: true - retry: 2 - variables: - SAST_BANDIT_EXCLUDED_PATHS: tests/*,docs/*,gitlab/*,insomnia/*,public/*,tasks/*,noxfile.py - SAST_EXCLUDED_PATHS: tests/*,docs/*,gitlab/*,insomnia/*,public/*,tasks/*,noxfile.py - -license_scanning: - # Overly slow to run and not very important, so skip normally to get 5 minutes back on - # each pipeline. - interruptible: true - retry: 2 - only: - - schedules - -dependency_scanning: - interruptible: true - retry: 2 - only: - - merge_requests - - schedules - -.venv: - before_script: - - which apt-get || (echo "Detected Alpine docker image, installing bash." && apk add bash) - - python -m venv .venv - - source .venv/bin/activate - - bash tasks/retry_aborts.sh pip install nox - only: - - branches - - merge_requests - - tags - - schedules - -.cache: - cache: - key: "${CI_STAGE_NAME}-${CI_JOB_NAME}" - paths: - - .nox/ - - .pytest_cache/ - - .venv/ - -.cpython-3.8.0: - interruptible: true - image: python:3.8.0 - extends: .venv - -.cpython-3.8.1: - interruptible: true - image: python:3.8.1 - extends: .venv - -.cpython-3.8.2: - interruptible: true - image: python:3.8.1 - extends: .venv - -.cpython-3.9-rc: - interruptible: true - image: python:3.9-rc - extends: .venv - -.cpython-tool-alpine: - interruptible: true - image: python:3.8.1-alpine - extends: .venv - -.cpython-tool-buster: - interruptible: true - extends: .cpython-3.8.1 - -.nox-test: - extends: .cache - script: - - apt-get install -qy git gcc g++ make - - bash tasks/retry_aborts.sh pip install nox - - source tasks/config.sh - - mkdir public || true - - bash tasks/retry_aborts.sh nox -stest - - mv .coverage public/${CI_JOB_NAME}.coverage - artifacts: - paths: - - public/coverage - - public/*.coverage - reports: - junit: public/tests.xml - -.nox-pip-install: - extends: .cache - script: - - apt install git -y - - bash tasks/retry_aborts.sh nox --sessions pip pip_sdist pip_bdist_wheel pip_git - -.nox-pip-install-showtime: - script: - - apt install git -y - - bash tasks/retry_aborts.sh nox -s pip -- --showtime - stage: post-deploy - only: - - master - - staging - -pytest-c3.8.0: - extends: - - .cpython-3.8.0 - - .nox-test - -pytest-c3.8.1: - extends: - - .cpython-3.8.1 - - .nox-test - -pytest-c3.8.2: - extends: - - .cpython-3.8.2 - - .nox-test - -pytest-c3.9-rc: - extends: - - .cpython-3.9-rc - - .nox-test - -install-c3.8.0: - extends: - - .cpython-3.8.0 - - .nox-pip-install - -install-c3.8.1: - extends: - - .cpython-3.8.1 - - .nox-pip-install - -install-c3.8.2: - extends: - - .cpython-3.8.2 - - .nox-pip-install - -install-c3.9-rc: - extends: - - .cpython-3.9-rc - - .nox-pip-install - -verify-c3.8.0-pypi: - extends: - - .cpython-3.8.0 - - .nox-pip-install-showtime - -verify-c3.8.1-pypi: - extends: - - .cpython-3.8.1 - - .nox-pip-install-showtime - -verify-c3.8.2-pypi: - extends: - - .cpython-3.8.2 - - .nox-pip-install-showtime - -verify-c3.9-rc-pypi: - extends: - - .cpython-3.9-rc - - .nox-pip-install-showtime - -coverage-coalesce: - extends: - - .cpython-tool-alpine - - .cache - stage: results - script: - - bash tasks/retry_aborts.sh pip install coverage - - ls public - - coverage combine public/*.coverage - - coverage report -i - coverage: /^TOTAL[\s\d%]+\s(\d+(?:\.\d+)?\%)$/ - only: - - branches - - merge_requests - - tags - - schedules - -verify-autoversioning-logic: - extends: .cpython-tool-alpine - stage: test - script: - - bash tasks/retry_aborts.sh pip install requests - - bash tasks/test_make_version_string.sh - -black: - extends: - - .cpython-tool-buster - - .cache - script: - - bash tasks/retry_aborts.sh nox -s format -- --check --diff - -safety: - extends: - - .cpython-tool-buster - script: - - bash tasks/retry_aborts.sh nox -s safety - -pylint: - extends: - - .cpython-tool-buster - - .cache - script: - - bash tasks/retry_aborts.sh nox -s lint - artifacts: - reports: - junit: public/pylint.xml - - -pydocstyle: - allow_failure: true - extends: - - .cpython-tool-buster - - .cache - script: - - bash tasks/retry_aborts.sh nox -s docstyle - artifacts: - reports: - junit: public/pylint.xml diff --git a/hikari/_about.py b/hikari/_about.py index a255fa020c..9c1d002cce 100644 --- a/hikari/_about.py +++ b/hikari/_about.py @@ -21,7 +21,7 @@ __author__ = "Nekokatt" __copyright__ = "© 2019-2020 Nekokatt" -__email__ = "3903853-nekokatt@users.noreply.gitlab.com" +__email__ = "3903853-nekokatt@users.noreply.ci.com" __license__ = "LGPL-3.0-ONLY" __version__ = "1.0.1.dev" __url__ = "https://gitlab.com/nekokatt/hikari" diff --git a/tasks/make_version_string.py b/tasks/make_version_string.py index 2ee050841e..4bc5467b5e 100644 --- a/tasks/make_version_string.py +++ b/tasks/make_version_string.py @@ -34,7 +34,7 @@ log_message("will use", "staging" if is_staging else "prod", "configuration for this next version") if is_pages: - log_message("will not bump versions up, this is just for gitlab pages") + log_message("will not bump versions up, this is just for ci pages") pypi_server = "pypi.org" api_name = os.getenv("API_NAME", "hikari") From 67262d484a7749a8fa5312a52a72d42b4670d7ba Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Mon, 13 Apr 2020 21:47:51 +0100 Subject: [PATCH 116/922] Started rewriting noxfile. --- ci/black.nox.py | 44 +++++ ci/integrations.gitlab-ci.yml | 4 +- ci/{linting.py => nox.py} | 18 ++ ci/{__init__.py => utils.nox.py} | 22 +++ hikari/_about.py | 2 +- lgtm.yml | 2 + noxfile.old.py | 313 +++++++++++++++++++++++++++++++ noxfile.py | 297 +---------------------------- setup.py | 4 +- tasks/make_version_string.py | 2 +- 10 files changed, 411 insertions(+), 297 deletions(-) create mode 100644 ci/black.nox.py rename ci/{linting.py => nox.py} (59%) rename ci/{__init__.py => utils.nox.py} (65%) create mode 100644 noxfile.old.py diff --git a/ci/black.nox.py b/ci/black.nox.py new file mode 100644 index 0000000000..78b6786ec2 --- /dev/null +++ b/ci/black.nox.py @@ -0,0 +1,44 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# Copyright © Nekoka.tt 2019-2020 +# +# This file is part of Hikari. +# +# Hikari is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Hikari is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Hikari. If not, see . +"""Black code-style jobs.""" +from ci import nox + + +PATHS = [ + "hikari", + "tests", + "docs", + "setup.py", + "noxfile.py", + "ci", +] + + +@nox.session(default=True, reuse_venv=True) +def reformat_code(session: nox.Session) -> None: + """Run black code formatter.""" + session.install("black") + session.run("black", *PATHS) + + +@nox.session(reuse_venv=True) +def check_formatting(session: nox.Session) -> None: + """Check that the code matches the black code style.""" + session.install("black") + session.run("black", *PATHS, "--check") diff --git a/ci/integrations.gitlab-ci.yml b/ci/integrations.gitlab-ci.yml index 7672735eee..515d251790 100644 --- a/ci/integrations.gitlab-ci.yml +++ b/ci/integrations.gitlab-ci.yml @@ -126,8 +126,8 @@ sast: retry: 1 stage: lint variables: - SAST_BANDIT_EXCLUDED_PATHS: tests/*,docs/*,ci/*,insomnia/*,public/*,tasks/*,noxfile.py + SAST_BANDIT_EXCLUDED_PATHS: tests/*,docs/*,ci/*,insomnia/*,public/*,tasks/*,noxfile.old.py SAST_DEFAULT_ANALYZERS: "bandit, flawfinder, secrets, kubesec, gitleaks, trufflehog" - SAST_EXCLUDED_PATHS: tests/*,docs/*,ci/*,insomnia/*,public/*,tasks/*,noxfile.py + SAST_EXCLUDED_PATHS: tests/*,docs/*,ci/*,insomnia/*,public/*,tasks/*,noxfile.old.py diff --git a/ci/linting.py b/ci/nox.py similarity index 59% rename from ci/linting.py rename to ci/nox.py index 1c1502a5ca..bd16f45368 100644 --- a/ci/linting.py +++ b/ci/nox.py @@ -16,3 +16,21 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . +"""Wrapper around nox to give default job kwargs.""" +from typing import Callable +from nox.sessions import Session +from nox import * + +_session = session + +options.sessions = [] + + +def session(default: bool = False, reuse_venv: bool = False, **kwargs): + def decorator(func: Callable[[Session], None]): + func.__name__ = func.__name__.replace("_", "-") + if default: + options.sessions.append(func.__name__) + return _session(reuse_venv=reuse_venv, **kwargs)(func) + + return decorator diff --git a/ci/__init__.py b/ci/utils.nox.py similarity index 65% rename from ci/__init__.py rename to ci/utils.nox.py index 1c1502a5ca..46e28f6635 100644 --- a/ci/__init__.py +++ b/ci/utils.nox.py @@ -16,3 +16,25 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . +"""Additional utilities for Nox.""" +import shutil + +from ci import nox + + +TRASH = [ + ".nox", + "build", + "dist", + "hikari.egg-info", + "public", + ".coverage", +] + + +@nox.session(reuse_venv=False) +def purge(_: nox.Session) -> None: + """Delete any nox-generated files.""" + for trash in TRASH: + print("Removing", trash) + shutil.rmtree(trash, ignore_errors=True) diff --git a/hikari/_about.py b/hikari/_about.py index 9c1d002cce..a255fa020c 100644 --- a/hikari/_about.py +++ b/hikari/_about.py @@ -21,7 +21,7 @@ __author__ = "Nekokatt" __copyright__ = "© 2019-2020 Nekokatt" -__email__ = "3903853-nekokatt@users.noreply.ci.com" +__email__ = "3903853-nekokatt@users.noreply.gitlab.com" __license__ = "LGPL-3.0-ONLY" __version__ = "1.0.1.dev" __url__ = "https://gitlab.com/nekokatt/hikari" diff --git a/lgtm.yml b/lgtm.yml index 990eb8eb8a..8ecf2dd36b 100644 --- a/lgtm.yml +++ b/lgtm.yml @@ -2,7 +2,9 @@ path_classifiers: test: - exclude: / - tests/ + - noxfile.old.py - noxfile.py + - ci/ - docs/ - tasks/ queries: diff --git a/noxfile.old.py b/noxfile.old.py new file mode 100644 index 0000000000..e02e3f9d93 --- /dev/null +++ b/noxfile.old.py @@ -0,0 +1,313 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# Copyright © Nekoka.tt 2019-2020 +# +# This file is part of Hikari. +# +# Hikari is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Hikari is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Hikari. If not, see . +import contextlib +import os +import re +import shutil +import subprocess +import tarfile +import tempfile + +import nox.sessions + +nox.options.sessions = [] + + +def default_session(func): + nox.options.sessions.append(func.__name__) + return func + + +def pathify(arg, *args, root=False): + return os.path.abspath(os.path.join(os.path.dirname(os.path.abspath(__file__)) if not root else "/", arg, *args)) + + +# Configuration stuff we probably might move around eventually. +MAIN_PACKAGE = "hikari" +OWNER = "nekokatt" +TECHNICAL_DIR = "technical" +TEST_PATH = "tests/hikari" +ARTIFACT_DIR = "public" +DOCUMENTATION_DIR = "docs" +CI_SCRIPT_DIR = "tasks" +BLACK_PACKAGES = [MAIN_PACKAGE, TEST_PATH] +BLACK_PATHS = [m.replace(".", "/") for m in BLACK_PACKAGES] + [ + __file__, + pathify(DOCUMENTATION_DIR, "conf.py"), + "noxfile.old.py", +] +BLACK_SHIM_PATH = pathify(CI_SCRIPT_DIR, "black.py") +MAIN_PACKAGE_PATH = MAIN_PACKAGE.replace(".", "/") +REPOSITORY = f"https://gitlab.com/{OWNER}/{MAIN_PACKAGE}" + + +@default_session +@nox.session(reuse_venv=True) +def format(session) -> None: + """Reformat code with Black. Pass the '--check' flag to check formatting only.""" + session.install("black") + session.run("python", BLACK_SHIM_PATH, *BLACK_PATHS, *session.posargs) + + +@default_session +@nox.session(reuse_venv=True) +def test(session) -> None: + """Run unit tests in Pytest.""" + session.install("-r", "requirements.txt") + session.install("-r", "dev-requirements.txt") + + additional_opts = ["--pastebin=all"] if os.getenv("CI") else [] + + # Apparently coverage doesn't replace this, leading to "no coverage was + # detected" which is helpful. + with contextlib.suppress(Exception): + shutil.move(".coverage", ".coverage.old") + + session.run( + "python", + "-m", + "pytest", + "-c", + "pytest.ini", + "-r", + "a", + *additional_opts, + "--full-trace", + "-n", + "auto", + "--cov", + MAIN_PACKAGE, + "--cov-config=coverage.ini", + "--cov-report", + "term", + "--cov-report", + f"html:{ARTIFACT_DIR}/coverage/html", + "--cov-branch", + f"--junitxml={ARTIFACT_DIR}/tests.xml", + "--showlocals", + *session.posargs, + TEST_PATH, + ) + + +@default_session +@nox.session(reuse_venv=True) +def docstyle(session) -> None: + """Check docstrings with pydocstyle.""" + session.install("pydocstyle") + session.chdir(MAIN_PACKAGE_PATH) + # add -e flag for explainations. + session.run("pydocstyle", "--config=../pydocstyle.ini") + + +@default_session +@nox.session(reuse_venv=True) +def lint(session) -> None: + """Check formating with pylint""" + session.install("-r", "requirements.txt") + session.install("-r", "dev-requirements.txt") + session.install("-r", "doc-requirements.txt") + session.install("pylint-junit==0.2.0") + session.install("pylint") + pkg = MAIN_PACKAGE.split(".")[0] + + try: + session.run( + "pylint", + pkg, + "--rcfile=pylint.ini", + "--spelling-private-dict-file=dict.txt", + success_codes=list(range(0, 256)), + ) + finally: + os.makedirs(ARTIFACT_DIR, exist_ok=True) + + with open(os.path.join(ARTIFACT_DIR, "pylint.xml"), "w") as fp: + session.run( + "pylint", + pkg, + "--rcfile=pylint.ini", + "--output-format=pylint_junit.JUnitReporter", + stdout=fp, + success_codes=list(range(0, 256)), + ) + + +@default_session +@nox.session(reuse_venv=True) +def sast(session) -> None: + """Run static application security testing with Bandit.""" + session.install("bandit") + pkg = MAIN_PACKAGE.split(".")[0] + session.run("bandit", pkg, "-r") + + +@default_session +@nox.session(reuse_venv=True) +def safety(session) -> None: + """Run safety checks against a vulnerability database using Safety.""" + session.install("-r", "requirements.txt") + session.install("safety") + session.run("safety", "check") + + +@default_session +@nox.session(reuse_venv=True) +def documentation(session) -> None: + """Generate documentation using Sphinx for the current branch.""" + session.install("-r", "requirements.txt") + session.install("-r", "dev-requirements.txt") + session.install("-r", "doc-requirements.txt") + + session.env["SPHINXOPTS"] = "-WTvvn" + session.run("sphinx-apidoc", "-e", "-o", DOCUMENTATION_DIR, MAIN_PACKAGE) + session.run( + "python", "-m", "sphinx.cmd.build", "-a", "-b", "html", "-j", "auto", "-n", DOCUMENTATION_DIR, ARTIFACT_DIR + ) + for f in os.listdir(DOCUMENTATION_DIR): + if f in ("hikari.rst", "modules.rst") or re.match(r"hikari\.(\w|\.)+\.rst", f): + os.unlink(pathify(DOCUMENTATION_DIR, f)) + + +if os.getenv("CI"): + + @nox.session(reuse_venv=False) + def pip(session: nox.sessions.Session): + """Run through sandboxed install of PyPI package (if running on CI)""" + if "--showtime" not in session.posargs: + pip_showtime(session) + else: + try: + pip_from_ref(session) + except Exception: + print("Failed to install from GitLab.") + raise KeyError from None + + +@nox.session(reuse_venv=False) +def pip_bdist_wheel(session: nox.sessions.Session): + """ + Test installing a bdist_wheel package. + """ + session.install("wheel") + session.run("python", "setup.py", "build", "bdist_wheel") + + print("Testing installing from wheel") + with tempfile.TemporaryDirectory() as temp_dir: + with temp_chdir(session, temp_dir) as project_dir: + dist = os.path.join(project_dir, "dist") + wheels = [os.path.join(dist, wheel) for wheel in os.listdir(dist) if wheel.endswith(".whl")] + wheels.sort(key=lambda wheel: os.stat(wheel).st_ctime) + newest_wheel = wheels.pop() + newest_wheel_name = os.path.basename(newest_wheel) + print(f"copying newest wheel found at {newest_wheel} and installing it in temp dir") + shutil.copyfile(newest_wheel, newest_wheel_name) + session.run("pip", "install", newest_wheel_name) + session.run("python", "-m", MAIN_PACKAGE) + + print("Installed as wheel in temporary environment successfully!") + + +@nox.session(reuse_venv=False) +def pip_sdist(session: nox.sessions.Session): + """ + Test installing an sdist package. + """ + session.install("wheel") + session.run("python", "setup.py", "build", "sdist") + + print("Testing installing from wheel") + with tempfile.TemporaryDirectory() as temp_dir: + with temp_chdir(session, temp_dir) as project_dir: + dist = os.path.join(project_dir, "dist") + wheels = [os.path.join(dist, wheel) for wheel in os.listdir(dist) if wheel.endswith(".tar.gz")] + wheels.sort(key=lambda wheel: os.stat(wheel).st_ctime) + newest_tarball = wheels.pop() + newest_tarball_name = os.path.basename(newest_tarball) + + if newest_tarball_name.lower().endswith(".tar.gz"): + newest_tarball_dir = newest_tarball_name[: -len(".tar.gz")] + else: + newest_tarball_dir = newest_tarball_name[: -len(".tgz")] + + print(f"copying newest tarball found at {newest_tarball} and installing it in temp dir") + shutil.copyfile(newest_tarball, newest_tarball_name) + + print("extracting tarball") + with tarfile.open(newest_tarball_name) as tar: + tar.extractall() + + print("installing sdist") + with temp_chdir(session, newest_tarball_dir): + session.run("python", "setup.py", "install") + session.run("python", "-m", MAIN_PACKAGE) + + print("Installed as wheel in temporary environment successfully!") + + +@nox.session(reuse_venv=False) +def pip_git(session: nox.sessions.Session): + """ + Test installing repository from Git. + """ + print("Testing installing from git repository only") + + try: + branch = os.environ["CI_COMMIT_SHA"] + except KeyError: + branch = subprocess.check_output(["git", "symbolic-ref", "--short", "HEAD"]).decode("utf8")[0:-1] + + print("Testing for branch", branch) + + with tempfile.TemporaryDirectory() as temp_dir: + with temp_chdir(session, temp_dir) as project_dir: + session.install(f"git+file://{project_dir}") + session.install(MAIN_PACKAGE) + session.run("python", "-m", MAIN_PACKAGE) + + print("Installed as git dir in temporary environment successfully!") + + +def pip_showtime(session): + print("Testing we can install packaged pypi object") + session.install(MAIN_PACKAGE) + session.run("python", "-c", f"import {MAIN_PACKAGE}; print({MAIN_PACKAGE}.__version__)") + # Prevent nox caching old versions and using those when tests run. + session.run("pip", "uninstall", "-vvv", "-y", MAIN_PACKAGE) + + +def pip_from_ref(session): + print("Testing published ref can be installed as a package.") + url = session.env.get("CI_PROJECT_URL", REPOSITORY) + sha1 = session.env.get("CI_COMMIT_SHA", "master") + slug = f"git+{url}.git@{sha1}" + session.install(slug) + session.run("python", "-c", f"import {MAIN_PACKAGE}; print({MAIN_PACKAGE}.__version__)") + # Prevent nox caching old versions and using those when tests run. + session.run("pip", "uninstall", "-vvv", "-y", MAIN_PACKAGE) + + +@contextlib.contextmanager +def temp_chdir(session, target): + cwd = os.path.abspath(os.getcwd()) + print("Changing directory from", cwd, "to", target) + session.chdir(target) + yield cwd + print("Changing directory from", target, "to", cwd) + session.chdir(cwd) diff --git a/noxfile.py b/noxfile.py index 9f2f543e4f..151a190ec8 100644 --- a/noxfile.py +++ b/noxfile.py @@ -16,298 +16,15 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . -import contextlib import os -import re -import shutil -import subprocess -import tarfile -import tempfile +import runpy +import sys -import nox.sessions +CI_PATH = "ci" -nox.options.sessions = [] +sys.path.append(os.getcwd()) -def default_session(func): - nox.options.sessions.append(func.__name__) - return func - - -def pathify(arg, *args, root=False): - return os.path.abspath(os.path.join(os.path.dirname(os.path.abspath(__file__)) if not root else "/", arg, *args)) - - -# Configuration stuff we probably might move around eventually. -MAIN_PACKAGE = "hikari" -OWNER = "nekokatt" -TECHNICAL_DIR = "technical" -TEST_PATH = "tests/hikari" -ARTIFACT_DIR = "public" -DOCUMENTATION_DIR = "docs" -CI_SCRIPT_DIR = "tasks" -BLACK_PACKAGES = [MAIN_PACKAGE, TEST_PATH] -BLACK_PATHS = [m.replace(".", "/") for m in BLACK_PACKAGES] + [ - __file__, - pathify(DOCUMENTATION_DIR, "conf.py"), - "noxfile.py", -] -BLACK_SHIM_PATH = pathify(CI_SCRIPT_DIR, "black.py") -MAIN_PACKAGE_PATH = MAIN_PACKAGE.replace(".", "/") -REPOSITORY = f"https://gitlab.com/{OWNER}/{MAIN_PACKAGE}" - - -@default_session -@nox.session(reuse_venv=True) -def format(session) -> None: - """Reformat code with Black. Pass the '--check' flag to check formatting only.""" - session.install("black") - session.run("python", BLACK_SHIM_PATH, *BLACK_PATHS, *session.posargs) - - -@default_session -@nox.session(reuse_venv=True) -def test(session) -> None: - """Run unit tests in Pytest.""" - session.install("-r", "requirements.txt") - session.install("-r", "dev-requirements.txt") - - additional_opts = ["--pastebin=all"] if os.getenv("CI") else [] - - # Apparently coverage doesn't replace this, leading to "no coverage was - # detected" which is helpful. - with contextlib.suppress(Exception): - shutil.move(".coverage", ".coverage.old") - - session.run( - "python", - "-m", - "pytest", - "-c", - "pytest.ini", - "-r", - "a", - *additional_opts, - "--full-trace", - "-n", - "auto", - "--cov", - MAIN_PACKAGE, - "--cov-config=coverage.ini", - "--cov-report", - "term", - "--cov-report", - f"html:{ARTIFACT_DIR}/coverage/html", - "--cov-branch", - f"--junitxml={ARTIFACT_DIR}/tests.xml", - "--showlocals", - *session.posargs, - TEST_PATH, - ) - - -@default_session -@nox.session(reuse_venv=True) -def docstyle(session) -> None: - """Check docstrings with pydocstyle.""" - session.install("pydocstyle") - session.chdir(MAIN_PACKAGE_PATH) - # add -e flag for explainations. - session.run("pydocstyle", "--config=../pydocstyle.ini") - - -@default_session -@nox.session(reuse_venv=True) -def lint(session) -> None: - """Check formating with pylint""" - session.install("-r", "requirements.txt") - session.install("-r", "dev-requirements.txt") - session.install("-r", "doc-requirements.txt") - session.install("pylint-junit==0.2.0") - session.install("pylint") - pkg = MAIN_PACKAGE.split(".")[0] - - try: - session.run( - "pylint", - pkg, - "--rcfile=pylint.ini", - "--spelling-private-dict-file=dict.txt", - success_codes=list(range(0, 256)), - ) - finally: - os.makedirs(ARTIFACT_DIR, exist_ok=True) - - with open(os.path.join(ARTIFACT_DIR, "pylint.xml"), "w") as fp: - session.run( - "pylint", - pkg, - "--rcfile=pylint.ini", - "--output-format=pylint_junit.JUnitReporter", - stdout=fp, - success_codes=list(range(0, 256)), - ) - - -@default_session -@nox.session(reuse_venv=True) -def sast(session) -> None: - """Run static application security testing with Bandit.""" - session.install("bandit") - pkg = MAIN_PACKAGE.split(".")[0] - session.run("bandit", pkg, "-r") - - -@default_session -@nox.session(reuse_venv=True) -def safety(session) -> None: - """Run safety checks against a vulnerability database using Safety.""" - session.install("-r", "requirements.txt") - session.install("safety") - session.run("safety", "check") - - -@default_session -@nox.session(reuse_venv=True) -def documentation(session) -> None: - """Generate documentation using Sphinx for the current branch.""" - session.install("-r", "requirements.txt") - session.install("-r", "dev-requirements.txt") - session.install("-r", "doc-requirements.txt") - - session.env["SPHINXOPTS"] = "-WTvvn" - session.run("sphinx-apidoc", "-e", "-o", DOCUMENTATION_DIR, MAIN_PACKAGE) - session.run( - "python", "-m", "sphinx.cmd.build", "-a", "-b", "html", "-j", "auto", "-n", DOCUMENTATION_DIR, ARTIFACT_DIR - ) - for f in os.listdir(DOCUMENTATION_DIR): - if f in ("hikari.rst", "modules.rst") or re.match(r"hikari\.(\w|\.)+\.rst", f): - os.unlink(pathify(DOCUMENTATION_DIR, f)) - - -if os.getenv("CI"): - - @nox.session(reuse_venv=False) - def pip(session: nox.sessions.Session): - """Run through sandboxed install of PyPI package (if running on CI)""" - if "--showtime" not in session.posargs: - pip_showtime(session) - else: - try: - pip_from_ref(session) - except Exception: - print("Failed to install from GitLab.") - raise KeyError from None - - -@nox.session(reuse_venv=False) -def pip_bdist_wheel(session: nox.sessions.Session): - """ - Test installing a bdist_wheel package. - """ - session.install("wheel") - session.run("python", "setup.py", "build", "bdist_wheel") - - print("Testing installing from wheel") - with tempfile.TemporaryDirectory() as temp_dir: - with temp_chdir(session, temp_dir) as project_dir: - dist = os.path.join(project_dir, "dist") - wheels = [os.path.join(dist, wheel) for wheel in os.listdir(dist) if wheel.endswith(".whl")] - wheels.sort(key=lambda wheel: os.stat(wheel).st_ctime) - newest_wheel = wheels.pop() - newest_wheel_name = os.path.basename(newest_wheel) - print(f"copying newest wheel found at {newest_wheel} and installing it in temp dir") - shutil.copyfile(newest_wheel, newest_wheel_name) - session.run("pip", "install", newest_wheel_name) - session.run("python", "-m", MAIN_PACKAGE) - - print("Installed as wheel in temporary environment successfully!") - - -@nox.session(reuse_venv=False) -def pip_sdist(session: nox.sessions.Session): - """ - Test installing an sdist package. - """ - session.install("wheel") - session.run("python", "setup.py", "build", "sdist") - - print("Testing installing from wheel") - with tempfile.TemporaryDirectory() as temp_dir: - with temp_chdir(session, temp_dir) as project_dir: - dist = os.path.join(project_dir, "dist") - wheels = [os.path.join(dist, wheel) for wheel in os.listdir(dist) if wheel.endswith(".tar.gz")] - wheels.sort(key=lambda wheel: os.stat(wheel).st_ctime) - newest_tarball = wheels.pop() - newest_tarball_name = os.path.basename(newest_tarball) - - if newest_tarball_name.lower().endswith(".tar.gz"): - newest_tarball_dir = newest_tarball_name[: -len(".tar.gz")] - else: - newest_tarball_dir = newest_tarball_name[: -len(".tgz")] - - print(f"copying newest tarball found at {newest_tarball} and installing it in temp dir") - shutil.copyfile(newest_tarball, newest_tarball_name) - - print("extracting tarball") - with tarfile.open(newest_tarball_name) as tar: - tar.extractall() - - print("installing sdist") - with temp_chdir(session, newest_tarball_dir): - session.run("python", "setup.py", "install") - session.run("python", "-m", MAIN_PACKAGE) - - print("Installed as wheel in temporary environment successfully!") - - -@nox.session(reuse_venv=False) -def pip_git(session: nox.sessions.Session): - """ - Test installing repository from Git. - """ - print("Testing installing from git repository only") - - try: - branch = os.environ["CI_COMMIT_SHA"] - except KeyError: - branch = subprocess.check_output(["git", "symbolic-ref", "--short", "HEAD"]).decode("utf8")[0:-1] - - print("Testing for branch", branch) - - with tempfile.TemporaryDirectory() as temp_dir: - with temp_chdir(session, temp_dir) as project_dir: - session.install(f"git+file://{project_dir}") - session.install(MAIN_PACKAGE) - session.run("python", "-m", MAIN_PACKAGE) - - print("Installed as git dir in temporary environment successfully!") - - -def pip_showtime(session): - print("Testing we can install packaged pypi object") - session.install(MAIN_PACKAGE) - session.run("python", "-c", f"import {MAIN_PACKAGE}; print({MAIN_PACKAGE}.__version__)") - # Prevent nox caching old versions and using those when tests run. - session.run("pip", "uninstall", "-vvv", "-y", MAIN_PACKAGE) - - -def pip_from_ref(session): - print("Testing published ref can be installed as a package.") - url = session.env.get("CI_PROJECT_URL", REPOSITORY) - sha1 = session.env.get("CI_COMMIT_SHA", "master") - slug = f"git+{url}.git@{sha1}" - session.install(slug) - session.run("python", "-c", f"import {MAIN_PACKAGE}; print({MAIN_PACKAGE}.__version__)") - # Prevent nox caching old versions and using those when tests run. - session.run("pip", "uninstall", "-vvv", "-y", MAIN_PACKAGE) - - -@contextlib.contextmanager -def temp_chdir(session, target): - cwd = os.path.abspath(os.getcwd()) - print("Changing directory from", cwd, "to", target) - session.chdir(target) - yield cwd - print("Changing directory from", target, "to", cwd) - session.chdir(cwd) +for f in os.listdir(CI_PATH): + if f.endswith(".nox.py"): + runpy.run_path(os.path.join(CI_PATH, f)) diff --git a/setup.py b/setup.py index 3defab75ae..83caba2ac8 100644 --- a/setup.py +++ b/setup.py @@ -34,9 +34,7 @@ def parse_meta(): with open(os.path.join(name, "_about.py")) as fp: code = fp.read() - token_pattern = re.compile( - r"^__(?P\w+)?__\s*=\s*(?P(?:'{3}|\"{3}|'|\"))(?P.*?)(?P=quote)", re.M - ) + token_pattern = re.compile(r"^__(?P\w+)?__\s*=\s*(?P(?:'{3}|\"{3}|'|\"))(?P.*?)(?P=quote)", re.M) groups = {} diff --git a/tasks/make_version_string.py b/tasks/make_version_string.py index 4bc5467b5e..2ee050841e 100644 --- a/tasks/make_version_string.py +++ b/tasks/make_version_string.py @@ -34,7 +34,7 @@ log_message("will use", "staging" if is_staging else "prod", "configuration for this next version") if is_pages: - log_message("will not bump versions up, this is just for ci pages") + log_message("will not bump versions up, this is just for gitlab pages") pypi_server = "pypi.org" api_name = os.getenv("API_NAME", "hikari") From 308582e556c734deee84844edbc9bda0569545f4 Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Tue, 14 Apr 2020 23:32:56 +0100 Subject: [PATCH 117/922] Implemented deploy scripts in nox --- ci/bandit.nox.py | 30 +++ ci/black.nox.py | 1 - ci/config.py | 66 ++++++ ci/deploy.nox.py | 235 +++++++++++++++++++ ci/installations.gitlab-ci.yml | 2 +- ci/integrations.gitlab-ci.yml | 14 +- ci/linting.gitlab-ci.yml | 8 +- ci/nox.py | 13 +- ci/pages.gitlab-ci.yml | 10 +- ci/pip.nox.py | 141 +++++++++++ tasks/black.py => ci/pydocstyle.nox.py | 28 +-- ci/pylint.nox.py | 64 +++++ ci/pytest.nox.py | 65 +++++ ci/releases.gitlab-ci.yml | 16 +- ci/safety.nox.py | 30 +++ ci/sphinx.nox.py | 57 +++++ ci/tests.gitlab-ci.yml | 17 +- doc-requirements.txt | 4 - noxfile.old.py | 313 ------------------------- tasks/config.sh | 70 ------ tasks/deploy.sh | 88 ------- tasks/gendoc.py | 99 -------- tasks/make_version_string.py | 147 ------------ tasks/notify.py | 53 ----- tasks/retry_aborts.sh | 30 --- tasks/test_fail.sh | 12 - tasks/test_make_version_string.sh | 62 ----- tasks/transform_versions.sh | 10 - tests/hikari/net/test_rest.py | 4 +- 29 files changed, 729 insertions(+), 960 deletions(-) create mode 100644 ci/bandit.nox.py create mode 100644 ci/config.py create mode 100644 ci/deploy.nox.py create mode 100644 ci/pip.nox.py rename tasks/black.py => ci/pydocstyle.nox.py (61%) create mode 100644 ci/pylint.nox.py create mode 100644 ci/pytest.nox.py create mode 100644 ci/safety.nox.py create mode 100644 ci/sphinx.nox.py delete mode 100644 doc-requirements.txt delete mode 100644 noxfile.old.py delete mode 100644 tasks/config.sh delete mode 100644 tasks/deploy.sh delete mode 100644 tasks/gendoc.py delete mode 100644 tasks/make_version_string.py delete mode 100644 tasks/notify.py delete mode 100644 tasks/retry_aborts.sh delete mode 100644 tasks/test_fail.sh delete mode 100644 tasks/test_make_version_string.sh delete mode 100644 tasks/transform_versions.sh diff --git a/ci/bandit.nox.py b/ci/bandit.nox.py new file mode 100644 index 0000000000..19ba850083 --- /dev/null +++ b/ci/bandit.nox.py @@ -0,0 +1,30 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# Copyright © Nekoka.tt 2019-2020 +# +# This file is part of Hikari. +# +# Hikari is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Hikari is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Hikari. If not, see . +"""Static application security testing.""" + +from ci import config +from ci import nox + + +# Do not reuse venv, download new definitions each run. +@nox.session(reuse_venv=False, default=True) +def bandit(session: nox.Session) -> None: + """Run static application security tests.""" + session.install("bandit") + session.run("bandit", config.MAIN_PACKAGE, "-r") diff --git a/ci/black.nox.py b/ci/black.nox.py index 78b6786ec2..20fad233fe 100644 --- a/ci/black.nox.py +++ b/ci/black.nox.py @@ -26,7 +26,6 @@ "docs", "setup.py", "noxfile.py", - "ci", ] diff --git a/ci/config.py b/ci/config.py new file mode 100644 index 0000000000..c21b474495 --- /dev/null +++ b/ci/config.py @@ -0,0 +1,66 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# Copyright © Nekoka.tt 2019-2020 +# +# This file is part of Hikari. +# +# Hikari is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Hikari is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Hikari. If not, see . +import os +import os as _os + +IS_CI = "CI" in _os.environ + +# PyPI dependencies +REQUIREMENTS = "requirements.txt" +DEV_REQUIREMENTS = "dev-requirements.txt" + +# Packaging +MAIN_PACKAGE = "hikari" +TEST_PACKAGE = "tests" + +# Generating documentation and artifacts. +ARTIFACT_DIRECTORY = "public" +DOCUMENTATION_DIRECTORY = "docs" + +# Linting and test configs. +PYDOCSTYLE_INI = "pydocstyle.ini" +PYLINT_INI = "pylint.ini" +PYLINT_JUNIT_OUTPUT_PATH = _os.path.join(ARTIFACT_DIRECTORY, "pylint.xml") +PYTEST_INI = "pytest.ini" +COVERAGE_HTML_PATH = _os.path.join(ARTIFACT_DIRECTORY, "coverage", "html") +COVERAGE_JUNIT_PATH = _os.path.join(ARTIFACT_DIRECTORY, "tests.xml") +COVERAGE_INI = "coverage.ini" + +# Deployment variables; these only apply to CI stuff specifically. +VERSION_FILE = _os.path.join(MAIN_PACKAGE, "_about.py") +API_NAME = "hikari" +GIT_SVC_HOST = "gitlab.com" +GIT_TEST_SSH_PATH = "git@gitlab.com" +AUTHOR = "Nekokatt" +ORIGINAL_REPO_URL = f"https://{GIT_SVC_HOST}/${AUTHOR}/{API_NAME}" +SSH_DIR = "~/.ssh" +SSH_PRIVATE_KEY_PATH = os.path.join(SSH_DIR, "id_rsa") +SSH_KNOWN_HOSTS = os.path.join(SSH_DIR, "known_hosts") +CI_ROBOT_NAME = AUTHOR +CI_ROBOT_EMAIL = "3903853-nekokatt@users.noreply.gitlab.com" +SKIP_CI_PHRASE = "[skip ci]" +SKIP_DEPLOY_PHRASE = "[skip deploy]" +SKIP_PAGES_PHRASE = "[skip pages]" +PROD_BRANCH = "master" +PREPROD_BRANCH = "staging" +REMOTE_NAME = "origin" +DISTS = ["sdist", "bdist_wheel"] +PYPI_REPO = "https://upload.pypi.org/legacy/" +PYPI = "https://pypi.org/" +PYPI_API = f"{PYPI}/pypi/{API_NAME}/json" diff --git a/ci/deploy.nox.py b/ci/deploy.nox.py new file mode 100644 index 0000000000..bb3e5e9d15 --- /dev/null +++ b/ci/deploy.nox.py @@ -0,0 +1,235 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# Copyright © Nekoka.tt 2019-2020 +# +# This file is part of Hikari. +# +# Hikari is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Hikari is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Hikari. If not, see . +"""Deployment scripts for CI only.""" +import contextlib +import json +import os +import re +import shlex +import subprocess + +from distutils.version import LooseVersion + +from ci import config +from ci import nox + + +def shell(arg, *args): + command = ' '.join((arg, *args)) + print("\033[35mnox > shell >\033[0m", command) + return subprocess.check_call(command, shell=True) + + +def update_version_string(version): + print("Updating version in version file to", version) + shell("sed", shlex.quote(f"s|^__version__.*|__version__ = \"{version}\"|g"), "-i", config.VERSION_FILE) + + +def increment_prod_to_next_dev(version): + version_obj = LooseVersion(version) + last_index = len(version_obj.version) - 1 + bits = [*map(str, version_obj.version[:last_index]), f"{version_obj.version[last_index] + 1}.dev"] + next_dev = ".".join(bits) + print(version, "prod version will be incremented to", next_dev) + return next_dev + + +def get_current_version(): + with open(config.VERSION_FILE) as fp: + fp_content = fp.read() + + aboutpy_v = LooseVersion(re.findall(r"^__version__\s*=\s*\"(.*?)\"", fp_content, re.M)[0]) + if not hasattr(aboutpy_v, "vstring"): + print("Corrupt _about.py, using default version 0.0.0") + current = "0.0.0" + else: + current = aboutpy_v.vstring + print("Current version", current) + return current + + +def get_next_prod_version_from_dev(version): + bits = LooseVersion(version).version[:3] + prod = ".".join(map(str, bits)) + print(version, "maps to prod release", prod) + return prod + + +def get_next_dev_version(version): + import requests + + version = LooseVersion(version) + + with requests.get(config.PYPI_API) as resp: + print("Looking at existing versions on", config.PYPI_API) + + if resp.status_code == 404: + print("Package does not seem to yet be deployed, using dummy values.") + return "0.0.1.dev1" + else: + resp.raise_for_status() + root = resp.json() + print("Found existing versions online, so adjusting versions to follow from that where appropriate...") + dev_releases = [LooseVersion(r) for r in root["releases"] if "dev" in r] + same_micro_dev_releases = [r for r in dev_releases if r.version[:3] == version.version[:3]] + latest_matching_staging_v = max(same_micro_dev_releases) if same_micro_dev_releases else version + try: + next_patch = latest_matching_staging_v.version[4] + 1 + except IndexError: + # someone messed the version string up or something, meh, just assume it is fine. + print(latest_matching_staging_v, "doesn't match a patch staging version, so just ignoring it") + next_patch = 1 + print("Using next patch of", next_patch) + bits = [*map(str, latest_matching_staging_v.version[:3]), f"dev{next_patch}"] + return ".".join(bits) + + +def build(session: nox.Session) -> None: + print("Building code") + session.install("-e", ".") + + +def deploy_to_pypi(session: nox.Session) -> None: + print("Performing PyPI deployment of current code") + session.run("python", "setup.py", *config.DISTS) + session.install("twine") + session.env["TWINE_USERNAME"] = os.environ["PYPI_USER"] + session.env["TWINE_PASSWORD"] = os.environ["PYPI_PASS"] + session.env["REPOSITORY_URL"] = config.PYPI_REPO + dists = [os.path.join("dist", n) for n in os.listdir("dist")] + session.run("twine", "upload", "--disable-progress-bar", "--skip-existing", *dists) + session.env.pop("TWINE_USERNAME") + session.env.pop("TWINE_PASSWORD") + session.env.pop("REPOSITORY_URL") + + +def deploy_to_git(next_version: str) -> None: + print("Registering SSH private key into container") + shell("eval $(ssh-agent -s)") + with contextlib.suppress(subprocess.CalledProcessError): + shell("mkdir", config.SSH_DIR) + shell("echo ${GIT_SSH_PRIVATE_KEY} >", config.SSH_PRIVATE_KEY_PATH) + shell("chmod 600", config.SSH_PRIVATE_KEY_PATH) + shell("ssh-keyscan -t rsa", config.GIT_SVC_HOST, ">>", config.SSH_KNOWN_HOSTS) + shell("ssh-add", config.SSH_PRIVATE_KEY_PATH) + + print("Fetching all branches") + shell("git fetch --all") + + print("Setting up the git repository ready to make automated changes") + shell("git config user.name", shlex.quote(config.CI_ROBOT_NAME)) + shell("git config user.email", shlex.quote(config.CI_ROBOT_EMAIL)) + shell( + "git remote set-url", + config.REMOTE_NAME, + '$(echo "$CI_REPOSITORY_URL" | perl -pe \'s#.*@(.+?(\\:\\d+)?)/#git@\\1:#\')' + ) + + print("Testing that I can contact the SVC host by SSH") + shell("ssh", config.GIT_TEST_SSH_PATH) + + print("Making deployment commit") + shell( + "git commit -am", + shlex.quote(f"(ci) Deployed {next_version} to PyPI {config.SKIP_DEPLOY_PHRASE}"), + "--allow-empty", + ) + + print("Tagging release") + shell("git tag", next_version) + + print("Merging prod back into preprod") + shell("git checkout", config.PREPROD_BRANCH) + shell(f"git reset --hard {config.REMOTE_NAME}/{config.PREPROD_BRANCH}") + + shell( + f"git merge {config.REMOTE_NAME}/{config.PROD_BRANCH}", + "--no-ff --strategy-option theirs --allow-unrelated-histories -m", + shlex.quote(f"(ci) Merged {config.PROD_BRANCH} {next_version} into {config.PREPROD_BRANCH}") + ) + update_version_string(increment_prod_to_next_dev(next_version)) + + print("Making next dev commit on preprod") + shell("git commit -am", shlex.quote(f"(ci) Updated version for next development release {config.SKIP_DEPLOY_PHRASE}")) + shell("git push --atomic", config.REMOTE_NAME, config.PREPROD_BRANCH, config.PROD_BRANCH, next_version) + + +def send_notification(version: str, title: str, description: str, color: str) -> None: + print("Sending webhook to Discord") + shell( + "curl", + "-X POST", + "-H", shlex.quote("Content-Type: application/json"), + "-d", shlex.quote(json.dumps({ + "embeds": [ + { + "title": title, + "description": description, + "author": {"name": config.AUTHOR}, + "footer": {"text": f"v{version}"}, + "url": f"{config.PYPI}/project/{config.API_NAME}/{version}", + "color": int(color, 16) + } + ] + })), + os.environ["RELEASE_WEBHOOK"], + ) + + +@nox.session() +def deploy(session: nox.Session) -> None: + """Perform a deployment. This will only work on the CI.""" + shell("pip install requests") + commit_ref = os.getenv("CI_COMMIT_REF_NAME", *session.posargs[0:1]) + print("Commit ref is", commit_ref) + current_version = get_current_version() + + if commit_ref == config.PREPROD_BRANCH: + print("preprod release!") + build(session) + next_version = get_next_dev_version(current_version) + update_version_string(next_version) + deploy_to_pypi(session) + send_notification( + next_version, + f"{config.API_NAME} v{next_version} has been released", + "Pick up the latest development release from pypi by running:\n" + "```bash\n" + f"pip install --pre -U {config.API_NAME}=={next_version}\n" + "```", + "2C2F33" + ) + elif commit_ref == config.PROD_BRANCH: + print("prod release!") + build(session) + next_version = get_next_prod_version_from_dev(current_version) + update_version_string(next_version) + deploy_to_pypi(session) + deploy_to_git(next_version) + send_notification( + next_version, + f"{config.API_NAME} v{next_version} has been released", + "Pick up the latest stable release from pypi by running:\n" + "```bash\n" + f"pip install -U {config.API_NAME}=={next_version}\n" + "```", + "7289DA" + ) + else: + print("not preprod or prod branch, nothing will be performed.") diff --git a/ci/installations.gitlab-ci.yml b/ci/installations.gitlab-ci.yml index 25a5b737c1..098a0a5304 100644 --- a/ci/installations.gitlab-ci.yml +++ b/ci/installations.gitlab-ci.yml @@ -26,7 +26,7 @@ set -x apt install git -y pip install nox - nox --sessions pip pip_sdist pip_bdist_wheel pip_git + nox --sessions pip pip-sdist pip-bdist-wheel pip-git stage: install ### ### Setup script tests for CPython 3.8.0 diff --git a/ci/integrations.gitlab-ci.yml b/ci/integrations.gitlab-ci.yml index 515d251790..6956183614 100644 --- a/ci/integrations.gitlab-ci.yml +++ b/ci/integrations.gitlab-ci.yml @@ -36,9 +36,8 @@ include: - |+ set -e set -x - pip install coverage - coverage combine "public/*.coverage" - coverage report -i + pip install nox + nox -s coalesce-coverage stage: report ### @@ -123,11 +122,14 @@ pages: ### sast: interruptible: true + only: + # Overly slow to run and not very important, so skip normally to get 5 + # minutes back on + # each pipeline. + - merge_requests + - schedules retry: 1 stage: lint variables: SAST_BANDIT_EXCLUDED_PATHS: tests/*,docs/*,ci/*,insomnia/*,public/*,tasks/*,noxfile.old.py - SAST_DEFAULT_ANALYZERS: "bandit, flawfinder, secrets, kubesec, gitleaks, trufflehog" SAST_EXCLUDED_PATHS: tests/*,docs/*,ci/*,insomnia/*,public/*,tasks/*,noxfile.old.py - - diff --git a/ci/linting.gitlab-ci.yml b/ci/linting.gitlab-ci.yml index ac2c4a178e..295db11edd 100644 --- a/ci/linting.gitlab-ci.yml +++ b/ci/linting.gitlab-ci.yml @@ -42,7 +42,7 @@ junit: public/pylint.xml extends: .lint script: - - nox -s lint + - nox -s pylint ### ### Documentation linting. @@ -54,7 +54,7 @@ allow_failure: true extends: .lint script: - - nox -s docstyle + - nox -s pydocstyle ### ### Code formatting. @@ -64,7 +64,7 @@ "black": extends: .lint script: - - nox -s format -- --check --diff + - nox -s check-formatting -- --check --diff ### ### Security checks #1 @@ -86,4 +86,4 @@ "bandit": extends: .lint script: - - nox -s sast + - nox -s bandit diff --git a/ci/nox.py b/ci/nox.py index bd16f45368..242fa4b3b0 100644 --- a/ci/nox.py +++ b/ci/nox.py @@ -18,19 +18,20 @@ # along with Hikari. If not, see . """Wrapper around nox to give default job kwargs.""" from typing import Callable + from nox.sessions import Session -from nox import * +from nox import session as _session +from nox import options as _options -_session = session -options.sessions = [] +_options.sessions = [] -def session(default: bool = False, reuse_venv: bool = False, **kwargs): +def session(*, only_if=lambda: True, default: bool = False, reuse_venv: bool = False, **kwargs): def decorator(func: Callable[[Session], None]): func.__name__ = func.__name__.replace("_", "-") if default: - options.sessions.append(func.__name__) - return _session(reuse_venv=reuse_venv, **kwargs)(func) + _options.sessions.append(func.__name__) + return _session(reuse_venv=reuse_venv, **kwargs)(func) if only_if() else func return decorator diff --git a/ci/pages.gitlab-ci.yml b/ci/pages.gitlab-ci.yml index 853c12a995..715b9dd641 100644 --- a/ci/pages.gitlab-ci.yml +++ b/ci/pages.gitlab-ci.yml @@ -33,12 +33,10 @@ set -x apt-get update apt-get install -qy graphviz - pip install requests - source tasks/deploy.sh - bash tasks/retry_aborts.sh pip install nox + pip install nox git clone ${CI_REPOSITORY_URL} -b ${TARGET_BRANCH} hikari_docs_${TARGET_BRANCH} --depth=1 cd "hikari_docs_${TARGET_BRANCH}" - bash tasks/retry_aborts.sh nox -s documentation + nox -s sphinx rm ../public -rf && mkdir ../public mv public "../public/${TARGET_BRANCH}" @@ -90,8 +88,8 @@ set -x apt-get update apt-get install -qy graphviz - pip install requests nox - bash tasks/retry_aborts.sh nox -s documentation + pip install nox + nox -s sphinx except: - master - staging diff --git a/ci/pip.nox.py b/ci/pip.nox.py new file mode 100644 index 0000000000..738f0e5926 --- /dev/null +++ b/ci/pip.nox.py @@ -0,0 +1,141 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# Copyright © Nekoka.tt 2019-2020 +# +# This file is part of Hikari. +# +# Hikari is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Hikari is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Hikari. If not, see . +"""Installation tests.""" +import contextlib +import os +import shutil +import subprocess +import tarfile +import tempfile + +from ci import config +from ci import nox + + +@contextlib.contextmanager +def temp_chdir(session: nox.Session, target: str): + cwd = os.path.abspath(os.getcwd()) + print("Changing directory from", cwd, "to", target) + session.chdir(target) + yield cwd + print("Changing directory from", target, "to", cwd) + session.chdir(cwd) + + +@nox.session(reuse_venv=False, only_if=lambda: "CI" in os.environ) +def pip(session: nox.Session): + """Run through sandboxed install of PyPI package.""" + if "--showtime" in session.posargs: + print("Testing we can install packaged pypi object") + session.install(config.MAIN_PACKAGE) + session.run("python", "-m", config.MAIN_PACKAGE) + # Prevent nox caching old versions and using those when tests run. + session.run("pip", "uninstall", "-vvv", "-y", config.MAIN_PACKAGE) + + else: + try: + print("Testing published ref can be installed as a package.") + url = session.env.get("CI_PROJECT_URL") + sha1 = session.env.get("CI_COMMIT_SHA", "master") + slug = f"git+{url}.git@{sha1}" + session.install(slug) + session.run("python", "-m", config.MAIN_PACKAGE) + # Prevent nox caching old versions and using those when tests run. + session.run("pip", "uninstall", "-vvv", "-y", config.MAIN_PACKAGE) + except Exception: + print("Failed to install from GitLab.") + raise KeyError from None + + +@nox.session(reuse_venv=False) +def pip_git(session: nox.Session): + """Test installing repository from Git repository directly via pip.""" + print("Testing installing from git repository only") + + try: + branch = os.environ["CI_COMMIT_SHA"] + except KeyError: + branch = subprocess.check_output(["git", "symbolic-ref", "--short", "HEAD"]).decode("utf8")[0:-1] + + print("Testing for branch", branch) + + with tempfile.TemporaryDirectory() as temp_dir: + with temp_chdir(session, temp_dir) as project_dir: + session.install(f"git+file://{project_dir}") + session.install(config.MAIN_PACKAGE) + session.run("python", "-m", config.MAIN_PACKAGE) + + print("Installed as git dir in temporary environment successfully!") + + +@nox.session(reuse_venv=False) +def pip_sdist(session: nox.Session): + """Test installing as an sdist package.""" + session.install("wheel") + session.run("python", "setup.py", "build", "sdist") + + print("Testing installing from wheel") + with tempfile.TemporaryDirectory() as temp_dir: + with temp_chdir(session, temp_dir) as project_dir: + dist = os.path.join(project_dir, "dist") + wheels = [os.path.join(dist, wheel) for wheel in os.listdir(dist) if wheel.endswith(".tar.gz")] + wheels.sort(key=lambda wheel: os.stat(wheel).st_ctime) + newest_tarball = wheels.pop() + newest_tarball_name = os.path.basename(newest_tarball) + + if newest_tarball_name.lower().endswith(".tar.gz"): + newest_tarball_dir = newest_tarball_name[: -len(".tar.gz")] + else: + newest_tarball_dir = newest_tarball_name[: -len(".tgz")] + + print(f"copying newest tarball found at {newest_tarball} and installing it in temp dir") + shutil.copyfile(newest_tarball, newest_tarball_name) + + print("extracting tarball") + with tarfile.open(newest_tarball_name) as tar: + tar.extractall() + + print("installing sdist") + with temp_chdir(session, newest_tarball_dir): + session.run("python", "setup.py", "install") + session.run("python", "-m", config.MAIN_PACKAGE) + + print("Installed as wheel in temporary environment successfully!") + + +@nox.session(reuse_venv=False) +def pip_bdist_wheel(session: nox.Session): + """Test installing as a platform independent bdist_wheel package.""" + session.install("wheel") + session.run("python", "setup.py", "build", "bdist_wheel") + + print("Testing installing from wheel") + with tempfile.TemporaryDirectory() as temp_dir: + with temp_chdir(session, temp_dir) as project_dir: + dist = os.path.join(project_dir, "dist") + wheels = [os.path.join(dist, wheel) for wheel in os.listdir(dist) if wheel.endswith(".whl")] + wheels.sort(key=lambda wheel: os.stat(wheel).st_ctime) + newest_wheel = wheels.pop() + newest_wheel_name = os.path.basename(newest_wheel) + print(f"copying newest wheel found at {newest_wheel} and installing it in temp dir") + shutil.copyfile(newest_wheel, newest_wheel_name) + session.run("pip", "install", newest_wheel_name) + session.run("python", "-m", config.MAIN_PACKAGE) + + print("Installed as wheel in temporary environment successfully!") diff --git a/tasks/black.py b/ci/pydocstyle.nox.py similarity index 61% rename from tasks/black.py rename to ci/pydocstyle.nox.py index e9e1e1ab99..d34b3d99b3 100644 --- a/tasks/black.py +++ b/ci/pydocstyle.nox.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 # -*- coding: utf-8 -*- # Copyright © Nekoka.tt 2019-2020 # @@ -16,20 +16,16 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . -""" -Black uses a multiprocessing Lock, which is fine until the -platform doesn't support sem_open syscalls, then all hell -breaks loose. This should allow it to fail silently :-) -""" -import multiprocessing import os -import sys -try: - multiprocessing.Lock() -except ImportError as ex: - print("Will not run black because", str(ex).lower()) - print("Exiting with success code anyway") - exit(0) -else: - os.system(f'black {" ".join(sys.argv[1:])}') +from ci import config +from ci import nox + + +@nox.session(default=True, reuse_venv=True) +def pydocstyle(session: nox.Session) -> None: + """Check documentation is formatted correctly.""" + session.install("pydocstyle") + session.chdir(config.MAIN_PACKAGE) + ini = os.path.join(os.path.pardir, config.PYDOCSTYLE_INI) + session.run("pydocstyle", "--config", ini) diff --git a/ci/pylint.nox.py b/ci/pylint.nox.py new file mode 100644 index 0000000000..bc348aff93 --- /dev/null +++ b/ci/pylint.nox.py @@ -0,0 +1,64 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# Copyright © Nekoka.tt 2019-2020 +# +# This file is part of Hikari. +# +# Hikari is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Hikari is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Hikari. If not, see . +"""Pylint support.""" +from concurrent import futures + +from ci import config +from ci import nox + + +FLAGS = [ + "pylint", config.MAIN_PACKAGE, + "--rcfile", config.PYLINT_INI +] + +SUCCESS_CODES = list(range(0, 256)) + + +@nox.session(default=True, reuse_venv=True) +def pylint(session: nox.Session) -> None: + """Run pylint against the code base and report any code smells or issues.""" + session.install( + "-r", config.REQUIREMENTS, + "-r", config.DEV_REQUIREMENTS, + "pylint", + "pylint-junit==0.2.0", + ) + + # Mapping concurrently halves the execution time (unless you have less than + # two CPU cores, but who cares). + with futures.ThreadPoolExecutor() as pool: + pool.map(lambda f: f(session), [pylint_text, pylint_junit]) + + +def pylint_text(session: nox.Session) -> None: + session.run( + *FLAGS, + success_codes=SUCCESS_CODES + ) + + +def pylint_junit(session: nox.Session) -> None: + with open(config.PYLINT_JUNIT_OUTPUT_PATH, "w") as fp: + session.run( + *FLAGS, + "--output-format", "pylint_junit/JUnitReporter", + stdout=fp, + success_codes=SUCCESS_CODES + ) diff --git a/ci/pytest.nox.py b/ci/pytest.nox.py new file mode 100644 index 0000000000..1254a902b3 --- /dev/null +++ b/ci/pytest.nox.py @@ -0,0 +1,65 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# Copyright © Nekoka.tt 2019-2020 +# +# This file is part of Hikari. +# +# Hikari is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Hikari is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Hikari. If not, see . +"""Py.test integration.""" +import os +import shutil + +from ci import config +from ci import nox + +FLAGS = [ + "-c", config.PYTEST_INI, + "-r", "a", + "--pastebin=all" if config.IS_CI else "", + "--full-trace", + "-n", "auto", + "--cov", config.MAIN_PACKAGE, + "--cov-config", config.COVERAGE_INI, + "--cov-report", "term", + "--cov-report", f"html:{config.COVERAGE_HTML_PATH}", + "--cov-branch", + "--junitxml", config.COVERAGE_JUNIT_PATH, + "--showlocals", + config.TEST_PACKAGE +] + + +@nox.session(default=True, reuse_venv=True) +def pytest(session: nox.Session) -> None: + """Run unit tests and measure code coverage.""" + session.install( + "-r", config.REQUIREMENTS, + "-r", config.DEV_REQUIREMENTS, + ) + shutil.rmtree(".coverage", ignore_errors=True) + session.run("python", "-m", "pytest", *FLAGS) + + +@nox.session(reuse_venv=True) +def coalesce_coverage(session: nox.Session) -> None: + """Combine coverage stats from several CI jobs.""" + session.install("coverage") + + coverage_files = [] + for file in os.listdir(config.ARTIFACT_DIRECTORY): + if file.endswith(".coverage"): + coverage_files.append(os.path.join(config.ARTIFACT_DIRECTORY, file)) + + session.run("coverage", "combine", *coverage_files) + session.run("coverage", "report", "-i") diff --git a/ci/releases.gitlab-ci.yml b/ci/releases.gitlab-ci.yml index 344650635a..b9c365cc5e 100644 --- a/ci/releases.gitlab-ci.yml +++ b/ci/releases.gitlab-ci.yml @@ -28,11 +28,9 @@ | grep -iqE "\[\s*(skip|no|don'?t|do\s+not)\s+deploy(ments?)?\s*\]" \ && echo "SKIPPING ${CI_JOB_STAGE} STAGE JOB" \ && exit 0 - source tasks/config.sh apt-get update apt-get install curl openssh-client -qy - pip install twine wheel - pip install -e . + pip install twine wheel nox extends: .cpython-stable interruptible: false resource_group: deploy @@ -60,11 +58,7 @@ - |+ set -e set -x - source tasks/deploy.sh - do-deployment - variables: - RELEASE_WEBHOOK_NAME: "New development snapshot of the next release has been published" - RELEASE_WEBHOOK_COLOUR: "0x2C2F33" + nox -s deploy ### ### Make a production release. @@ -87,8 +81,4 @@ - |+ set -e set -x - source tasks/deploy.sh - do-deployment - variables: - RELEASE_WEBHOOK_NAME: "NEW VERSION OF HIKARI HAS BEEN RELEASED!" - RELEASE_WEBHOOK_COLOUR: "0x7289DA" + nox -s deploy diff --git a/ci/safety.nox.py b/ci/safety.nox.py new file mode 100644 index 0000000000..c8fe26ac4d --- /dev/null +++ b/ci/safety.nox.py @@ -0,0 +1,30 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# Copyright © Nekoka.tt 2019-2020 +# +# This file is part of Hikari. +# +# Hikari is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Hikari is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Hikari. If not, see . +"""Dependency scanning.""" + +from ci import config +from ci import nox + + +# Do not reuse venv, download new definitions each run. +@nox.session(reuse_venv=False, default=True) +def safety(session: nox.Session) -> None: + """Perform dependency scanning.""" + session.install("safety", "-r", config.REQUIREMENTS) + session.run("safety", "check") diff --git a/ci/sphinx.nox.py b/ci/sphinx.nox.py new file mode 100644 index 0000000000..44425f2440 --- /dev/null +++ b/ci/sphinx.nox.py @@ -0,0 +1,57 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# Copyright © Nekoka.tt 2019-2020 +# +# This file is part of Hikari. +# +# Hikari is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Hikari is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Hikari. If not, see . +"""Sphinx documentation generation.""" +import os +import re + +from ci import config +from ci import nox + + +@nox.session(reuse_venv=False, default=True) +def sphinx(session: nox.Session) -> None: + """Generate Sphinx documentation.""" + session.install( + "-r", config.REQUIREMENTS, + "sphinx==3.0.1", + "https://github.com/ryan-roemer/sphinx-bootstrap-theme/zipball/v0.8.0" + ) + + session.env["SPHINXOPTS"] = "-WTvvn" + + print("Generating stubs") + session.run("sphinx-apidoc", "-e", "-o", config.DOCUMENTATION_DIRECTORY, config.MAIN_PACKAGE) + + print("Producing HTML documentation from stubs") + session.run( + "python", "-m", "sphinx.cmd.build", + "-a", "-b", "html", "-j", "auto", "-n", + config.DOCUMENTATION_DIRECTORY, config.ARTIFACT_DIRECTORY + ) + + if "--no-rm" in session.posargs: + print("Not removing stub files by request") + else: + print("Destroying stub files (skip by passing `-- --no-rm` flag)") + blacklist = (f"{config.MAIN_PACKAGE}.rst", "modules.rst") + for f in os.listdir(config.DOCUMENTATION_DIRECTORY): + if f in blacklist or re.match(f"{config.MAIN_PACKAGE}\\.(\\w|\\.)+\\.rst", f): + path = os.path.join(config.DOCUMENTATION_DIRECTORY, f) + print("rm", path) + os.unlink(path) diff --git a/ci/tests.gitlab-ci.yml b/ci/tests.gitlab-ci.yml index f948845136..0a0ffe940d 100644 --- a/ci/tests.gitlab-ci.yml +++ b/ci/tests.gitlab-ci.yml @@ -34,7 +34,7 @@ set -x apt-get install -qy git gcc g++ make pip install nox - nox -stest + nox -s pytest mv .coverage "public/${CI_JOB_NAME}.coverage" stage: test @@ -77,18 +77,3 @@ extends: - .cpython390 - .nox-test - -### -### Run the tests for the version updating scripts. -### -"test versioning": - extends: - - .cpython-stable - - .reactive-job - stage: test - script: - - |+ - set -e - set -x - pip install requests - bash tasks/test_make_version_string.sh diff --git a/doc-requirements.txt b/doc-requirements.txt deleted file mode 100644 index 6745559103..0000000000 --- a/doc-requirements.txt +++ /dev/null @@ -1,4 +0,0 @@ -Jinja2==2.11.1 -sphinx==3.0.1 -# sphinx-bootstrap-theme==0.7.1 -https://github.com/ryan-roemer/sphinx-bootstrap-theme/zipball/v0.8.0 diff --git a/noxfile.old.py b/noxfile.old.py deleted file mode 100644 index e02e3f9d93..0000000000 --- a/noxfile.old.py +++ /dev/null @@ -1,313 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# Copyright © Nekoka.tt 2019-2020 -# -# This file is part of Hikari. -# -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . -import contextlib -import os -import re -import shutil -import subprocess -import tarfile -import tempfile - -import nox.sessions - -nox.options.sessions = [] - - -def default_session(func): - nox.options.sessions.append(func.__name__) - return func - - -def pathify(arg, *args, root=False): - return os.path.abspath(os.path.join(os.path.dirname(os.path.abspath(__file__)) if not root else "/", arg, *args)) - - -# Configuration stuff we probably might move around eventually. -MAIN_PACKAGE = "hikari" -OWNER = "nekokatt" -TECHNICAL_DIR = "technical" -TEST_PATH = "tests/hikari" -ARTIFACT_DIR = "public" -DOCUMENTATION_DIR = "docs" -CI_SCRIPT_DIR = "tasks" -BLACK_PACKAGES = [MAIN_PACKAGE, TEST_PATH] -BLACK_PATHS = [m.replace(".", "/") for m in BLACK_PACKAGES] + [ - __file__, - pathify(DOCUMENTATION_DIR, "conf.py"), - "noxfile.old.py", -] -BLACK_SHIM_PATH = pathify(CI_SCRIPT_DIR, "black.py") -MAIN_PACKAGE_PATH = MAIN_PACKAGE.replace(".", "/") -REPOSITORY = f"https://gitlab.com/{OWNER}/{MAIN_PACKAGE}" - - -@default_session -@nox.session(reuse_venv=True) -def format(session) -> None: - """Reformat code with Black. Pass the '--check' flag to check formatting only.""" - session.install("black") - session.run("python", BLACK_SHIM_PATH, *BLACK_PATHS, *session.posargs) - - -@default_session -@nox.session(reuse_venv=True) -def test(session) -> None: - """Run unit tests in Pytest.""" - session.install("-r", "requirements.txt") - session.install("-r", "dev-requirements.txt") - - additional_opts = ["--pastebin=all"] if os.getenv("CI") else [] - - # Apparently coverage doesn't replace this, leading to "no coverage was - # detected" which is helpful. - with contextlib.suppress(Exception): - shutil.move(".coverage", ".coverage.old") - - session.run( - "python", - "-m", - "pytest", - "-c", - "pytest.ini", - "-r", - "a", - *additional_opts, - "--full-trace", - "-n", - "auto", - "--cov", - MAIN_PACKAGE, - "--cov-config=coverage.ini", - "--cov-report", - "term", - "--cov-report", - f"html:{ARTIFACT_DIR}/coverage/html", - "--cov-branch", - f"--junitxml={ARTIFACT_DIR}/tests.xml", - "--showlocals", - *session.posargs, - TEST_PATH, - ) - - -@default_session -@nox.session(reuse_venv=True) -def docstyle(session) -> None: - """Check docstrings with pydocstyle.""" - session.install("pydocstyle") - session.chdir(MAIN_PACKAGE_PATH) - # add -e flag for explainations. - session.run("pydocstyle", "--config=../pydocstyle.ini") - - -@default_session -@nox.session(reuse_venv=True) -def lint(session) -> None: - """Check formating with pylint""" - session.install("-r", "requirements.txt") - session.install("-r", "dev-requirements.txt") - session.install("-r", "doc-requirements.txt") - session.install("pylint-junit==0.2.0") - session.install("pylint") - pkg = MAIN_PACKAGE.split(".")[0] - - try: - session.run( - "pylint", - pkg, - "--rcfile=pylint.ini", - "--spelling-private-dict-file=dict.txt", - success_codes=list(range(0, 256)), - ) - finally: - os.makedirs(ARTIFACT_DIR, exist_ok=True) - - with open(os.path.join(ARTIFACT_DIR, "pylint.xml"), "w") as fp: - session.run( - "pylint", - pkg, - "--rcfile=pylint.ini", - "--output-format=pylint_junit.JUnitReporter", - stdout=fp, - success_codes=list(range(0, 256)), - ) - - -@default_session -@nox.session(reuse_venv=True) -def sast(session) -> None: - """Run static application security testing with Bandit.""" - session.install("bandit") - pkg = MAIN_PACKAGE.split(".")[0] - session.run("bandit", pkg, "-r") - - -@default_session -@nox.session(reuse_venv=True) -def safety(session) -> None: - """Run safety checks against a vulnerability database using Safety.""" - session.install("-r", "requirements.txt") - session.install("safety") - session.run("safety", "check") - - -@default_session -@nox.session(reuse_venv=True) -def documentation(session) -> None: - """Generate documentation using Sphinx for the current branch.""" - session.install("-r", "requirements.txt") - session.install("-r", "dev-requirements.txt") - session.install("-r", "doc-requirements.txt") - - session.env["SPHINXOPTS"] = "-WTvvn" - session.run("sphinx-apidoc", "-e", "-o", DOCUMENTATION_DIR, MAIN_PACKAGE) - session.run( - "python", "-m", "sphinx.cmd.build", "-a", "-b", "html", "-j", "auto", "-n", DOCUMENTATION_DIR, ARTIFACT_DIR - ) - for f in os.listdir(DOCUMENTATION_DIR): - if f in ("hikari.rst", "modules.rst") or re.match(r"hikari\.(\w|\.)+\.rst", f): - os.unlink(pathify(DOCUMENTATION_DIR, f)) - - -if os.getenv("CI"): - - @nox.session(reuse_venv=False) - def pip(session: nox.sessions.Session): - """Run through sandboxed install of PyPI package (if running on CI)""" - if "--showtime" not in session.posargs: - pip_showtime(session) - else: - try: - pip_from_ref(session) - except Exception: - print("Failed to install from GitLab.") - raise KeyError from None - - -@nox.session(reuse_venv=False) -def pip_bdist_wheel(session: nox.sessions.Session): - """ - Test installing a bdist_wheel package. - """ - session.install("wheel") - session.run("python", "setup.py", "build", "bdist_wheel") - - print("Testing installing from wheel") - with tempfile.TemporaryDirectory() as temp_dir: - with temp_chdir(session, temp_dir) as project_dir: - dist = os.path.join(project_dir, "dist") - wheels = [os.path.join(dist, wheel) for wheel in os.listdir(dist) if wheel.endswith(".whl")] - wheels.sort(key=lambda wheel: os.stat(wheel).st_ctime) - newest_wheel = wheels.pop() - newest_wheel_name = os.path.basename(newest_wheel) - print(f"copying newest wheel found at {newest_wheel} and installing it in temp dir") - shutil.copyfile(newest_wheel, newest_wheel_name) - session.run("pip", "install", newest_wheel_name) - session.run("python", "-m", MAIN_PACKAGE) - - print("Installed as wheel in temporary environment successfully!") - - -@nox.session(reuse_venv=False) -def pip_sdist(session: nox.sessions.Session): - """ - Test installing an sdist package. - """ - session.install("wheel") - session.run("python", "setup.py", "build", "sdist") - - print("Testing installing from wheel") - with tempfile.TemporaryDirectory() as temp_dir: - with temp_chdir(session, temp_dir) as project_dir: - dist = os.path.join(project_dir, "dist") - wheels = [os.path.join(dist, wheel) for wheel in os.listdir(dist) if wheel.endswith(".tar.gz")] - wheels.sort(key=lambda wheel: os.stat(wheel).st_ctime) - newest_tarball = wheels.pop() - newest_tarball_name = os.path.basename(newest_tarball) - - if newest_tarball_name.lower().endswith(".tar.gz"): - newest_tarball_dir = newest_tarball_name[: -len(".tar.gz")] - else: - newest_tarball_dir = newest_tarball_name[: -len(".tgz")] - - print(f"copying newest tarball found at {newest_tarball} and installing it in temp dir") - shutil.copyfile(newest_tarball, newest_tarball_name) - - print("extracting tarball") - with tarfile.open(newest_tarball_name) as tar: - tar.extractall() - - print("installing sdist") - with temp_chdir(session, newest_tarball_dir): - session.run("python", "setup.py", "install") - session.run("python", "-m", MAIN_PACKAGE) - - print("Installed as wheel in temporary environment successfully!") - - -@nox.session(reuse_venv=False) -def pip_git(session: nox.sessions.Session): - """ - Test installing repository from Git. - """ - print("Testing installing from git repository only") - - try: - branch = os.environ["CI_COMMIT_SHA"] - except KeyError: - branch = subprocess.check_output(["git", "symbolic-ref", "--short", "HEAD"]).decode("utf8")[0:-1] - - print("Testing for branch", branch) - - with tempfile.TemporaryDirectory() as temp_dir: - with temp_chdir(session, temp_dir) as project_dir: - session.install(f"git+file://{project_dir}") - session.install(MAIN_PACKAGE) - session.run("python", "-m", MAIN_PACKAGE) - - print("Installed as git dir in temporary environment successfully!") - - -def pip_showtime(session): - print("Testing we can install packaged pypi object") - session.install(MAIN_PACKAGE) - session.run("python", "-c", f"import {MAIN_PACKAGE}; print({MAIN_PACKAGE}.__version__)") - # Prevent nox caching old versions and using those when tests run. - session.run("pip", "uninstall", "-vvv", "-y", MAIN_PACKAGE) - - -def pip_from_ref(session): - print("Testing published ref can be installed as a package.") - url = session.env.get("CI_PROJECT_URL", REPOSITORY) - sha1 = session.env.get("CI_COMMIT_SHA", "master") - slug = f"git+{url}.git@{sha1}" - session.install(slug) - session.run("python", "-c", f"import {MAIN_PACKAGE}; print({MAIN_PACKAGE}.__version__)") - # Prevent nox caching old versions and using those when tests run. - session.run("pip", "uninstall", "-vvv", "-y", MAIN_PACKAGE) - - -@contextlib.contextmanager -def temp_chdir(session, target): - cwd = os.path.abspath(os.getcwd()) - print("Changing directory from", cwd, "to", target) - session.chdir(target) - yield cwd - print("Changing directory from", target, "to", cwd) - session.chdir(cwd) diff --git a/tasks/config.sh b/tasks/config.sh deleted file mode 100644 index b014b90063..0000000000 --- a/tasks/config.sh +++ /dev/null @@ -1,70 +0,0 @@ -#!/usr/bin/env bash - -echo "===============CONFIGURATION===============" - -function do_export() { - echo "exported $*" - export "${*?}" -} - -do_export CURRENT_VERSION_FILE="hikari/_about.py" -do_export CURRENT_VERSION_PATTERN="^__version__\s*=\s*\"\K[^\"]*" - -do_export API_NAME="hikari" -do_export GIT_SVC_HOST="gitlab.com" -do_export REPO_AUTHOR="Nekokatt" -do_export ORIGINAL_REPO_URL="https://${GIT_SVC_HOST}/${REPO_AUTHOR}/${API_NAME}" -do_export REPOSITORY_URL="$(echo "$CI_REPOSITORY_URL" | perl -pe 's#.*@(.+?(\:\d+)?)/#git@\1:#')" - -do_export SSH_PRIVATE_KEY_PATH="~/.ssh/id_rsa" -do_export GIT_TEST_SSH_PATH="git@${GIT_SVC_HOST}" - -do_export CI_ROBOT_NAME="${REPO_AUTHOR}" -do_export CI_ROBOT_EMAIL="3903853-nekokatt@users.noreply.gitlab.com" - -do_export SKIP_CI_COMMIT_PHRASE='[skip ci]' -do_export SKIP_DEPLOY_COMMIT_PHRASE='[skip deploy]' -do_export SKIP_PAGES_COMMIT_PHRASE='[skip pages]' - -do_export PROD_BRANCH="master" -do_export PREPROD_BRANCH="staging" -do_export REMOTE_NAME="origin" - -do_export COMMIT_REF="${CI_COMMIT_REF_NAME}" - -cat > /dev/null << EOF - SECURE VARIABLES TO DEFINE IN CI - ================================ - - PyPI credentials: - PYPI_USER (should always be __token__ if using token auth) - PYPI_PASS - - SSH: - GIT_SSH_PRIVATE_KEY - - Webhooks: - RELEASE_WEBHOOK (url of webhook to fire requests at to make a deployment message on Discord) - RELEASE_WEBHOOK_NAME (title of embed) - RELEASE_WEBHOOK_DESCRIPTION (description of embed) - RELEASE_WEBHOOK_COLOUR (integer colour code) - - GitLab API: - GITLAB_API_TOKEN (user API token used to trigger certain API endpoints such as to trigger housekeeping) - CI_PROJECT_ID (the project ID on GitLab, this is predefined by the CI environment) - - VARIABLES TO DEFINE IN CI PER ENVIRONMENT - ========================================= - - Any: - ENVIRONMENT - the name of the environment being used (GitLab sets this for us) - - Production environment: - PYPI_REPO - - Preproduction environment: - PYPI_REPO - -EOF - -echo "=============END CONFIGURATION=============" diff --git a/tasks/deploy.sh b/tasks/deploy.sh deleted file mode 100644 index e1770f05eb..0000000000 --- a/tasks/deploy.sh +++ /dev/null @@ -1,88 +0,0 @@ -#!/usr/bin/env bash - -# Load configuration. -# source "$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null 2>&1 && pwd)"/config.sh - -function deploy-to-pypi() { - python setup.py sdist bdist_wheel - set +x - twine upload --username="${PYPI_USER}" --password="${PYPI_PASS}" --repository-url=https://upload.pypi.org/legacy/ dist/* - set -x -} - -function notify() { - local version=${1} - python tasks/notify.py "${version}" "${API_NAME}" "${PYPI_REPO}" -} - -function deploy-to-svc() { - local repo - local old_version=${1} - local current_version=${2} - - # Init SSH auth. - set +x - eval "$(ssh-agent -s)" - mkdir ~/.ssh || true - echo "${GIT_SSH_PRIVATE_KEY}" > ~/.ssh/id_rsa - set -x - chmod 600 ~/.ssh/id_rsa - ssh-keyscan -t rsa ${GIT_SVC_HOST} >> ~/.ssh/known_hosts - ssh-add ~/.ssh/id_rsa - git remote set-url ${REMOTE_NAME} "${REPOSITORY_URL}" - # Verify the key works. - ssh "${GIT_TEST_SSH_PATH}" - git commit -am "(ci) Deployed ${current_version} to PyPI [skip deploy]" --allow-empty - git push ${REMOTE_NAME} ${PROD_BRANCH} - git tag "${current_version}" && git push ${REMOTE_NAME} "${current_version}" - # git -c color.status=always log --all --decorate --oneline --graph -n 50 - git fetch --all - git reset --hard origin/${PROD_BRANCH} - git checkout ${PREPROD_BRANCH} - git reset --hard origin/${PREPROD_BRANCH} - # git -c color.status=always log --all --decorate --oneline --graph -n 50 - # Use [skip deploy] instead of [skip ci] so that our pages rebuild still... - git merge origin/${PROD_BRANCH} --no-ff --strategy-option theirs --allow-unrelated-histories -m "(ci) Merged ${PROD_BRANCH} ${current_version} into ${PREPROD_BRANCH}" - bash tasks/transform_versions.sh $(python tasks/make_version_string.py ${PREPROD_BRANCH}) - git commit -am "(ci) Updated version for next development release [skip deploy]" - git push --atomic ${REMOTE_NAME} ${PREPROD_BRANCH} ${PROD_BRANCH} ${curr} -} - -function do-deployment() { - set -x - - local old_version - local current_version - git config user.name "${CI_ROBOT_NAME}" - git config user.email "${CI_ROBOT_EMAIL}" - - git fetch --all - git checkout -f "${COMMIT_REF}" - - old_version=$(grep -oP "${CURRENT_VERSION_PATTERN}" "${CURRENT_VERSION_FILE}") - current_version=$(python tasks/make_version_string.py "${COMMIT_REF}") - - bash tasks/transform_versions.sh "${current_version}" - pip install -e . - - case "${COMMIT_REF}" in - ${PROD_BRANCH}) - # Ensure we have the staging ref as well as the master one - git checkout "${PREPROD_BRANCH}" -f && git checkout "${PROD_BRANCH}" -f - bash tasks/transform_versions.sh "${current_version}" - # Push to GitLab and update both master and staging. - time deploy-to-pypi - time deploy-to-svc "${old_version}" "${current_version}" - ;; - ${PREPROD_BRANCH}) - time deploy-to-pypi - ;; - *) - echo -e "\e[1;31m${COMMIT_REF} is not ${PROD_BRANCH} or ${PREPROD_BRANCH}, so will not be updated.\e[0m" - exit 1 - ;; - esac - - notify "${current_version}" - set +x -} diff --git a/tasks/gendoc.py b/tasks/gendoc.py deleted file mode 100644 index 6993b86877..0000000000 --- a/tasks/gendoc.py +++ /dev/null @@ -1,99 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# Copyright © Nekoka.tt 2019-2020 -# -# This file is part of Hikari. -# -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . -""" -Generates module documentation for me. Turns out Sphinx is a pain for doing this in a "simple" way. All the stuff on -PyPi for doing this has annoying quirks that I don't care for, like not supporting asyncio, or only working on a Tuesday -or during a full moon, and it is just angering. - -Arg1 = package to document -Arg2 = templates dir for gendoc -Arg3 = path to write index.rst to -Arg4 = path to write module rst files to -""" -import os -import sys - -import jinja2 - - -def is_valid_python_file(path): - base = os.path.basename(path) - return not base.startswith("_") and (os.path.isdir(path) or base.endswith(".py")) - - -def to_module_name(base): - bits = base.split("/") - bits[-1], _ = os.path.splitext(bits[-1]) - return ".".join(bits) - - -def iter_all_module_paths_in(base): - stack = [os.path.join(base)] - while stack: - next_path = stack.pop() - - yield next_path - - if os.path.isdir(next_path): - children = os.listdir(next_path) - - for f in children: - next_file = os.path.join(next_path, f) - if is_valid_python_file(next_file): - if os.path.isdir(next_file): - stack.append(next_file) - elif os.path.isfile(next_file): - yield next_file - - -def main(*argv): - base = argv[0].replace('.', '/') - template_dir = argv[1] - index_file = argv[2] - documentation_path = argv[3] - - modules = [to_module_name(module_path) for module_path in sorted(iter_all_module_paths_in(base))] - print(f"Found {len(modules)} modules to document:", *modules, sep="\n - ") - - with open(os.path.join(template_dir, "index.rst")) as fp: - print("Reading", fp.name) - index_template = jinja2.Template(fp.read()) - - with open(index_file, "w") as fp: - print("Writing", fp.name) - fp.write(index_template.render(modules=modules, documentation_path=documentation_path)) - - with open(os.path.join(template_dir, "module.rst")) as fp: - print("Reading", fp.name) - module_template = jinja2.Template(fp.read()) - - os.makedirs(documentation_path, 0o1777, exist_ok=True) - - for m in modules: - with open(os.path.join(documentation_path, m + ".rst"), "w") as fp: - submodules = [sm for sm in modules if sm.startswith(m) and sm != m] - - print("Writing", fp.name) - - fp.write(module_template.render(module=m, rule=len(m) * "#", submodules=submodules)) - - -if __name__ == "__main__": - os.chdir(sys.argv[1]) - main(*sys.argv[2:]) diff --git a/tasks/make_version_string.py b/tasks/make_version_string.py deleted file mode 100644 index 2ee050841e..0000000000 --- a/tasks/make_version_string.py +++ /dev/null @@ -1,147 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# Copyright © Nekoka.tt 2019-2020 -# -# This file is part of Hikari. -# -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . -""" -Increments the next version -""" -import os -import re -import sys -import textwrap -from distutils.version import LooseVersion - -import requests - -log_message = lambda *a, **k: print(*a, **k, file=sys.stderr) - -is_staging = len(sys.argv) > 1 and "staging" in sys.argv[1:] -is_pages = len(sys.argv) > 1 and "pages" in sys.argv[1:] - -log_message("will use", "staging" if is_staging else "prod", "configuration for this next version") -if is_pages: - log_message("will not bump versions up, this is just for gitlab pages") - -pypi_server = "pypi.org" -api_name = os.getenv("API_NAME", "hikari") -pypi_json_url = f"https://{pypi_server}/pypi/{api_name}/json" - -# Inspect the version in pyproject.toml -if os.getenv("TEST_VERSION_STRING_VERSION_LINE"): - fp_content = os.environ["TEST_VERSION_STRING_VERSION_LINE"] -else: - with open(os.path.join(api_name, "_about.py")) as fp: - fp_content = fp.read() - - -aboutpy_v = LooseVersion(re.findall(r"^__version__\s*=\s*\"(.*?)\"", fp_content, re.M)[0]) -if not hasattr(aboutpy_v, "vstring"): - log_message("corrupt _about.py, using default version 0.0.0") - aboutpy_v = LooseVersion("0.0.0") - -log_message("version in _about.py is", aboutpy_v) - -with requests.get(pypi_json_url) as resp: - log_message("looking at existing versions on", pypi_server) - - if resp.status_code == 404: - log_message("package does not seem to yet be deployed, using dummy values.") - releases = [] - dev_releases = [] - staging_releases_before_published_prod = 0 - latest_pypi_prod_v = LooseVersion("0.0.0") - latest_pypi_staging_v = LooseVersion("0.0.1.dev0") - latest_matching_staging_v = LooseVersion("0.0.1.dev0") - else: - resp.raise_for_status() - root = resp.json() - log_message("found existing versions online, so adjusting versions to follow from that where appropriate...") - releases = [LooseVersion(r) for r in root["releases"]] - dev_releases = [LooseVersion(r) for r in root["releases"] if "dev" in r] - same_micro_dev_releases = [r for r in dev_releases if r.version[:3] == aboutpy_v.version[:3]] - latest_pypi_prod_v = LooseVersion(root["info"]["version"]) - staging_releases_before_published_prod = len( - [r for r in releases if r.version[:3] == latest_pypi_prod_v.version[:3]] - ) - latest_pypi_staging_v = max(dev_releases) - latest_matching_staging_v = max(same_micro_dev_releases) if same_micro_dev_releases else aboutpy_v - - log_message("there have been", len(releases), "total PyPI releases") - log_message("...", len(dev_releases), "of these were to staging and", len(releases) - len(dev_releases), - "were to prod") - log_message("... the latest prod release on PyPI is", latest_pypi_prod_v) - log_message( - "... there were", - staging_releases_before_published_prod, - "staging releases before the most recent already-published prod version was released" - ) - log_message("... the latest staging release on PyPI is", latest_pypi_staging_v) - log_message("... the latest same-micro staging release on PyPI as in _about.py is", latest_matching_staging_v) - -# Version that the about.py has ignoring the patches. -aboutpy_prod_v = LooseVersion(".".join(map(str, aboutpy_v.version[:3]))) -log_message("_about.py represents a version that would result in", aboutpy_prod_v, "being released to prod") - - -if is_staging: - if is_pages: - # Just keep the main version bits and the `dev` but not the specific patch, as it is easier to work with. - result_v = aboutpy_prod_v.vstring + ".dev" - else: - - # staging release. - # if we already have a pypi dev release with the same major.minor.micro as the one in _about.py, then check - # if the _about.py has a dev version in it or a release version. If it is a release version, we want to - # increment the micro digit, as this is the CI updating its version. Otherwise, find the biggest number - # and add 1 to the patch. - if aboutpy_prod_v.vstring == aboutpy_v.vstring: - log_message("looks like we are performing a new version number update from x.x.x to (x.x.x+1.dev)!") - last_index = len(aboutpy_prod_v.version) - 1 - bits = [*map(str, aboutpy_prod_v.version[:last_index]), f"{aboutpy_prod_v.version[last_index] + 1}.dev"] - result_v = LooseVersion(".".join(bits)) - else: - try: - next_patch = latest_matching_staging_v.version[4] + 1 - except IndexError: - # someone messed the version string up or something, meh, just assume it is fine. - log_message(latest_matching_staging_v, "doesn't match a patch staging version, so just ignoring it") - next_patch = 1 - log_message("using next patch of", next_patch) - bits = [*map(str, latest_matching_staging_v.version[:3]), f"dev{next_patch}"] - result_v = LooseVersion(".".join(bits)) -else: - if not is_pages: - # ignore what is on PyPI and just use the aboutpy_prod_version, unless it is on the releases list, then - # panic and ask Nekokatt or someone to fix their version number. - if aboutpy_prod_v in releases: - log_message(textwrap.dedent(f""" - HEY!! - - The _about.py contains a version of {aboutpy_v}. This implies the next prod upload should be for - {aboutpy_prod_v}. - - Unfortunately, you have already published this version, so I can't republish it! - - Please rectify this issue manually and try again... - """)) - sys.exit(1) - - # use the resultant prod version - result_v = aboutpy_prod_v - -log_message("resultant version", result_v) -print(result_v) diff --git a/tasks/notify.py b/tasks/notify.py deleted file mode 100644 index b73e221cb6..0000000000 --- a/tasks/notify.py +++ /dev/null @@ -1,53 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# Copyright © Nekoka.tt 2019-2020 -# -# This file is part of Hikari. -# -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . -""" -Notify on Discord via a webhook that a new version has been released to PyPi. -""" -import os -import sys -import traceback - -import requests - -try: - VERSION = sys.argv[1] - NAME = sys.argv[2] - DEPLOYMENT_HOST = "https://pypi.org" - WEBHOOK_URL = os.environ["RELEASE_WEBHOOK"] - MESSAGE = os.environ["RELEASE_WEBHOOK_NAME"] - COLOUR = os.environ["RELEASE_WEBHOOK_COLOUR"] - AUTHOR = os.environ["REPO_AUTHOR"] - - requests.post( - WEBHOOK_URL, - json = { - "embeds": [ - { - "title": f"{NAME} - {MESSAGE}", - "color": int(COLOUR, 16), - "author": {"name": AUTHOR}, - "description": f"[{NAME} v{VERSION}]({DEPLOYMENT_HOST}/project/{NAME}/{VERSION})" - } - ] - }, - ) -except Exception as ex: - traceback.print_exception(type(ex), ex, ex.__traceback__) -except (SystemExit, KeyboardInterrupt): - pass diff --git a/tasks/retry_aborts.sh b/tasks/retry_aborts.sh deleted file mode 100644 index 93426e69c7..0000000000 --- a/tasks/retry_aborts.sh +++ /dev/null @@ -1,30 +0,0 @@ -#!/usr/bin/env bash - -# This script attempts to detect when PyPI resets our connection and aborts the operation by repeating the operation -# up to 10 times if it occurs. It works by reading the stdout and stderr of the passed command and dumping the exit -# code to a temporary file each time. If the output contains 'connection reset by peer', an internal flag gets set -# that will make the script sleep for a couple of seconds and retry again. Otherwise, the exit code that was saved -# is used as the script exit code. -retries=10 -for i in $(seq 1 ${retries}); do - echo -e "\e[1;31mAttempt #$i/$retries of \e[0;33m$*\e[0m" - continue=1 - # shellcheck disable=SC2068 - exec 3< <($@ 2>&1; echo $? > /tmp/exit_code) - while read -ru 3 line; do - echo "${line}" - if echo "${line}" | grep -iq "Connection reset by peer"; then - echo -e "\e[1;31mConnection reset by peer, retrying.\e[0m" >&1 - continue=0 - fi - done - exit_code=$(xargs < /tmp/exit_code) - if [[ ! ${continue} -eq 0 ]]; then - exit "${exit_code}" - else - sleep 2 - fi -done -code=$(xargs < /tmp/exit_code) -rm /tmp/exit_code -exit "${code}" diff --git a/tasks/test_fail.sh b/tasks/test_fail.sh deleted file mode 100644 index 9d05106568..0000000000 --- a/tasks/test_fail.sh +++ /dev/null @@ -1,12 +0,0 @@ -#!/usr/bin/env bash -echo blah -sleep 0.5 -echo blah -sleep 0.5 -if [[ "$((1 + RANDOM % 4))" = "4" ]]; then - echo "success" - exit 2 -else - echo "connection reset by peer" - exit 1 -fi diff --git a/tasks/test_make_version_string.sh b/tasks/test_make_version_string.sh deleted file mode 100644 index 70f4fcce03..0000000000 --- a/tasks/test_make_version_string.sh +++ /dev/null @@ -1,62 +0,0 @@ -#!/bin/bash - -try() { - branch=$1 - pages=$2 - test_version=$3 - TEST_VERSION_STRING_VERSION_LINE="__version__ = \"${test_version}\"" python tasks/make_version_string.py "${branch}" "${pages}" -} - -# shellcheck disable=SC2068 -assert() { - expected_version=$1 - shift 3 - actual_version=$(try $@) - echo -en "\e[0;33m- ($*) results in a version of \e[0;35m${actual_version}\e[0;33m, we expected \e[0;36m${expected_version}\e[0m -- " - if [[ "${actual_version}" = "${expected_version}" ]]; then - echo -e "\e[1;32mPASSED\e[0m" - else - echo -e "\e[1;31mFAILED\e[0m" - exit 1 - fi -} - -tests() { - assert 300.300.300.dev when using staging pages 300.300.300.dev - assert 300.300.300.dev when using staging pages 300.300.300.dev1 - assert 300.300.300.dev when using staging pages 300.300.300.dev9999 - assert 300.300.300.dev when using staging pages 300.300.300 - assert 300.300.300.dev when using staging pages 300.300.300sdasdfgafg - assert 300.300.dev when using staging pages 300.300 - assert 300.dev when using staging pages 300 - assert 0.0.0.dev when using staging pages "" - - assert 300.300.300.dev1 when using staging no_pages 300.300.300.dev - assert 300.300.300.dev2 when using staging no_pages 300.300.300.dev1 - assert 300.300.300.dev10000 when using staging no_pages 300.300.300.dev9999 - assert 300.300.301.dev when using staging no_pages 300.300.300 - assert 300.300.300.dev1 when using staging no_pages 300.300.300sdasdfgafg - assert 300.301.dev when using staging no_pages 300.300 - assert 301.dev when using staging no_pages 300 - assert 0.0.1.dev when using staging no_pages "" - - assert 300.300.300 when using master pages 300.300.300.dev - assert 300.300.300 when using master pages 300.300.300.dev1 - assert 300.300.300 when using master pages 300.300.300.dev9999 - assert 300.300.300 when using master pages 300.300.300 - assert 300.300.300 when using master pages 300.300.300sdasdfgafg - assert 300.300 when using master pages 300.300 - assert 300 when using master pages 300 - assert 0.0.0 when using master pages "" - - assert 300.300.300 when using master no_pages 300.300.300.dev - assert 300.300.300 when using master no_pages 300.300.300.dev1 - assert 300.300.300 when using master no_pages 300.300.300.dev9999 - assert 300.300.300 when using master no_pages 300.300.300 - assert 300.300.300 when using master no_pages 300.300.300sdasdfgafg - assert 300.300 when using master no_pages 300.300 - assert 300 when using master no_pages 300 - assert 0.0.0 when using master no_pages "" -} - -time tests diff --git a/tasks/transform_versions.sh b/tasks/transform_versions.sh deleted file mode 100644 index 9b85131ed2..0000000000 --- a/tasks/transform_versions.sh +++ /dev/null @@ -1,10 +0,0 @@ -#!/usr/bin/env bash -set -e -set -x - -version=$1 -file=hikari/_about.py - -sed "s|^__version__.*|__version__ = \"${version}\"|g" -i ${file} - -git add ${file} diff --git a/tests/hikari/net/test_rest.py b/tests/hikari/net/test_rest.py index 87540780dd..50071ff3b7 100644 --- a/tests/hikari/net/test_rest.py +++ b/tests/hikari/net/test_rest.py @@ -221,9 +221,7 @@ def rest_impl_with__request(self, *args): rest_impl = rest.LowLevelRestfulClient(token="Bot token") rest_impl.logger = mock.MagicMock(debug=mock.MagicMock()) rest_impl.ratelimiter = mock.MagicMock( - ratelimits.HTTPBucketRateLimiterManager, - acquire=mock.MagicMock(), - update_rate_limits=mock.MagicMock(), + ratelimits.HTTPBucketRateLimiterManager, acquire=mock.MagicMock(), update_rate_limits=mock.MagicMock(), ) rest_impl.global_ratelimiter = mock.MagicMock( ratelimits.ManualRateLimiter, acquire=mock.MagicMock(), throttle=mock.MagicMock() From 4e2d580fa0939da93c229bf693127c2dc49fb01c Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Wed, 15 Apr 2020 00:29:48 +0100 Subject: [PATCH 118/922] Fixed page generation issue --- ci/pages.gitlab-ci.yml | 1 + ci/sphinx.nox.py | 2 ++ 2 files changed, 3 insertions(+) diff --git a/ci/pages.gitlab-ci.yml b/ci/pages.gitlab-ci.yml index 715b9dd641..5a15a0bd63 100644 --- a/ci/pages.gitlab-ci.yml +++ b/ci/pages.gitlab-ci.yml @@ -64,6 +64,7 @@ ### "master pages": extends: .generate-pages + allow_failure: true # FIXME: remove once in master. variables: TARGET_BRANCH: master diff --git a/ci/sphinx.nox.py b/ci/sphinx.nox.py index 44425f2440..645827cd60 100644 --- a/ci/sphinx.nox.py +++ b/ci/sphinx.nox.py @@ -55,3 +55,5 @@ def sphinx(session: nox.Session) -> None: path = os.path.join(config.DOCUMENTATION_DIRECTORY, f) print("rm", path) os.unlink(path) + + From 61bbc868463bc796e442a38733cf552051be55d1 Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Wed, 15 Apr 2020 00:32:02 +0100 Subject: [PATCH 119/922] Enabled master pages to build. --- ci/pages.gitlab-ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ci/pages.gitlab-ci.yml b/ci/pages.gitlab-ci.yml index 5a15a0bd63..6d2d827711 100644 --- a/ci/pages.gitlab-ci.yml +++ b/ci/pages.gitlab-ci.yml @@ -90,7 +90,7 @@ apt-get update apt-get install -qy graphviz pip install nox - nox -s sphinx + nox -s sphinx || nox -s pages # FIXME: remove once in master. except: - master - staging From db1a3dfebc9b652936c7744d3b3cded342b70fad Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Wed, 15 Apr 2020 00:43:52 +0100 Subject: [PATCH 120/922] Fixed bug in master docs [skip deploy] --- ci/deploy.nox.py | 2 +- ci/pages.gitlab-ci.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/ci/deploy.nox.py b/ci/deploy.nox.py index bb3e5e9d15..edb0630bc7 100644 --- a/ci/deploy.nox.py +++ b/ci/deploy.nox.py @@ -183,7 +183,7 @@ def send_notification(version: str, title: str, description: str, color: str) -> "description": description, "author": {"name": config.AUTHOR}, "footer": {"text": f"v{version}"}, - "url": f"{config.PYPI}/project/{config.API_NAME}/{version}", + "url": f"{config.PYPI}project/{config.API_NAME}/{version}", "color": int(color, 16) } ] diff --git a/ci/pages.gitlab-ci.yml b/ci/pages.gitlab-ci.yml index 6d2d827711..ed169af4aa 100644 --- a/ci/pages.gitlab-ci.yml +++ b/ci/pages.gitlab-ci.yml @@ -36,7 +36,7 @@ pip install nox git clone ${CI_REPOSITORY_URL} -b ${TARGET_BRANCH} hikari_docs_${TARGET_BRANCH} --depth=1 cd "hikari_docs_${TARGET_BRANCH}" - nox -s sphinx + nox -s sphinx || nox -s pages # FIXME: remove once in master. rm ../public -rf && mkdir ../public mv public "../public/${TARGET_BRANCH}" From 8207af72da9c6c6630dc6117301ff1315bf210c2 Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Wed, 15 Apr 2020 00:53:29 +0100 Subject: [PATCH 121/922] Fixed bug in pages [skip deploy] --- ci/pages.gitlab-ci.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/ci/pages.gitlab-ci.yml b/ci/pages.gitlab-ci.yml index ed169af4aa..682bcbcb9b 100644 --- a/ci/pages.gitlab-ci.yml +++ b/ci/pages.gitlab-ci.yml @@ -44,6 +44,7 @@ ### Base for generating documentation. ### .pages: + allow_failure: true # FIXME: remove once in master. artifacts: paths: - public/ @@ -63,8 +64,8 @@ ### Generate pages for master branch and export them. ### "master pages": - extends: .generate-pages allow_failure: true # FIXME: remove once in master. + extends: .generate-pages variables: TARGET_BRANCH: master From ba88f9931fb885b471f90dca98357a49797d1b63 Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Wed, 15 Apr 2020 00:55:19 +0100 Subject: [PATCH 122/922] Fixed bug in pages [skip deploy] --- ci/pages.gitlab-ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ci/pages.gitlab-ci.yml b/ci/pages.gitlab-ci.yml index 682bcbcb9b..688197af23 100644 --- a/ci/pages.gitlab-ci.yml +++ b/ci/pages.gitlab-ci.yml @@ -36,7 +36,7 @@ pip install nox git clone ${CI_REPOSITORY_URL} -b ${TARGET_BRANCH} hikari_docs_${TARGET_BRANCH} --depth=1 cd "hikari_docs_${TARGET_BRANCH}" - nox -s sphinx || nox -s pages # FIXME: remove once in master. + nox -s sphinx || nox -s documentation # FIXME: remove once in master. rm ../public -rf && mkdir ../public mv public "../public/${TARGET_BRANCH}" @@ -91,7 +91,7 @@ apt-get update apt-get install -qy graphviz pip install nox - nox -s sphinx || nox -s pages # FIXME: remove once in master. + nox -s sphinx || nox -s documentation # FIXME: remove once in master. except: - master - staging From ae8fd09a45d4e0bc913e9e793611325eacf1ca85 Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Wed, 15 Apr 2020 00:58:30 +0100 Subject: [PATCH 123/922] Added missing dependency to new pages job [skip deploy] --- ci/pages.gitlab-ci.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/ci/pages.gitlab-ci.yml b/ci/pages.gitlab-ci.yml index 688197af23..095bcc1f68 100644 --- a/ci/pages.gitlab-ci.yml +++ b/ci/pages.gitlab-ci.yml @@ -32,7 +32,7 @@ script: |+ set -x apt-get update - apt-get install -qy graphviz + apt-get install -qy graphviz git pip install nox git clone ${CI_REPOSITORY_URL} -b ${TARGET_BRANCH} hikari_docs_${TARGET_BRANCH} --depth=1 cd "hikari_docs_${TARGET_BRANCH}" @@ -52,6 +52,7 @@ - |+ set -e set -x + apt-get install git -qy git log -n 1 "${CI_COMMIT_SHA}" --format="%B" \ | grep -iqE "\[\s*(skip|no|don'?t|do\s+not)\s+pages?\s*\]" \ && echo "SKIPPING ${CI_JOB_STAGE} STAGE JOB" \ From 9432ae584b5603c7891c2b6f0adf57bbc15a924e Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Wed, 15 Apr 2020 00:14:25 +0000 Subject: [PATCH 124/922] Update pages to stop needing master to be moved --- ci/pages.gitlab-ci.yml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/ci/pages.gitlab-ci.yml b/ci/pages.gitlab-ci.yml index 095bcc1f68..bd8912a971 100644 --- a/ci/pages.gitlab-ci.yml +++ b/ci/pages.gitlab-ci.yml @@ -38,7 +38,11 @@ cd "hikari_docs_${TARGET_BRANCH}" nox -s sphinx || nox -s documentation # FIXME: remove once in master. rm ../public -rf && mkdir ../public - mv public "../public/${TARGET_BRANCH}" + if [[ "$TARGET_BRANCH" = master ]]; then + mv public ../publi + else + mv public "../public/${TARGET_BRANCH}" + fi ### ### Base for generating documentation. From 4103939247f08950fe952467dc4ba14e67dc73c9 Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Wed, 15 Apr 2020 00:15:37 +0000 Subject: [PATCH 125/922] Update integrations.gitlab-ci.yml to fix pages [skip deploy] --- ci/integrations.gitlab-ci.yml | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/ci/integrations.gitlab-ci.yml b/ci/integrations.gitlab-ci.yml index 6956183614..bb85d44dd3 100644 --- a/ci/integrations.gitlab-ci.yml +++ b/ci/integrations.gitlab-ci.yml @@ -101,15 +101,7 @@ pages: - |+ set -e set -x - git log -n 1 "${CI_COMMIT_SHA}" --format="%B" \ - | grep -iqE "\[\s*(skip|no|don'?t|do\s+not)\s+pages?\s*\]" \ - && echo "SKIPPING ${CI_JOB_STAGE} STAGE JOB" \ - && exit 0 - mkdir public || true - cd public/master - find . -maxdepth 1 -exec mv {} .. \; - cd ../.. - rm public/master -Rvf + ls public -R stage: deploy ### From cb727b5c2a2ffe5aff04b1cbc1622888fe33e94b Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Wed, 15 Apr 2020 00:25:33 +0000 Subject: [PATCH 126/922] Update integrations.gitlab-ci.yml --- ci/integrations.gitlab-ci.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/ci/integrations.gitlab-ci.yml b/ci/integrations.gitlab-ci.yml index bb85d44dd3..b290e380b0 100644 --- a/ci/integrations.gitlab-ci.yml +++ b/ci/integrations.gitlab-ci.yml @@ -90,6 +90,9 @@ license_scanning: ### This must use a specific name. Do not rename it for consistency. ### pages: + artifacts: + paths: + - public/ dependencies: - master pages - staging pages From 04d76b5bf962aab7b833a8184e6280124aaaf3e0 Mon Sep 17 00:00:00 2001 From: Faster Speeding Date: Wed, 15 Apr 2020 02:48:07 +0100 Subject: [PATCH 127/922] Fixes #295: merge `flags` and `public_flags` user fields + fixes and test coverage. - Fix PresenceUser inherited attributes (inc tests). - add test coverage for message nonce. - mark IDENTIFY scope on MyUser as required as this scope is required to retrieve this object. --- hikari/guilds.py | 64 +++++++++++++++++++++++++++++++++++ hikari/users.py | 33 +++++++++--------- tests/hikari/test_events.py | 1 + tests/hikari/test_guilds.py | 64 ++++++++++++++++++++++++++++++++++- tests/hikari/test_messages.py | 2 ++ tests/hikari/test_users.py | 5 +-- 6 files changed, 150 insertions(+), 19 deletions(-) diff --git a/hikari/guilds.py b/hikari/guilds.py index 60ffba915a..2df71f6a96 100644 --- a/hikari/guilds.py +++ b/hikari/guilds.py @@ -639,6 +639,70 @@ class PresenceUser(users.User): raw_name="system", deserializer=bool, if_undefined=entities.Unset, ) + #: The public flags for this user. + #: + #: :type: :obj:`typing.Union` [ :obj:`hikari.users.UserFlag`, :obj:`hikari.entities.UNSET` ] + flags: typing.Union[users.UserFlag, entities.Unset] = marshaller.attrib( + raw_name="public_flags", deserializer=users.UserFlag, if_undefined=entities.Unset + ) + + @property + def avatar_url(self) -> typing.Union[str, entities.Unset]: + """URL for this user's avatar if the relevant info is available. + + Note + ---- + This will be :obj:`hikari.entities.UNSET` if both :attr:`avatar_hash` + and :attr:`discriminator` are :obj:`hikari.entities.UNSET`. + """ + return self.format_avatar_url() + + def format_avatar_url( + self, fmt: typing.Optional[str] = None, size: int = 4096 + ) -> typing.Union[str, entities.Unset]: + """Generate the avatar URL for this user's avatar if available. + + Parameters + ---------- + fmt : :obj:`str` + The format to use for this URL, defaults to ``png`` or ``gif``. + Supports ``png``, ``jpeg``, ``jpg``, ``webp`` and ``gif`` (when + animated). Will be ignored for default avatars which can only be + ``png``. + size : :obj:`int` + The size to set for the URL, defaults to ``4096``. + Can be any power of two between 16 and 4096. + Will be ignored for default avatars. + + Returns + ------- + :obj:`typing.Union` [ :obj:`str`, :obj:`hikari.entities.UNSET` ] + The string URL of the user's custom avatar if + either :attr:`avatar_hash` is set or their default avatar if + :attr:`discriminator` is set, else :obj:`hikari.entities.UNSET`. + + Raises + ------ + :obj:`ValueError` + If ``size`` is not a power of two or not between 16 and 4096. + """ + if self.discriminator is entities.UNSET and self.avatar_hash is entities.UNSET: + return entities.UNSET + return super().format_avatar_url(fmt=fmt, size=size) + + @property + def default_avatar(self) -> typing.Union[int, entities.Unset]: + """Integer representation of this user's default avatar. + + Note + ---- + This will be :obj:`hikari.entities.UNSET` if :attr:`discriminator` is + :obj:`hikari.entities.UNSET`. + """ + if self.discriminator is not entities.UNSET: + return int(self.discriminator) % 5 + return entities.UNSET + @marshaller.marshallable() @attr.s(slots=True) diff --git a/hikari/users.py b/hikari/users.py index 4bbb8c4c8d..fa38a92524 100644 --- a/hikari/users.py +++ b/hikari/users.py @@ -129,7 +129,9 @@ class User(snowflakes.UniqueEntity, entities.Deserializable): #: #: #: :type: :obj:`UserFlag`, optional - public_flags: typing.Optional[UserFlag] = marshaller.attrib(deserializer=UserFlag, if_undefined=None) + flags: typing.Optional[UserFlag] = marshaller.attrib( + raw_name="public_flags", deserializer=UserFlag, if_undefined=None, + ) @property def avatar_url(self) -> str: @@ -181,37 +183,36 @@ class MyUser(User): """Represents a user with extended oauth2 information.""" #: Whether the user's account has 2fa enabled. - #: Requires the ``identify`` scope. #: - #: :type: :obj:`bool`, optional - is_mfa_enabled: typing.Optional[bool] = marshaller.attrib( - raw_name="mfa_enabled", deserializer=bool, if_undefined=None - ) + #: :type: :obj:`bool` + is_mfa_enabled: bool = marshaller.attrib(raw_name="mfa_enabled", deserializer=bool) - #: The user's set language, requires the ``identify`` scope. + #: The user's set language. #: - #: :type: :obj:`str`, optional - locale: typing.Optional[str] = marshaller.attrib(deserializer=str, if_undefined=None) + #: :type: :obj:`str` + locale: str = marshaller.attrib(deserializer=str) #: Whether the email for this user's account has been verified. - #: Requires the ``email`` scope. + #: Will be :obj:`None` if retrieved through the oauth2 flow without the + #: ``email`` scope. #: #: :type: :obj:`bool`, optional is_verified: typing.Optional[bool] = marshaller.attrib(raw_name="verified", deserializer=bool, if_undefined=None) - #: The user's set email, requires the ``email`` scope. - #: This will always be :obj:`None` for bots. + #: The user's set email. + #: Will be :obj:`None` if retrieved through the oauth2 flow without the + #: ``email`` scope and for bot users. #: #: :type: :obj:`str`, optional email: typing.Optional[str] = marshaller.attrib(deserializer=str, if_undefined=None, if_none=None) - #: This user account's flags, requires the ``identify`` scope. + #: This user account's flags. #: - #: :type: :obj:`UserFlag`, optional - flags: typing.Optional[UserFlag] = marshaller.attrib(deserializer=UserFlag, if_undefined=None) + #: :type: :obj:`UserFlag` + flags: UserFlag = marshaller.attrib(deserializer=UserFlag) #: The type of Nitro Subscription this user account had. - #: Requires the ``identify`` scope and will always be :obj:`None` for bots. + #: This will always be :obj:`None` for bots. #: #: :type: :obj:`PremiumType`, optional premium_type: typing.Optional[PremiumType] = marshaller.attrib(deserializer=PremiumType, if_undefined=None) diff --git a/tests/hikari/test_events.py b/tests/hikari/test_events.py index 2c09266715..19f9c84478 100644 --- a/tests/hikari/test_events.py +++ b/tests/hikari/test_events.py @@ -731,6 +731,7 @@ def test_deserialize( assert message_update_payload.application is mock_application assert message_update_payload.message_reference is mock_reference assert message_update_payload.flags == messages.MessageFlag.CROSSPOSTED | messages.MessageFlag.IS_CROSSPOST + assert message_update_payload.nonce == "6454345345345345" def test_partial_message_update(self): message_update_obj = events.MessageUpdateEvent.deserialize({"id": "393939", "channel_id": "434949"}) diff --git a/tests/hikari/test_guilds.py b/tests/hikari/test_guilds.py index 14ee13c534..8113537463 100644 --- a/tests/hikari/test_guilds.py +++ b/tests/hikari/test_guilds.py @@ -104,8 +104,11 @@ def test_user_payload(): "avatar": "1a2b3c4d", "mfa_enabled": True, "locale": "gb", + "system": True, + "bot": True, "flags": 0b00101101, - "premium_type": 0b1101101, + "premium_type": 1, + "public_flags": 0b0001101, } @@ -533,6 +536,9 @@ def test_deserialize_filled_presence_user(self, test_user_payload): assert presence_user_obj.username == "Boris Johnson" assert presence_user_obj.discriminator == "6969" assert presence_user_obj.avatar_hash == "1a2b3c4d" + assert presence_user_obj.is_system is True + assert presence_user_obj.is_bot is True + assert presence_user_obj.flags == users.UserFlag(0b0001101) def test_deserialize_partial_presence_user(self): presence_user_obj = guilds.PresenceUser.deserialize({"id": "115590097100865541"}) @@ -541,6 +547,62 @@ def test_deserialize_partial_presence_user(self): if attr != "id": assert getattr(presence_user_obj, attr) is entities.UNSET + @pytest.fixture() + def test_presence_user_obj(self): + return guilds.PresenceUser( + id=4242424242, + discriminator=entities.UNSET, + username=entities.UNSET, + avatar_hash=entities.UNSET, + is_bot=entities.UNSET, + is_system=entities.UNSET, + flags=entities.UNSET, + ) + + def test_avatar_url(self, test_presence_user_obj): + mock_url = mock.MagicMock(str) + test_presence_user_obj.discriminator = 2222 + with mock.patch.object(users.User, "format_avatar_url", return_value=mock_url): + assert test_presence_user_obj.avatar_url is mock_url + users.User.format_avatar_url.assert_called_once() + + @pytest.mark.parametrize(["avatar_hash", "discriminator"], [("dwaea22", entities.UNSET), (entities.UNSET, "2929")]) + def test_format_avatar_url_when_discriminator_or_avatar_hash_set_without_optionals( + self, test_presence_user_obj, avatar_hash, discriminator + ): + test_presence_user_obj.avatar_hash = avatar_hash + test_presence_user_obj.discriminator = discriminator + mock_url = mock.MagicMock(str) + with mock.patch.object(users.User, "format_avatar_url", return_value=mock_url): + assert test_presence_user_obj.format_avatar_url() is mock_url + users.User.format_avatar_url.assert_called_once_with(fmt=None, size=4096) + + @pytest.mark.parametrize(["avatar_hash", "discriminator"], [("dwaea22", entities.UNSET), (entities.UNSET, "2929")]) + def test_format_avatar_url_when_discriminator_or_avatar_hash_set_with_optionals( + self, test_presence_user_obj, avatar_hash, discriminator + ): + test_presence_user_obj.avatar_hash = avatar_hash + test_presence_user_obj.discriminator = discriminator + mock_url = mock.MagicMock(str) + with mock.patch.object(users.User, "format_avatar_url", return_value=mock_url): + assert test_presence_user_obj.format_avatar_url(fmt="nyaapeg", size=2048) is mock_url + users.User.format_avatar_url.assert_called_once_with(fmt="nyaapeg", size=2048) + + def test_format_avatar_url_when_discriminator_and_avatar_hash_unset(self, test_presence_user_obj): + test_presence_user_obj.avatar_hash = entities.UNSET + test_presence_user_obj.discriminator = entities.UNSET + with mock.patch.object(users.User, "format_avatar_url", return_value=...): + assert test_presence_user_obj.format_avatar_url() is entities.UNSET + users.User.format_avatar_url.assert_not_called() + + def test_default_avatar_when_discriminator_set(self, test_presence_user_obj): + test_presence_user_obj.discriminator = 4242 + assert test_presence_user_obj.default_avatar == 2 + + def test_default_avatar_when_discriminator_unset(self, test_presence_user_obj): + test_presence_user_obj.discriminator = entities.UNSET + assert test_presence_user_obj.default_avatar is entities.UNSET + @pytest.fixture() def test_guild_member_presence(test_user_payload, test_presence_activity_payload, test_client_status_payload): diff --git a/tests/hikari/test_messages.py b/tests/hikari/test_messages.py index 2079a7fa84..dcc2268a6e 100644 --- a/tests/hikari/test_messages.py +++ b/tests/hikari/test_messages.py @@ -123,6 +123,7 @@ def test_message_payload( "application": test_application_payload, "message_reference": test_message_crosspost_payload, "flags": 2, + "nonce": "171000788183678976", } @@ -258,3 +259,4 @@ def test_deserialize( assert message_obj.application == mock_app assert message_obj.message_reference == messages.MessageCrosspost.deserialize(test_message_crosspost_payload) assert message_obj.flags == messages.MessageFlag.IS_CROSSPOST + assert message_obj.nonce == "171000788183678976" diff --git a/tests/hikari/test_users.py b/tests/hikari/test_users.py index 1fb1dd0aa3..3365414101 100644 --- a/tests/hikari/test_users.py +++ b/tests/hikari/test_users.py @@ -48,6 +48,7 @@ def test_oauth_user_payload(): "verified": True, "locale": "en-US", "mfa_enabled": True, + "public_flags": int(users.UserFlag.VERIFIED_BOT_DEVELOPER), "flags": int(users.UserFlag.DISCORD_PARTNER | users.UserFlag.DISCORD_EMPLOYEE), "premium_type": 1, } @@ -62,7 +63,7 @@ def test_deserialize(self, test_user_payload): assert user_obj.discriminator == "6127" assert user_obj.is_bot is True assert user_obj.is_system is True - assert user_obj.public_flags == users.UserFlag.VERIFIED_BOT_DEVELOPER + assert user_obj.flags == users.UserFlag.VERIFIED_BOT_DEVELOPER @pytest.fixture() def user_obj(self, test_user_payload): @@ -73,7 +74,7 @@ def user_obj(self, test_user_payload): discriminator="6127", is_bot=None, is_system=None, - public_flags=None, + flags=None, ) def test_avatar_url(self, user_obj): From 658fb0214003daee94a97a3aae4a7ecbdedc804c Mon Sep 17 00:00:00 2001 From: Faster Speeding Date: Wed, 15 Apr 2020 15:22:18 +0100 Subject: [PATCH 128/922] Fixes #296: Fix unreliable test impl __init__ handling + fix several warnings. --- tests/hikari/clients/test_rest_client.py | 11 ++-- tests/hikari/internal/test_meta.py | 4 +- tests/hikari/net/test_rest.py | 55 ++++++------------- tests/hikari/state/test_event_dispatcher.py | 58 ++++++++++----------- 4 files changed, 52 insertions(+), 76 deletions(-) diff --git a/tests/hikari/clients/test_rest_client.py b/tests/hikari/clients/test_rest_client.py index 96d012669e..5e90c5063e 100644 --- a/tests/hikari/clients/test_rest_client.py +++ b/tests/hikari/clients/test_rest_client.py @@ -81,12 +81,9 @@ def low_level_rest_impl(self) -> rest.LowLevelRestfulClient: return mock.MagicMock(rest.LowLevelRestfulClient) @pytest.fixture() - def rest_clients_impl(self, low_level_rest_impl) -> rest_clients.RESTClient: - class RESTClient(rest_clients.RESTClient): - def __init__(self): - self._session: rest.LowLevelRestfulClient = low_level_rest_impl - - return RESTClient() + def rest_clients_impl(self, low_level_rest_impl, mock_config) -> rest_clients.RESTClient: + with mock.patch.object(rest, "LowLevelRestfulClient", return_value=low_level_rest_impl): + return rest_clients.RESTClient(mock_config) @pytest.mark.asyncio async def test_close_awaits_session_close(self, rest_clients_impl): @@ -1044,7 +1041,7 @@ async def test_safe_update_message_without_optionals(self, rest_clients_impl): rest_clients_impl.update_message = mock.AsyncMock(return_value=mock_message_obj) result = await rest_clients_impl.safe_update_message(message=message, channel=channel,) assert result is mock_message_obj - rest_clients_impl.update_message.safe_update_message( + rest_clients_impl.update_message.assert_called_once_with( message=message, channel=channel, content=..., diff --git a/tests/hikari/internal/test_meta.py b/tests/hikari/internal/test_meta.py index db02c8ecbe..66177e29fd 100644 --- a/tests/hikari/internal/test_meta.py +++ b/tests/hikari/internal/test_meta.py @@ -20,7 +20,7 @@ def test_SingletonMeta(): - class Test(metaclass=meta.SingletonMeta): + class StubSingleton(metaclass=meta.SingletonMeta): pass - assert Test() is Test() + assert StubSingleton() is StubSingleton() diff --git a/tests/hikari/net/test_rest.py b/tests/hikari/net/test_rest.py index 50071ff3b7..bf10fe9155 100644 --- a/tests/hikari/net/test_rest.py +++ b/tests/hikari/net/test_rest.py @@ -40,22 +40,16 @@ class TestLowLevelRestfulClient: @pytest.fixture def rest_impl(self): - class LowLevelRestfulClientImpl(rest.LowLevelRestfulClient): - def __init__(self, *args, **kwargs): - self.base_url = "https://discordapp.com/api/v6" - self.client_session = mock.MagicMock(close=mock.AsyncMock()) - self.logger = mock.MagicMock() - self.ratelimiter = mock.MagicMock( - ratelimits.HTTPBucketRateLimiterManager, - acquire=mock.MagicMock(), - update_rate_limits=mock.MagicMock(), - ) - self.global_ratelimiter = mock.MagicMock( - ratelimits.ManualRateLimiter, acquire=mock.MagicMock(), throttle=mock.MagicMock() - ) - self._request = mock.AsyncMock(return_value=...) + stack = contextlib.ExitStack() + stack.enter_context(mock.patch("aiohttp.ClientSession")) + stack.enter_context(mock.patch("hikari.internal.more_logging.get_named_logger")) + stack.enter_context(mock.patch.object(ratelimits, "HTTPBucketRateLimiterManager")) + stack.enter_context(mock.patch.object(ratelimits, "ManualRateLimiter")) + with stack: + client = rest.LowLevelRestfulClient(base_url="https://discordapp.com/api/v6", token="Bot blah.blah.blah") + client._request = mock.AsyncMock(return_value=...) - return LowLevelRestfulClientImpl() + return client @pytest.fixture def compiled_route(self): @@ -94,33 +88,18 @@ async def __aexit__(self, exc_type, exc_val, exc_tb): return Response() @pytest.mark.asyncio - async def test_rest___aenter___and___aexit__(self): - class LowLevelRestfulClientImpl(rest.LowLevelRestfulClient): - def __init__(self, *args, **kwargs): - kwargs.setdefault("token", "Bearer xxx") - super().__init__(*args, **kwargs) - self.close = mock.AsyncMock() + async def test_rest___aenter___and___aexit__(self, rest_impl): + rest_impl.close = mock.AsyncMock() - inst = LowLevelRestfulClientImpl() + async with rest_impl as client: + assert client is rest_impl - async with inst as client: - assert client is inst - - inst.close.assert_called_once_with() + rest_impl.close.assert_called_once_with() @pytest.mark.asyncio - async def test_rest_close_calls_client_session_close(self): - class LowLevelRestfulClientImpl(rest.LowLevelRestfulClient): - def __init__(self, *args, **kwargs): - self.client_session = mock.MagicMock() - self.client_session.close = mock.AsyncMock() - self.logger = logging.getLogger(__name__) - - inst = LowLevelRestfulClientImpl() - - await inst.close() - - inst.client_session.close.assert_called_with() + async def test_rest_close_calls_client_session_close(self, rest_impl): + await rest_impl.close() + rest_impl.client_session.close.assert_called_with() @pytest.mark.asyncio async def test__init__with_bot_token_and_without_optionals(self): diff --git a/tests/hikari/state/test_event_dispatcher.py b/tests/hikari/state/test_event_dispatcher.py index e6a8fff44a..f765852d8f 100644 --- a/tests/hikari/state/test_event_dispatcher.py +++ b/tests/hikari/state/test_event_dispatcher.py @@ -26,15 +26,15 @@ from tests.hikari import _helpers -class TestEvent1(events.HikariEvent): +class StudEvent1(events.HikariEvent): ... -class TestEvent2(events.HikariEvent): +class StudEvent2(events.HikariEvent): ... -class TestEvent3(events.HikariEvent): +class StudEvent3(events.HikariEvent): ... @@ -85,11 +85,11 @@ def fut(): ] dispatcher_inst._waiters = ( - waiters := {TestEvent1: test_event_1_waiters, TestEvent2: test_event_2_waiters,} + waiters := {StudEvent1: test_event_1_waiters, StudEvent2: test_event_2_waiters,} ) dispatcher_inst._listeners = ( - listeners := {TestEvent1: test_event_1_listeners, TestEvent2: test_event_2_listeners,} + listeners := {StudEvent1: test_event_1_listeners, StudEvent2: test_event_2_listeners,} ) dispatcher_inst.close() @@ -161,10 +161,10 @@ def test_dispatch_to_existing_muxes(self, dispatcher_inst): mock_coro_fn2 = mock.MagicMock() mock_coro_fn3 = mock.MagicMock() - ctx = TestEvent1() + ctx = StudEvent1() - dispatcher_inst._listeners[TestEvent1] = [mock_coro_fn1, mock_coro_fn2] - dispatcher_inst._listeners[TestEvent2] = [mock_coro_fn3] + dispatcher_inst._listeners[StudEvent1] = [mock_coro_fn1, mock_coro_fn2] + dispatcher_inst._listeners[StudEvent2] = [mock_coro_fn3] with mock.patch("asyncio.gather") as gather: dispatcher_inst.dispatch_event(ctx) @@ -176,7 +176,7 @@ def test_dispatch_to_non_existant_muxes(self, dispatcher_inst): # Should not throw. dispatcher_inst._waiters = {} dispatcher_inst._listeners = {} - dispatcher_inst.dispatch_event(TestEvent1()) + dispatcher_inst.dispatch_event(StudEvent1()) assert dispatcher_inst._waiters == {} assert dispatcher_inst._listeners == {} @@ -197,7 +197,7 @@ async def test_dispatch_is_awaitable_if_something_is_invoked(self, dispatcher_in @pytest.mark.asyncio @_helpers.timeout_after(1) async def test_dispatch_invokes_future_waker_if_registered_with_futures(self, dispatcher_inst, event_loop): - dispatcher_inst._waiters[TestEvent1] = {event_loop.create_future(): lambda _: False} + dispatcher_inst._waiters[StudEvent1] = {event_loop.create_future(): lambda _: False} @pytest.mark.asyncio @_helpers.timeout_after(1) @@ -208,11 +208,11 @@ async def test_dispatch_returns_exception_to_caller(self, dispatcher_inst, event future1 = event_loop.create_future() future2 = event_loop.create_future() - ctx = TestEvent3() + ctx = StudEvent3() - dispatcher_inst._waiters[TestEvent3] = {} - dispatcher_inst._waiters[TestEvent3][future1] = predicate1 - dispatcher_inst._waiters[TestEvent3][future2] = predicate2 + dispatcher_inst._waiters[StudEvent3] = {} + dispatcher_inst._waiters[StudEvent3][future1] = predicate1 + dispatcher_inst._waiters[StudEvent3][future2] = predicate2 await dispatcher_inst.dispatch_event(ctx) @@ -232,26 +232,26 @@ async def test_dispatch_returns_exception_to_caller(self, dispatcher_inst, event @pytest.mark.asyncio @_helpers.timeout_after(1) async def test_waiter_map_deleted_if_made_empty_during_this_dispatch(self, dispatcher_inst): - dispatcher_inst._waiters[TestEvent1] = {mock.MagicMock(): mock.MagicMock(return_value=True)} - dispatcher_inst.dispatch_event(TestEvent1()) + dispatcher_inst._waiters[StudEvent1] = {mock.MagicMock(): mock.MagicMock(return_value=True)} + dispatcher_inst.dispatch_event(StudEvent1()) await asyncio.sleep(0.1) - assert TestEvent1 not in dispatcher_inst._waiters + assert StudEvent1 not in dispatcher_inst._waiters @pytest.mark.asyncio @_helpers.timeout_after(1) async def test_waiter_map_not_deleted_if_not_empty(self, dispatcher_inst): - dispatcher_inst._waiters[TestEvent1] = {mock.MagicMock(): mock.MagicMock(return_value=False)} - dispatcher_inst.dispatch_event(TestEvent1()) + dispatcher_inst._waiters[StudEvent1] = {mock.MagicMock(): mock.MagicMock(return_value=False)} + dispatcher_inst.dispatch_event(StudEvent1()) await asyncio.sleep(0.1) - assert TestEvent1 in dispatcher_inst._waiters + assert StudEvent1 in dispatcher_inst._waiters @pytest.mark.asyncio @_helpers.timeout_after(2) async def test_wait_for_returns_event(self, dispatcher_inst): predicate = mock.MagicMock(return_value=True) - future = dispatcher_inst.wait_for(TestEvent1, timeout=5, predicate=predicate) + future = dispatcher_inst.wait_for(StudEvent1, timeout=5, predicate=predicate) - ctx = TestEvent1() + ctx = StudEvent1() await dispatcher_inst.dispatch_event(ctx) await asyncio.sleep(0.1) @@ -265,8 +265,8 @@ async def test_wait_for_returns_event(self, dispatcher_inst): @_helpers.timeout_after(2) async def test_wait_for_returns_matching_event_args_when_invoked_but_no_predicate_match(self, dispatcher_inst): predicate = mock.MagicMock(return_value=False) - ctx = TestEvent3() - future = dispatcher_inst.wait_for(TestEvent3, timeout=5, predicate=predicate) + ctx = StudEvent3() + future = dispatcher_inst.wait_for(StudEvent3, timeout=5, predicate=predicate) await dispatcher_inst.dispatch_event(ctx) await asyncio.sleep(0.1) @@ -286,8 +286,8 @@ async def test_wait_for_hits_timeout_and_raises(self, dispatcher_inst): @_helpers.assert_raises(type_=RuntimeError) async def test_wait_for_raises_predicate_errors(self, dispatcher_inst): predicate = mock.MagicMock(side_effect=RuntimeError) - ctx = TestEvent1() - future = dispatcher_inst.wait_for(TestEvent1, timeout=1, predicate=predicate) + ctx = StudEvent1() + future = dispatcher_inst.wait_for(StudEvent1, timeout=1, predicate=predicate) await dispatcher_inst.dispatch_event(ctx) await future @@ -309,7 +309,7 @@ async def test_other_events_in_same_waiter_event_name_do_not_awaken_us( @pytest.mark.asyncio async def test_catch_happy_path(self, dispatcher_inst): callback = mock.AsyncMock() - event = TestEvent1() + event = StudEvent1() dispatcher_inst.handle_exception = mock.MagicMock() await dispatcher_inst._catch(callback, event) callback.assert_awaited_once_with(event) @@ -320,7 +320,7 @@ async def test_catch_sad_path(self, dispatcher_inst): ex = RuntimeError() callback = mock.AsyncMock(side_effect=ex) dispatcher_inst.handle_exception = mock.MagicMock() - ctx = TestEvent3() + ctx = StudEvent3() await dispatcher_inst._catch(callback, ctx) dispatcher_inst.handle_exception.assert_called_once_with(ex, ctx, callback) @@ -328,7 +328,7 @@ def test_handle_exception_dispatches_exception_event_with_context(self, dispatch dispatcher_inst.dispatch_event = mock.MagicMock() ex = RuntimeError() - event = TestEvent1() + event = StudEvent1() callback = mock.AsyncMock() dispatcher_inst.handle_exception(ex, event, callback) From 311bc5a7fadd6ba172c3144f3e81054c51e23a7c Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Wed, 15 Apr 2020 17:00:41 +0100 Subject: [PATCH 129/922] Added intents to gateway runner flags --- hikari/clients/configs.py | 3 +- hikari/clients/gateway_runner.py | 10 ++++++- hikari/internal/conversions.py | 47 ++++++++++++++++++++++++++++++++ hikari/internal/marshaller.py | 46 ------------------------------- 4 files changed, 58 insertions(+), 48 deletions(-) diff --git a/hikari/clients/configs.py b/hikari/clients/configs.py index 4cc6857d31..cb1ef82036 100644 --- a/hikari/clients/configs.py +++ b/hikari/clients/configs.py @@ -37,6 +37,7 @@ import aiohttp import attr +from hikari.internal import conversions from hikari import entities from hikari import gateway_entities from hikari import guilds @@ -253,7 +254,7 @@ class WebsocketConfig(AIOHTTPConfig, TokenConfig, DebugConfig): #: #: :type: :obj:`hikari.net.codes.GatewayIntent`, optional intents: typing.Optional[codes.GatewayIntent] = marshaller.attrib( - deserializer=lambda value: marshaller.dereference_int_flag(codes.GatewayIntent, value), + deserializer=lambda value: conversions.dereference_int_flag(codes.GatewayIntent, value), if_undefined=None, default=None, ) diff --git a/hikari/clients/gateway_runner.py b/hikari/clients/gateway_runner.py index dc0d16cf71..fc96b26c31 100644 --- a/hikari/clients/gateway_runner.py +++ b/hikari/clients/gateway_runner.py @@ -30,6 +30,8 @@ from hikari.clients import configs from hikari.clients import gateway_managers +from hikari.internal import conversions +from hikari.net import codes from hikari.state import stateless_event_managers @@ -58,18 +60,23 @@ def _supports_color(): @click.option("--compression", default=True, type=click.BOOL, help="Enable or disable gateway compression.") @click.option("--color", default=_supports_color(), type=click.BOOL, help="Whether to enable or disable color.") @click.option("--debug", default=False, type=click.BOOL, help="Enable or disable debug mode.") +@click.option("--intents", default=None, type=click.STRING, help="Intent names to enable (comma separated)") @click.option("--logger", envvar="LOGGER", default="INFO", type=click.Choice(_LOGGER_LEVELS), help="Logger verbosity.") @click.option("--shards", default=1, type=click.IntRange(min=1), help="The number of shards to explicitly use.") @click.option("--token", required=True, envvar="TOKEN", help="The token to use to authenticate with Discord.") @click.option("--url", default="wss://gateway.discord.gg/", help="The websocket URL to connect to.") @click.option("--verify-ssl", default=True, type=click.BOOL, help="Enable or disable SSL verification.") @click.option("--version", default=6, type=click.IntRange(min=6), help="Version of the gateway to use.") -def run_gateway(compression, color, debug, logger, shards, token, url, verify_ssl, version) -> None: +def run_gateway(compression, color, debug, intents, logger, shards, token, url, verify_ssl, version) -> None: """:mod:`click` command line client for running a test gateway connection. This is provided for internal testing purposes for benchmarking API stability, etc. """ + if intents is not None: + intents = intents.split(",") + intents = conversions.dereference_int_flag(codes.GatewayIntent, intents) + logging.captureWarnings(True) logging.basicConfig(level=logger, format=_COLOR_FORMAT if color else _REGULAR_FORMAT, stream=sys.stdout) @@ -84,6 +91,7 @@ def run_gateway(compression, color, debug, logger, shards, token, url, verify_ss gateway_version=version, debug=debug, gateway_use_compression=compression, + intents=intents, verify_ssl=verify_ssl, ), url=url, diff --git a/hikari/internal/conversions.py b/hikari/internal/conversions.py index 80747fda9a..78ca540553 100644 --- a/hikari/internal/conversions.py +++ b/hikari/internal/conversions.py @@ -39,11 +39,17 @@ import contextlib import datetime import email.utils +import enum +import functools import io +import operator import re import types import typing + +IntFlagT = typing.TypeVar("IntFlagT", bound=enum.IntFlag) +RawIntFlagValueT = typing.Union[typing.AnyStr, typing.SupportsInt, int] DISCORD_EPOCH: typing.Final[int] = 1_420_070_400 ISO_8601_DATE_PART: typing.Final[typing.Pattern] = re.compile(r"^(\d{4})-(\d{2})-(\d{2})") ISO_8601_TIME_PART: typing.Final[typing.Pattern] = re.compile(r"T(\d{2}):(\d{2}):(\d{2})(?:\.(\d{1,6}))?", re.I) @@ -409,3 +415,44 @@ def snoop_typehint_from_scope(frame: types.FrameType, typehint: typing.Union[str except (AttributeError, KeyError): pass raise NameError(f"No attribute {typehint} was found in enclosing scope") + + +def dereference_int_flag( + int_flag_type: typing.Type[IntFlagT], + raw_value: typing.Union[RawIntFlagValueT, typing.Collection[RawIntFlagValueT]], +) -> IntFlagT: + """Cast to the provided :obj:`enum.IntFlag` type. + + This supports resolving bitfield integers as well as decoding a sequence + of case insensitive flag names into one combined value. + + Parameters + ---------- + int_flag_type : :obj:`typing.Type` [ :obj:`enum.IntFlag` ] + The type of the int flag to check. + raw_value : ``Castable Value`` + The raw value to convert. + + Returns + ------- + :obj:`enum.IntFlag` + The cast value as a flag. + + Notes + ----- + Types that are a ``Castable Value`` include: + - :obj:`str` + - :obj:`int` + - :obj:`typing.SupportsInt` + - :obj:`typing.Collection` [ ``Castable Value`` ] + + When a collection is passed, values will be combined using functional + reduction via the :obj:operator.or_` operator. + """ + if isinstance(raw_value, str) and raw_value.isdigit(): + raw_value = int(raw_value) + + if not isinstance(raw_value, int): + raw_value = functools.reduce(operator.or_, (int_flag_type[name.upper()] for name in raw_value)) + + return int_flag_type(raw_value) diff --git a/hikari/internal/marshaller.py b/hikari/internal/marshaller.py index 241caae76a..6f87042a87 100644 --- a/hikari/internal/marshaller.py +++ b/hikari/internal/marshaller.py @@ -34,10 +34,7 @@ "HikariEntityMarshaller", ] -import enum -import functools import importlib -import operator import typing import weakref @@ -54,8 +51,6 @@ _PASSED_THROUGH_SINGLETONS: typing.Final[typing.Sequence[bool]] = [False, True, None] RAISE: typing.Final[typing.Any] = object() -IntFlagT = typing.TypeVar("IntFlagT", bound=enum.IntFlag) -RawIntFlagValueT = typing.Union[typing.AnyStr, typing.SupportsInt, int] EntityT = typing.TypeVar("EntityT", contravariant=True) @@ -99,47 +94,6 @@ def dereference_handle(handle_string: str) -> typing.Any: return weakref.proxy(obj) -def dereference_int_flag( - int_flag_type: typing.Type[IntFlagT], - raw_value: typing.Union[RawIntFlagValueT, typing.Collection[RawIntFlagValueT]], -) -> IntFlagT: - """Cast to the provided :obj:`enum.IntFlag` type. - - This supports resolving bitfield integers as well as decoding a sequence - of case insensitive flag names into one combined value. - - Parameters - ---------- - int_flag_type : :obj:`typing.Type` [ :obj:`enum.IntFlag` ] - The type of the int flag to check. - raw_value : ``Castable Value`` - The raw value to convert. - - Returns - ------- - :obj:`enum.IntFlag` - The cast value as a flag. - - Notes - ----- - Types that are a ``Castable Value`` include: - - :obj:`str` - - :obj:`int` - - :obj:`typing.SupportsInt` - - :obj:`typing.Collection` [ ``Castable Value`` ] - - When a collection is passed, values will be combined using functional - reduction via the :obj:operator.or_` operator. - """ - if isinstance(raw_value, str) and raw_value.isdigit(): - raw_value = int(raw_value) - - if not isinstance(raw_value, int): - raw_value = functools.reduce(operator.or_, (int_flag_type[name.upper()] for name in raw_value)) - - return int_flag_type(raw_value) - - def attrib( *, # Mandatory! We do not want to rely on type annotations alone, as they will From ae4b539cc9502948bc56a831be1e446560205e4a Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Wed, 15 Apr 2020 18:11:29 +0100 Subject: [PATCH 130/922] Removed nested polling coroutine for shards as it was annoying me. --- ci/installations.gitlab-ci.yml | 2 +- ci/pip.nox.py | 7 +- hikari/clients/shard_clients.py | 2 +- hikari/net/shard.py | 93 ++++++++++----------- hikari/users.py | 6 +- tests/hikari/net/test_shard.py | 138 ++++++++++++++++++-------------- 6 files changed, 138 insertions(+), 110 deletions(-) diff --git a/ci/installations.gitlab-ci.yml b/ci/installations.gitlab-ci.yml index 098a0a5304..295220e07f 100644 --- a/ci/installations.gitlab-ci.yml +++ b/ci/installations.gitlab-ci.yml @@ -26,7 +26,7 @@ set -x apt install git -y pip install nox - nox --sessions pip pip-sdist pip-bdist-wheel pip-git + nox --sessions pip-sdist pip-bdist-wheel pip-git stage: install ### ### Setup script tests for CPython 3.8.0 diff --git a/ci/pip.nox.py b/ci/pip.nox.py index 738f0e5926..dc4f2b84d7 100644 --- a/ci/pip.nox.py +++ b/ci/pip.nox.py @@ -38,7 +38,12 @@ def temp_chdir(session: nox.Session, target: str): session.chdir(cwd) -@nox.session(reuse_venv=False, only_if=lambda: "CI" in os.environ) +def predicate(): + commit_ref = os.getenv("CI_COMMIT_REF_NAME") + return commit_ref in (config.PROD_BRANCH, config.PREPROD_BRANCH) and "CI" in os.environ + + +@nox.session(reuse_venv=False, only_if=predicate) def pip(session: nox.Session): """Run through sandboxed install of PyPI package.""" if "--showtime" in session.posargs: diff --git a/hikari/clients/shard_clients.py b/hikari/clients/shard_clients.py index 353a583b32..324f3a5098 100644 --- a/hikari/clients/shard_clients.py +++ b/hikari/clients/shard_clients.py @@ -436,7 +436,7 @@ async def _spin_up(self) -> asyncio.Task: self.logger.info("received HELLO, interval is %ss", self.connection.heartbeat_interval) completed, _ = await asyncio.wait( - [connect_task, self._connection.identify_event.wait()], return_when=asyncio.FIRST_COMPLETED + [connect_task, self._connection.handshake_event.wait()], return_when=asyncio.FIRST_COMPLETED ) if connect_task in completed: diff --git a/hikari/net/shard.py b/hikari/net/shard.py index e9e59ea1ee..d67fef6056 100644 --- a/hikari/net/shard.py +++ b/hikari/net/shard.py @@ -165,7 +165,7 @@ class ShardConnection: "heartbeat_interval", "heartbeat_latency", "hello_event", - "identify_event", + "handshake_event", "_intents", "_large_threshold", "_json_deserialize", @@ -237,7 +237,7 @@ class ShardConnection: #: be received. #: #: :type: :obj:`asyncio.Event` - identify_event: typing.Final[asyncio.Event] + handshake_event: typing.Final[asyncio.Event] #: The monotonic timestamp that the last ``HEARTBEAT`` was sent at, or #: ``nan`` if no ``HEARTBEAT`` has yet been sent. @@ -375,7 +375,7 @@ def __init__( self.heartbeat_interval: float = float("nan") self.heartbeat_latency: float = float("nan") self.hello_event: asyncio.Event = asyncio.Event() - self.identify_event: asyncio.Event = asyncio.Event() + self.handshake_event: asyncio.Event = asyncio.Event() self.last_heartbeat_sent: float = float("nan") self.last_message_received: float = float("nan") self.requesting_close_event: asyncio.Event = asyncio.Event() @@ -571,7 +571,7 @@ async def connect(self, client_session_type=aiohttp.ClientSession) -> None: self.closed_event.clear() self.hello_event.clear() - self.identify_event.clear() + self.handshake_event.clear() self.ready_event.clear() self.requesting_close_event.clear() self.resumed_event.clear() @@ -602,8 +602,7 @@ async def connect(self, client_session_type=aiohttp.ClientSession) -> None: self.logger.debug("received HELLO (interval:%ss)", self.heartbeat_interval) completed, pending_tasks = await asyncio.wait( - [self._heartbeat_keep_alive(self.heartbeat_interval), self._identify_or_resume_then_poll_events()], - return_when=asyncio.FIRST_COMPLETED, + [self._heartbeat_keep_alive(self.heartbeat_interval), self._run()], return_when=asyncio.FIRST_COMPLETED, ) # Kill other running tasks now. @@ -668,45 +667,6 @@ def _ws_connect_kwargs(self): def _cs_init_kwargs(self): return dict(connector=self._connector) - async def _identify_or_resume_then_poll_events(self): - if self.session_id is None: - self.logger.debug("preparing to send IDENTIFY") - - pl = { - "op": codes.GatewayOpcode.IDENTIFY, - "d": { - "token": self._token, - "compress": False, - "large_threshold": self._large_threshold, - "properties": user_agent.UserAgent().websocket_triplet, - "shard": [self.shard_id, self.shard_count], - }, - } - - # Do not always add this option; if it is None, exclude it for now. According to Mason, - # we can only use intents at the time of writing if our bot has less than 100 guilds. - # This means we need to give the user the option to opt in to this rather than breaking their - # bot with it if they have 100+ guilds. This restriction will be removed eventually. - if self._intents is not None: - pl["d"]["intents"] = self._intents - - if self._presence: - # noinspection PyTypeChecker - pl["d"]["presence"] = self._presence - await self._send(pl) - self.logger.debug("sent IDENTIFY, now listening to incoming events") - else: - self.logger.debug("preparing to send RESUME") - pl = { - "op": codes.GatewayOpcode.RESUME, - "d": {"token": self._token, "seq": self.seq, "session_id": self.session_id}, - } - await self._send(pl) - self.logger.debug("sent RESUME, now listening to incoming events") - - self.identify_event.set() - await self._poll_events() - async def _heartbeat_keep_alive(self, heartbeat_interval): while not self.requesting_close_event.is_set(): if self.last_message_received < self.last_heartbeat_sent: @@ -721,7 +681,48 @@ async def _heartbeat_keep_alive(self, heartbeat_interval): except asyncio.TimeoutError: pass - async def _poll_events(self): + async def _identify(self): + self.logger.debug("preparing to send IDENTIFY") + + pl = { + "op": codes.GatewayOpcode.IDENTIFY, + "d": { + "token": self._token, + "compress": False, + "large_threshold": self._large_threshold, + "properties": user_agent.UserAgent().websocket_triplet, + "shard": [self.shard_id, self.shard_count], + }, + } + + # From october 2020, we will likely just make this always passed + if self._intents is not None: + pl["d"]["intents"] = self._intents + + if self._presence: + # noinspection PyTypeChecker + pl["d"]["presence"] = self._presence + await self._send(pl) + self.logger.debug("sent IDENTIFY") + self.handshake_event.set() + + async def _resume(self): + self.logger.debug("preparing to send RESUME") + pl = { + "op": codes.GatewayOpcode.RESUME, + "d": {"token": self._token, "seq": self.seq, "session_id": self.session_id}, + } + await self._send(pl) + self.logger.debug("sent RESUME") + + async def _run(self): + if self.session_id is None: + await self._identify() + else: + await self._resume() + + self.handshake_event.set() + while not self.requesting_close_event.is_set(): next_pl = await self._receive() diff --git a/hikari/users.py b/hikari/users.py index fa38a92524..977853cc61 100644 --- a/hikari/users.py +++ b/hikari/users.py @@ -187,10 +187,10 @@ class MyUser(User): #: :type: :obj:`bool` is_mfa_enabled: bool = marshaller.attrib(raw_name="mfa_enabled", deserializer=bool) - #: The user's set language. + #: The user's set language. This is not provided by the ``READY`` event. #: - #: :type: :obj:`str` - locale: str = marshaller.attrib(deserializer=str) + #: :type: :obj:`str`, optional + locale: typing.Optional[str] = marshaller.attrib(deserializer=str, if_none=None, if_undefined=None) #: Whether the email for this user's account has been verified. #: Will be :obj:`None` if retrieved through the oauth2 flow without the diff --git a/tests/hikari/net/test_shard.py b/tests/hikari/net/test_shard.py index 0eab0878de..6134599400 100644 --- a/tests/hikari/net/test_shard.py +++ b/tests/hikari/net/test_shard.py @@ -83,7 +83,7 @@ async def ws_connect(self, *args, **kwargs): @pytest.mark.asyncio -class TestGatewayClientConstructor: +class TestShardConstructor: async def test_init_sets_shard_numbers_correctly(self,): input_shard_id, input_shard_count, expected_shard_id, expected_shard_count = 1, 2, 1, 2 client = shard.ShardConnection(shard_id=input_shard_id, shard_count=input_shard_count, token="xxx", url="yyy") @@ -125,7 +125,7 @@ async def test_init_connected_at_is_nan(self): @pytest.mark.asyncio -class TestGatewayClientUptimeProperty: +class TestShardUptimeProperty: @pytest.mark.parametrize( ["connected_at", "now", "expected_uptime"], [(float("nan"), 31.0, datetime.timedelta(seconds=0)), (10.0, 31.0, datetime.timedelta(seconds=21.0)),], @@ -138,7 +138,7 @@ async def test_uptime(self, connected_at, now, expected_uptime): @pytest.mark.asyncio -class TestGatewayClientIsConnectedProperty: +class TestShardIsConnectedProperty: @pytest.mark.parametrize(["connected_at", "is_connected"], [(float("nan"), False), (15, True), (2500.0, True),]) async def test_is_connected(self, connected_at, is_connected): client = shard.ShardConnection(token="xxx", url="yyy") @@ -182,7 +182,7 @@ async def test_returns_copy(self): @pytest.mark.asyncio -class TestGatewayClientAiohttpClientSessionKwargsProperty: +class TestShardAiohttpClientSessionKwargsProperty: async def test_right_stuff_is_included(self): connector = mock.MagicMock() @@ -192,7 +192,7 @@ async def test_right_stuff_is_included(self): @pytest.mark.asyncio -class TestGatewayClientWebSocketKwargsProperty: +class TestShardWebSocketKwargsProperty: async def test_right_stuff_is_included(self): url = "http://localhost.lan/discord" proxy_url = "http://localhost.lan/some_proxy" @@ -232,7 +232,7 @@ async def test_right_stuff_is_included(self): @pytest.mark.asyncio -class TestGatewayConnect: +class TestConnect: @pytest.fixture def client_session_t(self): return MockClientSession() @@ -269,11 +269,11 @@ async def test_RuntimeError_if_already_connected(self, client): pass assert client._ws is None - client._identify_or_resume_then_poll_events.assert_not_called() + client._run.assert_not_called() client._heartbeat_keep_alive.assert_not_called() @pytest.mark.parametrize( - "event_attr", ["closed_event", "identify_event", "ready_event", "requesting_close_event", "resumed_event"] + "event_attr", ["closed_event", "handshake_event", "ready_event", "requesting_close_event", "resumed_event"] ) @_helpers.timeout_after(10.0) async def test_events_unset_on_open(self, client, client_session_t, event_attr): @@ -425,7 +425,7 @@ async def test_identify_or_resume_then_poll_events_started(self, client, client_ with self.suppress_closure(): await client.connect(client_session_t) - client._identify_or_resume_then_poll_events.assert_called_once() + client._run.assert_called_once() @_helpers.timeout_after(10.0) async def test_waits_indefinitely_if_everything_is_working(self, client, client_session_t): @@ -433,7 +433,7 @@ async def deadlock(*_, **__): await asyncio.get_running_loop().create_future() client._heartbeat_keep_alive = deadlock - client._identify_or_resume_then_poll_events = deadlock + client._run = deadlock try: await asyncio.wait_for(client.connect(client_session_t), timeout=2.5) @@ -442,20 +442,18 @@ async def deadlock(*_, **__): pass @_helpers.timeout_after(10.0) - async def test_waits_for_identify_or_resume_then_poll_events_then_throws_that_exception( - self, client, client_session_t - ): + async def test_waits_for_run_then_throws_that_exception(self, client, client_session_t): async def deadlock(*_, **__): await asyncio.get_running_loop().create_future() class ExceptionThing(Exception): pass - async def identify_or_resume_then_poll_events(): + async def run(): raise ExceptionThing() client._heartbeat_keep_alive = deadlock - client._identify_or_resume_then_poll_events = identify_or_resume_then_poll_events + client._run = run try: await client.connect(client_session_t) @@ -473,7 +471,7 @@ async def heartbeat_keep_alive(_): pass client._heartbeat_keep_alive = heartbeat_keep_alive - client._identify_or_resume_then_poll_events = deadlock + client._run = deadlock try: await client.connect(client_session_t) @@ -487,11 +485,11 @@ async def test_waits_for_identify_or_resume_then_poll_events_to_return_throws_Ga async def deadlock(*_, **__): await asyncio.get_running_loop().create_future() - async def identify_or_resume_then_poll_events(): + async def run(): pass client._heartbeat_keep_alive = deadlock - client._identify_or_resume_then_poll_events = identify_or_resume_then_poll_events + client._run = run try: await client.connect(client_session_t) @@ -507,7 +505,7 @@ async def heartbeat_keep_alive(_): raise asyncio.TimeoutError("reee") client._heartbeat_keep_alive = heartbeat_keep_alive - client._identify_or_resume_then_poll_events = deadlock + client._run = deadlock try: await client.connect(client_session_t) @@ -521,11 +519,11 @@ async def test_TimeoutError_on_identify_or_resume_then_poll_events_raises_Gatewa async def deadlock(*_, **__): await asyncio.get_running_loop().create_future() - async def identify_or_resume_then_poll_events(): + async def run(): raise asyncio.TimeoutError("reee") client._heartbeat_keep_alive = deadlock - client._identify_or_resume_then_poll_events = identify_or_resume_then_poll_events + client._run = run try: await client.connect(client_session_t) @@ -535,46 +533,60 @@ async def identify_or_resume_then_poll_events(): @pytest.mark.asyncio -class TestGatewayClientIdentifyOrResumeThenPollEvents: +class TestShardRun: @pytest.fixture def client(self, event_loop): asyncio.set_event_loop(event_loop) client = _helpers.unslot_class(shard.ShardConnection)(token="1234", url="xxx") - client = _helpers.mock_methods_on(client, except_=("_identify_or_resume_then_poll_events",)) + client = _helpers.mock_methods_on(client, except_=("_run",)) - def send(_): - client.send_time = time.perf_counter() + def receive(): + client.recv_time = time.perf_counter() - def poll_events(): - client.poll_events_time = time.perf_counter() + def identify(): + client.identify_time = time.perf_counter() - client._send = mock.AsyncMock(wraps=send) - client._poll_events = mock.AsyncMock(spec=shard.ShardConnection._send, wraps=poll_events) + def resume(): + client.resume_time = time.perf_counter() + + client._identify = mock.AsyncMock(spec=shard.ShardConnection._identify, wraps=identify) + client._resume = mock.AsyncMock(spec=shard.ShardConnection._resume, wraps=resume) + client._receive = mock.AsyncMock(spec=shard.ShardConnection._receive, wraps=receive) return client async def test_no_session_id_sends_identify_then_polls_events(self, client): client.session_id = None - await client._identify_or_resume_then_poll_events() - - client._send.assert_awaited_once() - args, kwargs = client._send.call_args - assert len(args) == 1 - payload = args[0] - assert payload["op"] == 2 # IDENTIFY - client._poll_events.assert_awaited_once() - assert client.send_time <= client.poll_events_time + task = asyncio.create_task(client._run()) + await asyncio.sleep(0.25) + try: + client._identify.assert_awaited_once() + client._receive.assert_awaited_once() + client._resume.assert_not_called() + assert client.identify_time <= client.recv_time + finally: + task.cancel() async def test_session_id_sends_resume_then_polls_events(self, client): client.session_id = 69420 - await client._identify_or_resume_then_poll_events() + task = asyncio.create_task(client._run()) + await asyncio.sleep(0.25) + try: + client._resume.assert_awaited_once() + client._receive.assert_awaited_once() + client._identify.assert_not_called() + assert client.resume_time <= client.recv_time + finally: + task.cancel() + - client._send.assert_awaited_once() - args, kwargs = client._send.call_args - assert len(args) == 1 - payload = args[0] - assert payload["op"] == 6 # RESUME - client._poll_events.assert_awaited_once() - assert client.send_time <= client.poll_events_time +@pytest.mark.asyncio +class TestIdentify: + @pytest.fixture + def client(self, event_loop): + asyncio.set_event_loop(event_loop) + client = _helpers.unslot_class(shard.ShardConnection)(token="1234", url="xxx") + client = _helpers.mock_methods_on(client, except_=("_identify",)) + return client async def test_identify_payload_no_intents_no_presence(self, client): client._presence = None @@ -585,7 +597,7 @@ async def test_identify_payload_no_intents_no_presence(self, client): client.shard_id = 69 client.shard_count = 96 - await client._identify_or_resume_then_poll_events() + await client._identify() client._send.assert_awaited_once_with( { @@ -610,7 +622,7 @@ async def test_identify_payload_with_presence(self, client): client.shard_id = 69 client.shard_count = 96 - await client._identify_or_resume_then_poll_events() + await client._identify() client._send.assert_awaited_once_with( { @@ -636,7 +648,7 @@ async def test_identify_payload_with_intents(self, client): client.shard_id = 69 client.shard_count = 96 - await client._identify_or_resume_then_poll_events() + await client._identify() client._send.assert_awaited_once_with( { @@ -663,7 +675,7 @@ async def test_identify_payload_with_intents_and_presence(self, client): client.shard_id = 69 client.shard_count = 96 - await client._identify_or_resume_then_poll_events() + await client._identify() client._send.assert_awaited_once_with( { @@ -680,6 +692,16 @@ async def test_identify_payload_with_intents_and_presence(self, client): } ) + +@pytest.mark.asyncio +class TestResume: + @pytest.fixture + def client(self, event_loop): + asyncio.set_event_loop(event_loop) + client = _helpers.unslot_class(shard.ShardConnection)(token="1234", url="xxx") + client = _helpers.mock_methods_on(client, except_=("_resume",)) + return client + @_helpers.timeout_after(10.0) @pytest.mark.parametrize("seq", [None, 999]) async def test_resume_payload(self, client, seq): @@ -687,7 +709,7 @@ async def test_resume_payload(self, client, seq): client.seq = seq client.token = "reee" - await client._identify_or_resume_then_poll_events() + await client._resume() client._send.assert_awaited_once_with({"op": 6, "d": {"token": "1234", "session_id": 69420, "seq": seq,}}) @@ -713,13 +735,13 @@ def send(_): client._send = mock.AsyncMock(wraps=send) task: asyncio.Future = event_loop.create_task(client._heartbeat_keep_alive(0.01)) - await asyncio.sleep(2) + await asyncio.sleep(1.5) if task.done(): raise task.exception() client.requesting_close_event.set() - await asyncio.sleep(2) + await asyncio.sleep(1.5) assert task.done() assert client._send.await_count > 2 # arbitrary number to imply a lot of calls. @@ -746,8 +768,8 @@ async def test_last_heartbeat_ack_received_less_than_last_heartbeat_sent_raises_ async def test_heartbeat_payload(self, client, seq): client.seq = seq with contextlib.suppress(asyncio.TimeoutError): - with async_timeout.timeout(1.0): - await client._heartbeat_keep_alive(1.0) + with async_timeout.timeout(0.5): + await client._heartbeat_keep_alive(1) client._send.assert_awaited_once_with({"op": 1, "d": seq}) @@ -829,7 +851,7 @@ class TestPollEvents: def client(self, event_loop): asyncio.set_event_loop(event_loop) client = _helpers.unslot_class(shard.ShardConnection)(token="1234", url="xxx") - client = _helpers.mock_methods_on(client, except_=("_poll_events",)) + client = _helpers.mock_methods_on(client, except_=("_run",)) return client @_helpers.timeout_after(5.0) @@ -840,7 +862,7 @@ def receive(): client._receive = mock.AsyncMock(wraps=receive) - await client._poll_events() + await client._run() client.dispatch.assert_called_with(client, "MESSAGE_CREATE", {"content": "whatever"}) @@ -855,7 +877,7 @@ def receive(): client._receive = mock.AsyncMock(wraps=receive) - await client._poll_events() + await client._run() client.dispatch.assert_called_with(client, "READY", {"v": 69, "session_id": "1a2b3c4d"}) From a50bdb6090a88ce843695a21b8ab56d7ea613acf Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Wed, 15 Apr 2020 18:43:46 +0100 Subject: [PATCH 131/922] Fixed typo in master page generation stuff [skip deploy] --- ci/pages.gitlab-ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ci/pages.gitlab-ci.yml b/ci/pages.gitlab-ci.yml index bd8912a971..55d42a8bca 100644 --- a/ci/pages.gitlab-ci.yml +++ b/ci/pages.gitlab-ci.yml @@ -39,7 +39,7 @@ nox -s sphinx || nox -s documentation # FIXME: remove once in master. rm ../public -rf && mkdir ../public if [[ "$TARGET_BRANCH" = master ]]; then - mv public ../publi + mv public ../public else mv public "../public/${TARGET_BRANCH}" fi From 6078eb1e2aa0a8979b9f41d47c6f94f89e54f216 Mon Sep 17 00:00:00 2001 From: davfsa Date: Wed, 15 Apr 2020 09:21:30 +0200 Subject: [PATCH 132/922] Added default parameter to attrib in entities --- hikari/audit_logs.py | 24 +-- hikari/channels.py | 24 +-- hikari/clients/configs.py | 2 +- hikari/embeds.py | 28 +-- hikari/emojis.py | 21 +- hikari/entities.py | 2 +- hikari/events.py | 188 ++++++++++-------- hikari/gateway_entities.py | 6 +- hikari/guilds.py | 166 +++++++++------- hikari/invites.py | 30 ++- hikari/messages.py | 39 ++-- hikari/oauth2.py | 41 ++-- hikari/snowflakes.py | 2 +- hikari/state/stateless_event_managers.py | 4 + hikari/users.py | 22 +- hikari/voices.py | 10 +- hikari/webhooks.py | 10 +- .../state/test_stateless_event_managers.py | 6 +- tests/hikari/test_snowflake.py | 2 +- 19 files changed, 352 insertions(+), 275 deletions(-) diff --git a/hikari/audit_logs.py b/hikari/audit_logs.py index b4b4ec74cf..9c82e181d1 100644 --- a/hikari/audit_logs.py +++ b/hikari/audit_logs.py @@ -166,7 +166,7 @@ def __str__(self) -> str: @marshaller.marshallable() -@attr.s(slots=True) +@attr.s(slots=True, kw_only=True) class AuditLogChange(entities.HikariEntity, entities.Deserializable): """Represents a change made to an audit log entry's target entity.""" @@ -272,7 +272,7 @@ def decorator(cls): @marshaller.marshallable() -@attr.s(slots=True) +@attr.s(slots=True, kw_only=True) class BaseAuditLogEntryInfo(abc.ABC, entities.HikariEntity, entities.Deserializable): """A base object that all audit log entry info objects will inherit from.""" @@ -283,7 +283,7 @@ class BaseAuditLogEntryInfo(abc.ABC, entities.HikariEntity, entities.Deserializa AuditLogEventType.CHANNEL_OVERWRITE_DELETE, ) @marshaller.marshallable() -@attr.s(slots=True) +@attr.s(slots=True, kw_only=True) class ChannelOverwriteEntryInfo(BaseAuditLogEntryInfo): """Represents the extra information for overwrite related audit log entries. @@ -305,12 +305,12 @@ class ChannelOverwriteEntryInfo(BaseAuditLogEntryInfo): #: The name of the role this overwrite targets, if it targets a role. #: #: :type: :obj:`str`, optional - role_name: typing.Optional[str] = marshaller.attrib(deserializer=str, if_undefined=None) + role_name: typing.Optional[str] = marshaller.attrib(deserializer=str, if_undefined=None, default=None) @register_audit_log_entry_info(AuditLogEventType.MESSAGE_PIN, AuditLogEventType.MESSAGE_UNPIN) @marshaller.marshallable() -@attr.s(slots=True) +@attr.s(slots=True, kw_only=True) class MessagePinEntryInfo(BaseAuditLogEntryInfo): """The extra information for message pin related audit log entries. @@ -331,7 +331,7 @@ class MessagePinEntryInfo(BaseAuditLogEntryInfo): @register_audit_log_entry_info(AuditLogEventType.MEMBER_PRUNE) @marshaller.marshallable() -@attr.s(slots=True) +@attr.s(slots=True, kw_only=True) class MemberPruneEntryInfo(BaseAuditLogEntryInfo): """Represents the extra information attached to guild prune log entries.""" @@ -351,7 +351,7 @@ class MemberPruneEntryInfo(BaseAuditLogEntryInfo): @register_audit_log_entry_info(AuditLogEventType.MESSAGE_BULK_DELETE) @marshaller.marshallable() -@attr.s(slots=True) +@attr.s(slots=True, kw_only=True) class MessageBulkDeleteEntryInfo(BaseAuditLogEntryInfo): """Represents extra information for the message bulk delete audit entry.""" @@ -363,7 +363,7 @@ class MessageBulkDeleteEntryInfo(BaseAuditLogEntryInfo): @register_audit_log_entry_info(AuditLogEventType.MESSAGE_DELETE) @marshaller.marshallable() -@attr.s(slots=True) +@attr.s(slots=True, kw_only=True) class MessageDeleteEntryInfo(MessageBulkDeleteEntryInfo): """Represents extra information attached to the message delete audit entry.""" @@ -375,7 +375,7 @@ class MessageDeleteEntryInfo(MessageBulkDeleteEntryInfo): @register_audit_log_entry_info(AuditLogEventType.MEMBER_DISCONNECT) @marshaller.marshallable() -@attr.s(slots=True) +@attr.s(slots=True, kw_only=True) class MemberDisconnectEntryInfo(BaseAuditLogEntryInfo): """Represents extra information for the voice chat member disconnect entry.""" @@ -387,7 +387,7 @@ class MemberDisconnectEntryInfo(BaseAuditLogEntryInfo): @register_audit_log_entry_info(AuditLogEventType.MEMBER_MOVE) @marshaller.marshallable() -@attr.s(slots=True) +@attr.s(slots=True, kw_only=True) class MemberMoveEntryInfo(MemberDisconnectEntryInfo): """Represents extra information for the voice chat based member move entry.""" @@ -428,7 +428,7 @@ def get_entry_info_entity(type_: int) -> typing.Type[BaseAuditLogEntryInfo]: @marshaller.marshallable() -@attr.s(slots=True) +@attr.s(slots=True, kw_only=True) class AuditLogEntry(snowflakes.UniqueEntity, entities.Deserializable): """Represents an entry in a guild's audit log.""" @@ -495,7 +495,7 @@ def deserialize(cls, payload: entities.RawEntityT) -> "AuditLogEntry": @marshaller.marshallable() -@attr.s(slots=True) +@attr.s(slots=True, kw_only=True) class AuditLog(entities.HikariEntity, entities.Deserializable): """Represents a guilds audit log.""" diff --git a/hikari/channels.py b/hikari/channels.py index bd54d84301..a9f5ccf0a0 100644 --- a/hikari/channels.py +++ b/hikari/channels.py @@ -101,7 +101,7 @@ def __str__(self) -> str: @marshaller.marshallable() -@attr.s(slots=True) +@attr.s(slots=True, kw_only=True) class PermissionOverwrite(snowflakes.UniqueEntity, entities.Deserializable, entities.Serializable): """Represents permission overwrites for a channel or role in a channel.""" @@ -156,7 +156,7 @@ def decorator(cls): @marshaller.marshallable() -@attr.s(slots=True) +@attr.s(slots=True, kw_only=True) class Channel(snowflakes.UniqueEntity, entities.Deserializable): """Base class for all channels.""" @@ -167,7 +167,7 @@ class Channel(snowflakes.UniqueEntity, entities.Deserializable): @marshaller.marshallable() -@attr.s(slots=True) +@attr.s(slots=True, kw_only=True) class PartialChannel(Channel): """Represents a channel where we've only received it's basic information. @@ -182,7 +182,7 @@ class PartialChannel(Channel): @register_channel_type(ChannelType.DM) @marshaller.marshallable() -@attr.s(slots=True) +@attr.s(slots=True, kw_only=True) class DMChannel(Channel): """Represents a DM channel.""" @@ -208,7 +208,7 @@ class DMChannel(Channel): @register_channel_type(ChannelType.GROUP_DM) @marshaller.marshallable() -@attr.s(slots=True) +@attr.s(slots=True, kw_only=True) class GroupDMChannel(DMChannel): """Represents a DM group channel.""" @@ -232,12 +232,12 @@ class GroupDMChannel(DMChannel): #: #: :type: :obj:`hikari.snowflakes.Snowflake`, optional application_id: typing.Optional[snowflakes.Snowflake] = marshaller.attrib( - deserializer=snowflakes.Snowflake.deserialize, if_undefined=None + deserializer=snowflakes.Snowflake.deserialize, if_undefined=None, default=None ) @marshaller.marshallable() -@attr.s(slots=True) +@attr.s(slots=True, kw_only=True) class GuildChannel(Channel): """The base for anything that is a guild channel.""" @@ -276,14 +276,14 @@ class GuildChannel(Channel): @register_channel_type(ChannelType.GUILD_CATEGORY) @marshaller.marshallable() -@attr.s(slots=True) +@attr.s(slots=True, kw_only=True) class GuildCategory(GuildChannel): """Represents a guild category.""" @register_channel_type(ChannelType.GUILD_TEXT) @marshaller.marshallable() -@attr.s(slots=True) +@attr.s(slots=True, kw_only=True) class GuildTextChannel(GuildChannel): """Represents a guild text channel.""" @@ -320,7 +320,7 @@ class GuildTextChannel(GuildChannel): @register_channel_type(ChannelType.GUILD_NEWS) @marshaller.marshallable() -@attr.s(slots=True) +@attr.s(slots=True, kw_only=True) class GuildNewsChannel(GuildChannel): """Represents an news channel.""" @@ -344,14 +344,14 @@ class GuildNewsChannel(GuildChannel): @register_channel_type(ChannelType.GUILD_STORE) @marshaller.marshallable() -@attr.s(slots=True) +@attr.s(slots=True, kw_only=True) class GuildStoreChannel(GuildChannel): """Represents a store channel.""" @register_channel_type(ChannelType.GUILD_VOICE) @marshaller.marshallable() -@attr.s(slots=True) +@attr.s(slots=True, kw_only=True) class GuildVoiceChannel(GuildChannel): """Represents an voice channel.""" diff --git a/hikari/clients/configs.py b/hikari/clients/configs.py index cb1ef82036..c96ac1aab5 100644 --- a/hikari/clients/configs.py +++ b/hikari/clients/configs.py @@ -262,7 +262,7 @@ class WebsocketConfig(AIOHTTPConfig, TokenConfig, DebugConfig): #: The large threshold to use. #: #: :type: :obj:`int` - large_threshold: int = marshaller.attrib(deserializer=int, if_undefined=lambda: 250, default=True) + large_threshold: int = marshaller.attrib(deserializer=int, if_undefined=lambda: 250, default=250) def _parse_shard_info(payload): diff --git a/hikari/embeds.py b/hikari/embeds.py index 194ae6fe66..3963f2f9d9 100644 --- a/hikari/embeds.py +++ b/hikari/embeds.py @@ -40,7 +40,7 @@ @marshaller.marshallable() -@attr.s(slots=True) +@attr.s(slots=True, kw_only=True) class EmbedFooter(entities.HikariEntity, entities.Deserializable, entities.Serializable): """Represents an embed footer.""" @@ -71,7 +71,7 @@ class EmbedFooter(entities.HikariEntity, entities.Deserializable, entities.Seria @marshaller.marshallable() -@attr.s(slots=True) +@attr.s(slots=True, kw_only=True) class EmbedImage(entities.HikariEntity, entities.Deserializable, entities.Serializable): """Represents an embed image.""" @@ -117,7 +117,7 @@ class EmbedImage(entities.HikariEntity, entities.Deserializable, entities.Serial @marshaller.marshallable() -@attr.s(slots=True) +@attr.s(slots=True, kw_only=True) class EmbedThumbnail(entities.HikariEntity, entities.Deserializable, entities.Serializable): """Represents an embed thumbnail.""" @@ -163,7 +163,7 @@ class EmbedThumbnail(entities.HikariEntity, entities.Deserializable, entities.Se @marshaller.marshallable() -@attr.s(slots=True) +@attr.s(slots=True, kw_only=True) class EmbedVideo(entities.HikariEntity, entities.Deserializable): """Represents an embed video. @@ -176,21 +176,21 @@ class EmbedVideo(entities.HikariEntity, entities.Deserializable): #: The URL of the video. #: #: :type: :obj:`str`, optional - url: typing.Optional[str] = marshaller.attrib(deserializer=str, if_undefined=None) + url: typing.Optional[str] = marshaller.attrib(deserializer=str, if_undefined=None, default=None) #: The height of the video. #: #: :type: :obj:`int`, optional - height: typing.Optional[int] = marshaller.attrib(deserializer=int, if_undefined=None) + height: typing.Optional[int] = marshaller.attrib(deserializer=int, if_undefined=None, default=None) #: The width of the video. #: #: :type: :obj:`int`, optional - width: typing.Optional[int] = marshaller.attrib(deserializer=int, if_undefined=None) + width: typing.Optional[int] = marshaller.attrib(deserializer=int, if_undefined=None, default=None) @marshaller.marshallable() -@attr.s(slots=True) +@attr.s(slots=True, kw_only=True) class EmbedProvider(entities.HikariEntity, entities.Deserializable): """Represents an embed provider. @@ -203,16 +203,16 @@ class EmbedProvider(entities.HikariEntity, entities.Deserializable): #: The name of the provider. #: #: :type: :obj:`str`, optional - name: typing.Optional[str] = marshaller.attrib(deserializer=str, if_undefined=None) + name: typing.Optional[str] = marshaller.attrib(deserializer=str, if_undefined=None, default=None) #: The URL of the provider. #: #: :type: :obj:`str`, optional - url: typing.Optional[str] = marshaller.attrib(deserializer=str, if_undefined=None, if_none=None) + url: typing.Optional[str] = marshaller.attrib(deserializer=str, if_undefined=None, if_none=None, default=None) @marshaller.marshallable() -@attr.s(slots=True) +@attr.s(slots=True, kw_only=True) class EmbedAuthor(entities.HikariEntity, entities.Deserializable, entities.Serializable): """Represents an embed author.""" @@ -248,7 +248,7 @@ class EmbedAuthor(entities.HikariEntity, entities.Deserializable, entities.Seria @marshaller.marshallable() -@attr.s(slots=True) +@attr.s(slots=True, kw_only=True) class EmbedField(entities.HikariEntity, entities.Deserializable, entities.Serializable): """Represents a field in a embed.""" @@ -266,12 +266,12 @@ class EmbedField(entities.HikariEntity, entities.Deserializable, entities.Serial #: #: :type: :obj:`bool` is_inline: bool = marshaller.attrib( - raw_name="inline", deserializer=bool, serializer=bool, if_undefined=False, default=True + raw_name="inline", deserializer=bool, serializer=bool, if_undefined=False, default=False ) @marshaller.marshallable() -@attr.s(slots=True) +@attr.s(slots=True, kw_only=True) class Embed(entities.HikariEntity, entities.Deserializable, entities.Serializable): """Represents an embed.""" diff --git a/hikari/emojis.py b/hikari/emojis.py index 37f81beba9..c8eb8f57fd 100644 --- a/hikari/emojis.py +++ b/hikari/emojis.py @@ -30,13 +30,13 @@ @marshaller.marshallable() -@attr.s(slots=True) +@attr.s(slots=True, kw_only=True) class Emoji(entities.HikariEntity, entities.Deserializable): """Base class for all emojis.""" @marshaller.marshallable() -@attr.s(slots=True) +@attr.s(slots=True, kw_only=True) class UnicodeEmoji(Emoji): """Represents a unicode emoji.""" @@ -57,7 +57,7 @@ def mention(self) -> str: @marshaller.marshallable() -@attr.s(slots=True) +@attr.s(slots=True, kw_only=True) class UnknownEmoji(Emoji, snowflakes.UniqueEntity): """Represents a unknown emoji.""" @@ -69,7 +69,9 @@ class UnknownEmoji(Emoji, snowflakes.UniqueEntity): #: Whether the emoji is animated. #: #: :type: :obj:`bool` - is_animated: bool = marshaller.attrib(raw_name="animated", deserializer=bool, if_undefined=False) + is_animated: bool = marshaller.attrib( + raw_name="animated", deserializer=bool, if_undefined=False, if_none=None, default=False + ) @property def url_name(self) -> str: @@ -78,7 +80,7 @@ def url_name(self) -> str: @marshaller.marshallable() -@attr.s(slots=True) +@attr.s(slots=True, kw_only=True) class GuildEmoji(UnknownEmoji): """Represents a guild emoji.""" @@ -89,6 +91,7 @@ class GuildEmoji(UnknownEmoji): raw_name="roles", deserializer=lambda roles: {snowflakes.Snowflake.deserialize(r) for r in roles}, if_undefined=dict, + factory=dict, ) #: The user that created the emoji. @@ -101,20 +104,22 @@ class GuildEmoji(UnknownEmoji): #: #: :type: :obj:`hikari.users.User`, optional user: typing.Optional[users.User] = marshaller.attrib( - deserializer=users.User.deserialize, if_none=None, if_undefined=None + deserializer=users.User.deserialize, if_none=None, if_undefined=None, default=None ) #: Whether this emoji must be wrapped in colons. #: #: :type: :obj:`bool`, optional is_colons_required: typing.Optional[bool] = marshaller.attrib( - raw_name="require_colons", deserializer=bool, if_undefined=None + raw_name="require_colons", deserializer=bool, if_undefined=None, default=None ) #: Whether the emoji is managed by an integration. #: #: :type: :obj:`bool`, optional - is_managed: typing.Optional[bool] = marshaller.attrib(raw_name="managed", deserializer=bool, if_undefined=None) + is_managed: typing.Optional[bool] = marshaller.attrib( + raw_name="managed", deserializer=bool, if_undefined=None, default=None + ) @property def mention(self) -> str: diff --git a/hikari/entities.py b/hikari/entities.py index 77c261a753..c748527f87 100644 --- a/hikari/entities.py +++ b/hikari/entities.py @@ -53,7 +53,7 @@ def __repr__(self): @marshaller.marshallable() -@attr.s(slots=True) +@attr.s(slots=True, kw_only=True) class HikariEntity(metaclass=abc.ABCMeta): """The base for any entity used in this API.""" diff --git a/hikari/events.py b/hikari/events.py index 948199705f..db748d90cb 100644 --- a/hikari/events.py +++ b/hikari/events.py @@ -90,7 +90,7 @@ # Base event, is not deserialized @marshaller.marshallable() -@attr.s(slots=True) +@attr.s(slots=True, kw_only=True) class HikariEvent(entities.HikariEntity): """The base class that all events inherit from.""" @@ -171,7 +171,7 @@ class ResumedEvent(HikariEvent): @marshaller.marshallable() -@attr.s(slots=True) +@attr.s(slots=True, kw_only=True) class ReadyEvent(HikariEvent, entities.Deserializable): """Represents the gateway Ready event. @@ -206,7 +206,7 @@ class ReadyEvent(HikariEvent, entities.Deserializable): #: #: :type: :obj:`typing.Tuple` [ :obj:`int`, :obj:`int` ], optional _shard_information: typing.Optional[typing.Tuple[int, int]] = marshaller.attrib( - raw_name="shard", deserializer=tuple, if_undefined=None + raw_name="shard", deserializer=tuple, if_undefined=None, default=None ) @property @@ -227,7 +227,7 @@ def shard_count(self) -> typing.Optional[int]: @marshaller.marshallable() -@attr.s(slots=True) +@attr.s(slots=True, kw_only=True) class BaseChannelEvent(HikariEvent, snowflakes.UniqueEntity, entities.Deserializable): """A base object that Channel events will inherit from.""" @@ -262,41 +262,43 @@ class BaseChannelEvent(HikariEvent, snowflakes.UniqueEntity, entities.Deserializ #: The name of this channel, if applicable. #: #: :type: :obj:`str`, optional - name: typing.Optional[str] = marshaller.attrib(deserializer=str, if_undefined=None) + name: typing.Optional[str] = marshaller.attrib(deserializer=str, if_undefined=None, default=None) #: The topic of this channel, if applicable and set. #: #: :type: :obj:`str`, optional - topic: typing.Optional[str] = marshaller.attrib(deserializer=str, if_undefined=None, if_none=None) + topic: typing.Optional[str] = marshaller.attrib(deserializer=str, if_undefined=None, if_none=None, default=None) #: Whether this channel is nsfw, will be :obj:`None` if not applicable. #: #: :type: :obj:`bool`, optional - is_nsfw: typing.Optional[bool] = marshaller.attrib(raw_name="nsfw", deserializer=bool, if_undefined=None) + is_nsfw: typing.Optional[bool] = marshaller.attrib( + raw_name="nsfw", deserializer=bool, if_undefined=None, default=None + ) #: The ID of the last message sent, if it's a text type channel. #: #: :type: :obj:`hikari.snowflakes.Snowflake`, optional last_message_id: typing.Optional[snowflakes.Snowflake] = marshaller.attrib( - deserializer=snowflakes.Snowflake.deserialize, if_none=None, if_undefined=None + deserializer=snowflakes.Snowflake.deserialize, if_none=None, if_undefined=None, default=None ) #: The bitrate (in bits) of this channel, if it's a guild voice channel. #: #: :type: :obj:`bool`, optional - bitrate: typing.Optional[int] = marshaller.attrib(deserializer=int, if_undefined=None) + bitrate: typing.Optional[int] = marshaller.attrib(deserializer=int, if_undefined=None, default=None) #: The user limit for this channel if it's a guild voice channel. #: #: :type: :obj:`bool`, optional - user_limit: typing.Optional[int] = marshaller.attrib(deserializer=int, if_undefined=None) + user_limit: typing.Optional[int] = marshaller.attrib(deserializer=int, if_undefined=None, default=None) #: The rate limit a user has to wait before sending another message in this #: channel, if it's a guild text like channel. #: #: :type: :obj:`datetime.timedelta`, optional rate_limit_per_user: typing.Optional[datetime.timedelta] = marshaller.attrib( - deserializer=lambda delta: datetime.timedelta(seconds=delta), if_undefined=None, + deserializer=lambda delta: datetime.timedelta(seconds=delta), if_undefined=None, default=None ) #: A mapping of this channel's recipient users, if it's a DM or group DM. @@ -305,20 +307,21 @@ class BaseChannelEvent(HikariEvent, snowflakes.UniqueEntity, entities.Deserializ recipients: typing.Optional[typing.Mapping[snowflakes.Snowflake, users.User]] = marshaller.attrib( deserializer=lambda recipients: {user.id: user for user in map(users.User.deserialize, recipients)}, if_undefined=None, + default=None, ) #: The hash of this channel's icon, if it's a group DM channel and is set. #: #: :type: :obj:`str`, optional icon_hash: typing.Optional[str] = marshaller.attrib( - raw_name="icon", deserializer=str, if_undefined=None, if_none=None + raw_name="icon", deserializer=str, if_undefined=None, if_none=None, default=None ) #: The ID of this channel's creator, if it's a DM channel. #: #: :type: :obj:`hikari.snowflakes.Snowflake`, optional owner_id: typing.Optional[snowflakes.Snowflake] = marshaller.attrib( - deserializer=snowflakes.Snowflake.deserialize, if_undefined=None + deserializer=snowflakes.Snowflake.deserialize, if_undefined=None, default=None ) #: The ID of the application that created the group DM, if it's a @@ -326,14 +329,14 @@ class BaseChannelEvent(HikariEvent, snowflakes.UniqueEntity, entities.Deserializ #: #: :type: :obj:`hikari.snowflakes.Snowflake`, optional application_id: typing.Optional[snowflakes.Snowflake] = marshaller.attrib( - deserializer=snowflakes.Snowflake.deserialize, if_undefined=None + deserializer=snowflakes.Snowflake.deserialize, if_undefined=None, default=None ) #: The ID of this channels's parent category within guild, if set. #: #: :type: :obj:`hikari.snowflakes.Snowflake`, optional parent_id: typing.Optional[snowflakes.Snowflake] = marshaller.attrib( - deserializer=snowflakes.Snowflake.deserialize, if_undefined=None, if_none=None + deserializer=snowflakes.Snowflake.deserialize, if_undefined=None, if_none=None, default=None ) #: The datetime of when the last message was pinned in this channel, @@ -341,12 +344,12 @@ class BaseChannelEvent(HikariEvent, snowflakes.UniqueEntity, entities.Deserializ #: #: :type: :obj:`datetime.datetime`, optional last_pin_timestamp: typing.Optional[datetime.datetime] = marshaller.attrib( - deserializer=conversions.parse_iso_8601_ts, if_undefined=None + deserializer=conversions.parse_iso_8601_ts, if_undefined=None, default=None ) @marshaller.marshallable() -@attr.s(slots=True) +@attr.s(slots=True, kw_only=True) class ChannelCreateEvent(BaseChannelEvent): """Represents Channel Create gateway events. @@ -356,19 +359,19 @@ class ChannelCreateEvent(BaseChannelEvent): @marshaller.marshallable() -@attr.s(slots=True) +@attr.s(slots=True, kw_only=True) class ChannelUpdateEvent(BaseChannelEvent): """Represents Channel Update gateway events.""" @marshaller.marshallable() -@attr.s(slots=True) +@attr.s(slots=True, kw_only=True) class ChannelDeleteEvent(BaseChannelEvent): """Represents Channel Delete gateway events.""" @marshaller.marshallable() -@attr.s(slots=True) +@attr.s(slots=True, kw_only=True) class ChannelPinUpdateEvent(HikariEvent, entities.Deserializable): """Used to represent the Channel Pins Update gateway event. @@ -381,7 +384,7 @@ class ChannelPinUpdateEvent(HikariEvent, entities.Deserializable): #: #: :type: :obj:`hikari.snowflakes.Snowflake`, optional guild_id: typing.Optional[snowflakes.Snowflake] = marshaller.attrib( - deserializer=snowflakes.Snowflake.deserialize, if_undefined=None + deserializer=snowflakes.Snowflake.deserialize, if_undefined=None, default=None ) #: The ID of the channel where the message was pinned or unpinned. @@ -394,12 +397,12 @@ class ChannelPinUpdateEvent(HikariEvent, entities.Deserializable): #: #: :type: :obj:`datetime.datetime`, optional last_pin_timestamp: typing.Optional[datetime.datetime] = marshaller.attrib( - deserializer=conversions.parse_iso_8601_ts, if_undefined=None + deserializer=conversions.parse_iso_8601_ts, if_undefined=None, default=None ) @marshaller.marshallable() -@attr.s(slots=True) +@attr.s(slots=True, kw_only=True) class GuildCreateEvent(HikariEvent, entities.Deserializable): """Used to represent Guild Create gateway events. @@ -409,13 +412,13 @@ class GuildCreateEvent(HikariEvent, entities.Deserializable): @marshaller.marshallable() -@attr.s(slots=True) +@attr.s(slots=True, kw_only=True) class GuildUpdateEvent(HikariEvent, entities.Deserializable): """Used to represent Guild Update gateway events.""" @marshaller.marshallable() -@attr.s(slots=True) +@attr.s(slots=True, kw_only=True) class GuildLeaveEvent(HikariEvent, snowflakes.UniqueEntity, entities.Deserializable): """Fired when the current user leaves the guild or is kicked/banned from it. @@ -426,7 +429,7 @@ class GuildLeaveEvent(HikariEvent, snowflakes.UniqueEntity, entities.Deserializa @marshaller.marshallable() -@attr.s(slots=True) +@attr.s(slots=True, kw_only=True) class GuildUnavailableEvent(HikariEvent, snowflakes.UniqueEntity, entities.Deserializable): """Fired when a guild becomes temporarily unavailable due to an outage. @@ -437,7 +440,7 @@ class GuildUnavailableEvent(HikariEvent, snowflakes.UniqueEntity, entities.Deser @marshaller.marshallable() -@attr.s(slots=True) +@attr.s(slots=True, kw_only=True) class BaseGuildBanEvent(HikariEvent, entities.Deserializable): """A base object that guild ban events will inherit from.""" @@ -453,19 +456,19 @@ class BaseGuildBanEvent(HikariEvent, entities.Deserializable): @marshaller.marshallable() -@attr.s(slots=True) +@attr.s(slots=True, kw_only=True) class GuildBanAddEvent(BaseGuildBanEvent): """Used to represent a Guild Ban Add gateway event.""" @marshaller.marshallable() -@attr.s(slots=True) +@attr.s(slots=True, kw_only=True) class GuildBanRemoveEvent(BaseGuildBanEvent): """Used to represent a Guild Ban Remove gateway event.""" @marshaller.marshallable() -@attr.s(slots=True) +@attr.s(slots=True, kw_only=True) class GuildEmojisUpdateEvent(HikariEvent, entities.Deserializable): """Represents a Guild Emoji Update gateway event.""" @@ -483,7 +486,7 @@ class GuildEmojisUpdateEvent(HikariEvent, entities.Deserializable): @marshaller.marshallable() -@attr.s(slots=True) +@attr.s(slots=True, kw_only=True) class GuildIntegrationsUpdateEvent(HikariEvent, entities.Deserializable): """Used to represent Guild Integration Update gateway events.""" @@ -494,7 +497,7 @@ class GuildIntegrationsUpdateEvent(HikariEvent, entities.Deserializable): @marshaller.marshallable() -@attr.s(slots=True) +@attr.s(slots=True, kw_only=True) class GuildMemberAddEvent(HikariEvent, guilds.GuildMember): """Used to represent a Guild Member Add gateway event.""" @@ -505,7 +508,7 @@ class GuildMemberAddEvent(HikariEvent, guilds.GuildMember): @marshaller.marshallable() -@attr.s(slots=True) +@attr.s(slots=True, kw_only=True) class GuildMemberUpdateEvent(HikariEvent, entities.Deserializable): """Used to represent a Guild Member Update gateway event. @@ -534,7 +537,7 @@ class GuildMemberUpdateEvent(HikariEvent, entities.Deserializable): #: #: :type: :obj:`typing.Union` [ :obj:`str`, :obj:`hikari.entities.UNSET` ], optional nickname: typing.Union[None, str, entities.Unset] = marshaller.attrib( - raw_name="nick", deserializer=str, if_none=None, if_undefined=entities.Unset, + raw_name="nick", deserializer=str, if_none=None, if_undefined=entities.Unset, default=entities.UNSET ) #: The datetime of when this member started "boosting" this guild. @@ -542,12 +545,12 @@ class GuildMemberUpdateEvent(HikariEvent, entities.Deserializable): #: #: :type: :obj:`typing.Union` [ :obj:`datetime.datetime`, :obj:`hikari.entities.UNSET` ], optional premium_since: typing.Union[None, datetime.datetime, entities.Unset] = marshaller.attrib( - deserializer=conversions.parse_iso_8601_ts, if_none=None, if_undefined=entities.Unset + deserializer=conversions.parse_iso_8601_ts, if_none=None, if_undefined=entities.Unset, default=entities.UNSET ) @marshaller.marshallable() -@attr.s(slots=True) +@attr.s(slots=True, kw_only=True) class GuildMemberRemoveEvent(HikariEvent, entities.Deserializable): """Used to represent Guild Member Remove gateway events. @@ -566,7 +569,7 @@ class GuildMemberRemoveEvent(HikariEvent, entities.Deserializable): @marshaller.marshallable() -@attr.s(slots=True) +@attr.s(slots=True, kw_only=True) class GuildRoleCreateEvent(HikariEvent, entities.Deserializable): """Used to represent a Guild Role Create gateway event.""" @@ -582,7 +585,7 @@ class GuildRoleCreateEvent(HikariEvent, entities.Deserializable): @marshaller.marshallable() -@attr.s(slots=True) +@attr.s(slots=True, kw_only=True) class GuildRoleUpdateEvent(HikariEvent, entities.Deserializable): """Used to represent a Guild Role Create gateway event.""" @@ -598,7 +601,7 @@ class GuildRoleUpdateEvent(HikariEvent, entities.Deserializable): @marshaller.marshallable() -@attr.s(slots=True) +@attr.s(slots=True, kw_only=True) class GuildRoleDeleteEvent(HikariEvent, entities.Deserializable): """Represents a gateway Guild Role Delete Event.""" @@ -614,7 +617,7 @@ class GuildRoleDeleteEvent(HikariEvent, entities.Deserializable): @marshaller.marshallable() -@attr.s(slots=True) +@attr.s(slots=True, kw_only=True) class InviteCreateEvent(HikariEvent, entities.Deserializable): """Represents a gateway Invite Create event.""" @@ -638,13 +641,15 @@ class InviteCreateEvent(HikariEvent, entities.Deserializable): #: #: :type: :obj:`hikari.snowflakes.Snowflake`, optional guild_id: typing.Optional[snowflakes.Snowflake] = marshaller.attrib( - deserializer=snowflakes.Snowflake.deserialize, if_undefined=None + deserializer=snowflakes.Snowflake.deserialize, if_undefined=None, default=None ) #: The object of the user who created this invite, if applicable. #: #: :type: :obj:`hikari.users.User`, optional - inviter: typing.Optional[users.User] = marshaller.attrib(deserializer=users.User.deserialize, if_undefined=None) + inviter: typing.Optional[users.User] = marshaller.attrib( + deserializer=users.User.deserialize, if_undefined=None, default=None + ) #: The timedelta of how long this invite will be valid for. #: If set to :obj:`None` then this is unlimited. @@ -663,13 +668,15 @@ class InviteCreateEvent(HikariEvent, entities.Deserializable): #: The object of the user who this invite targets, if set. #: #: :type: :obj:`hikari.users.User`, optional - target_user: typing.Optional[users.User] = marshaller.attrib(deserializer=users.User.deserialize, if_undefined=None) + target_user: typing.Optional[users.User] = marshaller.attrib( + deserializer=users.User.deserialize, if_undefined=None, default=None + ) #: The type of user target this invite is, if applicable. #: #: :type: :obj:`hikari.invites.TargetUserType`, optional target_user_type: typing.Optional[invites.TargetUserType] = marshaller.attrib( - deserializer=invites.TargetUserType, if_undefined=None + deserializer=invites.TargetUserType, if_undefined=None, default=None ) #: Whether this invite grants temporary membership. @@ -684,7 +691,7 @@ class InviteCreateEvent(HikariEvent, entities.Deserializable): @marshaller.marshallable() -@attr.s(slots=True) +@attr.s(slots=True, kw_only=True) class InviteDeleteEvent(HikariEvent, entities.Deserializable): """Used to represent Invite Delete gateway events. @@ -706,19 +713,19 @@ class InviteDeleteEvent(HikariEvent, entities.Deserializable): #: #: :type: :obj:`hikari.snowflakes.Snowflake`, optional guild_id: typing.Optional[snowflakes.Snowflake] = marshaller.attrib( - deserializer=snowflakes.Snowflake.deserialize, if_undefined=None + deserializer=snowflakes.Snowflake.deserialize, if_undefined=None, default=None ) @marshaller.marshallable() -@attr.s(slots=True) +@attr.s(slots=True, kw_only=True) class MessageCreateEvent(HikariEvent, messages.Message): """Used to represent Message Create gateway events.""" # This is an arbitrarily partial version of `messages.Message` @marshaller.marshallable() -@attr.s(slots=True) +@attr.s(slots=True, kw_only=True) class MessageUpdateEvent(HikariEvent, snowflakes.UniqueEntity, entities.Deserializable): """Represents Message Update gateway events. @@ -739,33 +746,35 @@ class MessageUpdateEvent(HikariEvent, snowflakes.UniqueEntity, entities.Deserial #: #: :type: :obj:`typing.Union` [ :obj:`hikari.snowflakes.Snowflake`, :obj:`hikari.entities.UNSET` ] guild_id: typing.Union[snowflakes.Snowflake, entities.Unset] = marshaller.attrib( - deserializer=snowflakes.Snowflake.deserialize, if_undefined=entities.Unset + deserializer=snowflakes.Snowflake.deserialize, if_undefined=entities.Unset, default=entities.UNSET ) #: The author of this message. #: #: :type: :obj:`typing.Union` [ :obj:`hikari.users.User`, :obj:`hikari.entities.UNSET` ] author: typing.Union[users.User, entities.Unset] = marshaller.attrib( - deserializer=users.User.deserialize, if_undefined=entities.Unset + deserializer=users.User.deserialize, if_undefined=entities.Unset, default=entities.UNSET ) #: The member properties for the message's author. #: #: :type: :obj:`typing.Union` [ :obj:`hikari.guilds.GuildMember`, :obj:`hikari.entities.UNSET` ] member: typing.Union[guilds.GuildMember, entities.Unset] = marshaller.attrib( - deserializer=guilds.GuildMember.deserialize, if_undefined=entities.Unset + deserializer=guilds.GuildMember.deserialize, if_undefined=entities.Unset, default=entities.UNSET ) #: The content of the message. #: #: :type: :obj:`typing.Union` [ :obj:`str`, :obj:`hikari.entities.UNSET` ] - content: typing.Union[str, entities.Unset] = marshaller.attrib(deserializer=str, if_undefined=entities.Unset) + content: typing.Union[str, entities.Unset] = marshaller.attrib( + deserializer=str, if_undefined=entities.Unset, default=entities.UNSET + ) #: The timestamp that the message was sent at. #: #: :type: :obj:`typing.Union` [ :obj:`datetime.datetime`, :obj:`hikari.entities.UNSET` ] timestamp: typing.Union[datetime.datetime, entities.Unset] = marshaller.attrib( - deserializer=conversions.parse_iso_8601_ts, if_undefined=entities.Unset + deserializer=conversions.parse_iso_8601_ts, if_undefined=entities.Unset, default=entities.UNSET ) #: The timestamp that the message was last edited at, or :obj:`None` if @@ -773,21 +782,21 @@ class MessageUpdateEvent(HikariEvent, snowflakes.UniqueEntity, entities.Deserial #: #: :type: :obj:`typing.Union` [ :obj:`datetime.datetime`, :obj:`hikari.entities.UNSET` ], optional edited_timestamp: typing.Union[datetime.datetime, entities.Unset, None] = marshaller.attrib( - deserializer=conversions.parse_iso_8601_ts, if_none=None, if_undefined=entities.Unset + deserializer=conversions.parse_iso_8601_ts, if_none=None, if_undefined=entities.Unset, default=entities.UNSET ) #: Whether the message is a TTS message. #: #: :type: :obj:`typing.Union` [ :obj:`bool`, :obj:`hikari.entities.UNSET` ] is_tts: typing.Union[bool, entities.Unset] = marshaller.attrib( - raw_name="tts", deserializer=bool, if_undefined=entities.Unset + raw_name="tts", deserializer=bool, if_undefined=entities.Unset, default=entities.UNSET ) #: Whether the message mentions ``@everyone`` or ``@here``. #: #: :type: :obj:`typing.Union` [ :obj:`bool`, :obj:`hikari.entities.UNSET` ] is_mentioning_everyone: typing.Union[bool, entities.Unset] = marshaller.attrib( - raw_name="mention_everyone", deserializer=bool, if_undefined=entities.Unset + raw_name="mention_everyone", deserializer=bool, if_undefined=entities.Unset, default=entities.UNSET ) #: The users the message mentions. @@ -797,6 +806,7 @@ class MessageUpdateEvent(HikariEvent, snowflakes.UniqueEntity, entities.Deserial raw_name="mentions", deserializer=lambda user_mentions: {snowflakes.Snowflake.deserialize(u["id"]) for u in user_mentions}, if_undefined=entities.Unset, + default=entities.UNSET, ) #: The roles the message mentions. @@ -806,6 +816,7 @@ class MessageUpdateEvent(HikariEvent, snowflakes.UniqueEntity, entities.Deserial raw_name="mention_roles", deserializer=lambda role_mentions: {snowflakes.Snowflake.deserialize(r) for r in role_mentions}, if_undefined=entities.Unset, + default=entities.UNSET, ) #: The channels the message mentions. @@ -815,6 +826,7 @@ class MessageUpdateEvent(HikariEvent, snowflakes.UniqueEntity, entities.Deserial raw_name="mention_channels", deserializer=lambda channel_mentions: {snowflakes.Snowflake.deserialize(c["id"]) for c in channel_mentions}, if_undefined=entities.Unset, + default=entities.UNSET, ) #: The message attachments. @@ -823,13 +835,16 @@ class MessageUpdateEvent(HikariEvent, snowflakes.UniqueEntity, entities.Deserial attachments: typing.Union[typing.Sequence[messages.Attachment], entities.Unset] = marshaller.attrib( deserializer=lambda attachments: [messages.Attachment.deserialize(a) for a in attachments], if_undefined=entities.Unset, + default=entities.UNSET, ) #: The message's embeds. #: #: :type: :obj:`typing.Union` [ :obj:`typing.Sequence` [ :obj:`hikari.embeds.Embed` ], :obj:`hikari.entities.UNSET` ] embeds: typing.Union[typing.Sequence[_embeds.Embed], entities.Unset] = marshaller.attrib( - deserializer=lambda embed_objs: [_embeds.Embed.deserialize(e) for e in embed_objs], if_undefined=entities.Unset, + deserializer=lambda embed_objs: [_embeds.Embed.deserialize(e) for e in embed_objs], + if_undefined=entities.Unset, + default=entities.UNSET, ) #: The message's reactions. @@ -838,66 +853,69 @@ class MessageUpdateEvent(HikariEvent, snowflakes.UniqueEntity, entities.Deserial reactions: typing.Union[typing.Sequence[messages.Reaction], entities.Unset] = marshaller.attrib( deserializer=lambda reactions: [messages.Reaction.deserialize(r) for r in reactions], if_undefined=entities.Unset, + default=entities.UNSET, ) #: Whether the message is pinned. #: #: :type: :obj:`typing.Union` [ :obj:`bool`, :obj:`hikari.entities.UNSET` ] is_pinned: typing.Union[bool, entities.Unset] = marshaller.attrib( - raw_name="pinned", deserializer=bool, if_undefined=entities.Unset + raw_name="pinned", deserializer=bool, if_undefined=entities.Unset, default=entities.UNSET ) #: If the message was generated by a webhook, the webhook's id. #: #: :type: :obj:`typing.Union` [ :obj:`hikari.snowflakes.Snowflake`, :obj:`hikari.entities.UNSET` ] webhook_id: typing.Union[snowflakes.Snowflake, entities.Unset] = marshaller.attrib( - deserializer=snowflakes.Snowflake.deserialize, if_undefined=entities.Unset + deserializer=snowflakes.Snowflake.deserialize, if_undefined=entities.Unset, default=entities.UNSET ) #: The message's type. #: #: :type: :obj:`typing.Union` [ :obj:`hikari.messages.MessageType`, :obj:`hikari.entities.UNSET` ] type: typing.Union[messages.MessageType, entities.Unset] = marshaller.attrib( - deserializer=messages.MessageType, if_undefined=entities.Unset + deserializer=messages.MessageType, if_undefined=entities.Unset, default=entities.UNSET ) #: The message's activity. #: #: :type: :obj:`typing.Union` [ :obj:`hikari.messages.MessageActivity`, :obj:`hikari.entities.UNSET` ] activity: typing.Union[messages.MessageActivity, entities.Unset] = marshaller.attrib( - deserializer=messages.MessageActivity.deserialize, if_undefined=entities.Unset + deserializer=messages.MessageActivity.deserialize, if_undefined=entities.Unset, default=entities.UNSET ) #: The message's application. #: #: :type: :obj:`typing.Union` [ :obj:`hikari.oauth2.Application`, :obj:`hikari.entities.UNSET` ] application: typing.Optional[oauth2.Application] = marshaller.attrib( - deserializer=oauth2.Application.deserialize, if_undefined=entities.Unset + deserializer=oauth2.Application.deserialize, if_undefined=entities.Unset, default=entities.UNSET ) #: The message's crossposted reference data. #: #: :type: :obj:`typing.Union` [ :obj:`hikari.messages.MessageCrosspost`, :obj:`hikari.entities.UNSET` ] message_reference: typing.Union[messages.MessageCrosspost, entities.Unset] = marshaller.attrib( - deserializer=messages.MessageCrosspost.deserialize, if_undefined=entities.Unset + deserializer=messages.MessageCrosspost.deserialize, if_undefined=entities.Unset, default=entities.UNSET ) #: The message's flags. #: #: :type: :obj:`typing.Union` [ :obj:`hikari.messages.MessageFlag`, :obj:`hikari.entities.UNSET` ] flags: typing.Union[messages.MessageFlag, entities.Unset] = marshaller.attrib( - deserializer=messages.MessageFlag, if_undefined=entities.Unset + deserializer=messages.MessageFlag, if_undefined=entities.Unset, default=entities.UNSET ) #: The message nonce. This is a string used for validating #: a message was sent. #: #: :type: :obj:`typing.Union` [ :obj:`hikari.messages.MessageFlag`, :obj:`hikari.entities.UNSET` ] - nonce: typing.Union[str, entities.Unset] = marshaller.attrib(deserializer=str, if_undefined=entities.Unset) + nonce: typing.Union[str, entities.Unset] = marshaller.attrib( + deserializer=str, if_undefined=entities.Unset, default=entities.UNSET + ) @marshaller.marshallable() -@attr.s(slots=True) +@attr.s(slots=True, kw_only=True) class MessageDeleteEvent(HikariEvent, entities.Deserializable): """Used to represent Message Delete gateway events. @@ -914,7 +932,7 @@ class MessageDeleteEvent(HikariEvent, entities.Deserializable): #: #: :type: :obj:`hikari.snowflakes.Snowflake`, optional guild_id: typing.Optional[snowflakes.Snowflake] = marshaller.attrib( - deserializer=snowflakes.Snowflake.deserialize, if_undefined=None + deserializer=snowflakes.Snowflake.deserialize, if_undefined=None, default=None ) #: The ID of the message that was deleted. #: @@ -923,7 +941,7 @@ class MessageDeleteEvent(HikariEvent, entities.Deserializable): @marshaller.marshallable() -@attr.s(slots=True) +@attr.s(slots=True, kw_only=True) class MessageDeleteBulkEvent(HikariEvent, entities.Deserializable): """Used to represent Message Bulk Delete gateway events. @@ -952,7 +970,7 @@ class MessageDeleteBulkEvent(HikariEvent, entities.Deserializable): @marshaller.marshallable() -@attr.s(slots=True) +@attr.s(slots=True, kw_only=True) class MessageReactionAddEvent(HikariEvent, entities.Deserializable): """Used to represent Message Reaction Add gateway events.""" @@ -976,7 +994,7 @@ class MessageReactionAddEvent(HikariEvent, entities.Deserializable): #: #: :type: :obj:`hikari.snowflakes.Snowflake`, optional guild_id: typing.Optional[snowflakes.Snowflake] = marshaller.attrib( - deserializer=snowflakes.Snowflake.deserialize, if_undefined=None + deserializer=snowflakes.Snowflake.deserialize, if_undefined=None, default=None ) #: The member object of the user who's adding this reaction, if this is @@ -984,7 +1002,7 @@ class MessageReactionAddEvent(HikariEvent, entities.Deserializable): #: #: :type: :obj:`hikari.guilds.GuildMember`, optional member: typing.Optional[guilds.GuildMember] = marshaller.attrib( - deserializer=guilds.GuildMember.deserialize, if_undefined=None + deserializer=guilds.GuildMember.deserialize, if_undefined=None, default=None ) #: The object of the emoji being added. @@ -996,7 +1014,7 @@ class MessageReactionAddEvent(HikariEvent, entities.Deserializable): @marshaller.marshallable() -@attr.s(slots=True) +@attr.s(slots=True, kw_only=True) class MessageReactionRemoveEvent(HikariEvent, entities.Deserializable): """Used to represent Message Reaction Remove gateway events.""" @@ -1020,7 +1038,7 @@ class MessageReactionRemoveEvent(HikariEvent, entities.Deserializable): #: #: :type: :obj:`hikari.snowflakes.Snowflake`, optional guild_id: typing.Optional[snowflakes.Snowflake] = marshaller.attrib( - deserializer=snowflakes.Snowflake.deserialize, if_undefined=None + deserializer=snowflakes.Snowflake.deserialize, if_undefined=None, default=None ) #: The object of the emoji being removed. @@ -1032,7 +1050,7 @@ class MessageReactionRemoveEvent(HikariEvent, entities.Deserializable): @marshaller.marshallable() -@attr.s(slots=True) +@attr.s(slots=True, kw_only=True) class MessageReactionRemoveAllEvent(HikariEvent, entities.Deserializable): """Used to represent Message Reaction Remove All gateway events. @@ -1053,12 +1071,12 @@ class MessageReactionRemoveAllEvent(HikariEvent, entities.Deserializable): #: #: :type: :obj:`hikari.snowflakes.Snowflake` guild_id: typing.Optional[snowflakes.Snowflake] = marshaller.attrib( - deserializer=snowflakes.Snowflake.deserialize, if_undefined=None + deserializer=snowflakes.Snowflake.deserialize, if_undefined=None, default=None ) @marshaller.marshallable() -@attr.s(slots=True) +@attr.s(slots=True, kw_only=True) class MessageReactionRemoveEmojiEvent(HikariEvent, entities.Deserializable): """Represents Message Reaction Remove Emoji events. @@ -1074,7 +1092,7 @@ class MessageReactionRemoveEmojiEvent(HikariEvent, entities.Deserializable): #: #: :type: :obj:`hikari.snowflakes.Snowflake` guild_id: typing.Optional[snowflakes.Snowflake] = marshaller.attrib( - deserializer=snowflakes.Snowflake.deserialize, if_undefined=None + deserializer=snowflakes.Snowflake.deserialize, if_undefined=None, default=None ) #: The ID of the message the reactions are being removed from. @@ -1091,7 +1109,7 @@ class MessageReactionRemoveEmojiEvent(HikariEvent, entities.Deserializable): @marshaller.marshallable() -@attr.s(slots=True) +@attr.s(slots=True, kw_only=True) class PresenceUpdateEvent(HikariEvent, guilds.GuildMemberPresence): """Used to represent Presence Update gateway events. @@ -1100,7 +1118,7 @@ class PresenceUpdateEvent(HikariEvent, guilds.GuildMemberPresence): @marshaller.marshallable() -@attr.s(slots=True) +@attr.s(slots=True, kw_only=True) class TypingStartEvent(HikariEvent, entities.Deserializable): """Used to represent typing start gateway events. @@ -1117,7 +1135,7 @@ class TypingStartEvent(HikariEvent, entities.Deserializable): #: #: :type: :obj:`hikari.snowflakes.Snowflake`, optional guild_id: typing.Optional[snowflakes.Snowflake] = marshaller.attrib( - deserializer=snowflakes.Snowflake.deserialize, if_undefined=None + deserializer=snowflakes.Snowflake.deserialize, if_undefined=None, default=None ) #: The ID of the user who triggered this typing event. @@ -1137,12 +1155,12 @@ class TypingStartEvent(HikariEvent, entities.Deserializable): #: #: :type: :obj:`hikari.guilds.GuildMember`, optional member: typing.Optional[guilds.GuildMember] = marshaller.attrib( - deserializer=guilds.GuildMember.deserialize, if_undefined=None + deserializer=guilds.GuildMember.deserialize, if_undefined=None, default=None ) @marshaller.marshallable() -@attr.s(slots=True) +@attr.s(slots=True, kw_only=True) class UserUpdateEvent(HikariEvent, users.MyUser): """Used to represent User Update gateway events. @@ -1151,7 +1169,7 @@ class UserUpdateEvent(HikariEvent, users.MyUser): @marshaller.marshallable() -@attr.s(slots=True) +@attr.s(slots=True, kw_only=True) class VoiceStateUpdateEvent(HikariEvent, voices.VoiceState): """Used to represent voice state update gateway events. @@ -1160,7 +1178,7 @@ class VoiceStateUpdateEvent(HikariEvent, voices.VoiceState): @marshaller.marshallable() -@attr.s(slots=True) +@attr.s(slots=True, kw_only=True) class VoiceServerUpdateEvent(HikariEvent, entities.Deserializable): """Used to represent voice server update gateway events. @@ -1185,7 +1203,7 @@ class VoiceServerUpdateEvent(HikariEvent, entities.Deserializable): @marshaller.marshallable() -@attr.s(slots=True) +@attr.s(slots=True, kw_only=True) class WebhookUpdateEvent(HikariEvent, entities.Deserializable): """Used to represent webhook update gateway events. diff --git a/hikari/gateway_entities.py b/hikari/gateway_entities.py index 81092aab72..d2cdf8c119 100644 --- a/hikari/gateway_entities.py +++ b/hikari/gateway_entities.py @@ -30,7 +30,7 @@ @marshaller.marshallable() -@attr.s(slots=True) +@attr.s(slots=True, kw_only=True) class SessionStartLimit(entities.HikariEntity, entities.Deserializable): """Used to represent information about the current session start limits.""" @@ -54,7 +54,7 @@ class SessionStartLimit(entities.HikariEntity, entities.Deserializable): @marshaller.marshallable() -@attr.s(slots=True) +@attr.s(slots=True, kw_only=True) class GatewayBot(entities.HikariEntity, entities.Deserializable): """Used to represent gateway information for the connected bot.""" @@ -75,7 +75,7 @@ class GatewayBot(entities.HikariEntity, entities.Deserializable): @marshaller.marshallable() -@attr.s(slots=True) +@attr.s(slots=True, kw_only=True) class GatewayActivity(entities.Deserializable, entities.Serializable): """An activity that the bot can set for one or more shards. diff --git a/hikari/guilds.py b/hikari/guilds.py index 2df71f6a96..e6e2cdeb54 100644 --- a/hikari/guilds.py +++ b/hikari/guilds.py @@ -197,7 +197,7 @@ class GuildVerificationLevel(enum.IntEnum): @marshaller.marshallable() -@attr.s(slots=True) +@attr.s(slots=True, kw_only=True) class GuildEmbed(entities.HikariEntity, entities.Deserializable): """Represents a guild embed.""" @@ -215,7 +215,7 @@ class GuildEmbed(entities.HikariEntity, entities.Deserializable): @marshaller.marshallable() -@attr.s(slots=True) +@attr.s(slots=True, kw_only=True) class GuildMember(entities.HikariEntity, entities.Deserializable): """Used to represent a guild bound member.""" @@ -223,13 +223,15 @@ class GuildMember(entities.HikariEntity, entities.Deserializable): #: Create and Update gateway events. #: #: :type: :obj:`hikari.users.User`, optional - user: typing.Optional[users.User] = marshaller.attrib(deserializer=users.User.deserialize, if_undefined=None) + user: typing.Optional[users.User] = marshaller.attrib( + deserializer=users.User.deserialize, if_undefined=None, default=None + ) #: This member's nickname, if set. #: #: :type: :obj:`str`, optional nickname: typing.Optional[str] = marshaller.attrib( - raw_name="nick", deserializer=str, if_none=None, if_undefined=None, + raw_name="nick", deserializer=str, if_none=None, if_undefined=None, default=None ) #: A sequence of the IDs of the member's current roles. @@ -249,7 +251,7 @@ class GuildMember(entities.HikariEntity, entities.Deserializable): #: #: :type: :obj:`datetime.datetime`, optional premium_since: typing.Optional[datetime.datetime] = marshaller.attrib( - deserializer=conversions.parse_iso_8601_ts, if_none=None, if_undefined=None, + deserializer=conversions.parse_iso_8601_ts, if_none=None, if_undefined=None, default=None ) #: Whether this member is deafened by this guild in it's voice channels. @@ -264,7 +266,7 @@ class GuildMember(entities.HikariEntity, entities.Deserializable): @marshaller.marshallable() -@attr.s(slots=True) +@attr.s(slots=True, kw_only=True) class PartialGuildRole(snowflakes.UniqueEntity, entities.Deserializable): """Represents a partial guild bound Role object.""" @@ -275,7 +277,7 @@ class PartialGuildRole(snowflakes.UniqueEntity, entities.Deserializable): @marshaller.marshallable() -@attr.s(slots=True) +@attr.s(slots=True, kw_only=True) class GuildRole(PartialGuildRole, entities.Serializable): """Represents a guild bound Role object.""" @@ -347,7 +349,7 @@ class ActivityType(enum.IntEnum): @marshaller.marshallable() -@attr.s(slots=True) +@attr.s(slots=True, kw_only=True) class ActivityTimestamps(entities.HikariEntity, entities.Deserializable): """The datetimes for the start and/or end of an activity session.""" @@ -355,32 +357,32 @@ class ActivityTimestamps(entities.HikariEntity, entities.Deserializable): #: #: :type: :obj:`datetime.datetime`, optional start: typing.Optional[datetime.datetime] = marshaller.attrib( - deserializer=conversions.unix_epoch_to_datetime, if_undefined=None + deserializer=conversions.unix_epoch_to_datetime, if_undefined=None, default=None ) #: When this activity's session will end, if applicable. #: #: :type: :obj:`datetime.datetime`, optional end: typing.Optional[datetime.datetime] = marshaller.attrib( - deserializer=conversions.unix_epoch_to_datetime, if_undefined=None + deserializer=conversions.unix_epoch_to_datetime, if_undefined=None, default=None ) @marshaller.marshallable() -@attr.s(slots=True) +@attr.s(slots=True, kw_only=True) class ActivityParty(entities.HikariEntity, entities.Deserializable): """Used to represent activity groups of users.""" #: The string id of this party instance, if set. #: #: :type: :obj:`str`, optional - id: typing.Optional[str] = marshaller.attrib(deserializer=str, if_undefined=None) + id: typing.Optional[str] = marshaller.attrib(deserializer=str, if_undefined=None, default=None) #: The size metadata of this party, if applicable. #: #: :type: :obj:`typing.Tuple` [ :obj:`int`, :obj:`int` ], optional _size_information: typing.Optional[typing.Tuple[int, int]] = marshaller.attrib( - raw_name="size", deserializer=tuple, if_undefined=None, + raw_name="size", deserializer=tuple, if_undefined=None, default=None ) # Ignore docstring not starting in an imperative mood @@ -396,50 +398,50 @@ def max_size(self) -> typing.Optional[int]: @marshaller.marshallable() -@attr.s(slots=True) +@attr.s(slots=True, kw_only=True) class ActivityAssets(entities.HikariEntity, entities.Deserializable): """Used to represent possible assets for an activity.""" #: The ID of the asset's large image, if set. #: #: :type: :obj:`str`, optional - large_image: typing.Optional[str] = marshaller.attrib(deserializer=str, if_undefined=None) + large_image: typing.Optional[str] = marshaller.attrib(deserializer=str, if_undefined=None, default=None) #: The text that'll appear when hovering over the large image, if set. #: #: :type: :obj:`str`, optional - large_text: typing.Optional[str] = marshaller.attrib(deserializer=str, if_undefined=None) + large_text: typing.Optional[str] = marshaller.attrib(deserializer=str, if_undefined=None, default=None) #: The ID of the asset's small image, if set. #: #: :type: :obj:`str`, optional - small_image: typing.Optional[str] = marshaller.attrib(deserializer=str, if_undefined=None) + small_image: typing.Optional[str] = marshaller.attrib(deserializer=str, if_undefined=None, default=None) #: The text that'll appear when hovering over the small image, if set. #: #: :type: :obj:`str`, optional - small_text: typing.Optional[str] = marshaller.attrib(deserializer=str, if_undefined=None) + small_text: typing.Optional[str] = marshaller.attrib(deserializer=str, if_undefined=None, default=None) @marshaller.marshallable() -@attr.s(slots=True) +@attr.s(slots=True, kw_only=True) class ActivitySecret(entities.HikariEntity, entities.Deserializable): """The secrets used for interacting with an activity party.""" #: The secret used for joining a party, if applicable. #: #: :type: :obj:`str`, optional - join: typing.Optional[str] = marshaller.attrib(deserializer=str, if_undefined=None) + join: typing.Optional[str] = marshaller.attrib(deserializer=str, if_undefined=None, default=None) #: The secret used for spectating a party, if applicable. #: #: :type: :obj:`str`, optional - spectate: typing.Optional[str] = marshaller.attrib(deserializer=str, if_undefined=None) + spectate: typing.Optional[str] = marshaller.attrib(deserializer=str, if_undefined=None, default=None) #: The secret used for joining a party, if applicable. #: #: :type: :obj:`str`, optional - match: typing.Optional[str] = marshaller.attrib(deserializer=str, if_undefined=None) + match: typing.Optional[str] = marshaller.attrib(deserializer=str, if_undefined=None, default=None) @enum.unique @@ -469,7 +471,7 @@ class ActivityFlag(enum.IntFlag): @marshaller.marshallable() -@attr.s(slots=True) +@attr.s(slots=True, kw_only=True) class PresenceActivity(entities.HikariEntity, entities.Deserializable): """Represents an activity that will be attached to a member's presence.""" @@ -486,7 +488,7 @@ class PresenceActivity(entities.HikariEntity, entities.Deserializable): #: The URL for a ``STREAM`` type activity, if applicable. #: #: :type: :obj:`str`, optional - url: typing.Optional[str] = marshaller.attrib(deserializer=str, if_undefined=None, if_none=None) + url: typing.Optional[str] = marshaller.attrib(deserializer=str, if_undefined=None, if_none=None, default=None) #: When this activity was added to the user's session. #: @@ -498,61 +500,65 @@ class PresenceActivity(entities.HikariEntity, entities.Deserializable): #: #: :type: :obj:`ActivityTimestamps`, optional timestamps: typing.Optional[ActivityTimestamps] = marshaller.attrib( - deserializer=ActivityTimestamps.deserialize, if_undefined=None + deserializer=ActivityTimestamps.deserialize, if_undefined=None, default=None ) #: The ID of the application this activity is for, if applicable. #: #: :type: :obj:`hikari.snowflakes.Snowflake`, optional application_id: typing.Optional[snowflakes.Snowflake] = marshaller.attrib( - deserializer=snowflakes.Snowflake.deserialize, if_undefined=None + deserializer=snowflakes.Snowflake.deserialize, if_undefined=None, default=None ) #: The text that describes what the activity's target is doing, if set. #: #: :type: :obj:`str`, optional - details: typing.Optional[str] = marshaller.attrib(deserializer=str, if_undefined=None, if_none=None) + details: typing.Optional[str] = marshaller.attrib(deserializer=str, if_undefined=None, if_none=None, default=None) #: The current status of this activity's target, if set. #: #: :type: :obj:`str`, optional - state: typing.Optional[str] = marshaller.attrib(deserializer=str, if_undefined=None, if_none=None) + state: typing.Optional[str] = marshaller.attrib(deserializer=str, if_undefined=None, if_none=None, default=None) #: The emoji of this activity, if it is a custom status and set. #: #: :type: :obj:`typing.Union` [ :obj:`hikari.emojis.UnicodeEmoji`, :obj:`hikari.emojis.UnknownEmoji` ], optional emoji: typing.Union[None, _emojis.UnicodeEmoji, _emojis.UnknownEmoji] = marshaller.attrib( - deserializer=_emojis.deserialize_reaction_emoji, if_undefined=None + deserializer=_emojis.deserialize_reaction_emoji, if_undefined=None, default=None ) #: Information about the party associated with this activity, if set. #: #: :type: :obj:`ActivityParty`, optional - party: typing.Optional[ActivityParty] = marshaller.attrib(deserializer=ActivityParty.deserialize, if_undefined=None) + party: typing.Optional[ActivityParty] = marshaller.attrib( + deserializer=ActivityParty.deserialize, if_undefined=None, default=None + ) #: Images and their hover over text for the activity. #: #: :type: :obj:`ActivityAssets`, optional assets: typing.Optional[ActivityAssets] = marshaller.attrib( - deserializer=ActivityAssets.deserialize, if_undefined=None + deserializer=ActivityAssets.deserialize, if_undefined=None, default=None ) #: Secrets for Rich Presence joining and spectating. #: #: :type: :obj:`ActivitySecret`, optional secrets: typing.Optional[ActivitySecret] = marshaller.attrib( - deserializer=ActivitySecret.deserialize, if_undefined=None + deserializer=ActivitySecret.deserialize, if_undefined=None, default=None ) #: Whether this activity is an instanced game session. #: #: :type: :obj:`bool`, optional - is_instance: typing.Optional[bool] = marshaller.attrib(raw_name="instance", deserializer=bool, if_undefined=None) + is_instance: typing.Optional[bool] = marshaller.attrib( + raw_name="instance", deserializer=bool, if_undefined=None, default=None + ) #: Flags that describe what the activity includes. #: #: :type: :obj:`ActivityFlag` - flags: ActivityFlag = marshaller.attrib(deserializer=ActivityFlag, if_undefined=None) + flags: ActivityFlag = marshaller.attrib(deserializer=ActivityFlag, if_undefined=None, default=None) class PresenceStatus(str, enum.Enum): @@ -575,7 +581,7 @@ class PresenceStatus(str, enum.Enum): @marshaller.marshallable() -@attr.s(slots=True) +@attr.s(slots=True, kw_only=True) class ClientStatus(entities.HikariEntity, entities.Deserializable): """The client statuses for this member.""" @@ -583,22 +589,26 @@ class ClientStatus(entities.HikariEntity, entities.Deserializable): #: #: :type: :obj:`PresenceStatus` desktop: PresenceStatus = marshaller.attrib( - deserializer=PresenceStatus, if_undefined=lambda: PresenceStatus.OFFLINE, + deserializer=PresenceStatus, if_undefined=lambda: PresenceStatus.OFFLINE, default=PresenceStatus.OFFLINE ) #: The status of the target user's mobile session. #: #: :type: :obj:`PresenceStatus` - mobile: PresenceStatus = marshaller.attrib(deserializer=PresenceStatus, if_undefined=lambda: PresenceStatus.OFFLINE) + mobile: PresenceStatus = marshaller.attrib( + deserializer=PresenceStatus, if_undefined=lambda: PresenceStatus.OFFLINE, default=PresenceStatus.OFFLINE + ) #: The status of the target user's web session. #: #: :type: :obj:`PresenceStatus` - web: PresenceStatus = marshaller.attrib(deserializer=PresenceStatus, if_undefined=lambda: PresenceStatus.OFFLINE) + web: PresenceStatus = marshaller.attrib( + deserializer=PresenceStatus, if_undefined=lambda: PresenceStatus.OFFLINE, default=PresenceStatus.OFFLINE + ) @marshaller.marshallable() -@attr.s(slots=True) +@attr.s(slots=True, kw_only=True) class PresenceUser(users.User): """A user representation specifically used for presence updates. @@ -611,32 +621,36 @@ class PresenceUser(users.User): #: This user's discriminator. #: #: :type: :obj:`typing.Union` [ :obj:`str`, :obj:`hikari.entities.UNSET` ] - discriminator: typing.Union[str, entities.Unset] = marshaller.attrib(deserializer=str, if_undefined=entities.Unset) + discriminator: typing.Union[str, entities.Unset] = marshaller.attrib( + deserializer=str, if_undefined=entities.Unset, default=entities.UNSET + ) #: This user's username. #: #: :type: :obj:`typing.Union` [ :obj:`str`, :obj:`hikari.entities.UNSET` ] - username: typing.Union[str, entities.Unset] = marshaller.attrib(deserializer=str, if_undefined=entities.Unset) + username: typing.Union[str, entities.Unset] = marshaller.attrib( + deserializer=str, if_undefined=entities.Unset, default=entities.UNSET + ) #: This user's avatar hash, if set. #: #: :type: :obj:`typing.Union` [ :obj:`str`, :obj:`hikari.entities.UNSET` ], optional avatar_hash: typing.Union[None, str, entities.Unset] = marshaller.attrib( - raw_name="avatar", deserializer=str, if_none=None, if_undefined=entities.Unset + raw_name="avatar", deserializer=str, if_none=None, if_undefined=entities.Unset, default=entities.UNSET ) #: Whether this user is a bot account. #: #: :type: :obj:`typing.Union` [ :obj:`bool`, :obj:`hikari.entities.UNSET` ] is_bot: typing.Union[bool, entities.Unset] = marshaller.attrib( - raw_name="bot", deserializer=bool, if_undefined=entities.Unset + raw_name="bot", deserializer=bool, if_undefined=entities.Unset, default=entities.UNSET ) #: Whether this user is a system account. #: #: :type: :obj:`typing.Union` [ :obj:`bool`, :obj:`hikari.entities.UNSET` ] is_system: typing.Union[bool, entities.Unset] = marshaller.attrib( - raw_name="system", deserializer=bool, if_undefined=entities.Unset, + raw_name="system", deserializer=bool, if_undefined=entities.Unset, default=entities.UNSET ) #: The public flags for this user. @@ -705,7 +719,7 @@ def default_avatar(self) -> typing.Union[int, entities.Unset]: @marshaller.marshallable() -@attr.s(slots=True) +@attr.s(slots=True, kw_only=True) class GuildMemberPresence(entities.HikariEntity, entities.Deserializable): """Used to represent a guild member's presence.""" @@ -752,13 +766,15 @@ class GuildMemberPresence(entities.HikariEntity, entities.Deserializable): #: #: :type: :obj:`datetime.datetime`, optional premium_since: typing.Optional[datetime.datetime] = marshaller.attrib( - deserializer=conversions.parse_iso_8601_ts, if_none=None, if_undefined=None, + deserializer=conversions.parse_iso_8601_ts, if_undefined=None, if_none=None, default=None ) #: This member's nickname, if set. #: #: :type: :obj:`str`, optional - nick: typing.Optional[str] = marshaller.attrib(raw_name="nick", deserializer=str, if_undefined=None, if_none=None) + nick: typing.Optional[str] = marshaller.attrib( + raw_name="nick", deserializer=str, if_undefined=None, if_none=None, default=None + ) @enum.unique @@ -773,7 +789,7 @@ class IntegrationExpireBehaviour(enum.IntEnum): @marshaller.marshallable() -@attr.s(slots=True) +@attr.s(slots=True, kw_only=True) class IntegrationAccount(entities.HikariEntity, entities.Deserializable): """An account that's linked to an integration.""" @@ -789,7 +805,7 @@ class IntegrationAccount(entities.HikariEntity, entities.Deserializable): @marshaller.marshallable() -@attr.s(slots=True) +@attr.s(slots=True, kw_only=True) class PartialGuildIntegration(snowflakes.UniqueEntity, entities.Deserializable): """A partial representation of an integration, found in audit logs.""" @@ -810,7 +826,7 @@ class PartialGuildIntegration(snowflakes.UniqueEntity, entities.Deserializable): @marshaller.marshallable() -@attr.s(slots=True) +@attr.s(slots=True, kw_only=True) class GuildIntegration(snowflakes.UniqueEntity, entities.Deserializable): """Represents a guild integration object.""" @@ -834,7 +850,7 @@ class GuildIntegration(snowflakes.UniqueEntity, entities.Deserializable): #: #: :type: :obj:`bool`, optional is_emojis_enabled: typing.Optional[bool] = marshaller.attrib( - raw_name="enable_emoticons", deserializer=bool, if_undefined=None, + raw_name="enable_emoticons", deserializer=bool, if_undefined=None, default=None ) #: How members should be treated after their connected subscription expires @@ -865,7 +881,7 @@ class GuildIntegration(snowflakes.UniqueEntity, entities.Deserializable): @marshaller.marshallable() -@attr.s(slots=True) +@attr.s(slots=True, kw_only=True) class GuildMemberBan(entities.HikariEntity, entities.Deserializable): """Used to represent guild bans.""" @@ -881,7 +897,7 @@ class GuildMemberBan(entities.HikariEntity, entities.Deserializable): @marshaller.marshallable() -@attr.s(slots=True) +@attr.s(slots=True, kw_only=True) class UnavailableGuild(snowflakes.UniqueEntity, entities.Deserializable): """An unavailable guild object, received during gateway events such as READY. @@ -900,7 +916,7 @@ def is_unavailable(self) -> bool: # noqa: D401 @marshaller.marshallable() -@attr.s(slots=True) +@attr.s(slots=True, kw_only=True) class PartialGuild(snowflakes.UniqueEntity, entities.Deserializable): """Base object for any partial guild objects.""" @@ -959,7 +975,7 @@ def icon_url(self) -> typing.Optional[str]: @marshaller.marshallable() -@attr.s(slots=True) +@attr.s(slots=True, kw_only=True) class GuildPreview(PartialGuild): """A preview of a guild with the :obj:`GuildFeature.PUBLIC` feature.""" @@ -1066,7 +1082,7 @@ def discovery_splash_url(self) -> typing.Optional[str]: @marshaller.marshallable() -@attr.s(slots=True) +@attr.s(slots=True, kw_only=True) class Guild(PartialGuild): """A representation of a guild on Discord. @@ -1101,7 +1117,7 @@ class Guild(PartialGuild): #: #: :type: :obj:`hikari.permissions.Permission` my_permissions: _permissions.Permission = marshaller.attrib( - raw_name="permissions", deserializer=_permissions.Permission, if_undefined=None + raw_name="permissions", deserializer=_permissions.Permission, if_undefined=None, default=None ) #: The voice region for the guild. @@ -1133,7 +1149,7 @@ class Guild(PartialGuild): #: #: :type: :obj:`bool`, optional is_embed_enabled: typing.Optional[bool] = marshaller.attrib( - raw_name="embed_enabled", if_undefined=False, deserializer=bool + raw_name="embed_enabled", deserializer=bool, if_undefined=False, default=False ) #: The channel ID that the guild embed will generate an invite to, if @@ -1143,7 +1159,7 @@ class Guild(PartialGuild): #: #: :type: :obj:`hikari.snowflakes.Snowflake`, optional embed_channel_id: typing.Optional[snowflakes.Snowflake] = marshaller.attrib( - deserializer=snowflakes.Snowflake.deserialize, if_none=None, if_undefined=None + deserializer=snowflakes.Snowflake.deserialize, if_undefined=None, if_none=None, default=None ) #: The verification level required for a user to participate in this guild. @@ -1204,7 +1220,7 @@ class Guild(PartialGuild): #: #: :type: :obj:`bool`, optional is_unavailable: typing.Optional[bool] = marshaller.attrib( - raw_name="unavailable", if_undefined=None, deserializer=bool + raw_name="unavailable", deserializer=bool, if_undefined=None, default=None ) # TODO: document in which cases this information is not available. @@ -1213,7 +1229,7 @@ class Guild(PartialGuild): #: #: :type: :obj:`bool`, optional is_widget_enabled: typing.Optional[bool] = marshaller.attrib( - raw_name="widget_enabled", if_undefined=None, deserializer=bool + raw_name="widget_enabled", deserializer=bool, if_undefined=None, default=None ) #: The channel ID that the widget's generated invite will send the user to, @@ -1221,7 +1237,7 @@ class Guild(PartialGuild): #: #: :type: :obj:`hikari.snowflakes.Snowflake`, optional widget_channel_id: typing.Optional[snowflakes.Snowflake] = marshaller.attrib( - if_undefined=None, if_none=None, deserializer=snowflakes.Snowflake.deserialize + deserializer=snowflakes.Snowflake.deserialize, if_undefined=None, if_none=None, default=None ) #: The ID of the system channel (where welcome messages and Nitro boost @@ -1257,7 +1273,7 @@ class Guild(PartialGuild): #: #: :type: :obj:`datetime.datetime`, optional joined_at: typing.Optional[datetime.datetime] = marshaller.attrib( - deserializer=conversions.parse_iso_8601_ts, if_undefined=None + deserializer=conversions.parse_iso_8601_ts, if_undefined=None, default=None ) #: Whether the guild is considered to be large or not. @@ -1270,7 +1286,9 @@ class Guild(PartialGuild): #: not be sent about members who are offline or invisible. #: #: :type: :obj:`bool`, optional - is_large: typing.Optional[bool] = marshaller.attrib(raw_name="large", if_undefined=None, deserializer=bool) + is_large: typing.Optional[bool] = marshaller.attrib( + raw_name="large", deserializer=bool, if_undefined=None, default=None + ) #: The number of members in this guild. #: @@ -1279,7 +1297,7 @@ class Guild(PartialGuild): #: this will always be :obj:`None`. #: #: :type: :obj:`int`, optional - member_count: typing.Optional[int] = marshaller.attrib(if_undefined=None, deserializer=int) + member_count: typing.Optional[int] = marshaller.attrib(deserializer=int, if_undefined=None, default=None) #: A mapping of ID to the corresponding guild members in this guild. #: @@ -1300,7 +1318,9 @@ class Guild(PartialGuild): #: #: :type: :obj:`typing.Mapping` [ :obj:`hikari.snowflakes.Snowflake`, :obj:`GuildMember` ], optional members: typing.Optional[typing.Mapping[snowflakes.Snowflake, GuildMember]] = marshaller.attrib( - deserializer=lambda members: {m.user.id: m for m in map(GuildMember.deserialize, members)}, if_undefined=None, + deserializer=lambda members: {m.user.id: m for m in map(GuildMember.deserialize, members)}, + if_undefined=None, + default=None, ) #: A mapping of ID to the corresponding guild channels in this guild. @@ -1322,6 +1342,7 @@ class Guild(PartialGuild): channels: typing.Optional[typing.Mapping[snowflakes.Snowflake, _channels.GuildChannel]] = marshaller.attrib( deserializer=lambda guild_channels: {c.id: c for c in map(_channels.deserialize_channel, guild_channels)}, if_undefined=None, + default=None, ) #: A mapping of member ID to the corresponding presence information for @@ -1344,6 +1365,7 @@ class Guild(PartialGuild): presences: typing.Optional[typing.Mapping[snowflakes.Snowflake, GuildMemberPresence]] = marshaller.attrib( deserializer=lambda presences: {p.user.id: p for p in map(GuildMemberPresence.deserialize, presences)}, if_undefined=None, + default=None, ) #: The maximum number of presences for the guild. @@ -1351,14 +1373,16 @@ class Guild(PartialGuild): #: If this is :obj:`None`, then the default value is used (currently 5000). #: #: :type: :obj:`int`, optional - max_presences: typing.Optional[int] = marshaller.attrib(if_none=None, if_undefined=None, deserializer=int) + max_presences: typing.Optional[int] = marshaller.attrib( + deserializer=int, if_undefined=None, if_none=None, default=None + ) #: The maximum number of members allowed in this guild. #: #: This information may not be present, in which case, it will be :obj:`None`. #: #: :type: :obj:`int`, optional - max_members: typing.Optional[int] = marshaller.attrib(if_undefined=None, deserializer=int) + max_members: typing.Optional[int] = marshaller.attrib(deserializer=int, if_undefined=None, default=None) #: The vanity URL code for the guild's vanity URL. #: @@ -1366,7 +1390,7 @@ class Guild(PartialGuild): #: ``features`` for this guild. If not, this will always be :obj:`None`. #: #: :type: :obj:`str`, optional - vanity_url_code: typing.Optional[str] = marshaller.attrib(if_none=None, deserializer=str) + vanity_url_code: typing.Optional[str] = marshaller.attrib(deserializer=str, if_none=None) #: The guild's description. #: @@ -1395,7 +1419,9 @@ class Guild(PartialGuild): #: This information may not be present, in which case, it will be :obj:`None`. #: #: :type: :obj:`int`, optional - premium_subscription_count: typing.Optional[int] = marshaller.attrib(if_undefined=None, deserializer=int) + premium_subscription_count: typing.Optional[int] = marshaller.attrib( + deserializer=int, if_undefined=None, default=None + ) #: The preferred locale to use for this guild. #: diff --git a/hikari/invites.py b/hikari/invites.py index 669b04df23..1928b1e791 100644 --- a/hikari/invites.py +++ b/hikari/invites.py @@ -43,7 +43,7 @@ class TargetUserType(enum.IntEnum): @marshaller.marshallable() -@attr.s(slots=True) +@attr.s(slots=True, kw_only=True) class VanityUrl(entities.HikariEntity, entities.Deserializable): """A special case invite object, that represents a guild's vanity url.""" @@ -59,7 +59,7 @@ class VanityUrl(entities.HikariEntity, entities.Deserializable): @marshaller.marshallable() -@attr.s(slots=True) +@attr.s(slots=True, kw_only=True) class InviteGuild(guilds.PartialGuild): """Represents the partial data of a guild that'll be attached to invites.""" @@ -164,7 +164,7 @@ def banner_url(self) -> typing.Optional[str]: @marshaller.marshallable() -@attr.s(slots=True) +@attr.s(slots=True, kw_only=True) class Invite(entities.HikariEntity, entities.Deserializable): """Represents an invite that's used to add users to a guild or group dm.""" @@ -177,7 +177,9 @@ class Invite(entities.HikariEntity, entities.Deserializable): #: Will be :obj:`None` for group dm invites. #: #: :type: :obj:`InviteGuild`, optional - guild: typing.Optional[InviteGuild] = marshaller.attrib(deserializer=InviteGuild.deserialize, if_undefined=None) + guild: typing.Optional[InviteGuild] = marshaller.attrib( + deserializer=InviteGuild.deserialize, if_undefined=None, default=None + ) #: The partial object of the channel this invite targets. #: #: :type: :obj:`hikari.channels.PartialChannel` @@ -186,35 +188,43 @@ class Invite(entities.HikariEntity, entities.Deserializable): #: The object of the user who created this invite. #: #: :type: :obj:`hikari.users.User`, optional - inviter: typing.Optional[users.User] = marshaller.attrib(deserializer=users.User.deserialize, if_undefined=None) + inviter: typing.Optional[users.User] = marshaller.attrib( + deserializer=users.User.deserialize, if_undefined=None, default=None + ) #: The object of the user who this invite targets, if set. #: #: :type: :obj:`hikari.users.User`, optional - target_user: typing.Optional[users.User] = marshaller.attrib(deserializer=users.User.deserialize, if_undefined=None) + target_user: typing.Optional[users.User] = marshaller.attrib( + deserializer=users.User.deserialize, if_undefined=None, default=None + ) #: The type of user target this invite is, if applicable. #: #: :type: :obj:`TargetUserType`, optional target_user_type: typing.Optional[TargetUserType] = marshaller.attrib( - deserializer=TargetUserType, if_undefined=None + deserializer=TargetUserType, if_undefined=None, default=None ) #: The approximate amount of presences in this invite's guild, only present #: when ``with_counts`` is passed as :obj:`True` to the GET Invites endpoint. #: #: :type: :obj:`int`, optional - approximate_presence_count: typing.Optional[int] = marshaller.attrib(deserializer=int, if_undefined=None) + approximate_presence_count: typing.Optional[int] = marshaller.attrib( + deserializer=int, if_undefined=None, default=None + ) #: The approximate amount of members in this invite's guild, only present #: when ``with_counts`` is passed as :obj:`True` to the GET Invites endpoint. #: #: :type: :obj:`int`, optional - approximate_member_count: typing.Optional[int] = marshaller.attrib(deserializer=int, if_undefined=None) + approximate_member_count: typing.Optional[int] = marshaller.attrib( + deserializer=int, if_undefined=None, default=None + ) @marshaller.marshallable() -@attr.s(slots=True) +@attr.s(slots=True, kw_only=True) class InviteWithMetadata(Invite): """Extends the base :obj:`Invite` object with metadata. diff --git a/hikari/messages.py b/hikari/messages.py index 4547e8554d..f7a470c9d8 100644 --- a/hikari/messages.py +++ b/hikari/messages.py @@ -132,7 +132,7 @@ class MessageActivityType(enum.IntEnum): @marshaller.marshallable() -@attr.s(slots=True) +@attr.s(slots=True, kw_only=True) class Attachment(snowflakes.UniqueEntity, entities.Deserializable): """Represents a file attached to a message.""" @@ -159,16 +159,16 @@ class Attachment(snowflakes.UniqueEntity, entities.Deserializable): #: The height of the image (if the file is an image). #: #: :type: :obj:`int`, optional - height: typing.Optional[int] = marshaller.attrib(deserializer=int, if_undefined=None) + height: typing.Optional[int] = marshaller.attrib(deserializer=int, if_undefined=None, default=None) #: The width of the image (if the file is an image). #: #: :type: :obj:`int`, optional - width: typing.Optional[int] = marshaller.attrib(deserializer=int, if_undefined=None) + width: typing.Optional[int] = marshaller.attrib(deserializer=int, if_undefined=None, default=None) @marshaller.marshallable() -@attr.s(slots=True) +@attr.s(slots=True, kw_only=True) class Reaction(entities.HikariEntity, entities.Deserializable): """Represents a reaction in a message.""" @@ -191,7 +191,7 @@ class Reaction(entities.HikariEntity, entities.Deserializable): @marshaller.marshallable() -@attr.s(slots=True) +@attr.s(slots=True, kw_only=True) class MessageActivity(entities.HikariEntity, entities.Deserializable): """Represents the activity of a rich presence-enabled message.""" @@ -203,11 +203,11 @@ class MessageActivity(entities.HikariEntity, entities.Deserializable): #: The party ID of the message activity. #: #: :type: :obj:`str`, optional - party_id: typing.Optional[str] = marshaller.attrib(deserializer=str, if_undefined=None) + party_id: typing.Optional[str] = marshaller.attrib(deserializer=str, if_undefined=None, default=None) @marshaller.marshallable() -@attr.s(slots=True) +@attr.s(slots=True, kw_only=True) class MessageCrosspost(entities.HikariEntity, entities.Deserializable): """Represents information about a cross-posted message and the origin of the original message.""" @@ -222,7 +222,7 @@ class MessageCrosspost(entities.HikariEntity, entities.Deserializable): #: #: :type: :obj:`hikari.snowflakes.Snowflake`, optional message_id: typing.Optional[snowflakes.Snowflake] = marshaller.attrib( - deserializer=snowflakes.Snowflake.deserialize, if_undefined=None + deserializer=snowflakes.Snowflake.deserialize, if_undefined=None, default=None ) #: The ID of the channel that the message originated from. @@ -240,12 +240,12 @@ class MessageCrosspost(entities.HikariEntity, entities.Deserializable): #: #: :type: :obj:`hikari.snowflakes.Snowflake`, optional guild_id: typing.Optional[snowflakes.Snowflake] = marshaller.attrib( - deserializer=snowflakes.Snowflake.deserialize, if_undefined=None + deserializer=snowflakes.Snowflake.deserialize, if_undefined=None, default=None ) @marshaller.marshallable() -@attr.s(slots=True) +@attr.s(slots=True, kw_only=True) class Message(snowflakes.UniqueEntity, entities.Deserializable): """Represents a message.""" @@ -258,7 +258,7 @@ class Message(snowflakes.UniqueEntity, entities.Deserializable): #: #: :type: :obj:`hikari.snowflakes.Snowflake`, optional guild_id: typing.Optional[snowflakes.Snowflake] = marshaller.attrib( - deserializer=snowflakes.Snowflake.deserialize, if_undefined=None + deserializer=snowflakes.Snowflake.deserialize, if_undefined=None, default=None ) #: The author of this message. @@ -270,7 +270,7 @@ class Message(snowflakes.UniqueEntity, entities.Deserializable): #: #: :type: :obj:`hikari.guilds.GuildMember`, optional member: typing.Optional[guilds.GuildMember] = marshaller.attrib( - deserializer=guilds.GuildMember.deserialize, if_undefined=None + deserializer=guilds.GuildMember.deserialize, if_undefined=None, default=None ) #: The content of the message. @@ -324,6 +324,7 @@ class Message(snowflakes.UniqueEntity, entities.Deserializable): raw_name="mention_channels", deserializer=lambda channel_mentions: {snowflakes.Snowflake.deserialize(c["id"]) for c in channel_mentions}, if_undefined=dict, + factory=dict, ) #: The message attachments. @@ -344,7 +345,7 @@ class Message(snowflakes.UniqueEntity, entities.Deserializable): #: #: :type: :obj:`typing.Sequence` [ :obj:`Reaction` ] reactions: typing.Sequence[Reaction] = marshaller.attrib( - deserializer=lambda reactions: [Reaction.deserialize(r) for r in reactions], if_undefined=dict + deserializer=lambda reactions: [Reaction.deserialize(r) for r in reactions], if_undefined=dict, factory=dict ) #: Whether the message is pinned. @@ -356,7 +357,7 @@ class Message(snowflakes.UniqueEntity, entities.Deserializable): #: #: :type: :obj:`hikari.snowflakes.Snowflake`, optional webhook_id: typing.Optional[snowflakes.Snowflake] = marshaller.attrib( - deserializer=snowflakes.Snowflake.deserialize, if_undefined=None + deserializer=snowflakes.Snowflake.deserialize, if_undefined=None, default=None ) #: The message type. @@ -368,30 +369,30 @@ class Message(snowflakes.UniqueEntity, entities.Deserializable): #: #: :type: :obj:`MessageActivity`, optional activity: typing.Optional[MessageActivity] = marshaller.attrib( - deserializer=MessageActivity.deserialize, if_undefined=None + deserializer=MessageActivity.deserialize, if_undefined=None, default=None ) #: The message application. #: #: :type: :obj:`hikari.oauth2.Application`, optional application: typing.Optional[oauth2.Application] = marshaller.attrib( - deserializer=oauth2.Application.deserialize, if_undefined=None + deserializer=oauth2.Application.deserialize, if_undefined=None, default=None ) #: The message crossposted reference data. #: #: :type: :obj:`MessageCrosspost`, optional message_reference: typing.Optional[MessageCrosspost] = marshaller.attrib( - deserializer=MessageCrosspost.deserialize, if_undefined=None + deserializer=MessageCrosspost.deserialize, if_undefined=None, default=None ) #: The message flags. #: #: :type: :obj:`MessageFlag`, optional - flags: typing.Optional[MessageFlag] = marshaller.attrib(deserializer=MessageFlag, if_undefined=None) + flags: typing.Optional[MessageFlag] = marshaller.attrib(deserializer=MessageFlag, if_undefined=None, default=None) #: The message nonce. This is a string used for validating #: a message was sent. #: #: :type: :obj:`str`, optional - nonce: typing.Optional[str] = marshaller.attrib(deserializer=str, if_undefined=None) + nonce: typing.Optional[str] = marshaller.attrib(deserializer=str, if_undefined=None, default=None) diff --git a/hikari/oauth2.py b/hikari/oauth2.py index f30171e4dd..dcb89299a9 100644 --- a/hikari/oauth2.py +++ b/hikari/oauth2.py @@ -45,7 +45,7 @@ class ConnectionVisibility(enum.IntEnum): @marshaller.marshallable() -@attr.s(slots=True) +@attr.s(slots=True, kw_only=True) class OwnConnection(entities.HikariEntity, entities.Deserializable): """Represents a user's connection with a third party account. @@ -75,7 +75,7 @@ class OwnConnection(entities.HikariEntity, entities.Deserializable): #: Whether the connection has been revoked. #: #: :type: :obj:`bool` - is_revoked: bool = marshaller.attrib(raw_name="revoked", deserializer=bool, if_undefined=False) + is_revoked: bool = marshaller.attrib(raw_name="revoked", deserializer=bool, if_undefined=False, default=False) #: A sequence of the partial guild integration objects this connection has. #: @@ -85,6 +85,7 @@ class OwnConnection(entities.HikariEntity, entities.Deserializable): guilds.PartialGuildIntegration.deserialize(integration) for integration in payload ], if_undefined=list, + factory=list, ) #: Whether the connection has been verified. @@ -110,7 +111,7 @@ class OwnConnection(entities.HikariEntity, entities.Deserializable): @marshaller.marshallable() -@attr.s(slots=True) +@attr.s(slots=True, kw_only=True) class OwnGuild(guilds.PartialGuild): """Represents a user bound partial guild object.""" @@ -139,7 +140,7 @@ class TeamMembershipState(enum.IntEnum): @marshaller.marshallable() -@attr.s(slots=True) +@attr.s(slots=True, kw_only=True) class TeamMember(entities.HikariEntity, entities.Deserializable): """Represents a member of a Team.""" @@ -166,7 +167,7 @@ class TeamMember(entities.HikariEntity, entities.Deserializable): @marshaller.marshallable() -@attr.s(slots=True) +@attr.s(slots=True, kw_only=True) class Team(snowflakes.UniqueEntity, entities.Deserializable): """Represents a development team, along with all its members.""" @@ -220,7 +221,7 @@ def format_icon_url(self, fmt: str = "png", size: int = 4096) -> typing.Optional @marshaller.marshallable() -@attr.s(slots=True) +@attr.s(slots=True, kw_only=True) class ApplicationOwner(users.User): """Represents the user who owns an application, may be a team user.""" @@ -236,7 +237,7 @@ def is_team_user(self) -> bool: @marshaller.marshallable() -@attr.s(slots=True) +@attr.s(slots=True, kw_only=True) class Application(snowflakes.UniqueEntity, entities.Deserializable): """Represents the information of an Oauth2 Application.""" @@ -255,7 +256,7 @@ class Application(snowflakes.UniqueEntity, entities.Deserializable): #: #: :type: :obj:`bool`, optional is_bot_public: typing.Optional[bool] = marshaller.attrib( - raw_name="bot_public", deserializer=bool, if_undefined=None + raw_name="bot_public", deserializer=bool, if_undefined=None, default=None ) #: Whether the bot associated with this application is requiring code grant @@ -263,7 +264,7 @@ class Application(snowflakes.UniqueEntity, entities.Deserializable): #: #: :type: :obj:`bool`, optional is_bot_code_grant_required: typing.Optional[bool] = marshaller.attrib( - raw_name="bot_require_code_grant", deserializer=bool, if_undefined=None + raw_name="bot_require_code_grant", deserializer=bool, if_undefined=None, default=None ) #: The object of this application's owner. @@ -272,13 +273,13 @@ class Application(snowflakes.UniqueEntity, entities.Deserializable): #: #: :type: :obj:`ApplicationOwner`, optional owner: typing.Optional[ApplicationOwner] = marshaller.attrib( - deserializer=ApplicationOwner.deserialize, if_undefined=None + deserializer=ApplicationOwner.deserialize, if_undefined=None, default=None ) #: A collection of this application's rpc origin URLs, if rpc is enabled. #: #: :type: :obj:`typing.Set` [ :obj:`str` ], optional - rpc_origins: typing.Optional[typing.Set[str]] = marshaller.attrib(deserializer=set, if_undefined=None) + rpc_origins: typing.Optional[typing.Set[str]] = marshaller.attrib(deserializer=set, if_undefined=None, default=None) #: This summary for this application's primary SKU if it's sold on Discord. #: Will be an empty string if unset. @@ -290,45 +291,49 @@ class Application(snowflakes.UniqueEntity, entities.Deserializable): #: #: :type: :obj:`bytes`, optional verify_key: typing.Optional[bytes] = marshaller.attrib( - deserializer=lambda key: bytes(key, "utf-8"), if_undefined=None + deserializer=lambda key: bytes(key, "utf-8"), if_undefined=None, default=None ) #: The hash of this application's icon, if set. #: #: :type: :obj:`str`, optional - icon_hash: typing.Optional[str] = marshaller.attrib(raw_name="icon", deserializer=str, if_undefined=None) + icon_hash: typing.Optional[str] = marshaller.attrib( + raw_name="icon", deserializer=str, if_undefined=None, default=None + ) #: This application's team if it belongs to one. #: #: :type: :obj:`Team`, optional - team: typing.Optional[Team] = marshaller.attrib(deserializer=Team.deserialize, if_undefined=None, if_none=None) + team: typing.Optional[Team] = marshaller.attrib( + deserializer=Team.deserialize, if_undefined=None, if_none=None, default=None + ) #: The ID of the guild this application is linked to #: if it's sold on Discord. #: #: :type: :obj:`hikari.snowflakes.Snowflake`, optional guild_id: typing.Optional[snowflakes.Snowflake] = marshaller.attrib( - deserializer=snowflakes.Snowflake.deserialize, if_undefined=None + deserializer=snowflakes.Snowflake.deserialize, if_undefined=None, default=None ) #: The ID of the primary "Game SKU" of a game that's sold on Discord. #: #: :type: :obj:`hikari.snowflakes.Snowflake`, optional primary_sku_id: typing.Optional[snowflakes.Snowflake] = marshaller.attrib( - deserializer=snowflakes.Snowflake.deserialize, if_undefined=None + deserializer=snowflakes.Snowflake.deserialize, if_undefined=None, default=None ) #: The URL slug that links to this application's store page #: if it's sold on Discord. #: #: :type: :obj:`str`, optional - slug: typing.Optional[str] = marshaller.attrib(deserializer=str, if_undefined=None) + slug: typing.Optional[str] = marshaller.attrib(deserializer=str, if_undefined=None, default=None) #: The hash of this application's cover image on it's store, if set. #: #: :type: :obj:`str`, optional cover_image_hash: typing.Optional[str] = marshaller.attrib( - raw_name="cover_image", deserializer=str, if_undefined=None + raw_name="cover_image", deserializer=str, if_undefined=None, default=None ) @property diff --git a/hikari/snowflakes.py b/hikari/snowflakes.py index 44b866c76a..f5ceba2737 100644 --- a/hikari/snowflakes.py +++ b/hikari/snowflakes.py @@ -113,7 +113,7 @@ def from_timestamp(cls, timestamp: float) -> "Snowflake": @marshaller.marshallable() -@attr.s(slots=True) +@attr.s(slots=True, kw_only=True) class UniqueEntity(entities.HikariEntity, typing.SupportsInt): """An entity that has an integer ID of some sort.""" diff --git a/hikari/state/stateless_event_managers.py b/hikari/state/stateless_event_managers.py index d2e06e144c..789f841d3f 100644 --- a/hikari/state/stateless_event_managers.py +++ b/hikari/state/stateless_event_managers.py @@ -210,12 +210,16 @@ def on_message_reaction_add(self, _, payload): @event_managers.raw_event_mapper("MESSAGE_REACTION_REMOVE") def on_message_reaction_remove(self, _, payload): """Handle MESSAGE_REACTION_REMOVE events.""" + payload["emoji"].setdefault("animated", None) + event = events.MessageReactionRemoveEvent.deserialize(payload) self.event_dispatcher.dispatch_event(event) @event_managers.raw_event_mapper("MESSAGE_REACTION_REMOVE_EMOJI") def on_message_reaction_remove_emoji(self, _, payload): """Handle MESSAGE_REACTION_REMOVE_EMOJI events.""" + payload["emoji"].setdefault("animated", None) + event = events.MessageReactionRemoveEmojiEvent.deserialize(payload) self.event_dispatcher.dispatch_event(event) diff --git a/hikari/users.py b/hikari/users.py index 977853cc61..304d57ed17 100644 --- a/hikari/users.py +++ b/hikari/users.py @@ -92,7 +92,7 @@ class PremiumType(enum.IntEnum): @marshaller.marshallable() -@attr.s(slots=True) +@attr.s(slots=True, kw_only=True) class User(snowflakes.UniqueEntity, entities.Deserializable): """Represents a user.""" @@ -114,12 +114,12 @@ class User(snowflakes.UniqueEntity, entities.Deserializable): #: Whether this user is a bot account. #: #: :type: :obj:`bool` - is_bot: bool = marshaller.attrib(raw_name="bot", deserializer=bool, if_undefined=False) + is_bot: bool = marshaller.attrib(raw_name="bot", deserializer=bool, if_undefined=False, default=False) #: Whether this user is a system account. #: #: :type: :obj:`bool` - is_system: bool = marshaller.attrib(raw_name="system", deserializer=bool, if_undefined=False) + is_system: bool = marshaller.attrib(raw_name="system", deserializer=bool, if_undefined=False, default=False) #: The public flags for this user. #: @@ -130,7 +130,7 @@ class User(snowflakes.UniqueEntity, entities.Deserializable): #: #: :type: :obj:`UserFlag`, optional flags: typing.Optional[UserFlag] = marshaller.attrib( - raw_name="public_flags", deserializer=UserFlag, if_undefined=None, + raw_name="public_flags", deserializer=UserFlag, if_undefined=None, default=None ) @property @@ -178,7 +178,7 @@ def default_avatar(self) -> int: @marshaller.marshallable() -@attr.s(slots=True) +@attr.s(slots=True, kw_only=True) class MyUser(User): """Represents a user with extended oauth2 information.""" @@ -190,21 +190,23 @@ class MyUser(User): #: The user's set language. This is not provided by the ``READY`` event. #: #: :type: :obj:`str`, optional - locale: typing.Optional[str] = marshaller.attrib(deserializer=str, if_none=None, if_undefined=None) + locale: typing.Optional[str] = marshaller.attrib(deserializer=str, if_none=None, if_undefined=None, default=None) #: Whether the email for this user's account has been verified. #: Will be :obj:`None` if retrieved through the oauth2 flow without the #: ``email`` scope. #: #: :type: :obj:`bool`, optional - is_verified: typing.Optional[bool] = marshaller.attrib(raw_name="verified", deserializer=bool, if_undefined=None) + is_verified: typing.Optional[bool] = marshaller.attrib( + raw_name="verified", deserializer=bool, if_undefined=None, default=None + ) #: The user's set email. #: Will be :obj:`None` if retrieved through the oauth2 flow without the #: ``email`` scope and for bot users. #: #: :type: :obj:`str`, optional - email: typing.Optional[str] = marshaller.attrib(deserializer=str, if_undefined=None, if_none=None) + email: typing.Optional[str] = marshaller.attrib(deserializer=str, if_undefined=None, if_none=None, default=None) #: This user account's flags. #: @@ -215,4 +217,6 @@ class MyUser(User): #: This will always be :obj:`None` for bots. #: #: :type: :obj:`PremiumType`, optional - premium_type: typing.Optional[PremiumType] = marshaller.attrib(deserializer=PremiumType, if_undefined=None) + premium_type: typing.Optional[PremiumType] = marshaller.attrib( + deserializer=PremiumType, if_undefined=None, default=None + ) diff --git a/hikari/voices.py b/hikari/voices.py index e02cb9e8d4..5fcbebe46e 100644 --- a/hikari/voices.py +++ b/hikari/voices.py @@ -30,7 +30,7 @@ @marshaller.marshallable() -@attr.s(slots=True) +@attr.s(slots=True, kw_only=True) class VoiceState(entities.HikariEntity, entities.Deserializable): """Represents a user's voice connection status.""" @@ -38,7 +38,7 @@ class VoiceState(entities.HikariEntity, entities.Deserializable): #: #: :type: :obj:`hikari.snowflakes.Snowflake`, optional guild_id: typing.Optional[snowflakes.Snowflake] = marshaller.attrib( - deserializer=snowflakes.Snowflake.deserialize, if_undefined=None + deserializer=snowflakes.Snowflake.deserialize, if_undefined=None, default=None ) #: The ID of the channel this user is connected to, will be :obj:`None` if @@ -59,7 +59,7 @@ class VoiceState(entities.HikariEntity, entities.Deserializable): #: #: :type: :obj:`hikari.guilds.GuildMember`, optional member: typing.Optional[guilds.GuildMember] = marshaller.attrib( - deserializer=guilds.GuildMember.deserialize, if_undefined=None + deserializer=guilds.GuildMember.deserialize, if_undefined=None, default=None ) #: The ID of this voice state's session. @@ -90,7 +90,7 @@ class VoiceState(entities.HikariEntity, entities.Deserializable): #: Whether this user is streaming using "Go Live". #: #: :type: :obj:`bool` - is_streaming: bool = marshaller.attrib(raw_name="self_stream", deserializer=bool, if_undefined=False) + is_streaming: bool = marshaller.attrib(raw_name="self_stream", deserializer=bool, if_undefined=False, default=False) #: Whether this user is muted by the current user. #: @@ -99,7 +99,7 @@ class VoiceState(entities.HikariEntity, entities.Deserializable): @marshaller.marshallable() -@attr.s(slots=True) +@attr.s(slots=True, kw_only=True) class VoiceRegion(entities.HikariEntity, entities.Deserializable): """Represent's a voice region server.""" diff --git a/hikari/webhooks.py b/hikari/webhooks.py index b642a7f2ff..e18652b26f 100644 --- a/hikari/webhooks.py +++ b/hikari/webhooks.py @@ -42,7 +42,7 @@ class WebhookType(enum.IntEnum): @marshaller.marshallable() -@attr.s(slots=True) +@attr.s(slots=True, kw_only=True) class Webhook(snowflakes.UniqueEntity, entities.Deserializable): """Represents a webhook object on Discord. @@ -60,7 +60,7 @@ class Webhook(snowflakes.UniqueEntity, entities.Deserializable): #: #: :type: :obj:`hikari.snowflakes.Snowflake`, optional guild_id: typing.Optional[snowflakes.Snowflake] = marshaller.attrib( - deserializer=snowflakes.Snowflake.deserialize, if_undefined=None + deserializer=snowflakes.Snowflake.deserialize, if_undefined=None, default=None ) #: The channel ID this webhook is for. @@ -77,7 +77,9 @@ class Webhook(snowflakes.UniqueEntity, entities.Deserializable): #: #: #: :type: :obj:`hikari.users.User`, optional - user: typing.Optional[users.User] = marshaller.attrib(deserializer=users.User.deserialize, if_undefined=None) + user: typing.Optional[users.User] = marshaller.attrib( + deserializer=users.User.deserialize, if_undefined=None, default=None + ) #: The default name of the webhook. #: @@ -97,4 +99,4 @@ class Webhook(snowflakes.UniqueEntity, entities.Deserializable): #: #: #: :type: :obj:`str`, optional - token: typing.Optional[str] = marshaller.attrib(deserializer=str, if_undefined=None) + token: typing.Optional[str] = marshaller.attrib(deserializer=str, if_undefined=None, default=None) diff --git a/tests/hikari/state/test_stateless_event_managers.py b/tests/hikari/state/test_stateless_event_managers.py index b7fdc3cdad..842697d19a 100644 --- a/tests/hikari/state/test_stateless_event_managers.py +++ b/tests/hikari/state/test_stateless_event_managers.py @@ -333,16 +333,18 @@ def test_on_message_reaction_add(self, event_manager_impl, mock_payload): event_manager_impl.event_dispatcher.dispatch_event.assert_called_once_with(mock_event) def test_on_message_reaction_remove(self, event_manager_impl, mock_payload): + mock_payload["emoji"] = {} mock_event = mock.MagicMock(events.MessageReactionRemoveEvent) with mock.patch("hikari.events.MessageReactionRemoveEvent.deserialize", return_value=mock_event) as event: event_manager_impl.on_message_reaction_remove(None, mock_payload) assert event_manager_impl.on_message_reaction_remove.___event_name___ == {"MESSAGE_REACTION_REMOVE"} - event.assert_called_once_with(mock_payload) + event.assert_called_once_with({"emoji": {"animated": None}}) event_manager_impl.event_dispatcher.dispatch_event.assert_called_once_with(mock_event) def test_on_message_reaction_remove_emoji(self, event_manager_impl, mock_payload): + mock_payload["emoji"] = {} mock_event = mock.MagicMock(events.MessageReactionRemoveEmojiEvent) with mock.patch("hikari.events.MessageReactionRemoveEmojiEvent.deserialize", return_value=mock_event) as event: @@ -351,7 +353,7 @@ def test_on_message_reaction_remove_emoji(self, event_manager_impl, mock_payload assert event_manager_impl.on_message_reaction_remove_emoji.___event_name___ == { "MESSAGE_REACTION_REMOVE_EMOJI" } - event.assert_called_once_with(mock_payload) + event.assert_called_once_with({"emoji": {"animated": None}}) event_manager_impl.event_dispatcher.dispatch_event.assert_called_once_with(mock_event) def test_on_presence_update(self, event_manager_impl, mock_payload): diff --git a/tests/hikari/test_snowflake.py b/tests/hikari/test_snowflake.py index 7ae4bcc62a..ed9d367c87 100644 --- a/tests/hikari/test_snowflake.py +++ b/tests/hikari/test_snowflake.py @@ -92,7 +92,7 @@ def test_int(self): @pytest.fixture() def stud_marshal_entity(self): @marshaller.marshallable() - @attr.s(slots=True) + @attr.s(slots=True, kw_only=True) class StudEntity(snowflakes.UniqueEntity, entities.Deserializable, entities.Serializable): ... From f286e300c6d0bba13d95c3f8416270591242f69e Mon Sep 17 00:00:00 2001 From: Faster Speeding Date: Wed, 15 Apr 2020 19:03:30 +0100 Subject: [PATCH 133/922] Fix python 3.9.0 deprecation warnings caused by passing coroutines to asyncio.wait. --- hikari/internal/more_asyncio.py | 17 +++++++++++++++++ hikari/net/shard.py | 3 ++- tests/hikari/internal/test_more_asyncio.py | 18 ++++++++++++++++++ 3 files changed, 37 insertions(+), 1 deletion(-) diff --git a/hikari/internal/more_asyncio.py b/hikari/internal/more_asyncio.py index bf968e4c42..b47cbb19e4 100644 --- a/hikari/internal/more_asyncio.py +++ b/hikari/internal/more_asyncio.py @@ -134,3 +134,20 @@ def completed_future(result=None, /): future = asyncio.get_event_loop().create_future() future.set_result(result) return future + + +def wait( + aws, *, timeout=None, return_when=asyncio.ALL_COMPLETED +) -> typing.Coroutine[typing.Any, typing.Any, typing.Tuple[typing.Set[Future], typing.Set[Future]]]: + """Run awaitable objects in the aws set concurrently. + + This blocks until the condition specified by `return_value`. + + Returns + ------- + :obj:`typing.Coroutine` [ :obj:`typing.Any`, :obj:`typing.Any`, :obj:`typing.Tuple` [ :obj:`typing.Set` [ :obj:`Future` ], :obj:`typing.Set` [ :obj:`Future` ] ] ] + The coroutine returned by :obj:`asyncio.wait` of two sets of + Tasks/Futures (done, pending). + """ + # noinspection PyTypeChecker + return asyncio.wait([asyncio.ensure_future(f) for f in aws], timeout=timeout, return_when=return_when) diff --git a/hikari/net/shard.py b/hikari/net/shard.py index d67fef6056..43836c5c59 100644 --- a/hikari/net/shard.py +++ b/hikari/net/shard.py @@ -47,6 +47,7 @@ import aiohttp.typedefs from hikari import errors +from hikari.internal import more_asyncio from hikari.internal import more_logging from hikari.net import codes from hikari.net import ratelimits @@ -601,7 +602,7 @@ async def connect(self, client_session_type=aiohttp.ClientSession) -> None: self.dispatch(self, "CONNECTED", {}) self.logger.debug("received HELLO (interval:%ss)", self.heartbeat_interval) - completed, pending_tasks = await asyncio.wait( + completed, pending_tasks = await more_asyncio.wait( [self._heartbeat_keep_alive(self.heartbeat_interval), self._run()], return_when=asyncio.FIRST_COMPLETED, ) diff --git a/tests/hikari/internal/test_more_asyncio.py b/tests/hikari/internal/test_more_asyncio.py index 7fb634a304..58a0e63ccd 100644 --- a/tests/hikari/internal/test_more_asyncio.py +++ b/tests/hikari/internal/test_more_asyncio.py @@ -17,10 +17,13 @@ # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . import asyncio +import contextlib +from unittest import mock import pytest from hikari.internal import more_asyncio +from tests.hikari import _helpers class CoroutineStub: @@ -75,3 +78,18 @@ async def test_default_result_is_none(self): @pytest.mark.asyncio async def test_non_default_result(self): assert more_asyncio.completed_future(...).result() is ... + + +@pytest.mark.asyncio +async def test_wait(): + mock_futures = ([mock.MagicMock(asyncio.Future)], [mock.MagicMock(asyncio.Future)]) + mock_awaitable = _helpers.AwaitableMock() + mock_future = mock.MagicMock(asyncio.Future) + stack = contextlib.ExitStack() + stack.enter_context(mock.patch.object(asyncio, "wait", return_value=mock_futures)) + stack.enter_context(mock.patch.object(asyncio, "ensure_future", return_value=mock_future)) + with stack: + result = await more_asyncio.wait([mock_awaitable], timeout=42, return_when=asyncio.FIRST_COMPLETED) + assert result == mock_futures + asyncio.wait.assert_called_once_with([mock_future], timeout=42, return_when=asyncio.FIRST_COMPLETED) + asyncio.ensure_future.assert_called_once_with(mock_awaitable) From 5bdce5a963eb5710d9fd3d461d7ea890dd83d9ec Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Wed, 15 Apr 2020 19:09:34 +0100 Subject: [PATCH 134/922] Fixing more CI bugs [skip deploy] --- ci/deploy.nox.py | 2 +- ci/pages.gitlab-ci.yml | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/ci/deploy.nox.py b/ci/deploy.nox.py index edb0630bc7..258fd68731 100644 --- a/ci/deploy.nox.py +++ b/ci/deploy.nox.py @@ -211,7 +211,7 @@ def deploy(session: nox.Session) -> None: f"{config.API_NAME} v{next_version} has been released", "Pick up the latest development release from pypi by running:\n" "```bash\n" - f"pip install --pre -U {config.API_NAME}=={next_version}\n" + f"pip install -U {config.API_NAME}=={next_version}\n" "```", "2C2F33" ) diff --git a/ci/pages.gitlab-ci.yml b/ci/pages.gitlab-ci.yml index 55d42a8bca..187a91e284 100644 --- a/ci/pages.gitlab-ci.yml +++ b/ci/pages.gitlab-ci.yml @@ -39,7 +39,8 @@ nox -s sphinx || nox -s documentation # FIXME: remove once in master. rm ../public -rf && mkdir ../public if [[ "$TARGET_BRANCH" = master ]]; then - mv public ../public + cd public + find . -maxdepth 1 -exec mv {} ../../public \; else mv public "../public/${TARGET_BRANCH}" fi From cef8ca22958183b7580f9b6e358ffb840c439e79 Mon Sep 17 00:00:00 2001 From: davfsa Date: Wed, 15 Apr 2020 20:25:37 +0200 Subject: [PATCH 135/922] Replace `unittest.mock` with `mock` --- dev-requirements.txt | 1 + tests/hikari/_helpers.py | 2 +- tests/hikari/clients/test_gateway_managers.py | 2 +- tests/hikari/clients/test_rest_client.py | 2 +- tests/hikari/clients/test_shard_clients.py | 2 +- tests/hikari/internal/test_conversions.py | 2 +- tests/hikari/internal/test_marshaller.py | 2 +- .../hikari/internal/test_marshaller_pep563.py | 2 +- tests/hikari/net/test_ratelimits.py | 2 +- tests/hikari/net/test_rest.py | 20 +++++++++---------- tests/hikari/net/test_shard.py | 2 +- tests/hikari/state/test_event_dispatcher.py | 2 +- .../state/test_stateless_event_managers.py | 2 +- tests/hikari/test_audit_logs.py | 2 +- tests/hikari/test_channels.py | 2 +- tests/hikari/test_embeds.py | 2 +- tests/hikari/test_emojis.py | 2 +- tests/hikari/test_events.py | 2 +- tests/hikari/test_gateway_entities.py | 2 +- tests/hikari/test_guilds.py | 2 +- tests/hikari/test_invites.py | 2 +- tests/hikari/test_messages.py | 2 +- tests/hikari/test_oauth2.py | 2 +- tests/hikari/test_users.py | 2 +- tests/hikari/test_webhook.py | 2 +- 25 files changed, 33 insertions(+), 34 deletions(-) diff --git a/dev-requirements.txt b/dev-requirements.txt index 8b155f0f5e..a026b5e448 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -6,3 +6,4 @@ pytest-asyncio==0.10.0 pytest-cov==2.8.1 pytest-html==2.1.1 pytest-xdist==1.31.0 +mock==4.0.2 diff --git a/tests/hikari/_helpers.py b/tests/hikari/_helpers.py index f45f1e2c18..4df177a404 100644 --- a/tests/hikari/_helpers.py +++ b/tests/hikari/_helpers.py @@ -30,7 +30,7 @@ import typing import warnings import weakref -from unittest import mock +import mock import async_timeout import pytest diff --git a/tests/hikari/clients/test_gateway_managers.py b/tests/hikari/clients/test_gateway_managers.py index cb080f0695..6792b2ea05 100644 --- a/tests/hikari/clients/test_gateway_managers.py +++ b/tests/hikari/clients/test_gateway_managers.py @@ -17,7 +17,7 @@ # You should have received a copy of the GNU Lesser General Public License # along ith Hikari. If not, see . import math -from unittest import mock +import mock import pytest diff --git a/tests/hikari/clients/test_rest_client.py b/tests/hikari/clients/test_rest_client.py index 5e90c5063e..4cd6f55f19 100644 --- a/tests/hikari/clients/test_rest_client.py +++ b/tests/hikari/clients/test_rest_client.py @@ -19,7 +19,7 @@ import contextlib import datetime import io -from unittest import mock +import mock import pytest diff --git a/tests/hikari/clients/test_shard_clients.py b/tests/hikari/clients/test_shard_clients.py index a8e969e1e1..9a0f80ae4f 100644 --- a/tests/hikari/clients/test_shard_clients.py +++ b/tests/hikari/clients/test_shard_clients.py @@ -18,7 +18,7 @@ # along ith Hikari. If not, see . import datetime import math -from unittest import mock +import mock import aiohttp import pytest diff --git a/tests/hikari/internal/test_conversions.py b/tests/hikari/internal/test_conversions.py index ecefd9ad32..06a490f1b5 100644 --- a/tests/hikari/internal/test_conversions.py +++ b/tests/hikari/internal/test_conversions.py @@ -21,7 +21,7 @@ import inspect import io import typing -from unittest import mock +import mock import pytest diff --git a/tests/hikari/internal/test_marshaller.py b/tests/hikari/internal/test_marshaller.py index 47d2b856e3..4300dd557e 100644 --- a/tests/hikari/internal/test_marshaller.py +++ b/tests/hikari/internal/test_marshaller.py @@ -16,7 +16,7 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . -from unittest import mock +import mock import attr import pytest diff --git a/tests/hikari/internal/test_marshaller_pep563.py b/tests/hikari/internal/test_marshaller_pep563.py index bb60e49161..4f72979a9f 100644 --- a/tests/hikari/internal/test_marshaller_pep563.py +++ b/tests/hikari/internal/test_marshaller_pep563.py @@ -23,7 +23,7 @@ """ from __future__ import annotations -from unittest import mock +import mock import attr import pytest diff --git a/tests/hikari/net/test_ratelimits.py b/tests/hikari/net/test_ratelimits.py index 29f3844be6..b102c2bb29 100644 --- a/tests/hikari/net/test_ratelimits.py +++ b/tests/hikari/net/test_ratelimits.py @@ -23,7 +23,7 @@ import math import statistics import time -from unittest import mock +import mock import pytest diff --git a/tests/hikari/net/test_rest.py b/tests/hikari/net/test_rest.py index bf10fe9155..b5d89d925a 100644 --- a/tests/hikari/net/test_rest.py +++ b/tests/hikari/net/test_rest.py @@ -22,8 +22,7 @@ import json import logging import ssl -import unittest.mock -from unittest import mock +import mock import aiohttp import pytest @@ -690,10 +689,10 @@ async def test_create_message_without_optionals(self, rest_impl): rest_impl._request.assert_called_once_with(mock_route, form_body=mock_form, re_seekable_resources=[]) @pytest.mark.asyncio - @unittest.mock.patch.object(routes, "CHANNEL_MESSAGES") - @unittest.mock.patch.object(aiohttp, "FormData", autospec=True) - @unittest.mock.patch.object(conversions, "make_resource_seekable") - @unittest.mock.patch.object(json, "dumps") + @mock.patch.object(routes, "CHANNEL_MESSAGES") + @mock.patch.object(aiohttp, "FormData", autospec=True) + @mock.patch.object(conversions, "make_resource_seekable") + @mock.patch.object(json, "dumps") async def test_create_message_with_optionals( self, dumps, make_resource_seekable, FormData, CHANNEL_MESSAGES, rest_impl ): @@ -2067,12 +2066,11 @@ async def test_execute_webhook_without_optionals(self, rest_impl): mock_route, form_body=mock_form, re_seekable_resources=[], query={}, suppress_authorization_header=True, ) - # cymock doesn't work right with the patch @pytest.mark.asyncio - @unittest.mock.patch.object(aiohttp, "FormData", autospec=True) - @unittest.mock.patch.object(routes, "WEBHOOK_WITH_TOKEN") - @unittest.mock.patch.object(json, "dumps") - @unittest.mock.patch.object(conversions, "make_resource_seekable") + @mock.patch.object(aiohttp, "FormData", autospec=True) + @mock.patch.object(routes, "WEBHOOK_WITH_TOKEN") + @mock.patch.object(json, "dumps") + @mock.patch.object(conversions, "make_resource_seekable") async def test_execute_webhook_with_optionals( self, make_resource_seekable, dumps, WEBHOOK_WITH_TOKEN, FormData, rest_impl ): diff --git a/tests/hikari/net/test_shard.py b/tests/hikari/net/test_shard.py index 6134599400..823e155101 100644 --- a/tests/hikari/net/test_shard.py +++ b/tests/hikari/net/test_shard.py @@ -22,7 +22,7 @@ import math import time import urllib.parse -from unittest import mock +import mock import aiohttp import async_timeout diff --git a/tests/hikari/state/test_event_dispatcher.py b/tests/hikari/state/test_event_dispatcher.py index f765852d8f..515f8cd016 100644 --- a/tests/hikari/state/test_event_dispatcher.py +++ b/tests/hikari/state/test_event_dispatcher.py @@ -17,7 +17,7 @@ # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . import asyncio -from unittest import mock +import mock import pytest diff --git a/tests/hikari/state/test_stateless_event_managers.py b/tests/hikari/state/test_stateless_event_managers.py index 842697d19a..f5448d7c68 100644 --- a/tests/hikari/state/test_stateless_event_managers.py +++ b/tests/hikari/state/test_stateless_event_managers.py @@ -16,7 +16,7 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . -from unittest import mock +import mock import pytest diff --git a/tests/hikari/test_audit_logs.py b/tests/hikari/test_audit_logs.py index 52451cdf00..de403b7e11 100644 --- a/tests/hikari/test_audit_logs.py +++ b/tests/hikari/test_audit_logs.py @@ -18,7 +18,7 @@ # along ith Hikari. If not, see . import contextlib import datetime -from unittest import mock +import mock import pytest diff --git a/tests/hikari/test_channels.py b/tests/hikari/test_channels.py index 4ab6fb10a5..2bfe4fb469 100644 --- a/tests/hikari/test_channels.py +++ b/tests/hikari/test_channels.py @@ -17,7 +17,7 @@ # You should have received a copy of the GNU Lesser General Public License # along ith Hikari. If not, see . import datetime -from unittest import mock +import mock import pytest diff --git a/tests/hikari/test_embeds.py b/tests/hikari/test_embeds.py index effc6ebc02..d9ae694c35 100644 --- a/tests/hikari/test_embeds.py +++ b/tests/hikari/test_embeds.py @@ -17,7 +17,7 @@ # You should have received a copy of the GNU Lesser General Public License # along ith Hikari. If not, see . import datetime -from unittest import mock +import mock import pytest diff --git a/tests/hikari/test_emojis.py b/tests/hikari/test_emojis.py index 54b4a0cf40..7543f2d7cf 100644 --- a/tests/hikari/test_emojis.py +++ b/tests/hikari/test_emojis.py @@ -16,7 +16,7 @@ # # You should have received a copy of the GNU Lesser General Public License # along ith Hikari. If not, see . -from unittest import mock +import mock import pytest diff --git a/tests/hikari/test_events.py b/tests/hikari/test_events.py index 19f9c84478..635357e76b 100644 --- a/tests/hikari/test_events.py +++ b/tests/hikari/test_events.py @@ -18,7 +18,7 @@ # along ith Hikari. If not, see . import contextlib import datetime -from unittest import mock +import mock import pytest diff --git a/tests/hikari/test_gateway_entities.py b/tests/hikari/test_gateway_entities.py index 825d043dbb..88fdca7e08 100644 --- a/tests/hikari/test_gateway_entities.py +++ b/tests/hikari/test_gateway_entities.py @@ -17,7 +17,7 @@ # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . import datetime -from unittest import mock +import mock import pytest diff --git a/tests/hikari/test_guilds.py b/tests/hikari/test_guilds.py index 8113537463..762c9adf4c 100644 --- a/tests/hikari/test_guilds.py +++ b/tests/hikari/test_guilds.py @@ -18,7 +18,7 @@ # along ith Hikari. If not, see . import contextlib import datetime -from unittest import mock +import mock import pytest diff --git a/tests/hikari/test_invites.py b/tests/hikari/test_invites.py index 15c2807b3d..f6b9c02df7 100644 --- a/tests/hikari/test_invites.py +++ b/tests/hikari/test_invites.py @@ -18,7 +18,7 @@ # along ith Hikari. If not, see . import contextlib import datetime -from unittest import mock +import mock import pytest diff --git a/tests/hikari/test_messages.py b/tests/hikari/test_messages.py index dcc2268a6e..464b3bd60b 100644 --- a/tests/hikari/test_messages.py +++ b/tests/hikari/test_messages.py @@ -18,7 +18,7 @@ # along ith Hikari. If not, see . import contextlib import datetime -from unittest import mock +import mock import pytest diff --git a/tests/hikari/test_oauth2.py b/tests/hikari/test_oauth2.py index a85d19ac55..26fe44bbbd 100644 --- a/tests/hikari/test_oauth2.py +++ b/tests/hikari/test_oauth2.py @@ -16,7 +16,7 @@ # # You should have received a copy of the GNU Lesser General Public License # along ith Hikari. If not, see . -from unittest import mock +import mock import pytest diff --git a/tests/hikari/test_users.py b/tests/hikari/test_users.py index 3365414101..71e988e801 100644 --- a/tests/hikari/test_users.py +++ b/tests/hikari/test_users.py @@ -16,7 +16,7 @@ # # You should have received a copy of the GNU Lesser General Public License # along ith Hikari. If not, see . -from unittest import mock +import mock import pytest diff --git a/tests/hikari/test_webhook.py b/tests/hikari/test_webhook.py index f87a1a6a6e..b702373ad2 100644 --- a/tests/hikari/test_webhook.py +++ b/tests/hikari/test_webhook.py @@ -16,7 +16,7 @@ # # You should have received a copy of the GNU Lesser General Public License # along ith Hikari. If not, see . -from unittest import mock +import mock from hikari import users from hikari import webhooks From 802386fc79e05a6b5e3af325797edf0581a164b0 Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Wed, 15 Apr 2020 19:51:02 +0100 Subject: [PATCH 136/922] Fixed bugs, we are now able to get a basic stateless bot running. --- hikari/clients/bot_clients.py | 5 +++-- hikari/clients/configs.py | 14 ++++++++++++++ hikari/clients/rest_clients.py | 2 +- hikari/state/event_dispatchers.py | 4 ++++ 4 files changed, 22 insertions(+), 3 deletions(-) diff --git a/hikari/clients/bot_clients.py b/hikari/clients/bot_clients.py index 87747ed5ed..ccd8e9d142 100644 --- a/hikari/clients/bot_clients.py +++ b/hikari/clients/bot_clients.py @@ -79,9 +79,10 @@ def __init__(self, config: configs.BotConfig, event_manager: event_managers.Even self.config = config self.event_manager = event_manager self.gateway = NotImplemented - self.rest = rest_clients.RESTClient(self.config) + self.rest = NotImplemented async def start(self): + self.rest = rest_clients.RESTClient(self.config) gateway_bot = await self.rest.fetch_gateway_bot() self.logger.info( @@ -100,7 +101,7 @@ async def start(self): raw_event_consumer_impl=self.event_manager, shard_ids=shard_ids, shard_count=shard_count, - dispatcher=self.event_manager.event_dispatcher.dispatch_event, + dispatcher=self.event_manager.event_dispatcher, ) await self.gateway.start() diff --git a/hikari/clients/configs.py b/hikari/clients/configs.py index c96ac1aab5..f55201aa5e 100644 --- a/hikari/clients/configs.py +++ b/hikari/clients/configs.py @@ -328,6 +328,20 @@ class ShardConfig(BaseConfig): class RESTConfig(AIOHTTPConfig, TokenConfig): """REST-specific configuration details.""" + #: Token authentication scheme. + #: + #: Should be ``"Bot"`` or ``"Bearer"``, or ``None`` if not relevant. + #: + #: Defaults to ``"Bot"`` + #: + #: :type: :obj:`str` + token_type: typing.Optional[str] = marshaller.attrib( + deserializer=str, + if_undefined=lambda: "Bot", + if_none=None, + default="Bot" + ) + #: The HTTP API version to use. #: #: If unspecified, then V7 is used. diff --git a/hikari/clients/rest_clients.py b/hikari/clients/rest_clients.py index 21de40d83c..e756556d79 100644 --- a/hikari/clients/rest_clients.py +++ b/hikari/clients/rest_clients.py @@ -86,7 +86,7 @@ def __init__(self, config: configs.RESTConfig) -> None: ssl_context=config.ssl_context, verify_ssl=config.verify_ssl, timeout=config.request_timeout, - token=config.token, + token=f"{config.token_type} {config.token}" if config.token_type is not None else config.token, version=config.rest_version, ) diff --git a/hikari/state/event_dispatchers.py b/hikari/state/event_dispatchers.py index be680d3fee..5996fa1f44 100644 --- a/hikari/state/event_dispatchers.py +++ b/hikari/state/event_dispatchers.py @@ -57,6 +57,10 @@ class EventDispatcher(abc.ABC): __slots__ = () + @abc.abstractmethod + def close(self) -> None: + """Cancel anything that is waiting for an event to be dispatched.""" + @abc.abstractmethod def add_listener(self, event_type: typing.Type[EventT], callback: EventCallbackT) -> EventCallbackT: """Register a new event callback to a given event name. From 5afec2b598f00c22bed5c2bd578a03fd5417f537 Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Wed, 15 Apr 2020 19:59:11 +0100 Subject: [PATCH 137/922] Fixed unittest schenanigans --- tests/hikari/clients/test_rest_client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/hikari/clients/test_rest_client.py b/tests/hikari/clients/test_rest_client.py index 5e90c5063e..10874d8d35 100644 --- a/tests/hikari/clients/test_rest_client.py +++ b/tests/hikari/clients/test_rest_client.py @@ -71,7 +71,7 @@ def test_init(self, mock_config): ssl_context=mock_config.ssl_context, verify_ssl=mock_config.verify_ssl, timeout=mock_config.request_timeout, - token=mock_config.token, + token=f"{mock_config.token_type} {mock_config.token}", version=mock_config.rest_version, ) assert cli._session is mock_low_level_rest_clients From a859a15b5d1a1b56e7dbfe105bbaf6c5b0b76d68 Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Wed, 15 Apr 2020 20:10:35 +0100 Subject: [PATCH 138/922] Fixed formatting issue. --- hikari/clients/configs.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/hikari/clients/configs.py b/hikari/clients/configs.py index f55201aa5e..47e54e03bf 100644 --- a/hikari/clients/configs.py +++ b/hikari/clients/configs.py @@ -336,10 +336,7 @@ class RESTConfig(AIOHTTPConfig, TokenConfig): #: #: :type: :obj:`str` token_type: typing.Optional[str] = marshaller.attrib( - deserializer=str, - if_undefined=lambda: "Bot", - if_none=None, - default="Bot" + deserializer=str, if_undefined=lambda: "Bot", if_none=None, default="Bot" ) #: The HTTP API version to use. From 7e3b9f3814f27764a9ce2f5473b377e31c0798e2 Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Wed, 15 Apr 2020 21:29:04 +0100 Subject: [PATCH 139/922] Fixed weird italics in sphinx, fixed how "Ellipsis" shows in signatures. --- docs/_static/style.css | 15 +++++++++++++++ docs/conf.py | 20 +++++++++++++++----- 2 files changed, 30 insertions(+), 5 deletions(-) diff --git a/docs/_static/style.css b/docs/_static/style.css index 7152545bad..9509c5f333 100644 --- a/docs/_static/style.css +++ b/docs/_static/style.css @@ -335,3 +335,18 @@ a.reference > code { padding-top: 0; padding-bottom: 0; } + +/* + * Stop EM elements for parameters being italic. + */ +em.sig-param { + font-style: normal; +} + + +/* + * Fix out-of-lined-ed-ness for param tables. + */ +dt, dd { + line-height: 2em !important; +} diff --git a/docs/conf.py b/docs/conf.py index 3f7203be2e..b87bb93276 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -33,13 +33,10 @@ import sphinx_bootstrap_theme - sys.path.insert(0, os.path.abspath("..")) - name = "hikari" - with open(os.path.join("..", name, "_about.py")) as fp: code = fp.read() @@ -56,7 +53,6 @@ del groups, token_pattern, code, fp - # -- Project information ----------------------------------------------------- project = name.title() @@ -87,7 +83,6 @@ # -- Pygments style ---------------------------------------------------------- pygments_style = "fruity" - # -- Options for HTML output ------------------------------------------------- html_theme = "bootstrap" html_theme_path = sphinx_bootstrap_theme.get_html_theme_path() @@ -228,5 +223,20 @@ ) +# -- Hacks and formatting --------------------------------------------- + +def pretty_signature(app, what, name, obj, options, signature, return_annotation): + if what not in ('function', 'method', 'class'): + return + + if signature is None: + return + + signature = re.sub(r"(?P\w+)=Ellipsis", r"\1=...", signature) + signature = signature.replace("/", "/ ") + return signature, return_annotation + + def setup(app): + app.connect('autodoc-process-signature', pretty_signature) app.add_css_file("style.css") From 1ae40e3fe22a9bbe09ce8ec895177f806bfbb5cf Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Wed, 15 Apr 2020 21:32:35 +0100 Subject: [PATCH 140/922] Found a magic way to hide the full name of modules in docs. Now stuff isn't a bjillion lines long with this one simple trick. Doctors hate him! --- hikari/audit_logs.py | 78 +- hikari/channels.py | 66 +- hikari/clients/bot_clients.py | 14 +- hikari/clients/configs.py | 84 +- hikari/clients/gateway_managers.py | 22 +- hikari/clients/rest_clients.py | 1224 +++++++++++++-------------- hikari/clients/runnable.py | 2 +- hikari/clients/shard_clients.py | 60 +- hikari/colors.py | 62 +- hikari/embeds.py | 70 +- hikari/emojis.py | 16 +- hikari/errors.py | 72 +- hikari/events.py | 274 +++--- hikari/gateway_entities.py | 18 +- hikari/guilds.py | 378 ++++----- hikari/internal/assertions.py | 8 +- hikari/internal/conversions.py | 96 +-- hikari/internal/marshaller.py | 72 +- hikari/internal/meta.py | 2 +- hikari/internal/more_asyncio.py | 16 +- hikari/internal/more_collections.py | 2 +- hikari/internal/more_logging.py | 2 +- hikari/internal/urls.py | 16 +- hikari/invites.py | 80 +- hikari/messages.py | 80 +- hikari/net/ratelimits.py | 178 ++-- hikari/net/rest.py | 1088 ++++++++++++------------ hikari/net/routes.py | 38 +- hikari/net/shard.py | 122 +-- hikari/net/user_agent.py | 10 +- hikari/oauth2.py | 96 +-- hikari/snowflakes.py | 8 +- hikari/state/event_dispatchers.py | 54 +- hikari/state/event_managers.py | 18 +- hikari/state/raw_event_consumers.py | 6 +- hikari/users.py | 40 +- hikari/voices.py | 36 +- hikari/webhooks.py | 16 +- 38 files changed, 2262 insertions(+), 2262 deletions(-) diff --git a/hikari/audit_logs.py b/hikari/audit_logs.py index 9c82e181d1..bb0c45382f 100644 --- a/hikari/audit_logs.py +++ b/hikari/audit_logs.py @@ -172,17 +172,17 @@ class AuditLogChange(entities.HikariEntity, entities.Deserializable): #: The new value of the key, if something was added or changed. #: - #: :type: :obj:`typing.Any`, optional + #: :type: :obj:`~typing.Any`, optional new_value: typing.Optional[typing.Any] = marshaller.attrib() #: The old value of the key, if something was removed or changed. #: - #: :type: :obj:`typing.Any`, optional + #: :type: :obj:`~typing.Any`, optional old_value: typing.Optional[typing.Any] = marshaller.attrib() #: The name of the audit log change's key. #: - #: :type: :obj:`typing.Union` [ :obj:`AuditLogChangeKey`, :obj:`str` ] + #: :type: :obj:`~typing.Union` [ :obj:`~AuditLogChangeKey`, :obj:`~str` ] key: typing.Union[AuditLogChangeKey, str] = marshaller.attrib() @classmethod @@ -250,9 +250,9 @@ def register_audit_log_entry_info( Parameters ---------- - type_ : :obj:`AuditLogEventType` + type_ : :obj:`~AuditLogEventType` An entry types to associate the entity with. - *additional_types : :obj:`AuditLogEventType` + *additional_types : :obj:`~AuditLogEventType` Extra entry types to associate the entity with. Returns @@ -294,17 +294,17 @@ class ChannelOverwriteEntryInfo(BaseAuditLogEntryInfo): #: The ID of the overwrite being updated, added or removed (and the entity #: it targets). #: - #: :type: :obj:`hikari.snowflakes.Snowflake` + #: :type: :obj:`~hikari.snowflakes.Snowflake` id: snowflakes.Snowflake = marshaller.attrib(deserializer=snowflakes.Snowflake.deserialize) #: The type of entity this overwrite targets. #: - #: :type: :obj:`hikari.channels.PermissionOverwriteType` + #: :type: :obj:`~hikari.channels.PermissionOverwriteType` type: channels.PermissionOverwriteType = marshaller.attrib(deserializer=channels.PermissionOverwriteType) #: The name of the role this overwrite targets, if it targets a role. #: - #: :type: :obj:`str`, optional + #: :type: :obj:`~str`, optional role_name: typing.Optional[str] = marshaller.attrib(deserializer=str, if_undefined=None, default=None) @@ -320,12 +320,12 @@ class MessagePinEntryInfo(BaseAuditLogEntryInfo): #: The ID of the guild text based channel where this pinned message is #: being added or removed. #: - #: :type: :obj:`hikari.snowflakes.Snowflake` + #: :type: :obj:`~hikari.snowflakes.Snowflake` channel_id: snowflakes.Snowflake = marshaller.attrib(deserializer=snowflakes.Snowflake.deserialize) #: The ID of the message that's being pinned or unpinned. #: - #: :type: :obj:`hikari.snowflakes.Snowflake` + #: :type: :obj:`~hikari.snowflakes.Snowflake` message_id: snowflakes.Snowflake = marshaller.attrib(deserializer=snowflakes.Snowflake.deserialize) @@ -338,14 +338,14 @@ class MemberPruneEntryInfo(BaseAuditLogEntryInfo): #: The timedelta of how many days members were pruned for inactivity based #: on. #: - #: :type: :obj:`datetime.timedelta` + #: :type: :obj:`~datetime.timedelta` delete_member_days: datetime.timedelta = marshaller.attrib( deserializer=lambda payload: datetime.timedelta(days=int(payload)) ) #: The number of members who were removed by this prune. #: - #: :type: :obj:`int` + #: :type: :obj:`~int` members_removed: int = marshaller.attrib(deserializer=int) @@ -357,7 +357,7 @@ class MessageBulkDeleteEntryInfo(BaseAuditLogEntryInfo): #: The amount of messages that were deleted. #: - #: :type: :obj:`int` + #: :type: :obj:`~int` count: int = marshaller.attrib(deserializer=int) @@ -369,7 +369,7 @@ class MessageDeleteEntryInfo(MessageBulkDeleteEntryInfo): #: The guild text based channel where these message(s) were deleted. #: - #: :type: :obj:`hikari.snowflakes.Snowflake` + #: :type: :obj:`~hikari.snowflakes.Snowflake` channel_id: snowflakes.Snowflake = marshaller.attrib(deserializer=snowflakes.Snowflake.deserialize) @@ -381,7 +381,7 @@ class MemberDisconnectEntryInfo(BaseAuditLogEntryInfo): #: The amount of members who were disconnected from voice in this entry. #: - #: :type: :obj:`int` + #: :type: :obj:`~int` count: int = marshaller.attrib(deserializer=int) @@ -393,7 +393,7 @@ class MemberMoveEntryInfo(MemberDisconnectEntryInfo): #: The channel these member(s) were moved to. #: - #: :type: :obj:`hikari.snowflakes.Snowflake` + #: :type: :obj:`~hikari.snowflakes.Snowflake` channel_id: snowflakes.Snowflake = marshaller.attrib(deserializer=snowflakes.Snowflake.deserialize) @@ -413,14 +413,14 @@ def get_entry_info_entity(type_: int) -> typing.Type[BaseAuditLogEntryInfo]: Parameters ---------- - :obj:`int` + :obj:`~int` The int Returns ------- - :obj:`typing.Type` [ :obj:`BaseAuditLogEntryInfo` ] + :obj:`~typing.Type` [ :obj:`~BaseAuditLogEntryInfo` ] The associated options entity. If not implemented then this will be - :obj:`UnrecognisedAuditLogEntryInfo` + :obj:`~UnrecognisedAuditLogEntryInfo` """ types = getattr(register_audit_log_entry_info, "types", more_collections.EMPTY_DICT) entry_type = types.get(type_) @@ -434,38 +434,38 @@ class AuditLogEntry(snowflakes.UniqueEntity, entities.Deserializable): #: The ID of the entity affected by this change, if applicable. #: - #: :type: :obj:`hikari.snowflakes.Snowflake`, optional + #: :type: :obj:`~hikari.snowflakes.Snowflake`, optional target_id: typing.Optional[snowflakes.Snowflake] = marshaller.attrib() #: A sequence of the changes made to :attr:`target_id` #: - #: :type: :obj:`typing.Sequence` [ :obj:`AuditLogChange` ] + #: :type: :obj:`~typing.Sequence` [ :obj:`~AuditLogChange` ] changes: typing.Sequence[AuditLogChange] = marshaller.attrib() #: The ID of the user who made this change. #: - #: :type: :obj:`hikari.snowflakes.Snowflake` + #: :type: :obj:`~hikari.snowflakes.Snowflake` user_id: snowflakes.Snowflake = marshaller.attrib() #: The ID of this entry. #: - #: :type: :obj:`hikari.snowflakes.Snowflake` + #: :type: :obj:`~hikari.snowflakes.Snowflake` id: snowflakes.Snowflake = marshaller.attrib() #: The type of action this entry represents. #: - #: :type: :obj:`typing.Union` [ :obj:`AuditLogEventType`, :obj:`str` ] + #: :type: :obj:`~typing.Union` [ :obj:`~AuditLogEventType`, :obj:`~str` ] action_type: typing.Union[AuditLogEventType, str] = marshaller.attrib() #: Extra information about this entry. Will only be provided for certain #: :attr:`action_type`. #: - #: :type: :obj:`BaseAuditLogEntryInfo`, optional + #: :type: :obj:`~BaseAuditLogEntryInfo`, optional options: typing.Optional[BaseAuditLogEntryInfo] = marshaller.attrib() #: The reason for this change, if set (between 0-512 characters). #: - #: :type: :obj:`str` + #: :type: :obj:`~str` reason: typing.Optional[str] = marshaller.attrib() @classmethod @@ -501,7 +501,7 @@ class AuditLog(entities.HikariEntity, entities.Deserializable): #: A sequence of the audit log's entries. #: - #: :type: :obj:`typing.Mapping` [ :obj:`hikari.snowflakes.Snowflake`, :obj:`AuditLogEntry` ] + #: :type: :obj:`~typing.Mapping` [ :obj:`~hikari.snowflakes.Snowflake`, :obj:`~AuditLogEntry` ] entries: typing.Mapping[snowflakes.Snowflake, AuditLogEntry] = marshaller.attrib( raw_name="audit_log_entries", deserializer=lambda payload: {entry.id: entry for entry in map(AuditLogEntry.deserialize, payload)}, @@ -509,7 +509,7 @@ class AuditLog(entities.HikariEntity, entities.Deserializable): #: A mapping of the partial objects of integrations found in this audit log. #: - #: :type: :obj:`typing.Mapping` [ :obj:`hikari.snowflakes.Snowflake`, :obj:`hikari.guilds.GuildIntegration` ] + #: :type: :obj:`~typing.Mapping` [ :obj:`~hikari.snowflakes.Snowflake`, :obj:`~hikari.guilds.GuildIntegration` ] integrations: typing.Mapping[snowflakes.Snowflake, guilds.GuildIntegration] = marshaller.attrib( deserializer=lambda payload: { integration.id: integration for integration in map(guilds.PartialGuildIntegration.deserialize, payload) @@ -518,14 +518,14 @@ class AuditLog(entities.HikariEntity, entities.Deserializable): #: A mapping of the objects of users found in this audit log. #: - #: :type: :obj:`typing.Mapping` [ :obj:`hikari.snowflakes.Snowflake`, :obj:`hikari.users.User` ] + #: :type: :obj:`~typing.Mapping` [ :obj:`~hikari.snowflakes.Snowflake`, :obj:`~hikari.users.User` ] users: typing.Mapping[snowflakes.Snowflake, _users.User] = marshaller.attrib( deserializer=lambda payload: {user.id: user for user in map(_users.User.deserialize, payload)} ) #: A mapping of the objects of webhooks found in this audit log. #: - #: :type: :obj:`typing.Mapping` [ :obj:`hikari.snowflakes.Snowflake`, :obj:`hikari.webhooks.Webhook` ] + #: :type: :obj:`~typing.Mapping` [ :obj:`~hikari.snowflakes.Snowflake`, :obj:`~hikari.webhooks.Webhook` ] webhooks: typing.Mapping[snowflakes.Snowflake, _webhooks.Webhook] = marshaller.attrib( deserializer=lambda payload: {webhook.id: webhook for webhook in map(_webhooks.Webhook.deserialize, payload)} ) @@ -539,19 +539,19 @@ class AuditLogIterator(typing.AsyncIterator[AuditLogEntry]): Parameters ---------- - guild_id : :obj:`str` + guild_id : :obj:`~str` The guild ID to look up. - request : :obj:`typing.Callable` [ ``...``, :obj:`typing.Coroutine` [ :obj:`typing.Any`, :obj:`typing.Any`, :obj:`typing.Any` ] ] + request : :obj:`~typing.Callable` [ ``...``, :obj:`~typing.Coroutine` [ :obj:`~typing.Any`, :obj:`~typing.Any`, :obj:`~typing.Any` ] ] The session bound function that this iterator should use for making Get Guild Audit Log requests. - user_id : :obj:`str` + user_id : :obj:`~str` If specified, the user ID to filter by. - action_type : :obj:`int` + action_type : :obj:`~int` If specified, the action type to look up. - limit : :obj:`int` + limit : :obj:`~int` If specified, the limit to how many entries this iterator should return else unlimited. - before : :obj:`str` + before : :obj:`~str` If specified, an entry ID to specify where this iterator's returned audit log entries should start . @@ -577,17 +577,17 @@ class AuditLogIterator(typing.AsyncIterator[AuditLogEntry]): #: A mapping of the partial objects of integrations found in this audit log #: so far. #: - #: :type: :obj:`typing.Mapping` [ :obj:`hikari.snowflakes.Snowflake`, :obj:`hikari.guilds.GuildIntegration` ] + #: :type: :obj:`~typing.Mapping` [ :obj:`~hikari.snowflakes.Snowflake`, :obj:`~hikari.guilds.GuildIntegration` ] integrations: typing.Mapping[snowflakes.Snowflake, guilds.GuildIntegration] #: A mapping of the objects of users found in this audit log so far. #: - #: :type: :obj:`typing.Mapping` [ :obj:`hikari.snowflakes.Snowflake`, :obj:`hikari.users.User` ] + #: :type: :obj:`~typing.Mapping` [ :obj:`~hikari.snowflakes.Snowflake`, :obj:`~hikari.users.User` ] users: typing.Mapping[snowflakes.Snowflake, _users.User] #: A mapping of the objects of webhooks found in this audit log so far. #: - #: :type: :obj:`typing.Mapping` [ :obj:`hikari.snowflakes.Snowflake`, :obj:`hikari.webhooks.Webhook` ] + #: :type: :obj:`~typing.Mapping` [ :obj:`~hikari.snowflakes.Snowflake`, :obj:`~hikari.webhooks.Webhook` ] webhooks: typing.Mapping[snowflakes.Snowflake, _webhooks.Webhook] def __init__( diff --git a/hikari/channels.py b/hikari/channels.py index a9f5ccf0a0..7d760f07c7 100644 --- a/hikari/channels.py +++ b/hikari/channels.py @@ -107,19 +107,19 @@ class PermissionOverwrite(snowflakes.UniqueEntity, entities.Deserializable, enti #: The type of entity this overwrite targets. #: - #: :type: :obj:`PermissionOverwriteType` + #: :type: :obj:`~PermissionOverwriteType` type: PermissionOverwriteType = marshaller.attrib(deserializer=PermissionOverwriteType, serializer=str) #: The permissions this overwrite allows. #: - #: :type: :obj:`hikari.permissions.Permission` + #: :type: :obj:`~hikari.permissions.Permission` allow: permissions.Permission = marshaller.attrib( deserializer=permissions.Permission, serializer=int, default=permissions.Permission(0) ) #: The permissions this overwrite denies. #: - #: :type: :obj:`hikari.permissions.Permission` + #: :type: :obj:`~hikari.permissions.Permission` deny: permissions.Permission = marshaller.attrib( deserializer=permissions.Permission, serializer=int, default=permissions.Permission(0) ) @@ -137,7 +137,7 @@ def register_channel_type(type_: ChannelType) -> typing.Callable[[typing.Type["C Parameters ---------- - type_ : :obj:`ChannelType` + type_ : :obj:`~ChannelType` The channel type to associate with. Returns @@ -162,7 +162,7 @@ class Channel(snowflakes.UniqueEntity, entities.Deserializable): #: The channel's type. #: - #: :type: :obj:`ChannelType` + #: :type: :obj:`~ChannelType` type: ChannelType = marshaller.attrib(deserializer=ChannelType) @@ -176,7 +176,7 @@ class PartialChannel(Channel): #: The channel's name. #: - #: :type: :obj:`str` + #: :type: :obj:`~str` name: str = marshaller.attrib(deserializer=str) @@ -193,14 +193,14 @@ class DMChannel(Channel): #: This might point to an invalid or deleted message. #: #: - #: :type: :obj:`hikari.snowflakes.Snowflake`, optional + #: :type: :obj:`~hikari.snowflakes.Snowflake`, optional last_message_id: snowflakes.Snowflake = marshaller.attrib( deserializer=snowflakes.Snowflake.deserialize, if_none=None ) #: The recipients of the DM. #: - #: :type: :obj:`typing.Mapping` [ :obj:`hikari.snowflakes.Snowflake`, :obj:`hikari.users.User` ] + #: :type: :obj:`~typing.Mapping` [ :obj:`~hikari.snowflakes.Snowflake`, :obj:`~hikari.users.User` ] recipients: typing.Mapping[snowflakes.Snowflake, users.User] = marshaller.attrib( deserializer=lambda recipients: {user.id: user for user in map(users.User.deserialize, recipients)} ) @@ -214,23 +214,23 @@ class GroupDMChannel(DMChannel): #: The group's name. #: - #: :type: :obj:`str` + #: :type: :obj:`~str` name: str = marshaller.attrib(deserializer=str) #: The ID of the owner of the group. #: - #: :type: :obj:`hikari.snowflakes.Snowflake` + #: :type: :obj:`~hikari.snowflakes.Snowflake` owner_id: snowflakes.Snowflake = marshaller.attrib(deserializer=snowflakes.Snowflake.deserialize) #: The hash of the icon of the group. #: - #: :type: :obj:`str`, optional + #: :type: :obj:`~str`, optional icon_hash: typing.Optional[str] = marshaller.attrib(raw_name="icon", deserializer=str, if_none=None) #: The ID of the application that created the group DM, if it's a #: bot based group DM. #: - #: :type: :obj:`hikari.snowflakes.Snowflake`, optional + #: :type: :obj:`~hikari.snowflakes.Snowflake`, optional application_id: typing.Optional[snowflakes.Snowflake] = marshaller.attrib( deserializer=snowflakes.Snowflake.deserialize, if_undefined=None, default=None ) @@ -243,34 +243,34 @@ class GuildChannel(Channel): #: The ID of the guild the channel belongs to. #: - #: :type: :obj:`hikari.snowflakes.Snowflake` + #: :type: :obj:`~hikari.snowflakes.Snowflake` guild_id: snowflakes.Snowflake = marshaller.attrib(deserializer=snowflakes.Snowflake.deserialize) #: The sorting position of the channel. #: - #: :type: :obj:`int` + #: :type: :obj:`~int` position: int = marshaller.attrib(deserializer=int) #: The permission overwrites for the channel. #: - #: :type: :obj:`typing.Mapping` [ :obj:`hikari.snowflakes.Snowflake`, :obj:`PermissionOverwrite` ] + #: :type: :obj:`~typing.Mapping` [ :obj:`~hikari.snowflakes.Snowflake`, :obj:`~PermissionOverwrite` ] permission_overwrites: PermissionOverwrite = marshaller.attrib( deserializer=lambda overwrites: {o.id: o for o in map(PermissionOverwrite.deserialize, overwrites)} ) #: The name of the channel. #: - #: :type: :obj:`str` + #: :type: :obj:`~str` name: str = marshaller.attrib(deserializer=str) #: Wheter the channel is marked as NSFW. #: - #: :type: :obj:`bool` + #: :type: :obj:`~bool` is_nsfw: bool = marshaller.attrib(raw_name="nsfw", deserializer=bool) #: The ID of the parent category the channel belongs to. #: - #: :type: :obj:`hikari.snowflakes.Snowflake`, optional + #: :type: :obj:`~hikari.snowflakes.Snowflake`, optional parent_id: snowflakes.Snowflake = marshaller.attrib(deserializer=snowflakes.Snowflake.deserialize, if_none=None) @@ -289,7 +289,7 @@ class GuildTextChannel(GuildChannel): #: The topic of the channel. #: - #: :type: :obj:`str`, optional + #: :type: :obj:`~str`, optional topic: str = marshaller.attrib(deserializer=str, if_none=None) #: The ID of the last message sent in this channel. @@ -299,7 +299,7 @@ class GuildTextChannel(GuildChannel): #: This might point to an invalid or deleted message. #: #: - #: :type: :obj:`hikari.snowflakes.Snowflake`, optional + #: :type: :obj:`~hikari.snowflakes.Snowflake`, optional last_message_id: snowflakes.Snowflake = marshaller.attrib( deserializer=snowflakes.Snowflake.deserialize, if_none=None ) @@ -312,7 +312,7 @@ class GuildTextChannel(GuildChannel): #: Bots, as well as users with ``MANAGE_MESSAGES`` or #: ``MANAGE_CHANNEL``, are not afected by this. #: - #: :type: :obj:`datetime.timedelta` + #: :type: :obj:`~datetime.timedelta` rate_limit_per_user: datetime.timedelta = marshaller.attrib( deserializer=lambda payload: datetime.timedelta(seconds=payload) ) @@ -326,7 +326,7 @@ class GuildNewsChannel(GuildChannel): #: The topic of the channel. #: - #: :type: :obj:`str`, optional + #: :type: :obj:`~str`, optional topic: str = marshaller.attrib(deserializer=str, if_none=None) #: The ID of the last message sent in this channel. @@ -336,7 +336,7 @@ class GuildNewsChannel(GuildChannel): #: This might point to an invalid or deleted message. #: #: - #: :type: :obj:`hikari.snowflakes.Snowflake`, optional + #: :type: :obj:`~hikari.snowflakes.Snowflake`, optional last_message_id: snowflakes.Snowflake = marshaller.attrib( deserializer=snowflakes.Snowflake.deserialize, if_none=None ) @@ -357,12 +357,12 @@ class GuildVoiceChannel(GuildChannel): #: The bitrate for the voice channel (in bits). #: - #: :type: :obj:`int` + #: :type: :obj:`~int` bitrate: int = marshaller.attrib(deserializer=int) #: The user limit for the voice channel. #: - #: :type: :obj:`int` + #: :type: :obj:`~int` user_limit: int = marshaller.attrib(deserializer=int) @@ -373,7 +373,7 @@ class GuildChannelBuilder(entities.Serializable): ---------- channel_name : str The name to set for the channel. - channel_type : :obj:`ChannelType` + channel_type : :obj:`~ChannelType` The type of channel this should build. Example @@ -417,7 +417,7 @@ def with_permission_overwrites(self, overwrites: typing.Sequence[PermissionOverw Parameters ---------- - overwrites : :obj:`typing.Sequence` [ :obj:`PermissionOverwrite` ] + overwrites : :obj:`~typing.Sequence` [ :obj:`~PermissionOverwrite` ] A sequence of overwrite objects to add, where the first overwrite object """ @@ -429,7 +429,7 @@ def with_topic(self, topic: str) -> "GuildChannelBuilder": Parameters ---------- - topic : :obj:`str` + topic : :obj:`~str` The string topic to set. """ self._payload["topic"] = topic @@ -440,7 +440,7 @@ def with_bitrate(self, bitrate: int) -> "GuildChannelBuilder": Parameters ---------- - bitrate : :obj:`int` + bitrate : :obj:`~int` The bitrate to set in bits. """ self._payload["bitrate"] = int(bitrate) @@ -451,7 +451,7 @@ def with_user_limit(self, user_limit: int) -> "GuildChannelBuilder": Parameters ---------- - user_limit : :obj:`int` + user_limit : :obj:`~int` The user limit to set. """ self._payload["user_limit"] = int(user_limit) @@ -464,7 +464,7 @@ def with_rate_limit_per_user( Parameters ---------- - rate_limit_per_user : :obj:`typing.Union` [ :obj:`datetime.timedelta`, :obj:`int` ] + rate_limit_per_user : :obj:`~typing.Union` [ :obj:`~datetime.timedelta`, :obj:`~int` ] The amount of seconds users will have to wait before sending another message in the channel to set. """ @@ -480,7 +480,7 @@ def with_parent_category(self, category: typing.Union[snowflakes.Snowflake, int] Parameters ---------- - category : :obj:`typing.Union` [ :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ] + category : :obj:`~typing.Union` [ :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] The placeholder ID of the category channel that should be this channel's parent. """ @@ -497,7 +497,7 @@ def with_id(self, channel_id: typing.Union[snowflakes.Snowflake, int]) -> "Guild Parameters ---------- - channel_id : :obj:`typing.Union` [ :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ] + channel_id : :obj:`~typing.Union` [ :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] The placeholder ID to use. """ self._payload["id"] = str(int(channel_id)) diff --git a/hikari/clients/bot_clients.py b/hikari/clients/bot_clients.py index ccd8e9d142..b15e3819f7 100644 --- a/hikari/clients/bot_clients.py +++ b/hikari/clients/bot_clients.py @@ -42,7 +42,7 @@ class BotBase(runnable.RunnableClient, event_dispatchers.EventDispatcher): Parameters ---------- - config : :obj:`hikari.clients.configs.BotConfig` + config : :obj:`~hikari.clients.configs.BotConfig` The config object to use. event_manager : ``hikari.state.event_managers.EventManager`` The event manager to use. @@ -50,27 +50,27 @@ class BotBase(runnable.RunnableClient, event_dispatchers.EventDispatcher): #: The config for this bot. #: - #: :type: :obj:`hikari.clients.configs.BotConfig` + #: :type: :obj:`~hikari.clients.configs.BotConfig` config: configs.BotConfig #: The event manager for this bot. #: - #: :type: a subclass of :obj:`hikari.state.event_managers.EventManager` + #: :type: a subclass of :obj:`~hikari.state.event_managers.EventManager` event_manager: event_managers.EventManager #: The gateway for this bot. #: - #: :type: :obj:`hikari.clients.gateway_managers.GatewayManager` [ :obj:`hikari.clients.shard_clients.ShardClient` ] + #: :type: :obj:`~hikari.clients.gateway_managers.GatewayManager` [ :obj:`~hikari.clients.shard_clients.ShardClient` ] gateway: gateway_managers.GatewayManager[shard_clients.ShardClient] #: The logger to use for this bot. #: - #: :type: :obj:`logging.Logger` + #: :type: :obj:`~logging.Logger` logger: logging.Logger #: The REST HTTP client to use for this bot. #: - #: :type: :obj:`hikari.clients.rest_clients.RESTClient` + #: :type: :obj:`~hikari.clients.rest_clients.RESTClient` rest: rest_clients.RESTClient @abc.abstractmethod @@ -142,7 +142,7 @@ class StatelessBot(BotBase): Parameters ---------- - config : :obj:`hikari.clients.configs.BotConfig` + config : :obj:`~hikari.clients.configs.BotConfig` The config object to use. """ diff --git a/hikari/clients/configs.py b/hikari/clients/configs.py index 47e54e03bf..417cbfe048 100644 --- a/hikari/clients/configs.py +++ b/hikari/clients/configs.py @@ -58,7 +58,7 @@ class DebugConfig(BaseConfig): #: Whether to enable debugging mode. Usually you don't want to enable this. #: - #: :type: :obj:`bool` + #: :type: :obj:`~bool` debug: bool = marshaller.attrib(deserializer=bool, if_undefined=False, default=False) @@ -67,36 +67,36 @@ class DebugConfig(BaseConfig): class AIOHTTPConfig(BaseConfig): """Config for components that use AIOHTTP somewhere.""" - #: If :obj:`True`, allow following redirects from ``3xx`` HTTP responses. + #: If :obj:`~True`, allow following redirects from ``3xx`` HTTP responses. #: Generally you do not want to enable this unless you have a good reason #: to. #: - #: Defaults to :obj:`False` if unspecified during deserialization. + #: Defaults to :obj:`~False` if unspecified during deserialization. #: - #: :type: :obj:`bool` + #: :type: :obj:`~bool` allow_redirects: bool = marshaller.attrib(deserializer=bool, if_undefined=False, default=False) - #: Either an implementation of :obj:`aiohttp.TCPConnector`. + #: Either an implementation of :obj:`~aiohttp.TCPConnector`. #: - #: This may otherwise be :obj:`None` to use the default settings provided + #: This may otherwise be :obj:`~None` to use the default settings provided #: by :mod:`aiohttp`. #: #: This is deserialized as an object reference in the format #: ``package.module#object.attribute`` that is expected to point to the #: desired value. #: - #: Defaults to :obj:`None` if unspecified during deserialization. + #: Defaults to :obj:`~None` if unspecified during deserialization. #: - #: :type: :obj:`aiohttp.TCPConnector`, optional + #: :type: :obj:`~aiohttp.TCPConnector`, optional tcp_connector: typing.Optional[aiohttp.TCPConnector] = marshaller.attrib( deserializer=marshaller.dereference_handle, if_none=None, if_undefined=None, default=None ) #: Optional proxy headers to provide in any HTTP requests. #: - #: Defaults to :obj:`None` if unspecified during deserialization. + #: Defaults to :obj:`~None` if unspecified during deserialization. #: - #: :type: :obj:`typing.Mapping` [ :obj:`str`, :obj:`str` ], optional + #: :type: :obj:`~typing.Mapping` [ :obj:`~str`, :obj:`~str` ], optional proxy_headers: typing.Optional[typing.Mapping[str, str]] = marshaller.attrib( deserializer=dict, if_none=None, if_undefined=None, default=None ) @@ -105,29 +105,29 @@ class AIOHTTPConfig(BaseConfig): #: #: This is deserialized using the format ``"basic {{base 64 string here}}"``. #: - #: Defaults to :obj:`None` if unspecified during deserialization. + #: Defaults to :obj:`~None` if unspecified during deserialization. #: - #: :type: :obj:`aiohttp.BasicAuth`, optional + #: :type: :obj:`~aiohttp.BasicAuth`, optional proxy_auth: typing.Optional[aiohttp.BasicAuth] = marshaller.attrib( deserializer=aiohttp.BasicAuth.decode, if_none=None, if_undefined=None, default=None ) #: The optional URL of the proxy to send requests via. #: - #: Defaults to :obj:`None` if unspecified during deserialization. + #: Defaults to :obj:`~None` if unspecified during deserialization. #: - #: :type: :obj:`str`, optional + #: :type: :obj:`~str`, optional proxy_url: typing.Optional[str] = marshaller.attrib(deserializer=str, if_undefined=None, if_none=None, default=None) #: Optional request timeout to use. If an HTTP request takes longer than #: this, it will be aborted. #: - #: If not :obj:`None`, the value represents a number of seconds as a + #: If not :obj:`~None`, the value represents a number of seconds as a #: floating point number. #: - #: Defaults to :obj:`None` if unspecified during deserialization. + #: Defaults to :obj:`~None` if unspecified during deserialization. #: - #: :type: :obj:`float`, optional + #: :type: :obj:`~float`, optional request_timeout: typing.Optional[float] = marshaller.attrib( deserializer=float, if_undefined=None, if_none=None, default=None ) @@ -138,22 +138,22 @@ class AIOHTTPConfig(BaseConfig): #: ``package.module#object.attribute`` that is expected to point to the #: desired value. #: - #: Defaults to :obj:`None` if unspecified during deserialization. + #: Defaults to :obj:`~None` if unspecified during deserialization. #: - #: :type: :obj:`ssl.SSLContext`, optional + #: :type: :obj:`~ssl.SSLContext`, optional ssl_context: typing.Optional[ssl.SSLContext] = marshaller.attrib( deserializer=marshaller.dereference_handle, if_none=None, if_undefined=None, default=None ) - #: If :obj:`True`, then responses with invalid SSL certificates will be + #: If :obj:`~True`, then responses with invalid SSL certificates will be #: rejected. Generally you want to keep this enabled unless you have a #: problem with SSL and you know exactly what you are doing by disabling #: this. Disabling SSL verification can have major security implications. #: You turn this off at your own risk. #: - #: Defaults to :obj:`True` if unspecified during deserialization. + #: Defaults to :obj:`~True` if unspecified during deserialization. #: - #: :type: :obj:`bool` + #: :type: :obj:`~bool` verify_ssl: bool = marshaller.attrib(deserializer=bool, if_undefined=True, default=True) @@ -164,7 +164,7 @@ class TokenConfig(BaseConfig): #: The token to use. #: - #: :type: :obj:`str`, optional + #: :type: :obj:`~str`, optional token: typing.Optional[str] = marshaller.attrib(deserializer=str, if_none=None, if_undefined=None, default=None) @@ -176,27 +176,27 @@ class WebsocketConfig(AIOHTTPConfig, TokenConfig, DebugConfig): #: Whether to use zlib compression on the gateway for inbound messages or #: not. Usually you want this turned on. #: - #: :type: :obj:`bool` + #: :type: :obj:`~bool` gateway_use_compression: bool = marshaller.attrib(deserializer=bool, if_undefined=True, default=True) #: The gateway API version to use. #: #: If unspecified, then V6 is used. #: - #: :type: :obj:`int` + #: :type: :obj:`~int` gateway_version: int = marshaller.attrib(deserializer=int, if_undefined=lambda: 6, default=6) #: The initial activity to set all shards to when starting the gateway. If - #: :obj:`None`, then no activity will be set. + #: :obj:`~None`, then no activity will be set. #: - #: :type: :obj:`hikari.gateway_entities.GatewayActivity`, optional + #: :type: :obj:`~hikari.gateway_entities.GatewayActivity`, optional initial_activity: typing.Optional[gateway_entities.GatewayActivity] = marshaller.attrib( deserializer=gateway_entities.GatewayActivity.deserialize, if_none=None, if_undefined=None, default=None ) #: The initial status to set the shards to when starting the gateway. #: - #: :type: :obj:`hikari.guilds.PresenceStatus` + #: :type: :obj:`~hikari.guilds.PresenceStatus` initial_status: guilds.PresenceStatus = marshaller.attrib( deserializer=guilds.PresenceStatus, if_undefined=lambda: guilds.PresenceStatus.ONLINE, @@ -205,13 +205,13 @@ class WebsocketConfig(AIOHTTPConfig, TokenConfig, DebugConfig): #: Whether to show up as AFK or not on sign-in. #: - #: :type: :obj:`bool` + #: :type: :obj:`~bool` initial_is_afk: bool = marshaller.attrib(deserializer=bool, if_undefined=False, default=False) - #: The idle time to show on signing in, or :obj:`None` to not show an idle + #: The idle time to show on signing in, or :obj:`~None` to not show an idle #: time. #: - #: :type: :obj:`datetime.datetime`, optional + #: :type: :obj:`~datetime.datetime`, optional initial_idle_since: typing.Optional[datetime.datetime] = marshaller.attrib( deserializer=datetime.datetime.fromtimestamp, if_none=None, if_undefined=None, default=None ) @@ -220,7 +220,7 @@ class WebsocketConfig(AIOHTTPConfig, TokenConfig, DebugConfig): #: #: If being deserialized, this can be an integer bitfield, or a sequence of #: intent names. If - #: unspecified, this will be set to :obj:`None`. + #: unspecified, this will be set to :obj:`~None`. #: #: Examples #: -------- @@ -237,7 +237,7 @@ class WebsocketConfig(AIOHTTPConfig, TokenConfig, DebugConfig): #: // JSON example, using an array of names #: [ "GUILDS", "GUILD_MESSAGES" ] #: - #: See :obj:`hikari.net.codes.GatewayIntent` for valid names of + #: See :obj:`~hikari.net.codes.GatewayIntent` for valid names of #: intents you can use. Integer values are as documented on Discord's #: developer portal. #: @@ -249,10 +249,10 @@ class WebsocketConfig(AIOHTTPConfig, TokenConfig, DebugConfig): #: will require you to whitelist your application in order to use them. #: #: If you are using the V6 gateway implementation, setting this to - #: :obj:`None` will simply opt you into every event you can subscribe to. + #: :obj:`~None` will simply opt you into every event you can subscribe to. #: #: - #: :type: :obj:`hikari.net.codes.GatewayIntent`, optional + #: :type: :obj:`~hikari.net.codes.GatewayIntent`, optional intents: typing.Optional[codes.GatewayIntent] = marshaller.attrib( deserializer=lambda value: conversions.dereference_int_flag(codes.GatewayIntent, value), if_undefined=None, @@ -261,7 +261,7 @@ class WebsocketConfig(AIOHTTPConfig, TokenConfig, DebugConfig): #: The large threshold to use. #: - #: :type: :obj:`int` + #: :type: :obj:`~int` large_threshold: int = marshaller.attrib(deserializer=int, if_undefined=lambda: 250, default=250) @@ -303,12 +303,12 @@ class ShardConfig(BaseConfig): #: ``"5...16"``: #: A range string. Three periods indicate a range of #: ``[5, 17]`` (inclusive beginning, inclusive end). - #: :obj:`None`: + #: :obj:`~None`: #: The ``shard_count`` will be considered and that many shards will - #: be created for you. If the ``shard_count`` is also :obj:`None`, + #: be created for you. If the ``shard_count`` is also :obj:`~None`, #: then auto-sharding will be performed for you. #: - #: :type: :obj:`typing.Sequence` [ :obj:`int` ], optional + #: :type: :obj:`~typing.Sequence` [ :obj:`~int` ], optional shard_ids: typing.Optional[typing.Sequence[int]] = marshaller.attrib( deserializer=_parse_shard_info, if_none=None, if_undefined=None, default=None ) @@ -319,7 +319,7 @@ class ShardConfig(BaseConfig): #: #: This can be set to `None` to enable auto-sharding. This is the default. #: - #: :type: :obj:`int`, optional. + #: :type: :obj:`~int`, optional. shard_count: typing.Optional[int] = marshaller.attrib(deserializer=int, if_undefined=None, default=None) @@ -334,7 +334,7 @@ class RESTConfig(AIOHTTPConfig, TokenConfig): #: #: Defaults to ``"Bot"`` #: - #: :type: :obj:`str` + #: :type: :obj:`~str` token_type: typing.Optional[str] = marshaller.attrib( deserializer=str, if_undefined=lambda: "Bot", if_none=None, default="Bot" ) @@ -343,7 +343,7 @@ class RESTConfig(AIOHTTPConfig, TokenConfig): #: #: If unspecified, then V7 is used. #: - #: :type: :obj:`int` + #: :type: :obj:`~int` rest_version: int = marshaller.attrib(deserializer=int, if_undefined=lambda: 7, default=7) diff --git a/hikari/clients/gateway_managers.py b/hikari/clients/gateway_managers.py index 7c7ac35339..2a6b0e8cc3 100644 --- a/hikari/clients/gateway_managers.py +++ b/hikari/clients/gateway_managers.py @@ -16,7 +16,7 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . -"""Defines a facade around :obj:`hikari.clients.shard_clients.ShardClient`. +"""Defines a facade around :obj:`~hikari.clients.shard_clients.ShardClient`. This provides functionality such as keeping multiple shards alive """ @@ -77,14 +77,14 @@ def latency(self) -> float: This will return a mean of all the heartbeat intervals for all shards with a valid heartbeat latency that are in the - :obj:`hikari.clients.shard_clients.ShardState.READY` state. + :obj:`~hikari.clients.shard_clients.ShardState.READY` state. If no shards are in this state, this will return ``float('nan')`` instead. Returns ------- - :obj:`float` + :obj:`~float` The mean latency for all ``READY`` shards that have sent at least one acknowledged ``HEARTBEAT`` payload. If there is not at least one shard that meets this criteria, this will instead return @@ -170,21 +170,21 @@ async def update_presence( Notes ----- If you wish to update a presence for a specific shard, you can do this - by using the ``shards`` :obj:`typing.Mapping` to find the shard you + by using the ``shards`` :obj:`~typing.Mapping` to find the shard you wish to update. Parameters ---------- - status : :obj:`hikari.guilds.PresenceStatus` + status : :obj:`~hikari.guilds.PresenceStatus` If specified, the new status to set. - activity : :obj:`hikari.gateway_entities.GatewayActivity`, optional + activity : :obj:`~hikari.gateway_entities.GatewayActivity`, optional If specified, the new activity to set. - idle_since : :obj:`datetime.datetime`, optional + idle_since : :obj:`~datetime.datetime`, optional If specified, the time to show up as being idle since, - or :obj:`None` if not applicable. - is_afk : :obj:`bool` - If specified, :obj:`True` if the user should be marked as AFK, - or :obj:`False` otherwise. + or :obj:`~None` if not applicable. + is_afk : :obj:`~bool` + If specified, :obj:`~True` if the user should be marked as AFK, + or :obj:`~False` otherwise. """ await asyncio.gather( *( diff --git a/hikari/clients/rest_clients.py b/hikari/clients/rest_clients.py index e756556d79..6014f76005 100644 --- a/hikari/clients/rest_clients.py +++ b/hikari/clients/rest_clients.py @@ -62,12 +62,12 @@ class RESTClient: A marshalling object-oriented HTTP API. This component bridges the basic HTTP API exposed by - :obj:`hikari.net.rest.LowLevelRestfulClient` and wraps it in a unit of + :obj:`~hikari.net.rest.LowLevelRestfulClient` and wraps it in a unit of processing that can handle parsing API objects into Hikari entity objects. Parameters ---------- - config : :obj:`hikari.clients.configs.RESTConfig` + config : :obj:`~hikari.clients.configs.RESTConfig` A HTTP configuration object. Note @@ -107,7 +107,7 @@ async def fetch_gateway_url(self) -> str: Returns ------- - :obj:`str` + :obj:`~str` A static URL to use to connect to the gateway with. Note @@ -121,7 +121,7 @@ async def fetch_gateway_bot(self) -> gateway_entities.GatewayBot: Returns ------- - :obj:`hikari.gateway_entities.GatewayBot` + :obj:`~hikari.gateway_entities.GatewayBot` The bot specific gateway information object. Note @@ -144,33 +144,33 @@ async def fetch_audit_log( Parameters ---------- - guild : :obj:`typing.Union` [ :obj:`hikari.guilds.Guild`, :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ] + guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] The object or ID of the guild to get the audit logs for. - user : :obj:`typing.Union` [ :obj:`hikari.users.User`, :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ] + user : :obj:`~typing.Union` [ :obj:`~hikari.users.User`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] If specified, the object or ID of the user to filter by. - action_type : :obj:`typing.Union` [ :obj:`hikari.audit_logs.AuditLogEventType`, :obj:`int` ] + action_type : :obj:`~typing.Union` [ :obj:`~hikari.audit_logs.AuditLogEventType`, :obj:`~int` ] If specified, the action type to look up. Passing a raw integer for this may lead to unexpected behaviour. - limit : :obj:`int` + limit : :obj:`~int` If specified, the limit to apply to the number of records. Defaults to ``50``. Must be between ``1`` and ``100`` inclusive. - before : :obj:`typing.Union` [ :obj:`datetime.datetime`, :obj:`hikari.audit_logs.AuditLogEntry`, :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ] + before : :obj:`~typing.Union` [ :obj:`~datetime.datetime`, :obj:`~hikari.audit_logs.AuditLogEntry`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] If specified, the object or ID of the entry that all retrieved entries should have occurred befor. Returns ------- - :obj:`hikari.audit_logs.AuditLog` + :obj:`~hikari.audit_logs.AuditLog` An audit log object. Raises ------ - :obj:`hikari.errors.BadRequestHTTPError` + :obj:`~hikari.errors.BadRequestHTTPError` If any invalid snowflake IDs are passed; a snowflake may be invalid due to it being outside of the range of a 64 bit integer. - :obj:`hikari.errors.ForbiddenHTTPError` + :obj:`~hikari.errors.ForbiddenHTTPError` If you lack the given permissions to view an audit log. - :obj:`hikari.errors.NotFoundHTTPError` + :obj:`~hikari.errors.NotFoundHTTPError` If the guild does not exist. """ if isinstance(before, datetime.datetime): @@ -204,18 +204,18 @@ def fetch_audit_log_entries_before( Parameters ---------- - guild : :obj:`typing.Union` [ :obj:`hikari.guilds.Guild`, :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ] + guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] The ID or object of the guild to get audit log entries for - before : :obj:`typing.Union` [ :obj:`datetime.datetime`, :obj:`hikari.audit_logs.AuditLogEntry`, :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ], optional + before : :obj:`~typing.Union` [ :obj:`~datetime.datetime`, :obj:`~hikari.audit_logs.AuditLogEntry`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ], optional If specified, the ID or object of the entry or datetime to get entries that happened before otherwise this will start from the newest entry. - user : :obj:`typing.Union` [ :obj:`hikari.users.User`, :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ] + user : :obj:`~typing.Union` [ :obj:`~hikari.users.User`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] If specified, the object or ID of the user to filter by. - action_type : :obj:`typing.Union` [ :obj:`hikari.audit_logs.AuditLogEventType`, :obj:`int` ] + action_type : :obj:`~typing.Union` [ :obj:`~hikari.audit_logs.AuditLogEventType`, :obj:`~int` ] If specified, the action type to look up. Passing a raw integer for this may lead to unexpected behaviour. - limit : :obj:`int`, optional + limit : :obj:`~int`, optional If specified, the limit for how many entries this iterator should return, defaults to unlimited. @@ -238,10 +238,10 @@ def fetch_audit_log_entries_before( Returns ------- - :obj:`hikari.errors.BadRequestHTTPError` + :obj:`~hikari.errors.BadRequestHTTPError` If any invalid snowflake IDs are passed; a snowflake may be invalid due to it being outside of the range of a 64 bit integer. - :obj:`hikari.audit_logs.AuditLogIterator` + :obj:`~hikari.audit_logs.AuditLogIterator` An async iterator of the audit log entries in a guild (from newest to oldest). """ @@ -265,22 +265,22 @@ async def fetch_channel(self, channel: snowflakes.HashableT[_channels.Channel]) Parameters ---------- - channel : :obj:`typing.Union` [ :obj:`hikari.channels.Channel`, :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ] + channel : :obj:`~typing.Union` [ :obj:`~hikari.channels.Channel`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] The object ID of the channel to look up. Returns ------- - :obj:`hikari.channels.Channel` + :obj:`~hikari.channels.Channel` The channel object that has been found. Raises ------ - :obj:`hikari.errors.BadRequestHTTPError` + :obj:`~hikari.errors.BadRequestHTTPError` If any invalid snowflake IDs are passed; a snowflake may be invalid due to it being outside of the range of a 64 bit integer. - :obj:`hikari.errors.ForbiddenHTTPError` + :obj:`~hikari.errors.ForbiddenHTTPError` If you don't have access to the channel. - :obj:`hikari.errors.NotFoundHTTPError` + :obj:`~hikari.errors.NotFoundHTTPError` If the channel does not exist. """ payload = await self._session.get_channel( @@ -307,57 +307,57 @@ async def update_channel( Parameters ---------- - channel : :obj:`typing.Union` [ :obj:`hikari.channels.Channel`, :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ] + channel : :obj:`~typing.Union` [ :obj:`~hikari.channels.Channel`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] The channel ID to update. - name : :obj:`str` + name : :obj:`~str` If specified, the new name for the channel. This must be inclusively between ``1`` and ``100`` characters in length. - position : :obj:`int` + position : :obj:`~int` If specified, the position to change the channel to. - topic : :obj:`str` + topic : :obj:`~str` If specified, the topic to set. This is only applicable to text channels. This must be inclusively between ``0`` and ``1024`` characters in length. - nsfw : :obj:`bool` - Mark the channel as being not safe for work (NSFW) if :obj:`True`. - If :obj:`False` or unspecified, then the channel is not marked as + nsfw : :obj:`~bool` + Mark the channel as being not safe for work (NSFW) if :obj:`~True`. + If :obj:`~False` or unspecified, then the channel is not marked as NSFW. Will have no visible effect for non-text guild channels. - rate_limit_per_user : :obj:`typing.Union` [ :obj:`int`, :obj:`datetime.timedelta` ] + rate_limit_per_user : :obj:`~typing.Union` [ :obj:`~int`, :obj:`~datetime.timedelta` ] If specified, the time delta of seconds the user has to wait before sending another message. This will not apply to bots, or to members with ``MANAGE_MESSAGES`` or ``MANAGE_CHANNEL`` permissions. This must be inclusively between ``0`` and ``21600`` seconds. - bitrate : :obj:`int` + bitrate : :obj:`~int` If specified, the bitrate in bits per second allowable for the channel. This only applies to voice channels and must be inclusively between ``8000`` and ``96000`` for normal servers or ``8000`` and ``128000`` for VIP servers. - user_limit : :obj:`int` + user_limit : :obj:`~int` If specified, the new max number of users to allow in a voice channel. This must be between ``0`` and ``99`` inclusive, where ``0`` implies no limit. - permission_overwrites : :obj:`typing.Sequence` [ :obj:`hikari.channels.PermissionOverwrite` ] + permission_overwrites : :obj:`~typing.Sequence` [ :obj:`~hikari.channels.PermissionOverwrite` ] If specified, the new list of permission overwrites that are category specific to replace the existing overwrites with. - parent_category : :obj:`typing.Union` [ :obj:`hikari.channels.Channel`, :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ], optional + parent_category : :obj:`~typing.Union` [ :obj:`~hikari.channels.Channel`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ], optional If specified, the new parent category ID to set for the channel, - pass :obj:`None` to unset. - reason : :obj:`str` + pass :obj:`~None` to unset. + reason : :obj:`~str` If specified, the audit log reason explaining why the operation was performed. Returns ------- - :obj:`hikari.channels.Channel` + :obj:`~hikari.channels.Channel` The channel object that has been modified. Raises ------ - :obj:`hikari.errors.NotFoundHTTPError` + :obj:`~hikari.errors.NotFoundHTTPError` If the channel does not exist. - :obj:`hikari.errors.ForbiddenHTTPError` + :obj:`~hikari.errors.ForbiddenHTTPError` If you lack the permission to make the change. - :obj:`hikari.errors.BadRequestHTTPError` + :obj:`~hikari.errors.BadRequestHTTPError` If you provide incorrect options for the corresponding channel type (e.g. a ``bitrate`` for a text channel). If any invalid snowflake IDs are passed; a snowflake may be invalid @@ -395,24 +395,24 @@ async def delete_channel(self, channel: snowflakes.HashableT[_channels.Channel]) Parameters ---------- - channel : :obj:`typing.Union` [ :obj:`hikari.channels.Channel`, :obj:`hikari.snowflakes.Snowflake` :obj:`str` ] + channel : :obj:`~typing.Union` [ :obj:`~hikari.channels.Channel`, :obj:`~hikari.snowflakes.Snowflake` :obj:`~str` ] The object or ID of the channel to delete. Returns ------- - :obj:`None` + :obj:`~None` Nothing, unlike what the API specifies. This is done to maintain consistency with other calls of a similar nature in this API wrapper. Raises ------ - :obj:`hikari.errors.BadRequestHTTPError` + :obj:`~hikari.errors.BadRequestHTTPError` If any invalid snowflake IDs are passed; a snowflake may be invalid due to it being outside of the range of a 64 bit integer. - :obj:`hikari.errors.NotFoundHTTPError` + :obj:`~hikari.errors.NotFoundHTTPError` If the channel does not exist. - :obj:`hikari.errors.ForbiddenHTTPError` + :obj:`~hikari.errors.ForbiddenHTTPError` If you do not have permission to delete the channel. Note @@ -443,12 +443,12 @@ def fetch_messages_after( Parameters ---------- - channel : :obj:`typing.Union` [ :obj:`hikari.channels.Channel`, :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ] + channel : :obj:`~typing.Union` [ :obj:`~hikari.channels.Channel`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] The ID of the channel to retrieve the messages from. - limit : :obj:`int` + limit : :obj:`~int` If specified, the maximum number of how many messages this iterator should return. - after : :obj:`typing.Union` [ :obj:`datetime.datetime`, :obj:`hikari.channels.Channel`, :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ] + after : :obj:`~typing.Union` [ :obj:`~datetime.datetime`, :obj:`~hikari.channels.Channel`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] A object or ID message. Only return messages sent AFTER this message if it's specified else this will return every message after (and including) the first message in the channel. @@ -463,24 +463,24 @@ def fetch_messages_after( Returns ------- - :obj:`typing.AsyncIterator` [ :obj:`hikari.messages.Message` ] + :obj:`~typing.AsyncIterator` [ :obj:`~hikari.messages.Message` ] An async iterator that retrieves the channel's message objects. Raises ------ - :obj:`hikari.errors.BadRequestHTTPError` + :obj:`~hikari.errors.BadRequestHTTPError` If any invalid snowflake IDs are passed; a snowflake may be invalid due to it being outside of the range of a 64 bit integer. - :obj:`hikari.errors.ForbiddenHTTPError` + :obj:`~hikari.errors.ForbiddenHTTPError` If you lack permission to read the channel. - :obj:`hikari.errors.NotFoundHTTPError` + :obj:`~hikari.errors.NotFoundHTTPError` If the channel is not found, or the message provided for one of the filter arguments is not found. Note ---- If you are missing the ``VIEW_CHANNEL`` permission, you will receive a - :obj:`hikari.errors.ForbiddenHTTPError`. If you are instead missing + :obj:`~hikari.errors.ForbiddenHTTPError`. If you are instead missing the ``READ_MESSAGE_HISTORY`` permission, you will always receive zero results, and thus an empty list will be returned instead. """ @@ -512,12 +512,12 @@ def fetch_messages_before( Parameters ---------- - channel : :obj:`typing.Union` [ :obj:`hikari.channels.Channel`, :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ] + channel : :obj:`~typing.Union` [ :obj:`~hikari.channels.Channel`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] The ID of the channel to retrieve the messages from. - limit : :obj:`int` + limit : :obj:`~int` If specified, the maximum number of how many messages this iterator should return. - before : :obj:`typing.Union` [ :obj:`datetime.datetime`, :obj:`hikari.channels.Channel`, :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ] + before : :obj:`~typing.Union` [ :obj:`~datetime.datetime`, :obj:`~hikari.channels.Channel`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] A message object or ID. Only return messages sent BEFORE this message if this is specified else this will return every message before (and including) the most recent message in the @@ -533,24 +533,24 @@ def fetch_messages_before( Returns ------- - :obj:`typing.AsyncIterator` [ :obj:`hikari.messages.Message` ] + :obj:`~typing.AsyncIterator` [ :obj:`~hikari.messages.Message` ] An async iterator that retrieves the channel's message objects. Raises ------ - :obj:`hikari.errors.BadRequestHTTPError` + :obj:`~hikari.errors.BadRequestHTTPError` If any invalid snowflake IDs are passed; a snowflake may be invalid due to it being outside of the range of a 64 bit integer. - :obj:`hikari.errors.ForbiddenHTTPError` + :obj:`~hikari.errors.ForbiddenHTTPError` If you lack permission to read the channel. - :obj:`hikari.errors.NotFoundHTTPError` + :obj:`~hikari.errors.NotFoundHTTPError` If the channel is not found, or the message provided for one of the filter arguments is not found. Note ---- If you are missing the ``VIEW_CHANNEL`` permission, you will receive a - :obj:`hikari.errors.ForbiddenHTTPError`. If you are instead missing + :obj:`~hikari.errors.ForbiddenHTTPError`. If you are instead missing the ``READ_MESSAGE_HISTORY`` permission, you will always receive zero results, and thus an empty list will be returned instead. """ @@ -583,14 +583,14 @@ async def fetch_messages_around( Parameters ---------- - channel : :obj:`typing.Union` [ :obj:`hikari.channels.Channel`, :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ] + channel : :obj:`~typing.Union` [ :obj:`~hikari.channels.Channel`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] The ID of the channel to retrieve the messages from. - around : :obj:`typing.Union` [ :obj:`datetime.datetime`, :obj:`hikari.channels.Channel`, :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ] + around : :obj:`~typing.Union` [ :obj:`~datetime.datetime`, :obj:`~hikari.channels.Channel`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] The object or ID of the message to get messages that were sent AROUND it in the provided channel, unlike ``before`` and ``after``, this argument is required and the provided message will also be returned if it still exists. - limit : :obj:`int` + limit : :obj:`~int` If specified, the maximum number of how many messages this iterator should return, cannot be more than `100` @@ -604,24 +604,24 @@ async def fetch_messages_around( Returns ------- - :obj:`typing.AsyncIterator` [ :obj:`hikari.messages.Message` ] + :obj:`~typing.AsyncIterator` [ :obj:`~hikari.messages.Message` ] An async iterator that retrieves the found message objects. Raises ------ - :obj:`hikari.errors.BadRequestHTTPError` + :obj:`~hikari.errors.BadRequestHTTPError` If any invalid snowflake IDs are passed; a snowflake may be invalid due to it being outside of the range of a 64 bit integer. - :obj:`hikari.errors.ForbiddenHTTPError` + :obj:`~hikari.errors.ForbiddenHTTPError` If you lack permission to read the channel. - :obj:`hikari.errors.NotFoundHTTPError` + :obj:`~hikari.errors.NotFoundHTTPError` If the channel is not found, or the message provided for one of the filter arguments is not found. Note ---- If you are missing the ``VIEW_CHANNEL`` permission, you will receive a - :obj:`hikari.errors.ForbiddenHTTPError`. If you are instead missing + :obj:`~hikari.errors.ForbiddenHTTPError`. If you are instead missing the ``READ_MESSAGE_HISTORY`` permission, you will always receive zero results, and thus an empty list will be returned instead. """ @@ -653,34 +653,34 @@ async def _pagination_handler( Parameters ---------- - deserializer : :obj:`typing.Callable` [ [ :obj:`typing.Any` ], :obj:`typing.Any` ] + deserializer : :obj:`~typing.Callable` [ [ :obj:`~typing.Any` ], :obj:`~typing.Any` ] The deserializer to use to deserialize raw elements. - direction : :obj:`typing.Union` [ ``"before"``, ``"after"`` ] + direction : :obj:`~typing.Union` [ ``"before"``, ``"after"`` ] The direction that this paginator should go in. - request : :obj:`typing.Callable` [ ``...``, :obj:`typing.Coroutine` [ :obj:`typing.Any`, :obj:`typing.Any`, :obj:`typing.Any` ] ] + request : :obj:`~typing.Callable` [ ``...``, :obj:`~typing.Coroutine` [ :obj:`~typing.Any`, :obj:`~typing.Any`, :obj:`~typing.Any` ] ] The function on :attr:`_session` that should be called to make requests for this paginator. - reversing : :obj:`bool` + reversing : :obj:`~bool` Whether the retrieved array of objects should be reversed before iterating through it, this is needed for certain endpoints like ``fetch_messages_before`` where the order is static regardless of if you're using ``before`` or ``after``. - start : :obj:`int`, optional + start : :obj:`~int`, optional The snowflake ID that this paginator should start at, ``0`` may be passed for ``forward`` pagination to start at the first created - entity and :obj:`None` may be passed for ``before`` pagination to + entity and :obj:`~None` may be passed for ``before`` pagination to start at the newest entity (based on when it's snowflake timestamp). - limit : :obj:`int`, optional + limit : :obj:`~int`, optional The amount of deserialized entities that the iterator should return - total, will be unlimited if set to :obj:`None`. - id_getter : :obj:`typing.Callable` [ [ :obj:`typing.Any` ], :obj:`str` ] + total, will be unlimited if set to :obj:`~None`. + id_getter : :obj:`~typing.Callable` [ [ :obj:`~typing.Any` ], :obj:`~str` ] **kwargs Kwargs to pass through to ``request`` for every request made along with the current decided limit and direction snowflake. Returns ------- - :obj:`typing.AsyncIterator` [ :obj:`typing.Any` ] + :obj:`~typing.AsyncIterator` [ :obj:`~typing.Any` ] An async iterator of the found deserialized found objects. """ @@ -708,14 +708,14 @@ async def fetch_message( Parameters ---------- - channel : :obj:`typing.Union` [ :obj:`hikari.channels.Channel`, :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ] + channel : :obj:`~typing.Union` [ :obj:`~hikari.channels.Channel`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] The object or ID of the channel to get the message from. - message : :obj:`typing.Union` [ :obj:`hikari.messages.Message`, :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ] + message : :obj:`~typing.Union` [ :obj:`~hikari.messages.Message`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] The object or ID of the message to retrieve. Returns ------- - :obj:`hikari.messages.Message` + :obj:`~hikari.messages.Message` The found message object. Note @@ -724,12 +724,12 @@ async def fetch_message( Raises ------ - :obj:`hikari.errors.BadRequestHTTPError` + :obj:`~hikari.errors.BadRequestHTTPError` If any invalid snowflake IDs are passed; a snowflake may be invalid due to it being outside of the range of a 64 bit integer. - :obj:`hikari.errors.ForbiddenHTTPError` + :obj:`~hikari.errors.ForbiddenHTTPError` If you lack permission to see the message. - :obj:`hikari.errors.NotFoundHTTPError` + :obj:`~hikari.errors.NotFoundHTTPError` If the channel or message is not found. """ payload = await self._session.get_channel_message( @@ -748,21 +748,21 @@ def _generate_allowed_mentions( Parameters ---------- - mentions_everyone : :obj:`bool` + mentions_everyone : :obj:`~bool` Whether ``@everyone`` and ``@here`` mentions should be resolved by discord and lead to actual pings. - user_mentions : :obj:`typing.Union` [ :obj:`typing.Collection` [ :obj:`typing.Union` [ :obj:`hikari.users.User`, :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ], :obj:`bool` ] + user_mentions : :obj:`~typing.Union` [ :obj:`~typing.Collection` [ :obj:`~typing.Union` [ :obj:`~hikari.users.User`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ], :obj:`~bool` ] Either an array of user objects/IDs to allow mentions for, - :obj:`True` to allow all user mentions or :obj:`False` to block all + :obj:`~True` to allow all user mentions or :obj:`~False` to block all user mentions from resolving. - role_mentions : :obj:`typing.Union` [ :obj:`typing.Collection` [ :obj:`typing.Union` [ :obj:`hikari.guilds.GuildRole`, :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ] ], :obj:`bool` ] + role_mentions : :obj:`~typing.Union` [ :obj:`~typing.Collection` [ :obj:`~typing.Union` [ :obj:`~hikari.guilds.GuildRole`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] ], :obj:`~bool` ] Either an array of guild role objects/IDs to allow mentions for, - :obj:`True` to allow all role mentions or :obj:`False` to block all + :obj:`~True` to allow all role mentions or :obj:`~False` to block all role mentions from resolving. Returns ------- - :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Sequence` [ :obj:`str` ] ] + :obj:`~typing.Dict` [ :obj:`~str`, :obj:`~typing.Sequence` [ :obj:`~str` ] ] The resulting allowed mentions dict object. """ parsed_mentions = [] @@ -817,50 +817,50 @@ async def create_message( Parameters ---------- - channel : :obj:`typing.Union` [ :obj:`hikari.channels.Channel`, :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ] + channel : :obj:`~typing.Union` [ :obj:`~hikari.channels.Channel`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] The channel or ID of the channel to send to. - content : :obj:`str` + content : :obj:`~str` If specified, the message content to send with the message. - nonce : :obj:`str` + nonce : :obj:`~str` If specified, an optional ID to send for opportunistic message creation. This doesn't serve any real purpose for general use, and can usually be ignored. - tts : :obj:`bool` + tts : :obj:`~bool` If specified, whether the message will be sent as a TTS message. - files : :obj:`typing.Collection` [ ``hikari.media.IO`` ] + files : :obj:`~typing.Collection` [ ``hikari.media.IO`` ] If specified, this should be a list of inclusively between ``1`` and ``5`` IO like media objects, as defined in :mod:`hikari.media`. - embed : :obj:`hikari.embeds.Embed` + embed : :obj:`~hikari.embeds.Embed` If specified, the embed object to send with the message. - mentions_everyone : :obj:`bool` + mentions_everyone : :obj:`~bool` Whether ``@everyone`` and ``@here`` mentions should be resolved by - discord and lead to actual pings, defaults to :obj:`True`. - user_mentions : :obj:`typing.Union` [ :obj:`typing.Collection` [ :obj:`typing.Union` [ :obj:`hikari.users.User`, :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ], :obj:`bool` ] + discord and lead to actual pings, defaults to :obj:`~True`. + user_mentions : :obj:`~typing.Union` [ :obj:`~typing.Collection` [ :obj:`~typing.Union` [ :obj:`~hikari.users.User`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ], :obj:`~bool` ] Either an array of user objects/IDs to allow mentions for, - :obj:`True` to allow all user mentions or :obj:`False` to block all - user mentions from resolving, defaults to :obj:`True`. - role_mentions : :obj:`typing.Union` [ :obj:`typing.Collection` [ :obj:`typing.Union` [ :obj:`hikari.guilds.GuildRole`, :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ] ], :obj:`bool` ] + :obj:`~True` to allow all user mentions or :obj:`~False` to block all + user mentions from resolving, defaults to :obj:`~True`. + role_mentions : :obj:`~typing.Union` [ :obj:`~typing.Collection` [ :obj:`~typing.Union` [ :obj:`~hikari.guilds.GuildRole`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] ], :obj:`~bool` ] Either an array of guild role objects/IDs to allow mentions for, - :obj:`True` to allow all role mentions or :obj:`False` to block all - role mentions from resolving, defaults to :obj:`True`. + :obj:`~True` to allow all role mentions or :obj:`~False` to block all + role mentions from resolving, defaults to :obj:`~True`. Returns ------- - :obj:`hikari.messages.Message` + :obj:`~hikari.messages.Message` The created message object. Raises ------ - :obj:`hikari.errors.NotFoundHTTPError` + :obj:`~hikari.errors.NotFoundHTTPError` If the channel is not found. - :obj:`hikari.errors.BadRequestHTTPError` + :obj:`~hikari.errors.BadRequestHTTPError` This can be raised if the file is too large; if the embed exceeds the defined limits; if the message content is specified only and empty or greater than ``2000`` characters; if neither content, files or embed are specified. If any invalid snowflake IDs are passed; a snowflake may be invalid due to it being outside of the range of a 64 bit integer. - :obj:`hikari.errors.ForbiddenHTTPError` + :obj:`~hikari.errors.ForbiddenHTTPError` If you lack permissions to send to this channel. """ payload = await self._session.create_message( @@ -893,7 +893,7 @@ def safe_create_message( This endpoint has the same signature as :attr:`create_message` with the only difference being that ``mentions_everyone``, - ``user_mentions`` and ``role_mentions`` default to :obj:`False`. + ``user_mentions`` and ``role_mentions`` default to :obj:`~False`. """ return self.create_message( channel=channel, @@ -917,11 +917,11 @@ async def create_reaction( Parameters ---------- - channel : :obj:`typing.Union` [ :obj:`hikari.channels.Channel`, :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ] + channel : :obj:`~typing.Union` [ :obj:`~hikari.channels.Channel`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] The object or ID of the channel to add this reaction in. - message : :obj:`typing.Union` [ :obj:`hikari.messages.Message`, :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ] + message : :obj:`~typing.Union` [ :obj:`~hikari.messages.Message`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] The object or ID of the message to add the reaction in. - emoji : :obj:`typing.Union` [ :obj:`hikari.emojis.Emoji`, :obj:`str` ] + emoji : :obj:`~typing.Union` [ :obj:`~hikari.emojis.Emoji`, :obj:`~str` ] The emoji to add. This can either be an emoji object or a string representation of an emoji. The string representation will be either ``"name:id"`` for custom emojis else it's unicode character(s) (can @@ -929,13 +929,13 @@ async def create_reaction( Raises ------ - :obj:`hikari.errors.ForbiddenHTTPError` + :obj:`~hikari.errors.ForbiddenHTTPError` If this is the first reaction using this specific emoji on this message and you lack the ``ADD_REACTIONS`` permission. If you lack ``READ_MESSAGE_HISTORY``, this may also raise this error. - :obj:`hikari.errors.NotFoundHTTPError` + :obj:`~hikari.errors.NotFoundHTTPError` If the channel or message is not found, or if the emoji is not found. - :obj:`hikari.errors.BadRequestHTTPError` + :obj:`~hikari.errors.BadRequestHTTPError` If the emoji is not valid, unknown, or formatted incorrectly. If any invalid snowflake IDs are passed; a snowflake may be invalid due to it being outside of the range of a 64 bit integer. @@ -956,11 +956,11 @@ async def delete_reaction( Parameters ---------- - channel : :obj:`typing.Union` [ :obj:`hikari.channels.Channel`, :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ] + channel : :obj:`~typing.Union` [ :obj:`~hikari.channels.Channel`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] The object or ID of the channel to add this reaction in. - message : :obj:`typing.Union` [ :obj:`hikari.messages.Message`, :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ] + message : :obj:`~typing.Union` [ :obj:`~hikari.messages.Message`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] The object or ID of the message to add the reaction in. - emoji : :obj:`typing.Union` [ :obj:`hikari.emojis.Emoji`, :obj:`str` ] + emoji : :obj:`~typing.Union` [ :obj:`~hikari.emojis.Emoji`, :obj:`~str` ] The emoji to add. This can either be an emoji object or a string representation of an emoji. The string representation will be either ``"name:id"`` for custom emojis else it's unicode @@ -968,14 +968,14 @@ async def delete_reaction( Raises ------ - :obj:`hikari.errors.ForbiddenHTTPError` + :obj:`~hikari.errors.ForbiddenHTTPError` If this is the first reaction using this specific emoji on this message and you lack the ``ADD_REACTIONS`` permission. If you lack ``READ_MESSAGE_HISTORY``, this may also raise this error. - :obj:`hikari.errors.NotFoundHTTPError` + :obj:`~hikari.errors.NotFoundHTTPError` If the channel or message is not found, or if the emoji is not found. - :obj:`hikari.errors.BadRequestHTTPError` + :obj:`~hikari.errors.BadRequestHTTPError` If the emoji is not valid, unknown, or formatted incorrectly. If any invalid snowflake IDs are passed; a snowflake may be invalid due to it being outside of the range of a 64 bit integer. @@ -993,19 +993,19 @@ async def delete_all_reactions( Parameters ---------- - channel : :obj:`typing.Union` [ :obj:`hikari.channels.Channel`, :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ] + channel : :obj:`~typing.Union` [ :obj:`~hikari.channels.Channel`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] The object or ID of the channel to get the message from. - message : :obj:`typing.Union` [ :obj:`hikari.messages.Message`, :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ] + message : :obj:`~typing.Union` [ :obj:`~hikari.messages.Message`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] The object or ID of the message to remove all reactions from. Raises ------ - :obj:`hikari.errors.BadRequestHTTPError` + :obj:`~hikari.errors.BadRequestHTTPError` If any invalid snowflake IDs are passed; a snowflake may be invalid due to it being outside of the range of a 64 bit integer. - :obj:`hikari.errors.NotFoundHTTPError` + :obj:`~hikari.errors.NotFoundHTTPError` If the channel or message is not found. - :obj:`hikari.errors.ForbiddenHTTPError` + :obj:`~hikari.errors.ForbiddenHTTPError` If you lack the ``MANAGE_MESSAGES`` permission. """ await self._session.delete_all_reactions( @@ -1023,23 +1023,23 @@ async def delete_all_reactions_for_emoji( Parameters ---------- - channel : :obj:`typing.Union` [ :obj:`hikari.channels.Channel`, :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ] + channel : :obj:`~typing.Union` [ :obj:`~hikari.channels.Channel`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] The object or ID of the channel to get the message from. - message : :obj:`typing.Union` [ :obj:`hikari.messages.Message`, :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ] + message : :obj:`~typing.Union` [ :obj:`~hikari.messages.Message`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] The object or ID of the message to delete the reactions from. - emoji : :obj:`typing.Union` [ :obj:`hikari.emojis.Emoji`, :obj:`str` ] + emoji : :obj:`~typing.Union` [ :obj:`~hikari.emojis.Emoji`, :obj:`~str` ] The object or string representatiom of the emoji to delete. The string representation will be either ``"name:id"`` for custom emojis else it's unicode character(s) (can be UTF-32). Raises ------ - :obj:`hikari.errors.BadRequestHTTPError` + :obj:`~hikari.errors.BadRequestHTTPError` If any invalid snowflake IDs are passed; a snowflake may be invalid due to it being outside of the range of a 64 bit integer. - :obj:`hikari.errors.NotFoundHTTPError` + :obj:`~hikari.errors.NotFoundHTTPError` If the channel or message or emoji or user is not found. - :obj:`hikari.errors.ForbiddenHTTPError` + :obj:`~hikari.errors.ForbiddenHTTPError` If you lack the ``MANAGE_MESSAGES`` permission, or the channel is a DM channel. """ @@ -1065,20 +1065,20 @@ def fetch_reactors_after( Parameters ---------- - channel : :obj:`typing.Union` [ :obj:`hikari.channels.Channel`, :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ] + channel : :obj:`~typing.Union` [ :obj:`~hikari.channels.Channel`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] The object or ID of the channel to get the message from. - message : :obj:`typing.Union` [ :obj:`hikari.messages.Message`, :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ] + message : :obj:`~typing.Union` [ :obj:`~hikari.messages.Message`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] The object or ID of the message to get the reactions from. - emoji : :obj:`typing.Union` [ :obj:`hikari.emojis.Emoji`, :obj:`str` ] + emoji : :obj:`~typing.Union` [ :obj:`~hikari.emojis.Emoji`, :obj:`~str` ] The emoji to get. This can either be it's object or the string representation of the emoji. The string representation will be either ``"name:id"`` for custom emojis else it's unicode character(s) (can be UTF-32). - after : :obj:`typing.Union` [ :obj:`datetime.datetime`, :obj:`hikari.users.User`, :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ] + after : :obj:`~typing.Union` [ :obj:`~datetime.datetime`, :obj:`~hikari.users.User`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] If specified, a object or ID user. If specified, only users with a snowflake that is lexicographically greater than the value will be returned. - limit : :obj:`str` + limit : :obj:`~str` If specified, the limit of the number of users this iterator should return. @@ -1092,17 +1092,17 @@ def fetch_reactors_after( Returns ------- - :obj:`typing.AsyncIterator` [ :obj:`hikari.users.User` ] + :obj:`~typing.AsyncIterator` [ :obj:`~hikari.users.User` ] An async iterator of user objects. Raises ------ - :obj:`hikari.errors.BadRequestHTTPError` + :obj:`~hikari.errors.BadRequestHTTPError` If any invalid snowflake IDs are passed; a snowflake may be invalid due to it being outside of the range of a 64 bit integer. - :obj:`hikari.errors.ForbiddenHTTPError` + :obj:`~hikari.errors.ForbiddenHTTPError` If you lack access to the message. - :obj:`hikari.errors.NotFoundHTTPError` + :obj:`~hikari.errors.NotFoundHTTPError` If the channel or message is not found. """ if isinstance(after, datetime.datetime): @@ -1137,49 +1137,49 @@ async def update_message( Parameters ---------- - channel : :obj:`typing.Union` [ :obj:`hikari.channels.Channel`, :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ] + channel : :obj:`~typing.Union` [ :obj:`~hikari.channels.Channel`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] The object or ID of the channel to get the message from. - message : :obj:`typing.Union` [ :obj:`hikari.messages.Message`, :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ] + message : :obj:`~typing.Union` [ :obj:`~hikari.messages.Message`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] The object or ID of the message to edit. - content : :obj:`str`, optional + content : :obj:`~str`, optional If specified, the string content to replace with in the message. - If :obj:`None`, the content will be removed from the message. - embed : :obj:`hikari.embeds.Embed`, optional + If :obj:`~None`, the content will be removed from the message. + embed : :obj:`~hikari.embeds.Embed`, optional If specified, the embed to replace with in the message. - If :obj:`None`, the embed will be removed from the message. - flags : :obj:`hikari.messages.MessageFlag` + If :obj:`~None`, the embed will be removed from the message. + flags : :obj:`~hikari.messages.MessageFlag` If specified, the new flags for this message, while a raw int may be passed for this, this can lead to unexpected behaviour if it's outside the range of the MessageFlag int flag. - mentions_everyone : :obj:`bool` + mentions_everyone : :obj:`~bool` Whether ``@everyone`` and ``@here`` mentions should be resolved by - discord and lead to actual pings, defaults to :obj:`True`. - user_mentions : :obj:`typing.Union` [ :obj:`typing.Collection` [ :obj:`typing.Union` [ :obj:`hikari.users.User`, :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ], :obj:`bool` ] + discord and lead to actual pings, defaults to :obj:`~True`. + user_mentions : :obj:`~typing.Union` [ :obj:`~typing.Collection` [ :obj:`~typing.Union` [ :obj:`~hikari.users.User`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ], :obj:`~bool` ] Either an array of user objects/IDs to allow mentions for, - :obj:`True` to allow all user mentions or :obj:`False` to block all - user mentions from resolving, defaults to :obj:`True`. - role_mentions : :obj:`typing.Union` [ :obj:`typing.Collection` [ :obj:`typing.Union` [ :obj:`hikari.guilds.GuildRole`, :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ] ], :obj:`bool` ] + :obj:`~True` to allow all user mentions or :obj:`~False` to block all + user mentions from resolving, defaults to :obj:`~True`. + role_mentions : :obj:`~typing.Union` [ :obj:`~typing.Collection` [ :obj:`~typing.Union` [ :obj:`~hikari.guilds.GuildRole`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] ], :obj:`~bool` ] Either an array of guild role objects/IDs to allow mentions for, - :obj:`True` to allow all role mentions or :obj:`False` to block all - role mentions from resolving, defaults to :obj:`True`. + :obj:`~True` to allow all role mentions or :obj:`~False` to block all + role mentions from resolving, defaults to :obj:`~True`. Returns ------- - :obj:`hikari.messages.Message` + :obj:`~hikari.messages.Message` The edited message object. Raises ------ - :obj:`hikari.errors.NotFoundHTTPError` + :obj:`~hikari.errors.NotFoundHTTPError` If the channel or message is not found. - :obj:`hikari.errors.BadRequestHTTPError` + :obj:`~hikari.errors.BadRequestHTTPError` This can be raised if the embed exceeds the defined limits; if the message content is specified only and empty or greater than ``2000`` characters; if neither content, file or embed are specified. If any invalid snowflake IDs are passed; a snowflake may be invalid due to it being outside of the range of a 64 bit integer. - :obj:`hikari.errors.ForbiddenHTTPError` + :obj:`~hikari.errors.ForbiddenHTTPError` If you try to edit ``content`` or ``embed`` or ``allowed_mentions` on a message you did not author. If you try to edit the flags on a message you did not author without @@ -1213,7 +1213,7 @@ def safe_update_message( This endpoint has the same signature as :attr:`execute_webhook` with the only difference being that ``mentions_everyone``, - ``user_mentions`` and ``role_mentions`` default to :obj:`False`. + ``user_mentions`` and ``role_mentions`` default to :obj:`~False`. """ return self.update_message( message=message, @@ -1236,26 +1236,26 @@ async def delete_messages( Parameters ---------- - channel : :obj:`typing.Union` [ :obj:`hikari.channels.Channel`, :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ] + channel : :obj:`~typing.Union` [ :obj:`~hikari.channels.Channel`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] The object or ID of the channel to get the message from. - message : :obj:`typing.Union` [ :obj:`hikari.messages.Message`, :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ] + message : :obj:`~typing.Union` [ :obj:`~hikari.messages.Message`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] The object or ID of the message to delete. - *additional_messages : :obj:`typing.Union` [ :obj:`hikari.messages.Message`, :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ] + *additional_messages : :obj:`~typing.Union` [ :obj:`~hikari.messages.Message`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] Objects and/or IDs of additional messages to delete in the same channel, in total you can delete up to 100 messages in a request. Raises ------ - :obj:`hikari.errors.BadRequestHTTPError` + :obj:`~hikari.errors.BadRequestHTTPError` If any invalid snowflake IDs are passed; a snowflake may be invalid due to it being outside of the range of a 64 bit integer. - :obj:`hikari.errors.ForbiddenHTTPError` + :obj:`~hikari.errors.ForbiddenHTTPError` If you did not author the message and are in a DM, or if you did not author the message and lack the ``MANAGE_MESSAGES`` permission in a guild channel. - :obj:`hikari.errors.NotFoundHTTPError` + :obj:`~hikari.errors.NotFoundHTTPError` If the channel or message is not found. - :obj:`ValueError` + :obj:`~ValueError` If you try to delete over ``100`` messages in a single request. Note @@ -1304,32 +1304,32 @@ async def update_channel_overwrite( Parameters ---------- - channel : :obj:`typing.Union` [ :obj:`hikari.messages.Message`, :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ] + channel : :obj:`~typing.Union` [ :obj:`~hikari.messages.Message`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] The object or ID of the channel to edit permissions for. - overwrite : :obj:`typing.Union` [ :obj:`hikari.channels.PermissionOverwrite`, :obj:`hikari.guilds.GuildRole`, :obj:`hikari.users.User`, :obj:`hikari.snowflakes.Snowflake` , :obj:`int` ] + overwrite : :obj:`~typing.Union` [ :obj:`~hikari.channels.PermissionOverwrite`, :obj:`~hikari.guilds.GuildRole`, :obj:`~hikari.users.User`, :obj:`~hikari.snowflakes.Snowflake` , :obj:`~int` ] The object or ID of the target member or role to edit/create the overwrite for. - target_type : :obj:`typing.Union` [ :obj:`hikari.channels.PermissionOverwriteType`, :obj:`int` ] + target_type : :obj:`~typing.Union` [ :obj:`~hikari.channels.PermissionOverwriteType`, :obj:`~int` ] The type of overwrite, passing a raw string that's outside of the enum's range for this may lead to unexpected behaviour. - allow : :obj:`typing.Union` [ :obj:`hikari.permissions.Permission`, :obj:`int` ] + allow : :obj:`~typing.Union` [ :obj:`~hikari.permissions.Permission`, :obj:`~int` ] If specified, the value of all permissions to set to be allowed, passing a raw integer for this may lead to unexpected behaviour. - deny : :obj:`typing.Union` [ :obj:`hikari.permissions.Permission`, :obj:`int` ] + deny : :obj:`~typing.Union` [ :obj:`~hikari.permissions.Permission`, :obj:`~int` ] If specified, the value of all permissions to set to be denied, passing a raw integer for this may lead to unexpected behaviour. - reason : :obj:`str` + reason : :obj:`~str` If specified, the audit log reason explaining why the operation was performed. Raises ------ - :obj:`hikari.errors.BadRequestHTTPError` + :obj:`~hikari.errors.BadRequestHTTPError` If any invalid snowflake IDs are passed; a snowflake may be invalid due to it being outside of the range of a 64 bit integer. - :obj:`hikari.errors.NotFoundHTTPError` + :obj:`~hikari.errors.NotFoundHTTPError` If the target channel or overwrite doesn't exist. - :obj:`hikari.errors.ForbiddenHTTPError` + :obj:`~hikari.errors.ForbiddenHTTPError` If you lack permission to do this. """ await self._session.edit_channel_permissions( @@ -1348,22 +1348,22 @@ async def fetch_invites_for_channel( Parameters ---------- - channel : :obj:`typing.Union` [ :obj:`hikari.channels.Channel`, :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ] + channel : :obj:`~typing.Union` [ :obj:`~hikari.channels.Channel`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] The object or ID of the channel to get invites for. Returns ------- - :obj:`typing.Sequence` [ :obj:`hikari.invites.InviteWithMetadata` ] + :obj:`~typing.Sequence` [ :obj:`~hikari.invites.InviteWithMetadata` ] A list of invite objects. Raises ------ - :obj:`hikari.errors.BadRequestHTTPError` + :obj:`~hikari.errors.BadRequestHTTPError` If any invalid snowflake IDs are passed; a snowflake may be invalid due to it being outside of the range of a 64 bit integer. - :obj:`hikari.errors.ForbiddenHTTPError` + :obj:`~hikari.errors.ForbiddenHTTPError` If you lack the ``MANAGE_CHANNELS`` permission. - :obj:`hikari.errors.NotFoundHTTPError` + :obj:`~hikari.errors.NotFoundHTTPError` If the channel does not exist. """ payload = await self._session.get_channel_invites( @@ -1387,42 +1387,42 @@ async def create_invite_for_channel( Parameters ---------- - channel : :obj:`typing.Union` [ :obj:`datetime.timedelta`, :obj:`str` ] + channel : :obj:`~typing.Union` [ :obj:`~datetime.timedelta`, :obj:`~str` ] The object or ID of the channel to create the invite for. - max_age : :obj:`int` + max_age : :obj:`~int` If specified, the seconds time delta for the max age of the invite, defaults to ``86400`` seconds (``24`` hours). Set to ``0`` seconds to never expire. - max_uses : :obj:`int` + max_uses : :obj:`~int` If specified, the max number of uses this invite can have, or ``0`` for unlimited (as per the default). - temporary : :obj:`bool` + temporary : :obj:`~bool` If specified, whether to grant temporary membership, meaning the user is kicked when their session ends unless they are given a role. - unique : :obj:`bool` + unique : :obj:`~bool` If specified, whether to try to reuse a similar invite. - target_user : :obj:`typing.Union` [ :obj:`hikari.users.User`, :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ] + target_user : :obj:`~typing.Union` [ :obj:`~hikari.users.User`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] If specified, the object or ID of the user this invite should target. - target_user_type : :obj:`typing.Union` [ :obj:`hikari.invites.TargetUserType`, :obj:`int` ] + target_user_type : :obj:`~typing.Union` [ :obj:`~hikari.invites.TargetUserType`, :obj:`~int` ] If specified, the type of target for this invite, passing a raw integer for this may lead to unexpected results. - reason : :obj:`str` + reason : :obj:`~str` If specified, the audit log reason explaining why the operation was performed. Returns ------- - :obj:`hikari.invites.InviteWithMetadata` + :obj:`~hikari.invites.InviteWithMetadata` The created invite object. Raises ------ - :obj:`hikari.errors.ForbiddenHTTPError` + :obj:`~hikari.errors.ForbiddenHTTPError` If you lack the ``CREATE_INSTANT_MESSAGES`` permission. - :obj:`hikari.errors.NotFoundHTTPError` + :obj:`~hikari.errors.NotFoundHTTPError` If the channel does not exist. - :obj:`hikari.errors.BadRequestHTTPError` + :obj:`~hikari.errors.BadRequestHTTPError` If the arguments provided are not valid (e.g. negative age, etc). If any invalid snowflake IDs are passed; a snowflake may be invalid due to it being outside of the range of a 64 bit integer. @@ -1452,19 +1452,19 @@ async def delete_channel_overwrite( Parameters ---------- - channel : :obj:`typing.Union` [ :obj:`hikari.channels.Channel`, :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ] + channel : :obj:`~typing.Union` [ :obj:`~hikari.channels.Channel`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] The object or ID of the channel to delete the overwrite from. - overwrite : :obj:`typing.Union` [ :obj:`hikari.channels.PermissionOverwrite`, :obj:`hikari.guilds.GuildRole`, :obj:`hikari.users.User`, :obj:`hikari.snowflakes.Snowflake`, :obj:int ] + overwrite : :obj:`~typing.Union` [ :obj:`~hikari.channels.PermissionOverwrite`, :obj:`~hikari.guilds.GuildRole`, :obj:`~hikari.users.User`, :obj:`~hikari.snowflakes.Snowflake`, :obj:int ] The ID of the entity this overwrite targets. Raises ------ - :obj:`hikari.errors.BadRequestHTTPError` + :obj:`~hikari.errors.BadRequestHTTPError` If any invalid snowflake IDs are passed; a snowflake may be invalid due to it being outside of the range of a 64 bit integer. - :obj:`hikari.errors.NotFoundHTTPError` + :obj:`~hikari.errors.NotFoundHTTPError` If the overwrite or channel do not exist. - :obj:`hikari.errors.ForbiddenHTTPError` + :obj:`~hikari.errors.ForbiddenHTTPError` If you lack the ``MANAGE_ROLES`` permission for that channel. """ await self._session.delete_channel_permission( @@ -1477,17 +1477,17 @@ async def trigger_typing(self, channel: snowflakes.HashableT[_channels.Channel]) Parameters ---------- - channel : :obj:`typing.Union` [ :obj:`hikari.channels.Channel`, :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ] + channel : :obj:`~typing.Union` [ :obj:`~hikari.channels.Channel`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] The object or ID of the channel to appear to be typing in. Raises ------ - :obj:`hikari.errors.BadRequestHTTPError` + :obj:`~hikari.errors.BadRequestHTTPError` If any invalid snowflake IDs are passed; a snowflake may be invalid due to it being outside of the range of a 64 bit integer. - :obj:`hikari.errors.NotFoundHTTPError` + :obj:`~hikari.errors.NotFoundHTTPError` If the channel is not found. - :obj:`hikari.errors.ForbiddenHTTPError` + :obj:`~hikari.errors.ForbiddenHTTPError` If you are not able to type in the channel. """ await self._session.trigger_typing_indicator( @@ -1501,22 +1501,22 @@ async def fetch_pins( Parameters ---------- - channel : :obj:`typing.Union` [ :obj:`hikari.channels.Channel`, :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ] + channel : :obj:`~typing.Union` [ :obj:`~hikari.channels.Channel`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] The object or ID of the channel to get messages from. Returns ------- - :obj:`typing.Mapping` [ :obj:`hikari.snowflakes.Snowflake`, :obj:`hikari.messages.Message` ] + :obj:`~typing.Mapping` [ :obj:`~hikari.snowflakes.Snowflake`, :obj:`~hikari.messages.Message` ] A list of message objects. Raises ------ - :obj:`hikari.errors.BadRequestHTTPError` + :obj:`~hikari.errors.BadRequestHTTPError` If any invalid snowflake IDs are passed; a snowflake may be invalid due to it being outside of the range of a 64 bit integer. - :obj:`hikari.errors.NotFoundHTTPError` + :obj:`~hikari.errors.NotFoundHTTPError` If the channel is not found. - :obj:`hikari.errors.ForbiddenHTTPError` + :obj:`~hikari.errors.ForbiddenHTTPError` If you are not able to see the channel. Note @@ -1537,19 +1537,19 @@ async def pin_message( Parameters ---------- - channel : :obj:`typing.Union` [ :obj:`hikari.channels.Channel`, :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ] + channel : :obj:`~typing.Union` [ :obj:`~hikari.channels.Channel`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] The object or ID of the channel to pin a message to. - message : :obj:`typing.Union` [ :obj:`hikari.messages.Message`, :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ] + message : :obj:`~typing.Union` [ :obj:`~hikari.messages.Message`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] The object or ID of the message to pin. Raises ------ - :obj:`hikari.errors.BadRequestHTTPError` + :obj:`~hikari.errors.BadRequestHTTPError` If any invalid snowflake IDs are passed; a snowflake may be invalid due to it being outside of the range of a 64 bit integer. - :obj:`hikari.errors.ForbiddenHTTPError` + :obj:`~hikari.errors.ForbiddenHTTPError` If you lack the ``MANAGE_MESSAGES`` permission. - :obj:`hikari.errors.NotFoundHTTPError` + :obj:`~hikari.errors.NotFoundHTTPError` If the message or channel do not exist. """ await self._session.add_pinned_channel_message( @@ -1566,19 +1566,19 @@ async def unpin_message( Parameters ---------- - channel : :obj:`typing.Union` [ :obj:`hikari.channels.Channel`, :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ] + channel : :obj:`~typing.Union` [ :obj:`~hikari.channels.Channel`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] The ID of the channel to remove a pin from. - message : :obj:`typing.Union` [ :obj:`hikari.messages.Message`, :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ] + message : :obj:`~typing.Union` [ :obj:`~hikari.messages.Message`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] The object or ID of the message to unpin. Raises ------ - :obj:`hikari.errors.BadRequestHTTPError` + :obj:`~hikari.errors.BadRequestHTTPError` If any invalid snowflake IDs are passed; a snowflake may be invalid due to it being outside of the range of a 64 bit integer. - :obj:`hikari.errors.ForbiddenHTTPError` + :obj:`~hikari.errors.ForbiddenHTTPError` If you lack the ``MANAGE_MESSAGES`` permission. - :obj:`hikari.errors.NotFoundHTTPError` + :obj:`~hikari.errors.NotFoundHTTPError` If the message or channel do not exist. """ await self._session.delete_pinned_channel_message( @@ -1593,24 +1593,24 @@ async def fetch_guild_emoji( Parameters ---------- - guild : :obj:`typing.Union` [ :obj:`hikari.guilds.Guild`, :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ] + guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] The object or ID of the guild to get the emoji from. - emoji : :obj:`typing.Union` [ :obj:`hikari.emojis.GuildEmoji`, :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ] + emoji : :obj:`~typing.Union` [ :obj:`~hikari.emojis.GuildEmoji`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] The object or ID of the emoji to get. Returns ------- - :obj:`hikari.emojis.GuildEmoji` + :obj:`~hikari.emojis.GuildEmoji` A guild emoji object. Raises ------ - :obj:`hikari.errors.BadRequestHTTPError` + :obj:`~hikari.errors.BadRequestHTTPError` If any invalid snowflake IDs are passed; a snowflake may be invalid due to it being outside of the range of a 64 bit integer. - :obj:`hikari.errors.NotFoundHTTPError` + :obj:`~hikari.errors.NotFoundHTTPError` If either the guild or the emoji aren't found. - :obj:`hikari.errors.ForbiddenHTTPError` + :obj:`~hikari.errors.ForbiddenHTTPError` If you aren't a member of said guild. """ payload = await self._session.get_guild_emoji( @@ -1624,22 +1624,22 @@ async def fetch_guild_emojis(self, guild: snowflakes.HashableT[guilds.Guild]) -> Parameters ---------- - guild : :obj:`typing.Union` [ :obj:`hikari.guilds.Guild`, :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ] + guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] The object or ID of the guild to get the emojis for. Returns ------- - :obj:`typing.Sequence` [ :obj:`hikari.emojis.GuildEmoji` ] + :obj:`~typing.Sequence` [ :obj:`~hikari.emojis.GuildEmoji` ] A list of guild emoji objects. Raises ------ - :obj:`hikari.errors.BadRequestHTTPError` + :obj:`~hikari.errors.BadRequestHTTPError` If any invalid snowflake IDs are passed; a snowflake may be invalid due to it being outside of the range of a 64 bit integer. - :obj:`hikari.errors.NotFoundHTTPError` + :obj:`~hikari.errors.NotFoundHTTPError` If the guild is not found. - :obj:`hikari.errors.ForbiddenHTTPError` + :obj:`~hikari.errors.ForbiddenHTTPError` If you aren't a member of the guild. """ payload = await self._session.list_guild_emojis( @@ -1660,34 +1660,34 @@ async def create_guild_emoji( Parameters ---------- - guild : :obj:`typing.Union` [ :obj:`hikari.guilds.GuildRole`, :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ] + guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.GuildRole`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] The object or ID of the guild to create the emoji in. - name : :obj:`str` + name : :obj:`~str` The new emoji's name. image_data : ``hikari.internal.conversions.FileLikeT`` The ``128x128`` image data. - roles : :obj:`typing.Sequence` [ :obj:`typing.Union` [ :obj:`hikari.guilds.GuildRole`, :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ] ] + roles : :obj:`~typing.Sequence` [ :obj:`~typing.Union` [ :obj:`~hikari.guilds.GuildRole`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] ] If specified, a list of role objects or IDs for which the emoji will be whitelisted. If empty, all roles are whitelisted. - reason : :obj:`str` + reason : :obj:`~str` If specified, the audit log reason explaining why the operation was performed. Returns ------- - :obj:`hikari.emojis.GuildEmoji` + :obj:`~hikari.emojis.GuildEmoji` The newly created emoji object. Raises ------ - :obj:`ValueError` - If ``image`` is :obj:`None`. - :obj:`hikari.errors.NotFoundHTTPError` + :obj:`~ValueError` + If ``image`` is :obj:`~None`. + :obj:`~hikari.errors.NotFoundHTTPError` If the guild is not found. - :obj:`hikari.errors.ForbiddenHTTPError` + :obj:`~hikari.errors.ForbiddenHTTPError` If you either lack the ``MANAGE_EMOJIS`` permission or aren't a member of said guild. - :obj:`hikari.errors.BadRequestHTTPError` + :obj:`~hikari.errors.BadRequestHTTPError` If you attempt to upload an image larger than ``256kb``, an empty image or an invalid image format. If any invalid snowflake IDs are passed; a snowflake may be invalid @@ -1717,34 +1717,34 @@ async def update_guild_emoji( Parameters ---------- - guild : :obj:`typing.Union` [ :obj:`hikari.guilds.Guild`, :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ] + guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] The object or ID of the guild to which the emoji to edit belongs to. - emoji : :obj:`typing.Union` [ :obj:`hikari.emojis.GuildEmoji`, :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ] + emoji : :obj:`~typing.Union` [ :obj:`~hikari.emojis.GuildEmoji`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] The object or ID of the emoji to edit. - name : :obj:`str` + name : :obj:`~str` If specified, a new emoji name string. Keep unspecified to leave the name unchanged. - roles : :obj:`typing.Sequence` [ :obj:`typing.Union` [ :obj:`hikari.guilds.GuildRole`, :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ] ] + roles : :obj:`~typing.Sequence` [ :obj:`~typing.Union` [ :obj:`~hikari.guilds.GuildRole`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] ] If specified, a list of objects or IDs for the new whitelisted roles. Set to an empty list to whitelist all roles. Keep unspecified to leave the same roles already set. - reason : :obj:`str` + reason : :obj:`~str` If specified, the audit log reason explaining why the operation was performed. Returns ------- - :obj:`hikari.emojis.GuildEmoji` + :obj:`~hikari.emojis.GuildEmoji` The updated emoji object. Raises ------ - :obj:`hikari.errors.BadRequestHTTPError` + :obj:`~hikari.errors.BadRequestHTTPError` If any invalid snowflake IDs are passed; a snowflake may be invalid due to it being outside of the range of a 64 bit integer. - :obj:`hikari.errors.NotFoundHTTPError` + :obj:`~hikari.errors.NotFoundHTTPError` If either the guild or the emoji aren't found. - :obj:`hikari.errors.ForbiddenHTTPError` + :obj:`~hikari.errors.ForbiddenHTTPError` If you either lack the ``MANAGE_EMOJIS`` permission or are not a member of the given guild. """ @@ -1766,19 +1766,19 @@ async def delete_guild_emoji( Parameters ---------- - guild : :obj:`typing.Union` [ :obj:`hikari.guilds.Guild`, :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ] + guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] The object or ID of the guild to delete the emoji from. - emoji : :obj:`typing.Union` [ :obj:`hikari.emojis.GuildEmoji`, :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ] + emoji : :obj:`~typing.Union` [ :obj:`~hikari.emojis.GuildEmoji`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] The object or ID of the guild emoji to be deleted. Raises ------ - :obj:`hikari.errors.BadRequestHTTPError` + :obj:`~hikari.errors.BadRequestHTTPError` If any invalid snowflake IDs are passed; a snowflake may be invalid due to it being outside of the range of a 64 bit integer. - :obj:`hikari.errors.NotFoundHTTPError` + :obj:`~hikari.errors.NotFoundHTTPError` If either the guild or the emoji aren't found. - :obj:`hikari.errors.ForbiddenHTTPError` + :obj:`~hikari.errors.ForbiddenHTTPError` If you either lack the ``MANAGE_EMOJIS`` permission or aren't a member of said guild. """ @@ -1807,40 +1807,40 @@ async def create_guild( Parameters ---------- - name : :obj:`str` + name : :obj:`~str` The name string for the new guild (``2-100`` characters). - region : :obj:`str` + region : :obj:`~str` If specified, the voice region ID for new guild. You can use :meth:`fetch_guild_voice_regions` to see which region IDs are available. icon_data : ``hikari.internal.conversions.FileLikeT`` If specified, the guild icon image data. - verification_level : :obj:`typing.Union` [ :obj:`hikari.guilds.GuildVerificationLevel`, :obj:`int` ] + verification_level : :obj:`~typing.Union` [ :obj:`~hikari.guilds.GuildVerificationLevel`, :obj:`~int` ] If specified, the verification level. Passing a raw int for this may lead to unexpected behaviour. - default_message_notifications : :obj:`typing.Union` [ :obj:`hikari.guilds.GuildMessageNotificationsLevel`, :obj:`int` ] + default_message_notifications : :obj:`~typing.Union` [ :obj:`~hikari.guilds.GuildMessageNotificationsLevel`, :obj:`~int` ] If specified, the default notification level. Passing a raw int for this may lead to unexpected behaviour. - explicit_content_filter : :obj:`typing.Union` [ :obj:`hikari.guilds.GuildExplicitContentFilterLevel`, :obj:`int` ] + explicit_content_filter : :obj:`~typing.Union` [ :obj:`~hikari.guilds.GuildExplicitContentFilterLevel`, :obj:`~int` ] If specified, the explicit content filter. Passing a raw int for this may lead to unexpected behaviour. - roles : :obj:`typing.Sequence` [ :obj:`hikari.guilds.GuildRole` ] + roles : :obj:`~typing.Sequence` [ :obj:`~hikari.guilds.GuildRole` ] If specified, an array of role objects to be created alongside the guild. First element changes the ``@everyone`` role. - channels : :obj:`typing.Sequence` [ :obj:`hikari.channels.GuildChannelBuilder` ] + channels : :obj:`~typing.Sequence` [ :obj:`~hikari.channels.GuildChannelBuilder` ] If specified, an array of guild channel builder objects to be created within the guild. Returns ------- - :obj:`hikari.guilds.Guild` + :obj:`~hikari.guilds.Guild` The newly created guild object. Raises ------ - :obj:`hikari.errors.ForbiddenHTTPError` + :obj:`~hikari.errors.ForbiddenHTTPError` If you are in ``10`` or more guilds. - :obj:`hikari.errors.BadRequestHTTPError` + :obj:`~hikari.errors.BadRequestHTTPError` If you provide unsupported fields like ``parent_id`` in channel objects. If any invalid snowflake IDs are passed; a snowflake may be invalid @@ -1863,22 +1863,22 @@ async def fetch_guild(self, guild: snowflakes.HashableT[guilds.Guild]) -> guilds Parameters ---------- - guild : :obj:`typing.Union` [ :obj:`hikari.guilds.Guild`, :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ] + guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] The object or ID of the guild to get. Returns ------- - :obj:`hikari.guilds.Guild` + :obj:`~hikari.guilds.Guild` The requested guild object. Raises ------ - :obj:`hikari.errors.BadRequestHTTPError` + :obj:`~hikari.errors.BadRequestHTTPError` If any invalid snowflake IDs are passed; a snowflake may be invalid due to it being outside of the range of a 64 bit integer. - :obj:`hikari.errors.NotFoundHTTPError` + :obj:`~hikari.errors.NotFoundHTTPError` If the guild is not found. - :obj:`hikari.errors.ForbiddenHTTPError` + :obj:`~hikari.errors.ForbiddenHTTPError` If you don't have access to the guild. """ payload = await self._session.get_guild( @@ -1891,12 +1891,12 @@ async def fetch_guild_preview(self, guild: snowflakes.HashableT[guilds.Guild]) - Parameters ---------- - guild : :obj:`typing.Union` [ :obj:`hikari.guilds.Guild`, :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ] + guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] The object or ID of the guild to get the preview object for. Returns ------- - :obj:`hikari.guilds.GuildPreview` + :obj:`~hikari.guilds.GuildPreview` The requested guild preview object. Note @@ -1906,10 +1906,10 @@ async def fetch_guild_preview(self, guild: snowflakes.HashableT[guilds.Guild]) - Raises ------ - :obj:`hikari.errors.BadRequestHTTPError` + :obj:`~hikari.errors.BadRequestHTTPError` If any invalid snowflake IDs are passed; a snowflake may be invalid due to it being outside of the range of UINT64. - :obj:`hikari.errors.NotFoundHTTPError` + :obj:`~hikari.errors.NotFoundHTTPError` If the guild is not found or it isn't ``PUBLIC``. """ payload = await self._session.get_guild_preview( @@ -1938,52 +1938,52 @@ async def update_guild( Parameters ---------- - guild : :obj:`typing.Union` [ :obj:`hikari.guilds.Guild`, :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ] + guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] The object or ID of the guild to be edited. - name : :obj:`str` + name : :obj:`~str` If specified, the new name string for the guild (``2-100`` characters). - region : :obj:`str` + region : :obj:`~str` If specified, the new voice region ID for guild. You can use :meth:`fetch_guild_voice_regions` to see which region IDs are available. - verification_level : :obj:`typing.Union` [ :obj:`hikari.guilds.GuildVerificationLevel`, :obj:`int` ] + verification_level : :obj:`~typing.Union` [ :obj:`~hikari.guilds.GuildVerificationLevel`, :obj:`~int` ] If specified, the new verification level. Passing a raw int for this may lead to unexpected behaviour. - default_message_notifications : :obj:`typing.Union` [ :obj:`hikari.guilds.GuildMessageNotificationsLevel`, :obj:`int` ] + default_message_notifications : :obj:`~typing.Union` [ :obj:`~hikari.guilds.GuildMessageNotificationsLevel`, :obj:`~int` ] If specified, the new default notification level. Passing a raw int for this may lead to unexpected behaviour. - explicit_content_filter : :obj:`typing.Union` [ :obj:`hikari.guilds.GuildExplicitContentFilterLevel`, :obj:`int` ] + explicit_content_filter : :obj:`~typing.Union` [ :obj:`~hikari.guilds.GuildExplicitContentFilterLevel`, :obj:`~int` ] If specified, the new explicit content filter. Passing a raw int for this may lead to unexpected behaviour. - afk_channel : :obj:`typing.Union` [ :obj:`hikari.channels.GuildVoiceChannel`, :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ] + afk_channel : :obj:`~typing.Union` [ :obj:`~hikari.channels.GuildVoiceChannel`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] If specified, the object or ID for the new AFK voice channel. - afk_timeout : :obj:`typing.Union` [ :obj:`datetime.timedelta`, :obj:`int` ] + afk_timeout : :obj:`~typing.Union` [ :obj:`~datetime.timedelta`, :obj:`~int` ] If specified, the new AFK timeout seconds timedelta. icon_data : ``hikari.internal.conversions.FileLikeT`` If specified, the new guild icon image file data. - owner : :obj:`typing.Union` [ :obj:`hikari.users.User`, :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ] + owner : :obj:`~typing.Union` [ :obj:`~hikari.users.User`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] If specified, the object or ID of the new guild owner. splash_data : ``hikari.internal.conversions.FileLikeT`` If specified, the new new splash image file data. - system_channel : :obj:`typing.Union` [ :obj:`hikari.channels.GuildVoiceChannel`, :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ] + system_channel : :obj:`~typing.Union` [ :obj:`~hikari.channels.GuildVoiceChannel`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] If specified, the object or ID of the new system channel. - reason : :obj:`str` + reason : :obj:`~str` If specified, the audit log reason explaining why the operation was performed. Returns ------- - :obj:`hikari.guilds.Guild` + :obj:`~hikari.guilds.Guild` The edited guild object. Raises ------ - :obj:`hikari.errors.BadRequestHTTPError` + :obj:`~hikari.errors.BadRequestHTTPError` If any invalid snowflake IDs are passed; a snowflake may be invalid due to it being outside of the range of a 64 bit integer. - :obj:`hikari.errors.NotFoundHTTPError` + :obj:`~hikari.errors.NotFoundHTTPError` If the guild is not found. - :obj:`hikari.errors.ForbiddenHTTPError` + :obj:`~hikari.errors.ForbiddenHTTPError` If you lack the ``MANAGE_GUILD`` permission or are not in the guild. """ payload = await self._session.modify_guild( @@ -2020,17 +2020,17 @@ async def delete_guild(self, guild: snowflakes.HashableT[guilds.Guild]) -> None: Parameters ---------- - guild : :obj:`typing.Union` [ :obj:`hikari.guilds.Guild`, :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ] + guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] The object or ID of the guild to be deleted. Raises ------ - :obj:`hikari.errors.BadRequestHTTPError` + :obj:`~hikari.errors.BadRequestHTTPError` If any invalid snowflake IDs are passed; a snowflake may be invalid due to it being outside of the range of a 64 bit integer. - :obj:`hikari.errors.NotFoundHTTPError` + :obj:`~hikari.errors.NotFoundHTTPError` If the guild is not found. - :obj:`hikari.errors.ForbiddenHTTPError` + :obj:`~hikari.errors.ForbiddenHTTPError` If you are not the guild owner. """ await self._session.delete_guild( @@ -2044,22 +2044,22 @@ async def fetch_guild_channels( Parameters ---------- - guild : :obj:`typing.Union` [ :obj:`hikari.guilds.Guild`, :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ] + guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] The object or ID of the guild to get the channels from. Returns ------- - :obj:`typing.Sequence` [ :obj:`hikari.channels.GuildChannel` ] + :obj:`~typing.Sequence` [ :obj:`~hikari.channels.GuildChannel` ] A list of guild channel objects. Raises ------ - :obj:`hikari.errors.BadRequestHTTPError` + :obj:`~hikari.errors.BadRequestHTTPError` If any invalid snowflake IDs are passed; a snowflake may be invalid due to it being outside of the range of a 64 bit integer. - :obj:`hikari.errors.NotFoundHTTPError` + :obj:`~hikari.errors.NotFoundHTTPError` If the guild is not found. - :obj:`hikari.errors.ForbiddenHTTPError` + :obj:`~hikari.errors.ForbiddenHTTPError` If you are not in the guild. """ payload = await self._session.list_guild_channels( @@ -2086,60 +2086,60 @@ async def create_guild_channel( Parameters ---------- - guild : :obj:`typing.Union` [ :obj:`hikari.guilds.Guild`, :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ] + guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] The object or ID of the guild to create the channel in. - name : :obj:`str` + name : :obj:`~str` If specified, the name for the channel. This must be inclusively between ``1` and ``100`` characters in length. - channel_type: :obj:`typing.Union` [ :obj:`hikari.channels.ChannelType`, :obj:`int` ] + channel_type: :obj:`~typing.Union` [ :obj:`~hikari.channels.ChannelType`, :obj:`~int` ] If specified, the channel type, passing through a raw integer here may lead to unexpected behaviour. - position : :obj:`int` + position : :obj:`~int` If specified, the position to change the channel to. - topic : :obj:`str` + topic : :obj:`~str` If specified, the topic to set. This is only applicable to text channels. This must be inclusively between ``0`` and ``1024`` characters in length. - nsfw : :obj:`bool` + nsfw : :obj:`~bool` If specified, whether the channel will be marked as NSFW. Only applicable for text channels. - rate_limit_per_user : :obj:`typing.Union` [ :obj:`datetime.timedelta`, :obj:`int` ] + rate_limit_per_user : :obj:`~typing.Union` [ :obj:`~datetime.timedelta`, :obj:`~int` ] If specified, the second time delta the user has to wait before sending another message. This will not apply to bots, or to members with ``MANAGE_MESSAGES`` or ``MANAGE_CHANNEL`` permissions. This must be inclusively between ``0`` and ``21600`` seconds. - bitrate : :obj:`int` + bitrate : :obj:`~int` If specified, the bitrate in bits per second allowable for the channel. This only applies to voice channels and must be inclusively between ``8000`` and ``96000`` for normal servers or ``8000`` and ``128000`` for VIP servers. - user_limit : :obj:`int` + user_limit : :obj:`~int` If specified, the max number of users to allow in a voice channel. This must be between ``0`` and ``99`` inclusive, where ``0`` implies no limit. - permission_overwrites : :obj:`typing.Sequence` [ :obj:`hikari.channels.PermissionOverwrite` ] + permission_overwrites : :obj:`~typing.Sequence` [ :obj:`~hikari.channels.PermissionOverwrite` ] If specified, the list of permission overwrite objects that are category specific to replace the existing overwrites with. - parent_category : :obj:`typing.Union` [ :obj:`hikari.channels.GuildCategory`, :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ] + parent_category : :obj:`~typing.Union` [ :obj:`~hikari.channels.GuildCategory`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] If specified, the object or ID of the parent category to set for the channel. - reason : :obj:`str` + reason : :obj:`~str` If specified, the audit log reason explaining why the operation was performed. Returns ------- - :obj:`hikari.channels.GuildChannel` + :obj:`~hikari.channels.GuildChannel` The newly created channel object. Raises ------ - :obj:`hikari.errors.NotFoundHTTPError` + :obj:`~hikari.errors.NotFoundHTTPError` If the guild is not found. - :obj:`hikari.errors.ForbiddenHTTPError` + :obj:`~hikari.errors.ForbiddenHTTPError` If you lack the ``MANAGE_CHANNEL`` permission or are not in the guild. - :obj:`hikari.errors.BadRequestHTTPError` + :obj:`~hikari.errors.BadRequestHTTPError` If you provide incorrect options for the corresponding channel type (e.g. a ``bitrate`` for a text channel). If any invalid snowflake IDs are passed; a snowflake may be invalid @@ -2183,24 +2183,24 @@ async def reposition_guild_channels( Parameters ---------- - guild : :obj:`typing.Union` [ :obj:`hikari.guilds.Guild`, :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ] + guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] The object or ID of the guild in which to edit the channels. - channel : :obj:`typing.Tuple` [ :obj:`int` , :obj:`typing.Union` [ :obj:`hikari.channels.GuildChannel`, :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ] ] + channel : :obj:`~typing.Tuple` [ :obj:`~int` , :obj:`~typing.Union` [ :obj:`~hikari.channels.GuildChannel`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] ] The first channel to change the position of. This is a tuple of the integer position the channel object or ID. - *additional_channels : :obj:`typing.Tuple` [ :obj:`int`, :obj:`typing.Union` [ :obj:`hikari.channels.GuildChannel`, :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ] ] + *additional_channels : :obj:`~typing.Tuple` [ :obj:`~int`, :obj:`~typing.Union` [ :obj:`~hikari.channels.GuildChannel`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] ] Optional additional channels to change the position of. These must be tuples of integer positions to change to and the channel object or ID and the. Raises ------ - :obj:`hikari.errors.NotFoundHTTPError` + :obj:`~hikari.errors.NotFoundHTTPError` If either the guild or any of the channels aren't found. - :obj:`hikari.errors.ForbiddenHTTPError` + :obj:`~hikari.errors.ForbiddenHTTPError` If you either lack the ``MANAGE_CHANNELS`` permission or are not a member of said guild or are not in the guild. - :obj:`hikari.errors.BadRequestHTTPError` + :obj:`~hikari.errors.BadRequestHTTPError` If you provide anything other than the ``id`` and ``position`` fields for the channels. If any invalid snowflake IDs are passed; a snowflake may be invalid @@ -2221,24 +2221,24 @@ async def fetch_member( Parameters ---------- - guild : :obj:`typing.Union` [ :obj:`hikari.guilds.Guild`, :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ] + guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] The object or ID of the guild to get the member from. - user : :obj:`typing.Union` [ :obj:`hikari.users.User`, :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ] + user : :obj:`~typing.Union` [ :obj:`~hikari.users.User`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] The object or ID of the member to get. Returns ------- - :obj:`hikari.guilds.GuildMember` + :obj:`~hikari.guilds.GuildMember` The requested member object. Raises ------ - :obj:`hikari.errors.BadRequestHTTPError` + :obj:`~hikari.errors.BadRequestHTTPError` If any invalid snowflake IDs are passed; a snowflake may be invalid due to it being outside of the range of a 64 bit integer. - :obj:`hikari.errors.NotFoundHTTPError` + :obj:`~hikari.errors.NotFoundHTTPError` If either the guild or the member aren't found. - :obj:`hikari.errors.ForbiddenHTTPError` + :obj:`~hikari.errors.ForbiddenHTTPError` If you don't have access to the target guild. """ payload = await self._session.get_guild_member( @@ -2262,12 +2262,12 @@ def fetch_members_after( Parameters ---------- - guild : :obj:`typing.Union` [ :obj:`hikari.guilds.Guild`, :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ] + guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] The object or ID of the guild to get the members from. - limit : :obj:`int` + limit : :obj:`~int` If specified, the maximum number of members this iterator should return. - after : :obj:`typing.Union` [ :obj:`datetime.datetime`, :obj:`hikari.users.User`, :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ] + after : :obj:`~typing.Union` [ :obj:`~datetime.datetime`, :obj:`~hikari.users.User`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] The object or ID of the user this iterator should start after if specified, else this will start at the oldest user. @@ -2281,17 +2281,17 @@ def fetch_members_after( Returns ------- - :obj:`typing.AsyncIterator` [ :obj:`hikari.guilds.GuildMember` ] + :obj:`~typing.AsyncIterator` [ :obj:`~hikari.guilds.GuildMember` ] An async iterator of member objects. Raises ------ - :obj:`hikari.errors.BadRequestHTTPError` + :obj:`~hikari.errors.BadRequestHTTPError` If any invalid snowflake IDs are passed; a snowflake may be invalid due to it being outside of the range of a 64 bit integer. - :obj:`hikari.errors.NotFoundHTTPError` + :obj:`~hikari.errors.NotFoundHTTPError` If the guild is not found. - :obj:`hikari.errors.ForbiddenHTTPError` + :obj:`~hikari.errors.ForbiddenHTTPError` If you are not in the guild. """ if isinstance(after, datetime.datetime): @@ -2324,39 +2324,39 @@ async def update_member( Parameters ---------- - guild : :obj:`typing.Union` [ :obj:`hikari.guilds.Guild`, :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ] + guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] The object or ID of the guild to edit the member from. - user : :obj:`typing.Union` [ :obj:`hikari.guilds.GuildMember`, :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ] + user : :obj:`~typing.Union` [ :obj:`~hikari.guilds.GuildMember`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] The object or ID of the member to edit. - nickname : :obj:`str`, optional - If specified, the new nickname string. Setting it to :obj:`None` + nickname : :obj:`~str`, optional + If specified, the new nickname string. Setting it to :obj:`~None` explicitly will clear the nickname. - roles : :obj:`typing.Sequence` [ :obj:`typing.Union` [ :obj:`hikari.guilds.GuildRole`, :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ] ] + roles : :obj:`~typing.Sequence` [ :obj:`~typing.Union` [ :obj:`~hikari.guilds.GuildRole`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] ] If specified, a list of role IDs the member should have. - mute : :obj:`bool` + mute : :obj:`~bool` If specified, whether the user should be muted in the voice channel or not. - deaf : :obj:`bool` + deaf : :obj:`~bool` If specified, whether the user should be deafen in the voice channel or not. - voice_channel : :obj:`typing.Union` [ :obj:`hikari.channels.GuildVoiceChannel`, :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ], optional + voice_channel : :obj:`~typing.Union` [ :obj:`~hikari.channels.GuildVoiceChannel`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ], optional If specified, the ID of the channel to move the member to. Setting - it to :obj:`None` explicitly will disconnect the user. - reason : :obj:`str` + it to :obj:`~None` explicitly will disconnect the user. + reason : :obj:`~str` If specified, the audit log reason explaining why the operation was performed. Raises ------ - :obj:`hikari.errors.NotFoundHTTPError` + :obj:`~hikari.errors.NotFoundHTTPError` If either the guild, user, channel or any of the roles aren't found. - :obj:`hikari.errors.ForbiddenHTTPError` + :obj:`~hikari.errors.ForbiddenHTTPError` If you lack any of the applicable permissions (``MANAGE_NICKNAMES``, ``MANAGE_ROLES``, ``MUTE_MEMBERS``, ``DEAFEN_MEMBERS`` or ``MOVE_MEMBERS``). Note that to move a member you must also have permission to connect to the end channel. This will also be raised if you're not in the guild. - :obj:`hikari.errors.BadRequestHTTPError` + :obj:`~hikari.errors.BadRequestHTTPError` If you pass ``mute``, ``deaf`` or ``channel_id`` while the member is not connected to a voice channel. If any invalid snowflake IDs are passed; a snowflake may be invalid @@ -2388,22 +2388,22 @@ async def update_my_member_nickname( Parameters ---------- - guild : :obj:`typing.Union` [ :obj:`hikari.guilds.Guild`, :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ] + guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] The object or ID of the guild you want to change the nick on. - nickname : :obj:`str`, optional + nickname : :obj:`~str`, optional The new nick string. Setting this to `None` clears the nickname. - reason : :obj:`str` + reason : :obj:`~str` If specified, the audit log reason explaining why the operation was performed. Raises ------ - :obj:`hikari.errors.NotFoundHTTPError` + :obj:`~hikari.errors.NotFoundHTTPError` If the guild is not found. - :obj:`hikari.errors.ForbiddenHTTPError` + :obj:`~hikari.errors.ForbiddenHTTPError` If you lack the ``CHANGE_NICKNAME`` permission or are not in the guild. - :obj:`hikari.errors.BadRequestHTTPError` + :obj:`~hikari.errors.BadRequestHTTPError` If you provide a disallowed nickname, one that is too long, or one that is empty. If any invalid snowflake IDs are passed; a snowflake may be invalid @@ -2427,24 +2427,24 @@ async def add_role_to_member( Parameters ---------- - guild : :obj:`typing.Union` [ :obj:`hikari.guilds.Guild`, :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ] + guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] The object or ID of the guild the member belongs to. - user : :obj:`typing.Union` [ :obj:`hikari.users.User`, :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ] + user : :obj:`~typing.Union` [ :obj:`~hikari.users.User`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] The object or ID of the member you want to add the role to. - role : :obj:`typing.Union` [ :obj:`hikari.guilds.GuildRole`, :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ] + role : :obj:`~typing.Union` [ :obj:`~hikari.guilds.GuildRole`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] The object or ID of the role you want to add. - reason : :obj:`str` + reason : :obj:`~str` If specified, the audit log reason explaining why the operation was performed. Raises ------ - :obj:`hikari.errors.BadRequestHTTPError` + :obj:`~hikari.errors.BadRequestHTTPError` If any invalid snowflake IDs are passed; a snowflake may be invalid due to it being outside of the range of a 64 bit integer. - :obj:`hikari.errors.NotFoundHTTPError` + :obj:`~hikari.errors.NotFoundHTTPError` If either the guild, member or role aren't found. - :obj:`hikari.errors.ForbiddenHTTPError` + :obj:`~hikari.errors.ForbiddenHTTPError` If you lack the ``MANAGE_ROLES`` permission or are not in the guild. """ await self._session.add_guild_member_role( @@ -2466,24 +2466,24 @@ async def remove_role_from_member( Parameters ---------- - guild : :obj:`typing.Union` [ :obj:`hikari.guilds.Guild`, :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ] + guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] The object or ID of the guild the member belongs to. - user : :obj:`typing.Union` [ :obj:`hikari.users.User`, :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ] + user : :obj:`~typing.Union` [ :obj:`~hikari.users.User`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] The object or ID of the member you want to remove the role from. - role : :obj:`typing.Union` [ :obj:`hikari.guilds.GuildRole`, :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ] + role : :obj:`~typing.Union` [ :obj:`~hikari.guilds.GuildRole`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] The object or ID of the role you want to remove. - reason : :obj:`str` + reason : :obj:`~str` If specified, the audit log reason explaining why the operation was performed. Raises ------ - :obj:`hikari.errors.BadRequestHTTPError` + :obj:`~hikari.errors.BadRequestHTTPError` If any invalid snowflake IDs are passed; a snowflake may be invalid due to it being outside of the range of a 64 bit integer. - :obj:`hikari.errors.NotFoundHTTPError` + :obj:`~hikari.errors.NotFoundHTTPError` If either the guild, member or role aren't found. - :obj:`hikari.errors.ForbiddenHTTPError` + :obj:`~hikari.errors.ForbiddenHTTPError` If you lack the ``MANAGE_ROLES`` permission or are not in the guild. """ await self._session.remove_guild_member_role( @@ -2500,22 +2500,22 @@ async def kick_member( Parameters ---------- - guild : :obj:`typing.Union` [ :obj:`hikari.guilds.Guild`, :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ] + guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] The object or ID of the guild the member belongs to. - user : :obj:`typing.Union` [ :obj:`hikari.users.User`, :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ] + user : :obj:`~typing.Union` [ :obj:`~hikari.users.User`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] The object or ID of the member you want to kick. - reason : :obj:`str` + reason : :obj:`~str` If specified, the audit log reason explaining why the operation was performed. Raises ------ - :obj:`hikari.errors.BadRequestHTTPError` + :obj:`~hikari.errors.BadRequestHTTPError` If any invalid snowflake IDs are passed; a snowflake may be invalid due to it being outside of the range of a 64 bit integer. - :obj:`hikari.errors.NotFoundHTTPError` + :obj:`~hikari.errors.NotFoundHTTPError` If either the guild or member aren't found. - :obj:`hikari.errors.ForbiddenHTTPError` + :obj:`~hikari.errors.ForbiddenHTTPError` If you lack the ``KICK_MEMBERS`` permission or are not in the guild. """ await self._session.remove_guild_member( @@ -2531,25 +2531,25 @@ async def fetch_ban( Parameters ---------- - guild : :obj:`typing.Union` [ :obj:`hikari.guilds.Guild`, :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ] + guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] The object or ID of the guild you want to get the ban from. - user : :obj:`typing.Union` [ :obj:`hikari.users.User`, :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ] + user : :obj:`~typing.Union` [ :obj:`~hikari.users.User`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] The object or ID of the user to get the ban information for. Returns ------- - :obj:`hikari.guilds.GuildMemberBan` + :obj:`~hikari.guilds.GuildMemberBan` A ban object for the requested user. Raises ------ - :obj:`hikari.errors.BadRequestHTTPError` + :obj:`~hikari.errors.BadRequestHTTPError` If any invalid snowflake IDs are passed; a snowflake may be invalid due to it being outside of the range of a 64 bit integer. - :obj:`hikari.errors.NotFoundHTTPError` + :obj:`~hikari.errors.NotFoundHTTPError` If either the guild or the user aren't found, or if the user is not banned. - :obj:`hikari.errors.ForbiddenHTTPError` + :obj:`~hikari.errors.ForbiddenHTTPError` If you lack the ``BAN_MEMBERS`` permission or are not in the guild. """ payload = await self._session.get_guild_ban( @@ -2563,22 +2563,22 @@ async def fetch_bans(self, guild: snowflakes.HashableT[guilds.Guild],) -> typing Parameters ---------- - guild : :obj:`typing.Union` [ :obj:`hikari.guilds.Guild`, :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ] + guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] The object or ID of the guild you want to get the bans from. Returns ------- - :obj:`typing.Sequence` [ :obj:`hikari.guilds.GuildMemberBan` ] + :obj:`~typing.Sequence` [ :obj:`~hikari.guilds.GuildMemberBan` ] A list of ban objects. Raises ------ - :obj:`hikari.errors.BadRequestHTTPError` + :obj:`~hikari.errors.BadRequestHTTPError` If any invalid snowflake IDs are passed; a snowflake may be invalid due to it being outside of the range of a 64 bit integer. - :obj:`hikari.errors.NotFoundHTTPError` + :obj:`~hikari.errors.NotFoundHTTPError` If the guild is not found. - :obj:`hikari.errors.ForbiddenHTTPError` + :obj:`~hikari.errors.ForbiddenHTTPError` If you lack the ``BAN_MEMBERS`` permission or are not in the guild. """ payload = await self._session.get_guild_bans( @@ -2598,25 +2598,25 @@ async def ban_member( Parameters ---------- - guild : :obj:`typing.Union` [ :obj:`hikari.guilds.Guild`, :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ] + guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] The object or ID of the guild the member belongs to. - user : :obj:`typing.Union` [ :obj:`hikari.users.User`, :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ] + user : :obj:`~typing.Union` [ :obj:`~hikari.users.User`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] The object or ID of the member you want to ban. - delete_message_days : :obj:`typing.Union` [ :obj:`datetime.timedelta`, :obj:`int` ] + delete_message_days : :obj:`~typing.Union` [ :obj:`~datetime.timedelta`, :obj:`~int` ] If specified, the tim delta of how many days of messages from the user should be removed. - reason : :obj:`str` + reason : :obj:`~str` If specified, the audit log reason explaining why the operation was performed. Raises ------ - :obj:`hikari.errors.BadRequestHTTPError` + :obj:`~hikari.errors.BadRequestHTTPError` If any invalid snowflake IDs are passed; a snowflake may be invalid due to it being outside of the range of a 64 bit integer. - :obj:`hikari.errors.NotFoundHTTPError` + :obj:`~hikari.errors.NotFoundHTTPError` If either the guild or member aren't found. - :obj:`hikari.errors.ForbiddenHTTPError` + :obj:`~hikari.errors.ForbiddenHTTPError` If you lack the ``BAN_MEMBERS`` permission or are not in the guild. """ await self._session.create_guild_ban( @@ -2633,23 +2633,23 @@ async def unban_member( Parameters ---------- - guild : :obj:`typing.Union` [ :obj:`hikari.guilds.Guild`, :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ] + guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] The object or ID of the guild to un-ban the user from. - user : :obj:`typing.Union` [ :obj:`hikari.users.User`, :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ] + user : :obj:`~typing.Union` [ :obj:`~hikari.users.User`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] The ID of the user you want to un-ban. - reason : :obj:`str` + reason : :obj:`~str` If specified, the audit log reason explaining why the operation was performed. Raises ------ - :obj:`hikari.errors.BadRequestHTTPError` + :obj:`~hikari.errors.BadRequestHTTPError` If any invalid snowflake IDs are passed; a snowflake may be invalid due to it being outside of the range of a 64 bit integer. - :obj:`hikari.errors.NotFoundHTTPError` + :obj:`~hikari.errors.NotFoundHTTPError` If either the guild or member aren't found, or the member is not banned. - :obj:`hikari.errors.ForbiddenHTTPError` + :obj:`~hikari.errors.ForbiddenHTTPError` If you lack the ``BAN_MEMBERS`` permission or are not a in the guild. """ @@ -2666,22 +2666,22 @@ async def fetch_roles( Parameters ---------- - guild : :obj:`typing.Union` [ :obj:`hikari.guilds.Guild`, :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ] + guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] The object or ID of the guild you want to get the roles from. Returns ------- - :obj:`typing.Mapping` [ :obj:`hikari.snowflakes.Snowflake`, :obj:`hikari.guilds.GuildRole` ] + :obj:`~typing.Mapping` [ :obj:`~hikari.snowflakes.Snowflake`, :obj:`~hikari.guilds.GuildRole` ] A list of role objects. Raises ------ - :obj:`hikari.errors.BadRequestHTTPError` + :obj:`~hikari.errors.BadRequestHTTPError` If any invalid snowflake IDs are passed; a snowflake may be invalid due to it being outside of the range of a 64 bit integer. - :obj:`hikari.errors.NotFoundHTTPError` + :obj:`~hikari.errors.NotFoundHTTPError` If the guild is not found. - :obj:`hikari.errors.ForbiddenHTTPError` + :obj:`~hikari.errors.ForbiddenHTTPError` If you're not in the guild. """ payload = await self._session.get_guild_roles( @@ -2704,37 +2704,37 @@ async def create_role( Parameters ---------- - guild : :obj:`typing.Union` [ :obj:`hikari.guilds.Guild`, :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ] + guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] The object or ID of the guild you want to create the role on. - name : :obj:`str` + name : :obj:`~str` If specified, the new role name string. - permissions : :obj:`typing.Union` [ :obj:`hikari.permissions.Permission`, :obj:`int` ] + permissions : :obj:`~typing.Union` [ :obj:`~hikari.permissions.Permission`, :obj:`~int` ] If specified, the permissions integer for the role, passing a raw integer rather than the int flag may lead to unexpected results. - color : :obj:`typing.Union` [ :obj:`hikari.colors.Color`, :obj:`int` ] + color : :obj:`~typing.Union` [ :obj:`~hikari.colors.Color`, :obj:`~int` ] If specified, the color for the role. - hoist : :obj:`bool` + hoist : :obj:`~bool` If specified, whether the role will be hoisted. - mentionable : :obj:`bool` + mentionable : :obj:`~bool` If specified, whether the role will be able to be mentioned by any user. - reason : :obj:`str` + reason : :obj:`~str` If specified, the audit log reason explaining why the operation was performed. Returns ------- - :obj:`hikari.guilds.GuildRole` + :obj:`~hikari.guilds.GuildRole` The newly created role object. Raises ------ - :obj:`hikari.errors.NotFoundHTTPError` + :obj:`~hikari.errors.NotFoundHTTPError` If the guild is not found. - :obj:`hikari.errors.ForbiddenHTTPError` + :obj:`~hikari.errors.ForbiddenHTTPError` If you lack the ``MANAGE_ROLES`` permission or you're not in the guild. - :obj:`hikari.errors.BadRequestHTTPError` + :obj:`~hikari.errors.BadRequestHTTPError` If you provide invalid values for the role attributes. If any invalid snowflake IDs are passed; a snowflake may be invalid due to it being outside of the range of a 64 bit integer. @@ -2760,28 +2760,28 @@ async def reposition_roles( Parameters ---------- - guild : :obj:`typing.Union` [ :obj:`hikari.guilds.Guild`, :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ] + guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] The ID of the guild the roles belong to. - role : :obj:`typing.Tuple` [ :obj:`int`, :obj:`typing.Union` [ :obj:`hikari.guilds.GuildRole`, :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ] ] + role : :obj:`~typing.Tuple` [ :obj:`~int`, :obj:`~typing.Union` [ :obj:`~hikari.guilds.GuildRole`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] ] The first role to move. This is a tuple of the integer position and the role object or ID. - *additional_roles : :obj:`typing.Tuple` [ :obj:`int`, :obj:`typing.Union` [ :obj:`hikari.guilds.GuildRole`, :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ] ] + *additional_roles : :obj:`~typing.Tuple` [ :obj:`~int`, :obj:`~typing.Union` [ :obj:`~hikari.guilds.GuildRole`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] ] Optional extra roles to move. These must be tuples of the integer position and the role object or ID. Returns ------- - :obj:`typing.Sequence` [ :obj:`hikari.guilds.GuildRole` ] + :obj:`~typing.Sequence` [ :obj:`~hikari.guilds.GuildRole` ] A list of all the guild roles. Raises ------ - :obj:`hikari.errors.NotFoundHTTPError` + :obj:`~hikari.errors.NotFoundHTTPError` If either the guild or any of the roles aren't found. - :obj:`hikari.errors.ForbiddenHTTPError` + :obj:`~hikari.errors.ForbiddenHTTPError` If you lack the ``MANAGE_ROLES`` permission or you're not in the guild. - :obj:`hikari.errors.BadRequestHTTPError` + :obj:`~hikari.errors.BadRequestHTTPError` If you provide invalid values for the `position` fields. If any invalid snowflake IDs are passed; a snowflake may be invalid due to it being outside of the range of a 64 bit integer. @@ -2811,39 +2811,39 @@ async def update_role( Parameters ---------- - guild : :obj:`typing.Union` [ :obj:`hikari.guilds.Guild`, :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ] + guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] The object or ID of the guild the role belong to. - role : :obj:`typing.Union` [ :obj:`hikari.guilds.GuildRole`, :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ] + role : :obj:`~typing.Union` [ :obj:`~hikari.guilds.GuildRole`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] The object or ID of the role you want to edit. - name : :obj:`str` + name : :obj:`~str` If specified, the new role's name string. - permissions : :obj:`typing.Union` [ :obj:`hikari.permissions.Permission`, :obj:`int` ] + permissions : :obj:`~typing.Union` [ :obj:`~hikari.permissions.Permission`, :obj:`~int` ] If specified, the new permissions integer for the role, passing a raw integer for this may lead to unexpected behaviour. - color : :obj:`typing.Union` [ :obj:`hikari.colors.Color`, :obj:`int` ] + color : :obj:`~typing.Union` [ :obj:`~hikari.colors.Color`, :obj:`~int` ] If specified, the new color for the new role passing a raw integer for this may lead to unexpected behaviour. - hoist : :obj:`bool` + hoist : :obj:`~bool` If specified, whether the role should hoist or not. - mentionable : :obj:`bool` + mentionable : :obj:`~bool` If specified, whether the role should be mentionable or not. - reason : :obj:`str` + reason : :obj:`~str` If specified, the audit log reason explaining why the operation was performed. Returns ------- - :obj:`hikari.guilds.GuildRole` + :obj:`~hikari.guilds.GuildRole` The edited role object. Raises ------ - :obj:`hikari.errors.NotFoundHTTPError` + :obj:`~hikari.errors.NotFoundHTTPError` If either the guild or role aren't found. - :obj:`hikari.errors.ForbiddenHTTPError` + :obj:`~hikari.errors.ForbiddenHTTPError` If you lack the ``MANAGE_ROLES`` permission or you're not in the guild. - :obj:`hikari.errors.BadRequestHTTPError` + :obj:`~hikari.errors.BadRequestHTTPError` If you provide invalid values for the role attributes. If any invalid snowflake IDs are passed; a snowflake may be invalid due to it being outside of the range of a 64 bit integer. @@ -2867,19 +2867,19 @@ async def delete_role( Parameters ---------- - guild : :obj:`typing.Union` [ :obj:`hikari.guilds.Guild`, :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ] + guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] The object or ID of the guild you want to remove the role from. - role : :obj:`typing.Union` [ :obj:`hikari.guilds.GuildRole`, :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ] + role : :obj:`~typing.Union` [ :obj:`~hikari.guilds.GuildRole`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] The object or ID of the role you want to delete. Raises ------ - :obj:`hikari.errors.BadRequestHTTPError` + :obj:`~hikari.errors.BadRequestHTTPError` If any invalid snowflake IDs are passed; a snowflake may be invalid due to it being outside of the range of a 64 bit integer. - :obj:`hikari.errors.NotFoundHTTPError` + :obj:`~hikari.errors.NotFoundHTTPError` If either the guild or the role aren't found. - :obj:`hikari.errors.ForbiddenHTTPError` + :obj:`~hikari.errors.ForbiddenHTTPError` If you lack the ``MANAGE_ROLES`` permission or are not in the guild. """ await self._session.delete_guild_role( @@ -2894,23 +2894,23 @@ async def estimate_guild_prune_count( Parameters ---------- - guild : :obj:`typing.Union` [ :obj:`hikari.guilds.Guild`, :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ] + guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] The object or ID of the guild you want to get the count for. - days : :obj:`typing.Union` [ :obj:`datetime.timedelta`, :obj:`int` ] + days : :obj:`~typing.Union` [ :obj:`~datetime.timedelta`, :obj:`~int` ] The time delta of days to count prune for (at least ``1``). Returns ------- - :obj:`int` + :obj:`~int` The number of members estimated to be pruned. Raises ------ - :obj:`hikari.errors.NotFoundHTTPError` + :obj:`~hikari.errors.NotFoundHTTPError` If the guild is not found. - :obj:`hikari.errors.ForbiddenHTTPError` + :obj:`~hikari.errors.ForbiddenHTTPError` If you lack the ``KICK_MEMBERS`` or you are not in the guild. - :obj:`hikari.errors.BadRequestHTTPError` + :obj:`~hikari.errors.BadRequestHTTPError` If you pass an invalid amount of days. If any invalid snowflake IDs are passed; a snowflake may be invalid due to it being outside of the range of a 64 bit integer. @@ -2932,30 +2932,30 @@ async def begin_guild_prune( Parameters ---------- - guild : :obj:`typing.Union` [ :obj:`hikari.guilds.Guild`, :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ] + guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] The object or ID of the guild you want to prune member of. - days : :obj:`typing.Union` [ :obj:`datetime.timedelta`, :obj:`int` ] + days : :obj:`~typing.Union` [ :obj:`~datetime.timedelta`, :obj:`~int` ] The time delta of inactivity days you want to use as filter. - compute_prune_count : :obj:`bool` + compute_prune_count : :obj:`~bool` Whether a count of pruned members is returned or not. Discouraged for large guilds out of politeness. - reason : :obj:`str` + reason : :obj:`~str` If specified, the audit log reason explaining why the operation was performed. Returns ------- - :obj:`int`, optional + :obj:`~int`, optional The number of members who were kicked if ``compute_prune_count`` - is :obj:`True`, else :obj:`None`. + is :obj:`~True`, else :obj:`~None`. Raises ------ - :obj:`hikari.errors.NotFoundHTTPError` + :obj:`~hikari.errors.NotFoundHTTPError` If the guild is not found: - :obj:`hikari.errors.ForbiddenHTTPError` + :obj:`~hikari.errors.ForbiddenHTTPError` If you lack the ``KICK_MEMBER`` permission or are not in the guild. - :obj:`hikari.errors.BadRequestHTTPError` + :obj:`~hikari.errors.BadRequestHTTPError` If you provide invalid values for the ``days`` or ``compute_prune_count`` fields. If any invalid snowflake IDs are passed; a snowflake may be invalid @@ -2975,22 +2975,22 @@ async def fetch_guild_voice_regions( Parameters ---------- - guild : :obj:`typing.Union` [ :obj:`hikari.guilds.Guild`, :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ] + guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] The object or ID of the guild to get the voice regions for. Returns ------- - :obj:`typing.Sequence` [ :obj:`hikari.voices.VoiceRegion` ] + :obj:`~typing.Sequence` [ :obj:`~hikari.voices.VoiceRegion` ] A list of voice region objects. Raises ------ - :obj:`hikari.errors.BadRequestHTTPError` + :obj:`~hikari.errors.BadRequestHTTPError` If any invalid snowflake IDs are passed; a snowflake may be invalid due to it being outside of the range of a 64 bit integer. - :obj:`hikari.errors.NotFoundHTTPError` + :obj:`~hikari.errors.NotFoundHTTPError` If the guild is not found. - :obj:`hikari.errors.ForbiddenHTTPError` + :obj:`~hikari.errors.ForbiddenHTTPError` If you are not in the guild. """ payload = await self._session.get_guild_voice_regions( @@ -3005,22 +3005,22 @@ async def fetch_guild_invites( Parameters ---------- - guild : :obj:`typing.Union` [ :obj:`hikari.guilds.Guild`, :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ] + guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] The object or ID of the guild to get the invites for. Returns ------- - :obj:`typing.Sequence` [ :obj:`hikari.invites.InviteWithMetadata` ] + :obj:`~typing.Sequence` [ :obj:`~hikari.invites.InviteWithMetadata` ] A list of invite objects (with metadata). Raises ------ - :obj:`hikari.errors.BadRequestHTTPError` + :obj:`~hikari.errors.BadRequestHTTPError` If any invalid snowflake IDs are passed; a snowflake may be invalid due to it being outside of the range of a 64 bit integer. - :obj:`hikari.errors.NotFoundHTTPError` + :obj:`~hikari.errors.NotFoundHTTPError` If the guild is not found. - :obj:`hikari.errors.ForbiddenHTTPError` + :obj:`~hikari.errors.ForbiddenHTTPError` If you lack the ``MANAGE_GUILD`` permission or are not in the guild. """ payload = await self._session.get_guild_invites( @@ -3035,22 +3035,22 @@ async def fetch_integrations( Parameters ---------- - guild : :obj:`typing.Union` [ :obj:`hikari.guilds.Guild`, :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ] + guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] The object or ID of the guild to get the integrations for. Returns ------- - :obj:`typing.Sequence` [ :obj:`hikari.guilds.GuildIntegration` ] + :obj:`~typing.Sequence` [ :obj:`~hikari.guilds.GuildIntegration` ] A list of integration objects. Raises ------ - :obj:`hikari.errors.BadRequestHTTPError` + :obj:`~hikari.errors.BadRequestHTTPError` If any invalid snowflake IDs are passed; a snowflake may be invalid due to it being outside of the range of a 64 bit integer. - :obj:`hikari.errors.NotFoundHTTPError` + :obj:`~hikari.errors.NotFoundHTTPError` If the guild is not found. - :obj:`hikari.errors.ForbiddenHTTPError` + :obj:`~hikari.errors.ForbiddenHTTPError` If you lack the ``MANAGE_GUILD`` permission or are not in the guild. """ payload = await self._session.get_guild_integrations( @@ -3072,31 +3072,31 @@ async def update_integration( Parameters ---------- - guild : :obj:`typing.Union` [ :obj:`hikari.guilds.Guild`, :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ] + guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] The object or ID of the guild to which the integration belongs to. - integration : :obj:`typing.Union` [ :obj:`hikari.guilds.GuildIntegration`, :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ] + integration : :obj:`~typing.Union` [ :obj:`~hikari.guilds.GuildIntegration`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] The object or ID of the integration to update. - expire_behaviour : :obj:`typing.Union` [ :obj:`hikari.guilds.IntegrationExpireBehaviour`, :obj:`int` ] + expire_behaviour : :obj:`~typing.Union` [ :obj:`~hikari.guilds.IntegrationExpireBehaviour`, :obj:`~int` ] If specified, the behaviour for when an integration subscription expires (passing a raw integer for this may lead to unexpected behaviour). - expire_grace_period : :obj:`typing.Union` [ :obj:`datetime.timedelta`, :obj:`int` ] + expire_grace_period : :obj:`~typing.Union` [ :obj:`~datetime.timedelta`, :obj:`~int` ] If specified, time time delta of how many days the integration will ignore lapsed subscriptions for. - enable_emojis : :obj:`bool` + enable_emojis : :obj:`~bool` If specified, whether emojis should be synced for this integration. - reason : :obj:`str` + reason : :obj:`~str` If specified, the audit log reason explaining why the operation was performed. Raises ------ - :obj:`hikari.errors.BadRequestHTTPError` + :obj:`~hikari.errors.BadRequestHTTPError` If any invalid snowflake IDs are passed; a snowflake may be invalid due to it being outside of the range of a 64 bit integer. - :obj:`hikari.errors.NotFoundHTTPError` + :obj:`~hikari.errors.NotFoundHTTPError` If either the guild or the integration aren't found. - :obj:`hikari.errors.ForbiddenHTTPError` + :obj:`~hikari.errors.ForbiddenHTTPError` If you lack the ``MANAGE_GUILD`` permission or are not in the guild. """ await self._session.modify_guild_integration( @@ -3121,22 +3121,22 @@ async def delete_integration( Parameters ---------- - guild : :obj:`typing.Union` [ :obj:`hikari.guilds.Guild`, :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ] + guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] The object or ID of the guild to which the integration belongs to. - integration : :obj:`typing.Union` [ :obj:`hikari.guilds.GuildIntegration`, :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ] + integration : :obj:`~typing.Union` [ :obj:`~hikari.guilds.GuildIntegration`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] The object or ID of the integration to delete. - reason : :obj:`str` + reason : :obj:`~str` If specified, the audit log reason explaining why the operation was performed. Raises ------ - :obj:`hikari.errors.BadRequestHTTPError` + :obj:`~hikari.errors.BadRequestHTTPError` If any invalid snowflake IDs are passed; a snowflake may be invalid due to it being outside of the range of a 64 bit integer. - :obj:`hikari.errors.NotFoundHTTPError` + :obj:`~hikari.errors.NotFoundHTTPError` If either the guild or the integration aren't found. - :obj:`hikari.errors.ForbiddenHTTPError` + :obj:`~hikari.errors.ForbiddenHTTPError` If you lack the `MANAGE_GUILD` permission or are not in the guild. """ await self._session.delete_guild_integration( @@ -3154,19 +3154,19 @@ async def sync_guild_integration( Parameters ---------- - guild : :obj:`typing.Union` [ :obj:`hikari.guilds.Guild`, :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ] + guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] The object or ID of the guild to which the integration belongs to. - integration : :obj:`typing.Union` [ :obj:`hikari.guilds.GuildIntegration`, :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ] + integration : :obj:`~typing.Union` [ :obj:`~hikari.guilds.GuildIntegration`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] The ID of the integration to sync. Raises ------ - :obj:`hikari.errors.BadRequestHTTPError` + :obj:`~hikari.errors.BadRequestHTTPError` If any invalid snowflake IDs are passed; a snowflake may be invalid due to it being outside of the range of a 64 bit integer. - :obj:`hikari.errors.NotFoundHTTPError` + :obj:`~hikari.errors.NotFoundHTTPError` If either the guild or the integration aren't found. - :obj:`hikari.errors.ForbiddenHTTPError` + :obj:`~hikari.errors.ForbiddenHTTPError` If you lack the ``MANAGE_GUILD`` permission or are not in the guild. """ await self._session.sync_guild_integration( @@ -3181,22 +3181,22 @@ async def fetch_guild_embed(self, guild: snowflakes.HashableT[guilds.Guild],) -> Parameters ---------- - guild : :obj:`typing.Union` [ :obj:`hikari.guilds.Guild`, :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ] + guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] The object or ID of the guild to get the embed for. Returns ------- - :obj:`hikari.guilds.GuildEmbed` + :obj:`~hikari.guilds.GuildEmbed` A guild embed object. Raises ------ - :obj:`hikari.errors.BadRequestHTTPError` + :obj:`~hikari.errors.BadRequestHTTPError` If any invalid snowflake IDs are passed; a snowflake may be invalid due to it being outside of the range of a 64 bit integer. - :obj:`hikari.errors.NotFoundHTTPError` + :obj:`~hikari.errors.NotFoundHTTPError` If the guild is not found. - :obj:`hikari.errors.ForbiddenHTTPError` + :obj:`~hikari.errors.ForbiddenHTTPError` If you either lack the ``MANAGE_GUILD`` permission or are not in the guild. """ @@ -3217,31 +3217,31 @@ async def update_guild_embed( Parameters ---------- - guild : :obj:`typing.Union` [ :obj:`hikari.guilds.Guild`, :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ] + guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] The object or ID of the guild to edit the embed for. - channel : :obj:`typing.Union` [ :obj:`hikari.channels.GuildChannel`, :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ], optional + channel : :obj:`~typing.Union` [ :obj:`~hikari.channels.GuildChannel`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ], optional If specified, the object or ID of the channel that this embed's - invite should target. Set to :obj:`None` to disable invites for this + invite should target. Set to :obj:`~None` to disable invites for this embed. - enabled : :obj:`bool` + enabled : :obj:`~bool` If specified, whether this embed should be enabled. - reason : :obj:`str` + reason : :obj:`~str` If specified, the audit log reason explaining why the operation was performed. Returns ------- - :obj:`hikari.guilds.GuildEmbed` + :obj:`~hikari.guilds.GuildEmbed` The updated embed object. Raises ------ - :obj:`hikari.errors.BadRequestHTTPError` + :obj:`~hikari.errors.BadRequestHTTPError` If any invalid snowflake IDs are passed; a snowflake may be invalid due to it being outside of the range of a 64 bit integer. - :obj:`hikari.errors.NotFoundHTTPError` + :obj:`~hikari.errors.NotFoundHTTPError` If the guild is not found. - :obj:`hikari.errors.ForbiddenHTTPError` + :obj:`~hikari.errors.ForbiddenHTTPError` If you either lack the ``MANAGE_GUILD`` permission or are not in the guild. """ @@ -3263,23 +3263,23 @@ async def fetch_guild_vanity_url(self, guild: snowflakes.HashableT[guilds.Guild] Parameters ---------- - guild : :obj:`typing.Union` [ :obj:`hikari.guilds.Guild`, :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ] + guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] The object or ID of the guild to get the vanity URL for. Returns ------- - :obj:`hikari.invites.VanityUrl` + :obj:`~hikari.invites.VanityUrl` A partial invite object containing the vanity URL in the ``code`` field. Raises ------ - :obj:`hikari.errors.BadRequestHTTPError` + :obj:`~hikari.errors.BadRequestHTTPError` If any invalid snowflake IDs are passed; a snowflake may be invalid due to it being outside of the range of a 64 bit integer. - :obj:`hikari.errors.NotFoundHTTPError` + :obj:`~hikari.errors.NotFoundHTTPError` If the guild is not found. - :obj:`hikari.errors.ForbiddenHTTPError` + :obj:`~hikari.errors.ForbiddenHTTPError` If you either lack the ``MANAGE_GUILD`` permission or are not in the guild. """ @@ -3293,14 +3293,14 @@ def format_guild_widget_image(self, guild: snowflakes.HashableT[guilds.Guild], * Parameters ---------- - guild : :obj:`typing.Union` [ :obj:`hikari.guilds.Guild`, :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ] + guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] The object or ID of the guild to form the widget. - style : :obj:`str` + style : :obj:`~str` If specified, the syle of the widget. Returns ------- - :obj:`str` + :obj:`~str` A URL to retrieve a PNG widget for your guild. Note @@ -3324,7 +3324,7 @@ async def fetch_invite( Parameters ---------- - invite : :obj:`typing.Union` [ :obj:`hikari.invites.Invite`, :obj:`str` ] + invite : :obj:`~typing.Union` [ :obj:`~hikari.invites.Invite`, :obj:`~str` ] The object or code of the wanted invite. with_counts : :bool: If specified, whether to attempt to count the number of @@ -3332,15 +3332,15 @@ async def fetch_invite( Returns ------- - :obj:`hikari.invites.Invite` + :obj:`~hikari.invites.Invite` The requested invite object. Raises ------ - :obj:`hikari.errors.BadRequestHTTPError` + :obj:`~hikari.errors.BadRequestHTTPError` If any invalid snowflake IDs are passed; a snowflake may be invalid due to it being outside of the range of a 64 bit integer. - :obj:`hikari.errors.NotFoundHTTPError` + :obj:`~hikari.errors.NotFoundHTTPError` If the invite is not found. """ payload = await self._session.get_invite(invite_code=getattr(invite, "code", invite), with_counts=with_counts) @@ -3351,20 +3351,20 @@ async def delete_invite(self, invite: typing.Union[invites.Invite, str]) -> None Parameters ---------- - invite : :obj:`typing.Union` [ :obj:`hikari.invites.Invite`, :obj:`str` ] + invite : :obj:`~typing.Union` [ :obj:`~hikari.invites.Invite`, :obj:`~str` ] The object or ID for the invite to be deleted. Returns ------- - :obj:`None` + :obj:`~None` Nothing, unlike what the API specifies. This is done to maintain consistency with other calls of a similar nature in this API wrapper. Raises ------ - :obj:`hikari.errors.NotFoundHTTPError` + :obj:`~hikari.errors.NotFoundHTTPError` If the invite is not found. - :obj:`hikari.errors.ForbiddenHTTPError` + :obj:`~hikari.errors.ForbiddenHTTPError` If you lack either ``MANAGE_CHANNELS`` on the channel the invite belongs to or ``MANAGE_GUILD`` for guild-global delete. """ @@ -3375,20 +3375,20 @@ async def fetch_user(self, user: snowflakes.HashableT[users.User]) -> users.User Parameters ---------- - user : :obj:`typing.Union` [ :obj:`hikari.users.User`, :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ] + user : :obj:`~typing.Union` [ :obj:`~hikari.users.User`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] The object or ID of the user to get. Returns ------- - :obj:`hikari.users.User` + :obj:`~hikari.users.User` The requested user object. Raises ------ - :obj:`hikari.errors.BadRequestHTTPError` + :obj:`~hikari.errors.BadRequestHTTPError` If any invalid snowflake IDs are passed; a snowflake may be invalid due to it being outside of the range of a 64 bit integer. - :obj:`hikari.errors.NotFoundHTTPError` + :obj:`~hikari.errors.NotFoundHTTPError` If the user is not found. """ payload = await self._session.get_user( @@ -3401,7 +3401,7 @@ async def fetch_my_application_info(self) -> oauth2.Application: Returns ------- - :obj:`hikari.oauth2.Application` + :obj:`~hikari.oauth2.Application` An application info object. """ payload = await self._session.get_current_application_info() @@ -3412,7 +3412,7 @@ async def fetch_me(self) -> users.MyUser: Returns ------- - :obj:`hikari.users.MyUser` + :obj:`~hikari.users.MyUser` The current user object. """ payload = await self._session.get_current_user() @@ -3425,20 +3425,20 @@ async def update_me( Parameters ---------- - username : :obj:`str` + username : :obj:`~str` If specified, the new username string. avatar_data : ``hikari.internal.conversions.FileLikeT``, optional If specified, the new avatar image data. - If it is :obj:`None`, the avatar is removed. + If it is :obj:`~None`, the avatar is removed. Returns ------- - :obj:`hikari.users.MyUser` + :obj:`~hikari.users.MyUser` The updated user object. Raises ------ - :obj:`hikari.errors.BadRequestHTTPError` + :obj:`~hikari.errors.BadRequestHTTPError` If you pass username longer than the limit (``2-32``) or an invalid image. """ payload = await self._session.modify_current_user( @@ -3459,7 +3459,7 @@ async def fetch_my_connections(self) -> typing.Sequence[oauth2.OwnConnection]: Returns ------- - :obj:`typing.Sequence` [ :obj:`hikari.oauth2.OwnConnection` ] + :obj:`~typing.Sequence` [ :obj:`~hikari.oauth2.OwnConnection` ] A list of connection objects. """ payload = await self._session.get_current_user_connections() @@ -3478,10 +3478,10 @@ def fetch_my_guilds_after( Parameters ---------- - after : :obj:`typing.Union` [ :obj:`datetime.datetime`, :obj:`hikari.guilds.Guild`, :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ] + after : :obj:`~typing.Union` [ :obj:`~datetime.datetime`, :obj:`~hikari.guilds.Guild`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] The object or ID of a guild to get guilds that were created after it if specified, else this will start at the oldest guild. - limit : :obj:`int` + limit : :obj:`~int` If specified, the maximum amount of guilds that this paginator should return. @@ -3494,14 +3494,14 @@ def fetch_my_guilds_after( Returns ------- - :obj:`typing.AsyncIterator` [ :obj:`hikari.oauth2.OwnGuild` ] + :obj:`~typing.AsyncIterator` [ :obj:`~hikari.oauth2.OwnGuild` ] An async iterator of partial guild objects. Raises ------ - :obj:`hikari.errors.NotFoundHTTPError` + :obj:`~hikari.errors.NotFoundHTTPError` If the guild is not found. - :obj:`hikari.errors.BadRequestHTTPError` + :obj:`~hikari.errors.BadRequestHTTPError` If any invalid snowflake IDs are passed; a snowflake may be invalid due to it being outside of the range of a 64 bit integer. """ @@ -3531,23 +3531,23 @@ def fetch_my_guilds_before( Parameters ---------- - before : :obj:`typing.Union` [ :obj:`datetime.datetime`, :obj:`hikari.guilds.Guild`, :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ] + before : :obj:`~typing.Union` [ :obj:`~datetime.datetime`, :obj:`~hikari.guilds.Guild`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] The object or ID of a guild to get guilds that were created before it if specified, else this will start at the newest guild. - limit : :obj:`int` + limit : :obj:`~int` If specified, the maximum amount of guilds that this paginator should return. Returns ------- - :obj:`typing.AsyncIterator` [ :obj:`hikari.oauth2.OwnGuild` ] + :obj:`~typing.AsyncIterator` [ :obj:`~hikari.oauth2.OwnGuild` ] An async iterator of partial guild objects. Raises ------ - :obj:`hikari.errors.NotFoundHTTPError` + :obj:`~hikari.errors.NotFoundHTTPError` If the guild is not found. - :obj:`hikari.errors.BadRequestHTTPError` + :obj:`~hikari.errors.BadRequestHTTPError` If any invalid snowflake IDs are passed; a snowflake may be invalid due to it being outside of the range of a 64 bit integer. """ @@ -3569,14 +3569,14 @@ async def leave_guild(self, guild: snowflakes.HashableT[guilds.Guild]) -> None: Parameters ---------- - guild : :obj:`typing.Union` [ :obj:`hikari.guilds.Guild`, :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ] + guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] The object or ID of the guild to leave. Raises ------ - :obj:`hikari.errors.NotFoundHTTPError` + :obj:`~hikari.errors.NotFoundHTTPError` If the guild is not found. - :obj:`hikari.errors.BadRequestHTTPError` + :obj:`~hikari.errors.BadRequestHTTPError` If any invalid snowflake IDs are passed; a snowflake may be invalid due to it being outside of the range of a 64 bit integer. """ @@ -3589,19 +3589,19 @@ async def create_dm_channel(self, recipient: snowflakes.HashableT[users.User]) - Parameters ---------- - recipient : :obj:`typing.Union` [ :obj:`hikari.users.User`, :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ] + recipient : :obj:`~typing.Union` [ :obj:`~hikari.users.User`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] The object or ID of the user to create the new DM channel with. Returns ------- - :obj:`hikari.channels.DMChannel` + :obj:`~hikari.channels.DMChannel` The newly created DM channel object. Raises ------ - :obj:`hikari.errors.NotFoundHTTPError` + :obj:`~hikari.errors.NotFoundHTTPError` If the recipient is not found. - :obj:`hikari.errors.BadRequestHTTPError` + :obj:`~hikari.errors.BadRequestHTTPError` If any invalid snowflake IDs are passed; a snowflake may be invalid due to it being outside of the range of a 64 bit integer. """ @@ -3615,7 +3615,7 @@ async def fetch_voice_regions(self) -> typing.Sequence[voices.VoiceRegion]: Returns ------- - :obj:`typing.Sequence` [ :obj:`hikari.voices.VoiceRegion` ] + :obj:`~typing.Sequence` [ :obj:`~hikari.voices.VoiceRegion` ] A list of voice regions available Note @@ -3637,29 +3637,29 @@ async def create_webhook( Parameters ---------- - channel : :obj:`typing.Union` [ :obj:`hikari.channels.GuildChannel`, :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ] + channel : :obj:`~typing.Union` [ :obj:`~hikari.channels.GuildChannel`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] The object or ID of the channel for webhook to be created in. - name : :obj:`str` + name : :obj:`~str` The webhook's name string. avatar_data : ``hikari.internal.conversions.FileLikeT`` If specified, the avatar image data. - reason : :obj:`str` + reason : :obj:`~str` If specified, the audit log reason explaining why the operation was performed. Returns ------- - :obj:`hikari.webhooks.Webhook` + :obj:`~hikari.webhooks.Webhook` The newly created webhook object. Raises ------ - :obj:`hikari.errors.NotFoundHTTPError` + :obj:`~hikari.errors.NotFoundHTTPError` If the channel is not found. - :obj:`hikari.errors.ForbiddenHTTPError` + :obj:`~hikari.errors.ForbiddenHTTPError` If you either lack the ``MANAGE_WEBHOOKS`` permission or can not see the given channel. - :obj:`hikari.errors.BadRequestHTTPError` + :obj:`~hikari.errors.BadRequestHTTPError` If the avatar image is too big or the format is invalid. If any invalid snowflake IDs are passed; a snowflake may be invalid due to it being outside of the range of a 64 bit integer. @@ -3679,22 +3679,22 @@ async def fetch_channel_webhooks( Parameters ---------- - channel : :obj:`typing.Union` [ :obj:`hikari.channels.GuildChannel`, :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ] + channel : :obj:`~typing.Union` [ :obj:`~hikari.channels.GuildChannel`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] The object or ID of the guild channel to get the webhooks from. Returns ------- - :obj:`typing.Sequence` [ :obj:`hikari.webhooks.Webhook` ] + :obj:`~typing.Sequence` [ :obj:`~hikari.webhooks.Webhook` ] A list of webhook objects for the give channel. Raises ------ - :obj:`hikari.errors.BadRequestHTTPError` + :obj:`~hikari.errors.BadRequestHTTPError` If any invalid snowflake IDs are passed; a snowflake may be invalid due to it being outside of the range of a 64 bit integer. - :obj:`hikari.errors.NotFoundHTTPError` + :obj:`~hikari.errors.NotFoundHTTPError` If the channel is not found. - :obj:`hikari.errors.ForbiddenHTTPError` + :obj:`~hikari.errors.ForbiddenHTTPError` If you either lack the ``MANAGE_WEBHOOKS`` permission or can not see the given channel. """ @@ -3710,22 +3710,22 @@ async def fetch_guild_webhooks( Parameters ---------- - guild : :obj:`typing.Union` [ :obj:`hikari.guilds.Guild`, :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ] + guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] The object or ID for the guild to get the webhooks from. Returns ------- - :obj:`typing.Sequence` [ :obj:`hikari.webhooks.Webhook` ] + :obj:`~typing.Sequence` [ :obj:`~hikari.webhooks.Webhook` ] A list of webhook objects for the given guild. Raises ------ - :obj:`hikari.errors.BadRequestHTTPError` + :obj:`~hikari.errors.BadRequestHTTPError` If any invalid snowflake IDs are passed; a snowflake may be invalid due to it being outside of the range of a 64 bit integer. - :obj:`hikari.errors.NotFoundHTTPError` + :obj:`~hikari.errors.NotFoundHTTPError` If the guild is not found. - :obj:`hikari.errors.ForbiddenHTTPError` + :obj:`~hikari.errors.ForbiddenHTTPError` If you either lack the ``MANAGE_WEBHOOKS`` permission or aren't a member of the given guild. """ @@ -3741,28 +3741,28 @@ async def fetch_webhook( Parameters ---------- - webhook : :obj:`typing.Union` [ :obj:`hikari.webhooks.Webhook`, :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ] + webhook : :obj:`~typing.Union` [ :obj:`~hikari.webhooks.Webhook`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] The object or ID of the webhook to get. - webhook_token : :obj:`str` + webhook_token : :obj:`~str` If specified, the webhook token to use to get it (bypassing this session's provided authorization ``token``). Returns ------- - :obj:`hikari.webhooks.Webhook` + :obj:`~hikari.webhooks.Webhook` The requested webhook object. Raises ------ - :obj:`hikari.errors.BadRequestHTTPError` + :obj:`~hikari.errors.BadRequestHTTPError` If any invalid snowflake IDs are passed; a snowflake may be invalid due to it being outside of the range of a 64 bit integer. - :obj:`hikari.errors.NotFoundHTTPError` + :obj:`~hikari.errors.NotFoundHTTPError` If the webhook is not found. - :obj:`hikari.errors.ForbiddenHTTPError` + :obj:`~hikari.errors.ForbiddenHTTPError` If you're not in the guild that owns this webhook or lack the ``MANAGE_WEBHOOKS`` permission. - :obj:`hikari.errors.UnauthorizedHTTPError` + :obj:`~hikari.errors.UnauthorizedHTTPError` If you pass a token that's invalid for the target webhook. """ payload = await self._session.get_webhook( @@ -3785,39 +3785,39 @@ async def update_webhook( Parameters ---------- - webhook : :obj:`typing.Union` [ :obj:`hikari.webhooks.Webhook`, :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ] + webhook : :obj:`~typing.Union` [ :obj:`~hikari.webhooks.Webhook`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] The object or ID of the webhook to edit. - webhook_token : :obj:`str` + webhook_token : :obj:`~str` If specified, the webhook token to use to modify it (bypassing this session's provided authorization ``token``). - name : :obj:`str` + name : :obj:`~str` If specified, the new name string. avatar_data : ``hikari.internal.conversions.FileLikeT``, optional - If specified, the new avatar image file object. If :obj:`None`, then + If specified, the new avatar image file object. If :obj:`~None`, then it is removed. - channel : :obj:`typing.Union` [ :obj:`hikari.channels.GuildChannel`, :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ] + channel : :obj:`~typing.Union` [ :obj:`~hikari.channels.GuildChannel`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] If specified, the object or ID of the new channel the given webhook should be moved to. - reason : :obj:`str` + reason : :obj:`~str` If specified, the audit log reason explaining why the operation was performed. Returns ------- - :obj:`hikari.webhooks.Webhook` + :obj:`~hikari.webhooks.Webhook` The updated webhook object. Raises ------ - :obj:`hikari.errors.BadRequestHTTPError` + :obj:`~hikari.errors.BadRequestHTTPError` If any invalid snowflake IDs are passed; a snowflake may be invalid due to it being outside of the range of a 64 bit integer. - :obj:`hikari.errors.NotFoundHTTPError` + :obj:`~hikari.errors.NotFoundHTTPError` If either the webhook or the channel aren't found. - :obj:`hikari.errors.ForbiddenHTTPError` + :obj:`~hikari.errors.ForbiddenHTTPError` If you either lack the ``MANAGE_WEBHOOKS`` permission or aren't a member of the guild this webhook belongs to. - :obj:`hikari.errors.UnauthorizedHTTPError` + :obj:`~hikari.errors.UnauthorizedHTTPError` If you pass a token that's invalid for the target webhook. """ payload = await self._session.modify_webhook( @@ -3845,23 +3845,23 @@ async def delete_webhook( Parameters ---------- - webhook : :obj:`typing.Union` [ :obj:`hikari.webhooks.Webhook`, :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ] + webhook : :obj:`~typing.Union` [ :obj:`~hikari.webhooks.Webhook`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] The object or ID of the webhook to delete - webhook_token : :obj:`str` + webhook_token : :obj:`~str` If specified, the webhook token to use to delete it (bypassing this session's provided authorization ``token``). Raises ------ - :obj:`hikari.errors.BadRequestHTTPError` + :obj:`~hikari.errors.BadRequestHTTPError` If any invalid snowflake IDs are passed; a snowflake may be invalid due to it being outside of the range of a 64 bit integer. - :obj:`hikari.errors.NotFoundHTTPError` + :obj:`~hikari.errors.NotFoundHTTPError` If the webhook is not found. - :obj:`hikari.errors.ForbiddenHTTPError` + :obj:`~hikari.errors.ForbiddenHTTPError` If you either lack the ``MANAGE_WEBHOOKS`` permission or aren't a member of the guild this webhook belongs to. - :obj:`hikari.errors.UnauthorizedHTTPError` + :obj:`~hikari.errors.UnauthorizedHTTPError` If you pass a token that's invalid for the target webhook. """ await self._session.delete_webhook( @@ -3889,61 +3889,61 @@ async def execute_webhook( Parameters ---------- - webhook : :obj:`typing.Union` [ :obj:`hikari.webhooks.Webhook`, :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ] + webhook : :obj:`~typing.Union` [ :obj:`~hikari.webhooks.Webhook`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] The object or ID of the webhook to execute. - webhook_token : :obj:`str` + webhook_token : :obj:`~str` The token of the webhook to execute. - content : :obj:`str` + content : :obj:`~str` If specified, the message content to send with the message. - username : :obj:`str` + username : :obj:`~str` If specified, the username to override the webhook's username for this request. - avatar_url : :obj:`str` + avatar_url : :obj:`~str` If specified, the url of an image to override the webhook's avatar with for this request. - tts : :obj:`bool` + tts : :obj:`~bool` If specified, whether the message will be sent as a TTS message. - wait : :obj:`bool` + wait : :obj:`~bool` If specified, whether this request should wait for the webhook to be executed and return the resultant message object. file : ``hikari.media.IO`` If specified, this is a file object to send along with the webhook as defined in :mod:`hikari.media`. - embeds : :obj:`typing.Sequence` [ :obj:`hikari.embeds.Embed` ] + embeds : :obj:`~typing.Sequence` [ :obj:`~hikari.embeds.Embed` ] If specified, a sequence of ``1`` to ``10`` embed objects to send with the embed. - mentions_everyone : :obj:`bool` + mentions_everyone : :obj:`~bool` Whether ``@everyone`` and ``@here`` mentions should be resolved by - discord and lead to actual pings, defaults to :obj:`True`. - user_mentions : :obj:`typing.Union` [ :obj:`typing.Collection` [ :obj:`typing.Union` [ :obj:`hikari.users.User`, :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ], :obj:`bool` ] + discord and lead to actual pings, defaults to :obj:`~True`. + user_mentions : :obj:`~typing.Union` [ :obj:`~typing.Collection` [ :obj:`~typing.Union` [ :obj:`~hikari.users.User`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ], :obj:`~bool` ] Either an array of user objects/IDs to allow mentions for, - :obj:`True` to allow all user mentions or :obj:`False` to block all - user mentions from resolving, defaults to :obj:`True`. - role_mentions : :obj:`typing.Union` [ :obj:`typing.Collection` [ :obj:`typing.Union` [ :obj:`hikari.guilds.GuildRole`, :obj:`hikari.snowflakes.Snowflake`, :obj:`int` ] ], :obj:`bool` ] + :obj:`~True` to allow all user mentions or :obj:`~False` to block all + user mentions from resolving, defaults to :obj:`~True`. + role_mentions : :obj:`~typing.Union` [ :obj:`~typing.Collection` [ :obj:`~typing.Union` [ :obj:`~hikari.guilds.GuildRole`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] ], :obj:`~bool` ] Either an array of guild role objects/IDs to allow mentions for, - :obj:`True` to allow all role mentions or :obj:`False` to block all - role mentions from resolving, defaults to :obj:`True`. + :obj:`~True` to allow all role mentions or :obj:`~False` to block all + role mentions from resolving, defaults to :obj:`~True`. Returns ------- - :obj:`hikari.messages.Message`, optional - The created message object, if ``wait`` is :obj:`True`, else - :obj:`None`. + :obj:`~hikari.messages.Message`, optional + The created message object, if ``wait`` is :obj:`~True`, else + :obj:`~None`. Raises ------ - :obj:`hikari.errors.NotFoundHTTPError` + :obj:`~hikari.errors.NotFoundHTTPError` If the channel ID or webhook ID is not found. - :obj:`hikari.errors.BadRequestHTTPError` + :obj:`~hikari.errors.BadRequestHTTPError` This can be raised if the file is too large; if the embed exceeds the defined limits; if the message content is specified only and empty or greater than ``2000`` characters; if neither content, file or embeds are specified. If any invalid snowflake IDs are passed; a snowflake may be invalid due to it being outside of the range of a 64 bit integer. - :obj:`hikari.errors.ForbiddenHTTPError` + :obj:`~hikari.errors.ForbiddenHTTPError` If you lack permissions to send to this channel. - :obj:`hikari.errors.UnauthorizedHTTPError` + :obj:`~hikari.errors.UnauthorizedHTTPError` If you pass a token that's invalid for the target webhook. """ payload = await self._session.execute_webhook( @@ -3984,7 +3984,7 @@ def safe_webhook_execute( This endpoint has the same signature as :attr:`execute_webhook` with the only difference being that ``mentions_everyone``, - ``user_mentions`` and ``role_mentions`` default to :obj:`False`. + ``user_mentions`` and ``role_mentions`` default to :obj:`~False`. """ return self.execute_webhook( webhook=webhook, diff --git a/hikari/clients/runnable.py b/hikari/clients/runnable.py index d8337e335e..324894ad08 100644 --- a/hikari/clients/runnable.py +++ b/hikari/clients/runnable.py @@ -34,7 +34,7 @@ class RunnableClient(abc.ABC): #: The logger to use for this client. #: - #: :type: :obj:`logging.Logger` + #: :type: :obj:`~logging.Logger` logger: logging.Logger @abc.abstractmethod diff --git a/hikari/clients/shard_clients.py b/hikari/clients/shard_clients.py index 324f3a5098..b123845b4f 100644 --- a/hikari/clients/shard_clients.py +++ b/hikari/clients/shard_clients.py @@ -16,7 +16,7 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . -"""Provides a facade around :obj:`hikari.net.shard.ShardConnection`. +"""Provides a facade around :obj:`~hikari.net.shard.ShardConnection`. This handles parsing and initializing the object from a configuration, as well as restarting it if it disconnects. @@ -87,27 +87,27 @@ class ShardClient(runnable.RunnableClient): Parameters ---------- - shard_id : :obj:`int` + shard_id : :obj:`~int` The ID of this specific shard. - shard_id : :obj:`int` + shard_id : :obj:`~int` The number of shards that make up this distributed application. - config : :obj:`hikari.clients.configs.WebsocketConfig` + config : :obj:`~hikari.clients.configs.WebsocketConfig` The gateway configuration to use to initialize this shard. - raw_event_consumer_impl : :obj:`hikari.state.raw_event_consumers.RawEventConsumer` + raw_event_consumer_impl : :obj:`~hikari.state.raw_event_consumers.RawEventConsumer` The consumer of a raw event. - url : :obj:`str` + url : :obj:`~str` The URL to connect the gateway to. - dispatcher : :obj:`hikari.state.event_dispatchers.EventDispatcher`, optional + dispatcher : :obj:`~hikari.state.event_dispatchers.EventDispatcher`, optional The high level event dispatcher to use for dispatching start and stop - events. Set this to :obj:`None` to disable that functionality (useful if + events. Set this to :obj:`~None` to disable that functionality (useful if you use a gateway manager to orchestrate multiple shards instead and - provide this functionality there). Defaults to :obj:`None` if + provide this functionality there). Defaults to :obj:`~None` if unspecified. Notes ----- Generally, you want to use - :obj:`hikari.clients.gateway_managers.GatewayManager` rather than this class + :obj:`~hikari.clients.gateway_managers.GatewayManager` rather than this class directly, as that will handle sharding where enabled and applicable, and provides a few more bits and pieces that may be useful such as state management and event dispatcher integration. and If you want to customize @@ -178,7 +178,7 @@ def connection(self) -> shard.ShardConnection: Returns ------- - :obj:`hikari.net.shard.ShardConnection` + :obj:`~hikari.net.shard.ShardConnection` The low-level gateway client used for this shard. """ return self._connection @@ -189,7 +189,7 @@ def shard_id(self) -> int: Returns ------- - :obj:`int` + :obj:`~int` The 0-indexed shard ID. """ return self._connection.shard_id @@ -200,7 +200,7 @@ def shard_count(self) -> int: Returns ------- - :obj:`int` + :obj:`~int` The number of shards that make up this bot. """ return self._connection.shard_count @@ -212,7 +212,7 @@ def status(self) -> guilds.PresenceStatus: # noqa: D401 Returns ------- - :obj:`hikari.guilds.PresenceStatus` + :obj:`~hikari.guilds.PresenceStatus` The current user status for this shard. """ return self._status @@ -224,8 +224,8 @@ def activity(self) -> typing.Optional[gateway_entities.GatewayActivity]: # noqa Returns ------- - :obj:`hikari.gateway_entities.GatewayActivity`, optional - The current activity for the user on this shard, or :obj:`None` if + :obj:`~hikari.gateway_entities.GatewayActivity`, optional + The current activity for the user on this shard, or :obj:`~None` if there is no activity. """ return self._activity @@ -236,21 +236,21 @@ def idle_since(self) -> typing.Optional[datetime.datetime]: Returns ------- - :obj:`datetime.datetime`, optional + :obj:`~datetime.datetime`, optional The timestamp when the user of this shard appeared to be idle, or - :obj:`None` if not applicable. + :obj:`~None` if not applicable. """ return self._idle_since # Ignore docstring not starting in an imperative mood @property def is_afk(self) -> bool: # noqa: D401 - """:obj:`True` if the user is AFK, :obj:`False` otherwise. + """:obj:`~True` if the user is AFK, :obj:`~False` otherwise. Returns ------- - :obj:`bool` - :obj:`True` if the user is AFK, :obj:`False` otherwise. + :obj:`~bool` + :obj:`~True` if the user is AFK, :obj:`~False` otherwise. """ return self._is_afk @@ -260,7 +260,7 @@ def latency(self) -> float: Returns ------- - :obj:`float` + :obj:`~float` The heartbeat latency in seconds. This will be ``float('nan')`` until the first heartbeat is performed. """ @@ -272,7 +272,7 @@ def heartbeat_interval(self) -> float: Returns ------- - :obj:`float` + :obj:`~float` The heartbeat interval in seconds. This will be ``float('nan')`` until the connection has received a ``HELLO`` payload. """ @@ -286,7 +286,7 @@ def reconnect_count(self) -> int: Returns ------- - :obj:`int` + :obj:`~int` The number of reconnects this shard has performed. """ return self._connection.reconnect_count @@ -297,7 +297,7 @@ def connection_state(self) -> ShardState: Returns ------- - :obj:`ShardState` + :obj:`~ShardState` The state of this shard. """ return self._shard_state @@ -493,14 +493,14 @@ async def update_presence( Parameters ---------- - status : :obj:`hikari.guilds.PresenceStatus` + status : :obj:`~hikari.guilds.PresenceStatus` If specified, the new status to set. - activity : :obj:`hikari.gateway_entities.GatewayActivity`, optional + activity : :obj:`~hikari.gateway_entities.GatewayActivity`, optional If specified, the new activity to set. - idle_since : :obj:`datetime.datetime`, optional + idle_since : :obj:`~datetime.datetime`, optional If specified, the time to show up as being idle since, or - :obj:`None` if not applicable. - is_afk : :obj:`bool` + :obj:`~None` if not applicable. + is_afk : :obj:`~bool` If specified, whether the user should be marked as AFK. """ status = self._status if status is ... else status diff --git a/hikari/colors.py b/hikari/colors.py index 6e97734e1c..1dbf659b09 100644 --- a/hikari/colors.py +++ b/hikari/colors.py @@ -38,7 +38,7 @@ class Color(int, typing.SupportsInt): This value is immutable. - This is a specialization of :obj:`int` which provides alternative overrides for common methods and color system + This is a specialization of :obj:`~int` which provides alternative overrides for common methods and color system conversions. This currently supports: @@ -168,33 +168,33 @@ def raw_hex_code(self) -> str: # Ignore docstring not starting in an imperative mood @property def is_web_safe(self) -> bool: # noqa: D401 - """:obj:`True` if the color is web safe, :obj:`False` otherwise.""" + """:obj:`~True` if the color is web safe, :obj:`~False` otherwise.""" hex_code = self.raw_hex_code return all(_all_same(*c) for c in (hex_code[:2], hex_code[2:4], hex_code[4:])) @classmethod def from_rgb(cls, red: int, green: int, blue: int) -> Color: - """Convert the given RGB to a :obj:`Color` object. + """Convert the given RGB to a :obj:`~Color` object. Each channel must be withing the range [0, 255] (0x0, 0xFF). Parameters ---------- - red : :obj:`int` + red : :obj:`~int` Red channel. - green : :obj:`int` + green : :obj:`~int` Green channel. - blue : :obj:`int` + blue : :obj:`~int` Blue channel. Returns ------- - :obj:`Color` + :obj:`~Color` A Color object. Raises ------ - :obj:`ValueError` + :obj:`~ValueError` If red, green, or blue are outside the range [0x0, 0xFF]. """ assertions.assert_in_range(red, 0, 0xFF, "red") @@ -205,28 +205,28 @@ def from_rgb(cls, red: int, green: int, blue: int) -> Color: @classmethod def from_rgb_float(cls, red_f: float, green_f: float, blue_f: float) -> Color: - """Convert the given RGB to a :obj:`Color` object. + """Convert the given RGB to a :obj:`~Color` object. The colorspace represented values have to be within the range [0, 1]. Parameters ---------- - red_f : :obj:`float` + red_f : :obj:`~float` Red channel. - green_f : :obj:`float` + green_f : :obj:`~float` Green channel. - blue_f : :obj:`float` + blue_f : :obj:`~float` Blue channel. Returns ------- - :obj:`Color` + :obj:`~Color` A Color object. Raises ------ - :obj:`ValueError` + :obj:`~ValueError` If red, green or blue are outside the range [0, 1]. """ assertions.assert_in_range(red_f, 0, 1, "red") @@ -237,7 +237,7 @@ def from_rgb_float(cls, red_f: float, green_f: float, blue_f: float) -> Color: @classmethod def from_hex_code(cls, hex_code: str) -> Color: - """Convert the given hexadecimal color code to a :obj:`Color`. + """Convert the given hexadecimal color code to a :obj:`~Color`. The inputs may be of the following format (case insensitive): ``1a2``, ``#1a2``, ``0x1a2`` (for websafe colors), or @@ -245,17 +245,17 @@ def from_hex_code(cls, hex_code: str) -> Color: Parameters ---------- - hex_code : :obj:`str` + hex_code : :obj:`~str` A hexadecimal color code to parse. Returns ------- - :obj:`Color` + :obj:`~Color` A corresponding Color object. Raises ------ - :obj:`ValueError` + :obj:`~ValueError` If ``hex_code`` is not a hexadecimal or is a inalid length. """ if hex_code.startswith("#"): @@ -279,16 +279,16 @@ def from_hex_code(cls, hex_code: str) -> Color: @classmethod def from_int(cls, i: typing.SupportsInt) -> Color: - """Convert the given :obj:`typing.SupportsInt` to a :obj:`Color`. + """Convert the given :obj:`~typing.SupportsInt` to a :obj:`~Color`. Parameters ---------- - i : :obj:`typing.SupportsInt` + i : :obj:`~typing.SupportsInt` The raw color integer. Returns ------- - :obj:`Color` + :obj:`~Color` The Color object. """ return cls(i) @@ -296,23 +296,23 @@ def from_int(cls, i: typing.SupportsInt) -> Color: # Partially chose to override these as the docstrings contain typos according to Sphinx. @classmethod def from_bytes(cls, bytes_: typing.Sequence[int], byteorder: str, *, signed: bool = True) -> Color: - """Convert the bytes to a :obj:`Color`. + """Convert the bytes to a :obj:`~Color`. Parameters ---------- - bytes_ : :obj:`typing.Iterable` [ :obj:`int` ] - A iterable of :obj:`int` byte values. + bytes_ : :obj:`~typing.Iterable` [ :obj:`~int` ] + A iterable of :obj:`~int` byte values. - byteorder : :obj:`str` + byteorder : :obj:`~str` The endianess of the value represented by the bytes. Can be ``"big"`` endian or ``"little"`` endian. - signed : :obj:`bool` + signed : :obj:`~bool` Whether the value is signed or unsigned. Returns ------- - :obj:`Color` + :obj:`~Color` The Color object. """ return Color(int.from_bytes(bytes_, byteorder, signed=signed)) @@ -322,19 +322,19 @@ def to_bytes(self, length: int, byteorder: str, *, signed: bool = True) -> bytes Parameters ---------- - length : :obj:`int` + length : :obj:`~int` The number of bytes to produce. Should be around ``3``, but not less. - byteorder : :obj:`str` + byteorder : :obj:`~str` The endianess of the value represented by the bytes. Can be ``"big"`` endian or ``"little"`` endian. - signed : :obj:`bool` + signed : :obj:`~bool` Whether the value is signed or unsigned. Returns ------- - :obj:`bytes` + :obj:`~bytes` The bytes representation of the Color. """ return int(self).to_bytes(length, byteorder, signed=signed) diff --git a/hikari/embeds.py b/hikari/embeds.py index 3963f2f9d9..f99cf94b66 100644 --- a/hikari/embeds.py +++ b/hikari/embeds.py @@ -46,12 +46,12 @@ class EmbedFooter(entities.HikariEntity, entities.Deserializable, entities.Seria #: The footer text. #: - #: :type: :obj:`str` + #: :type: :obj:`~str` text: str = marshaller.attrib(deserializer=str, serializer=str) #: The URL of the footer icon. #: - #: :type: :obj:`str`, optional + #: :type: :obj:`~str`, optional icon_url: typing.Optional[str] = marshaller.attrib( deserializer=str, serializer=str, if_undefined=None, default=None ) @@ -64,7 +64,7 @@ class EmbedFooter(entities.HikariEntity, entities.Deserializable, entities.Seria #: will be ignored during serialization. #: #: - #: :type: :obj:`str`, optional + #: :type: :obj:`~str`, optional proxy_icon_url: typing.Optional[str] = marshaller.attrib( deserializer=str, transient=True, if_undefined=None, default=None ) @@ -77,7 +77,7 @@ class EmbedImage(entities.HikariEntity, entities.Deserializable, entities.Serial #: The URL of the image. #: - #: :type: :obj:`str`, optional + #: :type: :obj:`~str`, optional url: typing.Optional[str] = marshaller.attrib(deserializer=str, serializer=str, if_undefined=None, default=None) #: The proxied URL of the image. @@ -88,7 +88,7 @@ class EmbedImage(entities.HikariEntity, entities.Deserializable, entities.Serial #: will be ignored during serialization. #: #: - #: :type: :obj:`str`, optional + #: :type: :obj:`~str`, optional proxy_url: typing.Optional[str] = marshaller.attrib( deserializer=str, transient=True, if_undefined=None, default=None ) @@ -101,7 +101,7 @@ class EmbedImage(entities.HikariEntity, entities.Deserializable, entities.Serial #: will be ignored during serialization. #: #: - #: :type: :obj:`int`, optional + #: :type: :obj:`~int`, optional height: typing.Optional[int] = marshaller.attrib(deserializer=int, transient=True, if_undefined=None, default=None) #: The width of the image. @@ -112,7 +112,7 @@ class EmbedImage(entities.HikariEntity, entities.Deserializable, entities.Serial #: will be ignored during serialization. #: #: - #: :type: :obj:`int`, optional + #: :type: :obj:`~int`, optional width: typing.Optional[int] = marshaller.attrib(deserializer=int, transient=True, if_undefined=None, default=None) @@ -123,7 +123,7 @@ class EmbedThumbnail(entities.HikariEntity, entities.Deserializable, entities.Se #: The URL of the thumbnail. #: - #: :type: :obj:`str`, optional + #: :type: :obj:`~str`, optional url: typing.Optional[str] = marshaller.attrib(deserializer=str, serializer=str, if_undefined=None, default=None) #: The proxied URL of the thumbnail. @@ -134,7 +134,7 @@ class EmbedThumbnail(entities.HikariEntity, entities.Deserializable, entities.Se #: will be ignored during serialization. #: #: - #: :type: :obj:`str`, optional + #: :type: :obj:`~str`, optional proxy_url: typing.Optional[str] = marshaller.attrib( deserializer=str, transient=True, if_undefined=None, default=None ) @@ -147,7 +147,7 @@ class EmbedThumbnail(entities.HikariEntity, entities.Deserializable, entities.Se #: will be ignored during serialization. #: #: - #: :type: :obj:`int`, optional + #: :type: :obj:`~int`, optional height: typing.Optional[int] = marshaller.attrib(deserializer=int, transient=True, if_undefined=None, default=None) #: The width of the thumbnail. @@ -158,7 +158,7 @@ class EmbedThumbnail(entities.HikariEntity, entities.Deserializable, entities.Se #: will be ignored during serialization. #: #: - #: :type: :obj:`int`, optional + #: :type: :obj:`~int`, optional width: typing.Optional[int] = marshaller.attrib(deserializer=int, transient=True, if_undefined=None, default=None) @@ -175,17 +175,17 @@ class EmbedVideo(entities.HikariEntity, entities.Deserializable): #: The URL of the video. #: - #: :type: :obj:`str`, optional + #: :type: :obj:`~str`, optional url: typing.Optional[str] = marshaller.attrib(deserializer=str, if_undefined=None, default=None) #: The height of the video. #: - #: :type: :obj:`int`, optional + #: :type: :obj:`~int`, optional height: typing.Optional[int] = marshaller.attrib(deserializer=int, if_undefined=None, default=None) #: The width of the video. #: - #: :type: :obj:`int`, optional + #: :type: :obj:`~int`, optional width: typing.Optional[int] = marshaller.attrib(deserializer=int, if_undefined=None, default=None) @@ -202,12 +202,12 @@ class EmbedProvider(entities.HikariEntity, entities.Deserializable): #: The name of the provider. #: - #: :type: :obj:`str`, optional + #: :type: :obj:`~str`, optional name: typing.Optional[str] = marshaller.attrib(deserializer=str, if_undefined=None, default=None) #: The URL of the provider. #: - #: :type: :obj:`str`, optional + #: :type: :obj:`~str`, optional url: typing.Optional[str] = marshaller.attrib(deserializer=str, if_undefined=None, if_none=None, default=None) @@ -218,17 +218,17 @@ class EmbedAuthor(entities.HikariEntity, entities.Deserializable, entities.Seria #: The name of the author. #: - #: :type: :obj:`str`, optional + #: :type: :obj:`~str`, optional name: typing.Optional[str] = marshaller.attrib(deserializer=str, serializer=str, if_undefined=None, default=None) #: The URL of the author. #: - #: :type: :obj:`str`, optional + #: :type: :obj:`~str`, optional url: typing.Optional[str] = marshaller.attrib(deserializer=str, serializer=str, if_undefined=None, default=None) #: The URL of the author icon. #: - #: :type: :obj:`str`, optional + #: :type: :obj:`~str`, optional icon_url: typing.Optional[str] = marshaller.attrib( deserializer=str, serializer=str, if_undefined=None, default=None ) @@ -241,7 +241,7 @@ class EmbedAuthor(entities.HikariEntity, entities.Deserializable, entities.Seria #: will be ignored during serialization. #: #: - #: :type: :obj:`str`, optional + #: :type: :obj:`~str`, optional proxy_icon_url: typing.Optional[str] = marshaller.attrib( deserializer=str, transient=True, if_undefined=None, default=None ) @@ -254,17 +254,17 @@ class EmbedField(entities.HikariEntity, entities.Deserializable, entities.Serial #: The name of the field. #: - #: :type: :obj:`str` + #: :type: :obj:`~str` name: str = marshaller.attrib(deserializer=str, serializer=str) #: The value of the field. #: - #: :type: :obj:`str` + #: :type: :obj:`~str` value: str = marshaller.attrib(deserializer=str, serializer=str) - #: Whether the field should display inline. Defaults to :obj:`False`. + #: Whether the field should display inline. Defaults to :obj:`~False`. #: - #: :type: :obj:`bool` + #: :type: :obj:`~bool` is_inline: bool = marshaller.attrib( raw_name="inline", deserializer=bool, serializer=bool, if_undefined=False, default=False ) @@ -277,24 +277,24 @@ class Embed(entities.HikariEntity, entities.Deserializable, entities.Serializabl #: The title of the embed. #: - #: :type: :obj:`str`, optional + #: :type: :obj:`~str`, optional title: typing.Optional[str] = marshaller.attrib(deserializer=str, serializer=str, if_undefined=None, default=None) #: The description of the embed. #: - #: :type: :obj:`str`, optional + #: :type: :obj:`~str`, optional description: typing.Optional[str] = marshaller.attrib( deserializer=str, serializer=str, if_undefined=None, default=None ) #: The URL of the embed. #: - #: :type: :obj:`str`, optional + #: :type: :obj:`~str`, optional url: typing.Optional[str] = marshaller.attrib(deserializer=str, serializer=str, if_undefined=None, default=None) #: The timestamp of the embed. #: - #: :type: :obj:`datetime.datetime`, optional + #: :type: :obj:`~datetime.datetime`, optional timestamp: typing.Optional[datetime.datetime] = marshaller.attrib( deserializer=hikari.internal.conversions.parse_iso_8601_ts, serializer=lambda timestamp: timestamp.replace(tzinfo=datetime.timezone.utc).isoformat(), @@ -308,21 +308,21 @@ class Embed(entities.HikariEntity, entities.Deserializable, entities.Serializabl #: The footer of the embed. #: - #: :type: :obj:`EmbedFooter`, optional + #: :type: :obj:`~EmbedFooter`, optional footer: typing.Optional[EmbedFooter] = marshaller.attrib( deserializer=EmbedFooter.deserialize, serializer=EmbedFooter.serialize, if_undefined=None, default=None ) #: The image of the embed. #: - #: :type: :obj:`EmbedImage`, optional + #: :type: :obj:`~EmbedImage`, optional image: typing.Optional[EmbedImage] = marshaller.attrib( deserializer=EmbedImage.deserialize, serializer=EmbedImage.serialize, if_undefined=None, default=None ) #: The thumbnail of the embed. #: - #: :type: :obj:`EmbedThumbnail`, optional + #: :type: :obj:`~EmbedThumbnail`, optional thumbnail: typing.Optional[EmbedThumbnail] = marshaller.attrib( deserializer=EmbedThumbnail.deserialize, serializer=EmbedThumbnail.serialize, if_undefined=None, default=None ) @@ -335,7 +335,7 @@ class Embed(entities.HikariEntity, entities.Deserializable, entities.Serializabl #: will be ignored during serialization. #: #: - #: :type: :obj:`EmbedVideo`, optional + #: :type: :obj:`~EmbedVideo`, optional video: typing.Optional[EmbedVideo] = marshaller.attrib( deserializer=EmbedVideo.deserialize, transient=True, if_undefined=None, default=None, ) @@ -348,21 +348,21 @@ class Embed(entities.HikariEntity, entities.Deserializable, entities.Serializabl #: will be ignored during serialization. #: #: - #: :type: :obj:`EmbedProvider`, optional + #: :type: :obj:`~EmbedProvider`, optional provider: typing.Optional[EmbedProvider] = marshaller.attrib( deserializer=EmbedProvider.deserialize, transient=True, if_undefined=None, default=None ) #: The author of the embed. #: - #: :type: :obj:`EmbedAuthor`, optional + #: :type: :obj:`~EmbedAuthor`, optional author: typing.Optional[EmbedAuthor] = marshaller.attrib( deserializer=EmbedAuthor.deserialize, serializer=EmbedAuthor.serialize, if_undefined=None, default=None ) #: The fields of the embed. #: - #: :type: :obj:`typing.Sequence` [ :obj:`EmbedField` ], optional + #: :type: :obj:`~typing.Sequence` [ :obj:`~EmbedField` ], optional fields: typing.Optional[typing.Sequence[EmbedField]] = marshaller.attrib( deserializer=lambda fields: [EmbedField.deserialize(f) for f in fields], serializer=lambda fields: [f.serialize() for f in fields], diff --git a/hikari/emojis.py b/hikari/emojis.py index c8eb8f57fd..33b4ac7d56 100644 --- a/hikari/emojis.py +++ b/hikari/emojis.py @@ -42,7 +42,7 @@ class UnicodeEmoji(Emoji): #: The codepoints that form the emoji. #: - #: :type: :obj:`str` + #: :type: :obj:`~str` name: str = marshaller.attrib(deserializer=str) @property @@ -63,12 +63,12 @@ class UnknownEmoji(Emoji, snowflakes.UniqueEntity): #: The name of the emoji. #: - #: :type: :obj:`str`, optional + #: :type: :obj:`~str`, optional name: typing.Optional[str] = marshaller.attrib(deserializer=str, if_none=None) #: Whether the emoji is animated. #: - #: :type: :obj:`bool` + #: :type: :obj:`~bool` is_animated: bool = marshaller.attrib( raw_name="animated", deserializer=bool, if_undefined=False, if_none=None, default=False ) @@ -86,7 +86,7 @@ class GuildEmoji(UnknownEmoji): #: The whitelisted role IDs to use this emoji. #: - #: :type: :obj:`typing.Set` [ :obj:`hikari.snowflakes.Snowflake` ] + #: :type: :obj:`~typing.Set` [ :obj:`~hikari.snowflakes.Snowflake` ] role_ids: typing.Set[snowflakes.Snowflake] = marshaller.attrib( raw_name="roles", deserializer=lambda roles: {snowflakes.Snowflake.deserialize(r) for r in roles}, @@ -98,25 +98,25 @@ class GuildEmoji(UnknownEmoji): #: #: Note #: ---- - #: This will be :obj:`None` if you are missing the ``MANAGE_EMOJIS`` + #: This will be :obj:`~None` if you are missing the ``MANAGE_EMOJIS`` #: permission in the server the emoji is from. #: #: - #: :type: :obj:`hikari.users.User`, optional + #: :type: :obj:`~hikari.users.User`, optional user: typing.Optional[users.User] = marshaller.attrib( deserializer=users.User.deserialize, if_none=None, if_undefined=None, default=None ) #: Whether this emoji must be wrapped in colons. #: - #: :type: :obj:`bool`, optional + #: :type: :obj:`~bool`, optional is_colons_required: typing.Optional[bool] = marshaller.attrib( raw_name="require_colons", deserializer=bool, if_undefined=None, default=None ) #: Whether the emoji is managed by an integration. #: - #: :type: :obj:`bool`, optional + #: :type: :obj:`~bool`, optional is_managed: typing.Optional[bool] = marshaller.attrib( raw_name="managed", deserializer=bool, if_undefined=None, default=None ) diff --git a/hikari/errors.py b/hikari/errors.py index 512496ccd2..ee93cba096 100644 --- a/hikari/errors.py +++ b/hikari/errors.py @@ -60,13 +60,13 @@ class GatewayError(HikariError): Parameters ---------- - reason : :obj:`str` + reason : :obj:`~str` A string explaining the issue. """ #: A string to explain the issue. #: - #: :type: :obj:`str` + #: :type: :obj:`~str` reason: str def __init__(self, reason: str) -> None: @@ -82,7 +82,7 @@ class GatewayClientClosedError(GatewayError): Parameters ---------- - reason : :obj:`str` + reason : :obj:`~str` A string explaining the issue. """ @@ -95,9 +95,9 @@ class GatewayServerClosedConnectionError(GatewayError): Parameters ---------- - close_code : :obj:`hikari.net.codes.GatewayCloseCode`, :obj:`int`, optional + close_code : :obj:`~hikari.net.codes.GatewayCloseCode`, :obj:`~int`, optional The close code provided by the server, if there was one. - reason : :obj:`str`, optional + reason : :obj:`~str`, optional A string explaining the issue. """ @@ -135,16 +135,16 @@ class GatewayInvalidSessionError(GatewayServerClosedConnectionError): Parameters ---------- - can_resume : :obj:`bool` - :obj:`True` if the connection will be able to RESUME next time it starts - rather than re-IDENTIFYing, or :obj:`False` if you need to IDENTIFY + can_resume : :obj:`~bool` + :obj:`~True` if the connection will be able to RESUME next time it starts + rather than re-IDENTIFYing, or :obj:`~False` if you need to IDENTIFY again instead. """ - #: :obj:`True` if the next reconnection can be RESUMED. :obj:`False` if it + #: :obj:`~True` if the next reconnection can be RESUMED. :obj:`~False` if it #: has to be coordinated by re-IDENFITYing. #: - #: :type: :obj:`bool` + #: :type: :obj:`~bool` can_resume: bool def __init__(self, can_resume: bool) -> None: @@ -192,13 +192,13 @@ class HTTPError(HikariError): Parameters ---------- - reason : :obj:`str` + reason : :obj:`~str` A meaningful explanation of the problem. """ #: A meaningful explanation of the problem. #: - #: :type: :obj:`str` + #: :type: :obj:`~str` reason: str def __init__(self, reason: str) -> None: @@ -214,36 +214,36 @@ class CodedHTTPError(HTTPError): Parameters ---------- - status : :obj:`int` or :obj:`hikari.net.codes.HTTPStatusCode` + status : :obj:`~int` or :obj:`~hikari.net.codes.HTTPStatusCode` The HTTP status code that was returned by the server. - route : :obj:`hikari.net.routes.CompiledRoute` + route : :obj:`~hikari.net.routes.CompiledRoute` The HTTP route that was being invoked when this exception occurred. - message : :obj:`str`, optional + message : :obj:`~str`, optional An optional message if provided in the response payload. - json_code : :obj:`hikari.net.codes.JSONErrorCode`, :obj:`int`, optional + json_code : :obj:`~hikari.net.codes.JSONErrorCode`, :obj:`~int`, optional An optional error code the server provided us. """ #: The HTTP status code that was returned by the server. #: - #: :type: :obj:`int` or :obj:`hikari.net.codes.HTTPStatusCode` + #: :type: :obj:`~int` or :obj:`~hikari.net.codes.HTTPStatusCode` status: typing.Union[int, codes.HTTPStatusCode] #: The HTTP route that was being invoked when this exception occurred. #: - #: :type: :obj:`hikari.net.routes.CompiledRoute` + #: :type: :obj:`~hikari.net.routes.CompiledRoute` route: routes.CompiledRoute #: An optional contextual message the server provided us with in the #: response body. # - #: :type: :obj:`str`, optional + #: :type: :obj:`~str`, optional message: typing.Optional[str] #: An optional contextual error code the server provided us with in the #: response body. # - #: :type: :obj:`hikari.net.codes.JSONErrorCode` or :obj:`int`, optional + #: :type: :obj:`~hikari.net.codes.JSONErrorCode` or :obj:`~int`, optional json_code: typing.Optional[typing.Union[codes.JSONErrorCode, int]] def __init__( @@ -283,7 +283,7 @@ class ClientHTTPError(CodedHTTPError): class BadRequestHTTPError(CodedHTTPError): - """A specific case of :obj:`CodedHTTPError`. + """A specific case of :obj:`~CodedHTTPError`. This can occur hat occurs when you send Discord information in an unexpected format, miss required information out, or give bad values for stuff. @@ -293,11 +293,11 @@ class BadRequestHTTPError(CodedHTTPError): Parameters ---------- - route : :obj:`hikari.net.routes.CompiledRoute` + route : :obj:`~hikari.net.routes.CompiledRoute` The HTTP route that was being invoked when this exception occurred. - message : :obj:`str`, optional + message : :obj:`~str`, optional An optional message if provided in the response payload. - json_code : :obj:`hikari.net.codes.JSONErrorCode`, :obj:`int`, optional + json_code : :obj:`~hikari.net.codes.JSONErrorCode`, :obj:`~int`, optional An optional error code the server provided us. """ @@ -311,7 +311,7 @@ def __init__( class UnauthorizedHTTPError(ClientHTTPError): - """A specific case of :obj:`ClientHTTPError`. + """A specific case of :obj:`~ClientHTTPError`. This occurs when you have invalid authorization details to access the given resource. @@ -320,11 +320,11 @@ class UnauthorizedHTTPError(ClientHTTPError): Parameters ---------- - route : :obj:`hikari.net.routes.CompiledRoute` + route : :obj:`~hikari.net.routes.CompiledRoute` The HTTP route that was being invoked when this exception occurred. - message : :obj:`str`, optional + message : :obj:`~str`, optional An optional message if provided in the response payload. - json_code : :obj:`hikari.net.codes.JSONErrorCode`, :obj:`int`, optional + json_code : :obj:`~hikari.net.codes.JSONErrorCode`, :obj:`~int`, optional An optional error code the server provided us. """ @@ -338,7 +338,7 @@ def __init__( class ForbiddenHTTPError(ClientHTTPError): - """A specific case of :obj:`ClientHTTPError`. + """A specific case of :obj:`~ClientHTTPError`. This occurs when you are missing permissions, or are using an endpoint that your account is not allowed to see without being whitelisted. @@ -347,11 +347,11 @@ class ForbiddenHTTPError(ClientHTTPError): Parameters ---------- - route : :obj:`hikari.net.routes.CompiledRoute` + route : :obj:`~hikari.net.routes.CompiledRoute` The HTTP route that was being invoked when this exception occurred. - message : :obj:`str`, optional + message : :obj:`~str`, optional An optional message if provided in the response payload. - json_code : :obj:`hikari.net.codes.JSONErrorCode`, :obj:`int`, optional + json_code : :obj:`~hikari.net.codes.JSONErrorCode`, :obj:`~int`, optional An optional error code the server provided us. """ @@ -365,7 +365,7 @@ def __init__( class NotFoundHTTPError(ClientHTTPError): - """A specific case of :obj:`ClientHTTPError`. + """A specific case of :obj:`~ClientHTTPError`. This occurs when you try to refer to something that doesn't exist on Discord. This might be referring to a user ID, channel ID, guild ID, etc that does @@ -374,11 +374,11 @@ class NotFoundHTTPError(ClientHTTPError): Parameters ---------- - route : :obj:`hikari.net.routes.CompiledRoute` + route : :obj:`~hikari.net.routes.CompiledRoute` The HTTP route that was being invoked when this exception occurred. - message : :obj:`str`, optional + message : :obj:`~str`, optional An optional message if provided in the response payload. - json_code : :obj:`hikari.net.codes.JSONErrorCode`, :obj:`int`, optional + json_code : :obj:`~hikari.net.codes.JSONErrorCode`, :obj:`~int`, optional An optional error code the server provided us. """ diff --git a/hikari/events.py b/hikari/events.py index db748d90cb..b16e65100a 100644 --- a/hikari/events.py +++ b/hikari/events.py @@ -102,17 +102,17 @@ class ExceptionEvent(HikariEvent): #: The exception that was raised. #: - #: :type: :obj:`Exception` + #: :type: :obj:`~Exception` exception: Exception #: The event that was being invoked when the exception occurred. #: - #: :type: :obj:`HikariEvent` + #: :type: :obj:`~HikariEvent` event: HikariEvent #: The event that was being invoked when the exception occurred. #: - #: :type: coroutine function ( :obj:`HikariEvent` ) -> :obj:`None` + #: :type: coroutine function ( :obj:`~HikariEvent` ) -> :obj:`~None` callback: typing.Callable[[HikariEvent], typing.Awaitable[None]] @@ -146,7 +146,7 @@ class ConnectedEvent(HikariEvent, entities.Deserializable): #: The shard that connected. #: - #: :type: :obj:`hikari.clients.shard_clients.ShardClient` + #: :type: :obj:`~hikari.clients.shard_clients.ShardClient` shard: shard_clients.ShardClient @@ -156,7 +156,7 @@ class DisconnectedEvent(HikariEvent, entities.Deserializable): #: The shard that disconnected. #: - #: :type: :obj:`hikari.clients.shard_clients.ShardClient` + #: :type: :obj:`~hikari.clients.shard_clients.ShardClient` shard: shard_clients.ShardClient @@ -166,7 +166,7 @@ class ResumedEvent(HikariEvent): #: The shard that reconnected. #: - #: :type: :obj:`hikari.clients.shard_clients.ShardClient` + #: :type: :obj:`~hikari.clients.shard_clients.ShardClient` shard: shard_clients.ShardClient @@ -180,18 +180,18 @@ class ReadyEvent(HikariEvent, entities.Deserializable): #: The gateway version this is currently connected to. #: - #: :type: :obj:`int` + #: :type: :obj:`~int` gateway_version: int = marshaller.attrib(raw_name="v", deserializer=int) #: The object of the current bot account this connection is for. #: - #: :type: :obj:`hikari.users.MyUser` + #: :type: :obj:`~hikari.users.MyUser` my_user: users.MyUser = marshaller.attrib(raw_name="user", deserializer=users.MyUser.deserialize) #: A mapping of the guilds this bot is currently in. All guilds will start #: off "unavailable". #: - #: :type: :obj:`typing.Mapping` [ :obj:`hikari.snowflakes.Snowflake`, :obj:`hikari.guilds.UnavailableGuild` ] + #: :type: :obj:`~typing.Mapping` [ :obj:`~hikari.snowflakes.Snowflake`, :obj:`~hikari.guilds.UnavailableGuild` ] unavailable_guilds: typing.Mapping[snowflakes.Snowflake, guilds.UnavailableGuild] = marshaller.attrib( raw_name="guilds", deserializer=lambda guilds_objs: {g.id: g for g in map(guilds.UnavailableGuild.deserialize, guilds_objs)}, @@ -199,12 +199,12 @@ class ReadyEvent(HikariEvent, entities.Deserializable): #: The id of the current gateway session, used for reconnecting. #: - #: :type: :obj:`str` + #: :type: :obj:`~str` session_id: str = marshaller.attrib(deserializer=str) #: Information about the current shard, only provided when IDENTIFYing. #: - #: :type: :obj:`typing.Tuple` [ :obj:`int`, :obj:`int` ], optional + #: :type: :obj:`~typing.Tuple` [ :obj:`~int`, :obj:`~int` ], optional _shard_information: typing.Optional[typing.Tuple[int, int]] = marshaller.attrib( raw_name="shard", deserializer=tuple, if_undefined=None, default=None ) @@ -233,12 +233,12 @@ class BaseChannelEvent(HikariEvent, snowflakes.UniqueEntity, entities.Deserializ #: The channel's type. #: - #: :type: :obj:`hikari.channels.ChannelType` + #: :type: :obj:`~hikari.channels.ChannelType` type: channels.ChannelType = marshaller.attrib(deserializer=channels.ChannelType) - #: The ID of the guild this channel is in, will be :obj:`None` for DMs. + #: The ID of the guild this channel is in, will be :obj:`~None` for DMs. #: - #: :type: :obj:`hikari.snowflakes.Snowflake`, optional + #: :type: :obj:`~hikari.snowflakes.Snowflake`, optional guild_id: typing.Optional[snowflakes.Snowflake] = marshaller.attrib( deserializer=snowflakes.Snowflake.deserialize, if_none=None ) @@ -246,12 +246,12 @@ class BaseChannelEvent(HikariEvent, snowflakes.UniqueEntity, entities.Deserializ #: The sorting position of this channel, will be relative to the #: :attr:`parent_id` if set. #: - #: :type: :obj:`int`, optional + #: :type: :obj:`~int`, optional position: typing.Optional[int] = marshaller.attrib(deserializer=int, if_none=None) #: An mapping of the set permission overwrites for this channel, if applicable. #: - #: :type: :obj:`typing.Mapping` [ :obj:`hikari.snowflakes.Snowflake`, :obj:`hikari.channels.PermissionOverwrite` ], optional + #: :type: :obj:`~typing.Mapping` [ :obj:`~hikari.snowflakes.Snowflake`, :obj:`~hikari.channels.PermissionOverwrite` ], optional permission_overwrites: typing.Optional[ typing.Mapping[snowflakes.Snowflake, channels.PermissionOverwrite] ] = marshaller.attrib( @@ -261,49 +261,49 @@ class BaseChannelEvent(HikariEvent, snowflakes.UniqueEntity, entities.Deserializ #: The name of this channel, if applicable. #: - #: :type: :obj:`str`, optional + #: :type: :obj:`~str`, optional name: typing.Optional[str] = marshaller.attrib(deserializer=str, if_undefined=None, default=None) #: The topic of this channel, if applicable and set. #: - #: :type: :obj:`str`, optional + #: :type: :obj:`~str`, optional topic: typing.Optional[str] = marshaller.attrib(deserializer=str, if_undefined=None, if_none=None, default=None) - #: Whether this channel is nsfw, will be :obj:`None` if not applicable. + #: Whether this channel is nsfw, will be :obj:`~None` if not applicable. #: - #: :type: :obj:`bool`, optional + #: :type: :obj:`~bool`, optional is_nsfw: typing.Optional[bool] = marshaller.attrib( raw_name="nsfw", deserializer=bool, if_undefined=None, default=None ) #: The ID of the last message sent, if it's a text type channel. #: - #: :type: :obj:`hikari.snowflakes.Snowflake`, optional + #: :type: :obj:`~hikari.snowflakes.Snowflake`, optional last_message_id: typing.Optional[snowflakes.Snowflake] = marshaller.attrib( deserializer=snowflakes.Snowflake.deserialize, if_none=None, if_undefined=None, default=None ) #: The bitrate (in bits) of this channel, if it's a guild voice channel. #: - #: :type: :obj:`bool`, optional + #: :type: :obj:`~bool`, optional bitrate: typing.Optional[int] = marshaller.attrib(deserializer=int, if_undefined=None, default=None) #: The user limit for this channel if it's a guild voice channel. #: - #: :type: :obj:`bool`, optional + #: :type: :obj:`~bool`, optional user_limit: typing.Optional[int] = marshaller.attrib(deserializer=int, if_undefined=None, default=None) #: The rate limit a user has to wait before sending another message in this #: channel, if it's a guild text like channel. #: - #: :type: :obj:`datetime.timedelta`, optional + #: :type: :obj:`~datetime.timedelta`, optional rate_limit_per_user: typing.Optional[datetime.timedelta] = marshaller.attrib( deserializer=lambda delta: datetime.timedelta(seconds=delta), if_undefined=None, default=None ) #: A mapping of this channel's recipient users, if it's a DM or group DM. #: - #: :type: :obj:`typing.Mapping` [ :obj:`hikari.snowflakes.Snowflake`, :obj:`hikari.users.User` ], optional + #: :type: :obj:`~typing.Mapping` [ :obj:`~hikari.snowflakes.Snowflake`, :obj:`~hikari.users.User` ], optional recipients: typing.Optional[typing.Mapping[snowflakes.Snowflake, users.User]] = marshaller.attrib( deserializer=lambda recipients: {user.id: user for user in map(users.User.deserialize, recipients)}, if_undefined=None, @@ -312,14 +312,14 @@ class BaseChannelEvent(HikariEvent, snowflakes.UniqueEntity, entities.Deserializ #: The hash of this channel's icon, if it's a group DM channel and is set. #: - #: :type: :obj:`str`, optional + #: :type: :obj:`~str`, optional icon_hash: typing.Optional[str] = marshaller.attrib( raw_name="icon", deserializer=str, if_undefined=None, if_none=None, default=None ) #: The ID of this channel's creator, if it's a DM channel. #: - #: :type: :obj:`hikari.snowflakes.Snowflake`, optional + #: :type: :obj:`~hikari.snowflakes.Snowflake`, optional owner_id: typing.Optional[snowflakes.Snowflake] = marshaller.attrib( deserializer=snowflakes.Snowflake.deserialize, if_undefined=None, default=None ) @@ -327,14 +327,14 @@ class BaseChannelEvent(HikariEvent, snowflakes.UniqueEntity, entities.Deserializ #: The ID of the application that created the group DM, if it's a #: bot based group DM. #: - #: :type: :obj:`hikari.snowflakes.Snowflake`, optional + #: :type: :obj:`~hikari.snowflakes.Snowflake`, optional application_id: typing.Optional[snowflakes.Snowflake] = marshaller.attrib( deserializer=snowflakes.Snowflake.deserialize, if_undefined=None, default=None ) #: The ID of this channels's parent category within guild, if set. #: - #: :type: :obj:`hikari.snowflakes.Snowflake`, optional + #: :type: :obj:`~hikari.snowflakes.Snowflake`, optional parent_id: typing.Optional[snowflakes.Snowflake] = marshaller.attrib( deserializer=snowflakes.Snowflake.deserialize, if_undefined=None, if_none=None, default=None ) @@ -342,7 +342,7 @@ class BaseChannelEvent(HikariEvent, snowflakes.UniqueEntity, entities.Deserializ #: The datetime of when the last message was pinned in this channel, #: if set and applicable. #: - #: :type: :obj:`datetime.datetime`, optional + #: :type: :obj:`~datetime.datetime`, optional last_pin_timestamp: typing.Optional[datetime.datetime] = marshaller.attrib( deserializer=conversions.parse_iso_8601_ts, if_undefined=None, default=None ) @@ -380,22 +380,22 @@ class ChannelPinUpdateEvent(HikariEvent, entities.Deserializable): """ #: The ID of the guild where this event happened. - #: Will be :obj:`None` if this happened in a DM channel. + #: Will be :obj:`~None` if this happened in a DM channel. #: - #: :type: :obj:`hikari.snowflakes.Snowflake`, optional + #: :type: :obj:`~hikari.snowflakes.Snowflake`, optional guild_id: typing.Optional[snowflakes.Snowflake] = marshaller.attrib( deserializer=snowflakes.Snowflake.deserialize, if_undefined=None, default=None ) #: The ID of the channel where the message was pinned or unpinned. #: - #: :type: :obj:`hikari.snowflakes.Snowflake` + #: :type: :obj:`~hikari.snowflakes.Snowflake` channel_id: snowflakes.Snowflake = marshaller.attrib(deserializer=snowflakes.Snowflake.deserialize) #: The datetime of when the most recent message was pinned in this channel. - #: Will be :obj:`None` if there are no messages pinned after this change. + #: Will be :obj:`~None` if there are no messages pinned after this change. #: - #: :type: :obj:`datetime.datetime`, optional + #: :type: :obj:`~datetime.datetime`, optional last_pin_timestamp: typing.Optional[datetime.datetime] = marshaller.attrib( deserializer=conversions.parse_iso_8601_ts, if_undefined=None, default=None ) @@ -446,12 +446,12 @@ class BaseGuildBanEvent(HikariEvent, entities.Deserializable): #: The ID of the guild this ban is in. #: - #: :type: :obj:`hikari.snowflakes.Snowflake` + #: :type: :obj:`~hikari.snowflakes.Snowflake` guild_id: snowflakes.Snowflake = marshaller.attrib(deserializer=snowflakes.Snowflake.deserialize) #: The object of the user this ban targets. #: - #: :type: :obj:`hikari.users.User` + #: :type: :obj:`~hikari.users.User` user: users.User = marshaller.attrib(deserializer=users.User.deserialize) @@ -474,12 +474,12 @@ class GuildEmojisUpdateEvent(HikariEvent, entities.Deserializable): #: The ID of the guild this emoji was updated in. #: - #: :type: :obj:`hikari.snowflakes.Snowflake` + #: :type: :obj:`~hikari.snowflakes.Snowflake` guild_id: snowflakes.Snowflake = marshaller.attrib(deserializer=snowflakes.Snowflake.deserialize) #: The updated mapping of emojis by their ID. #: - #: :type: :obj:`typing.Mapping` [ :obj:`hikari.snowflakes.Snowflake`, :obj:`hikari.emojis.GuildEmoji` ] + #: :type: :obj:`~typing.Mapping` [ :obj:`~hikari.snowflakes.Snowflake`, :obj:`~hikari.emojis.GuildEmoji` ] emojis: typing.Mapping[snowflakes.Snowflake, _emojis.GuildEmoji] = marshaller.attrib( deserializer=lambda ems: {emoji.id: emoji for emoji in map(_emojis.GuildEmoji.deserialize, ems)} ) @@ -492,7 +492,7 @@ class GuildIntegrationsUpdateEvent(HikariEvent, entities.Deserializable): #: The ID of the guild the integration was updated in. #: - #: :type: :obj:`hikari.snowflakes.Snowflake` + #: :type: :obj:`~hikari.snowflakes.Snowflake` guild_id: snowflakes.Snowflake = marshaller.attrib(deserializer=snowflakes.Snowflake.deserialize) @@ -503,7 +503,7 @@ class GuildMemberAddEvent(HikariEvent, guilds.GuildMember): #: The ID of the guild where this member was added. #: - #: :type: :obj:`hikari.snowflakes.Snowflake` + #: :type: :obj:`~hikari.snowflakes.Snowflake` guild_id: snowflakes.Snowflake = marshaller.attrib(deserializer=snowflakes.Snowflake.deserialize) @@ -517,33 +517,33 @@ class GuildMemberUpdateEvent(HikariEvent, entities.Deserializable): #: The ID of the guild this member was updated in. #: - #: :type: :obj:`hikari.snowflakes.Snowflake` + #: :type: :obj:`~hikari.snowflakes.Snowflake` guild_id: snowflakes.Snowflake = marshaller.attrib(deserializer=snowflakes.Snowflake.deserialize) #: A sequence of the IDs of the member's current roles. #: - #: :type: :obj:`typing.Sequence` [ :obj:`hikari.snowflakes.Snowflake` ] + #: :type: :obj:`~typing.Sequence` [ :obj:`~hikari.snowflakes.Snowflake` ] role_ids: typing.Sequence[snowflakes.Snowflake] = marshaller.attrib( raw_name="roles", deserializer=lambda role_ids: [snowflakes.Snowflake.deserialize(rid) for rid in role_ids], ) #: The object of the user who was updated. #: - #: :type: :obj:`hikari.users.User` + #: :type: :obj:`~hikari.users.User` user: users.User = marshaller.attrib(deserializer=users.User.deserialize) - #: This member's nickname. When set to :obj:`None`, this has been removed - #: and when set to :obj:`hikari.entities.UNSET` this hasn't been acted on. + #: This member's nickname. When set to :obj:`~None`, this has been removed + #: and when set to :obj:`~hikari.entities.UNSET` this hasn't been acted on. #: - #: :type: :obj:`typing.Union` [ :obj:`str`, :obj:`hikari.entities.UNSET` ], optional + #: :type: :obj:`~typing.Union` [ :obj:`~str`, :obj:`~hikari.entities.UNSET` ], optional nickname: typing.Union[None, str, entities.Unset] = marshaller.attrib( raw_name="nick", deserializer=str, if_none=None, if_undefined=entities.Unset, default=entities.UNSET ) #: The datetime of when this member started "boosting" this guild. - #: Will be :obj:`None` if they aren't boosting. + #: Will be :obj:`~None` if they aren't boosting. #: - #: :type: :obj:`typing.Union` [ :obj:`datetime.datetime`, :obj:`hikari.entities.UNSET` ], optional + #: :type: :obj:`~typing.Union` [ :obj:`~datetime.datetime`, :obj:`~hikari.entities.UNSET` ], optional premium_since: typing.Union[None, datetime.datetime, entities.Unset] = marshaller.attrib( deserializer=conversions.parse_iso_8601_ts, if_none=None, if_undefined=entities.Unset, default=entities.UNSET ) @@ -559,12 +559,12 @@ class GuildMemberRemoveEvent(HikariEvent, entities.Deserializable): #: The ID of the guild this user was removed from. #: - #: :type: :obj:`hikari.snowflakes.Snowflake` + #: :type: :obj:`~hikari.snowflakes.Snowflake` guild_id: snowflakes.Snowflake = marshaller.attrib(deserializer=snowflakes.Snowflake.deserialize) #: The object of the user who was removed from this guild. #: - #: :type: :obj:`hikari.users.User` + #: :type: :obj:`~hikari.users.User` user: users.User = marshaller.attrib(deserializer=users.User.deserialize) @@ -575,12 +575,12 @@ class GuildRoleCreateEvent(HikariEvent, entities.Deserializable): #: The ID of the guild where this role was created. #: - #: :type: :obj:`hikari.snowflakes.Snowflake` + #: :type: :obj:`~hikari.snowflakes.Snowflake` guild_id: snowflakes.Snowflake = marshaller.attrib(deserializer=snowflakes.Snowflake.deserialize) #: The object of the role that was created. #: - #: :type: :obj:`hikari.guilds.GuildRole` + #: :type: :obj:`~hikari.guilds.GuildRole` role: guilds.GuildRole = marshaller.attrib(deserializer=guilds.GuildRole.deserialize) @@ -591,12 +591,12 @@ class GuildRoleUpdateEvent(HikariEvent, entities.Deserializable): #: The ID of the guild where this role was updated. #: - #: :type: :obj:`hikari.snowflakes.Snowflake` + #: :type: :obj:`~hikari.snowflakes.Snowflake` guild_id: snowflakes.Snowflake = marshaller.attrib(deserializer=snowflakes.Snowflake.deserialize) #: The updated role object. #: - #: :type: :obj:`hikari.guilds.GuildRole` + #: :type: :obj:`~hikari.guilds.GuildRole` role: guilds.GuildRole = marshaller.attrib(deserializer=guilds.GuildRole.deserialize) @@ -607,12 +607,12 @@ class GuildRoleDeleteEvent(HikariEvent, entities.Deserializable): #: The ID of the guild where this role is being deleted. #: - #: :type: :obj:`hikari.snowflakes.Snowflake` + #: :type: :obj:`~hikari.snowflakes.Snowflake` guild_id: snowflakes.Snowflake = marshaller.attrib(deserializer=snowflakes.Snowflake.deserialize) #: The ID of the role being deleted. #: - #: :type: :obj:`hikari.snowflakes.Snowflake` + #: :type: :obj:`~hikari.snowflakes.Snowflake` role_id: snowflakes.Snowflake = marshaller.attrib(deserializer=snowflakes.Snowflake.deserialize) @@ -623,38 +623,38 @@ class InviteCreateEvent(HikariEvent, entities.Deserializable): #: The ID of the channel this invite targets. #: - #: :type: :obj:`hikari.snowflakes.Snowflake` + #: :type: :obj:`~hikari.snowflakes.Snowflake` channel_id: snowflakes.Snowflake = marshaller.attrib(deserializer=snowflakes.Snowflake.deserialize) #: The code that identifies this invite #: - #: :type: :obj:`str` + #: :type: :obj:`~str` code: str = marshaller.attrib(deserializer=str) #: The datetime of when this invite was created. #: - #: :type: :obj:`datetime.datetime` + #: :type: :obj:`~datetime.datetime` created_at: datetime.datetime = marshaller.attrib(deserializer=conversions.parse_iso_8601_ts) #: The ID of the guild this invite was created in, if applicable. - #: Will be :obj:`None` for group DM invites. + #: Will be :obj:`~None` for group DM invites. #: - #: :type: :obj:`hikari.snowflakes.Snowflake`, optional + #: :type: :obj:`~hikari.snowflakes.Snowflake`, optional guild_id: typing.Optional[snowflakes.Snowflake] = marshaller.attrib( deserializer=snowflakes.Snowflake.deserialize, if_undefined=None, default=None ) #: The object of the user who created this invite, if applicable. #: - #: :type: :obj:`hikari.users.User`, optional + #: :type: :obj:`~hikari.users.User`, optional inviter: typing.Optional[users.User] = marshaller.attrib( deserializer=users.User.deserialize, if_undefined=None, default=None ) #: The timedelta of how long this invite will be valid for. - #: If set to :obj:`None` then this is unlimited. + #: If set to :obj:`~None` then this is unlimited. #: - #: :type: :obj:`datetime.timedelta`, optional + #: :type: :obj:`~datetime.timedelta`, optional max_age: typing.Optional[datetime.timedelta] = marshaller.attrib( deserializer=lambda age: datetime.timedelta(seconds=age) if age > 0 else None, ) @@ -662,31 +662,31 @@ class InviteCreateEvent(HikariEvent, entities.Deserializable): #: The limit for how many times this invite can be used before it expires. #: If set to ``0``, or infinity (``float("inf")``) then this is unlimited. #: - #: :type: :obj:`typing.Union` [ :obj:`int`, :obj:`float` ( ``"inf"`` ) ] + #: :type: :obj:`~typing.Union` [ :obj:`~int`, :obj:`~float` ( ``"inf"`` ) ] max_uses: typing.Union[int, float] = marshaller.attrib(deserializer=lambda count: count or float("inf")) #: The object of the user who this invite targets, if set. #: - #: :type: :obj:`hikari.users.User`, optional + #: :type: :obj:`~hikari.users.User`, optional target_user: typing.Optional[users.User] = marshaller.attrib( deserializer=users.User.deserialize, if_undefined=None, default=None ) #: The type of user target this invite is, if applicable. #: - #: :type: :obj:`hikari.invites.TargetUserType`, optional + #: :type: :obj:`~hikari.invites.TargetUserType`, optional target_user_type: typing.Optional[invites.TargetUserType] = marshaller.attrib( deserializer=invites.TargetUserType, if_undefined=None, default=None ) #: Whether this invite grants temporary membership. #: - #: :type: :obj:`bool` + #: :type: :obj:`~bool` is_temporary: bool = marshaller.attrib(raw_name="temporary", deserializer=bool) #: The amount of times this invite has been used. #: - #: :type: :obj:`int` + #: :type: :obj:`~int` uses: int = marshaller.attrib(deserializer=int) @@ -700,18 +700,18 @@ class InviteDeleteEvent(HikariEvent, entities.Deserializable): #: The ID of the channel this ID was attached to #: - #: :type: :obj:`hikari.snowflakes.Snowflake` + #: :type: :obj:`~hikari.snowflakes.Snowflake` channel_id: snowflakes.Snowflake = marshaller.attrib(deserializer=snowflakes.Snowflake.deserialize) #: The code of this invite. #: - #: :type: :obj:`str` + #: :type: :obj:`~str` code: str = marshaller.attrib(deserializer=str) #: The ID of the guild this invite was deleted in. - #: This will be :obj:`None` if this invite belonged to a DM channel. + #: This will be :obj:`~None` if this invite belonged to a DM channel. #: - #: :type: :obj:`hikari.snowflakes.Snowflake`, optional + #: :type: :obj:`~hikari.snowflakes.Snowflake`, optional guild_id: typing.Optional[snowflakes.Snowflake] = marshaller.attrib( deserializer=snowflakes.Snowflake.deserialize, if_undefined=None, default=None ) @@ -731,77 +731,77 @@ class MessageUpdateEvent(HikariEvent, snowflakes.UniqueEntity, entities.Deserial Note ---- - All fields on this model except :attr:`channel_id` and :obj:``HikariEvent.id`` may be - set to :obj:`hikari.entities.UNSET` (a singleton defined in + All fields on this model except :attr:`channel_id` and :obj:`~`HikariEvent.id`` may be + set to :obj:`~hikari.entities.UNSET` (a singleton defined in ``hikari.entities``) if we have not received information about their state from Discord alongside field nullability. """ #: The ID of the channel that the message was sent in. #: - #: :type: :obj:`hikari.snowflakes.Snowflake` + #: :type: :obj:`~hikari.snowflakes.Snowflake` channel_id: snowflakes.Snowflake = marshaller.attrib(deserializer=snowflakes.Snowflake.deserialize) #: The ID of the guild that the message was sent in. #: - #: :type: :obj:`typing.Union` [ :obj:`hikari.snowflakes.Snowflake`, :obj:`hikari.entities.UNSET` ] + #: :type: :obj:`~typing.Union` [ :obj:`~hikari.snowflakes.Snowflake`, :obj:`~hikari.entities.UNSET` ] guild_id: typing.Union[snowflakes.Snowflake, entities.Unset] = marshaller.attrib( deserializer=snowflakes.Snowflake.deserialize, if_undefined=entities.Unset, default=entities.UNSET ) #: The author of this message. #: - #: :type: :obj:`typing.Union` [ :obj:`hikari.users.User`, :obj:`hikari.entities.UNSET` ] + #: :type: :obj:`~typing.Union` [ :obj:`~hikari.users.User`, :obj:`~hikari.entities.UNSET` ] author: typing.Union[users.User, entities.Unset] = marshaller.attrib( deserializer=users.User.deserialize, if_undefined=entities.Unset, default=entities.UNSET ) #: The member properties for the message's author. #: - #: :type: :obj:`typing.Union` [ :obj:`hikari.guilds.GuildMember`, :obj:`hikari.entities.UNSET` ] + #: :type: :obj:`~typing.Union` [ :obj:`~hikari.guilds.GuildMember`, :obj:`~hikari.entities.UNSET` ] member: typing.Union[guilds.GuildMember, entities.Unset] = marshaller.attrib( deserializer=guilds.GuildMember.deserialize, if_undefined=entities.Unset, default=entities.UNSET ) #: The content of the message. #: - #: :type: :obj:`typing.Union` [ :obj:`str`, :obj:`hikari.entities.UNSET` ] + #: :type: :obj:`~typing.Union` [ :obj:`~str`, :obj:`~hikari.entities.UNSET` ] content: typing.Union[str, entities.Unset] = marshaller.attrib( deserializer=str, if_undefined=entities.Unset, default=entities.UNSET ) #: The timestamp that the message was sent at. #: - #: :type: :obj:`typing.Union` [ :obj:`datetime.datetime`, :obj:`hikari.entities.UNSET` ] + #: :type: :obj:`~typing.Union` [ :obj:`~datetime.datetime`, :obj:`~hikari.entities.UNSET` ] timestamp: typing.Union[datetime.datetime, entities.Unset] = marshaller.attrib( deserializer=conversions.parse_iso_8601_ts, if_undefined=entities.Unset, default=entities.UNSET ) - #: The timestamp that the message was last edited at, or :obj:`None` if + #: The timestamp that the message was last edited at, or :obj:`~None` if #: it wasn't ever edited. #: - #: :type: :obj:`typing.Union` [ :obj:`datetime.datetime`, :obj:`hikari.entities.UNSET` ], optional + #: :type: :obj:`~typing.Union` [ :obj:`~datetime.datetime`, :obj:`~hikari.entities.UNSET` ], optional edited_timestamp: typing.Union[datetime.datetime, entities.Unset, None] = marshaller.attrib( deserializer=conversions.parse_iso_8601_ts, if_none=None, if_undefined=entities.Unset, default=entities.UNSET ) #: Whether the message is a TTS message. #: - #: :type: :obj:`typing.Union` [ :obj:`bool`, :obj:`hikari.entities.UNSET` ] + #: :type: :obj:`~typing.Union` [ :obj:`~bool`, :obj:`~hikari.entities.UNSET` ] is_tts: typing.Union[bool, entities.Unset] = marshaller.attrib( raw_name="tts", deserializer=bool, if_undefined=entities.Unset, default=entities.UNSET ) #: Whether the message mentions ``@everyone`` or ``@here``. #: - #: :type: :obj:`typing.Union` [ :obj:`bool`, :obj:`hikari.entities.UNSET` ] + #: :type: :obj:`~typing.Union` [ :obj:`~bool`, :obj:`~hikari.entities.UNSET` ] is_mentioning_everyone: typing.Union[bool, entities.Unset] = marshaller.attrib( raw_name="mention_everyone", deserializer=bool, if_undefined=entities.Unset, default=entities.UNSET ) #: The users the message mentions. #: - #: :type: :obj:`typing.Union` [ :obj:`typing.Set` [ :obj:`hikari.snowflakes.Snowflake` ], :obj:`hikari.entities.UNSET` ] + #: :type: :obj:`~typing.Union` [ :obj:`~typing.Set` [ :obj:`~hikari.snowflakes.Snowflake` ], :obj:`~hikari.entities.UNSET` ] user_mentions: typing.Union[typing.Set[snowflakes.Snowflake], entities.Unset] = marshaller.attrib( raw_name="mentions", deserializer=lambda user_mentions: {snowflakes.Snowflake.deserialize(u["id"]) for u in user_mentions}, @@ -811,7 +811,7 @@ class MessageUpdateEvent(HikariEvent, snowflakes.UniqueEntity, entities.Deserial #: The roles the message mentions. #: - #: :type: :obj:`typing.Union` [ :obj:`typing.Set` [ :obj:`hikari.snowflakes.Snowflake` ], :obj:`hikari.entities.UNSET` ] + #: :type: :obj:`~typing.Union` [ :obj:`~typing.Set` [ :obj:`~hikari.snowflakes.Snowflake` ], :obj:`~hikari.entities.UNSET` ] role_mentions: typing.Union[typing.Set[snowflakes.Snowflake], entities.Unset] = marshaller.attrib( raw_name="mention_roles", deserializer=lambda role_mentions: {snowflakes.Snowflake.deserialize(r) for r in role_mentions}, @@ -821,7 +821,7 @@ class MessageUpdateEvent(HikariEvent, snowflakes.UniqueEntity, entities.Deserial #: The channels the message mentions. #: - #: :type: :obj:`typing.Union` [ :obj:`typing.Set` [ :obj:`hikari.snowflakes.Snowflake` ], :obj:`hikari.entities.UNSET` ] + #: :type: :obj:`~typing.Union` [ :obj:`~typing.Set` [ :obj:`~hikari.snowflakes.Snowflake` ], :obj:`~hikari.entities.UNSET` ] channel_mentions: typing.Union[typing.Set[snowflakes.Snowflake], entities.Unset] = marshaller.attrib( raw_name="mention_channels", deserializer=lambda channel_mentions: {snowflakes.Snowflake.deserialize(c["id"]) for c in channel_mentions}, @@ -831,7 +831,7 @@ class MessageUpdateEvent(HikariEvent, snowflakes.UniqueEntity, entities.Deserial #: The message attachments. #: - #: :type: :obj:`typing.Union` [ :obj:`typing.Sequence` [ :obj:`hikari.messages.Attachment` ], :obj:`hikari.entities.UNSET` ] + #: :type: :obj:`~typing.Union` [ :obj:`~typing.Sequence` [ :obj:`~hikari.messages.Attachment` ], :obj:`~hikari.entities.UNSET` ] attachments: typing.Union[typing.Sequence[messages.Attachment], entities.Unset] = marshaller.attrib( deserializer=lambda attachments: [messages.Attachment.deserialize(a) for a in attachments], if_undefined=entities.Unset, @@ -840,7 +840,7 @@ class MessageUpdateEvent(HikariEvent, snowflakes.UniqueEntity, entities.Deserial #: The message's embeds. #: - #: :type: :obj:`typing.Union` [ :obj:`typing.Sequence` [ :obj:`hikari.embeds.Embed` ], :obj:`hikari.entities.UNSET` ] + #: :type: :obj:`~typing.Union` [ :obj:`~typing.Sequence` [ :obj:`~hikari.embeds.Embed` ], :obj:`~hikari.entities.UNSET` ] embeds: typing.Union[typing.Sequence[_embeds.Embed], entities.Unset] = marshaller.attrib( deserializer=lambda embed_objs: [_embeds.Embed.deserialize(e) for e in embed_objs], if_undefined=entities.Unset, @@ -849,7 +849,7 @@ class MessageUpdateEvent(HikariEvent, snowflakes.UniqueEntity, entities.Deserial #: The message's reactions. #: - #: :type: :obj:`typing.Union` [ :obj:`typing.Sequence` [ :obj:`hikari.messages.Reaction` ], :obj:`hikari.entities.UNSET` ] + #: :type: :obj:`~typing.Union` [ :obj:`~typing.Sequence` [ :obj:`~hikari.messages.Reaction` ], :obj:`~hikari.entities.UNSET` ] reactions: typing.Union[typing.Sequence[messages.Reaction], entities.Unset] = marshaller.attrib( deserializer=lambda reactions: [messages.Reaction.deserialize(r) for r in reactions], if_undefined=entities.Unset, @@ -858,49 +858,49 @@ class MessageUpdateEvent(HikariEvent, snowflakes.UniqueEntity, entities.Deserial #: Whether the message is pinned. #: - #: :type: :obj:`typing.Union` [ :obj:`bool`, :obj:`hikari.entities.UNSET` ] + #: :type: :obj:`~typing.Union` [ :obj:`~bool`, :obj:`~hikari.entities.UNSET` ] is_pinned: typing.Union[bool, entities.Unset] = marshaller.attrib( raw_name="pinned", deserializer=bool, if_undefined=entities.Unset, default=entities.UNSET ) #: If the message was generated by a webhook, the webhook's id. #: - #: :type: :obj:`typing.Union` [ :obj:`hikari.snowflakes.Snowflake`, :obj:`hikari.entities.UNSET` ] + #: :type: :obj:`~typing.Union` [ :obj:`~hikari.snowflakes.Snowflake`, :obj:`~hikari.entities.UNSET` ] webhook_id: typing.Union[snowflakes.Snowflake, entities.Unset] = marshaller.attrib( deserializer=snowflakes.Snowflake.deserialize, if_undefined=entities.Unset, default=entities.UNSET ) #: The message's type. #: - #: :type: :obj:`typing.Union` [ :obj:`hikari.messages.MessageType`, :obj:`hikari.entities.UNSET` ] + #: :type: :obj:`~typing.Union` [ :obj:`~hikari.messages.MessageType`, :obj:`~hikari.entities.UNSET` ] type: typing.Union[messages.MessageType, entities.Unset] = marshaller.attrib( deserializer=messages.MessageType, if_undefined=entities.Unset, default=entities.UNSET ) #: The message's activity. #: - #: :type: :obj:`typing.Union` [ :obj:`hikari.messages.MessageActivity`, :obj:`hikari.entities.UNSET` ] + #: :type: :obj:`~typing.Union` [ :obj:`~hikari.messages.MessageActivity`, :obj:`~hikari.entities.UNSET` ] activity: typing.Union[messages.MessageActivity, entities.Unset] = marshaller.attrib( deserializer=messages.MessageActivity.deserialize, if_undefined=entities.Unset, default=entities.UNSET ) #: The message's application. #: - #: :type: :obj:`typing.Union` [ :obj:`hikari.oauth2.Application`, :obj:`hikari.entities.UNSET` ] + #: :type: :obj:`~typing.Union` [ :obj:`~hikari.oauth2.Application`, :obj:`~hikari.entities.UNSET` ] application: typing.Optional[oauth2.Application] = marshaller.attrib( deserializer=oauth2.Application.deserialize, if_undefined=entities.Unset, default=entities.UNSET ) #: The message's crossposted reference data. #: - #: :type: :obj:`typing.Union` [ :obj:`hikari.messages.MessageCrosspost`, :obj:`hikari.entities.UNSET` ] + #: :type: :obj:`~typing.Union` [ :obj:`~hikari.messages.MessageCrosspost`, :obj:`~hikari.entities.UNSET` ] message_reference: typing.Union[messages.MessageCrosspost, entities.Unset] = marshaller.attrib( deserializer=messages.MessageCrosspost.deserialize, if_undefined=entities.Unset, default=entities.UNSET ) #: The message's flags. #: - #: :type: :obj:`typing.Union` [ :obj:`hikari.messages.MessageFlag`, :obj:`hikari.entities.UNSET` ] + #: :type: :obj:`~typing.Union` [ :obj:`~hikari.messages.MessageFlag`, :obj:`~hikari.entities.UNSET` ] flags: typing.Union[messages.MessageFlag, entities.Unset] = marshaller.attrib( deserializer=messages.MessageFlag, if_undefined=entities.Unset, default=entities.UNSET ) @@ -908,7 +908,7 @@ class MessageUpdateEvent(HikariEvent, snowflakes.UniqueEntity, entities.Deserial #: The message nonce. This is a string used for validating #: a message was sent. #: - #: :type: :obj:`typing.Union` [ :obj:`hikari.messages.MessageFlag`, :obj:`hikari.entities.UNSET` ] + #: :type: :obj:`~typing.Union` [ :obj:`~hikari.messages.MessageFlag`, :obj:`~hikari.entities.UNSET` ] nonce: typing.Union[str, entities.Unset] = marshaller.attrib( deserializer=str, if_undefined=entities.Unset, default=entities.UNSET ) @@ -924,19 +924,19 @@ class MessageDeleteEvent(HikariEvent, entities.Deserializable): #: The ID of the channel where this message was deleted. #: - #: :type: :obj:`hikari.snowflakes.Snowflake` + #: :type: :obj:`~hikari.snowflakes.Snowflake` channel_id: snowflakes.Snowflake = marshaller.attrib(deserializer=snowflakes.Snowflake.deserialize) #: The ID of the guild where this message was deleted. - #: Will be :obj:`None` if this message was deleted in a DM channel. + #: Will be :obj:`~None` if this message was deleted in a DM channel. #: - #: :type: :obj:`hikari.snowflakes.Snowflake`, optional + #: :type: :obj:`~hikari.snowflakes.Snowflake`, optional guild_id: typing.Optional[snowflakes.Snowflake] = marshaller.attrib( deserializer=snowflakes.Snowflake.deserialize, if_undefined=None, default=None ) #: The ID of the message that was deleted. #: - #: :type: :obj:`hikari.snowflakes.Snowflake` + #: :type: :obj:`~hikari.snowflakes.Snowflake` message_id: snowflakes.Snowflake = marshaller.attrib(raw_name="id", deserializer=snowflakes.Snowflake.deserialize) @@ -950,20 +950,20 @@ class MessageDeleteBulkEvent(HikariEvent, entities.Deserializable): #: The ID of the channel these messages have been deleted in. #: - #: :type: :obj:`hikari.snowflakes.Snowflake` + #: :type: :obj:`~hikari.snowflakes.Snowflake` channel_id: snowflakes.Snowflake = marshaller.attrib(deserializer=snowflakes.Snowflake.deserialize) #: The ID of the channel these messages have been deleted in. - #: Will be :obj:`None` if these messages were bulk deleted in a DM channel. + #: Will be :obj:`~None` if these messages were bulk deleted in a DM channel. #: - #: :type: :obj:`hikari.snowflakes.Snowflake`, optional + #: :type: :obj:`~hikari.snowflakes.Snowflake`, optional guild_id: typing.Optional[snowflakes.Snowflake] = marshaller.attrib( deserializer=snowflakes.Snowflake.deserialize, if_none=None ) #: A collection of the IDs of the messages that were deleted. #: - #: :type: :obj:`typing.Set` [ :obj:`hikari.snowflakes.Snowflake` ] + #: :type: :obj:`~typing.Set` [ :obj:`~hikari.snowflakes.Snowflake` ] message_ids: typing.Set[snowflakes.Snowflake] = marshaller.attrib( raw_name="ids", deserializer=lambda msgs: {snowflakes.Snowflake.deserialize(m) for m in msgs} ) @@ -976,23 +976,23 @@ class MessageReactionAddEvent(HikariEvent, entities.Deserializable): #: The ID of the user adding the reaction. #: - #: :type: :obj:`hikari.snowflakes.Snowflake` + #: :type: :obj:`~hikari.snowflakes.Snowflake` user_id: snowflakes.Snowflake = marshaller.attrib(deserializer=snowflakes.Snowflake.deserialize) #: The ID of the channel where this reaction is being added. #: - #: :type: :obj:`hikari.snowflakes.Snowflake` + #: :type: :obj:`~hikari.snowflakes.Snowflake` channel_id: snowflakes.Snowflake = marshaller.attrib(deserializer=snowflakes.Snowflake.deserialize) #: The ID of the message this reaction is being added to. #: - #: :type: :obj:`hikari.snowflakes.Snowflake` + #: :type: :obj:`~hikari.snowflakes.Snowflake` message_id: snowflakes.Snowflake = marshaller.attrib(deserializer=snowflakes.Snowflake.deserialize) #: The ID of the guild where this reaction is being added, unless this is #: happening in a DM channel. #: - #: :type: :obj:`hikari.snowflakes.Snowflake`, optional + #: :type: :obj:`~hikari.snowflakes.Snowflake`, optional guild_id: typing.Optional[snowflakes.Snowflake] = marshaller.attrib( deserializer=snowflakes.Snowflake.deserialize, if_undefined=None, default=None ) @@ -1000,14 +1000,14 @@ class MessageReactionAddEvent(HikariEvent, entities.Deserializable): #: The member object of the user who's adding this reaction, if this is #: occurring in a guild. #: - #: :type: :obj:`hikari.guilds.GuildMember`, optional + #: :type: :obj:`~hikari.guilds.GuildMember`, optional member: typing.Optional[guilds.GuildMember] = marshaller.attrib( deserializer=guilds.GuildMember.deserialize, if_undefined=None, default=None ) #: The object of the emoji being added. #: - #: :type: :obj:`typing.Union` [ :obj:`hikari.emojis.UnknownEmoji`, :obj:`hikari.emojis.UnicodeEmoji` ] + #: :type: :obj:`~typing.Union` [ :obj:`~hikari.emojis.UnknownEmoji`, :obj:`~hikari.emojis.UnicodeEmoji` ] emoji: typing.Union[_emojis.UnknownEmoji, _emojis.UnicodeEmoji] = marshaller.attrib( deserializer=_emojis.deserialize_reaction_emoji, ) @@ -1020,30 +1020,30 @@ class MessageReactionRemoveEvent(HikariEvent, entities.Deserializable): #: The ID of the user who is removing their reaction. #: - #: :type: :obj:`hikari.snowflakes.Snowflake` + #: :type: :obj:`~hikari.snowflakes.Snowflake` user_id: snowflakes.Snowflake = marshaller.attrib(deserializer=snowflakes.Snowflake.deserialize) #: The ID of the channel where this reaction is being removed. #: - #: :type: :obj:`hikari.snowflakes.Snowflake` + #: :type: :obj:`~hikari.snowflakes.Snowflake` channel_id: snowflakes.Snowflake = marshaller.attrib(deserializer=snowflakes.Snowflake.deserialize) #: The ID of the message this reaction is being removed from. #: - #: :type: :obj:`hikari.snowflakes.Snowflake` + #: :type: :obj:`~hikari.snowflakes.Snowflake` message_id: snowflakes.Snowflake = marshaller.attrib(deserializer=snowflakes.Snowflake.deserialize) #: The ID of the guild where this reaction is being removed, unless this is #: happening in a DM channel. #: - #: :type: :obj:`hikari.snowflakes.Snowflake`, optional + #: :type: :obj:`~hikari.snowflakes.Snowflake`, optional guild_id: typing.Optional[snowflakes.Snowflake] = marshaller.attrib( deserializer=snowflakes.Snowflake.deserialize, if_undefined=None, default=None ) #: The object of the emoji being removed. #: - #: :type: :obj:`typing.Union` [ :obj:`hikari.emojis.UnknownEmoji`, :obj:`hikari.emojis.UnicodeEmoji` ] + #: :type: :obj:`~typing.Union` [ :obj:`~hikari.emojis.UnknownEmoji`, :obj:`~hikari.emojis.UnicodeEmoji` ] emoji: typing.Union[_emojis.UnicodeEmoji, _emojis.UnknownEmoji] = marshaller.attrib( deserializer=_emojis.deserialize_reaction_emoji, ) @@ -1059,17 +1059,17 @@ class MessageReactionRemoveAllEvent(HikariEvent, entities.Deserializable): #: The ID of the channel where the targeted message is. #: - #: :type: :obj:`hikari.snowflakes.Snowflake` + #: :type: :obj:`~hikari.snowflakes.Snowflake` channel_id: snowflakes.Snowflake = marshaller.attrib(deserializer=snowflakes.Snowflake.deserialize) #: The ID of the message all reactions are being removed from. #: - #: :type: :obj:`hikari.snowflakes.Snowflake` + #: :type: :obj:`~hikari.snowflakes.Snowflake` message_id: snowflakes.Snowflake = marshaller.attrib(deserializer=snowflakes.Snowflake.deserialize) #: The ID of the guild where the targeted message is, if applicable. #: - #: :type: :obj:`hikari.snowflakes.Snowflake` + #: :type: :obj:`~hikari.snowflakes.Snowflake` guild_id: typing.Optional[snowflakes.Snowflake] = marshaller.attrib( deserializer=snowflakes.Snowflake.deserialize, if_undefined=None, default=None ) @@ -1085,24 +1085,24 @@ class MessageReactionRemoveEmojiEvent(HikariEvent, entities.Deserializable): #: The ID of the channel where the targeted message is. #: - #: :type: :obj:`hikari.snowflakes.Snowflake` + #: :type: :obj:`~hikari.snowflakes.Snowflake` channel_id: snowflakes.Snowflake = marshaller.attrib(deserializer=snowflakes.Snowflake.deserialize) #: The ID of the guild where the targeted message is, if applicable. #: - #: :type: :obj:`hikari.snowflakes.Snowflake` + #: :type: :obj:`~hikari.snowflakes.Snowflake` guild_id: typing.Optional[snowflakes.Snowflake] = marshaller.attrib( deserializer=snowflakes.Snowflake.deserialize, if_undefined=None, default=None ) #: The ID of the message the reactions are being removed from. #: - #: :type: :obj:`hikari.snowflakes.Snowflake` + #: :type: :obj:`~hikari.snowflakes.Snowflake` message_id: snowflakes.Snowflake = marshaller.attrib(deserializer=snowflakes.Snowflake.deserialize) #: The object of the emoji that's being removed. #: - #: :type: :obj:`typing.Union` [ :obj:`hikari.emojis.UnknownEmoji`, :obj:`hikari.emojis.UnicodeEmoji` ] + #: :type: :obj:`~typing.Union` [ :obj:`~hikari.emojis.UnknownEmoji`, :obj:`~hikari.emojis.UnicodeEmoji` ] emoji: typing.Union[_emojis.UnicodeEmoji, _emojis.UnknownEmoji] = marshaller.attrib( deserializer=_emojis.deserialize_reaction_emoji, ) @@ -1127,25 +1127,25 @@ class TypingStartEvent(HikariEvent, entities.Deserializable): #: The ID of the channel this typing event is occurring in. #: - #: :type: :obj:`hikari.snowflakes.Snowflake` + #: :type: :obj:`~hikari.snowflakes.Snowflake` channel_id: snowflakes.Snowflake = marshaller.attrib(deserializer=snowflakes.Snowflake.deserialize) #: The ID of the guild this typing event is occurring in. - #: Will be :obj:`None` if this event is happening in a DM channel. + #: Will be :obj:`~None` if this event is happening in a DM channel. #: - #: :type: :obj:`hikari.snowflakes.Snowflake`, optional + #: :type: :obj:`~hikari.snowflakes.Snowflake`, optional guild_id: typing.Optional[snowflakes.Snowflake] = marshaller.attrib( deserializer=snowflakes.Snowflake.deserialize, if_undefined=None, default=None ) #: The ID of the user who triggered this typing event. #: - #: :type: :obj:`hikari.snowflakes.Snowflake` + #: :type: :obj:`~hikari.snowflakes.Snowflake` user_id: snowflakes.Snowflake = marshaller.attrib(deserializer=snowflakes.Snowflake.deserialize) #: The datetime of when this typing event started. #: - #: :type: :obj:`datetime.datetime` + #: :type: :obj:`~datetime.datetime` timestamp: datetime.datetime = marshaller.attrib( deserializer=lambda date: datetime.datetime.fromtimestamp(date, datetime.timezone.utc) ) @@ -1153,7 +1153,7 @@ class TypingStartEvent(HikariEvent, entities.Deserializable): #: The member object of the user who triggered this typing event, #: if this was triggered in a guild. #: - #: :type: :obj:`hikari.guilds.GuildMember`, optional + #: :type: :obj:`~hikari.guilds.GuildMember`, optional member: typing.Optional[guilds.GuildMember] = marshaller.attrib( deserializer=guilds.GuildMember.deserialize, if_undefined=None, default=None ) @@ -1188,17 +1188,17 @@ class VoiceServerUpdateEvent(HikariEvent, entities.Deserializable): #: The voice connection's token #: - #: :type: :obj:`str` + #: :type: :obj:`~str` token: str = marshaller.attrib(deserializer=str) #: The ID of the guild this voice server update is for #: - #: :type: :obj:`hikari.snowflakes.Snowflake` + #: :type: :obj:`~hikari.snowflakes.Snowflake` guild_id: snowflakes.Snowflake = marshaller.attrib(deserializer=snowflakes.Snowflake.deserialize) #: The uri for this voice server host. #: - #: :type: :obj:`str` + #: :type: :obj:`~str` endpoint: str = marshaller.attrib(deserializer=str) @@ -1212,10 +1212,10 @@ class WebhookUpdateEvent(HikariEvent, entities.Deserializable): #: The ID of the guild this webhook is being updated in. #: - #: :type: :obj:`hikari.snowflakes.Snowflake` + #: :type: :obj:`~hikari.snowflakes.Snowflake` guild_id: snowflakes.Snowflake = marshaller.attrib(deserializer=snowflakes.Snowflake.deserialize) #: The ID of the channel this webhook is being updated in. #: - #: :type: :obj:`hikari.snowflakes.Snowflake` + #: :type: :obj:`~hikari.snowflakes.Snowflake` channel_id: snowflakes.Snowflake = marshaller.attrib(deserializer=snowflakes.Snowflake.deserialize) diff --git a/hikari/gateway_entities.py b/hikari/gateway_entities.py index d2cdf8c119..f4fd38880e 100644 --- a/hikari/gateway_entities.py +++ b/hikari/gateway_entities.py @@ -36,18 +36,18 @@ class SessionStartLimit(entities.HikariEntity, entities.Deserializable): #: The total number of session starts the current bot is allowed. #: - #: :type: :obj:`int` + #: :type: :obj:`~int` total: int = marshaller.attrib(deserializer=int) #: The remaining number of session starts this bot has. #: - #: :type: :obj:`int` + #: :type: :obj:`~int` remaining: int = marshaller.attrib(deserializer=int) #: The timedelta of when :attr:`remaining` will reset back to :attr:`total` #: for the current bot. #: - #: :type: :obj:`datetime.timedelta` + #: :type: :obj:`~datetime.timedelta` reset_after: datetime.timedelta = marshaller.attrib( deserializer=lambda after: datetime.timedelta(milliseconds=after), ) @@ -60,17 +60,17 @@ class GatewayBot(entities.HikariEntity, entities.Deserializable): #: The WSS URL that can be used for connecting to the gateway. #: - #: :type: :obj:`str` + #: :type: :obj:`~str` url: str = marshaller.attrib(deserializer=str) #: The recommended number of shards to use when connecting to the gateway. #: - #: :type: :obj:`int` + #: :type: :obj:`~int` shard_count: int = marshaller.attrib(raw_name="shards", deserializer=int) #: Information about the bot's current session start limit. #: - #: :type: :obj:`SessionStartLimit` + #: :type: :obj:`~SessionStartLimit` session_start_limit: SessionStartLimit = marshaller.attrib(deserializer=SessionStartLimit.deserialize) @@ -84,19 +84,19 @@ class GatewayActivity(entities.Deserializable, entities.Serializable): #: The activity name. #: - #: :type: :obj:`str` + #: :type: :obj:`~str` name: str = marshaller.attrib(deserializer=str, serializer=str) #: The activity URL. Only valid for ``STREAMING`` activities. #: - #: :type: :obj:`str`, optional + #: :type: :obj:`~str`, optional url: typing.Optional[str] = marshaller.attrib( deserializer=str, serializer=str, if_none=None, if_undefined=None, default=None ) #: The activity type. #: - #: :type: :obj:`hikari.guilds.ActivityType` + #: :type: :obj:`~hikari.guilds.ActivityType` type: guilds.ActivityType = marshaller.attrib( deserializer=guilds.ActivityType, serializer=int, diff --git a/hikari/guilds.py b/hikari/guilds.py index e6e2cdeb54..bdbb0d17d9 100644 --- a/hikari/guilds.py +++ b/hikari/guilds.py @@ -203,14 +203,14 @@ class GuildEmbed(entities.HikariEntity, entities.Deserializable): #: The ID of the channel the invite for this embed targets, if enabled #: - #: :type: :obj:`hikari.snowflakes.Snowflake`, optional + #: :type: :obj:`~hikari.snowflakes.Snowflake`, optional channel_id: typing.Optional[snowflakes.Snowflake] = marshaller.attrib( deserializer=snowflakes.Snowflake.deserialize, serializer=str, if_none=None ) #: Whether this embed is enabled. #: - #: :type: :obj:`bool` + #: :type: :obj:`~bool` is_enabled: bool = marshaller.attrib(raw_name="enabled", deserializer=bool, serializer=bool) @@ -219,49 +219,49 @@ class GuildEmbed(entities.HikariEntity, entities.Deserializable): class GuildMember(entities.HikariEntity, entities.Deserializable): """Used to represent a guild bound member.""" - #: This member's user object, will be :obj:`None` when attached to Message + #: This member's user object, will be :obj:`~None` when attached to Message #: Create and Update gateway events. #: - #: :type: :obj:`hikari.users.User`, optional + #: :type: :obj:`~hikari.users.User`, optional user: typing.Optional[users.User] = marshaller.attrib( deserializer=users.User.deserialize, if_undefined=None, default=None ) #: This member's nickname, if set. #: - #: :type: :obj:`str`, optional + #: :type: :obj:`~str`, optional nickname: typing.Optional[str] = marshaller.attrib( raw_name="nick", deserializer=str, if_none=None, if_undefined=None, default=None ) #: A sequence of the IDs of the member's current roles. #: - #: :type: :obj:`typing.Sequence` [ :obj:`hikari.snowflakes.Snowflake` ] + #: :type: :obj:`~typing.Sequence` [ :obj:`~hikari.snowflakes.Snowflake` ] role_ids: typing.Sequence[snowflakes.Snowflake] = marshaller.attrib( raw_name="roles", deserializer=lambda role_ids: [snowflakes.Snowflake.deserialize(rid) for rid in role_ids], ) #: The datetime of when this member joined the guild they belong to. #: - #: :type: :obj:`datetime.datetime` + #: :type: :obj:`~datetime.datetime` joined_at: datetime.datetime = marshaller.attrib(deserializer=conversions.parse_iso_8601_ts) #: The datetime of when this member started "boosting" this guild. - #: Will be :obj:`None` if they aren't boosting. + #: Will be :obj:`~None` if they aren't boosting. #: - #: :type: :obj:`datetime.datetime`, optional + #: :type: :obj:`~datetime.datetime`, optional premium_since: typing.Optional[datetime.datetime] = marshaller.attrib( deserializer=conversions.parse_iso_8601_ts, if_none=None, if_undefined=None, default=None ) #: Whether this member is deafened by this guild in it's voice channels. #: - #: :type: :obj:`bool` + #: :type: :obj:`~bool` is_deaf: bool = marshaller.attrib(raw_name="deaf", deserializer=bool) #: Whether this member is muted by this guild in it's voice channels. #: - #: :type: :obj:`bool` + #: :type: :obj:`~bool` is_mute: bool = marshaller.attrib(raw_name="mute", deserializer=bool) @@ -272,7 +272,7 @@ class PartialGuildRole(snowflakes.UniqueEntity, entities.Deserializable): #: The role's name. #: - #: :type: :obj:`str` + #: :type: :obj:`~str` name: str = marshaller.attrib(deserializer=str, serializer=str) @@ -284,38 +284,38 @@ class GuildRole(PartialGuildRole, entities.Serializable): #: The colour of this role, will be applied to a member's name in chat #: if it's their top coloured role. #: - #: :type: :obj:`hikari.colors.Color` + #: :type: :obj:`~hikari.colors.Color` color: colors.Color = marshaller.attrib(deserializer=colors.Color, serializer=int, default=colors.Color(0)) #: Whether this role is hoisting the members it's attached to in the member #: list, members will be hoisted under their highest role where #: :attr:`is_hoisted` is true. #: - #: :type: :obj:`bool` + #: :type: :obj:`~bool` is_hoisted: bool = marshaller.attrib(raw_name="hoist", deserializer=bool, serializer=bool, default=False) #: The position of this role in the role hierarchy. #: - #: :type: :obj:`int` + #: :type: :obj:`~int` position: int = marshaller.attrib(deserializer=int, serializer=int, default=None) #: The guild wide permissions this role gives to the members it's attached #: to, may be overridden by channel overwrites. #: - #: :type: :obj:`hikari.permissions.Permission` + #: :type: :obj:`~hikari.permissions.Permission` permissions: _permissions.Permission = marshaller.attrib( deserializer=_permissions.Permission, serializer=int, default=_permissions.Permission(0) ) #: Whether this role is managed by an integration. #: - #: :type: :obj:`bool` + #: :type: :obj:`~bool` is_managed: bool = marshaller.attrib(raw_name="managed", deserializer=bool, transient=True, default=None) #: Whether this role can be mentioned by all, regardless of the #: ``MENTION_EVERYONE`` permission. #: - #: :type: :obj:`bool` + #: :type: :obj:`~bool` is_mentionable: bool = marshaller.attrib(raw_name="mentionable", deserializer=bool, serializer=bool, default=False) @@ -355,14 +355,14 @@ class ActivityTimestamps(entities.HikariEntity, entities.Deserializable): #: When this activity's session was started, if applicable. #: - #: :type: :obj:`datetime.datetime`, optional + #: :type: :obj:`~datetime.datetime`, optional start: typing.Optional[datetime.datetime] = marshaller.attrib( deserializer=conversions.unix_epoch_to_datetime, if_undefined=None, default=None ) #: When this activity's session will end, if applicable. #: - #: :type: :obj:`datetime.datetime`, optional + #: :type: :obj:`~datetime.datetime`, optional end: typing.Optional[datetime.datetime] = marshaller.attrib( deserializer=conversions.unix_epoch_to_datetime, if_undefined=None, default=None ) @@ -375,12 +375,12 @@ class ActivityParty(entities.HikariEntity, entities.Deserializable): #: The string id of this party instance, if set. #: - #: :type: :obj:`str`, optional + #: :type: :obj:`~str`, optional id: typing.Optional[str] = marshaller.attrib(deserializer=str, if_undefined=None, default=None) #: The size metadata of this party, if applicable. #: - #: :type: :obj:`typing.Tuple` [ :obj:`int`, :obj:`int` ], optional + #: :type: :obj:`~typing.Tuple` [ :obj:`~int`, :obj:`~int` ], optional _size_information: typing.Optional[typing.Tuple[int, int]] = marshaller.attrib( raw_name="size", deserializer=tuple, if_undefined=None, default=None ) @@ -404,22 +404,22 @@ class ActivityAssets(entities.HikariEntity, entities.Deserializable): #: The ID of the asset's large image, if set. #: - #: :type: :obj:`str`, optional + #: :type: :obj:`~str`, optional large_image: typing.Optional[str] = marshaller.attrib(deserializer=str, if_undefined=None, default=None) #: The text that'll appear when hovering over the large image, if set. #: - #: :type: :obj:`str`, optional + #: :type: :obj:`~str`, optional large_text: typing.Optional[str] = marshaller.attrib(deserializer=str, if_undefined=None, default=None) #: The ID of the asset's small image, if set. #: - #: :type: :obj:`str`, optional + #: :type: :obj:`~str`, optional small_image: typing.Optional[str] = marshaller.attrib(deserializer=str, if_undefined=None, default=None) #: The text that'll appear when hovering over the small image, if set. #: - #: :type: :obj:`str`, optional + #: :type: :obj:`~str`, optional small_text: typing.Optional[str] = marshaller.attrib(deserializer=str, if_undefined=None, default=None) @@ -430,17 +430,17 @@ class ActivitySecret(entities.HikariEntity, entities.Deserializable): #: The secret used for joining a party, if applicable. #: - #: :type: :obj:`str`, optional + #: :type: :obj:`~str`, optional join: typing.Optional[str] = marshaller.attrib(deserializer=str, if_undefined=None, default=None) #: The secret used for spectating a party, if applicable. #: - #: :type: :obj:`str`, optional + #: :type: :obj:`~str`, optional spectate: typing.Optional[str] = marshaller.attrib(deserializer=str, if_undefined=None, default=None) #: The secret used for joining a party, if applicable. #: - #: :type: :obj:`str`, optional + #: :type: :obj:`~str`, optional match: typing.Optional[str] = marshaller.attrib(deserializer=str, if_undefined=None, default=None) @@ -477,87 +477,87 @@ class PresenceActivity(entities.HikariEntity, entities.Deserializable): #: The activity's name. #: - #: :type: :obj:`str` + #: :type: :obj:`~str` name: str = marshaller.attrib(deserializer=str) #: The activity's type. #: - #: :type: :obj:`ActivityType` + #: :type: :obj:`~ActivityType` type: ActivityType = marshaller.attrib(deserializer=ActivityType) #: The URL for a ``STREAM`` type activity, if applicable. #: - #: :type: :obj:`str`, optional + #: :type: :obj:`~str`, optional url: typing.Optional[str] = marshaller.attrib(deserializer=str, if_undefined=None, if_none=None, default=None) #: When this activity was added to the user's session. #: - #: :type: :obj:`datetime.datetime` + #: :type: :obj:`~datetime.datetime` created_at: datetime.datetime = marshaller.attrib(deserializer=conversions.unix_epoch_to_datetime) #: The timestamps for when this activity's current state will start and #: end, if applicable. #: - #: :type: :obj:`ActivityTimestamps`, optional + #: :type: :obj:`~ActivityTimestamps`, optional timestamps: typing.Optional[ActivityTimestamps] = marshaller.attrib( deserializer=ActivityTimestamps.deserialize, if_undefined=None, default=None ) #: The ID of the application this activity is for, if applicable. #: - #: :type: :obj:`hikari.snowflakes.Snowflake`, optional + #: :type: :obj:`~hikari.snowflakes.Snowflake`, optional application_id: typing.Optional[snowflakes.Snowflake] = marshaller.attrib( deserializer=snowflakes.Snowflake.deserialize, if_undefined=None, default=None ) #: The text that describes what the activity's target is doing, if set. #: - #: :type: :obj:`str`, optional + #: :type: :obj:`~str`, optional details: typing.Optional[str] = marshaller.attrib(deserializer=str, if_undefined=None, if_none=None, default=None) #: The current status of this activity's target, if set. #: - #: :type: :obj:`str`, optional + #: :type: :obj:`~str`, optional state: typing.Optional[str] = marshaller.attrib(deserializer=str, if_undefined=None, if_none=None, default=None) #: The emoji of this activity, if it is a custom status and set. #: - #: :type: :obj:`typing.Union` [ :obj:`hikari.emojis.UnicodeEmoji`, :obj:`hikari.emojis.UnknownEmoji` ], optional + #: :type: :obj:`~typing.Union` [ :obj:`~hikari.emojis.UnicodeEmoji`, :obj:`~hikari.emojis.UnknownEmoji` ], optional emoji: typing.Union[None, _emojis.UnicodeEmoji, _emojis.UnknownEmoji] = marshaller.attrib( deserializer=_emojis.deserialize_reaction_emoji, if_undefined=None, default=None ) #: Information about the party associated with this activity, if set. #: - #: :type: :obj:`ActivityParty`, optional + #: :type: :obj:`~ActivityParty`, optional party: typing.Optional[ActivityParty] = marshaller.attrib( deserializer=ActivityParty.deserialize, if_undefined=None, default=None ) #: Images and their hover over text for the activity. #: - #: :type: :obj:`ActivityAssets`, optional + #: :type: :obj:`~ActivityAssets`, optional assets: typing.Optional[ActivityAssets] = marshaller.attrib( deserializer=ActivityAssets.deserialize, if_undefined=None, default=None ) #: Secrets for Rich Presence joining and spectating. #: - #: :type: :obj:`ActivitySecret`, optional + #: :type: :obj:`~ActivitySecret`, optional secrets: typing.Optional[ActivitySecret] = marshaller.attrib( deserializer=ActivitySecret.deserialize, if_undefined=None, default=None ) #: Whether this activity is an instanced game session. #: - #: :type: :obj:`bool`, optional + #: :type: :obj:`~bool`, optional is_instance: typing.Optional[bool] = marshaller.attrib( raw_name="instance", deserializer=bool, if_undefined=None, default=None ) #: Flags that describe what the activity includes. #: - #: :type: :obj:`ActivityFlag` + #: :type: :obj:`~ActivityFlag` flags: ActivityFlag = marshaller.attrib(deserializer=ActivityFlag, if_undefined=None, default=None) @@ -587,21 +587,21 @@ class ClientStatus(entities.HikariEntity, entities.Deserializable): #: The status of the target user's desktop session. #: - #: :type: :obj:`PresenceStatus` + #: :type: :obj:`~PresenceStatus` desktop: PresenceStatus = marshaller.attrib( deserializer=PresenceStatus, if_undefined=lambda: PresenceStatus.OFFLINE, default=PresenceStatus.OFFLINE ) #: The status of the target user's mobile session. #: - #: :type: :obj:`PresenceStatus` + #: :type: :obj:`~PresenceStatus` mobile: PresenceStatus = marshaller.attrib( deserializer=PresenceStatus, if_undefined=lambda: PresenceStatus.OFFLINE, default=PresenceStatus.OFFLINE ) #: The status of the target user's web session. #: - #: :type: :obj:`PresenceStatus` + #: :type: :obj:`~PresenceStatus` web: PresenceStatus = marshaller.attrib( deserializer=PresenceStatus, if_undefined=lambda: PresenceStatus.OFFLINE, default=PresenceStatus.OFFLINE ) @@ -614,48 +614,48 @@ class PresenceUser(users.User): Warnings -------- - Every attribute except ``id`` may be received as :obj:`hikari.entities.UNSET` + Every attribute except ``id`` may be received as :obj:`~hikari.entities.UNSET` unless it is specifically being modified for this update. """ #: This user's discriminator. #: - #: :type: :obj:`typing.Union` [ :obj:`str`, :obj:`hikari.entities.UNSET` ] + #: :type: :obj:`~typing.Union` [ :obj:`~str`, :obj:`~hikari.entities.UNSET` ] discriminator: typing.Union[str, entities.Unset] = marshaller.attrib( deserializer=str, if_undefined=entities.Unset, default=entities.UNSET ) #: This user's username. #: - #: :type: :obj:`typing.Union` [ :obj:`str`, :obj:`hikari.entities.UNSET` ] + #: :type: :obj:`~typing.Union` [ :obj:`~str`, :obj:`~hikari.entities.UNSET` ] username: typing.Union[str, entities.Unset] = marshaller.attrib( deserializer=str, if_undefined=entities.Unset, default=entities.UNSET ) #: This user's avatar hash, if set. #: - #: :type: :obj:`typing.Union` [ :obj:`str`, :obj:`hikari.entities.UNSET` ], optional + #: :type: :obj:`~typing.Union` [ :obj:`~str`, :obj:`~hikari.entities.UNSET` ], optional avatar_hash: typing.Union[None, str, entities.Unset] = marshaller.attrib( raw_name="avatar", deserializer=str, if_none=None, if_undefined=entities.Unset, default=entities.UNSET ) #: Whether this user is a bot account. #: - #: :type: :obj:`typing.Union` [ :obj:`bool`, :obj:`hikari.entities.UNSET` ] + #: :type: :obj:`~typing.Union` [ :obj:`~bool`, :obj:`~hikari.entities.UNSET` ] is_bot: typing.Union[bool, entities.Unset] = marshaller.attrib( raw_name="bot", deserializer=bool, if_undefined=entities.Unset, default=entities.UNSET ) #: Whether this user is a system account. #: - #: :type: :obj:`typing.Union` [ :obj:`bool`, :obj:`hikari.entities.UNSET` ] + #: :type: :obj:`~typing.Union` [ :obj:`~bool`, :obj:`~hikari.entities.UNSET` ] is_system: typing.Union[bool, entities.Unset] = marshaller.attrib( raw_name="system", deserializer=bool, if_undefined=entities.Unset, default=entities.UNSET ) #: The public flags for this user. #: - #: :type: :obj:`typing.Union` [ :obj:`hikari.users.UserFlag`, :obj:`hikari.entities.UNSET` ] + #: :type: :obj:`~typing.Union` [ :obj:`~hikari.users.UserFlag`, :obj:`~hikari.entities.UNSET` ] flags: typing.Union[users.UserFlag, entities.Unset] = marshaller.attrib( raw_name="public_flags", deserializer=users.UserFlag, if_undefined=entities.Unset ) @@ -666,8 +666,8 @@ def avatar_url(self) -> typing.Union[str, entities.Unset]: Note ---- - This will be :obj:`hikari.entities.UNSET` if both :attr:`avatar_hash` - and :attr:`discriminator` are :obj:`hikari.entities.UNSET`. + This will be :obj:`~hikari.entities.UNSET` if both :attr:`avatar_hash` + and :attr:`discriminator` are :obj:`~hikari.entities.UNSET`. """ return self.format_avatar_url() @@ -678,26 +678,26 @@ def format_avatar_url( Parameters ---------- - fmt : :obj:`str` + fmt : :obj:`~str` The format to use for this URL, defaults to ``png`` or ``gif``. Supports ``png``, ``jpeg``, ``jpg``, ``webp`` and ``gif`` (when animated). Will be ignored for default avatars which can only be ``png``. - size : :obj:`int` + size : :obj:`~int` The size to set for the URL, defaults to ``4096``. Can be any power of two between 16 and 4096. Will be ignored for default avatars. Returns ------- - :obj:`typing.Union` [ :obj:`str`, :obj:`hikari.entities.UNSET` ] + :obj:`~typing.Union` [ :obj:`~str`, :obj:`~hikari.entities.UNSET` ] The string URL of the user's custom avatar if either :attr:`avatar_hash` is set or their default avatar if - :attr:`discriminator` is set, else :obj:`hikari.entities.UNSET`. + :attr:`discriminator` is set, else :obj:`~hikari.entities.UNSET`. Raises ------ - :obj:`ValueError` + :obj:`~ValueError` If ``size`` is not a power of two or not between 16 and 4096. """ if self.discriminator is entities.UNSET and self.avatar_hash is entities.UNSET: @@ -710,8 +710,8 @@ def default_avatar(self) -> typing.Union[int, entities.Unset]: Note ---- - This will be :obj:`hikari.entities.UNSET` if :attr:`discriminator` is - :obj:`hikari.entities.UNSET`. + This will be :obj:`~hikari.entities.UNSET` if :attr:`discriminator` is + :obj:`~hikari.entities.UNSET`. """ if self.discriminator is not entities.UNSET: return int(self.discriminator) % 5 @@ -727,51 +727,51 @@ class GuildMemberPresence(entities.HikariEntity, entities.Deserializable): #: for this partial object, with other attributes only being included when #: when they are being changed in an event. #: - #: :type: :obj:`PresenceUser` + #: :type: :obj:`~PresenceUser` user: PresenceUser = marshaller.attrib(deserializer=PresenceUser.deserialize) #: A sequence of the ids of the user's current roles in the guild this #: presence belongs to. #: - #: :type: :obj:`typing.Sequence` [ :obj:`hikari.snowflakes.Snowflake` ] + #: :type: :obj:`~typing.Sequence` [ :obj:`~hikari.snowflakes.Snowflake` ] role_ids: typing.Sequence[snowflakes.Snowflake] = marshaller.attrib( raw_name="roles", deserializer=lambda roles: [snowflakes.Snowflake.deserialize(rid) for rid in roles], ) #: The ID of the guild this presence belongs to. #: - #: :type: :obj:`hikari.snowflakes.Snowflake` + #: :type: :obj:`~hikari.snowflakes.Snowflake` guild_id: snowflakes.Snowflake = marshaller.attrib(deserializer=snowflakes.Snowflake.deserialize) #: This user's current status being displayed by the client. #: - #: :type: :obj:`PresenceStatus` + #: :type: :obj:`~PresenceStatus` visible_status: PresenceStatus = marshaller.attrib(raw_name="status", deserializer=PresenceStatus) #: An array of the user's activities, with the top one will being #: prioritised by the client. #: - #: :type: :obj:`typing.Sequence` [ :obj:`PresenceActivity` ] + #: :type: :obj:`~typing.Sequence` [ :obj:`~PresenceActivity` ] activities: typing.Sequence[PresenceActivity] = marshaller.attrib( deserializer=lambda activities: [PresenceActivity.deserialize(a) for a in activities] ) #: An object of the target user's client statuses. #: - #: :type: :obj:`ClientStatus` + #: :type: :obj:`~ClientStatus` client_status: ClientStatus = marshaller.attrib(deserializer=ClientStatus.deserialize) #: The datetime of when this member started "boosting" this guild. - #: Will be :obj:`None` if they aren't boosting. + #: Will be :obj:`~None` if they aren't boosting. #: - #: :type: :obj:`datetime.datetime`, optional + #: :type: :obj:`~datetime.datetime`, optional premium_since: typing.Optional[datetime.datetime] = marshaller.attrib( deserializer=conversions.parse_iso_8601_ts, if_undefined=None, if_none=None, default=None ) #: This member's nickname, if set. #: - #: :type: :obj:`str`, optional + #: :type: :obj:`~str`, optional nick: typing.Optional[str] = marshaller.attrib( raw_name="nick", deserializer=str, if_undefined=None, if_none=None, default=None ) @@ -795,12 +795,12 @@ class IntegrationAccount(entities.HikariEntity, entities.Deserializable): #: The string ID of this (likely) third party account. #: - #: :type: :obj:`str` + #: :type: :obj:`~str` id: str = marshaller.attrib(deserializer=str) #: The name of this account. #: - #: :type: :obj:`str` + #: :type: :obj:`~str` name: str = marshaller.attrib(deserializer=str) @@ -811,17 +811,17 @@ class PartialGuildIntegration(snowflakes.UniqueEntity, entities.Deserializable): #: The name of this integration. #: - #: :type: :obj:`str` + #: :type: :obj:`~str` name: str = marshaller.attrib(deserializer=str) #: The type of this integration. #: - #: :type: :obj:`str` + #: :type: :obj:`~str` type: str = marshaller.attrib(deserializer=str) #: The account connected to this integration. #: - #: :type: :obj:`IntegrationAccount` + #: :type: :obj:`~IntegrationAccount` account: IntegrationAccount = marshaller.attrib(deserializer=IntegrationAccount.deserialize) @@ -832,23 +832,23 @@ class GuildIntegration(snowflakes.UniqueEntity, entities.Deserializable): #: Whether this integration is enabled. #: - #: :type: :obj:`bool` + #: :type: :obj:`~bool` is_enabled: bool = marshaller.attrib(raw_name="enabled", deserializer=bool) #: Whether this integration is syncing subscribers/emojis. #: - #: :type: :obj:`bool` + #: :type: :obj:`~bool` is_syncing: bool = marshaller.attrib(raw_name="syncing", deserializer=bool) #: The ID of the managed role used for this integration's subscribers. #: - #: :type: :obj:`hikari.snowflakes.Snowflake` + #: :type: :obj:`~hikari.snowflakes.Snowflake` role_id: typing.Optional[snowflakes.Snowflake] = marshaller.attrib(deserializer=snowflakes.Snowflake.deserialize) #: Whether users under this integration are allowed to use it's custom #: emojis. #: - #: :type: :obj:`bool`, optional + #: :type: :obj:`~bool`, optional is_emojis_enabled: typing.Optional[bool] = marshaller.attrib( raw_name="enable_emoticons", deserializer=bool, if_undefined=None, default=None ) @@ -856,25 +856,25 @@ class GuildIntegration(snowflakes.UniqueEntity, entities.Deserializable): #: How members should be treated after their connected subscription expires #: This won't be enacted until after :attr:`expire_grace_period` passes. #: - #: :type: :obj:`IntegrationExpireBehaviour` + #: :type: :obj:`~IntegrationExpireBehaviour` expire_behavior: IntegrationExpireBehaviour = marshaller.attrib(deserializer=IntegrationExpireBehaviour) #: The time delta for how many days users with expired subscriptions are #: given until :attr:`expire_behavior` is enacted out on them #: - #: :type: :obj:`datetime.timedelta` + #: :type: :obj:`~datetime.timedelta` expire_grace_period: datetime.timedelta = marshaller.attrib( deserializer=lambda delta: datetime.timedelta(days=delta), ) #: The user this integration belongs to. #: - #: :type: :obj:`hikari.users.User` + #: :type: :obj:`~hikari.users.User` user: users.User = marshaller.attrib(deserializer=users.User.deserialize) #: The datetime of when this integration's subscribers were last synced. #: - #: :type: :obj:`datetime.datetime` + #: :type: :obj:`~datetime.datetime` last_synced_at: datetime.datetime = marshaller.attrib( raw_name="synced_at", deserializer=conversions.parse_iso_8601_ts, if_none=None ) @@ -885,14 +885,14 @@ class GuildIntegration(snowflakes.UniqueEntity, entities.Deserializable): class GuildMemberBan(entities.HikariEntity, entities.Deserializable): """Used to represent guild bans.""" - #: The reason for this ban, will be :obj:`None` if no reason was given. + #: The reason for this ban, will be :obj:`~None` if no reason was given. #: - #: :type: :obj:`str`, optional + #: :type: :obj:`~str`, optional reason: str = marshaller.attrib(deserializer=str, if_none=None) #: The object of the user this ban targets. #: - #: :type: :obj:`hikari.users.User` + #: :type: :obj:`~hikari.users.User` user: users.User = marshaller.attrib(deserializer=users.User.deserialize) @@ -908,9 +908,9 @@ class UnavailableGuild(snowflakes.UniqueEntity, entities.Deserializable): # Ignore docstring not starting in an imperative mood @property def is_unavailable(self) -> bool: # noqa: D401 - """:obj:`True` if this guild is unavailable, else :obj:`False`. + """:obj:`~True` if this guild is unavailable, else :obj:`~False`. - This value is always :obj:`True`, and is only provided for consistency. + This value is always :obj:`~True`, and is only provided for consistency. """ return True @@ -922,17 +922,17 @@ class PartialGuild(snowflakes.UniqueEntity, entities.Deserializable): #: The name of the guild. #: - #: :type: :obj:`str` + #: :type: :obj:`~str` name: str = marshaller.attrib(deserializer=str) #: The hash for the guild icon, if there is one. #: - #: :type: :obj:`str`, optional + #: :type: :obj:`~str`, optional icon_hash: typing.Optional[str] = marshaller.attrib(raw_name="icon", deserializer=str, if_none=None) #: A set of the features in this guild. #: - #: :type: :obj:`typing.Set` [ :obj:`GuildFeature` ] + #: :type: :obj:`~typing.Set` [ :obj:`~GuildFeature` ] features: typing.Set[GuildFeature] = marshaller.attrib( deserializer=lambda features: {conversions.try_cast(f, GuildFeature, f) for f in features}, ) @@ -942,22 +942,22 @@ def format_icon_url(self, fmt: typing.Optional[str] = None, size: int = 4096) -> Parameters ---------- - fmt : :obj:`str` + fmt : :obj:`~str` The format to use for this URL, defaults to ``png`` or ``gif``. Supports ``png``, ``jpeg``, `jpg`, ``webp`` and ``gif`` (when animated). - size : :obj:`int` + size : :obj:`~int` The size to set for the URL, defaults to ``4096``. Can be any power of two between 16 and 4096. Returns ------- - :obj:`str`, optional + :obj:`~str`, optional The string URL. Raises ------ - :obj:`ValueError` + :obj:`~ValueError` If ``size`` is not a power of two or not between 16 and 4096. """ if self.icon_hash: @@ -977,16 +977,16 @@ def icon_url(self) -> typing.Optional[str]: @marshaller.marshallable() @attr.s(slots=True, kw_only=True) class GuildPreview(PartialGuild): - """A preview of a guild with the :obj:`GuildFeature.PUBLIC` feature.""" + """A preview of a guild with the :obj:`~GuildFeature.PUBLIC` feature.""" #: The hash of the splash for the guild, if there is one. #: - #: :type: :obj:`str`, optional + #: :type: :obj:`~str`, optional splash_hash: typing.Optional[str] = marshaller.attrib(raw_name="splash", deserializer=str, if_none=None) #: The hash of the discovery splash for the guild, if there is one. #: - #: :type: :obj:`str`, optional + #: :type: :obj:`~str`, optional discovery_splash_hash: typing.Optional[str] = marshaller.attrib( raw_name="discovery_splash", deserializer=str, if_none=None ) @@ -994,7 +994,7 @@ class GuildPreview(PartialGuild): #: The emojis that this guild provides, represented as a mapping of ID to #: emoji object. #: - #: :type: :obj:`typing.Mapping` [ :obj:`hikari.snowflakes.Snowflake`, :obj:`hikari.emojis.GuildEmoji` ] + #: :type: :obj:`~typing.Mapping` [ :obj:`~hikari.snowflakes.Snowflake`, :obj:`~hikari.emojis.GuildEmoji` ] emojis: typing.Mapping[snowflakes.Snowflake, _emojis.GuildEmoji] = marshaller.attrib( deserializer=lambda emojis: {e.id: e for e in map(_emojis.GuildEmoji.deserialize, emojis)}, ) @@ -1002,18 +1002,18 @@ class GuildPreview(PartialGuild): #: The approximate amount of presences in this invite's guild, only present #: when ``with_counts`` is passed as ``True`` to the GET Invites endpoint. #: - #: :type: :obj:`int`, optional + #: :type: :obj:`~int`, optional approximate_presence_count: typing.Optional[int] = marshaller.attrib(deserializer=int) #: The approximate amount of members in this invite's guild, only present #: when ``with_counts`` is passed as ``True`` to the GET Invites endpoint. #: - #: :type: :obj:`int`, optional + #: :type: :obj:`~int`, optional approximate_member_count: typing.Optional[int] = marshaller.attrib(deserializer=int) #: The guild's description, if set. #: - #: :type: :obj:`str`, optional + #: :type: :obj:`~str`, optional description: typing.Optional[str] = marshaller.attrib(if_none=None, deserializer=str) def format_splash_url(self, fmt: str = "png", size: int = 4096) -> typing.Optional[str]: @@ -1021,21 +1021,21 @@ def format_splash_url(self, fmt: str = "png", size: int = 4096) -> typing.Option Parameters ---------- - fmt : :obj:`str` + fmt : :obj:`~str` The format to use for this URL, defaults to ``png``. Supports ``png``, ``jpeg``, ``jpg`` and ``webp``. - size : :obj:`int` + size : :obj:`~int` The size to set for the URL, defaults to ``4096``. Can be any power of two between 16 and 4096. Returns ------- - :obj:`str`, optional + :obj:`~str`, optional The string URL. Raises ------ - :obj:`ValueError` + :obj:`~ValueError` If ``size`` is not a power of two or not between 16 and 4096. """ if self.splash_hash: @@ -1052,21 +1052,21 @@ def format_discovery_splash_url(self, fmt: str = "png", size: int = 4096) -> typ Parameters ---------- - fmt : :obj:`str` + fmt : :obj:`~str` The format to use for this URL, defaults to ``png``. Supports ``png``, ``jpeg``, ``jpg`` and ``webp``. - size : :obj:`int` + size : :obj:`~int` The size to set for the URL, defaults to ``4096``. Can be any power of two between 16 and 4096. Returns ------- - :obj:`str`, optional + :obj:`~str`, optional The string URL. Raises ------ - :obj:`ValueError` + :obj:`~ValueError` If ``size`` is not a power of two or not between 16 and 4096. """ if self.discovery_splash_hash: @@ -1096,39 +1096,39 @@ class Guild(PartialGuild): #: The hash of the splash for the guild, if there is one. #: - #: :type: :obj:`str`, optional + #: :type: :obj:`~str`, optional splash_hash: typing.Optional[str] = marshaller.attrib(raw_name="splash", deserializer=str, if_none=None) #: The hash of the discovery splash for the guild, if there is one. #: - #: :type: :obj:`str`, optional + #: :type: :obj:`~str`, optional discovery_splash_hash: typing.Optional[str] = marshaller.attrib( raw_name="discovery_splash", deserializer=str, if_none=None ) #: The ID of the owner of this guild. #: - #: :type: :obj:`hikari.snowflakes.Snowflake` + #: :type: :obj:`~hikari.snowflakes.Snowflake` owner_id: snowflakes.Snowflake = marshaller.attrib(deserializer=snowflakes.Snowflake.deserialize) #: The guild level permissions that apply to the bot user, - #: Will be :obj:`None` when this object is retrieved through a REST request + #: Will be :obj:`~None` when this object is retrieved through a REST request #: rather than from the gateway. #: - #: :type: :obj:`hikari.permissions.Permission` + #: :type: :obj:`~hikari.permissions.Permission` my_permissions: _permissions.Permission = marshaller.attrib( raw_name="permissions", deserializer=_permissions.Permission, if_undefined=None, default=None ) #: The voice region for the guild. #: - #: :type: :obj:`str` + #: :type: :obj:`~str` region: str = marshaller.attrib(deserializer=str) #: The ID for the channel that AFK voice users get sent to, if set for the #: guild. #: - #: :type: :obj:`hikari.snowflakes.Snowflake`, optional + #: :type: :obj:`~hikari.snowflakes.Snowflake`, optional afk_channel_id: typing.Optional[snowflakes.Snowflake] = marshaller.attrib( deserializer=snowflakes.Snowflake.deserialize, if_none=None ) @@ -1136,7 +1136,7 @@ class Guild(PartialGuild): #: How long a voice user has to be AFK for before they are classed as being #: AFK and are moved to the AFK channel (:attr:`afk_channel_id`). #: - #: :type: :obj:`datetime.timedelta` + #: :type: :obj:`~datetime.timedelta` afk_timeout: datetime.timedelta = marshaller.attrib( deserializer=lambda seconds: datetime.timedelta(seconds=seconds) ) @@ -1145,9 +1145,9 @@ class Guild(PartialGuild): #: Defines if the guild embed is enabled or not. #: #: This information may not be present, in which case, - #: it will be :obj:`None` instead. + #: it will be :obj:`~None` instead. #: - #: :type: :obj:`bool`, optional + #: :type: :obj:`~bool`, optional is_embed_enabled: typing.Optional[bool] = marshaller.attrib( raw_name="embed_enabled", deserializer=bool, if_undefined=False, default=False ) @@ -1155,35 +1155,35 @@ class Guild(PartialGuild): #: The channel ID that the guild embed will generate an invite to, if #: enabled for this guild. #: - #: Will be :obj:`None` if invites are disabled for this guild's embed. + #: Will be :obj:`~None` if invites are disabled for this guild's embed. #: - #: :type: :obj:`hikari.snowflakes.Snowflake`, optional + #: :type: :obj:`~hikari.snowflakes.Snowflake`, optional embed_channel_id: typing.Optional[snowflakes.Snowflake] = marshaller.attrib( deserializer=snowflakes.Snowflake.deserialize, if_undefined=None, if_none=None, default=None ) #: The verification level required for a user to participate in this guild. #: - #: :type: :obj:`GuildVerificationLevel` + #: :type: :obj:`~GuildVerificationLevel` verification_level: GuildVerificationLevel = marshaller.attrib(deserializer=GuildVerificationLevel) #: The default setting for message notifications in this guild. #: - #: :type: :obj:`GuildMessageNotificationsLevel` + #: :type: :obj:`~GuildMessageNotificationsLevel` default_message_notifications: GuildMessageNotificationsLevel = marshaller.attrib( deserializer=GuildMessageNotificationsLevel ) #: The setting for the explicit content filter in this guild. #: - #: :type: :obj:`GuildExplicitContentFilterLevel` + #: :type: :obj:`~GuildExplicitContentFilterLevel` explicit_content_filter: GuildExplicitContentFilterLevel = marshaller.attrib( deserializer=GuildExplicitContentFilterLevel ) #: The roles in this guild, represented as a mapping of ID to role object. #: - #: :type: :obj:`typing.Mapping` [ :obj:`hikari.snowflakes.Snowflake`, :obj:`GuildRole` ] + #: :type: :obj:`~typing.Mapping` [ :obj:`~hikari.snowflakes.Snowflake`, :obj:`~GuildRole` ] roles: typing.Mapping[snowflakes.Snowflake, GuildRole] = marshaller.attrib( deserializer=lambda roles: {r.id: r for r in map(GuildRole.deserialize, roles)}, ) @@ -1191,20 +1191,20 @@ class Guild(PartialGuild): #: The emojis that this guild provides, represented as a mapping of ID to #: emoji object. #: - #: :type: :obj:`typing.Mapping` [ :obj:`hikari.snowflakes.Snowflake`, :obj:`hikari.emojis.GuildEmoji` ] + #: :type: :obj:`~typing.Mapping` [ :obj:`~hikari.snowflakes.Snowflake`, :obj:`~hikari.emojis.GuildEmoji` ] emojis: typing.Mapping[snowflakes.Snowflake, _emojis.GuildEmoji] = marshaller.attrib( deserializer=lambda emojis: {e.id: e for e in map(_emojis.GuildEmoji.deserialize, emojis)}, ) #: The required MFA level for users wishing to participate in this guild. #: - #: :type: :obj:`GuildMFALevel` + #: :type: :obj:`~GuildMFALevel` mfa_level: GuildMFALevel = marshaller.attrib(deserializer=GuildMFALevel) #: The ID of the application that created this guild, if it was created by - #: a bot. If not, this is always :obj:`None`. + #: a bot. If not, this is always :obj:`~None`. #: - #: :type: :obj:`hikari.snowflakes.Snowflake`, optional + #: :type: :obj:`~hikari.snowflakes.Snowflake`, optional application_id: typing.Optional[snowflakes.Snowflake] = marshaller.attrib( deserializer=snowflakes.Snowflake.deserialize, if_none=None ) @@ -1213,37 +1213,37 @@ class Guild(PartialGuild): #: #: This information is only available if the guild was sent via a #: ``GUILD_CREATE`` event. If the guild is received from any other place, - #: this will always be :obj:`None`. + #: this will always be :obj:`~None`. #: #: An unavailable guild cannot be interacted with, and most information may #: be outdated if that is the case. #: - #: :type: :obj:`bool`, optional + #: :type: :obj:`~bool`, optional is_unavailable: typing.Optional[bool] = marshaller.attrib( raw_name="unavailable", deserializer=bool, if_undefined=None, default=None ) # TODO: document in which cases this information is not available. #: Describes whether the guild widget is enabled or not. If this information - #: is not present, this will be :obj:`None`. + #: is not present, this will be :obj:`~None`. #: - #: :type: :obj:`bool`, optional + #: :type: :obj:`~bool`, optional is_widget_enabled: typing.Optional[bool] = marshaller.attrib( raw_name="widget_enabled", deserializer=bool, if_undefined=None, default=None ) #: The channel ID that the widget's generated invite will send the user to, - #: if enabled. If this information is unavailable, this will be :obj:`None`. + #: if enabled. If this information is unavailable, this will be :obj:`~None`. #: - #: :type: :obj:`hikari.snowflakes.Snowflake`, optional + #: :type: :obj:`~hikari.snowflakes.Snowflake`, optional widget_channel_id: typing.Optional[snowflakes.Snowflake] = marshaller.attrib( deserializer=snowflakes.Snowflake.deserialize, if_undefined=None, if_none=None, default=None ) #: The ID of the system channel (where welcome messages and Nitro boost - #: messages are sent), or :obj:`None` if it is not enabled. + #: messages are sent), or :obj:`~None` if it is not enabled. #: - #: :type: :obj:`hikari.snowflakes.Snowflake`, optional + #: :type: :obj:`~hikari.snowflakes.Snowflake`, optional system_channel_id: typing.Optional[snowflakes.Snowflake] = marshaller.attrib( if_none=None, deserializer=snowflakes.Snowflake.deserialize ) @@ -1251,16 +1251,16 @@ class Guild(PartialGuild): #: Flags for the guild system channel to describe which notification #: features are suppressed. #: - #: :type: :obj:`GuildSystemChannelFlag` + #: :type: :obj:`~GuildSystemChannelFlag` system_channel_flags: GuildSystemChannelFlag = marshaller.attrib(deserializer=GuildSystemChannelFlag) - #: The ID of the channel where guilds with the :obj:`GuildFeature.PUBLIC` + #: The ID of the channel where guilds with the :obj:`~GuildFeature.PUBLIC` #: ``features`` display rules and guidelines. #: - #: If the :obj:`GuildFeature.PUBLIC` feature is not defined, then this is - #: :obj:`None`. + #: If the :obj:`~GuildFeature.PUBLIC` feature is not defined, then this is + #: :obj:`~None`. #: - #: :type: :obj:`hikari.snowflakes.Snowflake`, optional + #: :type: :obj:`~hikari.snowflakes.Snowflake`, optional rules_channel_id: typing.Optional[snowflakes.Snowflake] = marshaller.attrib( if_none=None, deserializer=snowflakes.Snowflake.deserialize ) @@ -1269,9 +1269,9 @@ class Guild(PartialGuild): #: #: This information is only available if the guild was sent via a #: ``GUILD_CREATE`` event. If the guild is received from any other place, - #: this will always be :obj:`None`. + #: this will always be :obj:`~None`. #: - #: :type: :obj:`datetime.datetime`, optional + #: :type: :obj:`~datetime.datetime`, optional joined_at: typing.Optional[datetime.datetime] = marshaller.attrib( deserializer=conversions.parse_iso_8601_ts, if_undefined=None, default=None ) @@ -1280,12 +1280,12 @@ class Guild(PartialGuild): #: #: This information is only available if the guild was sent via a #: ``GUILD_CREATE`` event. If the guild is received from any other place, - #: this will always be :obj:`None`. + #: this will always be :obj:`~None`. #: #: The implications of a large guild are that presence information will #: not be sent about members who are offline or invisible. #: - #: :type: :obj:`bool`, optional + #: :type: :obj:`~bool`, optional is_large: typing.Optional[bool] = marshaller.attrib( raw_name="large", deserializer=bool, if_undefined=None, default=None ) @@ -1294,16 +1294,16 @@ class Guild(PartialGuild): #: #: This information is only available if the guild was sent via a #: ``GUILD_CREATE`` event. If the guild is received from any other place, - #: this will always be :obj:`None`. + #: this will always be :obj:`~None`. #: - #: :type: :obj:`int`, optional + #: :type: :obj:`~int`, optional member_count: typing.Optional[int] = marshaller.attrib(deserializer=int, if_undefined=None, default=None) #: A mapping of ID to the corresponding guild members in this guild. #: #: This information is only available if the guild was sent via a #: ``GUILD_CREATE`` event. If the guild is received from any other place, - #: this will always be :obj:`None`. + #: this will always be :obj:`~None`. #: #: Additionally, any offline members may not be included here, especially #: if there are more members than the large threshold set for the gateway @@ -1316,7 +1316,7 @@ class Guild(PartialGuild): #: representation. If you need complete accurate information, you should #: query the members using the appropriate API call instead. #: - #: :type: :obj:`typing.Mapping` [ :obj:`hikari.snowflakes.Snowflake`, :obj:`GuildMember` ], optional + #: :type: :obj:`~typing.Mapping` [ :obj:`~hikari.snowflakes.Snowflake`, :obj:`~GuildMember` ], optional members: typing.Optional[typing.Mapping[snowflakes.Snowflake, GuildMember]] = marshaller.attrib( deserializer=lambda members: {m.user.id: m for m in map(GuildMember.deserialize, members)}, if_undefined=None, @@ -1327,7 +1327,7 @@ class Guild(PartialGuild): #: #: This information is only available if the guild was sent via a #: ``GUILD_CREATE`` event. If the guild is received from any other place, - #: this will always be :obj:`None`. + #: this will always be :obj:`~None`. #: #: Additionally, any channels that you lack permissions to see will not be #: defined here. @@ -1338,7 +1338,7 @@ class Guild(PartialGuild): #: To retrieve a list of channels in any other case, you should make an #: appropriate API call to retrieve this information. #: - #: :type: :obj:`typing.Mapping` [ :obj:`hikari.snowflakes.Snowflake`, :obj:`hikari.channels.GuildChannel` ], optional + #: :type: :obj:`~typing.Mapping` [ :obj:`~hikari.snowflakes.Snowflake`, :obj:`~hikari.channels.GuildChannel` ], optional channels: typing.Optional[typing.Mapping[snowflakes.Snowflake, _channels.GuildChannel]] = marshaller.attrib( deserializer=lambda guild_channels: {c.id: c for c in map(_channels.deserialize_channel, guild_channels)}, if_undefined=None, @@ -1350,7 +1350,7 @@ class Guild(PartialGuild): #: #: This information is only available if the guild was sent via a #: ``GUILD_CREATE`` event. If the guild is received from any other place, - #: this will always be :obj:`None`. + #: this will always be :obj:`~None`. #: #: Additionally, any channels that you lack permissions to see will not be #: defined here. @@ -1361,7 +1361,7 @@ class Guild(PartialGuild): #: To retrieve a list of presences in any other case, you should make an #: appropriate API call to retrieve this information. #: - #: :type: :obj:`typing.Mapping` [ :obj:`hikari.snowflakes.Snowflake`, :obj:`GuildMemberPresence` ], optional + #: :type: :obj:`~typing.Mapping` [ :obj:`~hikari.snowflakes.Snowflake`, :obj:`~GuildMemberPresence` ], optional presences: typing.Optional[typing.Mapping[snowflakes.Snowflake, GuildMemberPresence]] = marshaller.attrib( deserializer=lambda presences: {p.user.id: p for p in map(GuildMemberPresence.deserialize, presences)}, if_undefined=None, @@ -1370,75 +1370,75 @@ class Guild(PartialGuild): #: The maximum number of presences for the guild. #: - #: If this is :obj:`None`, then the default value is used (currently 5000). + #: If this is :obj:`~None`, then the default value is used (currently 5000). #: - #: :type: :obj:`int`, optional + #: :type: :obj:`~int`, optional max_presences: typing.Optional[int] = marshaller.attrib( deserializer=int, if_undefined=None, if_none=None, default=None ) #: The maximum number of members allowed in this guild. #: - #: This information may not be present, in which case, it will be :obj:`None`. + #: This information may not be present, in which case, it will be :obj:`~None`. #: - #: :type: :obj:`int`, optional + #: :type: :obj:`~int`, optional max_members: typing.Optional[int] = marshaller.attrib(deserializer=int, if_undefined=None, default=None) #: The vanity URL code for the guild's vanity URL. #: - #: This is only present if :obj:`GuildFeature.VANITY_URL` is in the - #: ``features`` for this guild. If not, this will always be :obj:`None`. + #: This is only present if :obj:`~GuildFeature.VANITY_URL` is in the + #: ``features`` for this guild. If not, this will always be :obj:`~None`. #: - #: :type: :obj:`str`, optional + #: :type: :obj:`~str`, optional vanity_url_code: typing.Optional[str] = marshaller.attrib(deserializer=str, if_none=None) #: The guild's description. #: - #: This is only present if certain :obj:`GuildFeature`'s are set in the - #: ``features`` for this guild. Otherwise, this will always be :obj:`None`. + #: This is only present if certain :obj:`~GuildFeature`'s are set in the + #: ``features`` for this guild. Otherwise, this will always be :obj:`~None`. #: - #: :type: :obj:`str`, optional + #: :type: :obj:`~str`, optional description: typing.Optional[str] = marshaller.attrib(if_none=None, deserializer=str) #: The hash for the guild's banner. #: - #: This is only present if the guild has :obj:`GuildFeature.BANNER` in the + #: This is only present if the guild has :obj:`~GuildFeature.BANNER` in the #: ``features`` for this guild. For all other purposes, it is - # :obj:`None`. + # :obj:`~None`. #: - #: :type: :obj:`str`, optional + #: :type: :obj:`~str`, optional banner_hash: typing.Optional[str] = marshaller.attrib(raw_name="banner", if_none=None, deserializer=str) #: The premium tier for this guild. #: - #: :type: :obj:`GuildPremiumTier` + #: :type: :obj:`~GuildPremiumTier` premium_tier: GuildPremiumTier = marshaller.attrib(deserializer=GuildPremiumTier) #: The number of nitro boosts that the server currently has. #: - #: This information may not be present, in which case, it will be :obj:`None`. + #: This information may not be present, in which case, it will be :obj:`~None`. #: - #: :type: :obj:`int`, optional + #: :type: :obj:`~int`, optional premium_subscription_count: typing.Optional[int] = marshaller.attrib( deserializer=int, if_undefined=None, default=None ) #: The preferred locale to use for this guild. #: - #: This can only be change if :obj:`GuildFeature.PUBLIC` is in the + #: This can only be change if :obj:`~GuildFeature.PUBLIC` is in the #: ``features`` for this guild and will otherwise default to ``en-US```. #: - #: :type: :obj:`str` + #: :type: :obj:`~str` preferred_locale: str = marshaller.attrib(deserializer=str) #: The channel ID of the channel where admins and moderators receive notices #: from Discord. #: - #: This is only present if :obj:`GuildFeature.PUBLIC` is in the + #: This is only present if :obj:`~GuildFeature.PUBLIC` is in the #: ``features`` for this guild. For all other purposes, it should be - #: considered to be :obj:`None`. + #: considered to be :obj:`~None`. #: - #: :type: :obj:`hikari.snowflakes.Snowflake`, optional + #: :type: :obj:`~hikari.snowflakes.Snowflake`, optional public_updates_channel_id: typing.Optional[snowflakes.Snowflake] = marshaller.attrib( if_none=None, deserializer=snowflakes.Snowflake.deserialize ) @@ -1448,21 +1448,21 @@ def format_splash_url(self, fmt: str = "png", size: int = 4096) -> typing.Option Parameters ---------- - fmt : :obj:`str` + fmt : :obj:`~str` The format to use for this URL, defaults to ``png``. Supports ``png``, ``jpeg``, ``jpg`` and ``webp``. - size : :obj:`int` + size : :obj:`~int` The size to set for the URL, defaults to ``4096``. Can be any power of two between 16 and 4096. Returns ------- - :obj:`str`, optional + :obj:`~str`, optional The string URL. Raises ------ - :obj:`ValueError` + :obj:`~ValueError` If ``size`` is not a power of two or not between 16 and 4096. """ if self.splash_hash: @@ -1479,21 +1479,21 @@ def format_discovery_splash_url(self, fmt: str = "png", size: int = 4096) -> typ Parameters ---------- - fmt : :obj:`str` + fmt : :obj:`~str` The format to use for this URL, defaults to ``png``. Supports ``png``, ``jpeg``, ``jpg`` and ``webp``. - size : :obj:`int` + size : :obj:`~int` The size to set for the URL, defaults to ``4096``. Can be any power of two between 16 and 4096. Returns ------- - :obj:`str`, optional + :obj:`~str`, optional The string URL. Raises ------ - :obj:`ValueError` + :obj:`~ValueError` If ``size`` is not a power of two or not between 16 and 4096. """ if self.discovery_splash_hash: @@ -1512,21 +1512,21 @@ def format_banner_url(self, fmt: str = "png", size: int = 4096) -> typing.Option Parameters ---------- - fmt : :obj:`str` + fmt : :obj:`~str` The format to use for this URL, defaults to ``png``. Supports ``png``, ``jpeg``, ``jpg`` and ``webp``. - size : :obj:`int` + size : :obj:`~int` The size to set for the URL, defaults to ``4096``. Can be any power of two between 16 and 4096. Returns ------- - :obj:`str`, optional + :obj:`~str`, optional The string URL. Raises ------ - :obj:`ValueError` + :obj:`~ValueError` If ``size`` is not a power of two or not between 16 and 4096. """ if self.banner_hash: diff --git a/hikari/internal/assertions.py b/hikari/internal/assertions.py index 7d17095d4a..3883f5c054 100644 --- a/hikari/internal/assertions.py +++ b/hikari/internal/assertions.py @@ -39,7 +39,7 @@ def assert_that(condition: bool, message: str = None, error_type: type = ValueError) -> None: - """If the given condition is falsified, raise a :obj:`ValueError`. + """If the given condition is falsified, raise a :obj:`~ValueError`. Will be raised with the optional description if provided. """ @@ -48,7 +48,7 @@ def assert_that(condition: bool, message: str = None, error_type: type = ValueEr def assert_not_none(value: ValueT, message: typing.Optional[str] = None) -> ValueT: - """If the given value is :obj:`None`, raise a :obj:`ValueError`. + """If the given value is :obj:`~None`, raise a :obj:`~ValueError`. Will be raised with the optional description if provided. """ @@ -58,13 +58,13 @@ def assert_not_none(value: ValueT, message: typing.Optional[str] = None) -> Valu def assert_in_range(value, min_inclusive, max_inclusive, name: str = None): - """If a value is not in the range [min, max], raise a :obj:`ValueError`.""" + """If a value is not in the range [min, max], raise a :obj:`~ValueError`.""" if not (min_inclusive <= value <= max_inclusive): name = name or "The value" raise ValueError(f"{name} must be in the inclusive range of {min_inclusive} and {max_inclusive}") def assert_is_int_power(value: int, power: int) -> bool: - """If a value is not a power the given int, raise :obj:`ValueError`.""" + """If a value is not a power the given int, raise :obj:`~ValueError`.""" logarithm = math.log(value, power) assert_that(logarithm.is_integer(), f"value must be an integer power of {power}") diff --git a/hikari/internal/conversions.py b/hikari/internal/conversions.py index 78ca540553..04de16d123 100644 --- a/hikari/internal/conversions.py +++ b/hikari/internal/conversions.py @@ -67,8 +67,8 @@ def nullable_cast(value: CastInputT, cast: TypeCastT, /) -> ResultT: """Attempt to cast the given ``value`` with the given ``cast``. - This will only succeed if ``value`` is not :obj:`None`. If it is - :obj:`None`, then :obj:`None` is returned instead. + This will only succeed if ``value`` is not :obj:`~None`. If it is + :obj:`~None`, then :obj:`~None` is returned instead. """ if value is None: return None @@ -78,7 +78,7 @@ def nullable_cast(value: CastInputT, cast: TypeCastT, /) -> ResultT: def try_cast(value: CastInputT, cast: TypeCastT, default: DefaultT = None, /) -> ResultT: """Try to cast the given value to the given cast. - If it throws a :obj:`Exception` or derivative, it will return ``default`` + If it throws a :obj:`~Exception` or derivative, it will return ``default`` instead of the cast value instead. """ with contextlib.suppress(Exception): @@ -91,7 +91,7 @@ def try_cast_or_defer_unary_operator(type_: typing.Type, /): Parameters ---------- - type_ : :obj:`typing.Callable` [ ..., ``output type`` ] + type_ : :obj:`~typing.Callable` [ ..., ``output type`` ] The type to cast to. """ return lambda data: try_cast(data, type_, data) @@ -108,13 +108,13 @@ def put_if_specified( Parameters ---------- - mapping : :obj:`typing.Dict` [ :obj:`typing.Hashable`, :obj:`typing.Any` ] + mapping : :obj:`~typing.Dict` [ :obj:`~typing.Hashable`, :obj:`~typing.Any` ] The mapping to add to. - key : :obj:`typing.Hashable` + key : :obj:`~typing.Hashable` The key to add the value under. - value : :obj:`typing.Any` + value : :obj:`~typing.Any` The value to add. - type_after : :obj:`typing.Callable` [ [ ``input type`` ], ``output type`` ], optional + type_after : :obj:`~typing.Callable` [ [ ``input type`` ], ``output type`` ], optional Type to apply to the value when added. """ if value is not ...: @@ -129,19 +129,19 @@ def image_bytes_to_image_data(img_bytes: typing.Optional[bytes] = None, /) -> ty Parameters ---------- - img_bytes : :obj:`bytes`, optional + img_bytes : :obj:`~bytes`, optional The image bytes. Raises ------ - :obj:`ValueError` + :obj:`~ValueError` If the image type passed is not supported. Returns ------- - :obj:`str`, optional + :obj:`~str`, optional The ``image_bytes`` given encoded into an image data string or - :obj:`None`. + :obj:`~None`. Note ---- @@ -171,12 +171,12 @@ def parse_http_date(date_str: str, /) -> datetime.datetime: Parameters ---------- - date_str : :obj:`str` + date_str : :obj:`~str` The RFC-2822 (section 3.3) compliant date string to parse. Returns ------- - :obj:`datetime.datetime` + :obj:`~datetime.datetime` The HTTP date as a datetime object. See Also @@ -187,16 +187,16 @@ def parse_http_date(date_str: str, /) -> datetime.datetime: def parse_iso_8601_ts(date_string: str, /) -> datetime.datetime: - """Parse an ISO 8601 date string into a :obj:`datetime.datetime` object. + """Parse an ISO 8601 date string into a :obj:`~datetime.datetime` object. Parameters ---------- - date_string : :obj:`str` + date_string : :obj:`~str` The ISO 8601 compliant date string to parse. Returns ------- - :obj:`datetime.datetime` + :obj:`~datetime.datetime` The ISO 8601 date string as a datetime object. See Also @@ -225,32 +225,32 @@ def parse_iso_8601_ts(date_string: str, /) -> datetime.datetime: def discord_epoch_to_datetime(epoch: int, /) -> datetime.datetime: - """Parse a Discord epoch into a :obj:`datetime.datetime` object. + """Parse a Discord epoch into a :obj:`~datetime.datetime` object. Parameters ---------- - epoch : :obj:`int` + epoch : :obj:`~int` Number of milliseconds since 1/1/2015 (UTC) Returns ------- - :obj:`datetime.datetime` + :obj:`~datetime.datetime` Number of seconds since 1/1/1970 within a datetime object (UTC). """ return datetime.datetime.fromtimestamp(epoch / 1000 + DISCORD_EPOCH, datetime.timezone.utc) def unix_epoch_to_datetime(epoch: int, /) -> datetime.datetime: - """Parse a UNIX epoch to a :obj:`datetime.datetime` object. + """Parse a UNIX epoch to a :obj:`~datetime.datetime` object. Parameters ---------- - epoch : :obj:`int` + epoch : :obj:`~int` Number of milliseconds since 1/1/1970 (UTC) Returns ------- - :obj:`datetime.datetime` + :obj:`~datetime.datetime` Number of seconds since 1/1/1970 within a datetime object (UTC). """ return datetime.datetime.fromtimestamp(epoch / 1000, datetime.timezone.utc) @@ -266,9 +266,9 @@ def seek( Parameters ---------- - offset : :obj:`int` + offset : :obj:`~int` The offset to seek to. - whence : :obj:`int` + whence : :obj:`~int` If ``0``, as the default, then use absolute file positioning. If ``1``, then seek to the current position. If ``2``, then seek relative to the end of the file. @@ -279,7 +279,7 @@ def tell(self) -> int: Returns ------- - :obj:`int` + :obj:`~int` The stream position. """ @@ -288,7 +288,7 @@ def read(self) -> typing.AnyStr: Returns ------- - :obj:`str` + :obj:`~str` The string that was read. """ @@ -299,17 +299,17 @@ def close(self) -> None: def make_resource_seekable(resource: typing.Any, /) -> Seekable: """Make a seekable resource to use off some representation of data. - This supports :obj:`bytes`, :obj:`bytearray`, :obj:`memoryview`, and - :obj:`str`. Anything else is just returned. + This supports :obj:`~bytes`, :obj:`~bytearray`, :obj:`~memoryview`, and + :obj:`~str`. Anything else is just returned. Parameters ---------- - resource : :obj:`typing.Any` + resource : :obj:`~typing.Any` The resource to check. Returns ------- - :obj:`typing.Union` [ :obj:`io.BytesIO`, :obj:`io.StringIO` ] + :obj:`~typing.Union` [ :obj:`~io.BytesIO`, :obj:`~io.StringIO` ] An stream-compatible resource where possible. """ if isinstance(resource, (bytes, bytearray)): @@ -335,7 +335,7 @@ def get_bytes_from_resource(resource: typing.Any) -> bytes: Returns ------- - :obj:`bytes` + :obj:`~bytes` The resulting bytes. """ if isinstance(resource, bytearray): @@ -363,38 +363,38 @@ def snoop_typehint_from_scope(frame: types.FrameType, typehint: typing.Union[str ``from __future__ import annotations`` directive is used, the physical thing that the type hint represents will no longer be evaluated by the interpreter. This is an implementation that does not require the use of - :obj:`eval`, and thus reduces the risk of arbitrary code execution as a + :obj:`~eval`, and thus reduces the risk of arbitrary code execution as a result. - Nested parameters such as :obj:`typing.Sequence` should also be able to be + Nested parameters such as :obj:`~typing.Sequence` should also be able to be resolved correctly. Parameters ---------- - frame : :obj:`types.FrameType` + frame : :obj:`~types.FrameType` The stack frame that the element with the typehint was defined in. - This is retrieved using :obj:`inspect.stack` ``(frame_no)[0][0]``, + This is retrieved using :obj:`~inspect.stack` ``(frame_no)[0][0]``, where ``frame_no`` is the number of frames from this invocation that you want to snoop the scope at. - typehint : :obj:`typing.Union` [ :obj:`str`, :obj:`typing.Any` ] - The type hint to resolve. If a non-:obj:`str` is passed, then this is + typehint : :obj:`~typing.Union` [ :obj:`~str`, :obj:`~typing.Any` ] + The type hint to resolve. If a non-:obj:`~str` is passed, then this is returned immediately as the result. Returns ------- - :obj:`typing.Any` + :obj:`~typing.Any` The physical representation of the given type hint. Raises ------ - :obj:`NameError` + :obj:`~NameError` If the attribute was not found. Warnings -------- The input frame must be manually dereferenced using the ``del`` keyword after use. Any functions that are decorated and wrapped when using this - lookup must use :obj:`functools.wraps` to ensure that the correct scope is + lookup must use :obj:`~functools.wraps` to ensure that the correct scope is identified on the stack. This is incredibly unpythonic and baremetal, but due to @@ -421,30 +421,30 @@ def dereference_int_flag( int_flag_type: typing.Type[IntFlagT], raw_value: typing.Union[RawIntFlagValueT, typing.Collection[RawIntFlagValueT]], ) -> IntFlagT: - """Cast to the provided :obj:`enum.IntFlag` type. + """Cast to the provided :obj:`~enum.IntFlag` type. This supports resolving bitfield integers as well as decoding a sequence of case insensitive flag names into one combined value. Parameters ---------- - int_flag_type : :obj:`typing.Type` [ :obj:`enum.IntFlag` ] + int_flag_type : :obj:`~typing.Type` [ :obj:`~enum.IntFlag` ] The type of the int flag to check. raw_value : ``Castable Value`` The raw value to convert. Returns ------- - :obj:`enum.IntFlag` + :obj:`~enum.IntFlag` The cast value as a flag. Notes ----- Types that are a ``Castable Value`` include: - - :obj:`str` - - :obj:`int` - - :obj:`typing.SupportsInt` - - :obj:`typing.Collection` [ ``Castable Value`` ] + - :obj:`~str` + - :obj:`~int` + - :obj:`~typing.SupportsInt` + - :obj:`~typing.Collection` [ ``Castable Value`` ] When a collection is passed, values will be combined using functional reduction via the :obj:operator.or_` operator. diff --git a/hikari/internal/marshaller.py b/hikari/internal/marshaller.py index 6f87042a87..b30bb61981 100644 --- a/hikari/internal/marshaller.py +++ b/hikari/internal/marshaller.py @@ -59,24 +59,24 @@ def dereference_handle(handle_string: str) -> typing.Any: Parameters ---------- - handle_string : :obj:`str` + handle_string : :obj:`~str` The handle to the object to refer to. This is in the format ``fully.qualified.module.name#object.attribute``. If no ``#`` is input, then the reference will be made to the module itself. Returns ------- - :obj:`typing.Any` + :obj:`~typing.Any` The thing that is referred to from this reference. Examples -------- ``"collections#deque"``: - Refers to :obj:`collections.deque` + Refers to :obj:`~collections.deque` ``"asyncio.tasks#Task"``: Refers to ``asyncio.tasks.Task`` ``"hikari.net"``: - Refers to :obj:`hikari.net` + Refers to :obj:`~hikari.net` ``"foo.bar#baz.bork.qux"``: Would refer to a theoretical ``qux`` attribute on a ``bork`` attribute on a ``baz`` object in the ``foo.bar`` module. @@ -114,36 +114,36 @@ def attrib( Parameters ---------- - deserializer : :obj:`typing.Callable` [ [ :obj:`typing.Any` ], :obj:`typing.Any` ], optional + deserializer : :obj:`~typing.Callable` [ [ :obj:`~typing.Any` ], :obj:`~typing.Any` ], optional The deserializer to use to deserialize raw elements. - raw_name : :obj:`str`, optional + raw_name : :obj:`~str`, optional The raw name of the element in its raw serialized form. If not provided, then this will use the field's default name later. - transient : :obj:`bool` - If :obj:`True`, the field is marked as transient, meaning it will not be - serialized. Defaults to :obj:`False`. + transient : :obj:`~bool` + If :obj:`~True`, the field is marked as transient, meaning it will not be + serialized. Defaults to :obj:`~False`. if_none Either a default factory function called to get the default for when - this field is :obj:`None` or one of :obj:`None`, :obj:`False` or - :obj:`True` to specify that this should default to the given singleton. - Will raise an exception when :obj:`None` is received for this field + this field is :obj:`~None` or one of :obj:`~None`, :obj:`~False` or + :obj:`~True` to specify that this should default to the given singleton. + Will raise an exception when :obj:`~None` is received for this field later if this isn't specified. if_undefined Either a default factory function called to get the default for when - this field isn't defined or one of :obj:`None`, :obj:`False` or - :obj:`True` to specify that this should default to the given singleton. + this field isn't defined or one of :obj:`~None`, :obj:`~False` or + :obj:`~True` to specify that this should default to the given singleton. Will raise an exception when this field is undefined later on if this isn't specified. - serializer : :obj:`typing.Callable` [ [ :obj:`typing.Any` ], :obj:`typing.Any` ], optional + serializer : :obj:`~typing.Callable` [ [ :obj:`~typing.Any` ], :obj:`~typing.Any` ], optional The serializer to use. If not specified, then serializing the entire - class that this attribute is in will trigger a :obj:`TypeError` + class that this attribute is in will trigger a :obj:`~TypeError` later. **kwargs : Any kwargs to pass to :func:`attr.ib`. Returns ------- - :obj:`typing.Any` + :obj:`~typing.Any` The result of :func:`attr.ib` internally being called with additional metadata. """ @@ -265,8 +265,8 @@ class HikariEntityMarshaller: """Hikari's utility to manage automated serialization and deserialization. It can deserialize and serialize any internal components that that are - decorated with the :obj:`marshallable` decorator, and that are - :func:`attr.s` classes using fields with the :obj:`attrib` function call + decorated with the :obj:`~marshallable` decorator, and that are + :func:`attr.s` classes using fields with the :obj:`~attrib` function call descriptor. """ @@ -278,19 +278,19 @@ def register(self, cls: typing.Type[EntityT]) -> typing.Type[EntityT]: Parameters ---------- - cls : :obj:`typing.Type` [ :obj:`typing.Any` ] + cls : :obj:`~typing.Type` [ :obj:`~typing.Any` ] The type to register. Returns ------- - :obj:`typing.Type` [ :obj:`typing.Any` ] + :obj:`~typing.Type` [ :obj:`~typing.Any` ] The input argument. This enables this to be used as a decorator if desired. Raises ------ - :obj:`TypeError` - If the class is not an :obj:`attr.s` class. + :obj:`~TypeError` + If the class is not an :obj:`~attr.s` class. """ entity_descriptor = _construct_entity_descriptor(cls) self._registered_entities[cls] = entity_descriptor @@ -301,24 +301,24 @@ def deserialize(self, raw_data: typing.Mapping[str, typing.Any], target_type: ty Parameters ---------- - raw_data : :obj:`typing.Mapping` [ :obj:`str`, :obj:`typing.Any` ] + raw_data : :obj:`~typing.Mapping` [ :obj:`~str`, :obj:`~typing.Any` ] The raw data to deserialize. - target_type : :obj:`typing.Type` [ :obj:`typing.Any` ] + target_type : :obj:`~typing.Type` [ :obj:`~typing.Any` ] The type to deserialize to. Returns ------- - :obj:`typing.Any` + :obj:`~typing.Any` The deserialized instance. Raises ------ - :obj:`LookupError` + :obj:`~LookupError` If the entity is not registered. - :obj:`AttributeError` + :obj:`~AttributeError` If the field is not optional, but the field was not present in the - raw payload, or it was present, but it was assigned :obj:`None`. - :obj:`TypeError` + raw payload, or it was present, but it was assigned :obj:`~None`. + :obj:`~TypeError` If the deserialization call failed for some reason. """ try: @@ -374,17 +374,17 @@ def serialize(self, obj: typing.Optional[typing.Any]) -> typing.Optional[typing. Parameters ---------- - obj : :obj:`typing.Any`, optional + obj : :obj:`~typing.Any`, optional The entity to serialize. Returns ------- - :obj:`typing.Mapping` [ :obj:`str`, :obj:`typing.Any` ], optional + :obj:`~typing.Mapping` [ :obj:`~str`, :obj:`~typing.Any` ], optional The serialized raw data item. Raises ------ - :obj:`LookupError` + :obj:`~LookupError` If the entity is not registered. """ if obj is None: @@ -413,11 +413,11 @@ def serialize(self, obj: typing.Optional[typing.Any]) -> typing.Optional[typing. def marshallable(*, marshaller: HikariEntityMarshaller = HIKARI_ENTITY_MARSHALLER): - """Create a decorator for a class to make it into an :obj:`attr.s` class. + """Create a decorator for a class to make it into an :obj:`~attr.s` class. Parameters ---------- - marshaller : :obj:`HikariEntityMarshaller` + marshaller : :obj:`~HikariEntityMarshaller` If specified, this should be an instance of a marshaller to use. For most internal purposes, you want to not specify this, since it will then default to the hikari-global marshaller instead. This is @@ -430,7 +430,7 @@ def marshallable(*, marshaller: HikariEntityMarshaller = HIKARI_ENTITY_MARSHALLE Notes ----- - The ``auto_attribs`` functionality provided by :obj:`attr.s` is not + The ``auto_attribs`` functionality provided by :obj:`~attr.s` is not supported by this marshaller utility. Do not attempt to use it! Example diff --git a/hikari/internal/meta.py b/hikari/internal/meta.py index 544e5059cf..9ae80ab1b4 100644 --- a/hikari/internal/meta.py +++ b/hikari/internal/meta.py @@ -60,7 +60,7 @@ def __call__(cls): class Singleton(metaclass=SingletonMeta): - """Base type for anything implementing the :obj:`SingletonMeta` metaclass. + """Base type for anything implementing the :obj:`~SingletonMeta` metaclass. Once an instance has been defined at runtime, it will exist until the interpreter that created it is terminated. diff --git a/hikari/internal/more_asyncio.py b/hikari/internal/more_asyncio.py index b47cbb19e4..a581fe0b3a 100644 --- a/hikari/internal/more_asyncio.py +++ b/hikari/internal/more_asyncio.py @@ -41,9 +41,9 @@ # pylint:disable=unused-variable @typing.runtime_checkable class Future(typing.Protocol[T]): - """Typed protocol representation of an :obj:`asyncio.Future`. + """Typed protocol representation of an :obj:`~asyncio.Future`. - You should consult the documentation for :obj:`asyncio.Future` for usage. + You should consult the documentation for :obj:`~asyncio.Future` for usage. """ def result(self) -> T: @@ -87,9 +87,9 @@ def __await__(self) -> typing.Coroutine[None, None, T]: # pylint:disable=unused-variable class Task(Future[T]): - """Typed protocol representation of an :obj:`asyncio.Task`. + """Typed protocol representation of an :obj:`~asyncio.Task`. - You should consult the documentation for :obj:`asyncio.Task` for usage. + You should consult the documentation for :obj:`~asyncio.Task` for usage. """ def get_stack(self, *, limit: typing.Optional[int] = None) -> typing.Sequence[StackFrameType]: @@ -123,12 +123,12 @@ def completed_future(result=None, /): Parameters ---------- - result : :obj:`typing.Any` + result : :obj:`~typing.Any` The value to set for the result of the future. Returns ------- - :obj:`asyncio.Future` + :obj:`~asyncio.Future` The completed future. """ future = asyncio.get_event_loop().create_future() @@ -145,8 +145,8 @@ def wait( Returns ------- - :obj:`typing.Coroutine` [ :obj:`typing.Any`, :obj:`typing.Any`, :obj:`typing.Tuple` [ :obj:`typing.Set` [ :obj:`Future` ], :obj:`typing.Set` [ :obj:`Future` ] ] ] - The coroutine returned by :obj:`asyncio.wait` of two sets of + :obj:`~typing.Coroutine` [ :obj:`~typing.Any`, :obj:`~typing.Any`, :obj:`~typing.Tuple` [ :obj:`~typing.Set` [ :obj:`~Future` ], :obj:`~typing.Set` [ :obj:`~Future` ] ] ] + The coroutine returned by :obj:`~asyncio.wait` of two sets of Tasks/Futures (done, pending). """ # noinspection PyTypeChecker diff --git a/hikari/internal/more_collections.py b/hikari/internal/more_collections.py index 3dc5d7fee2..76cb798268 100644 --- a/hikari/internal/more_collections.py +++ b/hikari/internal/more_collections.py @@ -49,7 +49,7 @@ class WeakKeyDictionary(weakref.WeakKeyDictionary, typing.MutableMapping[K, V]): """A dictionary that has weak references to the keys. - This is a type-safe version of :obj:`weakref.WeakKeyDictionary`. + This is a type-safe version of :obj:`~weakref.WeakKeyDictionary`. """ __slots__ = () diff --git a/hikari/internal/more_logging.py b/hikari/internal/more_logging.py index e579ba97f9..4333bd130d 100644 --- a/hikari/internal/more_logging.py +++ b/hikari/internal/more_logging.py @@ -45,7 +45,7 @@ def get_named_logger(obj: typing.Any, *extra_objs: typing.Any) -> logging.Logger Returns ------- - :obj:`logging.Logger` + :obj:`~logging.Logger` A created logger. """ if not isinstance(obj, str): diff --git a/hikari/internal/urls.py b/hikari/internal/urls.py index 69e29e11e2..f5ba2d37de 100644 --- a/hikari/internal/urls.py +++ b/hikari/internal/urls.py @@ -32,13 +32,13 @@ #: The URL for the CDN. #: -#: :type: :obj:`str` +#: :type: :obj:`~str` BASE_CDN_URL: typing.Final[str] = "https://cdn.discordapp.com" #: The URL for the REST API. This contains a version number parameter that #: should be interpolated. #: -#: :type: :obj:`str` +#: :type: :obj:`~str` REST_API_URL: typing.Final[str] = "https://discordapp.com/api/v{0.version}" @@ -47,25 +47,25 @@ def generate_cdn_url(*route_parts: str, fmt: str, size: typing.Optional[int]) -> Parameters ---------- - route_parts : :obj:`str` + route_parts : :obj:`~str` The string route parts that will be used to form the link. - fmt : :obj:`str` + fmt : :obj:`~str` The format to use for the wanted cdn entity, will usually be one of ``webp``, ``png``, ``jpeg``, ``jpg`` or ``gif`` (which will be invalid if the target entity doesn't have an animated version available). - size : :obj:`int`, optional + size : :obj:`~int`, optional The size to specify for the image in the query string if applicable, - should be passed through as :obj:`None` to avoid the param being set. + should be passed through as :obj:`~None` to avoid the param being set. Must be any power of two between 16 and 4096. Returns ------- - :obj:`str` + :obj:`~str` The URL to the resource on the Discord CDN. Raises ------ - :obj:`ValueError` + :obj:`~ValueError` If ``size`` is not a power of two or not between 16 and 4096. """ if size: diff --git a/hikari/invites.py b/hikari/invites.py index 1928b1e791..af713eec14 100644 --- a/hikari/invites.py +++ b/hikari/invites.py @@ -49,12 +49,12 @@ class VanityUrl(entities.HikariEntity, entities.Deserializable): #: The code for this invite. #: - #: :type: :obj:`str` + #: :type: :obj:`~str` code: str = marshaller.attrib(deserializer=str) #: The amount of times this invite has been used. #: - #: :type: :obj:`int` + #: :type: :obj:`~int` uses: int = marshaller.attrib(deserializer=int) @@ -65,39 +65,39 @@ class InviteGuild(guilds.PartialGuild): #: The hash of the splash for the guild, if there is one. #: - #: :type: :obj:`str`, optional + #: :type: :obj:`~str`, optional splash_hash: typing.Optional[str] = marshaller.attrib(raw_name="splash", deserializer=str, if_none=None) #: The hash for the guild's banner. #: - #: This is only present if :obj:`hikari.guilds.GuildFeature.BANNER` + #: This is only present if :obj:`~hikari.guilds.GuildFeature.BANNER` #: is in the ``features`` for this guild. For all other purposes, it is - #: :obj:`None`. + #: :obj:`~None`. #: - #: :type: :obj:`str`, optional + #: :type: :obj:`~str`, optional banner_hash: typing.Optional[str] = marshaller.attrib(raw_name="banner", if_none=None, deserializer=str) #: The guild's description. #: #: This is only present if certain ``features`` are set in this guild. - #: Otherwise, this will always be :obj:`None`. For all other purposes, it is - #: :obj:`None`. + #: Otherwise, this will always be :obj:`~None`. For all other purposes, it is + #: :obj:`~None`. #: - #: :type: :obj:`str`, optional + #: :type: :obj:`~str`, optional description: typing.Optional[str] = marshaller.attrib(if_none=None, deserializer=str) #: The verification level required for a user to participate in this guild. #: - #: :type: :obj:`hikari.guilds.GuildVerificationLevel` + #: :type: :obj:`~hikari.guilds.GuildVerificationLevel` verification_level: guilds.GuildVerificationLevel = marshaller.attrib(deserializer=guilds.GuildVerificationLevel) #: The vanity URL code for the guild's vanity URL. #: - #: This is only present if :obj:`hikari.guilds.GuildFeature.VANITY_URL` + #: This is only present if :obj:`~hikari.guilds.GuildFeature.VANITY_URL` #: is in the ``features`` for this guild. If not, this will always be - #: :obj:`None`. + #: :obj:`~None`. #: - #: :type: :obj:`str`, optional + #: :type: :obj:`~str`, optional vanity_url_code: typing.Optional[str] = marshaller.attrib(if_none=None, deserializer=str) def format_splash_url(self, fmt: str = "png", size: int = 4096) -> typing.Optional[str]: @@ -105,21 +105,21 @@ def format_splash_url(self, fmt: str = "png", size: int = 4096) -> typing.Option Parameters ---------- - fmt : :obj:`str` + fmt : :obj:`~str` The format to use for this URL, defaults to ``png``. Supports ``png``, ``jpeg``, ``jpg` and ``webp``. - size : :obj:`int` + size : :obj:`~int` The size to set for the URL, defaults to ``4096``. Can be any power of two between 16 and 4096. Returns ------- - :obj:`str`, optional + :obj:`~str`, optional The string URL. Raises ------ - :obj:`ValueError` + :obj:`~ValueError` If ``size`` is not a power of two or not between 16 and 4096. """ if self.splash_hash: @@ -136,21 +136,21 @@ def format_banner_url(self, fmt: str = "png", size: int = 4096) -> typing.Option Parameters ---------- - fmt : :obj:`str` + fmt : :obj:`~str` The format to use for this URL, defaults to ``png``. Supports ``png``, ``jpeg``, ``jpg`` and ``webp``. - size : :obj:`int` + size : :obj:`~int` The size to set for the URL, defaults to ``4096``. Can be any power of two between 16 and 4096. Returns ------- - :obj:`str`, optional + :obj:`~str`, optional The string URL. Raises ------ - :obj:`ValueError` + :obj:`~ValueError` If ``size`` is not a power of two or not between 16 and 4096. """ if self.banner_hash: @@ -170,54 +170,54 @@ class Invite(entities.HikariEntity, entities.Deserializable): #: The code for this invite. #: - #: :type: :obj:`str` + #: :type: :obj:`~str` code: str = marshaller.attrib(deserializer=str) #: The partial object of the guild this dm belongs to. - #: Will be :obj:`None` for group dm invites. + #: Will be :obj:`~None` for group dm invites. #: - #: :type: :obj:`InviteGuild`, optional + #: :type: :obj:`~InviteGuild`, optional guild: typing.Optional[InviteGuild] = marshaller.attrib( deserializer=InviteGuild.deserialize, if_undefined=None, default=None ) #: The partial object of the channel this invite targets. #: - #: :type: :obj:`hikari.channels.PartialChannel` + #: :type: :obj:`~hikari.channels.PartialChannel` channel: channels.PartialChannel = marshaller.attrib(deserializer=channels.PartialChannel.deserialize) #: The object of the user who created this invite. #: - #: :type: :obj:`hikari.users.User`, optional + #: :type: :obj:`~hikari.users.User`, optional inviter: typing.Optional[users.User] = marshaller.attrib( deserializer=users.User.deserialize, if_undefined=None, default=None ) #: The object of the user who this invite targets, if set. #: - #: :type: :obj:`hikari.users.User`, optional + #: :type: :obj:`~hikari.users.User`, optional target_user: typing.Optional[users.User] = marshaller.attrib( deserializer=users.User.deserialize, if_undefined=None, default=None ) #: The type of user target this invite is, if applicable. #: - #: :type: :obj:`TargetUserType`, optional + #: :type: :obj:`~TargetUserType`, optional target_user_type: typing.Optional[TargetUserType] = marshaller.attrib( deserializer=TargetUserType, if_undefined=None, default=None ) #: The approximate amount of presences in this invite's guild, only present - #: when ``with_counts`` is passed as :obj:`True` to the GET Invites endpoint. + #: when ``with_counts`` is passed as :obj:`~True` to the GET Invites endpoint. #: - #: :type: :obj:`int`, optional + #: :type: :obj:`~int`, optional approximate_presence_count: typing.Optional[int] = marshaller.attrib( deserializer=int, if_undefined=None, default=None ) #: The approximate amount of members in this invite's guild, only present - #: when ``with_counts`` is passed as :obj:`True` to the GET Invites endpoint. + #: when ``with_counts`` is passed as :obj:`~True` to the GET Invites endpoint. #: - #: :type: :obj:`int`, optional + #: :type: :obj:`~int`, optional approximate_member_count: typing.Optional[int] = marshaller.attrib( deserializer=int, if_undefined=None, default=None ) @@ -226,7 +226,7 @@ class Invite(entities.HikariEntity, entities.Deserializable): @marshaller.marshallable() @attr.s(slots=True, kw_only=True) class InviteWithMetadata(Invite): - """Extends the base :obj:`Invite` object with metadata. + """Extends the base :obj:`~Invite` object with metadata. The metadata is only returned when getting an invite with guild permissions, rather than it's code. @@ -234,38 +234,38 @@ class InviteWithMetadata(Invite): #: The amount of times this invite has been used. #: - #: :type: :obj:`int` + #: :type: :obj:`~int` uses: int = marshaller.attrib(deserializer=int) #: The limit for how many times this invite can be used before it expires. #: If set to ``0`` then this is unlimited. #: - #: :type: :obj:`int` + #: :type: :obj:`~int` max_uses: int = marshaller.attrib(deserializer=int) #: The timedelta of how long this invite will be valid for. - #: If set to :obj:`None` then this is unlimited. + #: If set to :obj:`~None` then this is unlimited. #: - #: :type: :obj:`datetime.timedelta`, optional + #: :type: :obj:`~datetime.timedelta`, optional max_age: typing.Optional[datetime.timedelta] = marshaller.attrib( deserializer=lambda age: datetime.timedelta(seconds=age) if age > 0 else None ) #: Whether this invite grants temporary membership. #: - #: :type: :obj:`bool` + #: :type: :obj:`~bool` is_temporary: bool = marshaller.attrib(raw_name="temporary", deserializer=bool) #: When this invite was created. #: - #: :type: :obj:`datetime.datetime` + #: :type: :obj:`~datetime.datetime` created_at: datetime.datetime = marshaller.attrib(deserializer=conversions.parse_iso_8601_ts) @property def expires_at(self) -> typing.Optional[datetime.datetime]: """When this invite should expire, if ``max_age`` is set. - If this invite doesn't have a set expiry then this will be :obj:`None`. + If this invite doesn't have a set expiry then this will be :obj:`~None`. """ if self.max_age: return self.created_at + self.max_age diff --git a/hikari/messages.py b/hikari/messages.py index f7a470c9d8..0ec2de382a 100644 --- a/hikari/messages.py +++ b/hikari/messages.py @@ -138,32 +138,32 @@ class Attachment(snowflakes.UniqueEntity, entities.Deserializable): #: The name of the file. #: - #: :type: :obj:`str` + #: :type: :obj:`~str` filename: str = marshaller.attrib(deserializer=str) #: The size of the file in bytes. #: - #: :type: :obj:`int` + #: :type: :obj:`~int` size: int = marshaller.attrib(deserializer=int) #: The source URL of file. #: - #: :type: :obj:`str` + #: :type: :obj:`~str` url: str = marshaller.attrib(deserializer=str) #: The proxied URL of file. #: - #: :type: :obj:`str` + #: :type: :obj:`~str` proxy_url: str = marshaller.attrib(deserializer=str) #: The height of the image (if the file is an image). #: - #: :type: :obj:`int`, optional + #: :type: :obj:`~int`, optional height: typing.Optional[int] = marshaller.attrib(deserializer=int, if_undefined=None, default=None) #: The width of the image (if the file is an image). #: - #: :type: :obj:`int`, optional + #: :type: :obj:`~int`, optional width: typing.Optional[int] = marshaller.attrib(deserializer=int, if_undefined=None, default=None) @@ -174,19 +174,19 @@ class Reaction(entities.HikariEntity, entities.Deserializable): #: The amount of times the emoji has been used to react. #: - #: :type: :obj:`int` + #: :type: :obj:`~int` count: int = marshaller.attrib(deserializer=int) #: The emoji used to react. #: - #: :type: :obj:`typing.Union` [ :obj:`hikari.emojis.UnicodeEmoji`, :obj:`hikari.emojis.UnknownEmoji`] + #: :type: :obj:`~typing.Union` [ :obj:`~hikari.emojis.UnicodeEmoji`, :obj:`~hikari.emojis.UnknownEmoji`] emoji: typing.Union[_emojis.UnicodeEmoji, _emojis.UnknownEmoji] = marshaller.attrib( deserializer=_emojis.deserialize_reaction_emoji ) #: Whether the current user reacted using this emoji. #: - #: :type: :obj:`bool` + #: :type: :obj:`~bool` is_reacted_by_me: bool = marshaller.attrib(raw_name="me", deserializer=bool) @@ -197,12 +197,12 @@ class MessageActivity(entities.HikariEntity, entities.Deserializable): #: The type of message activity. #: - #: :type: :obj:`MessageActivityType` + #: :type: :obj:`~MessageActivityType` type: MessageActivityType = marshaller.attrib(deserializer=MessageActivityType) #: The party ID of the message activity. #: - #: :type: :obj:`str`, optional + #: :type: :obj:`~str`, optional party_id: typing.Optional[str] = marshaller.attrib(deserializer=str, if_undefined=None, default=None) @@ -215,30 +215,30 @@ class MessageCrosspost(entities.HikariEntity, entities.Deserializable): #: #: Warning #: ------- - #: This may be :obj:`None` in some cases according to the Discord API + #: This may be :obj:`~None` in some cases according to the Discord API #: documentation, but the situations that cause this to occur are not #: currently documented. #: #: - #: :type: :obj:`hikari.snowflakes.Snowflake`, optional + #: :type: :obj:`~hikari.snowflakes.Snowflake`, optional message_id: typing.Optional[snowflakes.Snowflake] = marshaller.attrib( deserializer=snowflakes.Snowflake.deserialize, if_undefined=None, default=None ) #: The ID of the channel that the message originated from. #: - #: :type: :obj:`hikari.snowflakes.Snowflake` + #: :type: :obj:`~hikari.snowflakes.Snowflake` channel_id: snowflakes.Snowflake = marshaller.attrib(deserializer=snowflakes.Snowflake.deserialize) #: The ID of the guild that the message originated from. #: #: Warning #: ------- - #: This may be :obj:`None` in some cases according to the Discord API + #: This may be :obj:`~None` in some cases according to the Discord API #: documentation, but the situations that cause this to occur are not #: currently documented. #: - #: :type: :obj:`hikari.snowflakes.Snowflake`, optional + #: :type: :obj:`~hikari.snowflakes.Snowflake`, optional guild_id: typing.Optional[snowflakes.Snowflake] = marshaller.attrib( deserializer=snowflakes.Snowflake.deserialize, if_undefined=None, default=None ) @@ -251,59 +251,59 @@ class Message(snowflakes.UniqueEntity, entities.Deserializable): #: The ID of the channel that the message was sent in. #: - #: :type: :obj:`hikari.snowflakes.Snowflake` + #: :type: :obj:`~hikari.snowflakes.Snowflake` channel_id: snowflakes.Snowflake = marshaller.attrib(deserializer=snowflakes.Snowflake.deserialize) #: The ID of the guild that the message was sent in. #: - #: :type: :obj:`hikari.snowflakes.Snowflake`, optional + #: :type: :obj:`~hikari.snowflakes.Snowflake`, optional guild_id: typing.Optional[snowflakes.Snowflake] = marshaller.attrib( deserializer=snowflakes.Snowflake.deserialize, if_undefined=None, default=None ) #: The author of this message. #: - #: :type: :obj:`hikari.users.User` + #: :type: :obj:`~hikari.users.User` author: users.User = marshaller.attrib(deserializer=users.User.deserialize) #: The member properties for the message's author. #: - #: :type: :obj:`hikari.guilds.GuildMember`, optional + #: :type: :obj:`~hikari.guilds.GuildMember`, optional member: typing.Optional[guilds.GuildMember] = marshaller.attrib( deserializer=guilds.GuildMember.deserialize, if_undefined=None, default=None ) #: The content of the message. #: - #: :type: :obj:`str` + #: :type: :obj:`~str` content: str = marshaller.attrib(deserializer=str) #: The timestamp that the message was sent at. #: - #: :type: :obj:`datetime.datetime` + #: :type: :obj:`~datetime.datetime` timestamp: datetime.datetime = marshaller.attrib(deserializer=conversions.parse_iso_8601_ts) - #: The timestamp that the message was last edited at, or :obj:`None` if it + #: The timestamp that the message was last edited at, or :obj:`~None` if it #: wasn't ever edited. #: - #: :type: :obj:`datetime.datetime`, optional + #: :type: :obj:`~datetime.datetime`, optional edited_timestamp: typing.Optional[datetime.datetime] = marshaller.attrib( deserializer=conversions.parse_iso_8601_ts, if_none=None ) #: Whether the message is a TTS message. #: - #: :type: :obj:`bool` + #: :type: :obj:`~bool` is_tts: bool = marshaller.attrib(raw_name="tts", deserializer=bool) #: Whether the message mentions ``@everyone`` or ``@here``. #: - #: :type: :obj:`bool` + #: :type: :obj:`~bool` is_mentioning_everyone: bool = marshaller.attrib(raw_name="mention_everyone", deserializer=bool) #: The users the message mentions. #: - #: :type: :obj:`typing.Set` [ :obj:`hikari.snowflakes.Snowflake` ] + #: :type: :obj:`~typing.Set` [ :obj:`~hikari.snowflakes.Snowflake` ] user_mentions: typing.Set[snowflakes.Snowflake] = marshaller.attrib( raw_name="mentions", deserializer=lambda user_mentions: {snowflakes.Snowflake.deserialize(u["id"]) for u in user_mentions}, @@ -311,7 +311,7 @@ class Message(snowflakes.UniqueEntity, entities.Deserializable): #: The roles the message mentions. #: - #: :type: :obj:`typing.Set` [ :obj:`hikari.snowflakes.Snowflake` ] + #: :type: :obj:`~typing.Set` [ :obj:`~hikari.snowflakes.Snowflake` ] role_mentions: typing.Set[snowflakes.Snowflake] = marshaller.attrib( raw_name="mention_roles", deserializer=lambda role_mentions: {snowflakes.Snowflake.deserialize(mention) for mention in role_mentions}, @@ -319,7 +319,7 @@ class Message(snowflakes.UniqueEntity, entities.Deserializable): #: The channels the message mentions. #: - #: :type: :obj:`typing.Set` [ :obj:`hikari.snowflakes.Snowflake` ] + #: :type: :obj:`~typing.Set` [ :obj:`~hikari.snowflakes.Snowflake` ] channel_mentions: typing.Set[snowflakes.Snowflake] = marshaller.attrib( raw_name="mention_channels", deserializer=lambda channel_mentions: {snowflakes.Snowflake.deserialize(c["id"]) for c in channel_mentions}, @@ -329,70 +329,70 @@ class Message(snowflakes.UniqueEntity, entities.Deserializable): #: The message attachments. #: - #: :type: :obj:`typing.Sequence` [ :obj:`Attachment` ] + #: :type: :obj:`~typing.Sequence` [ :obj:`~Attachment` ] attachments: typing.Sequence[Attachment] = marshaller.attrib( deserializer=lambda attachments: [Attachment.deserialize(a) for a in attachments] ) #: The message embeds. #: - #: :type: :obj:`typing.Sequence` [ :obj:`hikari.embeds.Embed` ] + #: :type: :obj:`~typing.Sequence` [ :obj:`~hikari.embeds.Embed` ] embeds: typing.Sequence[_embeds.Embed] = marshaller.attrib( deserializer=lambda embeds: [_embeds.Embed.deserialize(e) for e in embeds] ) #: The message reactions. #: - #: :type: :obj:`typing.Sequence` [ :obj:`Reaction` ] + #: :type: :obj:`~typing.Sequence` [ :obj:`~Reaction` ] reactions: typing.Sequence[Reaction] = marshaller.attrib( deserializer=lambda reactions: [Reaction.deserialize(r) for r in reactions], if_undefined=dict, factory=dict ) #: Whether the message is pinned. #: - #: :type: :obj:`bool` + #: :type: :obj:`~bool` is_pinned: bool = marshaller.attrib(raw_name="pinned", deserializer=bool) #: If the message was generated by a webhook, the webhook's id. #: - #: :type: :obj:`hikari.snowflakes.Snowflake`, optional + #: :type: :obj:`~hikari.snowflakes.Snowflake`, optional webhook_id: typing.Optional[snowflakes.Snowflake] = marshaller.attrib( deserializer=snowflakes.Snowflake.deserialize, if_undefined=None, default=None ) #: The message type. #: - #: :type: :obj:`MessageType` + #: :type: :obj:`~MessageType` type: MessageType = marshaller.attrib(deserializer=MessageType) #: The message activity. #: - #: :type: :obj:`MessageActivity`, optional + #: :type: :obj:`~MessageActivity`, optional activity: typing.Optional[MessageActivity] = marshaller.attrib( deserializer=MessageActivity.deserialize, if_undefined=None, default=None ) #: The message application. #: - #: :type: :obj:`hikari.oauth2.Application`, optional + #: :type: :obj:`~hikari.oauth2.Application`, optional application: typing.Optional[oauth2.Application] = marshaller.attrib( deserializer=oauth2.Application.deserialize, if_undefined=None, default=None ) #: The message crossposted reference data. #: - #: :type: :obj:`MessageCrosspost`, optional + #: :type: :obj:`~MessageCrosspost`, optional message_reference: typing.Optional[MessageCrosspost] = marshaller.attrib( deserializer=MessageCrosspost.deserialize, if_undefined=None, default=None ) #: The message flags. #: - #: :type: :obj:`MessageFlag`, optional + #: :type: :obj:`~MessageFlag`, optional flags: typing.Optional[MessageFlag] = marshaller.attrib(deserializer=MessageFlag, if_undefined=None, default=None) #: The message nonce. This is a string used for validating #: a message was sent. #: - #: :type: :obj:`str`, optional + #: :type: :obj:`~str`, optional nonce: typing.Optional[str] = marshaller.attrib(deserializer=str, if_undefined=None, default=None) diff --git a/hikari/net/ratelimits.py b/hikari/net/ratelimits.py index 2651325d55..0852623d54 100644 --- a/hikari/net/ratelimits.py +++ b/hikari/net/ratelimits.py @@ -25,18 +25,18 @@ What is the theory behind this implementation? ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -In this module, we refer to a :obj:`hikari.net.routes.CompiledRoute` as a +In this module, we refer to a :obj:`~hikari.net.routes.CompiledRoute` as a definition of a route with specific major parameter values included (e.g. -``POST /channels/123/messages``), and a :obj:`hikari.net.routes.RouteTemplate` +``POST /channels/123/messages``), and a :obj:`~hikari.net.routes.RouteTemplate` as a definition of a route without specific parameter values included (e.g. ``POST /channels/{channel_id}/messages``). We can compile a -:obj:`hikari.net.routes.CompiledRoute` from a -:obj:`hikari.net.routes.RouteTemplate` by providing the corresponding +:obj:`~hikari.net.routes.CompiledRoute` from a +:obj:`~hikari.net.routes.RouteTemplate` by providing the corresponding parameters as kwargs, as you may already know. In this module, a "bucket" is an internal data structure that tracks and enforces the rate limit state for a specific -:obj:`hikari.net.routes.CompiledRoute`, and can manage delaying tasks in the +:obj:`~hikari.net.routes.CompiledRoute`, and can manage delaying tasks in the event that we begin to get rate limited. It also supports providing in-order execution of queued tasks. @@ -56,13 +56,13 @@ Rate limits, on the other hand, apply to a bucket and are specific to the major parameters of the compiled route. This means that ``POST /channels/123/messages`` and ``POST /channels/456/messages`` do not share the same real bucket, despite -Discord providing the same bucket hash. A real bucket hash is the :obj:`str` +Discord providing the same bucket hash. A real bucket hash is the :obj:`~str` hash of the bucket that Discord sends us in a response concatenated to the corresponding major parameters. This is used for quick bucket indexing internally in this module. One issue that occurs from this is that we cannot effectively hash a -:obj:`hikari.net.routes.CompiledRoute` that has not yet been hit, meaning that +:obj:`~hikari.net.routes.CompiledRoute` that has not yet been hit, meaning that until we receive a response from this endpoint, we have no idea what our rate limits could be, nor the bucket that they sit in. This is usually not problematic, as the first request to an endpoint should never be rate limited @@ -76,18 +76,18 @@ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Each time you ``BaseRateLimiter.acquire()`` a request -timeslice for a given :obj:`hikari.net.routes.CompiledRoute`, several +timeslice for a given :obj:`~hikari.net.routes.CompiledRoute`, several things happen. The first is that we attempt to find the existing bucket for that route, if there is one, or get an unknown bucket otherwise. This is done by creating a real bucket hash` from the compiled route. The initial hash -is calculated using a lookup table that maps :obj:`hikari.net.routes.CompiledRoute` +is calculated using a lookup table that maps :obj:`~hikari.net.routes.CompiledRoute` objects to their corresponding initial hash codes, or to the unknown bucket hash code if not yet known. This initial hash is processed by the -:obj:`hikari.net.routes.CompiledRoute` to provide the real bucket hash we +:obj:`~hikari.net.routes.CompiledRoute` to provide the real bucket hash we need to get the route's bucket object internally. The ``acquire`` method will take the bucket and acquire a new timeslice on -it. This takes the form of a :obj:`asyncio.Future` which should be awaited by +it. This takes the form of a :obj:`~asyncio.Future` which should be awaited by the caller and will complete once the caller is allowed to make a request. Most of the time, this is done instantly, but if the bucket has an active rate limit preventing requests being sent, then the future will be paused until the rate @@ -102,7 +102,7 @@ becomes empty. The result of ``RateLimiter.acquire()`` is a tuple of a -:obj:`asyncio.Future` to await on which completes when you are allowed to +:obj:`~asyncio.Future` to await on which completes when you are allowed to proceed with making a request, and a real bucket hash which should be stored temporarily. This will be explained in the next section. @@ -120,20 +120,20 @@ * ``Date``: the response date on the server. This should be parsed to a - :obj:`datetime.datetime` using :func:`email.utils.parsedate_to_datetime`. + :obj:`~datetime.datetime` using :func:`email.utils.parsedate_to_datetime`. * ``X-RateLimit-Limit``: - an :obj:`int` describing the max requests in the bucket + an :obj:`~int` describing the max requests in the bucket from empty to being rate limited. * ``X-RateLimit-Remaining``: - an :obj:`int` describing the remaining number of + an :obj:`~int` describing the remaining number of requests before rate limiting occurs in the current window. * ``X-RateLimit-Bucket``: - a :obj:`str` containing the initial bucket hash. + a :obj:`~str` containing the initial bucket hash. * ``X-RateLimit-Reset``: - a :obj:`float` containing the number of seconds since + a :obj:`~float` containing the number of seconds since 1st January 1970 at 0:00:00 UTC at which the current ratelimit window - resets. This should be parsed to a :obj:`datetime.datetime` using - :meth:`datetime.datetime.fromtimestamp`, passing :obj:`datetime.timezone.utc` + resets. This should be parsed to a :obj:`~datetime.datetime` using + :meth:`datetime.datetime.fromtimestamp`, passing :obj:`~datetime.timezone.utc` as ``tz``. Each of the above values should be passed to the @@ -147,16 +147,16 @@ Tidying up ~~~~~~~~~~ -To prevent unused buckets cluttering up memory, each :obj:`BaseRateLimiter` -instance spins up a :obj:`asyncio.Task` that periodically locks the bucket +To prevent unused buckets cluttering up memory, each :obj:`~BaseRateLimiter` +instance spins up a :obj:`~asyncio.Task` that periodically locks the bucket list (not threadsafe, only using the concept of asyncio not yielding in regular functions) and disposes of any clearly stale buckets that are no longer needed. These will be recreated again in the future if they are needed. When shutting down an application, one must remember to ``close()`` the -:obj:`BaseRateLimiter` that has been used. This will ensure the garbage collection +:obj:`~BaseRateLimiter` that has been used. This will ensure the garbage collection task is stopped, and will also ensure any remaining futures in any bucket queues -have an :obj:`asyncio.CancelledError` set on them to prevent deadlocking +have an :obj:`~asyncio.CancelledError` set on them to prevent deadlocking ratelimited calls that may be waiting to be unlocked. """ __all__ = [ @@ -185,7 +185,7 @@ #: The hash used for an unknown bucket that has not yet been resolved. #: -#: :type: :obj:`str` +#: :type: :obj:`~str` UNKNOWN_HASH: typing.Final[str] = "UNKNOWN" @@ -207,7 +207,7 @@ def acquire(self) -> more_asyncio.Future[None]: Returns ------- - :obj:`asyncio.Future` + :obj:`~asyncio.Future` A future that should be awaited. Once the future is complete, you can proceed to execute your rate-limited task. """ @@ -234,22 +234,22 @@ class BurstRateLimiter(BaseRateLimiter, abc.ABC): #: The name of the rate limiter. #: - #: :type: :obj:`str` + #: :type: :obj:`~str` name: typing.Final[str] - #: The throttling task, or :obj:`None` if it isn't running. + #: The throttling task, or :obj:`~None` if it isn't running. #: - #: :type: :obj:`asyncio.Task`, optional + #: :type: :obj:`~asyncio.Task`, optional throttle_task: typing.Optional[more_asyncio.Task[None]] #: The queue of any futures under a rate limit. #: - #: :type: :obj:`asyncio.Queue` [`asyncio.Future`] + #: :type: :obj:`~asyncio.Queue` [`asyncio.Future`] queue: typing.Final[typing.List[more_asyncio.Future[None]]] #: The logger used by this rate limiter. #: - #: :type: :obj:`logging.Logger` + #: :type: :obj:`~logging.Logger` logger: typing.Final[logging.Logger] def __init__(self, name: str) -> None: @@ -266,7 +266,7 @@ def acquire(self) -> more_asyncio.Future[None]: Returns ------- - :obj:`asyncio.Future` + :obj:`~asyncio.Future` A future that should be immediately awaited. Once the await completes, you are able to proceed with the operation that is under this rate limit. @@ -294,7 +294,7 @@ def close(self) -> None: @property def is_empty(self) -> bool: - """Return :obj:`True` if no futures are on the queue being rate limited.""" + """Return :obj:`~True` if no futures are on the queue being rate limited.""" return len(self.queue) == 0 @@ -328,7 +328,7 @@ def acquire(self) -> more_asyncio.Future[None]: Returns ------- - :obj:`asyncio.Future` + :obj:`~asyncio.Future` A future that should be immediately awaited. Once the await completes, you are able to proceed with the operation that is under this rate limit. @@ -347,7 +347,7 @@ def throttle(self, retry_after: float) -> None: Iterates repeatedly while the queue is not empty, adhering to any rate limits that occur in the mean time. - retry_after : :obj:`float` + retry_after : :obj:`~float` How long to sleep for before unlocking and releasing any futures in the queue. @@ -357,9 +357,9 @@ def throttle(self, retry_after: float) -> None: (it will not await it to finish) When the :meth:`unlock_later` coroutine function completes, it should be - expected to set the `throttle_task`` to :obj:`None`. This means you can + expected to set the `throttle_task`` to :obj:`~None`. This means you can check if throttling is occurring by checking if ``throttle_task`` - is not :obj:`None`. + is not :obj:`~None`. If this is invoked while another throttle is in progress, that one is cancelled and a new one is started. This enables new rate limits to @@ -376,7 +376,7 @@ async def unlock_later(self, retry_after: float) -> None: Parameters ---------- - retry_after : :obj:`float` + retry_after : :obj:`~float` How long to sleep for before unlocking and releasing any futures in the queue. @@ -386,9 +386,9 @@ async def unlock_later(self, retry_after: float) -> None: instead. When the :meth:`unlock_later` coroutine function completes, it should be - expected to set the ``throttle_task`` to :obj:`None`. This means you can + expected to set the ``throttle_task`` to :obj:`~None`. This means you can check if throttling is occurring by checking if ``throttle_task`` - is not :obj:`None`. + is not :obj:`~None`. """ self.logger.warning("you are being globally rate limited for %ss", retry_after) await asyncio.sleep(retry_after) @@ -431,23 +431,23 @@ class WindowedBurstRateLimiter(BurstRateLimiter): #: The :func:`time.perf_counter` that the limit window ends at. #: - #: :type: :obj:`float` + #: :type: :obj:`~float` reset_at: float #: The number of :meth:`acquire`'s left in this window before you will get #: rate limited. #: - #: :type: :obj:`int` + #: :type: :obj:`~int` remaining: int #: How long the window lasts for from the start in seconds. #: - #: :type: :obj:`float` + #: :type: :obj:`~float` period: float #: The maximum number of :meth:`acquire`'s allowed in this time window. #: - #: :type: :obj:`int` + #: :type: :obj:`~int` limit: int def __init__(self, name: str, period: float, limit: int) -> None: @@ -462,7 +462,7 @@ def acquire(self) -> more_asyncio.Future[None]: Returns ------- - :obj:`asyncio.Future` + :obj:`~asyncio.Future` A future that should be immediately awaited. Once the await completes, you are able to proceed with the operation that is under this rate limit. @@ -489,12 +489,12 @@ def get_time_until_reset(self, now: float) -> float: Parameters ---------- - now : :obj:`float` + now : :obj:`~float` The monotonic :func:`time.perf_counter` timestamp. Returns ------- - :obj:`float` + :obj:`~float` The time left to sleep before the rate limit is reset. If no rate limit is in effect, then this will return ``0.0`` instead. @@ -515,13 +515,13 @@ def is_rate_limited(self, now: float) -> bool: Parameters ---------- - now : :obj:`float` + now : :obj:`~float` The monotonic :func:`time.perf_counter` timestamp. Returns ------- - :obj:`bool` - :obj:`True` if we are being rate limited. :obj:`False` if we are not. + :obj:`~bool` + :obj:`~True` if we are being rate limited. :obj:`~False` if we are not. Warning ------- @@ -555,8 +555,8 @@ async def throttle(self) -> None: task immediately in ``throttle_task``. When this coroutine function completes, it will set the - ``throttle_task`` to :obj:`None`. This means you can check if throttling - is occurring by checking if ``throttle_task`` is not :obj:`None`. + ``throttle_task`` to :obj:`~None`. This means you can check if throttling + is occurring by checking if ``throttle_task`` is not :obj:`~None`. """ self.logger.debug( "you are being rate limited on bucket %s, backing off for %ss", @@ -581,7 +581,7 @@ class HTTPBucketRateLimiter(WindowedBurstRateLimiter): Component to represent an active rate limit bucket on a specific HTTP route with a specific major parameter combo. - This is somewhat similar to the :obj:`WindowedBurstRateLimiter` in how it + This is somewhat similar to the :obj:`~WindowedBurstRateLimiter` in how it works. This algorithm will use fixed-period time windows that have a given limit @@ -601,7 +601,7 @@ class HTTPBucketRateLimiter(WindowedBurstRateLimiter): #: The compiled route that this rate limit is covering. #: - #: :type: :obj:`hikari.net.routes.CompiledRoute` + #: :type: :obj:`~hikari.net.routes.CompiledRoute` compiled_route: typing.Final[routes.CompiledRoute] def __init__(self, name: str, compiled_route: routes.CompiledRoute) -> None: @@ -612,7 +612,7 @@ def __init__(self, name: str, compiled_route: routes.CompiledRoute) -> None: @property def is_unknown(self) -> bool: - """Return :obj:`True` if the bucket represents an ``UNKNOWN`` bucket.""" + """Return :obj:`~True` if the bucket represents an ``UNKNOWN`` bucket.""" return self.name.startswith(UNKNOWN_HASH) def acquire(self) -> more_asyncio.Future[None]: @@ -620,7 +620,7 @@ def acquire(self) -> more_asyncio.Future[None]: Returns ------- - :obj:`asyncio.Future` + :obj:`~asyncio.Future` A future that should be awaited immediately. Once the future completes, you are allowed to proceed with your operation. @@ -636,11 +636,11 @@ def update_rate_limit(self, remaining: int, limit: int, reset_at: float) -> None Parameters ---------- - remaining : :obj:`int` + remaining : :obj:`~int` The calls remaining in this time window. - limit : :obj:`int` + limit : :obj:`~int` The total calls allowed in this time window. - reset_at : :obj:`float` + reset_at : :obj:`~float` The epoch at which to reset the limit. Note @@ -685,29 +685,29 @@ class HTTPBucketRateLimiterManager: #: Maps compiled routes to their ``X-RateLimit-Bucket`` header being used. #: - #: :type: :obj:`typing.MutableMapping` [ :obj:`hikari.net.routes.CompiledRoute`, :obj:`str` ] + #: :type: :obj:`~typing.MutableMapping` [ :obj:`~hikari.net.routes.CompiledRoute`, :obj:`~str` ] routes_to_hashes: typing.Final[typing.MutableMapping[routes.CompiledRoute, str]] #: Maps full bucket hashes (``X-RateLimit-Bucket`` appended with a hash of #: major parameters used in that compiled route) to their corresponding rate #: limiters. #: - #: :type: :obj:`typing.MutableMapping` [ :obj:`str`, :obj:`HTTPBucketRateLimiter` ] + #: :type: :obj:`~typing.MutableMapping` [ :obj:`~str`, :obj:`~HTTPBucketRateLimiter` ] real_hashes_to_buckets: typing.Final[typing.MutableMapping[str, HTTPBucketRateLimiter]] #: An internal event that is set when the object is shut down. #: - #: :type: :obj:`asyncio.Event` + #: :type: :obj:`~asyncio.Event` closed_event: typing.Final[asyncio.Event] #: The internal garbage collector task. #: - #: :type: :obj:`asyncio.Task`, optional + #: :type: :obj:`~asyncio.Task`, optional gc_task: typing.Optional[more_asyncio.Task[None]] #: The logger to use for this object. #: - #: :type: :obj:`logging.Logger` + #: :type: :obj:`~logging.Logger` logger: typing.Final[logging.Logger] def __init__(self) -> None: @@ -735,7 +735,7 @@ def start(self, poll_period: float = 20) -> None: Parameters ---------- - poll_period : :obj:`float` + poll_period : :obj:`~float` Period to poll the garbage collector at in seconds. Defaults to ``20`` seconds. """ @@ -766,7 +766,7 @@ async def gc(self, poll_period: float = 20) -> None: # noqa: D401 Parameters ---------- - poll_period : :obj:`float` + poll_period : :obj:`~float` The period to poll at. This defaults to once every ``20`` seconds. Warnings @@ -820,12 +820,12 @@ def acquire(self, compiled_route: routes.CompiledRoute) -> more_asyncio.Future: Parameters ---------- - compiled_route : :obj:`hikari.net.routes.CompiledRoute` + compiled_route : :obj:`~hikari.net.routes.CompiledRoute` The route to get the bucket for. Returns ------- - :obj:`asyncio.Future` + :obj:`~asyncio.Future` A future to await that completes when you are allowed to run your request logic. @@ -868,20 +868,20 @@ def update_rate_limits( Parameters ---------- - compiled_route : :obj:`hikari.net.routes.CompiledRoute` + compiled_route : :obj:`~hikari.net.routes.CompiledRoute` The compiled route to get the bucket for. - bucket_header : :obj:`str`, optional + bucket_header : :obj:`~str`, optional The ``X-RateLimit-Bucket`` header that was provided in the response, - or :obj:`None` if not present. - remaining_header : :obj:`int` - The ``X-RateLimit-Remaining`` header cast to an :obj:`int`. - limit_header : :obj:`int` - The ``X-RateLimit-Limit`` header cast to an :obj:`int`. - date_header : :obj:`datetime.datetime` - The ``Date`` header value as a :obj:`datetime.datetime`. - reset_at_header : :obj:`datetime.datetime` + or :obj:`~None` if not present. + remaining_header : :obj:`~int` + The ``X-RateLimit-Remaining`` header cast to an :obj:`~int`. + limit_header : :obj:`~int` + The ``X-RateLimit-Limit`` header cast to an :obj:`~int`. + date_header : :obj:`~datetime.datetime` + The ``Date`` header value as a :obj:`~datetime.datetime`. + reset_at_header : :obj:`~datetime.datetime` The ``X-RateLimit-Reset`` header value as a - :obj:`datetime.datetime`. + :obj:`~datetime.datetime`. """ self.routes_to_hashes[compiled_route] = bucket_header @@ -912,13 +912,13 @@ class ExponentialBackOff: Parameters ---------- - base : :obj:`float` + base : :obj:`~float` The base to use. Defaults to ``2``. - maximum : :obj:`float`, optional - If not :obj:`None`, then this is the max value the backoff can be in a - single iteration before an :obj:`asyncio.TimeoutError` is raised. + maximum : :obj:`~float`, optional + If not :obj:`~None`, then this is the max value the backoff can be in a + single iteration before an :obj:`~asyncio.TimeoutError` is raised. Defaults to ``64`` seconds. - jitter_multiplier : :obj:`float` + jitter_multiplier : :obj:`~float` The multiplier for the random jitter. Defaults to ``1``. Set to ``0`` to disable jitter. """ @@ -927,24 +927,24 @@ class ExponentialBackOff: #: The base to use. Defaults to 2. #: - #: :type: :obj:`float` + #: :type: :obj:`~float` base: typing.Final[float] #: The current increment. #: - #: :type: :obj:`int` + #: :type: :obj:`~int` increment: int - #: If not :obj:`None`, then this is the max value the backoff can be in a - #: single iteration before an :obj:`asyncio.TimeoutError` is raised. + #: If not :obj:`~None`, then this is the max value the backoff can be in a + #: single iteration before an :obj:`~asyncio.TimeoutError` is raised. #: - #: :type: :obj:`float`, optional + #: :type: :obj:`~float`, optional maximum: typing.Optional[float] #: The multiplier for the random jitter. Defaults to ``1``. #: Set to ``0`` to disable jitter. #: - #: :type: :obj:`float` + #: :type: :obj:`~float` jitter_multiplier: typing.Final[float] def __init__(self, base: float = 2, maximum: typing.Optional[float] = 64, jitter_multiplier: float = 1) -> None: diff --git a/hikari/net/rest.py b/hikari/net/rest.py index 5454c94fe4..e105d9093a 100644 --- a/hikari/net/rest.py +++ b/hikari/net/rest.py @@ -50,25 +50,25 @@ class LowLevelRestfulClient: Parameters ---------- - base_url: :obj:`str` + base_url: :obj:`~str` The base URL and route for the discord API - allow_redirects: :obj:`bool` + allow_redirects: :obj:`~bool` Whether to allow redirects or not. - connector: :obj:`aiohttp.BaseConnector`, optional + connector: :obj:`~aiohttp.BaseConnector`, optional Optional aiohttp connector info for making an HTTP connection - proxy_headers: :obj:`typing.Mapping` [ :obj:`str`, :obj:`str` ], optional + proxy_headers: :obj:`~typing.Mapping` [ :obj:`~str`, :obj:`~str` ], optional Optional proxy headers to pass to HTTP requests. - proxy_auth: :obj:`aiohttp.BasicAuth`, optional + proxy_auth: :obj:`~aiohttp.BasicAuth`, optional Optional authorization to be used if using a proxy. - proxy_url: :obj:`str`, optional + proxy_url: :obj:`~str`, optional Optional proxy URL to use for HTTP requests. - ssl_context: :obj:`ssl.SSLContext`, optional + ssl_context: :obj:`~ssl.SSLContext`, optional The optional SSL context to be used. - verify_ssl: :obj:`bool` + verify_ssl: :obj:`~bool` Whether or not the client should enforce SSL signed certificate - verification. If :obj:`False` it will ignore potentially malicious + verification. If :obj:`~False` it will ignore potentially malicious SSL certificates. - timeout: :obj:`float`, optional + timeout: :obj:`~float`, optional The optional timeout for all HTTP requests. json_deserialize: ``deserialization function`` A custom JSON deserializer function to use. Defaults to @@ -76,14 +76,14 @@ class LowLevelRestfulClient: json_serialize: ``serialization function`` A custom JSON serializer function to use. Defaults to :func:`json.dumps`. - token: :obj:`string`, optional + token: :obj:`~string`, optional The bot token for the client to use. You may start this with a prefix of either ``Bot`` or ``Bearer`` to force the token type, or not provide this information if you want to have it auto-detected. - If this is passed as :obj:`None`, then no token is used. - This will be passed as the ``Authorization`` header if not :obj:`None` + If this is passed as :obj:`~None`, then no token is used. + This will be passed as the ``Authorization`` header if not :obj:`~None` for each request. - version: :obj:`typing.Union` [ :obj:`int`, :obj:`hikari.net.versions.HTTPAPIVersion` ] + version: :obj:`~typing.Union` [ :obj:`~int`, :obj:`~hikari.net.versions.HTTPAPIVersion` ] The version of the API to use. Defaults to the most recent stable version. """ @@ -98,37 +98,37 @@ class LowLevelRestfulClient: _AUTHENTICATION_SCHEMES = ("Bearer", "Bot") - #: :obj:`True` if HTTP redirects are enabled, or :obj:`False` otherwise. + #: :obj:`~True` if HTTP redirects are enabled, or :obj:`~False` otherwise. #: - #: :type: :obj:`bool` + #: :type: :obj:`~bool` allow_redirects: bool #: The base URL to send requests to. #: - #: :type: :obj:`str` + #: :type: :obj:`~str` base_url: str #: The :mod:`aiohttp` client session to make requests with. #: - #: :type: :obj:`aiohttp.ClientSession` + #: :type: :obj:`~aiohttp.ClientSession` client_session: aiohttp.ClientSession #: The internal correlation ID for the number of requests sent. This will #: increase each time a REST request is sent. #: - #: :type: :obj:`int` + #: :type: :obj:`~int` in_count: int #: The global ratelimiter. This is used if Discord declares a ratelimit #: across the entire API, regardless of the endpoint. If this is set, then #: any HTTP operation using this session will be paused. #: - #: :type: :obj:`hikari.net.ratelimits.ManualRateLimiter` + #: :type: :obj:`~hikari.net.ratelimits.ManualRateLimiter` global_ratelimiter: ratelimits.ManualRateLimiter #: The logger to use for this object. #: - #: :type: :obj:`logging.Logger` + #: :type: :obj:`~logging.Logger` logger: logging.Logger #: The JSON deserialization function. This consumes a JSON string and @@ -141,17 +141,17 @@ class LowLevelRestfulClient: #: Proxy authorization to use. #: - #: :type: :obj:`aiohttp.BasicAuth`, optional + #: :type: :obj:`~aiohttp.BasicAuth`, optional proxy_auth: typing.Optional[aiohttp.BasicAuth] #: A set of headers to provide to a proxy server. #: - #: :type: :obj:`typing.Mapping` [ :obj:`str`, :obj:`str` ], optional + #: :type: :obj:`~typing.Mapping` [ :obj:`~str`, :obj:`~str` ], optional proxy_headers: typing.Optional[typing.Mapping[str, str]] #: An optional proxy URL to send requests to. #: - #: :type: :obj:`str`, optional + #: :type: :obj:`~str`, optional proxy_url: typing.Optional[str] #: The per-route ratelimit manager. This handles tracking any ratelimits @@ -163,17 +163,17 @@ class LowLevelRestfulClient: #: #: You should not ever need to touch this implementation. #: - #: :type: :obj:`hikari.net.ratelimits.HTTPBucketRateLimiterManager` + #: :type: :obj:`~hikari.net.ratelimits.HTTPBucketRateLimiterManager` ratelimiter: ratelimits.HTTPBucketRateLimiterManager #: The custom SSL context to use. #: - #: :type: :obj:`ssl.SSLContext` + #: :type: :obj:`~ssl.SSLContext` ssl_context: typing.Optional[ssl.SSLContext] #: The HTTP request timeout to abort requests after. #: - #: :type: :obj:`float` + #: :type: :obj:`~float` timeout: typing.Optional[float] #: The bot token. This will be prefixed with either ``"Bearer "`` or @@ -182,10 +182,10 @@ class LowLevelRestfulClient: #: This value will be used for the ``Authorization`` HTTP header on each #: API request. #: - #: If no token is set, then the value will be :obj:`None`. In this case, + #: If no token is set, then the value will be :obj:`~None`. In this case, #: no ``Authorization`` header will be sent. #: - #: :type: :obj:`str`, optional + #: :type: :obj:`~str`, optional token: typing.Optional[str] #: The ``User-Agent`` header to send to Discord. @@ -197,20 +197,20 @@ class LowLevelRestfulClient: #: ``User-Agent`` header that conforms to specific requirements. #: Your mileage may vary (YMMV). #: - #: :type: :obj:`str` + #: :type: :obj:`~str` user_agent: str - #: If :obj:`True`, SSL certificates are verified for each request, and + #: If :obj:`~True`, SSL certificates are verified for each request, and #: invalid SSL certificates are rejected, causing an exception. If - #: :obj:`False`, then unrecognised certificates that may be illegitimate + #: :obj:`~False`, then unrecognised certificates that may be illegitimate #: are accepted and ignored. #: - #: :type: :obj:`bool` + #: :type: :obj:`~bool` verify_ssl: bool #: The API version number that is being used. #: - #: :type: :obj:`int` + #: :type: :obj:`~int` version: int def __init__( @@ -445,7 +445,7 @@ async def get_gateway(self) -> str: Returns ------- - :obj:`str` + :obj:`~str` A static URL to use to connect to the gateway with. Note @@ -460,8 +460,8 @@ async def get_gateway_bot(self) -> typing.Dict[str, typing.Any]: Returns ------- - :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ] - An object containing a ``url`` to connect to, an :obj:`int` number of shards recommended to use + :obj:`~typing.Dict` [ :obj:`~str`, :obj:`~typing.Any` ] + An object containing a ``url`` to connect to, an :obj:`~int` number of shards recommended to use for connecting, and a ``session_start_limit`` object. Note @@ -477,29 +477,29 @@ async def get_guild_audit_log( Parameters ---------- - guild_id : :obj:`str` + guild_id : :obj:`~str` The guild ID to look up. - user_id : :obj:`str` + user_id : :obj:`~str` If specified, the user ID to filter by. - action_type : :obj:`int` + action_type : :obj:`~int` If specified, the action type to look up. - limit : :obj:`int` + limit : :obj:`~int` If specified, the limit to apply to the number of records. Defaults to ``50``. Must be between ``1`` and ``100`` inclusive. - before : :obj:`str` + before : :obj:`~str` If specified, the ID of the entry that all retrieved entries will have occurred before. Returns ------- - :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ] + :obj:`~typing.Dict` [ :obj:`~str`, :obj:`~typing.Any` ] An audit log object. Raises ------ - :obj:`hikari.errors.ForbiddenHTTPError` + :obj:`~hikari.errors.ForbiddenHTTPError` If you lack the given permissions to view an audit log. - :obj:`hikari.errors.NotFoundHTTPError` + :obj:`~hikari.errors.NotFoundHTTPError` If the guild does not exist. """ query = {} @@ -515,19 +515,19 @@ async def get_channel(self, channel_id: str) -> typing.Dict[str, typing.Any]: Parameters ---------- - channel_id : :obj:`str` + channel_id : :obj:`~str` The channel ID to look up. Returns ------- - :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ] + :obj:`~typing.Dict` [ :obj:`~str`, :obj:`~typing.Any` ] The channel object that has been found. Raises ------ - :obj:`hikari.errors.ForbiddenHTTPError` + :obj:`~hikari.errors.ForbiddenHTTPError` If you don't have access to the channel. - :obj:`hikari.errors.NotFoundHTTPError` + :obj:`~hikari.errors.NotFoundHTTPError` If the channel does not exist. """ route = routes.CHANNEL.compile(self.GET, channel_id=channel_id) @@ -552,56 +552,56 @@ async def modify_channel( # lgtm [py/similar-function] Parameters ---------- - channel_id : :obj:`str` + channel_id : :obj:`~str` The channel ID to update. - name : :obj:`str` + name : :obj:`~str` If specified, the new name for the channel. This must be between ``2`` and ``100`` characters in length. - position : :obj:`int` + position : :obj:`~int` If specified, the position to change the channel to. - topic : :obj:`str` + topic : :obj:`~str` If specified, the topic to set. This is only applicable to text channels. This must be between ``0`` and ``1024`` characters in length. - nsfw : :obj:`bool` + nsfw : :obj:`~bool` If specified, whether the channel will be marked as NSFW. Only applicable to text channels. - rate_limit_per_user : :obj:`int` + rate_limit_per_user : :obj:`~int` If specified, the number of seconds the user has to wait before sending another message. This will not apply to bots, or to members with ``MANAGE_MESSAGES`` or ``MANAGE_CHANNEL`` permissions. This must be between ``0`` and ``21600`` seconds. - bitrate : :obj:`int` + bitrate : :obj:`~int` If specified, the bitrate in bits per second allowable for the channel. This only applies to voice channels and must be between ``8000`` and ``96000`` for normal servers or ``8000`` and ``128000`` for VIP servers. - user_limit : :obj:`int` + user_limit : :obj:`~int` If specified, the new max number of users to allow in a voice channel. This must be between ``0`` and ``99`` inclusive, where ``0`` implies no limit. - permission_overwrites : :obj:`typing.Sequence` [ :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ] ] + permission_overwrites : :obj:`~typing.Sequence` [ :obj:`~typing.Dict` [ :obj:`~str`, :obj:`~typing.Any` ] ] If specified, the new list of permission overwrites that are category specific to replace the existing overwrites with. - parent_id : :obj:`str`, optional + parent_id : :obj:`~str`, optional If specified, the new parent category ID to set for the channel., - pass :obj:`None` to unset. - reason : :obj:`str` + pass :obj:`~None` to unset. + reason : :obj:`~str` If specified, the audit log reason explaining why the operation was performed. Returns ------- - :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ] + :obj:`~typing.Dict` [ :obj:`~str`, :obj:`~typing.Any` ] The channel object that has been modified. Raises ------ - :obj:`hikari.errors.NotFoundHTTPError` + :obj:`~hikari.errors.NotFoundHTTPError` If the channel does not exist. - :obj:`hikari.errors.ForbiddenHTTPError` + :obj:`~hikari.errors.ForbiddenHTTPError` If you lack the permission to make the change. - :obj:`hikari.errors.BadRequestHTTPError` + :obj:`~hikari.errors.BadRequestHTTPError` If you provide incorrect options for the corresponding channel type (e.g. a ``bitrate`` for a text channel). """ @@ -623,20 +623,20 @@ async def delete_close_channel(self, channel_id: str) -> None: Parameters ---------- - channel_id : :obj:`str` + channel_id : :obj:`~str` The channel ID to delete, or direct message channel to close. Returns ------- - :obj:`None` + :obj:`~None` Nothing, unlike what the API specifies. This is done to maintain consistency with other calls of a similar nature in this API wrapper. Raises ------ - :obj:`hikari.errors.NotFoundHTTPError` + :obj:`~hikari.errors.NotFoundHTTPError` If the channel does not exist. - :obj:`hikari.errors.ForbiddenHTTPError` + :obj:`~hikari.errors.ForbiddenHTTPError` If you do not have permission to delete the channel. Warning @@ -653,47 +653,47 @@ async def get_channel_messages( Parameters ---------- - channel_id : :obj:`str` + channel_id : :obj:`~str` The ID of the channel to retrieve the messages from. - limit : :obj:`int` + limit : :obj:`~int` If specified, the number of messages to return. Must be between ``1`` and ``100`` inclusive.Defaults to ``50`` if unspecified. - after : :obj:`str` + after : :obj:`~str` A message ID. If specified, only return messages sent AFTER this message. - before : :obj:`str` + before : :obj:`~str` A message ID. If specified, only return messages sent BEFORE this message. - around : :obj:`str` + around : :obj:`~str` A message ID. If specified, only return messages sent AROUND and including (if it still exists) this message. Returns ------- - :obj:`typing.Sequence` [ :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ] ] + :obj:`~typing.Sequence` [ :obj:`~typing.Dict` [ :obj:`~str`, :obj:`~typing.Any` ] ] A list of message objects. Raises ------ - :obj:`hikari.errors.ForbiddenHTTPError` + :obj:`~hikari.errors.ForbiddenHTTPError` If you lack permission to read the channel. - :obj:`hikari.errors.BadRequestHTTPError` + :obj:`~hikari.errors.BadRequestHTTPError` If your query is malformed, has an invalid value for ``limit``, or contains more than one of ``after``, ``before`` and ``around``. - :obj:`hikari.errors.NotFoundHTTPError` + :obj:`~hikari.errors.NotFoundHTTPError` If the channel is not found, or the message provided for one of the filter arguments is not found. Note ---- If you are missing the ``VIEW_CHANNEL`` permission, you will receive a - :obj:`hikari.errors.ForbiddenHTTPError`. If you are instead missing + :obj:`~hikari.errors.ForbiddenHTTPError`. If you are instead missing the ``READ_MESSAGE_HISTORY`` permission, you will always receive zero results, and thus an empty list will be returned instead. Warning ------- You can only specify a maximum of one from ``before``, ``after``, and ``around``. - Specifying more than one will cause a :obj:`hikari.errors.BadRequestHTTPError` to be raised. + Specifying more than one will cause a :obj:`~hikari.errors.BadRequestHTTPError` to be raised. """ query = {} conversions.put_if_specified(query, "limit", limit) @@ -708,14 +708,14 @@ async def get_channel_message(self, channel_id: str, message_id: str) -> typing. Parameters ---------- - channel_id : :obj:`str` + channel_id : :obj:`~str` The ID of the channel to get the message from. - message_id : :obj:`str` + message_id : :obj:`~str` The ID of the message to retrieve. Returns ------- - :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ] + :obj:`~typing.Dict` [ :obj:`~str`, :obj:`~typing.Any` ] A message object. Note @@ -724,9 +724,9 @@ async def get_channel_message(self, channel_id: str, message_id: str) -> typing. Raises ------ - :obj:`hikari.errors.ForbiddenHTTPError` + :obj:`~hikari.errors.ForbiddenHTTPError` If you lack permission to see the message. - :obj:`hikari.errors.NotFoundHTTPError` + :obj:`~hikari.errors.NotFoundHTTPError` If the channel or message is not found. """ route = routes.CHANNEL_MESSAGE.compile(self.GET, channel_id=channel_id, message_id=message_id) @@ -747,37 +747,37 @@ async def create_message( Parameters ---------- - channel_id : :obj:`str` + channel_id : :obj:`~str` The ID of the channel to send to. - content : :obj:`str` + content : :obj:`~str` If specified, the message content to send with the message. - nonce : :obj:`str` + nonce : :obj:`~str` If specified, an optional ID to send for opportunistic message creation. This doesn't serve any real purpose for general use, and can usually be ignored. - tts : :obj:`bool` + tts : :obj:`~bool` If specified, whether the message will be sent as a TTS message. - files : :obj:`typing.Sequence` [ :obj:`typing.Tuple` [ :obj:`str`, :obj:`io.IOBase` ] ] + files : :obj:`~typing.Sequence` [ :obj:`~typing.Tuple` [ :obj:`~str`, :obj:`~io.IOBase` ] ] If specified, this should be a list of between ``1`` and ``5`` tuples. Each tuple should consist of the file name, and either - raw :obj:`bytes` or an :obj:`io.IOBase` derived object with + raw :obj:`~bytes` or an :obj:`~io.IOBase` derived object with a seek that points to a buffer containing said file. - embed : :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ] + embed : :obj:`~typing.Dict` [ :obj:`~str`, :obj:`~typing.Any` ] If specified, the embed to send with the message. - allowed_mentions : :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ] + allowed_mentions : :obj:`~typing.Dict` [ :obj:`~str`, :obj:`~typing.Any` ] If specified, the mentions to parse from the ``content``. If not specified, will parse all mentions from the ``content``. Returns ------- - :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ] + :obj:`~typing.Dict` [ :obj:`~str`, :obj:`~typing.Any` ] The created message object. Raises ------ - :obj:`hikari.errors.NotFoundHTTPError` + :obj:`~hikari.errors.NotFoundHTTPError` If the channel is not found. - :obj:`hikari.errors.BadRequestHTTPError` + :obj:`~hikari.errors.BadRequestHTTPError` This can be raised if the file is too large; if the embed exceeds the defined limits; if the message content is specified only and empty or greater than ``2000`` characters; if neither content, file @@ -785,7 +785,7 @@ async def create_message( fields in ``allowed_mentions``; if you specify to parse all users/roles mentions but also specify which users/roles to parse only. - :obj:`hikari.errors.ForbiddenHTTPError` + :obj:`~hikari.errors.ForbiddenHTTPError` If you lack permissions to send to this channel. """ form = aiohttp.FormData() @@ -814,24 +814,24 @@ async def create_reaction(self, channel_id: str, message_id: str, emoji: str) -> Parameters ---------- - channel_id : :obj:`str` + channel_id : :obj:`~str` The ID of the channel to add this reaction in. - message_id : :obj:`str` + message_id : :obj:`~str` The ID of the message to add the reaction in. - emoji : :obj:`str` + emoji : :obj:`~str` The emoji to add. This can either be a series of unicode characters making up a valid Discord emoji, or it can be a the url representation of a custom emoji ``<{emoji.name}:{emoji.id}>``. Raises ------ - :obj:`hikari.errors.ForbiddenHTTPError` + :obj:`~hikari.errors.ForbiddenHTTPError` If this is the first reaction using this specific emoji on this message and you lack the ``ADD_REACTIONS`` permission. If you lack ``READ_MESSAGE_HISTORY``, this may also raise this error. - :obj:`hikari.errors.NotFoundHTTPError` + :obj:`~hikari.errors.NotFoundHTTPError` If the channel or message is not found, or if the emoji is not found. - :obj:`hikari.errors.BadRequestHTTPError` + :obj:`~hikari.errors.BadRequestHTTPError` If the emoji is not valid, unknown, or formatted incorrectly. """ route = routes.OWN_REACTION.compile(self.PUT, channel_id=channel_id, message_id=message_id, emoji=emoji) @@ -842,20 +842,20 @@ async def delete_own_reaction(self, channel_id: str, message_id: str, emoji: str Parameters ---------- - channel_id : :obj:`str` + channel_id : :obj:`~str` The ID of the channel to get the message from. - message_id : :obj:`str` + message_id : :obj:`~str` The ID of the message to delete the reaction from. - emoji : :obj:`str` + emoji : :obj:`~str` The emoji to delete. This can either be a series of unicode characters making up a valid Discord emoji, or it can be a snowflake ID for a custom emoji. Raises ------ - :obj:`hikari.errors.ForbiddenHTTPError` + :obj:`~hikari.errors.ForbiddenHTTPError` If you lack permission to do this. - :obj:`hikari.errors.NotFoundHTTPError` + :obj:`~hikari.errors.NotFoundHTTPError` If the channel or message or emoji is not found. """ route = routes.OWN_REACTION.compile(self.DELETE, channel_id=channel_id, message_id=message_id, emoji=emoji) @@ -866,20 +866,20 @@ async def delete_all_reactions_for_emoji(self, channel_id: str, message_id: str, Parameters ---------- - channel_id : :obj:`str` + channel_id : :obj:`~str` The ID of the channel to get the message from. - message_id : :obj:`str` + message_id : :obj:`~str` The ID of the message to delete the reactions from. - emoji : :obj:`str` + emoji : :obj:`~str` The emoji to delete. This can either be a series of unicode characters making up a valid Discord emoji, or it can be a snowflake ID for a custom emoji. Raises ------ - :obj:`hikari.errors.NotFoundHTTPError` + :obj:`~hikari.errors.NotFoundHTTPError` If the channel or message or emoji or user is not found. - :obj:`hikari.errors.ForbiddenHTTPError` + :obj:`~hikari.errors.ForbiddenHTTPError` If you lack the ``MANAGE_MESSAGES`` permission, or are in DMs. """ route = routes.REACTION_EMOJI.compile(self.DELETE, channel_id=channel_id, message_id=message_id, emoji=emoji) @@ -890,22 +890,22 @@ async def delete_user_reaction(self, channel_id: str, message_id: str, emoji: st Parameters ---------- - channel_id : :obj:`str` + channel_id : :obj:`~str` The ID of the channel to get the message from. - message_id : :obj:`str` + message_id : :obj:`~str` The ID of the message to remove the reaction from. - emoji : :obj:`str` + emoji : :obj:`~str` The emoji to delete. This can either be a series of unicode characters making up a valid Discord emoji, or it can be a snowflake ID for a custom emoji. - user_id : :obj:`str` + user_id : :obj:`~str` The ID of the user who made the reaction that you wish to remove. Raises ------ - :obj:`hikari.errors.NotFoundHTTPError` + :obj:`~hikari.errors.NotFoundHTTPError` If the channel or message or emoji or user is not found. - :obj:`hikari.errors.ForbiddenHTTPError` + :obj:`~hikari.errors.ForbiddenHTTPError` If you lack the ``MANAGE_MESSAGES`` permission, or are in DMs. """ route = routes.REACTION_EMOJI_USER.compile( @@ -920,32 +920,32 @@ async def get_reactions( Parameters ---------- - channel_id : :obj:`str` + channel_id : :obj:`~str` The ID of the channel to get the message from. - message_id : :obj:`str` + message_id : :obj:`~str` The ID of the message to get the reactions from. - emoji : :obj:`str` + emoji : :obj:`~str` The emoji to get. This can either be a series of unicode characters making up a valid Discord emoji, or it can be a snowflake ID for a custom emoji. - after : :obj:`str` + after : :obj:`~str` If specified, the user ID. If specified, only users with a snowflake that is lexicographically greater than the value will be returned. - limit : :obj:`str` + limit : :obj:`~str` If specified, the limit of the number of values to return. Must be between ``1`` and ``100`` inclusive. If unspecified, defaults to ``25``. Returns ------- - :obj:`typing.Sequence` [ :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ] ] + :obj:`~typing.Sequence` [ :obj:`~typing.Dict` [ :obj:`~str`, :obj:`~typing.Any` ] ] A list of user objects. Raises ------ - :obj:`hikari.errors.ForbiddenHTTPError` + :obj:`~hikari.errors.ForbiddenHTTPError` If you lack access to the message. - :obj:`hikari.errors.NotFoundHTTPError` + :obj:`~hikari.errors.NotFoundHTTPError` If the channel or message is not found. """ query = {} @@ -959,16 +959,16 @@ async def delete_all_reactions(self, channel_id: str, message_id: str) -> None: Parameters ---------- - channel_id : :obj:`str` + channel_id : :obj:`~str` The ID of the channel to get the message from. - message_id : :obj:`str` + message_id : :obj:`~str` The ID of the message to remove all reactions from. Raises ------ - :obj:`hikari.errors.NotFoundHTTPError` + :obj:`~hikari.errors.NotFoundHTTPError` If the channel or message is not found. - :obj:`hikari.errors.ForbiddenHTTPError` + :obj:`~hikari.errors.ForbiddenHTTPError` If you lack the ``MANAGE_MESSAGES`` permission. """ route = routes.ALL_REACTIONS.compile(self.DELETE, channel_id=channel_id, message_id=message_id) @@ -988,38 +988,38 @@ async def edit_message( Parameters ---------- - channel_id : :obj:`str` + channel_id : :obj:`~str` The ID of the channel to get the message from. - message_id : :obj:`str` + message_id : :obj:`~str` The ID of the message to edit. - content : :obj:`str`, optional + content : :obj:`~str`, optional If specified, the string content to replace with in the message. - If :obj:`None`, the content will be removed from the message. - embed : :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ], optional + If :obj:`~None`, the content will be removed from the message. + embed : :obj:`~typing.Dict` [ :obj:`~str`, :obj:`~typing.Any` ], optional If specified, the embed to replace with in the message. - If :obj:`None`, the embed will be removed from the message. - flags : :obj:`int` + If :obj:`~None`, the embed will be removed from the message. + flags : :obj:`~int` If specified, the integer to replace the message's current flags. - allowed_mentions : :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ] + allowed_mentions : :obj:`~typing.Dict` [ :obj:`~str`, :obj:`~typing.Any` ] If specified, the mentions to parse from the ``content``. If not specified, will parse all mentions from the ``content``. Returns ------- - :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ] + :obj:`~typing.Dict` [ :obj:`~str`, :obj:`~typing.Any` ] The edited message object. Raises ------ - :obj:`hikari.errors.NotFoundHTTPError` + :obj:`~hikari.errors.NotFoundHTTPError` If the channel or message is not found. - :obj:`hikari.errors.BadRequestHTTPError` + :obj:`~hikari.errors.BadRequestHTTPError` This can be raised if the embed exceeds the defined limits; if the message content is specified only and empty or greater than ``2000`` characters; if neither content, file or embed are specified. parse only. - :obj:`hikari.errors.ForbiddenHTTPError` + :obj:`~hikari.errors.ForbiddenHTTPError` If you try to edit ``content`` or ``embed`` or ``allowed_mentions` on a message you did not author or try to edit the flags on a message you did not author without the ``MANAGE_MESSAGES`` @@ -1038,17 +1038,17 @@ async def delete_message(self, channel_id: str, message_id: str) -> None: Parameters ---------- - channel_id : :obj:`str` + channel_id : :obj:`~str` The ID of the channel to get the message from. - message_id : :obj:`str` + message_id : :obj:`~str` The ID of the message to delete. Raises ------ - :obj:`hikari.errors.ForbiddenHTTPError` + :obj:`~hikari.errors.ForbiddenHTTPError` If you did not author the message and are in a DM, or if you did not author the message and lack the ``MANAGE_MESSAGES`` permission in a guild channel. - :obj:`hikari.errors.NotFoundHTTPError` + :obj:`~hikari.errors.NotFoundHTTPError` If the channel or message is not found. """ route = routes.CHANNEL_MESSAGE.compile(self.DELETE, channel_id=channel_id, message_id=message_id) @@ -1059,18 +1059,18 @@ async def bulk_delete_messages(self, channel_id: str, messages: typing.Sequence[ Parameters ---------- - channel_id : :obj:`str` + channel_id : :obj:`~str` The ID of the channel to get the message from. - messages : :obj:`typing.Sequence` [ :obj:`str` ] + messages : :obj:`~typing.Sequence` [ :obj:`~str` ] A list of ``2-100`` message IDs to remove in the channel. Raises ------ - :obj:`hikari.errors.NotFoundHTTPError` + :obj:`~hikari.errors.NotFoundHTTPError` If the channel is not found. - :obj:`hikari.errors.ForbiddenHTTPError` + :obj:`~hikari.errors.ForbiddenHTTPError` If you lack the ``MANAGE_MESSAGES`` permission in the channel. - :obj:`hikari.errors.BadRequestHTTPError` + :obj:`~hikari.errors.BadRequestHTTPError` If any of the messages passed are older than ``2`` weeks in age or any duplicate message IDs are passed. @@ -1093,26 +1093,26 @@ async def edit_channel_permissions( Parameters ---------- - channel_id : :obj:`str` + channel_id : :obj:`~str` The ID of the channel to edit permissions for. - overwrite_id : :obj:`str` + overwrite_id : :obj:`~str` The overwrite ID to edit. - type_ : :obj:`str` + type_ : :obj:`~str` The type of overwrite. ``"member"`` if it is for a member, or ``"role"`` if it is for a role. - allow : :obj:`int` + allow : :obj:`~int` If specified, the bitwise value of all permissions to set to be allowed. - deny : :obj:`int` + deny : :obj:`~int` If specified, the bitwise value of all permissions to set to be denied. - reason : :obj:`str` + reason : :obj:`~str` If specified, the audit log reason explaining why the operation was performed. Raises ------ - :obj:`hikari.errors.NotFoundHTTPError` + :obj:`~hikari.errors.NotFoundHTTPError` If the target channel or overwrite doesn't exist. - :obj:`hikari.errors.ForbiddenHTTPError` + :obj:`~hikari.errors.ForbiddenHTTPError` If you lack permission to do this. """ payload = {"type": type_} @@ -1126,19 +1126,19 @@ async def get_channel_invites(self, channel_id: str) -> typing.Sequence[typing.D Parameters ---------- - channel_id : :obj:`str` + channel_id : :obj:`~str` The ID of the channel to get invites for. Returns ------- - :obj:`typing.Sequence` [ :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ] ] + :obj:`~typing.Sequence` [ :obj:`~typing.Dict` [ :obj:`~str`, :obj:`~typing.Any` ] ] A list of invite objects. Raises ------ - :obj:`hikari.errors.ForbiddenHTTPError` + :obj:`~hikari.errors.ForbiddenHTTPError` If you lack the ``MANAGE_CHANNELS`` permission. - :obj:`hikari.errors.NotFoundHTTPError` + :obj:`~hikari.errors.NotFoundHTTPError` If the channel does not exist. """ route = routes.CHANNEL_INVITES.compile(self.GET, channel_id=channel_id) @@ -1160,40 +1160,40 @@ async def create_channel_invite( Parameters ---------- - channel_id : :obj:`str` + channel_id : :obj:`~str` The ID of the channel to create the invite for. - max_age : :obj:`int` + max_age : :obj:`~int` If specified, the max age of the invite in seconds, defaults to ``86400`` (``24`` hours). Set to ``0`` to never expire. - max_uses : :obj:`int` + max_uses : :obj:`~int` If specified, the max number of uses this invite can have, or ``0`` for unlimited (as per the default). - temporary : :obj:`bool` + temporary : :obj:`~bool` If specified, whether to grant temporary membership, meaning the user is kicked when their session ends unless they are given a role. - unique : :obj:`bool` + unique : :obj:`~bool` If specified, whether to try to reuse a similar invite. - target_user : :obj:`str` + target_user : :obj:`~str` If specified, the ID of the user this invite should target. - target_user_type : :obj:`int` + target_user_type : :obj:`~int` If specified, the type of target for this invite. - reason : :obj:`str` + reason : :obj:`~str` If specified, the audit log reason explaining why the operation was performed. Returns ------- - :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ] + :obj:`~typing.Dict` [ :obj:`~str`, :obj:`~typing.Any` ] An invite object. Raises ------ - :obj:`hikari.errors.ForbiddenHTTPError` + :obj:`~hikari.errors.ForbiddenHTTPError` If you lack the ``CREATE_INSTANT_MESSAGES`` permission. - :obj:`hikari.errors.NotFoundHTTPError` + :obj:`~hikari.errors.NotFoundHTTPError` If the channel does not exist. - :obj:`hikari.errors.BadRequestHTTPError` + :obj:`~hikari.errors.BadRequestHTTPError` If the arguments provided are not valid (e.g. negative age, etc). """ payload = {} @@ -1211,16 +1211,16 @@ async def delete_channel_permission(self, channel_id: str, overwrite_id: str) -> Parameters ---------- - channel_id : :obj:`str` + channel_id : :obj:`~str` The ID of the channel to delete the overwrite from. - overwrite_id : :obj:`str` + overwrite_id : :obj:`~str` The ID of the overwrite to remove. Raises ------ - :obj:`hikari.errors.NotFoundHTTPError` + :obj:`~hikari.errors.NotFoundHTTPError` If the overwrite or channel do not exist. - :obj:`hikari.errors.ForbiddenHTTPError` + :obj:`~hikari.errors.ForbiddenHTTPError` If you lack the ``MANAGE_ROLES`` permission for that channel. """ route = routes.CHANNEL_PERMISSIONS.compile(self.DELETE, channel_id=channel_id, overwrite_id=overwrite_id) @@ -1231,14 +1231,14 @@ async def trigger_typing_indicator(self, channel_id: str) -> None: Parameters ---------- - channel_id : :obj:`str` + channel_id : :obj:`~str` The ID of the channel to appear to be typing in. Raises ------ - :obj:`hikari.errors.NotFoundHTTPError` + :obj:`~hikari.errors.NotFoundHTTPError` If the channel is not found. - :obj:`hikari.errors.ForbiddenHTTPError` + :obj:`~hikari.errors.ForbiddenHTTPError` If you are not able to type in the channel. """ route = routes.CHANNEL_TYPING.compile(self.POST, channel_id=channel_id) @@ -1249,19 +1249,19 @@ async def get_pinned_messages(self, channel_id: str) -> typing.Sequence[typing.D Parameters ---------- - channel_id : :obj:`str` + channel_id : :obj:`~str` The channel ID to get messages from. Returns ------- - :obj:`typing.Sequence` [ :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ] ] + :obj:`~typing.Sequence` [ :obj:`~typing.Dict` [ :obj:`~str`, :obj:`~typing.Any` ] ] A list of messages. Raises ------ - :obj:`hikari.errors.NotFoundHTTPError` + :obj:`~hikari.errors.NotFoundHTTPError` If the channel is not found. - :obj:`hikari.errors.ForbiddenHTTPError` + :obj:`~hikari.errors.ForbiddenHTTPError` If you are not able to see the channel. Note @@ -1277,16 +1277,16 @@ async def add_pinned_channel_message(self, channel_id: str, message_id: str) -> Parameters ---------- - channel_id : :obj:`str` + channel_id : :obj:`~str` The ID of the channel to pin a message to. - message_id : :obj:`str` + message_id : :obj:`~str` The ID of the message to pin. Raises ------ - :obj:`hikari.errors.ForbiddenHTTPError` + :obj:`~hikari.errors.ForbiddenHTTPError` If you lack the ``MANAGE_MESSAGES`` permission. - :obj:`hikari.errors.NotFoundHTTPError` + :obj:`~hikari.errors.NotFoundHTTPError` If the message or channel do not exist. """ route = routes.CHANNEL_PINS.compile(self.PUT, channel_id=channel_id, message_id=message_id) @@ -1299,16 +1299,16 @@ async def delete_pinned_channel_message(self, channel_id: str, message_id: str) Parameters ---------- - channel_id : :obj:`str` + channel_id : :obj:`~str` The ID of the channel to remove a pin from. - message_id : :obj:`str` + message_id : :obj:`~str` The ID of the message to unpin. Raises ------ - :obj:`hikari.errors.ForbiddenHTTPError` + :obj:`~hikari.errors.ForbiddenHTTPError` If you lack the ``MANAGE_MESSAGES`` permission. - :obj:`hikari.errors.NotFoundHTTPError` + :obj:`~hikari.errors.NotFoundHTTPError` If the message or channel do not exist. """ route = routes.CHANNEL_PIN.compile(self.DELETE, channel_id=channel_id, message_id=message_id) @@ -1319,19 +1319,19 @@ async def list_guild_emojis(self, guild_id: str) -> typing.Sequence[typing.Dict[ Parameters ---------- - guild_id : :obj:`str` + guild_id : :obj:`~str` The ID of the guild to get the emojis for. Returns ------- - :obj:`typing.Sequence` [ :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ] ] + :obj:`~typing.Sequence` [ :obj:`~typing.Dict` [ :obj:`~str`, :obj:`~typing.Any` ] ] A list of emoji objects. Raises ------ - :obj:`hikari.errors.NotFoundHTTPError` + :obj:`~hikari.errors.NotFoundHTTPError` If the guild is not found. - :obj:`hikari.errors.ForbiddenHTTPError` + :obj:`~hikari.errors.ForbiddenHTTPError` If you aren't a member of the guild. """ route = routes.GUILD_EMOJIS.compile(self.GET, guild_id=guild_id) @@ -1342,21 +1342,21 @@ async def get_guild_emoji(self, guild_id: str, emoji_id: str) -> typing.Dict[str Parameters ---------- - guild_id : :obj:`str` + guild_id : :obj:`~str` The ID of the guild to get the emoji from. - emoji_id : :obj:`str` + emoji_id : :obj:`~str` The ID of the emoji to get. Returns ------- - :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ] + :obj:`~typing.Dict` [ :obj:`~str`, :obj:`~typing.Any` ] An emoji object. Raises ------ - :obj:`hikari.errors.NotFoundHTTPError` + :obj:`~hikari.errors.NotFoundHTTPError` If either the guild or the emoji aren't found. - :obj:`hikari.errors.ForbiddenHTTPError` + :obj:`~hikari.errors.ForbiddenHTTPError` If you aren't a member of said guild. """ route = routes.GUILD_EMOJI.compile(self.GET, guild_id=guild_id, emoji_id=emoji_id) @@ -1369,33 +1369,33 @@ async def create_guild_emoji( Parameters ---------- - guild_id : :obj:`str` + guild_id : :obj:`~str` The ID of the guild to create the emoji in. - name : :obj:`str` + name : :obj:`~str` The new emoji's name. - image : :obj:`bytes` + image : :obj:`~bytes` The ``128x128`` image in bytes form. - roles : :obj:`typing.Sequence` [ :obj:`str` ] + roles : :obj:`~typing.Sequence` [ :obj:`~str` ] If specified, a list of roles for which the emoji will be whitelisted. If empty, all roles are whitelisted. - reason : :obj:`str` + reason : :obj:`~str` If specified, the audit log reason explaining why the operation was performed. Returns ------- - :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ] + :obj:`~typing.Dict` [ :obj:`~str`, :obj:`~typing.Any` ] The newly created emoji object. Raises ------ - :obj:`ValueError` - If ``image`` is :obj:`None`. - :obj:`hikari.errors.NotFoundHTTPError` + :obj:`~ValueError` + If ``image`` is :obj:`~None`. + :obj:`~hikari.errors.NotFoundHTTPError` If the guild is not found. - :obj:`hikari.errors.ForbiddenHTTPError` + :obj:`~hikari.errors.ForbiddenHTTPError` If you either lack the ``MANAGE_EMOJIS`` permission or aren't a member of said guild. - :obj:`hikari.errors.BadRequestHTTPError` + :obj:`~hikari.errors.BadRequestHTTPError` If you attempt to upload an image larger than ``256kb``, an empty image or an invalid image format. """ assertions.assert_not_none(image, "image must be a valid image") @@ -1414,30 +1414,30 @@ async def modify_guild_emoji( Parameters ---------- - guild_id : :obj:`str` + guild_id : :obj:`~str` The ID of the guild to which the emoji to update belongs to. - emoji_id : :obj:`str` + emoji_id : :obj:`~str` The ID of the emoji to update. - name : :obj:`str` + name : :obj:`~str` If specified, a new emoji name string. Keep unspecified to keep the name the same. - roles : :obj:`typing.Sequence` [ :obj:`str` ] + roles : :obj:`~typing.Sequence` [ :obj:`~str` ] If specified, a list of IDs for the new whitelisted roles. Set to an empty list to whitelist all roles. Keep unspecified to leave the same roles already set. - reason : :obj:`str` + reason : :obj:`~str` If specified, the audit log reason explaining why the operation was performed. Returns ------- - :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ] + :obj:`~typing.Dict` [ :obj:`~str`, :obj:`~typing.Any` ] The updated emoji object. Raises ------ - :obj:`hikari.errors.NotFoundHTTPError` + :obj:`~hikari.errors.NotFoundHTTPError` If either the guild or the emoji aren't found. - :obj:`hikari.errors.ForbiddenHTTPError` + :obj:`~hikari.errors.ForbiddenHTTPError` If you either lack the ``MANAGE_EMOJIS`` permission or are not a member of the given guild. """ payload = {} @@ -1451,16 +1451,16 @@ async def delete_guild_emoji(self, guild_id: str, emoji_id: str) -> None: Parameters ---------- - guild_id : :obj:`str` + guild_id : :obj:`~str` The ID of the guild to delete the emoji from. - emoji_id : :obj:`str` + emoji_id : :obj:`~str` The ID of the emoji to be deleted. Raises ------ - :obj:`hikari.errors.NotFoundHTTPError` + :obj:`~hikari.errors.NotFoundHTTPError` If either the guild or the emoji aren't found. - :obj:`hikari.errors.ForbiddenHTTPError` + :obj:`~hikari.errors.ForbiddenHTTPError` If you either lack the ``MANAGE_EMOJIS`` permission or aren't a member of said guild. """ route = routes.GUILD_EMOJI.compile(self.DELETE, guild_id=guild_id, emoji_id=emoji_id) @@ -1486,35 +1486,35 @@ async def create_guild( Parameters ---------- - name : :obj:`str` + name : :obj:`~str` The name string for the new guild (``2-100`` characters). - region : :obj:`str` + region : :obj:`~str` If specified, the voice region ID for new guild. You can use :meth:`list_voice_regions` to see which region IDs are available. - icon : :obj:`bytes` + icon : :obj:`~bytes` If specified, the guild icon image in bytes form. - verification_level : :obj:`int` + verification_level : :obj:`~int` If specified, the verification level integer (``0-5``). - default_message_notifications : :obj:`int` + default_message_notifications : :obj:`~int` If specified, the default notification level integer (``0-1``). - explicit_content_filter : :obj:`int` + explicit_content_filter : :obj:`~int` If specified, the explicit content filter integer (``0-2``). - roles : :obj:`typing.Sequence` [ :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ] ] + roles : :obj:`~typing.Sequence` [ :obj:`~typing.Dict` [ :obj:`~str`, :obj:`~typing.Any` ] ] If specified, an array of role objects to be created alongside the guild. First element changes the ``@everyone`` role. - channels : :obj:`typing.Sequence` [ :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ] ] + channels : :obj:`~typing.Sequence` [ :obj:`~typing.Dict` [ :obj:`~str`, :obj:`~typing.Any` ] ] If specified, an array of channel objects to be created alongside the guild. Returns ------- - :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ] + :obj:`~typing.Dict` [ :obj:`~str`, :obj:`~typing.Any` ] The newly created guild object. Raises ------ - :obj:`hikari.errors.ForbiddenHTTPError` + :obj:`~hikari.errors.ForbiddenHTTPError` If you are on ``10`` or more guilds. - :obj:`hikari.errors.BadRequestHTTPError` + :obj:`~hikari.errors.BadRequestHTTPError` If you provide unsupported fields like ``parent_id`` in channel objects. """ payload = {"name": name} @@ -1533,19 +1533,19 @@ async def get_guild(self, guild_id: str) -> typing.Dict[str, typing.Any]: Parameters ---------- - guild_id : :obj:`str` + guild_id : :obj:`~str` The ID of the guild to get. Returns ------- - :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ] + :obj:`~typing.Dict` [ :obj:`~str`, :obj:`~typing.Any` ] The requested guild object. Raises ------ - :obj:`hikari.errors.NotFoundHTTPError` + :obj:`~hikari.errors.NotFoundHTTPError` If the guild is not found. - :obj:`hikari.errors.ForbiddenHTTPError` + :obj:`~hikari.errors.ForbiddenHTTPError` If you don't have access to the guild. """ route = routes.GUILD.compile(self.GET, guild_id=guild_id) @@ -1556,12 +1556,12 @@ async def get_guild_preview(self, guild_id: str) -> typing.Dict[str, typing.Any] Parameters ---------- - guild_id : :obj:`str` + guild_id : :obj:`~str` The ID of the guild to get the preview object of. Returns ------- - :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ] + :obj:`~typing.Dict` [ :obj:`~str`, :obj:`~typing.Any` ] The requested guild preview object. Note @@ -1571,7 +1571,7 @@ async def get_guild_preview(self, guild_id: str) -> typing.Dict[str, typing.Any] Raises ------ - :obj:`hikari.errors.NotFoundHTTPError` + :obj:`~hikari.errors.NotFoundHTTPError` If the guild is not found or it isn't ``PUBLIC``. """ route = routes.GUILD_PREVIEW.compile(self.GET, guild_id=guild_id) @@ -1599,45 +1599,45 @@ async def modify_guild( # lgtm [py/similar-function] Parameters ---------- - guild_id : :obj:`str` + guild_id : :obj:`~str` The ID of the guild to be edited. - name : :obj:`str` + name : :obj:`~str` If specified, the new name string for the guild (``2-100`` characters). - region : :obj:`str` + region : :obj:`~str` If specified, the new voice region ID for guild. You can use :meth:`list_voice_regions` to see which region IDs are available. - verification_level : :obj:`int` + verification_level : :obj:`~int` If specified, the new verification level integer (``0-5``). - default_message_notifications : :obj:`int` + default_message_notifications : :obj:`~int` If specified, the new default notification level integer (``0-1``). - explicit_content_filter : :obj:`int` + explicit_content_filter : :obj:`~int` If specified, the new explicit content filter integer (``0-2``). - afk_channel_id : :obj:`str` + afk_channel_id : :obj:`~str` If specified, the new ID for the AFK voice channel. - afk_timeout : :obj:`int` + afk_timeout : :obj:`~int` If specified, the new AFK timeout period in seconds - icon : :obj:`bytes` + icon : :obj:`~bytes` If specified, the new guild icon image in bytes form. - owner_id : :obj:`str` + owner_id : :obj:`~str` If specified, the new ID of the new guild owner. - splash : :obj:`bytes` + splash : :obj:`~bytes` If specified, the new new splash image in bytes form. - system_channel_id : :obj:`str` + system_channel_id : :obj:`~str` If specified, the new ID of the new system channel. - reason : :obj:`str` + reason : :obj:`~str` If specified, the audit log reason explaining why the operation was performed. Returns ------- - :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ] + :obj:`~typing.Dict` [ :obj:`~str`, :obj:`~typing.Any` ] The edited guild object. Raises ------ - :obj:`hikari.errors.NotFoundHTTPError` + :obj:`~hikari.errors.NotFoundHTTPError` If the guild is not found. - :obj:`hikari.errors.ForbiddenHTTPError` + :obj:`~hikari.errors.ForbiddenHTTPError` If you lack the ``MANAGE_GUILD`` permission or are not in the guild. """ payload = {} @@ -1664,14 +1664,14 @@ async def delete_guild(self, guild_id: str) -> None: Parameters ---------- - guild_id : :obj:`str` + guild_id : :obj:`~str` The ID of the guild to be deleted. Raises ------ - :obj:`hikari.errors.NotFoundHTTPError` + :obj:`~hikari.errors.NotFoundHTTPError` If the guild is not found. - :obj:`hikari.errors.ForbiddenHTTPError` + :obj:`~hikari.errors.ForbiddenHTTPError` If you are not the guild owner. """ route = routes.GUILD.compile(self.DELETE, guild_id=guild_id) @@ -1682,19 +1682,19 @@ async def list_guild_channels(self, guild_id: str) -> typing.Sequence[typing.Dic Parameters ---------- - guild_id : :obj:`str` + guild_id : :obj:`~str` The ID of the guild to get the channels from. Returns ------- - :obj:`typing.Sequence` [ :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ] ] + :obj:`~typing.Sequence` [ :obj:`~typing.Dict` [ :obj:`~str`, :obj:`~typing.Any` ] ] A list of channel objects. Raises ------ - :obj:`hikari.errors.NotFoundHTTPError` + :obj:`~hikari.errors.NotFoundHTTPError` If the guild is not found. - :obj:`hikari.errors.ForbiddenHTTPError` + :obj:`~hikari.errors.ForbiddenHTTPError` If you are not in the guild. """ route = routes.GUILD_CHANNELS.compile(self.GET, guild_id=guild_id) @@ -1720,57 +1720,57 @@ async def create_guild_channel( Parameters ---------- - guild_id : :obj:`str` + guild_id : :obj:`~str` The ID of the guild to create the channel in. - name : :obj:`str` + name : :obj:`~str` If specified, the name for the channel.This must be between ``2`` and ``100`` characters in length. - type_: :obj:`int` + type_: :obj:`~int` If specified, the channel type integer (``0-6``). - position : :obj:`int` + position : :obj:`~int` If specified, the position to change the channel to. - topic : :obj:`str` + topic : :obj:`~str` If specified, the topic to set. This is only applicable to text channels. This must be between ``0`` and ``1024`` characters in length. - nsfw : :obj:`bool` + nsfw : :obj:`~bool` If specified, whether the channel will be marked as NSFW. Only applicable to text channels. - rate_limit_per_user : :obj:`int` + rate_limit_per_user : :obj:`~int` If specified, the number of seconds the user has to wait before sending another message. This will not apply to bots, or to members with ``MANAGE_MESSAGES`` or ``MANAGE_CHANNEL`` permissions. This must be between ``0`` and ``21600`` seconds. - bitrate : :obj:`int` + bitrate : :obj:`~int` If specified, the bitrate in bits per second allowable for the channel. This only applies to voice channels and must be between ``8000`` and ``96000`` for normal servers or ``8000`` and ``128000`` for VIP servers. - user_limit : :obj:`int` + user_limit : :obj:`~int` If specified, the max number of users to allow in a voice channel. This must be between ``0`` and ``99`` inclusive, where ``0`` implies no limit. - permission_overwrites : :obj:`typing.Sequence` [ :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ] ] + permission_overwrites : :obj:`~typing.Sequence` [ :obj:`~typing.Dict` [ :obj:`~str`, :obj:`~typing.Any` ] ] If specified, the list of permission overwrites that are category specific to replace the existing overwrites with. - parent_id : :obj:`str` + parent_id : :obj:`~str` If specified, the parent category ID to set for the channel. - reason : :obj:`str` + reason : :obj:`~str` If specified, the audit log reason explaining why the operation was performed. Returns ------- - :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ] + :obj:`~typing.Dict` [ :obj:`~str`, :obj:`~typing.Any` ] The newly created channel object. Raises ------ - :obj:`hikari.errors.NotFoundHTTPError` + :obj:`~hikari.errors.NotFoundHTTPError` If the guild is not found. - :obj:`hikari.errors.ForbiddenHTTPError` + :obj:`~hikari.errors.ForbiddenHTTPError` If you lack the ``MANAGE_CHANNEL`` permission or are not in the guild. - :obj:`hikari.errors.BadRequestHTTPError` + :obj:`~hikari.errors.BadRequestHTTPError` If you provide incorrect options for the corresponding channel type (e.g. a ``bitrate`` for a text channel). """ @@ -1795,22 +1795,22 @@ async def modify_guild_channel_positions( Parameters ---------- - guild_id : :obj:`str` + guild_id : :obj:`~str` The ID of the guild in which to edit the channels. - channel : :obj:`typing.Tuple` [ :obj:`str`, :obj:`int` ] + channel : :obj:`~typing.Tuple` [ :obj:`~str`, :obj:`~int` ] The first channel to change the position of. This is a tuple of the channel ID and the integer position. - *channels : :obj:`typing.Tuple` [ :obj:`str`, :obj:`int` ] + *channels : :obj:`~typing.Tuple` [ :obj:`~str`, :obj:`~int` ] Optional additional channels to change the position of. These must be tuples of the channel ID and the integer positions to change to. Raises ------ - :obj:`hikari.errors.NotFoundHTTPError` + :obj:`~hikari.errors.NotFoundHTTPError` If either the guild or any of the channels aren't found. - :obj:`hikari.errors.ForbiddenHTTPError` + :obj:`~hikari.errors.ForbiddenHTTPError` If you either lack the ``MANAGE_CHANNELS`` permission or are not a member of said guild or are not in the guild. - :obj:`hikari.errors.BadRequestHTTPError` + :obj:`~hikari.errors.BadRequestHTTPError` If you provide anything other than the ``id`` and ``position`` fields for the channels. """ payload = [{"id": ch[0], "position": ch[1]} for ch in (channel, *channels)] @@ -1822,21 +1822,21 @@ async def get_guild_member(self, guild_id: str, user_id: str) -> typing.Dict[str Parameters ---------- - guild_id : :obj:`str` + guild_id : :obj:`~str` The ID of the guild to get the member from. - user_id : :obj:`str` + user_id : :obj:`~str` The ID of the member to get. Returns ------- - :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ] + :obj:`~typing.Dict` [ :obj:`~str`, :obj:`~typing.Any` ] The requested member object. Raises ------ - :obj:`hikari.errors.NotFoundHTTPError` + :obj:`~hikari.errors.NotFoundHTTPError` If either the guild or the member aren't found. - :obj:`hikari.errors.ForbiddenHTTPError` + :obj:`~hikari.errors.ForbiddenHTTPError` If you don't have access to the target guild. """ route = routes.GUILD_MEMBER.compile(self.GET, guild_id=guild_id, user_id=user_id) @@ -1849,12 +1849,12 @@ async def list_guild_members( Parameters ---------- - guild_id : :obj:`str` + guild_id : :obj:`~str` The ID of the guild to get the members from. - limit : :obj:`int` + limit : :obj:`~int` If specified, the maximum number of members to return. This has to be between ``1`` and ``1000`` inclusive. - after : :obj:`str` + after : :obj:`~str` If specified, the highest ID in the previous page. This is used for retrieving more than ``1000`` members in a server using consecutive requests. @@ -1876,16 +1876,16 @@ async def list_guild_members( Returns ------- - :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ] + :obj:`~typing.Dict` [ :obj:`~str`, :obj:`~typing.Any` ] A list of member objects. Raises ------ - :obj:`hikari.errors.NotFoundHTTPError` + :obj:`~hikari.errors.NotFoundHTTPError` If the guild is not found. - :obj:`hikari.errors.ForbiddenHTTPError` + :obj:`~hikari.errors.ForbiddenHTTPError` If you are not in the guild. - :obj:`hikari.errors.BadRequestHTTPError` + :obj:`~hikari.errors.BadRequestHTTPError` If you provide invalid values for the ``limit`` or `after`` fields. """ query = {} @@ -1910,38 +1910,38 @@ async def modify_guild_member( # lgtm [py/similar-function] Parameters ---------- - guild_id : :obj:`str` + guild_id : :obj:`~str` The ID of the guild to edit the member from. - user_id : :obj:`str` + user_id : :obj:`~str` The ID of the member to edit. - nick : :obj:`str`, optional - If specified, the new nickname string. Setting it to :obj:`None` + nick : :obj:`~str`, optional + If specified, the new nickname string. Setting it to :obj:`~None` explicitly will clear the nickname. - roles : :obj:`typing.Sequence` [ :obj:`str` ] + roles : :obj:`~typing.Sequence` [ :obj:`~str` ] If specified, a list of role IDs the member should have. - mute : :obj:`bool` + mute : :obj:`~bool` If specified, whether the user should be muted in the voice channel or not. - deaf : :obj:`bool` + deaf : :obj:`~bool` If specified, whether the user should be deafen in the voice channel or not. - channel_id : :obj:`str` + channel_id : :obj:`~str` If specified, the ID of the channel to move the member to. Setting - it to :obj:`None` explicitly will disconnect the user. - reason : :obj:`str` + it to :obj:`~None` explicitly will disconnect the user. + reason : :obj:`~str` If specified, the audit log reason explaining why the operation was performed. Raises ------ - :obj:`hikari.errors.NotFoundHTTPError` + :obj:`~hikari.errors.NotFoundHTTPError` If either the guild, user, channel or any of the roles aren't found. - :obj:`hikari.errors.ForbiddenHTTPError` + :obj:`~hikari.errors.ForbiddenHTTPError` If you lack any of the applicable permissions (``MANAGE_NICKNAMES``, ``MANAGE_ROLES``, ``MUTE_MEMBERS``, ``DEAFEN_MEMBERS`` or ``MOVE_MEMBERS``). Note that to move a member you must also have permission to connect to the end channel. This will also be raised if you're not in the guild. - :obj:`hikari.errors.BadRequestHTTPError` + :obj:`~hikari.errors.BadRequestHTTPError` If you pass ```mute``, ``deaf`` or ``channel_id`` while the member is not connected to a voice channel. """ payload = {} @@ -1958,21 +1958,21 @@ async def modify_current_user_nick(self, guild_id: str, nick: typing.Optional[st Parameters ---------- - guild_id : :obj:`str` + guild_id : :obj:`~str` The ID of the guild you want to change the nick on. - nick : :obj:`str`, optional + nick : :obj:`~str`, optional The new nick string. Setting this to `None` clears the nickname. - reason : :obj:`str` + reason : :obj:`~str` If specified, the audit log reason explaining why the operation was performed. Raises ------ - :obj:`hikari.errors.NotFoundHTTPError` + :obj:`~hikari.errors.NotFoundHTTPError` If the guild is not found. - :obj:`hikari.errors.ForbiddenHTTPError` + :obj:`~hikari.errors.ForbiddenHTTPError` If you lack the ``CHANGE_NICKNAME`` permission or are not in the guild. - :obj:`hikari.errors.BadRequestHTTPError` + :obj:`~hikari.errors.BadRequestHTTPError` If you provide a disallowed nickname, one that is too long, or one that is empty. """ payload = {"nick": nick} @@ -1984,21 +1984,21 @@ async def add_guild_member_role(self, guild_id: str, user_id: str, role_id: str, Parameters ---------- - guild_id : :obj:`str` + guild_id : :obj:`~str` The ID of the guild the member belongs to. - user_id : :obj:`str` + user_id : :obj:`~str` The ID of the member you want to add the role to. - role_id : :obj:`str` + role_id : :obj:`~str` The ID of the role you want to add. - reason : :obj:`str` + reason : :obj:`~str` If specified, the audit log reason explaining why the operation was performed. Raises ------ - :obj:`hikari.errors.NotFoundHTTPError` + :obj:`~hikari.errors.NotFoundHTTPError` If either the guild, member or role aren't found. - :obj:`hikari.errors.ForbiddenHTTPError` + :obj:`~hikari.errors.ForbiddenHTTPError` If you lack the ``MANAGE_ROLES`` permission or are not in the guild. """ route = routes.GUILD_MEMBER_ROLE.compile(self.PUT, guild_id=guild_id, user_id=user_id, role_id=role_id) @@ -2009,21 +2009,21 @@ async def remove_guild_member_role(self, guild_id: str, user_id: str, role_id: s Parameters ---------- - guild_id : :obj:`str` + guild_id : :obj:`~str` The ID of the guild the member belongs to. - user_id : :obj:`str` + user_id : :obj:`~str` The ID of the member you want to remove the role from. - role_id : :obj:`str` + role_id : :obj:`~str` The ID of the role you want to remove. - reason : :obj:`str` + reason : :obj:`~str` If specified, the audit log reason explaining why the operation was performed. Raises ------ - :obj:`hikari.errors.NotFoundHTTPError` + :obj:`~hikari.errors.NotFoundHTTPError` If either the guild, member or role aren't found. - :obj:`hikari.errors.ForbiddenHTTPError` + :obj:`~hikari.errors.ForbiddenHTTPError` If you lack the ``MANAGE_ROLES`` permission or are not in the guild. """ route = routes.GUILD_MEMBER_ROLE.compile(self.DELETE, guild_id=guild_id, user_id=user_id, role_id=role_id) @@ -2034,19 +2034,19 @@ async def remove_guild_member(self, guild_id: str, user_id: str, *, reason: str Parameters ---------- - guild_id: :obj:`str` + guild_id: :obj:`~str` The ID of the guild the member belongs to. - user_id: :obj:`str` + user_id: :obj:`~str` The ID of the member you want to kick. - reason : :obj:`str` + reason : :obj:`~str` If specified, the audit log reason explaining why the operation was performed. Raises ------ - :obj:`hikari.errors.NotFoundHTTPError` + :obj:`~hikari.errors.NotFoundHTTPError` If either the guild or member aren't found. - :obj:`hikari.errors.ForbiddenHTTPError` + :obj:`~hikari.errors.ForbiddenHTTPError` If you lack the ``KICK_MEMBERS`` permission or are not in the guild. """ route = routes.GUILD_MEMBER.compile(self.DELETE, guild_id=guild_id, user_id=user_id) @@ -2057,19 +2057,19 @@ async def get_guild_bans(self, guild_id: str) -> typing.Sequence[typing.Dict[str Parameters ---------- - guild_id : :obj:`str` + guild_id : :obj:`~str` The ID of the guild you want to get the bans from. Returns ------- - :obj:`typing.Sequence` [ :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ] ] + :obj:`~typing.Sequence` [ :obj:`~typing.Dict` [ :obj:`~str`, :obj:`~typing.Any` ] ] A list of ban objects. Raises ------ - :obj:`hikari.errors.NotFoundHTTPError` + :obj:`~hikari.errors.NotFoundHTTPError` If the guild is not found. - :obj:`hikari.errors.ForbiddenHTTPError` + :obj:`~hikari.errors.ForbiddenHTTPError` If you lack the ``BAN_MEMBERS`` permission or are not in the guild. """ route = routes.GUILD_BANS.compile(self.GET, guild_id=guild_id) @@ -2080,21 +2080,21 @@ async def get_guild_ban(self, guild_id: str, user_id: str) -> typing.Dict[str, t Parameters ---------- - guild_id : :obj:`str` + guild_id : :obj:`~str` The ID of the guild you want to get the ban from. - user_id : :obj:`str` + user_id : :obj:`~str` The ID of the user to get the ban information for. Returns ------- - :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ] + :obj:`~typing.Dict` [ :obj:`~str`, :obj:`~typing.Any` ] A ban object for the requested user. Raises ------ - :obj:`hikari.errors.NotFoundHTTPError` + :obj:`~hikari.errors.NotFoundHTTPError` If either the guild or the user aren't found, or if the user is not banned. - :obj:`hikari.errors.ForbiddenHTTPError` + :obj:`~hikari.errors.ForbiddenHTTPError` If you lack the ``BAN_MEMBERS`` permission or are not in the guild. """ route = routes.GUILD_BAN.compile(self.GET, guild_id=guild_id, user_id=user_id) @@ -2107,22 +2107,22 @@ async def create_guild_ban( Parameters ---------- - guild_id : :obj:`str` + guild_id : :obj:`~str` The ID of the guild the member belongs to. - user_id : :obj:`str` + user_id : :obj:`~str` The ID of the member you want to ban. - delete_message_days : :obj:`str` + delete_message_days : :obj:`~str` If specified, how many days of messages from the user should be removed. - reason : :obj:`str` + reason : :obj:`~str` If specified, the audit log reason explaining why the operation was performed. Raises ------ - :obj:`hikari.errors.NotFoundHTTPError` + :obj:`~hikari.errors.NotFoundHTTPError` If either the guild or member aren't found. - :obj:`hikari.errors.ForbiddenHTTPError` + :obj:`~hikari.errors.ForbiddenHTTPError` If you lack the ``BAN_MEMBERS`` permission or are not in the guild. """ query = {} @@ -2136,19 +2136,19 @@ async def remove_guild_ban(self, guild_id: str, user_id: str, *, reason: str = . Parameters ---------- - guild_id : :obj:`str` + guild_id : :obj:`~str` The ID of the guild to un-ban the user from. - user_id : :obj:`str` + user_id : :obj:`~str` The ID of the user you want to un-ban. - reason : :obj:`str` + reason : :obj:`~str` If specified, the audit log reason explaining why the operation was performed. Raises ------ - :obj:`hikari.errors.NotFoundHTTPError` + :obj:`~hikari.errors.NotFoundHTTPError` If either the guild or member aren't found, or the member is not banned. - :obj:`hikari.errors.ForbiddenHTTPError` + :obj:`~hikari.errors.ForbiddenHTTPError` If you lack the ``BAN_MEMBERS`` permission or are not a in the guild. """ route = routes.GUILD_BAN.compile(self.DELETE, guild_id=guild_id, user_id=user_id) @@ -2159,19 +2159,19 @@ async def get_guild_roles(self, guild_id: str) -> typing.Sequence[typing.Dict[st Parameters ---------- - guild_id : :obj:`str` + guild_id : :obj:`~str` The ID of the guild you want to get the roles from. Returns ------- - :obj:`typing.Sequence` [ :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ] ] + :obj:`~typing.Sequence` [ :obj:`~typing.Dict` [ :obj:`~str`, :obj:`~typing.Any` ] ] A list of role objects. Raises ------ - :obj:`hikari.errors.NotFoundHTTPError` + :obj:`~hikari.errors.NotFoundHTTPError` If the guild is not found. - :obj:`hikari.errors.ForbiddenHTTPError` + :obj:`~hikari.errors.ForbiddenHTTPError` If you're not in the guild. """ route = routes.GUILD_ROLES.compile(self.GET, guild_id=guild_id) @@ -2192,34 +2192,34 @@ async def create_guild_role( Parameters ---------- - guild_id : :obj:`str` + guild_id : :obj:`~str` The ID of the guild you want to create the role on. - name : :obj:`str` + name : :obj:`~str` If specified, the new role name string. - permissions : :obj:`int` + permissions : :obj:`~int` If specified, the permissions integer for the role. - color : :obj:`int` + color : :obj:`~int` If specified, the color for the role. - hoist : :obj:`bool` + hoist : :obj:`~bool` If specified, whether the role will be hoisted. - mentionable : :obj:`bool` + mentionable : :obj:`~bool` If specified, whether the role will be able to be mentioned by any user. - reason : :obj:`str` + reason : :obj:`~str` If specified, the audit log reason explaining why the operation was performed. Returns ------- - :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ] + :obj:`~typing.Dict` [ :obj:`~str`, :obj:`~typing.Any` ] The newly created role object. Raises ------ - :obj:`hikari.errors.NotFoundHTTPError` + :obj:`~hikari.errors.NotFoundHTTPError` If the guild is not found. - :obj:`hikari.errors.ForbiddenHTTPError` + :obj:`~hikari.errors.ForbiddenHTTPError` If you lack the ``MANAGE_ROLES`` permission or you're not in the guild. - :obj:`hikari.errors.BadRequestHTTPError` + :obj:`~hikari.errors.BadRequestHTTPError` If you provide invalid values for the role attributes. """ payload = {} @@ -2240,25 +2240,25 @@ async def modify_guild_role_positions( ---------- guild_id : str The ID of the guild the roles belong to. - role : :obj:`typing.Tuple` [ :obj:`str`, :obj:`int` ] + role : :obj:`~typing.Tuple` [ :obj:`~str`, :obj:`~int` ] The first role to move. This is a tuple of the role ID and the integer position. - *roles : :obj:`typing.Tuple` [ :obj:`str`, :obj:`int` ] + *roles : :obj:`~typing.Tuple` [ :obj:`~str`, :obj:`~int` ] Optional extra roles to move. These must be tuples of the role ID and the integer position. Returns ------- - :obj:`typing.Sequence` [ :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ] ] + :obj:`~typing.Sequence` [ :obj:`~typing.Dict` [ :obj:`~str`, :obj:`~typing.Any` ] ] A list of all the guild roles. Raises ------ - :obj:`hikari.errors.NotFoundHTTPError` + :obj:`~hikari.errors.NotFoundHTTPError` If either the guild or any of the roles aren't found. - :obj:`hikari.errors.ForbiddenHTTPError` + :obj:`~hikari.errors.ForbiddenHTTPError` If you lack the ``MANAGE_ROLES`` permission or you're not in the guild. - :obj:`hikari.errors.BadRequestHTTPError` + :obj:`~hikari.errors.BadRequestHTTPError` If you provide invalid values for the `position` fields. """ payload = [{"id": r[0], "position": r[1]} for r in (role, *roles)] @@ -2281,36 +2281,36 @@ async def modify_guild_role( # lgtm [py/similar-function] Parameters ---------- - guild_id : :obj:`str` + guild_id : :obj:`~str` The ID of the guild the role belong to. - role_id : :obj:`str` + role_id : :obj:`~str` The ID of the role you want to edit. - name : :obj:`str` + name : :obj:`~str` If specified, the new role's name string. - permissions : :obj:`int` + permissions : :obj:`~int` If specified, the new permissions integer for the role. - color : :obj:`int` + color : :obj:`~int` If specified, the new color for the new role. - hoist : :obj:`bool` + hoist : :obj:`~bool` If specified, whether the role should hoist or not. - mentionable : :obj:`bool` + mentionable : :obj:`~bool` If specified, whether the role should be mentionable or not. - reason : :obj:`str` + reason : :obj:`~str` If specified, the audit log reason explaining why the operation was performed. Returns ------- - :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ] + :obj:`~typing.Dict` [ :obj:`~str`, :obj:`~typing.Any` ] The edited role object. Raises ------ - :obj:`hikari.errors.NotFoundHTTPError` + :obj:`~hikari.errors.NotFoundHTTPError` If either the guild or role aren't found. - :obj:`hikari.errors.ForbiddenHTTPError` + :obj:`~hikari.errors.ForbiddenHTTPError` If you lack the ``MANAGE_ROLES`` permission or you're not in the guild. - :obj:`hikari.errors.BadRequestHTTPError` + :obj:`~hikari.errors.BadRequestHTTPError` If you provide invalid values for the role attributes. """ payload = {} @@ -2327,16 +2327,16 @@ async def delete_guild_role(self, guild_id: str, role_id: str) -> None: Parameters ---------- - guild_id : :obj:`str` + guild_id : :obj:`~str` The ID of the guild you want to remove the role from. - role_id : :obj:`str` + role_id : :obj:`~str` The ID of the role you want to delete. Raises ------ - :obj:`hikari.errors.NotFoundHTTPError` + :obj:`~hikari.errors.NotFoundHTTPError` If either the guild or the role aren't found. - :obj:`hikari.errors.ForbiddenHTTPError` + :obj:`~hikari.errors.ForbiddenHTTPError` If you lack the ``MANAGE_ROLES`` permission or are not in the guild. """ route = routes.GUILD_ROLE.compile(self.DELETE, guild_id=guild_id, role_id=role_id) @@ -2347,23 +2347,23 @@ async def get_guild_prune_count(self, guild_id: str, days: int) -> int: Parameters ---------- - guild_id : :obj:`str` + guild_id : :obj:`~str` The ID of the guild you want to get the count for. - days : :obj:`int` + days : :obj:`~int` The number of days to count prune for (at least ``1``). Returns ------- - :obj:`int` + :obj:`~int` The number of members estimated to be pruned. Raises ------ - :obj:`hikari.errors.NotFoundHTTPError` + :obj:`~hikari.errors.NotFoundHTTPError` If the guild is not found. - :obj:`hikari.errors.ForbiddenHTTPError` + :obj:`~hikari.errors.ForbiddenHTTPError` If you lack the ``KICK_MEMBERS`` or you are not in the guild. - :obj:`hikari.errors.BadRequestHTTPError` + :obj:`~hikari.errors.BadRequestHTTPError` If you pass an invalid amount of days. """ payload = {"days": days} @@ -2378,30 +2378,30 @@ async def begin_guild_prune( Parameters ---------- - guild_id : :obj:`str` + guild_id : :obj:`~str` The ID of the guild you want to prune member of. - days : :obj:`int` + days : :obj:`~int` The number of inactivity days you want to use as filter. - compute_prune_count : :obj:`bool` + compute_prune_count : :obj:`~bool` Whether a count of pruned members is returned or not. Discouraged for large guilds out of politeness. - reason : :obj:`str` + reason : :obj:`~str` If specified, the audit log reason explaining why the operation was performed. Returns ------- - :obj:`int`, optional + :obj:`~int`, optional The number of members who were kicked if ``compute_prune_count`` - is :obj:`True`, else :obj:`None`. + is :obj:`~True`, else :obj:`~None`. Raises ------ - :obj:`hikari.errors.NotFoundHTTPError` + :obj:`~hikari.errors.NotFoundHTTPError` If the guild is not found: - :obj:`hikari.errors.ForbiddenHTTPError` + :obj:`~hikari.errors.ForbiddenHTTPError` If you lack the ``KICK_MEMBER`` permission or are not in the guild. - :obj:`hikari.errors.BadRequestHTTPError` + :obj:`~hikari.errors.BadRequestHTTPError` If you provide invalid values for the ``days`` or ``compute_prune_count`` fields. """ query = {"days": days} @@ -2419,19 +2419,19 @@ async def get_guild_voice_regions(self, guild_id: str) -> typing.Sequence[typing Parameters ---------- - guild_id : :obj:`str` + guild_id : :obj:`~str` The ID of the guild to get the voice regions for. Returns ------- - :obj:`typing.Sequence` [ :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ] ] + :obj:`~typing.Sequence` [ :obj:`~typing.Dict` [ :obj:`~str`, :obj:`~typing.Any` ] ] A list of voice region objects. Raises ------ - :obj:`hikari.errors.NotFoundHTTPError` + :obj:`~hikari.errors.NotFoundHTTPError` If the guild is not found. - :obj:`hikari.errors.ForbiddenHTTPError` + :obj:`~hikari.errors.ForbiddenHTTPError` If you are not in the guild. """ route = routes.GUILD_VOICE_REGIONS.compile(self.GET, guild_id=guild_id) @@ -2442,19 +2442,19 @@ async def get_guild_invites(self, guild_id: str) -> typing.Sequence[typing.Dict[ Parameters ---------- - guild_id : :obj:`str` + guild_id : :obj:`~str` The ID of the guild to get the invites for. Returns ------- - :obj:`typing.Sequence` [ :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ] ] + :obj:`~typing.Sequence` [ :obj:`~typing.Dict` [ :obj:`~str`, :obj:`~typing.Any` ] ] A list of invite objects (with metadata). Raises ------ - :obj:`hikari.errors.NotFoundHTTPError` + :obj:`~hikari.errors.NotFoundHTTPError` If the guild is not found. - :obj:`hikari.errors.ForbiddenHTTPError` + :obj:`~hikari.errors.ForbiddenHTTPError` If you lack the ``MANAGE_GUILD`` permission or are not in the guild. """ route = routes.GUILD_INVITES.compile(self.GET, guild_id=guild_id) @@ -2465,19 +2465,19 @@ async def get_guild_integrations(self, guild_id: str) -> typing.Sequence[typing. Parameters ---------- - guild_id : :obj:`int` + guild_id : :obj:`~int` The ID of the guild to get the integrations for. Returns ------- - :obj:`typing.Sequence` [ :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ] ] + :obj:`~typing.Sequence` [ :obj:`~typing.Dict` [ :obj:`~str`, :obj:`~typing.Any` ] ] A list of integration objects. Raises ------ - :obj:`hikari.errors.NotFoundHTTPError` + :obj:`~hikari.errors.NotFoundHTTPError` If the guild is not found. - :obj:`hikari.errors.ForbiddenHTTPError` + :obj:`~hikari.errors.ForbiddenHTTPError` If you lack the ``MANAGE_GUILD`` permission or are not in the guild. """ route = routes.GUILD_INTEGRATIONS.compile(self.GET, guild_id=guild_id) @@ -2497,27 +2497,27 @@ async def modify_guild_integration( Parameters ---------- - guild_id : :obj:`str` + guild_id : :obj:`~str` The ID of the guild to which the integration belongs to. - integration_id : :obj:`str` + integration_id : :obj:`~str` The ID of the integration. - expire_behaviour : :obj:`int` + expire_behaviour : :obj:`~int` If specified, the behaviour for when an integration subscription lapses. - expire_grace_period : :obj:`int` + expire_grace_period : :obj:`~int` If specified, time interval in seconds in which the integration will ignore lapsed subscriptions. - enable_emojis : :obj:`bool` + enable_emojis : :obj:`~bool` If specified, whether emojis should be synced for this integration. - reason : :obj:`str` + reason : :obj:`~str` If specified, the audit log reason explaining why the operation was performed. Raises ------ - :obj:`hikari.errors.NotFoundHTTPError` + :obj:`~hikari.errors.NotFoundHTTPError` If either the guild or the integration aren't found. - :obj:`hikari.errors.ForbiddenHTTPError` + :obj:`~hikari.errors.ForbiddenHTTPError` If you lack the ``MANAGE_GUILD`` permission or are not in the guild. """ payload = {} @@ -2533,19 +2533,19 @@ async def delete_guild_integration(self, guild_id: str, integration_id: str, *, Parameters ---------- - guild_id : :obj:`str` + guild_id : :obj:`~str` The ID of the guild to which the integration belongs to. - integration_id : :obj:`str` + integration_id : :obj:`~str` The ID of the integration to delete. - reason : :obj:`str` + reason : :obj:`~str` If specified, the audit log reason explaining why the operation was performed. Raises ------ - :obj:`hikari.errors.NotFoundHTTPError` + :obj:`~hikari.errors.NotFoundHTTPError` If either the guild or the integration aren't found. - :obj:`hikari.errors.ForbiddenHTTPError` + :obj:`~hikari.errors.ForbiddenHTTPError` If you lack the `MANAGE_GUILD` permission or are not in the guild. """ route = routes.GUILD_INTEGRATION.compile(self.DELETE, guild_id=guild_id, integration_id=integration_id) @@ -2556,16 +2556,16 @@ async def sync_guild_integration(self, guild_id: str, integration_id: str) -> No Parameters ---------- - guild_id : :obj:`str` + guild_id : :obj:`~str` The ID of the guild to which the integration belongs to. - integration_id : :obj:`str` + integration_id : :obj:`~str` The ID of the integration to sync. Raises ------ - :obj:`hikari.errors.NotFoundHTTPError` + :obj:`~hikari.errors.NotFoundHTTPError` If either the guild or the integration aren't found. - :obj:`hikari.errors.ForbiddenHTTPError` + :obj:`~hikari.errors.ForbiddenHTTPError` If you lack the ``MANAGE_GUILD`` permission or are not in the guild. """ route = routes.GUILD_INTEGRATION_SYNC.compile(self.POST, guild_id=guild_id, integration_id=integration_id) @@ -2576,19 +2576,19 @@ async def get_guild_embed(self, guild_id: str) -> typing.Dict[str, typing.Any]: Parameters ---------- - guild_id : :obj:`str` + guild_id : :obj:`~str` The ID of the guild to get the embed for. Returns ------- - :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ] + :obj:`~typing.Dict` [ :obj:`~str`, :obj:`~typing.Any` ] A guild embed object. Raises ------ - :obj:`hikari.errors.NotFoundHTTPError` + :obj:`~hikari.errors.NotFoundHTTPError` If the guild is not found. - :obj:`hikari.errors.ForbiddenHTTPError` + :obj:`~hikari.errors.ForbiddenHTTPError` If you either lack the ``MANAGE_GUILD`` permission or are not in the guild. """ route = routes.GUILD_EMBED.compile(self.GET, guild_id=guild_id) @@ -2601,27 +2601,27 @@ async def modify_guild_embed( Parameters ---------- - guild_id : :obj:`str` + guild_id : :obj:`~str` The ID of the guild to edit the embed for. - channel_id : :obj:`str`, optional + channel_id : :obj:`~str`, optional If specified, the channel that this embed's invite should target. - Set to :obj:`None` to disable invites for this embed. - enabled : :obj:`bool` + Set to :obj:`~None` to disable invites for this embed. + enabled : :obj:`~bool` If specified, whether this embed should be enabled. - reason : :obj:`str` + reason : :obj:`~str` If specified, the audit log reason explaining why the operation was performed. Returns ------- - :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ] + :obj:`~typing.Dict` [ :obj:`~str`, :obj:`~typing.Any` ] The updated embed object. Raises ------ - :obj:`hikari.errors.NotFoundHTTPError` + :obj:`~hikari.errors.NotFoundHTTPError` If the guild is not found. - :obj:`hikari.errors.ForbiddenHTTPError` + :obj:`~hikari.errors.ForbiddenHTTPError` If you either lack the ``MANAGE_GUILD`` permission or are not in the guild. """ payload = {} @@ -2635,19 +2635,19 @@ async def get_guild_vanity_url(self, guild_id: str) -> typing.Dict[str, typing.A Parameters ---------- - guild_id : :obj:`str` + guild_id : :obj:`~str` The ID of the guild to get the vanity URL for. Returns ------- - :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ] + :obj:`~typing.Dict` [ :obj:`~str`, :obj:`~typing.Any` ] A partial invite object containing the vanity URL in the ``code`` field. Raises ------ - :obj:`hikari.errors.NotFoundHTTPError` + :obj:`~hikari.errors.NotFoundHTTPError` If the guild is not found. - :obj:`hikari.errors.ForbiddenHTTPError` + :obj:`~hikari.errors.ForbiddenHTTPError` If you either lack the ``MANAGE_GUILD`` permission or are not in the guild. """ route = routes.GUILD_VANITY_URL.compile(self.GET, guild_id=guild_id) @@ -2658,14 +2658,14 @@ def get_guild_widget_image_url(self, guild_id: str, *, style: str = ...,) -> str Parameters ---------- - guild_id : :obj:`str` + guild_id : :obj:`~str` The guild ID to use for the widget. - style : :obj:`str` + style : :obj:`~str` If specified, the syle of the widget. Returns ------- - :obj:`str` + :obj:`~str` A URL to retrieve a PNG widget for your guild. Note @@ -2685,20 +2685,20 @@ async def get_invite(self, invite_code: str, *, with_counts: bool = ...) -> typi Parameters ---------- - invite_code : :obj:`str` + invite_code : :obj:`~str` The ID for wanted invite. - with_counts : :obj:`bool` + with_counts : :obj:`~bool` If specified, whether to attempt to count the number of times the invite has been used. Returns ------- - :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ] + :obj:`~typing.Dict` [ :obj:`~str`, :obj:`~typing.Any` ] The requested invite object. Raises ------ - :obj:`hikari.errors.NotFoundHTTPError` + :obj:`~hikari.errors.NotFoundHTTPError` If the invite is not found. """ query = {} @@ -2711,20 +2711,20 @@ async def delete_invite(self, invite_code: str) -> None: Parameters ---------- - invite_code : :obj:`str` + invite_code : :obj:`~str` The ID for the invite to be deleted. Returns ------- - :obj:`None` # Marker + :obj:`~None` # Marker Nothing, unlike what the API specifies. This is done to maintain consistency with other calls of a similar nature in this API wrapper. Raises ------ - :obj:`hikari.errors.NotFoundHTTPError` + :obj:`~hikari.errors.NotFoundHTTPError` If the invite is not found. - :obj:`hikari.errors.ForbiddenHTTPError` + :obj:`~hikari.errors.ForbiddenHTTPError` If you lack either ``MANAGE_CHANNELS`` on the channel the invite belongs to or ``MANAGE_GUILD`` for guild-global delete. """ @@ -2736,7 +2736,7 @@ async def get_current_user(self) -> typing.Dict[str, typing.Any]: Returns ------- - :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ] + :obj:`~typing.Dict` [ :obj:`~str`, :obj:`~typing.Any` ] The current user object. """ route = routes.OWN_USER.compile(self.GET) @@ -2747,17 +2747,17 @@ async def get_user(self, user_id: str) -> typing.Dict[str, typing.Any]: Parameters ---------- - user_id : :obj:`str` + user_id : :obj:`~str` The ID of the user to get. Returns ------- - :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ] + :obj:`~typing.Dict` [ :obj:`~str`, :obj:`~typing.Any` ] The requested user object. Raises ------ - :obj:`hikari.errors.NotFoundHTTPError` + :obj:`~hikari.errors.NotFoundHTTPError` If the user is not found. """ route = routes.USER.compile(self.GET, user_id=user_id) @@ -2770,20 +2770,20 @@ async def modify_current_user( Parameters ---------- - username : :obj:`str` + username : :obj:`~str` If specified, the new username string. - avatar : :obj:`bytes`, optional + avatar : :obj:`~bytes`, optional If specified, the new avatar image in bytes form. - If it is :obj:`None`, the avatar is removed. + If it is :obj:`~None`, the avatar is removed. Returns ------- - :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ] + :obj:`~typing.Dict` [ :obj:`~str`, :obj:`~typing.Any` ] The updated user object. Raises ------ - :obj:`hikari.errors.BadRequestHTTPError` + :obj:`~hikari.errors.BadRequestHTTPError` If you pass username longer than the limit (``2-32``) or an invalid image. """ payload = {} @@ -2801,7 +2801,7 @@ async def get_current_user_connections(self) -> typing.Sequence[typing.Dict[str, Returns ------- - :obj:`typing.Sequence` [ :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ] ] + :obj:`~typing.Sequence` [ :obj:`~typing.Dict` [ :obj:`~str`, :obj:`~typing.Any` ] ] A list of connection objects. """ route = routes.OWN_CONNECTIONS.compile(self.GET) @@ -2814,24 +2814,24 @@ async def get_current_user_guilds( Parameters ---------- - before : :obj:`str` + before : :obj:`~str` If specified, the guild ID to get guilds before it. - after : :obj:`str` + after : :obj:`~str` If specified, the guild ID to get guilds after it. - limit : :obj:`int` + limit : :obj:`~int` If specified, the limit of guilds to get. Has to be between ``1`` and ``100``. Returns ------- - :obj:`typing.Sequence` [ :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ] ] + :obj:`~typing.Sequence` [ :obj:`~typing.Dict` [ :obj:`~str`, :obj:`~typing.Any` ] ] A list of partial guild objects. Raises ------ - :obj:`hikari.errors.BadRequestHTTPError` + :obj:`~hikari.errors.BadRequestHTTPError` If you pass both ``before`` and ``after`` or an invalid value for ``limit``. """ @@ -2847,12 +2847,12 @@ async def leave_guild(self, guild_id: str) -> None: Parameters ---------- - guild_id : :obj:`str` + guild_id : :obj:`~str` The ID of the guild to leave. Raises ------ - :obj:`hikari.errors.NotFoundHTTPError` + :obj:`~hikari.errors.NotFoundHTTPError` If the guild is not found. """ route = routes.LEAVE_GUILD.compile(self.DELETE, guild_id=guild_id) @@ -2863,17 +2863,17 @@ async def create_dm(self, recipient_id: str) -> typing.Dict[str, typing.Any]: Parameters ---------- - recipient_id : :obj:`str` + recipient_id : :obj:`~str` The ID of the user to create the new DM channel with. Returns ------- - :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ] + :obj:`~typing.Dict` [ :obj:`~str`, :obj:`~typing.Any` ] The newly created DM channel object. Raises ------ - :obj:`hikari.errors.NotFoundHTTPError` + :obj:`~hikari.errors.NotFoundHTTPError` If the recipient is not found. """ payload = {"recipient_id": recipient_id} @@ -2885,7 +2885,7 @@ async def list_voice_regions(self) -> typing.Sequence[typing.Dict[str, typing.An Returns ------- - :obj:`typing.Sequence` [ :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ] ] + :obj:`~typing.Sequence` [ :obj:`~typing.Dict` [ :obj:`~str`, :obj:`~typing.Any` ] ] A list of voice regions available Note @@ -2902,29 +2902,29 @@ async def create_webhook( Parameters ---------- - channel_id : :obj:`str` + channel_id : :obj:`~str` The ID of the channel for webhook to be created in. - name : :obj:`str` + name : :obj:`~str` The webhook's name string. - avatar : :obj:`bytes` + avatar : :obj:`~bytes` If specified, the avatar image in bytes form. - reason : :obj:`str` + reason : :obj:`~str` If specified, the audit log reason explaining why the operation was performed. Returns ------- - :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ] + :obj:`~typing.Dict` [ :obj:`~str`, :obj:`~typing.Any` ] The newly created webhook object. Raises ------ - :obj:`hikari.errors.NotFoundHTTPError` + :obj:`~hikari.errors.NotFoundHTTPError` If the channel is not found. - :obj:`hikari.errors.ForbiddenHTTPError` + :obj:`~hikari.errors.ForbiddenHTTPError` If you either lack the ``MANAGE_WEBHOOKS`` permission or can not see the given channel. - :obj:`hikari.errors.BadRequestHTTPError` + :obj:`~hikari.errors.BadRequestHTTPError` If the avatar image is too big or the format is invalid. """ payload = {"name": name} @@ -2937,19 +2937,19 @@ async def get_channel_webhooks(self, channel_id: str) -> typing.Sequence[typing. Parameters ---------- - channel_id : :obj:`str` + channel_id : :obj:`~str` The ID of the channel to get the webhooks from. Returns ------- - :obj:`typing.Sequence` [ :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ] ] + :obj:`~typing.Sequence` [ :obj:`~typing.Dict` [ :obj:`~str`, :obj:`~typing.Any` ] ] A list of webhook objects for the give channel. Raises ------ - :obj:`hikari.errors.NotFoundHTTPError` + :obj:`~hikari.errors.NotFoundHTTPError` If the channel is not found. - :obj:`hikari.errors.ForbiddenHTTPError` + :obj:`~hikari.errors.ForbiddenHTTPError` If you either lack the ``MANAGE_WEBHOOKS`` permission or can not see the given channel. """ @@ -2961,19 +2961,19 @@ async def get_guild_webhooks(self, guild_id: str) -> typing.Sequence[typing.Dict Parameters ---------- - guild_id : :obj:`str` + guild_id : :obj:`~str` The ID for the guild to get the webhooks from. Returns ------- - :obj:`typing.Sequence` [ :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ] ] + :obj:`~typing.Sequence` [ :obj:`~typing.Dict` [ :obj:`~str`, :obj:`~typing.Any` ] ] A list of webhook objects for the given guild. Raises ------ - :obj:`hikari.errors.NotFoundHTTPError` + :obj:`~hikari.errors.NotFoundHTTPError` If the guild is not found. - :obj:`hikari.errors.ForbiddenHTTPError` + :obj:`~hikari.errors.ForbiddenHTTPError` If you either lack the ``MANAGE_WEBHOOKS`` permission or aren't a member of the given guild. """ @@ -2985,24 +2985,24 @@ async def get_webhook(self, webhook_id: str, *, webhook_token: str = ...) -> typ Parameters ---------- - webhook_id : :obj:`str` + webhook_id : :obj:`~str` The ID of the webhook to get. - webhook_token : :obj:`str` + webhook_token : :obj:`~str` If specified, the webhook token to use to get it (bypassing bot authorization). Returns ------- - :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ] + :obj:`~typing.Dict` [ :obj:`~str`, :obj:`~typing.Any` ] The requested webhook object. Raises ------ - :obj:`hikari.errors.NotFoundHTTPError` + :obj:`~hikari.errors.NotFoundHTTPError` If the webhook is not found. - :obj:`hikari.errors.ForbiddenHTTPError` + :obj:`~hikari.errors.ForbiddenHTTPError` If you're not in the guild that owns this webhook or lack the ``MANAGE_WEBHOOKS`` permission. - :obj:`hikari.errors.UnauthorizedHTTPError` + :obj:`~hikari.errors.UnauthorizedHTTPError` If you pass a token that's invalid for the target webhook. """ if webhook_token is ...: @@ -3025,35 +3025,35 @@ async def modify_webhook( Parameters ---------- - webhook_id : :obj:`str` + webhook_id : :obj:`~str` The ID of the webhook to edit. - webhook_token : :obj:`str` + webhook_token : :obj:`~str` If specified, the webhook token to use to modify it (bypassing bot authorization). - name : :obj:`str` + name : :obj:`~str` If specified, the new name string. - avatar : :obj:`bytes` + avatar : :obj:`~bytes` If specified, the new avatar image in bytes form. If None, then it is removed. - channel_id : :obj:`str` + channel_id : :obj:`~str` If specified, the ID of the new channel the given webhook should be moved to. - reason : :obj:`str` + reason : :obj:`~str` If specified, the audit log reason explaining why the operation was performed. Returns ------- - :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ] + :obj:`~typing.Dict` [ :obj:`~str`, :obj:`~typing.Any` ] The updated webhook object. Raises ------ - :obj:`hikari.errors.NotFoundHTTPError` + :obj:`~hikari.errors.NotFoundHTTPError` If either the webhook or the channel aren't found. - :obj:`hikari.errors.ForbiddenHTTPError` + :obj:`~hikari.errors.ForbiddenHTTPError` If you either lack the ``MANAGE_WEBHOOKS`` permission or aren't a member of the guild this webhook belongs to. - :obj:`hikari.errors.UnauthorizedHTTPError` + :obj:`~hikari.errors.UnauthorizedHTTPError` If you pass a token that's invalid for the target webhook. """ payload = {} @@ -3073,20 +3073,20 @@ async def delete_webhook(self, webhook_id: str, *, webhook_token: str = ...) -> Parameters ---------- - webhook_id : :obj:`str` + webhook_id : :obj:`~str` The ID of the webhook to delete - webhook_token : :obj:`str` + webhook_token : :obj:`~str` If specified, the webhook token to use to delete it (bypassing bot authorization). Raises ------ - :obj:`hikari.errors.NotFoundHTTPError` + :obj:`~hikari.errors.NotFoundHTTPError` If the webhook is not found. - :obj:`hikari.errors.ForbiddenHTTPError` + :obj:`~hikari.errors.ForbiddenHTTPError` If you either lack the ``MANAGE_WEBHOOKS`` permission or aren't a member of the guild this webhook belongs to. - :obj:`hikari.errors.UnauthorizedHTTPError` + :obj:`~hikari.errors.UnauthorizedHTTPError` If you pass a token that's invalid for the target webhook. """ if webhook_token is ...: @@ -3113,45 +3113,45 @@ async def execute_webhook( Parameters ---------- - webhook_id : :obj:`str` + webhook_id : :obj:`~str` The ID of the webhook to execute. - webhook_token : :obj:`str` + webhook_token : :obj:`~str` The token of the webhook to execute. - content : :obj:`str` + content : :obj:`~str` If specified, the webhook message content to send. - username : :obj:`str` + username : :obj:`~str` If specified, the username to override the webhook's username for this request. - avatar_url : :obj:`str` + avatar_url : :obj:`~str` If specified, the url of an image to override the webhook's avatar with for this request. - tts : :obj:`bool` + tts : :obj:`~bool` If specified, whether this webhook should create a TTS message. - wait : :obj:`bool` + wait : :obj:`~bool` If specified, whether this request should wait for the webhook to be executed and return the resultant message object. - file : :obj:`typing.Tuple` [ :obj:`str`, :obj:`io.IOBase` ] - If specified, a tuple of the file name and either raw :obj:`bytes` - or a :obj:`io.IOBase` derived object that points to a buffer + file : :obj:`~typing.Tuple` [ :obj:`~str`, :obj:`~io.IOBase` ] + If specified, a tuple of the file name and either raw :obj:`~bytes` + or a :obj:`~io.IOBase` derived object that points to a buffer containing said file. - embeds : :obj:`typing.Sequence` [:obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ]] + embeds : :obj:`~typing.Sequence` [:obj:`~typing.Dict` [ :obj:`~str`, :obj:`~typing.Any` ]] If specified, the sequence of embed objects that will be sent with this message. - allowed_mentions : :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ] + allowed_mentions : :obj:`~typing.Dict` [ :obj:`~str`, :obj:`~typing.Any` ] If specified, the mentions to parse from the ``content``. If not specified, will parse all mentions from the ``content``. Returns ------- - :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ], optional - The created message object if ``wait`` is :obj:`True`, else - :obj:`None`. + :obj:`~typing.Dict` [ :obj:`~str`, :obj:`~typing.Any` ], optional + The created message object if ``wait`` is :obj:`~True`, else + :obj:`~None`. Raises ------ - :obj:`hikari.errors.NotFoundHTTPError` + :obj:`~hikari.errors.NotFoundHTTPError` If the channel ID or webhook ID is not found. - :obj:`hikari.errors.BadRequestHTTPError` + :obj:`~hikari.errors.BadRequestHTTPError` This can be raised if the file is too large; if the embed exceeds the defined limits; if the message content is specified only and empty or greater than ``2000`` characters; if neither content, file @@ -3159,9 +3159,9 @@ async def execute_webhook( fields in ``allowed_mentions``; if you specify to parse all users/roles mentions but also specify which users/roles to parse only. - :obj:`hikari.errors.ForbiddenHTTPError` + :obj:`~hikari.errors.ForbiddenHTTPError` If you lack permissions to send to this channel. - :obj:`hikari.errors.UnauthorizedHTTPError` + :obj:`~hikari.errors.UnauthorizedHTTPError` If you pass a token that's invalid for the target webhook. """ form = aiohttp.FormData() @@ -3205,7 +3205,7 @@ async def get_current_application_info(self) -> typing.Dict[str, typing.Any]: Returns ------- - :obj:`typing.Dict` [ :obj:`str`, :obj:`typing.Any` ] + :obj:`~typing.Dict` [ :obj:`~str`, :obj:`~typing.Any` ] An application info object. """ route = routes.OAUTH2_APPLICATIONS_ME.compile(self.GET) diff --git a/hikari/net/routes.py b/hikari/net/routes.py index 55bd8e8dcd..32908cb611 100644 --- a/hikari/net/routes.py +++ b/hikari/net/routes.py @@ -29,11 +29,11 @@ class CompiledRoute: Parameters ---------- - method : :obj:`str` + method : :obj:`~str` The HTTP method to use. - path : :obj:`str` + path : :obj:`~str` The path with any major parameters interpolated in. - major_params_hash : :obj:`str` + major_params_hash : :obj:`~str` The part of the hash identifier to use for the compiled set of major parameters. """ @@ -41,22 +41,22 @@ class CompiledRoute: #: The method to use on the route. #: - #: :type: :obj:`str` + #: :type: :obj:`~str` method: typing.Final[str] #: The major parameters in a bucket hash-compatible representation. #: - #: :type: :obj:`str` + #: :type: :obj:`~str` major_params_hash: typing.Final[str] #: The compiled route path to use #: - #: :type: :obj:`str` + #: :type: :obj:`~str` compiled_path: typing.Final[str] #: The hash code #: - #: :type: :obj:`int` + #: :type: :obj:`~int` hash_code: typing.Final[int] def __init__(self, method: str, path_template: str, path: str, major_params_hash: str) -> None: @@ -70,12 +70,12 @@ def create_url(self, base_url: str) -> str: Parameters ---------- - base_url : :obj:`str` + base_url : :obj:`~str` The base of the URL to prepend to the compiled path. Returns ------- - :obj:`str` + :obj:`~str` The full URL for the route. """ return base_url + self.compiled_path @@ -88,13 +88,13 @@ def create_real_bucket_hash(self, initial_bucket_hash: str) -> str: Parameters ---------- - initial_bucket_hash: :obj:`str` + initial_bucket_hash: :obj:`~str` The initial bucket hash provided by Discord in the HTTP headers for a given response. Returns ------- - :obj:`str` + :obj:`~str` The input hash amalgamated with a hash code produced by the major parameters in this compiled route instance. """ @@ -128,9 +128,9 @@ class RouteTemplate: Parameters ---------- - path_template : :obj:`str` + path_template : :obj:`~str` The template string for the path to use. - major_params : :obj:`str` + major_params : :obj:`~str` A collection of major parameter names that appear in the template path. If not specified, the default major parameter names are extracted and used in-place. @@ -140,12 +140,12 @@ class RouteTemplate: #: The template string used for the path. #: - #: :type: :obj:`str` + #: :type: :obj:`~str` path_template: typing.Final[str] #: Major parameter names that appear in the template path. #: - #: :type: :obj:`typing.FrozenSet` [ :obj:`str` ] + #: :type: :obj:`~typing.FrozenSet` [ :obj:`~str` ] major_params: typing.Final[typing.FrozenSet[str]] def __init__(self, path_template: str, major_params: typing.Collection[str] = None) -> None: @@ -156,21 +156,21 @@ def __init__(self, path_template: str, major_params: typing.Collection[str] = No self.major_params = frozenset(major_params) def compile(self, method: str, /, **kwargs: typing.Any) -> CompiledRoute: - """Generate a formatted :obj:`CompiledRoute` for this route template. + """Generate a formatted :obj:`~CompiledRoute` for this route template. This takes into account any URL parameters that have been passed, and extracting the :attr:major_params" for bucket hash operations accordingly. Parameters ---------- - method : :obj:`str` + method : :obj:`~str` The method to use. - **kwargs : :obj:`typing.Any` + **kwargs : :obj:`~typing.Any` Any parameters to interpolate into the route path. Returns ------- - :obj:`CompiledRoute` + :obj:`~CompiledRoute` The compiled route. """ major_hash_part = "-".join((str(kwargs[p]) for p in self.major_params)) diff --git a/hikari/net/shard.py b/hikari/net/shard.py index 43836c5c59..547cb8b164 100644 --- a/hikari/net/shard.py +++ b/hikari/net/shard.py @@ -65,7 +65,7 @@ class ShardConnection: application of events that occur, and to allow you to change your presence, amongst other real-time applications. - Each :obj:`ShardConnection` represents a single shard. + Each :obj:`~ShardConnection` represents a single shard. Expected events that may be passed to the event dispatcher are documented in the `gateway event reference `_. @@ -78,15 +78,15 @@ class ShardConnection: Parameters ---------- - compression: :obj:`bool` + compression: :obj:`~bool` If True, then payload compression is enabled on the connection. If False, no payloads are compressed. You usually want to keep this enabled. - connector: :obj:`aiohttp.BaseConnector`, optional - The :obj:`aiohttp.BaseConnector` to use for the HTTP session that + connector: :obj:`~aiohttp.BaseConnector`, optional + The :obj:`~aiohttp.BaseConnector` to use for the HTTP session that gets upgraded to a websocket connection. You can use this to customise connection pooling, etc. - debug: :obj:`bool` + debug: :obj:`~bool` If True, the client is configured to provide extra contextual information to use when debugging this library or extending it. This includes logging every payload that is sent or received to the logger @@ -94,14 +94,14 @@ class ShardConnection: dispatch: ``dispatch function`` The function to invoke with any dispatched events. This must not be a coroutine function, and must take three arguments only. The first is - the reference to this :obj:`ShardConnection` The second is the + the reference to this :obj:`~ShardConnection` The second is the event name. - initial_presence: :obj:`typing.Dict`, optional - A raw JSON object as a :obj:`typing.Dict` that should be set as the - initial presence of the bot user once online. If :obj:`None`, then it + initial_presence: :obj:`~typing.Dict`, optional + A raw JSON object as a :obj:`~typing.Dict` that should be set as the + initial presence of the bot user once online. If :obj:`~None`, then it will be set to the default, which is showing up as online without a custom status message. - intents: :obj:`hikari.net.codes.GatewayIntent`, optional + intents: :obj:`~hikari.net.codes.GatewayIntent`, optional Bitfield of intents to use. If you use the V7 API, this is mandatory. This field will determine what events you will receive. json_deserialize: ``deserialization function`` @@ -110,47 +110,47 @@ class ShardConnection: json_serialize: ``serialization function`` A custom JSON serializer function to use. Defaults to :func:`json.dumps`. - large_threshold: :obj:`int` + large_threshold: :obj:`~int` The number of members that have to be in a guild for it to be considered to be "large". Large guilds will not have member information sent automatically, and must manually request that member chunks be sent using :meth:`request_guild_members`. - proxy_auth: :obj:`aiohttp.BasicAuth`, optional - Optional :obj:`aiohttp.BasicAuth` object that can be provided to - allow authenticating with a proxy if you use one. Leave :obj:`None` to + proxy_auth: :obj:`~aiohttp.BasicAuth`, optional + Optional :obj:`~aiohttp.BasicAuth` object that can be provided to + allow authenticating with a proxy if you use one. Leave :obj:`~None` to ignore. - proxy_headers: :obj:`typing.Mapping` [ :obj:`str`, :obj:`str` ], optional - Optional :obj:`typing.Mapping` to provide as headers to allow the - connection through a proxy if you use one. Leave :obj:`None` to ignore. - proxy_url: :obj:`str`, optional - Optional :obj:`str` to use for a proxy server. If :obj:`None`, then it + proxy_headers: :obj:`~typing.Mapping` [ :obj:`~str`, :obj:`~str` ], optional + Optional :obj:`~typing.Mapping` to provide as headers to allow the + connection through a proxy if you use one. Leave :obj:`~None` to ignore. + proxy_url: :obj:`~str`, optional + Optional :obj:`~str` to use for a proxy server. If :obj:`~None`, then it is ignored. - session_id: :obj:`str`, optional + session_id: :obj:`~str`, optional The session ID to use. If specified along with ``seq``, then the gateway client will attempt to ``RESUME`` an existing session rather than re-``IDENTIFY``. Otherwise, it will be ignored. - seq: :obj:`int`, optional + seq: :obj:`~int`, optional The sequence number to use. If specified along with ``session_id``, then the gateway client will attempt to ``RESUME`` an existing session rather than re-``IDENTIFY``. Otherwise, it will be ignored. - shard_id: :obj:`int` + shard_id: :obj:`~int` The shard ID of this gateway client. Defaults to ``0``. - shard_count: :obj:`int` + shard_count: :obj:`~int` The number of shards on this gateway. Defaults to ``1``, which implies no sharding is taking place. - ssl_context: :obj:`ssl.SSLContext`, optional - An optional custom :obj:`ssl.SSLContext` to provide to customise how + ssl_context: :obj:`~ssl.SSLContext`, optional + An optional custom :obj:`~ssl.SSLContext` to provide to customise how SSL works. - token: :obj:`str` + token: :obj:`~str` The mandatory bot token for the bot account to use, minus the "Bot" authentication prefix used elsewhere. - url: :obj:`str` + url: :obj:`~str` The websocket URL to use. - verify_ssl: :obj:`bool` - If :obj:`True`, SSL verification is enabled, which is generally what you + verify_ssl: :obj:`~bool` + If :obj:`~True`, SSL verification is enabled, which is generally what you want. If you get SSL issues, you can try turning this off at your own risk. - version: :obj:`hikari.net.versions.GatewayVersion` + version: :obj:`~hikari.net.versions.GatewayVersion` The version of the gateway API to use. Defaults to the most recent stable documented version. """ @@ -199,13 +199,13 @@ class ShardConnection: #: An event that is set when the connection closes. #: - #: :type: :obj:`asyncio.Event` + #: :type: :obj:`~asyncio.Event` closed_event: typing.Final[asyncio.Event] #: The number of times we have disconnected from the gateway on this #: client instance. #: - #: :type: :obj:`int` + #: :type: :obj:`~int` disconnect_count: int #: The dispatch method to call when dispatching a new event. This is @@ -215,7 +215,7 @@ class ShardConnection: #: The heartbeat interval Discord instructed the client to beat at. #: This is ``nan`` until this information is received. #: - #: :type: :obj:`float` + #: :type: :obj:`~float` heartbeat_interval: float #: The most recent heartbeat latency measurement in seconds. This is @@ -223,13 +223,13 @@ class ShardConnection: #: as the time between sending a ``HEARTBEAT`` payload and receiving a #: ``HEARTBEAT_ACK`` response. #: - #: :type: :obj:`float` + #: :type: :obj:`~float` heartbeat_latency: float #: An event that is set when Discord sends a ``HELLO`` payload. This #: indicates some sort of connection has successfully been made. #: - #: :type: :obj:`asyncio.Event` + #: :type: :obj:`~asyncio.Event` hello_event: typing.Final[asyncio.Event] #: An event that is set when the client has successfully ``IDENTIFY``ed @@ -237,13 +237,13 @@ class ShardConnection: #: can now take place on the connection and events can be expected to #: be received. #: - #: :type: :obj:`asyncio.Event` + #: :type: :obj:`~asyncio.Event` handshake_event: typing.Final[asyncio.Event] #: The monotonic timestamp that the last ``HEARTBEAT`` was sent at, or #: ``nan`` if no ``HEARTBEAT`` has yet been sent. #: - #: :type: :obj:`float` + #: :type: :obj:`~float` last_heartbeat_sent: float #: The monotonic timestamp at which the last payload was received from @@ -251,12 +251,12 @@ class ShardConnection: #: the current time, then the connection is assumed to be zombied and #: is shut down. If no messages have been received yet, this is ``nan``. #: - #: :type: :obj:`float` + #: :type: :obj:`~float` last_message_received: float #: The logger used for dumping information about what this client is doing. #: - #: :type: :obj:`logging.Logger` + #: :type: :obj:`~logging.Logger` logger: typing.Final[logging.Logger] #: An event that is triggered when a ``READY`` payload is received for the @@ -271,44 +271,44 @@ class ShardConnection: #: you should wait for the event to be fired in the ``dispatch`` function #: you provide. #: - #: :type: :obj:`asyncio.Event` + #: :type: :obj:`~asyncio.Event` ready_event: typing.Final[asyncio.Event] #: An event that is triggered when a resume has succeeded on the gateway. #: - #: :type: :obj:`asyncio.Event` + #: :type: :obj:`~asyncio.Event` resumed_event: typing.Final[asyncio.Event] #: An event that is set when something requests that the connection #: should close somewhere. #: - #: :type: :obj:`asyncio.Event` + #: :type: :obj:`~asyncio.Event` requesting_close_event: typing.Final[asyncio.Event] #: The current session ID, if known. #: - #: :type: :obj:`str`, optional + #: :type: :obj:`~str`, optional session_id: typing.Optional[str] #: The current sequence number for state synchronization with the API, #: if known. #: - #: :type: :obj:`int`, optional. + #: :type: :obj:`~int`, optional. seq: typing.Optional[int] #: The shard ID. #: - #: :type: :obj:`int` + #: :type: :obj:`~int` shard_id: typing.Final[int] #: The number of shards in use for the bot. #: - #: :type: :obj:`int` + #: :type: :obj:`~int` shard_count: typing.Final[int] #: The API version to use on Discord. #: - #: :type: :obj:`int` + #: :type: :obj:`~int` version: typing.Final[int] def __init__( @@ -396,7 +396,7 @@ def uptime(self) -> datetime.timedelta: Returns ------- - :obj:`datetime.timedelta` + :obj:`~datetime.timedelta` The amount of time the connection has been running for. If it isn't running, this will always return ``0`` seconds. """ @@ -409,7 +409,7 @@ def is_connected(self) -> bool: Returns ------- - :obj:`bool` + :obj:`~bool` True if this gateway client is actively connected to something, or False if it is not running. """ @@ -419,14 +419,14 @@ def is_connected(self) -> bool: def intents(self) -> typing.Optional[codes.GatewayIntent]: """Intents being used. - If this is :obj:`None`, no intent usage was being + If this is :obj:`~None`, no intent usage was being used on this shard. On V6 this would be regular usage as prior to the intents change in January 2020. If on V7, you just won't be able to connect at all to the gateway. Returns ------- - :obj:`hikari.net.codes.GatewayIntent`, optional + :obj:`~hikari.net.codes.GatewayIntent`, optional The intents being used. """ return self._intents @@ -440,7 +440,7 @@ def reconnect_count(self) -> int: Returns ------- - :obj:`int` + :obj:`~int` The amount of times the gateway has reconnected since initialization. """ # 0 disconnects + not is_connected => 0 @@ -458,7 +458,7 @@ def current_presence(self) -> typing.Dict: # noqa: D401 Returns ------- - :obj:`typing.Dict` + :obj:`~typing.Dict` The current presence for the gateway. """ # Make a shallow copy to prevent mutation. @@ -480,23 +480,23 @@ async def request_guild_members(self, guild_id, *guild_ids, **kwargs): Parameters ---------- - guild_id : :obj:`str` + guild_id : :obj:`~str` The first guild to request members for. - *guild_ids : :obj:`str` + *guild_ids : :obj:`~str` Additional guilds to request members for. **kwargs Optional arguments. Keyword Args ------------ - limit : :obj:`int` + limit : :obj:`~int` Limit for the number of members to respond with. Set to ``0`` to be unlimited. - query : :obj:`str` + query : :obj:`~str` An optional string to filter members with. If specified, only members who have a username starting with this string will be returned. - user_ids : :obj:`typing.Sequence` [ :obj:`str` ] + user_ids : :obj:`~typing.Sequence` [ :obj:`~str` ] An optional list of user IDs to return member info about. Note @@ -529,7 +529,7 @@ async def update_presence(self, presence: typing.Dict) -> None: Parameters ---------- - presence : :obj:`typing.Dict` + presence : :obj:`~typing.Dict` The new presence payload to set. """ presence.setdefault("since", None) @@ -546,7 +546,7 @@ async def close(self, close_code: int = 1000) -> None: Parameters ---------- - close_code : :obj:`int` + close_code : :obj:`~int` The close code to use. Defaults to ``1000`` (normal closure). """ if not self.requesting_close_event.is_set(): @@ -565,7 +565,7 @@ async def connect(self, client_session_type=aiohttp.ClientSession) -> None: ---------- client_session_type The client session implementation to use. You generally do not want - to change this from the default, which is :obj:`aiohttp.ClientSession`. + to change this from the default, which is :obj:`~aiohttp.ClientSession`. """ if self.is_connected: raise RuntimeError("Already connected") diff --git a/hikari/net/user_agent.py b/hikari/net/user_agent.py index b320c0b5dd..a36154ceea 100644 --- a/hikari/net/user_agent.py +++ b/hikari/net/user_agent.py @@ -45,7 +45,7 @@ class UserAgent(metaclass=meta.SingletonMeta): #: ------- #: ``"hikari 1.0.1"`` #: - #: :type: :obj:`typing.Final` [ :obj:`str` ] + #: :type: :obj:`~typing.Final` [ :obj:`~str` ] library_version: typing.Final[str] #: The platform version. @@ -54,7 +54,7 @@ class UserAgent(metaclass=meta.SingletonMeta): #: ------- #: ``"CPython 3.8.2 GCC 9.2.0"`` #: - #: :type: :obj:`typing.Final` [ :obj:`str` ] + #: :type: :obj:`~typing.Final` [ :obj:`~str` ] platform_version: typing.Final[str] #: The operating system type. @@ -63,7 +63,7 @@ class UserAgent(metaclass=meta.SingletonMeta): #: ------- #: ``"Linux-5.4.15-2-MANJARO-x86_64-with-glibc2.2.5"`` #: - #: :type: :obj:`typing.Final` [ :obj:`str` ] + #: :type: :obj:`~typing.Final` [ :obj:`~str` ] system_type: typing.Final[str] #: The Hikari-specific user-agent to use in HTTP connections to Discord. @@ -72,7 +72,7 @@ class UserAgent(metaclass=meta.SingletonMeta): #: ------- #: ``"DiscordBot (https://gitlab.com/nekokatt/hikari; 1.0.1; Nekokatt) CPython 3.8.2 GCC 9.2.0 Linux"`` #: - #: :type: :obj:`typing.Final` [ :obj:`str` ] + #: :type: :obj:`~typing.Final` [ :obj:`~str` ] user_agent: typing.Final[str] def __init__(self): @@ -103,7 +103,7 @@ def websocket_triplet(self) -> typing.Dict[str, str]: # noqa: D401 Returns ------- - :obj:`typing.Dict` [ :obj:`str`, :obj:`str` ] + :obj:`~typing.Dict` [ :obj:`~str`, :obj:`~str` ] The object to send to Discord representing device info when IDENTIFYing with the gateway. """ diff --git a/hikari/oauth2.py b/hikari/oauth2.py index dcb89299a9..a2bc68a4f7 100644 --- a/hikari/oauth2.py +++ b/hikari/oauth2.py @@ -59,27 +59,27 @@ class OwnConnection(entities.HikariEntity, entities.Deserializable): #: Seeing as this is a third party ID, it will not be a snowflake. #: #: - #: :type: :obj:`str` + #: :type: :obj:`~str` id: str = marshaller.attrib(deserializer=str) #: The username of the connected account. #: - #: :type: :obj:`str` + #: :type: :obj:`~str` name: str = marshaller.attrib(deserializer=str) #: The type of service this connection is for. #: - #: :type: :obj:`str` + #: :type: :obj:`~str` type: str = marshaller.attrib(deserializer=str) #: Whether the connection has been revoked. #: - #: :type: :obj:`bool` + #: :type: :obj:`~bool` is_revoked: bool = marshaller.attrib(raw_name="revoked", deserializer=bool, if_undefined=False, default=False) #: A sequence of the partial guild integration objects this connection has. #: - #: :type: :obj:`typing.Sequence` [ :obj:`hikari.guilds.PartialGuildIntegration` ] + #: :type: :obj:`~typing.Sequence` [ :obj:`~hikari.guilds.PartialGuildIntegration` ] integrations: typing.Sequence[guilds.PartialGuildIntegration] = marshaller.attrib( deserializer=lambda payload: [ guilds.PartialGuildIntegration.deserialize(integration) for integration in payload @@ -90,23 +90,23 @@ class OwnConnection(entities.HikariEntity, entities.Deserializable): #: Whether the connection has been verified. #: - #: :type: :obj:`bool` + #: :type: :obj:`~bool` is_verified: bool = marshaller.attrib(raw_name="verified", deserializer=bool) #: Whether friends should be added based on this connection. #: - #: :type: :obj:`bool` + #: :type: :obj:`~bool` is_friend_syncing: bool = marshaller.attrib(raw_name="friend_sync", deserializer=bool) #: Whether activities related to this connection will be shown in the #: user's presence updates. #: - #: :type: :obj:`bool` + #: :type: :obj:`~bool` is_showing_activity: bool = marshaller.attrib(raw_name="show_activity", deserializer=bool) #: The visibility of the connection. #: - #: :type: :obj:`ConnectionVisibility` + #: :type: :obj:`~ConnectionVisibility` visibility: ConnectionVisibility = marshaller.attrib(deserializer=ConnectionVisibility) @@ -117,12 +117,12 @@ class OwnGuild(guilds.PartialGuild): #: Whether the current user owns this guild. #: - #: :type: :obj:`bool` + #: :type: :obj:`~bool` is_owner: bool = marshaller.attrib(raw_name="owner", deserializer=bool) #: The guild level permissions that apply to the current user or bot. #: - #: :type: :obj:`hikari.permissions.Permission` + #: :type: :obj:`~hikari.permissions.Permission` my_permissions: permissions.Permission = marshaller.attrib( raw_name="permissions", deserializer=permissions.Permission ) @@ -146,23 +146,23 @@ class TeamMember(entities.HikariEntity, entities.Deserializable): #: The state of this user's membership. #: - #: :type: :obj:`TeamMembershipState` + #: :type: :obj:`~TeamMembershipState` membership_state: TeamMembershipState = marshaller.attrib(deserializer=TeamMembershipState) #: This member's permissions within a team. #: Will always be ``["*"]`` until Discord starts using this. #: - #: :type: :obj:`typing.Set` [ :obj:`str` ] + #: :type: :obj:`~typing.Set` [ :obj:`~str` ] permissions: typing.Set[str] = marshaller.attrib(deserializer=set) #: The ID of the team this member belongs to. #: - #: :type: :obj:`hikari.snowflakes.Snowflake` + #: :type: :obj:`~hikari.snowflakes.Snowflake` team_id: snowflakes.Snowflake = marshaller.attrib(deserializer=snowflakes.Snowflake.deserialize) #: The user object of this team member. #: - #: :type: :obj:`hikari.users.User` + #: :type: :obj:`~hikari.users.User` user: users.User = marshaller.attrib(deserializer=users.User.deserialize) @@ -173,19 +173,19 @@ class Team(snowflakes.UniqueEntity, entities.Deserializable): #: The hash of this team's icon, if set. #: - #: :type: :obj:`str`, optional + #: :type: :obj:`~str`, optional icon_hash: typing.Optional[str] = marshaller.attrib(raw_name="icon", deserializer=str) #: The member's that belong to this team. #: - #: :type: :obj:`typing.Mapping` [ :obj:`hikari.snowflakes.Snowflake`, :obj:`TeamMember` ] + #: :type: :obj:`~typing.Mapping` [ :obj:`~hikari.snowflakes.Snowflake`, :obj:`~TeamMember` ] members: typing.Mapping[snowflakes.Snowflake, TeamMember] = marshaller.attrib( deserializer=lambda members: {m.user.id: m for m in map(TeamMember.deserialize, members)} ) #: The ID of this team's owner. #: - #: :type: :obj:`hikari.snowflakes.Snowflake` + #: :type: :obj:`~hikari.snowflakes.Snowflake` owner_user_id: snowflakes.Snowflake = marshaller.attrib(deserializer=snowflakes.Snowflake.deserialize) @property @@ -198,21 +198,21 @@ def format_icon_url(self, fmt: str = "png", size: int = 4096) -> typing.Optional Parameters ---------- - fmt : :obj:`str` + fmt : :obj:`~str` The format to use for this URL, defaults to ``png``. Supports ``png``, ``jpeg``, ``jpg`` and ``webp``. - size : :obj:`int` + size : :obj:`~int` The size to set for the URL, defaults to ``4096``. Can be any power of two between 16 and 4096 inclusive. Returns ------- - :obj:`str`, optional + :obj:`~str`, optional The string URL. Raises ------ - :obj:`ValueError` + :obj:`~ValueError` If ``size`` is not a power of two or not between 16 and 4096. """ if self.icon_hash: @@ -227,7 +227,7 @@ class ApplicationOwner(users.User): #: This user's flags. #: - #: :type: :obj:`hikari.users.UserFlag` + #: :type: :obj:`~hikari.users.UserFlag` flags: int = marshaller.attrib(deserializer=users.UserFlag) @property @@ -243,67 +243,67 @@ class Application(snowflakes.UniqueEntity, entities.Deserializable): #: The name of this application. #: - #: :type: :obj:`str` + #: :type: :obj:`~str` name: str = marshaller.attrib(deserializer=str) #: The description of this application, will be an empty string if unset. #: - #: :type: :obj:`str` + #: :type: :obj:`~str` description: str = marshaller.attrib(deserializer=str) #: Whether the bot associated with this application is public. - #: Will be :obj:`None` if this application doesn't have an associated bot. + #: Will be :obj:`~None` if this application doesn't have an associated bot. #: - #: :type: :obj:`bool`, optional + #: :type: :obj:`~bool`, optional is_bot_public: typing.Optional[bool] = marshaller.attrib( raw_name="bot_public", deserializer=bool, if_undefined=None, default=None ) #: Whether the bot associated with this application is requiring code grant - #: for invites. Will be :obj:`None` if this application doesn't have a bot. + #: for invites. Will be :obj:`~None` if this application doesn't have a bot. #: - #: :type: :obj:`bool`, optional + #: :type: :obj:`~bool`, optional is_bot_code_grant_required: typing.Optional[bool] = marshaller.attrib( raw_name="bot_require_code_grant", deserializer=bool, if_undefined=None, default=None ) #: The object of this application's owner. - #: This should always be :obj:`None` in application objects retrieved + #: This should always be :obj:`~None` in application objects retrieved #: outside Discord's oauth2 flow. #: - #: :type: :obj:`ApplicationOwner`, optional + #: :type: :obj:`~ApplicationOwner`, optional owner: typing.Optional[ApplicationOwner] = marshaller.attrib( deserializer=ApplicationOwner.deserialize, if_undefined=None, default=None ) #: A collection of this application's rpc origin URLs, if rpc is enabled. #: - #: :type: :obj:`typing.Set` [ :obj:`str` ], optional + #: :type: :obj:`~typing.Set` [ :obj:`~str` ], optional rpc_origins: typing.Optional[typing.Set[str]] = marshaller.attrib(deserializer=set, if_undefined=None, default=None) #: This summary for this application's primary SKU if it's sold on Discord. #: Will be an empty string if unset. #: - #: :type: :obj:`str` + #: :type: :obj:`~str` summary: str = marshaller.attrib(deserializer=str) #: The base64 encoded key used for the GameSDK's ``GetTicket``. #: - #: :type: :obj:`bytes`, optional + #: :type: :obj:`~bytes`, optional verify_key: typing.Optional[bytes] = marshaller.attrib( deserializer=lambda key: bytes(key, "utf-8"), if_undefined=None, default=None ) #: The hash of this application's icon, if set. #: - #: :type: :obj:`str`, optional + #: :type: :obj:`~str`, optional icon_hash: typing.Optional[str] = marshaller.attrib( raw_name="icon", deserializer=str, if_undefined=None, default=None ) #: This application's team if it belongs to one. #: - #: :type: :obj:`Team`, optional + #: :type: :obj:`~Team`, optional team: typing.Optional[Team] = marshaller.attrib( deserializer=Team.deserialize, if_undefined=None, if_none=None, default=None ) @@ -311,14 +311,14 @@ class Application(snowflakes.UniqueEntity, entities.Deserializable): #: The ID of the guild this application is linked to #: if it's sold on Discord. #: - #: :type: :obj:`hikari.snowflakes.Snowflake`, optional + #: :type: :obj:`~hikari.snowflakes.Snowflake`, optional guild_id: typing.Optional[snowflakes.Snowflake] = marshaller.attrib( deserializer=snowflakes.Snowflake.deserialize, if_undefined=None, default=None ) #: The ID of the primary "Game SKU" of a game that's sold on Discord. #: - #: :type: :obj:`hikari.snowflakes.Snowflake`, optional + #: :type: :obj:`~hikari.snowflakes.Snowflake`, optional primary_sku_id: typing.Optional[snowflakes.Snowflake] = marshaller.attrib( deserializer=snowflakes.Snowflake.deserialize, if_undefined=None, default=None ) @@ -326,12 +326,12 @@ class Application(snowflakes.UniqueEntity, entities.Deserializable): #: The URL slug that links to this application's store page #: if it's sold on Discord. #: - #: :type: :obj:`str`, optional + #: :type: :obj:`~str`, optional slug: typing.Optional[str] = marshaller.attrib(deserializer=str, if_undefined=None, default=None) #: The hash of this application's cover image on it's store, if set. #: - #: :type: :obj:`str`, optional + #: :type: :obj:`~str`, optional cover_image_hash: typing.Optional[str] = marshaller.attrib( raw_name="cover_image", deserializer=str, if_undefined=None, default=None ) @@ -346,21 +346,21 @@ def format_icon_url(self, fmt: str = "png", size: int = 4096) -> typing.Optional Parameters ---------- - fmt : :obj:`str` + fmt : :obj:`~str` The format to use for this URL, defaults to ``png``. Supports ``png``, ``jpeg``, ``jpg`` and ```webp``. - size : :obj:`int` + size : :obj:`~int` The size to set for the URL, defaults to ``4096``. Can be any power of two between 16 and 4096. Returns ------- - :obj:`str`, optional + :obj:`~str`, optional The string URL. Raises ------ - :obj:`ValueError` + :obj:`~ValueError` If ``size`` is not a power of two or not between 16 and 4096. """ if self.icon_hash: @@ -377,21 +377,21 @@ def format_cover_image_url(self, fmt: str = "png", size: int = 4096) -> typing.O Parameters ---------- - fmt : :obj:`str` + fmt : :obj:`~str` The format to use for this URL, defaults to ``png``. Supports ``png``, ``jpeg``, ``jpg`` and ``webp``. - size : :obj:`int` + size : :obj:`~int` The size to set for the URL, defaults to ``4096``. Can be any power of two between 16 and 4096. Returns ------- - :obj:`str`, optional + :obj:`~str`, optional The string URL. Raises ------ - :obj:`ValueError` + :obj:`~ValueError` If ``size`` is not a power of two or not between 16 and 4096. """ if self.cover_image_hash: diff --git a/hikari/snowflakes.py b/hikari/snowflakes.py index f5ceba2737..184714e425 100644 --- a/hikari/snowflakes.py +++ b/hikari/snowflakes.py @@ -39,14 +39,14 @@ class Snowflake(entities.HikariEntity, typing.SupportsInt): """A concrete representation of a unique identifier for an object on Discord. - This object can be treated as a regular :obj:`int` for most purposes. + This object can be treated as a regular :obj:`~int` for most purposes. """ __slots__ = ("_value",) #: The integer value of this ID. #: - #: :type: :obj:`int` + #: :type: :obj:`~int` _value: int # noinspection PyMissingConstructor @@ -98,7 +98,7 @@ def serialize(self) -> str: @classmethod def deserialize(cls, value: str) -> "Snowflake": - """Take a :obj:`str` ID and convert it into a Snowflake object.""" + """Take a :obj:`~str` ID and convert it into a Snowflake object.""" return cls(value) @classmethod @@ -119,7 +119,7 @@ class UniqueEntity(entities.HikariEntity, typing.SupportsInt): #: The ID of this entity. #: - #: :type: :obj:`Snowflake` + #: :type: :obj:`~Snowflake` id: Snowflake = marshaller.attrib(hash=True, eq=True, repr=True, deserializer=Snowflake, serializer=str) def __int__(self): diff --git a/hikari/state/event_dispatchers.py b/hikari/state/event_dispatchers.py index 5996fa1f44..4df94a73dc 100644 --- a/hikari/state/event_dispatchers.py +++ b/hikari/state/event_dispatchers.py @@ -67,14 +67,14 @@ def add_listener(self, event_type: typing.Type[EventT], callback: EventCallbackT Parameters ---------- - event_type : :obj:`typing.Type` [ :obj:`hikari.events.HikariEvent` ] + event_type : :obj:`~typing.Type` [ :obj:`~hikari.events.HikariEvent` ] The event to register to. callback : ``async def callback(event: HikariEvent) -> ...`` The event callback to invoke when this event is fired. Raises ------ - :obj:`TypeError` + :obj:`~TypeError` If ``coroutine_function`` is not a coroutine. """ @@ -86,7 +86,7 @@ def remove_listener(self, event_type: typing.Type[EventT], callback: EventCallba Parameters ---------- - event_type : :obj:`typing.Type` [ :obj:`hikari.events.HikariEvent` ] + event_type : :obj:`~typing.Type` [ :obj:`~hikari.events.HikariEvent` ] The type of event to remove the callback from. callback : ``async def callback(event: HikariEvent) -> ...`` The event callback to invoke when this event is fired. @@ -100,12 +100,12 @@ def wait_for( Parameters ---------- - event_type : :obj:`typing.Type` [ :obj:`hikari.events.HikariEvent` ] + event_type : :obj:`~typing.Type` [ :obj:`~hikari.events.HikariEvent` ] The name of the event to wait for. - timeout : :obj:`float`, optional + timeout : :obj:`~float`, optional The timeout to wait for before cancelling and raising an - :obj:`asyncio.TimeoutError` instead. If this is :obj:`None`, this - will wait forever. Care must be taken if you use :obj:`None` as this + :obj:`~asyncio.TimeoutError` instead. If this is :obj:`~None`, this + will wait forever. Care must be taken if you use :obj:`~None` as this may leak memory if you do this from an event listener that gets repeatedly called. If you want to do this, you should consider using an event listener instead of this function. @@ -117,7 +117,7 @@ def wait_for( Returns ------- - :obj:`asyncio.Future`: + :obj:`~asyncio.Future`: A future to await. When the given event is matched, this will be completed with the corresponding event body. @@ -165,7 +165,7 @@ def on(self, event_type=None): Parameters ---------- - event_type : :obj:`typing.Type` [ :obj:`hikari.events.HikariEvent` ], optional + event_type : :obj:`~typing.Type` [ :obj:`~hikari.events.HikariEvent` ], optional The event type to register the produced decorator to. If this is not specified, then the given function is used instead and the type hint of the first argument is considered. If no type hint is present @@ -230,12 +230,12 @@ def dispatch_event(self, event: events.HikariEvent) -> more_asyncio.Future[typin Parameters ---------- - event : :obj:`hikari.events.HikariEvent` + event : :obj:`~hikari.events.HikariEvent` The event to dispatch. Returns ------- - :obj:`asyncio.Future`: + :obj:`~asyncio.Future`: a future that can be optionally awaited if you need to wait for all listener callbacks and waiters to be processed. If this is not awaited, the invocation is invoked soon on the current event loop. @@ -259,7 +259,7 @@ class EventDispatcherImpl(EventDispatcher): #: The logger used to write log messages. #: - #: :type: :obj:`logging.Logger` + #: :type: :obj:`~logging.Logger` logger: logging.Logger def __init__(self) -> None: @@ -284,14 +284,14 @@ def add_listener(self, event_type: typing.Type[events.HikariEvent], callback: Ev Parameters ---------- - event_type : :obj:`typing.Type` [ :obj:`hikari.events.HikariEvent` ] + event_type : :obj:`~typing.Type` [ :obj:`~hikari.events.HikariEvent` ] The event to register to. callback : ``async def callback(event: HikariEvent) -> ...`` The event callback to invoke when this event is fired. Raises ------ - :obj:`TypeError` + :obj:`~TypeError` If ``coroutine_function`` is not a coroutine. """ assertions.assert_that( @@ -308,7 +308,7 @@ def remove_listener(self, event_type: typing.Type[EventT], callback: EventCallba Parameters ---------- - event_type : :obj:`typing.Type` [ :obj:`hikari.events.HikariEvent` ] + event_type : :obj:`~typing.Type` [ :obj:`~hikari.events.HikariEvent` ] The type of event to remove the callback from. callback : ``async def callback(event: HikariEvent) -> ...`` The event callback to remove. @@ -326,12 +326,12 @@ def dispatch_event(self, event: events.HikariEvent): Parameters ---------- - event : :obj:`hikari.events.HikariEvent` + event : :obj:`~hikari.events.HikariEvent` The event to dispatch. Returns ------- - :obj:`asyncio.Future` + :obj:`~asyncio.Future` This may be a gathering future of the callbacks to invoke, or it may be a completed future object. Regardless, this result will be scheduled on the event loop automatically, and does not need to be @@ -402,17 +402,17 @@ def handle_exception( This allows users to override this with a custom implementation if desired. This implementation will check to see if the event that triggered the - exception is an :obj:`hikari.events.ExceptionEvent`. If this - exception was caused by the :obj:`hikari.events.ExceptionEvent`, + exception is an :obj:`~hikari.events.ExceptionEvent`. If this + exception was caused by the :obj:`~hikari.events.ExceptionEvent`, then nothing is dispatched (thus preventing an exception handler recursively - re-triggering itself). Otherwise, an :obj:`hikari.events.ExceptionEvent` + re-triggering itself). Otherwise, an :obj:`~hikari.events.ExceptionEvent` is dispatched. Parameters ---------- - exception: :obj:`Exception` + exception: :obj:`~Exception` The exception that triggered this call. - event: :obj:`hikari.events.HikariEvent` + event: :obj:`~hikari.events.HikariEvent` The event that was being dispatched. callback The callback that threw the exception. @@ -445,11 +445,11 @@ def wait_for( Parameters ---------- - event_type : :obj:`typing.Type` [ :obj:`hikari.events.HikariEvent` ] + event_type : :obj:`~typing.Type` [ :obj:`~hikari.events.HikariEvent` ] The name of the event to wait for. - timeout : :obj:`float`, optional + timeout : :obj:`~float`, optional The timeout to wait for before cancelling and raising an - :obj:`asyncio.TimeoutError` instead. If this is `None`, this will + :obj:`~asyncio.TimeoutError` instead. If this is `None`, this will wait forever. Care must be taken if you use `None` as this may leak memory if you do this from an event listener that gets repeatedly called. If you want to do this, you should consider @@ -462,7 +462,7 @@ def wait_for( Returns ------- - :obj:`asyncio.Future` + :obj:`~asyncio.Future` A future that when awaited will provide a the arguments passed to the first matching event. If no arguments are passed to the event, then `None` is the result. If one argument is passed to the event, @@ -471,7 +471,7 @@ def wait_for( Notes ----- - Awaiting this result will raise an :obj:`asyncio.TimeoutError` if the + Awaiting this result will raise an :obj:`~asyncio.TimeoutError` if the timeout is hit and no match is found. If the predicate throws any exception, this is raised immediately. """ diff --git a/hikari/state/event_managers.py b/hikari/state/event_managers.py index 97a99142da..ee624672cc 100644 --- a/hikari/state/event_managers.py +++ b/hikari/state/event_managers.py @@ -75,7 +75,7 @@ class EventManager(typing.Generic[EventDispatcherT], raw_event_consumers.RawEven """Abstract definition of the components for an event system for a bot. The class itself inherits from - :obj:`hikari.state.raw_event_consumers.RawEventConsumer` (which allows + :obj:`~hikari.state.raw_event_consumers.RawEventConsumer` (which allows it to provide the ability to transform a raw payload into an event object). This is designed as a basis to enable transformation of raw incoming events @@ -85,21 +85,21 @@ class EventManager(typing.Generic[EventDispatcherT], raw_event_consumers.RawEven Parameters ---------- - event_dispatcher_impl: :obj:`hikari.state.event_dispatchers.EventDispatcher`, optional + event_dispatcher_impl: :obj:`~hikari.state.event_dispatchers.EventDispatcher`, optional An implementation of event dispatcher that will store individual events and manage dispatching them after this object creates them. If - :obj:`None`, then a default implementation is chosen. + :obj:`~None`, then a default implementation is chosen. Notes ----- This object will detect internal event mapper functions by looking for - coroutine functions wrapped with :obj:`raw_event_mapper`. + coroutine functions wrapped with :obj:`~raw_event_mapper`. These methods are expected to have the following parameters: - shard_obj: :obj:`hikari.clients.shard_clients.ShardClient` + shard_obj: :obj:`~hikari.clients.shard_clients.ShardClient` The shard client that emitted the event. - payload: :obj:`typing.Any` + payload: :obj:`~typing.Any` The received payload. This is expected to be a JSON-compatible type. For example, if you want to provide an implementation that can consume @@ -159,11 +159,11 @@ def process_raw_event( Parameters ---------- - shard_client_obj: :obj:`hikari.clients.shard_clients.ShardClient` + shard_client_obj: :obj:`~hikari.clients.shard_clients.ShardClient` The shard that triggered this event. - name : :obj:`str` + name : :obj:`~str` The raw event name. - payload : :obj:`dict` + payload : :obj:`~dict` The payload that was sent. """ try: diff --git a/hikari/state/raw_event_consumers.py b/hikari/state/raw_event_consumers.py index 28ae67273a..15a8d2083b 100644 --- a/hikari/state/raw_event_consumers.py +++ b/hikari/state/raw_event_consumers.py @@ -49,10 +49,10 @@ def process_raw_event( Parameters ---------- - shard_client_obj : :obj:`hikari.clients.shard_clients.ShardClient` + shard_client_obj : :obj:`~hikari.clients.shard_clients.ShardClient` The client for the shard that received the event. - name : :obj:`str` + name : :obj:`~str` The raw event name. - payload : :obj:`typing.Any` + payload : :obj:`~typing.Any` The raw event payload. Will be a JSON-compatible type. """ diff --git a/hikari/users.py b/hikari/users.py index 304d57ed17..3fdb1584ca 100644 --- a/hikari/users.py +++ b/hikari/users.py @@ -98,37 +98,37 @@ class User(snowflakes.UniqueEntity, entities.Deserializable): #: This user's discriminator. #: - #: :type: :obj:`str` + #: :type: :obj:`~str` discriminator: str = marshaller.attrib(deserializer=str) #: This user's username. #: - #: :type: :obj:`str` + #: :type: :obj:`~str` username: str = marshaller.attrib(deserializer=str) #: This user's avatar hash, if set. #: - #: :type: :obj:`str`, optional + #: :type: :obj:`~str`, optional avatar_hash: typing.Optional[str] = marshaller.attrib(raw_name="avatar", deserializer=str, if_none=None) #: Whether this user is a bot account. #: - #: :type: :obj:`bool` + #: :type: :obj:`~bool` is_bot: bool = marshaller.attrib(raw_name="bot", deserializer=bool, if_undefined=False, default=False) #: Whether this user is a system account. #: - #: :type: :obj:`bool` + #: :type: :obj:`~bool` is_system: bool = marshaller.attrib(raw_name="system", deserializer=bool, if_undefined=False, default=False) #: The public flags for this user. #: #: Note #: ---- - #: This will be :obj:`None` if it's a webhook user. + #: This will be :obj:`~None` if it's a webhook user. #: #: - #: :type: :obj:`UserFlag`, optional + #: :type: :obj:`~UserFlag`, optional flags: typing.Optional[UserFlag] = marshaller.attrib( raw_name="public_flags", deserializer=UserFlag, if_undefined=None, default=None ) @@ -143,24 +143,24 @@ def format_avatar_url(self, fmt: typing.Optional[str] = None, size: int = 4096) Parameters ---------- - fmt : :obj:`str` + fmt : :obj:`~str` The format to use for this URL, defaults to ``png`` or ``gif``. Supports ``png``, ``jpeg``, ``jpg``, ``webp`` and ``gif`` (when animated). Will be ignored for default avatars which can only be ``png``. - size : :obj:`int` + size : :obj:`~int` The size to set for the URL, defaults to ``4096``. Can be any power of two between 16 and 4096. Will be ignored for default avatars. Returns ------- - :obj:`str` + :obj:`~str` The string URL. Raises ------ - :obj:`ValueError` + :obj:`~ValueError` If ``size`` is not a power of two or not between 16 and 4096. """ if not self.avatar_hash: @@ -184,39 +184,39 @@ class MyUser(User): #: Whether the user's account has 2fa enabled. #: - #: :type: :obj:`bool` + #: :type: :obj:`~bool` is_mfa_enabled: bool = marshaller.attrib(raw_name="mfa_enabled", deserializer=bool) #: The user's set language. This is not provided by the ``READY`` event. #: - #: :type: :obj:`str`, optional + #: :type: :obj:`~str`, optional locale: typing.Optional[str] = marshaller.attrib(deserializer=str, if_none=None, if_undefined=None, default=None) #: Whether the email for this user's account has been verified. - #: Will be :obj:`None` if retrieved through the oauth2 flow without the + #: Will be :obj:`~None` if retrieved through the oauth2 flow without the #: ``email`` scope. #: - #: :type: :obj:`bool`, optional + #: :type: :obj:`~bool`, optional is_verified: typing.Optional[bool] = marshaller.attrib( raw_name="verified", deserializer=bool, if_undefined=None, default=None ) #: The user's set email. - #: Will be :obj:`None` if retrieved through the oauth2 flow without the + #: Will be :obj:`~None` if retrieved through the oauth2 flow without the #: ``email`` scope and for bot users. #: - #: :type: :obj:`str`, optional + #: :type: :obj:`~str`, optional email: typing.Optional[str] = marshaller.attrib(deserializer=str, if_undefined=None, if_none=None, default=None) #: This user account's flags. #: - #: :type: :obj:`UserFlag` + #: :type: :obj:`~UserFlag` flags: UserFlag = marshaller.attrib(deserializer=UserFlag) #: The type of Nitro Subscription this user account had. - #: This will always be :obj:`None` for bots. + #: This will always be :obj:`~None` for bots. #: - #: :type: :obj:`PremiumType`, optional + #: :type: :obj:`~PremiumType`, optional premium_type: typing.Optional[PremiumType] = marshaller.attrib( deserializer=PremiumType, if_undefined=None, default=None ) diff --git a/hikari/voices.py b/hikari/voices.py index 5fcbebe46e..a8ab1f4e16 100644 --- a/hikari/voices.py +++ b/hikari/voices.py @@ -36,65 +36,65 @@ class VoiceState(entities.HikariEntity, entities.Deserializable): #: The ID of the guild this voice state is in, if applicable. #: - #: :type: :obj:`hikari.snowflakes.Snowflake`, optional + #: :type: :obj:`~hikari.snowflakes.Snowflake`, optional guild_id: typing.Optional[snowflakes.Snowflake] = marshaller.attrib( deserializer=snowflakes.Snowflake.deserialize, if_undefined=None, default=None ) - #: The ID of the channel this user is connected to, will be :obj:`None` if + #: The ID of the channel this user is connected to, will be :obj:`~None` if #: they are leaving voice. #: - #: :type: :obj:`hikari.snowflakes.Snowflake`, optional + #: :type: :obj:`~hikari.snowflakes.Snowflake`, optional channel_id: typing.Optional[snowflakes.Snowflake] = marshaller.attrib( deserializer=snowflakes.Snowflake.deserialize, if_none=None ) #: The ID of the user this voice state is for. #: - #: :type: :obj:`hikari.snowflakes.Snowflake` + #: :type: :obj:`~hikari.snowflakes.Snowflake` user_id: snowflakes.Snowflake = marshaller.attrib(deserializer=snowflakes.Snowflake.deserialize) #: The guild member this voice state is for if the voice state is in a #: guild. #: - #: :type: :obj:`hikari.guilds.GuildMember`, optional + #: :type: :obj:`~hikari.guilds.GuildMember`, optional member: typing.Optional[guilds.GuildMember] = marshaller.attrib( deserializer=guilds.GuildMember.deserialize, if_undefined=None, default=None ) #: The ID of this voice state's session. #: - #: :type: :obj:`str` + #: :type: :obj:`~str` session_id: str = marshaller.attrib(deserializer=str) #: Whether this user is deafened by the guild. #: - #: :type: :obj:`bool` + #: :type: :obj:`~bool` is_guild_deafened: bool = marshaller.attrib(raw_name="deaf", deserializer=bool) #: Whether this user is muted by the guild. #: - #: :type: :obj:`bool` + #: :type: :obj:`~bool` is_guild_muted: bool = marshaller.attrib(raw_name="mute", deserializer=bool) #: Whether this user is deafened by their client. #: - #: :type: :obj:`bool` + #: :type: :obj:`~bool` is_self_deafened: bool = marshaller.attrib(raw_name="self_deaf", deserializer=bool) #: Whether this user is muted by their client. #: - #: :type: :obj:`bool` + #: :type: :obj:`~bool` is_self_muted: bool = marshaller.attrib(raw_name="self_mute", deserializer=bool) #: Whether this user is streaming using "Go Live". #: - #: :type: :obj:`bool` + #: :type: :obj:`~bool` is_streaming: bool = marshaller.attrib(raw_name="self_stream", deserializer=bool, if_undefined=False, default=False) #: Whether this user is muted by the current user. #: - #: :type: :obj:`bool` + #: :type: :obj:`~bool` is_suppressed: bool = marshaller.attrib(raw_name="suppress", deserializer=bool) @@ -105,30 +105,30 @@ class VoiceRegion(entities.HikariEntity, entities.Deserializable): #: The ID of this region #: - #: :type: :obj:`str` + #: :type: :obj:`~str` id: str = marshaller.attrib(deserializer=str) #: The name of this region #: - #: :type: :obj:`str` + #: :type: :obj:`~str` name: str = marshaller.attrib(deserializer=str) #: Whether this region is vip-only. #: - #: :type: :obj:`bool` + #: :type: :obj:`~bool` is_vip: bool = marshaller.attrib(raw_name="vip", deserializer=bool) #: Whether this region's server is closest to the current user's client. #: - #: :type: :obj:`bool` + #: :type: :obj:`~bool` is_optimal_location: bool = marshaller.attrib(raw_name="optimal", deserializer=bool) #: Whether this region is deprecated. #: - #: :type: :obj:`bool` + #: :type: :obj:`~bool` is_deprecated: bool = marshaller.attrib(raw_name="deprecated", deserializer=bool) #: Whether this region is custom (e.g. used for events). #: - #: :type: :obj:`bool` + #: :type: :obj:`~bool` is_custom: bool = marshaller.attrib(raw_name="custom", deserializer=bool) diff --git a/hikari/webhooks.py b/hikari/webhooks.py index e18652b26f..56e133f6e8 100644 --- a/hikari/webhooks.py +++ b/hikari/webhooks.py @@ -53,42 +53,42 @@ class Webhook(snowflakes.UniqueEntity, entities.Deserializable): #: The type of the webhook. #: - #: :type: :obj:`WebhookType` + #: :type: :obj:`~WebhookType` type: WebhookType = marshaller.attrib(deserializer=WebhookType) #: The guild ID of the webhook. #: - #: :type: :obj:`hikari.snowflakes.Snowflake`, optional + #: :type: :obj:`~hikari.snowflakes.Snowflake`, optional guild_id: typing.Optional[snowflakes.Snowflake] = marshaller.attrib( deserializer=snowflakes.Snowflake.deserialize, if_undefined=None, default=None ) #: The channel ID this webhook is for. #: - #: :type: :obj:`hikari.snowflakes.Snowflake` + #: :type: :obj:`~hikari.snowflakes.Snowflake` channel_id: snowflakes.Snowflake = marshaller.attrib(deserializer=snowflakes.Snowflake.deserialize) #: The user that created the webhook #: #: Note #: ---- - #: This will be :obj:`None` when getting a webhook with bot authorization + #: This will be :obj:`~None` when getting a webhook with bot authorization #: rather than the webhook's token. #: #: - #: :type: :obj:`hikari.users.User`, optional + #: :type: :obj:`~hikari.users.User`, optional user: typing.Optional[users.User] = marshaller.attrib( deserializer=users.User.deserialize, if_undefined=None, default=None ) #: The default name of the webhook. #: - #: :type: :obj:`str`, optional + #: :type: :obj:`~str`, optional name: typing.Optional[str] = marshaller.attrib(deserializer=str, if_none=None) #: The default avatar hash of the webhook. #: - #: :type: :obj:`str`, optional + #: :type: :obj:`~str`, optional avatar_hash: typing.Optional[str] = marshaller.attrib(raw_name="avatar", deserializer=str, if_none=None) #: The token of the webhook. @@ -98,5 +98,5 @@ class Webhook(snowflakes.UniqueEntity, entities.Deserializable): #: This is only available for Incoming webhooks. #: #: - #: :type: :obj:`str`, optional + #: :type: :obj:`~str`, optional token: typing.Optional[str] = marshaller.attrib(deserializer=str, if_undefined=None, default=None) From 797dec66269a4e44127d1df778c452cfe2a5c66e Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Wed, 15 Apr 2020 21:41:58 +0100 Subject: [PATCH 141/922] Fixed formatting --- docs/conf.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index b87bb93276..79e185959d 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -225,8 +225,9 @@ # -- Hacks and formatting --------------------------------------------- + def pretty_signature(app, what, name, obj, options, signature, return_annotation): - if what not in ('function', 'method', 'class'): + if what not in ("function", "method", "class"): return if signature is None: @@ -238,5 +239,5 @@ def pretty_signature(app, what, name, obj, options, signature, return_annotation def setup(app): - app.connect('autodoc-process-signature', pretty_signature) + app.connect("autodoc-process-signature", pretty_signature) app.add_css_file("style.css") From d3a2c9f23c0c54d0eb4e694d578c203f1442dc66 Mon Sep 17 00:00:00 2001 From: Faster Speeding Date: Wed, 15 Apr 2020 03:29:28 +0100 Subject: [PATCH 142/922] Add emoji `is_available` field and mark bool fields as required on guild emoji object. --- hikari/clients/configs.py | 2 +- hikari/embeds.py | 8 ++++---- hikari/emojis.py | 29 +++++++++++++++++------------ hikari/messages.py | 6 +++--- hikari/net/codes.py | 4 ++-- tests/hikari/test_embeds.py | 5 ++++- tests/hikari/test_emojis.py | 3 +++ 7 files changed, 34 insertions(+), 23 deletions(-) diff --git a/hikari/clients/configs.py b/hikari/clients/configs.py index 417cbfe048..50acb42c65 100644 --- a/hikari/clients/configs.py +++ b/hikari/clients/configs.py @@ -334,7 +334,7 @@ class RESTConfig(AIOHTTPConfig, TokenConfig): #: #: Defaults to ``"Bot"`` #: - #: :type: :obj:`~str` + #: :type: :obj:`~str`, optional token_type: typing.Optional[str] = marshaller.attrib( deserializer=str, if_undefined=lambda: "Bot", if_none=None, default="Bot" ) diff --git a/hikari/embeds.py b/hikari/embeds.py index f99cf94b66..3a4f0f77ce 100644 --- a/hikari/embeds.py +++ b/hikari/embeds.py @@ -362,10 +362,10 @@ class Embed(entities.HikariEntity, entities.Deserializable, entities.Serializabl #: The fields of the embed. #: - #: :type: :obj:`~typing.Sequence` [ :obj:`~EmbedField` ], optional - fields: typing.Optional[typing.Sequence[EmbedField]] = marshaller.attrib( + #: :type: :obj:`~typing.Sequence` [ :obj:`~EmbedField` ] + fields: typing.Sequence[EmbedField] = marshaller.attrib( deserializer=lambda fields: [EmbedField.deserialize(f) for f in fields], serializer=lambda fields: [f.serialize() for f in fields], - if_undefined=None, - default=None, + if_undefined=list, + factory=list, ) diff --git a/hikari/emojis.py b/hikari/emojis.py index 33b4ac7d56..66dde55a39 100644 --- a/hikari/emojis.py +++ b/hikari/emojis.py @@ -68,8 +68,11 @@ class UnknownEmoji(Emoji, snowflakes.UniqueEntity): #: Whether the emoji is animated. #: - #: :type: :obj:`~bool` - is_animated: bool = marshaller.attrib( + #: Will be :obj:`None` when received in Message Reaction Remove and Message + #: Reaction Remove Emoji events. + #: + #: :type: :obj:`~bool`, optional + is_animated: typing.Optional[bool] = marshaller.attrib( raw_name="animated", deserializer=bool, if_undefined=False, if_none=None, default=False ) @@ -90,8 +93,8 @@ class GuildEmoji(UnknownEmoji): role_ids: typing.Set[snowflakes.Snowflake] = marshaller.attrib( raw_name="roles", deserializer=lambda roles: {snowflakes.Snowflake.deserialize(r) for r in roles}, - if_undefined=dict, - factory=dict, + if_undefined=set, + factory=set, ) #: The user that created the emoji. @@ -109,17 +112,19 @@ class GuildEmoji(UnknownEmoji): #: Whether this emoji must be wrapped in colons. #: - #: :type: :obj:`~bool`, optional - is_colons_required: typing.Optional[bool] = marshaller.attrib( - raw_name="require_colons", deserializer=bool, if_undefined=None, default=None - ) + #: :type: :obj:`~bool` + is_colons_required: bool = marshaller.attrib(raw_name="require_colons", deserializer=bool) #: Whether the emoji is managed by an integration. #: - #: :type: :obj:`~bool`, optional - is_managed: typing.Optional[bool] = marshaller.attrib( - raw_name="managed", deserializer=bool, if_undefined=None, default=None - ) + #: :type: :obj:`~bool` + is_managed: bool = marshaller.attrib(raw_name="managed", deserializer=bool) + + #: Whether this emoji can currently be used, may be :obj:`False` due to + #: a loss of Sever Boosts on the emoji's guild. + #: + #: :type: :obj:`~bool` + is_available: bool = marshaller.attrib(raw_name="available", deserializer=bool) @property def mention(self) -> str: diff --git a/hikari/messages.py b/hikari/messages.py index 0ec2de382a..a6a3ba24ab 100644 --- a/hikari/messages.py +++ b/hikari/messages.py @@ -323,8 +323,8 @@ class Message(snowflakes.UniqueEntity, entities.Deserializable): channel_mentions: typing.Set[snowflakes.Snowflake] = marshaller.attrib( raw_name="mention_channels", deserializer=lambda channel_mentions: {snowflakes.Snowflake.deserialize(c["id"]) for c in channel_mentions}, - if_undefined=dict, - factory=dict, + if_undefined=set, + factory=set, ) #: The message attachments. @@ -345,7 +345,7 @@ class Message(snowflakes.UniqueEntity, entities.Deserializable): #: #: :type: :obj:`~typing.Sequence` [ :obj:`~Reaction` ] reactions: typing.Sequence[Reaction] = marshaller.attrib( - deserializer=lambda reactions: [Reaction.deserialize(r) for r in reactions], if_undefined=dict, factory=dict + deserializer=lambda reactions: [Reaction.deserialize(r) for r in reactions], if_undefined=list, factory=list ) #: Whether the message is pinned. diff --git a/hikari/net/codes.py b/hikari/net/codes.py index a9d1fa7cc9..7fd08c2bba 100644 --- a/hikari/net/codes.py +++ b/hikari/net/codes.py @@ -317,10 +317,10 @@ class JSONErrorCode(enum.IntEnum): #: Maximum number of guild channels reached (500) MAX_GUILD_CHANNELS_REACHED = 30_013 - #: Maximun number of attachments in a message reached (10) + #: Maximum number of attachments in a message reached (10) MAX_MESSAGE_ATTACHMENTS_REACHED = 30_015 - #: Maximun number of invites reached (10000) + #: Maximum number of invites reached (10000) MAX_INVITES_REACHED = 30_016 #: Unauthorized diff --git a/tests/hikari/test_embeds.py b/tests/hikari/test_embeds.py index d9ae694c35..e74abd097d 100644 --- a/tests/hikari/test_embeds.py +++ b/tests/hikari/test_embeds.py @@ -258,7 +258,7 @@ def test_deserialize( assert embed_obj.author == embeds.EmbedAuthor.deserialize(test_author_payload) assert embed_obj.fields == [embeds.EmbedField.deserialize(test_field_payload)] - def test_serialize(self): + def test_serialize_full_embed(self): embed_obj = embeds.Embed( title="Nyaa me pls >////<", description="Nyan >////<", @@ -284,3 +284,6 @@ def test_serialize(self): "author": {"name": "a name", "url": "https://a-man"}, "fields": [{"name": "aField", "value": "agent69", "inline": True}], } + + def test_serialize_empty_embed(self): + assert embeds.Embed().serialize() == {"fields": []} diff --git a/tests/hikari/test_emojis.py b/tests/hikari/test_emojis.py index 7543f2d7cf..6221f61b83 100644 --- a/tests/hikari/test_emojis.py +++ b/tests/hikari/test_emojis.py @@ -64,6 +64,7 @@ def test_deserialize(self): "id": "12345", "name": "testing", "animated": False, + "available": True, "roles": ["123", "456"], "user": test_user_payload, "require_colons": True, @@ -79,11 +80,13 @@ def test_deserialize(self): assert emoji_obj.user == mock_user assert emoji_obj.is_colons_required is True assert emoji_obj.is_managed is False + assert emoji_obj.is_available is True @pytest.fixture() def mock_guild_emoji_obj(self): return emojis.GuildEmoji( is_animated=False, + is_available=True, id=650573534627758100, name="nyaa", role_ids=[], From ff2278db9059ec89be0ccd4b98b3341a1af598a2 Mon Sep 17 00:00:00 2001 From: davfsa Date: Thu, 16 Apr 2020 14:48:26 +0200 Subject: [PATCH 143/922] Fix BaseChannelEvent deserialization --- hikari/events.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/hikari/events.py b/hikari/events.py index b16e65100a..2d2bd7152f 100644 --- a/hikari/events.py +++ b/hikari/events.py @@ -240,14 +240,14 @@ class BaseChannelEvent(HikariEvent, snowflakes.UniqueEntity, entities.Deserializ #: #: :type: :obj:`~hikari.snowflakes.Snowflake`, optional guild_id: typing.Optional[snowflakes.Snowflake] = marshaller.attrib( - deserializer=snowflakes.Snowflake.deserialize, if_none=None + deserializer=snowflakes.Snowflake.deserialize, if_undefined=None, default=None ) #: The sorting position of this channel, will be relative to the #: :attr:`parent_id` if set. #: #: :type: :obj:`~int`, optional - position: typing.Optional[int] = marshaller.attrib(deserializer=int, if_none=None) + position: typing.Optional[int] = marshaller.attrib(deserializer=int, if_undefined=None, default=None) #: An mapping of the set permission overwrites for this channel, if applicable. #: @@ -256,7 +256,8 @@ class BaseChannelEvent(HikariEvent, snowflakes.UniqueEntity, entities.Deserializ typing.Mapping[snowflakes.Snowflake, channels.PermissionOverwrite] ] = marshaller.attrib( deserializer=lambda overwrites: {o.id: o for o in map(channels.PermissionOverwrite.deserialize, overwrites)}, - if_none=None, + if_undefined=None, + default=None, ) #: The name of this channel, if applicable. From 7d3806acef1c1d0b8b9827417e3ee6fb00025f6f Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Thu, 16 Apr 2020 19:35:02 +0100 Subject: [PATCH 144/922] Fixed 'memory-leak' where singleton classes never got garbage collected --- hikari/internal/meta.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/hikari/internal/meta.py b/hikari/internal/meta.py index 9ae80ab1b4..bfc17c94d4 100644 --- a/hikari/internal/meta.py +++ b/hikari/internal/meta.py @@ -22,6 +22,10 @@ """ __all__ = ["SingletonMeta", "Singleton"] +import typing + +from hikari.internal import more_collections + class SingletonMeta(type): """Metaclass that makes the class a singleton. @@ -50,7 +54,8 @@ class SingletonMeta(type): thread safe. """ - ___instances___ = {} + ___instance_dict_t___ = more_collections.WeakKeyDictionary[typing.Type[typing.Any], typing.Any] + ___instances___: ___instance_dict_t___ = more_collections.WeakKeyDictionary() __slots__ = () def __call__(cls): From 802756c3e106944572159888e2aaba0994fd2c54 Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Thu, 16 Apr 2020 20:08:44 +0100 Subject: [PATCH 145/922] Fixes #299: bot shutdown if event payload decode fails. --- hikari/state/event_managers.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/hikari/state/event_managers.py b/hikari/state/event_managers.py index ee624672cc..fdbe9576f0 100644 --- a/hikari/state/event_managers.py +++ b/hikari/state/event_managers.py @@ -171,5 +171,14 @@ def process_raw_event( except KeyError: self.logger.debug("no handler for event %s is registered", name) else: - event = handler(shard_client_obj, payload) - self.event_dispatcher.dispatch_event(event) + try: + event = handler(shard_client_obj, payload) + except Exception as ex: + self.logger.exception( + "Failed to unmarshal %r event payload. This is likely a bug in Hikari itself. " + "Please contact a library dev or make an issue on the issue tracker for more support.", + name, + exc_info=ex, + ) + else: + self.event_dispatcher.dispatch_event(event) From 58bef7ac1a983961df441fda49114c15b0cc1199 Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Thu, 16 Apr 2020 21:08:05 +0100 Subject: [PATCH 146/922] Amended shard error logging message to remove spurious %s --- hikari/clients/shard_clients.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hikari/clients/shard_clients.py b/hikari/clients/shard_clients.py index b123845b4f..5bb25e56b2 100644 --- a/hikari/clients/shard_clients.py +++ b/hikari/clients/shard_clients.py @@ -415,7 +415,7 @@ async def _keep_alive(self): self.logger.warning("shutting down") return except Exception as ex: - self.logger.debug("propagating unexpected exception %s", exc_info=ex) + self.logger.debug("propagating unexpected exception", exc_info=ex) raise ex async def _spin_up(self) -> asyncio.Task: From 3203a4afcd1bc08bec667b4e500653949199a44d Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Thu, 16 Apr 2020 20:22:03 +0100 Subject: [PATCH 147/922] Fixes #267 in response to !464: naming REST components consistently. --- hikari/net/ratelimits.py | 36 +++++++++---------- hikari/net/rest.py | 4 +-- tests/hikari/net/test_ratelimits.py | 56 ++++++++++++++--------------- tests/hikari/net/test_rest.py | 20 +++++------ 4 files changed, 57 insertions(+), 59 deletions(-) diff --git a/hikari/net/ratelimits.py b/hikari/net/ratelimits.py index 0852623d54..94c1a2b121 100644 --- a/hikari/net/ratelimits.py +++ b/hikari/net/ratelimits.py @@ -164,8 +164,8 @@ "BurstRateLimiter", "ManualRateLimiter", "WindowedBurstRateLimiter", - "HTTPBucketRateLimiter", - "HTTPBucketRateLimiterManager", + "RESTBucket", + "RESTBucketManager", "ExponentialBackOff", ] @@ -299,7 +299,7 @@ def is_empty(self) -> bool: class ManualRateLimiter(BurstRateLimiter): - """Rate limit handler for the global HTTP rate limit. + """Rate limit handler for the global REST rate limit. This is a non-preemptive rate limiting algorithm that will always return completed futures until :meth:`throttle` is invoked. Once this is invoked, @@ -312,8 +312,8 @@ class ManualRateLimiter(BurstRateLimiter): Triggering a throttle when it is already set will cancel the current throttle task that is sleeping and replace it. - This is used to enforce the global HTTP rate limit that will occur - "randomly" during HTTP API interaction. + This is used to enforce the global REST rate limit that will occur + "randomly" during REST API interaction. Expect random occurrences. """ @@ -321,7 +321,7 @@ class ManualRateLimiter(BurstRateLimiter): __slots__ = () def __init__(self) -> None: - super().__init__("global HTTP") + super().__init__("global REST") def acquire(self) -> more_asyncio.Future[None]: """Acquire time on this rate limiter. @@ -575,10 +575,10 @@ async def throttle(self) -> None: self.throttle_task = None -class HTTPBucketRateLimiter(WindowedBurstRateLimiter): - """Represents a rate limit for an HTTP endpoint. +class RESTBucket(WindowedBurstRateLimiter): + """Represents a rate limit for an REST endpoint. - Component to represent an active rate limit bucket on a specific HTTP route + Component to represent an active rate limit bucket on a specific REST route with a specific major parameter combo. This is somewhat similar to the :obj:`~WindowedBurstRateLimiter` in how it @@ -667,10 +667,10 @@ def drip(self) -> None: self.remaining -= 1 -class HTTPBucketRateLimiterManager: - """The main rate limiter implementation for HTTP clients. +class RESTBucketManager: + """The main rate limiter implementation for REST clients. - This is designed to provide bucketed rate limiting for Discord HTTP + This is designed to provide bucketed rate limiting for Discord REST endpoints that respects the ``X-RateLimit-Bucket`` rate limit header. To do this, it makes the assumption that any limit can change at any time. """ @@ -692,8 +692,8 @@ class HTTPBucketRateLimiterManager: #: major parameters used in that compiled route) to their corresponding rate #: limiters. #: - #: :type: :obj:`~typing.MutableMapping` [ :obj:`~str`, :obj:`~HTTPBucketRateLimiter` ] - real_hashes_to_buckets: typing.Final[typing.MutableMapping[str, HTTPBucketRateLimiter]] + #: :type: :obj:`~typing.MutableMapping` [ :obj:`~str`, :obj:`~RESTBucketRateLimiter` ] + real_hashes_to_buckets: typing.Final[typing.MutableMapping[str, RESTBucket]] #: An internal event that is set when the object is shut down. #: @@ -717,7 +717,7 @@ def __init__(self) -> None: self.gc_task: typing.Optional[asyncio.Task] = None self.logger: logging.Logger = more_logging.get_named_logger(self) - def __enter__(self) -> "HTTPBucketRateLimiterManager": + def __enter__(self) -> "RESTBucketManager": return self def __exit__(self, exc_type: typing.Type[Exception], exc_val: Exception, exc_tb: types.TracebackType) -> None: @@ -833,7 +833,7 @@ def acquire(self, compiled_route: routes.CompiledRoute) -> more_asyncio.Future: ---- The returned future MUST be awaited, and will complete when your turn to make a call comes along. You are expected to await this and then - immediately make your HTTP call. The returned future may already be + immediately make your REST call. The returned future may already be completed if you can make the call immediately. """ # Returns a future to await on to wait to be allowed to send the request, and a @@ -850,7 +850,7 @@ def acquire(self, compiled_route: routes.CompiledRoute) -> more_asyncio.Future: bucket = self.real_hashes_to_buckets[real_bucket_hash] except KeyError: self.logger.debug("creating new bucket for %s", real_bucket_hash) - bucket = HTTPBucketRateLimiter(real_bucket_hash, compiled_route) + bucket = RESTBucket(real_bucket_hash, compiled_route) self.real_hashes_to_buckets[real_bucket_hash] = bucket return bucket.acquire() @@ -888,7 +888,7 @@ def update_rate_limits( real_bucket_hash = compiled_route.create_real_bucket_hash(bucket_header) if real_bucket_hash not in self.real_hashes_to_buckets: - bucket = HTTPBucketRateLimiter(real_bucket_hash, compiled_route) + bucket = RESTBucket(real_bucket_hash, compiled_route) self.real_hashes_to_buckets[real_bucket_hash] = bucket else: bucket = self.real_hashes_to_buckets[real_bucket_hash] diff --git a/hikari/net/rest.py b/hikari/net/rest.py index e105d9093a..43815bd45f 100644 --- a/hikari/net/rest.py +++ b/hikari/net/rest.py @@ -164,7 +164,7 @@ class LowLevelRestfulClient: #: You should not ever need to touch this implementation. #: #: :type: :obj:`~hikari.net.ratelimits.HTTPBucketRateLimiterManager` - ratelimiter: ratelimits.HTTPBucketRateLimiterManager + ratelimiter: ratelimits.RESTBucketManager #: The custom SSL context to use. #: @@ -248,7 +248,7 @@ def __init__( self.global_ratelimiter = ratelimits.ManualRateLimiter() self.json_serialize = json_serialize self.json_deserialize = json_deserialize - self.ratelimiter = ratelimits.HTTPBucketRateLimiterManager() + self.ratelimiter = ratelimits.RESTBucketManager() self.ratelimiter.start() if token is not None and not token.startswith(self._AUTHENTICATION_SCHEMES): diff --git a/tests/hikari/net/test_ratelimits.py b/tests/hikari/net/test_ratelimits.py index b102c2bb29..f824b643c9 100644 --- a/tests/hikari/net/test_ratelimits.py +++ b/tests/hikari/net/test_ratelimits.py @@ -360,18 +360,18 @@ def test_is_rate_limited_when_rate_limit_not_expired_only_returns_expr(self, rem assert rl.is_rate_limited(now) is (remaining <= 0) -class TestHTTPBucketRateLimiter: +class TestRESTBucket: @pytest.fixture def compiled_route(self): return routes.CompiledRoute("get", "/foo/bar", "/foo/bar", "1a2b3c") @pytest.mark.parametrize("name", ["spaghetti", ratelimits.UNKNOWN_HASH]) def test_is_unknown(self, name, compiled_route): - with ratelimits.HTTPBucketRateLimiter(name, compiled_route) as rl: + with ratelimits.RESTBucket(name, compiled_route) as rl: assert rl.is_unknown is (name == ratelimits.UNKNOWN_HASH) def test_update_rate_limit(self, compiled_route): - with ratelimits.HTTPBucketRateLimiter(__name__, compiled_route) as rl: + with ratelimits.RESTBucket(__name__, compiled_route) as rl: rl.remaining = 1 rl.limit = 2 rl.reset_at = 3 @@ -387,13 +387,13 @@ def test_update_rate_limit(self, compiled_route): @pytest.mark.parametrize("name", ["spaghetti", ratelimits.UNKNOWN_HASH]) def test_drip(self, name, compiled_route): - with ratelimits.HTTPBucketRateLimiter(name, compiled_route) as rl: + with ratelimits.RESTBucket(name, compiled_route) as rl: rl.remaining = 1 rl.drip() assert rl.remaining == 0 if name != ratelimits.UNKNOWN_HASH else 1 -class TestHTTPBucketRateLimiterManager: +class TestRESTBucketManager: @pytest.mark.asyncio async def test_close_closes_all_buckets(self): class MockBucket: @@ -402,7 +402,7 @@ def __init__(self): buckets = [MockBucket() for _ in range(30)] - mgr = ratelimits.HTTPBucketRateLimiterManager() + mgr = ratelimits.RESTBucketManager() mgr.real_hashes_to_buckets = {f"blah{i}": bucket for i, bucket in enumerate(buckets)} mgr.close() @@ -412,14 +412,14 @@ def __init__(self): @pytest.mark.asyncio async def test_close_sets_closed_event(self): - mgr = ratelimits.HTTPBucketRateLimiterManager() + mgr = ratelimits.RESTBucketManager() assert not mgr.closed_event.is_set() mgr.close() assert mgr.closed_event.is_set() @pytest.mark.asyncio async def test_start(self): - with ratelimits.HTTPBucketRateLimiterManager() as mgr: + with ratelimits.RESTBucketManager() as mgr: assert mgr.gc_task is None mgr.start() mgr.start() @@ -428,9 +428,9 @@ async def test_start(self): @pytest.mark.asyncio async def test_exit_closes(self): - with mock.patch("hikari.net.ratelimits.HTTPBucketRateLimiterManager.close") as close: - with mock.patch("hikari.net.ratelimits.HTTPBucketRateLimiterManager.gc") as gc: - with ratelimits.HTTPBucketRateLimiterManager() as mgr: + with mock.patch("hikari.net.ratelimits.RESTBucketManager.close") as close: + with mock.patch("hikari.net.ratelimits.RESTBucketManager.gc") as gc: + with ratelimits.RESTBucketManager() as mgr: mgr.start(0.01) gc.assert_called_once_with(0.01) close.assert_called() @@ -438,7 +438,7 @@ async def test_exit_closes(self): @pytest.mark.asyncio async def test_gc_polls_until_closed_event_set(self): # This is shit, but it is good shit. - with ratelimits.HTTPBucketRateLimiterManager() as mgr: + with ratelimits.RESTBucketManager() as mgr: mgr.start(0.01) assert mgr.gc_task is not None assert not mgr.gc_task.done() @@ -456,7 +456,7 @@ async def test_gc_polls_until_closed_event_set(self): @pytest.mark.asyncio async def test_gc_calls_do_pass(self): - with _helpers.unslot_class(ratelimits.HTTPBucketRateLimiterManager)() as mgr: + with _helpers.unslot_class(ratelimits.RESTBucketManager)() as mgr: mgr.do_gc_pass = mock.MagicMock() mgr.start(0.01) try: @@ -467,7 +467,7 @@ async def test_gc_calls_do_pass(self): @pytest.mark.asyncio async def test_do_gc_pass_any_buckets_that_are_empty_and_unknown_get_closed(self): - with _helpers.unslot_class(ratelimits.HTTPBucketRateLimiterManager)() as mgr: + with _helpers.unslot_class(ratelimits.RESTBucketManager)() as mgr: bucket = mock.MagicMock() bucket.is_empty = True bucket.is_unknown = True @@ -481,7 +481,7 @@ async def test_do_gc_pass_any_buckets_that_are_empty_and_unknown_get_closed(self @pytest.mark.asyncio async def test_do_gc_pass_any_buckets_that_are_empty_and_known_but_still_rate_limited_are_kept(self): - with _helpers.unslot_class(ratelimits.HTTPBucketRateLimiterManager)() as mgr: + with _helpers.unslot_class(ratelimits.RESTBucketManager)() as mgr: bucket = mock.MagicMock() bucket.is_empty = True bucket.is_unknown = False @@ -496,7 +496,7 @@ async def test_do_gc_pass_any_buckets_that_are_empty_and_known_but_still_rate_li @pytest.mark.asyncio async def test_do_gc_pass_any_buckets_that_are_empty_and_known_but_not_rate_limited_are_closed(self): - with _helpers.unslot_class(ratelimits.HTTPBucketRateLimiterManager)() as mgr: + with _helpers.unslot_class(ratelimits.RESTBucketManager)() as mgr: bucket = mock.MagicMock() bucket.is_empty = True bucket.is_unknown = False @@ -511,7 +511,7 @@ async def test_do_gc_pass_any_buckets_that_are_empty_and_known_but_not_rate_limi @pytest.mark.asyncio async def test_do_gc_pass_any_buckets_that_are_not_empty_are_kept(self): - with _helpers.unslot_class(ratelimits.HTTPBucketRateLimiterManager)() as mgr: + with _helpers.unslot_class(ratelimits.RESTBucketManager)() as mgr: bucket = mock.MagicMock() bucket.is_empty = False bucket.is_unknown = True @@ -525,7 +525,7 @@ async def test_do_gc_pass_any_buckets_that_are_not_empty_are_kept(self): @pytest.mark.asyncio async def test_acquire_route_when_not_in_routes_to_real_hashes_makes_new_bucket_using_initial_hash(self): - with ratelimits.HTTPBucketRateLimiterManager() as mgr: + with ratelimits.RESTBucketManager() as mgr: route = mock.MagicMock() route.create_real_bucket_hash = mock.MagicMock(wraps=lambda intial_hash: intial_hash + ";bobs") @@ -534,11 +534,11 @@ async def test_acquire_route_when_not_in_routes_to_real_hashes_makes_new_bucket_ mgr.acquire(route) assert "UNKNOWN;bobs" in mgr.real_hashes_to_buckets - assert isinstance(mgr.real_hashes_to_buckets["UNKNOWN;bobs"], ratelimits.HTTPBucketRateLimiter) + assert isinstance(mgr.real_hashes_to_buckets["UNKNOWN;bobs"], ratelimits.RESTBucket) @pytest.mark.asyncio async def test_acquire_route_when_not_in_routes_to_real_hashes_caches_route(self): - with ratelimits.HTTPBucketRateLimiterManager() as mgr: + with ratelimits.RESTBucketManager() as mgr: route = mock.MagicMock() route.create_real_bucket_hash = mock.MagicMock(wraps=lambda intial_hash: intial_hash + ";bobs") @@ -550,7 +550,7 @@ async def test_acquire_route_when_not_in_routes_to_real_hashes_caches_route(self @pytest.mark.asyncio async def test_acquire_route_when_route_cached_already_obtains_hash_from_route_and_bucket_from_hash(self): - with ratelimits.HTTPBucketRateLimiterManager() as mgr: + with ratelimits.RESTBucketManager() as mgr: route = mock.MagicMock() route.create_real_bucket_hash = mock.MagicMock(return_value="eat pant;1234") bucket = mock.MagicMock() @@ -566,11 +566,11 @@ async def test_acquire_route_when_route_cached_already_obtains_hash_from_route_a @pytest.mark.asyncio async def test_acquire_route_returns_acquired_future(self): - with ratelimits.HTTPBucketRateLimiterManager() as mgr: + with ratelimits.RESTBucketManager() as mgr: route = mock.MagicMock() bucket = mock.MagicMock() - with mock.patch("hikari.net.ratelimits.HTTPBucketRateLimiter", return_value=bucket): + with mock.patch("hikari.net.ratelimits.RESTBucket", return_value=bucket): route.create_real_bucket_hash = mock.MagicMock(wraps=lambda intial_hash: intial_hash + ";bobs") f = mgr.acquire(route) @@ -578,7 +578,7 @@ async def test_acquire_route_returns_acquired_future(self): @pytest.mark.asyncio async def test_acquire_route_returns_acquired_future_for_new_bucket(self): - with ratelimits.HTTPBucketRateLimiterManager() as mgr: + with ratelimits.RESTBucketManager() as mgr: route = mock.MagicMock() route.create_real_bucket_hash = mock.MagicMock(return_value="eat pant;bobs") bucket = mock.MagicMock() @@ -590,17 +590,17 @@ async def test_acquire_route_returns_acquired_future_for_new_bucket(self): @pytest.mark.asyncio async def test_update_rate_limits_if_wrong_bucket_hash_reroutes_route(self): - with ratelimits.HTTPBucketRateLimiterManager() as mgr: + with ratelimits.RESTBucketManager() as mgr: route = mock.MagicMock() route.create_real_bucket_hash = mock.MagicMock(wraps=lambda intial_hash: intial_hash + ";bobs") mgr.routes_to_hashes[route] = "123" mgr.update_rate_limits(route, "blep", 22, 23, datetime.datetime.now(), datetime.datetime.now()) assert mgr.routes_to_hashes[route] == "blep" - assert isinstance(mgr.real_hashes_to_buckets["blep;bobs"], ratelimits.HTTPBucketRateLimiter) + assert isinstance(mgr.real_hashes_to_buckets["blep;bobs"], ratelimits.RESTBucket) @pytest.mark.asyncio async def test_update_rate_limits_if_right_bucket_hash_does_nothing_to_hash(self): - with ratelimits.HTTPBucketRateLimiterManager() as mgr: + with ratelimits.RESTBucketManager() as mgr: route = mock.MagicMock() route.create_real_bucket_hash = mock.MagicMock(wraps=lambda intial_hash: intial_hash + ";bobs") mgr.routes_to_hashes[route] = "123" @@ -612,7 +612,7 @@ async def test_update_rate_limits_if_right_bucket_hash_does_nothing_to_hash(self @pytest.mark.asyncio async def test_update_rate_limits_updates_params(self): - with ratelimits.HTTPBucketRateLimiterManager() as mgr: + with ratelimits.RESTBucketManager() as mgr: route = mock.MagicMock() route.create_real_bucket_hash = mock.MagicMock(wraps=lambda intial_hash: intial_hash + ";bobs") mgr.routes_to_hashes[route] = "123" diff --git a/tests/hikari/net/test_rest.py b/tests/hikari/net/test_rest.py index b5d89d925a..73354b7056 100644 --- a/tests/hikari/net/test_rest.py +++ b/tests/hikari/net/test_rest.py @@ -42,7 +42,7 @@ def rest_impl(self): stack = contextlib.ExitStack() stack.enter_context(mock.patch("aiohttp.ClientSession")) stack.enter_context(mock.patch("hikari.internal.more_logging.get_named_logger")) - stack.enter_context(mock.patch.object(ratelimits, "HTTPBucketRateLimiterManager")) + stack.enter_context(mock.patch.object(ratelimits, "RESTBucketManager")) stack.enter_context(mock.patch.object(ratelimits, "ManualRateLimiter")) with stack: client = rest.LowLevelRestfulClient(base_url="https://discordapp.com/api/v6", token="Bot blah.blah.blah") @@ -108,7 +108,7 @@ async def test__init__with_bot_token_and_without_optionals(self): stack = contextlib.ExitStack() stack.enter_context(mock.patch.object(ratelimits, "ManualRateLimiter", return_value=mock_manual_rate_limiter)) - stack.enter_context(mock.patch.object(ratelimits, "HTTPBucketRateLimiterManager", return_value=buckets_mock)) + stack.enter_context(mock.patch.object(ratelimits, "RESTBucketManager", return_value=buckets_mock)) stack.enter_context(mock.patch.object(aiohttp, "ClientSession", return_value=mock_client_session)) with stack: @@ -126,7 +126,7 @@ async def test__init__with_bot_token_and_without_optionals(self): async def test__init__with_bearer_token_and_without_optionals(self): stack = contextlib.ExitStack() stack.enter_context(mock.patch.object(ratelimits, "ManualRateLimiter")) - stack.enter_context(mock.patch.object(ratelimits, "HTTPBucketRateLimiterManager")) + stack.enter_context(mock.patch.object(ratelimits, "RESTBucketManager")) stack.enter_context(mock.patch.object(aiohttp, "ClientSession")) with stack: client = rest.LowLevelRestfulClient(token="Bearer token.otacon.a-token") @@ -135,7 +135,7 @@ async def test__init__with_bearer_token_and_without_optionals(self): @pytest.mark.asyncio async def test__init__with_optionals(self): mock_manual_rate_limiter = mock.MagicMock(ratelimits.ManualRateLimiter) - mock_http_bucket_rate_limit_manager = mock.MagicMock(ratelimits.HTTPBucketRateLimiterManager) + mock_http_bucket_rate_limit_manager = mock.MagicMock(ratelimits.RESTBucketManager) mock_connector = mock.MagicMock(aiohttp.BaseConnector) mock_dumps = mock.MagicMock(json.dumps) mock_loads = mock.MagicMock(json.loads) @@ -147,9 +147,7 @@ async def test__init__with_optionals(self): stack.enter_context(mock.patch.object(aiohttp, "ClientSession")) stack.enter_context(mock.patch.object(ratelimits, "ManualRateLimiter", return_value=mock_manual_rate_limiter)) stack.enter_context( - mock.patch.object( - ratelimits, "HTTPBucketRateLimiterManager", return_value=mock_http_bucket_rate_limit_manager - ) + mock.patch.object(ratelimits, "RESTBucketManager", return_value=mock_http_bucket_rate_limit_manager) ) with stack: @@ -179,7 +177,7 @@ async def test__init__with_optionals(self): async def test__init__raises_runtime_error_with_invalid_token(self, *_): stack = contextlib.ExitStack() stack.enter_context(mock.patch.object(ratelimits, "ManualRateLimiter")) - stack.enter_context(mock.patch.object(ratelimits, "HTTPBucketRateLimiterManager")) + stack.enter_context(mock.patch.object(ratelimits, "RESTBucketManager")) stack.enter_context(mock.patch.object(aiohttp, "ClientSession")) with stack: async with rest.LowLevelRestfulClient(token="An-invalid-TOKEN"): @@ -193,13 +191,13 @@ async def test_close(self, rest_impl): @pytest.fixture() @mock.patch.object(ratelimits, "ManualRateLimiter") - @mock.patch.object(ratelimits, "HTTPBucketRateLimiterManager") + @mock.patch.object(ratelimits, "RESTBucketManager") @mock.patch.object(aiohttp, "ClientSession") def rest_impl_with__request(self, *args): rest_impl = rest.LowLevelRestfulClient(token="Bot token") rest_impl.logger = mock.MagicMock(debug=mock.MagicMock()) rest_impl.ratelimiter = mock.MagicMock( - ratelimits.HTTPBucketRateLimiterManager, acquire=mock.MagicMock(), update_rate_limits=mock.MagicMock(), + ratelimits.RESTBucketManager, acquire=mock.MagicMock(), update_rate_limits=mock.MagicMock(), ) rest_impl.global_ratelimiter = mock.MagicMock( ratelimits.ManualRateLimiter, acquire=mock.MagicMock(), throttle=mock.MagicMock() @@ -455,7 +453,7 @@ async def test__request_raises_appropriate_error_for_status_code( ): stack = contextlib.ExitStack() stack.enter_context(mock.patch.object(ratelimits, "ManualRateLimiter")) - stack.enter_context(mock.patch.object(ratelimits, "HTTPBucketRateLimiterManager")) + stack.enter_context(mock.patch.object(ratelimits, "RESTBucketManager")) stack.enter_context(mock.patch.object(aiohttp, "ClientSession")) discord_response.status = status_code with stack: From 62c6fa2131747836c52a8ad8fadcd7b99b300f20 Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Thu, 16 Apr 2020 22:33:22 +0100 Subject: [PATCH 148/922] Closes #303 by handling dodgy responses more consistently. This will log a warning and then return the None value. Added additional logging code for handling weird response types from REST API. This fixes an issue where a 4xx with no body causes a key error also. --- hikari/net/rest.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/hikari/net/rest.py b/hikari/net/rest.py index 43815bd45f..3b832b65b4 100644 --- a/hikari/net/rest.py +++ b/hikari/net/rest.py @@ -388,6 +388,10 @@ async def _request( ) continue else: + self.logger.debug( + "received unexpected response shape. Status: %s, Content-Type: %s, Body: %s", + status, content_type, raw_body + ) body = None self.ratelimiter.update_rate_limits(compiled_route, bucket, remaining, limit, now_date, reset_date) @@ -402,7 +406,9 @@ async def _request( if status >= codes.HTTPStatusCode.BAD_REQUEST: code = None - if self.version == versions.HTTPAPIVersion.V6: + if body is None: + message = raw_body + elif self.version == versions.HTTPAPIVersion.V6: message = ", ".join(f"{k} - {v}" for k, v in body.items()) else: message = body.get("message") From 185561924d837f6c826dff8630f81dca0ac3606f Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Thu, 16 Apr 2020 22:47:47 +0100 Subject: [PATCH 149/922] Fixes #302 by detecting uninitialized fields. --- hikari/clients/bot_clients.py | 56 +++++++++++++++++++++++++++++++---- hikari/net/rest.py | 2 +- 2 files changed, 51 insertions(+), 7 deletions(-) diff --git a/hikari/clients/bot_clients.py b/hikari/clients/bot_clients.py index b15e3819f7..41f347f22f 100644 --- a/hikari/clients/bot_clients.py +++ b/hikari/clients/bot_clients.py @@ -30,6 +30,7 @@ from hikari.clients import rest_clients from hikari.clients import runnable from hikari.clients import shard_clients +from hikari.internal import meta from hikari.internal import more_asyncio from hikari.internal import more_logging from hikari.state import event_dispatchers @@ -37,6 +38,33 @@ from hikari.state import stateless_event_managers +class _NotInitializedYet(meta.Singleton): + """Sentinel value for fields that are not yet initialized. + + These will be filled once the bot has started. + """ + __slots__ = () + + def __bool__(self) -> bool: + return False + + def __defer(self, *args, **kwargs) -> typing.NoReturn: + raise TypeError(f"Bot has not yet initialized, so attribute is not available.") from None + + __call__ = __defer + __getattr__ = __defer + __setattr__ = __defer + __delattr__ = __defer + __getitem__ = __defer + __setitem__ = __defer + __delitem__ = __defer + __await__ = __defer + __enter__ = __defer + __aenter__ = __defer + __exit__ = __defer + __aexit__ = __defer + + class BotBase(runnable.RunnableClient, event_dispatchers.EventDispatcher): """An abstract base class for a bot implementation. @@ -60,6 +88,10 @@ class BotBase(runnable.RunnableClient, event_dispatchers.EventDispatcher): #: The gateway for this bot. #: + #: Note + #: ---- + #: This will be initialized lazily once the bot has started. + #: #: :type: :obj:`~hikari.clients.gateway_managers.GatewayManager` [ :obj:`~hikari.clients.shard_clients.ShardClient` ] gateway: gateway_managers.GatewayManager[shard_clients.ShardClient] @@ -70,6 +102,10 @@ class BotBase(runnable.RunnableClient, event_dispatchers.EventDispatcher): #: The REST HTTP client to use for this bot. #: + #: Note + #: ---- + #: This will be initialized lazily once the bot has started. + #: #: :type: :obj:`~hikari.clients.rest_clients.RESTClient` rest: rest_clients.RESTClient @@ -78,10 +114,13 @@ def __init__(self, config: configs.BotConfig, event_manager: event_managers.Even super().__init__(more_logging.get_named_logger(self)) self.config = config self.event_manager = event_manager - self.gateway = NotImplemented - self.rest = NotImplemented + self.gateway = _NotInitializedYet() + self.rest = _NotInitializedYet() async def start(self): + if self.rest or self.gateway: + raise RuntimeError("Bot is already running.") + self.rest = rest_clients.RESTClient(self.config) gateway_bot = await self.rest.fetch_gateway_bot() @@ -106,13 +145,18 @@ async def start(self): await self.gateway.start() - async def close(self): - await self.gateway.close() + async def close(self) -> None: + if self.gateway: + await self.gateway.close() self.event_manager.event_dispatcher.close() - await self.rest.close() + if self.rest: + await self.rest.close() + self.gateway = _NotInitializedYet() + self.rest = _NotInitializedYet() async def join(self) -> None: - await self.gateway.join() + if self.gateway: + await self.gateway.join() def add_listener( self, event_type: typing.Type[event_dispatchers.EventT], callback: event_dispatchers.EventCallbackT diff --git a/hikari/net/rest.py b/hikari/net/rest.py index 3b832b65b4..cbcc54f056 100644 --- a/hikari/net/rest.py +++ b/hikari/net/rest.py @@ -388,7 +388,7 @@ async def _request( ) continue else: - self.logger.debug( + self.logger.warning( "received unexpected response shape. Status: %s, Content-Type: %s, Body: %s", status, content_type, raw_body ) From 329fcbfefc9b8f16bb3c34c29a3aafcbe6525ccd Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Thu, 16 Apr 2020 23:30:16 +0100 Subject: [PATCH 150/922] Increased shutdown logging, fixed bug causing deadlock on interrupt when connection cannot be gracefully closed. --- hikari/clients/shard_clients.py | 2 ++ hikari/net/shard.py | 7 +++++-- hikari/state/event_dispatchers.py | 1 + 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/hikari/clients/shard_clients.py b/hikari/clients/shard_clients.py index 5bb25e56b2..a67d22a399 100644 --- a/hikari/clients/shard_clients.py +++ b/hikari/clients/shard_clients.py @@ -344,6 +344,8 @@ async def close(self) -> None: if self._dispatcher is not None: await self._dispatcher.dispatch_event(events.StoppedEvent()) + else: + self.logger.debug("shard already requested to stop, will not do anything else") async def _keep_alive(self): back_off = ratelimits.ExponentialBackOff(maximum=None) diff --git a/hikari/net/shard.py b/hikari/net/shard.py index 547cb8b164..b7fe9559db 100644 --- a/hikari/net/shard.py +++ b/hikari/net/shard.py @@ -550,13 +550,16 @@ async def close(self, close_code: int = 1000) -> None: The close code to use. Defaults to ``1000`` (normal closure). """ if not self.requesting_close_event.is_set(): + self.logger.debug("closing websocket connection") self.requesting_close_event.set() # These will attribute error if they are not set; in this case we don't care, just ignore it. with contextlib.suppress(asyncio.TimeoutError, AttributeError): - await asyncio.wait_for(asyncio.shield(self._ws.close(code=close_code)), timeout=2.0) + await asyncio.wait_for(self._ws.close(code=close_code), timeout=2.0) with contextlib.suppress(asyncio.TimeoutError, AttributeError): - await asyncio.wait_for(asyncio.shield(self._session.close()), timeout=2.0) + await asyncio.wait_for(self._session.close(), timeout=2.0) self.closed_event.set() + else: + self.logger.debug("websocket connection already requested to be closed, will not do anything else") async def connect(self, client_session_type=aiohttp.ClientSession) -> None: """Connect to the gateway and return when it closes. diff --git a/hikari/state/event_dispatchers.py b/hikari/state/event_dispatchers.py index 4df94a73dc..1859736f6b 100644 --- a/hikari/state/event_dispatchers.py +++ b/hikari/state/event_dispatchers.py @@ -339,6 +339,7 @@ def dispatch_event(self, event: events.HikariEvent): event handlers. """ event_t = type(event) + self.logger.debug("dispatching %s", event_t.__name__) futs = [] From bf977361e23117c9eab472a1e155d1810d51affc Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Thu, 16 Apr 2020 23:30:39 +0100 Subject: [PATCH 151/922] Fixed spurious double dispatch of every event that occurs (second is always None) --- hikari/state/event_managers.py | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/hikari/state/event_managers.py b/hikari/state/event_managers.py index fdbe9576f0..909d69074d 100644 --- a/hikari/state/event_managers.py +++ b/hikari/state/event_managers.py @@ -170,15 +170,14 @@ def process_raw_event( handler = self.raw_event_mappers[name] except KeyError: self.logger.debug("no handler for event %s is registered", name) - else: - try: - event = handler(shard_client_obj, payload) - except Exception as ex: - self.logger.exception( - "Failed to unmarshal %r event payload. This is likely a bug in Hikari itself. " - "Please contact a library dev or make an issue on the issue tracker for more support.", - name, - exc_info=ex, - ) - else: - self.event_dispatcher.dispatch_event(event) + return + + try: + handler(shard_client_obj, payload) + except Exception as ex: + self.logger.exception( + "Failed to unmarshal %r event payload. This is likely a bug in Hikari itself. " + "Please contact a library dev or make an issue on the issue tracker for more support.", + name, + exc_info=ex, + ) From 283991996b5c93356bea5f7d58de6e9dc09f05a7 Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Thu, 16 Apr 2020 23:37:23 +0100 Subject: [PATCH 152/922] Fixed DISCONNECT firing on address resolution failure when it shouldn't be. --- hikari/net/shard.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/hikari/net/shard.py b/hikari/net/shard.py index b7fe9559db..04d3e9aef4 100644 --- a/hikari/net/shard.py +++ b/hikari/net/shard.py @@ -558,7 +558,7 @@ async def close(self, close_code: int = 1000) -> None: with contextlib.suppress(asyncio.TimeoutError, AttributeError): await asyncio.wait_for(self._session.close(), timeout=2.0) self.closed_event.set() - else: + elif self._debug: self.logger.debug("websocket connection already requested to be closed, will not do anything else") async def connect(self, client_session_type=aiohttp.ClientSession) -> None: @@ -641,17 +641,20 @@ async def connect(self, client_session_type=aiohttp.ClientSession) -> None: raise ex finally: - await self.close(close_code) self.closed_event.set() self._connected_at = float("nan") self.last_heartbeat_sent = float("nan") self.heartbeat_latency = float("nan") self.last_message_received = float("nan") - self.disconnect_count += 1 - self._ws = None + + if self._ws is not None: + await self.close(close_code) + self.dispatch(self, "DISCONNECTED", {}) + self.disconnect_count += 1 + self._ws = None + await self._session.close() self._session = None - self.dispatch(self, "DISCONNECTED", {}) @property def _ws_connect_kwargs(self): From cd563c1bc37cf1ec12670086ebd9c968865c04c2 Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Thu, 16 Apr 2020 23:43:33 +0100 Subject: [PATCH 153/922] Fixed connection being assumed to be open before first payload received on websocket. --- hikari/net/shard.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/hikari/net/shard.py b/hikari/net/shard.py index 04d3e9aef4..37299783e4 100644 --- a/hikari/net/shard.py +++ b/hikari/net/shard.py @@ -588,12 +588,12 @@ async def connect(self, client_session_type=aiohttp.ClientSession) -> None: try: self._ws = await self._session.ws_connect(**self._ws_connect_kwargs) - - self._connected_at = time.perf_counter() self._zlib = zlib.decompressobj() self.logger.debug("expecting HELLO") pl = await self._receive() + self._connected_at = time.perf_counter() + op = pl["op"] if op != 10: raise errors.GatewayError(f"Expected HELLO opcode 10 but received {op}") @@ -642,16 +642,18 @@ async def connect(self, client_session_type=aiohttp.ClientSession) -> None: raise ex finally: self.closed_event.set() - self._connected_at = float("nan") - self.last_heartbeat_sent = float("nan") - self.heartbeat_latency = float("nan") - self.last_message_received = float("nan") - if self._ws is not None: + if not math.isnan(self._connected_at): await self.close(close_code) self.dispatch(self, "DISCONNECTED", {}) self.disconnect_count += 1 - self._ws = None + + self._ws = None + + self._connected_at = float("nan") + self.last_heartbeat_sent = float("nan") + self.heartbeat_latency = float("nan") + self.last_message_received = float("nan") await self._session.close() self._session = None From 1e0b9b6976b6da3221af1013f5c1311e6831a0ec Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Thu, 16 Apr 2020 23:56:53 +0100 Subject: [PATCH 154/922] Reduced spacing of exponential backoff and made it a little less spammy for gateway reconnection issues. --- hikari/clients/bot_clients.py | 1 + hikari/clients/shard_clients.py | 6 ++---- hikari/net/ratelimits.py | 12 ++++++++++-- hikari/net/rest.py | 4 +++- 4 files changed, 16 insertions(+), 7 deletions(-) diff --git a/hikari/clients/bot_clients.py b/hikari/clients/bot_clients.py index 41f347f22f..4bc6061658 100644 --- a/hikari/clients/bot_clients.py +++ b/hikari/clients/bot_clients.py @@ -43,6 +43,7 @@ class _NotInitializedYet(meta.Singleton): These will be filled once the bot has started. """ + __slots__ = () def __bool__(self) -> bool: diff --git a/hikari/clients/shard_clients.py b/hikari/clients/shard_clients.py index a67d22a399..fbd448786b 100644 --- a/hikari/clients/shard_clients.py +++ b/hikari/clients/shard_clients.py @@ -344,11 +344,9 @@ async def close(self) -> None: if self._dispatcher is not None: await self._dispatcher.dispatch_event(events.StoppedEvent()) - else: - self.logger.debug("shard already requested to stop, will not do anything else") async def _keep_alive(self): - back_off = ratelimits.ExponentialBackOff(maximum=None) + back_off = ratelimits.ExponentialBackOff(base=1.85, maximum=600, initial_increment=2) last_start = time.perf_counter() do_not_back_off = True @@ -360,7 +358,7 @@ async def _keep_alive(self): if not do_not_back_off and time.perf_counter() - last_start < 30: next_backoff = next(back_off) self.logger.info( - "restarted within 30 seconds, will backoff for %ss", next_backoff, + "restarted within 30 seconds, will backoff for %.2fs", next_backoff, ) await asyncio.sleep(next_backoff) else: diff --git a/hikari/net/ratelimits.py b/hikari/net/ratelimits.py index 94c1a2b121..7511cf54f3 100644 --- a/hikari/net/ratelimits.py +++ b/hikari/net/ratelimits.py @@ -921,6 +921,8 @@ class ExponentialBackOff: jitter_multiplier : :obj:`~float` The multiplier for the random jitter. Defaults to ``1``. Set to ``0`` to disable jitter. + initial_increment : :obj:`~int` + The initial increment to start at. Defaults to ``0``. """ __slots__ = ("base", "increment", "maximum", "jitter_multiplier") @@ -947,10 +949,16 @@ class ExponentialBackOff: #: :type: :obj:`~float` jitter_multiplier: typing.Final[float] - def __init__(self, base: float = 2, maximum: typing.Optional[float] = 64, jitter_multiplier: float = 1) -> None: + def __init__( + self, + base: float = 2, + maximum: typing.Optional[float] = 64, + jitter_multiplier: float = 1, + initial_increment: int = 0, + ) -> None: self.base = base self.maximum = maximum - self.increment = 0 + self.increment = initial_increment self.jitter_multiplier = jitter_multiplier def __next__(self) -> float: diff --git a/hikari/net/rest.py b/hikari/net/rest.py index cbcc54f056..028038bf78 100644 --- a/hikari/net/rest.py +++ b/hikari/net/rest.py @@ -390,7 +390,9 @@ async def _request( else: self.logger.warning( "received unexpected response shape. Status: %s, Content-Type: %s, Body: %s", - status, content_type, raw_body + status, + content_type, + raw_body, ) body = None From cc0886362c909b0d196c0f46b27d0d6ec161f522 Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Fri, 17 Apr 2020 00:11:32 +0100 Subject: [PATCH 155/922] Removed arbitrary fstring --- hikari/clients/bot_clients.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hikari/clients/bot_clients.py b/hikari/clients/bot_clients.py index 4bc6061658..80c2081dcf 100644 --- a/hikari/clients/bot_clients.py +++ b/hikari/clients/bot_clients.py @@ -50,7 +50,7 @@ def __bool__(self) -> bool: return False def __defer(self, *args, **kwargs) -> typing.NoReturn: - raise TypeError(f"Bot has not yet initialized, so attribute is not available.") from None + raise TypeError("Bot has not yet initialized, so attribute is not available.") from None __call__ = __defer __getattr__ = __defer From adc4378cfd59fb40a0e171307aa4b1ad4941284f Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Fri, 17 Apr 2020 18:59:15 +0100 Subject: [PATCH 156/922] Fixes #304 by modifying heartbeat timeout behaviour slightly. --- hikari/internal/more_collections.py | 16 ++++++- hikari/net/shard.py | 74 ++++++++++++++++++----------- tests/hikari/net/test_shard.py | 36 ++++++++++---- 3 files changed, 85 insertions(+), 41 deletions(-) diff --git a/hikari/internal/more_collections.py b/hikari/internal/more_collections.py index 76cb798268..29878967fd 100644 --- a/hikari/internal/more_collections.py +++ b/hikari/internal/more_collections.py @@ -46,10 +46,22 @@ EMPTY_GENERATOR_EXPRESSION: typing.Final[typing.Iterator[T]] = (_ for _ in EMPTY_COLLECTION) -class WeakKeyDictionary(weakref.WeakKeyDictionary, typing.MutableMapping[K, V]): +class WeakKeyDictionary(typing.Generic[K, V], weakref.WeakKeyDictionary, typing.MutableMapping[K, V]): """A dictionary that has weak references to the keys. - This is a type-safe version of :obj:`~weakref.WeakKeyDictionary`. + This is a type-safe version of :obj:`~weakref.WeakKeyDictionary` which + is subscriptable. + + Example + ------- + + .. code-block:: python + + @attr.s(auto_attribs=True) + class Commands: + instances: Set[Command] + aliases: WeakKeyDictionary[Command, str] + """ __slots__ = () diff --git a/hikari/net/shard.py b/hikari/net/shard.py index 37299783e4..86f82ca343 100644 --- a/hikari/net/shard.py +++ b/hikari/net/shard.py @@ -405,7 +405,7 @@ def uptime(self) -> datetime.timedelta: @property def is_connected(self) -> bool: - """Whether the gateway is connecter or not. + """Whether the gateway is connected or not. Returns ------- @@ -541,26 +541,6 @@ async def update_presence(self, presence: typing.Dict) -> None: await self._send({"op": codes.GatewayOpcode.PRESENCE_UPDATE, "d": presence}) self._presence = presence - async def close(self, close_code: int = 1000) -> None: - """Request this gateway connection closes. - - Parameters - ---------- - close_code : :obj:`~int` - The close code to use. Defaults to ``1000`` (normal closure). - """ - if not self.requesting_close_event.is_set(): - self.logger.debug("closing websocket connection") - self.requesting_close_event.set() - # These will attribute error if they are not set; in this case we don't care, just ignore it. - with contextlib.suppress(asyncio.TimeoutError, AttributeError): - await asyncio.wait_for(self._ws.close(code=close_code), timeout=2.0) - with contextlib.suppress(asyncio.TimeoutError, AttributeError): - await asyncio.wait_for(self._session.close(), timeout=2.0) - self.closed_event.set() - elif self._debug: - self.logger.debug("websocket connection already requested to be closed, will not do anything else") - async def connect(self, client_session_type=aiohttp.ClientSession) -> None: """Connect to the gateway and return when it closes. @@ -580,14 +560,14 @@ async def connect(self, client_session_type=aiohttp.ClientSession) -> None: self.requesting_close_event.clear() self.resumed_event.clear() - self._session = client_session_type(**self._cs_init_kwargs) + self._session = client_session_type(**self._cs_init_kwargs()) # 1000 and 1001 will invalidate sessions, 1006 (used here before) # is a sketchy area as to the intent. 4000 is known to work normally. close_code = codes.GatewayCloseCode.UNKNOWN_ERROR try: - self._ws = await self._session.ws_connect(**self._ws_connect_kwargs) + self._ws = await self._session.ws_connect(**self._ws_connect_kwargs()) self._zlib = zlib.decompressobj() self.logger.debug("expecting HELLO") pl = await self._receive() @@ -630,6 +610,7 @@ async def connect(self, client_session_type=aiohttp.ClientSession) -> None: # the close event was set without an error causing that flag to be changed. ex = errors.GatewayClientClosedError() close_code = codes.GatewayCloseCode.NORMAL_CLOSURE + elif isinstance(ex, asyncio.TimeoutError): # If we get timeout errors receiving stuff, propagate as a zombied connection. This # is already done by the ping keepalive and heartbeat keepalive partially, but this @@ -640,6 +621,7 @@ async def connect(self, client_session_type=aiohttp.ClientSession) -> None: close_code = ex.close_code raise ex + finally: self.closed_event.set() @@ -658,7 +640,26 @@ async def connect(self, client_session_type=aiohttp.ClientSession) -> None: await self._session.close() self._session = None - @property + async def close(self, close_code: int = 1000) -> None: + """Request this gateway connection closes. + + Parameters + ---------- + close_code : :obj:`~int` + The close code to use. Defaults to ``1000`` (normal closure). + """ + if not self.requesting_close_event.is_set(): + self.logger.debug("closing websocket connection") + self.requesting_close_event.set() + # These will attribute error if they are not set; in this case we don't care, just ignore it. + with contextlib.suppress(asyncio.TimeoutError, AttributeError): + await asyncio.wait_for(self._ws.close(code=close_code), timeout=2.0) + with contextlib.suppress(asyncio.TimeoutError, AttributeError): + await asyncio.wait_for(self._session.close(), timeout=2.0) + self.closed_event.set() + elif self._debug: + self.logger.debug("websocket connection already requested to be closed, will not do anything else") + def _ws_connect_kwargs(self): return dict( url=self._url, @@ -672,24 +673,28 @@ def _ws_connect_kwargs(self): ssl_context=self._ssl_context, ) - @property def _cs_init_kwargs(self): return dict(connector=self._connector) async def _heartbeat_keep_alive(self, heartbeat_interval): while not self.requesting_close_event.is_set(): - if self.last_message_received < self.last_heartbeat_sent: - raise asyncio.TimeoutError( - f"{self.shard_id}: connection is a zombie, haven't received HEARTBEAT ACK for too long" - ) + self._zombie_detector(heartbeat_interval) self.logger.debug("preparing to send HEARTBEAT (s:%s, interval:%ss)", self.seq, self.heartbeat_interval) await self._send({"op": codes.GatewayOpcode.HEARTBEAT, "d": self.seq}) self.last_heartbeat_sent = time.perf_counter() + try: await asyncio.wait_for(self.requesting_close_event.wait(), timeout=heartbeat_interval) except asyncio.TimeoutError: pass + def _zombie_detector(self, heartbeat_interval): + time_since_message = time.perf_counter() - self.last_message_received + if heartbeat_interval < time_since_message: + raise asyncio.TimeoutError( + f"{self.shard_id}: connection is a zombie, haven't received any message for {time_since_message}s" + ) + async def _identify(self): self.logger.debug("preparing to send IDENTIFY") @@ -758,28 +763,34 @@ async def _run(self): self.logger.debug("connection has RESUMED (session:%s, s:%s)", self.session_id, self.seq) self.dispatch(self, event_name, d) + elif op == codes.GatewayOpcode.HEARTBEAT: self.logger.debug("received HEARTBEAT, preparing to send HEARTBEAT ACK to server in response") await self._send({"op": codes.GatewayOpcode.HEARTBEAT_ACK}) + elif op == codes.GatewayOpcode.RECONNECT: self.logger.debug("instructed by gateway server to restart connection") raise errors.GatewayMustReconnectError() + elif op == codes.GatewayOpcode.INVALID_SESSION: can_resume = bool(d) self.logger.debug( "instructed by gateway server to %s session", "resume" if can_resume else "restart", ) raise errors.GatewayInvalidSessionError(can_resume) + elif op == codes.GatewayOpcode.HEARTBEAT_ACK: now = time.perf_counter() self.heartbeat_latency = now - self.last_heartbeat_sent self.logger.debug("received HEARTBEAT ACK (latency:%ss)", self.heartbeat_latency) + else: self.logger.debug("ignoring opcode %s with data %r", op, d) async def _receive(self): while True: message = await self._receive_one_packet() + if message.type == aiohttp.WSMsgType.TEXT: obj = self._json_deserialize(message.data) @@ -794,6 +805,7 @@ async def _receive(self): len(message.data), ) return obj + elif message.type == aiohttp.WSMsgType.BINARY: buffer = bytearray(message.data) packets = 1 @@ -809,6 +821,7 @@ async def _receive(self): if self._debug: self.logger.debug("receive %s zlib-encoded packets containing payload %r", packets, pl) + else: self.logger.debug( "receive zlib payload (op:%s, t:%s, s:%s, size:%s, packets:%s)", @@ -828,10 +841,13 @@ async def _receive(self): pass self.logger.debug("connection closed with code %s", close_code) + if close_code == codes.GatewayCloseCode.AUTHENTICATION_FAILED: raise errors.GatewayInvalidTokenError() + if close_code in (codes.GatewayCloseCode.SESSION_TIMEOUT, codes.GatewayCloseCode.INVALID_SEQ): raise errors.GatewayInvalidSessionError(False) + if close_code == codes.GatewayCloseCode.SHARDING_REQUIRED: raise errors.GatewayNeedsShardingError() diff --git a/tests/hikari/net/test_shard.py b/tests/hikari/net/test_shard.py index 823e155101..ee0c4505f6 100644 --- a/tests/hikari/net/test_shard.py +++ b/tests/hikari/net/test_shard.py @@ -188,7 +188,7 @@ async def test_right_stuff_is_included(self): client = shard.ShardConnection(url="...", token="...", connector=connector,) - assert client._cs_init_kwargs == dict(connector=connector) + assert client._cs_init_kwargs() == dict(connector=connector) @pytest.mark.asyncio @@ -218,7 +218,7 @@ async def test_right_stuff_is_included(self): client._url = url - assert client._ws_connect_kwargs == dict( + assert client._ws_connect_kwargs() == dict( url=url, compress=0, autoping=True, @@ -249,7 +249,7 @@ def non_hello_payload(self): def client(self, event_loop): asyncio.set_event_loop(event_loop) client = _helpers.unslot_class(shard.ShardConnection)(url="ws://localhost", token="xxx") - client = _helpers.mock_methods_on(client, except_=("connect",)) + client = _helpers.mock_methods_on(client, except_=("connect", "_cs_init_kwargs", "_ws_connect_kwargs")) client._receive = mock.AsyncMock(return_value=self.hello_payload) return client @@ -308,14 +308,14 @@ async def test_session_opened_with_expected_kwargs(self, client, client_session_ with self.suppress_closure(): await client.connect(client_session_t) assert client_session_t.args == () - assert client_session_t.kwargs == client._cs_init_kwargs + assert client_session_t.kwargs == client._cs_init_kwargs() @_helpers.timeout_after(10.0) async def test_ws_opened_with_expected_kwargs(self, client, client_session_t): with self.suppress_closure(): await client.connect(client_session_t) assert client_session_t.ws.args == () - assert client_session_t.ws.kwargs == client._ws_connect_kwargs + assert client_session_t.ws.kwargs == client._ws_connect_kwargs() @_helpers.timeout_after(10.0) async def test_ws_closed_afterwards(self, client, client_session_t): @@ -720,7 +720,7 @@ class TestHeartbeatKeepAlive: def client(self, event_loop): asyncio.set_event_loop(event_loop) client = _helpers.unslot_class(shard.ShardConnection)(token="1234", url="xxx") - client = _helpers.mock_methods_on(client, except_=("_heartbeat_keep_alive",)) + client = _helpers.mock_methods_on(client, except_=("_heartbeat_keep_alive", "_zombie_detector")) client._send = mock.AsyncMock() # This won't get set on the right event loop if we are not careful client.closed_event = asyncio.Event() @@ -729,12 +729,14 @@ def client(self, event_loop): @_helpers.timeout_after(10.0) async def test_loops_indefinitely_until_requesting_close_event_set(self, client, event_loop): - def send(_): - client.last_heartbeat_ack_received = time.perf_counter() + 3 + async def recv(): + await asyncio.sleep(0.1) + client.last_heartbeat_ack_received = time.perf_counter() + client.last_message_received = client.last_heartbeat_ack_receied - client._send = mock.AsyncMock(wraps=send) + client._send = mock.AsyncMock(wraps=lambda *_: asyncio.create_task(recv())) - task: asyncio.Future = event_loop.create_task(client._heartbeat_keep_alive(0.01)) + task: asyncio.Future = event_loop.create_task(client._heartbeat_keep_alive(0.5)) await asyncio.sleep(1.5) if task.done(): @@ -773,6 +775,20 @@ async def test_heartbeat_payload(self, client, seq): client._send.assert_awaited_once_with({"op": 1, "d": seq}) + @_helpers.assert_does_not_raise(type_=asyncio.TimeoutError) + async def test_zombie_detector_not_a_zombie(self): + client = mock.MagicMock() + client.last_message_received = time.perf_counter() - 5 + heartbeat_interval = 41.25 + shard.ShardConnection._zombie_detector(client, heartbeat_interval) + + @_helpers.assert_raises(type_=asyncio.TimeoutError) + async def test_zombie_detector_is_a_zombie(self): + client = mock.MagicMock() + client.last_message_received = time.perf_counter() - 500000 + heartbeat_interval = 41.25 + shard.ShardConnection._zombie_detector(client, heartbeat_interval) + @pytest.mark.asyncio class TestClose: From 933a52df3a813a25e6a8315bebba79c2173a50bd Mon Sep 17 00:00:00 2001 From: Faster Speeding Date: Thu, 16 Apr 2020 20:15:34 +0100 Subject: [PATCH 157/922] Closes #289: split up RESTClient into several component modules. --- hikari/clients/rest_clients.py | 4002 ----------------- hikari/clients/rest_clients/__init__.py | 85 + .../rest_clients/channels_component.py | 1114 +++++ hikari/clients/rest_clients/component_base.py | 58 + .../rest_clients/current_users_component.py | 241 + .../rest_clients/gateways_component.py | 59 + .../clients/rest_clients/guilds_component.py | 1939 ++++++++ .../clients/rest_clients/invites_component.py | 84 + .../clients/rest_clients/oauth2_component.py | 41 + .../rest_clients/reactions_component.py | 251 ++ .../clients/rest_clients/users_component.py | 57 + .../clients/rest_clients/voices_component.py | 46 + .../rest_clients/webhooks_component.py | 310 ++ hikari/embeds.py | 4 +- hikari/events.py | 2 +- hikari/internal/allowed_mentions.py | 100 + hikari/internal/meta.py | 36 + hikari/internal/pagination.py | 91 + hikari/net/ratelimits.py | 2 +- hikari/net/rest.py | 2 +- hikari/state/event_managers.py | 2 +- pylint.ini | 2 +- tests/hikari/clients/test_rest_client.py | 2762 ------------ .../clients/test_rest_clients/__init__.py | 18 + .../test_rest_clients/test___init__.py | 78 + .../test_channels_component.py | 820 ++++ .../test_rest_clients/test_component_base.py | 52 + .../test_current_users_component.py | 194 + .../test_gateways_component.py | 54 + .../test_guilds_component.py | 1146 +++++ .../test_invites_component.py | 69 + .../test_oauth2_component.py | 48 + .../test_reactions_component.py | 151 + .../test_rest_clients/test_users_component.py | 48 + .../test_voices_component.py | 46 + .../test_webhooks_component.py | 289 ++ tests/hikari/internal/__init__.py | 16 + .../hikari/internal/test_allowed_mentions.py | 119 + tests/hikari/internal/test_meta.py | 55 + tests/hikari/internal/test_pagination.py | 228 + tests/hikari/test_embeds.py | 7 +- tests/hikari/test_guilds.py | 18 +- 42 files changed, 7961 insertions(+), 6785 deletions(-) delete mode 100644 hikari/clients/rest_clients.py create mode 100644 hikari/clients/rest_clients/__init__.py create mode 100644 hikari/clients/rest_clients/channels_component.py create mode 100644 hikari/clients/rest_clients/component_base.py create mode 100644 hikari/clients/rest_clients/current_users_component.py create mode 100644 hikari/clients/rest_clients/gateways_component.py create mode 100644 hikari/clients/rest_clients/guilds_component.py create mode 100644 hikari/clients/rest_clients/invites_component.py create mode 100644 hikari/clients/rest_clients/oauth2_component.py create mode 100644 hikari/clients/rest_clients/reactions_component.py create mode 100644 hikari/clients/rest_clients/users_component.py create mode 100644 hikari/clients/rest_clients/voices_component.py create mode 100644 hikari/clients/rest_clients/webhooks_component.py create mode 100644 hikari/internal/allowed_mentions.py create mode 100644 hikari/internal/pagination.py delete mode 100644 tests/hikari/clients/test_rest_client.py create mode 100644 tests/hikari/clients/test_rest_clients/__init__.py create mode 100644 tests/hikari/clients/test_rest_clients/test___init__.py create mode 100644 tests/hikari/clients/test_rest_clients/test_channels_component.py create mode 100644 tests/hikari/clients/test_rest_clients/test_component_base.py create mode 100644 tests/hikari/clients/test_rest_clients/test_current_users_component.py create mode 100644 tests/hikari/clients/test_rest_clients/test_gateways_component.py create mode 100644 tests/hikari/clients/test_rest_clients/test_guilds_component.py create mode 100644 tests/hikari/clients/test_rest_clients/test_invites_component.py create mode 100644 tests/hikari/clients/test_rest_clients/test_oauth2_component.py create mode 100644 tests/hikari/clients/test_rest_clients/test_reactions_component.py create mode 100644 tests/hikari/clients/test_rest_clients/test_users_component.py create mode 100644 tests/hikari/clients/test_rest_clients/test_voices_component.py create mode 100644 tests/hikari/clients/test_rest_clients/test_webhooks_component.py create mode 100644 tests/hikari/internal/test_allowed_mentions.py create mode 100644 tests/hikari/internal/test_pagination.py diff --git a/hikari/clients/rest_clients.py b/hikari/clients/rest_clients.py deleted file mode 100644 index 6014f76005..0000000000 --- a/hikari/clients/rest_clients.py +++ /dev/null @@ -1,4002 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# Copyright © Nekoka.tt 2019-2020 -# -# This file is part of Hikari. -# -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . -"""Marshall wrappings for the REST implementation in :mod:`hikari.net.rest`. - -This provides an object-oriented interface for interacting with discord's REST -API. -""" - -__all__ = ["RESTClient"] - -import asyncio -import datetime -import types - -import typing - -from hikari.clients import configs -from hikari.internal import assertions -from hikari.internal import conversions -from hikari.internal import more_collections -from hikari.net import rest -from hikari import audit_logs -from hikari import channels as _channels -from hikari import colors -from hikari import embeds as _embeds -from hikari import emojis -from hikari import gateway_entities -from hikari import guilds -from hikari import invites -from hikari import media -from hikari import messages as _messages -from hikari import oauth2 -from hikari import permissions as _permissions -from hikari import snowflakes -from hikari import users -from hikari import voices -from hikari import webhooks - - -def _get_member_id(member: guilds.GuildMember) -> str: - return str(member.user.id) - - -class RESTClient: - """ - A marshalling object-oriented HTTP API. - - This component bridges the basic HTTP API exposed by - :obj:`~hikari.net.rest.LowLevelRestfulClient` and wraps it in a unit of - processing that can handle parsing API objects into Hikari entity objects. - - Parameters - ---------- - config : :obj:`~hikari.clients.configs.RESTConfig` - A HTTP configuration object. - - Note - ---- - For all endpoints where a ``reason`` argument is provided, this may be a - string inclusively between ``0`` and ``512`` characters length, with any - additional characters being cut off. - """ - - def __init__(self, config: configs.RESTConfig) -> None: - self._session = rest.LowLevelRestfulClient( - allow_redirects=config.allow_redirects, - connector=config.tcp_connector, - proxy_headers=config.proxy_headers, - proxy_auth=config.proxy_auth, - ssl_context=config.ssl_context, - verify_ssl=config.verify_ssl, - timeout=config.request_timeout, - token=f"{config.token_type} {config.token}" if config.token_type is not None else config.token, - version=config.rest_version, - ) - - async def close(self) -> None: - """Shut down the REST client safely.""" - await self._session.close() - - async def __aenter__(self) -> "RESTClient": - return self - - async def __aexit__( - self, exc_type: typing.Type[BaseException], exc_val: BaseException, exc_tb: types.TracebackType - ) -> None: - await self.close() - - async def fetch_gateway_url(self) -> str: - """Get a generic url used for establishing a Discord gateway connection. - - Returns - ------- - :obj:`~str` - A static URL to use to connect to the gateway with. - - Note - ---- - Users are expected to attempt to cache this result. - """ - return await self._session.get_gateway() - - async def fetch_gateway_bot(self) -> gateway_entities.GatewayBot: - """Get bot specific gateway information. - - Returns - ------- - :obj:`~hikari.gateway_entities.GatewayBot` - The bot specific gateway information object. - - Note - ---- - Unlike :meth:`fetch_gateway_url`, this requires a valid token to work. - """ - payload = await self._session.get_gateway_bot() - return gateway_entities.GatewayBot.deserialize(payload) - - async def fetch_audit_log( - self, - guild: snowflakes.HashableT[guilds.Guild], - *, - user: snowflakes.HashableT[users.User] = ..., - action_type: typing.Union[audit_logs.AuditLogEventType, int] = ..., - limit: int = ..., - before: typing.Union[datetime.datetime, snowflakes.HashableT[audit_logs.AuditLogEntry]] = ..., - ) -> audit_logs.AuditLog: - """Get an audit log object for the given guild. - - Parameters - ---------- - guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] - The object or ID of the guild to get the audit logs for. - user : :obj:`~typing.Union` [ :obj:`~hikari.users.User`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] - If specified, the object or ID of the user to filter by. - action_type : :obj:`~typing.Union` [ :obj:`~hikari.audit_logs.AuditLogEventType`, :obj:`~int` ] - If specified, the action type to look up. Passing a raw integer - for this may lead to unexpected behaviour. - limit : :obj:`~int` - If specified, the limit to apply to the number of records. - Defaults to ``50``. Must be between ``1`` and ``100`` inclusive. - before : :obj:`~typing.Union` [ :obj:`~datetime.datetime`, :obj:`~hikari.audit_logs.AuditLogEntry`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] - If specified, the object or ID of the entry that all retrieved - entries should have occurred befor. - - Returns - ------- - :obj:`~hikari.audit_logs.AuditLog` - An audit log object. - - Raises - ------ - :obj:`~hikari.errors.BadRequestHTTPError` - If any invalid snowflake IDs are passed; a snowflake may be invalid - due to it being outside of the range of a 64 bit integer. - :obj:`~hikari.errors.ForbiddenHTTPError` - If you lack the given permissions to view an audit log. - :obj:`~hikari.errors.NotFoundHTTPError` - If the guild does not exist. - """ - if isinstance(before, datetime.datetime): - before = str(snowflakes.Snowflake.from_datetime(before)) - elif before is not ...: - before = str(before.id if isinstance(before, snowflakes.UniqueEntity) else int(before)) - payload = await self._session.get_guild_audit_log( - guild_id=str(guild.id if isinstance(guilds, snowflakes.UniqueEntity) else int(guild)), - user_id=( - str(user.id if isinstance(user, snowflakes.UniqueEntity) else int(user)) if user is not ... else ... - ), - action_type=action_type, - limit=limit, - before=before, - ) - return audit_logs.AuditLog.deserialize(payload) - - def fetch_audit_log_entries_before( - self, - guild: snowflakes.HashableT[guilds.Guild], - *, - before: typing.Union[datetime.datetime, snowflakes.HashableT[audit_logs.AuditLogEntry], None] = None, - user: snowflakes.HashableT[users.User] = ..., - action_type: typing.Union[audit_logs.AuditLogEventType, int] = ..., - limit: typing.Optional[int] = None, - ) -> audit_logs.AuditLogIterator: - """Return an async iterator that retrieves a guild's audit log entries. - - This will return the audit log entries before a given entry object/ID or - from the first guild audit log entry. - - Parameters - ---------- - guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] - The ID or object of the guild to get audit log entries for - before : :obj:`~typing.Union` [ :obj:`~datetime.datetime`, :obj:`~hikari.audit_logs.AuditLogEntry`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ], optional - If specified, the ID or object of the entry or datetime to get - entries that happened before otherwise this will start from the - newest entry. - user : :obj:`~typing.Union` [ :obj:`~hikari.users.User`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] - If specified, the object or ID of the user to filter by. - action_type : :obj:`~typing.Union` [ :obj:`~hikari.audit_logs.AuditLogEventType`, :obj:`~int` ] - If specified, the action type to look up. Passing a raw integer - for this may lead to unexpected behaviour. - limit : :obj:`~int`, optional - If specified, the limit for how many entries this iterator should - return, defaults to unlimited. - - Example - ------- - .. code-block:: python - - audit_log_entries = client.fetch_audit_log_entries_before(guild, before=9876543, limit=6969) - async for entry in audit_log_entries: - if (user := audit_log_entries.users[entry.user_id]).is_bot: - await client.ban_member(guild, user) - - Note - ---- - The returned iterator has the attributes ``users``, ``members`` and - ``integrations`` which are mappings of snowflake IDs to objects for the - relevant entities that are referenced by the retrieved audit log - entries. These will be filled over time as more audit log entries are - fetched by the iterator. - - Returns - ------- - :obj:`~hikari.errors.BadRequestHTTPError` - If any invalid snowflake IDs are passed; a snowflake may be invalid - due to it being outside of the range of a 64 bit integer. - :obj:`~hikari.audit_logs.AuditLogIterator` - An async iterator of the audit log entries in a guild (from newest - to oldest). - """ - if isinstance(before, datetime.datetime): - before = str(snowflakes.Snowflake.from_datetime(before)) - elif before is not None: - before = str(before.id if isinstance(before, snowflakes.UniqueEntity) else int(before)) - return audit_logs.AuditLogIterator( - guild_id=str(guild.id if isinstance(guild, snowflakes.UniqueEntity) else int(guild)), - request=self._session.get_guild_audit_log, - before=before, - user_id=( - str(user.id if isinstance(user, snowflakes.UniqueEntity) else int(user)) if user is not ... else ... - ), - action_type=action_type, - limit=limit, - ) - - async def fetch_channel(self, channel: snowflakes.HashableT[_channels.Channel]) -> _channels.Channel: - """Get an up to date channel object from a given channel object or ID. - - Parameters - ---------- - channel : :obj:`~typing.Union` [ :obj:`~hikari.channels.Channel`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] - The object ID of the channel to look up. - - Returns - ------- - :obj:`~hikari.channels.Channel` - The channel object that has been found. - - Raises - ------ - :obj:`~hikari.errors.BadRequestHTTPError` - If any invalid snowflake IDs are passed; a snowflake may be invalid - due to it being outside of the range of a 64 bit integer. - :obj:`~hikari.errors.ForbiddenHTTPError` - If you don't have access to the channel. - :obj:`~hikari.errors.NotFoundHTTPError` - If the channel does not exist. - """ - payload = await self._session.get_channel( - channel_id=str(channel.id if isinstance(channel, snowflakes.UniqueEntity) else int(channel)) - ) - return _channels.deserialize_channel(payload) - - async def update_channel( - self, - channel: snowflakes.HashableT[_channels.Channel], - *, - name: str = ..., - position: int = ..., - topic: str = ..., - nsfw: bool = ..., - bitrate: int = ..., - user_limit: int = ..., - rate_limit_per_user: typing.Union[int, datetime.timedelta] = ..., - permission_overwrites: typing.Sequence[_channels.PermissionOverwrite] = ..., - parent_category: typing.Optional[snowflakes.HashableT[_channels.GuildCategory]] = ..., - reason: str = ..., - ) -> _channels.Channel: - """Update one or more aspects of a given channel ID. - - Parameters - ---------- - channel : :obj:`~typing.Union` [ :obj:`~hikari.channels.Channel`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] - The channel ID to update. - name : :obj:`~str` - If specified, the new name for the channel. This must be - inclusively between ``1`` and ``100`` characters in length. - position : :obj:`~int` - If specified, the position to change the channel to. - topic : :obj:`~str` - If specified, the topic to set. This is only applicable to - text channels. This must be inclusively between ``0`` and ``1024`` - characters in length. - nsfw : :obj:`~bool` - Mark the channel as being not safe for work (NSFW) if :obj:`~True`. - If :obj:`~False` or unspecified, then the channel is not marked as - NSFW. Will have no visible effect for non-text guild channels. - rate_limit_per_user : :obj:`~typing.Union` [ :obj:`~int`, :obj:`~datetime.timedelta` ] - If specified, the time delta of seconds the user has to wait - before sending another message. This will not apply to bots, or to - members with ``MANAGE_MESSAGES`` or ``MANAGE_CHANNEL`` permissions. - This must be inclusively between ``0`` and ``21600`` seconds. - bitrate : :obj:`~int` - If specified, the bitrate in bits per second allowable for the - channel. This only applies to voice channels and must be inclusively - between ``8000`` and ``96000`` for normal servers or ``8000`` and - ``128000`` for VIP servers. - user_limit : :obj:`~int` - If specified, the new max number of users to allow in a voice - channel. This must be between ``0`` and ``99`` inclusive, where - ``0`` implies no limit. - permission_overwrites : :obj:`~typing.Sequence` [ :obj:`~hikari.channels.PermissionOverwrite` ] - If specified, the new list of permission overwrites that are - category specific to replace the existing overwrites with. - parent_category : :obj:`~typing.Union` [ :obj:`~hikari.channels.Channel`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ], optional - If specified, the new parent category ID to set for the channel, - pass :obj:`~None` to unset. - reason : :obj:`~str` - If specified, the audit log reason explaining why the operation - was performed. - - Returns - ------- - :obj:`~hikari.channels.Channel` - The channel object that has been modified. - - Raises - ------ - :obj:`~hikari.errors.NotFoundHTTPError` - If the channel does not exist. - :obj:`~hikari.errors.ForbiddenHTTPError` - If you lack the permission to make the change. - :obj:`~hikari.errors.BadRequestHTTPError` - If you provide incorrect options for the corresponding channel type - (e.g. a ``bitrate`` for a text channel). - If any invalid snowflake IDs are passed; a snowflake may be invalid - due to it being outside of the range of a 64 bit integer. - """ - payload = await self._session.modify_channel( - channel_id=str(channel.id if isinstance(channel, snowflakes.UniqueEntity) else int(channel)), - name=name, - position=position, - topic=topic, - nsfw=nsfw, - bitrate=bitrate, - user_limit=user_limit, - rate_limit_per_user=( - int(rate_limit_per_user.total_seconds()) - if isinstance(rate_limit_per_user, datetime.timedelta) - else rate_limit_per_user - ), - permission_overwrites=( - [po.serialize() for po in permission_overwrites] if permission_overwrites is not ... else ... - ), - parent_id=( - str( - parent_category.id if isinstance(parent_category, snowflakes.UniqueEntity) else int(parent_category) - ) - if parent_category is not ... and parent_category is not None - else parent_category - ), - reason=reason, - ) - return _channels.deserialize_channel(payload) - - async def delete_channel(self, channel: snowflakes.HashableT[_channels.Channel]) -> None: - """Delete the given channel ID, or if it is a DM, close it. - - Parameters - ---------- - channel : :obj:`~typing.Union` [ :obj:`~hikari.channels.Channel`, :obj:`~hikari.snowflakes.Snowflake` :obj:`~str` ] - The object or ID of the channel to delete. - - Returns - ------- - :obj:`~None` - Nothing, unlike what the API specifies. This is done to maintain - consistency with other calls of a similar nature in this API - wrapper. - - Raises - ------ - :obj:`~hikari.errors.BadRequestHTTPError` - If any invalid snowflake IDs are passed; a snowflake may be invalid - due to it being outside of the range of a 64 bit integer. - :obj:`~hikari.errors.NotFoundHTTPError` - If the channel does not exist. - :obj:`~hikari.errors.ForbiddenHTTPError` - If you do not have permission to delete the channel. - - Note - ---- - Closing a DM channel won't raise an exception but will have no effect - and "closed" DM channels will not have to be reopened to send messages - in theme. - - Warning - ------- - Deleted channels cannot be un-deleted. - """ - await self._session.delete_close_channel( - channel_id=str(channel.id if isinstance(channel, snowflakes.UniqueEntity) else int(channel)) - ) - - def fetch_messages_after( - self, - channel: snowflakes.HashableT[_channels.Channel], - *, - after: typing.Union[datetime.datetime, snowflakes.HashableT[_messages.Message]] = 0, - limit: typing.Optional[int] = None, - ) -> typing.AsyncIterator[_messages.Message]: - """Return an async iterator that retrieves a channel's message history. - - This will return the message created after a given message object/ID or - from the first message in the channel. - - Parameters - ---------- - channel : :obj:`~typing.Union` [ :obj:`~hikari.channels.Channel`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] - The ID of the channel to retrieve the messages from. - limit : :obj:`~int` - If specified, the maximum number of how many messages this iterator - should return. - after : :obj:`~typing.Union` [ :obj:`~datetime.datetime`, :obj:`~hikari.channels.Channel`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] - A object or ID message. Only return messages sent AFTER this - message if it's specified else this will return every message after - (and including) the first message in the channel. - - Example - ------- - .. code-block:: python - - async for message in client.fetch_messages_after(channel, after=9876543, limit=3232): - if message.author.id in BLACKLISTED_USERS: - await client.ban_member(channel.guild_id, message.author) - - Returns - ------- - :obj:`~typing.AsyncIterator` [ :obj:`~hikari.messages.Message` ] - An async iterator that retrieves the channel's message objects. - - Raises - ------ - :obj:`~hikari.errors.BadRequestHTTPError` - If any invalid snowflake IDs are passed; a snowflake may be invalid - due to it being outside of the range of a 64 bit integer. - :obj:`~hikari.errors.ForbiddenHTTPError` - If you lack permission to read the channel. - :obj:`~hikari.errors.NotFoundHTTPError` - If the channel is not found, or the message - provided for one of the filter arguments is not found. - - Note - ---- - If you are missing the ``VIEW_CHANNEL`` permission, you will receive a - :obj:`~hikari.errors.ForbiddenHTTPError`. If you are instead missing - the ``READ_MESSAGE_HISTORY`` permission, you will always receive - zero results, and thus an empty list will be returned instead. - """ - if isinstance(after, datetime.datetime): - after = str(snowflakes.Snowflake.from_datetime(after)) - else: - after = str(after.id if isinstance(after, snowflakes.UniqueEntity) else int(after)) - return self._pagination_handler( - channel_id=str(channel.id if isinstance(channel, snowflakes.UniqueEntity) else int(channel)), - deserializer=_messages.Message.deserialize, - direction="after", - start=after, - request=self._session.get_channel_messages, - reversing=True, # This is the only known endpoint where reversing is needed. - limit=limit, - ) - - def fetch_messages_before( - self, - channel: snowflakes.HashableT[_channels.Channel], - *, - before: typing.Union[datetime.datetime, snowflakes.HashableT[_messages.Message], None] = None, - limit: typing.Optional[int] = None, - ) -> typing.AsyncIterator[_messages.Message]: - """Return an async iterator that retrieves a channel's message history. - - This returns the message created after a given message object/ID or - from the first message in the channel. - - Parameters - ---------- - channel : :obj:`~typing.Union` [ :obj:`~hikari.channels.Channel`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] - The ID of the channel to retrieve the messages from. - limit : :obj:`~int` - If specified, the maximum number of how many messages this iterator - should return. - before : :obj:`~typing.Union` [ :obj:`~datetime.datetime`, :obj:`~hikari.channels.Channel`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] - A message object or ID. Only return messages sent BEFORE - this message if this is specified else this will return every - message before (and including) the most recent message in the - channel. - - Example - ------- - .. code-block:: python - - async for message in client.fetch_messages_before(channel, before=9876543, limit=1231): - if message.content.lower().contains("delete this"): - await client.delete_message(channel, message) - - Returns - ------- - :obj:`~typing.AsyncIterator` [ :obj:`~hikari.messages.Message` ] - An async iterator that retrieves the channel's message objects. - - Raises - ------ - :obj:`~hikari.errors.BadRequestHTTPError` - If any invalid snowflake IDs are passed; a snowflake may be invalid - due to it being outside of the range of a 64 bit integer. - :obj:`~hikari.errors.ForbiddenHTTPError` - If you lack permission to read the channel. - :obj:`~hikari.errors.NotFoundHTTPError` - If the channel is not found, or the message - provided for one of the filter arguments is not found. - - Note - ---- - If you are missing the ``VIEW_CHANNEL`` permission, you will receive a - :obj:`~hikari.errors.ForbiddenHTTPError`. If you are instead missing - the ``READ_MESSAGE_HISTORY`` permission, you will always receive - zero results, and thus an empty list will be returned instead. - """ - if isinstance(before, datetime.datetime): - before = str(snowflakes.Snowflake.from_datetime(before)) - elif before is not None: - before = str(before.id if isinstance(before, snowflakes.UniqueEntity) else int(before)) - return self._pagination_handler( - channel_id=str(channel.id if isinstance(channel, snowflakes.UniqueEntity) else int(channel)), - deserializer=_messages.Message.deserialize, - direction="before", - start=before, - request=self._session.get_channel_messages, - reversing=False, - limit=limit, - ) - - async def fetch_messages_around( - self, - channel: snowflakes.HashableT[_channels.Channel], - around: typing.Union[datetime.datetime, snowflakes.HashableT[_messages.Message]], - *, - limit: int = ..., - ) -> typing.AsyncIterator[_messages.Message]: - """Return an async iterator that retrieves up to 100 messages. - - This will return messages in order from newest to oldest, is based - around the creation time of the supplied message object/ID and will - include the given message if it still exists. - - Parameters - ---------- - channel : :obj:`~typing.Union` [ :obj:`~hikari.channels.Channel`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] - The ID of the channel to retrieve the messages from. - around : :obj:`~typing.Union` [ :obj:`~datetime.datetime`, :obj:`~hikari.channels.Channel`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] - The object or ID of the message to get messages that were sent - AROUND it in the provided channel, unlike ``before`` and ``after``, - this argument is required and the provided message will also be - returned if it still exists. - limit : :obj:`~int` - If specified, the maximum number of how many messages this iterator - should return, cannot be more than `100` - - Example - ------- - .. code-block:: python - - async for message in client.fetch_messages_around(channel, around=9876543, limit=42): - if message.embeds and not message.author.is_bot: - await client.delete_message(channel, message) - - Returns - ------- - :obj:`~typing.AsyncIterator` [ :obj:`~hikari.messages.Message` ] - An async iterator that retrieves the found message objects. - - Raises - ------ - :obj:`~hikari.errors.BadRequestHTTPError` - If any invalid snowflake IDs are passed; a snowflake may be invalid - due to it being outside of the range of a 64 bit integer. - :obj:`~hikari.errors.ForbiddenHTTPError` - If you lack permission to read the channel. - :obj:`~hikari.errors.NotFoundHTTPError` - If the channel is not found, or the message - provided for one of the filter arguments is not found. - - Note - ---- - If you are missing the ``VIEW_CHANNEL`` permission, you will receive a - :obj:`~hikari.errors.ForbiddenHTTPError`. If you are instead missing - the ``READ_MESSAGE_HISTORY`` permission, you will always receive - zero results, and thus an empty list will be returned instead. - """ - if isinstance(around, datetime.datetime): - around = str(snowflakes.Snowflake.from_datetime(around)) - else: - around = str(around.id if isinstance(around, snowflakes.UniqueEntity) else int(around)) - for payload in await self._session.get_channel_messages( - channel_id=str(channel.id if isinstance(channel, snowflakes.UniqueEntity) else int(channel)), - limit=limit, - around=around, - ): - yield _messages.Message.deserialize(payload) - - @staticmethod - async def _pagination_handler( - deserializer: typing.Callable[[typing.Any], typing.Any], - direction: typing.Union[typing.Literal["before"], typing.Literal["after"]], - request: typing.Callable[..., typing.Coroutine[typing.Any, typing.Any, typing.Any]], - reversing: bool, - start: typing.Union[str, None], - limit: typing.Optional[int] = None, - id_getter: typing.Callable[[typing.Any], str] = lambda entity: str(entity.id), - **kwargs, - ) -> typing.AsyncIterator[typing.Any]: - """Generate an async iterator for handling paginated endpoints. - - This will handle Discord's ``before`` and ``after`` pagination. - - Parameters - ---------- - deserializer : :obj:`~typing.Callable` [ [ :obj:`~typing.Any` ], :obj:`~typing.Any` ] - The deserializer to use to deserialize raw elements. - direction : :obj:`~typing.Union` [ ``"before"``, ``"after"`` ] - The direction that this paginator should go in. - request : :obj:`~typing.Callable` [ ``...``, :obj:`~typing.Coroutine` [ :obj:`~typing.Any`, :obj:`~typing.Any`, :obj:`~typing.Any` ] ] - The function on :attr:`_session` that should be called to make - requests for this paginator. - reversing : :obj:`~bool` - Whether the retrieved array of objects should be reversed before - iterating through it, this is needed for certain endpoints like - ``fetch_messages_before`` where the order is static regardless of - if you're using ``before`` or ``after``. - start : :obj:`~int`, optional - The snowflake ID that this paginator should start at, ``0`` may be - passed for ``forward`` pagination to start at the first created - entity and :obj:`~None` may be passed for ``before`` pagination to - start at the newest entity (based on when it's snowflake timestamp). - limit : :obj:`~int`, optional - The amount of deserialized entities that the iterator should return - total, will be unlimited if set to :obj:`~None`. - id_getter : :obj:`~typing.Callable` [ [ :obj:`~typing.Any` ], :obj:`~str` ] - **kwargs - Kwargs to pass through to ``request`` for every request made along - with the current decided limit and direction snowflake. - - Returns - ------- - :obj:`~typing.AsyncIterator` [ :obj:`~typing.Any` ] - An async iterator of the found deserialized found objects. - - """ - while payloads := await request( - limit=100 if limit is None or limit > 100 else limit, - **{direction: start if start is not None else ...}, - **kwargs, - ): - if reversing: - payloads.reverse() - if limit is not None: - limit -= len(payloads) - - for payload in payloads: - entity = deserializer(payload) - yield entity - if limit == 0: - break - start = id_getter(entity) - - async def fetch_message( - self, channel: snowflakes.HashableT[_channels.Channel], message: snowflakes.HashableT[_messages.Message], - ) -> _messages.Message: - """Get a message from known channel that we can access. - - Parameters - ---------- - channel : :obj:`~typing.Union` [ :obj:`~hikari.channels.Channel`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] - The object or ID of the channel to get the message from. - message : :obj:`~typing.Union` [ :obj:`~hikari.messages.Message`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] - The object or ID of the message to retrieve. - - Returns - ------- - :obj:`~hikari.messages.Message` - The found message object. - - Note - ---- - This requires the ``READ_MESSAGE_HISTORY`` permission. - - Raises - ------ - :obj:`~hikari.errors.BadRequestHTTPError` - If any invalid snowflake IDs are passed; a snowflake may be invalid - due to it being outside of the range of a 64 bit integer. - :obj:`~hikari.errors.ForbiddenHTTPError` - If you lack permission to see the message. - :obj:`~hikari.errors.NotFoundHTTPError` - If the channel or message is not found. - """ - payload = await self._session.get_channel_message( - channel_id=str(channel.id if isinstance(channel, snowflakes.UniqueEntity) else int(channel)), - message_id=str(message.id if isinstance(message, snowflakes.UniqueEntity) else int(message)), - ) - return _messages.Message.deserialize(payload) - - @staticmethod - def _generate_allowed_mentions( - mentions_everyone: bool, - user_mentions: typing.Union[typing.Collection[snowflakes.HashableT[users.User]], bool], - role_mentions: typing.Union[typing.Collection[snowflakes.HashableT[guilds.GuildRole]], bool], - ) -> typing.Dict[str, typing.Sequence[str]]: - """Generate an allowed mentions object based on input mention rules. - - Parameters - ---------- - mentions_everyone : :obj:`~bool` - Whether ``@everyone`` and ``@here`` mentions should be resolved by - discord and lead to actual pings. - user_mentions : :obj:`~typing.Union` [ :obj:`~typing.Collection` [ :obj:`~typing.Union` [ :obj:`~hikari.users.User`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ], :obj:`~bool` ] - Either an array of user objects/IDs to allow mentions for, - :obj:`~True` to allow all user mentions or :obj:`~False` to block all - user mentions from resolving. - role_mentions : :obj:`~typing.Union` [ :obj:`~typing.Collection` [ :obj:`~typing.Union` [ :obj:`~hikari.guilds.GuildRole`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] ], :obj:`~bool` ] - Either an array of guild role objects/IDs to allow mentions for, - :obj:`~True` to allow all role mentions or :obj:`~False` to block all - role mentions from resolving. - - Returns - ------- - :obj:`~typing.Dict` [ :obj:`~str`, :obj:`~typing.Sequence` [ :obj:`~str` ] ] - The resulting allowed mentions dict object. - """ - parsed_mentions = [] - allowed_mentions = {} - if mentions_everyone is True: - parsed_mentions.append("everyone") - if user_mentions is True: - parsed_mentions.append("users") - # This covers both `False` and an array of IDs/objs by using `user_mentions or EMPTY_SEQUENCE`, where a - # resultant empty list will mean that all user mentions are blacklisted. - else: - allowed_mentions["users"] = list( - # dict.fromkeys is used to remove duplicate entries that would cause discord to return an error. - dict.fromkeys( - str(user.id if isinstance(user, snowflakes.UniqueEntity) else int(user)) - for user in user_mentions or more_collections.EMPTY_SEQUENCE - ) - ) - assertions.assert_that(len(allowed_mentions["users"]) <= 100, "Only up to 100 users can be provided.") - if role_mentions is True: - parsed_mentions.append("roles") - # This covers both `False` and an array of IDs/objs by using `user_mentions or EMPTY_SEQUENCE`, where a - # resultant empty list will mean that all role mentions are blacklisted. - else: - allowed_mentions["roles"] = list( - # dict.fromkeys is used to remove duplicate entries that would cause discord to return an error. - dict.fromkeys( - str(role.id if isinstance(role, snowflakes.UniqueEntity) else int(role)) - for role in role_mentions or more_collections.EMPTY_SEQUENCE - ) - ) - assertions.assert_that(len(allowed_mentions["roles"]) <= 100, "Only up to 100 roles can be provided.") - allowed_mentions["parse"] = parsed_mentions - # As a note, discord will also treat an empty `allowed_mentions` object as if it wasn't passed at all, so we - # want to use empty lists for blacklisting elements rather than just not including blacklisted elements. - return allowed_mentions - - async def create_message( - self, - channel: snowflakes.HashableT[_channels.Channel], - *, - content: str = ..., - nonce: str = ..., - tts: bool = ..., - files: typing.Collection[media.IO] = ..., - embed: _embeds.Embed = ..., - mentions_everyone: bool = True, - user_mentions: typing.Union[typing.Collection[snowflakes.HashableT[users.User]], bool] = True, - role_mentions: typing.Union[typing.Collection[snowflakes.HashableT[guilds.GuildRole]], bool] = True, - ) -> _messages.Message: - """Create a message in the given channel. - - Parameters - ---------- - channel : :obj:`~typing.Union` [ :obj:`~hikari.channels.Channel`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] - The channel or ID of the channel to send to. - content : :obj:`~str` - If specified, the message content to send with the message. - nonce : :obj:`~str` - If specified, an optional ID to send for opportunistic message - creation. This doesn't serve any real purpose for general use, - and can usually be ignored. - tts : :obj:`~bool` - If specified, whether the message will be sent as a TTS message. - files : :obj:`~typing.Collection` [ ``hikari.media.IO`` ] - If specified, this should be a list of inclusively between ``1`` and - ``5`` IO like media objects, as defined in :mod:`hikari.media`. - embed : :obj:`~hikari.embeds.Embed` - If specified, the embed object to send with the message. - mentions_everyone : :obj:`~bool` - Whether ``@everyone`` and ``@here`` mentions should be resolved by - discord and lead to actual pings, defaults to :obj:`~True`. - user_mentions : :obj:`~typing.Union` [ :obj:`~typing.Collection` [ :obj:`~typing.Union` [ :obj:`~hikari.users.User`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ], :obj:`~bool` ] - Either an array of user objects/IDs to allow mentions for, - :obj:`~True` to allow all user mentions or :obj:`~False` to block all - user mentions from resolving, defaults to :obj:`~True`. - role_mentions : :obj:`~typing.Union` [ :obj:`~typing.Collection` [ :obj:`~typing.Union` [ :obj:`~hikari.guilds.GuildRole`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] ], :obj:`~bool` ] - Either an array of guild role objects/IDs to allow mentions for, - :obj:`~True` to allow all role mentions or :obj:`~False` to block all - role mentions from resolving, defaults to :obj:`~True`. - - Returns - ------- - :obj:`~hikari.messages.Message` - The created message object. - - Raises - ------ - :obj:`~hikari.errors.NotFoundHTTPError` - If the channel is not found. - :obj:`~hikari.errors.BadRequestHTTPError` - This can be raised if the file is too large; if the embed exceeds - the defined limits; if the message content is specified only and - empty or greater than ``2000`` characters; if neither content, files - or embed are specified. - If any invalid snowflake IDs are passed; a snowflake may be invalid - due to it being outside of the range of a 64 bit integer. - :obj:`~hikari.errors.ForbiddenHTTPError` - If you lack permissions to send to this channel. - """ - payload = await self._session.create_message( - channel_id=str(channel.id if isinstance(channel, snowflakes.UniqueEntity) else int(channel)), - content=content, - nonce=nonce, - tts=tts, - files=await asyncio.gather(*(media.safe_read_file(file) for file in files)) if files is not ... else ..., - embed=embed.serialize() if embed is not ... else ..., - allowed_mentions=self._generate_allowed_mentions( - mentions_everyone=mentions_everyone, user_mentions=user_mentions, role_mentions=role_mentions - ), - ) - return _messages.Message.deserialize(payload) - - def safe_create_message( - self, - channel: snowflakes.HashableT[_channels.Channel], - *, - content: str = ..., - nonce: str = ..., - tts: bool = ..., - files: typing.Collection[media.IO] = ..., - embed: _embeds.Embed = ..., - mentions_everyone: bool = False, - user_mentions: typing.Union[typing.Collection[snowflakes.HashableT[users.User]], bool] = False, - role_mentions: typing.Union[typing.Collection[snowflakes.HashableT[guilds.GuildRole]], bool] = False, - ) -> typing.Coroutine[typing.Any, typing.Any, _messages.Message]: - """Create a message in the given channel with mention safety. - - This endpoint has the same signature as :attr:`create_message` with - the only difference being that ``mentions_everyone``, - ``user_mentions`` and ``role_mentions`` default to :obj:`~False`. - """ - return self.create_message( - channel=channel, - content=content, - nonce=nonce, - tts=tts, - files=files, - embed=embed, - mentions_everyone=mentions_everyone, - user_mentions=user_mentions, - role_mentions=role_mentions, - ) - - async def create_reaction( - self, - channel: snowflakes.HashableT[_channels.Channel], - message: snowflakes.HashableT[_messages.Message], - emoji: typing.Union[emojis.Emoji, str], - ) -> None: - """Add a reaction to the given message in the given channel. - - Parameters - ---------- - channel : :obj:`~typing.Union` [ :obj:`~hikari.channels.Channel`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] - The object or ID of the channel to add this reaction in. - message : :obj:`~typing.Union` [ :obj:`~hikari.messages.Message`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] - The object or ID of the message to add the reaction in. - emoji : :obj:`~typing.Union` [ :obj:`~hikari.emojis.Emoji`, :obj:`~str` ] - The emoji to add. This can either be an emoji object or a string - representation of an emoji. The string representation will be either - ``"name:id"`` for custom emojis else it's unicode character(s) (can - be UTF-32). - - Raises - ------ - :obj:`~hikari.errors.ForbiddenHTTPError` - If this is the first reaction using this specific emoji on this - message and you lack the ``ADD_REACTIONS`` permission. If you lack - ``READ_MESSAGE_HISTORY``, this may also raise this error. - :obj:`~hikari.errors.NotFoundHTTPError` - If the channel or message is not found, or if the emoji is not found. - :obj:`~hikari.errors.BadRequestHTTPError` - If the emoji is not valid, unknown, or formatted incorrectly. - If any invalid snowflake IDs are passed; a snowflake may be invalid - due to it being outside of the range of a 64 bit integer. - """ - await self._session.create_reaction( - channel_id=str(channel.id if isinstance(channel, snowflakes.UniqueEntity) else int(channel)), - message_id=str(message.id if isinstance(message, snowflakes.UniqueEntity) else int(message)), - emoji=str(getattr(emoji, "url_name", emoji)), - ) - - async def delete_reaction( - self, - channel: snowflakes.HashableT[_channels.Channel], - message: snowflakes.HashableT[_messages.Message], - emoji: typing.Union[emojis.Emoji, str], - ) -> None: - """Remove your own reaction from the given message in the given channel. - - Parameters - ---------- - channel : :obj:`~typing.Union` [ :obj:`~hikari.channels.Channel`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] - The object or ID of the channel to add this reaction in. - message : :obj:`~typing.Union` [ :obj:`~hikari.messages.Message`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] - The object or ID of the message to add the reaction in. - emoji : :obj:`~typing.Union` [ :obj:`~hikari.emojis.Emoji`, :obj:`~str` ] - The emoji to add. This can either be an emoji object or a - string representation of an emoji. The string representation will be - either ``"name:id"`` for custom emojis else it's unicode - character(s) (can be UTF-32). - - Raises - ------ - :obj:`~hikari.errors.ForbiddenHTTPError` - If this is the first reaction using this specific emoji on this - message and you lack the ``ADD_REACTIONS`` permission. If you lack - ``READ_MESSAGE_HISTORY``, this may also raise this error. - :obj:`~hikari.errors.NotFoundHTTPError` - If the channel or message is not found, or if the emoji is not - found. - :obj:`~hikari.errors.BadRequestHTTPError` - If the emoji is not valid, unknown, or formatted incorrectly. - If any invalid snowflake IDs are passed; a snowflake may be invalid - due to it being outside of the range of a 64 bit integer. - """ - await self._session.delete_own_reaction( - channel_id=str(channel.id if isinstance(channel, snowflakes.UniqueEntity) else int(channel)), - message_id=str(message.id if isinstance(message, snowflakes.UniqueEntity) else int(message)), - emoji=str(getattr(emoji, "url_name", emoji)), - ) - - async def delete_all_reactions( - self, channel: snowflakes.HashableT[_channels.Channel], message: snowflakes.HashableT[_messages.Message], - ) -> None: - """Delete all reactions from a given message in a given channel. - - Parameters - ---------- - channel : :obj:`~typing.Union` [ :obj:`~hikari.channels.Channel`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] - The object or ID of the channel to get the message from. - message : :obj:`~typing.Union` [ :obj:`~hikari.messages.Message`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] - The object or ID of the message to remove all reactions from. - - Raises - ------ - :obj:`~hikari.errors.BadRequestHTTPError` - If any invalid snowflake IDs are passed; a snowflake may be invalid - due to it being outside of the range of a 64 bit integer. - :obj:`~hikari.errors.NotFoundHTTPError` - If the channel or message is not found. - :obj:`~hikari.errors.ForbiddenHTTPError` - If you lack the ``MANAGE_MESSAGES`` permission. - """ - await self._session.delete_all_reactions( - channel_id=str(channel.id if isinstance(channel, snowflakes.UniqueEntity) else int(channel)), - message_id=str(message.id if isinstance(message, snowflakes.UniqueEntity) else int(message)), - ) - - async def delete_all_reactions_for_emoji( - self, - channel: snowflakes.HashableT[_channels.Channel], - message: snowflakes.HashableT[_messages.Message], - emoji: typing.Union[emojis.Emoji, str], - ) -> None: - """Remove all reactions for a single given emoji on a given message. - - Parameters - ---------- - channel : :obj:`~typing.Union` [ :obj:`~hikari.channels.Channel`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] - The object or ID of the channel to get the message from. - message : :obj:`~typing.Union` [ :obj:`~hikari.messages.Message`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] - The object or ID of the message to delete the reactions from. - emoji : :obj:`~typing.Union` [ :obj:`~hikari.emojis.Emoji`, :obj:`~str` ] - The object or string representatiom of the emoji to delete. The - string representation will be either ``"name:id"`` for custom emojis - else it's unicode character(s) (can be UTF-32). - - Raises - ------ - :obj:`~hikari.errors.BadRequestHTTPError` - If any invalid snowflake IDs are passed; a snowflake may be invalid - due to it being outside of the range of a 64 bit integer. - :obj:`~hikari.errors.NotFoundHTTPError` - If the channel or message or emoji or user is not found. - :obj:`~hikari.errors.ForbiddenHTTPError` - If you lack the ``MANAGE_MESSAGES`` permission, or the channel is a - DM channel. - """ - await self._session.delete_all_reactions_for_emoji( - channel_id=str(channel.id if isinstance(channel, snowflakes.UniqueEntity) else int(channel)), - message_id=str(message.id if isinstance(message, snowflakes.UniqueEntity) else int(message)), - emoji=str(getattr(emoji, "url_name", emoji)), - ) - - def fetch_reactors_after( - self, - channel: snowflakes.HashableT[_channels.Channel], - message: snowflakes.HashableT[_messages.Message], - emoji: typing.Union[emojis.Emoji, str], - *, - after: typing.Union[datetime.datetime, snowflakes.HashableT[users.User]] = 0, - limit: typing.Optional[int] = None, - ) -> typing.AsyncIterator[users.User]: - """Get an async iterator of the users who reacted to a message. - - This returns the users created after a given user object/ID or from the - oldest user who reacted. - - Parameters - ---------- - channel : :obj:`~typing.Union` [ :obj:`~hikari.channels.Channel`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] - The object or ID of the channel to get the message from. - message : :obj:`~typing.Union` [ :obj:`~hikari.messages.Message`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] - The object or ID of the message to get the reactions from. - emoji : :obj:`~typing.Union` [ :obj:`~hikari.emojis.Emoji`, :obj:`~str` ] - The emoji to get. This can either be it's object or the string - representation of the emoji. The string representation will be - either ``"name:id"`` for custom emojis else it's unicode - character(s) (can be UTF-32). - after : :obj:`~typing.Union` [ :obj:`~datetime.datetime`, :obj:`~hikari.users.User`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] - If specified, a object or ID user. If specified, only users with a - snowflake that is lexicographically greater than the value will be - returned. - limit : :obj:`~str` - If specified, the limit of the number of users this iterator should - return. - - Example - ------- - .. code-block:: python - - async for user in client.fetch_reactors_after(channel, message, emoji, after=9876543, limit=1231): - if user.is_bot: - await client.kick_member(channel.guild_id, user) - - Returns - ------- - :obj:`~typing.AsyncIterator` [ :obj:`~hikari.users.User` ] - An async iterator of user objects. - - Raises - ------ - :obj:`~hikari.errors.BadRequestHTTPError` - If any invalid snowflake IDs are passed; a snowflake may be invalid - due to it being outside of the range of a 64 bit integer. - :obj:`~hikari.errors.ForbiddenHTTPError` - If you lack access to the message. - :obj:`~hikari.errors.NotFoundHTTPError` - If the channel or message is not found. - """ - if isinstance(after, datetime.datetime): - after = str(snowflakes.Snowflake.from_datetime(after)) - else: - after = str(after.id if isinstance(after, snowflakes.UniqueEntity) else int(after)) - return self._pagination_handler( - channel_id=str(channel.id if isinstance(channel, snowflakes.UniqueEntity) else int(channel)), - message_id=str(message.id if isinstance(message, snowflakes.UniqueEntity) else int(message)), - emoji=getattr(emoji, "url_name", emoji), - deserializer=users.User.deserialize, - direction="after", - request=self._session.get_reactions, - reversing=False, - start=after, - limit=limit, - ) - - async def update_message( - self, - message: snowflakes.HashableT[_messages.Message], - channel: snowflakes.HashableT[_channels.Channel], - *, - content: typing.Optional[str] = ..., - embed: typing.Optional[_embeds.Embed] = ..., - flags: int = ..., - mentions_everyone: bool = True, - user_mentions: typing.Union[typing.Collection[snowflakes.HashableT[users.User]], bool] = True, - role_mentions: typing.Union[typing.Collection[snowflakes.HashableT[guilds.GuildRole]], bool] = True, - ) -> _messages.Message: - """Update the given message. - - Parameters - ---------- - channel : :obj:`~typing.Union` [ :obj:`~hikari.channels.Channel`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] - The object or ID of the channel to get the message from. - message : :obj:`~typing.Union` [ :obj:`~hikari.messages.Message`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] - The object or ID of the message to edit. - content : :obj:`~str`, optional - If specified, the string content to replace with in the message. - If :obj:`~None`, the content will be removed from the message. - embed : :obj:`~hikari.embeds.Embed`, optional - If specified, the embed to replace with in the message. - If :obj:`~None`, the embed will be removed from the message. - flags : :obj:`~hikari.messages.MessageFlag` - If specified, the new flags for this message, while a raw int may - be passed for this, this can lead to unexpected behaviour if it's - outside the range of the MessageFlag int flag. - mentions_everyone : :obj:`~bool` - Whether ``@everyone`` and ``@here`` mentions should be resolved by - discord and lead to actual pings, defaults to :obj:`~True`. - user_mentions : :obj:`~typing.Union` [ :obj:`~typing.Collection` [ :obj:`~typing.Union` [ :obj:`~hikari.users.User`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ], :obj:`~bool` ] - Either an array of user objects/IDs to allow mentions for, - :obj:`~True` to allow all user mentions or :obj:`~False` to block all - user mentions from resolving, defaults to :obj:`~True`. - role_mentions : :obj:`~typing.Union` [ :obj:`~typing.Collection` [ :obj:`~typing.Union` [ :obj:`~hikari.guilds.GuildRole`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] ], :obj:`~bool` ] - Either an array of guild role objects/IDs to allow mentions for, - :obj:`~True` to allow all role mentions or :obj:`~False` to block all - role mentions from resolving, defaults to :obj:`~True`. - - Returns - ------- - :obj:`~hikari.messages.Message` - The edited message object. - - Raises - ------ - :obj:`~hikari.errors.NotFoundHTTPError` - If the channel or message is not found. - :obj:`~hikari.errors.BadRequestHTTPError` - This can be raised if the embed exceeds the defined limits; - if the message content is specified only and empty or greater - than ``2000`` characters; if neither content, file or embed - are specified. - If any invalid snowflake IDs are passed; a snowflake may be invalid - due to it being outside of the range of a 64 bit integer. - :obj:`~hikari.errors.ForbiddenHTTPError` - If you try to edit ``content`` or ``embed`` or ``allowed_mentions` - on a message you did not author. - If you try to edit the flags on a message you did not author without - the ``MANAGE_MESSAGES`` permission. - """ - payload = await self._session.edit_message( - channel_id=str(channel.id if isinstance(channel, snowflakes.UniqueEntity) else int(channel)), - message_id=str(message.id if isinstance(message, snowflakes.UniqueEntity) else int(message)), - content=content, - embed=embed.serialize() if embed is not ... and embed is not None else embed, - flags=flags, - allowed_mentions=self._generate_allowed_mentions( - mentions_everyone=mentions_everyone, user_mentions=user_mentions, role_mentions=role_mentions, - ), - ) - return _messages.Message.deserialize(payload) - - def safe_update_message( - self, - message: snowflakes.HashableT[_messages.Message], - channel: snowflakes.HashableT[_channels.Channel], - *, - content: typing.Optional[str] = ..., - embed: typing.Optional[_embeds.Embed] = ..., - flags: int = ..., - mentions_everyone: bool = False, - user_mentions: typing.Union[typing.Collection[snowflakes.HashableT[users.User]], bool] = False, - role_mentions: typing.Union[typing.Collection[snowflakes.HashableT[guilds.GuildRole]], bool] = False, - ) -> typing.Coroutine[typing.Any, typing.Any, _messages.Message]: - """Update a message in the given channel with mention safety. - - This endpoint has the same signature as :attr:`execute_webhook` with - the only difference being that ``mentions_everyone``, - ``user_mentions`` and ``role_mentions`` default to :obj:`~False`. - """ - return self.update_message( - message=message, - channel=channel, - content=content, - embed=embed, - flags=flags, - mentions_everyone=mentions_everyone, - user_mentions=user_mentions, - role_mentions=role_mentions, - ) - - async def delete_messages( - self, - channel: snowflakes.HashableT[_channels.Channel], - message: snowflakes.HashableT[_messages.Message], - *additional_messages: snowflakes.HashableT[_messages.Message], - ) -> None: - """Delete a message in a given channel. - - Parameters - ---------- - channel : :obj:`~typing.Union` [ :obj:`~hikari.channels.Channel`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] - The object or ID of the channel to get the message from. - message : :obj:`~typing.Union` [ :obj:`~hikari.messages.Message`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] - The object or ID of the message to delete. - *additional_messages : :obj:`~typing.Union` [ :obj:`~hikari.messages.Message`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] - Objects and/or IDs of additional messages to delete in the same - channel, in total you can delete up to 100 messages in a request. - - Raises - ------ - :obj:`~hikari.errors.BadRequestHTTPError` - If any invalid snowflake IDs are passed; a snowflake may be invalid - due to it being outside of the range of a 64 bit integer. - :obj:`~hikari.errors.ForbiddenHTTPError` - If you did not author the message and are in a DM, or if you did - not author the message and lack the ``MANAGE_MESSAGES`` - permission in a guild channel. - :obj:`~hikari.errors.NotFoundHTTPError` - If the channel or message is not found. - :obj:`~ValueError` - If you try to delete over ``100`` messages in a single request. - - Note - ---- - This can only be used on guild text channels. - Any message IDs that do not exist or are invalid still add towards the - total ``100`` max messages to remove. This can only delete messages - that are newer than ``2`` weeks in age. If any of the messages ar - older than ``2`` weeks then this call will fail. - """ - if additional_messages: - messages = list( - # dict.fromkeys is used to remove duplicate entries that would cause discord to return an error. - dict.fromkeys( - str(m.id if isinstance(m, snowflakes.UniqueEntity) else int(m)) - for m in (message, *additional_messages) - ) - ) - assertions.assert_that( - len(messages) <= 100, "Only up to 100 messages can be bulk deleted in a single request." - ) - - if len(messages) > 1: - await self._session.bulk_delete_messages( - channel_id=str(channel.id if isinstance(channel, snowflakes.UniqueEntity) else int(channel)), - messages=messages, - ) - return None - - await self._session.delete_message( - channel_id=str(channel.id if isinstance(channel, snowflakes.UniqueEntity) else int(channel)), - message_id=str(message.id if isinstance(message, snowflakes.UniqueEntity) else int(message)), - ) - - async def update_channel_overwrite( - self, - channel: snowflakes.HashableT[_messages.Message], - overwrite: typing.Union[_channels.PermissionOverwrite, users.User, guilds.GuildRole, snowflakes.Snowflake, int], - target_type: typing.Union[_channels.PermissionOverwriteType, str], - *, - allow: typing.Union[_permissions.Permission, int] = ..., - deny: typing.Union[_permissions.Permission, int] = ..., - reason: str = ..., - ) -> None: - """Edit permissions for a given channel. - - Parameters - ---------- - channel : :obj:`~typing.Union` [ :obj:`~hikari.messages.Message`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] - The object or ID of the channel to edit permissions for. - overwrite : :obj:`~typing.Union` [ :obj:`~hikari.channels.PermissionOverwrite`, :obj:`~hikari.guilds.GuildRole`, :obj:`~hikari.users.User`, :obj:`~hikari.snowflakes.Snowflake` , :obj:`~int` ] - The object or ID of the target member or role to edit/create the - overwrite for. - target_type : :obj:`~typing.Union` [ :obj:`~hikari.channels.PermissionOverwriteType`, :obj:`~int` ] - The type of overwrite, passing a raw string that's outside of the - enum's range for this may lead to unexpected behaviour. - allow : :obj:`~typing.Union` [ :obj:`~hikari.permissions.Permission`, :obj:`~int` ] - If specified, the value of all permissions to set to be allowed, - passing a raw integer for this may lead to unexpected behaviour. - deny : :obj:`~typing.Union` [ :obj:`~hikari.permissions.Permission`, :obj:`~int` ] - If specified, the value of all permissions to set to be denied, - passing a raw integer for this may lead to unexpected behaviour. - reason : :obj:`~str` - If specified, the audit log reason explaining why the operation - was performed. - - Raises - ------ - :obj:`~hikari.errors.BadRequestHTTPError` - If any invalid snowflake IDs are passed; a snowflake may be invalid - due to it being outside of the range of a 64 bit integer. - :obj:`~hikari.errors.NotFoundHTTPError` - If the target channel or overwrite doesn't exist. - :obj:`~hikari.errors.ForbiddenHTTPError` - If you lack permission to do this. - """ - await self._session.edit_channel_permissions( - channel_id=str(channel.id if isinstance(channel, snowflakes.UniqueEntity) else int(channel)), - overwrite_id=str(overwrite.id if isinstance(overwrite, snowflakes.UniqueEntity) else int(overwrite)), - type_=target_type, - allow=allow, - deny=deny, - reason=reason, - ) - - async def fetch_invites_for_channel( - self, channel: snowflakes.HashableT[_channels.Channel] - ) -> typing.Sequence[invites.InviteWithMetadata]: - """Get invites for a given channel. - - Parameters - ---------- - channel : :obj:`~typing.Union` [ :obj:`~hikari.channels.Channel`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] - The object or ID of the channel to get invites for. - - Returns - ------- - :obj:`~typing.Sequence` [ :obj:`~hikari.invites.InviteWithMetadata` ] - A list of invite objects. - - Raises - ------ - :obj:`~hikari.errors.BadRequestHTTPError` - If any invalid snowflake IDs are passed; a snowflake may be invalid - due to it being outside of the range of a 64 bit integer. - :obj:`~hikari.errors.ForbiddenHTTPError` - If you lack the ``MANAGE_CHANNELS`` permission. - :obj:`~hikari.errors.NotFoundHTTPError` - If the channel does not exist. - """ - payload = await self._session.get_channel_invites( - channel_id=str(channel.id if isinstance(channel, snowflakes.UniqueEntity) else int(channel)) - ) - return [invites.InviteWithMetadata.deserialize(invite) for invite in payload] - - async def create_invite_for_channel( - self, - channel: snowflakes.HashableT[_channels.Channel], - *, - max_age: typing.Union[int, datetime.timedelta] = ..., - max_uses: int = ..., - temporary: bool = ..., - unique: bool = ..., - target_user: snowflakes.HashableT[users.User] = ..., - target_user_type: typing.Union[invites.TargetUserType, int] = ..., - reason: str = ..., - ) -> invites.InviteWithMetadata: - """Create a new invite for the given channel. - - Parameters - ---------- - channel : :obj:`~typing.Union` [ :obj:`~datetime.timedelta`, :obj:`~str` ] - The object or ID of the channel to create the invite for. - max_age : :obj:`~int` - If specified, the seconds time delta for the max age of the invite, - defaults to ``86400`` seconds (``24`` hours). - Set to ``0`` seconds to never expire. - max_uses : :obj:`~int` - If specified, the max number of uses this invite can have, or ``0`` - for unlimited (as per the default). - temporary : :obj:`~bool` - If specified, whether to grant temporary membership, meaning the - user is kicked when their session ends unless they are given a role. - unique : :obj:`~bool` - If specified, whether to try to reuse a similar invite. - target_user : :obj:`~typing.Union` [ :obj:`~hikari.users.User`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] - If specified, the object or ID of the user this invite should - target. - target_user_type : :obj:`~typing.Union` [ :obj:`~hikari.invites.TargetUserType`, :obj:`~int` ] - If specified, the type of target for this invite, passing a raw - integer for this may lead to unexpected results. - reason : :obj:`~str` - If specified, the audit log reason explaining why the operation - was performed. - - Returns - ------- - :obj:`~hikari.invites.InviteWithMetadata` - The created invite object. - - Raises - ------ - :obj:`~hikari.errors.ForbiddenHTTPError` - If you lack the ``CREATE_INSTANT_MESSAGES`` permission. - :obj:`~hikari.errors.NotFoundHTTPError` - If the channel does not exist. - :obj:`~hikari.errors.BadRequestHTTPError` - If the arguments provided are not valid (e.g. negative age, etc). - If any invalid snowflake IDs are passed; a snowflake may be invalid - due to it being outside of the range of a 64 bit integer. - """ - payload = await self._session.create_channel_invite( - channel_id=str(channel.id if isinstance(channel, snowflakes.UniqueEntity) else int(channel)), - max_age=int(max_age.total_seconds()) if isinstance(max_age, datetime.timedelta) else max_age, - max_uses=max_uses, - temporary=temporary, - unique=unique, - target_user=( - str(target_user.id if isinstance(target_user, snowflakes.UniqueEntity) else int(target_user)) - if target_user is not ... - else ... - ), - target_user_type=target_user_type, - reason=reason, - ) - return invites.InviteWithMetadata.deserialize(payload) - - async def delete_channel_overwrite( - self, - channel: snowflakes.HashableT[_channels.Channel], - overwrite: typing.Union[_channels.PermissionOverwrite, guilds.GuildRole, users.User, snowflakes.Snowflake, int], - ) -> None: - """Delete a channel permission overwrite for a user or a role. - - Parameters - ---------- - channel : :obj:`~typing.Union` [ :obj:`~hikari.channels.Channel`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] - The object or ID of the channel to delete the overwrite from. - overwrite : :obj:`~typing.Union` [ :obj:`~hikari.channels.PermissionOverwrite`, :obj:`~hikari.guilds.GuildRole`, :obj:`~hikari.users.User`, :obj:`~hikari.snowflakes.Snowflake`, :obj:int ] - The ID of the entity this overwrite targets. - - Raises - ------ - :obj:`~hikari.errors.BadRequestHTTPError` - If any invalid snowflake IDs are passed; a snowflake may be invalid - due to it being outside of the range of a 64 bit integer. - :obj:`~hikari.errors.NotFoundHTTPError` - If the overwrite or channel do not exist. - :obj:`~hikari.errors.ForbiddenHTTPError` - If you lack the ``MANAGE_ROLES`` permission for that channel. - """ - await self._session.delete_channel_permission( - channel_id=str(channel.id if isinstance(channel, snowflakes.UniqueEntity) else int(channel)), - overwrite_id=str(overwrite.id if isinstance(overwrite, snowflakes.UniqueEntity) else int(overwrite)), - ) - - async def trigger_typing(self, channel: snowflakes.HashableT[_channels.Channel]) -> None: - """Trigger the typing indicator for ``10`` seconds in a channel. - - Parameters - ---------- - channel : :obj:`~typing.Union` [ :obj:`~hikari.channels.Channel`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] - The object or ID of the channel to appear to be typing in. - - Raises - ------ - :obj:`~hikari.errors.BadRequestHTTPError` - If any invalid snowflake IDs are passed; a snowflake may be invalid - due to it being outside of the range of a 64 bit integer. - :obj:`~hikari.errors.NotFoundHTTPError` - If the channel is not found. - :obj:`~hikari.errors.ForbiddenHTTPError` - If you are not able to type in the channel. - """ - await self._session.trigger_typing_indicator( - channel_id=str(channel.id if isinstance(channel, snowflakes.UniqueEntity) else int(channel)) - ) - - async def fetch_pins( - self, channel: snowflakes.HashableT[_channels.Channel] - ) -> typing.Mapping[snowflakes.Snowflake, _messages.Message]: - """Get pinned messages for a given channel. - - Parameters - ---------- - channel : :obj:`~typing.Union` [ :obj:`~hikari.channels.Channel`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] - The object or ID of the channel to get messages from. - - Returns - ------- - :obj:`~typing.Mapping` [ :obj:`~hikari.snowflakes.Snowflake`, :obj:`~hikari.messages.Message` ] - A list of message objects. - - Raises - ------ - :obj:`~hikari.errors.BadRequestHTTPError` - If any invalid snowflake IDs are passed; a snowflake may be invalid - due to it being outside of the range of a 64 bit integer. - :obj:`~hikari.errors.NotFoundHTTPError` - If the channel is not found. - :obj:`~hikari.errors.ForbiddenHTTPError` - If you are not able to see the channel. - - Note - ---- - If you are not able to see the pinned message (eg. you are missing - ``READ_MESSAGE_HISTORY`` and the pinned message is an old message), it - will not be returned. - """ - payload = await self._session.get_pinned_messages( - channel_id=str(channel.id if isinstance(channel, snowflakes.UniqueEntity) else int(channel)) - ) - return {message.id: message for message in map(_messages.Message.deserialize, payload)} - - async def pin_message( - self, channel: snowflakes.HashableT[_channels.Channel], message: snowflakes.HashableT[_messages.Message], - ) -> None: - """Add a pinned message to the channel. - - Parameters - ---------- - channel : :obj:`~typing.Union` [ :obj:`~hikari.channels.Channel`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] - The object or ID of the channel to pin a message to. - message : :obj:`~typing.Union` [ :obj:`~hikari.messages.Message`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] - The object or ID of the message to pin. - - Raises - ------ - :obj:`~hikari.errors.BadRequestHTTPError` - If any invalid snowflake IDs are passed; a snowflake may be invalid - due to it being outside of the range of a 64 bit integer. - :obj:`~hikari.errors.ForbiddenHTTPError` - If you lack the ``MANAGE_MESSAGES`` permission. - :obj:`~hikari.errors.NotFoundHTTPError` - If the message or channel do not exist. - """ - await self._session.add_pinned_channel_message( - channel_id=str(channel.id if isinstance(channel, snowflakes.UniqueEntity) else int(channel)), - message_id=str(message.id if isinstance(message, snowflakes.UniqueEntity) else int(message)), - ) - - async def unpin_message( - self, channel: snowflakes.HashableT[_channels.Channel], message: snowflakes.HashableT[_messages.Message], - ) -> None: - """Remove a pinned message from the channel. - - This will only unpin the message, not delete it. - - Parameters - ---------- - channel : :obj:`~typing.Union` [ :obj:`~hikari.channels.Channel`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] - The ID of the channel to remove a pin from. - message : :obj:`~typing.Union` [ :obj:`~hikari.messages.Message`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] - The object or ID of the message to unpin. - - Raises - ------ - :obj:`~hikari.errors.BadRequestHTTPError` - If any invalid snowflake IDs are passed; a snowflake may be invalid - due to it being outside of the range of a 64 bit integer. - :obj:`~hikari.errors.ForbiddenHTTPError` - If you lack the ``MANAGE_MESSAGES`` permission. - :obj:`~hikari.errors.NotFoundHTTPError` - If the message or channel do not exist. - """ - await self._session.delete_pinned_channel_message( - channel_id=str(channel.id if isinstance(channel, snowflakes.UniqueEntity) else int(channel)), - message_id=str(message.id if isinstance(message, snowflakes.UniqueEntity) else int(message)), - ) - - async def fetch_guild_emoji( - self, guild: snowflakes.HashableT[guilds.Guild], emoji: snowflakes.HashableT[emojis.GuildEmoji], - ) -> emojis.GuildEmoji: - """Get an updated emoji object from a specific guild. - - Parameters - ---------- - guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] - The object or ID of the guild to get the emoji from. - emoji : :obj:`~typing.Union` [ :obj:`~hikari.emojis.GuildEmoji`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] - The object or ID of the emoji to get. - - Returns - ------- - :obj:`~hikari.emojis.GuildEmoji` - A guild emoji object. - - Raises - ------ - :obj:`~hikari.errors.BadRequestHTTPError` - If any invalid snowflake IDs are passed; a snowflake may be invalid - due to it being outside of the range of a 64 bit integer. - :obj:`~hikari.errors.NotFoundHTTPError` - If either the guild or the emoji aren't found. - :obj:`~hikari.errors.ForbiddenHTTPError` - If you aren't a member of said guild. - """ - payload = await self._session.get_guild_emoji( - guild_id=str(guild.id if isinstance(guild, snowflakes.UniqueEntity) else int(guild)), - emoji_id=str(emoji.id if isinstance(emoji, snowflakes.UniqueEntity) else int(emoji)), - ) - return emojis.GuildEmoji.deserialize(payload) - - async def fetch_guild_emojis(self, guild: snowflakes.HashableT[guilds.Guild]) -> typing.Sequence[emojis.GuildEmoji]: - """Get emojis for a given guild object or ID. - - Parameters - ---------- - guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] - The object or ID of the guild to get the emojis for. - - Returns - ------- - :obj:`~typing.Sequence` [ :obj:`~hikari.emojis.GuildEmoji` ] - A list of guild emoji objects. - - Raises - ------ - :obj:`~hikari.errors.BadRequestHTTPError` - If any invalid snowflake IDs are passed; a snowflake may be invalid - due to it being outside of the range of a 64 bit integer. - :obj:`~hikari.errors.NotFoundHTTPError` - If the guild is not found. - :obj:`~hikari.errors.ForbiddenHTTPError` - If you aren't a member of the guild. - """ - payload = await self._session.list_guild_emojis( - guild_id=str(guild.id if isinstance(guild, snowflakes.UniqueEntity) else int(guild)) - ) - return [emojis.GuildEmoji.deserialize(emoji) for emoji in payload] - - async def create_guild_emoji( - self, - guild: snowflakes.HashableT[guilds.GuildRole], - name: str, - image_data: conversions.FileLikeT, - *, - roles: typing.Sequence[snowflakes.HashableT[guilds.GuildRole]] = ..., - reason: str = ..., - ) -> emojis.GuildEmoji: - """Create a new emoji for a given guild. - - Parameters - ---------- - guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.GuildRole`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] - The object or ID of the guild to create the emoji in. - name : :obj:`~str` - The new emoji's name. - image_data : ``hikari.internal.conversions.FileLikeT`` - The ``128x128`` image data. - roles : :obj:`~typing.Sequence` [ :obj:`~typing.Union` [ :obj:`~hikari.guilds.GuildRole`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] ] - If specified, a list of role objects or IDs for which the emoji - will be whitelisted. If empty, all roles are whitelisted. - reason : :obj:`~str` - If specified, the audit log reason explaining why the operation - was performed. - - Returns - ------- - :obj:`~hikari.emojis.GuildEmoji` - The newly created emoji object. - - Raises - ------ - :obj:`~ValueError` - If ``image`` is :obj:`~None`. - :obj:`~hikari.errors.NotFoundHTTPError` - If the guild is not found. - :obj:`~hikari.errors.ForbiddenHTTPError` - If you either lack the ``MANAGE_EMOJIS`` permission or aren't a - member of said guild. - :obj:`~hikari.errors.BadRequestHTTPError` - If you attempt to upload an image larger than ``256kb``, an empty - image or an invalid image format. - If any invalid snowflake IDs are passed; a snowflake may be invalid - due to it being outside of the range of a 64 bit integer. - """ - payload = await self._session.create_guild_emoji( - guild_id=str(guild.id if isinstance(guild, snowflakes.UniqueEntity) else int(guild)), - name=name, - image=conversions.get_bytes_from_resource(image_data), - roles=[str(role.id if isinstance(role, snowflakes.UniqueEntity) else int(role)) for role in roles] - if roles is not ... - else ..., - reason=reason, - ) - return emojis.GuildEmoji.deserialize(payload) - - async def update_guild_emoji( - self, - guild: snowflakes.HashableT[guilds.Guild], - emoji: snowflakes.HashableT[emojis.GuildEmoji], - *, - name: str = ..., - roles: typing.Sequence[snowflakes.HashableT[guilds.GuildRole]] = ..., - reason: str = ..., - ) -> emojis.GuildEmoji: - """Edits an emoji of a given guild. - - Parameters - ---------- - guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] - The object or ID of the guild to which the emoji to edit belongs to. - emoji : :obj:`~typing.Union` [ :obj:`~hikari.emojis.GuildEmoji`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] - The object or ID of the emoji to edit. - name : :obj:`~str` - If specified, a new emoji name string. Keep unspecified to leave the - name unchanged. - roles : :obj:`~typing.Sequence` [ :obj:`~typing.Union` [ :obj:`~hikari.guilds.GuildRole`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] ] - If specified, a list of objects or IDs for the new whitelisted - roles. Set to an empty list to whitelist all roles. - Keep unspecified to leave the same roles already set. - reason : :obj:`~str` - If specified, the audit log reason explaining why the operation - was performed. - - Returns - ------- - :obj:`~hikari.emojis.GuildEmoji` - The updated emoji object. - - Raises - ------ - :obj:`~hikari.errors.BadRequestHTTPError` - If any invalid snowflake IDs are passed; a snowflake may be invalid - due to it being outside of the range of a 64 bit integer. - :obj:`~hikari.errors.NotFoundHTTPError` - If either the guild or the emoji aren't found. - :obj:`~hikari.errors.ForbiddenHTTPError` - If you either lack the ``MANAGE_EMOJIS`` permission or are not a - member of the given guild. - """ - payload = await self._session.modify_guild_emoji( - guild_id=str(guild.id if isinstance(guild, snowflakes.UniqueEntity) else int(guild)), - emoji_id=str(emoji.id if isinstance(emoji, snowflakes.UniqueEntity) else int(emoji)), - name=name, - roles=[str(role.id if isinstance(role, snowflakes.UniqueEntity) else int(role)) for role in roles] - if roles is not ... - else ..., - reason=reason, - ) - return emojis.GuildEmoji.deserialize(payload) - - async def delete_guild_emoji( - self, guild: snowflakes.HashableT[guilds.Guild], emoji: snowflakes.HashableT[emojis.GuildEmoji], - ) -> None: - """Delete an emoji from a given guild. - - Parameters - ---------- - guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] - The object or ID of the guild to delete the emoji from. - emoji : :obj:`~typing.Union` [ :obj:`~hikari.emojis.GuildEmoji`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] - The object or ID of the guild emoji to be deleted. - - Raises - ------ - :obj:`~hikari.errors.BadRequestHTTPError` - If any invalid snowflake IDs are passed; a snowflake may be invalid - due to it being outside of the range of a 64 bit integer. - :obj:`~hikari.errors.NotFoundHTTPError` - If either the guild or the emoji aren't found. - :obj:`~hikari.errors.ForbiddenHTTPError` - If you either lack the ``MANAGE_EMOJIS`` permission or aren't a - member of said guild. - """ - await self._session.delete_guild_emoji( - guild_id=str(guild.id if isinstance(guild, snowflakes.UniqueEntity) else int(guild)), - emoji_id=str(emoji.id if isinstance(emoji, snowflakes.UniqueEntity) else int(emoji)), - ) - - async def create_guild( - self, - name: str, - *, - region: typing.Union[voices.VoiceRegion, str] = ..., - icon_data: conversions.FileLikeT = ..., - verification_level: typing.Union[guilds.GuildVerificationLevel, int] = ..., - default_message_notifications: typing.Union[guilds.GuildMessageNotificationsLevel, int] = ..., - explicit_content_filter: typing.Union[guilds.GuildExplicitContentFilterLevel, int] = ..., - roles: typing.Sequence[guilds.GuildRole] = ..., - channels: typing.Sequence[_channels.GuildChannelBuilder] = ..., - ) -> guilds.Guild: - """Create a new guild. - - Warning - ------- - Can only be used by bots in less than ``10`` guilds. - - Parameters - ---------- - name : :obj:`~str` - The name string for the new guild (``2-100`` characters). - region : :obj:`~str` - If specified, the voice region ID for new guild. You can use - :meth:`fetch_guild_voice_regions` to see which region IDs are - available. - icon_data : ``hikari.internal.conversions.FileLikeT`` - If specified, the guild icon image data. - verification_level : :obj:`~typing.Union` [ :obj:`~hikari.guilds.GuildVerificationLevel`, :obj:`~int` ] - If specified, the verification level. Passing a raw int for this - may lead to unexpected behaviour. - default_message_notifications : :obj:`~typing.Union` [ :obj:`~hikari.guilds.GuildMessageNotificationsLevel`, :obj:`~int` ] - If specified, the default notification level. Passing a raw int for - this may lead to unexpected behaviour. - explicit_content_filter : :obj:`~typing.Union` [ :obj:`~hikari.guilds.GuildExplicitContentFilterLevel`, :obj:`~int` ] - If specified, the explicit content filter. Passing a raw int for - this may lead to unexpected behaviour. - roles : :obj:`~typing.Sequence` [ :obj:`~hikari.guilds.GuildRole` ] - If specified, an array of role objects to be created alongside the - guild. First element changes the ``@everyone`` role. - channels : :obj:`~typing.Sequence` [ :obj:`~hikari.channels.GuildChannelBuilder` ] - If specified, an array of guild channel builder objects to be - created within the guild. - - Returns - ------- - :obj:`~hikari.guilds.Guild` - The newly created guild object. - - Raises - ------ - :obj:`~hikari.errors.ForbiddenHTTPError` - If you are in ``10`` or more guilds. - :obj:`~hikari.errors.BadRequestHTTPError` - If you provide unsupported fields like ``parent_id`` in channel - objects. - If any invalid snowflake IDs are passed; a snowflake may be invalid - due to it being outside of the range of a 64 bit integer. - """ - payload = await self._session.create_guild( - name=name, - region=getattr(region, "id", region), - icon=conversions.get_bytes_from_resource(icon_data), - verification_level=verification_level, - default_message_notifications=default_message_notifications, - explicit_content_filter=explicit_content_filter, - roles=[role.serialize() for role in roles] if roles is not ... else ..., - channels=[channel.serialize() for channel in channels] if channels is not ... else ..., - ) - return guilds.Guild.deserialize(payload) - - async def fetch_guild(self, guild: snowflakes.HashableT[guilds.Guild]) -> guilds.Guild: - """Get a given guild's object. - - Parameters - ---------- - guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] - The object or ID of the guild to get. - - Returns - ------- - :obj:`~hikari.guilds.Guild` - The requested guild object. - - Raises - ------ - :obj:`~hikari.errors.BadRequestHTTPError` - If any invalid snowflake IDs are passed; a snowflake may be invalid - due to it being outside of the range of a 64 bit integer. - :obj:`~hikari.errors.NotFoundHTTPError` - If the guild is not found. - :obj:`~hikari.errors.ForbiddenHTTPError` - If you don't have access to the guild. - """ - payload = await self._session.get_guild( - guild_id=str(guild.id if isinstance(guild, snowflakes.UniqueEntity) else int(guild)) - ) - return guilds.Guild.deserialize(payload) - - async def fetch_guild_preview(self, guild: snowflakes.HashableT[guilds.Guild]) -> guilds.GuildPreview: - """Get a given guild's object. - - Parameters - ---------- - guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] - The object or ID of the guild to get the preview object for. - - Returns - ------- - :obj:`~hikari.guilds.GuildPreview` - The requested guild preview object. - - Note - ---- - Unlike other guild endpoints, the bot doesn't have to be in the target - guild to get it's preview. - - Raises - ------ - :obj:`~hikari.errors.BadRequestHTTPError` - If any invalid snowflake IDs are passed; a snowflake may be invalid - due to it being outside of the range of UINT64. - :obj:`~hikari.errors.NotFoundHTTPError` - If the guild is not found or it isn't ``PUBLIC``. - """ - payload = await self._session.get_guild_preview( - guild_id=str(guild.id if isinstance(guild, snowflakes.UniqueEntity) else int(guild)) - ) - return guilds.GuildPreview.deserialize(payload) - - async def update_guild( - self, - guild: snowflakes.HashableT[guilds.Guild], - *, - name: str = ..., - region: typing.Union[voices.VoiceRegion, str] = ..., - verification_level: typing.Union[guilds.GuildVerificationLevel, int] = ..., - default_message_notifications: typing.Union[guilds.GuildMessageNotificationsLevel, int] = ..., - explicit_content_filter: typing.Union[guilds.GuildExplicitContentFilterLevel, int] = ..., - afk_channel: snowflakes.HashableT[_channels.GuildVoiceChannel] = ..., - afk_timeout: typing.Union[datetime.timedelta, int] = ..., - icon_data: conversions.FileLikeT = ..., - owner: snowflakes.HashableT[users.User] = ..., - splash_data: conversions.FileLikeT = ..., - system_channel: snowflakes.HashableT[_channels.Channel] = ..., - reason: str = ..., - ) -> guilds.Guild: - """Edit a given guild. - - Parameters - ---------- - guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] - The object or ID of the guild to be edited. - name : :obj:`~str` - If specified, the new name string for the guild (``2-100`` characters). - region : :obj:`~str` - If specified, the new voice region ID for guild. You can use - :meth:`fetch_guild_voice_regions` to see which region IDs are - available. - verification_level : :obj:`~typing.Union` [ :obj:`~hikari.guilds.GuildVerificationLevel`, :obj:`~int` ] - If specified, the new verification level. Passing a raw int for this - may lead to unexpected behaviour. - default_message_notifications : :obj:`~typing.Union` [ :obj:`~hikari.guilds.GuildMessageNotificationsLevel`, :obj:`~int` ] - If specified, the new default notification level. Passing a raw int - for this may lead to unexpected behaviour. - explicit_content_filter : :obj:`~typing.Union` [ :obj:`~hikari.guilds.GuildExplicitContentFilterLevel`, :obj:`~int` ] - If specified, the new explicit content filter. Passing a raw int for - this may lead to unexpected behaviour. - afk_channel : :obj:`~typing.Union` [ :obj:`~hikari.channels.GuildVoiceChannel`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] - If specified, the object or ID for the new AFK voice channel. - afk_timeout : :obj:`~typing.Union` [ :obj:`~datetime.timedelta`, :obj:`~int` ] - If specified, the new AFK timeout seconds timedelta. - icon_data : ``hikari.internal.conversions.FileLikeT`` - If specified, the new guild icon image file data. - owner : :obj:`~typing.Union` [ :obj:`~hikari.users.User`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] - If specified, the object or ID of the new guild owner. - splash_data : ``hikari.internal.conversions.FileLikeT`` - If specified, the new new splash image file data. - system_channel : :obj:`~typing.Union` [ :obj:`~hikari.channels.GuildVoiceChannel`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] - If specified, the object or ID of the new system channel. - reason : :obj:`~str` - If specified, the audit log reason explaining why the operation - was performed. - - Returns - ------- - :obj:`~hikari.guilds.Guild` - The edited guild object. - - Raises - ------ - :obj:`~hikari.errors.BadRequestHTTPError` - If any invalid snowflake IDs are passed; a snowflake may be invalid - due to it being outside of the range of a 64 bit integer. - :obj:`~hikari.errors.NotFoundHTTPError` - If the guild is not found. - :obj:`~hikari.errors.ForbiddenHTTPError` - If you lack the ``MANAGE_GUILD`` permission or are not in the guild. - """ - payload = await self._session.modify_guild( - guild_id=str(guild.id if isinstance(guild, snowflakes.UniqueEntity) else int(guild)), - name=name, - region=getattr(region, "id", region) if region is not ... else ..., - verification_level=verification_level, - default_message_notifications=default_message_notifications, - explicit_content_filter=explicit_content_filter, - afk_timeout=afk_timeout.total_seconds() if isinstance(afk_timeout, datetime.timedelta) else afk_timeout, - afk_channel_id=( - str(afk_channel.id if isinstance(afk_channel, snowflakes.UniqueEntity) else int(afk_channel)) - if afk_channel is not ... - else ... - ), - icon=conversions.get_bytes_from_resource(icon_data) if icon_data is not ... else ..., - owner_id=( - str(owner.id if isinstance(owner, snowflakes.UniqueEntity) else int(owner)) if owner is not ... else ... - ), - splash=conversions.get_bytes_from_resource(splash_data) if splash_data is not ... else ..., - system_channel_id=( - str(system_channel.id if isinstance(system_channel, snowflakes.UniqueEntity) else int(system_channel)) - if system_channel is not ... - else ... - ), - reason=reason, - ) - return guilds.Guild.deserialize(payload) - - async def delete_guild(self, guild: snowflakes.HashableT[guilds.Guild]) -> None: - """Permanently deletes the given guild. - - You must be owner of the guild to perform this action. - - Parameters - ---------- - guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] - The object or ID of the guild to be deleted. - - Raises - ------ - :obj:`~hikari.errors.BadRequestHTTPError` - If any invalid snowflake IDs are passed; a snowflake may be invalid - due to it being outside of the range of a 64 bit integer. - :obj:`~hikari.errors.NotFoundHTTPError` - If the guild is not found. - :obj:`~hikari.errors.ForbiddenHTTPError` - If you are not the guild owner. - """ - await self._session.delete_guild( - guild_id=str(guild.id if isinstance(guild, snowflakes.UniqueEntity) else int(guild)) - ) - - async def fetch_guild_channels( - self, guild: snowflakes.HashableT[guilds.Guild] - ) -> typing.Sequence[_channels.GuildChannel]: - """Get all the channels for a given guild. - - Parameters - ---------- - guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] - The object or ID of the guild to get the channels from. - - Returns - ------- - :obj:`~typing.Sequence` [ :obj:`~hikari.channels.GuildChannel` ] - A list of guild channel objects. - - Raises - ------ - :obj:`~hikari.errors.BadRequestHTTPError` - If any invalid snowflake IDs are passed; a snowflake may be invalid - due to it being outside of the range of a 64 bit integer. - :obj:`~hikari.errors.NotFoundHTTPError` - If the guild is not found. - :obj:`~hikari.errors.ForbiddenHTTPError` - If you are not in the guild. - """ - payload = await self._session.list_guild_channels( - guild_id=str(guild.id if isinstance(guild, snowflakes.UniqueEntity) else int(guild)) - ) - return [_channels.deserialize_channel(channel) for channel in payload] - - async def create_guild_channel( - self, - guild: snowflakes.HashableT[guilds.Guild], - name: str, - channel_type: typing.Union[_channels.ChannelType, int] = ..., - position: int = ..., - topic: str = ..., - nsfw: bool = ..., - rate_limit_per_user: typing.Union[datetime.timedelta, int] = ..., - bitrate: int = ..., - user_limit: int = ..., - permission_overwrites: typing.Sequence[_channels.PermissionOverwrite] = ..., - parent_category: snowflakes.HashableT[_channels.GuildCategory] = ..., - reason: str = ..., - ) -> _channels.GuildChannel: - """Create a channel in a given guild. - - Parameters - ---------- - guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] - The object or ID of the guild to create the channel in. - name : :obj:`~str` - If specified, the name for the channel. This must be - inclusively between ``1` and ``100`` characters in length. - channel_type: :obj:`~typing.Union` [ :obj:`~hikari.channels.ChannelType`, :obj:`~int` ] - If specified, the channel type, passing through a raw integer here - may lead to unexpected behaviour. - position : :obj:`~int` - If specified, the position to change the channel to. - topic : :obj:`~str` - If specified, the topic to set. This is only applicable to - text channels. This must be inclusively between ``0`` and ``1024`` - characters in length. - nsfw : :obj:`~bool` - If specified, whether the channel will be marked as NSFW. - Only applicable for text channels. - rate_limit_per_user : :obj:`~typing.Union` [ :obj:`~datetime.timedelta`, :obj:`~int` ] - If specified, the second time delta the user has to wait before - sending another message. This will not apply to bots, or to - members with ``MANAGE_MESSAGES`` or ``MANAGE_CHANNEL`` permissions. - This must be inclusively between ``0`` and ``21600`` seconds. - bitrate : :obj:`~int` - If specified, the bitrate in bits per second allowable for the - channel. This only applies to voice channels and must be inclusively - between ``8000`` and ``96000`` for normal servers or ``8000`` and - ``128000`` for VIP servers. - user_limit : :obj:`~int` - If specified, the max number of users to allow in a voice channel. - This must be between ``0`` and ``99`` inclusive, where - ``0`` implies no limit. - permission_overwrites : :obj:`~typing.Sequence` [ :obj:`~hikari.channels.PermissionOverwrite` ] - If specified, the list of permission overwrite objects that are - category specific to replace the existing overwrites with. - parent_category : :obj:`~typing.Union` [ :obj:`~hikari.channels.GuildCategory`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] - If specified, the object or ID of the parent category to set for - the channel. - reason : :obj:`~str` - If specified, the audit log reason explaining why the operation - was performed. - - Returns - ------- - :obj:`~hikari.channels.GuildChannel` - The newly created channel object. - - Raises - ------ - :obj:`~hikari.errors.NotFoundHTTPError` - If the guild is not found. - :obj:`~hikari.errors.ForbiddenHTTPError` - If you lack the ``MANAGE_CHANNEL`` permission or are not in the - guild. - :obj:`~hikari.errors.BadRequestHTTPError` - If you provide incorrect options for the corresponding channel type - (e.g. a ``bitrate`` for a text channel). - If any invalid snowflake IDs are passed; a snowflake may be invalid - due to it being outside of the range of a 64 bit integer. - """ - payload = await self._session.create_guild_channel( - guild_id=str(guild.id if isinstance(guild, snowflakes.UniqueEntity) else int(guild)), - name=name, - type_=channel_type, - position=position, - topic=topic, - nsfw=nsfw, - rate_limit_per_user=( - int(rate_limit_per_user.total_seconds()) - if isinstance(rate_limit_per_user, datetime.timedelta) - else rate_limit_per_user - ), - bitrate=bitrate, - user_limit=user_limit, - permission_overwrites=( - [po.serialize() for po in permission_overwrites] if permission_overwrites is not ... else ... - ), - parent_id=( - str( - parent_category.id if isinstance(parent_category, snowflakes.UniqueEntity) else int(parent_category) - ) - if parent_category is not ... - else ... - ), - reason=reason, - ) - return _channels.deserialize_channel(payload) - - async def reposition_guild_channels( - self, - guild: snowflakes.HashableT[guilds.Guild], - channel: typing.Tuple[int, snowflakes.HashableT[_channels.GuildChannel]], - *additional_channels: typing.Tuple[int, snowflakes.HashableT[_channels.GuildChannel]], - ) -> None: - """Edits the position of one or more given channels. - - Parameters - ---------- - guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] - The object or ID of the guild in which to edit the channels. - channel : :obj:`~typing.Tuple` [ :obj:`~int` , :obj:`~typing.Union` [ :obj:`~hikari.channels.GuildChannel`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] ] - The first channel to change the position of. This is a tuple of the - integer position the channel object or ID. - *additional_channels : :obj:`~typing.Tuple` [ :obj:`~int`, :obj:`~typing.Union` [ :obj:`~hikari.channels.GuildChannel`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] ] - Optional additional channels to change the position of. These must - be tuples of integer positions to change to and the channel object - or ID and the. - - Raises - ------ - :obj:`~hikari.errors.NotFoundHTTPError` - If either the guild or any of the channels aren't found. - :obj:`~hikari.errors.ForbiddenHTTPError` - If you either lack the ``MANAGE_CHANNELS`` permission or are not a - member of said guild or are not in the guild. - :obj:`~hikari.errors.BadRequestHTTPError` - If you provide anything other than the ``id`` and ``position`` - fields for the channels. - If any invalid snowflake IDs are passed; a snowflake may be invalid - due to it being outside of the range of a 64 bit integer. - """ - await self._session.modify_guild_channel_positions( - str(guild.id if isinstance(guild, snowflakes.UniqueEntity) else int(guild)), - *[ - (str(channel.id if isinstance(channel, snowflakes.UniqueEntity) else int(channel)), position) - for position, channel in [channel, *additional_channels] - ], - ) - - async def fetch_member( - self, guild: snowflakes.HashableT[guilds.Guild], user: snowflakes.HashableT[users.User], - ) -> guilds.GuildMember: - """Get a given guild member. - - Parameters - ---------- - guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] - The object or ID of the guild to get the member from. - user : :obj:`~typing.Union` [ :obj:`~hikari.users.User`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] - The object or ID of the member to get. - - Returns - ------- - :obj:`~hikari.guilds.GuildMember` - The requested member object. - - Raises - ------ - :obj:`~hikari.errors.BadRequestHTTPError` - If any invalid snowflake IDs are passed; a snowflake may be invalid - due to it being outside of the range of a 64 bit integer. - :obj:`~hikari.errors.NotFoundHTTPError` - If either the guild or the member aren't found. - :obj:`~hikari.errors.ForbiddenHTTPError` - If you don't have access to the target guild. - """ - payload = await self._session.get_guild_member( - guild_id=str(guild.id if isinstance(guild, snowflakes.UniqueEntity) else int(guild)), - user_id=str(user.id if isinstance(user, snowflakes.UniqueEntity) else int(user)), - ) - return guilds.GuildMember.deserialize(payload) - - def fetch_members_after( - self, - guild: snowflakes.HashableT[guilds.Guild], - *, - after: typing.Union[datetime.datetime, snowflakes.HashableT[users.User]] = 0, - limit: typing.Optional[int] = None, - ) -> typing.AsyncIterator[guilds.GuildMember]: - """Get an async iterator of all the members in a given guild. - - This returns the member objects with a user object/ID that was created - after the given user object/ID or from the member object or the oldest - user. - - Parameters - ---------- - guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] - The object or ID of the guild to get the members from. - limit : :obj:`~int` - If specified, the maximum number of members this iterator - should return. - after : :obj:`~typing.Union` [ :obj:`~datetime.datetime`, :obj:`~hikari.users.User`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] - The object or ID of the user this iterator should start - after if specified, else this will start at the oldest user. - - Example - ------- - .. code-block:: python - - async for user in client.fetch_members_after(guild, after=9876543, limit=1231): - if member.user.username[0] in HOIST_BLACKLIST: - await client.update_member(member, nickname="💩") - - Returns - ------- - :obj:`~typing.AsyncIterator` [ :obj:`~hikari.guilds.GuildMember` ] - An async iterator of member objects. - - Raises - ------ - :obj:`~hikari.errors.BadRequestHTTPError` - If any invalid snowflake IDs are passed; a snowflake may be invalid - due to it being outside of the range of a 64 bit integer. - :obj:`~hikari.errors.NotFoundHTTPError` - If the guild is not found. - :obj:`~hikari.errors.ForbiddenHTTPError` - If you are not in the guild. - """ - if isinstance(after, datetime.datetime): - after = str(snowflakes.Snowflake.from_datetime(after)) - else: - after = str(after.id if isinstance(after, snowflakes.UniqueEntity) else int(after)) - return self._pagination_handler( - guild_id=str(guild.id if isinstance(guild, snowflakes.UniqueEntity) else int(guild)), - deserializer=guilds.GuildMember.deserialize, - direction="after", - request=self._session.list_guild_members, - reversing=False, - start=after, - limit=limit, - id_getter=_get_member_id, - ) - - async def update_member( - self, - guild: snowflakes.HashableT[guilds.Guild], - user: snowflakes.HashableT[users.User], - nickname: typing.Optional[str] = ..., - roles: typing.Sequence[snowflakes.HashableT[guilds.GuildRole]] = ..., - mute: bool = ..., - deaf: bool = ..., - voice_channel: typing.Optional[snowflakes.HashableT[_channels.GuildVoiceChannel]] = ..., - reason: str = ..., - ) -> None: - """Edits a guild's member, any unspecified fields will not be changed. - - Parameters - ---------- - guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] - The object or ID of the guild to edit the member from. - user : :obj:`~typing.Union` [ :obj:`~hikari.guilds.GuildMember`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] - The object or ID of the member to edit. - nickname : :obj:`~str`, optional - If specified, the new nickname string. Setting it to :obj:`~None` - explicitly will clear the nickname. - roles : :obj:`~typing.Sequence` [ :obj:`~typing.Union` [ :obj:`~hikari.guilds.GuildRole`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] ] - If specified, a list of role IDs the member should have. - mute : :obj:`~bool` - If specified, whether the user should be muted in the voice channel - or not. - deaf : :obj:`~bool` - If specified, whether the user should be deafen in the voice - channel or not. - voice_channel : :obj:`~typing.Union` [ :obj:`~hikari.channels.GuildVoiceChannel`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ], optional - If specified, the ID of the channel to move the member to. Setting - it to :obj:`~None` explicitly will disconnect the user. - reason : :obj:`~str` - If specified, the audit log reason explaining why the operation - was performed. - - Raises - ------ - :obj:`~hikari.errors.NotFoundHTTPError` - If either the guild, user, channel or any of the roles aren't found. - :obj:`~hikari.errors.ForbiddenHTTPError` - If you lack any of the applicable permissions - (``MANAGE_NICKNAMES``, ``MANAGE_ROLES``, ``MUTE_MEMBERS``, ``DEAFEN_MEMBERS`` or ``MOVE_MEMBERS``). - Note that to move a member you must also have permission to connect - to the end channel. This will also be raised if you're not in the - guild. - :obj:`~hikari.errors.BadRequestHTTPError` - If you pass ``mute``, ``deaf`` or ``channel_id`` while the member - is not connected to a voice channel. - If any invalid snowflake IDs are passed; a snowflake may be invalid - due to it being outside of the range of a 64 bit integer. - """ - await self._session.modify_guild_member( - guild_id=str(guild.id if isinstance(guild, snowflakes.UniqueEntity) else int(guild)), - user_id=str(user.id if isinstance(user, snowflakes.UniqueEntity) else int(user)), - nick=nickname, - roles=( - [str(role.id if isinstance(role, snowflakes.UniqueEntity) else int(role)) for role in roles] - if roles is not ... - else ... - ), - mute=mute, - deaf=deaf, - channel_id=( - str(voice_channel.id if isinstance(voice_channel, snowflakes.UniqueEntity) else int(voice_channel)) - if voice_channel is not ... - else ... - ), - reason=reason, - ) - - async def update_my_member_nickname( - self, guild: snowflakes.HashableT[guilds.Guild], nickname: typing.Optional[str], *, reason: str = ..., - ) -> None: - """Edits the current user's nickname for a given guild. - - Parameters - ---------- - guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] - The object or ID of the guild you want to change the nick on. - nickname : :obj:`~str`, optional - The new nick string. Setting this to `None` clears the nickname. - reason : :obj:`~str` - If specified, the audit log reason explaining why the operation - was performed. - - Raises - ------ - :obj:`~hikari.errors.NotFoundHTTPError` - If the guild is not found. - :obj:`~hikari.errors.ForbiddenHTTPError` - If you lack the ``CHANGE_NICKNAME`` permission or are not in the - guild. - :obj:`~hikari.errors.BadRequestHTTPError` - If you provide a disallowed nickname, one that is too long, or one - that is empty. - If any invalid snowflake IDs are passed; a snowflake may be invalid - due to it being outside of the range of a 64 bit integer. - """ - await self._session.modify_current_user_nick( - guild_id=str(guild.id if isinstance(guild, snowflakes.UniqueEntity) else int(guild)), - nick=nickname, - reason=reason, - ) - - async def add_role_to_member( - self, - guild: snowflakes.HashableT[guilds.Guild], - user: snowflakes.HashableT[users.User], - role: snowflakes.HashableT[guilds.GuildRole], - *, - reason: str = ..., - ) -> None: - """Add a role to a given member. - - Parameters - ---------- - guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] - The object or ID of the guild the member belongs to. - user : :obj:`~typing.Union` [ :obj:`~hikari.users.User`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] - The object or ID of the member you want to add the role to. - role : :obj:`~typing.Union` [ :obj:`~hikari.guilds.GuildRole`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] - The object or ID of the role you want to add. - reason : :obj:`~str` - If specified, the audit log reason explaining why the operation - was performed. - - Raises - ------ - :obj:`~hikari.errors.BadRequestHTTPError` - If any invalid snowflake IDs are passed; a snowflake may be invalid - due to it being outside of the range of a 64 bit integer. - :obj:`~hikari.errors.NotFoundHTTPError` - If either the guild, member or role aren't found. - :obj:`~hikari.errors.ForbiddenHTTPError` - If you lack the ``MANAGE_ROLES`` permission or are not in the guild. - """ - await self._session.add_guild_member_role( - guild_id=str(guild.id if isinstance(guild, snowflakes.UniqueEntity) else int(guild)), - user_id=str(user.id if isinstance(user, snowflakes.UniqueEntity) else int(user)), - role_id=str(role.id if isinstance(role, snowflakes.UniqueEntity) else int(role)), - reason=reason, - ) - - async def remove_role_from_member( - self, - guild: snowflakes.HashableT[guilds.Guild], - user: snowflakes.HashableT[users.User], - role: snowflakes.HashableT[guilds.GuildRole], - *, - reason: str = ..., - ) -> None: - """Remove a role from a given member. - - Parameters - ---------- - guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] - The object or ID of the guild the member belongs to. - user : :obj:`~typing.Union` [ :obj:`~hikari.users.User`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] - The object or ID of the member you want to remove the role from. - role : :obj:`~typing.Union` [ :obj:`~hikari.guilds.GuildRole`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] - The object or ID of the role you want to remove. - reason : :obj:`~str` - If specified, the audit log reason explaining why the operation - was performed. - - Raises - ------ - :obj:`~hikari.errors.BadRequestHTTPError` - If any invalid snowflake IDs are passed; a snowflake may be invalid - due to it being outside of the range of a 64 bit integer. - :obj:`~hikari.errors.NotFoundHTTPError` - If either the guild, member or role aren't found. - :obj:`~hikari.errors.ForbiddenHTTPError` - If you lack the ``MANAGE_ROLES`` permission or are not in the guild. - """ - await self._session.remove_guild_member_role( - guild_id=str(guild.id if isinstance(guild, snowflakes.UniqueEntity) else int(guild)), - user_id=str(user.id if isinstance(user, snowflakes.UniqueEntity) else int(user)), - role_id=str(role.id if isinstance(role, snowflakes.UniqueEntity) else int(role)), - reason=reason, - ) - - async def kick_member( - self, guild: snowflakes.HashableT[guilds.Guild], user: snowflakes.HashableT[users.User], *, reason: str = ..., - ) -> None: - """Kicks a user from a given guild. - - Parameters - ---------- - guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] - The object or ID of the guild the member belongs to. - user : :obj:`~typing.Union` [ :obj:`~hikari.users.User`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] - The object or ID of the member you want to kick. - reason : :obj:`~str` - If specified, the audit log reason explaining why the operation - was performed. - - Raises - ------ - :obj:`~hikari.errors.BadRequestHTTPError` - If any invalid snowflake IDs are passed; a snowflake may be invalid - due to it being outside of the range of a 64 bit integer. - :obj:`~hikari.errors.NotFoundHTTPError` - If either the guild or member aren't found. - :obj:`~hikari.errors.ForbiddenHTTPError` - If you lack the ``KICK_MEMBERS`` permission or are not in the guild. - """ - await self._session.remove_guild_member( - guild_id=str(guild.id if isinstance(guild, snowflakes.UniqueEntity) else int(guild)), - user_id=str(user.id if isinstance(user, snowflakes.UniqueEntity) else int(user)), - reason=reason, - ) - - async def fetch_ban( - self, guild: snowflakes.HashableT[guilds.Guild], user: snowflakes.HashableT[users.User], - ) -> guilds.GuildMemberBan: - """Get a ban from a given guild. - - Parameters - ---------- - guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] - The object or ID of the guild you want to get the ban from. - user : :obj:`~typing.Union` [ :obj:`~hikari.users.User`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] - The object or ID of the user to get the ban information for. - - Returns - ------- - :obj:`~hikari.guilds.GuildMemberBan` - A ban object for the requested user. - - Raises - ------ - :obj:`~hikari.errors.BadRequestHTTPError` - If any invalid snowflake IDs are passed; a snowflake may be invalid - due to it being outside of the range of a 64 bit integer. - :obj:`~hikari.errors.NotFoundHTTPError` - If either the guild or the user aren't found, or if the user is not - banned. - :obj:`~hikari.errors.ForbiddenHTTPError` - If you lack the ``BAN_MEMBERS`` permission or are not in the guild. - """ - payload = await self._session.get_guild_ban( - guild_id=str(guild.id if isinstance(guild, snowflakes.UniqueEntity) else int(guild)), - user_id=str(user.id if isinstance(user, snowflakes.UniqueEntity) else int(user)), - ) - return guilds.GuildMemberBan.deserialize(payload) - - async def fetch_bans(self, guild: snowflakes.HashableT[guilds.Guild],) -> typing.Sequence[guilds.GuildMemberBan]: - """Get the bans for a given guild. - - Parameters - ---------- - guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] - The object or ID of the guild you want to get the bans from. - - Returns - ------- - :obj:`~typing.Sequence` [ :obj:`~hikari.guilds.GuildMemberBan` ] - A list of ban objects. - - Raises - ------ - :obj:`~hikari.errors.BadRequestHTTPError` - If any invalid snowflake IDs are passed; a snowflake may be invalid - due to it being outside of the range of a 64 bit integer. - :obj:`~hikari.errors.NotFoundHTTPError` - If the guild is not found. - :obj:`~hikari.errors.ForbiddenHTTPError` - If you lack the ``BAN_MEMBERS`` permission or are not in the guild. - """ - payload = await self._session.get_guild_bans( - guild_id=str(guild.id if isinstance(guild, snowflakes.UniqueEntity) else int(guild)) - ) - return [guilds.GuildMemberBan.deserialize(ban) for ban in payload] - - async def ban_member( - self, - guild: snowflakes.HashableT[guilds.Guild], - user: snowflakes.HashableT[users.User], - *, - delete_message_days: typing.Union[datetime.timedelta, int] = ..., - reason: str = ..., - ) -> None: - """Bans a user from a given guild. - - Parameters - ---------- - guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] - The object or ID of the guild the member belongs to. - user : :obj:`~typing.Union` [ :obj:`~hikari.users.User`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] - The object or ID of the member you want to ban. - delete_message_days : :obj:`~typing.Union` [ :obj:`~datetime.timedelta`, :obj:`~int` ] - If specified, the tim delta of how many days of messages from the - user should be removed. - reason : :obj:`~str` - If specified, the audit log reason explaining why the operation - was performed. - - Raises - ------ - :obj:`~hikari.errors.BadRequestHTTPError` - If any invalid snowflake IDs are passed; a snowflake may be invalid - due to it being outside of the range of a 64 bit integer. - :obj:`~hikari.errors.NotFoundHTTPError` - If either the guild or member aren't found. - :obj:`~hikari.errors.ForbiddenHTTPError` - If you lack the ``BAN_MEMBERS`` permission or are not in the guild. - """ - await self._session.create_guild_ban( - guild_id=str(guild.id if isinstance(guild, snowflakes.UniqueEntity) else int(guild)), - user_id=str(user.id if isinstance(user, snowflakes.UniqueEntity) else int(user)), - delete_message_days=getattr(delete_message_days, "days", delete_message_days), - reason=reason, - ) - - async def unban_member( - self, guild: snowflakes.HashableT[guilds.Guild], user: snowflakes.HashableT[users.User], *, reason: str = ..., - ) -> None: - """Un-bans a user from a given guild. - - Parameters - ---------- - guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] - The object or ID of the guild to un-ban the user from. - user : :obj:`~typing.Union` [ :obj:`~hikari.users.User`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] - The ID of the user you want to un-ban. - reason : :obj:`~str` - If specified, the audit log reason explaining why the operation - was performed. - - Raises - ------ - :obj:`~hikari.errors.BadRequestHTTPError` - If any invalid snowflake IDs are passed; a snowflake may be invalid - due to it being outside of the range of a 64 bit integer. - :obj:`~hikari.errors.NotFoundHTTPError` - If either the guild or member aren't found, or the member is not - banned. - :obj:`~hikari.errors.ForbiddenHTTPError` - If you lack the ``BAN_MEMBERS`` permission or are not a in the - guild. - """ - await self._session.remove_guild_ban( - guild_id=str(guild.id if isinstance(guild, snowflakes.UniqueEntity) else int(guild)), - user_id=str(user.id if isinstance(user, snowflakes.UniqueEntity) else int(user)), - reason=reason, - ) - - async def fetch_roles( - self, guild: snowflakes.HashableT[guilds.Guild], - ) -> typing.Mapping[snowflakes.Snowflake, guilds.GuildRole]: - """Get the roles for a given guild. - - Parameters - ---------- - guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] - The object or ID of the guild you want to get the roles from. - - Returns - ------- - :obj:`~typing.Mapping` [ :obj:`~hikari.snowflakes.Snowflake`, :obj:`~hikari.guilds.GuildRole` ] - A list of role objects. - - Raises - ------ - :obj:`~hikari.errors.BadRequestHTTPError` - If any invalid snowflake IDs are passed; a snowflake may be invalid - due to it being outside of the range of a 64 bit integer. - :obj:`~hikari.errors.NotFoundHTTPError` - If the guild is not found. - :obj:`~hikari.errors.ForbiddenHTTPError` - If you're not in the guild. - """ - payload = await self._session.get_guild_roles( - guild_id=str(guild.id if isinstance(guild, snowflakes.UniqueEntity) else int(guild)) - ) - return {role.id: role for role in map(guilds.GuildRole.deserialize, payload)} - - async def create_role( - self, - guild: snowflakes.HashableT[guilds.Guild], - *, - name: str = ..., - permissions: typing.Union[_permissions.Permission, int] = ..., - color: typing.Union[colors.Color, int] = ..., - hoist: bool = ..., - mentionable: bool = ..., - reason: str = ..., - ) -> guilds.GuildRole: - """Create a new role for a given guild. - - Parameters - ---------- - guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] - The object or ID of the guild you want to create the role on. - name : :obj:`~str` - If specified, the new role name string. - permissions : :obj:`~typing.Union` [ :obj:`~hikari.permissions.Permission`, :obj:`~int` ] - If specified, the permissions integer for the role, passing a raw - integer rather than the int flag may lead to unexpected results. - color : :obj:`~typing.Union` [ :obj:`~hikari.colors.Color`, :obj:`~int` ] - If specified, the color for the role. - hoist : :obj:`~bool` - If specified, whether the role will be hoisted. - mentionable : :obj:`~bool` - If specified, whether the role will be able to be mentioned by any - user. - reason : :obj:`~str` - If specified, the audit log reason explaining why the operation - was performed. - - Returns - ------- - :obj:`~hikari.guilds.GuildRole` - The newly created role object. - - Raises - ------ - :obj:`~hikari.errors.NotFoundHTTPError` - If the guild is not found. - :obj:`~hikari.errors.ForbiddenHTTPError` - If you lack the ``MANAGE_ROLES`` permission or you're not in the - guild. - :obj:`~hikari.errors.BadRequestHTTPError` - If you provide invalid values for the role attributes. - If any invalid snowflake IDs are passed; a snowflake may be invalid - due to it being outside of the range of a 64 bit integer. - """ - payload = await self._session.create_guild_role( - guild_id=str(guild.id if isinstance(guild, snowflakes.UniqueEntity) else int(guild)), - name=name, - permissions=permissions, - color=color, - hoist=hoist, - mentionable=mentionable, - reason=reason, - ) - return guilds.GuildRole.deserialize(payload) - - async def reposition_roles( - self, - guild: snowflakes.HashableT[guilds.Guild], - role: typing.Tuple[int, snowflakes.HashableT[guilds.GuildRole]], - *additional_roles: typing.Tuple[int, snowflakes.HashableT[guilds.GuildRole]], - ) -> typing.Sequence[guilds.GuildRole]: - """Edits the position of two or more roles in a given guild. - - Parameters - ---------- - guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] - The ID of the guild the roles belong to. - role : :obj:`~typing.Tuple` [ :obj:`~int`, :obj:`~typing.Union` [ :obj:`~hikari.guilds.GuildRole`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] ] - The first role to move. This is a tuple of the integer position and - the role object or ID. - *additional_roles : :obj:`~typing.Tuple` [ :obj:`~int`, :obj:`~typing.Union` [ :obj:`~hikari.guilds.GuildRole`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] ] - Optional extra roles to move. These must be tuples of the integer - position and the role object or ID. - - Returns - ------- - :obj:`~typing.Sequence` [ :obj:`~hikari.guilds.GuildRole` ] - A list of all the guild roles. - - Raises - ------ - :obj:`~hikari.errors.NotFoundHTTPError` - If either the guild or any of the roles aren't found. - :obj:`~hikari.errors.ForbiddenHTTPError` - If you lack the ``MANAGE_ROLES`` permission or you're not in the - guild. - :obj:`~hikari.errors.BadRequestHTTPError` - If you provide invalid values for the `position` fields. - If any invalid snowflake IDs are passed; a snowflake may be invalid - due to it being outside of the range of a 64 bit integer. - """ - payload = await self._session.modify_guild_role_positions( - str(guild.id if isinstance(guild, snowflakes.UniqueEntity) else int(guild)), - *[ - (str(channel.id if isinstance(channel, snowflakes.UniqueEntity) else int(channel)), position) - for position, channel in [role, *additional_roles] - ], - ) - return [guilds.GuildRole.deserialize(role) for role in payload] - - async def update_role( - self, - guild: snowflakes.HashableT[guilds.Guild], - role: snowflakes.HashableT[guilds.GuildRole], - *, - name: str = ..., - permissions: typing.Union[_permissions.Permission, int] = ..., - color: typing.Union[colors.Color, int] = ..., - hoist: bool = ..., - mentionable: bool = ..., - reason: str = ..., - ) -> guilds.GuildRole: - """Edits a role in a given guild. - - Parameters - ---------- - guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] - The object or ID of the guild the role belong to. - role : :obj:`~typing.Union` [ :obj:`~hikari.guilds.GuildRole`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] - The object or ID of the role you want to edit. - name : :obj:`~str` - If specified, the new role's name string. - permissions : :obj:`~typing.Union` [ :obj:`~hikari.permissions.Permission`, :obj:`~int` ] - If specified, the new permissions integer for the role, passing a - raw integer for this may lead to unexpected behaviour. - color : :obj:`~typing.Union` [ :obj:`~hikari.colors.Color`, :obj:`~int` ] - If specified, the new color for the new role passing a raw integer - for this may lead to unexpected behaviour. - hoist : :obj:`~bool` - If specified, whether the role should hoist or not. - mentionable : :obj:`~bool` - If specified, whether the role should be mentionable or not. - reason : :obj:`~str` - If specified, the audit log reason explaining why the operation - was performed. - - Returns - ------- - :obj:`~hikari.guilds.GuildRole` - The edited role object. - - Raises - ------ - :obj:`~hikari.errors.NotFoundHTTPError` - If either the guild or role aren't found. - :obj:`~hikari.errors.ForbiddenHTTPError` - If you lack the ``MANAGE_ROLES`` permission or you're not in the - guild. - :obj:`~hikari.errors.BadRequestHTTPError` - If you provide invalid values for the role attributes. - If any invalid snowflake IDs are passed; a snowflake may be invalid - due to it being outside of the range of a 64 bit integer. - """ - payload = await self._session.modify_guild_role( - guild_id=str(guild.id if isinstance(guild, snowflakes.UniqueEntity) else int(guild)), - role_id=str(role.id if isinstance(role, snowflakes.UniqueEntity) else int(role)), - name=name, - permissions=permissions, - color=color, - hoist=hoist, - mentionable=mentionable, - reason=reason, - ) - return guilds.GuildRole.deserialize(payload) - - async def delete_role( - self, guild: snowflakes.HashableT[guilds.Guild], role: snowflakes.HashableT[guilds.GuildRole], - ) -> None: - """Delete a role from a given guild. - - Parameters - ---------- - guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] - The object or ID of the guild you want to remove the role from. - role : :obj:`~typing.Union` [ :obj:`~hikari.guilds.GuildRole`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] - The object or ID of the role you want to delete. - - Raises - ------ - :obj:`~hikari.errors.BadRequestHTTPError` - If any invalid snowflake IDs are passed; a snowflake may be invalid - due to it being outside of the range of a 64 bit integer. - :obj:`~hikari.errors.NotFoundHTTPError` - If either the guild or the role aren't found. - :obj:`~hikari.errors.ForbiddenHTTPError` - If you lack the ``MANAGE_ROLES`` permission or are not in the guild. - """ - await self._session.delete_guild_role( - guild_id=str(guild.id if isinstance(guild, snowflakes.UniqueEntity) else int(guild)), - role_id=str(role.id if isinstance(role, snowflakes.UniqueEntity) else int(role)), - ) - - async def estimate_guild_prune_count( - self, guild: snowflakes.HashableT[guilds.Guild], days: typing.Union[datetime.timedelta, int], - ) -> int: - """Get the estimated prune count for a given guild. - - Parameters - ---------- - guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] - The object or ID of the guild you want to get the count for. - days : :obj:`~typing.Union` [ :obj:`~datetime.timedelta`, :obj:`~int` ] - The time delta of days to count prune for (at least ``1``). - - Returns - ------- - :obj:`~int` - The number of members estimated to be pruned. - - Raises - ------ - :obj:`~hikari.errors.NotFoundHTTPError` - If the guild is not found. - :obj:`~hikari.errors.ForbiddenHTTPError` - If you lack the ``KICK_MEMBERS`` or you are not in the guild. - :obj:`~hikari.errors.BadRequestHTTPError` - If you pass an invalid amount of days. - If any invalid snowflake IDs are passed; a snowflake may be invalid - due to it being outside of the range of a 64 bit integer. - """ - return await self._session.get_guild_prune_count( - guild_id=str(guild.id if isinstance(guild, snowflakes.UniqueEntity) else int(guild)), - days=getattr(days, "days", days), - ) - - async def begin_guild_prune( - self, - guild: snowflakes.HashableT[guilds.Guild], - days: typing.Union[datetime.timedelta, int], - *, - compute_prune_count: bool = ..., - reason: str = ..., - ) -> int: - """Prunes members of a given guild based on the number of inactive days. - - Parameters - ---------- - guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] - The object or ID of the guild you want to prune member of. - days : :obj:`~typing.Union` [ :obj:`~datetime.timedelta`, :obj:`~int` ] - The time delta of inactivity days you want to use as filter. - compute_prune_count : :obj:`~bool` - Whether a count of pruned members is returned or not. - Discouraged for large guilds out of politeness. - reason : :obj:`~str` - If specified, the audit log reason explaining why the operation - was performed. - - Returns - ------- - :obj:`~int`, optional - The number of members who were kicked if ``compute_prune_count`` - is :obj:`~True`, else :obj:`~None`. - - Raises - ------ - :obj:`~hikari.errors.NotFoundHTTPError` - If the guild is not found: - :obj:`~hikari.errors.ForbiddenHTTPError` - If you lack the ``KICK_MEMBER`` permission or are not in the guild. - :obj:`~hikari.errors.BadRequestHTTPError` - If you provide invalid values for the ``days`` or - ``compute_prune_count`` fields. - If any invalid snowflake IDs are passed; a snowflake may be invalid - due to it being outside of the range of a 64 bit integer. - """ - return await self._session.begin_guild_prune( - guild_id=str(guild.id if isinstance(guild, snowflakes.UniqueEntity) else int(guild)), - days=getattr(days, "days", days), - compute_prune_count=compute_prune_count, - reason=reason, - ) - - async def fetch_guild_voice_regions( - self, guild: snowflakes.HashableT[guilds.Guild], - ) -> typing.Sequence[voices.VoiceRegion]: - """Get the voice regions for a given guild. - - Parameters - ---------- - guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] - The object or ID of the guild to get the voice regions for. - - Returns - ------- - :obj:`~typing.Sequence` [ :obj:`~hikari.voices.VoiceRegion` ] - A list of voice region objects. - - Raises - ------ - :obj:`~hikari.errors.BadRequestHTTPError` - If any invalid snowflake IDs are passed; a snowflake may be invalid - due to it being outside of the range of a 64 bit integer. - :obj:`~hikari.errors.NotFoundHTTPError` - If the guild is not found. - :obj:`~hikari.errors.ForbiddenHTTPError` - If you are not in the guild. - """ - payload = await self._session.get_guild_voice_regions( - guild_id=str(guild.id if isinstance(guild, snowflakes.UniqueEntity) else int(guild)) - ) - return [voices.VoiceRegion.deserialize(region) for region in payload] - - async def fetch_guild_invites( - self, guild: snowflakes.HashableT[guilds.Guild], - ) -> typing.Sequence[invites.InviteWithMetadata]: - """Get the invites for a given guild. - - Parameters - ---------- - guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] - The object or ID of the guild to get the invites for. - - Returns - ------- - :obj:`~typing.Sequence` [ :obj:`~hikari.invites.InviteWithMetadata` ] - A list of invite objects (with metadata). - - Raises - ------ - :obj:`~hikari.errors.BadRequestHTTPError` - If any invalid snowflake IDs are passed; a snowflake may be invalid - due to it being outside of the range of a 64 bit integer. - :obj:`~hikari.errors.NotFoundHTTPError` - If the guild is not found. - :obj:`~hikari.errors.ForbiddenHTTPError` - If you lack the ``MANAGE_GUILD`` permission or are not in the guild. - """ - payload = await self._session.get_guild_invites( - guild_id=str(guild.id if isinstance(guild, snowflakes.UniqueEntity) else int(guild)) - ) - return [invites.InviteWithMetadata.deserialize(invite) for invite in payload] - - async def fetch_integrations( - self, guild: snowflakes.HashableT[guilds.Guild] - ) -> typing.Sequence[guilds.GuildIntegration]: - """Get the integrations for a given guild. - - Parameters - ---------- - guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] - The object or ID of the guild to get the integrations for. - - Returns - ------- - :obj:`~typing.Sequence` [ :obj:`~hikari.guilds.GuildIntegration` ] - A list of integration objects. - - Raises - ------ - :obj:`~hikari.errors.BadRequestHTTPError` - If any invalid snowflake IDs are passed; a snowflake may be invalid - due to it being outside of the range of a 64 bit integer. - :obj:`~hikari.errors.NotFoundHTTPError` - If the guild is not found. - :obj:`~hikari.errors.ForbiddenHTTPError` - If you lack the ``MANAGE_GUILD`` permission or are not in the guild. - """ - payload = await self._session.get_guild_integrations( - guild_id=str(guild.id if isinstance(guild, snowflakes.UniqueEntity) else int(guild)) - ) - return [guilds.GuildIntegration.deserialize(integration) for integration in payload] - - async def update_integration( - self, - guild: snowflakes.HashableT[guilds.Guild], - integration: snowflakes.HashableT[guilds.GuildIntegration], - *, - expire_behaviour: typing.Union[guilds.IntegrationExpireBehaviour, int] = ..., - expire_grace_period: typing.Union[datetime.timedelta, int] = ..., - enable_emojis: bool = ..., - reason: str = ..., - ) -> None: - """Edits an integrations for a given guild. - - Parameters - ---------- - guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] - The object or ID of the guild to which the integration belongs to. - integration : :obj:`~typing.Union` [ :obj:`~hikari.guilds.GuildIntegration`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] - The object or ID of the integration to update. - expire_behaviour : :obj:`~typing.Union` [ :obj:`~hikari.guilds.IntegrationExpireBehaviour`, :obj:`~int` ] - If specified, the behaviour for when an integration subscription - expires (passing a raw integer for this may lead to unexpected - behaviour). - expire_grace_period : :obj:`~typing.Union` [ :obj:`~datetime.timedelta`, :obj:`~int` ] - If specified, time time delta of how many days the integration will - ignore lapsed subscriptions for. - enable_emojis : :obj:`~bool` - If specified, whether emojis should be synced for this integration. - reason : :obj:`~str` - If specified, the audit log reason explaining why the operation - was performed. - - Raises - ------ - :obj:`~hikari.errors.BadRequestHTTPError` - If any invalid snowflake IDs are passed; a snowflake may be invalid - due to it being outside of the range of a 64 bit integer. - :obj:`~hikari.errors.NotFoundHTTPError` - If either the guild or the integration aren't found. - :obj:`~hikari.errors.ForbiddenHTTPError` - If you lack the ``MANAGE_GUILD`` permission or are not in the guild. - """ - await self._session.modify_guild_integration( - guild_id=str(guild.id if isinstance(guild, snowflakes.UniqueEntity) else int(guild)), - integration_id=str( - integration.id if isinstance(integration, snowflakes.UniqueEntity) else int(integration) - ), - expire_behaviour=expire_behaviour, - expire_grace_period=getattr(expire_grace_period, "days", expire_grace_period), - enable_emojis=enable_emojis, - reason=reason, - ) - - async def delete_integration( - self, - guild: snowflakes.HashableT[guilds.Guild], - integration: snowflakes.HashableT[guilds.GuildIntegration], - *, - reason: str = ..., - ) -> None: - """Delete an integration for the given guild. - - Parameters - ---------- - guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] - The object or ID of the guild to which the integration belongs to. - integration : :obj:`~typing.Union` [ :obj:`~hikari.guilds.GuildIntegration`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] - The object or ID of the integration to delete. - reason : :obj:`~str` - If specified, the audit log reason explaining why the operation - was performed. - - Raises - ------ - :obj:`~hikari.errors.BadRequestHTTPError` - If any invalid snowflake IDs are passed; a snowflake may be invalid - due to it being outside of the range of a 64 bit integer. - :obj:`~hikari.errors.NotFoundHTTPError` - If either the guild or the integration aren't found. - :obj:`~hikari.errors.ForbiddenHTTPError` - If you lack the `MANAGE_GUILD` permission or are not in the guild. - """ - await self._session.delete_guild_integration( - guild_id=str(guild.id if isinstance(guild, snowflakes.UniqueEntity) else int(guild)), - integration_id=str( - integration.id if isinstance(integration, snowflakes.UniqueEntity) else int(integration) - ), - reason=reason, - ) - - async def sync_guild_integration( - self, guild: snowflakes.HashableT[guilds.Guild], integration: snowflakes.HashableT[guilds.GuildIntegration], - ) -> None: - """Sync the given integration's subscribers/emojis. - - Parameters - ---------- - guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] - The object or ID of the guild to which the integration belongs to. - integration : :obj:`~typing.Union` [ :obj:`~hikari.guilds.GuildIntegration`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] - The ID of the integration to sync. - - Raises - ------ - :obj:`~hikari.errors.BadRequestHTTPError` - If any invalid snowflake IDs are passed; a snowflake may be invalid - due to it being outside of the range of a 64 bit integer. - :obj:`~hikari.errors.NotFoundHTTPError` - If either the guild or the integration aren't found. - :obj:`~hikari.errors.ForbiddenHTTPError` - If you lack the ``MANAGE_GUILD`` permission or are not in the guild. - """ - await self._session.sync_guild_integration( - guild_id=str(guild.id if isinstance(guild, snowflakes.UniqueEntity) else int(guild)), - integration_id=str( - integration.id if isinstance(integration, snowflakes.UniqueEntity) else int(integration) - ), - ) - - async def fetch_guild_embed(self, guild: snowflakes.HashableT[guilds.Guild],) -> guilds.GuildEmbed: - """Get the embed for a given guild. - - Parameters - ---------- - guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] - The object or ID of the guild to get the embed for. - - Returns - ------- - :obj:`~hikari.guilds.GuildEmbed` - A guild embed object. - - Raises - ------ - :obj:`~hikari.errors.BadRequestHTTPError` - If any invalid snowflake IDs are passed; a snowflake may be invalid - due to it being outside of the range of a 64 bit integer. - :obj:`~hikari.errors.NotFoundHTTPError` - If the guild is not found. - :obj:`~hikari.errors.ForbiddenHTTPError` - If you either lack the ``MANAGE_GUILD`` permission or are not in - the guild. - """ - payload = await self._session.get_guild_embed( - guild_id=str(guild.id if isinstance(guild, snowflakes.UniqueEntity) else int(guild)) - ) - return guilds.GuildEmbed.deserialize(payload) - - async def update_guild_embed( - self, - guild: snowflakes.HashableT[guilds.Guild], - *, - channel: snowflakes.HashableT[_channels.GuildChannel] = ..., - enabled: bool = ..., - reason: str = ..., - ) -> guilds.GuildEmbed: - """Edits the embed for a given guild. - - Parameters - ---------- - guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] - The object or ID of the guild to edit the embed for. - channel : :obj:`~typing.Union` [ :obj:`~hikari.channels.GuildChannel`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ], optional - If specified, the object or ID of the channel that this embed's - invite should target. Set to :obj:`~None` to disable invites for this - embed. - enabled : :obj:`~bool` - If specified, whether this embed should be enabled. - reason : :obj:`~str` - If specified, the audit log reason explaining why the operation - was performed. - - Returns - ------- - :obj:`~hikari.guilds.GuildEmbed` - The updated embed object. - - Raises - ------ - :obj:`~hikari.errors.BadRequestHTTPError` - If any invalid snowflake IDs are passed; a snowflake may be invalid - due to it being outside of the range of a 64 bit integer. - :obj:`~hikari.errors.NotFoundHTTPError` - If the guild is not found. - :obj:`~hikari.errors.ForbiddenHTTPError` - If you either lack the ``MANAGE_GUILD`` permission or are not in - the guild. - """ - payload = await self._session.modify_guild_embed( - guild_id=str(guild.id if isinstance(guild, snowflakes.UniqueEntity) else int(guild)), - channel_id=( - str(channel.id if isinstance(channel, snowflakes.UniqueEntity) else int(channel)) - if channel is not ... - else ... - ), - enabled=enabled, - reason=reason, - ) - return guilds.GuildEmbed.deserialize(payload) - - async def fetch_guild_vanity_url(self, guild: snowflakes.HashableT[guilds.Guild],) -> invites.VanityUrl: - """ - Get the vanity URL for a given guild. - - Parameters - ---------- - guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] - The object or ID of the guild to get the vanity URL for. - - Returns - ------- - :obj:`~hikari.invites.VanityUrl` - A partial invite object containing the vanity URL in the ``code`` - field. - - Raises - ------ - :obj:`~hikari.errors.BadRequestHTTPError` - If any invalid snowflake IDs are passed; a snowflake may be invalid - due to it being outside of the range of a 64 bit integer. - :obj:`~hikari.errors.NotFoundHTTPError` - If the guild is not found. - :obj:`~hikari.errors.ForbiddenHTTPError` - If you either lack the ``MANAGE_GUILD`` permission or are not in - the guild. - """ - payload = await self._session.get_guild_vanity_url( - guild_id=str(guild.id if isinstance(guild, snowflakes.UniqueEntity) else int(guild)) - ) - return invites.VanityUrl.deserialize(payload) - - def format_guild_widget_image(self, guild: snowflakes.HashableT[guilds.Guild], *, style: str = ...) -> str: - """Get the URL for a guild widget. - - Parameters - ---------- - guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] - The object or ID of the guild to form the widget. - style : :obj:`~str` - If specified, the syle of the widget. - - Returns - ------- - :obj:`~str` - A URL to retrieve a PNG widget for your guild. - - Note - ---- - This does not actually make any form of request, and shouldn't be - awaited. Thus, it doesn't have rate limits either. - - Warning - ------- - The guild must have the widget enabled in the guild settings for this - to be valid. - """ - return self._session.get_guild_widget_image_url( - guild_id=str(guild.id if isinstance(guild, snowflakes.UniqueEntity) else int(guild)), style=style - ) - - async def fetch_invite( - self, invite: typing.Union[invites.Invite, str], *, with_counts: bool = ... - ) -> invites.Invite: - """Get the given invite. - - Parameters - ---------- - invite : :obj:`~typing.Union` [ :obj:`~hikari.invites.Invite`, :obj:`~str` ] - The object or code of the wanted invite. - with_counts : :bool: - If specified, whether to attempt to count the number of - times the invite has been used. - - Returns - ------- - :obj:`~hikari.invites.Invite` - The requested invite object. - - Raises - ------ - :obj:`~hikari.errors.BadRequestHTTPError` - If any invalid snowflake IDs are passed; a snowflake may be invalid - due to it being outside of the range of a 64 bit integer. - :obj:`~hikari.errors.NotFoundHTTPError` - If the invite is not found. - """ - payload = await self._session.get_invite(invite_code=getattr(invite, "code", invite), with_counts=with_counts) - return invites.Invite.deserialize(payload) - - async def delete_invite(self, invite: typing.Union[invites.Invite, str]) -> None: - """Delete a given invite. - - Parameters - ---------- - invite : :obj:`~typing.Union` [ :obj:`~hikari.invites.Invite`, :obj:`~str` ] - The object or ID for the invite to be deleted. - - Returns - ------- - :obj:`~None` - Nothing, unlike what the API specifies. This is done to maintain - consistency with other calls of a similar nature in this API wrapper. - - Raises - ------ - :obj:`~hikari.errors.NotFoundHTTPError` - If the invite is not found. - :obj:`~hikari.errors.ForbiddenHTTPError` - If you lack either ``MANAGE_CHANNELS`` on the channel the invite - belongs to or ``MANAGE_GUILD`` for guild-global delete. - """ - await self._session.delete_invite(invite_code=getattr(invite, "code", invite)) - - async def fetch_user(self, user: snowflakes.HashableT[users.User]) -> users.User: - """Get a given user. - - Parameters - ---------- - user : :obj:`~typing.Union` [ :obj:`~hikari.users.User`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] - The object or ID of the user to get. - - Returns - ------- - :obj:`~hikari.users.User` - The requested user object. - - Raises - ------ - :obj:`~hikari.errors.BadRequestHTTPError` - If any invalid snowflake IDs are passed; a snowflake may be invalid - due to it being outside of the range of a 64 bit integer. - :obj:`~hikari.errors.NotFoundHTTPError` - If the user is not found. - """ - payload = await self._session.get_user( - user_id=str(user.id if isinstance(user, snowflakes.UniqueEntity) else int(user)) - ) - return users.User.deserialize(payload) - - async def fetch_my_application_info(self) -> oauth2.Application: - """Get the current application information. - - Returns - ------- - :obj:`~hikari.oauth2.Application` - An application info object. - """ - payload = await self._session.get_current_application_info() - return oauth2.Application.deserialize(payload) - - async def fetch_me(self) -> users.MyUser: - """Get the current user that of the token given to the client. - - Returns - ------- - :obj:`~hikari.users.MyUser` - The current user object. - """ - payload = await self._session.get_current_user() - return users.MyUser.deserialize(payload) - - async def update_me( - self, *, username: str = ..., avatar_data: typing.Optional[conversions.FileLikeT] = ..., - ) -> users.MyUser: - """Edit the current user. - - Parameters - ---------- - username : :obj:`~str` - If specified, the new username string. - avatar_data : ``hikari.internal.conversions.FileLikeT``, optional - If specified, the new avatar image data. - If it is :obj:`~None`, the avatar is removed. - - Returns - ------- - :obj:`~hikari.users.MyUser` - The updated user object. - - Raises - ------ - :obj:`~hikari.errors.BadRequestHTTPError` - If you pass username longer than the limit (``2-32``) or an invalid image. - """ - payload = await self._session.modify_current_user( - username=username, - avatar=conversions.get_bytes_from_resource(avatar_data) if avatar_data is not ... else ..., - ) - return users.MyUser.deserialize(payload) - - async def fetch_my_connections(self) -> typing.Sequence[oauth2.OwnConnection]: - """ - Get the current user's connections. - - Note - ---- - This endpoint can be used with both ``Bearer`` and ``Bot`` tokens but - will usually return an empty list for bots (with there being some - exceptions to this, like user accounts that have been converted to bots). - - Returns - ------- - :obj:`~typing.Sequence` [ :obj:`~hikari.oauth2.OwnConnection` ] - A list of connection objects. - """ - payload = await self._session.get_current_user_connections() - return [oauth2.OwnConnection.deserialize(connection) for connection in payload] - - def fetch_my_guilds_after( - self, - *, - after: typing.Union[datetime.datetime, snowflakes.HashableT[guilds.Guild]] = 0, - limit: typing.Optional[int] = None, - ) -> typing.AsyncIterator[oauth2.OwnGuild]: - """Get an async iterator of the guilds the current user is in. - - This returns the guilds created after a given guild object/ID or from - the oldest guild. - - Parameters - ---------- - after : :obj:`~typing.Union` [ :obj:`~datetime.datetime`, :obj:`~hikari.guilds.Guild`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] - The object or ID of a guild to get guilds that were created after - it if specified, else this will start at the oldest guild. - limit : :obj:`~int` - If specified, the maximum amount of guilds that this paginator - should return. - - Example - ------- - .. code-block:: python - - async for user in client.fetch_my_guilds_after(after=9876543, limit=1231): - await client.leave_guild(guild) - - Returns - ------- - :obj:`~typing.AsyncIterator` [ :obj:`~hikari.oauth2.OwnGuild` ] - An async iterator of partial guild objects. - - Raises - ------ - :obj:`~hikari.errors.NotFoundHTTPError` - If the guild is not found. - :obj:`~hikari.errors.BadRequestHTTPError` - If any invalid snowflake IDs are passed; a snowflake may be invalid - due to it being outside of the range of a 64 bit integer. - """ - if isinstance(after, datetime.datetime): - after = str(snowflakes.Snowflake.from_datetime(after)) - else: - after = str(after.id if isinstance(after, snowflakes.UniqueEntity) else int(after)) - return self._pagination_handler( - deserializer=oauth2.OwnGuild.deserialize, - direction="after", - request=self._session.get_current_user_guilds, - reversing=False, - start=after, - limit=limit, - ) - - def fetch_my_guilds_before( - self, - *, - before: typing.Union[datetime.datetime, snowflakes.HashableT[guilds.Guild], None] = None, - limit: typing.Optional[int] = None, - ) -> typing.AsyncIterator[oauth2.OwnGuild]: - """Get an async iterator of the guilds the current user is in. - - This returns the guilds that were created before a given user object/ID - or from the newest guild. - - Parameters - ---------- - before : :obj:`~typing.Union` [ :obj:`~datetime.datetime`, :obj:`~hikari.guilds.Guild`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] - The object or ID of a guild to get guilds that were created - before it if specified, else this will start at the newest guild. - limit : :obj:`~int` - If specified, the maximum amount of guilds that this paginator - should return. - - Returns - ------- - :obj:`~typing.AsyncIterator` [ :obj:`~hikari.oauth2.OwnGuild` ] - An async iterator of partial guild objects. - - Raises - ------ - :obj:`~hikari.errors.NotFoundHTTPError` - If the guild is not found. - :obj:`~hikari.errors.BadRequestHTTPError` - If any invalid snowflake IDs are passed; a snowflake may be invalid - due to it being outside of the range of a 64 bit integer. - """ - if isinstance(before, datetime.datetime): - before = str(snowflakes.Snowflake.from_datetime(before)) - elif before is not None: - before = str(before.id if isinstance(before, snowflakes.UniqueEntity) else int(before)) - return self._pagination_handler( - deserializer=oauth2.OwnGuild.deserialize, - direction="before", - request=self._session.get_current_user_guilds, - reversing=False, - start=before, - limit=limit, - ) - - async def leave_guild(self, guild: snowflakes.HashableT[guilds.Guild]) -> None: - """Make the current user leave a given guild. - - Parameters - ---------- - guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] - The object or ID of the guild to leave. - - Raises - ------ - :obj:`~hikari.errors.NotFoundHTTPError` - If the guild is not found. - :obj:`~hikari.errors.BadRequestHTTPError` - If any invalid snowflake IDs are passed; a snowflake may be invalid - due to it being outside of the range of a 64 bit integer. - """ - await self._session.leave_guild( - guild_id=str(guild.id if isinstance(guild, snowflakes.UniqueEntity) else int(guild)) - ) - - async def create_dm_channel(self, recipient: snowflakes.HashableT[users.User]) -> _channels.DMChannel: - """Create a new DM channel with a given user. - - Parameters - ---------- - recipient : :obj:`~typing.Union` [ :obj:`~hikari.users.User`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] - The object or ID of the user to create the new DM channel with. - - Returns - ------- - :obj:`~hikari.channels.DMChannel` - The newly created DM channel object. - - Raises - ------ - :obj:`~hikari.errors.NotFoundHTTPError` - If the recipient is not found. - :obj:`~hikari.errors.BadRequestHTTPError` - If any invalid snowflake IDs are passed; a snowflake may be invalid - due to it being outside of the range of a 64 bit integer. - """ - payload = await self._session.create_dm( - recipient_id=str(recipient.id if isinstance(recipient, snowflakes.UniqueEntity) else int(recipient)) - ) - return _channels.DMChannel.deserialize(payload) - - async def fetch_voice_regions(self) -> typing.Sequence[voices.VoiceRegion]: - """Get the voice regions that are available. - - Returns - ------- - :obj:`~typing.Sequence` [ :obj:`~hikari.voices.VoiceRegion` ] - A list of voice regions available - - Note - ---- - This does not include VIP servers. - """ - payload = await self._session.list_voice_regions() - return [voices.VoiceRegion.deserialize(region) for region in payload] - - async def create_webhook( - self, - channel: snowflakes.HashableT[_channels.GuildChannel], - name: str, - *, - avatar_data: conversions.FileLikeT = ..., - reason: str = ..., - ) -> webhooks.Webhook: - """Create a webhook for a given channel. - - Parameters - ---------- - channel : :obj:`~typing.Union` [ :obj:`~hikari.channels.GuildChannel`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] - The object or ID of the channel for webhook to be created in. - name : :obj:`~str` - The webhook's name string. - avatar_data : ``hikari.internal.conversions.FileLikeT`` - If specified, the avatar image data. - reason : :obj:`~str` - If specified, the audit log reason explaining why the operation - was performed. - - Returns - ------- - :obj:`~hikari.webhooks.Webhook` - The newly created webhook object. - - Raises - ------ - :obj:`~hikari.errors.NotFoundHTTPError` - If the channel is not found. - :obj:`~hikari.errors.ForbiddenHTTPError` - If you either lack the ``MANAGE_WEBHOOKS`` permission or - can not see the given channel. - :obj:`~hikari.errors.BadRequestHTTPError` - If the avatar image is too big or the format is invalid. - If any invalid snowflake IDs are passed; a snowflake may be invalid - due to it being outside of the range of a 64 bit integer. - """ - payload = await self._session.create_webhook( - channel_id=str(channel.id if isinstance(channel, snowflakes.UniqueEntity) else int(channel)), - name=name, - avatar=conversions.get_bytes_from_resource(avatar_data) if avatar_data is not ... else ..., - reason=reason, - ) - return webhooks.Webhook.deserialize(payload) - - async def fetch_channel_webhooks( - self, channel: snowflakes.HashableT[_channels.GuildChannel] - ) -> typing.Sequence[webhooks.Webhook]: - """Get all webhooks from a given channel. - - Parameters - ---------- - channel : :obj:`~typing.Union` [ :obj:`~hikari.channels.GuildChannel`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] - The object or ID of the guild channel to get the webhooks from. - - Returns - ------- - :obj:`~typing.Sequence` [ :obj:`~hikari.webhooks.Webhook` ] - A list of webhook objects for the give channel. - - Raises - ------ - :obj:`~hikari.errors.BadRequestHTTPError` - If any invalid snowflake IDs are passed; a snowflake may be invalid - due to it being outside of the range of a 64 bit integer. - :obj:`~hikari.errors.NotFoundHTTPError` - If the channel is not found. - :obj:`~hikari.errors.ForbiddenHTTPError` - If you either lack the ``MANAGE_WEBHOOKS`` permission or - can not see the given channel. - """ - payload = await self._session.get_channel_webhooks( - channel_id=str(channel.id if isinstance(channel, snowflakes.UniqueEntity) else int(channel)) - ) - return [webhooks.Webhook.deserialize(webhook) for webhook in payload] - - async def fetch_guild_webhooks( - self, guild: snowflakes.HashableT[guilds.Guild] - ) -> typing.Sequence[webhooks.Webhook]: - """Get all webhooks for a given guild. - - Parameters - ---------- - guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] - The object or ID for the guild to get the webhooks from. - - Returns - ------- - :obj:`~typing.Sequence` [ :obj:`~hikari.webhooks.Webhook` ] - A list of webhook objects for the given guild. - - Raises - ------ - :obj:`~hikari.errors.BadRequestHTTPError` - If any invalid snowflake IDs are passed; a snowflake may be invalid - due to it being outside of the range of a 64 bit integer. - :obj:`~hikari.errors.NotFoundHTTPError` - If the guild is not found. - :obj:`~hikari.errors.ForbiddenHTTPError` - If you either lack the ``MANAGE_WEBHOOKS`` permission or - aren't a member of the given guild. - """ - payload = await self._session.get_guild_webhooks( - guild_id=str(guild.id if isinstance(guild, snowflakes.UniqueEntity) else int(guild)) - ) - return [webhooks.Webhook.deserialize(webhook) for webhook in payload] - - async def fetch_webhook( - self, webhook: snowflakes.HashableT[webhooks.Webhook], *, webhook_token: str = ... - ) -> webhooks.Webhook: - """Get a given webhook. - - Parameters - ---------- - webhook : :obj:`~typing.Union` [ :obj:`~hikari.webhooks.Webhook`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] - The object or ID of the webhook to get. - webhook_token : :obj:`~str` - If specified, the webhook token to use to get it (bypassing this - session's provided authorization ``token``). - - Returns - ------- - :obj:`~hikari.webhooks.Webhook` - The requested webhook object. - - Raises - ------ - :obj:`~hikari.errors.BadRequestHTTPError` - If any invalid snowflake IDs are passed; a snowflake may be invalid - due to it being outside of the range of a 64 bit integer. - :obj:`~hikari.errors.NotFoundHTTPError` - If the webhook is not found. - :obj:`~hikari.errors.ForbiddenHTTPError` - If you're not in the guild that owns this webhook or - lack the ``MANAGE_WEBHOOKS`` permission. - :obj:`~hikari.errors.UnauthorizedHTTPError` - If you pass a token that's invalid for the target webhook. - """ - payload = await self._session.get_webhook( - webhook_id=str(webhook.id if isinstance(webhook, snowflakes.UniqueEntity) else int(webhook)), - webhook_token=webhook_token, - ) - return webhooks.Webhook.deserialize(payload) - - async def update_webhook( - self, - webhook: snowflakes.HashableT[webhooks.Webhook], - *, - webhook_token: str = ..., - name: str = ..., - avatar_data: typing.Optional[conversions.FileLikeT] = ..., - channel: snowflakes.HashableT[_channels.GuildChannel] = ..., - reason: str = ..., - ) -> webhooks.Webhook: - """Edit a given webhook. - - Parameters - ---------- - webhook : :obj:`~typing.Union` [ :obj:`~hikari.webhooks.Webhook`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] - The object or ID of the webhook to edit. - webhook_token : :obj:`~str` - If specified, the webhook token to use to modify it (bypassing this - session's provided authorization ``token``). - name : :obj:`~str` - If specified, the new name string. - avatar_data : ``hikari.internal.conversions.FileLikeT``, optional - If specified, the new avatar image file object. If :obj:`~None`, then - it is removed. - channel : :obj:`~typing.Union` [ :obj:`~hikari.channels.GuildChannel`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] - If specified, the object or ID of the new channel the given - webhook should be moved to. - reason : :obj:`~str` - If specified, the audit log reason explaining why the operation - was performed. - - Returns - ------- - :obj:`~hikari.webhooks.Webhook` - The updated webhook object. - - Raises - ------ - :obj:`~hikari.errors.BadRequestHTTPError` - If any invalid snowflake IDs are passed; a snowflake may be invalid - due to it being outside of the range of a 64 bit integer. - :obj:`~hikari.errors.NotFoundHTTPError` - If either the webhook or the channel aren't found. - :obj:`~hikari.errors.ForbiddenHTTPError` - If you either lack the ``MANAGE_WEBHOOKS`` permission or - aren't a member of the guild this webhook belongs to. - :obj:`~hikari.errors.UnauthorizedHTTPError` - If you pass a token that's invalid for the target webhook. - """ - payload = await self._session.modify_webhook( - webhook_id=str(webhook.id if isinstance(webhook, snowflakes.UniqueEntity) else int(webhook)), - webhook_token=webhook_token, - name=name, - avatar=( - conversions.get_bytes_from_resource(avatar_data) - if avatar_data and avatar_data is not ... - else avatar_data - ), - channel_id=( - str(channel.id if isinstance(channel, snowflakes.UniqueEntity) else int(channel)) - if channel and channel is not ... - else channel - ), - reason=reason, - ) - return webhooks.Webhook.deserialize(payload) - - async def delete_webhook( - self, webhook: snowflakes.HashableT[webhooks.Webhook], *, webhook_token: str = ... - ) -> None: - """Delete a given webhook. - - Parameters - ---------- - webhook : :obj:`~typing.Union` [ :obj:`~hikari.webhooks.Webhook`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] - The object or ID of the webhook to delete - webhook_token : :obj:`~str` - If specified, the webhook token to use to delete it (bypassing this - session's provided authorization ``token``). - - Raises - ------ - :obj:`~hikari.errors.BadRequestHTTPError` - If any invalid snowflake IDs are passed; a snowflake may be invalid - due to it being outside of the range of a 64 bit integer. - :obj:`~hikari.errors.NotFoundHTTPError` - If the webhook is not found. - :obj:`~hikari.errors.ForbiddenHTTPError` - If you either lack the ``MANAGE_WEBHOOKS`` permission or - aren't a member of the guild this webhook belongs to. - :obj:`~hikari.errors.UnauthorizedHTTPError` - If you pass a token that's invalid for the target webhook. - """ - await self._session.delete_webhook( - webhook_id=str(webhook.id if isinstance(webhook, snowflakes.UniqueEntity) else int(webhook)), - webhook_token=webhook_token, - ) - - async def execute_webhook( - self, - webhook: snowflakes.HashableT[webhooks.Webhook], - webhook_token: str, - *, - content: str = ..., - username: str = ..., - avatar_url: str = ..., - tts: bool = ..., - wait: bool = False, - file: media.IO = ..., - embeds: typing.Sequence[_embeds.Embed] = ..., - mentions_everyone: bool = True, - user_mentions: typing.Union[typing.Collection[snowflakes.HashableT[users.User]], bool] = True, - role_mentions: typing.Union[typing.Collection[snowflakes.HashableT[guilds.GuildRole]], bool] = True, - ) -> typing.Optional[_messages.Message]: - """Execute a webhook to create a message. - - Parameters - ---------- - webhook : :obj:`~typing.Union` [ :obj:`~hikari.webhooks.Webhook`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] - The object or ID of the webhook to execute. - webhook_token : :obj:`~str` - The token of the webhook to execute. - content : :obj:`~str` - If specified, the message content to send with the message. - username : :obj:`~str` - If specified, the username to override the webhook's username - for this request. - avatar_url : :obj:`~str` - If specified, the url of an image to override the webhook's - avatar with for this request. - tts : :obj:`~bool` - If specified, whether the message will be sent as a TTS message. - wait : :obj:`~bool` - If specified, whether this request should wait for the webhook - to be executed and return the resultant message object. - file : ``hikari.media.IO`` - If specified, this is a file object to send along with the webhook - as defined in :mod:`hikari.media`. - embeds : :obj:`~typing.Sequence` [ :obj:`~hikari.embeds.Embed` ] - If specified, a sequence of ``1`` to ``10`` embed objects to send - with the embed. - mentions_everyone : :obj:`~bool` - Whether ``@everyone`` and ``@here`` mentions should be resolved by - discord and lead to actual pings, defaults to :obj:`~True`. - user_mentions : :obj:`~typing.Union` [ :obj:`~typing.Collection` [ :obj:`~typing.Union` [ :obj:`~hikari.users.User`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ], :obj:`~bool` ] - Either an array of user objects/IDs to allow mentions for, - :obj:`~True` to allow all user mentions or :obj:`~False` to block all - user mentions from resolving, defaults to :obj:`~True`. - role_mentions : :obj:`~typing.Union` [ :obj:`~typing.Collection` [ :obj:`~typing.Union` [ :obj:`~hikari.guilds.GuildRole`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] ], :obj:`~bool` ] - Either an array of guild role objects/IDs to allow mentions for, - :obj:`~True` to allow all role mentions or :obj:`~False` to block all - role mentions from resolving, defaults to :obj:`~True`. - - Returns - ------- - :obj:`~hikari.messages.Message`, optional - The created message object, if ``wait`` is :obj:`~True`, else - :obj:`~None`. - - Raises - ------ - :obj:`~hikari.errors.NotFoundHTTPError` - If the channel ID or webhook ID is not found. - :obj:`~hikari.errors.BadRequestHTTPError` - This can be raised if the file is too large; if the embed exceeds - the defined limits; if the message content is specified only and - empty or greater than ``2000`` characters; if neither content, file - or embeds are specified. - If any invalid snowflake IDs are passed; a snowflake may be invalid - due to it being outside of the range of a 64 bit integer. - :obj:`~hikari.errors.ForbiddenHTTPError` - If you lack permissions to send to this channel. - :obj:`~hikari.errors.UnauthorizedHTTPError` - If you pass a token that's invalid for the target webhook. - """ - payload = await self._session.execute_webhook( - webhook_id=str(webhook.id if isinstance(webhook, snowflakes.UniqueEntity) else int(webhook)), - webhook_token=webhook_token, - content=content, - username=username, - avatar_url=avatar_url, - tts=tts, - wait=wait, - file=await media.safe_read_file(file) if file is not ... else ..., - embeds=[embed.serialize() for embed in embeds] if embeds is not ... else ..., - allowed_mentions=self._generate_allowed_mentions( - mentions_everyone=mentions_everyone, user_mentions=user_mentions, role_mentions=role_mentions - ), - ) - if wait is True: - return _messages.Message.deserialize(payload) - return None - - def safe_webhook_execute( - self, - webhook: snowflakes.HashableT[webhooks.Webhook], - webhook_token: str, - *, - content: str = ..., - username: str = ..., - avatar_url: str = ..., - tts: bool = ..., - wait: bool = False, - file: media.IO = ..., - embeds: typing.Sequence[_embeds.Embed] = ..., - mentions_everyone: bool = False, - user_mentions: typing.Union[typing.Collection[snowflakes.HashableT[users.User]], bool] = False, - role_mentions: typing.Union[typing.Collection[snowflakes.HashableT[guilds.GuildRole]], bool] = False, - ) -> typing.Coroutine[typing.Any, typing.Any, typing.Optional[_messages.Message]]: - """Execute a webhook to create a message with mention safety. - - This endpoint has the same signature as :attr:`execute_webhook` with - the only difference being that ``mentions_everyone``, - ``user_mentions`` and ``role_mentions`` default to :obj:`~False`. - """ - return self.execute_webhook( - webhook=webhook, - webhook_token=webhook_token, - content=content, - username=username, - avatar_url=avatar_url, - tts=tts, - wait=wait, - file=file, - embeds=embeds, - mentions_everyone=mentions_everyone, - user_mentions=user_mentions, - role_mentions=role_mentions, - ) diff --git a/hikari/clients/rest_clients/__init__.py b/hikari/clients/rest_clients/__init__.py new file mode 100644 index 0000000000..4107206743 --- /dev/null +++ b/hikari/clients/rest_clients/__init__.py @@ -0,0 +1,85 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# Copyright © Nekoka.tt 2019-2020 +# +# This file is part of Hikari. +# +# Hikari is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Hikari is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Hikari. If not, see . +"""Marshall wrappings for the REST implementation in :mod:`hikari.net.rest`. + +This provides an object-oriented interface for interacting with discord's REST +API. +""" + +__all__ = ["RESTClient"] + +from hikari.clients.rest_clients import channels_component +from hikari.clients.rest_clients import current_users_component +from hikari.clients.rest_clients import gateways_component +from hikari.clients.rest_clients import guilds_component +from hikari.clients.rest_clients import invites_component +from hikari.clients.rest_clients import oauth2_component +from hikari.clients.rest_clients import reactions_component +from hikari.clients.rest_clients import users_component +from hikari.clients.rest_clients import voices_component +from hikari.clients.rest_clients import webhooks_component +from hikari.clients import configs +from hikari.net import rest + + +class RESTClient( + channels_component.RESTChannelComponent, + current_users_component.RESTCurrentUserComponent, + gateways_component.RESTGatewayComponent, + guilds_component.RESTGuildComponent, + invites_component.RESTInviteComponent, + oauth2_component.RESTOauth2Component, + reactions_component.RESTReactionComponent, + users_component.RESTUserComponent, + voices_component.RESTVoiceComponent, + webhooks_component.RESTWebhookComponent, +): + """ + A marshalling object-oriented REST API client. + + This client bridges the basic REST API exposed by + :obj:`~hikari.net.rest.LowLevelRestfulClient` and wraps it in a unit of + processing that can handle parsing API objects into Hikari entity objects. + + Parameters + ---------- + config : :obj:`~hikari.clients.configs.RESTConfig` + A HTTP configuration object. + + Note + ---- + For all endpoints where a ``reason`` argument is provided, this may be a + string inclusively between ``0`` and ``512`` characters length, with any + additional characters being cut off. + """ + + def __init__(self, config: configs.RESTConfig) -> None: + super().__init__( + rest.LowLevelRestfulClient( + allow_redirects=config.allow_redirects, + connector=config.tcp_connector, + proxy_headers=config.proxy_headers, + proxy_auth=config.proxy_auth, + ssl_context=config.ssl_context, + verify_ssl=config.verify_ssl, + timeout=config.request_timeout, + token=f"{config.token_type} {config.token}" if config.token_type is not None else config.token, + version=config.rest_version, + ) + ) diff --git a/hikari/clients/rest_clients/channels_component.py b/hikari/clients/rest_clients/channels_component.py new file mode 100644 index 0000000000..fd63941b32 --- /dev/null +++ b/hikari/clients/rest_clients/channels_component.py @@ -0,0 +1,1114 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# Copyright © Nekoka.tt 2019-2020 +# +# This file is part of Hikari. +# +# Hikari is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Hikari is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Hikari. If not, see . +"""The logic for handling requests to channel endpoints.""" + +__all__ = ["RESTChannelComponent"] + +import asyncio +import datetime +import typing + +from hikari.clients.rest_clients import component_base +from hikari.internal import allowed_mentions +from hikari.internal import assertions +from hikari.internal import conversions +from hikari.internal import pagination +from hikari import channels as _channels +from hikari import embeds as _embeds +from hikari import guilds +from hikari import invites +from hikari import media +from hikari import messages as _messages +from hikari import permissions as _permissions +from hikari import snowflakes +from hikari import users +from hikari import webhooks + + +class RESTChannelComponent(component_base.BaseRESTComponent): # pylint: disable=W0223 + """The REST client component for handling requests to channel endpoints.""" + + async def fetch_channel(self, channel: snowflakes.HashableT[_channels.Channel]) -> _channels.Channel: + """Get an up to date channel object from a given channel object or ID. + + Parameters + ---------- + channel : :obj:`~typing.Union` [ :obj:`~hikari.channels.Channel`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] + The object ID of the channel to look up. + + Returns + ------- + :obj:`~hikari.channels.Channel` + The channel object that has been found. + + Raises + ------ + :obj:`~hikari.errors.BadRequestHTTPError` + If any invalid snowflake IDs are passed; a snowflake may be invalid + due to it being outside of the range of a 64 bit integer. + :obj:`~hikari.errors.ForbiddenHTTPError` + If you don't have access to the channel. + :obj:`~hikari.errors.NotFoundHTTPError` + If the channel does not exist. + """ + payload = await self._session.get_channel( + channel_id=str(channel.id if isinstance(channel, snowflakes.UniqueEntity) else int(channel)) + ) + return _channels.deserialize_channel(payload) + + async def update_channel( + self, + channel: snowflakes.HashableT[_channels.Channel], + *, + name: str = ..., + position: int = ..., + topic: str = ..., + nsfw: bool = ..., + bitrate: int = ..., + user_limit: int = ..., + rate_limit_per_user: typing.Union[int, datetime.timedelta] = ..., + permission_overwrites: typing.Sequence[_channels.PermissionOverwrite] = ..., + parent_category: typing.Optional[snowflakes.HashableT[_channels.GuildCategory]] = ..., + reason: str = ..., + ) -> _channels.Channel: + """Update one or more aspects of a given channel ID. + + Parameters + ---------- + channel : :obj:`~typing.Union` [ :obj:`~hikari.channels.Channel`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] + The channel ID to update. + name : :obj:`~str` + If specified, the new name for the channel. This must be + inclusively between ``1`` and ``100`` characters in length. + position : :obj:`~int` + If specified, the position to change the channel to. + topic : :obj:`~str` + If specified, the topic to set. This is only applicable to + text channels. This must be inclusively between ``0`` and ``1024`` + characters in length. + nsfw : :obj:`~bool` + Mark the channel as being not safe for work (NSFW) if :obj:`~True`. + If :obj:`~False` or unspecified, then the channel is not marked as + NSFW. Will have no visible effect for non-text guild channels. + rate_limit_per_user : :obj:`~typing.Union` [ :obj:`~int`, :obj:`~datetime.timedelta` ] + If specified, the time delta of seconds the user has to wait + before sending another message. This will not apply to bots, or to + members with ``MANAGE_MESSAGES`` or ``MANAGE_CHANNEL`` permissions. + This must be inclusively between ``0`` and ``21600`` seconds. + bitrate : :obj:`~int` + If specified, the bitrate in bits per second allowable for the + channel. This only applies to voice channels and must be inclusively + between ``8000`` and ``96000`` for normal servers or ``8000`` and + ``128000`` for VIP servers. + user_limit : :obj:`~int` + If specified, the new max number of users to allow in a voice + channel. This must be between ``0`` and ``99`` inclusive, where + ``0`` implies no limit. + permission_overwrites : :obj:`~typing.Sequence` [ :obj:`~hikari.channels.PermissionOverwrite` ] + If specified, the new list of permission overwrites that are + category specific to replace the existing overwrites with. + parent_category : :obj:`~typing.Union` [ :obj:`~hikari.channels.Channel`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ], optional + If specified, the new parent category ID to set for the channel, + pass :obj:`~None` to unset. + reason : :obj:`~str` + If specified, the audit log reason explaining why the operation + was performed. + + Returns + ------- + :obj:`~hikari.channels.Channel` + The channel object that has been modified. + + Raises + ------ + :obj:`~hikari.errors.NotFoundHTTPError` + If the channel does not exist. + :obj:`~hikari.errors.ForbiddenHTTPError` + If you lack the permission to make the change. + :obj:`~hikari.errors.BadRequestHTTPError` + If you provide incorrect options for the corresponding channel type + (e.g. a ``bitrate`` for a text channel). + If any invalid snowflake IDs are passed; a snowflake may be invalid + due to it being outside of the range of a 64 bit integer. + """ + payload = await self._session.modify_channel( + channel_id=str(channel.id if isinstance(channel, snowflakes.UniqueEntity) else int(channel)), + name=name, + position=position, + topic=topic, + nsfw=nsfw, + bitrate=bitrate, + user_limit=user_limit, + rate_limit_per_user=( + int(rate_limit_per_user.total_seconds()) + if isinstance(rate_limit_per_user, datetime.timedelta) + else rate_limit_per_user + ), + permission_overwrites=( + [po.serialize() for po in permission_overwrites] if permission_overwrites is not ... else ... + ), + parent_id=( + str( + parent_category.id if isinstance(parent_category, snowflakes.UniqueEntity) else int(parent_category) + ) + if parent_category is not ... and parent_category is not None + else parent_category + ), + reason=reason, + ) + return _channels.deserialize_channel(payload) + + async def delete_channel(self, channel: snowflakes.HashableT[_channels.Channel]) -> None: + """Delete the given channel ID, or if it is a DM, close it. + + Parameters + ---------- + channel : :obj:`~typing.Union` [ :obj:`~hikari.channels.Channel`, :obj:`~hikari.snowflakes.Snowflake` :obj:`~str` ] + The object or ID of the channel to delete. + + Returns + ------- + :obj:`~None` + Nothing, unlike what the API specifies. This is done to maintain + consistency with other calls of a similar nature in this API + wrapper. + + Raises + ------ + :obj:`~hikari.errors.BadRequestHTTPError` + If any invalid snowflake IDs are passed; a snowflake may be invalid + due to it being outside of the range of a 64 bit integer. + :obj:`~hikari.errors.NotFoundHTTPError` + If the channel does not exist. + :obj:`~hikari.errors.ForbiddenHTTPError` + If you do not have permission to delete the channel. + + Note + ---- + Closing a DM channel won't raise an exception but will have no effect + and "closed" DM channels will not have to be reopened to send messages + in theme. + + Warning + ------- + Deleted channels cannot be un-deleted. + """ + await self._session.delete_close_channel( + channel_id=str(channel.id if isinstance(channel, snowflakes.UniqueEntity) else int(channel)) + ) + + def fetch_messages_after( + self, + channel: snowflakes.HashableT[_channels.Channel], + *, + after: typing.Union[datetime.datetime, snowflakes.HashableT[_messages.Message]] = 0, + limit: typing.Optional[int] = None, + ) -> typing.AsyncIterator[_messages.Message]: + """Return an async iterator that retrieves a channel's message history. + + This will return the message created after a given message object/ID or + from the first message in the channel. + + Parameters + ---------- + channel : :obj:`~typing.Union` [ :obj:`~hikari.channels.Channel`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] + The ID of the channel to retrieve the messages from. + limit : :obj:`~int` + If specified, the maximum number of how many messages this iterator + should return. + after : :obj:`~typing.Union` [ :obj:`~datetime.datetime`, :obj:`~hikari.channels.Channel`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] + A object or ID message. Only return messages sent AFTER this + message if it's specified else this will return every message after + (and including) the first message in the channel. + + Example + ------- + .. code-block:: python + + async for message in client.fetch_messages_after(channel, after=9876543, limit=3232): + if message.author.id in BLACKLISTED_USERS: + await client.ban_member(channel.guild_id, message.author) + + Returns + ------- + :obj:`~typing.AsyncIterator` [ :obj:`~hikari.messages.Message` ] + An async iterator that retrieves the channel's message objects. + + Raises + ------ + :obj:`~hikari.errors.BadRequestHTTPError` + If any invalid snowflake IDs are passed; a snowflake may be invalid + due to it being outside of the range of a 64 bit integer. + :obj:`~hikari.errors.ForbiddenHTTPError` + If you lack permission to read the channel. + :obj:`~hikari.errors.NotFoundHTTPError` + If the channel is not found, or the message + provided for one of the filter arguments is not found. + + Note + ---- + If you are missing the ``VIEW_CHANNEL`` permission, you will receive a + :obj:`~hikari.errors.ForbiddenHTTPError`. If you are instead missing + the ``READ_MESSAGE_HISTORY`` permission, you will always receive + zero results, and thus an empty list will be returned instead. + """ + if isinstance(after, datetime.datetime): + after = str(snowflakes.Snowflake.from_datetime(after)) + else: + after = str(after.id if isinstance(after, snowflakes.UniqueEntity) else int(after)) + return pagination.pagination_handler( + channel_id=str(channel.id if isinstance(channel, snowflakes.UniqueEntity) else int(channel)), + deserializer=_messages.Message.deserialize, + direction="after", + start=after, + request=self._session.get_channel_messages, + reversing=True, # This is the only known endpoint where reversing is needed. + limit=limit, + ) + + def fetch_messages_before( + self, + channel: snowflakes.HashableT[_channels.Channel], + *, + before: typing.Union[datetime.datetime, snowflakes.HashableT[_messages.Message], None] = None, + limit: typing.Optional[int] = None, + ) -> typing.AsyncIterator[_messages.Message]: + """Return an async iterator that retrieves a channel's message history. + + This returns the message created after a given message object/ID or + from the first message in the channel. + + Parameters + ---------- + channel : :obj:`~typing.Union` [ :obj:`~hikari.channels.Channel`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] + The ID of the channel to retrieve the messages from. + limit : :obj:`~int` + If specified, the maximum number of how many messages this iterator + should return. + before : :obj:`~typing.Union` [ :obj:`~datetime.datetime`, :obj:`~hikari.channels.Channel`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] + A message object or ID. Only return messages sent BEFORE + this message if this is specified else this will return every + message before (and including) the most recent message in the + channel. + + Example + ------- + .. code-block:: python + + async for message in client.fetch_messages_before(channel, before=9876543, limit=1231): + if message.content.lower().contains("delete this"): + await client.delete_message(channel, message) + + Returns + ------- + :obj:`~typing.AsyncIterator` [ :obj:`~hikari.messages.Message` ] + An async iterator that retrieves the channel's message objects. + + Raises + ------ + :obj:`~hikari.errors.BadRequestHTTPError` + If any invalid snowflake IDs are passed; a snowflake may be invalid + due to it being outside of the range of a 64 bit integer. + :obj:`~hikari.errors.ForbiddenHTTPError` + If you lack permission to read the channel. + :obj:`~hikari.errors.NotFoundHTTPError` + If the channel is not found, or the message + provided for one of the filter arguments is not found. + + Note + ---- + If you are missing the ``VIEW_CHANNEL`` permission, you will receive a + :obj:`~hikari.errors.ForbiddenHTTPError`. If you are instead missing + the ``READ_MESSAGE_HISTORY`` permission, you will always receive + zero results, and thus an empty list will be returned instead. + """ + if isinstance(before, datetime.datetime): + before = str(snowflakes.Snowflake.from_datetime(before)) + elif before is not None: + before = str(before.id if isinstance(before, snowflakes.UniqueEntity) else int(before)) + return pagination.pagination_handler( + channel_id=str(channel.id if isinstance(channel, snowflakes.UniqueEntity) else int(channel)), + deserializer=_messages.Message.deserialize, + direction="before", + start=before, + request=self._session.get_channel_messages, + reversing=False, + limit=limit, + ) + + async def fetch_messages_around( + self, + channel: snowflakes.HashableT[_channels.Channel], + around: typing.Union[datetime.datetime, snowflakes.HashableT[_messages.Message]], + *, + limit: int = ..., + ) -> typing.AsyncIterator[_messages.Message]: + """Return an async iterator that retrieves up to 100 messages. + + This will return messages in order from newest to oldest, is based + around the creation time of the supplied message object/ID and will + include the given message if it still exists. + + Parameters + ---------- + channel : :obj:`~typing.Union` [ :obj:`~hikari.channels.Channel`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] + The ID of the channel to retrieve the messages from. + around : :obj:`~typing.Union` [ :obj:`~datetime.datetime`, :obj:`~hikari.channels.Channel`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] + The object or ID of the message to get messages that were sent + AROUND it in the provided channel, unlike ``before`` and ``after``, + this argument is required and the provided message will also be + returned if it still exists. + limit : :obj:`~int` + If specified, the maximum number of how many messages this iterator + should return, cannot be more than `100` + + Example + ------- + .. code-block:: python + + async for message in client.fetch_messages_around(channel, around=9876543, limit=42): + if message.embeds and not message.author.is_bot: + await client.delete_message(channel, message) + + Returns + ------- + :obj:`~typing.AsyncIterator` [ :obj:`~hikari.messages.Message` ] + An async iterator that retrieves the found message objects. + + Raises + ------ + :obj:`~hikari.errors.BadRequestHTTPError` + If any invalid snowflake IDs are passed; a snowflake may be invalid + due to it being outside of the range of a 64 bit integer. + :obj:`~hikari.errors.ForbiddenHTTPError` + If you lack permission to read the channel. + :obj:`~hikari.errors.NotFoundHTTPError` + If the channel is not found, or the message + provided for one of the filter arguments is not found. + + Note + ---- + If you are missing the ``VIEW_CHANNEL`` permission, you will receive a + :obj:`~hikari.errors.ForbiddenHTTPError`. If you are instead missing + the ``READ_MESSAGE_HISTORY`` permission, you will always receive + zero results, and thus an empty list will be returned instead. + """ + if isinstance(around, datetime.datetime): + around = str(snowflakes.Snowflake.from_datetime(around)) + else: + around = str(around.id if isinstance(around, snowflakes.UniqueEntity) else int(around)) + for payload in await self._session.get_channel_messages( + channel_id=str(channel.id if isinstance(channel, snowflakes.UniqueEntity) else int(channel)), + limit=limit, + around=around, + ): + yield _messages.Message.deserialize(payload) + + async def fetch_message( + self, channel: snowflakes.HashableT[_channels.Channel], message: snowflakes.HashableT[_messages.Message], + ) -> _messages.Message: + """Get a message from known channel that we can access. + + Parameters + ---------- + channel : :obj:`~typing.Union` [ :obj:`~hikari.channels.Channel`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] + The object or ID of the channel to get the message from. + message : :obj:`~typing.Union` [ :obj:`~hikari.messages.Message`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] + The object or ID of the message to retrieve. + + Returns + ------- + :obj:`~hikari.messages.Message` + The found message object. + + Note + ---- + This requires the ``READ_MESSAGE_HISTORY`` permission. + + Raises + ------ + :obj:`~hikari.errors.BadRequestHTTPError` + If any invalid snowflake IDs are passed; a snowflake may be invalid + due to it being outside of the range of a 64 bit integer. + :obj:`~hikari.errors.ForbiddenHTTPError` + If you lack permission to see the message. + :obj:`~hikari.errors.NotFoundHTTPError` + If the channel or message is not found. + """ + payload = await self._session.get_channel_message( + channel_id=str(channel.id if isinstance(channel, snowflakes.UniqueEntity) else int(channel)), + message_id=str(message.id if isinstance(message, snowflakes.UniqueEntity) else int(message)), + ) + return _messages.Message.deserialize(payload) + + async def create_message( + self, + channel: snowflakes.HashableT[_channels.Channel], + *, + content: str = ..., + nonce: str = ..., + tts: bool = ..., + files: typing.Collection[media.IO] = ..., + embed: _embeds.Embed = ..., + mentions_everyone: bool = True, + user_mentions: typing.Union[typing.Collection[snowflakes.HashableT[users.User]], bool] = True, + role_mentions: typing.Union[typing.Collection[snowflakes.HashableT[guilds.GuildRole]], bool] = True, + ) -> _messages.Message: + """Create a message in the given channel. + + Parameters + ---------- + channel : :obj:`~typing.Union` [ :obj:`~hikari.channels.Channel`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] + The channel or ID of the channel to send to. + content : :obj:`~str` + If specified, the message content to send with the message. + nonce : :obj:`~str` + If specified, an optional ID to send for opportunistic message + creation. This doesn't serve any real purpose for general use, + and can usually be ignored. + tts : :obj:`~bool` + If specified, whether the message will be sent as a TTS message. + files : :obj:`~typing.Collection` [ ``hikari.media.IO`` ] + If specified, this should be a list of inclusively between ``1`` and + ``5`` IO like media objects, as defined in :mod:`hikari.media`. + embed : :obj:`~hikari.embeds.Embed` + If specified, the embed object to send with the message. + mentions_everyone : :obj:`~bool` + Whether ``@everyone`` and ``@here`` mentions should be resolved by + discord and lead to actual pings, defaults to :obj:`~True`. + user_mentions : :obj:`~typing.Union` [ :obj:`~typing.Collection` [ :obj:`~typing.Union` [ :obj:`~hikari.users.User`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ], :obj:`~bool` ] + Either an array of user objects/IDs to allow mentions for, + :obj:`~True` to allow all user mentions or :obj:`~False` to block all + user mentions from resolving, defaults to :obj:`~True`. + role_mentions : :obj:`~typing.Union` [ :obj:`~typing.Collection` [ :obj:`~typing.Union` [ :obj:`~hikari.guilds.GuildRole`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] ], :obj:`~bool` ] + Either an array of guild role objects/IDs to allow mentions for, + :obj:`~True` to allow all role mentions or :obj:`~False` to block all + role mentions from resolving, defaults to :obj:`~True`. + + Returns + ------- + :obj:`~hikari.messages.Message` + The created message object. + + Raises + ------ + :obj:`~hikari.errors.NotFoundHTTPError` + If the channel is not found. + :obj:`~hikari.errors.BadRequestHTTPError` + This can be raised if the file is too large; if the embed exceeds + the defined limits; if the message content is specified only and + empty or greater than ``2000`` characters; if neither content, files + or embed are specified. + If any invalid snowflake IDs are passed; a snowflake may be invalid + due to it being outside of the range of a 64 bit integer. + :obj:`~hikari.errors.ForbiddenHTTPError` + If you lack permissions to send to this channel. + :obj:`~ValueError` + If more than 100 unique objects/snowflakes are passed for + ``role_mentions`` or ``user_mentions``. + """ + payload = await self._session.create_message( + channel_id=str(channel.id if isinstance(channel, snowflakes.UniqueEntity) else int(channel)), + content=content, + nonce=nonce, + tts=tts, + files=await asyncio.gather(*(media.safe_read_file(file) for file in files)) if files is not ... else ..., + embed=embed.serialize() if embed is not ... else ..., + allowed_mentions=allowed_mentions.generate_allowed_mentions( + mentions_everyone=mentions_everyone, user_mentions=user_mentions, role_mentions=role_mentions + ), + ) + return _messages.Message.deserialize(payload) + + def safe_create_message( + self, + channel: snowflakes.HashableT[_channels.Channel], + *, + content: str = ..., + nonce: str = ..., + tts: bool = ..., + files: typing.Collection[media.IO] = ..., + embed: _embeds.Embed = ..., + mentions_everyone: bool = False, + user_mentions: typing.Union[typing.Collection[snowflakes.HashableT[users.User]], bool] = False, + role_mentions: typing.Union[typing.Collection[snowflakes.HashableT[guilds.GuildRole]], bool] = False, + ) -> typing.Coroutine[typing.Any, typing.Any, _messages.Message]: + """Create a message in the given channel with mention safety. + + This endpoint has the same signature as :attr:`create_message` with + the only difference being that ``mentions_everyone``, + ``user_mentions`` and ``role_mentions`` default to :obj:`~False`. + """ + return self.create_message( + channel=channel, + content=content, + nonce=nonce, + tts=tts, + files=files, + embed=embed, + mentions_everyone=mentions_everyone, + user_mentions=user_mentions, + role_mentions=role_mentions, + ) + + async def update_message( + self, + message: snowflakes.HashableT[_messages.Message], + channel: snowflakes.HashableT[_channels.Channel], + *, + content: typing.Optional[str] = ..., + embed: typing.Optional[_embeds.Embed] = ..., + flags: int = ..., + mentions_everyone: bool = True, + user_mentions: typing.Union[typing.Collection[snowflakes.HashableT[users.User]], bool] = True, + role_mentions: typing.Union[typing.Collection[snowflakes.HashableT[guilds.GuildRole]], bool] = True, + ) -> _messages.Message: + """Update the given message. + + Parameters + ---------- + channel : :obj:`~typing.Union` [ :obj:`~hikari.channels.Channel`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] + The object or ID of the channel to get the message from. + message : :obj:`~typing.Union` [ :obj:`~hikari.messages.Message`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] + The object or ID of the message to edit. + content : :obj:`~str`, optional + If specified, the string content to replace with in the message. + If :obj:`~None`, the content will be removed from the message. + embed : :obj:`~hikari.embeds.Embed`, optional + If specified, the embed to replace with in the message. + If :obj:`~None`, the embed will be removed from the message. + flags : :obj:`~hikari.messages.MessageFlag` + If specified, the new flags for this message, while a raw int may + be passed for this, this can lead to unexpected behaviour if it's + outside the range of the MessageFlag int flag. + mentions_everyone : :obj:`~bool` + Whether ``@everyone`` and ``@here`` mentions should be resolved by + discord and lead to actual pings, defaults to :obj:`~True`. + user_mentions : :obj:`~typing.Union` [ :obj:`~typing.Collection` [ :obj:`~typing.Union` [ :obj:`~hikari.users.User`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ], :obj:`~bool` ] + Either an array of user objects/IDs to allow mentions for, + :obj:`~True` to allow all user mentions or :obj:`~False` to block all + user mentions from resolving, defaults to :obj:`~True`. + role_mentions : :obj:`~typing.Union` [ :obj:`~typing.Collection` [ :obj:`~typing.Union` [ :obj:`~hikari.guilds.GuildRole`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] ], :obj:`~bool` ] + Either an array of guild role objects/IDs to allow mentions for, + :obj:`~True` to allow all role mentions or :obj:`~False` to block all + role mentions from resolving, defaults to :obj:`~True`. + + Returns + ------- + :obj:`~hikari.messages.Message` + The edited message object. + + Raises + ------ + :obj:`~hikari.errors.NotFoundHTTPError` + If the channel or message is not found. + :obj:`~hikari.errors.BadRequestHTTPError` + This can be raised if the embed exceeds the defined limits; + if the message content is specified only and empty or greater + than ``2000`` characters; if neither content, file or embed + are specified. + If any invalid snowflake IDs are passed; a snowflake may be invalid + due to it being outside of the range of a 64 bit integer. + :obj:`~hikari.errors.ForbiddenHTTPError` + If you try to edit ``content`` or ``embed`` or ``allowed_mentions` + on a message you did not author. + If you try to edit the flags on a message you did not author without + the ``MANAGE_MESSAGES`` permission. + :obj:`~ValueError` + If more than 100 unique objects/snowflakes are passed for + ``role_mentions`` or ``user_mentions``. + """ + payload = await self._session.edit_message( + channel_id=str(channel.id if isinstance(channel, snowflakes.UniqueEntity) else int(channel)), + message_id=str(message.id if isinstance(message, snowflakes.UniqueEntity) else int(message)), + content=content, + embed=embed.serialize() if embed is not ... and embed is not None else embed, + flags=flags, + allowed_mentions=allowed_mentions.generate_allowed_mentions( + mentions_everyone=mentions_everyone, user_mentions=user_mentions, role_mentions=role_mentions, + ), + ) + return _messages.Message.deserialize(payload) + + def safe_update_message( + self, + message: snowflakes.HashableT[_messages.Message], + channel: snowflakes.HashableT[_channels.Channel], + *, + content: typing.Optional[str] = ..., + embed: typing.Optional[_embeds.Embed] = ..., + flags: int = ..., + mentions_everyone: bool = False, + user_mentions: typing.Union[typing.Collection[snowflakes.HashableT[users.User]], bool] = False, + role_mentions: typing.Union[typing.Collection[snowflakes.HashableT[guilds.GuildRole]], bool] = False, + ) -> typing.Coroutine[typing.Any, typing.Any, _messages.Message]: + """Update a message in the given channel with mention safety. + + This endpoint has the same signature as :attr:`update_message` with + the only difference being that ``mentions_everyone``, + ``user_mentions`` and ``role_mentions`` default to :obj:`~False`. + """ + return self.update_message( + message=message, + channel=channel, + content=content, + embed=embed, + flags=flags, + mentions_everyone=mentions_everyone, + user_mentions=user_mentions, + role_mentions=role_mentions, + ) + + async def delete_messages( + self, + channel: snowflakes.HashableT[_channels.Channel], + message: snowflakes.HashableT[_messages.Message], + *additional_messages: snowflakes.HashableT[_messages.Message], + ) -> None: + """Delete a message in a given channel. + + Parameters + ---------- + channel : :obj:`~typing.Union` [ :obj:`~hikari.channels.Channel`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] + The object or ID of the channel to get the message from. + message : :obj:`~typing.Union` [ :obj:`~hikari.messages.Message`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] + The object or ID of the message to delete. + *additional_messages : :obj:`~typing.Union` [ :obj:`~hikari.messages.Message`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] + Objects and/or IDs of additional messages to delete in the same + channel, in total you can delete up to 100 messages in a request. + + Raises + ------ + :obj:`~hikari.errors.BadRequestHTTPError` + If any invalid snowflake IDs are passed; a snowflake may be invalid + due to it being outside of the range of a 64 bit integer. + :obj:`~hikari.errors.ForbiddenHTTPError` + If you did not author the message and are in a DM, or if you did + not author the message and lack the ``MANAGE_MESSAGES`` + permission in a guild channel. + :obj:`~hikari.errors.NotFoundHTTPError` + If the channel or message is not found. + :obj:`~ValueError` + If you try to delete over ``100`` messages in a single request. + + Note + ---- + This can only be used on guild text channels. + Any message IDs that do not exist or are invalid still add towards the + total ``100`` max messages to remove. This can only delete messages + that are newer than ``2`` weeks in age. If any of the messages ar + older than ``2`` weeks then this call will fail. + """ + if additional_messages: + messages = list( + # dict.fromkeys is used to remove duplicate entries that would cause discord to return an error. + dict.fromkeys( + str(m.id if isinstance(m, snowflakes.UniqueEntity) else int(m)) + for m in (message, *additional_messages) + ) + ) + assertions.assert_that( + len(messages) <= 100, "Only up to 100 messages can be bulk deleted in a single request." + ) + + if len(messages) > 1: + await self._session.bulk_delete_messages( + channel_id=str(channel.id if isinstance(channel, snowflakes.UniqueEntity) else int(channel)), + messages=messages, + ) + return None + + await self._session.delete_message( + channel_id=str(channel.id if isinstance(channel, snowflakes.UniqueEntity) else int(channel)), + message_id=str(message.id if isinstance(message, snowflakes.UniqueEntity) else int(message)), + ) + + async def update_channel_overwrite( + self, + channel: snowflakes.HashableT[_messages.Message], + overwrite: typing.Union[_channels.PermissionOverwrite, users.User, guilds.GuildRole, snowflakes.Snowflake, int], + target_type: typing.Union[_channels.PermissionOverwriteType, str], + *, + allow: typing.Union[_permissions.Permission, int] = ..., + deny: typing.Union[_permissions.Permission, int] = ..., + reason: str = ..., + ) -> None: + """Edit permissions for a given channel. + + Parameters + ---------- + channel : :obj:`~typing.Union` [ :obj:`~hikari.messages.Message`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] + The object or ID of the channel to edit permissions for. + overwrite : :obj:`~typing.Union` [ :obj:`~hikari.channels.PermissionOverwrite`, :obj:`~hikari.guilds.GuildRole`, :obj:`~hikari.users.User`, :obj:`~hikari.snowflakes.Snowflake` , :obj:`~int` ] + The object or ID of the target member or role to edit/create the + overwrite for. + target_type : :obj:`~typing.Union` [ :obj:`~hikari.channels.PermissionOverwriteType`, :obj:`~int` ] + The type of overwrite, passing a raw string that's outside of the + enum's range for this may lead to unexpected behaviour. + allow : :obj:`~typing.Union` [ :obj:`~hikari.permissions.Permission`, :obj:`~int` ] + If specified, the value of all permissions to set to be allowed, + passing a raw integer for this may lead to unexpected behaviour. + deny : :obj:`~typing.Union` [ :obj:`~hikari.permissions.Permission`, :obj:`~int` ] + If specified, the value of all permissions to set to be denied, + passing a raw integer for this may lead to unexpected behaviour. + reason : :obj:`~str` + If specified, the audit log reason explaining why the operation + was performed. + + Raises + ------ + :obj:`~hikari.errors.BadRequestHTTPError` + If any invalid snowflake IDs are passed; a snowflake may be invalid + due to it being outside of the range of a 64 bit integer. + :obj:`~hikari.errors.NotFoundHTTPError` + If the target channel or overwrite doesn't exist. + :obj:`~hikari.errors.ForbiddenHTTPError` + If you lack permission to do this. + """ + await self._session.edit_channel_permissions( + channel_id=str(channel.id if isinstance(channel, snowflakes.UniqueEntity) else int(channel)), + overwrite_id=str(overwrite.id if isinstance(overwrite, snowflakes.UniqueEntity) else int(overwrite)), + type_=target_type, + allow=allow, + deny=deny, + reason=reason, + ) + + async def fetch_invites_for_channel( + self, channel: snowflakes.HashableT[_channels.Channel] + ) -> typing.Sequence[invites.InviteWithMetadata]: + """Get invites for a given channel. + + Parameters + ---------- + channel : :obj:`~typing.Union` [ :obj:`~hikari.channels.Channel`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] + The object or ID of the channel to get invites for. + + Returns + ------- + :obj:`~typing.Sequence` [ :obj:`~hikari.invites.InviteWithMetadata` ] + A list of invite objects. + + Raises + ------ + :obj:`~hikari.errors.BadRequestHTTPError` + If any invalid snowflake IDs are passed; a snowflake may be invalid + due to it being outside of the range of a 64 bit integer. + :obj:`~hikari.errors.ForbiddenHTTPError` + If you lack the ``MANAGE_CHANNELS`` permission. + :obj:`~hikari.errors.NotFoundHTTPError` + If the channel does not exist. + """ + payload = await self._session.get_channel_invites( + channel_id=str(channel.id if isinstance(channel, snowflakes.UniqueEntity) else int(channel)) + ) + return [invites.InviteWithMetadata.deserialize(invite) for invite in payload] + + async def create_invite_for_channel( + self, + channel: snowflakes.HashableT[_channels.Channel], + *, + max_age: typing.Union[int, datetime.timedelta] = ..., + max_uses: int = ..., + temporary: bool = ..., + unique: bool = ..., + target_user: snowflakes.HashableT[users.User] = ..., + target_user_type: typing.Union[invites.TargetUserType, int] = ..., + reason: str = ..., + ) -> invites.InviteWithMetadata: + """Create a new invite for the given channel. + + Parameters + ---------- + channel : :obj:`~typing.Union` [ :obj:`~datetime.timedelta`, :obj:`~str` ] + The object or ID of the channel to create the invite for. + max_age : :obj:`~int` + If specified, the seconds time delta for the max age of the invite, + defaults to ``86400`` seconds (``24`` hours). + Set to ``0`` seconds to never expire. + max_uses : :obj:`~int` + If specified, the max number of uses this invite can have, or ``0`` + for unlimited (as per the default). + temporary : :obj:`~bool` + If specified, whether to grant temporary membership, meaning the + user is kicked when their session ends unless they are given a role. + unique : :obj:`~bool` + If specified, whether to try to reuse a similar invite. + target_user : :obj:`~typing.Union` [ :obj:`~hikari.users.User`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] + If specified, the object or ID of the user this invite should + target. + target_user_type : :obj:`~typing.Union` [ :obj:`~hikari.invites.TargetUserType`, :obj:`~int` ] + If specified, the type of target for this invite, passing a raw + integer for this may lead to unexpected results. + reason : :obj:`~str` + If specified, the audit log reason explaining why the operation + was performed. + + Returns + ------- + :obj:`~hikari.invites.InviteWithMetadata` + The created invite object. + + Raises + ------ + :obj:`~hikari.errors.ForbiddenHTTPError` + If you lack the ``CREATE_INSTANT_MESSAGES`` permission. + :obj:`~hikari.errors.NotFoundHTTPError` + If the channel does not exist. + :obj:`~hikari.errors.BadRequestHTTPError` + If the arguments provided are not valid (e.g. negative age, etc). + If any invalid snowflake IDs are passed; a snowflake may be invalid + due to it being outside of the range of a 64 bit integer. + """ + payload = await self._session.create_channel_invite( + channel_id=str(channel.id if isinstance(channel, snowflakes.UniqueEntity) else int(channel)), + max_age=int(max_age.total_seconds()) if isinstance(max_age, datetime.timedelta) else max_age, + max_uses=max_uses, + temporary=temporary, + unique=unique, + target_user=( + str(target_user.id if isinstance(target_user, snowflakes.UniqueEntity) else int(target_user)) + if target_user is not ... + else ... + ), + target_user_type=target_user_type, + reason=reason, + ) + return invites.InviteWithMetadata.deserialize(payload) + + async def delete_channel_overwrite( + self, + channel: snowflakes.HashableT[_channels.Channel], + overwrite: typing.Union[_channels.PermissionOverwrite, guilds.GuildRole, users.User, snowflakes.Snowflake, int], + ) -> None: + """Delete a channel permission overwrite for a user or a role. + + Parameters + ---------- + channel : :obj:`~typing.Union` [ :obj:`~hikari.channels.Channel`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] + The object or ID of the channel to delete the overwrite from. + overwrite : :obj:`~typing.Union` [ :obj:`~hikari.channels.PermissionOverwrite`, :obj:`~hikari.guilds.GuildRole`, :obj:`~hikari.users.User`, :obj:`~hikari.snowflakes.Snowflake`, :obj:int ] + The ID of the entity this overwrite targets. + + Raises + ------ + :obj:`~hikari.errors.BadRequestHTTPError` + If any invalid snowflake IDs are passed; a snowflake may be invalid + due to it being outside of the range of a 64 bit integer. + :obj:`~hikari.errors.NotFoundHTTPError` + If the overwrite or channel do not exist. + :obj:`~hikari.errors.ForbiddenHTTPError` + If you lack the ``MANAGE_ROLES`` permission for that channel. + """ + await self._session.delete_channel_permission( + channel_id=str(channel.id if isinstance(channel, snowflakes.UniqueEntity) else int(channel)), + overwrite_id=str(overwrite.id if isinstance(overwrite, snowflakes.UniqueEntity) else int(overwrite)), + ) + + async def trigger_typing(self, channel: snowflakes.HashableT[_channels.Channel]) -> None: + """Trigger the typing indicator for ``10`` seconds in a channel. + + Parameters + ---------- + channel : :obj:`~typing.Union` [ :obj:`~hikari.channels.Channel`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] + The object or ID of the channel to appear to be typing in. + + Raises + ------ + :obj:`~hikari.errors.BadRequestHTTPError` + If any invalid snowflake IDs are passed; a snowflake may be invalid + due to it being outside of the range of a 64 bit integer. + :obj:`~hikari.errors.NotFoundHTTPError` + If the channel is not found. + :obj:`~hikari.errors.ForbiddenHTTPError` + If you are not able to type in the channel. + """ + await self._session.trigger_typing_indicator( + channel_id=str(channel.id if isinstance(channel, snowflakes.UniqueEntity) else int(channel)) + ) + + async def fetch_pins( + self, channel: snowflakes.HashableT[_channels.Channel] + ) -> typing.Mapping[snowflakes.Snowflake, _messages.Message]: + """Get pinned messages for a given channel. + + Parameters + ---------- + channel : :obj:`~typing.Union` [ :obj:`~hikari.channels.Channel`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] + The object or ID of the channel to get messages from. + + Returns + ------- + :obj:`~typing.Mapping` [ :obj:`~hikari.snowflakes.Snowflake`, :obj:`~hikari.messages.Message` ] + A list of message objects. + + Raises + ------ + :obj:`~hikari.errors.BadRequestHTTPError` + If any invalid snowflake IDs are passed; a snowflake may be invalid + due to it being outside of the range of a 64 bit integer. + :obj:`~hikari.errors.NotFoundHTTPError` + If the channel is not found. + :obj:`~hikari.errors.ForbiddenHTTPError` + If you are not able to see the channel. + + Note + ---- + If you are not able to see the pinned message (eg. you are missing + ``READ_MESSAGE_HISTORY`` and the pinned message is an old message), it + will not be returned. + """ + payload = await self._session.get_pinned_messages( + channel_id=str(channel.id if isinstance(channel, snowflakes.UniqueEntity) else int(channel)) + ) + return {message.id: message for message in map(_messages.Message.deserialize, payload)} + + async def pin_message( + self, channel: snowflakes.HashableT[_channels.Channel], message: snowflakes.HashableT[_messages.Message], + ) -> None: + """Add a pinned message to the channel. + + Parameters + ---------- + channel : :obj:`~typing.Union` [ :obj:`~hikari.channels.Channel`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] + The object or ID of the channel to pin a message to. + message : :obj:`~typing.Union` [ :obj:`~hikari.messages.Message`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] + The object or ID of the message to pin. + + Raises + ------ + :obj:`~hikari.errors.BadRequestHTTPError` + If any invalid snowflake IDs are passed; a snowflake may be invalid + due to it being outside of the range of a 64 bit integer. + :obj:`~hikari.errors.ForbiddenHTTPError` + If you lack the ``MANAGE_MESSAGES`` permission. + :obj:`~hikari.errors.NotFoundHTTPError` + If the message or channel do not exist. + """ + await self._session.add_pinned_channel_message( + channel_id=str(channel.id if isinstance(channel, snowflakes.UniqueEntity) else int(channel)), + message_id=str(message.id if isinstance(message, snowflakes.UniqueEntity) else int(message)), + ) + + async def unpin_message( + self, channel: snowflakes.HashableT[_channels.Channel], message: snowflakes.HashableT[_messages.Message], + ) -> None: + """Remove a pinned message from the channel. + + This will only unpin the message, not delete it. + + Parameters + ---------- + channel : :obj:`~typing.Union` [ :obj:`~hikari.channels.Channel`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] + The ID of the channel to remove a pin from. + message : :obj:`~typing.Union` [ :obj:`~hikari.messages.Message`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] + The object or ID of the message to unpin. + + Raises + ------ + :obj:`~hikari.errors.BadRequestHTTPError` + If any invalid snowflake IDs are passed; a snowflake may be invalid + due to it being outside of the range of a 64 bit integer. + :obj:`~hikari.errors.ForbiddenHTTPError` + If you lack the ``MANAGE_MESSAGES`` permission. + :obj:`~hikari.errors.NotFoundHTTPError` + If the message or channel do not exist. + """ + await self._session.delete_pinned_channel_message( + channel_id=str(channel.id if isinstance(channel, snowflakes.UniqueEntity) else int(channel)), + message_id=str(message.id if isinstance(message, snowflakes.UniqueEntity) else int(message)), + ) + + async def create_webhook( + self, + channel: snowflakes.HashableT[_channels.GuildChannel], + name: str, + *, + avatar_data: conversions.FileLikeT = ..., + reason: str = ..., + ) -> webhooks.Webhook: + """Create a webhook for a given channel. + + Parameters + ---------- + channel : :obj:`~typing.Union` [ :obj:`~hikari.channels.GuildChannel`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] + The object or ID of the channel for webhook to be created in. + name : :obj:`~str` + The webhook's name string. + avatar_data : ``hikari.internal.conversions.FileLikeT`` + If specified, the avatar image data. + reason : :obj:`~str` + If specified, the audit log reason explaining why the operation + was performed. + + Returns + ------- + :obj:`~hikari.webhooks.Webhook` + The newly created webhook object. + + Raises + ------ + :obj:`~hikari.errors.NotFoundHTTPError` + If the channel is not found. + :obj:`~hikari.errors.ForbiddenHTTPError` + If you either lack the ``MANAGE_WEBHOOKS`` permission or + can not see the given channel. + :obj:`~hikari.errors.BadRequestHTTPError` + If the avatar image is too big or the format is invalid. + If any invalid snowflake IDs are passed; a snowflake may be invalid + due to it being outside of the range of a 64 bit integer. + """ + payload = await self._session.create_webhook( + channel_id=str(channel.id if isinstance(channel, snowflakes.UniqueEntity) else int(channel)), + name=name, + avatar=conversions.get_bytes_from_resource(avatar_data) if avatar_data is not ... else ..., + reason=reason, + ) + return webhooks.Webhook.deserialize(payload) + + async def fetch_channel_webhooks( + self, channel: snowflakes.HashableT[_channels.GuildChannel] + ) -> typing.Sequence[webhooks.Webhook]: + """Get all webhooks from a given channel. + + Parameters + ---------- + channel : :obj:`~typing.Union` [ :obj:`~hikari.channels.GuildChannel`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] + The object or ID of the guild channel to get the webhooks from. + + Returns + ------- + :obj:`~typing.Sequence` [ :obj:`~hikari.webhooks.Webhook` ] + A list of webhook objects for the give channel. + + Raises + ------ + :obj:`~hikari.errors.BadRequestHTTPError` + If any invalid snowflake IDs are passed; a snowflake may be invalid + due to it being outside of the range of a 64 bit integer. + :obj:`~hikari.errors.NotFoundHTTPError` + If the channel is not found. + :obj:`~hikari.errors.ForbiddenHTTPError` + If you either lack the ``MANAGE_WEBHOOKS`` permission or + can not see the given channel. + """ + payload = await self._session.get_channel_webhooks( + channel_id=str(channel.id if isinstance(channel, snowflakes.UniqueEntity) else int(channel)) + ) + return [webhooks.Webhook.deserialize(webhook) for webhook in payload] diff --git a/hikari/clients/rest_clients/component_base.py b/hikari/clients/rest_clients/component_base.py new file mode 100644 index 0000000000..bfe5262ffb --- /dev/null +++ b/hikari/clients/rest_clients/component_base.py @@ -0,0 +1,58 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# Copyright © Nekoka.tt 2019-2020 +# +# This file is part of Hikari. +# +# Hikari is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Hikari is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Hikari. If not, see . +"""The abstract class that all REST client logic classes should inherit from.""" + +__all__ = ["BaseRESTComponent"] + +import abc +import types +import typing + +from hikari.internal import meta +from hikari.net import rest + + +class BaseRESTComponent(abc.ABC, metaclass=meta.UniqueFunctionMeta): + """An abstract class that all REST client logic classes should inherit from. + + This defines the abstract method ``__init__`` which will assign an instance + of :obj:`~hikari.net.rest.LowLevelRestfulClient` to the attribute that all + components will expect to make calls to. + """ + + @abc.abstractmethod + def __init__(self, session: rest.LowLevelRestfulClient) -> None: + self._session: rest.LowLevelRestfulClient = session + + async def __aenter__(self) -> "BaseRESTComponent": + return self + + async def __aexit__( + self, exc_type: typing.Type[BaseException], exc_val: BaseException, exc_tb: types.TracebackType + ) -> None: + await self.close() + + async def close(self) -> None: + """Shut down the REST client safely.""" + await self._session.close() + + @property + def session(self) -> rest.LowLevelRestfulClient: + """Get the :obj:`hikari.net.rest.LowLevelRestfulClient` session object.""" + return self._session diff --git a/hikari/clients/rest_clients/current_users_component.py b/hikari/clients/rest_clients/current_users_component.py new file mode 100644 index 0000000000..bcd21a67a2 --- /dev/null +++ b/hikari/clients/rest_clients/current_users_component.py @@ -0,0 +1,241 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# Copyright © Nekoka.tt 2019-2020 +# +# This file is part of Hikari. +# +# Hikari is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Hikari is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Hikari. If not, see . +"""The logic for handling requests to ``@me`` endpoints.""" + +__all__ = ["RESTCurrentUserComponent"] + +import abc +import datetime +import typing + +from hikari.clients.rest_clients import component_base +from hikari.internal import conversions +from hikari.internal import pagination +from hikari import channels as _channels +from hikari import guilds +from hikari import oauth2 +from hikari import snowflakes +from hikari import users + + +class RESTCurrentUserComponent(component_base.BaseRESTComponent, abc.ABC): # pylint: disable=W0223 + """The REST client component for handling requests to ``@me`` endpoints.""" + + async def fetch_me(self) -> users.MyUser: + """Get the current user that of the token given to the client. + + Returns + ------- + :obj:`~hikari.users.MyUser` + The current user object. + """ + payload = await self._session.get_current_user() + return users.MyUser.deserialize(payload) + + async def update_me( + self, *, username: str = ..., avatar_data: typing.Optional[conversions.FileLikeT] = ..., + ) -> users.MyUser: + """Edit the current user. + + Parameters + ---------- + username : :obj:`~str` + If specified, the new username string. + avatar_data : ``hikari.internal.conversions.FileLikeT``, optional + If specified, the new avatar image data. + If it is :obj:`~None`, the avatar is removed. + + Returns + ------- + :obj:`~hikari.users.MyUser` + The updated user object. + + Raises + ------ + :obj:`~hikari.errors.BadRequestHTTPError` + If you pass username longer than the limit (``2-32``) or an invalid image. + """ + payload = await self._session.modify_current_user( + username=username, + avatar=conversions.get_bytes_from_resource(avatar_data) if avatar_data is not ... else ..., + ) + return users.MyUser.deserialize(payload) + + async def fetch_my_connections(self) -> typing.Sequence[oauth2.OwnConnection]: + """ + Get the current user's connections. + + Note + ---- + This endpoint can be used with both ``Bearer`` and ``Bot`` tokens but + will usually return an empty list for bots (with there being some + exceptions to this, like user accounts that have been converted to bots). + + Returns + ------- + :obj:`~typing.Sequence` [ :obj:`~hikari.oauth2.OwnConnection` ] + A list of connection objects. + """ + payload = await self._session.get_current_user_connections() + return [oauth2.OwnConnection.deserialize(connection) for connection in payload] + + def fetch_my_guilds_after( + self, + *, + after: typing.Union[datetime.datetime, snowflakes.HashableT[guilds.Guild]] = 0, + limit: typing.Optional[int] = None, + ) -> typing.AsyncIterator[oauth2.OwnGuild]: + """Get an async iterator of the guilds the current user is in. + + This returns the guilds created after a given guild object/ID or from + the oldest guild. + + Parameters + ---------- + after : :obj:`~typing.Union` [ :obj:`~datetime.datetime`, :obj:`~hikari.guilds.Guild`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] + The object or ID of a guild to get guilds that were created after + it if specified, else this will start at the oldest guild. + limit : :obj:`~int` + If specified, the maximum amount of guilds that this paginator + should return. + + Example + ------- + .. code-block:: python + + async for user in client.fetch_my_guilds_after(after=9876543, limit=1231): + await client.leave_guild(guild) + + Returns + ------- + :obj:`~typing.AsyncIterator` [ :obj:`~hikari.oauth2.OwnGuild` ] + An async iterator of partial guild objects. + + Raises + ------ + :obj:`~hikari.errors.NotFoundHTTPError` + If the guild is not found. + :obj:`~hikari.errors.BadRequestHTTPError` + If any invalid snowflake IDs are passed; a snowflake may be invalid + due to it being outside of the range of a 64 bit integer. + """ + if isinstance(after, datetime.datetime): + after = str(snowflakes.Snowflake.from_datetime(after)) + else: + after = str(after.id if isinstance(after, snowflakes.UniqueEntity) else int(after)) + return pagination.pagination_handler( + deserializer=oauth2.OwnGuild.deserialize, + direction="after", + request=self._session.get_current_user_guilds, + reversing=False, + start=after, + limit=limit, + ) + + def fetch_my_guilds_before( + self, + *, + before: typing.Union[datetime.datetime, snowflakes.HashableT[guilds.Guild], None] = None, + limit: typing.Optional[int] = None, + ) -> typing.AsyncIterator[oauth2.OwnGuild]: + """Get an async iterator of the guilds the current user is in. + + This returns the guilds that were created before a given user object/ID + or from the newest guild. + + Parameters + ---------- + before : :obj:`~typing.Union` [ :obj:`~datetime.datetime`, :obj:`~hikari.guilds.Guild`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] + The object or ID of a guild to get guilds that were created + before it if specified, else this will start at the newest guild. + limit : :obj:`~int` + If specified, the maximum amount of guilds that this paginator + should return. + + Returns + ------- + :obj:`~typing.AsyncIterator` [ :obj:`~hikari.oauth2.OwnGuild` ] + An async iterator of partial guild objects. + + Raises + ------ + :obj:`~hikari.errors.NotFoundHTTPError` + If the guild is not found. + :obj:`~hikari.errors.BadRequestHTTPError` + If any invalid snowflake IDs are passed; a snowflake may be invalid + due to it being outside of the range of a 64 bit integer. + """ + if isinstance(before, datetime.datetime): + before = str(snowflakes.Snowflake.from_datetime(before)) + elif before is not None: + before = str(before.id if isinstance(before, snowflakes.UniqueEntity) else int(before)) + return pagination.pagination_handler( + deserializer=oauth2.OwnGuild.deserialize, + direction="before", + request=self._session.get_current_user_guilds, + reversing=False, + start=before, + limit=limit, + ) + + async def leave_guild(self, guild: snowflakes.HashableT[guilds.Guild]) -> None: + """Make the current user leave a given guild. + + Parameters + ---------- + guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] + The object or ID of the guild to leave. + + Raises + ------ + :obj:`~hikari.errors.NotFoundHTTPError` + If the guild is not found. + :obj:`~hikari.errors.BadRequestHTTPError` + If any invalid snowflake IDs are passed; a snowflake may be invalid + due to it being outside of the range of a 64 bit integer. + """ + await self._session.leave_guild( + guild_id=str(guild.id if isinstance(guild, snowflakes.UniqueEntity) else int(guild)) + ) + + async def create_dm_channel(self, recipient: snowflakes.HashableT[users.User]) -> _channels.DMChannel: + """Create a new DM channel with a given user. + + Parameters + ---------- + recipient : :obj:`~typing.Union` [ :obj:`~hikari.users.User`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] + The object or ID of the user to create the new DM channel with. + + Returns + ------- + :obj:`~hikari.channels.DMChannel` + The newly created DM channel object. + + Raises + ------ + :obj:`~hikari.errors.NotFoundHTTPError` + If the recipient is not found. + :obj:`~hikari.errors.BadRequestHTTPError` + If any invalid snowflake IDs are passed; a snowflake may be invalid + due to it being outside of the range of a 64 bit integer. + """ + payload = await self._session.create_dm( + recipient_id=str(recipient.id if isinstance(recipient, snowflakes.UniqueEntity) else int(recipient)) + ) + return _channels.DMChannel.deserialize(payload) diff --git a/hikari/clients/rest_clients/gateways_component.py b/hikari/clients/rest_clients/gateways_component.py new file mode 100644 index 0000000000..9a85681505 --- /dev/null +++ b/hikari/clients/rest_clients/gateways_component.py @@ -0,0 +1,59 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# Copyright © Nekoka.tt 2019-2020 +# +# This file is part of Hikari. +# +# Hikari is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Hikari is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Hikari. If not, see . +"""The logic for handling requests to gateway endpoints.""" + +__all__ = ["RESTGatewayComponent"] + +import abc + +from hikari.clients.rest_clients import component_base +from hikari import gateway_entities + + +class RESTGatewayComponent(component_base.BaseRESTComponent, abc.ABC): # pylint: disable=W0223 + """The REST client component for handling requests to gateway endpoints.""" + + async def fetch_gateway_url(self) -> str: + """Get a generic url used for establishing a Discord gateway connection. + + Returns + ------- + :obj:`~str` + A static URL to use to connect to the gateway with. + + Note + ---- + Users are expected to attempt to cache this result. + """ + return await self._session.get_gateway() + + async def fetch_gateway_bot(self) -> gateway_entities.GatewayBot: + """Get bot specific gateway information. + + Returns + ------- + :obj:`~hikari.gateway_entities.GatewayBot` + The bot specific gateway information object. + + Note + ---- + Unlike :meth:`fetch_gateway_url`, this requires a valid token to work. + """ + payload = await self._session.get_gateway_bot() + return gateway_entities.GatewayBot.deserialize(payload) diff --git a/hikari/clients/rest_clients/guilds_component.py b/hikari/clients/rest_clients/guilds_component.py new file mode 100644 index 0000000000..ebb59fbe1b --- /dev/null +++ b/hikari/clients/rest_clients/guilds_component.py @@ -0,0 +1,1939 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# Copyright © Nekoka.tt 2019-2020 +# +# This file is part of Hikari. +# +# Hikari is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Hikari is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Hikari. If not, see . +"""The logic for handling requests to guild endpoints.""" + +__all__ = ["RESTGuildComponent"] + +import abc +import datetime +import typing + +from hikari.clients.rest_clients import component_base +from hikari.internal import conversions +from hikari.internal import pagination +from hikari import audit_logs +from hikari import channels as _channels +from hikari import colors +from hikari import emojis +from hikari import guilds +from hikari import invites +from hikari import permissions as _permissions +from hikari import snowflakes +from hikari import users +from hikari import voices +from hikari import webhooks + + +def _get_member_id(member: guilds.GuildMember) -> str: + return str(member.user.id) + + +class RESTGuildComponent(component_base.BaseRESTComponent, abc.ABC): # pylint: disable=W0223 + """The REST client component for handling requests to guild endpoints.""" + + async def fetch_audit_log( + self, + guild: snowflakes.HashableT[guilds.Guild], + *, + user: snowflakes.HashableT[users.User] = ..., + action_type: typing.Union[audit_logs.AuditLogEventType, int] = ..., + limit: int = ..., + before: typing.Union[datetime.datetime, snowflakes.HashableT[audit_logs.AuditLogEntry]] = ..., + ) -> audit_logs.AuditLog: + """Get an audit log object for the given guild. + + Parameters + ---------- + guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] + The object or ID of the guild to get the audit logs for. + user : :obj:`~typing.Union` [ :obj:`~hikari.users.User`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] + If specified, the object or ID of the user to filter by. + action_type : :obj:`~typing.Union` [ :obj:`~hikari.audit_logs.AuditLogEventType`, :obj:`~int` ] + If specified, the action type to look up. Passing a raw integer + for this may lead to unexpected behaviour. + limit : :obj:`~int` + If specified, the limit to apply to the number of records. + Defaults to ``50``. Must be between ``1`` and ``100`` inclusive. + before : :obj:`~typing.Union` [ :obj:`~datetime.datetime`, :obj:`~hikari.audit_logs.AuditLogEntry`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] + If specified, the object or ID of the entry that all retrieved + entries should have occurred befor. + + Returns + ------- + :obj:`~hikari.audit_logs.AuditLog` + An audit log object. + + Raises + ------ + :obj:`~hikari.errors.BadRequestHTTPError` + If any invalid snowflake IDs are passed; a snowflake may be invalid + due to it being outside of the range of a 64 bit integer. + :obj:`~hikari.errors.ForbiddenHTTPError` + If you lack the given permissions to view an audit log. + :obj:`~hikari.errors.NotFoundHTTPError` + If the guild does not exist. + """ + if isinstance(before, datetime.datetime): + before = str(snowflakes.Snowflake.from_datetime(before)) + elif before is not ...: + before = str(before.id if isinstance(before, snowflakes.UniqueEntity) else int(before)) + payload = await self._session.get_guild_audit_log( + guild_id=str(guild.id if isinstance(guild, snowflakes.UniqueEntity) else int(guild)), + user_id=( + str(user.id if isinstance(user, snowflakes.UniqueEntity) else int(user)) if user is not ... else ... + ), + action_type=action_type, + limit=limit, + before=before, + ) + return audit_logs.AuditLog.deserialize(payload) + + def fetch_audit_log_entries_before( + self, + guild: snowflakes.HashableT[guilds.Guild], + *, + before: typing.Union[datetime.datetime, snowflakes.HashableT[audit_logs.AuditLogEntry], None] = None, + user: snowflakes.HashableT[users.User] = ..., + action_type: typing.Union[audit_logs.AuditLogEventType, int] = ..., + limit: typing.Optional[int] = None, + ) -> audit_logs.AuditLogIterator: + """Return an async iterator that retrieves a guild's audit log entries. + + This will return the audit log entries before a given entry object/ID or + from the first guild audit log entry. + + Parameters + ---------- + guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] + The ID or object of the guild to get audit log entries for + before : :obj:`~typing.Union` [ :obj:`~datetime.datetime`, :obj:`~hikari.audit_logs.AuditLogEntry`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ], optional + If specified, the ID or object of the entry or datetime to get + entries that happened before otherwise this will start from the + newest entry. + user : :obj:`~typing.Union` [ :obj:`~hikari.users.User`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] + If specified, the object or ID of the user to filter by. + action_type : :obj:`~typing.Union` [ :obj:`~hikari.audit_logs.AuditLogEventType`, :obj:`~int` ] + If specified, the action type to look up. Passing a raw integer + for this may lead to unexpected behaviour. + limit : :obj:`~int`, optional + If specified, the limit for how many entries this iterator should + return, defaults to unlimited. + + Example + ------- + .. code-block:: python + + audit_log_entries = client.fetch_audit_log_entries_before(guild, before=9876543, limit=6969) + async for entry in audit_log_entries: + if (user := audit_log_entries.users[entry.user_id]).is_bot: + await client.ban_member(guild, user) + + Note + ---- + The returned iterator has the attributes ``users``, ``members`` and + ``integrations`` which are mappings of snowflake IDs to objects for the + relevant entities that are referenced by the retrieved audit log + entries. These will be filled over time as more audit log entries are + fetched by the iterator. + + Returns + ------- + :obj:`~hikari.errors.BadRequestHTTPError` + If any invalid snowflake IDs are passed; a snowflake may be invalid + due to it being outside of the range of a 64 bit integer. + :obj:`~hikari.audit_logs.AuditLogIterator` + An async iterator of the audit log entries in a guild (from newest + to oldest). + """ + if isinstance(before, datetime.datetime): + before = str(snowflakes.Snowflake.from_datetime(before)) + elif before is not None: + before = str(before.id if isinstance(before, snowflakes.UniqueEntity) else int(before)) + return audit_logs.AuditLogIterator( + guild_id=str(guild.id if isinstance(guild, snowflakes.UniqueEntity) else int(guild)), + request=self._session.get_guild_audit_log, + before=before, + user_id=( + str(user.id if isinstance(user, snowflakes.UniqueEntity) else int(user)) if user is not ... else ... + ), + action_type=action_type, + limit=limit, + ) + + async def fetch_guild_emoji( + self, guild: snowflakes.HashableT[guilds.Guild], emoji: snowflakes.HashableT[emojis.GuildEmoji], + ) -> emojis.GuildEmoji: + """Get an updated emoji object from a specific guild. + + Parameters + ---------- + guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] + The object or ID of the guild to get the emoji from. + emoji : :obj:`~typing.Union` [ :obj:`~hikari.emojis.GuildEmoji`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] + The object or ID of the emoji to get. + + Returns + ------- + :obj:`~hikari.emojis.GuildEmoji` + A guild emoji object. + + Raises + ------ + :obj:`~hikari.errors.BadRequestHTTPError` + If any invalid snowflake IDs are passed; a snowflake may be invalid + due to it being outside of the range of a 64 bit integer. + :obj:`~hikari.errors.NotFoundHTTPError` + If either the guild or the emoji aren't found. + :obj:`~hikari.errors.ForbiddenHTTPError` + If you aren't a member of said guild. + """ + payload = await self._session.get_guild_emoji( + guild_id=str(guild.id if isinstance(guild, snowflakes.UniqueEntity) else int(guild)), + emoji_id=str(emoji.id if isinstance(emoji, snowflakes.UniqueEntity) else int(emoji)), + ) + return emojis.GuildEmoji.deserialize(payload) + + async def fetch_guild_emojis(self, guild: snowflakes.HashableT[guilds.Guild]) -> typing.Sequence[emojis.GuildEmoji]: + """Get emojis for a given guild object or ID. + + Parameters + ---------- + guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] + The object or ID of the guild to get the emojis for. + + Returns + ------- + :obj:`~typing.Sequence` [ :obj:`~hikari.emojis.GuildEmoji` ] + A list of guild emoji objects. + + Raises + ------ + :obj:`~hikari.errors.BadRequestHTTPError` + If any invalid snowflake IDs are passed; a snowflake may be invalid + due to it being outside of the range of a 64 bit integer. + :obj:`~hikari.errors.NotFoundHTTPError` + If the guild is not found. + :obj:`~hikari.errors.ForbiddenHTTPError` + If you aren't a member of the guild. + """ + payload = await self._session.list_guild_emojis( + guild_id=str(guild.id if isinstance(guild, snowflakes.UniqueEntity) else int(guild)) + ) + return [emojis.GuildEmoji.deserialize(emoji) for emoji in payload] + + async def create_guild_emoji( + self, + guild: snowflakes.HashableT[guilds.GuildRole], + name: str, + image_data: conversions.FileLikeT, + *, + roles: typing.Sequence[snowflakes.HashableT[guilds.GuildRole]] = ..., + reason: str = ..., + ) -> emojis.GuildEmoji: + """Create a new emoji for a given guild. + + Parameters + ---------- + guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.GuildRole`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] + The object or ID of the guild to create the emoji in. + name : :obj:`~str` + The new emoji's name. + image_data : ``hikari.internal.conversions.FileLikeT`` + The ``128x128`` image data. + roles : :obj:`~typing.Sequence` [ :obj:`~typing.Union` [ :obj:`~hikari.guilds.GuildRole`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] ] + If specified, a list of role objects or IDs for which the emoji + will be whitelisted. If empty, all roles are whitelisted. + reason : :obj:`~str` + If specified, the audit log reason explaining why the operation + was performed. + + Returns + ------- + :obj:`~hikari.emojis.GuildEmoji` + The newly created emoji object. + + Raises + ------ + :obj:`~ValueError` + If ``image`` is :obj:`~None`. + :obj:`~hikari.errors.NotFoundHTTPError` + If the guild is not found. + :obj:`~hikari.errors.ForbiddenHTTPError` + If you either lack the ``MANAGE_EMOJIS`` permission or aren't a + member of said guild. + :obj:`~hikari.errors.BadRequestHTTPError` + If you attempt to upload an image larger than ``256kb``, an empty + image or an invalid image format. + If any invalid snowflake IDs are passed; a snowflake may be invalid + due to it being outside of the range of a 64 bit integer. + """ + payload = await self._session.create_guild_emoji( + guild_id=str(guild.id if isinstance(guild, snowflakes.UniqueEntity) else int(guild)), + name=name, + image=conversions.get_bytes_from_resource(image_data), + roles=[str(role.id if isinstance(role, snowflakes.UniqueEntity) else int(role)) for role in roles] + if roles is not ... + else ..., + reason=reason, + ) + return emojis.GuildEmoji.deserialize(payload) + + async def update_guild_emoji( + self, + guild: snowflakes.HashableT[guilds.Guild], + emoji: snowflakes.HashableT[emojis.GuildEmoji], + *, + name: str = ..., + roles: typing.Sequence[snowflakes.HashableT[guilds.GuildRole]] = ..., + reason: str = ..., + ) -> emojis.GuildEmoji: + """Edits an emoji of a given guild. + + Parameters + ---------- + guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] + The object or ID of the guild to which the emoji to edit belongs to. + emoji : :obj:`~typing.Union` [ :obj:`~hikari.emojis.GuildEmoji`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] + The object or ID of the emoji to edit. + name : :obj:`~str` + If specified, a new emoji name string. Keep unspecified to leave the + name unchanged. + roles : :obj:`~typing.Sequence` [ :obj:`~typing.Union` [ :obj:`~hikari.guilds.GuildRole`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] ] + If specified, a list of objects or IDs for the new whitelisted + roles. Set to an empty list to whitelist all roles. + Keep unspecified to leave the same roles already set. + reason : :obj:`~str` + If specified, the audit log reason explaining why the operation + was performed. + + Returns + ------- + :obj:`~hikari.emojis.GuildEmoji` + The updated emoji object. + + Raises + ------ + :obj:`~hikari.errors.BadRequestHTTPError` + If any invalid snowflake IDs are passed; a snowflake may be invalid + due to it being outside of the range of a 64 bit integer. + :obj:`~hikari.errors.NotFoundHTTPError` + If either the guild or the emoji aren't found. + :obj:`~hikari.errors.ForbiddenHTTPError` + If you either lack the ``MANAGE_EMOJIS`` permission or are not a + member of the given guild. + """ + payload = await self._session.modify_guild_emoji( + guild_id=str(guild.id if isinstance(guild, snowflakes.UniqueEntity) else int(guild)), + emoji_id=str(emoji.id if isinstance(emoji, snowflakes.UniqueEntity) else int(emoji)), + name=name, + roles=[str(role.id if isinstance(role, snowflakes.UniqueEntity) else int(role)) for role in roles] + if roles is not ... + else ..., + reason=reason, + ) + return emojis.GuildEmoji.deserialize(payload) + + async def delete_guild_emoji( + self, guild: snowflakes.HashableT[guilds.Guild], emoji: snowflakes.HashableT[emojis.GuildEmoji], + ) -> None: + """Delete an emoji from a given guild. + + Parameters + ---------- + guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] + The object or ID of the guild to delete the emoji from. + emoji : :obj:`~typing.Union` [ :obj:`~hikari.emojis.GuildEmoji`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] + The object or ID of the guild emoji to be deleted. + + Raises + ------ + :obj:`~hikari.errors.BadRequestHTTPError` + If any invalid snowflake IDs are passed; a snowflake may be invalid + due to it being outside of the range of a 64 bit integer. + :obj:`~hikari.errors.NotFoundHTTPError` + If either the guild or the emoji aren't found. + :obj:`~hikari.errors.ForbiddenHTTPError` + If you either lack the ``MANAGE_EMOJIS`` permission or aren't a + member of said guild. + """ + await self._session.delete_guild_emoji( + guild_id=str(guild.id if isinstance(guild, snowflakes.UniqueEntity) else int(guild)), + emoji_id=str(emoji.id if isinstance(emoji, snowflakes.UniqueEntity) else int(emoji)), + ) + + async def create_guild( + self, + name: str, + *, + region: typing.Union[voices.VoiceRegion, str] = ..., + icon_data: conversions.FileLikeT = ..., + verification_level: typing.Union[guilds.GuildVerificationLevel, int] = ..., + default_message_notifications: typing.Union[guilds.GuildMessageNotificationsLevel, int] = ..., + explicit_content_filter: typing.Union[guilds.GuildExplicitContentFilterLevel, int] = ..., + roles: typing.Sequence[guilds.GuildRole] = ..., + channels: typing.Sequence[_channels.GuildChannelBuilder] = ..., + ) -> guilds.Guild: + """Create a new guild. + + Warning + ------- + Can only be used by bots in less than ``10`` guilds. + + Parameters + ---------- + name : :obj:`~str` + The name string for the new guild (``2-100`` characters). + region : :obj:`~str` + If specified, the voice region ID for new guild. You can use + :meth:`fetch_guild_voice_regions` to see which region IDs are + available. + icon_data : ``hikari.internal.conversions.FileLikeT`` + If specified, the guild icon image data. + verification_level : :obj:`~typing.Union` [ :obj:`~hikari.guilds.GuildVerificationLevel`, :obj:`~int` ] + If specified, the verification level. Passing a raw int for this + may lead to unexpected behaviour. + default_message_notifications : :obj:`~typing.Union` [ :obj:`~hikari.guilds.GuildMessageNotificationsLevel`, :obj:`~int` ] + If specified, the default notification level. Passing a raw int for + this may lead to unexpected behaviour. + explicit_content_filter : :obj:`~typing.Union` [ :obj:`~hikari.guilds.GuildExplicitContentFilterLevel`, :obj:`~int` ] + If specified, the explicit content filter. Passing a raw int for + this may lead to unexpected behaviour. + roles : :obj:`~typing.Sequence` [ :obj:`~hikari.guilds.GuildRole` ] + If specified, an array of role objects to be created alongside the + guild. First element changes the ``@everyone`` role. + channels : :obj:`~typing.Sequence` [ :obj:`~hikari.channels.GuildChannelBuilder` ] + If specified, an array of guild channel builder objects to be + created within the guild. + + Returns + ------- + :obj:`~hikari.guilds.Guild` + The newly created guild object. + + Raises + ------ + :obj:`~hikari.errors.ForbiddenHTTPError` + If you are in ``10`` or more guilds. + :obj:`~hikari.errors.BadRequestHTTPError` + If you provide unsupported fields like ``parent_id`` in channel + objects. + If any invalid snowflake IDs are passed; a snowflake may be invalid + due to it being outside of the range of a 64 bit integer. + """ + payload = await self._session.create_guild( + name=name, + region=getattr(region, "id", region), + icon=conversions.get_bytes_from_resource(icon_data), + verification_level=verification_level, + default_message_notifications=default_message_notifications, + explicit_content_filter=explicit_content_filter, + roles=[role.serialize() for role in roles] if roles is not ... else ..., + channels=[channel.serialize() for channel in channels] if channels is not ... else ..., + ) + return guilds.Guild.deserialize(payload) + + async def fetch_guild(self, guild: snowflakes.HashableT[guilds.Guild]) -> guilds.Guild: + """Get a given guild's object. + + Parameters + ---------- + guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] + The object or ID of the guild to get. + + Returns + ------- + :obj:`~hikari.guilds.Guild` + The requested guild object. + + Raises + ------ + :obj:`~hikari.errors.BadRequestHTTPError` + If any invalid snowflake IDs are passed; a snowflake may be invalid + due to it being outside of the range of a 64 bit integer. + :obj:`~hikari.errors.NotFoundHTTPError` + If the guild is not found. + :obj:`~hikari.errors.ForbiddenHTTPError` + If you don't have access to the guild. + """ + payload = await self._session.get_guild( + guild_id=str(guild.id if isinstance(guild, snowflakes.UniqueEntity) else int(guild)) + ) + return guilds.Guild.deserialize(payload) + + async def fetch_guild_preview(self, guild: snowflakes.HashableT[guilds.Guild]) -> guilds.GuildPreview: + """Get a given guild's object. + + Parameters + ---------- + guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] + The object or ID of the guild to get the preview object for. + + Returns + ------- + :obj:`~hikari.guilds.GuildPreview` + The requested guild preview object. + + Note + ---- + Unlike other guild endpoints, the bot doesn't have to be in the target + guild to get it's preview. + + Raises + ------ + :obj:`~hikari.errors.BadRequestHTTPError` + If any invalid snowflake IDs are passed; a snowflake may be invalid + due to it being outside of the range of UINT64. + :obj:`~hikari.errors.NotFoundHTTPError` + If the guild is not found or it isn't ``PUBLIC``. + """ + payload = await self._session.get_guild_preview( + guild_id=str(guild.id if isinstance(guild, snowflakes.UniqueEntity) else int(guild)) + ) + return guilds.GuildPreview.deserialize(payload) + + async def update_guild( + self, + guild: snowflakes.HashableT[guilds.Guild], + *, + name: str = ..., + region: typing.Union[voices.VoiceRegion, str] = ..., + verification_level: typing.Union[guilds.GuildVerificationLevel, int] = ..., + default_message_notifications: typing.Union[guilds.GuildMessageNotificationsLevel, int] = ..., + explicit_content_filter: typing.Union[guilds.GuildExplicitContentFilterLevel, int] = ..., + afk_channel: snowflakes.HashableT[_channels.GuildVoiceChannel] = ..., + afk_timeout: typing.Union[datetime.timedelta, int] = ..., + icon_data: conversions.FileLikeT = ..., + owner: snowflakes.HashableT[users.User] = ..., + splash_data: conversions.FileLikeT = ..., + system_channel: snowflakes.HashableT[_channels.Channel] = ..., + reason: str = ..., + ) -> guilds.Guild: + """Edit a given guild. + + Parameters + ---------- + guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] + The object or ID of the guild to be edited. + name : :obj:`~str` + If specified, the new name string for the guild (``2-100`` characters). + region : :obj:`~str` + If specified, the new voice region ID for guild. You can use + :meth:`fetch_guild_voice_regions` to see which region IDs are + available. + verification_level : :obj:`~typing.Union` [ :obj:`~hikari.guilds.GuildVerificationLevel`, :obj:`~int` ] + If specified, the new verification level. Passing a raw int for this + may lead to unexpected behaviour. + default_message_notifications : :obj:`~typing.Union` [ :obj:`~hikari.guilds.GuildMessageNotificationsLevel`, :obj:`~int` ] + If specified, the new default notification level. Passing a raw int + for this may lead to unexpected behaviour. + explicit_content_filter : :obj:`~typing.Union` [ :obj:`~hikari.guilds.GuildExplicitContentFilterLevel`, :obj:`~int` ] + If specified, the new explicit content filter. Passing a raw int for + this may lead to unexpected behaviour. + afk_channel : :obj:`~typing.Union` [ :obj:`~hikari.channels.GuildVoiceChannel`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] + If specified, the object or ID for the new AFK voice channel. + afk_timeout : :obj:`~typing.Union` [ :obj:`~datetime.timedelta`, :obj:`~int` ] + If specified, the new AFK timeout seconds timedelta. + icon_data : ``hikari.internal.conversions.FileLikeT`` + If specified, the new guild icon image file data. + owner : :obj:`~typing.Union` [ :obj:`~hikari.users.User`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] + If specified, the object or ID of the new guild owner. + splash_data : ``hikari.internal.conversions.FileLikeT`` + If specified, the new new splash image file data. + system_channel : :obj:`~typing.Union` [ :obj:`~hikari.channels.GuildVoiceChannel`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] + If specified, the object or ID of the new system channel. + reason : :obj:`~str` + If specified, the audit log reason explaining why the operation + was performed. + + Returns + ------- + :obj:`~hikari.guilds.Guild` + The edited guild object. + + Raises + ------ + :obj:`~hikari.errors.BadRequestHTTPError` + If any invalid snowflake IDs are passed; a snowflake may be invalid + due to it being outside of the range of a 64 bit integer. + :obj:`~hikari.errors.NotFoundHTTPError` + If the guild is not found. + :obj:`~hikari.errors.ForbiddenHTTPError` + If you lack the ``MANAGE_GUILD`` permission or are not in the guild. + """ + payload = await self._session.modify_guild( + guild_id=str(guild.id if isinstance(guild, snowflakes.UniqueEntity) else int(guild)), + name=name, + region=getattr(region, "id", region) if region is not ... else ..., + verification_level=verification_level, + default_message_notifications=default_message_notifications, + explicit_content_filter=explicit_content_filter, + afk_timeout=afk_timeout.total_seconds() if isinstance(afk_timeout, datetime.timedelta) else afk_timeout, + afk_channel_id=( + str(afk_channel.id if isinstance(afk_channel, snowflakes.UniqueEntity) else int(afk_channel)) + if afk_channel is not ... + else ... + ), + icon=conversions.get_bytes_from_resource(icon_data) if icon_data is not ... else ..., + owner_id=( + str(owner.id if isinstance(owner, snowflakes.UniqueEntity) else int(owner)) if owner is not ... else ... + ), + splash=conversions.get_bytes_from_resource(splash_data) if splash_data is not ... else ..., + system_channel_id=( + str(system_channel.id if isinstance(system_channel, snowflakes.UniqueEntity) else int(system_channel)) + if system_channel is not ... + else ... + ), + reason=reason, + ) + return guilds.Guild.deserialize(payload) + + async def delete_guild(self, guild: snowflakes.HashableT[guilds.Guild]) -> None: + """Permanently deletes the given guild. + + You must be owner of the guild to perform this action. + + Parameters + ---------- + guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] + The object or ID of the guild to be deleted. + + Raises + ------ + :obj:`~hikari.errors.BadRequestHTTPError` + If any invalid snowflake IDs are passed; a snowflake may be invalid + due to it being outside of the range of a 64 bit integer. + :obj:`~hikari.errors.NotFoundHTTPError` + If the guild is not found. + :obj:`~hikari.errors.ForbiddenHTTPError` + If you are not the guild owner. + """ + await self._session.delete_guild( + guild_id=str(guild.id if isinstance(guild, snowflakes.UniqueEntity) else int(guild)) + ) + + async def fetch_guild_channels( + self, guild: snowflakes.HashableT[guilds.Guild] + ) -> typing.Sequence[_channels.GuildChannel]: + """Get all the channels for a given guild. + + Parameters + ---------- + guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] + The object or ID of the guild to get the channels from. + + Returns + ------- + :obj:`~typing.Sequence` [ :obj:`~hikari.channels.GuildChannel` ] + A list of guild channel objects. + + Raises + ------ + :obj:`~hikari.errors.BadRequestHTTPError` + If any invalid snowflake IDs are passed; a snowflake may be invalid + due to it being outside of the range of a 64 bit integer. + :obj:`~hikari.errors.NotFoundHTTPError` + If the guild is not found. + :obj:`~hikari.errors.ForbiddenHTTPError` + If you are not in the guild. + """ + payload = await self._session.list_guild_channels( + guild_id=str(guild.id if isinstance(guild, snowflakes.UniqueEntity) else int(guild)) + ) + return [_channels.deserialize_channel(channel) for channel in payload] + + async def create_guild_channel( + self, + guild: snowflakes.HashableT[guilds.Guild], + name: str, + channel_type: typing.Union[_channels.ChannelType, int] = ..., + position: int = ..., + topic: str = ..., + nsfw: bool = ..., + rate_limit_per_user: typing.Union[datetime.timedelta, int] = ..., + bitrate: int = ..., + user_limit: int = ..., + permission_overwrites: typing.Sequence[_channels.PermissionOverwrite] = ..., + parent_category: snowflakes.HashableT[_channels.GuildCategory] = ..., + reason: str = ..., + ) -> _channels.GuildChannel: + """Create a channel in a given guild. + + Parameters + ---------- + guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] + The object or ID of the guild to create the channel in. + name : :obj:`~str` + If specified, the name for the channel. This must be + inclusively between ``1` and ``100`` characters in length. + channel_type: :obj:`~typing.Union` [ :obj:`~hikari.channels.ChannelType`, :obj:`~int` ] + If specified, the channel type, passing through a raw integer here + may lead to unexpected behaviour. + position : :obj:`~int` + If specified, the position to change the channel to. + topic : :obj:`~str` + If specified, the topic to set. This is only applicable to + text channels. This must be inclusively between ``0`` and ``1024`` + characters in length. + nsfw : :obj:`~bool` + If specified, whether the channel will be marked as NSFW. + Only applicable for text channels. + rate_limit_per_user : :obj:`~typing.Union` [ :obj:`~datetime.timedelta`, :obj:`~int` ] + If specified, the second time delta the user has to wait before + sending another message. This will not apply to bots, or to + members with ``MANAGE_MESSAGES`` or ``MANAGE_CHANNEL`` permissions. + This must be inclusively between ``0`` and ``21600`` seconds. + bitrate : :obj:`~int` + If specified, the bitrate in bits per second allowable for the + channel. This only applies to voice channels and must be inclusively + between ``8000`` and ``96000`` for normal servers or ``8000`` and + ``128000`` for VIP servers. + user_limit : :obj:`~int` + If specified, the max number of users to allow in a voice channel. + This must be between ``0`` and ``99`` inclusive, where + ``0`` implies no limit. + permission_overwrites : :obj:`~typing.Sequence` [ :obj:`~hikari.channels.PermissionOverwrite` ] + If specified, the list of permission overwrite objects that are + category specific to replace the existing overwrites with. + parent_category : :obj:`~typing.Union` [ :obj:`~hikari.channels.GuildCategory`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] + If specified, the object or ID of the parent category to set for + the channel. + reason : :obj:`~str` + If specified, the audit log reason explaining why the operation + was performed. + + Returns + ------- + :obj:`~hikari.channels.GuildChannel` + The newly created channel object. + + Raises + ------ + :obj:`~hikari.errors.NotFoundHTTPError` + If the guild is not found. + :obj:`~hikari.errors.ForbiddenHTTPError` + If you lack the ``MANAGE_CHANNEL`` permission or are not in the + guild. + :obj:`~hikari.errors.BadRequestHTTPError` + If you provide incorrect options for the corresponding channel type + (e.g. a ``bitrate`` for a text channel). + If any invalid snowflake IDs are passed; a snowflake may be invalid + due to it being outside of the range of a 64 bit integer. + """ + payload = await self._session.create_guild_channel( + guild_id=str(guild.id if isinstance(guild, snowflakes.UniqueEntity) else int(guild)), + name=name, + type_=channel_type, + position=position, + topic=topic, + nsfw=nsfw, + rate_limit_per_user=( + int(rate_limit_per_user.total_seconds()) + if isinstance(rate_limit_per_user, datetime.timedelta) + else rate_limit_per_user + ), + bitrate=bitrate, + user_limit=user_limit, + permission_overwrites=( + [po.serialize() for po in permission_overwrites] if permission_overwrites is not ... else ... + ), + parent_id=( + str( + parent_category.id if isinstance(parent_category, snowflakes.UniqueEntity) else int(parent_category) + ) + if parent_category is not ... + else ... + ), + reason=reason, + ) + return _channels.deserialize_channel(payload) + + async def reposition_guild_channels( + self, + guild: snowflakes.HashableT[guilds.Guild], + channel: typing.Tuple[int, snowflakes.HashableT[_channels.GuildChannel]], + *additional_channels: typing.Tuple[int, snowflakes.HashableT[_channels.GuildChannel]], + ) -> None: + """Edits the position of one or more given channels. + + Parameters + ---------- + guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] + The object or ID of the guild in which to edit the channels. + channel : :obj:`~typing.Tuple` [ :obj:`~int` , :obj:`~typing.Union` [ :obj:`~hikari.channels.GuildChannel`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] ] + The first channel to change the position of. This is a tuple of the + integer position the channel object or ID. + *additional_channels : :obj:`~typing.Tuple` [ :obj:`~int`, :obj:`~typing.Union` [ :obj:`~hikari.channels.GuildChannel`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] ] + Optional additional channels to change the position of. These must + be tuples of integer positions to change to and the channel object + or ID and the. + + Raises + ------ + :obj:`~hikari.errors.NotFoundHTTPError` + If either the guild or any of the channels aren't found. + :obj:`~hikari.errors.ForbiddenHTTPError` + If you either lack the ``MANAGE_CHANNELS`` permission or are not a + member of said guild or are not in the guild. + :obj:`~hikari.errors.BadRequestHTTPError` + If you provide anything other than the ``id`` and ``position`` + fields for the channels. + If any invalid snowflake IDs are passed; a snowflake may be invalid + due to it being outside of the range of a 64 bit integer. + """ + await self._session.modify_guild_channel_positions( + str(guild.id if isinstance(guild, snowflakes.UniqueEntity) else int(guild)), + *[ + (str(channel.id if isinstance(channel, snowflakes.UniqueEntity) else int(channel)), position) + for position, channel in [channel, *additional_channels] + ], + ) + + async def fetch_member( + self, guild: snowflakes.HashableT[guilds.Guild], user: snowflakes.HashableT[users.User], + ) -> guilds.GuildMember: + """Get a given guild member. + + Parameters + ---------- + guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] + The object or ID of the guild to get the member from. + user : :obj:`~typing.Union` [ :obj:`~hikari.users.User`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] + The object or ID of the member to get. + + Returns + ------- + :obj:`~hikari.guilds.GuildMember` + The requested member object. + + Raises + ------ + :obj:`~hikari.errors.BadRequestHTTPError` + If any invalid snowflake IDs are passed; a snowflake may be invalid + due to it being outside of the range of a 64 bit integer. + :obj:`~hikari.errors.NotFoundHTTPError` + If either the guild or the member aren't found. + :obj:`~hikari.errors.ForbiddenHTTPError` + If you don't have access to the target guild. + """ + payload = await self._session.get_guild_member( + guild_id=str(guild.id if isinstance(guild, snowflakes.UniqueEntity) else int(guild)), + user_id=str(user.id if isinstance(user, snowflakes.UniqueEntity) else int(user)), + ) + return guilds.GuildMember.deserialize(payload) + + def fetch_members_after( + self, + guild: snowflakes.HashableT[guilds.Guild], + *, + after: typing.Union[datetime.datetime, snowflakes.HashableT[users.User]] = 0, + limit: typing.Optional[int] = None, + ) -> typing.AsyncIterator[guilds.GuildMember]: + """Get an async iterator of all the members in a given guild. + + This returns the member objects with a user object/ID that was created + after the given user object/ID or from the member object or the oldest + user. + + Parameters + ---------- + guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] + The object or ID of the guild to get the members from. + limit : :obj:`~int` + If specified, the maximum number of members this iterator + should return. + after : :obj:`~typing.Union` [ :obj:`~datetime.datetime`, :obj:`~hikari.users.User`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] + The object or ID of the user this iterator should start + after if specified, else this will start at the oldest user. + + Example + ------- + .. code-block:: python + + async for user in client.fetch_members_after(guild, after=9876543, limit=1231): + if member.user.username[0] in HOIST_BLACKLIST: + await client.update_member(member, nickname="💩") + + Returns + ------- + :obj:`~typing.AsyncIterator` [ :obj:`~hikari.guilds.GuildMember` ] + An async iterator of member objects. + + Raises + ------ + :obj:`~hikari.errors.BadRequestHTTPError` + If any invalid snowflake IDs are passed; a snowflake may be invalid + due to it being outside of the range of a 64 bit integer. + :obj:`~hikari.errors.NotFoundHTTPError` + If the guild is not found. + :obj:`~hikari.errors.ForbiddenHTTPError` + If you are not in the guild. + """ + if isinstance(after, datetime.datetime): + after = str(snowflakes.Snowflake.from_datetime(after)) + else: + after = str(after.id if isinstance(after, snowflakes.UniqueEntity) else int(after)) + return pagination.pagination_handler( + guild_id=str(guild.id if isinstance(guild, snowflakes.UniqueEntity) else int(guild)), + deserializer=guilds.GuildMember.deserialize, + direction="after", + request=self._session.list_guild_members, + reversing=False, + start=after, + limit=limit, + id_getter=_get_member_id, + ) + + async def update_member( + self, + guild: snowflakes.HashableT[guilds.Guild], + user: snowflakes.HashableT[users.User], + nickname: typing.Optional[str] = ..., + roles: typing.Sequence[snowflakes.HashableT[guilds.GuildRole]] = ..., + mute: bool = ..., + deaf: bool = ..., + voice_channel: typing.Optional[snowflakes.HashableT[_channels.GuildVoiceChannel]] = ..., + reason: str = ..., + ) -> None: + """Edits a guild's member, any unspecified fields will not be changed. + + Parameters + ---------- + guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] + The object or ID of the guild to edit the member from. + user : :obj:`~typing.Union` [ :obj:`~hikari.guilds.GuildMember`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] + The object or ID of the member to edit. + nickname : :obj:`~str`, optional + If specified, the new nickname string. Setting it to :obj:`~None` + explicitly will clear the nickname. + roles : :obj:`~typing.Sequence` [ :obj:`~typing.Union` [ :obj:`~hikari.guilds.GuildRole`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] ] + If specified, a list of role IDs the member should have. + mute : :obj:`~bool` + If specified, whether the user should be muted in the voice channel + or not. + deaf : :obj:`~bool` + If specified, whether the user should be deafen in the voice + channel or not. + voice_channel : :obj:`~typing.Union` [ :obj:`~hikari.channels.GuildVoiceChannel`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ], optional + If specified, the ID of the channel to move the member to. Setting + it to :obj:`~None` explicitly will disconnect the user. + reason : :obj:`~str` + If specified, the audit log reason explaining why the operation + was performed. + + Raises + ------ + :obj:`~hikari.errors.NotFoundHTTPError` + If either the guild, user, channel or any of the roles aren't found. + :obj:`~hikari.errors.ForbiddenHTTPError` + If you lack any of the applicable permissions + (``MANAGE_NICKNAMES``, ``MANAGE_ROLES``, ``MUTE_MEMBERS``, ``DEAFEN_MEMBERS`` or ``MOVE_MEMBERS``). + Note that to move a member you must also have permission to connect + to the end channel. This will also be raised if you're not in the + guild. + :obj:`~hikari.errors.BadRequestHTTPError` + If you pass ``mute``, ``deaf`` or ``channel_id`` while the member + is not connected to a voice channel. + If any invalid snowflake IDs are passed; a snowflake may be invalid + due to it being outside of the range of a 64 bit integer. + """ + await self._session.modify_guild_member( + guild_id=str(guild.id if isinstance(guild, snowflakes.UniqueEntity) else int(guild)), + user_id=str(user.id if isinstance(user, snowflakes.UniqueEntity) else int(user)), + nick=nickname, + roles=( + [str(role.id if isinstance(role, snowflakes.UniqueEntity) else int(role)) for role in roles] + if roles is not ... + else ... + ), + mute=mute, + deaf=deaf, + channel_id=( + str(voice_channel.id if isinstance(voice_channel, snowflakes.UniqueEntity) else int(voice_channel)) + if voice_channel is not ... + else ... + ), + reason=reason, + ) + + async def update_my_member_nickname( + self, guild: snowflakes.HashableT[guilds.Guild], nickname: typing.Optional[str], *, reason: str = ..., + ) -> None: + """Edits the current user's nickname for a given guild. + + Parameters + ---------- + guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] + The object or ID of the guild you want to change the nick on. + nickname : :obj:`~str`, optional + The new nick string. Setting this to `None` clears the nickname. + reason : :obj:`~str` + If specified, the audit log reason explaining why the operation + was performed. + + Raises + ------ + :obj:`~hikari.errors.NotFoundHTTPError` + If the guild is not found. + :obj:`~hikari.errors.ForbiddenHTTPError` + If you lack the ``CHANGE_NICKNAME`` permission or are not in the + guild. + :obj:`~hikari.errors.BadRequestHTTPError` + If you provide a disallowed nickname, one that is too long, or one + that is empty. + If any invalid snowflake IDs are passed; a snowflake may be invalid + due to it being outside of the range of a 64 bit integer. + """ + await self._session.modify_current_user_nick( + guild_id=str(guild.id if isinstance(guild, snowflakes.UniqueEntity) else int(guild)), + nick=nickname, + reason=reason, + ) + + async def add_role_to_member( + self, + guild: snowflakes.HashableT[guilds.Guild], + user: snowflakes.HashableT[users.User], + role: snowflakes.HashableT[guilds.GuildRole], + *, + reason: str = ..., + ) -> None: + """Add a role to a given member. + + Parameters + ---------- + guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] + The object or ID of the guild the member belongs to. + user : :obj:`~typing.Union` [ :obj:`~hikari.users.User`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] + The object or ID of the member you want to add the role to. + role : :obj:`~typing.Union` [ :obj:`~hikari.guilds.GuildRole`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] + The object or ID of the role you want to add. + reason : :obj:`~str` + If specified, the audit log reason explaining why the operation + was performed. + + Raises + ------ + :obj:`~hikari.errors.BadRequestHTTPError` + If any invalid snowflake IDs are passed; a snowflake may be invalid + due to it being outside of the range of a 64 bit integer. + :obj:`~hikari.errors.NotFoundHTTPError` + If either the guild, member or role aren't found. + :obj:`~hikari.errors.ForbiddenHTTPError` + If you lack the ``MANAGE_ROLES`` permission or are not in the guild. + """ + await self._session.add_guild_member_role( + guild_id=str(guild.id if isinstance(guild, snowflakes.UniqueEntity) else int(guild)), + user_id=str(user.id if isinstance(user, snowflakes.UniqueEntity) else int(user)), + role_id=str(role.id if isinstance(role, snowflakes.UniqueEntity) else int(role)), + reason=reason, + ) + + async def remove_role_from_member( + self, + guild: snowflakes.HashableT[guilds.Guild], + user: snowflakes.HashableT[users.User], + role: snowflakes.HashableT[guilds.GuildRole], + *, + reason: str = ..., + ) -> None: + """Remove a role from a given member. + + Parameters + ---------- + guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] + The object or ID of the guild the member belongs to. + user : :obj:`~typing.Union` [ :obj:`~hikari.users.User`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] + The object or ID of the member you want to remove the role from. + role : :obj:`~typing.Union` [ :obj:`~hikari.guilds.GuildRole`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] + The object or ID of the role you want to remove. + reason : :obj:`~str` + If specified, the audit log reason explaining why the operation + was performed. + + Raises + ------ + :obj:`~hikari.errors.BadRequestHTTPError` + If any invalid snowflake IDs are passed; a snowflake may be invalid + due to it being outside of the range of a 64 bit integer. + :obj:`~hikari.errors.NotFoundHTTPError` + If either the guild, member or role aren't found. + :obj:`~hikari.errors.ForbiddenHTTPError` + If you lack the ``MANAGE_ROLES`` permission or are not in the guild. + """ + await self._session.remove_guild_member_role( + guild_id=str(guild.id if isinstance(guild, snowflakes.UniqueEntity) else int(guild)), + user_id=str(user.id if isinstance(user, snowflakes.UniqueEntity) else int(user)), + role_id=str(role.id if isinstance(role, snowflakes.UniqueEntity) else int(role)), + reason=reason, + ) + + async def kick_member( + self, guild: snowflakes.HashableT[guilds.Guild], user: snowflakes.HashableT[users.User], *, reason: str = ..., + ) -> None: + """Kicks a user from a given guild. + + Parameters + ---------- + guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] + The object or ID of the guild the member belongs to. + user : :obj:`~typing.Union` [ :obj:`~hikari.users.User`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] + The object or ID of the member you want to kick. + reason : :obj:`~str` + If specified, the audit log reason explaining why the operation + was performed. + + Raises + ------ + :obj:`~hikari.errors.BadRequestHTTPError` + If any invalid snowflake IDs are passed; a snowflake may be invalid + due to it being outside of the range of a 64 bit integer. + :obj:`~hikari.errors.NotFoundHTTPError` + If either the guild or member aren't found. + :obj:`~hikari.errors.ForbiddenHTTPError` + If you lack the ``KICK_MEMBERS`` permission or are not in the guild. + """ + await self._session.remove_guild_member( + guild_id=str(guild.id if isinstance(guild, snowflakes.UniqueEntity) else int(guild)), + user_id=str(user.id if isinstance(user, snowflakes.UniqueEntity) else int(user)), + reason=reason, + ) + + async def fetch_ban( + self, guild: snowflakes.HashableT[guilds.Guild], user: snowflakes.HashableT[users.User], + ) -> guilds.GuildMemberBan: + """Get a ban from a given guild. + + Parameters + ---------- + guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] + The object or ID of the guild you want to get the ban from. + user : :obj:`~typing.Union` [ :obj:`~hikari.users.User`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] + The object or ID of the user to get the ban information for. + + Returns + ------- + :obj:`~hikari.guilds.GuildMemberBan` + A ban object for the requested user. + + Raises + ------ + :obj:`~hikari.errors.BadRequestHTTPError` + If any invalid snowflake IDs are passed; a snowflake may be invalid + due to it being outside of the range of a 64 bit integer. + :obj:`~hikari.errors.NotFoundHTTPError` + If either the guild or the user aren't found, or if the user is not + banned. + :obj:`~hikari.errors.ForbiddenHTTPError` + If you lack the ``BAN_MEMBERS`` permission or are not in the guild. + """ + payload = await self._session.get_guild_ban( + guild_id=str(guild.id if isinstance(guild, snowflakes.UniqueEntity) else int(guild)), + user_id=str(user.id if isinstance(user, snowflakes.UniqueEntity) else int(user)), + ) + return guilds.GuildMemberBan.deserialize(payload) + + async def fetch_bans(self, guild: snowflakes.HashableT[guilds.Guild],) -> typing.Sequence[guilds.GuildMemberBan]: + """Get the bans for a given guild. + + Parameters + ---------- + guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] + The object or ID of the guild you want to get the bans from. + + Returns + ------- + :obj:`~typing.Sequence` [ :obj:`~hikari.guilds.GuildMemberBan` ] + A list of ban objects. + + Raises + ------ + :obj:`~hikari.errors.BadRequestHTTPError` + If any invalid snowflake IDs are passed; a snowflake may be invalid + due to it being outside of the range of a 64 bit integer. + :obj:`~hikari.errors.NotFoundHTTPError` + If the guild is not found. + :obj:`~hikari.errors.ForbiddenHTTPError` + If you lack the ``BAN_MEMBERS`` permission or are not in the guild. + """ + payload = await self._session.get_guild_bans( + guild_id=str(guild.id if isinstance(guild, snowflakes.UniqueEntity) else int(guild)) + ) + return [guilds.GuildMemberBan.deserialize(ban) for ban in payload] + + async def ban_member( + self, + guild: snowflakes.HashableT[guilds.Guild], + user: snowflakes.HashableT[users.User], + *, + delete_message_days: typing.Union[datetime.timedelta, int] = ..., + reason: str = ..., + ) -> None: + """Bans a user from a given guild. + + Parameters + ---------- + guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] + The object or ID of the guild the member belongs to. + user : :obj:`~typing.Union` [ :obj:`~hikari.users.User`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] + The object or ID of the member you want to ban. + delete_message_days : :obj:`~typing.Union` [ :obj:`~datetime.timedelta`, :obj:`~int` ] + If specified, the tim delta of how many days of messages from the + user should be removed. + reason : :obj:`~str` + If specified, the audit log reason explaining why the operation + was performed. + + Raises + ------ + :obj:`~hikari.errors.BadRequestHTTPError` + If any invalid snowflake IDs are passed; a snowflake may be invalid + due to it being outside of the range of a 64 bit integer. + :obj:`~hikari.errors.NotFoundHTTPError` + If either the guild or member aren't found. + :obj:`~hikari.errors.ForbiddenHTTPError` + If you lack the ``BAN_MEMBERS`` permission or are not in the guild. + """ + await self._session.create_guild_ban( + guild_id=str(guild.id if isinstance(guild, snowflakes.UniqueEntity) else int(guild)), + user_id=str(user.id if isinstance(user, snowflakes.UniqueEntity) else int(user)), + delete_message_days=getattr(delete_message_days, "days", delete_message_days), + reason=reason, + ) + + async def unban_member( + self, guild: snowflakes.HashableT[guilds.Guild], user: snowflakes.HashableT[users.User], *, reason: str = ..., + ) -> None: + """Un-bans a user from a given guild. + + Parameters + ---------- + guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] + The object or ID of the guild to un-ban the user from. + user : :obj:`~typing.Union` [ :obj:`~hikari.users.User`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] + The ID of the user you want to un-ban. + reason : :obj:`~str` + If specified, the audit log reason explaining why the operation + was performed. + + Raises + ------ + :obj:`~hikari.errors.BadRequestHTTPError` + If any invalid snowflake IDs are passed; a snowflake may be invalid + due to it being outside of the range of a 64 bit integer. + :obj:`~hikari.errors.NotFoundHTTPError` + If either the guild or member aren't found, or the member is not + banned. + :obj:`~hikari.errors.ForbiddenHTTPError` + If you lack the ``BAN_MEMBERS`` permission or are not a in the + guild. + """ + await self._session.remove_guild_ban( + guild_id=str(guild.id if isinstance(guild, snowflakes.UniqueEntity) else int(guild)), + user_id=str(user.id if isinstance(user, snowflakes.UniqueEntity) else int(user)), + reason=reason, + ) + + async def fetch_roles( + self, guild: snowflakes.HashableT[guilds.Guild], + ) -> typing.Mapping[snowflakes.Snowflake, guilds.GuildRole]: + """Get the roles for a given guild. + + Parameters + ---------- + guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] + The object or ID of the guild you want to get the roles from. + + Returns + ------- + :obj:`~typing.Mapping` [ :obj:`~hikari.snowflakes.Snowflake`, :obj:`~hikari.guilds.GuildRole` ] + A list of role objects. + + Raises + ------ + :obj:`~hikari.errors.BadRequestHTTPError` + If any invalid snowflake IDs are passed; a snowflake may be invalid + due to it being outside of the range of a 64 bit integer. + :obj:`~hikari.errors.NotFoundHTTPError` + If the guild is not found. + :obj:`~hikari.errors.ForbiddenHTTPError` + If you're not in the guild. + """ + payload = await self._session.get_guild_roles( + guild_id=str(guild.id if isinstance(guild, snowflakes.UniqueEntity) else int(guild)) + ) + return {role.id: role for role in map(guilds.GuildRole.deserialize, payload)} + + async def create_role( + self, + guild: snowflakes.HashableT[guilds.Guild], + *, + name: str = ..., + permissions: typing.Union[_permissions.Permission, int] = ..., + color: typing.Union[colors.Color, int] = ..., + hoist: bool = ..., + mentionable: bool = ..., + reason: str = ..., + ) -> guilds.GuildRole: + """Create a new role for a given guild. + + Parameters + ---------- + guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] + The object or ID of the guild you want to create the role on. + name : :obj:`~str` + If specified, the new role name string. + permissions : :obj:`~typing.Union` [ :obj:`~hikari.permissions.Permission`, :obj:`~int` ] + If specified, the permissions integer for the role, passing a raw + integer rather than the int flag may lead to unexpected results. + color : :obj:`~typing.Union` [ :obj:`~hikari.colors.Color`, :obj:`~int` ] + If specified, the color for the role. + hoist : :obj:`~bool` + If specified, whether the role will be hoisted. + mentionable : :obj:`~bool` + If specified, whether the role will be able to be mentioned by any + user. + reason : :obj:`~str` + If specified, the audit log reason explaining why the operation + was performed. + + Returns + ------- + :obj:`~hikari.guilds.GuildRole` + The newly created role object. + + Raises + ------ + :obj:`~hikari.errors.NotFoundHTTPError` + If the guild is not found. + :obj:`~hikari.errors.ForbiddenHTTPError` + If you lack the ``MANAGE_ROLES`` permission or you're not in the + guild. + :obj:`~hikari.errors.BadRequestHTTPError` + If you provide invalid values for the role attributes. + If any invalid snowflake IDs are passed; a snowflake may be invalid + due to it being outside of the range of a 64 bit integer. + """ + payload = await self._session.create_guild_role( + guild_id=str(guild.id if isinstance(guild, snowflakes.UniqueEntity) else int(guild)), + name=name, + permissions=permissions, + color=color, + hoist=hoist, + mentionable=mentionable, + reason=reason, + ) + return guilds.GuildRole.deserialize(payload) + + async def reposition_roles( + self, + guild: snowflakes.HashableT[guilds.Guild], + role: typing.Tuple[int, snowflakes.HashableT[guilds.GuildRole]], + *additional_roles: typing.Tuple[int, snowflakes.HashableT[guilds.GuildRole]], + ) -> typing.Sequence[guilds.GuildRole]: + """Edits the position of two or more roles in a given guild. + + Parameters + ---------- + guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] + The ID of the guild the roles belong to. + role : :obj:`~typing.Tuple` [ :obj:`~int`, :obj:`~typing.Union` [ :obj:`~hikari.guilds.GuildRole`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] ] + The first role to move. This is a tuple of the integer position and + the role object or ID. + *additional_roles : :obj:`~typing.Tuple` [ :obj:`~int`, :obj:`~typing.Union` [ :obj:`~hikari.guilds.GuildRole`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] ] + Optional extra roles to move. These must be tuples of the integer + position and the role object or ID. + + Returns + ------- + :obj:`~typing.Sequence` [ :obj:`~hikari.guilds.GuildRole` ] + A list of all the guild roles. + + Raises + ------ + :obj:`~hikari.errors.NotFoundHTTPError` + If either the guild or any of the roles aren't found. + :obj:`~hikari.errors.ForbiddenHTTPError` + If you lack the ``MANAGE_ROLES`` permission or you're not in the + guild. + :obj:`~hikari.errors.BadRequestHTTPError` + If you provide invalid values for the `position` fields. + If any invalid snowflake IDs are passed; a snowflake may be invalid + due to it being outside of the range of a 64 bit integer. + """ + payload = await self._session.modify_guild_role_positions( + str(guild.id if isinstance(guild, snowflakes.UniqueEntity) else int(guild)), + *[ + (str(channel.id if isinstance(channel, snowflakes.UniqueEntity) else int(channel)), position) + for position, channel in [role, *additional_roles] + ], + ) + return [guilds.GuildRole.deserialize(role) for role in payload] + + async def update_role( + self, + guild: snowflakes.HashableT[guilds.Guild], + role: snowflakes.HashableT[guilds.GuildRole], + *, + name: str = ..., + permissions: typing.Union[_permissions.Permission, int] = ..., + color: typing.Union[colors.Color, int] = ..., + hoist: bool = ..., + mentionable: bool = ..., + reason: str = ..., + ) -> guilds.GuildRole: + """Edits a role in a given guild. + + Parameters + ---------- + guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] + The object or ID of the guild the role belong to. + role : :obj:`~typing.Union` [ :obj:`~hikari.guilds.GuildRole`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] + The object or ID of the role you want to edit. + name : :obj:`~str` + If specified, the new role's name string. + permissions : :obj:`~typing.Union` [ :obj:`~hikari.permissions.Permission`, :obj:`~int` ] + If specified, the new permissions integer for the role, passing a + raw integer for this may lead to unexpected behaviour. + color : :obj:`~typing.Union` [ :obj:`~hikari.colors.Color`, :obj:`~int` ] + If specified, the new color for the new role passing a raw integer + for this may lead to unexpected behaviour. + hoist : :obj:`~bool` + If specified, whether the role should hoist or not. + mentionable : :obj:`~bool` + If specified, whether the role should be mentionable or not. + reason : :obj:`~str` + If specified, the audit log reason explaining why the operation + was performed. + + Returns + ------- + :obj:`~hikari.guilds.GuildRole` + The edited role object. + + Raises + ------ + :obj:`~hikari.errors.NotFoundHTTPError` + If either the guild or role aren't found. + :obj:`~hikari.errors.ForbiddenHTTPError` + If you lack the ``MANAGE_ROLES`` permission or you're not in the + guild. + :obj:`~hikari.errors.BadRequestHTTPError` + If you provide invalid values for the role attributes. + If any invalid snowflake IDs are passed; a snowflake may be invalid + due to it being outside of the range of a 64 bit integer. + """ + payload = await self._session.modify_guild_role( + guild_id=str(guild.id if isinstance(guild, snowflakes.UniqueEntity) else int(guild)), + role_id=str(role.id if isinstance(role, snowflakes.UniqueEntity) else int(role)), + name=name, + permissions=permissions, + color=color, + hoist=hoist, + mentionable=mentionable, + reason=reason, + ) + return guilds.GuildRole.deserialize(payload) + + async def delete_role( + self, guild: snowflakes.HashableT[guilds.Guild], role: snowflakes.HashableT[guilds.GuildRole], + ) -> None: + """Delete a role from a given guild. + + Parameters + ---------- + guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] + The object or ID of the guild you want to remove the role from. + role : :obj:`~typing.Union` [ :obj:`~hikari.guilds.GuildRole`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] + The object or ID of the role you want to delete. + + Raises + ------ + :obj:`~hikari.errors.BadRequestHTTPError` + If any invalid snowflake IDs are passed; a snowflake may be invalid + due to it being outside of the range of a 64 bit integer. + :obj:`~hikari.errors.NotFoundHTTPError` + If either the guild or the role aren't found. + :obj:`~hikari.errors.ForbiddenHTTPError` + If you lack the ``MANAGE_ROLES`` permission or are not in the guild. + """ + await self._session.delete_guild_role( + guild_id=str(guild.id if isinstance(guild, snowflakes.UniqueEntity) else int(guild)), + role_id=str(role.id if isinstance(role, snowflakes.UniqueEntity) else int(role)), + ) + + async def estimate_guild_prune_count( + self, guild: snowflakes.HashableT[guilds.Guild], days: typing.Union[datetime.timedelta, int], + ) -> int: + """Get the estimated prune count for a given guild. + + Parameters + ---------- + guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] + The object or ID of the guild you want to get the count for. + days : :obj:`~typing.Union` [ :obj:`~datetime.timedelta`, :obj:`~int` ] + The time delta of days to count prune for (at least ``1``). + + Returns + ------- + :obj:`~int` + The number of members estimated to be pruned. + + Raises + ------ + :obj:`~hikari.errors.NotFoundHTTPError` + If the guild is not found. + :obj:`~hikari.errors.ForbiddenHTTPError` + If you lack the ``KICK_MEMBERS`` or you are not in the guild. + :obj:`~hikari.errors.BadRequestHTTPError` + If you pass an invalid amount of days. + If any invalid snowflake IDs are passed; a snowflake may be invalid + due to it being outside of the range of a 64 bit integer. + """ + return await self._session.get_guild_prune_count( + guild_id=str(guild.id if isinstance(guild, snowflakes.UniqueEntity) else int(guild)), + days=getattr(days, "days", days), + ) + + async def begin_guild_prune( + self, + guild: snowflakes.HashableT[guilds.Guild], + days: typing.Union[datetime.timedelta, int], + *, + compute_prune_count: bool = ..., + reason: str = ..., + ) -> int: + """Prunes members of a given guild based on the number of inactive days. + + Parameters + ---------- + guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] + The object or ID of the guild you want to prune member of. + days : :obj:`~typing.Union` [ :obj:`~datetime.timedelta`, :obj:`~int` ] + The time delta of inactivity days you want to use as filter. + compute_prune_count : :obj:`~bool` + Whether a count of pruned members is returned or not. + Discouraged for large guilds out of politeness. + reason : :obj:`~str` + If specified, the audit log reason explaining why the operation + was performed. + + Returns + ------- + :obj:`~int`, optional + The number of members who were kicked if ``compute_prune_count`` + is :obj:`~True`, else :obj:`~None`. + + Raises + ------ + :obj:`~hikari.errors.NotFoundHTTPError` + If the guild is not found: + :obj:`~hikari.errors.ForbiddenHTTPError` + If you lack the ``KICK_MEMBER`` permission or are not in the guild. + :obj:`~hikari.errors.BadRequestHTTPError` + If you provide invalid values for the ``days`` or + ``compute_prune_count`` fields. + If any invalid snowflake IDs are passed; a snowflake may be invalid + due to it being outside of the range of a 64 bit integer. + """ + return await self._session.begin_guild_prune( + guild_id=str(guild.id if isinstance(guild, snowflakes.UniqueEntity) else int(guild)), + days=getattr(days, "days", days), + compute_prune_count=compute_prune_count, + reason=reason, + ) + + async def fetch_guild_voice_regions( + self, guild: snowflakes.HashableT[guilds.Guild], + ) -> typing.Sequence[voices.VoiceRegion]: + """Get the voice regions for a given guild. + + Parameters + ---------- + guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] + The object or ID of the guild to get the voice regions for. + + Returns + ------- + :obj:`~typing.Sequence` [ :obj:`~hikari.voices.VoiceRegion` ] + A list of voice region objects. + + Raises + ------ + :obj:`~hikari.errors.BadRequestHTTPError` + If any invalid snowflake IDs are passed; a snowflake may be invalid + due to it being outside of the range of a 64 bit integer. + :obj:`~hikari.errors.NotFoundHTTPError` + If the guild is not found. + :obj:`~hikari.errors.ForbiddenHTTPError` + If you are not in the guild. + """ + payload = await self._session.get_guild_voice_regions( + guild_id=str(guild.id if isinstance(guild, snowflakes.UniqueEntity) else int(guild)) + ) + return [voices.VoiceRegion.deserialize(region) for region in payload] + + async def fetch_guild_invites( + self, guild: snowflakes.HashableT[guilds.Guild], + ) -> typing.Sequence[invites.InviteWithMetadata]: + """Get the invites for a given guild. + + Parameters + ---------- + guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] + The object or ID of the guild to get the invites for. + + Returns + ------- + :obj:`~typing.Sequence` [ :obj:`~hikari.invites.InviteWithMetadata` ] + A list of invite objects (with metadata). + + Raises + ------ + :obj:`~hikari.errors.BadRequestHTTPError` + If any invalid snowflake IDs are passed; a snowflake may be invalid + due to it being outside of the range of a 64 bit integer. + :obj:`~hikari.errors.NotFoundHTTPError` + If the guild is not found. + :obj:`~hikari.errors.ForbiddenHTTPError` + If you lack the ``MANAGE_GUILD`` permission or are not in the guild. + """ + payload = await self._session.get_guild_invites( + guild_id=str(guild.id if isinstance(guild, snowflakes.UniqueEntity) else int(guild)) + ) + return [invites.InviteWithMetadata.deserialize(invite) for invite in payload] + + async def fetch_integrations( + self, guild: snowflakes.HashableT[guilds.Guild] + ) -> typing.Sequence[guilds.GuildIntegration]: + """Get the integrations for a given guild. + + Parameters + ---------- + guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] + The object or ID of the guild to get the integrations for. + + Returns + ------- + :obj:`~typing.Sequence` [ :obj:`~hikari.guilds.GuildIntegration` ] + A list of integration objects. + + Raises + ------ + :obj:`~hikari.errors.BadRequestHTTPError` + If any invalid snowflake IDs are passed; a snowflake may be invalid + due to it being outside of the range of a 64 bit integer. + :obj:`~hikari.errors.NotFoundHTTPError` + If the guild is not found. + :obj:`~hikari.errors.ForbiddenHTTPError` + If you lack the ``MANAGE_GUILD`` permission or are not in the guild. + """ + payload = await self._session.get_guild_integrations( + guild_id=str(guild.id if isinstance(guild, snowflakes.UniqueEntity) else int(guild)) + ) + return [guilds.GuildIntegration.deserialize(integration) for integration in payload] + + async def update_integration( + self, + guild: snowflakes.HashableT[guilds.Guild], + integration: snowflakes.HashableT[guilds.GuildIntegration], + *, + expire_behaviour: typing.Union[guilds.IntegrationExpireBehaviour, int] = ..., + expire_grace_period: typing.Union[datetime.timedelta, int] = ..., + enable_emojis: bool = ..., + reason: str = ..., + ) -> None: + """Edits an integrations for a given guild. + + Parameters + ---------- + guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] + The object or ID of the guild to which the integration belongs to. + integration : :obj:`~typing.Union` [ :obj:`~hikari.guilds.GuildIntegration`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] + The object or ID of the integration to update. + expire_behaviour : :obj:`~typing.Union` [ :obj:`~hikari.guilds.IntegrationExpireBehaviour`, :obj:`~int` ] + If specified, the behaviour for when an integration subscription + expires (passing a raw integer for this may lead to unexpected + behaviour). + expire_grace_period : :obj:`~typing.Union` [ :obj:`~datetime.timedelta`, :obj:`~int` ] + If specified, time time delta of how many days the integration will + ignore lapsed subscriptions for. + enable_emojis : :obj:`~bool` + If specified, whether emojis should be synced for this integration. + reason : :obj:`~str` + If specified, the audit log reason explaining why the operation + was performed. + + Raises + ------ + :obj:`~hikari.errors.BadRequestHTTPError` + If any invalid snowflake IDs are passed; a snowflake may be invalid + due to it being outside of the range of a 64 bit integer. + :obj:`~hikari.errors.NotFoundHTTPError` + If either the guild or the integration aren't found. + :obj:`~hikari.errors.ForbiddenHTTPError` + If you lack the ``MANAGE_GUILD`` permission or are not in the guild. + """ + await self._session.modify_guild_integration( + guild_id=str(guild.id if isinstance(guild, snowflakes.UniqueEntity) else int(guild)), + integration_id=str( + integration.id if isinstance(integration, snowflakes.UniqueEntity) else int(integration) + ), + expire_behaviour=expire_behaviour, + expire_grace_period=getattr(expire_grace_period, "days", expire_grace_period), + enable_emojis=enable_emojis, + reason=reason, + ) + + async def delete_integration( + self, + guild: snowflakes.HashableT[guilds.Guild], + integration: snowflakes.HashableT[guilds.GuildIntegration], + *, + reason: str = ..., + ) -> None: + """Delete an integration for the given guild. + + Parameters + ---------- + guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] + The object or ID of the guild to which the integration belongs to. + integration : :obj:`~typing.Union` [ :obj:`~hikari.guilds.GuildIntegration`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] + The object or ID of the integration to delete. + reason : :obj:`~str` + If specified, the audit log reason explaining why the operation + was performed. + + Raises + ------ + :obj:`~hikari.errors.BadRequestHTTPError` + If any invalid snowflake IDs are passed; a snowflake may be invalid + due to it being outside of the range of a 64 bit integer. + :obj:`~hikari.errors.NotFoundHTTPError` + If either the guild or the integration aren't found. + :obj:`~hikari.errors.ForbiddenHTTPError` + If you lack the `MANAGE_GUILD` permission or are not in the guild. + """ + await self._session.delete_guild_integration( + guild_id=str(guild.id if isinstance(guild, snowflakes.UniqueEntity) else int(guild)), + integration_id=str( + integration.id if isinstance(integration, snowflakes.UniqueEntity) else int(integration) + ), + reason=reason, + ) + + async def sync_guild_integration( + self, guild: snowflakes.HashableT[guilds.Guild], integration: snowflakes.HashableT[guilds.GuildIntegration], + ) -> None: + """Sync the given integration's subscribers/emojis. + + Parameters + ---------- + guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] + The object or ID of the guild to which the integration belongs to. + integration : :obj:`~typing.Union` [ :obj:`~hikari.guilds.GuildIntegration`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] + The ID of the integration to sync. + + Raises + ------ + :obj:`~hikari.errors.BadRequestHTTPError` + If any invalid snowflake IDs are passed; a snowflake may be invalid + due to it being outside of the range of a 64 bit integer. + :obj:`~hikari.errors.NotFoundHTTPError` + If either the guild or the integration aren't found. + :obj:`~hikari.errors.ForbiddenHTTPError` + If you lack the ``MANAGE_GUILD`` permission or are not in the guild. + """ + await self._session.sync_guild_integration( + guild_id=str(guild.id if isinstance(guild, snowflakes.UniqueEntity) else int(guild)), + integration_id=str( + integration.id if isinstance(integration, snowflakes.UniqueEntity) else int(integration) + ), + ) + + async def fetch_guild_embed(self, guild: snowflakes.HashableT[guilds.Guild],) -> guilds.GuildEmbed: + """Get the embed for a given guild. + + Parameters + ---------- + guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] + The object or ID of the guild to get the embed for. + + Returns + ------- + :obj:`~hikari.guilds.GuildEmbed` + A guild embed object. + + Raises + ------ + :obj:`~hikari.errors.BadRequestHTTPError` + If any invalid snowflake IDs are passed; a snowflake may be invalid + due to it being outside of the range of a 64 bit integer. + :obj:`~hikari.errors.NotFoundHTTPError` + If the guild is not found. + :obj:`~hikari.errors.ForbiddenHTTPError` + If you either lack the ``MANAGE_GUILD`` permission or are not in + the guild. + """ + payload = await self._session.get_guild_embed( + guild_id=str(guild.id if isinstance(guild, snowflakes.UniqueEntity) else int(guild)) + ) + return guilds.GuildEmbed.deserialize(payload) + + async def update_guild_embed( + self, + guild: snowflakes.HashableT[guilds.Guild], + *, + channel: snowflakes.HashableT[_channels.GuildChannel] = ..., + enabled: bool = ..., + reason: str = ..., + ) -> guilds.GuildEmbed: + """Edits the embed for a given guild. + + Parameters + ---------- + guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] + The object or ID of the guild to edit the embed for. + channel : :obj:`~typing.Union` [ :obj:`~hikari.channels.GuildChannel`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ], optional + If specified, the object or ID of the channel that this embed's + invite should target. Set to :obj:`~None` to disable invites for this + embed. + enabled : :obj:`~bool` + If specified, whether this embed should be enabled. + reason : :obj:`~str` + If specified, the audit log reason explaining why the operation + was performed. + + Returns + ------- + :obj:`~hikari.guilds.GuildEmbed` + The updated embed object. + + Raises + ------ + :obj:`~hikari.errors.BadRequestHTTPError` + If any invalid snowflake IDs are passed; a snowflake may be invalid + due to it being outside of the range of a 64 bit integer. + :obj:`~hikari.errors.NotFoundHTTPError` + If the guild is not found. + :obj:`~hikari.errors.ForbiddenHTTPError` + If you either lack the ``MANAGE_GUILD`` permission or are not in + the guild. + """ + payload = await self._session.modify_guild_embed( + guild_id=str(guild.id if isinstance(guild, snowflakes.UniqueEntity) else int(guild)), + channel_id=( + str(channel.id if isinstance(channel, snowflakes.UniqueEntity) else int(channel)) + if channel is not ... + else ... + ), + enabled=enabled, + reason=reason, + ) + return guilds.GuildEmbed.deserialize(payload) + + async def fetch_guild_vanity_url(self, guild: snowflakes.HashableT[guilds.Guild],) -> invites.VanityUrl: + """ + Get the vanity URL for a given guild. + + Parameters + ---------- + guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] + The object or ID of the guild to get the vanity URL for. + + Returns + ------- + :obj:`~hikari.invites.VanityUrl` + A partial invite object containing the vanity URL in the ``code`` + field. + + Raises + ------ + :obj:`~hikari.errors.BadRequestHTTPError` + If any invalid snowflake IDs are passed; a snowflake may be invalid + due to it being outside of the range of a 64 bit integer. + :obj:`~hikari.errors.NotFoundHTTPError` + If the guild is not found. + :obj:`~hikari.errors.ForbiddenHTTPError` + If you either lack the ``MANAGE_GUILD`` permission or are not in + the guild. + """ + payload = await self._session.get_guild_vanity_url( + guild_id=str(guild.id if isinstance(guild, snowflakes.UniqueEntity) else int(guild)) + ) + return invites.VanityUrl.deserialize(payload) + + def format_guild_widget_image(self, guild: snowflakes.HashableT[guilds.Guild], *, style: str = ...) -> str: + """Get the URL for a guild widget. + + Parameters + ---------- + guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] + The object or ID of the guild to form the widget. + style : :obj:`~str` + If specified, the syle of the widget. + + Returns + ------- + :obj:`~str` + A URL to retrieve a PNG widget for your guild. + + Note + ---- + This does not actually make any form of request, and shouldn't be + awaited. Thus, it doesn't have rate limits either. + + Warning + ------- + The guild must have the widget enabled in the guild settings for this + to be valid. + """ + return self._session.get_guild_widget_image_url( + guild_id=str(guild.id if isinstance(guild, snowflakes.UniqueEntity) else int(guild)), style=style + ) + + async def fetch_guild_webhooks( + self, guild: snowflakes.HashableT[guilds.Guild] + ) -> typing.Sequence[webhooks.Webhook]: + """Get all webhooks for a given guild. + + Parameters + ---------- + guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] + The object or ID for the guild to get the webhooks from. + + Returns + ------- + :obj:`~typing.Sequence` [ :obj:`~hikari.webhooks.Webhook` ] + A list of webhook objects for the given guild. + + Raises + ------ + :obj:`~hikari.errors.BadRequestHTTPError` + If any invalid snowflake IDs are passed; a snowflake may be invalid + due to it being outside of the range of a 64 bit integer. + :obj:`~hikari.errors.NotFoundHTTPError` + If the guild is not found. + :obj:`~hikari.errors.ForbiddenHTTPError` + If you either lack the ``MANAGE_WEBHOOKS`` permission or + aren't a member of the given guild. + """ + payload = await self._session.get_guild_webhooks( + guild_id=str(guild.id if isinstance(guild, snowflakes.UniqueEntity) else int(guild)) + ) + return [webhooks.Webhook.deserialize(webhook) for webhook in payload] diff --git a/hikari/clients/rest_clients/invites_component.py b/hikari/clients/rest_clients/invites_component.py new file mode 100644 index 0000000000..715674e483 --- /dev/null +++ b/hikari/clients/rest_clients/invites_component.py @@ -0,0 +1,84 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# Copyright © Nekoka.tt 2019-2020 +# +# This file is part of Hikari. +# +# Hikari is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Hikari is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Hikari. If not, see . +"""The logic for handling requests to invite endpoints.""" + +__all__ = ["RESTInviteComponent"] + +import abc +import typing + +from hikari.clients.rest_clients import component_base +from hikari import invites + + +class RESTInviteComponent(component_base.BaseRESTComponent, abc.ABC): # pylint: disable=W0223 + """The REST client component for handling requests to invite endpoints.""" + + async def fetch_invite( + self, invite: typing.Union[invites.Invite, str], *, with_counts: bool = ... + ) -> invites.Invite: + """Get the given invite. + + Parameters + ---------- + invite : :obj:`~typing.Union` [ :obj:`~hikari.invites.Invite`, :obj:`~str` ] + The object or code of the wanted invite. + with_counts : :bool: + If specified, whether to attempt to count the number of + times the invite has been used. + + Returns + ------- + :obj:`~hikari.invites.Invite` + The requested invite object. + + Raises + ------ + :obj:`~hikari.errors.BadRequestHTTPError` + If any invalid snowflake IDs are passed; a snowflake may be invalid + due to it being outside of the range of a 64 bit integer. + :obj:`~hikari.errors.NotFoundHTTPError` + If the invite is not found. + """ + payload = await self._session.get_invite(invite_code=getattr(invite, "code", invite), with_counts=with_counts) + return invites.Invite.deserialize(payload) + + async def delete_invite(self, invite: typing.Union[invites.Invite, str]) -> None: + """Delete a given invite. + + Parameters + ---------- + invite : :obj:`~typing.Union` [ :obj:`~hikari.invites.Invite`, :obj:`~str` ] + The object or ID for the invite to be deleted. + + Returns + ------- + :obj:`~None` + Nothing, unlike what the API specifies. This is done to maintain + consistency with other calls of a similar nature in this API wrapper. + + Raises + ------ + :obj:`~hikari.errors.NotFoundHTTPError` + If the invite is not found. + :obj:`~hikari.errors.ForbiddenHTTPError` + If you lack either ``MANAGE_CHANNELS`` on the channel the invite + belongs to or ``MANAGE_GUILD`` for guild-global delete. + """ + await self._session.delete_invite(invite_code=getattr(invite, "code", invite)) diff --git a/hikari/clients/rest_clients/oauth2_component.py b/hikari/clients/rest_clients/oauth2_component.py new file mode 100644 index 0000000000..d31ba4874b --- /dev/null +++ b/hikari/clients/rest_clients/oauth2_component.py @@ -0,0 +1,41 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# Copyright © Nekoka.tt 2019-2020 +# +# This file is part of Hikari. +# +# Hikari is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Hikari is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Hikari. If not, see . +"""The logic for handling all requests to oauth2 endpoints.""" + +__all__ = ["RESTOauth2Component"] + +import abc + +from hikari.clients.rest_clients import component_base +from hikari import oauth2 + + +class RESTOauth2Component(component_base.BaseRESTComponent, abc.ABC): # pylint: disable=W0223 + """The REST client component for handling requests to oauth2 endpoints.""" + + async def fetch_my_application_info(self) -> oauth2.Application: + """Get the current application information. + + Returns + ------- + :obj:`~hikari.oauth2.Application` + An application info object. + """ + payload = await self._session.get_current_application_info() + return oauth2.Application.deserialize(payload) diff --git a/hikari/clients/rest_clients/reactions_component.py b/hikari/clients/rest_clients/reactions_component.py new file mode 100644 index 0000000000..9df5af605c --- /dev/null +++ b/hikari/clients/rest_clients/reactions_component.py @@ -0,0 +1,251 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# Copyright © Nekoka.tt 2019-2020 +# +# This file is part of Hikari. +# +# Hikari is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Hikari is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Hikari. If not, see . +"""Logic for handling all requests to reaction endpoints.""" + +__all__ = ["RESTReactionComponent"] + +import abc +import datetime +import typing + +from hikari.clients.rest_clients import component_base +from hikari.internal import pagination +from hikari import channels as _channels +from hikari import messages as _messages +from hikari import emojis +from hikari import snowflakes +from hikari import users + + +class RESTReactionComponent(component_base.BaseRESTComponent, abc.ABC): # pylint: disable=W0223 + """The REST client component for handling requests to reaction endpoints.""" + + async def create_reaction( + self, + channel: snowflakes.HashableT[_channels.Channel], + message: snowflakes.HashableT[_messages.Message], + emoji: typing.Union[emojis.Emoji, str], + ) -> None: + """Add a reaction to the given message in the given channel. + + Parameters + ---------- + channel : :obj:`~typing.Union` [ :obj:`~hikari.channels.Channel`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] + The object or ID of the channel to add this reaction in. + message : :obj:`~typing.Union` [ :obj:`~hikari.messages.Message`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] + The object or ID of the message to add the reaction in. + emoji : :obj:`~typing.Union` [ :obj:`~hikari.emojis.Emoji`, :obj:`~str` ] + The emoji to add. This can either be an emoji object or a string + representation of an emoji. The string representation will be either + ``"name:id"`` for custom emojis else it's unicode character(s) (can + be UTF-32). + + Raises + ------ + :obj:`~hikari.errors.ForbiddenHTTPError` + If this is the first reaction using this specific emoji on this + message and you lack the ``ADD_REACTIONS`` permission. If you lack + ``READ_MESSAGE_HISTORY``, this may also raise this error. + :obj:`~hikari.errors.NotFoundHTTPError` + If the channel or message is not found, or if the emoji is not found. + :obj:`~hikari.errors.BadRequestHTTPError` + If the emoji is not valid, unknown, or formatted incorrectly. + If any invalid snowflake IDs are passed; a snowflake may be invalid + due to it being outside of the range of a 64 bit integer. + """ + await self._session.create_reaction( + channel_id=str(channel.id if isinstance(channel, snowflakes.UniqueEntity) else int(channel)), + message_id=str(message.id if isinstance(message, snowflakes.UniqueEntity) else int(message)), + emoji=str(getattr(emoji, "url_name", emoji)), + ) + + async def delete_reaction( + self, + channel: snowflakes.HashableT[_channels.Channel], + message: snowflakes.HashableT[_messages.Message], + emoji: typing.Union[emojis.Emoji, str], + ) -> None: + """Remove your own reaction from the given message in the given channel. + + Parameters + ---------- + channel : :obj:`~typing.Union` [ :obj:`~hikari.channels.Channel`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] + The object or ID of the channel to add this reaction in. + message : :obj:`~typing.Union` [ :obj:`~hikari.messages.Message`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] + The object or ID of the message to add the reaction in. + emoji : :obj:`~typing.Union` [ :obj:`~hikari.emojis.Emoji`, :obj:`~str` ] + The emoji to add. This can either be an emoji object or a + string representation of an emoji. The string representation will be + either ``"name:id"`` for custom emojis else it's unicode + character(s) (can be UTF-32). + + Raises + ------ + :obj:`~hikari.errors.ForbiddenHTTPError` + If this is the first reaction using this specific emoji on this + message and you lack the ``ADD_REACTIONS`` permission. If you lack + ``READ_MESSAGE_HISTORY``, this may also raise this error. + :obj:`~hikari.errors.NotFoundHTTPError` + If the channel or message is not found, or if the emoji is not + found. + :obj:`~hikari.errors.BadRequestHTTPError` + If the emoji is not valid, unknown, or formatted incorrectly. + If any invalid snowflake IDs are passed; a snowflake may be invalid + due to it being outside of the range of a 64 bit integer. + """ + await self._session.delete_own_reaction( + channel_id=str(channel.id if isinstance(channel, snowflakes.UniqueEntity) else int(channel)), + message_id=str(message.id if isinstance(message, snowflakes.UniqueEntity) else int(message)), + emoji=str(getattr(emoji, "url_name", emoji)), + ) + + async def delete_all_reactions( + self, channel: snowflakes.HashableT[_channels.Channel], message: snowflakes.HashableT[_messages.Message], + ) -> None: + """Delete all reactions from a given message in a given channel. + + Parameters + ---------- + channel : :obj:`~typing.Union` [ :obj:`~hikari.channels.Channel`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] + The object or ID of the channel to get the message from. + message : :obj:`~typing.Union` [ :obj:`~hikari.messages.Message`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] + The object or ID of the message to remove all reactions from. + + Raises + ------ + :obj:`~hikari.errors.BadRequestHTTPError` + If any invalid snowflake IDs are passed; a snowflake may be invalid + due to it being outside of the range of a 64 bit integer. + :obj:`~hikari.errors.NotFoundHTTPError` + If the channel or message is not found. + :obj:`~hikari.errors.ForbiddenHTTPError` + If you lack the ``MANAGE_MESSAGES`` permission. + """ + await self._session.delete_all_reactions( + channel_id=str(channel.id if isinstance(channel, snowflakes.UniqueEntity) else int(channel)), + message_id=str(message.id if isinstance(message, snowflakes.UniqueEntity) else int(message)), + ) + + async def delete_all_reactions_for_emoji( + self, + channel: snowflakes.HashableT[_channels.Channel], + message: snowflakes.HashableT[_messages.Message], + emoji: typing.Union[emojis.Emoji, str], + ) -> None: + """Remove all reactions for a single given emoji on a given message. + + Parameters + ---------- + channel : :obj:`~typing.Union` [ :obj:`~hikari.channels.Channel`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] + The object or ID of the channel to get the message from. + message : :obj:`~typing.Union` [ :obj:`~hikari.messages.Message`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] + The object or ID of the message to delete the reactions from. + emoji : :obj:`~typing.Union` [ :obj:`~hikari.emojis.Emoji`, :obj:`~str` ] + The object or string representatiom of the emoji to delete. The + string representation will be either ``"name:id"`` for custom emojis + else it's unicode character(s) (can be UTF-32). + + Raises + ------ + :obj:`~hikari.errors.BadRequestHTTPError` + If any invalid snowflake IDs are passed; a snowflake may be invalid + due to it being outside of the range of a 64 bit integer. + :obj:`~hikari.errors.NotFoundHTTPError` + If the channel or message or emoji or user is not found. + :obj:`~hikari.errors.ForbiddenHTTPError` + If you lack the ``MANAGE_MESSAGES`` permission, or the channel is a + DM channel. + """ + await self._session.delete_all_reactions_for_emoji( + channel_id=str(channel.id if isinstance(channel, snowflakes.UniqueEntity) else int(channel)), + message_id=str(message.id if isinstance(message, snowflakes.UniqueEntity) else int(message)), + emoji=str(getattr(emoji, "url_name", emoji)), + ) + + def fetch_reactors_after( + self, + channel: snowflakes.HashableT[_channels.Channel], + message: snowflakes.HashableT[_messages.Message], + emoji: typing.Union[emojis.Emoji, str], + *, + after: typing.Union[datetime.datetime, snowflakes.HashableT[users.User]] = 0, + limit: typing.Optional[int] = None, + ) -> typing.AsyncIterator[users.User]: + """Get an async iterator of the users who reacted to a message. + + This returns the users created after a given user object/ID or from the + oldest user who reacted. + + Parameters + ---------- + channel : :obj:`~typing.Union` [ :obj:`~hikari.channels.Channel`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] + The object or ID of the channel to get the message from. + message : :obj:`~typing.Union` [ :obj:`~hikari.messages.Message`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] + The object or ID of the message to get the reactions from. + emoji : :obj:`~typing.Union` [ :obj:`~hikari.emojis.Emoji`, :obj:`~str` ] + The emoji to get. This can either be it's object or the string + representation of the emoji. The string representation will be + either ``"name:id"`` for custom emojis else it's unicode + character(s) (can be UTF-32). + after : :obj:`~typing.Union` [ :obj:`~datetime.datetime`, :obj:`~hikari.users.User`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] + If specified, a object or ID user. If specified, only users with a + snowflake that is lexicographically greater than the value will be + returned. + limit : :obj:`~str` + If specified, the limit of the number of users this iterator should + return. + + Example + ------- + .. code-block:: python + + async for user in client.fetch_reactors_after(channel, message, emoji, after=9876543, limit=1231): + if user.is_bot: + await client.kick_member(channel.guild_id, user) + + Returns + ------- + :obj:`~typing.AsyncIterator` [ :obj:`~hikari.users.User` ] + An async iterator of user objects. + + Raises + ------ + :obj:`~hikari.errors.BadRequestHTTPError` + If any invalid snowflake IDs are passed; a snowflake may be invalid + due to it being outside of the range of a 64 bit integer. + :obj:`~hikari.errors.ForbiddenHTTPError` + If you lack access to the message. + :obj:`~hikari.errors.NotFoundHTTPError` + If the channel or message is not found. + """ + if isinstance(after, datetime.datetime): + after = str(snowflakes.Snowflake.from_datetime(after)) + else: + after = str(after.id if isinstance(after, snowflakes.UniqueEntity) else int(after)) + return pagination.pagination_handler( + channel_id=str(channel.id if isinstance(channel, snowflakes.UniqueEntity) else int(channel)), + message_id=str(message.id if isinstance(message, snowflakes.UniqueEntity) else int(message)), + emoji=getattr(emoji, "url_name", emoji), + deserializer=users.User.deserialize, + direction="after", + request=self._session.get_reactions, + reversing=False, + start=after, + limit=limit, + ) diff --git a/hikari/clients/rest_clients/users_component.py b/hikari/clients/rest_clients/users_component.py new file mode 100644 index 0000000000..1b44fd2c71 --- /dev/null +++ b/hikari/clients/rest_clients/users_component.py @@ -0,0 +1,57 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# Copyright © Nekoka.tt 2019-2020 +# +# This file is part of Hikari. +# +# Hikari is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Hikari is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Hikari. If not, see . +"""The logic for handling all requests to user endpoints.""" + +__all__ = ["RESTUserComponent"] + +import abc + +from hikari.clients.rest_clients import component_base +from hikari import snowflakes +from hikari import users + + +class RESTUserComponent(component_base.BaseRESTComponent, abc.ABC): # pylint: disable=W0223 + """The REST client component for handling requests to user endpoints.""" + + async def fetch_user(self, user: snowflakes.HashableT[users.User]) -> users.User: + """Get a given user. + + Parameters + ---------- + user : :obj:`~typing.Union` [ :obj:`~hikari.users.User`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] + The object or ID of the user to get. + + Returns + ------- + :obj:`~hikari.users.User` + The requested user object. + + Raises + ------ + :obj:`~hikari.errors.BadRequestHTTPError` + If any invalid snowflake IDs are passed; a snowflake may be invalid + due to it being outside of the range of a 64 bit integer. + :obj:`~hikari.errors.NotFoundHTTPError` + If the user is not found. + """ + payload = await self._session.get_user( + user_id=str(user.id if isinstance(user, snowflakes.UniqueEntity) else int(user)) + ) + return users.User.deserialize(payload) diff --git a/hikari/clients/rest_clients/voices_component.py b/hikari/clients/rest_clients/voices_component.py new file mode 100644 index 0000000000..b5c3c19dd9 --- /dev/null +++ b/hikari/clients/rest_clients/voices_component.py @@ -0,0 +1,46 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# Copyright © Nekoka.tt 2019-2020 +# +# This file is part of Hikari. +# +# Hikari is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Hikari is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Hikari. If not, see . +"""The logic for handling all requests to voice endpoints.""" + +__all__ = ["RESTVoiceComponent"] + +import abc +import typing + +from hikari.clients.rest_clients import component_base +from hikari import voices + + +class RESTVoiceComponent(component_base.BaseRESTComponent, abc.ABC): # pylint: disable=W0223 + """The REST client component for handling requests to voice endpoints.""" + + async def fetch_voice_regions(self) -> typing.Sequence[voices.VoiceRegion]: + """Get the voice regions that are available. + + Returns + ------- + :obj:`~typing.Sequence` [ :obj:`~hikari.voices.VoiceRegion` ] + A list of voice regions available + + Note + ---- + This does not include VIP servers. + """ + payload = await self._session.list_voice_regions() + return [voices.VoiceRegion.deserialize(region) for region in payload] diff --git a/hikari/clients/rest_clients/webhooks_component.py b/hikari/clients/rest_clients/webhooks_component.py new file mode 100644 index 0000000000..e9090c9c36 --- /dev/null +++ b/hikari/clients/rest_clients/webhooks_component.py @@ -0,0 +1,310 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# Copyright © Nekoka.tt 2019-2020 +# +# This file is part of Hikari. +# +# Hikari is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Hikari is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Hikari. If not, see . +"""The logic for handling all requests to webhook endpoints.""" + +__all__ = ["RESTWebhookComponent"] + +import abc +import typing + +from hikari.clients.rest_clients import component_base +from hikari.internal import allowed_mentions +from hikari.internal import conversions +from hikari import channels as _channels +from hikari import embeds as _embeds +from hikari import guilds +from hikari import media +from hikari import messages as _messages +from hikari import snowflakes +from hikari import users +from hikari import webhooks + + +class RESTWebhookComponent(component_base.BaseRESTComponent, abc.ABC): # pylint: disable=W0223 + """The REST client component for handling requests to webhook endpoints.""" + + async def fetch_webhook( + self, webhook: snowflakes.HashableT[webhooks.Webhook], *, webhook_token: str = ... + ) -> webhooks.Webhook: + """Get a given webhook. + + Parameters + ---------- + webhook : :obj:`~typing.Union` [ :obj:`~hikari.webhooks.Webhook`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] + The object or ID of the webhook to get. + webhook_token : :obj:`~str` + If specified, the webhook token to use to get it (bypassing this + session's provided authorization ``token``). + + Returns + ------- + :obj:`~hikari.webhooks.Webhook` + The requested webhook object. + + Raises + ------ + :obj:`~hikari.errors.BadRequestHTTPError` + If any invalid snowflake IDs are passed; a snowflake may be invalid + due to it being outside of the range of a 64 bit integer. + :obj:`~hikari.errors.NotFoundHTTPError` + If the webhook is not found. + :obj:`~hikari.errors.ForbiddenHTTPError` + If you're not in the guild that owns this webhook or + lack the ``MANAGE_WEBHOOKS`` permission. + :obj:`~hikari.errors.UnauthorizedHTTPError` + If you pass a token that's invalid for the target webhook. + """ + payload = await self._session.get_webhook( + webhook_id=str(webhook.id if isinstance(webhook, snowflakes.UniqueEntity) else int(webhook)), + webhook_token=webhook_token, + ) + return webhooks.Webhook.deserialize(payload) + + async def update_webhook( + self, + webhook: snowflakes.HashableT[webhooks.Webhook], + *, + webhook_token: str = ..., + name: str = ..., + avatar_data: typing.Optional[conversions.FileLikeT] = ..., + channel: snowflakes.HashableT[_channels.GuildChannel] = ..., + reason: str = ..., + ) -> webhooks.Webhook: + """Edit a given webhook. + + Parameters + ---------- + webhook : :obj:`~typing.Union` [ :obj:`~hikari.webhooks.Webhook`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] + The object or ID of the webhook to edit. + webhook_token : :obj:`~str` + If specified, the webhook token to use to modify it (bypassing this + session's provided authorization ``token``). + name : :obj:`~str` + If specified, the new name string. + avatar_data : ``hikari.internal.conversions.FileLikeT``, optional + If specified, the new avatar image file object. If :obj:`~None`, then + it is removed. + channel : :obj:`~typing.Union` [ :obj:`~hikari.channels.GuildChannel`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] + If specified, the object or ID of the new channel the given + webhook should be moved to. + reason : :obj:`~str` + If specified, the audit log reason explaining why the operation + was performed. + + Returns + ------- + :obj:`~hikari.webhooks.Webhook` + The updated webhook object. + + Raises + ------ + :obj:`~hikari.errors.BadRequestHTTPError` + If any invalid snowflake IDs are passed; a snowflake may be invalid + due to it being outside of the range of a 64 bit integer. + :obj:`~hikari.errors.NotFoundHTTPError` + If either the webhook or the channel aren't found. + :obj:`~hikari.errors.ForbiddenHTTPError` + If you either lack the ``MANAGE_WEBHOOKS`` permission or + aren't a member of the guild this webhook belongs to. + :obj:`~hikari.errors.UnauthorizedHTTPError` + If you pass a token that's invalid for the target webhook. + """ + payload = await self._session.modify_webhook( + webhook_id=str(webhook.id if isinstance(webhook, snowflakes.UniqueEntity) else int(webhook)), + webhook_token=webhook_token, + name=name, + avatar=( + conversions.get_bytes_from_resource(avatar_data) + if avatar_data and avatar_data is not ... + else avatar_data + ), + channel_id=( + str(channel.id if isinstance(channel, snowflakes.UniqueEntity) else int(channel)) + if channel and channel is not ... + else channel + ), + reason=reason, + ) + return webhooks.Webhook.deserialize(payload) + + async def delete_webhook( + self, webhook: snowflakes.HashableT[webhooks.Webhook], *, webhook_token: str = ... + ) -> None: + """Delete a given webhook. + + Parameters + ---------- + webhook : :obj:`~typing.Union` [ :obj:`~hikari.webhooks.Webhook`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] + The object or ID of the webhook to delete + webhook_token : :obj:`~str` + If specified, the webhook token to use to delete it (bypassing this + session's provided authorization ``token``). + + Raises + ------ + :obj:`~hikari.errors.BadRequestHTTPError` + If any invalid snowflake IDs are passed; a snowflake may be invalid + due to it being outside of the range of a 64 bit integer. + :obj:`~hikari.errors.NotFoundHTTPError` + If the webhook is not found. + :obj:`~hikari.errors.ForbiddenHTTPError` + If you either lack the ``MANAGE_WEBHOOKS`` permission or + aren't a member of the guild this webhook belongs to. + :obj:`~hikari.errors.UnauthorizedHTTPError` + If you pass a token that's invalid for the target webhook. + """ + await self._session.delete_webhook( + webhook_id=str(webhook.id if isinstance(webhook, snowflakes.UniqueEntity) else int(webhook)), + webhook_token=webhook_token, + ) + + async def execute_webhook( + self, + webhook: snowflakes.HashableT[webhooks.Webhook], + webhook_token: str, + *, + content: str = ..., + username: str = ..., + avatar_url: str = ..., + tts: bool = ..., + wait: bool = False, + file: media.IO = ..., + embeds: typing.Sequence[_embeds.Embed] = ..., + mentions_everyone: bool = True, + user_mentions: typing.Union[typing.Collection[snowflakes.HashableT[users.User]], bool] = True, + role_mentions: typing.Union[typing.Collection[snowflakes.HashableT[guilds.GuildRole]], bool] = True, + ) -> typing.Optional[_messages.Message]: + """Execute a webhook to create a message. + + Parameters + ---------- + webhook : :obj:`~typing.Union` [ :obj:`~hikari.webhooks.Webhook`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] + The object or ID of the webhook to execute. + webhook_token : :obj:`~str` + The token of the webhook to execute. + content : :obj:`~str` + If specified, the message content to send with the message. + username : :obj:`~str` + If specified, the username to override the webhook's username + for this request. + avatar_url : :obj:`~str` + If specified, the url of an image to override the webhook's + avatar with for this request. + tts : :obj:`~bool` + If specified, whether the message will be sent as a TTS message. + wait : :obj:`~bool` + If specified, whether this request should wait for the webhook + to be executed and return the resultant message object. + file : ``hikari.media.IO`` + If specified, this is a file object to send along with the webhook + as defined in :mod:`hikari.media`. + embeds : :obj:`~typing.Sequence` [ :obj:`~hikari.embeds.Embed` ] + If specified, a sequence of ``1`` to ``10`` embed objects to send + with the embed. + mentions_everyone : :obj:`~bool` + Whether ``@everyone`` and ``@here`` mentions should be resolved by + discord and lead to actual pings, defaults to :obj:`~True`. + user_mentions : :obj:`~typing.Union` [ :obj:`~typing.Collection` [ :obj:`~typing.Union` [ :obj:`~hikari.users.User`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ], :obj:`~bool` ] + Either an array of user objects/IDs to allow mentions for, + :obj:`~True` to allow all user mentions or :obj:`~False` to block all + user mentions from resolving, defaults to :obj:`~True`. + role_mentions : :obj:`~typing.Union` [ :obj:`~typing.Collection` [ :obj:`~typing.Union` [ :obj:`~hikari.guilds.GuildRole`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] ], :obj:`~bool` ] + Either an array of guild role objects/IDs to allow mentions for, + :obj:`~True` to allow all role mentions or :obj:`~False` to block all + role mentions from resolving, defaults to :obj:`~True`. + + Returns + ------- + :obj:`~hikari.messages.Message`, optional + The created message object, if ``wait`` is :obj:`~True`, else + :obj:`~None`. + + Raises + ------ + :obj:`~hikari.errors.NotFoundHTTPError` + If the channel ID or webhook ID is not found. + :obj:`~hikari.errors.BadRequestHTTPError` + This can be raised if the file is too large; if the embed exceeds + the defined limits; if the message content is specified only and + empty or greater than ``2000`` characters; if neither content, file + or embeds are specified. + If any invalid snowflake IDs are passed; a snowflake may be invalid + due to it being outside of the range of a 64 bit integer. + :obj:`~hikari.errors.ForbiddenHTTPError` + If you lack permissions to send to this channel. + :obj:`~hikari.errors.UnauthorizedHTTPError` + If you pass a token that's invalid for the target webhook. + :obj:`~ValueError` + If more than 100 unique objects/snowflakes are passed for + ``role_mentions`` or ``user_mentions``. + """ + payload = await self._session.execute_webhook( + webhook_id=str(webhook.id if isinstance(webhook, snowflakes.UniqueEntity) else int(webhook)), + webhook_token=webhook_token, + content=content, + username=username, + avatar_url=avatar_url, + tts=tts, + wait=wait, + file=await media.safe_read_file(file) if file is not ... else ..., + embeds=[embed.serialize() for embed in embeds] if embeds is not ... else ..., + allowed_mentions=allowed_mentions.generate_allowed_mentions( + mentions_everyone=mentions_everyone, user_mentions=user_mentions, role_mentions=role_mentions + ), + ) + if wait is True: + return _messages.Message.deserialize(payload) + return None + + def safe_webhook_execute( + self, + webhook: snowflakes.HashableT[webhooks.Webhook], + webhook_token: str, + *, + content: str = ..., + username: str = ..., + avatar_url: str = ..., + tts: bool = ..., + wait: bool = False, + file: media.IO = ..., + embeds: typing.Sequence[_embeds.Embed] = ..., + mentions_everyone: bool = False, + user_mentions: typing.Union[typing.Collection[snowflakes.HashableT[users.User]], bool] = False, + role_mentions: typing.Union[typing.Collection[snowflakes.HashableT[guilds.GuildRole]], bool] = False, + ) -> typing.Coroutine[typing.Any, typing.Any, typing.Optional[_messages.Message]]: + """Execute a webhook to create a message with mention safety. + + This endpoint has the same signature as :attr:`execute_webhook` with + the only difference being that ``mentions_everyone``, + ``user_mentions`` and ``role_mentions`` default to :obj:`~False`. + """ + return self.execute_webhook( + webhook=webhook, + webhook_token=webhook_token, + content=content, + username=username, + avatar_url=avatar_url, + tts=tts, + wait=wait, + file=file, + embeds=embeds, + mentions_everyone=mentions_everyone, + user_mentions=user_mentions, + role_mentions=role_mentions, + ) diff --git a/hikari/embeds.py b/hikari/embeds.py index 3a4f0f77ce..dfc83cea84 100644 --- a/hikari/embeds.py +++ b/hikari/embeds.py @@ -33,9 +33,9 @@ import attr -import hikari.internal.conversions from hikari import colors from hikari import entities +from hikari.internal import conversions from hikari.internal import marshaller @@ -296,7 +296,7 @@ class Embed(entities.HikariEntity, entities.Deserializable, entities.Serializabl #: #: :type: :obj:`~datetime.datetime`, optional timestamp: typing.Optional[datetime.datetime] = marshaller.attrib( - deserializer=hikari.internal.conversions.parse_iso_8601_ts, + deserializer=conversions.parse_iso_8601_ts, serializer=lambda timestamp: timestamp.replace(tzinfo=datetime.timezone.utc).isoformat(), if_undefined=None, default=None, diff --git a/hikari/events.py b/hikari/events.py index 2d2bd7152f..53293cae86 100644 --- a/hikari/events.py +++ b/hikari/events.py @@ -732,7 +732,7 @@ class MessageUpdateEvent(HikariEvent, snowflakes.UniqueEntity, entities.Deserial Note ---- - All fields on this model except :attr:`channel_id` and :obj:`~`HikariEvent.id`` may be + All fields on this model except :attr:`channel_id` and ``id`` may be set to :obj:`~hikari.entities.UNSET` (a singleton defined in ``hikari.entities``) if we have not received information about their state from Discord alongside field nullability. diff --git a/hikari/internal/allowed_mentions.py b/hikari/internal/allowed_mentions.py new file mode 100644 index 0000000000..549da6dc4a --- /dev/null +++ b/hikari/internal/allowed_mentions.py @@ -0,0 +1,100 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# Copyright © Nekokatt 2019-2020 +# +# This file is part of Hikari. +# +# Hikari is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Hikari is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Hikari. If not, see . +"""Logic for generating a allowed mentions dict objects. + +|internal| +""" + +__all__ = ["generate_allowed_mentions"] + +import typing + +from hikari import guilds +from hikari import snowflakes +from hikari import users +from hikari.internal import assertions +from hikari.internal import more_collections + + +def generate_allowed_mentions( + mentions_everyone: bool, + user_mentions: typing.Union[typing.Collection[snowflakes.HashableT[users.User]], bool], + role_mentions: typing.Union[typing.Collection[snowflakes.HashableT[guilds.GuildRole]], bool], +) -> typing.Dict[str, typing.Sequence[str]]: + """Generate an allowed mentions object based on input mention rules. + + Parameters + ---------- + mentions_everyone : :obj:`~bool` + Whether ``@everyone`` and ``@here`` mentions should be resolved by + discord and lead to actual pings. + user_mentions : :obj:`~typing.Union` [ :obj:`~typing.Collection` [ :obj:`~typing.Union` [ :obj:`~hikari.users.User`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ], :obj:`~bool` ] + Either an array of user objects/IDs to allow mentions for, + :obj:`~True` to allow all user mentions or :obj:`~False` to block all + user mentions from resolving. + role_mentions : :obj:`~typing.Union` [ :obj:`~typing.Collection` [ :obj:`~typing.Union` [ :obj:`~hikari.guilds.GuildRole`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] ], :obj:`~bool` ] + Either an array of guild role objects/IDs to allow mentions for, + :obj:`~True` to allow all role mentions or :obj:`~False` to block all + role mentions from resolving. + + Returns + ------- + :obj:`~typing.Dict` [ :obj:`~str`, :obj:`~typing.Sequence` [ :obj:`~str` ] ] + The resulting allowed mentions dict object. + + Raises + ------ + :obj:`~ValueError` + If more than 100 unique objects/snowflakes are passed for + ``role_mentions`` or ``user_mentions. + """ + parsed_mentions = [] + allowed_mentions = {} + if mentions_everyone is True: + parsed_mentions.append("everyone") + if user_mentions is True: + parsed_mentions.append("users") + # This covers both `False` and an array of IDs/objs by using `user_mentions or EMPTY_SEQUENCE`, where a + # resultant empty list will mean that all user mentions are blacklisted. + else: + allowed_mentions["users"] = list( + # dict.fromkeys is used to remove duplicate entries that would cause discord to return an error. + dict.fromkeys( + str(user.id if isinstance(user, snowflakes.UniqueEntity) else int(user)) + for user in user_mentions or more_collections.EMPTY_SEQUENCE + ) + ) + assertions.assert_that(len(allowed_mentions["users"]) <= 100, "Only up to 100 users can be provided.") + if role_mentions is True: + parsed_mentions.append("roles") + # This covers both `False` and an array of IDs/objs by using `user_mentions or EMPTY_SEQUENCE`, where a + # resultant empty list will mean that all role mentions are blacklisted. + else: + allowed_mentions["roles"] = list( + # dict.fromkeys is used to remove duplicate entries that would cause discord to return an error. + dict.fromkeys( + str(role.id if isinstance(role, snowflakes.UniqueEntity) else int(role)) + for role in role_mentions or more_collections.EMPTY_SEQUENCE + ) + ) + assertions.assert_that(len(allowed_mentions["roles"]) <= 100, "Only up to 100 roles can be provided.") + allowed_mentions["parse"] = parsed_mentions + # As a note, discord will also treat an empty `allowed_mentions` object as if it wasn't passed at all, so we + # want to use empty lists for blacklisting elements rather than just not including blacklisted elements. + return allowed_mentions diff --git a/hikari/internal/meta.py b/hikari/internal/meta.py index bfc17c94d4..a5027e7b0d 100644 --- a/hikari/internal/meta.py +++ b/hikari/internal/meta.py @@ -22,6 +22,8 @@ """ __all__ = ["SingletonMeta", "Singleton"] +import abc +import inspect import typing from hikari.internal import more_collections @@ -54,7 +56,9 @@ class SingletonMeta(type): thread safe. """ + # pylint: disable=E1136 ___instance_dict_t___ = more_collections.WeakKeyDictionary[typing.Type[typing.Any], typing.Any] + # pylint: enable=E1136 ___instances___: ___instance_dict_t___ = more_collections.WeakKeyDictionary() __slots__ = () @@ -89,3 +93,35 @@ class Singleton(metaclass=SingletonMeta): Constructing instances of this class or derived classes may not be thread safe. """ + + +class UniqueFunctionMeta(abc.ABCMeta): + """Metaclass for mixins that are expected to provide unique function names. + + If subclassing from two mixins that are derived from this type and both + mixins provide the same function, a type error is raised when the class is + defined. + + Note + ---- + This metaclass derives from :obj:`~abc.ABCMeta`, and thus is compatible + with abstract method conduit. + """ + + @classmethod + def __prepare__(mcs, name, bases, **kwargs): + routines = {} + + for base in bases: + for identifier, method in inspect.getmembers(base, inspect.isroutine): + if identifier.startswith("__"): + continue + + if identifier in routines and method != routines[identifier]: + raise TypeError( + f"Conflicting methods {routines[identifier].__qualname__} and {method.__qualname__} found." + ) + + routines[identifier] = method + + return super().__prepare__(name, bases, **kwargs) diff --git a/hikari/internal/pagination.py b/hikari/internal/pagination.py new file mode 100644 index 0000000000..552b26c3f6 --- /dev/null +++ b/hikari/internal/pagination.py @@ -0,0 +1,91 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# Copyright © Nekokatt 2019-2020 +# +# This file is part of Hikari. +# +# Hikari is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Hikari is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Hikari. If not, see . +"""Logic for handling Discord's generic paginated endpoints. + +|internal| +""" + +__all__ = ["pagination_handler"] + +import typing + + +async def pagination_handler( + deserializer: typing.Callable[[typing.Any], typing.Any], + direction: typing.Union[typing.Literal["before"], typing.Literal["after"]], + request: typing.Callable[..., typing.Coroutine[typing.Any, typing.Any, typing.Any]], + reversing: bool, + start: typing.Union[str, None], + limit: typing.Optional[int] = None, + id_getter: typing.Callable[[typing.Any], str] = lambda entity: str(entity.id), + **kwargs, +) -> typing.AsyncIterator[typing.Any]: + """Generate an async iterator for handling paginated endpoints. + + This will handle Discord's ``before`` and ``after`` pagination. + + Parameters + ---------- + deserializer : :obj:`~typing.Callable` [ [ :obj:`~typing.Any` ], :obj:`~typing.Any` ] + The deserializer to use to deserialize raw elements. + direction : :obj:`~typing.Union` [ ``"before"``, ``"after"`` ] + The direction that this paginator should go in. + request : :obj:`~typing.Callable` [ ``...``, :obj:`~typing.Coroutine` [ :obj:`~typing.Any`, :obj:`~typing.Any`, :obj:`~typing.Any` ] ] + The :obj:`hikari.net.rest.LowLevelRestfulClient` method that should be + called to make requests for this paginator. + reversing : :obj:`~bool` + Whether the retrieved array of objects should be reversed before + iterating through it, this is needed for certain endpoints like + ``fetch_messages_before`` where the order is static regardless of + if you're using ``before`` or ``after``. + start : :obj:`~int`, optional + The snowflake ID that this paginator should start at, ``0`` may be + passed for ``forward`` pagination to start at the first created + entity and :obj:`~None` may be passed for ``before`` pagination to + start at the newest entity (based on when it's snowflake timestamp). + limit : :obj:`~int`, optional + The amount of deserialized entities that the iterator should return + total, will be unlimited if set to :obj:`~None`. + id_getter : :obj:`~typing.Callable` [ [ :obj:`~typing.Any` ], :obj:`~str` ] + **kwargs + Kwargs to pass through to ``request`` for every request made along + with the current decided limit and direction snowflake. + + Returns + ------- + :obj:`~typing.AsyncIterator` [ :obj:`~typing.Any` ] + An async iterator of the found deserialized found objects. + + """ + while payloads := await request( + limit=100 if limit is None or limit > 100 else limit, + **{direction: start if start is not None else ...}, + **kwargs, + ): + if reversing: + payloads.reverse() + if limit is not None: + limit -= len(payloads) + + for payload in payloads: + entity = deserializer(payload) + yield entity + if limit == 0: + break + start = id_getter(entity) diff --git a/hikari/net/ratelimits.py b/hikari/net/ratelimits.py index 7511cf54f3..7dad9ed9b4 100644 --- a/hikari/net/ratelimits.py +++ b/hikari/net/ratelimits.py @@ -692,7 +692,7 @@ class RESTBucketManager: #: major parameters used in that compiled route) to their corresponding rate #: limiters. #: - #: :type: :obj:`~typing.MutableMapping` [ :obj:`~str`, :obj:`~RESTBucketRateLimiter` ] + #: :type: :obj:`~typing.MutableMapping` [ :obj:`~str`, :obj:`~RESTBucket` ] real_hashes_to_buckets: typing.Final[typing.MutableMapping[str, RESTBucket]] #: An internal event that is set when the object is shut down. diff --git a/hikari/net/rest.py b/hikari/net/rest.py index 028038bf78..d5a5e4c988 100644 --- a/hikari/net/rest.py +++ b/hikari/net/rest.py @@ -163,7 +163,7 @@ class LowLevelRestfulClient: #: #: You should not ever need to touch this implementation. #: - #: :type: :obj:`~hikari.net.ratelimits.HTTPBucketRateLimiterManager` + #: :type: :obj:`~hikari.net.ratelimits.RESTBucketManager` ratelimiter: ratelimits.RESTBucketManager #: The custom SSL context to use. diff --git a/hikari/state/event_managers.py b/hikari/state/event_managers.py index 909d69074d..9909a2fa5e 100644 --- a/hikari/state/event_managers.py +++ b/hikari/state/event_managers.py @@ -174,7 +174,7 @@ def process_raw_event( try: handler(shard_client_obj, payload) - except Exception as ex: + except Exception as ex: # pylint: disable=W0703 self.logger.exception( "Failed to unmarshal %r event payload. This is likely a bug in Hikari itself. " "Please contact a library dev or make an issue on the issue tracker for more support.", diff --git a/pylint.ini b/pylint.ini index 67eed34fa8..945551886e 100644 --- a/pylint.ini +++ b/pylint.ini @@ -504,7 +504,7 @@ exclude-protected=_asdict, valid-classmethod-first-arg=cls # List of valid names for the first argument in a metaclass class method. -valid-metaclass-classmethod-first-arg=cls +valid-metaclass-classmethod-first-arg=mcs [IMPORTS] diff --git a/tests/hikari/clients/test_rest_client.py b/tests/hikari/clients/test_rest_client.py deleted file mode 100644 index 220f401261..0000000000 --- a/tests/hikari/clients/test_rest_client.py +++ /dev/null @@ -1,2762 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# Copyright © Nekoka.tt 2019-2020 -# -# This file is part of Hikari. -# -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along ith Hikari. If not, see . -import contextlib -import datetime -import io -import mock - -import pytest - -from hikari import audit_logs -from hikari import channels -from hikari import colors -from hikari import embeds -from hikari import emojis -from hikari import gateway_entities -from hikari import guilds -from hikari import invites -from hikari import media -from hikari import messages -from hikari import oauth2 -from hikari import permissions -from hikari import snowflakes -from hikari import users -from hikari import voices -from hikari import webhooks -from hikari.clients import configs -from hikari.clients import rest_clients -from hikari.internal import conversions -from hikari.net import rest -from tests.hikari import _helpers - - -def test__get_member_id(): - member = mock.MagicMock( - guilds.GuildMember, user=mock.MagicMock(users.User, id=123123123, __int__=users.User.__int__) - ) - assert rest_clients._get_member_id(member) == "123123123" - - -class TestRESTClient: - @pytest.fixture() - def mock_config(self): - # Mocking the Configs leads to attribute errors regardless of spec set. - return configs.RESTConfig(token="blah.blah.blah") - - def test_init(self, mock_config): - mock_low_level_rest_clients = mock.MagicMock(rest.LowLevelRestfulClient) - with mock.patch.object(rest, "LowLevelRestfulClient", return_value=mock_low_level_rest_clients) as patched_init: - cli = rest_clients.RESTClient(mock_config) - patched_init.assert_called_once_with( - allow_redirects=mock_config.allow_redirects, - connector=mock_config.tcp_connector, - proxy_headers=mock_config.proxy_headers, - proxy_auth=mock_config.proxy_auth, - ssl_context=mock_config.ssl_context, - verify_ssl=mock_config.verify_ssl, - timeout=mock_config.request_timeout, - token=f"{mock_config.token_type} {mock_config.token}", - version=mock_config.rest_version, - ) - assert cli._session is mock_low_level_rest_clients - - @pytest.fixture() - def low_level_rest_impl(self) -> rest.LowLevelRestfulClient: - return mock.MagicMock(rest.LowLevelRestfulClient) - - @pytest.fixture() - def rest_clients_impl(self, low_level_rest_impl, mock_config) -> rest_clients.RESTClient: - with mock.patch.object(rest, "LowLevelRestfulClient", return_value=low_level_rest_impl): - return rest_clients.RESTClient(mock_config) - - @pytest.mark.asyncio - async def test_close_awaits_session_close(self, rest_clients_impl): - await rest_clients_impl.close() - rest_clients_impl._session.close.assert_called_once() - - @pytest.mark.asyncio - async def test___aenter___and___aexit__(self, rest_clients_impl): - rest_clients_impl.close = mock.AsyncMock() - async with rest_clients_impl as client: - assert client is rest_clients_impl - rest_clients_impl.close.assert_called_once_with() - - @pytest.mark.asyncio - async def test_fetch_gateway_url(self, rest_clients_impl): - mock_url = "wss://gateway.discord.gg/" - rest_clients_impl._session.get_gateway.return_value = mock_url - assert await rest_clients_impl.fetch_gateway_url() == mock_url - rest_clients_impl._session.get_gateway.assert_called_once() - - @pytest.mark.asyncio - async def test_fetch_gateway_bot(self, rest_clients_impl): - mock_payload = {"url": "wss://gateway.discord.gg/", "shards": 9, "session_start_limit": {}} - mock_gateway_bot_obj = mock.MagicMock(gateway_entities.GatewayBot) - rest_clients_impl._session.get_gateway_bot.return_value = mock_payload - with mock.patch.object(gateway_entities.GatewayBot, "deserialize", return_value=mock_gateway_bot_obj): - assert await rest_clients_impl.fetch_gateway_bot() is mock_gateway_bot_obj - rest_clients_impl._session.get_gateway_bot.assert_called_once() - gateway_entities.GatewayBot.deserialize.assert_called_once_with(mock_payload) - - @_helpers.parametrize_valid_id_formats_for_models("guild", 379953393319542784, guilds.Guild) - @_helpers.parametrize_valid_id_formats_for_models("user", 22222222, users.User) - @_helpers.parametrize_valid_id_formats_for_models("before", 123123123123, audit_logs.AuditLogEntry) - def test_fetch_audit_log_entries_before_with_optionals(self, rest_clients_impl, guild, before, user): - mock_audit_log_iterator = mock.MagicMock(audit_logs.AuditLogIterator) - with mock.patch.object(audit_logs, "AuditLogIterator", return_value=mock_audit_log_iterator): - result = rest_clients_impl.fetch_audit_log_entries_before( - guild, before=before, user=user, action_type=audit_logs.AuditLogEventType.MEMBER_MOVE, limit=42, - ) - assert result is mock_audit_log_iterator - audit_logs.AuditLogIterator.assert_called_once_with( - guild_id="379953393319542784", - request=rest_clients_impl._session.get_guild_audit_log, - before="123123123123", - user_id="22222222", - action_type=26, - limit=42, - ) - - @_helpers.parametrize_valid_id_formats_for_models("guild", 379953393319542784, guilds.Guild) - def test_fetch_audit_log_entries_before_without_optionals(self, rest_clients_impl, guild): - mock_audit_log_iterator = mock.MagicMock(audit_logs.AuditLogIterator) - with mock.patch.object(audit_logs, "AuditLogIterator", return_value=mock_audit_log_iterator): - assert rest_clients_impl.fetch_audit_log_entries_before(guild) is mock_audit_log_iterator - audit_logs.AuditLogIterator.assert_called_once_with( - guild_id="379953393319542784", - request=rest_clients_impl._session.get_guild_audit_log, - before=None, - user_id=..., - action_type=..., - limit=None, - ) - - def test_fetch_audit_log_entries_before_with_datetime_object(self, rest_clients_impl): - mock_audit_log_iterator = mock.MagicMock(audit_logs.AuditLogIterator) - with mock.patch.object(audit_logs, "AuditLogIterator", return_value=mock_audit_log_iterator): - date = datetime.datetime(2019, 1, 22, 18, 41, 15, 283_000, tzinfo=datetime.timezone.utc) - result = rest_clients_impl.fetch_audit_log_entries_before(123123123, before=date) - assert result is mock_audit_log_iterator - audit_logs.AuditLogIterator.assert_called_once_with( - guild_id="123123123", - request=rest_clients_impl._session.get_guild_audit_log, - before="537340988620800000", - user_id=..., - action_type=..., - limit=None, - ) - - @pytest.mark.asyncio - @_helpers.parametrize_valid_id_formats_for_models("guild", 379953393319542784, guilds.Guild) - @_helpers.parametrize_valid_id_formats_for_models("user", 115590097100865541, users.User) - @_helpers.parametrize_valid_id_formats_for_models("before", 1231231123, audit_logs.AuditLogEntry) - async def test_fetch_audit_log_with_optionals(self, rest_clients_impl, guild, user, before): - mock_audit_log_payload = {"entries": [], "integrations": [], "webhooks": [], "users": []} - mock_audit_log_obj = mock.MagicMock(audit_logs.AuditLog) - rest_clients_impl._session.get_guild_audit_log.return_value = mock_audit_log_payload - with mock.patch.object(audit_logs.AuditLog, "deserialize", return_value=mock_audit_log_obj): - result = await rest_clients_impl.fetch_audit_log( - guild, user=user, action_type=audit_logs.AuditLogEventType.MEMBER_MOVE, limit=100, before=before, - ) - assert result is mock_audit_log_obj - rest_clients_impl._session.get_guild_audit_log.assert_called_once_with( - guild_id="379953393319542784", - user_id="115590097100865541", - action_type=26, - limit=100, - before="1231231123", - ) - - @pytest.mark.asyncio - @_helpers.parametrize_valid_id_formats_for_models("guild", 379953393319542784, guilds.Guild) - async def test_fetch_audit_log_without_optionals(self, rest_clients_impl, guild): - mock_audit_log_payload = {"entries": [], "integrations": [], "webhooks": [], "users": []} - mock_audit_log_obj = mock.MagicMock(audit_logs.AuditLog) - rest_clients_impl._session.get_guild_audit_log.return_value = mock_audit_log_payload - with mock.patch.object(audit_logs.AuditLog, "deserialize", return_value=mock_audit_log_obj): - assert await rest_clients_impl.fetch_audit_log(guild) is mock_audit_log_obj - rest_clients_impl._session.get_guild_audit_log.assert_called_once_with( - guild_id="379953393319542784", user_id=..., action_type=..., limit=..., before=... - ) - - @pytest.mark.asyncio - @_helpers.parametrize_valid_id_formats_for_models("guild", 379953393319542784, guilds.Guild) - async def test_fetch_audit_log_handles_datetime_object(self, rest_clients_impl, guild): - mock_audit_log_payload = {"entries": [], "integrations": [], "webhooks": [], "users": []} - mock_audit_log_obj = mock.MagicMock(audit_logs.AuditLog) - rest_clients_impl._session.get_guild_audit_log.return_value = mock_audit_log_payload - date = datetime.datetime(2019, 1, 22, 18, 41, 15, 283_000, tzinfo=datetime.timezone.utc) - with mock.patch.object(audit_logs.AuditLog, "deserialize", return_value=mock_audit_log_obj): - assert await rest_clients_impl.fetch_audit_log(guild, before=date) is mock_audit_log_obj - rest_clients_impl._session.get_guild_audit_log.assert_called_once_with( - guild_id="379953393319542784", user_id=..., action_type=..., limit=..., before="537340988620800000" - ) - - @pytest.mark.asyncio - @_helpers.parametrize_valid_id_formats_for_models("channel", 1234, channels.Channel) - async def test_fetch_channel(self, rest_clients_impl, channel): - mock_payload = {"id": "49494994", "type": 3} - mock_channel_obj = mock.MagicMock(channels.Channel) - rest_clients_impl._session.get_channel.return_value = mock_payload - with mock.patch.object(channels, "deserialize_channel", return_value=mock_channel_obj): - assert await rest_clients_impl.fetch_channel(channel) is mock_channel_obj - rest_clients_impl._session.get_channel.assert_called_once_with(channel_id="1234") - channels.deserialize_channel.assert_called_once_with(mock_payload) - - @pytest.mark.asyncio - @_helpers.parametrize_valid_id_formats_for_models("channel", 379953393319542784, channels.Channel) - @_helpers.parametrize_valid_id_formats_for_models("parent_channel", 115590097100865541, channels.Channel) - @pytest.mark.parametrize("rate_limit_per_user", [42, datetime.timedelta(seconds=42)]) - async def test_update_channel_with_optionals(self, rest_clients_impl, channel, parent_channel, rate_limit_per_user): - mock_payload = {"name": "Qts", "type": 2} - mock_channel_obj = mock.MagicMock(channels.Channel) - mock_overwrite_payload = {"type": "user", "id": 543543543} - mock_overwrite_obj = mock.MagicMock(channels.PermissionOverwrite) - mock_overwrite_obj.serialize = mock.MagicMock(return_value=mock_overwrite_payload) - rest_clients_impl._session.modify_channel.return_value = mock_payload - with mock.patch.object(channels, "deserialize_channel", return_value=mock_channel_obj): - result = await rest_clients_impl.update_channel( - channel=channel, - name="ohNo", - position=7, - topic="camelsAreGreat", - nsfw=True, - bitrate=32000, - user_limit=42, - rate_limit_per_user=rate_limit_per_user, - permission_overwrites=[mock_overwrite_obj], - parent_category=parent_channel, - reason="Get Nyaa'd.", - ) - assert result is mock_channel_obj - rest_clients_impl._session.modify_channel.assert_called_once_with( - channel_id="379953393319542784", - name="ohNo", - position=7, - topic="camelsAreGreat", - nsfw=True, - rate_limit_per_user=42, - bitrate=32000, - user_limit=42, - permission_overwrites=[mock_overwrite_payload], - parent_id="115590097100865541", - reason="Get Nyaa'd.", - ) - mock_overwrite_obj.serialize.assert_called_once() - channels.deserialize_channel.assert_called_once_with(mock_payload) - - @pytest.mark.asyncio - @_helpers.parametrize_valid_id_formats_for_models("channel", 379953393319542784, channels.Channel) - async def test_update_channel_without_optionals( - self, rest_clients_impl, channel, - ): - mock_payload = {"name": "Qts", "type": 2} - mock_channel_obj = mock.MagicMock(channels.Channel) - rest_clients_impl._session.modify_channel.return_value = mock_payload - with mock.patch.object(channels, "deserialize_channel", return_value=mock_channel_obj): - result = await rest_clients_impl.update_channel(channel=channel,) - assert result is mock_channel_obj - rest_clients_impl._session.modify_channel.assert_called_once_with( - channel_id="379953393319542784", - name=..., - position=..., - topic=..., - nsfw=..., - rate_limit_per_user=..., - bitrate=..., - user_limit=..., - permission_overwrites=..., - parent_id=..., - reason=..., - ) - channels.deserialize_channel.assert_called_once_with(mock_payload) - - @pytest.mark.asyncio - @_helpers.parametrize_valid_id_formats_for_models("channel", 55555, channels.Channel) - async def test_delete_channel(self, rest_clients_impl, channel): - rest_clients_impl._session.delete_close_channel.return_value = ... - assert await rest_clients_impl.delete_channel(channel) is None - rest_clients_impl._session.delete_close_channel.assert_called_once_with(channel_id="55555") - - @_helpers.parametrize_valid_id_formats_for_models("channel", 123123123, channels.Channel) - @_helpers.parametrize_valid_id_formats_for_models("message", 777777777, messages.Message) - def test_fetch_messages_after_with_optionals(self, rest_clients_impl, channel, message): - mock_generator = mock.AsyncMock() - rest_clients_impl._pagination_handler = mock.MagicMock(return_value=mock_generator) - result = rest_clients_impl.fetch_messages_after(channel=channel, after=message, limit=52) - assert result is mock_generator - rest_clients_impl._pagination_handler.assert_called_once_with( - channel_id="123123123", - deserializer=messages.Message.deserialize, - direction="after", - start="777777777", - request=rest_clients_impl._session.get_channel_messages, - reversing=True, - limit=52, - ) - - @_helpers.parametrize_valid_id_formats_for_models("channel", 123123123, channels.Channel) - def test_fetch_messages_after_without_optionals(self, rest_clients_impl, channel): - mock_generator = mock.AsyncMock() - rest_clients_impl._pagination_handler = mock.MagicMock(return_value=mock_generator) - assert rest_clients_impl.fetch_messages_after(channel=channel) is mock_generator - rest_clients_impl._pagination_handler.assert_called_once_with( - channel_id="123123123", - deserializer=messages.Message.deserialize, - direction="after", - start="0", - request=rest_clients_impl._session.get_channel_messages, - reversing=True, - limit=None, - ) - - def test_fetch_messages_after_with_datetime_object(self, rest_clients_impl): - mock_generator = mock.AsyncMock() - rest_clients_impl._pagination_handler = mock.MagicMock(return_value=mock_generator) - date = datetime.datetime(2019, 1, 22, 18, 41, 15, 283_000, tzinfo=datetime.timezone.utc) - assert rest_clients_impl.fetch_messages_after(channel=123123123, after=date) is mock_generator - rest_clients_impl._pagination_handler.assert_called_once_with( - channel_id="123123123", - deserializer=messages.Message.deserialize, - direction="after", - start="537340988620800000", - request=rest_clients_impl._session.get_channel_messages, - reversing=True, - limit=None, - ) - - @_helpers.parametrize_valid_id_formats_for_models("channel", 123123123, channels.Channel) - @_helpers.parametrize_valid_id_formats_for_models("message", 777777777, messages.Message) - def test_fetch_messages_before_with_optionals(self, rest_clients_impl, channel, message): - mock_generator = mock.AsyncMock() - rest_clients_impl._pagination_handler = mock.MagicMock(return_value=mock_generator) - result = rest_clients_impl.fetch_messages_before(channel=channel, before=message, limit=52) - assert result is mock_generator - rest_clients_impl._pagination_handler.assert_called_once_with( - channel_id="123123123", - deserializer=messages.Message.deserialize, - direction="before", - start="777777777", - request=rest_clients_impl._session.get_channel_messages, - reversing=False, - limit=52, - ) - - @_helpers.parametrize_valid_id_formats_for_models("channel", 123123123, channels.Channel) - def test_fetch_messages_before_without_optionals(self, rest_clients_impl, channel): - mock_generator = mock.AsyncMock() - rest_clients_impl._pagination_handler = mock.MagicMock(return_value=mock_generator) - assert rest_clients_impl.fetch_messages_before(channel=channel) is mock_generator - rest_clients_impl._pagination_handler.assert_called_once_with( - channel_id="123123123", - deserializer=messages.Message.deserialize, - direction="before", - start=None, - request=rest_clients_impl._session.get_channel_messages, - reversing=False, - limit=None, - ) - - def test_fetch_messages_before_with_datetime_object(self, rest_clients_impl): - mock_generator = mock.AsyncMock() - rest_clients_impl._pagination_handler = mock.MagicMock(return_value=mock_generator) - date = datetime.datetime(2019, 1, 22, 18, 41, 15, 283_000, tzinfo=datetime.timezone.utc) - assert rest_clients_impl.fetch_messages_before(channel=123123123, before=date) is mock_generator - rest_clients_impl._pagination_handler.assert_called_once_with( - channel_id="123123123", - deserializer=messages.Message.deserialize, - direction="before", - start="537340988620800000", - request=rest_clients_impl._session.get_channel_messages, - reversing=False, - limit=None, - ) - - @pytest.mark.asyncio - @_helpers.parametrize_valid_id_formats_for_models("channel", 123123123, channels.Channel) - @_helpers.parametrize_valid_id_formats_for_models("message", 777777777, messages.Message) - async def test_fetch_messages_around_with_limit(self, rest_clients_impl, channel, message): - mock_message_payloads = [{"id": "202020", "content": "Nyaa"}, {"id": "2020222", "content": "Nyaa 2"}] - mock_message_objects = [mock.MagicMock(messages.Message), mock.MagicMock(messages.Message)] - rest_clients_impl._session.get_channel_messages.return_value = mock_message_payloads - with mock.patch.object(messages.Message, "deserialize", side_effect=mock_message_objects): - results = [] - async for result in rest_clients_impl.fetch_messages_around(channel, message, limit=2): - results.append(result) - assert results == mock_message_objects - messages.Message.deserialize.assert_has_calls( - [mock.call(mock_message_payloads[0]), mock.call(mock_message_payloads[1])] - ) - rest_clients_impl._session.get_channel_messages.assert_called_once_with( - channel_id="123123123", around="777777777", limit=2 - ) - - @pytest.mark.asyncio - @_helpers.parametrize_valid_id_formats_for_models("channel", 123123123, channels.Channel) - @_helpers.parametrize_valid_id_formats_for_models("message", 777777777, messages.Message) - async def test_fetch_messages_around_without_limit(self, rest_clients_impl, channel, message): - mock_message_payloads = [{"id": "202020", "content": "Nyaa"}, {"id": "2020222", "content": "Nyaa 2"}] - mock_message_objects = [mock.MagicMock(messages.Message), mock.MagicMock(messages.Message)] - rest_clients_impl._session.get_channel_messages.return_value = mock_message_payloads - with mock.patch.object(messages.Message, "deserialize", side_effect=mock_message_objects): - results = [] - async for result in rest_clients_impl.fetch_messages_around(channel, message): - results.append(result) - assert results == mock_message_objects - messages.Message.deserialize.assert_has_calls( - [mock.call(mock_message_payloads[0]), mock.call(mock_message_payloads[1])] - ) - rest_clients_impl._session.get_channel_messages.assert_called_once_with( - channel_id="123123123", around="777777777", limit=... - ) - - @pytest.mark.asyncio - async def test_fetch_messages_around_with_datetime_object(self, rest_clients_impl): - mock_message_payloads = [{"id": "202020", "content": "Nyaa"}, {"id": "2020222", "content": "Nyaa 2"}] - mock_message_objects = [mock.MagicMock(messages.Message), mock.MagicMock(messages.Message)] - rest_clients_impl._session.get_channel_messages.return_value = mock_message_payloads - date = datetime.datetime(2019, 1, 22, 18, 41, 15, 283_000, tzinfo=datetime.timezone.utc) - with mock.patch.object(messages.Message, "deserialize", side_effect=mock_message_objects): - results = [] - async for result in rest_clients_impl.fetch_messages_around(123123123, date): - results.append(result) - assert results == mock_message_objects - messages.Message.deserialize.assert_has_calls( - [mock.call(mock_message_payloads[0]), mock.call(mock_message_payloads[1])] - ) - rest_clients_impl._session.get_channel_messages.assert_called_once_with( - channel_id="123123123", around="537340988620800000", limit=... - ) - - @pytest.mark.asyncio - async def test__pagination_handler_ends_handles_empty_resource(self, rest_clients_impl): - mock_deserialize = mock.MagicMock() - mock_request = mock.AsyncMock(side_effect=[[]]) - async for _ in rest_clients_impl._pagination_handler( - random_kwarg="test", - deserializer=mock_deserialize, - direction="before", - request=mock_request, - reversing=True, - start="123123123", - limit=42, - ): - assert False, "Async generator shouldn't have yielded anything." - mock_request.assert_called_once_with( - limit=42, before="123123123", random_kwarg="test", - ) - mock_deserialize.assert_not_called() - - @pytest.mark.asyncio - async def test__pagination_handler_ends_without_limit_with_start(self, rest_clients_impl): - mock_payloads = [{"id": "312312312"}, {"id": "31231231"}, {"id": "20202020"}] - mock_models = [mock.MagicMock(), mock.MagicMock(), mock.MagicMock(id=20202020)] - mock_deserialize = mock.MagicMock(side_effect=mock_models) - mock_request = mock.AsyncMock(side_effect=[mock_payloads, []]) - results = [] - async for result in rest_clients_impl._pagination_handler( - random_kwarg="test", - deserializer=mock_deserialize, - direction="before", - request=mock_request, - reversing=True, - start="123123123", - limit=None, - ): - results.append(result) - assert results == mock_models - mock_request.assert_has_calls( - [ - mock.call(limit=100, before="123123123", random_kwarg="test"), - mock.call(limit=100, before="20202020", random_kwarg="test"), - ], - ) - mock_deserialize.assert_has_calls( - [mock.call({"id": "20202020"}), mock.call({"id": "31231231"}), mock.call({"id": "312312312"})] - ) - - @pytest.mark.asyncio - async def test__pagination_handler_ends_without_limit_without_start(self, rest_clients_impl): - mock_payloads = [{"id": "312312312"}, {"id": "31231231"}, {"id": "20202020"}] - mock_models = [mock.MagicMock(), mock.MagicMock(), mock.MagicMock(id=20202020)] - mock_deserialize = mock.MagicMock(side_effect=mock_models) - mock_request = mock.AsyncMock(side_effect=[mock_payloads, []]) - results = [] - async for result in rest_clients_impl._pagination_handler( - random_kwarg="test", - deserializer=mock_deserialize, - direction="before", - request=mock_request, - reversing=True, - start=None, - limit=None, - ): - results.append(result) - assert results == mock_models - mock_request.assert_has_calls( - [ - mock.call(limit=100, before=..., random_kwarg="test"), - mock.call(limit=100, before="20202020", random_kwarg="test"), - ], - ) - mock_deserialize.assert_has_calls( - [mock.call({"id": "20202020"}), mock.call({"id": "31231231"}), mock.call({"id": "312312312"})] - ) - - @pytest.mark.asyncio - async def test__pagination_handler_tracks_ends_when_hits_limit(self, rest_clients_impl): - mock_payloads = [{"id": "312312312"}, {"id": "31231231"}] - mock_models = [mock.MagicMock(), mock.MagicMock(id=20202020)] - mock_deserialize = mock.MagicMock(side_effect=mock_models) - mock_request = mock.AsyncMock(side_effect=[mock_payloads, []]) - results = [] - async for result in rest_clients_impl._pagination_handler( - random_kwarg="test", - deserializer=mock_deserialize, - direction="before", - request=mock_request, - reversing=False, - start=None, - limit=2, - ): - results.append(result) - assert results == mock_models - mock_request.assert_called_once_with(limit=2, before=..., random_kwarg="test") - mock_deserialize.assert_has_calls([mock.call({"id": "312312312"}), mock.call({"id": "31231231"})]) - - @pytest.mark.asyncio - async def test__pagination_handler_tracks_ends_when_limit_set_but_exhausts_requested_data(self, rest_clients_impl): - mock_payloads = [{"id": "312312312"}, {"id": "31231231"}, {"id": "20202020"}] - mock_models = [mock.MagicMock(), mock.MagicMock(), mock.MagicMock(id=20202020)] - mock_deserialize = mock.MagicMock(side_effect=mock_models) - mock_request = mock.AsyncMock(side_effect=[mock_payloads, []]) - results = [] - async for result in rest_clients_impl._pagination_handler( - random_kwarg="test", - deserializer=mock_deserialize, - direction="before", - request=mock_request, - reversing=False, - start=None, - limit=42, - ): - results.append(result) - assert results == mock_models - mock_request.assert_has_calls( - [ - mock.call(limit=42, before=..., random_kwarg="test"), - mock.call(limit=39, before="20202020", random_kwarg="test"), - ], - ) - mock_deserialize.assert_has_calls( - [mock.call({"id": "312312312"}), mock.call({"id": "31231231"}), mock.call({"id": "20202020"})] - ) - - @pytest.mark.asyncio - async def test__pagination_handler_reverses_data_when_reverse_is_true(self, rest_clients_impl): - mock_payloads = [{"id": "312312312"}, {"id": "31231231"}, {"id": "20202020"}] - mock_models = [mock.MagicMock(), mock.MagicMock(), mock.MagicMock(id=20202020)] - mock_deserialize = mock.MagicMock(side_effect=mock_models) - mock_request = mock.AsyncMock(side_effect=[mock_payloads, []]) - results = [] - async for result in rest_clients_impl._pagination_handler( - random_kwarg="test", - deserializer=mock_deserialize, - direction="before", - request=mock_request, - reversing=True, - start=None, - limit=None, - ): - results.append(result) - assert results == mock_models - mock_request.assert_has_calls( - [ - mock.call(limit=100, before=..., random_kwarg="test"), - mock.call(limit=100, before="20202020", random_kwarg="test"), - ], - ) - mock_deserialize.assert_has_calls( - [mock.call({"id": "20202020"}), mock.call({"id": "31231231"}), mock.call({"id": "312312312"})] - ) - - @pytest.mark.asyncio - async def test__pagination_handler_id_getter(self, rest_clients_impl): - mock_payloads = [{"id": "312312312"}, {"id": "20202020"}] - mock_models = [mock.MagicMock(), mock.MagicMock(user=mock.MagicMock(__int__=lambda x: 20202020))] - mock_deserialize = mock.MagicMock(side_effect=mock_models) - mock_request = mock.AsyncMock(side_effect=[mock_payloads, []]) - results = [] - async for result in rest_clients_impl._pagination_handler( - random_kwarg="test", - deserializer=mock_deserialize, - direction="before", - request=mock_request, - reversing=False, - start=None, - id_getter=lambda entity: str(int(entity.user)), - limit=None, - ): - results.append(result) - assert results == mock_models - mock_request.assert_has_calls( - [ - mock.call(limit=100, before=..., random_kwarg="test"), - mock.call(limit=100, before="20202020", random_kwarg="test"), - ], - ) - mock_deserialize.assert_has_calls([mock.call({"id": "312312312"}), mock.call({"id": "20202020"})]) - - @pytest.mark.asyncio - async def test__pagination_handler_handles_no_initial_data(self, rest_clients_impl): - mock_deserialize = mock.MagicMock() - mock_request = mock.AsyncMock(side_effect=[[]]) - async for _ in rest_clients_impl._pagination_handler( - random_kwarg="test", - deserializer=mock_deserialize, - direction="before", - request=mock_request, - reversing=True, - start=None, - limit=None, - ): - assert False, "Async generator shouldn't have yielded anything." - mock_request.assert_called_once_with( - limit=100, before=..., random_kwarg="test", - ) - mock_deserialize.assert_not_called() - - @pytest.mark.asyncio - @_helpers.parametrize_valid_id_formats_for_models("channel", 55555, channels.Channel) - @_helpers.parametrize_valid_id_formats_for_models("message", 565656, messages.Message) - async def test_fetch_message(self, rest_clients_impl, channel, message): - mock_payload = {"id": "9409404", "content": "I AM A MESSAGE!"} - mock_message_obj = mock.MagicMock(messages.Message) - rest_clients_impl._session.get_channel_message.return_value = mock_payload - with mock.patch.object(messages.Message, "deserialize", return_value=mock_message_obj): - assert await rest_clients_impl.fetch_message(channel=channel, message=message) is mock_message_obj - rest_clients_impl._session.get_channel_message.assert_called_once_with( - channel_id="55555", message_id="565656", - ) - messages.Message.deserialize.assert_called_once_with(mock_payload) - - @pytest.mark.parametrize( - ("kwargs", "expected_result"), - [ - ( - {"mentions_everyone": True, "user_mentions": True, "role_mentions": True}, - {"parse": ["everyone", "users", "roles"]}, - ), - ( - {"mentions_everyone": False, "user_mentions": False, "role_mentions": False}, - {"parse": [], "users": [], "roles": []}, - ), - ( - {"mentions_everyone": True, "user_mentions": ["1123123"], "role_mentions": True}, - {"parse": ["everyone", "roles"], "users": ["1123123"]}, - ), - ( - {"mentions_everyone": True, "user_mentions": True, "role_mentions": ["1231123"]}, - {"parse": ["everyone", "users"], "roles": ["1231123"]}, - ), - ( - {"mentions_everyone": False, "user_mentions": ["1123123"], "role_mentions": True}, - {"parse": ["roles"], "users": ["1123123"]}, - ), - ( - {"mentions_everyone": False, "user_mentions": True, "role_mentions": ["1231123"]}, - {"parse": ["users"], "roles": ["1231123"]}, - ), - ( - {"mentions_everyone": False, "user_mentions": ["1123123"], "role_mentions": False}, - {"parse": [], "roles": [], "users": ["1123123"]}, - ), - ( - {"mentions_everyone": False, "user_mentions": False, "role_mentions": ["1231123"]}, - {"parse": [], "roles": ["1231123"], "users": []}, - ), - ( - {"mentions_everyone": False, "user_mentions": ["22222"], "role_mentions": ["1231123"]}, - {"parse": [], "users": ["22222"], "roles": ["1231123"]}, - ), - ( - {"mentions_everyone": True, "user_mentions": ["22222"], "role_mentions": ["1231123"]}, - {"parse": ["everyone"], "users": ["22222"], "roles": ["1231123"]}, - ), - ], - ) - def test_generate_allowed_mentions(self, rest_clients_impl, kwargs, expected_result): - assert rest_clients_impl._generate_allowed_mentions(**kwargs) == expected_result - - @_helpers.parametrize_valid_id_formats_for_models("role", 3, guilds.GuildRole) - def test_generate_allowed_mentions_removes_duplicate_role_ids(self, rest_clients_impl, role): - result = rest_clients_impl._generate_allowed_mentions( - role_mentions=["1", "2", "1", "3", "5", "7", "2", role], user_mentions=True, mentions_everyone=True - ) - assert result == {"roles": ["1", "2", "3", "5", "7"], "parse": ["everyone", "users"]} - - @_helpers.parametrize_valid_id_formats_for_models("user", 3, users.User) - def test_generate_allowed_mentions_removes_duplicate_user_ids(self, rest_clients_impl, user): - result = rest_clients_impl._generate_allowed_mentions( - role_mentions=True, user_mentions=["1", "2", "1", "3", "5", "7", "2", user], mentions_everyone=True - ) - assert result == {"users": ["1", "2", "3", "5", "7"], "parse": ["everyone", "roles"]} - - @_helpers.parametrize_valid_id_formats_for_models("role", 190007233919057920, guilds.GuildRole) - def test_generate_allowed_mentions_handles_all_role_formats(self, rest_clients_impl, role): - result = rest_clients_impl._generate_allowed_mentions( - role_mentions=[role], user_mentions=True, mentions_everyone=True - ) - assert result == {"roles": ["190007233919057920"], "parse": ["everyone", "users"]} - - @_helpers.parametrize_valid_id_formats_for_models("user", 190007233919057920, users.User) - def test_generate_allowed_mentions_handles_all_user_formats(self, rest_clients_impl, user): - result = rest_clients_impl._generate_allowed_mentions( - role_mentions=True, user_mentions=[user], mentions_everyone=True - ) - assert result == {"users": ["190007233919057920"], "parse": ["everyone", "roles"]} - - @_helpers.assert_raises(type_=ValueError) - def test_generate_allowed_mentions_raises_error_on_too_many_roles(self, rest_clients_impl): - rest_clients_impl._generate_allowed_mentions( - user_mentions=False, role_mentions=list(range(101)), mentions_everyone=False - ) - - @_helpers.assert_raises(type_=ValueError) - def test_generate_allowed_mentions_raises_error_on_too_many_users(self, rest_clients_impl): - rest_clients_impl._generate_allowed_mentions( - user_mentions=list(range(101)), role_mentions=False, mentions_everyone=False - ) - - @pytest.mark.asyncio - @_helpers.parametrize_valid_id_formats_for_models("channel", 694463529998352394, channels.Channel) - async def test_create_message_with_optionals(self, rest_clients_impl, channel): - mock_message_obj = mock.MagicMock(messages.Message) - mock_message_payload = {"id": "2929292992", "content": "222922"} - rest_clients_impl._session.create_message.return_value = mock_message_payload - mock_allowed_mentions_payload = {"parse": ["everyone", "users", "roles"]} - rest_clients_impl._generate_allowed_mentions = mock.MagicMock(return_value=mock_allowed_mentions_payload) - mock_embed_payload = {"description": "424242"} - mock_embed_obj = mock.MagicMock(embeds.Embed) - mock_embed_obj.serialize = mock.MagicMock(return_value=mock_embed_payload) - mock_media_obj = mock.MagicMock() - mock_media_payload = ("aName.png", mock.MagicMock()) - stack = contextlib.ExitStack() - stack.enter_context(mock.patch.object(messages.Message, "deserialize", return_value=mock_message_obj)) - stack.enter_context(mock.patch.object(media, "safe_read_file", return_value=mock_media_payload)) - with stack: - result = await rest_clients_impl.create_message( - channel, - content="A CONTENT", - nonce="69696969696969", - tts=True, - files=[mock_media_obj], - embed=mock_embed_obj, - mentions_everyone=False, - user_mentions=False, - role_mentions=False, - ) - assert result is mock_message_obj - media.safe_read_file.assert_called_once_with(mock_media_obj) - messages.Message.deserialize.assert_called_once_with(mock_message_payload) - rest_clients_impl._session.create_message.assert_called_once_with( - channel_id="694463529998352394", - content="A CONTENT", - nonce="69696969696969", - tts=True, - files=[mock_media_payload], - embed=mock_embed_payload, - allowed_mentions=mock_allowed_mentions_payload, - ) - mock_embed_obj.serialize.assert_called_once() - rest_clients_impl._generate_allowed_mentions.assert_called_once_with( - mentions_everyone=False, user_mentions=False, role_mentions=False - ) - - @pytest.mark.asyncio - @_helpers.parametrize_valid_id_formats_for_models("channel", 694463529998352394, channels.Channel) - async def test_create_message_without_optionals(self, rest_clients_impl, channel): - mock_message_obj = mock.MagicMock(messages.Message) - mock_message_payload = {"id": "2929292992", "content": "222922"} - rest_clients_impl._session.create_message.return_value = mock_message_payload - mock_allowed_mentions_payload = {"parse": ["everyone", "users", "roles"]} - rest_clients_impl._generate_allowed_mentions = mock.MagicMock(return_value=mock_allowed_mentions_payload) - with mock.patch.object(messages.Message, "deserialize", return_value=mock_message_obj): - assert await rest_clients_impl.create_message(channel) is mock_message_obj - messages.Message.deserialize.assert_called_once_with(mock_message_payload) - rest_clients_impl._session.create_message.assert_called_once_with( - channel_id="694463529998352394", - content=..., - nonce=..., - tts=..., - files=..., - embed=..., - allowed_mentions=mock_allowed_mentions_payload, - ) - rest_clients_impl._generate_allowed_mentions.assert_called_once_with( - mentions_everyone=True, user_mentions=True, role_mentions=True - ) - - @pytest.mark.asyncio - async def test_safe_create_message_without_optionals(self, rest_clients_impl): - channel = mock.MagicMock(channels.Channel) - mock_message_obj = mock.MagicMock(messages.Message) - rest_clients_impl.create_message = mock.AsyncMock(return_value=mock_message_obj) - result = await rest_clients_impl.safe_create_message(channel,) - assert result is mock_message_obj - rest_clients_impl.create_message.assert_called_once_with( - channel=channel, - content=..., - nonce=..., - tts=..., - files=..., - embed=..., - mentions_everyone=False, - user_mentions=False, - role_mentions=False, - ) - - @pytest.mark.asyncio - async def test_safe_create_message_with_optionals(self, rest_clients_impl): - channel = mock.MagicMock(channels.Channel) - mock_embed_obj = mock.MagicMock(embeds.Embed) - mock_message_obj = mock.MagicMock(messages.Message) - mock_media_obj = mock.MagicMock(bytes) - rest_clients_impl.create_message = mock.AsyncMock(return_value=mock_message_obj) - result = await rest_clients_impl.safe_create_message( - channel=channel, - content="A CONTENT", - nonce="69696969696969", - tts=True, - files=[mock_media_obj], - embed=mock_embed_obj, - mentions_everyone=True, - user_mentions=True, - role_mentions=True, - ) - assert result is mock_message_obj - rest_clients_impl.create_message.assert_called_once_with( - channel=channel, - content="A CONTENT", - nonce="69696969696969", - tts=True, - files=[mock_media_obj], - embed=mock_embed_obj, - mentions_everyone=True, - user_mentions=True, - role_mentions=True, - ) - - @pytest.mark.asyncio - @_helpers.parametrize_valid_id_formats_for_models("channel", 213123, channels.Channel) - @_helpers.parametrize_valid_id_formats_for_models("message", 987654321, messages.Message) - @pytest.mark.parametrize("emoji", ["blah:123", emojis.UnknownEmoji(name="blah", id=123, is_animated=False)]) - async def test_create_reaction(self, rest_clients_impl, channel, message, emoji): - rest_clients_impl._session.create_reaction.return_value = ... - assert await rest_clients_impl.create_reaction(channel=channel, message=message, emoji=emoji) is None - rest_clients_impl._session.create_reaction.assert_called_once_with( - channel_id="213123", message_id="987654321", emoji="blah:123", - ) - - @pytest.mark.asyncio - @_helpers.parametrize_valid_id_formats_for_models("channel", 213123, channels.Channel) - @_helpers.parametrize_valid_id_formats_for_models("message", 987654321, messages.Message) - @pytest.mark.parametrize("emoji", ["blah:123", emojis.UnknownEmoji(name="blah", id=123, is_animated=False)]) - async def test_delete_reaction(self, rest_clients_impl, channel, message, emoji): - rest_clients_impl._session.delete_own_reaction.return_value = ... - assert await rest_clients_impl.delete_reaction(channel=channel, message=message, emoji=emoji) is None - rest_clients_impl._session.delete_own_reaction.assert_called_once_with( - channel_id="213123", message_id="987654321", emoji="blah:123", - ) - - @pytest.mark.asyncio - @_helpers.parametrize_valid_id_formats_for_models("channel", 213123, channels.Channel) - @_helpers.parametrize_valid_id_formats_for_models("message", 987654321, messages.Message) - async def test_delete_all_reactions(self, rest_clients_impl, channel, message): - rest_clients_impl._session.delete_all_reactions.return_value = ... - assert await rest_clients_impl.delete_all_reactions(channel=channel, message=message) is None - rest_clients_impl._session.delete_all_reactions.assert_called_once_with( - channel_id="213123", message_id="987654321", - ) - - @pytest.mark.asyncio - @_helpers.parametrize_valid_id_formats_for_models("channel", 213123, channels.Channel) - @_helpers.parametrize_valid_id_formats_for_models("message", 987654321, messages.Message) - @pytest.mark.parametrize("emoji", ["blah:123", emojis.UnknownEmoji(name="blah", id=123, is_animated=False)]) - async def test_delete_all_reactions_for_emoji(self, rest_clients_impl, channel, message, emoji): - rest_clients_impl._session.delete_all_reactions_for_emoji.return_value = ... - assert ( - await rest_clients_impl.delete_all_reactions_for_emoji(channel=channel, message=message, emoji=emoji) - is None - ) - rest_clients_impl._session.delete_all_reactions_for_emoji.assert_called_once_with( - channel_id="213123", message_id="987654321", emoji="blah:123", - ) - - @_helpers.parametrize_valid_id_formats_for_models("message", 432, messages.Message) - @_helpers.parametrize_valid_id_formats_for_models("channel", 123, channels.Channel) - @pytest.mark.parametrize( - "emoji", ["tutu1:456371206225002499", mock.MagicMock(emojis.GuildEmoji, url_name="tutu1:456371206225002499")] - ) - @_helpers.parametrize_valid_id_formats_for_models("user", 140502780547694592, users.User) - def test_fetch_reactors_after_with_optionals(self, rest_clients_impl, message, channel, emoji, user): - mock_generator = mock.AsyncMock() - rest_clients_impl._pagination_handler = mock.MagicMock(return_value=mock_generator) - result = rest_clients_impl.fetch_reactors_after(channel, message, emoji, after=user, limit=47) - assert result is mock_generator - rest_clients_impl._pagination_handler.assert_called_once_with( - channel_id="123", - message_id="432", - emoji="tutu1:456371206225002499", - deserializer=users.User.deserialize, - direction="after", - request=rest_clients_impl._session.get_reactions, - reversing=False, - start="140502780547694592", - limit=47, - ) - - @_helpers.parametrize_valid_id_formats_for_models("message", 432, messages.Message) - @_helpers.parametrize_valid_id_formats_for_models("channel", 123, channels.Channel) - @pytest.mark.parametrize( - "emoji", ["tutu1:456371206225002499", mock.MagicMock(emojis.GuildEmoji, url_name="tutu1:456371206225002499")] - ) - def test_fetch_reactors_after_without_optionals(self, rest_clients_impl, message, channel, emoji): - mock_generator = mock.AsyncMock() - rest_clients_impl._pagination_handler = mock.MagicMock(return_value=mock_generator) - assert rest_clients_impl.fetch_reactors_after(channel, message, emoji) is mock_generator - rest_clients_impl._pagination_handler.assert_called_once_with( - channel_id="123", - message_id="432", - emoji="tutu1:456371206225002499", - deserializer=users.User.deserialize, - direction="after", - request=rest_clients_impl._session.get_reactions, - reversing=False, - start="0", - limit=None, - ) - - def test_fetch_reactors_after_with_datetime_object(self, rest_clients_impl): - mock_generator = mock.AsyncMock() - rest_clients_impl._pagination_handler = mock.MagicMock(return_value=mock_generator) - date = datetime.datetime(2019, 1, 22, 18, 41, 15, 283_000, tzinfo=datetime.timezone.utc) - result = rest_clients_impl.fetch_reactors_after(123, 432, "tutu1:456371206225002499", after=date) - assert result is mock_generator - rest_clients_impl._pagination_handler.assert_called_once_with( - channel_id="123", - message_id="432", - emoji="tutu1:456371206225002499", - deserializer=users.User.deserialize, - direction="after", - request=rest_clients_impl._session.get_reactions, - reversing=False, - start="537340988620800000", - limit=None, - ) - - @pytest.mark.asyncio - @_helpers.parametrize_valid_id_formats_for_models("message", 432, messages.Message) - @_helpers.parametrize_valid_id_formats_for_models("channel", 123, channels.Channel) - async def test_update_message_with_optionals(self, rest_clients_impl, message, channel): - mock_payload = {"id": "4242", "content": "I HAVE BEEN UPDATED!"} - mock_message_obj = mock.MagicMock(messages.Message) - mock_embed_payload = {"description": "blahblah"} - mock_embed = mock.MagicMock(embeds.Embed) - mock_embed.serialize = mock.MagicMock(return_value=mock_embed_payload) - mock_allowed_mentions_payload = {"parse": [], "users": ["123"]} - rest_clients_impl._generate_allowed_mentions = mock.MagicMock(return_value=mock_allowed_mentions_payload) - rest_clients_impl._session.edit_message.return_value = mock_payload - with mock.patch.object(messages.Message, "deserialize", return_value=mock_message_obj): - result = await rest_clients_impl.update_message( - message=message, - channel=channel, - content="C O N T E N T", - embed=mock_embed, - flags=messages.MessageFlag.IS_CROSSPOST | messages.MessageFlag.SUPPRESS_EMBEDS, - mentions_everyone=False, - role_mentions=False, - user_mentions=[123123123], - ) - assert result is mock_message_obj - rest_clients_impl._session.edit_message.assert_called_once_with( - channel_id="123", - message_id="432", - content="C O N T E N T", - embed=mock_embed_payload, - flags=6, - allowed_mentions=mock_allowed_mentions_payload, - ) - mock_embed.serialize.assert_called_once() - messages.Message.deserialize.assert_called_once_with(mock_payload) - rest_clients_impl._generate_allowed_mentions.assert_called_once_with( - mentions_everyone=False, role_mentions=False, user_mentions=[123123123] - ) - - @pytest.mark.asyncio - @_helpers.parametrize_valid_id_formats_for_models("message", 432, messages.Message) - @_helpers.parametrize_valid_id_formats_for_models("channel", 123, channels.Channel) - async def test_update_message_without_optionals(self, rest_clients_impl, message, channel): - mock_payload = {"id": "4242", "content": "I HAVE BEEN UPDATED!"} - mock_message_obj = mock.MagicMock(messages.Message) - mock_allowed_mentions_payload = {"parse": ["everyone", "users", "roles"]} - rest_clients_impl._generate_allowed_mentions = mock.MagicMock(return_value=mock_allowed_mentions_payload) - rest_clients_impl._session.edit_message.return_value = mock_payload - with mock.patch.object(messages.Message, "deserialize", return_value=mock_message_obj): - assert await rest_clients_impl.update_message(message=message, channel=channel) is mock_message_obj - rest_clients_impl._session.edit_message.assert_called_once_with( - channel_id="123", - message_id="432", - content=..., - embed=..., - flags=..., - allowed_mentions=mock_allowed_mentions_payload, - ) - messages.Message.deserialize.assert_called_once_with(mock_payload) - rest_clients_impl._generate_allowed_mentions.assert_called_once_with( - mentions_everyone=True, user_mentions=True, role_mentions=True - ) - - @pytest.mark.asyncio - async def test_safe_update_message_without_optionals(self, rest_clients_impl): - message = mock.MagicMock(messages.Message) - channel = mock.MagicMock(channels.Channel) - mock_message_obj = mock.MagicMock(messages.Message) - rest_clients_impl.update_message = mock.AsyncMock(return_value=mock_message_obj) - result = await rest_clients_impl.safe_update_message(message=message, channel=channel,) - assert result is mock_message_obj - rest_clients_impl.update_message.assert_called_once_with( - message=message, - channel=channel, - content=..., - embed=..., - flags=..., - mentions_everyone=False, - role_mentions=False, - user_mentions=False, - ) - - @pytest.mark.asyncio - async def test_safe_update_message_with_optionals(self, rest_clients_impl): - message = mock.MagicMock(messages.Message) - channel = mock.MagicMock(channels.Channel) - mock_embed = mock.MagicMock(embeds.Embed) - mock_message_obj = mock.MagicMock(messages.Message) - rest_clients_impl.update_message = mock.AsyncMock(return_value=mock_message_obj) - result = await rest_clients_impl.safe_update_message( - message=message, - channel=channel, - content="C O N T E N T", - embed=mock_embed, - flags=messages.MessageFlag.IS_CROSSPOST | messages.MessageFlag.SUPPRESS_EMBEDS, - mentions_everyone=True, - role_mentions=True, - user_mentions=True, - ) - assert result is mock_message_obj - rest_clients_impl.update_message.assert_called_once_with( - message=message, - channel=channel, - content="C O N T E N T", - embed=mock_embed, - flags=messages.MessageFlag.IS_CROSSPOST | messages.MessageFlag.SUPPRESS_EMBEDS, - mentions_everyone=True, - role_mentions=True, - user_mentions=True, - ) - - @pytest.mark.asyncio - @_helpers.parametrize_valid_id_formats_for_models("channel", 379953393319542784, channels.Channel) - @_helpers.parametrize_valid_id_formats_for_models("message", 115590097100865541, messages.Message) - async def test_delete_messages_singular(self, rest_clients_impl, channel, message): - rest_clients_impl._session.delete_message.return_value = ... - assert await rest_clients_impl.delete_messages(channel, message) is None - rest_clients_impl._session.delete_message.assert_called_once_with( - channel_id="379953393319542784", message_id="115590097100865541", - ) - - @pytest.mark.asyncio - @_helpers.parametrize_valid_id_formats_for_models("channel", 379953393319542784, channels.Channel) - @_helpers.parametrize_valid_id_formats_for_models("message", 115590097100865541, messages.Message) - @_helpers.parametrize_valid_id_formats_for_models("additional_message", 115590097100865541, messages.Message) - async def test_delete_messages_singular_after_duplicate_removal( - self, rest_clients_impl, channel, message, additional_message - ): - rest_clients_impl._session.delete_message.return_value = ... - assert await rest_clients_impl.delete_messages(channel, message, additional_message) is None - rest_clients_impl._session.delete_message.assert_called_once_with( - channel_id="379953393319542784", message_id="115590097100865541", - ) - - @pytest.mark.asyncio - @_helpers.parametrize_valid_id_formats_for_models("channel", 379953393319542784, channels.Channel) - @_helpers.parametrize_valid_id_formats_for_models("message", 115590097100865541, messages.Message) - @_helpers.parametrize_valid_id_formats_for_models("additional_message", 572144340277919754, messages.Message) - async def test_delete_messages_bulk_removes_duplicates( - self, rest_clients_impl, channel, message, additional_message - ): - rest_clients_impl._session.bulk_delete_messages.return_value = ... - assert await rest_clients_impl.delete_messages(channel, message, additional_message, 115590097100865541) is None - rest_clients_impl._session.bulk_delete_messages.assert_called_once_with( - channel_id="379953393319542784", messages=["115590097100865541", "572144340277919754"], - ) - rest_clients_impl._session.delete_message.assert_not_called() - - @pytest.mark.asyncio - @_helpers.assert_raises(type_=ValueError) - async def test_delete_messages_raises_value_error_on_over_100_messages(self, rest_clients_impl): - rest_clients_impl._session.bulk_delete_messages.return_value = ... - assert await rest_clients_impl.delete_messages(123123, *list(range(0, 111))) is None - - @pytest.mark.asyncio - @_helpers.parametrize_valid_id_formats_for_models("channel", 4123123, channels.Channel) - @_helpers.parametrize_valid_id_formats_for_models("overwrite", 9999, channels.PermissionOverwrite) - async def test_update_channel_overwrite_with_optionals(self, rest_clients_impl, channel, overwrite): - rest_clients_impl._session.edit_channel_permissions.return_value = ... - result = await rest_clients_impl.update_channel_overwrite( - channel=channel, - overwrite=overwrite, - target_type="member", - allow=messages.MessageFlag.IS_CROSSPOST | messages.MessageFlag.SUPPRESS_EMBEDS, - deny=21, - reason="get Nyaa'd", - ) - assert result is None - rest_clients_impl._session.edit_channel_permissions.assert_called_once_with( - channel_id="4123123", overwrite_id="9999", type_="member", allow=6, deny=21, reason="get Nyaa'd", - ) - - @pytest.mark.asyncio - @_helpers.parametrize_valid_id_formats_for_models("channel", 4123123, channels.Channel) - @_helpers.parametrize_valid_id_formats_for_models("overwrite", 9999, channels.PermissionOverwrite) - async def test_update_channel_overwrite_without_optionals(self, rest_clients_impl, channel, overwrite): - rest_clients_impl._session.edit_channel_permissions.return_value = ... - result = await rest_clients_impl.update_channel_overwrite( - channel=channel, overwrite=overwrite, target_type="member" - ) - assert result is None - rest_clients_impl._session.edit_channel_permissions.assert_called_once_with( - channel_id="4123123", overwrite_id="9999", type_="member", allow=..., deny=..., reason=..., - ) - - @pytest.mark.asyncio - @pytest.mark.parametrize( - "target", - [ - mock.MagicMock(guilds.GuildRole, id=snowflakes.Snowflake(9999), __int__=guilds.GuildRole.__int__), - mock.MagicMock(users.User, id=snowflakes.Snowflake(9999), __int__=users.User.__int__), - ], - ) - async def test_update_channel_overwrite_with_alternative_target_object(self, rest_clients_impl, target): - rest_clients_impl._session.edit_channel_permissions.return_value = ... - result = await rest_clients_impl.update_channel_overwrite( - channel=4123123, overwrite=target, target_type="member" - ) - assert result is None - rest_clients_impl._session.edit_channel_permissions.assert_called_once_with( - channel_id="4123123", overwrite_id="9999", type_="member", allow=..., deny=..., reason=..., - ) - - @pytest.mark.asyncio - @_helpers.parametrize_valid_id_formats_for_models("channel", 123123123, channels.Channel) - async def test_fetch_invites_for_channel(self, rest_clients_impl, channel): - mock_invite_payload = {"code": "ogogogogogogogo", "guild_id": "123123123"} - mock_invite_obj = mock.MagicMock(invites.InviteWithMetadata) - rest_clients_impl._session.get_channel_invites.return_value = [mock_invite_payload] - with mock.patch.object(invites.InviteWithMetadata, "deserialize", return_value=mock_invite_obj): - assert await rest_clients_impl.fetch_invites_for_channel(channel=channel) == [mock_invite_obj] - rest_clients_impl._session.get_channel_invites.assert_called_once_with(channel_id="123123123") - invites.InviteWithMetadata.deserialize.assert_called_once_with(mock_invite_payload) - - @pytest.mark.asyncio - @_helpers.parametrize_valid_id_formats_for_models("channel", 234123, channels.Channel) - @_helpers.parametrize_valid_id_formats_for_models("user", 333333, users.User) - @pytest.mark.parametrize("max_age", [4444, datetime.timedelta(seconds=4444)]) - async def test_create_invite_for_channel_with_optionals(self, rest_clients_impl, channel, user, max_age): - mock_invite_payload = {"code": "ogogogogogogogo", "guild_id": "123123123"} - mock_invite_obj = mock.MagicMock(invites.InviteWithMetadata) - rest_clients_impl._session.create_channel_invite.return_value = mock_invite_payload - with mock.patch.object(invites.InviteWithMetadata, "deserialize", return_value=mock_invite_obj): - result = await rest_clients_impl.create_invite_for_channel( - channel, - max_age=max_age, - max_uses=444, - temporary=True, - unique=False, - target_user=user, - target_user_type=invites.TargetUserType.STREAM, - reason="Hello there.", - ) - assert result is mock_invite_obj - rest_clients_impl._session.create_channel_invite.assert_called_once_with( - channel_id="234123", - max_age=4444, - max_uses=444, - temporary=True, - unique=False, - target_user="333333", - target_user_type=1, - reason="Hello there.", - ) - invites.InviteWithMetadata.deserialize.assert_called_once_with(mock_invite_payload) - - @pytest.mark.asyncio - @_helpers.parametrize_valid_id_formats_for_models("channel", 234123, channels.Channel) - async def test_create_invite_for_channel_without_optionals(self, rest_clients_impl, channel): - mock_invite_payload = {"code": "ogogogogogogogo", "guild_id": "123123123"} - mock_invite_obj = mock.MagicMock(invites.InviteWithMetadata) - rest_clients_impl._session.create_channel_invite.return_value = mock_invite_payload - with mock.patch.object(invites.InviteWithMetadata, "deserialize", return_value=mock_invite_obj): - assert await rest_clients_impl.create_invite_for_channel(channel) is mock_invite_obj - rest_clients_impl._session.create_channel_invite.assert_called_once_with( - channel_id="234123", - max_age=..., - max_uses=..., - temporary=..., - unique=..., - target_user=..., - target_user_type=..., - reason=..., - ) - invites.InviteWithMetadata.deserialize.assert_called_once_with(mock_invite_payload) - - @pytest.mark.asyncio - @_helpers.parametrize_valid_id_formats_for_models("channel", 379953393319542784, channels.Channel) - @_helpers.parametrize_valid_id_formats_for_models("overwrite", 123123123, channels.PermissionOverwrite) - async def test_delete_channel_overwrite(self, rest_clients_impl, channel, overwrite): - rest_clients_impl._session.delete_channel_permission.return_value = ... - assert await rest_clients_impl.delete_channel_overwrite(channel=channel, overwrite=overwrite) is None - rest_clients_impl._session.delete_channel_permission.assert_called_once_with( - channel_id="379953393319542784", overwrite_id="123123123", - ) - - @pytest.mark.asyncio - @pytest.mark.parametrize( - "target", - [ - mock.MagicMock(guilds.GuildRole, id=snowflakes.Snowflake(123123123), __int__=guilds.GuildRole.__int__), - mock.MagicMock(users.User, id=snowflakes.Snowflake(123123123), __int__=users.User.__int__), - ], - ) - async def test_delete_channel_overwrite_with_alternative_target_objects(self, rest_clients_impl, target): - rest_clients_impl._session.delete_channel_permission.return_value = ... - assert await rest_clients_impl.delete_channel_overwrite(channel=379953393319542784, overwrite=target) is None - rest_clients_impl._session.delete_channel_permission.assert_called_once_with( - channel_id="379953393319542784", overwrite_id="123123123", - ) - - @pytest.mark.asyncio - @_helpers.parametrize_valid_id_formats_for_models("channel", 379953393319542784, channels.PermissionOverwrite) - async def test_trigger_typing(self, rest_clients_impl, channel): - rest_clients_impl._session.trigger_typing_indicator.return_value = ... - assert await rest_clients_impl.trigger_typing(channel) is None - rest_clients_impl._session.trigger_typing_indicator.assert_called_once_with(channel_id="379953393319542784") - - @pytest.mark.asyncio - @_helpers.parametrize_valid_id_formats_for_models("channel", 123123123, channels.Channel) - async def test_fetch_pins(self, rest_clients_impl, channel): - mock_message_payload = {"id": "21232", "content": "CONTENT"} - mock_message_obj = mock.MagicMock(messages.Message, id=21232) - rest_clients_impl._session.get_pinned_messages.return_value = [mock_message_payload] - with mock.patch.object(messages.Message, "deserialize", return_value=mock_message_obj): - assert await rest_clients_impl.fetch_pins(channel) == {21232: mock_message_obj} - rest_clients_impl._session.get_pinned_messages.assert_called_once_with(channel_id="123123123") - messages.Message.deserialize.assert_called_once_with(mock_message_payload) - - @pytest.mark.asyncio - @_helpers.parametrize_valid_id_formats_for_models("channel", 292929, channels.Channel) - @_helpers.parametrize_valid_id_formats_for_models("message", 123123, messages.Message) - async def test_pin_message(self, rest_clients_impl, channel, message): - rest_clients_impl._session.add_pinned_channel_message.return_value = ... - assert await rest_clients_impl.pin_message(channel, message) is None - rest_clients_impl._session.add_pinned_channel_message.assert_called_once_with( - channel_id="292929", message_id="123123" - ) - - @pytest.mark.asyncio - @_helpers.parametrize_valid_id_formats_for_models("channel", 292929, channels.Channel) - @_helpers.parametrize_valid_id_formats_for_models("message", 123123, messages.Message) - async def test_unpin_message(self, rest_clients_impl, channel, message): - rest_clients_impl._session.delete_pinned_channel_message.return_value = ... - assert await rest_clients_impl.unpin_message(channel, message) is None - rest_clients_impl._session.delete_pinned_channel_message.assert_called_once_with( - channel_id="292929", message_id="123123" - ) - - @pytest.mark.asyncio - @_helpers.parametrize_valid_id_formats_for_models("guild", 93443949, guilds.Guild) - @_helpers.parametrize_valid_id_formats_for_models("emoji", 40404040404, emojis.GuildEmoji) - async def test_fetch_guild_emoji(self, rest_clients_impl, guild, emoji): - mock_emoji_payload = {"id": "92929", "name": "nyaa", "animated": True} - mock_emoji_obj = mock.MagicMock(emojis.GuildEmoji) - rest_clients_impl._session.get_guild_emoji.return_value = mock_emoji_payload - with mock.patch.object(emojis.GuildEmoji, "deserialize", return_value=mock_emoji_obj): - assert await rest_clients_impl.fetch_guild_emoji(guild=guild, emoji=emoji) is mock_emoji_obj - rest_clients_impl._session.get_guild_emoji.assert_called_once_with( - guild_id="93443949", emoji_id="40404040404", - ) - emojis.GuildEmoji.deserialize.assert_called_once_with(mock_emoji_payload) - - @pytest.mark.asyncio - @_helpers.parametrize_valid_id_formats_for_models("guild", 93443949, guilds.Guild) - async def test_fetch_guild_emojis(self, rest_clients_impl, guild): - mock_emoji_payload = {"id": "92929", "name": "nyaa", "animated": True} - mock_emoji_obj = mock.MagicMock(emojis.GuildEmoji) - rest_clients_impl._session.list_guild_emojis.return_value = [mock_emoji_payload] - with mock.patch.object(emojis.GuildEmoji, "deserialize", return_value=mock_emoji_obj): - assert await rest_clients_impl.fetch_guild_emojis(guild=guild) == [mock_emoji_obj] - rest_clients_impl._session.list_guild_emojis.assert_called_once_with(guild_id="93443949",) - emojis.GuildEmoji.deserialize.assert_called_once_with(mock_emoji_payload) - - @pytest.mark.asyncio - @_helpers.parametrize_valid_id_formats_for_models("guild", 93443949, guilds.Guild) - @_helpers.parametrize_valid_id_formats_for_models("role", 537340989808050216, guilds.GuildRole) - async def test_create_guild_emoji_with_optionals(self, rest_clients_impl, guild, role): - mock_emoji_payload = {"id": "229292929", "animated": True} - mock_emoji_obj = mock.MagicMock(emojis.GuildEmoji) - rest_clients_impl._session.create_guild_emoji.return_value = mock_emoji_payload - mock_image_obj = mock.MagicMock(io.BytesIO) - mock_image_data = mock.MagicMock(bytes) - stack = contextlib.ExitStack() - stack.enter_context(mock.patch.object(conversions, "get_bytes_from_resource", return_value=mock_image_data)) - stack.enter_context(mock.patch.object(emojis.GuildEmoji, "deserialize", return_value=mock_emoji_obj)) - with stack: - result = await rest_clients_impl.create_guild_emoji( - guild=guild, name="fairEmoji", image_data=mock_image_obj, roles=[role], reason="hello", - ) - assert result is mock_emoji_obj - emojis.GuildEmoji.deserialize.assert_called_once_with(mock_emoji_payload) - conversions.get_bytes_from_resource.assert_called_once_with(mock_image_obj) - rest_clients_impl._session.create_guild_emoji.assert_called_once_with( - guild_id="93443949", name="fairEmoji", image=mock_image_data, roles=["537340989808050216"], reason="hello", - ) - - @pytest.mark.asyncio - @_helpers.parametrize_valid_id_formats_for_models("guild", 93443949, guilds.Guild) - async def test_create_guild_emoji_without_optionals(self, rest_clients_impl, guild): - mock_emoji_payload = {"id": "229292929", "animated": True} - mock_emoji_obj = mock.MagicMock(emojis.GuildEmoji) - rest_clients_impl._session.create_guild_emoji.return_value = mock_emoji_payload - mock_image_obj = mock.MagicMock(io.BytesIO) - mock_image_data = mock.MagicMock(bytes) - stack = contextlib.ExitStack() - stack.enter_context(mock.patch.object(conversions, "get_bytes_from_resource", return_value=mock_image_data)) - stack.enter_context(mock.patch.object(emojis.GuildEmoji, "deserialize", return_value=mock_emoji_obj)) - with stack: - result = await rest_clients_impl.create_guild_emoji( - guild=guild, name="fairEmoji", image_data=mock_image_obj, - ) - assert result is mock_emoji_obj - emojis.GuildEmoji.deserialize.assert_called_once_with(mock_emoji_payload) - conversions.get_bytes_from_resource.assert_called_once_with(mock_image_obj) - rest_clients_impl._session.create_guild_emoji.assert_called_once_with( - guild_id="93443949", name="fairEmoji", image=mock_image_data, roles=..., reason=..., - ) - - @pytest.mark.asyncio - @_helpers.parametrize_valid_id_formats_for_models("guild", 93443949, guilds.Guild) - @_helpers.parametrize_valid_id_formats_for_models("emoji", 4123321, emojis.GuildEmoji) - async def test_update_guild_emoji_without_optionals(self, rest_clients_impl, guild, emoji): - mock_emoji_payload = {"id": "202020", "name": "Nyaa", "animated": True} - mock_emoji_obj = mock.MagicMock(emojis.GuildEmoji) - rest_clients_impl._session.modify_guild_emoji.return_value = mock_emoji_payload - with mock.patch.object(emojis.GuildEmoji, "deserialize", return_value=mock_emoji_obj): - assert await rest_clients_impl.update_guild_emoji(guild, emoji) is mock_emoji_obj - rest_clients_impl._session.modify_guild_emoji.assert_called_once_with( - guild_id="93443949", emoji_id="4123321", name=..., roles=..., reason=..., - ) - emojis.GuildEmoji.deserialize.assert_called_once_with(mock_emoji_payload) - - @pytest.mark.asyncio - @_helpers.parametrize_valid_id_formats_for_models("guild", 93443949, guilds.Guild) - @_helpers.parametrize_valid_id_formats_for_models("emoji", 4123321, emojis.GuildEmoji) - @_helpers.parametrize_valid_id_formats_for_models("role", 123123123, guilds.GuildRole) - async def test_update_guild_emoji_with_optionals(self, rest_clients_impl, guild, emoji, role): - mock_emoji_payload = {"id": "202020", "name": "Nyaa", "animated": True} - mock_emoji_obj = mock.MagicMock(emojis.GuildEmoji) - rest_clients_impl._session.modify_guild_emoji.return_value = mock_emoji_payload - with mock.patch.object(emojis.GuildEmoji, "deserialize", return_value=mock_emoji_obj): - result = await rest_clients_impl.update_guild_emoji( - guild, emoji, name="Nyaa", roles=[role], reason="Agent 42" - ) - assert result is mock_emoji_obj - rest_clients_impl._session.modify_guild_emoji.assert_called_once_with( - guild_id="93443949", emoji_id="4123321", name="Nyaa", roles=["123123123"], reason="Agent 42", - ) - emojis.GuildEmoji.deserialize.assert_called_once_with(mock_emoji_payload) - - @pytest.mark.asyncio - @_helpers.parametrize_valid_id_formats_for_models("guild", 93443949, guilds.Guild) - @_helpers.parametrize_valid_id_formats_for_models("emoji", 4123321, emojis.GuildEmoji) - async def test_delete_guild_emoji(self, rest_clients_impl, guild, emoji): - rest_clients_impl._session.delete_guild_emoji.return_value = ... - assert await rest_clients_impl.delete_guild_emoji(guild, emoji) is None - rest_clients_impl._session.delete_guild_emoji.assert_called_once_with(guild_id="93443949", emoji_id="4123321") - - @pytest.mark.asyncio - @pytest.mark.parametrize("region", [mock.MagicMock(voices.VoiceRegion, id="LONDON"), "LONDON"]) - async def test_create_guild_with_optionals(self, rest_clients_impl, region): - mock_guild_payload = {"id": "299292929292992", "region": "LONDON"} - mock_guild_obj = mock.MagicMock(guilds.Guild) - rest_clients_impl._session.create_guild.return_value = mock_guild_payload - mock_image_obj = mock.MagicMock(io.BytesIO) - mock_image_data = mock.MagicMock(bytes) - mock_role_payload = {"permissions": 123123} - mock_role_obj = mock.MagicMock(guilds.GuildRole) - mock_role_obj.serialize = mock.MagicMock(return_value=mock_role_payload) - mock_channel_payload = {"type": 2, "name": "aChannel"} - mock_channel_obj = mock.MagicMock(channels.GuildChannelBuilder) - mock_channel_obj.serialize = mock.MagicMock(return_value=mock_channel_payload) - stack = contextlib.ExitStack() - stack.enter_context(mock.patch.object(guilds.Guild, "deserialize", return_value=mock_guild_obj)) - stack.enter_context(mock.patch.object(conversions, "get_bytes_from_resource", return_value=mock_image_data)) - with stack: - result = await rest_clients_impl.create_guild( - name="OK", - region=region, - icon_data=mock_image_obj, - verification_level=guilds.GuildVerificationLevel.NONE, - default_message_notifications=guilds.GuildMessageNotificationsLevel.ONLY_MENTIONS, - explicit_content_filter=guilds.GuildExplicitContentFilterLevel.MEMBERS_WITHOUT_ROLES, - roles=[mock_role_obj], - channels=[mock_channel_obj], - ) - assert result is mock_guild_obj - conversions.get_bytes_from_resource.assert_called_once_with(mock_image_obj) - guilds.Guild.deserialize.assert_called_once_with(mock_guild_payload) - mock_channel_obj.serialize.assert_called_once() - mock_role_obj.serialize.assert_called_once() - rest_clients_impl._session.create_guild.assert_called_once_with( - name="OK", - region="LONDON", - icon=mock_image_data, - verification_level=0, - default_message_notifications=1, - explicit_content_filter=1, - roles=[mock_role_payload], - channels=[mock_channel_payload], - ) - - @pytest.mark.asyncio - async def test_create_guild_without_optionals(self, rest_clients_impl): - mock_guild_payload = {"id": "299292929292992", "region": "LONDON"} - mock_guild_obj = mock.MagicMock(guilds.Guild) - rest_clients_impl._session.create_guild.return_value = mock_guild_payload - with mock.patch.object(guilds.Guild, "deserialize", return_value=mock_guild_obj): - assert await rest_clients_impl.create_guild(name="OK") is mock_guild_obj - guilds.Guild.deserialize.assert_called_once_with(mock_guild_payload) - rest_clients_impl._session.create_guild.assert_called_once_with( - name="OK", - region=..., - icon=..., - verification_level=..., - default_message_notifications=..., - explicit_content_filter=..., - roles=..., - channels=..., - ) - - @pytest.mark.asyncio - @_helpers.parametrize_valid_id_formats_for_models("guild", 379953393319542784, guilds.Guild) - async def test_fetch_guild(self, rest_clients_impl, guild): - mock_guild_payload = {"id": "94949494", "name": "A guild", "roles": []} - mock_guild_obj = mock.MagicMock(guilds.Guild) - rest_clients_impl._session.get_guild.return_value = mock_guild_payload - with mock.patch.object(guilds.Guild, "deserialize", return_value=mock_guild_obj): - assert await rest_clients_impl.fetch_guild(guild) is mock_guild_obj - rest_clients_impl._session.get_guild.assert_called_once_with(guild_id="379953393319542784") - guilds.Guild.deserialize.assert_called_once_with(mock_guild_payload) - - @pytest.mark.asyncio - @_helpers.parametrize_valid_id_formats_for_models("guild", 379953393319542784, guilds.Guild) - async def test_fetch_guild_preview(self, rest_clients_impl, guild): - mock_guild_preview_payload = {"id": "94949494", "name": "A guild", "emojis": []} - mock_guild_preview_obj = mock.MagicMock(guilds.GuildPreview) - rest_clients_impl._session.get_guild_preview.return_value = mock_guild_preview_payload - with mock.patch.object(guilds.GuildPreview, "deserialize", return_value=mock_guild_preview_obj): - assert await rest_clients_impl.fetch_guild_preview(guild) is mock_guild_preview_obj - rest_clients_impl._session.get_guild_preview.assert_called_once_with(guild_id="379953393319542784") - guilds.GuildPreview.deserialize.assert_called_once_with(mock_guild_preview_payload) - - @pytest.mark.asyncio - @_helpers.parametrize_valid_id_formats_for_models("guild", 379953393319542784, guilds.Guild) - @_helpers.parametrize_valid_id_formats_for_models("afk_channel", 669517187031105607, guilds.Guild) - @_helpers.parametrize_valid_id_formats_for_models("owner", 379953393319542784, users.User) - @_helpers.parametrize_valid_id_formats_for_models("system_channel", 537340989808050216, users.User) - @pytest.mark.parametrize("region", ["LONDON", mock.MagicMock(voices.VoiceRegion, id="LONDON")]) - @pytest.mark.parametrize("afk_timeout", [300, datetime.timedelta(seconds=300)]) - async def test_update_guild_with_optionals( - self, rest_clients_impl, guild, region, afk_channel, afk_timeout, owner, system_channel - ): - mock_guild_payload = {"id": "424242", "splash": "2lmKmklsdlksalkd"} - mock_guild_obj = mock.MagicMock(guilds.Guild) - rest_clients_impl._session.modify_guild.return_value = mock_guild_payload - mock_icon_data = mock.MagicMock(bytes) - mock_icon_obj = mock.MagicMock(io.BytesIO) - mock_splash_data = mock.MagicMock(bytes) - mock_splash_obj = mock.MagicMock(io.BytesIO) - stack = contextlib.ExitStack() - stack.enter_context(mock.patch.object(guilds.Guild, "deserialize", return_value=mock_guild_obj)) - stack.enter_context( - mock.patch.object(conversions, "get_bytes_from_resource", side_effect=[mock_icon_data, mock_splash_data]) - ) - with stack: - result = await rest_clients_impl.update_guild( - guild, - name="aNewName", - region=region, - verification_level=guilds.GuildVerificationLevel.LOW, - default_message_notifications=guilds.GuildMessageNotificationsLevel.ONLY_MENTIONS, - explicit_content_filter=guilds.GuildExplicitContentFilterLevel.ALL_MEMBERS, - afk_channel=afk_channel, - afk_timeout=afk_timeout, - icon_data=mock_icon_obj, - owner=owner, - splash_data=mock_splash_obj, - system_channel=system_channel, - reason="A good reason", - ) - assert result is mock_guild_obj - guilds.Guild.deserialize.assert_called_once_with(mock_guild_payload) - conversions.get_bytes_from_resource.has_calls(mock.call(mock_icon_obj), mock.call(mock_splash_obj)) - rest_clients_impl._session.modify_guild.assert_called_once_with( - guild_id="379953393319542784", - name="aNewName", - region="LONDON", - verification_level=1, - default_message_notifications=1, - explicit_content_filter=2, - afk_channel_id="669517187031105607", - afk_timeout=300, - icon=mock_icon_data, - owner_id="379953393319542784", - splash=mock_splash_data, - system_channel_id="537340989808050216", - reason="A good reason", - ) - - @pytest.mark.asyncio - @_helpers.parametrize_valid_id_formats_for_models("guild", 379953393319542784, guilds.Guild) - async def test_update_guild_without_optionals(self, rest_clients_impl, guild): - mock_guild_payload = {"id": "424242", "splash": "2lmKmklsdlksalkd"} - mock_guild_obj = mock.MagicMock(guilds.Guild) - rest_clients_impl._session.modify_guild.return_value = mock_guild_payload - with mock.patch.object(guilds.Guild, "deserialize", return_value=mock_guild_obj): - assert await rest_clients_impl.update_guild(guild) is mock_guild_obj - rest_clients_impl._session.modify_guild.assert_called_once_with( - guild_id="379953393319542784", - name=..., - region=..., - verification_level=..., - default_message_notifications=..., - explicit_content_filter=..., - afk_channel_id=..., - afk_timeout=..., - icon=..., - owner_id=..., - splash=..., - system_channel_id=..., - reason=..., - ) - - @pytest.mark.asyncio - @_helpers.parametrize_valid_id_formats_for_models("guild", 379953393319542784, guilds.Guild) - async def test_delete_guild(self, rest_clients_impl, guild): - rest_clients_impl._session.delete_guild.return_value = ... - assert await rest_clients_impl.delete_guild(guild) is None - rest_clients_impl._session.delete_guild.assert_called_once_with(guild_id="379953393319542784") - - @pytest.mark.asyncio - @_helpers.parametrize_valid_id_formats_for_models("guild", 379953393319542784, guilds.Guild) - async def test_fetch_guild_channels(self, rest_clients_impl, guild): - mock_channel_payload = {"id": "292929", "type": 1, "description": "A CHANNEL"} - mock_channel_obj = mock.MagicMock(channels.GuildChannel) - rest_clients_impl._session.list_guild_channels.return_value = [mock_channel_payload] - with mock.patch.object(channels, "deserialize_channel", return_value=mock_channel_obj): - assert await rest_clients_impl.fetch_guild_channels(guild) == [mock_channel_obj] - rest_clients_impl._session.list_guild_channels.assert_called_once_with(guild_id="379953393319542784") - channels.deserialize_channel.assert_called_once_with(mock_channel_payload) - - @pytest.mark.asyncio - @_helpers.parametrize_valid_id_formats_for_models("guild", 123123123, guilds.Guild) - @_helpers.parametrize_valid_id_formats_for_models("category", 5555, channels.GuildCategory) - @pytest.mark.parametrize("rate_limit_per_user", [500, datetime.timedelta(seconds=500)]) - async def test_create_guild_channel_with_optionals(self, rest_clients_impl, guild, category, rate_limit_per_user): - mock_channel_payload = {"id": "22929292", "type": "5", "description": "A C H A N N E L"} - mock_channel_obj = mock.MagicMock(channels.GuildChannel) - mock_overwrite_payload = {"type": "member", "id": "30303030"} - mock_overwrite_obj = mock.MagicMock( - channels.PermissionOverwrite, serialize=mock.MagicMock(return_value=mock_overwrite_payload) - ) - rest_clients_impl._session.create_guild_channel.return_value = mock_channel_payload - with mock.patch.object(channels, "deserialize_channel", return_value=mock_channel_obj): - result = await rest_clients_impl.create_guild_channel( - guild, - "Hi-i-am-a-name", - channel_type=channels.ChannelType.GUILD_VOICE, - position=42, - topic="A TOPIC", - nsfw=True, - rate_limit_per_user=rate_limit_per_user, - bitrate=36000, - user_limit=5, - permission_overwrites=[mock_overwrite_obj], - parent_category=category, - reason="A GOOD REASON!", - ) - assert result is mock_channel_obj - mock_overwrite_obj.serialize.assert_called_once() - rest_clients_impl._session.create_guild_channel.assert_called_once_with( - guild_id="123123123", - name="Hi-i-am-a-name", - type_=2, - position=42, - topic="A TOPIC", - nsfw=True, - rate_limit_per_user=500, - bitrate=36000, - user_limit=5, - permission_overwrites=[mock_overwrite_payload], - parent_id="5555", - reason="A GOOD REASON!", - ) - channels.deserialize_channel.assert_called_once_with(mock_channel_payload) - - @pytest.mark.asyncio - @_helpers.parametrize_valid_id_formats_for_models("guild", 123123123, guilds.Guild) - async def test_create_guild_channel_without_optionals(self, rest_clients_impl, guild): - mock_channel_payload = {"id": "22929292", "type": "5", "description": "A C H A N N E L"} - mock_channel_obj = mock.MagicMock(channels.GuildChannel) - rest_clients_impl._session.create_guild_channel.return_value = mock_channel_payload - with mock.patch.object(channels, "deserialize_channel", return_value=mock_channel_obj): - assert await rest_clients_impl.create_guild_channel(guild, "Hi-i-am-a-name") is mock_channel_obj - rest_clients_impl._session.create_guild_channel.assert_called_once_with( - guild_id="123123123", - name="Hi-i-am-a-name", - type_=..., - position=..., - topic=..., - nsfw=..., - rate_limit_per_user=..., - bitrate=..., - user_limit=..., - permission_overwrites=..., - parent_id=..., - reason=..., - ) - channels.deserialize_channel.assert_called_once_with(mock_channel_payload) - - @pytest.mark.asyncio - @_helpers.parametrize_valid_id_formats_for_models("guild", 123123123, guilds.Guild) - @_helpers.parametrize_valid_id_formats_for_models("channel", 379953393319542784, channels.GuildChannel) - @_helpers.parametrize_valid_id_formats_for_models("second_channel", 115590097100865541, channels.GuildChannel) - async def test_reposition_guild_channels(self, rest_clients_impl, guild, channel, second_channel): - rest_clients_impl._session.modify_guild_channel_positions.return_value = ... - assert await rest_clients_impl.reposition_guild_channels(guild, (1, channel), (2, second_channel)) is None - rest_clients_impl._session.modify_guild_channel_positions.assert_called_once_with( - "123123123", ("379953393319542784", 1), ("115590097100865541", 2) - ) - - @pytest.mark.asyncio - @_helpers.parametrize_valid_id_formats_for_models("guild", 444444, guilds.Guild) - @_helpers.parametrize_valid_id_formats_for_models("user", 123123123123, users.User) - async def test_fetch_member(self, rest_clients_impl, guild, user): - mock_member_payload = {"user": {}, "nick": "! Agent 47"} - mock_member_obj = mock.MagicMock(guilds.GuildMember) - rest_clients_impl._session.get_guild_member.return_value = mock_member_payload - with mock.patch.object(guilds.GuildMember, "deserialize", return_value=mock_member_obj): - assert await rest_clients_impl.fetch_member(guild, user) is mock_member_obj - rest_clients_impl._session.get_guild_member.assert_called_once_with( - guild_id="444444", user_id="123123123123" - ) - guilds.GuildMember.deserialize.assert_called_once_with(mock_member_payload) - - @_helpers.parametrize_valid_id_formats_for_models("guild", 574921006817476608, guilds.Guild) - @_helpers.parametrize_valid_id_formats_for_models("user", 115590097100865541, users.User) - def test_fetch_members_after_with_optionals(self, rest_clients_impl, guild, user): - mock_generator = mock.AsyncMock() - rest_clients_impl._pagination_handler = mock.MagicMock(return_value=mock_generator) - assert rest_clients_impl.fetch_members_after(guild, after=user, limit=34) is mock_generator - rest_clients_impl._pagination_handler.assert_called_once_with( - guild_id="574921006817476608", - deserializer=guilds.GuildMember.deserialize, - direction="after", - request=rest_clients_impl._session.list_guild_members, - reversing=False, - start="115590097100865541", - limit=34, - id_getter=rest_clients._get_member_id, - ) - - @_helpers.parametrize_valid_id_formats_for_models("guild", 574921006817476608, guilds.Guild) - def test_fetch_members_after_without_optionals(self, rest_clients_impl, guild): - mock_generator = mock.AsyncMock() - rest_clients_impl._pagination_handler = mock.MagicMock(return_value=mock_generator) - assert rest_clients_impl.fetch_members_after(guild) is mock_generator - rest_clients_impl._pagination_handler.assert_called_once_with( - guild_id="574921006817476608", - deserializer=guilds.GuildMember.deserialize, - direction="after", - request=rest_clients_impl._session.list_guild_members, - reversing=False, - start="0", - limit=None, - id_getter=rest_clients._get_member_id, - ) - - def test_fetch_members_after_with_datetime_object(self, rest_clients_impl): - mock_generator = mock.AsyncMock() - rest_clients_impl._pagination_handler = mock.MagicMock(return_value=mock_generator) - date = datetime.datetime(2019, 1, 22, 18, 41, 15, 283_000, tzinfo=datetime.timezone.utc) - assert rest_clients_impl.fetch_members_after(574921006817476608, after=date) is mock_generator - rest_clients_impl._pagination_handler.assert_called_once_with( - guild_id="574921006817476608", - deserializer=guilds.GuildMember.deserialize, - direction="after", - request=rest_clients_impl._session.list_guild_members, - reversing=False, - start="537340988620800000", - limit=None, - id_getter=rest_clients._get_member_id, - ) - - @pytest.mark.asyncio - @_helpers.parametrize_valid_id_formats_for_models("guild", 229292992, guilds.Guild) - @_helpers.parametrize_valid_id_formats_for_models("user", 1010101010, users.User) - @_helpers.parametrize_valid_id_formats_for_models("role", 11100010, guilds.GuildRole) - @_helpers.parametrize_valid_id_formats_for_models("channel", 33333333, channels.GuildVoiceChannel) - async def test_update_member_with_optionals(self, rest_clients_impl, guild, user, role, channel): - rest_clients_impl._session.modify_guild_member.return_value = ... - result = await rest_clients_impl.update_member( - guild, - user, - nickname="Nick's Name", - roles=[role], - mute=True, - deaf=False, - voice_channel=channel, - reason="Get Tagged.", - ) - assert result is None - rest_clients_impl._session.modify_guild_member.assert_called_once_with( - guild_id="229292992", - user_id="1010101010", - nick="Nick's Name", - roles=["11100010"], - mute=True, - deaf=False, - channel_id="33333333", - reason="Get Tagged.", - ) - - @pytest.mark.asyncio - @_helpers.parametrize_valid_id_formats_for_models("guild", 229292992, guilds.Guild) - @_helpers.parametrize_valid_id_formats_for_models("user", 1010101010, users.User) - async def test_update_member_without_optionals(self, rest_clients_impl, guild, user): - rest_clients_impl._session.modify_guild_member.return_value = ... - assert await rest_clients_impl.update_member(guild, user) is None - rest_clients_impl._session.modify_guild_member.assert_called_once_with( - guild_id="229292992", - user_id="1010101010", - nick=..., - roles=..., - mute=..., - deaf=..., - channel_id=..., - reason=..., - ) - - @pytest.mark.asyncio - @_helpers.parametrize_valid_id_formats_for_models("guild", 229292992, guilds.Guild) - async def test_update_my_member_nickname_with_reason(self, rest_clients_impl, guild): - rest_clients_impl._session.modify_current_user_nick.return_value = ... - result = await rest_clients_impl.update_my_member_nickname( - guild, "Nick's nick", reason="I want to drink your blood." - ) - assert result is None - rest_clients_impl._session.modify_current_user_nick.assert_called_once_with( - guild_id="229292992", nick="Nick's nick", reason="I want to drink your blood." - ) - - @pytest.mark.asyncio - @_helpers.parametrize_valid_id_formats_for_models("guild", 229292992, guilds.Guild) - async def test_update_my_member_nickname_without_reason(self, rest_clients_impl, guild): - rest_clients_impl._session.modify_current_user_nick.return_value = ... - assert await rest_clients_impl.update_my_member_nickname(guild, "Nick's nick") is None - rest_clients_impl._session.modify_current_user_nick.assert_called_once_with( - guild_id="229292992", nick="Nick's nick", reason=... - ) - - @pytest.mark.asyncio - @_helpers.parametrize_valid_id_formats_for_models("guild", 123123123, guilds.Guild) - @_helpers.parametrize_valid_id_formats_for_models("user", 4444444, users.User) - @_helpers.parametrize_valid_id_formats_for_models("role", 101010101, guilds.GuildRole) - async def test_add_role_to_member_with_reason(self, rest_clients_impl, guild, user, role): - rest_clients_impl._session.add_guild_member_role.return_value = ... - assert await rest_clients_impl.add_role_to_member(guild, user, role, reason="Get role'd") is None - rest_clients_impl._session.add_guild_member_role.assert_called_once_with( - guild_id="123123123", user_id="4444444", role_id="101010101", reason="Get role'd" - ) - - @pytest.mark.asyncio - @_helpers.parametrize_valid_id_formats_for_models("guild", 123123123, guilds.Guild) - @_helpers.parametrize_valid_id_formats_for_models("user", 4444444, users.User) - @_helpers.parametrize_valid_id_formats_for_models("role", 101010101, guilds.GuildRole) - async def test_add_role_to_member_without_reason(self, rest_clients_impl, guild, user, role): - rest_clients_impl._session.add_guild_member_role.return_value = ... - assert await rest_clients_impl.add_role_to_member(guild, user, role) is None - rest_clients_impl._session.add_guild_member_role.assert_called_once_with( - guild_id="123123123", user_id="4444444", role_id="101010101", reason=... - ) - - @pytest.mark.asyncio - @_helpers.parametrize_valid_id_formats_for_models("guild", 123123123, guilds.Guild) - @_helpers.parametrize_valid_id_formats_for_models("user", 4444444, users.User) - @_helpers.parametrize_valid_id_formats_for_models("role", 101010101, guilds.GuildRole) - async def test_remove_role_from_member_with_reason(self, rest_clients_impl, guild, user, role): - rest_clients_impl._session.remove_guild_member_role.return_value = ... - assert await rest_clients_impl.remove_role_from_member(guild, user, role, reason="Get role'd") is None - rest_clients_impl._session.remove_guild_member_role.assert_called_once_with( - guild_id="123123123", user_id="4444444", role_id="101010101", reason="Get role'd" - ) - - @pytest.mark.asyncio - @_helpers.parametrize_valid_id_formats_for_models("guild", 123123123, guilds.Guild) - @_helpers.parametrize_valid_id_formats_for_models("user", 4444444, users.User) - @_helpers.parametrize_valid_id_formats_for_models("role", 101010101, guilds.GuildRole) - async def test_remove_role_from_member_without_reason(self, rest_clients_impl, guild, user, role): - rest_clients_impl._session.remove_guild_member_role.return_value = ... - assert await rest_clients_impl.remove_role_from_member(guild, user, role) is None - rest_clients_impl._session.remove_guild_member_role.assert_called_once_with( - guild_id="123123123", user_id="4444444", role_id="101010101", reason=... - ) - - @pytest.mark.asyncio - @_helpers.parametrize_valid_id_formats_for_models("guild", 123123123, guilds.Guild) - @_helpers.parametrize_valid_id_formats_for_models("user", 4444444, users.User) - async def test_kick_member_with_reason(self, rest_clients_impl, guild, user): - rest_clients_impl._session.remove_guild_member.return_value = ... - assert await rest_clients_impl.kick_member(guild, user, reason="TO DO") is None - rest_clients_impl._session.remove_guild_member.assert_called_once_with( - guild_id="123123123", user_id="4444444", reason="TO DO" - ) - - @pytest.mark.asyncio - @_helpers.parametrize_valid_id_formats_for_models("guild", 123123123, guilds.Guild) - @_helpers.parametrize_valid_id_formats_for_models("user", 4444444, users.User) - async def test_kick_member_without_reason(self, rest_clients_impl, guild, user): - rest_clients_impl._session.remove_guild_member.return_value = ... - assert await rest_clients_impl.kick_member(guild, user) is None - rest_clients_impl._session.remove_guild_member.assert_called_once_with( - guild_id="123123123", user_id="4444444", reason=..., - ) - - @pytest.mark.asyncio - @_helpers.parametrize_valid_id_formats_for_models("guild", 123123123, guilds.Guild) - @_helpers.parametrize_valid_id_formats_for_models("user", 4444444, users.User) - async def test_fetch_ban(self, rest_clients_impl, guild, user): - mock_ban_payload = {"reason": "42'd", "user": {}} - mock_ban_obj = mock.MagicMock(guilds.GuildMemberBan) - rest_clients_impl._session.get_guild_ban.return_value = mock_ban_payload - with mock.patch.object(guilds.GuildMemberBan, "deserialize", return_value=mock_ban_obj): - assert await rest_clients_impl.fetch_ban(guild, user) is mock_ban_obj - rest_clients_impl._session.get_guild_ban.assert_called_once_with(guild_id="123123123", user_id="4444444") - guilds.GuildMemberBan.deserialize.assert_called_once_with(mock_ban_payload) - - @pytest.mark.asyncio - @_helpers.parametrize_valid_id_formats_for_models("guild", 123123123, guilds.Guild) - async def test_fetch_bans(self, rest_clients_impl, guild): - mock_ban_payload = {"reason": "42'd", "user": {}} - mock_ban_obj = mock.MagicMock(guilds.GuildMemberBan) - rest_clients_impl._session.get_guild_bans.return_value = [mock_ban_payload] - with mock.patch.object(guilds.GuildMemberBan, "deserialize", return_value=mock_ban_obj): - assert await rest_clients_impl.fetch_bans(guild) == [mock_ban_obj] - rest_clients_impl._session.get_guild_bans.assert_called_once_with(guild_id="123123123") - guilds.GuildMemberBan.deserialize.assert_called_once_with(mock_ban_payload) - - @pytest.mark.asyncio - @_helpers.parametrize_valid_id_formats_for_models("guild", 123123123, guilds.Guild) - @_helpers.parametrize_valid_id_formats_for_models("user", 4444444, users.User) - @pytest.mark.parametrize("delete_message_days", [datetime.timedelta(days=12), 12]) - async def test_ban_member_with_optionals(self, rest_clients_impl, guild, user, delete_message_days): - rest_clients_impl._session.create_guild_ban.return_value = ... - result = await rest_clients_impl.ban_member(guild, user, delete_message_days=delete_message_days, reason="bye") - assert result is None - rest_clients_impl._session.create_guild_ban.assert_called_once_with( - guild_id="123123123", user_id="4444444", delete_message_days=12, reason="bye" - ) - - @pytest.mark.asyncio - @_helpers.parametrize_valid_id_formats_for_models("guild", 123123123, guilds.Guild) - @_helpers.parametrize_valid_id_formats_for_models("user", 4444444, users.User) - async def test_ban_member_without_optionals(self, rest_clients_impl, guild, user): - rest_clients_impl._session.create_guild_ban.return_value = ... - assert await rest_clients_impl.ban_member(guild, user) is None - rest_clients_impl._session.create_guild_ban.assert_called_once_with( - guild_id="123123123", user_id="4444444", delete_message_days=..., reason=... - ) - - @pytest.mark.asyncio - @_helpers.parametrize_valid_id_formats_for_models("guild", 123123123, guilds.Guild) - @_helpers.parametrize_valid_id_formats_for_models("user", 4444444, users.User) - async def test_unban_member_with_reason(self, rest_clients_impl, guild, user): - rest_clients_impl._session.remove_guild_ban.return_value = ... - result = await rest_clients_impl.unban_member(guild, user, reason="bye") - assert result is None - rest_clients_impl._session.remove_guild_ban.assert_called_once_with( - guild_id="123123123", user_id="4444444", reason="bye" - ) - - @pytest.mark.asyncio - @_helpers.parametrize_valid_id_formats_for_models("guild", 123123123, guilds.Guild) - @_helpers.parametrize_valid_id_formats_for_models("user", 4444444, users.User) - async def test_unban_member_without_reason(self, rest_clients_impl, guild, user): - rest_clients_impl._session.remove_guild_ban.return_value = ... - assert await rest_clients_impl.unban_member(guild, user) is None - rest_clients_impl._session.remove_guild_ban.assert_called_once_with( - guild_id="123123123", user_id="4444444", reason=... - ) - - @pytest.mark.asyncio - @_helpers.parametrize_valid_id_formats_for_models("guild", 574921006817476608, guilds.Guild) - async def test_fetch_roles(self, rest_clients_impl, guild): - mock_role_payload = {"id": "33030", "permissions": 333, "name": "ROlE"} - mock_role_obj = mock.MagicMock(guilds.GuildRole, id=33030) - rest_clients_impl._session.get_guild_roles.return_value = [mock_role_payload] - with mock.patch.object(guilds.GuildRole, "deserialize", return_value=mock_role_obj): - assert await rest_clients_impl.fetch_roles(guild) == {33030: mock_role_obj} - rest_clients_impl._session.get_guild_roles.assert_called_once_with(guild_id="574921006817476608") - guilds.GuildRole.deserialize.assert_called_once_with(mock_role_payload) - - @pytest.mark.asyncio - @_helpers.parametrize_valid_id_formats_for_models("guild", 574921006817476608, guilds.Guild) - async def test_create_role_with_optionals(self, rest_clients_impl, guild): - mock_role_payload = {"id": "033030", "permissions": 333, "name": "ROlE"} - mock_role_obj = mock.MagicMock(guilds.GuildRole) - rest_clients_impl._session.create_guild_role.return_value = mock_role_payload - with mock.patch.object(guilds.GuildRole, "deserialize", return_value=mock_role_obj): - result = await rest_clients_impl.create_role( - guild, - name="Roleington", - permissions=permissions.Permission.STREAM | permissions.Permission.EMBED_LINKS, - color=colors.Color(21312), - hoist=True, - mentionable=False, - reason="And then there was a role.", - ) - assert result is mock_role_obj - rest_clients_impl._session.create_guild_role.assert_called_once_with( - guild_id="574921006817476608", - name="Roleington", - permissions=16896, - color=21312, - hoist=True, - mentionable=False, - reason="And then there was a role.", - ) - guilds.GuildRole.deserialize.assert_called_once_with(mock_role_payload) - - @pytest.mark.asyncio - @_helpers.parametrize_valid_id_formats_for_models("guild", 574921006817476608, guilds.Guild) - async def test_create_role_without_optionals(self, rest_clients_impl, guild): - mock_role_payload = {"id": "033030", "permissions": 333, "name": "ROlE"} - mock_role_obj = mock.MagicMock(guilds.GuildRole) - rest_clients_impl._session.create_guild_role.return_value = mock_role_payload - with mock.patch.object(guilds.GuildRole, "deserialize", return_value=mock_role_obj): - result = await rest_clients_impl.create_role(guild) - assert result is mock_role_obj - rest_clients_impl._session.create_guild_role.assert_called_once_with( - guild_id="574921006817476608", - name=..., - permissions=..., - color=..., - hoist=..., - mentionable=..., - reason=..., - ) - guilds.GuildRole.deserialize.assert_called_once_with(mock_role_payload) - - @pytest.mark.asyncio - @_helpers.parametrize_valid_id_formats_for_models("guild", 574921006817476608, guilds.Guild) - @_helpers.parametrize_valid_id_formats_for_models("role", 123123, guilds.GuildRole) - @_helpers.parametrize_valid_id_formats_for_models("additional_role", 123456, guilds.GuildRole) - async def test_reposition_roles(self, rest_clients_impl, guild, role, additional_role): - mock_role_payload = {"id": "033030", "permissions": 333, "name": "ROlE"} - mock_role_obj = mock.MagicMock(guilds.GuildRole) - rest_clients_impl._session.modify_guild_role_positions.return_value = [mock_role_payload] - with mock.patch.object(guilds.GuildRole, "deserialize", return_value=mock_role_obj): - result = await rest_clients_impl.reposition_roles(guild, (1, role), (2, additional_role)) - assert result == [mock_role_obj] - rest_clients_impl._session.modify_guild_role_positions.assert_called_once_with( - "574921006817476608", ("123123", 1), ("123456", 2) - ) - guilds.GuildRole.deserialize.assert_called_once_with(mock_role_payload) - - @pytest.mark.asyncio - @_helpers.parametrize_valid_id_formats_for_models("guild", 574921006817476608, guilds.Guild) - @_helpers.parametrize_valid_id_formats_for_models("role", 123123, guilds.GuildRole) - async def test_update_role_with_optionals(self, rest_clients_impl, guild, role): - mock_role_payload = {"id": "033030", "permissions": 333, "name": "ROlE"} - mock_role_obj = mock.MagicMock(guilds.GuildRole) - rest_clients_impl._session.modify_guild_role.return_value = mock_role_payload - with mock.patch.object(guilds.GuildRole, "deserialize", return_value=mock_role_obj): - result = await rest_clients_impl.update_role( - guild, - role, - name="ROLE", - permissions=permissions.Permission.STREAM | permissions.Permission.EMBED_LINKS, - color=colors.Color(12312), - hoist=True, - mentionable=False, - reason="Why not?", - ) - assert result is mock_role_obj - rest_clients_impl._session.modify_guild_role.assert_called_once_with( - guild_id="574921006817476608", - role_id="123123", - name="ROLE", - permissions=16896, - color=12312, - hoist=True, - mentionable=False, - reason="Why not?", - ) - guilds.GuildRole.deserialize.assert_called_once_with(mock_role_payload) - - @pytest.mark.asyncio - @_helpers.parametrize_valid_id_formats_for_models("guild", 574921006817476608, guilds.Guild) - @_helpers.parametrize_valid_id_formats_for_models("role", 123123, guilds.GuildRole) - async def test_update_role_without_optionals(self, rest_clients_impl, guild, role): - mock_role_payload = {"id": "033030", "permissions": 333, "name": "ROlE"} - mock_role_obj = mock.MagicMock(guilds.GuildRole) - rest_clients_impl._session.modify_guild_role.return_value = mock_role_payload - with mock.patch.object(guilds.GuildRole, "deserialize", return_value=mock_role_obj): - assert await rest_clients_impl.update_role(guild, role) is mock_role_obj - rest_clients_impl._session.modify_guild_role.assert_called_once_with( - guild_id="574921006817476608", - role_id="123123", - name=..., - permissions=..., - color=..., - hoist=..., - mentionable=..., - reason=..., - ) - guilds.GuildRole.deserialize.assert_called_once_with(mock_role_payload) - - @pytest.mark.asyncio - @_helpers.parametrize_valid_id_formats_for_models("guild", 574921006817476608, guilds.Guild) - @_helpers.parametrize_valid_id_formats_for_models("role", 123123, guilds.GuildRole) - async def test_delete_role(self, rest_clients_impl, guild, role): - rest_clients_impl._session.delete_guild_role.return_value = ... - assert await rest_clients_impl.delete_role(guild, role) is None - rest_clients_impl._session.delete_guild_role.assert_called_once_with( - guild_id="574921006817476608", role_id="123123" - ) - - @pytest.mark.asyncio - @_helpers.parametrize_valid_id_formats_for_models("guild", 574921006817476608, guilds.Guild) - @pytest.mark.parametrize("days", [7, datetime.timedelta(days=7)]) - async def test_estimate_guild_prune_count(self, rest_clients_impl, guild, days): - rest_clients_impl._session.get_guild_prune_count.return_value = 42 - assert await rest_clients_impl.estimate_guild_prune_count(guild, days) == 42 - rest_clients_impl._session.get_guild_prune_count.assert_called_once_with(guild_id="574921006817476608", days=7) - - @pytest.mark.asyncio - @_helpers.parametrize_valid_id_formats_for_models("guild", 574921006817476608, guilds.Guild) - @pytest.mark.parametrize("days", [7, datetime.timedelta(days=7)]) - async def test_estimate_guild_with_optionals(self, rest_clients_impl, guild, days): - rest_clients_impl._session.begin_guild_prune.return_value = None - assert await rest_clients_impl.begin_guild_prune(guild, days, compute_prune_count=True, reason="nah m8") is None - rest_clients_impl._session.begin_guild_prune.assert_called_once_with( - guild_id="574921006817476608", days=7, compute_prune_count=True, reason="nah m8" - ) - - @pytest.mark.asyncio - @_helpers.parametrize_valid_id_formats_for_models("guild", 574921006817476608, guilds.Guild) - @pytest.mark.parametrize("days", [7, datetime.timedelta(days=7)]) - async def test_estimate_guild_without_optionals(self, rest_clients_impl, guild, days): - rest_clients_impl._session.begin_guild_prune.return_value = 42 - assert await rest_clients_impl.begin_guild_prune(guild, days) == 42 - rest_clients_impl._session.begin_guild_prune.assert_called_once_with( - guild_id="574921006817476608", days=7, compute_prune_count=..., reason=... - ) - - @pytest.mark.asyncio - @_helpers.parametrize_valid_id_formats_for_models("guild", 574921006817476608, guilds.Guild) - async def test_fetch_guild_voice_regions(self, rest_clients_impl, guild): - mock_voice_payload = {"name": "london", "id": "LONDON"} - mock_voice_obj = mock.MagicMock(voices.VoiceRegion) - rest_clients_impl._session.get_guild_voice_regions.return_value = [mock_voice_payload] - with mock.patch.object(voices.VoiceRegion, "deserialize", return_value=mock_voice_obj): - assert await rest_clients_impl.fetch_guild_voice_regions(guild) == [mock_voice_obj] - rest_clients_impl._session.get_guild_voice_regions.assert_called_once_with(guild_id="574921006817476608") - voices.VoiceRegion.deserialize.assert_called_once_with(mock_voice_payload) - - @pytest.mark.asyncio - @_helpers.parametrize_valid_id_formats_for_models("guild", 574921006817476608, guilds.Guild) - async def test_fetch_guild_invites(self, rest_clients_impl, guild): - mock_invite_payload = {"code": "dododo"} - mock_invite_obj = mock.MagicMock(invites.InviteWithMetadata) - rest_clients_impl._session.get_guild_invites.return_value = [mock_invite_payload] - with mock.patch.object(invites.InviteWithMetadata, "deserialize", return_value=mock_invite_obj): - assert await rest_clients_impl.fetch_guild_invites(guild) == [mock_invite_obj] - invites.InviteWithMetadata.deserialize.assert_called_once_with(mock_invite_payload) - - @pytest.mark.asyncio - @_helpers.parametrize_valid_id_formats_for_models("guild", 574921006817476608, guilds.Guild) - async def test_fetch_integrations(self, rest_clients_impl, guild): - mock_integration_payload = {"id": "123123", "name": "Integrated", "type": "twitch"} - mock_integration_obj = mock.MagicMock(guilds.GuildIntegration) - rest_clients_impl._session.get_guild_integrations.return_value = [mock_integration_payload] - with mock.patch.object(guilds.GuildIntegration, "deserialize", return_value=mock_integration_obj): - assert await rest_clients_impl.fetch_integrations(guild) == [mock_integration_obj] - rest_clients_impl._session.get_guild_integrations.assert_called_once_with(guild_id="574921006817476608") - guilds.GuildIntegration.deserialize.assert_called_once_with(mock_integration_payload) - - @pytest.mark.asyncio - @_helpers.parametrize_valid_id_formats_for_models("guild", 574921006817476608, guilds.Guild) - @_helpers.parametrize_valid_id_formats_for_models("integration", 379953393319542784, guilds.GuildIntegration) - @pytest.mark.parametrize("period", [datetime.timedelta(days=7), 7]) - async def test_update_integration_with_optionals(self, rest_clients_impl, guild, integration, period): - rest_clients_impl._session.modify_guild_integration.return_value = ... - result = await rest_clients_impl.update_integration( - guild, - integration, - expire_behaviour=guilds.IntegrationExpireBehaviour.KICK, - expire_grace_period=period, - enable_emojis=True, - reason="GET YEET'D", - ) - assert result is None - rest_clients_impl._session.modify_guild_integration.assert_called_once_with( - guild_id="574921006817476608", - integration_id="379953393319542784", - expire_behaviour=1, - expire_grace_period=7, - enable_emojis=True, - reason="GET YEET'D", - ) - - @pytest.mark.asyncio - @_helpers.parametrize_valid_id_formats_for_models("guild", 574921006817476608, guilds.Guild) - @_helpers.parametrize_valid_id_formats_for_models("integration", 379953393319542784, guilds.GuildIntegration) - async def test_update_integration_without_optionals(self, rest_clients_impl, guild, integration): - rest_clients_impl._session.modify_guild_integration.return_value = ... - assert await rest_clients_impl.update_integration(guild, integration) is None - rest_clients_impl._session.modify_guild_integration.assert_called_once_with( - guild_id="574921006817476608", - integration_id="379953393319542784", - expire_behaviour=..., - expire_grace_period=..., - enable_emojis=..., - reason=..., - ) - - @pytest.mark.asyncio - @_helpers.parametrize_valid_id_formats_for_models("guild", 574921006817476608, guilds.Guild) - @_helpers.parametrize_valid_id_formats_for_models("integration", 379953393319542784, guilds.GuildIntegration) - async def test_delete_integration_with_reason(self, rest_clients_impl, guild, integration): - rest_clients_impl._session.delete_guild_integration.return_value = ... - assert await rest_clients_impl.delete_integration(guild, integration, reason="B Y E") is None - rest_clients_impl._session.delete_guild_integration.assert_called_once_with( - guild_id="574921006817476608", integration_id="379953393319542784", reason="B Y E" - ) - - @pytest.mark.asyncio - @_helpers.parametrize_valid_id_formats_for_models("guild", 574921006817476608, guilds.Guild) - @_helpers.parametrize_valid_id_formats_for_models("integration", 379953393319542784, guilds.GuildIntegration) - async def test_delete_integration_without_reason(self, rest_clients_impl, guild, integration): - rest_clients_impl._session.delete_guild_integration.return_value = ... - assert await rest_clients_impl.delete_integration(guild, integration) is None - rest_clients_impl._session.delete_guild_integration.assert_called_once_with( - guild_id="574921006817476608", integration_id="379953393319542784", reason=... - ) - - @pytest.mark.asyncio - @_helpers.parametrize_valid_id_formats_for_models("guild", 574921006817476608, guilds.Guild) - @_helpers.parametrize_valid_id_formats_for_models("integration", 379953393319542784, guilds.GuildIntegration) - async def test_sync_guild_integration(self, rest_clients_impl, guild, integration): - rest_clients_impl._session.sync_guild_integration.return_value = ... - assert await rest_clients_impl.sync_guild_integration(guild, integration) is None - rest_clients_impl._session.sync_guild_integration.assert_called_once_with( - guild_id="574921006817476608", integration_id="379953393319542784", - ) - - @pytest.mark.asyncio - @_helpers.parametrize_valid_id_formats_for_models("guild", 574921006817476608, guilds.Guild) - async def test_fetch_guild_embed(self, rest_clients_impl, guild): - mock_embed_payload = {"enabled": True, "channel_id": "2020202"} - mock_embed_obj = mock.MagicMock(guilds.GuildEmbed) - rest_clients_impl._session.get_guild_embed.return_value = mock_embed_payload - with mock.patch.object(guilds.GuildEmbed, "deserialize", return_value=mock_embed_obj): - assert await rest_clients_impl.fetch_guild_embed(guild) is mock_embed_obj - rest_clients_impl._session.get_guild_embed.assert_called_once_with(guild_id="574921006817476608") - - @pytest.mark.asyncio - @_helpers.parametrize_valid_id_formats_for_models("guild", 574921006817476608, guilds.Guild) - @_helpers.parametrize_valid_id_formats_for_models("channel", 123123, channels.GuildChannel) - async def test_update_guild_embed_with_optionnal(self, rest_clients_impl, guild, channel): - mock_embed_payload = {"enabled": True, "channel_id": "2020202"} - mock_embed_obj = mock.MagicMock(guilds.GuildEmbed) - rest_clients_impl._session.modify_guild_embed.return_value = mock_embed_payload - with mock.patch.object(guilds.GuildEmbed, "deserialize", return_value=mock_embed_obj): - result = await rest_clients_impl.update_guild_embed(guild, channel=channel, enabled=True, reason="Nyaa!!!") - assert result is mock_embed_obj - rest_clients_impl._session.modify_guild_embed.assert_called_once_with( - guild_id="574921006817476608", channel_id="123123", enabled=True, reason="Nyaa!!!" - ) - - @pytest.mark.asyncio - @_helpers.parametrize_valid_id_formats_for_models("guild", 574921006817476608, guilds.Guild) - async def test_update_guild_embed_without_optionnal(self, rest_clients_impl, guild): - mock_embed_payload = {"enabled": True, "channel_id": "2020202"} - mock_embed_obj = mock.MagicMock(guilds.GuildEmbed) - rest_clients_impl._session.modify_guild_embed.return_value = mock_embed_payload - with mock.patch.object(guilds.GuildEmbed, "deserialize", return_value=mock_embed_obj): - assert await rest_clients_impl.update_guild_embed(guild) is mock_embed_obj - rest_clients_impl._session.modify_guild_embed.assert_called_once_with( - guild_id="574921006817476608", channel_id=..., enabled=..., reason=... - ) - - @pytest.mark.asyncio - @_helpers.parametrize_valid_id_formats_for_models("guild", 574921006817476608, guilds.Guild) - async def test_fetch_guild_vanity_url(self, rest_clients_impl, guild): - mock_vanity_payload = {"code": "akfdk", "uses": 5} - mock_vanity_obj = mock.MagicMock(invites.VanityUrl) - rest_clients_impl._session.get_guild_vanity_url.return_value = mock_vanity_payload - with mock.patch.object(invites.VanityUrl, "deserialize", return_value=mock_vanity_obj): - assert await rest_clients_impl.fetch_guild_vanity_url(guild) is mock_vanity_obj - rest_clients_impl._session.get_guild_vanity_url.assert_called_once_with(guild_id="574921006817476608") - invites.VanityUrl.deserialize.assert_called_once_with(mock_vanity_payload) - - @_helpers.parametrize_valid_id_formats_for_models("guild", 574921006817476608, guilds.Guild) - def test_fetch_guild_widget_image_with_style(self, rest_clients_impl, guild): - mock_url = "not/a/url" - rest_clients_impl._session.get_guild_widget_image_url.return_value = mock_url - assert rest_clients_impl.format_guild_widget_image(guild, style="notAStyle") == mock_url - rest_clients_impl._session.get_guild_widget_image_url.assert_called_once_with( - guild_id="574921006817476608", style="notAStyle", - ) - - @_helpers.parametrize_valid_id_formats_for_models("guild", 574921006817476608, guilds.Guild) - def test_fetch_guild_widget_image_without_style(self, rest_clients_impl, guild): - mock_url = "not/a/url" - rest_clients_impl._session.get_guild_widget_image_url.return_value = mock_url - assert rest_clients_impl.format_guild_widget_image(guild) == mock_url - rest_clients_impl._session.get_guild_widget_image_url.assert_called_once_with( - guild_id="574921006817476608", style=..., - ) - - @pytest.mark.asyncio - @pytest.mark.parametrize("invite", [mock.MagicMock(invites.Invite, code="AAAAAAAAAAAAAAAA"), "AAAAAAAAAAAAAAAA"]) - async def test_fetch_invite_with_counts(self, rest_clients_impl, invite): - mock_invite_payload = {"code": "AAAAAAAAAAAAAAAA", "guild": {}, "channel": {}} - mock_invite_obj = mock.MagicMock(invites.Invite) - rest_clients_impl._session.get_invite.return_value = mock_invite_payload - with mock.patch.object(invites.Invite, "deserialize", return_value=mock_invite_obj): - assert await rest_clients_impl.fetch_invite(invite, with_counts=True) is mock_invite_obj - rest_clients_impl._session.get_invite.assert_called_once_with( - invite_code="AAAAAAAAAAAAAAAA", with_counts=True, - ) - invites.Invite.deserialize.assert_called_once_with(mock_invite_payload) - - @pytest.mark.asyncio - @pytest.mark.parametrize("invite", [mock.MagicMock(invites.Invite, code="AAAAAAAAAAAAAAAA"), "AAAAAAAAAAAAAAAA"]) - async def test_fetch_invite_without_counts(self, rest_clients_impl, invite): - mock_invite_payload = {"code": "AAAAAAAAAAAAAAAA", "guild": {}, "channel": {}} - mock_invite_obj = mock.MagicMock(invites.Invite) - rest_clients_impl._session.get_invite.return_value = mock_invite_payload - with mock.patch.object(invites.Invite, "deserialize", return_value=mock_invite_obj): - assert await rest_clients_impl.fetch_invite(invite) is mock_invite_obj - rest_clients_impl._session.get_invite.assert_called_once_with( - invite_code="AAAAAAAAAAAAAAAA", with_counts=..., - ) - invites.Invite.deserialize.assert_called_once_with(mock_invite_payload) - - @pytest.mark.asyncio - @pytest.mark.parametrize("invite", [mock.MagicMock(invites.Invite, code="AAAAAAAAAAAAAAAA"), "AAAAAAAAAAAAAAAA"]) - async def test_delete_invite(self, rest_clients_impl, invite): - rest_clients_impl._session.delete_invite.return_value = ... - assert await rest_clients_impl.delete_invite(invite) is None - rest_clients_impl._session.delete_invite.assert_called_once_with(invite_code="AAAAAAAAAAAAAAAA") - - @pytest.mark.asyncio - @_helpers.parametrize_valid_id_formats_for_models("user", 123123123, users.User) - async def test_fetch_user(self, rest_clients_impl, user): - mock_user_payload = {"id": "123", "username": "userName"} - mock_user_obj = mock.MagicMock(users.User) - rest_clients_impl._session.get_user.return_value = mock_user_payload - with mock.patch.object(users.User, "deserialize", return_value=mock_user_obj): - assert await rest_clients_impl.fetch_user(user) is mock_user_obj - rest_clients_impl._session.get_user.assert_called_once_with(user_id="123123123") - users.User.deserialize.assert_called_once_with(mock_user_payload) - - @pytest.mark.asyncio - async def test_fetch_application_info(self, rest_clients_impl): - mock_application_payload = {"id": "2929292", "name": "blah blah", "description": "an app"} - mock_application_obj = mock.MagicMock(oauth2.Application) - rest_clients_impl._session.get_current_application_info.return_value = mock_application_payload - with mock.patch.object(oauth2.Application, "deserialize", return_value=mock_application_obj): - assert await rest_clients_impl.fetch_my_application_info() is mock_application_obj - rest_clients_impl._session.get_current_application_info.assert_called_once_with() - oauth2.Application.deserialize.assert_called_once_with(mock_application_payload) - - @pytest.mark.asyncio - async def test_fetch_me(self, rest_clients_impl): - mock_user_payload = {"username": "A User", "id": "202020200202"} - mock_user_obj = mock.MagicMock(users.MyUser) - rest_clients_impl._session.get_current_user.return_value = mock_user_payload - with mock.patch.object(users.MyUser, "deserialize", return_value=mock_user_obj): - assert await rest_clients_impl.fetch_me() is mock_user_obj - rest_clients_impl._session.get_current_user.assert_called_once() - users.MyUser.deserialize.assert_called_once_with(mock_user_payload) - - @pytest.mark.asyncio - async def test_update_me_with_optionals(self, rest_clients_impl): - mock_user_payload = {"id": "424242", "flags": "420", "discriminator": "6969"} - mock_user_obj = mock.MagicMock(users.MyUser) - rest_clients_impl._session.modify_current_user.return_value = mock_user_payload - mock_avatar_obj = mock.MagicMock(io.BytesIO) - mock_avatar_data = mock.MagicMock(bytes) - stack = contextlib.ExitStack() - stack.enter_context(mock.patch.object(users.MyUser, "deserialize", return_value=mock_user_obj)) - stack.enter_context(mock.patch.object(conversions, "get_bytes_from_resource", return_value=mock_avatar_data)) - with stack: - assert await rest_clients_impl.update_me(username="aNewName", avatar_data=mock_avatar_obj) is mock_user_obj - rest_clients_impl._session.modify_current_user.assert_called_once_with( - username="aNewName", avatar=mock_avatar_data - ) - conversions.get_bytes_from_resource.assert_called_once_with(mock_avatar_obj) - users.MyUser.deserialize.assert_called_once_with(mock_user_payload) - - @pytest.mark.asyncio - async def test_update_me_without_optionals(self, rest_clients_impl): - mock_user_payload = {"id": "424242", "flags": "420", "discriminator": "6969"} - mock_user_obj = mock.MagicMock(users.MyUser) - rest_clients_impl._session.modify_current_user.return_value = mock_user_payload - with mock.patch.object(users.MyUser, "deserialize", return_value=mock_user_obj): - assert await rest_clients_impl.update_me() is mock_user_obj - rest_clients_impl._session.modify_current_user.assert_called_once_with(username=..., avatar=...) - users.MyUser.deserialize.assert_called_once_with(mock_user_payload) - - @pytest.mark.asyncio - async def test_fetch_my_connections(self, rest_clients_impl): - mock_connection_payload = {"id": "odnkwu", "type": "twitch", "name": "eric"} - mock_connection_obj = mock.MagicMock(oauth2.OwnConnection) - rest_clients_impl._session.get_current_user_connections.return_value = [mock_connection_payload] - with mock.patch.object(oauth2.OwnConnection, "deserialize", return_value=mock_connection_obj): - assert await rest_clients_impl.fetch_my_connections() == [mock_connection_obj] - rest_clients_impl._session.get_current_user_connections.assert_called_once() - oauth2.OwnConnection.deserialize.assert_called_once_with(mock_connection_payload) - - @_helpers.parametrize_valid_id_formats_for_models("guild", 574921006817476608, guilds.Guild) - def test_fetch_my_guilds_after_with_optionals(self, rest_clients_impl, guild): - mock_generator = mock.AsyncMock() - rest_clients_impl._pagination_handler = mock.MagicMock(return_value=mock_generator) - assert rest_clients_impl.fetch_my_guilds_after(after=guild, limit=50) is mock_generator - rest_clients_impl._pagination_handler.assert_called_once_with( - deserializer=oauth2.OwnGuild.deserialize, - direction="after", - request=rest_clients_impl._session.get_current_user_guilds, - reversing=False, - start="574921006817476608", - limit=50, - ) - - def test_fetch_my_guilds_after_without_optionals(self, rest_clients_impl): - mock_generator = mock.AsyncMock() - rest_clients_impl._pagination_handler = mock.MagicMock(return_value=mock_generator) - assert rest_clients_impl.fetch_my_guilds_after() is mock_generator - rest_clients_impl._pagination_handler.assert_called_once_with( - deserializer=oauth2.OwnGuild.deserialize, - direction="after", - request=rest_clients_impl._session.get_current_user_guilds, - reversing=False, - start="0", - limit=None, - ) - - def test_fetch_my_guilds_after_with_datetime_object(self, rest_clients_impl): - mock_generator = mock.AsyncMock() - rest_clients_impl._pagination_handler = mock.MagicMock(return_value=mock_generator) - date = datetime.datetime(2019, 1, 22, 18, 41, 15, 283_000, tzinfo=datetime.timezone.utc) - assert rest_clients_impl.fetch_my_guilds_after(after=date) is mock_generator - rest_clients_impl._pagination_handler.assert_called_once_with( - deserializer=oauth2.OwnGuild.deserialize, - direction="after", - request=rest_clients_impl._session.get_current_user_guilds, - reversing=False, - start="537340988620800000", - limit=None, - ) - - @_helpers.parametrize_valid_id_formats_for_models("guild", 574921006817476608, guilds.Guild) - def test_fetch_my_guilds_before_with_optionals(self, rest_clients_impl, guild): - mock_generator = mock.AsyncMock() - rest_clients_impl._pagination_handler = mock.MagicMock(return_value=mock_generator) - assert rest_clients_impl.fetch_my_guilds_before(before=guild, limit=50) is mock_generator - rest_clients_impl._pagination_handler.assert_called_once_with( - deserializer=oauth2.OwnGuild.deserialize, - direction="before", - request=rest_clients_impl._session.get_current_user_guilds, - reversing=False, - start="574921006817476608", - limit=50, - ) - - def test_fetch_my_guilds_before_without_optionals(self, rest_clients_impl): - mock_generator = mock.AsyncMock() - rest_clients_impl._pagination_handler = mock.MagicMock(return_value=mock_generator) - assert rest_clients_impl.fetch_my_guilds_before() is mock_generator - rest_clients_impl._pagination_handler.assert_called_once_with( - deserializer=oauth2.OwnGuild.deserialize, - direction="before", - request=rest_clients_impl._session.get_current_user_guilds, - reversing=False, - start=None, - limit=None, - ) - - def test_fetch_my_guilds_before_with_datetime_object(self, rest_clients_impl): - mock_generator = mock.AsyncMock() - rest_clients_impl._pagination_handler = mock.MagicMock(return_value=mock_generator) - date = datetime.datetime(2019, 1, 22, 18, 41, 15, 283_000, tzinfo=datetime.timezone.utc) - assert rest_clients_impl.fetch_my_guilds_before(before=date) is mock_generator - rest_clients_impl._pagination_handler.assert_called_once_with( - deserializer=oauth2.OwnGuild.deserialize, - direction="before", - request=rest_clients_impl._session.get_current_user_guilds, - reversing=False, - start="537340988620800000", - limit=None, - ) - - @pytest.mark.asyncio - @_helpers.parametrize_valid_id_formats_for_models("guild", 574921006817476608, guilds.Guild) - async def test_leave_guild(self, rest_clients_impl, guild): - rest_clients_impl._session.leave_guild.return_value = ... - assert await rest_clients_impl.leave_guild(guild) is None - rest_clients_impl._session.leave_guild.assert_called_once_with(guild_id="574921006817476608") - - @pytest.mark.asyncio - @_helpers.parametrize_valid_id_formats_for_models("recipient", 115590097100865541, users.User) - async def test_create_dm_channel(self, rest_clients_impl, recipient): - mock_dm_payload = {"id": "2202020", "type": 2, "recipients": []} - mock_dm_obj = mock.MagicMock(channels.DMChannel) - rest_clients_impl._session.create_dm.return_value = mock_dm_payload - with mock.patch.object(channels.DMChannel, "deserialize", return_value=mock_dm_obj): - assert await rest_clients_impl.create_dm_channel(recipient) is mock_dm_obj - rest_clients_impl._session.create_dm.assert_called_once_with(recipient_id="115590097100865541") - channels.DMChannel.deserialize.assert_called_once_with(mock_dm_payload) - - @pytest.mark.asyncio - async def test_fetch_voice_regions(self, rest_clients_impl): - mock_voice_payload = {"id": "LONDON", "name": "london"} - mock_voice_obj = mock.MagicMock(voices.VoiceRegion) - rest_clients_impl._session.list_voice_regions.return_value = [mock_voice_payload] - with mock.patch.object(voices.VoiceRegion, "deserialize", return_value=mock_voice_obj): - assert await rest_clients_impl.fetch_voice_regions() == [mock_voice_obj] - rest_clients_impl._session.list_voice_regions.assert_called_once() - voices.VoiceRegion.deserialize.assert_called_once_with(mock_voice_payload) - - @pytest.mark.asyncio - @_helpers.parametrize_valid_id_formats_for_models("channel", 115590097100865541, channels.Channel) - async def test_create_webhook_with_optionals(self, rest_clients_impl, channel): - mock_webhook_payload = {"id": "29292929", "channel_id": "2292992"} - mock_webhook_obj = mock.MagicMock(webhooks.Webhook) - rest_clients_impl._session.create_webhook.return_value = mock_webhook_payload - mock_image_obj = mock.MagicMock(io.BytesIO) - mock_image_data = mock.MagicMock(bytes) - stack = contextlib.ExitStack() - stack.enter_context(mock.patch.object(webhooks.Webhook, "deserialize", return_value=mock_webhook_obj)) - stack.enter_context(mock.patch.object(conversions, "get_bytes_from_resource", return_value=mock_image_data)) - with stack: - result = await rest_clients_impl.create_webhook( - channel=channel, name="aWebhook", avatar_data=mock_image_obj, reason="And a webhook is born." - ) - assert result is mock_webhook_obj - conversions.get_bytes_from_resource.assert_called_once_with(mock_image_obj) - webhooks.Webhook.deserialize.assert_called_once_with(mock_webhook_payload) - rest_clients_impl._session.create_webhook.assert_called_once_with( - channel_id="115590097100865541", name="aWebhook", avatar=mock_image_data, reason="And a webhook is born." - ) - - @pytest.mark.asyncio - @_helpers.parametrize_valid_id_formats_for_models("channel", 115590097100865541, channels.Channel) - async def test_create_webhook_without_optionals(self, rest_clients_impl, channel): - mock_webhook_payload = {"id": "29292929", "channel_id": "2292992"} - mock_webhook_obj = mock.MagicMock(webhooks.Webhook) - rest_clients_impl._session.create_webhook.return_value = mock_webhook_payload - with mock.patch.object(webhooks.Webhook, "deserialize", return_value=mock_webhook_obj): - assert await rest_clients_impl.create_webhook(channel=channel, name="aWebhook") is mock_webhook_obj - webhooks.Webhook.deserialize.assert_called_once_with(mock_webhook_payload) - rest_clients_impl._session.create_webhook.assert_called_once_with( - channel_id="115590097100865541", name="aWebhook", avatar=..., reason=... - ) - - @pytest.mark.asyncio - @_helpers.parametrize_valid_id_formats_for_models("channel", 115590097100865541, channels.GuildChannel) - async def test_fetch_channel_webhooks(self, rest_clients_impl, channel): - mock_webhook_payload = {"id": "29292929", "channel_id": "2292992"} - mock_webhook_obj = mock.MagicMock(webhooks.Webhook) - rest_clients_impl._session.get_channel_webhooks.return_value = [mock_webhook_payload] - with mock.patch.object(webhooks.Webhook, "deserialize", return_value=mock_webhook_obj): - assert await rest_clients_impl.fetch_channel_webhooks(channel) == [mock_webhook_obj] - rest_clients_impl._session.get_channel_webhooks.assert_called_once_with(channel_id="115590097100865541") - webhooks.Webhook.deserialize.assert_called_once_with(mock_webhook_payload) - - @pytest.mark.asyncio - @_helpers.parametrize_valid_id_formats_for_models("channel", 115590097100865541, channels.GuildChannel) - async def test_fetch_guild_webhooks(self, rest_clients_impl, channel): - mock_webhook_payload = {"id": "29292929", "channel_id": "2292992"} - mock_webhook_obj = mock.MagicMock(webhooks.Webhook) - rest_clients_impl._session.get_guild_webhooks.return_value = [mock_webhook_payload] - with mock.patch.object(webhooks.Webhook, "deserialize", return_value=mock_webhook_obj): - assert await rest_clients_impl.fetch_guild_webhooks(channel) == [mock_webhook_obj] - rest_clients_impl._session.get_guild_webhooks.assert_called_once_with(guild_id="115590097100865541") - webhooks.Webhook.deserialize.assert_called_once_with(mock_webhook_payload) - - @pytest.mark.asyncio - @_helpers.parametrize_valid_id_formats_for_models("webhook", 379953393319542784, webhooks.Webhook) - async def test_fetch_webhook_with_webhook_token(self, rest_clients_impl, webhook): - mock_webhook_payload = {"id": "29292929", "channel_id": "2292992"} - mock_webhook_obj = mock.MagicMock(webhooks.Webhook) - rest_clients_impl._session.get_webhook.return_value = mock_webhook_payload - with mock.patch.object(webhooks.Webhook, "deserialize", return_value=mock_webhook_obj): - assert await rest_clients_impl.fetch_webhook(webhook, webhook_token="dsawqoepql.kmsdao") is mock_webhook_obj - rest_clients_impl._session.get_webhook.assert_called_once_with( - webhook_id="379953393319542784", webhook_token="dsawqoepql.kmsdao", - ) - webhooks.Webhook.deserialize.assert_called_once_with(mock_webhook_payload) - - @pytest.mark.asyncio - @_helpers.parametrize_valid_id_formats_for_models("webhook", 379953393319542784, webhooks.Webhook) - async def test_fetch_webhook_without_webhook_token(self, rest_clients_impl, webhook): - mock_webhook_payload = {"id": "29292929", "channel_id": "2292992"} - mock_webhook_obj = mock.MagicMock(webhooks.Webhook) - rest_clients_impl._session.get_webhook.return_value = mock_webhook_payload - with mock.patch.object(webhooks.Webhook, "deserialize", return_value=mock_webhook_obj): - assert await rest_clients_impl.fetch_webhook(webhook) is mock_webhook_obj - rest_clients_impl._session.get_webhook.assert_called_once_with( - webhook_id="379953393319542784", webhook_token=..., - ) - webhooks.Webhook.deserialize.assert_called_once_with(mock_webhook_payload) - - @pytest.mark.asyncio - @_helpers.parametrize_valid_id_formats_for_models("webhook", 379953393319542784, webhooks.Webhook) - @_helpers.parametrize_valid_id_formats_for_models("channel", 115590097100865541, webhooks.Webhook) - async def test_update_webhook_with_optionals(self, rest_clients_impl, webhook, channel): - mock_webhook_obj = mock.MagicMock(webhooks.Webhook) - mock_webhook_payload = {"id": "123123", "avatar": "1wedoklpasdoiksdoka"} - rest_clients_impl._session.modify_webhook.return_value = mock_webhook_payload - mock_image_obj = mock.MagicMock(io.BytesIO) - mock_image_data = mock.MagicMock(bytes) - stack = contextlib.ExitStack() - stack.enter_context(mock.patch.object(webhooks.Webhook, "deserialize", return_value=mock_webhook_obj)) - stack.enter_context(mock.patch.object(conversions, "get_bytes_from_resource", return_value=mock_image_data)) - with stack: - result = await rest_clients_impl.update_webhook( - webhook, - webhook_token="a.wEbHoOk.ToKeN", - name="blah_blah_blah", - avatar_data=mock_image_obj, - channel=channel, - reason="A reason", - ) - assert result is mock_webhook_obj - rest_clients_impl._session.modify_webhook.assert_called_once_with( - webhook_id="379953393319542784", - webhook_token="a.wEbHoOk.ToKeN", - name="blah_blah_blah", - avatar=mock_image_data, - channel_id="115590097100865541", - reason="A reason", - ) - webhooks.Webhook.deserialize.assert_called_once_with(mock_webhook_payload) - conversions.get_bytes_from_resource.assert_called_once_with(mock_image_obj) - - @pytest.mark.asyncio - @_helpers.parametrize_valid_id_formats_for_models("webhook", 379953393319542784, webhooks.Webhook) - async def test_update_webhook_without_optionals(self, rest_clients_impl, webhook): - mock_webhook_obj = mock.MagicMock(webhooks.Webhook) - mock_webhook_payload = {"id": "123123", "avatar": "1wedoklpasdoiksdoka"} - rest_clients_impl._session.modify_webhook.return_value = mock_webhook_payload - with mock.patch.object(webhooks.Webhook, "deserialize", return_value=mock_webhook_obj): - assert await rest_clients_impl.update_webhook(webhook) is mock_webhook_obj - rest_clients_impl._session.modify_webhook.assert_called_once_with( - webhook_id="379953393319542784", webhook_token=..., name=..., avatar=..., channel_id=..., reason=..., - ) - webhooks.Webhook.deserialize.assert_called_once_with(mock_webhook_payload) - - @pytest.mark.asyncio - @_helpers.parametrize_valid_id_formats_for_models("webhook", 379953393319542784, webhooks.Webhook) - async def test_delete_webhook_with_webhook_token(self, rest_clients_impl, webhook): - rest_clients_impl._session.delete_webhook.return_value = ... - assert await rest_clients_impl.delete_webhook(webhook, webhook_token="dsawqoepql.kmsdao") is None - rest_clients_impl._session.delete_webhook.assert_called_once_with( - webhook_id="379953393319542784", webhook_token="dsawqoepql.kmsdao" - ) - - @pytest.mark.asyncio - @_helpers.parametrize_valid_id_formats_for_models("webhook", 379953393319542784, webhooks.Webhook) - async def test_delete_webhook_without_webhook_token(self, rest_clients_impl, webhook): - rest_clients_impl._session.delete_webhook.return_value = ... - assert await rest_clients_impl.delete_webhook(webhook) is None - rest_clients_impl._session.delete_webhook.assert_called_once_with( - webhook_id="379953393319542784", webhook_token=... - ) - - @pytest.mark.asyncio - @_helpers.parametrize_valid_id_formats_for_models("webhook", 379953393319542784, webhooks.Webhook) - async def test_execute_webhook_without_optionals(self, rest_clients_impl, webhook): - rest_clients_impl._session.execute_webhook.return_value = ... - mock_allowed_mentions_payload = {"parse": ["everyone", "users", "roles"]} - rest_clients_impl._generate_allowed_mentions = mock.MagicMock(return_value=mock_allowed_mentions_payload) - assert await rest_clients_impl.execute_webhook(webhook, "a.webhook.token") is None - rest_clients_impl._session.execute_webhook.assert_called_once_with( - webhook_id="379953393319542784", - webhook_token="a.webhook.token", - content=..., - username=..., - avatar_url=..., - tts=..., - wait=False, - file=..., - embeds=..., - allowed_mentions=mock_allowed_mentions_payload, - ) - - @pytest.mark.asyncio - @_helpers.parametrize_valid_id_formats_for_models("webhook", 379953393319542784, webhooks.Webhook) - async def test_execute_webhook_with_optionals(self, rest_clients_impl, webhook): - rest_clients_impl._session.execute_webhook.return_value = ... - mock_allowed_mentions_payload = {"parse": ["everyone", "users", "roles"]} - rest_clients_impl._generate_allowed_mentions = mock.MagicMock(return_value=mock_allowed_mentions_payload) - mock_embed_payload = {"description": "424242"} - mock_embed_obj = mock.MagicMock(embeds.Embed) - mock_embed_obj.serialize = mock.MagicMock(return_value=mock_embed_payload) - mock_media_obj = mock.MagicMock() - mock_media_payload = ("aName.png", mock.MagicMock()) - stack = contextlib.ExitStack() - stack.enter_context(mock.patch.object(media, "safe_read_file", return_value=mock_media_payload)) - stack.enter_context(mock.patch.object(messages.Message, "deserialize")) - with stack: - await rest_clients_impl.execute_webhook( - webhook, - "a.webhook.token", - content="THE TRUTH", - username="User 97", - avatar_url="httttttt/L//", - tts=True, - wait=True, - file=mock_media_obj, - embeds=[mock_embed_obj], - mentions_everyone=False, - role_mentions=False, - user_mentions=False, - ) - media.safe_read_file.assert_called_once_with(mock_media_obj) - rest_clients_impl._session.execute_webhook.assert_called_once_with( - webhook_id="379953393319542784", - webhook_token="a.webhook.token", - content="THE TRUTH", - username="User 97", - avatar_url="httttttt/L//", - tts=True, - wait=True, - file=mock_media_payload, - embeds=[mock_embed_payload], - allowed_mentions=mock_allowed_mentions_payload, - ) - mock_embed_obj.serialize.assert_called_once() - rest_clients_impl._generate_allowed_mentions.assert_called_once_with( - mentions_everyone=False, user_mentions=False, role_mentions=False - ) - - @pytest.mark.asyncio - @_helpers.parametrize_valid_id_formats_for_models("webhook", 379953393319542784, webhooks.Webhook) - async def test_execute_webhook_returns_message_when_wait_is_true(self, rest_clients_impl, webhook): - mock_message_payload = {"id": "6796959949034", "content": "Nyaa Nyaa"} - mock_message_obj = mock.MagicMock(messages.Message) - rest_clients_impl._session.execute_webhook.return_value = mock_message_payload - mock_allowed_mentions_payload = {"parse": ["everyone", "users", "roles"]} - rest_clients_impl._generate_allowed_mentions = mock.MagicMock(return_value=mock_allowed_mentions_payload) - with mock.patch.object(messages.Message, "deserialize", return_value=mock_message_obj): - assert await rest_clients_impl.execute_webhook(webhook, "a.webhook.token", wait=True) is mock_message_obj - messages.Message.deserialize.assert_called_once_with(mock_message_payload) - - @pytest.mark.asyncio - async def test_safe_execute_webhook_without_optionals(self, rest_clients_impl): - webhook = mock.MagicMock(webhooks.Webhook) - mock_message_obj = mock.MagicMock(messages.Message) - rest_clients_impl.execute_webhook = mock.AsyncMock(return_value=mock_message_obj) - result = await rest_clients_impl.safe_webhook_execute(webhook, "a.webhook.token",) - assert result is mock_message_obj - rest_clients_impl.execute_webhook.assert_called_once_with( - webhook=webhook, - webhook_token="a.webhook.token", - content=..., - username=..., - avatar_url=..., - tts=..., - wait=False, - file=..., - embeds=..., - mentions_everyone=False, - user_mentions=False, - role_mentions=False, - ) - - @pytest.mark.asyncio - async def test_safe_execute_webhook_with_optionals(self, rest_clients_impl): - webhook = mock.MagicMock(webhooks.Webhook) - mock_media_obj = mock.MagicMock(bytes) - mock_embed_obj = mock.MagicMock(embeds.Embed) - mock_message_obj = mock.MagicMock(messages.Message) - rest_clients_impl.execute_webhook = mock.AsyncMock(return_value=mock_message_obj) - result = await rest_clients_impl.safe_webhook_execute( - webhook, - "a.webhook.token", - content="THE TRUTH", - username="User 97", - avatar_url="httttttt/L//", - tts=True, - wait=True, - file=mock_media_obj, - embeds=[mock_embed_obj], - mentions_everyone=False, - role_mentions=False, - user_mentions=False, - ) - assert result is mock_message_obj - rest_clients_impl.execute_webhook.assert_called_once_with( - webhook=webhook, - webhook_token="a.webhook.token", - content="THE TRUTH", - username="User 97", - avatar_url="httttttt/L//", - tts=True, - wait=True, - file=mock_media_obj, - embeds=[mock_embed_obj], - mentions_everyone=False, - role_mentions=False, - user_mentions=False, - ) diff --git a/tests/hikari/clients/test_rest_clients/__init__.py b/tests/hikari/clients/test_rest_clients/__init__.py new file mode 100644 index 0000000000..1c1502a5ca --- /dev/null +++ b/tests/hikari/clients/test_rest_clients/__init__.py @@ -0,0 +1,18 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# Copyright © Nekoka.tt 2019-2020 +# +# This file is part of Hikari. +# +# Hikari is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Hikari is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Hikari. If not, see . diff --git a/tests/hikari/clients/test_rest_clients/test___init__.py b/tests/hikari/clients/test_rest_clients/test___init__.py new file mode 100644 index 0000000000..e281f67648 --- /dev/null +++ b/tests/hikari/clients/test_rest_clients/test___init__.py @@ -0,0 +1,78 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# Copyright © Nekoka.tt 2019-2020 +# +# This file is part of Hikari. +# +# Hikari is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Hikari is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Hikari. If not, see . +import inspect + +import mock +import pytest + +from hikari.clients import configs +from hikari.clients import rest_clients +from hikari.net import rest + + +class TestRESTClient: + @pytest.fixture() + def mock_config(self): + # Mocking the Configs leads to attribute errors regardless of spec set. + return configs.RESTConfig(token="blah.blah.blah") + + def test_init(self, mock_config): + mock_low_level_rest_clients = mock.MagicMock(rest.LowLevelRestfulClient) + with mock.patch.object(rest, "LowLevelRestfulClient", return_value=mock_low_level_rest_clients) as patched_init: + cli = rest_clients.RESTClient(mock_config) + patched_init.assert_called_once_with( + allow_redirects=mock_config.allow_redirects, + connector=mock_config.tcp_connector, + proxy_headers=mock_config.proxy_headers, + proxy_auth=mock_config.proxy_auth, + ssl_context=mock_config.ssl_context, + verify_ssl=mock_config.verify_ssl, + timeout=mock_config.request_timeout, + token=f"{mock_config.token_type} {mock_config.token}", + version=mock_config.rest_version, + ) + assert cli._session is mock_low_level_rest_clients + + def test_inheritance(self): + for attr, routine in ( + member + for component in [ + rest_clients.channels_component.RESTChannelComponent, + rest_clients.current_users_component.RESTCurrentUserComponent, + rest_clients.gateways_component.RESTGatewayComponent, + rest_clients.guilds_component.RESTGuildComponent, + rest_clients.invites_component.RESTInviteComponent, + rest_clients.oauth2_component.RESTOauth2Component, + rest_clients.reactions_component.RESTReactionComponent, + rest_clients.users_component.RESTUserComponent, + rest_clients.voices_component.RESTVoiceComponent, + rest_clients.webhooks_component.RESTWebhookComponent, + ] + for member in inspect.getmembers(component, inspect.isroutine) + ): + if not attr.startswith("__"): + assert hasattr(rest_clients.RESTClient, attr), ( + f"Missing {routine.__qualname__} on RestClient; the component might not be being " + "inherited properly or at all." + ) + assert getattr(rest_clients.RESTClient, attr) == routine, ( + f"Mismatching method found on RestClient; expected {routine.__qualname__} but got " + f"{getattr(rest_clients.RESTClient, attr).__qualname__}. `{attr}` is most likely being declared on" + "multiple components." + ) diff --git a/tests/hikari/clients/test_rest_clients/test_channels_component.py b/tests/hikari/clients/test_rest_clients/test_channels_component.py new file mode 100644 index 0000000000..5109c02061 --- /dev/null +++ b/tests/hikari/clients/test_rest_clients/test_channels_component.py @@ -0,0 +1,820 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# Copyright © Nekoka.tt 2019-2020 +# +# This file is part of Hikari. +# +# Hikari is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Hikari is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Hikari. If not, see . +import contextlib +import datetime +import io + +import mock +import pytest + +from hikari import channels +from hikari import embeds +from hikari import guilds +from hikari import invites +from hikari import media +from hikari import messages +from hikari import snowflakes +from hikari import users +from hikari import webhooks +from hikari.clients.rest_clients import channels_component +from hikari.internal import allowed_mentions +from hikari.internal import conversions +from hikari.internal import pagination +from hikari.net import rest +from tests.hikari import _helpers + + +class TestRESTChannelLogig: + @pytest.fixture() + def rest_channel_logic_impl(self): + mock_low_level_restful_client = mock.MagicMock(rest.LowLevelRestfulClient) + + class RESTChannelLogicImpl(channels_component.RESTChannelComponent): + def __init__(self): + super().__init__(mock_low_level_restful_client) + + return RESTChannelLogicImpl() + + @pytest.mark.asyncio + @_helpers.parametrize_valid_id_formats_for_models("channel", 1234, channels.Channel) + async def test_fetch_channel(self, rest_channel_logic_impl, channel): + mock_payload = {"id": "49494994", "type": 3} + mock_channel_obj = mock.MagicMock(channels.Channel) + rest_channel_logic_impl._session.get_channel.return_value = mock_payload + with mock.patch.object(channels, "deserialize_channel", return_value=mock_channel_obj): + assert await rest_channel_logic_impl.fetch_channel(channel) is mock_channel_obj + rest_channel_logic_impl._session.get_channel.assert_called_once_with(channel_id="1234") + channels.deserialize_channel.assert_called_once_with(mock_payload) + + @pytest.mark.asyncio + @_helpers.parametrize_valid_id_formats_for_models("channel", 379953393319542784, channels.Channel) + @_helpers.parametrize_valid_id_formats_for_models("parent_channel", 115590097100865541, channels.Channel) + @pytest.mark.parametrize("rate_limit_per_user", [42, datetime.timedelta(seconds=42)]) + async def test_update_channel_with_optionals( + self, rest_channel_logic_impl, channel, parent_channel, rate_limit_per_user + ): + mock_payload = {"name": "Qts", "type": 2} + mock_channel_obj = mock.MagicMock(channels.Channel) + mock_overwrite_payload = {"type": "user", "id": 543543543} + mock_overwrite_obj = mock.MagicMock(channels.PermissionOverwrite) + mock_overwrite_obj.serialize = mock.MagicMock(return_value=mock_overwrite_payload) + rest_channel_logic_impl._session.modify_channel.return_value = mock_payload + with mock.patch.object(channels, "deserialize_channel", return_value=mock_channel_obj): + result = await rest_channel_logic_impl.update_channel( + channel=channel, + name="ohNo", + position=7, + topic="camelsAreGreat", + nsfw=True, + bitrate=32000, + user_limit=42, + rate_limit_per_user=rate_limit_per_user, + permission_overwrites=[mock_overwrite_obj], + parent_category=parent_channel, + reason="Get Nyaa'd.", + ) + assert result is mock_channel_obj + rest_channel_logic_impl._session.modify_channel.assert_called_once_with( + channel_id="379953393319542784", + name="ohNo", + position=7, + topic="camelsAreGreat", + nsfw=True, + rate_limit_per_user=42, + bitrate=32000, + user_limit=42, + permission_overwrites=[mock_overwrite_payload], + parent_id="115590097100865541", + reason="Get Nyaa'd.", + ) + mock_overwrite_obj.serialize.assert_called_once() + channels.deserialize_channel.assert_called_once_with(mock_payload) + + @pytest.mark.asyncio + @_helpers.parametrize_valid_id_formats_for_models("channel", 379953393319542784, channels.Channel) + async def test_update_channel_without_optionals( + self, rest_channel_logic_impl, channel, + ): + mock_payload = {"name": "Qts", "type": 2} + mock_channel_obj = mock.MagicMock(channels.Channel) + rest_channel_logic_impl._session.modify_channel.return_value = mock_payload + with mock.patch.object(channels, "deserialize_channel", return_value=mock_channel_obj): + result = await rest_channel_logic_impl.update_channel(channel=channel,) + assert result is mock_channel_obj + rest_channel_logic_impl._session.modify_channel.assert_called_once_with( + channel_id="379953393319542784", + name=..., + position=..., + topic=..., + nsfw=..., + rate_limit_per_user=..., + bitrate=..., + user_limit=..., + permission_overwrites=..., + parent_id=..., + reason=..., + ) + channels.deserialize_channel.assert_called_once_with(mock_payload) + + @pytest.mark.asyncio + @_helpers.parametrize_valid_id_formats_for_models("channel", 55555, channels.Channel) + async def test_delete_channel(self, rest_channel_logic_impl, channel): + rest_channel_logic_impl._session.delete_close_channel.return_value = ... + assert await rest_channel_logic_impl.delete_channel(channel) is None + rest_channel_logic_impl._session.delete_close_channel.assert_called_once_with(channel_id="55555") + + @_helpers.parametrize_valid_id_formats_for_models("channel", 123123123, channels.Channel) + @_helpers.parametrize_valid_id_formats_for_models("message", 777777777, messages.Message) + def test_fetch_messages_after_with_optionals(self, rest_channel_logic_impl, channel, message): + mock_generator = mock.AsyncMock() + with mock.patch.object(pagination, "pagination_handler", return_value=mock_generator): + result = rest_channel_logic_impl.fetch_messages_after(channel=channel, after=message, limit=52) + assert result is mock_generator + pagination.pagination_handler.assert_called_once_with( + channel_id="123123123", + deserializer=messages.Message.deserialize, + direction="after", + start="777777777", + request=rest_channel_logic_impl._session.get_channel_messages, + reversing=True, + limit=52, + ) + + @_helpers.parametrize_valid_id_formats_for_models("channel", 123123123, channels.Channel) + def test_fetch_messages_after_without_optionals(self, rest_channel_logic_impl, channel): + mock_generator = mock.AsyncMock() + with mock.patch.object(pagination, "pagination_handler", return_value=mock_generator): + assert rest_channel_logic_impl.fetch_messages_after(channel=channel) is mock_generator + pagination.pagination_handler.assert_called_once_with( + channel_id="123123123", + deserializer=messages.Message.deserialize, + direction="after", + start="0", + request=rest_channel_logic_impl._session.get_channel_messages, + reversing=True, + limit=None, + ) + + def test_fetch_messages_after_with_datetime_object(self, rest_channel_logic_impl): + mock_generator = mock.AsyncMock() + date = datetime.datetime(2019, 1, 22, 18, 41, 15, 283_000, tzinfo=datetime.timezone.utc) + with mock.patch.object(pagination, "pagination_handler", return_value=mock_generator): + assert rest_channel_logic_impl.fetch_messages_after(channel=123123123, after=date) is mock_generator + pagination.pagination_handler.assert_called_once_with( + channel_id="123123123", + deserializer=messages.Message.deserialize, + direction="after", + start="537340988620800000", + request=rest_channel_logic_impl._session.get_channel_messages, + reversing=True, + limit=None, + ) + + @_helpers.parametrize_valid_id_formats_for_models("channel", 123123123, channels.Channel) + @_helpers.parametrize_valid_id_formats_for_models("message", 777777777, messages.Message) + def test_fetch_messages_before_with_optionals(self, rest_channel_logic_impl, channel, message): + mock_generator = mock.AsyncMock() + with mock.patch.object(pagination, "pagination_handler", return_value=mock_generator): + result = rest_channel_logic_impl.fetch_messages_before(channel=channel, before=message, limit=52) + assert result is mock_generator + pagination.pagination_handler.assert_called_once_with( + channel_id="123123123", + deserializer=messages.Message.deserialize, + direction="before", + start="777777777", + request=rest_channel_logic_impl._session.get_channel_messages, + reversing=False, + limit=52, + ) + + @_helpers.parametrize_valid_id_formats_for_models("channel", 123123123, channels.Channel) + def test_fetch_messages_before_without_optionals(self, rest_channel_logic_impl, channel): + mock_generator = mock.AsyncMock() + with mock.patch.object(pagination, "pagination_handler", return_value=mock_generator): + assert rest_channel_logic_impl.fetch_messages_before(channel=channel) is mock_generator + pagination.pagination_handler.assert_called_once_with( + channel_id="123123123", + deserializer=messages.Message.deserialize, + direction="before", + start=None, + request=rest_channel_logic_impl._session.get_channel_messages, + reversing=False, + limit=None, + ) + + def test_fetch_messages_before_with_datetime_object(self, rest_channel_logic_impl): + mock_generator = mock.AsyncMock() + date = datetime.datetime(2019, 1, 22, 18, 41, 15, 283_000, tzinfo=datetime.timezone.utc) + with mock.patch.object(pagination, "pagination_handler", return_value=mock_generator): + assert rest_channel_logic_impl.fetch_messages_before(channel=123123123, before=date) is mock_generator + pagination.pagination_handler.assert_called_once_with( + channel_id="123123123", + deserializer=messages.Message.deserialize, + direction="before", + start="537340988620800000", + request=rest_channel_logic_impl._session.get_channel_messages, + reversing=False, + limit=None, + ) + + @pytest.mark.asyncio + @_helpers.parametrize_valid_id_formats_for_models("channel", 123123123, channels.Channel) + @_helpers.parametrize_valid_id_formats_for_models("message", 777777777, messages.Message) + async def test_fetch_messages_around_with_limit(self, rest_channel_logic_impl, channel, message): + mock_message_payloads = [{"id": "202020", "content": "Nyaa"}, {"id": "2020222", "content": "Nyaa 2"}] + mock_message_objects = [mock.MagicMock(messages.Message), mock.MagicMock(messages.Message)] + rest_channel_logic_impl._session.get_channel_messages.return_value = mock_message_payloads + with mock.patch.object(messages.Message, "deserialize", side_effect=mock_message_objects): + results = [] + async for result in rest_channel_logic_impl.fetch_messages_around(channel, message, limit=2): + results.append(result) + assert results == mock_message_objects + messages.Message.deserialize.assert_has_calls( + [mock.call(mock_message_payloads[0]), mock.call(mock_message_payloads[1])] + ) + rest_channel_logic_impl._session.get_channel_messages.assert_called_once_with( + channel_id="123123123", around="777777777", limit=2 + ) + + @pytest.mark.asyncio + @_helpers.parametrize_valid_id_formats_for_models("channel", 123123123, channels.Channel) + @_helpers.parametrize_valid_id_formats_for_models("message", 777777777, messages.Message) + async def test_fetch_messages_around_without_limit(self, rest_channel_logic_impl, channel, message): + mock_message_payloads = [{"id": "202020", "content": "Nyaa"}, {"id": "2020222", "content": "Nyaa 2"}] + mock_message_objects = [mock.MagicMock(messages.Message), mock.MagicMock(messages.Message)] + rest_channel_logic_impl._session.get_channel_messages.return_value = mock_message_payloads + with mock.patch.object(messages.Message, "deserialize", side_effect=mock_message_objects): + results = [] + async for result in rest_channel_logic_impl.fetch_messages_around(channel, message): + results.append(result) + assert results == mock_message_objects + messages.Message.deserialize.assert_has_calls( + [mock.call(mock_message_payloads[0]), mock.call(mock_message_payloads[1])] + ) + rest_channel_logic_impl._session.get_channel_messages.assert_called_once_with( + channel_id="123123123", around="777777777", limit=... + ) + + @pytest.mark.asyncio + async def test_fetch_messages_around_with_datetime_object(self, rest_channel_logic_impl): + mock_message_payloads = [{"id": "202020", "content": "Nyaa"}, {"id": "2020222", "content": "Nyaa 2"}] + mock_message_objects = [mock.MagicMock(messages.Message), mock.MagicMock(messages.Message)] + rest_channel_logic_impl._session.get_channel_messages.return_value = mock_message_payloads + date = datetime.datetime(2019, 1, 22, 18, 41, 15, 283_000, tzinfo=datetime.timezone.utc) + with mock.patch.object(messages.Message, "deserialize", side_effect=mock_message_objects): + results = [] + async for result in rest_channel_logic_impl.fetch_messages_around(123123123, date): + results.append(result) + assert results == mock_message_objects + messages.Message.deserialize.assert_has_calls( + [mock.call(mock_message_payloads[0]), mock.call(mock_message_payloads[1])] + ) + rest_channel_logic_impl._session.get_channel_messages.assert_called_once_with( + channel_id="123123123", around="537340988620800000", limit=... + ) + + @pytest.mark.asyncio + @_helpers.parametrize_valid_id_formats_for_models("channel", 55555, channels.Channel) + @_helpers.parametrize_valid_id_formats_for_models("message", 565656, messages.Message) + async def test_fetch_message(self, rest_channel_logic_impl, channel, message): + mock_payload = {"id": "9409404", "content": "I AM A MESSAGE!"} + mock_message_obj = mock.MagicMock(messages.Message) + rest_channel_logic_impl._session.get_channel_message.return_value = mock_payload + with mock.patch.object(messages.Message, "deserialize", return_value=mock_message_obj): + assert await rest_channel_logic_impl.fetch_message(channel=channel, message=message) is mock_message_obj + rest_channel_logic_impl._session.get_channel_message.assert_called_once_with( + channel_id="55555", message_id="565656", + ) + messages.Message.deserialize.assert_called_once_with(mock_payload) + + @pytest.mark.asyncio + @_helpers.parametrize_valid_id_formats_for_models("channel", 694463529998352394, channels.Channel) + async def test_create_message_with_optionals(self, rest_channel_logic_impl, channel): + mock_message_obj = mock.MagicMock(messages.Message) + mock_message_payload = {"id": "2929292992", "content": "222922"} + rest_channel_logic_impl._session.create_message.return_value = mock_message_payload + mock_allowed_mentions_payload = {"parse": ["everyone", "users", "roles"]} + mock_embed_payload = {"description": "424242"} + mock_embed_obj = mock.MagicMock(embeds.Embed) + mock_embed_obj.serialize = mock.MagicMock(return_value=mock_embed_payload) + mock_media_obj = mock.MagicMock() + mock_media_payload = ("aName.png", mock.MagicMock()) + stack = contextlib.ExitStack() + stack.enter_context( + mock.patch.object(allowed_mentions, "generate_allowed_mentions", return_value=mock_allowed_mentions_payload) + ) + stack.enter_context(mock.patch.object(messages.Message, "deserialize", return_value=mock_message_obj)) + stack.enter_context(mock.patch.object(media, "safe_read_file", return_value=mock_media_payload)) + with stack: + result = await rest_channel_logic_impl.create_message( + channel, + content="A CONTENT", + nonce="69696969696969", + tts=True, + files=[mock_media_obj], + embed=mock_embed_obj, + mentions_everyone=False, + user_mentions=False, + role_mentions=False, + ) + assert result is mock_message_obj + media.safe_read_file.assert_called_once_with(mock_media_obj) + messages.Message.deserialize.assert_called_once_with(mock_message_payload) + allowed_mentions.generate_allowed_mentions.assert_called_once_with( + mentions_everyone=False, user_mentions=False, role_mentions=False + ) + rest_channel_logic_impl._session.create_message.assert_called_once_with( + channel_id="694463529998352394", + content="A CONTENT", + nonce="69696969696969", + tts=True, + files=[mock_media_payload], + embed=mock_embed_payload, + allowed_mentions=mock_allowed_mentions_payload, + ) + mock_embed_obj.serialize.assert_called_once() + + @pytest.mark.asyncio + @_helpers.parametrize_valid_id_formats_for_models("channel", 694463529998352394, channels.Channel) + async def test_create_message_without_optionals(self, rest_channel_logic_impl, channel): + mock_message_obj = mock.MagicMock(messages.Message) + mock_message_payload = {"id": "2929292992", "content": "222922"} + rest_channel_logic_impl._session.create_message.return_value = mock_message_payload + mock_allowed_mentions_payload = {"parse": ["everyone", "users", "roles"]} + stack = contextlib.ExitStack() + stack.enter_context( + mock.patch.object(allowed_mentions, "generate_allowed_mentions", return_value=mock_allowed_mentions_payload) + ) + stack.enter_context(mock.patch.object(messages.Message, "deserialize", return_value=mock_message_obj)) + with stack: + assert await rest_channel_logic_impl.create_message(channel) is mock_message_obj + messages.Message.deserialize.assert_called_once_with(mock_message_payload) + allowed_mentions.generate_allowed_mentions.assert_called_once_with( + mentions_everyone=True, user_mentions=True, role_mentions=True + ) + rest_channel_logic_impl._session.create_message.assert_called_once_with( + channel_id="694463529998352394", + content=..., + nonce=..., + tts=..., + files=..., + embed=..., + allowed_mentions=mock_allowed_mentions_payload, + ) + + @pytest.mark.asyncio + async def test_safe_create_message_without_optionals(self, rest_channel_logic_impl): + channel = mock.MagicMock(channels.Channel) + mock_message_obj = mock.MagicMock(messages.Message) + rest_channel_logic_impl.create_message = mock.AsyncMock(return_value=mock_message_obj) + result = await rest_channel_logic_impl.safe_create_message(channel,) + assert result is mock_message_obj + rest_channel_logic_impl.create_message.assert_called_once_with( + channel=channel, + content=..., + nonce=..., + tts=..., + files=..., + embed=..., + mentions_everyone=False, + user_mentions=False, + role_mentions=False, + ) + + @pytest.mark.asyncio + async def test_safe_create_message_with_optionals(self, rest_channel_logic_impl): + channel = mock.MagicMock(channels.Channel) + mock_embed_obj = mock.MagicMock(embeds.Embed) + mock_message_obj = mock.MagicMock(messages.Message) + mock_media_obj = mock.MagicMock(bytes) + rest_channel_logic_impl.create_message = mock.AsyncMock(return_value=mock_message_obj) + result = await rest_channel_logic_impl.safe_create_message( + channel=channel, + content="A CONTENT", + nonce="69696969696969", + tts=True, + files=[mock_media_obj], + embed=mock_embed_obj, + mentions_everyone=True, + user_mentions=True, + role_mentions=True, + ) + assert result is mock_message_obj + rest_channel_logic_impl.create_message.assert_called_once_with( + channel=channel, + content="A CONTENT", + nonce="69696969696969", + tts=True, + files=[mock_media_obj], + embed=mock_embed_obj, + mentions_everyone=True, + user_mentions=True, + role_mentions=True, + ) + + @pytest.mark.asyncio + @_helpers.parametrize_valid_id_formats_for_models("message", 432, messages.Message) + @_helpers.parametrize_valid_id_formats_for_models("channel", 123, channels.Channel) + async def test_update_message_with_optionals(self, rest_channel_logic_impl, message, channel): + mock_payload = {"id": "4242", "content": "I HAVE BEEN UPDATED!"} + mock_message_obj = mock.MagicMock(messages.Message) + mock_embed_payload = {"description": "blahblah"} + mock_embed = mock.MagicMock(embeds.Embed) + mock_embed.serialize = mock.MagicMock(return_value=mock_embed_payload) + mock_allowed_mentions_payload = {"parse": [], "users": ["123"]} + rest_channel_logic_impl._session.edit_message.return_value = mock_payload + stack = contextlib.ExitStack() + stack.enter_context( + mock.patch.object(allowed_mentions, "generate_allowed_mentions", return_value=mock_allowed_mentions_payload) + ) + stack.enter_context(mock.patch.object(messages.Message, "deserialize", return_value=mock_message_obj)) + with stack: + result = await rest_channel_logic_impl.update_message( + message=message, + channel=channel, + content="C O N T E N T", + embed=mock_embed, + flags=messages.MessageFlag.IS_CROSSPOST | messages.MessageFlag.SUPPRESS_EMBEDS, + mentions_everyone=False, + role_mentions=False, + user_mentions=[123123123], + ) + assert result is mock_message_obj + rest_channel_logic_impl._session.edit_message.assert_called_once_with( + channel_id="123", + message_id="432", + content="C O N T E N T", + embed=mock_embed_payload, + flags=6, + allowed_mentions=mock_allowed_mentions_payload, + ) + mock_embed.serialize.assert_called_once() + messages.Message.deserialize.assert_called_once_with(mock_payload) + allowed_mentions.generate_allowed_mentions.assert_called_once_with( + mentions_everyone=False, role_mentions=False, user_mentions=[123123123] + ) + + @pytest.mark.asyncio + @_helpers.parametrize_valid_id_formats_for_models("message", 432, messages.Message) + @_helpers.parametrize_valid_id_formats_for_models("channel", 123, channels.Channel) + async def test_update_message_without_optionals(self, rest_channel_logic_impl, message, channel): + mock_payload = {"id": "4242", "content": "I HAVE BEEN UPDATED!"} + mock_message_obj = mock.MagicMock(messages.Message) + mock_allowed_mentions_payload = {"parse": ["everyone", "users", "roles"]} + rest_channel_logic_impl._session.edit_message.return_value = mock_payload + stack = contextlib.ExitStack() + stack.enter_context( + mock.patch.object(allowed_mentions, "generate_allowed_mentions", return_value=mock_allowed_mentions_payload) + ) + stack.enter_context(mock.patch.object(messages.Message, "deserialize", return_value=mock_message_obj)) + with stack: + assert await rest_channel_logic_impl.update_message(message=message, channel=channel) is mock_message_obj + rest_channel_logic_impl._session.edit_message.assert_called_once_with( + channel_id="123", + message_id="432", + content=..., + embed=..., + flags=..., + allowed_mentions=mock_allowed_mentions_payload, + ) + messages.Message.deserialize.assert_called_once_with(mock_payload) + allowed_mentions.generate_allowed_mentions.assert_called_once_with( + mentions_everyone=True, user_mentions=True, role_mentions=True + ) + + @pytest.mark.asyncio + async def test_safe_update_message_without_optionals(self, rest_channel_logic_impl): + message = mock.MagicMock(messages.Message) + channel = mock.MagicMock(channels.Channel) + mock_message_obj = mock.MagicMock(messages.Message) + rest_channel_logic_impl.update_message = mock.AsyncMock(return_value=mock_message_obj) + result = await rest_channel_logic_impl.safe_update_message(message=message, channel=channel,) + assert result is mock_message_obj + rest_channel_logic_impl.update_message.assert_called_once_with( + message=message, + channel=channel, + content=..., + embed=..., + flags=..., + mentions_everyone=False, + role_mentions=False, + user_mentions=False, + ) + + @pytest.mark.asyncio + async def test_safe_update_message_with_optionals(self, rest_channel_logic_impl): + message = mock.MagicMock(messages.Message) + channel = mock.MagicMock(channels.Channel) + mock_embed = mock.MagicMock(embeds.Embed) + mock_message_obj = mock.MagicMock(messages.Message) + rest_channel_logic_impl.update_message = mock.AsyncMock(return_value=mock_message_obj) + result = await rest_channel_logic_impl.safe_update_message( + message=message, + channel=channel, + content="C O N T E N T", + embed=mock_embed, + flags=messages.MessageFlag.IS_CROSSPOST | messages.MessageFlag.SUPPRESS_EMBEDS, + mentions_everyone=True, + role_mentions=True, + user_mentions=True, + ) + assert result is mock_message_obj + rest_channel_logic_impl.update_message.assert_called_once_with( + message=message, + channel=channel, + content="C O N T E N T", + embed=mock_embed, + flags=messages.MessageFlag.IS_CROSSPOST | messages.MessageFlag.SUPPRESS_EMBEDS, + mentions_everyone=True, + role_mentions=True, + user_mentions=True, + ) + + @pytest.mark.asyncio + @_helpers.parametrize_valid_id_formats_for_models("channel", 379953393319542784, channels.Channel) + @_helpers.parametrize_valid_id_formats_for_models("message", 115590097100865541, messages.Message) + async def test_delete_messages_singular(self, rest_channel_logic_impl, channel, message): + rest_channel_logic_impl._session.delete_message.return_value = ... + assert await rest_channel_logic_impl.delete_messages(channel, message) is None + rest_channel_logic_impl._session.delete_message.assert_called_once_with( + channel_id="379953393319542784", message_id="115590097100865541", + ) + + @pytest.mark.asyncio + @_helpers.parametrize_valid_id_formats_for_models("channel", 379953393319542784, channels.Channel) + @_helpers.parametrize_valid_id_formats_for_models("message", 115590097100865541, messages.Message) + @_helpers.parametrize_valid_id_formats_for_models("additional_message", 115590097100865541, messages.Message) + async def test_delete_messages_singular_after_duplicate_removal( + self, rest_channel_logic_impl, channel, message, additional_message + ): + rest_channel_logic_impl._session.delete_message.return_value = ... + assert await rest_channel_logic_impl.delete_messages(channel, message, additional_message) is None + rest_channel_logic_impl._session.delete_message.assert_called_once_with( + channel_id="379953393319542784", message_id="115590097100865541", + ) + + @pytest.mark.asyncio + @_helpers.parametrize_valid_id_formats_for_models("channel", 379953393319542784, channels.Channel) + @_helpers.parametrize_valid_id_formats_for_models("message", 115590097100865541, messages.Message) + @_helpers.parametrize_valid_id_formats_for_models("additional_message", 572144340277919754, messages.Message) + async def test_delete_messages_bulk_removes_duplicates( + self, rest_channel_logic_impl, channel, message, additional_message + ): + rest_channel_logic_impl._session.bulk_delete_messages.return_value = ... + assert ( + await rest_channel_logic_impl.delete_messages(channel, message, additional_message, 115590097100865541) + is None + ) + rest_channel_logic_impl._session.bulk_delete_messages.assert_called_once_with( + channel_id="379953393319542784", messages=["115590097100865541", "572144340277919754"], + ) + rest_channel_logic_impl._session.delete_message.assert_not_called() + + @pytest.mark.asyncio + @_helpers.assert_raises(type_=ValueError) + async def test_delete_messages_raises_value_error_on_over_100_messages(self, rest_channel_logic_impl): + rest_channel_logic_impl._session.bulk_delete_messages.return_value = ... + assert await rest_channel_logic_impl.delete_messages(123123, *list(range(0, 111))) is None + + @pytest.mark.asyncio + @_helpers.parametrize_valid_id_formats_for_models("channel", 4123123, channels.Channel) + @_helpers.parametrize_valid_id_formats_for_models("overwrite", 9999, channels.PermissionOverwrite) + async def test_update_channel_overwrite_with_optionals(self, rest_channel_logic_impl, channel, overwrite): + rest_channel_logic_impl._session.edit_channel_permissions.return_value = ... + result = await rest_channel_logic_impl.update_channel_overwrite( + channel=channel, + overwrite=overwrite, + target_type="member", + allow=messages.MessageFlag.IS_CROSSPOST | messages.MessageFlag.SUPPRESS_EMBEDS, + deny=21, + reason="get Nyaa'd", + ) + assert result is None + rest_channel_logic_impl._session.edit_channel_permissions.assert_called_once_with( + channel_id="4123123", overwrite_id="9999", type_="member", allow=6, deny=21, reason="get Nyaa'd", + ) + + @pytest.mark.asyncio + @_helpers.parametrize_valid_id_formats_for_models("channel", 4123123, channels.Channel) + @_helpers.parametrize_valid_id_formats_for_models("overwrite", 9999, channels.PermissionOverwrite) + async def test_update_channel_overwrite_without_optionals(self, rest_channel_logic_impl, channel, overwrite): + rest_channel_logic_impl._session.edit_channel_permissions.return_value = ... + result = await rest_channel_logic_impl.update_channel_overwrite( + channel=channel, overwrite=overwrite, target_type="member" + ) + assert result is None + rest_channel_logic_impl._session.edit_channel_permissions.assert_called_once_with( + channel_id="4123123", overwrite_id="9999", type_="member", allow=..., deny=..., reason=..., + ) + + @pytest.mark.asyncio + @pytest.mark.parametrize( + "target", + [ + mock.MagicMock(guilds.GuildRole, id=snowflakes.Snowflake(9999), __int__=guilds.GuildRole.__int__), + mock.MagicMock(users.User, id=snowflakes.Snowflake(9999), __int__=users.User.__int__), + ], + ) + async def test_update_channel_overwrite_with_alternative_target_object(self, rest_channel_logic_impl, target): + rest_channel_logic_impl._session.edit_channel_permissions.return_value = ... + result = await rest_channel_logic_impl.update_channel_overwrite( + channel=4123123, overwrite=target, target_type="member" + ) + assert result is None + rest_channel_logic_impl._session.edit_channel_permissions.assert_called_once_with( + channel_id="4123123", overwrite_id="9999", type_="member", allow=..., deny=..., reason=..., + ) + + @pytest.mark.asyncio + @_helpers.parametrize_valid_id_formats_for_models("channel", 123123123, channels.Channel) + async def test_fetch_invites_for_channel(self, rest_channel_logic_impl, channel): + mock_invite_payload = {"code": "ogogogogogogogo", "guild_id": "123123123"} + mock_invite_obj = mock.MagicMock(invites.InviteWithMetadata) + rest_channel_logic_impl._session.get_channel_invites.return_value = [mock_invite_payload] + with mock.patch.object(invites.InviteWithMetadata, "deserialize", return_value=mock_invite_obj): + assert await rest_channel_logic_impl.fetch_invites_for_channel(channel=channel) == [mock_invite_obj] + rest_channel_logic_impl._session.get_channel_invites.assert_called_once_with(channel_id="123123123") + invites.InviteWithMetadata.deserialize.assert_called_once_with(mock_invite_payload) + + @pytest.mark.asyncio + @_helpers.parametrize_valid_id_formats_for_models("channel", 234123, channels.Channel) + @_helpers.parametrize_valid_id_formats_for_models("user", 333333, users.User) + @pytest.mark.parametrize("max_age", [4444, datetime.timedelta(seconds=4444)]) + async def test_create_invite_for_channel_with_optionals(self, rest_channel_logic_impl, channel, user, max_age): + mock_invite_payload = {"code": "ogogogogogogogo", "guild_id": "123123123"} + mock_invite_obj = mock.MagicMock(invites.InviteWithMetadata) + rest_channel_logic_impl._session.create_channel_invite.return_value = mock_invite_payload + with mock.patch.object(invites.InviteWithMetadata, "deserialize", return_value=mock_invite_obj): + result = await rest_channel_logic_impl.create_invite_for_channel( + channel, + max_age=max_age, + max_uses=444, + temporary=True, + unique=False, + target_user=user, + target_user_type=invites.TargetUserType.STREAM, + reason="Hello there.", + ) + assert result is mock_invite_obj + rest_channel_logic_impl._session.create_channel_invite.assert_called_once_with( + channel_id="234123", + max_age=4444, + max_uses=444, + temporary=True, + unique=False, + target_user="333333", + target_user_type=1, + reason="Hello there.", + ) + invites.InviteWithMetadata.deserialize.assert_called_once_with(mock_invite_payload) + + @pytest.mark.asyncio + @_helpers.parametrize_valid_id_formats_for_models("channel", 234123, channels.Channel) + async def test_create_invite_for_channel_without_optionals(self, rest_channel_logic_impl, channel): + mock_invite_payload = {"code": "ogogogogogogogo", "guild_id": "123123123"} + mock_invite_obj = mock.MagicMock(invites.InviteWithMetadata) + rest_channel_logic_impl._session.create_channel_invite.return_value = mock_invite_payload + with mock.patch.object(invites.InviteWithMetadata, "deserialize", return_value=mock_invite_obj): + assert await rest_channel_logic_impl.create_invite_for_channel(channel) is mock_invite_obj + rest_channel_logic_impl._session.create_channel_invite.assert_called_once_with( + channel_id="234123", + max_age=..., + max_uses=..., + temporary=..., + unique=..., + target_user=..., + target_user_type=..., + reason=..., + ) + invites.InviteWithMetadata.deserialize.assert_called_once_with(mock_invite_payload) + + @pytest.mark.asyncio + @_helpers.parametrize_valid_id_formats_for_models("channel", 379953393319542784, channels.Channel) + @_helpers.parametrize_valid_id_formats_for_models("overwrite", 123123123, channels.PermissionOverwrite) + async def test_delete_channel_overwrite(self, rest_channel_logic_impl, channel, overwrite): + rest_channel_logic_impl._session.delete_channel_permission.return_value = ... + assert await rest_channel_logic_impl.delete_channel_overwrite(channel=channel, overwrite=overwrite) is None + rest_channel_logic_impl._session.delete_channel_permission.assert_called_once_with( + channel_id="379953393319542784", overwrite_id="123123123", + ) + + @pytest.mark.asyncio + @pytest.mark.parametrize( + "target", + [ + mock.MagicMock(guilds.GuildRole, id=snowflakes.Snowflake(123123123), __int__=guilds.GuildRole.__int__), + mock.MagicMock(users.User, id=snowflakes.Snowflake(123123123), __int__=users.User.__int__), + ], + ) + async def test_delete_channel_overwrite_with_alternative_target_objects(self, rest_channel_logic_impl, target): + rest_channel_logic_impl._session.delete_channel_permission.return_value = ... + assert ( + await rest_channel_logic_impl.delete_channel_overwrite(channel=379953393319542784, overwrite=target) is None + ) + rest_channel_logic_impl._session.delete_channel_permission.assert_called_once_with( + channel_id="379953393319542784", overwrite_id="123123123", + ) + + @pytest.mark.asyncio + @_helpers.parametrize_valid_id_formats_for_models("channel", 379953393319542784, channels.PermissionOverwrite) + async def test_trigger_typing(self, rest_channel_logic_impl, channel): + rest_channel_logic_impl._session.trigger_typing_indicator.return_value = ... + assert await rest_channel_logic_impl.trigger_typing(channel) is None + rest_channel_logic_impl._session.trigger_typing_indicator.assert_called_once_with( + channel_id="379953393319542784" + ) + + @pytest.mark.asyncio + @_helpers.parametrize_valid_id_formats_for_models("channel", 123123123, channels.Channel) + async def test_fetch_pins(self, rest_channel_logic_impl, channel): + mock_message_payload = {"id": "21232", "content": "CONTENT"} + mock_message_obj = mock.MagicMock(messages.Message, id=21232) + rest_channel_logic_impl._session.get_pinned_messages.return_value = [mock_message_payload] + with mock.patch.object(messages.Message, "deserialize", return_value=mock_message_obj): + assert await rest_channel_logic_impl.fetch_pins(channel) == {21232: mock_message_obj} + rest_channel_logic_impl._session.get_pinned_messages.assert_called_once_with(channel_id="123123123") + messages.Message.deserialize.assert_called_once_with(mock_message_payload) + + @pytest.mark.asyncio + @_helpers.parametrize_valid_id_formats_for_models("channel", 292929, channels.Channel) + @_helpers.parametrize_valid_id_formats_for_models("message", 123123, messages.Message) + async def test_pin_message(self, rest_channel_logic_impl, channel, message): + rest_channel_logic_impl._session.add_pinned_channel_message.return_value = ... + assert await rest_channel_logic_impl.pin_message(channel, message) is None + rest_channel_logic_impl._session.add_pinned_channel_message.assert_called_once_with( + channel_id="292929", message_id="123123" + ) + + @pytest.mark.asyncio + @_helpers.parametrize_valid_id_formats_for_models("channel", 292929, channels.Channel) + @_helpers.parametrize_valid_id_formats_for_models("message", 123123, messages.Message) + async def test_unpin_message(self, rest_channel_logic_impl, channel, message): + rest_channel_logic_impl._session.delete_pinned_channel_message.return_value = ... + assert await rest_channel_logic_impl.unpin_message(channel, message) is None + rest_channel_logic_impl._session.delete_pinned_channel_message.assert_called_once_with( + channel_id="292929", message_id="123123" + ) + + @pytest.mark.asyncio + @_helpers.parametrize_valid_id_formats_for_models("channel", 115590097100865541, channels.Channel) + async def test_create_webhook_with_optionals(self, rest_channel_logic_impl, channel): + mock_webhook_payload = {"id": "29292929", "channel_id": "2292992"} + mock_webhook_obj = mock.MagicMock(webhooks.Webhook) + rest_channel_logic_impl._session.create_webhook.return_value = mock_webhook_payload + mock_image_obj = mock.MagicMock(io.BytesIO) + mock_image_data = mock.MagicMock(bytes) + stack = contextlib.ExitStack() + stack.enter_context(mock.patch.object(webhooks.Webhook, "deserialize", return_value=mock_webhook_obj)) + stack.enter_context(mock.patch.object(conversions, "get_bytes_from_resource", return_value=mock_image_data)) + with stack: + result = await rest_channel_logic_impl.create_webhook( + channel=channel, name="aWebhook", avatar_data=mock_image_obj, reason="And a webhook is born." + ) + assert result is mock_webhook_obj + conversions.get_bytes_from_resource.assert_called_once_with(mock_image_obj) + webhooks.Webhook.deserialize.assert_called_once_with(mock_webhook_payload) + rest_channel_logic_impl._session.create_webhook.assert_called_once_with( + channel_id="115590097100865541", name="aWebhook", avatar=mock_image_data, reason="And a webhook is born." + ) + + @pytest.mark.asyncio + @_helpers.parametrize_valid_id_formats_for_models("channel", 115590097100865541, channels.Channel) + async def test_create_webhook_without_optionals(self, rest_channel_logic_impl, channel): + mock_webhook_payload = {"id": "29292929", "channel_id": "2292992"} + mock_webhook_obj = mock.MagicMock(webhooks.Webhook) + rest_channel_logic_impl._session.create_webhook.return_value = mock_webhook_payload + with mock.patch.object(webhooks.Webhook, "deserialize", return_value=mock_webhook_obj): + assert await rest_channel_logic_impl.create_webhook(channel=channel, name="aWebhook") is mock_webhook_obj + webhooks.Webhook.deserialize.assert_called_once_with(mock_webhook_payload) + rest_channel_logic_impl._session.create_webhook.assert_called_once_with( + channel_id="115590097100865541", name="aWebhook", avatar=..., reason=... + ) + + @pytest.mark.asyncio + @_helpers.parametrize_valid_id_formats_for_models("channel", 115590097100865541, channels.GuildChannel) + async def test_fetch_channel_webhooks(self, rest_channel_logic_impl, channel): + mock_webhook_payload = {"id": "29292929", "channel_id": "2292992"} + mock_webhook_obj = mock.MagicMock(webhooks.Webhook) + rest_channel_logic_impl._session.get_channel_webhooks.return_value = [mock_webhook_payload] + with mock.patch.object(webhooks.Webhook, "deserialize", return_value=mock_webhook_obj): + assert await rest_channel_logic_impl.fetch_channel_webhooks(channel) == [mock_webhook_obj] + rest_channel_logic_impl._session.get_channel_webhooks.assert_called_once_with( + channel_id="115590097100865541" + ) + webhooks.Webhook.deserialize.assert_called_once_with(mock_webhook_payload) diff --git a/tests/hikari/clients/test_rest_clients/test_component_base.py b/tests/hikari/clients/test_rest_clients/test_component_base.py new file mode 100644 index 0000000000..5b5f4a573b --- /dev/null +++ b/tests/hikari/clients/test_rest_clients/test_component_base.py @@ -0,0 +1,52 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# Copyright © Nekoka.tt 2019-2020 +# +# This file is part of Hikari. +# +# Hikari is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Hikari is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Hikari. If not, see . +import mock +import pytest + +from hikari.clients.rest_clients import component_base +from hikari.net import rest + + +class TestBaseRESTComponent: + @pytest.fixture() + def low_level_rest_impl(self) -> rest.LowLevelRestfulClient: + return mock.MagicMock(rest.LowLevelRestfulClient) + + @pytest.fixture() + def rest_clients_impl(self, low_level_rest_impl) -> component_base.BaseRESTComponent: + class RestClientImpl(component_base.BaseRESTComponent): + def __init__(self): + super().__init__(low_level_rest_impl) + + return RestClientImpl() + + @pytest.mark.asyncio + async def test___aenter___and___aexit__(self, rest_clients_impl): + rest_clients_impl.close = mock.AsyncMock() + async with rest_clients_impl as client: + assert client is rest_clients_impl + rest_clients_impl.close.assert_called_once_with() + + @pytest.mark.asyncio + async def test_close_awaits_session_close(self, rest_clients_impl): + await rest_clients_impl.close() + rest_clients_impl._session.close.assert_called_once() + + def test_session_property(self, low_level_rest_impl, rest_clients_impl): + assert rest_clients_impl.session is low_level_rest_impl diff --git a/tests/hikari/clients/test_rest_clients/test_current_users_component.py b/tests/hikari/clients/test_rest_clients/test_current_users_component.py new file mode 100644 index 0000000000..bf973ea0e5 --- /dev/null +++ b/tests/hikari/clients/test_rest_clients/test_current_users_component.py @@ -0,0 +1,194 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# Copyright © Nekoka.tt 2019-2020 +# +# This file is part of Hikari. +# +# Hikari is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Hikari is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Hikari. If not, see . +import contextlib +import datetime +import io + +import mock +import pytest + +from hikari import channels +from hikari import guilds +from hikari import oauth2 +from hikari import users +from hikari.clients.rest_clients import current_users_component +from hikari.internal import conversions +from hikari.internal import pagination +from hikari.net import rest +from tests.hikari import _helpers + + +class TestRESTInviteLogic: + @pytest.fixture() + def rest_clients_impl(self): + mock_low_level_restful_client = mock.MagicMock(rest.LowLevelRestfulClient) + + class RESTCurrentUserLogicImpl(current_users_component.RESTCurrentUserComponent): + def __init__(self): + super().__init__(mock_low_level_restful_client) + + return RESTCurrentUserLogicImpl() + + @pytest.mark.asyncio + async def test_fetch_me(self, rest_clients_impl): + mock_user_payload = {"username": "A User", "id": "202020200202"} + mock_user_obj = mock.MagicMock(users.MyUser) + rest_clients_impl._session.get_current_user.return_value = mock_user_payload + with mock.patch.object(users.MyUser, "deserialize", return_value=mock_user_obj): + assert await rest_clients_impl.fetch_me() is mock_user_obj + rest_clients_impl._session.get_current_user.assert_called_once() + users.MyUser.deserialize.assert_called_once_with(mock_user_payload) + + @pytest.mark.asyncio + async def test_update_me_with_optionals(self, rest_clients_impl): + mock_user_payload = {"id": "424242", "flags": "420", "discriminator": "6969"} + mock_user_obj = mock.MagicMock(users.MyUser) + rest_clients_impl._session.modify_current_user.return_value = mock_user_payload + mock_avatar_obj = mock.MagicMock(io.BytesIO) + mock_avatar_data = mock.MagicMock(bytes) + stack = contextlib.ExitStack() + stack.enter_context(mock.patch.object(users.MyUser, "deserialize", return_value=mock_user_obj)) + stack.enter_context(mock.patch.object(conversions, "get_bytes_from_resource", return_value=mock_avatar_data)) + with stack: + assert await rest_clients_impl.update_me(username="aNewName", avatar_data=mock_avatar_obj) is mock_user_obj + rest_clients_impl._session.modify_current_user.assert_called_once_with( + username="aNewName", avatar=mock_avatar_data + ) + conversions.get_bytes_from_resource.assert_called_once_with(mock_avatar_obj) + users.MyUser.deserialize.assert_called_once_with(mock_user_payload) + + @pytest.mark.asyncio + async def test_update_me_without_optionals(self, rest_clients_impl): + mock_user_payload = {"id": "424242", "flags": "420", "discriminator": "6969"} + mock_user_obj = mock.MagicMock(users.MyUser) + rest_clients_impl._session.modify_current_user.return_value = mock_user_payload + with mock.patch.object(users.MyUser, "deserialize", return_value=mock_user_obj): + assert await rest_clients_impl.update_me() is mock_user_obj + rest_clients_impl._session.modify_current_user.assert_called_once_with(username=..., avatar=...) + users.MyUser.deserialize.assert_called_once_with(mock_user_payload) + + @pytest.mark.asyncio + async def test_fetch_my_connections(self, rest_clients_impl): + mock_connection_payload = {"id": "odnkwu", "type": "twitch", "name": "eric"} + mock_connection_obj = mock.MagicMock(oauth2.OwnConnection) + rest_clients_impl._session.get_current_user_connections.return_value = [mock_connection_payload] + with mock.patch.object(oauth2.OwnConnection, "deserialize", return_value=mock_connection_obj): + assert await rest_clients_impl.fetch_my_connections() == [mock_connection_obj] + rest_clients_impl._session.get_current_user_connections.assert_called_once() + oauth2.OwnConnection.deserialize.assert_called_once_with(mock_connection_payload) + + @_helpers.parametrize_valid_id_formats_for_models("guild", 574921006817476608, guilds.Guild) + def test_fetch_my_guilds_after_with_optionals(self, rest_clients_impl, guild): + mock_generator = mock.AsyncMock() + with mock.patch.object(pagination, "pagination_handler", return_value=mock_generator): + assert rest_clients_impl.fetch_my_guilds_after(after=guild, limit=50) is mock_generator + pagination.pagination_handler.assert_called_once_with( + deserializer=oauth2.OwnGuild.deserialize, + direction="after", + request=rest_clients_impl._session.get_current_user_guilds, + reversing=False, + start="574921006817476608", + limit=50, + ) + + def test_fetch_my_guilds_after_without_optionals(self, rest_clients_impl): + mock_generator = mock.AsyncMock() + with mock.patch.object(pagination, "pagination_handler", return_value=mock_generator): + assert rest_clients_impl.fetch_my_guilds_after() is mock_generator + pagination.pagination_handler.assert_called_once_with( + deserializer=oauth2.OwnGuild.deserialize, + direction="after", + request=rest_clients_impl._session.get_current_user_guilds, + reversing=False, + start="0", + limit=None, + ) + + def test_fetch_my_guilds_after_with_datetime_object(self, rest_clients_impl): + mock_generator = mock.AsyncMock() + date = datetime.datetime(2019, 1, 22, 18, 41, 15, 283_000, tzinfo=datetime.timezone.utc) + with mock.patch.object(pagination, "pagination_handler", return_value=mock_generator): + assert rest_clients_impl.fetch_my_guilds_after(after=date) is mock_generator + pagination.pagination_handler.assert_called_once_with( + deserializer=oauth2.OwnGuild.deserialize, + direction="after", + request=rest_clients_impl._session.get_current_user_guilds, + reversing=False, + start="537340988620800000", + limit=None, + ) + + @_helpers.parametrize_valid_id_formats_for_models("guild", 574921006817476608, guilds.Guild) + def test_fetch_my_guilds_before_with_optionals(self, rest_clients_impl, guild): + mock_generator = mock.AsyncMock() + with mock.patch.object(pagination, "pagination_handler", return_value=mock_generator): + assert rest_clients_impl.fetch_my_guilds_before(before=guild, limit=50) is mock_generator + pagination.pagination_handler.assert_called_once_with( + deserializer=oauth2.OwnGuild.deserialize, + direction="before", + request=rest_clients_impl._session.get_current_user_guilds, + reversing=False, + start="574921006817476608", + limit=50, + ) + + def test_fetch_my_guilds_before_without_optionals(self, rest_clients_impl): + mock_generator = mock.AsyncMock() + with mock.patch.object(pagination, "pagination_handler", return_value=mock_generator): + assert rest_clients_impl.fetch_my_guilds_before() is mock_generator + pagination.pagination_handler.assert_called_once_with( + deserializer=oauth2.OwnGuild.deserialize, + direction="before", + request=rest_clients_impl._session.get_current_user_guilds, + reversing=False, + start=None, + limit=None, + ) + + def test_fetch_my_guilds_before_with_datetime_object(self, rest_clients_impl): + mock_generator = mock.AsyncMock() + date = datetime.datetime(2019, 1, 22, 18, 41, 15, 283_000, tzinfo=datetime.timezone.utc) + with mock.patch.object(pagination, "pagination_handler", return_value=mock_generator): + assert rest_clients_impl.fetch_my_guilds_before(before=date) is mock_generator + pagination.pagination_handler.assert_called_once_with( + deserializer=oauth2.OwnGuild.deserialize, + direction="before", + request=rest_clients_impl._session.get_current_user_guilds, + reversing=False, + start="537340988620800000", + limit=None, + ) + + @pytest.mark.asyncio + @_helpers.parametrize_valid_id_formats_for_models("guild", 574921006817476608, guilds.Guild) + async def test_leave_guild(self, rest_clients_impl, guild): + rest_clients_impl._session.leave_guild.return_value = ... + assert await rest_clients_impl.leave_guild(guild) is None + rest_clients_impl._session.leave_guild.assert_called_once_with(guild_id="574921006817476608") + + @pytest.mark.asyncio + @_helpers.parametrize_valid_id_formats_for_models("recipient", 115590097100865541, users.User) + async def test_create_dm_channel(self, rest_clients_impl, recipient): + mock_dm_payload = {"id": "2202020", "type": 2, "recipients": []} + mock_dm_obj = mock.MagicMock(channels.DMChannel) + rest_clients_impl._session.create_dm.return_value = mock_dm_payload + with mock.patch.object(channels.DMChannel, "deserialize", return_value=mock_dm_obj): + assert await rest_clients_impl.create_dm_channel(recipient) is mock_dm_obj + rest_clients_impl._session.create_dm.assert_called_once_with(recipient_id="115590097100865541") + channels.DMChannel.deserialize.assert_called_once_with(mock_dm_payload) diff --git a/tests/hikari/clients/test_rest_clients/test_gateways_component.py b/tests/hikari/clients/test_rest_clients/test_gateways_component.py new file mode 100644 index 0000000000..abce2490a0 --- /dev/null +++ b/tests/hikari/clients/test_rest_clients/test_gateways_component.py @@ -0,0 +1,54 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# Copyright © Nekoka.tt 2019-2020 +# +# This file is part of Hikari. +# +# Hikari is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Hikari is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Hikari. If not, see . + +import mock +import pytest + +from hikari import gateway_entities +from hikari.clients.rest_clients import gateways_component +from hikari.net import rest + + +class TestRESTReactionLogic: + @pytest.fixture() + def rest_gateway_logic_impl(self): + mock_low_level_restful_client = mock.MagicMock(rest.LowLevelRestfulClient) + + class RESTGatewayLogicImpl(gateways_component.RESTGatewayComponent): + def __init__(self): + super().__init__(mock_low_level_restful_client) + + return RESTGatewayLogicImpl() + + @pytest.mark.asyncio + async def test_fetch_gateway_url(self, rest_gateway_logic_impl): + mock_url = "wss://gateway.discord.gg/" + rest_gateway_logic_impl._session.get_gateway.return_value = mock_url + assert await rest_gateway_logic_impl.fetch_gateway_url() == mock_url + rest_gateway_logic_impl._session.get_gateway.assert_called_once() + + @pytest.mark.asyncio + async def test_fetch_gateway_bot(self, rest_gateway_logic_impl): + mock_payload = {"url": "wss://gateway.discord.gg/", "shards": 9, "session_start_limit": {}} + mock_gateway_bot_obj = mock.MagicMock(gateway_entities.GatewayBot) + rest_gateway_logic_impl._session.get_gateway_bot.return_value = mock_payload + with mock.patch.object(gateway_entities.GatewayBot, "deserialize", return_value=mock_gateway_bot_obj): + assert await rest_gateway_logic_impl.fetch_gateway_bot() is mock_gateway_bot_obj + rest_gateway_logic_impl._session.get_gateway_bot.assert_called_once() + gateway_entities.GatewayBot.deserialize.assert_called_once_with(mock_payload) diff --git a/tests/hikari/clients/test_rest_clients/test_guilds_component.py b/tests/hikari/clients/test_rest_clients/test_guilds_component.py new file mode 100644 index 0000000000..fd4ab17217 --- /dev/null +++ b/tests/hikari/clients/test_rest_clients/test_guilds_component.py @@ -0,0 +1,1146 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# Copyright © Nekoka.tt 2019-2020 +# +# This file is part of Hikari. +# +# Hikari is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Hikari is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Hikari. If not, see . +import contextlib +import datetime +import io + +import mock +import pytest + +from hikari import audit_logs +from hikari import channels +from hikari import colors +from hikari import emojis +from hikari import guilds +from hikari import invites +from hikari import permissions +from hikari import users +from hikari import voices +from hikari import webhooks +from hikari.clients.rest_clients import guilds_component +from hikari.internal import conversions +from hikari.internal import pagination +from hikari.net import rest +from tests.hikari import _helpers + + +def test__get_member_id(): + member = mock.MagicMock( + guilds.GuildMember, user=mock.MagicMock(users.User, id=123123123, __int__=users.User.__int__) + ) + assert guilds_component._get_member_id(member) == "123123123" + + +class TestRESTGuildLogic: + @pytest.fixture() + def rest_guild_logic_impl(self): + mock_low_level_restful_client = mock.MagicMock(rest.LowLevelRestfulClient) + + class RESTGuildLogicImpl(guilds_component.RESTGuildComponent): + def __init__(self): + super().__init__(mock_low_level_restful_client) + + return RESTGuildLogicImpl() + + @_helpers.parametrize_valid_id_formats_for_models("guild", 379953393319542784, guilds.Guild) + @_helpers.parametrize_valid_id_formats_for_models("user", 22222222, users.User) + @_helpers.parametrize_valid_id_formats_for_models("before", 123123123123, audit_logs.AuditLogEntry) + def test_fetch_audit_log_entries_before_with_optionals(self, rest_guild_logic_impl, guild, before, user): + mock_audit_log_iterator = mock.MagicMock(audit_logs.AuditLogIterator) + with mock.patch.object(audit_logs, "AuditLogIterator", return_value=mock_audit_log_iterator): + result = rest_guild_logic_impl.fetch_audit_log_entries_before( + guild, before=before, user=user, action_type=audit_logs.AuditLogEventType.MEMBER_MOVE, limit=42, + ) + assert result is mock_audit_log_iterator + audit_logs.AuditLogIterator.assert_called_once_with( + guild_id="379953393319542784", + request=rest_guild_logic_impl._session.get_guild_audit_log, + before="123123123123", + user_id="22222222", + action_type=26, + limit=42, + ) + + @_helpers.parametrize_valid_id_formats_for_models("guild", 379953393319542784, guilds.Guild) + def test_fetch_audit_log_entries_before_without_optionals(self, rest_guild_logic_impl, guild): + mock_audit_log_iterator = mock.MagicMock(audit_logs.AuditLogIterator) + with mock.patch.object(audit_logs, "AuditLogIterator", return_value=mock_audit_log_iterator): + assert rest_guild_logic_impl.fetch_audit_log_entries_before(guild) is mock_audit_log_iterator + audit_logs.AuditLogIterator.assert_called_once_with( + guild_id="379953393319542784", + request=rest_guild_logic_impl._session.get_guild_audit_log, + before=None, + user_id=..., + action_type=..., + limit=None, + ) + + def test_fetch_audit_log_entries_before_with_datetime_object(self, rest_guild_logic_impl): + mock_audit_log_iterator = mock.MagicMock(audit_logs.AuditLogIterator) + with mock.patch.object(audit_logs, "AuditLogIterator", return_value=mock_audit_log_iterator): + date = datetime.datetime(2019, 1, 22, 18, 41, 15, 283_000, tzinfo=datetime.timezone.utc) + result = rest_guild_logic_impl.fetch_audit_log_entries_before(123123123, before=date) + assert result is mock_audit_log_iterator + audit_logs.AuditLogIterator.assert_called_once_with( + guild_id="123123123", + request=rest_guild_logic_impl._session.get_guild_audit_log, + before="537340988620800000", + user_id=..., + action_type=..., + limit=None, + ) + + @pytest.mark.asyncio + @_helpers.parametrize_valid_id_formats_for_models("guild", 379953393319542784, guilds.Guild) + @_helpers.parametrize_valid_id_formats_for_models("user", 115590097100865541, users.User) + @_helpers.parametrize_valid_id_formats_for_models("before", 1231231123, audit_logs.AuditLogEntry) + async def test_fetch_audit_log_with_optionals(self, rest_guild_logic_impl, guild, user, before): + mock_audit_log_payload = {"entries": [], "integrations": [], "webhooks": [], "users": []} + mock_audit_log_obj = mock.MagicMock(audit_logs.AuditLog) + rest_guild_logic_impl._session.get_guild_audit_log.return_value = mock_audit_log_payload + with mock.patch.object(audit_logs.AuditLog, "deserialize", return_value=mock_audit_log_obj): + result = await rest_guild_logic_impl.fetch_audit_log( + guild, user=user, action_type=audit_logs.AuditLogEventType.MEMBER_MOVE, limit=100, before=before, + ) + assert result is mock_audit_log_obj + rest_guild_logic_impl._session.get_guild_audit_log.assert_called_once_with( + guild_id="379953393319542784", + user_id="115590097100865541", + action_type=26, + limit=100, + before="1231231123", + ) + + @pytest.mark.asyncio + @_helpers.parametrize_valid_id_formats_for_models("guild", 379953393319542784, guilds.Guild) + async def test_fetch_audit_log_without_optionals(self, rest_guild_logic_impl, guild): + mock_audit_log_payload = {"entries": [], "integrations": [], "webhooks": [], "users": []} + mock_audit_log_obj = mock.MagicMock(audit_logs.AuditLog) + rest_guild_logic_impl._session.get_guild_audit_log.return_value = mock_audit_log_payload + with mock.patch.object(audit_logs.AuditLog, "deserialize", return_value=mock_audit_log_obj): + assert await rest_guild_logic_impl.fetch_audit_log(guild) is mock_audit_log_obj + rest_guild_logic_impl._session.get_guild_audit_log.assert_called_once_with( + guild_id="379953393319542784", user_id=..., action_type=..., limit=..., before=... + ) + + @pytest.mark.asyncio + @_helpers.parametrize_valid_id_formats_for_models("guild", 379953393319542784, guilds.Guild) + async def test_fetch_audit_log_handles_datetime_object(self, rest_guild_logic_impl, guild): + mock_audit_log_payload = {"entries": [], "integrations": [], "webhooks": [], "users": []} + mock_audit_log_obj = mock.MagicMock(audit_logs.AuditLog) + rest_guild_logic_impl._session.get_guild_audit_log.return_value = mock_audit_log_payload + date = datetime.datetime(2019, 1, 22, 18, 41, 15, 283_000, tzinfo=datetime.timezone.utc) + with mock.patch.object(audit_logs.AuditLog, "deserialize", return_value=mock_audit_log_obj): + assert await rest_guild_logic_impl.fetch_audit_log(guild, before=date) is mock_audit_log_obj + rest_guild_logic_impl._session.get_guild_audit_log.assert_called_once_with( + guild_id="379953393319542784", user_id=..., action_type=..., limit=..., before="537340988620800000" + ) + + @pytest.mark.asyncio + @_helpers.parametrize_valid_id_formats_for_models("guild", 93443949, guilds.Guild) + @_helpers.parametrize_valid_id_formats_for_models("emoji", 40404040404, emojis.GuildEmoji) + async def test_fetch_guild_emoji(self, rest_guild_logic_impl, guild, emoji): + mock_emoji_payload = {"id": "92929", "name": "nyaa", "animated": True} + mock_emoji_obj = mock.MagicMock(emojis.GuildEmoji) + rest_guild_logic_impl._session.get_guild_emoji.return_value = mock_emoji_payload + with mock.patch.object(emojis.GuildEmoji, "deserialize", return_value=mock_emoji_obj): + assert await rest_guild_logic_impl.fetch_guild_emoji(guild=guild, emoji=emoji) is mock_emoji_obj + rest_guild_logic_impl._session.get_guild_emoji.assert_called_once_with( + guild_id="93443949", emoji_id="40404040404", + ) + emojis.GuildEmoji.deserialize.assert_called_once_with(mock_emoji_payload) + + @pytest.mark.asyncio + @_helpers.parametrize_valid_id_formats_for_models("guild", 93443949, guilds.Guild) + async def test_fetch_guild_emojis(self, rest_guild_logic_impl, guild): + mock_emoji_payload = {"id": "92929", "name": "nyaa", "animated": True} + mock_emoji_obj = mock.MagicMock(emojis.GuildEmoji) + rest_guild_logic_impl._session.list_guild_emojis.return_value = [mock_emoji_payload] + with mock.patch.object(emojis.GuildEmoji, "deserialize", return_value=mock_emoji_obj): + assert await rest_guild_logic_impl.fetch_guild_emojis(guild=guild) == [mock_emoji_obj] + rest_guild_logic_impl._session.list_guild_emojis.assert_called_once_with(guild_id="93443949",) + emojis.GuildEmoji.deserialize.assert_called_once_with(mock_emoji_payload) + + @pytest.mark.asyncio + @_helpers.parametrize_valid_id_formats_for_models("guild", 93443949, guilds.Guild) + @_helpers.parametrize_valid_id_formats_for_models("role", 537340989808050216, guilds.GuildRole) + async def test_create_guild_emoji_with_optionals(self, rest_guild_logic_impl, guild, role): + mock_emoji_payload = {"id": "229292929", "animated": True} + mock_emoji_obj = mock.MagicMock(emojis.GuildEmoji) + rest_guild_logic_impl._session.create_guild_emoji.return_value = mock_emoji_payload + mock_image_obj = mock.MagicMock(io.BytesIO) + mock_image_data = mock.MagicMock(bytes) + stack = contextlib.ExitStack() + stack.enter_context(mock.patch.object(conversions, "get_bytes_from_resource", return_value=mock_image_data)) + stack.enter_context(mock.patch.object(emojis.GuildEmoji, "deserialize", return_value=mock_emoji_obj)) + with stack: + result = await rest_guild_logic_impl.create_guild_emoji( + guild=guild, name="fairEmoji", image_data=mock_image_obj, roles=[role], reason="hello", + ) + assert result is mock_emoji_obj + emojis.GuildEmoji.deserialize.assert_called_once_with(mock_emoji_payload) + conversions.get_bytes_from_resource.assert_called_once_with(mock_image_obj) + rest_guild_logic_impl._session.create_guild_emoji.assert_called_once_with( + guild_id="93443949", name="fairEmoji", image=mock_image_data, roles=["537340989808050216"], reason="hello", + ) + + @pytest.mark.asyncio + @_helpers.parametrize_valid_id_formats_for_models("guild", 93443949, guilds.Guild) + async def test_create_guild_emoji_without_optionals(self, rest_guild_logic_impl, guild): + mock_emoji_payload = {"id": "229292929", "animated": True} + mock_emoji_obj = mock.MagicMock(emojis.GuildEmoji) + rest_guild_logic_impl._session.create_guild_emoji.return_value = mock_emoji_payload + mock_image_obj = mock.MagicMock(io.BytesIO) + mock_image_data = mock.MagicMock(bytes) + stack = contextlib.ExitStack() + stack.enter_context(mock.patch.object(conversions, "get_bytes_from_resource", return_value=mock_image_data)) + stack.enter_context(mock.patch.object(emojis.GuildEmoji, "deserialize", return_value=mock_emoji_obj)) + with stack: + result = await rest_guild_logic_impl.create_guild_emoji( + guild=guild, name="fairEmoji", image_data=mock_image_obj, + ) + assert result is mock_emoji_obj + emojis.GuildEmoji.deserialize.assert_called_once_with(mock_emoji_payload) + conversions.get_bytes_from_resource.assert_called_once_with(mock_image_obj) + rest_guild_logic_impl._session.create_guild_emoji.assert_called_once_with( + guild_id="93443949", name="fairEmoji", image=mock_image_data, roles=..., reason=..., + ) + + @pytest.mark.asyncio + @_helpers.parametrize_valid_id_formats_for_models("guild", 93443949, guilds.Guild) + @_helpers.parametrize_valid_id_formats_for_models("emoji", 4123321, emojis.GuildEmoji) + async def test_update_guild_emoji_without_optionals(self, rest_guild_logic_impl, guild, emoji): + mock_emoji_payload = {"id": "202020", "name": "Nyaa", "animated": True} + mock_emoji_obj = mock.MagicMock(emojis.GuildEmoji) + rest_guild_logic_impl._session.modify_guild_emoji.return_value = mock_emoji_payload + with mock.patch.object(emojis.GuildEmoji, "deserialize", return_value=mock_emoji_obj): + assert await rest_guild_logic_impl.update_guild_emoji(guild, emoji) is mock_emoji_obj + rest_guild_logic_impl._session.modify_guild_emoji.assert_called_once_with( + guild_id="93443949", emoji_id="4123321", name=..., roles=..., reason=..., + ) + emojis.GuildEmoji.deserialize.assert_called_once_with(mock_emoji_payload) + + @pytest.mark.asyncio + @_helpers.parametrize_valid_id_formats_for_models("guild", 93443949, guilds.Guild) + @_helpers.parametrize_valid_id_formats_for_models("emoji", 4123321, emojis.GuildEmoji) + @_helpers.parametrize_valid_id_formats_for_models("role", 123123123, guilds.GuildRole) + async def test_update_guild_emoji_with_optionals(self, rest_guild_logic_impl, guild, emoji, role): + mock_emoji_payload = {"id": "202020", "name": "Nyaa", "animated": True} + mock_emoji_obj = mock.MagicMock(emojis.GuildEmoji) + rest_guild_logic_impl._session.modify_guild_emoji.return_value = mock_emoji_payload + with mock.patch.object(emojis.GuildEmoji, "deserialize", return_value=mock_emoji_obj): + result = await rest_guild_logic_impl.update_guild_emoji( + guild, emoji, name="Nyaa", roles=[role], reason="Agent 42" + ) + assert result is mock_emoji_obj + rest_guild_logic_impl._session.modify_guild_emoji.assert_called_once_with( + guild_id="93443949", emoji_id="4123321", name="Nyaa", roles=["123123123"], reason="Agent 42", + ) + emojis.GuildEmoji.deserialize.assert_called_once_with(mock_emoji_payload) + + @pytest.mark.asyncio + @_helpers.parametrize_valid_id_formats_for_models("guild", 93443949, guilds.Guild) + @_helpers.parametrize_valid_id_formats_for_models("emoji", 4123321, emojis.GuildEmoji) + async def test_delete_guild_emoji(self, rest_guild_logic_impl, guild, emoji): + rest_guild_logic_impl._session.delete_guild_emoji.return_value = ... + assert await rest_guild_logic_impl.delete_guild_emoji(guild, emoji) is None + rest_guild_logic_impl._session.delete_guild_emoji.assert_called_once_with( + guild_id="93443949", emoji_id="4123321" + ) + + @pytest.mark.asyncio + @pytest.mark.parametrize("region", [mock.MagicMock(voices.VoiceRegion, id="LONDON"), "LONDON"]) + async def test_create_guild_with_optionals(self, rest_guild_logic_impl, region): + mock_guild_payload = {"id": "299292929292992", "region": "LONDON"} + mock_guild_obj = mock.MagicMock(guilds.Guild) + rest_guild_logic_impl._session.create_guild.return_value = mock_guild_payload + mock_image_obj = mock.MagicMock(io.BytesIO) + mock_image_data = mock.MagicMock(bytes) + mock_role_payload = {"permissions": 123123} + mock_role_obj = mock.MagicMock(guilds.GuildRole) + mock_role_obj.serialize = mock.MagicMock(return_value=mock_role_payload) + mock_channel_payload = {"type": 2, "name": "aChannel"} + mock_channel_obj = mock.MagicMock(channels.GuildChannelBuilder) + mock_channel_obj.serialize = mock.MagicMock(return_value=mock_channel_payload) + stack = contextlib.ExitStack() + stack.enter_context(mock.patch.object(guilds.Guild, "deserialize", return_value=mock_guild_obj)) + stack.enter_context(mock.patch.object(conversions, "get_bytes_from_resource", return_value=mock_image_data)) + with stack: + result = await rest_guild_logic_impl.create_guild( + name="OK", + region=region, + icon_data=mock_image_obj, + verification_level=guilds.GuildVerificationLevel.NONE, + default_message_notifications=guilds.GuildMessageNotificationsLevel.ONLY_MENTIONS, + explicit_content_filter=guilds.GuildExplicitContentFilterLevel.MEMBERS_WITHOUT_ROLES, + roles=[mock_role_obj], + channels=[mock_channel_obj], + ) + assert result is mock_guild_obj + conversions.get_bytes_from_resource.assert_called_once_with(mock_image_obj) + guilds.Guild.deserialize.assert_called_once_with(mock_guild_payload) + mock_channel_obj.serialize.assert_called_once() + mock_role_obj.serialize.assert_called_once() + rest_guild_logic_impl._session.create_guild.assert_called_once_with( + name="OK", + region="LONDON", + icon=mock_image_data, + verification_level=0, + default_message_notifications=1, + explicit_content_filter=1, + roles=[mock_role_payload], + channels=[mock_channel_payload], + ) + + @pytest.mark.asyncio + async def test_create_guild_without_optionals(self, rest_guild_logic_impl): + mock_guild_payload = {"id": "299292929292992", "region": "LONDON"} + mock_guild_obj = mock.MagicMock(guilds.Guild) + rest_guild_logic_impl._session.create_guild.return_value = mock_guild_payload + with mock.patch.object(guilds.Guild, "deserialize", return_value=mock_guild_obj): + assert await rest_guild_logic_impl.create_guild(name="OK") is mock_guild_obj + guilds.Guild.deserialize.assert_called_once_with(mock_guild_payload) + rest_guild_logic_impl._session.create_guild.assert_called_once_with( + name="OK", + region=..., + icon=..., + verification_level=..., + default_message_notifications=..., + explicit_content_filter=..., + roles=..., + channels=..., + ) + + @pytest.mark.asyncio + @_helpers.parametrize_valid_id_formats_for_models("guild", 379953393319542784, guilds.Guild) + async def test_fetch_guild(self, rest_guild_logic_impl, guild): + mock_guild_payload = {"id": "94949494", "name": "A guild", "roles": []} + mock_guild_obj = mock.MagicMock(guilds.Guild) + rest_guild_logic_impl._session.get_guild.return_value = mock_guild_payload + with mock.patch.object(guilds.Guild, "deserialize", return_value=mock_guild_obj): + assert await rest_guild_logic_impl.fetch_guild(guild) is mock_guild_obj + rest_guild_logic_impl._session.get_guild.assert_called_once_with(guild_id="379953393319542784") + guilds.Guild.deserialize.assert_called_once_with(mock_guild_payload) + + @pytest.mark.asyncio + @_helpers.parametrize_valid_id_formats_for_models("guild", 379953393319542784, guilds.Guild) + async def test_fetch_guild_preview(self, rest_guild_logic_impl, guild): + mock_guild_preview_payload = {"id": "94949494", "name": "A guild", "emojis": []} + mock_guild_preview_obj = mock.MagicMock(guilds.GuildPreview) + rest_guild_logic_impl._session.get_guild_preview.return_value = mock_guild_preview_payload + with mock.patch.object(guilds.GuildPreview, "deserialize", return_value=mock_guild_preview_obj): + assert await rest_guild_logic_impl.fetch_guild_preview(guild) is mock_guild_preview_obj + rest_guild_logic_impl._session.get_guild_preview.assert_called_once_with(guild_id="379953393319542784") + guilds.GuildPreview.deserialize.assert_called_once_with(mock_guild_preview_payload) + + @pytest.mark.asyncio + @_helpers.parametrize_valid_id_formats_for_models("guild", 379953393319542784, guilds.Guild) + @_helpers.parametrize_valid_id_formats_for_models("afk_channel", 669517187031105607, guilds.Guild) + @_helpers.parametrize_valid_id_formats_for_models("owner", 379953393319542784, users.User) + @_helpers.parametrize_valid_id_formats_for_models("system_channel", 537340989808050216, users.User) + @pytest.mark.parametrize("region", ["LONDON", mock.MagicMock(voices.VoiceRegion, id="LONDON")]) + @pytest.mark.parametrize("afk_timeout", [300, datetime.timedelta(seconds=300)]) + async def test_update_guild_with_optionals( + self, rest_guild_logic_impl, guild, region, afk_channel, afk_timeout, owner, system_channel + ): + mock_guild_payload = {"id": "424242", "splash": "2lmKmklsdlksalkd"} + mock_guild_obj = mock.MagicMock(guilds.Guild) + rest_guild_logic_impl._session.modify_guild.return_value = mock_guild_payload + mock_icon_data = mock.MagicMock(bytes) + mock_icon_obj = mock.MagicMock(io.BytesIO) + mock_splash_data = mock.MagicMock(bytes) + mock_splash_obj = mock.MagicMock(io.BytesIO) + stack = contextlib.ExitStack() + stack.enter_context(mock.patch.object(guilds.Guild, "deserialize", return_value=mock_guild_obj)) + stack.enter_context( + mock.patch.object(conversions, "get_bytes_from_resource", side_effect=[mock_icon_data, mock_splash_data]) + ) + with stack: + result = await rest_guild_logic_impl.update_guild( + guild, + name="aNewName", + region=region, + verification_level=guilds.GuildVerificationLevel.LOW, + default_message_notifications=guilds.GuildMessageNotificationsLevel.ONLY_MENTIONS, + explicit_content_filter=guilds.GuildExplicitContentFilterLevel.ALL_MEMBERS, + afk_channel=afk_channel, + afk_timeout=afk_timeout, + icon_data=mock_icon_obj, + owner=owner, + splash_data=mock_splash_obj, + system_channel=system_channel, + reason="A good reason", + ) + assert result is mock_guild_obj + guilds.Guild.deserialize.assert_called_once_with(mock_guild_payload) + conversions.get_bytes_from_resource.has_calls(mock.call(mock_icon_obj), mock.call(mock_splash_obj)) + rest_guild_logic_impl._session.modify_guild.assert_called_once_with( + guild_id="379953393319542784", + name="aNewName", + region="LONDON", + verification_level=1, + default_message_notifications=1, + explicit_content_filter=2, + afk_channel_id="669517187031105607", + afk_timeout=300, + icon=mock_icon_data, + owner_id="379953393319542784", + splash=mock_splash_data, + system_channel_id="537340989808050216", + reason="A good reason", + ) + + @pytest.mark.asyncio + @_helpers.parametrize_valid_id_formats_for_models("guild", 379953393319542784, guilds.Guild) + async def test_update_guild_without_optionals(self, rest_guild_logic_impl, guild): + mock_guild_payload = {"id": "424242", "splash": "2lmKmklsdlksalkd"} + mock_guild_obj = mock.MagicMock(guilds.Guild) + rest_guild_logic_impl._session.modify_guild.return_value = mock_guild_payload + with mock.patch.object(guilds.Guild, "deserialize", return_value=mock_guild_obj): + assert await rest_guild_logic_impl.update_guild(guild) is mock_guild_obj + rest_guild_logic_impl._session.modify_guild.assert_called_once_with( + guild_id="379953393319542784", + name=..., + region=..., + verification_level=..., + default_message_notifications=..., + explicit_content_filter=..., + afk_channel_id=..., + afk_timeout=..., + icon=..., + owner_id=..., + splash=..., + system_channel_id=..., + reason=..., + ) + + @pytest.mark.asyncio + @_helpers.parametrize_valid_id_formats_for_models("guild", 379953393319542784, guilds.Guild) + async def test_delete_guild(self, rest_guild_logic_impl, guild): + rest_guild_logic_impl._session.delete_guild.return_value = ... + assert await rest_guild_logic_impl.delete_guild(guild) is None + rest_guild_logic_impl._session.delete_guild.assert_called_once_with(guild_id="379953393319542784") + + @pytest.mark.asyncio + @_helpers.parametrize_valid_id_formats_for_models("guild", 379953393319542784, guilds.Guild) + async def test_fetch_guild_channels(self, rest_guild_logic_impl, guild): + mock_channel_payload = {"id": "292929", "type": 1, "description": "A CHANNEL"} + mock_channel_obj = mock.MagicMock(channels.GuildChannel) + rest_guild_logic_impl._session.list_guild_channels.return_value = [mock_channel_payload] + with mock.patch.object(channels, "deserialize_channel", return_value=mock_channel_obj): + assert await rest_guild_logic_impl.fetch_guild_channels(guild) == [mock_channel_obj] + rest_guild_logic_impl._session.list_guild_channels.assert_called_once_with(guild_id="379953393319542784") + channels.deserialize_channel.assert_called_once_with(mock_channel_payload) + + @pytest.mark.asyncio + @_helpers.parametrize_valid_id_formats_for_models("guild", 123123123, guilds.Guild) + @_helpers.parametrize_valid_id_formats_for_models("category", 5555, channels.GuildCategory) + @pytest.mark.parametrize("rate_limit_per_user", [500, datetime.timedelta(seconds=500)]) + async def test_create_guild_channel_with_optionals( + self, rest_guild_logic_impl, guild, category, rate_limit_per_user + ): + mock_channel_payload = {"id": "22929292", "type": "5", "description": "A C H A N N E L"} + mock_channel_obj = mock.MagicMock(channels.GuildChannel) + mock_overwrite_payload = {"type": "member", "id": "30303030"} + mock_overwrite_obj = mock.MagicMock( + channels.PermissionOverwrite, serialize=mock.MagicMock(return_value=mock_overwrite_payload) + ) + rest_guild_logic_impl._session.create_guild_channel.return_value = mock_channel_payload + with mock.patch.object(channels, "deserialize_channel", return_value=mock_channel_obj): + result = await rest_guild_logic_impl.create_guild_channel( + guild, + "Hi-i-am-a-name", + channel_type=channels.ChannelType.GUILD_VOICE, + position=42, + topic="A TOPIC", + nsfw=True, + rate_limit_per_user=rate_limit_per_user, + bitrate=36000, + user_limit=5, + permission_overwrites=[mock_overwrite_obj], + parent_category=category, + reason="A GOOD REASON!", + ) + assert result is mock_channel_obj + mock_overwrite_obj.serialize.assert_called_once() + rest_guild_logic_impl._session.create_guild_channel.assert_called_once_with( + guild_id="123123123", + name="Hi-i-am-a-name", + type_=2, + position=42, + topic="A TOPIC", + nsfw=True, + rate_limit_per_user=500, + bitrate=36000, + user_limit=5, + permission_overwrites=[mock_overwrite_payload], + parent_id="5555", + reason="A GOOD REASON!", + ) + channels.deserialize_channel.assert_called_once_with(mock_channel_payload) + + @pytest.mark.asyncio + @_helpers.parametrize_valid_id_formats_for_models("guild", 123123123, guilds.Guild) + async def test_create_guild_channel_without_optionals(self, rest_guild_logic_impl, guild): + mock_channel_payload = {"id": "22929292", "type": "5", "description": "A C H A N N E L"} + mock_channel_obj = mock.MagicMock(channels.GuildChannel) + rest_guild_logic_impl._session.create_guild_channel.return_value = mock_channel_payload + with mock.patch.object(channels, "deserialize_channel", return_value=mock_channel_obj): + assert await rest_guild_logic_impl.create_guild_channel(guild, "Hi-i-am-a-name") is mock_channel_obj + rest_guild_logic_impl._session.create_guild_channel.assert_called_once_with( + guild_id="123123123", + name="Hi-i-am-a-name", + type_=..., + position=..., + topic=..., + nsfw=..., + rate_limit_per_user=..., + bitrate=..., + user_limit=..., + permission_overwrites=..., + parent_id=..., + reason=..., + ) + channels.deserialize_channel.assert_called_once_with(mock_channel_payload) + + @pytest.mark.asyncio + @_helpers.parametrize_valid_id_formats_for_models("guild", 123123123, guilds.Guild) + @_helpers.parametrize_valid_id_formats_for_models("channel", 379953393319542784, channels.GuildChannel) + @_helpers.parametrize_valid_id_formats_for_models("second_channel", 115590097100865541, channels.GuildChannel) + async def test_reposition_guild_channels(self, rest_guild_logic_impl, guild, channel, second_channel): + rest_guild_logic_impl._session.modify_guild_channel_positions.return_value = ... + assert await rest_guild_logic_impl.reposition_guild_channels(guild, (1, channel), (2, second_channel)) is None + rest_guild_logic_impl._session.modify_guild_channel_positions.assert_called_once_with( + "123123123", ("379953393319542784", 1), ("115590097100865541", 2) + ) + + @pytest.mark.asyncio + @_helpers.parametrize_valid_id_formats_for_models("guild", 444444, guilds.Guild) + @_helpers.parametrize_valid_id_formats_for_models("user", 123123123123, users.User) + async def test_fetch_member(self, rest_guild_logic_impl, guild, user): + mock_member_payload = {"user": {}, "nick": "! Agent 47"} + mock_member_obj = mock.MagicMock(guilds.GuildMember) + rest_guild_logic_impl._session.get_guild_member.return_value = mock_member_payload + with mock.patch.object(guilds.GuildMember, "deserialize", return_value=mock_member_obj): + assert await rest_guild_logic_impl.fetch_member(guild, user) is mock_member_obj + rest_guild_logic_impl._session.get_guild_member.assert_called_once_with( + guild_id="444444", user_id="123123123123" + ) + guilds.GuildMember.deserialize.assert_called_once_with(mock_member_payload) + + @_helpers.parametrize_valid_id_formats_for_models("guild", 574921006817476608, guilds.Guild) + @_helpers.parametrize_valid_id_formats_for_models("user", 115590097100865541, users.User) + def test_fetch_members_after_with_optionals(self, rest_guild_logic_impl, guild, user): + mock_generator = mock.AsyncMock() + with mock.patch.object(pagination, "pagination_handler", return_value=mock_generator): + assert rest_guild_logic_impl.fetch_members_after(guild, after=user, limit=34) is mock_generator + pagination.pagination_handler.assert_called_once_with( + guild_id="574921006817476608", + deserializer=guilds.GuildMember.deserialize, + direction="after", + request=rest_guild_logic_impl._session.list_guild_members, + reversing=False, + start="115590097100865541", + limit=34, + id_getter=guilds_component._get_member_id, + ) + + @_helpers.parametrize_valid_id_formats_for_models("guild", 574921006817476608, guilds.Guild) + def test_fetch_members_after_without_optionals(self, rest_guild_logic_impl, guild): + mock_generator = mock.AsyncMock() + with mock.patch.object(pagination, "pagination_handler", return_value=mock_generator): + assert rest_guild_logic_impl.fetch_members_after(guild) is mock_generator + pagination.pagination_handler.assert_called_once_with( + guild_id="574921006817476608", + deserializer=guilds.GuildMember.deserialize, + direction="after", + request=rest_guild_logic_impl._session.list_guild_members, + reversing=False, + start="0", + limit=None, + id_getter=guilds_component._get_member_id, + ) + + def test_fetch_members_after_with_datetime_object(self, rest_guild_logic_impl): + mock_generator = mock.AsyncMock() + date = datetime.datetime(2019, 1, 22, 18, 41, 15, 283_000, tzinfo=datetime.timezone.utc) + with mock.patch.object(pagination, "pagination_handler", return_value=mock_generator): + assert rest_guild_logic_impl.fetch_members_after(574921006817476608, after=date) is mock_generator + pagination.pagination_handler.assert_called_once_with( + guild_id="574921006817476608", + deserializer=guilds.GuildMember.deserialize, + direction="after", + request=rest_guild_logic_impl._session.list_guild_members, + reversing=False, + start="537340988620800000", + limit=None, + id_getter=guilds_component._get_member_id, + ) + + @pytest.mark.asyncio + @_helpers.parametrize_valid_id_formats_for_models("guild", 229292992, guilds.Guild) + @_helpers.parametrize_valid_id_formats_for_models("user", 1010101010, users.User) + @_helpers.parametrize_valid_id_formats_for_models("role", 11100010, guilds.GuildRole) + @_helpers.parametrize_valid_id_formats_for_models("channel", 33333333, channels.GuildVoiceChannel) + async def test_update_member_with_optionals(self, rest_guild_logic_impl, guild, user, role, channel): + rest_guild_logic_impl._session.modify_guild_member.return_value = ... + result = await rest_guild_logic_impl.update_member( + guild, + user, + nickname="Nick's Name", + roles=[role], + mute=True, + deaf=False, + voice_channel=channel, + reason="Get Tagged.", + ) + assert result is None + rest_guild_logic_impl._session.modify_guild_member.assert_called_once_with( + guild_id="229292992", + user_id="1010101010", + nick="Nick's Name", + roles=["11100010"], + mute=True, + deaf=False, + channel_id="33333333", + reason="Get Tagged.", + ) + + @pytest.mark.asyncio + @_helpers.parametrize_valid_id_formats_for_models("guild", 229292992, guilds.Guild) + @_helpers.parametrize_valid_id_formats_for_models("user", 1010101010, users.User) + async def test_update_member_without_optionals(self, rest_guild_logic_impl, guild, user): + rest_guild_logic_impl._session.modify_guild_member.return_value = ... + assert await rest_guild_logic_impl.update_member(guild, user) is None + rest_guild_logic_impl._session.modify_guild_member.assert_called_once_with( + guild_id="229292992", + user_id="1010101010", + nick=..., + roles=..., + mute=..., + deaf=..., + channel_id=..., + reason=..., + ) + + @pytest.mark.asyncio + @_helpers.parametrize_valid_id_formats_for_models("guild", 229292992, guilds.Guild) + async def test_update_my_member_nickname_with_reason(self, rest_guild_logic_impl, guild): + rest_guild_logic_impl._session.modify_current_user_nick.return_value = ... + result = await rest_guild_logic_impl.update_my_member_nickname( + guild, "Nick's nick", reason="I want to drink your blood." + ) + assert result is None + rest_guild_logic_impl._session.modify_current_user_nick.assert_called_once_with( + guild_id="229292992", nick="Nick's nick", reason="I want to drink your blood." + ) + + @pytest.mark.asyncio + @_helpers.parametrize_valid_id_formats_for_models("guild", 229292992, guilds.Guild) + async def test_update_my_member_nickname_without_reason(self, rest_guild_logic_impl, guild): + rest_guild_logic_impl._session.modify_current_user_nick.return_value = ... + assert await rest_guild_logic_impl.update_my_member_nickname(guild, "Nick's nick") is None + rest_guild_logic_impl._session.modify_current_user_nick.assert_called_once_with( + guild_id="229292992", nick="Nick's nick", reason=... + ) + + @pytest.mark.asyncio + @_helpers.parametrize_valid_id_formats_for_models("guild", 123123123, guilds.Guild) + @_helpers.parametrize_valid_id_formats_for_models("user", 4444444, users.User) + @_helpers.parametrize_valid_id_formats_for_models("role", 101010101, guilds.GuildRole) + async def test_add_role_to_member_with_reason(self, rest_guild_logic_impl, guild, user, role): + rest_guild_logic_impl._session.add_guild_member_role.return_value = ... + assert await rest_guild_logic_impl.add_role_to_member(guild, user, role, reason="Get role'd") is None + rest_guild_logic_impl._session.add_guild_member_role.assert_called_once_with( + guild_id="123123123", user_id="4444444", role_id="101010101", reason="Get role'd" + ) + + @pytest.mark.asyncio + @_helpers.parametrize_valid_id_formats_for_models("guild", 123123123, guilds.Guild) + @_helpers.parametrize_valid_id_formats_for_models("user", 4444444, users.User) + @_helpers.parametrize_valid_id_formats_for_models("role", 101010101, guilds.GuildRole) + async def test_add_role_to_member_without_reason(self, rest_guild_logic_impl, guild, user, role): + rest_guild_logic_impl._session.add_guild_member_role.return_value = ... + assert await rest_guild_logic_impl.add_role_to_member(guild, user, role) is None + rest_guild_logic_impl._session.add_guild_member_role.assert_called_once_with( + guild_id="123123123", user_id="4444444", role_id="101010101", reason=... + ) + + @pytest.mark.asyncio + @_helpers.parametrize_valid_id_formats_for_models("guild", 123123123, guilds.Guild) + @_helpers.parametrize_valid_id_formats_for_models("user", 4444444, users.User) + @_helpers.parametrize_valid_id_formats_for_models("role", 101010101, guilds.GuildRole) + async def test_remove_role_from_member_with_reason(self, rest_guild_logic_impl, guild, user, role): + rest_guild_logic_impl._session.remove_guild_member_role.return_value = ... + assert await rest_guild_logic_impl.remove_role_from_member(guild, user, role, reason="Get role'd") is None + rest_guild_logic_impl._session.remove_guild_member_role.assert_called_once_with( + guild_id="123123123", user_id="4444444", role_id="101010101", reason="Get role'd" + ) + + @pytest.mark.asyncio + @_helpers.parametrize_valid_id_formats_for_models("guild", 123123123, guilds.Guild) + @_helpers.parametrize_valid_id_formats_for_models("user", 4444444, users.User) + @_helpers.parametrize_valid_id_formats_for_models("role", 101010101, guilds.GuildRole) + async def test_remove_role_from_member_without_reason(self, rest_guild_logic_impl, guild, user, role): + rest_guild_logic_impl._session.remove_guild_member_role.return_value = ... + assert await rest_guild_logic_impl.remove_role_from_member(guild, user, role) is None + rest_guild_logic_impl._session.remove_guild_member_role.assert_called_once_with( + guild_id="123123123", user_id="4444444", role_id="101010101", reason=... + ) + + @pytest.mark.asyncio + @_helpers.parametrize_valid_id_formats_for_models("guild", 123123123, guilds.Guild) + @_helpers.parametrize_valid_id_formats_for_models("user", 4444444, users.User) + async def test_kick_member_with_reason(self, rest_guild_logic_impl, guild, user): + rest_guild_logic_impl._session.remove_guild_member.return_value = ... + assert await rest_guild_logic_impl.kick_member(guild, user, reason="TO DO") is None + rest_guild_logic_impl._session.remove_guild_member.assert_called_once_with( + guild_id="123123123", user_id="4444444", reason="TO DO" + ) + + @pytest.mark.asyncio + @_helpers.parametrize_valid_id_formats_for_models("guild", 123123123, guilds.Guild) + @_helpers.parametrize_valid_id_formats_for_models("user", 4444444, users.User) + async def test_kick_member_without_reason(self, rest_guild_logic_impl, guild, user): + rest_guild_logic_impl._session.remove_guild_member.return_value = ... + assert await rest_guild_logic_impl.kick_member(guild, user) is None + rest_guild_logic_impl._session.remove_guild_member.assert_called_once_with( + guild_id="123123123", user_id="4444444", reason=..., + ) + + @pytest.mark.asyncio + @_helpers.parametrize_valid_id_formats_for_models("guild", 123123123, guilds.Guild) + @_helpers.parametrize_valid_id_formats_for_models("user", 4444444, users.User) + async def test_fetch_ban(self, rest_guild_logic_impl, guild, user): + mock_ban_payload = {"reason": "42'd", "user": {}} + mock_ban_obj = mock.MagicMock(guilds.GuildMemberBan) + rest_guild_logic_impl._session.get_guild_ban.return_value = mock_ban_payload + with mock.patch.object(guilds.GuildMemberBan, "deserialize", return_value=mock_ban_obj): + assert await rest_guild_logic_impl.fetch_ban(guild, user) is mock_ban_obj + rest_guild_logic_impl._session.get_guild_ban.assert_called_once_with( + guild_id="123123123", user_id="4444444" + ) + guilds.GuildMemberBan.deserialize.assert_called_once_with(mock_ban_payload) + + @pytest.mark.asyncio + @_helpers.parametrize_valid_id_formats_for_models("guild", 123123123, guilds.Guild) + async def test_fetch_bans(self, rest_guild_logic_impl, guild): + mock_ban_payload = {"reason": "42'd", "user": {}} + mock_ban_obj = mock.MagicMock(guilds.GuildMemberBan) + rest_guild_logic_impl._session.get_guild_bans.return_value = [mock_ban_payload] + with mock.patch.object(guilds.GuildMemberBan, "deserialize", return_value=mock_ban_obj): + assert await rest_guild_logic_impl.fetch_bans(guild) == [mock_ban_obj] + rest_guild_logic_impl._session.get_guild_bans.assert_called_once_with(guild_id="123123123") + guilds.GuildMemberBan.deserialize.assert_called_once_with(mock_ban_payload) + + @pytest.mark.asyncio + @_helpers.parametrize_valid_id_formats_for_models("guild", 123123123, guilds.Guild) + @_helpers.parametrize_valid_id_formats_for_models("user", 4444444, users.User) + @pytest.mark.parametrize("delete_message_days", [datetime.timedelta(days=12), 12]) + async def test_ban_member_with_optionals(self, rest_guild_logic_impl, guild, user, delete_message_days): + rest_guild_logic_impl._session.create_guild_ban.return_value = ... + result = await rest_guild_logic_impl.ban_member( + guild, user, delete_message_days=delete_message_days, reason="bye" + ) + assert result is None + rest_guild_logic_impl._session.create_guild_ban.assert_called_once_with( + guild_id="123123123", user_id="4444444", delete_message_days=12, reason="bye" + ) + + @pytest.mark.asyncio + @_helpers.parametrize_valid_id_formats_for_models("guild", 123123123, guilds.Guild) + @_helpers.parametrize_valid_id_formats_for_models("user", 4444444, users.User) + async def test_ban_member_without_optionals(self, rest_guild_logic_impl, guild, user): + rest_guild_logic_impl._session.create_guild_ban.return_value = ... + assert await rest_guild_logic_impl.ban_member(guild, user) is None + rest_guild_logic_impl._session.create_guild_ban.assert_called_once_with( + guild_id="123123123", user_id="4444444", delete_message_days=..., reason=... + ) + + @pytest.mark.asyncio + @_helpers.parametrize_valid_id_formats_for_models("guild", 123123123, guilds.Guild) + @_helpers.parametrize_valid_id_formats_for_models("user", 4444444, users.User) + async def test_unban_member_with_reason(self, rest_guild_logic_impl, guild, user): + rest_guild_logic_impl._session.remove_guild_ban.return_value = ... + result = await rest_guild_logic_impl.unban_member(guild, user, reason="bye") + assert result is None + rest_guild_logic_impl._session.remove_guild_ban.assert_called_once_with( + guild_id="123123123", user_id="4444444", reason="bye" + ) + + @pytest.mark.asyncio + @_helpers.parametrize_valid_id_formats_for_models("guild", 123123123, guilds.Guild) + @_helpers.parametrize_valid_id_formats_for_models("user", 4444444, users.User) + async def test_unban_member_without_reason(self, rest_guild_logic_impl, guild, user): + rest_guild_logic_impl._session.remove_guild_ban.return_value = ... + assert await rest_guild_logic_impl.unban_member(guild, user) is None + rest_guild_logic_impl._session.remove_guild_ban.assert_called_once_with( + guild_id="123123123", user_id="4444444", reason=... + ) + + @pytest.mark.asyncio + @_helpers.parametrize_valid_id_formats_for_models("guild", 574921006817476608, guilds.Guild) + async def test_fetch_roles(self, rest_guild_logic_impl, guild): + mock_role_payload = {"id": "33030", "permissions": 333, "name": "ROlE"} + mock_role_obj = mock.MagicMock(guilds.GuildRole, id=33030) + rest_guild_logic_impl._session.get_guild_roles.return_value = [mock_role_payload] + with mock.patch.object(guilds.GuildRole, "deserialize", return_value=mock_role_obj): + assert await rest_guild_logic_impl.fetch_roles(guild) == {33030: mock_role_obj} + rest_guild_logic_impl._session.get_guild_roles.assert_called_once_with(guild_id="574921006817476608") + guilds.GuildRole.deserialize.assert_called_once_with(mock_role_payload) + + @pytest.mark.asyncio + @_helpers.parametrize_valid_id_formats_for_models("guild", 574921006817476608, guilds.Guild) + async def test_create_role_with_optionals(self, rest_guild_logic_impl, guild): + mock_role_payload = {"id": "033030", "permissions": 333, "name": "ROlE"} + mock_role_obj = mock.MagicMock(guilds.GuildRole) + rest_guild_logic_impl._session.create_guild_role.return_value = mock_role_payload + with mock.patch.object(guilds.GuildRole, "deserialize", return_value=mock_role_obj): + result = await rest_guild_logic_impl.create_role( + guild, + name="Roleington", + permissions=permissions.Permission.STREAM | permissions.Permission.EMBED_LINKS, + color=colors.Color(21312), + hoist=True, + mentionable=False, + reason="And then there was a role.", + ) + assert result is mock_role_obj + rest_guild_logic_impl._session.create_guild_role.assert_called_once_with( + guild_id="574921006817476608", + name="Roleington", + permissions=16896, + color=21312, + hoist=True, + mentionable=False, + reason="And then there was a role.", + ) + guilds.GuildRole.deserialize.assert_called_once_with(mock_role_payload) + + @pytest.mark.asyncio + @_helpers.parametrize_valid_id_formats_for_models("guild", 574921006817476608, guilds.Guild) + async def test_create_role_without_optionals(self, rest_guild_logic_impl, guild): + mock_role_payload = {"id": "033030", "permissions": 333, "name": "ROlE"} + mock_role_obj = mock.MagicMock(guilds.GuildRole) + rest_guild_logic_impl._session.create_guild_role.return_value = mock_role_payload + with mock.patch.object(guilds.GuildRole, "deserialize", return_value=mock_role_obj): + result = await rest_guild_logic_impl.create_role(guild) + assert result is mock_role_obj + rest_guild_logic_impl._session.create_guild_role.assert_called_once_with( + guild_id="574921006817476608", + name=..., + permissions=..., + color=..., + hoist=..., + mentionable=..., + reason=..., + ) + guilds.GuildRole.deserialize.assert_called_once_with(mock_role_payload) + + @pytest.mark.asyncio + @_helpers.parametrize_valid_id_formats_for_models("guild", 574921006817476608, guilds.Guild) + @_helpers.parametrize_valid_id_formats_for_models("role", 123123, guilds.GuildRole) + @_helpers.parametrize_valid_id_formats_for_models("additional_role", 123456, guilds.GuildRole) + async def test_reposition_roles(self, rest_guild_logic_impl, guild, role, additional_role): + mock_role_payload = {"id": "033030", "permissions": 333, "name": "ROlE"} + mock_role_obj = mock.MagicMock(guilds.GuildRole) + rest_guild_logic_impl._session.modify_guild_role_positions.return_value = [mock_role_payload] + with mock.patch.object(guilds.GuildRole, "deserialize", return_value=mock_role_obj): + result = await rest_guild_logic_impl.reposition_roles(guild, (1, role), (2, additional_role)) + assert result == [mock_role_obj] + rest_guild_logic_impl._session.modify_guild_role_positions.assert_called_once_with( + "574921006817476608", ("123123", 1), ("123456", 2) + ) + guilds.GuildRole.deserialize.assert_called_once_with(mock_role_payload) + + @pytest.mark.asyncio + @_helpers.parametrize_valid_id_formats_for_models("guild", 574921006817476608, guilds.Guild) + @_helpers.parametrize_valid_id_formats_for_models("role", 123123, guilds.GuildRole) + async def test_update_role_with_optionals(self, rest_guild_logic_impl, guild, role): + mock_role_payload = {"id": "033030", "permissions": 333, "name": "ROlE"} + mock_role_obj = mock.MagicMock(guilds.GuildRole) + rest_guild_logic_impl._session.modify_guild_role.return_value = mock_role_payload + with mock.patch.object(guilds.GuildRole, "deserialize", return_value=mock_role_obj): + result = await rest_guild_logic_impl.update_role( + guild, + role, + name="ROLE", + permissions=permissions.Permission.STREAM | permissions.Permission.EMBED_LINKS, + color=colors.Color(12312), + hoist=True, + mentionable=False, + reason="Why not?", + ) + assert result is mock_role_obj + rest_guild_logic_impl._session.modify_guild_role.assert_called_once_with( + guild_id="574921006817476608", + role_id="123123", + name="ROLE", + permissions=16896, + color=12312, + hoist=True, + mentionable=False, + reason="Why not?", + ) + guilds.GuildRole.deserialize.assert_called_once_with(mock_role_payload) + + @pytest.mark.asyncio + @_helpers.parametrize_valid_id_formats_for_models("guild", 574921006817476608, guilds.Guild) + @_helpers.parametrize_valid_id_formats_for_models("role", 123123, guilds.GuildRole) + async def test_update_role_without_optionals(self, rest_guild_logic_impl, guild, role): + mock_role_payload = {"id": "033030", "permissions": 333, "name": "ROlE"} + mock_role_obj = mock.MagicMock(guilds.GuildRole) + rest_guild_logic_impl._session.modify_guild_role.return_value = mock_role_payload + with mock.patch.object(guilds.GuildRole, "deserialize", return_value=mock_role_obj): + assert await rest_guild_logic_impl.update_role(guild, role) is mock_role_obj + rest_guild_logic_impl._session.modify_guild_role.assert_called_once_with( + guild_id="574921006817476608", + role_id="123123", + name=..., + permissions=..., + color=..., + hoist=..., + mentionable=..., + reason=..., + ) + guilds.GuildRole.deserialize.assert_called_once_with(mock_role_payload) + + @pytest.mark.asyncio + @_helpers.parametrize_valid_id_formats_for_models("guild", 574921006817476608, guilds.Guild) + @_helpers.parametrize_valid_id_formats_for_models("role", 123123, guilds.GuildRole) + async def test_delete_role(self, rest_guild_logic_impl, guild, role): + rest_guild_logic_impl._session.delete_guild_role.return_value = ... + assert await rest_guild_logic_impl.delete_role(guild, role) is None + rest_guild_logic_impl._session.delete_guild_role.assert_called_once_with( + guild_id="574921006817476608", role_id="123123" + ) + + @pytest.mark.asyncio + @_helpers.parametrize_valid_id_formats_for_models("guild", 574921006817476608, guilds.Guild) + @pytest.mark.parametrize("days", [7, datetime.timedelta(days=7)]) + async def test_estimate_guild_prune_count(self, rest_guild_logic_impl, guild, days): + rest_guild_logic_impl._session.get_guild_prune_count.return_value = 42 + assert await rest_guild_logic_impl.estimate_guild_prune_count(guild, days) == 42 + rest_guild_logic_impl._session.get_guild_prune_count.assert_called_once_with( + guild_id="574921006817476608", days=7 + ) + + @pytest.mark.asyncio + @_helpers.parametrize_valid_id_formats_for_models("guild", 574921006817476608, guilds.Guild) + @pytest.mark.parametrize("days", [7, datetime.timedelta(days=7)]) + async def test_estimate_guild_with_optionals(self, rest_guild_logic_impl, guild, days): + rest_guild_logic_impl._session.begin_guild_prune.return_value = None + assert ( + await rest_guild_logic_impl.begin_guild_prune(guild, days, compute_prune_count=True, reason="nah m8") + is None + ) + rest_guild_logic_impl._session.begin_guild_prune.assert_called_once_with( + guild_id="574921006817476608", days=7, compute_prune_count=True, reason="nah m8" + ) + + @pytest.mark.asyncio + @_helpers.parametrize_valid_id_formats_for_models("guild", 574921006817476608, guilds.Guild) + @pytest.mark.parametrize("days", [7, datetime.timedelta(days=7)]) + async def test_estimate_guild_without_optionals(self, rest_guild_logic_impl, guild, days): + rest_guild_logic_impl._session.begin_guild_prune.return_value = 42 + assert await rest_guild_logic_impl.begin_guild_prune(guild, days) == 42 + rest_guild_logic_impl._session.begin_guild_prune.assert_called_once_with( + guild_id="574921006817476608", days=7, compute_prune_count=..., reason=... + ) + + @pytest.mark.asyncio + @_helpers.parametrize_valid_id_formats_for_models("guild", 574921006817476608, guilds.Guild) + async def test_fetch_guild_voice_regions(self, rest_guild_logic_impl, guild): + mock_voice_payload = {"name": "london", "id": "LONDON"} + mock_voice_obj = mock.MagicMock(voices.VoiceRegion) + rest_guild_logic_impl._session.get_guild_voice_regions.return_value = [mock_voice_payload] + with mock.patch.object(voices.VoiceRegion, "deserialize", return_value=mock_voice_obj): + assert await rest_guild_logic_impl.fetch_guild_voice_regions(guild) == [mock_voice_obj] + rest_guild_logic_impl._session.get_guild_voice_regions.assert_called_once_with( + guild_id="574921006817476608" + ) + voices.VoiceRegion.deserialize.assert_called_once_with(mock_voice_payload) + + @pytest.mark.asyncio + @_helpers.parametrize_valid_id_formats_for_models("guild", 574921006817476608, guilds.Guild) + async def test_fetch_guild_invites(self, rest_guild_logic_impl, guild): + mock_invite_payload = {"code": "dododo"} + mock_invite_obj = mock.MagicMock(invites.InviteWithMetadata) + rest_guild_logic_impl._session.get_guild_invites.return_value = [mock_invite_payload] + with mock.patch.object(invites.InviteWithMetadata, "deserialize", return_value=mock_invite_obj): + assert await rest_guild_logic_impl.fetch_guild_invites(guild) == [mock_invite_obj] + invites.InviteWithMetadata.deserialize.assert_called_once_with(mock_invite_payload) + + @pytest.mark.asyncio + @_helpers.parametrize_valid_id_formats_for_models("guild", 574921006817476608, guilds.Guild) + async def test_fetch_integrations(self, rest_guild_logic_impl, guild): + mock_integration_payload = {"id": "123123", "name": "Integrated", "type": "twitch"} + mock_integration_obj = mock.MagicMock(guilds.GuildIntegration) + rest_guild_logic_impl._session.get_guild_integrations.return_value = [mock_integration_payload] + with mock.patch.object(guilds.GuildIntegration, "deserialize", return_value=mock_integration_obj): + assert await rest_guild_logic_impl.fetch_integrations(guild) == [mock_integration_obj] + rest_guild_logic_impl._session.get_guild_integrations.assert_called_once_with(guild_id="574921006817476608") + guilds.GuildIntegration.deserialize.assert_called_once_with(mock_integration_payload) + + @pytest.mark.asyncio + @_helpers.parametrize_valid_id_formats_for_models("guild", 574921006817476608, guilds.Guild) + @_helpers.parametrize_valid_id_formats_for_models("integration", 379953393319542784, guilds.GuildIntegration) + @pytest.mark.parametrize("period", [datetime.timedelta(days=7), 7]) + async def test_update_integration_with_optionals(self, rest_guild_logic_impl, guild, integration, period): + rest_guild_logic_impl._session.modify_guild_integration.return_value = ... + result = await rest_guild_logic_impl.update_integration( + guild, + integration, + expire_behaviour=guilds.IntegrationExpireBehaviour.KICK, + expire_grace_period=period, + enable_emojis=True, + reason="GET YEET'D", + ) + assert result is None + rest_guild_logic_impl._session.modify_guild_integration.assert_called_once_with( + guild_id="574921006817476608", + integration_id="379953393319542784", + expire_behaviour=1, + expire_grace_period=7, + enable_emojis=True, + reason="GET YEET'D", + ) + + @pytest.mark.asyncio + @_helpers.parametrize_valid_id_formats_for_models("guild", 574921006817476608, guilds.Guild) + @_helpers.parametrize_valid_id_formats_for_models("integration", 379953393319542784, guilds.GuildIntegration) + async def test_update_integration_without_optionals(self, rest_guild_logic_impl, guild, integration): + rest_guild_logic_impl._session.modify_guild_integration.return_value = ... + assert await rest_guild_logic_impl.update_integration(guild, integration) is None + rest_guild_logic_impl._session.modify_guild_integration.assert_called_once_with( + guild_id="574921006817476608", + integration_id="379953393319542784", + expire_behaviour=..., + expire_grace_period=..., + enable_emojis=..., + reason=..., + ) + + @pytest.mark.asyncio + @_helpers.parametrize_valid_id_formats_for_models("guild", 574921006817476608, guilds.Guild) + @_helpers.parametrize_valid_id_formats_for_models("integration", 379953393319542784, guilds.GuildIntegration) + async def test_delete_integration_with_reason(self, rest_guild_logic_impl, guild, integration): + rest_guild_logic_impl._session.delete_guild_integration.return_value = ... + assert await rest_guild_logic_impl.delete_integration(guild, integration, reason="B Y E") is None + rest_guild_logic_impl._session.delete_guild_integration.assert_called_once_with( + guild_id="574921006817476608", integration_id="379953393319542784", reason="B Y E" + ) + + @pytest.mark.asyncio + @_helpers.parametrize_valid_id_formats_for_models("guild", 574921006817476608, guilds.Guild) + @_helpers.parametrize_valid_id_formats_for_models("integration", 379953393319542784, guilds.GuildIntegration) + async def test_delete_integration_without_reason(self, rest_guild_logic_impl, guild, integration): + rest_guild_logic_impl._session.delete_guild_integration.return_value = ... + assert await rest_guild_logic_impl.delete_integration(guild, integration) is None + rest_guild_logic_impl._session.delete_guild_integration.assert_called_once_with( + guild_id="574921006817476608", integration_id="379953393319542784", reason=... + ) + + @pytest.mark.asyncio + @_helpers.parametrize_valid_id_formats_for_models("guild", 574921006817476608, guilds.Guild) + @_helpers.parametrize_valid_id_formats_for_models("integration", 379953393319542784, guilds.GuildIntegration) + async def test_sync_guild_integration(self, rest_guild_logic_impl, guild, integration): + rest_guild_logic_impl._session.sync_guild_integration.return_value = ... + assert await rest_guild_logic_impl.sync_guild_integration(guild, integration) is None + rest_guild_logic_impl._session.sync_guild_integration.assert_called_once_with( + guild_id="574921006817476608", integration_id="379953393319542784", + ) + + @pytest.mark.asyncio + @_helpers.parametrize_valid_id_formats_for_models("guild", 574921006817476608, guilds.Guild) + async def test_fetch_guild_embed(self, rest_guild_logic_impl, guild): + mock_embed_payload = {"enabled": True, "channel_id": "2020202"} + mock_embed_obj = mock.MagicMock(guilds.GuildEmbed) + rest_guild_logic_impl._session.get_guild_embed.return_value = mock_embed_payload + with mock.patch.object(guilds.GuildEmbed, "deserialize", return_value=mock_embed_obj): + assert await rest_guild_logic_impl.fetch_guild_embed(guild) is mock_embed_obj + rest_guild_logic_impl._session.get_guild_embed.assert_called_once_with(guild_id="574921006817476608") + + @pytest.mark.asyncio + @_helpers.parametrize_valid_id_formats_for_models("guild", 574921006817476608, guilds.Guild) + @_helpers.parametrize_valid_id_formats_for_models("channel", 123123, channels.GuildChannel) + async def test_update_guild_embed_with_optionnal(self, rest_guild_logic_impl, guild, channel): + mock_embed_payload = {"enabled": True, "channel_id": "2020202"} + mock_embed_obj = mock.MagicMock(guilds.GuildEmbed) + rest_guild_logic_impl._session.modify_guild_embed.return_value = mock_embed_payload + with mock.patch.object(guilds.GuildEmbed, "deserialize", return_value=mock_embed_obj): + result = await rest_guild_logic_impl.update_guild_embed( + guild, channel=channel, enabled=True, reason="Nyaa!!!" + ) + assert result is mock_embed_obj + rest_guild_logic_impl._session.modify_guild_embed.assert_called_once_with( + guild_id="574921006817476608", channel_id="123123", enabled=True, reason="Nyaa!!!" + ) + + @pytest.mark.asyncio + @_helpers.parametrize_valid_id_formats_for_models("guild", 574921006817476608, guilds.Guild) + async def test_update_guild_embed_without_optionnal(self, rest_guild_logic_impl, guild): + mock_embed_payload = {"enabled": True, "channel_id": "2020202"} + mock_embed_obj = mock.MagicMock(guilds.GuildEmbed) + rest_guild_logic_impl._session.modify_guild_embed.return_value = mock_embed_payload + with mock.patch.object(guilds.GuildEmbed, "deserialize", return_value=mock_embed_obj): + assert await rest_guild_logic_impl.update_guild_embed(guild) is mock_embed_obj + rest_guild_logic_impl._session.modify_guild_embed.assert_called_once_with( + guild_id="574921006817476608", channel_id=..., enabled=..., reason=... + ) + + @pytest.mark.asyncio + @_helpers.parametrize_valid_id_formats_for_models("guild", 574921006817476608, guilds.Guild) + async def test_fetch_guild_vanity_url(self, rest_guild_logic_impl, guild): + mock_vanity_payload = {"code": "akfdk", "uses": 5} + mock_vanity_obj = mock.MagicMock(invites.VanityUrl) + rest_guild_logic_impl._session.get_guild_vanity_url.return_value = mock_vanity_payload + with mock.patch.object(invites.VanityUrl, "deserialize", return_value=mock_vanity_obj): + assert await rest_guild_logic_impl.fetch_guild_vanity_url(guild) is mock_vanity_obj + rest_guild_logic_impl._session.get_guild_vanity_url.assert_called_once_with(guild_id="574921006817476608") + invites.VanityUrl.deserialize.assert_called_once_with(mock_vanity_payload) + + @_helpers.parametrize_valid_id_formats_for_models("guild", 574921006817476608, guilds.Guild) + def test_fetch_guild_widget_image_with_style(self, rest_guild_logic_impl, guild): + mock_url = "not/a/url" + rest_guild_logic_impl._session.get_guild_widget_image_url.return_value = mock_url + assert rest_guild_logic_impl.format_guild_widget_image(guild, style="notAStyle") == mock_url + rest_guild_logic_impl._session.get_guild_widget_image_url.assert_called_once_with( + guild_id="574921006817476608", style="notAStyle", + ) + + @_helpers.parametrize_valid_id_formats_for_models("guild", 574921006817476608, guilds.Guild) + def test_fetch_guild_widget_image_without_style(self, rest_guild_logic_impl, guild): + mock_url = "not/a/url" + rest_guild_logic_impl._session.get_guild_widget_image_url.return_value = mock_url + assert rest_guild_logic_impl.format_guild_widget_image(guild) == mock_url + rest_guild_logic_impl._session.get_guild_widget_image_url.assert_called_once_with( + guild_id="574921006817476608", style=..., + ) + + @pytest.mark.asyncio + @_helpers.parametrize_valid_id_formats_for_models("channel", 115590097100865541, channels.GuildChannel) + async def test_fetch_guild_webhooks(self, rest_guild_logic_impl, channel): + mock_webhook_payload = {"id": "29292929", "channel_id": "2292992"} + mock_webhook_obj = mock.MagicMock(webhooks.Webhook) + rest_guild_logic_impl._session.get_guild_webhooks.return_value = [mock_webhook_payload] + with mock.patch.object(webhooks.Webhook, "deserialize", return_value=mock_webhook_obj): + assert await rest_guild_logic_impl.fetch_guild_webhooks(channel) == [mock_webhook_obj] + rest_guild_logic_impl._session.get_guild_webhooks.assert_called_once_with(guild_id="115590097100865541") + webhooks.Webhook.deserialize.assert_called_once_with(mock_webhook_payload) diff --git a/tests/hikari/clients/test_rest_clients/test_invites_component.py b/tests/hikari/clients/test_rest_clients/test_invites_component.py new file mode 100644 index 0000000000..8e3d8eccba --- /dev/null +++ b/tests/hikari/clients/test_rest_clients/test_invites_component.py @@ -0,0 +1,69 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# Copyright © Nekoka.tt 2019-2020 +# +# This file is part of Hikari. +# +# Hikari is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Hikari is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Hikari. If not, see . +import mock +import pytest + +from hikari import invites +from hikari.clients.rest_clients import invites_component +from hikari.net import rest + + +class TestRESTInviteLogic: + @pytest.fixture() + def rest_invite_logic_impl(self): + mock_low_level_restful_client = mock.MagicMock(rest.LowLevelRestfulClient) + + class RESTInviteLogicImpl(invites_component.RESTInviteComponent): + def __init__(self): + super().__init__(mock_low_level_restful_client) + + return RESTInviteLogicImpl() + + @pytest.mark.asyncio + @pytest.mark.parametrize("invite", [mock.MagicMock(invites.Invite, code="AAAAAAAAAAAAAAAA"), "AAAAAAAAAAAAAAAA"]) + async def test_fetch_invite_with_counts(self, rest_invite_logic_impl, invite): + mock_invite_payload = {"code": "AAAAAAAAAAAAAAAA", "guild": {}, "channel": {}} + mock_invite_obj = mock.MagicMock(invites.Invite) + rest_invite_logic_impl._session.get_invite.return_value = mock_invite_payload + with mock.patch.object(invites.Invite, "deserialize", return_value=mock_invite_obj): + assert await rest_invite_logic_impl.fetch_invite(invite, with_counts=True) is mock_invite_obj + rest_invite_logic_impl._session.get_invite.assert_called_once_with( + invite_code="AAAAAAAAAAAAAAAA", with_counts=True, + ) + invites.Invite.deserialize.assert_called_once_with(mock_invite_payload) + + @pytest.mark.asyncio + @pytest.mark.parametrize("invite", [mock.MagicMock(invites.Invite, code="AAAAAAAAAAAAAAAA"), "AAAAAAAAAAAAAAAA"]) + async def test_fetch_invite_without_counts(self, rest_invite_logic_impl, invite): + mock_invite_payload = {"code": "AAAAAAAAAAAAAAAA", "guild": {}, "channel": {}} + mock_invite_obj = mock.MagicMock(invites.Invite) + rest_invite_logic_impl._session.get_invite.return_value = mock_invite_payload + with mock.patch.object(invites.Invite, "deserialize", return_value=mock_invite_obj): + assert await rest_invite_logic_impl.fetch_invite(invite) is mock_invite_obj + rest_invite_logic_impl._session.get_invite.assert_called_once_with( + invite_code="AAAAAAAAAAAAAAAA", with_counts=..., + ) + invites.Invite.deserialize.assert_called_once_with(mock_invite_payload) + + @pytest.mark.asyncio + @pytest.mark.parametrize("invite", [mock.MagicMock(invites.Invite, code="AAAAAAAAAAAAAAAA"), "AAAAAAAAAAAAAAAA"]) + async def test_delete_invite(self, rest_invite_logic_impl, invite): + rest_invite_logic_impl._session.delete_invite.return_value = ... + assert await rest_invite_logic_impl.delete_invite(invite) is None + rest_invite_logic_impl._session.delete_invite.assert_called_once_with(invite_code="AAAAAAAAAAAAAAAA") diff --git a/tests/hikari/clients/test_rest_clients/test_oauth2_component.py b/tests/hikari/clients/test_rest_clients/test_oauth2_component.py new file mode 100644 index 0000000000..80daf14583 --- /dev/null +++ b/tests/hikari/clients/test_rest_clients/test_oauth2_component.py @@ -0,0 +1,48 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# Copyright © Nekoka.tt 2019-2020 +# +# This file is part of Hikari. +# +# Hikari is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Hikari is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Hikari. If not, see . +import datetime + +import mock +import pytest + +from hikari import oauth2 +from hikari.clients.rest_clients import oauth2_component +from hikari.net import rest + + +class TestRESTReactionLogic: + @pytest.fixture() + def rest_oauth2_logic_impl(self): + mock_low_level_restful_client = mock.MagicMock(rest.LowLevelRestfulClient) + + class RESTOauth2LogicImpl(oauth2_component.RESTOauth2Component): + def __init__(self): + super().__init__(mock_low_level_restful_client) + + return RESTOauth2LogicImpl() + + @pytest.mark.asyncio + async def test_fetch_my_application_info(self, rest_oauth2_logic_impl): + mock_application_payload = {"id": "2929292", "name": "blah blah", "description": "an app"} + mock_application_obj = mock.MagicMock(oauth2.Application) + rest_oauth2_logic_impl._session.get_current_application_info.return_value = mock_application_payload + with mock.patch.object(oauth2.Application, "deserialize", return_value=mock_application_obj): + assert await rest_oauth2_logic_impl.fetch_my_application_info() is mock_application_obj + rest_oauth2_logic_impl._session.get_current_application_info.assert_called_once_with() + oauth2.Application.deserialize.assert_called_once_with(mock_application_payload) diff --git a/tests/hikari/clients/test_rest_clients/test_reactions_component.py b/tests/hikari/clients/test_rest_clients/test_reactions_component.py new file mode 100644 index 0000000000..73b9de0549 --- /dev/null +++ b/tests/hikari/clients/test_rest_clients/test_reactions_component.py @@ -0,0 +1,151 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# Copyright © Nekoka.tt 2019-2020 +# +# This file is part of Hikari. +# +# Hikari is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Hikari is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Hikari. If not, see . +import datetime + +import mock +import pytest + +from hikari import channels +from hikari import emojis +from hikari import messages +from hikari import users +from hikari.clients.rest_clients import reactions_component +from hikari.internal import pagination +from hikari.net import rest +from tests.hikari import _helpers + + +class TestRESTReactionLogic: + @pytest.fixture() + def rest_reaction_logic_impl(self): + mock_low_level_restful_client = mock.MagicMock(rest.LowLevelRestfulClient) + + class RESTReactionLogicImpl(reactions_component.RESTReactionComponent): + def __init__(self): + super().__init__(mock_low_level_restful_client) + + return RESTReactionLogicImpl() + + @pytest.mark.asyncio + @_helpers.parametrize_valid_id_formats_for_models("channel", 213123, channels.Channel) + @_helpers.parametrize_valid_id_formats_for_models("message", 987654321, messages.Message) + @pytest.mark.parametrize("emoji", ["blah:123", emojis.UnknownEmoji(name="blah", id=123, is_animated=False)]) + async def test_create_reaction(self, rest_reaction_logic_impl, channel, message, emoji): + rest_reaction_logic_impl._session.create_reaction.return_value = ... + assert await rest_reaction_logic_impl.create_reaction(channel=channel, message=message, emoji=emoji) is None + rest_reaction_logic_impl._session.create_reaction.assert_called_once_with( + channel_id="213123", message_id="987654321", emoji="blah:123", + ) + + @pytest.mark.asyncio + @_helpers.parametrize_valid_id_formats_for_models("channel", 213123, channels.Channel) + @_helpers.parametrize_valid_id_formats_for_models("message", 987654321, messages.Message) + @pytest.mark.parametrize("emoji", ["blah:123", emojis.UnknownEmoji(name="blah", id=123, is_animated=False)]) + async def test_delete_reaction(self, rest_reaction_logic_impl, channel, message, emoji): + rest_reaction_logic_impl._session.delete_own_reaction.return_value = ... + assert await rest_reaction_logic_impl.delete_reaction(channel=channel, message=message, emoji=emoji) is None + rest_reaction_logic_impl._session.delete_own_reaction.assert_called_once_with( + channel_id="213123", message_id="987654321", emoji="blah:123", + ) + + @pytest.mark.asyncio + @_helpers.parametrize_valid_id_formats_for_models("channel", 213123, channels.Channel) + @_helpers.parametrize_valid_id_formats_for_models("message", 987654321, messages.Message) + async def test_delete_all_reactions(self, rest_reaction_logic_impl, channel, message): + rest_reaction_logic_impl._session.delete_all_reactions.return_value = ... + assert await rest_reaction_logic_impl.delete_all_reactions(channel=channel, message=message) is None + rest_reaction_logic_impl._session.delete_all_reactions.assert_called_once_with( + channel_id="213123", message_id="987654321", + ) + + @pytest.mark.asyncio + @_helpers.parametrize_valid_id_formats_for_models("channel", 213123, channels.Channel) + @_helpers.parametrize_valid_id_formats_for_models("message", 987654321, messages.Message) + @pytest.mark.parametrize("emoji", ["blah:123", emojis.UnknownEmoji(name="blah", id=123, is_animated=False)]) + async def test_delete_all_reactions_for_emoji(self, rest_reaction_logic_impl, channel, message, emoji): + rest_reaction_logic_impl._session.delete_all_reactions_for_emoji.return_value = ... + assert ( + await rest_reaction_logic_impl.delete_all_reactions_for_emoji(channel=channel, message=message, emoji=emoji) + is None + ) + rest_reaction_logic_impl._session.delete_all_reactions_for_emoji.assert_called_once_with( + channel_id="213123", message_id="987654321", emoji="blah:123", + ) + + @_helpers.parametrize_valid_id_formats_for_models("message", 432, messages.Message) + @_helpers.parametrize_valid_id_formats_for_models("channel", 123, channels.Channel) + @pytest.mark.parametrize( + "emoji", ["tutu1:456371206225002499", mock.MagicMock(emojis.GuildEmoji, url_name="tutu1:456371206225002499")] + ) + @_helpers.parametrize_valid_id_formats_for_models("user", 140502780547694592, users.User) + def test_fetch_reactors_after_with_optionals(self, rest_reaction_logic_impl, message, channel, emoji, user): + mock_generator = mock.AsyncMock() + with mock.patch.object(pagination, "pagination_handler", return_value=mock_generator): + result = rest_reaction_logic_impl.fetch_reactors_after(channel, message, emoji, after=user, limit=47) + assert result is mock_generator + pagination.pagination_handler.assert_called_once_with( + channel_id="123", + message_id="432", + emoji="tutu1:456371206225002499", + deserializer=users.User.deserialize, + direction="after", + request=rest_reaction_logic_impl._session.get_reactions, + reversing=False, + start="140502780547694592", + limit=47, + ) + + @_helpers.parametrize_valid_id_formats_for_models("message", 432, messages.Message) + @_helpers.parametrize_valid_id_formats_for_models("channel", 123, channels.Channel) + @pytest.mark.parametrize( + "emoji", ["tutu1:456371206225002499", mock.MagicMock(emojis.GuildEmoji, url_name="tutu1:456371206225002499")] + ) + def test_fetch_reactors_after_without_optionals(self, rest_reaction_logic_impl, message, channel, emoji): + mock_generator = mock.AsyncMock() + with mock.patch.object(pagination, "pagination_handler", return_value=mock_generator): + assert rest_reaction_logic_impl.fetch_reactors_after(channel, message, emoji) is mock_generator + pagination.pagination_handler.assert_called_once_with( + channel_id="123", + message_id="432", + emoji="tutu1:456371206225002499", + deserializer=users.User.deserialize, + direction="after", + request=rest_reaction_logic_impl._session.get_reactions, + reversing=False, + start="0", + limit=None, + ) + + def test_fetch_reactors_after_with_datetime_object(self, rest_reaction_logic_impl): + mock_generator = mock.AsyncMock() + date = datetime.datetime(2019, 1, 22, 18, 41, 15, 283_000, tzinfo=datetime.timezone.utc) + with mock.patch.object(pagination, "pagination_handler", return_value=mock_generator): + result = rest_reaction_logic_impl.fetch_reactors_after(123, 432, "tutu1:456371206225002499", after=date) + assert result is mock_generator + pagination.pagination_handler.assert_called_once_with( + channel_id="123", + message_id="432", + emoji="tutu1:456371206225002499", + deserializer=users.User.deserialize, + direction="after", + request=rest_reaction_logic_impl._session.get_reactions, + reversing=False, + start="537340988620800000", + limit=None, + ) diff --git a/tests/hikari/clients/test_rest_clients/test_users_component.py b/tests/hikari/clients/test_rest_clients/test_users_component.py new file mode 100644 index 0000000000..3c6f8ee4cf --- /dev/null +++ b/tests/hikari/clients/test_rest_clients/test_users_component.py @@ -0,0 +1,48 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# Copyright © Nekoka.tt 2019-2020 +# +# This file is part of Hikari. +# +# Hikari is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Hikari is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Hikari. If not, see . +import mock +import pytest + +from hikari import users +from hikari.clients.rest_clients import users_component +from hikari.net import rest +from tests.hikari import _helpers + + +class TestRESTUserLogic: + @pytest.fixture() + def rest_user_logic_impl(self): + mock_low_level_restful_client = mock.MagicMock(rest.LowLevelRestfulClient) + + class RESTUserLogicImpl(users_component.RESTUserComponent): + def __init__(self): + super().__init__(mock_low_level_restful_client) + + return RESTUserLogicImpl() + + @pytest.mark.asyncio + @_helpers.parametrize_valid_id_formats_for_models("user", 123123123, users.User) + async def test_fetch_user(self, rest_user_logic_impl, user): + mock_user_payload = {"id": "123", "username": "userName"} + mock_user_obj = mock.MagicMock(users.User) + rest_user_logic_impl._session.get_user.return_value = mock_user_payload + with mock.patch.object(users.User, "deserialize", return_value=mock_user_obj): + assert await rest_user_logic_impl.fetch_user(user) is mock_user_obj + rest_user_logic_impl._session.get_user.assert_called_once_with(user_id="123123123") + users.User.deserialize.assert_called_once_with(mock_user_payload) diff --git a/tests/hikari/clients/test_rest_clients/test_voices_component.py b/tests/hikari/clients/test_rest_clients/test_voices_component.py new file mode 100644 index 0000000000..a9df770d2c --- /dev/null +++ b/tests/hikari/clients/test_rest_clients/test_voices_component.py @@ -0,0 +1,46 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# Copyright © Nekoka.tt 2019-2020 +# +# This file is part of Hikari. +# +# Hikari is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Hikari is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Hikari. If not, see . +import mock +import pytest + +from hikari import voices +from hikari.clients.rest_clients import voices_component +from hikari.net import rest + + +class TestRESTUserLogic: + @pytest.fixture() + def rest_voice_logic_impl(self): + mock_low_level_restful_client = mock.MagicMock(rest.LowLevelRestfulClient) + + class RESTVoiceLogicImpl(voices_component.RESTVoiceComponent): + def __init__(self): + super().__init__(mock_low_level_restful_client) + + return RESTVoiceLogicImpl() + + @pytest.mark.asyncio + async def test_fetch_voice_regions(self, rest_voice_logic_impl): + mock_voice_payload = {"id": "LONDON", "name": "london"} + mock_voice_obj = mock.MagicMock(voices.VoiceRegion) + rest_voice_logic_impl._session.list_voice_regions.return_value = [mock_voice_payload] + with mock.patch.object(voices.VoiceRegion, "deserialize", return_value=mock_voice_obj): + assert await rest_voice_logic_impl.fetch_voice_regions() == [mock_voice_obj] + rest_voice_logic_impl._session.list_voice_regions.assert_called_once() + voices.VoiceRegion.deserialize.assert_called_once_with(mock_voice_payload) diff --git a/tests/hikari/clients/test_rest_clients/test_webhooks_component.py b/tests/hikari/clients/test_rest_clients/test_webhooks_component.py new file mode 100644 index 0000000000..191e074750 --- /dev/null +++ b/tests/hikari/clients/test_rest_clients/test_webhooks_component.py @@ -0,0 +1,289 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# Copyright © Nekoka.tt 2019-2020 +# +# This file is part of Hikari. +# +# Hikari is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Hikari is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Hikari. If not, see . +import contextlib +import io + +import mock +import pytest + +from hikari import embeds +from hikari import media +from hikari import messages +from hikari import webhooks +from hikari.clients.rest_clients import webhooks_component +from hikari.internal import allowed_mentions +from hikari.internal import conversions +from hikari.net import rest +from tests.hikari import _helpers + + +class TestRESTUserLogic: + @pytest.fixture() + def rest_webhook_logic_impl(self): + mock_low_level_restful_client = mock.MagicMock(rest.LowLevelRestfulClient) + + class RESTWebhookLogicImpl(webhooks_component.RESTWebhookComponent): + def __init__(self): + super().__init__(mock_low_level_restful_client) + + return RESTWebhookLogicImpl() + + @pytest.mark.asyncio + @_helpers.parametrize_valid_id_formats_for_models("webhook", 379953393319542784, webhooks.Webhook) + async def test_fetch_webhook_with_webhook_token(self, rest_webhook_logic_impl, webhook): + mock_webhook_payload = {"id": "29292929", "channel_id": "2292992"} + mock_webhook_obj = mock.MagicMock(webhooks.Webhook) + rest_webhook_logic_impl._session.get_webhook.return_value = mock_webhook_payload + with mock.patch.object(webhooks.Webhook, "deserialize", return_value=mock_webhook_obj): + assert ( + await rest_webhook_logic_impl.fetch_webhook(webhook, webhook_token="dsawqoepql.kmsdao") + is mock_webhook_obj + ) + rest_webhook_logic_impl._session.get_webhook.assert_called_once_with( + webhook_id="379953393319542784", webhook_token="dsawqoepql.kmsdao", + ) + webhooks.Webhook.deserialize.assert_called_once_with(mock_webhook_payload) + + @pytest.mark.asyncio + @_helpers.parametrize_valid_id_formats_for_models("webhook", 379953393319542784, webhooks.Webhook) + async def test_fetch_webhook_without_webhook_token(self, rest_webhook_logic_impl, webhook): + mock_webhook_payload = {"id": "29292929", "channel_id": "2292992"} + mock_webhook_obj = mock.MagicMock(webhooks.Webhook) + rest_webhook_logic_impl._session.get_webhook.return_value = mock_webhook_payload + with mock.patch.object(webhooks.Webhook, "deserialize", return_value=mock_webhook_obj): + assert await rest_webhook_logic_impl.fetch_webhook(webhook) is mock_webhook_obj + rest_webhook_logic_impl._session.get_webhook.assert_called_once_with( + webhook_id="379953393319542784", webhook_token=..., + ) + webhooks.Webhook.deserialize.assert_called_once_with(mock_webhook_payload) + + @pytest.mark.asyncio + @_helpers.parametrize_valid_id_formats_for_models("webhook", 379953393319542784, webhooks.Webhook) + @_helpers.parametrize_valid_id_formats_for_models("channel", 115590097100865541, webhooks.Webhook) + async def test_update_webhook_with_optionals(self, rest_webhook_logic_impl, webhook, channel): + mock_webhook_obj = mock.MagicMock(webhooks.Webhook) + mock_webhook_payload = {"id": "123123", "avatar": "1wedoklpasdoiksdoka"} + rest_webhook_logic_impl._session.modify_webhook.return_value = mock_webhook_payload + mock_image_obj = mock.MagicMock(io.BytesIO) + mock_image_data = mock.MagicMock(bytes) + stack = contextlib.ExitStack() + stack.enter_context(mock.patch.object(webhooks.Webhook, "deserialize", return_value=mock_webhook_obj)) + stack.enter_context(mock.patch.object(conversions, "get_bytes_from_resource", return_value=mock_image_data)) + with stack: + result = await rest_webhook_logic_impl.update_webhook( + webhook, + webhook_token="a.wEbHoOk.ToKeN", + name="blah_blah_blah", + avatar_data=mock_image_obj, + channel=channel, + reason="A reason", + ) + assert result is mock_webhook_obj + rest_webhook_logic_impl._session.modify_webhook.assert_called_once_with( + webhook_id="379953393319542784", + webhook_token="a.wEbHoOk.ToKeN", + name="blah_blah_blah", + avatar=mock_image_data, + channel_id="115590097100865541", + reason="A reason", + ) + webhooks.Webhook.deserialize.assert_called_once_with(mock_webhook_payload) + conversions.get_bytes_from_resource.assert_called_once_with(mock_image_obj) + + @pytest.mark.asyncio + @_helpers.parametrize_valid_id_formats_for_models("webhook", 379953393319542784, webhooks.Webhook) + async def test_update_webhook_without_optionals(self, rest_webhook_logic_impl, webhook): + mock_webhook_obj = mock.MagicMock(webhooks.Webhook) + mock_webhook_payload = {"id": "123123", "avatar": "1wedoklpasdoiksdoka"} + rest_webhook_logic_impl._session.modify_webhook.return_value = mock_webhook_payload + with mock.patch.object(webhooks.Webhook, "deserialize", return_value=mock_webhook_obj): + assert await rest_webhook_logic_impl.update_webhook(webhook) is mock_webhook_obj + rest_webhook_logic_impl._session.modify_webhook.assert_called_once_with( + webhook_id="379953393319542784", webhook_token=..., name=..., avatar=..., channel_id=..., reason=..., + ) + webhooks.Webhook.deserialize.assert_called_once_with(mock_webhook_payload) + + @pytest.mark.asyncio + @_helpers.parametrize_valid_id_formats_for_models("webhook", 379953393319542784, webhooks.Webhook) + async def test_delete_webhook_with_webhook_token(self, rest_webhook_logic_impl, webhook): + rest_webhook_logic_impl._session.delete_webhook.return_value = ... + assert await rest_webhook_logic_impl.delete_webhook(webhook, webhook_token="dsawqoepql.kmsdao") is None + rest_webhook_logic_impl._session.delete_webhook.assert_called_once_with( + webhook_id="379953393319542784", webhook_token="dsawqoepql.kmsdao" + ) + + @pytest.mark.asyncio + @_helpers.parametrize_valid_id_formats_for_models("webhook", 379953393319542784, webhooks.Webhook) + async def test_delete_webhook_without_webhook_token(self, rest_webhook_logic_impl, webhook): + rest_webhook_logic_impl._session.delete_webhook.return_value = ... + assert await rest_webhook_logic_impl.delete_webhook(webhook) is None + rest_webhook_logic_impl._session.delete_webhook.assert_called_once_with( + webhook_id="379953393319542784", webhook_token=... + ) + + @pytest.mark.asyncio + @_helpers.parametrize_valid_id_formats_for_models("webhook", 379953393319542784, webhooks.Webhook) + async def test_execute_webhook_without_optionals(self, rest_webhook_logic_impl, webhook): + rest_webhook_logic_impl._session.execute_webhook.return_value = ... + mock_allowed_mentions_payload = {"parse": ["everyone", "users", "roles"]} + with mock.patch.object( + allowed_mentions, "generate_allowed_mentions", return_value=mock_allowed_mentions_payload + ): + assert await rest_webhook_logic_impl.execute_webhook(webhook, "a.webhook.token") is None + allowed_mentions.generate_allowed_mentions.assert_called_once_with( + mentions_everyone=True, user_mentions=True, role_mentions=True + ) + rest_webhook_logic_impl._session.execute_webhook.assert_called_once_with( + webhook_id="379953393319542784", + webhook_token="a.webhook.token", + content=..., + username=..., + avatar_url=..., + tts=..., + wait=False, + file=..., + embeds=..., + allowed_mentions=mock_allowed_mentions_payload, + ) + + @pytest.mark.asyncio + @_helpers.parametrize_valid_id_formats_for_models("webhook", 379953393319542784, webhooks.Webhook) + async def test_execute_webhook_with_optionals(self, rest_webhook_logic_impl, webhook): + rest_webhook_logic_impl._session.execute_webhook.return_value = ... + mock_allowed_mentions_payload = {"parse": ["everyone", "users", "roles"]} + mock_embed_payload = {"description": "424242"} + mock_embed_obj = mock.MagicMock(embeds.Embed) + mock_embed_obj.serialize = mock.MagicMock(return_value=mock_embed_payload) + mock_media_obj = mock.MagicMock() + mock_media_payload = ("aName.png", mock.MagicMock()) + stack = contextlib.ExitStack() + stack.enter_context(mock.patch.object(media, "safe_read_file", return_value=mock_media_payload)) + stack.enter_context(mock.patch.object(messages.Message, "deserialize")) + stack.enter_context( + mock.patch.object(allowed_mentions, "generate_allowed_mentions", return_value=mock_allowed_mentions_payload) + ) + with stack: + await rest_webhook_logic_impl.execute_webhook( + webhook, + "a.webhook.token", + content="THE TRUTH", + username="User 97", + avatar_url="httttttt/L//", + tts=True, + wait=True, + file=mock_media_obj, + embeds=[mock_embed_obj], + mentions_everyone=False, + role_mentions=False, + user_mentions=False, + ) + media.safe_read_file.assert_called_once_with(mock_media_obj) + allowed_mentions.generate_allowed_mentions.assert_called_once_with( + mentions_everyone=False, user_mentions=False, role_mentions=False + ) + rest_webhook_logic_impl._session.execute_webhook.assert_called_once_with( + webhook_id="379953393319542784", + webhook_token="a.webhook.token", + content="THE TRUTH", + username="User 97", + avatar_url="httttttt/L//", + tts=True, + wait=True, + file=mock_media_payload, + embeds=[mock_embed_payload], + allowed_mentions=mock_allowed_mentions_payload, + ) + mock_embed_obj.serialize.assert_called_once() + + @pytest.mark.asyncio + @_helpers.parametrize_valid_id_formats_for_models("webhook", 379953393319542784, webhooks.Webhook) + async def test_execute_webhook_returns_message_when_wait_is_true(self, rest_webhook_logic_impl, webhook): + mock_message_payload = {"id": "6796959949034", "content": "Nyaa Nyaa"} + mock_message_obj = mock.MagicMock(messages.Message) + rest_webhook_logic_impl._session.execute_webhook.return_value = mock_message_payload + mock_allowed_mentions_payload = {"parse": ["everyone", "users", "roles"]} + stack = contextlib.ExitStack() + stack.enter_context( + mock.patch.object(allowed_mentions, "generate_allowed_mentions", return_value=mock_allowed_mentions_payload) + ) + stack.enter_context(mock.patch.object(messages.Message, "deserialize", return_value=mock_message_obj)) + with stack: + assert ( + await rest_webhook_logic_impl.execute_webhook(webhook, "a.webhook.token", wait=True) is mock_message_obj + ) + messages.Message.deserialize.assert_called_once_with(mock_message_payload) + + @pytest.mark.asyncio + async def test_safe_execute_webhook_without_optionals(self, rest_webhook_logic_impl): + webhook = mock.MagicMock(webhooks.Webhook) + mock_message_obj = mock.MagicMock(messages.Message) + rest_webhook_logic_impl.execute_webhook = mock.AsyncMock(return_value=mock_message_obj) + result = await rest_webhook_logic_impl.safe_webhook_execute(webhook, "a.webhook.token",) + assert result is mock_message_obj + rest_webhook_logic_impl.execute_webhook.assert_called_once_with( + webhook=webhook, + webhook_token="a.webhook.token", + content=..., + username=..., + avatar_url=..., + tts=..., + wait=False, + file=..., + embeds=..., + mentions_everyone=False, + user_mentions=False, + role_mentions=False, + ) + + @pytest.mark.asyncio + async def test_safe_execute_webhook_with_optionals(self, rest_webhook_logic_impl): + webhook = mock.MagicMock(webhooks.Webhook) + mock_media_obj = mock.MagicMock(bytes) + mock_embed_obj = mock.MagicMock(embeds.Embed) + mock_message_obj = mock.MagicMock(messages.Message) + rest_webhook_logic_impl.execute_webhook = mock.AsyncMock(return_value=mock_message_obj) + result = await rest_webhook_logic_impl.safe_webhook_execute( + webhook, + "a.webhook.token", + content="THE TRUTH", + username="User 97", + avatar_url="httttttt/L//", + tts=True, + wait=True, + file=mock_media_obj, + embeds=[mock_embed_obj], + mentions_everyone=False, + role_mentions=False, + user_mentions=False, + ) + assert result is mock_message_obj + rest_webhook_logic_impl.execute_webhook.assert_called_once_with( + webhook=webhook, + webhook_token="a.webhook.token", + content="THE TRUTH", + username="User 97", + avatar_url="httttttt/L//", + tts=True, + wait=True, + file=mock_media_obj, + embeds=[mock_embed_obj], + mentions_everyone=False, + role_mentions=False, + user_mentions=False, + ) diff --git a/tests/hikari/internal/__init__.py b/tests/hikari/internal/__init__.py index 56fafa58b3..1c1502a5ca 100644 --- a/tests/hikari/internal/__init__.py +++ b/tests/hikari/internal/__init__.py @@ -1,2 +1,18 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- +# Copyright © Nekoka.tt 2019-2020 +# +# This file is part of Hikari. +# +# Hikari is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Hikari is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Hikari. If not, see . diff --git a/tests/hikari/internal/test_allowed_mentions.py b/tests/hikari/internal/test_allowed_mentions.py new file mode 100644 index 0000000000..36fdfcd1e6 --- /dev/null +++ b/tests/hikari/internal/test_allowed_mentions.py @@ -0,0 +1,119 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# Copyright © Nekoka.tt 2019-2020 +# +# This file is part of Hikari. +# +# Hikari is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Hikari is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Hikari. If not, see . +import pytest + +from hikari import guilds +from hikari import users +from hikari.internal import allowed_mentions +from tests.hikari import _helpers + + +@pytest.mark.parametrize( + ("kwargs", "expected_result"), + [ + ( + {"mentions_everyone": True, "user_mentions": True, "role_mentions": True}, + {"parse": ["everyone", "users", "roles"]}, + ), + ( + {"mentions_everyone": False, "user_mentions": False, "role_mentions": False}, + {"parse": [], "users": [], "roles": []}, + ), + ( + {"mentions_everyone": True, "user_mentions": ["1123123"], "role_mentions": True}, + {"parse": ["everyone", "roles"], "users": ["1123123"]}, + ), + ( + {"mentions_everyone": True, "user_mentions": True, "role_mentions": ["1231123"]}, + {"parse": ["everyone", "users"], "roles": ["1231123"]}, + ), + ( + {"mentions_everyone": False, "user_mentions": ["1123123"], "role_mentions": True}, + {"parse": ["roles"], "users": ["1123123"]}, + ), + ( + {"mentions_everyone": False, "user_mentions": True, "role_mentions": ["1231123"]}, + {"parse": ["users"], "roles": ["1231123"]}, + ), + ( + {"mentions_everyone": False, "user_mentions": ["1123123"], "role_mentions": False}, + {"parse": [], "roles": [], "users": ["1123123"]}, + ), + ( + {"mentions_everyone": False, "user_mentions": False, "role_mentions": ["1231123"]}, + {"parse": [], "roles": ["1231123"], "users": []}, + ), + ( + {"mentions_everyone": False, "user_mentions": ["22222"], "role_mentions": ["1231123"]}, + {"parse": [], "users": ["22222"], "roles": ["1231123"]}, + ), + ( + {"mentions_everyone": True, "user_mentions": ["22222"], "role_mentions": ["1231123"]}, + {"parse": ["everyone"], "users": ["22222"], "roles": ["1231123"]}, + ), + ], +) +def test_generate_allowed_mentions(kwargs, expected_result): + assert allowed_mentions.generate_allowed_mentions(**kwargs) == expected_result + + +@_helpers.parametrize_valid_id_formats_for_models("role", 3, guilds.GuildRole) +def test_generate_allowed_mentions_removes_duplicate_role_ids(role): + result = allowed_mentions.generate_allowed_mentions( + role_mentions=["1", "2", "1", "3", "5", "7", "2", role], user_mentions=True, mentions_everyone=True + ) + assert result == {"roles": ["1", "2", "3", "5", "7"], "parse": ["everyone", "users"]} + + +@_helpers.parametrize_valid_id_formats_for_models("user", 3, users.User) +def test_generate_allowed_mentions_removes_duplicate_user_ids(user): + result = allowed_mentions.generate_allowed_mentions( + role_mentions=True, user_mentions=["1", "2", "1", "3", "5", "7", "2", user], mentions_everyone=True + ) + assert result == {"users": ["1", "2", "3", "5", "7"], "parse": ["everyone", "roles"]} + + +@_helpers.parametrize_valid_id_formats_for_models("role", 190007233919057920, guilds.GuildRole) +def test_generate_allowed_mentions_handles_all_role_formats(role): + result = allowed_mentions.generate_allowed_mentions( + role_mentions=[role], user_mentions=True, mentions_everyone=True + ) + assert result == {"roles": ["190007233919057920"], "parse": ["everyone", "users"]} + + +@_helpers.parametrize_valid_id_formats_for_models("user", 190007233919057920, users.User) +def test_generate_allowed_mentions_handles_all_user_formats(user): + result = allowed_mentions.generate_allowed_mentions( + role_mentions=True, user_mentions=[user], mentions_everyone=True + ) + assert result == {"users": ["190007233919057920"], "parse": ["everyone", "roles"]} + + +@_helpers.assert_raises(type_=ValueError) +def test_generate_allowed_mentions_raises_error_on_too_many_roles(): + allowed_mentions.generate_allowed_mentions( + user_mentions=False, role_mentions=list(range(101)), mentions_everyone=False + ) + + +@_helpers.assert_raises(type_=ValueError) +def test_generate_allowed_mentions_raises_error_on_too_many_users(): + allowed_mentions.generate_allowed_mentions( + user_mentions=list(range(101)), role_mentions=False, mentions_everyone=False + ) diff --git a/tests/hikari/internal/test_meta.py b/tests/hikari/internal/test_meta.py index 66177e29fd..804c09ea24 100644 --- a/tests/hikari/internal/test_meta.py +++ b/tests/hikari/internal/test_meta.py @@ -24,3 +24,58 @@ class StubSingleton(metaclass=meta.SingletonMeta): pass assert StubSingleton() is StubSingleton() + + +class TestUniqueFunctionMeta: + def test_raises_type_error_on_duplicated_methods(self): + class StubMixin1(metaclass=meta.UniqueFunctionMeta): + def foo(self): + ... + + def bar(cls): + ... + + class StubMixin2(metaclass=meta.UniqueFunctionMeta): + def foo(cls): + ... + + def baz(cls): + ... + + try: + + class Impl(StubMixin1, StubMixin2): + ... + + assert False, "Should've raised a TypeError on overwritten function." + except TypeError: + pass + + def test_passes_when_no_duplication_present(self): + class StubMixin1(metaclass=meta.UniqueFunctionMeta): + def foo(self): + ... + + def bar(cls): + ... + + class StubMixin2(metaclass=meta.UniqueFunctionMeta): + def baz(cls): + ... + + class Impl(StubMixin1, StubMixin2): + ... + + def test_allows_duplicate_methods_when_inherited_from_same_base_further_up(self): + class StubMixin0(metaclass=meta.UniqueFunctionMeta): + def nyaa(self): + ... + + class StubMixin1(StubMixin0): + ... + + class StubMixin2(StubMixin0): + ... + + class Impl(StubMixin1, StubMixin2): + ... diff --git a/tests/hikari/internal/test_pagination.py b/tests/hikari/internal/test_pagination.py new file mode 100644 index 0000000000..f0f35d95e5 --- /dev/null +++ b/tests/hikari/internal/test_pagination.py @@ -0,0 +1,228 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# Copyright © Nekoka.tt 2019-2020 +# +# This file is part of Hikari. +# +# Hikari is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Hikari is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Hikari. If not, see . +import pytest +import mock + +from hikari.internal import pagination + + +@pytest.mark.asyncio +async def test__pagination_handler_ends_handles_empty_resource(): + mock_deserialize = mock.MagicMock() + mock_request = mock.AsyncMock(side_effect=[[]]) + async for _ in pagination.pagination_handler( + random_kwarg="test", + deserializer=mock_deserialize, + direction="before", + request=mock_request, + reversing=True, + start="123123123", + limit=42, + ): + assert False, "Async generator shouldn't have yielded anything." + mock_request.assert_called_once_with( + limit=42, before="123123123", random_kwarg="test", + ) + mock_deserialize.assert_not_called() + + +@pytest.mark.asyncio +async def test__pagination_handler_ends_without_limit_with_start(): + mock_payloads = [{"id": "312312312"}, {"id": "31231231"}, {"id": "20202020"}] + mock_models = [mock.MagicMock(), mock.MagicMock(), mock.MagicMock(id=20202020)] + mock_deserialize = mock.MagicMock(side_effect=mock_models) + mock_request = mock.AsyncMock(side_effect=[mock_payloads, []]) + results = [] + async for result in pagination.pagination_handler( + random_kwarg="test", + deserializer=mock_deserialize, + direction="before", + request=mock_request, + reversing=True, + start="123123123", + limit=None, + ): + results.append(result) + assert results == mock_models + mock_request.assert_has_calls( + [ + mock.call(limit=100, before="123123123", random_kwarg="test"), + mock.call(limit=100, before="20202020", random_kwarg="test"), + ], + ) + mock_deserialize.assert_has_calls( + [mock.call({"id": "20202020"}), mock.call({"id": "31231231"}), mock.call({"id": "312312312"})] + ) + + +@pytest.mark.asyncio +async def test__pagination_handler_ends_without_limit_without_start(): + mock_payloads = [{"id": "312312312"}, {"id": "31231231"}, {"id": "20202020"}] + mock_models = [mock.MagicMock(), mock.MagicMock(), mock.MagicMock(id=20202020)] + mock_deserialize = mock.MagicMock(side_effect=mock_models) + mock_request = mock.AsyncMock(side_effect=[mock_payloads, []]) + results = [] + async for result in pagination.pagination_handler( + random_kwarg="test", + deserializer=mock_deserialize, + direction="before", + request=mock_request, + reversing=True, + start=None, + limit=None, + ): + results.append(result) + assert results == mock_models + mock_request.assert_has_calls( + [ + mock.call(limit=100, before=..., random_kwarg="test"), + mock.call(limit=100, before="20202020", random_kwarg="test"), + ], + ) + mock_deserialize.assert_has_calls( + [mock.call({"id": "20202020"}), mock.call({"id": "31231231"}), mock.call({"id": "312312312"})] + ) + + +@pytest.mark.asyncio +async def test__pagination_handler_tracks_ends_when_hits_limit(): + mock_payloads = [{"id": "312312312"}, {"id": "31231231"}] + mock_models = [mock.MagicMock(), mock.MagicMock(id=20202020)] + mock_deserialize = mock.MagicMock(side_effect=mock_models) + mock_request = mock.AsyncMock(side_effect=[mock_payloads, []]) + results = [] + async for result in pagination.pagination_handler( + random_kwarg="test", + deserializer=mock_deserialize, + direction="before", + request=mock_request, + reversing=False, + start=None, + limit=2, + ): + results.append(result) + assert results == mock_models + mock_request.assert_called_once_with(limit=2, before=..., random_kwarg="test") + mock_deserialize.assert_has_calls([mock.call({"id": "312312312"}), mock.call({"id": "31231231"})]) + + +@pytest.mark.asyncio +async def test__pagination_handler_tracks_ends_when_limit_set_but_exhausts_requested_data(): + mock_payloads = [{"id": "312312312"}, {"id": "31231231"}, {"id": "20202020"}] + mock_models = [mock.MagicMock(), mock.MagicMock(), mock.MagicMock(id=20202020)] + mock_deserialize = mock.MagicMock(side_effect=mock_models) + mock_request = mock.AsyncMock(side_effect=[mock_payloads, []]) + results = [] + async for result in pagination.pagination_handler( + random_kwarg="test", + deserializer=mock_deserialize, + direction="before", + request=mock_request, + reversing=False, + start=None, + limit=42, + ): + results.append(result) + assert results == mock_models + mock_request.assert_has_calls( + [ + mock.call(limit=42, before=..., random_kwarg="test"), + mock.call(limit=39, before="20202020", random_kwarg="test"), + ], + ) + mock_deserialize.assert_has_calls( + [mock.call({"id": "312312312"}), mock.call({"id": "31231231"}), mock.call({"id": "20202020"})] + ) + + +@pytest.mark.asyncio +async def test__pagination_handler_reverses_data_when_reverse_is_true(): + mock_payloads = [{"id": "312312312"}, {"id": "31231231"}, {"id": "20202020"}] + mock_models = [mock.MagicMock(), mock.MagicMock(), mock.MagicMock(id=20202020)] + mock_deserialize = mock.MagicMock(side_effect=mock_models) + mock_request = mock.AsyncMock(side_effect=[mock_payloads, []]) + results = [] + async for result in pagination.pagination_handler( + random_kwarg="test", + deserializer=mock_deserialize, + direction="before", + request=mock_request, + reversing=True, + start=None, + limit=None, + ): + results.append(result) + assert results == mock_models + mock_request.assert_has_calls( + [ + mock.call(limit=100, before=..., random_kwarg="test"), + mock.call(limit=100, before="20202020", random_kwarg="test"), + ], + ) + mock_deserialize.assert_has_calls( + [mock.call({"id": "20202020"}), mock.call({"id": "31231231"}), mock.call({"id": "312312312"})] + ) + + +@pytest.mark.asyncio +async def test__pagination_handler_id_getter(): + mock_payloads = [{"id": "312312312"}, {"id": "20202020"}] + mock_models = [mock.MagicMock(), mock.MagicMock(user=mock.MagicMock(__int__=lambda x: 20202020))] + mock_deserialize = mock.MagicMock(side_effect=mock_models) + mock_request = mock.AsyncMock(side_effect=[mock_payloads, []]) + results = [] + async for result in pagination.pagination_handler( + random_kwarg="test", + deserializer=mock_deserialize, + direction="before", + request=mock_request, + reversing=False, + start=None, + id_getter=lambda entity: str(int(entity.user)), + limit=None, + ): + results.append(result) + assert results == mock_models + mock_request.assert_has_calls( + [ + mock.call(limit=100, before=..., random_kwarg="test"), + mock.call(limit=100, before="20202020", random_kwarg="test"), + ], + ) + mock_deserialize.assert_has_calls([mock.call({"id": "312312312"}), mock.call({"id": "20202020"})]) + + +@pytest.mark.asyncio +async def test__pagination_handler_handles_no_initial_data(): + mock_deserialize = mock.MagicMock() + mock_request = mock.AsyncMock(side_effect=[[]]) + async for _ in pagination.pagination_handler( + random_kwarg="test", + deserializer=mock_deserialize, + direction="before", + request=mock_request, + reversing=True, + start=None, + limit=None, + ): + assert False, "Async generator shouldn't have yielded anything." + mock_request.assert_called_once_with( + limit=100, before=..., random_kwarg="test", + ) + mock_deserialize.assert_not_called() diff --git a/tests/hikari/test_embeds.py b/tests/hikari/test_embeds.py index e74abd097d..5f2a2107b2 100644 --- a/tests/hikari/test_embeds.py +++ b/tests/hikari/test_embeds.py @@ -21,9 +21,9 @@ import pytest -import hikari.internal.conversions from hikari import colors from hikari import embeds +from hikari.internal import conversions from tests.hikari import _helpers @@ -237,10 +237,7 @@ def test_deserialize( mock_datetime = mock.MagicMock(datetime.datetime) with _helpers.patch_marshal_attr( - embeds.Embed, - "timestamp", - deserializer=hikari.internal.conversions.parse_iso_8601_ts, - return_value=mock_datetime, + embeds.Embed, "timestamp", deserializer=conversions.parse_iso_8601_ts, return_value=mock_datetime, ) as patched_timestamp_deserializer: embed_obj = embeds.Embed.deserialize(test_embed_payload) patched_timestamp_deserializer.assert_called_once_with("2020-03-22T16:40:39.218000+00:00") diff --git a/tests/hikari/test_guilds.py b/tests/hikari/test_guilds.py index 762c9adf4c..6669b6d972 100644 --- a/tests/hikari/test_guilds.py +++ b/tests/hikari/test_guilds.py @@ -22,7 +22,6 @@ import pytest -import hikari.internal.conversions from hikari import channels from hikari import colors from hikari import emojis @@ -30,6 +29,7 @@ from hikari import guilds from hikari import permissions from hikari import users +from hikari.internal import conversions from hikari.internal import urls from tests.hikari import _helpers @@ -302,7 +302,7 @@ def test_deserialize(self, test_member_payload, test_user_payload): _helpers.patch_marshal_attr( guilds.GuildMember, "joined_at", - deserializer=hikari.internal.conversions.parse_iso_8601_ts, + deserializer=conversions.parse_iso_8601_ts, return_value=mock_datetime_1, ) ) @@ -310,7 +310,7 @@ def test_deserialize(self, test_member_payload, test_user_payload): _helpers.patch_marshal_attr( guilds.GuildMember, "premium_since", - deserializer=hikari.internal.conversions.parse_iso_8601_ts, + deserializer=conversions.parse_iso_8601_ts, return_value=mock_datetime_2, ) ) @@ -405,7 +405,7 @@ def test_deserialize(self, test_activity_timestamps_payload): _helpers.patch_marshal_attr( guilds.ActivityTimestamps, "start", - deserializer=hikari.internal.conversions.unix_epoch_to_datetime, + deserializer=conversions.unix_epoch_to_datetime, return_value=mock_start_date, ) ) @@ -413,7 +413,7 @@ def test_deserialize(self, test_activity_timestamps_payload): _helpers.patch_marshal_attr( guilds.ActivityTimestamps, "end", - deserializer=hikari.internal.conversions.unix_epoch_to_datetime, + deserializer=conversions.unix_epoch_to_datetime, return_value=mock_end_date, ) ) @@ -483,7 +483,7 @@ def test_deserialize( _helpers.patch_marshal_attr( guilds.PresenceActivity, "created_at", - deserializer=hikari.internal.conversions.unix_epoch_to_datetime, + deserializer=conversions.unix_epoch_to_datetime, return_value=mock_created_at, ) ) @@ -627,7 +627,7 @@ def test_deserialize( with _helpers.patch_marshal_attr( guilds.GuildMemberPresence, "premium_since", - deserializer=hikari.internal.conversions.parse_iso_8601_ts, + deserializer=conversions.parse_iso_8601_ts, return_value=mock_since, ) as patched_since_deserializer: guild_member_presence_obj = guilds.GuildMemberPresence.deserialize(test_guild_member_presence) @@ -717,7 +717,7 @@ def test_deserialize(self, test_guild_integration_payload, test_user_payload, te _helpers.patch_marshal_attr( guilds.GuildIntegration, "last_synced_at", - deserializer=hikari.internal.conversions.parse_iso_8601_ts, + deserializer=conversions.parse_iso_8601_ts, return_value=mock_sync_date, ) ) @@ -944,7 +944,7 @@ def test_deserialize( | guilds.GuildSystemChannelFlag.SUPPRESS_USER_JOIN ) assert guild_obj.rules_channel_id == 42042069 - assert guild_obj.joined_at == hikari.internal.conversions.parse_iso_8601_ts("2019-05-17T06:26:56.936000+00:00") + assert guild_obj.joined_at == conversions.parse_iso_8601_ts("2019-05-17T06:26:56.936000+00:00") assert guild_obj.is_large is False assert guild_obj.member_count == 14 assert guild_obj.channels == {6969: mock_guild_channel} From e3a61071f1125aa5029ad26a833069f5eda09b41 Mon Sep 17 00:00:00 2001 From: davfsa Date: Fri, 17 Apr 2020 21:43:25 +0200 Subject: [PATCH 158/922] Fix a bug where the bot would silently shutdown after error --- hikari/clients/shard_clients.py | 3 ++- hikari/errors.py | 14 ++++++++++++++ hikari/net/shard.py | 8 ++++++-- tests/hikari/clients/test_shard_clients.py | 1 + 4 files changed, 23 insertions(+), 3 deletions(-) diff --git a/hikari/clients/shard_clients.py b/hikari/clients/shard_clients.py index 5bb25e56b2..1b55d957e2 100644 --- a/hikari/clients/shard_clients.py +++ b/hikari/clients/shard_clients.py @@ -410,7 +410,8 @@ async def _keep_alive(self): raise ex from None self.logger.warning("disconnected by Discord, will attempt to reconnect") - + except errors.GatewayClientDisconnectedError: + self.logger.warning("unexpected connection close, will attempt to reconnect") except errors.GatewayClientClosedError: self.logger.warning("shutting down") return diff --git a/hikari/errors.py b/hikari/errors.py index ee93cba096..1ab9d6ad11 100644 --- a/hikari/errors.py +++ b/hikari/errors.py @@ -33,6 +33,7 @@ "GatewayInvalidTokenError", "GatewayServerClosedConnectionError", "GatewayClientClosedError", + "GatewayClientDisconnectedError", "GatewayError", ] @@ -90,6 +91,19 @@ def __init__(self, reason: str = "The gateway client has been closed") -> None: super().__init__(reason) +class GatewayClientDisconnectedError(GatewayError): + """An exception raised when the bot client-side disconnects unexpectedly. + + Parameters + ---------- + reason : :obj:`~str` + A string explaining the issue. + """ + + def __init__(self, reason: str = "The gateway client has disconnected unexpectedly") -> None: + super().__init__(reason) + + class GatewayServerClosedConnectionError(GatewayError): """An exception raised when the server closes the connection. diff --git a/hikari/net/shard.py b/hikari/net/shard.py index 547cb8b164..6f6bafcb53 100644 --- a/hikari/net/shard.py +++ b/hikari/net/shard.py @@ -830,8 +830,12 @@ async def _receive(self): raise errors.GatewayServerClosedConnectionError(close_code) if message.type in (aiohttp.WSMsgType.CLOSING, aiohttp.WSMsgType.CLOSED): - self.logger.debug("connection has been marked as closed") - raise errors.GatewayClientClosedError() + if self.requesting_close_event.is_set(): + self.logger.debug("connection has been marked as closed") + raise errors.GatewayClientClosedError() + + self.logger.debug("connection has been marked as closed unexpectedly") + raise errors.GatewayClientDisconnectedError() if message.type == aiohttp.WSMsgType.ERROR: ex = self._ws.exception() diff --git a/tests/hikari/clients/test_shard_clients.py b/tests/hikari/clients/test_shard_clients.py index 9a0f80ae4f..7cf2fd5fb3 100644 --- a/tests/hikari/clients/test_shard_clients.py +++ b/tests/hikari/clients/test_shard_clients.py @@ -166,6 +166,7 @@ async def test_close_when_already_stopping(self, shard_client_obj): errors.GatewayInvalidSessionError(False), errors.GatewayInvalidSessionError(True), errors.GatewayMustReconnectError, + errors.GatewayClientDisconnectedError, ], ) @pytest.mark.asyncio From 34a42d7a659fd97c2252d22b5fb05727696b0864 Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Fri, 17 Apr 2020 20:17:14 +0100 Subject: [PATCH 159/922] Updated gateway runner to be a test runner, and added one piece of functionality to it as a proof of concept. --- hikari/clients/gateway_runner.py | 106 ---------------- hikari/clients/shard_clients.py | 3 + hikari/clients/test_client.py | 188 ++++++++++++++++++++++++++++ hikari/internal/more_collections.py | 1 - 4 files changed, 191 insertions(+), 107 deletions(-) delete mode 100644 hikari/clients/gateway_runner.py create mode 100644 hikari/clients/test_client.py diff --git a/hikari/clients/gateway_runner.py b/hikari/clients/gateway_runner.py deleted file mode 100644 index fc96b26c31..0000000000 --- a/hikari/clients/gateway_runner.py +++ /dev/null @@ -1,106 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# Copyright © Nekoka.tt 2019-2020 -# -# This file is part of Hikari. -# -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . -"""An executable module to be used to test that the gateway works as intended. - -This is only for use by developers of this library, regular users do not need -to use this. -""" -import logging -import os -import sys -import typing - -import click - -from hikari.clients import configs -from hikari.clients import gateway_managers -from hikari.internal import conversions -from hikari.net import codes -from hikari.state import stateless_event_managers - - -_LOGGER_LEVELS: typing.Final[typing.Sequence[str]] = ["DEBUG", "INFO", "WARNING", "ERROR", "NOTSET"] - - -def _supports_color(): - plat = sys.platform - supported_platform = plat != "Pocket PC" and (plat != "win32" or "ANSICON" in os.environ) - # isatty is not always implemented, #6223. - is_a_tty = hasattr(sys.stdout, "isatty") and sys.stdout.isatty() - return supported_platform and is_a_tty - - -_COLOR_FORMAT: typing.Final[str] = ( - "\033[1;35m%(levelname)1.1s \033[0;37m%(name)25.25s \033[0;31m%(asctime)23.23s \033[1;34m%(module)-15.15s " - "\033[1;32m#%(lineno)-4d \033[0m:: \033[0;33m%(message)s\033[0m" -) - -_REGULAR_FORMAT: typing.Final[str] = ( - "%(levelname)1.1s %(name)25.25s %(asctime)23.23s %(module)-15.15s #%(lineno)-4d :: %(message)s" -) - - -@click.command() -@click.option("--compression", default=True, type=click.BOOL, help="Enable or disable gateway compression.") -@click.option("--color", default=_supports_color(), type=click.BOOL, help="Whether to enable or disable color.") -@click.option("--debug", default=False, type=click.BOOL, help="Enable or disable debug mode.") -@click.option("--intents", default=None, type=click.STRING, help="Intent names to enable (comma separated)") -@click.option("--logger", envvar="LOGGER", default="INFO", type=click.Choice(_LOGGER_LEVELS), help="Logger verbosity.") -@click.option("--shards", default=1, type=click.IntRange(min=1), help="The number of shards to explicitly use.") -@click.option("--token", required=True, envvar="TOKEN", help="The token to use to authenticate with Discord.") -@click.option("--url", default="wss://gateway.discord.gg/", help="The websocket URL to connect to.") -@click.option("--verify-ssl", default=True, type=click.BOOL, help="Enable or disable SSL verification.") -@click.option("--version", default=6, type=click.IntRange(min=6), help="Version of the gateway to use.") -def run_gateway(compression, color, debug, intents, logger, shards, token, url, verify_ssl, version) -> None: - """:mod:`click` command line client for running a test gateway connection. - - This is provided for internal testing purposes for benchmarking API - stability, etc. - """ - if intents is not None: - intents = intents.split(",") - intents = conversions.dereference_int_flag(codes.GatewayIntent, intents) - - logging.captureWarnings(True) - - logging.basicConfig(level=logger, format=_COLOR_FORMAT if color else _REGULAR_FORMAT, stream=sys.stdout) - - manager = stateless_event_managers.StatelessEventManagerImpl() - - client = gateway_managers.GatewayManager( - shard_ids=[*range(shards)], - shard_count=shards, - config=configs.WebsocketConfig( - token=token, - gateway_version=version, - debug=debug, - gateway_use_compression=compression, - intents=intents, - verify_ssl=verify_ssl, - ), - url=url, - raw_event_consumer_impl=manager, - dispatcher=manager.event_dispatcher, - ) - - client.run() - - -if __name__ == "__main__": - run_gateway() # pylint:disable=no-value-for-parameter diff --git a/hikari/clients/shard_clients.py b/hikari/clients/shard_clients.py index fbd448786b..869ec6a56b 100644 --- a/hikari/clients/shard_clients.py +++ b/hikari/clients/shard_clients.py @@ -77,6 +77,9 @@ class ShardState(enum.IntEnum): #: The shard has shut down and is no longer connected. STOPPED = enum.auto() + def __str__(self) -> str: + return self.name + class ShardClient(runnable.RunnableClient): """The primary interface for a single shard connection. diff --git a/hikari/clients/test_client.py b/hikari/clients/test_client.py new file mode 100644 index 0000000000..2d73fedea0 --- /dev/null +++ b/hikari/clients/test_client.py @@ -0,0 +1,188 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# Copyright © Nekoka.tt 2019-2020 +# +# This file is part of Hikari. +# +# Hikari is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Hikari is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Hikari. If not, see . +"""An executable module to be used to test that the gateway works as intended. + +This is only for use by developers of this library, regular users do not need +to use this. +""" +import datetime +import logging +import math +import os +import re +import sys +import time +import typing + +import click + +import hikari +from hikari.internal import conversions +from hikari.net import codes + +_LOGGER_LEVELS: typing.Final[typing.Sequence[str]] = ["DEBUG", "INFO", "WARNING", "ERROR", "NOTSET"] + + +def _supports_color(): + plat = sys.platform + supported_platform = plat != "Pocket PC" and (plat != "win32" or "ANSICON" in os.environ) + # isatty is not always implemented, #6223. + is_a_tty = hasattr(sys.stdout, "isatty") and sys.stdout.isatty() + return supported_platform and is_a_tty + + +_COLOR_FORMAT: typing.Final[str] = ( + "\033[1;35m%(levelname)1.1s \033[0;37m%(name)25.25s \033[0;31m%(asctime)23.23s \033[1;34m%(module)-15.15s " + "\033[1;32m#%(lineno)-4d \033[0m:: \033[0;33m%(message)s\033[0m" +) + +_REGULAR_FORMAT: typing.Final[str] = ( + "%(levelname)1.1s %(name)25.25s %(asctime)23.23s %(module)-15.15s #%(lineno)-4d :: %(message)s" +) + + +@click.command() +@click.option("--compression", default=True, type=click.BOOL, help="Enable or disable gateway compression.") +@click.option("--color", default=_supports_color(), type=click.BOOL, help="Whether to enable or disable color.") +@click.option("--debug", default=False, type=click.BOOL, help="Enable or disable debug mode.") +@click.option("--intents", default=None, type=click.STRING, help="Intent names to enable (comma separated)") +@click.option("--logger", envvar="LOGGER", default="INFO", type=click.Choice(_LOGGER_LEVELS), help="Logger verbosity.") +@click.option("--shards", default=1, type=click.IntRange(min=1), help="The number of shards to explicitly use.") +@click.option("--token", required=True, envvar="TOKEN", help="The token to use to authenticate with Discord.") +@click.option("--verify-ssl", default=True, type=click.BOOL, help="Enable or disable SSL verification.") +@click.option("--version", default=6, type=click.IntRange(min=6), help="Version of the gateway to use.") +def run_gateway(compression, color, debug, intents, logger, shards, token, verify_ssl, version) -> None: + """:mod:`click` command line client for running a test gateway connection. + + This is provided for internal testing purposes for benchmarking API + stability, etc. + """ + if intents is not None: + intents = intents.split(",") + intents = conversions.dereference_int_flag(codes.GatewayIntent, intents) + + logging.captureWarnings(True) + + logging.basicConfig(level=logger, format=_COLOR_FORMAT if color else _REGULAR_FORMAT, stream=sys.stdout) + + client = hikari.StatelessBot( + hikari.BotConfig( + token=token, + gateway_version=version, + debug=debug, + gateway_use_compression=compression, + intents=intents, + verify_ssl=verify_ssl, + shard_count=shards, + initial_activity=hikari.GatewayActivity(name="people mention me", type=hikari.ActivityType.LISTENING,), + ) + ) + + bot_id = 0 + bot_avatar_url = "about:blank" + startup_time = 0 + + @client.on() + async def on_start(_: hikari.StartingEvent): + nonlocal startup_time + startup_time = time.perf_counter() + + @client.on() + async def on_ready(event: hikari.ReadyEvent) -> None: + nonlocal bot_id, bot_avatar_url + bot_id = event.my_user.id + bot_avatar_url = event.my_user.avatar_url + + def since(epoch): + if math.isnan(epoch): + return "never" + return datetime.timedelta(seconds=time.perf_counter() - epoch) + + @client.on() + async def on_message(event: hikari.MessageCreateEvent) -> None: + if not event.author.is_bot and re.match(f"^<@!?{bot_id}>$", event.content): + start = time.perf_counter() + message = await client.rest.create_message(event.channel_id, content="Pong!") + rest_time = time.perf_counter() - start + + if (raw_intents := client.gateway.shards[0].connection.intents) is not None: + active_intents = [] + for i in hikari.GatewayIntent: + if i & raw_intents: + active_intents.append(i.name) + active_intents = ", ".join(active_intents) + else: + active_intents = "not enabled" + + shard_infos = [] + for shard_id, shard in client.gateway.shards.items(): + shard_info = ( + f"latency: {shard.latency * 1_000:.0f} ms\n" + f"seq: {shard.connection.seq}\n" + f"session id: {shard.connection.session_id}\n" + f"reconnects: {shard.reconnect_count}\n" + f"last payload: {since(shard.connection.last_message_received)} ago\n" + f"last heartbeat: {since(shard.connection.last_heartbeat_sent)} ago\n" + f"heartbeat interval: {shard.connection.heartbeat_interval} s\n" + f"state: {shard.connection_state.name}\n" + ) + + shard_infos.append(hikari.EmbedField(name=f"Shard {shard_id}", value=shard_info, is_inline=False)) + + gw_info = ( + f"average latency: {client.gateway.latency * 1_000:.0f} ms\n" + f"shards: {len(client.gateway.shards)}\n" + f"version: {version}\n" + f"compression: {compression}\n" + f"debug: {debug}\n" + f"intents: {hex(intents)} ({active_intents})" + ) + + rest_info = ( + f"message edit time: {rest_time * 1_000:.0f} ms\n" + f"global rate limiter backlog: {len(client.rest._session.global_ratelimiter.queue)}\n" + f"bucket rate limiter active routes: {len(client.rest._session.ratelimiter.routes_to_hashes)}\n" + f"bucket rate limiter active buckets: {len(client.rest._session.ratelimiter.real_hashes_to_buckets)}\n" + "bucket rate limiter backlog: " + f"{sum(len(b.queue) for b in client.rest._session.ratelimiter.real_hashes_to_buckets.values())}\n" + f"bucket rate limiter GC: {getattr(client.rest._session.ratelimiter.gc_task, '_state', 'dead')}\n" + ) + + embed = hikari.Embed( + author=hikari.EmbedAuthor(name=hikari.__copyright__), + url=hikari.__url__, + title=f"Hikari {hikari.__version__} debugging test client", + footer=hikari.EmbedFooter(text=hikari.__license__), + description=f"Uptime: {since(startup_time)}", + fields=[ + hikari.EmbedField(name="REST", value=rest_info, is_inline=False), + hikari.EmbedField(name="Gateway Manager", value=gw_info, is_inline=False), + *shard_infos[:3], + ], + thumbnail=hikari.EmbedThumbnail(url=bot_avatar_url), + color=hikari.Color["#F660AB"], + ) + + await client.rest.update_message(message, message.channel_id, content="Pong!", embed=embed) + + client.run() + + +if __name__ == "__main__": + run_gateway() # pylint:disable=no-value-for-parameter diff --git a/hikari/internal/more_collections.py b/hikari/internal/more_collections.py index 29878967fd..d3baf0262b 100644 --- a/hikari/internal/more_collections.py +++ b/hikari/internal/more_collections.py @@ -54,7 +54,6 @@ class WeakKeyDictionary(typing.Generic[K, V], weakref.WeakKeyDictionary, typing. Example ------- - .. code-block:: python @attr.s(auto_attribs=True) From a2dd1e041b7eb8bf87fc7c48db83c6cd2b6b1251 Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Fri, 17 Apr 2020 17:38:27 +0100 Subject: [PATCH 160/922] Moved 'unset' to its own file, moved deserialize/serialize to marshaller. --- hikari/__init__.py | 2 + hikari/audit_logs.py | 8 +- hikari/channels.py | 10 +- hikari/clients/bot_clients.py | 44 +--- hikari/clients/configs.py | 5 +- hikari/embeds.py | 16 +- hikari/emojis.py | 2 +- hikari/entities.py | 50 +--- hikari/events.py | 219 +++++++++--------- hikari/gateway_entities.py | 6 +- hikari/guilds.py | 103 ++++---- hikari/internal/__init__.py | 2 +- hikari/internal/marshaller.py | 32 +++ hikari/invites.py | 6 +- hikari/messages.py | 10 +- hikari/oauth2.py | 10 +- hikari/snowflakes.py | 2 +- hikari/unset.py | 69 ++++++ hikari/users.py | 5 +- hikari/voices.py | 4 +- hikari/webhooks.py | 3 +- tests/hikari/_helpers.py | 2 +- tests/hikari/clients/test_gateway_managers.py | 2 +- tests/hikari/clients/test_shard_clients.py | 2 +- tests/hikari/internal/test_conversions.py | 2 +- tests/hikari/internal/test_marshaller.py | 3 +- .../hikari/internal/test_marshaller_pep563.py | 3 +- tests/hikari/net/test_ratelimits.py | 2 +- tests/hikari/net/test_rest.py | 3 +- tests/hikari/net/test_shard.py | 2 +- tests/hikari/state/test_event_dispatcher.py | 2 +- .../state/test_stateless_event_managers.py | 2 - tests/hikari/test_audit_logs.py | 2 +- tests/hikari/test_channels.py | 2 +- tests/hikari/test_embeds.py | 2 +- tests/hikari/test_emojis.py | 1 - tests/hikari/test_entities.py | 16 -- tests/hikari/test_events.py | 24 +- tests/hikari/test_gateway_entities.py | 2 +- tests/hikari/test_guilds.py | 32 +-- tests/hikari/test_invites.py | 2 +- tests/hikari/test_messages.py | 11 +- tests/hikari/test_oauth2.py | 1 - tests/hikari/test_snowflake.py | 3 +- tests/hikari/test_unset.py | 34 +++ tests/hikari/test_users.py | 1 - 46 files changed, 398 insertions(+), 368 deletions(-) create mode 100644 hikari/unset.py create mode 100644 tests/hikari/test_unset.py diff --git a/hikari/__init__.py b/hikari/__init__.py index 1181796085..becef4608c 100644 --- a/hikari/__init__.py +++ b/hikari/__init__.py @@ -63,6 +63,7 @@ from hikari.permissions import * from hikari.snowflakes import * from hikari.state import * +from hikari.unset import * from hikari.users import * from hikari.voices import * from hikari.webhooks import * @@ -88,6 +89,7 @@ *permissions.__all__, *snowflakes.__all__, *state.__all__, + *unset.__all__, *users.__all__, *voices.__all__, *webhooks.__all__, diff --git a/hikari/audit_logs.py b/hikari/audit_logs.py index bb0c45382f..8f279c4c5b 100644 --- a/hikari/audit_logs.py +++ b/hikari/audit_logs.py @@ -167,7 +167,7 @@ def __str__(self) -> str: @marshaller.marshallable() @attr.s(slots=True, kw_only=True) -class AuditLogChange(entities.HikariEntity, entities.Deserializable): +class AuditLogChange(entities.HikariEntity, marshaller.Deserializable): """Represents a change made to an audit log entry's target entity.""" #: The new value of the key, if something was added or changed. @@ -273,7 +273,7 @@ def decorator(cls): @marshaller.marshallable() @attr.s(slots=True, kw_only=True) -class BaseAuditLogEntryInfo(abc.ABC, entities.HikariEntity, entities.Deserializable): +class BaseAuditLogEntryInfo(abc.ABC, entities.HikariEntity, marshaller.Deserializable): """A base object that all audit log entry info objects will inherit from.""" @@ -429,7 +429,7 @@ def get_entry_info_entity(type_: int) -> typing.Type[BaseAuditLogEntryInfo]: @marshaller.marshallable() @attr.s(slots=True, kw_only=True) -class AuditLogEntry(snowflakes.UniqueEntity, entities.Deserializable): +class AuditLogEntry(snowflakes.UniqueEntity, marshaller.Deserializable): """Represents an entry in a guild's audit log.""" #: The ID of the entity affected by this change, if applicable. @@ -496,7 +496,7 @@ def deserialize(cls, payload: entities.RawEntityT) -> "AuditLogEntry": @marshaller.marshallable() @attr.s(slots=True, kw_only=True) -class AuditLog(entities.HikariEntity, entities.Deserializable): +class AuditLog(entities.HikariEntity, marshaller.Deserializable): """Represents a guilds audit log.""" #: A sequence of the audit log's entries. diff --git a/hikari/channels.py b/hikari/channels.py index 7d760f07c7..8ac5b7feff 100644 --- a/hikari/channels.py +++ b/hikari/channels.py @@ -102,7 +102,7 @@ def __str__(self) -> str: @marshaller.marshallable() @attr.s(slots=True, kw_only=True) -class PermissionOverwrite(snowflakes.UniqueEntity, entities.Deserializable, entities.Serializable): +class PermissionOverwrite(snowflakes.UniqueEntity, marshaller.Deserializable, marshaller.Serializable): """Represents permission overwrites for a channel or role in a channel.""" #: The type of entity this overwrite targets. @@ -157,7 +157,7 @@ def decorator(cls): @marshaller.marshallable() @attr.s(slots=True, kw_only=True) -class Channel(snowflakes.UniqueEntity, entities.Deserializable): +class Channel(snowflakes.UniqueEntity, marshaller.Deserializable): """Base class for all channels.""" #: The channel's type. @@ -366,7 +366,7 @@ class GuildVoiceChannel(GuildChannel): user_limit: int = marshaller.attrib(deserializer=int) -class GuildChannelBuilder(entities.Serializable): +class GuildChannelBuilder(marshaller.Serializable): """Used to create channel objects to send in guild create requests. Parameters @@ -393,12 +393,12 @@ class GuildChannelBuilder(entities.Serializable): __slots__ = ("_payload",) def __init__(self, channel_name: str, channel_type: ChannelType) -> None: - self._payload: entities.RawEntityT = { + self._payload: typing.Dict[str, typing.Any] = { "type": channel_type, "name": channel_name, } - def serialize(self: entities.T_co) -> entities.RawEntityT: + def serialize(self: "GuildChannelBuilder") -> entities.RawEntityT: """Serialize this instance into a naive value.""" return self._payload diff --git a/hikari/clients/bot_clients.py b/hikari/clients/bot_clients.py index 80c2081dcf..fd2c36ce71 100644 --- a/hikari/clients/bot_clients.py +++ b/hikari/clients/bot_clients.py @@ -25,12 +25,12 @@ import typing from hikari import events +from hikari import unset from hikari.clients import configs from hikari.clients import gateway_managers from hikari.clients import rest_clients from hikari.clients import runnable from hikari.clients import shard_clients -from hikari.internal import meta from hikari.internal import more_asyncio from hikari.internal import more_logging from hikari.state import event_dispatchers @@ -38,34 +38,6 @@ from hikari.state import stateless_event_managers -class _NotInitializedYet(meta.Singleton): - """Sentinel value for fields that are not yet initialized. - - These will be filled once the bot has started. - """ - - __slots__ = () - - def __bool__(self) -> bool: - return False - - def __defer(self, *args, **kwargs) -> typing.NoReturn: - raise TypeError("Bot has not yet initialized, so attribute is not available.") from None - - __call__ = __defer - __getattr__ = __defer - __setattr__ = __defer - __delattr__ = __defer - __getitem__ = __defer - __setitem__ = __defer - __delitem__ = __defer - __await__ = __defer - __enter__ = __defer - __aenter__ = __defer - __exit__ = __defer - __aexit__ = __defer - - class BotBase(runnable.RunnableClient, event_dispatchers.EventDispatcher): """An abstract base class for a bot implementation. @@ -94,7 +66,7 @@ class BotBase(runnable.RunnableClient, event_dispatchers.EventDispatcher): #: This will be initialized lazily once the bot has started. #: #: :type: :obj:`~hikari.clients.gateway_managers.GatewayManager` [ :obj:`~hikari.clients.shard_clients.ShardClient` ] - gateway: gateway_managers.GatewayManager[shard_clients.ShardClient] + gateway: unset.MayBeUnset[gateway_managers.GatewayManager[shard_clients.ShardClient]] #: The logger to use for this bot. #: @@ -108,15 +80,15 @@ class BotBase(runnable.RunnableClient, event_dispatchers.EventDispatcher): #: This will be initialized lazily once the bot has started. #: #: :type: :obj:`~hikari.clients.rest_clients.RESTClient` - rest: rest_clients.RESTClient + rest: unset.MayBeUnset[rest_clients.RESTClient] @abc.abstractmethod def __init__(self, config: configs.BotConfig, event_manager: event_managers.EventManager) -> None: super().__init__(more_logging.get_named_logger(self)) self.config = config self.event_manager = event_manager - self.gateway = _NotInitializedYet() - self.rest = _NotInitializedYet() + self.gateway = unset.UNSET + self.rest = unset.UNSET async def start(self): if self.rest or self.gateway: @@ -152,11 +124,11 @@ async def close(self) -> None: self.event_manager.event_dispatcher.close() if self.rest: await self.rest.close() - self.gateway = _NotInitializedYet() - self.rest = _NotInitializedYet() + self.gateway = unset.UNSET + self.rest = unset.UNSET async def join(self) -> None: - if self.gateway: + if not unset.is_unset(self.gateway): await self.gateway.join() def add_listener( diff --git a/hikari/clients/configs.py b/hikari/clients/configs.py index 50acb42c65..a962b8ae05 100644 --- a/hikari/clients/configs.py +++ b/hikari/clients/configs.py @@ -37,17 +37,16 @@ import aiohttp import attr -from hikari.internal import conversions -from hikari import entities from hikari import gateway_entities from hikari import guilds +from hikari.internal import conversions from hikari.internal import marshaller from hikari.net import codes @marshaller.marshallable() @attr.s(kw_only=True) -class BaseConfig(entities.Deserializable): +class BaseConfig(marshaller.Deserializable): """Base class for any configuration data class.""" diff --git a/hikari/embeds.py b/hikari/embeds.py index dfc83cea84..5b5a53c6bd 100644 --- a/hikari/embeds.py +++ b/hikari/embeds.py @@ -41,7 +41,7 @@ @marshaller.marshallable() @attr.s(slots=True, kw_only=True) -class EmbedFooter(entities.HikariEntity, entities.Deserializable, entities.Serializable): +class EmbedFooter(entities.HikariEntity, marshaller.Deserializable, marshaller.Serializable): """Represents an embed footer.""" #: The footer text. @@ -72,7 +72,7 @@ class EmbedFooter(entities.HikariEntity, entities.Deserializable, entities.Seria @marshaller.marshallable() @attr.s(slots=True, kw_only=True) -class EmbedImage(entities.HikariEntity, entities.Deserializable, entities.Serializable): +class EmbedImage(entities.HikariEntity, marshaller.Deserializable, marshaller.Serializable): """Represents an embed image.""" #: The URL of the image. @@ -118,7 +118,7 @@ class EmbedImage(entities.HikariEntity, entities.Deserializable, entities.Serial @marshaller.marshallable() @attr.s(slots=True, kw_only=True) -class EmbedThumbnail(entities.HikariEntity, entities.Deserializable, entities.Serializable): +class EmbedThumbnail(entities.HikariEntity, marshaller.Deserializable, marshaller.Serializable): """Represents an embed thumbnail.""" #: The URL of the thumbnail. @@ -164,7 +164,7 @@ class EmbedThumbnail(entities.HikariEntity, entities.Deserializable, entities.Se @marshaller.marshallable() @attr.s(slots=True, kw_only=True) -class EmbedVideo(entities.HikariEntity, entities.Deserializable): +class EmbedVideo(entities.HikariEntity, marshaller.Deserializable): """Represents an embed video. Note @@ -191,7 +191,7 @@ class EmbedVideo(entities.HikariEntity, entities.Deserializable): @marshaller.marshallable() @attr.s(slots=True, kw_only=True) -class EmbedProvider(entities.HikariEntity, entities.Deserializable): +class EmbedProvider(entities.HikariEntity, marshaller.Deserializable): """Represents an embed provider. Note @@ -213,7 +213,7 @@ class EmbedProvider(entities.HikariEntity, entities.Deserializable): @marshaller.marshallable() @attr.s(slots=True, kw_only=True) -class EmbedAuthor(entities.HikariEntity, entities.Deserializable, entities.Serializable): +class EmbedAuthor(entities.HikariEntity, marshaller.Deserializable, marshaller.Serializable): """Represents an embed author.""" #: The name of the author. @@ -249,7 +249,7 @@ class EmbedAuthor(entities.HikariEntity, entities.Deserializable, entities.Seria @marshaller.marshallable() @attr.s(slots=True, kw_only=True) -class EmbedField(entities.HikariEntity, entities.Deserializable, entities.Serializable): +class EmbedField(entities.HikariEntity, marshaller.Deserializable, marshaller.Serializable): """Represents a field in a embed.""" #: The name of the field. @@ -272,7 +272,7 @@ class EmbedField(entities.HikariEntity, entities.Deserializable, entities.Serial @marshaller.marshallable() @attr.s(slots=True, kw_only=True) -class Embed(entities.HikariEntity, entities.Deserializable, entities.Serializable): +class Embed(entities.HikariEntity, marshaller.Deserializable, marshaller.Serializable): """Represents an embed.""" #: The title of the embed. diff --git a/hikari/emojis.py b/hikari/emojis.py index 66dde55a39..6e5ff76f2b 100644 --- a/hikari/emojis.py +++ b/hikari/emojis.py @@ -31,7 +31,7 @@ @marshaller.marshallable() @attr.s(slots=True, kw_only=True) -class Emoji(entities.HikariEntity, entities.Deserializable): +class Emoji(entities.HikariEntity, marshaller.Deserializable): """Base class for all emojis.""" diff --git a/hikari/entities.py b/hikari/entities.py index c748527f87..ac89c36211 100644 --- a/hikari/entities.py +++ b/hikari/entities.py @@ -17,7 +17,7 @@ # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . """Datastructure bases.""" -__all__ = ["HikariEntity", "Serializable", "Deserializable", "RawEntityT", "UNSET"] +__all__ = ["HikariEntity", "RawEntityT"] import abc import typing @@ -25,61 +25,13 @@ import attr from hikari.internal import marshaller -from hikari.internal import meta RawEntityT = typing.Union[ None, bool, int, float, str, bytes, typing.Sequence[typing.Any], typing.Mapping[str, typing.Any] ] -T_contra = typing.TypeVar("T_contra", contravariant=True) -T_co = typing.TypeVar("T_co", covariant=True) - - -class Unset(metaclass=meta.SingletonMeta): - """A singleton value that represents an unset field.""" - - def __bool__(self): - return False - - def __repr__(self): - return type(self).__name__.upper() - - __str__ = __repr__ - - -#: A variable used for certain update events where a field being unset will -#: mean that it's not being acted on, mostly just seen attached to event models. -UNSET = Unset() - @marshaller.marshallable() @attr.s(slots=True, kw_only=True) class HikariEntity(metaclass=abc.ABCMeta): """The base for any entity used in this API.""" - - -class Deserializable: - """Mixin that enables the class to be deserialized from a raw entity.""" - - __slots__ = () - - @classmethod - def deserialize(cls: typing.Type[T_contra], payload: RawEntityT) -> T_contra: - """Deserialize the given payload into the object. - - Parameters - ---------- - payload - The payload to deserialize into the object. - """ - return marshaller.HIKARI_ENTITY_MARSHALLER.deserialize(payload, cls) - - -class Serializable: - """Mixin that enables an instance of the class to be serialized.""" - - __slots__ = () - - def serialize(self: T_co) -> RawEntityT: - """Serialize this instance into a naive value.""" - return marshaller.HIKARI_ENTITY_MARSHALLER.serialize(self) diff --git a/hikari/events.py b/hikari/events.py index 53293cae86..5ed540da7f 100644 --- a/hikari/events.py +++ b/hikari/events.py @@ -79,6 +79,7 @@ from hikari import messages from hikari import oauth2 from hikari import snowflakes +from hikari import unset from hikari import users from hikari import voices from hikari.clients import shard_clients @@ -141,7 +142,7 @@ class StoppedEvent(HikariEvent): @attr.s(slots=True, kw_only=True, auto_attribs=True) -class ConnectedEvent(HikariEvent, entities.Deserializable): +class ConnectedEvent(HikariEvent, marshaller.Deserializable): """Event invoked each time a shard connects.""" #: The shard that connected. @@ -151,7 +152,7 @@ class ConnectedEvent(HikariEvent, entities.Deserializable): @attr.s(slots=True, kw_only=True, auto_attribs=True) -class DisconnectedEvent(HikariEvent, entities.Deserializable): +class DisconnectedEvent(HikariEvent, marshaller.Deserializable): """Event invoked each time a shard disconnects.""" #: The shard that disconnected. @@ -172,7 +173,7 @@ class ResumedEvent(HikariEvent): @marshaller.marshallable() @attr.s(slots=True, kw_only=True) -class ReadyEvent(HikariEvent, entities.Deserializable): +class ReadyEvent(HikariEvent, marshaller.Deserializable): """Represents the gateway Ready event. This is received only when IDENTIFYing with the gateway. @@ -228,7 +229,7 @@ def shard_count(self) -> typing.Optional[int]: @marshaller.marshallable() @attr.s(slots=True, kw_only=True) -class BaseChannelEvent(HikariEvent, snowflakes.UniqueEntity, entities.Deserializable): +class BaseChannelEvent(HikariEvent, snowflakes.UniqueEntity, marshaller.Deserializable): """A base object that Channel events will inherit from.""" #: The channel's type. @@ -373,7 +374,7 @@ class ChannelDeleteEvent(BaseChannelEvent): @marshaller.marshallable() @attr.s(slots=True, kw_only=True) -class ChannelPinUpdateEvent(HikariEvent, entities.Deserializable): +class ChannelPinUpdateEvent(HikariEvent, marshaller.Deserializable): """Used to represent the Channel Pins Update gateway event. Sent when a message is pinned or unpinned in a channel but not @@ -404,7 +405,7 @@ class ChannelPinUpdateEvent(HikariEvent, entities.Deserializable): @marshaller.marshallable() @attr.s(slots=True, kw_only=True) -class GuildCreateEvent(HikariEvent, entities.Deserializable): +class GuildCreateEvent(HikariEvent, marshaller.Deserializable): """Used to represent Guild Create gateway events. Will be received when the bot joins a guild, and when a guild becomes @@ -414,13 +415,13 @@ class GuildCreateEvent(HikariEvent, entities.Deserializable): @marshaller.marshallable() @attr.s(slots=True, kw_only=True) -class GuildUpdateEvent(HikariEvent, entities.Deserializable): +class GuildUpdateEvent(HikariEvent, marshaller.Deserializable): """Used to represent Guild Update gateway events.""" @marshaller.marshallable() @attr.s(slots=True, kw_only=True) -class GuildLeaveEvent(HikariEvent, snowflakes.UniqueEntity, entities.Deserializable): +class GuildLeaveEvent(HikariEvent, snowflakes.UniqueEntity, marshaller.Deserializable): """Fired when the current user leaves the guild or is kicked/banned from it. Notes @@ -431,7 +432,7 @@ class GuildLeaveEvent(HikariEvent, snowflakes.UniqueEntity, entities.Deserializa @marshaller.marshallable() @attr.s(slots=True, kw_only=True) -class GuildUnavailableEvent(HikariEvent, snowflakes.UniqueEntity, entities.Deserializable): +class GuildUnavailableEvent(HikariEvent, snowflakes.UniqueEntity, marshaller.Deserializable): """Fired when a guild becomes temporarily unavailable due to an outage. Notes @@ -442,7 +443,7 @@ class GuildUnavailableEvent(HikariEvent, snowflakes.UniqueEntity, entities.Deser @marshaller.marshallable() @attr.s(slots=True, kw_only=True) -class BaseGuildBanEvent(HikariEvent, entities.Deserializable): +class BaseGuildBanEvent(HikariEvent, marshaller.Deserializable): """A base object that guild ban events will inherit from.""" #: The ID of the guild this ban is in. @@ -470,7 +471,7 @@ class GuildBanRemoveEvent(BaseGuildBanEvent): @marshaller.marshallable() @attr.s(slots=True, kw_only=True) -class GuildEmojisUpdateEvent(HikariEvent, entities.Deserializable): +class GuildEmojisUpdateEvent(HikariEvent, marshaller.Deserializable): """Represents a Guild Emoji Update gateway event.""" #: The ID of the guild this emoji was updated in. @@ -488,7 +489,7 @@ class GuildEmojisUpdateEvent(HikariEvent, entities.Deserializable): @marshaller.marshallable() @attr.s(slots=True, kw_only=True) -class GuildIntegrationsUpdateEvent(HikariEvent, entities.Deserializable): +class GuildIntegrationsUpdateEvent(HikariEvent, marshaller.Deserializable): """Used to represent Guild Integration Update gateway events.""" #: The ID of the guild the integration was updated in. @@ -510,7 +511,7 @@ class GuildMemberAddEvent(HikariEvent, guilds.GuildMember): @marshaller.marshallable() @attr.s(slots=True, kw_only=True) -class GuildMemberUpdateEvent(HikariEvent, entities.Deserializable): +class GuildMemberUpdateEvent(HikariEvent, marshaller.Deserializable): """Used to represent a Guild Member Update gateway event. Sent when a guild member or their inner user object is updated. @@ -534,25 +535,25 @@ class GuildMemberUpdateEvent(HikariEvent, entities.Deserializable): user: users.User = marshaller.attrib(deserializer=users.User.deserialize) #: This member's nickname. When set to :obj:`~None`, this has been removed - #: and when set to :obj:`~hikari.entities.UNSET` this hasn't been acted on. + #: and when set to :obj:`~hikari.unset.UNSET` this hasn't been acted on. #: - #: :type: :obj:`~typing.Union` [ :obj:`~str`, :obj:`~hikari.entities.UNSET` ], optional - nickname: typing.Union[None, str, entities.Unset] = marshaller.attrib( - raw_name="nick", deserializer=str, if_none=None, if_undefined=entities.Unset, default=entities.UNSET + #: :type: :obj:`~typing.Union` [ :obj:`~str`, :obj:`~hikari.unset.UNSET` ], optional + nickname: typing.Union[None, str, unset.Unset] = marshaller.attrib( + raw_name="nick", deserializer=str, if_none=None, if_undefined=unset.Unset, default=unset.UNSET ) #: The datetime of when this member started "boosting" this guild. #: Will be :obj:`~None` if they aren't boosting. #: - #: :type: :obj:`~typing.Union` [ :obj:`~datetime.datetime`, :obj:`~hikari.entities.UNSET` ], optional - premium_since: typing.Union[None, datetime.datetime, entities.Unset] = marshaller.attrib( - deserializer=conversions.parse_iso_8601_ts, if_none=None, if_undefined=entities.Unset, default=entities.UNSET + #: :type: :obj:`~typing.Union` [ :obj:`~datetime.datetime`, :obj:`~hikari.unset.UNSET` ], optional + premium_since: typing.Union[None, datetime.datetime, unset.Unset] = marshaller.attrib( + deserializer=conversions.parse_iso_8601_ts, if_none=None, if_undefined=unset.Unset, default=unset.UNSET ) @marshaller.marshallable() @attr.s(slots=True, kw_only=True) -class GuildMemberRemoveEvent(HikariEvent, entities.Deserializable): +class GuildMemberRemoveEvent(HikariEvent, marshaller.Deserializable): """Used to represent Guild Member Remove gateway events. Sent when a member is kicked, banned or leaves a guild. @@ -571,7 +572,7 @@ class GuildMemberRemoveEvent(HikariEvent, entities.Deserializable): @marshaller.marshallable() @attr.s(slots=True, kw_only=True) -class GuildRoleCreateEvent(HikariEvent, entities.Deserializable): +class GuildRoleCreateEvent(HikariEvent, marshaller.Deserializable): """Used to represent a Guild Role Create gateway event.""" #: The ID of the guild where this role was created. @@ -587,7 +588,7 @@ class GuildRoleCreateEvent(HikariEvent, entities.Deserializable): @marshaller.marshallable() @attr.s(slots=True, kw_only=True) -class GuildRoleUpdateEvent(HikariEvent, entities.Deserializable): +class GuildRoleUpdateEvent(HikariEvent, marshaller.Deserializable): """Used to represent a Guild Role Create gateway event.""" #: The ID of the guild where this role was updated. @@ -603,7 +604,7 @@ class GuildRoleUpdateEvent(HikariEvent, entities.Deserializable): @marshaller.marshallable() @attr.s(slots=True, kw_only=True) -class GuildRoleDeleteEvent(HikariEvent, entities.Deserializable): +class GuildRoleDeleteEvent(HikariEvent, marshaller.Deserializable): """Represents a gateway Guild Role Delete Event.""" #: The ID of the guild where this role is being deleted. @@ -619,7 +620,7 @@ class GuildRoleDeleteEvent(HikariEvent, entities.Deserializable): @marshaller.marshallable() @attr.s(slots=True, kw_only=True) -class InviteCreateEvent(HikariEvent, entities.Deserializable): +class InviteCreateEvent(HikariEvent, marshaller.Deserializable): """Represents a gateway Invite Create event.""" #: The ID of the channel this invite targets. @@ -693,7 +694,7 @@ class InviteCreateEvent(HikariEvent, entities.Deserializable): @marshaller.marshallable() @attr.s(slots=True, kw_only=True) -class InviteDeleteEvent(HikariEvent, entities.Deserializable): +class InviteDeleteEvent(HikariEvent, marshaller.Deserializable): """Used to represent Invite Delete gateway events. Sent when an invite is deleted for a channel we can access. @@ -727,13 +728,13 @@ class MessageCreateEvent(HikariEvent, messages.Message): # This is an arbitrarily partial version of `messages.Message` @marshaller.marshallable() @attr.s(slots=True, kw_only=True) -class MessageUpdateEvent(HikariEvent, snowflakes.UniqueEntity, entities.Deserializable): +class MessageUpdateEvent(HikariEvent, snowflakes.UniqueEntity, marshaller.Deserializable): """Represents Message Update gateway events. Note ---- - All fields on this model except :attr:`channel_id` and ``id`` may be - set to :obj:`~hikari.entities.UNSET` (a singleton defined in + All fields on this model except :attr:`channel_id` and :obj:`~`HikariEvent.id`` may be + set to :obj:`~hikari.unset.UNSET` (a singleton defined in ``hikari.entities``) if we have not received information about their state from Discord alongside field nullability. """ @@ -745,179 +746,179 @@ class MessageUpdateEvent(HikariEvent, snowflakes.UniqueEntity, entities.Deserial #: The ID of the guild that the message was sent in. #: - #: :type: :obj:`~typing.Union` [ :obj:`~hikari.snowflakes.Snowflake`, :obj:`~hikari.entities.UNSET` ] - guild_id: typing.Union[snowflakes.Snowflake, entities.Unset] = marshaller.attrib( - deserializer=snowflakes.Snowflake.deserialize, if_undefined=entities.Unset, default=entities.UNSET + #: :type: :obj:`~typing.Union` [ :obj:`~hikari.snowflakes.Snowflake`, :obj:`~hikari.unset.UNSET` ] + guild_id: typing.Union[snowflakes.Snowflake, unset.Unset] = marshaller.attrib( + deserializer=snowflakes.Snowflake.deserialize, if_undefined=unset.Unset, default=unset.UNSET ) #: The author of this message. #: - #: :type: :obj:`~typing.Union` [ :obj:`~hikari.users.User`, :obj:`~hikari.entities.UNSET` ] - author: typing.Union[users.User, entities.Unset] = marshaller.attrib( - deserializer=users.User.deserialize, if_undefined=entities.Unset, default=entities.UNSET + #: :type: :obj:`~typing.Union` [ :obj:`~hikari.users.User`, :obj:`~hikari.unset.UNSET` ] + author: typing.Union[users.User, unset.Unset] = marshaller.attrib( + deserializer=users.User.deserialize, if_undefined=unset.Unset, default=unset.UNSET ) #: The member properties for the message's author. #: - #: :type: :obj:`~typing.Union` [ :obj:`~hikari.guilds.GuildMember`, :obj:`~hikari.entities.UNSET` ] - member: typing.Union[guilds.GuildMember, entities.Unset] = marshaller.attrib( - deserializer=guilds.GuildMember.deserialize, if_undefined=entities.Unset, default=entities.UNSET + #: :type: :obj:`~typing.Union` [ :obj:`~hikari.guilds.GuildMember`, :obj:`~hikari.unset.UNSET` ] + member: typing.Union[guilds.GuildMember, unset.Unset] = marshaller.attrib( + deserializer=guilds.GuildMember.deserialize, if_undefined=unset.Unset, default=unset.UNSET ) #: The content of the message. #: - #: :type: :obj:`~typing.Union` [ :obj:`~str`, :obj:`~hikari.entities.UNSET` ] - content: typing.Union[str, entities.Unset] = marshaller.attrib( - deserializer=str, if_undefined=entities.Unset, default=entities.UNSET + #: :type: :obj:`~typing.Union` [ :obj:`~str`, :obj:`~hikari.unset.UNSET` ] + content: typing.Union[str, unset.Unset] = marshaller.attrib( + deserializer=str, if_undefined=unset.Unset, default=unset.UNSET ) #: The timestamp that the message was sent at. #: - #: :type: :obj:`~typing.Union` [ :obj:`~datetime.datetime`, :obj:`~hikari.entities.UNSET` ] - timestamp: typing.Union[datetime.datetime, entities.Unset] = marshaller.attrib( - deserializer=conversions.parse_iso_8601_ts, if_undefined=entities.Unset, default=entities.UNSET + #: :type: :obj:`~typing.Union` [ :obj:`~datetime.datetime`, :obj:`~hikari.unset.UNSET` ] + timestamp: typing.Union[datetime.datetime, unset.Unset] = marshaller.attrib( + deserializer=conversions.parse_iso_8601_ts, if_undefined=unset.Unset, default=unset.UNSET ) #: The timestamp that the message was last edited at, or :obj:`~None` if #: it wasn't ever edited. #: - #: :type: :obj:`~typing.Union` [ :obj:`~datetime.datetime`, :obj:`~hikari.entities.UNSET` ], optional - edited_timestamp: typing.Union[datetime.datetime, entities.Unset, None] = marshaller.attrib( - deserializer=conversions.parse_iso_8601_ts, if_none=None, if_undefined=entities.Unset, default=entities.UNSET + #: :type: :obj:`~typing.Union` [ :obj:`~datetime.datetime`, :obj:`~hikari.unset.UNSET` ], optional + edited_timestamp: typing.Union[datetime.datetime, unset.Unset, None] = marshaller.attrib( + deserializer=conversions.parse_iso_8601_ts, if_none=None, if_undefined=unset.Unset, default=unset.UNSET ) #: Whether the message is a TTS message. #: - #: :type: :obj:`~typing.Union` [ :obj:`~bool`, :obj:`~hikari.entities.UNSET` ] - is_tts: typing.Union[bool, entities.Unset] = marshaller.attrib( - raw_name="tts", deserializer=bool, if_undefined=entities.Unset, default=entities.UNSET + #: :type: :obj:`~typing.Union` [ :obj:`~bool`, :obj:`~hikari.unset.UNSET` ] + is_tts: typing.Union[bool, unset.Unset] = marshaller.attrib( + raw_name="tts", deserializer=bool, if_undefined=unset.Unset, default=unset.UNSET ) #: Whether the message mentions ``@everyone`` or ``@here``. #: - #: :type: :obj:`~typing.Union` [ :obj:`~bool`, :obj:`~hikari.entities.UNSET` ] - is_mentioning_everyone: typing.Union[bool, entities.Unset] = marshaller.attrib( - raw_name="mention_everyone", deserializer=bool, if_undefined=entities.Unset, default=entities.UNSET + #: :type: :obj:`~typing.Union` [ :obj:`~bool`, :obj:`~hikari.unset.UNSET` ] + is_mentioning_everyone: typing.Union[bool, unset.Unset] = marshaller.attrib( + raw_name="mention_everyone", deserializer=bool, if_undefined=unset.Unset, default=unset.UNSET ) #: The users the message mentions. #: - #: :type: :obj:`~typing.Union` [ :obj:`~typing.Set` [ :obj:`~hikari.snowflakes.Snowflake` ], :obj:`~hikari.entities.UNSET` ] - user_mentions: typing.Union[typing.Set[snowflakes.Snowflake], entities.Unset] = marshaller.attrib( + #: :type: :obj:`~typing.Union` [ :obj:`~typing.Set` [ :obj:`~hikari.snowflakes.Snowflake` ], :obj:`~hikari.unset.UNSET` ] + user_mentions: typing.Union[typing.Set[snowflakes.Snowflake], unset.Unset] = marshaller.attrib( raw_name="mentions", deserializer=lambda user_mentions: {snowflakes.Snowflake.deserialize(u["id"]) for u in user_mentions}, - if_undefined=entities.Unset, - default=entities.UNSET, + if_undefined=unset.Unset, + default=unset.UNSET, ) #: The roles the message mentions. #: - #: :type: :obj:`~typing.Union` [ :obj:`~typing.Set` [ :obj:`~hikari.snowflakes.Snowflake` ], :obj:`~hikari.entities.UNSET` ] - role_mentions: typing.Union[typing.Set[snowflakes.Snowflake], entities.Unset] = marshaller.attrib( + #: :type: :obj:`~typing.Union` [ :obj:`~typing.Set` [ :obj:`~hikari.snowflakes.Snowflake` ], :obj:`~hikari.unset.UNSET` ] + role_mentions: typing.Union[typing.Set[snowflakes.Snowflake], unset.Unset] = marshaller.attrib( raw_name="mention_roles", deserializer=lambda role_mentions: {snowflakes.Snowflake.deserialize(r) for r in role_mentions}, - if_undefined=entities.Unset, - default=entities.UNSET, + if_undefined=unset.Unset, + default=unset.UNSET, ) #: The channels the message mentions. #: - #: :type: :obj:`~typing.Union` [ :obj:`~typing.Set` [ :obj:`~hikari.snowflakes.Snowflake` ], :obj:`~hikari.entities.UNSET` ] - channel_mentions: typing.Union[typing.Set[snowflakes.Snowflake], entities.Unset] = marshaller.attrib( + #: :type: :obj:`~typing.Union` [ :obj:`~typing.Set` [ :obj:`~hikari.snowflakes.Snowflake` ], :obj:`~hikari.unset.UNSET` ] + channel_mentions: typing.Union[typing.Set[snowflakes.Snowflake], unset.Unset] = marshaller.attrib( raw_name="mention_channels", deserializer=lambda channel_mentions: {snowflakes.Snowflake.deserialize(c["id"]) for c in channel_mentions}, - if_undefined=entities.Unset, - default=entities.UNSET, + if_undefined=unset.Unset, + default=unset.UNSET, ) #: The message attachments. #: - #: :type: :obj:`~typing.Union` [ :obj:`~typing.Sequence` [ :obj:`~hikari.messages.Attachment` ], :obj:`~hikari.entities.UNSET` ] - attachments: typing.Union[typing.Sequence[messages.Attachment], entities.Unset] = marshaller.attrib( + #: :type: :obj:`~typing.Union` [ :obj:`~typing.Sequence` [ :obj:`~hikari.messages.Attachment` ], :obj:`~hikari.unset.UNSET` ] + attachments: typing.Union[typing.Sequence[messages.Attachment], unset.Unset] = marshaller.attrib( deserializer=lambda attachments: [messages.Attachment.deserialize(a) for a in attachments], - if_undefined=entities.Unset, - default=entities.UNSET, + if_undefined=unset.Unset, + default=unset.UNSET, ) #: The message's embeds. #: - #: :type: :obj:`~typing.Union` [ :obj:`~typing.Sequence` [ :obj:`~hikari.embeds.Embed` ], :obj:`~hikari.entities.UNSET` ] - embeds: typing.Union[typing.Sequence[_embeds.Embed], entities.Unset] = marshaller.attrib( + #: :type: :obj:`~typing.Union` [ :obj:`~typing.Sequence` [ :obj:`~hikari.embeds.Embed` ], :obj:`~hikari.unset.UNSET` ] + embeds: typing.Union[typing.Sequence[_embeds.Embed], unset.Unset] = marshaller.attrib( deserializer=lambda embed_objs: [_embeds.Embed.deserialize(e) for e in embed_objs], - if_undefined=entities.Unset, - default=entities.UNSET, + if_undefined=unset.Unset, + default=unset.UNSET, ) #: The message's reactions. #: - #: :type: :obj:`~typing.Union` [ :obj:`~typing.Sequence` [ :obj:`~hikari.messages.Reaction` ], :obj:`~hikari.entities.UNSET` ] - reactions: typing.Union[typing.Sequence[messages.Reaction], entities.Unset] = marshaller.attrib( + #: :type: :obj:`~typing.Union` [ :obj:`~typing.Sequence` [ :obj:`~hikari.messages.Reaction` ], :obj:`~hikari.unset.UNSET` ] + reactions: typing.Union[typing.Sequence[messages.Reaction], unset.Unset] = marshaller.attrib( deserializer=lambda reactions: [messages.Reaction.deserialize(r) for r in reactions], - if_undefined=entities.Unset, - default=entities.UNSET, + if_undefined=unset.Unset, + default=unset.UNSET, ) #: Whether the message is pinned. #: - #: :type: :obj:`~typing.Union` [ :obj:`~bool`, :obj:`~hikari.entities.UNSET` ] - is_pinned: typing.Union[bool, entities.Unset] = marshaller.attrib( - raw_name="pinned", deserializer=bool, if_undefined=entities.Unset, default=entities.UNSET + #: :type: :obj:`~typing.Union` [ :obj:`~bool`, :obj:`~hikari.unset.UNSET` ] + is_pinned: typing.Union[bool, unset.Unset] = marshaller.attrib( + raw_name="pinned", deserializer=bool, if_undefined=unset.Unset, default=unset.UNSET ) #: If the message was generated by a webhook, the webhook's id. #: - #: :type: :obj:`~typing.Union` [ :obj:`~hikari.snowflakes.Snowflake`, :obj:`~hikari.entities.UNSET` ] - webhook_id: typing.Union[snowflakes.Snowflake, entities.Unset] = marshaller.attrib( - deserializer=snowflakes.Snowflake.deserialize, if_undefined=entities.Unset, default=entities.UNSET + #: :type: :obj:`~typing.Union` [ :obj:`~hikari.snowflakes.Snowflake`, :obj:`~hikari.unset.UNSET` ] + webhook_id: typing.Union[snowflakes.Snowflake, unset.Unset] = marshaller.attrib( + deserializer=snowflakes.Snowflake.deserialize, if_undefined=unset.Unset, default=unset.UNSET ) #: The message's type. #: - #: :type: :obj:`~typing.Union` [ :obj:`~hikari.messages.MessageType`, :obj:`~hikari.entities.UNSET` ] - type: typing.Union[messages.MessageType, entities.Unset] = marshaller.attrib( - deserializer=messages.MessageType, if_undefined=entities.Unset, default=entities.UNSET + #: :type: :obj:`~typing.Union` [ :obj:`~hikari.messages.MessageType`, :obj:`~hikari.unset.UNSET` ] + type: typing.Union[messages.MessageType, unset.Unset] = marshaller.attrib( + deserializer=messages.MessageType, if_undefined=unset.Unset, default=unset.UNSET ) #: The message's activity. #: - #: :type: :obj:`~typing.Union` [ :obj:`~hikari.messages.MessageActivity`, :obj:`~hikari.entities.UNSET` ] - activity: typing.Union[messages.MessageActivity, entities.Unset] = marshaller.attrib( - deserializer=messages.MessageActivity.deserialize, if_undefined=entities.Unset, default=entities.UNSET + #: :type: :obj:`~typing.Union` [ :obj:`~hikari.messages.MessageActivity`, :obj:`~hikari.unset.UNSET` ] + activity: typing.Union[messages.MessageActivity, unset.Unset] = marshaller.attrib( + deserializer=messages.MessageActivity.deserialize, if_undefined=unset.Unset, default=unset.UNSET ) #: The message's application. #: - #: :type: :obj:`~typing.Union` [ :obj:`~hikari.oauth2.Application`, :obj:`~hikari.entities.UNSET` ] + #: :type: :obj:`~typing.Union` [ :obj:`~hikari.oauth2.Application`, :obj:`~hikari.unset.UNSET` ] application: typing.Optional[oauth2.Application] = marshaller.attrib( - deserializer=oauth2.Application.deserialize, if_undefined=entities.Unset, default=entities.UNSET + deserializer=oauth2.Application.deserialize, if_undefined=unset.Unset, default=unset.UNSET ) #: The message's crossposted reference data. #: - #: :type: :obj:`~typing.Union` [ :obj:`~hikari.messages.MessageCrosspost`, :obj:`~hikari.entities.UNSET` ] - message_reference: typing.Union[messages.MessageCrosspost, entities.Unset] = marshaller.attrib( - deserializer=messages.MessageCrosspost.deserialize, if_undefined=entities.Unset, default=entities.UNSET + #: :type: :obj:`~typing.Union` [ :obj:`~hikari.messages.MessageCrosspost`, :obj:`~hikari.unset.UNSET` ] + message_reference: typing.Union[messages.MessageCrosspost, unset.Unset] = marshaller.attrib( + deserializer=messages.MessageCrosspost.deserialize, if_undefined=unset.Unset, default=unset.UNSET ) #: The message's flags. #: - #: :type: :obj:`~typing.Union` [ :obj:`~hikari.messages.MessageFlag`, :obj:`~hikari.entities.UNSET` ] - flags: typing.Union[messages.MessageFlag, entities.Unset] = marshaller.attrib( - deserializer=messages.MessageFlag, if_undefined=entities.Unset, default=entities.UNSET + #: :type: :obj:`~typing.Union` [ :obj:`~hikari.messages.MessageFlag`, :obj:`~hikari.unset.UNSET` ] + flags: typing.Union[messages.MessageFlag, unset.Unset] = marshaller.attrib( + deserializer=messages.MessageFlag, if_undefined=unset.Unset, default=unset.UNSET ) #: The message nonce. This is a string used for validating #: a message was sent. #: - #: :type: :obj:`~typing.Union` [ :obj:`~hikari.messages.MessageFlag`, :obj:`~hikari.entities.UNSET` ] - nonce: typing.Union[str, entities.Unset] = marshaller.attrib( - deserializer=str, if_undefined=entities.Unset, default=entities.UNSET + #: :type: :obj:`~typing.Union` [ :obj:`~hikari.messages.MessageFlag`, :obj:`~hikari.unset.UNSET` ] + nonce: typing.Union[str, unset.Unset] = marshaller.attrib( + deserializer=str, if_undefined=unset.Unset, default=unset.UNSET ) @marshaller.marshallable() @attr.s(slots=True, kw_only=True) -class MessageDeleteEvent(HikariEvent, entities.Deserializable): +class MessageDeleteEvent(HikariEvent, marshaller.Deserializable): """Used to represent Message Delete gateway events. Sent when a message is deleted in a channel we have access to. @@ -943,7 +944,7 @@ class MessageDeleteEvent(HikariEvent, entities.Deserializable): @marshaller.marshallable() @attr.s(slots=True, kw_only=True) -class MessageDeleteBulkEvent(HikariEvent, entities.Deserializable): +class MessageDeleteBulkEvent(HikariEvent, marshaller.Deserializable): """Used to represent Message Bulk Delete gateway events. Sent when multiple messages are deleted in a channel at once. @@ -972,7 +973,7 @@ class MessageDeleteBulkEvent(HikariEvent, entities.Deserializable): @marshaller.marshallable() @attr.s(slots=True, kw_only=True) -class MessageReactionAddEvent(HikariEvent, entities.Deserializable): +class MessageReactionAddEvent(HikariEvent, marshaller.Deserializable): """Used to represent Message Reaction Add gateway events.""" #: The ID of the user adding the reaction. @@ -1016,7 +1017,7 @@ class MessageReactionAddEvent(HikariEvent, entities.Deserializable): @marshaller.marshallable() @attr.s(slots=True, kw_only=True) -class MessageReactionRemoveEvent(HikariEvent, entities.Deserializable): +class MessageReactionRemoveEvent(HikariEvent, marshaller.Deserializable): """Used to represent Message Reaction Remove gateway events.""" #: The ID of the user who is removing their reaction. @@ -1052,7 +1053,7 @@ class MessageReactionRemoveEvent(HikariEvent, entities.Deserializable): @marshaller.marshallable() @attr.s(slots=True, kw_only=True) -class MessageReactionRemoveAllEvent(HikariEvent, entities.Deserializable): +class MessageReactionRemoveAllEvent(HikariEvent, marshaller.Deserializable): """Used to represent Message Reaction Remove All gateway events. Sent when all the reactions are removed from a message, regardless of emoji. @@ -1078,7 +1079,7 @@ class MessageReactionRemoveAllEvent(HikariEvent, entities.Deserializable): @marshaller.marshallable() @attr.s(slots=True, kw_only=True) -class MessageReactionRemoveEmojiEvent(HikariEvent, entities.Deserializable): +class MessageReactionRemoveEmojiEvent(HikariEvent, marshaller.Deserializable): """Represents Message Reaction Remove Emoji events. Sent when all the reactions for a single emoji are removed from a message. @@ -1120,7 +1121,7 @@ class PresenceUpdateEvent(HikariEvent, guilds.GuildMemberPresence): @marshaller.marshallable() @attr.s(slots=True, kw_only=True) -class TypingStartEvent(HikariEvent, entities.Deserializable): +class TypingStartEvent(HikariEvent, marshaller.Deserializable): """Used to represent typing start gateway events. Received when a user or bot starts "typing" in a channel. @@ -1180,7 +1181,7 @@ class VoiceStateUpdateEvent(HikariEvent, voices.VoiceState): @marshaller.marshallable() @attr.s(slots=True, kw_only=True) -class VoiceServerUpdateEvent(HikariEvent, entities.Deserializable): +class VoiceServerUpdateEvent(HikariEvent, marshaller.Deserializable): """Used to represent voice server update gateway events. Sent when initially connecting to voice and when the current voice instance @@ -1205,7 +1206,7 @@ class VoiceServerUpdateEvent(HikariEvent, entities.Deserializable): @marshaller.marshallable() @attr.s(slots=True, kw_only=True) -class WebhookUpdateEvent(HikariEvent, entities.Deserializable): +class WebhookUpdateEvent(HikariEvent, marshaller.Deserializable): """Used to represent webhook update gateway events. Sent when a webhook is updated, created or deleted in a guild. diff --git a/hikari/gateway_entities.py b/hikari/gateway_entities.py index f4fd38880e..8a59d28bfe 100644 --- a/hikari/gateway_entities.py +++ b/hikari/gateway_entities.py @@ -31,7 +31,7 @@ @marshaller.marshallable() @attr.s(slots=True, kw_only=True) -class SessionStartLimit(entities.HikariEntity, entities.Deserializable): +class SessionStartLimit(entities.HikariEntity, marshaller.Deserializable): """Used to represent information about the current session start limits.""" #: The total number of session starts the current bot is allowed. @@ -55,7 +55,7 @@ class SessionStartLimit(entities.HikariEntity, entities.Deserializable): @marshaller.marshallable() @attr.s(slots=True, kw_only=True) -class GatewayBot(entities.HikariEntity, entities.Deserializable): +class GatewayBot(entities.HikariEntity, marshaller.Deserializable): """Used to represent gateway information for the connected bot.""" #: The WSS URL that can be used for connecting to the gateway. @@ -76,7 +76,7 @@ class GatewayBot(entities.HikariEntity, entities.Deserializable): @marshaller.marshallable() @attr.s(slots=True, kw_only=True) -class GatewayActivity(entities.Deserializable, entities.Serializable): +class GatewayActivity(marshaller.Deserializable, marshaller.Serializable): """An activity that the bot can set for one or more shards. This will show the activity as the bot's presence. diff --git a/hikari/guilds.py b/hikari/guilds.py index bdbb0d17d9..74f5930025 100644 --- a/hikari/guilds.py +++ b/hikari/guilds.py @@ -55,10 +55,11 @@ from hikari import entities from hikari import permissions as _permissions from hikari import snowflakes +from hikari import unset from hikari import users -from hikari.internal import urls from hikari.internal import conversions from hikari.internal import marshaller +from hikari.internal import urls @enum.unique @@ -198,7 +199,7 @@ class GuildVerificationLevel(enum.IntEnum): @marshaller.marshallable() @attr.s(slots=True, kw_only=True) -class GuildEmbed(entities.HikariEntity, entities.Deserializable): +class GuildEmbed(entities.HikariEntity, marshaller.Deserializable): """Represents a guild embed.""" #: The ID of the channel the invite for this embed targets, if enabled @@ -216,7 +217,7 @@ class GuildEmbed(entities.HikariEntity, entities.Deserializable): @marshaller.marshallable() @attr.s(slots=True, kw_only=True) -class GuildMember(entities.HikariEntity, entities.Deserializable): +class GuildMember(entities.HikariEntity, marshaller.Deserializable): """Used to represent a guild bound member.""" #: This member's user object, will be :obj:`~None` when attached to Message @@ -267,7 +268,7 @@ class GuildMember(entities.HikariEntity, entities.Deserializable): @marshaller.marshallable() @attr.s(slots=True, kw_only=True) -class PartialGuildRole(snowflakes.UniqueEntity, entities.Deserializable): +class PartialGuildRole(snowflakes.UniqueEntity, marshaller.Deserializable): """Represents a partial guild bound Role object.""" #: The role's name. @@ -278,7 +279,7 @@ class PartialGuildRole(snowflakes.UniqueEntity, entities.Deserializable): @marshaller.marshallable() @attr.s(slots=True, kw_only=True) -class GuildRole(PartialGuildRole, entities.Serializable): +class GuildRole(PartialGuildRole, marshaller.Serializable): """Represents a guild bound Role object.""" #: The colour of this role, will be applied to a member's name in chat @@ -350,7 +351,7 @@ class ActivityType(enum.IntEnum): @marshaller.marshallable() @attr.s(slots=True, kw_only=True) -class ActivityTimestamps(entities.HikariEntity, entities.Deserializable): +class ActivityTimestamps(entities.HikariEntity, marshaller.Deserializable): """The datetimes for the start and/or end of an activity session.""" #: When this activity's session was started, if applicable. @@ -370,7 +371,7 @@ class ActivityTimestamps(entities.HikariEntity, entities.Deserializable): @marshaller.marshallable() @attr.s(slots=True, kw_only=True) -class ActivityParty(entities.HikariEntity, entities.Deserializable): +class ActivityParty(entities.HikariEntity, marshaller.Deserializable): """Used to represent activity groups of users.""" #: The string id of this party instance, if set. @@ -399,7 +400,7 @@ def max_size(self) -> typing.Optional[int]: @marshaller.marshallable() @attr.s(slots=True, kw_only=True) -class ActivityAssets(entities.HikariEntity, entities.Deserializable): +class ActivityAssets(entities.HikariEntity, marshaller.Deserializable): """Used to represent possible assets for an activity.""" #: The ID of the asset's large image, if set. @@ -425,7 +426,7 @@ class ActivityAssets(entities.HikariEntity, entities.Deserializable): @marshaller.marshallable() @attr.s(slots=True, kw_only=True) -class ActivitySecret(entities.HikariEntity, entities.Deserializable): +class ActivitySecret(entities.HikariEntity, marshaller.Deserializable): """The secrets used for interacting with an activity party.""" #: The secret used for joining a party, if applicable. @@ -472,7 +473,7 @@ class ActivityFlag(enum.IntFlag): @marshaller.marshallable() @attr.s(slots=True, kw_only=True) -class PresenceActivity(entities.HikariEntity, entities.Deserializable): +class PresenceActivity(entities.HikariEntity, marshaller.Deserializable): """Represents an activity that will be attached to a member's presence.""" #: The activity's name. @@ -582,7 +583,7 @@ class PresenceStatus(str, enum.Enum): @marshaller.marshallable() @attr.s(slots=True, kw_only=True) -class ClientStatus(entities.HikariEntity, entities.Deserializable): +class ClientStatus(entities.HikariEntity, marshaller.Deserializable): """The client statuses for this member.""" #: The status of the target user's desktop session. @@ -614,66 +615,64 @@ class PresenceUser(users.User): Warnings -------- - Every attribute except ``id`` may be received as :obj:`~hikari.entities.UNSET` + Every attribute except ``id`` may be received as :obj:`~hikari.unset.UNSET` unless it is specifically being modified for this update. """ #: This user's discriminator. #: - #: :type: :obj:`~typing.Union` [ :obj:`~str`, :obj:`~hikari.entities.UNSET` ] - discriminator: typing.Union[str, entities.Unset] = marshaller.attrib( - deserializer=str, if_undefined=entities.Unset, default=entities.UNSET + #: :type: :obj:`~typing.Union` [ :obj:`~str`, :obj:`~hikari.unset.UNSET` ] + discriminator: typing.Union[str, unset.Unset] = marshaller.attrib( + deserializer=str, if_undefined=unset.Unset, default=unset.UNSET ) #: This user's username. #: - #: :type: :obj:`~typing.Union` [ :obj:`~str`, :obj:`~hikari.entities.UNSET` ] - username: typing.Union[str, entities.Unset] = marshaller.attrib( - deserializer=str, if_undefined=entities.Unset, default=entities.UNSET + #: :type: :obj:`~typing.Union` [ :obj:`~str`, :obj:`~hikari.unset.UNSET` ] + username: typing.Union[str, unset.Unset] = marshaller.attrib( + deserializer=str, if_undefined=unset.Unset, default=unset.UNSET ) #: This user's avatar hash, if set. #: - #: :type: :obj:`~typing.Union` [ :obj:`~str`, :obj:`~hikari.entities.UNSET` ], optional - avatar_hash: typing.Union[None, str, entities.Unset] = marshaller.attrib( - raw_name="avatar", deserializer=str, if_none=None, if_undefined=entities.Unset, default=entities.UNSET + #: :type: :obj:`~typing.Union` [ :obj:`~str`, :obj:`~hikari.unset.UNSET` ], optional + avatar_hash: typing.Union[None, str, unset.Unset] = marshaller.attrib( + raw_name="avatar", deserializer=str, if_none=None, if_undefined=unset.Unset, default=unset.UNSET ) #: Whether this user is a bot account. #: - #: :type: :obj:`~typing.Union` [ :obj:`~bool`, :obj:`~hikari.entities.UNSET` ] - is_bot: typing.Union[bool, entities.Unset] = marshaller.attrib( - raw_name="bot", deserializer=bool, if_undefined=entities.Unset, default=entities.UNSET + #: :type: :obj:`~typing.Union` [ :obj:`~bool`, :obj:`~hikari.unset.UNSET` ] + is_bot: typing.Union[bool, unset.Unset] = marshaller.attrib( + raw_name="bot", deserializer=bool, if_undefined=unset.Unset, default=unset.UNSET ) #: Whether this user is a system account. #: - #: :type: :obj:`~typing.Union` [ :obj:`~bool`, :obj:`~hikari.entities.UNSET` ] - is_system: typing.Union[bool, entities.Unset] = marshaller.attrib( - raw_name="system", deserializer=bool, if_undefined=entities.Unset, default=entities.UNSET + #: :type: :obj:`~typing.Union` [ :obj:`~bool`, :obj:`~hikari.unset.UNSET` ] + is_system: typing.Union[bool, unset.Unset] = marshaller.attrib( + raw_name="system", deserializer=bool, if_undefined=unset.Unset, default=unset.UNSET ) #: The public flags for this user. #: - #: :type: :obj:`~typing.Union` [ :obj:`~hikari.users.UserFlag`, :obj:`~hikari.entities.UNSET` ] - flags: typing.Union[users.UserFlag, entities.Unset] = marshaller.attrib( - raw_name="public_flags", deserializer=users.UserFlag, if_undefined=entities.Unset + #: :type: :obj:`~typing.Union` [ :obj:`~hikari.users.UserFlag`, :obj:`~hikari.unset.UNSET` ] + flags: typing.Union[users.UserFlag, unset.Unset] = marshaller.attrib( + raw_name="public_flags", deserializer=users.UserFlag, if_undefined=unset.Unset ) @property - def avatar_url(self) -> typing.Union[str, entities.Unset]: + def avatar_url(self) -> typing.Union[str, unset.Unset]: """URL for this user's avatar if the relevant info is available. Note ---- - This will be :obj:`~hikari.entities.UNSET` if both :attr:`avatar_hash` - and :attr:`discriminator` are :obj:`~hikari.entities.UNSET`. + This will be :obj:`~hikari.unset.UNSET` if both :attr:`avatar_hash` + and :attr:`discriminator` are :obj:`~hikari.unset.UNSET`. """ return self.format_avatar_url() - def format_avatar_url( - self, fmt: typing.Optional[str] = None, size: int = 4096 - ) -> typing.Union[str, entities.Unset]: + def format_avatar_url(self, fmt: typing.Optional[str] = None, size: int = 4096) -> typing.Union[str, unset.Unset]: """Generate the avatar URL for this user's avatar if available. Parameters @@ -690,37 +689,37 @@ def format_avatar_url( Returns ------- - :obj:`~typing.Union` [ :obj:`~str`, :obj:`~hikari.entities.UNSET` ] + :obj:`~typing.Union` [ :obj:`~str`, :obj:`~hikari.unset.UNSET` ] The string URL of the user's custom avatar if either :attr:`avatar_hash` is set or their default avatar if - :attr:`discriminator` is set, else :obj:`~hikari.entities.UNSET`. + :attr:`discriminator` is set, else :obj:`~hikari.unset.UNSET`. Raises ------ :obj:`~ValueError` If ``size`` is not a power of two or not between 16 and 4096. """ - if self.discriminator is entities.UNSET and self.avatar_hash is entities.UNSET: - return entities.UNSET + if self.discriminator is unset.UNSET and self.avatar_hash is unset.UNSET: + return unset.UNSET return super().format_avatar_url(fmt=fmt, size=size) @property - def default_avatar(self) -> typing.Union[int, entities.Unset]: + def default_avatar(self) -> typing.Union[int, unset.Unset]: """Integer representation of this user's default avatar. Note ---- - This will be :obj:`~hikari.entities.UNSET` if :attr:`discriminator` is - :obj:`~hikari.entities.UNSET`. + This will be :obj:`~hikari.unset.UNSET` if :attr:`discriminator` is + :obj:`~hikari.unset.UNSET`. """ - if self.discriminator is not entities.UNSET: + if self.discriminator is not unset.UNSET: return int(self.discriminator) % 5 - return entities.UNSET + return unset.UNSET @marshaller.marshallable() @attr.s(slots=True, kw_only=True) -class GuildMemberPresence(entities.HikariEntity, entities.Deserializable): +class GuildMemberPresence(entities.HikariEntity, marshaller.Deserializable): """Used to represent a guild member's presence.""" #: The object of the user who this presence is for, only `id` is guaranteed @@ -790,7 +789,7 @@ class IntegrationExpireBehaviour(enum.IntEnum): @marshaller.marshallable() @attr.s(slots=True, kw_only=True) -class IntegrationAccount(entities.HikariEntity, entities.Deserializable): +class IntegrationAccount(entities.HikariEntity, marshaller.Deserializable): """An account that's linked to an integration.""" #: The string ID of this (likely) third party account. @@ -806,7 +805,7 @@ class IntegrationAccount(entities.HikariEntity, entities.Deserializable): @marshaller.marshallable() @attr.s(slots=True, kw_only=True) -class PartialGuildIntegration(snowflakes.UniqueEntity, entities.Deserializable): +class PartialGuildIntegration(snowflakes.UniqueEntity, marshaller.Deserializable): """A partial representation of an integration, found in audit logs.""" #: The name of this integration. @@ -827,7 +826,7 @@ class PartialGuildIntegration(snowflakes.UniqueEntity, entities.Deserializable): @marshaller.marshallable() @attr.s(slots=True, kw_only=True) -class GuildIntegration(snowflakes.UniqueEntity, entities.Deserializable): +class GuildIntegration(snowflakes.UniqueEntity, marshaller.Deserializable): """Represents a guild integration object.""" #: Whether this integration is enabled. @@ -882,7 +881,7 @@ class GuildIntegration(snowflakes.UniqueEntity, entities.Deserializable): @marshaller.marshallable() @attr.s(slots=True, kw_only=True) -class GuildMemberBan(entities.HikariEntity, entities.Deserializable): +class GuildMemberBan(entities.HikariEntity, marshaller.Deserializable): """Used to represent guild bans.""" #: The reason for this ban, will be :obj:`~None` if no reason was given. @@ -898,7 +897,7 @@ class GuildMemberBan(entities.HikariEntity, entities.Deserializable): @marshaller.marshallable() @attr.s(slots=True, kw_only=True) -class UnavailableGuild(snowflakes.UniqueEntity, entities.Deserializable): +class UnavailableGuild(snowflakes.UniqueEntity, marshaller.Deserializable): """An unavailable guild object, received during gateway events such as READY. An unavailable guild cannot be interacted with, and most information may @@ -917,7 +916,7 @@ def is_unavailable(self) -> bool: # noqa: D401 @marshaller.marshallable() @attr.s(slots=True, kw_only=True) -class PartialGuild(snowflakes.UniqueEntity, entities.Deserializable): +class PartialGuild(snowflakes.UniqueEntity, marshaller.Deserializable): """Base object for any partial guild objects.""" #: The name of the guild. diff --git a/hikari/internal/__init__.py b/hikari/internal/__init__.py index 770a13acb1..96c9e63a3c 100644 --- a/hikari/internal/__init__.py +++ b/hikari/internal/__init__.py @@ -21,10 +21,10 @@ |internal| """ from hikari.internal import assertions -from hikari.internal import urls from hikari.internal import conversions from hikari.internal import marshaller from hikari.internal import meta from hikari.internal import more_asyncio from hikari.internal import more_collections from hikari.internal import more_logging +from hikari.internal import urls diff --git a/hikari/internal/marshaller.py b/hikari/internal/marshaller.py index b30bb61981..8d1fb73193 100644 --- a/hikari/internal/marshaller.py +++ b/hikari/internal/marshaller.py @@ -32,6 +32,8 @@ "marshallable", "HIKARI_ENTITY_MARSHALLER", "HikariEntityMarshaller", + "Deserializable", + "Serializable", ] import importlib @@ -51,6 +53,9 @@ _PASSED_THROUGH_SINGLETONS: typing.Final[typing.Sequence[bool]] = [False, True, None] RAISE: typing.Final[typing.Any] = object() +T_contra = typing.TypeVar("T_contra", contravariant=True) +T_co = typing.TypeVar("T_co", covariant=True) + EntityT = typing.TypeVar("EntityT", contravariant=True) @@ -450,3 +455,30 @@ def decorator(cls): return marshaller.register(cls) return decorator + + +class Deserializable: + """Mixin that enables the class to be deserialized from a raw entity.""" + + __slots__ = () + + @classmethod + def deserialize(cls: typing.Type[T_contra], payload: typing.Any) -> T_contra: + """Deserialize the given payload into the object. + + Parameters + ---------- + payload + The payload to deserialize into the object. + """ + return HIKARI_ENTITY_MARSHALLER.deserialize(payload, cls) + + +class Serializable: + """Mixin that enables an instance of the class to be serialized.""" + + __slots__ = () + + def serialize(self: T_co) -> typing.Any: + """Serialize this instance into a naive value.""" + return HIKARI_ENTITY_MARSHALLER.serialize(self) diff --git a/hikari/invites.py b/hikari/invites.py index af713eec14..10e6d0125f 100644 --- a/hikari/invites.py +++ b/hikari/invites.py @@ -29,9 +29,9 @@ from hikari import entities from hikari import guilds from hikari import users -from hikari.internal import urls from hikari.internal import conversions from hikari.internal import marshaller +from hikari.internal import urls @enum.unique @@ -44,7 +44,7 @@ class TargetUserType(enum.IntEnum): @marshaller.marshallable() @attr.s(slots=True, kw_only=True) -class VanityUrl(entities.HikariEntity, entities.Deserializable): +class VanityUrl(entities.HikariEntity, marshaller.Deserializable): """A special case invite object, that represents a guild's vanity url.""" #: The code for this invite. @@ -165,7 +165,7 @@ def banner_url(self) -> typing.Optional[str]: @marshaller.marshallable() @attr.s(slots=True, kw_only=True) -class Invite(entities.HikariEntity, entities.Deserializable): +class Invite(entities.HikariEntity, marshaller.Deserializable): """Represents an invite that's used to add users to a guild or group dm.""" #: The code for this invite. diff --git a/hikari/messages.py b/hikari/messages.py index a6a3ba24ab..b7a0eba1ab 100644 --- a/hikari/messages.py +++ b/hikari/messages.py @@ -133,7 +133,7 @@ class MessageActivityType(enum.IntEnum): @marshaller.marshallable() @attr.s(slots=True, kw_only=True) -class Attachment(snowflakes.UniqueEntity, entities.Deserializable): +class Attachment(snowflakes.UniqueEntity, marshaller.Deserializable): """Represents a file attached to a message.""" #: The name of the file. @@ -169,7 +169,7 @@ class Attachment(snowflakes.UniqueEntity, entities.Deserializable): @marshaller.marshallable() @attr.s(slots=True, kw_only=True) -class Reaction(entities.HikariEntity, entities.Deserializable): +class Reaction(entities.HikariEntity, marshaller.Deserializable): """Represents a reaction in a message.""" #: The amount of times the emoji has been used to react. @@ -192,7 +192,7 @@ class Reaction(entities.HikariEntity, entities.Deserializable): @marshaller.marshallable() @attr.s(slots=True, kw_only=True) -class MessageActivity(entities.HikariEntity, entities.Deserializable): +class MessageActivity(entities.HikariEntity, marshaller.Deserializable): """Represents the activity of a rich presence-enabled message.""" #: The type of message activity. @@ -208,7 +208,7 @@ class MessageActivity(entities.HikariEntity, entities.Deserializable): @marshaller.marshallable() @attr.s(slots=True, kw_only=True) -class MessageCrosspost(entities.HikariEntity, entities.Deserializable): +class MessageCrosspost(entities.HikariEntity, marshaller.Deserializable): """Represents information about a cross-posted message and the origin of the original message.""" #: The ID of the original message. @@ -246,7 +246,7 @@ class MessageCrosspost(entities.HikariEntity, entities.Deserializable): @marshaller.marshallable() @attr.s(slots=True, kw_only=True) -class Message(snowflakes.UniqueEntity, entities.Deserializable): +class Message(snowflakes.UniqueEntity, marshaller.Deserializable): """Represents a message.""" #: The ID of the channel that the message was sent in. diff --git a/hikari/oauth2.py b/hikari/oauth2.py index a2bc68a4f7..c90d473fca 100644 --- a/hikari/oauth2.py +++ b/hikari/oauth2.py @@ -29,8 +29,8 @@ from hikari import permissions from hikari import snowflakes from hikari import users -from hikari.internal import urls from hikari.internal import marshaller +from hikari.internal import urls @enum.unique @@ -46,7 +46,7 @@ class ConnectionVisibility(enum.IntEnum): @marshaller.marshallable() @attr.s(slots=True, kw_only=True) -class OwnConnection(entities.HikariEntity, entities.Deserializable): +class OwnConnection(entities.HikariEntity, marshaller.Deserializable): """Represents a user's connection with a third party account. Returned by the ``GET Current User Connections`` endpoint. @@ -141,7 +141,7 @@ class TeamMembershipState(enum.IntEnum): @marshaller.marshallable() @attr.s(slots=True, kw_only=True) -class TeamMember(entities.HikariEntity, entities.Deserializable): +class TeamMember(entities.HikariEntity, marshaller.Deserializable): """Represents a member of a Team.""" #: The state of this user's membership. @@ -168,7 +168,7 @@ class TeamMember(entities.HikariEntity, entities.Deserializable): @marshaller.marshallable() @attr.s(slots=True, kw_only=True) -class Team(snowflakes.UniqueEntity, entities.Deserializable): +class Team(snowflakes.UniqueEntity, marshaller.Deserializable): """Represents a development team, along with all its members.""" #: The hash of this team's icon, if set. @@ -238,7 +238,7 @@ def is_team_user(self) -> bool: @marshaller.marshallable() @attr.s(slots=True, kw_only=True) -class Application(snowflakes.UniqueEntity, entities.Deserializable): +class Application(snowflakes.UniqueEntity, marshaller.Deserializable): """Represents the information of an Oauth2 Application.""" #: The name of this application. diff --git a/hikari/snowflakes.py b/hikari/snowflakes.py index 184714e425..1644d2b076 100644 --- a/hikari/snowflakes.py +++ b/hikari/snowflakes.py @@ -30,9 +30,9 @@ import attr +from hikari import entities from hikari.internal import conversions from hikari.internal import marshaller -from hikari import entities @functools.total_ordering diff --git a/hikari/unset.py b/hikari/unset.py new file mode 100644 index 0000000000..b05eee7495 --- /dev/null +++ b/hikari/unset.py @@ -0,0 +1,69 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# Copyright © Nekoka.tt 2019-2020 +# +# This file is part of Hikari. +# +# Hikari is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Hikari is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Hikari. If not, see . +"""Sentinel for an unset value or attribute.""" +__all__ = ["Unset", "UNSET", "MayBeUnset"] + +import typing + +from hikari.internal import meta + + +class Unset(meta.Singleton): + """A singleton value that represents an unset field. + + This will always have a falsified value. + """ + + __slots__ = () + + def __bool__(self) -> bool: + return False + + def __repr__(self) -> str: + return type(self).__name__.upper() + + __str__ = __repr__ + + def __init_subclass__(cls, **kwargs: typing.Any) -> typing.NoReturn: + raise TypeError("Cannot subclass Unset type") + + def ___defer___(self, *_, **__) -> typing.NoReturn: + raise TypeError("This value is unset because it is not available at this time") + + +T = typing.TypeVar("T") +MayBeUnset = typing.Union[Unset, T] + +#: A global instance of :class:`~Unset`. +UNSET: typing.Final[Unset] = Unset() + + +@typing.overload +def is_unset(obj: UNSET) -> typing.Literal[True]: + """Return ``True`` always.""" + + +@typing.overload +def is_unset(obj: typing.Any) -> typing.Literal[False]: + """Return ``False`` always.""" + + +def is_unset(obj): + """Return ``True`` if the object is an :obj:`~Unset` value.""" + return isinstance(obj, Unset) diff --git a/hikari/users.py b/hikari/users.py index 3fdb1584ca..914976dbc1 100644 --- a/hikari/users.py +++ b/hikari/users.py @@ -24,10 +24,9 @@ import attr -from hikari import entities from hikari import snowflakes -from hikari.internal import urls from hikari.internal import marshaller +from hikari.internal import urls @enum.unique @@ -93,7 +92,7 @@ class PremiumType(enum.IntEnum): @marshaller.marshallable() @attr.s(slots=True, kw_only=True) -class User(snowflakes.UniqueEntity, entities.Deserializable): +class User(snowflakes.UniqueEntity, marshaller.Deserializable): """Represents a user.""" #: This user's discriminator. diff --git a/hikari/voices.py b/hikari/voices.py index a8ab1f4e16..8862ec50e1 100644 --- a/hikari/voices.py +++ b/hikari/voices.py @@ -31,7 +31,7 @@ @marshaller.marshallable() @attr.s(slots=True, kw_only=True) -class VoiceState(entities.HikariEntity, entities.Deserializable): +class VoiceState(entities.HikariEntity, marshaller.Deserializable): """Represents a user's voice connection status.""" #: The ID of the guild this voice state is in, if applicable. @@ -100,7 +100,7 @@ class VoiceState(entities.HikariEntity, entities.Deserializable): @marshaller.marshallable() @attr.s(slots=True, kw_only=True) -class VoiceRegion(entities.HikariEntity, entities.Deserializable): +class VoiceRegion(entities.HikariEntity, marshaller.Deserializable): """Represent's a voice region server.""" #: The ID of this region diff --git a/hikari/webhooks.py b/hikari/webhooks.py index 56e133f6e8..e1226bb204 100644 --- a/hikari/webhooks.py +++ b/hikari/webhooks.py @@ -24,7 +24,6 @@ import attr -from hikari import entities from hikari import snowflakes from hikari import users from hikari.internal import marshaller @@ -43,7 +42,7 @@ class WebhookType(enum.IntEnum): @marshaller.marshallable() @attr.s(slots=True, kw_only=True) -class Webhook(snowflakes.UniqueEntity, entities.Deserializable): +class Webhook(snowflakes.UniqueEntity, marshaller.Deserializable): """Represents a webhook object on Discord. This is an endpoint that can have messages sent to it using standard diff --git a/tests/hikari/_helpers.py b/tests/hikari/_helpers.py index 4df177a404..d85e4e58bb 100644 --- a/tests/hikari/_helpers.py +++ b/tests/hikari/_helpers.py @@ -30,9 +30,9 @@ import typing import warnings import weakref -import mock import async_timeout +import mock import pytest from hikari import snowflakes diff --git a/tests/hikari/clients/test_gateway_managers.py b/tests/hikari/clients/test_gateway_managers.py index 6792b2ea05..ba9622f59a 100644 --- a/tests/hikari/clients/test_gateway_managers.py +++ b/tests/hikari/clients/test_gateway_managers.py @@ -17,8 +17,8 @@ # You should have received a copy of the GNU Lesser General Public License # along ith Hikari. If not, see . import math -import mock +import mock import pytest from hikari.clients import gateway_managers diff --git a/tests/hikari/clients/test_shard_clients.py b/tests/hikari/clients/test_shard_clients.py index 7cf2fd5fb3..66f4d47c47 100644 --- a/tests/hikari/clients/test_shard_clients.py +++ b/tests/hikari/clients/test_shard_clients.py @@ -18,9 +18,9 @@ # along ith Hikari. If not, see . import datetime import math -import mock import aiohttp +import mock import pytest from hikari import errors diff --git a/tests/hikari/internal/test_conversions.py b/tests/hikari/internal/test_conversions.py index 06a490f1b5..f110a11d7f 100644 --- a/tests/hikari/internal/test_conversions.py +++ b/tests/hikari/internal/test_conversions.py @@ -21,8 +21,8 @@ import inspect import io import typing -import mock +import mock import pytest from hikari.internal import conversions diff --git a/tests/hikari/internal/test_marshaller.py b/tests/hikari/internal/test_marshaller.py index 4300dd557e..b9525ff1bc 100644 --- a/tests/hikari/internal/test_marshaller.py +++ b/tests/hikari/internal/test_marshaller.py @@ -16,9 +16,8 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . -import mock - import attr +import mock import pytest from hikari.internal import marshaller diff --git a/tests/hikari/internal/test_marshaller_pep563.py b/tests/hikari/internal/test_marshaller_pep563.py index 4f72979a9f..59ba0daf85 100644 --- a/tests/hikari/internal/test_marshaller_pep563.py +++ b/tests/hikari/internal/test_marshaller_pep563.py @@ -23,9 +23,8 @@ """ from __future__ import annotations -import mock - import attr +import mock import pytest from hikari.internal import marshaller diff --git a/tests/hikari/net/test_ratelimits.py b/tests/hikari/net/test_ratelimits.py index f824b643c9..ca02afe053 100644 --- a/tests/hikari/net/test_ratelimits.py +++ b/tests/hikari/net/test_ratelimits.py @@ -23,8 +23,8 @@ import math import statistics import time -import mock +import mock import pytest from hikari.net import ratelimits diff --git a/tests/hikari/net/test_rest.py b/tests/hikari/net/test_rest.py index 73354b7056..ee425d6175 100644 --- a/tests/hikari/net/test_rest.py +++ b/tests/hikari/net/test_rest.py @@ -20,11 +20,10 @@ import contextlib import io import json -import logging import ssl -import mock import aiohttp +import mock import pytest from hikari import errors diff --git a/tests/hikari/net/test_shard.py b/tests/hikari/net/test_shard.py index ee0c4505f6..132d7c5b8e 100644 --- a/tests/hikari/net/test_shard.py +++ b/tests/hikari/net/test_shard.py @@ -22,10 +22,10 @@ import math import time import urllib.parse -import mock import aiohttp import async_timeout +import mock import pytest from hikari import errors diff --git a/tests/hikari/state/test_event_dispatcher.py b/tests/hikari/state/test_event_dispatcher.py index 515f8cd016..56775a060e 100644 --- a/tests/hikari/state/test_event_dispatcher.py +++ b/tests/hikari/state/test_event_dispatcher.py @@ -17,8 +17,8 @@ # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . import asyncio -import mock +import mock import pytest from hikari import events diff --git a/tests/hikari/state/test_stateless_event_managers.py b/tests/hikari/state/test_stateless_event_managers.py index f5448d7c68..73ea3bbe3b 100644 --- a/tests/hikari/state/test_stateless_event_managers.py +++ b/tests/hikari/state/test_stateless_event_managers.py @@ -17,13 +17,11 @@ # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . import mock - import pytest from hikari import events from hikari.clients import shard_clients from hikari.state import stateless_event_managers -from tests.hikari import _helpers class TestStatelessEventManagerImpl: diff --git a/tests/hikari/test_audit_logs.py b/tests/hikari/test_audit_logs.py index de403b7e11..07c5890e02 100644 --- a/tests/hikari/test_audit_logs.py +++ b/tests/hikari/test_audit_logs.py @@ -18,8 +18,8 @@ # along ith Hikari. If not, see . import contextlib import datetime -import mock +import mock import pytest from hikari import audit_logs diff --git a/tests/hikari/test_channels.py b/tests/hikari/test_channels.py index 2bfe4fb469..dc04ff6a3b 100644 --- a/tests/hikari/test_channels.py +++ b/tests/hikari/test_channels.py @@ -17,8 +17,8 @@ # You should have received a copy of the GNU Lesser General Public License # along ith Hikari. If not, see . import datetime -import mock +import mock import pytest from hikari import channels diff --git a/tests/hikari/test_embeds.py b/tests/hikari/test_embeds.py index 5f2a2107b2..01fc9a1108 100644 --- a/tests/hikari/test_embeds.py +++ b/tests/hikari/test_embeds.py @@ -17,8 +17,8 @@ # You should have received a copy of the GNU Lesser General Public License # along ith Hikari. If not, see . import datetime -import mock +import mock import pytest from hikari import colors diff --git a/tests/hikari/test_emojis.py b/tests/hikari/test_emojis.py index 6221f61b83..07350ae5ab 100644 --- a/tests/hikari/test_emojis.py +++ b/tests/hikari/test_emojis.py @@ -17,7 +17,6 @@ # You should have received a copy of the GNU Lesser General Public License # along ith Hikari. If not, see . import mock - import pytest from hikari import emojis diff --git a/tests/hikari/test_entities.py b/tests/hikari/test_entities.py index e2ef6586d1..10add0633d 100644 --- a/tests/hikari/test_entities.py +++ b/tests/hikari/test_entities.py @@ -16,19 +16,3 @@ # # You should have received a copy of the GNU Lesser General Public License # along ith Hikari. If not, see . -from hikari import entities - - -class TestUnset: - def test_repr(self): - assert repr(entities.UNSET) == "UNSET" - - def test_str(self): - assert str(entities.UNSET) == "UNSET" - - def test_bool(self): - assert bool(entities.UNSET) is False - - def test_singleton_behaviour(self): - assert entities.Unset() is entities.Unset() - assert entities.UNSET is entities.Unset() diff --git a/tests/hikari/test_events.py b/tests/hikari/test_events.py index 635357e76b..774e423fca 100644 --- a/tests/hikari/test_events.py +++ b/tests/hikari/test_events.py @@ -18,21 +18,21 @@ # along ith Hikari. If not, see . import contextlib import datetime -import mock +import mock import pytest -import hikari.internal.conversions from hikari import channels from hikari import embeds from hikari import emojis -from hikari import entities from hikari import events from hikari import guilds from hikari import invites from hikari import messages from hikari import oauth2 +from hikari import unset from hikari import users +from hikari.internal import conversions from tests.hikari import _helpers @@ -213,7 +213,7 @@ def test_deserialize(self, test_base_channel_payload, test_overwrite_payload, te _helpers.patch_marshal_attr( events.BaseChannelEvent, "last_pin_timestamp", - deserializer=hikari.internal.conversions.parse_iso_8601_ts, + deserializer=conversions.parse_iso_8601_ts, return_value=mock_timestamp, ) ) @@ -272,7 +272,7 @@ def test_deserialize(self, test_chanel_pin_update_payload): with _helpers.patch_marshal_attr( events.ChannelPinUpdateEvent, "last_pin_timestamp", - deserializer=hikari.internal.conversions.parse_iso_8601_ts, + deserializer=conversions.parse_iso_8601_ts, return_value=mock_timestamp, ) as patched_iso_parser: channel_pin_add_obj = events.ChannelPinUpdateEvent.deserialize(test_chanel_pin_update_payload) @@ -397,7 +397,7 @@ def test_deserialize(self, guild_member_update_payload, test_user_payload): _helpers.patch_marshal_attr( events.GuildMemberUpdateEvent, "premium_since", - deserializer=hikari.internal.conversions.parse_iso_8601_ts, + deserializer=conversions.parse_iso_8601_ts, return_value=mock_premium_since, ) ) @@ -416,8 +416,8 @@ def test_partial_deserializer(self, guild_member_update_payload): del guild_member_update_payload["premium_since"] with _helpers.patch_marshal_attr(events.GuildMemberUpdateEvent, "user", deserializer=users.User.deserialize): guild_member_update_obj = events.GuildMemberUpdateEvent.deserialize(guild_member_update_payload) - assert guild_member_update_obj.nickname is entities.UNSET - assert guild_member_update_obj.premium_since is entities.UNSET + assert guild_member_update_obj.nickname is unset.UNSET + assert guild_member_update_obj.premium_since is unset.UNSET @pytest.fixture() @@ -500,7 +500,7 @@ def test_deserialize(self, test_invite_create_payload, test_user_payload): _helpers.patch_marshal_attr( events.InviteCreateEvent, "created_at", - deserializer=hikari.internal.conversions.parse_iso_8601_ts, + deserializer=conversions.parse_iso_8601_ts, return_value=mock_created_at, ) ) @@ -658,7 +658,7 @@ def test_deserialize( _helpers.patch_marshal_attr( events.MessageUpdateEvent, "timestamp", - deserializer=hikari.internal.conversions.parse_iso_8601_ts, + deserializer=conversions.parse_iso_8601_ts, return_value=mock_timestamp, ) ) @@ -666,7 +666,7 @@ def test_deserialize( _helpers.patch_marshal_attr( events.MessageUpdateEvent, "edited_timestamp", - deserializer=hikari.internal.conversions.parse_iso_8601_ts, + deserializer=conversions.parse_iso_8601_ts, return_value=mock_edited_timestamp, ) ) @@ -738,7 +738,7 @@ def test_partial_message_update(self): for key in message_update_obj.__slots__: if key in ("id", "channel_id"): continue - assert getattr(message_update_obj, key) is entities.UNSET + assert getattr(message_update_obj, key) is unset.UNSET assert message_update_obj.id == 393939 assert message_update_obj.channel_id == 434949 diff --git a/tests/hikari/test_gateway_entities.py b/tests/hikari/test_gateway_entities.py index 88fdca7e08..96bfe08a42 100644 --- a/tests/hikari/test_gateway_entities.py +++ b/tests/hikari/test_gateway_entities.py @@ -17,8 +17,8 @@ # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . import datetime -import mock +import mock import pytest from hikari import gateway_entities diff --git a/tests/hikari/test_guilds.py b/tests/hikari/test_guilds.py index 6669b6d972..3fdbb8a2cb 100644 --- a/tests/hikari/test_guilds.py +++ b/tests/hikari/test_guilds.py @@ -18,16 +18,16 @@ # along ith Hikari. If not, see . import contextlib import datetime -import mock +import mock import pytest from hikari import channels from hikari import colors from hikari import emojis -from hikari import entities from hikari import guilds from hikari import permissions +from hikari import unset from hikari import users from hikari.internal import conversions from hikari.internal import urls @@ -545,18 +545,18 @@ def test_deserialize_partial_presence_user(self): assert presence_user_obj.id == 115590097100865541 for attr in presence_user_obj.__slots__: if attr != "id": - assert getattr(presence_user_obj, attr) is entities.UNSET + assert getattr(presence_user_obj, attr) is unset.UNSET @pytest.fixture() def test_presence_user_obj(self): return guilds.PresenceUser( id=4242424242, - discriminator=entities.UNSET, - username=entities.UNSET, - avatar_hash=entities.UNSET, - is_bot=entities.UNSET, - is_system=entities.UNSET, - flags=entities.UNSET, + discriminator=unset.UNSET, + username=unset.UNSET, + avatar_hash=unset.UNSET, + is_bot=unset.UNSET, + is_system=unset.UNSET, + flags=unset.UNSET, ) def test_avatar_url(self, test_presence_user_obj): @@ -566,7 +566,7 @@ def test_avatar_url(self, test_presence_user_obj): assert test_presence_user_obj.avatar_url is mock_url users.User.format_avatar_url.assert_called_once() - @pytest.mark.parametrize(["avatar_hash", "discriminator"], [("dwaea22", entities.UNSET), (entities.UNSET, "2929")]) + @pytest.mark.parametrize(["avatar_hash", "discriminator"], [("dwaea22", unset.UNSET), (unset.UNSET, "2929")]) def test_format_avatar_url_when_discriminator_or_avatar_hash_set_without_optionals( self, test_presence_user_obj, avatar_hash, discriminator ): @@ -577,7 +577,7 @@ def test_format_avatar_url_when_discriminator_or_avatar_hash_set_without_optiona assert test_presence_user_obj.format_avatar_url() is mock_url users.User.format_avatar_url.assert_called_once_with(fmt=None, size=4096) - @pytest.mark.parametrize(["avatar_hash", "discriminator"], [("dwaea22", entities.UNSET), (entities.UNSET, "2929")]) + @pytest.mark.parametrize(["avatar_hash", "discriminator"], [("dwaea22", unset.UNSET), (unset.UNSET, "2929")]) def test_format_avatar_url_when_discriminator_or_avatar_hash_set_with_optionals( self, test_presence_user_obj, avatar_hash, discriminator ): @@ -589,10 +589,10 @@ def test_format_avatar_url_when_discriminator_or_avatar_hash_set_with_optionals( users.User.format_avatar_url.assert_called_once_with(fmt="nyaapeg", size=2048) def test_format_avatar_url_when_discriminator_and_avatar_hash_unset(self, test_presence_user_obj): - test_presence_user_obj.avatar_hash = entities.UNSET - test_presence_user_obj.discriminator = entities.UNSET + test_presence_user_obj.avatar_hash = unset.UNSET + test_presence_user_obj.discriminator = unset.UNSET with mock.patch.object(users.User, "format_avatar_url", return_value=...): - assert test_presence_user_obj.format_avatar_url() is entities.UNSET + assert test_presence_user_obj.format_avatar_url() is unset.UNSET users.User.format_avatar_url.assert_not_called() def test_default_avatar_when_discriminator_set(self, test_presence_user_obj): @@ -600,8 +600,8 @@ def test_default_avatar_when_discriminator_set(self, test_presence_user_obj): assert test_presence_user_obj.default_avatar == 2 def test_default_avatar_when_discriminator_unset(self, test_presence_user_obj): - test_presence_user_obj.discriminator = entities.UNSET - assert test_presence_user_obj.default_avatar is entities.UNSET + test_presence_user_obj.discriminator = unset.UNSET + assert test_presence_user_obj.default_avatar is unset.UNSET @pytest.fixture() diff --git a/tests/hikari/test_invites.py b/tests/hikari/test_invites.py index f6b9c02df7..fdd509d8c3 100644 --- a/tests/hikari/test_invites.py +++ b/tests/hikari/test_invites.py @@ -18,8 +18,8 @@ # along ith Hikari. If not, see . import contextlib import datetime -import mock +import mock import pytest from hikari import channels diff --git a/tests/hikari/test_messages.py b/tests/hikari/test_messages.py index 464b3bd60b..58e091fba8 100644 --- a/tests/hikari/test_messages.py +++ b/tests/hikari/test_messages.py @@ -18,17 +18,17 @@ # along ith Hikari. If not, see . import contextlib import datetime -import mock +import mock import pytest -import hikari.internal.conversions from hikari import embeds from hikari import emojis from hikari import guilds from hikari import messages from hikari import oauth2 from hikari import users +from hikari.internal import conversions from tests.hikari import _helpers @@ -203,17 +203,14 @@ def test_deserialize( ) patched_timestamp_deserializer = stack.enter_context( _helpers.patch_marshal_attr( - messages.Message, - "timestamp", - deserializer=hikari.internal.conversions.parse_iso_8601_ts, - return_value=mock_datetime, + messages.Message, "timestamp", deserializer=conversions.parse_iso_8601_ts, return_value=mock_datetime, ) ) patched_edited_timestamp_deserializer = stack.enter_context( _helpers.patch_marshal_attr( messages.Message, "edited_timestamp", - deserializer=hikari.internal.conversions.parse_iso_8601_ts, + deserializer=conversions.parse_iso_8601_ts, return_value=mock_datetime2, ) ) diff --git a/tests/hikari/test_oauth2.py b/tests/hikari/test_oauth2.py index 26fe44bbbd..5da8419a3f 100644 --- a/tests/hikari/test_oauth2.py +++ b/tests/hikari/test_oauth2.py @@ -17,7 +17,6 @@ # You should have received a copy of the GNU Lesser General Public License # along ith Hikari. If not, see . import mock - import pytest from hikari import guilds diff --git a/tests/hikari/test_snowflake.py b/tests/hikari/test_snowflake.py index ed9d367c87..21f0d26095 100644 --- a/tests/hikari/test_snowflake.py +++ b/tests/hikari/test_snowflake.py @@ -21,7 +21,6 @@ import attr import pytest -from hikari import entities from hikari import snowflakes from hikari.internal import marshaller @@ -93,7 +92,7 @@ def test_int(self): def stud_marshal_entity(self): @marshaller.marshallable() @attr.s(slots=True, kw_only=True) - class StudEntity(snowflakes.UniqueEntity, entities.Deserializable, entities.Serializable): + class StudEntity(snowflakes.UniqueEntity, marshaller.Deserializable, marshaller.Serializable): ... return StudEntity diff --git a/tests/hikari/test_unset.py b/tests/hikari/test_unset.py new file mode 100644 index 0000000000..2beb505e29 --- /dev/null +++ b/tests/hikari/test_unset.py @@ -0,0 +1,34 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# Copyright © Nekoka.tt 2019-2020 +# +# This file is part of Hikari. +# +# Hikari is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Hikari is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Hikari. If not, see . +from hikari import unset + + +class TestUnset: + def test_repr(self): + assert repr(unset.UNSET) == "UNSET" + + def test_str(self): + assert str(unset.UNSET) == "UNSET" + + def test_bool(self): + assert bool(unset.UNSET) is False + + def test_singleton_behaviour(self): + assert unset.Unset() is unset.Unset() + assert unset.UNSET is unset.Unset() diff --git a/tests/hikari/test_users.py b/tests/hikari/test_users.py index 71e988e801..47cc48c104 100644 --- a/tests/hikari/test_users.py +++ b/tests/hikari/test_users.py @@ -17,7 +17,6 @@ # You should have received a copy of the GNU Lesser General Public License # along ith Hikari. If not, see . import mock - import pytest from hikari import users From 39b4e1ecd70fd173b2c88bc5afa1be495833b28a Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Sat, 18 Apr 2020 14:41:41 +0100 Subject: [PATCH 161/922] Added properties to clients and renamed ones that were inconsistent. --- hikari/clients/gateway_managers.py | 55 ++++++- hikari/clients/shard_clients.py | 87 +++++++++-- hikari/clients/test_client.py | 19 +-- hikari/internal/__init__.py | 8 - hikari/internal/conversions.py | 1 - hikari/internal/more_enums.py | 60 ++++++++ hikari/net/codes.py | 13 +- pre-commit | 9 +- tests/hikari/clients/test_gateway_managers.py | 108 ++++++++++++-- tests/hikari/clients/test_shard_clients.py | 140 ++++++++++++------ tests/hikari/internal/test_more_enums.py | 48 ++++++ 11 files changed, 442 insertions(+), 106 deletions(-) create mode 100644 hikari/internal/more_enums.py create mode 100644 tests/hikari/internal/test_more_enums.py diff --git a/hikari/clients/gateway_managers.py b/hikari/clients/gateway_managers.py index 2a6b0e8cc3..21fadd1e5d 100644 --- a/hikari/clients/gateway_managers.py +++ b/hikari/clients/gateway_managers.py @@ -37,6 +37,7 @@ from hikari.clients import shard_clients from hikari.internal import conversions from hikari.internal import more_logging +from hikari.net import codes from hikari.state import event_dispatchers from hikari.state import raw_event_consumers @@ -63,7 +64,7 @@ def __init__( ) -> None: super().__init__(more_logging.get_named_logger(self, conversions.pluralize(shard_count, "shard"))) self._is_running = False - self.config = config + self._config = config self.raw_event_consumer = raw_event_consumer_impl self._dispatcher = dispatcher self.shards: typing.Dict[int, ShardT] = { @@ -72,7 +73,7 @@ def __init__( self.shard_ids = shard_ids @property - def latency(self) -> float: + def heartbeat_latency(self) -> float: """Average heartbeat latency for all valid shards. This will return a mean of all the heartbeat intervals for all shards @@ -92,11 +93,57 @@ def latency(self) -> float: """ latencies = [] for shard in self.shards.values(): - if not math.isnan(shard.latency): - latencies.append(shard.latency) + if not math.isnan(shard.heartbeat_latency): + latencies.append(shard.heartbeat_latency) return sum(latencies) / len(latencies) if latencies else float("nan") + @property + def total_disconnect_count(self) -> int: + """Total number of times any shard has disconnected. + + Returns + ------- + :obj:`int` + Total disconnect count. + """ + return sum(s.disconnect_count for s in self.shards.values()) + + @property + def total_reconnect_count(self) -> int: + """Total number of times any shard has reconnected. + + Returns + ------- + :obj:`int` + Total reconnect count. + """ + return sum(s.reconnect_count for s in self.shards.values()) + + @property + def intents(self) -> typing.Optional[codes.GatewayIntent]: + """Intent values that any shard connections will be using. + + Returns + ------- + :obj:`~hikari.net.codes.GatewayIntent`, optional + A :obj:`~enum.IntFlag` enum containing each intent that is set. If + intents are not being used at all, then this will return + :obj:`~None` instead. + """ + return self._config.intents + + @property + def version(self) -> float: + """Version being used for the gateway API. + + Returns + ------- + :obj:`~int` + The API version being used. + """ + return self._config.gateway_version + async def start(self) -> None: """Start all shards. diff --git a/hikari/clients/shard_clients.py b/hikari/clients/shard_clients.py index e63cefd2e4..2c00dd47ce 100644 --- a/hikari/clients/shard_clients.py +++ b/hikari/clients/shard_clients.py @@ -175,17 +175,6 @@ def __init__( version=config.gateway_version, ) - @property - def connection(self) -> shard.ShardConnection: - """Low-level gateway client used for this shard. - - Returns - ------- - :obj:`~hikari.net.shard.ShardConnection` - The low-level gateway client used for this shard. - """ - return self._connection - @property def shard_id(self) -> int: """Shard ID. @@ -258,7 +247,7 @@ def is_afk(self) -> bool: # noqa: D401 return self._is_afk @property - def latency(self) -> float: + def heartbeat_latency(self) -> float: """Latency between sending a HEARTBEAT and receiving an ACK. Returns @@ -281,6 +270,17 @@ def heartbeat_interval(self) -> float: """ return self._connection.heartbeat_interval + @property + def disconnect_count(self) -> int: + """Count of number of times the internal connection has disconnected. + + Returns + ------- + :obj:`~int` + The number of disconnects this shard has performed. + """ + return self._connection.disconnect_count + @property def reconnect_count(self) -> int: """Count of number of times the internal connection has reconnected. @@ -305,6 +305,65 @@ def connection_state(self) -> ShardState: """ return self._shard_state + @property + def is_connected(self) -> bool: + """Whether the shard is connected or not. + + Returns + ------- + :obj:`~bool` + :obj:`~True` if connected; :obj:`~False` otherwise. + """ + return self._connection.is_connected + + @property + def seq(self) -> typing.Optional[int]: + """Sequence ID of the shard. + + Returns + ------- + :obj:`~int`, optional + The sequence number for the shard. This is the number of payloads + that have been received since an ``IDENTIFY`` was sent. + """ + return self._connection.seq + + @property + def session_id(self) -> typing.Optional[str]: + """Session ID. + + Returns + ------- + :obj:`~str`, optional + The session ID for the shard connection, if there is one. If not, + then :obj:`~None`. + """ + return self._connection.session_id + + @property + def version(self) -> float: + """Version being used for the gateway API. + + Returns + ------- + :obj:`~int` + The API version being used. + """ + return self._connection.version + + @property + def intents(self) -> typing.Optional[codes.GatewayIntent]: + """Intent values that this connection is using. + + Returns + ------- + :obj:`~hikari.net.codes.GatewayIntent`, optional + A :obj:`~enum.IntFlag` enum containing each intent that is set. If + intents are not being used at all, then this will return + :obj:`~None` instead. + """ + return self._connection.intents + async def start(self): """Connect to the gateway on this shard and keep the connection alive. @@ -437,7 +496,7 @@ async def _spin_up(self) -> asyncio.Task: if connect_task in completed: raise connect_task.exception() - self.logger.info("received HELLO, interval is %ss", self.connection.heartbeat_interval) + self.logger.info("received HELLO, interval is %ss", self._connection.heartbeat_interval) completed, _ = await asyncio.wait( [connect_task, self._connection.handshake_event.wait()], return_when=asyncio.FIRST_COMPLETED @@ -536,7 +595,7 @@ def _create_presence_pl( } def __str__(self) -> str: - return f"Shard {self.connection.shard_id} in pool of {self.connection.shard_count} shards" + return f"Shard {self.shard_id} in pool of {self.shard_count} shards" def __repr__(self) -> str: return ( diff --git a/hikari/clients/test_client.py b/hikari/clients/test_client.py index 2d73fedea0..ee141ba729 100644 --- a/hikari/clients/test_client.py +++ b/hikari/clients/test_client.py @@ -121,25 +121,16 @@ async def on_message(event: hikari.MessageCreateEvent) -> None: message = await client.rest.create_message(event.channel_id, content="Pong!") rest_time = time.perf_counter() - start - if (raw_intents := client.gateway.shards[0].connection.intents) is not None: - active_intents = [] - for i in hikari.GatewayIntent: - if i & raw_intents: - active_intents.append(i.name) - active_intents = ", ".join(active_intents) - else: - active_intents = "not enabled" + active_intents = str(client.gateway._config.intents or "not provided") shard_infos = [] for shard_id, shard in client.gateway.shards.items(): shard_info = ( - f"latency: {shard.latency * 1_000:.0f} ms\n" - f"seq: {shard.connection.seq}\n" - f"session id: {shard.connection.session_id}\n" + f"latency: {shard.heartbeat_latency * 1_000:.0f} ms\n" + f"seq: {shard.seq}\n" + f"session id: {shard.session_id}\n" f"reconnects: {shard.reconnect_count}\n" - f"last payload: {since(shard.connection.last_message_received)} ago\n" - f"last heartbeat: {since(shard.connection.last_heartbeat_sent)} ago\n" - f"heartbeat interval: {shard.connection.heartbeat_interval} s\n" + f"heartbeat interval: {shard.heartbeat_interval} s\n" f"state: {shard.connection_state.name}\n" ) diff --git a/hikari/internal/__init__.py b/hikari/internal/__init__.py index 96c9e63a3c..a8944f790e 100644 --- a/hikari/internal/__init__.py +++ b/hikari/internal/__init__.py @@ -20,11 +20,3 @@ |internal| """ -from hikari.internal import assertions -from hikari.internal import conversions -from hikari.internal import marshaller -from hikari.internal import meta -from hikari.internal import more_asyncio -from hikari.internal import more_collections -from hikari.internal import more_logging -from hikari.internal import urls diff --git a/hikari/internal/conversions.py b/hikari/internal/conversions.py index 04de16d123..a2f720a223 100644 --- a/hikari/internal/conversions.py +++ b/hikari/internal/conversions.py @@ -47,7 +47,6 @@ import types import typing - IntFlagT = typing.TypeVar("IntFlagT", bound=enum.IntFlag) RawIntFlagValueT = typing.Union[typing.AnyStr, typing.SupportsInt, int] DISCORD_EPOCH: typing.Final[int] = 1_420_070_400 diff --git a/hikari/internal/more_enums.py b/hikari/internal/more_enums.py new file mode 100644 index 0000000000..84eadf9406 --- /dev/null +++ b/hikari/internal/more_enums.py @@ -0,0 +1,60 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# Copyright © Nekoka.tt 2019-2020 +# +# This file is part of Hikari. +# +# Hikari is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Hikari is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Hikari. If not, see . +"""Mixin utilities for defining enums.""" +__all__ = ["EnumMixin", "FlagMixin"] + +import typing + + +class EnumMixin: + """Mixin for a non-flag enum type. + + This gives a more meaningful ``__str__`` implementation. + + The class should inherit this mixin before any type defined in :mod:`~enum`. + """ + + __slots__ = () + + #: The name of the enum member. + #: + #: :obj:`~str` + name: str + + def __str__(self) -> str: + return self.name + + +class FlagMixin: + """Mixin for a flag enum type. + + This gives a more meaningful ``__str__`` implementation. + + The class should inherit this mixin before any type defined in :mod:`~enum`. + """ + + __slots__ = () + + #: The name of the enum member. + #: + #: :obj:`~str` + name: str + + def __str__(self) -> str: + return ", ".join(flag.name for flag in typing.cast(typing.Iterable, type(self)) if flag & self) diff --git a/hikari/net/codes.py b/hikari/net/codes.py index 7fd08c2bba..4f145a8e39 100644 --- a/hikari/net/codes.py +++ b/hikari/net/codes.py @@ -24,8 +24,11 @@ # Doesnt work correctly with enums, so since this file is all enums, ignore # pylint: disable=no-member +from hikari.internal import more_enums + + @enum.unique -class HTTPStatusCode(enum.IntEnum): +class HTTPStatusCode(more_enums.EnumMixin, enum.IntEnum): """HTTP response codes expected from RESTful components.""" #: Continue @@ -106,7 +109,7 @@ def __str__(self) -> str: @enum.unique -class GatewayCloseCode(enum.IntEnum): +class GatewayCloseCode(more_enums.EnumMixin, enum.IntEnum): """Reasons for closing a gateway connection. Note @@ -171,7 +174,7 @@ def __str__(self) -> str: @enum.unique -class GatewayOpcode(enum.IntEnum): +class GatewayOpcode(more_enums.EnumMixin, enum.IntEnum): """Opcodes that the gateway uses internally.""" #: An event was dispatched. @@ -217,7 +220,7 @@ def __str__(self) -> str: @enum.unique -class JSONErrorCode(enum.IntEnum): +class JSONErrorCode(more_enums.EnumMixin, enum.IntEnum): """Error codes that can be returned by the REST API.""" #: This is sent if the payload is screwed up, etc. @@ -420,7 +423,7 @@ def __str__(self) -> str: @enum.unique -class GatewayIntent(enum.IntFlag): +class GatewayIntent(more_enums.FlagMixin, enum.IntFlag): """Represents an intent on the gateway. This is a bitfield representation of all the categories of event diff --git a/pre-commit b/pre-commit index 0e00fd39ca..fd879659c3 100755 --- a/pre-commit +++ b/pre-commit @@ -1,5 +1,10 @@ -#!/bin/sh +#!/bin/bash # Put this in .git/hooks to make sure 'black' runs every time you commit. -nox -sformat +if [[ -z ${SKIP_HOOK} ]]; then + echo -e '\e[1;32mRUNNING HOOKS - prepend your invocation of git commit with SKIP_HOOK=1 to skip this.\e[0m' + nox --sessions reformat-code pytest pylint pydocstyle +else + echo -e '\e[1;31mSKIPPING COMMIT HOOKS. THIS IS HERESY!\e[0m' +fi diff --git a/tests/hikari/clients/test_gateway_managers.py b/tests/hikari/clients/test_gateway_managers.py index ba9622f59a..e4ed2a8782 100644 --- a/tests/hikari/clients/test_gateway_managers.py +++ b/tests/hikari/clients/test_gateway_managers.py @@ -21,16 +21,18 @@ import mock import pytest +from hikari.clients import configs from hikari.clients import gateway_managers from hikari.clients import shard_clients +from hikari.net import codes from tests.hikari import _helpers -class TestGatewayManager: - def test_latency(self): - shard1 = mock.MagicMock(shard_clients.ShardClient, latency=20) - shard2 = mock.MagicMock(shard_clients.ShardClient, latency=30) - shard3 = mock.MagicMock(shard_clients.ShardClient, latency=40) +class TestGatewayManagerProperties: + def test_heartbeat_latency(self): + shard1 = mock.MagicMock(shard_clients.ShardClient, heartbeat_latency=20) + shard2 = mock.MagicMock(shard_clients.ShardClient, heartbeat_latency=30) + shard3 = mock.MagicMock(shard_clients.ShardClient, heartbeat_latency=40) with mock.patch("hikari.clients.shard_clients.ShardClient", side_effect=[shard1, shard2, shard3]): gateway_manager_obj = gateway_managers.GatewayManager( @@ -42,12 +44,12 @@ def test_latency(self): shard_type=shard_clients.ShardClient, ) - assert gateway_manager_obj.latency == 30 + assert gateway_manager_obj.heartbeat_latency == 30 - def test_latency_doesnt_take_into_a_count_shards_with_no_latency(self): - shard1 = mock.MagicMock(shard_clients.ShardClient, latency=20) - shard2 = mock.MagicMock(shard_clients.ShardClient, latency=30) - shard3 = mock.MagicMock(shard_clients.ShardClient, latency=float("nan")) + def test_heartbeat_latency_doesnt_take_into_a_count_shards_with_no_latency(self): + shard1 = mock.MagicMock(shard_clients.ShardClient, heartbeat_latency=20) + shard2 = mock.MagicMock(shard_clients.ShardClient, heartbeat_latency=30) + shard3 = mock.MagicMock(shard_clients.ShardClient, heartbeat_latency=float("nan")) with mock.patch("hikari.clients.shard_clients.ShardClient", side_effect=[shard1, shard2, shard3]): gateway_manager_obj = gateway_managers.GatewayManager( @@ -59,12 +61,12 @@ def test_latency_doesnt_take_into_a_count_shards_with_no_latency(self): shard_type=shard_clients.ShardClient, ) - assert gateway_manager_obj.latency == 25 + assert gateway_manager_obj.heartbeat_latency == 25 - def test_latency_returns_nan_if_all_shards_have_no_latency(self): - shard1 = mock.MagicMock(shard_clients.ShardClient, latency=float("nan")) - shard2 = mock.MagicMock(shard_clients.ShardClient, latency=float("nan")) - shard3 = mock.MagicMock(shard_clients.ShardClient, latency=float("nan")) + def test_heartbeat_latency_returns_nan_if_all_shards_have_no_latency(self): + shard1 = mock.MagicMock(shard_clients.ShardClient, heartbeat_latency=float("nan")) + shard2 = mock.MagicMock(shard_clients.ShardClient, heartbeat_latency=float("nan")) + shard3 = mock.MagicMock(shard_clients.ShardClient, heartbeat_latency=float("nan")) with mock.patch("hikari.clients.shard_clients.ShardClient", side_effect=[shard1, shard2, shard3]): gateway_manager_obj = gateway_managers.GatewayManager( @@ -76,8 +78,82 @@ def test_latency_returns_nan_if_all_shards_have_no_latency(self): shard_type=shard_clients.ShardClient, ) - assert math.isnan(gateway_manager_obj.latency) + assert math.isnan(gateway_manager_obj.heartbeat_latency) + def test_total_disconnect_count(self): + shard1 = mock.MagicMock(shard_clients.ShardClient, disconnect_count=7) + shard2 = mock.MagicMock(shard_clients.ShardClient, disconnect_count=2) + shard3 = mock.MagicMock(shard_clients.ShardClient, disconnect_count=13) + + with mock.patch("hikari.clients.shard_clients.ShardClient", side_effect=[shard1, shard2, shard3]): + gateway_manager_obj = gateway_managers.GatewayManager( + shard_ids=[0, 1, 2], + shard_count=3, + config=None, + url="some_url", + raw_event_consumer_impl=None, + shard_type=shard_clients.ShardClient, + ) + + assert gateway_manager_obj.total_disconnect_count == 22 + + def test_total_reconnect_count(self): + shard1 = mock.MagicMock(shard_clients.ShardClient, reconnect_count=7) + shard2 = mock.MagicMock(shard_clients.ShardClient, reconnect_count=2) + shard3 = mock.MagicMock(shard_clients.ShardClient, reconnect_count=13) + + with mock.patch("hikari.clients.shard_clients.ShardClient", side_effect=[shard1, shard2, shard3]): + gateway_manager_obj = gateway_managers.GatewayManager( + shard_ids=[0, 1, 2], + shard_count=3, + config=None, + url="some_url", + raw_event_consumer_impl=None, + shard_type=shard_clients.ShardClient, + ) + + assert gateway_manager_obj.total_reconnect_count == 22 + + def test_intents(self): + shard1 = mock.MagicMock(shard_clients.ShardClient) + shard2 = mock.MagicMock(shard_clients.ShardClient) + shard3 = mock.MagicMock(shard_clients.ShardClient) + + intents = codes.GatewayIntent.DIRECT_MESSAGE_TYPING | codes.GatewayIntent.DIRECT_MESSAGE_REACTIONS + + with mock.patch("hikari.clients.shard_clients.ShardClient", side_effect=[shard1, shard2, shard3]): + gateway_manager_obj = gateway_managers.GatewayManager( + shard_ids=[0, 1, 2], + shard_count=3, + config=configs.BotConfig(intents=intents), + url="some_url", + raw_event_consumer_impl=None, + shard_type=shard_clients.ShardClient, + ) + + assert gateway_manager_obj.intents == intents + + def test_version(self): + shard1 = mock.MagicMock(shard_clients.ShardClient) + shard2 = mock.MagicMock(shard_clients.ShardClient) + shard3 = mock.MagicMock(shard_clients.ShardClient) + + version = 7 + + with mock.patch("hikari.clients.shard_clients.ShardClient", side_effect=[shard1, shard2, shard3]): + gateway_manager_obj = gateway_managers.GatewayManager( + shard_ids=[0, 1, 2], + shard_count=3, + config=configs.BotConfig(gateway_version=version), + url="some_url", + raw_event_consumer_impl=None, + shard_type=shard_clients.ShardClient, + ) + + assert gateway_manager_obj.version == version + + +class TestGatewayManagerManagement: @pytest.mark.asyncio async def test_start_waits_five_seconds_between_shard_startup(self): mock_sleep = mock.AsyncMock() diff --git a/tests/hikari/clients/test_shard_clients.py b/tests/hikari/clients/test_shard_clients.py index 66f4d47c47..00e254e86c 100644 --- a/tests/hikari/clients/test_shard_clients.py +++ b/tests/hikari/clients/test_shard_clients.py @@ -33,31 +33,33 @@ from tests.hikari import _helpers -class TestShardClient: - @pytest.fixture - def shard_client_obj(self): - mock_shard_connection = mock.MagicMock( - shard.ShardConnection, - heartbeat_latency=float("nan"), - heartbeat_interval=float("nan"), - reconnect_count=0, - seq=None, - session_id=None, - ) - with mock.patch("hikari.net.shard.ShardConnection", return_value=mock_shard_connection): - return _helpers.unslot_class(shard_clients.ShardClient)(0, 1, configs.WebsocketConfig(), None, "some_url") - - def _generate_mock_task(self, exception=None): - class Task(mock.MagicMock): - def __init__(self, exception): - super().__init__() - self._exception = exception - - def exception(self): - return self._exception +def _generate_mock_task(exception=None): + class Task(mock.MagicMock): + def __init__(self): + super().__init__() + self._exception = exception + + def exception(self): + return self._exception + + return Task() + + +@pytest.fixture +def shard_client_obj(): + mock_shard_connection = mock.MagicMock( + shard.ShardConnection, + heartbeat_latency=float("nan"), + heartbeat_interval=float("nan"), + reconnect_count=0, + seq=None, + session_id=None, + ) + with mock.patch("hikari.net.shard.ShardConnection", return_value=mock_shard_connection): + return _helpers.unslot_class(shard_clients.ShardClient)(0, 1, configs.WebsocketConfig(), None, "some_url") - return Task(exception) +class TestShardClient: def test_raw_event_consumer_in_shardclient(self): class DummyConsumer(raw_event_consumers.RawEventConsumer): def process_raw_event(self, _client, name, payload): @@ -67,42 +69,92 @@ def process_raw_event(self, _client, name, payload): assert shard_client_obj._connection.dispatch(shard_client_obj, "TEST", {}) == "ASSERT TRUE" - def test_connection(self, shard_client_obj): + def test_connection_is_set(self, shard_client_obj): mock_shard_connection = mock.MagicMock(shard.ShardConnection) with mock.patch("hikari.net.shard.ShardConnection", return_value=mock_shard_connection): shard_client_obj = shard_clients.ShardClient(0, 1, configs.WebsocketConfig(), None, "some_url") - assert shard_client_obj.connection == mock_shard_connection + assert shard_client_obj._connection is mock_shard_connection + +class TestShardClientDelegateProperties: def test_status(self, shard_client_obj): - assert shard_client_obj.status == guilds.PresenceStatus.ONLINE + marker = object() + shard_client_obj._status = marker + assert shard_client_obj.status is marker def test_activity(self, shard_client_obj): - assert shard_client_obj.activity is None + marker = object() + shard_client_obj._activity = marker + assert shard_client_obj.activity is marker def test_idle_since(self, shard_client_obj): - assert shard_client_obj.idle_since is None + marker = object() + shard_client_obj._idle_since = marker + assert shard_client_obj.idle_since is marker def test_is_afk(self, shard_client_obj): - assert shard_client_obj.is_afk is False + marker = object() + shard_client_obj._is_afk = marker + assert shard_client_obj.is_afk is marker - def test_latency(self, shard_client_obj): - assert math.isnan(shard_client_obj.latency) + def test_heartbeat_latency(self, shard_client_obj): + marker = object() + shard_client_obj._connection.heartbeat_latency = marker + assert shard_client_obj.heartbeat_latency is marker def test_heartbeat_interval(self, shard_client_obj): - assert math.isnan(shard_client_obj.heartbeat_interval) + marker = object() + shard_client_obj._connection.heartbeat_interval = marker + assert shard_client_obj.heartbeat_interval is marker def test_reconnect_count(self, shard_client_obj): - assert shard_client_obj.reconnect_count == 0 + marker = object() + shard_client_obj._connection.reconnect_count = marker + assert shard_client_obj.reconnect_count is marker + + def test_disconnect_count(self, shard_client_obj): + marker = object() + shard_client_obj._connection.disconnect_count = marker + assert shard_client_obj.disconnect_count is marker def test_connection_state(self, shard_client_obj): - assert shard_client_obj.connection_state == shard_clients.ShardState.NOT_RUNNING + marker = object() + shard_client_obj._shard_state = marker + assert shard_client_obj.connection_state is marker + def test_is_connected(self, shard_client_obj): + marker = object() + shard_client_obj._connection.is_connected = marker + assert shard_client_obj.is_connected is marker + + def test_seq(self, shard_client_obj): + marker = object() + shard_client_obj._connection.seq = marker + assert shard_client_obj.seq is marker + + def test_session_id(self, shard_client_obj): + marker = object() + shard_client_obj._connection.session_id = marker + assert shard_client_obj.session_id is marker + + def test_version(self, shard_client_obj): + marker = object() + shard_client_obj._connection.version = marker + assert shard_client_obj.version is marker + + def test_intents(self, shard_client_obj): + marker = object() + shard_client_obj._connection.intents = marker + assert shard_client_obj.intents is marker + + +class TestShardClientStart: @pytest.mark.asyncio async def test_start_when_ready_event_completes_first(self, shard_client_obj): shard_client_obj._keep_alive = mock.AsyncMock() - task_mock = self._generate_mock_task() + task_mock = _generate_mock_task() with mock.patch("asyncio.create_task", return_value=task_mock): with mock.patch("asyncio.wait", return_value=([], None)): @@ -112,7 +164,7 @@ async def test_start_when_ready_event_completes_first(self, shard_client_obj): @pytest.mark.asyncio async def test_start_when_task_completes(self, shard_client_obj): shard_client_obj._keep_alive = mock.AsyncMock() - task_mock = self._generate_mock_task(RuntimeError) + task_mock = _generate_mock_task(RuntimeError) with mock.patch("asyncio.create_task", return_value=task_mock): with mock.patch("asyncio.wait", return_value=([task_mock], None)): @@ -237,10 +289,12 @@ def side_effect(*args): with mock.patch("asyncio.sleep", new=mock.AsyncMock()): await shard_client_obj._keep_alive() + +class TestShardClientSpinUp: @_helpers.assert_raises(type_=RuntimeError) @pytest.mark.asyncio async def test__spin_up_if_connect_task_is_completed_raises_exception_during_hello_event(self, shard_client_obj): - task_mock = self._generate_mock_task(RuntimeError) + task_mock = _generate_mock_task(RuntimeError) with mock.patch("asyncio.create_task", return_value=task_mock): with mock.patch("asyncio.wait", return_value=([task_mock], None)): @@ -249,7 +303,7 @@ async def test__spin_up_if_connect_task_is_completed_raises_exception_during_hel @_helpers.assert_raises(type_=RuntimeError) @pytest.mark.asyncio async def test__spin_up_if_connect_task_is_completed_raises_exception_during_identify_event(self, shard_client_obj): - task_mock = self._generate_mock_task(RuntimeError) + task_mock = _generate_mock_task(RuntimeError) with mock.patch("asyncio.create_task", return_value=task_mock): with mock.patch("asyncio.wait", side_effect=[([], None), ([task_mock], None)]): @@ -259,7 +313,7 @@ async def test__spin_up_if_connect_task_is_completed_raises_exception_during_ide async def test__spin_up_when_resuming(self, shard_client_obj): shard_client_obj._connection.seq = 123 shard_client_obj._connection.session_id = 123 - task_mock = self._generate_mock_task() + task_mock = _generate_mock_task() with mock.patch("asyncio.create_task", return_value=task_mock): with mock.patch("asyncio.wait", side_effect=[([], None), ([], None), ([], None)]): @@ -270,7 +324,7 @@ async def test__spin_up_when_resuming(self, shard_client_obj): async def test__spin_up_if_connect_task_is_completed_raises_exception_during_resumed_event(self, shard_client_obj): shard_client_obj._connection.seq = 123 shard_client_obj._connection.session_id = 123 - task_mock = self._generate_mock_task(RuntimeError) + task_mock = _generate_mock_task(RuntimeError) with mock.patch("asyncio.create_task", return_value=task_mock): with mock.patch("asyncio.wait", side_effect=[([], None), ([], None), ([task_mock], None)]): @@ -278,7 +332,7 @@ async def test__spin_up_if_connect_task_is_completed_raises_exception_during_res @pytest.mark.asyncio async def test__spin_up_when_not_resuming(self, shard_client_obj): - task_mock = self._generate_mock_task() + task_mock = _generate_mock_task() with mock.patch("asyncio.create_task", return_value=task_mock): with mock.patch("asyncio.wait", side_effect=[([], None), ([], None), ([], None)]): @@ -287,12 +341,14 @@ async def test__spin_up_when_not_resuming(self, shard_client_obj): @_helpers.assert_raises(type_=RuntimeError) @pytest.mark.asyncio async def test__spin_up_if_connect_task_is_completed_raises_exception_during_ready_event(self, shard_client_obj): - task_mock = self._generate_mock_task(RuntimeError) + task_mock = _generate_mock_task(RuntimeError) with mock.patch("asyncio.create_task", return_value=task_mock): with mock.patch("asyncio.wait", side_effect=[([], None), ([], None), ([task_mock], None)]): await shard_client_obj._spin_up() + +class TestShardClientUpdatePresence: @pytest.mark.asyncio async def test_update_presence(self, shard_client_obj): await shard_client_obj.update_presence() diff --git a/tests/hikari/internal/test_more_enums.py b/tests/hikari/internal/test_more_enums.py new file mode 100644 index 0000000000..c70ab9e2a8 --- /dev/null +++ b/tests/hikari/internal/test_more_enums.py @@ -0,0 +1,48 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# Copyright © Nekoka.tt 2019-2020 +# +# This file is part of Hikari. +# +# Hikari is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Hikari is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Hikari. If not, see . +import enum + +from hikari.internal import more_enums + + +class TestEnumMixin: + def test_str(self): + class TestType(more_enums.FlagMixin, enum.IntEnum): + a = 1 + b = 2 + c = 4 + d = 8 + e = 16 + + inst = TestType(2) + assert str(inst) == "b" + + +class TestFlagMixin: + def test_str(self): + class TestType(more_enums.FlagMixin, enum.IntFlag): + a = 1 + b = 2 + c = 4 + d = 8 + e = 16 + + inst = TestType(7) + + assert str(inst) == "a, b, c" From d576d26828dceda010e4c104a99598f1cd742ac7 Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Sat, 18 Apr 2020 14:55:44 +0100 Subject: [PATCH 162/922] Fixed inconsistent naming of .net components. --- hikari/clients/rest_clients/__init__.py | 6 +- hikari/clients/rest_clients/component_base.py | 12 ++-- hikari/clients/shard_clients.py | 6 +- hikari/internal/pagination.py | 2 +- hikari/net/__init__.py | 12 ++-- hikari/net/{rest.py => rest_sessions.py} | 4 +- hikari/net/{shard.py => shards.py} | 4 +- hikari/net/{user_agent.py => user_agents.py} | 0 .../test_rest_clients/test___init__.py | 8 ++- .../test_channels_component.py | 4 +- .../test_rest_clients/test_component_base.py | 6 +- .../test_current_users_component.py | 4 +- .../test_gateways_component.py | 4 +- .../test_guilds_component.py | 4 +- .../test_invites_component.py | 4 +- .../test_oauth2_component.py | 4 +- .../test_reactions_component.py | 4 +- .../test_rest_clients/test_users_component.py | 4 +- .../test_voices_component.py | 4 +- .../test_webhooks_component.py | 4 +- tests/hikari/clients/test_shard_clients.py | 10 +-- .../{test_rest.py => test_rest_sessions.py} | 20 +++--- .../net/{test_shard.py => test_shards.py} | 64 +++++++++---------- ...test_user_agent.py => test_user_agents.py} | 16 ++--- 24 files changed, 107 insertions(+), 103 deletions(-) rename hikari/net/{rest.py => rest_sessions.py} (99%) rename hikari/net/{shard.py => shards.py} (99%) rename hikari/net/{user_agent.py => user_agents.py} (100%) rename tests/hikari/net/{test_rest.py => test_rest_sessions.py} (99%) rename tests/hikari/net/{test_shard.py => test_shards.py} (92%) rename tests/hikari/net/{test_user_agent.py => test_user_agents.py} (67%) diff --git a/hikari/clients/rest_clients/__init__.py b/hikari/clients/rest_clients/__init__.py index 4107206743..0bab562852 100644 --- a/hikari/clients/rest_clients/__init__.py +++ b/hikari/clients/rest_clients/__init__.py @@ -35,7 +35,7 @@ from hikari.clients.rest_clients import voices_component from hikari.clients.rest_clients import webhooks_component from hikari.clients import configs -from hikari.net import rest +from hikari.net import rest_sessions class RESTClient( @@ -54,7 +54,7 @@ class RESTClient( A marshalling object-oriented REST API client. This client bridges the basic REST API exposed by - :obj:`~hikari.net.rest.LowLevelRestfulClient` and wraps it in a unit of + :obj:`~hikari.net.rest_sessions.LowLevelRestfulClient` and wraps it in a unit of processing that can handle parsing API objects into Hikari entity objects. Parameters @@ -71,7 +71,7 @@ class RESTClient( def __init__(self, config: configs.RESTConfig) -> None: super().__init__( - rest.LowLevelRestfulClient( + rest_sessions.LowLevelRestfulClient( allow_redirects=config.allow_redirects, connector=config.tcp_connector, proxy_headers=config.proxy_headers, diff --git a/hikari/clients/rest_clients/component_base.py b/hikari/clients/rest_clients/component_base.py index bfe5262ffb..b6eeb7a533 100644 --- a/hikari/clients/rest_clients/component_base.py +++ b/hikari/clients/rest_clients/component_base.py @@ -25,20 +25,20 @@ import typing from hikari.internal import meta -from hikari.net import rest +from hikari.net import rest_sessions class BaseRESTComponent(abc.ABC, metaclass=meta.UniqueFunctionMeta): """An abstract class that all REST client logic classes should inherit from. This defines the abstract method ``__init__`` which will assign an instance - of :obj:`~hikari.net.rest.LowLevelRestfulClient` to the attribute that all + of :obj:`~hikari.net.rest_sessions.LowLevelRestfulClient` to the attribute that all components will expect to make calls to. """ @abc.abstractmethod - def __init__(self, session: rest.LowLevelRestfulClient) -> None: - self._session: rest.LowLevelRestfulClient = session + def __init__(self, session: rest_sessions.LowLevelRestfulClient) -> None: + self._session: rest_sessions.LowLevelRestfulClient = session async def __aenter__(self) -> "BaseRESTComponent": return self @@ -53,6 +53,6 @@ async def close(self) -> None: await self._session.close() @property - def session(self) -> rest.LowLevelRestfulClient: - """Get the :obj:`hikari.net.rest.LowLevelRestfulClient` session object.""" + def session(self) -> rest_sessions.LowLevelRestfulClient: + """Get the :obj:`hikari.net.rest_sessions.LowLevelRestfulClient` session object.""" return self._session diff --git a/hikari/clients/shard_clients.py b/hikari/clients/shard_clients.py index 2c00dd47ce..6b55c6a7b8 100644 --- a/hikari/clients/shard_clients.py +++ b/hikari/clients/shard_clients.py @@ -16,7 +16,7 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . -"""Provides a facade around :obj:`~hikari.net.shard.ShardConnection`. +"""Provides a facade around :obj:`~hikari.net.shards.ShardConnection`. This handles parsing and initializing the object from a configuration, as well as restarting it if it disconnects. @@ -46,7 +46,7 @@ from hikari.internal import more_logging from hikari.net import codes from hikari.net import ratelimits -from hikari.net import shard +from hikari.net import shards from hikari.state import event_dispatchers from hikari.state import raw_event_consumers @@ -148,7 +148,7 @@ def __init__( self._shard_state = ShardState.NOT_RUNNING self._task = None self._dispatcher = dispatcher - self._connection = shard.ShardConnection( + self._connection = shards.ShardConnection( compression=config.gateway_use_compression, connector=config.tcp_connector, debug=config.debug, diff --git a/hikari/internal/pagination.py b/hikari/internal/pagination.py index 552b26c3f6..9fe11ecf32 100644 --- a/hikari/internal/pagination.py +++ b/hikari/internal/pagination.py @@ -47,7 +47,7 @@ async def pagination_handler( direction : :obj:`~typing.Union` [ ``"before"``, ``"after"`` ] The direction that this paginator should go in. request : :obj:`~typing.Callable` [ ``...``, :obj:`~typing.Coroutine` [ :obj:`~typing.Any`, :obj:`~typing.Any`, :obj:`~typing.Any` ] ] - The :obj:`hikari.net.rest.LowLevelRestfulClient` method that should be + The :obj:`hikari.net.rest_sessions.LowLevelRestfulClient` method that should be called to make requests for this paginator. reversing : :obj:`~bool` Whether the retrieved array of objects should be reversed before diff --git a/hikari/net/__init__.py b/hikari/net/__init__.py index 7c49fa7d80..d4bdf295c9 100644 --- a/hikari/net/__init__.py +++ b/hikari/net/__init__.py @@ -23,15 +23,15 @@ """ from hikari.net import codes from hikari.net import ratelimits -from hikari.net import rest +from hikari.net import rest_sessions from hikari.net import routes -from hikari.net import shard -from hikari.net import user_agent +from hikari.net import shards +from hikari.net import user_agents from hikari.net import versions from hikari.net.codes import * -from hikari.net.rest import * -from hikari.net.shard import * +from hikari.net.rest_sessions import * +from hikari.net.shards import * from hikari.net.versions import * -__all__ = codes.__all__ + shard.__all__ + rest.__all__ + versions.__all__ +__all__ = codes.__all__ + shards.__all__ + rest_sessions.__all__ + versions.__all__ diff --git a/hikari/net/rest.py b/hikari/net/rest_sessions.py similarity index 99% rename from hikari/net/rest.py rename to hikari/net/rest_sessions.py index d5a5e4c988..ee44f62dcf 100644 --- a/hikari/net/rest.py +++ b/hikari/net/rest_sessions.py @@ -41,7 +41,7 @@ from hikari.net import codes from hikari.net import ratelimits from hikari.net import routes -from hikari.net import user_agent +from hikari.net import user_agents from hikari.net import versions @@ -235,7 +235,7 @@ def __init__( connector=connector, version=aiohttp.HttpVersion11, json_serialize=json_serialize or json.dumps, ) self.logger = more_logging.get_named_logger(self) - self.user_agent = user_agent.UserAgent().user_agent + self.user_agent = user_agents.UserAgent().user_agent self.verify_ssl = verify_ssl self.proxy_url = proxy_url self.proxy_auth = proxy_auth diff --git a/hikari/net/shard.py b/hikari/net/shards.py similarity index 99% rename from hikari/net/shard.py rename to hikari/net/shards.py index 5bdde8f08a..49405066f0 100644 --- a/hikari/net/shard.py +++ b/hikari/net/shards.py @@ -51,7 +51,7 @@ from hikari.internal import more_logging from hikari.net import codes from hikari.net import ratelimits -from hikari.net import user_agent +from hikari.net import user_agents from hikari.net import versions #: The signature for an event dispatch callback. @@ -704,7 +704,7 @@ async def _identify(self): "token": self._token, "compress": False, "large_threshold": self._large_threshold, - "properties": user_agent.UserAgent().websocket_triplet, + "properties": user_agents.UserAgent().websocket_triplet, "shard": [self.shard_id, self.shard_count], }, } diff --git a/hikari/net/user_agent.py b/hikari/net/user_agents.py similarity index 100% rename from hikari/net/user_agent.py rename to hikari/net/user_agents.py diff --git a/tests/hikari/clients/test_rest_clients/test___init__.py b/tests/hikari/clients/test_rest_clients/test___init__.py index e281f67648..c0c1a90b0e 100644 --- a/tests/hikari/clients/test_rest_clients/test___init__.py +++ b/tests/hikari/clients/test_rest_clients/test___init__.py @@ -23,7 +23,7 @@ from hikari.clients import configs from hikari.clients import rest_clients -from hikari.net import rest +from hikari.net import rest_sessions class TestRESTClient: @@ -33,8 +33,10 @@ def mock_config(self): return configs.RESTConfig(token="blah.blah.blah") def test_init(self, mock_config): - mock_low_level_rest_clients = mock.MagicMock(rest.LowLevelRestfulClient) - with mock.patch.object(rest, "LowLevelRestfulClient", return_value=mock_low_level_rest_clients) as patched_init: + mock_low_level_rest_clients = mock.MagicMock(rest_sessions.LowLevelRestfulClient) + with mock.patch.object( + rest_sessions, "LowLevelRestfulClient", return_value=mock_low_level_rest_clients + ) as patched_init: cli = rest_clients.RESTClient(mock_config) patched_init.assert_called_once_with( allow_redirects=mock_config.allow_redirects, diff --git a/tests/hikari/clients/test_rest_clients/test_channels_component.py b/tests/hikari/clients/test_rest_clients/test_channels_component.py index 5109c02061..f6a41846bd 100644 --- a/tests/hikari/clients/test_rest_clients/test_channels_component.py +++ b/tests/hikari/clients/test_rest_clients/test_channels_component.py @@ -36,14 +36,14 @@ from hikari.internal import allowed_mentions from hikari.internal import conversions from hikari.internal import pagination -from hikari.net import rest +from hikari.net import rest_sessions from tests.hikari import _helpers class TestRESTChannelLogig: @pytest.fixture() def rest_channel_logic_impl(self): - mock_low_level_restful_client = mock.MagicMock(rest.LowLevelRestfulClient) + mock_low_level_restful_client = mock.MagicMock(rest_sessions.LowLevelRestfulClient) class RESTChannelLogicImpl(channels_component.RESTChannelComponent): def __init__(self): diff --git a/tests/hikari/clients/test_rest_clients/test_component_base.py b/tests/hikari/clients/test_rest_clients/test_component_base.py index 5b5f4a573b..dd7297d885 100644 --- a/tests/hikari/clients/test_rest_clients/test_component_base.py +++ b/tests/hikari/clients/test_rest_clients/test_component_base.py @@ -20,13 +20,13 @@ import pytest from hikari.clients.rest_clients import component_base -from hikari.net import rest +from hikari.net import rest_sessions class TestBaseRESTComponent: @pytest.fixture() - def low_level_rest_impl(self) -> rest.LowLevelRestfulClient: - return mock.MagicMock(rest.LowLevelRestfulClient) + def low_level_rest_impl(self) -> rest_sessions.LowLevelRestfulClient: + return mock.MagicMock(rest_sessions.LowLevelRestfulClient) @pytest.fixture() def rest_clients_impl(self, low_level_rest_impl) -> component_base.BaseRESTComponent: diff --git a/tests/hikari/clients/test_rest_clients/test_current_users_component.py b/tests/hikari/clients/test_rest_clients/test_current_users_component.py index bf973ea0e5..e69af64607 100644 --- a/tests/hikari/clients/test_rest_clients/test_current_users_component.py +++ b/tests/hikari/clients/test_rest_clients/test_current_users_component.py @@ -30,14 +30,14 @@ from hikari.clients.rest_clients import current_users_component from hikari.internal import conversions from hikari.internal import pagination -from hikari.net import rest +from hikari.net import rest_sessions from tests.hikari import _helpers class TestRESTInviteLogic: @pytest.fixture() def rest_clients_impl(self): - mock_low_level_restful_client = mock.MagicMock(rest.LowLevelRestfulClient) + mock_low_level_restful_client = mock.MagicMock(rest_sessions.LowLevelRestfulClient) class RESTCurrentUserLogicImpl(current_users_component.RESTCurrentUserComponent): def __init__(self): diff --git a/tests/hikari/clients/test_rest_clients/test_gateways_component.py b/tests/hikari/clients/test_rest_clients/test_gateways_component.py index abce2490a0..ef3a239dd3 100644 --- a/tests/hikari/clients/test_rest_clients/test_gateways_component.py +++ b/tests/hikari/clients/test_rest_clients/test_gateways_component.py @@ -22,13 +22,13 @@ from hikari import gateway_entities from hikari.clients.rest_clients import gateways_component -from hikari.net import rest +from hikari.net import rest_sessions class TestRESTReactionLogic: @pytest.fixture() def rest_gateway_logic_impl(self): - mock_low_level_restful_client = mock.MagicMock(rest.LowLevelRestfulClient) + mock_low_level_restful_client = mock.MagicMock(rest_sessions.LowLevelRestfulClient) class RESTGatewayLogicImpl(gateways_component.RESTGatewayComponent): def __init__(self): diff --git a/tests/hikari/clients/test_rest_clients/test_guilds_component.py b/tests/hikari/clients/test_rest_clients/test_guilds_component.py index fd4ab17217..0cc0a69f6e 100644 --- a/tests/hikari/clients/test_rest_clients/test_guilds_component.py +++ b/tests/hikari/clients/test_rest_clients/test_guilds_component.py @@ -36,7 +36,7 @@ from hikari.clients.rest_clients import guilds_component from hikari.internal import conversions from hikari.internal import pagination -from hikari.net import rest +from hikari.net import rest_sessions from tests.hikari import _helpers @@ -50,7 +50,7 @@ def test__get_member_id(): class TestRESTGuildLogic: @pytest.fixture() def rest_guild_logic_impl(self): - mock_low_level_restful_client = mock.MagicMock(rest.LowLevelRestfulClient) + mock_low_level_restful_client = mock.MagicMock(rest_sessions.LowLevelRestfulClient) class RESTGuildLogicImpl(guilds_component.RESTGuildComponent): def __init__(self): diff --git a/tests/hikari/clients/test_rest_clients/test_invites_component.py b/tests/hikari/clients/test_rest_clients/test_invites_component.py index 8e3d8eccba..1831ca6d74 100644 --- a/tests/hikari/clients/test_rest_clients/test_invites_component.py +++ b/tests/hikari/clients/test_rest_clients/test_invites_component.py @@ -21,13 +21,13 @@ from hikari import invites from hikari.clients.rest_clients import invites_component -from hikari.net import rest +from hikari.net import rest_sessions class TestRESTInviteLogic: @pytest.fixture() def rest_invite_logic_impl(self): - mock_low_level_restful_client = mock.MagicMock(rest.LowLevelRestfulClient) + mock_low_level_restful_client = mock.MagicMock(rest_sessions.LowLevelRestfulClient) class RESTInviteLogicImpl(invites_component.RESTInviteComponent): def __init__(self): diff --git a/tests/hikari/clients/test_rest_clients/test_oauth2_component.py b/tests/hikari/clients/test_rest_clients/test_oauth2_component.py index 80daf14583..d6da8623b7 100644 --- a/tests/hikari/clients/test_rest_clients/test_oauth2_component.py +++ b/tests/hikari/clients/test_rest_clients/test_oauth2_component.py @@ -23,13 +23,13 @@ from hikari import oauth2 from hikari.clients.rest_clients import oauth2_component -from hikari.net import rest +from hikari.net import rest_sessions class TestRESTReactionLogic: @pytest.fixture() def rest_oauth2_logic_impl(self): - mock_low_level_restful_client = mock.MagicMock(rest.LowLevelRestfulClient) + mock_low_level_restful_client = mock.MagicMock(rest_sessions.LowLevelRestfulClient) class RESTOauth2LogicImpl(oauth2_component.RESTOauth2Component): def __init__(self): diff --git a/tests/hikari/clients/test_rest_clients/test_reactions_component.py b/tests/hikari/clients/test_rest_clients/test_reactions_component.py index 73b9de0549..466eceb7a5 100644 --- a/tests/hikari/clients/test_rest_clients/test_reactions_component.py +++ b/tests/hikari/clients/test_rest_clients/test_reactions_component.py @@ -27,14 +27,14 @@ from hikari import users from hikari.clients.rest_clients import reactions_component from hikari.internal import pagination -from hikari.net import rest +from hikari.net import rest_sessions from tests.hikari import _helpers class TestRESTReactionLogic: @pytest.fixture() def rest_reaction_logic_impl(self): - mock_low_level_restful_client = mock.MagicMock(rest.LowLevelRestfulClient) + mock_low_level_restful_client = mock.MagicMock(rest_sessions.LowLevelRestfulClient) class RESTReactionLogicImpl(reactions_component.RESTReactionComponent): def __init__(self): diff --git a/tests/hikari/clients/test_rest_clients/test_users_component.py b/tests/hikari/clients/test_rest_clients/test_users_component.py index 3c6f8ee4cf..f4a5eb5f85 100644 --- a/tests/hikari/clients/test_rest_clients/test_users_component.py +++ b/tests/hikari/clients/test_rest_clients/test_users_component.py @@ -21,14 +21,14 @@ from hikari import users from hikari.clients.rest_clients import users_component -from hikari.net import rest +from hikari.net import rest_sessions from tests.hikari import _helpers class TestRESTUserLogic: @pytest.fixture() def rest_user_logic_impl(self): - mock_low_level_restful_client = mock.MagicMock(rest.LowLevelRestfulClient) + mock_low_level_restful_client = mock.MagicMock(rest_sessions.LowLevelRestfulClient) class RESTUserLogicImpl(users_component.RESTUserComponent): def __init__(self): diff --git a/tests/hikari/clients/test_rest_clients/test_voices_component.py b/tests/hikari/clients/test_rest_clients/test_voices_component.py index a9df770d2c..af69f78c5f 100644 --- a/tests/hikari/clients/test_rest_clients/test_voices_component.py +++ b/tests/hikari/clients/test_rest_clients/test_voices_component.py @@ -21,13 +21,13 @@ from hikari import voices from hikari.clients.rest_clients import voices_component -from hikari.net import rest +from hikari.net import rest_sessions class TestRESTUserLogic: @pytest.fixture() def rest_voice_logic_impl(self): - mock_low_level_restful_client = mock.MagicMock(rest.LowLevelRestfulClient) + mock_low_level_restful_client = mock.MagicMock(rest_sessions.LowLevelRestfulClient) class RESTVoiceLogicImpl(voices_component.RESTVoiceComponent): def __init__(self): diff --git a/tests/hikari/clients/test_rest_clients/test_webhooks_component.py b/tests/hikari/clients/test_rest_clients/test_webhooks_component.py index 191e074750..420d161b20 100644 --- a/tests/hikari/clients/test_rest_clients/test_webhooks_component.py +++ b/tests/hikari/clients/test_rest_clients/test_webhooks_component.py @@ -29,14 +29,14 @@ from hikari.clients.rest_clients import webhooks_component from hikari.internal import allowed_mentions from hikari.internal import conversions -from hikari.net import rest +from hikari.net import rest_sessions from tests.hikari import _helpers class TestRESTUserLogic: @pytest.fixture() def rest_webhook_logic_impl(self): - mock_low_level_restful_client = mock.MagicMock(rest.LowLevelRestfulClient) + mock_low_level_restful_client = mock.MagicMock(rest_sessions.LowLevelRestfulClient) class RESTWebhookLogicImpl(webhooks_component.RESTWebhookComponent): def __init__(self): diff --git a/tests/hikari/clients/test_shard_clients.py b/tests/hikari/clients/test_shard_clients.py index 00e254e86c..50fc072c19 100644 --- a/tests/hikari/clients/test_shard_clients.py +++ b/tests/hikari/clients/test_shard_clients.py @@ -28,7 +28,7 @@ from hikari.clients import configs from hikari.clients import shard_clients from hikari.net import codes -from hikari.net import shard +from hikari.net import shards from hikari.state import raw_event_consumers from tests.hikari import _helpers @@ -48,14 +48,14 @@ def exception(self): @pytest.fixture def shard_client_obj(): mock_shard_connection = mock.MagicMock( - shard.ShardConnection, + shards.ShardConnection, heartbeat_latency=float("nan"), heartbeat_interval=float("nan"), reconnect_count=0, seq=None, session_id=None, ) - with mock.patch("hikari.net.shard.ShardConnection", return_value=mock_shard_connection): + with mock.patch("hikari.net.shards.ShardConnection", return_value=mock_shard_connection): return _helpers.unslot_class(shard_clients.ShardClient)(0, 1, configs.WebsocketConfig(), None, "some_url") @@ -70,9 +70,9 @@ def process_raw_event(self, _client, name, payload): assert shard_client_obj._connection.dispatch(shard_client_obj, "TEST", {}) == "ASSERT TRUE" def test_connection_is_set(self, shard_client_obj): - mock_shard_connection = mock.MagicMock(shard.ShardConnection) + mock_shard_connection = mock.MagicMock(shards.ShardConnection) - with mock.patch("hikari.net.shard.ShardConnection", return_value=mock_shard_connection): + with mock.patch("hikari.net.shards.ShardConnection", return_value=mock_shard_connection): shard_client_obj = shard_clients.ShardClient(0, 1, configs.WebsocketConfig(), None, "some_url") assert shard_client_obj._connection is mock_shard_connection diff --git a/tests/hikari/net/test_rest.py b/tests/hikari/net/test_rest_sessions.py similarity index 99% rename from tests/hikari/net/test_rest.py rename to tests/hikari/net/test_rest_sessions.py index ee425d6175..4dcfe9fe37 100644 --- a/tests/hikari/net/test_rest.py +++ b/tests/hikari/net/test_rest_sessions.py @@ -29,7 +29,7 @@ from hikari import errors from hikari.internal import conversions from hikari.net import ratelimits -from hikari.net import rest +from hikari.net import rest_sessions from hikari.net import routes from hikari.net import versions from tests.hikari import _helpers @@ -44,7 +44,9 @@ def rest_impl(self): stack.enter_context(mock.patch.object(ratelimits, "RESTBucketManager")) stack.enter_context(mock.patch.object(ratelimits, "ManualRateLimiter")) with stack: - client = rest.LowLevelRestfulClient(base_url="https://discordapp.com/api/v6", token="Bot blah.blah.blah") + client = rest_sessions.LowLevelRestfulClient( + base_url="https://discordapp.com/api/v6", token="Bot blah.blah.blah" + ) client._request = mock.AsyncMock(return_value=...) return client @@ -111,7 +113,7 @@ async def test__init__with_bot_token_and_without_optionals(self): stack.enter_context(mock.patch.object(aiohttp, "ClientSession", return_value=mock_client_session)) with stack: - client = rest.LowLevelRestfulClient(token="Bot token.otacon.a-token") + client = rest_sessions.LowLevelRestfulClient(token="Bot token.otacon.a-token") assert client.base_url == f"https://discordapp.com/api/v{int(versions.HTTPAPIVersion.STABLE)}" assert client.client_session is mock_client_session @@ -128,7 +130,7 @@ async def test__init__with_bearer_token_and_without_optionals(self): stack.enter_context(mock.patch.object(ratelimits, "RESTBucketManager")) stack.enter_context(mock.patch.object(aiohttp, "ClientSession")) with stack: - client = rest.LowLevelRestfulClient(token="Bearer token.otacon.a-token") + client = rest_sessions.LowLevelRestfulClient(token="Bearer token.otacon.a-token") assert client.token == "Bearer token.otacon.a-token" @pytest.mark.asyncio @@ -150,7 +152,7 @@ async def test__init__with_optionals(self): ) with stack: - client = rest.LowLevelRestfulClient( + client = rest_sessions.LowLevelRestfulClient( token="Bot token.otacon.a-token", base_url="https://discordapp.com/api/v69420", allow_redirects=True, @@ -179,7 +181,7 @@ async def test__init__raises_runtime_error_with_invalid_token(self, *_): stack.enter_context(mock.patch.object(ratelimits, "RESTBucketManager")) stack.enter_context(mock.patch.object(aiohttp, "ClientSession")) with stack: - async with rest.LowLevelRestfulClient(token="An-invalid-TOKEN"): + async with rest_sessions.LowLevelRestfulClient(token="An-invalid-TOKEN"): pass @pytest.mark.asyncio @@ -193,7 +195,7 @@ async def test_close(self, rest_impl): @mock.patch.object(ratelimits, "RESTBucketManager") @mock.patch.object(aiohttp, "ClientSession") def rest_impl_with__request(self, *args): - rest_impl = rest.LowLevelRestfulClient(token="Bot token") + rest_impl = rest_sessions.LowLevelRestfulClient(token="Bot token") rest_impl.logger = mock.MagicMock(debug=mock.MagicMock()) rest_impl.ratelimiter = mock.MagicMock( ratelimits.RESTBucketManager, acquire=mock.MagicMock(), update_rate_limits=mock.MagicMock(), @@ -427,7 +429,7 @@ async def test__request_when_TOO_MANY_REQUESTS_when_not_global( rest_impl_with__request.logger.debug.side_effect = [None, exit_error] with mock.patch("asyncio.gather", return_value=_helpers.AwaitableMock()): - with mock.patch.object(rest.LowLevelRestfulClient, "_request", return_value=discord_response): + with mock.patch.object(rest_sessions.LowLevelRestfulClient, "_request", return_value=discord_response): try: await rest_impl_with__request._request(compiled_route) except exit_error: @@ -456,7 +458,7 @@ async def test__request_raises_appropriate_error_for_status_code( stack.enter_context(mock.patch.object(aiohttp, "ClientSession")) discord_response.status = status_code with stack: - rest_impl = rest.LowLevelRestfulClient(token="Bot token", version=api_version) + rest_impl = rest_sessions.LowLevelRestfulClient(token="Bot token", version=api_version) rest_impl.ratelimiter = mock.MagicMock() rest_impl.global_ratelimiter = mock.MagicMock() rest_impl.client_session.request = mock.MagicMock(return_value=discord_response) diff --git a/tests/hikari/net/test_shard.py b/tests/hikari/net/test_shards.py similarity index 92% rename from tests/hikari/net/test_shard.py rename to tests/hikari/net/test_shards.py index 132d7c5b8e..41e5c56c4d 100644 --- a/tests/hikari/net/test_shard.py +++ b/tests/hikari/net/test_shards.py @@ -30,8 +30,8 @@ from hikari import errors from hikari.internal import more_collections -from hikari.net import shard -from hikari.net import user_agent +from hikari.net import shards +from hikari.net import user_agents from hikari.net import versions from tests.hikari import _helpers @@ -86,12 +86,12 @@ async def ws_connect(self, *args, **kwargs): class TestShardConstructor: async def test_init_sets_shard_numbers_correctly(self,): input_shard_id, input_shard_count, expected_shard_id, expected_shard_count = 1, 2, 1, 2 - client = shard.ShardConnection(shard_id=input_shard_id, shard_count=input_shard_count, token="xxx", url="yyy") + client = shards.ShardConnection(shard_id=input_shard_id, shard_count=input_shard_count, token="xxx", url="yyy") assert client.shard_id == expected_shard_id assert client.shard_count == expected_shard_count async def test_dispatch_is_callable(self): - client = shard.ShardConnection(token="xxx", url="yyy") + client = shards.ShardConnection(token="xxx", url="yyy") client.dispatch(client, "ping", "pong") @pytest.mark.parametrize( @@ -103,7 +103,7 @@ async def test_dispatch_is_callable(self): ) async def test_compression(self, compression, expected_url_query): url = "ws://baka-im-not-a-http-url:49620/locate/the/bloody/websocket?ayyyyy=lmao" - client = shard.ShardConnection(token="xxx", url=url, compression=compression) + client = shards.ShardConnection(token="xxx", url=url, compression=compression) scheme, netloc, path, params, query, fragment = urllib.parse.urlparse(client._url) assert scheme == "ws" assert netloc == "baka-im-not-a-http-url:49620" @@ -114,13 +114,13 @@ async def test_compression(self, compression, expected_url_query): assert fragment == "" async def test_init_hearbeat_defaults_before_startup(self): - client = shard.ShardConnection(token="xxx", url="yyy") + client = shards.ShardConnection(token="xxx", url="yyy") assert math.isnan(client.last_heartbeat_sent) assert math.isnan(client.heartbeat_latency) assert math.isnan(client.last_message_received) async def test_init_connected_at_is_nan(self): - client = shard.ShardConnection(token="xxx", url="yyy") + client = shards.ShardConnection(token="xxx", url="yyy") assert math.isnan(client._connected_at) @@ -132,7 +132,7 @@ class TestShardUptimeProperty: ) async def test_uptime(self, connected_at, now, expected_uptime): with mock.patch("time.perf_counter", return_value=now): - client = shard.ShardConnection(token="xxx", url="yyy") + client = shards.ShardConnection(token="xxx", url="yyy") client._connected_at = connected_at assert client.uptime == expected_uptime @@ -141,7 +141,7 @@ async def test_uptime(self, connected_at, now, expected_uptime): class TestShardIsConnectedProperty: @pytest.mark.parametrize(["connected_at", "is_connected"], [(float("nan"), False), (15, True), (2500.0, True),]) async def test_is_connected(self, connected_at, is_connected): - client = shard.ShardConnection(token="xxx", url="yyy") + client = shards.ShardConnection(token="xxx", url="yyy") client._connected_at = connected_at assert client.is_connected is is_connected @@ -162,7 +162,7 @@ class TestGatewayReconnectCountProperty: ], ) async def test_value(self, disconnect_count, is_connected, expected_reconnect_count): - client = shard.ShardConnection(token="xxx", url="yyy") + client = shards.ShardConnection(token="xxx", url="yyy") client.disconnect_count = disconnect_count client._connected_at = 420 if is_connected else float("nan") assert client.reconnect_count == expected_reconnect_count @@ -171,12 +171,12 @@ async def test_value(self, disconnect_count, is_connected, expected_reconnect_co @pytest.mark.asyncio class TestGatewayCurrentPresenceProperty: async def test_returns_presence(self): - client = shard.ShardConnection(token="xxx", url="yyy") + client = shards.ShardConnection(token="xxx", url="yyy") client._presence = {"foo": "bar"} assert client.current_presence == {"foo": "bar"} async def test_returns_copy(self): - client = shard.ShardConnection(token="xxx", url="yyy") + client = shards.ShardConnection(token="xxx", url="yyy") client._presence = {"foo": "bar"} assert client.current_presence is not client._presence @@ -186,7 +186,7 @@ class TestShardAiohttpClientSessionKwargsProperty: async def test_right_stuff_is_included(self): connector = mock.MagicMock() - client = shard.ShardConnection(url="...", token="...", connector=connector,) + client = shards.ShardConnection(url="...", token="...", connector=connector,) assert client._cs_init_kwargs() == dict(connector=connector) @@ -201,7 +201,7 @@ async def test_right_stuff_is_included(self): verify_ssl = True ssl_context = mock.MagicMock() - client = shard.ShardConnection( + client = shards.ShardConnection( url=url, token="...", proxy_url=proxy_url, @@ -248,7 +248,7 @@ def non_hello_payload(self): @pytest.fixture def client(self, event_loop): asyncio.set_event_loop(event_loop) - client = _helpers.unslot_class(shard.ShardConnection)(url="ws://localhost", token="xxx") + client = _helpers.unslot_class(shards.ShardConnection)(url="ws://localhost", token="xxx") client = _helpers.mock_methods_on(client, except_=("connect", "_cs_init_kwargs", "_ws_connect_kwargs")) client._receive = mock.AsyncMock(return_value=self.hello_payload) return client @@ -537,7 +537,7 @@ class TestShardRun: @pytest.fixture def client(self, event_loop): asyncio.set_event_loop(event_loop) - client = _helpers.unslot_class(shard.ShardConnection)(token="1234", url="xxx") + client = _helpers.unslot_class(shards.ShardConnection)(token="1234", url="xxx") client = _helpers.mock_methods_on(client, except_=("_run",)) def receive(): @@ -549,9 +549,9 @@ def identify(): def resume(): client.resume_time = time.perf_counter() - client._identify = mock.AsyncMock(spec=shard.ShardConnection._identify, wraps=identify) - client._resume = mock.AsyncMock(spec=shard.ShardConnection._resume, wraps=resume) - client._receive = mock.AsyncMock(spec=shard.ShardConnection._receive, wraps=receive) + client._identify = mock.AsyncMock(spec=shards.ShardConnection._identify, wraps=identify) + client._resume = mock.AsyncMock(spec=shards.ShardConnection._resume, wraps=resume) + client._receive = mock.AsyncMock(spec=shards.ShardConnection._receive, wraps=receive) return client async def test_no_session_id_sends_identify_then_polls_events(self, client): @@ -584,7 +584,7 @@ class TestIdentify: @pytest.fixture def client(self, event_loop): asyncio.set_event_loop(event_loop) - client = _helpers.unslot_class(shard.ShardConnection)(token="1234", url="xxx") + client = _helpers.unslot_class(shards.ShardConnection)(token="1234", url="xxx") client = _helpers.mock_methods_on(client, except_=("_identify",)) return client @@ -606,7 +606,7 @@ async def test_identify_payload_no_intents_no_presence(self, client): "token": "aaaa", "compress": False, "large_threshold": 420, - "properties": user_agent.UserAgent().websocket_triplet, + "properties": user_agents.UserAgent().websocket_triplet, "shard": [69, 96], }, } @@ -631,7 +631,7 @@ async def test_identify_payload_with_presence(self, client): "token": "aaaa", "compress": False, "large_threshold": 420, - "properties": user_agent.UserAgent().websocket_triplet, + "properties": user_agents.UserAgent().websocket_triplet, "shard": [69, 96], "presence": presence, }, @@ -657,7 +657,7 @@ async def test_identify_payload_with_intents(self, client): "token": "aaaa", "compress": False, "large_threshold": 420, - "properties": user_agent.UserAgent().websocket_triplet, + "properties": user_agents.UserAgent().websocket_triplet, "shard": [69, 96], "intents": intents, }, @@ -684,7 +684,7 @@ async def test_identify_payload_with_intents_and_presence(self, client): "token": "aaaa", "compress": False, "large_threshold": 420, - "properties": user_agent.UserAgent().websocket_triplet, + "properties": user_agents.UserAgent().websocket_triplet, "shard": [69, 96], "intents": intents, "presence": presence, @@ -698,7 +698,7 @@ class TestResume: @pytest.fixture def client(self, event_loop): asyncio.set_event_loop(event_loop) - client = _helpers.unslot_class(shard.ShardConnection)(token="1234", url="xxx") + client = _helpers.unslot_class(shards.ShardConnection)(token="1234", url="xxx") client = _helpers.mock_methods_on(client, except_=("_resume",)) return client @@ -719,7 +719,7 @@ class TestHeartbeatKeepAlive: @pytest.fixture def client(self, event_loop): asyncio.set_event_loop(event_loop) - client = _helpers.unslot_class(shard.ShardConnection)(token="1234", url="xxx") + client = _helpers.unslot_class(shards.ShardConnection)(token="1234", url="xxx") client = _helpers.mock_methods_on(client, except_=("_heartbeat_keep_alive", "_zombie_detector")) client._send = mock.AsyncMock() # This won't get set on the right event loop if we are not careful @@ -780,14 +780,14 @@ async def test_zombie_detector_not_a_zombie(self): client = mock.MagicMock() client.last_message_received = time.perf_counter() - 5 heartbeat_interval = 41.25 - shard.ShardConnection._zombie_detector(client, heartbeat_interval) + shards.ShardConnection._zombie_detector(client, heartbeat_interval) @_helpers.assert_raises(type_=asyncio.TimeoutError) async def test_zombie_detector_is_a_zombie(self): client = mock.MagicMock() client.last_message_received = time.perf_counter() - 500000 heartbeat_interval = 41.25 - shard.ShardConnection._zombie_detector(client, heartbeat_interval) + shards.ShardConnection._zombie_detector(client, heartbeat_interval) @pytest.mark.asyncio @@ -795,7 +795,7 @@ class TestClose: @pytest.fixture def client(self, event_loop): asyncio.set_event_loop(event_loop) - client = _helpers.unslot_class(shard.ShardConnection)(token="1234", url="xxx") + client = _helpers.unslot_class(shards.ShardConnection)(token="1234", url="xxx") client = _helpers.mock_methods_on(client, except_=("close",)) client.ws = mock.MagicMock(aiohttp.ClientWebSocketResponse) client.session = mock.MagicMock(aiohttp.ClientSession) @@ -866,7 +866,7 @@ class TestPollEvents: @pytest.fixture def client(self, event_loop): asyncio.set_event_loop(event_loop) - client = _helpers.unslot_class(shard.ShardConnection)(token="1234", url="xxx") + client = _helpers.unslot_class(shards.ShardConnection)(token="1234", url="xxx") client = _helpers.mock_methods_on(client, except_=("_run",)) return client @@ -906,7 +906,7 @@ class TestRequestGuildMembers: @pytest.fixture def client(self, event_loop): asyncio.set_event_loop(event_loop) - client = _helpers.unslot_class(shard.ShardConnection)(token="1234", url="xxx") + client = _helpers.unslot_class(shards.ShardConnection)(token="1234", url="xxx") client = _helpers.mock_methods_on(client, except_=("request_guild_members",)) return client @@ -943,7 +943,7 @@ class TestUpdatePresence: @pytest.fixture def client(self, event_loop): asyncio.set_event_loop(event_loop) - client = _helpers.unslot_class(shard.ShardConnection)(token="1234", url="xxx") + client = _helpers.unslot_class(shards.ShardConnection)(token="1234", url="xxx") client = _helpers.mock_methods_on(client, except_=("update_presence",)) return client diff --git a/tests/hikari/net/test_user_agent.py b/tests/hikari/net/test_user_agents.py similarity index 67% rename from tests/hikari/net/test_user_agent.py rename to tests/hikari/net/test_user_agents.py index fda87c48da..c10e2388b9 100644 --- a/tests/hikari/net/test_user_agent.py +++ b/tests/hikari/net/test_user_agents.py @@ -16,24 +16,24 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . -from hikari.net import user_agent +from hikari.net import user_agents def test_library_version_is_callable_and_produces_string(): - assert isinstance(user_agent.UserAgent().library_version, str) + assert isinstance(user_agents.UserAgent().library_version, str) def test_platform_version_is_callable_and_produces_string(): - assert isinstance(user_agent.UserAgent().platform_version, str) + assert isinstance(user_agents.UserAgent().platform_version, str) def test_system_type_produces_string(): - assert isinstance(user_agent.UserAgent().system_type, str) + assert isinstance(user_agents.UserAgent().system_type, str) def test_websocket_triplet_produces_trio(): - assert user_agent.UserAgent().websocket_triplet == { - "$os": user_agent.UserAgent().system_type, - "$browser": user_agent.UserAgent().library_version, - "$device": user_agent.UserAgent().platform_version, + assert user_agents.UserAgent().websocket_triplet == { + "$os": user_agents.UserAgent().system_type, + "$browser": user_agents.UserAgent().library_version, + "$device": user_agents.UserAgent().platform_version, } From 45f59391c8278cf6078e6d0674076012ebe082ba Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Sat, 18 Apr 2020 15:13:01 +0100 Subject: [PATCH 163/922] Fixed bug in test_client causing error. --- hikari/clients/test_client.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/hikari/clients/test_client.py b/hikari/clients/test_client.py index ee141ba729..63b7c6b52d 100644 --- a/hikari/clients/test_client.py +++ b/hikari/clients/test_client.py @@ -137,12 +137,12 @@ async def on_message(event: hikari.MessageCreateEvent) -> None: shard_infos.append(hikari.EmbedField(name=f"Shard {shard_id}", value=shard_info, is_inline=False)) gw_info = ( - f"average latency: {client.gateway.latency * 1_000:.0f} ms\n" + f"intents: {client.gateway.intents}\n" + f"version: {client.gateway.version}\n" + f"average latency: {client.gateway.heartbeat_latency * 1_000:.0f} ms\n" f"shards: {len(client.gateway.shards)}\n" - f"version: {version}\n" f"compression: {compression}\n" f"debug: {debug}\n" - f"intents: {hex(intents)} ({active_intents})" ) rest_info = ( From 4c02116ff337b4c53ae3f033bd4d7f358ebf0530 Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Sat, 18 Apr 2020 15:13:28 +0100 Subject: [PATCH 164/922] Removed 'versions' module as it was pointless. --- hikari/net/__init__.py | 4 +- hikari/net/rest_sessions.py | 31 ++++++++------- hikari/net/shards.py | 15 +++++--- hikari/net/versions.py | 52 -------------------------- tests/hikari/net/test_rest_sessions.py | 6 +-- tests/hikari/net/test_shards.py | 7 ++-- 6 files changed, 33 insertions(+), 82 deletions(-) delete mode 100644 hikari/net/versions.py diff --git a/hikari/net/__init__.py b/hikari/net/__init__.py index d4bdf295c9..7e662c2fac 100644 --- a/hikari/net/__init__.py +++ b/hikari/net/__init__.py @@ -27,11 +27,9 @@ from hikari.net import routes from hikari.net import shards from hikari.net import user_agents -from hikari.net import versions from hikari.net.codes import * from hikari.net.rest_sessions import * from hikari.net.shards import * -from hikari.net.versions import * -__all__ = codes.__all__ + shards.__all__ + rest_sessions.__all__ + versions.__all__ +__all__ = codes.__all__ + shards.__all__ + rest_sessions.__all__ diff --git a/hikari/net/rest_sessions.py b/hikari/net/rest_sessions.py index ee44f62dcf..10c8f062a3 100644 --- a/hikari/net/rest_sessions.py +++ b/hikari/net/rest_sessions.py @@ -42,7 +42,10 @@ from hikari.net import ratelimits from hikari.net import routes from hikari.net import user_agents -from hikari.net import versions + + +VERSION_6: typing.Final[int] = 6 +VERSION_7: typing.Final[int] = 7 class LowLevelRestfulClient: @@ -83,20 +86,20 @@ class LowLevelRestfulClient: If this is passed as :obj:`~None`, then no token is used. This will be passed as the ``Authorization`` header if not :obj:`~None` for each request. - version: :obj:`~typing.Union` [ :obj:`~int`, :obj:`~hikari.net.versions.HTTPAPIVersion` ] + version: :obj:`~int` The version of the API to use. Defaults to the most recent stable - version. + version (v6). """ - GET = "get" - POST = "post" - PATCH = "patch" - PUT = "put" - HEAD = "head" - DELETE = "delete" - OPTIONS = "options" + GET: typing.Final[str] = "get" + POST: typing.Final[str] = "post" + PATCH: typing.Final[str] = "patch" + PUT: typing.Final[str] = "put" + HEAD: typing.Final[str] = "head" + DELETE: typing.Final[str] = "delete" + OPTIONS: typing.Final[str] = "options" - _AUTHENTICATION_SCHEMES = ("Bearer", "Bot") + _AUTHENTICATION_SCHEMES: typing.Final[typing.Tuple[str, ...]] = ("Bearer", "Bot") #: :obj:`~True` if HTTP redirects are enabled, or :obj:`~False` otherwise. #: @@ -228,7 +231,7 @@ def __init__( json_deserialize: typing.Callable[[typing.AnyStr], typing.Dict] = json.loads, json_serialize: typing.Callable[[typing.Dict], typing.AnyStr] = json.dumps, token: typing.Optional[str], - version: typing.Union[int, versions.HTTPAPIVersion] = versions.HTTPAPIVersion.STABLE, + version: int = VERSION_6, ) -> None: self.allow_redirects = allow_redirects self.client_session = aiohttp.ClientSession( @@ -243,7 +246,7 @@ def __init__( self.ssl_context: ssl.SSLContext = ssl_context self.timeout = timeout self.in_count = 0 - self.version = int(version) + self.version = version self.base_url = base_url.format(self) self.global_ratelimiter = ratelimits.ManualRateLimiter() self.json_serialize = json_serialize @@ -410,7 +413,7 @@ async def _request( if body is None: message = raw_body - elif self.version == versions.HTTPAPIVersion.V6: + elif self.version == VERSION_7: message = ", ".join(f"{k} - {v}" for k, v in body.items()) else: message = body.get("message") diff --git a/hikari/net/shards.py b/hikari/net/shards.py index 49405066f0..dc540e99d1 100644 --- a/hikari/net/shards.py +++ b/hikari/net/shards.py @@ -52,12 +52,15 @@ from hikari.net import codes from hikari.net import ratelimits from hikari.net import user_agents -from hikari.net import versions #: The signature for an event dispatch callback. DispatchT = typing.Callable[["ShardConnection", str, typing.Dict], None] +VERSION_6: typing.Final[int] = 6 +VERSION_7: typing.Final[int] = 7 + + class ShardConnection: """Implementation of a client for the Discord Gateway. @@ -150,9 +153,9 @@ class ShardConnection: If :obj:`~True`, SSL verification is enabled, which is generally what you want. If you get SSL issues, you can try turning this off at your own risk. - version: :obj:`~hikari.net.versions.GatewayVersion` - The version of the gateway API to use. Defaults to the most recent - stable documented version. + version: :obj:`~int` + The version of the API to use. Defaults to the most recent stable + version (v6). """ __slots__ = ( @@ -334,7 +337,7 @@ def __init__( token: str, url: str, verify_ssl: bool = True, - version: typing.Union[int, versions.GatewayVersion] = versions.GatewayVersion.STABLE, + version: int = VERSION_6, ) -> None: # Sanitise the URL... scheme, netloc, path, params, _, _ = urllib.parse.urlparse(url, allow_fragments=True) @@ -386,7 +389,7 @@ def __init__( self.seq: typing.Optional[int] = seq self.shard_id: int = shard_id self.shard_count: int = shard_count - self.version: int = int(version) + self.version: int = version self.logger: logging.Logger = more_logging.get_named_logger(self, f"#{shard_id}", f"v{self.version}") diff --git a/hikari/net/versions.py b/hikari/net/versions.py deleted file mode 100644 index 880bcd1b67..0000000000 --- a/hikari/net/versions.py +++ /dev/null @@ -1,52 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# Copyright © Nekokatt 2019-2020 -# -# This file is part of Hikari. -# -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . -"""API version enumeration.""" -__all__ = ["HTTPAPIVersion", "GatewayVersion"] - -import enum - - -class HTTPAPIVersion(enum.IntEnum): - """Supported versions for the REST API.""" - - #: The V6 API. This is currently the stable release that should be used unless you have a reason - #: to use V7 otherwise. - V6 = 6 - - #: Development API version. This is not documented at the time of writing, and is subject to - #: change at any time without warning. - V7 = 7 - - #: The recommended stable API release to default to. - STABLE = V6 - - -class GatewayVersion(enum.IntEnum): - """Supported versions for the Gateway.""" - - #: The V6 API. This is currently the stable release that should be used unless you have a reason - #: to use V7 otherwise. - V6 = 6 - - #: Development API version. This is not documented at the time of writing, and is subject to - #: change at any time without warning. - V7 = 7 - - #: The recommended stable API release to default to. - STABLE = V6 diff --git a/tests/hikari/net/test_rest_sessions.py b/tests/hikari/net/test_rest_sessions.py index 4dcfe9fe37..626bd52b70 100644 --- a/tests/hikari/net/test_rest_sessions.py +++ b/tests/hikari/net/test_rest_sessions.py @@ -31,10 +31,10 @@ from hikari.net import ratelimits from hikari.net import rest_sessions from hikari.net import routes -from hikari.net import versions from tests.hikari import _helpers +# noinspection PyUnresolvedReferences class TestLowLevelRestfulClient: @pytest.fixture def rest_impl(self): @@ -115,7 +115,7 @@ async def test__init__with_bot_token_and_without_optionals(self): with stack: client = rest_sessions.LowLevelRestfulClient(token="Bot token.otacon.a-token") - assert client.base_url == f"https://discordapp.com/api/v{int(versions.HTTPAPIVersion.STABLE)}" + assert client.base_url == f"https://discordapp.com/api/v{rest_sessions.VERSION_6}" assert client.client_session is mock_client_session assert client.global_ratelimiter is mock_manual_rate_limiter assert client.json_serialize is json.dumps @@ -438,7 +438,7 @@ async def test__request_when_TOO_MANY_REQUESTS_when_not_global( rest_impl_with__request.global_ratelimiter.throttle.assert_not_called() @pytest.mark.asyncio - @pytest.mark.parametrize("api_version", [versions.HTTPAPIVersion.V6, versions.HTTPAPIVersion.V7]) + @pytest.mark.parametrize("api_version", [6, 7]) @pytest.mark.parametrize( ["status_code", "error"], [ diff --git a/tests/hikari/net/test_shards.py b/tests/hikari/net/test_shards.py index 41e5c56c4d..94f5da279e 100644 --- a/tests/hikari/net/test_shards.py +++ b/tests/hikari/net/test_shards.py @@ -32,7 +32,6 @@ from hikari.internal import more_collections from hikari.net import shards from hikari.net import user_agents -from hikari.net import versions from tests.hikari import _helpers @@ -84,7 +83,7 @@ async def ws_connect(self, *args, **kwargs): @pytest.mark.asyncio class TestShardConstructor: - async def test_init_sets_shard_numbers_correctly(self,): + async def test_init_sets_shard_numbers_correctly(self): input_shard_id, input_shard_count, expected_shard_id, expected_shard_count = 1, 2, 1, 2 client = shards.ShardConnection(shard_id=input_shard_id, shard_count=input_shard_count, token="xxx", url="yyy") assert client.shard_id == expected_shard_id @@ -209,12 +208,12 @@ async def test_right_stuff_is_included(self): proxy_headers=proxy_headers, verify_ssl=verify_ssl, ssl_context=ssl_context, - version=versions.GatewayVersion.STABLE, + version=6, ) scheme, netloc, url, params, query, fragment = urllib.parse.urlparse(client._url) query = urllib.parse.parse_qs(query) - assert query["v"] == [str(versions.GatewayVersion.STABLE.value)] + assert query["v"] == ["6"] client._url = url From 27d1c2868b141227b1dc06804de87950136c0b1b Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Sat, 18 Apr 2020 15:20:05 +0100 Subject: [PATCH 165/922] Fixed bug in IDEs for typing of UNSET --- hikari/clients/bot_clients.py | 18 +++++++++++------- hikari/unset.py | 2 +- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/hikari/clients/bot_clients.py b/hikari/clients/bot_clients.py index fd2c36ce71..114ef0cdc2 100644 --- a/hikari/clients/bot_clients.py +++ b/hikari/clients/bot_clients.py @@ -59,6 +59,11 @@ class BotBase(runnable.RunnableClient, event_dispatchers.EventDispatcher): #: :type: a subclass of :obj:`~hikari.state.event_managers.EventManager` event_manager: event_managers.EventManager + #: The logger to use for this bot. + #: + #: :type: :obj:`~logging.Logger` + logger: logging.Logger + #: The gateway for this bot. #: #: Note @@ -68,11 +73,6 @@ class BotBase(runnable.RunnableClient, event_dispatchers.EventDispatcher): #: :type: :obj:`~hikari.clients.gateway_managers.GatewayManager` [ :obj:`~hikari.clients.shard_clients.ShardClient` ] gateway: unset.MayBeUnset[gateway_managers.GatewayManager[shard_clients.ShardClient]] - #: The logger to use for this bot. - #: - #: :type: :obj:`~logging.Logger` - logger: logging.Logger - #: The REST HTTP client to use for this bot. #: #: Note @@ -83,12 +83,16 @@ class BotBase(runnable.RunnableClient, event_dispatchers.EventDispatcher): rest: unset.MayBeUnset[rest_clients.RESTClient] @abc.abstractmethod + @typing.no_type_check def __init__(self, config: configs.BotConfig, event_manager: event_managers.EventManager) -> None: super().__init__(more_logging.get_named_logger(self)) self.config = config self.event_manager = event_manager - self.gateway = unset.UNSET - self.rest = unset.UNSET + + # Use the typing.cast to fix issue in some linters where they give precedence to the assigned + # value over the type hint. + self.rest = typing.cast(rest_clients.RESTClient, unset.UNSET) + self.gateway = typing.cast(gateway_managers.GatewayManager[shard_clients.ShardClient], unset.UNSET) async def start(self): if self.rest or self.gateway: diff --git a/hikari/unset.py b/hikari/unset.py index b05eee7495..36af4e339e 100644 --- a/hikari/unset.py +++ b/hikari/unset.py @@ -48,7 +48,7 @@ def ___defer___(self, *_, **__) -> typing.NoReturn: T = typing.TypeVar("T") -MayBeUnset = typing.Union[Unset, T] +MayBeUnset = typing.Union[T, Unset] #: A global instance of :class:`~Unset`. UNSET: typing.Final[Unset] = Unset() From 2ba1329f4b98c24d09802ad65fee9b69b23629cd Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Sat, 18 Apr 2020 19:20:01 +0100 Subject: [PATCH 166/922] - Removed `hikari.clients.config.ShardConfig` and merged behaviour into `hikari.clients.configs.WebsocketConfig`. - Fixes #298 where we were unable to eagerly initialize the `RESTClient`. - Removed `hikari.clients.gateway_manager.GatewayManager` as this was a needless abstraction. That behaviour is now in `BotBase`, so does not have an issue with lazy initalization anymore (relates to same issue that #298 discussed, just for gateway stuff instead.) - Added properties for gateway shard stats to `BotBase`. - Extracted a `ShardClient` ABC. The previous implementation detail now extends this class and is renamed to `ShardClientImpl`. - `BotBase` is now a generic type, taking the type of the shard clients as the primary argument. - Removed `hikari.internal.more_logging`. Wasn't useful. - Reformatted how components with indexes are named in logs. Shards are now called `hikari.ShardClient.12`, allowing users to disable logs for specific shards nicely. - Fixed an oversight on my behalf where the manual ratelimiter is never shut down properly on the low level REST client. - Renamed `ratelimiter` to `bucket_ratelimiters` in low level REST client to reduce ambiguity of API. - Added more low level REST tests. --- hikari/clients/__init__.py | 3 - hikari/clients/bot_clients.py | 369 +++++++++++++++--- hikari/clients/configs.py | 54 ++- hikari/clients/gateway_managers.py | 243 ------------ hikari/clients/rest_clients/component_base.py | 5 - hikari/clients/runnable.py | 3 +- hikari/clients/shard_clients.py | 367 ++++++++++------- hikari/clients/test_client.py | 35 +- hikari/internal/more_logging.py | 61 --- hikari/net/ratelimits.py | 7 +- hikari/net/rest_sessions.py | 42 +- hikari/net/shards.py | 135 ++++--- hikari/state/event_dispatchers.py | 28 +- hikari/state/event_managers.py | 4 +- pylint.ini | 2 +- tests/hikari/clients/test_configs.py | 33 +- tests/hikari/clients/test_gateway_managers.py | 296 -------------- .../test_rest_clients/test_component_base.py | 8 - tests/hikari/clients/test_shard_clients.py | 18 +- tests/hikari/internal/test_more_logging.py | 55 --- tests/hikari/net/test_rest_sessions.py | 109 ++++-- 21 files changed, 813 insertions(+), 1064 deletions(-) delete mode 100644 hikari/clients/gateway_managers.py delete mode 100644 hikari/internal/more_logging.py delete mode 100644 tests/hikari/clients/test_gateway_managers.py delete mode 100644 tests/hikari/internal/test_more_logging.py diff --git a/hikari/clients/__init__.py b/hikari/clients/__init__.py index f7dada76ec..82b6b9dc71 100644 --- a/hikari/clients/__init__.py +++ b/hikari/clients/__init__.py @@ -20,12 +20,10 @@ from hikari.clients import bot_clients from hikari.clients import configs -from hikari.clients import gateway_managers from hikari.clients import rest_clients from hikari.clients import runnable from hikari.clients.bot_clients import * from hikari.clients.configs import * -from hikari.clients.gateway_managers import * from hikari.clients.rest_clients import * from hikari.clients.runnable import * from hikari.clients.shard_clients import * @@ -33,7 +31,6 @@ __all__ = [ *bot_clients.__all__, *configs.__all__, - *gateway_managers.__all__, *rest_clients.__all__, *shard_clients.__all__, *runnable.__all__, diff --git a/hikari/clients/bot_clients.py b/hikari/clients/bot_clients.py index 114ef0cdc2..7a40fa7d19 100644 --- a/hikari/clients/bot_clients.py +++ b/hikari/clients/bot_clients.py @@ -20,120 +20,230 @@ __all__ = ["BotBase", "StatelessBot"] import abc +import asyncio import datetime import logging +import math +import time import typing from hikari import events -from hikari import unset +from hikari import gateway_entities +from hikari import guilds from hikari.clients import configs -from hikari.clients import gateway_managers from hikari.clients import rest_clients from hikari.clients import runnable from hikari.clients import shard_clients +from hikari.internal import conversions from hikari.internal import more_asyncio -from hikari.internal import more_logging +from hikari.internal import more_collections +from hikari.net import codes from hikari.state import event_dispatchers from hikari.state import event_managers from hikari.state import stateless_event_managers -class BotBase(runnable.RunnableClient, event_dispatchers.EventDispatcher): +ShardClientT = typing.TypeVar("ShardClientT", bound=shard_clients.ShardClient) +EventManagerT = typing.TypeVar("EventManagerT", bound=event_managers.EventManager) +RESTClientT = typing.TypeVar("RESTClientT", bound=rest_clients.RESTClient) +BotConfigT = typing.TypeVar("BotConfigT", bound=configs.BotConfig) + + +class BotBase( + typing.Generic[ShardClientT, RESTClientT, EventManagerT, BotConfigT], + runnable.RunnableClient, + event_dispatchers.EventDispatcher, + abc.ABC, +): """An abstract base class for a bot implementation. + This takes several generic parameter types in the following order: + - ``ShardClientT`` - the implementation of + :obj:`~hikari.clients.shard_clients.ShardClient` to use for shards. + - ``RESTClientT`` - the implementation of + :obj:`~hikari.clients.rest_clients.RESTClient` to use for API calls. + - ``EventManagerT`` - the implementation of + :obj:`~hikari.state.event_managers.EventManager` to use for + event management, translation, and dispatching. + - ``BotConfigT`` - the implementation of + :obj:`~hikari.clients.configs.BotConfig` to read component-specific + details from. + Parameters ---------- config : :obj:`~hikari.clients.configs.BotConfig` The config object to use. - event_manager : ``hikari.state.event_managers.EventManager`` - The event manager to use. """ #: The config for this bot. #: #: :type: :obj:`~hikari.clients.configs.BotConfig` - config: configs.BotConfig + _config: BotConfigT #: The event manager for this bot. #: - #: :type: a subclass of :obj:`~hikari.state.event_managers.EventManager` - event_manager: event_managers.EventManager + #: :type: an implementation instance of :obj:`~hikari.state.event_managers.EventManager` + event_manager: EventManagerT #: The logger to use for this bot. #: #: :type: :obj:`~logging.Logger` logger: logging.Logger - #: The gateway for this bot. - #: - #: Note - #: ---- - #: This will be initialized lazily once the bot has started. + #: The REST HTTP client to use for this bot. #: - #: :type: :obj:`~hikari.clients.gateway_managers.GatewayManager` [ :obj:`~hikari.clients.shard_clients.ShardClient` ] - gateway: unset.MayBeUnset[gateway_managers.GatewayManager[shard_clients.ShardClient]] + #: :type: :obj:`~hikari.clients.rest_clients.RESTClient` + rest: RESTClientT - #: The REST HTTP client to use for this bot. + #: Shards registered to this bot. #: - #: Note - #: ---- - #: This will be initialized lazily once the bot has started. + #: These will be created once the bot has started execution. #: - #: :type: :obj:`~hikari.clients.rest_clients.RESTClient` - rest: unset.MayBeUnset[rest_clients.RESTClient] + #: :type: :obj:`~typing.Mapping` [ :obj:`~int`, ? extends :obj:`~hikari.clients.shard_client.ShardClient` ] + shards: typing.Mapping[int, ShardClientT] @abc.abstractmethod - @typing.no_type_check - def __init__(self, config: configs.BotConfig, event_manager: event_managers.EventManager) -> None: - super().__init__(more_logging.get_named_logger(self)) - self.config = config - self.event_manager = event_manager + def __init__(self, config: configs.BotConfig) -> None: + super().__init__(logging.getLogger(f"hikari.{type(self).__qualname__}")) + self._config = config + self.event_manager = self._create_event_manager() + self.rest = self._create_rest(config) + self.shards = more_collections.EMPTY_DICT + + @property + def heartbeat_latency(self) -> float: + """Average heartbeat latency for all valid shards. + + This will return a mean of all the heartbeat intervals for all shards + with a valid heartbeat latency that are in the + :obj:`~hikari.clients.shard_clients.ShardState.READY` state. + + If no shards are in this state, this will return ``float('nan')`` + instead. + + Returns + ------- + :obj:`~float` + The mean latency for all ``READY`` shards that have sent at least + one acknowledged ``HEARTBEAT`` payload. If there is not at least + one shard that meets this criteria, this will instead return + ``float('nan')``. + """ + latencies = [] + for shard in self.shards.values(): + if not math.isnan(shard.heartbeat_latency): + latencies.append(shard.heartbeat_latency) + + return sum(latencies) / len(latencies) if latencies else float("nan") + + @property + def total_disconnect_count(self) -> int: + """Total number of times any shard has disconnected. + + Returns + ------- + :obj:`int` + Total disconnect count. + """ + return sum(s.disconnect_count for s in self.shards.values()) - # Use the typing.cast to fix issue in some linters where they give precedence to the assigned - # value over the type hint. - self.rest = typing.cast(rest_clients.RESTClient, unset.UNSET) - self.gateway = typing.cast(gateway_managers.GatewayManager[shard_clients.ShardClient], unset.UNSET) + @property + def total_reconnect_count(self) -> int: + """Total number of times any shard has reconnected. + + Returns + ------- + :obj:`int` + Total reconnect count. + """ + return sum(s.reconnect_count for s in self.shards.values()) + + @property + def intents(self) -> typing.Optional[codes.GatewayIntent]: + """Intent values that any shard connections will be using. + + Returns + ------- + :obj:`~hikari.net.codes.GatewayIntent`, optional + A :obj:`~enum.IntFlag` enum containing each intent that is set. If + intents are not being used at all, then this will return + :obj:`~None` instead. + """ + return self._config.intents + + @property + def version(self) -> float: + """Version being used for the gateway API. + + Returns + ------- + :obj:`~int` + The API version being used. + """ + return self._config.gateway_version async def start(self): - if self.rest or self.gateway: + if self.shards: raise RuntimeError("Bot is already running.") - self.rest = rest_clients.RESTClient(self.config) gateway_bot = await self.rest.fetch_gateway_bot() self.logger.info( - "You have sent an IDENTIFY %s time(s) before now, and have %s remaining. This will reset at %s.", + "you have sent an IDENTIFY %s time(s) before now, and have %s remaining. This will reset at %s.", gateway_bot.session_start_limit.total - gateway_bot.session_start_limit.remaining, gateway_bot.session_start_limit.remaining, datetime.datetime.now() + gateway_bot.session_start_limit.reset_after, ) - shard_count = self.config.shard_count if self.config.shard_count else gateway_bot.shard_count - shard_ids = self.config.shard_ids if self.config.shard_ids else [*range(shard_count)] + shard_count = self._config.shard_count if self._config.shard_count else gateway_bot.shard_count + shard_ids = self._config.shard_ids if self._config.shard_ids else [*range(shard_count)] + url = gateway_bot.url - self.gateway = gateway_managers.GatewayManager( - config=self.config, - url=gateway_bot.url, - raw_event_consumer_impl=self.event_manager, - shard_ids=shard_ids, - shard_count=shard_count, - dispatcher=self.event_manager.event_dispatcher, - ) + self.logger.info("will connect shards to %s", url) + + shards = {} + for shard_id in shard_ids: + shard = self._create_shard(shard_id, shard_count, url, self._config, self.event_manager) + shards[shard_id] = shard - await self.gateway.start() + self.shards = shards + + self.logger.info("starting %s", conversions.pluralize(len(self.shards), "shard")) + + start_time = time.perf_counter() + + for i, shard_id in enumerate(self.shards): + if i > 0: + self.logger.info("idling for 5 seconds to avoid an invalid session") + await asyncio.sleep(5) + + shard_obj = self.shards[shard_id] + await shard_obj.start() + + finish_time = time.perf_counter() + + self.logger.info("started %s shard(s) in approx %.2fs", len(self.shards), finish_time - start_time) + + if self.event_manager is not None: + await self.dispatch_event(events.StartedEvent()) async def close(self) -> None: - if self.gateway: - await self.gateway.close() - self.event_manager.event_dispatcher.close() - if self.rest: + try: + if self.shards: + self.logger.info("stopping %s shard(s)", len(self.shards)) + start_time = time.perf_counter() + try: + await self.dispatch_event(events.StoppingEvent()) + await asyncio.gather(*(shard_obj.close() for shard_obj in self.shards.values())) + finally: + finish_time = time.perf_counter() + self.logger.info("stopped %s shard(s) in approx %.2fs", len(self.shards), finish_time - start_time) + await self.dispatch_event(events.StoppedEvent()) + finally: await self.rest.close() - self.gateway = unset.UNSET - self.rest = unset.UNSET async def join(self) -> None: - if not unset.is_unset(self.gateway): - await self.gateway.join() + await asyncio.gather(*(shard_obj.join() for shard_obj in self.shards.values())) def add_listener( self, event_type: typing.Type[event_dispatchers.EventT], callback: event_dispatchers.EventCallbackT @@ -157,15 +267,150 @@ def wait_for( def dispatch_event(self, event: events.HikariEvent) -> more_asyncio.Future[typing.Any]: return self.event_manager.event_dispatcher.dispatch_event(event) + async def update_presence( + self, + *, + status: guilds.PresenceStatus = ..., + activity: typing.Optional[gateway_entities.GatewayActivity] = ..., + idle_since: typing.Optional[datetime.datetime] = ..., + is_afk: bool = ..., + ) -> None: + """Update the presence of the user for all shards. -class StatelessBot(BotBase): - """Bot client without any state internals. + This will only update arguments that you explicitly specify a value for. + Any arguments that you do not explicitly provide some value for will + not be changed. - Parameters - ---------- - config : :obj:`~hikari.clients.configs.BotConfig` - The config object to use. - """ + Warning + ------- + This will only apply to connected shards. - def __init__(self, config: configs.BotConfig) -> None: - super().__init__(config, stateless_event_managers.StatelessEventManagerImpl()) + Notes + ----- + If you wish to update a presence for a specific shard, you can do this + by using the ``shards`` :obj:`~typing.Mapping` to find the shard you + wish to update. + + Parameters + ---------- + status : :obj:`~hikari.guilds.PresenceStatus` + If specified, the new status to set. + activity : :obj:`~hikari.gateway_entities.GatewayActivity`, optional + If specified, the new activity to set. + idle_since : :obj:`~datetime.datetime`, optional + If specified, the time to show up as being idle since, + or :obj:`~None` if not applicable. + is_afk : :obj:`~bool` + If specified, :obj:`~True` if the user should be marked as AFK, + or :obj:`~False` otherwise. + """ + await asyncio.gather( + *( + s.update_presence(status=status, activity=activity, idle_since=idle_since, is_afk=is_afk) + for s in self.shards.values() + if s.connection_state in (shard_clients.ShardState.WAITING_FOR_READY, shard_clients.ShardState.READY) + ) + ) + + @classmethod + @abc.abstractmethod + def _create_shard( + cls, shard_id: int, shard_count: int, url: str, config: BotConfigT, event_manager: EventManagerT, + ) -> ShardClientT: + """Return a new shard for the given parameters. + + Parameters + ---------- + shard_id : :obj:`~int` + The shard ID to use. + shard_count : :obj:`~int` + The shard count to use. + url : :obj:`~str` + The gateway URL to connect to. + config : :obj:`~hikari.clients.configs.BotConfig` + The bot config to use. + event_manager :obj:`~hikari.state.event_managers.EventManager` + The event manager to use. + + Returns + ------- + :obj:`~hikari.clients.shard_clients.ShardClient` + The shard client implementation to use for the given shard ID. + + Notes + ----- + The ``shard_id`` and ``shard_count`` may be set within the ``config`` + object passed, but any conforming implementations are expected to + use the value passed in the ``shard_id` and ``shard_count`` parameters + regardless. Failure to do so may result in an invalid sharding + configuration being used. + + """ + + @classmethod + @abc.abstractmethod + def _create_rest(cls, config: BotConfigT) -> RESTClientT: + """Return a new REST client from the given configuration. + + Parameters + ---------- + config : :obj:`~hikari.clients.configs.BotConfig` + The bot config to use. + + Returns + ------- + :obj:`~hikari.clients.rest_clients.RESTClient` + The REST client to use. + + """ + + @classmethod + @abc.abstractmethod + def _create_event_manager(cls): + """Return a new instance of an event manager implementation. + + Returns + ------- + :obj:`~hikari.state.event_managers.EventManager` + The event manager to use internally. + """ + + +class StatelessBot( + BotBase[ + shard_clients.ShardClientImpl, + rest_clients.RESTClient, + stateless_event_managers.StatelessEventManagerImpl, + configs.BotConfig, + ] +): + """Bot client without any state internals.""" + + def __init__(self, config=configs.BotConfig) -> None: + super().__init__(config) + + @classmethod + def _create_shard( + cls, + shard_id: int, + shard_count: int, + url: str, + config: configs.BotConfig, + event_manager: stateless_event_managers.StatelessEventManagerImpl, + ) -> shard_clients.ShardClientImpl: + return shard_clients.ShardClientImpl( + shard_id=shard_id, + shard_count=shard_count, + config=config, + raw_event_consumer_impl=event_manager, + url=url, + dispatcher=event_manager.event_dispatcher, + ) + + @classmethod + def _create_rest(cls, config: BotConfigT) -> rest_clients.RESTClient: + return rest_clients.RESTClient(config) + + @classmethod + def _create_event_manager(cls) -> EventManagerT: + return stateless_event_managers.StatelessEventManagerImpl() diff --git a/hikari/clients/configs.py b/hikari/clients/configs.py index a962b8ae05..5e4d313022 100644 --- a/hikari/clients/configs.py +++ b/hikari/clients/configs.py @@ -23,8 +23,7 @@ "DebugConfig", "AIOHTTPConfig", "TokenConfig", - "WebsocketConfig", - "ShardConfig", + "GatewayConfig", "RESTConfig", "BotConfig", ] @@ -167,9 +166,29 @@ class TokenConfig(BaseConfig): token: typing.Optional[str] = marshaller.attrib(deserializer=str, if_none=None, if_undefined=None, default=None) +def _parse_shard_info(payload): + range_matcher = re.search(r"(\d+)\s*(\.{2,3})\s*(\d+)", payload) if isinstance(payload, str) else None + + if not range_matcher: + if isinstance(payload, int): + return [payload] + + if isinstance(payload, list): + return payload + + raise ValueError('expected shard_ids to be one of int, list of int, or range string ("x..y" or "x...y")') + + minimum, range_mod, maximum = range_matcher.groups() + minimum, maximum = int(minimum), int(maximum) + if len(range_mod) == 3: + maximum += 1 + + return [*range(minimum, maximum)] + + @marshaller.marshallable() @attr.s(kw_only=True) -class WebsocketConfig(AIOHTTPConfig, TokenConfig, DebugConfig): +class GatewayConfig(AIOHTTPConfig, TokenConfig, DebugConfig): """Single-websocket specific configuration options.""" #: Whether to use zlib compression on the gateway for inbound messages or @@ -263,30 +282,6 @@ class WebsocketConfig(AIOHTTPConfig, TokenConfig, DebugConfig): #: :type: :obj:`~int` large_threshold: int = marshaller.attrib(deserializer=int, if_undefined=lambda: 250, default=250) - -def _parse_shard_info(payload): - range_matcher = re.search(r"(\d+)\s*(\.{2,3})\s*(\d+)", payload) if isinstance(payload, str) else None - - if not range_matcher: - if isinstance(payload, int): - return [payload] - - if isinstance(payload, list): - return payload - - raise ValueError('expected shard_ids to be one of int, list of int, or range string ("x..y" or "x...y")') - - minimum, range_mod, maximum = range_matcher.groups() - minimum, maximum = int(minimum), int(maximum) - if len(range_mod) == 3: - maximum += 1 - - return [*range(minimum, maximum)] - - -@marshaller.marshallable() -@attr.s(kw_only=True) -class ShardConfig(BaseConfig): """Definition of shard management configuration settings.""" #: The shard IDs to produce shard connections for. @@ -316,7 +311,8 @@ class ShardConfig(BaseConfig): #: of. If you run multiple distributed instances of the bot, you should #: ensure this value is consistent. #: - #: This can be set to `None` to enable auto-sharding. This is the default. + #: This can be set to :obj:`~None` to enable auto-sharding. This is the + #: default behaviour. #: #: :type: :obj:`~int`, optional. shard_count: typing.Optional[int] = marshaller.attrib(deserializer=int, if_undefined=None, default=None) @@ -348,5 +344,5 @@ class RESTConfig(AIOHTTPConfig, TokenConfig): @marshaller.marshallable() @attr.s(kw_only=True) -class BotConfig(RESTConfig, ShardConfig, WebsocketConfig): +class BotConfig(RESTConfig, GatewayConfig): """Configuration for a standard bot.""" diff --git a/hikari/clients/gateway_managers.py b/hikari/clients/gateway_managers.py deleted file mode 100644 index 21fadd1e5d..0000000000 --- a/hikari/clients/gateway_managers.py +++ /dev/null @@ -1,243 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# Copyright © Nekoka.tt 2019-2020 -# -# This file is part of Hikari. -# -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . -"""Defines a facade around :obj:`~hikari.clients.shard_clients.ShardClient`. - -This provides functionality such as keeping multiple shards alive -""" - -__all__ = ["GatewayManager"] - -import asyncio -import datetime -import math -import time -import typing - -from hikari import events -from hikari import gateway_entities -from hikari import guilds -from hikari.clients import configs -from hikari.clients import runnable -from hikari.clients import shard_clients -from hikari.internal import conversions -from hikari.internal import more_logging -from hikari.net import codes -from hikari.state import event_dispatchers -from hikari.state import raw_event_consumers - -ShardT = typing.TypeVar("ShardT", bound=shard_clients.ShardClient) - - -class GatewayManager(typing.Generic[ShardT], runnable.RunnableClient): - """Provides a management layer for multiple-sharded bots. - - This also provides additional conduit used to connect up shards to the - rest of this framework to enable management of dispatched events, etc. - """ - - def __init__( - self, - *, - shard_ids: typing.Sequence[int], - shard_count: int, - config: configs.WebsocketConfig, - url: str, - raw_event_consumer_impl: raw_event_consumers.RawEventConsumer, - shard_type: typing.Type[ShardT] = shard_clients.ShardClient, - dispatcher: typing.Optional[event_dispatchers.EventDispatcher] = None, - ) -> None: - super().__init__(more_logging.get_named_logger(self, conversions.pluralize(shard_count, "shard"))) - self._is_running = False - self._config = config - self.raw_event_consumer = raw_event_consumer_impl - self._dispatcher = dispatcher - self.shards: typing.Dict[int, ShardT] = { - shard_id: shard_type(shard_id, shard_count, config, raw_event_consumer_impl, url) for shard_id in shard_ids - } - self.shard_ids = shard_ids - - @property - def heartbeat_latency(self) -> float: - """Average heartbeat latency for all valid shards. - - This will return a mean of all the heartbeat intervals for all shards - with a valid heartbeat latency that are in the - :obj:`~hikari.clients.shard_clients.ShardState.READY` state. - - If no shards are in this state, this will return ``float('nan')`` - instead. - - Returns - ------- - :obj:`~float` - The mean latency for all ``READY`` shards that have sent at least - one acknowledged ``HEARTBEAT`` payload. If there is not at least - one shard that meets this criteria, this will instead return - ``float('nan')``. - """ - latencies = [] - for shard in self.shards.values(): - if not math.isnan(shard.heartbeat_latency): - latencies.append(shard.heartbeat_latency) - - return sum(latencies) / len(latencies) if latencies else float("nan") - - @property - def total_disconnect_count(self) -> int: - """Total number of times any shard has disconnected. - - Returns - ------- - :obj:`int` - Total disconnect count. - """ - return sum(s.disconnect_count for s in self.shards.values()) - - @property - def total_reconnect_count(self) -> int: - """Total number of times any shard has reconnected. - - Returns - ------- - :obj:`int` - Total reconnect count. - """ - return sum(s.reconnect_count for s in self.shards.values()) - - @property - def intents(self) -> typing.Optional[codes.GatewayIntent]: - """Intent values that any shard connections will be using. - - Returns - ------- - :obj:`~hikari.net.codes.GatewayIntent`, optional - A :obj:`~enum.IntFlag` enum containing each intent that is set. If - intents are not being used at all, then this will return - :obj:`~None` instead. - """ - return self._config.intents - - @property - def version(self) -> float: - """Version being used for the gateway API. - - Returns - ------- - :obj:`~int` - The API version being used. - """ - return self._config.gateway_version - - async def start(self) -> None: - """Start all shards. - - This safely starts all shards at the correct rate to prevent invalid - session spam. This involves starting each shard sequentially with a - 5 second pause between each. - """ - if self._is_running: - raise RuntimeError("Cannot start a client twice.") - - if self._dispatcher is not None: - await self._dispatcher.dispatch_event(events.StartingEvent()) - - self._is_running = True - self.logger.info("starting %s shard(s)", len(self.shards)) - start_time = time.perf_counter() - for i, shard_id in enumerate(self.shard_ids): - if i > 0: - await asyncio.sleep(5) - - shard_obj = self.shards[shard_id] - await shard_obj.start() - finish_time = time.perf_counter() - - self.logger.info("started %s shard(s) in approx %.2fs", len(self.shards), finish_time - start_time) - - if self._dispatcher is not None: - await self._dispatcher.dispatch_event(events.StartedEvent()) - - async def join(self) -> None: - """Wait for all shards to finish executing, then return.""" - await asyncio.gather(*(shard_obj.join() for shard_obj in self.shards.values())) - - async def close(self) -> None: - """Close all shards. - - Waits for all shards to shut down before returning. - """ - if self._is_running: - self.logger.info("stopping %s shard(s)", len(self.shards)) - start_time = time.perf_counter() - try: - if self._dispatcher is not None: - await self._dispatcher.dispatch_event(events.StoppingEvent()) - await asyncio.gather(*(shard_obj.close() for shard_obj in self.shards.values())) - finally: - finish_time = time.perf_counter() - self.logger.info("stopped %s shard(s) in approx %.2fs", len(self.shards), finish_time - start_time) - self._is_running = False - if self._dispatcher is not None: - await self._dispatcher.dispatch_event(events.StoppedEvent()) - - async def update_presence( - self, - *, - status: guilds.PresenceStatus = ..., - activity: typing.Optional[gateway_entities.GatewayActivity] = ..., - idle_since: typing.Optional[datetime.datetime] = ..., - is_afk: bool = ..., - ) -> None: - """Update the presence of the user for all shards. - - This will only update arguments that you explicitly specify a value for. - Any arguments that you do not explicitly provide some value for will - not be changed. - - Warning - ------- - This will only apply to connected shards. - - Notes - ----- - If you wish to update a presence for a specific shard, you can do this - by using the ``shards`` :obj:`~typing.Mapping` to find the shard you - wish to update. - - Parameters - ---------- - status : :obj:`~hikari.guilds.PresenceStatus` - If specified, the new status to set. - activity : :obj:`~hikari.gateway_entities.GatewayActivity`, optional - If specified, the new activity to set. - idle_since : :obj:`~datetime.datetime`, optional - If specified, the time to show up as being idle since, - or :obj:`~None` if not applicable. - is_afk : :obj:`~bool` - If specified, :obj:`~True` if the user should be marked as AFK, - or :obj:`~False` otherwise. - """ - await asyncio.gather( - *( - shard.update_presence(status=status, activity=activity, idle_since=idle_since, is_afk=is_afk) - for shard in self.shards.values() - if shard.connection_state - in (shard_clients.ShardState.WAITING_FOR_READY, shard_clients.ShardState.READY) - ) - ) diff --git a/hikari/clients/rest_clients/component_base.py b/hikari/clients/rest_clients/component_base.py index b6eeb7a533..189a3f7293 100644 --- a/hikari/clients/rest_clients/component_base.py +++ b/hikari/clients/rest_clients/component_base.py @@ -51,8 +51,3 @@ async def __aexit__( async def close(self) -> None: """Shut down the REST client safely.""" await self._session.close() - - @property - def session(self) -> rest_sessions.LowLevelRestfulClient: - """Get the :obj:`hikari.net.rest_sessions.LowLevelRestfulClient` session object.""" - return self._session diff --git a/hikari/clients/runnable.py b/hikari/clients/runnable.py index 324894ad08..4525a9b583 100644 --- a/hikari/clients/runnable.py +++ b/hikari/clients/runnable.py @@ -25,6 +25,7 @@ import contextlib import logging import signal +import typing class RunnableClient(abc.ABC): @@ -38,7 +39,7 @@ class RunnableClient(abc.ABC): logger: logging.Logger @abc.abstractmethod - def __init__(self, logger: logging.Logger) -> None: + def __init__(self, logger: typing.Union[logging.Logger, logging.LoggerAdapter]) -> None: self.logger = logger @abc.abstractmethod diff --git a/hikari/clients/shard_clients.py b/hikari/clients/shard_clients.py index 6b55c6a7b8..e015dbc1cf 100644 --- a/hikari/clients/shard_clients.py +++ b/hikari/clients/shard_clients.py @@ -26,12 +26,13 @@ """ from __future__ import annotations -__all__ = ["ShardState", "ShardClient"] +__all__ = ["ShardState", "ShardClient", "ShardClientImpl"] +import abc import asyncio -import contextlib import datetime import enum +import logging import time import typing @@ -43,7 +44,6 @@ from hikari import guilds from hikari.clients import configs from hikari.clients import runnable -from hikari.internal import more_logging from hikari.net import codes from hikari.net import ratelimits from hikari.net import shards @@ -81,101 +81,13 @@ def __str__(self) -> str: return self.name -class ShardClient(runnable.RunnableClient): - """The primary interface for a single shard connection. - - This contains several abstractions to enable usage of the low - level gateway network interface with the higher level constructs - in :mod:`hikari`. - - Parameters - ---------- - shard_id : :obj:`~int` - The ID of this specific shard. - shard_id : :obj:`~int` - The number of shards that make up this distributed application. - config : :obj:`~hikari.clients.configs.WebsocketConfig` - The gateway configuration to use to initialize this shard. - raw_event_consumer_impl : :obj:`~hikari.state.raw_event_consumers.RawEventConsumer` - The consumer of a raw event. - url : :obj:`~str` - The URL to connect the gateway to. - dispatcher : :obj:`~hikari.state.event_dispatchers.EventDispatcher`, optional - The high level event dispatcher to use for dispatching start and stop - events. Set this to :obj:`~None` to disable that functionality (useful if - you use a gateway manager to orchestrate multiple shards instead and - provide this functionality there). Defaults to :obj:`~None` if - unspecified. - - Notes - ----- - Generally, you want to use - :obj:`~hikari.clients.gateway_managers.GatewayManager` rather than this class - directly, as that will handle sharding where enabled and applicable, and - provides a few more bits and pieces that may be useful such as state - management and event dispatcher integration. and If you want to customize - this, you can subclass it and simply override anything you want. - """ +class ShardClient(runnable.RunnableClient, abc.ABC): + """Definition of the interface for a conforming shard client.""" - __slots__ = ( - "logger", - "_raw_event_consumer", - "_connection", - "_status", - "_activity", - "_idle_since", - "_is_afk", - "_task", - "_shard_state", - "_dispatcher", - ) - - def __init__( - self, - shard_id: int, - shard_count: int, - config: configs.WebsocketConfig, - raw_event_consumer_impl: raw_event_consumers.RawEventConsumer, - url: str, - dispatcher: typing.Optional[event_dispatchers.EventDispatcher] = None, - ) -> None: - super().__init__(more_logging.get_named_logger(self, f"#{shard_id}")) - self._raw_event_consumer = raw_event_consumer_impl - self._activity = config.initial_activity - self._idle_since = config.initial_idle_since - self._is_afk = config.initial_is_afk - self._status = config.initial_status - self._shard_state = ShardState.NOT_RUNNING - self._task = None - self._dispatcher = dispatcher - self._connection = shards.ShardConnection( - compression=config.gateway_use_compression, - connector=config.tcp_connector, - debug=config.debug, - dispatch=lambda c, n, pl: raw_event_consumer_impl.process_raw_event(self, n, pl), - initial_presence=self._create_presence_pl( - status=config.initial_status, - activity=config.initial_activity, - idle_since=config.initial_idle_since, - is_afk=config.initial_is_afk, - ), - intents=config.intents, - large_threshold=config.large_threshold, - proxy_auth=config.proxy_auth, - proxy_headers=config.proxy_headers, - proxy_url=config.proxy_url, - session_id=None, - seq=None, - shard_id=shard_id, - shard_count=shard_count, - ssl_context=config.ssl_context, - token=config.token, - url=url, - verify_ssl=config.verify_ssl, - version=config.gateway_version, - ) + __slots__ = () @property + @abc.abstractmethod def shard_id(self) -> int: """Shard ID. @@ -184,9 +96,9 @@ def shard_id(self) -> int: :obj:`~int` The 0-indexed shard ID. """ - return self._connection.shard_id @property + @abc.abstractmethod def shard_count(self) -> int: """Shard count. @@ -195,24 +107,22 @@ def shard_count(self) -> int: :obj:`~int` The number of shards that make up this bot. """ - return self._connection.shard_count - # Ignore docstring not starting in an imperative mood @property - def status(self) -> guilds.PresenceStatus: # noqa: D401 - """Current user status for this shard. + @abc.abstractmethod + def status(self) -> guilds.PresenceStatus: + """User status for this shard. Returns ------- :obj:`~hikari.guilds.PresenceStatus` The current user status for this shard. """ - return self._status - # Ignore docstring not starting in an imperative mood @property - def activity(self) -> typing.Optional[gateway_entities.GatewayActivity]: # noqa: D401 - """Current activity for the user status for this shard. + @abc.abstractmethod + def activity(self) -> typing.Optional[gateway_entities.GatewayActivity]: + """Activity for the user status for this shard. Returns ------- @@ -220,9 +130,9 @@ def activity(self) -> typing.Optional[gateway_entities.GatewayActivity]: # noqa The current activity for the user on this shard, or :obj:`~None` if there is no activity. """ - return self._activity @property + @abc.abstractmethod def idle_since(self) -> typing.Optional[datetime.datetime]: """Timestamp when the user of this shard appeared to be idle. @@ -232,21 +142,20 @@ def idle_since(self) -> typing.Optional[datetime.datetime]: The timestamp when the user of this shard appeared to be idle, or :obj:`~None` if not applicable. """ - return self._idle_since - # Ignore docstring not starting in an imperative mood @property - def is_afk(self) -> bool: # noqa: D401 - """:obj:`~True` if the user is AFK, :obj:`~False` otherwise. + @abc.abstractmethod + def is_afk(self) -> bool: + """Whether the user is AFK or not. Returns ------- :obj:`~bool` :obj:`~True` if the user is AFK, :obj:`~False` otherwise. """ - return self._is_afk @property + @abc.abstractmethod def heartbeat_latency(self) -> float: """Latency between sending a HEARTBEAT and receiving an ACK. @@ -256,9 +165,9 @@ def heartbeat_latency(self) -> float: The heartbeat latency in seconds. This will be ``float('nan')`` until the first heartbeat is performed. """ - return self._connection.heartbeat_latency @property + @abc.abstractmethod def heartbeat_interval(self) -> float: """Time period to wait between sending HEARTBEAT payloads. @@ -268,9 +177,9 @@ def heartbeat_interval(self) -> float: The heartbeat interval in seconds. This will be ``float('nan')`` until the connection has received a ``HELLO`` payload. """ - return self._connection.heartbeat_interval @property + @abc.abstractmethod def disconnect_count(self) -> int: """Count of number of times the internal connection has disconnected. @@ -279,9 +188,9 @@ def disconnect_count(self) -> int: :obj:`~int` The number of disconnects this shard has performed. """ - return self._connection.disconnect_count @property + @abc.abstractmethod def reconnect_count(self) -> int: """Count of number of times the internal connection has reconnected. @@ -292,9 +201,9 @@ def reconnect_count(self) -> int: :obj:`~int` The number of reconnects this shard has performed. """ - return self._connection.reconnect_count @property + @abc.abstractmethod def connection_state(self) -> ShardState: """State of this shard. @@ -303,9 +212,9 @@ def connection_state(self) -> ShardState: :obj:`~ShardState` The state of this shard. """ - return self._shard_state @property + @abc.abstractmethod def is_connected(self) -> bool: """Whether the shard is connected or not. @@ -314,9 +223,9 @@ def is_connected(self) -> bool: :obj:`~bool` :obj:`~True` if connected; :obj:`~False` otherwise. """ - return self._connection.is_connected @property + @abc.abstractmethod def seq(self) -> typing.Optional[int]: """Sequence ID of the shard. @@ -326,9 +235,9 @@ def seq(self) -> typing.Optional[int]: The sequence number for the shard. This is the number of payloads that have been received since an ``IDENTIFY`` was sent. """ - return self._connection.seq @property + @abc.abstractmethod def session_id(self) -> typing.Optional[str]: """Session ID. @@ -338,9 +247,9 @@ def session_id(self) -> typing.Optional[str]: The session ID for the shard connection, if there is one. If not, then :obj:`~None`. """ - return self._connection.session_id @property + @abc.abstractmethod def version(self) -> float: """Version being used for the gateway API. @@ -349,9 +258,9 @@ def version(self) -> float: :obj:`~int` The API version being used. """ - return self._connection.version @property + @abc.abstractmethod def intents(self) -> typing.Optional[codes.GatewayIntent]: """Intent values that this connection is using. @@ -362,6 +271,196 @@ def intents(self) -> typing.Optional[codes.GatewayIntent]: intents are not being used at all, then this will return :obj:`~None` instead. """ + + @abc.abstractmethod + async def update_presence( + self, + *, + status: guilds.PresenceStatus = ..., + activity: typing.Optional[gateway_entities.GatewayActivity] = ..., + idle_since: typing.Optional[datetime.datetime] = ..., + is_afk: bool = ..., + ) -> None: + """Update the presence of the user for the shard. + + This will only update arguments that you explicitly specify a value for. + Any arguments that you do not explicitly provide some value for will + not be changed. + + Warnings + -------- + This will fail if the shard is not online. + + Parameters + ---------- + status : :obj:`~hikari.guilds.PresenceStatus` + If specified, the new status to set. + activity : :obj:`~hikari.gateway_entities.GatewayActivity`, optional + If specified, the new activity to set. + idle_since : :obj:`~datetime.datetime`, optional + If specified, the time to show up as being idle since, or + :obj:`~None` if not applicable. + is_afk : :obj:`~bool` + If specified, whether the user should be marked as AFK. + """ + + +class ShardClientImpl(ShardClient): + """The primary interface for a single shard connection. + + This contains several abstractions to enable usage of the low + level gateway network interface with the higher level constructs + in :mod:`hikari`. + + Parameters + ---------- + shard_id : :obj:`~int` + The ID of this specific shard. + shard_id : :obj:`~int` + The number of shards that make up this distributed application. + config : :obj:`~hikari.clients.configs.WebsocketConfig` + The gateway configuration to use to initialize this shard. + raw_event_consumer_impl : :obj:`~hikari.state.raw_event_consumers.RawEventConsumer` + The consumer of a raw event. + url : :obj:`~str` + The URL to connect the gateway to. + dispatcher : :obj:`~hikari.state.event_dispatchers.EventDispatcher`, optional + The high level event dispatcher to use for dispatching start and stop + events. Set this to :obj:`~None` to disable that functionality (useful if + you use a gateway manager to orchestrate multiple shards instead and + provide this functionality there). Defaults to :obj:`~None` if + unspecified. + + Notes + ----- + Generally, you want to use + :obj:`~hikari.clients.gateway_managers.GatewayManager` rather than this class + directly, as that will handle sharding where enabled and applicable, and + provides a few more bits and pieces that may be useful such as state + management and event dispatcher integration. and If you want to customize + this, you can subclass it and simply override anything you want. + """ + + __slots__ = ( + "logger", + "_raw_event_consumer", + "_connection", + "_status", + "_activity", + "_idle_since", + "_is_afk", + "_task", + "_shard_state", + "_dispatcher", + ) + + def __init__( + self, + shard_id: int, + shard_count: int, + config: configs.GatewayConfig, + raw_event_consumer_impl: raw_event_consumers.RawEventConsumer, + url: str, + dispatcher: typing.Optional[event_dispatchers.EventDispatcher] = None, + ) -> None: + super().__init__(logging.getLogger(f"hikari.{type(self).__qualname__}.{shard_id}")) + self._raw_event_consumer = raw_event_consumer_impl + self._activity = config.initial_activity + self._idle_since = config.initial_idle_since + self._is_afk = config.initial_is_afk + self._status = config.initial_status + self._shard_state = ShardState.NOT_RUNNING + self._task = None + self._dispatcher = dispatcher + self._connection = shards.ShardConnection( + compression=config.gateway_use_compression, + connector=config.tcp_connector, + debug=config.debug, + dispatch=lambda c, n, pl: raw_event_consumer_impl.process_raw_event(self, n, pl), + initial_presence=self._create_presence_pl( + status=config.initial_status, + activity=config.initial_activity, + idle_since=config.initial_idle_since, + is_afk=config.initial_is_afk, + ), + intents=config.intents, + large_threshold=config.large_threshold, + proxy_auth=config.proxy_auth, + proxy_headers=config.proxy_headers, + proxy_url=config.proxy_url, + session_id=None, + seq=None, + shard_id=shard_id, + shard_count=shard_count, + ssl_context=config.ssl_context, + token=config.token, + url=url, + verify_ssl=config.verify_ssl, + version=config.gateway_version, + ) + + @property + def shard_id(self) -> int: + return self._connection.shard_id + + @property + def shard_count(self) -> int: + return self._connection.shard_count + + @property + def status(self) -> guilds.PresenceStatus: + return self._status + + @property + def activity(self) -> typing.Optional[gateway_entities.GatewayActivity]: + return self._activity + + @property + def idle_since(self) -> typing.Optional[datetime.datetime]: + return self._idle_since + + @property + def is_afk(self) -> bool: + return self._is_afk + + @property + def heartbeat_latency(self) -> float: + return self._connection.heartbeat_latency + + @property + def heartbeat_interval(self) -> float: + return self._connection.heartbeat_interval + + @property + def disconnect_count(self) -> int: + return self._connection.disconnect_count + + @property + def reconnect_count(self) -> int: + return self._connection.reconnect_count + + @property + def connection_state(self) -> ShardState: + return self._shard_state + + @property + def is_connected(self) -> bool: + return self._connection.is_connected + + @property + def seq(self) -> typing.Optional[int]: + return self._connection.seq + + @property + def session_id(self) -> typing.Optional[str]: + return self._connection.session_id + + @property + def version(self) -> float: + return self._connection.version + + @property + def intents(self) -> typing.Optional[codes.GatewayIntent]: return self._connection.intents async def start(self): @@ -401,7 +500,7 @@ async def close(self) -> None: await self._connection.close() - with contextlib.suppress(): + if self._task is not None: await self._task if self._dispatcher is not None: @@ -442,8 +541,10 @@ async def _keep_alive(self): self.logger.exception( "failed to connect to Discord to initialize a websocket connection", exc_info=ex, ) + except errors.GatewayZombiedError: self.logger.warning("entered a zombie state and will be restarted") + except errors.GatewayInvalidSessionError as ex: if ex.can_resume: self.logger.warning("invalid session, so will attempt to resume") @@ -454,10 +555,12 @@ async def _keep_alive(self): do_not_back_off = True await asyncio.sleep(5) + except errors.GatewayMustReconnectError: self.logger.warning("instructed by Discord to reconnect") do_not_back_off = True await asyncio.sleep(5) + except errors.GatewayServerClosedConnectionError as ex: if ex.close_code in ( codes.GatewayCloseCode.NOT_AUTHENTICATED, @@ -472,8 +575,10 @@ async def _keep_alive(self): raise ex from None self.logger.warning("disconnected by Discord, will attempt to reconnect") + except errors.GatewayClientDisconnectedError: self.logger.warning("unexpected connection close, will attempt to reconnect") + except errors.GatewayClientClosedError: self.logger.warning("shutting down") return @@ -544,28 +649,6 @@ async def update_presence( idle_since: typing.Optional[datetime.datetime] = ..., is_afk: bool = ..., ) -> None: - """Update the presence of the user for the shard. - - This will only update arguments that you explicitly specify a value for. - Any arguments that you do not explicitly provide some value for will - not be changed. - - Warnings - -------- - This will fail if the shard is not online. - - Parameters - ---------- - status : :obj:`~hikari.guilds.PresenceStatus` - If specified, the new status to set. - activity : :obj:`~hikari.gateway_entities.GatewayActivity`, optional - If specified, the new activity to set. - idle_since : :obj:`~datetime.datetime`, optional - If specified, the time to show up as being idle since, or - :obj:`~None` if not applicable. - is_afk : :obj:`~bool` - If specified, whether the user should be marked as AFK. - """ status = self._status if status is ... else status activity = self._activity if activity is ... else activity idle_since = self._idle_since if idle_since is ... else idle_since diff --git a/hikari/clients/test_client.py b/hikari/clients/test_client.py index 63b7c6b52d..27af5622ab 100644 --- a/hikari/clients/test_client.py +++ b/hikari/clients/test_client.py @@ -121,10 +121,8 @@ async def on_message(event: hikari.MessageCreateEvent) -> None: message = await client.rest.create_message(event.channel_id, content="Pong!") rest_time = time.perf_counter() - start - active_intents = str(client.gateway._config.intents or "not provided") - shard_infos = [] - for shard_id, shard in client.gateway.shards.items(): + for shard_id, shard in client.shards.items(): shard_info = ( f"latency: {shard.heartbeat_latency * 1_000:.0f} ms\n" f"seq: {shard.seq}\n" @@ -137,22 +135,33 @@ async def on_message(event: hikari.MessageCreateEvent) -> None: shard_infos.append(hikari.EmbedField(name=f"Shard {shard_id}", value=shard_info, is_inline=False)) gw_info = ( - f"intents: {client.gateway.intents}\n" - f"version: {client.gateway.version}\n" - f"average latency: {client.gateway.heartbeat_latency * 1_000:.0f} ms\n" - f"shards: {len(client.gateway.shards)}\n" + f"intents: {client.intents}\n" + f"version: {client.version}\n" + f"average latency: {client.heartbeat_latency * 1_000:.0f} ms\n" + f"shards: {len(client.shards)}\n" f"compression: {compression}\n" f"debug: {debug}\n" ) + actively_limited_routes = sum( + 1 + for b in client.rest._session.bucket_ratelimiters.real_hashes_to_buckets.values() + if b.throttle_task is not None + ) + + actively_limited_calls = sum( + len(b.queue) + for b in client.rest._session.bucket_ratelimiters.real_hashes_to_buckets.values() + if b.throttle_task is not None + ) + rest_info = ( f"message edit time: {rest_time * 1_000:.0f} ms\n" - f"global rate limiter backlog: {len(client.rest._session.global_ratelimiter.queue)}\n" - f"bucket rate limiter active routes: {len(client.rest._session.ratelimiter.routes_to_hashes)}\n" - f"bucket rate limiter active buckets: {len(client.rest._session.ratelimiter.real_hashes_to_buckets)}\n" - "bucket rate limiter backlog: " - f"{sum(len(b.queue) for b in client.rest._session.ratelimiter.real_hashes_to_buckets.values())}\n" - f"bucket rate limiter GC: {getattr(client.rest._session.ratelimiter.gc_task, '_state', 'dead')}\n" + f"global ratelimiter backlog: {len(client.rest._session.global_ratelimiter.queue)}\n" + f"cached limiter routes: {len(client.rest._session.bucket_ratelimiters.routes_to_hashes)}\n" + f"cached limiter buckets: {len(client.rest._session.bucket_ratelimiters.real_hashes_to_buckets)}\n" + f"actively limited routes: {actively_limited_routes}\n" + f"actively limited calls: {actively_limited_calls}" ) embed = hikari.Embed( diff --git a/hikari/internal/more_logging.py b/hikari/internal/more_logging.py deleted file mode 100644 index 4333bd130d..0000000000 --- a/hikari/internal/more_logging.py +++ /dev/null @@ -1,61 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# Copyright © Nekokatt 2019-2020 -# -# This file is part of Hikari. -# -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . -"""Utilities for creating and naming loggers with consistent names. - -|internal| -""" - -__all__ = ["get_named_logger"] - -import logging -import typing - - -def get_named_logger(obj: typing.Any, *extra_objs: typing.Any) -> logging.Logger: - """Build an appropriately named logger. - - If the passed object is an instance of a class, the class is used instead. - - If a class is provided/used, then the class name is used to name the logger. - - If a string is provided, then the string is used as the name. This is not recommended. - - Parameters - ---------- - obj - The object to study to produce a logger for. - extra_objs - optional extra components to add to the end of the logger name. - - Returns - ------- - :obj:`~logging.Logger` - A created logger. - """ - if not isinstance(obj, str): - if not isinstance(obj, type): - obj = type(obj) - - obj = obj.__qualname__ - - if extra_objs: - extras = ", ".join(map(str, extra_objs)) - obj = f"{obj}[{extras}]" - - return logging.getLogger(obj) diff --git a/hikari/net/ratelimits.py b/hikari/net/ratelimits.py index 7dad9ed9b4..c637aa3802 100644 --- a/hikari/net/ratelimits.py +++ b/hikari/net/ratelimits.py @@ -180,7 +180,6 @@ import weakref from hikari.internal import more_asyncio -from hikari.internal import more_logging from hikari.net import routes #: The hash used for an unknown bucket that has not yet been resolved. @@ -256,7 +255,7 @@ def __init__(self, name: str) -> None: self.name = name self.throttle_task = None self.queue = [] - self.logger: logging.Logger = more_logging.get_named_logger(self) + self.logger = logging.getLogger(f"hikari.net.ratelimits.{type(self).__qualname__}.{name}") @abc.abstractmethod def acquire(self) -> more_asyncio.Future[None]: @@ -321,7 +320,7 @@ class ManualRateLimiter(BurstRateLimiter): __slots__ = () def __init__(self) -> None: - super().__init__("global REST") + super().__init__("global") def acquire(self) -> more_asyncio.Future[None]: """Acquire time on this rate limiter. @@ -715,7 +714,7 @@ def __init__(self) -> None: self.real_hashes_to_buckets = {} self.closed_event: asyncio.Event = asyncio.Event() self.gc_task: typing.Optional[asyncio.Task] = None - self.logger: logging.Logger = more_logging.get_named_logger(self) + self.logger = logging.getLogger(f"hikari.net.ratelimits.{type(self).__qualname__}") def __enter__(self) -> "RESTBucketManager": return self diff --git a/hikari/net/rest_sessions.py b/hikari/net/rest_sessions.py index 10c8f062a3..cc34ea8d59 100644 --- a/hikari/net/rest_sessions.py +++ b/hikari/net/rest_sessions.py @@ -33,10 +33,10 @@ import aiohttp.typedefs from hikari import errors +from hikari import unset from hikari.internal import assertions from hikari.internal import conversions from hikari.internal import more_collections -from hikari.internal import more_logging from hikari.internal import urls from hikari.net import codes from hikari.net import ratelimits @@ -113,8 +113,17 @@ class LowLevelRestfulClient: #: The :mod:`aiohttp` client session to make requests with. #: + #: This will be :obj:`~None` until the first request, due to limitations + #: with how :obj:`aiohttp.ClientSession` is able to be initialized outside + #: of a running event loop. + #: #: :type: :obj:`~aiohttp.ClientSession` - client_session: aiohttp.ClientSession + client_session: typing.Optional[aiohttp.ClientSession] + + #: The base connector for the :obj:`~aiohttp.ClientSession`, if provided. + #: + #: :type: :obj:`~aiohttp.BaseConnector`, optional + connector: typing.Optional[aiohttp.BaseConnector] #: The internal correlation ID for the number of requests sent. This will #: increase each time a REST request is sent. @@ -167,7 +176,7 @@ class LowLevelRestfulClient: #: You should not ever need to touch this implementation. #: #: :type: :obj:`~hikari.net.ratelimits.RESTBucketManager` - ratelimiter: ratelimits.RESTBucketManager + bucket_ratelimiters: ratelimits.RESTBucketManager #: The custom SSL context to use. #: @@ -233,11 +242,10 @@ def __init__( token: typing.Optional[str], version: int = VERSION_6, ) -> None: + self.client_session = None + self.connector = connector self.allow_redirects = allow_redirects - self.client_session = aiohttp.ClientSession( - connector=connector, version=aiohttp.HttpVersion11, json_serialize=json_serialize or json.dumps, - ) - self.logger = more_logging.get_named_logger(self) + self.logger = logging.getLogger(f"hikari.net.{type(self).__qualname__}") self.user_agent = user_agents.UserAgent().user_agent self.verify_ssl = verify_ssl self.proxy_url = proxy_url @@ -251,8 +259,7 @@ def __init__( self.global_ratelimiter = ratelimits.ManualRateLimiter() self.json_serialize = json_serialize self.json_deserialize = json_deserialize - self.ratelimiter = ratelimits.RESTBucketManager() - self.ratelimiter.start() + self.bucket_ratelimiters = ratelimits.RESTBucketManager() if token is not None and not token.startswith(self._AUTHENTICATION_SCHEMES): this_type = type(self).__name__ @@ -264,10 +271,13 @@ def __init__( async def close(self) -> None: """Shut down the REST client safely, and terminate any rate limiters executing in the background.""" with contextlib.suppress(Exception): - self.ratelimiter.close() + self.bucket_ratelimiters.close() + with contextlib.suppress(Exception): + self.global_ratelimiter.close() with contextlib.suppress(Exception): self.logger.debug("Closing %s", type(self).__qualname__) await self.client_session.close() + self.client_session = None async def __aenter__(self) -> "LowLevelRestfulClient": return self @@ -290,7 +300,15 @@ async def _request( suppress_authorization_header: bool = False, **kwargs, ) -> typing.Union[typing.Dict[str, typing.Any], typing.Sequence[typing.Any], None]: - bucket_ratelimit_future = self.ratelimiter.acquire(compiled_route) + if self.client_session is None: + self.client_session = aiohttp.ClientSession( + connector=self.connector, + version=aiohttp.HttpVersion11, + json_serialize=self.json_serialize or json.dumps, + ) + self.bucket_ratelimiters.start() + + bucket_ratelimit_future = self.bucket_ratelimiters.acquire(compiled_route) request_headers = {"X-RateLimit-Precision": "millisecond"} if self.token is not None and not suppress_authorization_header: @@ -399,7 +417,7 @@ async def _request( ) body = None - self.ratelimiter.update_rate_limits(compiled_route, bucket, remaining, limit, now_date, reset_date) + self.bucket_ratelimiters.update_rate_limits(compiled_route, bucket, remaining, limit, now_date, reset_date) if status == codes.HTTPStatusCode.TOO_MANY_REQUESTS: # We are being rate limited. diff --git a/hikari/net/shards.py b/hikari/net/shards.py index dc540e99d1..21205befa9 100644 --- a/hikari/net/shards.py +++ b/hikari/net/shards.py @@ -48,7 +48,6 @@ from hikari import errors from hikari.internal import more_asyncio -from hikari.internal import more_logging from hikari.net import codes from hikari.net import ratelimits from hikari.net import user_agents @@ -159,47 +158,68 @@ class ShardConnection: """ __slots__ = ( - "closed_event", "_compression", "_connected_at", "_connector", "_debug", - "disconnect_count", - "dispatch", - "heartbeat_interval", - "heartbeat_latency", - "hello_event", - "handshake_event", "_intents", - "_large_threshold", "_json_deserialize", "_json_serialize", - "last_heartbeat_sent", - "last_message_received", - "logger", + "_large_threshold", "_presence", "_proxy_auth", "_proxy_headers", "_proxy_url", "_ratelimiter", - "ready_event", - "resumed_event", - "requesting_close_event", "_session", - "session_id", - "seq", - "shard_id", - "shard_count", "_ssl_context", - "status", "_token", "_url", "_verify_ssl", - "version", "_ws", "_zlib", + "closed_event", + "disconnect_count", + "dispatch", + "handshake_event", + "heartbeat_interval", + "heartbeat_latency", + "hello_event", + "last_heartbeat_sent", + "last_message_received", + "logger", + "ready_event", + "requesting_close_event", + "resumed_event", + "seq", + "session_id", + "shard_count", + "shard_id", + "status", + "version", ) + _compression: bool + _connected_at: float + _connector: typing.Optional[aiohttp.BaseConnector] + _debug: bool + _intents: typing.Optional[codes.GatewayIntent] + _large_threshold: int + _json_deserialize: typing.Callable[[typing.AnyStr], typing.Dict] + _json_serialize: typing.Callable[[typing.Dict], typing.AnyStr] + _presence: typing.Optional[typing.Dict] + _proxy_auth: typing.Optional[aiohttp.BasicAuth] + _proxy_headers: typing.Optional[aiohttp.typedefs.LooseHeaders] + _proxy_url: typing.Optional[str] + _ratelimiter: ratelimits.WindowedBurstRateLimiter + _session: typing.Optional[aiohttp.ClientSession] + _ssl_context: typing.Optional[ssl.SSLContext] + _token: str + _url: str + _verify_ssl: bool + _ws: typing.Optional[aiohttp.ClientWebSocketResponse] + _zlib: typing.Optional[zlib.decompressobj] + #: An event that is set when the connection closes. #: #: :type: :obj:`~asyncio.Event` @@ -351,47 +371,44 @@ def __init__( url = urllib.parse.urlunparse((scheme, netloc, path, params, new_query, "")) - self._compression: bool = compression - self._connected_at: float = float("nan") - self._connector: typing.Optional[aiohttp.BaseConnector] = connector - self._debug: bool = debug - self._intents: typing.Optional[intents.GatewayIntent] = intents - self._large_threshold: int = large_threshold - self._json_deserialize: typing.Callable[[typing.AnyStr], typing.Dict] = json_deserialize - self._json_serialize: typing.Callable[[typing.Dict], typing.AnyStr] = json_serialize - self._presence: typing.Optional[typing.Dict] = initial_presence - self._proxy_auth: typing.Optional[aiohttp.BasicAuth] = proxy_auth - self._proxy_headers: typing.Optional[aiohttp.typedefs.LooseHeaders] = proxy_headers - self._proxy_url: typing.Optional[str] = proxy_url - self._ratelimiter: ratelimits.WindowedBurstRateLimiter = ratelimits.WindowedBurstRateLimiter( - f"gateway shard {shard_id}/{shard_count}", 60.0, 120 - ) + self._compression = compression + self._connected_at = float("nan") + self._connector = connector + self._debug = debug + self._intents = intents + self._large_threshold = large_threshold + self._json_deserialize = json_deserialize + self._json_serialize = json_serialize + self._presence = initial_presence + self._proxy_auth = proxy_auth + self._proxy_headers = proxy_headers + self._proxy_url = proxy_url + self._ratelimiter = ratelimits.WindowedBurstRateLimiter(str(shard_id), 60.0, 120) self._session: typing.Optional[aiohttp.ClientSession] = None self._ssl_context: typing.Optional[ssl.SSLContext] = ssl_context - self._token: str = token - self._url: str = url - self._verify_ssl: bool = verify_ssl - self._ws: typing.Optional[aiohttp.ClientWebSocketResponse] = None - self._zlib: typing.Optional[zlib.decompressobj] = None - self.closed_event: asyncio.Event = asyncio.Event() - self.disconnect_count: int = 0 - self.dispatch: DispatchT = dispatch - self.heartbeat_interval: float = float("nan") - self.heartbeat_latency: float = float("nan") - self.hello_event: asyncio.Event = asyncio.Event() - self.handshake_event: asyncio.Event = asyncio.Event() - self.last_heartbeat_sent: float = float("nan") - self.last_message_received: float = float("nan") - self.requesting_close_event: asyncio.Event = asyncio.Event() - self.ready_event: asyncio.Event = asyncio.Event() - self.resumed_event: asyncio.Event = asyncio.Event() + self._token = token + self._url = url + self._verify_ssl = verify_ssl + self._ws = None + self._zlib = None + self.closed_event = asyncio.Event() + self.disconnect_count = 0 + self.dispatch = dispatch + self.heartbeat_interval = float("nan") + self.heartbeat_latency = float("nan") + self.hello_event = asyncio.Event() + self.handshake_event = asyncio.Event() + self.last_heartbeat_sent = float("nan") + self.last_message_received = float("nan") + self.logger = logging.getLogger(f"hikari.net.{type(self).__qualname__}.{shard_id}") + self.requesting_close_event = asyncio.Event() + self.ready_event = asyncio.Event() + self.resumed_event = asyncio.Event() self.session_id = session_id - self.seq: typing.Optional[int] = seq - self.shard_id: int = shard_id - self.shard_count: int = shard_count - self.version: int = version - - self.logger: logging.Logger = more_logging.get_named_logger(self, f"#{shard_id}", f"v{self.version}") + self.seq = seq + self.shard_id = shard_id + self.shard_count = shard_count + self.version = version @property def uptime(self) -> datetime.timedelta: diff --git a/hikari/state/event_dispatchers.py b/hikari/state/event_dispatchers.py index 1859736f6b..7c45131f8e 100644 --- a/hikari/state/event_dispatchers.py +++ b/hikari/state/event_dispatchers.py @@ -33,17 +33,22 @@ from hikari.internal import conversions from hikari.internal import more_asyncio from hikari.internal import more_collections -from hikari.internal import more_logging # Prevents a circular reference that prevents importing correctly. if typing.TYPE_CHECKING: EventT = typing.TypeVar("EventT", bound=events.HikariEvent) - PredicateT = typing.Callable[[EventT], typing.Union[bool, typing.Coroutine[None, None, bool]]] - EventCallbackT = typing.Callable[[EventT], typing.Coroutine[None, None, typing.Any]] + PredicateT = typing.Callable[[EventT], typing.Union[bool, typing.Coroutine[typing.Any, typing.Any, bool]]] + EventCallbackT = typing.Callable[[EventT], typing.Coroutine[typing.Any, typing.Any, typing.Any]] + WaiterMapT = typing.Dict[ + typing.Type[EventT], more_collections.WeakKeyDictionary[more_asyncio.Future[typing.Any], PredicateT] + ] + ListenerMapT = typing.Dict[typing.Type[EventT], typing.List[EventCallbackT]] else: - EventT = typing.TypeVar("EventT") - PredicateT = typing.TypeVar("PredicateT") - EventCallbackT = typing.TypeVar("EventCallbackT") + EventT = typing.Any + PredicateT = typing.Any + EventCallbackT = typing.Any + WaiterMapT = typing.Any + ListenerMapT = typing.Any class EventDispatcher(abc.ABC): @@ -263,13 +268,11 @@ class EventDispatcherImpl(EventDispatcher): logger: logging.Logger def __init__(self) -> None: - self._listeners: typing.Dict[typing.Type[EventT], typing.List[EventCallbackT]] = {} + self._listeners: ListenerMapT = {} # pylint: disable=E1136 - self._waiters: typing.Dict[ - typing.Type[EventT], more_collections.WeakKeyDictionary[asyncio.Future, PredicateT] - ] = {} + self._waiters: WaiterMapT = {} # pylint: enable=E1136 - self.logger = more_logging.get_named_logger(self) + self.logger = logging.getLogger(type(self).__qualname__) def close(self) -> None: """Cancel anything that is waiting for an event to be dispatched.""" @@ -349,9 +352,10 @@ def dispatch_event(self, event: events.HikariEvent): # Only try to awaken waiters when the waiter is registered as a valid # event type and there is more than 0 waiters in that dict. - if waiters := self._waiters.get(event_t): + if (waiters := self._waiters.get(event_t)) is not None: # Run this in the background as a coroutine so that any async predicates # can be awaited concurrently. + # noinspection PyTypeChecker futs.append(asyncio.create_task(self._awaken_waiters(waiters, event))) result = asyncio.gather(*futs) if futs else more_asyncio.completed_future() # lgtm [py/unused-local-variable] diff --git a/hikari/state/event_managers.py b/hikari/state/event_managers.py index 9909a2fa5e..f18a075753 100644 --- a/hikari/state/event_managers.py +++ b/hikari/state/event_managers.py @@ -20,12 +20,12 @@ __all__ = ["raw_event_mapper", "EventManager"] import inspect +import logging import typing from hikari import entities from hikari.clients import shard_clients from hikari.internal import assertions -from hikari.internal import more_logging from hikari.state import event_dispatchers from hikari.state import raw_event_consumers @@ -139,7 +139,7 @@ def __init__(self, event_dispatcher_impl: typing.Optional[EventDispatcherT] = No if event_dispatcher_impl is None: event_dispatcher_impl = event_dispatchers.EventDispatcherImpl() - self.logger = more_logging.get_named_logger(self) + self.logger = logging.getLogger(type(self).__qualname__) self.event_dispatcher = event_dispatcher_impl self.raw_event_mappers = {} diff --git a/pylint.ini b/pylint.ini index 945551886e..a5d5514880 100644 --- a/pylint.ini +++ b/pylint.ini @@ -11,7 +11,7 @@ ignore=CVS # Add files or directories matching the regex patterns to the blacklist. The # regex matches against base names, not paths. -ignore-patterns= +ignore-patterns=test_client.py # Python code to execute, usually for sys.path manipulation such as # pygtk.require(). diff --git a/tests/hikari/clients/test_configs.py b/tests/hikari/clients/test_configs.py index 41d80ae943..e9e8437263 100644 --- a/tests/hikari/clients/test_configs.py +++ b/tests/hikari/clients/test_configs.py @@ -64,25 +64,22 @@ def test_websocket_config(test_debug_config, test_aiohttp_config, test_token_con "initial_idle_since": None, # Set in test "intents": 513, "large_threshold": 1000, + "shard_ids": "5...10", + "shard_count": "17", **test_debug_config, **test_aiohttp_config, **test_token_config, } -@pytest.fixture -def test_shard_config(): - return {"shard_ids": "5...10", "shard_count": "17"} - - @pytest.fixture def test_rest_config(test_aiohttp_config, test_token_config): return {"rest_version": 6, **test_aiohttp_config, **test_token_config} @pytest.fixture -def test_bot_config(test_rest_config, test_shard_config, test_websocket_config): - return {**test_rest_config, **test_shard_config, **test_websocket_config} +def test_bot_config(test_rest_config, test_websocket_config): + return {**test_rest_config, **test_websocket_config} class TestDebugConfig: @@ -141,7 +138,7 @@ class TestWebsocketConfig: def test_deserialize(self, test_websocket_config): datetime_obj = datetime.datetime.now() test_websocket_config["initial_idle_since"] = datetime_obj.timestamp() - websocket_config_obj = configs.WebsocketConfig.deserialize(test_websocket_config) + websocket_config_obj = configs.GatewayConfig.deserialize(test_websocket_config) assert websocket_config_obj.gateway_use_compression is False assert websocket_config_obj.gateway_version == 7 @@ -164,9 +161,11 @@ def test_deserialize(self, test_websocket_config): assert websocket_config_obj.ssl_context == ssl.SSLContext assert websocket_config_obj.verify_ssl is False assert websocket_config_obj.token == "token" + assert websocket_config_obj.shard_ids == [5, 6, 7, 8, 9, 10] + assert websocket_config_obj.shard_count == 17 def test_empty_deserialize(self): - websocket_config_obj = configs.WebsocketConfig.deserialize({}) + websocket_config_obj = configs.GatewayConfig.deserialize({}) assert websocket_config_obj.gateway_use_compression is True assert websocket_config_obj.gateway_version == 6 @@ -185,6 +184,8 @@ def test_empty_deserialize(self): assert websocket_config_obj.ssl_context is None assert websocket_config_obj.verify_ssl is True assert websocket_config_obj.token is None + assert websocket_config_obj.shard_ids is None + assert websocket_config_obj.shard_count is None class TestParseShardInfo: @@ -205,20 +206,6 @@ def test__parse_shard_info_when_invalid(self): configs._parse_shard_info("something invalid") -class TestShardConfig: - def test_deserialize(self, test_shard_config): - shard_config_obj = configs.ShardConfig.deserialize(test_shard_config) - - assert shard_config_obj.shard_ids == [5, 6, 7, 8, 9, 10] - assert shard_config_obj.shard_count == 17 - - def test_empty_deserialize(self): - shard_config_obj = configs.ShardConfig.deserialize({}) - - assert shard_config_obj.shard_ids is None - assert shard_config_obj.shard_count is None - - class TestRESTConfig: def test_deserialize(self, test_rest_config): rest_config_obj = configs.RESTConfig.deserialize(test_rest_config) diff --git a/tests/hikari/clients/test_gateway_managers.py b/tests/hikari/clients/test_gateway_managers.py deleted file mode 100644 index e4ed2a8782..0000000000 --- a/tests/hikari/clients/test_gateway_managers.py +++ /dev/null @@ -1,296 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# Copyright © Nekoka.tt 2019-2020 -# -# This file is part of Hikari. -# -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along ith Hikari. If not, see . -import math - -import mock -import pytest - -from hikari.clients import configs -from hikari.clients import gateway_managers -from hikari.clients import shard_clients -from hikari.net import codes -from tests.hikari import _helpers - - -class TestGatewayManagerProperties: - def test_heartbeat_latency(self): - shard1 = mock.MagicMock(shard_clients.ShardClient, heartbeat_latency=20) - shard2 = mock.MagicMock(shard_clients.ShardClient, heartbeat_latency=30) - shard3 = mock.MagicMock(shard_clients.ShardClient, heartbeat_latency=40) - - with mock.patch("hikari.clients.shard_clients.ShardClient", side_effect=[shard1, shard2, shard3]): - gateway_manager_obj = gateway_managers.GatewayManager( - shard_ids=[0, 1, 2], - shard_count=3, - config=None, - url="some_url", - raw_event_consumer_impl=None, - shard_type=shard_clients.ShardClient, - ) - - assert gateway_manager_obj.heartbeat_latency == 30 - - def test_heartbeat_latency_doesnt_take_into_a_count_shards_with_no_latency(self): - shard1 = mock.MagicMock(shard_clients.ShardClient, heartbeat_latency=20) - shard2 = mock.MagicMock(shard_clients.ShardClient, heartbeat_latency=30) - shard3 = mock.MagicMock(shard_clients.ShardClient, heartbeat_latency=float("nan")) - - with mock.patch("hikari.clients.shard_clients.ShardClient", side_effect=[shard1, shard2, shard3]): - gateway_manager_obj = gateway_managers.GatewayManager( - shard_ids=[0, 1, 2], - shard_count=3, - config=None, - url="some_url", - raw_event_consumer_impl=None, - shard_type=shard_clients.ShardClient, - ) - - assert gateway_manager_obj.heartbeat_latency == 25 - - def test_heartbeat_latency_returns_nan_if_all_shards_have_no_latency(self): - shard1 = mock.MagicMock(shard_clients.ShardClient, heartbeat_latency=float("nan")) - shard2 = mock.MagicMock(shard_clients.ShardClient, heartbeat_latency=float("nan")) - shard3 = mock.MagicMock(shard_clients.ShardClient, heartbeat_latency=float("nan")) - - with mock.patch("hikari.clients.shard_clients.ShardClient", side_effect=[shard1, shard2, shard3]): - gateway_manager_obj = gateway_managers.GatewayManager( - shard_ids=[0, 1, 2], - shard_count=3, - config=None, - url="some_url", - raw_event_consumer_impl=None, - shard_type=shard_clients.ShardClient, - ) - - assert math.isnan(gateway_manager_obj.heartbeat_latency) - - def test_total_disconnect_count(self): - shard1 = mock.MagicMock(shard_clients.ShardClient, disconnect_count=7) - shard2 = mock.MagicMock(shard_clients.ShardClient, disconnect_count=2) - shard3 = mock.MagicMock(shard_clients.ShardClient, disconnect_count=13) - - with mock.patch("hikari.clients.shard_clients.ShardClient", side_effect=[shard1, shard2, shard3]): - gateway_manager_obj = gateway_managers.GatewayManager( - shard_ids=[0, 1, 2], - shard_count=3, - config=None, - url="some_url", - raw_event_consumer_impl=None, - shard_type=shard_clients.ShardClient, - ) - - assert gateway_manager_obj.total_disconnect_count == 22 - - def test_total_reconnect_count(self): - shard1 = mock.MagicMock(shard_clients.ShardClient, reconnect_count=7) - shard2 = mock.MagicMock(shard_clients.ShardClient, reconnect_count=2) - shard3 = mock.MagicMock(shard_clients.ShardClient, reconnect_count=13) - - with mock.patch("hikari.clients.shard_clients.ShardClient", side_effect=[shard1, shard2, shard3]): - gateway_manager_obj = gateway_managers.GatewayManager( - shard_ids=[0, 1, 2], - shard_count=3, - config=None, - url="some_url", - raw_event_consumer_impl=None, - shard_type=shard_clients.ShardClient, - ) - - assert gateway_manager_obj.total_reconnect_count == 22 - - def test_intents(self): - shard1 = mock.MagicMock(shard_clients.ShardClient) - shard2 = mock.MagicMock(shard_clients.ShardClient) - shard3 = mock.MagicMock(shard_clients.ShardClient) - - intents = codes.GatewayIntent.DIRECT_MESSAGE_TYPING | codes.GatewayIntent.DIRECT_MESSAGE_REACTIONS - - with mock.patch("hikari.clients.shard_clients.ShardClient", side_effect=[shard1, shard2, shard3]): - gateway_manager_obj = gateway_managers.GatewayManager( - shard_ids=[0, 1, 2], - shard_count=3, - config=configs.BotConfig(intents=intents), - url="some_url", - raw_event_consumer_impl=None, - shard_type=shard_clients.ShardClient, - ) - - assert gateway_manager_obj.intents == intents - - def test_version(self): - shard1 = mock.MagicMock(shard_clients.ShardClient) - shard2 = mock.MagicMock(shard_clients.ShardClient) - shard3 = mock.MagicMock(shard_clients.ShardClient) - - version = 7 - - with mock.patch("hikari.clients.shard_clients.ShardClient", side_effect=[shard1, shard2, shard3]): - gateway_manager_obj = gateway_managers.GatewayManager( - shard_ids=[0, 1, 2], - shard_count=3, - config=configs.BotConfig(gateway_version=version), - url="some_url", - raw_event_consumer_impl=None, - shard_type=shard_clients.ShardClient, - ) - - assert gateway_manager_obj.version == version - - -class TestGatewayManagerManagement: - @pytest.mark.asyncio - async def test_start_waits_five_seconds_between_shard_startup(self): - mock_sleep = mock.AsyncMock() - - class MockStart(mock.AsyncMock): - def __init__(self, condition): - super().__init__() - self.condition = condition - - def __call__(self): - if self.condition: - mock_sleep.assert_called_once_with(5) - mock_sleep.reset_mock() - else: - mock_sleep.assert_not_called() - - return super().__call__() - - shard1 = mock.MagicMock(shard_clients.ShardClient, start=MockStart(condition=False)) - shard2 = mock.MagicMock(shard_clients.ShardClient, start=MockStart(condition=True)) - shard3 = mock.MagicMock(shard_clients.ShardClient, start=MockStart(condition=True)) - - with mock.patch("hikari.clients.shard_clients.ShardClient", side_effect=[shard1, shard2, shard3]): - with mock.patch("asyncio.sleep", wraps=mock_sleep): - gateway_manager_obj = gateway_managers.GatewayManager( - shard_ids=[0, 1, 2], - shard_count=3, - config=None, - url="some_url", - raw_event_consumer_impl=None, - shard_type=shard_clients.ShardClient, - ) - await gateway_manager_obj.start() - mock_sleep.assert_not_called() - - shard1.start.assert_called_once() - shard2.start.assert_called_once() - shard3.start.assert_called_once() - - @pytest.mark.asyncio - async def test_join_calls_join_on_all_shards(self): - shard1 = mock.MagicMock(shard_clients.ShardClient, join=mock.MagicMock()) - shard2 = mock.MagicMock(shard_clients.ShardClient, join=mock.MagicMock()) - shard3 = mock.MagicMock(shard_clients.ShardClient, join=mock.MagicMock()) - - with mock.patch("hikari.clients.shard_clients.ShardClient", side_effect=[shard1, shard2, shard3]): - with mock.patch("asyncio.gather", return_value=_helpers.AwaitableMock()): - gateway_manager_obj = gateway_managers.GatewayManager( - shard_ids=[0, 1, 2], - shard_count=3, - config=None, - url="some_url", - raw_event_consumer_impl=None, - shard_type=shard_clients.ShardClient, - ) - await gateway_manager_obj.join() - - shard1.join.assert_called_once() - shard2.join.assert_called_once() - shard3.join.assert_called_once() - - @pytest.mark.asyncio - async def test_close_closes_all_shards(self): - shard1 = mock.MagicMock(shard_clients.ShardClient, close=mock.MagicMock()) - shard2 = mock.MagicMock(shard_clients.ShardClient, close=mock.MagicMock()) - shard3 = mock.MagicMock(shard_clients.ShardClient, close=mock.MagicMock()) - - with mock.patch("hikari.clients.shard_clients.ShardClient", side_effect=[shard1, shard2, shard3]): - with mock.patch("asyncio.gather", return_value=_helpers.AwaitableMock()): - gateway_manager_obj = gateway_managers.GatewayManager( - shard_ids=[0, 1, 2], - shard_count=3, - config=None, - url="some_url", - raw_event_consumer_impl=None, - shard_type=shard_clients.ShardClient, - ) - gateway_manager_obj._is_running = True - await gateway_manager_obj.close() - - shard1.close.assert_called_once_with() - shard2.close.assert_called_once_with() - shard3.close.assert_called_once_with() - - @pytest.mark.asyncio - async def test_close_does_nothing_if_not_running(self): - shard1 = mock.MagicMock(shard_clients.ShardClient, close=mock.MagicMock()) - shard2 = mock.MagicMock(shard_clients.ShardClient, close=mock.MagicMock()) - shard3 = mock.MagicMock(shard_clients.ShardClient, close=mock.MagicMock()) - - with mock.patch("hikari.clients.shard_clients.ShardClient", side_effect=[shard1, shard2, shard3]): - with mock.patch("asyncio.gather", return_value=_helpers.AwaitableMock()): - gateway_manager_obj = gateway_managers.GatewayManager( - shard_ids=[0, 1, 2], - shard_count=3, - config=None, - url="some_url", - raw_event_consumer_impl=None, - shard_type=shard_clients.ShardClient, - ) - gateway_manager_obj._is_running = False - await gateway_manager_obj.close() - - shard1.close.assert_not_called() - shard2.close.assert_not_called() - shard3.close.assert_not_called() - - @pytest.mark.asyncio - async def test_update_presence_updates_presence_in_all_ready_or_waiting_for_ready_shards(self): - shard1 = mock.MagicMock( - shard_clients.ShardClient, - update_presence=mock.MagicMock(), - connection_state=shard_clients.ShardState.READY, - ) - shard2 = mock.MagicMock( - shard_clients.ShardClient, - update_presence=mock.MagicMock(), - connection_state=shard_clients.ShardState.WAITING_FOR_READY, - ) - shard3 = mock.MagicMock( - shard_clients.ShardClient, - update_presence=mock.MagicMock(), - connection_state=shard_clients.ShardState.CONNECTING, - ) - - with mock.patch("hikari.clients.shard_clients.ShardClient", side_effect=[shard1, shard2, shard3]): - with mock.patch("asyncio.gather", return_value=_helpers.AwaitableMock()): - gateway_manager_obj = gateway_managers.GatewayManager( - shard_ids=[0, 1, 2], - shard_count=3, - config=None, - url="some_url", - raw_event_consumer_impl=None, - shard_type=shard_clients.ShardClient, - ) - await gateway_manager_obj.update_presence(status=None, activity=None, idle_since=None, is_afk=True) - - shard1.update_presence.assert_called_once_with(status=None, activity=None, idle_since=None, is_afk=True) - shard2.update_presence.assert_called_once_with(status=None, activity=None, idle_since=None, is_afk=True) - shard3.update_presence.assert_not_called() diff --git a/tests/hikari/clients/test_rest_clients/test_component_base.py b/tests/hikari/clients/test_rest_clients/test_component_base.py index dd7297d885..d9b7550f97 100644 --- a/tests/hikari/clients/test_rest_clients/test_component_base.py +++ b/tests/hikari/clients/test_rest_clients/test_component_base.py @@ -42,11 +42,3 @@ async def test___aenter___and___aexit__(self, rest_clients_impl): async with rest_clients_impl as client: assert client is rest_clients_impl rest_clients_impl.close.assert_called_once_with() - - @pytest.mark.asyncio - async def test_close_awaits_session_close(self, rest_clients_impl): - await rest_clients_impl.close() - rest_clients_impl._session.close.assert_called_once() - - def test_session_property(self, low_level_rest_impl, rest_clients_impl): - assert rest_clients_impl.session is low_level_rest_impl diff --git a/tests/hikari/clients/test_shard_clients.py b/tests/hikari/clients/test_shard_clients.py index 50fc072c19..4841e7133f 100644 --- a/tests/hikari/clients/test_shard_clients.py +++ b/tests/hikari/clients/test_shard_clients.py @@ -56,16 +56,16 @@ def shard_client_obj(): session_id=None, ) with mock.patch("hikari.net.shards.ShardConnection", return_value=mock_shard_connection): - return _helpers.unslot_class(shard_clients.ShardClient)(0, 1, configs.WebsocketConfig(), None, "some_url") + return _helpers.unslot_class(shard_clients.ShardClientImpl)(0, 1, configs.GatewayConfig(), None, "some_url") -class TestShardClient: - def test_raw_event_consumer_in_shardclient(self): +class TestShardClientImpl: + def test_raw_event_consumer_in_ShardClientImpl(self): class DummyConsumer(raw_event_consumers.RawEventConsumer): def process_raw_event(self, _client, name, payload): return "ASSERT TRUE" - shard_client_obj = shard_clients.ShardClient(0, 1, configs.WebsocketConfig(), DummyConsumer(), "some_url") + shard_client_obj = shard_clients.ShardClientImpl(0, 1, configs.GatewayConfig(), DummyConsumer(), "some_url") assert shard_client_obj._connection.dispatch(shard_client_obj, "TEST", {}) == "ASSERT TRUE" @@ -73,12 +73,12 @@ def test_connection_is_set(self, shard_client_obj): mock_shard_connection = mock.MagicMock(shards.ShardConnection) with mock.patch("hikari.net.shards.ShardConnection", return_value=mock_shard_connection): - shard_client_obj = shard_clients.ShardClient(0, 1, configs.WebsocketConfig(), None, "some_url") + shard_client_obj = shard_clients.ShardClientImpl(0, 1, configs.GatewayConfig(), None, "some_url") assert shard_client_obj._connection is mock_shard_connection -class TestShardClientDelegateProperties: +class TestShardClientImplDelegateProperties: def test_status(self, shard_client_obj): marker = object() shard_client_obj._status = marker @@ -150,7 +150,7 @@ def test_intents(self, shard_client_obj): assert shard_client_obj.intents is marker -class TestShardClientStart: +class TestShardClientImplStart: @pytest.mark.asyncio async def test_start_when_ready_event_completes_first(self, shard_client_obj): shard_client_obj._keep_alive = mock.AsyncMock() @@ -290,7 +290,7 @@ def side_effect(*args): await shard_client_obj._keep_alive() -class TestShardClientSpinUp: +class TestShardClientImplSpinUp: @_helpers.assert_raises(type_=RuntimeError) @pytest.mark.asyncio async def test__spin_up_if_connect_task_is_completed_raises_exception_during_hello_event(self, shard_client_obj): @@ -348,7 +348,7 @@ async def test__spin_up_if_connect_task_is_completed_raises_exception_during_rea await shard_client_obj._spin_up() -class TestShardClientUpdatePresence: +class TestShardClientImplUpdatePresence: @pytest.mark.asyncio async def test_update_presence(self, shard_client_obj): await shard_client_obj.update_presence() diff --git a/tests/hikari/internal/test_more_logging.py b/tests/hikari/internal/test_more_logging.py deleted file mode 100644 index fc685c38f6..0000000000 --- a/tests/hikari/internal/test_more_logging.py +++ /dev/null @@ -1,55 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# Copyright © Nekoka.tt 2019-2020 -# -# This file is part of Hikari. -# -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . - -from hikari.internal import more_logging - - -class Dummy: - class NestedDummy: - pass - - -def test_get_named_logger_with_global_class(): - logger = more_logging.get_named_logger(Dummy) - assert logger.name == "Dummy" - - -def test_get_named_logger_with_nested_class(): - logger = more_logging.get_named_logger(Dummy.NestedDummy) - assert logger.name == "Dummy.NestedDummy" - - -def test_get_named_logger_with_global_class_instance(): - logger = more_logging.get_named_logger(Dummy()) - assert logger.name == "Dummy" - - -def test_get_named_logger_with_nested_class_instance(): - logger = more_logging.get_named_logger(Dummy.NestedDummy()) - assert logger.name == "Dummy.NestedDummy" - - -def test_get_named_logger_with_string(): - logger = more_logging.get_named_logger("potato") - assert logger.name == "potato" - - -def test_get_named_logger_with_extras(): - logger = more_logging.get_named_logger("potato", "foo", "bar", "baz") - assert logger.name == "potato[foo, bar, baz]" diff --git a/tests/hikari/net/test_rest_sessions.py b/tests/hikari/net/test_rest_sessions.py index 626bd52b70..379d499655 100644 --- a/tests/hikari/net/test_rest_sessions.py +++ b/tests/hikari/net/test_rest_sessions.py @@ -40,7 +40,6 @@ class TestLowLevelRestfulClient: def rest_impl(self): stack = contextlib.ExitStack() stack.enter_context(mock.patch("aiohttp.ClientSession")) - stack.enter_context(mock.patch("hikari.internal.more_logging.get_named_logger")) stack.enter_context(mock.patch.object(ratelimits, "RESTBucketManager")) stack.enter_context(mock.patch.object(ratelimits, "ManualRateLimiter")) with stack: @@ -48,6 +47,7 @@ def rest_impl(self): base_url="https://discordapp.com/api/v6", token="Bot blah.blah.blah" ) client._request = mock.AsyncMock(return_value=...) + client.client_session = mock.MagicMock(aiohttp.ClientSession, spec_set=True) return client @@ -96,31 +96,23 @@ async def test_rest___aenter___and___aexit__(self, rest_impl): rest_impl.close.assert_called_once_with() - @pytest.mark.asyncio - async def test_rest_close_calls_client_session_close(self, rest_impl): - await rest_impl.close() - rest_impl.client_session.close.assert_called_with() - @pytest.mark.asyncio async def test__init__with_bot_token_and_without_optionals(self): - mock_manual_rate_limiter = mock.MagicMock() - buckets_mock = mock.MagicMock() - mock_client_session = mock.MagicMock(aiohttp.ClientSession) - + mock_manual_rate_limiter = mock.MagicMock(close=mock.MagicMock()) + buckets_mock = mock.MagicMock(close=mock.MagicMock()) stack = contextlib.ExitStack() stack.enter_context(mock.patch.object(ratelimits, "ManualRateLimiter", return_value=mock_manual_rate_limiter)) stack.enter_context(mock.patch.object(ratelimits, "RESTBucketManager", return_value=buckets_mock)) - stack.enter_context(mock.patch.object(aiohttp, "ClientSession", return_value=mock_client_session)) with stack: client = rest_sessions.LowLevelRestfulClient(token="Bot token.otacon.a-token") assert client.base_url == f"https://discordapp.com/api/v{rest_sessions.VERSION_6}" - assert client.client_session is mock_client_session + assert client.client_session is None assert client.global_ratelimiter is mock_manual_rate_limiter assert client.json_serialize is json.dumps assert client.json_deserialize is json.loads - assert client.ratelimiter is buckets_mock + assert client.bucket_ratelimiters is buckets_mock assert client.token == "Bot token.otacon.a-token" @pytest.mark.asyncio @@ -128,7 +120,6 @@ async def test__init__with_bearer_token_and_without_optionals(self): stack = contextlib.ExitStack() stack.enter_context(mock.patch.object(ratelimits, "ManualRateLimiter")) stack.enter_context(mock.patch.object(ratelimits, "RESTBucketManager")) - stack.enter_context(mock.patch.object(aiohttp, "ClientSession")) with stack: client = rest_sessions.LowLevelRestfulClient(token="Bearer token.otacon.a-token") assert client.token == "Bearer token.otacon.a-token" @@ -145,7 +136,6 @@ async def test__init__with_optionals(self): mock_ssl_context = mock.MagicMock(ssl.SSLContext) stack = contextlib.ExitStack() - stack.enter_context(mock.patch.object(aiohttp, "ClientSession")) stack.enter_context(mock.patch.object(ratelimits, "ManualRateLimiter", return_value=mock_manual_rate_limiter)) stack.enter_context( mock.patch.object(ratelimits, "RESTBucketManager", return_value=mock_http_bucket_rate_limit_manager) @@ -170,7 +160,7 @@ async def test__init__with_optionals(self): assert client.global_ratelimiter is mock_manual_rate_limiter assert client.json_serialize is mock_dumps assert client.json_deserialize is mock_loads - assert client.ratelimiter is mock_http_bucket_rate_limit_manager + assert client.bucket_ratelimiters is mock_http_bucket_rate_limit_manager assert client.token == "Bot token.otacon.a-token" @pytest.mark.asyncio @@ -179,16 +169,85 @@ async def test__init__raises_runtime_error_with_invalid_token(self, *_): stack = contextlib.ExitStack() stack.enter_context(mock.patch.object(ratelimits, "ManualRateLimiter")) stack.enter_context(mock.patch.object(ratelimits, "RESTBucketManager")) - stack.enter_context(mock.patch.object(aiohttp, "ClientSession")) with stack: async with rest_sessions.LowLevelRestfulClient(token="An-invalid-TOKEN"): pass @pytest.mark.asyncio - async def test_close(self, rest_impl): + async def test_close_when_session_is_unset(self, rest_impl): + rest_impl.bucket_ratelimiters = mock.MagicMock(close=mock.MagicMock()) + rest_impl.global_ratelimiter = mock.MagicMock(close=mock.MagicMock()) + rest_impl.client_session = None + await rest_impl.close() + assert rest_impl.client_session is None + + @pytest.mark.asyncio + async def test_close_when_session_is_set(self, rest_impl): + client_session = mock.MagicMock(aiohttp.ClientSession, close=mock.AsyncMock()) + rest_impl.bucket_ratelimiters = mock.MagicMock(close=mock.MagicMock()) + rest_impl.global_ratelimiter = mock.MagicMock(close=mock.MagicMock()) + rest_impl.client_session = client_session + await rest_impl.close() + assert rest_impl.client_session is None + client_session.close.assert_awaited_once() + + @pytest.mark.asyncio + async def test_close_shuts_down_bucket_rate_limiter(self, rest_impl): + rest_impl.bucket_ratelimiters = mock.MagicMock(close=mock.MagicMock()) + rest_impl.global_ratelimiter = mock.MagicMock(close=mock.MagicMock()) await rest_impl.close() - rest_impl.ratelimiter.close.assert_called_once_with() - rest_impl.client_session.close.assert_called_once_with() + rest_impl.bucket_ratelimiters.close.assert_called_once_with() + + @pytest.mark.asyncio + async def test_close_shuts_down_bucket_rate_limiter(self, rest_impl): + rest_impl.bucket_ratelimiters = mock.MagicMock(close=mock.MagicMock()) + rest_impl.global_ratelimiter = mock.MagicMock(close=mock.MagicMock()) + await rest_impl.close() + rest_impl.global_ratelimiter.close.assert_called_once_with() + + @pytest.mark.asyncio + async def test_first_call_to_request_opens_client_session(self): + stack = contextlib.ExitStack() + + manual_rate_limiter_mock = mock.MagicMock(ratelimits.ManualRateLimiter) + buckets_mock = mock.MagicMock(ratelimits.RESTBucketManager) + client_session_mock = mock.MagicMock(aiohttp.ClientSession) + + stack.enter_context(mock.patch.object(ratelimits, "ManualRateLimiter", return_value=manual_rate_limiter_mock)) + stack.enter_context(mock.patch.object(ratelimits, "RESTBucketManager", return_value=buckets_mock)) + stack.enter_context(mock.patch.object(aiohttp, "ClientSession", return_value=client_session_mock)) + + with stack: + client = rest_sessions.LowLevelRestfulClient(token="Bot token.otacon.a-token") + assert client.client_session is None # lazy init + with contextlib.suppress(Exception): + await client._request(mock.MagicMock(routes.CompiledRoute, spec_set=True)) + assert client.client_session is not None + + @pytest.mark.asyncio + async def test_more_calls_to_request_use_existing_client_session(self): + stack = contextlib.ExitStack() + + manual_rate_limiter_mock = mock.MagicMock(ratelimits.ManualRateLimiter) + buckets_mock = mock.MagicMock(ratelimits.RESTBucketManager) + client_session_mock = mock.MagicMock(aiohttp.ClientSession) + + stack.enter_context(mock.patch.object(ratelimits, "ManualRateLimiter", return_value=manual_rate_limiter_mock)) + stack.enter_context(mock.patch.object(ratelimits, "RESTBucketManager", return_value=buckets_mock)) + stack.enter_context(mock.patch.object(aiohttp, "ClientSession", return_value=client_session_mock)) + + with stack: + client = rest_sessions.LowLevelRestfulClient(token="Bot token.otacon.a-token") + with contextlib.suppress(Exception): + await client._request(mock.MagicMock(routes.CompiledRoute, spec_set=True)) + + session = client.client_session + + for i in range(20): + with contextlib.suppress(Exception): + await client._request(mock.MagicMock(routes.CompiledRoute, spec_set=True)) + + assert client.client_session is session @pytest.fixture() @mock.patch.object(ratelimits, "ManualRateLimiter") @@ -197,12 +256,14 @@ async def test_close(self, rest_impl): def rest_impl_with__request(self, *args): rest_impl = rest_sessions.LowLevelRestfulClient(token="Bot token") rest_impl.logger = mock.MagicMock(debug=mock.MagicMock()) - rest_impl.ratelimiter = mock.MagicMock( + rest_impl.bucket_ratelimiters = mock.MagicMock( ratelimits.RESTBucketManager, acquire=mock.MagicMock(), update_rate_limits=mock.MagicMock(), ) rest_impl.global_ratelimiter = mock.MagicMock( ratelimits.ManualRateLimiter, acquire=mock.MagicMock(), throttle=mock.MagicMock() ) + rest_impl.client_session = mock.MagicMock(aiohttp.ClientSession, request=mock.MagicMock()) + return rest_impl @pytest.mark.asyncio @@ -215,7 +276,7 @@ async def test__request_acquires_ratelimiter(self, compiled_route, exit_error, r except exit_error: pass - rest_impl_with__request.ratelimiter.acquire.asset_called_once_with(compiled_route) + rest_impl_with__request.bucket_ratelimiters.acquire.asset_called_once_with(compiled_route) @pytest.mark.asyncio async def test__request_sets_Authentication_if_token(self, compiled_route, exit_error, rest_impl_with__request): @@ -455,12 +516,12 @@ async def test__request_raises_appropriate_error_for_status_code( stack = contextlib.ExitStack() stack.enter_context(mock.patch.object(ratelimits, "ManualRateLimiter")) stack.enter_context(mock.patch.object(ratelimits, "RESTBucketManager")) - stack.enter_context(mock.patch.object(aiohttp, "ClientSession")) discord_response.status = status_code with stack: rest_impl = rest_sessions.LowLevelRestfulClient(token="Bot token", version=api_version) - rest_impl.ratelimiter = mock.MagicMock() + rest_impl.bucket_ratelimiters = mock.MagicMock() rest_impl.global_ratelimiter = mock.MagicMock() + rest_impl.client_session = mock.MagicMock(aiohttp.ClientSession) rest_impl.client_session.request = mock.MagicMock(return_value=discord_response) with mock.patch("asyncio.gather", return_value=_helpers.AwaitableMock()): From f62ee59fb23a01f18539d008dd37d89addf54c83 Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Sat, 18 Apr 2020 21:46:54 +0100 Subject: [PATCH 167/922] Fixed one of the warnings in tests. --- hikari/clients/shard_clients.py | 2 +- tests/hikari/__init__.py | 7 ++++ tests/hikari/clients/test_shard_clients.py | 40 ++++++++++++++++------ 3 files changed, 38 insertions(+), 11 deletions(-) diff --git a/hikari/clients/shard_clients.py b/hikari/clients/shard_clients.py index e015dbc1cf..6b992ae3d7 100644 --- a/hikari/clients/shard_clients.py +++ b/hikari/clients/shard_clients.py @@ -685,7 +685,7 @@ def __repr__(self) -> str: "ShardClient(" + ", ".join( f"{k}={getattr(self, k)!r}" - for k in ("shard_id", "shard_count", "connection_state", "heartbeat_interval", "latency") + for k in ("shard_id", "shard_count", "connection_state", "heartbeat_interval", "heartbeat_latency") ) + ")" ) diff --git a/tests/hikari/__init__.py b/tests/hikari/__init__.py index c9c74ac9bb..2d3e6ef370 100644 --- a/tests/hikari/__init__.py +++ b/tests/hikari/__init__.py @@ -17,6 +17,8 @@ # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . import asyncio +import contextlib +import sys _real_new_event_loop = asyncio.new_event_loop @@ -28,3 +30,8 @@ def _new_event_loop(): asyncio.new_event_loop = _new_event_loop + + +with contextlib.suppress(AttributeError): + # provisional since py37 + sys.set_coroutine_origin_tracking_depth(20) diff --git a/tests/hikari/clients/test_shard_clients.py b/tests/hikari/clients/test_shard_clients.py index 4841e7133f..d50ac50032 100644 --- a/tests/hikari/clients/test_shard_clients.py +++ b/tests/hikari/clients/test_shard_clients.py @@ -16,6 +16,7 @@ # # You should have received a copy of the GNU Lesser General Public License # along ith Hikari. If not, see . +import asyncio import datetime import math @@ -42,6 +43,9 @@ def __init__(self): def exception(self): return self._exception + def done(self): + return True + return Task() @@ -214,11 +218,11 @@ async def test_close_when_already_stopping(self, shard_client_obj): "error", [ aiohttp.ClientConnectorError(mock.MagicMock(), mock.MagicMock()), - errors.GatewayZombiedError, + errors.GatewayZombiedError(), errors.GatewayInvalidSessionError(False), errors.GatewayInvalidSessionError(True), - errors.GatewayMustReconnectError, - errors.GatewayClientDisconnectedError, + errors.GatewayMustReconnectError(), + errors.GatewayClientDisconnectedError(), ], ) @pytest.mark.asyncio @@ -233,7 +237,7 @@ def side_effect(*args): should_return = True return _helpers.AwaitableMock(return_value=error) - shard_client_obj._spin_up = mock.AsyncMock(side_effect=side_effect) + shard_client_obj._spin_up = mock.MagicMock(side_effect=side_effect) with mock.patch("asyncio.sleep", new=mock.AsyncMock()): await shard_client_obj._keep_alive() @@ -338,14 +342,30 @@ async def test__spin_up_when_not_resuming(self, shard_client_obj): with mock.patch("asyncio.wait", side_effect=[([], None), ([], None), ([], None)]): assert await shard_client_obj._spin_up() == task_mock - @_helpers.assert_raises(type_=RuntimeError) + @_helpers.timeout_after(10) + #@_helpers.assert_raises(type_=RuntimeError) @pytest.mark.asyncio async def test__spin_up_if_connect_task_is_completed_raises_exception_during_ready_event(self, shard_client_obj): - task_mock = _generate_mock_task(RuntimeError) - - with mock.patch("asyncio.create_task", return_value=task_mock): - with mock.patch("asyncio.wait", side_effect=[([], None), ([], None), ([task_mock], None)]): - await shard_client_obj._spin_up() + stop_event = asyncio.Event() + try: + async def forever(): + # make this so that it doesn't complete in time; + await stop_event.wait() + + # Make this last a really long time so it doesn't complete immediately. + shard_client_obj._connection.connect = mock.MagicMock(wraps=forever) + + # Make these finish immediately. + shard_client_obj._connection.hello_event = mock.MagicMock(wait=mock.AsyncMock()) + shard_client_obj._connection.handshake_event = mock.MagicMock(wait=mock.AsyncMock()) + + # Make this one go boom. + shard_client_obj._connection.ready_event = mock.MagicMock(wait=mock.AsyncMock(side_effect=RuntimeError)) + + # Do iiiit. + await shard_client_obj._spin_up() + finally: + stop_event.set() class TestShardClientImplUpdatePresence: From b412711791a7174b666e0fed17f8770a7f526f64 Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Sat, 18 Apr 2020 22:02:24 +0100 Subject: [PATCH 168/922] Fixed unused module in rest_sessions. --- hikari/net/rest_sessions.py | 1 - tests/hikari/clients/test_shard_clients.py | 3 ++- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/hikari/net/rest_sessions.py b/hikari/net/rest_sessions.py index cc34ea8d59..2255d9f89a 100644 --- a/hikari/net/rest_sessions.py +++ b/hikari/net/rest_sessions.py @@ -33,7 +33,6 @@ import aiohttp.typedefs from hikari import errors -from hikari import unset from hikari.internal import assertions from hikari.internal import conversions from hikari.internal import more_collections diff --git a/tests/hikari/clients/test_shard_clients.py b/tests/hikari/clients/test_shard_clients.py index d50ac50032..285a8bcb63 100644 --- a/tests/hikari/clients/test_shard_clients.py +++ b/tests/hikari/clients/test_shard_clients.py @@ -343,11 +343,12 @@ async def test__spin_up_when_not_resuming(self, shard_client_obj): assert await shard_client_obj._spin_up() == task_mock @_helpers.timeout_after(10) - #@_helpers.assert_raises(type_=RuntimeError) + # @_helpers.assert_raises(type_=RuntimeError) @pytest.mark.asyncio async def test__spin_up_if_connect_task_is_completed_raises_exception_during_ready_event(self, shard_client_obj): stop_event = asyncio.Event() try: + async def forever(): # make this so that it doesn't complete in time; await stop_event.wait() From a72b13a823b2184e0d8d0aec50f5f7b1bf639ea1 Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Sat, 18 Apr 2020 21:24:02 +0000 Subject: [PATCH 169/922] Update docker-compose.yml [skip ci] --- docker-compose.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker-compose.yml b/docker-compose.yml index 5013ddb19b..d7790a7d82 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -3,7 +3,7 @@ version: "3" services: gateway-test: build: . - entrypoint: python -m hikari.clients.gateway_runner --logger=DEBUG + entrypoint: python -m hikari.clients.test_client --logger=DEBUG env_file: - credentials.env restart: always From e47c82c4cc56a393ffbf56af252150d4c04c9395 Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Sun, 19 Apr 2020 13:47:03 +0100 Subject: [PATCH 170/922] More tidying up and reorganising internals. - `hikari.net.codes.GatewayIntent` is now hikari.intents.Intent. - Removed `deserialize_channel` from `__all__`. - Moved a bunch of typehints into a `hikari.internal.more_typehints` module. - Moved `hikari.snowflakes.Snowflakes` into `hikari.entities` - Removed `RawEntityT` and made a set of JSON types instead. - Moved protocols from `hikari.internal.more_asyncio` into `more_typehints`. - Renamed `hikari.entities` to `hikari.bases`. - Added `abc.ABC` to `UniqueEntity` definition. - Fixed some docstrings randomly across the library. - Fixed marshaller typehints. --- hikari/__init__.py | 12 +- hikari/audit_logs.py | 104 ++-- hikari/bases.py | 135 +++++ hikari/channels.py | 64 +-- hikari/clients/bot_clients.py | 15 +- hikari/clients/configs.py | 14 +- hikari/clients/rest_clients/__init__.py | 2 +- .../rest_clients/channels_component.py | 257 +++++---- .../rest_clients/current_users_component.py | 38 +- .../rest_clients/gateways_component.py | 2 +- .../clients/rest_clients/guilds_component.py | 490 +++++++++--------- .../clients/rest_clients/invites_component.py | 2 +- .../clients/rest_clients/oauth2_component.py | 2 +- .../rest_clients/reactions_component.py | 74 +-- .../clients/rest_clients/users_component.py | 10 +- .../clients/rest_clients/voices_component.py | 2 +- .../rest_clients/webhooks_component.py | 56 +- hikari/clients/shard_clients.py | 12 +- hikari/clients/test_client.py | 2 +- hikari/embeds.py | 18 +- hikari/emojis.py | 13 +- hikari/entities.py | 37 -- hikari/events.py | 289 +++++------ hikari/gateway_entities.py | 10 +- hikari/guilds.py | 137 +++-- hikari/intents.py | 145 ++++++ hikari/internal/allowed_mentions.py | 16 +- hikari/internal/marshaller.py | 19 +- hikari/internal/more_asyncio.py | 92 +--- hikari/internal/more_collections.py | 18 +- hikari/internal/more_typing.py | 185 +++++++ hikari/internal/pagination.py | 5 +- hikari/invites.py | 6 +- hikari/messages.py | 63 ++- hikari/net/codes.py | 124 +---- hikari/net/ratelimits.py | 19 +- hikari/net/rest_sessions.py | 1 - hikari/net/shards.py | 13 +- hikari/oauth2.py | 35 +- hikari/snowflakes.py | 6 +- hikari/state/event_dispatchers.py | 15 +- hikari/state/event_managers.py | 5 +- hikari/state/raw_event_consumers.py | 4 +- hikari/users.py | 4 +- hikari/voices.py | 23 +- hikari/webhooks.py | 14 +- tests/hikari/clients/test_configs.py | 10 +- .../test_oauth2_component.py | 1 - tests/hikari/clients/test_shard_clients.py | 1 - tests/hikari/internal/test_pagination.py | 2 +- .../{test_snowflake.py => test_bases.py} | 47 +- tests/hikari/test_entities.py | 18 - tests/hikari/test_gateway_entities.py | 8 +- 53 files changed, 1434 insertions(+), 1262 deletions(-) create mode 100644 hikari/bases.py delete mode 100644 hikari/entities.py create mode 100644 hikari/intents.py create mode 100644 hikari/internal/more_typing.py rename tests/hikari/{test_snowflake.py => test_bases.py} (65%) delete mode 100644 tests/hikari/test_entities.py diff --git a/hikari/__init__.py b/hikari/__init__.py index becef4608c..e52f7c73ad 100644 --- a/hikari/__init__.py +++ b/hikari/__init__.py @@ -18,23 +18,23 @@ # along with Hikari. If not, see . """Hikari's models framework for writing Discord bots in Python.""" from hikari import audit_logs +from hikari import bases from hikari import channels from hikari import clients from hikari import colors from hikari import colours from hikari import embeds from hikari import emojis -from hikari import entities from hikari import errors from hikari import events from hikari import gateway_entities from hikari import guilds +from hikari import intents from hikari import invites from hikari import messages from hikari import net from hikari import oauth2 from hikari import permissions -from hikari import snowflakes from hikari import state from hikari import users from hikari import voices @@ -46,13 +46,13 @@ from hikari._about import __url__ from hikari._about import __version__ from hikari.audit_logs import * +from hikari.bases import * from hikari.channels import * from hikari.clients import * from hikari.colors import * from hikari.colours import * from hikari.embeds import * from hikari.emojis import * -from hikari.entities import * from hikari.events import * from hikari.gateway_entities import * from hikari.guilds import * @@ -61,7 +61,6 @@ from hikari.net import * from hikari.oauth2 import * from hikari.permissions import * -from hikari.snowflakes import * from hikari.state import * from hikari.unset import * from hikari.users import * @@ -78,16 +77,17 @@ *colours.__all__, *embeds.__all__, *emojis.__all__, - *entities.__all__, + *bases.__all__, *events.__all__, *gateway_entities.__all__, *guilds.__all__, + *intents.__all__, *invites.__all__, *messages.__all__, *net.__all__, *oauth2.__all__, *permissions.__all__, - *snowflakes.__all__, + *bases.__all__, *state.__all__, *unset.__all__, *users.__all__, diff --git a/hikari/audit_logs.py b/hikari/audit_logs.py index 8f279c4c5b..61db78d69a 100644 --- a/hikari/audit_logs.py +++ b/hikari/audit_logs.py @@ -47,17 +47,17 @@ import attr +from hikari import bases from hikari import channels from hikari import colors -from hikari import entities from hikari import guilds from hikari import permissions -from hikari import snowflakes from hikari import users as _users from hikari import webhooks as _webhooks from hikari.internal import conversions from hikari.internal import marshaller from hikari.internal import more_collections +from hikari.internal import more_typing class AuditLogChangeKey(str, enum.Enum): @@ -125,8 +125,8 @@ def __str__(self) -> str: AUDIT_LOG_ENTRY_CONVERTERS = { - AuditLogChangeKey.OWNER_ID: snowflakes.Snowflake.deserialize, - AuditLogChangeKey.AFK_CHANNEL_ID: snowflakes.Snowflake.deserialize, + AuditLogChangeKey.OWNER_ID: bases.Snowflake.deserialize, + AuditLogChangeKey.AFK_CHANNEL_ID: bases.Snowflake.deserialize, AuditLogChangeKey.AFK_TIMEOUT: lambda payload: datetime.timedelta(seconds=payload), AuditLogChangeKey.MFA_LEVEL: guilds.GuildMFALevel, AuditLogChangeKey.VERIFICATION_LEVEL: guilds.GuildVerificationLevel, @@ -139,35 +139,35 @@ def __str__(self) -> str: role.id: role for role in map(guilds.PartialGuildRole.deserialize, payload) }, AuditLogChangeKey.PRUNE_DELETE_DAYS: lambda payload: datetime.timedelta(days=int(payload)), - AuditLogChangeKey.WIDGET_CHANNEL_ID: snowflakes.Snowflake.deserialize, + AuditLogChangeKey.WIDGET_CHANNEL_ID: bases.Snowflake.deserialize, AuditLogChangeKey.POSITION: int, AuditLogChangeKey.BITRATE: int, AuditLogChangeKey.PERMISSION_OVERWRITES: lambda payload: { overwrite.id: overwrite for overwrite in map(channels.PermissionOverwrite.deserialize, payload) }, - AuditLogChangeKey.APPLICATION_ID: snowflakes.Snowflake.deserialize, + AuditLogChangeKey.APPLICATION_ID: bases.Snowflake.deserialize, AuditLogChangeKey.PERMISSIONS: permissions.Permission, AuditLogChangeKey.COLOR: colors.Color, AuditLogChangeKey.ALLOW: permissions.Permission, AuditLogChangeKey.DENY: permissions.Permission, - AuditLogChangeKey.CHANNEL_ID: snowflakes.Snowflake.deserialize, - AuditLogChangeKey.INVITER_ID: snowflakes.Snowflake.deserialize, + AuditLogChangeKey.CHANNEL_ID: bases.Snowflake.deserialize, + AuditLogChangeKey.INVITER_ID: bases.Snowflake.deserialize, AuditLogChangeKey.MAX_USES: lambda payload: int(payload) if payload > 0 else float("inf"), AuditLogChangeKey.USES: int, AuditLogChangeKey.MAX_AGE: lambda payload: datetime.timedelta(seconds=payload) if payload > 0 else None, - AuditLogChangeKey.ID: snowflakes.Snowflake.deserialize, + AuditLogChangeKey.ID: bases.Snowflake.deserialize, AuditLogChangeKey.TYPE: str, AuditLogChangeKey.ENABLE_EMOTICONS: bool, AuditLogChangeKey.EXPIRE_BEHAVIOR: guilds.IntegrationExpireBehaviour, AuditLogChangeKey.EXPIRE_GRACE_PERIOD: lambda payload: datetime.timedelta(days=payload), AuditLogChangeKey.RATE_LIMIT_PER_USER: lambda payload: datetime.timedelta(seconds=payload), - AuditLogChangeKey.SYSTEM_CHANNEL_ID: snowflakes.Snowflake.deserialize, + AuditLogChangeKey.SYSTEM_CHANNEL_ID: bases.Snowflake.deserialize, } @marshaller.marshallable() @attr.s(slots=True, kw_only=True) -class AuditLogChange(entities.HikariEntity, marshaller.Deserializable): +class AuditLogChange(bases.HikariEntity, marshaller.Deserializable): """Represents a change made to an audit log entry's target entity.""" #: The new value of the key, if something was added or changed. @@ -186,7 +186,7 @@ class AuditLogChange(entities.HikariEntity, marshaller.Deserializable): key: typing.Union[AuditLogChangeKey, str] = marshaller.attrib() @classmethod - def deserialize(cls, payload: entities.RawEntityT) -> "AuditLogChange": + def deserialize(cls, payload: typing.Mapping[str, str]) -> "AuditLogChange": """Deserialize this model from a raw payload.""" key = conversions.try_cast(payload["key"], AuditLogChangeKey, payload["key"]) new_value = payload.get("new_value") @@ -273,7 +273,7 @@ def decorator(cls): @marshaller.marshallable() @attr.s(slots=True, kw_only=True) -class BaseAuditLogEntryInfo(abc.ABC, entities.HikariEntity, marshaller.Deserializable): +class BaseAuditLogEntryInfo(bases.HikariEntity, marshaller.Deserializable, abc.ABC): """A base object that all audit log entry info objects will inherit from.""" @@ -294,8 +294,8 @@ class ChannelOverwriteEntryInfo(BaseAuditLogEntryInfo): #: The ID of the overwrite being updated, added or removed (and the entity #: it targets). #: - #: :type: :obj:`~hikari.snowflakes.Snowflake` - id: snowflakes.Snowflake = marshaller.attrib(deserializer=snowflakes.Snowflake.deserialize) + #: :type: :obj:`~hikari.entities.Snowflake` + id: bases.Snowflake = marshaller.attrib(deserializer=bases.Snowflake.deserialize) #: The type of entity this overwrite targets. #: @@ -320,13 +320,13 @@ class MessagePinEntryInfo(BaseAuditLogEntryInfo): #: The ID of the guild text based channel where this pinned message is #: being added or removed. #: - #: :type: :obj:`~hikari.snowflakes.Snowflake` - channel_id: snowflakes.Snowflake = marshaller.attrib(deserializer=snowflakes.Snowflake.deserialize) + #: :type: :obj:`~hikari.entities.Snowflake` + channel_id: bases.Snowflake = marshaller.attrib(deserializer=bases.Snowflake.deserialize) #: The ID of the message that's being pinned or unpinned. #: - #: :type: :obj:`~hikari.snowflakes.Snowflake` - message_id: snowflakes.Snowflake = marshaller.attrib(deserializer=snowflakes.Snowflake.deserialize) + #: :type: :obj:`~hikari.entities.Snowflake` + message_id: bases.Snowflake = marshaller.attrib(deserializer=bases.Snowflake.deserialize) @register_audit_log_entry_info(AuditLogEventType.MEMBER_PRUNE) @@ -369,8 +369,8 @@ class MessageDeleteEntryInfo(MessageBulkDeleteEntryInfo): #: The guild text based channel where these message(s) were deleted. #: - #: :type: :obj:`~hikari.snowflakes.Snowflake` - channel_id: snowflakes.Snowflake = marshaller.attrib(deserializer=snowflakes.Snowflake.deserialize) + #: :type: :obj:`~hikari.entities.Snowflake` + channel_id: bases.Snowflake = marshaller.attrib(deserializer=bases.Snowflake.deserialize) @register_audit_log_entry_info(AuditLogEventType.MEMBER_DISCONNECT) @@ -393,18 +393,18 @@ class MemberMoveEntryInfo(MemberDisconnectEntryInfo): #: The channel these member(s) were moved to. #: - #: :type: :obj:`~hikari.snowflakes.Snowflake` - channel_id: snowflakes.Snowflake = marshaller.attrib(deserializer=snowflakes.Snowflake.deserialize) + #: :type: :obj:`~hikari.entities.Snowflake` + channel_id: bases.Snowflake = marshaller.attrib(deserializer=bases.Snowflake.deserialize) class UnrecognisedAuditLogEntryInfo(BaseAuditLogEntryInfo): """Represents any audit log entry options that haven't been implemented.""" - def __init__(self, payload: entities.RawEntityT) -> None: + def __init__(self, payload: typing.Mapping[str, str]) -> None: self.__dict__.update(payload) @classmethod - def deserialize(cls, payload: entities.RawEntityT) -> "UnrecognisedAuditLogEntryInfo": + def deserialize(cls, payload: typing.Mapping[str, str]) -> "UnrecognisedAuditLogEntryInfo": return cls(payload) @@ -429,13 +429,13 @@ def get_entry_info_entity(type_: int) -> typing.Type[BaseAuditLogEntryInfo]: @marshaller.marshallable() @attr.s(slots=True, kw_only=True) -class AuditLogEntry(snowflakes.UniqueEntity, marshaller.Deserializable): +class AuditLogEntry(bases.UniqueEntity, marshaller.Deserializable): """Represents an entry in a guild's audit log.""" #: The ID of the entity affected by this change, if applicable. #: - #: :type: :obj:`~hikari.snowflakes.Snowflake`, optional - target_id: typing.Optional[snowflakes.Snowflake] = marshaller.attrib() + #: :type: :obj:`~hikari.entities.Snowflake`, optional + target_id: typing.Optional[bases.Snowflake] = marshaller.attrib() #: A sequence of the changes made to :attr:`target_id` #: @@ -444,13 +444,13 @@ class AuditLogEntry(snowflakes.UniqueEntity, marshaller.Deserializable): #: The ID of the user who made this change. #: - #: :type: :obj:`~hikari.snowflakes.Snowflake` - user_id: snowflakes.Snowflake = marshaller.attrib() + #: :type: :obj:`~hikari.entities.Snowflake` + user_id: bases.Snowflake = marshaller.attrib() #: The ID of this entry. #: - #: :type: :obj:`~hikari.snowflakes.Snowflake` - id: snowflakes.Snowflake = marshaller.attrib() + #: :type: :obj:`~hikari.entities.Snowflake` + id: bases.Snowflake = marshaller.attrib() #: The type of action this entry represents. #: @@ -469,11 +469,11 @@ class AuditLogEntry(snowflakes.UniqueEntity, marshaller.Deserializable): reason: typing.Optional[str] = marshaller.attrib() @classmethod - def deserialize(cls, payload: entities.RawEntityT) -> "AuditLogEntry": + def deserialize(cls, payload: typing.Mapping[str, str]) -> "AuditLogEntry": """Deserialize this model from a raw payload.""" action_type = conversions.try_cast(payload["action_type"], AuditLogEventType, payload["action_type"]) if target_id := payload.get("target_id"): - target_id = snowflakes.Snowflake.deserialize(target_id) + target_id = bases.Snowflake.deserialize(target_id) if (options := payload.get("options")) is not None: if option_converter := get_entry_info_entity(action_type): @@ -486,8 +486,8 @@ def deserialize(cls, payload: entities.RawEntityT) -> "AuditLogEntry": AuditLogChange.deserialize(payload) for payload in payload.get("changes", more_collections.EMPTY_SEQUENCE) ], - user_id=snowflakes.Snowflake.deserialize(payload["user_id"]), - id=snowflakes.Snowflake.deserialize(payload["id"]), + user_id=bases.Snowflake.deserialize(payload["user_id"]), + id=bases.Snowflake.deserialize(payload["id"]), action_type=action_type, options=options, reason=payload.get("reason"), @@ -496,21 +496,21 @@ def deserialize(cls, payload: entities.RawEntityT) -> "AuditLogEntry": @marshaller.marshallable() @attr.s(slots=True, kw_only=True) -class AuditLog(entities.HikariEntity, marshaller.Deserializable): +class AuditLog(bases.HikariEntity, marshaller.Deserializable): """Represents a guilds audit log.""" #: A sequence of the audit log's entries. #: - #: :type: :obj:`~typing.Mapping` [ :obj:`~hikari.snowflakes.Snowflake`, :obj:`~AuditLogEntry` ] - entries: typing.Mapping[snowflakes.Snowflake, AuditLogEntry] = marshaller.attrib( + #: :type: :obj:`~typing.Mapping` [ :obj:`~hikari.entities.Snowflake`, :obj:`~AuditLogEntry` ] + entries: typing.Mapping[bases.Snowflake, AuditLogEntry] = marshaller.attrib( raw_name="audit_log_entries", deserializer=lambda payload: {entry.id: entry for entry in map(AuditLogEntry.deserialize, payload)}, ) #: A mapping of the partial objects of integrations found in this audit log. #: - #: :type: :obj:`~typing.Mapping` [ :obj:`~hikari.snowflakes.Snowflake`, :obj:`~hikari.guilds.GuildIntegration` ] - integrations: typing.Mapping[snowflakes.Snowflake, guilds.GuildIntegration] = marshaller.attrib( + #: :type: :obj:`~typing.Mapping` [ :obj:`~hikari.entities.Snowflake`, :obj:`~hikari.guilds.GuildIntegration` ] + integrations: typing.Mapping[bases.Snowflake, guilds.GuildIntegration] = marshaller.attrib( deserializer=lambda payload: { integration.id: integration for integration in map(guilds.PartialGuildIntegration.deserialize, payload) } @@ -518,15 +518,15 @@ class AuditLog(entities.HikariEntity, marshaller.Deserializable): #: A mapping of the objects of users found in this audit log. #: - #: :type: :obj:`~typing.Mapping` [ :obj:`~hikari.snowflakes.Snowflake`, :obj:`~hikari.users.User` ] - users: typing.Mapping[snowflakes.Snowflake, _users.User] = marshaller.attrib( + #: :type: :obj:`~typing.Mapping` [ :obj:`~hikari.entities.Snowflake`, :obj:`~hikari.users.User` ] + users: typing.Mapping[bases.Snowflake, _users.User] = marshaller.attrib( deserializer=lambda payload: {user.id: user for user in map(_users.User.deserialize, payload)} ) #: A mapping of the objects of webhooks found in this audit log. #: - #: :type: :obj:`~typing.Mapping` [ :obj:`~hikari.snowflakes.Snowflake`, :obj:`~hikari.webhooks.Webhook` ] - webhooks: typing.Mapping[snowflakes.Snowflake, _webhooks.Webhook] = marshaller.attrib( + #: :type: :obj:`~typing.Mapping` [ :obj:`~hikari.entities.Snowflake`, :obj:`~hikari.webhooks.Webhook` ] + webhooks: typing.Mapping[bases.Snowflake, _webhooks.Webhook] = marshaller.attrib( deserializer=lambda payload: {webhook.id: webhook for webhook in map(_webhooks.Webhook.deserialize, payload)} ) @@ -577,23 +577,23 @@ class AuditLogIterator(typing.AsyncIterator[AuditLogEntry]): #: A mapping of the partial objects of integrations found in this audit log #: so far. #: - #: :type: :obj:`~typing.Mapping` [ :obj:`~hikari.snowflakes.Snowflake`, :obj:`~hikari.guilds.GuildIntegration` ] - integrations: typing.Mapping[snowflakes.Snowflake, guilds.GuildIntegration] + #: :type: :obj:`~typing.Mapping` [ :obj:`~hikari.entities.Snowflake`, :obj:`~hikari.guilds.GuildIntegration` ] + integrations: typing.Mapping[bases.Snowflake, guilds.GuildIntegration] #: A mapping of the objects of users found in this audit log so far. #: - #: :type: :obj:`~typing.Mapping` [ :obj:`~hikari.snowflakes.Snowflake`, :obj:`~hikari.users.User` ] - users: typing.Mapping[snowflakes.Snowflake, _users.User] + #: :type: :obj:`~typing.Mapping` [ :obj:`~hikari.entities.Snowflake`, :obj:`~hikari.users.User` ] + users: typing.Mapping[bases.Snowflake, _users.User] #: A mapping of the objects of webhooks found in this audit log so far. #: - #: :type: :obj:`~typing.Mapping` [ :obj:`~hikari.snowflakes.Snowflake`, :obj:`~hikari.webhooks.Webhook` ] - webhooks: typing.Mapping[snowflakes.Snowflake, _webhooks.Webhook] + #: :type: :obj:`~typing.Mapping` [ :obj:`~hikari.entities.Snowflake`, :obj:`~hikari.webhooks.Webhook` ] + webhooks: typing.Mapping[bases.Snowflake, _webhooks.Webhook] def __init__( self, guild_id: str, - request: typing.Callable[..., typing.Coroutine[typing.Any, typing.Any, typing.Any]], + request: typing.Callable[..., more_typing.Coroutine[typing.Any]], before: typing.Optional[str] = None, user_id: str = ..., action_type: int = ..., diff --git a/hikari/bases.py b/hikari/bases.py new file mode 100644 index 0000000000..19d1dc5d94 --- /dev/null +++ b/hikari/bases.py @@ -0,0 +1,135 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# Copyright © Nekokatt 2019-2020 +# +# This file is part of Hikari. +# +# Hikari is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Hikari is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Hikari. If not, see . +"""Datastructure bases.""" +__all__ = ["HikariEntity", "Snowflake", "UniqueEntity"] + +import abc +import datetime +import functools +import typing + +import attr + +from hikari.internal import conversions +from hikari.internal import marshaller + + +@marshaller.marshallable() +@attr.s(slots=True, kw_only=True, init=False) +class HikariEntity(abc.ABC): + """The base for any entity used in this API.""" + + +@functools.total_ordering +class Snowflake(HikariEntity, typing.SupportsInt): + """A concrete representation of a unique identifier for an object on Discord. + + This object can be treated as a regular :obj:`~int` for most purposes. + """ + + __slots__ = ("_value",) + + #: The integer value of this ID. + #: + #: :type: :obj:`~int` + _value: int + + # noinspection PyMissingConstructor + def __init__(self, value: typing.Union[int, str]) -> None: + self._value = int(value) + + @property + def created_at(self) -> datetime.datetime: + """When the object was created.""" + epoch = self._value >> 22 + return conversions.discord_epoch_to_datetime(epoch) + + @property + def internal_worker_id(self) -> int: + """ID of the worker that created this snowflake on Discord's systems.""" + return (self._value & 0x3E0_000) >> 17 + + @property + def internal_process_id(self) -> int: + """ID of the process that created this snowflake on Discord's systems.""" + return (self._value & 0x1F_000) >> 12 + + @property + def increment(self) -> int: + """Increment of Discord's system when this object was made.""" + return self._value & 0xFFF + + def __hash__(self) -> int: + return hash(self._value) + + def __int__(self) -> int: + return self._value + + def __repr__(self) -> str: + return repr(self._value) + + def __str__(self) -> str: + return str(self._value) + + def __eq__(self, other: typing.Any) -> bool: + return isinstance(other, typing.SupportsInt) and int(other) == self._value + + def __lt__(self, other: "Snowflake") -> bool: + return self._value < int(other) + + def serialize(self) -> str: + """Generate a JSON-friendly representation of this object.""" + return str(self._value) + + @classmethod + def deserialize(cls, value: str) -> "Snowflake": + """Take a :obj:`~str` ID and convert it into a Snowflake object.""" + return cls(value) + + @classmethod + def from_datetime(cls, date: datetime.datetime) -> "Snowflake": + """Get a snowflake object from a datetime object.""" + return cls.from_timestamp(date.timestamp()) + + @classmethod + def from_timestamp(cls, timestamp: float) -> "Snowflake": + """Get a snowflake object from a seconds timestamp.""" + return cls(int(timestamp - conversions.DISCORD_EPOCH) * 1000 << 22) + + +@marshaller.marshallable() +@attr.s(slots=True, kw_only=True) +class UniqueEntity(HikariEntity, typing.SupportsInt, abc.ABC): + """A base for an entity that has an integer ID of some sort. + + Casting an object of this type to an :obj:`~int` will produce the + integer ID of the object. + """ + + #: The ID of this entity. + #: + #: :type: :obj:`~Snowflake` + id: Snowflake = marshaller.attrib(hash=True, eq=True, repr=True, deserializer=Snowflake, serializer=str) + + def __int__(self) -> int: + return int(self.id) + + +T = typing.TypeVar("T", bound=UniqueEntity) +Hashable = typing.Union[Snowflake, int, T] diff --git a/hikari/channels.py b/hikari/channels.py index 8ac5b7feff..48f25eb9bc 100644 --- a/hikari/channels.py +++ b/hikari/channels.py @@ -24,7 +24,7 @@ hikari.entities.HikariEntity hikari.entities.Deserializable hikari.entities.Serializable - hikari.snowflakes.UniqueEntity + hikari.entities.UniqueEntity :parts: 1 """ @@ -42,7 +42,6 @@ "GuildStoreChannel", "GuildVoiceChannel", "GuildChannelBuilder", - "deserialize_channel", ] import datetime @@ -51,9 +50,8 @@ import attr -from hikari import entities +from hikari import bases from hikari import permissions -from hikari import snowflakes from hikari import users from hikari.internal import marshaller from hikari.internal import more_collections @@ -102,7 +100,7 @@ def __str__(self) -> str: @marshaller.marshallable() @attr.s(slots=True, kw_only=True) -class PermissionOverwrite(snowflakes.UniqueEntity, marshaller.Deserializable, marshaller.Serializable): +class PermissionOverwrite(bases.UniqueEntity, marshaller.Deserializable, marshaller.Serializable): """Represents permission overwrites for a channel or role in a channel.""" #: The type of entity this overwrite targets. @@ -157,7 +155,7 @@ def decorator(cls): @marshaller.marshallable() @attr.s(slots=True, kw_only=True) -class Channel(snowflakes.UniqueEntity, marshaller.Deserializable): +class Channel(bases.UniqueEntity, marshaller.Deserializable): """Base class for all channels.""" #: The channel's type. @@ -193,15 +191,13 @@ class DMChannel(Channel): #: This might point to an invalid or deleted message. #: #: - #: :type: :obj:`~hikari.snowflakes.Snowflake`, optional - last_message_id: snowflakes.Snowflake = marshaller.attrib( - deserializer=snowflakes.Snowflake.deserialize, if_none=None - ) + #: :type: :obj:`~hikari.entities.Snowflake`, optional + last_message_id: bases.Snowflake = marshaller.attrib(deserializer=bases.Snowflake.deserialize, if_none=None) #: The recipients of the DM. #: - #: :type: :obj:`~typing.Mapping` [ :obj:`~hikari.snowflakes.Snowflake`, :obj:`~hikari.users.User` ] - recipients: typing.Mapping[snowflakes.Snowflake, users.User] = marshaller.attrib( + #: :type: :obj:`~typing.Mapping` [ :obj:`~hikari.entities.Snowflake`, :obj:`~hikari.users.User` ] + recipients: typing.Mapping[bases.Snowflake, users.User] = marshaller.attrib( deserializer=lambda recipients: {user.id: user for user in map(users.User.deserialize, recipients)} ) @@ -219,8 +215,8 @@ class GroupDMChannel(DMChannel): #: The ID of the owner of the group. #: - #: :type: :obj:`~hikari.snowflakes.Snowflake` - owner_id: snowflakes.Snowflake = marshaller.attrib(deserializer=snowflakes.Snowflake.deserialize) + #: :type: :obj:`~hikari.entities.Snowflake` + owner_id: bases.Snowflake = marshaller.attrib(deserializer=bases.Snowflake.deserialize) #: The hash of the icon of the group. #: @@ -230,9 +226,9 @@ class GroupDMChannel(DMChannel): #: The ID of the application that created the group DM, if it's a #: bot based group DM. #: - #: :type: :obj:`~hikari.snowflakes.Snowflake`, optional - application_id: typing.Optional[snowflakes.Snowflake] = marshaller.attrib( - deserializer=snowflakes.Snowflake.deserialize, if_undefined=None, default=None + #: :type: :obj:`~hikari.entities.Snowflake`, optional + application_id: typing.Optional[bases.Snowflake] = marshaller.attrib( + deserializer=bases.Snowflake.deserialize, if_undefined=None, default=None ) @@ -243,8 +239,8 @@ class GuildChannel(Channel): #: The ID of the guild the channel belongs to. #: - #: :type: :obj:`~hikari.snowflakes.Snowflake` - guild_id: snowflakes.Snowflake = marshaller.attrib(deserializer=snowflakes.Snowflake.deserialize) + #: :type: :obj:`~hikari.entities.Snowflake` + guild_id: bases.Snowflake = marshaller.attrib(deserializer=bases.Snowflake.deserialize) #: The sorting position of the channel. #: @@ -253,7 +249,7 @@ class GuildChannel(Channel): #: The permission overwrites for the channel. #: - #: :type: :obj:`~typing.Mapping` [ :obj:`~hikari.snowflakes.Snowflake`, :obj:`~PermissionOverwrite` ] + #: :type: :obj:`~typing.Mapping` [ :obj:`~hikari.entities.Snowflake`, :obj:`~PermissionOverwrite` ] permission_overwrites: PermissionOverwrite = marshaller.attrib( deserializer=lambda overwrites: {o.id: o for o in map(PermissionOverwrite.deserialize, overwrites)} ) @@ -270,8 +266,8 @@ class GuildChannel(Channel): #: The ID of the parent category the channel belongs to. #: - #: :type: :obj:`~hikari.snowflakes.Snowflake`, optional - parent_id: snowflakes.Snowflake = marshaller.attrib(deserializer=snowflakes.Snowflake.deserialize, if_none=None) + #: :type: :obj:`~hikari.entities.Snowflake`, optional + parent_id: bases.Snowflake = marshaller.attrib(deserializer=bases.Snowflake.deserialize, if_none=None) @register_channel_type(ChannelType.GUILD_CATEGORY) @@ -299,10 +295,8 @@ class GuildTextChannel(GuildChannel): #: This might point to an invalid or deleted message. #: #: - #: :type: :obj:`~hikari.snowflakes.Snowflake`, optional - last_message_id: snowflakes.Snowflake = marshaller.attrib( - deserializer=snowflakes.Snowflake.deserialize, if_none=None - ) + #: :type: :obj:`~hikari.entities.Snowflake`, optional + last_message_id: bases.Snowflake = marshaller.attrib(deserializer=bases.Snowflake.deserialize, if_none=None) #: The delay (in seconds) between a user can send a message #: to this channel. @@ -336,10 +330,8 @@ class GuildNewsChannel(GuildChannel): #: This might point to an invalid or deleted message. #: #: - #: :type: :obj:`~hikari.snowflakes.Snowflake`, optional - last_message_id: snowflakes.Snowflake = marshaller.attrib( - deserializer=snowflakes.Snowflake.deserialize, if_none=None - ) + #: :type: :obj:`~hikari.entities.Snowflake`, optional + last_message_id: bases.Snowflake = marshaller.attrib(deserializer=bases.Snowflake.deserialize, if_none=None) @register_channel_type(ChannelType.GUILD_STORE) @@ -398,8 +390,8 @@ def __init__(self, channel_name: str, channel_type: ChannelType) -> None: "name": channel_name, } - def serialize(self: "GuildChannelBuilder") -> entities.RawEntityT: - """Serialize this instance into a naive value.""" + def serialize(self: "GuildChannelBuilder") -> typing.Mapping[str, typing.Any]: + """Serialize this instance into a payload to send to Discord.""" return self._payload def is_nsfw(self) -> "GuildChannelBuilder": @@ -475,19 +467,19 @@ def with_rate_limit_per_user( ) return self - def with_parent_category(self, category: typing.Union[snowflakes.Snowflake, int]) -> "GuildChannelBuilder": + def with_parent_category(self, category: typing.Union[bases.Snowflake, int]) -> "GuildChannelBuilder": """Set the parent category for this channel. Parameters ---------- - category : :obj:`~typing.Union` [ :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] + category : :obj:`~typing.Union` [ :obj:`~hikari.entities.Snowflake`, :obj:`~int` ] The placeholder ID of the category channel that should be this channel's parent. """ self._payload["parent_id"] = str(int(category)) return self - def with_id(self, channel_id: typing.Union[snowflakes.Snowflake, int]) -> "GuildChannelBuilder": + def with_id(self, channel_id: typing.Union[bases.Snowflake, int]) -> "GuildChannelBuilder": """Set the placeholder ID for this channel. Notes @@ -497,7 +489,7 @@ def with_id(self, channel_id: typing.Union[snowflakes.Snowflake, int]) -> "Guild Parameters ---------- - channel_id : :obj:`~typing.Union` [ :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] + channel_id : :obj:`~typing.Union` [ :obj:`~hikari.entities.Snowflake`, :obj:`~int` ] The placeholder ID to use. """ self._payload["id"] = str(int(channel_id)) diff --git a/hikari/clients/bot_clients.py b/hikari/clients/bot_clients.py index 7a40fa7d19..bc335196e2 100644 --- a/hikari/clients/bot_clients.py +++ b/hikari/clients/bot_clients.py @@ -30,19 +30,18 @@ from hikari import events from hikari import gateway_entities from hikari import guilds +from hikari import intents from hikari.clients import configs from hikari.clients import rest_clients from hikari.clients import runnable from hikari.clients import shard_clients from hikari.internal import conversions -from hikari.internal import more_asyncio from hikari.internal import more_collections -from hikari.net import codes +from hikari.internal import more_typing from hikari.state import event_dispatchers from hikari.state import event_managers from hikari.state import stateless_event_managers - ShardClientT = typing.TypeVar("ShardClientT", bound=shard_clients.ShardClient) EventManagerT = typing.TypeVar("EventManagerT", bound=event_managers.EventManager) RESTClientT = typing.TypeVar("RESTClientT", bound=rest_clients.RESTClient) @@ -159,12 +158,12 @@ def total_reconnect_count(self) -> int: return sum(s.reconnect_count for s in self.shards.values()) @property - def intents(self) -> typing.Optional[codes.GatewayIntent]: + def intents(self) -> typing.Optional[intents.Intent]: """Intent values that any shard connections will be using. Returns ------- - :obj:`~hikari.net.codes.GatewayIntent`, optional + :obj:`~hikari.intents.Intent`, optional A :obj:`~enum.IntFlag` enum containing each intent that is set. If intents are not being used at all, then this will return :obj:`~None` instead. @@ -261,17 +260,17 @@ def wait_for( *, timeout: typing.Optional[float], predicate: event_dispatchers.PredicateT, - ) -> more_asyncio.Future: + ) -> more_typing.Future: return self.event_manager.event_dispatcher.wait_for(event_type, timeout=timeout, predicate=predicate) - def dispatch_event(self, event: events.HikariEvent) -> more_asyncio.Future[typing.Any]: + def dispatch_event(self, event: events.HikariEvent) -> more_typing.Future[typing.Any]: return self.event_manager.event_dispatcher.dispatch_event(event) async def update_presence( self, *, status: guilds.PresenceStatus = ..., - activity: typing.Optional[gateway_entities.GatewayActivity] = ..., + activity: typing.Optional[gateway_entities.Activity] = ..., idle_since: typing.Optional[datetime.datetime] = ..., is_afk: bool = ..., ) -> None: diff --git a/hikari/clients/configs.py b/hikari/clients/configs.py index 5e4d313022..02a515d8e6 100644 --- a/hikari/clients/configs.py +++ b/hikari/clients/configs.py @@ -38,9 +38,9 @@ from hikari import gateway_entities from hikari import guilds +from hikari import intents as _intents from hikari.internal import conversions from hikari.internal import marshaller -from hikari.net import codes @marshaller.marshallable() @@ -208,8 +208,8 @@ class GatewayConfig(AIOHTTPConfig, TokenConfig, DebugConfig): #: :obj:`~None`, then no activity will be set. #: #: :type: :obj:`~hikari.gateway_entities.GatewayActivity`, optional - initial_activity: typing.Optional[gateway_entities.GatewayActivity] = marshaller.attrib( - deserializer=gateway_entities.GatewayActivity.deserialize, if_none=None, if_undefined=None, default=None + initial_activity: typing.Optional[gateway_entities.Activity] = marshaller.attrib( + deserializer=gateway_entities.Activity.deserialize, if_none=None, if_undefined=None, default=None ) #: The initial status to set the shards to when starting the gateway. @@ -255,7 +255,7 @@ class GatewayConfig(AIOHTTPConfig, TokenConfig, DebugConfig): #: // JSON example, using an array of names #: [ "GUILDS", "GUILD_MESSAGES" ] #: - #: See :obj:`~hikari.net.codes.GatewayIntent` for valid names of + #: See :obj:`~hikari.intents.Intent` for valid names of #: intents you can use. Integer values are as documented on Discord's #: developer portal. #: @@ -270,9 +270,9 @@ class GatewayConfig(AIOHTTPConfig, TokenConfig, DebugConfig): #: :obj:`~None` will simply opt you into every event you can subscribe to. #: #: - #: :type: :obj:`~hikari.net.codes.GatewayIntent`, optional - intents: typing.Optional[codes.GatewayIntent] = marshaller.attrib( - deserializer=lambda value: conversions.dereference_int_flag(codes.GatewayIntent, value), + #: :type: :obj:`~hikari.intents.Intent`, optional + intents: typing.Optional[_intents.Intent] = marshaller.attrib( + deserializer=lambda value: conversions.dereference_int_flag(_intents.Intent, value), if_undefined=None, default=None, ) diff --git a/hikari/clients/rest_clients/__init__.py b/hikari/clients/rest_clients/__init__.py index 0bab562852..f6961838c5 100644 --- a/hikari/clients/rest_clients/__init__.py +++ b/hikari/clients/rest_clients/__init__.py @@ -24,6 +24,7 @@ __all__ = ["RESTClient"] +from hikari.clients import configs from hikari.clients.rest_clients import channels_component from hikari.clients.rest_clients import current_users_component from hikari.clients.rest_clients import gateways_component @@ -34,7 +35,6 @@ from hikari.clients.rest_clients import users_component from hikari.clients.rest_clients import voices_component from hikari.clients.rest_clients import webhooks_component -from hikari.clients import configs from hikari.net import rest_sessions diff --git a/hikari/clients/rest_clients/channels_component.py b/hikari/clients/rest_clients/channels_component.py index fd63941b32..9c1292890e 100644 --- a/hikari/clients/rest_clients/channels_component.py +++ b/hikari/clients/rest_clients/channels_component.py @@ -20,15 +20,12 @@ __all__ = ["RESTChannelComponent"] +import abc import asyncio import datetime import typing -from hikari.clients.rest_clients import component_base -from hikari.internal import allowed_mentions -from hikari.internal import assertions -from hikari.internal import conversions -from hikari.internal import pagination +from hikari import bases from hikari import channels as _channels from hikari import embeds as _embeds from hikari import guilds @@ -36,20 +33,25 @@ from hikari import media from hikari import messages as _messages from hikari import permissions as _permissions -from hikari import snowflakes from hikari import users from hikari import webhooks +from hikari.clients.rest_clients import component_base +from hikari.internal import allowed_mentions +from hikari.internal import assertions +from hikari.internal import conversions +from hikari.internal import more_typing +from hikari.internal import pagination -class RESTChannelComponent(component_base.BaseRESTComponent): # pylint: disable=W0223 +class RESTChannelComponent(component_base.BaseRESTComponent, abc.ABC): # pylint: disable=W0223 """The REST client component for handling requests to channel endpoints.""" - async def fetch_channel(self, channel: snowflakes.HashableT[_channels.Channel]) -> _channels.Channel: + async def fetch_channel(self, channel: bases.Hashable[_channels.Channel]) -> _channels.Channel: """Get an up to date channel object from a given channel object or ID. Parameters ---------- - channel : :obj:`~typing.Union` [ :obj:`~hikari.channels.Channel`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] + channel : :obj:`~typing.Union` [ :obj:`~hikari.channels.Channel`, :obj:`~hikari.entities.Snowflake`, :obj:`~int` ] The object ID of the channel to look up. Returns @@ -68,13 +70,13 @@ async def fetch_channel(self, channel: snowflakes.HashableT[_channels.Channel]) If the channel does not exist. """ payload = await self._session.get_channel( - channel_id=str(channel.id if isinstance(channel, snowflakes.UniqueEntity) else int(channel)) + channel_id=str(channel.id if isinstance(channel, bases.UniqueEntity) else int(channel)) ) return _channels.deserialize_channel(payload) async def update_channel( self, - channel: snowflakes.HashableT[_channels.Channel], + channel: bases.Hashable[_channels.Channel], *, name: str = ..., position: int = ..., @@ -84,14 +86,14 @@ async def update_channel( user_limit: int = ..., rate_limit_per_user: typing.Union[int, datetime.timedelta] = ..., permission_overwrites: typing.Sequence[_channels.PermissionOverwrite] = ..., - parent_category: typing.Optional[snowflakes.HashableT[_channels.GuildCategory]] = ..., + parent_category: typing.Optional[bases.Hashable[_channels.GuildCategory]] = ..., reason: str = ..., ) -> _channels.Channel: """Update one or more aspects of a given channel ID. Parameters ---------- - channel : :obj:`~typing.Union` [ :obj:`~hikari.channels.Channel`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] + channel : :obj:`~typing.Union` [ :obj:`~hikari.channels.Channel`, :obj:`~hikari.entities.Snowflake`, :obj:`~int` ] The channel ID to update. name : :obj:`~str` If specified, the new name for the channel. This must be @@ -123,7 +125,7 @@ async def update_channel( permission_overwrites : :obj:`~typing.Sequence` [ :obj:`~hikari.channels.PermissionOverwrite` ] If specified, the new list of permission overwrites that are category specific to replace the existing overwrites with. - parent_category : :obj:`~typing.Union` [ :obj:`~hikari.channels.Channel`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ], optional + parent_category : :obj:`~typing.Union` [ :obj:`~hikari.channels.Channel`, :obj:`~hikari.entities.Snowflake`, :obj:`~int` ], optional If specified, the new parent category ID to set for the channel, pass :obj:`~None` to unset. reason : :obj:`~str` @@ -148,7 +150,7 @@ async def update_channel( due to it being outside of the range of a 64 bit integer. """ payload = await self._session.modify_channel( - channel_id=str(channel.id if isinstance(channel, snowflakes.UniqueEntity) else int(channel)), + channel_id=str(channel.id if isinstance(channel, bases.UniqueEntity) else int(channel)), name=name, position=position, topic=topic, @@ -164,9 +166,7 @@ async def update_channel( [po.serialize() for po in permission_overwrites] if permission_overwrites is not ... else ... ), parent_id=( - str( - parent_category.id if isinstance(parent_category, snowflakes.UniqueEntity) else int(parent_category) - ) + str(parent_category.id if isinstance(parent_category, bases.UniqueEntity) else int(parent_category)) if parent_category is not ... and parent_category is not None else parent_category ), @@ -174,12 +174,12 @@ async def update_channel( ) return _channels.deserialize_channel(payload) - async def delete_channel(self, channel: snowflakes.HashableT[_channels.Channel]) -> None: + async def delete_channel(self, channel: bases.Hashable[_channels.Channel]) -> None: """Delete the given channel ID, or if it is a DM, close it. Parameters ---------- - channel : :obj:`~typing.Union` [ :obj:`~hikari.channels.Channel`, :obj:`~hikari.snowflakes.Snowflake` :obj:`~str` ] + channel : :obj:`~typing.Union` [ :obj:`~hikari.channels.Channel`, :obj:`~hikari.entities.Snowflake` :obj:`~str` ] The object or ID of the channel to delete. Returns @@ -210,14 +210,14 @@ async def delete_channel(self, channel: snowflakes.HashableT[_channels.Channel]) Deleted channels cannot be un-deleted. """ await self._session.delete_close_channel( - channel_id=str(channel.id if isinstance(channel, snowflakes.UniqueEntity) else int(channel)) + channel_id=str(channel.id if isinstance(channel, bases.UniqueEntity) else int(channel)) ) def fetch_messages_after( self, - channel: snowflakes.HashableT[_channels.Channel], + channel: bases.Hashable[_channels.Channel], *, - after: typing.Union[datetime.datetime, snowflakes.HashableT[_messages.Message]] = 0, + after: typing.Union[datetime.datetime, bases.Hashable[_messages.Message]] = 0, limit: typing.Optional[int] = None, ) -> typing.AsyncIterator[_messages.Message]: """Return an async iterator that retrieves a channel's message history. @@ -227,12 +227,12 @@ def fetch_messages_after( Parameters ---------- - channel : :obj:`~typing.Union` [ :obj:`~hikari.channels.Channel`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] + channel : :obj:`~typing.Union` [ :obj:`~hikari.channels.Channel`, :obj:`~hikari.entities.Snowflake`, :obj:`~int` ] The ID of the channel to retrieve the messages from. limit : :obj:`~int` If specified, the maximum number of how many messages this iterator should return. - after : :obj:`~typing.Union` [ :obj:`~datetime.datetime`, :obj:`~hikari.channels.Channel`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] + after : :obj:`~typing.Union` [ :obj:`~datetime.datetime`, :obj:`~hikari.channels.Channel`, :obj:`~hikari.entities.Snowflake`, :obj:`~int` ] A object or ID message. Only return messages sent AFTER this message if it's specified else this will return every message after (and including) the first message in the channel. @@ -269,11 +269,11 @@ def fetch_messages_after( zero results, and thus an empty list will be returned instead. """ if isinstance(after, datetime.datetime): - after = str(snowflakes.Snowflake.from_datetime(after)) + after = str(bases.Snowflake.from_datetime(after)) else: - after = str(after.id if isinstance(after, snowflakes.UniqueEntity) else int(after)) + after = str(after.id if isinstance(after, bases.UniqueEntity) else int(after)) return pagination.pagination_handler( - channel_id=str(channel.id if isinstance(channel, snowflakes.UniqueEntity) else int(channel)), + channel_id=str(channel.id if isinstance(channel, bases.UniqueEntity) else int(channel)), deserializer=_messages.Message.deserialize, direction="after", start=after, @@ -284,9 +284,9 @@ def fetch_messages_after( def fetch_messages_before( self, - channel: snowflakes.HashableT[_channels.Channel], + channel: bases.Hashable[_channels.Channel], *, - before: typing.Union[datetime.datetime, snowflakes.HashableT[_messages.Message], None] = None, + before: typing.Union[datetime.datetime, bases.Hashable[_messages.Message], None] = None, limit: typing.Optional[int] = None, ) -> typing.AsyncIterator[_messages.Message]: """Return an async iterator that retrieves a channel's message history. @@ -296,12 +296,12 @@ def fetch_messages_before( Parameters ---------- - channel : :obj:`~typing.Union` [ :obj:`~hikari.channels.Channel`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] + channel : :obj:`~typing.Union` [ :obj:`~hikari.channels.Channel`, :obj:`~hikari.entities.Snowflake`, :obj:`~int` ] The ID of the channel to retrieve the messages from. limit : :obj:`~int` If specified, the maximum number of how many messages this iterator should return. - before : :obj:`~typing.Union` [ :obj:`~datetime.datetime`, :obj:`~hikari.channels.Channel`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] + before : :obj:`~typing.Union` [ :obj:`~datetime.datetime`, :obj:`~hikari.channels.Channel`, :obj:`~hikari.entities.Snowflake`, :obj:`~int` ] A message object or ID. Only return messages sent BEFORE this message if this is specified else this will return every message before (and including) the most recent message in the @@ -339,11 +339,11 @@ def fetch_messages_before( zero results, and thus an empty list will be returned instead. """ if isinstance(before, datetime.datetime): - before = str(snowflakes.Snowflake.from_datetime(before)) + before = str(bases.Snowflake.from_datetime(before)) elif before is not None: - before = str(before.id if isinstance(before, snowflakes.UniqueEntity) else int(before)) + before = str(before.id if isinstance(before, bases.UniqueEntity) else int(before)) return pagination.pagination_handler( - channel_id=str(channel.id if isinstance(channel, snowflakes.UniqueEntity) else int(channel)), + channel_id=str(channel.id if isinstance(channel, bases.UniqueEntity) else int(channel)), deserializer=_messages.Message.deserialize, direction="before", start=before, @@ -354,8 +354,8 @@ def fetch_messages_before( async def fetch_messages_around( self, - channel: snowflakes.HashableT[_channels.Channel], - around: typing.Union[datetime.datetime, snowflakes.HashableT[_messages.Message]], + channel: bases.Hashable[_channels.Channel], + around: typing.Union[datetime.datetime, bases.Hashable[_messages.Message]], *, limit: int = ..., ) -> typing.AsyncIterator[_messages.Message]: @@ -367,9 +367,9 @@ async def fetch_messages_around( Parameters ---------- - channel : :obj:`~typing.Union` [ :obj:`~hikari.channels.Channel`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] + channel : :obj:`~typing.Union` [ :obj:`~hikari.channels.Channel`, :obj:`~hikari.entities.Snowflake`, :obj:`~int` ] The ID of the channel to retrieve the messages from. - around : :obj:`~typing.Union` [ :obj:`~datetime.datetime`, :obj:`~hikari.channels.Channel`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] + around : :obj:`~typing.Union` [ :obj:`~datetime.datetime`, :obj:`~hikari.channels.Channel`, :obj:`~hikari.entities.Snowflake`, :obj:`~int` ] The object or ID of the message to get messages that were sent AROUND it in the provided channel, unlike ``before`` and ``after``, this argument is required and the provided message will also be @@ -410,26 +410,26 @@ async def fetch_messages_around( zero results, and thus an empty list will be returned instead. """ if isinstance(around, datetime.datetime): - around = str(snowflakes.Snowflake.from_datetime(around)) + around = str(bases.Snowflake.from_datetime(around)) else: - around = str(around.id if isinstance(around, snowflakes.UniqueEntity) else int(around)) + around = str(around.id if isinstance(around, bases.UniqueEntity) else int(around)) for payload in await self._session.get_channel_messages( - channel_id=str(channel.id if isinstance(channel, snowflakes.UniqueEntity) else int(channel)), + channel_id=str(channel.id if isinstance(channel, bases.UniqueEntity) else int(channel)), limit=limit, around=around, ): yield _messages.Message.deserialize(payload) async def fetch_message( - self, channel: snowflakes.HashableT[_channels.Channel], message: snowflakes.HashableT[_messages.Message], + self, channel: bases.Hashable[_channels.Channel], message: bases.Hashable[_messages.Message], ) -> _messages.Message: """Get a message from known channel that we can access. Parameters ---------- - channel : :obj:`~typing.Union` [ :obj:`~hikari.channels.Channel`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] + channel : :obj:`~typing.Union` [ :obj:`~hikari.channels.Channel`, :obj:`~hikari.entities.Snowflake`, :obj:`~int` ] The object or ID of the channel to get the message from. - message : :obj:`~typing.Union` [ :obj:`~hikari.messages.Message`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] + message : :obj:`~typing.Union` [ :obj:`~hikari.messages.Message`, :obj:`~hikari.entities.Snowflake`, :obj:`~int` ] The object or ID of the message to retrieve. Returns @@ -452,14 +452,14 @@ async def fetch_message( If the channel or message is not found. """ payload = await self._session.get_channel_message( - channel_id=str(channel.id if isinstance(channel, snowflakes.UniqueEntity) else int(channel)), - message_id=str(message.id if isinstance(message, snowflakes.UniqueEntity) else int(message)), + channel_id=str(channel.id if isinstance(channel, bases.UniqueEntity) else int(channel)), + message_id=str(message.id if isinstance(message, bases.UniqueEntity) else int(message)), ) return _messages.Message.deserialize(payload) async def create_message( self, - channel: snowflakes.HashableT[_channels.Channel], + channel: bases.Hashable[_channels.Channel], *, content: str = ..., nonce: str = ..., @@ -467,14 +467,14 @@ async def create_message( files: typing.Collection[media.IO] = ..., embed: _embeds.Embed = ..., mentions_everyone: bool = True, - user_mentions: typing.Union[typing.Collection[snowflakes.HashableT[users.User]], bool] = True, - role_mentions: typing.Union[typing.Collection[snowflakes.HashableT[guilds.GuildRole]], bool] = True, + user_mentions: typing.Union[typing.Collection[bases.Hashable[users.User]], bool] = True, + role_mentions: typing.Union[typing.Collection[bases.Hashable[guilds.GuildRole]], bool] = True, ) -> _messages.Message: """Create a message in the given channel. Parameters ---------- - channel : :obj:`~typing.Union` [ :obj:`~hikari.channels.Channel`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] + channel : :obj:`~typing.Union` [ :obj:`~hikari.channels.Channel`, :obj:`~hikari.entities.Snowflake`, :obj:`~int` ] The channel or ID of the channel to send to. content : :obj:`~str` If specified, the message content to send with the message. @@ -492,11 +492,11 @@ async def create_message( mentions_everyone : :obj:`~bool` Whether ``@everyone`` and ``@here`` mentions should be resolved by discord and lead to actual pings, defaults to :obj:`~True`. - user_mentions : :obj:`~typing.Union` [ :obj:`~typing.Collection` [ :obj:`~typing.Union` [ :obj:`~hikari.users.User`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ], :obj:`~bool` ] + user_mentions : :obj:`~typing.Union` [ :obj:`~typing.Collection` [ :obj:`~typing.Union` [ :obj:`~hikari.users.User`, :obj:`~hikari.entities.Snowflake`, :obj:`~int` ], :obj:`~bool` ] Either an array of user objects/IDs to allow mentions for, :obj:`~True` to allow all user mentions or :obj:`~False` to block all user mentions from resolving, defaults to :obj:`~True`. - role_mentions : :obj:`~typing.Union` [ :obj:`~typing.Collection` [ :obj:`~typing.Union` [ :obj:`~hikari.guilds.GuildRole`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] ], :obj:`~bool` ] + role_mentions : :obj:`~typing.Union` [ :obj:`~typing.Collection` [ :obj:`~typing.Union` [ :obj:`~hikari.guilds.GuildRole`, :obj:`~hikari.entities.Snowflake`, :obj:`~int` ] ], :obj:`~bool` ] Either an array of guild role objects/IDs to allow mentions for, :obj:`~True` to allow all role mentions or :obj:`~False` to block all role mentions from resolving, defaults to :obj:`~True`. @@ -520,11 +520,11 @@ async def create_message( :obj:`~hikari.errors.ForbiddenHTTPError` If you lack permissions to send to this channel. :obj:`~ValueError` - If more than 100 unique objects/snowflakes are passed for + If more than 100 unique objects/entities are passed for ``role_mentions`` or ``user_mentions``. """ payload = await self._session.create_message( - channel_id=str(channel.id if isinstance(channel, snowflakes.UniqueEntity) else int(channel)), + channel_id=str(channel.id if isinstance(channel, bases.UniqueEntity) else int(channel)), content=content, nonce=nonce, tts=tts, @@ -538,7 +538,7 @@ async def create_message( def safe_create_message( self, - channel: snowflakes.HashableT[_channels.Channel], + channel: bases.Hashable[_channels.Channel], *, content: str = ..., nonce: str = ..., @@ -546,9 +546,9 @@ def safe_create_message( files: typing.Collection[media.IO] = ..., embed: _embeds.Embed = ..., mentions_everyone: bool = False, - user_mentions: typing.Union[typing.Collection[snowflakes.HashableT[users.User]], bool] = False, - role_mentions: typing.Union[typing.Collection[snowflakes.HashableT[guilds.GuildRole]], bool] = False, - ) -> typing.Coroutine[typing.Any, typing.Any, _messages.Message]: + user_mentions: typing.Union[typing.Collection[bases.Hashable[users.User]], bool] = False, + role_mentions: typing.Union[typing.Collection[bases.Hashable[guilds.GuildRole]], bool] = False, + ) -> more_typing.Coroutine[_messages.Message]: """Create a message in the given channel with mention safety. This endpoint has the same signature as :attr:`create_message` with @@ -569,23 +569,23 @@ def safe_create_message( async def update_message( self, - message: snowflakes.HashableT[_messages.Message], - channel: snowflakes.HashableT[_channels.Channel], + message: bases.Hashable[_messages.Message], + channel: bases.Hashable[_channels.Channel], *, content: typing.Optional[str] = ..., embed: typing.Optional[_embeds.Embed] = ..., flags: int = ..., mentions_everyone: bool = True, - user_mentions: typing.Union[typing.Collection[snowflakes.HashableT[users.User]], bool] = True, - role_mentions: typing.Union[typing.Collection[snowflakes.HashableT[guilds.GuildRole]], bool] = True, + user_mentions: typing.Union[typing.Collection[bases.Hashable[users.User]], bool] = True, + role_mentions: typing.Union[typing.Collection[bases.Hashable[guilds.GuildRole]], bool] = True, ) -> _messages.Message: """Update the given message. Parameters ---------- - channel : :obj:`~typing.Union` [ :obj:`~hikari.channels.Channel`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] + channel : :obj:`~typing.Union` [ :obj:`~hikari.channels.Channel`, :obj:`~hikari.entities.Snowflake`, :obj:`~int` ] The object or ID of the channel to get the message from. - message : :obj:`~typing.Union` [ :obj:`~hikari.messages.Message`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] + message : :obj:`~typing.Union` [ :obj:`~hikari.messages.Message`, :obj:`~hikari.entities.Snowflake`, :obj:`~int` ] The object or ID of the message to edit. content : :obj:`~str`, optional If specified, the string content to replace with in the message. @@ -600,11 +600,11 @@ async def update_message( mentions_everyone : :obj:`~bool` Whether ``@everyone`` and ``@here`` mentions should be resolved by discord and lead to actual pings, defaults to :obj:`~True`. - user_mentions : :obj:`~typing.Union` [ :obj:`~typing.Collection` [ :obj:`~typing.Union` [ :obj:`~hikari.users.User`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ], :obj:`~bool` ] + user_mentions : :obj:`~typing.Union` [ :obj:`~typing.Collection` [ :obj:`~typing.Union` [ :obj:`~hikari.users.User`, :obj:`~hikari.entities.Snowflake`, :obj:`~int` ], :obj:`~bool` ] Either an array of user objects/IDs to allow mentions for, :obj:`~True` to allow all user mentions or :obj:`~False` to block all user mentions from resolving, defaults to :obj:`~True`. - role_mentions : :obj:`~typing.Union` [ :obj:`~typing.Collection` [ :obj:`~typing.Union` [ :obj:`~hikari.guilds.GuildRole`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] ], :obj:`~bool` ] + role_mentions : :obj:`~typing.Union` [ :obj:`~typing.Collection` [ :obj:`~typing.Union` [ :obj:`~hikari.guilds.GuildRole`, :obj:`~hikari.entities.Snowflake`, :obj:`~int` ] ], :obj:`~bool` ] Either an array of guild role objects/IDs to allow mentions for, :obj:`~True` to allow all role mentions or :obj:`~False` to block all role mentions from resolving, defaults to :obj:`~True`. @@ -631,12 +631,12 @@ async def update_message( If you try to edit the flags on a message you did not author without the ``MANAGE_MESSAGES`` permission. :obj:`~ValueError` - If more than 100 unique objects/snowflakes are passed for + If more than 100 unique objects/entities are passed for ``role_mentions`` or ``user_mentions``. """ payload = await self._session.edit_message( - channel_id=str(channel.id if isinstance(channel, snowflakes.UniqueEntity) else int(channel)), - message_id=str(message.id if isinstance(message, snowflakes.UniqueEntity) else int(message)), + channel_id=str(channel.id if isinstance(channel, bases.UniqueEntity) else int(channel)), + message_id=str(message.id if isinstance(message, bases.UniqueEntity) else int(message)), content=content, embed=embed.serialize() if embed is not ... and embed is not None else embed, flags=flags, @@ -648,15 +648,15 @@ async def update_message( def safe_update_message( self, - message: snowflakes.HashableT[_messages.Message], - channel: snowflakes.HashableT[_channels.Channel], + message: bases.Hashable[_messages.Message], + channel: bases.Hashable[_channels.Channel], *, content: typing.Optional[str] = ..., embed: typing.Optional[_embeds.Embed] = ..., flags: int = ..., mentions_everyone: bool = False, - user_mentions: typing.Union[typing.Collection[snowflakes.HashableT[users.User]], bool] = False, - role_mentions: typing.Union[typing.Collection[snowflakes.HashableT[guilds.GuildRole]], bool] = False, + user_mentions: typing.Union[typing.Collection[bases.Hashable[users.User]], bool] = False, + role_mentions: typing.Union[typing.Collection[bases.Hashable[guilds.GuildRole]], bool] = False, ) -> typing.Coroutine[typing.Any, typing.Any, _messages.Message]: """Update a message in the given channel with mention safety. @@ -677,19 +677,19 @@ def safe_update_message( async def delete_messages( self, - channel: snowflakes.HashableT[_channels.Channel], - message: snowflakes.HashableT[_messages.Message], - *additional_messages: snowflakes.HashableT[_messages.Message], + channel: bases.Hashable[_channels.Channel], + message: bases.Hashable[_messages.Message], + *additional_messages: bases.Hashable[_messages.Message], ) -> None: """Delete a message in a given channel. Parameters ---------- - channel : :obj:`~typing.Union` [ :obj:`~hikari.channels.Channel`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] + channel : :obj:`~typing.Union` [ :obj:`~hikari.channels.Channel`, :obj:`~hikari.entities.Snowflake`, :obj:`~int` ] The object or ID of the channel to get the message from. - message : :obj:`~typing.Union` [ :obj:`~hikari.messages.Message`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] + message : :obj:`~typing.Union` [ :obj:`~hikari.messages.Message`, :obj:`~hikari.entities.Snowflake`, :obj:`~int` ] The object or ID of the message to delete. - *additional_messages : :obj:`~typing.Union` [ :obj:`~hikari.messages.Message`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] + *additional_messages : :obj:`~typing.Union` [ :obj:`~hikari.messages.Message`, :obj:`~hikari.entities.Snowflake`, :obj:`~int` ] Objects and/or IDs of additional messages to delete in the same channel, in total you can delete up to 100 messages in a request. @@ -719,8 +719,7 @@ async def delete_messages( messages = list( # dict.fromkeys is used to remove duplicate entries that would cause discord to return an error. dict.fromkeys( - str(m.id if isinstance(m, snowflakes.UniqueEntity) else int(m)) - for m in (message, *additional_messages) + str(m.id if isinstance(m, bases.UniqueEntity) else int(m)) for m in (message, *additional_messages) ) ) assertions.assert_that( @@ -729,20 +728,20 @@ async def delete_messages( if len(messages) > 1: await self._session.bulk_delete_messages( - channel_id=str(channel.id if isinstance(channel, snowflakes.UniqueEntity) else int(channel)), + channel_id=str(channel.id if isinstance(channel, bases.UniqueEntity) else int(channel)), messages=messages, ) return None await self._session.delete_message( - channel_id=str(channel.id if isinstance(channel, snowflakes.UniqueEntity) else int(channel)), - message_id=str(message.id if isinstance(message, snowflakes.UniqueEntity) else int(message)), + channel_id=str(channel.id if isinstance(channel, bases.UniqueEntity) else int(channel)), + message_id=str(message.id if isinstance(message, bases.UniqueEntity) else int(message)), ) async def update_channel_overwrite( self, - channel: snowflakes.HashableT[_messages.Message], - overwrite: typing.Union[_channels.PermissionOverwrite, users.User, guilds.GuildRole, snowflakes.Snowflake, int], + channel: bases.Hashable[_messages.Message], + overwrite: typing.Union[_channels.PermissionOverwrite, users.User, guilds.GuildRole, bases.Snowflake, int], target_type: typing.Union[_channels.PermissionOverwriteType, str], *, allow: typing.Union[_permissions.Permission, int] = ..., @@ -753,9 +752,9 @@ async def update_channel_overwrite( Parameters ---------- - channel : :obj:`~typing.Union` [ :obj:`~hikari.messages.Message`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] + channel : :obj:`~typing.Union` [ :obj:`~hikari.messages.Message`, :obj:`~hikari.entities.Snowflake`, :obj:`~int` ] The object or ID of the channel to edit permissions for. - overwrite : :obj:`~typing.Union` [ :obj:`~hikari.channels.PermissionOverwrite`, :obj:`~hikari.guilds.GuildRole`, :obj:`~hikari.users.User`, :obj:`~hikari.snowflakes.Snowflake` , :obj:`~int` ] + overwrite : :obj:`~typing.Union` [ :obj:`~hikari.channels.PermissionOverwrite`, :obj:`~hikari.guilds.GuildRole`, :obj:`~hikari.users.User`, :obj:`~hikari.entities.Snowflake` , :obj:`~int` ] The object or ID of the target member or role to edit/create the overwrite for. target_type : :obj:`~typing.Union` [ :obj:`~hikari.channels.PermissionOverwriteType`, :obj:`~int` ] @@ -782,8 +781,8 @@ async def update_channel_overwrite( If you lack permission to do this. """ await self._session.edit_channel_permissions( - channel_id=str(channel.id if isinstance(channel, snowflakes.UniqueEntity) else int(channel)), - overwrite_id=str(overwrite.id if isinstance(overwrite, snowflakes.UniqueEntity) else int(overwrite)), + channel_id=str(channel.id if isinstance(channel, bases.UniqueEntity) else int(channel)), + overwrite_id=str(overwrite.id if isinstance(overwrite, bases.UniqueEntity) else int(overwrite)), type_=target_type, allow=allow, deny=deny, @@ -791,13 +790,13 @@ async def update_channel_overwrite( ) async def fetch_invites_for_channel( - self, channel: snowflakes.HashableT[_channels.Channel] + self, channel: bases.Hashable[_channels.Channel] ) -> typing.Sequence[invites.InviteWithMetadata]: """Get invites for a given channel. Parameters ---------- - channel : :obj:`~typing.Union` [ :obj:`~hikari.channels.Channel`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] + channel : :obj:`~typing.Union` [ :obj:`~hikari.channels.Channel`, :obj:`~hikari.entities.Snowflake`, :obj:`~int` ] The object or ID of the channel to get invites for. Returns @@ -816,19 +815,19 @@ async def fetch_invites_for_channel( If the channel does not exist. """ payload = await self._session.get_channel_invites( - channel_id=str(channel.id if isinstance(channel, snowflakes.UniqueEntity) else int(channel)) + channel_id=str(channel.id if isinstance(channel, bases.UniqueEntity) else int(channel)) ) return [invites.InviteWithMetadata.deserialize(invite) for invite in payload] async def create_invite_for_channel( self, - channel: snowflakes.HashableT[_channels.Channel], + channel: bases.Hashable[_channels.Channel], *, max_age: typing.Union[int, datetime.timedelta] = ..., max_uses: int = ..., temporary: bool = ..., unique: bool = ..., - target_user: snowflakes.HashableT[users.User] = ..., + target_user: bases.Hashable[users.User] = ..., target_user_type: typing.Union[invites.TargetUserType, int] = ..., reason: str = ..., ) -> invites.InviteWithMetadata: @@ -850,7 +849,7 @@ async def create_invite_for_channel( user is kicked when their session ends unless they are given a role. unique : :obj:`~bool` If specified, whether to try to reuse a similar invite. - target_user : :obj:`~typing.Union` [ :obj:`~hikari.users.User`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] + target_user : :obj:`~typing.Union` [ :obj:`~hikari.users.User`, :obj:`~hikari.entities.Snowflake`, :obj:`~int` ] If specified, the object or ID of the user this invite should target. target_user_type : :obj:`~typing.Union` [ :obj:`~hikari.invites.TargetUserType`, :obj:`~int` ] @@ -877,13 +876,13 @@ async def create_invite_for_channel( due to it being outside of the range of a 64 bit integer. """ payload = await self._session.create_channel_invite( - channel_id=str(channel.id if isinstance(channel, snowflakes.UniqueEntity) else int(channel)), + channel_id=str(channel.id if isinstance(channel, bases.UniqueEntity) else int(channel)), max_age=int(max_age.total_seconds()) if isinstance(max_age, datetime.timedelta) else max_age, max_uses=max_uses, temporary=temporary, unique=unique, target_user=( - str(target_user.id if isinstance(target_user, snowflakes.UniqueEntity) else int(target_user)) + str(target_user.id if isinstance(target_user, bases.UniqueEntity) else int(target_user)) if target_user is not ... else ... ), @@ -894,16 +893,16 @@ async def create_invite_for_channel( async def delete_channel_overwrite( self, - channel: snowflakes.HashableT[_channels.Channel], - overwrite: typing.Union[_channels.PermissionOverwrite, guilds.GuildRole, users.User, snowflakes.Snowflake, int], + channel: bases.Hashable[_channels.Channel], + overwrite: typing.Union[_channels.PermissionOverwrite, guilds.GuildRole, users.User, bases.Snowflake, int], ) -> None: """Delete a channel permission overwrite for a user or a role. Parameters ---------- - channel : :obj:`~typing.Union` [ :obj:`~hikari.channels.Channel`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] + channel : :obj:`~typing.Union` [ :obj:`~hikari.channels.Channel`, :obj:`~hikari.entities.Snowflake`, :obj:`~int` ] The object or ID of the channel to delete the overwrite from. - overwrite : :obj:`~typing.Union` [ :obj:`~hikari.channels.PermissionOverwrite`, :obj:`~hikari.guilds.GuildRole`, :obj:`~hikari.users.User`, :obj:`~hikari.snowflakes.Snowflake`, :obj:int ] + overwrite : :obj:`~typing.Union` [ :obj:`~hikari.channels.PermissionOverwrite`, :obj:`~hikari.guilds.GuildRole`, :obj:`~hikari.users.User`, :obj:`~hikari.entities.Snowflake`, :obj:int ] The ID of the entity this overwrite targets. Raises @@ -917,16 +916,16 @@ async def delete_channel_overwrite( If you lack the ``MANAGE_ROLES`` permission for that channel. """ await self._session.delete_channel_permission( - channel_id=str(channel.id if isinstance(channel, snowflakes.UniqueEntity) else int(channel)), - overwrite_id=str(overwrite.id if isinstance(overwrite, snowflakes.UniqueEntity) else int(overwrite)), + channel_id=str(channel.id if isinstance(channel, bases.UniqueEntity) else int(channel)), + overwrite_id=str(overwrite.id if isinstance(overwrite, bases.UniqueEntity) else int(overwrite)), ) - async def trigger_typing(self, channel: snowflakes.HashableT[_channels.Channel]) -> None: + async def trigger_typing(self, channel: bases.Hashable[_channels.Channel]) -> None: """Trigger the typing indicator for ``10`` seconds in a channel. Parameters ---------- - channel : :obj:`~typing.Union` [ :obj:`~hikari.channels.Channel`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] + channel : :obj:`~typing.Union` [ :obj:`~hikari.channels.Channel`, :obj:`~hikari.entities.Snowflake`, :obj:`~int` ] The object or ID of the channel to appear to be typing in. Raises @@ -940,22 +939,22 @@ async def trigger_typing(self, channel: snowflakes.HashableT[_channels.Channel]) If you are not able to type in the channel. """ await self._session.trigger_typing_indicator( - channel_id=str(channel.id if isinstance(channel, snowflakes.UniqueEntity) else int(channel)) + channel_id=str(channel.id if isinstance(channel, bases.UniqueEntity) else int(channel)) ) async def fetch_pins( - self, channel: snowflakes.HashableT[_channels.Channel] - ) -> typing.Mapping[snowflakes.Snowflake, _messages.Message]: + self, channel: bases.Hashable[_channels.Channel] + ) -> typing.Mapping[bases.Snowflake, _messages.Message]: """Get pinned messages for a given channel. Parameters ---------- - channel : :obj:`~typing.Union` [ :obj:`~hikari.channels.Channel`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] + channel : :obj:`~typing.Union` [ :obj:`~hikari.channels.Channel`, :obj:`~hikari.entities.Snowflake`, :obj:`~int` ] The object or ID of the channel to get messages from. Returns ------- - :obj:`~typing.Mapping` [ :obj:`~hikari.snowflakes.Snowflake`, :obj:`~hikari.messages.Message` ] + :obj:`~typing.Mapping` [ :obj:`~hikari.entities.Snowflake`, :obj:`~hikari.messages.Message` ] A list of message objects. Raises @@ -975,20 +974,20 @@ async def fetch_pins( will not be returned. """ payload = await self._session.get_pinned_messages( - channel_id=str(channel.id if isinstance(channel, snowflakes.UniqueEntity) else int(channel)) + channel_id=str(channel.id if isinstance(channel, bases.UniqueEntity) else int(channel)) ) return {message.id: message for message in map(_messages.Message.deserialize, payload)} async def pin_message( - self, channel: snowflakes.HashableT[_channels.Channel], message: snowflakes.HashableT[_messages.Message], + self, channel: bases.Hashable[_channels.Channel], message: bases.Hashable[_messages.Message], ) -> None: """Add a pinned message to the channel. Parameters ---------- - channel : :obj:`~typing.Union` [ :obj:`~hikari.channels.Channel`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] + channel : :obj:`~typing.Union` [ :obj:`~hikari.channels.Channel`, :obj:`~hikari.entities.Snowflake`, :obj:`~int` ] The object or ID of the channel to pin a message to. - message : :obj:`~typing.Union` [ :obj:`~hikari.messages.Message`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] + message : :obj:`~typing.Union` [ :obj:`~hikari.messages.Message`, :obj:`~hikari.entities.Snowflake`, :obj:`~int` ] The object or ID of the message to pin. Raises @@ -1002,12 +1001,12 @@ async def pin_message( If the message or channel do not exist. """ await self._session.add_pinned_channel_message( - channel_id=str(channel.id if isinstance(channel, snowflakes.UniqueEntity) else int(channel)), - message_id=str(message.id if isinstance(message, snowflakes.UniqueEntity) else int(message)), + channel_id=str(channel.id if isinstance(channel, bases.UniqueEntity) else int(channel)), + message_id=str(message.id if isinstance(message, bases.UniqueEntity) else int(message)), ) async def unpin_message( - self, channel: snowflakes.HashableT[_channels.Channel], message: snowflakes.HashableT[_messages.Message], + self, channel: bases.Hashable[_channels.Channel], message: bases.Hashable[_messages.Message], ) -> None: """Remove a pinned message from the channel. @@ -1015,9 +1014,9 @@ async def unpin_message( Parameters ---------- - channel : :obj:`~typing.Union` [ :obj:`~hikari.channels.Channel`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] + channel : :obj:`~typing.Union` [ :obj:`~hikari.channels.Channel`, :obj:`~hikari.entities.Snowflake`, :obj:`~int` ] The ID of the channel to remove a pin from. - message : :obj:`~typing.Union` [ :obj:`~hikari.messages.Message`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] + message : :obj:`~typing.Union` [ :obj:`~hikari.messages.Message`, :obj:`~hikari.entities.Snowflake`, :obj:`~int` ] The object or ID of the message to unpin. Raises @@ -1031,13 +1030,13 @@ async def unpin_message( If the message or channel do not exist. """ await self._session.delete_pinned_channel_message( - channel_id=str(channel.id if isinstance(channel, snowflakes.UniqueEntity) else int(channel)), - message_id=str(message.id if isinstance(message, snowflakes.UniqueEntity) else int(message)), + channel_id=str(channel.id if isinstance(channel, bases.UniqueEntity) else int(channel)), + message_id=str(message.id if isinstance(message, bases.UniqueEntity) else int(message)), ) async def create_webhook( self, - channel: snowflakes.HashableT[_channels.GuildChannel], + channel: bases.Hashable[_channels.GuildChannel], name: str, *, avatar_data: conversions.FileLikeT = ..., @@ -1047,7 +1046,7 @@ async def create_webhook( Parameters ---------- - channel : :obj:`~typing.Union` [ :obj:`~hikari.channels.GuildChannel`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] + channel : :obj:`~typing.Union` [ :obj:`~hikari.channels.GuildChannel`, :obj:`~hikari.entities.Snowflake`, :obj:`~int` ] The object or ID of the channel for webhook to be created in. name : :obj:`~str` The webhook's name string. @@ -1075,7 +1074,7 @@ async def create_webhook( due to it being outside of the range of a 64 bit integer. """ payload = await self._session.create_webhook( - channel_id=str(channel.id if isinstance(channel, snowflakes.UniqueEntity) else int(channel)), + channel_id=str(channel.id if isinstance(channel, bases.UniqueEntity) else int(channel)), name=name, avatar=conversions.get_bytes_from_resource(avatar_data) if avatar_data is not ... else ..., reason=reason, @@ -1083,13 +1082,13 @@ async def create_webhook( return webhooks.Webhook.deserialize(payload) async def fetch_channel_webhooks( - self, channel: snowflakes.HashableT[_channels.GuildChannel] + self, channel: bases.Hashable[_channels.GuildChannel] ) -> typing.Sequence[webhooks.Webhook]: """Get all webhooks from a given channel. Parameters ---------- - channel : :obj:`~typing.Union` [ :obj:`~hikari.channels.GuildChannel`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] + channel : :obj:`~typing.Union` [ :obj:`~hikari.channels.GuildChannel`, :obj:`~hikari.entities.Snowflake`, :obj:`~int` ] The object or ID of the guild channel to get the webhooks from. Returns @@ -1109,6 +1108,6 @@ async def fetch_channel_webhooks( can not see the given channel. """ payload = await self._session.get_channel_webhooks( - channel_id=str(channel.id if isinstance(channel, snowflakes.UniqueEntity) else int(channel)) + channel_id=str(channel.id if isinstance(channel, bases.UniqueEntity) else int(channel)) ) return [webhooks.Webhook.deserialize(webhook) for webhook in payload] diff --git a/hikari/clients/rest_clients/current_users_component.py b/hikari/clients/rest_clients/current_users_component.py index bcd21a67a2..b615698a23 100644 --- a/hikari/clients/rest_clients/current_users_component.py +++ b/hikari/clients/rest_clients/current_users_component.py @@ -24,14 +24,14 @@ import datetime import typing -from hikari.clients.rest_clients import component_base -from hikari.internal import conversions -from hikari.internal import pagination +from hikari import bases from hikari import channels as _channels from hikari import guilds from hikari import oauth2 -from hikari import snowflakes from hikari import users +from hikari.clients.rest_clients import component_base +from hikari.internal import conversions +from hikari.internal import pagination class RESTCurrentUserComponent(component_base.BaseRESTComponent, abc.ABC): # pylint: disable=W0223 @@ -98,7 +98,7 @@ async def fetch_my_connections(self) -> typing.Sequence[oauth2.OwnConnection]: def fetch_my_guilds_after( self, *, - after: typing.Union[datetime.datetime, snowflakes.HashableT[guilds.Guild]] = 0, + after: typing.Union[datetime.datetime, bases.Hashable[guilds.Guild]] = 0, limit: typing.Optional[int] = None, ) -> typing.AsyncIterator[oauth2.OwnGuild]: """Get an async iterator of the guilds the current user is in. @@ -108,7 +108,7 @@ def fetch_my_guilds_after( Parameters ---------- - after : :obj:`~typing.Union` [ :obj:`~datetime.datetime`, :obj:`~hikari.guilds.Guild`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] + after : :obj:`~typing.Union` [ :obj:`~datetime.datetime`, :obj:`~hikari.guilds.Guild`, :obj:`~hikari.entities.Snowflake`, :obj:`~int` ] The object or ID of a guild to get guilds that were created after it if specified, else this will start at the oldest guild. limit : :obj:`~int` @@ -136,9 +136,9 @@ def fetch_my_guilds_after( due to it being outside of the range of a 64 bit integer. """ if isinstance(after, datetime.datetime): - after = str(snowflakes.Snowflake.from_datetime(after)) + after = str(bases.Snowflake.from_datetime(after)) else: - after = str(after.id if isinstance(after, snowflakes.UniqueEntity) else int(after)) + after = str(after.id if isinstance(after, bases.UniqueEntity) else int(after)) return pagination.pagination_handler( deserializer=oauth2.OwnGuild.deserialize, direction="after", @@ -151,7 +151,7 @@ def fetch_my_guilds_after( def fetch_my_guilds_before( self, *, - before: typing.Union[datetime.datetime, snowflakes.HashableT[guilds.Guild], None] = None, + before: typing.Union[datetime.datetime, bases.Hashable[guilds.Guild], None] = None, limit: typing.Optional[int] = None, ) -> typing.AsyncIterator[oauth2.OwnGuild]: """Get an async iterator of the guilds the current user is in. @@ -161,7 +161,7 @@ def fetch_my_guilds_before( Parameters ---------- - before : :obj:`~typing.Union` [ :obj:`~datetime.datetime`, :obj:`~hikari.guilds.Guild`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] + before : :obj:`~typing.Union` [ :obj:`~datetime.datetime`, :obj:`~hikari.guilds.Guild`, :obj:`~hikari.entities.Snowflake`, :obj:`~int` ] The object or ID of a guild to get guilds that were created before it if specified, else this will start at the newest guild. limit : :obj:`~int` @@ -182,9 +182,9 @@ def fetch_my_guilds_before( due to it being outside of the range of a 64 bit integer. """ if isinstance(before, datetime.datetime): - before = str(snowflakes.Snowflake.from_datetime(before)) + before = str(bases.Snowflake.from_datetime(before)) elif before is not None: - before = str(before.id if isinstance(before, snowflakes.UniqueEntity) else int(before)) + before = str(before.id if isinstance(before, bases.UniqueEntity) else int(before)) return pagination.pagination_handler( deserializer=oauth2.OwnGuild.deserialize, direction="before", @@ -194,12 +194,12 @@ def fetch_my_guilds_before( limit=limit, ) - async def leave_guild(self, guild: snowflakes.HashableT[guilds.Guild]) -> None: + async def leave_guild(self, guild: bases.Hashable[guilds.Guild]) -> None: """Make the current user leave a given guild. Parameters ---------- - guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] + guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.entities.Snowflake`, :obj:`~int` ] The object or ID of the guild to leave. Raises @@ -210,16 +210,14 @@ async def leave_guild(self, guild: snowflakes.HashableT[guilds.Guild]) -> None: If any invalid snowflake IDs are passed; a snowflake may be invalid due to it being outside of the range of a 64 bit integer. """ - await self._session.leave_guild( - guild_id=str(guild.id if isinstance(guild, snowflakes.UniqueEntity) else int(guild)) - ) + await self._session.leave_guild(guild_id=str(guild.id if isinstance(guild, bases.UniqueEntity) else int(guild))) - async def create_dm_channel(self, recipient: snowflakes.HashableT[users.User]) -> _channels.DMChannel: + async def create_dm_channel(self, recipient: bases.Hashable[users.User]) -> _channels.DMChannel: """Create a new DM channel with a given user. Parameters ---------- - recipient : :obj:`~typing.Union` [ :obj:`~hikari.users.User`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] + recipient : :obj:`~typing.Union` [ :obj:`~hikari.users.User`, :obj:`~hikari.entities.Snowflake`, :obj:`~int` ] The object or ID of the user to create the new DM channel with. Returns @@ -236,6 +234,6 @@ async def create_dm_channel(self, recipient: snowflakes.HashableT[users.User]) - due to it being outside of the range of a 64 bit integer. """ payload = await self._session.create_dm( - recipient_id=str(recipient.id if isinstance(recipient, snowflakes.UniqueEntity) else int(recipient)) + recipient_id=str(recipient.id if isinstance(recipient, bases.UniqueEntity) else int(recipient)) ) return _channels.DMChannel.deserialize(payload) diff --git a/hikari/clients/rest_clients/gateways_component.py b/hikari/clients/rest_clients/gateways_component.py index 9a85681505..1c87f4ea50 100644 --- a/hikari/clients/rest_clients/gateways_component.py +++ b/hikari/clients/rest_clients/gateways_component.py @@ -22,8 +22,8 @@ import abc -from hikari.clients.rest_clients import component_base from hikari import gateway_entities +from hikari.clients.rest_clients import component_base class RESTGatewayComponent(component_base.BaseRESTComponent, abc.ABC): # pylint: disable=W0223 diff --git a/hikari/clients/rest_clients/guilds_component.py b/hikari/clients/rest_clients/guilds_component.py index ebb59fbe1b..1455fcb272 100644 --- a/hikari/clients/rest_clients/guilds_component.py +++ b/hikari/clients/rest_clients/guilds_component.py @@ -24,20 +24,20 @@ import datetime import typing -from hikari.clients.rest_clients import component_base -from hikari.internal import conversions -from hikari.internal import pagination from hikari import audit_logs +from hikari import bases from hikari import channels as _channels from hikari import colors from hikari import emojis from hikari import guilds from hikari import invites from hikari import permissions as _permissions -from hikari import snowflakes from hikari import users from hikari import voices from hikari import webhooks +from hikari.clients.rest_clients import component_base +from hikari.internal import conversions +from hikari.internal import pagination def _get_member_id(member: guilds.GuildMember) -> str: @@ -49,20 +49,20 @@ class RESTGuildComponent(component_base.BaseRESTComponent, abc.ABC): # pylint: async def fetch_audit_log( self, - guild: snowflakes.HashableT[guilds.Guild], + guild: bases.Hashable[guilds.Guild], *, - user: snowflakes.HashableT[users.User] = ..., + user: bases.Hashable[users.User] = ..., action_type: typing.Union[audit_logs.AuditLogEventType, int] = ..., limit: int = ..., - before: typing.Union[datetime.datetime, snowflakes.HashableT[audit_logs.AuditLogEntry]] = ..., + before: typing.Union[datetime.datetime, bases.Hashable[audit_logs.AuditLogEntry]] = ..., ) -> audit_logs.AuditLog: """Get an audit log object for the given guild. Parameters ---------- - guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] + guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.entities.Snowflake`, :obj:`~int` ] The object or ID of the guild to get the audit logs for. - user : :obj:`~typing.Union` [ :obj:`~hikari.users.User`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] + user : :obj:`~typing.Union` [ :obj:`~hikari.users.User`, :obj:`~hikari.entities.Snowflake`, :obj:`~int` ] If specified, the object or ID of the user to filter by. action_type : :obj:`~typing.Union` [ :obj:`~hikari.audit_logs.AuditLogEventType`, :obj:`~int` ] If specified, the action type to look up. Passing a raw integer @@ -70,7 +70,7 @@ async def fetch_audit_log( limit : :obj:`~int` If specified, the limit to apply to the number of records. Defaults to ``50``. Must be between ``1`` and ``100`` inclusive. - before : :obj:`~typing.Union` [ :obj:`~datetime.datetime`, :obj:`~hikari.audit_logs.AuditLogEntry`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] + before : :obj:`~typing.Union` [ :obj:`~datetime.datetime`, :obj:`~hikari.audit_logs.AuditLogEntry`, :obj:`~hikari.entities.Snowflake`, :obj:`~int` ] If specified, the object or ID of the entry that all retrieved entries should have occurred befor. @@ -90,14 +90,12 @@ async def fetch_audit_log( If the guild does not exist. """ if isinstance(before, datetime.datetime): - before = str(snowflakes.Snowflake.from_datetime(before)) + before = str(bases.Snowflake.from_datetime(before)) elif before is not ...: - before = str(before.id if isinstance(before, snowflakes.UniqueEntity) else int(before)) + before = str(before.id if isinstance(before, bases.UniqueEntity) else int(before)) payload = await self._session.get_guild_audit_log( - guild_id=str(guild.id if isinstance(guild, snowflakes.UniqueEntity) else int(guild)), - user_id=( - str(user.id if isinstance(user, snowflakes.UniqueEntity) else int(user)) if user is not ... else ... - ), + guild_id=str(guild.id if isinstance(guild, bases.UniqueEntity) else int(guild)), + user_id=(str(user.id if isinstance(user, bases.UniqueEntity) else int(user)) if user is not ... else ...), action_type=action_type, limit=limit, before=before, @@ -106,10 +104,10 @@ async def fetch_audit_log( def fetch_audit_log_entries_before( self, - guild: snowflakes.HashableT[guilds.Guild], + guild: bases.Hashable[guilds.Guild], *, - before: typing.Union[datetime.datetime, snowflakes.HashableT[audit_logs.AuditLogEntry], None] = None, - user: snowflakes.HashableT[users.User] = ..., + before: typing.Union[datetime.datetime, bases.Hashable[audit_logs.AuditLogEntry], None] = None, + user: bases.Hashable[users.User] = ..., action_type: typing.Union[audit_logs.AuditLogEventType, int] = ..., limit: typing.Optional[int] = None, ) -> audit_logs.AuditLogIterator: @@ -120,13 +118,13 @@ def fetch_audit_log_entries_before( Parameters ---------- - guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] + guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.entities.Snowflake`, :obj:`~int` ] The ID or object of the guild to get audit log entries for - before : :obj:`~typing.Union` [ :obj:`~datetime.datetime`, :obj:`~hikari.audit_logs.AuditLogEntry`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ], optional + before : :obj:`~typing.Union` [ :obj:`~datetime.datetime`, :obj:`~hikari.audit_logs.AuditLogEntry`, :obj:`~hikari.entities.Snowflake`, :obj:`~int` ], optional If specified, the ID or object of the entry or datetime to get entries that happened before otherwise this will start from the newest entry. - user : :obj:`~typing.Union` [ :obj:`~hikari.users.User`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] + user : :obj:`~typing.Union` [ :obj:`~hikari.users.User`, :obj:`~hikari.entities.Snowflake`, :obj:`~int` ] If specified, the object or ID of the user to filter by. action_type : :obj:`~typing.Union` [ :obj:`~hikari.audit_logs.AuditLogEventType`, :obj:`~int` ] If specified, the action type to look up. Passing a raw integer @@ -162,30 +160,28 @@ def fetch_audit_log_entries_before( to oldest). """ if isinstance(before, datetime.datetime): - before = str(snowflakes.Snowflake.from_datetime(before)) + before = str(bases.Snowflake.from_datetime(before)) elif before is not None: - before = str(before.id if isinstance(before, snowflakes.UniqueEntity) else int(before)) + before = str(before.id if isinstance(before, bases.UniqueEntity) else int(before)) return audit_logs.AuditLogIterator( - guild_id=str(guild.id if isinstance(guild, snowflakes.UniqueEntity) else int(guild)), + guild_id=str(guild.id if isinstance(guild, bases.UniqueEntity) else int(guild)), request=self._session.get_guild_audit_log, before=before, - user_id=( - str(user.id if isinstance(user, snowflakes.UniqueEntity) else int(user)) if user is not ... else ... - ), + user_id=(str(user.id if isinstance(user, bases.UniqueEntity) else int(user)) if user is not ... else ...), action_type=action_type, limit=limit, ) async def fetch_guild_emoji( - self, guild: snowflakes.HashableT[guilds.Guild], emoji: snowflakes.HashableT[emojis.GuildEmoji], + self, guild: bases.Hashable[guilds.Guild], emoji: bases.Hashable[emojis.GuildEmoji], ) -> emojis.GuildEmoji: """Get an updated emoji object from a specific guild. Parameters ---------- - guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] + guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.entities.Snowflake`, :obj:`~int` ] The object or ID of the guild to get the emoji from. - emoji : :obj:`~typing.Union` [ :obj:`~hikari.emojis.GuildEmoji`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] + emoji : :obj:`~typing.Union` [ :obj:`~hikari.emojis.GuildEmoji`, :obj:`~hikari.entities.Snowflake`, :obj:`~int` ] The object or ID of the emoji to get. Returns @@ -204,17 +200,17 @@ async def fetch_guild_emoji( If you aren't a member of said guild. """ payload = await self._session.get_guild_emoji( - guild_id=str(guild.id if isinstance(guild, snowflakes.UniqueEntity) else int(guild)), - emoji_id=str(emoji.id if isinstance(emoji, snowflakes.UniqueEntity) else int(emoji)), + guild_id=str(guild.id if isinstance(guild, bases.UniqueEntity) else int(guild)), + emoji_id=str(emoji.id if isinstance(emoji, bases.UniqueEntity) else int(emoji)), ) return emojis.GuildEmoji.deserialize(payload) - async def fetch_guild_emojis(self, guild: snowflakes.HashableT[guilds.Guild]) -> typing.Sequence[emojis.GuildEmoji]: + async def fetch_guild_emojis(self, guild: bases.Hashable[guilds.Guild]) -> typing.Sequence[emojis.GuildEmoji]: """Get emojis for a given guild object or ID. Parameters ---------- - guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] + guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.entities.Snowflake`, :obj:`~int` ] The object or ID of the guild to get the emojis for. Returns @@ -233,30 +229,30 @@ async def fetch_guild_emojis(self, guild: snowflakes.HashableT[guilds.Guild]) -> If you aren't a member of the guild. """ payload = await self._session.list_guild_emojis( - guild_id=str(guild.id if isinstance(guild, snowflakes.UniqueEntity) else int(guild)) + guild_id=str(guild.id if isinstance(guild, bases.UniqueEntity) else int(guild)) ) return [emojis.GuildEmoji.deserialize(emoji) for emoji in payload] async def create_guild_emoji( self, - guild: snowflakes.HashableT[guilds.GuildRole], + guild: bases.Hashable[guilds.GuildRole], name: str, image_data: conversions.FileLikeT, *, - roles: typing.Sequence[snowflakes.HashableT[guilds.GuildRole]] = ..., + roles: typing.Sequence[bases.Hashable[guilds.GuildRole]] = ..., reason: str = ..., ) -> emojis.GuildEmoji: """Create a new emoji for a given guild. Parameters ---------- - guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.GuildRole`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] + guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.GuildRole`, :obj:`~hikari.entities.Snowflake`, :obj:`~int` ] The object or ID of the guild to create the emoji in. name : :obj:`~str` The new emoji's name. image_data : ``hikari.internal.conversions.FileLikeT`` The ``128x128`` image data. - roles : :obj:`~typing.Sequence` [ :obj:`~typing.Union` [ :obj:`~hikari.guilds.GuildRole`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] ] + roles : :obj:`~typing.Sequence` [ :obj:`~typing.Union` [ :obj:`~hikari.guilds.GuildRole`, :obj:`~hikari.entities.Snowflake`, :obj:`~int` ] ] If specified, a list of role objects or IDs for which the emoji will be whitelisted. If empty, all roles are whitelisted. reason : :obj:`~str` @@ -284,10 +280,10 @@ async def create_guild_emoji( due to it being outside of the range of a 64 bit integer. """ payload = await self._session.create_guild_emoji( - guild_id=str(guild.id if isinstance(guild, snowflakes.UniqueEntity) else int(guild)), + guild_id=str(guild.id if isinstance(guild, bases.UniqueEntity) else int(guild)), name=name, image=conversions.get_bytes_from_resource(image_data), - roles=[str(role.id if isinstance(role, snowflakes.UniqueEntity) else int(role)) for role in roles] + roles=[str(role.id if isinstance(role, bases.UniqueEntity) else int(role)) for role in roles] if roles is not ... else ..., reason=reason, @@ -296,25 +292,25 @@ async def create_guild_emoji( async def update_guild_emoji( self, - guild: snowflakes.HashableT[guilds.Guild], - emoji: snowflakes.HashableT[emojis.GuildEmoji], + guild: bases.Hashable[guilds.Guild], + emoji: bases.Hashable[emojis.GuildEmoji], *, name: str = ..., - roles: typing.Sequence[snowflakes.HashableT[guilds.GuildRole]] = ..., + roles: typing.Sequence[bases.Hashable[guilds.GuildRole]] = ..., reason: str = ..., ) -> emojis.GuildEmoji: """Edits an emoji of a given guild. Parameters ---------- - guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] + guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.entities.Snowflake`, :obj:`~int` ] The object or ID of the guild to which the emoji to edit belongs to. - emoji : :obj:`~typing.Union` [ :obj:`~hikari.emojis.GuildEmoji`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] + emoji : :obj:`~typing.Union` [ :obj:`~hikari.emojis.GuildEmoji`, :obj:`~hikari.entities.Snowflake`, :obj:`~int` ] The object or ID of the emoji to edit. name : :obj:`~str` If specified, a new emoji name string. Keep unspecified to leave the name unchanged. - roles : :obj:`~typing.Sequence` [ :obj:`~typing.Union` [ :obj:`~hikari.guilds.GuildRole`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] ] + roles : :obj:`~typing.Sequence` [ :obj:`~typing.Union` [ :obj:`~hikari.guilds.GuildRole`, :obj:`~hikari.entities.Snowflake`, :obj:`~int` ] ] If specified, a list of objects or IDs for the new whitelisted roles. Set to an empty list to whitelist all roles. Keep unspecified to leave the same roles already set. @@ -339,10 +335,10 @@ async def update_guild_emoji( member of the given guild. """ payload = await self._session.modify_guild_emoji( - guild_id=str(guild.id if isinstance(guild, snowflakes.UniqueEntity) else int(guild)), - emoji_id=str(emoji.id if isinstance(emoji, snowflakes.UniqueEntity) else int(emoji)), + guild_id=str(guild.id if isinstance(guild, bases.UniqueEntity) else int(guild)), + emoji_id=str(emoji.id if isinstance(emoji, bases.UniqueEntity) else int(emoji)), name=name, - roles=[str(role.id if isinstance(role, snowflakes.UniqueEntity) else int(role)) for role in roles] + roles=[str(role.id if isinstance(role, bases.UniqueEntity) else int(role)) for role in roles] if roles is not ... else ..., reason=reason, @@ -350,15 +346,15 @@ async def update_guild_emoji( return emojis.GuildEmoji.deserialize(payload) async def delete_guild_emoji( - self, guild: snowflakes.HashableT[guilds.Guild], emoji: snowflakes.HashableT[emojis.GuildEmoji], + self, guild: bases.Hashable[guilds.Guild], emoji: bases.Hashable[emojis.GuildEmoji], ) -> None: """Delete an emoji from a given guild. Parameters ---------- - guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] + guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.entities.Snowflake`, :obj:`~int` ] The object or ID of the guild to delete the emoji from. - emoji : :obj:`~typing.Union` [ :obj:`~hikari.emojis.GuildEmoji`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] + emoji : :obj:`~typing.Union` [ :obj:`~hikari.emojis.GuildEmoji`, :obj:`~hikari.entities.Snowflake`, :obj:`~int` ] The object or ID of the guild emoji to be deleted. Raises @@ -373,8 +369,8 @@ async def delete_guild_emoji( member of said guild. """ await self._session.delete_guild_emoji( - guild_id=str(guild.id if isinstance(guild, snowflakes.UniqueEntity) else int(guild)), - emoji_id=str(emoji.id if isinstance(emoji, snowflakes.UniqueEntity) else int(emoji)), + guild_id=str(guild.id if isinstance(guild, bases.UniqueEntity) else int(guild)), + emoji_id=str(emoji.id if isinstance(emoji, bases.UniqueEntity) else int(emoji)), ) async def create_guild( @@ -448,12 +444,12 @@ async def create_guild( ) return guilds.Guild.deserialize(payload) - async def fetch_guild(self, guild: snowflakes.HashableT[guilds.Guild]) -> guilds.Guild: + async def fetch_guild(self, guild: bases.Hashable[guilds.Guild]) -> guilds.Guild: """Get a given guild's object. Parameters ---------- - guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] + guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.entities.Snowflake`, :obj:`~int` ] The object or ID of the guild to get. Returns @@ -472,16 +468,16 @@ async def fetch_guild(self, guild: snowflakes.HashableT[guilds.Guild]) -> guilds If you don't have access to the guild. """ payload = await self._session.get_guild( - guild_id=str(guild.id if isinstance(guild, snowflakes.UniqueEntity) else int(guild)) + guild_id=str(guild.id if isinstance(guild, bases.UniqueEntity) else int(guild)) ) return guilds.Guild.deserialize(payload) - async def fetch_guild_preview(self, guild: snowflakes.HashableT[guilds.Guild]) -> guilds.GuildPreview: + async def fetch_guild_preview(self, guild: bases.Hashable[guilds.Guild]) -> guilds.GuildPreview: """Get a given guild's object. Parameters ---------- - guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] + guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.entities.Snowflake`, :obj:`~int` ] The object or ID of the guild to get the preview object for. Returns @@ -503,32 +499,32 @@ async def fetch_guild_preview(self, guild: snowflakes.HashableT[guilds.Guild]) - If the guild is not found or it isn't ``PUBLIC``. """ payload = await self._session.get_guild_preview( - guild_id=str(guild.id if isinstance(guild, snowflakes.UniqueEntity) else int(guild)) + guild_id=str(guild.id if isinstance(guild, bases.UniqueEntity) else int(guild)) ) return guilds.GuildPreview.deserialize(payload) async def update_guild( self, - guild: snowflakes.HashableT[guilds.Guild], + guild: bases.Hashable[guilds.Guild], *, name: str = ..., region: typing.Union[voices.VoiceRegion, str] = ..., verification_level: typing.Union[guilds.GuildVerificationLevel, int] = ..., default_message_notifications: typing.Union[guilds.GuildMessageNotificationsLevel, int] = ..., explicit_content_filter: typing.Union[guilds.GuildExplicitContentFilterLevel, int] = ..., - afk_channel: snowflakes.HashableT[_channels.GuildVoiceChannel] = ..., + afk_channel: bases.Hashable[_channels.GuildVoiceChannel] = ..., afk_timeout: typing.Union[datetime.timedelta, int] = ..., icon_data: conversions.FileLikeT = ..., - owner: snowflakes.HashableT[users.User] = ..., + owner: bases.Hashable[users.User] = ..., splash_data: conversions.FileLikeT = ..., - system_channel: snowflakes.HashableT[_channels.Channel] = ..., + system_channel: bases.Hashable[_channels.Channel] = ..., reason: str = ..., ) -> guilds.Guild: """Edit a given guild. Parameters ---------- - guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] + guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.entities.Snowflake`, :obj:`~int` ] The object or ID of the guild to be edited. name : :obj:`~str` If specified, the new name string for the guild (``2-100`` characters). @@ -545,17 +541,17 @@ async def update_guild( explicit_content_filter : :obj:`~typing.Union` [ :obj:`~hikari.guilds.GuildExplicitContentFilterLevel`, :obj:`~int` ] If specified, the new explicit content filter. Passing a raw int for this may lead to unexpected behaviour. - afk_channel : :obj:`~typing.Union` [ :obj:`~hikari.channels.GuildVoiceChannel`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] + afk_channel : :obj:`~typing.Union` [ :obj:`~hikari.channels.GuildVoiceChannel`, :obj:`~hikari.entities.Snowflake`, :obj:`~int` ] If specified, the object or ID for the new AFK voice channel. afk_timeout : :obj:`~typing.Union` [ :obj:`~datetime.timedelta`, :obj:`~int` ] If specified, the new AFK timeout seconds timedelta. icon_data : ``hikari.internal.conversions.FileLikeT`` If specified, the new guild icon image file data. - owner : :obj:`~typing.Union` [ :obj:`~hikari.users.User`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] + owner : :obj:`~typing.Union` [ :obj:`~hikari.users.User`, :obj:`~hikari.entities.Snowflake`, :obj:`~int` ] If specified, the object or ID of the new guild owner. splash_data : ``hikari.internal.conversions.FileLikeT`` If specified, the new new splash image file data. - system_channel : :obj:`~typing.Union` [ :obj:`~hikari.channels.GuildVoiceChannel`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] + system_channel : :obj:`~typing.Union` [ :obj:`~hikari.channels.GuildVoiceChannel`, :obj:`~hikari.entities.Snowflake`, :obj:`~int` ] If specified, the object or ID of the new system channel. reason : :obj:`~str` If specified, the audit log reason explaining why the operation @@ -577,7 +573,7 @@ async def update_guild( If you lack the ``MANAGE_GUILD`` permission or are not in the guild. """ payload = await self._session.modify_guild( - guild_id=str(guild.id if isinstance(guild, snowflakes.UniqueEntity) else int(guild)), + guild_id=str(guild.id if isinstance(guild, bases.UniqueEntity) else int(guild)), name=name, region=getattr(region, "id", region) if region is not ... else ..., verification_level=verification_level, @@ -585,17 +581,17 @@ async def update_guild( explicit_content_filter=explicit_content_filter, afk_timeout=afk_timeout.total_seconds() if isinstance(afk_timeout, datetime.timedelta) else afk_timeout, afk_channel_id=( - str(afk_channel.id if isinstance(afk_channel, snowflakes.UniqueEntity) else int(afk_channel)) + str(afk_channel.id if isinstance(afk_channel, bases.UniqueEntity) else int(afk_channel)) if afk_channel is not ... else ... ), icon=conversions.get_bytes_from_resource(icon_data) if icon_data is not ... else ..., owner_id=( - str(owner.id if isinstance(owner, snowflakes.UniqueEntity) else int(owner)) if owner is not ... else ... + str(owner.id if isinstance(owner, bases.UniqueEntity) else int(owner)) if owner is not ... else ... ), splash=conversions.get_bytes_from_resource(splash_data) if splash_data is not ... else ..., system_channel_id=( - str(system_channel.id if isinstance(system_channel, snowflakes.UniqueEntity) else int(system_channel)) + str(system_channel.id if isinstance(system_channel, bases.UniqueEntity) else int(system_channel)) if system_channel is not ... else ... ), @@ -603,14 +599,14 @@ async def update_guild( ) return guilds.Guild.deserialize(payload) - async def delete_guild(self, guild: snowflakes.HashableT[guilds.Guild]) -> None: + async def delete_guild(self, guild: bases.Hashable[guilds.Guild]) -> None: """Permanently deletes the given guild. You must be owner of the guild to perform this action. Parameters ---------- - guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] + guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.entities.Snowflake`, :obj:`~int` ] The object or ID of the guild to be deleted. Raises @@ -624,17 +620,17 @@ async def delete_guild(self, guild: snowflakes.HashableT[guilds.Guild]) -> None: If you are not the guild owner. """ await self._session.delete_guild( - guild_id=str(guild.id if isinstance(guild, snowflakes.UniqueEntity) else int(guild)) + guild_id=str(guild.id if isinstance(guild, bases.UniqueEntity) else int(guild)) ) async def fetch_guild_channels( - self, guild: snowflakes.HashableT[guilds.Guild] + self, guild: bases.Hashable[guilds.Guild] ) -> typing.Sequence[_channels.GuildChannel]: """Get all the channels for a given guild. Parameters ---------- - guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] + guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.entities.Snowflake`, :obj:`~int` ] The object or ID of the guild to get the channels from. Returns @@ -653,13 +649,13 @@ async def fetch_guild_channels( If you are not in the guild. """ payload = await self._session.list_guild_channels( - guild_id=str(guild.id if isinstance(guild, snowflakes.UniqueEntity) else int(guild)) + guild_id=str(guild.id if isinstance(guild, bases.UniqueEntity) else int(guild)) ) return [_channels.deserialize_channel(channel) for channel in payload] async def create_guild_channel( self, - guild: snowflakes.HashableT[guilds.Guild], + guild: bases.Hashable[guilds.Guild], name: str, channel_type: typing.Union[_channels.ChannelType, int] = ..., position: int = ..., @@ -669,14 +665,14 @@ async def create_guild_channel( bitrate: int = ..., user_limit: int = ..., permission_overwrites: typing.Sequence[_channels.PermissionOverwrite] = ..., - parent_category: snowflakes.HashableT[_channels.GuildCategory] = ..., + parent_category: bases.Hashable[_channels.GuildCategory] = ..., reason: str = ..., ) -> _channels.GuildChannel: """Create a channel in a given guild. Parameters ---------- - guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] + guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.entities.Snowflake`, :obj:`~int` ] The object or ID of the guild to create the channel in. name : :obj:`~str` If specified, the name for the channel. This must be @@ -710,7 +706,7 @@ async def create_guild_channel( permission_overwrites : :obj:`~typing.Sequence` [ :obj:`~hikari.channels.PermissionOverwrite` ] If specified, the list of permission overwrite objects that are category specific to replace the existing overwrites with. - parent_category : :obj:`~typing.Union` [ :obj:`~hikari.channels.GuildCategory`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] + parent_category : :obj:`~typing.Union` [ :obj:`~hikari.channels.GuildCategory`, :obj:`~hikari.entities.Snowflake`, :obj:`~int` ] If specified, the object or ID of the parent category to set for the channel. reason : :obj:`~str` @@ -736,7 +732,7 @@ async def create_guild_channel( due to it being outside of the range of a 64 bit integer. """ payload = await self._session.create_guild_channel( - guild_id=str(guild.id if isinstance(guild, snowflakes.UniqueEntity) else int(guild)), + guild_id=str(guild.id if isinstance(guild, bases.UniqueEntity) else int(guild)), name=name, type_=channel_type, position=position, @@ -753,9 +749,7 @@ async def create_guild_channel( [po.serialize() for po in permission_overwrites] if permission_overwrites is not ... else ... ), parent_id=( - str( - parent_category.id if isinstance(parent_category, snowflakes.UniqueEntity) else int(parent_category) - ) + str(parent_category.id if isinstance(parent_category, bases.UniqueEntity) else int(parent_category)) if parent_category is not ... else ... ), @@ -765,20 +759,20 @@ async def create_guild_channel( async def reposition_guild_channels( self, - guild: snowflakes.HashableT[guilds.Guild], - channel: typing.Tuple[int, snowflakes.HashableT[_channels.GuildChannel]], - *additional_channels: typing.Tuple[int, snowflakes.HashableT[_channels.GuildChannel]], + guild: bases.Hashable[guilds.Guild], + channel: typing.Tuple[int, bases.Hashable[_channels.GuildChannel]], + *additional_channels: typing.Tuple[int, bases.Hashable[_channels.GuildChannel]], ) -> None: """Edits the position of one or more given channels. Parameters ---------- - guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] + guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.entities.Snowflake`, :obj:`~int` ] The object or ID of the guild in which to edit the channels. - channel : :obj:`~typing.Tuple` [ :obj:`~int` , :obj:`~typing.Union` [ :obj:`~hikari.channels.GuildChannel`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] ] + channel : :obj:`~typing.Tuple` [ :obj:`~int` , :obj:`~typing.Union` [ :obj:`~hikari.channels.GuildChannel`, :obj:`~hikari.entities.Snowflake`, :obj:`~int` ] ] The first channel to change the position of. This is a tuple of the integer position the channel object or ID. - *additional_channels : :obj:`~typing.Tuple` [ :obj:`~int`, :obj:`~typing.Union` [ :obj:`~hikari.channels.GuildChannel`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] ] + *additional_channels : :obj:`~typing.Tuple` [ :obj:`~int`, :obj:`~typing.Union` [ :obj:`~hikari.channels.GuildChannel`, :obj:`~hikari.entities.Snowflake`, :obj:`~int` ] ] Optional additional channels to change the position of. These must be tuples of integer positions to change to and the channel object or ID and the. @@ -797,23 +791,23 @@ async def reposition_guild_channels( due to it being outside of the range of a 64 bit integer. """ await self._session.modify_guild_channel_positions( - str(guild.id if isinstance(guild, snowflakes.UniqueEntity) else int(guild)), + str(guild.id if isinstance(guild, bases.UniqueEntity) else int(guild)), *[ - (str(channel.id if isinstance(channel, snowflakes.UniqueEntity) else int(channel)), position) + (str(channel.id if isinstance(channel, bases.UniqueEntity) else int(channel)), position) for position, channel in [channel, *additional_channels] ], ) async def fetch_member( - self, guild: snowflakes.HashableT[guilds.Guild], user: snowflakes.HashableT[users.User], + self, guild: bases.Hashable[guilds.Guild], user: bases.Hashable[users.User], ) -> guilds.GuildMember: """Get a given guild member. Parameters ---------- - guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] + guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.entities.Snowflake`, :obj:`~int` ] The object or ID of the guild to get the member from. - user : :obj:`~typing.Union` [ :obj:`~hikari.users.User`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] + user : :obj:`~typing.Union` [ :obj:`~hikari.users.User`, :obj:`~hikari.entities.Snowflake`, :obj:`~int` ] The object or ID of the member to get. Returns @@ -832,16 +826,16 @@ async def fetch_member( If you don't have access to the target guild. """ payload = await self._session.get_guild_member( - guild_id=str(guild.id if isinstance(guild, snowflakes.UniqueEntity) else int(guild)), - user_id=str(user.id if isinstance(user, snowflakes.UniqueEntity) else int(user)), + guild_id=str(guild.id if isinstance(guild, bases.UniqueEntity) else int(guild)), + user_id=str(user.id if isinstance(user, bases.UniqueEntity) else int(user)), ) return guilds.GuildMember.deserialize(payload) def fetch_members_after( self, - guild: snowflakes.HashableT[guilds.Guild], + guild: bases.Hashable[guilds.Guild], *, - after: typing.Union[datetime.datetime, snowflakes.HashableT[users.User]] = 0, + after: typing.Union[datetime.datetime, bases.Hashable[users.User]] = 0, limit: typing.Optional[int] = None, ) -> typing.AsyncIterator[guilds.GuildMember]: """Get an async iterator of all the members in a given guild. @@ -852,12 +846,12 @@ def fetch_members_after( Parameters ---------- - guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] + guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.entities.Snowflake`, :obj:`~int` ] The object or ID of the guild to get the members from. limit : :obj:`~int` If specified, the maximum number of members this iterator should return. - after : :obj:`~typing.Union` [ :obj:`~datetime.datetime`, :obj:`~hikari.users.User`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] + after : :obj:`~typing.Union` [ :obj:`~datetime.datetime`, :obj:`~hikari.users.User`, :obj:`~hikari.entities.Snowflake`, :obj:`~int` ] The object or ID of the user this iterator should start after if specified, else this will start at the oldest user. @@ -885,11 +879,11 @@ def fetch_members_after( If you are not in the guild. """ if isinstance(after, datetime.datetime): - after = str(snowflakes.Snowflake.from_datetime(after)) + after = str(bases.Snowflake.from_datetime(after)) else: - after = str(after.id if isinstance(after, snowflakes.UniqueEntity) else int(after)) + after = str(after.id if isinstance(after, bases.UniqueEntity) else int(after)) return pagination.pagination_handler( - guild_id=str(guild.id if isinstance(guild, snowflakes.UniqueEntity) else int(guild)), + guild_id=str(guild.id if isinstance(guild, bases.UniqueEntity) else int(guild)), deserializer=guilds.GuildMember.deserialize, direction="after", request=self._session.list_guild_members, @@ -901,27 +895,27 @@ def fetch_members_after( async def update_member( self, - guild: snowflakes.HashableT[guilds.Guild], - user: snowflakes.HashableT[users.User], + guild: bases.Hashable[guilds.Guild], + user: bases.Hashable[users.User], nickname: typing.Optional[str] = ..., - roles: typing.Sequence[snowflakes.HashableT[guilds.GuildRole]] = ..., + roles: typing.Sequence[bases.Hashable[guilds.GuildRole]] = ..., mute: bool = ..., deaf: bool = ..., - voice_channel: typing.Optional[snowflakes.HashableT[_channels.GuildVoiceChannel]] = ..., + voice_channel: typing.Optional[bases.Hashable[_channels.GuildVoiceChannel]] = ..., reason: str = ..., ) -> None: """Edits a guild's member, any unspecified fields will not be changed. Parameters ---------- - guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] + guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.entities.Snowflake`, :obj:`~int` ] The object or ID of the guild to edit the member from. - user : :obj:`~typing.Union` [ :obj:`~hikari.guilds.GuildMember`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] + user : :obj:`~typing.Union` [ :obj:`~hikari.guilds.GuildMember`, :obj:`~hikari.entities.Snowflake`, :obj:`~int` ] The object or ID of the member to edit. nickname : :obj:`~str`, optional If specified, the new nickname string. Setting it to :obj:`~None` explicitly will clear the nickname. - roles : :obj:`~typing.Sequence` [ :obj:`~typing.Union` [ :obj:`~hikari.guilds.GuildRole`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] ] + roles : :obj:`~typing.Sequence` [ :obj:`~typing.Union` [ :obj:`~hikari.guilds.GuildRole`, :obj:`~hikari.entities.Snowflake`, :obj:`~int` ] ] If specified, a list of role IDs the member should have. mute : :obj:`~bool` If specified, whether the user should be muted in the voice channel @@ -929,7 +923,7 @@ async def update_member( deaf : :obj:`~bool` If specified, whether the user should be deafen in the voice channel or not. - voice_channel : :obj:`~typing.Union` [ :obj:`~hikari.channels.GuildVoiceChannel`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ], optional + voice_channel : :obj:`~typing.Union` [ :obj:`~hikari.channels.GuildVoiceChannel`, :obj:`~hikari.entities.Snowflake`, :obj:`~int` ], optional If specified, the ID of the channel to move the member to. Setting it to :obj:`~None` explicitly will disconnect the user. reason : :obj:`~str` @@ -953,18 +947,18 @@ async def update_member( due to it being outside of the range of a 64 bit integer. """ await self._session.modify_guild_member( - guild_id=str(guild.id if isinstance(guild, snowflakes.UniqueEntity) else int(guild)), - user_id=str(user.id if isinstance(user, snowflakes.UniqueEntity) else int(user)), + guild_id=str(guild.id if isinstance(guild, bases.UniqueEntity) else int(guild)), + user_id=str(user.id if isinstance(user, bases.UniqueEntity) else int(user)), nick=nickname, roles=( - [str(role.id if isinstance(role, snowflakes.UniqueEntity) else int(role)) for role in roles] + [str(role.id if isinstance(role, bases.UniqueEntity) else int(role)) for role in roles] if roles is not ... else ... ), mute=mute, deaf=deaf, channel_id=( - str(voice_channel.id if isinstance(voice_channel, snowflakes.UniqueEntity) else int(voice_channel)) + str(voice_channel.id if isinstance(voice_channel, bases.UniqueEntity) else int(voice_channel)) if voice_channel is not ... else ... ), @@ -972,13 +966,13 @@ async def update_member( ) async def update_my_member_nickname( - self, guild: snowflakes.HashableT[guilds.Guild], nickname: typing.Optional[str], *, reason: str = ..., + self, guild: bases.Hashable[guilds.Guild], nickname: typing.Optional[str], *, reason: str = ..., ) -> None: """Edits the current user's nickname for a given guild. Parameters ---------- - guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] + guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.entities.Snowflake`, :obj:`~int` ] The object or ID of the guild you want to change the nick on. nickname : :obj:`~str`, optional The new nick string. Setting this to `None` clears the nickname. @@ -1000,16 +994,16 @@ async def update_my_member_nickname( due to it being outside of the range of a 64 bit integer. """ await self._session.modify_current_user_nick( - guild_id=str(guild.id if isinstance(guild, snowflakes.UniqueEntity) else int(guild)), + guild_id=str(guild.id if isinstance(guild, bases.UniqueEntity) else int(guild)), nick=nickname, reason=reason, ) async def add_role_to_member( self, - guild: snowflakes.HashableT[guilds.Guild], - user: snowflakes.HashableT[users.User], - role: snowflakes.HashableT[guilds.GuildRole], + guild: bases.Hashable[guilds.Guild], + user: bases.Hashable[users.User], + role: bases.Hashable[guilds.GuildRole], *, reason: str = ..., ) -> None: @@ -1017,11 +1011,11 @@ async def add_role_to_member( Parameters ---------- - guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] + guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.entities.Snowflake`, :obj:`~int` ] The object or ID of the guild the member belongs to. - user : :obj:`~typing.Union` [ :obj:`~hikari.users.User`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] + user : :obj:`~typing.Union` [ :obj:`~hikari.users.User`, :obj:`~hikari.entities.Snowflake`, :obj:`~int` ] The object or ID of the member you want to add the role to. - role : :obj:`~typing.Union` [ :obj:`~hikari.guilds.GuildRole`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] + role : :obj:`~typing.Union` [ :obj:`~hikari.guilds.GuildRole`, :obj:`~hikari.entities.Snowflake`, :obj:`~int` ] The object or ID of the role you want to add. reason : :obj:`~str` If specified, the audit log reason explaining why the operation @@ -1038,17 +1032,17 @@ async def add_role_to_member( If you lack the ``MANAGE_ROLES`` permission or are not in the guild. """ await self._session.add_guild_member_role( - guild_id=str(guild.id if isinstance(guild, snowflakes.UniqueEntity) else int(guild)), - user_id=str(user.id if isinstance(user, snowflakes.UniqueEntity) else int(user)), - role_id=str(role.id if isinstance(role, snowflakes.UniqueEntity) else int(role)), + guild_id=str(guild.id if isinstance(guild, bases.UniqueEntity) else int(guild)), + user_id=str(user.id if isinstance(user, bases.UniqueEntity) else int(user)), + role_id=str(role.id if isinstance(role, bases.UniqueEntity) else int(role)), reason=reason, ) async def remove_role_from_member( self, - guild: snowflakes.HashableT[guilds.Guild], - user: snowflakes.HashableT[users.User], - role: snowflakes.HashableT[guilds.GuildRole], + guild: bases.Hashable[guilds.Guild], + user: bases.Hashable[users.User], + role: bases.Hashable[guilds.GuildRole], *, reason: str = ..., ) -> None: @@ -1056,11 +1050,11 @@ async def remove_role_from_member( Parameters ---------- - guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] + guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.entities.Snowflake`, :obj:`~int` ] The object or ID of the guild the member belongs to. - user : :obj:`~typing.Union` [ :obj:`~hikari.users.User`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] + user : :obj:`~typing.Union` [ :obj:`~hikari.users.User`, :obj:`~hikari.entities.Snowflake`, :obj:`~int` ] The object or ID of the member you want to remove the role from. - role : :obj:`~typing.Union` [ :obj:`~hikari.guilds.GuildRole`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] + role : :obj:`~typing.Union` [ :obj:`~hikari.guilds.GuildRole`, :obj:`~hikari.entities.Snowflake`, :obj:`~int` ] The object or ID of the role you want to remove. reason : :obj:`~str` If specified, the audit log reason explaining why the operation @@ -1077,22 +1071,22 @@ async def remove_role_from_member( If you lack the ``MANAGE_ROLES`` permission or are not in the guild. """ await self._session.remove_guild_member_role( - guild_id=str(guild.id if isinstance(guild, snowflakes.UniqueEntity) else int(guild)), - user_id=str(user.id if isinstance(user, snowflakes.UniqueEntity) else int(user)), - role_id=str(role.id if isinstance(role, snowflakes.UniqueEntity) else int(role)), + guild_id=str(guild.id if isinstance(guild, bases.UniqueEntity) else int(guild)), + user_id=str(user.id if isinstance(user, bases.UniqueEntity) else int(user)), + role_id=str(role.id if isinstance(role, bases.UniqueEntity) else int(role)), reason=reason, ) async def kick_member( - self, guild: snowflakes.HashableT[guilds.Guild], user: snowflakes.HashableT[users.User], *, reason: str = ..., + self, guild: bases.Hashable[guilds.Guild], user: bases.Hashable[users.User], *, reason: str = ..., ) -> None: """Kicks a user from a given guild. Parameters ---------- - guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] + guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.entities.Snowflake`, :obj:`~int` ] The object or ID of the guild the member belongs to. - user : :obj:`~typing.Union` [ :obj:`~hikari.users.User`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] + user : :obj:`~typing.Union` [ :obj:`~hikari.users.User`, :obj:`~hikari.entities.Snowflake`, :obj:`~int` ] The object or ID of the member you want to kick. reason : :obj:`~str` If specified, the audit log reason explaining why the operation @@ -1109,21 +1103,21 @@ async def kick_member( If you lack the ``KICK_MEMBERS`` permission or are not in the guild. """ await self._session.remove_guild_member( - guild_id=str(guild.id if isinstance(guild, snowflakes.UniqueEntity) else int(guild)), - user_id=str(user.id if isinstance(user, snowflakes.UniqueEntity) else int(user)), + guild_id=str(guild.id if isinstance(guild, bases.UniqueEntity) else int(guild)), + user_id=str(user.id if isinstance(user, bases.UniqueEntity) else int(user)), reason=reason, ) async def fetch_ban( - self, guild: snowflakes.HashableT[guilds.Guild], user: snowflakes.HashableT[users.User], + self, guild: bases.Hashable[guilds.Guild], user: bases.Hashable[users.User], ) -> guilds.GuildMemberBan: """Get a ban from a given guild. Parameters ---------- - guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] + guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.entities.Snowflake`, :obj:`~int` ] The object or ID of the guild you want to get the ban from. - user : :obj:`~typing.Union` [ :obj:`~hikari.users.User`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] + user : :obj:`~typing.Union` [ :obj:`~hikari.users.User`, :obj:`~hikari.entities.Snowflake`, :obj:`~int` ] The object or ID of the user to get the ban information for. Returns @@ -1143,17 +1137,17 @@ async def fetch_ban( If you lack the ``BAN_MEMBERS`` permission or are not in the guild. """ payload = await self._session.get_guild_ban( - guild_id=str(guild.id if isinstance(guild, snowflakes.UniqueEntity) else int(guild)), - user_id=str(user.id if isinstance(user, snowflakes.UniqueEntity) else int(user)), + guild_id=str(guild.id if isinstance(guild, bases.UniqueEntity) else int(guild)), + user_id=str(user.id if isinstance(user, bases.UniqueEntity) else int(user)), ) return guilds.GuildMemberBan.deserialize(payload) - async def fetch_bans(self, guild: snowflakes.HashableT[guilds.Guild],) -> typing.Sequence[guilds.GuildMemberBan]: + async def fetch_bans(self, guild: bases.Hashable[guilds.Guild],) -> typing.Sequence[guilds.GuildMemberBan]: """Get the bans for a given guild. Parameters ---------- - guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] + guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.entities.Snowflake`, :obj:`~int` ] The object or ID of the guild you want to get the bans from. Returns @@ -1172,14 +1166,14 @@ async def fetch_bans(self, guild: snowflakes.HashableT[guilds.Guild],) -> typing If you lack the ``BAN_MEMBERS`` permission or are not in the guild. """ payload = await self._session.get_guild_bans( - guild_id=str(guild.id if isinstance(guild, snowflakes.UniqueEntity) else int(guild)) + guild_id=str(guild.id if isinstance(guild, bases.UniqueEntity) else int(guild)) ) return [guilds.GuildMemberBan.deserialize(ban) for ban in payload] async def ban_member( self, - guild: snowflakes.HashableT[guilds.Guild], - user: snowflakes.HashableT[users.User], + guild: bases.Hashable[guilds.Guild], + user: bases.Hashable[users.User], *, delete_message_days: typing.Union[datetime.timedelta, int] = ..., reason: str = ..., @@ -1188,9 +1182,9 @@ async def ban_member( Parameters ---------- - guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] + guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.entities.Snowflake`, :obj:`~int` ] The object or ID of the guild the member belongs to. - user : :obj:`~typing.Union` [ :obj:`~hikari.users.User`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] + user : :obj:`~typing.Union` [ :obj:`~hikari.users.User`, :obj:`~hikari.entities.Snowflake`, :obj:`~int` ] The object or ID of the member you want to ban. delete_message_days : :obj:`~typing.Union` [ :obj:`~datetime.timedelta`, :obj:`~int` ] If specified, the tim delta of how many days of messages from the @@ -1210,22 +1204,22 @@ async def ban_member( If you lack the ``BAN_MEMBERS`` permission or are not in the guild. """ await self._session.create_guild_ban( - guild_id=str(guild.id if isinstance(guild, snowflakes.UniqueEntity) else int(guild)), - user_id=str(user.id if isinstance(user, snowflakes.UniqueEntity) else int(user)), + guild_id=str(guild.id if isinstance(guild, bases.UniqueEntity) else int(guild)), + user_id=str(user.id if isinstance(user, bases.UniqueEntity) else int(user)), delete_message_days=getattr(delete_message_days, "days", delete_message_days), reason=reason, ) async def unban_member( - self, guild: snowflakes.HashableT[guilds.Guild], user: snowflakes.HashableT[users.User], *, reason: str = ..., + self, guild: bases.Hashable[guilds.Guild], user: bases.Hashable[users.User], *, reason: str = ..., ) -> None: """Un-bans a user from a given guild. Parameters ---------- - guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] + guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.entities.Snowflake`, :obj:`~int` ] The object or ID of the guild to un-ban the user from. - user : :obj:`~typing.Union` [ :obj:`~hikari.users.User`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] + user : :obj:`~typing.Union` [ :obj:`~hikari.users.User`, :obj:`~hikari.entities.Snowflake`, :obj:`~int` ] The ID of the user you want to un-ban. reason : :obj:`~str` If specified, the audit log reason explaining why the operation @@ -1244,24 +1238,24 @@ async def unban_member( guild. """ await self._session.remove_guild_ban( - guild_id=str(guild.id if isinstance(guild, snowflakes.UniqueEntity) else int(guild)), - user_id=str(user.id if isinstance(user, snowflakes.UniqueEntity) else int(user)), + guild_id=str(guild.id if isinstance(guild, bases.UniqueEntity) else int(guild)), + user_id=str(user.id if isinstance(user, bases.UniqueEntity) else int(user)), reason=reason, ) async def fetch_roles( - self, guild: snowflakes.HashableT[guilds.Guild], - ) -> typing.Mapping[snowflakes.Snowflake, guilds.GuildRole]: + self, guild: bases.Hashable[guilds.Guild], + ) -> typing.Mapping[bases.Snowflake, guilds.GuildRole]: """Get the roles for a given guild. Parameters ---------- - guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] + guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.entities.Snowflake`, :obj:`~int` ] The object or ID of the guild you want to get the roles from. Returns ------- - :obj:`~typing.Mapping` [ :obj:`~hikari.snowflakes.Snowflake`, :obj:`~hikari.guilds.GuildRole` ] + :obj:`~typing.Mapping` [ :obj:`~hikari.entities.Snowflake`, :obj:`~hikari.guilds.GuildRole` ] A list of role objects. Raises @@ -1275,13 +1269,13 @@ async def fetch_roles( If you're not in the guild. """ payload = await self._session.get_guild_roles( - guild_id=str(guild.id if isinstance(guild, snowflakes.UniqueEntity) else int(guild)) + guild_id=str(guild.id if isinstance(guild, bases.UniqueEntity) else int(guild)) ) return {role.id: role for role in map(guilds.GuildRole.deserialize, payload)} async def create_role( self, - guild: snowflakes.HashableT[guilds.Guild], + guild: bases.Hashable[guilds.Guild], *, name: str = ..., permissions: typing.Union[_permissions.Permission, int] = ..., @@ -1294,7 +1288,7 @@ async def create_role( Parameters ---------- - guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] + guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.entities.Snowflake`, :obj:`~int` ] The object or ID of the guild you want to create the role on. name : :obj:`~str` If specified, the new role name string. @@ -1330,7 +1324,7 @@ async def create_role( due to it being outside of the range of a 64 bit integer. """ payload = await self._session.create_guild_role( - guild_id=str(guild.id if isinstance(guild, snowflakes.UniqueEntity) else int(guild)), + guild_id=str(guild.id if isinstance(guild, bases.UniqueEntity) else int(guild)), name=name, permissions=permissions, color=color, @@ -1342,20 +1336,20 @@ async def create_role( async def reposition_roles( self, - guild: snowflakes.HashableT[guilds.Guild], - role: typing.Tuple[int, snowflakes.HashableT[guilds.GuildRole]], - *additional_roles: typing.Tuple[int, snowflakes.HashableT[guilds.GuildRole]], + guild: bases.Hashable[guilds.Guild], + role: typing.Tuple[int, bases.Hashable[guilds.GuildRole]], + *additional_roles: typing.Tuple[int, bases.Hashable[guilds.GuildRole]], ) -> typing.Sequence[guilds.GuildRole]: """Edits the position of two or more roles in a given guild. Parameters ---------- - guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] + guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.entities.Snowflake`, :obj:`~int` ] The ID of the guild the roles belong to. - role : :obj:`~typing.Tuple` [ :obj:`~int`, :obj:`~typing.Union` [ :obj:`~hikari.guilds.GuildRole`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] ] + role : :obj:`~typing.Tuple` [ :obj:`~int`, :obj:`~typing.Union` [ :obj:`~hikari.guilds.GuildRole`, :obj:`~hikari.entities.Snowflake`, :obj:`~int` ] ] The first role to move. This is a tuple of the integer position and the role object or ID. - *additional_roles : :obj:`~typing.Tuple` [ :obj:`~int`, :obj:`~typing.Union` [ :obj:`~hikari.guilds.GuildRole`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] ] + *additional_roles : :obj:`~typing.Tuple` [ :obj:`~int`, :obj:`~typing.Union` [ :obj:`~hikari.guilds.GuildRole`, :obj:`~hikari.entities.Snowflake`, :obj:`~int` ] ] Optional extra roles to move. These must be tuples of the integer position and the role object or ID. @@ -1377,9 +1371,9 @@ async def reposition_roles( due to it being outside of the range of a 64 bit integer. """ payload = await self._session.modify_guild_role_positions( - str(guild.id if isinstance(guild, snowflakes.UniqueEntity) else int(guild)), + str(guild.id if isinstance(guild, bases.UniqueEntity) else int(guild)), *[ - (str(channel.id if isinstance(channel, snowflakes.UniqueEntity) else int(channel)), position) + (str(channel.id if isinstance(channel, bases.UniqueEntity) else int(channel)), position) for position, channel in [role, *additional_roles] ], ) @@ -1387,8 +1381,8 @@ async def reposition_roles( async def update_role( self, - guild: snowflakes.HashableT[guilds.Guild], - role: snowflakes.HashableT[guilds.GuildRole], + guild: bases.Hashable[guilds.Guild], + role: bases.Hashable[guilds.GuildRole], *, name: str = ..., permissions: typing.Union[_permissions.Permission, int] = ..., @@ -1401,9 +1395,9 @@ async def update_role( Parameters ---------- - guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] + guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.entities.Snowflake`, :obj:`~int` ] The object or ID of the guild the role belong to. - role : :obj:`~typing.Union` [ :obj:`~hikari.guilds.GuildRole`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] + role : :obj:`~typing.Union` [ :obj:`~hikari.guilds.GuildRole`, :obj:`~hikari.entities.Snowflake`, :obj:`~int` ] The object or ID of the role you want to edit. name : :obj:`~str` If specified, the new role's name string. @@ -1439,8 +1433,8 @@ async def update_role( due to it being outside of the range of a 64 bit integer. """ payload = await self._session.modify_guild_role( - guild_id=str(guild.id if isinstance(guild, snowflakes.UniqueEntity) else int(guild)), - role_id=str(role.id if isinstance(role, snowflakes.UniqueEntity) else int(role)), + guild_id=str(guild.id if isinstance(guild, bases.UniqueEntity) else int(guild)), + role_id=str(role.id if isinstance(role, bases.UniqueEntity) else int(role)), name=name, permissions=permissions, color=color, @@ -1450,16 +1444,14 @@ async def update_role( ) return guilds.GuildRole.deserialize(payload) - async def delete_role( - self, guild: snowflakes.HashableT[guilds.Guild], role: snowflakes.HashableT[guilds.GuildRole], - ) -> None: + async def delete_role(self, guild: bases.Hashable[guilds.Guild], role: bases.Hashable[guilds.GuildRole],) -> None: """Delete a role from a given guild. Parameters ---------- - guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] + guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.entities.Snowflake`, :obj:`~int` ] The object or ID of the guild you want to remove the role from. - role : :obj:`~typing.Union` [ :obj:`~hikari.guilds.GuildRole`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] + role : :obj:`~typing.Union` [ :obj:`~hikari.guilds.GuildRole`, :obj:`~hikari.entities.Snowflake`, :obj:`~int` ] The object or ID of the role you want to delete. Raises @@ -1473,18 +1465,18 @@ async def delete_role( If you lack the ``MANAGE_ROLES`` permission or are not in the guild. """ await self._session.delete_guild_role( - guild_id=str(guild.id if isinstance(guild, snowflakes.UniqueEntity) else int(guild)), - role_id=str(role.id if isinstance(role, snowflakes.UniqueEntity) else int(role)), + guild_id=str(guild.id if isinstance(guild, bases.UniqueEntity) else int(guild)), + role_id=str(role.id if isinstance(role, bases.UniqueEntity) else int(role)), ) async def estimate_guild_prune_count( - self, guild: snowflakes.HashableT[guilds.Guild], days: typing.Union[datetime.timedelta, int], + self, guild: bases.Hashable[guilds.Guild], days: typing.Union[datetime.timedelta, int], ) -> int: """Get the estimated prune count for a given guild. Parameters ---------- - guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] + guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.entities.Snowflake`, :obj:`~int` ] The object or ID of the guild you want to get the count for. days : :obj:`~typing.Union` [ :obj:`~datetime.timedelta`, :obj:`~int` ] The time delta of days to count prune for (at least ``1``). @@ -1506,13 +1498,13 @@ async def estimate_guild_prune_count( due to it being outside of the range of a 64 bit integer. """ return await self._session.get_guild_prune_count( - guild_id=str(guild.id if isinstance(guild, snowflakes.UniqueEntity) else int(guild)), + guild_id=str(guild.id if isinstance(guild, bases.UniqueEntity) else int(guild)), days=getattr(days, "days", days), ) async def begin_guild_prune( self, - guild: snowflakes.HashableT[guilds.Guild], + guild: bases.Hashable[guilds.Guild], days: typing.Union[datetime.timedelta, int], *, compute_prune_count: bool = ..., @@ -1522,7 +1514,7 @@ async def begin_guild_prune( Parameters ---------- - guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] + guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.entities.Snowflake`, :obj:`~int` ] The object or ID of the guild you want to prune member of. days : :obj:`~typing.Union` [ :obj:`~datetime.timedelta`, :obj:`~int` ] The time delta of inactivity days you want to use as filter. @@ -1552,20 +1544,20 @@ async def begin_guild_prune( due to it being outside of the range of a 64 bit integer. """ return await self._session.begin_guild_prune( - guild_id=str(guild.id if isinstance(guild, snowflakes.UniqueEntity) else int(guild)), + guild_id=str(guild.id if isinstance(guild, bases.UniqueEntity) else int(guild)), days=getattr(days, "days", days), compute_prune_count=compute_prune_count, reason=reason, ) async def fetch_guild_voice_regions( - self, guild: snowflakes.HashableT[guilds.Guild], + self, guild: bases.Hashable[guilds.Guild], ) -> typing.Sequence[voices.VoiceRegion]: """Get the voice regions for a given guild. Parameters ---------- - guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] + guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.entities.Snowflake`, :obj:`~int` ] The object or ID of the guild to get the voice regions for. Returns @@ -1584,18 +1576,18 @@ async def fetch_guild_voice_regions( If you are not in the guild. """ payload = await self._session.get_guild_voice_regions( - guild_id=str(guild.id if isinstance(guild, snowflakes.UniqueEntity) else int(guild)) + guild_id=str(guild.id if isinstance(guild, bases.UniqueEntity) else int(guild)) ) return [voices.VoiceRegion.deserialize(region) for region in payload] async def fetch_guild_invites( - self, guild: snowflakes.HashableT[guilds.Guild], + self, guild: bases.Hashable[guilds.Guild], ) -> typing.Sequence[invites.InviteWithMetadata]: """Get the invites for a given guild. Parameters ---------- - guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] + guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.entities.Snowflake`, :obj:`~int` ] The object or ID of the guild to get the invites for. Returns @@ -1614,18 +1606,16 @@ async def fetch_guild_invites( If you lack the ``MANAGE_GUILD`` permission or are not in the guild. """ payload = await self._session.get_guild_invites( - guild_id=str(guild.id if isinstance(guild, snowflakes.UniqueEntity) else int(guild)) + guild_id=str(guild.id if isinstance(guild, bases.UniqueEntity) else int(guild)) ) return [invites.InviteWithMetadata.deserialize(invite) for invite in payload] - async def fetch_integrations( - self, guild: snowflakes.HashableT[guilds.Guild] - ) -> typing.Sequence[guilds.GuildIntegration]: + async def fetch_integrations(self, guild: bases.Hashable[guilds.Guild]) -> typing.Sequence[guilds.GuildIntegration]: """Get the integrations for a given guild. Parameters ---------- - guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] + guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.entities.Snowflake`, :obj:`~int` ] The object or ID of the guild to get the integrations for. Returns @@ -1644,14 +1634,14 @@ async def fetch_integrations( If you lack the ``MANAGE_GUILD`` permission or are not in the guild. """ payload = await self._session.get_guild_integrations( - guild_id=str(guild.id if isinstance(guild, snowflakes.UniqueEntity) else int(guild)) + guild_id=str(guild.id if isinstance(guild, bases.UniqueEntity) else int(guild)) ) return [guilds.GuildIntegration.deserialize(integration) for integration in payload] async def update_integration( self, - guild: snowflakes.HashableT[guilds.Guild], - integration: snowflakes.HashableT[guilds.GuildIntegration], + guild: bases.Hashable[guilds.Guild], + integration: bases.Hashable[guilds.GuildIntegration], *, expire_behaviour: typing.Union[guilds.IntegrationExpireBehaviour, int] = ..., expire_grace_period: typing.Union[datetime.timedelta, int] = ..., @@ -1662,9 +1652,9 @@ async def update_integration( Parameters ---------- - guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] + guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.entities.Snowflake`, :obj:`~int` ] The object or ID of the guild to which the integration belongs to. - integration : :obj:`~typing.Union` [ :obj:`~hikari.guilds.GuildIntegration`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] + integration : :obj:`~typing.Union` [ :obj:`~hikari.guilds.GuildIntegration`, :obj:`~hikari.entities.Snowflake`, :obj:`~int` ] The object or ID of the integration to update. expire_behaviour : :obj:`~typing.Union` [ :obj:`~hikari.guilds.IntegrationExpireBehaviour`, :obj:`~int` ] If specified, the behaviour for when an integration subscription @@ -1690,10 +1680,8 @@ async def update_integration( If you lack the ``MANAGE_GUILD`` permission or are not in the guild. """ await self._session.modify_guild_integration( - guild_id=str(guild.id if isinstance(guild, snowflakes.UniqueEntity) else int(guild)), - integration_id=str( - integration.id if isinstance(integration, snowflakes.UniqueEntity) else int(integration) - ), + guild_id=str(guild.id if isinstance(guild, bases.UniqueEntity) else int(guild)), + integration_id=str(integration.id if isinstance(integration, bases.UniqueEntity) else int(integration)), expire_behaviour=expire_behaviour, expire_grace_period=getattr(expire_grace_period, "days", expire_grace_period), enable_emojis=enable_emojis, @@ -1702,8 +1690,8 @@ async def update_integration( async def delete_integration( self, - guild: snowflakes.HashableT[guilds.Guild], - integration: snowflakes.HashableT[guilds.GuildIntegration], + guild: bases.Hashable[guilds.Guild], + integration: bases.Hashable[guilds.GuildIntegration], *, reason: str = ..., ) -> None: @@ -1711,9 +1699,9 @@ async def delete_integration( Parameters ---------- - guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] + guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.entities.Snowflake`, :obj:`~int` ] The object or ID of the guild to which the integration belongs to. - integration : :obj:`~typing.Union` [ :obj:`~hikari.guilds.GuildIntegration`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] + integration : :obj:`~typing.Union` [ :obj:`~hikari.guilds.GuildIntegration`, :obj:`~hikari.entities.Snowflake`, :obj:`~int` ] The object or ID of the integration to delete. reason : :obj:`~str` If specified, the audit log reason explaining why the operation @@ -1730,23 +1718,21 @@ async def delete_integration( If you lack the `MANAGE_GUILD` permission or are not in the guild. """ await self._session.delete_guild_integration( - guild_id=str(guild.id if isinstance(guild, snowflakes.UniqueEntity) else int(guild)), - integration_id=str( - integration.id if isinstance(integration, snowflakes.UniqueEntity) else int(integration) - ), + guild_id=str(guild.id if isinstance(guild, bases.UniqueEntity) else int(guild)), + integration_id=str(integration.id if isinstance(integration, bases.UniqueEntity) else int(integration)), reason=reason, ) async def sync_guild_integration( - self, guild: snowflakes.HashableT[guilds.Guild], integration: snowflakes.HashableT[guilds.GuildIntegration], + self, guild: bases.Hashable[guilds.Guild], integration: bases.Hashable[guilds.GuildIntegration], ) -> None: """Sync the given integration's subscribers/emojis. Parameters ---------- - guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] + guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.entities.Snowflake`, :obj:`~int` ] The object or ID of the guild to which the integration belongs to. - integration : :obj:`~typing.Union` [ :obj:`~hikari.guilds.GuildIntegration`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] + integration : :obj:`~typing.Union` [ :obj:`~hikari.guilds.GuildIntegration`, :obj:`~hikari.entities.Snowflake`, :obj:`~int` ] The ID of the integration to sync. Raises @@ -1760,18 +1746,16 @@ async def sync_guild_integration( If you lack the ``MANAGE_GUILD`` permission or are not in the guild. """ await self._session.sync_guild_integration( - guild_id=str(guild.id if isinstance(guild, snowflakes.UniqueEntity) else int(guild)), - integration_id=str( - integration.id if isinstance(integration, snowflakes.UniqueEntity) else int(integration) - ), + guild_id=str(guild.id if isinstance(guild, bases.UniqueEntity) else int(guild)), + integration_id=str(integration.id if isinstance(integration, bases.UniqueEntity) else int(integration)), ) - async def fetch_guild_embed(self, guild: snowflakes.HashableT[guilds.Guild],) -> guilds.GuildEmbed: + async def fetch_guild_embed(self, guild: bases.Hashable[guilds.Guild],) -> guilds.GuildEmbed: """Get the embed for a given guild. Parameters ---------- - guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] + guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.entities.Snowflake`, :obj:`~int` ] The object or ID of the guild to get the embed for. Returns @@ -1791,15 +1775,15 @@ async def fetch_guild_embed(self, guild: snowflakes.HashableT[guilds.Guild],) -> the guild. """ payload = await self._session.get_guild_embed( - guild_id=str(guild.id if isinstance(guild, snowflakes.UniqueEntity) else int(guild)) + guild_id=str(guild.id if isinstance(guild, bases.UniqueEntity) else int(guild)) ) return guilds.GuildEmbed.deserialize(payload) async def update_guild_embed( self, - guild: snowflakes.HashableT[guilds.Guild], + guild: bases.Hashable[guilds.Guild], *, - channel: snowflakes.HashableT[_channels.GuildChannel] = ..., + channel: bases.Hashable[_channels.GuildChannel] = ..., enabled: bool = ..., reason: str = ..., ) -> guilds.GuildEmbed: @@ -1807,9 +1791,9 @@ async def update_guild_embed( Parameters ---------- - guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] + guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.entities.Snowflake`, :obj:`~int` ] The object or ID of the guild to edit the embed for. - channel : :obj:`~typing.Union` [ :obj:`~hikari.channels.GuildChannel`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ], optional + channel : :obj:`~typing.Union` [ :obj:`~hikari.channels.GuildChannel`, :obj:`~hikari.entities.Snowflake`, :obj:`~int` ], optional If specified, the object or ID of the channel that this embed's invite should target. Set to :obj:`~None` to disable invites for this embed. @@ -1836,9 +1820,9 @@ async def update_guild_embed( the guild. """ payload = await self._session.modify_guild_embed( - guild_id=str(guild.id if isinstance(guild, snowflakes.UniqueEntity) else int(guild)), + guild_id=str(guild.id if isinstance(guild, bases.UniqueEntity) else int(guild)), channel_id=( - str(channel.id if isinstance(channel, snowflakes.UniqueEntity) else int(channel)) + str(channel.id if isinstance(channel, bases.UniqueEntity) else int(channel)) if channel is not ... else ... ), @@ -1847,13 +1831,13 @@ async def update_guild_embed( ) return guilds.GuildEmbed.deserialize(payload) - async def fetch_guild_vanity_url(self, guild: snowflakes.HashableT[guilds.Guild],) -> invites.VanityUrl: + async def fetch_guild_vanity_url(self, guild: bases.Hashable[guilds.Guild],) -> invites.VanityUrl: """ Get the vanity URL for a given guild. Parameters ---------- - guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] + guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.entities.Snowflake`, :obj:`~int` ] The object or ID of the guild to get the vanity URL for. Returns @@ -1874,16 +1858,16 @@ async def fetch_guild_vanity_url(self, guild: snowflakes.HashableT[guilds.Guild] the guild. """ payload = await self._session.get_guild_vanity_url( - guild_id=str(guild.id if isinstance(guild, snowflakes.UniqueEntity) else int(guild)) + guild_id=str(guild.id if isinstance(guild, bases.UniqueEntity) else int(guild)) ) return invites.VanityUrl.deserialize(payload) - def format_guild_widget_image(self, guild: snowflakes.HashableT[guilds.Guild], *, style: str = ...) -> str: + def format_guild_widget_image(self, guild: bases.Hashable[guilds.Guild], *, style: str = ...) -> str: """Get the URL for a guild widget. Parameters ---------- - guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] + guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.entities.Snowflake`, :obj:`~int` ] The object or ID of the guild to form the widget. style : :obj:`~str` If specified, the syle of the widget. @@ -1904,17 +1888,15 @@ def format_guild_widget_image(self, guild: snowflakes.HashableT[guilds.Guild], * to be valid. """ return self._session.get_guild_widget_image_url( - guild_id=str(guild.id if isinstance(guild, snowflakes.UniqueEntity) else int(guild)), style=style + guild_id=str(guild.id if isinstance(guild, bases.UniqueEntity) else int(guild)), style=style ) - async def fetch_guild_webhooks( - self, guild: snowflakes.HashableT[guilds.Guild] - ) -> typing.Sequence[webhooks.Webhook]: + async def fetch_guild_webhooks(self, guild: bases.Hashable[guilds.Guild]) -> typing.Sequence[webhooks.Webhook]: """Get all webhooks for a given guild. Parameters ---------- - guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] + guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.entities.Snowflake`, :obj:`~int` ] The object or ID for the guild to get the webhooks from. Returns @@ -1934,6 +1916,6 @@ async def fetch_guild_webhooks( aren't a member of the given guild. """ payload = await self._session.get_guild_webhooks( - guild_id=str(guild.id if isinstance(guild, snowflakes.UniqueEntity) else int(guild)) + guild_id=str(guild.id if isinstance(guild, bases.UniqueEntity) else int(guild)) ) return [webhooks.Webhook.deserialize(webhook) for webhook in payload] diff --git a/hikari/clients/rest_clients/invites_component.py b/hikari/clients/rest_clients/invites_component.py index 715674e483..e22e2b95cc 100644 --- a/hikari/clients/rest_clients/invites_component.py +++ b/hikari/clients/rest_clients/invites_component.py @@ -23,8 +23,8 @@ import abc import typing -from hikari.clients.rest_clients import component_base from hikari import invites +from hikari.clients.rest_clients import component_base class RESTInviteComponent(component_base.BaseRESTComponent, abc.ABC): # pylint: disable=W0223 diff --git a/hikari/clients/rest_clients/oauth2_component.py b/hikari/clients/rest_clients/oauth2_component.py index d31ba4874b..75e89afbda 100644 --- a/hikari/clients/rest_clients/oauth2_component.py +++ b/hikari/clients/rest_clients/oauth2_component.py @@ -22,8 +22,8 @@ import abc -from hikari.clients.rest_clients import component_base from hikari import oauth2 +from hikari.clients.rest_clients import component_base class RESTOauth2Component(component_base.BaseRESTComponent, abc.ABC): # pylint: disable=W0223 diff --git a/hikari/clients/rest_clients/reactions_component.py b/hikari/clients/rest_clients/reactions_component.py index 9df5af605c..49ed21fcb4 100644 --- a/hikari/clients/rest_clients/reactions_component.py +++ b/hikari/clients/rest_clients/reactions_component.py @@ -24,13 +24,13 @@ import datetime import typing -from hikari.clients.rest_clients import component_base -from hikari.internal import pagination +from hikari import bases from hikari import channels as _channels -from hikari import messages as _messages from hikari import emojis -from hikari import snowflakes +from hikari import messages as _messages from hikari import users +from hikari.clients.rest_clients import component_base +from hikari.internal import pagination class RESTReactionComponent(component_base.BaseRESTComponent, abc.ABC): # pylint: disable=W0223 @@ -38,17 +38,17 @@ class RESTReactionComponent(component_base.BaseRESTComponent, abc.ABC): # pylin async def create_reaction( self, - channel: snowflakes.HashableT[_channels.Channel], - message: snowflakes.HashableT[_messages.Message], + channel: bases.Hashable[_channels.Channel], + message: bases.Hashable[_messages.Message], emoji: typing.Union[emojis.Emoji, str], ) -> None: """Add a reaction to the given message in the given channel. Parameters ---------- - channel : :obj:`~typing.Union` [ :obj:`~hikari.channels.Channel`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] + channel : :obj:`~typing.Union` [ :obj:`~hikari.channels.Channel`, :obj:`~hikari.entities.Snowflake`, :obj:`~int` ] The object or ID of the channel to add this reaction in. - message : :obj:`~typing.Union` [ :obj:`~hikari.messages.Message`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] + message : :obj:`~typing.Union` [ :obj:`~hikari.messages.Message`, :obj:`~hikari.entities.Snowflake`, :obj:`~int` ] The object or ID of the message to add the reaction in. emoji : :obj:`~typing.Union` [ :obj:`~hikari.emojis.Emoji`, :obj:`~str` ] The emoji to add. This can either be an emoji object or a string @@ -70,24 +70,24 @@ async def create_reaction( due to it being outside of the range of a 64 bit integer. """ await self._session.create_reaction( - channel_id=str(channel.id if isinstance(channel, snowflakes.UniqueEntity) else int(channel)), - message_id=str(message.id if isinstance(message, snowflakes.UniqueEntity) else int(message)), + channel_id=str(channel.id if isinstance(channel, bases.UniqueEntity) else int(channel)), + message_id=str(message.id if isinstance(message, bases.UniqueEntity) else int(message)), emoji=str(getattr(emoji, "url_name", emoji)), ) async def delete_reaction( self, - channel: snowflakes.HashableT[_channels.Channel], - message: snowflakes.HashableT[_messages.Message], + channel: bases.Hashable[_channels.Channel], + message: bases.Hashable[_messages.Message], emoji: typing.Union[emojis.Emoji, str], ) -> None: """Remove your own reaction from the given message in the given channel. Parameters ---------- - channel : :obj:`~typing.Union` [ :obj:`~hikari.channels.Channel`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] + channel : :obj:`~typing.Union` [ :obj:`~hikari.channels.Channel`, :obj:`~hikari.entities.Snowflake`, :obj:`~int` ] The object or ID of the channel to add this reaction in. - message : :obj:`~typing.Union` [ :obj:`~hikari.messages.Message`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] + message : :obj:`~typing.Union` [ :obj:`~hikari.messages.Message`, :obj:`~hikari.entities.Snowflake`, :obj:`~int` ] The object or ID of the message to add the reaction in. emoji : :obj:`~typing.Union` [ :obj:`~hikari.emojis.Emoji`, :obj:`~str` ] The emoji to add. This can either be an emoji object or a @@ -110,21 +110,21 @@ async def delete_reaction( due to it being outside of the range of a 64 bit integer. """ await self._session.delete_own_reaction( - channel_id=str(channel.id if isinstance(channel, snowflakes.UniqueEntity) else int(channel)), - message_id=str(message.id if isinstance(message, snowflakes.UniqueEntity) else int(message)), + channel_id=str(channel.id if isinstance(channel, bases.UniqueEntity) else int(channel)), + message_id=str(message.id if isinstance(message, bases.UniqueEntity) else int(message)), emoji=str(getattr(emoji, "url_name", emoji)), ) async def delete_all_reactions( - self, channel: snowflakes.HashableT[_channels.Channel], message: snowflakes.HashableT[_messages.Message], + self, channel: bases.Hashable[_channels.Channel], message: bases.Hashable[_messages.Message], ) -> None: """Delete all reactions from a given message in a given channel. Parameters ---------- - channel : :obj:`~typing.Union` [ :obj:`~hikari.channels.Channel`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] + channel : :obj:`~typing.Union` [ :obj:`~hikari.channels.Channel`, :obj:`~hikari.entities.Snowflake`, :obj:`~int` ] The object or ID of the channel to get the message from. - message : :obj:`~typing.Union` [ :obj:`~hikari.messages.Message`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] + message : :obj:`~typing.Union` [ :obj:`~hikari.messages.Message`, :obj:`~hikari.entities.Snowflake`, :obj:`~int` ] The object or ID of the message to remove all reactions from. Raises @@ -138,23 +138,23 @@ async def delete_all_reactions( If you lack the ``MANAGE_MESSAGES`` permission. """ await self._session.delete_all_reactions( - channel_id=str(channel.id if isinstance(channel, snowflakes.UniqueEntity) else int(channel)), - message_id=str(message.id if isinstance(message, snowflakes.UniqueEntity) else int(message)), + channel_id=str(channel.id if isinstance(channel, bases.UniqueEntity) else int(channel)), + message_id=str(message.id if isinstance(message, bases.UniqueEntity) else int(message)), ) async def delete_all_reactions_for_emoji( self, - channel: snowflakes.HashableT[_channels.Channel], - message: snowflakes.HashableT[_messages.Message], + channel: bases.Hashable[_channels.Channel], + message: bases.Hashable[_messages.Message], emoji: typing.Union[emojis.Emoji, str], ) -> None: """Remove all reactions for a single given emoji on a given message. Parameters ---------- - channel : :obj:`~typing.Union` [ :obj:`~hikari.channels.Channel`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] + channel : :obj:`~typing.Union` [ :obj:`~hikari.channels.Channel`, :obj:`~hikari.entities.Snowflake`, :obj:`~int` ] The object or ID of the channel to get the message from. - message : :obj:`~typing.Union` [ :obj:`~hikari.messages.Message`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] + message : :obj:`~typing.Union` [ :obj:`~hikari.messages.Message`, :obj:`~hikari.entities.Snowflake`, :obj:`~int` ] The object or ID of the message to delete the reactions from. emoji : :obj:`~typing.Union` [ :obj:`~hikari.emojis.Emoji`, :obj:`~str` ] The object or string representatiom of the emoji to delete. The @@ -173,18 +173,18 @@ async def delete_all_reactions_for_emoji( DM channel. """ await self._session.delete_all_reactions_for_emoji( - channel_id=str(channel.id if isinstance(channel, snowflakes.UniqueEntity) else int(channel)), - message_id=str(message.id if isinstance(message, snowflakes.UniqueEntity) else int(message)), + channel_id=str(channel.id if isinstance(channel, bases.UniqueEntity) else int(channel)), + message_id=str(message.id if isinstance(message, bases.UniqueEntity) else int(message)), emoji=str(getattr(emoji, "url_name", emoji)), ) def fetch_reactors_after( self, - channel: snowflakes.HashableT[_channels.Channel], - message: snowflakes.HashableT[_messages.Message], + channel: bases.Hashable[_channels.Channel], + message: bases.Hashable[_messages.Message], emoji: typing.Union[emojis.Emoji, str], *, - after: typing.Union[datetime.datetime, snowflakes.HashableT[users.User]] = 0, + after: typing.Union[datetime.datetime, bases.Hashable[users.User]] = 0, limit: typing.Optional[int] = None, ) -> typing.AsyncIterator[users.User]: """Get an async iterator of the users who reacted to a message. @@ -194,16 +194,16 @@ def fetch_reactors_after( Parameters ---------- - channel : :obj:`~typing.Union` [ :obj:`~hikari.channels.Channel`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] + channel : :obj:`~typing.Union` [ :obj:`~hikari.channels.Channel`, :obj:`~hikari.entities.Snowflake`, :obj:`~int` ] The object or ID of the channel to get the message from. - message : :obj:`~typing.Union` [ :obj:`~hikari.messages.Message`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] + message : :obj:`~typing.Union` [ :obj:`~hikari.messages.Message`, :obj:`~hikari.entities.Snowflake`, :obj:`~int` ] The object or ID of the message to get the reactions from. emoji : :obj:`~typing.Union` [ :obj:`~hikari.emojis.Emoji`, :obj:`~str` ] The emoji to get. This can either be it's object or the string representation of the emoji. The string representation will be either ``"name:id"`` for custom emojis else it's unicode character(s) (can be UTF-32). - after : :obj:`~typing.Union` [ :obj:`~datetime.datetime`, :obj:`~hikari.users.User`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] + after : :obj:`~typing.Union` [ :obj:`~datetime.datetime`, :obj:`~hikari.users.User`, :obj:`~hikari.entities.Snowflake`, :obj:`~int` ] If specified, a object or ID user. If specified, only users with a snowflake that is lexicographically greater than the value will be returned. @@ -235,12 +235,12 @@ def fetch_reactors_after( If the channel or message is not found. """ if isinstance(after, datetime.datetime): - after = str(snowflakes.Snowflake.from_datetime(after)) + after = str(bases.Snowflake.from_datetime(after)) else: - after = str(after.id if isinstance(after, snowflakes.UniqueEntity) else int(after)) + after = str(after.id if isinstance(after, bases.UniqueEntity) else int(after)) return pagination.pagination_handler( - channel_id=str(channel.id if isinstance(channel, snowflakes.UniqueEntity) else int(channel)), - message_id=str(message.id if isinstance(message, snowflakes.UniqueEntity) else int(message)), + channel_id=str(channel.id if isinstance(channel, bases.UniqueEntity) else int(channel)), + message_id=str(message.id if isinstance(message, bases.UniqueEntity) else int(message)), emoji=getattr(emoji, "url_name", emoji), deserializer=users.User.deserialize, direction="after", diff --git a/hikari/clients/rest_clients/users_component.py b/hikari/clients/rest_clients/users_component.py index 1b44fd2c71..3879c28e04 100644 --- a/hikari/clients/rest_clients/users_component.py +++ b/hikari/clients/rest_clients/users_component.py @@ -22,20 +22,20 @@ import abc -from hikari.clients.rest_clients import component_base -from hikari import snowflakes +from hikari import bases from hikari import users +from hikari.clients.rest_clients import component_base class RESTUserComponent(component_base.BaseRESTComponent, abc.ABC): # pylint: disable=W0223 """The REST client component for handling requests to user endpoints.""" - async def fetch_user(self, user: snowflakes.HashableT[users.User]) -> users.User: + async def fetch_user(self, user: bases.Hashable[users.User]) -> users.User: """Get a given user. Parameters ---------- - user : :obj:`~typing.Union` [ :obj:`~hikari.users.User`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] + user : :obj:`~typing.Union` [ :obj:`~hikari.users.User`, :obj:`~hikari.entities.Snowflake`, :obj:`~int` ] The object or ID of the user to get. Returns @@ -52,6 +52,6 @@ async def fetch_user(self, user: snowflakes.HashableT[users.User]) -> users.User If the user is not found. """ payload = await self._session.get_user( - user_id=str(user.id if isinstance(user, snowflakes.UniqueEntity) else int(user)) + user_id=str(user.id if isinstance(user, bases.UniqueEntity) else int(user)) ) return users.User.deserialize(payload) diff --git a/hikari/clients/rest_clients/voices_component.py b/hikari/clients/rest_clients/voices_component.py index b5c3c19dd9..5c8ad837a1 100644 --- a/hikari/clients/rest_clients/voices_component.py +++ b/hikari/clients/rest_clients/voices_component.py @@ -23,8 +23,8 @@ import abc import typing -from hikari.clients.rest_clients import component_base from hikari import voices +from hikari.clients.rest_clients import component_base class RESTVoiceComponent(component_base.BaseRESTComponent, abc.ABC): # pylint: disable=W0223 diff --git a/hikari/clients/rest_clients/webhooks_component.py b/hikari/clients/rest_clients/webhooks_component.py index e9090c9c36..24eb221ccd 100644 --- a/hikari/clients/rest_clients/webhooks_component.py +++ b/hikari/clients/rest_clients/webhooks_component.py @@ -23,30 +23,30 @@ import abc import typing -from hikari.clients.rest_clients import component_base -from hikari.internal import allowed_mentions -from hikari.internal import conversions +from hikari import bases from hikari import channels as _channels from hikari import embeds as _embeds from hikari import guilds from hikari import media from hikari import messages as _messages -from hikari import snowflakes from hikari import users from hikari import webhooks +from hikari.clients.rest_clients import component_base +from hikari.internal import allowed_mentions +from hikari.internal import conversions class RESTWebhookComponent(component_base.BaseRESTComponent, abc.ABC): # pylint: disable=W0223 """The REST client component for handling requests to webhook endpoints.""" async def fetch_webhook( - self, webhook: snowflakes.HashableT[webhooks.Webhook], *, webhook_token: str = ... + self, webhook: bases.Hashable[webhooks.Webhook], *, webhook_token: str = ... ) -> webhooks.Webhook: """Get a given webhook. Parameters ---------- - webhook : :obj:`~typing.Union` [ :obj:`~hikari.webhooks.Webhook`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] + webhook : :obj:`~typing.Union` [ :obj:`~hikari.webhooks.Webhook`, :obj:`~hikari.entities.Snowflake`, :obj:`~int` ] The object or ID of the webhook to get. webhook_token : :obj:`~str` If specified, the webhook token to use to get it (bypassing this @@ -71,26 +71,26 @@ async def fetch_webhook( If you pass a token that's invalid for the target webhook. """ payload = await self._session.get_webhook( - webhook_id=str(webhook.id if isinstance(webhook, snowflakes.UniqueEntity) else int(webhook)), + webhook_id=str(webhook.id if isinstance(webhook, bases.UniqueEntity) else int(webhook)), webhook_token=webhook_token, ) return webhooks.Webhook.deserialize(payload) async def update_webhook( self, - webhook: snowflakes.HashableT[webhooks.Webhook], + webhook: bases.Hashable[webhooks.Webhook], *, webhook_token: str = ..., name: str = ..., avatar_data: typing.Optional[conversions.FileLikeT] = ..., - channel: snowflakes.HashableT[_channels.GuildChannel] = ..., + channel: bases.Hashable[_channels.GuildChannel] = ..., reason: str = ..., ) -> webhooks.Webhook: """Edit a given webhook. Parameters ---------- - webhook : :obj:`~typing.Union` [ :obj:`~hikari.webhooks.Webhook`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] + webhook : :obj:`~typing.Union` [ :obj:`~hikari.webhooks.Webhook`, :obj:`~hikari.entities.Snowflake`, :obj:`~int` ] The object or ID of the webhook to edit. webhook_token : :obj:`~str` If specified, the webhook token to use to modify it (bypassing this @@ -100,7 +100,7 @@ async def update_webhook( avatar_data : ``hikari.internal.conversions.FileLikeT``, optional If specified, the new avatar image file object. If :obj:`~None`, then it is removed. - channel : :obj:`~typing.Union` [ :obj:`~hikari.channels.GuildChannel`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] + channel : :obj:`~typing.Union` [ :obj:`~hikari.channels.GuildChannel`, :obj:`~hikari.entities.Snowflake`, :obj:`~int` ] If specified, the object or ID of the new channel the given webhook should be moved to. reason : :obj:`~str` @@ -126,7 +126,7 @@ async def update_webhook( If you pass a token that's invalid for the target webhook. """ payload = await self._session.modify_webhook( - webhook_id=str(webhook.id if isinstance(webhook, snowflakes.UniqueEntity) else int(webhook)), + webhook_id=str(webhook.id if isinstance(webhook, bases.UniqueEntity) else int(webhook)), webhook_token=webhook_token, name=name, avatar=( @@ -135,7 +135,7 @@ async def update_webhook( else avatar_data ), channel_id=( - str(channel.id if isinstance(channel, snowflakes.UniqueEntity) else int(channel)) + str(channel.id if isinstance(channel, bases.UniqueEntity) else int(channel)) if channel and channel is not ... else channel ), @@ -143,14 +143,12 @@ async def update_webhook( ) return webhooks.Webhook.deserialize(payload) - async def delete_webhook( - self, webhook: snowflakes.HashableT[webhooks.Webhook], *, webhook_token: str = ... - ) -> None: + async def delete_webhook(self, webhook: bases.Hashable[webhooks.Webhook], *, webhook_token: str = ...) -> None: """Delete a given webhook. Parameters ---------- - webhook : :obj:`~typing.Union` [ :obj:`~hikari.webhooks.Webhook`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] + webhook : :obj:`~typing.Union` [ :obj:`~hikari.webhooks.Webhook`, :obj:`~hikari.entities.Snowflake`, :obj:`~int` ] The object or ID of the webhook to delete webhook_token : :obj:`~str` If specified, the webhook token to use to delete it (bypassing this @@ -170,13 +168,13 @@ async def delete_webhook( If you pass a token that's invalid for the target webhook. """ await self._session.delete_webhook( - webhook_id=str(webhook.id if isinstance(webhook, snowflakes.UniqueEntity) else int(webhook)), + webhook_id=str(webhook.id if isinstance(webhook, bases.UniqueEntity) else int(webhook)), webhook_token=webhook_token, ) async def execute_webhook( self, - webhook: snowflakes.HashableT[webhooks.Webhook], + webhook: bases.Hashable[webhooks.Webhook], webhook_token: str, *, content: str = ..., @@ -187,14 +185,14 @@ async def execute_webhook( file: media.IO = ..., embeds: typing.Sequence[_embeds.Embed] = ..., mentions_everyone: bool = True, - user_mentions: typing.Union[typing.Collection[snowflakes.HashableT[users.User]], bool] = True, - role_mentions: typing.Union[typing.Collection[snowflakes.HashableT[guilds.GuildRole]], bool] = True, + user_mentions: typing.Union[typing.Collection[bases.Hashable[users.User]], bool] = True, + role_mentions: typing.Union[typing.Collection[bases.Hashable[guilds.GuildRole]], bool] = True, ) -> typing.Optional[_messages.Message]: """Execute a webhook to create a message. Parameters ---------- - webhook : :obj:`~typing.Union` [ :obj:`~hikari.webhooks.Webhook`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] + webhook : :obj:`~typing.Union` [ :obj:`~hikari.webhooks.Webhook`, :obj:`~hikari.entities.Snowflake`, :obj:`~int` ] The object or ID of the webhook to execute. webhook_token : :obj:`~str` The token of the webhook to execute. @@ -220,11 +218,11 @@ async def execute_webhook( mentions_everyone : :obj:`~bool` Whether ``@everyone`` and ``@here`` mentions should be resolved by discord and lead to actual pings, defaults to :obj:`~True`. - user_mentions : :obj:`~typing.Union` [ :obj:`~typing.Collection` [ :obj:`~typing.Union` [ :obj:`~hikari.users.User`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ], :obj:`~bool` ] + user_mentions : :obj:`~typing.Union` [ :obj:`~typing.Collection` [ :obj:`~typing.Union` [ :obj:`~hikari.users.User`, :obj:`~hikari.entities.Snowflake`, :obj:`~int` ], :obj:`~bool` ] Either an array of user objects/IDs to allow mentions for, :obj:`~True` to allow all user mentions or :obj:`~False` to block all user mentions from resolving, defaults to :obj:`~True`. - role_mentions : :obj:`~typing.Union` [ :obj:`~typing.Collection` [ :obj:`~typing.Union` [ :obj:`~hikari.guilds.GuildRole`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] ], :obj:`~bool` ] + role_mentions : :obj:`~typing.Union` [ :obj:`~typing.Collection` [ :obj:`~typing.Union` [ :obj:`~hikari.guilds.GuildRole`, :obj:`~hikari.entities.Snowflake`, :obj:`~int` ] ], :obj:`~bool` ] Either an array of guild role objects/IDs to allow mentions for, :obj:`~True` to allow all role mentions or :obj:`~False` to block all role mentions from resolving, defaults to :obj:`~True`. @@ -251,11 +249,11 @@ async def execute_webhook( :obj:`~hikari.errors.UnauthorizedHTTPError` If you pass a token that's invalid for the target webhook. :obj:`~ValueError` - If more than 100 unique objects/snowflakes are passed for + If more than 100 unique objects/entities are passed for ``role_mentions`` or ``user_mentions``. """ payload = await self._session.execute_webhook( - webhook_id=str(webhook.id if isinstance(webhook, snowflakes.UniqueEntity) else int(webhook)), + webhook_id=str(webhook.id if isinstance(webhook, bases.UniqueEntity) else int(webhook)), webhook_token=webhook_token, content=content, username=username, @@ -274,7 +272,7 @@ async def execute_webhook( def safe_webhook_execute( self, - webhook: snowflakes.HashableT[webhooks.Webhook], + webhook: bases.Hashable[webhooks.Webhook], webhook_token: str, *, content: str = ..., @@ -285,8 +283,8 @@ def safe_webhook_execute( file: media.IO = ..., embeds: typing.Sequence[_embeds.Embed] = ..., mentions_everyone: bool = False, - user_mentions: typing.Union[typing.Collection[snowflakes.HashableT[users.User]], bool] = False, - role_mentions: typing.Union[typing.Collection[snowflakes.HashableT[guilds.GuildRole]], bool] = False, + user_mentions: typing.Union[typing.Collection[bases.Hashable[users.User]], bool] = False, + role_mentions: typing.Union[typing.Collection[bases.Hashable[guilds.GuildRole]], bool] = False, ) -> typing.Coroutine[typing.Any, typing.Any, typing.Optional[_messages.Message]]: """Execute a webhook to create a message with mention safety. diff --git a/hikari/clients/shard_clients.py b/hikari/clients/shard_clients.py index 6b992ae3d7..81301aa4f1 100644 --- a/hikari/clients/shard_clients.py +++ b/hikari/clients/shard_clients.py @@ -121,7 +121,7 @@ def status(self) -> guilds.PresenceStatus: @property @abc.abstractmethod - def activity(self) -> typing.Optional[gateway_entities.GatewayActivity]: + def activity(self) -> typing.Optional[gateway_entities.Activity]: """Activity for the user status for this shard. Returns @@ -266,7 +266,7 @@ def intents(self) -> typing.Optional[codes.GatewayIntent]: Returns ------- - :obj:`~hikari.net.codes.GatewayIntent`, optional + :obj:`~hikari.intents.Intent`, optional A :obj:`~enum.IntFlag` enum containing each intent that is set. If intents are not being used at all, then this will return :obj:`~None` instead. @@ -277,7 +277,7 @@ async def update_presence( self, *, status: guilds.PresenceStatus = ..., - activity: typing.Optional[gateway_entities.GatewayActivity] = ..., + activity: typing.Optional[gateway_entities.Activity] = ..., idle_since: typing.Optional[datetime.datetime] = ..., is_afk: bool = ..., ) -> None: @@ -412,7 +412,7 @@ def status(self) -> guilds.PresenceStatus: return self._status @property - def activity(self) -> typing.Optional[gateway_entities.GatewayActivity]: + def activity(self) -> typing.Optional[gateway_entities.Activity]: return self._activity @property @@ -645,7 +645,7 @@ async def update_presence( self, *, status: guilds.PresenceStatus = ..., - activity: typing.Optional[gateway_entities.GatewayActivity] = ..., + activity: typing.Optional[gateway_entities.Activity] = ..., idle_since: typing.Optional[datetime.datetime] = ..., is_afk: bool = ..., ) -> None: @@ -666,7 +666,7 @@ async def update_presence( @staticmethod def _create_presence_pl( status: guilds.PresenceStatus, - activity: typing.Optional[gateway_entities.GatewayActivity], + activity: typing.Optional[gateway_entities.Activity], idle_since: typing.Optional[datetime.datetime], is_afk: bool, ) -> typing.Dict[str, typing.Any]: diff --git a/hikari/clients/test_client.py b/hikari/clients/test_client.py index 27af5622ab..4f2140993e 100644 --- a/hikari/clients/test_client.py +++ b/hikari/clients/test_client.py @@ -90,7 +90,7 @@ def run_gateway(compression, color, debug, intents, logger, shards, token, verif intents=intents, verify_ssl=verify_ssl, shard_count=shards, - initial_activity=hikari.GatewayActivity(name="people mention me", type=hikari.ActivityType.LISTENING,), + initial_activity=hikari.Activity(name="people mention me", type=hikari.ActivityType.LISTENING,), ) ) diff --git a/hikari/embeds.py b/hikari/embeds.py index 5b5a53c6bd..451f09eedf 100644 --- a/hikari/embeds.py +++ b/hikari/embeds.py @@ -33,15 +33,15 @@ import attr +from hikari import bases from hikari import colors -from hikari import entities from hikari.internal import conversions from hikari.internal import marshaller @marshaller.marshallable() @attr.s(slots=True, kw_only=True) -class EmbedFooter(entities.HikariEntity, marshaller.Deserializable, marshaller.Serializable): +class EmbedFooter(bases.HikariEntity, marshaller.Deserializable, marshaller.Serializable): """Represents an embed footer.""" #: The footer text. @@ -72,7 +72,7 @@ class EmbedFooter(entities.HikariEntity, marshaller.Deserializable, marshaller.S @marshaller.marshallable() @attr.s(slots=True, kw_only=True) -class EmbedImage(entities.HikariEntity, marshaller.Deserializable, marshaller.Serializable): +class EmbedImage(bases.HikariEntity, marshaller.Deserializable, marshaller.Serializable): """Represents an embed image.""" #: The URL of the image. @@ -118,7 +118,7 @@ class EmbedImage(entities.HikariEntity, marshaller.Deserializable, marshaller.Se @marshaller.marshallable() @attr.s(slots=True, kw_only=True) -class EmbedThumbnail(entities.HikariEntity, marshaller.Deserializable, marshaller.Serializable): +class EmbedThumbnail(bases.HikariEntity, marshaller.Deserializable, marshaller.Serializable): """Represents an embed thumbnail.""" #: The URL of the thumbnail. @@ -164,7 +164,7 @@ class EmbedThumbnail(entities.HikariEntity, marshaller.Deserializable, marshalle @marshaller.marshallable() @attr.s(slots=True, kw_only=True) -class EmbedVideo(entities.HikariEntity, marshaller.Deserializable): +class EmbedVideo(bases.HikariEntity, marshaller.Deserializable): """Represents an embed video. Note @@ -191,7 +191,7 @@ class EmbedVideo(entities.HikariEntity, marshaller.Deserializable): @marshaller.marshallable() @attr.s(slots=True, kw_only=True) -class EmbedProvider(entities.HikariEntity, marshaller.Deserializable): +class EmbedProvider(bases.HikariEntity, marshaller.Deserializable): """Represents an embed provider. Note @@ -213,7 +213,7 @@ class EmbedProvider(entities.HikariEntity, marshaller.Deserializable): @marshaller.marshallable() @attr.s(slots=True, kw_only=True) -class EmbedAuthor(entities.HikariEntity, marshaller.Deserializable, marshaller.Serializable): +class EmbedAuthor(bases.HikariEntity, marshaller.Deserializable, marshaller.Serializable): """Represents an embed author.""" #: The name of the author. @@ -249,7 +249,7 @@ class EmbedAuthor(entities.HikariEntity, marshaller.Deserializable, marshaller.S @marshaller.marshallable() @attr.s(slots=True, kw_only=True) -class EmbedField(entities.HikariEntity, marshaller.Deserializable, marshaller.Serializable): +class EmbedField(bases.HikariEntity, marshaller.Deserializable, marshaller.Serializable): """Represents a field in a embed.""" #: The name of the field. @@ -272,7 +272,7 @@ class EmbedField(entities.HikariEntity, marshaller.Deserializable, marshaller.Se @marshaller.marshallable() @attr.s(slots=True, kw_only=True) -class Embed(entities.HikariEntity, marshaller.Deserializable, marshaller.Serializable): +class Embed(bases.HikariEntity, marshaller.Deserializable, marshaller.Serializable): """Represents an embed.""" #: The title of the embed. diff --git a/hikari/emojis.py b/hikari/emojis.py index 6e5ff76f2b..34e85231a4 100644 --- a/hikari/emojis.py +++ b/hikari/emojis.py @@ -21,8 +21,7 @@ import attr -from hikari import entities -from hikari import snowflakes +from hikari import bases from hikari import users from hikari.internal import marshaller @@ -31,7 +30,7 @@ @marshaller.marshallable() @attr.s(slots=True, kw_only=True) -class Emoji(entities.HikariEntity, marshaller.Deserializable): +class Emoji(bases.HikariEntity, marshaller.Deserializable): """Base class for all emojis.""" @@ -58,7 +57,7 @@ def mention(self) -> str: @marshaller.marshallable() @attr.s(slots=True, kw_only=True) -class UnknownEmoji(Emoji, snowflakes.UniqueEntity): +class UnknownEmoji(Emoji, bases.UniqueEntity): """Represents a unknown emoji.""" #: The name of the emoji. @@ -89,10 +88,10 @@ class GuildEmoji(UnknownEmoji): #: The whitelisted role IDs to use this emoji. #: - #: :type: :obj:`~typing.Set` [ :obj:`~hikari.snowflakes.Snowflake` ] - role_ids: typing.Set[snowflakes.Snowflake] = marshaller.attrib( + #: :type: :obj:`~typing.Set` [ :obj:`~hikari.entities.Snowflake` ] + role_ids: typing.Set[bases.Snowflake] = marshaller.attrib( raw_name="roles", - deserializer=lambda roles: {snowflakes.Snowflake.deserialize(r) for r in roles}, + deserializer=lambda roles: {bases.Snowflake.deserialize(r) for r in roles}, if_undefined=set, factory=set, ) diff --git a/hikari/entities.py b/hikari/entities.py deleted file mode 100644 index ac89c36211..0000000000 --- a/hikari/entities.py +++ /dev/null @@ -1,37 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# Copyright © Nekokatt 2019-2020 -# -# This file is part of Hikari. -# -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . -"""Datastructure bases.""" -__all__ = ["HikariEntity", "RawEntityT"] - -import abc -import typing - -import attr - -from hikari.internal import marshaller - -RawEntityT = typing.Union[ - None, bool, int, float, str, bytes, typing.Sequence[typing.Any], typing.Mapping[str, typing.Any] -] - - -@marshaller.marshallable() -@attr.s(slots=True, kw_only=True) -class HikariEntity(metaclass=abc.ABCMeta): - """The base for any entity used in this API.""" diff --git a/hikari/events.py b/hikari/events.py index 5ed540da7f..a968358390 100644 --- a/hikari/events.py +++ b/hikari/events.py @@ -70,15 +70,14 @@ import attr +from hikari import bases from hikari import channels from hikari import embeds as _embeds from hikari import emojis as _emojis -from hikari import entities from hikari import guilds from hikari import invites from hikari import messages from hikari import oauth2 -from hikari import snowflakes from hikari import unset from hikari import users from hikari import voices @@ -92,7 +91,7 @@ # Base event, is not deserialized @marshaller.marshallable() @attr.s(slots=True, kw_only=True) -class HikariEvent(entities.HikariEntity): +class HikariEvent(bases.HikariEntity): """The base class that all events inherit from.""" @@ -192,8 +191,8 @@ class ReadyEvent(HikariEvent, marshaller.Deserializable): #: A mapping of the guilds this bot is currently in. All guilds will start #: off "unavailable". #: - #: :type: :obj:`~typing.Mapping` [ :obj:`~hikari.snowflakes.Snowflake`, :obj:`~hikari.guilds.UnavailableGuild` ] - unavailable_guilds: typing.Mapping[snowflakes.Snowflake, guilds.UnavailableGuild] = marshaller.attrib( + #: :type: :obj:`~typing.Mapping` [ :obj:`~hikari.entities.Snowflake`, :obj:`~hikari.guilds.UnavailableGuild` ] + unavailable_guilds: typing.Mapping[bases.Snowflake, guilds.UnavailableGuild] = marshaller.attrib( raw_name="guilds", deserializer=lambda guilds_objs: {g.id: g for g in map(guilds.UnavailableGuild.deserialize, guilds_objs)}, ) @@ -229,7 +228,7 @@ def shard_count(self) -> typing.Optional[int]: @marshaller.marshallable() @attr.s(slots=True, kw_only=True) -class BaseChannelEvent(HikariEvent, snowflakes.UniqueEntity, marshaller.Deserializable): +class BaseChannelEvent(HikariEvent, bases.UniqueEntity, marshaller.Deserializable): """A base object that Channel events will inherit from.""" #: The channel's type. @@ -239,9 +238,9 @@ class BaseChannelEvent(HikariEvent, snowflakes.UniqueEntity, marshaller.Deserial #: The ID of the guild this channel is in, will be :obj:`~None` for DMs. #: - #: :type: :obj:`~hikari.snowflakes.Snowflake`, optional - guild_id: typing.Optional[snowflakes.Snowflake] = marshaller.attrib( - deserializer=snowflakes.Snowflake.deserialize, if_undefined=None, default=None + #: :type: :obj:`~hikari.entities.Snowflake`, optional + guild_id: typing.Optional[bases.Snowflake] = marshaller.attrib( + deserializer=bases.Snowflake.deserialize, if_undefined=None, default=None ) #: The sorting position of this channel, will be relative to the @@ -252,9 +251,9 @@ class BaseChannelEvent(HikariEvent, snowflakes.UniqueEntity, marshaller.Deserial #: An mapping of the set permission overwrites for this channel, if applicable. #: - #: :type: :obj:`~typing.Mapping` [ :obj:`~hikari.snowflakes.Snowflake`, :obj:`~hikari.channels.PermissionOverwrite` ], optional + #: :type: :obj:`~typing.Mapping` [ :obj:`~hikari.entities.Snowflake`, :obj:`~hikari.channels.PermissionOverwrite` ], optional permission_overwrites: typing.Optional[ - typing.Mapping[snowflakes.Snowflake, channels.PermissionOverwrite] + typing.Mapping[bases.Snowflake, channels.PermissionOverwrite] ] = marshaller.attrib( deserializer=lambda overwrites: {o.id: o for o in map(channels.PermissionOverwrite.deserialize, overwrites)}, if_undefined=None, @@ -280,9 +279,9 @@ class BaseChannelEvent(HikariEvent, snowflakes.UniqueEntity, marshaller.Deserial #: The ID of the last message sent, if it's a text type channel. #: - #: :type: :obj:`~hikari.snowflakes.Snowflake`, optional - last_message_id: typing.Optional[snowflakes.Snowflake] = marshaller.attrib( - deserializer=snowflakes.Snowflake.deserialize, if_none=None, if_undefined=None, default=None + #: :type: :obj:`~hikari.entities.Snowflake`, optional + last_message_id: typing.Optional[bases.Snowflake] = marshaller.attrib( + deserializer=bases.Snowflake.deserialize, if_none=None, if_undefined=None, default=None ) #: The bitrate (in bits) of this channel, if it's a guild voice channel. @@ -305,8 +304,8 @@ class BaseChannelEvent(HikariEvent, snowflakes.UniqueEntity, marshaller.Deserial #: A mapping of this channel's recipient users, if it's a DM or group DM. #: - #: :type: :obj:`~typing.Mapping` [ :obj:`~hikari.snowflakes.Snowflake`, :obj:`~hikari.users.User` ], optional - recipients: typing.Optional[typing.Mapping[snowflakes.Snowflake, users.User]] = marshaller.attrib( + #: :type: :obj:`~typing.Mapping` [ :obj:`~hikari.entities.Snowflake`, :obj:`~hikari.users.User` ], optional + recipients: typing.Optional[typing.Mapping[bases.Snowflake, users.User]] = marshaller.attrib( deserializer=lambda recipients: {user.id: user for user in map(users.User.deserialize, recipients)}, if_undefined=None, default=None, @@ -321,24 +320,24 @@ class BaseChannelEvent(HikariEvent, snowflakes.UniqueEntity, marshaller.Deserial #: The ID of this channel's creator, if it's a DM channel. #: - #: :type: :obj:`~hikari.snowflakes.Snowflake`, optional - owner_id: typing.Optional[snowflakes.Snowflake] = marshaller.attrib( - deserializer=snowflakes.Snowflake.deserialize, if_undefined=None, default=None + #: :type: :obj:`~hikari.entities.Snowflake`, optional + owner_id: typing.Optional[bases.Snowflake] = marshaller.attrib( + deserializer=bases.Snowflake.deserialize, if_undefined=None, default=None ) #: The ID of the application that created the group DM, if it's a #: bot based group DM. #: - #: :type: :obj:`~hikari.snowflakes.Snowflake`, optional - application_id: typing.Optional[snowflakes.Snowflake] = marshaller.attrib( - deserializer=snowflakes.Snowflake.deserialize, if_undefined=None, default=None + #: :type: :obj:`~hikari.entities.Snowflake`, optional + application_id: typing.Optional[bases.Snowflake] = marshaller.attrib( + deserializer=bases.Snowflake.deserialize, if_undefined=None, default=None ) #: The ID of this channels's parent category within guild, if set. #: - #: :type: :obj:`~hikari.snowflakes.Snowflake`, optional - parent_id: typing.Optional[snowflakes.Snowflake] = marshaller.attrib( - deserializer=snowflakes.Snowflake.deserialize, if_undefined=None, if_none=None, default=None + #: :type: :obj:`~hikari.entities.Snowflake`, optional + parent_id: typing.Optional[bases.Snowflake] = marshaller.attrib( + deserializer=bases.Snowflake.deserialize, if_undefined=None, if_none=None, default=None ) #: The datetime of when the last message was pinned in this channel, @@ -384,15 +383,15 @@ class ChannelPinUpdateEvent(HikariEvent, marshaller.Deserializable): #: The ID of the guild where this event happened. #: Will be :obj:`~None` if this happened in a DM channel. #: - #: :type: :obj:`~hikari.snowflakes.Snowflake`, optional - guild_id: typing.Optional[snowflakes.Snowflake] = marshaller.attrib( - deserializer=snowflakes.Snowflake.deserialize, if_undefined=None, default=None + #: :type: :obj:`~hikari.entities.Snowflake`, optional + guild_id: typing.Optional[bases.Snowflake] = marshaller.attrib( + deserializer=bases.Snowflake.deserialize, if_undefined=None, default=None ) #: The ID of the channel where the message was pinned or unpinned. #: - #: :type: :obj:`~hikari.snowflakes.Snowflake` - channel_id: snowflakes.Snowflake = marshaller.attrib(deserializer=snowflakes.Snowflake.deserialize) + #: :type: :obj:`~hikari.entities.Snowflake` + channel_id: bases.Snowflake = marshaller.attrib(deserializer=bases.Snowflake.deserialize) #: The datetime of when the most recent message was pinned in this channel. #: Will be :obj:`~None` if there are no messages pinned after this change. @@ -421,7 +420,7 @@ class GuildUpdateEvent(HikariEvent, marshaller.Deserializable): @marshaller.marshallable() @attr.s(slots=True, kw_only=True) -class GuildLeaveEvent(HikariEvent, snowflakes.UniqueEntity, marshaller.Deserializable): +class GuildLeaveEvent(HikariEvent, bases.UniqueEntity, marshaller.Deserializable): """Fired when the current user leaves the guild or is kicked/banned from it. Notes @@ -432,7 +431,7 @@ class GuildLeaveEvent(HikariEvent, snowflakes.UniqueEntity, marshaller.Deseriali @marshaller.marshallable() @attr.s(slots=True, kw_only=True) -class GuildUnavailableEvent(HikariEvent, snowflakes.UniqueEntity, marshaller.Deserializable): +class GuildUnavailableEvent(HikariEvent, bases.UniqueEntity, marshaller.Deserializable): """Fired when a guild becomes temporarily unavailable due to an outage. Notes @@ -448,8 +447,8 @@ class BaseGuildBanEvent(HikariEvent, marshaller.Deserializable): #: The ID of the guild this ban is in. #: - #: :type: :obj:`~hikari.snowflakes.Snowflake` - guild_id: snowflakes.Snowflake = marshaller.attrib(deserializer=snowflakes.Snowflake.deserialize) + #: :type: :obj:`~hikari.entities.Snowflake` + guild_id: bases.Snowflake = marshaller.attrib(deserializer=bases.Snowflake.deserialize) #: The object of the user this ban targets. #: @@ -476,13 +475,13 @@ class GuildEmojisUpdateEvent(HikariEvent, marshaller.Deserializable): #: The ID of the guild this emoji was updated in. #: - #: :type: :obj:`~hikari.snowflakes.Snowflake` - guild_id: snowflakes.Snowflake = marshaller.attrib(deserializer=snowflakes.Snowflake.deserialize) + #: :type: :obj:`~hikari.entities.Snowflake` + guild_id: bases.Snowflake = marshaller.attrib(deserializer=bases.Snowflake.deserialize) #: The updated mapping of emojis by their ID. #: - #: :type: :obj:`~typing.Mapping` [ :obj:`~hikari.snowflakes.Snowflake`, :obj:`~hikari.emojis.GuildEmoji` ] - emojis: typing.Mapping[snowflakes.Snowflake, _emojis.GuildEmoji] = marshaller.attrib( + #: :type: :obj:`~typing.Mapping` [ :obj:`~hikari.entities.Snowflake`, :obj:`~hikari.emojis.GuildEmoji` ] + emojis: typing.Mapping[bases.Snowflake, _emojis.GuildEmoji] = marshaller.attrib( deserializer=lambda ems: {emoji.id: emoji for emoji in map(_emojis.GuildEmoji.deserialize, ems)} ) @@ -494,8 +493,8 @@ class GuildIntegrationsUpdateEvent(HikariEvent, marshaller.Deserializable): #: The ID of the guild the integration was updated in. #: - #: :type: :obj:`~hikari.snowflakes.Snowflake` - guild_id: snowflakes.Snowflake = marshaller.attrib(deserializer=snowflakes.Snowflake.deserialize) + #: :type: :obj:`~hikari.entities.Snowflake` + guild_id: bases.Snowflake = marshaller.attrib(deserializer=bases.Snowflake.deserialize) @marshaller.marshallable() @@ -505,8 +504,8 @@ class GuildMemberAddEvent(HikariEvent, guilds.GuildMember): #: The ID of the guild where this member was added. #: - #: :type: :obj:`~hikari.snowflakes.Snowflake` - guild_id: snowflakes.Snowflake = marshaller.attrib(deserializer=snowflakes.Snowflake.deserialize) + #: :type: :obj:`~hikari.entities.Snowflake` + guild_id: bases.Snowflake = marshaller.attrib(deserializer=bases.Snowflake.deserialize) @marshaller.marshallable() @@ -519,14 +518,14 @@ class GuildMemberUpdateEvent(HikariEvent, marshaller.Deserializable): #: The ID of the guild this member was updated in. #: - #: :type: :obj:`~hikari.snowflakes.Snowflake` - guild_id: snowflakes.Snowflake = marshaller.attrib(deserializer=snowflakes.Snowflake.deserialize) + #: :type: :obj:`~hikari.entities.Snowflake` + guild_id: bases.Snowflake = marshaller.attrib(deserializer=bases.Snowflake.deserialize) #: A sequence of the IDs of the member's current roles. #: - #: :type: :obj:`~typing.Sequence` [ :obj:`~hikari.snowflakes.Snowflake` ] - role_ids: typing.Sequence[snowflakes.Snowflake] = marshaller.attrib( - raw_name="roles", deserializer=lambda role_ids: [snowflakes.Snowflake.deserialize(rid) for rid in role_ids], + #: :type: :obj:`~typing.Sequence` [ :obj:`~hikari.entities.Snowflake` ] + role_ids: typing.Sequence[bases.Snowflake] = marshaller.attrib( + raw_name="roles", deserializer=lambda role_ids: [bases.Snowflake.deserialize(rid) for rid in role_ids], ) #: The object of the user who was updated. @@ -561,8 +560,8 @@ class GuildMemberRemoveEvent(HikariEvent, marshaller.Deserializable): #: The ID of the guild this user was removed from. #: - #: :type: :obj:`~hikari.snowflakes.Snowflake` - guild_id: snowflakes.Snowflake = marshaller.attrib(deserializer=snowflakes.Snowflake.deserialize) + #: :type: :obj:`~hikari.entities.Snowflake` + guild_id: bases.Snowflake = marshaller.attrib(deserializer=bases.Snowflake.deserialize) #: The object of the user who was removed from this guild. #: @@ -577,8 +576,8 @@ class GuildRoleCreateEvent(HikariEvent, marshaller.Deserializable): #: The ID of the guild where this role was created. #: - #: :type: :obj:`~hikari.snowflakes.Snowflake` - guild_id: snowflakes.Snowflake = marshaller.attrib(deserializer=snowflakes.Snowflake.deserialize) + #: :type: :obj:`~hikari.entities.Snowflake` + guild_id: bases.Snowflake = marshaller.attrib(deserializer=bases.Snowflake.deserialize) #: The object of the role that was created. #: @@ -593,8 +592,8 @@ class GuildRoleUpdateEvent(HikariEvent, marshaller.Deserializable): #: The ID of the guild where this role was updated. #: - #: :type: :obj:`~hikari.snowflakes.Snowflake` - guild_id: snowflakes.Snowflake = marshaller.attrib(deserializer=snowflakes.Snowflake.deserialize) + #: :type: :obj:`~hikari.entities.Snowflake` + guild_id: bases.Snowflake = marshaller.attrib(deserializer=bases.Snowflake.deserialize) #: The updated role object. #: @@ -609,13 +608,13 @@ class GuildRoleDeleteEvent(HikariEvent, marshaller.Deserializable): #: The ID of the guild where this role is being deleted. #: - #: :type: :obj:`~hikari.snowflakes.Snowflake` - guild_id: snowflakes.Snowflake = marshaller.attrib(deserializer=snowflakes.Snowflake.deserialize) + #: :type: :obj:`~hikari.entities.Snowflake` + guild_id: bases.Snowflake = marshaller.attrib(deserializer=bases.Snowflake.deserialize) #: The ID of the role being deleted. #: - #: :type: :obj:`~hikari.snowflakes.Snowflake` - role_id: snowflakes.Snowflake = marshaller.attrib(deserializer=snowflakes.Snowflake.deserialize) + #: :type: :obj:`~hikari.entities.Snowflake` + role_id: bases.Snowflake = marshaller.attrib(deserializer=bases.Snowflake.deserialize) @marshaller.marshallable() @@ -625,8 +624,8 @@ class InviteCreateEvent(HikariEvent, marshaller.Deserializable): #: The ID of the channel this invite targets. #: - #: :type: :obj:`~hikari.snowflakes.Snowflake` - channel_id: snowflakes.Snowflake = marshaller.attrib(deserializer=snowflakes.Snowflake.deserialize) + #: :type: :obj:`~hikari.entities.Snowflake` + channel_id: bases.Snowflake = marshaller.attrib(deserializer=bases.Snowflake.deserialize) #: The code that identifies this invite #: @@ -641,9 +640,9 @@ class InviteCreateEvent(HikariEvent, marshaller.Deserializable): #: The ID of the guild this invite was created in, if applicable. #: Will be :obj:`~None` for group DM invites. #: - #: :type: :obj:`~hikari.snowflakes.Snowflake`, optional - guild_id: typing.Optional[snowflakes.Snowflake] = marshaller.attrib( - deserializer=snowflakes.Snowflake.deserialize, if_undefined=None, default=None + #: :type: :obj:`~hikari.entities.Snowflake`, optional + guild_id: typing.Optional[bases.Snowflake] = marshaller.attrib( + deserializer=bases.Snowflake.deserialize, if_undefined=None, default=None ) #: The object of the user who created this invite, if applicable. @@ -702,8 +701,8 @@ class InviteDeleteEvent(HikariEvent, marshaller.Deserializable): #: The ID of the channel this ID was attached to #: - #: :type: :obj:`~hikari.snowflakes.Snowflake` - channel_id: snowflakes.Snowflake = marshaller.attrib(deserializer=snowflakes.Snowflake.deserialize) + #: :type: :obj:`~hikari.entities.Snowflake` + channel_id: bases.Snowflake = marshaller.attrib(deserializer=bases.Snowflake.deserialize) #: The code of this invite. #: @@ -713,9 +712,9 @@ class InviteDeleteEvent(HikariEvent, marshaller.Deserializable): #: The ID of the guild this invite was deleted in. #: This will be :obj:`~None` if this invite belonged to a DM channel. #: - #: :type: :obj:`~hikari.snowflakes.Snowflake`, optional - guild_id: typing.Optional[snowflakes.Snowflake] = marshaller.attrib( - deserializer=snowflakes.Snowflake.deserialize, if_undefined=None, default=None + #: :type: :obj:`~hikari.entities.Snowflake`, optional + guild_id: typing.Optional[bases.Snowflake] = marshaller.attrib( + deserializer=bases.Snowflake.deserialize, if_undefined=None, default=None ) @@ -728,7 +727,7 @@ class MessageCreateEvent(HikariEvent, messages.Message): # This is an arbitrarily partial version of `messages.Message` @marshaller.marshallable() @attr.s(slots=True, kw_only=True) -class MessageUpdateEvent(HikariEvent, snowflakes.UniqueEntity, marshaller.Deserializable): +class MessageUpdateEvent(HikariEvent, bases.UniqueEntity, marshaller.Deserializable): """Represents Message Update gateway events. Note @@ -741,14 +740,14 @@ class MessageUpdateEvent(HikariEvent, snowflakes.UniqueEntity, marshaller.Deseri #: The ID of the channel that the message was sent in. #: - #: :type: :obj:`~hikari.snowflakes.Snowflake` - channel_id: snowflakes.Snowflake = marshaller.attrib(deserializer=snowflakes.Snowflake.deserialize) + #: :type: :obj:`~hikari.entities.Snowflake` + channel_id: bases.Snowflake = marshaller.attrib(deserializer=bases.Snowflake.deserialize) #: The ID of the guild that the message was sent in. #: - #: :type: :obj:`~typing.Union` [ :obj:`~hikari.snowflakes.Snowflake`, :obj:`~hikari.unset.UNSET` ] - guild_id: typing.Union[snowflakes.Snowflake, unset.Unset] = marshaller.attrib( - deserializer=snowflakes.Snowflake.deserialize, if_undefined=unset.Unset, default=unset.UNSET + #: :type: :obj:`~typing.Union` [ :obj:`~hikari.entities.Snowflake`, :obj:`~hikari.unset.UNSET` ] + guild_id: typing.Union[bases.Snowflake, unset.Unset] = marshaller.attrib( + deserializer=bases.Snowflake.deserialize, if_undefined=unset.Unset, default=unset.UNSET ) #: The author of this message. @@ -803,30 +802,30 @@ class MessageUpdateEvent(HikariEvent, snowflakes.UniqueEntity, marshaller.Deseri #: The users the message mentions. #: - #: :type: :obj:`~typing.Union` [ :obj:`~typing.Set` [ :obj:`~hikari.snowflakes.Snowflake` ], :obj:`~hikari.unset.UNSET` ] - user_mentions: typing.Union[typing.Set[snowflakes.Snowflake], unset.Unset] = marshaller.attrib( + #: :type: :obj:`~typing.Union` [ :obj:`~typing.Set` [ :obj:`~hikari.entities.Snowflake` ], :obj:`~hikari.unset.UNSET` ] + user_mentions: typing.Union[typing.Set[bases.Snowflake], unset.Unset] = marshaller.attrib( raw_name="mentions", - deserializer=lambda user_mentions: {snowflakes.Snowflake.deserialize(u["id"]) for u in user_mentions}, + deserializer=lambda user_mentions: {bases.Snowflake.deserialize(u["id"]) for u in user_mentions}, if_undefined=unset.Unset, default=unset.UNSET, ) #: The roles the message mentions. #: - #: :type: :obj:`~typing.Union` [ :obj:`~typing.Set` [ :obj:`~hikari.snowflakes.Snowflake` ], :obj:`~hikari.unset.UNSET` ] - role_mentions: typing.Union[typing.Set[snowflakes.Snowflake], unset.Unset] = marshaller.attrib( + #: :type: :obj:`~typing.Union` [ :obj:`~typing.Set` [ :obj:`~hikari.entities.Snowflake` ], :obj:`~hikari.unset.UNSET` ] + role_mentions: typing.Union[typing.Set[bases.Snowflake], unset.Unset] = marshaller.attrib( raw_name="mention_roles", - deserializer=lambda role_mentions: {snowflakes.Snowflake.deserialize(r) for r in role_mentions}, + deserializer=lambda role_mentions: {bases.Snowflake.deserialize(r) for r in role_mentions}, if_undefined=unset.Unset, default=unset.UNSET, ) #: The channels the message mentions. #: - #: :type: :obj:`~typing.Union` [ :obj:`~typing.Set` [ :obj:`~hikari.snowflakes.Snowflake` ], :obj:`~hikari.unset.UNSET` ] - channel_mentions: typing.Union[typing.Set[snowflakes.Snowflake], unset.Unset] = marshaller.attrib( + #: :type: :obj:`~typing.Union` [ :obj:`~typing.Set` [ :obj:`~hikari.entities.Snowflake` ], :obj:`~hikari.unset.UNSET` ] + channel_mentions: typing.Union[typing.Set[bases.Snowflake], unset.Unset] = marshaller.attrib( raw_name="mention_channels", - deserializer=lambda channel_mentions: {snowflakes.Snowflake.deserialize(c["id"]) for c in channel_mentions}, + deserializer=lambda channel_mentions: {bases.Snowflake.deserialize(c["id"]) for c in channel_mentions}, if_undefined=unset.Unset, default=unset.UNSET, ) @@ -867,9 +866,9 @@ class MessageUpdateEvent(HikariEvent, snowflakes.UniqueEntity, marshaller.Deseri #: If the message was generated by a webhook, the webhook's id. #: - #: :type: :obj:`~typing.Union` [ :obj:`~hikari.snowflakes.Snowflake`, :obj:`~hikari.unset.UNSET` ] - webhook_id: typing.Union[snowflakes.Snowflake, unset.Unset] = marshaller.attrib( - deserializer=snowflakes.Snowflake.deserialize, if_undefined=unset.Unset, default=unset.UNSET + #: :type: :obj:`~typing.Union` [ :obj:`~hikari.entities.Snowflake`, :obj:`~hikari.unset.UNSET` ] + webhook_id: typing.Union[bases.Snowflake, unset.Unset] = marshaller.attrib( + deserializer=bases.Snowflake.deserialize, if_undefined=unset.Unset, default=unset.UNSET ) #: The message's type. @@ -926,20 +925,20 @@ class MessageDeleteEvent(HikariEvent, marshaller.Deserializable): #: The ID of the channel where this message was deleted. #: - #: :type: :obj:`~hikari.snowflakes.Snowflake` - channel_id: snowflakes.Snowflake = marshaller.attrib(deserializer=snowflakes.Snowflake.deserialize) + #: :type: :obj:`~hikari.entities.Snowflake` + channel_id: bases.Snowflake = marshaller.attrib(deserializer=bases.Snowflake.deserialize) #: The ID of the guild where this message was deleted. #: Will be :obj:`~None` if this message was deleted in a DM channel. #: - #: :type: :obj:`~hikari.snowflakes.Snowflake`, optional - guild_id: typing.Optional[snowflakes.Snowflake] = marshaller.attrib( - deserializer=snowflakes.Snowflake.deserialize, if_undefined=None, default=None + #: :type: :obj:`~hikari.entities.Snowflake`, optional + guild_id: typing.Optional[bases.Snowflake] = marshaller.attrib( + deserializer=bases.Snowflake.deserialize, if_undefined=None, default=None ) #: The ID of the message that was deleted. #: - #: :type: :obj:`~hikari.snowflakes.Snowflake` - message_id: snowflakes.Snowflake = marshaller.attrib(raw_name="id", deserializer=snowflakes.Snowflake.deserialize) + #: :type: :obj:`~hikari.entities.Snowflake` + message_id: bases.Snowflake = marshaller.attrib(raw_name="id", deserializer=bases.Snowflake.deserialize) @marshaller.marshallable() @@ -952,22 +951,22 @@ class MessageDeleteBulkEvent(HikariEvent, marshaller.Deserializable): #: The ID of the channel these messages have been deleted in. #: - #: :type: :obj:`~hikari.snowflakes.Snowflake` - channel_id: snowflakes.Snowflake = marshaller.attrib(deserializer=snowflakes.Snowflake.deserialize) + #: :type: :obj:`~hikari.entities.Snowflake` + channel_id: bases.Snowflake = marshaller.attrib(deserializer=bases.Snowflake.deserialize) #: The ID of the channel these messages have been deleted in. #: Will be :obj:`~None` if these messages were bulk deleted in a DM channel. #: - #: :type: :obj:`~hikari.snowflakes.Snowflake`, optional - guild_id: typing.Optional[snowflakes.Snowflake] = marshaller.attrib( - deserializer=snowflakes.Snowflake.deserialize, if_none=None + #: :type: :obj:`~hikari.entities.Snowflake`, optional + guild_id: typing.Optional[bases.Snowflake] = marshaller.attrib( + deserializer=bases.Snowflake.deserialize, if_none=None ) #: A collection of the IDs of the messages that were deleted. #: - #: :type: :obj:`~typing.Set` [ :obj:`~hikari.snowflakes.Snowflake` ] - message_ids: typing.Set[snowflakes.Snowflake] = marshaller.attrib( - raw_name="ids", deserializer=lambda msgs: {snowflakes.Snowflake.deserialize(m) for m in msgs} + #: :type: :obj:`~typing.Set` [ :obj:`~hikari.entities.Snowflake` ] + message_ids: typing.Set[bases.Snowflake] = marshaller.attrib( + raw_name="ids", deserializer=lambda msgs: {bases.Snowflake.deserialize(m) for m in msgs} ) @@ -978,25 +977,25 @@ class MessageReactionAddEvent(HikariEvent, marshaller.Deserializable): #: The ID of the user adding the reaction. #: - #: :type: :obj:`~hikari.snowflakes.Snowflake` - user_id: snowflakes.Snowflake = marshaller.attrib(deserializer=snowflakes.Snowflake.deserialize) + #: :type: :obj:`~hikari.entities.Snowflake` + user_id: bases.Snowflake = marshaller.attrib(deserializer=bases.Snowflake.deserialize) #: The ID of the channel where this reaction is being added. #: - #: :type: :obj:`~hikari.snowflakes.Snowflake` - channel_id: snowflakes.Snowflake = marshaller.attrib(deserializer=snowflakes.Snowflake.deserialize) + #: :type: :obj:`~hikari.entities.Snowflake` + channel_id: bases.Snowflake = marshaller.attrib(deserializer=bases.Snowflake.deserialize) #: The ID of the message this reaction is being added to. #: - #: :type: :obj:`~hikari.snowflakes.Snowflake` - message_id: snowflakes.Snowflake = marshaller.attrib(deserializer=snowflakes.Snowflake.deserialize) + #: :type: :obj:`~hikari.entities.Snowflake` + message_id: bases.Snowflake = marshaller.attrib(deserializer=bases.Snowflake.deserialize) #: The ID of the guild where this reaction is being added, unless this is #: happening in a DM channel. #: - #: :type: :obj:`~hikari.snowflakes.Snowflake`, optional - guild_id: typing.Optional[snowflakes.Snowflake] = marshaller.attrib( - deserializer=snowflakes.Snowflake.deserialize, if_undefined=None, default=None + #: :type: :obj:`~hikari.entities.Snowflake`, optional + guild_id: typing.Optional[bases.Snowflake] = marshaller.attrib( + deserializer=bases.Snowflake.deserialize, if_undefined=None, default=None ) #: The member object of the user who's adding this reaction, if this is @@ -1022,25 +1021,25 @@ class MessageReactionRemoveEvent(HikariEvent, marshaller.Deserializable): #: The ID of the user who is removing their reaction. #: - #: :type: :obj:`~hikari.snowflakes.Snowflake` - user_id: snowflakes.Snowflake = marshaller.attrib(deserializer=snowflakes.Snowflake.deserialize) + #: :type: :obj:`~hikari.entities.Snowflake` + user_id: bases.Snowflake = marshaller.attrib(deserializer=bases.Snowflake.deserialize) #: The ID of the channel where this reaction is being removed. #: - #: :type: :obj:`~hikari.snowflakes.Snowflake` - channel_id: snowflakes.Snowflake = marshaller.attrib(deserializer=snowflakes.Snowflake.deserialize) + #: :type: :obj:`~hikari.entities.Snowflake` + channel_id: bases.Snowflake = marshaller.attrib(deserializer=bases.Snowflake.deserialize) #: The ID of the message this reaction is being removed from. #: - #: :type: :obj:`~hikari.snowflakes.Snowflake` - message_id: snowflakes.Snowflake = marshaller.attrib(deserializer=snowflakes.Snowflake.deserialize) + #: :type: :obj:`~hikari.entities.Snowflake` + message_id: bases.Snowflake = marshaller.attrib(deserializer=bases.Snowflake.deserialize) #: The ID of the guild where this reaction is being removed, unless this is #: happening in a DM channel. #: - #: :type: :obj:`~hikari.snowflakes.Snowflake`, optional - guild_id: typing.Optional[snowflakes.Snowflake] = marshaller.attrib( - deserializer=snowflakes.Snowflake.deserialize, if_undefined=None, default=None + #: :type: :obj:`~hikari.entities.Snowflake`, optional + guild_id: typing.Optional[bases.Snowflake] = marshaller.attrib( + deserializer=bases.Snowflake.deserialize, if_undefined=None, default=None ) #: The object of the emoji being removed. @@ -1061,19 +1060,19 @@ class MessageReactionRemoveAllEvent(HikariEvent, marshaller.Deserializable): #: The ID of the channel where the targeted message is. #: - #: :type: :obj:`~hikari.snowflakes.Snowflake` - channel_id: snowflakes.Snowflake = marshaller.attrib(deserializer=snowflakes.Snowflake.deserialize) + #: :type: :obj:`~hikari.entities.Snowflake` + channel_id: bases.Snowflake = marshaller.attrib(deserializer=bases.Snowflake.deserialize) #: The ID of the message all reactions are being removed from. #: - #: :type: :obj:`~hikari.snowflakes.Snowflake` - message_id: snowflakes.Snowflake = marshaller.attrib(deserializer=snowflakes.Snowflake.deserialize) + #: :type: :obj:`~hikari.entities.Snowflake` + message_id: bases.Snowflake = marshaller.attrib(deserializer=bases.Snowflake.deserialize) #: The ID of the guild where the targeted message is, if applicable. #: - #: :type: :obj:`~hikari.snowflakes.Snowflake` - guild_id: typing.Optional[snowflakes.Snowflake] = marshaller.attrib( - deserializer=snowflakes.Snowflake.deserialize, if_undefined=None, default=None + #: :type: :obj:`~hikari.entities.Snowflake` + guild_id: typing.Optional[bases.Snowflake] = marshaller.attrib( + deserializer=bases.Snowflake.deserialize, if_undefined=None, default=None ) @@ -1087,20 +1086,20 @@ class MessageReactionRemoveEmojiEvent(HikariEvent, marshaller.Deserializable): #: The ID of the channel where the targeted message is. #: - #: :type: :obj:`~hikari.snowflakes.Snowflake` - channel_id: snowflakes.Snowflake = marshaller.attrib(deserializer=snowflakes.Snowflake.deserialize) + #: :type: :obj:`~hikari.entities.Snowflake` + channel_id: bases.Snowflake = marshaller.attrib(deserializer=bases.Snowflake.deserialize) #: The ID of the guild where the targeted message is, if applicable. #: - #: :type: :obj:`~hikari.snowflakes.Snowflake` - guild_id: typing.Optional[snowflakes.Snowflake] = marshaller.attrib( - deserializer=snowflakes.Snowflake.deserialize, if_undefined=None, default=None + #: :type: :obj:`~hikari.entities.Snowflake` + guild_id: typing.Optional[bases.Snowflake] = marshaller.attrib( + deserializer=bases.Snowflake.deserialize, if_undefined=None, default=None ) #: The ID of the message the reactions are being removed from. #: - #: :type: :obj:`~hikari.snowflakes.Snowflake` - message_id: snowflakes.Snowflake = marshaller.attrib(deserializer=snowflakes.Snowflake.deserialize) + #: :type: :obj:`~hikari.entities.Snowflake` + message_id: bases.Snowflake = marshaller.attrib(deserializer=bases.Snowflake.deserialize) #: The object of the emoji that's being removed. #: @@ -1129,21 +1128,21 @@ class TypingStartEvent(HikariEvent, marshaller.Deserializable): #: The ID of the channel this typing event is occurring in. #: - #: :type: :obj:`~hikari.snowflakes.Snowflake` - channel_id: snowflakes.Snowflake = marshaller.attrib(deserializer=snowflakes.Snowflake.deserialize) + #: :type: :obj:`~hikari.entities.Snowflake` + channel_id: bases.Snowflake = marshaller.attrib(deserializer=bases.Snowflake.deserialize) #: The ID of the guild this typing event is occurring in. #: Will be :obj:`~None` if this event is happening in a DM channel. #: - #: :type: :obj:`~hikari.snowflakes.Snowflake`, optional - guild_id: typing.Optional[snowflakes.Snowflake] = marshaller.attrib( - deserializer=snowflakes.Snowflake.deserialize, if_undefined=None, default=None + #: :type: :obj:`~hikari.entities.Snowflake`, optional + guild_id: typing.Optional[bases.Snowflake] = marshaller.attrib( + deserializer=bases.Snowflake.deserialize, if_undefined=None, default=None ) #: The ID of the user who triggered this typing event. #: - #: :type: :obj:`~hikari.snowflakes.Snowflake` - user_id: snowflakes.Snowflake = marshaller.attrib(deserializer=snowflakes.Snowflake.deserialize) + #: :type: :obj:`~hikari.entities.Snowflake` + user_id: bases.Snowflake = marshaller.attrib(deserializer=bases.Snowflake.deserialize) #: The datetime of when this typing event started. #: @@ -1195,8 +1194,8 @@ class VoiceServerUpdateEvent(HikariEvent, marshaller.Deserializable): #: The ID of the guild this voice server update is for #: - #: :type: :obj:`~hikari.snowflakes.Snowflake` - guild_id: snowflakes.Snowflake = marshaller.attrib(deserializer=snowflakes.Snowflake.deserialize) + #: :type: :obj:`~hikari.entities.Snowflake` + guild_id: bases.Snowflake = marshaller.attrib(deserializer=bases.Snowflake.deserialize) #: The uri for this voice server host. #: @@ -1214,10 +1213,10 @@ class WebhookUpdateEvent(HikariEvent, marshaller.Deserializable): #: The ID of the guild this webhook is being updated in. #: - #: :type: :obj:`~hikari.snowflakes.Snowflake` - guild_id: snowflakes.Snowflake = marshaller.attrib(deserializer=snowflakes.Snowflake.deserialize) + #: :type: :obj:`~hikari.entities.Snowflake` + guild_id: bases.Snowflake = marshaller.attrib(deserializer=bases.Snowflake.deserialize) #: The ID of the channel this webhook is being updated in. #: - #: :type: :obj:`~hikari.snowflakes.Snowflake` - channel_id: snowflakes.Snowflake = marshaller.attrib(deserializer=snowflakes.Snowflake.deserialize) + #: :type: :obj:`~hikari.entities.Snowflake` + channel_id: bases.Snowflake = marshaller.attrib(deserializer=bases.Snowflake.deserialize) diff --git a/hikari/gateway_entities.py b/hikari/gateway_entities.py index 8a59d28bfe..950b455f8c 100644 --- a/hikari/gateway_entities.py +++ b/hikari/gateway_entities.py @@ -17,21 +17,21 @@ # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . """Entities directly related to creating and managing gateway shards.""" -__all__ = ["GatewayBot", "GatewayActivity"] +__all__ = ["GatewayBot", "Activity"] import datetime import typing import attr -from hikari import entities +from hikari import bases from hikari import guilds from hikari.internal import marshaller @marshaller.marshallable() @attr.s(slots=True, kw_only=True) -class SessionStartLimit(entities.HikariEntity, marshaller.Deserializable): +class SessionStartLimit(bases.HikariEntity, marshaller.Deserializable): """Used to represent information about the current session start limits.""" #: The total number of session starts the current bot is allowed. @@ -55,7 +55,7 @@ class SessionStartLimit(entities.HikariEntity, marshaller.Deserializable): @marshaller.marshallable() @attr.s(slots=True, kw_only=True) -class GatewayBot(entities.HikariEntity, marshaller.Deserializable): +class GatewayBot(bases.HikariEntity, marshaller.Deserializable): """Used to represent gateway information for the connected bot.""" #: The WSS URL that can be used for connecting to the gateway. @@ -76,7 +76,7 @@ class GatewayBot(entities.HikariEntity, marshaller.Deserializable): @marshaller.marshallable() @attr.s(slots=True, kw_only=True) -class GatewayActivity(marshaller.Deserializable, marshaller.Serializable): +class Activity(marshaller.Deserializable, marshaller.Serializable): """An activity that the bot can set for one or more shards. This will show the activity as the bot's presence. diff --git a/hikari/guilds.py b/hikari/guilds.py index 74f5930025..5578aef9b6 100644 --- a/hikari/guilds.py +++ b/hikari/guilds.py @@ -49,12 +49,11 @@ import attr +from hikari import bases from hikari import channels as _channels from hikari import colors from hikari import emojis as _emojis -from hikari import entities from hikari import permissions as _permissions -from hikari import snowflakes from hikari import unset from hikari import users from hikari.internal import conversions @@ -199,14 +198,14 @@ class GuildVerificationLevel(enum.IntEnum): @marshaller.marshallable() @attr.s(slots=True, kw_only=True) -class GuildEmbed(entities.HikariEntity, marshaller.Deserializable): +class GuildEmbed(bases.HikariEntity, marshaller.Deserializable): """Represents a guild embed.""" #: The ID of the channel the invite for this embed targets, if enabled #: - #: :type: :obj:`~hikari.snowflakes.Snowflake`, optional - channel_id: typing.Optional[snowflakes.Snowflake] = marshaller.attrib( - deserializer=snowflakes.Snowflake.deserialize, serializer=str, if_none=None + #: :type: :obj:`~hikari.entities.Snowflake`, optional + channel_id: typing.Optional[bases.Snowflake] = marshaller.attrib( + deserializer=bases.Snowflake.deserialize, serializer=str, if_none=None ) #: Whether this embed is enabled. @@ -217,7 +216,7 @@ class GuildEmbed(entities.HikariEntity, marshaller.Deserializable): @marshaller.marshallable() @attr.s(slots=True, kw_only=True) -class GuildMember(entities.HikariEntity, marshaller.Deserializable): +class GuildMember(bases.HikariEntity, marshaller.Deserializable): """Used to represent a guild bound member.""" #: This member's user object, will be :obj:`~None` when attached to Message @@ -237,9 +236,9 @@ class GuildMember(entities.HikariEntity, marshaller.Deserializable): #: A sequence of the IDs of the member's current roles. #: - #: :type: :obj:`~typing.Sequence` [ :obj:`~hikari.snowflakes.Snowflake` ] - role_ids: typing.Sequence[snowflakes.Snowflake] = marshaller.attrib( - raw_name="roles", deserializer=lambda role_ids: [snowflakes.Snowflake.deserialize(rid) for rid in role_ids], + #: :type: :obj:`~typing.Sequence` [ :obj:`~hikari.entities.Snowflake` ] + role_ids: typing.Sequence[bases.Snowflake] = marshaller.attrib( + raw_name="roles", deserializer=lambda role_ids: [bases.Snowflake.deserialize(rid) for rid in role_ids], ) #: The datetime of when this member joined the guild they belong to. @@ -268,7 +267,7 @@ class GuildMember(entities.HikariEntity, marshaller.Deserializable): @marshaller.marshallable() @attr.s(slots=True, kw_only=True) -class PartialGuildRole(snowflakes.UniqueEntity, marshaller.Deserializable): +class PartialGuildRole(bases.UniqueEntity, marshaller.Deserializable): """Represents a partial guild bound Role object.""" #: The role's name. @@ -351,7 +350,7 @@ class ActivityType(enum.IntEnum): @marshaller.marshallable() @attr.s(slots=True, kw_only=True) -class ActivityTimestamps(entities.HikariEntity, marshaller.Deserializable): +class ActivityTimestamps(bases.HikariEntity, marshaller.Deserializable): """The datetimes for the start and/or end of an activity session.""" #: When this activity's session was started, if applicable. @@ -371,7 +370,7 @@ class ActivityTimestamps(entities.HikariEntity, marshaller.Deserializable): @marshaller.marshallable() @attr.s(slots=True, kw_only=True) -class ActivityParty(entities.HikariEntity, marshaller.Deserializable): +class ActivityParty(bases.HikariEntity, marshaller.Deserializable): """Used to represent activity groups of users.""" #: The string id of this party instance, if set. @@ -400,7 +399,7 @@ def max_size(self) -> typing.Optional[int]: @marshaller.marshallable() @attr.s(slots=True, kw_only=True) -class ActivityAssets(entities.HikariEntity, marshaller.Deserializable): +class ActivityAssets(bases.HikariEntity, marshaller.Deserializable): """Used to represent possible assets for an activity.""" #: The ID of the asset's large image, if set. @@ -426,7 +425,7 @@ class ActivityAssets(entities.HikariEntity, marshaller.Deserializable): @marshaller.marshallable() @attr.s(slots=True, kw_only=True) -class ActivitySecret(entities.HikariEntity, marshaller.Deserializable): +class ActivitySecret(bases.HikariEntity, marshaller.Deserializable): """The secrets used for interacting with an activity party.""" #: The secret used for joining a party, if applicable. @@ -473,7 +472,7 @@ class ActivityFlag(enum.IntFlag): @marshaller.marshallable() @attr.s(slots=True, kw_only=True) -class PresenceActivity(entities.HikariEntity, marshaller.Deserializable): +class PresenceActivity(bases.HikariEntity, marshaller.Deserializable): """Represents an activity that will be attached to a member's presence.""" #: The activity's name. @@ -506,9 +505,9 @@ class PresenceActivity(entities.HikariEntity, marshaller.Deserializable): #: The ID of the application this activity is for, if applicable. #: - #: :type: :obj:`~hikari.snowflakes.Snowflake`, optional - application_id: typing.Optional[snowflakes.Snowflake] = marshaller.attrib( - deserializer=snowflakes.Snowflake.deserialize, if_undefined=None, default=None + #: :type: :obj:`~hikari.entities.Snowflake`, optional + application_id: typing.Optional[bases.Snowflake] = marshaller.attrib( + deserializer=bases.Snowflake.deserialize, if_undefined=None, default=None ) #: The text that describes what the activity's target is doing, if set. @@ -583,7 +582,7 @@ class PresenceStatus(str, enum.Enum): @marshaller.marshallable() @attr.s(slots=True, kw_only=True) -class ClientStatus(entities.HikariEntity, marshaller.Deserializable): +class ClientStatus(bases.HikariEntity, marshaller.Deserializable): """The client statuses for this member.""" #: The status of the target user's desktop session. @@ -719,7 +718,7 @@ def default_avatar(self) -> typing.Union[int, unset.Unset]: @marshaller.marshallable() @attr.s(slots=True, kw_only=True) -class GuildMemberPresence(entities.HikariEntity, marshaller.Deserializable): +class GuildMemberPresence(bases.HikariEntity, marshaller.Deserializable): """Used to represent a guild member's presence.""" #: The object of the user who this presence is for, only `id` is guaranteed @@ -732,15 +731,15 @@ class GuildMemberPresence(entities.HikariEntity, marshaller.Deserializable): #: A sequence of the ids of the user's current roles in the guild this #: presence belongs to. #: - #: :type: :obj:`~typing.Sequence` [ :obj:`~hikari.snowflakes.Snowflake` ] - role_ids: typing.Sequence[snowflakes.Snowflake] = marshaller.attrib( - raw_name="roles", deserializer=lambda roles: [snowflakes.Snowflake.deserialize(rid) for rid in roles], + #: :type: :obj:`~typing.Sequence` [ :obj:`~hikari.entities.Snowflake` ] + role_ids: typing.Sequence[bases.Snowflake] = marshaller.attrib( + raw_name="roles", deserializer=lambda roles: [bases.Snowflake.deserialize(rid) for rid in roles], ) #: The ID of the guild this presence belongs to. #: - #: :type: :obj:`~hikari.snowflakes.Snowflake` - guild_id: snowflakes.Snowflake = marshaller.attrib(deserializer=snowflakes.Snowflake.deserialize) + #: :type: :obj:`~hikari.entities.Snowflake` + guild_id: bases.Snowflake = marshaller.attrib(deserializer=bases.Snowflake.deserialize) #: This user's current status being displayed by the client. #: @@ -789,7 +788,7 @@ class IntegrationExpireBehaviour(enum.IntEnum): @marshaller.marshallable() @attr.s(slots=True, kw_only=True) -class IntegrationAccount(entities.HikariEntity, marshaller.Deserializable): +class IntegrationAccount(bases.HikariEntity, marshaller.Deserializable): """An account that's linked to an integration.""" #: The string ID of this (likely) third party account. @@ -805,7 +804,7 @@ class IntegrationAccount(entities.HikariEntity, marshaller.Deserializable): @marshaller.marshallable() @attr.s(slots=True, kw_only=True) -class PartialGuildIntegration(snowflakes.UniqueEntity, marshaller.Deserializable): +class PartialGuildIntegration(bases.UniqueEntity, marshaller.Deserializable): """A partial representation of an integration, found in audit logs.""" #: The name of this integration. @@ -826,7 +825,7 @@ class PartialGuildIntegration(snowflakes.UniqueEntity, marshaller.Deserializable @marshaller.marshallable() @attr.s(slots=True, kw_only=True) -class GuildIntegration(snowflakes.UniqueEntity, marshaller.Deserializable): +class GuildIntegration(bases.UniqueEntity, marshaller.Deserializable): """Represents a guild integration object.""" #: Whether this integration is enabled. @@ -841,8 +840,8 @@ class GuildIntegration(snowflakes.UniqueEntity, marshaller.Deserializable): #: The ID of the managed role used for this integration's subscribers. #: - #: :type: :obj:`~hikari.snowflakes.Snowflake` - role_id: typing.Optional[snowflakes.Snowflake] = marshaller.attrib(deserializer=snowflakes.Snowflake.deserialize) + #: :type: :obj:`~hikari.entities.Snowflake` + role_id: typing.Optional[bases.Snowflake] = marshaller.attrib(deserializer=bases.Snowflake.deserialize) #: Whether users under this integration are allowed to use it's custom #: emojis. @@ -881,7 +880,7 @@ class GuildIntegration(snowflakes.UniqueEntity, marshaller.Deserializable): @marshaller.marshallable() @attr.s(slots=True, kw_only=True) -class GuildMemberBan(entities.HikariEntity, marshaller.Deserializable): +class GuildMemberBan(bases.HikariEntity, marshaller.Deserializable): """Used to represent guild bans.""" #: The reason for this ban, will be :obj:`~None` if no reason was given. @@ -897,7 +896,7 @@ class GuildMemberBan(entities.HikariEntity, marshaller.Deserializable): @marshaller.marshallable() @attr.s(slots=True, kw_only=True) -class UnavailableGuild(snowflakes.UniqueEntity, marshaller.Deserializable): +class UnavailableGuild(bases.UniqueEntity, marshaller.Deserializable): """An unavailable guild object, received during gateway events such as READY. An unavailable guild cannot be interacted with, and most information may @@ -916,7 +915,7 @@ def is_unavailable(self) -> bool: # noqa: D401 @marshaller.marshallable() @attr.s(slots=True, kw_only=True) -class PartialGuild(snowflakes.UniqueEntity, marshaller.Deserializable): +class PartialGuild(bases.UniqueEntity, marshaller.Deserializable): """Base object for any partial guild objects.""" #: The name of the guild. @@ -993,8 +992,8 @@ class GuildPreview(PartialGuild): #: The emojis that this guild provides, represented as a mapping of ID to #: emoji object. #: - #: :type: :obj:`~typing.Mapping` [ :obj:`~hikari.snowflakes.Snowflake`, :obj:`~hikari.emojis.GuildEmoji` ] - emojis: typing.Mapping[snowflakes.Snowflake, _emojis.GuildEmoji] = marshaller.attrib( + #: :type: :obj:`~typing.Mapping` [ :obj:`~hikari.entities.Snowflake`, :obj:`~hikari.emojis.GuildEmoji` ] + emojis: typing.Mapping[bases.Snowflake, _emojis.GuildEmoji] = marshaller.attrib( deserializer=lambda emojis: {e.id: e for e in map(_emojis.GuildEmoji.deserialize, emojis)}, ) @@ -1107,8 +1106,8 @@ class Guild(PartialGuild): #: The ID of the owner of this guild. #: - #: :type: :obj:`~hikari.snowflakes.Snowflake` - owner_id: snowflakes.Snowflake = marshaller.attrib(deserializer=snowflakes.Snowflake.deserialize) + #: :type: :obj:`~hikari.entities.Snowflake` + owner_id: bases.Snowflake = marshaller.attrib(deserializer=bases.Snowflake.deserialize) #: The guild level permissions that apply to the bot user, #: Will be :obj:`~None` when this object is retrieved through a REST request @@ -1127,9 +1126,9 @@ class Guild(PartialGuild): #: The ID for the channel that AFK voice users get sent to, if set for the #: guild. #: - #: :type: :obj:`~hikari.snowflakes.Snowflake`, optional - afk_channel_id: typing.Optional[snowflakes.Snowflake] = marshaller.attrib( - deserializer=snowflakes.Snowflake.deserialize, if_none=None + #: :type: :obj:`~hikari.entities.Snowflake`, optional + afk_channel_id: typing.Optional[bases.Snowflake] = marshaller.attrib( + deserializer=bases.Snowflake.deserialize, if_none=None ) #: How long a voice user has to be AFK for before they are classed as being @@ -1156,9 +1155,9 @@ class Guild(PartialGuild): #: #: Will be :obj:`~None` if invites are disabled for this guild's embed. #: - #: :type: :obj:`~hikari.snowflakes.Snowflake`, optional - embed_channel_id: typing.Optional[snowflakes.Snowflake] = marshaller.attrib( - deserializer=snowflakes.Snowflake.deserialize, if_undefined=None, if_none=None, default=None + #: :type: :obj:`~hikari.entities.Snowflake`, optional + embed_channel_id: typing.Optional[bases.Snowflake] = marshaller.attrib( + deserializer=bases.Snowflake.deserialize, if_undefined=None, if_none=None, default=None ) #: The verification level required for a user to participate in this guild. @@ -1182,16 +1181,16 @@ class Guild(PartialGuild): #: The roles in this guild, represented as a mapping of ID to role object. #: - #: :type: :obj:`~typing.Mapping` [ :obj:`~hikari.snowflakes.Snowflake`, :obj:`~GuildRole` ] - roles: typing.Mapping[snowflakes.Snowflake, GuildRole] = marshaller.attrib( + #: :type: :obj:`~typing.Mapping` [ :obj:`~hikari.entities.Snowflake`, :obj:`~GuildRole` ] + roles: typing.Mapping[bases.Snowflake, GuildRole] = marshaller.attrib( deserializer=lambda roles: {r.id: r for r in map(GuildRole.deserialize, roles)}, ) #: The emojis that this guild provides, represented as a mapping of ID to #: emoji object. #: - #: :type: :obj:`~typing.Mapping` [ :obj:`~hikari.snowflakes.Snowflake`, :obj:`~hikari.emojis.GuildEmoji` ] - emojis: typing.Mapping[snowflakes.Snowflake, _emojis.GuildEmoji] = marshaller.attrib( + #: :type: :obj:`~typing.Mapping` [ :obj:`~hikari.entities.Snowflake`, :obj:`~hikari.emojis.GuildEmoji` ] + emojis: typing.Mapping[bases.Snowflake, _emojis.GuildEmoji] = marshaller.attrib( deserializer=lambda emojis: {e.id: e for e in map(_emojis.GuildEmoji.deserialize, emojis)}, ) @@ -1203,9 +1202,9 @@ class Guild(PartialGuild): #: The ID of the application that created this guild, if it was created by #: a bot. If not, this is always :obj:`~None`. #: - #: :type: :obj:`~hikari.snowflakes.Snowflake`, optional - application_id: typing.Optional[snowflakes.Snowflake] = marshaller.attrib( - deserializer=snowflakes.Snowflake.deserialize, if_none=None + #: :type: :obj:`~hikari.entities.Snowflake`, optional + application_id: typing.Optional[bases.Snowflake] = marshaller.attrib( + deserializer=bases.Snowflake.deserialize, if_none=None ) #: Whether the guild is unavailable or not. @@ -1234,17 +1233,17 @@ class Guild(PartialGuild): #: The channel ID that the widget's generated invite will send the user to, #: if enabled. If this information is unavailable, this will be :obj:`~None`. #: - #: :type: :obj:`~hikari.snowflakes.Snowflake`, optional - widget_channel_id: typing.Optional[snowflakes.Snowflake] = marshaller.attrib( - deserializer=snowflakes.Snowflake.deserialize, if_undefined=None, if_none=None, default=None + #: :type: :obj:`~hikari.entities.Snowflake`, optional + widget_channel_id: typing.Optional[bases.Snowflake] = marshaller.attrib( + deserializer=bases.Snowflake.deserialize, if_undefined=None, if_none=None, default=None ) #: The ID of the system channel (where welcome messages and Nitro boost #: messages are sent), or :obj:`~None` if it is not enabled. #: - #: :type: :obj:`~hikari.snowflakes.Snowflake`, optional - system_channel_id: typing.Optional[snowflakes.Snowflake] = marshaller.attrib( - if_none=None, deserializer=snowflakes.Snowflake.deserialize + #: :type: :obj:`~hikari.entities.Snowflake`, optional + system_channel_id: typing.Optional[bases.Snowflake] = marshaller.attrib( + if_none=None, deserializer=bases.Snowflake.deserialize ) #: Flags for the guild system channel to describe which notification @@ -1259,9 +1258,9 @@ class Guild(PartialGuild): #: If the :obj:`~GuildFeature.PUBLIC` feature is not defined, then this is #: :obj:`~None`. #: - #: :type: :obj:`~hikari.snowflakes.Snowflake`, optional - rules_channel_id: typing.Optional[snowflakes.Snowflake] = marshaller.attrib( - if_none=None, deserializer=snowflakes.Snowflake.deserialize + #: :type: :obj:`~hikari.entities.Snowflake`, optional + rules_channel_id: typing.Optional[bases.Snowflake] = marshaller.attrib( + if_none=None, deserializer=bases.Snowflake.deserialize ) #: The date and time that the bot user joined this guild. @@ -1315,8 +1314,8 @@ class Guild(PartialGuild): #: representation. If you need complete accurate information, you should #: query the members using the appropriate API call instead. #: - #: :type: :obj:`~typing.Mapping` [ :obj:`~hikari.snowflakes.Snowflake`, :obj:`~GuildMember` ], optional - members: typing.Optional[typing.Mapping[snowflakes.Snowflake, GuildMember]] = marshaller.attrib( + #: :type: :obj:`~typing.Mapping` [ :obj:`~hikari.entities.Snowflake`, :obj:`~GuildMember` ], optional + members: typing.Optional[typing.Mapping[bases.Snowflake, GuildMember]] = marshaller.attrib( deserializer=lambda members: {m.user.id: m for m in map(GuildMember.deserialize, members)}, if_undefined=None, default=None, @@ -1337,8 +1336,8 @@ class Guild(PartialGuild): #: To retrieve a list of channels in any other case, you should make an #: appropriate API call to retrieve this information. #: - #: :type: :obj:`~typing.Mapping` [ :obj:`~hikari.snowflakes.Snowflake`, :obj:`~hikari.channels.GuildChannel` ], optional - channels: typing.Optional[typing.Mapping[snowflakes.Snowflake, _channels.GuildChannel]] = marshaller.attrib( + #: :type: :obj:`~typing.Mapping` [ :obj:`~hikari.entities.Snowflake`, :obj:`~hikari.channels.GuildChannel` ], optional + channels: typing.Optional[typing.Mapping[bases.Snowflake, _channels.GuildChannel]] = marshaller.attrib( deserializer=lambda guild_channels: {c.id: c for c in map(_channels.deserialize_channel, guild_channels)}, if_undefined=None, default=None, @@ -1360,8 +1359,8 @@ class Guild(PartialGuild): #: To retrieve a list of presences in any other case, you should make an #: appropriate API call to retrieve this information. #: - #: :type: :obj:`~typing.Mapping` [ :obj:`~hikari.snowflakes.Snowflake`, :obj:`~GuildMemberPresence` ], optional - presences: typing.Optional[typing.Mapping[snowflakes.Snowflake, GuildMemberPresence]] = marshaller.attrib( + #: :type: :obj:`~typing.Mapping` [ :obj:`~hikari.entities.Snowflake`, :obj:`~GuildMemberPresence` ], optional + presences: typing.Optional[typing.Mapping[bases.Snowflake, GuildMemberPresence]] = marshaller.attrib( deserializer=lambda presences: {p.user.id: p for p in map(GuildMemberPresence.deserialize, presences)}, if_undefined=None, default=None, @@ -1437,9 +1436,9 @@ class Guild(PartialGuild): #: ``features`` for this guild. For all other purposes, it should be #: considered to be :obj:`~None`. #: - #: :type: :obj:`~hikari.snowflakes.Snowflake`, optional - public_updates_channel_id: typing.Optional[snowflakes.Snowflake] = marshaller.attrib( - if_none=None, deserializer=snowflakes.Snowflake.deserialize + #: :type: :obj:`~hikari.entities.Snowflake`, optional + public_updates_channel_id: typing.Optional[bases.Snowflake] = marshaller.attrib( + if_none=None, deserializer=bases.Snowflake.deserialize ) def format_splash_url(self, fmt: str = "png", size: int = 4096) -> typing.Optional[str]: diff --git a/hikari/intents.py b/hikari/intents.py new file mode 100644 index 0000000000..e66fd619f6 --- /dev/null +++ b/hikari/intents.py @@ -0,0 +1,145 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# Copyright © Nekoka.tt 2019-2020 +# +# This file is part of Hikari. +# +# Hikari is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Hikari is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Hikari. If not, see . +"""Shard intents for controlling which events the application receives.""" +__all__ = ["Intent"] + +import enum + +from hikari.internal import more_enums + + +@enum.unique +class Intent(more_enums.FlagMixin, enum.IntFlag): + """Represents an intent on the gateway. + + This is a bitfield representation of all the categories of event + that you wish to receive. + + Any events not in an intent category will be fired regardless of what + intents you provide. + + Warnings + -------- + If you are using the V7 Gateway, you will be REQUIRED to provide some form + of intent value when you connect. Failure to do so may result in immediate + termination of the session server-side. + + Notes + ----- + Discord now places limits on certain events you can receive without + whitelisting your bot first. On the ``Bot`` tab in the developer's portal + for your bot, you should now have the option to enable functionality + for receiving these events. + + If you attempt to request an intent type that you have not whitelisted + your bot for, you will be disconnected on startup with a ``4014`` closure + code. + """ + + #: Subscribes to the following events: + #: * GUILD_CREATE + #: * GUILD_UPDATE + #: * GUILD_DELETE + #: * GUILD_ROLE_CREATE + #: * GUILD_ROLE_UPDATE + #: * GUILD_ROLE_DELETE + #: * CHANNEL_CREATE + #: * CHANNEL_UPDATE + #: * CHANNEL_DELETE + #: * CHANNEL_PINS_UPDATE + GUILDS = 1 << 0 + + #: Subscribes to the following events: + #: * GUILD_MEMBER_ADD + #: * GUILD_MEMBER_UPDATE + #: * GUILD_MEMBER_REMOVE + #: + #: Warnings + #: -------- + #: This intent is privileged, and requires enabling/whitelisting to use. + GUILD_MEMBERS = 1 << 1 + + #: Subscribes to the following events: + #: * GUILD_BAN_ADD + #: * GUILD_BAN_REMOVE + GUILD_BANS = 1 << 2 + + #: Subscribes to the following events: + #: * GUILD_EMOJIS_UPDATE + GUILD_EMOJIS = 1 << 3 + + #: Subscribes to the following events: + #: * GUILD_INTEGRATIONS_UPDATE + GUILD_INTEGRATIONS = 1 << 4 + + #: Subscribes to the following events: + #: * WEBHOOKS_UPDATE + GUILD_WEBHOOKS = 1 << 5 + + #: Subscribes to the following events: + #: * INVITE_CREATE + #: * INVITE_DELETE + GUILD_INVITES = 1 << 6 + + #: Subscribes to the following events: + #: * VOICE_STATE_UPDATE + GUILD_VOICE_STATES = 1 << 7 + + #: Subscribes to the following events: + #: * PRESENCE_UPDATE + #: + #: Warnings + #: -------- + #: This intent is privileged, and requires enabling/whitelisting to use. + GUILD_PRESENCES = 1 << 8 + + #: Subscribes to the following events: + #: * MESSAGE_CREATE + #: * MESSAGE_UPDATE + #: * MESSAGE_DELETE + #: * MESSAGE_BULK + GUILD_MESSAGES = 1 << 9 + + #: Subscribes to the following events: + #: * MESSAGE_REACTION_ADD + #: * MESSAGE_REACTION_REMOVE + #: * MESSAGE_REACTION_REMOVE_ALL + #: * MESSAGE_REACTION_REMOVE_EMOJI + GUILD_MESSAGE_REACTIONS = 1 << 10 + + #: Subscribes to the following events: + #: * TYPING_START + GUILD_MESSAGE_TYPING = 1 << 11 + + #: Subscribes to the following events: + #: * CHANNEL_CREATE + #: * MESSAGE_CREATE + #: * MESSAGE_UPDATE + #: * MESSAGE_DELETE + DIRECT_MESSAGES = 1 << 12 + + #: Subscribes to the following events: + #: * MESSAGE_REACTION_ADD + #: * MESSAGE_REACTION_REMOVE + #: * MESSAGE_REACTION_REMOVE_ALL + DIRECT_MESSAGE_REACTIONS = 1 << 13 + + #: Subscribes to the following events + #: * TYPING_START + DIRECT_MESSAGE_TYPING = 1 << 14 diff --git a/hikari/internal/allowed_mentions.py b/hikari/internal/allowed_mentions.py index 549da6dc4a..7121a9df90 100644 --- a/hikari/internal/allowed_mentions.py +++ b/hikari/internal/allowed_mentions.py @@ -25,8 +25,8 @@ import typing +from hikari import bases from hikari import guilds -from hikari import snowflakes from hikari import users from hikari.internal import assertions from hikari.internal import more_collections @@ -34,8 +34,8 @@ def generate_allowed_mentions( mentions_everyone: bool, - user_mentions: typing.Union[typing.Collection[snowflakes.HashableT[users.User]], bool], - role_mentions: typing.Union[typing.Collection[snowflakes.HashableT[guilds.GuildRole]], bool], + user_mentions: typing.Union[typing.Collection[bases.Hashable[users.User]], bool], + role_mentions: typing.Union[typing.Collection[bases.Hashable[guilds.GuildRole]], bool], ) -> typing.Dict[str, typing.Sequence[str]]: """Generate an allowed mentions object based on input mention rules. @@ -44,11 +44,11 @@ def generate_allowed_mentions( mentions_everyone : :obj:`~bool` Whether ``@everyone`` and ``@here`` mentions should be resolved by discord and lead to actual pings. - user_mentions : :obj:`~typing.Union` [ :obj:`~typing.Collection` [ :obj:`~typing.Union` [ :obj:`~hikari.users.User`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ], :obj:`~bool` ] + user_mentions : :obj:`~typing.Union` [ :obj:`~typing.Collection` [ :obj:`~typing.Union` [ :obj:`~hikari.users.User`, :obj:`~hikari.entities.Snowflake`, :obj:`~int` ], :obj:`~bool` ] Either an array of user objects/IDs to allow mentions for, :obj:`~True` to allow all user mentions or :obj:`~False` to block all user mentions from resolving. - role_mentions : :obj:`~typing.Union` [ :obj:`~typing.Collection` [ :obj:`~typing.Union` [ :obj:`~hikari.guilds.GuildRole`, :obj:`~hikari.snowflakes.Snowflake`, :obj:`~int` ] ], :obj:`~bool` ] + role_mentions : :obj:`~typing.Union` [ :obj:`~typing.Collection` [ :obj:`~typing.Union` [ :obj:`~hikari.guilds.GuildRole`, :obj:`~hikari.entities.Snowflake`, :obj:`~int` ] ], :obj:`~bool` ] Either an array of guild role objects/IDs to allow mentions for, :obj:`~True` to allow all role mentions or :obj:`~False` to block all role mentions from resolving. @@ -61,7 +61,7 @@ def generate_allowed_mentions( Raises ------ :obj:`~ValueError` - If more than 100 unique objects/snowflakes are passed for + If more than 100 unique objects/entities are passed for ``role_mentions`` or ``user_mentions. """ parsed_mentions = [] @@ -76,7 +76,7 @@ def generate_allowed_mentions( allowed_mentions["users"] = list( # dict.fromkeys is used to remove duplicate entries that would cause discord to return an error. dict.fromkeys( - str(user.id if isinstance(user, snowflakes.UniqueEntity) else int(user)) + str(user.id if isinstance(user, bases.UniqueEntity) else int(user)) for user in user_mentions or more_collections.EMPTY_SEQUENCE ) ) @@ -89,7 +89,7 @@ def generate_allowed_mentions( allowed_mentions["roles"] = list( # dict.fromkeys is used to remove duplicate entries that would cause discord to return an error. dict.fromkeys( - str(role.id if isinstance(role, snowflakes.UniqueEntity) else int(role)) + str(role.id if isinstance(role, bases.UniqueEntity) else int(role)) for role in role_mentions or more_collections.EMPTY_SEQUENCE ) ) diff --git a/hikari/internal/marshaller.py b/hikari/internal/marshaller.py index 8d1fb73193..40492f95c6 100644 --- a/hikari/internal/marshaller.py +++ b/hikari/internal/marshaller.py @@ -43,6 +43,7 @@ import attr from hikari.internal import assertions +from hikari.internal import more_typing _RAW_NAME_ATTR: typing.Final[str] = __name__ + "_RAW_NAME" _SERIALIZER_ATTR: typing.Final[str] = __name__ + "_SERIALIZER" @@ -52,10 +53,6 @@ _IF_NONE: typing.Final[str] = __name__ + "_IF_NONE" _PASSED_THROUGH_SINGLETONS: typing.Final[typing.Sequence[bool]] = [False, True, None] RAISE: typing.Final[typing.Any] = object() - -T_contra = typing.TypeVar("T_contra", contravariant=True) -T_co = typing.TypeVar("T_co", covariant=True) - EntityT = typing.TypeVar("EntityT", contravariant=True) @@ -229,7 +226,7 @@ def __init__( class _EntityDescriptor: __slots__ = ("entity_type", "attribs") - def __init__(self, entity_type: typing.Type, attribs: typing.Collection[_AttributeDescriptor],) -> None: + def __init__(self, entity_type: typing.Type, attribs: typing.Collection[_AttributeDescriptor]) -> None: self.entity_type = entity_type self.attribs = tuple(attribs) @@ -256,7 +253,7 @@ def _construct_attribute_descriptor(field: attr.Attribute) -> _AttributeDescript ) -def _construct_entity_descriptor(entity: typing.Any): +def _construct_entity_descriptor(entity: typing.Any) -> _EntityDescriptor: assertions.assert_that( hasattr(entity, "__attrs_attrs__"), f"{entity.__module__}.{entity.__qualname__} is not an attr class", @@ -275,6 +272,8 @@ class HikariEntityMarshaller: descriptor. """ + __slots__ = ("_registered_entities",) + def __init__(self) -> None: self._registered_entities: typing.MutableMapping[typing.Type, _EntityDescriptor] = {} @@ -301,7 +300,7 @@ def register(self, cls: typing.Type[EntityT]) -> typing.Type[EntityT]: self._registered_entities[cls] = entity_descriptor return cls - def deserialize(self, raw_data: typing.Mapping[str, typing.Any], target_type: typing.Type[EntityT]) -> EntityT: + def deserialize(self, raw_data: more_typing.JSONObject, target_type: typing.Type[EntityT]) -> EntityT: """Deserialize a given raw data item into the target type. Parameters @@ -374,7 +373,7 @@ def deserialize(self, raw_data: typing.Mapping[str, typing.Any], target_type: ty return target_type(**kwargs) - def serialize(self, obj: typing.Optional[typing.Any]) -> typing.Optional[typing.Mapping[str, typing.Any]]: + def serialize(self, obj: typing.Optional[typing.Any]) -> more_typing.NullableJSONObject: """Serialize a given entity into a raw data item. Parameters @@ -463,7 +462,7 @@ class Deserializable: __slots__ = () @classmethod - def deserialize(cls: typing.Type[T_contra], payload: typing.Any) -> T_contra: + def deserialize(cls: typing.Type[more_typing.T_contra], payload: more_typing.JSONType) -> more_typing.T_contra: """Deserialize the given payload into the object. Parameters @@ -479,6 +478,6 @@ class Serializable: __slots__ = () - def serialize(self: T_co) -> typing.Any: + def serialize(self: more_typing.T_contra) -> more_typing.JSONType: """Serialize this instance into a naive value.""" return HIKARI_ENTITY_MARSHALLER.serialize(self) diff --git a/hikari/internal/more_asyncio.py b/hikari/internal/more_asyncio.py index a581fe0b3a..4bd02e05dc 100644 --- a/hikari/internal/more_asyncio.py +++ b/hikari/internal/more_asyncio.py @@ -22,99 +22,21 @@ """ from __future__ import annotations -__all__ = ["Future", "Task", "completed_future"] +__all__ = ["completed_future"] import asyncio -import contextvars import typing -T = typing.TypeVar("T") -T_co = typing.TypeVar("T_co") - -try: - raise Exception -except Exception as ex: # pylint:disable=broad-except - tb = ex.__traceback__ - StackFrameType = type(tb.tb_frame) - - -# pylint:disable=unused-variable -@typing.runtime_checkable -class Future(typing.Protocol[T]): - """Typed protocol representation of an :obj:`~asyncio.Future`. - - You should consult the documentation for :obj:`~asyncio.Future` for usage. - """ - - def result(self) -> T: - """See :meth:`asyncio.Future.result`.""" - - def set_result(self, result: T, /) -> None: - """See :meth:`asyncio.Future.set_result`.""" - - def set_exception(self, exception: Exception, /) -> None: - """See :meth:`asyncio.Future.set_exception`.""" - - def done(self) -> bool: - """See :meth:`asyncio.Future.done`.""" - - def cancelled(self) -> bool: - """See :meth:`asyncio.Future.cancelled`.""" - - def add_done_callback( - self, callback: typing.Callable[[Future[T]], None], /, *, context: typing.Optional[contextvars.Context], - ) -> None: - """See :meth:`asyncio.Future.add_done_callback`.""" - - def remove_done_callback(self, callback: typing.Callable[[Future[T]], None], /) -> None: - """See :meth:`asyncio.Future.remove_done_callback`.""" - - def cancel(self) -> bool: - """See :meth:`asyncio.Future.cancel`.""" - - def exception(self) -> typing.Optional[Exception]: - """See :meth:`asyncio.Future.exception`.""" - - def get_loop(self) -> asyncio.AbstractEventLoop: - """See :meth:`asyncio.Future.get_loop`.""" - - def __await__(self) -> typing.Coroutine[None, None, T]: - ... - - -# pylint:enable=unused-variable - - -# pylint:disable=unused-variable -class Task(Future[T]): - """Typed protocol representation of an :obj:`~asyncio.Task`. - - You should consult the documentation for :obj:`~asyncio.Task` for usage. - """ - - def get_stack(self, *, limit: typing.Optional[int] = None) -> typing.Sequence[StackFrameType]: - """See :meth:`asyncio.Task.get_stack`.""" - - def print_stack(self, *, limit: typing.Optional[int] = None, file: typing.Optional[typing.IO] = None) -> None: - """See :meth:`asyncio.Task.print_stack`.""" - - def get_name(self) -> str: - """See :meth:`asyncio.Task.get_name`.""" - - def set_name(self, value: str, /) -> None: - """See :meth:`asyncio.Task.set_name`.""" - - -# pylint:enable=unused-variable +from hikari.internal import more_typing @typing.overload -def completed_future() -> Future[None]: +def completed_future() -> more_typing.Future[None]: """Return a completed future with no result.""" @typing.overload -def completed_future(result: T, /) -> Future[T]: +def completed_future(result: more_typing.T_contra, /) -> more_typing.Future[more_typing.T_contra]: """Return a completed future with the given value as the result.""" @@ -137,15 +59,15 @@ def completed_future(result=None, /): def wait( - aws, *, timeout=None, return_when=asyncio.ALL_COMPLETED -) -> typing.Coroutine[typing.Any, typing.Any, typing.Tuple[typing.Set[Future], typing.Set[Future]]]: + aws: typing.Union[more_typing.Coroutine, typing.Awaitable], *, timeout=None, return_when=asyncio.ALL_COMPLETED +) -> more_typing.Coroutine[typing.Tuple[typing.Set[more_typing.Future], typing.Set[more_typing.Future]]]: """Run awaitable objects in the aws set concurrently. This blocks until the condition specified by `return_value`. Returns ------- - :obj:`~typing.Coroutine` [ :obj:`~typing.Any`, :obj:`~typing.Any`, :obj:`~typing.Tuple` [ :obj:`~typing.Set` [ :obj:`~Future` ], :obj:`~typing.Set` [ :obj:`~Future` ] ] ] + :obj:`~typing.Tuple` with two :obj:`~typing.Set` of futures. The coroutine returned by :obj:`~asyncio.wait` of two sets of Tasks/Futures (done, pending). """ diff --git a/hikari/internal/more_collections.py b/hikari/internal/more_collections.py index d3baf0262b..ea1263de09 100644 --- a/hikari/internal/more_collections.py +++ b/hikari/internal/more_collections.py @@ -35,18 +35,18 @@ import weakref -T = typing.TypeVar("T") -K = typing.TypeVar("K", bound=typing.Hashable) -V = typing.TypeVar("V") +_T = typing.TypeVar("_T") +_K = typing.TypeVar("_K", bound=typing.Hashable) +_V = typing.TypeVar("_V") -EMPTY_SEQUENCE: typing.Final[typing.Sequence[T]] = tuple() -EMPTY_SET: typing.Final[typing.AbstractSet[T]] = frozenset() -EMPTY_COLLECTION: typing.Final[typing.Collection[T]] = tuple() -EMPTY_DICT: typing.Final[typing.Mapping[K, V]] = types.MappingProxyType({}) -EMPTY_GENERATOR_EXPRESSION: typing.Final[typing.Iterator[T]] = (_ for _ in EMPTY_COLLECTION) +EMPTY_SEQUENCE: typing.Final[typing.Sequence[_T]] = tuple() +EMPTY_SET: typing.Final[typing.AbstractSet[_T]] = frozenset() +EMPTY_COLLECTION: typing.Final[typing.Collection[_T]] = tuple() +EMPTY_DICT: typing.Final[typing.Mapping[_K, _V]] = types.MappingProxyType({}) +EMPTY_GENERATOR_EXPRESSION: typing.Final[typing.Iterator[_T]] = (_ for _ in EMPTY_COLLECTION) -class WeakKeyDictionary(typing.Generic[K, V], weakref.WeakKeyDictionary, typing.MutableMapping[K, V]): +class WeakKeyDictionary(typing.Generic[_K, _V], weakref.WeakKeyDictionary, typing.MutableMapping[_K, _V]): """A dictionary that has weak references to the keys. This is a type-safe version of :obj:`~weakref.WeakKeyDictionary` which diff --git a/hikari/internal/more_typing.py b/hikari/internal/more_typing.py new file mode 100644 index 0000000000..f5790d3ac8 --- /dev/null +++ b/hikari/internal/more_typing.py @@ -0,0 +1,185 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# Copyright © Nekoka.tt 2019-2020 +# +# This file is part of Hikari. +# +# Hikari is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Hikari is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Hikari. If not, see . +"""Various reusable type-hints for this library.""" +# pylint:disable=unused-variable +from __future__ import annotations + +# Don't export anything. +__all__ = [] + +import asyncio +import contextvars + +# Hide any imports; this encourages any uses of this to use the typing module +# for regular stuff rather than relying on it being in here as well. +from types import FrameType as _FrameType +from typing import Any as _Any +from typing import AnyStr as _AnyStr +from typing import Callable as _Callable +from typing import Coroutine as _Coroutine +from typing import IO as _IO +from typing import Mapping as _Mapping +from typing import Optional as _Optional +from typing import Protocol as _Protocol +from typing import runtime_checkable as _runtime_checkable +from typing import Sequence as _Sequence +from typing import TypeVar as _TypeVar +from typing import Union as _Union + +T_contra = _TypeVar("T_contra", contravariant=True) +# noinspection PyShadowingBuiltins +T_co = _TypeVar("T_co", covariant=True) + +########################## +# HTTP TYPE HINT HELPERS # +########################## + +#: Any JSON type. +JSONType = _Union[ + _Mapping[str, "NullableJSONType"], _Sequence["NullableJSONType"], _AnyStr, int, float, bool, +] + +#: Any JSON type, including ``null``. +NullableJSONType = _Optional[JSONType] + +#: A mapping produced from a JSON object. +JSONObject = _Mapping[str, NullableJSONType] + +#: A mapping produced from a JSON object that may or may not be present. +NullableJSONObject = _Optional[JSONObject] + +#: A sequence produced from a JSON array. +JSONArray = _Sequence[NullableJSONType] + +#: A sequence produced from a JSON array that may or may not be present. +NullableJSONArray = _Optional[JSONArray] + +#: HTTP headers. +Headers = _Mapping[str, _Union[_Sequence[str], str]] + +############################# +# ASYNCIO TYPE HINT HELPERS # +############################# + +#: A coroutine object. +#: +#: This is awaitable but MUST be awaited somewhere to be +#: completed correctly. +Coroutine = _Coroutine[_Any, _Any, T_co] + + +@_runtime_checkable +class Future(_Protocol[T_contra]): + """Typed protocol representation of an :obj:`~asyncio.Future`. + + You should consult the documentation for :obj:`~asyncio.Future` for usage. + """ + + def result(self) -> T_contra: + """See :meth:`asyncio.Future.result`.""" + + def set_result(self, result: T_contra, /) -> None: + """See :meth:`asyncio.Future.set_result`.""" + + def set_exception(self, exception: Exception, /) -> None: + """See :meth:`asyncio.Future.set_exception`.""" + + def done(self) -> bool: + """See :meth:`asyncio.Future.done`.""" + + def cancelled(self) -> bool: + """See :meth:`asyncio.Future.cancelled`.""" + + def add_done_callback( + self, callback: _Callable[[Future[T_contra]], None], /, *, context: _Optional[contextvars.Context], + ) -> None: + """See :meth:`asyncio.Future.add_done_callback`.""" + + def remove_done_callback(self, callback: _Callable[[Future[T_contra]], None], /) -> None: + """See :meth:`asyncio.Future.remove_done_callback`.""" + + def cancel(self) -> bool: + """See :meth:`asyncio.Future.cancel`.""" + + def exception(self) -> _Optional[Exception]: + """See :meth:`asyncio.Future.exception`.""" + + def get_loop(self) -> asyncio.AbstractEventLoop: + """See :meth:`asyncio.Future.get_loop`.""" + + def __await__(self) -> Coroutine[T_contra]: + ... + + +@_runtime_checkable +class Task(_Protocol[T_contra]): + """Typed protocol representation of an :obj:`~asyncio.Task`. + + You should consult the documentation for :obj:`~asyncio.Task` for usage. + """ + + def result(self) -> T_contra: + """See :meth:`asyncio.Future.result`.""" + + def set_result(self, result: T_contra, /) -> None: + """See :meth:`asyncio.Future.set_result`.""" + + def set_exception(self, exception: Exception, /) -> None: + """See :meth:`asyncio.Future.set_exception`.""" + + def done(self) -> bool: + """See :meth:`asyncio.Future.done`.""" + + def cancelled(self) -> bool: + """See :meth:`asyncio.Future.cancelled`.""" + + def add_done_callback( + self, callback: _Callable[[Future[T_contra]], None], /, *, context: _Optional[contextvars.Context], + ) -> None: + """See :meth:`asyncio.Future.add_done_callback`.""" + + def remove_done_callback(self, callback: _Callable[[Future[T_contra]], None], /) -> None: + """See :meth:`asyncio.Future.remove_done_callback`.""" + + def cancel(self) -> bool: + """See :meth:`asyncio.Future.cancel`.""" + + def exception(self) -> _Optional[Exception]: + """See :meth:`asyncio.Future.exception`.""" + + def get_loop(self) -> asyncio.AbstractEventLoop: + """See :meth:`asyncio.Future.get_loop`.""" + + def get_stack(self, *, limit: _Optional[int] = None) -> _Sequence[_FrameType]: + """See :meth:`asyncio.Task.get_stack`.""" + + def print_stack(self, *, limit: _Optional[int] = None, file: _Optional[_IO] = None) -> None: + """See :meth:`asyncio.Task.print_stack`.""" + + def get_name(self) -> str: + """See :meth:`asyncio.Task.get_name`.""" + + def set_name(self, value: str, /) -> None: + """See :meth:`asyncio.Task.set_name`.""" + + def __await__(self) -> Coroutine[T_contra]: + ... + + +# pylint:enable=unused-variable diff --git a/hikari/internal/pagination.py b/hikari/internal/pagination.py index 9fe11ecf32..c9f392f73c 100644 --- a/hikari/internal/pagination.py +++ b/hikari/internal/pagination.py @@ -25,11 +25,13 @@ import typing +from hikari.internal import more_typing + async def pagination_handler( deserializer: typing.Callable[[typing.Any], typing.Any], direction: typing.Union[typing.Literal["before"], typing.Literal["after"]], - request: typing.Callable[..., typing.Coroutine[typing.Any, typing.Any, typing.Any]], + request: typing.Callable[..., more_typing.Coroutine[typing.Any]], reversing: bool, start: typing.Union[str, None], limit: typing.Optional[int] = None, @@ -88,4 +90,5 @@ async def pagination_handler( yield entity if limit == 0: break + # TODO: @FasterSpeeding: can `payloads` ever be empty, leading this to be undefined? start = id_getter(entity) diff --git a/hikari/invites.py b/hikari/invites.py index 10e6d0125f..7478ed5c11 100644 --- a/hikari/invites.py +++ b/hikari/invites.py @@ -25,8 +25,8 @@ import attr +from hikari import bases from hikari import channels -from hikari import entities from hikari import guilds from hikari import users from hikari.internal import conversions @@ -44,7 +44,7 @@ class TargetUserType(enum.IntEnum): @marshaller.marshallable() @attr.s(slots=True, kw_only=True) -class VanityUrl(entities.HikariEntity, marshaller.Deserializable): +class VanityUrl(bases.HikariEntity, marshaller.Deserializable): """A special case invite object, that represents a guild's vanity url.""" #: The code for this invite. @@ -165,7 +165,7 @@ def banner_url(self) -> typing.Optional[str]: @marshaller.marshallable() @attr.s(slots=True, kw_only=True) -class Invite(entities.HikariEntity, marshaller.Deserializable): +class Invite(bases.HikariEntity, marshaller.Deserializable): """Represents an invite that's used to add users to a guild or group dm.""" #: The code for this invite. diff --git a/hikari/messages.py b/hikari/messages.py index b7a0eba1ab..89b3d5a468 100644 --- a/hikari/messages.py +++ b/hikari/messages.py @@ -34,12 +34,11 @@ import attr +from hikari import bases from hikari import embeds as _embeds from hikari import emojis as _emojis -from hikari import entities from hikari import guilds from hikari import oauth2 -from hikari import snowflakes from hikari import users from hikari.internal import conversions from hikari.internal import marshaller @@ -133,7 +132,7 @@ class MessageActivityType(enum.IntEnum): @marshaller.marshallable() @attr.s(slots=True, kw_only=True) -class Attachment(snowflakes.UniqueEntity, marshaller.Deserializable): +class Attachment(bases.UniqueEntity, marshaller.Deserializable): """Represents a file attached to a message.""" #: The name of the file. @@ -169,7 +168,7 @@ class Attachment(snowflakes.UniqueEntity, marshaller.Deserializable): @marshaller.marshallable() @attr.s(slots=True, kw_only=True) -class Reaction(entities.HikariEntity, marshaller.Deserializable): +class Reaction(bases.HikariEntity, marshaller.Deserializable): """Represents a reaction in a message.""" #: The amount of times the emoji has been used to react. @@ -192,7 +191,7 @@ class Reaction(entities.HikariEntity, marshaller.Deserializable): @marshaller.marshallable() @attr.s(slots=True, kw_only=True) -class MessageActivity(entities.HikariEntity, marshaller.Deserializable): +class MessageActivity(bases.HikariEntity, marshaller.Deserializable): """Represents the activity of a rich presence-enabled message.""" #: The type of message activity. @@ -208,7 +207,7 @@ class MessageActivity(entities.HikariEntity, marshaller.Deserializable): @marshaller.marshallable() @attr.s(slots=True, kw_only=True) -class MessageCrosspost(entities.HikariEntity, marshaller.Deserializable): +class MessageCrosspost(bases.HikariEntity, marshaller.Deserializable): """Represents information about a cross-posted message and the origin of the original message.""" #: The ID of the original message. @@ -220,15 +219,15 @@ class MessageCrosspost(entities.HikariEntity, marshaller.Deserializable): #: currently documented. #: #: - #: :type: :obj:`~hikari.snowflakes.Snowflake`, optional - message_id: typing.Optional[snowflakes.Snowflake] = marshaller.attrib( - deserializer=snowflakes.Snowflake.deserialize, if_undefined=None, default=None + #: :type: :obj:`~hikari.entities.Snowflake`, optional + message_id: typing.Optional[bases.Snowflake] = marshaller.attrib( + deserializer=bases.Snowflake.deserialize, if_undefined=None, default=None ) #: The ID of the channel that the message originated from. #: - #: :type: :obj:`~hikari.snowflakes.Snowflake` - channel_id: snowflakes.Snowflake = marshaller.attrib(deserializer=snowflakes.Snowflake.deserialize) + #: :type: :obj:`~hikari.entities.Snowflake` + channel_id: bases.Snowflake = marshaller.attrib(deserializer=bases.Snowflake.deserialize) #: The ID of the guild that the message originated from. #: @@ -238,27 +237,27 @@ class MessageCrosspost(entities.HikariEntity, marshaller.Deserializable): #: documentation, but the situations that cause this to occur are not #: currently documented. #: - #: :type: :obj:`~hikari.snowflakes.Snowflake`, optional - guild_id: typing.Optional[snowflakes.Snowflake] = marshaller.attrib( - deserializer=snowflakes.Snowflake.deserialize, if_undefined=None, default=None + #: :type: :obj:`~hikari.entities.Snowflake`, optional + guild_id: typing.Optional[bases.Snowflake] = marshaller.attrib( + deserializer=bases.Snowflake.deserialize, if_undefined=None, default=None ) @marshaller.marshallable() @attr.s(slots=True, kw_only=True) -class Message(snowflakes.UniqueEntity, marshaller.Deserializable): +class Message(bases.UniqueEntity, marshaller.Deserializable): """Represents a message.""" #: The ID of the channel that the message was sent in. #: - #: :type: :obj:`~hikari.snowflakes.Snowflake` - channel_id: snowflakes.Snowflake = marshaller.attrib(deserializer=snowflakes.Snowflake.deserialize) + #: :type: :obj:`~hikari.entities.Snowflake` + channel_id: bases.Snowflake = marshaller.attrib(deserializer=bases.Snowflake.deserialize) #: The ID of the guild that the message was sent in. #: - #: :type: :obj:`~hikari.snowflakes.Snowflake`, optional - guild_id: typing.Optional[snowflakes.Snowflake] = marshaller.attrib( - deserializer=snowflakes.Snowflake.deserialize, if_undefined=None, default=None + #: :type: :obj:`~hikari.entities.Snowflake`, optional + guild_id: typing.Optional[bases.Snowflake] = marshaller.attrib( + deserializer=bases.Snowflake.deserialize, if_undefined=None, default=None ) #: The author of this message. @@ -303,26 +302,26 @@ class Message(snowflakes.UniqueEntity, marshaller.Deserializable): #: The users the message mentions. #: - #: :type: :obj:`~typing.Set` [ :obj:`~hikari.snowflakes.Snowflake` ] - user_mentions: typing.Set[snowflakes.Snowflake] = marshaller.attrib( + #: :type: :obj:`~typing.Set` [ :obj:`~hikari.entities.Snowflake` ] + user_mentions: typing.Set[bases.Snowflake] = marshaller.attrib( raw_name="mentions", - deserializer=lambda user_mentions: {snowflakes.Snowflake.deserialize(u["id"]) for u in user_mentions}, + deserializer=lambda user_mentions: {bases.Snowflake.deserialize(u["id"]) for u in user_mentions}, ) #: The roles the message mentions. #: - #: :type: :obj:`~typing.Set` [ :obj:`~hikari.snowflakes.Snowflake` ] - role_mentions: typing.Set[snowflakes.Snowflake] = marshaller.attrib( + #: :type: :obj:`~typing.Set` [ :obj:`~hikari.entities.Snowflake` ] + role_mentions: typing.Set[bases.Snowflake] = marshaller.attrib( raw_name="mention_roles", - deserializer=lambda role_mentions: {snowflakes.Snowflake.deserialize(mention) for mention in role_mentions}, + deserializer=lambda role_mentions: {bases.Snowflake.deserialize(mention) for mention in role_mentions}, ) #: The channels the message mentions. #: - #: :type: :obj:`~typing.Set` [ :obj:`~hikari.snowflakes.Snowflake` ] - channel_mentions: typing.Set[snowflakes.Snowflake] = marshaller.attrib( + #: :type: :obj:`~typing.Set` [ :obj:`~hikari.entities.Snowflake` ] + channel_mentions: typing.Set[bases.Snowflake] = marshaller.attrib( raw_name="mention_channels", - deserializer=lambda channel_mentions: {snowflakes.Snowflake.deserialize(c["id"]) for c in channel_mentions}, + deserializer=lambda channel_mentions: {bases.Snowflake.deserialize(c["id"]) for c in channel_mentions}, if_undefined=set, factory=set, ) @@ -355,9 +354,9 @@ class Message(snowflakes.UniqueEntity, marshaller.Deserializable): #: If the message was generated by a webhook, the webhook's id. #: - #: :type: :obj:`~hikari.snowflakes.Snowflake`, optional - webhook_id: typing.Optional[snowflakes.Snowflake] = marshaller.attrib( - deserializer=snowflakes.Snowflake.deserialize, if_undefined=None, default=None + #: :type: :obj:`~hikari.entities.Snowflake`, optional + webhook_id: typing.Optional[bases.Snowflake] = marshaller.attrib( + deserializer=bases.Snowflake.deserialize, if_undefined=None, default=None ) #: The message type. diff --git a/hikari/net/codes.py b/hikari/net/codes.py index 4f145a8e39..741da9090c 100644 --- a/hikari/net/codes.py +++ b/hikari/net/codes.py @@ -17,11 +17,10 @@ # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . """Enumerations for opcodes and status codes.""" -__all__ = ["HTTPStatusCode", "GatewayCloseCode", "GatewayOpcode", "JSONErrorCode", "GatewayIntent"] +__all__ = ["HTTPStatusCode", "GatewayCloseCode", "GatewayOpcode", "JSONErrorCode"] import enum - # Doesnt work correctly with enums, so since this file is all enums, ignore # pylint: disable=no-member from hikari.internal import more_enums @@ -422,125 +421,4 @@ def __str__(self) -> str: return f"{self.value} {name}" -@enum.unique -class GatewayIntent(more_enums.FlagMixin, enum.IntFlag): - """Represents an intent on the gateway. - - This is a bitfield representation of all the categories of event - that you wish to receive. - - Any events not in an intent category will be fired regardless of what - intents you provide. - - Warnings - -------- - If you are using the V7 Gateway, you will be REQUIRED to provide some form - of intent value when you connect. Failure to do so may result in immediate - termination of the session server-side. - - Notes - ----- - Discord now places limits on certain events you can receive without - whitelisting your bot first. On the ``Bot`` tab in the developer's portal - for your bot, you should now have the option to enable functionality - for receiving these events. - - If you attempt to request an intent type that you have not whitelisted - your bot for, you will be disconnected on startup with a ``4014`` closure - code. - """ - - #: Subscribes to the following events: - #: * GUILD_CREATE - #: * GUILD_UPDATE - #: * GUILD_DELETE - #: * GUILD_ROLE_CREATE - #: * GUILD_ROLE_UPDATE - #: * GUILD_ROLE_DELETE - #: * CHANNEL_CREATE - #: * CHANNEL_UPDATE - #: * CHANNEL_DELETE - #: * CHANNEL_PINS_UPDATE - GUILDS = 1 << 0 - - #: Subscribes to the following events: - #: * GUILD_MEMBER_ADD - #: * GUILD_MEMBER_UPDATE - #: * GUILD_MEMBER_REMOVE - #: - #: Warnings - #: -------- - #: This intent is privileged, and requires enabling/whitelisting to use. - GUILD_MEMBERS = 1 << 1 - - #: Subscribes to the following events: - #: * GUILD_BAN_ADD - #: * GUILD_BAN_REMOVE - GUILD_BANS = 1 << 2 - - #: Subscribes to the following events: - #: * GUILD_EMOJIS_UPDATE - GUILD_EMOJIS = 1 << 3 - - #: Subscribes to the following events: - #: * GUILD_INTEGRATIONS_UPDATE - GUILD_INTEGRATIONS = 1 << 4 - - #: Subscribes to the following events: - #: * WEBHOOKS_UPDATE - GUILD_WEBHOOKS = 1 << 5 - - #: Subscribes to the following events: - #: * INVITE_CREATE - #: * INVITE_DELETE - GUILD_INVITES = 1 << 6 - - #: Subscribes to the following events: - #: * VOICE_STATE_UPDATE - GUILD_VOICE_STATES = 1 << 7 - - #: Subscribes to the following events: - #: * PRESENCE_UPDATE - #: - #: Warnings - #: -------- - #: This intent is privileged, and requires enabling/whitelisting to use. - GUILD_PRESENCES = 1 << 8 - - #: Subscribes to the following events: - #: * MESSAGE_CREATE - #: * MESSAGE_UPDATE - #: * MESSAGE_DELETE - #: * MESSAGE_BULK - GUILD_MESSAGES = 1 << 9 - - #: Subscribes to the following events: - #: * MESSAGE_REACTION_ADD - #: * MESSAGE_REACTION_REMOVE - #: * MESSAGE_REACTION_REMOVE_ALL - #: * MESSAGE_REACTION_REMOVE_EMOJI - GUILD_MESSAGE_REACTIONS = 1 << 10 - - #: Subscribes to the following events: - #: * TYPING_START - GUILD_MESSAGE_TYPING = 1 << 11 - - #: Subscribes to the following events: - #: * CHANNEL_CREATE - #: * MESSAGE_CREATE - #: * MESSAGE_UPDATE - #: * MESSAGE_DELETE - DIRECT_MESSAGES = 1 << 12 - - #: Subscribes to the following events: - #: * MESSAGE_REACTION_ADD - #: * MESSAGE_REACTION_REMOVE - #: * MESSAGE_REACTION_REMOVE_ALL - DIRECT_MESSAGE_REACTIONS = 1 << 13 - - #: Subscribes to the following events - #: * TYPING_START - DIRECT_MESSAGE_TYPING = 1 << 14 - - # pylint: enable=no-member diff --git a/hikari/net/ratelimits.py b/hikari/net/ratelimits.py index c637aa3802..b761c0cde1 100644 --- a/hikari/net/ratelimits.py +++ b/hikari/net/ratelimits.py @@ -180,6 +180,7 @@ import weakref from hikari.internal import more_asyncio +from hikari.internal import more_typing from hikari.net import routes #: The hash used for an unknown bucket that has not yet been resolved. @@ -201,7 +202,7 @@ class BaseRateLimiter(abc.ABC): __slots__ = () @abc.abstractmethod - def acquire(self) -> more_asyncio.Future[None]: + def acquire(self) -> more_typing.Future[None]: """Acquire permission to perform a task that needs to have rate limit management enforced. Returns @@ -239,12 +240,12 @@ class BurstRateLimiter(BaseRateLimiter, abc.ABC): #: The throttling task, or :obj:`~None` if it isn't running. #: #: :type: :obj:`~asyncio.Task`, optional - throttle_task: typing.Optional[more_asyncio.Task[None]] + throttle_task: typing.Optional[more_typing.Task[None]] #: The queue of any futures under a rate limit. #: #: :type: :obj:`~asyncio.Queue` [`asyncio.Future`] - queue: typing.Final[typing.List[more_asyncio.Future[None]]] + queue: typing.Final[typing.List[more_typing.Future[None]]] #: The logger used by this rate limiter. #: @@ -258,7 +259,7 @@ def __init__(self, name: str) -> None: self.logger = logging.getLogger(f"hikari.net.ratelimits.{type(self).__qualname__}.{name}") @abc.abstractmethod - def acquire(self) -> more_asyncio.Future[None]: + def acquire(self) -> more_typing.Future[None]: """Acquire time on this rate limiter. The implementation should define this. @@ -322,7 +323,7 @@ class ManualRateLimiter(BurstRateLimiter): def __init__(self) -> None: super().__init__("global") - def acquire(self) -> more_asyncio.Future[None]: + def acquire(self) -> more_typing.Future[None]: """Acquire time on this rate limiter. Returns @@ -456,7 +457,7 @@ def __init__(self, name: str, period: float, limit: int) -> None: self.limit = limit self.period = period - def acquire(self) -> more_asyncio.Future[None]: + def acquire(self) -> more_typing.Future[None]: """Acquire time on this rate limiter. Returns @@ -614,7 +615,7 @@ def is_unknown(self) -> bool: """Return :obj:`~True` if the bucket represents an ``UNKNOWN`` bucket.""" return self.name.startswith(UNKNOWN_HASH) - def acquire(self) -> more_asyncio.Future[None]: + def acquire(self) -> more_typing.Future[None]: """Acquire time on this rate limiter. Returns @@ -702,7 +703,7 @@ class RESTBucketManager: #: The internal garbage collector task. #: #: :type: :obj:`~asyncio.Task`, optional - gc_task: typing.Optional[more_asyncio.Task[None]] + gc_task: typing.Optional[more_typing.Task[None]] #: The logger to use for this object. #: @@ -814,7 +815,7 @@ def do_gc_pass(self) -> None: self.logger.debug("purged %s stale buckets", len(buckets_to_purge)) - def acquire(self, compiled_route: routes.CompiledRoute) -> more_asyncio.Future: + def acquire(self, compiled_route: routes.CompiledRoute) -> more_typing.Future: """Acquire a bucket for the given route. Parameters diff --git a/hikari/net/rest_sessions.py b/hikari/net/rest_sessions.py index 2255d9f89a..325e9aba77 100644 --- a/hikari/net/rest_sessions.py +++ b/hikari/net/rest_sessions.py @@ -42,7 +42,6 @@ from hikari.net import routes from hikari.net import user_agents - VERSION_6: typing.Final[int] = 6 VERSION_7: typing.Final[int] = 7 diff --git a/hikari/net/shards.py b/hikari/net/shards.py index 21205befa9..198f5aa249 100644 --- a/hikari/net/shards.py +++ b/hikari/net/shards.py @@ -47,6 +47,7 @@ import aiohttp.typedefs from hikari import errors +from hikari import intents as _intents from hikari.internal import more_asyncio from hikari.net import codes from hikari.net import ratelimits @@ -103,7 +104,7 @@ class ShardConnection: initial presence of the bot user once online. If :obj:`~None`, then it will be set to the default, which is showing up as online without a custom status message. - intents: :obj:`~hikari.net.codes.GatewayIntent`, optional + intents: :obj:`~hikari.intents.Intent`, optional Bitfield of intents to use. If you use the V7 API, this is mandatory. This field will determine what events you will receive. json_deserialize: ``deserialization function`` @@ -203,7 +204,7 @@ class ShardConnection: _connected_at: float _connector: typing.Optional[aiohttp.BaseConnector] _debug: bool - _intents: typing.Optional[codes.GatewayIntent] + _intents: typing.Optional[_intents.Intent] _large_threshold: int _json_deserialize: typing.Callable[[typing.AnyStr], typing.Dict] _json_serialize: typing.Callable[[typing.Dict], typing.AnyStr] @@ -342,7 +343,7 @@ def __init__( debug: bool = False, dispatch: DispatchT = lambda gw, e, p: None, initial_presence: typing.Optional[typing.Dict] = None, - intents: typing.Optional[codes.GatewayIntent] = None, + intents: typing.Optional[_intents.Intent] = None, json_deserialize: typing.Callable[[typing.AnyStr], typing.Dict] = json.loads, json_serialize: typing.Callable[[typing.Dict], typing.AnyStr] = json.dumps, large_threshold: int = 250, @@ -436,7 +437,7 @@ def is_connected(self) -> bool: return not math.isnan(self._connected_at) @property - def intents(self) -> typing.Optional[codes.GatewayIntent]: + def intents(self) -> typing.Optional[_intents.Intent]: """Intents being used. If this is :obj:`~None`, no intent usage was being @@ -446,7 +447,7 @@ def intents(self) -> typing.Optional[codes.GatewayIntent]: Returns ------- - :obj:`~hikari.net.codes.GatewayIntent`, optional + :obj:`~hikari.intents.Intent`, optional The intents being used. """ return self._intents @@ -718,6 +719,7 @@ def _zombie_detector(self, heartbeat_interval): async def _identify(self): self.logger.debug("preparing to send IDENTIFY") + # noinspection PyArgumentList pl = { "op": codes.GatewayOpcode.IDENTIFY, "d": { @@ -856,6 +858,7 @@ async def _receive(self): if message.type == aiohttp.WSMsgType.CLOSE: close_code = self._ws.close_code try: + # noinspection PyArgumentList close_code = codes.GatewayCloseCode(close_code) except ValueError: pass diff --git a/hikari/oauth2.py b/hikari/oauth2.py index c90d473fca..e47ca63abd 100644 --- a/hikari/oauth2.py +++ b/hikari/oauth2.py @@ -24,10 +24,9 @@ import attr -from hikari import entities +from hikari import bases from hikari import guilds from hikari import permissions -from hikari import snowflakes from hikari import users from hikari.internal import marshaller from hikari.internal import urls @@ -46,7 +45,7 @@ class ConnectionVisibility(enum.IntEnum): @marshaller.marshallable() @attr.s(slots=True, kw_only=True) -class OwnConnection(entities.HikariEntity, marshaller.Deserializable): +class OwnConnection(bases.HikariEntity, marshaller.Deserializable): """Represents a user's connection with a third party account. Returned by the ``GET Current User Connections`` endpoint. @@ -141,7 +140,7 @@ class TeamMembershipState(enum.IntEnum): @marshaller.marshallable() @attr.s(slots=True, kw_only=True) -class TeamMember(entities.HikariEntity, marshaller.Deserializable): +class TeamMember(bases.HikariEntity, marshaller.Deserializable): """Represents a member of a Team.""" #: The state of this user's membership. @@ -157,8 +156,8 @@ class TeamMember(entities.HikariEntity, marshaller.Deserializable): #: The ID of the team this member belongs to. #: - #: :type: :obj:`~hikari.snowflakes.Snowflake` - team_id: snowflakes.Snowflake = marshaller.attrib(deserializer=snowflakes.Snowflake.deserialize) + #: :type: :obj:`~hikari.entities.Snowflake` + team_id: bases.Snowflake = marshaller.attrib(deserializer=bases.Snowflake.deserialize) #: The user object of this team member. #: @@ -168,7 +167,7 @@ class TeamMember(entities.HikariEntity, marshaller.Deserializable): @marshaller.marshallable() @attr.s(slots=True, kw_only=True) -class Team(snowflakes.UniqueEntity, marshaller.Deserializable): +class Team(bases.UniqueEntity, marshaller.Deserializable): """Represents a development team, along with all its members.""" #: The hash of this team's icon, if set. @@ -178,15 +177,15 @@ class Team(snowflakes.UniqueEntity, marshaller.Deserializable): #: The member's that belong to this team. #: - #: :type: :obj:`~typing.Mapping` [ :obj:`~hikari.snowflakes.Snowflake`, :obj:`~TeamMember` ] - members: typing.Mapping[snowflakes.Snowflake, TeamMember] = marshaller.attrib( + #: :type: :obj:`~typing.Mapping` [ :obj:`~hikari.entities.Snowflake`, :obj:`~TeamMember` ] + members: typing.Mapping[bases.Snowflake, TeamMember] = marshaller.attrib( deserializer=lambda members: {m.user.id: m for m in map(TeamMember.deserialize, members)} ) #: The ID of this team's owner. #: - #: :type: :obj:`~hikari.snowflakes.Snowflake` - owner_user_id: snowflakes.Snowflake = marshaller.attrib(deserializer=snowflakes.Snowflake.deserialize) + #: :type: :obj:`~hikari.entities.Snowflake` + owner_user_id: bases.Snowflake = marshaller.attrib(deserializer=bases.Snowflake.deserialize) @property def icon_url(self) -> typing.Optional[str]: @@ -238,7 +237,7 @@ def is_team_user(self) -> bool: @marshaller.marshallable() @attr.s(slots=True, kw_only=True) -class Application(snowflakes.UniqueEntity, marshaller.Deserializable): +class Application(bases.UniqueEntity, marshaller.Deserializable): """Represents the information of an Oauth2 Application.""" #: The name of this application. @@ -311,16 +310,16 @@ class Application(snowflakes.UniqueEntity, marshaller.Deserializable): #: The ID of the guild this application is linked to #: if it's sold on Discord. #: - #: :type: :obj:`~hikari.snowflakes.Snowflake`, optional - guild_id: typing.Optional[snowflakes.Snowflake] = marshaller.attrib( - deserializer=snowflakes.Snowflake.deserialize, if_undefined=None, default=None + #: :type: :obj:`~hikari.entities.Snowflake`, optional + guild_id: typing.Optional[bases.Snowflake] = marshaller.attrib( + deserializer=bases.Snowflake.deserialize, if_undefined=None, default=None ) #: The ID of the primary "Game SKU" of a game that's sold on Discord. #: - #: :type: :obj:`~hikari.snowflakes.Snowflake`, optional - primary_sku_id: typing.Optional[snowflakes.Snowflake] = marshaller.attrib( - deserializer=snowflakes.Snowflake.deserialize, if_undefined=None, default=None + #: :type: :obj:`~hikari.entities.Snowflake`, optional + primary_sku_id: typing.Optional[bases.Snowflake] = marshaller.attrib( + deserializer=bases.Snowflake.deserialize, if_undefined=None, default=None ) #: The URL slug that links to this application's store page diff --git a/hikari/snowflakes.py b/hikari/snowflakes.py index 1644d2b076..b124e3160e 100644 --- a/hikari/snowflakes.py +++ b/hikari/snowflakes.py @@ -30,13 +30,13 @@ import attr -from hikari import entities +from hikari import bases from hikari.internal import conversions from hikari.internal import marshaller @functools.total_ordering -class Snowflake(entities.HikariEntity, typing.SupportsInt): +class Snowflake(bases.HikariEntity, typing.SupportsInt): """A concrete representation of a unique identifier for an object on Discord. This object can be treated as a regular :obj:`~int` for most purposes. @@ -114,7 +114,7 @@ def from_timestamp(cls, timestamp: float) -> "Snowflake": @marshaller.marshallable() @attr.s(slots=True, kw_only=True) -class UniqueEntity(entities.HikariEntity, typing.SupportsInt): +class UniqueEntity(bases.HikariEntity, typing.SupportsInt): """An entity that has an integer ID of some sort.""" #: The ID of this entity. diff --git a/hikari/state/event_dispatchers.py b/hikari/state/event_dispatchers.py index 7c45131f8e..96e67922c9 100644 --- a/hikari/state/event_dispatchers.py +++ b/hikari/state/event_dispatchers.py @@ -33,14 +33,15 @@ from hikari.internal import conversions from hikari.internal import more_asyncio from hikari.internal import more_collections - # Prevents a circular reference that prevents importing correctly. +from hikari.internal import more_typing + if typing.TYPE_CHECKING: EventT = typing.TypeVar("EventT", bound=events.HikariEvent) - PredicateT = typing.Callable[[EventT], typing.Union[bool, typing.Coroutine[typing.Any, typing.Any, bool]]] - EventCallbackT = typing.Callable[[EventT], typing.Coroutine[typing.Any, typing.Any, typing.Any]] + PredicateT = typing.Callable[[EventT], typing.Union[bool, more_typing.Coroutine[bool]]] + EventCallbackT = typing.Callable[[EventT], more_typing.Coroutine[typing.Any]] WaiterMapT = typing.Dict[ - typing.Type[EventT], more_collections.WeakKeyDictionary[more_asyncio.Future[typing.Any], PredicateT] + typing.Type[EventT], more_collections.WeakKeyDictionary[more_typing.Future[typing.Any], PredicateT] ] ListenerMapT = typing.Dict[typing.Type[EventT], typing.List[EventCallbackT]] else: @@ -100,7 +101,7 @@ def remove_listener(self, event_type: typing.Type[EventT], callback: EventCallba @abc.abstractmethod def wait_for( self, event_type: typing.Type[EventT], *, timeout: typing.Optional[float], predicate: PredicateT - ) -> more_asyncio.Future: + ) -> more_typing.Future: """Wait for the given event type to occur. Parameters @@ -230,7 +231,7 @@ def decorator(callback: EventCallbackT) -> EventCallbackT: # Do not add an annotation here, it will mess with type hints in PyCharm which can lead to # confusing telepathy comments to the user. @abc.abstractmethod - def dispatch_event(self, event: events.HikariEvent) -> more_asyncio.Future[typing.Any]: + def dispatch_event(self, event: events.HikariEvent) -> more_typing.Future[typing.Any]: """Dispatch a given event to any listeners and waiters. Parameters @@ -437,7 +438,7 @@ def handle_exception( def wait_for( self, event_type: typing.Type[EventT], *, timeout: typing.Optional[float], predicate: PredicateT, - ) -> more_asyncio.Future: + ) -> more_typing.Future: """Wait for a event to occur once and then return the arguments the event was called with. Events can be filtered using a given predicate function. If unspecified, diff --git a/hikari/state/event_managers.py b/hikari/state/event_managers.py index f18a075753..440d53b548 100644 --- a/hikari/state/event_managers.py +++ b/hikari/state/event_managers.py @@ -23,7 +23,6 @@ import logging import typing -from hikari import entities from hikari.clients import shard_clients from hikari.internal import assertions from hikari.state import event_dispatchers @@ -31,7 +30,7 @@ EVENT_MARKER_ATTR: typing.Final[str] = "___event_name___" -EventConsumerT = typing.Callable[[str, entities.RawEntityT], typing.Awaitable[None]] +EventConsumerT = typing.Callable[[str, typing.Mapping[str, str]], typing.Awaitable[None]] def raw_event_mapper(name: str) -> typing.Callable[[EventConsumerT], EventConsumerT]: @@ -150,7 +149,7 @@ def __init__(self, event_dispatcher_impl: typing.Optional[EventDispatcherT] = No self.raw_event_mappers[event_name] = member def process_raw_event( - self, shard_client_obj: shard_clients.ShardClient, name: str, payload: entities.RawEntityT, + self, shard_client_obj: shard_clients.ShardClient, name: str, payload: typing.Mapping[str, typing.Any], ) -> None: """Process a low level event. diff --git a/hikari/state/raw_event_consumers.py b/hikari/state/raw_event_consumers.py index 15a8d2083b..46995bf0cc 100644 --- a/hikari/state/raw_event_consumers.py +++ b/hikari/state/raw_event_consumers.py @@ -25,8 +25,8 @@ __all__ = ["RawEventConsumer"] import abc +import typing -from hikari import entities from hikari.clients import shard_clients @@ -43,7 +43,7 @@ class RawEventConsumer(abc.ABC): @abc.abstractmethod def process_raw_event( - self, shard_client_obj: shard_clients.ShardClient, name: str, payload: entities.RawEntityT, + self, shard_client_obj: shard_clients.ShardClient, name: str, payload: typing.Mapping[str, str], ) -> None: """Consume a raw event that was received from a shard connection. diff --git a/hikari/users.py b/hikari/users.py index 914976dbc1..209f86a059 100644 --- a/hikari/users.py +++ b/hikari/users.py @@ -24,7 +24,7 @@ import attr -from hikari import snowflakes +from hikari import bases from hikari.internal import marshaller from hikari.internal import urls @@ -92,7 +92,7 @@ class PremiumType(enum.IntEnum): @marshaller.marshallable() @attr.s(slots=True, kw_only=True) -class User(snowflakes.UniqueEntity, marshaller.Deserializable): +class User(bases.UniqueEntity, marshaller.Deserializable): """Represents a user.""" #: This user's discriminator. diff --git a/hikari/voices.py b/hikari/voices.py index 8862ec50e1..729f6cd208 100644 --- a/hikari/voices.py +++ b/hikari/voices.py @@ -23,36 +23,35 @@ import attr -from hikari import entities +from hikari import bases from hikari import guilds -from hikari import snowflakes from hikari.internal import marshaller @marshaller.marshallable() @attr.s(slots=True, kw_only=True) -class VoiceState(entities.HikariEntity, marshaller.Deserializable): +class VoiceState(bases.HikariEntity, marshaller.Deserializable): """Represents a user's voice connection status.""" #: The ID of the guild this voice state is in, if applicable. #: - #: :type: :obj:`~hikari.snowflakes.Snowflake`, optional - guild_id: typing.Optional[snowflakes.Snowflake] = marshaller.attrib( - deserializer=snowflakes.Snowflake.deserialize, if_undefined=None, default=None + #: :type: :obj:`~hikari.entities.Snowflake`, optional + guild_id: typing.Optional[bases.Snowflake] = marshaller.attrib( + deserializer=bases.Snowflake.deserialize, if_undefined=None, default=None ) #: The ID of the channel this user is connected to, will be :obj:`~None` if #: they are leaving voice. #: - #: :type: :obj:`~hikari.snowflakes.Snowflake`, optional - channel_id: typing.Optional[snowflakes.Snowflake] = marshaller.attrib( - deserializer=snowflakes.Snowflake.deserialize, if_none=None + #: :type: :obj:`~hikari.entities.Snowflake`, optional + channel_id: typing.Optional[bases.Snowflake] = marshaller.attrib( + deserializer=bases.Snowflake.deserialize, if_none=None ) #: The ID of the user this voice state is for. #: - #: :type: :obj:`~hikari.snowflakes.Snowflake` - user_id: snowflakes.Snowflake = marshaller.attrib(deserializer=snowflakes.Snowflake.deserialize) + #: :type: :obj:`~hikari.entities.Snowflake` + user_id: bases.Snowflake = marshaller.attrib(deserializer=bases.Snowflake.deserialize) #: The guild member this voice state is for if the voice state is in a #: guild. @@ -100,7 +99,7 @@ class VoiceState(entities.HikariEntity, marshaller.Deserializable): @marshaller.marshallable() @attr.s(slots=True, kw_only=True) -class VoiceRegion(entities.HikariEntity, marshaller.Deserializable): +class VoiceRegion(bases.HikariEntity, marshaller.Deserializable): """Represent's a voice region server.""" #: The ID of this region diff --git a/hikari/webhooks.py b/hikari/webhooks.py index e1226bb204..5a338c3b4e 100644 --- a/hikari/webhooks.py +++ b/hikari/webhooks.py @@ -24,7 +24,7 @@ import attr -from hikari import snowflakes +from hikari import bases from hikari import users from hikari.internal import marshaller @@ -42,7 +42,7 @@ class WebhookType(enum.IntEnum): @marshaller.marshallable() @attr.s(slots=True, kw_only=True) -class Webhook(snowflakes.UniqueEntity, marshaller.Deserializable): +class Webhook(bases.UniqueEntity, marshaller.Deserializable): """Represents a webhook object on Discord. This is an endpoint that can have messages sent to it using standard @@ -57,15 +57,15 @@ class Webhook(snowflakes.UniqueEntity, marshaller.Deserializable): #: The guild ID of the webhook. #: - #: :type: :obj:`~hikari.snowflakes.Snowflake`, optional - guild_id: typing.Optional[snowflakes.Snowflake] = marshaller.attrib( - deserializer=snowflakes.Snowflake.deserialize, if_undefined=None, default=None + #: :type: :obj:`~hikari.entities.Snowflake`, optional + guild_id: typing.Optional[bases.Snowflake] = marshaller.attrib( + deserializer=bases.Snowflake.deserialize, if_undefined=None, default=None ) #: The channel ID this webhook is for. #: - #: :type: :obj:`~hikari.snowflakes.Snowflake` - channel_id: snowflakes.Snowflake = marshaller.attrib(deserializer=snowflakes.Snowflake.deserialize) + #: :type: :obj:`~hikari.entities.Snowflake` + channel_id: bases.Snowflake = marshaller.attrib(deserializer=bases.Snowflake.deserialize) #: The user that created the webhook #: diff --git a/tests/hikari/clients/test_configs.py b/tests/hikari/clients/test_configs.py index e9e8437263..4478b54b06 100644 --- a/tests/hikari/clients/test_configs.py +++ b/tests/hikari/clients/test_configs.py @@ -24,8 +24,8 @@ from hikari import gateway_entities from hikari import guilds +from hikari import intents from hikari.clients import configs -from hikari.net import codes from tests.hikari import _helpers @@ -142,12 +142,12 @@ def test_deserialize(self, test_websocket_config): assert websocket_config_obj.gateway_use_compression is False assert websocket_config_obj.gateway_version == 7 - assert websocket_config_obj.initial_activity == gateway_entities.GatewayActivity.deserialize( + assert websocket_config_obj.initial_activity == gateway_entities.Activity.deserialize( {"name": "test", "url": "some_url", "type": 0} ) assert websocket_config_obj.initial_status == guilds.PresenceStatus.DND assert websocket_config_obj.initial_idle_since == datetime_obj - assert websocket_config_obj.intents == codes.GatewayIntent.GUILD_MESSAGES | codes.GatewayIntent.GUILDS + assert websocket_config_obj.intents == intents.Intent.GUILD_MESSAGES | intents.Intent.GUILDS assert websocket_config_obj.large_threshold == 1000 assert websocket_config_obj.debug is True assert websocket_config_obj.allow_redirects is True @@ -260,12 +260,12 @@ def test_deserialize(self, test_bot_config): assert bot_config_obj.shard_count == 17 assert bot_config_obj.gateway_use_compression is False assert bot_config_obj.gateway_version == 7 - assert bot_config_obj.initial_activity == gateway_entities.GatewayActivity.deserialize( + assert bot_config_obj.initial_activity == gateway_entities.Activity.deserialize( {"name": "test", "url": "some_url", "type": 0} ) assert bot_config_obj.initial_status == guilds.PresenceStatus.DND assert bot_config_obj.initial_idle_since == datetime_obj - assert bot_config_obj.intents == codes.GatewayIntent.GUILD_MESSAGES | codes.GatewayIntent.GUILDS + assert bot_config_obj.intents == intents.Intent.GUILD_MESSAGES | intents.Intent.GUILDS assert bot_config_obj.large_threshold == 1000 assert bot_config_obj.debug is True diff --git a/tests/hikari/clients/test_rest_clients/test_oauth2_component.py b/tests/hikari/clients/test_rest_clients/test_oauth2_component.py index d6da8623b7..5b697896aa 100644 --- a/tests/hikari/clients/test_rest_clients/test_oauth2_component.py +++ b/tests/hikari/clients/test_rest_clients/test_oauth2_component.py @@ -16,7 +16,6 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . -import datetime import mock import pytest diff --git a/tests/hikari/clients/test_shard_clients.py b/tests/hikari/clients/test_shard_clients.py index 285a8bcb63..109b59bf72 100644 --- a/tests/hikari/clients/test_shard_clients.py +++ b/tests/hikari/clients/test_shard_clients.py @@ -18,7 +18,6 @@ # along ith Hikari. If not, see . import asyncio import datetime -import math import aiohttp import mock diff --git a/tests/hikari/internal/test_pagination.py b/tests/hikari/internal/test_pagination.py index f0f35d95e5..90eba24b65 100644 --- a/tests/hikari/internal/test_pagination.py +++ b/tests/hikari/internal/test_pagination.py @@ -16,8 +16,8 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . -import pytest import mock +import pytest from hikari.internal import pagination diff --git a/tests/hikari/test_snowflake.py b/tests/hikari/test_bases.py similarity index 65% rename from tests/hikari/test_snowflake.py rename to tests/hikari/test_bases.py index 21f0d26095..77d4eb3d21 100644 --- a/tests/hikari/test_snowflake.py +++ b/tests/hikari/test_bases.py @@ -15,13 +15,13 @@ # GNU Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . +# along ith Hikari. If not, see . import datetime import attr import pytest -from hikari import snowflakes +from hikari import bases from hikari.internal import marshaller @@ -32,7 +32,7 @@ def raw_id(self): @pytest.fixture() def neko_snowflake(self, raw_id): - return snowflakes.Snowflake.deserialize(raw_id) + return bases.Snowflake.deserialize(raw_id) def test_created_at(self, neko_snowflake): assert neko_snowflake.created_at == datetime.datetime( @@ -62,45 +62,42 @@ def test_repr_cast(self, neko_snowflake, raw_id): def test_eq(self, neko_snowflake, raw_id): assert neko_snowflake == raw_id - assert neko_snowflake == snowflakes.Snowflake.deserialize(raw_id) + assert neko_snowflake == bases.Snowflake.deserialize(raw_id) assert str(raw_id) != neko_snowflake def test_lt(self, neko_snowflake, raw_id): assert neko_snowflake < raw_id + 1 def test_deserialize(self, neko_snowflake, raw_id): - assert neko_snowflake == snowflakes.Snowflake.deserialize(raw_id) + assert neko_snowflake == bases.Snowflake.deserialize(raw_id) def test_from_datetime(self): - result = snowflakes.Snowflake.from_datetime( + result = bases.Snowflake.from_datetime( datetime.datetime(2019, 1, 22, 18, 41, 15, 283_000, tzinfo=datetime.timezone.utc) ) assert result == 537340988620800000 - assert isinstance(result, snowflakes.Snowflake) + assert isinstance(result, bases.Snowflake) def test_from_timestamp(self): - result = snowflakes.Snowflake.from_timestamp(1548182475.283) + result = bases.Snowflake.from_timestamp(1548182475.283) assert result == 537340988620800000 - assert isinstance(result, snowflakes.Snowflake) + assert isinstance(result, bases.Snowflake) -class TestUniqueEntity: - def test_int(self): - assert int(snowflakes.UniqueEntity(id=snowflakes.Snowflake.deserialize("2333333"))) == 2333333 +@marshaller.marshallable() +@attr.s(slots=True) +class StubEntity(bases.UniqueEntity, marshaller.Deserializable, marshaller.Serializable): + ... - @pytest.fixture() - def stud_marshal_entity(self): - @marshaller.marshallable() - @attr.s(slots=True, kw_only=True) - class StudEntity(snowflakes.UniqueEntity, marshaller.Deserializable, marshaller.Serializable): - ... - return StudEntity +class TestUniqueEntity: + def test_int(self): + assert int(bases.UniqueEntity(id=bases.Snowflake.deserialize("2333333"))) == 2333333 - def test_deserialize(self, stud_marshal_entity): - unique_entity = stud_marshal_entity.deserialize({"id": "5445"}) - assert unique_entity.id == snowflakes.Snowflake("5445") - assert isinstance(unique_entity.id, snowflakes.Snowflake) + def test_deserialize(self): + unique_entity = StubEntity.deserialize({"id": "5445"}) + assert unique_entity.id == bases.Snowflake("5445") + assert isinstance(unique_entity.id, bases.Snowflake) - def test_serialize(self, stud_marshal_entity): - assert stud_marshal_entity(id=snowflakes.Snowflake(5445)).serialize() == {"id": "5445"} + def test_serialize(self): + assert StubEntity(id=bases.Snowflake(5445)).serialize() == {"id": "5445"} diff --git a/tests/hikari/test_entities.py b/tests/hikari/test_entities.py deleted file mode 100644 index 10add0633d..0000000000 --- a/tests/hikari/test_entities.py +++ /dev/null @@ -1,18 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# Copyright © Nekoka.tt 2019-2020 -# -# This file is part of Hikari. -# -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along ith Hikari. If not, see . diff --git a/tests/hikari/test_gateway_entities.py b/tests/hikari/test_gateway_entities.py index 96bfe08a42..17119d16b3 100644 --- a/tests/hikari/test_gateway_entities.py +++ b/tests/hikari/test_gateway_entities.py @@ -65,19 +65,19 @@ def test_gateway_activity_config(self): return {"name": "Presence me baby", "url": "http://a-url-name", "type": 1} def test_deserialize_full_config(self, test_gateway_activity_config): - gateway_activity_obj = gateway_entities.GatewayActivity.deserialize(test_gateway_activity_config) + gateway_activity_obj = gateway_entities.Activity.deserialize(test_gateway_activity_config) assert gateway_activity_obj.name == "Presence me baby" assert gateway_activity_obj.url == "http://a-url-name" assert gateway_activity_obj.type is guilds.ActivityType.STREAMING def test_deserialize_partial_config(self): - gateway_activity_obj = gateway_entities.GatewayActivity.deserialize({"name": "Presence me baby"}) + gateway_activity_obj = gateway_entities.Activity.deserialize({"name": "Presence me baby"}) assert gateway_activity_obj.name == "Presence me baby" assert gateway_activity_obj.url == None assert gateway_activity_obj.type is guilds.ActivityType.PLAYING def test_serialize_full_activity(self): - gateway_activity_obj = gateway_entities.GatewayActivity( + gateway_activity_obj = gateway_entities.Activity( name="Presence me baby", url="http://a-url-name", type=guilds.ActivityType.STREAMING ) assert gateway_activity_obj.serialize() == { @@ -87,7 +87,7 @@ def test_serialize_full_activity(self): } def test_serialize_partial_activity(self): - gateway_activity_obj = gateway_entities.GatewayActivity(name="Presence me baby",) + gateway_activity_obj = gateway_entities.Activity(name="Presence me baby",) assert gateway_activity_obj.serialize() == { "name": "Presence me baby", "type": 0, From db6dc9bc2413f055bff7e041489701c58234fe52 Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Sun, 19 Apr 2020 13:59:36 +0100 Subject: [PATCH 171/922] Fixed typo in class name for a test (TestRESTChannelLogig -> TestRESTChannelLogging) --- hikari/state/event_dispatchers.py | 1 + .../hikari/clients/test_rest_clients/test_channels_component.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/hikari/state/event_dispatchers.py b/hikari/state/event_dispatchers.py index 96e67922c9..7422bd2d31 100644 --- a/hikari/state/event_dispatchers.py +++ b/hikari/state/event_dispatchers.py @@ -33,6 +33,7 @@ from hikari.internal import conversions from hikari.internal import more_asyncio from hikari.internal import more_collections + # Prevents a circular reference that prevents importing correctly. from hikari.internal import more_typing diff --git a/tests/hikari/clients/test_rest_clients/test_channels_component.py b/tests/hikari/clients/test_rest_clients/test_channels_component.py index f6a41846bd..d84ee89d98 100644 --- a/tests/hikari/clients/test_rest_clients/test_channels_component.py +++ b/tests/hikari/clients/test_rest_clients/test_channels_component.py @@ -40,7 +40,7 @@ from tests.hikari import _helpers -class TestRESTChannelLogig: +class TestRESTChannelLogging: @pytest.fixture() def rest_channel_logic_impl(self): mock_low_level_restful_client = mock.MagicMock(rest_sessions.LowLevelRestfulClient) From c82a120b6518358bc4b5233019acd573986d0aca Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Sun, 19 Apr 2020 17:15:27 +0100 Subject: [PATCH 172/922] Fixes #306 and removes all warnings from tests. --- hikari/clients/shard_clients.py | 30 +-- tests/hikari/clients/test_shard_clients.py | 210 +++++++++++++++++---- 2 files changed, 192 insertions(+), 48 deletions(-) diff --git a/hikari/clients/shard_clients.py b/hikari/clients/shard_clients.py index 81301aa4f1..9b559f3214 100644 --- a/hikari/clients/shard_clients.py +++ b/hikari/clients/shard_clients.py @@ -42,6 +42,7 @@ from hikari import events from hikari import gateway_entities from hikari import guilds +from hikari import intents as _intents from hikari.clients import configs from hikari.clients import runnable from hikari.net import codes @@ -261,7 +262,7 @@ def version(self) -> float: @property @abc.abstractmethod - def intents(self) -> typing.Optional[codes.GatewayIntent]: + def intents(self) -> typing.Optional[_intents.Intent]: """Intent values that this connection is using. Returns @@ -460,7 +461,7 @@ def version(self) -> float: return self._connection.version @property - def intents(self) -> typing.Optional[codes.GatewayIntent]: + def intents(self) -> typing.Optional[_intents.Intent]: return self._connection.intents async def start(self): @@ -478,8 +479,9 @@ async def start(self): [self._task, self._connection.ready_event.wait()], return_when=asyncio.FIRST_COMPLETED ) - if self._task in completed: - raise self._task.exception() + for task in completed: + if ex := task.exception(): + raise ex async def join(self) -> None: """Wait for the shard to shut down fully.""" @@ -598,8 +600,9 @@ async def _spin_up(self) -> asyncio.Task: [connect_task, self._connection.hello_event.wait()], return_when=asyncio.FIRST_COMPLETED ) - if connect_task in completed: - raise connect_task.exception() + for task in completed: + if ex := task.exception(): + raise ex self.logger.info("received HELLO, interval is %ss", self._connection.heartbeat_interval) @@ -607,8 +610,9 @@ async def _spin_up(self) -> asyncio.Task: [connect_task, self._connection.handshake_event.wait()], return_when=asyncio.FIRST_COMPLETED ) - if connect_task in completed: - raise connect_task.exception() + for task in completed: + if ex := task.exception(): + raise ex if is_resume: self.logger.info("sent RESUME, waiting for RESUMED event") @@ -618,8 +622,9 @@ async def _spin_up(self) -> asyncio.Task: [connect_task, self._connection.resumed_event.wait()], return_when=asyncio.FIRST_COMPLETED ) - if connect_task in completed: - raise connect_task.exception() + for task in completed: + if ex := task.exception(): + raise ex self.logger.info("now RESUMED") @@ -632,8 +637,9 @@ async def _spin_up(self) -> asyncio.Task: [connect_task, self._connection.ready_event.wait()], return_when=asyncio.FIRST_COMPLETED ) - if connect_task in completed: - raise connect_task.exception() + for task in completed: + if ex := task.exception(): + raise ex self.logger.info("now READY") diff --git a/tests/hikari/clients/test_shard_clients.py b/tests/hikari/clients/test_shard_clients.py index 109b59bf72..c8cb318ad2 100644 --- a/tests/hikari/clients/test_shard_clients.py +++ b/tests/hikari/clients/test_shard_clients.py @@ -155,23 +155,86 @@ def test_intents(self, shard_client_obj): class TestShardClientImplStart: @pytest.mark.asyncio - async def test_start_when_ready_event_completes_first(self, shard_client_obj): - shard_client_obj._keep_alive = mock.AsyncMock() - task_mock = _generate_mock_task() + async def test_start_when_ready_event_completes_first_without_error(self, shard_client_obj): + shard_client_obj._connection.seq = 123 + shard_client_obj._connection.session_id = 123 + stop_event = asyncio.Event() + try: + + async def forever(): + # make this so that it doesn't complete in time; + await stop_event.wait() + + shard_client_obj._keep_alive = mock.MagicMock(wraps=forever) + # Make this last a really long time so it doesn't complete immediately. + shard_client_obj._connection.ready_event = mock.MagicMock(wait=mock.AsyncMock()) + + # Do iiiit. + await shard_client_obj.start() + finally: + stop_event.set() + + @_helpers.assert_raises(type_=LookupError) + @pytest.mark.asyncio + async def test_start_when_ready_event_completes_first_with_error(self, shard_client_obj): + shard_client_obj._connection.seq = 123 + shard_client_obj._connection.session_id = 123 + stop_event = asyncio.Event() + try: + + async def forever(): + # make this so that it doesn't complete in time; + await stop_event.wait() + + shard_client_obj._keep_alive = mock.MagicMock(wraps=forever) + # Make this last a really long time so it doesn't complete immediately. + shard_client_obj._connection.ready_event = mock.MagicMock(wait=mock.AsyncMock(side_effect=LookupError)) + + # Do iiiit. + await shard_client_obj.start() + finally: + stop_event.set() + + @pytest.mark.asyncio + async def test_start_when_task_completes_with_no_exception(self, shard_client_obj): + shard_client_obj._connection.seq = 123 + shard_client_obj._connection.session_id = 123 + stop_event = asyncio.Event() + try: + + async def forever(): + # make this so that it doesn't complete in time; + await stop_event.wait() + + shard_client_obj._keep_alive = mock.AsyncMock() + # Make this last a really long time so it doesn't complete immediately. + shard_client_obj._connection.ready_event = mock.MagicMock(wait=forever) - with mock.patch("asyncio.create_task", return_value=task_mock): - with mock.patch("asyncio.wait", return_value=([], None)): - await shard_client_obj.start() + # Do iiiit. + await shard_client_obj.start() + finally: + stop_event.set() @_helpers.assert_raises(type_=RuntimeError) @pytest.mark.asyncio - async def test_start_when_task_completes(self, shard_client_obj): - shard_client_obj._keep_alive = mock.AsyncMock() - task_mock = _generate_mock_task(RuntimeError) + async def test_start_when_task_completes_with_exception(self, shard_client_obj): + shard_client_obj._connection.seq = 123 + shard_client_obj._connection.session_id = 123 + stop_event = asyncio.Event() + try: - with mock.patch("asyncio.create_task", return_value=task_mock): - with mock.patch("asyncio.wait", return_value=([task_mock], None)): - await shard_client_obj.start() + async def forever(): + # make this so that it doesn't complete in time; + await stop_event.wait() + + shard_client_obj._keep_alive = mock.AsyncMock(side_effect=RuntimeError) + # Make this last a really long time so it doesn't complete immediately. + shard_client_obj._connection.ready_event = mock.MagicMock(wait=forever) + + # Do iiiit. + await shard_client_obj.start() + finally: + stop_event.set() @_helpers.assert_raises(type_=RuntimeError) @pytest.mark.asyncio @@ -297,52 +360,127 @@ class TestShardClientImplSpinUp: @_helpers.assert_raises(type_=RuntimeError) @pytest.mark.asyncio async def test__spin_up_if_connect_task_is_completed_raises_exception_during_hello_event(self, shard_client_obj): - task_mock = _generate_mock_task(RuntimeError) + stop_event = asyncio.Event() + try: + + async def forever(): + # make this so that it doesn't complete in time; + await stop_event.wait() + + # Make this last a really long time so it doesn't complete immediately. + shard_client_obj._connection.connect = mock.MagicMock(wraps=forever) - with mock.patch("asyncio.create_task", return_value=task_mock): - with mock.patch("asyncio.wait", return_value=([task_mock], None)): - await shard_client_obj._spin_up() + # Make these finish immediately. + shard_client_obj._connection.hello_event = mock.MagicMock(wait=mock.AsyncMock(side_effect=RuntimeError)) + + # Do iiiit. + await shard_client_obj._spin_up() + finally: + stop_event.set() @_helpers.assert_raises(type_=RuntimeError) @pytest.mark.asyncio async def test__spin_up_if_connect_task_is_completed_raises_exception_during_identify_event(self, shard_client_obj): - task_mock = _generate_mock_task(RuntimeError) + stop_event = asyncio.Event() + try: + + async def forever(): + # make this so that it doesn't complete in time; + await stop_event.wait() - with mock.patch("asyncio.create_task", return_value=task_mock): - with mock.patch("asyncio.wait", side_effect=[([], None), ([task_mock], None)]): - await shard_client_obj._spin_up() + # Make this last a really long time so it doesn't complete immediately. + shard_client_obj._connection.connect = mock.MagicMock(wraps=forever) + + # Make these finish immediately. + shard_client_obj._connection.hello_event = mock.MagicMock(wait=mock.AsyncMock()) + shard_client_obj._connection.handshake_event = mock.MagicMock(wait=mock.AsyncMock(side_effect=RuntimeError)) + + # Do iiiit. + await shard_client_obj._spin_up() + finally: + stop_event.set() @pytest.mark.asyncio async def test__spin_up_when_resuming(self, shard_client_obj): shard_client_obj._connection.seq = 123 shard_client_obj._connection.session_id = 123 - task_mock = _generate_mock_task() + stop_event = asyncio.Event() + try: - with mock.patch("asyncio.create_task", return_value=task_mock): - with mock.patch("asyncio.wait", side_effect=[([], None), ([], None), ([], None)]): - assert await shard_client_obj._spin_up() == task_mock + async def forever(): + # make this so that it doesn't complete in time; + await stop_event.wait() + + # Make this last a really long time so it doesn't complete immediately. + shard_client_obj._connection.connect = mock.MagicMock(wraps=forever) + + # Make these finish immediately. + shard_client_obj._connection.hello_event = mock.MagicMock(wait=mock.AsyncMock()) + shard_client_obj._connection.handshake_event = mock.MagicMock(wait=mock.AsyncMock()) + + # Make this one go boom. + shard_client_obj._connection.resumed_event = mock.MagicMock(wait=mock.AsyncMock()) + + # Do iiiit. + await shard_client_obj._spin_up() + finally: + stop_event.set() @_helpers.assert_raises(type_=RuntimeError) @pytest.mark.asyncio async def test__spin_up_if_connect_task_is_completed_raises_exception_during_resumed_event(self, shard_client_obj): shard_client_obj._connection.seq = 123 shard_client_obj._connection.session_id = 123 - task_mock = _generate_mock_task(RuntimeError) + stop_event = asyncio.Event() + try: - with mock.patch("asyncio.create_task", return_value=task_mock): - with mock.patch("asyncio.wait", side_effect=[([], None), ([], None), ([task_mock], None)]): - await shard_client_obj._spin_up() + async def forever(): + # make this so that it doesn't complete in time; + await stop_event.wait() + + # Make this last a really long time so it doesn't complete immediately. + shard_client_obj._connection.connect = mock.MagicMock(wraps=forever) + + # Make these finish immediately. + shard_client_obj._connection.hello_event = mock.MagicMock(wait=mock.AsyncMock()) + shard_client_obj._connection.handshake_event = mock.MagicMock(wait=mock.AsyncMock()) + + # Make this one go boom. + shard_client_obj._connection.resumed_event = mock.MagicMock(wait=mock.AsyncMock(side_effect=RuntimeError)) + + # Do iiiit. + await shard_client_obj._spin_up() + finally: + stop_event.set() @pytest.mark.asyncio async def test__spin_up_when_not_resuming(self, shard_client_obj): - task_mock = _generate_mock_task() + shard_client_obj._connection.seq = None + shard_client_obj._connection.session_id = None + stop_event = asyncio.Event() + try: - with mock.patch("asyncio.create_task", return_value=task_mock): - with mock.patch("asyncio.wait", side_effect=[([], None), ([], None), ([], None)]): - assert await shard_client_obj._spin_up() == task_mock + async def forever(): + # make this so that it doesn't complete in time; + await stop_event.wait() + + # Make this last a really long time so it doesn't complete immediately. + shard_client_obj._connection.connect = mock.MagicMock(wraps=forever) + + # Make these finish immediately. + shard_client_obj._connection.hello_event = mock.MagicMock(wait=mock.AsyncMock()) + shard_client_obj._connection.handshake_event = mock.MagicMock(wait=mock.AsyncMock()) + + # Make this one go boom. + shard_client_obj._connection.ready_event = mock.MagicMock(wait=mock.AsyncMock()) + + # Do iiiit. + await shard_client_obj._spin_up() + finally: + stop_event.set() @_helpers.timeout_after(10) - # @_helpers.assert_raises(type_=RuntimeError) + @_helpers.assert_raises(type_=RuntimeError) @pytest.mark.asyncio async def test__spin_up_if_connect_task_is_completed_raises_exception_during_ready_event(self, shard_client_obj): stop_event = asyncio.Event() @@ -378,8 +516,8 @@ async def test_update_presence(self, shard_client_obj): ) assert shard_client_obj._status == guilds.PresenceStatus.ONLINE - assert shard_client_obj._activity == None - assert shard_client_obj._idle_since == None + assert shard_client_obj._activity is None + assert shard_client_obj._idle_since is None assert shard_client_obj._is_afk is False @pytest.mark.asyncio @@ -395,7 +533,7 @@ async def test_update_presence_with_optionals(self, shard_client_obj): ) assert shard_client_obj._status == guilds.PresenceStatus.DND - assert shard_client_obj._activity == None + assert shard_client_obj._activity is None assert shard_client_obj._idle_since == datetime_obj assert shard_client_obj._is_afk is True From fcd917b84d4f1b8ff4f64b36d85230671a5cd55a Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Sun, 19 Apr 2020 17:58:26 +0100 Subject: [PATCH 173/922] Shortened REST client file names. --- hikari/__init__.py | 6 +-- hikari/{oauth2.py => applications.py} | 0 hikari/clients/rest_clients/__init__.py | 40 +++++++------- .../{component_base.py => base.py} | 0 .../{channels_component.py => channel.py} | 4 +- .../{gateways_component.py => gateway.py} | 4 +- .../{guilds_component.py => guild.py} | 4 +- .../{invites_component.py => invite.py} | 4 +- .../{current_users_component.py => me.py} | 18 +++---- .../{oauth2_component.py => oauth2.py} | 12 ++--- .../{reactions_component.py => react.py} | 4 +- .../{users_component.py => user.py} | 4 +- .../{voices_component.py => voice.py} | 4 +- .../{webhooks_component.py => webhook.py} | 4 +- hikari/clients/test_client.py | 6 +-- hikari/events.py | 6 +-- hikari/messages.py | 6 +-- .../test_rest_clients/test___init__.py | 20 +++---- .../{test_component_base.py => test_base.py} | 6 +-- ..._channels_component.py => test_channel.py} | 4 +- ..._gateways_component.py => test_gateway.py} | 4 +- ...test_guilds_component.py => test_guild.py} | 12 ++--- ...st_invites_component.py => test_invite.py} | 4 +- ..._current_users_component.py => test_me.py} | 24 ++++----- ...est_oauth2_component.py => test_oauth2.py} | 12 ++--- ...t_reactions_component.py => test_react.py} | 4 +- .../{test_users_component.py => test_user.py} | 4 +- ...test_voices_component.py => test_voice.py} | 4 +- ..._webhooks_component.py => test_webhook.py} | 4 +- .../{test_oauth2.py => test_applications.py} | 52 +++++++++---------- tests/hikari/test_events.py | 6 +-- tests/hikari/test_messages.py | 9 ++-- 32 files changed, 149 insertions(+), 146 deletions(-) rename hikari/{oauth2.py => applications.py} (100%) rename hikari/clients/rest_clients/{component_base.py => base.py} (100%) rename hikari/clients/rest_clients/{channels_component.py => channel.py} (99%) rename hikari/clients/rest_clients/{gateways_component.py => gateway.py} (92%) rename hikari/clients/rest_clients/{guilds_component.py => guild.py} (99%) rename hikari/clients/rest_clients/{invites_component.py => invite.py} (95%) rename hikari/clients/rest_clients/{current_users_component.py => me.py} (93%) rename hikari/clients/rest_clients/{oauth2_component.py => oauth2.py} (77%) rename hikari/clients/rest_clients/{reactions_component.py => react.py} (98%) rename hikari/clients/rest_clients/{users_component.py => user.py} (92%) rename hikari/clients/rest_clients/{voices_component.py => voice.py} (90%) rename hikari/clients/rest_clients/{webhooks_component.py => webhook.py} (98%) rename tests/hikari/clients/test_rest_clients/{test_component_base.py => test_base.py} (87%) rename tests/hikari/clients/test_rest_clients/{test_channels_component.py => test_channel.py} (99%) rename tests/hikari/clients/test_rest_clients/{test_gateways_component.py => test_gateway.py} (94%) rename tests/hikari/clients/test_rest_clients/{test_guilds_component.py => test_guild.py} (99%) rename tests/hikari/clients/test_rest_clients/{test_invites_component.py => test_invite.py} (96%) rename tests/hikari/clients/test_rest_clients/{test_current_users_component.py => test_me.py} (92%) rename tests/hikari/clients/test_rest_clients/{test_oauth2_component.py => test_oauth2.py} (79%) rename tests/hikari/clients/test_rest_clients/{test_reactions_component.py => test_react.py} (98%) rename tests/hikari/clients/test_rest_clients/{test_users_component.py => test_user.py} (93%) rename tests/hikari/clients/test_rest_clients/{test_voices_component.py => test_voice.py} (93%) rename tests/hikari/clients/test_rest_clients/{test_webhooks_component.py => test_webhook.py} (99%) rename tests/hikari/{test_oauth2.py => test_applications.py} (82%) diff --git a/hikari/__init__.py b/hikari/__init__.py index e52f7c73ad..0e1db6f8a5 100644 --- a/hikari/__init__.py +++ b/hikari/__init__.py @@ -33,7 +33,7 @@ from hikari import invites from hikari import messages from hikari import net -from hikari import oauth2 +from hikari import applications from hikari import permissions from hikari import state from hikari import users @@ -59,7 +59,7 @@ from hikari.invites import * from hikari.messages import * from hikari.net import * -from hikari.oauth2 import * +from hikari.applications import * from hikari.permissions import * from hikari.state import * from hikari.unset import * @@ -85,7 +85,7 @@ *invites.__all__, *messages.__all__, *net.__all__, - *oauth2.__all__, + *applications.__all__, *permissions.__all__, *bases.__all__, *state.__all__, diff --git a/hikari/oauth2.py b/hikari/applications.py similarity index 100% rename from hikari/oauth2.py rename to hikari/applications.py diff --git a/hikari/clients/rest_clients/__init__.py b/hikari/clients/rest_clients/__init__.py index f6961838c5..7707f88731 100644 --- a/hikari/clients/rest_clients/__init__.py +++ b/hikari/clients/rest_clients/__init__.py @@ -25,30 +25,30 @@ __all__ = ["RESTClient"] from hikari.clients import configs -from hikari.clients.rest_clients import channels_component -from hikari.clients.rest_clients import current_users_component -from hikari.clients.rest_clients import gateways_component -from hikari.clients.rest_clients import guilds_component -from hikari.clients.rest_clients import invites_component -from hikari.clients.rest_clients import oauth2_component -from hikari.clients.rest_clients import reactions_component -from hikari.clients.rest_clients import users_component -from hikari.clients.rest_clients import voices_component -from hikari.clients.rest_clients import webhooks_component +from hikari.clients.rest_clients import channel +from hikari.clients.rest_clients import me +from hikari.clients.rest_clients import gateway +from hikari.clients.rest_clients import guild +from hikari.clients.rest_clients import invite +from hikari.clients.rest_clients import oauth2 +from hikari.clients.rest_clients import react +from hikari.clients.rest_clients import user +from hikari.clients.rest_clients import voice +from hikari.clients.rest_clients import webhook from hikari.net import rest_sessions class RESTClient( - channels_component.RESTChannelComponent, - current_users_component.RESTCurrentUserComponent, - gateways_component.RESTGatewayComponent, - guilds_component.RESTGuildComponent, - invites_component.RESTInviteComponent, - oauth2_component.RESTOauth2Component, - reactions_component.RESTReactionComponent, - users_component.RESTUserComponent, - voices_component.RESTVoiceComponent, - webhooks_component.RESTWebhookComponent, + channel.RESTChannelComponent, + me.RESTCurrentUserComponent, + gateway.RESTGatewayComponent, + guild.RESTGuildComponent, + invite.RESTInviteComponent, + oauth2.RESTOAuth2Component, + react.RESTReactionComponent, + user.RESTUserComponent, + voice.RESTVoiceComponent, + webhook.RESTWebhookComponent, ): """ A marshalling object-oriented REST API client. diff --git a/hikari/clients/rest_clients/component_base.py b/hikari/clients/rest_clients/base.py similarity index 100% rename from hikari/clients/rest_clients/component_base.py rename to hikari/clients/rest_clients/base.py diff --git a/hikari/clients/rest_clients/channels_component.py b/hikari/clients/rest_clients/channel.py similarity index 99% rename from hikari/clients/rest_clients/channels_component.py rename to hikari/clients/rest_clients/channel.py index 9c1292890e..0897b7116c 100644 --- a/hikari/clients/rest_clients/channels_component.py +++ b/hikari/clients/rest_clients/channel.py @@ -35,7 +35,7 @@ from hikari import permissions as _permissions from hikari import users from hikari import webhooks -from hikari.clients.rest_clients import component_base +from hikari.clients.rest_clients import base from hikari.internal import allowed_mentions from hikari.internal import assertions from hikari.internal import conversions @@ -43,7 +43,7 @@ from hikari.internal import pagination -class RESTChannelComponent(component_base.BaseRESTComponent, abc.ABC): # pylint: disable=W0223 +class RESTChannelComponent(base.BaseRESTComponent, abc.ABC): # pylint: disable=W0223 """The REST client component for handling requests to channel endpoints.""" async def fetch_channel(self, channel: bases.Hashable[_channels.Channel]) -> _channels.Channel: diff --git a/hikari/clients/rest_clients/gateways_component.py b/hikari/clients/rest_clients/gateway.py similarity index 92% rename from hikari/clients/rest_clients/gateways_component.py rename to hikari/clients/rest_clients/gateway.py index 1c87f4ea50..2e318366f5 100644 --- a/hikari/clients/rest_clients/gateways_component.py +++ b/hikari/clients/rest_clients/gateway.py @@ -23,10 +23,10 @@ import abc from hikari import gateway_entities -from hikari.clients.rest_clients import component_base +from hikari.clients.rest_clients import base -class RESTGatewayComponent(component_base.BaseRESTComponent, abc.ABC): # pylint: disable=W0223 +class RESTGatewayComponent(base.BaseRESTComponent, abc.ABC): # pylint: disable=W0223 """The REST client component for handling requests to gateway endpoints.""" async def fetch_gateway_url(self) -> str: diff --git a/hikari/clients/rest_clients/guilds_component.py b/hikari/clients/rest_clients/guild.py similarity index 99% rename from hikari/clients/rest_clients/guilds_component.py rename to hikari/clients/rest_clients/guild.py index 1455fcb272..e1963472dd 100644 --- a/hikari/clients/rest_clients/guilds_component.py +++ b/hikari/clients/rest_clients/guild.py @@ -35,7 +35,7 @@ from hikari import users from hikari import voices from hikari import webhooks -from hikari.clients.rest_clients import component_base +from hikari.clients.rest_clients import base from hikari.internal import conversions from hikari.internal import pagination @@ -44,7 +44,7 @@ def _get_member_id(member: guilds.GuildMember) -> str: return str(member.user.id) -class RESTGuildComponent(component_base.BaseRESTComponent, abc.ABC): # pylint: disable=W0223 +class RESTGuildComponent(base.BaseRESTComponent, abc.ABC): # pylint: disable=W0223 """The REST client component for handling requests to guild endpoints.""" async def fetch_audit_log( diff --git a/hikari/clients/rest_clients/invites_component.py b/hikari/clients/rest_clients/invite.py similarity index 95% rename from hikari/clients/rest_clients/invites_component.py rename to hikari/clients/rest_clients/invite.py index e22e2b95cc..e783f33a2b 100644 --- a/hikari/clients/rest_clients/invites_component.py +++ b/hikari/clients/rest_clients/invite.py @@ -24,10 +24,10 @@ import typing from hikari import invites -from hikari.clients.rest_clients import component_base +from hikari.clients.rest_clients import base -class RESTInviteComponent(component_base.BaseRESTComponent, abc.ABC): # pylint: disable=W0223 +class RESTInviteComponent(base.BaseRESTComponent, abc.ABC): # pylint: disable=W0223 """The REST client component for handling requests to invite endpoints.""" async def fetch_invite( diff --git a/hikari/clients/rest_clients/current_users_component.py b/hikari/clients/rest_clients/me.py similarity index 93% rename from hikari/clients/rest_clients/current_users_component.py rename to hikari/clients/rest_clients/me.py index b615698a23..6f7ad015e2 100644 --- a/hikari/clients/rest_clients/current_users_component.py +++ b/hikari/clients/rest_clients/me.py @@ -27,14 +27,14 @@ from hikari import bases from hikari import channels as _channels from hikari import guilds -from hikari import oauth2 +from hikari import applications from hikari import users -from hikari.clients.rest_clients import component_base +from hikari.clients.rest_clients import base from hikari.internal import conversions from hikari.internal import pagination -class RESTCurrentUserComponent(component_base.BaseRESTComponent, abc.ABC): # pylint: disable=W0223 +class RESTCurrentUserComponent(base.BaseRESTComponent, abc.ABC): # pylint: disable=W0223 """The REST client component for handling requests to ``@me`` endpoints.""" async def fetch_me(self) -> users.MyUser: @@ -77,7 +77,7 @@ async def update_me( ) return users.MyUser.deserialize(payload) - async def fetch_my_connections(self) -> typing.Sequence[oauth2.OwnConnection]: + async def fetch_my_connections(self) -> typing.Sequence[applications.OwnConnection]: """ Get the current user's connections. @@ -93,14 +93,14 @@ async def fetch_my_connections(self) -> typing.Sequence[oauth2.OwnConnection]: A list of connection objects. """ payload = await self._session.get_current_user_connections() - return [oauth2.OwnConnection.deserialize(connection) for connection in payload] + return [applications.OwnConnection.deserialize(connection) for connection in payload] def fetch_my_guilds_after( self, *, after: typing.Union[datetime.datetime, bases.Hashable[guilds.Guild]] = 0, limit: typing.Optional[int] = None, - ) -> typing.AsyncIterator[oauth2.OwnGuild]: + ) -> typing.AsyncIterator[applications.OwnGuild]: """Get an async iterator of the guilds the current user is in. This returns the guilds created after a given guild object/ID or from @@ -140,7 +140,7 @@ def fetch_my_guilds_after( else: after = str(after.id if isinstance(after, bases.UniqueEntity) else int(after)) return pagination.pagination_handler( - deserializer=oauth2.OwnGuild.deserialize, + deserializer=applications.OwnGuild.deserialize, direction="after", request=self._session.get_current_user_guilds, reversing=False, @@ -153,7 +153,7 @@ def fetch_my_guilds_before( *, before: typing.Union[datetime.datetime, bases.Hashable[guilds.Guild], None] = None, limit: typing.Optional[int] = None, - ) -> typing.AsyncIterator[oauth2.OwnGuild]: + ) -> typing.AsyncIterator[applications.OwnGuild]: """Get an async iterator of the guilds the current user is in. This returns the guilds that were created before a given user object/ID @@ -186,7 +186,7 @@ def fetch_my_guilds_before( elif before is not None: before = str(before.id if isinstance(before, bases.UniqueEntity) else int(before)) return pagination.pagination_handler( - deserializer=oauth2.OwnGuild.deserialize, + deserializer=applications.OwnGuild.deserialize, direction="before", request=self._session.get_current_user_guilds, reversing=False, diff --git a/hikari/clients/rest_clients/oauth2_component.py b/hikari/clients/rest_clients/oauth2.py similarity index 77% rename from hikari/clients/rest_clients/oauth2_component.py rename to hikari/clients/rest_clients/oauth2.py index 75e89afbda..885f6b25f5 100644 --- a/hikari/clients/rest_clients/oauth2_component.py +++ b/hikari/clients/rest_clients/oauth2.py @@ -18,18 +18,18 @@ # along with Hikari. If not, see . """The logic for handling all requests to oauth2 endpoints.""" -__all__ = ["RESTOauth2Component"] +__all__ = ["RESTOAuth2Component"] import abc -from hikari import oauth2 -from hikari.clients.rest_clients import component_base +from hikari import applications +from hikari.clients.rest_clients import base -class RESTOauth2Component(component_base.BaseRESTComponent, abc.ABC): # pylint: disable=W0223 +class RESTOAuth2Component(base.BaseRESTComponent, abc.ABC): # pylint: disable=W0223 """The REST client component for handling requests to oauth2 endpoints.""" - async def fetch_my_application_info(self) -> oauth2.Application: + async def fetch_my_application_info(self) -> applications.Application: """Get the current application information. Returns @@ -38,4 +38,4 @@ async def fetch_my_application_info(self) -> oauth2.Application: An application info object. """ payload = await self._session.get_current_application_info() - return oauth2.Application.deserialize(payload) + return applications.Application.deserialize(payload) diff --git a/hikari/clients/rest_clients/reactions_component.py b/hikari/clients/rest_clients/react.py similarity index 98% rename from hikari/clients/rest_clients/reactions_component.py rename to hikari/clients/rest_clients/react.py index 49ed21fcb4..70b5db8c9b 100644 --- a/hikari/clients/rest_clients/reactions_component.py +++ b/hikari/clients/rest_clients/react.py @@ -29,11 +29,11 @@ from hikari import emojis from hikari import messages as _messages from hikari import users -from hikari.clients.rest_clients import component_base +from hikari.clients.rest_clients import base from hikari.internal import pagination -class RESTReactionComponent(component_base.BaseRESTComponent, abc.ABC): # pylint: disable=W0223 +class RESTReactionComponent(base.BaseRESTComponent, abc.ABC): # pylint: disable=W0223 """The REST client component for handling requests to reaction endpoints.""" async def create_reaction( diff --git a/hikari/clients/rest_clients/users_component.py b/hikari/clients/rest_clients/user.py similarity index 92% rename from hikari/clients/rest_clients/users_component.py rename to hikari/clients/rest_clients/user.py index 3879c28e04..7c4ed148eb 100644 --- a/hikari/clients/rest_clients/users_component.py +++ b/hikari/clients/rest_clients/user.py @@ -24,10 +24,10 @@ from hikari import bases from hikari import users -from hikari.clients.rest_clients import component_base +from hikari.clients.rest_clients import base -class RESTUserComponent(component_base.BaseRESTComponent, abc.ABC): # pylint: disable=W0223 +class RESTUserComponent(base.BaseRESTComponent, abc.ABC): # pylint: disable=W0223 """The REST client component for handling requests to user endpoints.""" async def fetch_user(self, user: bases.Hashable[users.User]) -> users.User: diff --git a/hikari/clients/rest_clients/voices_component.py b/hikari/clients/rest_clients/voice.py similarity index 90% rename from hikari/clients/rest_clients/voices_component.py rename to hikari/clients/rest_clients/voice.py index 5c8ad837a1..5cdc1e1e79 100644 --- a/hikari/clients/rest_clients/voices_component.py +++ b/hikari/clients/rest_clients/voice.py @@ -24,10 +24,10 @@ import typing from hikari import voices -from hikari.clients.rest_clients import component_base +from hikari.clients.rest_clients import base -class RESTVoiceComponent(component_base.BaseRESTComponent, abc.ABC): # pylint: disable=W0223 +class RESTVoiceComponent(base.BaseRESTComponent, abc.ABC): # pylint: disable=W0223 """The REST client component for handling requests to voice endpoints.""" async def fetch_voice_regions(self) -> typing.Sequence[voices.VoiceRegion]: diff --git a/hikari/clients/rest_clients/webhooks_component.py b/hikari/clients/rest_clients/webhook.py similarity index 98% rename from hikari/clients/rest_clients/webhooks_component.py rename to hikari/clients/rest_clients/webhook.py index 24eb221ccd..2fbb1ff397 100644 --- a/hikari/clients/rest_clients/webhooks_component.py +++ b/hikari/clients/rest_clients/webhook.py @@ -31,12 +31,12 @@ from hikari import messages as _messages from hikari import users from hikari import webhooks -from hikari.clients.rest_clients import component_base +from hikari.clients.rest_clients import base from hikari.internal import allowed_mentions from hikari.internal import conversions -class RESTWebhookComponent(component_base.BaseRESTComponent, abc.ABC): # pylint: disable=W0223 +class RESTWebhookComponent(base.BaseRESTComponent, abc.ABC): # pylint: disable=W0223 """The REST client component for handling requests to webhook endpoints.""" async def fetch_webhook( diff --git a/hikari/clients/test_client.py b/hikari/clients/test_client.py index 4f2140993e..26bccd5e63 100644 --- a/hikari/clients/test_client.py +++ b/hikari/clients/test_client.py @@ -32,9 +32,9 @@ import click -import hikari +import hikari # lgtm [py/import-and-import-from] +from hikari import intents as _intents from hikari.internal import conversions -from hikari.net import codes _LOGGER_LEVELS: typing.Final[typing.Sequence[str]] = ["DEBUG", "INFO", "WARNING", "ERROR", "NOTSET"] @@ -75,7 +75,7 @@ def run_gateway(compression, color, debug, intents, logger, shards, token, verif """ if intents is not None: intents = intents.split(",") - intents = conversions.dereference_int_flag(codes.GatewayIntent, intents) + intents = conversions.dereference_int_flag(_intents.Intent, intents) logging.captureWarnings(True) diff --git a/hikari/events.py b/hikari/events.py index a968358390..9a82323675 100644 --- a/hikari/events.py +++ b/hikari/events.py @@ -77,7 +77,7 @@ from hikari import guilds from hikari import invites from hikari import messages -from hikari import oauth2 +from hikari import applications from hikari import unset from hikari import users from hikari import voices @@ -888,8 +888,8 @@ class MessageUpdateEvent(HikariEvent, bases.UniqueEntity, marshaller.Deserializa #: The message's application. #: #: :type: :obj:`~typing.Union` [ :obj:`~hikari.oauth2.Application`, :obj:`~hikari.unset.UNSET` ] - application: typing.Optional[oauth2.Application] = marshaller.attrib( - deserializer=oauth2.Application.deserialize, if_undefined=unset.Unset, default=unset.UNSET + application: typing.Optional[applications.Application] = marshaller.attrib( + deserializer=applications.Application.deserialize, if_undefined=unset.Unset, default=unset.UNSET ) #: The message's crossposted reference data. diff --git a/hikari/messages.py b/hikari/messages.py index 89b3d5a468..c3b175119f 100644 --- a/hikari/messages.py +++ b/hikari/messages.py @@ -38,7 +38,7 @@ from hikari import embeds as _embeds from hikari import emojis as _emojis from hikari import guilds -from hikari import oauth2 +from hikari import applications from hikari import users from hikari.internal import conversions from hikari.internal import marshaller @@ -374,8 +374,8 @@ class Message(bases.UniqueEntity, marshaller.Deserializable): #: The message application. #: #: :type: :obj:`~hikari.oauth2.Application`, optional - application: typing.Optional[oauth2.Application] = marshaller.attrib( - deserializer=oauth2.Application.deserialize, if_undefined=None, default=None + application: typing.Optional[applications.Application] = marshaller.attrib( + deserializer=applications.Application.deserialize, if_undefined=None, default=None ) #: The message crossposted reference data. diff --git a/tests/hikari/clients/test_rest_clients/test___init__.py b/tests/hikari/clients/test_rest_clients/test___init__.py index c0c1a90b0e..f2d72e3a63 100644 --- a/tests/hikari/clients/test_rest_clients/test___init__.py +++ b/tests/hikari/clients/test_rest_clients/test___init__.py @@ -55,16 +55,16 @@ def test_inheritance(self): for attr, routine in ( member for component in [ - rest_clients.channels_component.RESTChannelComponent, - rest_clients.current_users_component.RESTCurrentUserComponent, - rest_clients.gateways_component.RESTGatewayComponent, - rest_clients.guilds_component.RESTGuildComponent, - rest_clients.invites_component.RESTInviteComponent, - rest_clients.oauth2_component.RESTOauth2Component, - rest_clients.reactions_component.RESTReactionComponent, - rest_clients.users_component.RESTUserComponent, - rest_clients.voices_component.RESTVoiceComponent, - rest_clients.webhooks_component.RESTWebhookComponent, + rest_clients.channel.RESTChannelComponent, + rest_clients.me.RESTCurrentUserComponent, + rest_clients.gateway.RESTGatewayComponent, + rest_clients.guild.RESTGuildComponent, + rest_clients.invite.RESTInviteComponent, + rest_clients.oauth2.RESTOAuth2Component, + rest_clients.react.RESTReactionComponent, + rest_clients.user.RESTUserComponent, + rest_clients.voice.RESTVoiceComponent, + rest_clients.webhook.RESTWebhookComponent, ] for member in inspect.getmembers(component, inspect.isroutine) ): diff --git a/tests/hikari/clients/test_rest_clients/test_component_base.py b/tests/hikari/clients/test_rest_clients/test_base.py similarity index 87% rename from tests/hikari/clients/test_rest_clients/test_component_base.py rename to tests/hikari/clients/test_rest_clients/test_base.py index d9b7550f97..b54aee2e7b 100644 --- a/tests/hikari/clients/test_rest_clients/test_component_base.py +++ b/tests/hikari/clients/test_rest_clients/test_base.py @@ -19,7 +19,7 @@ import mock import pytest -from hikari.clients.rest_clients import component_base +from hikari.clients.rest_clients import base from hikari.net import rest_sessions @@ -29,8 +29,8 @@ def low_level_rest_impl(self) -> rest_sessions.LowLevelRestfulClient: return mock.MagicMock(rest_sessions.LowLevelRestfulClient) @pytest.fixture() - def rest_clients_impl(self, low_level_rest_impl) -> component_base.BaseRESTComponent: - class RestClientImpl(component_base.BaseRESTComponent): + def rest_clients_impl(self, low_level_rest_impl) -> base.BaseRESTComponent: + class RestClientImpl(base.BaseRESTComponent): def __init__(self): super().__init__(low_level_rest_impl) diff --git a/tests/hikari/clients/test_rest_clients/test_channels_component.py b/tests/hikari/clients/test_rest_clients/test_channel.py similarity index 99% rename from tests/hikari/clients/test_rest_clients/test_channels_component.py rename to tests/hikari/clients/test_rest_clients/test_channel.py index d84ee89d98..00f9d22659 100644 --- a/tests/hikari/clients/test_rest_clients/test_channels_component.py +++ b/tests/hikari/clients/test_rest_clients/test_channel.py @@ -32,7 +32,7 @@ from hikari import snowflakes from hikari import users from hikari import webhooks -from hikari.clients.rest_clients import channels_component +from hikari.clients.rest_clients import channel from hikari.internal import allowed_mentions from hikari.internal import conversions from hikari.internal import pagination @@ -45,7 +45,7 @@ class TestRESTChannelLogging: def rest_channel_logic_impl(self): mock_low_level_restful_client = mock.MagicMock(rest_sessions.LowLevelRestfulClient) - class RESTChannelLogicImpl(channels_component.RESTChannelComponent): + class RESTChannelLogicImpl(channel.RESTChannelComponent): def __init__(self): super().__init__(mock_low_level_restful_client) diff --git a/tests/hikari/clients/test_rest_clients/test_gateways_component.py b/tests/hikari/clients/test_rest_clients/test_gateway.py similarity index 94% rename from tests/hikari/clients/test_rest_clients/test_gateways_component.py rename to tests/hikari/clients/test_rest_clients/test_gateway.py index ef3a239dd3..1fef2150f1 100644 --- a/tests/hikari/clients/test_rest_clients/test_gateways_component.py +++ b/tests/hikari/clients/test_rest_clients/test_gateway.py @@ -21,7 +21,7 @@ import pytest from hikari import gateway_entities -from hikari.clients.rest_clients import gateways_component +from hikari.clients.rest_clients import gateway from hikari.net import rest_sessions @@ -30,7 +30,7 @@ class TestRESTReactionLogic: def rest_gateway_logic_impl(self): mock_low_level_restful_client = mock.MagicMock(rest_sessions.LowLevelRestfulClient) - class RESTGatewayLogicImpl(gateways_component.RESTGatewayComponent): + class RESTGatewayLogicImpl(gateway.RESTGatewayComponent): def __init__(self): super().__init__(mock_low_level_restful_client) diff --git a/tests/hikari/clients/test_rest_clients/test_guilds_component.py b/tests/hikari/clients/test_rest_clients/test_guild.py similarity index 99% rename from tests/hikari/clients/test_rest_clients/test_guilds_component.py rename to tests/hikari/clients/test_rest_clients/test_guild.py index 0cc0a69f6e..94dc8d6c5c 100644 --- a/tests/hikari/clients/test_rest_clients/test_guilds_component.py +++ b/tests/hikari/clients/test_rest_clients/test_guild.py @@ -33,7 +33,7 @@ from hikari import users from hikari import voices from hikari import webhooks -from hikari.clients.rest_clients import guilds_component +from hikari.clients.rest_clients import guild as _guild from hikari.internal import conversions from hikari.internal import pagination from hikari.net import rest_sessions @@ -44,7 +44,7 @@ def test__get_member_id(): member = mock.MagicMock( guilds.GuildMember, user=mock.MagicMock(users.User, id=123123123, __int__=users.User.__int__) ) - assert guilds_component._get_member_id(member) == "123123123" + assert _guild._get_member_id(member) == "123123123" class TestRESTGuildLogic: @@ -52,7 +52,7 @@ class TestRESTGuildLogic: def rest_guild_logic_impl(self): mock_low_level_restful_client = mock.MagicMock(rest_sessions.LowLevelRestfulClient) - class RESTGuildLogicImpl(guilds_component.RESTGuildComponent): + class RESTGuildLogicImpl(_guild.RESTGuildComponent): def __init__(self): super().__init__(mock_low_level_restful_client) @@ -558,7 +558,7 @@ def test_fetch_members_after_with_optionals(self, rest_guild_logic_impl, guild, reversing=False, start="115590097100865541", limit=34, - id_getter=guilds_component._get_member_id, + id_getter=_guild._get_member_id, ) @_helpers.parametrize_valid_id_formats_for_models("guild", 574921006817476608, guilds.Guild) @@ -574,7 +574,7 @@ def test_fetch_members_after_without_optionals(self, rest_guild_logic_impl, guil reversing=False, start="0", limit=None, - id_getter=guilds_component._get_member_id, + id_getter=_guild._get_member_id, ) def test_fetch_members_after_with_datetime_object(self, rest_guild_logic_impl): @@ -590,7 +590,7 @@ def test_fetch_members_after_with_datetime_object(self, rest_guild_logic_impl): reversing=False, start="537340988620800000", limit=None, - id_getter=guilds_component._get_member_id, + id_getter=_guild._get_member_id, ) @pytest.mark.asyncio diff --git a/tests/hikari/clients/test_rest_clients/test_invites_component.py b/tests/hikari/clients/test_rest_clients/test_invite.py similarity index 96% rename from tests/hikari/clients/test_rest_clients/test_invites_component.py rename to tests/hikari/clients/test_rest_clients/test_invite.py index 1831ca6d74..3d1a1e5040 100644 --- a/tests/hikari/clients/test_rest_clients/test_invites_component.py +++ b/tests/hikari/clients/test_rest_clients/test_invite.py @@ -20,7 +20,7 @@ import pytest from hikari import invites -from hikari.clients.rest_clients import invites_component +from hikari.clients.rest_clients import invite from hikari.net import rest_sessions @@ -29,7 +29,7 @@ class TestRESTInviteLogic: def rest_invite_logic_impl(self): mock_low_level_restful_client = mock.MagicMock(rest_sessions.LowLevelRestfulClient) - class RESTInviteLogicImpl(invites_component.RESTInviteComponent): + class RESTInviteLogicImpl(invite.RESTInviteComponent): def __init__(self): super().__init__(mock_low_level_restful_client) diff --git a/tests/hikari/clients/test_rest_clients/test_current_users_component.py b/tests/hikari/clients/test_rest_clients/test_me.py similarity index 92% rename from tests/hikari/clients/test_rest_clients/test_current_users_component.py rename to tests/hikari/clients/test_rest_clients/test_me.py index e69af64607..c1d3ae6424 100644 --- a/tests/hikari/clients/test_rest_clients/test_current_users_component.py +++ b/tests/hikari/clients/test_rest_clients/test_me.py @@ -25,9 +25,9 @@ from hikari import channels from hikari import guilds -from hikari import oauth2 +from hikari import applications from hikari import users -from hikari.clients.rest_clients import current_users_component +from hikari.clients.rest_clients import me from hikari.internal import conversions from hikari.internal import pagination from hikari.net import rest_sessions @@ -39,7 +39,7 @@ class TestRESTInviteLogic: def rest_clients_impl(self): mock_low_level_restful_client = mock.MagicMock(rest_sessions.LowLevelRestfulClient) - class RESTCurrentUserLogicImpl(current_users_component.RESTCurrentUserComponent): + class RESTCurrentUserLogicImpl(me.RESTCurrentUserComponent): def __init__(self): super().__init__(mock_low_level_restful_client) @@ -86,12 +86,12 @@ async def test_update_me_without_optionals(self, rest_clients_impl): @pytest.mark.asyncio async def test_fetch_my_connections(self, rest_clients_impl): mock_connection_payload = {"id": "odnkwu", "type": "twitch", "name": "eric"} - mock_connection_obj = mock.MagicMock(oauth2.OwnConnection) + mock_connection_obj = mock.MagicMock(applications.OwnConnection) rest_clients_impl._session.get_current_user_connections.return_value = [mock_connection_payload] - with mock.patch.object(oauth2.OwnConnection, "deserialize", return_value=mock_connection_obj): + with mock.patch.object(applications.OwnConnection, "deserialize", return_value=mock_connection_obj): assert await rest_clients_impl.fetch_my_connections() == [mock_connection_obj] rest_clients_impl._session.get_current_user_connections.assert_called_once() - oauth2.OwnConnection.deserialize.assert_called_once_with(mock_connection_payload) + applications.OwnConnection.deserialize.assert_called_once_with(mock_connection_payload) @_helpers.parametrize_valid_id_formats_for_models("guild", 574921006817476608, guilds.Guild) def test_fetch_my_guilds_after_with_optionals(self, rest_clients_impl, guild): @@ -99,7 +99,7 @@ def test_fetch_my_guilds_after_with_optionals(self, rest_clients_impl, guild): with mock.patch.object(pagination, "pagination_handler", return_value=mock_generator): assert rest_clients_impl.fetch_my_guilds_after(after=guild, limit=50) is mock_generator pagination.pagination_handler.assert_called_once_with( - deserializer=oauth2.OwnGuild.deserialize, + deserializer=applications.OwnGuild.deserialize, direction="after", request=rest_clients_impl._session.get_current_user_guilds, reversing=False, @@ -112,7 +112,7 @@ def test_fetch_my_guilds_after_without_optionals(self, rest_clients_impl): with mock.patch.object(pagination, "pagination_handler", return_value=mock_generator): assert rest_clients_impl.fetch_my_guilds_after() is mock_generator pagination.pagination_handler.assert_called_once_with( - deserializer=oauth2.OwnGuild.deserialize, + deserializer=applications.OwnGuild.deserialize, direction="after", request=rest_clients_impl._session.get_current_user_guilds, reversing=False, @@ -126,7 +126,7 @@ def test_fetch_my_guilds_after_with_datetime_object(self, rest_clients_impl): with mock.patch.object(pagination, "pagination_handler", return_value=mock_generator): assert rest_clients_impl.fetch_my_guilds_after(after=date) is mock_generator pagination.pagination_handler.assert_called_once_with( - deserializer=oauth2.OwnGuild.deserialize, + deserializer=applications.OwnGuild.deserialize, direction="after", request=rest_clients_impl._session.get_current_user_guilds, reversing=False, @@ -140,7 +140,7 @@ def test_fetch_my_guilds_before_with_optionals(self, rest_clients_impl, guild): with mock.patch.object(pagination, "pagination_handler", return_value=mock_generator): assert rest_clients_impl.fetch_my_guilds_before(before=guild, limit=50) is mock_generator pagination.pagination_handler.assert_called_once_with( - deserializer=oauth2.OwnGuild.deserialize, + deserializer=applications.OwnGuild.deserialize, direction="before", request=rest_clients_impl._session.get_current_user_guilds, reversing=False, @@ -153,7 +153,7 @@ def test_fetch_my_guilds_before_without_optionals(self, rest_clients_impl): with mock.patch.object(pagination, "pagination_handler", return_value=mock_generator): assert rest_clients_impl.fetch_my_guilds_before() is mock_generator pagination.pagination_handler.assert_called_once_with( - deserializer=oauth2.OwnGuild.deserialize, + deserializer=applications.OwnGuild.deserialize, direction="before", request=rest_clients_impl._session.get_current_user_guilds, reversing=False, @@ -167,7 +167,7 @@ def test_fetch_my_guilds_before_with_datetime_object(self, rest_clients_impl): with mock.patch.object(pagination, "pagination_handler", return_value=mock_generator): assert rest_clients_impl.fetch_my_guilds_before(before=date) is mock_generator pagination.pagination_handler.assert_called_once_with( - deserializer=oauth2.OwnGuild.deserialize, + deserializer=applications.OwnGuild.deserialize, direction="before", request=rest_clients_impl._session.get_current_user_guilds, reversing=False, diff --git a/tests/hikari/clients/test_rest_clients/test_oauth2_component.py b/tests/hikari/clients/test_rest_clients/test_oauth2.py similarity index 79% rename from tests/hikari/clients/test_rest_clients/test_oauth2_component.py rename to tests/hikari/clients/test_rest_clients/test_oauth2.py index 5b697896aa..0a6ebf4745 100644 --- a/tests/hikari/clients/test_rest_clients/test_oauth2_component.py +++ b/tests/hikari/clients/test_rest_clients/test_oauth2.py @@ -20,8 +20,8 @@ import mock import pytest -from hikari import oauth2 -from hikari.clients.rest_clients import oauth2_component +from hikari import applications +from hikari.clients.rest_clients import oauth2 from hikari.net import rest_sessions @@ -30,7 +30,7 @@ class TestRESTReactionLogic: def rest_oauth2_logic_impl(self): mock_low_level_restful_client = mock.MagicMock(rest_sessions.LowLevelRestfulClient) - class RESTOauth2LogicImpl(oauth2_component.RESTOauth2Component): + class RESTOauth2LogicImpl(oauth2.RESTOAuth2Component): def __init__(self): super().__init__(mock_low_level_restful_client) @@ -39,9 +39,9 @@ def __init__(self): @pytest.mark.asyncio async def test_fetch_my_application_info(self, rest_oauth2_logic_impl): mock_application_payload = {"id": "2929292", "name": "blah blah", "description": "an app"} - mock_application_obj = mock.MagicMock(oauth2.Application) + mock_application_obj = mock.MagicMock(applications.Application) rest_oauth2_logic_impl._session.get_current_application_info.return_value = mock_application_payload - with mock.patch.object(oauth2.Application, "deserialize", return_value=mock_application_obj): + with mock.patch.object(applications.Application, "deserialize", return_value=mock_application_obj): assert await rest_oauth2_logic_impl.fetch_my_application_info() is mock_application_obj rest_oauth2_logic_impl._session.get_current_application_info.assert_called_once_with() - oauth2.Application.deserialize.assert_called_once_with(mock_application_payload) + applications.Application.deserialize.assert_called_once_with(mock_application_payload) diff --git a/tests/hikari/clients/test_rest_clients/test_reactions_component.py b/tests/hikari/clients/test_rest_clients/test_react.py similarity index 98% rename from tests/hikari/clients/test_rest_clients/test_reactions_component.py rename to tests/hikari/clients/test_rest_clients/test_react.py index 466eceb7a5..cf601cd341 100644 --- a/tests/hikari/clients/test_rest_clients/test_reactions_component.py +++ b/tests/hikari/clients/test_rest_clients/test_react.py @@ -25,7 +25,7 @@ from hikari import emojis from hikari import messages from hikari import users -from hikari.clients.rest_clients import reactions_component +from hikari.clients.rest_clients import react from hikari.internal import pagination from hikari.net import rest_sessions from tests.hikari import _helpers @@ -36,7 +36,7 @@ class TestRESTReactionLogic: def rest_reaction_logic_impl(self): mock_low_level_restful_client = mock.MagicMock(rest_sessions.LowLevelRestfulClient) - class RESTReactionLogicImpl(reactions_component.RESTReactionComponent): + class RESTReactionLogicImpl(react.RESTReactionComponent): def __init__(self): super().__init__(mock_low_level_restful_client) diff --git a/tests/hikari/clients/test_rest_clients/test_users_component.py b/tests/hikari/clients/test_rest_clients/test_user.py similarity index 93% rename from tests/hikari/clients/test_rest_clients/test_users_component.py rename to tests/hikari/clients/test_rest_clients/test_user.py index f4a5eb5f85..82dd32795d 100644 --- a/tests/hikari/clients/test_rest_clients/test_users_component.py +++ b/tests/hikari/clients/test_rest_clients/test_user.py @@ -20,7 +20,7 @@ import pytest from hikari import users -from hikari.clients.rest_clients import users_component +from hikari.clients.rest_clients import user from hikari.net import rest_sessions from tests.hikari import _helpers @@ -30,7 +30,7 @@ class TestRESTUserLogic: def rest_user_logic_impl(self): mock_low_level_restful_client = mock.MagicMock(rest_sessions.LowLevelRestfulClient) - class RESTUserLogicImpl(users_component.RESTUserComponent): + class RESTUserLogicImpl(user.RESTUserComponent): def __init__(self): super().__init__(mock_low_level_restful_client) diff --git a/tests/hikari/clients/test_rest_clients/test_voices_component.py b/tests/hikari/clients/test_rest_clients/test_voice.py similarity index 93% rename from tests/hikari/clients/test_rest_clients/test_voices_component.py rename to tests/hikari/clients/test_rest_clients/test_voice.py index af69f78c5f..8ff852760f 100644 --- a/tests/hikari/clients/test_rest_clients/test_voices_component.py +++ b/tests/hikari/clients/test_rest_clients/test_voice.py @@ -20,7 +20,7 @@ import pytest from hikari import voices -from hikari.clients.rest_clients import voices_component +from hikari.clients.rest_clients import voice from hikari.net import rest_sessions @@ -29,7 +29,7 @@ class TestRESTUserLogic: def rest_voice_logic_impl(self): mock_low_level_restful_client = mock.MagicMock(rest_sessions.LowLevelRestfulClient) - class RESTVoiceLogicImpl(voices_component.RESTVoiceComponent): + class RESTVoiceLogicImpl(voice.RESTVoiceComponent): def __init__(self): super().__init__(mock_low_level_restful_client) diff --git a/tests/hikari/clients/test_rest_clients/test_webhooks_component.py b/tests/hikari/clients/test_rest_clients/test_webhook.py similarity index 99% rename from tests/hikari/clients/test_rest_clients/test_webhooks_component.py rename to tests/hikari/clients/test_rest_clients/test_webhook.py index 420d161b20..d2d323008e 100644 --- a/tests/hikari/clients/test_rest_clients/test_webhooks_component.py +++ b/tests/hikari/clients/test_rest_clients/test_webhook.py @@ -26,7 +26,7 @@ from hikari import media from hikari import messages from hikari import webhooks -from hikari.clients.rest_clients import webhooks_component +from hikari.clients.rest_clients import webhook from hikari.internal import allowed_mentions from hikari.internal import conversions from hikari.net import rest_sessions @@ -38,7 +38,7 @@ class TestRESTUserLogic: def rest_webhook_logic_impl(self): mock_low_level_restful_client = mock.MagicMock(rest_sessions.LowLevelRestfulClient) - class RESTWebhookLogicImpl(webhooks_component.RESTWebhookComponent): + class RESTWebhookLogicImpl(webhook.RESTWebhookComponent): def __init__(self): super().__init__(mock_low_level_restful_client) diff --git a/tests/hikari/test_oauth2.py b/tests/hikari/test_applications.py similarity index 82% rename from tests/hikari/test_oauth2.py rename to tests/hikari/test_applications.py index 5da8419a3f..5727730bff 100644 --- a/tests/hikari/test_oauth2.py +++ b/tests/hikari/test_applications.py @@ -20,7 +20,7 @@ import pytest from hikari import guilds -from hikari import oauth2 +from hikari import applications from hikari import users from hikari.internal import urls from tests.hikari import _helpers @@ -108,7 +108,7 @@ class TestOwnConnection: def test_deserialize(self, own_connection_payload, test_partial_integration): mock_integration_obj = mock.MagicMock(guilds.PartialGuildIntegration) with mock.patch.object(guilds.PartialGuildIntegration, "deserialize", return_value=mock_integration_obj): - connection_obj = oauth2.OwnConnection.deserialize(own_connection_payload) + connection_obj = applications.OwnConnection.deserialize(own_connection_payload) guilds.PartialGuildIntegration.deserialize.assert_called_once_with(test_partial_integration) assert connection_obj.id == "2513849648" assert connection_obj.name == "FS" @@ -118,12 +118,12 @@ def test_deserialize(self, own_connection_payload, test_partial_integration): assert connection_obj.is_verified is True assert connection_obj.is_friend_syncing is False assert connection_obj.is_showing_activity is True - assert connection_obj.visibility is oauth2.ConnectionVisibility.NONE + assert connection_obj.visibility is applications.ConnectionVisibility.NONE class TestOwnGuild: def test_deserialize(self, own_guild_payload): - own_guild_obj = oauth2.OwnGuild.deserialize(own_guild_payload) + own_guild_obj = applications.OwnGuild.deserialize(own_guild_payload) assert own_guild_obj.is_owner is False assert own_guild_obj.my_permissions == 2147483647 @@ -131,7 +131,7 @@ def test_deserialize(self, own_guild_payload): class TestApplicationOwner: @pytest.fixture() def owner_obj(self, owner_payload): - return oauth2.ApplicationOwner.deserialize(owner_payload) + return applications.ApplicationOwner.deserialize(owner_payload) def test_deserialize(self, owner_obj): assert owner_obj.username == "agent 47" @@ -151,22 +151,22 @@ class TestTeamMember: def test_deserialize(self, member_payload, team_user_payload): mock_team_user = mock.MagicMock(users.User) with _helpers.patch_marshal_attr( - oauth2.TeamMember, "user", deserializer=users.User.deserialize, return_value=mock_team_user + applications.TeamMember, "user", deserializer=users.User.deserialize, return_value=mock_team_user ) as patched_deserializer: - member_obj = oauth2.TeamMember.deserialize(member_payload) + member_obj = applications.TeamMember.deserialize(member_payload) patched_deserializer.assert_called_once_with(team_user_payload) assert member_obj.user is mock_team_user - assert member_obj.membership_state is oauth2.TeamMembershipState.INVITED + assert member_obj.membership_state is applications.TeamMembershipState.INVITED assert member_obj.permissions == {"*"} assert member_obj.team_id == 209333111222 class TestTeam: def test_deserialize(self, team_payload, member_payload): - mock_member = mock.MagicMock(oauth2.Team, user=mock.MagicMock(id=123)) - with mock.patch.object(oauth2.TeamMember, "deserialize", return_value=mock_member): - team_obj = oauth2.Team.deserialize(team_payload) - oauth2.TeamMember.deserialize.assert_called_once_with(member_payload) + mock_member = mock.MagicMock(applications.Team, user=mock.MagicMock(id=123)) + with mock.patch.object(applications.TeamMember, "deserialize", return_value=mock_member): + team_obj = applications.Team.deserialize(team_payload) + applications.TeamMember.deserialize.assert_called_once_with(member_payload) assert team_obj.members == {123: mock_member} assert team_obj.icon_hash == "hashtag" assert team_obj.id == 202020202 @@ -174,20 +174,20 @@ def test_deserialize(self, team_payload, member_payload): @pytest.fixture() def team_obj(self, team_payload): - return oauth2.Team(id=None, icon_hash="3o2o32o", members=None, owner_user_id=None,) + return applications.Team(id=None, icon_hash="3o2o32o", members=None, owner_user_id=None,) def test_format_icon_url(self): - mock_team = mock.MagicMock(oauth2.Team, icon_hash="3o2o32o", id=22323) + mock_team = mock.MagicMock(applications.Team, icon_hash="3o2o32o", id=22323) mock_url = "https://cdn.discordapp.com/team-icons/22323/3o2o32o.jpg?size=64" with mock.patch.object(urls, "generate_cdn_url", return_value=mock_url): - url = oauth2.Team.format_icon_url(mock_team, fmt="jpg", size=64) + url = applications.Team.format_icon_url(mock_team, fmt="jpg", size=64) urls.generate_cdn_url.assert_called_once_with("team-icons", "22323", "3o2o32o", fmt="jpg", size=64) assert url == mock_url def test_format_icon_url_returns_none(self): - mock_team = mock.MagicMock(oauth2.Team, icon_hash=None, id=22323) + mock_team = mock.MagicMock(applications.Team, icon_hash=None, id=22323) with mock.patch.object(urls, "generate_cdn_url", return_value=...): - url = oauth2.Team.format_icon_url(mock_team, fmt="jpg", size=64) + url = applications.Team.format_icon_url(mock_team, fmt="jpg", size=64) urls.generate_cdn_url.assert_not_called() assert url is None @@ -201,9 +201,9 @@ def test_icon_url(self, team_obj): class TestApplication: def test_deserialize(self, application_information_payload, team_payload, owner_payload): - application_obj = oauth2.Application.deserialize(application_information_payload) - assert application_obj.team == oauth2.Team.deserialize(team_payload) - assert application_obj.owner == oauth2.ApplicationOwner.deserialize(owner_payload) + application_obj = applications.Application.deserialize(application_information_payload) + assert application_obj.team == applications.Team.deserialize(team_payload) + assert application_obj.owner == applications.ApplicationOwner.deserialize(owner_payload) assert application_obj.id == 209333111222 assert application_obj.name == "Dream Sweet in Sea Major" assert application_obj.icon_hash == "iwiwiwiwiw" @@ -220,7 +220,7 @@ def test_deserialize(self, application_information_payload, team_payload, owner_ @pytest.fixture() def application_obj(self, application_information_payload): - return oauth2.Application( + return applications.Application( team=None, owner=None, id=209333111222, @@ -240,7 +240,7 @@ def application_obj(self, application_information_payload): @pytest.fixture() def mock_application(self): - return mock.MagicMock(oauth2.Application, id=22222) + return mock.MagicMock(applications.Application, id=22222) def test_icon_url(self, application_obj): mock_url = "https://cdn.discordapp.com/app-icons/209333111222/iwiwiwiwiw.png?size=4096" @@ -253,14 +253,14 @@ def test_format_icon_url(self, mock_application): mock_application.icon_hash = "wosososoos" mock_url = "https://cdn.discordapp.com/app-icons/22222/wosososoos.jpg?size=4" with mock.patch.object(urls, "generate_cdn_url", return_value=mock_url): - url = oauth2.Application.format_icon_url(mock_application, fmt="jpg", size=4) + url = applications.Application.format_icon_url(mock_application, fmt="jpg", size=4) urls.generate_cdn_url.assert_called_once_with("app-icons", "22222", "wosososoos", fmt="jpg", size=4) assert url == mock_url def test_format_icon_url_returns_none(self, mock_application): mock_application.icon_hash = None with mock.patch.object(urls, "generate_cdn_url", return_value=...): - url = oauth2.Application.format_icon_url(mock_application, fmt="jpg", size=4) + url = applications.Application.format_icon_url(mock_application, fmt="jpg", size=4) urls.generate_cdn_url.assert_not_called() assert url is None @@ -275,13 +275,13 @@ def test_format_cover_image_url(self, mock_application): mock_application.cover_image_hash = "wowowowowo" mock_url = "https://cdn.discordapp.com/app-assets/22222/wowowowowo.jpg?size=42" with mock.patch.object(urls, "generate_cdn_url", return_value=mock_url): - url = oauth2.Application.format_cover_image_url(mock_application, fmt="jpg", size=42) + url = applications.Application.format_cover_image_url(mock_application, fmt="jpg", size=42) urls.generate_cdn_url.assert_called_once_with("app-assets", "22222", "wowowowowo", fmt="jpg", size=42) assert url == mock_url def test_format_cover_image_url_returns_none(self, mock_application): mock_application.cover_image_hash = None with mock.patch.object(urls, "generate_cdn_url", return_value=...): - url = oauth2.Application.format_cover_image_url(mock_application, fmt="jpg", size=42) + url = applications.Application.format_cover_image_url(mock_application, fmt="jpg", size=42) urls.generate_cdn_url.assert_not_called() assert url is None diff --git a/tests/hikari/test_events.py b/tests/hikari/test_events.py index 774e423fca..86f7cfa6f8 100644 --- a/tests/hikari/test_events.py +++ b/tests/hikari/test_events.py @@ -29,7 +29,7 @@ from hikari import guilds from hikari import invites from hikari import messages -from hikari import oauth2 +from hikari import applications from hikari import unset from hikari import users from hikari.internal import conversions @@ -638,7 +638,7 @@ def test_deserialize( mock_embed = mock.MagicMock(embeds.Embed) mock_reaction = mock.MagicMock(messages.Reaction) mock_activity = mock.MagicMock(messages.MessageActivity) - mock_application = mock.MagicMock(oauth2.Application) + mock_application = mock.MagicMock(applications.Application) mock_reference = mock.MagicMock(messages.MessageCrosspost) stack = contextlib.ExitStack() patched_author_deserializer = stack.enter_context( @@ -682,7 +682,7 @@ def test_deserialize( _helpers.patch_marshal_attr( events.MessageUpdateEvent, "application", - deserializer=oauth2.Application.deserialize, + deserializer=applications.Application.deserialize, return_value=mock_application, ) ) diff --git a/tests/hikari/test_messages.py b/tests/hikari/test_messages.py index 58e091fba8..47de52842c 100644 --- a/tests/hikari/test_messages.py +++ b/tests/hikari/test_messages.py @@ -26,7 +26,7 @@ from hikari import emojis from hikari import guilds from hikari import messages -from hikari import oauth2 +from hikari import applications from hikari import users from hikari.internal import conversions from tests.hikari import _helpers @@ -188,7 +188,7 @@ def test_deserialize( mock_datetime = mock.MagicMock(datetime.datetime) mock_datetime2 = mock.MagicMock(datetime.datetime) mock_emoji = mock.MagicMock(messages._emojis) - mock_app = mock.MagicMock(oauth2.Application) + mock_app = mock.MagicMock(applications.Application) stack = contextlib.ExitStack() patched_author_deserializer = stack.enter_context( @@ -216,7 +216,10 @@ def test_deserialize( ) patched_application_deserializer = stack.enter_context( _helpers.patch_marshal_attr( - messages.Message, "application", deserializer=oauth2.Application.deserialize, return_value=mock_app, + messages.Message, + "application", + deserializer=applications.Application.deserialize, + return_value=mock_app, ) ) patched_emoji_deserializer = stack.enter_context( From ad96ec03aa757af0fe2570b43ed8a9514ea2e522 Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Mon, 20 Apr 2020 19:33:45 +0100 Subject: [PATCH 174/922] Final reorganisation and naming consistency sweep. --- hikari/clients/__init__.py | 16 +++--- hikari/clients/{bot_clients.py => bot.py} | 53 +++++++++---------- .../{rest_clients => rest}/__init__.py | 24 ++++----- hikari/clients/{rest_clients => rest}/base.py | 6 +-- .../clients/{rest_clients => rest}/channel.py | 2 +- .../clients/{rest_clients => rest}/gateway.py | 2 +- .../clients/{rest_clients => rest}/guild.py | 2 +- .../clients/{rest_clients => rest}/invite.py | 2 +- hikari/clients/{rest_clients => rest}/me.py | 2 +- .../clients/{rest_clients => rest}/oauth2.py | 2 +- .../clients/{rest_clients => rest}/react.py | 2 +- hikari/clients/{rest_clients => rest}/user.py | 2 +- .../clients/{rest_clients => rest}/voice.py | 2 +- .../clients/{rest_clients => rest}/webhook.py | 2 +- .../clients/{shard_clients.py => shards.py} | 14 ++--- hikari/clients/{test_client.py => test.py} | 0 hikari/events.py | 8 +-- hikari/net/__init__.py | 6 +-- hikari/net/{rest_sessions.py => rest.py} | 8 +-- hikari/net/shards.py | 10 ++-- .../{raw_event_consumers.py => consumers.py} | 4 +- .../{event_dispatchers.py => dispatchers.py} | 0 hikari/state/event_managers.py | 14 ++--- ...ateless_event_managers.py => stateless.py} | 4 +- .../__init__.py | 0 .../test___init__.py | 38 +++++++------ .../test_base.py | 8 +-- .../test_channel.py | 6 +-- .../test_gateway.py | 6 +-- .../test_guild.py | 6 +-- .../test_invite.py | 6 +-- .../test_me.py | 6 +-- .../test_oauth2.py | 6 +-- .../test_react.py | 6 +-- .../test_user.py | 6 +-- .../test_voice.py | 6 +-- .../test_webhook.py | 6 +-- .../{test_shard_clients.py => test_shards.py} | 26 ++++----- .../{test_rest_sessions.py => test_rest.py} | 26 +++++---- tests/hikari/net/test_shards.py | 52 +++++++++--------- ...ispatcher.py => test_event_dispatchers.py} | 4 +- ...ss_event_managers.py => test_stateless.py} | 8 +-- 42 files changed, 200 insertions(+), 209 deletions(-) rename hikari/clients/{bot_clients.py => bot.py} (88%) rename hikari/clients/{rest_clients => rest}/__init__.py (82%) rename hikari/clients/{rest_clients => rest}/base.py (90%) rename hikari/clients/{rest_clients => rest}/channel.py (99%) rename hikari/clients/{rest_clients => rest}/gateway.py (97%) rename hikari/clients/{rest_clients => rest}/guild.py (99%) rename hikari/clients/{rest_clients => rest}/invite.py (98%) rename hikari/clients/{rest_clients => rest}/me.py (99%) rename hikari/clients/{rest_clients => rest}/oauth2.py (96%) rename hikari/clients/{rest_clients => rest}/react.py (99%) rename hikari/clients/{rest_clients => rest}/user.py (97%) rename hikari/clients/{rest_clients => rest}/voice.py (97%) rename hikari/clients/{rest_clients => rest}/webhook.py (99%) rename hikari/clients/{shard_clients.py => shards.py} (98%) rename hikari/clients/{test_client.py => test.py} (100%) rename hikari/net/{rest_sessions.py => rest.py} (99%) rename hikari/state/{raw_event_consumers.py => consumers.py} (93%) rename hikari/state/{event_dispatchers.py => dispatchers.py} (100%) rename hikari/state/{stateless_event_managers.py => stateless.py} (98%) rename tests/hikari/clients/{test_rest_clients => test_rest}/__init__.py (100%) rename tests/hikari/clients/{test_rest_clients => test_rest}/test___init__.py (65%) rename tests/hikari/clients/{test_rest_clients => test_rest}/test_base.py (86%) rename tests/hikari/clients/{test_rest_clients => test_rest}/test_channel.py (99%) rename tests/hikari/clients/{test_rest_clients => test_rest}/test_gateway.py (92%) rename tests/hikari/clients/{test_rest_clients => test_rest}/test_guild.py (99%) rename tests/hikari/clients/{test_rest_clients => test_rest}/test_invite.py (95%) rename tests/hikari/clients/{test_rest_clients => test_rest}/test_me.py (98%) rename tests/hikari/clients/{test_rest_clients => test_rest}/test_oauth2.py (91%) rename tests/hikari/clients/{test_rest_clients => test_rest}/test_react.py (97%) rename tests/hikari/clients/{test_rest_clients => test_rest}/test_user.py (91%) rename tests/hikari/clients/{test_rest_clients => test_rest}/test_voice.py (90%) rename tests/hikari/clients/{test_rest_clients => test_rest}/test_webhook.py (98%) rename tests/hikari/clients/{test_shard_clients.py => test_shards.py} (95%) rename tests/hikari/net/{test_rest_sessions.py => test_rest.py} (99%) rename tests/hikari/state/{test_event_dispatcher.py => test_event_dispatchers.py} (99%) rename tests/hikari/state/{test_stateless_event_managers.py => test_stateless.py} (98%) diff --git a/hikari/clients/__init__.py b/hikari/clients/__init__.py index 82b6b9dc71..1f6d9e2ec5 100644 --- a/hikari/clients/__init__.py +++ b/hikari/clients/__init__.py @@ -18,20 +18,20 @@ # along with Hikari. If not, see . """The models API for interacting with Discord directly.""" -from hikari.clients import bot_clients +from hikari.clients import bot from hikari.clients import configs -from hikari.clients import rest_clients +from hikari.clients import rest from hikari.clients import runnable -from hikari.clients.bot_clients import * +from hikari.clients.bot import * from hikari.clients.configs import * -from hikari.clients.rest_clients import * +from hikari.clients.rest import * from hikari.clients.runnable import * -from hikari.clients.shard_clients import * +from hikari.clients.shards import * __all__ = [ - *bot_clients.__all__, + *bot.__all__, *configs.__all__, - *rest_clients.__all__, - *shard_clients.__all__, + *rest.__all__, + *shards.__all__, *runnable.__all__, ] diff --git a/hikari/clients/bot_clients.py b/hikari/clients/bot.py similarity index 88% rename from hikari/clients/bot_clients.py rename to hikari/clients/bot.py index bc335196e2..c872c8ec40 100644 --- a/hikari/clients/bot_clients.py +++ b/hikari/clients/bot.py @@ -32,26 +32,26 @@ from hikari import guilds from hikari import intents from hikari.clients import configs -from hikari.clients import rest_clients +from hikari.clients import rest from hikari.clients import runnable -from hikari.clients import shard_clients +from hikari.clients import shards from hikari.internal import conversions from hikari.internal import more_collections from hikari.internal import more_typing -from hikari.state import event_dispatchers +from hikari.state import dispatchers from hikari.state import event_managers -from hikari.state import stateless_event_managers +from hikari.state import stateless -ShardClientT = typing.TypeVar("ShardClientT", bound=shard_clients.ShardClient) +ShardClientT = typing.TypeVar("ShardClientT", bound=shards.ShardClient) EventManagerT = typing.TypeVar("EventManagerT", bound=event_managers.EventManager) -RESTClientT = typing.TypeVar("RESTClientT", bound=rest_clients.RESTClient) +RESTClientT = typing.TypeVar("RESTClientT", bound=rest.RESTClient) BotConfigT = typing.TypeVar("BotConfigT", bound=configs.BotConfig) class BotBase( typing.Generic[ShardClientT, RESTClientT, EventManagerT, BotConfigT], runnable.RunnableClient, - event_dispatchers.EventDispatcher, + dispatchers.EventDispatcher, abc.ABC, ): """An abstract base class for a bot implementation. @@ -60,7 +60,7 @@ class BotBase( - ``ShardClientT`` - the implementation of :obj:`~hikari.clients.shard_clients.ShardClient` to use for shards. - ``RESTClientT`` - the implementation of - :obj:`~hikari.clients.rest_clients.RESTClient` to use for API calls. + :obj:`~hikari.clients.rest.RESTClient` to use for API calls. - ``EventManagerT`` - the implementation of :obj:`~hikari.state.event_managers.EventManager` to use for event management, translation, and dispatching. @@ -91,7 +91,7 @@ class BotBase( #: The REST HTTP client to use for this bot. #: - #: :type: :obj:`~hikari.clients.rest_clients.RESTClient` + #: :type: :obj:`~hikari.clients.rest.RESTClient` rest: RESTClientT #: Shards registered to this bot. @@ -245,21 +245,21 @@ async def join(self) -> None: await asyncio.gather(*(shard_obj.join() for shard_obj in self.shards.values())) def add_listener( - self, event_type: typing.Type[event_dispatchers.EventT], callback: event_dispatchers.EventCallbackT - ) -> event_dispatchers.EventCallbackT: + self, event_type: typing.Type[dispatchers.EventT], callback: dispatchers.EventCallbackT + ) -> dispatchers.EventCallbackT: return self.event_manager.event_dispatcher.add_listener(event_type, callback) def remove_listener( - self, event_type: typing.Type[event_dispatchers.EventT], callback: event_dispatchers.EventCallbackT - ) -> event_dispatchers.EventCallbackT: + self, event_type: typing.Type[dispatchers.EventT], callback: dispatchers.EventCallbackT + ) -> dispatchers.EventCallbackT: return self.event_manager.event_dispatcher.remove_listener(event_type, callback) def wait_for( self, - event_type: typing.Type[event_dispatchers.EventT], + event_type: typing.Type[dispatchers.EventT], *, timeout: typing.Optional[float], - predicate: event_dispatchers.PredicateT, + predicate: dispatchers.PredicateT, ) -> more_typing.Future: return self.event_manager.event_dispatcher.wait_for(event_type, timeout=timeout, predicate=predicate) @@ -307,7 +307,7 @@ async def update_presence( *( s.update_presence(status=status, activity=activity, idle_since=idle_since, is_afk=is_afk) for s in self.shards.values() - if s.connection_state in (shard_clients.ShardState.WAITING_FOR_READY, shard_clients.ShardState.READY) + if s.connection_state in (shards.ShardState.WAITING_FOR_READY, shards.ShardState.READY) ) ) @@ -358,7 +358,7 @@ def _create_rest(cls, config: BotConfigT) -> RESTClientT: Returns ------- - :obj:`~hikari.clients.rest_clients.RESTClient` + :obj:`~hikari.clients.rest.RESTClient` The REST client to use. """ @@ -376,12 +376,7 @@ def _create_event_manager(cls): class StatelessBot( - BotBase[ - shard_clients.ShardClientImpl, - rest_clients.RESTClient, - stateless_event_managers.StatelessEventManagerImpl, - configs.BotConfig, - ] + BotBase[shards.ShardClientImpl, rest.RESTClient, stateless.StatelessEventManagerImpl, configs.BotConfig,] ): """Bot client without any state internals.""" @@ -395,9 +390,9 @@ def _create_shard( shard_count: int, url: str, config: configs.BotConfig, - event_manager: stateless_event_managers.StatelessEventManagerImpl, - ) -> shard_clients.ShardClientImpl: - return shard_clients.ShardClientImpl( + event_manager: stateless.StatelessEventManagerImpl, + ) -> shards.ShardClientImpl: + return shards.ShardClientImpl( shard_id=shard_id, shard_count=shard_count, config=config, @@ -407,9 +402,9 @@ def _create_shard( ) @classmethod - def _create_rest(cls, config: BotConfigT) -> rest_clients.RESTClient: - return rest_clients.RESTClient(config) + def _create_rest(cls, config: BotConfigT) -> rest.RESTClient: + return rest.RESTClient(config) @classmethod def _create_event_manager(cls) -> EventManagerT: - return stateless_event_managers.StatelessEventManagerImpl() + return stateless.StatelessEventManagerImpl() diff --git a/hikari/clients/rest_clients/__init__.py b/hikari/clients/rest/__init__.py similarity index 82% rename from hikari/clients/rest_clients/__init__.py rename to hikari/clients/rest/__init__.py index 7707f88731..e13865dd04 100644 --- a/hikari/clients/rest_clients/__init__.py +++ b/hikari/clients/rest/__init__.py @@ -25,17 +25,17 @@ __all__ = ["RESTClient"] from hikari.clients import configs -from hikari.clients.rest_clients import channel -from hikari.clients.rest_clients import me -from hikari.clients.rest_clients import gateway -from hikari.clients.rest_clients import guild -from hikari.clients.rest_clients import invite -from hikari.clients.rest_clients import oauth2 -from hikari.clients.rest_clients import react -from hikari.clients.rest_clients import user -from hikari.clients.rest_clients import voice -from hikari.clients.rest_clients import webhook -from hikari.net import rest_sessions +from hikari.clients.rest import channel +from hikari.clients.rest import me +from hikari.clients.rest import gateway +from hikari.clients.rest import guild +from hikari.clients.rest import invite +from hikari.clients.rest import oauth2 +from hikari.clients.rest import react +from hikari.clients.rest import user +from hikari.clients.rest import voice +from hikari.clients.rest import webhook +from hikari.net import rest as low_level_rest class RESTClient( @@ -71,7 +71,7 @@ class RESTClient( def __init__(self, config: configs.RESTConfig) -> None: super().__init__( - rest_sessions.LowLevelRestfulClient( + low_level_rest.REST( allow_redirects=config.allow_redirects, connector=config.tcp_connector, proxy_headers=config.proxy_headers, diff --git a/hikari/clients/rest_clients/base.py b/hikari/clients/rest/base.py similarity index 90% rename from hikari/clients/rest_clients/base.py rename to hikari/clients/rest/base.py index 189a3f7293..26cbafb668 100644 --- a/hikari/clients/rest_clients/base.py +++ b/hikari/clients/rest/base.py @@ -25,7 +25,7 @@ import typing from hikari.internal import meta -from hikari.net import rest_sessions +from hikari.net import rest class BaseRESTComponent(abc.ABC, metaclass=meta.UniqueFunctionMeta): @@ -37,8 +37,8 @@ class BaseRESTComponent(abc.ABC, metaclass=meta.UniqueFunctionMeta): """ @abc.abstractmethod - def __init__(self, session: rest_sessions.LowLevelRestfulClient) -> None: - self._session: rest_sessions.LowLevelRestfulClient = session + def __init__(self, session: rest.REST) -> None: + self._session: rest.REST = session async def __aenter__(self) -> "BaseRESTComponent": return self diff --git a/hikari/clients/rest_clients/channel.py b/hikari/clients/rest/channel.py similarity index 99% rename from hikari/clients/rest_clients/channel.py rename to hikari/clients/rest/channel.py index 0897b7116c..e6396ae0c4 100644 --- a/hikari/clients/rest_clients/channel.py +++ b/hikari/clients/rest/channel.py @@ -35,7 +35,7 @@ from hikari import permissions as _permissions from hikari import users from hikari import webhooks -from hikari.clients.rest_clients import base +from hikari.clients.rest import base from hikari.internal import allowed_mentions from hikari.internal import assertions from hikari.internal import conversions diff --git a/hikari/clients/rest_clients/gateway.py b/hikari/clients/rest/gateway.py similarity index 97% rename from hikari/clients/rest_clients/gateway.py rename to hikari/clients/rest/gateway.py index 2e318366f5..d2ce803adf 100644 --- a/hikari/clients/rest_clients/gateway.py +++ b/hikari/clients/rest/gateway.py @@ -23,7 +23,7 @@ import abc from hikari import gateway_entities -from hikari.clients.rest_clients import base +from hikari.clients.rest import base class RESTGatewayComponent(base.BaseRESTComponent, abc.ABC): # pylint: disable=W0223 diff --git a/hikari/clients/rest_clients/guild.py b/hikari/clients/rest/guild.py similarity index 99% rename from hikari/clients/rest_clients/guild.py rename to hikari/clients/rest/guild.py index e1963472dd..ab79d4a91b 100644 --- a/hikari/clients/rest_clients/guild.py +++ b/hikari/clients/rest/guild.py @@ -35,7 +35,7 @@ from hikari import users from hikari import voices from hikari import webhooks -from hikari.clients.rest_clients import base +from hikari.clients.rest import base from hikari.internal import conversions from hikari.internal import pagination diff --git a/hikari/clients/rest_clients/invite.py b/hikari/clients/rest/invite.py similarity index 98% rename from hikari/clients/rest_clients/invite.py rename to hikari/clients/rest/invite.py index e783f33a2b..07d5757e2b 100644 --- a/hikari/clients/rest_clients/invite.py +++ b/hikari/clients/rest/invite.py @@ -24,7 +24,7 @@ import typing from hikari import invites -from hikari.clients.rest_clients import base +from hikari.clients.rest import base class RESTInviteComponent(base.BaseRESTComponent, abc.ABC): # pylint: disable=W0223 diff --git a/hikari/clients/rest_clients/me.py b/hikari/clients/rest/me.py similarity index 99% rename from hikari/clients/rest_clients/me.py rename to hikari/clients/rest/me.py index 6f7ad015e2..79835664be 100644 --- a/hikari/clients/rest_clients/me.py +++ b/hikari/clients/rest/me.py @@ -29,7 +29,7 @@ from hikari import guilds from hikari import applications from hikari import users -from hikari.clients.rest_clients import base +from hikari.clients.rest import base from hikari.internal import conversions from hikari.internal import pagination diff --git a/hikari/clients/rest_clients/oauth2.py b/hikari/clients/rest/oauth2.py similarity index 96% rename from hikari/clients/rest_clients/oauth2.py rename to hikari/clients/rest/oauth2.py index 885f6b25f5..91f8ab5d91 100644 --- a/hikari/clients/rest_clients/oauth2.py +++ b/hikari/clients/rest/oauth2.py @@ -23,7 +23,7 @@ import abc from hikari import applications -from hikari.clients.rest_clients import base +from hikari.clients.rest import base class RESTOAuth2Component(base.BaseRESTComponent, abc.ABC): # pylint: disable=W0223 diff --git a/hikari/clients/rest_clients/react.py b/hikari/clients/rest/react.py similarity index 99% rename from hikari/clients/rest_clients/react.py rename to hikari/clients/rest/react.py index 70b5db8c9b..f07082df88 100644 --- a/hikari/clients/rest_clients/react.py +++ b/hikari/clients/rest/react.py @@ -29,7 +29,7 @@ from hikari import emojis from hikari import messages as _messages from hikari import users -from hikari.clients.rest_clients import base +from hikari.clients.rest import base from hikari.internal import pagination diff --git a/hikari/clients/rest_clients/user.py b/hikari/clients/rest/user.py similarity index 97% rename from hikari/clients/rest_clients/user.py rename to hikari/clients/rest/user.py index 7c4ed148eb..a956a7cdbf 100644 --- a/hikari/clients/rest_clients/user.py +++ b/hikari/clients/rest/user.py @@ -24,7 +24,7 @@ from hikari import bases from hikari import users -from hikari.clients.rest_clients import base +from hikari.clients.rest import base class RESTUserComponent(base.BaseRESTComponent, abc.ABC): # pylint: disable=W0223 diff --git a/hikari/clients/rest_clients/voice.py b/hikari/clients/rest/voice.py similarity index 97% rename from hikari/clients/rest_clients/voice.py rename to hikari/clients/rest/voice.py index 5cdc1e1e79..d519f8a8a9 100644 --- a/hikari/clients/rest_clients/voice.py +++ b/hikari/clients/rest/voice.py @@ -24,7 +24,7 @@ import typing from hikari import voices -from hikari.clients.rest_clients import base +from hikari.clients.rest import base class RESTVoiceComponent(base.BaseRESTComponent, abc.ABC): # pylint: disable=W0223 diff --git a/hikari/clients/rest_clients/webhook.py b/hikari/clients/rest/webhook.py similarity index 99% rename from hikari/clients/rest_clients/webhook.py rename to hikari/clients/rest/webhook.py index 2fbb1ff397..da4af44a21 100644 --- a/hikari/clients/rest_clients/webhook.py +++ b/hikari/clients/rest/webhook.py @@ -31,7 +31,7 @@ from hikari import messages as _messages from hikari import users from hikari import webhooks -from hikari.clients.rest_clients import base +from hikari.clients.rest import base from hikari.internal import allowed_mentions from hikari.internal import conversions diff --git a/hikari/clients/shard_clients.py b/hikari/clients/shards.py similarity index 98% rename from hikari/clients/shard_clients.py rename to hikari/clients/shards.py index 9b559f3214..7d47ad6723 100644 --- a/hikari/clients/shard_clients.py +++ b/hikari/clients/shards.py @@ -16,7 +16,7 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . -"""Provides a facade around :obj:`~hikari.net.shards.ShardConnection`. +"""Provides a facade around :obj:`~hikari.net.shards.Shard`. This handles parsing and initializing the object from a configuration, as well as restarting it if it disconnects. @@ -48,8 +48,8 @@ from hikari.net import codes from hikari.net import ratelimits from hikari.net import shards -from hikari.state import event_dispatchers -from hikari.state import raw_event_consumers +from hikari.state import dispatchers +from hikari.state import consumers @enum.unique @@ -360,9 +360,9 @@ def __init__( shard_id: int, shard_count: int, config: configs.GatewayConfig, - raw_event_consumer_impl: raw_event_consumers.RawEventConsumer, + raw_event_consumer_impl: consumers.RawEventConsumer, url: str, - dispatcher: typing.Optional[event_dispatchers.EventDispatcher] = None, + dispatcher: typing.Optional[dispatchers.EventDispatcher] = None, ) -> None: super().__init__(logging.getLogger(f"hikari.{type(self).__qualname__}.{shard_id}")) self._raw_event_consumer = raw_event_consumer_impl @@ -373,7 +373,7 @@ def __init__( self._shard_state = ShardState.NOT_RUNNING self._task = None self._dispatcher = dispatcher - self._connection = shards.ShardConnection( + self._connection = shards.Shard( compression=config.gateway_use_compression, connector=config.tcp_connector, debug=config.debug, @@ -594,7 +594,7 @@ async def _spin_up(self) -> asyncio.Task: is_resume = self._connection.seq is not None and self._connection.session_id is not None - connect_task = asyncio.create_task(self._connection.connect(), name="ShardConnection#connect") + connect_task = asyncio.create_task(self._connection.connect(), name="Shard#connect") completed, _ = await asyncio.wait( [connect_task, self._connection.hello_event.wait()], return_when=asyncio.FIRST_COMPLETED diff --git a/hikari/clients/test_client.py b/hikari/clients/test.py similarity index 100% rename from hikari/clients/test_client.py rename to hikari/clients/test.py diff --git a/hikari/events.py b/hikari/events.py index 9a82323675..ea7c3e7af4 100644 --- a/hikari/events.py +++ b/hikari/events.py @@ -81,7 +81,7 @@ from hikari import unset from hikari import users from hikari import voices -from hikari.clients import shard_clients +from hikari.clients import shards from hikari.internal import conversions from hikari.internal import marshaller @@ -147,7 +147,7 @@ class ConnectedEvent(HikariEvent, marshaller.Deserializable): #: The shard that connected. #: #: :type: :obj:`~hikari.clients.shard_clients.ShardClient` - shard: shard_clients.ShardClient + shard: shards.ShardClient @attr.s(slots=True, kw_only=True, auto_attribs=True) @@ -157,7 +157,7 @@ class DisconnectedEvent(HikariEvent, marshaller.Deserializable): #: The shard that disconnected. #: #: :type: :obj:`~hikari.clients.shard_clients.ShardClient` - shard: shard_clients.ShardClient + shard: shards.ShardClient @attr.s(slots=True, kw_only=True, auto_attribs=True) @@ -167,7 +167,7 @@ class ResumedEvent(HikariEvent): #: The shard that reconnected. #: #: :type: :obj:`~hikari.clients.shard_clients.ShardClient` - shard: shard_clients.ShardClient + shard: shards.ShardClient @marshaller.marshallable() diff --git a/hikari/net/__init__.py b/hikari/net/__init__.py index 7e662c2fac..d238142713 100644 --- a/hikari/net/__init__.py +++ b/hikari/net/__init__.py @@ -23,13 +23,13 @@ """ from hikari.net import codes from hikari.net import ratelimits -from hikari.net import rest_sessions +from hikari.net import rest from hikari.net import routes from hikari.net import shards from hikari.net import user_agents from hikari.net.codes import * -from hikari.net.rest_sessions import * +from hikari.net.rest import * from hikari.net.shards import * -__all__ = codes.__all__ + shards.__all__ + rest_sessions.__all__ +__all__ = codes.__all__ + shards.__all__ + rest.__all__ diff --git a/hikari/net/rest_sessions.py b/hikari/net/rest.py similarity index 99% rename from hikari/net/rest_sessions.py rename to hikari/net/rest.py index 325e9aba77..934df23a70 100644 --- a/hikari/net/rest_sessions.py +++ b/hikari/net/rest.py @@ -17,7 +17,7 @@ # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . """Implementation of a basic HTTP client that uses aiohttp to interact with the Discord API.""" -__all__ = ["LowLevelRestfulClient"] +__all__ = ["REST"] import asyncio import contextlib @@ -46,8 +46,8 @@ VERSION_7: typing.Final[int] = 7 -class LowLevelRestfulClient: - """A RESTful client to allow you to interact with the Discord API. +class REST: + """A low-level RESTful client to allow you to interact with the Discord API. Parameters ---------- @@ -277,7 +277,7 @@ async def close(self) -> None: await self.client_session.close() self.client_session = None - async def __aenter__(self) -> "LowLevelRestfulClient": + async def __aenter__(self) -> "REST": return self async def __aexit__( diff --git a/hikari/net/shards.py b/hikari/net/shards.py index 198f5aa249..7554747749 100644 --- a/hikari/net/shards.py +++ b/hikari/net/shards.py @@ -30,7 +30,7 @@ * Gateway documentation: https://discordapp.com/developers/docs/topics/gateway * Opcode documentation: https://discordapp.com/developers/docs/topics/opcodes-and-status-codes """ -__all__ = ["ShardConnection"] +__all__ = ["Shard"] import asyncio import contextlib @@ -54,21 +54,21 @@ from hikari.net import user_agents #: The signature for an event dispatch callback. -DispatchT = typing.Callable[["ShardConnection", str, typing.Dict], None] +DispatchT = typing.Callable[["Shard", str, typing.Dict], None] VERSION_6: typing.Final[int] = 6 VERSION_7: typing.Final[int] = 7 -class ShardConnection: +class Shard: """Implementation of a client for the Discord Gateway. This is a websocket connection to Discord that is used to inform your application of events that occur, and to allow you to change your presence, amongst other real-time applications. - Each :obj:`~ShardConnection` represents a single shard. + Each :obj:`~Shard` represents a single shard. Expected events that may be passed to the event dispatcher are documented in the `gateway event reference `_. @@ -97,7 +97,7 @@ class ShardConnection: dispatch: ``dispatch function`` The function to invoke with any dispatched events. This must not be a coroutine function, and must take three arguments only. The first is - the reference to this :obj:`~ShardConnection` The second is the + the reference to this :obj:`~Shard` The second is the event name. initial_presence: :obj:`~typing.Dict`, optional A raw JSON object as a :obj:`~typing.Dict` that should be set as the diff --git a/hikari/state/raw_event_consumers.py b/hikari/state/consumers.py similarity index 93% rename from hikari/state/raw_event_consumers.py rename to hikari/state/consumers.py index 46995bf0cc..184f458643 100644 --- a/hikari/state/raw_event_consumers.py +++ b/hikari/state/consumers.py @@ -27,7 +27,7 @@ import abc import typing -from hikari.clients import shard_clients +from hikari.clients import shards class RawEventConsumer(abc.ABC): @@ -43,7 +43,7 @@ class RawEventConsumer(abc.ABC): @abc.abstractmethod def process_raw_event( - self, shard_client_obj: shard_clients.ShardClient, name: str, payload: typing.Mapping[str, str], + self, shard_client_obj: shards.ShardClient, name: str, payload: typing.Mapping[str, str], ) -> None: """Consume a raw event that was received from a shard connection. diff --git a/hikari/state/event_dispatchers.py b/hikari/state/dispatchers.py similarity index 100% rename from hikari/state/event_dispatchers.py rename to hikari/state/dispatchers.py diff --git a/hikari/state/event_managers.py b/hikari/state/event_managers.py index 440d53b548..aeeefd1963 100644 --- a/hikari/state/event_managers.py +++ b/hikari/state/event_managers.py @@ -23,10 +23,10 @@ import logging import typing -from hikari.clients import shard_clients +from hikari.clients import shards from hikari.internal import assertions -from hikari.state import event_dispatchers -from hikari.state import raw_event_consumers +from hikari.state import dispatchers +from hikari.state import consumers EVENT_MARKER_ATTR: typing.Final[str] = "___event_name___" @@ -67,10 +67,10 @@ def _get_event_marker(obj: typing.Any) -> typing.Set[str]: return getattr(obj, EVENT_MARKER_ATTR) -EventDispatcherT = typing.TypeVar("EventDispatcherT", bound=event_dispatchers.EventDispatcher) +EventDispatcherT = typing.TypeVar("EventDispatcherT", bound=dispatchers.EventDispatcher) -class EventManager(typing.Generic[EventDispatcherT], raw_event_consumers.RawEventConsumer): +class EventManager(typing.Generic[EventDispatcherT], consumers.RawEventConsumer): """Abstract definition of the components for an event system for a bot. The class itself inherits from @@ -136,7 +136,7 @@ def _process_message_create(self, shard, payload) -> MessageCreateEvent: def __init__(self, event_dispatcher_impl: typing.Optional[EventDispatcherT] = None) -> None: if event_dispatcher_impl is None: - event_dispatcher_impl = event_dispatchers.EventDispatcherImpl() + event_dispatcher_impl = dispatchers.EventDispatcherImpl() self.logger = logging.getLogger(type(self).__qualname__) self.event_dispatcher = event_dispatcher_impl @@ -149,7 +149,7 @@ def __init__(self, event_dispatcher_impl: typing.Optional[EventDispatcherT] = No self.raw_event_mappers[event_name] = member def process_raw_event( - self, shard_client_obj: shard_clients.ShardClient, name: str, payload: typing.Mapping[str, typing.Any], + self, shard_client_obj: shards.ShardClient, name: str, payload: typing.Mapping[str, typing.Any], ) -> None: """Process a low level event. diff --git a/hikari/state/stateless_event_managers.py b/hikari/state/stateless.py similarity index 98% rename from hikari/state/stateless_event_managers.py rename to hikari/state/stateless.py index 789f841d3f..0731114793 100644 --- a/hikari/state/stateless_event_managers.py +++ b/hikari/state/stateless.py @@ -21,11 +21,11 @@ __all__ = ["StatelessEventManagerImpl"] from hikari import events -from hikari.state import event_dispatchers +from hikari.state import dispatchers from hikari.state import event_managers -class StatelessEventManagerImpl(event_managers.EventManager[event_dispatchers.EventDispatcher]): +class StatelessEventManagerImpl(event_managers.EventManager[dispatchers.EventDispatcher]): """Stateless event manager implementation for stateless bots. This is an implementation that does not rely on querying prior information to diff --git a/tests/hikari/clients/test_rest_clients/__init__.py b/tests/hikari/clients/test_rest/__init__.py similarity index 100% rename from tests/hikari/clients/test_rest_clients/__init__.py rename to tests/hikari/clients/test_rest/__init__.py diff --git a/tests/hikari/clients/test_rest_clients/test___init__.py b/tests/hikari/clients/test_rest/test___init__.py similarity index 65% rename from tests/hikari/clients/test_rest_clients/test___init__.py rename to tests/hikari/clients/test_rest/test___init__.py index f2d72e3a63..1db1bf680a 100644 --- a/tests/hikari/clients/test_rest_clients/test___init__.py +++ b/tests/hikari/clients/test_rest/test___init__.py @@ -22,8 +22,8 @@ import pytest from hikari.clients import configs -from hikari.clients import rest_clients -from hikari.net import rest_sessions +from hikari.clients import rest as high_level_rest +from hikari.net import rest as low_level_rest class TestRESTClient: @@ -33,11 +33,9 @@ def mock_config(self): return configs.RESTConfig(token="blah.blah.blah") def test_init(self, mock_config): - mock_low_level_rest_clients = mock.MagicMock(rest_sessions.LowLevelRestfulClient) - with mock.patch.object( - rest_sessions, "LowLevelRestfulClient", return_value=mock_low_level_rest_clients - ) as patched_init: - cli = rest_clients.RESTClient(mock_config) + mock_low_level_rest_clients = mock.MagicMock(low_level_rest.REST) + with mock.patch.object(low_level_rest, "REST", return_value=mock_low_level_rest_clients) as patched_init: + cli = high_level_rest.RESTClient(mock_config) patched_init.assert_called_once_with( allow_redirects=mock_config.allow_redirects, connector=mock_config.tcp_connector, @@ -55,26 +53,26 @@ def test_inheritance(self): for attr, routine in ( member for component in [ - rest_clients.channel.RESTChannelComponent, - rest_clients.me.RESTCurrentUserComponent, - rest_clients.gateway.RESTGatewayComponent, - rest_clients.guild.RESTGuildComponent, - rest_clients.invite.RESTInviteComponent, - rest_clients.oauth2.RESTOAuth2Component, - rest_clients.react.RESTReactionComponent, - rest_clients.user.RESTUserComponent, - rest_clients.voice.RESTVoiceComponent, - rest_clients.webhook.RESTWebhookComponent, + high_level_rest.channel.RESTChannelComponent, + high_level_rest.me.RESTCurrentUserComponent, + high_level_rest.gateway.RESTGatewayComponent, + high_level_rest.guild.RESTGuildComponent, + high_level_rest.invite.RESTInviteComponent, + high_level_rest.oauth2.RESTOAuth2Component, + high_level_rest.react.RESTReactionComponent, + high_level_rest.user.RESTUserComponent, + high_level_rest.voice.RESTVoiceComponent, + high_level_rest.webhook.RESTWebhookComponent, ] for member in inspect.getmembers(component, inspect.isroutine) ): if not attr.startswith("__"): - assert hasattr(rest_clients.RESTClient, attr), ( + assert hasattr(high_level_rest.RESTClient, attr), ( f"Missing {routine.__qualname__} on RestClient; the component might not be being " "inherited properly or at all." ) - assert getattr(rest_clients.RESTClient, attr) == routine, ( + assert getattr(high_level_rest.RESTClient, attr) == routine, ( f"Mismatching method found on RestClient; expected {routine.__qualname__} but got " - f"{getattr(rest_clients.RESTClient, attr).__qualname__}. `{attr}` is most likely being declared on" + f"{getattr(high_level_rest.RESTClient, attr).__qualname__}. `{attr}` is most likely being declared on" "multiple components." ) diff --git a/tests/hikari/clients/test_rest_clients/test_base.py b/tests/hikari/clients/test_rest/test_base.py similarity index 86% rename from tests/hikari/clients/test_rest_clients/test_base.py rename to tests/hikari/clients/test_rest/test_base.py index b54aee2e7b..aadfc5ba94 100644 --- a/tests/hikari/clients/test_rest_clients/test_base.py +++ b/tests/hikari/clients/test_rest/test_base.py @@ -19,14 +19,14 @@ import mock import pytest -from hikari.clients.rest_clients import base -from hikari.net import rest_sessions +from hikari.clients.rest import base +from hikari.net import rest class TestBaseRESTComponent: @pytest.fixture() - def low_level_rest_impl(self) -> rest_sessions.LowLevelRestfulClient: - return mock.MagicMock(rest_sessions.LowLevelRestfulClient) + def low_level_rest_impl(self) -> rest.REST: + return mock.MagicMock(rest.REST) @pytest.fixture() def rest_clients_impl(self, low_level_rest_impl) -> base.BaseRESTComponent: diff --git a/tests/hikari/clients/test_rest_clients/test_channel.py b/tests/hikari/clients/test_rest/test_channel.py similarity index 99% rename from tests/hikari/clients/test_rest_clients/test_channel.py rename to tests/hikari/clients/test_rest/test_channel.py index 00f9d22659..7b4086e7e5 100644 --- a/tests/hikari/clients/test_rest_clients/test_channel.py +++ b/tests/hikari/clients/test_rest/test_channel.py @@ -32,18 +32,18 @@ from hikari import snowflakes from hikari import users from hikari import webhooks -from hikari.clients.rest_clients import channel +from hikari.clients.rest import channel from hikari.internal import allowed_mentions from hikari.internal import conversions from hikari.internal import pagination -from hikari.net import rest_sessions +from hikari.net import rest from tests.hikari import _helpers class TestRESTChannelLogging: @pytest.fixture() def rest_channel_logic_impl(self): - mock_low_level_restful_client = mock.MagicMock(rest_sessions.LowLevelRestfulClient) + mock_low_level_restful_client = mock.MagicMock(rest.REST) class RESTChannelLogicImpl(channel.RESTChannelComponent): def __init__(self): diff --git a/tests/hikari/clients/test_rest_clients/test_gateway.py b/tests/hikari/clients/test_rest/test_gateway.py similarity index 92% rename from tests/hikari/clients/test_rest_clients/test_gateway.py rename to tests/hikari/clients/test_rest/test_gateway.py index 1fef2150f1..b2a67080c4 100644 --- a/tests/hikari/clients/test_rest_clients/test_gateway.py +++ b/tests/hikari/clients/test_rest/test_gateway.py @@ -21,14 +21,14 @@ import pytest from hikari import gateway_entities -from hikari.clients.rest_clients import gateway -from hikari.net import rest_sessions +from hikari.clients.rest import gateway +from hikari.net import rest class TestRESTReactionLogic: @pytest.fixture() def rest_gateway_logic_impl(self): - mock_low_level_restful_client = mock.MagicMock(rest_sessions.LowLevelRestfulClient) + mock_low_level_restful_client = mock.MagicMock(rest.REST) class RESTGatewayLogicImpl(gateway.RESTGatewayComponent): def __init__(self): diff --git a/tests/hikari/clients/test_rest_clients/test_guild.py b/tests/hikari/clients/test_rest/test_guild.py similarity index 99% rename from tests/hikari/clients/test_rest_clients/test_guild.py rename to tests/hikari/clients/test_rest/test_guild.py index 94dc8d6c5c..84cb906381 100644 --- a/tests/hikari/clients/test_rest_clients/test_guild.py +++ b/tests/hikari/clients/test_rest/test_guild.py @@ -33,10 +33,10 @@ from hikari import users from hikari import voices from hikari import webhooks -from hikari.clients.rest_clients import guild as _guild +from hikari.clients.rest import guild as _guild from hikari.internal import conversions from hikari.internal import pagination -from hikari.net import rest_sessions +from hikari.net import rest from tests.hikari import _helpers @@ -50,7 +50,7 @@ def test__get_member_id(): class TestRESTGuildLogic: @pytest.fixture() def rest_guild_logic_impl(self): - mock_low_level_restful_client = mock.MagicMock(rest_sessions.LowLevelRestfulClient) + mock_low_level_restful_client = mock.MagicMock(rest.REST) class RESTGuildLogicImpl(_guild.RESTGuildComponent): def __init__(self): diff --git a/tests/hikari/clients/test_rest_clients/test_invite.py b/tests/hikari/clients/test_rest/test_invite.py similarity index 95% rename from tests/hikari/clients/test_rest_clients/test_invite.py rename to tests/hikari/clients/test_rest/test_invite.py index 3d1a1e5040..9e0ae1145b 100644 --- a/tests/hikari/clients/test_rest_clients/test_invite.py +++ b/tests/hikari/clients/test_rest/test_invite.py @@ -20,14 +20,14 @@ import pytest from hikari import invites -from hikari.clients.rest_clients import invite -from hikari.net import rest_sessions +from hikari.clients.rest import invite +from hikari.net import rest class TestRESTInviteLogic: @pytest.fixture() def rest_invite_logic_impl(self): - mock_low_level_restful_client = mock.MagicMock(rest_sessions.LowLevelRestfulClient) + mock_low_level_restful_client = mock.MagicMock(rest.REST) class RESTInviteLogicImpl(invite.RESTInviteComponent): def __init__(self): diff --git a/tests/hikari/clients/test_rest_clients/test_me.py b/tests/hikari/clients/test_rest/test_me.py similarity index 98% rename from tests/hikari/clients/test_rest_clients/test_me.py rename to tests/hikari/clients/test_rest/test_me.py index c1d3ae6424..b64a0544bb 100644 --- a/tests/hikari/clients/test_rest_clients/test_me.py +++ b/tests/hikari/clients/test_rest/test_me.py @@ -27,17 +27,17 @@ from hikari import guilds from hikari import applications from hikari import users -from hikari.clients.rest_clients import me +from hikari.clients.rest import me from hikari.internal import conversions from hikari.internal import pagination -from hikari.net import rest_sessions +from hikari.net import rest from tests.hikari import _helpers class TestRESTInviteLogic: @pytest.fixture() def rest_clients_impl(self): - mock_low_level_restful_client = mock.MagicMock(rest_sessions.LowLevelRestfulClient) + mock_low_level_restful_client = mock.MagicMock(rest.REST) class RESTCurrentUserLogicImpl(me.RESTCurrentUserComponent): def __init__(self): diff --git a/tests/hikari/clients/test_rest_clients/test_oauth2.py b/tests/hikari/clients/test_rest/test_oauth2.py similarity index 91% rename from tests/hikari/clients/test_rest_clients/test_oauth2.py rename to tests/hikari/clients/test_rest/test_oauth2.py index 0a6ebf4745..3190718cf5 100644 --- a/tests/hikari/clients/test_rest_clients/test_oauth2.py +++ b/tests/hikari/clients/test_rest/test_oauth2.py @@ -21,14 +21,14 @@ import pytest from hikari import applications -from hikari.clients.rest_clients import oauth2 -from hikari.net import rest_sessions +from hikari.clients.rest import oauth2 +from hikari.net import rest class TestRESTReactionLogic: @pytest.fixture() def rest_oauth2_logic_impl(self): - mock_low_level_restful_client = mock.MagicMock(rest_sessions.LowLevelRestfulClient) + mock_low_level_restful_client = mock.MagicMock(rest.REST) class RESTOauth2LogicImpl(oauth2.RESTOAuth2Component): def __init__(self): diff --git a/tests/hikari/clients/test_rest_clients/test_react.py b/tests/hikari/clients/test_rest/test_react.py similarity index 97% rename from tests/hikari/clients/test_rest_clients/test_react.py rename to tests/hikari/clients/test_rest/test_react.py index cf601cd341..6442e717eb 100644 --- a/tests/hikari/clients/test_rest_clients/test_react.py +++ b/tests/hikari/clients/test_rest/test_react.py @@ -25,16 +25,16 @@ from hikari import emojis from hikari import messages from hikari import users -from hikari.clients.rest_clients import react +from hikari.clients.rest import react from hikari.internal import pagination -from hikari.net import rest_sessions +from hikari.net import rest from tests.hikari import _helpers class TestRESTReactionLogic: @pytest.fixture() def rest_reaction_logic_impl(self): - mock_low_level_restful_client = mock.MagicMock(rest_sessions.LowLevelRestfulClient) + mock_low_level_restful_client = mock.MagicMock(rest.REST) class RESTReactionLogicImpl(react.RESTReactionComponent): def __init__(self): diff --git a/tests/hikari/clients/test_rest_clients/test_user.py b/tests/hikari/clients/test_rest/test_user.py similarity index 91% rename from tests/hikari/clients/test_rest_clients/test_user.py rename to tests/hikari/clients/test_rest/test_user.py index 82dd32795d..1001a70d3a 100644 --- a/tests/hikari/clients/test_rest_clients/test_user.py +++ b/tests/hikari/clients/test_rest/test_user.py @@ -20,15 +20,15 @@ import pytest from hikari import users -from hikari.clients.rest_clients import user -from hikari.net import rest_sessions +from hikari.clients.rest import user +from hikari.net import rest from tests.hikari import _helpers class TestRESTUserLogic: @pytest.fixture() def rest_user_logic_impl(self): - mock_low_level_restful_client = mock.MagicMock(rest_sessions.LowLevelRestfulClient) + mock_low_level_restful_client = mock.MagicMock(rest.REST) class RESTUserLogicImpl(user.RESTUserComponent): def __init__(self): diff --git a/tests/hikari/clients/test_rest_clients/test_voice.py b/tests/hikari/clients/test_rest/test_voice.py similarity index 90% rename from tests/hikari/clients/test_rest_clients/test_voice.py rename to tests/hikari/clients/test_rest/test_voice.py index 8ff852760f..d040d26583 100644 --- a/tests/hikari/clients/test_rest_clients/test_voice.py +++ b/tests/hikari/clients/test_rest/test_voice.py @@ -20,14 +20,14 @@ import pytest from hikari import voices -from hikari.clients.rest_clients import voice -from hikari.net import rest_sessions +from hikari.clients.rest import voice +from hikari.net import rest class TestRESTUserLogic: @pytest.fixture() def rest_voice_logic_impl(self): - mock_low_level_restful_client = mock.MagicMock(rest_sessions.LowLevelRestfulClient) + mock_low_level_restful_client = mock.MagicMock(rest.REST) class RESTVoiceLogicImpl(voice.RESTVoiceComponent): def __init__(self): diff --git a/tests/hikari/clients/test_rest_clients/test_webhook.py b/tests/hikari/clients/test_rest/test_webhook.py similarity index 98% rename from tests/hikari/clients/test_rest_clients/test_webhook.py rename to tests/hikari/clients/test_rest/test_webhook.py index d2d323008e..eac25b82cd 100644 --- a/tests/hikari/clients/test_rest_clients/test_webhook.py +++ b/tests/hikari/clients/test_rest/test_webhook.py @@ -26,17 +26,17 @@ from hikari import media from hikari import messages from hikari import webhooks -from hikari.clients.rest_clients import webhook +from hikari.clients.rest import webhook from hikari.internal import allowed_mentions from hikari.internal import conversions -from hikari.net import rest_sessions +from hikari.net import rest from tests.hikari import _helpers class TestRESTUserLogic: @pytest.fixture() def rest_webhook_logic_impl(self): - mock_low_level_restful_client = mock.MagicMock(rest_sessions.LowLevelRestfulClient) + mock_low_level_restful_client = mock.MagicMock(rest.REST) class RESTWebhookLogicImpl(webhook.RESTWebhookComponent): def __init__(self): diff --git a/tests/hikari/clients/test_shard_clients.py b/tests/hikari/clients/test_shards.py similarity index 95% rename from tests/hikari/clients/test_shard_clients.py rename to tests/hikari/clients/test_shards.py index c8cb318ad2..57043ba01d 100644 --- a/tests/hikari/clients/test_shard_clients.py +++ b/tests/hikari/clients/test_shards.py @@ -26,10 +26,10 @@ from hikari import errors from hikari import guilds from hikari.clients import configs -from hikari.clients import shard_clients +from hikari.clients import shards as high_level_shards from hikari.net import codes -from hikari.net import shards -from hikari.state import raw_event_consumers +from hikari.net import shards as low_level_shards +from hikari.state import consumers from tests.hikari import _helpers @@ -51,32 +51,32 @@ def done(self): @pytest.fixture def shard_client_obj(): mock_shard_connection = mock.MagicMock( - shards.ShardConnection, + low_level_shards.Shard, heartbeat_latency=float("nan"), heartbeat_interval=float("nan"), reconnect_count=0, seq=None, session_id=None, ) - with mock.patch("hikari.net.shards.ShardConnection", return_value=mock_shard_connection): - return _helpers.unslot_class(shard_clients.ShardClientImpl)(0, 1, configs.GatewayConfig(), None, "some_url") + with mock.patch("hikari.net.shards.Shard", return_value=mock_shard_connection): + return _helpers.unslot_class(high_level_shards.ShardClientImpl)(0, 1, configs.GatewayConfig(), None, "some_url") class TestShardClientImpl: def test_raw_event_consumer_in_ShardClientImpl(self): - class DummyConsumer(raw_event_consumers.RawEventConsumer): + class DummyConsumer(consumers.RawEventConsumer): def process_raw_event(self, _client, name, payload): return "ASSERT TRUE" - shard_client_obj = shard_clients.ShardClientImpl(0, 1, configs.GatewayConfig(), DummyConsumer(), "some_url") + shard_client_obj = high_level_shards.ShardClientImpl(0, 1, configs.GatewayConfig(), DummyConsumer(), "some_url") assert shard_client_obj._connection.dispatch(shard_client_obj, "TEST", {}) == "ASSERT TRUE" def test_connection_is_set(self, shard_client_obj): - mock_shard_connection = mock.MagicMock(shards.ShardConnection) + mock_shard_connection = mock.MagicMock(low_level_shards.Shard) - with mock.patch("hikari.net.shards.ShardConnection", return_value=mock_shard_connection): - shard_client_obj = shard_clients.ShardClientImpl(0, 1, configs.GatewayConfig(), None, "some_url") + with mock.patch("hikari.net.shards.Shard", return_value=mock_shard_connection): + shard_client_obj = high_level_shards.ShardClientImpl(0, 1, configs.GatewayConfig(), None, "some_url") assert shard_client_obj._connection is mock_shard_connection @@ -239,7 +239,7 @@ async def forever(): @_helpers.assert_raises(type_=RuntimeError) @pytest.mark.asyncio async def test_start_when_already_started(self, shard_client_obj): - shard_client_obj._shard_state = shard_clients.ShardState.READY + shard_client_obj._shard_state = high_level_shards.ShardState.READY await shard_client_obj.start() @@ -269,7 +269,7 @@ async def test_close(self, shard_client_obj): @pytest.mark.asyncio async def test_close_when_already_stopping(self, shard_client_obj): - shard_client_obj._shard_state = shard_clients.ShardState.STOPPING + shard_client_obj._shard_state = high_level_shards.ShardState.STOPPING await shard_client_obj.close() diff --git a/tests/hikari/net/test_rest_sessions.py b/tests/hikari/net/test_rest.py similarity index 99% rename from tests/hikari/net/test_rest_sessions.py rename to tests/hikari/net/test_rest.py index 379d499655..fa23094c7a 100644 --- a/tests/hikari/net/test_rest_sessions.py +++ b/tests/hikari/net/test_rest.py @@ -29,7 +29,7 @@ from hikari import errors from hikari.internal import conversions from hikari.net import ratelimits -from hikari.net import rest_sessions +from hikari.net import rest from hikari.net import routes from tests.hikari import _helpers @@ -43,9 +43,7 @@ def rest_impl(self): stack.enter_context(mock.patch.object(ratelimits, "RESTBucketManager")) stack.enter_context(mock.patch.object(ratelimits, "ManualRateLimiter")) with stack: - client = rest_sessions.LowLevelRestfulClient( - base_url="https://discordapp.com/api/v6", token="Bot blah.blah.blah" - ) + client = rest.REST(base_url="https://discordapp.com/api/v6", token="Bot blah.blah.blah") client._request = mock.AsyncMock(return_value=...) client.client_session = mock.MagicMock(aiohttp.ClientSession, spec_set=True) @@ -105,9 +103,9 @@ async def test__init__with_bot_token_and_without_optionals(self): stack.enter_context(mock.patch.object(ratelimits, "RESTBucketManager", return_value=buckets_mock)) with stack: - client = rest_sessions.LowLevelRestfulClient(token="Bot token.otacon.a-token") + client = rest.REST(token="Bot token.otacon.a-token") - assert client.base_url == f"https://discordapp.com/api/v{rest_sessions.VERSION_6}" + assert client.base_url == f"https://discordapp.com/api/v{rest.VERSION_6}" assert client.client_session is None assert client.global_ratelimiter is mock_manual_rate_limiter assert client.json_serialize is json.dumps @@ -121,7 +119,7 @@ async def test__init__with_bearer_token_and_without_optionals(self): stack.enter_context(mock.patch.object(ratelimits, "ManualRateLimiter")) stack.enter_context(mock.patch.object(ratelimits, "RESTBucketManager")) with stack: - client = rest_sessions.LowLevelRestfulClient(token="Bearer token.otacon.a-token") + client = rest.REST(token="Bearer token.otacon.a-token") assert client.token == "Bearer token.otacon.a-token" @pytest.mark.asyncio @@ -142,7 +140,7 @@ async def test__init__with_optionals(self): ) with stack: - client = rest_sessions.LowLevelRestfulClient( + client = rest.REST( token="Bot token.otacon.a-token", base_url="https://discordapp.com/api/v69420", allow_redirects=True, @@ -170,7 +168,7 @@ async def test__init__raises_runtime_error_with_invalid_token(self, *_): stack.enter_context(mock.patch.object(ratelimits, "ManualRateLimiter")) stack.enter_context(mock.patch.object(ratelimits, "RESTBucketManager")) with stack: - async with rest_sessions.LowLevelRestfulClient(token="An-invalid-TOKEN"): + async with rest.REST(token="An-invalid-TOKEN"): pass @pytest.mark.asyncio @@ -218,7 +216,7 @@ async def test_first_call_to_request_opens_client_session(self): stack.enter_context(mock.patch.object(aiohttp, "ClientSession", return_value=client_session_mock)) with stack: - client = rest_sessions.LowLevelRestfulClient(token="Bot token.otacon.a-token") + client = rest.REST(token="Bot token.otacon.a-token") assert client.client_session is None # lazy init with contextlib.suppress(Exception): await client._request(mock.MagicMock(routes.CompiledRoute, spec_set=True)) @@ -237,7 +235,7 @@ async def test_more_calls_to_request_use_existing_client_session(self): stack.enter_context(mock.patch.object(aiohttp, "ClientSession", return_value=client_session_mock)) with stack: - client = rest_sessions.LowLevelRestfulClient(token="Bot token.otacon.a-token") + client = rest.REST(token="Bot token.otacon.a-token") with contextlib.suppress(Exception): await client._request(mock.MagicMock(routes.CompiledRoute, spec_set=True)) @@ -254,7 +252,7 @@ async def test_more_calls_to_request_use_existing_client_session(self): @mock.patch.object(ratelimits, "RESTBucketManager") @mock.patch.object(aiohttp, "ClientSession") def rest_impl_with__request(self, *args): - rest_impl = rest_sessions.LowLevelRestfulClient(token="Bot token") + rest_impl = rest.REST(token="Bot token") rest_impl.logger = mock.MagicMock(debug=mock.MagicMock()) rest_impl.bucket_ratelimiters = mock.MagicMock( ratelimits.RESTBucketManager, acquire=mock.MagicMock(), update_rate_limits=mock.MagicMock(), @@ -490,7 +488,7 @@ async def test__request_when_TOO_MANY_REQUESTS_when_not_global( rest_impl_with__request.logger.debug.side_effect = [None, exit_error] with mock.patch("asyncio.gather", return_value=_helpers.AwaitableMock()): - with mock.patch.object(rest_sessions.LowLevelRestfulClient, "_request", return_value=discord_response): + with mock.patch.object(rest.REST, "_request", return_value=discord_response): try: await rest_impl_with__request._request(compiled_route) except exit_error: @@ -518,7 +516,7 @@ async def test__request_raises_appropriate_error_for_status_code( stack.enter_context(mock.patch.object(ratelimits, "RESTBucketManager")) discord_response.status = status_code with stack: - rest_impl = rest_sessions.LowLevelRestfulClient(token="Bot token", version=api_version) + rest_impl = rest.REST(token="Bot token", version=api_version) rest_impl.bucket_ratelimiters = mock.MagicMock() rest_impl.global_ratelimiter = mock.MagicMock() rest_impl.client_session = mock.MagicMock(aiohttp.ClientSession) diff --git a/tests/hikari/net/test_shards.py b/tests/hikari/net/test_shards.py index 94f5da279e..60c07da579 100644 --- a/tests/hikari/net/test_shards.py +++ b/tests/hikari/net/test_shards.py @@ -85,12 +85,12 @@ async def ws_connect(self, *args, **kwargs): class TestShardConstructor: async def test_init_sets_shard_numbers_correctly(self): input_shard_id, input_shard_count, expected_shard_id, expected_shard_count = 1, 2, 1, 2 - client = shards.ShardConnection(shard_id=input_shard_id, shard_count=input_shard_count, token="xxx", url="yyy") + client = shards.Shard(shard_id=input_shard_id, shard_count=input_shard_count, token="xxx", url="yyy") assert client.shard_id == expected_shard_id assert client.shard_count == expected_shard_count async def test_dispatch_is_callable(self): - client = shards.ShardConnection(token="xxx", url="yyy") + client = shards.Shard(token="xxx", url="yyy") client.dispatch(client, "ping", "pong") @pytest.mark.parametrize( @@ -102,7 +102,7 @@ async def test_dispatch_is_callable(self): ) async def test_compression(self, compression, expected_url_query): url = "ws://baka-im-not-a-http-url:49620/locate/the/bloody/websocket?ayyyyy=lmao" - client = shards.ShardConnection(token="xxx", url=url, compression=compression) + client = shards.Shard(token="xxx", url=url, compression=compression) scheme, netloc, path, params, query, fragment = urllib.parse.urlparse(client._url) assert scheme == "ws" assert netloc == "baka-im-not-a-http-url:49620" @@ -113,13 +113,13 @@ async def test_compression(self, compression, expected_url_query): assert fragment == "" async def test_init_hearbeat_defaults_before_startup(self): - client = shards.ShardConnection(token="xxx", url="yyy") + client = shards.Shard(token="xxx", url="yyy") assert math.isnan(client.last_heartbeat_sent) assert math.isnan(client.heartbeat_latency) assert math.isnan(client.last_message_received) async def test_init_connected_at_is_nan(self): - client = shards.ShardConnection(token="xxx", url="yyy") + client = shards.Shard(token="xxx", url="yyy") assert math.isnan(client._connected_at) @@ -131,7 +131,7 @@ class TestShardUptimeProperty: ) async def test_uptime(self, connected_at, now, expected_uptime): with mock.patch("time.perf_counter", return_value=now): - client = shards.ShardConnection(token="xxx", url="yyy") + client = shards.Shard(token="xxx", url="yyy") client._connected_at = connected_at assert client.uptime == expected_uptime @@ -140,7 +140,7 @@ async def test_uptime(self, connected_at, now, expected_uptime): class TestShardIsConnectedProperty: @pytest.mark.parametrize(["connected_at", "is_connected"], [(float("nan"), False), (15, True), (2500.0, True),]) async def test_is_connected(self, connected_at, is_connected): - client = shards.ShardConnection(token="xxx", url="yyy") + client = shards.Shard(token="xxx", url="yyy") client._connected_at = connected_at assert client.is_connected is is_connected @@ -161,7 +161,7 @@ class TestGatewayReconnectCountProperty: ], ) async def test_value(self, disconnect_count, is_connected, expected_reconnect_count): - client = shards.ShardConnection(token="xxx", url="yyy") + client = shards.Shard(token="xxx", url="yyy") client.disconnect_count = disconnect_count client._connected_at = 420 if is_connected else float("nan") assert client.reconnect_count == expected_reconnect_count @@ -170,12 +170,12 @@ async def test_value(self, disconnect_count, is_connected, expected_reconnect_co @pytest.mark.asyncio class TestGatewayCurrentPresenceProperty: async def test_returns_presence(self): - client = shards.ShardConnection(token="xxx", url="yyy") + client = shards.Shard(token="xxx", url="yyy") client._presence = {"foo": "bar"} assert client.current_presence == {"foo": "bar"} async def test_returns_copy(self): - client = shards.ShardConnection(token="xxx", url="yyy") + client = shards.Shard(token="xxx", url="yyy") client._presence = {"foo": "bar"} assert client.current_presence is not client._presence @@ -185,7 +185,7 @@ class TestShardAiohttpClientSessionKwargsProperty: async def test_right_stuff_is_included(self): connector = mock.MagicMock() - client = shards.ShardConnection(url="...", token="...", connector=connector,) + client = shards.Shard(url="...", token="...", connector=connector,) assert client._cs_init_kwargs() == dict(connector=connector) @@ -200,7 +200,7 @@ async def test_right_stuff_is_included(self): verify_ssl = True ssl_context = mock.MagicMock() - client = shards.ShardConnection( + client = shards.Shard( url=url, token="...", proxy_url=proxy_url, @@ -247,7 +247,7 @@ def non_hello_payload(self): @pytest.fixture def client(self, event_loop): asyncio.set_event_loop(event_loop) - client = _helpers.unslot_class(shards.ShardConnection)(url="ws://localhost", token="xxx") + client = _helpers.unslot_class(shards.Shard)(url="ws://localhost", token="xxx") client = _helpers.mock_methods_on(client, except_=("connect", "_cs_init_kwargs", "_ws_connect_kwargs")) client._receive = mock.AsyncMock(return_value=self.hello_payload) return client @@ -536,7 +536,7 @@ class TestShardRun: @pytest.fixture def client(self, event_loop): asyncio.set_event_loop(event_loop) - client = _helpers.unslot_class(shards.ShardConnection)(token="1234", url="xxx") + client = _helpers.unslot_class(shards.Shard)(token="1234", url="xxx") client = _helpers.mock_methods_on(client, except_=("_run",)) def receive(): @@ -548,9 +548,9 @@ def identify(): def resume(): client.resume_time = time.perf_counter() - client._identify = mock.AsyncMock(spec=shards.ShardConnection._identify, wraps=identify) - client._resume = mock.AsyncMock(spec=shards.ShardConnection._resume, wraps=resume) - client._receive = mock.AsyncMock(spec=shards.ShardConnection._receive, wraps=receive) + client._identify = mock.AsyncMock(spec=shards.Shard._identify, wraps=identify) + client._resume = mock.AsyncMock(spec=shards.Shard._resume, wraps=resume) + client._receive = mock.AsyncMock(spec=shards.Shard._receive, wraps=receive) return client async def test_no_session_id_sends_identify_then_polls_events(self, client): @@ -583,7 +583,7 @@ class TestIdentify: @pytest.fixture def client(self, event_loop): asyncio.set_event_loop(event_loop) - client = _helpers.unslot_class(shards.ShardConnection)(token="1234", url="xxx") + client = _helpers.unslot_class(shards.Shard)(token="1234", url="xxx") client = _helpers.mock_methods_on(client, except_=("_identify",)) return client @@ -697,7 +697,7 @@ class TestResume: @pytest.fixture def client(self, event_loop): asyncio.set_event_loop(event_loop) - client = _helpers.unslot_class(shards.ShardConnection)(token="1234", url="xxx") + client = _helpers.unslot_class(shards.Shard)(token="1234", url="xxx") client = _helpers.mock_methods_on(client, except_=("_resume",)) return client @@ -718,7 +718,7 @@ class TestHeartbeatKeepAlive: @pytest.fixture def client(self, event_loop): asyncio.set_event_loop(event_loop) - client = _helpers.unslot_class(shards.ShardConnection)(token="1234", url="xxx") + client = _helpers.unslot_class(shards.Shard)(token="1234", url="xxx") client = _helpers.mock_methods_on(client, except_=("_heartbeat_keep_alive", "_zombie_detector")) client._send = mock.AsyncMock() # This won't get set on the right event loop if we are not careful @@ -779,14 +779,14 @@ async def test_zombie_detector_not_a_zombie(self): client = mock.MagicMock() client.last_message_received = time.perf_counter() - 5 heartbeat_interval = 41.25 - shards.ShardConnection._zombie_detector(client, heartbeat_interval) + shards.Shard._zombie_detector(client, heartbeat_interval) @_helpers.assert_raises(type_=asyncio.TimeoutError) async def test_zombie_detector_is_a_zombie(self): client = mock.MagicMock() client.last_message_received = time.perf_counter() - 500000 heartbeat_interval = 41.25 - shards.ShardConnection._zombie_detector(client, heartbeat_interval) + shards.Shard._zombie_detector(client, heartbeat_interval) @pytest.mark.asyncio @@ -794,7 +794,7 @@ class TestClose: @pytest.fixture def client(self, event_loop): asyncio.set_event_loop(event_loop) - client = _helpers.unslot_class(shards.ShardConnection)(token="1234", url="xxx") + client = _helpers.unslot_class(shards.Shard)(token="1234", url="xxx") client = _helpers.mock_methods_on(client, except_=("close",)) client.ws = mock.MagicMock(aiohttp.ClientWebSocketResponse) client.session = mock.MagicMock(aiohttp.ClientSession) @@ -865,7 +865,7 @@ class TestPollEvents: @pytest.fixture def client(self, event_loop): asyncio.set_event_loop(event_loop) - client = _helpers.unslot_class(shards.ShardConnection)(token="1234", url="xxx") + client = _helpers.unslot_class(shards.Shard)(token="1234", url="xxx") client = _helpers.mock_methods_on(client, except_=("_run",)) return client @@ -905,7 +905,7 @@ class TestRequestGuildMembers: @pytest.fixture def client(self, event_loop): asyncio.set_event_loop(event_loop) - client = _helpers.unslot_class(shards.ShardConnection)(token="1234", url="xxx") + client = _helpers.unslot_class(shards.Shard)(token="1234", url="xxx") client = _helpers.mock_methods_on(client, except_=("request_guild_members",)) return client @@ -942,7 +942,7 @@ class TestUpdatePresence: @pytest.fixture def client(self, event_loop): asyncio.set_event_loop(event_loop) - client = _helpers.unslot_class(shards.ShardConnection)(token="1234", url="xxx") + client = _helpers.unslot_class(shards.Shard)(token="1234", url="xxx") client = _helpers.mock_methods_on(client, except_=("update_presence",)) return client diff --git a/tests/hikari/state/test_event_dispatcher.py b/tests/hikari/state/test_event_dispatchers.py similarity index 99% rename from tests/hikari/state/test_event_dispatcher.py rename to tests/hikari/state/test_event_dispatchers.py index 56775a060e..abbe9d370b 100644 --- a/tests/hikari/state/test_event_dispatcher.py +++ b/tests/hikari/state/test_event_dispatchers.py @@ -22,7 +22,7 @@ import pytest from hikari import events -from hikari.state import event_dispatchers +from hikari.state import dispatchers from tests.hikari import _helpers @@ -41,7 +41,7 @@ class StudEvent3(events.HikariEvent): class TestEventDispatcherImpl: @pytest.fixture def dispatcher_inst(self): - return _helpers.unslot_class(event_dispatchers.EventDispatcherImpl)() + return _helpers.unslot_class(dispatchers.EventDispatcherImpl)() # noinspection PyTypeChecker @_helpers.assert_raises(type_=TypeError) diff --git a/tests/hikari/state/test_stateless_event_managers.py b/tests/hikari/state/test_stateless.py similarity index 98% rename from tests/hikari/state/test_stateless_event_managers.py rename to tests/hikari/state/test_stateless.py index 73ea3bbe3b..acd95c416b 100644 --- a/tests/hikari/state/test_stateless_event_managers.py +++ b/tests/hikari/state/test_stateless.py @@ -20,8 +20,8 @@ import pytest from hikari import events -from hikari.clients import shard_clients -from hikari.state import stateless_event_managers +from hikari.clients import shards +from hikari.state import stateless class TestStatelessEventManagerImpl: @@ -34,11 +34,11 @@ def event_manager_impl(self): class MockDispatcher: dispatch_event = mock.MagicMock() - return stateless_event_managers.StatelessEventManagerImpl(event_dispatcher_impl=MockDispatcher()) + return stateless.StatelessEventManagerImpl(event_dispatcher_impl=MockDispatcher()) @pytest.fixture def mock_shard(self): - return mock.MagicMock(shard_clients.ShardClient) + return mock.MagicMock(shards.ShardClient) def test_on_connect(self, event_manager_impl, mock_shard): mock_event = mock.MagicMock(events.ConnectedEvent) From 3e5f92e73a004bf73711bb0341edd838cc80e99a Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Mon, 20 Apr 2020 19:36:50 +0100 Subject: [PATCH 175/922] Updated pre-commit, pylint, pytest, docker-compose configs. --- docker-compose.yml | 4 ++-- pre-commit | 1 + pylint.ini | 2 +- pytest.ini | 2 +- 4 files changed, 5 insertions(+), 4 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index d7790a7d82..3e93f5c351 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,9 +1,9 @@ version: "3" services: - gateway-test: + test-client: build: . - entrypoint: python -m hikari.clients.test_client --logger=DEBUG + entrypoint: python -m hikari.clients.test env_file: - credentials.env restart: always diff --git a/pre-commit b/pre-commit index fd879659c3..2c27225fc8 100755 --- a/pre-commit +++ b/pre-commit @@ -3,6 +3,7 @@ # Put this in .git/hooks to make sure 'black' runs every time you commit. if [[ -z ${SKIP_HOOK} ]]; then + command -v nox || pip install nox echo -e '\e[1;32mRUNNING HOOKS - prepend your invocation of git commit with SKIP_HOOK=1 to skip this.\e[0m' nox --sessions reformat-code pytest pylint pydocstyle else diff --git a/pylint.ini b/pylint.ini index a5d5514880..0760ac9ae2 100644 --- a/pylint.ini +++ b/pylint.ini @@ -11,7 +11,7 @@ ignore=CVS # Add files or directories matching the regex patterns to the blacklist. The # regex matches against base names, not paths. -ignore-patterns=test_client.py +ignore-patterns=test.py # Python code to execute, usually for sys.path manipulation such as # pygtk.require(). diff --git a/pytest.ini b/pytest.ini index 084bf00402..88c71ec4bc 100644 --- a/pytest.ini +++ b/pytest.ini @@ -16,7 +16,7 @@ markers = integration: Mark this test as an integration test. xfail_strict = true -norecursedirs = docs *.egg-info .git tasks .nox .pytest_cache .venv venv public +norecursedirs = docs *.egg-info .git tasks .nox .pytest_cache .venv venv public ci filterwarnings = ignore:.*"@coroutine" decorator is deprecated.*:DeprecationWarning From ebdb59ae92990ba2cda49081f2a1f5c7c91e2ece Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Mon, 20 Apr 2020 19:39:42 +0100 Subject: [PATCH 176/922] Fixed missed out import for intents in global ns. --- hikari/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/hikari/__init__.py b/hikari/__init__.py index 0e1db6f8a5..2a1c29a6bb 100644 --- a/hikari/__init__.py +++ b/hikari/__init__.py @@ -56,6 +56,7 @@ from hikari.events import * from hikari.gateway_entities import * from hikari.guilds import * +from hikari.intents import * from hikari.invites import * from hikari.messages import * from hikari.net import * From bd5b6680b4b9343af8ee57819dc6ef093c9ffb52 Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Mon, 20 Apr 2020 20:05:57 +0100 Subject: [PATCH 177/922] Added my C-marshaller for later, added mission JSON codes. --- hikari/internal/marshaller.c | 206 +++++++++++++++++++++++++++++++++++ hikari/net/codes.py | 10 +- 2 files changed, 214 insertions(+), 2 deletions(-) create mode 100644 hikari/internal/marshaller.c diff --git a/hikari/internal/marshaller.c b/hikari/internal/marshaller.c new file mode 100644 index 0000000000..cb7a689b31 --- /dev/null +++ b/hikari/internal/marshaller.c @@ -0,0 +1,206 @@ +/******************************************************************************* + * Copyright © Nekokatt 2019-2020 * + * * + * This file is part of Hikari. * + * * + * Hikari is free software: you can redistribute it and/or modify * + * it under the terms of the GNU Lesser General Public License as published by * + * the Free Software Foundation, either version 3 of the License, or * + * (at your option) any later version. * + * * + * Hikari is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * + * GNU Lesser General Public License for more details. * + * * + * You should have received a copy of the GNU Lesser General Public License * + * along with Hikari. If not, see . * + * * + *******************************************************************************/ + +/// +/// Accelerated implementation of hikari.internal.marshaller for platforms with +/// a C compiler in the working environment, and built using CPython. +/// +/// @author Nekokatt +/// @since 1.0.1 +/// + +// Ensure cross compatibility with any Python version >= 3.8.0. +#define Py_LIMITED_API 0x308000 + +#include + +#include +#include + + +PyDoc_STRVAR( + module_doc, + "An internal marshalling utility used by internal API components.\n" + "\n" + "Warning\n" + "-------\n" + "You should not change anything in this file, if you do, you will likely get\n" + "unexpected behaviour elsewhere.\n" + "\n" + "|internal|\n" +); + + +static PyObject *_import_module; + + +/// +/// Given a root object "obj", and a period-delimited collection +/// of attribute names, perform the equivalent of +/// "eval('obj.' + attr_name)" without executing arbitrary code +/// unnecesarilly. +/// +/// @param obj the object to begin on. +/// @param attr_name the string name of the attribute to get, +/// period delimited. +/// @returns NULL if an exception occurred, or the referenced object +/// +/// @author Nekokatt +/// @since 1.0.1 +/// +static PyObject * +_recursive_getattr(PyObject * obj, const char * restrict attr_name) +{ + char * temp = malloc(strlen(attr_name) + 1); + char * delim; + + while ((delim = strstr(attr_name, "."))) { + temp = strncpy(temp, attr_name, delim - attr_name); + // skip pass the period. + attr_name = delim + 1; + obj = PyObject_GetAttrString(obj, temp); + } + + // getattr + obj = PyObject_GetAttrString(obj, attr_name); + free(temp); + + return obj; +} + + +PyDoc_STRVAR( + dereference_handle_doc, + "Parse a given handle string into an object reference.\n" + "\n" + "Parameters\n" + "----------\n" + "handle_string : :obj:`str`\n" + " The handle to the object to refer to. This is in the format\n" + " ``fully.qualified.module.name#object.attribute``. If no ``#`` is\n" + " input, then the reference will be made to the module itself.\n" + "\n" + "Returns\n" + "-------\n" + ":obj:`typing.Any`\n" + " The thing that is referred to from this reference.\n" + "\n" + "Examples\n" + "--------\n" + "``\"collections#deque\"``:\n" + " Refers to :obj:`collections.deque`\n" + "``\"asyncio.tasks#Task\"``:\n" + " Refers to ``asyncio.tasks.Task``\n" + "``\"hikari.net\"``:\n" + " Refers to :obj:`hikari.net`\n" + "``\"foo.bar#baz.bork.qux\"``:\n" + " Would refer to a theoretical ``qux`` attribute on a ``bork``\n" + " attribute on a ``baz`` object in the ``foo.bar`` module.\n" +); + + +/// +/// Take an input string such as "foo", "foo#bar", "foo.bar#baz.bork"; +/// and attempt to "eval" it to get the result. This works by delimiting +/// a module and an attribute by the "#" (or treating the whole string +/// as a module if that character is not present), and then +/// traversing the object graph to get nested attributes. +/// +/// This is an accelerated implementation of +/// `marshaller.dereference_handle` in `marshaller.py`. +/// +/// @param _ the module. +/// @param args any parameters passed to the function call. +/// @returns NULL if an exception was raised, or a PyObject reference +/// otherwise. +/// @author Nekokatt +/// @since 1.0.1 +/// +static PyObject * +dereference_handle(PyObject *_, PyObject *args) +{ + const char * handle_str; + PyArg_ParseTuple(args, "s:handle_string", &handle_str); + // Substring from the '#' onwards + const char * p_strstr = strstr(handle_str, "#"); + + if (p_strstr == NULL || (p_strstr - handle_str) <= 1) { + // string was in format "module_name" only, or just + // ended with a "#" erraneously (just ignore this). + return PyObject_CallFunction(_import_module, "s", handle_str); + } else { + // +1 for null end byte..., -1 because it would include "#" otherwise. + const size_t module_len = p_strstr - handle_str; + char * module_str = malloc(module_len); + + strncpy(module_str, handle_str, module_len); + PyObject * module = PyObject_CallFunction(_import_module, "s", module_str); + free(module_str); + + if (module == NULL) { + // Expect an exception to have been raised in the interpreter. + return NULL; + } + + PyObject * result = _recursive_getattr(module, p_strstr + 1); + Py_DECREF(module); + return result; + } +} + +/// +/// Public method table for this module. +/// +static PyMethodDef method_table[] = { + {"dereference_handle", dereference_handle, METH_VARARGS, dereference_handle_doc}, + {NULL, NULL, 0, NULL} +}; + +/// +/// This module descriptor. +/// +static struct PyModuleDef this_module = { + PyModuleDef_HEAD_INIT, + "hikari.internal.marshaller", + module_doc, + -1, + method_table, +}; + +/// +/// Init this module. +/// +PyMODINIT_FUNC +PyInit_marshaller(void) +{ + PyObject * importlib = PyImport_ImportModule("importlib"); + if (importlib == NULL) { + return NULL; + } + + _import_module = PyObject_GetAttrString(importlib, "import_module"); + Py_DecRef(importlib); + + if (_import_module == NULL) { + return NULL; + } + + return PyModule_Create(&this_module); +} diff --git a/hikari/net/codes.py b/hikari/net/codes.py index 741da9090c..655f760c22 100644 --- a/hikari/net/codes.py +++ b/hikari/net/codes.py @@ -264,12 +264,15 @@ class JSONErrorCode(more_enums.EnumMixin, enum.IntEnum): #: Unknown user UNKNOWN_USER = 10_013 - #: Unknown Emoji + #: Unknown emoji UNKNOWN_EMOJI = 10_014 - #: Unknown Webhook + #: Unknown webhook UNKNOWN_WEBHOOK = 10_015 + #: Unknown ban + UNKNOWN_BAN = 10_026 + #: Bots cannot use this endpoint #: #: Note @@ -328,6 +331,9 @@ class JSONErrorCode(more_enums.EnumMixin, enum.IntEnum): #: Unauthorized UNAUTHORIZED = 40_001 + #: You need to verify your account to perform this action. + NEEDS_VERIFICATION = 40_002 + #: Request entity too large. Try sending something smaller in size TOO_LARGE = 40_005 From 6a196b692de245506aa93497b034303409fc999e Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Mon, 20 Apr 2020 20:53:53 +0100 Subject: [PATCH 178/922] Rewrote README.md --- README.md | 134 +++++++++++++++++++++++++++++++++++++++++------------- 1 file changed, 102 insertions(+), 32 deletions(-) diff --git a/README.md b/README.md index 164e69bb04..8c4dcfae99 100644 --- a/README.md +++ b/README.md @@ -1,47 +1,117 @@ -# hikari +**Note:** this API is still under active daily development, and is in a +**pre-alpha** stage. If you are looking to give feedback, or want to help us +out, then feel free to join our [Discord server](https://discord.gg/HMnGbsv) and +chat to us. Any help is greatly appreciated, no matter what your experience +level may be! :-) -An opinionated Discord API for Python 3 and asyncio. +--- -**THIS API IS CURRENTLY IN A PRE-ALPHA STAGE, SO NEW VERSIONS WILL CONTAIN BREAKING CHANGES WITHOUT A MINOR -VERSION INCREMENTATION. ALL FEATURES ARE PROVISIONAL AND CAN CHANGE AT ANY TIME UNTIL THIS API IS IN A USABLE -STATE FOR A FULL FIRST RELEASE.** - - **[Please VISIT MY DISCORD](https://discord.gg/HMnGbsv) if you wish to receive progress updates or help out, any - help and contribution is more than welcome :-)** +# hikari.py + +An opinionated Discord API for Python 3 and asyncio. Built on good intentions +and the hope that it will be extendable and reusable, rather than an obstacle. + +```py +import hikari + +bot = hikari.StatelessBot(config=hikari.BotConfig(token="...")) + +@bot.on() +async def ping(event: hikari.MessageCreateEvent) -> None: + if not event.author.is_bot and event.content.startswith("hk.ping"): + await bot.rest.create_message(event.channel_id, content="Pong!") + + +bot.run() +``` + +And if that is too verbose, this will be actively reduced with the +functionality provided by the Stateful bot implementation coming soon! + +## What does _hikari_ aim to do? + +- **Provide 100% documentation for the entire library.** Build your application + bottom-up or top-down with comprehensive documentation as standard. +- **Ensure all components are reusable.** Most people want a basic framework for + writing a bot, and _hikari_ will provide that. However, if you decide on a + bespoke solution using custom components, such as a _Redis_ state cache, or + a system where all events get put on a message queue, then _hikari_ provides + the conduit to make that happen. +- **Automate testing as much as possible.** You don't want to introduce bugs + into your bot with version updates, and neither do we. _hikari_ aims for 100% + test coverage as standard. This significantly reduces the amount of bugs and + broken features that appear in library releases -- something most Python + Discord libraries cannot provide any guarantee of. +- **Small improvements. Regularly.** Discord is known for pushing sudden changes + to their public APIs with little or no warning. When this happens, you want a + fix, and quickly. You do not want to wait for weeks for a usable solution to + be released. _hikari_ is developed using a fully automated CI pipeline with + extensive quality assurance. This enables bugfixes and new features to be + shipped within 30 minutes, not 30 days. -If you wish to explore the code in an online interactive viewer, you can use Sourcegraph on -[master](https://sourcegraph.com/gitlab.com/nekokatt/hikari@master) and [staging](https://sourcegraph.com/gitlab.com/nekokatt/hikari@staging) -too! +## What does _hikari_ currently support? + +### Library features + +_hikari_ has been designed with the best practises at heart to allow developers +to freely contribute and help the library grow. This is achieved in multiple +ways. + +- Modular, reusable components. +- Extensive documentation. +- Full type-checking. +- Minimal dependencies. +- Full unit test suite. -## What is this API? +### Network level components -A base Python Discord API framework for CPython 3.8 Designed for ease of use, -customization, and sane defaults. +The heart of any application that uses Discord is the network layer. _hikari_ +exposes all of these components with full documentation and with the ability to +reuse them in as many ways as you can think of. -This API is designed to provide the pure-python interface to the RESTful Discord API and the Gateway. This will provide -a set of basic models and abstractions that can be used to build a basic Discord bot in Python with asyncio in a -logical, idiomatic, Pythonic, concise way. +Most mainstream Python Discord APIs lack one or more of the following features. _hikari_ aims to +implement each feature as part of the design, rather than an additional component. This enables you +to utilize these components as a black box where necessary. -Other APIs may exist that are faster, use less memory, or are smaller. I wont dispute that. The aim of this library is -to provide a solid and consistent interface that can be __relied__ upon by the end user, and to provide regular updates -that are able to be opted into. +- Low level REST API implementation. +- Low level gateway websocket shard implementation. +- Rate limiting that complies with the `X-RateLimit-Bucket` header __properly__. +- Gateway websocket ratelimiting (prevents your websocket getting completely invalidated). +- Intents +- Proxy support for websockets and REST API. +- File IO that doesn't block you. -I also aim to provide as much automated test coverage as possible. I want to be able to immediately prove that a -function does what is expected of it to provide hard evidence that a build is not fundamentally broken before -deploying it. +### High level components -The final aim is for maintainability. This API attempts to be as documented and expandable as possible internally. If -something isn't right and you have some understanding of Python, hopefully you should be able to pick it up and tweak it -to solve your use case, rather than fighting inflexible internal abstractions that hide the information you need. +- Stateless, object-oriented bot API. Serve thousands of servers on little memory. +- Sensible, type-safe event dispatching system that is reactive to type annotations, and + supports [PEP-563](https://www.python.org/dev/peps/pep-0563/) without broken hacks and + bodges. +- Models that extend the format provided by Discord, not fight against it. Working as close + to the original format of information provided by Discord as possible ensures that minimal + changes are required when a breaking API design is introduced. This reduces the amount of + stuff you need to fix in your applications as a result. +- REST only API functionality. Want to write a web dashboard? Feel free to just reuse the + REST client components to achive that! + +### Stuff coming soon -## What is this API **not**? +- Optional, optimised C implementations of internals to give large applications a + well-deserved performance boost. +- Voice support. +- Stateful bot support (intents will be supported as first-class citizens). -This API is **not** for people using anything older than CPython 3.8. +### Planned extension modules for the future -It currently is **not** able to provide voice functionality. Again, this may be added in the future. +- Command framework (make commands and groups with the flick of a wrist). +- Optional dependency injection tools (declare what components you want in your application, and + where you want them. Let _hikari_ work out how to put it together!) +- Full voice transcoding support, natively in your application. Do not rely on invoking ffmpeg + in a subprocess ever again! -## FAQ -### Contributing to Hikari +## Getting started -[View the contributing guide!](https://gitlab.com/nekokatt/hikari/wikis/Contributing) +This section is still very bare, and we are still actively writing this framework every day. +[Why not pop in and say hi?](https://discord.gg/HMnGbsv) More comprehensive tutorials will be +provided soon! From da0e6a568a3e075e7e5c6c8587547c3c8238f82d Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Tue, 21 Apr 2020 10:44:31 +0100 Subject: [PATCH 179/922] Fixed incorrect refs in docstrings for Snowflake. --- hikari/applications.py | 10 +- hikari/audit_logs.py | 30 +++--- hikari/channels.py | 30 +++--- hikari/clients/rest/channel.py | 74 ++++++------- hikari/clients/rest/guild.py | 160 ++++++++++++++-------------- hikari/clients/rest/me.py | 8 +- hikari/clients/rest/react.py | 22 ++-- hikari/clients/rest/user.py | 2 +- hikari/clients/rest/webhook.py | 14 +-- hikari/emojis.py | 2 +- hikari/events.py | 118 ++++++++++---------- hikari/guilds.py | 40 +++---- hikari/internal/allowed_mentions.py | 4 +- hikari/messages.py | 18 ++-- hikari/voices.py | 6 +- hikari/webhooks.py | 4 +- 16 files changed, 271 insertions(+), 271 deletions(-) diff --git a/hikari/applications.py b/hikari/applications.py index e47ca63abd..30b2c411b9 100644 --- a/hikari/applications.py +++ b/hikari/applications.py @@ -156,7 +156,7 @@ class TeamMember(bases.HikariEntity, marshaller.Deserializable): #: The ID of the team this member belongs to. #: - #: :type: :obj:`~hikari.entities.Snowflake` + #: :type: :obj:`~hikari.bases.Snowflake` team_id: bases.Snowflake = marshaller.attrib(deserializer=bases.Snowflake.deserialize) #: The user object of this team member. @@ -177,14 +177,14 @@ class Team(bases.UniqueEntity, marshaller.Deserializable): #: The member's that belong to this team. #: - #: :type: :obj:`~typing.Mapping` [ :obj:`~hikari.entities.Snowflake`, :obj:`~TeamMember` ] + #: :type: :obj:`~typing.Mapping` [ :obj:`~hikari.bases.Snowflake`, :obj:`~TeamMember` ] members: typing.Mapping[bases.Snowflake, TeamMember] = marshaller.attrib( deserializer=lambda members: {m.user.id: m for m in map(TeamMember.deserialize, members)} ) #: The ID of this team's owner. #: - #: :type: :obj:`~hikari.entities.Snowflake` + #: :type: :obj:`~hikari.bases.Snowflake` owner_user_id: bases.Snowflake = marshaller.attrib(deserializer=bases.Snowflake.deserialize) @property @@ -310,14 +310,14 @@ class Application(bases.UniqueEntity, marshaller.Deserializable): #: The ID of the guild this application is linked to #: if it's sold on Discord. #: - #: :type: :obj:`~hikari.entities.Snowflake`, optional + #: :type: :obj:`~hikari.bases.Snowflake`, optional guild_id: typing.Optional[bases.Snowflake] = marshaller.attrib( deserializer=bases.Snowflake.deserialize, if_undefined=None, default=None ) #: The ID of the primary "Game SKU" of a game that's sold on Discord. #: - #: :type: :obj:`~hikari.entities.Snowflake`, optional + #: :type: :obj:`~hikari.bases.Snowflake`, optional primary_sku_id: typing.Optional[bases.Snowflake] = marshaller.attrib( deserializer=bases.Snowflake.deserialize, if_undefined=None, default=None ) diff --git a/hikari/audit_logs.py b/hikari/audit_logs.py index 61db78d69a..e484c40fb0 100644 --- a/hikari/audit_logs.py +++ b/hikari/audit_logs.py @@ -294,7 +294,7 @@ class ChannelOverwriteEntryInfo(BaseAuditLogEntryInfo): #: The ID of the overwrite being updated, added or removed (and the entity #: it targets). #: - #: :type: :obj:`~hikari.entities.Snowflake` + #: :type: :obj:`~hikari.bases.Snowflake` id: bases.Snowflake = marshaller.attrib(deserializer=bases.Snowflake.deserialize) #: The type of entity this overwrite targets. @@ -320,12 +320,12 @@ class MessagePinEntryInfo(BaseAuditLogEntryInfo): #: The ID of the guild text based channel where this pinned message is #: being added or removed. #: - #: :type: :obj:`~hikari.entities.Snowflake` + #: :type: :obj:`~hikari.bases.Snowflake` channel_id: bases.Snowflake = marshaller.attrib(deserializer=bases.Snowflake.deserialize) #: The ID of the message that's being pinned or unpinned. #: - #: :type: :obj:`~hikari.entities.Snowflake` + #: :type: :obj:`~hikari.bases.Snowflake` message_id: bases.Snowflake = marshaller.attrib(deserializer=bases.Snowflake.deserialize) @@ -369,7 +369,7 @@ class MessageDeleteEntryInfo(MessageBulkDeleteEntryInfo): #: The guild text based channel where these message(s) were deleted. #: - #: :type: :obj:`~hikari.entities.Snowflake` + #: :type: :obj:`~hikari.bases.Snowflake` channel_id: bases.Snowflake = marshaller.attrib(deserializer=bases.Snowflake.deserialize) @@ -393,7 +393,7 @@ class MemberMoveEntryInfo(MemberDisconnectEntryInfo): #: The channel these member(s) were moved to. #: - #: :type: :obj:`~hikari.entities.Snowflake` + #: :type: :obj:`~hikari.bases.Snowflake` channel_id: bases.Snowflake = marshaller.attrib(deserializer=bases.Snowflake.deserialize) @@ -434,7 +434,7 @@ class AuditLogEntry(bases.UniqueEntity, marshaller.Deserializable): #: The ID of the entity affected by this change, if applicable. #: - #: :type: :obj:`~hikari.entities.Snowflake`, optional + #: :type: :obj:`~hikari.bases.Snowflake`, optional target_id: typing.Optional[bases.Snowflake] = marshaller.attrib() #: A sequence of the changes made to :attr:`target_id` @@ -444,12 +444,12 @@ class AuditLogEntry(bases.UniqueEntity, marshaller.Deserializable): #: The ID of the user who made this change. #: - #: :type: :obj:`~hikari.entities.Snowflake` + #: :type: :obj:`~hikari.bases.Snowflake` user_id: bases.Snowflake = marshaller.attrib() #: The ID of this entry. #: - #: :type: :obj:`~hikari.entities.Snowflake` + #: :type: :obj:`~hikari.bases.Snowflake` id: bases.Snowflake = marshaller.attrib() #: The type of action this entry represents. @@ -501,7 +501,7 @@ class AuditLog(bases.HikariEntity, marshaller.Deserializable): #: A sequence of the audit log's entries. #: - #: :type: :obj:`~typing.Mapping` [ :obj:`~hikari.entities.Snowflake`, :obj:`~AuditLogEntry` ] + #: :type: :obj:`~typing.Mapping` [ :obj:`~hikari.bases.Snowflake`, :obj:`~AuditLogEntry` ] entries: typing.Mapping[bases.Snowflake, AuditLogEntry] = marshaller.attrib( raw_name="audit_log_entries", deserializer=lambda payload: {entry.id: entry for entry in map(AuditLogEntry.deserialize, payload)}, @@ -509,7 +509,7 @@ class AuditLog(bases.HikariEntity, marshaller.Deserializable): #: A mapping of the partial objects of integrations found in this audit log. #: - #: :type: :obj:`~typing.Mapping` [ :obj:`~hikari.entities.Snowflake`, :obj:`~hikari.guilds.GuildIntegration` ] + #: :type: :obj:`~typing.Mapping` [ :obj:`~hikari.bases.Snowflake`, :obj:`~hikari.guilds.GuildIntegration` ] integrations: typing.Mapping[bases.Snowflake, guilds.GuildIntegration] = marshaller.attrib( deserializer=lambda payload: { integration.id: integration for integration in map(guilds.PartialGuildIntegration.deserialize, payload) @@ -518,14 +518,14 @@ class AuditLog(bases.HikariEntity, marshaller.Deserializable): #: A mapping of the objects of users found in this audit log. #: - #: :type: :obj:`~typing.Mapping` [ :obj:`~hikari.entities.Snowflake`, :obj:`~hikari.users.User` ] + #: :type: :obj:`~typing.Mapping` [ :obj:`~hikari.bases.Snowflake`, :obj:`~hikari.users.User` ] users: typing.Mapping[bases.Snowflake, _users.User] = marshaller.attrib( deserializer=lambda payload: {user.id: user for user in map(_users.User.deserialize, payload)} ) #: A mapping of the objects of webhooks found in this audit log. #: - #: :type: :obj:`~typing.Mapping` [ :obj:`~hikari.entities.Snowflake`, :obj:`~hikari.webhooks.Webhook` ] + #: :type: :obj:`~typing.Mapping` [ :obj:`~hikari.bases.Snowflake`, :obj:`~hikari.webhooks.Webhook` ] webhooks: typing.Mapping[bases.Snowflake, _webhooks.Webhook] = marshaller.attrib( deserializer=lambda payload: {webhook.id: webhook for webhook in map(_webhooks.Webhook.deserialize, payload)} ) @@ -577,17 +577,17 @@ class AuditLogIterator(typing.AsyncIterator[AuditLogEntry]): #: A mapping of the partial objects of integrations found in this audit log #: so far. #: - #: :type: :obj:`~typing.Mapping` [ :obj:`~hikari.entities.Snowflake`, :obj:`~hikari.guilds.GuildIntegration` ] + #: :type: :obj:`~typing.Mapping` [ :obj:`~hikari.bases.Snowflake`, :obj:`~hikari.guilds.GuildIntegration` ] integrations: typing.Mapping[bases.Snowflake, guilds.GuildIntegration] #: A mapping of the objects of users found in this audit log so far. #: - #: :type: :obj:`~typing.Mapping` [ :obj:`~hikari.entities.Snowflake`, :obj:`~hikari.users.User` ] + #: :type: :obj:`~typing.Mapping` [ :obj:`~hikari.bases.Snowflake`, :obj:`~hikari.users.User` ] users: typing.Mapping[bases.Snowflake, _users.User] #: A mapping of the objects of webhooks found in this audit log so far. #: - #: :type: :obj:`~typing.Mapping` [ :obj:`~hikari.entities.Snowflake`, :obj:`~hikari.webhooks.Webhook` ] + #: :type: :obj:`~typing.Mapping` [ :obj:`~hikari.bases.Snowflake`, :obj:`~hikari.webhooks.Webhook` ] webhooks: typing.Mapping[bases.Snowflake, _webhooks.Webhook] def __init__( diff --git a/hikari/channels.py b/hikari/channels.py index 48f25eb9bc..df517a3c19 100644 --- a/hikari/channels.py +++ b/hikari/channels.py @@ -21,10 +21,10 @@ .. inheritance-diagram:: hikari.channels enum.IntEnum - hikari.entities.HikariEntity - hikari.entities.Deserializable - hikari.entities.Serializable - hikari.entities.UniqueEntity + hikari.bases.HikariEntity + hikari.bases.Deserializable + hikari.bases.Serializable + hikari.bases.UniqueEntity :parts: 1 """ @@ -191,12 +191,12 @@ class DMChannel(Channel): #: This might point to an invalid or deleted message. #: #: - #: :type: :obj:`~hikari.entities.Snowflake`, optional + #: :type: :obj:`~hikari.bases.Snowflake`, optional last_message_id: bases.Snowflake = marshaller.attrib(deserializer=bases.Snowflake.deserialize, if_none=None) #: The recipients of the DM. #: - #: :type: :obj:`~typing.Mapping` [ :obj:`~hikari.entities.Snowflake`, :obj:`~hikari.users.User` ] + #: :type: :obj:`~typing.Mapping` [ :obj:`~hikari.bases.Snowflake`, :obj:`~hikari.users.User` ] recipients: typing.Mapping[bases.Snowflake, users.User] = marshaller.attrib( deserializer=lambda recipients: {user.id: user for user in map(users.User.deserialize, recipients)} ) @@ -215,7 +215,7 @@ class GroupDMChannel(DMChannel): #: The ID of the owner of the group. #: - #: :type: :obj:`~hikari.entities.Snowflake` + #: :type: :obj:`~hikari.bases.Snowflake` owner_id: bases.Snowflake = marshaller.attrib(deserializer=bases.Snowflake.deserialize) #: The hash of the icon of the group. @@ -226,7 +226,7 @@ class GroupDMChannel(DMChannel): #: The ID of the application that created the group DM, if it's a #: bot based group DM. #: - #: :type: :obj:`~hikari.entities.Snowflake`, optional + #: :type: :obj:`~hikari.bases.Snowflake`, optional application_id: typing.Optional[bases.Snowflake] = marshaller.attrib( deserializer=bases.Snowflake.deserialize, if_undefined=None, default=None ) @@ -239,7 +239,7 @@ class GuildChannel(Channel): #: The ID of the guild the channel belongs to. #: - #: :type: :obj:`~hikari.entities.Snowflake` + #: :type: :obj:`~hikari.bases.Snowflake` guild_id: bases.Snowflake = marshaller.attrib(deserializer=bases.Snowflake.deserialize) #: The sorting position of the channel. @@ -249,7 +249,7 @@ class GuildChannel(Channel): #: The permission overwrites for the channel. #: - #: :type: :obj:`~typing.Mapping` [ :obj:`~hikari.entities.Snowflake`, :obj:`~PermissionOverwrite` ] + #: :type: :obj:`~typing.Mapping` [ :obj:`~hikari.bases.Snowflake`, :obj:`~PermissionOverwrite` ] permission_overwrites: PermissionOverwrite = marshaller.attrib( deserializer=lambda overwrites: {o.id: o for o in map(PermissionOverwrite.deserialize, overwrites)} ) @@ -266,7 +266,7 @@ class GuildChannel(Channel): #: The ID of the parent category the channel belongs to. #: - #: :type: :obj:`~hikari.entities.Snowflake`, optional + #: :type: :obj:`~hikari.bases.Snowflake`, optional parent_id: bases.Snowflake = marshaller.attrib(deserializer=bases.Snowflake.deserialize, if_none=None) @@ -295,7 +295,7 @@ class GuildTextChannel(GuildChannel): #: This might point to an invalid or deleted message. #: #: - #: :type: :obj:`~hikari.entities.Snowflake`, optional + #: :type: :obj:`~hikari.bases.Snowflake`, optional last_message_id: bases.Snowflake = marshaller.attrib(deserializer=bases.Snowflake.deserialize, if_none=None) #: The delay (in seconds) between a user can send a message @@ -330,7 +330,7 @@ class GuildNewsChannel(GuildChannel): #: This might point to an invalid or deleted message. #: #: - #: :type: :obj:`~hikari.entities.Snowflake`, optional + #: :type: :obj:`~hikari.bases.Snowflake`, optional last_message_id: bases.Snowflake = marshaller.attrib(deserializer=bases.Snowflake.deserialize, if_none=None) @@ -472,7 +472,7 @@ def with_parent_category(self, category: typing.Union[bases.Snowflake, int]) -> Parameters ---------- - category : :obj:`~typing.Union` [ :obj:`~hikari.entities.Snowflake`, :obj:`~int` ] + category : :obj:`~typing.Union` [ :obj:`~hikari.bases.Snowflake`, :obj:`~int` ] The placeholder ID of the category channel that should be this channel's parent. """ @@ -489,7 +489,7 @@ def with_id(self, channel_id: typing.Union[bases.Snowflake, int]) -> "GuildChann Parameters ---------- - channel_id : :obj:`~typing.Union` [ :obj:`~hikari.entities.Snowflake`, :obj:`~int` ] + channel_id : :obj:`~typing.Union` [ :obj:`~hikari.bases.Snowflake`, :obj:`~int` ] The placeholder ID to use. """ self._payload["id"] = str(int(channel_id)) diff --git a/hikari/clients/rest/channel.py b/hikari/clients/rest/channel.py index e6396ae0c4..b03380566b 100644 --- a/hikari/clients/rest/channel.py +++ b/hikari/clients/rest/channel.py @@ -51,7 +51,7 @@ async def fetch_channel(self, channel: bases.Hashable[_channels.Channel]) -> _ch Parameters ---------- - channel : :obj:`~typing.Union` [ :obj:`~hikari.channels.Channel`, :obj:`~hikari.entities.Snowflake`, :obj:`~int` ] + channel : :obj:`~typing.Union` [ :obj:`~hikari.channels.Channel`, :obj:`~hikari.bases.Snowflake`, :obj:`~int` ] The object ID of the channel to look up. Returns @@ -93,7 +93,7 @@ async def update_channel( Parameters ---------- - channel : :obj:`~typing.Union` [ :obj:`~hikari.channels.Channel`, :obj:`~hikari.entities.Snowflake`, :obj:`~int` ] + channel : :obj:`~typing.Union` [ :obj:`~hikari.channels.Channel`, :obj:`~hikari.bases.Snowflake`, :obj:`~int` ] The channel ID to update. name : :obj:`~str` If specified, the new name for the channel. This must be @@ -125,7 +125,7 @@ async def update_channel( permission_overwrites : :obj:`~typing.Sequence` [ :obj:`~hikari.channels.PermissionOverwrite` ] If specified, the new list of permission overwrites that are category specific to replace the existing overwrites with. - parent_category : :obj:`~typing.Union` [ :obj:`~hikari.channels.Channel`, :obj:`~hikari.entities.Snowflake`, :obj:`~int` ], optional + parent_category : :obj:`~typing.Union` [ :obj:`~hikari.channels.Channel`, :obj:`~hikari.bases.Snowflake`, :obj:`~int` ], optional If specified, the new parent category ID to set for the channel, pass :obj:`~None` to unset. reason : :obj:`~str` @@ -179,7 +179,7 @@ async def delete_channel(self, channel: bases.Hashable[_channels.Channel]) -> No Parameters ---------- - channel : :obj:`~typing.Union` [ :obj:`~hikari.channels.Channel`, :obj:`~hikari.entities.Snowflake` :obj:`~str` ] + channel : :obj:`~typing.Union` [ :obj:`~hikari.channels.Channel`, :obj:`~hikari.bases.Snowflake` :obj:`~str` ] The object or ID of the channel to delete. Returns @@ -227,12 +227,12 @@ def fetch_messages_after( Parameters ---------- - channel : :obj:`~typing.Union` [ :obj:`~hikari.channels.Channel`, :obj:`~hikari.entities.Snowflake`, :obj:`~int` ] + channel : :obj:`~typing.Union` [ :obj:`~hikari.channels.Channel`, :obj:`~hikari.bases.Snowflake`, :obj:`~int` ] The ID of the channel to retrieve the messages from. limit : :obj:`~int` If specified, the maximum number of how many messages this iterator should return. - after : :obj:`~typing.Union` [ :obj:`~datetime.datetime`, :obj:`~hikari.channels.Channel`, :obj:`~hikari.entities.Snowflake`, :obj:`~int` ] + after : :obj:`~typing.Union` [ :obj:`~datetime.datetime`, :obj:`~hikari.channels.Channel`, :obj:`~hikari.bases.Snowflake`, :obj:`~int` ] A object or ID message. Only return messages sent AFTER this message if it's specified else this will return every message after (and including) the first message in the channel. @@ -296,12 +296,12 @@ def fetch_messages_before( Parameters ---------- - channel : :obj:`~typing.Union` [ :obj:`~hikari.channels.Channel`, :obj:`~hikari.entities.Snowflake`, :obj:`~int` ] + channel : :obj:`~typing.Union` [ :obj:`~hikari.channels.Channel`, :obj:`~hikari.bases.Snowflake`, :obj:`~int` ] The ID of the channel to retrieve the messages from. limit : :obj:`~int` If specified, the maximum number of how many messages this iterator should return. - before : :obj:`~typing.Union` [ :obj:`~datetime.datetime`, :obj:`~hikari.channels.Channel`, :obj:`~hikari.entities.Snowflake`, :obj:`~int` ] + before : :obj:`~typing.Union` [ :obj:`~datetime.datetime`, :obj:`~hikari.channels.Channel`, :obj:`~hikari.bases.Snowflake`, :obj:`~int` ] A message object or ID. Only return messages sent BEFORE this message if this is specified else this will return every message before (and including) the most recent message in the @@ -367,9 +367,9 @@ async def fetch_messages_around( Parameters ---------- - channel : :obj:`~typing.Union` [ :obj:`~hikari.channels.Channel`, :obj:`~hikari.entities.Snowflake`, :obj:`~int` ] + channel : :obj:`~typing.Union` [ :obj:`~hikari.channels.Channel`, :obj:`~hikari.bases.Snowflake`, :obj:`~int` ] The ID of the channel to retrieve the messages from. - around : :obj:`~typing.Union` [ :obj:`~datetime.datetime`, :obj:`~hikari.channels.Channel`, :obj:`~hikari.entities.Snowflake`, :obj:`~int` ] + around : :obj:`~typing.Union` [ :obj:`~datetime.datetime`, :obj:`~hikari.channels.Channel`, :obj:`~hikari.bases.Snowflake`, :obj:`~int` ] The object or ID of the message to get messages that were sent AROUND it in the provided channel, unlike ``before`` and ``after``, this argument is required and the provided message will also be @@ -427,9 +427,9 @@ async def fetch_message( Parameters ---------- - channel : :obj:`~typing.Union` [ :obj:`~hikari.channels.Channel`, :obj:`~hikari.entities.Snowflake`, :obj:`~int` ] + channel : :obj:`~typing.Union` [ :obj:`~hikari.channels.Channel`, :obj:`~hikari.bases.Snowflake`, :obj:`~int` ] The object or ID of the channel to get the message from. - message : :obj:`~typing.Union` [ :obj:`~hikari.messages.Message`, :obj:`~hikari.entities.Snowflake`, :obj:`~int` ] + message : :obj:`~typing.Union` [ :obj:`~hikari.messages.Message`, :obj:`~hikari.bases.Snowflake`, :obj:`~int` ] The object or ID of the message to retrieve. Returns @@ -474,7 +474,7 @@ async def create_message( Parameters ---------- - channel : :obj:`~typing.Union` [ :obj:`~hikari.channels.Channel`, :obj:`~hikari.entities.Snowflake`, :obj:`~int` ] + channel : :obj:`~typing.Union` [ :obj:`~hikari.channels.Channel`, :obj:`~hikari.bases.Snowflake`, :obj:`~int` ] The channel or ID of the channel to send to. content : :obj:`~str` If specified, the message content to send with the message. @@ -492,11 +492,11 @@ async def create_message( mentions_everyone : :obj:`~bool` Whether ``@everyone`` and ``@here`` mentions should be resolved by discord and lead to actual pings, defaults to :obj:`~True`. - user_mentions : :obj:`~typing.Union` [ :obj:`~typing.Collection` [ :obj:`~typing.Union` [ :obj:`~hikari.users.User`, :obj:`~hikari.entities.Snowflake`, :obj:`~int` ], :obj:`~bool` ] + user_mentions : :obj:`~typing.Union` [ :obj:`~typing.Collection` [ :obj:`~typing.Union` [ :obj:`~hikari.users.User`, :obj:`~hikari.bases.Snowflake`, :obj:`~int` ], :obj:`~bool` ] Either an array of user objects/IDs to allow mentions for, :obj:`~True` to allow all user mentions or :obj:`~False` to block all user mentions from resolving, defaults to :obj:`~True`. - role_mentions : :obj:`~typing.Union` [ :obj:`~typing.Collection` [ :obj:`~typing.Union` [ :obj:`~hikari.guilds.GuildRole`, :obj:`~hikari.entities.Snowflake`, :obj:`~int` ] ], :obj:`~bool` ] + role_mentions : :obj:`~typing.Union` [ :obj:`~typing.Collection` [ :obj:`~typing.Union` [ :obj:`~hikari.guilds.GuildRole`, :obj:`~hikari.bases.Snowflake`, :obj:`~int` ] ], :obj:`~bool` ] Either an array of guild role objects/IDs to allow mentions for, :obj:`~True` to allow all role mentions or :obj:`~False` to block all role mentions from resolving, defaults to :obj:`~True`. @@ -583,9 +583,9 @@ async def update_message( Parameters ---------- - channel : :obj:`~typing.Union` [ :obj:`~hikari.channels.Channel`, :obj:`~hikari.entities.Snowflake`, :obj:`~int` ] + channel : :obj:`~typing.Union` [ :obj:`~hikari.channels.Channel`, :obj:`~hikari.bases.Snowflake`, :obj:`~int` ] The object or ID of the channel to get the message from. - message : :obj:`~typing.Union` [ :obj:`~hikari.messages.Message`, :obj:`~hikari.entities.Snowflake`, :obj:`~int` ] + message : :obj:`~typing.Union` [ :obj:`~hikari.messages.Message`, :obj:`~hikari.bases.Snowflake`, :obj:`~int` ] The object or ID of the message to edit. content : :obj:`~str`, optional If specified, the string content to replace with in the message. @@ -600,11 +600,11 @@ async def update_message( mentions_everyone : :obj:`~bool` Whether ``@everyone`` and ``@here`` mentions should be resolved by discord and lead to actual pings, defaults to :obj:`~True`. - user_mentions : :obj:`~typing.Union` [ :obj:`~typing.Collection` [ :obj:`~typing.Union` [ :obj:`~hikari.users.User`, :obj:`~hikari.entities.Snowflake`, :obj:`~int` ], :obj:`~bool` ] + user_mentions : :obj:`~typing.Union` [ :obj:`~typing.Collection` [ :obj:`~typing.Union` [ :obj:`~hikari.users.User`, :obj:`~hikari.bases.Snowflake`, :obj:`~int` ], :obj:`~bool` ] Either an array of user objects/IDs to allow mentions for, :obj:`~True` to allow all user mentions or :obj:`~False` to block all user mentions from resolving, defaults to :obj:`~True`. - role_mentions : :obj:`~typing.Union` [ :obj:`~typing.Collection` [ :obj:`~typing.Union` [ :obj:`~hikari.guilds.GuildRole`, :obj:`~hikari.entities.Snowflake`, :obj:`~int` ] ], :obj:`~bool` ] + role_mentions : :obj:`~typing.Union` [ :obj:`~typing.Collection` [ :obj:`~typing.Union` [ :obj:`~hikari.guilds.GuildRole`, :obj:`~hikari.bases.Snowflake`, :obj:`~int` ] ], :obj:`~bool` ] Either an array of guild role objects/IDs to allow mentions for, :obj:`~True` to allow all role mentions or :obj:`~False` to block all role mentions from resolving, defaults to :obj:`~True`. @@ -685,11 +685,11 @@ async def delete_messages( Parameters ---------- - channel : :obj:`~typing.Union` [ :obj:`~hikari.channels.Channel`, :obj:`~hikari.entities.Snowflake`, :obj:`~int` ] + channel : :obj:`~typing.Union` [ :obj:`~hikari.channels.Channel`, :obj:`~hikari.bases.Snowflake`, :obj:`~int` ] The object or ID of the channel to get the message from. - message : :obj:`~typing.Union` [ :obj:`~hikari.messages.Message`, :obj:`~hikari.entities.Snowflake`, :obj:`~int` ] + message : :obj:`~typing.Union` [ :obj:`~hikari.messages.Message`, :obj:`~hikari.bases.Snowflake`, :obj:`~int` ] The object or ID of the message to delete. - *additional_messages : :obj:`~typing.Union` [ :obj:`~hikari.messages.Message`, :obj:`~hikari.entities.Snowflake`, :obj:`~int` ] + *additional_messages : :obj:`~typing.Union` [ :obj:`~hikari.messages.Message`, :obj:`~hikari.bases.Snowflake`, :obj:`~int` ] Objects and/or IDs of additional messages to delete in the same channel, in total you can delete up to 100 messages in a request. @@ -752,9 +752,9 @@ async def update_channel_overwrite( Parameters ---------- - channel : :obj:`~typing.Union` [ :obj:`~hikari.messages.Message`, :obj:`~hikari.entities.Snowflake`, :obj:`~int` ] + channel : :obj:`~typing.Union` [ :obj:`~hikari.messages.Message`, :obj:`~hikari.bases.Snowflake`, :obj:`~int` ] The object or ID of the channel to edit permissions for. - overwrite : :obj:`~typing.Union` [ :obj:`~hikari.channels.PermissionOverwrite`, :obj:`~hikari.guilds.GuildRole`, :obj:`~hikari.users.User`, :obj:`~hikari.entities.Snowflake` , :obj:`~int` ] + overwrite : :obj:`~typing.Union` [ :obj:`~hikari.channels.PermissionOverwrite`, :obj:`~hikari.guilds.GuildRole`, :obj:`~hikari.users.User`, :obj:`~hikari.bases.Snowflake` , :obj:`~int` ] The object or ID of the target member or role to edit/create the overwrite for. target_type : :obj:`~typing.Union` [ :obj:`~hikari.channels.PermissionOverwriteType`, :obj:`~int` ] @@ -796,7 +796,7 @@ async def fetch_invites_for_channel( Parameters ---------- - channel : :obj:`~typing.Union` [ :obj:`~hikari.channels.Channel`, :obj:`~hikari.entities.Snowflake`, :obj:`~int` ] + channel : :obj:`~typing.Union` [ :obj:`~hikari.channels.Channel`, :obj:`~hikari.bases.Snowflake`, :obj:`~int` ] The object or ID of the channel to get invites for. Returns @@ -849,7 +849,7 @@ async def create_invite_for_channel( user is kicked when their session ends unless they are given a role. unique : :obj:`~bool` If specified, whether to try to reuse a similar invite. - target_user : :obj:`~typing.Union` [ :obj:`~hikari.users.User`, :obj:`~hikari.entities.Snowflake`, :obj:`~int` ] + target_user : :obj:`~typing.Union` [ :obj:`~hikari.users.User`, :obj:`~hikari.bases.Snowflake`, :obj:`~int` ] If specified, the object or ID of the user this invite should target. target_user_type : :obj:`~typing.Union` [ :obj:`~hikari.invites.TargetUserType`, :obj:`~int` ] @@ -900,9 +900,9 @@ async def delete_channel_overwrite( Parameters ---------- - channel : :obj:`~typing.Union` [ :obj:`~hikari.channels.Channel`, :obj:`~hikari.entities.Snowflake`, :obj:`~int` ] + channel : :obj:`~typing.Union` [ :obj:`~hikari.channels.Channel`, :obj:`~hikari.bases.Snowflake`, :obj:`~int` ] The object or ID of the channel to delete the overwrite from. - overwrite : :obj:`~typing.Union` [ :obj:`~hikari.channels.PermissionOverwrite`, :obj:`~hikari.guilds.GuildRole`, :obj:`~hikari.users.User`, :obj:`~hikari.entities.Snowflake`, :obj:int ] + overwrite : :obj:`~typing.Union` [ :obj:`~hikari.channels.PermissionOverwrite`, :obj:`~hikari.guilds.GuildRole`, :obj:`~hikari.users.User`, :obj:`~hikari.bases.Snowflake`, :obj:int ] The ID of the entity this overwrite targets. Raises @@ -925,7 +925,7 @@ async def trigger_typing(self, channel: bases.Hashable[_channels.Channel]) -> No Parameters ---------- - channel : :obj:`~typing.Union` [ :obj:`~hikari.channels.Channel`, :obj:`~hikari.entities.Snowflake`, :obj:`~int` ] + channel : :obj:`~typing.Union` [ :obj:`~hikari.channels.Channel`, :obj:`~hikari.bases.Snowflake`, :obj:`~int` ] The object or ID of the channel to appear to be typing in. Raises @@ -949,12 +949,12 @@ async def fetch_pins( Parameters ---------- - channel : :obj:`~typing.Union` [ :obj:`~hikari.channels.Channel`, :obj:`~hikari.entities.Snowflake`, :obj:`~int` ] + channel : :obj:`~typing.Union` [ :obj:`~hikari.channels.Channel`, :obj:`~hikari.bases.Snowflake`, :obj:`~int` ] The object or ID of the channel to get messages from. Returns ------- - :obj:`~typing.Mapping` [ :obj:`~hikari.entities.Snowflake`, :obj:`~hikari.messages.Message` ] + :obj:`~typing.Mapping` [ :obj:`~hikari.bases.Snowflake`, :obj:`~hikari.messages.Message` ] A list of message objects. Raises @@ -985,9 +985,9 @@ async def pin_message( Parameters ---------- - channel : :obj:`~typing.Union` [ :obj:`~hikari.channels.Channel`, :obj:`~hikari.entities.Snowflake`, :obj:`~int` ] + channel : :obj:`~typing.Union` [ :obj:`~hikari.channels.Channel`, :obj:`~hikari.bases.Snowflake`, :obj:`~int` ] The object or ID of the channel to pin a message to. - message : :obj:`~typing.Union` [ :obj:`~hikari.messages.Message`, :obj:`~hikari.entities.Snowflake`, :obj:`~int` ] + message : :obj:`~typing.Union` [ :obj:`~hikari.messages.Message`, :obj:`~hikari.bases.Snowflake`, :obj:`~int` ] The object or ID of the message to pin. Raises @@ -1014,9 +1014,9 @@ async def unpin_message( Parameters ---------- - channel : :obj:`~typing.Union` [ :obj:`~hikari.channels.Channel`, :obj:`~hikari.entities.Snowflake`, :obj:`~int` ] + channel : :obj:`~typing.Union` [ :obj:`~hikari.channels.Channel`, :obj:`~hikari.bases.Snowflake`, :obj:`~int` ] The ID of the channel to remove a pin from. - message : :obj:`~typing.Union` [ :obj:`~hikari.messages.Message`, :obj:`~hikari.entities.Snowflake`, :obj:`~int` ] + message : :obj:`~typing.Union` [ :obj:`~hikari.messages.Message`, :obj:`~hikari.bases.Snowflake`, :obj:`~int` ] The object or ID of the message to unpin. Raises @@ -1046,7 +1046,7 @@ async def create_webhook( Parameters ---------- - channel : :obj:`~typing.Union` [ :obj:`~hikari.channels.GuildChannel`, :obj:`~hikari.entities.Snowflake`, :obj:`~int` ] + channel : :obj:`~typing.Union` [ :obj:`~hikari.channels.GuildChannel`, :obj:`~hikari.bases.Snowflake`, :obj:`~int` ] The object or ID of the channel for webhook to be created in. name : :obj:`~str` The webhook's name string. @@ -1088,7 +1088,7 @@ async def fetch_channel_webhooks( Parameters ---------- - channel : :obj:`~typing.Union` [ :obj:`~hikari.channels.GuildChannel`, :obj:`~hikari.entities.Snowflake`, :obj:`~int` ] + channel : :obj:`~typing.Union` [ :obj:`~hikari.channels.GuildChannel`, :obj:`~hikari.bases.Snowflake`, :obj:`~int` ] The object or ID of the guild channel to get the webhooks from. Returns diff --git a/hikari/clients/rest/guild.py b/hikari/clients/rest/guild.py index ab79d4a91b..73b6262d0a 100644 --- a/hikari/clients/rest/guild.py +++ b/hikari/clients/rest/guild.py @@ -60,9 +60,9 @@ async def fetch_audit_log( Parameters ---------- - guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.entities.Snowflake`, :obj:`~int` ] + guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.bases.Snowflake`, :obj:`~int` ] The object or ID of the guild to get the audit logs for. - user : :obj:`~typing.Union` [ :obj:`~hikari.users.User`, :obj:`~hikari.entities.Snowflake`, :obj:`~int` ] + user : :obj:`~typing.Union` [ :obj:`~hikari.users.User`, :obj:`~hikari.bases.Snowflake`, :obj:`~int` ] If specified, the object or ID of the user to filter by. action_type : :obj:`~typing.Union` [ :obj:`~hikari.audit_logs.AuditLogEventType`, :obj:`~int` ] If specified, the action type to look up. Passing a raw integer @@ -70,7 +70,7 @@ async def fetch_audit_log( limit : :obj:`~int` If specified, the limit to apply to the number of records. Defaults to ``50``. Must be between ``1`` and ``100`` inclusive. - before : :obj:`~typing.Union` [ :obj:`~datetime.datetime`, :obj:`~hikari.audit_logs.AuditLogEntry`, :obj:`~hikari.entities.Snowflake`, :obj:`~int` ] + before : :obj:`~typing.Union` [ :obj:`~datetime.datetime`, :obj:`~hikari.audit_logs.AuditLogEntry`, :obj:`~hikari.bases.Snowflake`, :obj:`~int` ] If specified, the object or ID of the entry that all retrieved entries should have occurred befor. @@ -118,13 +118,13 @@ def fetch_audit_log_entries_before( Parameters ---------- - guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.entities.Snowflake`, :obj:`~int` ] + guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.bases.Snowflake`, :obj:`~int` ] The ID or object of the guild to get audit log entries for - before : :obj:`~typing.Union` [ :obj:`~datetime.datetime`, :obj:`~hikari.audit_logs.AuditLogEntry`, :obj:`~hikari.entities.Snowflake`, :obj:`~int` ], optional + before : :obj:`~typing.Union` [ :obj:`~datetime.datetime`, :obj:`~hikari.audit_logs.AuditLogEntry`, :obj:`~hikari.bases.Snowflake`, :obj:`~int` ], optional If specified, the ID or object of the entry or datetime to get entries that happened before otherwise this will start from the newest entry. - user : :obj:`~typing.Union` [ :obj:`~hikari.users.User`, :obj:`~hikari.entities.Snowflake`, :obj:`~int` ] + user : :obj:`~typing.Union` [ :obj:`~hikari.users.User`, :obj:`~hikari.bases.Snowflake`, :obj:`~int` ] If specified, the object or ID of the user to filter by. action_type : :obj:`~typing.Union` [ :obj:`~hikari.audit_logs.AuditLogEventType`, :obj:`~int` ] If specified, the action type to look up. Passing a raw integer @@ -179,9 +179,9 @@ async def fetch_guild_emoji( Parameters ---------- - guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.entities.Snowflake`, :obj:`~int` ] + guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.bases.Snowflake`, :obj:`~int` ] The object or ID of the guild to get the emoji from. - emoji : :obj:`~typing.Union` [ :obj:`~hikari.emojis.GuildEmoji`, :obj:`~hikari.entities.Snowflake`, :obj:`~int` ] + emoji : :obj:`~typing.Union` [ :obj:`~hikari.emojis.GuildEmoji`, :obj:`~hikari.bases.Snowflake`, :obj:`~int` ] The object or ID of the emoji to get. Returns @@ -210,7 +210,7 @@ async def fetch_guild_emojis(self, guild: bases.Hashable[guilds.Guild]) -> typin Parameters ---------- - guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.entities.Snowflake`, :obj:`~int` ] + guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.bases.Snowflake`, :obj:`~int` ] The object or ID of the guild to get the emojis for. Returns @@ -246,13 +246,13 @@ async def create_guild_emoji( Parameters ---------- - guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.GuildRole`, :obj:`~hikari.entities.Snowflake`, :obj:`~int` ] + guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.GuildRole`, :obj:`~hikari.bases.Snowflake`, :obj:`~int` ] The object or ID of the guild to create the emoji in. name : :obj:`~str` The new emoji's name. image_data : ``hikari.internal.conversions.FileLikeT`` The ``128x128`` image data. - roles : :obj:`~typing.Sequence` [ :obj:`~typing.Union` [ :obj:`~hikari.guilds.GuildRole`, :obj:`~hikari.entities.Snowflake`, :obj:`~int` ] ] + roles : :obj:`~typing.Sequence` [ :obj:`~typing.Union` [ :obj:`~hikari.guilds.GuildRole`, :obj:`~hikari.bases.Snowflake`, :obj:`~int` ] ] If specified, a list of role objects or IDs for which the emoji will be whitelisted. If empty, all roles are whitelisted. reason : :obj:`~str` @@ -303,14 +303,14 @@ async def update_guild_emoji( Parameters ---------- - guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.entities.Snowflake`, :obj:`~int` ] + guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.bases.Snowflake`, :obj:`~int` ] The object or ID of the guild to which the emoji to edit belongs to. - emoji : :obj:`~typing.Union` [ :obj:`~hikari.emojis.GuildEmoji`, :obj:`~hikari.entities.Snowflake`, :obj:`~int` ] + emoji : :obj:`~typing.Union` [ :obj:`~hikari.emojis.GuildEmoji`, :obj:`~hikari.bases.Snowflake`, :obj:`~int` ] The object or ID of the emoji to edit. name : :obj:`~str` If specified, a new emoji name string. Keep unspecified to leave the name unchanged. - roles : :obj:`~typing.Sequence` [ :obj:`~typing.Union` [ :obj:`~hikari.guilds.GuildRole`, :obj:`~hikari.entities.Snowflake`, :obj:`~int` ] ] + roles : :obj:`~typing.Sequence` [ :obj:`~typing.Union` [ :obj:`~hikari.guilds.GuildRole`, :obj:`~hikari.bases.Snowflake`, :obj:`~int` ] ] If specified, a list of objects or IDs for the new whitelisted roles. Set to an empty list to whitelist all roles. Keep unspecified to leave the same roles already set. @@ -352,9 +352,9 @@ async def delete_guild_emoji( Parameters ---------- - guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.entities.Snowflake`, :obj:`~int` ] + guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.bases.Snowflake`, :obj:`~int` ] The object or ID of the guild to delete the emoji from. - emoji : :obj:`~typing.Union` [ :obj:`~hikari.emojis.GuildEmoji`, :obj:`~hikari.entities.Snowflake`, :obj:`~int` ] + emoji : :obj:`~typing.Union` [ :obj:`~hikari.emojis.GuildEmoji`, :obj:`~hikari.bases.Snowflake`, :obj:`~int` ] The object or ID of the guild emoji to be deleted. Raises @@ -449,7 +449,7 @@ async def fetch_guild(self, guild: bases.Hashable[guilds.Guild]) -> guilds.Guild Parameters ---------- - guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.entities.Snowflake`, :obj:`~int` ] + guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.bases.Snowflake`, :obj:`~int` ] The object or ID of the guild to get. Returns @@ -477,7 +477,7 @@ async def fetch_guild_preview(self, guild: bases.Hashable[guilds.Guild]) -> guil Parameters ---------- - guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.entities.Snowflake`, :obj:`~int` ] + guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.bases.Snowflake`, :obj:`~int` ] The object or ID of the guild to get the preview object for. Returns @@ -524,7 +524,7 @@ async def update_guild( Parameters ---------- - guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.entities.Snowflake`, :obj:`~int` ] + guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.bases.Snowflake`, :obj:`~int` ] The object or ID of the guild to be edited. name : :obj:`~str` If specified, the new name string for the guild (``2-100`` characters). @@ -541,17 +541,17 @@ async def update_guild( explicit_content_filter : :obj:`~typing.Union` [ :obj:`~hikari.guilds.GuildExplicitContentFilterLevel`, :obj:`~int` ] If specified, the new explicit content filter. Passing a raw int for this may lead to unexpected behaviour. - afk_channel : :obj:`~typing.Union` [ :obj:`~hikari.channels.GuildVoiceChannel`, :obj:`~hikari.entities.Snowflake`, :obj:`~int` ] + afk_channel : :obj:`~typing.Union` [ :obj:`~hikari.channels.GuildVoiceChannel`, :obj:`~hikari.bases.Snowflake`, :obj:`~int` ] If specified, the object or ID for the new AFK voice channel. afk_timeout : :obj:`~typing.Union` [ :obj:`~datetime.timedelta`, :obj:`~int` ] If specified, the new AFK timeout seconds timedelta. icon_data : ``hikari.internal.conversions.FileLikeT`` If specified, the new guild icon image file data. - owner : :obj:`~typing.Union` [ :obj:`~hikari.users.User`, :obj:`~hikari.entities.Snowflake`, :obj:`~int` ] + owner : :obj:`~typing.Union` [ :obj:`~hikari.users.User`, :obj:`~hikari.bases.Snowflake`, :obj:`~int` ] If specified, the object or ID of the new guild owner. splash_data : ``hikari.internal.conversions.FileLikeT`` If specified, the new new splash image file data. - system_channel : :obj:`~typing.Union` [ :obj:`~hikari.channels.GuildVoiceChannel`, :obj:`~hikari.entities.Snowflake`, :obj:`~int` ] + system_channel : :obj:`~typing.Union` [ :obj:`~hikari.channels.GuildVoiceChannel`, :obj:`~hikari.bases.Snowflake`, :obj:`~int` ] If specified, the object or ID of the new system channel. reason : :obj:`~str` If specified, the audit log reason explaining why the operation @@ -606,7 +606,7 @@ async def delete_guild(self, guild: bases.Hashable[guilds.Guild]) -> None: Parameters ---------- - guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.entities.Snowflake`, :obj:`~int` ] + guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.bases.Snowflake`, :obj:`~int` ] The object or ID of the guild to be deleted. Raises @@ -630,7 +630,7 @@ async def fetch_guild_channels( Parameters ---------- - guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.entities.Snowflake`, :obj:`~int` ] + guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.bases.Snowflake`, :obj:`~int` ] The object or ID of the guild to get the channels from. Returns @@ -672,7 +672,7 @@ async def create_guild_channel( Parameters ---------- - guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.entities.Snowflake`, :obj:`~int` ] + guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.bases.Snowflake`, :obj:`~int` ] The object or ID of the guild to create the channel in. name : :obj:`~str` If specified, the name for the channel. This must be @@ -706,7 +706,7 @@ async def create_guild_channel( permission_overwrites : :obj:`~typing.Sequence` [ :obj:`~hikari.channels.PermissionOverwrite` ] If specified, the list of permission overwrite objects that are category specific to replace the existing overwrites with. - parent_category : :obj:`~typing.Union` [ :obj:`~hikari.channels.GuildCategory`, :obj:`~hikari.entities.Snowflake`, :obj:`~int` ] + parent_category : :obj:`~typing.Union` [ :obj:`~hikari.channels.GuildCategory`, :obj:`~hikari.bases.Snowflake`, :obj:`~int` ] If specified, the object or ID of the parent category to set for the channel. reason : :obj:`~str` @@ -767,12 +767,12 @@ async def reposition_guild_channels( Parameters ---------- - guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.entities.Snowflake`, :obj:`~int` ] + guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.bases.Snowflake`, :obj:`~int` ] The object or ID of the guild in which to edit the channels. - channel : :obj:`~typing.Tuple` [ :obj:`~int` , :obj:`~typing.Union` [ :obj:`~hikari.channels.GuildChannel`, :obj:`~hikari.entities.Snowflake`, :obj:`~int` ] ] + channel : :obj:`~typing.Tuple` [ :obj:`~int` , :obj:`~typing.Union` [ :obj:`~hikari.channels.GuildChannel`, :obj:`~hikari.bases.Snowflake`, :obj:`~int` ] ] The first channel to change the position of. This is a tuple of the integer position the channel object or ID. - *additional_channels : :obj:`~typing.Tuple` [ :obj:`~int`, :obj:`~typing.Union` [ :obj:`~hikari.channels.GuildChannel`, :obj:`~hikari.entities.Snowflake`, :obj:`~int` ] ] + *additional_channels : :obj:`~typing.Tuple` [ :obj:`~int`, :obj:`~typing.Union` [ :obj:`~hikari.channels.GuildChannel`, :obj:`~hikari.bases.Snowflake`, :obj:`~int` ] ] Optional additional channels to change the position of. These must be tuples of integer positions to change to and the channel object or ID and the. @@ -805,9 +805,9 @@ async def fetch_member( Parameters ---------- - guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.entities.Snowflake`, :obj:`~int` ] + guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.bases.Snowflake`, :obj:`~int` ] The object or ID of the guild to get the member from. - user : :obj:`~typing.Union` [ :obj:`~hikari.users.User`, :obj:`~hikari.entities.Snowflake`, :obj:`~int` ] + user : :obj:`~typing.Union` [ :obj:`~hikari.users.User`, :obj:`~hikari.bases.Snowflake`, :obj:`~int` ] The object or ID of the member to get. Returns @@ -846,12 +846,12 @@ def fetch_members_after( Parameters ---------- - guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.entities.Snowflake`, :obj:`~int` ] + guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.bases.Snowflake`, :obj:`~int` ] The object or ID of the guild to get the members from. limit : :obj:`~int` If specified, the maximum number of members this iterator should return. - after : :obj:`~typing.Union` [ :obj:`~datetime.datetime`, :obj:`~hikari.users.User`, :obj:`~hikari.entities.Snowflake`, :obj:`~int` ] + after : :obj:`~typing.Union` [ :obj:`~datetime.datetime`, :obj:`~hikari.users.User`, :obj:`~hikari.bases.Snowflake`, :obj:`~int` ] The object or ID of the user this iterator should start after if specified, else this will start at the oldest user. @@ -908,14 +908,14 @@ async def update_member( Parameters ---------- - guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.entities.Snowflake`, :obj:`~int` ] + guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.bases.Snowflake`, :obj:`~int` ] The object or ID of the guild to edit the member from. - user : :obj:`~typing.Union` [ :obj:`~hikari.guilds.GuildMember`, :obj:`~hikari.entities.Snowflake`, :obj:`~int` ] + user : :obj:`~typing.Union` [ :obj:`~hikari.guilds.GuildMember`, :obj:`~hikari.bases.Snowflake`, :obj:`~int` ] The object or ID of the member to edit. nickname : :obj:`~str`, optional If specified, the new nickname string. Setting it to :obj:`~None` explicitly will clear the nickname. - roles : :obj:`~typing.Sequence` [ :obj:`~typing.Union` [ :obj:`~hikari.guilds.GuildRole`, :obj:`~hikari.entities.Snowflake`, :obj:`~int` ] ] + roles : :obj:`~typing.Sequence` [ :obj:`~typing.Union` [ :obj:`~hikari.guilds.GuildRole`, :obj:`~hikari.bases.Snowflake`, :obj:`~int` ] ] If specified, a list of role IDs the member should have. mute : :obj:`~bool` If specified, whether the user should be muted in the voice channel @@ -923,7 +923,7 @@ async def update_member( deaf : :obj:`~bool` If specified, whether the user should be deafen in the voice channel or not. - voice_channel : :obj:`~typing.Union` [ :obj:`~hikari.channels.GuildVoiceChannel`, :obj:`~hikari.entities.Snowflake`, :obj:`~int` ], optional + voice_channel : :obj:`~typing.Union` [ :obj:`~hikari.channels.GuildVoiceChannel`, :obj:`~hikari.bases.Snowflake`, :obj:`~int` ], optional If specified, the ID of the channel to move the member to. Setting it to :obj:`~None` explicitly will disconnect the user. reason : :obj:`~str` @@ -972,7 +972,7 @@ async def update_my_member_nickname( Parameters ---------- - guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.entities.Snowflake`, :obj:`~int` ] + guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.bases.Snowflake`, :obj:`~int` ] The object or ID of the guild you want to change the nick on. nickname : :obj:`~str`, optional The new nick string. Setting this to `None` clears the nickname. @@ -1011,11 +1011,11 @@ async def add_role_to_member( Parameters ---------- - guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.entities.Snowflake`, :obj:`~int` ] + guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.bases.Snowflake`, :obj:`~int` ] The object or ID of the guild the member belongs to. - user : :obj:`~typing.Union` [ :obj:`~hikari.users.User`, :obj:`~hikari.entities.Snowflake`, :obj:`~int` ] + user : :obj:`~typing.Union` [ :obj:`~hikari.users.User`, :obj:`~hikari.bases.Snowflake`, :obj:`~int` ] The object or ID of the member you want to add the role to. - role : :obj:`~typing.Union` [ :obj:`~hikari.guilds.GuildRole`, :obj:`~hikari.entities.Snowflake`, :obj:`~int` ] + role : :obj:`~typing.Union` [ :obj:`~hikari.guilds.GuildRole`, :obj:`~hikari.bases.Snowflake`, :obj:`~int` ] The object or ID of the role you want to add. reason : :obj:`~str` If specified, the audit log reason explaining why the operation @@ -1050,11 +1050,11 @@ async def remove_role_from_member( Parameters ---------- - guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.entities.Snowflake`, :obj:`~int` ] + guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.bases.Snowflake`, :obj:`~int` ] The object or ID of the guild the member belongs to. - user : :obj:`~typing.Union` [ :obj:`~hikari.users.User`, :obj:`~hikari.entities.Snowflake`, :obj:`~int` ] + user : :obj:`~typing.Union` [ :obj:`~hikari.users.User`, :obj:`~hikari.bases.Snowflake`, :obj:`~int` ] The object or ID of the member you want to remove the role from. - role : :obj:`~typing.Union` [ :obj:`~hikari.guilds.GuildRole`, :obj:`~hikari.entities.Snowflake`, :obj:`~int` ] + role : :obj:`~typing.Union` [ :obj:`~hikari.guilds.GuildRole`, :obj:`~hikari.bases.Snowflake`, :obj:`~int` ] The object or ID of the role you want to remove. reason : :obj:`~str` If specified, the audit log reason explaining why the operation @@ -1084,9 +1084,9 @@ async def kick_member( Parameters ---------- - guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.entities.Snowflake`, :obj:`~int` ] + guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.bases.Snowflake`, :obj:`~int` ] The object or ID of the guild the member belongs to. - user : :obj:`~typing.Union` [ :obj:`~hikari.users.User`, :obj:`~hikari.entities.Snowflake`, :obj:`~int` ] + user : :obj:`~typing.Union` [ :obj:`~hikari.users.User`, :obj:`~hikari.bases.Snowflake`, :obj:`~int` ] The object or ID of the member you want to kick. reason : :obj:`~str` If specified, the audit log reason explaining why the operation @@ -1115,9 +1115,9 @@ async def fetch_ban( Parameters ---------- - guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.entities.Snowflake`, :obj:`~int` ] + guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.bases.Snowflake`, :obj:`~int` ] The object or ID of the guild you want to get the ban from. - user : :obj:`~typing.Union` [ :obj:`~hikari.users.User`, :obj:`~hikari.entities.Snowflake`, :obj:`~int` ] + user : :obj:`~typing.Union` [ :obj:`~hikari.users.User`, :obj:`~hikari.bases.Snowflake`, :obj:`~int` ] The object or ID of the user to get the ban information for. Returns @@ -1147,7 +1147,7 @@ async def fetch_bans(self, guild: bases.Hashable[guilds.Guild],) -> typing.Seque Parameters ---------- - guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.entities.Snowflake`, :obj:`~int` ] + guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.bases.Snowflake`, :obj:`~int` ] The object or ID of the guild you want to get the bans from. Returns @@ -1182,9 +1182,9 @@ async def ban_member( Parameters ---------- - guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.entities.Snowflake`, :obj:`~int` ] + guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.bases.Snowflake`, :obj:`~int` ] The object or ID of the guild the member belongs to. - user : :obj:`~typing.Union` [ :obj:`~hikari.users.User`, :obj:`~hikari.entities.Snowflake`, :obj:`~int` ] + user : :obj:`~typing.Union` [ :obj:`~hikari.users.User`, :obj:`~hikari.bases.Snowflake`, :obj:`~int` ] The object or ID of the member you want to ban. delete_message_days : :obj:`~typing.Union` [ :obj:`~datetime.timedelta`, :obj:`~int` ] If specified, the tim delta of how many days of messages from the @@ -1217,9 +1217,9 @@ async def unban_member( Parameters ---------- - guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.entities.Snowflake`, :obj:`~int` ] + guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.bases.Snowflake`, :obj:`~int` ] The object or ID of the guild to un-ban the user from. - user : :obj:`~typing.Union` [ :obj:`~hikari.users.User`, :obj:`~hikari.entities.Snowflake`, :obj:`~int` ] + user : :obj:`~typing.Union` [ :obj:`~hikari.users.User`, :obj:`~hikari.bases.Snowflake`, :obj:`~int` ] The ID of the user you want to un-ban. reason : :obj:`~str` If specified, the audit log reason explaining why the operation @@ -1250,12 +1250,12 @@ async def fetch_roles( Parameters ---------- - guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.entities.Snowflake`, :obj:`~int` ] + guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.bases.Snowflake`, :obj:`~int` ] The object or ID of the guild you want to get the roles from. Returns ------- - :obj:`~typing.Mapping` [ :obj:`~hikari.entities.Snowflake`, :obj:`~hikari.guilds.GuildRole` ] + :obj:`~typing.Mapping` [ :obj:`~hikari.bases.Snowflake`, :obj:`~hikari.guilds.GuildRole` ] A list of role objects. Raises @@ -1288,7 +1288,7 @@ async def create_role( Parameters ---------- - guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.entities.Snowflake`, :obj:`~int` ] + guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.bases.Snowflake`, :obj:`~int` ] The object or ID of the guild you want to create the role on. name : :obj:`~str` If specified, the new role name string. @@ -1344,12 +1344,12 @@ async def reposition_roles( Parameters ---------- - guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.entities.Snowflake`, :obj:`~int` ] + guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.bases.Snowflake`, :obj:`~int` ] The ID of the guild the roles belong to. - role : :obj:`~typing.Tuple` [ :obj:`~int`, :obj:`~typing.Union` [ :obj:`~hikari.guilds.GuildRole`, :obj:`~hikari.entities.Snowflake`, :obj:`~int` ] ] + role : :obj:`~typing.Tuple` [ :obj:`~int`, :obj:`~typing.Union` [ :obj:`~hikari.guilds.GuildRole`, :obj:`~hikari.bases.Snowflake`, :obj:`~int` ] ] The first role to move. This is a tuple of the integer position and the role object or ID. - *additional_roles : :obj:`~typing.Tuple` [ :obj:`~int`, :obj:`~typing.Union` [ :obj:`~hikari.guilds.GuildRole`, :obj:`~hikari.entities.Snowflake`, :obj:`~int` ] ] + *additional_roles : :obj:`~typing.Tuple` [ :obj:`~int`, :obj:`~typing.Union` [ :obj:`~hikari.guilds.GuildRole`, :obj:`~hikari.bases.Snowflake`, :obj:`~int` ] ] Optional extra roles to move. These must be tuples of the integer position and the role object or ID. @@ -1395,9 +1395,9 @@ async def update_role( Parameters ---------- - guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.entities.Snowflake`, :obj:`~int` ] + guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.bases.Snowflake`, :obj:`~int` ] The object or ID of the guild the role belong to. - role : :obj:`~typing.Union` [ :obj:`~hikari.guilds.GuildRole`, :obj:`~hikari.entities.Snowflake`, :obj:`~int` ] + role : :obj:`~typing.Union` [ :obj:`~hikari.guilds.GuildRole`, :obj:`~hikari.bases.Snowflake`, :obj:`~int` ] The object or ID of the role you want to edit. name : :obj:`~str` If specified, the new role's name string. @@ -1449,9 +1449,9 @@ async def delete_role(self, guild: bases.Hashable[guilds.Guild], role: bases.Has Parameters ---------- - guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.entities.Snowflake`, :obj:`~int` ] + guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.bases.Snowflake`, :obj:`~int` ] The object or ID of the guild you want to remove the role from. - role : :obj:`~typing.Union` [ :obj:`~hikari.guilds.GuildRole`, :obj:`~hikari.entities.Snowflake`, :obj:`~int` ] + role : :obj:`~typing.Union` [ :obj:`~hikari.guilds.GuildRole`, :obj:`~hikari.bases.Snowflake`, :obj:`~int` ] The object or ID of the role you want to delete. Raises @@ -1476,7 +1476,7 @@ async def estimate_guild_prune_count( Parameters ---------- - guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.entities.Snowflake`, :obj:`~int` ] + guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.bases.Snowflake`, :obj:`~int` ] The object or ID of the guild you want to get the count for. days : :obj:`~typing.Union` [ :obj:`~datetime.timedelta`, :obj:`~int` ] The time delta of days to count prune for (at least ``1``). @@ -1514,7 +1514,7 @@ async def begin_guild_prune( Parameters ---------- - guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.entities.Snowflake`, :obj:`~int` ] + guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.bases.Snowflake`, :obj:`~int` ] The object or ID of the guild you want to prune member of. days : :obj:`~typing.Union` [ :obj:`~datetime.timedelta`, :obj:`~int` ] The time delta of inactivity days you want to use as filter. @@ -1557,7 +1557,7 @@ async def fetch_guild_voice_regions( Parameters ---------- - guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.entities.Snowflake`, :obj:`~int` ] + guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.bases.Snowflake`, :obj:`~int` ] The object or ID of the guild to get the voice regions for. Returns @@ -1587,7 +1587,7 @@ async def fetch_guild_invites( Parameters ---------- - guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.entities.Snowflake`, :obj:`~int` ] + guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.bases.Snowflake`, :obj:`~int` ] The object or ID of the guild to get the invites for. Returns @@ -1615,7 +1615,7 @@ async def fetch_integrations(self, guild: bases.Hashable[guilds.Guild]) -> typin Parameters ---------- - guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.entities.Snowflake`, :obj:`~int` ] + guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.bases.Snowflake`, :obj:`~int` ] The object or ID of the guild to get the integrations for. Returns @@ -1652,9 +1652,9 @@ async def update_integration( Parameters ---------- - guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.entities.Snowflake`, :obj:`~int` ] + guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.bases.Snowflake`, :obj:`~int` ] The object or ID of the guild to which the integration belongs to. - integration : :obj:`~typing.Union` [ :obj:`~hikari.guilds.GuildIntegration`, :obj:`~hikari.entities.Snowflake`, :obj:`~int` ] + integration : :obj:`~typing.Union` [ :obj:`~hikari.guilds.GuildIntegration`, :obj:`~hikari.bases.Snowflake`, :obj:`~int` ] The object or ID of the integration to update. expire_behaviour : :obj:`~typing.Union` [ :obj:`~hikari.guilds.IntegrationExpireBehaviour`, :obj:`~int` ] If specified, the behaviour for when an integration subscription @@ -1699,9 +1699,9 @@ async def delete_integration( Parameters ---------- - guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.entities.Snowflake`, :obj:`~int` ] + guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.bases.Snowflake`, :obj:`~int` ] The object or ID of the guild to which the integration belongs to. - integration : :obj:`~typing.Union` [ :obj:`~hikari.guilds.GuildIntegration`, :obj:`~hikari.entities.Snowflake`, :obj:`~int` ] + integration : :obj:`~typing.Union` [ :obj:`~hikari.guilds.GuildIntegration`, :obj:`~hikari.bases.Snowflake`, :obj:`~int` ] The object or ID of the integration to delete. reason : :obj:`~str` If specified, the audit log reason explaining why the operation @@ -1730,9 +1730,9 @@ async def sync_guild_integration( Parameters ---------- - guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.entities.Snowflake`, :obj:`~int` ] + guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.bases.Snowflake`, :obj:`~int` ] The object or ID of the guild to which the integration belongs to. - integration : :obj:`~typing.Union` [ :obj:`~hikari.guilds.GuildIntegration`, :obj:`~hikari.entities.Snowflake`, :obj:`~int` ] + integration : :obj:`~typing.Union` [ :obj:`~hikari.guilds.GuildIntegration`, :obj:`~hikari.bases.Snowflake`, :obj:`~int` ] The ID of the integration to sync. Raises @@ -1755,7 +1755,7 @@ async def fetch_guild_embed(self, guild: bases.Hashable[guilds.Guild],) -> guild Parameters ---------- - guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.entities.Snowflake`, :obj:`~int` ] + guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.bases.Snowflake`, :obj:`~int` ] The object or ID of the guild to get the embed for. Returns @@ -1791,9 +1791,9 @@ async def update_guild_embed( Parameters ---------- - guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.entities.Snowflake`, :obj:`~int` ] + guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.bases.Snowflake`, :obj:`~int` ] The object or ID of the guild to edit the embed for. - channel : :obj:`~typing.Union` [ :obj:`~hikari.channels.GuildChannel`, :obj:`~hikari.entities.Snowflake`, :obj:`~int` ], optional + channel : :obj:`~typing.Union` [ :obj:`~hikari.channels.GuildChannel`, :obj:`~hikari.bases.Snowflake`, :obj:`~int` ], optional If specified, the object or ID of the channel that this embed's invite should target. Set to :obj:`~None` to disable invites for this embed. @@ -1837,7 +1837,7 @@ async def fetch_guild_vanity_url(self, guild: bases.Hashable[guilds.Guild],) -> Parameters ---------- - guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.entities.Snowflake`, :obj:`~int` ] + guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.bases.Snowflake`, :obj:`~int` ] The object or ID of the guild to get the vanity URL for. Returns @@ -1867,7 +1867,7 @@ def format_guild_widget_image(self, guild: bases.Hashable[guilds.Guild], *, styl Parameters ---------- - guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.entities.Snowflake`, :obj:`~int` ] + guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.bases.Snowflake`, :obj:`~int` ] The object or ID of the guild to form the widget. style : :obj:`~str` If specified, the syle of the widget. @@ -1896,7 +1896,7 @@ async def fetch_guild_webhooks(self, guild: bases.Hashable[guilds.Guild]) -> typ Parameters ---------- - guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.entities.Snowflake`, :obj:`~int` ] + guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.bases.Snowflake`, :obj:`~int` ] The object or ID for the guild to get the webhooks from. Returns diff --git a/hikari/clients/rest/me.py b/hikari/clients/rest/me.py index 79835664be..aaf57987a2 100644 --- a/hikari/clients/rest/me.py +++ b/hikari/clients/rest/me.py @@ -108,7 +108,7 @@ def fetch_my_guilds_after( Parameters ---------- - after : :obj:`~typing.Union` [ :obj:`~datetime.datetime`, :obj:`~hikari.guilds.Guild`, :obj:`~hikari.entities.Snowflake`, :obj:`~int` ] + after : :obj:`~typing.Union` [ :obj:`~datetime.datetime`, :obj:`~hikari.guilds.Guild`, :obj:`~hikari.bases.Snowflake`, :obj:`~int` ] The object or ID of a guild to get guilds that were created after it if specified, else this will start at the oldest guild. limit : :obj:`~int` @@ -161,7 +161,7 @@ def fetch_my_guilds_before( Parameters ---------- - before : :obj:`~typing.Union` [ :obj:`~datetime.datetime`, :obj:`~hikari.guilds.Guild`, :obj:`~hikari.entities.Snowflake`, :obj:`~int` ] + before : :obj:`~typing.Union` [ :obj:`~datetime.datetime`, :obj:`~hikari.guilds.Guild`, :obj:`~hikari.bases.Snowflake`, :obj:`~int` ] The object or ID of a guild to get guilds that were created before it if specified, else this will start at the newest guild. limit : :obj:`~int` @@ -199,7 +199,7 @@ async def leave_guild(self, guild: bases.Hashable[guilds.Guild]) -> None: Parameters ---------- - guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.entities.Snowflake`, :obj:`~int` ] + guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.bases.Snowflake`, :obj:`~int` ] The object or ID of the guild to leave. Raises @@ -217,7 +217,7 @@ async def create_dm_channel(self, recipient: bases.Hashable[users.User]) -> _cha Parameters ---------- - recipient : :obj:`~typing.Union` [ :obj:`~hikari.users.User`, :obj:`~hikari.entities.Snowflake`, :obj:`~int` ] + recipient : :obj:`~typing.Union` [ :obj:`~hikari.users.User`, :obj:`~hikari.bases.Snowflake`, :obj:`~int` ] The object or ID of the user to create the new DM channel with. Returns diff --git a/hikari/clients/rest/react.py b/hikari/clients/rest/react.py index f07082df88..b6751615b2 100644 --- a/hikari/clients/rest/react.py +++ b/hikari/clients/rest/react.py @@ -46,9 +46,9 @@ async def create_reaction( Parameters ---------- - channel : :obj:`~typing.Union` [ :obj:`~hikari.channels.Channel`, :obj:`~hikari.entities.Snowflake`, :obj:`~int` ] + channel : :obj:`~typing.Union` [ :obj:`~hikari.channels.Channel`, :obj:`~hikari.bases.Snowflake`, :obj:`~int` ] The object or ID of the channel to add this reaction in. - message : :obj:`~typing.Union` [ :obj:`~hikari.messages.Message`, :obj:`~hikari.entities.Snowflake`, :obj:`~int` ] + message : :obj:`~typing.Union` [ :obj:`~hikari.messages.Message`, :obj:`~hikari.bases.Snowflake`, :obj:`~int` ] The object or ID of the message to add the reaction in. emoji : :obj:`~typing.Union` [ :obj:`~hikari.emojis.Emoji`, :obj:`~str` ] The emoji to add. This can either be an emoji object or a string @@ -85,9 +85,9 @@ async def delete_reaction( Parameters ---------- - channel : :obj:`~typing.Union` [ :obj:`~hikari.channels.Channel`, :obj:`~hikari.entities.Snowflake`, :obj:`~int` ] + channel : :obj:`~typing.Union` [ :obj:`~hikari.channels.Channel`, :obj:`~hikari.bases.Snowflake`, :obj:`~int` ] The object or ID of the channel to add this reaction in. - message : :obj:`~typing.Union` [ :obj:`~hikari.messages.Message`, :obj:`~hikari.entities.Snowflake`, :obj:`~int` ] + message : :obj:`~typing.Union` [ :obj:`~hikari.messages.Message`, :obj:`~hikari.bases.Snowflake`, :obj:`~int` ] The object or ID of the message to add the reaction in. emoji : :obj:`~typing.Union` [ :obj:`~hikari.emojis.Emoji`, :obj:`~str` ] The emoji to add. This can either be an emoji object or a @@ -122,9 +122,9 @@ async def delete_all_reactions( Parameters ---------- - channel : :obj:`~typing.Union` [ :obj:`~hikari.channels.Channel`, :obj:`~hikari.entities.Snowflake`, :obj:`~int` ] + channel : :obj:`~typing.Union` [ :obj:`~hikari.channels.Channel`, :obj:`~hikari.bases.Snowflake`, :obj:`~int` ] The object or ID of the channel to get the message from. - message : :obj:`~typing.Union` [ :obj:`~hikari.messages.Message`, :obj:`~hikari.entities.Snowflake`, :obj:`~int` ] + message : :obj:`~typing.Union` [ :obj:`~hikari.messages.Message`, :obj:`~hikari.bases.Snowflake`, :obj:`~int` ] The object or ID of the message to remove all reactions from. Raises @@ -152,9 +152,9 @@ async def delete_all_reactions_for_emoji( Parameters ---------- - channel : :obj:`~typing.Union` [ :obj:`~hikari.channels.Channel`, :obj:`~hikari.entities.Snowflake`, :obj:`~int` ] + channel : :obj:`~typing.Union` [ :obj:`~hikari.channels.Channel`, :obj:`~hikari.bases.Snowflake`, :obj:`~int` ] The object or ID of the channel to get the message from. - message : :obj:`~typing.Union` [ :obj:`~hikari.messages.Message`, :obj:`~hikari.entities.Snowflake`, :obj:`~int` ] + message : :obj:`~typing.Union` [ :obj:`~hikari.messages.Message`, :obj:`~hikari.bases.Snowflake`, :obj:`~int` ] The object or ID of the message to delete the reactions from. emoji : :obj:`~typing.Union` [ :obj:`~hikari.emojis.Emoji`, :obj:`~str` ] The object or string representatiom of the emoji to delete. The @@ -194,16 +194,16 @@ def fetch_reactors_after( Parameters ---------- - channel : :obj:`~typing.Union` [ :obj:`~hikari.channels.Channel`, :obj:`~hikari.entities.Snowflake`, :obj:`~int` ] + channel : :obj:`~typing.Union` [ :obj:`~hikari.channels.Channel`, :obj:`~hikari.bases.Snowflake`, :obj:`~int` ] The object or ID of the channel to get the message from. - message : :obj:`~typing.Union` [ :obj:`~hikari.messages.Message`, :obj:`~hikari.entities.Snowflake`, :obj:`~int` ] + message : :obj:`~typing.Union` [ :obj:`~hikari.messages.Message`, :obj:`~hikari.bases.Snowflake`, :obj:`~int` ] The object or ID of the message to get the reactions from. emoji : :obj:`~typing.Union` [ :obj:`~hikari.emojis.Emoji`, :obj:`~str` ] The emoji to get. This can either be it's object or the string representation of the emoji. The string representation will be either ``"name:id"`` for custom emojis else it's unicode character(s) (can be UTF-32). - after : :obj:`~typing.Union` [ :obj:`~datetime.datetime`, :obj:`~hikari.users.User`, :obj:`~hikari.entities.Snowflake`, :obj:`~int` ] + after : :obj:`~typing.Union` [ :obj:`~datetime.datetime`, :obj:`~hikari.users.User`, :obj:`~hikari.bases.Snowflake`, :obj:`~int` ] If specified, a object or ID user. If specified, only users with a snowflake that is lexicographically greater than the value will be returned. diff --git a/hikari/clients/rest/user.py b/hikari/clients/rest/user.py index a956a7cdbf..9e81210b6c 100644 --- a/hikari/clients/rest/user.py +++ b/hikari/clients/rest/user.py @@ -35,7 +35,7 @@ async def fetch_user(self, user: bases.Hashable[users.User]) -> users.User: Parameters ---------- - user : :obj:`~typing.Union` [ :obj:`~hikari.users.User`, :obj:`~hikari.entities.Snowflake`, :obj:`~int` ] + user : :obj:`~typing.Union` [ :obj:`~hikari.users.User`, :obj:`~hikari.bases.Snowflake`, :obj:`~int` ] The object or ID of the user to get. Returns diff --git a/hikari/clients/rest/webhook.py b/hikari/clients/rest/webhook.py index da4af44a21..f69609af63 100644 --- a/hikari/clients/rest/webhook.py +++ b/hikari/clients/rest/webhook.py @@ -46,7 +46,7 @@ async def fetch_webhook( Parameters ---------- - webhook : :obj:`~typing.Union` [ :obj:`~hikari.webhooks.Webhook`, :obj:`~hikari.entities.Snowflake`, :obj:`~int` ] + webhook : :obj:`~typing.Union` [ :obj:`~hikari.webhooks.Webhook`, :obj:`~hikari.bases.Snowflake`, :obj:`~int` ] The object or ID of the webhook to get. webhook_token : :obj:`~str` If specified, the webhook token to use to get it (bypassing this @@ -90,7 +90,7 @@ async def update_webhook( Parameters ---------- - webhook : :obj:`~typing.Union` [ :obj:`~hikari.webhooks.Webhook`, :obj:`~hikari.entities.Snowflake`, :obj:`~int` ] + webhook : :obj:`~typing.Union` [ :obj:`~hikari.webhooks.Webhook`, :obj:`~hikari.bases.Snowflake`, :obj:`~int` ] The object or ID of the webhook to edit. webhook_token : :obj:`~str` If specified, the webhook token to use to modify it (bypassing this @@ -100,7 +100,7 @@ async def update_webhook( avatar_data : ``hikari.internal.conversions.FileLikeT``, optional If specified, the new avatar image file object. If :obj:`~None`, then it is removed. - channel : :obj:`~typing.Union` [ :obj:`~hikari.channels.GuildChannel`, :obj:`~hikari.entities.Snowflake`, :obj:`~int` ] + channel : :obj:`~typing.Union` [ :obj:`~hikari.channels.GuildChannel`, :obj:`~hikari.bases.Snowflake`, :obj:`~int` ] If specified, the object or ID of the new channel the given webhook should be moved to. reason : :obj:`~str` @@ -148,7 +148,7 @@ async def delete_webhook(self, webhook: bases.Hashable[webhooks.Webhook], *, web Parameters ---------- - webhook : :obj:`~typing.Union` [ :obj:`~hikari.webhooks.Webhook`, :obj:`~hikari.entities.Snowflake`, :obj:`~int` ] + webhook : :obj:`~typing.Union` [ :obj:`~hikari.webhooks.Webhook`, :obj:`~hikari.bases.Snowflake`, :obj:`~int` ] The object or ID of the webhook to delete webhook_token : :obj:`~str` If specified, the webhook token to use to delete it (bypassing this @@ -192,7 +192,7 @@ async def execute_webhook( Parameters ---------- - webhook : :obj:`~typing.Union` [ :obj:`~hikari.webhooks.Webhook`, :obj:`~hikari.entities.Snowflake`, :obj:`~int` ] + webhook : :obj:`~typing.Union` [ :obj:`~hikari.webhooks.Webhook`, :obj:`~hikari.bases.Snowflake`, :obj:`~int` ] The object or ID of the webhook to execute. webhook_token : :obj:`~str` The token of the webhook to execute. @@ -218,11 +218,11 @@ async def execute_webhook( mentions_everyone : :obj:`~bool` Whether ``@everyone`` and ``@here`` mentions should be resolved by discord and lead to actual pings, defaults to :obj:`~True`. - user_mentions : :obj:`~typing.Union` [ :obj:`~typing.Collection` [ :obj:`~typing.Union` [ :obj:`~hikari.users.User`, :obj:`~hikari.entities.Snowflake`, :obj:`~int` ], :obj:`~bool` ] + user_mentions : :obj:`~typing.Union` [ :obj:`~typing.Collection` [ :obj:`~typing.Union` [ :obj:`~hikari.users.User`, :obj:`~hikari.bases.Snowflake`, :obj:`~int` ], :obj:`~bool` ] Either an array of user objects/IDs to allow mentions for, :obj:`~True` to allow all user mentions or :obj:`~False` to block all user mentions from resolving, defaults to :obj:`~True`. - role_mentions : :obj:`~typing.Union` [ :obj:`~typing.Collection` [ :obj:`~typing.Union` [ :obj:`~hikari.guilds.GuildRole`, :obj:`~hikari.entities.Snowflake`, :obj:`~int` ] ], :obj:`~bool` ] + role_mentions : :obj:`~typing.Union` [ :obj:`~typing.Collection` [ :obj:`~typing.Union` [ :obj:`~hikari.guilds.GuildRole`, :obj:`~hikari.bases.Snowflake`, :obj:`~int` ] ], :obj:`~bool` ] Either an array of guild role objects/IDs to allow mentions for, :obj:`~True` to allow all role mentions or :obj:`~False` to block all role mentions from resolving, defaults to :obj:`~True`. diff --git a/hikari/emojis.py b/hikari/emojis.py index 34e85231a4..2984c17396 100644 --- a/hikari/emojis.py +++ b/hikari/emojis.py @@ -88,7 +88,7 @@ class GuildEmoji(UnknownEmoji): #: The whitelisted role IDs to use this emoji. #: - #: :type: :obj:`~typing.Set` [ :obj:`~hikari.entities.Snowflake` ] + #: :type: :obj:`~typing.Set` [ :obj:`~hikari.bases.Snowflake` ] role_ids: typing.Set[bases.Snowflake] = marshaller.attrib( raw_name="roles", deserializer=lambda roles: {bases.Snowflake.deserialize(r) for r in roles}, diff --git a/hikari/events.py b/hikari/events.py index ea7c3e7af4..174923745b 100644 --- a/hikari/events.py +++ b/hikari/events.py @@ -191,7 +191,7 @@ class ReadyEvent(HikariEvent, marshaller.Deserializable): #: A mapping of the guilds this bot is currently in. All guilds will start #: off "unavailable". #: - #: :type: :obj:`~typing.Mapping` [ :obj:`~hikari.entities.Snowflake`, :obj:`~hikari.guilds.UnavailableGuild` ] + #: :type: :obj:`~typing.Mapping` [ :obj:`~hikari.bases.Snowflake`, :obj:`~hikari.guilds.UnavailableGuild` ] unavailable_guilds: typing.Mapping[bases.Snowflake, guilds.UnavailableGuild] = marshaller.attrib( raw_name="guilds", deserializer=lambda guilds_objs: {g.id: g for g in map(guilds.UnavailableGuild.deserialize, guilds_objs)}, @@ -238,7 +238,7 @@ class BaseChannelEvent(HikariEvent, bases.UniqueEntity, marshaller.Deserializabl #: The ID of the guild this channel is in, will be :obj:`~None` for DMs. #: - #: :type: :obj:`~hikari.entities.Snowflake`, optional + #: :type: :obj:`~hikari.bases.Snowflake`, optional guild_id: typing.Optional[bases.Snowflake] = marshaller.attrib( deserializer=bases.Snowflake.deserialize, if_undefined=None, default=None ) @@ -251,7 +251,7 @@ class BaseChannelEvent(HikariEvent, bases.UniqueEntity, marshaller.Deserializabl #: An mapping of the set permission overwrites for this channel, if applicable. #: - #: :type: :obj:`~typing.Mapping` [ :obj:`~hikari.entities.Snowflake`, :obj:`~hikari.channels.PermissionOverwrite` ], optional + #: :type: :obj:`~typing.Mapping` [ :obj:`~hikari.bases.Snowflake`, :obj:`~hikari.channels.PermissionOverwrite` ], optional permission_overwrites: typing.Optional[ typing.Mapping[bases.Snowflake, channels.PermissionOverwrite] ] = marshaller.attrib( @@ -279,7 +279,7 @@ class BaseChannelEvent(HikariEvent, bases.UniqueEntity, marshaller.Deserializabl #: The ID of the last message sent, if it's a text type channel. #: - #: :type: :obj:`~hikari.entities.Snowflake`, optional + #: :type: :obj:`~hikari.bases.Snowflake`, optional last_message_id: typing.Optional[bases.Snowflake] = marshaller.attrib( deserializer=bases.Snowflake.deserialize, if_none=None, if_undefined=None, default=None ) @@ -304,7 +304,7 @@ class BaseChannelEvent(HikariEvent, bases.UniqueEntity, marshaller.Deserializabl #: A mapping of this channel's recipient users, if it's a DM or group DM. #: - #: :type: :obj:`~typing.Mapping` [ :obj:`~hikari.entities.Snowflake`, :obj:`~hikari.users.User` ], optional + #: :type: :obj:`~typing.Mapping` [ :obj:`~hikari.bases.Snowflake`, :obj:`~hikari.users.User` ], optional recipients: typing.Optional[typing.Mapping[bases.Snowflake, users.User]] = marshaller.attrib( deserializer=lambda recipients: {user.id: user for user in map(users.User.deserialize, recipients)}, if_undefined=None, @@ -320,7 +320,7 @@ class BaseChannelEvent(HikariEvent, bases.UniqueEntity, marshaller.Deserializabl #: The ID of this channel's creator, if it's a DM channel. #: - #: :type: :obj:`~hikari.entities.Snowflake`, optional + #: :type: :obj:`~hikari.bases.Snowflake`, optional owner_id: typing.Optional[bases.Snowflake] = marshaller.attrib( deserializer=bases.Snowflake.deserialize, if_undefined=None, default=None ) @@ -328,14 +328,14 @@ class BaseChannelEvent(HikariEvent, bases.UniqueEntity, marshaller.Deserializabl #: The ID of the application that created the group DM, if it's a #: bot based group DM. #: - #: :type: :obj:`~hikari.entities.Snowflake`, optional + #: :type: :obj:`~hikari.bases.Snowflake`, optional application_id: typing.Optional[bases.Snowflake] = marshaller.attrib( deserializer=bases.Snowflake.deserialize, if_undefined=None, default=None ) #: The ID of this channels's parent category within guild, if set. #: - #: :type: :obj:`~hikari.entities.Snowflake`, optional + #: :type: :obj:`~hikari.bases.Snowflake`, optional parent_id: typing.Optional[bases.Snowflake] = marshaller.attrib( deserializer=bases.Snowflake.deserialize, if_undefined=None, if_none=None, default=None ) @@ -383,14 +383,14 @@ class ChannelPinUpdateEvent(HikariEvent, marshaller.Deserializable): #: The ID of the guild where this event happened. #: Will be :obj:`~None` if this happened in a DM channel. #: - #: :type: :obj:`~hikari.entities.Snowflake`, optional + #: :type: :obj:`~hikari.bases.Snowflake`, optional guild_id: typing.Optional[bases.Snowflake] = marshaller.attrib( deserializer=bases.Snowflake.deserialize, if_undefined=None, default=None ) #: The ID of the channel where the message was pinned or unpinned. #: - #: :type: :obj:`~hikari.entities.Snowflake` + #: :type: :obj:`~hikari.bases.Snowflake` channel_id: bases.Snowflake = marshaller.attrib(deserializer=bases.Snowflake.deserialize) #: The datetime of when the most recent message was pinned in this channel. @@ -447,7 +447,7 @@ class BaseGuildBanEvent(HikariEvent, marshaller.Deserializable): #: The ID of the guild this ban is in. #: - #: :type: :obj:`~hikari.entities.Snowflake` + #: :type: :obj:`~hikari.bases.Snowflake` guild_id: bases.Snowflake = marshaller.attrib(deserializer=bases.Snowflake.deserialize) #: The object of the user this ban targets. @@ -475,12 +475,12 @@ class GuildEmojisUpdateEvent(HikariEvent, marshaller.Deserializable): #: The ID of the guild this emoji was updated in. #: - #: :type: :obj:`~hikari.entities.Snowflake` + #: :type: :obj:`~hikari.bases.Snowflake` guild_id: bases.Snowflake = marshaller.attrib(deserializer=bases.Snowflake.deserialize) #: The updated mapping of emojis by their ID. #: - #: :type: :obj:`~typing.Mapping` [ :obj:`~hikari.entities.Snowflake`, :obj:`~hikari.emojis.GuildEmoji` ] + #: :type: :obj:`~typing.Mapping` [ :obj:`~hikari.bases.Snowflake`, :obj:`~hikari.emojis.GuildEmoji` ] emojis: typing.Mapping[bases.Snowflake, _emojis.GuildEmoji] = marshaller.attrib( deserializer=lambda ems: {emoji.id: emoji for emoji in map(_emojis.GuildEmoji.deserialize, ems)} ) @@ -493,7 +493,7 @@ class GuildIntegrationsUpdateEvent(HikariEvent, marshaller.Deserializable): #: The ID of the guild the integration was updated in. #: - #: :type: :obj:`~hikari.entities.Snowflake` + #: :type: :obj:`~hikari.bases.Snowflake` guild_id: bases.Snowflake = marshaller.attrib(deserializer=bases.Snowflake.deserialize) @@ -504,7 +504,7 @@ class GuildMemberAddEvent(HikariEvent, guilds.GuildMember): #: The ID of the guild where this member was added. #: - #: :type: :obj:`~hikari.entities.Snowflake` + #: :type: :obj:`~hikari.bases.Snowflake` guild_id: bases.Snowflake = marshaller.attrib(deserializer=bases.Snowflake.deserialize) @@ -518,12 +518,12 @@ class GuildMemberUpdateEvent(HikariEvent, marshaller.Deserializable): #: The ID of the guild this member was updated in. #: - #: :type: :obj:`~hikari.entities.Snowflake` + #: :type: :obj:`~hikari.bases.Snowflake` guild_id: bases.Snowflake = marshaller.attrib(deserializer=bases.Snowflake.deserialize) #: A sequence of the IDs of the member's current roles. #: - #: :type: :obj:`~typing.Sequence` [ :obj:`~hikari.entities.Snowflake` ] + #: :type: :obj:`~typing.Sequence` [ :obj:`~hikari.bases.Snowflake` ] role_ids: typing.Sequence[bases.Snowflake] = marshaller.attrib( raw_name="roles", deserializer=lambda role_ids: [bases.Snowflake.deserialize(rid) for rid in role_ids], ) @@ -560,7 +560,7 @@ class GuildMemberRemoveEvent(HikariEvent, marshaller.Deserializable): #: The ID of the guild this user was removed from. #: - #: :type: :obj:`~hikari.entities.Snowflake` + #: :type: :obj:`~hikari.bases.Snowflake` guild_id: bases.Snowflake = marshaller.attrib(deserializer=bases.Snowflake.deserialize) #: The object of the user who was removed from this guild. @@ -576,7 +576,7 @@ class GuildRoleCreateEvent(HikariEvent, marshaller.Deserializable): #: The ID of the guild where this role was created. #: - #: :type: :obj:`~hikari.entities.Snowflake` + #: :type: :obj:`~hikari.bases.Snowflake` guild_id: bases.Snowflake = marshaller.attrib(deserializer=bases.Snowflake.deserialize) #: The object of the role that was created. @@ -592,7 +592,7 @@ class GuildRoleUpdateEvent(HikariEvent, marshaller.Deserializable): #: The ID of the guild where this role was updated. #: - #: :type: :obj:`~hikari.entities.Snowflake` + #: :type: :obj:`~hikari.bases.Snowflake` guild_id: bases.Snowflake = marshaller.attrib(deserializer=bases.Snowflake.deserialize) #: The updated role object. @@ -608,12 +608,12 @@ class GuildRoleDeleteEvent(HikariEvent, marshaller.Deserializable): #: The ID of the guild where this role is being deleted. #: - #: :type: :obj:`~hikari.entities.Snowflake` + #: :type: :obj:`~hikari.bases.Snowflake` guild_id: bases.Snowflake = marshaller.attrib(deserializer=bases.Snowflake.deserialize) #: The ID of the role being deleted. #: - #: :type: :obj:`~hikari.entities.Snowflake` + #: :type: :obj:`~hikari.bases.Snowflake` role_id: bases.Snowflake = marshaller.attrib(deserializer=bases.Snowflake.deserialize) @@ -624,7 +624,7 @@ class InviteCreateEvent(HikariEvent, marshaller.Deserializable): #: The ID of the channel this invite targets. #: - #: :type: :obj:`~hikari.entities.Snowflake` + #: :type: :obj:`~hikari.bases.Snowflake` channel_id: bases.Snowflake = marshaller.attrib(deserializer=bases.Snowflake.deserialize) #: The code that identifies this invite @@ -640,7 +640,7 @@ class InviteCreateEvent(HikariEvent, marshaller.Deserializable): #: The ID of the guild this invite was created in, if applicable. #: Will be :obj:`~None` for group DM invites. #: - #: :type: :obj:`~hikari.entities.Snowflake`, optional + #: :type: :obj:`~hikari.bases.Snowflake`, optional guild_id: typing.Optional[bases.Snowflake] = marshaller.attrib( deserializer=bases.Snowflake.deserialize, if_undefined=None, default=None ) @@ -701,7 +701,7 @@ class InviteDeleteEvent(HikariEvent, marshaller.Deserializable): #: The ID of the channel this ID was attached to #: - #: :type: :obj:`~hikari.entities.Snowflake` + #: :type: :obj:`~hikari.bases.Snowflake` channel_id: bases.Snowflake = marshaller.attrib(deserializer=bases.Snowflake.deserialize) #: The code of this invite. @@ -712,7 +712,7 @@ class InviteDeleteEvent(HikariEvent, marshaller.Deserializable): #: The ID of the guild this invite was deleted in. #: This will be :obj:`~None` if this invite belonged to a DM channel. #: - #: :type: :obj:`~hikari.entities.Snowflake`, optional + #: :type: :obj:`~hikari.bases.Snowflake`, optional guild_id: typing.Optional[bases.Snowflake] = marshaller.attrib( deserializer=bases.Snowflake.deserialize, if_undefined=None, default=None ) @@ -734,18 +734,18 @@ class MessageUpdateEvent(HikariEvent, bases.UniqueEntity, marshaller.Deserializa ---- All fields on this model except :attr:`channel_id` and :obj:`~`HikariEvent.id`` may be set to :obj:`~hikari.unset.UNSET` (a singleton defined in - ``hikari.entities``) if we have not received information about their + ``hikari.bases``) if we have not received information about their state from Discord alongside field nullability. """ #: The ID of the channel that the message was sent in. #: - #: :type: :obj:`~hikari.entities.Snowflake` + #: :type: :obj:`~hikari.bases.Snowflake` channel_id: bases.Snowflake = marshaller.attrib(deserializer=bases.Snowflake.deserialize) #: The ID of the guild that the message was sent in. #: - #: :type: :obj:`~typing.Union` [ :obj:`~hikari.entities.Snowflake`, :obj:`~hikari.unset.UNSET` ] + #: :type: :obj:`~typing.Union` [ :obj:`~hikari.bases.Snowflake`, :obj:`~hikari.unset.UNSET` ] guild_id: typing.Union[bases.Snowflake, unset.Unset] = marshaller.attrib( deserializer=bases.Snowflake.deserialize, if_undefined=unset.Unset, default=unset.UNSET ) @@ -802,7 +802,7 @@ class MessageUpdateEvent(HikariEvent, bases.UniqueEntity, marshaller.Deserializa #: The users the message mentions. #: - #: :type: :obj:`~typing.Union` [ :obj:`~typing.Set` [ :obj:`~hikari.entities.Snowflake` ], :obj:`~hikari.unset.UNSET` ] + #: :type: :obj:`~typing.Union` [ :obj:`~typing.Set` [ :obj:`~hikari.bases.Snowflake` ], :obj:`~hikari.unset.UNSET` ] user_mentions: typing.Union[typing.Set[bases.Snowflake], unset.Unset] = marshaller.attrib( raw_name="mentions", deserializer=lambda user_mentions: {bases.Snowflake.deserialize(u["id"]) for u in user_mentions}, @@ -812,7 +812,7 @@ class MessageUpdateEvent(HikariEvent, bases.UniqueEntity, marshaller.Deserializa #: The roles the message mentions. #: - #: :type: :obj:`~typing.Union` [ :obj:`~typing.Set` [ :obj:`~hikari.entities.Snowflake` ], :obj:`~hikari.unset.UNSET` ] + #: :type: :obj:`~typing.Union` [ :obj:`~typing.Set` [ :obj:`~hikari.bases.Snowflake` ], :obj:`~hikari.unset.UNSET` ] role_mentions: typing.Union[typing.Set[bases.Snowflake], unset.Unset] = marshaller.attrib( raw_name="mention_roles", deserializer=lambda role_mentions: {bases.Snowflake.deserialize(r) for r in role_mentions}, @@ -822,7 +822,7 @@ class MessageUpdateEvent(HikariEvent, bases.UniqueEntity, marshaller.Deserializa #: The channels the message mentions. #: - #: :type: :obj:`~typing.Union` [ :obj:`~typing.Set` [ :obj:`~hikari.entities.Snowflake` ], :obj:`~hikari.unset.UNSET` ] + #: :type: :obj:`~typing.Union` [ :obj:`~typing.Set` [ :obj:`~hikari.bases.Snowflake` ], :obj:`~hikari.unset.UNSET` ] channel_mentions: typing.Union[typing.Set[bases.Snowflake], unset.Unset] = marshaller.attrib( raw_name="mention_channels", deserializer=lambda channel_mentions: {bases.Snowflake.deserialize(c["id"]) for c in channel_mentions}, @@ -866,7 +866,7 @@ class MessageUpdateEvent(HikariEvent, bases.UniqueEntity, marshaller.Deserializa #: If the message was generated by a webhook, the webhook's id. #: - #: :type: :obj:`~typing.Union` [ :obj:`~hikari.entities.Snowflake`, :obj:`~hikari.unset.UNSET` ] + #: :type: :obj:`~typing.Union` [ :obj:`~hikari.bases.Snowflake`, :obj:`~hikari.unset.UNSET` ] webhook_id: typing.Union[bases.Snowflake, unset.Unset] = marshaller.attrib( deserializer=bases.Snowflake.deserialize, if_undefined=unset.Unset, default=unset.UNSET ) @@ -925,19 +925,19 @@ class MessageDeleteEvent(HikariEvent, marshaller.Deserializable): #: The ID of the channel where this message was deleted. #: - #: :type: :obj:`~hikari.entities.Snowflake` + #: :type: :obj:`~hikari.bases.Snowflake` channel_id: bases.Snowflake = marshaller.attrib(deserializer=bases.Snowflake.deserialize) #: The ID of the guild where this message was deleted. #: Will be :obj:`~None` if this message was deleted in a DM channel. #: - #: :type: :obj:`~hikari.entities.Snowflake`, optional + #: :type: :obj:`~hikari.bases.Snowflake`, optional guild_id: typing.Optional[bases.Snowflake] = marshaller.attrib( deserializer=bases.Snowflake.deserialize, if_undefined=None, default=None ) #: The ID of the message that was deleted. #: - #: :type: :obj:`~hikari.entities.Snowflake` + #: :type: :obj:`~hikari.bases.Snowflake` message_id: bases.Snowflake = marshaller.attrib(raw_name="id", deserializer=bases.Snowflake.deserialize) @@ -951,20 +951,20 @@ class MessageDeleteBulkEvent(HikariEvent, marshaller.Deserializable): #: The ID of the channel these messages have been deleted in. #: - #: :type: :obj:`~hikari.entities.Snowflake` + #: :type: :obj:`~hikari.bases.Snowflake` channel_id: bases.Snowflake = marshaller.attrib(deserializer=bases.Snowflake.deserialize) #: The ID of the channel these messages have been deleted in. #: Will be :obj:`~None` if these messages were bulk deleted in a DM channel. #: - #: :type: :obj:`~hikari.entities.Snowflake`, optional + #: :type: :obj:`~hikari.bases.Snowflake`, optional guild_id: typing.Optional[bases.Snowflake] = marshaller.attrib( deserializer=bases.Snowflake.deserialize, if_none=None ) #: A collection of the IDs of the messages that were deleted. #: - #: :type: :obj:`~typing.Set` [ :obj:`~hikari.entities.Snowflake` ] + #: :type: :obj:`~typing.Set` [ :obj:`~hikari.bases.Snowflake` ] message_ids: typing.Set[bases.Snowflake] = marshaller.attrib( raw_name="ids", deserializer=lambda msgs: {bases.Snowflake.deserialize(m) for m in msgs} ) @@ -977,23 +977,23 @@ class MessageReactionAddEvent(HikariEvent, marshaller.Deserializable): #: The ID of the user adding the reaction. #: - #: :type: :obj:`~hikari.entities.Snowflake` + #: :type: :obj:`~hikari.bases.Snowflake` user_id: bases.Snowflake = marshaller.attrib(deserializer=bases.Snowflake.deserialize) #: The ID of the channel where this reaction is being added. #: - #: :type: :obj:`~hikari.entities.Snowflake` + #: :type: :obj:`~hikari.bases.Snowflake` channel_id: bases.Snowflake = marshaller.attrib(deserializer=bases.Snowflake.deserialize) #: The ID of the message this reaction is being added to. #: - #: :type: :obj:`~hikari.entities.Snowflake` + #: :type: :obj:`~hikari.bases.Snowflake` message_id: bases.Snowflake = marshaller.attrib(deserializer=bases.Snowflake.deserialize) #: The ID of the guild where this reaction is being added, unless this is #: happening in a DM channel. #: - #: :type: :obj:`~hikari.entities.Snowflake`, optional + #: :type: :obj:`~hikari.bases.Snowflake`, optional guild_id: typing.Optional[bases.Snowflake] = marshaller.attrib( deserializer=bases.Snowflake.deserialize, if_undefined=None, default=None ) @@ -1021,23 +1021,23 @@ class MessageReactionRemoveEvent(HikariEvent, marshaller.Deserializable): #: The ID of the user who is removing their reaction. #: - #: :type: :obj:`~hikari.entities.Snowflake` + #: :type: :obj:`~hikari.bases.Snowflake` user_id: bases.Snowflake = marshaller.attrib(deserializer=bases.Snowflake.deserialize) #: The ID of the channel where this reaction is being removed. #: - #: :type: :obj:`~hikari.entities.Snowflake` + #: :type: :obj:`~hikari.bases.Snowflake` channel_id: bases.Snowflake = marshaller.attrib(deserializer=bases.Snowflake.deserialize) #: The ID of the message this reaction is being removed from. #: - #: :type: :obj:`~hikari.entities.Snowflake` + #: :type: :obj:`~hikari.bases.Snowflake` message_id: bases.Snowflake = marshaller.attrib(deserializer=bases.Snowflake.deserialize) #: The ID of the guild where this reaction is being removed, unless this is #: happening in a DM channel. #: - #: :type: :obj:`~hikari.entities.Snowflake`, optional + #: :type: :obj:`~hikari.bases.Snowflake`, optional guild_id: typing.Optional[bases.Snowflake] = marshaller.attrib( deserializer=bases.Snowflake.deserialize, if_undefined=None, default=None ) @@ -1060,17 +1060,17 @@ class MessageReactionRemoveAllEvent(HikariEvent, marshaller.Deserializable): #: The ID of the channel where the targeted message is. #: - #: :type: :obj:`~hikari.entities.Snowflake` + #: :type: :obj:`~hikari.bases.Snowflake` channel_id: bases.Snowflake = marshaller.attrib(deserializer=bases.Snowflake.deserialize) #: The ID of the message all reactions are being removed from. #: - #: :type: :obj:`~hikari.entities.Snowflake` + #: :type: :obj:`~hikari.bases.Snowflake` message_id: bases.Snowflake = marshaller.attrib(deserializer=bases.Snowflake.deserialize) #: The ID of the guild where the targeted message is, if applicable. #: - #: :type: :obj:`~hikari.entities.Snowflake` + #: :type: :obj:`~hikari.bases.Snowflake` guild_id: typing.Optional[bases.Snowflake] = marshaller.attrib( deserializer=bases.Snowflake.deserialize, if_undefined=None, default=None ) @@ -1086,19 +1086,19 @@ class MessageReactionRemoveEmojiEvent(HikariEvent, marshaller.Deserializable): #: The ID of the channel where the targeted message is. #: - #: :type: :obj:`~hikari.entities.Snowflake` + #: :type: :obj:`~hikari.bases.Snowflake` channel_id: bases.Snowflake = marshaller.attrib(deserializer=bases.Snowflake.deserialize) #: The ID of the guild where the targeted message is, if applicable. #: - #: :type: :obj:`~hikari.entities.Snowflake` + #: :type: :obj:`~hikari.bases.Snowflake` guild_id: typing.Optional[bases.Snowflake] = marshaller.attrib( deserializer=bases.Snowflake.deserialize, if_undefined=None, default=None ) #: The ID of the message the reactions are being removed from. #: - #: :type: :obj:`~hikari.entities.Snowflake` + #: :type: :obj:`~hikari.bases.Snowflake` message_id: bases.Snowflake = marshaller.attrib(deserializer=bases.Snowflake.deserialize) #: The object of the emoji that's being removed. @@ -1128,20 +1128,20 @@ class TypingStartEvent(HikariEvent, marshaller.Deserializable): #: The ID of the channel this typing event is occurring in. #: - #: :type: :obj:`~hikari.entities.Snowflake` + #: :type: :obj:`~hikari.bases.Snowflake` channel_id: bases.Snowflake = marshaller.attrib(deserializer=bases.Snowflake.deserialize) #: The ID of the guild this typing event is occurring in. #: Will be :obj:`~None` if this event is happening in a DM channel. #: - #: :type: :obj:`~hikari.entities.Snowflake`, optional + #: :type: :obj:`~hikari.bases.Snowflake`, optional guild_id: typing.Optional[bases.Snowflake] = marshaller.attrib( deserializer=bases.Snowflake.deserialize, if_undefined=None, default=None ) #: The ID of the user who triggered this typing event. #: - #: :type: :obj:`~hikari.entities.Snowflake` + #: :type: :obj:`~hikari.bases.Snowflake` user_id: bases.Snowflake = marshaller.attrib(deserializer=bases.Snowflake.deserialize) #: The datetime of when this typing event started. @@ -1194,7 +1194,7 @@ class VoiceServerUpdateEvent(HikariEvent, marshaller.Deserializable): #: The ID of the guild this voice server update is for #: - #: :type: :obj:`~hikari.entities.Snowflake` + #: :type: :obj:`~hikari.bases.Snowflake` guild_id: bases.Snowflake = marshaller.attrib(deserializer=bases.Snowflake.deserialize) #: The uri for this voice server host. @@ -1213,10 +1213,10 @@ class WebhookUpdateEvent(HikariEvent, marshaller.Deserializable): #: The ID of the guild this webhook is being updated in. #: - #: :type: :obj:`~hikari.entities.Snowflake` + #: :type: :obj:`~hikari.bases.Snowflake` guild_id: bases.Snowflake = marshaller.attrib(deserializer=bases.Snowflake.deserialize) #: The ID of the channel this webhook is being updated in. #: - #: :type: :obj:`~hikari.entities.Snowflake` + #: :type: :obj:`~hikari.bases.Snowflake` channel_id: bases.Snowflake = marshaller.attrib(deserializer=bases.Snowflake.deserialize) diff --git a/hikari/guilds.py b/hikari/guilds.py index 5578aef9b6..15599a3c68 100644 --- a/hikari/guilds.py +++ b/hikari/guilds.py @@ -203,7 +203,7 @@ class GuildEmbed(bases.HikariEntity, marshaller.Deserializable): #: The ID of the channel the invite for this embed targets, if enabled #: - #: :type: :obj:`~hikari.entities.Snowflake`, optional + #: :type: :obj:`~hikari.bases.Snowflake`, optional channel_id: typing.Optional[bases.Snowflake] = marshaller.attrib( deserializer=bases.Snowflake.deserialize, serializer=str, if_none=None ) @@ -236,7 +236,7 @@ class GuildMember(bases.HikariEntity, marshaller.Deserializable): #: A sequence of the IDs of the member's current roles. #: - #: :type: :obj:`~typing.Sequence` [ :obj:`~hikari.entities.Snowflake` ] + #: :type: :obj:`~typing.Sequence` [ :obj:`~hikari.bases.Snowflake` ] role_ids: typing.Sequence[bases.Snowflake] = marshaller.attrib( raw_name="roles", deserializer=lambda role_ids: [bases.Snowflake.deserialize(rid) for rid in role_ids], ) @@ -505,7 +505,7 @@ class PresenceActivity(bases.HikariEntity, marshaller.Deserializable): #: The ID of the application this activity is for, if applicable. #: - #: :type: :obj:`~hikari.entities.Snowflake`, optional + #: :type: :obj:`~hikari.bases.Snowflake`, optional application_id: typing.Optional[bases.Snowflake] = marshaller.attrib( deserializer=bases.Snowflake.deserialize, if_undefined=None, default=None ) @@ -731,14 +731,14 @@ class GuildMemberPresence(bases.HikariEntity, marshaller.Deserializable): #: A sequence of the ids of the user's current roles in the guild this #: presence belongs to. #: - #: :type: :obj:`~typing.Sequence` [ :obj:`~hikari.entities.Snowflake` ] + #: :type: :obj:`~typing.Sequence` [ :obj:`~hikari.bases.Snowflake` ] role_ids: typing.Sequence[bases.Snowflake] = marshaller.attrib( raw_name="roles", deserializer=lambda roles: [bases.Snowflake.deserialize(rid) for rid in roles], ) #: The ID of the guild this presence belongs to. #: - #: :type: :obj:`~hikari.entities.Snowflake` + #: :type: :obj:`~hikari.bases.Snowflake` guild_id: bases.Snowflake = marshaller.attrib(deserializer=bases.Snowflake.deserialize) #: This user's current status being displayed by the client. @@ -840,7 +840,7 @@ class GuildIntegration(bases.UniqueEntity, marshaller.Deserializable): #: The ID of the managed role used for this integration's subscribers. #: - #: :type: :obj:`~hikari.entities.Snowflake` + #: :type: :obj:`~hikari.bases.Snowflake` role_id: typing.Optional[bases.Snowflake] = marshaller.attrib(deserializer=bases.Snowflake.deserialize) #: Whether users under this integration are allowed to use it's custom @@ -992,7 +992,7 @@ class GuildPreview(PartialGuild): #: The emojis that this guild provides, represented as a mapping of ID to #: emoji object. #: - #: :type: :obj:`~typing.Mapping` [ :obj:`~hikari.entities.Snowflake`, :obj:`~hikari.emojis.GuildEmoji` ] + #: :type: :obj:`~typing.Mapping` [ :obj:`~hikari.bases.Snowflake`, :obj:`~hikari.emojis.GuildEmoji` ] emojis: typing.Mapping[bases.Snowflake, _emojis.GuildEmoji] = marshaller.attrib( deserializer=lambda emojis: {e.id: e for e in map(_emojis.GuildEmoji.deserialize, emojis)}, ) @@ -1106,7 +1106,7 @@ class Guild(PartialGuild): #: The ID of the owner of this guild. #: - #: :type: :obj:`~hikari.entities.Snowflake` + #: :type: :obj:`~hikari.bases.Snowflake` owner_id: bases.Snowflake = marshaller.attrib(deserializer=bases.Snowflake.deserialize) #: The guild level permissions that apply to the bot user, @@ -1126,7 +1126,7 @@ class Guild(PartialGuild): #: The ID for the channel that AFK voice users get sent to, if set for the #: guild. #: - #: :type: :obj:`~hikari.entities.Snowflake`, optional + #: :type: :obj:`~hikari.bases.Snowflake`, optional afk_channel_id: typing.Optional[bases.Snowflake] = marshaller.attrib( deserializer=bases.Snowflake.deserialize, if_none=None ) @@ -1155,7 +1155,7 @@ class Guild(PartialGuild): #: #: Will be :obj:`~None` if invites are disabled for this guild's embed. #: - #: :type: :obj:`~hikari.entities.Snowflake`, optional + #: :type: :obj:`~hikari.bases.Snowflake`, optional embed_channel_id: typing.Optional[bases.Snowflake] = marshaller.attrib( deserializer=bases.Snowflake.deserialize, if_undefined=None, if_none=None, default=None ) @@ -1181,7 +1181,7 @@ class Guild(PartialGuild): #: The roles in this guild, represented as a mapping of ID to role object. #: - #: :type: :obj:`~typing.Mapping` [ :obj:`~hikari.entities.Snowflake`, :obj:`~GuildRole` ] + #: :type: :obj:`~typing.Mapping` [ :obj:`~hikari.bases.Snowflake`, :obj:`~GuildRole` ] roles: typing.Mapping[bases.Snowflake, GuildRole] = marshaller.attrib( deserializer=lambda roles: {r.id: r for r in map(GuildRole.deserialize, roles)}, ) @@ -1189,7 +1189,7 @@ class Guild(PartialGuild): #: The emojis that this guild provides, represented as a mapping of ID to #: emoji object. #: - #: :type: :obj:`~typing.Mapping` [ :obj:`~hikari.entities.Snowflake`, :obj:`~hikari.emojis.GuildEmoji` ] + #: :type: :obj:`~typing.Mapping` [ :obj:`~hikari.bases.Snowflake`, :obj:`~hikari.emojis.GuildEmoji` ] emojis: typing.Mapping[bases.Snowflake, _emojis.GuildEmoji] = marshaller.attrib( deserializer=lambda emojis: {e.id: e for e in map(_emojis.GuildEmoji.deserialize, emojis)}, ) @@ -1202,7 +1202,7 @@ class Guild(PartialGuild): #: The ID of the application that created this guild, if it was created by #: a bot. If not, this is always :obj:`~None`. #: - #: :type: :obj:`~hikari.entities.Snowflake`, optional + #: :type: :obj:`~hikari.bases.Snowflake`, optional application_id: typing.Optional[bases.Snowflake] = marshaller.attrib( deserializer=bases.Snowflake.deserialize, if_none=None ) @@ -1233,7 +1233,7 @@ class Guild(PartialGuild): #: The channel ID that the widget's generated invite will send the user to, #: if enabled. If this information is unavailable, this will be :obj:`~None`. #: - #: :type: :obj:`~hikari.entities.Snowflake`, optional + #: :type: :obj:`~hikari.bases.Snowflake`, optional widget_channel_id: typing.Optional[bases.Snowflake] = marshaller.attrib( deserializer=bases.Snowflake.deserialize, if_undefined=None, if_none=None, default=None ) @@ -1241,7 +1241,7 @@ class Guild(PartialGuild): #: The ID of the system channel (where welcome messages and Nitro boost #: messages are sent), or :obj:`~None` if it is not enabled. #: - #: :type: :obj:`~hikari.entities.Snowflake`, optional + #: :type: :obj:`~hikari.bases.Snowflake`, optional system_channel_id: typing.Optional[bases.Snowflake] = marshaller.attrib( if_none=None, deserializer=bases.Snowflake.deserialize ) @@ -1258,7 +1258,7 @@ class Guild(PartialGuild): #: If the :obj:`~GuildFeature.PUBLIC` feature is not defined, then this is #: :obj:`~None`. #: - #: :type: :obj:`~hikari.entities.Snowflake`, optional + #: :type: :obj:`~hikari.bases.Snowflake`, optional rules_channel_id: typing.Optional[bases.Snowflake] = marshaller.attrib( if_none=None, deserializer=bases.Snowflake.deserialize ) @@ -1314,7 +1314,7 @@ class Guild(PartialGuild): #: representation. If you need complete accurate information, you should #: query the members using the appropriate API call instead. #: - #: :type: :obj:`~typing.Mapping` [ :obj:`~hikari.entities.Snowflake`, :obj:`~GuildMember` ], optional + #: :type: :obj:`~typing.Mapping` [ :obj:`~hikari.bases.Snowflake`, :obj:`~GuildMember` ], optional members: typing.Optional[typing.Mapping[bases.Snowflake, GuildMember]] = marshaller.attrib( deserializer=lambda members: {m.user.id: m for m in map(GuildMember.deserialize, members)}, if_undefined=None, @@ -1336,7 +1336,7 @@ class Guild(PartialGuild): #: To retrieve a list of channels in any other case, you should make an #: appropriate API call to retrieve this information. #: - #: :type: :obj:`~typing.Mapping` [ :obj:`~hikari.entities.Snowflake`, :obj:`~hikari.channels.GuildChannel` ], optional + #: :type: :obj:`~typing.Mapping` [ :obj:`~hikari.bases.Snowflake`, :obj:`~hikari.channels.GuildChannel` ], optional channels: typing.Optional[typing.Mapping[bases.Snowflake, _channels.GuildChannel]] = marshaller.attrib( deserializer=lambda guild_channels: {c.id: c for c in map(_channels.deserialize_channel, guild_channels)}, if_undefined=None, @@ -1359,7 +1359,7 @@ class Guild(PartialGuild): #: To retrieve a list of presences in any other case, you should make an #: appropriate API call to retrieve this information. #: - #: :type: :obj:`~typing.Mapping` [ :obj:`~hikari.entities.Snowflake`, :obj:`~GuildMemberPresence` ], optional + #: :type: :obj:`~typing.Mapping` [ :obj:`~hikari.bases.Snowflake`, :obj:`~GuildMemberPresence` ], optional presences: typing.Optional[typing.Mapping[bases.Snowflake, GuildMemberPresence]] = marshaller.attrib( deserializer=lambda presences: {p.user.id: p for p in map(GuildMemberPresence.deserialize, presences)}, if_undefined=None, @@ -1436,7 +1436,7 @@ class Guild(PartialGuild): #: ``features`` for this guild. For all other purposes, it should be #: considered to be :obj:`~None`. #: - #: :type: :obj:`~hikari.entities.Snowflake`, optional + #: :type: :obj:`~hikari.bases.Snowflake`, optional public_updates_channel_id: typing.Optional[bases.Snowflake] = marshaller.attrib( if_none=None, deserializer=bases.Snowflake.deserialize ) diff --git a/hikari/internal/allowed_mentions.py b/hikari/internal/allowed_mentions.py index 7121a9df90..4d10732893 100644 --- a/hikari/internal/allowed_mentions.py +++ b/hikari/internal/allowed_mentions.py @@ -44,11 +44,11 @@ def generate_allowed_mentions( mentions_everyone : :obj:`~bool` Whether ``@everyone`` and ``@here`` mentions should be resolved by discord and lead to actual pings. - user_mentions : :obj:`~typing.Union` [ :obj:`~typing.Collection` [ :obj:`~typing.Union` [ :obj:`~hikari.users.User`, :obj:`~hikari.entities.Snowflake`, :obj:`~int` ], :obj:`~bool` ] + user_mentions : :obj:`~typing.Union` [ :obj:`~typing.Collection` [ :obj:`~typing.Union` [ :obj:`~hikari.users.User`, :obj:`~hikari.bases.Snowflake`, :obj:`~int` ], :obj:`~bool` ] Either an array of user objects/IDs to allow mentions for, :obj:`~True` to allow all user mentions or :obj:`~False` to block all user mentions from resolving. - role_mentions : :obj:`~typing.Union` [ :obj:`~typing.Collection` [ :obj:`~typing.Union` [ :obj:`~hikari.guilds.GuildRole`, :obj:`~hikari.entities.Snowflake`, :obj:`~int` ] ], :obj:`~bool` ] + role_mentions : :obj:`~typing.Union` [ :obj:`~typing.Collection` [ :obj:`~typing.Union` [ :obj:`~hikari.guilds.GuildRole`, :obj:`~hikari.bases.Snowflake`, :obj:`~int` ] ], :obj:`~bool` ] Either an array of guild role objects/IDs to allow mentions for, :obj:`~True` to allow all role mentions or :obj:`~False` to block all role mentions from resolving. diff --git a/hikari/messages.py b/hikari/messages.py index c3b175119f..48f285cfd9 100644 --- a/hikari/messages.py +++ b/hikari/messages.py @@ -219,14 +219,14 @@ class MessageCrosspost(bases.HikariEntity, marshaller.Deserializable): #: currently documented. #: #: - #: :type: :obj:`~hikari.entities.Snowflake`, optional + #: :type: :obj:`~hikari.bases.Snowflake`, optional message_id: typing.Optional[bases.Snowflake] = marshaller.attrib( deserializer=bases.Snowflake.deserialize, if_undefined=None, default=None ) #: The ID of the channel that the message originated from. #: - #: :type: :obj:`~hikari.entities.Snowflake` + #: :type: :obj:`~hikari.bases.Snowflake` channel_id: bases.Snowflake = marshaller.attrib(deserializer=bases.Snowflake.deserialize) #: The ID of the guild that the message originated from. @@ -237,7 +237,7 @@ class MessageCrosspost(bases.HikariEntity, marshaller.Deserializable): #: documentation, but the situations that cause this to occur are not #: currently documented. #: - #: :type: :obj:`~hikari.entities.Snowflake`, optional + #: :type: :obj:`~hikari.bases.Snowflake`, optional guild_id: typing.Optional[bases.Snowflake] = marshaller.attrib( deserializer=bases.Snowflake.deserialize, if_undefined=None, default=None ) @@ -250,12 +250,12 @@ class Message(bases.UniqueEntity, marshaller.Deserializable): #: The ID of the channel that the message was sent in. #: - #: :type: :obj:`~hikari.entities.Snowflake` + #: :type: :obj:`~hikari.bases.Snowflake` channel_id: bases.Snowflake = marshaller.attrib(deserializer=bases.Snowflake.deserialize) #: The ID of the guild that the message was sent in. #: - #: :type: :obj:`~hikari.entities.Snowflake`, optional + #: :type: :obj:`~hikari.bases.Snowflake`, optional guild_id: typing.Optional[bases.Snowflake] = marshaller.attrib( deserializer=bases.Snowflake.deserialize, if_undefined=None, default=None ) @@ -302,7 +302,7 @@ class Message(bases.UniqueEntity, marshaller.Deserializable): #: The users the message mentions. #: - #: :type: :obj:`~typing.Set` [ :obj:`~hikari.entities.Snowflake` ] + #: :type: :obj:`~typing.Set` [ :obj:`~hikari.bases.Snowflake` ] user_mentions: typing.Set[bases.Snowflake] = marshaller.attrib( raw_name="mentions", deserializer=lambda user_mentions: {bases.Snowflake.deserialize(u["id"]) for u in user_mentions}, @@ -310,7 +310,7 @@ class Message(bases.UniqueEntity, marshaller.Deserializable): #: The roles the message mentions. #: - #: :type: :obj:`~typing.Set` [ :obj:`~hikari.entities.Snowflake` ] + #: :type: :obj:`~typing.Set` [ :obj:`~hikari.bases.Snowflake` ] role_mentions: typing.Set[bases.Snowflake] = marshaller.attrib( raw_name="mention_roles", deserializer=lambda role_mentions: {bases.Snowflake.deserialize(mention) for mention in role_mentions}, @@ -318,7 +318,7 @@ class Message(bases.UniqueEntity, marshaller.Deserializable): #: The channels the message mentions. #: - #: :type: :obj:`~typing.Set` [ :obj:`~hikari.entities.Snowflake` ] + #: :type: :obj:`~typing.Set` [ :obj:`~hikari.bases.Snowflake` ] channel_mentions: typing.Set[bases.Snowflake] = marshaller.attrib( raw_name="mention_channels", deserializer=lambda channel_mentions: {bases.Snowflake.deserialize(c["id"]) for c in channel_mentions}, @@ -354,7 +354,7 @@ class Message(bases.UniqueEntity, marshaller.Deserializable): #: If the message was generated by a webhook, the webhook's id. #: - #: :type: :obj:`~hikari.entities.Snowflake`, optional + #: :type: :obj:`~hikari.bases.Snowflake`, optional webhook_id: typing.Optional[bases.Snowflake] = marshaller.attrib( deserializer=bases.Snowflake.deserialize, if_undefined=None, default=None ) diff --git a/hikari/voices.py b/hikari/voices.py index 729f6cd208..1d50583388 100644 --- a/hikari/voices.py +++ b/hikari/voices.py @@ -35,7 +35,7 @@ class VoiceState(bases.HikariEntity, marshaller.Deserializable): #: The ID of the guild this voice state is in, if applicable. #: - #: :type: :obj:`~hikari.entities.Snowflake`, optional + #: :type: :obj:`~hikari.bases.Snowflake`, optional guild_id: typing.Optional[bases.Snowflake] = marshaller.attrib( deserializer=bases.Snowflake.deserialize, if_undefined=None, default=None ) @@ -43,14 +43,14 @@ class VoiceState(bases.HikariEntity, marshaller.Deserializable): #: The ID of the channel this user is connected to, will be :obj:`~None` if #: they are leaving voice. #: - #: :type: :obj:`~hikari.entities.Snowflake`, optional + #: :type: :obj:`~hikari.bases.Snowflake`, optional channel_id: typing.Optional[bases.Snowflake] = marshaller.attrib( deserializer=bases.Snowflake.deserialize, if_none=None ) #: The ID of the user this voice state is for. #: - #: :type: :obj:`~hikari.entities.Snowflake` + #: :type: :obj:`~hikari.bases.Snowflake` user_id: bases.Snowflake = marshaller.attrib(deserializer=bases.Snowflake.deserialize) #: The guild member this voice state is for if the voice state is in a diff --git a/hikari/webhooks.py b/hikari/webhooks.py index 5a338c3b4e..39b2553ce2 100644 --- a/hikari/webhooks.py +++ b/hikari/webhooks.py @@ -57,14 +57,14 @@ class Webhook(bases.UniqueEntity, marshaller.Deserializable): #: The guild ID of the webhook. #: - #: :type: :obj:`~hikari.entities.Snowflake`, optional + #: :type: :obj:`~hikari.bases.Snowflake`, optional guild_id: typing.Optional[bases.Snowflake] = marshaller.attrib( deserializer=bases.Snowflake.deserialize, if_undefined=None, default=None ) #: The channel ID this webhook is for. #: - #: :type: :obj:`~hikari.entities.Snowflake` + #: :type: :obj:`~hikari.bases.Snowflake` channel_id: bases.Snowflake = marshaller.attrib(deserializer=bases.Snowflake.deserialize) #: The user that created the webhook From dc19b78800f5507e5b75a544ecaf3541cb0f18e0 Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Tue, 21 Apr 2020 10:47:35 +0100 Subject: [PATCH 180/922] Added intent attributes to events. --- hikari/events.py | 101 ++++++++++++++++++++++++++++++---------------- hikari/intents.py | 10 +++++ 2 files changed, 76 insertions(+), 35 deletions(-) diff --git a/hikari/events.py b/hikari/events.py index ea7c3e7af4..e0a97655d2 100644 --- a/hikari/events.py +++ b/hikari/events.py @@ -65,6 +65,7 @@ "WebhookUpdateEvent", ] +import abc import datetime import typing @@ -75,6 +76,7 @@ from hikari import embeds as _embeds from hikari import emojis as _emojis from hikari import guilds +from hikari import intents from hikari import invites from hikari import messages from hikari import applications @@ -82,6 +84,7 @@ from hikari import users from hikari import voices from hikari.clients import shards +from hikari.internal import assertions from hikari.internal import conversions from hikari.internal import marshaller @@ -91,9 +94,29 @@ # Base event, is not deserialized @marshaller.marshallable() @attr.s(slots=True, kw_only=True) -class HikariEvent(bases.HikariEntity): +class HikariEvent(bases.HikariEntity, abc.ABC): """The base class that all events inherit from.""" + ___required_intents___: typing.ClassVar[intents.Intent] + + def __init_subclass__(cls, *, intent: intents.Intent = intents.Intent(0), **kwargs) -> None: + super().__init_subclass__() + assertions.assert_that(isinstance(intent, intents.Intent), "expected an intent type", TypeError) + cls.___required_intents___ = intent + + if intent: + new_doc = (cls.__doc__ + "\n\n") if cls.__doc__ is not None else "" + new_doc += "This event will only fire if the following intents are enabled: " + new_doc += ", ".join(i.name for i in intents.Intent if i & intent) + if intent.is_privileged: + new_doc += "\n\n" + new_doc += ( + "This event is privileged, meaning you will need to opt into using one or more " + "intents on the Discord Developer Portal." + ) + + cls.__doc__ = new_doc + # Synthetic event, is not deserialized, and is produced by the dispatcher. @attr.attrs(slots=True, auto_attribs=True) @@ -228,7 +251,9 @@ def shard_count(self) -> typing.Optional[int]: @marshaller.marshallable() @attr.s(slots=True, kw_only=True) -class BaseChannelEvent(HikariEvent, bases.UniqueEntity, marshaller.Deserializable): +class BaseChannelEvent( + HikariEvent, bases.UniqueEntity, marshaller.Deserializable, abc.ABC, intent=intents.Intent.GUILDS +): """A base object that Channel events will inherit from.""" #: The channel's type. @@ -351,7 +376,7 @@ class BaseChannelEvent(HikariEvent, bases.UniqueEntity, marshaller.Deserializabl @marshaller.marshallable() @attr.s(slots=True, kw_only=True) -class ChannelCreateEvent(BaseChannelEvent): +class ChannelCreateEvent(BaseChannelEvent, intent=intents.Intent.GUILDS): """Represents Channel Create gateway events. Will be sent when a guild channel is created and before all Create Message @@ -361,19 +386,19 @@ class ChannelCreateEvent(BaseChannelEvent): @marshaller.marshallable() @attr.s(slots=True, kw_only=True) -class ChannelUpdateEvent(BaseChannelEvent): +class ChannelUpdateEvent(BaseChannelEvent, intent=intents.Intent.GUILDS): """Represents Channel Update gateway events.""" @marshaller.marshallable() @attr.s(slots=True, kw_only=True) -class ChannelDeleteEvent(BaseChannelEvent): +class ChannelDeleteEvent(BaseChannelEvent, intent=intents.Intent.GUILDS): """Represents Channel Delete gateway events.""" @marshaller.marshallable() @attr.s(slots=True, kw_only=True) -class ChannelPinUpdateEvent(HikariEvent, marshaller.Deserializable): +class ChannelPinUpdateEvent(HikariEvent, marshaller.Deserializable, intent=intents.Intent.GUILDS): """Used to represent the Channel Pins Update gateway event. Sent when a message is pinned or unpinned in a channel but not @@ -404,7 +429,7 @@ class ChannelPinUpdateEvent(HikariEvent, marshaller.Deserializable): @marshaller.marshallable() @attr.s(slots=True, kw_only=True) -class GuildCreateEvent(HikariEvent, marshaller.Deserializable): +class GuildCreateEvent(HikariEvent, marshaller.Deserializable, intent=intents.Intent.GUILDS): """Used to represent Guild Create gateway events. Will be received when the bot joins a guild, and when a guild becomes @@ -414,13 +439,13 @@ class GuildCreateEvent(HikariEvent, marshaller.Deserializable): @marshaller.marshallable() @attr.s(slots=True, kw_only=True) -class GuildUpdateEvent(HikariEvent, marshaller.Deserializable): +class GuildUpdateEvent(HikariEvent, marshaller.Deserializable, intent=intents.Intent.GUILDS): """Used to represent Guild Update gateway events.""" @marshaller.marshallable() @attr.s(slots=True, kw_only=True) -class GuildLeaveEvent(HikariEvent, bases.UniqueEntity, marshaller.Deserializable): +class GuildLeaveEvent(HikariEvent, bases.UniqueEntity, marshaller.Deserializable, intent=intents.Intent.GUILDS): """Fired when the current user leaves the guild or is kicked/banned from it. Notes @@ -431,7 +456,7 @@ class GuildLeaveEvent(HikariEvent, bases.UniqueEntity, marshaller.Deserializable @marshaller.marshallable() @attr.s(slots=True, kw_only=True) -class GuildUnavailableEvent(HikariEvent, bases.UniqueEntity, marshaller.Deserializable): +class GuildUnavailableEvent(HikariEvent, bases.UniqueEntity, marshaller.Deserializable, intent=intents.Intent.GUILDS): """Fired when a guild becomes temporarily unavailable due to an outage. Notes @@ -442,7 +467,7 @@ class GuildUnavailableEvent(HikariEvent, bases.UniqueEntity, marshaller.Deserial @marshaller.marshallable() @attr.s(slots=True, kw_only=True) -class BaseGuildBanEvent(HikariEvent, marshaller.Deserializable): +class BaseGuildBanEvent(HikariEvent, marshaller.Deserializable, abc.ABC, intent=intents.Intent.GUILD_BANS): """A base object that guild ban events will inherit from.""" #: The ID of the guild this ban is in. @@ -458,19 +483,19 @@ class BaseGuildBanEvent(HikariEvent, marshaller.Deserializable): @marshaller.marshallable() @attr.s(slots=True, kw_only=True) -class GuildBanAddEvent(BaseGuildBanEvent): +class GuildBanAddEvent(BaseGuildBanEvent, intent=intents.Intent.GUILD_BANS): """Used to represent a Guild Ban Add gateway event.""" @marshaller.marshallable() @attr.s(slots=True, kw_only=True) -class GuildBanRemoveEvent(BaseGuildBanEvent): +class GuildBanRemoveEvent(BaseGuildBanEvent, intent=intents.Intent.GUILD_BANS): """Used to represent a Guild Ban Remove gateway event.""" @marshaller.marshallable() @attr.s(slots=True, kw_only=True) -class GuildEmojisUpdateEvent(HikariEvent, marshaller.Deserializable): +class GuildEmojisUpdateEvent(HikariEvent, marshaller.Deserializable, intent=intents.Intent.GUILD_EMOJIS): """Represents a Guild Emoji Update gateway event.""" #: The ID of the guild this emoji was updated in. @@ -488,7 +513,7 @@ class GuildEmojisUpdateEvent(HikariEvent, marshaller.Deserializable): @marshaller.marshallable() @attr.s(slots=True, kw_only=True) -class GuildIntegrationsUpdateEvent(HikariEvent, marshaller.Deserializable): +class GuildIntegrationsUpdateEvent(HikariEvent, marshaller.Deserializable, intent=intents.Intent.GUILD_INTEGRATIONS): """Used to represent Guild Integration Update gateway events.""" #: The ID of the guild the integration was updated in. @@ -499,7 +524,7 @@ class GuildIntegrationsUpdateEvent(HikariEvent, marshaller.Deserializable): @marshaller.marshallable() @attr.s(slots=True, kw_only=True) -class GuildMemberAddEvent(HikariEvent, guilds.GuildMember): +class GuildMemberAddEvent(HikariEvent, guilds.GuildMember, intent=intents.Intent.GUILD_MEMBERS): """Used to represent a Guild Member Add gateway event.""" #: The ID of the guild where this member was added. @@ -510,7 +535,7 @@ class GuildMemberAddEvent(HikariEvent, guilds.GuildMember): @marshaller.marshallable() @attr.s(slots=True, kw_only=True) -class GuildMemberUpdateEvent(HikariEvent, marshaller.Deserializable): +class GuildMemberUpdateEvent(HikariEvent, marshaller.Deserializable, intent=intents.Intent.GUILD_MEMBERS): """Used to represent a Guild Member Update gateway event. Sent when a guild member or their inner user object is updated. @@ -552,7 +577,7 @@ class GuildMemberUpdateEvent(HikariEvent, marshaller.Deserializable): @marshaller.marshallable() @attr.s(slots=True, kw_only=True) -class GuildMemberRemoveEvent(HikariEvent, marshaller.Deserializable): +class GuildMemberRemoveEvent(HikariEvent, marshaller.Deserializable, intent=intents.Intent.GUILD_MEMBERS): """Used to represent Guild Member Remove gateway events. Sent when a member is kicked, banned or leaves a guild. @@ -571,7 +596,7 @@ class GuildMemberRemoveEvent(HikariEvent, marshaller.Deserializable): @marshaller.marshallable() @attr.s(slots=True, kw_only=True) -class GuildRoleCreateEvent(HikariEvent, marshaller.Deserializable): +class GuildRoleCreateEvent(HikariEvent, marshaller.Deserializable, intent=intents.Intent.GUILDS): """Used to represent a Guild Role Create gateway event.""" #: The ID of the guild where this role was created. @@ -587,7 +612,7 @@ class GuildRoleCreateEvent(HikariEvent, marshaller.Deserializable): @marshaller.marshallable() @attr.s(slots=True, kw_only=True) -class GuildRoleUpdateEvent(HikariEvent, marshaller.Deserializable): +class GuildRoleUpdateEvent(HikariEvent, marshaller.Deserializable, intent=intents.Intent.GUILDS): """Used to represent a Guild Role Create gateway event.""" #: The ID of the guild where this role was updated. @@ -603,7 +628,7 @@ class GuildRoleUpdateEvent(HikariEvent, marshaller.Deserializable): @marshaller.marshallable() @attr.s(slots=True, kw_only=True) -class GuildRoleDeleteEvent(HikariEvent, marshaller.Deserializable): +class GuildRoleDeleteEvent(HikariEvent, marshaller.Deserializable, intent=intents.Intent.GUILDS): """Represents a gateway Guild Role Delete Event.""" #: The ID of the guild where this role is being deleted. @@ -619,7 +644,7 @@ class GuildRoleDeleteEvent(HikariEvent, marshaller.Deserializable): @marshaller.marshallable() @attr.s(slots=True, kw_only=True) -class InviteCreateEvent(HikariEvent, marshaller.Deserializable): +class InviteCreateEvent(HikariEvent, marshaller.Deserializable, intent=intents.Intent.GUILD_INVITES): """Represents a gateway Invite Create event.""" #: The ID of the channel this invite targets. @@ -693,7 +718,7 @@ class InviteCreateEvent(HikariEvent, marshaller.Deserializable): @marshaller.marshallable() @attr.s(slots=True, kw_only=True) -class InviteDeleteEvent(HikariEvent, marshaller.Deserializable): +class InviteDeleteEvent(HikariEvent, marshaller.Deserializable, intent=intents.Intent.GUILD_INVITES): """Used to represent Invite Delete gateway events. Sent when an invite is deleted for a channel we can access. @@ -720,14 +745,16 @@ class InviteDeleteEvent(HikariEvent, marshaller.Deserializable): @marshaller.marshallable() @attr.s(slots=True, kw_only=True) -class MessageCreateEvent(HikariEvent, messages.Message): +class MessageCreateEvent(HikariEvent, messages.Message, intent=intents.Intent.GUILD_MESSAGES): """Used to represent Message Create gateway events.""" # This is an arbitrarily partial version of `messages.Message` @marshaller.marshallable() @attr.s(slots=True, kw_only=True) -class MessageUpdateEvent(HikariEvent, bases.UniqueEntity, marshaller.Deserializable): +class MessageUpdateEvent( + HikariEvent, bases.UniqueEntity, marshaller.Deserializable, intent=intents.Intent.GUILD_MESSAGES +): """Represents Message Update gateway events. Note @@ -917,7 +944,7 @@ class MessageUpdateEvent(HikariEvent, bases.UniqueEntity, marshaller.Deserializa @marshaller.marshallable() @attr.s(slots=True, kw_only=True) -class MessageDeleteEvent(HikariEvent, marshaller.Deserializable): +class MessageDeleteEvent(HikariEvent, marshaller.Deserializable, intent=intents.Intent.GUILD_MESSAGES): """Used to represent Message Delete gateway events. Sent when a message is deleted in a channel we have access to. @@ -943,7 +970,7 @@ class MessageDeleteEvent(HikariEvent, marshaller.Deserializable): @marshaller.marshallable() @attr.s(slots=True, kw_only=True) -class MessageDeleteBulkEvent(HikariEvent, marshaller.Deserializable): +class MessageDeleteBulkEvent(HikariEvent, marshaller.Deserializable, intent=intents.Intent.GUILD_MESSAGES): """Used to represent Message Bulk Delete gateway events. Sent when multiple messages are deleted in a channel at once. @@ -972,7 +999,7 @@ class MessageDeleteBulkEvent(HikariEvent, marshaller.Deserializable): @marshaller.marshallable() @attr.s(slots=True, kw_only=True) -class MessageReactionAddEvent(HikariEvent, marshaller.Deserializable): +class MessageReactionAddEvent(HikariEvent, marshaller.Deserializable, intent=intents.Intent.GUILD_MESSAGE_REACTIONS): """Used to represent Message Reaction Add gateway events.""" #: The ID of the user adding the reaction. @@ -1016,7 +1043,7 @@ class MessageReactionAddEvent(HikariEvent, marshaller.Deserializable): @marshaller.marshallable() @attr.s(slots=True, kw_only=True) -class MessageReactionRemoveEvent(HikariEvent, marshaller.Deserializable): +class MessageReactionRemoveEvent(HikariEvent, marshaller.Deserializable, intent=intents.Intent.GUILD_MESSAGE_REACTIONS): """Used to represent Message Reaction Remove gateway events.""" #: The ID of the user who is removing their reaction. @@ -1052,7 +1079,9 @@ class MessageReactionRemoveEvent(HikariEvent, marshaller.Deserializable): @marshaller.marshallable() @attr.s(slots=True, kw_only=True) -class MessageReactionRemoveAllEvent(HikariEvent, marshaller.Deserializable): +class MessageReactionRemoveAllEvent( + HikariEvent, marshaller.Deserializable, intent=intents.Intent.GUILD_MESSAGE_REACTIONS +): """Used to represent Message Reaction Remove All gateway events. Sent when all the reactions are removed from a message, regardless of emoji. @@ -1078,7 +1107,9 @@ class MessageReactionRemoveAllEvent(HikariEvent, marshaller.Deserializable): @marshaller.marshallable() @attr.s(slots=True, kw_only=True) -class MessageReactionRemoveEmojiEvent(HikariEvent, marshaller.Deserializable): +class MessageReactionRemoveEmojiEvent( + HikariEvent, marshaller.Deserializable, intent=intents.Intent.GUILD_MESSAGE_REACTIONS +): """Represents Message Reaction Remove Emoji events. Sent when all the reactions for a single emoji are removed from a message. @@ -1111,7 +1142,7 @@ class MessageReactionRemoveEmojiEvent(HikariEvent, marshaller.Deserializable): @marshaller.marshallable() @attr.s(slots=True, kw_only=True) -class PresenceUpdateEvent(HikariEvent, guilds.GuildMemberPresence): +class PresenceUpdateEvent(HikariEvent, guilds.GuildMemberPresence, intent=intents.Intent.GUILD_PRESENCES): """Used to represent Presence Update gateway events. Sent when a guild member changes their presence. @@ -1120,7 +1151,7 @@ class PresenceUpdateEvent(HikariEvent, guilds.GuildMemberPresence): @marshaller.marshallable() @attr.s(slots=True, kw_only=True) -class TypingStartEvent(HikariEvent, marshaller.Deserializable): +class TypingStartEvent(HikariEvent, marshaller.Deserializable, intent=intents.Intent.GUILD_MESSAGE_TYPING): """Used to represent typing start gateway events. Received when a user or bot starts "typing" in a channel. @@ -1171,7 +1202,7 @@ class UserUpdateEvent(HikariEvent, users.MyUser): @marshaller.marshallable() @attr.s(slots=True, kw_only=True) -class VoiceStateUpdateEvent(HikariEvent, voices.VoiceState): +class VoiceStateUpdateEvent(HikariEvent, voices.VoiceState, intent=intents.Intent.GUILD_VOICE_STATES): """Used to represent voice state update gateway events. Sent when a user joins, leaves or moves voice channel(s). @@ -1205,7 +1236,7 @@ class VoiceServerUpdateEvent(HikariEvent, marshaller.Deserializable): @marshaller.marshallable() @attr.s(slots=True, kw_only=True) -class WebhookUpdateEvent(HikariEvent, marshaller.Deserializable): +class WebhookUpdateEvent(HikariEvent, marshaller.Deserializable, intent=intents.Intent.GUILD_MESSAGE_TYPING): """Used to represent webhook update gateway events. Sent when a webhook is updated, created or deleted in a guild. diff --git a/hikari/intents.py b/hikari/intents.py index e66fd619f6..688a30ae1d 100644 --- a/hikari/intents.py +++ b/hikari/intents.py @@ -143,3 +143,13 @@ class Intent(more_enums.FlagMixin, enum.IntFlag): #: Subscribes to the following events #: * TYPING_START DIRECT_MESSAGE_TYPING = 1 << 14 + + @property + def is_privileged(self) -> bool: + """Whether the intent requires elevated privileges. + + If this is ``True``, you will be required to opt-in to using this intent + on the Discord Developer Portal before you can utilise it in your + application. + """ + return bool(self & (Intent.GUILD_MEMBERS | Intent.GUILD_PRESENCES)) From 494f62c26717a09c3e3596e4f54fecc1a3e196a1 Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Tue, 21 Apr 2020 13:40:37 +0100 Subject: [PATCH 181/922] Fixes #309 and implements intent-detection and warning. Subscribing to an event not covered by the set intents you provide will now cause a warning to be raised. Fixed my commit hook to track files it reformats. --- hikari/__init__.py | 4 +- hikari/clients/__init__.py | 9 +- hikari/clients/{bot.py => bot_base.py} | 82 ++++---- hikari/clients/rest/__init__.py | 2 +- hikari/clients/rest/channel.py | 11 +- hikari/clients/rest/guild.py | 4 +- hikari/clients/rest/me.py | 8 +- hikari/clients/rest/react.py | 4 +- hikari/clients/rest/webhook.py | 4 +- hikari/clients/shards.py | 2 +- hikari/clients/stateless.py | 74 +++++++ hikari/errors.py | 23 ++- hikari/events.py | 181 +++++++++++------- .../{allowed_mentions.py => helpers.py} | 94 ++++++++- hikari/internal/pagination.py | 94 --------- hikari/messages.py | 2 +- hikari/state/dispatchers.py | 57 +++++- hikari/state/event_managers.py | 12 +- pre-commit | 3 +- .../hikari/clients/test_rest/test_channel.py | 44 ++--- tests/hikari/clients/test_rest/test_guild.py | 15 +- tests/hikari/clients/test_rest/test_me.py | 27 +-- tests/hikari/clients/test_rest/test_react.py | 14 +- .../hikari/clients/test_rest/test_webhook.py | 14 +- .../hikari/internal/test_allowed_mentions.py | 119 ------------ .../{test_pagination.py => test_helpers.py} | 108 ++++++++++- tests/hikari/state/test_event_dispatchers.py | 62 +++--- 27 files changed, 610 insertions(+), 463 deletions(-) rename hikari/clients/{bot.py => bot_base.py} (88%) create mode 100644 hikari/clients/stateless.py rename hikari/internal/{allowed_mentions.py => helpers.py} (54%) delete mode 100644 hikari/internal/pagination.py delete mode 100644 tests/hikari/internal/test_allowed_mentions.py rename tests/hikari/internal/{test_pagination.py => test_helpers.py} (63%) diff --git a/hikari/__init__.py b/hikari/__init__.py index 2a1c29a6bb..8b2faa13e9 100644 --- a/hikari/__init__.py +++ b/hikari/__init__.py @@ -17,6 +17,7 @@ # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . """Hikari's models framework for writing Discord bots in Python.""" +from hikari import applications from hikari import audit_logs from hikari import bases from hikari import channels @@ -33,7 +34,6 @@ from hikari import invites from hikari import messages from hikari import net -from hikari import applications from hikari import permissions from hikari import state from hikari import users @@ -45,6 +45,7 @@ from hikari._about import __license__ from hikari._about import __url__ from hikari._about import __version__ +from hikari.applications import * from hikari.audit_logs import * from hikari.bases import * from hikari.channels import * @@ -60,7 +61,6 @@ from hikari.invites import * from hikari.messages import * from hikari.net import * -from hikari.applications import * from hikari.permissions import * from hikari.state import * from hikari.unset import * diff --git a/hikari/clients/__init__.py b/hikari/clients/__init__.py index 1f6d9e2ec5..8bfeb67431 100644 --- a/hikari/clients/__init__.py +++ b/hikari/clients/__init__.py @@ -18,20 +18,23 @@ # along with Hikari. If not, see . """The models API for interacting with Discord directly.""" -from hikari.clients import bot +from hikari.clients import bot_base from hikari.clients import configs from hikari.clients import rest from hikari.clients import runnable -from hikari.clients.bot import * +from hikari.clients import stateless +from hikari.clients.bot_base import * from hikari.clients.configs import * from hikari.clients.rest import * from hikari.clients.runnable import * from hikari.clients.shards import * +from hikari.clients.stateless import * __all__ = [ - *bot.__all__, + *bot_base.__all__, *configs.__all__, *rest.__all__, *shards.__all__, *runnable.__all__, + *stateless.__all__, ] diff --git a/hikari/clients/bot.py b/hikari/clients/bot_base.py similarity index 88% rename from hikari/clients/bot.py rename to hikari/clients/bot_base.py index c872c8ec40..da5da1dfaa 100644 --- a/hikari/clients/bot.py +++ b/hikari/clients/bot_base.py @@ -17,7 +17,7 @@ # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . """A bot client might go here... eventually...""" -__all__ = ["BotBase", "StatelessBot"] +__all__ = ["BotBase"] import abc import asyncio @@ -40,16 +40,16 @@ from hikari.internal import more_typing from hikari.state import dispatchers from hikari.state import event_managers -from hikari.state import stateless ShardClientT = typing.TypeVar("ShardClientT", bound=shards.ShardClient) EventManagerT = typing.TypeVar("EventManagerT", bound=event_managers.EventManager) +EventDispatcherT = typing.TypeVar("EventDispatcherT", bound=dispatchers.EventDispatcher) RESTClientT = typing.TypeVar("RESTClientT", bound=rest.RESTClient) BotConfigT = typing.TypeVar("BotConfigT", bound=configs.BotConfig) class BotBase( - typing.Generic[ShardClientT, RESTClientT, EventManagerT, BotConfigT], + typing.Generic[ShardClientT, RESTClientT, EventDispatcherT, EventManagerT, BotConfigT], runnable.RunnableClient, dispatchers.EventDispatcher, abc.ABC, @@ -61,6 +61,11 @@ class BotBase( :obj:`~hikari.clients.shard_clients.ShardClient` to use for shards. - ``RESTClientT`` - the implementation of :obj:`~hikari.clients.rest.RESTClient` to use for API calls. + - ``EventDispatcherT`` - the implementation of + :obj:`~hikari.state.dispatchers.EventDispacher` to use for + dispatching events. This class will then delegate any calls inherited + from :obj:`~hikari.state.dispatchers.EventDispacher` to that + implementation when provided. - ``EventManagerT`` - the implementation of :obj:`~hikari.state.event_managers.EventManager` to use for event management, translation, and dispatching. @@ -79,6 +84,11 @@ class BotBase( #: :type: :obj:`~hikari.clients.configs.BotConfig` _config: BotConfigT + #: The event dispatcher for this bot. + #: + #: :type: an implementation instance of :obj:`~hikari.state.dispatcher.EventDispatcher` + event_dispatcher: EventDispatcherT + #: The event manager for this bot. #: #: :type: an implementation instance of :obj:`~hikari.state.event_managers.EventManager` @@ -101,11 +111,11 @@ class BotBase( #: :type: :obj:`~typing.Mapping` [ :obj:`~int`, ? extends :obj:`~hikari.clients.shard_client.ShardClient` ] shards: typing.Mapping[int, ShardClientT] - @abc.abstractmethod def __init__(self, config: configs.BotConfig) -> None: super().__init__(logging.getLogger(f"hikari.{type(self).__qualname__}")) self._config = config - self.event_manager = self._create_event_manager() + self.event_dispatcher = self._create_event_dispatcher(config) + self.event_manager = self._create_event_manager(config, self.event_dispatcher) self.rest = self._create_rest(config) self.shards = more_collections.EMPTY_DICT @@ -200,12 +210,12 @@ async def start(self): self.logger.info("will connect shards to %s", url) - shards = {} + shard_clients = {} for shard_id in shard_ids: shard = self._create_shard(shard_id, shard_count, url, self._config, self.event_manager) - shards[shard_id] = shard + shard_clients[shard_id] = shard - self.shards = shards + self.shards = shard_clients self.logger.info("starting %s", conversions.pluralize(len(self.shards), "shard")) @@ -245,9 +255,9 @@ async def join(self) -> None: await asyncio.gather(*(shard_obj.join() for shard_obj in self.shards.values())) def add_listener( - self, event_type: typing.Type[dispatchers.EventT], callback: dispatchers.EventCallbackT + self, event_type: typing.Type[dispatchers.EventT], callback: dispatchers.EventCallbackT, **kwargs ) -> dispatchers.EventCallbackT: - return self.event_manager.event_dispatcher.add_listener(event_type, callback) + return self.event_manager.event_dispatcher.add_listener(event_type, callback, _stack_level=4) def remove_listener( self, event_type: typing.Type[dispatchers.EventT], callback: dispatchers.EventCallbackT @@ -302,6 +312,7 @@ async def update_presence( is_afk : :obj:`~bool` If specified, :obj:`~True` if the user should be marked as AFK, or :obj:`~False` otherwise. + """ await asyncio.gather( *( @@ -365,46 +376,33 @@ def _create_rest(cls, config: BotConfigT) -> RESTClientT: @classmethod @abc.abstractmethod - def _create_event_manager(cls): + def _create_event_manager(cls, config: BotConfigT, dispatcher: EventDispatcherT) -> EventManagerT: """Return a new instance of an event manager implementation. + Parameters + ---------- + config : :obj:`~hikari.clients.configs.BotConfig` + The bot config to use. + Returns ------- :obj:`~hikari.state.event_managers.EventManager` The event manager to use internally. - """ - - -class StatelessBot( - BotBase[shards.ShardClientImpl, rest.RESTClient, stateless.StatelessEventManagerImpl, configs.BotConfig,] -): - """Bot client without any state internals.""" - def __init__(self, config=configs.BotConfig) -> None: - super().__init__(config) + """ @classmethod - def _create_shard( - cls, - shard_id: int, - shard_count: int, - url: str, - config: configs.BotConfig, - event_manager: stateless.StatelessEventManagerImpl, - ) -> shards.ShardClientImpl: - return shards.ShardClientImpl( - shard_id=shard_id, - shard_count=shard_count, - config=config, - raw_event_consumer_impl=event_manager, - url=url, - dispatcher=event_manager.event_dispatcher, - ) + @abc.abstractmethod + def _create_event_dispatcher(cls, config: BotConfigT) -> EventDispatcherT: + """Return a new instance of an event dispatcher implementation. - @classmethod - def _create_rest(cls, config: BotConfigT) -> rest.RESTClient: - return rest.RESTClient(config) + Parameters + ---------- + config : :obj:`~hikari.clients.configs.BotConfig` + The bot config to use. - @classmethod - def _create_event_manager(cls) -> EventManagerT: - return stateless.StatelessEventManagerImpl() + Returns + ------- + :obj:`~hikari.state.dispatchers.EventDispatcher` + + """ diff --git a/hikari/clients/rest/__init__.py b/hikari/clients/rest/__init__.py index e13865dd04..8a00c18180 100644 --- a/hikari/clients/rest/__init__.py +++ b/hikari/clients/rest/__init__.py @@ -26,10 +26,10 @@ from hikari.clients import configs from hikari.clients.rest import channel -from hikari.clients.rest import me from hikari.clients.rest import gateway from hikari.clients.rest import guild from hikari.clients.rest import invite +from hikari.clients.rest import me from hikari.clients.rest import oauth2 from hikari.clients.rest import react from hikari.clients.rest import user diff --git a/hikari/clients/rest/channel.py b/hikari/clients/rest/channel.py index e6396ae0c4..53262c19d2 100644 --- a/hikari/clients/rest/channel.py +++ b/hikari/clients/rest/channel.py @@ -36,11 +36,10 @@ from hikari import users from hikari import webhooks from hikari.clients.rest import base -from hikari.internal import allowed_mentions from hikari.internal import assertions from hikari.internal import conversions +from hikari.internal import helpers from hikari.internal import more_typing -from hikari.internal import pagination class RESTChannelComponent(base.BaseRESTComponent, abc.ABC): # pylint: disable=W0223 @@ -272,7 +271,7 @@ def fetch_messages_after( after = str(bases.Snowflake.from_datetime(after)) else: after = str(after.id if isinstance(after, bases.UniqueEntity) else int(after)) - return pagination.pagination_handler( + return helpers.pagination_handler( channel_id=str(channel.id if isinstance(channel, bases.UniqueEntity) else int(channel)), deserializer=_messages.Message.deserialize, direction="after", @@ -342,7 +341,7 @@ def fetch_messages_before( before = str(bases.Snowflake.from_datetime(before)) elif before is not None: before = str(before.id if isinstance(before, bases.UniqueEntity) else int(before)) - return pagination.pagination_handler( + return helpers.pagination_handler( channel_id=str(channel.id if isinstance(channel, bases.UniqueEntity) else int(channel)), deserializer=_messages.Message.deserialize, direction="before", @@ -530,7 +529,7 @@ async def create_message( tts=tts, files=await asyncio.gather(*(media.safe_read_file(file) for file in files)) if files is not ... else ..., embed=embed.serialize() if embed is not ... else ..., - allowed_mentions=allowed_mentions.generate_allowed_mentions( + allowed_mentions=helpers.generate_allowed_mentions( mentions_everyone=mentions_everyone, user_mentions=user_mentions, role_mentions=role_mentions ), ) @@ -640,7 +639,7 @@ async def update_message( content=content, embed=embed.serialize() if embed is not ... and embed is not None else embed, flags=flags, - allowed_mentions=allowed_mentions.generate_allowed_mentions( + allowed_mentions=helpers.generate_allowed_mentions( mentions_everyone=mentions_everyone, user_mentions=user_mentions, role_mentions=role_mentions, ), ) diff --git a/hikari/clients/rest/guild.py b/hikari/clients/rest/guild.py index ab79d4a91b..9e50dd379b 100644 --- a/hikari/clients/rest/guild.py +++ b/hikari/clients/rest/guild.py @@ -37,7 +37,7 @@ from hikari import webhooks from hikari.clients.rest import base from hikari.internal import conversions -from hikari.internal import pagination +from hikari.internal import helpers def _get_member_id(member: guilds.GuildMember) -> str: @@ -882,7 +882,7 @@ def fetch_members_after( after = str(bases.Snowflake.from_datetime(after)) else: after = str(after.id if isinstance(after, bases.UniqueEntity) else int(after)) - return pagination.pagination_handler( + return helpers.pagination_handler( guild_id=str(guild.id if isinstance(guild, bases.UniqueEntity) else int(guild)), deserializer=guilds.GuildMember.deserialize, direction="after", diff --git a/hikari/clients/rest/me.py b/hikari/clients/rest/me.py index 79835664be..b08e80044f 100644 --- a/hikari/clients/rest/me.py +++ b/hikari/clients/rest/me.py @@ -24,14 +24,14 @@ import datetime import typing +from hikari import applications from hikari import bases from hikari import channels as _channels from hikari import guilds -from hikari import applications from hikari import users from hikari.clients.rest import base from hikari.internal import conversions -from hikari.internal import pagination +from hikari.internal import helpers class RESTCurrentUserComponent(base.BaseRESTComponent, abc.ABC): # pylint: disable=W0223 @@ -139,7 +139,7 @@ def fetch_my_guilds_after( after = str(bases.Snowflake.from_datetime(after)) else: after = str(after.id if isinstance(after, bases.UniqueEntity) else int(after)) - return pagination.pagination_handler( + return helpers.pagination_handler( deserializer=applications.OwnGuild.deserialize, direction="after", request=self._session.get_current_user_guilds, @@ -185,7 +185,7 @@ def fetch_my_guilds_before( before = str(bases.Snowflake.from_datetime(before)) elif before is not None: before = str(before.id if isinstance(before, bases.UniqueEntity) else int(before)) - return pagination.pagination_handler( + return helpers.pagination_handler( deserializer=applications.OwnGuild.deserialize, direction="before", request=self._session.get_current_user_guilds, diff --git a/hikari/clients/rest/react.py b/hikari/clients/rest/react.py index f07082df88..95fe6b7e02 100644 --- a/hikari/clients/rest/react.py +++ b/hikari/clients/rest/react.py @@ -30,7 +30,7 @@ from hikari import messages as _messages from hikari import users from hikari.clients.rest import base -from hikari.internal import pagination +from hikari.internal import helpers class RESTReactionComponent(base.BaseRESTComponent, abc.ABC): # pylint: disable=W0223 @@ -238,7 +238,7 @@ def fetch_reactors_after( after = str(bases.Snowflake.from_datetime(after)) else: after = str(after.id if isinstance(after, bases.UniqueEntity) else int(after)) - return pagination.pagination_handler( + return helpers.pagination_handler( channel_id=str(channel.id if isinstance(channel, bases.UniqueEntity) else int(channel)), message_id=str(message.id if isinstance(message, bases.UniqueEntity) else int(message)), emoji=getattr(emoji, "url_name", emoji), diff --git a/hikari/clients/rest/webhook.py b/hikari/clients/rest/webhook.py index da4af44a21..3e503215bf 100644 --- a/hikari/clients/rest/webhook.py +++ b/hikari/clients/rest/webhook.py @@ -32,8 +32,8 @@ from hikari import users from hikari import webhooks from hikari.clients.rest import base -from hikari.internal import allowed_mentions from hikari.internal import conversions +from hikari.internal import helpers class RESTWebhookComponent(base.BaseRESTComponent, abc.ABC): # pylint: disable=W0223 @@ -262,7 +262,7 @@ async def execute_webhook( wait=wait, file=await media.safe_read_file(file) if file is not ... else ..., embeds=[embed.serialize() for embed in embeds] if embeds is not ... else ..., - allowed_mentions=allowed_mentions.generate_allowed_mentions( + allowed_mentions=helpers.generate_allowed_mentions( mentions_everyone=mentions_everyone, user_mentions=user_mentions, role_mentions=role_mentions ), ) diff --git a/hikari/clients/shards.py b/hikari/clients/shards.py index 7d47ad6723..9317c41272 100644 --- a/hikari/clients/shards.py +++ b/hikari/clients/shards.py @@ -48,8 +48,8 @@ from hikari.net import codes from hikari.net import ratelimits from hikari.net import shards -from hikari.state import dispatchers from hikari.state import consumers +from hikari.state import dispatchers @enum.unique diff --git a/hikari/clients/stateless.py b/hikari/clients/stateless.py new file mode 100644 index 0000000000..d0188c97f6 --- /dev/null +++ b/hikari/clients/stateless.py @@ -0,0 +1,74 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# Copyright © Nekoka.tt 2019-2020 +# +# This file is part of Hikari. +# +# Hikari is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Hikari is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Hikari. If not, see . +"""Stateless bot implementation.""" +__all__ = ["StatelessBot"] + +from hikari.clients import bot_base +from hikari.clients import configs +from hikari.clients import rest +from hikari.clients import shards +from hikari.state import dispatchers +from hikari.state import stateless + + +class StatelessBot( + bot_base.BotBase[ + shards.ShardClientImpl, + rest.RESTClient, + dispatchers.IntentAwareEventDispatcherImpl, + stateless.StatelessEventManagerImpl, + configs.BotConfig, + ] +): + """Bot client without any state internals. + + This is the most basic type of bot you can create. + """ + + @classmethod + def _create_shard( + cls, + shard_id: int, + shard_count: int, + url: str, + config: configs.BotConfig, + event_manager: stateless.StatelessEventManagerImpl, + ) -> shards.ShardClientImpl: + return shards.ShardClientImpl( + shard_id=shard_id, + shard_count=shard_count, + config=config, + raw_event_consumer_impl=event_manager, + url=url, + dispatcher=event_manager.event_dispatcher, + ) + + @classmethod + def _create_rest(cls, config: configs.BotConfig) -> rest.RESTClient: + return rest.RESTClient(config) + + @classmethod + def _create_event_manager( + cls, config: configs.BotConfig, dispatcher: dispatchers.IntentAwareEventDispatcherImpl + ) -> stateless.StatelessEventManagerImpl: + return stateless.StatelessEventManagerImpl(dispatcher) + + @classmethod + def _create_event_dispatcher(cls, config: configs.BotConfig) -> dispatchers.IntentAwareEventDispatcherImpl: + return dispatchers.IntentAwareEventDispatcherImpl(config.intents) diff --git a/hikari/errors.py b/hikari/errors.py index 1ab9d6ad11..184479c0ee 100644 --- a/hikari/errors.py +++ b/hikari/errors.py @@ -19,6 +19,7 @@ """Core errors that may be raised by this API implementation.""" __all__ = [ "HikariError", + "HikariWarning", "NotFoundHTTPError", "UnauthorizedHTTPError", "BadRequestHTTPError", @@ -46,7 +47,7 @@ class HikariError(RuntimeError): """Base for an error raised by this API. - Any should derive from this. + Any exceptions should derive from this. Note ---- @@ -56,6 +57,19 @@ class HikariError(RuntimeError): __slots__ = () +class HikariWarning(RuntimeWarning): + """Base for a warning raised by this API. + + Any warnings should derive from this. + + Note + ---- + You should never initialize this warning directly. + """ + + __slots__ = () + + class GatewayError(HikariError): """A base exception type for anything that can be thrown by the Gateway. @@ -403,3 +417,10 @@ def __init__( json_code: typing.Optional[typing.Union[codes.JSONErrorCode, int]], ) -> None: super().__init__(codes.HTTPStatusCode.NOT_FOUND, route, message, json_code) + + +class IntentWarning(HikariWarning): + """Warning raised when subscribing to an event that cannot be fired. + + This is caused by your application missing certain intents. + """ diff --git a/hikari/events.py b/hikari/events.py index e0a97655d2..568baa9a09 100644 --- a/hikari/events.py +++ b/hikari/events.py @@ -71,6 +71,7 @@ import attr +from hikari import applications from hikari import bases from hikari import channels from hikari import embeds as _embeds @@ -79,14 +80,13 @@ from hikari import intents from hikari import invites from hikari import messages -from hikari import applications from hikari import unset from hikari import users from hikari import voices from hikari.clients import shards -from hikari.internal import assertions from hikari.internal import conversions from hikari.internal import marshaller +from hikari.internal import more_collections T_contra = typing.TypeVar("T_contra", contravariant=True) @@ -97,29 +97,54 @@ class HikariEvent(bases.HikariEntity, abc.ABC): """The base class that all events inherit from.""" - ___required_intents___: typing.ClassVar[intents.Intent] - def __init_subclass__(cls, *, intent: intents.Intent = intents.Intent(0), **kwargs) -> None: - super().__init_subclass__() - assertions.assert_that(isinstance(intent, intents.Intent), "expected an intent type", TypeError) - cls.___required_intents___ = intent +_HikariEventT = typing.TypeVar("_HikariEventT", contravariant=True) + +_REQUIRED_INTENTS_ATTR: typing.Final[str] = "___required_intents___" + + +def get_required_intents_for(event_type: typing.Type[HikariEvent]) -> typing.Collection[intents.Intent]: + """Retrieve the intents that are required to listen to an event type. + + Parameters + ---------- + event_type : :obj:`~typing.Type` [ :obj:`~HikariEvent` ] + The event type to get required intents for. + + Returns + ------- + :obj:`~typing.Collection` [ :obj:`~hikari.intents.Intent` ] + Collection of acceptable subset combinations of intent needed to + be able to receive the given event type. + """ + return getattr(event_type, _REQUIRED_INTENTS_ATTR, more_collections.EMPTY_COLLECTION) + + +def requires_intents( + first: intents.Intent, *rest: intents.Intent +) -> typing.Callable[[typing.Type[_HikariEventT]], typing.Type[_HikariEventT]]: + """Decorate an event type to define what intents it requires. + + Parameters + ---------- + first : :obj:`~hikari.intents.Intent` + First combination of intents that are acceptable in order to receive + the decorated event type. + *rest : :obj:`~hikari.intents.Intent` + Zero or more additional combinations of intents to require for this + event to be subscribed to. + + """ - if intent: - new_doc = (cls.__doc__ + "\n\n") if cls.__doc__ is not None else "" - new_doc += "This event will only fire if the following intents are enabled: " - new_doc += ", ".join(i.name for i in intents.Intent if i & intent) - if intent.is_privileged: - new_doc += "\n\n" - new_doc += ( - "This event is privileged, meaning you will need to opt into using one or more " - "intents on the Discord Developer Portal." - ) + def decorator(cls: typing.Type[_HikariEventT]) -> typing.Type[_HikariEventT]: + cls.___required_intents___ = [first, *rest] + return cls - cls.__doc__ = new_doc + return decorator # Synthetic event, is not deserialized, and is produced by the dispatcher. -@attr.attrs(slots=True, auto_attribs=True) +@attr.s(slots=True, auto_attribs=True) class ExceptionEvent(HikariEvent): """Descriptor for an exception thrown while processing an event.""" @@ -140,25 +165,25 @@ class ExceptionEvent(HikariEvent): # Synthetic event, is not deserialized -@attr.attrs(slots=True, auto_attribs=True) +@attr.s(slots=True, auto_attribs=True) class StartingEvent(HikariEvent): """Event that is fired before the gateway client starts all shards.""" # Synthetic event, is not deserialized -@attr.attrs(slots=True, auto_attribs=True) +@attr.s(slots=True, auto_attribs=True) class StartedEvent(HikariEvent): """Event that is fired when the gateway client starts all shards.""" # Synthetic event, is not deserialized -@attr.attrs(slots=True, auto_attribs=True) +@attr.s(slots=True, auto_attribs=True) class StoppingEvent(HikariEvent): """Event that is fired when the gateway client is instructed to disconnect all shards.""" # Synthetic event, is not deserialized -@attr.attrs(slots=True, auto_attribs=True) +@attr.s(slots=True, auto_attribs=True) class StoppedEvent(HikariEvent): """Event that is fired when the gateway client has finished disconnecting all shards.""" @@ -249,11 +274,10 @@ def shard_count(self) -> typing.Optional[int]: return self._shard_information[1] if self._shard_information else None +@requires_intents(intents.Intent.GUILDS) @marshaller.marshallable() @attr.s(slots=True, kw_only=True) -class BaseChannelEvent( - HikariEvent, bases.UniqueEntity, marshaller.Deserializable, abc.ABC, intent=intents.Intent.GUILDS -): +class BaseChannelEvent(HikariEvent, bases.UniqueEntity, marshaller.Deserializable, abc.ABC): """A base object that Channel events will inherit from.""" #: The channel's type. @@ -374,9 +398,10 @@ class BaseChannelEvent( ) +@requires_intents(intents.Intent.GUILDS) @marshaller.marshallable() @attr.s(slots=True, kw_only=True) -class ChannelCreateEvent(BaseChannelEvent, intent=intents.Intent.GUILDS): +class ChannelCreateEvent(BaseChannelEvent): """Represents Channel Create gateway events. Will be sent when a guild channel is created and before all Create Message @@ -384,21 +409,24 @@ class ChannelCreateEvent(BaseChannelEvent, intent=intents.Intent.GUILDS): """ +@requires_intents(intents.Intent.GUILDS) @marshaller.marshallable() @attr.s(slots=True, kw_only=True) -class ChannelUpdateEvent(BaseChannelEvent, intent=intents.Intent.GUILDS): +class ChannelUpdateEvent(BaseChannelEvent): """Represents Channel Update gateway events.""" +@requires_intents(intents.Intent.GUILDS) @marshaller.marshallable() @attr.s(slots=True, kw_only=True) -class ChannelDeleteEvent(BaseChannelEvent, intent=intents.Intent.GUILDS): +class ChannelDeleteEvent(BaseChannelEvent): """Represents Channel Delete gateway events.""" +@requires_intents(intents.Intent.GUILDS) @marshaller.marshallable() @attr.s(slots=True, kw_only=True) -class ChannelPinUpdateEvent(HikariEvent, marshaller.Deserializable, intent=intents.Intent.GUILDS): +class ChannelPinUpdateEvent(HikariEvent, marshaller.Deserializable): """Used to represent the Channel Pins Update gateway event. Sent when a message is pinned or unpinned in a channel but not @@ -427,9 +455,10 @@ class ChannelPinUpdateEvent(HikariEvent, marshaller.Deserializable, intent=inten ) +@requires_intents(intents.Intent.GUILDS) @marshaller.marshallable() @attr.s(slots=True, kw_only=True) -class GuildCreateEvent(HikariEvent, marshaller.Deserializable, intent=intents.Intent.GUILDS): +class GuildCreateEvent(HikariEvent, marshaller.Deserializable): """Used to represent Guild Create gateway events. Will be received when the bot joins a guild, and when a guild becomes @@ -437,15 +466,17 @@ class GuildCreateEvent(HikariEvent, marshaller.Deserializable, intent=intents.In """ +@requires_intents(intents.Intent.GUILDS) @marshaller.marshallable() @attr.s(slots=True, kw_only=True) -class GuildUpdateEvent(HikariEvent, marshaller.Deserializable, intent=intents.Intent.GUILDS): +class GuildUpdateEvent(HikariEvent, marshaller.Deserializable): """Used to represent Guild Update gateway events.""" +@requires_intents(intents.Intent.GUILDS) @marshaller.marshallable() @attr.s(slots=True, kw_only=True) -class GuildLeaveEvent(HikariEvent, bases.UniqueEntity, marshaller.Deserializable, intent=intents.Intent.GUILDS): +class GuildLeaveEvent(HikariEvent, bases.UniqueEntity, marshaller.Deserializable): """Fired when the current user leaves the guild or is kicked/banned from it. Notes @@ -454,9 +485,10 @@ class GuildLeaveEvent(HikariEvent, bases.UniqueEntity, marshaller.Deserializable """ +@requires_intents(intents.Intent.GUILDS) @marshaller.marshallable() @attr.s(slots=True, kw_only=True) -class GuildUnavailableEvent(HikariEvent, bases.UniqueEntity, marshaller.Deserializable, intent=intents.Intent.GUILDS): +class GuildUnavailableEvent(HikariEvent, bases.UniqueEntity, marshaller.Deserializable): """Fired when a guild becomes temporarily unavailable due to an outage. Notes @@ -465,9 +497,10 @@ class GuildUnavailableEvent(HikariEvent, bases.UniqueEntity, marshaller.Deserial """ +@requires_intents(intents.Intent.GUILD_BANS) @marshaller.marshallable() @attr.s(slots=True, kw_only=True) -class BaseGuildBanEvent(HikariEvent, marshaller.Deserializable, abc.ABC, intent=intents.Intent.GUILD_BANS): +class BaseGuildBanEvent(HikariEvent, marshaller.Deserializable, abc.ABC): """A base object that guild ban events will inherit from.""" #: The ID of the guild this ban is in. @@ -481,21 +514,24 @@ class BaseGuildBanEvent(HikariEvent, marshaller.Deserializable, abc.ABC, intent= user: users.User = marshaller.attrib(deserializer=users.User.deserialize) +@requires_intents(intents.Intent.GUILD_BANS) @marshaller.marshallable() @attr.s(slots=True, kw_only=True) -class GuildBanAddEvent(BaseGuildBanEvent, intent=intents.Intent.GUILD_BANS): +class GuildBanAddEvent(BaseGuildBanEvent): """Used to represent a Guild Ban Add gateway event.""" +@requires_intents(intents.Intent.GUILD_BANS) @marshaller.marshallable() @attr.s(slots=True, kw_only=True) -class GuildBanRemoveEvent(BaseGuildBanEvent, intent=intents.Intent.GUILD_BANS): +class GuildBanRemoveEvent(BaseGuildBanEvent): """Used to represent a Guild Ban Remove gateway event.""" +@requires_intents(intents.Intent.GUILD_EMOJIS) @marshaller.marshallable() @attr.s(slots=True, kw_only=True) -class GuildEmojisUpdateEvent(HikariEvent, marshaller.Deserializable, intent=intents.Intent.GUILD_EMOJIS): +class GuildEmojisUpdateEvent(HikariEvent, marshaller.Deserializable): """Represents a Guild Emoji Update gateway event.""" #: The ID of the guild this emoji was updated in. @@ -511,9 +547,10 @@ class GuildEmojisUpdateEvent(HikariEvent, marshaller.Deserializable, intent=inte ) +@requires_intents(intents.Intent.GUILD_INTEGRATIONS) @marshaller.marshallable() @attr.s(slots=True, kw_only=True) -class GuildIntegrationsUpdateEvent(HikariEvent, marshaller.Deserializable, intent=intents.Intent.GUILD_INTEGRATIONS): +class GuildIntegrationsUpdateEvent(HikariEvent, marshaller.Deserializable): """Used to represent Guild Integration Update gateway events.""" #: The ID of the guild the integration was updated in. @@ -522,9 +559,10 @@ class GuildIntegrationsUpdateEvent(HikariEvent, marshaller.Deserializable, inten guild_id: bases.Snowflake = marshaller.attrib(deserializer=bases.Snowflake.deserialize) +@requires_intents(intents.Intent.GUILD_MEMBERS) @marshaller.marshallable() @attr.s(slots=True, kw_only=True) -class GuildMemberAddEvent(HikariEvent, guilds.GuildMember, intent=intents.Intent.GUILD_MEMBERS): +class GuildMemberAddEvent(HikariEvent, guilds.GuildMember): """Used to represent a Guild Member Add gateway event.""" #: The ID of the guild where this member was added. @@ -533,9 +571,10 @@ class GuildMemberAddEvent(HikariEvent, guilds.GuildMember, intent=intents.Intent guild_id: bases.Snowflake = marshaller.attrib(deserializer=bases.Snowflake.deserialize) +@requires_intents(intents.Intent.GUILD_MEMBERS) @marshaller.marshallable() @attr.s(slots=True, kw_only=True) -class GuildMemberUpdateEvent(HikariEvent, marshaller.Deserializable, intent=intents.Intent.GUILD_MEMBERS): +class GuildMemberUpdateEvent(HikariEvent, marshaller.Deserializable): """Used to represent a Guild Member Update gateway event. Sent when a guild member or their inner user object is updated. @@ -575,9 +614,10 @@ class GuildMemberUpdateEvent(HikariEvent, marshaller.Deserializable, intent=inte ) +@requires_intents(intents.Intent.GUILD_MEMBERS) @marshaller.marshallable() @attr.s(slots=True, kw_only=True) -class GuildMemberRemoveEvent(HikariEvent, marshaller.Deserializable, intent=intents.Intent.GUILD_MEMBERS): +class GuildMemberRemoveEvent(HikariEvent, marshaller.Deserializable): """Used to represent Guild Member Remove gateway events. Sent when a member is kicked, banned or leaves a guild. @@ -594,9 +634,10 @@ class GuildMemberRemoveEvent(HikariEvent, marshaller.Deserializable, intent=inte user: users.User = marshaller.attrib(deserializer=users.User.deserialize) +@requires_intents(intents.Intent.GUILDS) @marshaller.marshallable() @attr.s(slots=True, kw_only=True) -class GuildRoleCreateEvent(HikariEvent, marshaller.Deserializable, intent=intents.Intent.GUILDS): +class GuildRoleCreateEvent(HikariEvent, marshaller.Deserializable): """Used to represent a Guild Role Create gateway event.""" #: The ID of the guild where this role was created. @@ -610,9 +651,10 @@ class GuildRoleCreateEvent(HikariEvent, marshaller.Deserializable, intent=intent role: guilds.GuildRole = marshaller.attrib(deserializer=guilds.GuildRole.deserialize) +@requires_intents(intents.Intent.GUILDS) @marshaller.marshallable() @attr.s(slots=True, kw_only=True) -class GuildRoleUpdateEvent(HikariEvent, marshaller.Deserializable, intent=intents.Intent.GUILDS): +class GuildRoleUpdateEvent(HikariEvent, marshaller.Deserializable): """Used to represent a Guild Role Create gateway event.""" #: The ID of the guild where this role was updated. @@ -626,9 +668,10 @@ class GuildRoleUpdateEvent(HikariEvent, marshaller.Deserializable, intent=intent role: guilds.GuildRole = marshaller.attrib(deserializer=guilds.GuildRole.deserialize) +@requires_intents(intents.Intent.GUILDS) @marshaller.marshallable() @attr.s(slots=True, kw_only=True) -class GuildRoleDeleteEvent(HikariEvent, marshaller.Deserializable, intent=intents.Intent.GUILDS): +class GuildRoleDeleteEvent(HikariEvent, marshaller.Deserializable): """Represents a gateway Guild Role Delete Event.""" #: The ID of the guild where this role is being deleted. @@ -642,9 +685,10 @@ class GuildRoleDeleteEvent(HikariEvent, marshaller.Deserializable, intent=intent role_id: bases.Snowflake = marshaller.attrib(deserializer=bases.Snowflake.deserialize) +@requires_intents(intents.Intent.GUILD_INVITES) @marshaller.marshallable() @attr.s(slots=True, kw_only=True) -class InviteCreateEvent(HikariEvent, marshaller.Deserializable, intent=intents.Intent.GUILD_INVITES): +class InviteCreateEvent(HikariEvent, marshaller.Deserializable): """Represents a gateway Invite Create event.""" #: The ID of the channel this invite targets. @@ -716,9 +760,10 @@ class InviteCreateEvent(HikariEvent, marshaller.Deserializable, intent=intents.I uses: int = marshaller.attrib(deserializer=int) +@requires_intents(intents.Intent.GUILD_INVITES) @marshaller.marshallable() @attr.s(slots=True, kw_only=True) -class InviteDeleteEvent(HikariEvent, marshaller.Deserializable, intent=intents.Intent.GUILD_INVITES): +class InviteDeleteEvent(HikariEvent, marshaller.Deserializable): """Used to represent Invite Delete gateway events. Sent when an invite is deleted for a channel we can access. @@ -743,18 +788,18 @@ class InviteDeleteEvent(HikariEvent, marshaller.Deserializable, intent=intents.I ) +@requires_intents(intents.Intent.GUILD_MESSAGES, intents.Intent.DIRECT_MESSAGES) @marshaller.marshallable() @attr.s(slots=True, kw_only=True) -class MessageCreateEvent(HikariEvent, messages.Message, intent=intents.Intent.GUILD_MESSAGES): +class MessageCreateEvent(HikariEvent, messages.Message): """Used to represent Message Create gateway events.""" # This is an arbitrarily partial version of `messages.Message` +@requires_intents(intents.Intent.GUILD_MESSAGES, intents.Intent.DIRECT_MESSAGES) @marshaller.marshallable() @attr.s(slots=True, kw_only=True) -class MessageUpdateEvent( - HikariEvent, bases.UniqueEntity, marshaller.Deserializable, intent=intents.Intent.GUILD_MESSAGES -): +class MessageUpdateEvent(HikariEvent, bases.UniqueEntity, marshaller.Deserializable): """Represents Message Update gateway events. Note @@ -942,9 +987,10 @@ class MessageUpdateEvent( ) +@requires_intents(intents.Intent.GUILD_MESSAGES, intents.Intent.DIRECT_MESSAGES) @marshaller.marshallable() @attr.s(slots=True, kw_only=True) -class MessageDeleteEvent(HikariEvent, marshaller.Deserializable, intent=intents.Intent.GUILD_MESSAGES): +class MessageDeleteEvent(HikariEvent, marshaller.Deserializable): """Used to represent Message Delete gateway events. Sent when a message is deleted in a channel we have access to. @@ -968,9 +1014,10 @@ class MessageDeleteEvent(HikariEvent, marshaller.Deserializable, intent=intents. message_id: bases.Snowflake = marshaller.attrib(raw_name="id", deserializer=bases.Snowflake.deserialize) +@requires_intents(intents.Intent.GUILD_MESSAGES) @marshaller.marshallable() @attr.s(slots=True, kw_only=True) -class MessageDeleteBulkEvent(HikariEvent, marshaller.Deserializable, intent=intents.Intent.GUILD_MESSAGES): +class MessageDeleteBulkEvent(HikariEvent, marshaller.Deserializable): """Used to represent Message Bulk Delete gateway events. Sent when multiple messages are deleted in a channel at once. @@ -997,9 +1044,10 @@ class MessageDeleteBulkEvent(HikariEvent, marshaller.Deserializable, intent=inte ) +@requires_intents(intents.Intent.GUILD_MESSAGE_REACTIONS, intents.Intent.DIRECT_MESSAGE_REACTIONS) @marshaller.marshallable() @attr.s(slots=True, kw_only=True) -class MessageReactionAddEvent(HikariEvent, marshaller.Deserializable, intent=intents.Intent.GUILD_MESSAGE_REACTIONS): +class MessageReactionAddEvent(HikariEvent, marshaller.Deserializable): """Used to represent Message Reaction Add gateway events.""" #: The ID of the user adding the reaction. @@ -1041,9 +1089,10 @@ class MessageReactionAddEvent(HikariEvent, marshaller.Deserializable, intent=int ) +@requires_intents(intents.Intent.GUILD_MESSAGE_REACTIONS, intents.Intent.DIRECT_MESSAGE_REACTIONS) @marshaller.marshallable() @attr.s(slots=True, kw_only=True) -class MessageReactionRemoveEvent(HikariEvent, marshaller.Deserializable, intent=intents.Intent.GUILD_MESSAGE_REACTIONS): +class MessageReactionRemoveEvent(HikariEvent, marshaller.Deserializable): """Used to represent Message Reaction Remove gateway events.""" #: The ID of the user who is removing their reaction. @@ -1077,11 +1126,10 @@ class MessageReactionRemoveEvent(HikariEvent, marshaller.Deserializable, intent= ) +@requires_intents(intents.Intent.GUILD_MESSAGE_REACTIONS, intents.Intent.DIRECT_MESSAGE_REACTIONS) @marshaller.marshallable() @attr.s(slots=True, kw_only=True) -class MessageReactionRemoveAllEvent( - HikariEvent, marshaller.Deserializable, intent=intents.Intent.GUILD_MESSAGE_REACTIONS -): +class MessageReactionRemoveAllEvent(HikariEvent, marshaller.Deserializable): """Used to represent Message Reaction Remove All gateway events. Sent when all the reactions are removed from a message, regardless of emoji. @@ -1105,11 +1153,10 @@ class MessageReactionRemoveAllEvent( ) +@requires_intents(intents.Intent.GUILD_MESSAGE_REACTIONS, intents.Intent.DIRECT_MESSAGE_REACTIONS) @marshaller.marshallable() @attr.s(slots=True, kw_only=True) -class MessageReactionRemoveEmojiEvent( - HikariEvent, marshaller.Deserializable, intent=intents.Intent.GUILD_MESSAGE_REACTIONS -): +class MessageReactionRemoveEmojiEvent(HikariEvent, marshaller.Deserializable): """Represents Message Reaction Remove Emoji events. Sent when all the reactions for a single emoji are removed from a message. @@ -1140,18 +1187,20 @@ class MessageReactionRemoveEmojiEvent( ) +@requires_intents(intents.Intent.GUILD_PRESENCES) @marshaller.marshallable() @attr.s(slots=True, kw_only=True) -class PresenceUpdateEvent(HikariEvent, guilds.GuildMemberPresence, intent=intents.Intent.GUILD_PRESENCES): +class PresenceUpdateEvent(HikariEvent, guilds.GuildMemberPresence): """Used to represent Presence Update gateway events. Sent when a guild member changes their presence. """ +@requires_intents(intents.Intent.GUILD_MESSAGE_TYPING, intents.Intent.DIRECT_MESSAGE_TYPING) @marshaller.marshallable() @attr.s(slots=True, kw_only=True) -class TypingStartEvent(HikariEvent, marshaller.Deserializable, intent=intents.Intent.GUILD_MESSAGE_TYPING): +class TypingStartEvent(HikariEvent, marshaller.Deserializable): """Used to represent typing start gateway events. Received when a user or bot starts "typing" in a channel. @@ -1200,9 +1249,10 @@ class UserUpdateEvent(HikariEvent, users.MyUser): """ +@requires_intents(intents.Intent.GUILD_VOICE_STATES) @marshaller.marshallable() @attr.s(slots=True, kw_only=True) -class VoiceStateUpdateEvent(HikariEvent, voices.VoiceState, intent=intents.Intent.GUILD_VOICE_STATES): +class VoiceStateUpdateEvent(HikariEvent, voices.VoiceState): """Used to represent voice state update gateway events. Sent when a user joins, leaves or moves voice channel(s). @@ -1234,9 +1284,10 @@ class VoiceServerUpdateEvent(HikariEvent, marshaller.Deserializable): endpoint: str = marshaller.attrib(deserializer=str) +@requires_intents(intents.Intent.GUILD_WEBHOOKS) @marshaller.marshallable() @attr.s(slots=True, kw_only=True) -class WebhookUpdateEvent(HikariEvent, marshaller.Deserializable, intent=intents.Intent.GUILD_MESSAGE_TYPING): +class WebhookUpdateEvent(HikariEvent, marshaller.Deserializable): """Used to represent webhook update gateway events. Sent when a webhook is updated, created or deleted in a guild. diff --git a/hikari/internal/allowed_mentions.py b/hikari/internal/helpers.py similarity index 54% rename from hikari/internal/allowed_mentions.py rename to hikari/internal/helpers.py index 7121a9df90..60b81e6b62 100644 --- a/hikari/internal/allowed_mentions.py +++ b/hikari/internal/helpers.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -# Copyright © Nekokatt 2019-2020 +# Copyright © Nekoka.tt 2019-2020 # # This file is part of Hikari. # @@ -16,20 +16,36 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . -"""Logic for generating a allowed mentions dict objects. +"""General helper functions and classes that are not categorised elsewhere.""" -|internal| -""" - -__all__ = ["generate_allowed_mentions"] +__all__ = ["warning"] +import textwrap import typing +import warnings from hikari import bases from hikari import guilds from hikari import users from hikari.internal import assertions from hikari.internal import more_collections +from hikari.internal import more_typing + + +def warning(message: str, category: typing.Type[Warning], stack_level: int = 1) -> None: + """Generate a warning in a style consistent for this library. + + Parameters + ---------- + message : :obj:`~str` + The message to display. + category : :obj:`~typing.Type` [ :obj:`~Warning` ] + The type of warning to raise. + stack_level : :obj:`int` + How many stack frames to go back to find the user's invocation. + + """ + warnings.warn("\n" + textwrap.indent(message, " " * 6), category, stacklevel=stack_level + 1) def generate_allowed_mentions( @@ -98,3 +114,69 @@ def generate_allowed_mentions( # As a note, discord will also treat an empty `allowed_mentions` object as if it wasn't passed at all, so we # want to use empty lists for blacklisting elements rather than just not including blacklisted elements. return allowed_mentions + + +async def pagination_handler( + deserializer: typing.Callable[[typing.Any], typing.Any], + direction: typing.Union[typing.Literal["before"], typing.Literal["after"]], + request: typing.Callable[..., more_typing.Coroutine[typing.Any]], + reversing: bool, + start: typing.Union[str, None], + limit: typing.Optional[int] = None, + id_getter: typing.Callable[[typing.Any], str] = lambda entity: str(entity.id), + **kwargs, +) -> typing.AsyncIterator[typing.Any]: + """Generate an async iterator for handling paginated endpoints. + + This will handle Discord's ``before`` and ``after`` pagination. + + Parameters + ---------- + deserializer : :obj:`~typing.Callable` [ [ :obj:`~typing.Any` ], :obj:`~typing.Any` ] + The deserializer to use to deserialize raw elements. + direction : :obj:`~typing.Union` [ ``"before"``, ``"after"`` ] + The direction that this paginator should go in. + request : :obj:`~typing.Callable` [ ``...``, :obj:`~typing.Coroutine` [ :obj:`~typing.Any`, :obj:`~typing.Any`, :obj:`~typing.Any` ] ] + The :obj:`hikari.net.rest_sessions.LowLevelRestfulClient` method that should be + called to make requests for this paginator. + reversing : :obj:`~bool` + Whether the retrieved array of objects should be reversed before + iterating through it, this is needed for certain endpoints like + ``fetch_messages_before`` where the order is static regardless of + if you're using ``before`` or ``after``. + start : :obj:`~int`, optional + The snowflake ID that this paginator should start at, ``0`` may be + passed for ``forward`` pagination to start at the first created + entity and :obj:`~None` may be passed for ``before`` pagination to + start at the newest entity (based on when it's snowflake timestamp). + limit : :obj:`~int`, optional + The amount of deserialized entities that the iterator should return + total, will be unlimited if set to :obj:`~None`. + id_getter : :obj:`~typing.Callable` [ [ :obj:`~typing.Any` ], :obj:`~str` ] + **kwargs + Kwargs to pass through to ``request`` for every request made along + with the current decided limit and direction snowflake. + + Returns + ------- + :obj:`~typing.AsyncIterator` [ :obj:`~typing.Any` ] + An async iterator of the found deserialized found objects. + + """ + while payloads := await request( + limit=100 if limit is None or limit > 100 else limit, + **{direction: start if start is not None else ...}, + **kwargs, + ): + if reversing: + payloads.reverse() + if limit is not None: + limit -= len(payloads) + + for payload in payloads: + entity = deserializer(payload) + yield entity + if limit == 0: + break + # TODO: @FasterSpeeding: can `payloads` ever be empty, leading this to be undefined? + start = id_getter(entity) diff --git a/hikari/internal/pagination.py b/hikari/internal/pagination.py deleted file mode 100644 index c9f392f73c..0000000000 --- a/hikari/internal/pagination.py +++ /dev/null @@ -1,94 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# Copyright © Nekokatt 2019-2020 -# -# This file is part of Hikari. -# -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . -"""Logic for handling Discord's generic paginated endpoints. - -|internal| -""" - -__all__ = ["pagination_handler"] - -import typing - -from hikari.internal import more_typing - - -async def pagination_handler( - deserializer: typing.Callable[[typing.Any], typing.Any], - direction: typing.Union[typing.Literal["before"], typing.Literal["after"]], - request: typing.Callable[..., more_typing.Coroutine[typing.Any]], - reversing: bool, - start: typing.Union[str, None], - limit: typing.Optional[int] = None, - id_getter: typing.Callable[[typing.Any], str] = lambda entity: str(entity.id), - **kwargs, -) -> typing.AsyncIterator[typing.Any]: - """Generate an async iterator for handling paginated endpoints. - - This will handle Discord's ``before`` and ``after`` pagination. - - Parameters - ---------- - deserializer : :obj:`~typing.Callable` [ [ :obj:`~typing.Any` ], :obj:`~typing.Any` ] - The deserializer to use to deserialize raw elements. - direction : :obj:`~typing.Union` [ ``"before"``, ``"after"`` ] - The direction that this paginator should go in. - request : :obj:`~typing.Callable` [ ``...``, :obj:`~typing.Coroutine` [ :obj:`~typing.Any`, :obj:`~typing.Any`, :obj:`~typing.Any` ] ] - The :obj:`hikari.net.rest_sessions.LowLevelRestfulClient` method that should be - called to make requests for this paginator. - reversing : :obj:`~bool` - Whether the retrieved array of objects should be reversed before - iterating through it, this is needed for certain endpoints like - ``fetch_messages_before`` where the order is static regardless of - if you're using ``before`` or ``after``. - start : :obj:`~int`, optional - The snowflake ID that this paginator should start at, ``0`` may be - passed for ``forward`` pagination to start at the first created - entity and :obj:`~None` may be passed for ``before`` pagination to - start at the newest entity (based on when it's snowflake timestamp). - limit : :obj:`~int`, optional - The amount of deserialized entities that the iterator should return - total, will be unlimited if set to :obj:`~None`. - id_getter : :obj:`~typing.Callable` [ [ :obj:`~typing.Any` ], :obj:`~str` ] - **kwargs - Kwargs to pass through to ``request`` for every request made along - with the current decided limit and direction snowflake. - - Returns - ------- - :obj:`~typing.AsyncIterator` [ :obj:`~typing.Any` ] - An async iterator of the found deserialized found objects. - - """ - while payloads := await request( - limit=100 if limit is None or limit > 100 else limit, - **{direction: start if start is not None else ...}, - **kwargs, - ): - if reversing: - payloads.reverse() - if limit is not None: - limit -= len(payloads) - - for payload in payloads: - entity = deserializer(payload) - yield entity - if limit == 0: - break - # TODO: @FasterSpeeding: can `payloads` ever be empty, leading this to be undefined? - start = id_getter(entity) diff --git a/hikari/messages.py b/hikari/messages.py index c3b175119f..18166c8029 100644 --- a/hikari/messages.py +++ b/hikari/messages.py @@ -34,11 +34,11 @@ import attr +from hikari import applications from hikari import bases from hikari import embeds as _embeds from hikari import emojis as _emojis from hikari import guilds -from hikari import applications from hikari import users from hikari.internal import conversions from hikari.internal import marshaller diff --git a/hikari/state/dispatchers.py b/hikari/state/dispatchers.py index 7422bd2d31..e4984e8693 100644 --- a/hikari/state/dispatchers.py +++ b/hikari/state/dispatchers.py @@ -20,7 +20,7 @@ from __future__ import annotations -__all__ = ["EventDispatcher", "EventDispatcherImpl"] +__all__ = ["EventDispatcher", "IntentAwareEventDispatcherImpl"] import abc import asyncio @@ -28,9 +28,12 @@ import logging import typing +from hikari import errors from hikari import events +from hikari import intents from hikari.internal import assertions from hikari.internal import conversions +from hikari.internal import helpers from hikari.internal import more_asyncio from hikari.internal import more_collections @@ -69,7 +72,7 @@ def close(self) -> None: """Cancel anything that is waiting for an event to be dispatched.""" @abc.abstractmethod - def add_listener(self, event_type: typing.Type[EventT], callback: EventCallbackT) -> EventCallbackT: + def add_listener(self, event_type: typing.Type[EventT], callback: EventCallbackT, **kwargs) -> EventCallbackT: """Register a new event callback to a given event name. Parameters @@ -221,11 +224,13 @@ def decorator(callback: EventCallbackT) -> EventCallbackT: if not issubclass(resolved_type, events.HikariEvent): raise TypeError("Event typehints should subclass hikari.events.HikariEvent to be valid") - return self.add_listener(typing.cast(typing.Type[events.HikariEvent], resolved_type), callback) + return self.add_listener( + typing.cast(typing.Type[events.HikariEvent], resolved_type), callback, _stack_level=3 + ) finally: del frame, _ - return self.add_listener(event_type, callback) + return self.add_listener(event_type, callback, _stack_level=3) return decorator @@ -249,7 +254,7 @@ def dispatch_event(self, event: events.HikariEvent) -> more_typing.Future[typing """ -class EventDispatcherImpl(EventDispatcher): +class IntentAwareEventDispatcherImpl(EventDispatcher): """Handles storing and dispatching to event listeners and one-time event waiters. Event listeners once registered will be stored until they are manually @@ -262,6 +267,15 @@ class EventDispatcherImpl(EventDispatcher): to completing the waiter, with any event parameters being passed to the predicate. If the predicate returns False, the waiter is not completed. This allows filtering of certain events and conditions in a procedural way. + + Events that require a specific intent will trigger warnings on subscription + if the provided enabled intents are not a superset of this. + + Parameters + ---------- + enabled_intents : :obj:`~hikari.intents.Intent`, optional + The intents that are enabled for the application. If ``None``, then no + intent checks are performed when subscribing a new event. """ #: The logger used to write log messages. @@ -269,7 +283,8 @@ class EventDispatcherImpl(EventDispatcher): #: :type: :obj:`~logging.Logger` logger: logging.Logger - def __init__(self) -> None: + def __init__(self, enabled_intents: typing.Optional[intents.Intent]) -> None: + self._enabled_intents = enabled_intents self._listeners: ListenerMapT = {} # pylint: disable=E1136 self._waiters: WaiterMapT = {} @@ -284,7 +299,7 @@ def close(self) -> None: future.cancel() self._waiters.clear() - def add_listener(self, event_type: typing.Type[events.HikariEvent], callback: EventCallbackT) -> None: + def add_listener(self, event_type: typing.Type[events.HikariEvent], callback: EventCallbackT, **kwargs) -> None: """Register a new event callback to a given event name. Parameters @@ -298,10 +313,38 @@ def add_listener(self, event_type: typing.Type[events.HikariEvent], callback: Ev ------ :obj:`~TypeError` If ``coroutine_function`` is not a coroutine. + + Note + ---- + If you subscribe to an event that requires intents that you do not have + set, you will receive a warning. """ assertions.assert_that( asyncio.iscoroutinefunction(callback), "You must subscribe a coroutine function only", TypeError ) + + required_intents = events.get_required_intents_for(event_type) + enabled_intents = self._enabled_intents if self._enabled_intents is not None else 0 + + any_intent_match = any(enabled_intents & i == i for i in required_intents) + + if self._enabled_intents is not None and required_intents and not any_intent_match: + intents_lists = [] + for required in required_intents: + set_of_intents = [] + for intent in intents.Intent: + if required & intent: + set_of_intents.append(f"{intent.name} " if intent.is_privileged else intent.name) + intents_lists.append(" + ".join(set_of_intents)) + + message = ( + f"Event {event_type.__module__}.{event_type.__qualname__} will never be triggered\n" + f"unless you enable one of the following intents:\n" + + "\n".join(f" - {intent_list}" for intent_list in intents_lists) + ) + + helpers.warning(message, category=errors.IntentWarning, stack_level=kwargs.pop("_stack_level", 1)) + if event_type not in self._listeners: self._listeners[event_type] = [] self._listeners[event_type].append(callback) diff --git a/hikari/state/event_managers.py b/hikari/state/event_managers.py index aeeefd1963..8177510d59 100644 --- a/hikari/state/event_managers.py +++ b/hikari/state/event_managers.py @@ -25,8 +25,8 @@ from hikari.clients import shards from hikari.internal import assertions -from hikari.state import dispatchers from hikari.state import consumers +from hikari.state import dispatchers EVENT_MARKER_ATTR: typing.Final[str] = "___event_name___" @@ -84,10 +84,9 @@ class EventManager(typing.Generic[EventDispatcherT], consumers.RawEventConsumer) Parameters ---------- - event_dispatcher_impl: :obj:`~hikari.state.event_dispatchers.EventDispatcher`, optional + event_dispatcher_impl: :obj:`~hikari.state.event_dispatchers.EventDispatcher` An implementation of event dispatcher that will store individual events - and manage dispatching them after this object creates them. If - :obj:`~None`, then a default implementation is chosen. + and manage dispatching them after this object creates them. Notes ----- @@ -134,10 +133,7 @@ def _process_message_create(self, shard, payload) -> MessageCreateEvent: create your own as needed. """ - def __init__(self, event_dispatcher_impl: typing.Optional[EventDispatcherT] = None) -> None: - if event_dispatcher_impl is None: - event_dispatcher_impl = dispatchers.EventDispatcherImpl() - + def __init__(self, event_dispatcher_impl: EventDispatcherT) -> None: self.logger = logging.getLogger(type(self).__qualname__) self.event_dispatcher = event_dispatcher_impl self.raw_event_mappers = {} diff --git a/pre-commit b/pre-commit index 2c27225fc8..4712654801 100755 --- a/pre-commit +++ b/pre-commit @@ -5,7 +5,8 @@ if [[ -z ${SKIP_HOOK} ]]; then command -v nox || pip install nox echo -e '\e[1;32mRUNNING HOOKS - prepend your invocation of git commit with SKIP_HOOK=1 to skip this.\e[0m' - nox --sessions reformat-code pytest pylint pydocstyle + nox -s check-formatting || nox -s reformat-code | grep -oP '^reformatted \K.*' | xargs -d'\n' -I '{}' git add -n '{}' + nox --sessions pytest pylint pydocstyle else echo -e '\e[1;31mSKIPPING COMMIT HOOKS. THIS IS HERESY!\e[0m' fi diff --git a/tests/hikari/clients/test_rest/test_channel.py b/tests/hikari/clients/test_rest/test_channel.py index 7b4086e7e5..68563038a8 100644 --- a/tests/hikari/clients/test_rest/test_channel.py +++ b/tests/hikari/clients/test_rest/test_channel.py @@ -23,6 +23,7 @@ import mock import pytest +from hikari.internal import helpers from hikari import channels from hikari import embeds from hikari import guilds @@ -33,9 +34,8 @@ from hikari import users from hikari import webhooks from hikari.clients.rest import channel -from hikari.internal import allowed_mentions from hikari.internal import conversions -from hikari.internal import pagination +from hikari.internal import helpers from hikari.net import rest from tests.hikari import _helpers @@ -143,10 +143,10 @@ async def test_delete_channel(self, rest_channel_logic_impl, channel): @_helpers.parametrize_valid_id_formats_for_models("message", 777777777, messages.Message) def test_fetch_messages_after_with_optionals(self, rest_channel_logic_impl, channel, message): mock_generator = mock.AsyncMock() - with mock.patch.object(pagination, "pagination_handler", return_value=mock_generator): + with mock.patch.object(helpers, "pagination_handler", return_value=mock_generator): result = rest_channel_logic_impl.fetch_messages_after(channel=channel, after=message, limit=52) assert result is mock_generator - pagination.pagination_handler.assert_called_once_with( + helpers.pagination_handler.assert_called_once_with( channel_id="123123123", deserializer=messages.Message.deserialize, direction="after", @@ -159,9 +159,9 @@ def test_fetch_messages_after_with_optionals(self, rest_channel_logic_impl, chan @_helpers.parametrize_valid_id_formats_for_models("channel", 123123123, channels.Channel) def test_fetch_messages_after_without_optionals(self, rest_channel_logic_impl, channel): mock_generator = mock.AsyncMock() - with mock.patch.object(pagination, "pagination_handler", return_value=mock_generator): + with mock.patch.object(helpers, "pagination_handler", return_value=mock_generator): assert rest_channel_logic_impl.fetch_messages_after(channel=channel) is mock_generator - pagination.pagination_handler.assert_called_once_with( + helpers.pagination_handler.assert_called_once_with( channel_id="123123123", deserializer=messages.Message.deserialize, direction="after", @@ -174,9 +174,9 @@ def test_fetch_messages_after_without_optionals(self, rest_channel_logic_impl, c def test_fetch_messages_after_with_datetime_object(self, rest_channel_logic_impl): mock_generator = mock.AsyncMock() date = datetime.datetime(2019, 1, 22, 18, 41, 15, 283_000, tzinfo=datetime.timezone.utc) - with mock.patch.object(pagination, "pagination_handler", return_value=mock_generator): + with mock.patch.object(helpers, "pagination_handler", return_value=mock_generator): assert rest_channel_logic_impl.fetch_messages_after(channel=123123123, after=date) is mock_generator - pagination.pagination_handler.assert_called_once_with( + helpers.pagination_handler.assert_called_once_with( channel_id="123123123", deserializer=messages.Message.deserialize, direction="after", @@ -190,10 +190,10 @@ def test_fetch_messages_after_with_datetime_object(self, rest_channel_logic_impl @_helpers.parametrize_valid_id_formats_for_models("message", 777777777, messages.Message) def test_fetch_messages_before_with_optionals(self, rest_channel_logic_impl, channel, message): mock_generator = mock.AsyncMock() - with mock.patch.object(pagination, "pagination_handler", return_value=mock_generator): + with mock.patch.object(helpers, "pagination_handler", return_value=mock_generator): result = rest_channel_logic_impl.fetch_messages_before(channel=channel, before=message, limit=52) assert result is mock_generator - pagination.pagination_handler.assert_called_once_with( + helpers.pagination_handler.assert_called_once_with( channel_id="123123123", deserializer=messages.Message.deserialize, direction="before", @@ -206,9 +206,9 @@ def test_fetch_messages_before_with_optionals(self, rest_channel_logic_impl, cha @_helpers.parametrize_valid_id_formats_for_models("channel", 123123123, channels.Channel) def test_fetch_messages_before_without_optionals(self, rest_channel_logic_impl, channel): mock_generator = mock.AsyncMock() - with mock.patch.object(pagination, "pagination_handler", return_value=mock_generator): + with mock.patch.object(helpers, "pagination_handler", return_value=mock_generator): assert rest_channel_logic_impl.fetch_messages_before(channel=channel) is mock_generator - pagination.pagination_handler.assert_called_once_with( + helpers.pagination_handler.assert_called_once_with( channel_id="123123123", deserializer=messages.Message.deserialize, direction="before", @@ -221,9 +221,9 @@ def test_fetch_messages_before_without_optionals(self, rest_channel_logic_impl, def test_fetch_messages_before_with_datetime_object(self, rest_channel_logic_impl): mock_generator = mock.AsyncMock() date = datetime.datetime(2019, 1, 22, 18, 41, 15, 283_000, tzinfo=datetime.timezone.utc) - with mock.patch.object(pagination, "pagination_handler", return_value=mock_generator): + with mock.patch.object(helpers, "pagination_handler", return_value=mock_generator): assert rest_channel_logic_impl.fetch_messages_before(channel=123123123, before=date) is mock_generator - pagination.pagination_handler.assert_called_once_with( + helpers.pagination_handler.assert_called_once_with( channel_id="123123123", deserializer=messages.Message.deserialize, direction="before", @@ -317,7 +317,7 @@ async def test_create_message_with_optionals(self, rest_channel_logic_impl, chan mock_media_payload = ("aName.png", mock.MagicMock()) stack = contextlib.ExitStack() stack.enter_context( - mock.patch.object(allowed_mentions, "generate_allowed_mentions", return_value=mock_allowed_mentions_payload) + mock.patch.object(helpers, "generate_allowed_mentions", return_value=mock_allowed_mentions_payload) ) stack.enter_context(mock.patch.object(messages.Message, "deserialize", return_value=mock_message_obj)) stack.enter_context(mock.patch.object(media, "safe_read_file", return_value=mock_media_payload)) @@ -336,7 +336,7 @@ async def test_create_message_with_optionals(self, rest_channel_logic_impl, chan assert result is mock_message_obj media.safe_read_file.assert_called_once_with(mock_media_obj) messages.Message.deserialize.assert_called_once_with(mock_message_payload) - allowed_mentions.generate_allowed_mentions.assert_called_once_with( + helpers.generate_allowed_mentions.assert_called_once_with( mentions_everyone=False, user_mentions=False, role_mentions=False ) rest_channel_logic_impl._session.create_message.assert_called_once_with( @@ -359,13 +359,13 @@ async def test_create_message_without_optionals(self, rest_channel_logic_impl, c mock_allowed_mentions_payload = {"parse": ["everyone", "users", "roles"]} stack = contextlib.ExitStack() stack.enter_context( - mock.patch.object(allowed_mentions, "generate_allowed_mentions", return_value=mock_allowed_mentions_payload) + mock.patch.object(helpers, "generate_allowed_mentions", return_value=mock_allowed_mentions_payload) ) stack.enter_context(mock.patch.object(messages.Message, "deserialize", return_value=mock_message_obj)) with stack: assert await rest_channel_logic_impl.create_message(channel) is mock_message_obj messages.Message.deserialize.assert_called_once_with(mock_message_payload) - allowed_mentions.generate_allowed_mentions.assert_called_once_with( + helpers.generate_allowed_mentions.assert_called_once_with( mentions_everyone=True, user_mentions=True, role_mentions=True ) rest_channel_logic_impl._session.create_message.assert_called_once_with( @@ -441,7 +441,7 @@ async def test_update_message_with_optionals(self, rest_channel_logic_impl, mess rest_channel_logic_impl._session.edit_message.return_value = mock_payload stack = contextlib.ExitStack() stack.enter_context( - mock.patch.object(allowed_mentions, "generate_allowed_mentions", return_value=mock_allowed_mentions_payload) + mock.patch.object(helpers, "generate_allowed_mentions", return_value=mock_allowed_mentions_payload) ) stack.enter_context(mock.patch.object(messages.Message, "deserialize", return_value=mock_message_obj)) with stack: @@ -466,7 +466,7 @@ async def test_update_message_with_optionals(self, rest_channel_logic_impl, mess ) mock_embed.serialize.assert_called_once() messages.Message.deserialize.assert_called_once_with(mock_payload) - allowed_mentions.generate_allowed_mentions.assert_called_once_with( + helpers.generate_allowed_mentions.assert_called_once_with( mentions_everyone=False, role_mentions=False, user_mentions=[123123123] ) @@ -480,7 +480,7 @@ async def test_update_message_without_optionals(self, rest_channel_logic_impl, m rest_channel_logic_impl._session.edit_message.return_value = mock_payload stack = contextlib.ExitStack() stack.enter_context( - mock.patch.object(allowed_mentions, "generate_allowed_mentions", return_value=mock_allowed_mentions_payload) + mock.patch.object(helpers, "generate_allowed_mentions", return_value=mock_allowed_mentions_payload) ) stack.enter_context(mock.patch.object(messages.Message, "deserialize", return_value=mock_message_obj)) with stack: @@ -494,7 +494,7 @@ async def test_update_message_without_optionals(self, rest_channel_logic_impl, m allowed_mentions=mock_allowed_mentions_payload, ) messages.Message.deserialize.assert_called_once_with(mock_payload) - allowed_mentions.generate_allowed_mentions.assert_called_once_with( + helpers.generate_allowed_mentions.assert_called_once_with( mentions_everyone=True, user_mentions=True, role_mentions=True ) diff --git a/tests/hikari/clients/test_rest/test_guild.py b/tests/hikari/clients/test_rest/test_guild.py index 84cb906381..f1dcee1169 100644 --- a/tests/hikari/clients/test_rest/test_guild.py +++ b/tests/hikari/clients/test_rest/test_guild.py @@ -23,6 +23,7 @@ import mock import pytest +from hikari.internal import helpers from hikari import audit_logs from hikari import channels from hikari import colors @@ -35,7 +36,7 @@ from hikari import webhooks from hikari.clients.rest import guild as _guild from hikari.internal import conversions -from hikari.internal import pagination +from hikari.internal import helpers from hikari.net import rest from tests.hikari import _helpers @@ -548,9 +549,9 @@ async def test_fetch_member(self, rest_guild_logic_impl, guild, user): @_helpers.parametrize_valid_id_formats_for_models("user", 115590097100865541, users.User) def test_fetch_members_after_with_optionals(self, rest_guild_logic_impl, guild, user): mock_generator = mock.AsyncMock() - with mock.patch.object(pagination, "pagination_handler", return_value=mock_generator): + with mock.patch.object(helpers, "pagination_handler", return_value=mock_generator): assert rest_guild_logic_impl.fetch_members_after(guild, after=user, limit=34) is mock_generator - pagination.pagination_handler.assert_called_once_with( + helpers.pagination_handler.assert_called_once_with( guild_id="574921006817476608", deserializer=guilds.GuildMember.deserialize, direction="after", @@ -564,9 +565,9 @@ def test_fetch_members_after_with_optionals(self, rest_guild_logic_impl, guild, @_helpers.parametrize_valid_id_formats_for_models("guild", 574921006817476608, guilds.Guild) def test_fetch_members_after_without_optionals(self, rest_guild_logic_impl, guild): mock_generator = mock.AsyncMock() - with mock.patch.object(pagination, "pagination_handler", return_value=mock_generator): + with mock.patch.object(helpers, "pagination_handler", return_value=mock_generator): assert rest_guild_logic_impl.fetch_members_after(guild) is mock_generator - pagination.pagination_handler.assert_called_once_with( + helpers.pagination_handler.assert_called_once_with( guild_id="574921006817476608", deserializer=guilds.GuildMember.deserialize, direction="after", @@ -580,9 +581,9 @@ def test_fetch_members_after_without_optionals(self, rest_guild_logic_impl, guil def test_fetch_members_after_with_datetime_object(self, rest_guild_logic_impl): mock_generator = mock.AsyncMock() date = datetime.datetime(2019, 1, 22, 18, 41, 15, 283_000, tzinfo=datetime.timezone.utc) - with mock.patch.object(pagination, "pagination_handler", return_value=mock_generator): + with mock.patch.object(helpers, "pagination_handler", return_value=mock_generator): assert rest_guild_logic_impl.fetch_members_after(574921006817476608, after=date) is mock_generator - pagination.pagination_handler.assert_called_once_with( + helpers.pagination_handler.assert_called_once_with( guild_id="574921006817476608", deserializer=guilds.GuildMember.deserialize, direction="after", diff --git a/tests/hikari/clients/test_rest/test_me.py b/tests/hikari/clients/test_rest/test_me.py index b64a0544bb..b5fb0cd80c 100644 --- a/tests/hikari/clients/test_rest/test_me.py +++ b/tests/hikari/clients/test_rest/test_me.py @@ -23,13 +23,14 @@ import mock import pytest +from hikari.internal import helpers from hikari import channels from hikari import guilds from hikari import applications from hikari import users from hikari.clients.rest import me from hikari.internal import conversions -from hikari.internal import pagination +from hikari.internal import helpers from hikari.net import rest from tests.hikari import _helpers @@ -96,9 +97,9 @@ async def test_fetch_my_connections(self, rest_clients_impl): @_helpers.parametrize_valid_id_formats_for_models("guild", 574921006817476608, guilds.Guild) def test_fetch_my_guilds_after_with_optionals(self, rest_clients_impl, guild): mock_generator = mock.AsyncMock() - with mock.patch.object(pagination, "pagination_handler", return_value=mock_generator): + with mock.patch.object(helpers, "pagination_handler", return_value=mock_generator): assert rest_clients_impl.fetch_my_guilds_after(after=guild, limit=50) is mock_generator - pagination.pagination_handler.assert_called_once_with( + helpers.pagination_handler.assert_called_once_with( deserializer=applications.OwnGuild.deserialize, direction="after", request=rest_clients_impl._session.get_current_user_guilds, @@ -109,9 +110,9 @@ def test_fetch_my_guilds_after_with_optionals(self, rest_clients_impl, guild): def test_fetch_my_guilds_after_without_optionals(self, rest_clients_impl): mock_generator = mock.AsyncMock() - with mock.patch.object(pagination, "pagination_handler", return_value=mock_generator): + with mock.patch.object(helpers, "pagination_handler", return_value=mock_generator): assert rest_clients_impl.fetch_my_guilds_after() is mock_generator - pagination.pagination_handler.assert_called_once_with( + helpers.pagination_handler.assert_called_once_with( deserializer=applications.OwnGuild.deserialize, direction="after", request=rest_clients_impl._session.get_current_user_guilds, @@ -123,9 +124,9 @@ def test_fetch_my_guilds_after_without_optionals(self, rest_clients_impl): def test_fetch_my_guilds_after_with_datetime_object(self, rest_clients_impl): mock_generator = mock.AsyncMock() date = datetime.datetime(2019, 1, 22, 18, 41, 15, 283_000, tzinfo=datetime.timezone.utc) - with mock.patch.object(pagination, "pagination_handler", return_value=mock_generator): + with mock.patch.object(helpers, "pagination_handler", return_value=mock_generator): assert rest_clients_impl.fetch_my_guilds_after(after=date) is mock_generator - pagination.pagination_handler.assert_called_once_with( + helpers.pagination_handler.assert_called_once_with( deserializer=applications.OwnGuild.deserialize, direction="after", request=rest_clients_impl._session.get_current_user_guilds, @@ -137,9 +138,9 @@ def test_fetch_my_guilds_after_with_datetime_object(self, rest_clients_impl): @_helpers.parametrize_valid_id_formats_for_models("guild", 574921006817476608, guilds.Guild) def test_fetch_my_guilds_before_with_optionals(self, rest_clients_impl, guild): mock_generator = mock.AsyncMock() - with mock.patch.object(pagination, "pagination_handler", return_value=mock_generator): + with mock.patch.object(helpers, "pagination_handler", return_value=mock_generator): assert rest_clients_impl.fetch_my_guilds_before(before=guild, limit=50) is mock_generator - pagination.pagination_handler.assert_called_once_with( + helpers.pagination_handler.assert_called_once_with( deserializer=applications.OwnGuild.deserialize, direction="before", request=rest_clients_impl._session.get_current_user_guilds, @@ -150,9 +151,9 @@ def test_fetch_my_guilds_before_with_optionals(self, rest_clients_impl, guild): def test_fetch_my_guilds_before_without_optionals(self, rest_clients_impl): mock_generator = mock.AsyncMock() - with mock.patch.object(pagination, "pagination_handler", return_value=mock_generator): + with mock.patch.object(helpers, "pagination_handler", return_value=mock_generator): assert rest_clients_impl.fetch_my_guilds_before() is mock_generator - pagination.pagination_handler.assert_called_once_with( + helpers.pagination_handler.assert_called_once_with( deserializer=applications.OwnGuild.deserialize, direction="before", request=rest_clients_impl._session.get_current_user_guilds, @@ -164,9 +165,9 @@ def test_fetch_my_guilds_before_without_optionals(self, rest_clients_impl): def test_fetch_my_guilds_before_with_datetime_object(self, rest_clients_impl): mock_generator = mock.AsyncMock() date = datetime.datetime(2019, 1, 22, 18, 41, 15, 283_000, tzinfo=datetime.timezone.utc) - with mock.patch.object(pagination, "pagination_handler", return_value=mock_generator): + with mock.patch.object(helpers, "pagination_handler", return_value=mock_generator): assert rest_clients_impl.fetch_my_guilds_before(before=date) is mock_generator - pagination.pagination_handler.assert_called_once_with( + helpers.pagination_handler.assert_called_once_with( deserializer=applications.OwnGuild.deserialize, direction="before", request=rest_clients_impl._session.get_current_user_guilds, diff --git a/tests/hikari/clients/test_rest/test_react.py b/tests/hikari/clients/test_rest/test_react.py index 6442e717eb..55af6b292d 100644 --- a/tests/hikari/clients/test_rest/test_react.py +++ b/tests/hikari/clients/test_rest/test_react.py @@ -26,7 +26,7 @@ from hikari import messages from hikari import users from hikari.clients.rest import react -from hikari.internal import pagination +from hikari.internal import helpers from hikari.net import rest from tests.hikari import _helpers @@ -96,10 +96,10 @@ async def test_delete_all_reactions_for_emoji(self, rest_reaction_logic_impl, ch @_helpers.parametrize_valid_id_formats_for_models("user", 140502780547694592, users.User) def test_fetch_reactors_after_with_optionals(self, rest_reaction_logic_impl, message, channel, emoji, user): mock_generator = mock.AsyncMock() - with mock.patch.object(pagination, "pagination_handler", return_value=mock_generator): + with mock.patch.object(helpers, "pagination_handler", return_value=mock_generator): result = rest_reaction_logic_impl.fetch_reactors_after(channel, message, emoji, after=user, limit=47) assert result is mock_generator - pagination.pagination_handler.assert_called_once_with( + helpers.pagination_handler.assert_called_once_with( channel_id="123", message_id="432", emoji="tutu1:456371206225002499", @@ -118,9 +118,9 @@ def test_fetch_reactors_after_with_optionals(self, rest_reaction_logic_impl, mes ) def test_fetch_reactors_after_without_optionals(self, rest_reaction_logic_impl, message, channel, emoji): mock_generator = mock.AsyncMock() - with mock.patch.object(pagination, "pagination_handler", return_value=mock_generator): + with mock.patch.object(helpers, "pagination_handler", return_value=mock_generator): assert rest_reaction_logic_impl.fetch_reactors_after(channel, message, emoji) is mock_generator - pagination.pagination_handler.assert_called_once_with( + helpers.pagination_handler.assert_called_once_with( channel_id="123", message_id="432", emoji="tutu1:456371206225002499", @@ -135,10 +135,10 @@ def test_fetch_reactors_after_without_optionals(self, rest_reaction_logic_impl, def test_fetch_reactors_after_with_datetime_object(self, rest_reaction_logic_impl): mock_generator = mock.AsyncMock() date = datetime.datetime(2019, 1, 22, 18, 41, 15, 283_000, tzinfo=datetime.timezone.utc) - with mock.patch.object(pagination, "pagination_handler", return_value=mock_generator): + with mock.patch.object(helpers, "pagination_handler", return_value=mock_generator): result = rest_reaction_logic_impl.fetch_reactors_after(123, 432, "tutu1:456371206225002499", after=date) assert result is mock_generator - pagination.pagination_handler.assert_called_once_with( + helpers.pagination_handler.assert_called_once_with( channel_id="123", message_id="432", emoji="tutu1:456371206225002499", diff --git a/tests/hikari/clients/test_rest/test_webhook.py b/tests/hikari/clients/test_rest/test_webhook.py index eac25b82cd..e2fd4eca81 100644 --- a/tests/hikari/clients/test_rest/test_webhook.py +++ b/tests/hikari/clients/test_rest/test_webhook.py @@ -27,8 +27,8 @@ from hikari import messages from hikari import webhooks from hikari.clients.rest import webhook -from hikari.internal import allowed_mentions from hikari.internal import conversions +from hikari.internal import helpers from hikari.net import rest from tests.hikari import _helpers @@ -142,11 +142,9 @@ async def test_delete_webhook_without_webhook_token(self, rest_webhook_logic_imp async def test_execute_webhook_without_optionals(self, rest_webhook_logic_impl, webhook): rest_webhook_logic_impl._session.execute_webhook.return_value = ... mock_allowed_mentions_payload = {"parse": ["everyone", "users", "roles"]} - with mock.patch.object( - allowed_mentions, "generate_allowed_mentions", return_value=mock_allowed_mentions_payload - ): + with mock.patch.object(helpers, "generate_allowed_mentions", return_value=mock_allowed_mentions_payload): assert await rest_webhook_logic_impl.execute_webhook(webhook, "a.webhook.token") is None - allowed_mentions.generate_allowed_mentions.assert_called_once_with( + helpers.generate_allowed_mentions.assert_called_once_with( mentions_everyone=True, user_mentions=True, role_mentions=True ) rest_webhook_logic_impl._session.execute_webhook.assert_called_once_with( @@ -176,7 +174,7 @@ async def test_execute_webhook_with_optionals(self, rest_webhook_logic_impl, web stack.enter_context(mock.patch.object(media, "safe_read_file", return_value=mock_media_payload)) stack.enter_context(mock.patch.object(messages.Message, "deserialize")) stack.enter_context( - mock.patch.object(allowed_mentions, "generate_allowed_mentions", return_value=mock_allowed_mentions_payload) + mock.patch.object(helpers, "generate_allowed_mentions", return_value=mock_allowed_mentions_payload) ) with stack: await rest_webhook_logic_impl.execute_webhook( @@ -194,7 +192,7 @@ async def test_execute_webhook_with_optionals(self, rest_webhook_logic_impl, web user_mentions=False, ) media.safe_read_file.assert_called_once_with(mock_media_obj) - allowed_mentions.generate_allowed_mentions.assert_called_once_with( + helpers.generate_allowed_mentions.assert_called_once_with( mentions_everyone=False, user_mentions=False, role_mentions=False ) rest_webhook_logic_impl._session.execute_webhook.assert_called_once_with( @@ -220,7 +218,7 @@ async def test_execute_webhook_returns_message_when_wait_is_true(self, rest_webh mock_allowed_mentions_payload = {"parse": ["everyone", "users", "roles"]} stack = contextlib.ExitStack() stack.enter_context( - mock.patch.object(allowed_mentions, "generate_allowed_mentions", return_value=mock_allowed_mentions_payload) + mock.patch.object(helpers, "generate_allowed_mentions", return_value=mock_allowed_mentions_payload) ) stack.enter_context(mock.patch.object(messages.Message, "deserialize", return_value=mock_message_obj)) with stack: diff --git a/tests/hikari/internal/test_allowed_mentions.py b/tests/hikari/internal/test_allowed_mentions.py deleted file mode 100644 index 36fdfcd1e6..0000000000 --- a/tests/hikari/internal/test_allowed_mentions.py +++ /dev/null @@ -1,119 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# Copyright © Nekoka.tt 2019-2020 -# -# This file is part of Hikari. -# -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . -import pytest - -from hikari import guilds -from hikari import users -from hikari.internal import allowed_mentions -from tests.hikari import _helpers - - -@pytest.mark.parametrize( - ("kwargs", "expected_result"), - [ - ( - {"mentions_everyone": True, "user_mentions": True, "role_mentions": True}, - {"parse": ["everyone", "users", "roles"]}, - ), - ( - {"mentions_everyone": False, "user_mentions": False, "role_mentions": False}, - {"parse": [], "users": [], "roles": []}, - ), - ( - {"mentions_everyone": True, "user_mentions": ["1123123"], "role_mentions": True}, - {"parse": ["everyone", "roles"], "users": ["1123123"]}, - ), - ( - {"mentions_everyone": True, "user_mentions": True, "role_mentions": ["1231123"]}, - {"parse": ["everyone", "users"], "roles": ["1231123"]}, - ), - ( - {"mentions_everyone": False, "user_mentions": ["1123123"], "role_mentions": True}, - {"parse": ["roles"], "users": ["1123123"]}, - ), - ( - {"mentions_everyone": False, "user_mentions": True, "role_mentions": ["1231123"]}, - {"parse": ["users"], "roles": ["1231123"]}, - ), - ( - {"mentions_everyone": False, "user_mentions": ["1123123"], "role_mentions": False}, - {"parse": [], "roles": [], "users": ["1123123"]}, - ), - ( - {"mentions_everyone": False, "user_mentions": False, "role_mentions": ["1231123"]}, - {"parse": [], "roles": ["1231123"], "users": []}, - ), - ( - {"mentions_everyone": False, "user_mentions": ["22222"], "role_mentions": ["1231123"]}, - {"parse": [], "users": ["22222"], "roles": ["1231123"]}, - ), - ( - {"mentions_everyone": True, "user_mentions": ["22222"], "role_mentions": ["1231123"]}, - {"parse": ["everyone"], "users": ["22222"], "roles": ["1231123"]}, - ), - ], -) -def test_generate_allowed_mentions(kwargs, expected_result): - assert allowed_mentions.generate_allowed_mentions(**kwargs) == expected_result - - -@_helpers.parametrize_valid_id_formats_for_models("role", 3, guilds.GuildRole) -def test_generate_allowed_mentions_removes_duplicate_role_ids(role): - result = allowed_mentions.generate_allowed_mentions( - role_mentions=["1", "2", "1", "3", "5", "7", "2", role], user_mentions=True, mentions_everyone=True - ) - assert result == {"roles": ["1", "2", "3", "5", "7"], "parse": ["everyone", "users"]} - - -@_helpers.parametrize_valid_id_formats_for_models("user", 3, users.User) -def test_generate_allowed_mentions_removes_duplicate_user_ids(user): - result = allowed_mentions.generate_allowed_mentions( - role_mentions=True, user_mentions=["1", "2", "1", "3", "5", "7", "2", user], mentions_everyone=True - ) - assert result == {"users": ["1", "2", "3", "5", "7"], "parse": ["everyone", "roles"]} - - -@_helpers.parametrize_valid_id_formats_for_models("role", 190007233919057920, guilds.GuildRole) -def test_generate_allowed_mentions_handles_all_role_formats(role): - result = allowed_mentions.generate_allowed_mentions( - role_mentions=[role], user_mentions=True, mentions_everyone=True - ) - assert result == {"roles": ["190007233919057920"], "parse": ["everyone", "users"]} - - -@_helpers.parametrize_valid_id_formats_for_models("user", 190007233919057920, users.User) -def test_generate_allowed_mentions_handles_all_user_formats(user): - result = allowed_mentions.generate_allowed_mentions( - role_mentions=True, user_mentions=[user], mentions_everyone=True - ) - assert result == {"users": ["190007233919057920"], "parse": ["everyone", "roles"]} - - -@_helpers.assert_raises(type_=ValueError) -def test_generate_allowed_mentions_raises_error_on_too_many_roles(): - allowed_mentions.generate_allowed_mentions( - user_mentions=False, role_mentions=list(range(101)), mentions_everyone=False - ) - - -@_helpers.assert_raises(type_=ValueError) -def test_generate_allowed_mentions_raises_error_on_too_many_users(): - allowed_mentions.generate_allowed_mentions( - user_mentions=list(range(101)), role_mentions=False, mentions_everyone=False - ) diff --git a/tests/hikari/internal/test_pagination.py b/tests/hikari/internal/test_helpers.py similarity index 63% rename from tests/hikari/internal/test_pagination.py rename to tests/hikari/internal/test_helpers.py index 90eba24b65..de3681fc78 100644 --- a/tests/hikari/internal/test_pagination.py +++ b/tests/hikari/internal/test_helpers.py @@ -19,14 +19,104 @@ import mock import pytest -from hikari.internal import pagination +from hikari.internal import helpers +from hikari import guilds +from hikari import users +from tests.hikari import _helpers + + +@pytest.mark.parametrize( + ("kwargs", "expected_result"), + [ + ( + {"mentions_everyone": True, "user_mentions": True, "role_mentions": True}, + {"parse": ["everyone", "users", "roles"]}, + ), + ( + {"mentions_everyone": False, "user_mentions": False, "role_mentions": False}, + {"parse": [], "users": [], "roles": []}, + ), + ( + {"mentions_everyone": True, "user_mentions": ["1123123"], "role_mentions": True}, + {"parse": ["everyone", "roles"], "users": ["1123123"]}, + ), + ( + {"mentions_everyone": True, "user_mentions": True, "role_mentions": ["1231123"]}, + {"parse": ["everyone", "users"], "roles": ["1231123"]}, + ), + ( + {"mentions_everyone": False, "user_mentions": ["1123123"], "role_mentions": True}, + {"parse": ["roles"], "users": ["1123123"]}, + ), + ( + {"mentions_everyone": False, "user_mentions": True, "role_mentions": ["1231123"]}, + {"parse": ["users"], "roles": ["1231123"]}, + ), + ( + {"mentions_everyone": False, "user_mentions": ["1123123"], "role_mentions": False}, + {"parse": [], "roles": [], "users": ["1123123"]}, + ), + ( + {"mentions_everyone": False, "user_mentions": False, "role_mentions": ["1231123"]}, + {"parse": [], "roles": ["1231123"], "users": []}, + ), + ( + {"mentions_everyone": False, "user_mentions": ["22222"], "role_mentions": ["1231123"]}, + {"parse": [], "users": ["22222"], "roles": ["1231123"]}, + ), + ( + {"mentions_everyone": True, "user_mentions": ["22222"], "role_mentions": ["1231123"]}, + {"parse": ["everyone"], "users": ["22222"], "roles": ["1231123"]}, + ), + ], +) +def test_generate_allowed_mentions(kwargs, expected_result): + assert helpers.generate_allowed_mentions(**kwargs) == expected_result + + +@_helpers.parametrize_valid_id_formats_for_models("role", 3, guilds.GuildRole) +def test_generate_allowed_mentions_removes_duplicate_role_ids(role): + result = helpers.generate_allowed_mentions( + role_mentions=["1", "2", "1", "3", "5", "7", "2", role], user_mentions=True, mentions_everyone=True + ) + assert result == {"roles": ["1", "2", "3", "5", "7"], "parse": ["everyone", "users"]} + + +@_helpers.parametrize_valid_id_formats_for_models("user", 3, users.User) +def test_generate_allowed_mentions_removes_duplicate_user_ids(user): + result = helpers.generate_allowed_mentions( + role_mentions=True, user_mentions=["1", "2", "1", "3", "5", "7", "2", user], mentions_everyone=True + ) + assert result == {"users": ["1", "2", "3", "5", "7"], "parse": ["everyone", "roles"]} + + +@_helpers.parametrize_valid_id_formats_for_models("role", 190007233919057920, guilds.GuildRole) +def test_generate_allowed_mentions_handles_all_role_formats(role): + result = helpers.generate_allowed_mentions(role_mentions=[role], user_mentions=True, mentions_everyone=True) + assert result == {"roles": ["190007233919057920"], "parse": ["everyone", "users"]} + + +@_helpers.parametrize_valid_id_formats_for_models("user", 190007233919057920, users.User) +def test_generate_allowed_mentions_handles_all_user_formats(user): + result = helpers.generate_allowed_mentions(role_mentions=True, user_mentions=[user], mentions_everyone=True) + assert result == {"users": ["190007233919057920"], "parse": ["everyone", "roles"]} + + +@_helpers.assert_raises(type_=ValueError) +def test_generate_allowed_mentions_raises_error_on_too_many_roles(): + helpers.generate_allowed_mentions(user_mentions=False, role_mentions=list(range(101)), mentions_everyone=False) + + +@_helpers.assert_raises(type_=ValueError) +def test_generate_allowed_mentions_raises_error_on_too_many_users(): + helpers.generate_allowed_mentions(user_mentions=list(range(101)), role_mentions=False, mentions_everyone=False) @pytest.mark.asyncio async def test__pagination_handler_ends_handles_empty_resource(): mock_deserialize = mock.MagicMock() mock_request = mock.AsyncMock(side_effect=[[]]) - async for _ in pagination.pagination_handler( + async for _ in helpers.pagination_handler( random_kwarg="test", deserializer=mock_deserialize, direction="before", @@ -49,7 +139,7 @@ async def test__pagination_handler_ends_without_limit_with_start(): mock_deserialize = mock.MagicMock(side_effect=mock_models) mock_request = mock.AsyncMock(side_effect=[mock_payloads, []]) results = [] - async for result in pagination.pagination_handler( + async for result in helpers.pagination_handler( random_kwarg="test", deserializer=mock_deserialize, direction="before", @@ -78,7 +168,7 @@ async def test__pagination_handler_ends_without_limit_without_start(): mock_deserialize = mock.MagicMock(side_effect=mock_models) mock_request = mock.AsyncMock(side_effect=[mock_payloads, []]) results = [] - async for result in pagination.pagination_handler( + async for result in helpers.pagination_handler( random_kwarg="test", deserializer=mock_deserialize, direction="before", @@ -107,7 +197,7 @@ async def test__pagination_handler_tracks_ends_when_hits_limit(): mock_deserialize = mock.MagicMock(side_effect=mock_models) mock_request = mock.AsyncMock(side_effect=[mock_payloads, []]) results = [] - async for result in pagination.pagination_handler( + async for result in helpers.pagination_handler( random_kwarg="test", deserializer=mock_deserialize, direction="before", @@ -129,7 +219,7 @@ async def test__pagination_handler_tracks_ends_when_limit_set_but_exhausts_reque mock_deserialize = mock.MagicMock(side_effect=mock_models) mock_request = mock.AsyncMock(side_effect=[mock_payloads, []]) results = [] - async for result in pagination.pagination_handler( + async for result in helpers.pagination_handler( random_kwarg="test", deserializer=mock_deserialize, direction="before", @@ -158,7 +248,7 @@ async def test__pagination_handler_reverses_data_when_reverse_is_true(): mock_deserialize = mock.MagicMock(side_effect=mock_models) mock_request = mock.AsyncMock(side_effect=[mock_payloads, []]) results = [] - async for result in pagination.pagination_handler( + async for result in helpers.pagination_handler( random_kwarg="test", deserializer=mock_deserialize, direction="before", @@ -187,7 +277,7 @@ async def test__pagination_handler_id_getter(): mock_deserialize = mock.MagicMock(side_effect=mock_models) mock_request = mock.AsyncMock(side_effect=[mock_payloads, []]) results = [] - async for result in pagination.pagination_handler( + async for result in helpers.pagination_handler( random_kwarg="test", deserializer=mock_deserialize, direction="before", @@ -212,7 +302,7 @@ async def test__pagination_handler_id_getter(): async def test__pagination_handler_handles_no_initial_data(): mock_deserialize = mock.MagicMock() mock_request = mock.AsyncMock(side_effect=[[]]) - async for _ in pagination.pagination_handler( + async for _ in helpers.pagination_handler( random_kwarg="test", deserializer=mock_deserialize, direction="before", diff --git a/tests/hikari/state/test_event_dispatchers.py b/tests/hikari/state/test_event_dispatchers.py index abbe9d370b..62f3d3cf2b 100644 --- a/tests/hikari/state/test_event_dispatchers.py +++ b/tests/hikari/state/test_event_dispatchers.py @@ -26,22 +26,22 @@ from tests.hikari import _helpers -class StudEvent1(events.HikariEvent): +class StubEvent1(events.HikariEvent): ... -class StudEvent2(events.HikariEvent): +class StubEvent2(events.HikariEvent): ... -class StudEvent3(events.HikariEvent): +class StubEvent3(events.HikariEvent): ... class TestEventDispatcherImpl: @pytest.fixture def dispatcher_inst(self): - return _helpers.unslot_class(dispatchers.EventDispatcherImpl)() + return _helpers.unslot_class(dispatchers.IntentAwareEventDispatcherImpl)(None) # noinspection PyTypeChecker @_helpers.assert_raises(type_=TypeError) @@ -85,11 +85,11 @@ def fut(): ] dispatcher_inst._waiters = ( - waiters := {StudEvent1: test_event_1_waiters, StudEvent2: test_event_2_waiters,} + waiters := {StubEvent1: test_event_1_waiters, StubEvent2: test_event_2_waiters,} ) dispatcher_inst._listeners = ( - listeners := {StudEvent1: test_event_1_listeners, StudEvent2: test_event_2_listeners,} + listeners := {StubEvent1: test_event_1_listeners, StubEvent2: test_event_2_listeners,} ) dispatcher_inst.close() @@ -161,10 +161,10 @@ def test_dispatch_to_existing_muxes(self, dispatcher_inst): mock_coro_fn2 = mock.MagicMock() mock_coro_fn3 = mock.MagicMock() - ctx = StudEvent1() + ctx = StubEvent1() - dispatcher_inst._listeners[StudEvent1] = [mock_coro_fn1, mock_coro_fn2] - dispatcher_inst._listeners[StudEvent2] = [mock_coro_fn3] + dispatcher_inst._listeners[StubEvent1] = [mock_coro_fn1, mock_coro_fn2] + dispatcher_inst._listeners[StubEvent2] = [mock_coro_fn3] with mock.patch("asyncio.gather") as gather: dispatcher_inst.dispatch_event(ctx) @@ -176,7 +176,7 @@ def test_dispatch_to_non_existant_muxes(self, dispatcher_inst): # Should not throw. dispatcher_inst._waiters = {} dispatcher_inst._listeners = {} - dispatcher_inst.dispatch_event(StudEvent1()) + dispatcher_inst.dispatch_event(StubEvent1()) assert dispatcher_inst._waiters == {} assert dispatcher_inst._listeners == {} @@ -197,7 +197,7 @@ async def test_dispatch_is_awaitable_if_something_is_invoked(self, dispatcher_in @pytest.mark.asyncio @_helpers.timeout_after(1) async def test_dispatch_invokes_future_waker_if_registered_with_futures(self, dispatcher_inst, event_loop): - dispatcher_inst._waiters[StudEvent1] = {event_loop.create_future(): lambda _: False} + dispatcher_inst._waiters[StubEvent1] = {event_loop.create_future(): lambda _: False} @pytest.mark.asyncio @_helpers.timeout_after(1) @@ -208,11 +208,11 @@ async def test_dispatch_returns_exception_to_caller(self, dispatcher_inst, event future1 = event_loop.create_future() future2 = event_loop.create_future() - ctx = StudEvent3() + ctx = StubEvent3() - dispatcher_inst._waiters[StudEvent3] = {} - dispatcher_inst._waiters[StudEvent3][future1] = predicate1 - dispatcher_inst._waiters[StudEvent3][future2] = predicate2 + dispatcher_inst._waiters[StubEvent3] = {} + dispatcher_inst._waiters[StubEvent3][future1] = predicate1 + dispatcher_inst._waiters[StubEvent3][future2] = predicate2 await dispatcher_inst.dispatch_event(ctx) @@ -232,26 +232,26 @@ async def test_dispatch_returns_exception_to_caller(self, dispatcher_inst, event @pytest.mark.asyncio @_helpers.timeout_after(1) async def test_waiter_map_deleted_if_made_empty_during_this_dispatch(self, dispatcher_inst): - dispatcher_inst._waiters[StudEvent1] = {mock.MagicMock(): mock.MagicMock(return_value=True)} - dispatcher_inst.dispatch_event(StudEvent1()) + dispatcher_inst._waiters[StubEvent1] = {mock.MagicMock(): mock.MagicMock(return_value=True)} + dispatcher_inst.dispatch_event(StubEvent1()) await asyncio.sleep(0.1) - assert StudEvent1 not in dispatcher_inst._waiters + assert StubEvent1 not in dispatcher_inst._waiters @pytest.mark.asyncio @_helpers.timeout_after(1) async def test_waiter_map_not_deleted_if_not_empty(self, dispatcher_inst): - dispatcher_inst._waiters[StudEvent1] = {mock.MagicMock(): mock.MagicMock(return_value=False)} - dispatcher_inst.dispatch_event(StudEvent1()) + dispatcher_inst._waiters[StubEvent1] = {mock.MagicMock(): mock.MagicMock(return_value=False)} + dispatcher_inst.dispatch_event(StubEvent1()) await asyncio.sleep(0.1) - assert StudEvent1 in dispatcher_inst._waiters + assert StubEvent1 in dispatcher_inst._waiters @pytest.mark.asyncio @_helpers.timeout_after(2) async def test_wait_for_returns_event(self, dispatcher_inst): predicate = mock.MagicMock(return_value=True) - future = dispatcher_inst.wait_for(StudEvent1, timeout=5, predicate=predicate) + future = dispatcher_inst.wait_for(StubEvent1, timeout=5, predicate=predicate) - ctx = StudEvent1() + ctx = StubEvent1() await dispatcher_inst.dispatch_event(ctx) await asyncio.sleep(0.1) @@ -265,8 +265,8 @@ async def test_wait_for_returns_event(self, dispatcher_inst): @_helpers.timeout_after(2) async def test_wait_for_returns_matching_event_args_when_invoked_but_no_predicate_match(self, dispatcher_inst): predicate = mock.MagicMock(return_value=False) - ctx = StudEvent3() - future = dispatcher_inst.wait_for(StudEvent3, timeout=5, predicate=predicate) + ctx = StubEvent3() + future = dispatcher_inst.wait_for(StubEvent3, timeout=5, predicate=predicate) await dispatcher_inst.dispatch_event(ctx) await asyncio.sleep(0.1) @@ -286,8 +286,8 @@ async def test_wait_for_hits_timeout_and_raises(self, dispatcher_inst): @_helpers.assert_raises(type_=RuntimeError) async def test_wait_for_raises_predicate_errors(self, dispatcher_inst): predicate = mock.MagicMock(side_effect=RuntimeError) - ctx = StudEvent1() - future = dispatcher_inst.wait_for(StudEvent1, timeout=1, predicate=predicate) + ctx = StubEvent1() + future = dispatcher_inst.wait_for(StubEvent1, timeout=1, predicate=predicate) await dispatcher_inst.dispatch_event(ctx) await future @@ -309,7 +309,7 @@ async def test_other_events_in_same_waiter_event_name_do_not_awaken_us( @pytest.mark.asyncio async def test_catch_happy_path(self, dispatcher_inst): callback = mock.AsyncMock() - event = StudEvent1() + event = StubEvent1() dispatcher_inst.handle_exception = mock.MagicMock() await dispatcher_inst._catch(callback, event) callback.assert_awaited_once_with(event) @@ -320,7 +320,7 @@ async def test_catch_sad_path(self, dispatcher_inst): ex = RuntimeError() callback = mock.AsyncMock(side_effect=ex) dispatcher_inst.handle_exception = mock.MagicMock() - ctx = StudEvent3() + ctx = StubEvent3() await dispatcher_inst._catch(callback, ctx) dispatcher_inst.handle_exception.assert_called_once_with(ex, ctx, callback) @@ -328,7 +328,7 @@ def test_handle_exception_dispatches_exception_event_with_context(self, dispatch dispatcher_inst.dispatch_event = mock.MagicMock() ex = RuntimeError() - event = StudEvent1() + event = StubEvent1() callback = mock.AsyncMock() dispatcher_inst.handle_exception(ex, event, callback) @@ -340,3 +340,5 @@ def test_handle_exception_will_not_recursively_invoke_exception_handler_event(se dispatcher_inst.dispatch_event = mock.MagicMock() dispatcher_inst.handle_exception(RuntimeError(), events.ExceptionEvent(..., ..., ...), mock.AsyncMock()) dispatcher_inst.dispatch_event.assert_not_called() + + # TODO: test add, on, remove, etc From 104ed02ecdaf7ff5c2d428ba31dfa7b22e32db52 Mon Sep 17 00:00:00 2001 From: Faster Speeding Date: Sat, 18 Apr 2020 16:51:55 +0100 Subject: [PATCH 182/922] Initial proof of concept changes for pdoc3. --- ci/black.nox.py | 1 - ci/pdoc.nox.py | 42 ++++ ci/sphinx.nox.py | 59 ------ docs/.gitignore | 2 - docs/_static/.gitignore | 2 - docs/_static/style.css | 352 -------------------------------- docs/_templates/layout.html | 19 -- docs/_templates/lgplv3.html | 18 -- docs/conf.py | 243 ---------------------- docs/index.rst | 42 ---- hikari/audit_logs.py | 266 ++++++++++++------------ hikari/clients/rest/__init__.py | 10 +- hikari/clients/rest/base.py | 6 +- hikari/clients/rest/channel.py | 1 - pdoc/config.mako | 48 +++++ 15 files changed, 227 insertions(+), 884 deletions(-) create mode 100644 ci/pdoc.nox.py delete mode 100644 ci/sphinx.nox.py delete mode 100644 docs/.gitignore delete mode 100644 docs/_static/.gitignore delete mode 100644 docs/_static/style.css delete mode 100644 docs/_templates/layout.html delete mode 100644 docs/_templates/lgplv3.html delete mode 100644 docs/conf.py delete mode 100644 docs/index.rst create mode 100644 pdoc/config.mako diff --git a/ci/black.nox.py b/ci/black.nox.py index 20fad233fe..993fc9c4b9 100644 --- a/ci/black.nox.py +++ b/ci/black.nox.py @@ -23,7 +23,6 @@ PATHS = [ "hikari", "tests", - "docs", "setup.py", "noxfile.py", ] diff --git a/ci/pdoc.nox.py b/ci/pdoc.nox.py new file mode 100644 index 0000000000..2da4208b59 --- /dev/null +++ b/ci/pdoc.nox.py @@ -0,0 +1,42 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# Copyright © Nekoka.tt 2019-2020 +# +# This file is part of Hikari. +# +# Hikari is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Hikari is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Hikari. If not, see . +"""Pdoc documentation generation.""" +from ci import config +from ci import nox + + +@nox.session(reuse_venv=True, default=True) +def pdoc(session: nox.Session) -> None: + """Generate documentation with pdoc.""" + session.install( + "-r", + config.REQUIREMENTS, + "pdoc3==0.8.1" + ) + + session.run( + "python", + "-m", + "pdoc", + config.MAIN_PACKAGE, + "--html", + "-c", + "show_inherited_members=True", + "--force", + ) diff --git a/ci/sphinx.nox.py b/ci/sphinx.nox.py deleted file mode 100644 index 645827cd60..0000000000 --- a/ci/sphinx.nox.py +++ /dev/null @@ -1,59 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# Copyright © Nekoka.tt 2019-2020 -# -# This file is part of Hikari. -# -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . -"""Sphinx documentation generation.""" -import os -import re - -from ci import config -from ci import nox - - -@nox.session(reuse_venv=False, default=True) -def sphinx(session: nox.Session) -> None: - """Generate Sphinx documentation.""" - session.install( - "-r", config.REQUIREMENTS, - "sphinx==3.0.1", - "https://github.com/ryan-roemer/sphinx-bootstrap-theme/zipball/v0.8.0" - ) - - session.env["SPHINXOPTS"] = "-WTvvn" - - print("Generating stubs") - session.run("sphinx-apidoc", "-e", "-o", config.DOCUMENTATION_DIRECTORY, config.MAIN_PACKAGE) - - print("Producing HTML documentation from stubs") - session.run( - "python", "-m", "sphinx.cmd.build", - "-a", "-b", "html", "-j", "auto", "-n", - config.DOCUMENTATION_DIRECTORY, config.ARTIFACT_DIRECTORY - ) - - if "--no-rm" in session.posargs: - print("Not removing stub files by request") - else: - print("Destroying stub files (skip by passing `-- --no-rm` flag)") - blacklist = (f"{config.MAIN_PACKAGE}.rst", "modules.rst") - for f in os.listdir(config.DOCUMENTATION_DIRECTORY): - if f in blacklist or re.match(f"{config.MAIN_PACKAGE}\\.(\\w|\\.)+\\.rst", f): - path = os.path.join(config.DOCUMENTATION_DIRECTORY, f) - print("rm", path) - os.unlink(path) - - diff --git a/docs/.gitignore b/docs/.gitignore deleted file mode 100644 index 1a73054c6e..0000000000 --- a/docs/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -hikari.*rst -modules.*rst diff --git a/docs/_static/.gitignore b/docs/_static/.gitignore deleted file mode 100644 index 3c2c834211..0000000000 --- a/docs/_static/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -# we fetch the most recent build automatically. -snowstorm.js diff --git a/docs/_static/style.css b/docs/_static/style.css deleted file mode 100644 index 9509c5f333..0000000000 --- a/docs/_static/style.css +++ /dev/null @@ -1,352 +0,0 @@ -@import url('https://fonts.googleapis.com/css?family=Baloo+2|Roboto&display=swap'); -@import url('https://cdn.jsdelivr.net/gh/tonsky/FiraCode@1.207/distr/fira_code.css'); - -/* - * Copyright © Nekoka.tt 2019-2020 - * - * This file is part of Hikari. - * - * Hikari is free software: you can redistribute it and/or modify - * it under the terms of the GNU Lesser General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Hikari is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with Hikari. If not, see . - */ - - -/**************************** dark theme ****************************/ - -body { - background-color: #222; - color: #f8f9fa; - padding-top: 0 !important; - font-family: 'Roboto', sans-serif; -} - -.xref { - background-color: #272822 !important; - font-weight: lighter; -} - -.go { - color: #999 !important; -} - -.c1 { - color: #aa3344 !important; - background-color: transparent !important; -} - -/* whyyyyy */ -code, .literal, .literal > span.pre, .highlight { - color: #F92672 !important; - background-color: #272822; - border-color: transparent !important; -} - -.highlight { - /* bootstrap.css:5011 */ - border-radius: 4px !important; -} - -pre { - background-color: transparent !important; - border-color: transparent !important; -} - -dl.field-list > dt { - padding-left: 0 !important; - word-break: normal !important; -} - -dt:target, span.highlighted { - background-color: transparent !important; - text-decoration: underline; - text-decoration-color: #F92672; -} - -input[type="text"] { - background-color: #555; - color: #fff; - border-color: #333 !important; - border-radius: 3px; -} - -form.navbar-form { - padding: 0 !important; -} - -.alert { - border-color: transparent !important; -} - -.alert-info { - background-color: #505050; - color: #d9edf7; -} - -.alert-warning { - background-color: #f8d7da; - color: #721c24; -} - -.viewcode-link, .headerlink { - color: #66D9EF; -} - -.reference { - color: #F92672 !important; -} - -.external { - color: #F92672 !important -} - -nav, .alert, .admonition { - background-image: none !important; -} - -.navbar { - background-image: none !important; - background-color: #303030 !important; - border-color: transparent !important; - position: relative !important; -} - -/*************************** layout fixes ***************************/ - -h1, h2, h3, h4, h5, h6, .navbar-brand, .navbar-text .navbar-version { - word-wrap: break-word; - font-family: 'Baloo 2', cursive; -} - -h1 { - font-size: 2.5em; -} - -h2 { - font-size: 2em; -} - -h3 { - font-size: 1.5em; -} - -h4 { - font-size: 1.3em; -} - -/* - * Apparently if it is too thin in width on display: block, it makes the html - * element not fit the window, and then the text just flows off the viewport, - * making the viewport only 2/3 of the screen and looking ridiculous. If we use - * flex, it mediates this but then on large devices the viewport snaps rather - * than being responsive. - */ - -html { - display: flex !important; -} - -@media screen and (min-width: 430px){ - html { - display: block !important; - } -} - -/* - * Make body fill the viewport always - */ -body { - position: relative; - font-size: 1.7em; - min-height: 100vh; - height: 100%; -} - -.body { - width: 100% !important; -} - -div.body { - max-width: 100% !important; -} - - -body > div.container { - min-height: -webkit-fill-available; -} - -/* - * Make the footer sticky to the bottom of the body. - */ -html { - min-height: 100vh !important; -} - - -@media screen and (min-width: 600px){ - div.navbar { - position: sticky !important; - } - - div.container-fluid.navbar-inverse.navbar-expand-lg, div.navbar { - min-height: auto; - position: fixed; - box-sizing: border-box; - width: 100%; - z-index: 9999999999999; - } - - div.container-fluid.navbar-inverse.navbar-expand-lg { - bottom: -10px; - } -} - -.nav a { - font-weight: bold; -} - -#navbar.container { - margin-left: 1em; - margin-right: 1em; -} - -.navbar-text { - /* bootswatch bugfix for version not aligning with logo i guess */ - line-height: 21px; -} - -#navbar { - border-radius: 0px !important; -} - -.navbar a, .footer a { - color: #ccc !important; -} - -.navbar a:hover, .footer a:active { - color: #fff !important; -} - -.dropdown-menu { - background-color: #555 !important; -} - -.dropdown-menu>li>a:focus, .dropdown-menu>li>a:hover { - background-color: #666 !important; - background-image: none !important; -} - -.divider { - background-color: #333 !important; -} - -.dropdown-menu .caption { - padding-left: 1em; -} - -footer:before { - margin-bottom: 1em; -} - -footer.footer { - margin-top: 1em; - margin-bottom: 1em; - font-weight: bold; - border: 0; - color: white; -} - -code { - font-family: 'Fira Code', monospace !important; - font-feature-settings: "calt" 1 !important; - font-variant-ligatures: contextual !important; -} - -.highlight { - font-family: 'Fira Code', monospace !important; - font-feature-settings: "calt" 1 !important; - font-variant-ligatures: contextual !important; -} - -.pre { - font-family: 'Fira Code', monospace !important; - font-feature-settings: "calt" 1 !important; - font-variant-ligatures: contextual !important; -} - -@supports (font-variation-settings: normal) { - code { font-family: 'Fira Code VF', monospace; } - .highlight { font-family: 'Fira Code VF', monospace; } - pre { font-family: 'Fira Code VF', monospace; } -} - -.navbar-brand, h1, h2, h3, h4, h5, h6, .navbar-text .navbar-version { - font-family: 'Baloo 2', cursive; -} - -dl { - margin-top: 0em; - margin-bottom: 0em; -} - -dl.class { - margin-top: 4em !important; - margin-bottom: 4em !important; - line-height: 2em; -} - -dl > dd { - margin-left: 4em; -} - -.sig-name.descname { - font-size: inherit !important; -} - -p { - line-height: 2em; -} - -a { - color: #F92672 !important; -} - -/* - * I cant get this to hide, Sphinx is a pain in the arse, so might as well just hide the element... - * this hides an erroneous default value for attrs usage. - */ -dl.attribute > dt > em.property { - display: none; -} - -/* - * Fixes alignment voodoo for type names. - */ -a.reference > code { - padding-top: 0; - padding-bottom: 0; -} - -/* - * Stop EM elements for parameters being italic. - */ -em.sig-param { - font-style: normal; -} - - -/* - * Fix out-of-lined-ed-ness for param tables. - */ -dt, dd { - line-height: 2em !important; -} diff --git a/docs/_templates/layout.html b/docs/_templates/layout.html deleted file mode 100644 index 2d0c5837f2..0000000000 --- a/docs/_templates/layout.html +++ /dev/null @@ -1,19 +0,0 @@ - -{# Import the theme's layout. #} -{% include "lgplv3.html" %} -{% extends "!layout.html" %} - -{# Add metadata to show up in Discord embeds #} -{% block extrahead %} - - - - - - - {{ super() }} -{% endblock %} - -{# Don't add a footer #} -{% block footer %} -{% endblock %} diff --git a/docs/_templates/lgplv3.html b/docs/_templates/lgplv3.html deleted file mode 100644 index 55b58a84a2..0000000000 --- a/docs/_templates/lgplv3.html +++ /dev/null @@ -1,18 +0,0 @@ - diff --git a/docs/conf.py b/docs/conf.py deleted file mode 100644 index 79e185959d..0000000000 --- a/docs/conf.py +++ /dev/null @@ -1,243 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# Copyright © Nekoka.tt 2019-2020 -# -# This file is part of Hikari. -# -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . -""" -Sphinx documentation configuration. -""" -# Configuration file for the Sphinx documentation builder. -# -# This file only contains a selection of the most common options. For a full -# list see the documentation: -# http://www.sphinx-doc.org/en/master/config -import os -import re -import shutil -import sys -import textwrap -import types - -import sphinx_bootstrap_theme - -sys.path.insert(0, os.path.abspath("..")) - -name = "hikari" - -with open(os.path.join("..", name, "_about.py")) as fp: - code = fp.read() - -token_pattern = re.compile(r"^__(?P\w+)?__\s*=\s*(?P(?:'{3}|\"{3}|'|\"))(?P.*?)(?P=quote)", re.M) - -groups = {} - -for match in token_pattern.finditer(code): - group = match.groupdict() - groups[group["key"]] = group["value"] - del match, group - -meta = types.SimpleNamespace(**groups) - -del groups, token_pattern, code, fp - -# -- Project information ----------------------------------------------------- - -project = name.title() -author = meta.author -copyright = meta.copyright -version = meta.version - -is_staging = "dev" in version - -# -- General configuration --------------------------------------------------- - -extensions = [ - "sphinx.ext.autosummary", - "sphinx.ext.napoleon", - "sphinx.ext.autodoc", - "sphinx.ext.viewcode", - "sphinx.ext.intersphinx", - "sphinx.ext.mathjax", -] - -if shutil.which("dot"): - print("Inheritance diagram enabled") - extensions += ["sphinx.ext.graphviz", "sphinx.ext.inheritance_diagram"] - -templates_path = ["_templates"] -exclude_patterns = [] - -# -- Pygments style ---------------------------------------------------------- -pygments_style = "fruity" - -# -- Options for HTML output ------------------------------------------------- -html_theme = "bootstrap" -html_theme_path = sphinx_bootstrap_theme.get_html_theme_path() -html_static_path = ["_static"] - -# Theme options are theme-specific and customize the look and feel of a -# theme further. -html_theme_options = { - # Navigation bar title. (Default: ``project`` value) - # 'navbar_title': "", - # Tab name for entire site. (Default: "Site") - "navbar_site_name": "Modules", - # A list of tuples containing pages or urls to link to. - # Valid tuples should be in the following forms: - # (name, page) # a link to a page - # (name, "/aa/bb", 1) # a link to an arbitrary relative url - # (name, "http://example.com", True) # arbitrary absolute url - # Note the "1" or "True" value above as the third argument to indicate - # an arbitrary url. - "navbar_links": [ - ("Source", "http://gitlab.com/nekokatt/hikari", True), - ("Builds", "http://gitlab.com/nekokatt/hikari/pipelines", True), - ], - # Render the next and previous page links in navbar. (Default: true) - "navbar_sidebarrel": False, - # Render the current pages TOC in the navbar. (Default: true) - "navbar_pagenav": False, - # Tab name for the current pages TOC. (Default: "Page") - "navbar_pagenav_name": "This page", - # Global TOC depth for "site" navbar tab. (Default: 1) - # Switching to -1 shows all levels. - "globaltoc_depth": 6, - # Include hidden TOCs in Site navbar? - # - # Note: If this is "false", you cannot have mixed ``:hidden:`` and - # non-hidden ``toctree`` directives in the same page, or else the build - # will break. - # - # Values: "true" (default) or "false" - "globaltoc_includehidden": "false", - # HTML navbar class (Default: "navbar") to attach to
element. - # For black navbar, do "navbar navbar-inverse" - "navbar_class": "navbar navbar-inverse", - # Fix navigation bar to top of page? - # Values: "true" (default) or "false" - "navbar_fixed_top": "false", - # Location of link to source. - # Options are "nav" (default), "footer" or anything else to exclude. - "source_link_position": "footer", - # Bootswatch (http://bootswatch.com/) theme. - # - # Options are nothing (default) or the name of a valid theme - # such as "cosmo" or "sandstone". - # - # The set of valid themes depend on the version of Bootstrap - # that's used (the next config option). - # - # Currently, the supported themes are: - # - Bootstrap 2: https://bootswatch.com/2 - # - Bootstrap 3: https://bootswatch.com/3 - "bootswatch_theme": None, - # Choose Bootstrap version. - # Values: "3" (default) or "2" (in quotes) - "bootstrap_version": "3", -} - -# -- Autodoc options --------------------------------------------------------- -autoclass_content = "both" - -autodoc_default_options = { - # "member-order": "bysource", - # "member-order": "alphabetical", - "member-order": "groupwise", - "undoc-members": False, - "exclude-members": "__weakref__", - "show_inheritance": True, - "imported_members": False, - "ignore-module-all": True, - "inherited_members": True, - "members": True, -} - -autodoc_typehints = "none" -autodoc_mock_imports = ["aiohttp"] - -# -- Intersphinx options ----------------------------------------------------- -intersphinx_mapping = { - "python": ("https://docs.python.org/3", None), - "aiohttp": ("https://aiohttp.readthedocs.io/en/stable/", None), - "attrs": ("https://www.attrs.org/en/stable/", None), - "click": ("https://click.palletsprojects.com/en/7.x/", None), -} - -# -- Inheritance diagram options... ------------------------------------------------- - -inheritance_graph_attrs = dict( - bgcolor="transparent", rankdir="TD", ratio="auto", fontsize=10, splines="line", size='"20 50"', -) - -inheritance_node_attrs = dict( - fontsize=10, fontname='"monospace"', color='"#505050"', style='"filled,rounded"', fontcolor='"#FFFFFF"' -) -inheritance_edge_attrs = dict( - color='"#505050"', - arrowtail="oempty", - arrowhead="none", - arrowsize=1, - dir="both", - fontcolor='"#FFFFFF"', - style='"filled"', -) -graphviz_output_format = "svg" - -# -- Epilog to inject into each page... --------------------------------------------- - - -rst_epilog = """ -.. |internal| replace:: - These components are part of the hikari.internal module. - This means that anything located here is designed **only to be used internally by Hikari**, - and **you should not use it directly in your applications**. Changes to these files will occur - **without** warning or a deprecation period. It is only documented to ensure a complete reference - for application developers wishing to either contribute to or extend this library. -""" - -if not is_staging: - rst_epilog += textwrap.dedent( - """.. |staging_link| replace:: If you want the latest staging documentation instead, please visit - `this page `__. - - """ - ) -else: - rst_epilog += textwrap.dedent( - """.. |staging_link| replace:: This is the documentation for the development release. - - """ - ) - - -# -- Hacks and formatting --------------------------------------------- - - -def pretty_signature(app, what, name, obj, options, signature, return_annotation): - if what not in ("function", "method", "class"): - return - - if signature is None: - return - - signature = re.sub(r"(?P\w+)=Ellipsis", r"\1=...", signature) - signature = signature.replace("/", "/ ") - return signature, return_annotation - - -def setup(app): - app.connect("autodoc-process-signature", pretty_signature) - app.add_css_file("style.css") diff --git a/docs/index.rst b/docs/index.rst deleted file mode 100644 index c84ab8adae..0000000000 --- a/docs/index.rst +++ /dev/null @@ -1,42 +0,0 @@ -.. image:: https://img.shields.io/discord/574921006817476608.svg?logo=Discord&logoColor=white&label=discord - :target: https://discord.gg/HMnGbsv -.. image:: https://img.shields.io/lgtm/grade/python/gitlab/nekokatt/hikari - :target: https://lgtm.com/projects/gl/nekokatt/hikari?mode=tree -.. image:: https://gitlab.com/nekokatt/hikari/badges/master/coverage.svg - :target: https://gitlab.com/nekokatt/hikari/pipelines -.. image:: https://img.shields.io/gitlab/pipeline/nekokatt/hikari/master?label=pipelines&logo=gitlab - :target: https://gitlab.com/nekokatt/hikari/pipelines -.. image:: https://badgen.net/pypi/v/hikari - :target: https://pypi.org/project/hikari -.. image:: https://img.shields.io/pypi/pyversions/hikari - -Hikari Documentation -#################### - -This is for version |version|. |staging_link| - -Hikari is licensed under the GNU LGPLv3 https://www.gnu.org/licenses/lgpl-3.0.en.html - -Technical documentation ------------------------ - -.. toctree:: - :titlesonly: - - modules - -Other resources ---------------- - -* `Our Discord server `_ -* `Source code `_ -* `Pipelines and builds `_ -* `CI success statistics for nerds `_ -* `Discord API documentation `_ - -Search for a topic ------------------- - -* :ref:`genindex` -* :ref:`search` -* :ref:`modindex` diff --git a/hikari/audit_logs.py b/hikari/audit_logs.py index e484c40fb0..62912e978f 100644 --- a/hikari/audit_logs.py +++ b/hikari/audit_logs.py @@ -16,18 +16,14 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . -"""Components and entities that are used to describe audit logs on Discord. - -.. inheritance-diagram:: - hikari.audit_logs - :parts: 1 -""" +"""Components and entities that are used to describe audit logs on Discord.""" __all__ = [ "AuditLog", "AuditLogChange", "AuditLogChangeKey", "AuditLogEntry", "AuditLogEventType", + "AuditLogIterator", "BaseAuditLogEntryInfo", "ChannelOverwriteEntryInfo", "get_entry_info_entity", @@ -168,21 +164,20 @@ def __str__(self) -> str: @marshaller.marshallable() @attr.s(slots=True, kw_only=True) class AuditLogChange(bases.HikariEntity, marshaller.Deserializable): - """Represents a change made to an audit log entry's target entity.""" + """Represents a change made to an audit log entry's target entity. - #: The new value of the key, if something was added or changed. - #: - #: :type: :obj:`~typing.Any`, optional - new_value: typing.Optional[typing.Any] = marshaller.attrib() + Attributes + ---------- + new_value : typing.Any, optional + The new value of the key, if something was added or changed. + old_value : typing.Any, optional + The old value of the key, if something was removed or changed. + key : typing.Union [ AuditLogChangeKey, str ] + The name of the audit log change's key. + """ - #: The old value of the key, if something was removed or changed. - #: - #: :type: :obj:`~typing.Any`, optional + new_value: typing.Optional[typing.Any] = marshaller.attrib() old_value: typing.Optional[typing.Any] = marshaller.attrib() - - #: The name of the audit log change's key. - #: - #: :type: :obj:`~typing.Union` [ :obj:`~AuditLogChangeKey`, :obj:`~str` ] key: typing.Union[AuditLogChangeKey, str] = marshaller.attrib() @classmethod @@ -250,9 +245,9 @@ def register_audit_log_entry_info( Parameters ---------- - type_ : :obj:`~AuditLogEventType` + type_ : AuditLogEventType An entry types to associate the entity with. - *additional_types : :obj:`~AuditLogEventType` + *additional_types : AuditLogEventType Extra entry types to associate the entity with. Returns @@ -289,22 +284,20 @@ class ChannelOverwriteEntryInfo(BaseAuditLogEntryInfo): Will be attached to the overwrite create, update and delete audit log entries. + + Attributes + ---------- + id : hikari.snowflakes.Snowflake + The ID of the overwrite being updated, added or removed (and the entity + it targets). + type : hikari.channels.PermissionOverwriteType + The type of entity this overwrite targets. + role_name : str, optional + The name of the role this overwrite targets, if it targets a role. """ - #: The ID of the overwrite being updated, added or removed (and the entity - #: it targets). - #: - #: :type: :obj:`~hikari.bases.Snowflake` id: bases.Snowflake = marshaller.attrib(deserializer=bases.Snowflake.deserialize) - - #: The type of entity this overwrite targets. - #: - #: :type: :obj:`~hikari.channels.PermissionOverwriteType` type: channels.PermissionOverwriteType = marshaller.attrib(deserializer=channels.PermissionOverwriteType) - - #: The name of the role this overwrite targets, if it targets a role. - #: - #: :type: :obj:`~str`, optional role_name: typing.Optional[str] = marshaller.attrib(deserializer=str, if_undefined=None, default=None) @@ -315,17 +308,17 @@ class MessagePinEntryInfo(BaseAuditLogEntryInfo): """The extra information for message pin related audit log entries. Will be attached to the message pin and message unpin audit log entries. + + Attributes + ---------- + channel_id : hikari.snowflakes.Snowflake + The ID of the guild text based channel where this pinned message is + being added or removed. + message_id : hikari.snowflakes.Snowflake + The ID of the message that's being pinned or unpinned. """ - #: The ID of the guild text based channel where this pinned message is - #: being added or removed. - #: - #: :type: :obj:`~hikari.bases.Snowflake` channel_id: bases.Snowflake = marshaller.attrib(deserializer=bases.Snowflake.deserialize) - - #: The ID of the message that's being pinned or unpinned. - #: - #: :type: :obj:`~hikari.bases.Snowflake` message_id: bases.Snowflake = marshaller.attrib(deserializer=bases.Snowflake.deserialize) @@ -333,19 +326,20 @@ class MessagePinEntryInfo(BaseAuditLogEntryInfo): @marshaller.marshallable() @attr.s(slots=True, kw_only=True) class MemberPruneEntryInfo(BaseAuditLogEntryInfo): - """Represents the extra information attached to guild prune log entries.""" + """Represents the extra information attached to guild prune log entries. + + Attributes + ---------- + delete_member_days : datetime.timedelta + The timedelta of how many days members were pruned for inactivity based + on. + members_removed : int + The number of members who were removed by this prune. + """ - #: The timedelta of how many days members were pruned for inactivity based - #: on. - #: - #: :type: :obj:`~datetime.timedelta` delete_member_days: datetime.timedelta = marshaller.attrib( deserializer=lambda payload: datetime.timedelta(days=int(payload)) ) - - #: The number of members who were removed by this prune. - #: - #: :type: :obj:`~int` members_removed: int = marshaller.attrib(deserializer=int) @@ -353,11 +347,14 @@ class MemberPruneEntryInfo(BaseAuditLogEntryInfo): @marshaller.marshallable() @attr.s(slots=True, kw_only=True) class MessageBulkDeleteEntryInfo(BaseAuditLogEntryInfo): - """Represents extra information for the message bulk delete audit entry.""" + """Represents extra information for the message bulk delete audit entry. + + Attributes + ---------- + count : int + The amount of messages that were deleted. + """ - #: The amount of messages that were deleted. - #: - #: :type: :obj:`~int` count: int = marshaller.attrib(deserializer=int) @@ -365,11 +362,16 @@ class MessageBulkDeleteEntryInfo(BaseAuditLogEntryInfo): @marshaller.marshallable() @attr.s(slots=True, kw_only=True) class MessageDeleteEntryInfo(MessageBulkDeleteEntryInfo): - """Represents extra information attached to the message delete audit entry.""" + """Represents extra information attached to the message delete audit entry. + + Attributes + ---------- + channel_id : hikari.snowflakes.Snowflake + The guild text based channel where these message(s) were deleted. + count : int + The amount of messages that were deleted. + """ - #: The guild text based channel where these message(s) were deleted. - #: - #: :type: :obj:`~hikari.bases.Snowflake` channel_id: bases.Snowflake = marshaller.attrib(deserializer=bases.Snowflake.deserialize) @@ -377,11 +379,14 @@ class MessageDeleteEntryInfo(MessageBulkDeleteEntryInfo): @marshaller.marshallable() @attr.s(slots=True, kw_only=True) class MemberDisconnectEntryInfo(BaseAuditLogEntryInfo): - """Represents extra information for the voice chat member disconnect entry.""" + """Represents extra information for the voice chat member disconnect entry. + + Attributes + ---------- + count : int + The amount of members who were disconnected from voice in this entry. + """ - #: The amount of members who were disconnected from voice in this entry. - #: - #: :type: :obj:`~int` count: int = marshaller.attrib(deserializer=int) @@ -389,11 +394,16 @@ class MemberDisconnectEntryInfo(BaseAuditLogEntryInfo): @marshaller.marshallable() @attr.s(slots=True, kw_only=True) class MemberMoveEntryInfo(MemberDisconnectEntryInfo): - """Represents extra information for the voice chat based member move entry.""" + """Represents extra information for the voice chat based member move entry. + + Attributes + ---------- + channel_id : hikari.snowflakes.Snowflake + The channel these member(s) were moved to. + count : int + The amount of members who were disconnected from voice in this entry. + """ - #: The channel these member(s) were moved to. - #: - #: :type: :obj:`~hikari.bases.Snowflake` channel_id: bases.Snowflake = marshaller.attrib(deserializer=bases.Snowflake.deserialize) @@ -413,14 +423,14 @@ def get_entry_info_entity(type_: int) -> typing.Type[BaseAuditLogEntryInfo]: Parameters ---------- - :obj:`~int` - The int + type_ : int + The identifier for this entry type. Returns ------- - :obj:`~typing.Type` [ :obj:`~BaseAuditLogEntryInfo` ] + typing.Type [ BaseAuditLogEntryInfo ] The associated options entity. If not implemented then this will be - :obj:`~UnrecognisedAuditLogEntryInfo` + UnrecognisedAuditLogEntryInfo` """ types = getattr(register_audit_log_entry_info, "types", more_collections.EMPTY_DICT) entry_type = types.get(type_) @@ -430,42 +440,30 @@ def get_entry_info_entity(type_: int) -> typing.Type[BaseAuditLogEntryInfo]: @marshaller.marshallable() @attr.s(slots=True, kw_only=True) class AuditLogEntry(bases.UniqueEntity, marshaller.Deserializable): - """Represents an entry in a guild's audit log.""" + """Represents an entry in a guild's audit log. - #: The ID of the entity affected by this change, if applicable. - #: - #: :type: :obj:`~hikari.bases.Snowflake`, optional - target_id: typing.Optional[bases.Snowflake] = marshaller.attrib() + Attributes + ---------- + target_id : hikari.snowflakes.Snowflake, optional + The ID of the entity affected by this change, if applicable. + changes : typing.Sequence [ AuditLogChange ] + A sequence of the changes made to `AuditLogEntry.target_id`. + user_id : hikari.snowflakes.Snowflake + The ID of the user who made this change. + action_type : typing.Union [ AuditLogEventType, str ] + The type of action this entry represents. + options : BaseAuditLogEntryInfo, optional + Extra information about this entry. Will only be provided for certain + `action_type`. + reason : str, optional + The reason for this change, if set (between 0-512 characters). + """ - #: A sequence of the changes made to :attr:`target_id` - #: - #: :type: :obj:`~typing.Sequence` [ :obj:`~AuditLogChange` ] + target_id: typing.Optional[bases.Snowflake] = marshaller.attrib() changes: typing.Sequence[AuditLogChange] = marshaller.attrib() - - #: The ID of the user who made this change. - #: - #: :type: :obj:`~hikari.bases.Snowflake` user_id: bases.Snowflake = marshaller.attrib() - - #: The ID of this entry. - #: - #: :type: :obj:`~hikari.bases.Snowflake` - id: bases.Snowflake = marshaller.attrib() - - #: The type of action this entry represents. - #: - #: :type: :obj:`~typing.Union` [ :obj:`~AuditLogEventType`, :obj:`~str` ] action_type: typing.Union[AuditLogEventType, str] = marshaller.attrib() - - #: Extra information about this entry. Will only be provided for certain - #: :attr:`action_type`. - #: - #: :type: :obj:`~BaseAuditLogEntryInfo`, optional options: typing.Optional[BaseAuditLogEntryInfo] = marshaller.attrib() - - #: The reason for this change, if set (between 0-512 characters). - #: - #: :type: :obj:`~str` reason: typing.Optional[str] = marshaller.attrib() @classmethod @@ -497,35 +495,31 @@ def deserialize(cls, payload: typing.Mapping[str, str]) -> "AuditLogEntry": @marshaller.marshallable() @attr.s(slots=True, kw_only=True) class AuditLog(bases.HikariEntity, marshaller.Deserializable): - """Represents a guilds audit log.""" + """Represents a guilds audit log. - #: A sequence of the audit log's entries. - #: - #: :type: :obj:`~typing.Mapping` [ :obj:`~hikari.bases.Snowflake`, :obj:`~AuditLogEntry` ] + Attributes + ---------- + entries : typing.Mapping [ hikari.snowflakes.Snowflake, AuditLogEntry ] + A sequence of the audit log's entries. + integrations : typing.Mapping [ hikari.snowflakes.Snowflake, hikari.guilds.GuildIntegration ] + A mapping of the partial objects of integrations found in this audit log. + users : typing.Mapping [ hikari.bases.Snowflake, hikari.users.User` ] + A mapping of the objects of users found in this audit log. + webhooks : typing.Mapping [ hikari.snowflakes.Snowflake, hikari.webhooks.Webhook ] + A mapping of the objects of webhooks found in this audit log. + """ entries: typing.Mapping[bases.Snowflake, AuditLogEntry] = marshaller.attrib( raw_name="audit_log_entries", deserializer=lambda payload: {entry.id: entry for entry in map(AuditLogEntry.deserialize, payload)}, ) - - #: A mapping of the partial objects of integrations found in this audit log. - #: - #: :type: :obj:`~typing.Mapping` [ :obj:`~hikari.bases.Snowflake`, :obj:`~hikari.guilds.GuildIntegration` ] integrations: typing.Mapping[bases.Snowflake, guilds.GuildIntegration] = marshaller.attrib( deserializer=lambda payload: { integration.id: integration for integration in map(guilds.PartialGuildIntegration.deserialize, payload) } ) - - #: A mapping of the objects of users found in this audit log. - #: - #: :type: :obj:`~typing.Mapping` [ :obj:`~hikari.bases.Snowflake`, :obj:`~hikari.users.User` ] users: typing.Mapping[bases.Snowflake, _users.User] = marshaller.attrib( deserializer=lambda payload: {user.id: user for user in map(_users.User.deserialize, payload)} ) - - #: A mapping of the objects of webhooks found in this audit log. - #: - #: :type: :obj:`~typing.Mapping` [ :obj:`~hikari.bases.Snowflake`, :obj:`~hikari.webhooks.Webhook` ] webhooks: typing.Mapping[bases.Snowflake, _webhooks.Webhook] = marshaller.attrib( deserializer=lambda payload: {webhook.id: webhook for webhook in map(_webhooks.Webhook.deserialize, payload)} ) @@ -537,30 +531,40 @@ class AuditLogIterator(typing.AsyncIterator[AuditLogEntry]): This returns the audit log entries created before a given entry object/ID or from the newest audit log entry to the oldest. + Attributes + ---------- + integrations : typing.Mapping [ hikari.snowflakes.Snowflake, hikari.guilds.GuildIntegration ] + A mapping of the partial objects of integrations found in this audit log + so far. + users : typing.Mapping [ hikari.snowflakes.Snowflake, hikari.users.User ] + A mapping of the objects of users found in this audit log so far. + webhooks : typing.Mapping [ hikari.snowflakes.Snowflake, hikari.webhooks.Webhook ] + A mapping of the objects of webhooks found in this audit log so far. + Parameters ---------- - guild_id : :obj:`~str` + guild_id : str The guild ID to look up. - request : :obj:`~typing.Callable` [ ``...``, :obj:`~typing.Coroutine` [ :obj:`~typing.Any`, :obj:`~typing.Any`, :obj:`~typing.Any` ] ] + request : typing.Callable [ `...`, typing.Coroutine [ typing.Any, typing.Any, typing.Any ] ] The session bound function that this iterator should use for making Get Guild Audit Log requests. - user_id : :obj:`~str` + user_id : str If specified, the user ID to filter by. - action_type : :obj:`~int` + action_type : int If specified, the action type to look up. - limit : :obj:`~int` + limit : int If specified, the limit to how many entries this iterator should return else unlimited. - before : :obj:`~str` + before : str If specified, an entry ID to specify where this iterator's returned audit log entries should start . Note ---- - This iterator's attributes :attr:`integrations`, :attr:`users` and - :attr:`webhooks` will be filled up as this iterator makes requests to the - Get Guild Audit Log endpoint with the relevant objects for entities - referenced by returned entries. + This iterator's attributes `AuditLogIterator.integrations`, + `AuditLogIterator.users` and `AuditLogIterator.webhooks` will be filled up + as this iterator makes requests to the Get Guild Audit Log endpoint with + the relevant objects for entities referenced by returned entries. """ __slots__ = ( @@ -574,20 +578,8 @@ class AuditLogIterator(typing.AsyncIterator[AuditLogEntry]): "webhooks", ) - #: A mapping of the partial objects of integrations found in this audit log - #: so far. - #: - #: :type: :obj:`~typing.Mapping` [ :obj:`~hikari.bases.Snowflake`, :obj:`~hikari.guilds.GuildIntegration` ] integrations: typing.Mapping[bases.Snowflake, guilds.GuildIntegration] - - #: A mapping of the objects of users found in this audit log so far. - #: - #: :type: :obj:`~typing.Mapping` [ :obj:`~hikari.bases.Snowflake`, :obj:`~hikari.users.User` ] users: typing.Mapping[bases.Snowflake, _users.User] - - #: A mapping of the objects of webhooks found in this audit log so far. - #: - #: :type: :obj:`~typing.Mapping` [ :obj:`~hikari.bases.Snowflake`, :obj:`~hikari.webhooks.Webhook` ] webhooks: typing.Mapping[bases.Snowflake, _webhooks.Webhook] def __init__( diff --git a/hikari/clients/rest/__init__.py b/hikari/clients/rest/__init__.py index 8a00c18180..99b4216e6d 100644 --- a/hikari/clients/rest/__init__.py +++ b/hikari/clients/rest/__init__.py @@ -16,7 +16,7 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . -"""Marshall wrappings for the REST implementation in :mod:`hikari.net.rest`. +"""Marshall wrappings for the REST implementation in `hikari.net.rest`. This provides an object-oriented interface for interacting with discord's REST API. @@ -54,18 +54,18 @@ class RESTClient( A marshalling object-oriented REST API client. This client bridges the basic REST API exposed by - :obj:`~hikari.net.rest_sessions.LowLevelRestfulClient` and wraps it in a unit of + `hikari.net.rest_sessions.LowLevelRestfulClient` and wraps it in a unit of processing that can handle parsing API objects into Hikari entity objects. Parameters ---------- - config : :obj:`~hikari.clients.configs.RESTConfig` + config : `hikari.clients.configs.RESTConfig` A HTTP configuration object. Note ---- - For all endpoints where a ``reason`` argument is provided, this may be a - string inclusively between ``0`` and ``512`` characters length, with any + For all endpoints where a `reason` argument is provided, this may be a + string inclusively between `0` and `512` characters length, with any additional characters being cut off. """ diff --git a/hikari/clients/rest/base.py b/hikari/clients/rest/base.py index 26cbafb668..3e2b9cbc55 100644 --- a/hikari/clients/rest/base.py +++ b/hikari/clients/rest/base.py @@ -31,9 +31,9 @@ class BaseRESTComponent(abc.ABC, metaclass=meta.UniqueFunctionMeta): """An abstract class that all REST client logic classes should inherit from. - This defines the abstract method ``__init__`` which will assign an instance - of :obj:`~hikari.net.rest_sessions.LowLevelRestfulClient` to the attribute that all - components will expect to make calls to. + This defines the abstract method `__init__` which will assign an instance + of `hikari.net.rest_sessions.LowLevelRestfulClient` to the attribute that + all components will expect to make calls to. """ @abc.abstractmethod diff --git a/hikari/clients/rest/channel.py b/hikari/clients/rest/channel.py index 4c929f734e..b6e95b8607 100644 --- a/hikari/clients/rest/channel.py +++ b/hikari/clients/rest/channel.py @@ -238,7 +238,6 @@ def fetch_messages_after( Example ------- - .. code-block:: python async for message in client.fetch_messages_after(channel, after=9876543, limit=3232): if message.author.id in BLACKLISTED_USERS: diff --git a/pdoc/config.mako b/pdoc/config.mako new file mode 100644 index 0000000000..745d741533 --- /dev/null +++ b/pdoc/config.mako @@ -0,0 +1,48 @@ +<%! + # Template configuration. Copy over in your template directory + # (used with `--template-dir`) and adapt as necessary. + # Note, defaults are loaded from this distribution file, so your + # config.mako only needs to contain values you want overridden. + # You can also run pdoc with `--config KEY=VALUE` to override + # individual values. + html_lang = 'en' + show_inherited_members = True + extract_module_toc_into_sidebar = True + list_class_variables_in_index = True + sort_identifiers = True + show_type_annotations = True + # Show collapsed source code block next to each item. + # Disabling this can improve rendering speed of large modules. + show_source_code = True + # If set, format links to objects in online source code repository + # according to this template. Supported keywords for interpolation + # are: commit, path, start_line, end_line. + #git_link_template = 'https://github.com/USER/PROJECT/blob/{commit}/{path}#L{start_line}-L{end_line}' + #git_link_template = 'https://gitlab.com/USER/PROJECT/blob/{commit}/{path}#L{start_line}-L{end_line}' + #git_link_template = 'https://bitbucket.org/USER/PROJECT/src/{commit}/{path}#lines-{start_line}:{end_line}' + #git_link_template = 'https://CGIT_HOSTNAME/PROJECT/tree/{path}?id={commit}#n{start-line}' + git_link_template = None + # A prefix to use for every HTML hyperlink in the generated documentation. + # No prefix results in all links being relative. + link_prefix = '' + # Enable syntax highlighting for code/source blocks by including Highlight.js + syntax_highlighting = True + # Set the style keyword such as 'atom-one-light' or 'github-gist' + # Options: https://github.com/highlightjs/highlight.js/tree/master/src/styles + # Demo: https://highlightjs.org/static/demo/ + hljs_style = 'github' + # If set, insert Google Analytics tracking code. Value is GA + # tracking id (UA-XXXXXX-Y). + google_analytics = '' + # If set, insert Google Custom Search search bar widget above the sidebar index. + # The whitespace-separated tokens represent arbitrary extra queries (at least one + # must match) passed to regular Google search. Example: + #search_query = 'inurl:github.com/USER/PROJECT site:PROJECT.github.io site:PROJECT.website' + search_query = '' + # If set, render LaTeX math syntax within \(...\) (inline equations), + # or within \[...\] or $$...$$ or `.. math::` (block equations) + # as nicely-formatted math formulas using MathJax. + # Note: in Python docstrings, either all backslashes need to be escaped (\\) + # or you need to use raw r-strings. + latex_math = False +%> From dd42074baaba8aa5c4c69b279b2b01cd02aacf7c Mon Sep 17 00:00:00 2001 From: davfsa Date: Sun, 19 Apr 2020 11:37:52 +0200 Subject: [PATCH 183/922] Addec dark theme and config to docs --- ci/pages.gitlab-ci.yml | 4 +- ci/pdoc.nox.py | 12 +- {pdoc => docs}/config.mako | 29 ++- docs/css.mako | 379 +++++++++++++++++++++++++++++++++++++ docs/head.mako | 24 +++ 5 files changed, 431 insertions(+), 17 deletions(-) rename {pdoc => docs}/config.mako (68%) create mode 100644 docs/css.mako create mode 100644 docs/head.mako diff --git a/ci/pages.gitlab-ci.yml b/ci/pages.gitlab-ci.yml index 187a91e284..8cad99b837 100644 --- a/ci/pages.gitlab-ci.yml +++ b/ci/pages.gitlab-ci.yml @@ -36,7 +36,7 @@ pip install nox git clone ${CI_REPOSITORY_URL} -b ${TARGET_BRANCH} hikari_docs_${TARGET_BRANCH} --depth=1 cd "hikari_docs_${TARGET_BRANCH}" - nox -s sphinx || nox -s documentation # FIXME: remove once in master. + nox -s pdoc || nox -s documentation # FIXME: remove once in master. rm ../public -rf && mkdir ../public if [[ "$TARGET_BRANCH" = master ]]; then cd public @@ -97,7 +97,7 @@ apt-get update apt-get install -qy graphviz pip install nox - nox -s sphinx || nox -s documentation # FIXME: remove once in master. + nox -s pdoc || nox -s documentation # FIXME: remove once in master. except: - master - staging diff --git a/ci/pdoc.nox.py b/ci/pdoc.nox.py index 2da4208b59..a9cbdb512b 100644 --- a/ci/pdoc.nox.py +++ b/ci/pdoc.nox.py @@ -24,11 +24,7 @@ @nox.session(reuse_venv=True, default=True) def pdoc(session: nox.Session) -> None: """Generate documentation with pdoc.""" - session.install( - "-r", - config.REQUIREMENTS, - "pdoc3==0.8.1" - ) + session.install("-r", config.REQUIREMENTS, "pdoc3==0.8.1") session.run( "python", @@ -36,7 +32,9 @@ def pdoc(session: nox.Session) -> None: "pdoc", config.MAIN_PACKAGE, "--html", - "-c", - "show_inherited_members=True", + "--output-dir", + config.ARTIFACT_DIRECTORY, + "--template-dir", + config.DOCUMENTATION_DIRECTORY, "--force", ) diff --git a/pdoc/config.mako b/docs/config.mako similarity index 68% rename from pdoc/config.mako rename to docs/config.mako index 745d741533..7b6f419baf 100644 --- a/pdoc/config.mako +++ b/docs/config.mako @@ -1,3 +1,20 @@ +## Copyright © Nekokatt 2019-2020 +## +## This file is part of Hikari. +## +## Hikari is free software: you can redistribute it and/or modify +## it under the terms of the GNU Lesser General Public License as published by +## the Free Software Foundation, either version 3 of the License, or +## (at your option) any later version. +## +## Hikari is distributed in the hope that it will be useful, +## but WITHOUT ANY WARRANTY; without even the implied warranty of +## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +## GNU Lesser General Public License for more details. +## +## You should have received a copy of the GNU Lesser General Public License +## along with Hikari. If not, see . + <%! # Template configuration. Copy over in your template directory # (used with `--template-dir`) and adapt as necessary. @@ -17,11 +34,7 @@ # If set, format links to objects in online source code repository # according to this template. Supported keywords for interpolation # are: commit, path, start_line, end_line. - #git_link_template = 'https://github.com/USER/PROJECT/blob/{commit}/{path}#L{start_line}-L{end_line}' - #git_link_template = 'https://gitlab.com/USER/PROJECT/blob/{commit}/{path}#L{start_line}-L{end_line}' - #git_link_template = 'https://bitbucket.org/USER/PROJECT/src/{commit}/{path}#lines-{start_line}:{end_line}' - #git_link_template = 'https://CGIT_HOSTNAME/PROJECT/tree/{path}?id={commit}#n{start-line}' - git_link_template = None + git_link_template = 'https://gitlab.com/nekokatt/hikari/blob/{commit}/{path}#L{start_line}-L{end_line}' # A prefix to use for every HTML hyperlink in the generated documentation. # No prefix results in all links being relative. link_prefix = '' @@ -30,7 +43,7 @@ # Set the style keyword such as 'atom-one-light' or 'github-gist' # Options: https://github.com/highlightjs/highlight.js/tree/master/src/styles # Demo: https://highlightjs.org/static/demo/ - hljs_style = 'github' + hljs_style = 'rainbow' # If set, insert Google Analytics tracking code. Value is GA # tracking id (UA-XXXXXX-Y). google_analytics = '' @@ -38,11 +51,11 @@ # The whitespace-separated tokens represent arbitrary extra queries (at least one # must match) passed to regular Google search. Example: #search_query = 'inurl:github.com/USER/PROJECT site:PROJECT.github.io site:PROJECT.website' - search_query = '' + search_query = "inurl:github.com/nekokatt/hikari site:nekokatt.gitlab.io/hikari" # If set, render LaTeX math syntax within \(...\) (inline equations), # or within \[...\] or $$...$$ or `.. math::` (block equations) # as nicely-formatted math formulas using MathJax. # Note: in Python docstrings, either all backslashes need to be escaped (\\) # or you need to use raw r-strings. - latex_math = False + latex_math = True %> diff --git a/docs/css.mako b/docs/css.mako new file mode 100644 index 0000000000..d71b94d321 --- /dev/null +++ b/docs/css.mako @@ -0,0 +1,379 @@ +## Copyright © Nekokatt 2019-2020 +## +## This file is part of Hikari. +## +## Hikari is free software: you can redistribute it and/or modify +## it under the terms of the GNU Lesser General Public License as published by +## the Free Software Foundation, either version 3 of the License, or +## (at your option) any later version. +## +## Hikari is distributed in the hope that it will be useful, +## but WITHOUT ANY WARRANTY; without even the implied warranty of +## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +## GNU Lesser General Public License for more details. +## +## You should have received a copy of the GNU Lesser General Public License +## along with Hikari. If not, see . + +<%! + from pdoc.html_helpers import minify_css +%> + +<%def name="mobile()" filter="minify_css"> + .flex { + display: flex !important; + } + body { + background-color: #373b3e; + color: #FFFFFF; + line-height: 1.5em; + } + #content { + padding: 20px; + } + #sidebar { + padding: 30px; + overflow: hidden; + } + #sidebar > *:last-child { + margin-bottom: 2cm; + } + .http-server-breadcrumbs { + font-size: 130%; + margin: 0 0 15px 0; + } + #footer { + font-size: .75em; + padding: 5px 30px; + border-top: 1px solid #373b3e; + text-align: right; + } + #footer p { + margin: 0 0 0 1em; + display: inline-block; + } + #footer p:last-child { + margin-right: 30px; + } + h1, h2, h3, h4, h5 { + font-weight: 300; + } + h1 { + font-size: 2.5em; + line-height: 1.1em; + } + h2 { + font-size: 1.75em; + margin: 1em 0 .50em 0; + } + h3 { + font-size: 1.4em; + margin: 25px 0 10px 0; + } + h4 { + margin: 0; + font-size: 105%; + } + a { + color: #86cecb; + text-decoration: none; + transition: color .3s ease-in-out; + } + a:hover { + color: #137a7f; + } + .title code { + font-weight: bold; + } + h2[id^="header-"] { + margin-top: 2em; + } + .ident { + color: #e12885; + } + pre code { + font-size: .8em; + line-height: 1.4em; + } + code { + padding: 1px 4px; + overflow-wrap: break-word; + overflow: auto !important; + } + h1 code { background: transparent } + pre { + margin: 2em 0; + padding: 2ex; + } + #http-server-module-list { + display: flex; + flex-flow: column; + } + #http-server-module-list div { + display: flex; + } + #http-server-module-list dt { + min-width: 10%; + } + #http-server-module-list p { + margin-top: 0; + } + .toc ul, + #index { + list-style-type: none; + margin: 0; + padding: 0; + } + #index code { + background: transparent; + } + #index h3 { + border-bottom: 1px solid #373b3e; + } + #index ul { + padding: 0; + } + #index h4 { + margin-top: .6em; + font-weight: bold; + } + /* Make TOC lists have 2+ columns when viewport is wide enough. + Assuming ~20-character identifiers and ~30% wide sidebar. */ + @media (min-width: 200ex) { #index .two-column { column-count: 2 } } + @media (min-width: 300ex) { #index .two-column { column-count: 3 } } + dl { + margin-bottom: 2em; + } + dl dl:last-child { + margin-bottom: 4em; + } + dd { + margin: 0 0 1em 3em; + } + #header-classes + dl > dd { + margin-bottom: 3em; + } + dd dd { + margin-left: 2em; + } + dd p { + margin: 10px 0; + } + .name { + background: #373b3e; + font-weight: bold; + font-size: .85em; + padding: 5px 10px; + display: inline-block; + min-width: 40%; + } + .name > span:first-child { + white-space: nowrap; + } + .name.class > span:nth-child(2) { + margin-left: .4em; + } + .inherited { + border-left: 5px solid #bec8d1; + padding-left: 1em; + } + .inheritance em { + font-style: normal; + font-weight: bold; + } + /* Docstrings titles, e.g. in numpydoc format */ + .desc h2 { + font-weight: 400; + font-size: 1.25em; + } + .desc h3 { + font-size: 1em; + } + .desc dt code { + background: inherit; /* Don't grey-back parameters */ + } + .source summary, + .git-link-div { + color: #bec8d1; + text-align: right; + font-weight: 400; + font-size: .8em; + text-transform: uppercase; + } + .source summary > * { + white-space: nowrap; + cursor: pointer; + } + .git-link { + color: inherit; + margin-left: 1em; + } + .source pre { + max-height: 500px; + overflow: auto; + margin: 0; + } + .source pre code { + font-size: 12px; + overflow: visible; + } + .hlist { + list-style: none; + } + .hlist li { + display: inline; + } + .hlist li:after { + content: ',\2002'; + } + .hlist li:last-child:after { + content: none; + } + .hlist .hlist { + display: inline; + padding-left: 1em; + } + img { + max-width: 100%; + } + .admonition { + padding: .1em .5em; + margin-bottom: 1em; + } + .admonition-title { + font-weight: bold; + } + .admonition.note, + .admonition.info, + .admonition.important, + .admonition.todo, + .admonition.versionadded, + .admonition.tip, + .admonition.hint { + background: #505050; + } + .admonition.versionchanged, + .admonition.deprecated { + background: #cca300; + } + .admonition.warning, + .admonition.error, + .admonition.danger, + .admonition.caution { + background: #ff4d4d; + } + .gsc-control-cse { + background-color: #373b3e !important; + border-color: #373b3e !important; + } + .gsc-modal-background-image { + background-color: #373b3e !important; + } + + +<%def name="desktop()" filter="minify_css"> + @media screen and (min-width: 700px) { + #sidebar { + width: 30%; + height: 100vh; + overflow: auto; + position: sticky; + top: 0; + } + #content { + width: 70%; + max-width: 100ch; + padding: 3em 4em; + border-left: 1px solid #bec8d1; + } + pre code { + font-size: 1em; + } + .item .name { + font-size: 1em; + } + main { + display: flex; + flex-direction: row-reverse; + justify-content: flex-end; + } + .toc ul ul, + #index ul { + padding-left: 1.5em; + } + .toc > ul > li { + margin-top: .5em; + } + } + + +<%def name="print()" filter="minify_css"> +@media print { + #sidebar h1 { + page-break-before: always; + } + .source { + display: none; + } +} +@media print { + * { + background: transparent !important; + color: #000 !important; /* Black prints faster: h5bp.com/s */ + box-shadow: none !important; + text-shadow: none !important; + } + a[href]:after { + content: " (" attr(href) ")"; + font-size: 90%; + } + /* Internal, documentation links, recognized by having a title, + don't need the URL explicity stated. */ + a[href][title]:after { + content: none; + } + abbr[title]:after { + content: " (" attr(title) ")"; + } + /* + * Don't show links for images, or javascript/internal links + */ + .ir a:after, + a[href^="javascript:"]:after, + a[href^="#"]:after { + content: ""; + } + pre, + blockquote { + border: 1px solid #373b3e; + page-break-inside: avoid; + } + thead { + display: table-header-group; /* h5bp.com/t */ + } + tr, + img { + page-break-inside: avoid; + } + img { + max-width: 100% !important; + } + @page { + margin: 0.5cm; + } + p, + h2, + h3 { + orphans: 3; + widows: 3; + } + h1, + h2, + h3, + h4, + h5, + h6 { + page-break-after: avoid; + } +} + \ No newline at end of file diff --git a/docs/head.mako b/docs/head.mako new file mode 100644 index 0000000000..81ff3b21ce --- /dev/null +++ b/docs/head.mako @@ -0,0 +1,24 @@ + + + + + + + From 187890855645b27ea279e9eec18f985ff81e26a4 Mon Sep 17 00:00:00 2001 From: Faster Speeding Date: Mon, 20 Apr 2020 20:27:46 +0100 Subject: [PATCH 184/922] Change docs to pdoc3 compatible format and add site formatting/style --- docs/config.mako | 10 +- docs/credits.mako | 19 + docs/css.mako | 7 +- docs/head.mako | 48 + docs/logo.mako | 42 + hikari/__init__.py | 29 +- hikari/applications.py | 240 ++-- hikari/audit_logs.py | 193 ++-- hikari/bases.py | 14 +- hikari/channels.py | 218 ++-- hikari/clients/bot_base.py | 169 ++- hikari/clients/configs.py | 520 ++++++--- hikari/clients/rest/__init__.py | 15 +- hikari/clients/rest/base.py | 4 +- hikari/clients/rest/channel.py | 503 +++++---- hikari/clients/rest/gateway.py | 15 +- hikari/clients/rest/guild.py | 799 +++++++------- hikari/clients/rest/invite.py | 22 +- hikari/clients/rest/me.py | 70 +- hikari/clients/rest/oauth2.py | 2 +- hikari/clients/rest/react.py | 92 +- hikari/clients/rest/user.py | 8 +- hikari/clients/rest/voice.py | 7 +- hikari/clients/rest/webhook.py | 128 +-- hikari/clients/runnable.py | 8 +- hikari/clients/shards.py | 200 +--- hikari/clients/test.py | 2 +- hikari/colors.py | 103 +- hikari/colours.py | 2 +- hikari/embeds.py | 263 ++--- hikari/emojis.py | 57 +- hikari/errors.py | 99 +- hikari/events.py | 602 ++++------ hikari/gateway_entities.py | 42 +- hikari/guilds.py | 1074 ++++++++---------- hikari/intents.py | 183 ++-- hikari/internal/__init__.py | 5 +- hikari/internal/assertions.py | 10 +- hikari/internal/conversions.py | 151 ++- hikari/internal/helpers.py | 59 +- hikari/internal/marshaller.c | 40 +- hikari/internal/marshaller.py | 132 ++- hikari/internal/meta.py | 54 +- hikari/internal/more_asyncio.py | 15 +- hikari/internal/more_collections.py | 16 +- hikari/internal/more_enums.py | 16 +- hikari/internal/more_typing.py | 92 +- hikari/internal/urls.py | 32 +- hikari/invites.py | 167 ++- hikari/media.py | 2 +- hikari/messages.py | 225 ++-- hikari/net/codes.py | 304 +++--- hikari/net/ratelimits.py | 500 ++++----- hikari/net/rest.py | 1575 +++++++++++++-------------- hikari/net/routes.py | 55 +- hikari/net/shards.py | 331 +++--- hikari/net/user_agents.py | 66 +- hikari/permissions.py | 88 +- hikari/snowflakes.py | 12 +- hikari/state/__init__.py | 8 - hikari/state/consumers.py | 6 +- hikari/state/dispatchers.py | 123 +-- hikari/state/event_managers.py | 79 +- hikari/state/stateless.py | 74 +- hikari/unset.py | 8 +- hikari/users.py | 125 +-- hikari/voices.py | 73 +- hikari/webhooks.py | 52 +- 68 files changed, 4689 insertions(+), 5615 deletions(-) create mode 100644 docs/credits.mako create mode 100644 docs/logo.mako diff --git a/docs/config.mako b/docs/config.mako index 7b6f419baf..751dcb4dd7 100644 --- a/docs/config.mako +++ b/docs/config.mako @@ -22,7 +22,7 @@ # config.mako only needs to contain values you want overridden. # You can also run pdoc with `--config KEY=VALUE` to override # individual values. - html_lang = 'en' + html_lang = "en" show_inherited_members = True extract_module_toc_into_sidebar = True list_class_variables_in_index = True @@ -34,19 +34,19 @@ # If set, format links to objects in online source code repository # according to this template. Supported keywords for interpolation # are: commit, path, start_line, end_line. - git_link_template = 'https://gitlab.com/nekokatt/hikari/blob/{commit}/{path}#L{start_line}-L{end_line}' + git_link_template = "https://gitlab.com/nekokatt/hikari/blob/{commit}/{path}#L{start_line}" # A prefix to use for every HTML hyperlink in the generated documentation. # No prefix results in all links being relative. - link_prefix = '' + link_prefix = "" # Enable syntax highlighting for code/source blocks by including Highlight.js syntax_highlighting = True # Set the style keyword such as 'atom-one-light' or 'github-gist' # Options: https://github.com/highlightjs/highlight.js/tree/master/src/styles # Demo: https://highlightjs.org/static/demo/ - hljs_style = 'rainbow' + hljs_style = "rainbow" # If set, insert Google Analytics tracking code. Value is GA # tracking id (UA-XXXXXX-Y). - google_analytics = '' + google_analytics = "" # If set, insert Google Custom Search search bar widget above the sidebar index. # The whitespace-separated tokens represent arbitrary extra queries (at least one # must match) passed to regular Google search. Example: diff --git a/docs/credits.mako b/docs/credits.mako new file mode 100644 index 0000000000..395245cfcb --- /dev/null +++ b/docs/credits.mako @@ -0,0 +1,19 @@ +## Copyright © Nekokatt 2019-2020 +## +## This file is part of Hikari. +## +## Hikari is free software: you can redistribute it and/or modify +## it under the terms of the GNU Lesser General Public License as published by +## the Free Software Foundation, either version 3 of the License, or +## (at your option) any later version. +## +## Hikari is distributed in the hope that it will be useful, +## but WITHOUT ANY WARRANTY; without even the implied warranty of +## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +## GNU Lesser General Public License for more details. +## +## You should have received a copy of the GNU Lesser General Public License +## along with Hikari. If not, see . + +

Licensed under GNU LGPLv3.

+

Copyright © Nekokatt 2019-2020.

diff --git a/docs/css.mako b/docs/css.mako index d71b94d321..53d06b3fc4 100644 --- a/docs/css.mako +++ b/docs/css.mako @@ -96,6 +96,7 @@ line-height: 1.4em; } code { + background: #42464a; padding: 1px 4px; overflow-wrap: break-word; overflow: auto !important; @@ -125,7 +126,7 @@ padding: 0; } #index code { - background: transparent; + background: #42464a; } #index h3 { border-bottom: 1px solid #373b3e; @@ -160,7 +161,7 @@ margin: 10px 0; } .name { - background: #373b3e; + background: #42464a; font-weight: bold; font-size: .85em; padding: 5px 10px; @@ -376,4 +377,4 @@ page-break-after: avoid; } } - \ No newline at end of file + diff --git a/docs/head.mako b/docs/head.mako index 81ff3b21ce..34a415341b 100644 --- a/docs/head.mako +++ b/docs/head.mako @@ -16,9 +16,57 @@ | You should have received a copy of the GNU Lesser General Public License | along with Hikari. If not, see . !--> +<%! + from pdoc.html_helpers import minify_css +%> + + +<%def name="homelink()" filter="minify_css"> + .homelink { + display: block; + font-size: 2em; + font-weight: bold; + color: #ffffff; + border-bottom: .5px; + } + .homelink:hover { + color: inherit; + } + .homelink img { + max-width: 20%; + max-height: 5em; + margin: auto; + margin-bottom: .3em; + border-radius: 100%; + } + .homelink-footer { + border-bottom: 0em; + padding: 0em; + margin-bottom: 0em; + margain: auto; + } + +<%def name="links()" filter="minify_css"> + .links { + margin: auto; + margin-top: 0.5em; + margin-bottom: 1em; + list-style: none; + padding: 0em; + float: left; + background: transparent; + color: inherit; + font-size: 1.2em; + } + .links > li { + display: inline; + } + + + diff --git a/docs/logo.mako b/docs/logo.mako new file mode 100644 index 0000000000..0bd3681aa1 --- /dev/null +++ b/docs/logo.mako @@ -0,0 +1,42 @@ +## Copyright © Nekokatt 2019-2020 +## +## This file is part of Hikari. +## +## Hikari is free software: you can redistribute it and/or modify +## it under the terms of the GNU Lesser General Public License as published by +## the Free Software Foundation, either version 3 of the License, or +## (at your option) any later version. +## +## Hikari is distributed in the hope that it will be useful, +## but WITHOUT ANY WARRANTY; without even the implied warranty of +## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +## GNU Lesser General Public License for more details. +## +## You should have received a copy of the GNU Lesser General Public License +## along with Hikari. If not, see . +<%! + from distutils import version + + import hikari + + version = "staging" if "dev" in version.LooseVersion(hikari.__version__).version else "production" +%> + +
+ + + Hikari + + + % if version == "production": + + % endif + +
diff --git a/hikari/__init__.py b/hikari/__init__.py index 8b2faa13e9..b4c5a90afd 100644 --- a/hikari/__init__.py +++ b/hikari/__init__.py @@ -68,30 +68,5 @@ from hikari.voices import * from hikari.webhooks import * -# Import everything into this namespace. - -__all__ = [ - *audit_logs.__all__, - *channels.__all__, - *clients.__all__, - *colors.__all__, - *colours.__all__, - *embeds.__all__, - *emojis.__all__, - *bases.__all__, - *events.__all__, - *gateway_entities.__all__, - *guilds.__all__, - *intents.__all__, - *invites.__all__, - *messages.__all__, - *net.__all__, - *applications.__all__, - *permissions.__all__, - *bases.__all__, - *state.__all__, - *unset.__all__, - *users.__all__, - *voices.__all__, - *webhooks.__all__, -] +# Adding everything to `__all__` pollutes the top level index in our documentation, therefore this is left empty. +__all__ = [] diff --git a/hikari/applications.py b/hikari/applications.py index 30b2c411b9..9dc64618af 100644 --- a/hikari/applications.py +++ b/hikari/applications.py @@ -17,7 +17,16 @@ # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . """Components and entities related to discord's Oauth2 flow.""" -__all__ = ["Application", "ApplicationOwner", "OwnGuild", "Team", "TeamMember", "TeamMembershipState"] +__all__ = [ + "Application", + "ApplicationOwner", + "ConnectionVisibility", + "OwnConnection", + "OwnGuild", + "Team", + "TeamMember", + "TeamMembershipState", +] import enum import typing @@ -36,11 +45,11 @@ class ConnectionVisibility(enum.IntEnum): """Describes who can see a connection with a third party account.""" - #: Only you can see the connection. NONE = 0 + """Only you can see the connection.""" - #: Everyone can see the connection. EVERYONE = 1 + """Everyone can see the connection.""" @marshaller.marshallable() @@ -48,37 +57,25 @@ class ConnectionVisibility(enum.IntEnum): class OwnConnection(bases.HikariEntity, marshaller.Deserializable): """Represents a user's connection with a third party account. - Returned by the ``GET Current User Connections`` endpoint. + Returned by the `GET Current User Connections` endpoint. """ - #: The string ID of the third party connected account. - #: - #: Warning - #: ------- - #: Seeing as this is a third party ID, it will not be a snowflake. - #: - #: - #: :type: :obj:`~str` id: str = marshaller.attrib(deserializer=str) + """The string ID of the third party connected account. + + !!! warning + Seeing as this is a third party ID, it will not be a snowflake. + """ - #: The username of the connected account. - #: - #: :type: :obj:`~str` name: str = marshaller.attrib(deserializer=str) + """The username of the connected account.""" - #: The type of service this connection is for. - #: - #: :type: :obj:`~str` type: str = marshaller.attrib(deserializer=str) + """The type of service this connection is for.""" - #: Whether the connection has been revoked. - #: - #: :type: :obj:`~bool` is_revoked: bool = marshaller.attrib(raw_name="revoked", deserializer=bool, if_undefined=False, default=False) + """Whether the connection has been revoked.""" - #: A sequence of the partial guild integration objects this connection has. - #: - #: :type: :obj:`~typing.Sequence` [ :obj:`~hikari.guilds.PartialGuildIntegration` ] integrations: typing.Sequence[guilds.PartialGuildIntegration] = marshaller.attrib( deserializer=lambda payload: [ guilds.PartialGuildIntegration.deserialize(integration) for integration in payload @@ -86,27 +83,19 @@ class OwnConnection(bases.HikariEntity, marshaller.Deserializable): if_undefined=list, factory=list, ) + """A sequence of the partial guild integration objects this connection has.""" - #: Whether the connection has been verified. - #: - #: :type: :obj:`~bool` is_verified: bool = marshaller.attrib(raw_name="verified", deserializer=bool) + """Whether the connection has been verified.""" - #: Whether friends should be added based on this connection. - #: - #: :type: :obj:`~bool` is_friend_syncing: bool = marshaller.attrib(raw_name="friend_sync", deserializer=bool) + """Whether friends should be added based on this connection.""" - #: Whether activities related to this connection will be shown in the - #: user's presence updates. - #: - #: :type: :obj:`~bool` is_showing_activity: bool = marshaller.attrib(raw_name="show_activity", deserializer=bool) + """Whether this connection's activities are shown in the user's presence.""" - #: The visibility of the connection. - #: - #: :type: :obj:`~ConnectionVisibility` visibility: ConnectionVisibility = marshaller.attrib(deserializer=ConnectionVisibility) + """The visibility of the connection.""" @marshaller.marshallable() @@ -114,28 +103,24 @@ class OwnConnection(bases.HikariEntity, marshaller.Deserializable): class OwnGuild(guilds.PartialGuild): """Represents a user bound partial guild object.""" - #: Whether the current user owns this guild. - #: - #: :type: :obj:`~bool` is_owner: bool = marshaller.attrib(raw_name="owner", deserializer=bool) + """Whether the current user owns this guild.""" - #: The guild level permissions that apply to the current user or bot. - #: - #: :type: :obj:`~hikari.permissions.Permission` my_permissions: permissions.Permission = marshaller.attrib( raw_name="permissions", deserializer=permissions.Permission ) + """The guild level permissions that apply to the current user or bot.""" @enum.unique class TeamMembershipState(enum.IntEnum): """Represents the state of a user's team membership.""" - #: Denotes the user has been invited to the team but has yet to accept. INVITED = 1 + """Denotes the user has been invited to the team but has yet to accept.""" - #: Denotes the user has accepted the invite and is now a member. ACCEPTED = 2 + """Denotes the user has accepted the invite and is now a member.""" @marshaller.marshallable() @@ -143,26 +128,20 @@ class TeamMembershipState(enum.IntEnum): class TeamMember(bases.HikariEntity, marshaller.Deserializable): """Represents a member of a Team.""" - #: The state of this user's membership. - #: - #: :type: :obj:`~TeamMembershipState` membership_state: TeamMembershipState = marshaller.attrib(deserializer=TeamMembershipState) + """The state of this user's membership.""" - #: This member's permissions within a team. - #: Will always be ``["*"]`` until Discord starts using this. - #: - #: :type: :obj:`~typing.Set` [ :obj:`~str` ] permissions: typing.Set[str] = marshaller.attrib(deserializer=set) + """This member's permissions within a team. + + Will always be `["*"]` until Discord starts using this. + """ - #: The ID of the team this member belongs to. - #: - #: :type: :obj:`~hikari.bases.Snowflake` team_id: bases.Snowflake = marshaller.attrib(deserializer=bases.Snowflake.deserialize) + """The ID of the team this member belongs to.""" - #: The user object of this team member. - #: - #: :type: :obj:`~hikari.users.User` user: users.User = marshaller.attrib(deserializer=users.User.deserialize) + """The user object of this team member.""" @marshaller.marshallable() @@ -170,22 +149,16 @@ class TeamMember(bases.HikariEntity, marshaller.Deserializable): class Team(bases.UniqueEntity, marshaller.Deserializable): """Represents a development team, along with all its members.""" - #: The hash of this team's icon, if set. - #: - #: :type: :obj:`~str`, optional icon_hash: typing.Optional[str] = marshaller.attrib(raw_name="icon", deserializer=str) + """The hash of this team's icon, if set.""" - #: The member's that belong to this team. - #: - #: :type: :obj:`~typing.Mapping` [ :obj:`~hikari.bases.Snowflake`, :obj:`~TeamMember` ] members: typing.Mapping[bases.Snowflake, TeamMember] = marshaller.attrib( deserializer=lambda members: {m.user.id: m for m in map(TeamMember.deserialize, members)} ) + """The member's that belong to this team.""" - #: The ID of this team's owner. - #: - #: :type: :obj:`~hikari.bases.Snowflake` owner_user_id: bases.Snowflake = marshaller.attrib(deserializer=bases.Snowflake.deserialize) + """The ID of this team's owner.""" @property def icon_url(self) -> typing.Optional[str]: @@ -197,22 +170,22 @@ def format_icon_url(self, fmt: str = "png", size: int = 4096) -> typing.Optional Parameters ---------- - fmt : :obj:`~str` - The format to use for this URL, defaults to ``png``. - Supports ``png``, ``jpeg``, ``jpg`` and ``webp``. - size : :obj:`~int` - The size to set for the URL, defaults to ``4096``. Can be any power - of two between 16 and 4096 inclusive. + fmt : str + The format to use for this URL, defaults to `png`. + Supports `png`, `jpeg`, `jpg` and `webp`. + size : int + The size to set for the URL, defaults to `4096`. Can be any power + of two between `16` and `4096` inclusive. Returns ------- - :obj:`~str`, optional + str, optional The string URL. Raises ------ - :obj:`~ValueError` - If ``size`` is not a power of two or not between 16 and 4096. + ValueError + If `size` is not a power of two or not between 16 and 4096. """ if self.icon_hash: return urls.generate_cdn_url("team-icons", str(self.id), self.icon_hash, fmt=fmt, size=size) @@ -224,10 +197,8 @@ def format_icon_url(self, fmt: str = "png", size: int = 4096) -> typing.Optional class ApplicationOwner(users.User): """Represents the user who owns an application, may be a team user.""" - #: This user's flags. - #: - #: :type: :obj:`~hikari.users.UserFlag` flags: int = marshaller.attrib(deserializer=users.UserFlag) + """This user's flags.""" @property def is_team_user(self) -> bool: @@ -240,100 +211,81 @@ def is_team_user(self) -> bool: class Application(bases.UniqueEntity, marshaller.Deserializable): """Represents the information of an Oauth2 Application.""" - #: The name of this application. - #: - #: :type: :obj:`~str` name: str = marshaller.attrib(deserializer=str) + """The name of this application.""" - #: The description of this application, will be an empty string if unset. - #: - #: :type: :obj:`~str` description: str = marshaller.attrib(deserializer=str) + """The description of this application, will be an empty string if unset.""" - #: Whether the bot associated with this application is public. - #: Will be :obj:`~None` if this application doesn't have an associated bot. - #: - #: :type: :obj:`~bool`, optional is_bot_public: typing.Optional[bool] = marshaller.attrib( raw_name="bot_public", deserializer=bool, if_undefined=None, default=None ) + """Whether the bot associated with this application is public. + + Will be `None` if this application doesn't have an associated bot. + """ - #: Whether the bot associated with this application is requiring code grant - #: for invites. Will be :obj:`~None` if this application doesn't have a bot. - #: - #: :type: :obj:`~bool`, optional is_bot_code_grant_required: typing.Optional[bool] = marshaller.attrib( raw_name="bot_require_code_grant", deserializer=bool, if_undefined=None, default=None ) + """Whether this application's bot is requiring code grant for invites. + + Will be `None` if this application doesn't have a bot. + """ - #: The object of this application's owner. - #: This should always be :obj:`~None` in application objects retrieved - #: outside Discord's oauth2 flow. - #: - #: :type: :obj:`~ApplicationOwner`, optional owner: typing.Optional[ApplicationOwner] = marshaller.attrib( deserializer=ApplicationOwner.deserialize, if_undefined=None, default=None ) + """The object of this application's owner. + + This should always be `None` in application objects retrieved outside + Discord's oauth2 flow. + """ - #: A collection of this application's rpc origin URLs, if rpc is enabled. - #: - #: :type: :obj:`~typing.Set` [ :obj:`~str` ], optional rpc_origins: typing.Optional[typing.Set[str]] = marshaller.attrib(deserializer=set, if_undefined=None, default=None) + """A collection of this application's rpc origin URLs, if rpc is enabled.""" - #: This summary for this application's primary SKU if it's sold on Discord. - #: Will be an empty string if unset. - #: - #: :type: :obj:`~str` summary: str = marshaller.attrib(deserializer=str) + """This summary for this application's primary SKU if it's sold on Discord. + + Will be an empty string if unset. + """ - #: The base64 encoded key used for the GameSDK's ``GetTicket``. - #: - #: :type: :obj:`~bytes`, optional verify_key: typing.Optional[bytes] = marshaller.attrib( deserializer=lambda key: bytes(key, "utf-8"), if_undefined=None, default=None ) + """The base64 encoded key used for the GameSDK's `GetTicket`.""" - #: The hash of this application's icon, if set. - #: - #: :type: :obj:`~str`, optional icon_hash: typing.Optional[str] = marshaller.attrib( raw_name="icon", deserializer=str, if_undefined=None, default=None ) + """The hash of this application's icon, if set.""" - #: This application's team if it belongs to one. - #: - #: :type: :obj:`~Team`, optional team: typing.Optional[Team] = marshaller.attrib( deserializer=Team.deserialize, if_undefined=None, if_none=None, default=None ) + """This application's team if it belongs to one.""" - #: The ID of the guild this application is linked to - #: if it's sold on Discord. - #: - #: :type: :obj:`~hikari.bases.Snowflake`, optional guild_id: typing.Optional[bases.Snowflake] = marshaller.attrib( deserializer=bases.Snowflake.deserialize, if_undefined=None, default=None ) + """The ID of the guild this application is linked to if it's sold on Discord.""" - #: The ID of the primary "Game SKU" of a game that's sold on Discord. - #: - #: :type: :obj:`~hikari.bases.Snowflake`, optional primary_sku_id: typing.Optional[bases.Snowflake] = marshaller.attrib( deserializer=bases.Snowflake.deserialize, if_undefined=None, default=None ) + """The ID of the primary "Game SKU" of a game that's sold on Discord.""" - #: The URL slug that links to this application's store page - #: if it's sold on Discord. - #: - #: :type: :obj:`~str`, optional slug: typing.Optional[str] = marshaller.attrib(deserializer=str, if_undefined=None, default=None) + """The URL slug that links to this application's store page. + + Only applicable to applications sold on Discord. + """ - #: The hash of this application's cover image on it's store, if set. - #: - #: :type: :obj:`~str`, optional cover_image_hash: typing.Optional[str] = marshaller.attrib( raw_name="cover_image", deserializer=str, if_undefined=None, default=None ) + """The hash of this application's cover image on it's store, if set.""" @property def icon_url(self) -> typing.Optional[str]: @@ -345,22 +297,22 @@ def format_icon_url(self, fmt: str = "png", size: int = 4096) -> typing.Optional Parameters ---------- - fmt : :obj:`~str` - The format to use for this URL, defaults to ``png``. - Supports ``png``, ``jpeg``, ``jpg`` and ```webp``. - size : :obj:`~int` - The size to set for the URL, defaults to ``4096``. + fmt : str + The format to use for this URL, defaults to `png`. + Supports `png`, `jpeg`, `jpg` and `webp`. + size : int + The size to set for the URL, defaults to `4096`. Can be any power of two between 16 and 4096. Returns ------- - :obj:`~str`, optional + str, optional The string URL. Raises ------ - :obj:`~ValueError` - If ``size`` is not a power of two or not between 16 and 4096. + ValueError + If `size` is not a power of two or not between 16 and 4096. """ if self.icon_hash: return urls.generate_cdn_url("app-icons", str(self.id), self.icon_hash, fmt=fmt, size=size) @@ -376,22 +328,22 @@ def format_cover_image_url(self, fmt: str = "png", size: int = 4096) -> typing.O Parameters ---------- - fmt : :obj:`~str` - The format to use for this URL, defaults to ``png``. - Supports ``png``, ``jpeg``, ``jpg`` and ``webp``. - size : :obj:`~int` - The size to set for the URL, defaults to ``4096``. + fmt : str + The format to use for this URL, defaults to `png`. + Supports `png`, `jpeg`, `jpg` and `webp`. + size : int + The size to set for the URL, defaults to `4096`. Can be any power of two between 16 and 4096. Returns ------- - :obj:`~str`, optional + str, optional The string URL. Raises ------ - :obj:`~ValueError` - If ``size`` is not a power of two or not between 16 and 4096. + ValueError + If `size` is not a power of two or not between 16 and 4096. """ if self.cover_image_hash: return urls.generate_cdn_url("app-assets", str(self.id), self.cover_image_hash, fmt=fmt, size=size) diff --git a/hikari/audit_logs.py b/hikari/audit_logs.py index 62912e978f..f7eeb0c5a4 100644 --- a/hikari/audit_logs.py +++ b/hikari/audit_logs.py @@ -33,6 +33,7 @@ "MessageBulkDeleteEntryInfo", "MessageDeleteEntryInfo", "MessagePinEntryInfo", + "UnrecognisedAuditLogEntryInfo", ] import abc @@ -111,8 +112,8 @@ class AuditLogChangeKey(str, enum.Enum): RATE_LIMIT_PER_USER = "rate_limit_per_user" SYSTEM_CHANNEL_ID = "system_channel_id" - #: Alias for "COLOR" COLOUR = COLOR + """Alias for "COLOR""" def __str__(self) -> str: return self.name @@ -164,21 +165,16 @@ def __str__(self) -> str: @marshaller.marshallable() @attr.s(slots=True, kw_only=True) class AuditLogChange(bases.HikariEntity, marshaller.Deserializable): - """Represents a change made to an audit log entry's target entity. - - Attributes - ---------- - new_value : typing.Any, optional - The new value of the key, if something was added or changed. - old_value : typing.Any, optional - The old value of the key, if something was removed or changed. - key : typing.Union [ AuditLogChangeKey, str ] - The name of the audit log change's key. - """ + """Represents a change made to an audit log entry's target entity.""" new_value: typing.Optional[typing.Any] = marshaller.attrib() + """The new value of the key, if something was added or changed.""" + old_value: typing.Optional[typing.Any] = marshaller.attrib() + """The old value of the key, if something was removed or changed.""" + key: typing.Union[AuditLogChangeKey, str] = marshaller.attrib() + """The name of the audit log change's key.""" @classmethod def deserialize(cls, payload: typing.Mapping[str, str]) -> "AuditLogChange": @@ -252,7 +248,7 @@ def register_audit_log_entry_info( Returns ------- - ``decorator(T) -> T`` + decorator(T) -> T The decorator to decorate the class with. """ @@ -284,21 +280,15 @@ class ChannelOverwriteEntryInfo(BaseAuditLogEntryInfo): Will be attached to the overwrite create, update and delete audit log entries. - - Attributes - ---------- - id : hikari.snowflakes.Snowflake - The ID of the overwrite being updated, added or removed (and the entity - it targets). - type : hikari.channels.PermissionOverwriteType - The type of entity this overwrite targets. - role_name : str, optional - The name of the role this overwrite targets, if it targets a role. """ id: bases.Snowflake = marshaller.attrib(deserializer=bases.Snowflake.deserialize) + """The ID of the overwrite being updated, added or removed.""" type: channels.PermissionOverwriteType = marshaller.attrib(deserializer=channels.PermissionOverwriteType) + """The type of entity this overwrite targets.""" + role_name: typing.Optional[str] = marshaller.attrib(deserializer=str, if_undefined=None, default=None) + """The name of the role this overwrite targets, if it targets a role.""" @register_audit_log_entry_info(AuditLogEventType.MESSAGE_PIN, AuditLogEventType.MESSAGE_UNPIN) @@ -308,107 +298,77 @@ class MessagePinEntryInfo(BaseAuditLogEntryInfo): """The extra information for message pin related audit log entries. Will be attached to the message pin and message unpin audit log entries. - - Attributes - ---------- - channel_id : hikari.snowflakes.Snowflake - The ID of the guild text based channel where this pinned message is - being added or removed. - message_id : hikari.snowflakes.Snowflake - The ID of the message that's being pinned or unpinned. """ channel_id: bases.Snowflake = marshaller.attrib(deserializer=bases.Snowflake.deserialize) + """The ID of the text based channel where a pinned message is being targeted.""" + message_id: bases.Snowflake = marshaller.attrib(deserializer=bases.Snowflake.deserialize) + """The ID of the message that's being pinned or unpinned.""" @register_audit_log_entry_info(AuditLogEventType.MEMBER_PRUNE) @marshaller.marshallable() @attr.s(slots=True, kw_only=True) class MemberPruneEntryInfo(BaseAuditLogEntryInfo): - """Represents the extra information attached to guild prune log entries. - - Attributes - ---------- - delete_member_days : datetime.timedelta - The timedelta of how many days members were pruned for inactivity based - on. - members_removed : int - The number of members who were removed by this prune. - """ + """Represents the extra information attached to guild prune log entries.""" delete_member_days: datetime.timedelta = marshaller.attrib( deserializer=lambda payload: datetime.timedelta(days=int(payload)) ) + """The timedelta of how many days members were pruned for inactivity based on.""" + members_removed: int = marshaller.attrib(deserializer=int) + """The number of members who were removed by this prune.""" @register_audit_log_entry_info(AuditLogEventType.MESSAGE_BULK_DELETE) @marshaller.marshallable() @attr.s(slots=True, kw_only=True) class MessageBulkDeleteEntryInfo(BaseAuditLogEntryInfo): - """Represents extra information for the message bulk delete audit entry. - - Attributes - ---------- - count : int - The amount of messages that were deleted. - """ + """Represents extra information for the message bulk delete audit entry.""" count: int = marshaller.attrib(deserializer=int) + """The amount of messages that were deleted.""" @register_audit_log_entry_info(AuditLogEventType.MESSAGE_DELETE) @marshaller.marshallable() @attr.s(slots=True, kw_only=True) class MessageDeleteEntryInfo(MessageBulkDeleteEntryInfo): - """Represents extra information attached to the message delete audit entry. - - Attributes - ---------- - channel_id : hikari.snowflakes.Snowflake - The guild text based channel where these message(s) were deleted. - count : int - The amount of messages that were deleted. - """ + """Represents extra information attached to the message delete audit entry.""" channel_id: bases.Snowflake = marshaller.attrib(deserializer=bases.Snowflake.deserialize) + """The guild text based channel where these message(s) were deleted.""" @register_audit_log_entry_info(AuditLogEventType.MEMBER_DISCONNECT) @marshaller.marshallable() @attr.s(slots=True, kw_only=True) class MemberDisconnectEntryInfo(BaseAuditLogEntryInfo): - """Represents extra information for the voice chat member disconnect entry. - - Attributes - ---------- - count : int - The amount of members who were disconnected from voice in this entry. - """ + """Represents extra information for the voice chat member disconnect entry.""" count: int = marshaller.attrib(deserializer=int) + """The amount of members who were disconnected from voice in this entry.""" @register_audit_log_entry_info(AuditLogEventType.MEMBER_MOVE) @marshaller.marshallable() @attr.s(slots=True, kw_only=True) class MemberMoveEntryInfo(MemberDisconnectEntryInfo): - """Represents extra information for the voice chat based member move entry. - - Attributes - ---------- - channel_id : hikari.snowflakes.Snowflake - The channel these member(s) were moved to. - count : int - The amount of members who were disconnected from voice in this entry. - """ + """Represents extra information for the voice chat based member move entry.""" channel_id: bases.Snowflake = marshaller.attrib(deserializer=bases.Snowflake.deserialize) + """The amount of members who were disconnected from voice in this entry.""" class UnrecognisedAuditLogEntryInfo(BaseAuditLogEntryInfo): - """Represents any audit log entry options that haven't been implemented.""" + """Represents any audit log entry options that haven't been implemented. + + !!! note + This model has no slots and will have arbitrary undocumented attributes + (in it's `__dict__` based on the received payload). + """ def __init__(self, payload: typing.Mapping[str, str]) -> None: self.__dict__.update(payload) @@ -440,31 +400,23 @@ def get_entry_info_entity(type_: int) -> typing.Type[BaseAuditLogEntryInfo]: @marshaller.marshallable() @attr.s(slots=True, kw_only=True) class AuditLogEntry(bases.UniqueEntity, marshaller.Deserializable): - """Represents an entry in a guild's audit log. - - Attributes - ---------- - target_id : hikari.snowflakes.Snowflake, optional - The ID of the entity affected by this change, if applicable. - changes : typing.Sequence [ AuditLogChange ] - A sequence of the changes made to `AuditLogEntry.target_id`. - user_id : hikari.snowflakes.Snowflake - The ID of the user who made this change. - action_type : typing.Union [ AuditLogEventType, str ] - The type of action this entry represents. - options : BaseAuditLogEntryInfo, optional - Extra information about this entry. Will only be provided for certain - `action_type`. - reason : str, optional - The reason for this change, if set (between 0-512 characters). - """ + """Represents an entry in a guild's audit log.""" target_id: typing.Optional[bases.Snowflake] = marshaller.attrib() + """The ID of the entity affected by this change, if applicable.""" changes: typing.Sequence[AuditLogChange] = marshaller.attrib() + """A sequence of the changes made to `AuditLogEntry.target_id`.""" + user_id: bases.Snowflake = marshaller.attrib() + """The ID of the user who made this change.""" action_type: typing.Union[AuditLogEventType, str] = marshaller.attrib() + """The type of action this entry represents.""" + options: typing.Optional[BaseAuditLogEntryInfo] = marshaller.attrib() + """Extra information about this entry. Only be provided for certain `action_type`.""" + reason: typing.Optional[str] = marshaller.attrib() + """The reason for this change, if set (between 0-512 characters).""" @classmethod def deserialize(cls, payload: typing.Mapping[str, str]) -> "AuditLogEntry": @@ -495,34 +447,30 @@ def deserialize(cls, payload: typing.Mapping[str, str]) -> "AuditLogEntry": @marshaller.marshallable() @attr.s(slots=True, kw_only=True) class AuditLog(bases.HikariEntity, marshaller.Deserializable): - """Represents a guilds audit log. + """Represents a guilds audit log.""" - Attributes - ---------- - entries : typing.Mapping [ hikari.snowflakes.Snowflake, AuditLogEntry ] - A sequence of the audit log's entries. - integrations : typing.Mapping [ hikari.snowflakes.Snowflake, hikari.guilds.GuildIntegration ] - A mapping of the partial objects of integrations found in this audit log. - users : typing.Mapping [ hikari.bases.Snowflake, hikari.users.User` ] - A mapping of the objects of users found in this audit log. - webhooks : typing.Mapping [ hikari.snowflakes.Snowflake, hikari.webhooks.Webhook ] - A mapping of the objects of webhooks found in this audit log. - """ entries: typing.Mapping[bases.Snowflake, AuditLogEntry] = marshaller.attrib( raw_name="audit_log_entries", deserializer=lambda payload: {entry.id: entry for entry in map(AuditLogEntry.deserialize, payload)}, ) + """A sequence of the audit log's entries.""" + integrations: typing.Mapping[bases.Snowflake, guilds.GuildIntegration] = marshaller.attrib( deserializer=lambda payload: { integration.id: integration for integration in map(guilds.PartialGuildIntegration.deserialize, payload) } ) + """A mapping of the partial objects of integrations found in this audit log.""" + users: typing.Mapping[bases.Snowflake, _users.User] = marshaller.attrib( deserializer=lambda payload: {user.id: user for user in map(_users.User.deserialize, payload)} ) + """A mapping of the objects of users found in this audit log.""" + webhooks: typing.Mapping[bases.Snowflake, _webhooks.Webhook] = marshaller.attrib( deserializer=lambda payload: {webhook.id: webhook for webhook in map(_webhooks.Webhook.deserialize, payload)} ) + """A mapping of the objects of webhooks found in this audit log.""" class AuditLogIterator(typing.AsyncIterator[AuditLogEntry]): @@ -531,16 +479,6 @@ class AuditLogIterator(typing.AsyncIterator[AuditLogEntry]): This returns the audit log entries created before a given entry object/ID or from the newest audit log entry to the oldest. - Attributes - ---------- - integrations : typing.Mapping [ hikari.snowflakes.Snowflake, hikari.guilds.GuildIntegration ] - A mapping of the partial objects of integrations found in this audit log - so far. - users : typing.Mapping [ hikari.snowflakes.Snowflake, hikari.users.User ] - A mapping of the objects of users found in this audit log so far. - webhooks : typing.Mapping [ hikari.snowflakes.Snowflake, hikari.webhooks.Webhook ] - A mapping of the objects of webhooks found in this audit log so far. - Parameters ---------- guild_id : str @@ -557,14 +495,18 @@ class AuditLogIterator(typing.AsyncIterator[AuditLogEntry]): else unlimited. before : str If specified, an entry ID to specify where this iterator's returned - audit log entries should start . - - Note - ---- - This iterator's attributes `AuditLogIterator.integrations`, - `AuditLogIterator.users` and `AuditLogIterator.webhooks` will be filled up - as this iterator makes requests to the Get Guild Audit Log endpoint with - the relevant objects for entities referenced by returned entries. + audit log entries should start. + + Yields + ------ + AuditLogEntry + The entries found in this audit log. + + !!! note + This iterator's attributes `AuditLogIterator.integrations`, + `AuditLogIterator.users` and `AuditLogIterator.webhooks` will be filled + up as this iterator makes requests to the Get Guild Audit Log endpoint + with the relevant objects for entities referenced by returned entries. """ __slots__ = ( @@ -579,8 +521,13 @@ class AuditLogIterator(typing.AsyncIterator[AuditLogEntry]): ) integrations: typing.Mapping[bases.Snowflake, guilds.GuildIntegration] + """A mapping of the partial integrations objects found in this log so far.""" + users: typing.Mapping[bases.Snowflake, _users.User] + """A mapping of the objects of users found in this audit log so far.""" + webhooks: typing.Mapping[bases.Snowflake, _webhooks.Webhook] + """A mapping of the objects of webhooks found in this audit log so far.""" def __init__( self, @@ -614,7 +561,7 @@ async def __anext__(self) -> AuditLogEntry: raise StopAsyncIteration async def _fill(self) -> None: - """Retrieve entries before :attr:`_front` and add to :attr:`_buffer`.""" + """Retrieve entries before `_front` and add to `_buffer`.""" payload = await self._request( **self._kwargs, before=self._front if self._front is not None else ..., diff --git a/hikari/bases.py b/hikari/bases.py index 19d1dc5d94..dc6b7bc262 100644 --- a/hikari/bases.py +++ b/hikari/bases.py @@ -40,15 +40,13 @@ class HikariEntity(abc.ABC): class Snowflake(HikariEntity, typing.SupportsInt): """A concrete representation of a unique identifier for an object on Discord. - This object can be treated as a regular :obj:`~int` for most purposes. + This object can be treated as a regular `int` for most purposes. """ __slots__ = ("_value",) - #: The integer value of this ID. - #: - #: :type: :obj:`~int` _value: int + """The integer value of this ID.""" # noinspection PyMissingConstructor def __init__(self, value: typing.Union[int, str]) -> None: @@ -99,7 +97,7 @@ def serialize(self) -> str: @classmethod def deserialize(cls, value: str) -> "Snowflake": - """Take a :obj:`~str` ID and convert it into a Snowflake object.""" + """Take a `str` ID and convert it into a Snowflake object.""" return cls(value) @classmethod @@ -118,14 +116,12 @@ def from_timestamp(cls, timestamp: float) -> "Snowflake": class UniqueEntity(HikariEntity, typing.SupportsInt, abc.ABC): """A base for an entity that has an integer ID of some sort. - Casting an object of this type to an :obj:`~int` will produce the + Casting an object of this type to an `int` will produce the integer ID of the object. """ - #: The ID of this entity. - #: - #: :type: :obj:`~Snowflake` id: Snowflake = marshaller.attrib(hash=True, eq=True, repr=True, deserializer=Snowflake, serializer=str) + """The ID of this entity.""" def __int__(self) -> int: return int(self.id) diff --git a/hikari/channels.py b/hikari/channels.py index df517a3c19..aba57371b3 100644 --- a/hikari/channels.py +++ b/hikari/channels.py @@ -16,17 +16,7 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . -"""Components and entities that are used to describe both DMs and guild channels on Discord. - -.. inheritance-diagram:: - hikari.channels - enum.IntEnum - hikari.bases.HikariEntity - hikari.bases.Deserializable - hikari.bases.Serializable - hikari.bases.UniqueEntity - :parts: 1 -""" +"""Components and entities that are used to describe both DMs and guild channels on Discord.""" __all__ = [ "Channel", @@ -37,6 +27,7 @@ "DMChannel", "GroupDMChannel", "GuildCategory", + "GuildChannel", "GuildTextChannel", "GuildNewsChannel", "GuildStoreChannel", @@ -61,38 +52,37 @@ class ChannelType(enum.IntEnum): """The known channel types that are exposed to us by the API.""" - #: A text channel in a guild. GUILD_TEXT = 0 + """A text channel in a guild.""" - #: A direct channel between two users. DM = 1 + """A direct channel between two users.""" - #: A voice channel in a guild. GUILD_VOICE = 2 + """A voice channel in a guild.""" - #: A direct channel between multiple users. GROUP_DM = 3 + """A direct channel between multiple users.""" - #: An category used for organizing channels in a guild. GUILD_CATEGORY = 4 + """An category used for organizing channels in a guild.""" - #: A channel that can be followed and can crosspost. GUILD_NEWS = 5 + """A channel that can be followed and can crosspost.""" - #: A channel that show's a game's store page. GUILD_STORE = 6 + """A channel that show's a game's store page.""" @enum.unique class PermissionOverwriteType(str, enum.Enum): """The type of entity a Permission Overwrite targets.""" - #: A permission overwrite that targets all the members with a specific - #: guild role. ROLE = "role" + """A permission overwrite that targets all the members with a specific role.""" - #: A permission overwrite that targets a specific guild member. MEMBER = "member" + """A permission overwrite that targets a specific guild member.""" def __str__(self) -> str: return self.value @@ -103,24 +93,18 @@ def __str__(self) -> str: class PermissionOverwrite(bases.UniqueEntity, marshaller.Deserializable, marshaller.Serializable): """Represents permission overwrites for a channel or role in a channel.""" - #: The type of entity this overwrite targets. - #: - #: :type: :obj:`~PermissionOverwriteType` type: PermissionOverwriteType = marshaller.attrib(deserializer=PermissionOverwriteType, serializer=str) + """The type of entity this overwrite targets.""" - #: The permissions this overwrite allows. - #: - #: :type: :obj:`~hikari.permissions.Permission` allow: permissions.Permission = marshaller.attrib( deserializer=permissions.Permission, serializer=int, default=permissions.Permission(0) ) + """The permissions this overwrite allows.""" - #: The permissions this overwrite denies. - #: - #: :type: :obj:`~hikari.permissions.Permission` deny: permissions.Permission = marshaller.attrib( deserializer=permissions.Permission, serializer=int, default=permissions.Permission(0) ) + """The permissions this overwrite denies.""" @property def unset(self) -> permissions.Permission: @@ -135,12 +119,12 @@ def register_channel_type(type_: ChannelType) -> typing.Callable[[typing.Type["C Parameters ---------- - type_ : :obj:`~ChannelType` + type_ : ChannelType The channel type to associate with. Returns ------- - ``decorator(T) -> T`` + decorator(T) -> T The decorator to decorate the class with. """ @@ -158,10 +142,8 @@ def decorator(cls): class Channel(bases.UniqueEntity, marshaller.Deserializable): """Base class for all channels.""" - #: The channel's type. - #: - #: :type: :obj:`~ChannelType` type: ChannelType = marshaller.attrib(deserializer=ChannelType) + """The channel's type.""" @marshaller.marshallable() @@ -172,10 +154,8 @@ class PartialChannel(Channel): This is commonly received in REST responses. """ - #: The channel's name. - #: - #: :type: :obj:`~str` name: str = marshaller.attrib(deserializer=str) + """The channel's name.""" @register_channel_type(ChannelType.DM) @@ -184,22 +164,17 @@ class PartialChannel(Channel): class DMChannel(Channel): """Represents a DM channel.""" - #: The ID of the last message sent in this channel. - #: - #: Note - #: ---- - #: This might point to an invalid or deleted message. - #: - #: - #: :type: :obj:`~hikari.bases.Snowflake`, optional last_message_id: bases.Snowflake = marshaller.attrib(deserializer=bases.Snowflake.deserialize, if_none=None) + """The ID of the last message sent in this channel. + + !!! note + This might point to an invalid or deleted message. + """ - #: The recipients of the DM. - #: - #: :type: :obj:`~typing.Mapping` [ :obj:`~hikari.bases.Snowflake`, :obj:`~hikari.users.User` ] recipients: typing.Mapping[bases.Snowflake, users.User] = marshaller.attrib( deserializer=lambda recipients: {user.id: user for user in map(users.User.deserialize, recipients)} ) + """The recipients of the DM.""" @register_channel_type(ChannelType.GROUP_DM) @@ -208,28 +183,19 @@ class DMChannel(Channel): class GroupDMChannel(DMChannel): """Represents a DM group channel.""" - #: The group's name. - #: - #: :type: :obj:`~str` name: str = marshaller.attrib(deserializer=str) + """The group's name.""" - #: The ID of the owner of the group. - #: - #: :type: :obj:`~hikari.bases.Snowflake` owner_id: bases.Snowflake = marshaller.attrib(deserializer=bases.Snowflake.deserialize) + """The ID of the owner of the group.""" - #: The hash of the icon of the group. - #: - #: :type: :obj:`~str`, optional icon_hash: typing.Optional[str] = marshaller.attrib(raw_name="icon", deserializer=str, if_none=None) + """The hash of the icon of the group.""" - #: The ID of the application that created the group DM, if it's a - #: bot based group DM. - #: - #: :type: :obj:`~hikari.bases.Snowflake`, optional application_id: typing.Optional[bases.Snowflake] = marshaller.attrib( deserializer=bases.Snowflake.deserialize, if_undefined=None, default=None ) + """The ID of the application that created the group DM, if it's a bot based group DM.""" @marshaller.marshallable() @@ -237,37 +203,25 @@ class GroupDMChannel(DMChannel): class GuildChannel(Channel): """The base for anything that is a guild channel.""" - #: The ID of the guild the channel belongs to. - #: - #: :type: :obj:`~hikari.bases.Snowflake` guild_id: bases.Snowflake = marshaller.attrib(deserializer=bases.Snowflake.deserialize) + """The ID of the guild the channel belongs to.""" - #: The sorting position of the channel. - #: - #: :type: :obj:`~int` position: int = marshaller.attrib(deserializer=int) + """The sorting position of the channel.""" - #: The permission overwrites for the channel. - #: - #: :type: :obj:`~typing.Mapping` [ :obj:`~hikari.bases.Snowflake`, :obj:`~PermissionOverwrite` ] permission_overwrites: PermissionOverwrite = marshaller.attrib( deserializer=lambda overwrites: {o.id: o for o in map(PermissionOverwrite.deserialize, overwrites)} ) + """The permission overwrites for the channel.""" - #: The name of the channel. - #: - #: :type: :obj:`~str` name: str = marshaller.attrib(deserializer=str) + """The name of the channel.""" - #: Wheter the channel is marked as NSFW. - #: - #: :type: :obj:`~bool` is_nsfw: bool = marshaller.attrib(raw_name="nsfw", deserializer=bool) + """Whether the channel is marked as NSFW.""" - #: The ID of the parent category the channel belongs to. - #: - #: :type: :obj:`~hikari.bases.Snowflake`, optional parent_id: bases.Snowflake = marshaller.attrib(deserializer=bases.Snowflake.deserialize, if_none=None) + """The ID of the parent category the channel belongs to.""" @register_channel_type(ChannelType.GUILD_CATEGORY) @@ -283,33 +237,25 @@ class GuildCategory(GuildChannel): class GuildTextChannel(GuildChannel): """Represents a guild text channel.""" - #: The topic of the channel. - #: - #: :type: :obj:`~str`, optional topic: str = marshaller.attrib(deserializer=str, if_none=None) + """The topic of the channel.""" - #: The ID of the last message sent in this channel. - #: - #: Note - #: ---- - #: This might point to an invalid or deleted message. - #: - #: - #: :type: :obj:`~hikari.bases.Snowflake`, optional last_message_id: bases.Snowflake = marshaller.attrib(deserializer=bases.Snowflake.deserialize, if_none=None) + """The ID of the last message sent in this channel. + + !!! note + This might point to an invalid or deleted message. + """ - #: The delay (in seconds) between a user can send a message - #: to this channel. - #: - #: Note - #: ---- - #: Bots, as well as users with ``MANAGE_MESSAGES`` or - #: ``MANAGE_CHANNEL``, are not afected by this. - #: - #: :type: :obj:`~datetime.timedelta` rate_limit_per_user: datetime.timedelta = marshaller.attrib( deserializer=lambda payload: datetime.timedelta(seconds=payload) ) + """The delay (in seconds) between a user can send a message to this channel. + + !!! note + Bots, as well as users with `MANAGE_MESSAGES` or `MANAGE_CHANNEL`, + are not affected by this. + """ @register_channel_type(ChannelType.GUILD_NEWS) @@ -318,20 +264,15 @@ class GuildTextChannel(GuildChannel): class GuildNewsChannel(GuildChannel): """Represents an news channel.""" - #: The topic of the channel. - #: - #: :type: :obj:`~str`, optional topic: str = marshaller.attrib(deserializer=str, if_none=None) + """The topic of the channel.""" - #: The ID of the last message sent in this channel. - #: - #: Note - #: ---- - #: This might point to an invalid or deleted message. - #: - #: - #: :type: :obj:`~hikari.bases.Snowflake`, optional last_message_id: bases.Snowflake = marshaller.attrib(deserializer=bases.Snowflake.deserialize, if_none=None) + """The ID of the last message sent in this channel. + + !!! note + This might point to an invalid or deleted message. + """ @register_channel_type(ChannelType.GUILD_STORE) @@ -347,15 +288,11 @@ class GuildStoreChannel(GuildChannel): class GuildVoiceChannel(GuildChannel): """Represents an voice channel.""" - #: The bitrate for the voice channel (in bits). - #: - #: :type: :obj:`~int` bitrate: int = marshaller.attrib(deserializer=int) + """The bitrate for the voice channel (in bits).""" - #: The user limit for the voice channel. - #: - #: :type: :obj:`~int` user_limit: int = marshaller.attrib(deserializer=int) + """The user limit for the voice channel.""" class GuildChannelBuilder(marshaller.Serializable): @@ -365,13 +302,11 @@ class GuildChannelBuilder(marshaller.Serializable): ---------- channel_name : str The name to set for the channel. - channel_type : :obj:`~ChannelType` + channel_type : ChannelType The type of channel this should build. - Example - ------- - .. code-block:: python - + Examples + -------- channel_obj = ( channels.GuildChannelBuilder("Catgirl-appreciation", channels.ChannelType.GUILD_TEXT) .is_nsfw(True) @@ -402,16 +337,15 @@ def is_nsfw(self) -> "GuildChannelBuilder": def with_permission_overwrites(self, overwrites: typing.Sequence[PermissionOverwrite]) -> "GuildChannelBuilder": """Set the permission overwrites for this channel. - Note - ---- - Calling this multiple times will overwrite any previously added - overwrites. - Parameters ---------- - overwrites : :obj:`~typing.Sequence` [ :obj:`~PermissionOverwrite` ] + overwrites : typing.Sequence [ PermissionOverwrite ] A sequence of overwrite objects to add, where the first overwrite object + + !!! note + Calling this multiple times will overwrite any previously added + overwrites. """ self._payload["permission_overwrites"] = [o.serialize() for o in overwrites] return self @@ -421,7 +355,7 @@ def with_topic(self, topic: str) -> "GuildChannelBuilder": Parameters ---------- - topic : :obj:`~str` + topic : str The string topic to set. """ self._payload["topic"] = topic @@ -432,7 +366,7 @@ def with_bitrate(self, bitrate: int) -> "GuildChannelBuilder": Parameters ---------- - bitrate : :obj:`~int` + bitrate : int The bitrate to set in bits. """ self._payload["bitrate"] = int(bitrate) @@ -443,7 +377,7 @@ def with_user_limit(self, user_limit: int) -> "GuildChannelBuilder": Parameters ---------- - user_limit : :obj:`~int` + user_limit : int The user limit to set. """ self._payload["user_limit"] = int(user_limit) @@ -456,7 +390,7 @@ def with_rate_limit_per_user( Parameters ---------- - rate_limit_per_user : :obj:`~typing.Union` [ :obj:`~datetime.timedelta`, :obj:`~int` ] + rate_limit_per_user : typing.Union [ datetime.timedelta, int ] The amount of seconds users will have to wait before sending another message in the channel to set. """ @@ -472,7 +406,7 @@ def with_parent_category(self, category: typing.Union[bases.Snowflake, int]) -> Parameters ---------- - category : :obj:`~typing.Union` [ :obj:`~hikari.bases.Snowflake`, :obj:`~int` ] + category : typing.Union [ hikari.bases.Snowflake, int ] The placeholder ID of the category channel that should be this channel's parent. """ @@ -482,15 +416,14 @@ def with_parent_category(self, category: typing.Union[bases.Snowflake, int]) -> def with_id(self, channel_id: typing.Union[bases.Snowflake, int]) -> "GuildChannelBuilder": """Set the placeholder ID for this channel. - Notes - ----- - This ID is purely a place holder used for setting parent category - channels and will have no effect on the created channel's ID. - Parameters ---------- - channel_id : :obj:`~typing.Union` [ :obj:`~hikari.bases.Snowflake`, :obj:`~int` ] + channel_id : typing.Union [ hikari.bases.Snowflake, int ] The placeholder ID to use. + + !!! note + This ID is purely a place holder used for setting parent category + channels and will have no effect on the created channel's ID. """ self._payload["id"] = str(int(channel_id)) return self @@ -499,10 +432,9 @@ def with_id(self, channel_id: typing.Union[bases.Snowflake, int]) -> "GuildChann def deserialize_channel(payload: typing.Dict[str, typing.Any]) -> typing.Union[GuildChannel, DMChannel]: """Deserialize a channel object into the corresponding class. - Warning - ------- - This can only be used to deserialize full channel objects. To deserialize a - partial object, use ``PartialChannel.deserialize()``. + !!! warning + This can only be used to deserialize full channel objects. To + deserialize a partial object, use `PartialChannel.deserialize`. """ type_id = payload["type"] types = getattr(register_channel_type, "types", more_collections.EMPTY_DICT) diff --git a/hikari/clients/bot_base.py b/hikari/clients/bot_base.py index da5da1dfaa..85185b8bf8 100644 --- a/hikari/clients/bot_base.py +++ b/hikari/clients/bot_base.py @@ -57,59 +57,49 @@ class BotBase( """An abstract base class for a bot implementation. This takes several generic parameter types in the following order: - - ``ShardClientT`` - the implementation of - :obj:`~hikari.clients.shard_clients.ShardClient` to use for shards. - - ``RESTClientT`` - the implementation of - :obj:`~hikari.clients.rest.RESTClient` to use for API calls. - - ``EventDispatcherT`` - the implementation of - :obj:`~hikari.state.dispatchers.EventDispacher` to use for + + * `ShardClientT` - the implementation of + `hikari.clients.shards.ShardClient` to use for shards. + * `RESTClientT` - the implementation of + `hikari.clients.rest.RESTClient` to use for API calls. + * `EventDispatcherT` - the implementation of + `hikari.state.dispatchers.EventDispatcher` to use for dispatching events. This class will then delegate any calls inherited - from :obj:`~hikari.state.dispatchers.EventDispacher` to that + from `hikari.state.dispatchers.EventDispatcher` to that implementation when provided. - - ``EventManagerT`` - the implementation of - :obj:`~hikari.state.event_managers.EventManager` to use for + * `EventManagerT` - the implementation of + `hikari.state.event_managers.EventManager` to use for event management, translation, and dispatching. - - ``BotConfigT`` - the implementation of - :obj:`~hikari.clients.configs.BotConfig` to read component-specific + * `BotConfigT` - the implementation of + `hikari.clients.configs.BotConfig` to read component-specific details from. Parameters ---------- - config : :obj:`~hikari.clients.configs.BotConfig` + config : hikari.clients.configs.BotConfig The config object to use. """ - #: The config for this bot. - #: - #: :type: :obj:`~hikari.clients.configs.BotConfig` _config: BotConfigT + """The config for this bot.""" - #: The event dispatcher for this bot. - #: - #: :type: an implementation instance of :obj:`~hikari.state.dispatcher.EventDispatcher` event_dispatcher: EventDispatcherT + """The event dispatcher for this bot.""" - #: The event manager for this bot. - #: - #: :type: an implementation instance of :obj:`~hikari.state.event_managers.EventManager` event_manager: EventManagerT + """The event manager for this bot.""" - #: The logger to use for this bot. - #: - #: :type: :obj:`~logging.Logger` logger: logging.Logger + """The logger to use for this bot.""" - #: The REST HTTP client to use for this bot. - #: - #: :type: :obj:`~hikari.clients.rest.RESTClient` rest: RESTClientT + """The REST HTTP client to use for this bot.""" - #: Shards registered to this bot. - #: - #: These will be created once the bot has started execution. - #: - #: :type: :obj:`~typing.Mapping` [ :obj:`~int`, ? extends :obj:`~hikari.clients.shard_client.ShardClient` ] shards: typing.Mapping[int, ShardClientT] + """Shards registered to this bot. + + These will be created once the bot has started execution. + """ def __init__(self, config: configs.BotConfig) -> None: super().__init__(logging.getLogger(f"hikari.{type(self).__qualname__}")) @@ -125,18 +115,18 @@ def heartbeat_latency(self) -> float: This will return a mean of all the heartbeat intervals for all shards with a valid heartbeat latency that are in the - :obj:`~hikari.clients.shard_clients.ShardState.READY` state. + `hikari.clients.shards.ShardState.READY` state. - If no shards are in this state, this will return ``float('nan')`` + If no shards are in this state, this will return `float("nan")` instead. Returns ------- - :obj:`~float` - The mean latency for all ``READY`` shards that have sent at least - one acknowledged ``HEARTBEAT`` payload. If there is not at least + float + The mean latency for all `READY` shards that have sent at least + one acknowledged `HEARTBEAT` payload. If there is not at least one shard that meets this criteria, this will instead return - ``float('nan')``. + `float("nan")`. """ latencies = [] for shard in self.shards.values(): @@ -147,48 +137,25 @@ def heartbeat_latency(self) -> float: @property def total_disconnect_count(self) -> int: - """Total number of times any shard has disconnected. - - Returns - ------- - :obj:`int` - Total disconnect count. - """ + """Total number of times any shard has disconnected.""" return sum(s.disconnect_count for s in self.shards.values()) @property def total_reconnect_count(self) -> int: - """Total number of times any shard has reconnected. - - Returns - ------- - :obj:`int` - Total reconnect count. - """ + """Total number of times any shard has reconnected.""" return sum(s.reconnect_count for s in self.shards.values()) @property - def intents(self) -> typing.Optional[intents.Intent]: - """Intent values that any shard connections will be using. + def intents(self) -> typing.Optional[intents.Intent]: # noqa: D401 + """Intents that are in use for the connection. - Returns - ------- - :obj:`~hikari.intents.Intent`, optional - A :obj:`~enum.IntFlag` enum containing each intent that is set. If - intents are not being used at all, then this will return - :obj:`~None` instead. + If intents are not being used at all, then this will be `None` instead. """ return self._config.intents @property def version(self) -> float: - """Version being used for the gateway API. - - Returns - ------- - :obj:`~int` - The API version being used. - """ + """Version being used for the gateway API.""" return self._config.gateway_version async def start(self): @@ -290,29 +257,26 @@ async def update_presence( Any arguments that you do not explicitly provide some value for will not be changed. - Warning - ------- - This will only apply to connected shards. + !!! warning + This will only apply to connected shards. - Notes - ----- - If you wish to update a presence for a specific shard, you can do this - by using the ``shards`` :obj:`~typing.Mapping` to find the shard you - wish to update. + !!! note + If you wish to update a presence for a specific shard, you can do this + by using the `shards` `typing.Mapping` to find the shard you wish to + update. Parameters ---------- - status : :obj:`~hikari.guilds.PresenceStatus` + status : hikari.guilds.PresenceStatus If specified, the new status to set. - activity : :obj:`~hikari.gateway_entities.GatewayActivity`, optional + activity : hikari.gateway_entities.Activity, optional If specified, the new activity to set. - idle_since : :obj:`~datetime.datetime`, optional + idle_since : datetime.datetime, optional If specified, the time to show up as being idle since, - or :obj:`~None` if not applicable. - is_afk : :obj:`~bool` - If specified, :obj:`~True` if the user should be marked as AFK, - or :obj:`~False` otherwise. - + or `None` if not applicable. + is_afk : bool + If specified, `True` if the user should be marked as AFK, + or `False` otherwise. """ await asyncio.gather( *( @@ -331,30 +295,28 @@ def _create_shard( Parameters ---------- - shard_id : :obj:`~int` + shard_id : int The shard ID to use. - shard_count : :obj:`~int` + shard_count : int The shard count to use. - url : :obj:`~str` + url : str The gateway URL to connect to. - config : :obj:`~hikari.clients.configs.BotConfig` + config : hikari.clients.configs.BotConfig The bot config to use. - event_manager :obj:`~hikari.state.event_managers.EventManager` + event_manager hikari.state.event_managers.EventManager The event manager to use. Returns ------- - :obj:`~hikari.clients.shard_clients.ShardClient` + hikari.clients.shards.ShardClient The shard client implementation to use for the given shard ID. - Notes - ----- - The ``shard_id`` and ``shard_count`` may be set within the ``config`` - object passed, but any conforming implementations are expected to - use the value passed in the ``shard_id` and ``shard_count`` parameters - regardless. Failure to do so may result in an invalid sharding - configuration being used. - + !!! note + The `shard_id` and `shard_count` may be set within the `config` + object passed, but any conforming implementations are expected to + use the value passed in the `shard_id` and `shard_count` parameters + regardless. Failure to do so may result in an invalid sharding + configuration being used. """ @classmethod @@ -364,12 +326,12 @@ def _create_rest(cls, config: BotConfigT) -> RESTClientT: Parameters ---------- - config : :obj:`~hikari.clients.configs.BotConfig` + config : hikari.clients.configs.BotConfig The bot config to use. Returns ------- - :obj:`~hikari.clients.rest.RESTClient` + hikari.clients.rest.RESTClient The REST client to use. """ @@ -381,12 +343,12 @@ def _create_event_manager(cls, config: BotConfigT, dispatcher: EventDispatcherT) Parameters ---------- - config : :obj:`~hikari.clients.configs.BotConfig` + config : hikari.clients.configs.BotConfig The bot config to use. Returns ------- - :obj:`~hikari.state.event_managers.EventManager` + hikari.state.event_managers.EventManager The event manager to use internally. """ @@ -398,11 +360,10 @@ def _create_event_dispatcher(cls, config: BotConfigT) -> EventDispatcherT: Parameters ---------- - config : :obj:`~hikari.clients.configs.BotConfig` + config : hikari.clients.configs.BotConfig` The bot config to use. Returns ------- - :obj:`~hikari.state.dispatchers.EventDispatcher` - + hikari.state.dispatchers.EventDispatcher """ diff --git a/hikari/clients/configs.py b/hikari/clients/configs.py index 02a515d8e6..13c51afacd 100644 --- a/hikari/clients/configs.py +++ b/hikari/clients/configs.py @@ -52,118 +52,106 @@ class BaseConfig(marshaller.Deserializable): @marshaller.marshallable() @attr.s(kw_only=True) class DebugConfig(BaseConfig): - """Configuration for anything with a debugging mode.""" + """Configuration for anything with a debugging mode. + + Attributes + ---------- + debug : bool + Whether to enable debugging mode. Usually you don't want to enable this. + """ - #: Whether to enable debugging mode. Usually you don't want to enable this. - #: - #: :type: :obj:`~bool` debug: bool = marshaller.attrib(deserializer=bool, if_undefined=False, default=False) @marshaller.marshallable() @attr.s(kw_only=True) class AIOHTTPConfig(BaseConfig): - """Config for components that use AIOHTTP somewhere.""" - - #: If :obj:`~True`, allow following redirects from ``3xx`` HTTP responses. - #: Generally you do not want to enable this unless you have a good reason - #: to. - #: - #: Defaults to :obj:`~False` if unspecified during deserialization. - #: - #: :type: :obj:`~bool` + """Config for components that use AIOHTTP somewhere. + + Attributes + ---------- + allow_redirects : bool + If `True`, allow following redirects from `3xx` HTTP responses. + Generally you do not want to enable this unless you have a good reason to. + Defaults to `False` if unspecified during deserialization. + tcp_connector : aiohttp.TCPConnector, optional + This may otherwise be `None` to use the default settings provided by + `aiohttp`. + This is deserialized as an object reference in the format + `package.module#object.attribute` that is expected to point to the + desired value. + Defaults to `None` if unspecified during deserialization. + proxy_headers : typing.Mapping [ str, str ], optional + Optional proxy headers to provide in any HTTP requests. + Defaults to `None` if unspecified during deserialization. + proxy_auth : aiohttp.BasicAuth, optional + Optional proxy authorization to provide in any HTTP requests. + This is deserialized using the format `"basic {{base 64 string here}}"`. + Defaults to `None` if unspecified during deserialization. + proxy_url : str, optional + The optional URL of the proxy to send requests via. + Defaults to `None` if unspecified during deserialization. + request_timeout : float, optional + Optional request timeout to use. If an HTTP request takes longer than + this, it will be aborted. + If not `None`, the value represents a number of seconds as a floating + point number. + Defaults to `None` if unspecified during deserialization. + ssl_context : ssl.SSLContext, optional + The optional SSL context to use. + This is deserialized as an object reference in the format + `package.module#object.attribute` that is expected to point to the + desired value. + Defaults to `None` if unspecified during deserialization. + verify_ssl : bool + If `True`, then responses with invalid SSL certificates will be + rejected. Generally you want to keep this enabled unless you have a + problem with SSL and you know exactly what you are doing by disabling + this. Disabling SSL verification can have major security implications. + You turn this off at your own risk. + Defaults to `True` if unspecified during deserialization. + """ + allow_redirects: bool = marshaller.attrib(deserializer=bool, if_undefined=False, default=False) - #: Either an implementation of :obj:`~aiohttp.TCPConnector`. - #: - #: This may otherwise be :obj:`~None` to use the default settings provided - #: by :mod:`aiohttp`. - #: - #: This is deserialized as an object reference in the format - #: ``package.module#object.attribute`` that is expected to point to the - #: desired value. - #: - #: Defaults to :obj:`~None` if unspecified during deserialization. - #: - #: :type: :obj:`~aiohttp.TCPConnector`, optional tcp_connector: typing.Optional[aiohttp.TCPConnector] = marshaller.attrib( deserializer=marshaller.dereference_handle, if_none=None, if_undefined=None, default=None ) - #: Optional proxy headers to provide in any HTTP requests. - #: - #: Defaults to :obj:`~None` if unspecified during deserialization. - #: - #: :type: :obj:`~typing.Mapping` [ :obj:`~str`, :obj:`~str` ], optional proxy_headers: typing.Optional[typing.Mapping[str, str]] = marshaller.attrib( deserializer=dict, if_none=None, if_undefined=None, default=None ) - #: Optional proxy authorization to provide in any HTTP requests. - #: - #: This is deserialized using the format ``"basic {{base 64 string here}}"``. - #: - #: Defaults to :obj:`~None` if unspecified during deserialization. - #: - #: :type: :obj:`~aiohttp.BasicAuth`, optional proxy_auth: typing.Optional[aiohttp.BasicAuth] = marshaller.attrib( deserializer=aiohttp.BasicAuth.decode, if_none=None, if_undefined=None, default=None ) - #: The optional URL of the proxy to send requests via. - #: - #: Defaults to :obj:`~None` if unspecified during deserialization. - #: - #: :type: :obj:`~str`, optional proxy_url: typing.Optional[str] = marshaller.attrib(deserializer=str, if_undefined=None, if_none=None, default=None) - #: Optional request timeout to use. If an HTTP request takes longer than - #: this, it will be aborted. - #: - #: If not :obj:`~None`, the value represents a number of seconds as a - #: floating point number. - #: - #: Defaults to :obj:`~None` if unspecified during deserialization. - #: - #: :type: :obj:`~float`, optional request_timeout: typing.Optional[float] = marshaller.attrib( deserializer=float, if_undefined=None, if_none=None, default=None ) - #: The optional SSL context to use. - #: - #: This is deserialized as an object reference in the format - #: ``package.module#object.attribute`` that is expected to point to the - #: desired value. - #: - #: Defaults to :obj:`~None` if unspecified during deserialization. - #: - #: :type: :obj:`~ssl.SSLContext`, optional ssl_context: typing.Optional[ssl.SSLContext] = marshaller.attrib( deserializer=marshaller.dereference_handle, if_none=None, if_undefined=None, default=None ) - #: If :obj:`~True`, then responses with invalid SSL certificates will be - #: rejected. Generally you want to keep this enabled unless you have a - #: problem with SSL and you know exactly what you are doing by disabling - #: this. Disabling SSL verification can have major security implications. - #: You turn this off at your own risk. - #: - #: Defaults to :obj:`~True` if unspecified during deserialization. - #: - #: :type: :obj:`~bool` verify_ssl: bool = marshaller.attrib(deserializer=bool, if_undefined=True, default=True) @marshaller.marshallable() @attr.s(kw_only=True) class TokenConfig(BaseConfig): - """Token config options.""" + """Token config options. + + Attributes + ---------- + token : str, optional + The token to use. + """ - #: The token to use. - #: - #: :type: :obj:`~str`, optional token: typing.Optional[str] = marshaller.attrib(deserializer=str, if_none=None, if_undefined=None, default=None) + """The token to use.""" def _parse_shard_info(payload): @@ -189,160 +177,334 @@ def _parse_shard_info(payload): @marshaller.marshallable() @attr.s(kw_only=True) class GatewayConfig(AIOHTTPConfig, TokenConfig, DebugConfig): - """Single-websocket specific configuration options.""" + """Single-websocket specific configuration options. + + Attributes + ---------- + allow_redirects : bool + If `True`, allow following redirects from `3xx` HTTP responses. + Generally you do not want to enable this unless you have a good reason to. + Defaults to `False` if unspecified during deserialization. + tcp_connector : aiohttp.TCPConnector, optional + This may otherwise be `None` to use the default settings provided by + `aiohttp`. + This is deserialized as an object reference in the format + `package.module#object.attribute` that is expected to point to the + desired value. + Defaults to `None` if unspecified during deserialization. + proxy_headers : typing.Mapping [ str, str ], optional + Optional proxy headers to provide in any HTTP requests. + Defaults to `None` if unspecified during deserialization. + proxy_auth : aiohttp.BasicAuth, optional + Optional proxy authorization to provide in any HTTP requests. + This is deserialized using the format `"basic {{base 64 string here}}"`. + Defaults to `None` if unspecified during deserialization. + proxy_url : str, optional + The optional URL of the proxy to send requests via. + Defaults to `None` if unspecified during deserialization. + request_timeout : float, optional + Optional request timeout to use. If an HTTP request takes longer than + this, it will be aborted. + If not `None`, the value represents a number of seconds as a floating + point number. + Defaults to `None` if unspecified during deserialization. + ssl_context : ssl.SSLContext, optional + The optional SSL context to use. + This is deserialized as an object reference in the format + `package.module#object.attribute` that is expected to point to the + desired value. + Defaults to `None` if unspecified during deserialization. + verify_ssl : bool + If `True`, then responses with invalid SSL certificates will be + rejected. Generally you want to keep this enabled unless you have a + problem with SSL and you know exactly what you are doing by disabling + this. Disabling SSL verification can have major security implications. + You turn this off at your own risk. + Defaults to `True` if unspecified during deserialization. + token : str, optional + The token to use. + debug : bool + Whether to enable debugging mode. Usually you don't want to enable this. + gateway_use_compression : bool + Whether to use zlib compression on the gateway for inbound messages. + Usually you want this turned on. + gateway_version : int + The gateway API version to use. Defaults to v6 + initial_activity : hikari.gateway_entities.Activity, optional + The initial activity to set all shards to when starting the gateway. + If this is `None` then no activity will be set, this is the default. + initial_status : hikari.guilds.PresenceStatus + The initial status to set the shards to when starting the gateway. + Defaults to `ONLINE`. + initial_is_afk : bool + Whether to show up as AFK or not on sign-in. + initial_idle_since : datetime.datetime, optional + The idle time to show on signing in. + If set to `None` to not show an idle time, this is the default. + intents : hikari.intents.Intent + The intents to use for the connection. + If being deserialized, this can be an integer bitfield, or a sequence of + intent names. If unspecified, this will be set to `None`. + large_threshold : int + The large threshold to use. + shard_id : typing.Sequence [ int ], optional + The shard IDs to produce shard connections for. + If being deserialized, this can be several formats shown in `notes`. + shard_count : int, optional + The number of shards the entire distributed application should consists + of. If you run multiple distributed instances of the bot, you should + ensure this value is consistent. + This can be set to `None` to enable auto-sharding. This is the default. + + !!! note + The several formats for `shard_id` are as follows: + + * A specific shard ID (e.g. `12`); + * A sequence of shard IDs (e.g. `[0, 1, 2, 3, 8, 9, 10]`); + * A range string. Two periods indicate a range of `[5, 16]` + (inclusive beginning, exclusive end). + * A range string. Three periods indicate a range of + `[5, 17]` (inclusive beginning, inclusive end); + * `None` this means `shard_count` will be considered and that many + shards will be created for you. If the `shard_count` is also + `None` then auto-sharding will be performed for you. + + !!! note + + If being deserialized, `intents` can be an integer bitfield, or a + sequence of intent names. If unspecified, `intents` will be set to + `None`. + + See `hikari.intents.Intent` for valid names of intents you + can use. Integer values are as documented on Discord's developer portal. + + !!! warning + If you are using the V7 gateway implementation, you will NEED to provide + explicit `intents` values for this field in order to get online. + Additionally, intents that are classed by Discord as being privileged + will require you to whitelist your application in order to use them. + + If you are using the V6 gateway implementation, setting `intents` to + `None` will simply opt you into every event you can subscribe to. + """ - #: Whether to use zlib compression on the gateway for inbound messages or - #: not. Usually you want this turned on. - #: - #: :type: :obj:`~bool` gateway_use_compression: bool = marshaller.attrib(deserializer=bool, if_undefined=True, default=True) - #: The gateway API version to use. - #: - #: If unspecified, then V6 is used. - #: - #: :type: :obj:`~int` gateway_version: int = marshaller.attrib(deserializer=int, if_undefined=lambda: 6, default=6) - #: The initial activity to set all shards to when starting the gateway. If - #: :obj:`~None`, then no activity will be set. - #: - #: :type: :obj:`~hikari.gateway_entities.GatewayActivity`, optional initial_activity: typing.Optional[gateway_entities.Activity] = marshaller.attrib( deserializer=gateway_entities.Activity.deserialize, if_none=None, if_undefined=None, default=None ) - #: The initial status to set the shards to when starting the gateway. - #: - #: :type: :obj:`~hikari.guilds.PresenceStatus` initial_status: guilds.PresenceStatus = marshaller.attrib( deserializer=guilds.PresenceStatus, if_undefined=lambda: guilds.PresenceStatus.ONLINE, default=guilds.PresenceStatus.ONLINE, ) - #: Whether to show up as AFK or not on sign-in. - #: - #: :type: :obj:`~bool` initial_is_afk: bool = marshaller.attrib(deserializer=bool, if_undefined=False, default=False) - #: The idle time to show on signing in, or :obj:`~None` to not show an idle - #: time. - #: - #: :type: :obj:`~datetime.datetime`, optional initial_idle_since: typing.Optional[datetime.datetime] = marshaller.attrib( deserializer=datetime.datetime.fromtimestamp, if_none=None, if_undefined=None, default=None ) - #: The intents to use for the connection. - #: - #: If being deserialized, this can be an integer bitfield, or a sequence of - #: intent names. If - #: unspecified, this will be set to :obj:`~None`. - #: - #: Examples - #: -------- - #: - #: .. code-block:: python - #: - #: # Python example - #: GatewayIntent.GUILDS | GatewayIntent.GUILD_MESSAGES - #: - #: .. code-block:: js - #: - #: // JSON example, using explicit bitfield values - #: 513 - #: // JSON example, using an array of names - #: [ "GUILDS", "GUILD_MESSAGES" ] - #: - #: See :obj:`~hikari.intents.Intent` for valid names of - #: intents you can use. Integer values are as documented on Discord's - #: developer portal. - #: - #: Warnings - #: -------- - #: If you are using the V7 gateway implementation, you will NEED to provide - #: explicit intent values for this field in order to get online. - #: Additionally, intents that are classed by Discord as being privileged - #: will require you to whitelist your application in order to use them. - #: - #: If you are using the V6 gateway implementation, setting this to - #: :obj:`~None` will simply opt you into every event you can subscribe to. - #: - #: - #: :type: :obj:`~hikari.intents.Intent`, optional intents: typing.Optional[_intents.Intent] = marshaller.attrib( deserializer=lambda value: conversions.dereference_int_flag(_intents.Intent, value), if_undefined=None, default=None, ) - #: The large threshold to use. - #: - #: :type: :obj:`~int` large_threshold: int = marshaller.attrib(deserializer=int, if_undefined=lambda: 250, default=250) """Definition of shard management configuration settings.""" - #: The shard IDs to produce shard connections for. - #: - #: If being deserialized, this can be several formats. - #: ``12``: - #: A specific shard ID. - #: ``[0, 1, 2, 3, 8, 9, 10]``: - #: A sequence of shard IDs. - #: ``"5..16"``: - #: A range string. Two periods indicate a range of ``[5, 16)`` - #: (inclusive beginning, exclusive end). - #: ``"5...16"``: - #: A range string. Three periods indicate a range of - #: ``[5, 17]`` (inclusive beginning, inclusive end). - #: :obj:`~None`: - #: The ``shard_count`` will be considered and that many shards will - #: be created for you. If the ``shard_count`` is also :obj:`~None`, - #: then auto-sharding will be performed for you. - #: - #: :type: :obj:`~typing.Sequence` [ :obj:`~int` ], optional shard_ids: typing.Optional[typing.Sequence[int]] = marshaller.attrib( deserializer=_parse_shard_info, if_none=None, if_undefined=None, default=None ) - #: The number of shards the entire distributed application should consist - #: of. If you run multiple distributed instances of the bot, you should - #: ensure this value is consistent. - #: - #: This can be set to :obj:`~None` to enable auto-sharding. This is the - #: default behaviour. - #: - #: :type: :obj:`~int`, optional. shard_count: typing.Optional[int] = marshaller.attrib(deserializer=int, if_undefined=None, default=None) @marshaller.marshallable() @attr.s(kw_only=True) class RESTConfig(AIOHTTPConfig, TokenConfig): - """REST-specific configuration details.""" - - #: Token authentication scheme. - #: - #: Should be ``"Bot"`` or ``"Bearer"``, or ``None`` if not relevant. - #: - #: Defaults to ``"Bot"`` - #: - #: :type: :obj:`~str`, optional + """Single-websocket specific configuration options. + + Attributes + ---------- + allow_redirects : bool + If `True`, allow following redirects from `3xx` HTTP responses. + Generally you do not want to enable this unless you have a good reason to. + Defaults to `False` if unspecified during deserialization. + tcp_connector : aiohttp.TCPConnector, optional + This may otherwise be `None` to use the default settings provided by + `aiohttp`. + This is deserialized as an object reference in the format + `package.module#object.attribute` that is expected to point to the + desired value. + Defaults to `None` if unspecified during deserialization. + proxy_headers : typing.Mapping [ str, str ], optional + Optional proxy headers to provide in any HTTP requests. + Defaults to `None` if unspecified during deserialization. + proxy_auth : aiohttp.BasicAuth, optional + Optional proxy authorization to provide in any HTTP requests. + This is deserialized using the format `"basic {{base 64 string here}}"`. + Defaults to `None` if unspecified during deserialization. + proxy_url : str, optional + The optional URL of the proxy to send requests via. + Defaults to `None` if unspecified during deserialization. + request_timeout : float, optional + Optional request timeout to use. If an HTTP request takes longer than + this, it will be aborted. + If not `None`, the value represents a number of seconds as a floating + point number. + Defaults to `None` if unspecified during deserialization. + ssl_context : ssl.SSLContext, optional + The optional SSL context to use. + This is deserialized as an object reference in the format + `package.module#object.attribute` that is expected to point to the + desired value. + Defaults to `None` if unspecified during deserialization. + verify_ssl : bool + If `True`, then responses with invalid SSL certificates will be + rejected. Generally you want to keep this enabled unless you have a + problem with SSL and you know exactly what you are doing by disabling + this. Disabling SSL verification can have major security implications. + You turn this off at your own risk. + Defaults to `True` if unspecified during deserialization. + token : str, optional + The token to use. + debug : bool + Whether to enable debugging mode. Usually you don't want to enable this. + token_type : str, optional + Token authentication scheme, this defaults to `"Bot"` and should be + one of `"Bot"` or `"Bearer"`, or `None` if not relevant. + rest_version : int + The HTTP API version to use. If unspecified, then V7 is used. + """ + token_type: typing.Optional[str] = marshaller.attrib( deserializer=str, if_undefined=lambda: "Bot", if_none=None, default="Bot" ) - #: The HTTP API version to use. - #: - #: If unspecified, then V7 is used. - #: - #: :type: :obj:`~int` rest_version: int = marshaller.attrib(deserializer=int, if_undefined=lambda: 7, default=7) @marshaller.marshallable() @attr.s(kw_only=True) class BotConfig(RESTConfig, GatewayConfig): - """Configuration for a standard bot.""" + """Configuration for a standard bot. + + Attributes + ---------- + allow_redirects : bool + If `True`, allow following redirects from `3xx` HTTP responses. + Generally you do not want to enable this unless you have a good reason to. + Defaults to `False` if unspecified during deserialization. + tcp_connector : aiohttp.TCPConnector, optional + This may otherwise be `None` to use the default settings provided by + `aiohttp`. + This is deserialized as an object reference in the format + `package.module#object.attribute` that is expected to point to the + desired value. + Defaults to `None` if unspecified during deserialization. + proxy_headers : typing.Mapping [ str, str ], optional + Optional proxy headers to provide in any HTTP requests. + Defaults to `None` if unspecified during deserialization. + proxy_auth : aiohttp.BasicAuth, optional + Optional proxy authorization to provide in any HTTP requests. + This is deserialized using the format `"basic {{base 64 string here}}"`. + Defaults to `None` if unspecified during deserialization. + proxy_url : str, optional + The optional URL of the proxy to send requests via. + Defaults to `None` if unspecified during deserialization. + request_timeout : float, optional + Optional request timeout to use. If an HTTP request takes longer than + this, it will be aborted. + If not `None`, the value represents a number of seconds as a floating + point number. + Defaults to `None` if unspecified during deserialization. + ssl_context : ssl.SSLContext, optional + The optional SSL context to use. + This is deserialized as an object reference in the format + `package.module#object.attribute` that is expected to point to the + desired value. + Defaults to `None` if unspecified during deserialization. + verify_ssl : bool + If `True`, then responses with invalid SSL certificates will be + rejected. Generally you want to keep this enabled unless you have a + problem with SSL and you know exactly what you are doing by disabling + this. Disabling SSL verification can have major security implications. + You turn this off at your own risk. + Defaults to `True` if unspecified during deserialization. + token : str, optional + The token to use. + debug : bool + Whether to enable debugging mode. Usually you don't want to enable this. + gateway_use_compression : bool + Whether to use zlib compression on the gateway for inbound messages. + Usually you want this turned on. + gateway_version : int + The gateway API version to use. Defaults to v6 + initial_activity : hikari.gateway_entities.Activity, optional + The initial activity to set all shards to when starting the gateway. + If this is `None` then no activity will be set, this is the default. + initial_status : hikari.guilds.PresenceStatus + The initial status to set the shards to when starting the gateway. + Defaults to `ONLINE`. + initial_is_afk : bool + Whether to show up as AFK or not on sign-in. + initial_idle_since : datetime.datetime, optional + The idle time to show on signing in. + If set to `None` to not show an idle time, this is the default. + intents : hikari.intents.Intent + The intents to use for the connection. + If being deserialized, this can be an integer bitfield, or a sequence of + intent names. If unspecified, this will be set to `None`. + large_threshold : int + The large threshold to use. + shard_id : typing.Sequence [ int ], optional + The shard IDs to produce shard connections for. + If being deserialized, this can be several formats shown in `notes`. + shard_count : int, optional + The number of shards the entire distributed application should consists + of. If you run multiple distributed instances of the bot, you should + ensure this value is consistent. + This can be set to `None` to enable auto-sharding. This is the default. + token_type : str, optional + Token authentication scheme, this defaults to `"Bot"` and should be + one of `"Bot"` or `"Bearer"`, or `None` if not relevant. + rest_version : int + The HTTP API version to use. If unspecified, then V7 is used. + + !!! note + The several formats for `shard_id` are as follows: + + * A specific shard ID (e.g. `12`); + * A sequence of shard IDs (e.g. `[0, 1, 2, 3, 8, 9, 10]`); + * A range string. Two periods indicate a range of `[5, 16]` + (inclusive beginning, exclusive end). + * A range string. Three periods indicate a range of + `[5, 17]` (inclusive beginning, inclusive end); + * `None` this means `shard_count` will be considered and that many + shards will be created for you. If the `shard_count` is also + `None` then auto-sharding will be performed for you. + + !!! note + + If being deserialized, `intents` can be an integer bitfield, or a + sequence of intent names. If unspecified, `intents` will be set to + `None`. + + See `hikari.intents.Intent` for valid names of intents you + can use. Integer values are as documented on Discord's developer portal. + + !!! warning + If you are using the V7 gateway implementation, you will NEED to provide + explicit `intents` values for this field in order to get online. + Additionally, intents that are classed by Discord as being privileged + will require you to whitelist your application in order to use them. + + If you are using the V6 gateway implementation, setting `intents` to + `None` will simply opt you into every event you can subscribe to. + """ diff --git a/hikari/clients/rest/__init__.py b/hikari/clients/rest/__init__.py index 99b4216e6d..904fa028f0 100644 --- a/hikari/clients/rest/__init__.py +++ b/hikari/clients/rest/__init__.py @@ -54,19 +54,18 @@ class RESTClient( A marshalling object-oriented REST API client. This client bridges the basic REST API exposed by - `hikari.net.rest_sessions.LowLevelRestfulClient` and wraps it in a unit of - processing that can handle parsing API objects into Hikari entity objects. + `hikari.net.rest.REST` and wraps it in a unit of processing that can handle + handle parsing API objects into Hikari entity objects. Parameters ---------- - config : `hikari.clients.configs.RESTConfig` + config : hikari.clients.configs.RESTConfig A HTTP configuration object. - Note - ---- - For all endpoints where a `reason` argument is provided, this may be a - string inclusively between `0` and `512` characters length, with any - additional characters being cut off. + !!! note + For all endpoints where a `reason` argument is provided, this may be a + string inclusively between `0` and `512` characters length, with any + additional characters being cut off. """ def __init__(self, config: configs.RESTConfig) -> None: diff --git a/hikari/clients/rest/base.py b/hikari/clients/rest/base.py index 3e2b9cbc55..9d3921d7ec 100644 --- a/hikari/clients/rest/base.py +++ b/hikari/clients/rest/base.py @@ -32,8 +32,8 @@ class BaseRESTComponent(abc.ABC, metaclass=meta.UniqueFunctionMeta): """An abstract class that all REST client logic classes should inherit from. This defines the abstract method `__init__` which will assign an instance - of `hikari.net.rest_sessions.LowLevelRestfulClient` to the attribute that - all components will expect to make calls to. + of `hikari.net.rest.REST` to the attribute that all components will expect + to make calls to. """ @abc.abstractmethod diff --git a/hikari/clients/rest/channel.py b/hikari/clients/rest/channel.py index b6e95b8607..2d5e2f8a07 100644 --- a/hikari/clients/rest/channel.py +++ b/hikari/clients/rest/channel.py @@ -50,22 +50,22 @@ async def fetch_channel(self, channel: bases.Hashable[_channels.Channel]) -> _ch Parameters ---------- - channel : :obj:`~typing.Union` [ :obj:`~hikari.channels.Channel`, :obj:`~hikari.bases.Snowflake`, :obj:`~int` ] + channel : typing.Union [ hikari.channels.Channel, hikari.bases.Snowflake, int ] The object ID of the channel to look up. Returns ------- - :obj:`~hikari.channels.Channel` + hikari.channels.Channel The channel object that has been found. Raises ------ - :obj:`~hikari.errors.BadRequestHTTPError` + hikari.errors.BadRequestHTTPError If any invalid snowflake IDs are passed; a snowflake may be invalid due to it being outside of the range of a 64 bit integer. - :obj:`~hikari.errors.ForbiddenHTTPError` + hikari.errors.ForbiddenHTTPError If you don't have access to the channel. - :obj:`~hikari.errors.NotFoundHTTPError` + hikari.errors.NotFoundHTTPError If the channel does not exist. """ payload = await self._session.get_channel( @@ -92,59 +92,59 @@ async def update_channel( Parameters ---------- - channel : :obj:`~typing.Union` [ :obj:`~hikari.channels.Channel`, :obj:`~hikari.bases.Snowflake`, :obj:`~int` ] + channel : typing.Union [ hikari.channels.Channel, hikari.bases.Snowflake, int ] The channel ID to update. - name : :obj:`~str` + name : str If specified, the new name for the channel. This must be - inclusively between ``1`` and ``100`` characters in length. - position : :obj:`~int` + inclusively between `1` and `100` characters in length. + position : int If specified, the position to change the channel to. - topic : :obj:`~str` + topic : str If specified, the topic to set. This is only applicable to - text channels. This must be inclusively between ``0`` and ``1024`` + text channels. This must be inclusively between `0` and `1024` characters in length. - nsfw : :obj:`~bool` - Mark the channel as being not safe for work (NSFW) if :obj:`~True`. - If :obj:`~False` or unspecified, then the channel is not marked as + nsfw : bool + Mark the channel as being not safe for work (NSFW) if `True`. + If `False` or unspecified, then the channel is not marked as NSFW. Will have no visible effect for non-text guild channels. - rate_limit_per_user : :obj:`~typing.Union` [ :obj:`~int`, :obj:`~datetime.timedelta` ] + rate_limit_per_user : typing.Union [ int, datetime.timedelta ] If specified, the time delta of seconds the user has to wait before sending another message. This will not apply to bots, or to - members with ``MANAGE_MESSAGES`` or ``MANAGE_CHANNEL`` permissions. - This must be inclusively between ``0`` and ``21600`` seconds. - bitrate : :obj:`~int` + members with `MANAGE_MESSAGES` or `MANAGE_CHANNEL` permissions. + This must be inclusively between `0` and `21600` seconds. + bitrate : int If specified, the bitrate in bits per second allowable for the channel. This only applies to voice channels and must be inclusively - between ``8000`` and ``96000`` for normal servers or ``8000`` and - ``128000`` for VIP servers. - user_limit : :obj:`~int` + between `8000` and `96000` for normal servers or `8000` and + `128000` for VIP servers. + user_limit : int If specified, the new max number of users to allow in a voice - channel. This must be between ``0`` and ``99`` inclusive, where - ``0`` implies no limit. - permission_overwrites : :obj:`~typing.Sequence` [ :obj:`~hikari.channels.PermissionOverwrite` ] + channel. This must be between `0` and `99` inclusive, where + `0` implies no limit. + permission_overwrites : typing.Sequence [ hikari.channels.PermissionOverwrite ] If specified, the new list of permission overwrites that are category specific to replace the existing overwrites with. - parent_category : :obj:`~typing.Union` [ :obj:`~hikari.channels.Channel`, :obj:`~hikari.bases.Snowflake`, :obj:`~int` ], optional + parent_category : typing.Union [ hikari.channels.Channel, hikari.bases.Snowflake, int ], optional If specified, the new parent category ID to set for the channel, - pass :obj:`~None` to unset. - reason : :obj:`~str` + pass `None` to unset. + reason : str If specified, the audit log reason explaining why the operation was performed. Returns ------- - :obj:`~hikari.channels.Channel` + hikari.channels.Channel The channel object that has been modified. Raises ------ - :obj:`~hikari.errors.NotFoundHTTPError` + hikari.errors.NotFoundHTTPError If the channel does not exist. - :obj:`~hikari.errors.ForbiddenHTTPError` + hikari.errors.ForbiddenHTTPError If you lack the permission to make the change. - :obj:`~hikari.errors.BadRequestHTTPError` + hikari.errors.BadRequestHTTPError If you provide incorrect options for the corresponding channel type - (e.g. a ``bitrate`` for a text channel). + (e.g. a `bitrate` for a text channel). If any invalid snowflake IDs are passed; a snowflake may be invalid due to it being outside of the range of a 64 bit integer. """ @@ -178,35 +178,33 @@ async def delete_channel(self, channel: bases.Hashable[_channels.Channel]) -> No Parameters ---------- - channel : :obj:`~typing.Union` [ :obj:`~hikari.channels.Channel`, :obj:`~hikari.bases.Snowflake` :obj:`~str` ] + channel : typing.Union [ hikari.channels.Channel, hikari.bases.Snowflake str ] The object or ID of the channel to delete. Returns ------- - :obj:`~None` + None Nothing, unlike what the API specifies. This is done to maintain consistency with other calls of a similar nature in this API wrapper. Raises ------ - :obj:`~hikari.errors.BadRequestHTTPError` + hikari.errors.BadRequestHTTPError If any invalid snowflake IDs are passed; a snowflake may be invalid due to it being outside of the range of a 64 bit integer. - :obj:`~hikari.errors.NotFoundHTTPError` + hikari.errors.NotFoundHTTPError If the channel does not exist. - :obj:`~hikari.errors.ForbiddenHTTPError` + hikari.errors.ForbiddenHTTPError If you do not have permission to delete the channel. - Note - ---- - Closing a DM channel won't raise an exception but will have no effect - and "closed" DM channels will not have to be reopened to send messages - in theme. + !!! note + Closing a DM channel won't raise an exception but will have no + effect and "closed" DM channels will not have to be reopened to send + messages in theme. - Warning - ------- - Deleted channels cannot be un-deleted. + !!! warning + Deleted channels cannot be un-deleted. """ await self._session.delete_close_channel( channel_id=str(channel.id if isinstance(channel, bases.UniqueEntity) else int(channel)) @@ -226,45 +224,43 @@ def fetch_messages_after( Parameters ---------- - channel : :obj:`~typing.Union` [ :obj:`~hikari.channels.Channel`, :obj:`~hikari.bases.Snowflake`, :obj:`~int` ] + channel : typing.Union [ hikari.channels.Channel, hikari.bases.Snowflake, int ] The ID of the channel to retrieve the messages from. - limit : :obj:`~int` + limit : int If specified, the maximum number of how many messages this iterator should return. - after : :obj:`~typing.Union` [ :obj:`~datetime.datetime`, :obj:`~hikari.channels.Channel`, :obj:`~hikari.bases.Snowflake`, :obj:`~int` ] + after : typing.Union [ datetime.datetime, hikari.channels.Channel, hikari.bases.Snowflake, int ] A object or ID message. Only return messages sent AFTER this message if it's specified else this will return every message after (and including) the first message in the channel. - Example - ------- - + Examples + -------- async for message in client.fetch_messages_after(channel, after=9876543, limit=3232): if message.author.id in BLACKLISTED_USERS: await client.ban_member(channel.guild_id, message.author) Returns ------- - :obj:`~typing.AsyncIterator` [ :obj:`~hikari.messages.Message` ] + typing.AsyncIterator [ hikari.messages.Message ] An async iterator that retrieves the channel's message objects. Raises ------ - :obj:`~hikari.errors.BadRequestHTTPError` + hikari.errors.BadRequestHTTPError If any invalid snowflake IDs are passed; a snowflake may be invalid due to it being outside of the range of a 64 bit integer. - :obj:`~hikari.errors.ForbiddenHTTPError` + hikari.errors.ForbiddenHTTPError If you lack permission to read the channel. - :obj:`~hikari.errors.NotFoundHTTPError` + hikari.errors.NotFoundHTTPError If the channel is not found, or the message provided for one of the filter arguments is not found. - Note - ---- - If you are missing the ``VIEW_CHANNEL`` permission, you will receive a - :obj:`~hikari.errors.ForbiddenHTTPError`. If you are instead missing - the ``READ_MESSAGE_HISTORY`` permission, you will always receive - zero results, and thus an empty list will be returned instead. + !!! note + If you are missing the `VIEW_CHANNEL` permission, you will receive a + hikari.errors.ForbiddenHTTPError. If you are instead missing + the `READ_MESSAGE_HISTORY` permission, you will always receive + zero results, and thus an empty list will be returned instead. """ if isinstance(after, datetime.datetime): after = str(bases.Snowflake.from_datetime(after)) @@ -294,47 +290,44 @@ def fetch_messages_before( Parameters ---------- - channel : :obj:`~typing.Union` [ :obj:`~hikari.channels.Channel`, :obj:`~hikari.bases.Snowflake`, :obj:`~int` ] + channel : typing.Union [ hikari.channels.Channel, hikari.bases.Snowflake, int ] The ID of the channel to retrieve the messages from. - limit : :obj:`~int` + limit : int If specified, the maximum number of how many messages this iterator should return. - before : :obj:`~typing.Union` [ :obj:`~datetime.datetime`, :obj:`~hikari.channels.Channel`, :obj:`~hikari.bases.Snowflake`, :obj:`~int` ] + before : typing.Union [ datetime.datetime, hikari.channels.Channel, hikari.bases.Snowflake, int ] A message object or ID. Only return messages sent BEFORE this message if this is specified else this will return every message before (and including) the most recent message in the channel. - Example - ------- - .. code-block:: python - + Examples + -------- async for message in client.fetch_messages_before(channel, before=9876543, limit=1231): if message.content.lower().contains("delete this"): await client.delete_message(channel, message) Returns ------- - :obj:`~typing.AsyncIterator` [ :obj:`~hikari.messages.Message` ] + typing.AsyncIterator [ hikari.messages.Message ] An async iterator that retrieves the channel's message objects. Raises ------ - :obj:`~hikari.errors.BadRequestHTTPError` + hikari.errors.BadRequestHTTPError If any invalid snowflake IDs are passed; a snowflake may be invalid due to it being outside of the range of a 64 bit integer. - :obj:`~hikari.errors.ForbiddenHTTPError` + hikari.errors.ForbiddenHTTPError If you lack permission to read the channel. - :obj:`~hikari.errors.NotFoundHTTPError` + hikari.errors.NotFoundHTTPError If the channel is not found, or the message provided for one of the filter arguments is not found. - Note - ---- - If you are missing the ``VIEW_CHANNEL`` permission, you will receive a - :obj:`~hikari.errors.ForbiddenHTTPError`. If you are instead missing - the ``READ_MESSAGE_HISTORY`` permission, you will always receive - zero results, and thus an empty list will be returned instead. + !!! note + If you are missing the `VIEW_CHANNEL` permission, you will receive a + hikari.errors.ForbiddenHTTPError. If you are instead missing + the `READ_MESSAGE_HISTORY` permission, you will always receive + zero results, and thus an empty list will be returned instead. """ if isinstance(before, datetime.datetime): before = str(bases.Snowflake.from_datetime(before)) @@ -365,47 +358,44 @@ async def fetch_messages_around( Parameters ---------- - channel : :obj:`~typing.Union` [ :obj:`~hikari.channels.Channel`, :obj:`~hikari.bases.Snowflake`, :obj:`~int` ] + channel : typing.Union [ hikari.channels.Channel, hikari.bases.Snowflake, int ] The ID of the channel to retrieve the messages from. - around : :obj:`~typing.Union` [ :obj:`~datetime.datetime`, :obj:`~hikari.channels.Channel`, :obj:`~hikari.bases.Snowflake`, :obj:`~int` ] + around : typing.Union [ datetime.datetime, hikari.channels.Channel, hikari.bases.Snowflake, int ] The object or ID of the message to get messages that were sent - AROUND it in the provided channel, unlike ``before`` and ``after``, + AROUND it in the provided channel, unlike `before` and `after`, this argument is required and the provided message will also be returned if it still exists. - limit : :obj:`~int` + limit : int If specified, the maximum number of how many messages this iterator should return, cannot be more than `100` - Example - ------- - .. code-block:: python - + Examples + -------- async for message in client.fetch_messages_around(channel, around=9876543, limit=42): if message.embeds and not message.author.is_bot: await client.delete_message(channel, message) Returns ------- - :obj:`~typing.AsyncIterator` [ :obj:`~hikari.messages.Message` ] + typing.AsyncIterator [ hikari.messages.Message ] An async iterator that retrieves the found message objects. Raises ------ - :obj:`~hikari.errors.BadRequestHTTPError` + hikari.errors.BadRequestHTTPError If any invalid snowflake IDs are passed; a snowflake may be invalid due to it being outside of the range of a 64 bit integer. - :obj:`~hikari.errors.ForbiddenHTTPError` + hikari.errors.ForbiddenHTTPError If you lack permission to read the channel. - :obj:`~hikari.errors.NotFoundHTTPError` + hikari.errors.NotFoundHTTPError If the channel is not found, or the message provided for one of the filter arguments is not found. - Note - ---- - If you are missing the ``VIEW_CHANNEL`` permission, you will receive a - :obj:`~hikari.errors.ForbiddenHTTPError`. If you are instead missing - the ``READ_MESSAGE_HISTORY`` permission, you will always receive - zero results, and thus an empty list will be returned instead. + !!! note + If you are missing the `VIEW_CHANNEL` permission, you will receive a + `hikari.errors.ForbiddenHTTPError`. If you are instead missing + the `READ_MESSAGE_HISTORY` permission, you will always receive + zero results, and thus an empty list will be returned instead. """ if isinstance(around, datetime.datetime): around = str(bases.Snowflake.from_datetime(around)) @@ -425,28 +415,27 @@ async def fetch_message( Parameters ---------- - channel : :obj:`~typing.Union` [ :obj:`~hikari.channels.Channel`, :obj:`~hikari.bases.Snowflake`, :obj:`~int` ] + channel : typing.Union [ hikari.channels.Channel, hikari.bases.Snowflake, int ] The object or ID of the channel to get the message from. - message : :obj:`~typing.Union` [ :obj:`~hikari.messages.Message`, :obj:`~hikari.bases.Snowflake`, :obj:`~int` ] + message : typing.Union [ hikari.messages.Message, hikari.bases.Snowflake, int ] The object or ID of the message to retrieve. Returns ------- - :obj:`~hikari.messages.Message` + hikari.messages.Message The found message object. - Note - ---- - This requires the ``READ_MESSAGE_HISTORY`` permission. + !!! note + This requires the `READ_MESSAGE_HISTORY` permission. Raises ------ - :obj:`~hikari.errors.BadRequestHTTPError` + hikari.errors.BadRequestHTTPError If any invalid snowflake IDs are passed; a snowflake may be invalid due to it being outside of the range of a 64 bit integer. - :obj:`~hikari.errors.ForbiddenHTTPError` + hikari.errors.ForbiddenHTTPError If you lack permission to see the message. - :obj:`~hikari.errors.NotFoundHTTPError` + hikari.errors.NotFoundHTTPError If the channel or message is not found. """ payload = await self._session.get_channel_message( @@ -472,54 +461,54 @@ async def create_message( Parameters ---------- - channel : :obj:`~typing.Union` [ :obj:`~hikari.channels.Channel`, :obj:`~hikari.bases.Snowflake`, :obj:`~int` ] + channel : typing.Union [ hikari.channels.Channel, hikari.bases.Snowflake, int ] The channel or ID of the channel to send to. - content : :obj:`~str` + content : str If specified, the message content to send with the message. - nonce : :obj:`~str` + nonce : str If specified, an optional ID to send for opportunistic message creation. This doesn't serve any real purpose for general use, and can usually be ignored. - tts : :obj:`~bool` + tts : bool If specified, whether the message will be sent as a TTS message. - files : :obj:`~typing.Collection` [ ``hikari.media.IO`` ] - If specified, this should be a list of inclusively between ``1`` and - ``5`` IO like media objects, as defined in :mod:`hikari.media`. - embed : :obj:`~hikari.embeds.Embed` + files : typing.Collection [ `hikari.media.IO` ] + If specified, this should be a list of inclusively between `1` and + `5` IO like media objects, as defined in `hikari.media`. + embed : hikari.embeds.Embed If specified, the embed object to send with the message. - mentions_everyone : :obj:`~bool` - Whether ``@everyone`` and ``@here`` mentions should be resolved by - discord and lead to actual pings, defaults to :obj:`~True`. - user_mentions : :obj:`~typing.Union` [ :obj:`~typing.Collection` [ :obj:`~typing.Union` [ :obj:`~hikari.users.User`, :obj:`~hikari.bases.Snowflake`, :obj:`~int` ], :obj:`~bool` ] + mentions_everyone : bool + Whether `@everyone` and `@here` mentions should be resolved by + discord and lead to actual pings, defaults to `True`. + user_mentions : typing.Union [ typing.Collection [ typing.Union [ hikari.users.User, hikari.bases.Snowflake, int ], bool ] Either an array of user objects/IDs to allow mentions for, - :obj:`~True` to allow all user mentions or :obj:`~False` to block all - user mentions from resolving, defaults to :obj:`~True`. - role_mentions : :obj:`~typing.Union` [ :obj:`~typing.Collection` [ :obj:`~typing.Union` [ :obj:`~hikari.guilds.GuildRole`, :obj:`~hikari.bases.Snowflake`, :obj:`~int` ] ], :obj:`~bool` ] + `True` to allow all user mentions or `False` to block all + user mentions from resolving, defaults to `True`. + role_mentions : typing.Union [ typing.Collection [ typing.Union [ hikari.guilds.GuildRole, hikari.bases.Snowflake, int ] ], bool ] Either an array of guild role objects/IDs to allow mentions for, - :obj:`~True` to allow all role mentions or :obj:`~False` to block all - role mentions from resolving, defaults to :obj:`~True`. + `True` to allow all role mentions or `False` to block all + role mentions from resolving, defaults to `True`. Returns ------- - :obj:`~hikari.messages.Message` + hikari.messages.Message The created message object. Raises ------ - :obj:`~hikari.errors.NotFoundHTTPError` + hikari.errors.NotFoundHTTPError If the channel is not found. - :obj:`~hikari.errors.BadRequestHTTPError` + hikari.errors.BadRequestHTTPError This can be raised if the file is too large; if the embed exceeds the defined limits; if the message content is specified only and - empty or greater than ``2000`` characters; if neither content, files + empty or greater than `2000` characters; if neither content, files or embed are specified. If any invalid snowflake IDs are passed; a snowflake may be invalid due to it being outside of the range of a 64 bit integer. - :obj:`~hikari.errors.ForbiddenHTTPError` + hikari.errors.ForbiddenHTTPError If you lack permissions to send to this channel. - :obj:`~ValueError` + ValueError If more than 100 unique objects/entities are passed for - ``role_mentions`` or ``user_mentions``. + `role_mentions` or `user_mentions`. """ payload = await self._session.create_message( channel_id=str(channel.id if isinstance(channel, bases.UniqueEntity) else int(channel)), @@ -549,9 +538,10 @@ def safe_create_message( ) -> more_typing.Coroutine[_messages.Message]: """Create a message in the given channel with mention safety. - This endpoint has the same signature as :attr:`create_message` with - the only difference being that ``mentions_everyone``, - ``user_mentions`` and ``role_mentions`` default to :obj:`~False`. + This endpoint has the same signature as + `RESTChannelComponent.create_message` with the only difference being + that `mentions_everyone`, `user_mentions` and `role_mentions` default to + `False`. """ return self.create_message( channel=channel, @@ -581,56 +571,56 @@ async def update_message( Parameters ---------- - channel : :obj:`~typing.Union` [ :obj:`~hikari.channels.Channel`, :obj:`~hikari.bases.Snowflake`, :obj:`~int` ] + channel : typing.Union [ hikari.channels.Channel, hikari.bases.Snowflake, int ] The object or ID of the channel to get the message from. - message : :obj:`~typing.Union` [ :obj:`~hikari.messages.Message`, :obj:`~hikari.bases.Snowflake`, :obj:`~int` ] + message : typing.Union [ hikari.messages.Message, hikari.bases.Snowflake, int ] The object or ID of the message to edit. - content : :obj:`~str`, optional + content : str, optional If specified, the string content to replace with in the message. - If :obj:`~None`, the content will be removed from the message. - embed : :obj:`~hikari.embeds.Embed`, optional - If specified, the embed to replace with in the message. - If :obj:`~None`, the embed will be removed from the message. - flags : :obj:`~hikari.messages.MessageFlag` + If `None`, then the content will be removed from the message. + embed : hikari.embeds.Embed, optional + If specified, then the embed to replace with in the message. + If `None`, then the embed will be removed from the message. + flags : hikari.messages.MessageFlag If specified, the new flags for this message, while a raw int may be passed for this, this can lead to unexpected behaviour if it's outside the range of the MessageFlag int flag. - mentions_everyone : :obj:`~bool` - Whether ``@everyone`` and ``@here`` mentions should be resolved by - discord and lead to actual pings, defaults to :obj:`~True`. - user_mentions : :obj:`~typing.Union` [ :obj:`~typing.Collection` [ :obj:`~typing.Union` [ :obj:`~hikari.users.User`, :obj:`~hikari.bases.Snowflake`, :obj:`~int` ], :obj:`~bool` ] + mentions_everyone : bool + Whether `@everyone` and `@here` mentions should be resolved by + discord and lead to actual pings, defaults to `True`. + user_mentions : typing.Union [ typing.Collection [ typing.Union [ hikari.users.User, hikari.bases.Snowflake, int ], bool ] Either an array of user objects/IDs to allow mentions for, - :obj:`~True` to allow all user mentions or :obj:`~False` to block all - user mentions from resolving, defaults to :obj:`~True`. - role_mentions : :obj:`~typing.Union` [ :obj:`~typing.Collection` [ :obj:`~typing.Union` [ :obj:`~hikari.guilds.GuildRole`, :obj:`~hikari.bases.Snowflake`, :obj:`~int` ] ], :obj:`~bool` ] + `True` to allow all user mentions or `False` to block all + user mentions from resolving, defaults to `True`. + role_mentions : typing.Union [ typing.Collection [ typing.Union [ hikari.guilds.GuildRole, hikari.bases.Snowflake, int ] ], bool ] Either an array of guild role objects/IDs to allow mentions for, - :obj:`~True` to allow all role mentions or :obj:`~False` to block all - role mentions from resolving, defaults to :obj:`~True`. + `True` to allow all role mentions or `False` to block all + role mentions from resolving, defaults to `True`. Returns ------- - :obj:`~hikari.messages.Message` + hikari.messages.Message The edited message object. Raises ------ - :obj:`~hikari.errors.NotFoundHTTPError` + hikari.errors.NotFoundHTTPError If the channel or message is not found. - :obj:`~hikari.errors.BadRequestHTTPError` + hikari.errors.BadRequestHTTPError This can be raised if the embed exceeds the defined limits; if the message content is specified only and empty or greater - than ``2000`` characters; if neither content, file or embed + than `2000` characters; if neither content, file or embed are specified. If any invalid snowflake IDs are passed; a snowflake may be invalid due to it being outside of the range of a 64 bit integer. - :obj:`~hikari.errors.ForbiddenHTTPError` - If you try to edit ``content`` or ``embed`` or ``allowed_mentions` + hikari.errors.ForbiddenHTTPError + If you try to edit `content` or `embed` or `allowed_mentions` on a message you did not author. If you try to edit the flags on a message you did not author without - the ``MANAGE_MESSAGES`` permission. - :obj:`~ValueError` + the `MANAGE_MESSAGES` permission. + ValueError If more than 100 unique objects/entities are passed for - ``role_mentions`` or ``user_mentions``. + `role_mentions` or `user_mentions`. """ payload = await self._session.edit_message( channel_id=str(channel.id if isinstance(channel, bases.UniqueEntity) else int(channel)), @@ -658,9 +648,10 @@ def safe_update_message( ) -> typing.Coroutine[typing.Any, typing.Any, _messages.Message]: """Update a message in the given channel with mention safety. - This endpoint has the same signature as :attr:`update_message` with - the only difference being that ``mentions_everyone``, - ``user_mentions`` and ``role_mentions`` default to :obj:`~False`. + This endpoint has the same signature as + `RESTChannelComponent.update_message` with the only difference being + that `mentions_everyone`, `user_mentions` and `role_mentions` default to + `False`. """ return self.update_message( message=message, @@ -683,35 +674,34 @@ async def delete_messages( Parameters ---------- - channel : :obj:`~typing.Union` [ :obj:`~hikari.channels.Channel`, :obj:`~hikari.bases.Snowflake`, :obj:`~int` ] + channel : typing.Union [ hikari.channels.Channel, hikari.bases.Snowflake, int ] The object or ID of the channel to get the message from. - message : :obj:`~typing.Union` [ :obj:`~hikari.messages.Message`, :obj:`~hikari.bases.Snowflake`, :obj:`~int` ] + message : typing.Union [ hikari.messages.Message, hikari.bases.Snowflake, int ] The object or ID of the message to delete. - *additional_messages : :obj:`~typing.Union` [ :obj:`~hikari.messages.Message`, :obj:`~hikari.bases.Snowflake`, :obj:`~int` ] + *additional_messages : typing.Union [ hikari.messages.Message, hikari.bases.Snowflake, int ] Objects and/or IDs of additional messages to delete in the same channel, in total you can delete up to 100 messages in a request. Raises ------ - :obj:`~hikari.errors.BadRequestHTTPError` + hikari.errors.BadRequestHTTPError If any invalid snowflake IDs are passed; a snowflake may be invalid due to it being outside of the range of a 64 bit integer. - :obj:`~hikari.errors.ForbiddenHTTPError` + hikari.errors.ForbiddenHTTPError If you did not author the message and are in a DM, or if you did - not author the message and lack the ``MANAGE_MESSAGES`` + not author the message and lack the `MANAGE_MESSAGES` permission in a guild channel. - :obj:`~hikari.errors.NotFoundHTTPError` + hikari.errors.NotFoundHTTPError If the channel or message is not found. - :obj:`~ValueError` - If you try to delete over ``100`` messages in a single request. - - Note - ---- - This can only be used on guild text channels. - Any message IDs that do not exist or are invalid still add towards the - total ``100`` max messages to remove. This can only delete messages - that are newer than ``2`` weeks in age. If any of the messages ar - older than ``2`` weeks then this call will fail. + ValueError + If you try to delete over `100` messages in a single request. + + !!! note + This can only be used on guild text channels. + Any message IDs that do not exist or are invalid still add towards + the total `100` max messages to remove. This can only delete + messages that are newer than `2` weeks in age. If any of the + messages are older than `2` weeks then this call will fail. """ if additional_messages: messages = list( @@ -750,32 +740,32 @@ async def update_channel_overwrite( Parameters ---------- - channel : :obj:`~typing.Union` [ :obj:`~hikari.messages.Message`, :obj:`~hikari.bases.Snowflake`, :obj:`~int` ] + channel : typing.Union [ hikari.messages.Message, hikari.bases.Snowflake, int ] The object or ID of the channel to edit permissions for. - overwrite : :obj:`~typing.Union` [ :obj:`~hikari.channels.PermissionOverwrite`, :obj:`~hikari.guilds.GuildRole`, :obj:`~hikari.users.User`, :obj:`~hikari.bases.Snowflake` , :obj:`~int` ] + overwrite : typing.Union [ hikari.channels.PermissionOverwrite, hikari.guilds.GuildRole, hikari.users.User, hikari.bases.Snowflake , int ] The object or ID of the target member or role to edit/create the overwrite for. - target_type : :obj:`~typing.Union` [ :obj:`~hikari.channels.PermissionOverwriteType`, :obj:`~int` ] + target_type : typing.Union [ hikari.channels.PermissionOverwriteType, int ] The type of overwrite, passing a raw string that's outside of the enum's range for this may lead to unexpected behaviour. - allow : :obj:`~typing.Union` [ :obj:`~hikari.permissions.Permission`, :obj:`~int` ] + allow : typing.Union [ hikari.permissions.Permission, int ] If specified, the value of all permissions to set to be allowed, passing a raw integer for this may lead to unexpected behaviour. - deny : :obj:`~typing.Union` [ :obj:`~hikari.permissions.Permission`, :obj:`~int` ] + deny : typing.Union [ hikari.permissions.Permission, int ] If specified, the value of all permissions to set to be denied, passing a raw integer for this may lead to unexpected behaviour. - reason : :obj:`~str` + reason : str If specified, the audit log reason explaining why the operation was performed. Raises ------ - :obj:`~hikari.errors.BadRequestHTTPError` + hikari.errors.BadRequestHTTPError If any invalid snowflake IDs are passed; a snowflake may be invalid due to it being outside of the range of a 64 bit integer. - :obj:`~hikari.errors.NotFoundHTTPError` + hikari.errors.NotFoundHTTPError If the target channel or overwrite doesn't exist. - :obj:`~hikari.errors.ForbiddenHTTPError` + hikari.errors.ForbiddenHTTPError If you lack permission to do this. """ await self._session.edit_channel_permissions( @@ -794,22 +784,22 @@ async def fetch_invites_for_channel( Parameters ---------- - channel : :obj:`~typing.Union` [ :obj:`~hikari.channels.Channel`, :obj:`~hikari.bases.Snowflake`, :obj:`~int` ] + channel : typing.Union [ hikari.channels.Channel, hikari.bases.Snowflake, int ] The object or ID of the channel to get invites for. Returns ------- - :obj:`~typing.Sequence` [ :obj:`~hikari.invites.InviteWithMetadata` ] + typing.Sequence [ hikari.invites.InviteWithMetadata ] A list of invite objects. Raises ------ - :obj:`~hikari.errors.BadRequestHTTPError` + hikari.errors.BadRequestHTTPError If any invalid snowflake IDs are passed; a snowflake may be invalid due to it being outside of the range of a 64 bit integer. - :obj:`~hikari.errors.ForbiddenHTTPError` - If you lack the ``MANAGE_CHANNELS`` permission. - :obj:`~hikari.errors.NotFoundHTTPError` + hikari.errors.ForbiddenHTTPError + If you lack the `MANAGE_CHANNELS` permission. + hikari.errors.NotFoundHTTPError If the channel does not exist. """ payload = await self._session.get_channel_invites( @@ -833,42 +823,42 @@ async def create_invite_for_channel( Parameters ---------- - channel : :obj:`~typing.Union` [ :obj:`~datetime.timedelta`, :obj:`~str` ] + channel : typing.Union [ datetime.timedelta, str ] The object or ID of the channel to create the invite for. - max_age : :obj:`~int` + max_age : int If specified, the seconds time delta for the max age of the invite, - defaults to ``86400`` seconds (``24`` hours). - Set to ``0`` seconds to never expire. - max_uses : :obj:`~int` - If specified, the max number of uses this invite can have, or ``0`` + defaults to `86400` seconds (`24` hours). + Set to `0` seconds to never expire. + max_uses : int + If specified, the max number of uses this invite can have, or `0` for unlimited (as per the default). - temporary : :obj:`~bool` + temporary : bool If specified, whether to grant temporary membership, meaning the user is kicked when their session ends unless they are given a role. - unique : :obj:`~bool` + unique : bool If specified, whether to try to reuse a similar invite. - target_user : :obj:`~typing.Union` [ :obj:`~hikari.users.User`, :obj:`~hikari.bases.Snowflake`, :obj:`~int` ] + target_user : typing.Union [ hikari.users.User, hikari.bases.Snowflake, int ] If specified, the object or ID of the user this invite should target. - target_user_type : :obj:`~typing.Union` [ :obj:`~hikari.invites.TargetUserType`, :obj:`~int` ] + target_user_type : typing.Union [ hikari.invites.TargetUserType, int ] If specified, the type of target for this invite, passing a raw integer for this may lead to unexpected results. - reason : :obj:`~str` + reason : str If specified, the audit log reason explaining why the operation was performed. Returns ------- - :obj:`~hikari.invites.InviteWithMetadata` + hikari.invites.InviteWithMetadata The created invite object. Raises ------ - :obj:`~hikari.errors.ForbiddenHTTPError` - If you lack the ``CREATE_INSTANT_MESSAGES`` permission. - :obj:`~hikari.errors.NotFoundHTTPError` + hikari.errors.ForbiddenHTTPError + If you lack the `CREATE_INSTANT_MESSAGES` permission. + hikari.errors.NotFoundHTTPError If the channel does not exist. - :obj:`~hikari.errors.BadRequestHTTPError` + hikari.errors.BadRequestHTTPError If the arguments provided are not valid (e.g. negative age, etc). If any invalid snowflake IDs are passed; a snowflake may be invalid due to it being outside of the range of a 64 bit integer. @@ -898,20 +888,20 @@ async def delete_channel_overwrite( Parameters ---------- - channel : :obj:`~typing.Union` [ :obj:`~hikari.channels.Channel`, :obj:`~hikari.bases.Snowflake`, :obj:`~int` ] + channel : typing.Union [ hikari.channels.Channel, hikari.bases.Snowflake, int ] The object or ID of the channel to delete the overwrite from. - overwrite : :obj:`~typing.Union` [ :obj:`~hikari.channels.PermissionOverwrite`, :obj:`~hikari.guilds.GuildRole`, :obj:`~hikari.users.User`, :obj:`~hikari.bases.Snowflake`, :obj:int ] + overwrite : typing.Union [ hikari.channels.PermissionOverwrite, hikari.guilds.GuildRole, hikari.users.User, hikari.bases.Snowflake, int ] The ID of the entity this overwrite targets. Raises ------ - :obj:`~hikari.errors.BadRequestHTTPError` + hikari.errors.BadRequestHTTPError If any invalid snowflake IDs are passed; a snowflake may be invalid due to it being outside of the range of a 64 bit integer. - :obj:`~hikari.errors.NotFoundHTTPError` + hikari.errors.NotFoundHTTPError If the overwrite or channel do not exist. - :obj:`~hikari.errors.ForbiddenHTTPError` - If you lack the ``MANAGE_ROLES`` permission for that channel. + hikari.errors.ForbiddenHTTPError + If you lack the `MANAGE_ROLES` permission for that channel. """ await self._session.delete_channel_permission( channel_id=str(channel.id if isinstance(channel, bases.UniqueEntity) else int(channel)), @@ -919,21 +909,21 @@ async def delete_channel_overwrite( ) async def trigger_typing(self, channel: bases.Hashable[_channels.Channel]) -> None: - """Trigger the typing indicator for ``10`` seconds in a channel. + """Trigger the typing indicator for `10` seconds in a channel. Parameters ---------- - channel : :obj:`~typing.Union` [ :obj:`~hikari.channels.Channel`, :obj:`~hikari.bases.Snowflake`, :obj:`~int` ] + channel : typing.Union [ hikari.channels.Channel, hikari.bases.Snowflake, int ] The object or ID of the channel to appear to be typing in. Raises ------ - :obj:`~hikari.errors.BadRequestHTTPError` + hikari.errors.BadRequestHTTPError If any invalid snowflake IDs are passed; a snowflake may be invalid due to it being outside of the range of a 64 bit integer. - :obj:`~hikari.errors.NotFoundHTTPError` + hikari.errors.NotFoundHTTPError If the channel is not found. - :obj:`~hikari.errors.ForbiddenHTTPError` + hikari.errors.ForbiddenHTTPError If you are not able to type in the channel. """ await self._session.trigger_typing_indicator( @@ -947,29 +937,28 @@ async def fetch_pins( Parameters ---------- - channel : :obj:`~typing.Union` [ :obj:`~hikari.channels.Channel`, :obj:`~hikari.bases.Snowflake`, :obj:`~int` ] + channel : typing.Union [ hikari.channels.Channel, hikari.bases.Snowflake, int ] The object or ID of the channel to get messages from. Returns ------- - :obj:`~typing.Mapping` [ :obj:`~hikari.bases.Snowflake`, :obj:`~hikari.messages.Message` ] + typing.Mapping [ hikari.bases.Snowflake, hikari.messages.Message ] A list of message objects. Raises ------ - :obj:`~hikari.errors.BadRequestHTTPError` + hikari.errors.BadRequestHTTPError If any invalid snowflake IDs are passed; a snowflake may be invalid due to it being outside of the range of a 64 bit integer. - :obj:`~hikari.errors.NotFoundHTTPError` + hikari.errors.NotFoundHTTPError If the channel is not found. - :obj:`~hikari.errors.ForbiddenHTTPError` + hikari.errors.ForbiddenHTTPError If you are not able to see the channel. - Note - ---- - If you are not able to see the pinned message (eg. you are missing - ``READ_MESSAGE_HISTORY`` and the pinned message is an old message), it - will not be returned. + !!! note + If you are not able to see the pinned message (eg. you are missing + `READ_MESSAGE_HISTORY` and the pinned message is an old message), it + will not be returned. """ payload = await self._session.get_pinned_messages( channel_id=str(channel.id if isinstance(channel, bases.UniqueEntity) else int(channel)) @@ -983,19 +972,19 @@ async def pin_message( Parameters ---------- - channel : :obj:`~typing.Union` [ :obj:`~hikari.channels.Channel`, :obj:`~hikari.bases.Snowflake`, :obj:`~int` ] + channel : typing.Union [ hikari.channels.Channel, hikari.bases.Snowflake, int ] The object or ID of the channel to pin a message to. - message : :obj:`~typing.Union` [ :obj:`~hikari.messages.Message`, :obj:`~hikari.bases.Snowflake`, :obj:`~int` ] + message : typing.Union [ hikari.messages.Message, hikari.bases.Snowflake, int ] The object or ID of the message to pin. Raises ------ - :obj:`~hikari.errors.BadRequestHTTPError` + hikari.errors.BadRequestHTTPError If any invalid snowflake IDs are passed; a snowflake may be invalid due to it being outside of the range of a 64 bit integer. - :obj:`~hikari.errors.ForbiddenHTTPError` - If you lack the ``MANAGE_MESSAGES`` permission. - :obj:`~hikari.errors.NotFoundHTTPError` + hikari.errors.ForbiddenHTTPError + If you lack the `MANAGE_MESSAGES` permission. + hikari.errors.NotFoundHTTPError If the message or channel do not exist. """ await self._session.add_pinned_channel_message( @@ -1012,19 +1001,19 @@ async def unpin_message( Parameters ---------- - channel : :obj:`~typing.Union` [ :obj:`~hikari.channels.Channel`, :obj:`~hikari.bases.Snowflake`, :obj:`~int` ] + channel : typing.Union [ hikari.channels.Channel, hikari.bases.Snowflake, int ] The ID of the channel to remove a pin from. - message : :obj:`~typing.Union` [ :obj:`~hikari.messages.Message`, :obj:`~hikari.bases.Snowflake`, :obj:`~int` ] + message : typing.Union [ hikari.messages.Message, hikari.bases.Snowflake, int ] The object or ID of the message to unpin. Raises ------ - :obj:`~hikari.errors.BadRequestHTTPError` + hikari.errors.BadRequestHTTPError If any invalid snowflake IDs are passed; a snowflake may be invalid due to it being outside of the range of a 64 bit integer. - :obj:`~hikari.errors.ForbiddenHTTPError` - If you lack the ``MANAGE_MESSAGES`` permission. - :obj:`~hikari.errors.NotFoundHTTPError` + hikari.errors.ForbiddenHTTPError + If you lack the `MANAGE_MESSAGES` permission. + hikari.errors.NotFoundHTTPError If the message or channel do not exist. """ await self._session.delete_pinned_channel_message( @@ -1044,29 +1033,29 @@ async def create_webhook( Parameters ---------- - channel : :obj:`~typing.Union` [ :obj:`~hikari.channels.GuildChannel`, :obj:`~hikari.bases.Snowflake`, :obj:`~int` ] + channel : typing.Union [ hikari.channels.GuildChannel, hikari.bases.Snowflake, int ] The object or ID of the channel for webhook to be created in. - name : :obj:`~str` + name : str The webhook's name string. - avatar_data : ``hikari.internal.conversions.FileLikeT`` + avatar_data : `hikari.internal.conversions.FileLikeT` If specified, the avatar image data. - reason : :obj:`~str` + reason : str If specified, the audit log reason explaining why the operation was performed. Returns ------- - :obj:`~hikari.webhooks.Webhook` + hikari.webhooks.Webhook The newly created webhook object. Raises ------ - :obj:`~hikari.errors.NotFoundHTTPError` + hikari.errors.NotFoundHTTPError If the channel is not found. - :obj:`~hikari.errors.ForbiddenHTTPError` - If you either lack the ``MANAGE_WEBHOOKS`` permission or + hikari.errors.ForbiddenHTTPError + If you either lack the `MANAGE_WEBHOOKS` permission or can not see the given channel. - :obj:`~hikari.errors.BadRequestHTTPError` + hikari.errors.BadRequestHTTPError If the avatar image is too big or the format is invalid. If any invalid snowflake IDs are passed; a snowflake may be invalid due to it being outside of the range of a 64 bit integer. @@ -1086,23 +1075,23 @@ async def fetch_channel_webhooks( Parameters ---------- - channel : :obj:`~typing.Union` [ :obj:`~hikari.channels.GuildChannel`, :obj:`~hikari.bases.Snowflake`, :obj:`~int` ] + channel : typing.Union [ hikari.channels.GuildChannel, hikari.bases.Snowflake, int ] The object or ID of the guild channel to get the webhooks from. Returns ------- - :obj:`~typing.Sequence` [ :obj:`~hikari.webhooks.Webhook` ] + typing.Sequence [ hikari.webhooks.Webhook ] A list of webhook objects for the give channel. Raises ------ - :obj:`~hikari.errors.BadRequestHTTPError` + hikari.errors.BadRequestHTTPError If any invalid snowflake IDs are passed; a snowflake may be invalid due to it being outside of the range of a 64 bit integer. - :obj:`~hikari.errors.NotFoundHTTPError` + hikari.errors.NotFoundHTTPError If the channel is not found. - :obj:`~hikari.errors.ForbiddenHTTPError` - If you either lack the ``MANAGE_WEBHOOKS`` permission or + hikari.errors.ForbiddenHTTPError + If you either lack the `MANAGE_WEBHOOKS` permission or can not see the given channel. """ payload = await self._session.get_channel_webhooks( diff --git a/hikari/clients/rest/gateway.py b/hikari/clients/rest/gateway.py index d2ce803adf..2db1292b6a 100644 --- a/hikari/clients/rest/gateway.py +++ b/hikari/clients/rest/gateway.py @@ -34,12 +34,11 @@ async def fetch_gateway_url(self) -> str: Returns ------- - :obj:`~str` + str A static URL to use to connect to the gateway with. - Note - ---- - Users are expected to attempt to cache this result. + !!! note + Users are expected to attempt to cache this result. """ return await self._session.get_gateway() @@ -48,12 +47,12 @@ async def fetch_gateway_bot(self) -> gateway_entities.GatewayBot: Returns ------- - :obj:`~hikari.gateway_entities.GatewayBot` + hikari.gateway_entities.GatewayBot The bot specific gateway information object. - Note - ---- - Unlike :meth:`fetch_gateway_url`, this requires a valid token to work. + !!! note + Unlike `RESTGatewayComponent.fetch_gateway_url`, this requires a + valid token to work. """ payload = await self._session.get_gateway_bot() return gateway_entities.GatewayBot.deserialize(payload) diff --git a/hikari/clients/rest/guild.py b/hikari/clients/rest/guild.py index e58632db72..0842dbb107 100644 --- a/hikari/clients/rest/guild.py +++ b/hikari/clients/rest/guild.py @@ -60,33 +60,33 @@ async def fetch_audit_log( Parameters ---------- - guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.bases.Snowflake`, :obj:`~int` ] + guild : typing.Union [ hikari.guilds.Guild, hikari.bases.Snowflake, int ] The object or ID of the guild to get the audit logs for. - user : :obj:`~typing.Union` [ :obj:`~hikari.users.User`, :obj:`~hikari.bases.Snowflake`, :obj:`~int` ] + user : typing.Union [ hikari.users.User, hikari.bases.Snowflake, int ] If specified, the object or ID of the user to filter by. - action_type : :obj:`~typing.Union` [ :obj:`~hikari.audit_logs.AuditLogEventType`, :obj:`~int` ] + action_type : typing.Union [ hikari.audit_logs.AuditLogEventType, int ] If specified, the action type to look up. Passing a raw integer for this may lead to unexpected behaviour. - limit : :obj:`~int` + limit : int If specified, the limit to apply to the number of records. - Defaults to ``50``. Must be between ``1`` and ``100`` inclusive. - before : :obj:`~typing.Union` [ :obj:`~datetime.datetime`, :obj:`~hikari.audit_logs.AuditLogEntry`, :obj:`~hikari.bases.Snowflake`, :obj:`~int` ] + Defaults to `50`. Must be between `1` and `100` inclusive. + before : typing.Union [ datetime.datetime, hikari.audit_logs.AuditLogEntry, hikari.bases.Snowflake, int ] If specified, the object or ID of the entry that all retrieved - entries should have occurred befor. + entries should have occurred before. Returns ------- - :obj:`~hikari.audit_logs.AuditLog` + hikari.audit_logs.AuditLog An audit log object. Raises ------ - :obj:`~hikari.errors.BadRequestHTTPError` + hikari.errors.BadRequestHTTPError If any invalid snowflake IDs are passed; a snowflake may be invalid due to it being outside of the range of a 64 bit integer. - :obj:`~hikari.errors.ForbiddenHTTPError` + hikari.errors.ForbiddenHTTPError If you lack the given permissions to view an audit log. - :obj:`~hikari.errors.NotFoundHTTPError` + hikari.errors.NotFoundHTTPError If the guild does not exist. """ if isinstance(before, datetime.datetime): @@ -118,44 +118,41 @@ def fetch_audit_log_entries_before( Parameters ---------- - guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.bases.Snowflake`, :obj:`~int` ] + guild : typing.Union [ hikari.guilds.Guild, hikari.bases.Snowflake, int ] The ID or object of the guild to get audit log entries for - before : :obj:`~typing.Union` [ :obj:`~datetime.datetime`, :obj:`~hikari.audit_logs.AuditLogEntry`, :obj:`~hikari.bases.Snowflake`, :obj:`~int` ], optional + before : typing.Union [ datetime.datetime, hikari.audit_logs.AuditLogEntry, hikari.bases.Snowflake, int ], optional If specified, the ID or object of the entry or datetime to get entries that happened before otherwise this will start from the newest entry. - user : :obj:`~typing.Union` [ :obj:`~hikari.users.User`, :obj:`~hikari.bases.Snowflake`, :obj:`~int` ] + user : typing.Union [ hikari.users.User, hikari.bases.Snowflake, int ] If specified, the object or ID of the user to filter by. - action_type : :obj:`~typing.Union` [ :obj:`~hikari.audit_logs.AuditLogEventType`, :obj:`~int` ] + action_type : typing.Union [ hikari.audit_logs.AuditLogEventType, int ] If specified, the action type to look up. Passing a raw integer for this may lead to unexpected behaviour. - limit : :obj:`~int`, optional + limit : int, optional If specified, the limit for how many entries this iterator should return, defaults to unlimited. - Example - ------- - .. code-block:: python - + Examples + -------- audit_log_entries = client.fetch_audit_log_entries_before(guild, before=9876543, limit=6969) async for entry in audit_log_entries: if (user := audit_log_entries.users[entry.user_id]).is_bot: await client.ban_member(guild, user) - Note - ---- - The returned iterator has the attributes ``users``, ``members`` and - ``integrations`` which are mappings of snowflake IDs to objects for the - relevant entities that are referenced by the retrieved audit log - entries. These will be filled over time as more audit log entries are - fetched by the iterator. + !!! note + The returned iterator has the attributes `users`, `members` and + `integrations` which are mappings of snowflake IDs to objects for + the relevant entities that are referenced by the retrieved audit log + entries. These will be filled over time as more audit log entries + are fetched by the iterator. Returns ------- - :obj:`~hikari.errors.BadRequestHTTPError` + hikari.errors.BadRequestHTTPError If any invalid snowflake IDs are passed; a snowflake may be invalid due to it being outside of the range of a 64 bit integer. - :obj:`~hikari.audit_logs.AuditLogIterator` + hikari.audit_logs.AuditLogIterator An async iterator of the audit log entries in a guild (from newest to oldest). """ @@ -179,24 +176,24 @@ async def fetch_guild_emoji( Parameters ---------- - guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.bases.Snowflake`, :obj:`~int` ] + guild : typing.Union [ hikari.guilds.Guild, hikari.bases.Snowflake, int ] The object or ID of the guild to get the emoji from. - emoji : :obj:`~typing.Union` [ :obj:`~hikari.emojis.GuildEmoji`, :obj:`~hikari.bases.Snowflake`, :obj:`~int` ] + emoji : typing.Union [ hikari.emojis.GuildEmoji, hikari.bases.Snowflake, int ] The object or ID of the emoji to get. Returns ------- - :obj:`~hikari.emojis.GuildEmoji` + hikari.emojis.GuildEmoji A guild emoji object. Raises ------ - :obj:`~hikari.errors.BadRequestHTTPError` + hikari.errors.BadRequestHTTPError If any invalid snowflake IDs are passed; a snowflake may be invalid due to it being outside of the range of a 64 bit integer. - :obj:`~hikari.errors.NotFoundHTTPError` + hikari.errors.NotFoundHTTPError If either the guild or the emoji aren't found. - :obj:`~hikari.errors.ForbiddenHTTPError` + hikari.errors.ForbiddenHTTPError If you aren't a member of said guild. """ payload = await self._session.get_guild_emoji( @@ -210,22 +207,22 @@ async def fetch_guild_emojis(self, guild: bases.Hashable[guilds.Guild]) -> typin Parameters ---------- - guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.bases.Snowflake`, :obj:`~int` ] + guild : typing.Union [ hikari.guilds.Guild, hikari.bases.Snowflake, int ] The object or ID of the guild to get the emojis for. Returns ------- - :obj:`~typing.Sequence` [ :obj:`~hikari.emojis.GuildEmoji` ] + typing.Sequence [ hikari.emojis.GuildEmoji ] A list of guild emoji objects. Raises ------ - :obj:`~hikari.errors.BadRequestHTTPError` + hikari.errors.BadRequestHTTPError If any invalid snowflake IDs are passed; a snowflake may be invalid due to it being outside of the range of a 64 bit integer. - :obj:`~hikari.errors.NotFoundHTTPError` + hikari.errors.NotFoundHTTPError If the guild is not found. - :obj:`~hikari.errors.ForbiddenHTTPError` + hikari.errors.ForbiddenHTTPError If you aren't a member of the guild. """ payload = await self._session.list_guild_emojis( @@ -246,35 +243,35 @@ async def create_guild_emoji( Parameters ---------- - guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.GuildRole`, :obj:`~hikari.bases.Snowflake`, :obj:`~int` ] + guild : typing.Union [ hikari.guilds.GuildRole, hikari.bases.Snowflake, int ] The object or ID of the guild to create the emoji in. - name : :obj:`~str` + name : str The new emoji's name. - image_data : ``hikari.internal.conversions.FileLikeT`` - The ``128x128`` image data. - roles : :obj:`~typing.Sequence` [ :obj:`~typing.Union` [ :obj:`~hikari.guilds.GuildRole`, :obj:`~hikari.bases.Snowflake`, :obj:`~int` ] ] + image_data : hikari.internal.conversions.FileLikeT + The `128x128` image data. + roles : typing.Sequence [ typing.Union [ hikari.guilds.GuildRole, hikari.bases.Snowflake, int ] ] If specified, a list of role objects or IDs for which the emoji will be whitelisted. If empty, all roles are whitelisted. - reason : :obj:`~str` + reason : str If specified, the audit log reason explaining why the operation was performed. Returns ------- - :obj:`~hikari.emojis.GuildEmoji` + hikari.emojis.GuildEmoji The newly created emoji object. Raises ------ - :obj:`~ValueError` - If ``image`` is :obj:`~None`. - :obj:`~hikari.errors.NotFoundHTTPError` + ValueError + If `image` is `None`. + hikari.errors.NotFoundHTTPError If the guild is not found. - :obj:`~hikari.errors.ForbiddenHTTPError` - If you either lack the ``MANAGE_EMOJIS`` permission or aren't a + hikari.errors.ForbiddenHTTPError + If you either lack the `MANAGE_EMOJIS` permission or aren't a member of said guild. - :obj:`~hikari.errors.BadRequestHTTPError` - If you attempt to upload an image larger than ``256kb``, an empty + hikari.errors.BadRequestHTTPError + If you attempt to upload an image larger than `256kb`, an empty image or an invalid image format. If any invalid snowflake IDs are passed; a snowflake may be invalid due to it being outside of the range of a 64 bit integer. @@ -303,35 +300,35 @@ async def update_guild_emoji( Parameters ---------- - guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.bases.Snowflake`, :obj:`~int` ] + guild : typing.Union [ hikari.guilds.Guild, hikari.bases.Snowflake, int ] The object or ID of the guild to which the emoji to edit belongs to. - emoji : :obj:`~typing.Union` [ :obj:`~hikari.emojis.GuildEmoji`, :obj:`~hikari.bases.Snowflake`, :obj:`~int` ] + emoji : typing.Union [ hikari.emojis.GuildEmoji, hikari.bases.Snowflake, int ] The object or ID of the emoji to edit. - name : :obj:`~str` + name : str If specified, a new emoji name string. Keep unspecified to leave the name unchanged. - roles : :obj:`~typing.Sequence` [ :obj:`~typing.Union` [ :obj:`~hikari.guilds.GuildRole`, :obj:`~hikari.bases.Snowflake`, :obj:`~int` ] ] + roles : typing.Sequence [ typing.Union [ hikari.guilds.GuildRole, hikari.bases.Snowflake, int ] ] If specified, a list of objects or IDs for the new whitelisted roles. Set to an empty list to whitelist all roles. Keep unspecified to leave the same roles already set. - reason : :obj:`~str` + reason : str If specified, the audit log reason explaining why the operation was performed. Returns ------- - :obj:`~hikari.emojis.GuildEmoji` + hikari.emojis.GuildEmoji The updated emoji object. Raises ------ - :obj:`~hikari.errors.BadRequestHTTPError` + hikari.errors.BadRequestHTTPError If any invalid snowflake IDs are passed; a snowflake may be invalid due to it being outside of the range of a 64 bit integer. - :obj:`~hikari.errors.NotFoundHTTPError` + hikari.errors.NotFoundHTTPError If either the guild or the emoji aren't found. - :obj:`~hikari.errors.ForbiddenHTTPError` - If you either lack the ``MANAGE_EMOJIS`` permission or are not a + hikari.errors.ForbiddenHTTPError + If you either lack the `MANAGE_EMOJIS` permission or are not a member of the given guild. """ payload = await self._session.modify_guild_emoji( @@ -352,20 +349,20 @@ async def delete_guild_emoji( Parameters ---------- - guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.bases.Snowflake`, :obj:`~int` ] + guild : typing.Union [ hikari.guilds.Guild, hikari.bases.Snowflake, int ] The object or ID of the guild to delete the emoji from. - emoji : :obj:`~typing.Union` [ :obj:`~hikari.emojis.GuildEmoji`, :obj:`~hikari.bases.Snowflake`, :obj:`~int` ] + emoji : typing.Union [ hikari.emojis.GuildEmoji, hikari.bases.Snowflake, int ] The object or ID of the guild emoji to be deleted. Raises ------ - :obj:`~hikari.errors.BadRequestHTTPError` + hikari.errors.BadRequestHTTPError If any invalid snowflake IDs are passed; a snowflake may be invalid due to it being outside of the range of a 64 bit integer. - :obj:`~hikari.errors.NotFoundHTTPError` + hikari.errors.NotFoundHTTPError If either the guild or the emoji aren't found. - :obj:`~hikari.errors.ForbiddenHTTPError` - If you either lack the ``MANAGE_EMOJIS`` permission or aren't a + hikari.errors.ForbiddenHTTPError + If you either lack the `MANAGE_EMOJIS` permission or aren't a member of said guild. """ await self._session.delete_guild_emoji( @@ -387,47 +384,46 @@ async def create_guild( ) -> guilds.Guild: """Create a new guild. - Warning - ------- - Can only be used by bots in less than ``10`` guilds. + !!! warning + Can only be used by bots in less than `10` guilds. Parameters ---------- - name : :obj:`~str` - The name string for the new guild (``2-100`` characters). - region : :obj:`~str` + name : str + The name string for the new guild (`2-100` characters). + region : str If specified, the voice region ID for new guild. You can use - :meth:`fetch_guild_voice_regions` to see which region IDs are - available. - icon_data : ``hikari.internal.conversions.FileLikeT`` + `RESTGuildComponent.fetch_guild_voice_regions` to see which region + IDs are available. + icon_data : hikari.internal.conversions.FileLikeT If specified, the guild icon image data. - verification_level : :obj:`~typing.Union` [ :obj:`~hikari.guilds.GuildVerificationLevel`, :obj:`~int` ] + verification_level : typing.Union [ hikari.guilds.GuildVerificationLevel, int ] If specified, the verification level. Passing a raw int for this may lead to unexpected behaviour. - default_message_notifications : :obj:`~typing.Union` [ :obj:`~hikari.guilds.GuildMessageNotificationsLevel`, :obj:`~int` ] + default_message_notifications : typing.Union [ hikari.guilds.GuildMessageNotificationsLevel, int ] If specified, the default notification level. Passing a raw int for this may lead to unexpected behaviour. - explicit_content_filter : :obj:`~typing.Union` [ :obj:`~hikari.guilds.GuildExplicitContentFilterLevel`, :obj:`~int` ] + explicit_content_filter : typing.Union [ hikari.guilds.GuildExplicitContentFilterLevel, int ] If specified, the explicit content filter. Passing a raw int for this may lead to unexpected behaviour. - roles : :obj:`~typing.Sequence` [ :obj:`~hikari.guilds.GuildRole` ] + roles : typing.Sequence [ hikari.guilds.GuildRole ] If specified, an array of role objects to be created alongside the - guild. First element changes the ``@everyone`` role. - channels : :obj:`~typing.Sequence` [ :obj:`~hikari.channels.GuildChannelBuilder` ] + guild. First element changes the `@everyone` role. + channels : typing.Sequence [ hikari.channels.GuildChannelBuilder ] If specified, an array of guild channel builder objects to be created within the guild. Returns ------- - :obj:`~hikari.guilds.Guild` + hikari.guilds.Guild The newly created guild object. Raises ------ - :obj:`~hikari.errors.ForbiddenHTTPError` - If you are in ``10`` or more guilds. - :obj:`~hikari.errors.BadRequestHTTPError` - If you provide unsupported fields like ``parent_id`` in channel + hikari.errors.ForbiddenHTTPError + If you are in `10` or more guilds. + hikari.errors.BadRequestHTTPError + If you provide unsupported fields like `parent_id` in channel objects. If any invalid snowflake IDs are passed; a snowflake may be invalid due to it being outside of the range of a 64 bit integer. @@ -449,22 +445,22 @@ async def fetch_guild(self, guild: bases.Hashable[guilds.Guild]) -> guilds.Guild Parameters ---------- - guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.bases.Snowflake`, :obj:`~int` ] + guild : typing.Union [ hikari.guilds.Guild, hikari.bases.Snowflake, int ] The object or ID of the guild to get. Returns ------- - :obj:`~hikari.guilds.Guild` + hikari.guilds.Guild The requested guild object. Raises ------ - :obj:`~hikari.errors.BadRequestHTTPError` + hikari.errors.BadRequestHTTPError If any invalid snowflake IDs are passed; a snowflake may be invalid due to it being outside of the range of a 64 bit integer. - :obj:`~hikari.errors.NotFoundHTTPError` + hikari.errors.NotFoundHTTPError If the guild is not found. - :obj:`~hikari.errors.ForbiddenHTTPError` + hikari.errors.ForbiddenHTTPError If you don't have access to the guild. """ payload = await self._session.get_guild( @@ -477,26 +473,25 @@ async def fetch_guild_preview(self, guild: bases.Hashable[guilds.Guild]) -> guil Parameters ---------- - guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.bases.Snowflake`, :obj:`~int` ] + guild : typing.Union [ hikari.guilds.Guild, hikari.bases.Snowflake, int ] The object or ID of the guild to get the preview object for. Returns ------- - :obj:`~hikari.guilds.GuildPreview` + hikari.guilds.GuildPreview The requested guild preview object. - Note - ---- - Unlike other guild endpoints, the bot doesn't have to be in the target - guild to get it's preview. + !!! note + Unlike other guild endpoints, the bot doesn't have to be in the + target guild to get it's preview. Raises ------ - :obj:`~hikari.errors.BadRequestHTTPError` + hikari.errors.BadRequestHTTPError If any invalid snowflake IDs are passed; a snowflake may be invalid due to it being outside of the range of UINT64. - :obj:`~hikari.errors.NotFoundHTTPError` - If the guild is not found or it isn't ``PUBLIC``. + hikari.errors.NotFoundHTTPError + If the guild is not found or it isn't `PUBLIC`. """ payload = await self._session.get_guild_preview( guild_id=str(guild.id if isinstance(guild, bases.UniqueEntity) else int(guild)) @@ -524,53 +519,53 @@ async def update_guild( Parameters ---------- - guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.bases.Snowflake`, :obj:`~int` ] + guild : typing.Union [ hikari.guilds.Guild, hikari.bases.Snowflake, int ] The object or ID of the guild to be edited. - name : :obj:`~str` - If specified, the new name string for the guild (``2-100`` characters). - region : :obj:`~str` + name : str + If specified, the new name string for the guild (`2-100` characters). + region : str If specified, the new voice region ID for guild. You can use - :meth:`fetch_guild_voice_regions` to see which region IDs are - available. - verification_level : :obj:`~typing.Union` [ :obj:`~hikari.guilds.GuildVerificationLevel`, :obj:`~int` ] + `RESTGuildComponent.fetch_guild_voice_regions` to see which region + IDs are available. + verification_level : typing.Union [ hikari.guilds.GuildVerificationLevel, int ] If specified, the new verification level. Passing a raw int for this may lead to unexpected behaviour. - default_message_notifications : :obj:`~typing.Union` [ :obj:`~hikari.guilds.GuildMessageNotificationsLevel`, :obj:`~int` ] + default_message_notifications : typing.Union [ hikari.guilds.GuildMessageNotificationsLevel, int ] If specified, the new default notification level. Passing a raw int for this may lead to unexpected behaviour. - explicit_content_filter : :obj:`~typing.Union` [ :obj:`~hikari.guilds.GuildExplicitContentFilterLevel`, :obj:`~int` ] + explicit_content_filter : typing.Union [ hikari.guilds.GuildExplicitContentFilterLevel, int ] If specified, the new explicit content filter. Passing a raw int for this may lead to unexpected behaviour. - afk_channel : :obj:`~typing.Union` [ :obj:`~hikari.channels.GuildVoiceChannel`, :obj:`~hikari.bases.Snowflake`, :obj:`~int` ] + afk_channel : typing.Union [ hikari.channels.GuildVoiceChannel, hikari.bases.Snowflake, int ] If specified, the object or ID for the new AFK voice channel. - afk_timeout : :obj:`~typing.Union` [ :obj:`~datetime.timedelta`, :obj:`~int` ] + afk_timeout : typing.Union [ datetime.timedelta, int ] If specified, the new AFK timeout seconds timedelta. - icon_data : ``hikari.internal.conversions.FileLikeT`` + icon_data : hikari.internal.conversions.FileLikeT If specified, the new guild icon image file data. - owner : :obj:`~typing.Union` [ :obj:`~hikari.users.User`, :obj:`~hikari.bases.Snowflake`, :obj:`~int` ] + owner : typing.Union [ hikari.users.User, hikari.bases.Snowflake, int ] If specified, the object or ID of the new guild owner. - splash_data : ``hikari.internal.conversions.FileLikeT`` + splash_data : hikari.internal.conversions.FileLikeT If specified, the new new splash image file data. - system_channel : :obj:`~typing.Union` [ :obj:`~hikari.channels.GuildVoiceChannel`, :obj:`~hikari.bases.Snowflake`, :obj:`~int` ] + system_channel : typing.Union [ hikari.channels.GuildVoiceChannel, hikari.bases.Snowflake, int ] If specified, the object or ID of the new system channel. - reason : :obj:`~str` + reason : str If specified, the audit log reason explaining why the operation was performed. Returns ------- - :obj:`~hikari.guilds.Guild` + hikari.guilds.Guild The edited guild object. Raises ------ - :obj:`~hikari.errors.BadRequestHTTPError` + hikari.errors.BadRequestHTTPError If any invalid snowflake IDs are passed; a snowflake may be invalid due to it being outside of the range of a 64 bit integer. - :obj:`~hikari.errors.NotFoundHTTPError` + hikari.errors.NotFoundHTTPError If the guild is not found. - :obj:`~hikari.errors.ForbiddenHTTPError` - If you lack the ``MANAGE_GUILD`` permission or are not in the guild. + hikari.errors.ForbiddenHTTPError + If you lack the `MANAGE_GUILD` permission or are not in the guild. """ payload = await self._session.modify_guild( guild_id=str(guild.id if isinstance(guild, bases.UniqueEntity) else int(guild)), @@ -606,17 +601,17 @@ async def delete_guild(self, guild: bases.Hashable[guilds.Guild]) -> None: Parameters ---------- - guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.bases.Snowflake`, :obj:`~int` ] + guild : typing.Union [ hikari.guilds.Guild, hikari.bases.Snowflake, int ] The object or ID of the guild to be deleted. Raises ------ - :obj:`~hikari.errors.BadRequestHTTPError` + hikari.errors.BadRequestHTTPError If any invalid snowflake IDs are passed; a snowflake may be invalid due to it being outside of the range of a 64 bit integer. - :obj:`~hikari.errors.NotFoundHTTPError` + hikari.errors.NotFoundHTTPError If the guild is not found. - :obj:`~hikari.errors.ForbiddenHTTPError` + hikari.errors.ForbiddenHTTPError If you are not the guild owner. """ await self._session.delete_guild( @@ -630,22 +625,22 @@ async def fetch_guild_channels( Parameters ---------- - guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.bases.Snowflake`, :obj:`~int` ] + guild : typing.Union [ hikari.guilds.Guild, hikari.bases.Snowflake, int ] The object or ID of the guild to get the channels from. Returns ------- - :obj:`~typing.Sequence` [ :obj:`~hikari.channels.GuildChannel` ] + typing.Sequence [ hikari.channels.GuildChannel ] A list of guild channel objects. Raises ------ - :obj:`~hikari.errors.BadRequestHTTPError` + hikari.errors.BadRequestHTTPError If any invalid snowflake IDs are passed; a snowflake may be invalid due to it being outside of the range of a 64 bit integer. - :obj:`~hikari.errors.NotFoundHTTPError` + hikari.errors.NotFoundHTTPError If the guild is not found. - :obj:`~hikari.errors.ForbiddenHTTPError` + hikari.errors.ForbiddenHTTPError If you are not in the guild. """ payload = await self._session.list_guild_channels( @@ -672,62 +667,62 @@ async def create_guild_channel( Parameters ---------- - guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.bases.Snowflake`, :obj:`~int` ] + guild : typing.Union [ hikari.guilds.Guild, hikari.bases.Snowflake, int ] The object or ID of the guild to create the channel in. - name : :obj:`~str` + name : str If specified, the name for the channel. This must be - inclusively between ``1` and ``100`` characters in length. - channel_type: :obj:`~typing.Union` [ :obj:`~hikari.channels.ChannelType`, :obj:`~int` ] + inclusively between `1` and `100` characters in length. + channel_type: typing.Union [ hikari.channels.ChannelType, int ] If specified, the channel type, passing through a raw integer here may lead to unexpected behaviour. - position : :obj:`~int` + position : int If specified, the position to change the channel to. - topic : :obj:`~str` + topic : str If specified, the topic to set. This is only applicable to - text channels. This must be inclusively between ``0`` and ``1024`` + text channels. This must be inclusively between `0` and `1024` characters in length. - nsfw : :obj:`~bool` + nsfw : bool If specified, whether the channel will be marked as NSFW. Only applicable for text channels. - rate_limit_per_user : :obj:`~typing.Union` [ :obj:`~datetime.timedelta`, :obj:`~int` ] + rate_limit_per_user : typing.Union [ datetime.timedelta, int ] If specified, the second time delta the user has to wait before sending another message. This will not apply to bots, or to - members with ``MANAGE_MESSAGES`` or ``MANAGE_CHANNEL`` permissions. - This must be inclusively between ``0`` and ``21600`` seconds. - bitrate : :obj:`~int` + members with `MANAGE_MESSAGES` or `MANAGE_CHANNEL` permissions. + This must be inclusively between `0` and `21600` seconds. + bitrate : int If specified, the bitrate in bits per second allowable for the channel. This only applies to voice channels and must be inclusively - between ``8000`` and ``96000`` for normal servers or ``8000`` and - ``128000`` for VIP servers. - user_limit : :obj:`~int` + between `8000` and `96000` for normal servers or `8000` and + `128000` for VIP servers. + user_limit : int If specified, the max number of users to allow in a voice channel. - This must be between ``0`` and ``99`` inclusive, where - ``0`` implies no limit. - permission_overwrites : :obj:`~typing.Sequence` [ :obj:`~hikari.channels.PermissionOverwrite` ] + This must be between `0` and `99` inclusive, where + `0` implies no limit. + permission_overwrites : typing.Sequence [ hikari.channels.PermissionOverwrite ] If specified, the list of permission overwrite objects that are category specific to replace the existing overwrites with. - parent_category : :obj:`~typing.Union` [ :obj:`~hikari.channels.GuildCategory`, :obj:`~hikari.bases.Snowflake`, :obj:`~int` ] + parent_category : typing.Union [ hikari.channels.GuildCategory, hikari.bases.Snowflake, int ] If specified, the object or ID of the parent category to set for the channel. - reason : :obj:`~str` + reason : str If specified, the audit log reason explaining why the operation was performed. Returns ------- - :obj:`~hikari.channels.GuildChannel` + hikari.channels.GuildChannel The newly created channel object. Raises ------ - :obj:`~hikari.errors.NotFoundHTTPError` + hikari.errors.NotFoundHTTPError If the guild is not found. - :obj:`~hikari.errors.ForbiddenHTTPError` - If you lack the ``MANAGE_CHANNEL`` permission or are not in the + hikari.errors.ForbiddenHTTPError + If you lack the `MANAGE_CHANNEL` permission or are not in the guild. - :obj:`~hikari.errors.BadRequestHTTPError` + hikari.errors.BadRequestHTTPError If you provide incorrect options for the corresponding channel type - (e.g. a ``bitrate`` for a text channel). + (e.g. a `bitrate` for a text channel). If any invalid snowflake IDs are passed; a snowflake may be invalid due to it being outside of the range of a 64 bit integer. """ @@ -767,25 +762,25 @@ async def reposition_guild_channels( Parameters ---------- - guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.bases.Snowflake`, :obj:`~int` ] + guild : typing.Union [ hikari.guilds.Guild, hikari.bases.Snowflake, int ] The object or ID of the guild in which to edit the channels. - channel : :obj:`~typing.Tuple` [ :obj:`~int` , :obj:`~typing.Union` [ :obj:`~hikari.channels.GuildChannel`, :obj:`~hikari.bases.Snowflake`, :obj:`~int` ] ] + channel : typing.Tuple [ int , typing.Union [ hikari.channels.GuildChannel, hikari.bases.Snowflake, int ] ] The first channel to change the position of. This is a tuple of the integer position the channel object or ID. - *additional_channels : :obj:`~typing.Tuple` [ :obj:`~int`, :obj:`~typing.Union` [ :obj:`~hikari.channels.GuildChannel`, :obj:`~hikari.bases.Snowflake`, :obj:`~int` ] ] + *additional_channels : typing.Tuple [ int, typing.Union [ hikari.channels.GuildChannel, hikari.bases.Snowflake, int ] ] Optional additional channels to change the position of. These must be tuples of integer positions to change to and the channel object or ID and the. Raises ------ - :obj:`~hikari.errors.NotFoundHTTPError` + hikari.errors.NotFoundHTTPError If either the guild or any of the channels aren't found. - :obj:`~hikari.errors.ForbiddenHTTPError` - If you either lack the ``MANAGE_CHANNELS`` permission or are not a + hikari.errors.ForbiddenHTTPError + If you either lack the `MANAGE_CHANNELS` permission or are not a member of said guild or are not in the guild. - :obj:`~hikari.errors.BadRequestHTTPError` - If you provide anything other than the ``id`` and ``position`` + hikari.errors.BadRequestHTTPError + If you provide anything other than the `id` and `position` fields for the channels. If any invalid snowflake IDs are passed; a snowflake may be invalid due to it being outside of the range of a 64 bit integer. @@ -805,24 +800,24 @@ async def fetch_member( Parameters ---------- - guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.bases.Snowflake`, :obj:`~int` ] + guild : typing.Union [ hikari.guilds.Guild, hikari.bases.Snowflake, int ] The object or ID of the guild to get the member from. - user : :obj:`~typing.Union` [ :obj:`~hikari.users.User`, :obj:`~hikari.bases.Snowflake`, :obj:`~int` ] + user : typing.Union [ hikari.users.User, hikari.bases.Snowflake, int ] The object or ID of the member to get. Returns ------- - :obj:`~hikari.guilds.GuildMember` + hikari.guilds.GuildMember The requested member object. Raises ------ - :obj:`~hikari.errors.BadRequestHTTPError` + hikari.errors.BadRequestHTTPError If any invalid snowflake IDs are passed; a snowflake may be invalid due to it being outside of the range of a 64 bit integer. - :obj:`~hikari.errors.NotFoundHTTPError` + hikari.errors.NotFoundHTTPError If either the guild or the member aren't found. - :obj:`~hikari.errors.ForbiddenHTTPError` + hikari.errors.ForbiddenHTTPError If you don't have access to the target guild. """ payload = await self._session.get_guild_member( @@ -846,36 +841,34 @@ def fetch_members_after( Parameters ---------- - guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.bases.Snowflake`, :obj:`~int` ] + guild : typing.Union [ hikari.guilds.Guild, hikari.bases.Snowflake, int ] The object or ID of the guild to get the members from. - limit : :obj:`~int` + limit : int If specified, the maximum number of members this iterator should return. - after : :obj:`~typing.Union` [ :obj:`~datetime.datetime`, :obj:`~hikari.users.User`, :obj:`~hikari.bases.Snowflake`, :obj:`~int` ] + after : typing.Union [ datetime.datetime, hikari.users.User, hikari.bases.Snowflake, int ] The object or ID of the user this iterator should start after if specified, else this will start at the oldest user. - Example - ------- - .. code-block:: python - + Examples + -------- async for user in client.fetch_members_after(guild, after=9876543, limit=1231): if member.user.username[0] in HOIST_BLACKLIST: await client.update_member(member, nickname="💩") Returns ------- - :obj:`~typing.AsyncIterator` [ :obj:`~hikari.guilds.GuildMember` ] + typing.AsyncIterator [ hikari.guilds.GuildMember ] An async iterator of member objects. Raises ------ - :obj:`~hikari.errors.BadRequestHTTPError` + hikari.errors.BadRequestHTTPError If any invalid snowflake IDs are passed; a snowflake may be invalid due to it being outside of the range of a 64 bit integer. - :obj:`~hikari.errors.NotFoundHTTPError` + hikari.errors.NotFoundHTTPError If the guild is not found. - :obj:`~hikari.errors.ForbiddenHTTPError` + hikari.errors.ForbiddenHTTPError If you are not in the guild. """ if isinstance(after, datetime.datetime): @@ -908,40 +901,40 @@ async def update_member( Parameters ---------- - guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.bases.Snowflake`, :obj:`~int` ] + guild : typing.Union [ hikari.guilds.Guild, hikari.bases.Snowflake, int ] The object or ID of the guild to edit the member from. - user : :obj:`~typing.Union` [ :obj:`~hikari.guilds.GuildMember`, :obj:`~hikari.bases.Snowflake`, :obj:`~int` ] + user : typing.Union [ hikari.guilds.GuildMember, hikari.bases.Snowflake, int ] The object or ID of the member to edit. - nickname : :obj:`~str`, optional - If specified, the new nickname string. Setting it to :obj:`~None` + nickname : str, optional + If specified, the new nickname string. Setting it to `None` explicitly will clear the nickname. - roles : :obj:`~typing.Sequence` [ :obj:`~typing.Union` [ :obj:`~hikari.guilds.GuildRole`, :obj:`~hikari.bases.Snowflake`, :obj:`~int` ] ] + roles : typing.Sequence [ typing.Union [ hikari.guilds.GuildRole, hikari.bases.Snowflake, int ] ] If specified, a list of role IDs the member should have. - mute : :obj:`~bool` + mute : bool If specified, whether the user should be muted in the voice channel or not. - deaf : :obj:`~bool` + deaf : bool If specified, whether the user should be deafen in the voice channel or not. - voice_channel : :obj:`~typing.Union` [ :obj:`~hikari.channels.GuildVoiceChannel`, :obj:`~hikari.bases.Snowflake`, :obj:`~int` ], optional + voice_channel : typing.Union [ hikari.channels.GuildVoiceChannel, hikari.bases.Snowflake, int ], optional If specified, the ID of the channel to move the member to. Setting - it to :obj:`~None` explicitly will disconnect the user. - reason : :obj:`~str` + it to `None` explicitly will disconnect the user. + reason : str If specified, the audit log reason explaining why the operation was performed. Raises ------ - :obj:`~hikari.errors.NotFoundHTTPError` + hikari.errors.NotFoundHTTPError If either the guild, user, channel or any of the roles aren't found. - :obj:`~hikari.errors.ForbiddenHTTPError` - If you lack any of the applicable permissions - (``MANAGE_NICKNAMES``, ``MANAGE_ROLES``, ``MUTE_MEMBERS``, ``DEAFEN_MEMBERS`` or ``MOVE_MEMBERS``). + hikari.errors.ForbiddenHTTPError + If you lack any of the applicable permissions (`MANAGE_NICKNAMES`, + `MANAGE_ROLES`, `MUTE_MEMBERS`, `DEAFEN_MEMBERS` or `MOVE_MEMBERS`). Note that to move a member you must also have permission to connect to the end channel. This will also be raised if you're not in the guild. - :obj:`~hikari.errors.BadRequestHTTPError` - If you pass ``mute``, ``deaf`` or ``channel_id`` while the member + hikari.errors.BadRequestHTTPError + If you pass `mute`, `deaf` or `channel_id` while the member is not connected to a voice channel. If any invalid snowflake IDs are passed; a snowflake may be invalid due to it being outside of the range of a 64 bit integer. @@ -972,22 +965,22 @@ async def update_my_member_nickname( Parameters ---------- - guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.bases.Snowflake`, :obj:`~int` ] + guild : typing.Union [ hikari.guilds.Guild, hikari.bases.Snowflake, int ] The object or ID of the guild you want to change the nick on. - nickname : :obj:`~str`, optional + nickname : str, optional The new nick string. Setting this to `None` clears the nickname. - reason : :obj:`~str` + reason : str If specified, the audit log reason explaining why the operation was performed. Raises ------ - :obj:`~hikari.errors.NotFoundHTTPError` + hikari.errors.NotFoundHTTPError If the guild is not found. - :obj:`~hikari.errors.ForbiddenHTTPError` - If you lack the ``CHANGE_NICKNAME`` permission or are not in the + hikari.errors.ForbiddenHTTPError + If you lack the `CHANGE_NICKNAME` permission or are not in the guild. - :obj:`~hikari.errors.BadRequestHTTPError` + hikari.errors.BadRequestHTTPError If you provide a disallowed nickname, one that is too long, or one that is empty. If any invalid snowflake IDs are passed; a snowflake may be invalid @@ -1011,25 +1004,25 @@ async def add_role_to_member( Parameters ---------- - guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.bases.Snowflake`, :obj:`~int` ] + guild : typing.Union [ hikari.guilds.Guild, hikari.bases.Snowflake, int ] The object or ID of the guild the member belongs to. - user : :obj:`~typing.Union` [ :obj:`~hikari.users.User`, :obj:`~hikari.bases.Snowflake`, :obj:`~int` ] + user : typing.Union [ hikari.users.User, hikari.bases.Snowflake, int ] The object or ID of the member you want to add the role to. - role : :obj:`~typing.Union` [ :obj:`~hikari.guilds.GuildRole`, :obj:`~hikari.bases.Snowflake`, :obj:`~int` ] + role : typing.Union [ hikari.guilds.GuildRole, hikari.bases.Snowflake, int ] The object or ID of the role you want to add. - reason : :obj:`~str` + reason : str If specified, the audit log reason explaining why the operation was performed. Raises ------ - :obj:`~hikari.errors.BadRequestHTTPError` + hikari.errors.BadRequestHTTPError If any invalid snowflake IDs are passed; a snowflake may be invalid due to it being outside of the range of a 64 bit integer. - :obj:`~hikari.errors.NotFoundHTTPError` + hikari.errors.NotFoundHTTPError If either the guild, member or role aren't found. - :obj:`~hikari.errors.ForbiddenHTTPError` - If you lack the ``MANAGE_ROLES`` permission or are not in the guild. + hikari.errors.ForbiddenHTTPError + If you lack the `MANAGE_ROLES` permission or are not in the guild. """ await self._session.add_guild_member_role( guild_id=str(guild.id if isinstance(guild, bases.UniqueEntity) else int(guild)), @@ -1050,25 +1043,25 @@ async def remove_role_from_member( Parameters ---------- - guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.bases.Snowflake`, :obj:`~int` ] + guild : typing.Union [ hikari.guilds.Guild, hikari.bases.Snowflake, int ] The object or ID of the guild the member belongs to. - user : :obj:`~typing.Union` [ :obj:`~hikari.users.User`, :obj:`~hikari.bases.Snowflake`, :obj:`~int` ] + user : typing.Union [ hikari.users.User, hikari.bases.Snowflake, int ] The object or ID of the member you want to remove the role from. - role : :obj:`~typing.Union` [ :obj:`~hikari.guilds.GuildRole`, :obj:`~hikari.bases.Snowflake`, :obj:`~int` ] + role : typing.Union [ hikari.guilds.GuildRole, hikari.bases.Snowflake, int ] The object or ID of the role you want to remove. - reason : :obj:`~str` + reason : str If specified, the audit log reason explaining why the operation was performed. Raises ------ - :obj:`~hikari.errors.BadRequestHTTPError` + hikari.errors.BadRequestHTTPError If any invalid snowflake IDs are passed; a snowflake may be invalid due to it being outside of the range of a 64 bit integer. - :obj:`~hikari.errors.NotFoundHTTPError` + hikari.errors.NotFoundHTTPError If either the guild, member or role aren't found. - :obj:`~hikari.errors.ForbiddenHTTPError` - If you lack the ``MANAGE_ROLES`` permission or are not in the guild. + hikari.errors.ForbiddenHTTPError + If you lack the `MANAGE_ROLES` permission or are not in the guild. """ await self._session.remove_guild_member_role( guild_id=str(guild.id if isinstance(guild, bases.UniqueEntity) else int(guild)), @@ -1084,23 +1077,23 @@ async def kick_member( Parameters ---------- - guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.bases.Snowflake`, :obj:`~int` ] + guild : typing.Union [ hikari.guilds.Guild, hikari.bases.Snowflake, int ] The object or ID of the guild the member belongs to. - user : :obj:`~typing.Union` [ :obj:`~hikari.users.User`, :obj:`~hikari.bases.Snowflake`, :obj:`~int` ] + user : typing.Union [ hikari.users.User, hikari.bases.Snowflake, int ] The object or ID of the member you want to kick. - reason : :obj:`~str` + reason : str If specified, the audit log reason explaining why the operation was performed. Raises ------ - :obj:`~hikari.errors.BadRequestHTTPError` + hikari.errors.BadRequestHTTPError If any invalid snowflake IDs are passed; a snowflake may be invalid due to it being outside of the range of a 64 bit integer. - :obj:`~hikari.errors.NotFoundHTTPError` + hikari.errors.NotFoundHTTPError If either the guild or member aren't found. - :obj:`~hikari.errors.ForbiddenHTTPError` - If you lack the ``KICK_MEMBERS`` permission or are not in the guild. + hikari.errors.ForbiddenHTTPError + If you lack the `KICK_MEMBERS` permission or are not in the guild. """ await self._session.remove_guild_member( guild_id=str(guild.id if isinstance(guild, bases.UniqueEntity) else int(guild)), @@ -1115,26 +1108,26 @@ async def fetch_ban( Parameters ---------- - guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.bases.Snowflake`, :obj:`~int` ] + guild : typing.Union [ hikari.guilds.Guild, hikari.bases.Snowflake, int ] The object or ID of the guild you want to get the ban from. - user : :obj:`~typing.Union` [ :obj:`~hikari.users.User`, :obj:`~hikari.bases.Snowflake`, :obj:`~int` ] + user : typing.Union [ hikari.users.User, hikari.bases.Snowflake, int ] The object or ID of the user to get the ban information for. Returns ------- - :obj:`~hikari.guilds.GuildMemberBan` + hikari.guilds.GuildMemberBan A ban object for the requested user. Raises ------ - :obj:`~hikari.errors.BadRequestHTTPError` + hikari.errors.BadRequestHTTPError If any invalid snowflake IDs are passed; a snowflake may be invalid due to it being outside of the range of a 64 bit integer. - :obj:`~hikari.errors.NotFoundHTTPError` + hikari.errors.NotFoundHTTPError If either the guild or the user aren't found, or if the user is not banned. - :obj:`~hikari.errors.ForbiddenHTTPError` - If you lack the ``BAN_MEMBERS`` permission or are not in the guild. + hikari.errors.ForbiddenHTTPError + If you lack the `BAN_MEMBERS` permission or are not in the guild. """ payload = await self._session.get_guild_ban( guild_id=str(guild.id if isinstance(guild, bases.UniqueEntity) else int(guild)), @@ -1147,23 +1140,23 @@ async def fetch_bans(self, guild: bases.Hashable[guilds.Guild],) -> typing.Seque Parameters ---------- - guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.bases.Snowflake`, :obj:`~int` ] + guild : typing.Union [ hikari.guilds.Guild, hikari.bases.Snowflake, int ] The object or ID of the guild you want to get the bans from. Returns ------- - :obj:`~typing.Sequence` [ :obj:`~hikari.guilds.GuildMemberBan` ] + typing.Sequence [ hikari.guilds.GuildMemberBan ] A list of ban objects. Raises ------ - :obj:`~hikari.errors.BadRequestHTTPError` + hikari.errors.BadRequestHTTPError If any invalid snowflake IDs are passed; a snowflake may be invalid due to it being outside of the range of a 64 bit integer. - :obj:`~hikari.errors.NotFoundHTTPError` + hikari.errors.NotFoundHTTPError If the guild is not found. - :obj:`~hikari.errors.ForbiddenHTTPError` - If you lack the ``BAN_MEMBERS`` permission or are not in the guild. + hikari.errors.ForbiddenHTTPError + If you lack the `BAN_MEMBERS` permission or are not in the guild. """ payload = await self._session.get_guild_bans( guild_id=str(guild.id if isinstance(guild, bases.UniqueEntity) else int(guild)) @@ -1182,26 +1175,26 @@ async def ban_member( Parameters ---------- - guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.bases.Snowflake`, :obj:`~int` ] + guild : typing.Union [ hikari.guilds.Guild, hikari.bases.Snowflake, int ] The object or ID of the guild the member belongs to. - user : :obj:`~typing.Union` [ :obj:`~hikari.users.User`, :obj:`~hikari.bases.Snowflake`, :obj:`~int` ] + user : typing.Union [ hikari.users.User, hikari.bases.Snowflake, int ] The object or ID of the member you want to ban. - delete_message_days : :obj:`~typing.Union` [ :obj:`~datetime.timedelta`, :obj:`~int` ] + delete_message_days : typing.Union [ datetime.timedelta, int ] If specified, the tim delta of how many days of messages from the user should be removed. - reason : :obj:`~str` + reason : str If specified, the audit log reason explaining why the operation was performed. Raises ------ - :obj:`~hikari.errors.BadRequestHTTPError` + hikari.errors.BadRequestHTTPError If any invalid snowflake IDs are passed; a snowflake may be invalid due to it being outside of the range of a 64 bit integer. - :obj:`~hikari.errors.NotFoundHTTPError` + hikari.errors.NotFoundHTTPError If either the guild or member aren't found. - :obj:`~hikari.errors.ForbiddenHTTPError` - If you lack the ``BAN_MEMBERS`` permission or are not in the guild. + hikari.errors.ForbiddenHTTPError + If you lack the `BAN_MEMBERS` permission or are not in the guild. """ await self._session.create_guild_ban( guild_id=str(guild.id if isinstance(guild, bases.UniqueEntity) else int(guild)), @@ -1217,24 +1210,24 @@ async def unban_member( Parameters ---------- - guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.bases.Snowflake`, :obj:`~int` ] + guild : typing.Union [ hikari.guilds.Guild, hikari.bases.Snowflake, int ] The object or ID of the guild to un-ban the user from. - user : :obj:`~typing.Union` [ :obj:`~hikari.users.User`, :obj:`~hikari.bases.Snowflake`, :obj:`~int` ] + user : typing.Union [ hikari.users.User, hikari.bases.Snowflake, int ] The ID of the user you want to un-ban. - reason : :obj:`~str` + reason : str If specified, the audit log reason explaining why the operation was performed. Raises ------ - :obj:`~hikari.errors.BadRequestHTTPError` + hikari.errors.BadRequestHTTPError If any invalid snowflake IDs are passed; a snowflake may be invalid due to it being outside of the range of a 64 bit integer. - :obj:`~hikari.errors.NotFoundHTTPError` + hikari.errors.NotFoundHTTPError If either the guild or member aren't found, or the member is not banned. - :obj:`~hikari.errors.ForbiddenHTTPError` - If you lack the ``BAN_MEMBERS`` permission or are not a in the + hikari.errors.ForbiddenHTTPError + If you lack the `BAN_MEMBERS` permission or are not a in the guild. """ await self._session.remove_guild_ban( @@ -1250,22 +1243,22 @@ async def fetch_roles( Parameters ---------- - guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.bases.Snowflake`, :obj:`~int` ] + guild : typing.Union [ hikari.guilds.Guild, hikari.bases.Snowflake, int ] The object or ID of the guild you want to get the roles from. Returns ------- - :obj:`~typing.Mapping` [ :obj:`~hikari.bases.Snowflake`, :obj:`~hikari.guilds.GuildRole` ] + typing.Mapping [ hikari.bases.Snowflake, hikari.guilds.GuildRole ] A list of role objects. Raises ------ - :obj:`~hikari.errors.BadRequestHTTPError` + hikari.errors.BadRequestHTTPError If any invalid snowflake IDs are passed; a snowflake may be invalid due to it being outside of the range of a 64 bit integer. - :obj:`~hikari.errors.NotFoundHTTPError` + hikari.errors.NotFoundHTTPError If the guild is not found. - :obj:`~hikari.errors.ForbiddenHTTPError` + hikari.errors.ForbiddenHTTPError If you're not in the guild. """ payload = await self._session.get_guild_roles( @@ -1288,37 +1281,37 @@ async def create_role( Parameters ---------- - guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.bases.Snowflake`, :obj:`~int` ] + guild : typing.Union [ hikari.guilds.Guild, hikari.bases.Snowflake, int ] The object or ID of the guild you want to create the role on. - name : :obj:`~str` + name : str If specified, the new role name string. - permissions : :obj:`~typing.Union` [ :obj:`~hikari.permissions.Permission`, :obj:`~int` ] + permissions : typing.Union [ hikari.permissions.Permission, int ] If specified, the permissions integer for the role, passing a raw integer rather than the int flag may lead to unexpected results. - color : :obj:`~typing.Union` [ :obj:`~hikari.colors.Color`, :obj:`~int` ] + color : typing.Union [ hikari.colors.Color, int ] If specified, the color for the role. - hoist : :obj:`~bool` + hoist : bool If specified, whether the role will be hoisted. - mentionable : :obj:`~bool` + mentionable : bool If specified, whether the role will be able to be mentioned by any user. - reason : :obj:`~str` + reason : str If specified, the audit log reason explaining why the operation was performed. Returns ------- - :obj:`~hikari.guilds.GuildRole` + hikari.guilds.GuildRole The newly created role object. Raises ------ - :obj:`~hikari.errors.NotFoundHTTPError` + hikari.errors.NotFoundHTTPError If the guild is not found. - :obj:`~hikari.errors.ForbiddenHTTPError` - If you lack the ``MANAGE_ROLES`` permission or you're not in the + hikari.errors.ForbiddenHTTPError + If you lack the `MANAGE_ROLES` permission or you're not in the guild. - :obj:`~hikari.errors.BadRequestHTTPError` + hikari.errors.BadRequestHTTPError If you provide invalid values for the role attributes. If any invalid snowflake IDs are passed; a snowflake may be invalid due to it being outside of the range of a 64 bit integer. @@ -1344,28 +1337,28 @@ async def reposition_roles( Parameters ---------- - guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.bases.Snowflake`, :obj:`~int` ] + guild : typing.Union [ hikari.guilds.Guild, hikari.bases.Snowflake, int ] The ID of the guild the roles belong to. - role : :obj:`~typing.Tuple` [ :obj:`~int`, :obj:`~typing.Union` [ :obj:`~hikari.guilds.GuildRole`, :obj:`~hikari.bases.Snowflake`, :obj:`~int` ] ] + role : typing.Tuple [ int, typing.Union [ hikari.guilds.GuildRole, hikari.bases.Snowflake, int ] ] The first role to move. This is a tuple of the integer position and the role object or ID. - *additional_roles : :obj:`~typing.Tuple` [ :obj:`~int`, :obj:`~typing.Union` [ :obj:`~hikari.guilds.GuildRole`, :obj:`~hikari.bases.Snowflake`, :obj:`~int` ] ] + *additional_roles : typing.Tuple [ int, typing.Union [ hikari.guilds.GuildRole, hikari.bases.Snowflake, int ] ] Optional extra roles to move. These must be tuples of the integer position and the role object or ID. Returns ------- - :obj:`~typing.Sequence` [ :obj:`~hikari.guilds.GuildRole` ] + typing.Sequence [ hikari.guilds.GuildRole ] A list of all the guild roles. Raises ------ - :obj:`~hikari.errors.NotFoundHTTPError` + hikari.errors.NotFoundHTTPError If either the guild or any of the roles aren't found. - :obj:`~hikari.errors.ForbiddenHTTPError` - If you lack the ``MANAGE_ROLES`` permission or you're not in the + hikari.errors.ForbiddenHTTPError + If you lack the `MANAGE_ROLES` permission or you're not in the guild. - :obj:`~hikari.errors.BadRequestHTTPError` + hikari.errors.BadRequestHTTPError If you provide invalid values for the `position` fields. If any invalid snowflake IDs are passed; a snowflake may be invalid due to it being outside of the range of a 64 bit integer. @@ -1395,39 +1388,39 @@ async def update_role( Parameters ---------- - guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.bases.Snowflake`, :obj:`~int` ] + guild : typing.Union [ hikari.guilds.Guild, hikari.bases.Snowflake, int ] The object or ID of the guild the role belong to. - role : :obj:`~typing.Union` [ :obj:`~hikari.guilds.GuildRole`, :obj:`~hikari.bases.Snowflake`, :obj:`~int` ] + role : typing.Union [ hikari.guilds.GuildRole, hikari.bases.Snowflake, int ] The object or ID of the role you want to edit. - name : :obj:`~str` + name : str If specified, the new role's name string. - permissions : :obj:`~typing.Union` [ :obj:`~hikari.permissions.Permission`, :obj:`~int` ] + permissions : typing.Union [ hikari.permissions.Permission, int ] If specified, the new permissions integer for the role, passing a raw integer for this may lead to unexpected behaviour. - color : :obj:`~typing.Union` [ :obj:`~hikari.colors.Color`, :obj:`~int` ] + color : typing.Union [ hikari.colors.Color, int ] If specified, the new color for the new role passing a raw integer for this may lead to unexpected behaviour. - hoist : :obj:`~bool` + hoist : bool If specified, whether the role should hoist or not. - mentionable : :obj:`~bool` + mentionable : bool If specified, whether the role should be mentionable or not. - reason : :obj:`~str` + reason : str If specified, the audit log reason explaining why the operation was performed. Returns ------- - :obj:`~hikari.guilds.GuildRole` + hikari.guilds.GuildRole The edited role object. Raises ------ - :obj:`~hikari.errors.NotFoundHTTPError` + hikari.errors.NotFoundHTTPError If either the guild or role aren't found. - :obj:`~hikari.errors.ForbiddenHTTPError` - If you lack the ``MANAGE_ROLES`` permission or you're not in the + hikari.errors.ForbiddenHTTPError + If you lack the `MANAGE_ROLES` permission or you're not in the guild. - :obj:`~hikari.errors.BadRequestHTTPError` + hikari.errors.BadRequestHTTPError If you provide invalid values for the role attributes. If any invalid snowflake IDs are passed; a snowflake may be invalid due to it being outside of the range of a 64 bit integer. @@ -1449,20 +1442,20 @@ async def delete_role(self, guild: bases.Hashable[guilds.Guild], role: bases.Has Parameters ---------- - guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.bases.Snowflake`, :obj:`~int` ] + guild : typing.Union [ hikari.guilds.Guild, hikari.bases.Snowflake, int ] The object or ID of the guild you want to remove the role from. - role : :obj:`~typing.Union` [ :obj:`~hikari.guilds.GuildRole`, :obj:`~hikari.bases.Snowflake`, :obj:`~int` ] + role : typing.Union [ hikari.guilds.GuildRole, hikari.bases.Snowflake, int ] The object or ID of the role you want to delete. Raises ------ - :obj:`~hikari.errors.BadRequestHTTPError` + hikari.errors.BadRequestHTTPError If any invalid snowflake IDs are passed; a snowflake may be invalid due to it being outside of the range of a 64 bit integer. - :obj:`~hikari.errors.NotFoundHTTPError` + hikari.errors.NotFoundHTTPError If either the guild or the role aren't found. - :obj:`~hikari.errors.ForbiddenHTTPError` - If you lack the ``MANAGE_ROLES`` permission or are not in the guild. + hikari.errors.ForbiddenHTTPError + If you lack the `MANAGE_ROLES` permission or are not in the guild. """ await self._session.delete_guild_role( guild_id=str(guild.id if isinstance(guild, bases.UniqueEntity) else int(guild)), @@ -1476,23 +1469,23 @@ async def estimate_guild_prune_count( Parameters ---------- - guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.bases.Snowflake`, :obj:`~int` ] + guild : typing.Union [ hikari.guilds.Guild, hikari.bases.Snowflake, int ] The object or ID of the guild you want to get the count for. - days : :obj:`~typing.Union` [ :obj:`~datetime.timedelta`, :obj:`~int` ] - The time delta of days to count prune for (at least ``1``). + days : typing.Union [ datetime.timedelta, int ] + The time delta of days to count prune for (at least `1`). Returns ------- - :obj:`~int` + int The number of members estimated to be pruned. Raises ------ - :obj:`~hikari.errors.NotFoundHTTPError` + hikari.errors.NotFoundHTTPError If the guild is not found. - :obj:`~hikari.errors.ForbiddenHTTPError` - If you lack the ``KICK_MEMBERS`` or you are not in the guild. - :obj:`~hikari.errors.BadRequestHTTPError` + hikari.errors.ForbiddenHTTPError + If you lack the `KICK_MEMBERS` or you are not in the guild. + hikari.errors.BadRequestHTTPError If you pass an invalid amount of days. If any invalid snowflake IDs are passed; a snowflake may be invalid due to it being outside of the range of a 64 bit integer. @@ -1514,32 +1507,32 @@ async def begin_guild_prune( Parameters ---------- - guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.bases.Snowflake`, :obj:`~int` ] + guild : typing.Union [ hikari.guilds.Guild, hikari.bases.Snowflake, int ] The object or ID of the guild you want to prune member of. - days : :obj:`~typing.Union` [ :obj:`~datetime.timedelta`, :obj:`~int` ] + days : typing.Union [ datetime.timedelta, int ] The time delta of inactivity days you want to use as filter. - compute_prune_count : :obj:`~bool` + compute_prune_count : bool Whether a count of pruned members is returned or not. Discouraged for large guilds out of politeness. - reason : :obj:`~str` + reason : str If specified, the audit log reason explaining why the operation was performed. Returns ------- - :obj:`~int`, optional - The number of members who were kicked if ``compute_prune_count`` - is :obj:`~True`, else :obj:`~None`. + int, optional + The number of members who were kicked if `compute_prune_count` + is `True`, else `None`. Raises ------ - :obj:`~hikari.errors.NotFoundHTTPError` + hikari.errors.NotFoundHTTPError If the guild is not found: - :obj:`~hikari.errors.ForbiddenHTTPError` - If you lack the ``KICK_MEMBER`` permission or are not in the guild. - :obj:`~hikari.errors.BadRequestHTTPError` - If you provide invalid values for the ``days`` or - ``compute_prune_count`` fields. + hikari.errors.ForbiddenHTTPError + If you lack the `KICK_MEMBER` permission or are not in the guild. + hikari.errors.BadRequestHTTPError + If you provide invalid values for the `days` or + `compute_prune_count` fields. If any invalid snowflake IDs are passed; a snowflake may be invalid due to it being outside of the range of a 64 bit integer. """ @@ -1557,22 +1550,22 @@ async def fetch_guild_voice_regions( Parameters ---------- - guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.bases.Snowflake`, :obj:`~int` ] + guild : typing.Union [ hikari.guilds.Guild, hikari.bases.Snowflake, int ] The object or ID of the guild to get the voice regions for. Returns ------- - :obj:`~typing.Sequence` [ :obj:`~hikari.voices.VoiceRegion` ] + typing.Sequence [ hikari.voices.VoiceRegion ] A list of voice region objects. Raises ------ - :obj:`~hikari.errors.BadRequestHTTPError` + hikari.errors.BadRequestHTTPError If any invalid snowflake IDs are passed; a snowflake may be invalid due to it being outside of the range of a 64 bit integer. - :obj:`~hikari.errors.NotFoundHTTPError` + hikari.errors.NotFoundHTTPError If the guild is not found. - :obj:`~hikari.errors.ForbiddenHTTPError` + hikari.errors.ForbiddenHTTPError If you are not in the guild. """ payload = await self._session.get_guild_voice_regions( @@ -1587,23 +1580,23 @@ async def fetch_guild_invites( Parameters ---------- - guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.bases.Snowflake`, :obj:`~int` ] + guild : typing.Union [ hikari.guilds.Guild, hikari.bases.Snowflake, int ] The object or ID of the guild to get the invites for. Returns ------- - :obj:`~typing.Sequence` [ :obj:`~hikari.invites.InviteWithMetadata` ] + typing.Sequence [ hikari.invites.InviteWithMetadata ] A list of invite objects (with metadata). Raises ------ - :obj:`~hikari.errors.BadRequestHTTPError` + hikari.errors.BadRequestHTTPError If any invalid snowflake IDs are passed; a snowflake may be invalid due to it being outside of the range of a 64 bit integer. - :obj:`~hikari.errors.NotFoundHTTPError` + hikari.errors.NotFoundHTTPError If the guild is not found. - :obj:`~hikari.errors.ForbiddenHTTPError` - If you lack the ``MANAGE_GUILD`` permission or are not in the guild. + hikari.errors.ForbiddenHTTPError + If you lack the `MANAGE_GUILD` permission or are not in the guild. """ payload = await self._session.get_guild_invites( guild_id=str(guild.id if isinstance(guild, bases.UniqueEntity) else int(guild)) @@ -1615,23 +1608,23 @@ async def fetch_integrations(self, guild: bases.Hashable[guilds.Guild]) -> typin Parameters ---------- - guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.bases.Snowflake`, :obj:`~int` ] + guild : typing.Union [ hikari.guilds.Guild, hikari.bases.Snowflake, int ] The object or ID of the guild to get the integrations for. Returns ------- - :obj:`~typing.Sequence` [ :obj:`~hikari.guilds.GuildIntegration` ] + typing.Sequence [ hikari.guilds.GuildIntegration ] A list of integration objects. Raises ------ - :obj:`~hikari.errors.BadRequestHTTPError` + hikari.errors.BadRequestHTTPError If any invalid snowflake IDs are passed; a snowflake may be invalid due to it being outside of the range of a 64 bit integer. - :obj:`~hikari.errors.NotFoundHTTPError` + hikari.errors.NotFoundHTTPError If the guild is not found. - :obj:`~hikari.errors.ForbiddenHTTPError` - If you lack the ``MANAGE_GUILD`` permission or are not in the guild. + hikari.errors.ForbiddenHTTPError + If you lack the `MANAGE_GUILD` permission or are not in the guild. """ payload = await self._session.get_guild_integrations( guild_id=str(guild.id if isinstance(guild, bases.UniqueEntity) else int(guild)) @@ -1652,32 +1645,32 @@ async def update_integration( Parameters ---------- - guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.bases.Snowflake`, :obj:`~int` ] + guild : typing.Union [ hikari.guilds.Guild, hikari.bases.Snowflake, int ] The object or ID of the guild to which the integration belongs to. - integration : :obj:`~typing.Union` [ :obj:`~hikari.guilds.GuildIntegration`, :obj:`~hikari.bases.Snowflake`, :obj:`~int` ] + integration : typing.Union [ hikari.guilds.GuildIntegration, hikari.bases.Snowflake, int ] The object or ID of the integration to update. - expire_behaviour : :obj:`~typing.Union` [ :obj:`~hikari.guilds.IntegrationExpireBehaviour`, :obj:`~int` ] + expire_behaviour : typing.Union [ hikari.guilds.IntegrationExpireBehaviour, int ] If specified, the behaviour for when an integration subscription expires (passing a raw integer for this may lead to unexpected behaviour). - expire_grace_period : :obj:`~typing.Union` [ :obj:`~datetime.timedelta`, :obj:`~int` ] + expire_grace_period : typing.Union [ datetime.timedelta, int ] If specified, time time delta of how many days the integration will ignore lapsed subscriptions for. - enable_emojis : :obj:`~bool` + enable_emojis : bool If specified, whether emojis should be synced for this integration. - reason : :obj:`~str` + reason : str If specified, the audit log reason explaining why the operation was performed. Raises ------ - :obj:`~hikari.errors.BadRequestHTTPError` + hikari.errors.BadRequestHTTPError If any invalid snowflake IDs are passed; a snowflake may be invalid due to it being outside of the range of a 64 bit integer. - :obj:`~hikari.errors.NotFoundHTTPError` + hikari.errors.NotFoundHTTPError If either the guild or the integration aren't found. - :obj:`~hikari.errors.ForbiddenHTTPError` - If you lack the ``MANAGE_GUILD`` permission or are not in the guild. + hikari.errors.ForbiddenHTTPError + If you lack the `MANAGE_GUILD` permission or are not in the guild. """ await self._session.modify_guild_integration( guild_id=str(guild.id if isinstance(guild, bases.UniqueEntity) else int(guild)), @@ -1699,22 +1692,22 @@ async def delete_integration( Parameters ---------- - guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.bases.Snowflake`, :obj:`~int` ] + guild : typing.Union [ hikari.guilds.Guild, hikari.bases.Snowflake, int ] The object or ID of the guild to which the integration belongs to. - integration : :obj:`~typing.Union` [ :obj:`~hikari.guilds.GuildIntegration`, :obj:`~hikari.bases.Snowflake`, :obj:`~int` ] + integration : typing.Union [ hikari.guilds.GuildIntegration, hikari.bases.Snowflake, int ] The object or ID of the integration to delete. - reason : :obj:`~str` + reason : str If specified, the audit log reason explaining why the operation was performed. Raises ------ - :obj:`~hikari.errors.BadRequestHTTPError` + hikari.errors.BadRequestHTTPError If any invalid snowflake IDs are passed; a snowflake may be invalid due to it being outside of the range of a 64 bit integer. - :obj:`~hikari.errors.NotFoundHTTPError` + hikari.errors.NotFoundHTTPError If either the guild or the integration aren't found. - :obj:`~hikari.errors.ForbiddenHTTPError` + hikari.errors.ForbiddenHTTPError If you lack the `MANAGE_GUILD` permission or are not in the guild. """ await self._session.delete_guild_integration( @@ -1730,20 +1723,20 @@ async def sync_guild_integration( Parameters ---------- - guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.bases.Snowflake`, :obj:`~int` ] + guild : typing.Union [ hikari.guilds.Guild, hikari.bases.Snowflake, int ] The object or ID of the guild to which the integration belongs to. - integration : :obj:`~typing.Union` [ :obj:`~hikari.guilds.GuildIntegration`, :obj:`~hikari.bases.Snowflake`, :obj:`~int` ] + integration : typing.Union [ hikari.guilds.GuildIntegration, hikari.bases.Snowflake, int ] The ID of the integration to sync. Raises ------ - :obj:`~hikari.errors.BadRequestHTTPError` + hikari.errors.BadRequestHTTPError If any invalid snowflake IDs are passed; a snowflake may be invalid due to it being outside of the range of a 64 bit integer. - :obj:`~hikari.errors.NotFoundHTTPError` + hikari.errors.NotFoundHTTPError If either the guild or the integration aren't found. - :obj:`~hikari.errors.ForbiddenHTTPError` - If you lack the ``MANAGE_GUILD`` permission or are not in the guild. + hikari.errors.ForbiddenHTTPError + If you lack the `MANAGE_GUILD` permission or are not in the guild. """ await self._session.sync_guild_integration( guild_id=str(guild.id if isinstance(guild, bases.UniqueEntity) else int(guild)), @@ -1755,23 +1748,23 @@ async def fetch_guild_embed(self, guild: bases.Hashable[guilds.Guild],) -> guild Parameters ---------- - guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.bases.Snowflake`, :obj:`~int` ] + guild : typing.Union [ hikari.guilds.Guild, hikari.bases.Snowflake, int ] The object or ID of the guild to get the embed for. Returns ------- - :obj:`~hikari.guilds.GuildEmbed` + hikari.guilds.GuildEmbed A guild embed object. Raises ------ - :obj:`~hikari.errors.BadRequestHTTPError` + hikari.errors.BadRequestHTTPError If any invalid snowflake IDs are passed; a snowflake may be invalid due to it being outside of the range of a 64 bit integer. - :obj:`~hikari.errors.NotFoundHTTPError` + hikari.errors.NotFoundHTTPError If the guild is not found. - :obj:`~hikari.errors.ForbiddenHTTPError` - If you either lack the ``MANAGE_GUILD`` permission or are not in + hikari.errors.ForbiddenHTTPError + If you either lack the `MANAGE_GUILD` permission or are not in the guild. """ payload = await self._session.get_guild_embed( @@ -1791,32 +1784,32 @@ async def update_guild_embed( Parameters ---------- - guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.bases.Snowflake`, :obj:`~int` ] + guild : typing.Union [ hikari.guilds.Guild, hikari.bases.Snowflake, int ] The object or ID of the guild to edit the embed for. - channel : :obj:`~typing.Union` [ :obj:`~hikari.channels.GuildChannel`, :obj:`~hikari.bases.Snowflake`, :obj:`~int` ], optional + channel : typing.Union [ hikari.channels.GuildChannel, hikari.bases.Snowflake, int ], optional If specified, the object or ID of the channel that this embed's - invite should target. Set to :obj:`~None` to disable invites for this + invite should target. Set to `None` to disable invites for this embed. - enabled : :obj:`~bool` + enabled : bool If specified, whether this embed should be enabled. - reason : :obj:`~str` + reason : str If specified, the audit log reason explaining why the operation was performed. Returns ------- - :obj:`~hikari.guilds.GuildEmbed` + hikari.guilds.GuildEmbed The updated embed object. Raises ------ - :obj:`~hikari.errors.BadRequestHTTPError` + hikari.errors.BadRequestHTTPError If any invalid snowflake IDs are passed; a snowflake may be invalid due to it being outside of the range of a 64 bit integer. - :obj:`~hikari.errors.NotFoundHTTPError` + hikari.errors.NotFoundHTTPError If the guild is not found. - :obj:`~hikari.errors.ForbiddenHTTPError` - If you either lack the ``MANAGE_GUILD`` permission or are not in + hikari.errors.ForbiddenHTTPError + If you either lack the `MANAGE_GUILD` permission or are not in the guild. """ payload = await self._session.modify_guild_embed( @@ -1837,24 +1830,24 @@ async def fetch_guild_vanity_url(self, guild: bases.Hashable[guilds.Guild],) -> Parameters ---------- - guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.bases.Snowflake`, :obj:`~int` ] + guild : typing.Union [ hikari.guilds.Guild, hikari.bases.Snowflake, int ] The object or ID of the guild to get the vanity URL for. Returns ------- - :obj:`~hikari.invites.VanityUrl` - A partial invite object containing the vanity URL in the ``code`` + hikari.invites.VanityUrl + A partial invite object containing the vanity URL in the `code` field. Raises ------ - :obj:`~hikari.errors.BadRequestHTTPError` + hikari.errors.BadRequestHTTPError If any invalid snowflake IDs are passed; a snowflake may be invalid due to it being outside of the range of a 64 bit integer. - :obj:`~hikari.errors.NotFoundHTTPError` + hikari.errors.NotFoundHTTPError If the guild is not found. - :obj:`~hikari.errors.ForbiddenHTTPError` - If you either lack the ``MANAGE_GUILD`` permission or are not in + hikari.errors.ForbiddenHTTPError + If you either lack the `MANAGE_GUILD` permission or are not in the guild. """ payload = await self._session.get_guild_vanity_url( @@ -1867,25 +1860,23 @@ def format_guild_widget_image(self, guild: bases.Hashable[guilds.Guild], *, styl Parameters ---------- - guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.bases.Snowflake`, :obj:`~int` ] + guild : typing.Union [ hikari.guilds.Guild, hikari.bases.Snowflake, int ] The object or ID of the guild to form the widget. - style : :obj:`~str` - If specified, the syle of the widget. + style : str + If specified, the style of the widget. Returns ------- - :obj:`~str` + str A URL to retrieve a PNG widget for your guild. - Note - ---- - This does not actually make any form of request, and shouldn't be - awaited. Thus, it doesn't have rate limits either. + !!! note + This does not actually make any form of request, and shouldn't be + awaited. Thus, it doesn't have rate limits either. - Warning - ------- - The guild must have the widget enabled in the guild settings for this - to be valid. + !!! warning + The guild must have the widget enabled in the guild settings for + this to be valid. """ return self._session.get_guild_widget_image_url( guild_id=str(guild.id if isinstance(guild, bases.UniqueEntity) else int(guild)), style=style @@ -1896,23 +1887,23 @@ async def fetch_guild_webhooks(self, guild: bases.Hashable[guilds.Guild]) -> typ Parameters ---------- - guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.bases.Snowflake`, :obj:`~int` ] + guild : typing.Union [ hikari.guilds.Guild, hikari.bases.Snowflake, int ] The object or ID for the guild to get the webhooks from. Returns ------- - :obj:`~typing.Sequence` [ :obj:`~hikari.webhooks.Webhook` ] + typing.Sequence [ hikari.webhooks.Webhook ] A list of webhook objects for the given guild. Raises ------ - :obj:`~hikari.errors.BadRequestHTTPError` + hikari.errors.BadRequestHTTPError If any invalid snowflake IDs are passed; a snowflake may be invalid due to it being outside of the range of a 64 bit integer. - :obj:`~hikari.errors.NotFoundHTTPError` + hikari.errors.NotFoundHTTPError If the guild is not found. - :obj:`~hikari.errors.ForbiddenHTTPError` - If you either lack the ``MANAGE_WEBHOOKS`` permission or + hikari.errors.ForbiddenHTTPError + If you either lack the `MANAGE_WEBHOOKS` permission or aren't a member of the given guild. """ payload = await self._session.get_guild_webhooks( diff --git a/hikari/clients/rest/invite.py b/hikari/clients/rest/invite.py index 07d5757e2b..ba5cdf0880 100644 --- a/hikari/clients/rest/invite.py +++ b/hikari/clients/rest/invite.py @@ -37,23 +37,23 @@ async def fetch_invite( Parameters ---------- - invite : :obj:`~typing.Union` [ :obj:`~hikari.invites.Invite`, :obj:`~str` ] + invite : typing.Union [ hikari.invites.Invite, str ] The object or code of the wanted invite. - with_counts : :bool: + with_counts : bool If specified, whether to attempt to count the number of times the invite has been used. Returns ------- - :obj:`~hikari.invites.Invite` + hikari.invites.Invite The requested invite object. Raises ------ - :obj:`~hikari.errors.BadRequestHTTPError` + hikari.errors.BadRequestHTTPError If any invalid snowflake IDs are passed; a snowflake may be invalid due to it being outside of the range of a 64 bit integer. - :obj:`~hikari.errors.NotFoundHTTPError` + hikari.errors.NotFoundHTTPError If the invite is not found. """ payload = await self._session.get_invite(invite_code=getattr(invite, "code", invite), with_counts=with_counts) @@ -64,21 +64,21 @@ async def delete_invite(self, invite: typing.Union[invites.Invite, str]) -> None Parameters ---------- - invite : :obj:`~typing.Union` [ :obj:`~hikari.invites.Invite`, :obj:`~str` ] + invite : typing.Union [ hikari.invites.Invite, str ] The object or ID for the invite to be deleted. Returns ------- - :obj:`~None` + None Nothing, unlike what the API specifies. This is done to maintain consistency with other calls of a similar nature in this API wrapper. Raises ------ - :obj:`~hikari.errors.NotFoundHTTPError` + hikari.errors.NotFoundHTTPError If the invite is not found. - :obj:`~hikari.errors.ForbiddenHTTPError` - If you lack either ``MANAGE_CHANNELS`` on the channel the invite - belongs to or ``MANAGE_GUILD`` for guild-global delete. + hikari.errors.ForbiddenHTTPError + If you lack either `MANAGE_CHANNELS` on the channel the invite + belongs to or `MANAGE_GUILD` for guild-global delete. """ await self._session.delete_invite(invite_code=getattr(invite, "code", invite)) diff --git a/hikari/clients/rest/me.py b/hikari/clients/rest/me.py index 121a19c5ca..4cb1b54356 100644 --- a/hikari/clients/rest/me.py +++ b/hikari/clients/rest/me.py @@ -16,7 +16,7 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . -"""The logic for handling requests to ``@me`` endpoints.""" +"""The logic for handling requests to `@me` endpoints.""" __all__ = ["RESTCurrentUserComponent"] @@ -35,14 +35,14 @@ class RESTCurrentUserComponent(base.BaseRESTComponent, abc.ABC): # pylint: disable=W0223 - """The REST client component for handling requests to ``@me`` endpoints.""" + """The REST client component for handling requests to `@me` endpoints.""" async def fetch_me(self) -> users.MyUser: """Get the current user that of the token given to the client. Returns ------- - :obj:`~hikari.users.MyUser` + hikari.users.MyUser The current user object. """ payload = await self._session.get_current_user() @@ -55,21 +55,21 @@ async def update_me( Parameters ---------- - username : :obj:`~str` + username : str If specified, the new username string. - avatar_data : ``hikari.internal.conversions.FileLikeT``, optional + avatar_data : hikari.internal.conversions.FileLikeT, optional If specified, the new avatar image data. - If it is :obj:`~None`, the avatar is removed. + If it is None, the avatar is removed. Returns ------- - :obj:`~hikari.users.MyUser` + hikari.users.MyUser The updated user object. Raises ------ - :obj:`~hikari.errors.BadRequestHTTPError` - If you pass username longer than the limit (``2-32``) or an invalid image. + hikari.errors.BadRequestHTTPError + If you pass username longer than the limit (`2-32`) or an invalid image. """ payload = await self._session.modify_current_user( username=username, @@ -81,15 +81,15 @@ async def fetch_my_connections(self) -> typing.Sequence[applications.OwnConnecti """ Get the current user's connections. - Note - ---- - This endpoint can be used with both ``Bearer`` and ``Bot`` tokens but - will usually return an empty list for bots (with there being some - exceptions to this, like user accounts that have been converted to bots). + !!! note + This endpoint can be used with both `Bearer` and `Bot` tokens but + will usually return an empty list for bots (with there being some + exceptions to this, like user accounts that have been converted to + bots). Returns ------- - :obj:`~typing.Sequence` [ :obj:`~hikari.oauth2.OwnConnection` ] + typing.Sequence [ hikari.applications.OwnConnection ] A list of connection objects. """ payload = await self._session.get_current_user_connections() @@ -108,30 +108,28 @@ def fetch_my_guilds_after( Parameters ---------- - after : :obj:`~typing.Union` [ :obj:`~datetime.datetime`, :obj:`~hikari.guilds.Guild`, :obj:`~hikari.bases.Snowflake`, :obj:`~int` ] + after : typing.Union [ datetime.datetime, hikari.guilds.Guild, hikari.bases.Snowflake, int ] The object or ID of a guild to get guilds that were created after it if specified, else this will start at the oldest guild. - limit : :obj:`~int` + limit : int If specified, the maximum amount of guilds that this paginator should return. - Example - ------- - .. code-block:: python - + Examples + -------- async for user in client.fetch_my_guilds_after(after=9876543, limit=1231): await client.leave_guild(guild) Returns ------- - :obj:`~typing.AsyncIterator` [ :obj:`~hikari.oauth2.OwnGuild` ] + typing.AsyncIterator [ hikari.applications.OwnGuild ] An async iterator of partial guild objects. Raises ------ - :obj:`~hikari.errors.NotFoundHTTPError` + hikari.errors.NotFoundHTTPError If the guild is not found. - :obj:`~hikari.errors.BadRequestHTTPError` + hikari.errors.BadRequestHTTPError If any invalid snowflake IDs are passed; a snowflake may be invalid due to it being outside of the range of a 64 bit integer. """ @@ -161,23 +159,23 @@ def fetch_my_guilds_before( Parameters ---------- - before : :obj:`~typing.Union` [ :obj:`~datetime.datetime`, :obj:`~hikari.guilds.Guild`, :obj:`~hikari.bases.Snowflake`, :obj:`~int` ] + before : typing.Union [ datetime.datetime, hikari.guilds.Guild, hikari.bases.Snowflake, int ] The object or ID of a guild to get guilds that were created before it if specified, else this will start at the newest guild. - limit : :obj:`~int` + limit : int If specified, the maximum amount of guilds that this paginator should return. Returns ------- - :obj:`~typing.AsyncIterator` [ :obj:`~hikari.oauth2.OwnGuild` ] + typing.AsyncIterator [ hikari.applications.OwnGuild ] An async iterator of partial guild objects. Raises ------ - :obj:`~hikari.errors.NotFoundHTTPError` + hikari.errors.NotFoundHTTPError If the guild is not found. - :obj:`~hikari.errors.BadRequestHTTPError` + hikari.errors.BadRequestHTTPError If any invalid snowflake IDs are passed; a snowflake may be invalid due to it being outside of the range of a 64 bit integer. """ @@ -199,14 +197,14 @@ async def leave_guild(self, guild: bases.Hashable[guilds.Guild]) -> None: Parameters ---------- - guild : :obj:`~typing.Union` [ :obj:`~hikari.guilds.Guild`, :obj:`~hikari.bases.Snowflake`, :obj:`~int` ] + guild : typing.Union [ hikari.guilds.Guild, hikari.bases.Snowflake, int ] The object or ID of the guild to leave. Raises ------ - :obj:`~hikari.errors.NotFoundHTTPError` + hikari.errors.NotFoundHTTPError If the guild is not found. - :obj:`~hikari.errors.BadRequestHTTPError` + hikari.errors.BadRequestHTTPError If any invalid snowflake IDs are passed; a snowflake may be invalid due to it being outside of the range of a 64 bit integer. """ @@ -217,19 +215,19 @@ async def create_dm_channel(self, recipient: bases.Hashable[users.User]) -> _cha Parameters ---------- - recipient : :obj:`~typing.Union` [ :obj:`~hikari.users.User`, :obj:`~hikari.bases.Snowflake`, :obj:`~int` ] + recipient : typing.Union [ hikari.users.User, hikari.bases.Snowflake, int ] The object or ID of the user to create the new DM channel with. Returns ------- - :obj:`~hikari.channels.DMChannel` + hikari.channels.DMChannel The newly created DM channel object. Raises ------ - :obj:`~hikari.errors.NotFoundHTTPError` + hikari.errors.NotFoundHTTPError If the recipient is not found. - :obj:`~hikari.errors.BadRequestHTTPError` + hikari.errors.BadRequestHTTPError If any invalid snowflake IDs are passed; a snowflake may be invalid due to it being outside of the range of a 64 bit integer. """ diff --git a/hikari/clients/rest/oauth2.py b/hikari/clients/rest/oauth2.py index 91f8ab5d91..2c83bc9187 100644 --- a/hikari/clients/rest/oauth2.py +++ b/hikari/clients/rest/oauth2.py @@ -34,7 +34,7 @@ async def fetch_my_application_info(self) -> applications.Application: Returns ------- - :obj:`~hikari.oauth2.Application` + hikari.applications.Application An application info object. """ payload = await self._session.get_current_application_info() diff --git a/hikari/clients/rest/react.py b/hikari/clients/rest/react.py index dabac88c3e..4da9df3894 100644 --- a/hikari/clients/rest/react.py +++ b/hikari/clients/rest/react.py @@ -46,25 +46,25 @@ async def create_reaction( Parameters ---------- - channel : :obj:`~typing.Union` [ :obj:`~hikari.channels.Channel`, :obj:`~hikari.bases.Snowflake`, :obj:`~int` ] + channel : typing.Union [ hikari.channels.Channel, hikari.bases.Snowflake, int ] The object or ID of the channel to add this reaction in. - message : :obj:`~typing.Union` [ :obj:`~hikari.messages.Message`, :obj:`~hikari.bases.Snowflake`, :obj:`~int` ] + message : typing.Union [ hikari.messages.Message, hikari.bases.Snowflake, int ] The object or ID of the message to add the reaction in. - emoji : :obj:`~typing.Union` [ :obj:`~hikari.emojis.Emoji`, :obj:`~str` ] + emoji : typing.Union [ hikari.emojis.Emoji, str ] The emoji to add. This can either be an emoji object or a string representation of an emoji. The string representation will be either - ``"name:id"`` for custom emojis else it's unicode character(s) (can + `"name:id"` for custom emojis else it's unicode character(s) (can be UTF-32). Raises ------ - :obj:`~hikari.errors.ForbiddenHTTPError` + hikari.errors.ForbiddenHTTPError If this is the first reaction using this specific emoji on this - message and you lack the ``ADD_REACTIONS`` permission. If you lack - ``READ_MESSAGE_HISTORY``, this may also raise this error. - :obj:`~hikari.errors.NotFoundHTTPError` + message and you lack the `ADD_REACTIONS` permission. If you lack + `READ_MESSAGE_HISTORY`, this may also raise this error. + hikari.errors.NotFoundHTTPError If the channel or message is not found, or if the emoji is not found. - :obj:`~hikari.errors.BadRequestHTTPError` + hikari.errors.BadRequestHTTPError If the emoji is not valid, unknown, or formatted incorrectly. If any invalid snowflake IDs are passed; a snowflake may be invalid due to it being outside of the range of a 64 bit integer. @@ -85,26 +85,26 @@ async def delete_reaction( Parameters ---------- - channel : :obj:`~typing.Union` [ :obj:`~hikari.channels.Channel`, :obj:`~hikari.bases.Snowflake`, :obj:`~int` ] + channel : typing.Union [ hikari.channels.Channel, hikari.bases.Snowflake, int ] The object or ID of the channel to add this reaction in. - message : :obj:`~typing.Union` [ :obj:`~hikari.messages.Message`, :obj:`~hikari.bases.Snowflake`, :obj:`~int` ] + message : typing.Union [ hikari.messages.Message, hikari.bases.Snowflake, int ] The object or ID of the message to add the reaction in. - emoji : :obj:`~typing.Union` [ :obj:`~hikari.emojis.Emoji`, :obj:`~str` ] + emoji : typing.Union [ hikari.emojis.Emoji, str ] The emoji to add. This can either be an emoji object or a string representation of an emoji. The string representation will be - either ``"name:id"`` for custom emojis else it's unicode + either `"name:id"` for custom emojis else it's unicode character(s) (can be UTF-32). Raises ------ - :obj:`~hikari.errors.ForbiddenHTTPError` + hikari.errors.ForbiddenHTTPError If this is the first reaction using this specific emoji on this - message and you lack the ``ADD_REACTIONS`` permission. If you lack - ``READ_MESSAGE_HISTORY``, this may also raise this error. - :obj:`~hikari.errors.NotFoundHTTPError` + message and you lack the `ADD_REACTIONS` permission. If you lack + `READ_MESSAGE_HISTORY`, this may also raise this error. + hikari.errors.NotFoundHTTPError If the channel or message is not found, or if the emoji is not found. - :obj:`~hikari.errors.BadRequestHTTPError` + hikari.errors.BadRequestHTTPError If the emoji is not valid, unknown, or formatted incorrectly. If any invalid snowflake IDs are passed; a snowflake may be invalid due to it being outside of the range of a 64 bit integer. @@ -122,20 +122,20 @@ async def delete_all_reactions( Parameters ---------- - channel : :obj:`~typing.Union` [ :obj:`~hikari.channels.Channel`, :obj:`~hikari.bases.Snowflake`, :obj:`~int` ] + channel : typing.Union [ hikari.channels.Channel, hikari.bases.Snowflake, int ] The object or ID of the channel to get the message from. - message : :obj:`~typing.Union` [ :obj:`~hikari.messages.Message`, :obj:`~hikari.bases.Snowflake`, :obj:`~int` ] + message : typing.Union [ hikari.messages.Message, hikari.bases.Snowflake, int ] The object or ID of the message to remove all reactions from. Raises ------ - :obj:`~hikari.errors.BadRequestHTTPError` + hikari.errors.BadRequestHTTPError If any invalid snowflake IDs are passed; a snowflake may be invalid due to it being outside of the range of a 64 bit integer. - :obj:`~hikari.errors.NotFoundHTTPError` + hikari.errors.NotFoundHTTPError If the channel or message is not found. - :obj:`~hikari.errors.ForbiddenHTTPError` - If you lack the ``MANAGE_MESSAGES`` permission. + hikari.errors.ForbiddenHTTPError + If you lack the `MANAGE_MESSAGES` permission. """ await self._session.delete_all_reactions( channel_id=str(channel.id if isinstance(channel, bases.UniqueEntity) else int(channel)), @@ -152,24 +152,24 @@ async def delete_all_reactions_for_emoji( Parameters ---------- - channel : :obj:`~typing.Union` [ :obj:`~hikari.channels.Channel`, :obj:`~hikari.bases.Snowflake`, :obj:`~int` ] + channel : typing.Union [ hikari.channels.Channel, hikari.bases.Snowflake, int ] The object or ID of the channel to get the message from. - message : :obj:`~typing.Union` [ :obj:`~hikari.messages.Message`, :obj:`~hikari.bases.Snowflake`, :obj:`~int` ] + message : typing.Union [ hikari.messages.Message, hikari.bases.Snowflake, int ] The object or ID of the message to delete the reactions from. - emoji : :obj:`~typing.Union` [ :obj:`~hikari.emojis.Emoji`, :obj:`~str` ] - The object or string representatiom of the emoji to delete. The - string representation will be either ``"name:id"`` for custom emojis + emoji : typing.Union [ hikari.emojis.Emoji, str ] + The object or string representation of the emoji to delete. The + string representation will be either `"name:id"` for custom emojis else it's unicode character(s) (can be UTF-32). Raises ------ - :obj:`~hikari.errors.BadRequestHTTPError` + hikari.errors.BadRequestHTTPError If any invalid snowflake IDs are passed; a snowflake may be invalid due to it being outside of the range of a 64 bit integer. - :obj:`~hikari.errors.NotFoundHTTPError` + hikari.errors.NotFoundHTTPError If the channel or message or emoji or user is not found. - :obj:`~hikari.errors.ForbiddenHTTPError` - If you lack the ``MANAGE_MESSAGES`` permission, or the channel is a + hikari.errors.ForbiddenHTTPError + If you lack the `MANAGE_MESSAGES` permission, or the channel is a DM channel. """ await self._session.delete_all_reactions_for_emoji( @@ -194,44 +194,42 @@ def fetch_reactors_after( Parameters ---------- - channel : :obj:`~typing.Union` [ :obj:`~hikari.channels.Channel`, :obj:`~hikari.bases.Snowflake`, :obj:`~int` ] + channel : typing.Union [ hikari.channels.Channel, hikari.bases.Snowflake, int ] The object or ID of the channel to get the message from. - message : :obj:`~typing.Union` [ :obj:`~hikari.messages.Message`, :obj:`~hikari.bases.Snowflake`, :obj:`~int` ] + message : typing.Union [ hikari.messages.Message, hikari.bases.Snowflake, int ] The object or ID of the message to get the reactions from. - emoji : :obj:`~typing.Union` [ :obj:`~hikari.emojis.Emoji`, :obj:`~str` ] + emoji : typing.Union [ hikari.emojis.Emoji, str ] The emoji to get. This can either be it's object or the string representation of the emoji. The string representation will be - either ``"name:id"`` for custom emojis else it's unicode + either `"name:id"` for custom emojis else it's unicode character(s) (can be UTF-32). - after : :obj:`~typing.Union` [ :obj:`~datetime.datetime`, :obj:`~hikari.users.User`, :obj:`~hikari.bases.Snowflake`, :obj:`~int` ] + after : typing.Union [ datetime.datetime, hikari.users.User, hikari.bases.Snowflake, int ] If specified, a object or ID user. If specified, only users with a snowflake that is lexicographically greater than the value will be returned. - limit : :obj:`~str` + limit : str If specified, the limit of the number of users this iterator should return. - Example - ------- - .. code-block:: python - + Examples + -------- async for user in client.fetch_reactors_after(channel, message, emoji, after=9876543, limit=1231): if user.is_bot: await client.kick_member(channel.guild_id, user) Returns ------- - :obj:`~typing.AsyncIterator` [ :obj:`~hikari.users.User` ] + typing.AsyncIterator [ hikari.users.User ] An async iterator of user objects. Raises ------ - :obj:`~hikari.errors.BadRequestHTTPError` + hikari.errors.BadRequestHTTPError If any invalid snowflake IDs are passed; a snowflake may be invalid due to it being outside of the range of a 64 bit integer. - :obj:`~hikari.errors.ForbiddenHTTPError` + hikari.errors.ForbiddenHTTPError If you lack access to the message. - :obj:`~hikari.errors.NotFoundHTTPError` + hikari.errors.NotFoundHTTPError If the channel or message is not found. """ if isinstance(after, datetime.datetime): diff --git a/hikari/clients/rest/user.py b/hikari/clients/rest/user.py index 9e81210b6c..cac74e8dbb 100644 --- a/hikari/clients/rest/user.py +++ b/hikari/clients/rest/user.py @@ -35,20 +35,20 @@ async def fetch_user(self, user: bases.Hashable[users.User]) -> users.User: Parameters ---------- - user : :obj:`~typing.Union` [ :obj:`~hikari.users.User`, :obj:`~hikari.bases.Snowflake`, :obj:`~int` ] + user : typing.Union [ hikari.users.User, hikari.bases.Snowflake, int ] The object or ID of the user to get. Returns ------- - :obj:`~hikari.users.User` + hikari.users.User The requested user object. Raises ------ - :obj:`~hikari.errors.BadRequestHTTPError` + hikari.errors.BadRequestHTTPError If any invalid snowflake IDs are passed; a snowflake may be invalid due to it being outside of the range of a 64 bit integer. - :obj:`~hikari.errors.NotFoundHTTPError` + hikari.errors.NotFoundHTTPError If the user is not found. """ payload = await self._session.get_user( diff --git a/hikari/clients/rest/voice.py b/hikari/clients/rest/voice.py index d519f8a8a9..59d6b2735b 100644 --- a/hikari/clients/rest/voice.py +++ b/hikari/clients/rest/voice.py @@ -35,12 +35,11 @@ async def fetch_voice_regions(self) -> typing.Sequence[voices.VoiceRegion]: Returns ------- - :obj:`~typing.Sequence` [ :obj:`~hikari.voices.VoiceRegion` ] + typing.Sequence [ hikari.voices.VoiceRegion ] A list of voice regions available - Note - ---- - This does not include VIP servers. + !!! note + This does not include VIP servers. """ payload = await self._session.list_voice_regions() return [voices.VoiceRegion.deserialize(region) for region in payload] diff --git a/hikari/clients/rest/webhook.py b/hikari/clients/rest/webhook.py index 7ba3f1c36a..29a293daa3 100644 --- a/hikari/clients/rest/webhook.py +++ b/hikari/clients/rest/webhook.py @@ -46,28 +46,28 @@ async def fetch_webhook( Parameters ---------- - webhook : :obj:`~typing.Union` [ :obj:`~hikari.webhooks.Webhook`, :obj:`~hikari.bases.Snowflake`, :obj:`~int` ] + webhook : typing.Union [ hikari.webhooks.Webhook, hikari.bases.Snowflake, int ] The object or ID of the webhook to get. - webhook_token : :obj:`~str` + webhook_token : str If specified, the webhook token to use to get it (bypassing this - session's provided authorization ``token``). + session's provided authorization `token`). Returns ------- - :obj:`~hikari.webhooks.Webhook` + hikari.webhooks.Webhook The requested webhook object. Raises ------ - :obj:`~hikari.errors.BadRequestHTTPError` + hikari.errors.BadRequestHTTPError If any invalid snowflake IDs are passed; a snowflake may be invalid due to it being outside of the range of a 64 bit integer. - :obj:`~hikari.errors.NotFoundHTTPError` + hikari.errors.NotFoundHTTPError If the webhook is not found. - :obj:`~hikari.errors.ForbiddenHTTPError` + hikari.errors.ForbiddenHTTPError If you're not in the guild that owns this webhook or - lack the ``MANAGE_WEBHOOKS`` permission. - :obj:`~hikari.errors.UnauthorizedHTTPError` + lack the `MANAGE_WEBHOOKS` permission. + hikari.errors.UnauthorizedHTTPError If you pass a token that's invalid for the target webhook. """ payload = await self._session.get_webhook( @@ -90,39 +90,39 @@ async def update_webhook( Parameters ---------- - webhook : :obj:`~typing.Union` [ :obj:`~hikari.webhooks.Webhook`, :obj:`~hikari.bases.Snowflake`, :obj:`~int` ] + webhook : typing.Union [ hikari.webhooks.Webhook, hikari.bases.Snowflake, int ] The object or ID of the webhook to edit. - webhook_token : :obj:`~str` + webhook_token : str If specified, the webhook token to use to modify it (bypassing this - session's provided authorization ``token``). - name : :obj:`~str` + session's provided authorization `token`). + name : str If specified, the new name string. - avatar_data : ``hikari.internal.conversions.FileLikeT``, optional - If specified, the new avatar image file object. If :obj:`~None`, then + avatar_data : hikari.internal.conversions.FileLikeT, optional + If specified, the new avatar image file object. If `None`, then it is removed. - channel : :obj:`~typing.Union` [ :obj:`~hikari.channels.GuildChannel`, :obj:`~hikari.bases.Snowflake`, :obj:`~int` ] + channel : typing.Union [ hikari.channels.GuildChannel, hikari.bases.Snowflake, int ] If specified, the object or ID of the new channel the given webhook should be moved to. - reason : :obj:`~str` + reason : str If specified, the audit log reason explaining why the operation was performed. Returns ------- - :obj:`~hikari.webhooks.Webhook` + hikari.webhooks.Webhook The updated webhook object. Raises ------ - :obj:`~hikari.errors.BadRequestHTTPError` + hikari.errors.BadRequestHTTPError If any invalid snowflake IDs are passed; a snowflake may be invalid due to it being outside of the range of a 64 bit integer. - :obj:`~hikari.errors.NotFoundHTTPError` + hikari.errors.NotFoundHTTPError If either the webhook or the channel aren't found. - :obj:`~hikari.errors.ForbiddenHTTPError` - If you either lack the ``MANAGE_WEBHOOKS`` permission or + hikari.errors.ForbiddenHTTPError + If you either lack the `MANAGE_WEBHOOKS` permission or aren't a member of the guild this webhook belongs to. - :obj:`~hikari.errors.UnauthorizedHTTPError` + hikari.errors.UnauthorizedHTTPError If you pass a token that's invalid for the target webhook. """ payload = await self._session.modify_webhook( @@ -148,23 +148,23 @@ async def delete_webhook(self, webhook: bases.Hashable[webhooks.Webhook], *, web Parameters ---------- - webhook : :obj:`~typing.Union` [ :obj:`~hikari.webhooks.Webhook`, :obj:`~hikari.bases.Snowflake`, :obj:`~int` ] + webhook : typing.Union [ hikari.webhooks.Webhook, hikari.bases.Snowflake, int ] The object or ID of the webhook to delete - webhook_token : :obj:`~str` + webhook_token : str If specified, the webhook token to use to delete it (bypassing this - session's provided authorization ``token``). + session's provided authorization `token`). Raises ------ - :obj:`~hikari.errors.BadRequestHTTPError` + hikari.errors.BadRequestHTTPError If any invalid snowflake IDs are passed; a snowflake may be invalid due to it being outside of the range of a 64 bit integer. - :obj:`~hikari.errors.NotFoundHTTPError` + hikari.errors.NotFoundHTTPError If the webhook is not found. - :obj:`~hikari.errors.ForbiddenHTTPError` - If you either lack the ``MANAGE_WEBHOOKS`` permission or + hikari.errors.ForbiddenHTTPError + If you either lack the `MANAGE_WEBHOOKS` permission or aren't a member of the guild this webhook belongs to. - :obj:`~hikari.errors.UnauthorizedHTTPError` + hikari.errors.UnauthorizedHTTPError If you pass a token that's invalid for the target webhook. """ await self._session.delete_webhook( @@ -192,65 +192,64 @@ async def execute_webhook( Parameters ---------- - webhook : :obj:`~typing.Union` [ :obj:`~hikari.webhooks.Webhook`, :obj:`~hikari.bases.Snowflake`, :obj:`~int` ] + webhook : typing.Union [ hikari.webhooks.Webhook, hikari.bases.Snowflake, int ] The object or ID of the webhook to execute. - webhook_token : :obj:`~str` + webhook_token : str The token of the webhook to execute. - content : :obj:`~str` + content : str If specified, the message content to send with the message. - username : :obj:`~str` + username : str If specified, the username to override the webhook's username for this request. - avatar_url : :obj:`~str` + avatar_url : str If specified, the url of an image to override the webhook's avatar with for this request. - tts : :obj:`~bool` + tts : bool If specified, whether the message will be sent as a TTS message. - wait : :obj:`~bool` + wait : bool If specified, whether this request should wait for the webhook to be executed and return the resultant message object. - file : ``hikari.media.IO`` + file : hikari.media.IO If specified, this is a file object to send along with the webhook - as defined in :mod:`hikari.media`. - embeds : :obj:`~typing.Sequence` [ :obj:`~hikari.embeds.Embed` ] - If specified, a sequence of ``1`` to ``10`` embed objects to send + as defined in `hikari.media`. + embeds : typing.Sequence [ hikari.embeds.Embed ] + If specified, a sequence of `1` to `10` embed objects to send with the embed. - mentions_everyone : :obj:`~bool` - Whether ``@everyone`` and ``@here`` mentions should be resolved by - discord and lead to actual pings, defaults to :obj:`~True`. - user_mentions : :obj:`~typing.Union` [ :obj:`~typing.Collection` [ :obj:`~typing.Union` [ :obj:`~hikari.users.User`, :obj:`~hikari.bases.Snowflake`, :obj:`~int` ], :obj:`~bool` ] + mentions_everyone : bool + Whether `@everyone` and `@here` mentions should be resolved by + discord and lead to actual pings, defaults to `True`. + user_mentions : typing.Union [ typing.Collection [ typing.Union [ hikari.users.User, hikari.bases.Snowflake, int ], bool ] Either an array of user objects/IDs to allow mentions for, - :obj:`~True` to allow all user mentions or :obj:`~False` to block all - user mentions from resolving, defaults to :obj:`~True`. - role_mentions : :obj:`~typing.Union` [ :obj:`~typing.Collection` [ :obj:`~typing.Union` [ :obj:`~hikari.guilds.GuildRole`, :obj:`~hikari.bases.Snowflake`, :obj:`~int` ] ], :obj:`~bool` ] + `True` to allow all user mentions or `False` to block all + user mentions from resolving, defaults to `True`. + role_mentions : typing.Union [ typing.Collection [ typing.Union [ hikari.guilds.GuildRole, hikari.bases.Snowflake, int ] ], bool ] Either an array of guild role objects/IDs to allow mentions for, - :obj:`~True` to allow all role mentions or :obj:`~False` to block all - role mentions from resolving, defaults to :obj:`~True`. + `True` to allow all role mentions or `False` to block all + role mentions from resolving, defaults to `True`. Returns ------- - :obj:`~hikari.messages.Message`, optional - The created message object, if ``wait`` is :obj:`~True`, else - :obj:`~None`. + hikari.messages.Message, optional + The created message object, if `wait` is `True`, else `None`. Raises ------ - :obj:`~hikari.errors.NotFoundHTTPError` + hikari.errors.NotFoundHTTPError If the channel ID or webhook ID is not found. - :obj:`~hikari.errors.BadRequestHTTPError` + hikari.errors.BadRequestHTTPError This can be raised if the file is too large; if the embed exceeds the defined limits; if the message content is specified only and - empty or greater than ``2000`` characters; if neither content, file + empty or greater than `2000` characters; if neither content, file or embeds are specified. If any invalid snowflake IDs are passed; a snowflake may be invalid due to it being outside of the range of a 64 bit integer. - :obj:`~hikari.errors.ForbiddenHTTPError` + hikari.errors.ForbiddenHTTPError If you lack permissions to send to this channel. - :obj:`~hikari.errors.UnauthorizedHTTPError` + hikari.errors.UnauthorizedHTTPError If you pass a token that's invalid for the target webhook. - :obj:`~ValueError` + ValueError If more than 100 unique objects/entities are passed for - ``role_mentions`` or ``user_mentions``. + `role_mentions` or `user_mentions`. """ payload = await self._session.execute_webhook( webhook_id=str(webhook.id if isinstance(webhook, bases.UniqueEntity) else int(webhook)), @@ -288,9 +287,10 @@ def safe_webhook_execute( ) -> typing.Coroutine[typing.Any, typing.Any, typing.Optional[_messages.Message]]: """Execute a webhook to create a message with mention safety. - This endpoint has the same signature as :attr:`execute_webhook` with - the only difference being that ``mentions_everyone``, - ``user_mentions`` and ``role_mentions`` default to :obj:`~False`. + This endpoint has the same signature as + `RESTWebhookComponent.execute_webhook` with the only difference being + that `mentions_everyone`, `user_mentions` and `role_mentions` default to + `False`. """ return self.execute_webhook( webhook=webhook, diff --git a/hikari/clients/runnable.py b/hikari/clients/runnable.py index 4525a9b583..bf73bb68dd 100644 --- a/hikari/clients/runnable.py +++ b/hikari/clients/runnable.py @@ -33,10 +33,8 @@ class RunnableClient(abc.ABC): __slots__ = ("logger",) - #: The logger to use for this client. - #: - #: :type: :obj:`~logging.Logger` logger: logging.Logger + """The logger to use for this client.""" @abc.abstractmethod def __init__(self, logger: typing.Union[logging.Logger, logging.LoggerAdapter]) -> None: @@ -57,12 +55,12 @@ async def join(self) -> None: def run(self) -> None: """Execute this component on an event loop. - Performs the same job as :meth:`start`, but provides additional + Performs the same job as `RunnableClient.start`, but provides additional preparation such as registering OS signal handlers for interrupts, and preparing the initial event loop. This enables the client to be run immediately without having to - set up the :mod:`asyncio` event loop manually first. + set up the `asyncio` event loop manually first. """ loop = asyncio.get_event_loop() diff --git a/hikari/clients/shards.py b/hikari/clients/shards.py index 9317c41272..704d559491 100644 --- a/hikari/clients/shards.py +++ b/hikari/clients/shards.py @@ -16,13 +16,13 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . -"""Provides a facade around :obj:`~hikari.net.shards.Shard`. +"""Provides a facade around `hikari.net.shards.Shard`. This handles parsing and initializing the object from a configuration, as well as restarting it if it disconnects. Additional functions and coroutines are provided to update the presence on the -shard using models defined in :mod:`hikari`. +shard using models defined in `hikari`. """ from __future__ import annotations @@ -56,27 +56,29 @@ class ShardState(enum.IntEnum): """Describes the state of a shard.""" - #: The shard is not running. NOT_RUNNING = 0 + """The shard is not running.""" - #: The shard is undergoing the initial connection handshake. CONNECTING = enum.auto() + """The shard is undergoing the initial connection handshake.""" - #: The initialization handshake has completed. We are waiting for the shard - #: to receive the ``READY`` event. WAITING_FOR_READY = enum.auto() + """The initialization handshake has completed. + + We are waiting for the shard to receive the `READY` event. + """ - #: The shard is ``READY``. READY = enum.auto() + """The shard is `READY`.""" - #: The shard has sent a request to ``RESUME`` and is waiting for a response. RESUMING = enum.auto() + """The shard has sent a request to `RESUME` and is waiting for a response.""" - #: The shard is currently shutting down permanently. STOPPING = enum.auto() + """The shard is currently shutting down permanently.""" - #: The shard has shut down and is no longer connected. STOPPED = enum.auto() + """The shard has shut down and is no longer connected.""" def __str__(self) -> str: return self.name @@ -90,187 +92,107 @@ class ShardClient(runnable.RunnableClient, abc.ABC): @property @abc.abstractmethod def shard_id(self) -> int: - """Shard ID. - - Returns - ------- - :obj:`~int` - The 0-indexed shard ID. - """ + """Shard ID (this is 0-indexed).""" @property @abc.abstractmethod def shard_count(self) -> int: - """Shard count. - - Returns - ------- - :obj:`~int` - The number of shards that make up this bot. - """ + """Count of how many shards make up this bot.""" @property @abc.abstractmethod def status(self) -> guilds.PresenceStatus: - """User status for this shard. - - Returns - ------- - :obj:`~hikari.guilds.PresenceStatus` - The current user status for this shard. - """ + """User status for this shard.""" @property @abc.abstractmethod def activity(self) -> typing.Optional[gateway_entities.Activity]: """Activity for the user status for this shard. - Returns - ------- - :obj:`~hikari.gateway_entities.GatewayActivity`, optional - The current activity for the user on this shard, or :obj:`~None` if - there is no activity. + This will be `None` if there is no activity. """ @property @abc.abstractmethod def idle_since(self) -> typing.Optional[datetime.datetime]: - """Timestamp when the user of this shard appeared to be idle. + """Timestamp of when the user of this shard appeared to be idle. - Returns - ------- - :obj:`~datetime.datetime`, optional - The timestamp when the user of this shard appeared to be idle, or - :obj:`~None` if not applicable. + This will be `None` if not applicable. """ @property @abc.abstractmethod def is_afk(self) -> bool: - """Whether the user is AFK or not. - - Returns - ------- - :obj:`~bool` - :obj:`~True` if the user is AFK, :obj:`~False` otherwise. - """ + """Whether the user is appearing as AFK or not..""" @property @abc.abstractmethod def heartbeat_latency(self) -> float: - """Latency between sending a HEARTBEAT and receiving an ACK. + """Latency between sending a HEARTBEAT and receiving an ACK in seconds. - Returns - ------- - :obj:`~float` - The heartbeat latency in seconds. This will be ``float('nan')`` - until the first heartbeat is performed. + This will be `float("nan")` until the first heartbeat is performed. """ @property @abc.abstractmethod def heartbeat_interval(self) -> float: - """Time period to wait between sending HEARTBEAT payloads. + """Time period to wait between sending HEARTBEAT payloads in seconds. - Returns - ------- - :obj:`~float` - The heartbeat interval in seconds. This will be ``float('nan')`` - until the connection has received a ``HELLO`` payload. + This will be `float("nan")` until the connection has received a `HELLO` + payload. """ @property @abc.abstractmethod def disconnect_count(self) -> int: - """Count of number of times the internal connection has disconnected. - - Returns - ------- - :obj:`~int` - The number of disconnects this shard has performed. - """ + """Count of number of times this shard's connection has disconnected.""" @property @abc.abstractmethod def reconnect_count(self) -> int: - """Count of number of times the internal connection has reconnected. + """Count of number of times this shard's connection has reconnected. This includes RESUME and re-IDENTIFY events. - - Returns - ------- - :obj:`~int` - The number of reconnects this shard has performed. """ @property @abc.abstractmethod def connection_state(self) -> ShardState: - """State of this shard. - - Returns - ------- - :obj:`~ShardState` - The state of this shard. - """ + """State of this shard's connection.""" @property @abc.abstractmethod def is_connected(self) -> bool: - """Whether the shard is connected or not. - - Returns - ------- - :obj:`~bool` - :obj:`~True` if connected; :obj:`~False` otherwise. - """ + """Whether the shard is connected or not.""" @property @abc.abstractmethod def seq(self) -> typing.Optional[int]: """Sequence ID of the shard. - Returns - ------- - :obj:`~int`, optional - The sequence number for the shard. This is the number of payloads - that have been received since an ``IDENTIFY`` was sent. + This is the number of payloads that have been received since the last + `IDENTIFY` was sent. """ @property @abc.abstractmethod def session_id(self) -> typing.Optional[str]: - """Session ID. + """Session ID of the shard connection. - Returns - ------- - :obj:`~str`, optional - The session ID for the shard connection, if there is one. If not, - then :obj:`~None`. + Will be `None` if there is no session. """ @property @abc.abstractmethod def version(self) -> float: - """Version being used for the gateway API. - - Returns - ------- - :obj:`~int` - The API version being used. - """ + """Version being used for the gateway API.""" @property @abc.abstractmethod def intents(self) -> typing.Optional[_intents.Intent]: - """Intent values that this connection is using. - - Returns - ------- - :obj:`~hikari.intents.Intent`, optional - A :obj:`~enum.IntFlag` enum containing each intent that is set. If - intents are not being used at all, then this will return - :obj:`~None` instead. + """Intents that are in use for the shard connection. + + If intents are not being used at all, then this will be `None` instead. """ @abc.abstractmethod @@ -288,20 +210,19 @@ async def update_presence( Any arguments that you do not explicitly provide some value for will not be changed. - Warnings - -------- - This will fail if the shard is not online. + !!! warning + This will fail if the shard is not online. Parameters ---------- - status : :obj:`~hikari.guilds.PresenceStatus` + status : hikari.guilds.PresenceStatus If specified, the new status to set. - activity : :obj:`~hikari.gateway_entities.GatewayActivity`, optional + activity : hikari.gateway_entities.Activity, optional If specified, the new activity to set. - idle_since : :obj:`~datetime.datetime`, optional + idle_since : datetime.datetime, optional If specified, the time to show up as being idle since, or - :obj:`~None` if not applicable. - is_afk : :obj:`~bool` + `None` if not applicable. + is_afk : bool If specified, whether the user should be marked as AFK. """ @@ -311,35 +232,34 @@ class ShardClientImpl(ShardClient): This contains several abstractions to enable usage of the low level gateway network interface with the higher level constructs - in :mod:`hikari`. + in `hikari`. Parameters ---------- - shard_id : :obj:`~int` + shard_id : int The ID of this specific shard. - shard_id : :obj:`~int` + shard_id : int The number of shards that make up this distributed application. - config : :obj:`~hikari.clients.configs.WebsocketConfig` + config : hikari.clients.configs.GatewayConfig The gateway configuration to use to initialize this shard. - raw_event_consumer_impl : :obj:`~hikari.state.raw_event_consumers.RawEventConsumer` + raw_event_consumer_impl : hikari.state.consumers.RawEventConsumer The consumer of a raw event. - url : :obj:`~str` + url : str The URL to connect the gateway to. - dispatcher : :obj:`~hikari.state.event_dispatchers.EventDispatcher`, optional + dispatcher : hikari.state.dispatchers.EventDispatcher, optional The high level event dispatcher to use for dispatching start and stop - events. Set this to :obj:`~None` to disable that functionality (useful if + events. Set this to `None` to disable that functionality (useful if you use a gateway manager to orchestrate multiple shards instead and - provide this functionality there). Defaults to :obj:`~None` if + provide this functionality there). Defaults to `None` if unspecified. - Notes - ----- - Generally, you want to use - :obj:`~hikari.clients.gateway_managers.GatewayManager` rather than this class - directly, as that will handle sharding where enabled and applicable, and - provides a few more bits and pieces that may be useful such as state - management and event dispatcher integration. and If you want to customize - this, you can subclass it and simply override anything you want. + !!! note + Generally, you want to use + `hikari.clients.bot_base.BotBase` rather than this class + directly, as that will handle sharding where enabled and applicable, + and provides a few more bits and pieces that may be useful such as state + management and event dispatcher integration. and If you want to customize + this, you can subclass it and simply override anything you want. """ __slots__ = ( @@ -467,7 +387,7 @@ def intents(self) -> typing.Optional[_intents.Intent]: async def start(self): """Connect to the gateway on this shard and keep the connection alive. - This will wait for the shard to dispatch a ``READY`` event, and + This will wait for the shard to dispatch a `READY` event, and then return. """ if self._shard_state not in (ShardState.NOT_RUNNING, ShardState.STOPPED): diff --git a/hikari/clients/test.py b/hikari/clients/test.py index 26bccd5e63..6c65fbd9fd 100644 --- a/hikari/clients/test.py +++ b/hikari/clients/test.py @@ -68,7 +68,7 @@ def _supports_color(): @click.option("--verify-ssl", default=True, type=click.BOOL, help="Enable or disable SSL verification.") @click.option("--version", default=6, type=click.IntRange(min=6), help="Version of the gateway to use.") def run_gateway(compression, color, debug, intents, logger, shards, token, verify_ssl, version) -> None: - """:mod:`click` command line client for running a test gateway connection. + """`click` command line client for running a test gateway connection. This is provided for internal testing purposes for benchmarking API stability, etc. diff --git a/hikari/colors.py b/hikari/colors.py index 1dbf659b09..313e47cc22 100644 --- a/hikari/colors.py +++ b/hikari/colors.py @@ -16,13 +16,7 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . -"""Model that represents a common RGB color and provides simple conversions to other common color systems. - -.. inheritance-diagram:: - builtins.int - hikari.colors - :parts: 1 -""" +"""Model that represents a common RGB color and provides simple conversions to other common color systems.""" from __future__ import annotations __all__ = ["Color", "ColorCompatibleT"] @@ -38,8 +32,8 @@ class Color(int, typing.SupportsInt): This value is immutable. - This is a specialization of :obj:`~int` which provides alternative overrides for common methods and color system - conversions. + This is a specialization of `int` which provides alternative overrides for + common methods and color system conversions. This currently supports: @@ -53,7 +47,6 @@ class Color(int, typing.SupportsInt): Examples -------- Examples of conversions to given formats include: - .. code-block::python >>> c = Color(0xFF051A) Color(r=0xff, g=0x5, b=0x1a) @@ -72,7 +65,6 @@ class Color(int, typing.SupportsInt): Alternatively, if you have an arbitrary input in one of the above formats that you wish to become a color, you can use the get-attribute operator on the class itself to automatically attempt to resolve the color: - .. code-block::python >>> Color[0xFF051A] Color(r=0xff, g=0x5, b=0x1a) @@ -92,7 +84,6 @@ class Color(int, typing.SupportsInt): Color(r=0xff, g=0x5, b=0x1a) Examples of initialization of Color objects from given formats include: - .. code-block::python >>> c = Color(16712986) Color(r=0xff, g=0x5, b=0x1a) @@ -146,11 +137,11 @@ def rgb_float(self) -> typing.Tuple[float, float, float]: def hex_code(self) -> str: """Six-digit hexadecimal color code for this Color. - This is prepended with a ``#`` symbol, and will be in upper case. + This is prepended with a `#` symbol, and will be in upper case. - Example - ------- - ``#1A2B3C`` + Examples + -------- + `#1A2B3C` """ return "#" + self.raw_hex_code @@ -158,9 +149,9 @@ def hex_code(self) -> str: def raw_hex_code(self) -> str: """Raw hex code. - Example - ------- - ``1A2B3C`` + Examples + -------- + `1A2B3C` """ components = self.rgb return "".join(hex(c)[2:].zfill(2) for c in components).upper() @@ -168,33 +159,33 @@ def raw_hex_code(self) -> str: # Ignore docstring not starting in an imperative mood @property def is_web_safe(self) -> bool: # noqa: D401 - """:obj:`~True` if the color is web safe, :obj:`~False` otherwise.""" + """`True` if the color is web safe, `False` otherwise.""" hex_code = self.raw_hex_code return all(_all_same(*c) for c in (hex_code[:2], hex_code[2:4], hex_code[4:])) @classmethod def from_rgb(cls, red: int, green: int, blue: int) -> Color: - """Convert the given RGB to a :obj:`~Color` object. + """Convert the given RGB to a `Color` object. Each channel must be withing the range [0, 255] (0x0, 0xFF). Parameters ---------- - red : :obj:`~int` + red : int Red channel. - green : :obj:`~int` + green : int Green channel. - blue : :obj:`~int` + blue : int Blue channel. Returns ------- - :obj:`~Color` + Color A Color object. Raises ------ - :obj:`~ValueError` + ValueError If red, green, or blue are outside the range [0x0, 0xFF]. """ assertions.assert_in_range(red, 0, 0xFF, "red") @@ -205,28 +196,28 @@ def from_rgb(cls, red: int, green: int, blue: int) -> Color: @classmethod def from_rgb_float(cls, red_f: float, green_f: float, blue_f: float) -> Color: - """Convert the given RGB to a :obj:`~Color` object. + """Convert the given RGB to a `Color` object. The colorspace represented values have to be within the range [0, 1]. Parameters ---------- - red_f : :obj:`~float` + red_f : float Red channel. - green_f : :obj:`~float` + green_f : float Green channel. - blue_f : :obj:`~float` + blue_f : float Blue channel. Returns ------- - :obj:`~Color` + Color A Color object. Raises ------ - :obj:`~ValueError` + ValueError If red, green or blue are outside the range [0, 1]. """ assertions.assert_in_range(red_f, 0, 1, "red") @@ -237,26 +228,26 @@ def from_rgb_float(cls, red_f: float, green_f: float, blue_f: float) -> Color: @classmethod def from_hex_code(cls, hex_code: str) -> Color: - """Convert the given hexadecimal color code to a :obj:`~Color`. + """Convert the given hexadecimal color code to a `Color`. The inputs may be of the following format (case insensitive): - ``1a2``, ``#1a2``, ``0x1a2`` (for websafe colors), or - ``1a2b3c``, ``#1a2b3c``, ``0x1a2b3c`` (for regular 3-byte color-codes). + `1a2`, `#1a2`, `0x1a2` (for websafe colors), or + `1a2b3c`, `#1a2b3c`, `0x1a2b3c` (for regular 3-byte color-codes). Parameters ---------- - hex_code : :obj:`~str` + hex_code : str A hexadecimal color code to parse. Returns ------- - :obj:`~Color` + Color A corresponding Color object. Raises ------ - :obj:`~ValueError` - If ``hex_code`` is not a hexadecimal or is a inalid length. + ValueError + If `hex_code` is not a hexadecimal or is a invalid length. """ if hex_code.startswith("#"): hex_code = hex_code[1:] @@ -279,16 +270,16 @@ def from_hex_code(cls, hex_code: str) -> Color: @classmethod def from_int(cls, i: typing.SupportsInt) -> Color: - """Convert the given :obj:`~typing.SupportsInt` to a :obj:`~Color`. + """Convert the given `typing.SupportsInt` to a `Color`. Parameters ---------- - i : :obj:`~typing.SupportsInt` + i : typing.SupportsInt The raw color integer. Returns ------- - :obj:`~Color` + Color The Color object. """ return cls(i) @@ -296,23 +287,23 @@ def from_int(cls, i: typing.SupportsInt) -> Color: # Partially chose to override these as the docstrings contain typos according to Sphinx. @classmethod def from_bytes(cls, bytes_: typing.Sequence[int], byteorder: str, *, signed: bool = True) -> Color: - """Convert the bytes to a :obj:`~Color`. + """Convert the bytes to a `Color`. Parameters ---------- - bytes_ : :obj:`~typing.Iterable` [ :obj:`~int` ] - A iterable of :obj:`~int` byte values. + bytes_ : typing.Iterable [ int ] + A iterable of int byte values. - byteorder : :obj:`~str` + byteorder : str The endianess of the value represented by the bytes. - Can be ``"big"`` endian or ``"little"`` endian. + Can be `"big"` endian or `"little"` endian. - signed : :obj:`~bool` + signed : bool Whether the value is signed or unsigned. Returns ------- - :obj:`~Color` + Color The Color object. """ return Color(int.from_bytes(bytes_, byteorder, signed=signed)) @@ -322,19 +313,19 @@ def to_bytes(self, length: int, byteorder: str, *, signed: bool = True) -> bytes Parameters ---------- - length : :obj:`~int` - The number of bytes to produce. Should be around ``3``, but not less. + length : int + The number of bytes to produce. Should be around `3`, but not less. - byteorder : :obj:`~str` + byteorder : str The endianess of the value represented by the bytes. - Can be ``"big"`` endian or ``"little"`` endian. + Can be `"big"` endian or `"little"` endian. - signed : :obj:`~bool` + signed : bool Whether the value is signed or unsigned. Returns ------- - :obj:`~bytes` + bytes The bytes representation of the Color. """ return int(self).to_bytes(length, byteorder, signed=signed) @@ -377,7 +368,6 @@ def _all_same(first, *rest): return True -#: Any type that can be converted into a color object. ColorCompatibleT = typing.Union[ Color, typing.SupportsInt, @@ -387,3 +377,4 @@ def _all_same(first, *rest): typing.Sequence[typing.SupportsFloat], str, ] +"""Any type that can be converted into a color object.""" diff --git a/hikari/colours.py b/hikari/colours.py index 6e1e19bace..fc56d0f23f 100644 --- a/hikari/colours.py +++ b/hikari/colours.py @@ -16,7 +16,7 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . -"""Alias for the :mod:`hikari.colors` module.""" +"""Alias for the `hikari.colors` module.""" __all__ = ["Colour", "ColourCompatibleT"] from hikari.colors import Color as Colour diff --git a/hikari/embeds.py b/hikari/embeds.py index 451f09eedf..1d95892c5b 100644 --- a/hikari/embeds.py +++ b/hikari/embeds.py @@ -44,30 +44,23 @@ class EmbedFooter(bases.HikariEntity, marshaller.Deserializable, marshaller.Serializable): """Represents an embed footer.""" - #: The footer text. - #: - #: :type: :obj:`~str` text: str = marshaller.attrib(deserializer=str, serializer=str) + """The footer text.""" - #: The URL of the footer icon. - #: - #: :type: :obj:`~str`, optional icon_url: typing.Optional[str] = marshaller.attrib( deserializer=str, serializer=str, if_undefined=None, default=None ) + """The URL of the footer icon.""" - #: The proxied URL of the footer icon. - #: - #: Note - #: ---- - #: This field cannot be set by bots or webhooks while sending an embed and - #: will be ignored during serialization. - #: - #: - #: :type: :obj:`~str`, optional proxy_icon_url: typing.Optional[str] = marshaller.attrib( deserializer=str, transient=True, if_undefined=None, default=None ) + """The proxied URL of the footer icon. + + !!! note + This field cannot be set by bots or webhooks while sending an embed and + will be ignored during serialization. + """ @marshaller.marshallable() @@ -75,45 +68,34 @@ class EmbedFooter(bases.HikariEntity, marshaller.Deserializable, marshaller.Seri class EmbedImage(bases.HikariEntity, marshaller.Deserializable, marshaller.Serializable): """Represents an embed image.""" - #: The URL of the image. - #: - #: :type: :obj:`~str`, optional url: typing.Optional[str] = marshaller.attrib(deserializer=str, serializer=str, if_undefined=None, default=None) + """The URL of the image.""" - #: The proxied URL of the image. - #: - #: Note - #: ---- - #: This field cannot be set by bots or webhooks while sending an embed and - #: will be ignored during serialization. - #: - #: - #: :type: :obj:`~str`, optional proxy_url: typing.Optional[str] = marshaller.attrib( deserializer=str, transient=True, if_undefined=None, default=None ) + """The proxied URL of the image. + + !!! note + This field cannot be set by bots or webhooks while sending an embed and + will be ignored during serialization. + """ - #: The height of the image. - #: - #: Note - #: ---- - #: This field cannot be set by bots or webhooks while sending an embed and - #: will be ignored during serialization. - #: - #: - #: :type: :obj:`~int`, optional height: typing.Optional[int] = marshaller.attrib(deserializer=int, transient=True, if_undefined=None, default=None) + """The height of the image. + + !!! note + This field cannot be set by bots or webhooks while sending an embed and + will be ignored during serialization. + """ - #: The width of the image. - #: - #: Note - #: ---- - #: This field cannot be set by bots or webhooks while sending an embed and - #: will be ignored during serialization. - #: - #: - #: :type: :obj:`~int`, optional width: typing.Optional[int] = marshaller.attrib(deserializer=int, transient=True, if_undefined=None, default=None) + """The width of the image. + + !!! note + This field cannot be set by bots or webhooks while sending an embed and + will be ignored during serialization. + """ @marshaller.marshallable() @@ -121,45 +103,34 @@ class EmbedImage(bases.HikariEntity, marshaller.Deserializable, marshaller.Seria class EmbedThumbnail(bases.HikariEntity, marshaller.Deserializable, marshaller.Serializable): """Represents an embed thumbnail.""" - #: The URL of the thumbnail. - #: - #: :type: :obj:`~str`, optional url: typing.Optional[str] = marshaller.attrib(deserializer=str, serializer=str, if_undefined=None, default=None) + """The URL of the thumbnail.""" - #: The proxied URL of the thumbnail. - #: - #: Note - #: ---- - #: This field cannot be set by bots or webhooks while sending an embed and - #: will be ignored during serialization. - #: - #: - #: :type: :obj:`~str`, optional proxy_url: typing.Optional[str] = marshaller.attrib( deserializer=str, transient=True, if_undefined=None, default=None ) + """The proxied URL of the thumbnail. + + !!! note + This field cannot be set by bots or webhooks while sending an embed and + will be ignored during serialization. + """ - #: The height of the thumbnail. - #: - #: Note - #: ---- - #: This field cannot be set by bots or webhooks while sending an embed and - #: will be ignored during serialization. - #: - #: - #: :type: :obj:`~int`, optional height: typing.Optional[int] = marshaller.attrib(deserializer=int, transient=True, if_undefined=None, default=None) + """The height of the thumbnail. + + !!! note + This field cannot be set by bots or webhooks while sending an embed and + will be ignored during serialization. + """ - #: The width of the thumbnail. - #: - #: Note - #: ---- - #: This field cannot be set by bots or webhooks while sending an embed and - #: will be ignored during serialization. - #: - #: - #: :type: :obj:`~int`, optional width: typing.Optional[int] = marshaller.attrib(deserializer=int, transient=True, if_undefined=None, default=None) + """The width of the thumbnail. + + !!! note + This field cannot be set by bots or webhooks while sending an embed and + will be ignored during serialization. + """ @marshaller.marshallable() @@ -167,26 +138,20 @@ class EmbedThumbnail(bases.HikariEntity, marshaller.Deserializable, marshaller.S class EmbedVideo(bases.HikariEntity, marshaller.Deserializable): """Represents an embed video. - Note - ---- - This embed attached object cannot be sent by bots or webhooks while sending - an embed and therefore shouldn't be initiated like the other embed objects. + !!! note + This embed attached object cannot be sent by bots or webhooks while + sending an embed and therefore shouldn't be initiated like the other + embed objects. """ - #: The URL of the video. - #: - #: :type: :obj:`~str`, optional url: typing.Optional[str] = marshaller.attrib(deserializer=str, if_undefined=None, default=None) + """The URL of the video.""" - #: The height of the video. - #: - #: :type: :obj:`~int`, optional height: typing.Optional[int] = marshaller.attrib(deserializer=int, if_undefined=None, default=None) + """The height of the video.""" - #: The width of the video. - #: - #: :type: :obj:`~int`, optional width: typing.Optional[int] = marshaller.attrib(deserializer=int, if_undefined=None, default=None) + """The width of the video.""" @marshaller.marshallable() @@ -194,21 +159,17 @@ class EmbedVideo(bases.HikariEntity, marshaller.Deserializable): class EmbedProvider(bases.HikariEntity, marshaller.Deserializable): """Represents an embed provider. - Note - ---- - This embed attached object cannot be sent by bots or webhooks while sending - an embed and therefore shouldn't be initiated like the other embed objects. + !!! note + This embed attached object cannot be sent by bots or webhooks while + sending an embed and therefore shouldn't be initiated like the other + embed objects. """ - #: The name of the provider. - #: - #: :type: :obj:`~str`, optional name: typing.Optional[str] = marshaller.attrib(deserializer=str, if_undefined=None, default=None) + """The name of the provider.""" - #: The URL of the provider. - #: - #: :type: :obj:`~str`, optional url: typing.Optional[str] = marshaller.attrib(deserializer=str, if_undefined=None, if_none=None, default=None) + """The URL of the provider.""" @marshaller.marshallable() @@ -216,35 +177,26 @@ class EmbedProvider(bases.HikariEntity, marshaller.Deserializable): class EmbedAuthor(bases.HikariEntity, marshaller.Deserializable, marshaller.Serializable): """Represents an embed author.""" - #: The name of the author. - #: - #: :type: :obj:`~str`, optional name: typing.Optional[str] = marshaller.attrib(deserializer=str, serializer=str, if_undefined=None, default=None) + """The name of the author.""" - #: The URL of the author. - #: - #: :type: :obj:`~str`, optional url: typing.Optional[str] = marshaller.attrib(deserializer=str, serializer=str, if_undefined=None, default=None) + """The URL of the author.""" - #: The URL of the author icon. - #: - #: :type: :obj:`~str`, optional icon_url: typing.Optional[str] = marshaller.attrib( deserializer=str, serializer=str, if_undefined=None, default=None ) + """The URL of the author icon.""" - #: The proxied URL of the author icon. - #: - #: Note - #: ---- - #: This field cannot be set by bots or webhooks while sending an embed and - #: will be ignored during serialization. - #: - #: - #: :type: :obj:`~str`, optional proxy_icon_url: typing.Optional[str] = marshaller.attrib( deserializer=str, transient=True, if_undefined=None, default=None ) + """The proxied URL of the author icon. + + !!! note + This field cannot be set by bots or webhooks while sending an embed and + will be ignored during serialization. + """ @marshaller.marshallable() @@ -252,22 +204,16 @@ class EmbedAuthor(bases.HikariEntity, marshaller.Deserializable, marshaller.Seri class EmbedField(bases.HikariEntity, marshaller.Deserializable, marshaller.Serializable): """Represents a field in a embed.""" - #: The name of the field. - #: - #: :type: :obj:`~str` name: str = marshaller.attrib(deserializer=str, serializer=str) + """The name of the field.""" - #: The value of the field. - #: - #: :type: :obj:`~str` value: str = marshaller.attrib(deserializer=str, serializer=str) + """The value of the field.""" - #: Whether the field should display inline. Defaults to :obj:`~False`. - #: - #: :type: :obj:`~bool` is_inline: bool = marshaller.attrib( raw_name="inline", deserializer=bool, serializer=bool, if_undefined=False, default=False ) + """Whether the field should display inline. Defaults to `False`.""" @marshaller.marshallable() @@ -275,97 +221,74 @@ class EmbedField(bases.HikariEntity, marshaller.Deserializable, marshaller.Seria class Embed(bases.HikariEntity, marshaller.Deserializable, marshaller.Serializable): """Represents an embed.""" - #: The title of the embed. - #: - #: :type: :obj:`~str`, optional title: typing.Optional[str] = marshaller.attrib(deserializer=str, serializer=str, if_undefined=None, default=None) + """The title of the embed.""" - #: The description of the embed. - #: - #: :type: :obj:`~str`, optional description: typing.Optional[str] = marshaller.attrib( deserializer=str, serializer=str, if_undefined=None, default=None ) + """The description of the embed.""" - #: The URL of the embed. - #: - #: :type: :obj:`~str`, optional url: typing.Optional[str] = marshaller.attrib(deserializer=str, serializer=str, if_undefined=None, default=None) + """The URL of the embed.""" - #: The timestamp of the embed. - #: - #: :type: :obj:`~datetime.datetime`, optional timestamp: typing.Optional[datetime.datetime] = marshaller.attrib( deserializer=conversions.parse_iso_8601_ts, serializer=lambda timestamp: timestamp.replace(tzinfo=datetime.timezone.utc).isoformat(), if_undefined=None, default=None, ) + """The timestamp of the embed.""" color: typing.Optional[colors.Color] = marshaller.attrib( deserializer=colors.Color, serializer=int, if_undefined=None, default=None ) + """The colour of this embed's sidebar.""" - #: The footer of the embed. - #: - #: :type: :obj:`~EmbedFooter`, optional footer: typing.Optional[EmbedFooter] = marshaller.attrib( deserializer=EmbedFooter.deserialize, serializer=EmbedFooter.serialize, if_undefined=None, default=None ) + """The footer of the embed.""" - #: The image of the embed. - #: - #: :type: :obj:`~EmbedImage`, optional image: typing.Optional[EmbedImage] = marshaller.attrib( deserializer=EmbedImage.deserialize, serializer=EmbedImage.serialize, if_undefined=None, default=None ) + """The image of the embed.""" - #: The thumbnail of the embed. - #: - #: :type: :obj:`~EmbedThumbnail`, optional thumbnail: typing.Optional[EmbedThumbnail] = marshaller.attrib( deserializer=EmbedThumbnail.deserialize, serializer=EmbedThumbnail.serialize, if_undefined=None, default=None ) + """The thumbnail of the embed.""" - #: The video of the embed. - #: - #: Note - #: ---- - #: This field cannot be set by bots or webhooks while sending an embed and - #: will be ignored during serialization. - #: - #: - #: :type: :obj:`~EmbedVideo`, optional video: typing.Optional[EmbedVideo] = marshaller.attrib( deserializer=EmbedVideo.deserialize, transient=True, if_undefined=None, default=None, ) + """The video of the embed. + + !!! note + This field cannot be set by bots or webhooks while sending an embed and + will be ignored during serialization. + """ - #: The provider of the embed. - #: - #: Note - #: ---- - #: This field cannot be set by bots or webhooks while sending an embed and - #: will be ignored during serialization. - #: - #: - #: :type: :obj:`~EmbedProvider`, optional provider: typing.Optional[EmbedProvider] = marshaller.attrib( deserializer=EmbedProvider.deserialize, transient=True, if_undefined=None, default=None ) + """The provider of the embed. + + !!! note + This field cannot be set by bots or webhooks while sending an embed and + will be ignored during serialization. + """ - #: The author of the embed. - #: - #: :type: :obj:`~EmbedAuthor`, optional author: typing.Optional[EmbedAuthor] = marshaller.attrib( deserializer=EmbedAuthor.deserialize, serializer=EmbedAuthor.serialize, if_undefined=None, default=None ) + """The author of the embed.""" - #: The fields of the embed. - #: - #: :type: :obj:`~typing.Sequence` [ :obj:`~EmbedField` ] fields: typing.Sequence[EmbedField] = marshaller.attrib( deserializer=lambda fields: [EmbedField.deserialize(f) for f in fields], serializer=lambda fields: [f.serialize() for f in fields], if_undefined=list, factory=list, ) + """The fields of the embed.""" diff --git a/hikari/emojis.py b/hikari/emojis.py index 2984c17396..962bda87d2 100644 --- a/hikari/emojis.py +++ b/hikari/emojis.py @@ -39,10 +39,8 @@ class Emoji(bases.HikariEntity, marshaller.Deserializable): class UnicodeEmoji(Emoji): """Represents a unicode emoji.""" - #: The codepoints that form the emoji. - #: - #: :type: :obj:`~str` name: str = marshaller.attrib(deserializer=str) + """The codepoints that form the emoji.""" @property def url_name(self) -> str: @@ -60,20 +58,17 @@ def mention(self) -> str: class UnknownEmoji(Emoji, bases.UniqueEntity): """Represents a unknown emoji.""" - #: The name of the emoji. - #: - #: :type: :obj:`~str`, optional name: typing.Optional[str] = marshaller.attrib(deserializer=str, if_none=None) + """The name of the emoji.""" - #: Whether the emoji is animated. - #: - #: Will be :obj:`None` when received in Message Reaction Remove and Message - #: Reaction Remove Emoji events. - #: - #: :type: :obj:`~bool`, optional is_animated: typing.Optional[bool] = marshaller.attrib( raw_name="animated", deserializer=bool, if_undefined=False, if_none=None, default=False ) + """Whether the emoji is animated. + + Will be `None` when received in Message Reaction Remove and Message + Reaction Remove Emoji events. + """ @property def url_name(self) -> str: @@ -86,44 +81,38 @@ def url_name(self) -> str: class GuildEmoji(UnknownEmoji): """Represents a guild emoji.""" - #: The whitelisted role IDs to use this emoji. - #: - #: :type: :obj:`~typing.Set` [ :obj:`~hikari.bases.Snowflake` ] role_ids: typing.Set[bases.Snowflake] = marshaller.attrib( raw_name="roles", deserializer=lambda roles: {bases.Snowflake.deserialize(r) for r in roles}, if_undefined=set, factory=set, ) + """The IDs of the roles that are whitelisted to use this emoji. + + If this is empty than any user can use this emoji regardless of their roles. + """ - #: The user that created the emoji. - #: - #: Note - #: ---- - #: This will be :obj:`~None` if you are missing the ``MANAGE_EMOJIS`` - #: permission in the server the emoji is from. - #: - #: - #: :type: :obj:`~hikari.users.User`, optional user: typing.Optional[users.User] = marshaller.attrib( deserializer=users.User.deserialize, if_none=None, if_undefined=None, default=None ) + """The user that created the emoji. + + !!! note + This will be `None` if you are missing the `MANAGE_EMOJIS` + permission in the server the emoji is from. + """ - #: Whether this emoji must be wrapped in colons. - #: - #: :type: :obj:`~bool` is_colons_required: bool = marshaller.attrib(raw_name="require_colons", deserializer=bool) + """Whether this emoji must be wrapped in colons.""" - #: Whether the emoji is managed by an integration. - #: - #: :type: :obj:`~bool` is_managed: bool = marshaller.attrib(raw_name="managed", deserializer=bool) + """Whether the emoji is managed by an integration.""" - #: Whether this emoji can currently be used, may be :obj:`False` due to - #: a loss of Sever Boosts on the emoji's guild. - #: - #: :type: :obj:`~bool` is_available: bool = marshaller.attrib(raw_name="available", deserializer=bool) + """Whether this emoji can currently be used. + + May be `False` due to a loss of Sever Boosts on the emoji's guild. + """ @property def mention(self) -> str: diff --git a/hikari/errors.py b/hikari/errors.py index 184479c0ee..bf9db63f17 100644 --- a/hikari/errors.py +++ b/hikari/errors.py @@ -22,6 +22,7 @@ "HikariWarning", "NotFoundHTTPError", "UnauthorizedHTTPError", + "ForbiddenHTTPError", "BadRequestHTTPError", "ClientHTTPError", "ServerHTTPError", @@ -49,9 +50,8 @@ class HikariError(RuntimeError): Any exceptions should derive from this. - Note - ---- - You should never initialize this exception directly. + !!! note + You should never initialize this exception directly. """ __slots__ = () @@ -75,14 +75,12 @@ class GatewayError(HikariError): Parameters ---------- - reason : :obj:`~str` + reason : st A string explaining the issue. """ - #: A string to explain the issue. - #: - #: :type: :obj:`~str` reason: str + """A string to explain the issue.""" def __init__(self, reason: str) -> None: super().__init__() @@ -97,7 +95,7 @@ class GatewayClientClosedError(GatewayError): Parameters ---------- - reason : :obj:`~str` + reason : str A string explaining the issue. """ @@ -110,7 +108,7 @@ class GatewayClientDisconnectedError(GatewayError): Parameters ---------- - reason : :obj:`~str` + reason : str A string explaining the issue. """ @@ -123,9 +121,9 @@ class GatewayServerClosedConnectionError(GatewayError): Parameters ---------- - close_code : :obj:`~hikari.net.codes.GatewayCloseCode`, :obj:`~int`, optional + close_code : typing.Union [ hikari.net.codes.GatewayCloseCode, int ], optional The close code provided by the server, if there was one. - reason : :obj:`~str`, optional + reason : str, optional A string explaining the issue. """ @@ -163,17 +161,16 @@ class GatewayInvalidSessionError(GatewayServerClosedConnectionError): Parameters ---------- - can_resume : :obj:`~bool` - :obj:`~True` if the connection will be able to RESUME next time it starts - rather than re-IDENTIFYing, or :obj:`~False` if you need to IDENTIFY + can_resume : bool + `True` if the connection will be able to RESUME next time it starts + rather than re-IDENTIFYing, or `False` if you need to IDENTIFY again instead. """ - #: :obj:`~True` if the next reconnection can be RESUMED. :obj:`~False` if it - #: has to be coordinated by re-IDENFITYing. - #: - #: :type: :obj:`~bool` can_resume: bool + """`True` if the next reconnection can be RESUMED, + `False` if it has to be coordinated by re-IDENFITYing. + """ def __init__(self, can_resume: bool) -> None: self.can_resume = can_resume @@ -220,14 +217,12 @@ class HTTPError(HikariError): Parameters ---------- - reason : :obj:`~str` + reason : str A meaningful explanation of the problem. """ - #: A meaningful explanation of the problem. - #: - #: :type: :obj:`~str` reason: str + """A meaningful explanation of the problem.""" def __init__(self, reason: str) -> None: super().__init__() @@ -242,37 +237,27 @@ class CodedHTTPError(HTTPError): Parameters ---------- - status : :obj:`~int` or :obj:`~hikari.net.codes.HTTPStatusCode` + status : int or hikari.net.codes.HTTPStatusCode The HTTP status code that was returned by the server. - route : :obj:`~hikari.net.routes.CompiledRoute` + route : hikari.net.routes.CompiledRoute The HTTP route that was being invoked when this exception occurred. - message : :obj:`~str`, optional + message : str, optional An optional message if provided in the response payload. - json_code : :obj:`~hikari.net.codes.JSONErrorCode`, :obj:`~int`, optional + json_code : hikari.net.codes.JSONErrorCode, int, optional An optional error code the server provided us. """ - #: The HTTP status code that was returned by the server. - #: - #: :type: :obj:`~int` or :obj:`~hikari.net.codes.HTTPStatusCode` status: typing.Union[int, codes.HTTPStatusCode] + """The HTTP status code that was returned by the server.""" - #: The HTTP route that was being invoked when this exception occurred. - #: - #: :type: :obj:`~hikari.net.routes.CompiledRoute` route: routes.CompiledRoute + """The HTTP route that was being invoked when this exception occurred.""" - #: An optional contextual message the server provided us with in the - #: response body. - # - #: :type: :obj:`~str`, optional message: typing.Optional[str] + """An optional contextual message the server provided us with in the response body.""" - #: An optional contextual error code the server provided us with in the - #: response body. - # - #: :type: :obj:`~hikari.net.codes.JSONErrorCode` or :obj:`~int`, optional - json_code: typing.Optional[typing.Union[codes.JSONErrorCode, int]] + json_code: typing.Union[codes.JSONErrorCode, int, None] + """An optional contextual error code the server provided us with in the response body.""" def __init__( self, @@ -311,7 +296,7 @@ class ClientHTTPError(CodedHTTPError): class BadRequestHTTPError(CodedHTTPError): - """A specific case of :obj:`~CodedHTTPError`. + """A specific case of CodedHTTPError. This can occur hat occurs when you send Discord information in an unexpected format, miss required information out, or give bad values for stuff. @@ -321,11 +306,11 @@ class BadRequestHTTPError(CodedHTTPError): Parameters ---------- - route : :obj:`~hikari.net.routes.CompiledRoute` + route : hikari.net.routes.CompiledRoute The HTTP route that was being invoked when this exception occurred. - message : :obj:`~str`, optional + message : str, optional An optional message if provided in the response payload. - json_code : :obj:`~hikari.net.codes.JSONErrorCode`, :obj:`~int`, optional + json_code : hikari.net.codes.JSONErrorCode, int, optional An optional error code the server provided us. """ @@ -339,7 +324,7 @@ def __init__( class UnauthorizedHTTPError(ClientHTTPError): - """A specific case of :obj:`~ClientHTTPError`. + """A specific case of ClientHTTPError. This occurs when you have invalid authorization details to access the given resource. @@ -348,11 +333,11 @@ class UnauthorizedHTTPError(ClientHTTPError): Parameters ---------- - route : :obj:`~hikari.net.routes.CompiledRoute` + route : hikari.net.routes.CompiledRoute The HTTP route that was being invoked when this exception occurred. - message : :obj:`~str`, optional + message : str, optional An optional message if provided in the response payload. - json_code : :obj:`~hikari.net.codes.JSONErrorCode`, :obj:`~int`, optional + json_code : hikari.net.codes.JSONErrorCode, int, optional An optional error code the server provided us. """ @@ -366,7 +351,7 @@ def __init__( class ForbiddenHTTPError(ClientHTTPError): - """A specific case of :obj:`~ClientHTTPError`. + """A specific case of ClientHTTPError. This occurs when you are missing permissions, or are using an endpoint that your account is not allowed to see without being whitelisted. @@ -375,11 +360,11 @@ class ForbiddenHTTPError(ClientHTTPError): Parameters ---------- - route : :obj:`~hikari.net.routes.CompiledRoute` + route : hikari.net.routes.CompiledRoute The HTTP route that was being invoked when this exception occurred. - message : :obj:`~str`, optional + message : str, optional An optional message if provided in the response payload. - json_code : :obj:`~hikari.net.codes.JSONErrorCode`, :obj:`~int`, optional + json_code : hikari.net.codes.JSONErrorCode, int, optional An optional error code the server provided us. """ @@ -393,7 +378,7 @@ def __init__( class NotFoundHTTPError(ClientHTTPError): - """A specific case of :obj:`~ClientHTTPError`. + """A specific case of ClientHTTPError. This occurs when you try to refer to something that doesn't exist on Discord. This might be referring to a user ID, channel ID, guild ID, etc that does @@ -402,11 +387,11 @@ class NotFoundHTTPError(ClientHTTPError): Parameters ---------- - route : :obj:`~hikari.net.routes.CompiledRoute` + route : hikari.net.routes.CompiledRoute The HTTP route that was being invoked when this exception occurred. - message : :obj:`~str`, optional + message : str, optional An optional message if provided in the response payload. - json_code : :obj:`~hikari.net.codes.JSONErrorCode`, :obj:`~int`, optional + json_code : hikari.net.codes.JSONErrorCode, int, optional An optional error code the server provided us. """ diff --git a/hikari/events.py b/hikari/events.py index 7ac38d4be2..8e27aa3a1f 100644 --- a/hikari/events.py +++ b/hikari/events.py @@ -29,6 +29,7 @@ "StoppedEvent", "ReadyEvent", "ResumedEvent", + "BaseChannelEvent", "ChannelCreateEvent", "ChannelUpdateEvent", "ChannelDeleteEvent", @@ -37,6 +38,7 @@ "GuildUpdateEvent", "GuildLeaveEvent", "GuildUnavailableEvent", + "BaseGuildBanEvent", "GuildBanAddEvent", "GuildBanRemoveEvent", "GuildEmojisUpdateEvent", @@ -108,12 +110,12 @@ def get_required_intents_for(event_type: typing.Type[HikariEvent]) -> typing.Col Parameters ---------- - event_type : :obj:`~typing.Type` [ :obj:`~HikariEvent` ] + event_type : typing.Type [ HikariEvent ] The event type to get required intents for. Returns ------- - :obj:`~typing.Collection` [ :obj:`~hikari.intents.Intent` ] + typing.Collection [ hikari.intents.Intent ] Collection of acceptable subset combinations of intent needed to be able to receive the given event type. """ @@ -127,10 +129,10 @@ def requires_intents( Parameters ---------- - first : :obj:`~hikari.intents.Intent` + first : hikari.intents.Intent First combination of intents that are acceptable in order to receive the decorated event type. - *rest : :obj:`~hikari.intents.Intent` + *rest : hikari.intents.Intent Zero or more additional combinations of intents to require for this event to be subscribed to. @@ -148,20 +150,14 @@ def decorator(cls: typing.Type[_HikariEventT]) -> typing.Type[_HikariEventT]: class ExceptionEvent(HikariEvent): """Descriptor for an exception thrown while processing an event.""" - #: The exception that was raised. - #: - #: :type: :obj:`~Exception` exception: Exception + """The exception that was raised.""" - #: The event that was being invoked when the exception occurred. - #: - #: :type: :obj:`~HikariEvent` event: HikariEvent + """The event that was being invoked when the exception occurred.""" - #: The event that was being invoked when the exception occurred. - #: - #: :type: coroutine function ( :obj:`~HikariEvent` ) -> :obj:`~None` callback: typing.Callable[[HikariEvent], typing.Awaitable[None]] + """The event that was being invoked when the exception occurred.""" # Synthetic event, is not deserialized @@ -192,30 +188,24 @@ class StoppedEvent(HikariEvent): class ConnectedEvent(HikariEvent, marshaller.Deserializable): """Event invoked each time a shard connects.""" - #: The shard that connected. - #: - #: :type: :obj:`~hikari.clients.shard_clients.ShardClient` shard: shards.ShardClient + """The shard that connected.""" @attr.s(slots=True, kw_only=True, auto_attribs=True) class DisconnectedEvent(HikariEvent, marshaller.Deserializable): """Event invoked each time a shard disconnects.""" - #: The shard that disconnected. - #: - #: :type: :obj:`~hikari.clients.shard_clients.ShardClient` shard: shards.ShardClient + """The shard that disconnected.""" @attr.s(slots=True, kw_only=True, auto_attribs=True) class ResumedEvent(HikariEvent): """Represents a gateway Resume event.""" - #: The shard that reconnected. - #: - #: :type: :obj:`~hikari.clients.shard_clients.ShardClient` shard: shards.ShardClient + """The shard that reconnected.""" @marshaller.marshallable() @@ -226,36 +216,28 @@ class ReadyEvent(HikariEvent, marshaller.Deserializable): This is received only when IDENTIFYing with the gateway. """ - #: The gateway version this is currently connected to. - #: - #: :type: :obj:`~int` gateway_version: int = marshaller.attrib(raw_name="v", deserializer=int) + """The gateway version this is currently connected to.""" - #: The object of the current bot account this connection is for. - #: - #: :type: :obj:`~hikari.users.MyUser` my_user: users.MyUser = marshaller.attrib(raw_name="user", deserializer=users.MyUser.deserialize) + """The object of the current bot account this connection is for.""" - #: A mapping of the guilds this bot is currently in. All guilds will start - #: off "unavailable". - #: - #: :type: :obj:`~typing.Mapping` [ :obj:`~hikari.bases.Snowflake`, :obj:`~hikari.guilds.UnavailableGuild` ] unavailable_guilds: typing.Mapping[bases.Snowflake, guilds.UnavailableGuild] = marshaller.attrib( raw_name="guilds", deserializer=lambda guilds_objs: {g.id: g for g in map(guilds.UnavailableGuild.deserialize, guilds_objs)}, ) + """A mapping of the guilds this bot is currently in. + + All guilds will start off "unavailable". + """ - #: The id of the current gateway session, used for reconnecting. - #: - #: :type: :obj:`~str` session_id: str = marshaller.attrib(deserializer=str) + """The id of the current gateway session, used for reconnecting.""" - #: Information about the current shard, only provided when IDENTIFYing. - #: - #: :type: :obj:`~typing.Tuple` [ :obj:`~int`, :obj:`~int` ], optional _shard_information: typing.Optional[typing.Tuple[int, int]] = marshaller.attrib( raw_name="shard", deserializer=tuple, if_undefined=None, default=None ) + """Information about the current shard, only provided when IDENTIFYing.""" @property def shard_id(self) -> typing.Optional[int]: @@ -280,27 +262,20 @@ def shard_count(self) -> typing.Optional[int]: class BaseChannelEvent(HikariEvent, bases.UniqueEntity, marshaller.Deserializable, abc.ABC): """A base object that Channel events will inherit from.""" - #: The channel's type. - #: - #: :type: :obj:`~hikari.channels.ChannelType` type: channels.ChannelType = marshaller.attrib(deserializer=channels.ChannelType) + """The channel's type.""" - #: The ID of the guild this channel is in, will be :obj:`~None` for DMs. - #: - #: :type: :obj:`~hikari.bases.Snowflake`, optional guild_id: typing.Optional[bases.Snowflake] = marshaller.attrib( deserializer=bases.Snowflake.deserialize, if_undefined=None, default=None ) + """The ID of the guild this channel is in, will be `None` for DMs.""" - #: The sorting position of this channel, will be relative to the - #: :attr:`parent_id` if set. - #: - #: :type: :obj:`~int`, optional position: typing.Optional[int] = marshaller.attrib(deserializer=int, if_undefined=None, default=None) + """The sorting position of this channel. + + This will be relative to the `BaseChannelEvent.parent_id` if set. + """ - #: An mapping of the set permission overwrites for this channel, if applicable. - #: - #: :type: :obj:`~typing.Mapping` [ :obj:`~hikari.bases.Snowflake`, :obj:`~hikari.channels.PermissionOverwrite` ], optional permission_overwrites: typing.Optional[ typing.Mapping[bases.Snowflake, channels.PermissionOverwrite] ] = marshaller.attrib( @@ -308,94 +283,72 @@ class BaseChannelEvent(HikariEvent, bases.UniqueEntity, marshaller.Deserializabl if_undefined=None, default=None, ) + """An mapping of the set permission overwrites for this channel, if applicable.""" - #: The name of this channel, if applicable. - #: - #: :type: :obj:`~str`, optional name: typing.Optional[str] = marshaller.attrib(deserializer=str, if_undefined=None, default=None) + """The name of this channel, if applicable.""" - #: The topic of this channel, if applicable and set. - #: - #: :type: :obj:`~str`, optional topic: typing.Optional[str] = marshaller.attrib(deserializer=str, if_undefined=None, if_none=None, default=None) + """The topic of this channel, if applicable and set.""" - #: Whether this channel is nsfw, will be :obj:`~None` if not applicable. - #: - #: :type: :obj:`~bool`, optional is_nsfw: typing.Optional[bool] = marshaller.attrib( raw_name="nsfw", deserializer=bool, if_undefined=None, default=None ) + """Whether this channel is nsfw, will be `None` if not applicable.""" - #: The ID of the last message sent, if it's a text type channel. - #: - #: :type: :obj:`~hikari.bases.Snowflake`, optional last_message_id: typing.Optional[bases.Snowflake] = marshaller.attrib( deserializer=bases.Snowflake.deserialize, if_none=None, if_undefined=None, default=None ) + """The ID of the last message sent, if it's a text type channel.""" - #: The bitrate (in bits) of this channel, if it's a guild voice channel. - #: - #: :type: :obj:`~bool`, optional bitrate: typing.Optional[int] = marshaller.attrib(deserializer=int, if_undefined=None, default=None) + """The bitrate (in bits) of this channel, if it's a guild voice channel.""" - #: The user limit for this channel if it's a guild voice channel. - #: - #: :type: :obj:`~bool`, optional user_limit: typing.Optional[int] = marshaller.attrib(deserializer=int, if_undefined=None, default=None) + """The user limit for this channel if it's a guild voice channel.""" - #: The rate limit a user has to wait before sending another message in this - #: channel, if it's a guild text like channel. - #: - #: :type: :obj:`~datetime.timedelta`, optional rate_limit_per_user: typing.Optional[datetime.timedelta] = marshaller.attrib( deserializer=lambda delta: datetime.timedelta(seconds=delta), if_undefined=None, default=None ) + """How long a user has to wait before sending another message in this channel. + + This is only applicable to a guild text like channel. + """ - #: A mapping of this channel's recipient users, if it's a DM or group DM. - #: - #: :type: :obj:`~typing.Mapping` [ :obj:`~hikari.bases.Snowflake`, :obj:`~hikari.users.User` ], optional recipients: typing.Optional[typing.Mapping[bases.Snowflake, users.User]] = marshaller.attrib( deserializer=lambda recipients: {user.id: user for user in map(users.User.deserialize, recipients)}, if_undefined=None, default=None, ) + """A mapping of this channel's recipient users, if it's a DM or group DM.""" - #: The hash of this channel's icon, if it's a group DM channel and is set. - #: - #: :type: :obj:`~str`, optional icon_hash: typing.Optional[str] = marshaller.attrib( raw_name="icon", deserializer=str, if_undefined=None, if_none=None, default=None ) + """The hash of this channel's icon, if it's a group DM channel and is set.""" - #: The ID of this channel's creator, if it's a DM channel. - #: - #: :type: :obj:`~hikari.bases.Snowflake`, optional owner_id: typing.Optional[bases.Snowflake] = marshaller.attrib( deserializer=bases.Snowflake.deserialize, if_undefined=None, default=None ) + """The ID of this channel's creator, if it's a DM channel.""" - #: The ID of the application that created the group DM, if it's a - #: bot based group DM. - #: - #: :type: :obj:`~hikari.bases.Snowflake`, optional application_id: typing.Optional[bases.Snowflake] = marshaller.attrib( deserializer=bases.Snowflake.deserialize, if_undefined=None, default=None ) + """The ID of the application that created the group DM. + + This is only applicable to bot based group DMs. + """ - #: The ID of this channels's parent category within guild, if set. - #: - #: :type: :obj:`~hikari.bases.Snowflake`, optional parent_id: typing.Optional[bases.Snowflake] = marshaller.attrib( deserializer=bases.Snowflake.deserialize, if_undefined=None, if_none=None, default=None ) + """The ID of this channels's parent category within guild, if set.""" - #: The datetime of when the last message was pinned in this channel, - #: if set and applicable. - #: - #: :type: :obj:`~datetime.datetime`, optional last_pin_timestamp: typing.Optional[datetime.datetime] = marshaller.attrib( deserializer=conversions.parse_iso_8601_ts, if_undefined=None, default=None ) + """The datetime of when the last message was pinned in this channel.""" @requires_intents(intents.Intent.GUILDS) @@ -433,26 +386,24 @@ class ChannelPinUpdateEvent(HikariEvent, marshaller.Deserializable): when a pinned message is deleted. """ - #: The ID of the guild where this event happened. - #: Will be :obj:`~None` if this happened in a DM channel. - #: - #: :type: :obj:`~hikari.bases.Snowflake`, optional guild_id: typing.Optional[bases.Snowflake] = marshaller.attrib( deserializer=bases.Snowflake.deserialize, if_undefined=None, default=None ) + """The ID of the guild where this event happened. + + Will be `None` if this happened in a DM channel. + """ - #: The ID of the channel where the message was pinned or unpinned. - #: - #: :type: :obj:`~hikari.bases.Snowflake` channel_id: bases.Snowflake = marshaller.attrib(deserializer=bases.Snowflake.deserialize) + """The ID of the channel where the message was pinned or unpinned.""" - #: The datetime of when the most recent message was pinned in this channel. - #: Will be :obj:`~None` if there are no messages pinned after this change. - #: - #: :type: :obj:`~datetime.datetime`, optional last_pin_timestamp: typing.Optional[datetime.datetime] = marshaller.attrib( deserializer=conversions.parse_iso_8601_ts, if_undefined=None, default=None ) + """The datetime of when the most recent message was pinned in this channel. + + Will be `None` if there are no messages pinned after this change. + """ @requires_intents(intents.Intent.GUILDS) @@ -479,9 +430,8 @@ class GuildUpdateEvent(HikariEvent, marshaller.Deserializable): class GuildLeaveEvent(HikariEvent, bases.UniqueEntity, marshaller.Deserializable): """Fired when the current user leaves the guild or is kicked/banned from it. - Notes - ----- - This is fired based on Discord's Guild Delete gateway event. + !!! note + This is fired based on Discord's Guild Delete gateway event. """ @@ -491,9 +441,8 @@ class GuildLeaveEvent(HikariEvent, bases.UniqueEntity, marshaller.Deserializable class GuildUnavailableEvent(HikariEvent, bases.UniqueEntity, marshaller.Deserializable): """Fired when a guild becomes temporarily unavailable due to an outage. - Notes - ----- - This is fired based on Discord's Guild Delete gateway event. + !!! note + This is fired based on Discord's Guild Delete gateway event. """ @@ -503,15 +452,11 @@ class GuildUnavailableEvent(HikariEvent, bases.UniqueEntity, marshaller.Deserial class BaseGuildBanEvent(HikariEvent, marshaller.Deserializable, abc.ABC): """A base object that guild ban events will inherit from.""" - #: The ID of the guild this ban is in. - #: - #: :type: :obj:`~hikari.bases.Snowflake` guild_id: bases.Snowflake = marshaller.attrib(deserializer=bases.Snowflake.deserialize) + """The ID of the guild this ban is in.""" - #: The object of the user this ban targets. - #: - #: :type: :obj:`~hikari.users.User` user: users.User = marshaller.attrib(deserializer=users.User.deserialize) + """The object of the user this ban targets.""" @requires_intents(intents.Intent.GUILD_BANS) @@ -534,17 +479,13 @@ class GuildBanRemoveEvent(BaseGuildBanEvent): class GuildEmojisUpdateEvent(HikariEvent, marshaller.Deserializable): """Represents a Guild Emoji Update gateway event.""" - #: The ID of the guild this emoji was updated in. - #: - #: :type: :obj:`~hikari.bases.Snowflake` guild_id: bases.Snowflake = marshaller.attrib(deserializer=bases.Snowflake.deserialize) + """The ID of the guild this emoji was updated in.""" - #: The updated mapping of emojis by their ID. - #: - #: :type: :obj:`~typing.Mapping` [ :obj:`~hikari.bases.Snowflake`, :obj:`~hikari.emojis.GuildEmoji` ] emojis: typing.Mapping[bases.Snowflake, _emojis.GuildEmoji] = marshaller.attrib( deserializer=lambda ems: {emoji.id: emoji for emoji in map(_emojis.GuildEmoji.deserialize, ems)} ) + """The updated mapping of emojis by their ID.""" @requires_intents(intents.Intent.GUILD_INTEGRATIONS) @@ -553,10 +494,8 @@ class GuildEmojisUpdateEvent(HikariEvent, marshaller.Deserializable): class GuildIntegrationsUpdateEvent(HikariEvent, marshaller.Deserializable): """Used to represent Guild Integration Update gateway events.""" - #: The ID of the guild the integration was updated in. - #: - #: :type: :obj:`~hikari.bases.Snowflake` guild_id: bases.Snowflake = marshaller.attrib(deserializer=bases.Snowflake.deserialize) + """The ID of the guild the integration was updated in.""" @requires_intents(intents.Intent.GUILD_MEMBERS) @@ -565,10 +504,8 @@ class GuildIntegrationsUpdateEvent(HikariEvent, marshaller.Deserializable): class GuildMemberAddEvent(HikariEvent, guilds.GuildMember): """Used to represent a Guild Member Add gateway event.""" - #: The ID of the guild where this member was added. - #: - #: :type: :obj:`~hikari.bases.Snowflake` guild_id: bases.Snowflake = marshaller.attrib(deserializer=bases.Snowflake.deserialize) + """The ID of the guild where this member was added.""" @requires_intents(intents.Intent.GUILD_MEMBERS) @@ -580,38 +517,33 @@ class GuildMemberUpdateEvent(HikariEvent, marshaller.Deserializable): Sent when a guild member or their inner user object is updated. """ - #: The ID of the guild this member was updated in. - #: - #: :type: :obj:`~hikari.bases.Snowflake` guild_id: bases.Snowflake = marshaller.attrib(deserializer=bases.Snowflake.deserialize) + """The ID of the guild this member was updated in.""" - #: A sequence of the IDs of the member's current roles. - #: - #: :type: :obj:`~typing.Sequence` [ :obj:`~hikari.bases.Snowflake` ] role_ids: typing.Sequence[bases.Snowflake] = marshaller.attrib( raw_name="roles", deserializer=lambda role_ids: [bases.Snowflake.deserialize(rid) for rid in role_ids], ) + """A sequence of the IDs of the member's current roles.""" - #: The object of the user who was updated. - #: - #: :type: :obj:`~hikari.users.User` user: users.User = marshaller.attrib(deserializer=users.User.deserialize) + """The object of the user who was updated.""" - #: This member's nickname. When set to :obj:`~None`, this has been removed - #: and when set to :obj:`~hikari.unset.UNSET` this hasn't been acted on. - #: - #: :type: :obj:`~typing.Union` [ :obj:`~str`, :obj:`~hikari.unset.UNSET` ], optional nickname: typing.Union[None, str, unset.Unset] = marshaller.attrib( raw_name="nick", deserializer=str, if_none=None, if_undefined=unset.Unset, default=unset.UNSET ) + """This member's nickname. + + When set to `None`, this has been removed and when set to + `hikari.unset.UNSET` this hasn't been acted on. + """ - #: The datetime of when this member started "boosting" this guild. - #: Will be :obj:`~None` if they aren't boosting. - #: - #: :type: :obj:`~typing.Union` [ :obj:`~datetime.datetime`, :obj:`~hikari.unset.UNSET` ], optional premium_since: typing.Union[None, datetime.datetime, unset.Unset] = marshaller.attrib( deserializer=conversions.parse_iso_8601_ts, if_none=None, if_undefined=unset.Unset, default=unset.UNSET ) + """The datetime of when this member started "boosting" this guild. + + Will be `None` if they aren't boosting. + """ @requires_intents(intents.Intent.GUILD_MEMBERS) @@ -623,15 +555,11 @@ class GuildMemberRemoveEvent(HikariEvent, marshaller.Deserializable): Sent when a member is kicked, banned or leaves a guild. """ - #: The ID of the guild this user was removed from. - #: - #: :type: :obj:`~hikari.bases.Snowflake` guild_id: bases.Snowflake = marshaller.attrib(deserializer=bases.Snowflake.deserialize) + """The ID of the guild this user was removed from.""" - #: The object of the user who was removed from this guild. - #: - #: :type: :obj:`~hikari.users.User` user: users.User = marshaller.attrib(deserializer=users.User.deserialize) + """The object of the user who was removed from this guild.""" @requires_intents(intents.Intent.GUILDS) @@ -640,15 +568,11 @@ class GuildMemberRemoveEvent(HikariEvent, marshaller.Deserializable): class GuildRoleCreateEvent(HikariEvent, marshaller.Deserializable): """Used to represent a Guild Role Create gateway event.""" - #: The ID of the guild where this role was created. - #: - #: :type: :obj:`~hikari.bases.Snowflake` guild_id: bases.Snowflake = marshaller.attrib(deserializer=bases.Snowflake.deserialize) + """The ID of the guild where this role was created.""" - #: The object of the role that was created. - #: - #: :type: :obj:`~hikari.guilds.GuildRole` role: guilds.GuildRole = marshaller.attrib(deserializer=guilds.GuildRole.deserialize) + """The object of the role that was created.""" @requires_intents(intents.Intent.GUILDS) @@ -657,15 +581,11 @@ class GuildRoleCreateEvent(HikariEvent, marshaller.Deserializable): class GuildRoleUpdateEvent(HikariEvent, marshaller.Deserializable): """Used to represent a Guild Role Create gateway event.""" - #: The ID of the guild where this role was updated. - #: - #: :type: :obj:`~hikari.bases.Snowflake` guild_id: bases.Snowflake = marshaller.attrib(deserializer=bases.Snowflake.deserialize) + """The ID of the guild where this role was updated.""" - #: The updated role object. - #: - #: :type: :obj:`~hikari.guilds.GuildRole` role: guilds.GuildRole = marshaller.attrib(deserializer=guilds.GuildRole.deserialize) + """The updated role object.""" @requires_intents(intents.Intent.GUILDS) @@ -674,15 +594,11 @@ class GuildRoleUpdateEvent(HikariEvent, marshaller.Deserializable): class GuildRoleDeleteEvent(HikariEvent, marshaller.Deserializable): """Represents a gateway Guild Role Delete Event.""" - #: The ID of the guild where this role is being deleted. - #: - #: :type: :obj:`~hikari.bases.Snowflake` guild_id: bases.Snowflake = marshaller.attrib(deserializer=bases.Snowflake.deserialize) + """The ID of the guild where this role is being deleted.""" - #: The ID of the role being deleted. - #: - #: :type: :obj:`~hikari.bases.Snowflake` role_id: bases.Snowflake = marshaller.attrib(deserializer=bases.Snowflake.deserialize) + """The ID of the role being deleted.""" @requires_intents(intents.Intent.GUILD_INVITES) @@ -691,73 +607,57 @@ class GuildRoleDeleteEvent(HikariEvent, marshaller.Deserializable): class InviteCreateEvent(HikariEvent, marshaller.Deserializable): """Represents a gateway Invite Create event.""" - #: The ID of the channel this invite targets. - #: - #: :type: :obj:`~hikari.bases.Snowflake` channel_id: bases.Snowflake = marshaller.attrib(deserializer=bases.Snowflake.deserialize) + """The ID of the channel this invite targets.""" - #: The code that identifies this invite - #: - #: :type: :obj:`~str` code: str = marshaller.attrib(deserializer=str) + """The code that identifies this invite.""" - #: The datetime of when this invite was created. - #: - #: :type: :obj:`~datetime.datetime` created_at: datetime.datetime = marshaller.attrib(deserializer=conversions.parse_iso_8601_ts) + """The datetime of when this invite was created.""" - #: The ID of the guild this invite was created in, if applicable. - #: Will be :obj:`~None` for group DM invites. - #: - #: :type: :obj:`~hikari.bases.Snowflake`, optional guild_id: typing.Optional[bases.Snowflake] = marshaller.attrib( deserializer=bases.Snowflake.deserialize, if_undefined=None, default=None ) + """The ID of the guild this invite was created in, if applicable. + + Will be `None` for group DM invites. + """ - #: The object of the user who created this invite, if applicable. - #: - #: :type: :obj:`~hikari.users.User`, optional inviter: typing.Optional[users.User] = marshaller.attrib( deserializer=users.User.deserialize, if_undefined=None, default=None ) + """The object of the user who created this invite, if applicable.""" - #: The timedelta of how long this invite will be valid for. - #: If set to :obj:`~None` then this is unlimited. - #: - #: :type: :obj:`~datetime.timedelta`, optional max_age: typing.Optional[datetime.timedelta] = marshaller.attrib( deserializer=lambda age: datetime.timedelta(seconds=age) if age > 0 else None, ) + """The timedelta of how long this invite will be valid for. + + If set to `None` then this is unlimited. + """ - #: The limit for how many times this invite can be used before it expires. - #: If set to ``0``, or infinity (``float("inf")``) then this is unlimited. - #: - #: :type: :obj:`~typing.Union` [ :obj:`~int`, :obj:`~float` ( ``"inf"`` ) ] max_uses: typing.Union[int, float] = marshaller.attrib(deserializer=lambda count: count or float("inf")) + """The limit for how many times this invite can be used before it expires. + + If set to infinity (`float("inf")`) then this is unlimited. + """ - #: The object of the user who this invite targets, if set. - #: - #: :type: :obj:`~hikari.users.User`, optional target_user: typing.Optional[users.User] = marshaller.attrib( deserializer=users.User.deserialize, if_undefined=None, default=None ) + """The object of the user who this invite targets, if set.""" - #: The type of user target this invite is, if applicable. - #: - #: :type: :obj:`~hikari.invites.TargetUserType`, optional target_user_type: typing.Optional[invites.TargetUserType] = marshaller.attrib( deserializer=invites.TargetUserType, if_undefined=None, default=None ) + """The type of user target this invite is, if applicable.""" - #: Whether this invite grants temporary membership. - #: - #: :type: :obj:`~bool` is_temporary: bool = marshaller.attrib(raw_name="temporary", deserializer=bool) + """Whether this invite grants temporary membership.""" - #: The amount of times this invite has been used. - #: - #: :type: :obj:`~int` uses: int = marshaller.attrib(deserializer=int) + """The amount of times this invite has been used.""" @requires_intents(intents.Intent.GUILD_INVITES) @@ -769,23 +669,19 @@ class InviteDeleteEvent(HikariEvent, marshaller.Deserializable): Sent when an invite is deleted for a channel we can access. """ - #: The ID of the channel this ID was attached to - #: - #: :type: :obj:`~hikari.bases.Snowflake` channel_id: bases.Snowflake = marshaller.attrib(deserializer=bases.Snowflake.deserialize) + """The ID of the channel this ID was attached to.""" - #: The code of this invite. - #: - #: :type: :obj:`~str` code: str = marshaller.attrib(deserializer=str) + """The code of this invite.""" - #: The ID of the guild this invite was deleted in. - #: This will be :obj:`~None` if this invite belonged to a DM channel. - #: - #: :type: :obj:`~hikari.bases.Snowflake`, optional guild_id: typing.Optional[bases.Snowflake] = marshaller.attrib( deserializer=bases.Snowflake.deserialize, if_undefined=None, default=None ) + """The ID of the guild this invite was deleted in. + + This will be `None` if this invite belonged to a DM channel. + """ @requires_intents(intents.Intent.GUILD_MESSAGES, intents.Intent.DIRECT_MESSAGES) @@ -802,189 +698,146 @@ class MessageCreateEvent(HikariEvent, messages.Message): class MessageUpdateEvent(HikariEvent, bases.UniqueEntity, marshaller.Deserializable): """Represents Message Update gateway events. - Note - ---- - All fields on this model except :attr:`channel_id` and :obj:`~`HikariEvent.id`` may be - set to :obj:`~hikari.unset.UNSET` (a singleton defined in - ``hikari.bases``) if we have not received information about their - state from Discord alongside field nullability. + !!! note + All fields on this model except `MessageUpdateEvent.channel_id` and + `MessageUpdateEvent.id` may be set to `hikari.unset.UNSET` (a singleton) + we have not received information about their state from Discord + alongside field nullability. """ - #: The ID of the channel that the message was sent in. - #: - #: :type: :obj:`~hikari.bases.Snowflake` channel_id: bases.Snowflake = marshaller.attrib(deserializer=bases.Snowflake.deserialize) + """The ID of the channel that the message was sent in.""" - #: The ID of the guild that the message was sent in. - #: - #: :type: :obj:`~typing.Union` [ :obj:`~hikari.bases.Snowflake`, :obj:`~hikari.unset.UNSET` ] guild_id: typing.Union[bases.Snowflake, unset.Unset] = marshaller.attrib( deserializer=bases.Snowflake.deserialize, if_undefined=unset.Unset, default=unset.UNSET ) + """The ID of the guild that the message was sent in.""" - #: The author of this message. - #: - #: :type: :obj:`~typing.Union` [ :obj:`~hikari.users.User`, :obj:`~hikari.unset.UNSET` ] author: typing.Union[users.User, unset.Unset] = marshaller.attrib( deserializer=users.User.deserialize, if_undefined=unset.Unset, default=unset.UNSET ) + """The author of this message.""" - #: The member properties for the message's author. - #: - #: :type: :obj:`~typing.Union` [ :obj:`~hikari.guilds.GuildMember`, :obj:`~hikari.unset.UNSET` ] member: typing.Union[guilds.GuildMember, unset.Unset] = marshaller.attrib( deserializer=guilds.GuildMember.deserialize, if_undefined=unset.Unset, default=unset.UNSET ) + """The member properties for the message's author.""" - #: The content of the message. - #: - #: :type: :obj:`~typing.Union` [ :obj:`~str`, :obj:`~hikari.unset.UNSET` ] content: typing.Union[str, unset.Unset] = marshaller.attrib( deserializer=str, if_undefined=unset.Unset, default=unset.UNSET ) + """The content of the message.""" - #: The timestamp that the message was sent at. - #: - #: :type: :obj:`~typing.Union` [ :obj:`~datetime.datetime`, :obj:`~hikari.unset.UNSET` ] timestamp: typing.Union[datetime.datetime, unset.Unset] = marshaller.attrib( deserializer=conversions.parse_iso_8601_ts, if_undefined=unset.Unset, default=unset.UNSET ) + """The timestamp that the message was sent at.""" - #: The timestamp that the message was last edited at, or :obj:`~None` if - #: it wasn't ever edited. - #: - #: :type: :obj:`~typing.Union` [ :obj:`~datetime.datetime`, :obj:`~hikari.unset.UNSET` ], optional edited_timestamp: typing.Union[datetime.datetime, unset.Unset, None] = marshaller.attrib( deserializer=conversions.parse_iso_8601_ts, if_none=None, if_undefined=unset.Unset, default=unset.UNSET ) + """The timestamp that the message was last edited at. + + Will be `None` if the message wasn't ever edited. + """ - #: Whether the message is a TTS message. - #: - #: :type: :obj:`~typing.Union` [ :obj:`~bool`, :obj:`~hikari.unset.UNSET` ] is_tts: typing.Union[bool, unset.Unset] = marshaller.attrib( raw_name="tts", deserializer=bool, if_undefined=unset.Unset, default=unset.UNSET ) + """Whether the message is a TTS message.""" - #: Whether the message mentions ``@everyone`` or ``@here``. - #: - #: :type: :obj:`~typing.Union` [ :obj:`~bool`, :obj:`~hikari.unset.UNSET` ] is_mentioning_everyone: typing.Union[bool, unset.Unset] = marshaller.attrib( raw_name="mention_everyone", deserializer=bool, if_undefined=unset.Unset, default=unset.UNSET ) + """Whether the message mentions `@everyone` or `@here`.""" - #: The users the message mentions. - #: - #: :type: :obj:`~typing.Union` [ :obj:`~typing.Set` [ :obj:`~hikari.bases.Snowflake` ], :obj:`~hikari.unset.UNSET` ] user_mentions: typing.Union[typing.Set[bases.Snowflake], unset.Unset] = marshaller.attrib( raw_name="mentions", deserializer=lambda user_mentions: {bases.Snowflake.deserialize(u["id"]) for u in user_mentions}, if_undefined=unset.Unset, default=unset.UNSET, ) + """The users the message mentions.""" - #: The roles the message mentions. - #: - #: :type: :obj:`~typing.Union` [ :obj:`~typing.Set` [ :obj:`~hikari.bases.Snowflake` ], :obj:`~hikari.unset.UNSET` ] role_mentions: typing.Union[typing.Set[bases.Snowflake], unset.Unset] = marshaller.attrib( raw_name="mention_roles", deserializer=lambda role_mentions: {bases.Snowflake.deserialize(r) for r in role_mentions}, if_undefined=unset.Unset, default=unset.UNSET, ) + """The roles the message mentions.""" - #: The channels the message mentions. - #: - #: :type: :obj:`~typing.Union` [ :obj:`~typing.Set` [ :obj:`~hikari.bases.Snowflake` ], :obj:`~hikari.unset.UNSET` ] channel_mentions: typing.Union[typing.Set[bases.Snowflake], unset.Unset] = marshaller.attrib( raw_name="mention_channels", deserializer=lambda channel_mentions: {bases.Snowflake.deserialize(c["id"]) for c in channel_mentions}, if_undefined=unset.Unset, default=unset.UNSET, ) + """The channels the message mentions.""" - #: The message attachments. - #: - #: :type: :obj:`~typing.Union` [ :obj:`~typing.Sequence` [ :obj:`~hikari.messages.Attachment` ], :obj:`~hikari.unset.UNSET` ] attachments: typing.Union[typing.Sequence[messages.Attachment], unset.Unset] = marshaller.attrib( deserializer=lambda attachments: [messages.Attachment.deserialize(a) for a in attachments], if_undefined=unset.Unset, default=unset.UNSET, ) + """The message attachments.""" - #: The message's embeds. - #: - #: :type: :obj:`~typing.Union` [ :obj:`~typing.Sequence` [ :obj:`~hikari.embeds.Embed` ], :obj:`~hikari.unset.UNSET` ] embeds: typing.Union[typing.Sequence[_embeds.Embed], unset.Unset] = marshaller.attrib( deserializer=lambda embed_objs: [_embeds.Embed.deserialize(e) for e in embed_objs], if_undefined=unset.Unset, default=unset.UNSET, ) + """The message's embeds.""" - #: The message's reactions. - #: - #: :type: :obj:`~typing.Union` [ :obj:`~typing.Sequence` [ :obj:`~hikari.messages.Reaction` ], :obj:`~hikari.unset.UNSET` ] reactions: typing.Union[typing.Sequence[messages.Reaction], unset.Unset] = marshaller.attrib( deserializer=lambda reactions: [messages.Reaction.deserialize(r) for r in reactions], if_undefined=unset.Unset, default=unset.UNSET, ) + """The message's reactions.""" - #: Whether the message is pinned. - #: - #: :type: :obj:`~typing.Union` [ :obj:`~bool`, :obj:`~hikari.unset.UNSET` ] is_pinned: typing.Union[bool, unset.Unset] = marshaller.attrib( raw_name="pinned", deserializer=bool, if_undefined=unset.Unset, default=unset.UNSET ) + """Whether the message is pinned.""" - #: If the message was generated by a webhook, the webhook's id. - #: - #: :type: :obj:`~typing.Union` [ :obj:`~hikari.bases.Snowflake`, :obj:`~hikari.unset.UNSET` ] webhook_id: typing.Union[bases.Snowflake, unset.Unset] = marshaller.attrib( deserializer=bases.Snowflake.deserialize, if_undefined=unset.Unset, default=unset.UNSET ) + """If the message was generated by a webhook, the webhook's ID.""" - #: The message's type. - #: - #: :type: :obj:`~typing.Union` [ :obj:`~hikari.messages.MessageType`, :obj:`~hikari.unset.UNSET` ] type: typing.Union[messages.MessageType, unset.Unset] = marshaller.attrib( deserializer=messages.MessageType, if_undefined=unset.Unset, default=unset.UNSET ) + """The message's type.""" - #: The message's activity. - #: - #: :type: :obj:`~typing.Union` [ :obj:`~hikari.messages.MessageActivity`, :obj:`~hikari.unset.UNSET` ] activity: typing.Union[messages.MessageActivity, unset.Unset] = marshaller.attrib( deserializer=messages.MessageActivity.deserialize, if_undefined=unset.Unset, default=unset.UNSET ) + """The message's activity.""" - #: The message's application. - #: - #: :type: :obj:`~typing.Union` [ :obj:`~hikari.oauth2.Application`, :obj:`~hikari.unset.UNSET` ] application: typing.Optional[applications.Application] = marshaller.attrib( deserializer=applications.Application.deserialize, if_undefined=unset.Unset, default=unset.UNSET ) + """The message's application.""" - #: The message's crossposted reference data. - #: - #: :type: :obj:`~typing.Union` [ :obj:`~hikari.messages.MessageCrosspost`, :obj:`~hikari.unset.UNSET` ] message_reference: typing.Union[messages.MessageCrosspost, unset.Unset] = marshaller.attrib( deserializer=messages.MessageCrosspost.deserialize, if_undefined=unset.Unset, default=unset.UNSET ) + """The message's cross-posted reference data.""" - #: The message's flags. - #: - #: :type: :obj:`~typing.Union` [ :obj:`~hikari.messages.MessageFlag`, :obj:`~hikari.unset.UNSET` ] flags: typing.Union[messages.MessageFlag, unset.Unset] = marshaller.attrib( deserializer=messages.MessageFlag, if_undefined=unset.Unset, default=unset.UNSET ) + """The message's flags.""" - #: The message nonce. This is a string used for validating - #: a message was sent. - #: - #: :type: :obj:`~typing.Union` [ :obj:`~hikari.messages.MessageFlag`, :obj:`~hikari.unset.UNSET` ] nonce: typing.Union[str, unset.Unset] = marshaller.attrib( deserializer=str, if_undefined=unset.Unset, default=unset.UNSET ) + """The message nonce. + + This is a string used for validating a message was sent. + """ @requires_intents(intents.Intent.GUILD_MESSAGES, intents.Intent.DIRECT_MESSAGES) @@ -996,22 +849,19 @@ class MessageDeleteEvent(HikariEvent, marshaller.Deserializable): Sent when a message is deleted in a channel we have access to. """ - #: The ID of the channel where this message was deleted. - #: - #: :type: :obj:`~hikari.bases.Snowflake` channel_id: bases.Snowflake = marshaller.attrib(deserializer=bases.Snowflake.deserialize) + """The ID of the channel where this message was deleted.""" - #: The ID of the guild where this message was deleted. - #: Will be :obj:`~None` if this message was deleted in a DM channel. - #: - #: :type: :obj:`~hikari.bases.Snowflake`, optional guild_id: typing.Optional[bases.Snowflake] = marshaller.attrib( deserializer=bases.Snowflake.deserialize, if_undefined=None, default=None ) - #: The ID of the message that was deleted. - #: - #: :type: :obj:`~hikari.bases.Snowflake` + """The ID of the guild where this message was deleted. + + This will be `None` if this message was deleted in a DM channel. + """ + message_id: bases.Snowflake = marshaller.attrib(raw_name="id", deserializer=bases.Snowflake.deserialize) + """The ID of the message that was deleted.""" @requires_intents(intents.Intent.GUILD_MESSAGES) @@ -1023,25 +873,21 @@ class MessageDeleteBulkEvent(HikariEvent, marshaller.Deserializable): Sent when multiple messages are deleted in a channel at once. """ - #: The ID of the channel these messages have been deleted in. - #: - #: :type: :obj:`~hikari.bases.Snowflake` channel_id: bases.Snowflake = marshaller.attrib(deserializer=bases.Snowflake.deserialize) + """The ID of the channel these messages have been deleted in.""" - #: The ID of the channel these messages have been deleted in. - #: Will be :obj:`~None` if these messages were bulk deleted in a DM channel. - #: - #: :type: :obj:`~hikari.bases.Snowflake`, optional guild_id: typing.Optional[bases.Snowflake] = marshaller.attrib( deserializer=bases.Snowflake.deserialize, if_none=None ) + """The ID of the channel these messages have been deleted in. + + This will be `None` if these messages were bulk deleted in a DM channel. + """ - #: A collection of the IDs of the messages that were deleted. - #: - #: :type: :obj:`~typing.Set` [ :obj:`~hikari.bases.Snowflake` ] message_ids: typing.Set[bases.Snowflake] = marshaller.attrib( raw_name="ids", deserializer=lambda msgs: {bases.Snowflake.deserialize(m) for m in msgs} ) + """A collection of the IDs of the messages that were deleted.""" @requires_intents(intents.Intent.GUILD_MESSAGE_REACTIONS, intents.Intent.DIRECT_MESSAGE_REACTIONS) @@ -1050,43 +896,35 @@ class MessageDeleteBulkEvent(HikariEvent, marshaller.Deserializable): class MessageReactionAddEvent(HikariEvent, marshaller.Deserializable): """Used to represent Message Reaction Add gateway events.""" - #: The ID of the user adding the reaction. - #: - #: :type: :obj:`~hikari.bases.Snowflake` user_id: bases.Snowflake = marshaller.attrib(deserializer=bases.Snowflake.deserialize) + """The ID of the user adding the reaction.""" - #: The ID of the channel where this reaction is being added. - #: - #: :type: :obj:`~hikari.bases.Snowflake` channel_id: bases.Snowflake = marshaller.attrib(deserializer=bases.Snowflake.deserialize) + """The ID of the channel where this reaction is being added.""" - #: The ID of the message this reaction is being added to. - #: - #: :type: :obj:`~hikari.bases.Snowflake` message_id: bases.Snowflake = marshaller.attrib(deserializer=bases.Snowflake.deserialize) + """The ID of the message this reaction is being added to.""" - #: The ID of the guild where this reaction is being added, unless this is - #: happening in a DM channel. - #: - #: :type: :obj:`~hikari.bases.Snowflake`, optional guild_id: typing.Optional[bases.Snowflake] = marshaller.attrib( deserializer=bases.Snowflake.deserialize, if_undefined=None, default=None ) + """The ID of the guild where this reaction is being added. + + This will be `None` if this is happening in a DM channel. + """ - #: The member object of the user who's adding this reaction, if this is - #: occurring in a guild. - #: - #: :type: :obj:`~hikari.guilds.GuildMember`, optional member: typing.Optional[guilds.GuildMember] = marshaller.attrib( deserializer=guilds.GuildMember.deserialize, if_undefined=None, default=None ) + """The member object of the user who's adding this reaction. + + This will be `None` if this is happening in a DM channel. + """ - #: The object of the emoji being added. - #: - #: :type: :obj:`~typing.Union` [ :obj:`~hikari.emojis.UnknownEmoji`, :obj:`~hikari.emojis.UnicodeEmoji` ] emoji: typing.Union[_emojis.UnknownEmoji, _emojis.UnicodeEmoji] = marshaller.attrib( deserializer=_emojis.deserialize_reaction_emoji, ) + """The object of the emoji being added.""" @requires_intents(intents.Intent.GUILD_MESSAGE_REACTIONS, intents.Intent.DIRECT_MESSAGE_REACTIONS) @@ -1095,35 +933,27 @@ class MessageReactionAddEvent(HikariEvent, marshaller.Deserializable): class MessageReactionRemoveEvent(HikariEvent, marshaller.Deserializable): """Used to represent Message Reaction Remove gateway events.""" - #: The ID of the user who is removing their reaction. - #: - #: :type: :obj:`~hikari.bases.Snowflake` user_id: bases.Snowflake = marshaller.attrib(deserializer=bases.Snowflake.deserialize) + """The ID of the user who is removing their reaction.""" - #: The ID of the channel where this reaction is being removed. - #: - #: :type: :obj:`~hikari.bases.Snowflake` channel_id: bases.Snowflake = marshaller.attrib(deserializer=bases.Snowflake.deserialize) + """The ID of the channel where this reaction is being removed.""" - #: The ID of the message this reaction is being removed from. - #: - #: :type: :obj:`~hikari.bases.Snowflake` message_id: bases.Snowflake = marshaller.attrib(deserializer=bases.Snowflake.deserialize) + """The ID of the message this reaction is being removed from.""" - #: The ID of the guild where this reaction is being removed, unless this is - #: happening in a DM channel. - #: - #: :type: :obj:`~hikari.bases.Snowflake`, optional guild_id: typing.Optional[bases.Snowflake] = marshaller.attrib( deserializer=bases.Snowflake.deserialize, if_undefined=None, default=None ) + """The ID of the guild where this reaction is being removed + + This will be `None` if this event is happening in a DM channel. + """ - #: The object of the emoji being removed. - #: - #: :type: :obj:`~typing.Union` [ :obj:`~hikari.emojis.UnknownEmoji`, :obj:`~hikari.emojis.UnicodeEmoji` ] emoji: typing.Union[_emojis.UnicodeEmoji, _emojis.UnknownEmoji] = marshaller.attrib( deserializer=_emojis.deserialize_reaction_emoji, ) + """The object of the emoji being removed.""" @requires_intents(intents.Intent.GUILD_MESSAGE_REACTIONS, intents.Intent.DIRECT_MESSAGE_REACTIONS) @@ -1135,22 +965,16 @@ class MessageReactionRemoveAllEvent(HikariEvent, marshaller.Deserializable): Sent when all the reactions are removed from a message, regardless of emoji. """ - #: The ID of the channel where the targeted message is. - #: - #: :type: :obj:`~hikari.bases.Snowflake` channel_id: bases.Snowflake = marshaller.attrib(deserializer=bases.Snowflake.deserialize) + """The ID of the channel where the targeted message is.""" - #: The ID of the message all reactions are being removed from. - #: - #: :type: :obj:`~hikari.bases.Snowflake` message_id: bases.Snowflake = marshaller.attrib(deserializer=bases.Snowflake.deserialize) + """The ID of the message all reactions are being removed from.""" - #: The ID of the guild where the targeted message is, if applicable. - #: - #: :type: :obj:`~hikari.bases.Snowflake` guild_id: typing.Optional[bases.Snowflake] = marshaller.attrib( deserializer=bases.Snowflake.deserialize, if_undefined=None, default=None ) + """The ID of the guild where the targeted message is, if applicable.""" @requires_intents(intents.Intent.GUILD_MESSAGE_REACTIONS, intents.Intent.DIRECT_MESSAGE_REACTIONS) @@ -1162,29 +986,21 @@ class MessageReactionRemoveEmojiEvent(HikariEvent, marshaller.Deserializable): Sent when all the reactions for a single emoji are removed from a message. """ - #: The ID of the channel where the targeted message is. - #: - #: :type: :obj:`~hikari.bases.Snowflake` channel_id: bases.Snowflake = marshaller.attrib(deserializer=bases.Snowflake.deserialize) + """The ID of the channel where the targeted message is.""" - #: The ID of the guild where the targeted message is, if applicable. - #: - #: :type: :obj:`~hikari.bases.Snowflake` guild_id: typing.Optional[bases.Snowflake] = marshaller.attrib( deserializer=bases.Snowflake.deserialize, if_undefined=None, default=None ) + """The ID of the guild where the targeted message is, if applicable.""" - #: The ID of the message the reactions are being removed from. - #: - #: :type: :obj:`~hikari.bases.Snowflake` message_id: bases.Snowflake = marshaller.attrib(deserializer=bases.Snowflake.deserialize) + """The ID of the message the reactions are being removed from.""" - #: The object of the emoji that's being removed. - #: - #: :type: :obj:`~typing.Union` [ :obj:`~hikari.emojis.UnknownEmoji`, :obj:`~hikari.emojis.UnicodeEmoji` ] emoji: typing.Union[_emojis.UnicodeEmoji, _emojis.UnknownEmoji] = marshaller.attrib( deserializer=_emojis.deserialize_reaction_emoji, ) + """The object of the emoji that's being removed.""" @requires_intents(intents.Intent.GUILD_PRESENCES) @@ -1206,38 +1022,32 @@ class TypingStartEvent(HikariEvent, marshaller.Deserializable): Received when a user or bot starts "typing" in a channel. """ - #: The ID of the channel this typing event is occurring in. - #: - #: :type: :obj:`~hikari.bases.Snowflake` channel_id: bases.Snowflake = marshaller.attrib(deserializer=bases.Snowflake.deserialize) + """The ID of the channel this typing event is occurring in.""" - #: The ID of the guild this typing event is occurring in. - #: Will be :obj:`~None` if this event is happening in a DM channel. - #: - #: :type: :obj:`~hikari.bases.Snowflake`, optional guild_id: typing.Optional[bases.Snowflake] = marshaller.attrib( deserializer=bases.Snowflake.deserialize, if_undefined=None, default=None ) + """The ID of the guild this typing event is occurring in. + + Will be `None` if this event is happening in a DM channel. + """ - #: The ID of the user who triggered this typing event. - #: - #: :type: :obj:`~hikari.bases.Snowflake` user_id: bases.Snowflake = marshaller.attrib(deserializer=bases.Snowflake.deserialize) + """The ID of the user who triggered this typing event.""" - #: The datetime of when this typing event started. - #: - #: :type: :obj:`~datetime.datetime` timestamp: datetime.datetime = marshaller.attrib( deserializer=lambda date: datetime.datetime.fromtimestamp(date, datetime.timezone.utc) ) + """The datetime of when this typing event started.""" - #: The member object of the user who triggered this typing event, - #: if this was triggered in a guild. - #: - #: :type: :obj:`~hikari.guilds.GuildMember`, optional member: typing.Optional[guilds.GuildMember] = marshaller.attrib( deserializer=guilds.GuildMember.deserialize, if_undefined=None, default=None ) + """The member object of the user who triggered this typing event. + + Will be `None` if this was triggered in a DM. + """ @marshaller.marshallable() @@ -1268,20 +1078,14 @@ class VoiceServerUpdateEvent(HikariEvent, marshaller.Deserializable): falls over to a new server. """ - #: The voice connection's token - #: - #: :type: :obj:`~str` token: str = marshaller.attrib(deserializer=str) + """The voice connection's string token.""" - #: The ID of the guild this voice server update is for - #: - #: :type: :obj:`~hikari.bases.Snowflake` guild_id: bases.Snowflake = marshaller.attrib(deserializer=bases.Snowflake.deserialize) + """The ID of the guild this voice server update is for.""" - #: The uri for this voice server host. - #: - #: :type: :obj:`~str` endpoint: str = marshaller.attrib(deserializer=str) + """The uri for this voice server host.""" @requires_intents(intents.Intent.GUILD_WEBHOOKS) @@ -1293,12 +1097,8 @@ class WebhookUpdateEvent(HikariEvent, marshaller.Deserializable): Sent when a webhook is updated, created or deleted in a guild. """ - #: The ID of the guild this webhook is being updated in. - #: - #: :type: :obj:`~hikari.bases.Snowflake` guild_id: bases.Snowflake = marshaller.attrib(deserializer=bases.Snowflake.deserialize) + """The ID of the guild this webhook is being updated in.""" - #: The ID of the channel this webhook is being updated in. - #: - #: :type: :obj:`~hikari.bases.Snowflake` channel_id: bases.Snowflake = marshaller.attrib(deserializer=bases.Snowflake.deserialize) + """The ID of the channel this webhook is being updated in.""" diff --git a/hikari/gateway_entities.py b/hikari/gateway_entities.py index 950b455f8c..ab40340e0f 100644 --- a/hikari/gateway_entities.py +++ b/hikari/gateway_entities.py @@ -17,7 +17,7 @@ # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . """Entities directly related to creating and managing gateway shards.""" -__all__ = ["GatewayBot", "Activity"] +__all__ = ["Activity", "GatewayBot", "SessionStartLimit"] import datetime import typing @@ -34,23 +34,19 @@ class SessionStartLimit(bases.HikariEntity, marshaller.Deserializable): """Used to represent information about the current session start limits.""" - #: The total number of session starts the current bot is allowed. - #: - #: :type: :obj:`~int` total: int = marshaller.attrib(deserializer=int) + """The total number of session starts the current bot is allowed.""" - #: The remaining number of session starts this bot has. - #: - #: :type: :obj:`~int` remaining: int = marshaller.attrib(deserializer=int) + """The remaining number of session starts this bot has.""" - #: The timedelta of when :attr:`remaining` will reset back to :attr:`total` - #: for the current bot. - #: - #: :type: :obj:`~datetime.timedelta` reset_after: datetime.timedelta = marshaller.attrib( deserializer=lambda after: datetime.timedelta(milliseconds=after), ) + """When `SessionStartLimit.remaining` will reset for the current bot. + + After it resets it will be set to `SessionStartLimit.total`. + """ @marshaller.marshallable() @@ -58,20 +54,14 @@ class SessionStartLimit(bases.HikariEntity, marshaller.Deserializable): class GatewayBot(bases.HikariEntity, marshaller.Deserializable): """Used to represent gateway information for the connected bot.""" - #: The WSS URL that can be used for connecting to the gateway. - #: - #: :type: :obj:`~str` url: str = marshaller.attrib(deserializer=str) + """The WSS URL that can be used for connecting to the gateway.""" - #: The recommended number of shards to use when connecting to the gateway. - #: - #: :type: :obj:`~int` shard_count: int = marshaller.attrib(raw_name="shards", deserializer=int) + """The recommended number of shards to use when connecting to the gateway.""" - #: Information about the bot's current session start limit. - #: - #: :type: :obj:`~SessionStartLimit` session_start_limit: SessionStartLimit = marshaller.attrib(deserializer=SessionStartLimit.deserialize) + """Information about the bot's current session start limit.""" @marshaller.marshallable() @@ -82,24 +72,18 @@ class Activity(marshaller.Deserializable, marshaller.Serializable): This will show the activity as the bot's presence. """ - #: The activity name. - #: - #: :type: :obj:`~str` name: str = marshaller.attrib(deserializer=str, serializer=str) + """The activity name.""" - #: The activity URL. Only valid for ``STREAMING`` activities. - #: - #: :type: :obj:`~str`, optional url: typing.Optional[str] = marshaller.attrib( deserializer=str, serializer=str, if_none=None, if_undefined=None, default=None ) + """The activity URL. Only valid for `STREAMING` activities.""" - #: The activity type. - #: - #: :type: :obj:`~hikari.guilds.ActivityType` type: guilds.ActivityType = marshaller.attrib( deserializer=guilds.ActivityType, serializer=int, if_undefined=lambda: guilds.ActivityType.PLAYING, default=guilds.ActivityType.PLAYING, ) + """The activity type.""" diff --git a/hikari/guilds.py b/hikari/guilds.py index 15599a3c68..671f9311a3 100644 --- a/hikari/guilds.py +++ b/hikari/guilds.py @@ -16,14 +16,17 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . -"""Components and entities that are used to describe guilds on Discord. - -.. inheritance-diagram:: - hikari.guilds -""" +"""Components and entities that are used to describe guilds on Discord.""" __all__ = [ + "ActivityAssets", "ActivityFlag", + "ActivitySecret", + "ActivityTimestamps", "ActivityType", + "ActivityParty", + "ClientStatus", + "Guild", + "GuildEmbed", "GuildRole", "GuildFeature", "GuildSystemChannelFlag", @@ -32,15 +35,20 @@ "GuildMFALevel", "GuildVerificationLevel", "GuildPremiumTier", - "Guild", + "GuildPreview", "GuildMember", "GuildMemberPresence", "GuildIntegration", "GuildMemberBan", + "IntegrationAccount", + "IntegrationExpireBehaviour", "PartialGuild", "PartialGuildIntegration", "PartialGuildRole", + "PresenceActivity", "PresenceStatus", + "PresenceUser", + "UnavailableGuild", ] import datetime @@ -65,135 +73,137 @@ class GuildExplicitContentFilterLevel(enum.IntEnum): """Represents the explicit content filter setting for a guild.""" - #: No explicit content filter. DISABLED = 0 + """No explicit content filter.""" - #: Filter posts from anyone without a role. MEMBERS_WITHOUT_ROLES = 1 + """Filter posts from anyone without a role.""" - #: Filter all posts. ALL_MEMBERS = 2 + """Filter all posts.""" @enum.unique class GuildFeature(str, enum.Enum): """Features that a guild can provide.""" - #: Guild has access to set an animated guild icon. ANIMATED_ICON = "ANIMATED_ICON" + """Guild has access to set an animated guild icon.""" - #: Guild has access to set a guild banner image. BANNER = "BANNER" + """Guild has access to set a guild banner image.""" - #: Guild has access to use commerce features (i.e. create store channels). COMMERCE = "COMMERCE" + """Guild has access to use commerce features (i.e. create store channels).""" - #: Guild is able to be discovered in the directory. DISCOVERABLE = "DISCOVERABLE" + """Guild is able to be discovered in the directory.""" - #: Guild is able to be featured in the directory. FEATURABLE = "FEATURABLE" + """Guild is able to be featured in the directory.""" - #: Guild has access to set an invite splash background. INVITE_SPLASH = "INVITE_SPLASH" + """Guild has access to set an invite splash background.""" - #: More emojis can be hosted in this guild than normal. MORE_EMOJI = "MORE_EMOJI" + """More emojis can be hosted in this guild than normal.""" - #: Guild has access to create news channels. NEWS = "NEWS" + """Guild has access to create news channels.""" - #: People can view channels in this guild without joining. LURKABLE = "LURKABLE" + """People can view channels in this guild without joining.""" - #: Guild is partnered. PARTNERED = "PARTNERED" + """Guild is partnered.""" - #: Guild is public, go figure. PUBLIC = "PUBLIC" + """Guild is public, go figure.""" - #: Guild cannot be public. Who would have guessed? PUBLIC_DISABLED = "PUBLIC_DISABLED" + """Guild cannot be public. Who would have guessed?""" - #: Guild has access to set a vanity URL. VANITY_URL = "VANITY_URL" + """Guild has access to set a vanity URL.""" - #: Guild is verified. VERIFIED = "VERIFIED" + """Guild is verified.""" - #: Guild has access to set 384kbps bitrate in voice (previously - #: VIP voice servers). VIP_REGIONS = "VIP_REGIONS" + """Guild has access to set 384kbps bitrate in voice. + + Previously gave access to VIP voice servers. + """ @enum.unique class GuildMessageNotificationsLevel(enum.IntEnum): """Represents the default notification level for new messages in a guild.""" - #: Notify users when any message is sent. ALL_MESSAGES = 0 + """Notify users when any message is sent.""" - #: Only notify users when they are @mentioned. ONLY_MENTIONS = 1 + """Only notify users when they are @mentioned.""" @enum.unique class GuildMFALevel(enum.IntEnum): """Represents the multi-factor authorization requirement for a guild.""" - #: No MFA requirement. NONE = 0 + """No MFA requirement.""" - #: MFA requirement. ELEVATED = 1 + """MFA requirement.""" @enum.unique class GuildPremiumTier(enum.IntEnum): """Tier for Discord Nitro boosting in a guild.""" - #: No Nitro boosts. NONE = 0 + """No Nitro boost level.""" - #: Level 1 Nitro boost. TIER_1 = 1 + """Level 1 Nitro boost.""" - #: Level 2 Nitro boost. TIER_2 = 2 + """Level 2 Nitro boost.""" - #: Level 3 Nitro boost. TIER_3 = 3 + """Level 3 Nitro boost.""" @enum.unique class GuildSystemChannelFlag(enum.IntFlag): """Defines which features are suppressed in the system channel.""" - #: Display a message about new users joining. SUPPRESS_USER_JOIN = 1 << 0 + """Display a message about new users joining.""" - #: Display a message when the guild is Nitro boosted. SUPPRESS_PREMIUM_SUBSCRIPTION = 1 << 1 + """Display a message when the guild is Nitro boosted.""" @enum.unique class GuildVerificationLevel(enum.IntEnum): """Represents the level of verification of a guild.""" - #: Unrestricted NONE = 0 + """Unrestricted""" - #: Must have a verified email on their account. LOW = 1 + """Must have a verified email on their account.""" - #: Must have been registered on Discord for more than 5 minutes. MEDIUM = 2 + """Must have been registered on Discord for more than 5 minutes.""" - #: (╯°□°)╯︵ ┻━┻ - must be a member of the guild for longer than 10 minutes. HIGH = 3 + """(╯°□°)╯︵ ┻━┻ - must be a member of the guild for longer than 10 minutes.""" - #: ┻━┻ミヽ(ಠ益ಠ)ノ彡┻━┻ - must have a verified phone number. VERY_HIGH = 4 + """┻━┻ミヽ(ಠ益ಠ)ノ彡┻━┻ - must have a verified phone number.""" @marshaller.marshallable() @@ -201,17 +211,13 @@ class GuildVerificationLevel(enum.IntEnum): class GuildEmbed(bases.HikariEntity, marshaller.Deserializable): """Represents a guild embed.""" - #: The ID of the channel the invite for this embed targets, if enabled - #: - #: :type: :obj:`~hikari.bases.Snowflake`, optional channel_id: typing.Optional[bases.Snowflake] = marshaller.attrib( deserializer=bases.Snowflake.deserialize, serializer=str, if_none=None ) + """The ID of the channel the invite for this embed targets, if enabled.""" - #: Whether this embed is enabled. - #: - #: :type: :obj:`~bool` is_enabled: bool = marshaller.attrib(raw_name="enabled", deserializer=bool, serializer=bool) + """Whether this embed is enabled.""" @marshaller.marshallable() @@ -219,50 +225,40 @@ class GuildEmbed(bases.HikariEntity, marshaller.Deserializable): class GuildMember(bases.HikariEntity, marshaller.Deserializable): """Used to represent a guild bound member.""" - #: This member's user object, will be :obj:`~None` when attached to Message - #: Create and Update gateway events. - #: - #: :type: :obj:`~hikari.users.User`, optional user: typing.Optional[users.User] = marshaller.attrib( deserializer=users.User.deserialize, if_undefined=None, default=None ) + """This member's user object. + + This will be `None` when attached to Message Create and Update gateway events. + """ - #: This member's nickname, if set. - #: - #: :type: :obj:`~str`, optional nickname: typing.Optional[str] = marshaller.attrib( raw_name="nick", deserializer=str, if_none=None, if_undefined=None, default=None ) + """This member's nickname, if set.""" - #: A sequence of the IDs of the member's current roles. - #: - #: :type: :obj:`~typing.Sequence` [ :obj:`~hikari.bases.Snowflake` ] role_ids: typing.Sequence[bases.Snowflake] = marshaller.attrib( raw_name="roles", deserializer=lambda role_ids: [bases.Snowflake.deserialize(rid) for rid in role_ids], ) + """A sequence of the IDs of the member's current roles.""" - #: The datetime of when this member joined the guild they belong to. - #: - #: :type: :obj:`~datetime.datetime` joined_at: datetime.datetime = marshaller.attrib(deserializer=conversions.parse_iso_8601_ts) + """The datetime of when this member joined the guild they belong to.""" - #: The datetime of when this member started "boosting" this guild. - #: Will be :obj:`~None` if they aren't boosting. - #: - #: :type: :obj:`~datetime.datetime`, optional premium_since: typing.Optional[datetime.datetime] = marshaller.attrib( deserializer=conversions.parse_iso_8601_ts, if_none=None, if_undefined=None, default=None ) + """The datetime of when this member started "boosting" this guild. + + This will be `None` if they aren't boosting. + """ - #: Whether this member is deafened by this guild in it's voice channels. - #: - #: :type: :obj:`~bool` is_deaf: bool = marshaller.attrib(raw_name="deaf", deserializer=bool) + """Whether this member is deafened by this guild in it's voice channels.""" - #: Whether this member is muted by this guild in it's voice channels. - #: - #: :type: :obj:`~bool` is_mute: bool = marshaller.attrib(raw_name="mute", deserializer=bool) + """Whether this member is muted by this guild in it's voice channels.""" @marshaller.marshallable() @@ -270,10 +266,8 @@ class GuildMember(bases.HikariEntity, marshaller.Deserializable): class PartialGuildRole(bases.UniqueEntity, marshaller.Deserializable): """Represents a partial guild bound Role object.""" - #: The role's name. - #: - #: :type: :obj:`~str` name: str = marshaller.attrib(deserializer=str, serializer=str) + """The role's name.""" @marshaller.marshallable() @@ -281,71 +275,60 @@ class PartialGuildRole(bases.UniqueEntity, marshaller.Deserializable): class GuildRole(PartialGuildRole, marshaller.Serializable): """Represents a guild bound Role object.""" - #: The colour of this role, will be applied to a member's name in chat - #: if it's their top coloured role. - #: - #: :type: :obj:`~hikari.colors.Color` color: colors.Color = marshaller.attrib(deserializer=colors.Color, serializer=int, default=colors.Color(0)) + """The colour of this role. + + This will be applied to a member's name in chat if it's their top coloured role.""" - #: Whether this role is hoisting the members it's attached to in the member - #: list, members will be hoisted under their highest role where - #: :attr:`is_hoisted` is true. - #: - #: :type: :obj:`~bool` is_hoisted: bool = marshaller.attrib(raw_name="hoist", deserializer=bool, serializer=bool, default=False) + """Whether this role is hoisting the members it's attached to in the member list. + + members will be hoisted under their highest role where this is set to `True`.""" - #: The position of this role in the role hierarchy. - #: - #: :type: :obj:`~int` position: int = marshaller.attrib(deserializer=int, serializer=int, default=None) + """The position of this role in the role hierarchy.""" - #: The guild wide permissions this role gives to the members it's attached - #: to, may be overridden by channel overwrites. - #: - #: :type: :obj:`~hikari.permissions.Permission` permissions: _permissions.Permission = marshaller.attrib( deserializer=_permissions.Permission, serializer=int, default=_permissions.Permission(0) ) + """The guild wide permissions this role gives to the members it's attached to, + + This may be overridden by channel overwrites. + """ - #: Whether this role is managed by an integration. - #: - #: :type: :obj:`~bool` is_managed: bool = marshaller.attrib(raw_name="managed", deserializer=bool, transient=True, default=None) + """Whether this role is managed by an integration.""" - #: Whether this role can be mentioned by all, regardless of the - #: ``MENTION_EVERYONE`` permission. - #: - #: :type: :obj:`~bool` is_mentionable: bool = marshaller.attrib(raw_name="mentionable", deserializer=bool, serializer=bool, default=False) + """Whether this role can be mentioned by all regardless of permissions.""" @enum.unique class ActivityType(enum.IntEnum): """The activity type.""" - #: Shows up as ``Playing `` PLAYING = 0 + """Shows up as `Playing `""" - #: Shows up as ``Streaming ``. - #: - #: Warning - #: ------- - #: Corresponding presences must be associated with VALID Twitch or YouTube - #: stream URLS! STREAMING = 1 - #: Shows up as ``Listening to ``. LISTENING = 2 + """Shows up as `Listening to `.""" - #: Shows up as ``Watching ``. Note that this is not officially - #: documented, so will be likely removed in the near future. WATCHING = 3 + """Shows up as `Watching `. + + !!! note + this is not officially documented, so will be likely removed in the near + future. + """ - #: A custom status. - #: - #: To set an emoji with the status, place a unicode emoji or Discord emoji - #: (``:smiley:``) as the first part of the status activity name. CUSTOM = 4 + """A custom status. + + To set an emoji with the status, place a unicode emoji or Discord emoji + (`:smiley:`) as the first part of the status activity name. + """ @marshaller.marshallable() @@ -353,19 +336,15 @@ class ActivityType(enum.IntEnum): class ActivityTimestamps(bases.HikariEntity, marshaller.Deserializable): """The datetimes for the start and/or end of an activity session.""" - #: When this activity's session was started, if applicable. - #: - #: :type: :obj:`~datetime.datetime`, optional start: typing.Optional[datetime.datetime] = marshaller.attrib( deserializer=conversions.unix_epoch_to_datetime, if_undefined=None, default=None ) + """When this activity's session was started, if applicable.""" - #: When this activity's session will end, if applicable. - #: - #: :type: :obj:`~datetime.datetime`, optional end: typing.Optional[datetime.datetime] = marshaller.attrib( deserializer=conversions.unix_epoch_to_datetime, if_undefined=None, default=None ) + """When this activity's session will end, if applicable.""" @marshaller.marshallable() @@ -373,17 +352,13 @@ class ActivityTimestamps(bases.HikariEntity, marshaller.Deserializable): class ActivityParty(bases.HikariEntity, marshaller.Deserializable): """Used to represent activity groups of users.""" - #: The string id of this party instance, if set. - #: - #: :type: :obj:`~str`, optional id: typing.Optional[str] = marshaller.attrib(deserializer=str, if_undefined=None, default=None) + """The string id of this party instance, if set.""" - #: The size metadata of this party, if applicable. - #: - #: :type: :obj:`~typing.Tuple` [ :obj:`~int`, :obj:`~int` ], optional _size_information: typing.Optional[typing.Tuple[int, int]] = marshaller.attrib( raw_name="size", deserializer=tuple, if_undefined=None, default=None ) + """The size metadata of this party, if applicable.""" # Ignore docstring not starting in an imperative mood @property @@ -402,25 +377,17 @@ def max_size(self) -> typing.Optional[int]: class ActivityAssets(bases.HikariEntity, marshaller.Deserializable): """Used to represent possible assets for an activity.""" - #: The ID of the asset's large image, if set. - #: - #: :type: :obj:`~str`, optional large_image: typing.Optional[str] = marshaller.attrib(deserializer=str, if_undefined=None, default=None) + """The ID of the asset's large image, if set.""" - #: The text that'll appear when hovering over the large image, if set. - #: - #: :type: :obj:`~str`, optional large_text: typing.Optional[str] = marshaller.attrib(deserializer=str, if_undefined=None, default=None) + """The text that'll appear when hovering over the large image, if set.""" - #: The ID of the asset's small image, if set. - #: - #: :type: :obj:`~str`, optional small_image: typing.Optional[str] = marshaller.attrib(deserializer=str, if_undefined=None, default=None) + """The ID of the asset's small image, if set.""" - #: The text that'll appear when hovering over the small image, if set. - #: - #: :type: :obj:`~str`, optional small_text: typing.Optional[str] = marshaller.attrib(deserializer=str, if_undefined=None, default=None) + """The text that'll appear when hovering over the small image, if set.""" @marshaller.marshallable() @@ -428,20 +395,14 @@ class ActivityAssets(bases.HikariEntity, marshaller.Deserializable): class ActivitySecret(bases.HikariEntity, marshaller.Deserializable): """The secrets used for interacting with an activity party.""" - #: The secret used for joining a party, if applicable. - #: - #: :type: :obj:`~str`, optional join: typing.Optional[str] = marshaller.attrib(deserializer=str, if_undefined=None, default=None) + """The secret used for joining a party, if applicable.""" - #: The secret used for spectating a party, if applicable. - #: - #: :type: :obj:`~str`, optional spectate: typing.Optional[str] = marshaller.attrib(deserializer=str, if_undefined=None, default=None) + """The secret used for spectating a party, if applicable.""" - #: The secret used for joining a party, if applicable. - #: - #: :type: :obj:`~str`, optional match: typing.Optional[str] = marshaller.attrib(deserializer=str, if_undefined=None, default=None) + """The secret used for joining a party, if applicable.""" @enum.unique @@ -451,23 +412,23 @@ class ActivityFlag(enum.IntFlag): This can be more than one using bitwise-combinations. """ - #: Instance INSTANCE = 1 << 0 + """Instance""" - #: Join JOIN = 1 << 1 + """Join""" - #: Spectate SPECTATE = 1 << 2 + """Spectate""" - #: Join Request JOIN_REQUEST = 1 << 3 + """Join Request""" - #: Sync SYNC = 1 << 4 + """Sync""" - #: Play PLAY = 1 << 5 + """Play""" @marshaller.marshallable() @@ -475,109 +436,82 @@ class ActivityFlag(enum.IntFlag): class PresenceActivity(bases.HikariEntity, marshaller.Deserializable): """Represents an activity that will be attached to a member's presence.""" - #: The activity's name. - #: - #: :type: :obj:`~str` name: str = marshaller.attrib(deserializer=str) + """The activity's name.""" - #: The activity's type. - #: - #: :type: :obj:`~ActivityType` type: ActivityType = marshaller.attrib(deserializer=ActivityType) + """The activity's type.""" - #: The URL for a ``STREAM`` type activity, if applicable. - #: - #: :type: :obj:`~str`, optional url: typing.Optional[str] = marshaller.attrib(deserializer=str, if_undefined=None, if_none=None, default=None) + """The URL for a `STREAM` type activity, if applicable.""" - #: When this activity was added to the user's session. - #: - #: :type: :obj:`~datetime.datetime` created_at: datetime.datetime = marshaller.attrib(deserializer=conversions.unix_epoch_to_datetime) + """When this activity was added to the user's session.""" - #: The timestamps for when this activity's current state will start and - #: end, if applicable. - #: - #: :type: :obj:`~ActivityTimestamps`, optional timestamps: typing.Optional[ActivityTimestamps] = marshaller.attrib( deserializer=ActivityTimestamps.deserialize, if_undefined=None, default=None ) + """The timestamps for when this activity's current state will start and + end, if applicable. + """ - #: The ID of the application this activity is for, if applicable. - #: - #: :type: :obj:`~hikari.bases.Snowflake`, optional application_id: typing.Optional[bases.Snowflake] = marshaller.attrib( deserializer=bases.Snowflake.deserialize, if_undefined=None, default=None ) + """The ID of the application this activity is for, if applicable.""" - #: The text that describes what the activity's target is doing, if set. - #: - #: :type: :obj:`~str`, optional details: typing.Optional[str] = marshaller.attrib(deserializer=str, if_undefined=None, if_none=None, default=None) + """The text that describes what the activity's target is doing, if set.""" - #: The current status of this activity's target, if set. - #: - #: :type: :obj:`~str`, optional state: typing.Optional[str] = marshaller.attrib(deserializer=str, if_undefined=None, if_none=None, default=None) + """The current status of this activity's target, if set.""" - #: The emoji of this activity, if it is a custom status and set. - #: - #: :type: :obj:`~typing.Union` [ :obj:`~hikari.emojis.UnicodeEmoji`, :obj:`~hikari.emojis.UnknownEmoji` ], optional emoji: typing.Union[None, _emojis.UnicodeEmoji, _emojis.UnknownEmoji] = marshaller.attrib( deserializer=_emojis.deserialize_reaction_emoji, if_undefined=None, default=None ) + """The emoji of this activity, if it is a custom status and set.""" - #: Information about the party associated with this activity, if set. - #: - #: :type: :obj:`~ActivityParty`, optional party: typing.Optional[ActivityParty] = marshaller.attrib( deserializer=ActivityParty.deserialize, if_undefined=None, default=None ) + """Information about the party associated with this activity, if set.""" - #: Images and their hover over text for the activity. - #: - #: :type: :obj:`~ActivityAssets`, optional assets: typing.Optional[ActivityAssets] = marshaller.attrib( deserializer=ActivityAssets.deserialize, if_undefined=None, default=None ) + """Images and their hover over text for the activity.""" - #: Secrets for Rich Presence joining and spectating. - #: - #: :type: :obj:`~ActivitySecret`, optional secrets: typing.Optional[ActivitySecret] = marshaller.attrib( deserializer=ActivitySecret.deserialize, if_undefined=None, default=None ) + """Secrets for Rich Presence joining and spectating.""" - #: Whether this activity is an instanced game session. - #: - #: :type: :obj:`~bool`, optional is_instance: typing.Optional[bool] = marshaller.attrib( raw_name="instance", deserializer=bool, if_undefined=None, default=None ) + """Whether this activity is an instanced game session.""" - #: Flags that describe what the activity includes. - #: - #: :type: :obj:`~ActivityFlag` flags: ActivityFlag = marshaller.attrib(deserializer=ActivityFlag, if_undefined=None, default=None) + """Flags that describe what the activity includes.""" class PresenceStatus(str, enum.Enum): """The status of a member.""" - #: Online/green. ONLINE = "online" + """Online/green.""" - #: Idle/yellow. IDLE = "idle" + """Idle/yellow.""" - #: Do not disturb/red. DND = "dnd" + """Do not disturb/red.""" - #: An alias for :attr:`DND` DO_NOT_DISTURB = DND + """An alias for `PresenceStatus.DND`""" - #: Offline or invisible/grey. OFFLINE = "offline" + """Offline or invisible/grey.""" @marshaller.marshallable() @@ -585,26 +519,20 @@ class PresenceStatus(str, enum.Enum): class ClientStatus(bases.HikariEntity, marshaller.Deserializable): """The client statuses for this member.""" - #: The status of the target user's desktop session. - #: - #: :type: :obj:`~PresenceStatus` desktop: PresenceStatus = marshaller.attrib( deserializer=PresenceStatus, if_undefined=lambda: PresenceStatus.OFFLINE, default=PresenceStatus.OFFLINE ) + """The status of the target user's desktop session.""" - #: The status of the target user's mobile session. - #: - #: :type: :obj:`~PresenceStatus` mobile: PresenceStatus = marshaller.attrib( deserializer=PresenceStatus, if_undefined=lambda: PresenceStatus.OFFLINE, default=PresenceStatus.OFFLINE ) + """The status of the target user's mobile session.""" - #: The status of the target user's web session. - #: - #: :type: :obj:`~PresenceStatus` web: PresenceStatus = marshaller.attrib( deserializer=PresenceStatus, if_undefined=lambda: PresenceStatus.OFFLINE, default=PresenceStatus.OFFLINE ) + """The status of the target user's web session.""" @marshaller.marshallable() @@ -612,62 +540,48 @@ class ClientStatus(bases.HikariEntity, marshaller.Deserializable): class PresenceUser(users.User): """A user representation specifically used for presence updates. - Warnings - -------- - Every attribute except ``id`` may be received as :obj:`~hikari.unset.UNSET` - unless it is specifically being modified for this update. + !!! warning + Every attribute except `PresenceUser.id` may be as `hikari.unset.UNSET` + unless it is specifically being modified for this update. """ - #: This user's discriminator. - #: - #: :type: :obj:`~typing.Union` [ :obj:`~str`, :obj:`~hikari.unset.UNSET` ] discriminator: typing.Union[str, unset.Unset] = marshaller.attrib( deserializer=str, if_undefined=unset.Unset, default=unset.UNSET ) + """This user's discriminator.""" - #: This user's username. - #: - #: :type: :obj:`~typing.Union` [ :obj:`~str`, :obj:`~hikari.unset.UNSET` ] username: typing.Union[str, unset.Unset] = marshaller.attrib( deserializer=str, if_undefined=unset.Unset, default=unset.UNSET ) + """This user's username.""" - #: This user's avatar hash, if set. - #: - #: :type: :obj:`~typing.Union` [ :obj:`~str`, :obj:`~hikari.unset.UNSET` ], optional avatar_hash: typing.Union[None, str, unset.Unset] = marshaller.attrib( raw_name="avatar", deserializer=str, if_none=None, if_undefined=unset.Unset, default=unset.UNSET ) + """This user's avatar hash, if set.""" - #: Whether this user is a bot account. - #: - #: :type: :obj:`~typing.Union` [ :obj:`~bool`, :obj:`~hikari.unset.UNSET` ] is_bot: typing.Union[bool, unset.Unset] = marshaller.attrib( raw_name="bot", deserializer=bool, if_undefined=unset.Unset, default=unset.UNSET ) + """Whether this user is a bot account.""" - #: Whether this user is a system account. - #: - #: :type: :obj:`~typing.Union` [ :obj:`~bool`, :obj:`~hikari.unset.UNSET` ] is_system: typing.Union[bool, unset.Unset] = marshaller.attrib( raw_name="system", deserializer=bool, if_undefined=unset.Unset, default=unset.UNSET ) + """Whether this user is a system account.""" - #: The public flags for this user. - #: - #: :type: :obj:`~typing.Union` [ :obj:`~hikari.users.UserFlag`, :obj:`~hikari.unset.UNSET` ] flags: typing.Union[users.UserFlag, unset.Unset] = marshaller.attrib( raw_name="public_flags", deserializer=users.UserFlag, if_undefined=unset.Unset ) + """The public flags for this user.""" @property def avatar_url(self) -> typing.Union[str, unset.Unset]: """URL for this user's avatar if the relevant info is available. - Note - ---- - This will be :obj:`~hikari.unset.UNSET` if both :attr:`avatar_hash` - and :attr:`discriminator` are :obj:`~hikari.unset.UNSET`. + !!! note + This will be `hikari.unset.UNSET` if both `PresenceUser.avatar_hash` + and `PresenceUser.discriminator` are `hikari.unset.UNSET`. """ return self.format_avatar_url() @@ -676,27 +590,26 @@ def format_avatar_url(self, fmt: typing.Optional[str] = None, size: int = 4096) Parameters ---------- - fmt : :obj:`~str` - The format to use for this URL, defaults to ``png`` or ``gif``. - Supports ``png``, ``jpeg``, ``jpg``, ``webp`` and ``gif`` (when - animated). Will be ignored for default avatars which can only be - ``png``. - size : :obj:`~int` - The size to set for the URL, defaults to ``4096``. + fmt : str + The format to use for this URL, defaults to `png` or `gif`. + Supports `png`, `jpeg`, `jpg`, `webp` and `gif` (when animated). + Will be ignored for default avatars which can only be `png`. + size : int + The size to set for the URL, defaults to `4096`. Can be any power of two between 16 and 4096. Will be ignored for default avatars. Returns ------- - :obj:`~typing.Union` [ :obj:`~str`, :obj:`~hikari.unset.UNSET` ] + typing.Union [ str, hikari.unset.UNSET ] The string URL of the user's custom avatar if - either :attr:`avatar_hash` is set or their default avatar if - :attr:`discriminator` is set, else :obj:`~hikari.unset.UNSET`. + either `PresenceUser.avatar_hash` is set or their default avatar if + `PresenceUser.discriminator` is set, else `hikari.unset.UNSET`. Raises ------ - :obj:`~ValueError` - If ``size`` is not a power of two or not between 16 and 4096. + ValueError + If `size` is not a power of two or not between 16 and 4096. """ if self.discriminator is unset.UNSET and self.avatar_hash is unset.UNSET: return unset.UNSET @@ -706,10 +619,9 @@ def format_avatar_url(self, fmt: typing.Optional[str] = None, size: int = 4096) def default_avatar(self) -> typing.Union[int, unset.Unset]: """Integer representation of this user's default avatar. - Note - ---- - This will be :obj:`~hikari.unset.UNSET` if :attr:`discriminator` is - :obj:`~hikari.unset.UNSET`. + !!! note + This will be `hikari.unset.UNSET` if `PresenceUser.discriminator` is + `hikari.unset.UNSET`. """ if self.discriminator is not unset.UNSET: return int(self.discriminator) % 5 @@ -721,69 +633,59 @@ def default_avatar(self) -> typing.Union[int, unset.Unset]: class GuildMemberPresence(bases.HikariEntity, marshaller.Deserializable): """Used to represent a guild member's presence.""" - #: The object of the user who this presence is for, only `id` is guaranteed - #: for this partial object, with other attributes only being included when - #: when they are being changed in an event. - #: - #: :type: :obj:`~PresenceUser` user: PresenceUser = marshaller.attrib(deserializer=PresenceUser.deserialize) + """The object of the user who this presence is for. + + !!! info + Only `PresenceUser.id` is guaranteed for this partial object, + with other attributes only being included when when they are being + changed in an event. + """ - #: A sequence of the ids of the user's current roles in the guild this - #: presence belongs to. - #: - #: :type: :obj:`~typing.Sequence` [ :obj:`~hikari.bases.Snowflake` ] role_ids: typing.Sequence[bases.Snowflake] = marshaller.attrib( raw_name="roles", deserializer=lambda roles: [bases.Snowflake.deserialize(rid) for rid in roles], ) + """The ids of the user's current roles in the guild this presence belongs to.""" - #: The ID of the guild this presence belongs to. - #: - #: :type: :obj:`~hikari.bases.Snowflake` guild_id: bases.Snowflake = marshaller.attrib(deserializer=bases.Snowflake.deserialize) + """The ID of the guild this presence belongs to.""" - #: This user's current status being displayed by the client. - #: - #: :type: :obj:`~PresenceStatus` visible_status: PresenceStatus = marshaller.attrib(raw_name="status", deserializer=PresenceStatus) + """This user's current status being displayed by the client.""" - #: An array of the user's activities, with the top one will being - #: prioritised by the client. - #: - #: :type: :obj:`~typing.Sequence` [ :obj:`~PresenceActivity` ] activities: typing.Sequence[PresenceActivity] = marshaller.attrib( deserializer=lambda activities: [PresenceActivity.deserialize(a) for a in activities] ) + """An array of the user's activities, with the top one will being + prioritised by the client. + """ - #: An object of the target user's client statuses. - #: - #: :type: :obj:`~ClientStatus` client_status: ClientStatus = marshaller.attrib(deserializer=ClientStatus.deserialize) + """An object of the target user's client statuses.""" - #: The datetime of when this member started "boosting" this guild. - #: Will be :obj:`~None` if they aren't boosting. - #: - #: :type: :obj:`~datetime.datetime`, optional premium_since: typing.Optional[datetime.datetime] = marshaller.attrib( deserializer=conversions.parse_iso_8601_ts, if_undefined=None, if_none=None, default=None ) + """The datetime of when this member started "boosting" this guild. + + This will be `None` if they aren't boosting. + """ - #: This member's nickname, if set. - #: - #: :type: :obj:`~str`, optional nick: typing.Optional[str] = marshaller.attrib( raw_name="nick", deserializer=str, if_undefined=None, if_none=None, default=None ) + """This member's nickname, if set.""" @enum.unique class IntegrationExpireBehaviour(enum.IntEnum): """Behavior for expiring integration subscribers.""" - #: Remove the role. REMOVE_ROLE = 0 + """Remove the role.""" - #: Kick the subscriber. KICK = 1 + """Kick the subscriber.""" @marshaller.marshallable() @@ -791,15 +693,11 @@ class IntegrationExpireBehaviour(enum.IntEnum): class IntegrationAccount(bases.HikariEntity, marshaller.Deserializable): """An account that's linked to an integration.""" - #: The string ID of this (likely) third party account. - #: - #: :type: :obj:`~str` id: str = marshaller.attrib(deserializer=str) + """The string ID of this (likely) third party account.""" - #: The name of this account. - #: - #: :type: :obj:`~str` name: str = marshaller.attrib(deserializer=str) + """The name of this account.""" @marshaller.marshallable() @@ -807,20 +705,14 @@ class IntegrationAccount(bases.HikariEntity, marshaller.Deserializable): class PartialGuildIntegration(bases.UniqueEntity, marshaller.Deserializable): """A partial representation of an integration, found in audit logs.""" - #: The name of this integration. - #: - #: :type: :obj:`~str` name: str = marshaller.attrib(deserializer=str) + """The name of this integration.""" - #: The type of this integration. - #: - #: :type: :obj:`~str` type: str = marshaller.attrib(deserializer=str) + """The type of this integration.""" - #: The account connected to this integration. - #: - #: :type: :obj:`~IntegrationAccount` account: IntegrationAccount = marshaller.attrib(deserializer=IntegrationAccount.deserialize) + """The account connected to this integration.""" @marshaller.marshallable() @@ -828,54 +720,41 @@ class PartialGuildIntegration(bases.UniqueEntity, marshaller.Deserializable): class GuildIntegration(bases.UniqueEntity, marshaller.Deserializable): """Represents a guild integration object.""" - #: Whether this integration is enabled. - #: - #: :type: :obj:`~bool` is_enabled: bool = marshaller.attrib(raw_name="enabled", deserializer=bool) + """Whether this integration is enabled.""" - #: Whether this integration is syncing subscribers/emojis. - #: - #: :type: :obj:`~bool` is_syncing: bool = marshaller.attrib(raw_name="syncing", deserializer=bool) + """Whether this integration is syncing subscribers/emojis.""" - #: The ID of the managed role used for this integration's subscribers. - #: - #: :type: :obj:`~hikari.bases.Snowflake` role_id: typing.Optional[bases.Snowflake] = marshaller.attrib(deserializer=bases.Snowflake.deserialize) + """The ID of the managed role used for this integration's subscribers.""" - #: Whether users under this integration are allowed to use it's custom - #: emojis. - #: - #: :type: :obj:`~bool`, optional is_emojis_enabled: typing.Optional[bool] = marshaller.attrib( raw_name="enable_emoticons", deserializer=bool, if_undefined=None, default=None ) + """Whether users under this integration are allowed to use it's custom emojis.""" - #: How members should be treated after their connected subscription expires - #: This won't be enacted until after :attr:`expire_grace_period` passes. - #: - #: :type: :obj:`~IntegrationExpireBehaviour` expire_behavior: IntegrationExpireBehaviour = marshaller.attrib(deserializer=IntegrationExpireBehaviour) + """How members should be treated after their connected subscription expires. + + This won't be enacted until after `GuildIntegration.expire_grace_period` + passes. + """ - #: The time delta for how many days users with expired subscriptions are - #: given until :attr:`expire_behavior` is enacted out on them - #: - #: :type: :obj:`~datetime.timedelta` expire_grace_period: datetime.timedelta = marshaller.attrib( deserializer=lambda delta: datetime.timedelta(days=delta), ) + """How many days users with expired subscriptions are given until + `GuildIntegration.expire_behavior` is enacted out on them + """ - #: The user this integration belongs to. - #: - #: :type: :obj:`~hikari.users.User` user: users.User = marshaller.attrib(deserializer=users.User.deserialize) + """The user this integration belongs to.""" - #: The datetime of when this integration's subscribers were last synced. - #: - #: :type: :obj:`~datetime.datetime` last_synced_at: datetime.datetime = marshaller.attrib( raw_name="synced_at", deserializer=conversions.parse_iso_8601_ts, if_none=None ) + """The datetime of when this integration's subscribers were last synced.""" @marshaller.marshallable() @@ -883,15 +762,11 @@ class GuildIntegration(bases.UniqueEntity, marshaller.Deserializable): class GuildMemberBan(bases.HikariEntity, marshaller.Deserializable): """Used to represent guild bans.""" - #: The reason for this ban, will be :obj:`~None` if no reason was given. - #: - #: :type: :obj:`~str`, optional reason: str = marshaller.attrib(deserializer=str, if_none=None) + """The reason for this ban, will be `None` if no reason was given.""" - #: The object of the user this ban targets. - #: - #: :type: :obj:`~hikari.users.User` user: users.User = marshaller.attrib(deserializer=users.User.deserialize) + """The object of the user this ban targets.""" @marshaller.marshallable() @@ -906,9 +781,9 @@ class UnavailableGuild(bases.UniqueEntity, marshaller.Deserializable): # Ignore docstring not starting in an imperative mood @property def is_unavailable(self) -> bool: # noqa: D401 - """:obj:`~True` if this guild is unavailable, else :obj:`~False`. + """`True` if this guild is unavailable, else `False`. - This value is always :obj:`~True`, and is only provided for consistency. + This value is always `True`, and is only provided for consistency. """ return True @@ -918,45 +793,39 @@ def is_unavailable(self) -> bool: # noqa: D401 class PartialGuild(bases.UniqueEntity, marshaller.Deserializable): """Base object for any partial guild objects.""" - #: The name of the guild. - #: - #: :type: :obj:`~str` name: str = marshaller.attrib(deserializer=str) + """The name of the guild.""" - #: The hash for the guild icon, if there is one. - #: - #: :type: :obj:`~str`, optional icon_hash: typing.Optional[str] = marshaller.attrib(raw_name="icon", deserializer=str, if_none=None) + """The hash for the guild icon, if there is one.""" - #: A set of the features in this guild. - #: - #: :type: :obj:`~typing.Set` [ :obj:`~GuildFeature` ] features: typing.Set[GuildFeature] = marshaller.attrib( deserializer=lambda features: {conversions.try_cast(f, GuildFeature, f) for f in features}, ) + """A set of the features in this guild.""" def format_icon_url(self, fmt: typing.Optional[str] = None, size: int = 4096) -> typing.Optional[str]: """Generate the URL for this guild's custom icon, if set. Parameters ---------- - fmt : :obj:`~str` - The format to use for this URL, defaults to ``png`` or ``gif``. - Supports ``png``, ``jpeg``, `jpg`, ``webp`` and ``gif`` (when + fmt : str + The format to use for this URL, defaults to `png` or `gif`. + Supports `png`, `jpeg`, `jpg`, `webp` and `gif` (when animated). - size : :obj:`~int` - The size to set for the URL, defaults to ``4096``. + size : int + The size to set for the URL, defaults to `4096`. Can be any power of two between 16 and 4096. Returns ------- - :obj:`~str`, optional + str, optional The string URL. Raises ------ - :obj:`~ValueError` - If ``size`` is not a power of two or not between 16 and 4096. + ValueError + If `size` is not a power of two or not between 16 and 4096. """ if self.icon_hash: if fmt is None and self.icon_hash.startswith("a_"): @@ -975,66 +844,51 @@ def icon_url(self) -> typing.Optional[str]: @marshaller.marshallable() @attr.s(slots=True, kw_only=True) class GuildPreview(PartialGuild): - """A preview of a guild with the :obj:`~GuildFeature.PUBLIC` feature.""" + """A preview of a guild with the `GuildFeature.PUBLIC` feature.""" - #: The hash of the splash for the guild, if there is one. - #: - #: :type: :obj:`~str`, optional splash_hash: typing.Optional[str] = marshaller.attrib(raw_name="splash", deserializer=str, if_none=None) + """The hash of the splash for the guild, if there is one.""" - #: The hash of the discovery splash for the guild, if there is one. - #: - #: :type: :obj:`~str`, optional discovery_splash_hash: typing.Optional[str] = marshaller.attrib( raw_name="discovery_splash", deserializer=str, if_none=None ) + """The hash of the discovery splash for the guild, if there is one.""" - #: The emojis that this guild provides, represented as a mapping of ID to - #: emoji object. - #: - #: :type: :obj:`~typing.Mapping` [ :obj:`~hikari.bases.Snowflake`, :obj:`~hikari.emojis.GuildEmoji` ] emojis: typing.Mapping[bases.Snowflake, _emojis.GuildEmoji] = marshaller.attrib( deserializer=lambda emojis: {e.id: e for e in map(_emojis.GuildEmoji.deserialize, emojis)}, ) + """The mapping of IDs to the emojis this guild provides.""" + + approximate_presence_count: int = marshaller.attrib(deserializer=int) + """The approximate amount of presences in guild.""" + + approximate_member_count: int = marshaller.attrib(deserializer=int) + """The approximate amount of members in this guild.""" - #: The approximate amount of presences in this invite's guild, only present - #: when ``with_counts`` is passed as ``True`` to the GET Invites endpoint. - #: - #: :type: :obj:`~int`, optional - approximate_presence_count: typing.Optional[int] = marshaller.attrib(deserializer=int) - - #: The approximate amount of members in this invite's guild, only present - #: when ``with_counts`` is passed as ``True`` to the GET Invites endpoint. - #: - #: :type: :obj:`~int`, optional - approximate_member_count: typing.Optional[int] = marshaller.attrib(deserializer=int) - - #: The guild's description, if set. - #: - #: :type: :obj:`~str`, optional description: typing.Optional[str] = marshaller.attrib(if_none=None, deserializer=str) + """The guild's description, if set.""" def format_splash_url(self, fmt: str = "png", size: int = 4096) -> typing.Optional[str]: """Generate the URL for this guild's splash image, if set. Parameters ---------- - fmt : :obj:`~str` - The format to use for this URL, defaults to ``png``. - Supports ``png``, ``jpeg``, ``jpg`` and ``webp``. - size : :obj:`~int` - The size to set for the URL, defaults to ``4096``. + fmt : str + The format to use for this URL, defaults to `png`. + Supports `png`, `jpeg`, `jpg` and `webp`. + size : int + The size to set for the URL, defaults to `4096`. Can be any power of two between 16 and 4096. Returns ------- - :obj:`~str`, optional + str, optional The string URL. Raises ------ - :obj:`~ValueError` - If ``size`` is not a power of two or not between 16 and 4096. + ValueError + If `size` is not a power of two or not between 16 and 4096. """ if self.splash_hash: return urls.generate_cdn_url("splashes", str(self.id), self.splash_hash, fmt=fmt, size=size) @@ -1050,22 +904,22 @@ def format_discovery_splash_url(self, fmt: str = "png", size: int = 4096) -> typ Parameters ---------- - fmt : :obj:`~str` - The format to use for this URL, defaults to ``png``. - Supports ``png``, ``jpeg``, ``jpg`` and ``webp``. - size : :obj:`~int` - The size to set for the URL, defaults to ``4096``. + fmt : str + The format to use for this URL, defaults to `png`. + Supports `png`, `jpeg`, `jpg` and `webp`. + size : int + The size to set for the URL, defaults to `4096`. Can be any power of two between 16 and 4096. Returns ------- - :obj:`~str`, optional + str, optional The string URL. Raises ------ - :obj:`~ValueError` - If ``size`` is not a power of two or not between 16 and 4096. + ValueError + If `size` is not a power of two or not between 16 and 4096. """ if self.discovery_splash_hash: return urls.generate_cdn_url( @@ -1084,384 +938,332 @@ def discovery_splash_url(self) -> typing.Optional[str]: class Guild(PartialGuild): """A representation of a guild on Discord. - Note - ---- - If a guild object is considered to be unavailable, then the state of any - other fields other than the :attr:`is_unavailable` and ``id`` are outdated - or incorrect. If a guild is unavailable, then the contents of any other - fields should be ignored. + !!! note + If a guild object is considered to be unavailable, then the state of any + other fields other than the `Guild.is_unavailable` and `Guild.id` are + outdated or incorrect. If a guild is unavailable, then the contents of + any other fields should be ignored. """ - #: The hash of the splash for the guild, if there is one. - #: - #: :type: :obj:`~str`, optional splash_hash: typing.Optional[str] = marshaller.attrib(raw_name="splash", deserializer=str, if_none=None) + """The hash of the splash for the guild, if there is one.""" - #: The hash of the discovery splash for the guild, if there is one. - #: - #: :type: :obj:`~str`, optional discovery_splash_hash: typing.Optional[str] = marshaller.attrib( raw_name="discovery_splash", deserializer=str, if_none=None ) + """The hash of the discovery splash for the guild, if there is one.""" - #: The ID of the owner of this guild. - #: - #: :type: :obj:`~hikari.bases.Snowflake` owner_id: bases.Snowflake = marshaller.attrib(deserializer=bases.Snowflake.deserialize) + """The ID of the owner of this guild.""" - #: The guild level permissions that apply to the bot user, - #: Will be :obj:`~None` when this object is retrieved through a REST request - #: rather than from the gateway. - #: - #: :type: :obj:`~hikari.permissions.Permission` my_permissions: _permissions.Permission = marshaller.attrib( raw_name="permissions", deserializer=_permissions.Permission, if_undefined=None, default=None ) + """The guild level permissions that apply to the bot user. + + This will be `None` when this object is retrieved through a REST request + rather than from the gateway. + """ - #: The voice region for the guild. - #: - #: :type: :obj:`~str` region: str = marshaller.attrib(deserializer=str) + """The voice region for the guild.""" - #: The ID for the channel that AFK voice users get sent to, if set for the - #: guild. - #: - #: :type: :obj:`~hikari.bases.Snowflake`, optional afk_channel_id: typing.Optional[bases.Snowflake] = marshaller.attrib( deserializer=bases.Snowflake.deserialize, if_none=None ) + """The ID for the channel that AFK voice users get sent to, if set.""" - #: How long a voice user has to be AFK for before they are classed as being - #: AFK and are moved to the AFK channel (:attr:`afk_channel_id`). - #: - #: :type: :obj:`~datetime.timedelta` afk_timeout: datetime.timedelta = marshaller.attrib( deserializer=lambda seconds: datetime.timedelta(seconds=seconds) ) + """How long a voice user has to be AFK for before they are classed as being + AFK and are moved to the AFK channel (`Guild.afk_channel_id`). + """ # TODO: document when this is not specified. - #: Defines if the guild embed is enabled or not. - #: - #: This information may not be present, in which case, - #: it will be :obj:`~None` instead. - #: - #: :type: :obj:`~bool`, optional is_embed_enabled: typing.Optional[bool] = marshaller.attrib( raw_name="embed_enabled", deserializer=bool, if_undefined=False, default=False ) + """Defines if the guild embed is enabled or not. + + This information may not be present, in which case, it will be `None` + instead. + """ - #: The channel ID that the guild embed will generate an invite to, if - #: enabled for this guild. - #: - #: Will be :obj:`~None` if invites are disabled for this guild's embed. - #: - #: :type: :obj:`~hikari.bases.Snowflake`, optional embed_channel_id: typing.Optional[bases.Snowflake] = marshaller.attrib( deserializer=bases.Snowflake.deserialize, if_undefined=None, if_none=None, default=None ) + """The channel ID that the guild embed will generate an invite to. + + Will be `None` if invites are disabled for this guild's embed. + """ - #: The verification level required for a user to participate in this guild. - #: - #: :type: :obj:`~GuildVerificationLevel` verification_level: GuildVerificationLevel = marshaller.attrib(deserializer=GuildVerificationLevel) + """The verification level required for a user to participate in this guild.""" - #: The default setting for message notifications in this guild. - #: - #: :type: :obj:`~GuildMessageNotificationsLevel` default_message_notifications: GuildMessageNotificationsLevel = marshaller.attrib( deserializer=GuildMessageNotificationsLevel ) + """The default setting for message notifications in this guild.""" - #: The setting for the explicit content filter in this guild. - #: - #: :type: :obj:`~GuildExplicitContentFilterLevel` explicit_content_filter: GuildExplicitContentFilterLevel = marshaller.attrib( deserializer=GuildExplicitContentFilterLevel ) + """The setting for the explicit content filter in this guild.""" - #: The roles in this guild, represented as a mapping of ID to role object. - #: - #: :type: :obj:`~typing.Mapping` [ :obj:`~hikari.bases.Snowflake`, :obj:`~GuildRole` ] roles: typing.Mapping[bases.Snowflake, GuildRole] = marshaller.attrib( deserializer=lambda roles: {r.id: r for r in map(GuildRole.deserialize, roles)}, ) + """The roles in this guild, represented as a mapping of ID to role object.""" - #: The emojis that this guild provides, represented as a mapping of ID to - #: emoji object. - #: - #: :type: :obj:`~typing.Mapping` [ :obj:`~hikari.bases.Snowflake`, :obj:`~hikari.emojis.GuildEmoji` ] emojis: typing.Mapping[bases.Snowflake, _emojis.GuildEmoji] = marshaller.attrib( deserializer=lambda emojis: {e.id: e for e in map(_emojis.GuildEmoji.deserialize, emojis)}, ) + """A mapping of IDs to the objects of the emojis this guild provides.""" - #: The required MFA level for users wishing to participate in this guild. - #: - #: :type: :obj:`~GuildMFALevel` mfa_level: GuildMFALevel = marshaller.attrib(deserializer=GuildMFALevel) + """The required MFA level for users wishing to participate in this guild.""" - #: The ID of the application that created this guild, if it was created by - #: a bot. If not, this is always :obj:`~None`. - #: - #: :type: :obj:`~hikari.bases.Snowflake`, optional application_id: typing.Optional[bases.Snowflake] = marshaller.attrib( deserializer=bases.Snowflake.deserialize, if_none=None ) + """The ID of the application that created this guild. + + This will always be `None` for guilds that weren't created by a bot. + """ - #: Whether the guild is unavailable or not. - #: - #: This information is only available if the guild was sent via a - #: ``GUILD_CREATE`` event. If the guild is received from any other place, - #: this will always be :obj:`~None`. - #: - #: An unavailable guild cannot be interacted with, and most information may - #: be outdated if that is the case. - #: - #: :type: :obj:`~bool`, optional is_unavailable: typing.Optional[bool] = marshaller.attrib( raw_name="unavailable", deserializer=bool, if_undefined=None, default=None ) + """Whether the guild is unavailable or not. + + This information is only available if the guild was sent via a + `GUILD_CREATE` event. If the guild is received from any other place, this + will always be `None`. + + An unavailable guild cannot be interacted with, and most information may + be outdated if that is the case. + """ # TODO: document in which cases this information is not available. - #: Describes whether the guild widget is enabled or not. If this information - #: is not present, this will be :obj:`~None`. - #: - #: :type: :obj:`~bool`, optional is_widget_enabled: typing.Optional[bool] = marshaller.attrib( raw_name="widget_enabled", deserializer=bool, if_undefined=None, default=None ) + """Describes whether the guild widget is enabled or not. + + If this information is not present, this will be `None`. + """ - #: The channel ID that the widget's generated invite will send the user to, - #: if enabled. If this information is unavailable, this will be :obj:`~None`. - #: - #: :type: :obj:`~hikari.bases.Snowflake`, optional widget_channel_id: typing.Optional[bases.Snowflake] = marshaller.attrib( deserializer=bases.Snowflake.deserialize, if_undefined=None, if_none=None, default=None ) + """The channel ID that the widget's generated invite will send the user to. + + If this information is unavailable or this isn't enabled for the guild then + this will be `None`. + """ - #: The ID of the system channel (where welcome messages and Nitro boost - #: messages are sent), or :obj:`~None` if it is not enabled. - #: - #: :type: :obj:`~hikari.bases.Snowflake`, optional system_channel_id: typing.Optional[bases.Snowflake] = marshaller.attrib( if_none=None, deserializer=bases.Snowflake.deserialize ) + """The ID of the system channel or `None` if it is not enabled. + + Welcome messages and Nitro boost messages may be sent to this channel. + """ - #: Flags for the guild system channel to describe which notification - #: features are suppressed. - #: - #: :type: :obj:`~GuildSystemChannelFlag` system_channel_flags: GuildSystemChannelFlag = marshaller.attrib(deserializer=GuildSystemChannelFlag) + """Flags for the guild system channel to describe which notifications are suppressed.""" - #: The ID of the channel where guilds with the :obj:`~GuildFeature.PUBLIC` - #: ``features`` display rules and guidelines. - #: - #: If the :obj:`~GuildFeature.PUBLIC` feature is not defined, then this is - #: :obj:`~None`. - #: - #: :type: :obj:`~hikari.bases.Snowflake`, optional rules_channel_id: typing.Optional[bases.Snowflake] = marshaller.attrib( if_none=None, deserializer=bases.Snowflake.deserialize ) + """The ID of the channel where guilds with the `GuildFeature.PUBLIC` + `features` display rules and guidelines. + + If the `GuildFeature.PUBLIC` feature is not defined, then this is `None`. + """ - #: The date and time that the bot user joined this guild. - #: - #: This information is only available if the guild was sent via a - #: ``GUILD_CREATE`` event. If the guild is received from any other place, - #: this will always be :obj:`~None`. - #: - #: :type: :obj:`~datetime.datetime`, optional joined_at: typing.Optional[datetime.datetime] = marshaller.attrib( deserializer=conversions.parse_iso_8601_ts, if_undefined=None, default=None ) + """The date and time that the bot user joined this guild. + + This information is only available if the guild was sent via a `GUILD_CREATE` + event. If the guild is received from any other place, this will always be + `None`. + """ - #: Whether the guild is considered to be large or not. - #: - #: This information is only available if the guild was sent via a - #: ``GUILD_CREATE`` event. If the guild is received from any other place, - #: this will always be :obj:`~None`. - #: - #: The implications of a large guild are that presence information will - #: not be sent about members who are offline or invisible. - #: - #: :type: :obj:`~bool`, optional is_large: typing.Optional[bool] = marshaller.attrib( raw_name="large", deserializer=bool, if_undefined=None, default=None ) + """Whether the guild is considered to be large or not. + + This information is only available if the guild was sent via a `GUILD_CREATE` + event. If the guild is received from any other place, this will always b + `None`. + + The implications of a large guild are that presence information will not be + sent about members who are offline or invisible. + """ - #: The number of members in this guild. - #: - #: This information is only available if the guild was sent via a - #: ``GUILD_CREATE`` event. If the guild is received from any other place, - #: this will always be :obj:`~None`. - #: - #: :type: :obj:`~int`, optional member_count: typing.Optional[int] = marshaller.attrib(deserializer=int, if_undefined=None, default=None) + """The number of members in this guild. + + This information is only available if the guild was sent via a `GUILD_CREATE` + event. If the guild is received from any other place, this will always be + `None`. + """ - #: A mapping of ID to the corresponding guild members in this guild. - #: - #: This information is only available if the guild was sent via a - #: ``GUILD_CREATE`` event. If the guild is received from any other place, - #: this will always be :obj:`~None`. - #: - #: Additionally, any offline members may not be included here, especially - #: if there are more members than the large threshold set for the gateway - #: this object was send with. - #: - #: This information will only be updated if your shards have the correct - #: intents set for any update events. - #: - #: Essentially, you should not trust the information here to be a full - #: representation. If you need complete accurate information, you should - #: query the members using the appropriate API call instead. - #: - #: :type: :obj:`~typing.Mapping` [ :obj:`~hikari.bases.Snowflake`, :obj:`~GuildMember` ], optional members: typing.Optional[typing.Mapping[bases.Snowflake, GuildMember]] = marshaller.attrib( deserializer=lambda members: {m.user.id: m for m in map(GuildMember.deserialize, members)}, if_undefined=None, default=None, ) + """A mapping of ID to the corresponding guild members in this guild. + + This information is only available if the guild was sent via a `GUILD_CREATE` + event. If the guild is received from any other place, this will always be + `None`. + + Additionally, any offline members may not be included here, especially if + there are more members than the large threshold set for the gateway this + object was send with. + + This information will only be updated if your shards have the correct + intents set for any update events. + + Essentially, you should not trust the information here to be a full + representation. If you need complete accurate information, you should + query the members using the appropriate API call instead. + """ - #: A mapping of ID to the corresponding guild channels in this guild. - #: - #: This information is only available if the guild was sent via a - #: ``GUILD_CREATE`` event. If the guild is received from any other place, - #: this will always be :obj:`~None`. - #: - #: Additionally, any channels that you lack permissions to see will not be - #: defined here. - #: - #: This information will only be updated if your shards have the correct - #: intents set for any update events. - #: - #: To retrieve a list of channels in any other case, you should make an - #: appropriate API call to retrieve this information. - #: - #: :type: :obj:`~typing.Mapping` [ :obj:`~hikari.bases.Snowflake`, :obj:`~hikari.channels.GuildChannel` ], optional channels: typing.Optional[typing.Mapping[bases.Snowflake, _channels.GuildChannel]] = marshaller.attrib( deserializer=lambda guild_channels: {c.id: c for c in map(_channels.deserialize_channel, guild_channels)}, if_undefined=None, default=None, ) + """A mapping of ID to the corresponding guild channels in this guild. + + This information is only available if the guild was sent via a `GUILD_CREATE` + event. If the guild is received from any other place, this will always be + `None`. + + Additionally, any channels that you lack permissions to see will not be + defined here. + + This information will only be updated if your shards have the correct + intents set for any update events. + + To retrieve a list of channels in any other case, you should make an + appropriate API call to retrieve this information. + """ - #: A mapping of member ID to the corresponding presence information for - #: the given member, if available. - #: - #: This information is only available if the guild was sent via a - #: ``GUILD_CREATE`` event. If the guild is received from any other place, - #: this will always be :obj:`~None`. - #: - #: Additionally, any channels that you lack permissions to see will not be - #: defined here. - #: - #: This information will only be updated if your shards have the correct - #: intents set for any update events. - #: - #: To retrieve a list of presences in any other case, you should make an - #: appropriate API call to retrieve this information. - #: - #: :type: :obj:`~typing.Mapping` [ :obj:`~hikari.bases.Snowflake`, :obj:`~GuildMemberPresence` ], optional presences: typing.Optional[typing.Mapping[bases.Snowflake, GuildMemberPresence]] = marshaller.attrib( deserializer=lambda presences: {p.user.id: p for p in map(GuildMemberPresence.deserialize, presences)}, if_undefined=None, default=None, ) + """A mapping of member ID to the corresponding presence information for + the given member, if available. + + This information is only available if the guild was sent via a `GUILD_CREATE` + event. If the guild is received from any other place, this will always be + `None`. + + Additionally, any channels that you lack permissions to see will not be + defined here. + + This information will only be updated if your shards have the correct + intents set for any update events. + + To retrieve a list of presences in any other case, you should make an + appropriate API call to retrieve this information. + """ - #: The maximum number of presences for the guild. - #: - #: If this is :obj:`~None`, then the default value is used (currently 5000). - #: - #: :type: :obj:`~int`, optional max_presences: typing.Optional[int] = marshaller.attrib( deserializer=int, if_undefined=None, if_none=None, default=None ) + """The maximum number of presences for the guild. + + If this is `None`, then the default value is used (currently 5000). + """ - #: The maximum number of members allowed in this guild. - #: - #: This information may not be present, in which case, it will be :obj:`~None`. - #: - #: :type: :obj:`~int`, optional max_members: typing.Optional[int] = marshaller.attrib(deserializer=int, if_undefined=None, default=None) + """The maximum number of members allowed in this guild. + + This information may not be present, in which case, it will be `None`. + """ - #: The vanity URL code for the guild's vanity URL. - #: - #: This is only present if :obj:`~GuildFeature.VANITY_URL` is in the - #: ``features`` for this guild. If not, this will always be :obj:`~None`. - #: - #: :type: :obj:`~str`, optional vanity_url_code: typing.Optional[str] = marshaller.attrib(deserializer=str, if_none=None) + """The vanity URL code for the guild's vanity URL. + + This is only present if `GuildFeature.VANITY_URL` is in `Guild.features` for + this guild. If not, this will always be `None`. + """ - #: The guild's description. - #: - #: This is only present if certain :obj:`~GuildFeature`'s are set in the - #: ``features`` for this guild. Otherwise, this will always be :obj:`~None`. - #: - #: :type: :obj:`~str`, optional description: typing.Optional[str] = marshaller.attrib(if_none=None, deserializer=str) + """The guild's description. + + This is only present if certain `GuildFeature`'s are set in + `Guild.features` for this guild. Otherwise, this will always be `None`. + """ - #: The hash for the guild's banner. - #: - #: This is only present if the guild has :obj:`~GuildFeature.BANNER` in the - #: ``features`` for this guild. For all other purposes, it is - # :obj:`~None`. - #: - #: :type: :obj:`~str`, optional banner_hash: typing.Optional[str] = marshaller.attrib(raw_name="banner", if_none=None, deserializer=str) + """The hash for the guild's banner. + + This is only present if the guild has `GuildFeature.BANNER` in + `Guild.features` for this guild. For all other purposes, it is `None`. + """ - #: The premium tier for this guild. - #: - #: :type: :obj:`~GuildPremiumTier` premium_tier: GuildPremiumTier = marshaller.attrib(deserializer=GuildPremiumTier) + """The premium tier for this guild.""" - #: The number of nitro boosts that the server currently has. - #: - #: This information may not be present, in which case, it will be :obj:`~None`. - #: - #: :type: :obj:`~int`, optional premium_subscription_count: typing.Optional[int] = marshaller.attrib( deserializer=int, if_undefined=None, default=None ) + """The number of nitro boosts that the server currently has. + + This information may not be present, in which case, it will be `None`. + """ - #: The preferred locale to use for this guild. - #: - #: This can only be change if :obj:`~GuildFeature.PUBLIC` is in the - #: ``features`` for this guild and will otherwise default to ``en-US```. - #: - #: :type: :obj:`~str` preferred_locale: str = marshaller.attrib(deserializer=str) + """The preferred locale to use for this guild. + + This can only be change if `GuildFeature.PUBLIC` is in `Guild.features` + for this guild and will otherwise default to `en-US`. + """ - #: The channel ID of the channel where admins and moderators receive notices - #: from Discord. - #: - #: This is only present if :obj:`~GuildFeature.PUBLIC` is in the - #: ``features`` for this guild. For all other purposes, it should be - #: considered to be :obj:`~None`. - #: - #: :type: :obj:`~hikari.bases.Snowflake`, optional public_updates_channel_id: typing.Optional[bases.Snowflake] = marshaller.attrib( if_none=None, deserializer=bases.Snowflake.deserialize ) + """The channel ID of the channel where admins and moderators receive notices + from Discord. + + This is only present if `GuildFeature.PUBLIC` is in `Guild.features` for + this guild. For all other purposes, it should be considered to be `None`. + """ def format_splash_url(self, fmt: str = "png", size: int = 4096) -> typing.Optional[str]: """Generate the URL for this guild's splash image, if set. Parameters ---------- - fmt : :obj:`~str` - The format to use for this URL, defaults to ``png``. - Supports ``png``, ``jpeg``, ``jpg`` and ``webp``. - size : :obj:`~int` - The size to set for the URL, defaults to ``4096``. + fmt : str + The format to use for this URL, defaults to `png`. + Supports `png`, `jpeg`, `jpg` and `webp`. + size : int + The size to set for the URL, defaults to `4096`. Can be any power of two between 16 and 4096. Returns ------- - :obj:`~str`, optional + str, optional The string URL. Raises ------ - :obj:`~ValueError` - If ``size`` is not a power of two or not between 16 and 4096. + ValueError + If `size` is not a power of two or not between 16 and 4096. """ if self.splash_hash: return urls.generate_cdn_url("splashes", str(self.id), self.splash_hash, fmt=fmt, size=size) @@ -1477,22 +1279,22 @@ def format_discovery_splash_url(self, fmt: str = "png", size: int = 4096) -> typ Parameters ---------- - fmt : :obj:`~str` - The format to use for this URL, defaults to ``png``. - Supports ``png``, ``jpeg``, ``jpg`` and ``webp``. - size : :obj:`~int` - The size to set for the URL, defaults to ``4096``. + fmt : str + The format to use for this URL, defaults to `png`. + Supports `png`, `jpeg`, `jpg` and `webp`. + size : int + The size to set for the URL, defaults to `4096`. Can be any power of two between 16 and 4096. Returns ------- - :obj:`~str`, optional + str, optional The string URL. Raises ------ - :obj:`~ValueError` - If ``size`` is not a power of two or not between 16 and 4096. + ValueError + If `size` is not a power of two or not between 16 and 4096. """ if self.discovery_splash_hash: return urls.generate_cdn_url( @@ -1510,22 +1312,22 @@ def format_banner_url(self, fmt: str = "png", size: int = 4096) -> typing.Option Parameters ---------- - fmt : :obj:`~str` - The format to use for this URL, defaults to ``png``. - Supports ``png``, ``jpeg``, ``jpg`` and ``webp``. - size : :obj:`~int` - The size to set for the URL, defaults to ``4096``. + fmt : str + The format to use for this URL, defaults to `png`. + Supports `png`, `jpeg`, `jpg` and `webp`. + size : int + The size to set for the URL, defaults to `4096`. Can be any power of two between 16 and 4096. Returns ------- - :obj:`~str`, optional + str, optional The string URL. Raises ------ - :obj:`~ValueError` - If ``size`` is not a power of two or not between 16 and 4096. + ValueError + If `size` is not a power of two or not between 16 and 4096. """ if self.banner_hash: return urls.generate_cdn_url("banners", str(self.id), self.banner_hash, fmt=fmt, size=size) diff --git a/hikari/intents.py b/hikari/intents.py index 688a30ae1d..9c8030bea1 100644 --- a/hikari/intents.py +++ b/hikari/intents.py @@ -34,121 +34,146 @@ class Intent(more_enums.FlagMixin, enum.IntFlag): Any events not in an intent category will be fired regardless of what intents you provide. - Warnings - -------- - If you are using the V7 Gateway, you will be REQUIRED to provide some form - of intent value when you connect. Failure to do so may result in immediate - termination of the session server-side. - - Notes - ----- - Discord now places limits on certain events you can receive without - whitelisting your bot first. On the ``Bot`` tab in the developer's portal - for your bot, you should now have the option to enable functionality - for receiving these events. - - If you attempt to request an intent type that you have not whitelisted - your bot for, you will be disconnected on startup with a ``4014`` closure - code. + !!! info + Discord now places limits on certain events you can receive without + whitelisting your bot first. On the `Bot` tab in the developer's portal + for your bot, you should now have the option to enable functionality + for receiving these events. + + If you attempt to request an intent type that you have not whitelisted + your bot for, you will be disconnected on startup with a `4014` closure + code. + + !!! warning + If you are using the V7 Gateway, you will be REQUIRED to provide some + form of intent value when you connect. Failure to do so may result in + immediate termination of the session server-side. """ - #: Subscribes to the following events: - #: * GUILD_CREATE - #: * GUILD_UPDATE - #: * GUILD_DELETE - #: * GUILD_ROLE_CREATE - #: * GUILD_ROLE_UPDATE - #: * GUILD_ROLE_DELETE - #: * CHANNEL_CREATE - #: * CHANNEL_UPDATE - #: * CHANNEL_DELETE - #: * CHANNEL_PINS_UPDATE GUILDS = 1 << 0 + """Subscribes to the following events: + + * GUILD_CREATE + * GUILD_UPDATE + * GUILD_DELETE + * GUILD_ROLE_CREATE + * GUILD_ROLE_UPDATE + * GUILD_ROLE_DELETE + * CHANNEL_CREATE + * CHANNEL_UPDATE + * CHANNEL_DELETE + * CHANNEL_PINS_UPDATE + """ - #: Subscribes to the following events: - #: * GUILD_MEMBER_ADD - #: * GUILD_MEMBER_UPDATE - #: * GUILD_MEMBER_REMOVE - #: - #: Warnings - #: -------- - #: This intent is privileged, and requires enabling/whitelisting to use. GUILD_MEMBERS = 1 << 1 + """Subscribes to the following events: + + * GUILD_MEMBER_ADD + * GUILD_MEMBER_UPDATE + * GUILD_MEMBER_REMOVE + + !!! warning + This intent is privileged, and requires enabling/whitelisting to use. + """ - #: Subscribes to the following events: - #: * GUILD_BAN_ADD - #: * GUILD_BAN_REMOVE GUILD_BANS = 1 << 2 + """Subscribes to the following events: + + * GUILD_BAN_ADD + * GUILD_BAN_REMOVE + """ - #: Subscribes to the following events: - #: * GUILD_EMOJIS_UPDATE GUILD_EMOJIS = 1 << 3 + """Subscribes to the following events: + + * GUILD_EMOJIS_UPDATE + """ - #: Subscribes to the following events: - #: * GUILD_INTEGRATIONS_UPDATE GUILD_INTEGRATIONS = 1 << 4 + """Subscribes to the following events: + + * GUILD_INTEGRATIONS_UPDATE + """ - #: Subscribes to the following events: - #: * WEBHOOKS_UPDATE GUILD_WEBHOOKS = 1 << 5 + """Subscribes to the following events: + + * WEBHOOKS_UPDATE + """ - #: Subscribes to the following events: - #: * INVITE_CREATE - #: * INVITE_DELETE GUILD_INVITES = 1 << 6 + """Subscribes to the following events: + + * INVITE_CREATE + * INVITE_DELETE + """ - #: Subscribes to the following events: - #: * VOICE_STATE_UPDATE GUILD_VOICE_STATES = 1 << 7 + """Subscribes to the following events: + + * VOICE_STATE_UPDATE + """ - #: Subscribes to the following events: - #: * PRESENCE_UPDATE - #: - #: Warnings - #: -------- - #: This intent is privileged, and requires enabling/whitelisting to use. GUILD_PRESENCES = 1 << 8 + """Subscribes to the following events: + + * PRESENCE_UPDATE + + !!! warning + This intent is privileged, and requires enabling/whitelisting to use.""" - #: Subscribes to the following events: - #: * MESSAGE_CREATE - #: * MESSAGE_UPDATE - #: * MESSAGE_DELETE - #: * MESSAGE_BULK GUILD_MESSAGES = 1 << 9 + """Subscribes to the following events: + + * MESSAGE_CREATE + * MESSAGE_UPDATE + * MESSAGE_DELETE + * MESSAGE_BULK + """ - #: Subscribes to the following events: - #: * MESSAGE_REACTION_ADD - #: * MESSAGE_REACTION_REMOVE - #: * MESSAGE_REACTION_REMOVE_ALL - #: * MESSAGE_REACTION_REMOVE_EMOJI GUILD_MESSAGE_REACTIONS = 1 << 10 + """Subscribes to the following events: + + * MESSAGE_REACTION_ADD + * MESSAGE_REACTION_REMOVE + * MESSAGE_REACTION_REMOVE_ALL + * MESSAGE_REACTION_REMOVE_EMOJI + """ - #: Subscribes to the following events: - #: * TYPING_START GUILD_MESSAGE_TYPING = 1 << 11 + """Subscribes to the following events: + + * TYPING_START + """ - #: Subscribes to the following events: - #: * CHANNEL_CREATE - #: * MESSAGE_CREATE - #: * MESSAGE_UPDATE - #: * MESSAGE_DELETE DIRECT_MESSAGES = 1 << 12 + """Subscribes to the following events: + + * CHANNEL_CREATE + * MESSAGE_CREATE + * MESSAGE_UPDATE + * MESSAGE_DELETE + """ - #: Subscribes to the following events: - #: * MESSAGE_REACTION_ADD - #: * MESSAGE_REACTION_REMOVE - #: * MESSAGE_REACTION_REMOVE_ALL DIRECT_MESSAGE_REACTIONS = 1 << 13 + """Subscribes to the following events: + + * MESSAGE_REACTION_ADD + * MESSAGE_REACTION_REMOVE + * MESSAGE_REACTION_REMOVE_ALL + """ - #: Subscribes to the following events - #: * TYPING_START DIRECT_MESSAGE_TYPING = 1 << 14 + """Subscribes to the following events + + * TYPING_START + """ @property def is_privileged(self) -> bool: """Whether the intent requires elevated privileges. - If this is ``True``, you will be required to opt-in to using this intent + If this is `True`, you will be required to opt-in to using this intent on the Discord Developer Portal before you can utilise it in your application. """ diff --git a/hikari/internal/__init__.py b/hikari/internal/__init__.py index a8944f790e..638a2d5a11 100644 --- a/hikari/internal/__init__.py +++ b/hikari/internal/__init__.py @@ -16,7 +16,4 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . -"""Various utilities used internally within this API. - -|internal| -""" +"""Various utilities used internally within this API.""" diff --git a/hikari/internal/assertions.py b/hikari/internal/assertions.py index 3883f5c054..5976516d62 100644 --- a/hikari/internal/assertions.py +++ b/hikari/internal/assertions.py @@ -21,8 +21,6 @@ These are functions that validate a value, and are expected to return the value on success but error on any failure. This allows for quick checking of conditions that might break the function or cause it to misbehave. - -|internal| """ __all__ = [ "assert_that", @@ -39,7 +37,7 @@ def assert_that(condition: bool, message: str = None, error_type: type = ValueError) -> None: - """If the given condition is falsified, raise a :obj:`~ValueError`. + """If the given condition is falsified, raise a `ValueError`. Will be raised with the optional description if provided. """ @@ -48,7 +46,7 @@ def assert_that(condition: bool, message: str = None, error_type: type = ValueEr def assert_not_none(value: ValueT, message: typing.Optional[str] = None) -> ValueT: - """If the given value is :obj:`~None`, raise a :obj:`~ValueError`. + """If the given value is `None`, raise a `ValueError`. Will be raised with the optional description if provided. """ @@ -58,13 +56,13 @@ def assert_not_none(value: ValueT, message: typing.Optional[str] = None) -> Valu def assert_in_range(value, min_inclusive, max_inclusive, name: str = None): - """If a value is not in the range [min, max], raise a :obj:`~ValueError`.""" + """If a value is not in the range [min, max], raise a `ValueError`.""" if not (min_inclusive <= value <= max_inclusive): name = name or "The value" raise ValueError(f"{name} must be in the inclusive range of {min_inclusive} and {max_inclusive}") def assert_is_int_power(value: int, power: int) -> bool: - """If a value is not a power the given int, raise :obj:`~ValueError`.""" + """If a value is not a power the given int, raise `ValueError`.""" logarithm = math.log(value, power) assert_that(logarithm.is_integer(), f"value must be an integer power of {power}") diff --git a/hikari/internal/conversions.py b/hikari/internal/conversions.py index a2f720a223..d8cac674ec 100644 --- a/hikari/internal/conversions.py +++ b/hikari/internal/conversions.py @@ -64,10 +64,10 @@ def nullable_cast(value: CastInputT, cast: TypeCastT, /) -> ResultT: - """Attempt to cast the given ``value`` with the given ``cast``. + """Attempt to cast the given `value` with the given `cast`. - This will only succeed if ``value`` is not :obj:`~None`. If it is - :obj:`~None`, then :obj:`~None` is returned instead. + This will only succeed if `value` is not `None`. If it is `None`, then + `None` is returned instead. """ if value is None: return None @@ -77,7 +77,7 @@ def nullable_cast(value: CastInputT, cast: TypeCastT, /) -> ResultT: def try_cast(value: CastInputT, cast: TypeCastT, default: DefaultT = None, /) -> ResultT: """Try to cast the given value to the given cast. - If it throws a :obj:`~Exception` or derivative, it will return ``default`` + If it throws a `Exception` or derivative, it will return `default` instead of the cast value instead. """ with contextlib.suppress(Exception): @@ -90,7 +90,7 @@ def try_cast_or_defer_unary_operator(type_: typing.Type, /): Parameters ---------- - type_ : :obj:`~typing.Callable` [ ..., ``output type`` ] + type_ : typing.Callable [ ..., `output type` ] The type to cast to. """ return lambda data: try_cast(data, type_, data) @@ -103,17 +103,17 @@ def put_if_specified( type_after: typing.Optional[TypeCastT] = None, /, ) -> None: - """Add a value to the mapping under the given key as long as the value is not ``...``. + """Add a value to the mapping under the given key as long as the value is not `...`. Parameters ---------- - mapping : :obj:`~typing.Dict` [ :obj:`~typing.Hashable`, :obj:`~typing.Any` ] + mapping : typing.Dict [ typing.Hashable, typing.Any ] The mapping to add to. - key : :obj:`~typing.Hashable` + key : typing.Hashable The key to add the value under. - value : :obj:`~typing.Any` + value : typing.Any The value to add. - type_after : :obj:`~typing.Callable` [ [ ``input type`` ], ``output type`` ], optional + type_after : typing.Callable [ [ `input type` ], `output type` ], optional Type to apply to the value when added. """ if value is not ...: @@ -128,23 +128,22 @@ def image_bytes_to_image_data(img_bytes: typing.Optional[bytes] = None, /) -> ty Parameters ---------- - img_bytes : :obj:`~bytes`, optional + img_bytes : bytes, optional The image bytes. Raises ------ - :obj:`~ValueError` + ValueError If the image type passed is not supported. Returns ------- - :obj:`~str`, optional - The ``image_bytes`` given encoded into an image data string or - :obj:`~None`. + str, optional + The `image_bytes` given encoded into an image data string or + `None`. - Note - ---- - Supported image types: ``.png``, ``.jpeg``, ``.jfif``, ``.gif``, ``.webp`` + !!! note + Supported image types: `.png`, `.jpeg`, `.jfif`, `.gif`, `.webp` """ if img_bytes is None: return None @@ -170,12 +169,12 @@ def parse_http_date(date_str: str, /) -> datetime.datetime: Parameters ---------- - date_str : :obj:`~str` + date_str : str The RFC-2822 (section 3.3) compliant date string to parse. Returns ------- - :obj:`~datetime.datetime` + datetime.datetime The HTTP date as a datetime object. See Also @@ -186,21 +185,21 @@ def parse_http_date(date_str: str, /) -> datetime.datetime: def parse_iso_8601_ts(date_string: str, /) -> datetime.datetime: - """Parse an ISO 8601 date string into a :obj:`~datetime.datetime` object. + """Parse an ISO 8601 date string into a `datetime.datetime` object. Parameters ---------- - date_string : :obj:`~str` + date_string : str The ISO 8601 compliant date string to parse. Returns ------- - :obj:`~datetime.datetime` + datetime.datetime The ISO 8601 date string as a datetime object. See Also -------- - ``_ + https://en.wikipedia.org/wiki/ISO_8601 """ year, month, day = map(int, ISO_8601_DATE_PART.findall(date_string)[0]) @@ -224,32 +223,32 @@ def parse_iso_8601_ts(date_string: str, /) -> datetime.datetime: def discord_epoch_to_datetime(epoch: int, /) -> datetime.datetime: - """Parse a Discord epoch into a :obj:`~datetime.datetime` object. + """Parse a Discord epoch into a `datetime.datetime` object. Parameters ---------- - epoch : :obj:`~int` + epoch : int Number of milliseconds since 1/1/2015 (UTC) Returns ------- - :obj:`~datetime.datetime` + datetime.datetime Number of seconds since 1/1/1970 within a datetime object (UTC). """ return datetime.datetime.fromtimestamp(epoch / 1000 + DISCORD_EPOCH, datetime.timezone.utc) def unix_epoch_to_datetime(epoch: int, /) -> datetime.datetime: - """Parse a UNIX epoch to a :obj:`~datetime.datetime` object. + """Parse a UNIX epoch to a `datetime.datetime` object. Parameters ---------- - epoch : :obj:`~int` + epoch : int Number of milliseconds since 1/1/1970 (UTC) Returns ------- - :obj:`~datetime.datetime` + datetime.datetime Number of seconds since 1/1/1970 within a datetime object (UTC). """ return datetime.datetime.fromtimestamp(epoch / 1000, datetime.timezone.utc) @@ -265,12 +264,12 @@ def seek( Parameters ---------- - offset : :obj:`~int` + offset : int The offset to seek to. - whence : :obj:`~int` - If ``0``, as the default, then use absolute file positioning. - If ``1``, then seek to the current position. - If ``2``, then seek relative to the end of the file. + whence : int + If `0`, as the default, then use absolute file positioning. + If `1`, then seek to the current position. + If `2`, then seek relative to the end of the file. """ def tell(self) -> int: @@ -278,7 +277,7 @@ def tell(self) -> int: Returns ------- - :obj:`~int` + int The stream position. """ @@ -287,7 +286,7 @@ def read(self) -> typing.AnyStr: Returns ------- - :obj:`~str` + str The string that was read. """ @@ -298,17 +297,17 @@ def close(self) -> None: def make_resource_seekable(resource: typing.Any, /) -> Seekable: """Make a seekable resource to use off some representation of data. - This supports :obj:`~bytes`, :obj:`~bytearray`, :obj:`~memoryview`, and - :obj:`~str`. Anything else is just returned. + This supports `bytes`,`bytearray`, `memoryview`, and + `str`. Anything else is just returned. Parameters ---------- - resource : :obj:`~typing.Any` + resource : typing.Any The resource to check. Returns ------- - :obj:`~typing.Union` [ :obj:`~io.BytesIO`, :obj:`~io.StringIO` ] + typing.Union [ io.BytesIO, io.StringIO` ] An stream-compatible resource where possible. """ if isinstance(resource, (bytes, bytearray)): @@ -324,17 +323,17 @@ def make_resource_seekable(resource: typing.Any, /) -> Seekable: def get_bytes_from_resource(resource: typing.Any) -> bytes: """Take in any file-like object and return the raw bytes data from it. - Supports any ``FileLikeT`` type that isn't string based. + Supports any `FileLikeT` type that isn't string based. Anything else is just returned. Parameters ---------- - resource : ``FileLikeT`` + resource : FileLikeT The resource to get bytes from. Returns ------- - :obj:`~bytes` + byte The resulting bytes. """ if isinstance(resource, bytearray): @@ -359,46 +358,45 @@ def snoop_typehint_from_scope(frame: types.FrameType, typehint: typing.Union[str This snoops around the local and global scope for the given frame to find the given attribute name, taking into account nested function calls. The reason to do this is that if a string type hint is used, or the - ``from __future__ import annotations`` directive is used, the physical thing + `from __future__ import annotations` directive is used, the physical thing that the type hint represents will no longer be evaluated by the interpreter. This is an implementation that does not require the use of - :obj:`~eval`, and thus reduces the risk of arbitrary code execution as a + `eval`, and thus reduces the risk of arbitrary code execution as a result. - Nested parameters such as :obj:`~typing.Sequence` should also be able to be + Nested parameters such as `typing.Sequence` should also be able to be resolved correctly. Parameters ---------- - frame : :obj:`~types.FrameType` + frame : types.FrameType The stack frame that the element with the typehint was defined in. - This is retrieved using :obj:`~inspect.stack` ``(frame_no)[0][0]``, - where ``frame_no`` is the number of frames from this invocation that + This is retrieved using `inspect.stack` `(frame_no)[0][0]`, + where `frame_no` is the number of frames from this invocation that you want to snoop the scope at. - typehint : :obj:`~typing.Union` [ :obj:`~str`, :obj:`~typing.Any` ] - The type hint to resolve. If a non-:obj:`~str` is passed, then this is + typehint : typing.Union [ str, typing.Any ] + The type hint to resolve. If a non-`str` is passed, then this is returned immediately as the result. Returns ------- - :obj:`~typing.Any` + typing.Any The physical representation of the given type hint. Raises ------ - :obj:`~NameError` + NameError If the attribute was not found. - Warnings - -------- - The input frame must be manually dereferenced using the ``del`` keyword - after use. Any functions that are decorated and wrapped when using this - lookup must use :obj:`~functools.wraps` to ensure that the correct scope is - identified on the stack. - - This is incredibly unpythonic and baremetal, but due to - `PEP 563 ` there is no other - consistent way of making this work correctly. + !!! warning + The input frame must be manually dereferenced using the `del` keyword + after use. Any functions that are decorated and wrapped when using this + lookup must use `functools.wraps` to ensure that the correct scope is + identified on the stack. + + This is incredibly unpythonic and baremetal, but due to + [PEP 563](https://www.python.org/dev/peps/pep-0563/) there is no other + consistent way of making this work correctly. """ if not isinstance(typehint, str): return typehint @@ -420,33 +418,32 @@ def dereference_int_flag( int_flag_type: typing.Type[IntFlagT], raw_value: typing.Union[RawIntFlagValueT, typing.Collection[RawIntFlagValueT]], ) -> IntFlagT: - """Cast to the provided :obj:`~enum.IntFlag` type. + """Cast to the provided `enum.IntFlag` type. This supports resolving bitfield integers as well as decoding a sequence of case insensitive flag names into one combined value. Parameters ---------- - int_flag_type : :obj:`~typing.Type` [ :obj:`~enum.IntFlag` ] + int_flag_type : typing.Type [ enum.IntFlag ] The type of the int flag to check. - raw_value : ``Castable Value`` + raw_value : Castable Value The raw value to convert. Returns ------- - :obj:`~enum.IntFlag` + enum.IntFlag The cast value as a flag. - Notes - ----- - Types that are a ``Castable Value`` include: - - :obj:`~str` - - :obj:`~int` - - :obj:`~typing.SupportsInt` - - :obj:`~typing.Collection` [ ``Castable Value`` ] + !!! note + Types that are a `Castable Value` include: + - `str` + - `int` + - `typing.SupportsInt` + - `typing.Collection` [ `Castable Value` ] - When a collection is passed, values will be combined using functional - reduction via the :obj:operator.or_` operator. + When a collection is passed, values will be combined using functional + reduction via the `operator.or_` operator. """ if isinstance(raw_value, str) and raw_value.isdigit(): raw_value = int(raw_value) diff --git a/hikari/internal/helpers.py b/hikari/internal/helpers.py index b70a18d8fd..cc6fd89039 100644 --- a/hikari/internal/helpers.py +++ b/hikari/internal/helpers.py @@ -37,11 +37,11 @@ def warning(message: str, category: typing.Type[Warning], stack_level: int = 1) Parameters ---------- - message : :obj:`~str` + message : str The message to display. - category : :obj:`~typing.Type` [ :obj:`~Warning` ] + category : typing.Type [ Warning ] The type of warning to raise. - stack_level : :obj:`int` + stack_level : int How many stack frames to go back to find the user's invocation. """ @@ -57,28 +57,28 @@ def generate_allowed_mentions( Parameters ---------- - mentions_everyone : :obj:`~bool` - Whether ``@everyone`` and ``@here`` mentions should be resolved by + mentions_everyone : bool + Whether `@everyone` and `@here` mentions should be resolved by discord and lead to actual pings. - user_mentions : :obj:`~typing.Union` [ :obj:`~typing.Collection` [ :obj:`~typing.Union` [ :obj:`~hikari.users.User`, :obj:`~hikari.bases.Snowflake`, :obj:`~int` ], :obj:`~bool` ] + user_mentions : typing.Union [ typing.Collection [ typing.Union [ hikari.users.User, hikari.bases.Snowflake, int ], bool ] Either an array of user objects/IDs to allow mentions for, - :obj:`~True` to allow all user mentions or :obj:`~False` to block all + `True` to allow all user mentions or `False` to block all user mentions from resolving. - role_mentions : :obj:`~typing.Union` [ :obj:`~typing.Collection` [ :obj:`~typing.Union` [ :obj:`~hikari.guilds.GuildRole`, :obj:`~hikari.bases.Snowflake`, :obj:`~int` ] ], :obj:`~bool` ] + role_mentions : typing.Union [ typing.Collection [ typing.Union [ hikari.guilds.GuildRole, hikari.bases.Snowflake, int ] ], bool ] Either an array of guild role objects/IDs to allow mentions for, - :obj:`~True` to allow all role mentions or :obj:`~False` to block all + `True` to allow all role mentions or `False` to block all role mentions from resolving. Returns ------- - :obj:`~typing.Dict` [ :obj:`~str`, :obj:`~typing.Sequence` [ :obj:`~str` ] ] + typing.Dict [ str, typing.Sequence [ str ] ] The resulting allowed mentions dict object. Raises ------ - :obj:`~ValueError` + ValueError If more than 100 unique objects/entities are passed for - ``role_mentions`` or ``user_mentions. + `role_mentions` or `user_mentions. """ parsed_mentions = [] allowed_mentions = {} @@ -128,40 +128,39 @@ async def pagination_handler( ) -> typing.AsyncIterator[typing.Any]: """Generate an async iterator for handling paginated endpoints. - This will handle Discord's ``before`` and ``after`` pagination. + This will handle Discord's `before` and `after` pagination. Parameters ---------- - deserializer : :obj:`~typing.Callable` [ [ :obj:`~typing.Any` ], :obj:`~typing.Any` ] + deserializer : typing.Callable [ [ typing.Any ], typing.Any ] The deserializer to use to deserialize raw elements. - direction : :obj:`~typing.Union` [ ``"before"``, ``"after"`` ] + direction : typing.Union [ `"before"`, `"after"` ] The direction that this paginator should go in. - request : :obj:`~typing.Callable` [ ``...``, :obj:`~typing.Coroutine` [ :obj:`~typing.Any`, :obj:`~typing.Any`, :obj:`~typing.Any` ] ] - The :obj:`hikari.net.rest_sessions.LowLevelRestfulClient` method that should be + request : typing.Callable [ ..., typing.Coroutine [ typing.Any, typing.Any, typing.Any ] ] + The `hikari.net.rest.REST` method that should be called to make requests for this paginator. - reversing : :obj:`~bool` + reversing : bool Whether the retrieved array of objects should be reversed before iterating through it, this is needed for certain endpoints like - ``fetch_messages_before`` where the order is static regardless of - if you're using ``before`` or ``after``. - start : :obj:`~int`, optional - The snowflake ID that this paginator should start at, ``0`` may be - passed for ``forward`` pagination to start at the first created - entity and :obj:`~None` may be passed for ``before`` pagination to + `fetch_messages_before` where the order is static regardless of + if you're using `before` or `after`. + start : int, optional + The snowflake ID that this paginator should start at, `0` may be + passed for `forward` pagination to start at the first created + entity and `None` may be passed for `before` pagination to start at the newest entity (based on when it's snowflake timestamp). - limit : :obj:`~int`, optional + limit : int, optional The amount of deserialized entities that the iterator should return - total, will be unlimited if set to :obj:`~None`. - id_getter : :obj:`~typing.Callable` [ [ :obj:`~typing.Any` ], :obj:`~str` ] + total, will be unlimited if set to `None`. + id_getter : typing.Callable [ [ typing.Any ], str ] **kwargs - Kwargs to pass through to ``request`` for every request made along + Kwargs to pass through to `request` for every request made along with the current decided limit and direction snowflake. Returns ------- - :obj:`~typing.AsyncIterator` [ :obj:`~typing.Any` ] + typing.AsyncIterator [ typing.Any ] An async iterator of the found deserialized found objects. - """ while payloads := await request( limit=100 if limit is None or limit > 100 else limit, diff --git a/hikari/internal/marshaller.c b/hikari/internal/marshaller.c index cb7a689b31..b4fd0dec1b 100644 --- a/hikari/internal/marshaller.c +++ b/hikari/internal/marshaller.c @@ -39,12 +39,9 @@ PyDoc_STRVAR( module_doc, "An internal marshalling utility used by internal API components.\n" "\n" - "Warning\n" - "-------\n" - "You should not change anything in this file, if you do, you will likely get\n" - "unexpected behaviour elsewhere.\n" - "\n" - "|internal|\n" + "!!! warning\n" + " You should not change anything in this file, if you do, you will likely get\n" + " unexpected behaviour elsewhere.\n" ); @@ -92,27 +89,34 @@ PyDoc_STRVAR( "\n" "Parameters\n" "----------\n" - "handle_string : :obj:`str`\n" + "handle_string : str\n" " The handle to the object to refer to. This is in the format\n" - " ``fully.qualified.module.name#object.attribute``. If no ``#`` is\n" + " `fully.qualified.module.name#object.attribute`. If no `#` is\n" " input, then the reference will be made to the module itself.\n" "\n" "Returns\n" "-------\n" - ":obj:`typing.Any`\n" + "typing.Any\n" " The thing that is referred to from this reference.\n" "\n" "Examples\n" "--------\n" - "``\"collections#deque\"``:\n" - " Refers to :obj:`collections.deque`\n" - "``\"asyncio.tasks#Task\"``:\n" - " Refers to ``asyncio.tasks.Task``\n" - "``\"hikari.net\"``:\n" - " Refers to :obj:`hikari.net`\n" - "``\"foo.bar#baz.bork.qux\"``:\n" - " Would refer to a theoretical ``qux`` attribute on a ``bork``\n" - " attribute on a ``baz`` object in the ``foo.bar`` module.\n" + "* `\"collections#deque\":\n" + "\n" + " Refers to `collections.deque`\n" + "\n" + "* `\"asyncio.tasks#Task\"`:\n" + "\n" + " Refers to `asyncio.tasks.Task`\n" + "\n" + "* `\"hikari.net\"`:\n" + "\n" + " Refers to `hikari.net`\n" + "\n" + "* `\"foo.bar#baz.bork.qux\"`:\n" + "\n" + " Would refer to a theoretical `qux` attribute on a `bork`\n" + " attribute on a `baz` object in the `foo.bar` module.\n" ); diff --git a/hikari/internal/marshaller.py b/hikari/internal/marshaller.py index 40492f95c6..723afbe057 100644 --- a/hikari/internal/marshaller.py +++ b/hikari/internal/marshaller.py @@ -18,12 +18,9 @@ # along with Hikari. If not, see . """An internal marshalling utility used by internal API components. -Warning -------- -You should not change anything in this file, if you do, you will likely get -unexpected behaviour elsewhere. - -|internal| +!!! warning + You should not change anything in this file, if you do, you will likely get + unexpected behaviour elsewhere. """ __all__ = [ "RAISE", @@ -61,27 +58,34 @@ def dereference_handle(handle_string: str) -> typing.Any: Parameters ---------- - handle_string : :obj:`~str` + handle_string : str The handle to the object to refer to. This is in the format - ``fully.qualified.module.name#object.attribute``. If no ``#`` is + `fully.qualified.module.name#object.attribute`. If no `#` is input, then the reference will be made to the module itself. Returns ------- - :obj:`~typing.Any` + typing.Any The thing that is referred to from this reference. Examples -------- - ``"collections#deque"``: - Refers to :obj:`~collections.deque` - ``"asyncio.tasks#Task"``: - Refers to ``asyncio.tasks.Task`` - ``"hikari.net"``: - Refers to :obj:`~hikari.net` - ``"foo.bar#baz.bork.qux"``: - Would refer to a theoretical ``qux`` attribute on a ``bork`` - attribute on a ``baz`` object in the ``foo.bar`` module. + * `"collections#deque"`: + + Refers to `collections.deque` + + * `"asyncio.tasks#Task"`: + + Refers to `asyncio.tasks.Task` + + * `"hikari.net"`: + + Refers to `hikari.net` + + * `"foo.bar#baz.bork.qux"`: + + Would refer to a theoretical `qux` attribute on a `bork` + attribute on a `baz` object in the `foo.bar` module. """ if "#" not in handle_string: module, attribute_names = handle_string, () @@ -112,42 +116,38 @@ def attrib( serializer: typing.Optional[typing.Callable[[typing.Any], typing.Any]] = None, **kwargs, ) -> attr.Attribute: - """Create an :func:`attr.ib` with marshaller metadata attached. + """Create an `attr.ib` with marshaller metadata attached. Parameters ---------- - deserializer : :obj:`~typing.Callable` [ [ :obj:`~typing.Any` ], :obj:`~typing.Any` ], optional + deserializer : typing.Callable [ [ typing.Any ], typing.Any ], optional The deserializer to use to deserialize raw elements. - raw_name : :obj:`~str`, optional + raw_name : str, optional The raw name of the element in its raw serialized form. If not provided, then this will use the field's default name later. - transient : :obj:`~bool` - If :obj:`~True`, the field is marked as transient, meaning it will not be - serialized. Defaults to :obj:`~False`. + transient : bool + If `True`, the field is marked as transient, meaning it will not be + serialized. Defaults to `False`. if_none Either a default factory function called to get the default for when - this field is :obj:`~None` or one of :obj:`~None`, :obj:`~False` or - :obj:`~True` to specify that this should default to the given singleton. - Will raise an exception when :obj:`~None` is received for this field - later if this isn't specified. + this field is `None` or one of `None`, `False` or `True` to specify that + this should default to the given singleton. Will raise an exception when + `None` is received for this field later if this isn't specified. if_undefined Either a default factory function called to get the default for when - this field isn't defined or one of :obj:`~None`, :obj:`~False` or - :obj:`~True` to specify that this should default to the given singleton. - Will raise an exception when this field is undefined later on if this - isn't specified. - serializer : :obj:`~typing.Callable` [ [ :obj:`~typing.Any` ], :obj:`~typing.Any` ], optional + this field isn't defined or one of `None`, `False` or `True` to specify + that this should default to the given singleton. Will raise an exception + when this field is undefined later on if this isn't specified. + serializer : typing.Callable [ [ typing.Any ], typing.Any ], optional The serializer to use. If not specified, then serializing the entire - class that this attribute is in will trigger a :obj:`~TypeError` - later. + class that this attribute is in will trigger a `TypeError` later. **kwargs : - Any kwargs to pass to :func:`attr.ib`. + Any kwargs to pass to `attr.ib`. Returns ------- - :obj:`~typing.Any` - The result of :func:`attr.ib` internally being called with additional - metadata. + typing.Any + The result of `attr.ib` internally being called with additional metadata. """ # Sphinx decides to be really awkward and inject the wrong default values # by default. Not helpful when it documents non-optional shit as defaulting @@ -267,9 +267,8 @@ class HikariEntityMarshaller: """Hikari's utility to manage automated serialization and deserialization. It can deserialize and serialize any internal components that that are - decorated with the :obj:`~marshallable` decorator, and that are - :func:`attr.s` classes using fields with the :obj:`~attrib` function call - descriptor. + decorated with the `marshallable` decorator, and that are + `attr.s` classes using fields with the`attrib` function call descriptor. """ __slots__ = ("_registered_entities",) @@ -282,19 +281,19 @@ def register(self, cls: typing.Type[EntityT]) -> typing.Type[EntityT]: Parameters ---------- - cls : :obj:`~typing.Type` [ :obj:`~typing.Any` ] + cls : typing.Type [ typing.Any ] The type to register. Returns ------- - :obj:`~typing.Type` [ :obj:`~typing.Any` ] + typing.Type [ typing.Any ] The input argument. This enables this to be used as a decorator if desired. Raises ------ - :obj:`~TypeError` - If the class is not an :obj:`~attr.s` class. + TypeError + If the class is not an `attr.s` class. """ entity_descriptor = _construct_entity_descriptor(cls) self._registered_entities[cls] = entity_descriptor @@ -305,24 +304,24 @@ def deserialize(self, raw_data: more_typing.JSONObject, target_type: typing.Type Parameters ---------- - raw_data : :obj:`~typing.Mapping` [ :obj:`~str`, :obj:`~typing.Any` ] + raw_data : typing.Mapping [ str, typing.Any ] The raw data to deserialize. - target_type : :obj:`~typing.Type` [ :obj:`~typing.Any` ] + target_type : typing.Type [ typing.Any ] The type to deserialize to. Returns ------- - :obj:`~typing.Any` + typing.Any The deserialized instance. Raises ------ - :obj:`~LookupError` + LookupError If the entity is not registered. - :obj:`~AttributeError` + AttributeError If the field is not optional, but the field was not present in the - raw payload, or it was present, but it was assigned :obj:`~None`. - :obj:`~TypeError` + raw payload, or it was present, but it was assigned `None`. + TypeError If the deserialization call failed for some reason. """ try: @@ -378,17 +377,17 @@ def serialize(self, obj: typing.Optional[typing.Any]) -> more_typing.NullableJSO Parameters ---------- - obj : :obj:`~typing.Any`, optional + obj : typing.Any, optional The entity to serialize. Returns ------- - :obj:`~typing.Mapping` [ :obj:`~str`, :obj:`~typing.Any` ], optional + typing.Mapping [ str, typing.Any ], optional The serialized raw data item. Raises ------ - :obj:`~LookupError` + LookupError If the entity is not registered. """ if obj is None: @@ -417,11 +416,11 @@ def serialize(self, obj: typing.Optional[typing.Any]) -> more_typing.NullableJSO def marshallable(*, marshaller: HikariEntityMarshaller = HIKARI_ENTITY_MARSHALLER): - """Create a decorator for a class to make it into an :obj:`~attr.s` class. + """Create a decorator for a class to make it into an `attr.s` class. Parameters ---------- - marshaller : :obj:`~HikariEntityMarshaller` + marshaller : HikariEntityMarshaller If specified, this should be an instance of a marshaller to use. For most internal purposes, you want to not specify this, since it will then default to the hikari-global marshaller instead. This is @@ -429,25 +428,20 @@ def marshallable(*, marshaller: HikariEntityMarshaller = HIKARI_ENTITY_MARSHALLE Returns ------- - ``decorator(T) -> T`` + `decorator(T) -> T` A decorator to decorate a class with. - Notes - ----- - The ``auto_attribs`` functionality provided by :obj:`~attr.s` is not - supported by this marshaller utility. Do not attempt to use it! - - Example - ------- - - .. code-block:: python + !!! note + The `auto_attribs` functionality provided by `attr.s` is not + supported by this marshaller utility. Do not attempt to use it! + Examples + -------- @attrs() class MyEntity: id: int = attrib(deserializer=int, serializer=str) password: str = attrib(deserializer=int, transient=True) ... - """ def decorator(cls): diff --git a/hikari/internal/meta.py b/hikari/internal/meta.py index a5027e7b0d..1c9690b366 100644 --- a/hikari/internal/meta.py +++ b/hikari/internal/meta.py @@ -16,10 +16,7 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . -"""Various functional types and metatypes. - -|internal| -""" +"""Various functional types and metatypes.""" __all__ = ["SingletonMeta", "Singleton"] import abc @@ -35,25 +32,21 @@ class SingletonMeta(type): Once an instance has been defined at runtime, it will exist until the interpreter that created it is terminated. - Example - ------- - .. code-block:: python - + Examples + -------- >>> class Unknown(metaclass=SingletonMeta): ... def __init__(self): ... print("Initialized an Unknown!") >>> Unknown() is Unknown() # True - Note - ---- - The constructors of instances of this metaclass must not take any arguments - other than ``self``. + !!! note + The constructors of instances of this metaclass must not take any + arguments other than `self`. - Warning - ------- - Constructing instances of class instances of this metaclass may not be - thread safe. + !!! warning + Constructing instances of class instances of this metaclass may not be + thread safe. """ # pylint: disable=E1136 @@ -69,29 +62,25 @@ def __call__(cls): class Singleton(metaclass=SingletonMeta): - """Base type for anything implementing the :obj:`~SingletonMeta` metaclass. + """Base type for anything implementing the `SingletonMeta` metaclass. Once an instance has been defined at runtime, it will exist until the interpreter that created it is terminated. - Example - ------- - .. code-block:: python - + Examples + -------- >>> class MySingleton(Singleton): ... pass >>> assert MySingleton() is MySingleton() - Note - ---- - The constructors of child classes must not take any arguments other than - ``self``. + !!! note + The constructors of child classes must not take any arguments other than + `self`. - Warning - ------- - Constructing instances of this class or derived classes may not be thread - safe. + !!! warning + Constructing instances of this class or derived classes may not be + thread safe. """ @@ -102,10 +91,9 @@ class UniqueFunctionMeta(abc.ABCMeta): mixins provide the same function, a type error is raised when the class is defined. - Note - ---- - This metaclass derives from :obj:`~abc.ABCMeta`, and thus is compatible - with abstract method conduit. + !!! note + This metaclass derives from `abc.ABCMeta`, and thus is compatible with + abstract method conduit. """ @classmethod diff --git a/hikari/internal/more_asyncio.py b/hikari/internal/more_asyncio.py index 4bd02e05dc..d8ca5b3871 100644 --- a/hikari/internal/more_asyncio.py +++ b/hikari/internal/more_asyncio.py @@ -16,13 +16,10 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . -"""Asyncio extensions and utilities. - -|internal| -""" +"""Asyncio extensions and utilities.""" from __future__ import annotations -__all__ = ["completed_future"] +__all__ = ["completed_future", "wait"] import asyncio import typing @@ -45,12 +42,12 @@ def completed_future(result=None, /): Parameters ---------- - result : :obj:`~typing.Any` + result : typing.Any The value to set for the result of the future. Returns ------- - :obj:`~asyncio.Future` + asyncio.Future The completed future. """ future = asyncio.get_event_loop().create_future() @@ -67,8 +64,8 @@ def wait( Returns ------- - :obj:`~typing.Tuple` with two :obj:`~typing.Set` of futures. - The coroutine returned by :obj:`~asyncio.wait` of two sets of + `typing.Tuple with two typing.Set of futures` + The coroutine returned by `asyncio.wait` of two sets of Tasks/Futures (done, pending). """ # noinspection PyTypeChecker diff --git a/hikari/internal/more_collections.py b/hikari/internal/more_collections.py index ea1263de09..cad8cea367 100644 --- a/hikari/internal/more_collections.py +++ b/hikari/internal/more_collections.py @@ -16,10 +16,7 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . -"""Special data structures and utilities. - -|internal| -""" +"""Special data structures and utilities.""" __all__ = [ "EMPTY_SEQUENCE", @@ -49,18 +46,15 @@ class WeakKeyDictionary(typing.Generic[_K, _V], weakref.WeakKeyDictionary, typing.MutableMapping[_K, _V]): """A dictionary that has weak references to the keys. - This is a type-safe version of :obj:`~weakref.WeakKeyDictionary` which - is subscriptable. - - Example - ------- - .. code-block:: python + This is a type-safe version of `weakref.WeakKeyDictionary` which is + subscriptable. + Examples + -------- @attr.s(auto_attribs=True) class Commands: instances: Set[Command] aliases: WeakKeyDictionary[Command, str] - """ __slots__ = () diff --git a/hikari/internal/more_enums.py b/hikari/internal/more_enums.py index 84eadf9406..2b3206cb75 100644 --- a/hikari/internal/more_enums.py +++ b/hikari/internal/more_enums.py @@ -25,17 +25,15 @@ class EnumMixin: """Mixin for a non-flag enum type. - This gives a more meaningful ``__str__`` implementation. + This gives a more meaningful `__str__` implementation. - The class should inherit this mixin before any type defined in :mod:`~enum`. + The class should inherit this mixin before any type defined in `enum`. """ __slots__ = () - #: The name of the enum member. - #: - #: :obj:`~str` name: str + """The name of the enum member.""" def __str__(self) -> str: return self.name @@ -44,17 +42,15 @@ def __str__(self) -> str: class FlagMixin: """Mixin for a flag enum type. - This gives a more meaningful ``__str__`` implementation. + This gives a more meaningful `__str__` implementation. - The class should inherit this mixin before any type defined in :mod:`~enum`. + The class should inherit this mixin before any type defined in `enum`. """ __slots__ = () - #: The name of the enum member. - #: - #: :obj:`~str` name: str + """The name of the enum member.""" def __str__(self) -> str: return ", ".join(flag.name for flag in typing.cast(typing.Iterable, type(self)) if flag & self) diff --git a/hikari/internal/more_typing.py b/hikari/internal/more_typing.py index f5790d3ac8..a37b1223d0 100644 --- a/hikari/internal/more_typing.py +++ b/hikari/internal/more_typing.py @@ -20,8 +20,18 @@ # pylint:disable=unused-variable from __future__ import annotations -# Don't export anything. -__all__ = [] +__all__ = [ + "JSONType", + "NullableJSONArray", + "JSONObject", + "NullableJSONObject", + "JSONArray", + "NullableJSONType", + "Headers", + "Coroutine", + "Future", + "Task", +] import asyncio import contextvars @@ -50,78 +60,78 @@ # HTTP TYPE HINT HELPERS # ########################## -#: Any JSON type. JSONType = _Union[ _Mapping[str, "NullableJSONType"], _Sequence["NullableJSONType"], _AnyStr, int, float, bool, ] +"""Any JSON type.""" -#: Any JSON type, including ``null``. NullableJSONType = _Optional[JSONType] +"""Any JSON type, including `null`.""" -#: A mapping produced from a JSON object. JSONObject = _Mapping[str, NullableJSONType] +"""A mapping produced from a JSON object.""" -#: A mapping produced from a JSON object that may or may not be present. NullableJSONObject = _Optional[JSONObject] +"""A mapping produced from a JSON object that may or may not be present.""" -#: A sequence produced from a JSON array. JSONArray = _Sequence[NullableJSONType] +"""A sequence produced from a JSON array.""" -#: A sequence produced from a JSON array that may or may not be present. NullableJSONArray = _Optional[JSONArray] +"""A sequence produced from a JSON array that may or may not be present.""" -#: HTTP headers. Headers = _Mapping[str, _Union[_Sequence[str], str]] +"""HTTP headers.""" ############################# # ASYNCIO TYPE HINT HELPERS # ############################# -#: A coroutine object. -#: -#: This is awaitable but MUST be awaited somewhere to be -#: completed correctly. Coroutine = _Coroutine[_Any, _Any, T_co] +"""A coroutine object. + +This is awaitable but MUST be awaited somewhere to be completed correctly. +""" @_runtime_checkable class Future(_Protocol[T_contra]): - """Typed protocol representation of an :obj:`~asyncio.Future`. + """Typed protocol representation of an `asyncio.Future`. - You should consult the documentation for :obj:`~asyncio.Future` for usage. + You should consult the documentation for `asyncio.Future` for usage. """ def result(self) -> T_contra: - """See :meth:`asyncio.Future.result`.""" + """See `asyncio.Future.result`.""" def set_result(self, result: T_contra, /) -> None: - """See :meth:`asyncio.Future.set_result`.""" + """See `asyncio.Future.set_result`.""" def set_exception(self, exception: Exception, /) -> None: - """See :meth:`asyncio.Future.set_exception`.""" + """See `asyncio.Future.set_exception`.""" def done(self) -> bool: - """See :meth:`asyncio.Future.done`.""" + """See `asyncio.Future.done`.""" def cancelled(self) -> bool: - """See :meth:`asyncio.Future.cancelled`.""" + """See `asyncio.Future.cancelled`.""" def add_done_callback( self, callback: _Callable[[Future[T_contra]], None], /, *, context: _Optional[contextvars.Context], ) -> None: - """See :meth:`asyncio.Future.add_done_callback`.""" + """See `asyncio.Future.add_done_callback`.""" def remove_done_callback(self, callback: _Callable[[Future[T_contra]], None], /) -> None: - """See :meth:`asyncio.Future.remove_done_callback`.""" + """See `asyncio.Future.remove_done_callback`.""" def cancel(self) -> bool: - """See :meth:`asyncio.Future.cancel`.""" + """See `asyncio.Future.cancel`.""" def exception(self) -> _Optional[Exception]: - """See :meth:`asyncio.Future.exception`.""" + """See `asyncio.Future.exception`.""" def get_loop(self) -> asyncio.AbstractEventLoop: - """See :meth:`asyncio.Future.get_loop`.""" + """See `asyncio.Future.get_loop`.""" def __await__(self) -> Coroutine[T_contra]: ... @@ -129,54 +139,54 @@ def __await__(self) -> Coroutine[T_contra]: @_runtime_checkable class Task(_Protocol[T_contra]): - """Typed protocol representation of an :obj:`~asyncio.Task`. + """Typed protocol representation of an `asyncio.Task`. - You should consult the documentation for :obj:`~asyncio.Task` for usage. + You should consult the documentation for `asyncio.Task` for usage. """ def result(self) -> T_contra: - """See :meth:`asyncio.Future.result`.""" + """See`asyncio.Future.result`.""" def set_result(self, result: T_contra, /) -> None: - """See :meth:`asyncio.Future.set_result`.""" + """See `asyncio.Future.set_result`.""" def set_exception(self, exception: Exception, /) -> None: - """See :meth:`asyncio.Future.set_exception`.""" + """See `asyncio.Future.set_exception`.""" def done(self) -> bool: - """See :meth:`asyncio.Future.done`.""" + """See `asyncio.Future.done`.""" def cancelled(self) -> bool: - """See :meth:`asyncio.Future.cancelled`.""" + """See `asyncio.Future.cancelled`.""" def add_done_callback( self, callback: _Callable[[Future[T_contra]], None], /, *, context: _Optional[contextvars.Context], ) -> None: - """See :meth:`asyncio.Future.add_done_callback`.""" + """See `asyncio.Future.add_done_callback`.""" def remove_done_callback(self, callback: _Callable[[Future[T_contra]], None], /) -> None: - """See :meth:`asyncio.Future.remove_done_callback`.""" + """See `asyncio.Future.remove_done_callback`.""" def cancel(self) -> bool: - """See :meth:`asyncio.Future.cancel`.""" + """See `asyncio.Future.cancel`.""" def exception(self) -> _Optional[Exception]: - """See :meth:`asyncio.Future.exception`.""" + """See `asyncio.Future.exception`.""" def get_loop(self) -> asyncio.AbstractEventLoop: - """See :meth:`asyncio.Future.get_loop`.""" + """See `asyncio.Future.get_loop`.""" def get_stack(self, *, limit: _Optional[int] = None) -> _Sequence[_FrameType]: - """See :meth:`asyncio.Task.get_stack`.""" + """See `asyncio.Task.get_stack`.""" def print_stack(self, *, limit: _Optional[int] = None, file: _Optional[_IO] = None) -> None: - """See :meth:`asyncio.Task.print_stack`.""" + """See `asyncio.Task.print_stack`.""" def get_name(self) -> str: - """See :meth:`asyncio.Task.get_name`.""" + """See `asyncio.Task.get_name`.""" def set_name(self, value: str, /) -> None: - """See :meth:`asyncio.Task.set_name`.""" + """See `asyncio.Task.set_name`.""" def __await__(self) -> Coroutine[T_contra]: ... diff --git a/hikari/internal/urls.py b/hikari/internal/urls.py index f5ba2d37de..9a78accf0f 100644 --- a/hikari/internal/urls.py +++ b/hikari/internal/urls.py @@ -16,10 +16,7 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . -"""Discord-specific URIs that have to be hard-coded. - -|internal| -""" +"""Discord-specific URIs that have to be hard-coded.""" __all__ = [ "generate_cdn_url", @@ -30,16 +27,13 @@ from hikari.internal import assertions -#: The URL for the CDN. -#: -#: :type: :obj:`~str` BASE_CDN_URL: typing.Final[str] = "https://cdn.discordapp.com" +"""The URL for the CDN.""" -#: The URL for the REST API. This contains a version number parameter that -#: should be interpolated. -#: -#: :type: :obj:`~str` REST_API_URL: typing.Final[str] = "https://discordapp.com/api/v{0.version}" +"""The URL for the REST API. This contains a version number parameter that +should be interpolated. +""" def generate_cdn_url(*route_parts: str, fmt: str, size: typing.Optional[int]) -> str: @@ -47,26 +41,26 @@ def generate_cdn_url(*route_parts: str, fmt: str, size: typing.Optional[int]) -> Parameters ---------- - route_parts : :obj:`~str` + route_parts : str The string route parts that will be used to form the link. - fmt : :obj:`~str` + fmt : str The format to use for the wanted cdn entity, will usually be one of - ``webp``, ``png``, ``jpeg``, ``jpg`` or ``gif`` (which will be invalid + `webp`, `png`, `jpeg`, `jpg` or `gif` (which will be invalid if the target entity doesn't have an animated version available). - size : :obj:`~int`, optional + size : int, optional The size to specify for the image in the query string if applicable, - should be passed through as :obj:`~None` to avoid the param being set. + should be passed through as None to avoid the param being set. Must be any power of two between 16 and 4096. Returns ------- - :obj:`~str` + str The URL to the resource on the Discord CDN. Raises ------ - :obj:`~ValueError` - If ``size`` is not a power of two or not between 16 and 4096. + ValueError + If `size` is not a power of two or not between 16 and 4096. """ if size: assertions.assert_in_range(size, 16, 4096) diff --git a/hikari/invites.py b/hikari/invites.py index 7478ed5c11..3fab4abe1c 100644 --- a/hikari/invites.py +++ b/hikari/invites.py @@ -38,8 +38,8 @@ class TargetUserType(enum.IntEnum): """The reason a invite targets a user.""" - #: This invite is targeting a "Go Live" stream. STREAM = 1 + """This invite is targeting a "Go Live" stream.""" @marshaller.marshallable() @@ -47,15 +47,11 @@ class TargetUserType(enum.IntEnum): class VanityUrl(bases.HikariEntity, marshaller.Deserializable): """A special case invite object, that represents a guild's vanity url.""" - #: The code for this invite. - #: - #: :type: :obj:`~str` code: str = marshaller.attrib(deserializer=str) + """The code for this invite.""" - #: The amount of times this invite has been used. - #: - #: :type: :obj:`~int` uses: int = marshaller.attrib(deserializer=int) + """The amount of times this invite has been used.""" @marshaller.marshallable() @@ -63,64 +59,54 @@ class VanityUrl(bases.HikariEntity, marshaller.Deserializable): class InviteGuild(guilds.PartialGuild): """Represents the partial data of a guild that'll be attached to invites.""" - #: The hash of the splash for the guild, if there is one. - #: - #: :type: :obj:`~str`, optional splash_hash: typing.Optional[str] = marshaller.attrib(raw_name="splash", deserializer=str, if_none=None) + """The hash of the splash for the guild, if there is one.""" - #: The hash for the guild's banner. - #: - #: This is only present if :obj:`~hikari.guilds.GuildFeature.BANNER` - #: is in the ``features`` for this guild. For all other purposes, it is - #: :obj:`~None`. - #: - #: :type: :obj:`~str`, optional banner_hash: typing.Optional[str] = marshaller.attrib(raw_name="banner", if_none=None, deserializer=str) + """The hash for the guild's banner. + + This is only present if `hikari.guilds.GuildFeature.BANNER` is in the + `features` for this guild. For all other purposes, it is `None`. + """ - #: The guild's description. - #: - #: This is only present if certain ``features`` are set in this guild. - #: Otherwise, this will always be :obj:`~None`. For all other purposes, it is - #: :obj:`~None`. - #: - #: :type: :obj:`~str`, optional description: typing.Optional[str] = marshaller.attrib(if_none=None, deserializer=str) + """The guild's description. + + This is only present if certain `features` are set in this guild. + Otherwise, this will always be `None`. For all other purposes, it is `None`. + """ - #: The verification level required for a user to participate in this guild. - #: - #: :type: :obj:`~hikari.guilds.GuildVerificationLevel` verification_level: guilds.GuildVerificationLevel = marshaller.attrib(deserializer=guilds.GuildVerificationLevel) + """The verification level required for a user to participate in this guild.""" - #: The vanity URL code for the guild's vanity URL. - #: - #: This is only present if :obj:`~hikari.guilds.GuildFeature.VANITY_URL` - #: is in the ``features`` for this guild. If not, this will always be - #: :obj:`~None`. - #: - #: :type: :obj:`~str`, optional vanity_url_code: typing.Optional[str] = marshaller.attrib(if_none=None, deserializer=str) + """The vanity URL code for the guild's vanity URL. + + This is only present if `hikari.guilds.GuildFeature.VANITY_URL` is in the + `features` for this guild. If not, this will always be `None`. + """ def format_splash_url(self, fmt: str = "png", size: int = 4096) -> typing.Optional[str]: """Generate the URL for this guild's splash, if set. Parameters ---------- - fmt : :obj:`~str` - The format to use for this URL, defaults to ``png``. - Supports ``png``, ``jpeg``, ``jpg` and ``webp``. - size : :obj:`~int` - The size to set for the URL, defaults to ``4096``. + fmt : str + The format to use for this URL, defaults to `png`. + Supports `png`, `jpeg`, `jpg` and `webp`. + size : int + The size to set for the URL, defaults to `4096`. Can be any power of two between 16 and 4096. Returns ------- - :obj:`~str`, optional + str, optional The string URL. Raises ------ - :obj:`~ValueError` - If ``size`` is not a power of two or not between 16 and 4096. + ValueError + If `size` is not a power of two or not between 16 and 4096. """ if self.splash_hash: return urls.generate_cdn_url("splashes", str(self.id), self.splash_hash, fmt=fmt, size=size) @@ -136,22 +122,22 @@ def format_banner_url(self, fmt: str = "png", size: int = 4096) -> typing.Option Parameters ---------- - fmt : :obj:`~str` - The format to use for this URL, defaults to ``png``. - Supports ``png``, ``jpeg``, ``jpg`` and ``webp``. - size : :obj:`~int` - The size to set for the URL, defaults to ``4096``. + fmt : str + The format to use for this URL, defaults to `png`. + Supports `png`, `jpeg`, `jpg` and `webp`. + size : int + The size to set for the URL, defaults to `4096`. Can be any power of two between 16 and 4096. Returns ------- - :obj:`~str`, optional + str, optional The string URL. Raises ------ - :obj:`~ValueError` - If ``size`` is not a power of two or not between 16 and 4096. + ValueError + If `size` is not a power of two or not between 16 and 4096. """ if self.banner_hash: return urls.generate_cdn_url("banners", str(self.id), self.banner_hash, fmt=fmt, size=size) @@ -168,104 +154,91 @@ def banner_url(self) -> typing.Optional[str]: class Invite(bases.HikariEntity, marshaller.Deserializable): """Represents an invite that's used to add users to a guild or group dm.""" - #: The code for this invite. - #: - #: :type: :obj:`~str` code: str = marshaller.attrib(deserializer=str) + """The code for this invite.""" - #: The partial object of the guild this dm belongs to. - #: Will be :obj:`~None` for group dm invites. - #: - #: :type: :obj:`~InviteGuild`, optional guild: typing.Optional[InviteGuild] = marshaller.attrib( deserializer=InviteGuild.deserialize, if_undefined=None, default=None ) - #: The partial object of the channel this invite targets. - #: - #: :type: :obj:`~hikari.channels.PartialChannel` + """The partial object of the guild this dm belongs to. + + Will be `None` for group dm invites. + """ + channel: channels.PartialChannel = marshaller.attrib(deserializer=channels.PartialChannel.deserialize) + """The partial object of the channel this invite targets.""" - #: The object of the user who created this invite. - #: - #: :type: :obj:`~hikari.users.User`, optional inviter: typing.Optional[users.User] = marshaller.attrib( deserializer=users.User.deserialize, if_undefined=None, default=None ) + """The object of the user who created this invite.""" - #: The object of the user who this invite targets, if set. - #: - #: :type: :obj:`~hikari.users.User`, optional target_user: typing.Optional[users.User] = marshaller.attrib( deserializer=users.User.deserialize, if_undefined=None, default=None ) + """The object of the user who this invite targets, if set.""" - #: The type of user target this invite is, if applicable. - #: - #: :type: :obj:`~TargetUserType`, optional target_user_type: typing.Optional[TargetUserType] = marshaller.attrib( deserializer=TargetUserType, if_undefined=None, default=None ) + """The type of user target this invite is, if applicable.""" - #: The approximate amount of presences in this invite's guild, only present - #: when ``with_counts`` is passed as :obj:`~True` to the GET Invites endpoint. - #: - #: :type: :obj:`~int`, optional approximate_presence_count: typing.Optional[int] = marshaller.attrib( deserializer=int, if_undefined=None, default=None ) + """The approximate amount of presences in this invite's guild. + + This is only present when `with_counts` is passed as `True` to the GET + Invites endpoint. + """ - #: The approximate amount of members in this invite's guild, only present - #: when ``with_counts`` is passed as :obj:`~True` to the GET Invites endpoint. - #: - #: :type: :obj:`~int`, optional approximate_member_count: typing.Optional[int] = marshaller.attrib( deserializer=int, if_undefined=None, default=None ) + """The approximate amount of members in this invite's guild, only present + + This is only present when `with_counts` is passed as `True` to the GET + Invites endpoint. + """ @marshaller.marshallable() @attr.s(slots=True, kw_only=True) class InviteWithMetadata(Invite): - """Extends the base :obj:`~Invite` object with metadata. + """Extends the base `Invite` object with metadata. The metadata is only returned when getting an invite with guild permissions, rather than it's code. """ - #: The amount of times this invite has been used. - #: - #: :type: :obj:`~int` uses: int = marshaller.attrib(deserializer=int) + """The amount of times this invite has been used.""" - #: The limit for how many times this invite can be used before it expires. - #: If set to ``0`` then this is unlimited. - #: - #: :type: :obj:`~int` max_uses: int = marshaller.attrib(deserializer=int) + """The limit for how many times this invite can be used before it expires. + + If set to `0` then this is unlimited. + """ - #: The timedelta of how long this invite will be valid for. - #: If set to :obj:`~None` then this is unlimited. - #: - #: :type: :obj:`~datetime.timedelta`, optional max_age: typing.Optional[datetime.timedelta] = marshaller.attrib( deserializer=lambda age: datetime.timedelta(seconds=age) if age > 0 else None ) + """The timedelta of how long this invite will be valid for. + + If set to `None` then this is unlimited. + """ - #: Whether this invite grants temporary membership. - #: - #: :type: :obj:`~bool` is_temporary: bool = marshaller.attrib(raw_name="temporary", deserializer=bool) + """Whether this invite grants temporary membership.""" - #: When this invite was created. - #: - #: :type: :obj:`~datetime.datetime` created_at: datetime.datetime = marshaller.attrib(deserializer=conversions.parse_iso_8601_ts) + """When this invite was created.""" @property def expires_at(self) -> typing.Optional[datetime.datetime]: - """When this invite should expire, if ``max_age`` is set. + """When this invite should expire, if `InviteWithMetadata.max_age` is set. - If this invite doesn't have a set expiry then this will be :obj:`~None`. + If this invite doesn't have a set expiry then this will be `None`. """ if self.max_age: return self.created_at + self.max_age diff --git a/hikari/media.py b/hikari/media.py index 6dceecb966..a6b60baca6 100644 --- a/hikari/media.py +++ b/hikari/media.py @@ -43,5 +43,5 @@ async def safe_read_file(file: IO) -> typing.Tuple[str, conversions.FileLikeT]: - """Safely read an ``IO`` like object.""" + """Safely read an `IO` like object.""" raise NotImplementedError # TODO: Nekokatt: update this. diff --git a/hikari/messages.py b/hikari/messages.py index f3911b5a7b..ec0573cade 100644 --- a/hikari/messages.py +++ b/hikari/messages.py @@ -48,67 +48,67 @@ class MessageType(enum.IntEnum): """The type of a message.""" - #: A normal message. DEFAULT = 0 + """A normal message.""" - #: A message to denote a new recipient in a group. RECIPIENT_ADD = 1 + """A message to denote a new recipient in a group.""" - #: A message to denote that a recipient left the group. RECIPIENT_REMOVE = 2 + """A message to denote that a recipient left the group.""" - #: A message to denote a VoIP call. CALL = 3 + """A message to denote a VoIP call.""" - #: A message to denote that the name of a channel changed. CHANNEL_NAME_CHANGE = 4 + """A message to denote that the name of a channel changed.""" - #: A message to denote that the icon of a channel changed. CHANNEL_ICON_CHANGE = 5 + """A message to denote that the icon of a channel changed.""" - #: A message to denote that a message was pinned. CHANNEL_PINNED_MESSAGE = 6 + """A message to denote that a message was pinned.""" - #: A message to denote that a member joined the guild. GUILD_MEMBER_JOIN = 7 + """A message to denote that a member joined the guild.""" - #: A message to denote a Nitro subscription. USER_PREMIUM_GUILD_SUBSCRIPTION = 8 + """A message to denote a Nitro subscription.""" - #: A message to denote a tier 1 Nitro subscription. USER_PREMIUM_GUILD_SUBSCRIPTION_TIER_1 = 9 + """A message to denote a tier 1 Nitro subscription.""" - #: A message to denote a tier 2 Nitro subscription. USER_PREMIUM_GUILD_SUBSCRIPTION_TIER_2 = 10 + """A message to denote a tier 2 Nitro subscription.""" - #: A message to denote a tier 3 Nitro subscription. USER_PREMIUM_GUILD_SUBSCRIPTION_TIER_3 = 11 + """A message to denote a tier 3 Nitro subscription.""" - #: Channel follow add. CHANNEL_FOLLOW_ADD = 12 + """Channel follow add.""" @enum.unique class MessageFlag(enum.IntFlag): """Additional flags for message options.""" - #: None NONE = 0 + """None""" - #: This message has been published to subscribed channels via channel following. CROSSPOSTED = 1 << 0 + """This message has been published to subscribed channels via channel following.""" - #: This message originated from a message in another channel via channel following. IS_CROSSPOST = 1 << 1 + """This message originated from a message in another channel via channel following.""" - #: Any embeds on this message should be omitted when serializing the message. SUPPRESS_EMBEDS = 1 << 2 + """Any embeds on this message should be omitted when serializing the message.""" - #: The message this crosspost originated from was deleted via channel following. SOURCE_MESSAGE_DELETED = 1 << 3 + """The message this crosspost originated from was deleted via channel following.""" - #: This message came from the urgent message system. URGENT = 1 << 4 + """This message came from the urgent message system.""" @enum.unique @@ -116,18 +116,19 @@ class MessageActivityType(enum.IntEnum): """The type of a rich presence message activity.""" NONE = 0 + """No activity.""" - #: Join an activity. JOIN = 1 + """Join an activity.""" - #: Spectating something. SPECTATE = 2 + """Spectating something.""" - #: Listening to something. LISTEN = 3 + """Listening to something.""" - #: Request to join an activity. JOIN_REQUEST = 5 + """Request to join an activity.""" @marshaller.marshallable() @@ -135,35 +136,23 @@ class MessageActivityType(enum.IntEnum): class Attachment(bases.UniqueEntity, marshaller.Deserializable): """Represents a file attached to a message.""" - #: The name of the file. - #: - #: :type: :obj:`~str` filename: str = marshaller.attrib(deserializer=str) + """The name of the file.""" - #: The size of the file in bytes. - #: - #: :type: :obj:`~int` size: int = marshaller.attrib(deserializer=int) + """The size of the file in bytes.""" - #: The source URL of file. - #: - #: :type: :obj:`~str` url: str = marshaller.attrib(deserializer=str) + """The source URL of file.""" - #: The proxied URL of file. - #: - #: :type: :obj:`~str` proxy_url: str = marshaller.attrib(deserializer=str) + """The proxied URL of file.""" - #: The height of the image (if the file is an image). - #: - #: :type: :obj:`~int`, optional height: typing.Optional[int] = marshaller.attrib(deserializer=int, if_undefined=None, default=None) + """The height of the image (if the file is an image).""" - #: The width of the image (if the file is an image). - #: - #: :type: :obj:`~int`, optional width: typing.Optional[int] = marshaller.attrib(deserializer=int, if_undefined=None, default=None) + """The width of the image (if the file is an image).""" @marshaller.marshallable() @@ -171,22 +160,16 @@ class Attachment(bases.UniqueEntity, marshaller.Deserializable): class Reaction(bases.HikariEntity, marshaller.Deserializable): """Represents a reaction in a message.""" - #: The amount of times the emoji has been used to react. - #: - #: :type: :obj:`~int` count: int = marshaller.attrib(deserializer=int) + """The amount of times the emoji has been used to react.""" - #: The emoji used to react. - #: - #: :type: :obj:`~typing.Union` [ :obj:`~hikari.emojis.UnicodeEmoji`, :obj:`~hikari.emojis.UnknownEmoji`] emoji: typing.Union[_emojis.UnicodeEmoji, _emojis.UnknownEmoji] = marshaller.attrib( deserializer=_emojis.deserialize_reaction_emoji ) + """The emoji used to react.""" - #: Whether the current user reacted using this emoji. - #: - #: :type: :obj:`~bool` is_reacted_by_me: bool = marshaller.attrib(raw_name="me", deserializer=bool) + """Whether the current user reacted using this emoji.""" @marshaller.marshallable() @@ -194,15 +177,11 @@ class Reaction(bases.HikariEntity, marshaller.Deserializable): class MessageActivity(bases.HikariEntity, marshaller.Deserializable): """Represents the activity of a rich presence-enabled message.""" - #: The type of message activity. - #: - #: :type: :obj:`~MessageActivityType` type: MessageActivityType = marshaller.attrib(deserializer=MessageActivityType) + """The type of message activity.""" - #: The party ID of the message activity. - #: - #: :type: :obj:`~str`, optional party_id: typing.Optional[str] = marshaller.attrib(deserializer=str, if_undefined=None, default=None) + """The party ID of the message activity.""" @marshaller.marshallable() @@ -210,37 +189,30 @@ class MessageActivity(bases.HikariEntity, marshaller.Deserializable): class MessageCrosspost(bases.HikariEntity, marshaller.Deserializable): """Represents information about a cross-posted message and the origin of the original message.""" - #: The ID of the original message. - #: - #: Warning - #: ------- - #: This may be :obj:`~None` in some cases according to the Discord API - #: documentation, but the situations that cause this to occur are not - #: currently documented. - #: - #: - #: :type: :obj:`~hikari.bases.Snowflake`, optional message_id: typing.Optional[bases.Snowflake] = marshaller.attrib( deserializer=bases.Snowflake.deserialize, if_undefined=None, default=None ) + """The ID of the original message. + + !!! warning + This may be `None` in some cases according to the Discord API + documentation, but the situations that cause this to occur are not + currently documented. + """ - #: The ID of the channel that the message originated from. - #: - #: :type: :obj:`~hikari.bases.Snowflake` channel_id: bases.Snowflake = marshaller.attrib(deserializer=bases.Snowflake.deserialize) + """The ID of the channel that the message originated from.""" - #: The ID of the guild that the message originated from. - #: - #: Warning - #: ------- - #: This may be :obj:`~None` in some cases according to the Discord API - #: documentation, but the situations that cause this to occur are not - #: currently documented. - #: - #: :type: :obj:`~hikari.bases.Snowflake`, optional guild_id: typing.Optional[bases.Snowflake] = marshaller.attrib( deserializer=bases.Snowflake.deserialize, if_undefined=None, default=None ) + """The ID of the guild that the message originated from. + + !!! warning + This may be `None` in some cases according to the Discord API + documentation, but the situations that cause this to occur are not + currently documented. + """ @marshaller.marshallable() @@ -248,150 +220,105 @@ class MessageCrosspost(bases.HikariEntity, marshaller.Deserializable): class Message(bases.UniqueEntity, marshaller.Deserializable): """Represents a message.""" - #: The ID of the channel that the message was sent in. - #: - #: :type: :obj:`~hikari.bases.Snowflake` channel_id: bases.Snowflake = marshaller.attrib(deserializer=bases.Snowflake.deserialize) + """The ID of the channel that the message was sent in.""" - #: The ID of the guild that the message was sent in. - #: - #: :type: :obj:`~hikari.bases.Snowflake`, optional guild_id: typing.Optional[bases.Snowflake] = marshaller.attrib( deserializer=bases.Snowflake.deserialize, if_undefined=None, default=None ) + """The ID of the guild that the message was sent in.""" - #: The author of this message. - #: - #: :type: :obj:`~hikari.users.User` author: users.User = marshaller.attrib(deserializer=users.User.deserialize) + """The author of this message.""" - #: The member properties for the message's author. - #: - #: :type: :obj:`~hikari.guilds.GuildMember`, optional member: typing.Optional[guilds.GuildMember] = marshaller.attrib( deserializer=guilds.GuildMember.deserialize, if_undefined=None, default=None ) + """The member properties for the message's author.""" - #: The content of the message. - #: - #: :type: :obj:`~str` content: str = marshaller.attrib(deserializer=str) + """The content of the message.""" - #: The timestamp that the message was sent at. - #: - #: :type: :obj:`~datetime.datetime` timestamp: datetime.datetime = marshaller.attrib(deserializer=conversions.parse_iso_8601_ts) + """The timestamp that the message was sent at.""" - #: The timestamp that the message was last edited at, or :obj:`~None` if it - #: wasn't ever edited. - #: - #: :type: :obj:`~datetime.datetime`, optional edited_timestamp: typing.Optional[datetime.datetime] = marshaller.attrib( deserializer=conversions.parse_iso_8601_ts, if_none=None ) + """The timestamp that the message was last edited at. + + Will be `None` if it wasn't ever edited. + """ - #: Whether the message is a TTS message. - #: - #: :type: :obj:`~bool` is_tts: bool = marshaller.attrib(raw_name="tts", deserializer=bool) + """Whether the message is a TTS message.""" - #: Whether the message mentions ``@everyone`` or ``@here``. - #: - #: :type: :obj:`~bool` is_mentioning_everyone: bool = marshaller.attrib(raw_name="mention_everyone", deserializer=bool) + """Whether the message mentions `@everyone` or `@here`.""" - #: The users the message mentions. - #: - #: :type: :obj:`~typing.Set` [ :obj:`~hikari.bases.Snowflake` ] user_mentions: typing.Set[bases.Snowflake] = marshaller.attrib( raw_name="mentions", deserializer=lambda user_mentions: {bases.Snowflake.deserialize(u["id"]) for u in user_mentions}, ) + """The users the message mentions.""" - #: The roles the message mentions. - #: - #: :type: :obj:`~typing.Set` [ :obj:`~hikari.bases.Snowflake` ] role_mentions: typing.Set[bases.Snowflake] = marshaller.attrib( raw_name="mention_roles", deserializer=lambda role_mentions: {bases.Snowflake.deserialize(mention) for mention in role_mentions}, ) + """The roles the message mentions.""" - #: The channels the message mentions. - #: - #: :type: :obj:`~typing.Set` [ :obj:`~hikari.bases.Snowflake` ] channel_mentions: typing.Set[bases.Snowflake] = marshaller.attrib( raw_name="mention_channels", deserializer=lambda channel_mentions: {bases.Snowflake.deserialize(c["id"]) for c in channel_mentions}, if_undefined=set, factory=set, ) + """The channels the message mentions.""" - #: The message attachments. - #: - #: :type: :obj:`~typing.Sequence` [ :obj:`~Attachment` ] attachments: typing.Sequence[Attachment] = marshaller.attrib( deserializer=lambda attachments: [Attachment.deserialize(a) for a in attachments] ) + """The message attachments.""" - #: The message embeds. - #: - #: :type: :obj:`~typing.Sequence` [ :obj:`~hikari.embeds.Embed` ] embeds: typing.Sequence[_embeds.Embed] = marshaller.attrib( deserializer=lambda embeds: [_embeds.Embed.deserialize(e) for e in embeds] ) + """The message embeds.""" - #: The message reactions. - #: - #: :type: :obj:`~typing.Sequence` [ :obj:`~Reaction` ] reactions: typing.Sequence[Reaction] = marshaller.attrib( deserializer=lambda reactions: [Reaction.deserialize(r) for r in reactions], if_undefined=list, factory=list ) + """The message reactions.""" - #: Whether the message is pinned. - #: - #: :type: :obj:`~bool` is_pinned: bool = marshaller.attrib(raw_name="pinned", deserializer=bool) + """Whether the message is pinned.""" - #: If the message was generated by a webhook, the webhook's id. - #: - #: :type: :obj:`~hikari.bases.Snowflake`, optional webhook_id: typing.Optional[bases.Snowflake] = marshaller.attrib( deserializer=bases.Snowflake.deserialize, if_undefined=None, default=None ) + """If the message was generated by a webhook, the webhook's id.""" - #: The message type. - #: - #: :type: :obj:`~MessageType` type: MessageType = marshaller.attrib(deserializer=MessageType) + """The message type.""" - #: The message activity. - #: - #: :type: :obj:`~MessageActivity`, optional activity: typing.Optional[MessageActivity] = marshaller.attrib( deserializer=MessageActivity.deserialize, if_undefined=None, default=None ) + """The message activity.""" - #: The message application. - #: - #: :type: :obj:`~hikari.oauth2.Application`, optional application: typing.Optional[applications.Application] = marshaller.attrib( deserializer=applications.Application.deserialize, if_undefined=None, default=None ) + """The message application.""" - #: The message crossposted reference data. - #: - #: :type: :obj:`~MessageCrosspost`, optional message_reference: typing.Optional[MessageCrosspost] = marshaller.attrib( deserializer=MessageCrosspost.deserialize, if_undefined=None, default=None ) + """The message crossposted reference data.""" - #: The message flags. - #: - #: :type: :obj:`~MessageFlag`, optional flags: typing.Optional[MessageFlag] = marshaller.attrib(deserializer=MessageFlag, if_undefined=None, default=None) + """The message flags.""" - #: The message nonce. This is a string used for validating - #: a message was sent. - #: - #: :type: :obj:`~str`, optional nonce: typing.Optional[str] = marshaller.attrib(deserializer=str, if_undefined=None, default=None) + """The message nonce. This is a string used for validating a message was sent.""" diff --git a/hikari/net/codes.py b/hikari/net/codes.py index 655f760c22..35075eeebb 100644 --- a/hikari/net/codes.py +++ b/hikari/net/codes.py @@ -30,77 +30,77 @@ class HTTPStatusCode(more_enums.EnumMixin, enum.IntEnum): """HTTP response codes expected from RESTful components.""" - #: Continue CONTINUE = 100 + """Continue""" - #: OK OK = 200 + """OK""" - #: Created CREATED = 201 + """Created""" - #: Accepted ACCEPTED = 202 + """Accepted""" - #: No content NO_CONTENT = 204 + """No content""" - #: Moved permanently MOVED_PERMANENTLY = 301 + """Moved permanently""" - #: Bad request BAD_REQUEST = 400 + """Bad request""" - #: Unauthorized UNAUTHORIZED = 401 + """Unauthorized""" - #: Forbidden FORBIDDEN = 403 + """Forbidden""" - #: Not found NOT_FOUND = 404 + """Not found""" - #: Method not allowed METHOD_NOT_ALLOWED = 405 + """Method not allowed""" - #: Not acceptable NOT_ACCEPTABLE = 406 + """Not acceptable""" - #: Proxy authentication required PROXY_AUTHENTICATION_REQUIRED = 407 + """Proxy authentication required""" - #: Request entitiy too large REQUEST_ENTITY_TOO_LARGE = 413 + """Request entity too large""" - #: Request URI too long REQUEST_URI_TOO_LONG = 414 + """Request URI too long""" - #: Unsupported media type UNSUPPORTED_MEDIA_TYPE = 415 + """Unsupported media type""" - #: Im a teapot IM_A_TEAPOT = 418 + """The server refused the attempt to brew coffee with a teapot.""" - #: Too many requests TOO_MANY_REQUESTS = 429 + """Too many requests""" - #: Internal server error INTERNAL_SERVER_ERROR = 500 + """Internal server error""" - #: Not implemented NOT_IMPLEMENTED = 501 + """Not implemented""" - #: Bad gateway BAD_GATEWAY = 502 + """Bad gateway""" - #: Service unavailable SERVICE_UNAVAILABLE = 503 + """Service unavailable""" - #: Gateway timeout GATEWAY_TIMEOUT = 504 + """Gateway timeout""" - #: HTTP Version not supported HTTP_VERSION_NOT_SUPPORTED = 505 + """HTTP Version not supported""" def __str__(self) -> str: name = self.name.replace("_", " ").title() if self is not HTTPStatusCode.IM_A_TEAPOT else "I'm a teapot" @@ -111,61 +111,71 @@ def __str__(self) -> str: class GatewayCloseCode(more_enums.EnumMixin, enum.IntEnum): """Reasons for closing a gateway connection. - Note - ---- - Any codes greater than or equal to ``4000`` are server-side codes. Any codes - between ``1000`` and ``1999`` inclusive are generally client-side codes. + !!! note + Any codes greater than or equal to `4000` are server-side codes. Any + codes between `1000` and `1999` inclusive are generally client-side codes. """ - #: The application running closed. NORMAL_CLOSURE = 1000 + """The application running closed.""" - #: Discord is not sure what went wrong. Try reconnecting? UNKNOWN_ERROR = 4000 + """Discord is not sure what went wrong. Try reconnecting?""" - #: You sent an invalid Gateway opcode or an invalid payload for an opcode. - #: Don't do that! UNKNOWN_OPCODE = 4001 + """You sent an invalid Gateway opcode or an invalid payload for an opcode. + + Don't do that! + """ - #: You sent an invalid payload to Discord. Don't do that! DECODE_ERROR = 4002 + """You sent an invalid payload to Discord. Don't do that!""" - #: You sent Discord a payload prior to IDENTIFYing. NOT_AUTHENTICATED = 4003 + """You sent Discord a payload prior to IDENTIFYing.""" - #: The account token sent with your identify payload is incorrect. AUTHENTICATION_FAILED = 4004 + """The account token sent with your identify payload is incorrect.""" - #: You sent more than one identify payload. Don't do that! ALREADY_AUTHENTICATED = 4005 + """You sent more than one identify payload. Don't do that!""" - #: The sequence sent when resuming the session was invalid. Reconnect and - #: start a new session. INVALID_SEQ = 4007 + """The sequence sent when resuming the session was invalid. + + Reconnect and start a new session. + """ - #: Woah nelly! You're sending payloads to Discord too quickly. Slow it down! RATE_LIMITED = 4008 + """Woah nelly! You're sending payloads to Discord too quickly. Slow it down!""" - #: Your session timed out. Reconnect and start a new one. SESSION_TIMEOUT = 4009 + """Your session timed out. Reconnect and start a new one.""" - #: You sent Discord an invalid shard when IDENTIFYing. INVALID_SHARD = 4010 + """You sent Discord an invalid shard when IDENTIFYing.""" - #: The session would have handled too many guilds - you are required to - #: shard your connection in order to connect. SHARDING_REQUIRED = 4011 + """The session would have handled too many guilds. + + You are required to shard your connection in order to connect. + """ - #: You sent an invalid version for the gateway. INVALID_VERSION = 4012 + """You sent an invalid version for the gateway.""" - #: You sent an invalid intent for a Gateway Intent. You may have incorrectly - #: calculated the bitwise value. INVALID_INTENT = 4013 + """You sent an invalid intent for a Gateway Intent. + + You may have incorrectly calculated the bitwise value. + """ - #: You sent a disallowed intent for a Gateway Intent. You may have tried to - #: specify an intent that you have not enabled or are not whitelisted for. DISALLOWED_INTENT = 4014 + """You sent a disallowed intent for a Gateway Intent. + + You may have tried to specify an intent that you have not enabled or are not + whitelisted for. + """ def __str__(self) -> str: name = self.name.replace("_", " ").title() @@ -176,42 +186,44 @@ def __str__(self) -> str: class GatewayOpcode(more_enums.EnumMixin, enum.IntEnum): """Opcodes that the gateway uses internally.""" - #: An event was dispatched. DISPATCH = 0 + """An event was dispatched.""" - #: Used for ping checking. HEARTBEAT = 1 + """Used for ping checking.""" - #: Used for client handshake. IDENTIFY = 2 + """Used for client handshake.""" - #: Used to update the client status. PRESENCE_UPDATE = 3 + """Used to update the client status.""" - #: Used to join/move/leave voice channels. VOICE_STATE_UPDATE = 4 + """Used to join/move/leave voice channels.""" - #: Used to resume a closed connection. RESUME = 6 + """Used to resume a closed connection.""" - #: Used to tell clients to reconnect to the gateway. RECONNECT = 7 + """Used to tell clients to reconnect to the gateway.""" - #: Used to request guild members. REQUEST_GUILD_MEMBERS = 8 + """Used to request guild members.""" - #: Used to notify client they have an invalid session id. INVALID_SESSION = 9 + """Used to notify client they have an invalid session id.""" - #: Sent immediately after connecting, contains heartbeat and server debug - #: information. HELLO = 10 + """Sent immediately after connecting. + + Contains heartbeat and server debug information. + """ - #: Sent immediately following a client heartbeat that was received. HEARTBEAT_ACK = 11 + """Sent immediately following a client heartbeat that was received.""" - #: Not yet documented, so do not use. GUILD_SYNC = 12 + """Not yet documented, so do not use.""" def __str__(self) -> str: name = self.name.replace("_", " ").title() @@ -222,205 +234,207 @@ def __str__(self) -> str: class JSONErrorCode(more_enums.EnumMixin, enum.IntEnum): """Error codes that can be returned by the REST API.""" - #: This is sent if the payload is screwed up, etc. GENERAL_ERROR = 0 + """This is sent if the payload is screwed up, etc.""" - #: Unknown account UNKNOWN_ACCOUNT = 10_001 + """Unknown account""" - #: Unknown application UNKNOWN_APPLICATION = 10_002 + """Unknown application""" - #: Unknown channel UNKNOWN_CHANNEL = 10_003 + """Unknown channel""" - #: Unknown guild UNKNOWN_GUILD = 10_004 + """Unknown guild""" - #: Unknown integration UNKNOWN_INTEGRATION = 10_005 + """Unknown integration""" - #: Unknown invite UNKNOWN_INVITE = 10_006 + """Unknown invite""" - #: Unknown member UNKNOWN_MEMBER = 10_007 + """Unknown member""" - #: Unknown message UNKNOWN_MESSAGE = 10_008 + """Unknown message""" - #: Unknown overwrite UNKNOWN_OVERWRITE = 10_009 + """Unknown overwrite""" - #: Unknown provider UNKNOWN_PROVIDER = 10_010 + """Unknown provider""" - #: Unknown role UNKNOWN_ROLE = 10_011 + """Unknown role""" - #: Unknown token UNKNOWN_TOKEN = 10_012 + """Unknown token""" - #: Unknown user UNKNOWN_USER = 10_013 + """Unknown user""" - #: Unknown emoji UNKNOWN_EMOJI = 10_014 + """Unknown emoji""" - #: Unknown webhook UNKNOWN_WEBHOOK = 10_015 + """Unknown Webhook""" - #: Unknown ban UNKNOWN_BAN = 10_026 + """Unknown ban""" - #: Bots cannot use this endpoint - #: - #: Note - #: ---- - #: You should never expect to receive this in normal API usage. USERS_ONLY = 20_001 + """Bots cannot use this endpoint + + !!! note + You should never expect to receive this in normal API usage. + """ - #: Only bots can use this endpoint. - #: - #: Note - #: ---- - #: You should never expect to receive this in normal API usage. BOTS_ONLY = 20_002 + """Only bots can use this endpoint. + + !!! note + You should never expect to receive this in normal API usage. + """ - #: Maximum number of guilds reached (100) - #: - #: Note - #: ---- - #: You should never expect to receive this in normal API usage as this only - #: applies to user accounts. - #: - #: This is unlimited for bot accounts. MAX_GUILDS_REACHED = 30_001 + """Maximum number of guilds reached (100) + + !!! note + You should never expect to receive this in normal API usage as this only + applies to user accounts. + + This is unlimited for bot accounts. + """ - #: Maximum number of friends reached (1000) - #: - #: Note - #: ---- - #: You should never expect to receive this in normal API usage as this only - #: applies to user accounts. - #: - #: Bots cannot have friends :( . MAX_FRIENDS_REACHED = 30_002 + """Maximum number of friends reached (1000) + + !!! note + You should never expect to receive this in normal API usage as this only + applies to user accounts. + + Bots cannot have friends :( . + """ - #: Maximum number of pins reached (50) MAX_PINS_REACHED = 30_003 + """Maximum number of pins reached (50)""" - #: Maximum number of guild roles reached (250) MAX_GUILD_ROLES_REACHED = 30_005 + """Maximum number of guild roles reached (250)""" - #: Maximum number of webhooks reached (10) MAX_WEBHOOKS_REACHED = 30_007 + """Maximum number of webhooks reached (10)""" - #: Maximum number of reactions reached (20) MAX_REACTIONS_REACHED = 30_010 + """Maximum number of reactions reached (20)""" - #: Maximum number of guild channels reached (500) MAX_GUILD_CHANNELS_REACHED = 30_013 + """Maximum number of guild channels reached (500)""" - #: Maximum number of attachments in a message reached (10) MAX_MESSAGE_ATTACHMENTS_REACHED = 30_015 + """Maximum number of attachments in a message reached (10)""" - #: Maximum number of invites reached (10000) MAX_INVITES_REACHED = 30_016 + """Maximum number of invites reached (10000)""" - #: Unauthorized - UNAUTHORIZED = 40_001 - - #: You need to verify your account to perform this action. NEEDS_VERIFICATION = 40_002 + """You need to verify your account to perform this action.""" + + UNAUTHORIZED = 40_001 + """Unauthorized""" - #: Request entity too large. Try sending something smaller in size TOO_LARGE = 40_005 + """Request entity too large. Try sending something smaller in size""" - #: This feature has been temporarily disabled server-side DISABLED_TEMPORARILY = 40_006 + """This feature has been temporarily disabled server-side""" - #: The user is banned from this guild USER_BANNED = 40_007 + """The user is banned from this guild""" - #: Missing access MISSING_ACCESS = 50_001 + """Missing access""" - #: Invalid account type INVALID_ACCOUNT_TYPE = 50_002 + """Invalid account type""" - #: Cannot execute action on a DM channel CANNOT_EXECUTE_ACTION_ON_DM_CHANNEL = 50_003 + """Cannot execute action on a DM channel""" - #: Widget Disabled WIDGET_DISABLED = 50_004 + """Widget Disabled""" - #: Cannot edit a message authored by another user CANNOT_EDIT_A_MESSAGE_AUTHORED_BY_ANOTHER_USER = 50_005 + """Cannot edit a message authored by another user""" - #: Cannot send an empty message CANNOT_SEND_AN_EMPTY_MESSAGE = 50_006 + """Cannot send an empty message""" - #: Cannot send messages to this user CANNOT_SEND_MESSAGES_TO_THIS_USER = 50_007 + """Cannot send messages to this user""" - #: Cannot send messages in a voice channel CANNOT_SEND_MESSAGES_IN_VOICE_CHANNEL = 50_008 + """Cannot send messages in a voice channel""" - #: Channel verification level is too high CHANNEL_VERIFICATION_TOO_HIGH = 50_009 + """Channel verification level is too high""" - #: OAuth2 application does not have a bot OAUTH2_APPLICATION_DOES_NOT_HAVE_A_BOT = 50_010 + """OAuth2 application does not have a bot""" - #: OAuth2 application limit reached OAUTH2_APPLICATION_LIMIT_REACHED = 50_011 + """OAuth2 application limit reached""" - #: Invalid OAuth state INVALID_OAUTH2_STATE = 50_012 + """Invalid OAuth state""" - #: Missing permissions MISSING_PERMISSIONS = 50_013 + """Missing permissions""" - #: Invalid authentication token INVALID_AUTHENTICATION_TOKEN = 50_014 + """Invalid authentication token""" - #: Note is too long NOTE_IS_TOO_LONG = 50_015 + """Note is too long""" - #: Provided too few or too many messages to delete. Must provide at least 2 - #: and fewer than 100 messages to delete. INVALID_NUMBER_OF_MESSAGES_TO_DELETE = 50_016 + """Provided too few or too many messages to delete. + + Must provide at least 2 and fewer than 100 messages to delete. + """ - #: A message can only be pinned to the channel it was sent in CANNOT_PIN_A_MESSAGE_IN_A_DIFFERENT_CHANNEL = 50_019 + """A message can only be pinned to the channel it was sent in""" - #: Invite code is either invalid or taken. INVALID_INVITE = 50_020 + """Invite code is either invalid or taken.""" - #: Cannot execute action on a system message CANNOT_EXECUTE_ACTION_ON_SYSTEM_MESSAGE = 50_021 + """Cannot execute action on a system message""" - #: Invalid OAuth2 access token INVALID_OAUTH2_TOKEN = 50_025 + """Invalid OAuth2 access token""" - #: A message provided was too old to bulk delete MESSAGE_PROVIDED_WAS_TOO_OLD_TO_BULK_DELETE = 50_034 + """A message provided was too old to bulk delete""" - #: Invalid Form Body INVALID_FORM_BODY = 50_035 + """Invalid Form Body""" - #: An invite was accepted to a guild the application's bot is not in ACCEPTED_INVITE_TO_GUILD_BOT_IS_NOT_IN = 50_036 + """An invite was accepted to a guild the application's bot is not in""" - #: Invalid API version INVALID_API_VERSION = 50_041 + """Invalid API version""" - #: Reaction blocked REACTION_BLOCKED = 90_001 + """Reaction blocked""" - #: The resource is overloaded. RESOURCE_OVERLOADED = 130_000 + """The resource is overloaded.""" def __str__(self) -> str: name = self.name.replace("_", " ").title() diff --git a/hikari/net/ratelimits.py b/hikari/net/ratelimits.py index b761c0cde1..706c0a864f 100644 --- a/hikari/net/ratelimits.py +++ b/hikari/net/ratelimits.py @@ -25,49 +25,46 @@ What is the theory behind this implementation? ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -In this module, we refer to a :obj:`~hikari.net.routes.CompiledRoute` as a -definition of a route with specific major parameter values included (e.g. -``POST /channels/123/messages``), and a :obj:`~hikari.net.routes.RouteTemplate` -as a definition of a route without specific parameter values included (e.g. -``POST /channels/{channel_id}/messages``). We can compile a -:obj:`~hikari.net.routes.CompiledRoute` from a -:obj:`~hikari.net.routes.RouteTemplate` by providing the corresponding -parameters as kwargs, as you may already know. +In this module, we refer to a `hikari.net.routes.CompiledRoute` as a definition +of a route with specific major parameter values included (e.g. +`POST /channels/123/messages`), and a `hikari.net.routes.RouteTemplate` as a +definition of a route without specific parameter values included (e.g. +`POST /channels/{channel_id}/messages`). We can compile a +`hikari.net.routes.CompiledRoute` from a `hikari.net.routes.RouteTemplate` +by providing the corresponding parameters as kwargs, as you may already know. In this module, a "bucket" is an internal data structure that tracks and -enforces the rate limit state for a specific -:obj:`~hikari.net.routes.CompiledRoute`, and can manage delaying tasks in the -event that we begin to get rate limited. It also supports providing in-order -execution of queued tasks. +enforces the rate limit state for a specific `hikari.net.routes.CompiledRoute`, +and can manage delaying tasks in the event that we begin to get rate limited. +It also supports providing in-order execution of queued tasks. Discord allocates types of buckets to routes. If you are making a request and there is a valid rate limit on the route you hit, you should receive an -``X-RateLimit-Bucket`` header from the server in your response. This is a hash +`X-RateLimit-Bucket` header from the server in your response. This is a hash that identifies a route based on internal criteria that does not include major -parameters. This ``X-RateLimitBucket`` is known in this module as an -"bucket hash". +parameters. This `X-RateLimitBucket` is known in this module as an "bucket hash". This means that generally, the route `POST /channels/123/messages` and -``POST /channels/456/messages`` will usually sit in the same bucket, but -``GET /channels/123/messages/789`` and ``PATCH /channels/123/messages/789`` will +`POST /channels/456/messages` will usually sit in the same bucket, but +`GET /channels/123/messages/789` and `PATCH /channels/123/messages/789` will usually not share the same bucket. Discord may or may not change this at any time, so hard coding this logic is not a useful thing to be doing. Rate limits, on the other hand, apply to a bucket and are specific to the major -parameters of the compiled route. This means that ``POST /channels/123/messages`` -and ``POST /channels/456/messages`` do not share the same real bucket, despite -Discord providing the same bucket hash. A real bucket hash is the :obj:`~str` -hash of the bucket that Discord sends us in a response concatenated to the -corresponding major parameters. This is used for quick bucket indexing -internally in this module. +parameters of the compiled route. This means that `POST /channels/123/messages` +and `POST /channels/456/messages` do not share the same real bucket, despite +Discord providing the same bucket hash. A real bucket hash is the `str` hash of +the bucket that Discord sends us in a response concatenated to the corresponding +major parameters. This is used for quick bucket indexing internally in this +module. One issue that occurs from this is that we cannot effectively hash a -:obj:`~hikari.net.routes.CompiledRoute` that has not yet been hit, meaning that +`hikari.net.routes.CompiledRoute` that has not yet been hit, meaning that until we receive a response from this endpoint, we have no idea what our rate limits could be, nor the bucket that they sit in. This is usually not problematic, as the first request to an endpoint should never be rate limited unless you are hitting it from elsewhere in the same time window outside your -Hikari application. To manage this situation, unknown endpoints are allocated to +hikari.applications. To manage this situation, unknown endpoints are allocated to a special unlimited bucket until they have an initial bucket hash code allocated from a response. Once this happens, the route is reallocated a dedicated bucket. Unknown buckets have a hardcoded initial hash code internally. @@ -75,19 +72,18 @@ Initially acquiring time on a bucket ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Each time you ``BaseRateLimiter.acquire()`` a request -timeslice for a given :obj:`~hikari.net.routes.CompiledRoute`, several -things happen. The first is that we attempt to find the existing bucket for -that route, if there is one, or get an unknown bucket otherwise. This is done -by creating a real bucket hash` from the compiled route. The initial hash -is calculated using a lookup table that maps :obj:`~hikari.net.routes.CompiledRoute` -objects to their corresponding initial hash codes, or to the unknown bucket -hash code if not yet known. This initial hash is processed by the -:obj:`~hikari.net.routes.CompiledRoute` to provide the real bucket hash we -need to get the route's bucket object internally. - -The ``acquire`` method will take the bucket and acquire a new timeslice on -it. This takes the form of a :obj:`~asyncio.Future` which should be awaited by +Each time you `BaseRateLimiter.acquire()` a request timeslice for a given +`hikari.net.routes.CompiledRoute`, several things happen. The first is that we +attempt to find the existing bucket for that route, if there is one, or get an +unknown bucket otherwise. This is done by creating a real bucket hash` from the +compiled route. The initial hash is calculated using a lookup table that maps +`hikari.net.routes.CompiledRoute` objects to their corresponding initial hash +codes, or to the unknown bucket hash code if not yet known. This initial hash is +processed by the `hikari.net.routes.CompiledRoute` to provide the real bucket +hash we need to get the route's bucket object internally. + +The `acquire` method will take the bucket and acquire a new timeslice on +it. This takes the form of a `asyncio.Future` which should be awaited by the caller and will complete once the caller is allowed to make a request. Most of the time, this is done instantly, but if the bucket has an active rate limit preventing requests being sent, then the future will be paused until the rate @@ -101,15 +97,15 @@ tidies itself up and disposes of itself. This task will complete once the queue becomes empty. -The result of ``RateLimiter.acquire()`` is a tuple of a -:obj:`~asyncio.Future` to await on which completes when you are allowed to -proceed with making a request, and a real bucket hash which should be -stored temporarily. This will be explained in the next section. +The result of `RateLimiter.acquire()` is a tuple of a `asyncio.Future` to await +on which completes when you are allowed to proceed with making a request, and a +real bucket hash which should be stored temporarily. This will be explained in +the next section. When you make your response, you should be sure to set the -``X-RateLimit-Precision`` header to ``millisecond`` to ensure a much greater +`X-RateLimit-Precision` header to `millisecond` to ensure a much greater accuracy against rounding errors for rate limits (reduces the error margin from -``1`` second to ``1`` millisecond). +`1` second to `1` millisecond). Handling the rate limit headers of a response ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -118,28 +114,28 @@ the vital rate limit headers manually and parse them to the correct data types. These headers are: -* ``Date``: +* `Date`: the response date on the server. This should be parsed to a - :obj:`~datetime.datetime` using :func:`email.utils.parsedate_to_datetime`. -* ``X-RateLimit-Limit``: - an :obj:`~int` describing the max requests in the bucket - from empty to being rate limited. -* ``X-RateLimit-Remaining``: - an :obj:`~int` describing the remaining number of - requests before rate limiting occurs in the current window. -* ``X-RateLimit-Bucket``: - a :obj:`~str` containing the initial bucket hash. -* ``X-RateLimit-Reset``: - a :obj:`~float` containing the number of seconds since + `datetime.datetime` using `email.utils.parsedate_to_datetime`. +* `X-RateLimit-Limit`: + an `int` describing the max requests in the bucket from empty to being rate + limited. +* `X-RateLimit-Remaining`: + an `int` describing the remaining number of requests before rate limiting + occurs in the current window. +* `X-RateLimit-Bucket`: + a `str` containing the initial bucket hash. +* `X-RateLimit-Reset`: + a `float` containing the number of seconds since 1st January 1970 at 0:00:00 UTC at which the current ratelimit window - resets. This should be parsed to a :obj:`~datetime.datetime` using - :meth:`datetime.datetime.fromtimestamp`, passing :obj:`~datetime.timezone.utc` - as ``tz``. + resets. This should be parsed to a `datetime.datetime` using + `datetime.datetime.fromtimestamp`, passing `datetime.timezone.utc` + as `tz`. -Each of the above values should be passed to the -``update_rate_limits`` method to ensure that the bucket you acquired time -from is correctly updated should Discord decide to alter their ratelimits on the -fly without warning (including timings and the bucket). +Each of the above values should be passed to the `update_rate_limits` method to +ensure that the bucket you acquired time from is correctly updated should +Discord decide to alter their ratelimits on the fly without warning (including +timings and the bucket). This method will manage creating new buckets as needed and resetting vital information in each bucket you use. @@ -147,17 +143,17 @@ Tidying up ~~~~~~~~~~ -To prevent unused buckets cluttering up memory, each :obj:`~BaseRateLimiter` -instance spins up a :obj:`~asyncio.Task` that periodically locks the bucket -list (not threadsafe, only using the concept of asyncio not yielding in regular +To prevent unused buckets cluttering up memory, each `BaseRateLimiter` +instance spins up a `asyncio.Task` that periodically locks the bucket list +(not threadsafe, only using the concept of asyncio not yielding in regular functions) and disposes of any clearly stale buckets that are no longer needed. These will be recreated again in the future if they are needed. -When shutting down an application, one must remember to ``close()`` the -:obj:`~BaseRateLimiter` that has been used. This will ensure the garbage collection +When shutting down an application, one must remember to `close()` the +`BaseRateLimiter` that has been used. This will ensure the garbage collection task is stopped, and will also ensure any remaining futures in any bucket queues -have an :obj:`~asyncio.CancelledError` set on them to prevent deadlocking -ratelimited calls that may be waiting to be unlocked. +have an `asyncio.CancelledError` set on them to prevent deadlocking ratelimited +calls that may be waiting to be unlocked. """ __all__ = [ "BaseRateLimiter", @@ -183,10 +179,8 @@ from hikari.internal import more_typing from hikari.net import routes -#: The hash used for an unknown bucket that has not yet been resolved. -#: -#: :type: :obj:`~str` UNKNOWN_HASH: typing.Final[str] = "UNKNOWN" +"""The hash used for an unknown bucket that has not yet been resolved.""" class BaseRateLimiter(abc.ABC): @@ -194,9 +188,8 @@ class BaseRateLimiter(abc.ABC): Supports being used as a synchronous context manager. - Warnings - -------- - Async context manager support is not supported and will not be supported. + !!! warning + Async context manager support is not supported and will not be supported. """ __slots__ = () @@ -207,7 +200,7 @@ def acquire(self) -> more_typing.Future[None]: Returns ------- - :obj:`~asyncio.Future` + asyncio.Future A future that should be awaited. Once the future is complete, you can proceed to execute your rate-limited task. """ @@ -232,25 +225,17 @@ class BurstRateLimiter(BaseRateLimiter, abc.ABC): __slots__ = ("name", "throttle_task", "queue", "logger") - #: The name of the rate limiter. - #: - #: :type: :obj:`~str` name: typing.Final[str] + """The name of the rate limiter.""" - #: The throttling task, or :obj:`~None` if it isn't running. - #: - #: :type: :obj:`~asyncio.Task`, optional throttle_task: typing.Optional[more_typing.Task[None]] + """The throttling task, or `None` if it isn't running.""" - #: The queue of any futures under a rate limit. - #: - #: :type: :obj:`~asyncio.Queue` [`asyncio.Future`] queue: typing.Final[typing.List[more_typing.Future[None]]] + """The queue of any futures under a rate limit.""" - #: The logger used by this rate limiter. - #: - #: :type: :obj:`~logging.Logger` logger: typing.Final[logging.Logger] + """The logger used by this rate limiter.""" def __init__(self, name: str) -> None: self.name = name @@ -266,7 +251,7 @@ def acquire(self) -> more_typing.Future[None]: Returns ------- - :obj:`~asyncio.Future` + asyncio.Future A future that should be immediately awaited. Once the await completes, you are able to proceed with the operation that is under this rate limit. @@ -294,7 +279,7 @@ def close(self) -> None: @property def is_empty(self) -> bool: - """Return :obj:`~True` if no futures are on the queue being rate limited.""" + """Return `True` if no futures are on the queue being rate limited.""" return len(self.queue) == 0 @@ -302,12 +287,12 @@ class ManualRateLimiter(BurstRateLimiter): """Rate limit handler for the global REST rate limit. This is a non-preemptive rate limiting algorithm that will always return - completed futures until :meth:`throttle` is invoked. Once this is invoked, - any subsequent calls to :meth:`acquire` will return incomplete futures - that will be enqueued to an internal queue. A task will be spun up to wait - for a period of time given to the :meth:`throttle`. Once that has passed, - the lock will begin to re-consume incomplete futures on the queue, - completing them. + completed futures until `ManualRateLimiter.throttle` is invoked. Once this + is invoked, any subsequent calls to `ManualRateLimiter.acquire` will return + incomplete futures that will be enqueued to an internal queue. A task will + be spun up to wait for a period of time given to the + `ManualRateLimiter.throttle`. Once that has passed, the lock will begin to + re-consume incomplete futures on the queue, completing them. Triggering a throttle when it is already set will cancel the current throttle task that is sleeping and replace it. @@ -328,7 +313,7 @@ def acquire(self) -> more_typing.Future[None]: Returns ------- - :obj:`~asyncio.Future` + asyncio.Future A future that should be immediately awaited. Once the await completes, you are able to proceed with the operation that is under this rate limit. @@ -347,23 +332,22 @@ def throttle(self, retry_after: float) -> None: Iterates repeatedly while the queue is not empty, adhering to any rate limits that occur in the mean time. - retry_after : :obj:`~float` + retry_after : float How long to sleep for before unlocking and releasing any futures in the queue. - Note - ---- - This will invoke :meth:`unlock_later` as a scheduled task in the future - (it will not await it to finish) + !!! note + This will invoke `ManualRateLimiter.unlock_later` as a scheduled task + in the future (it will not await it to finish). - When the :meth:`unlock_later` coroutine function completes, it should be - expected to set the `throttle_task`` to :obj:`~None`. This means you can - check if throttling is occurring by checking if ``throttle_task`` - is not :obj:`~None`. + When the `ManualRateLimiter.unlock_later` coroutine function + completes, it should be expected to set the `throttle_task` to + `None`. This means you can check if throttling is occurring by + checking if `throttle_task` is not `None`. - If this is invoked while another throttle is in progress, that one is - cancelled and a new one is started. This enables new rate limits to - override existing ones. + If this is invoked while another throttle is in progress, that one + is cancelled and a new one is started. This enables new rate limits + to override existing ones. """ if self.throttle_task is not None: self.throttle_task.cancel() @@ -376,19 +360,18 @@ async def unlock_later(self, retry_after: float) -> None: Parameters ---------- - retry_after : :obj:`~float` + retry_after : float How long to sleep for before unlocking and releasing any futures in the queue. - Note - ---- - You shouldn't need to invoke this directly. Call :meth:`throttle` - instead. + !!! note + You shouldn't need to invoke this directly. Call + `ManualRateLimiter.throttle` instead. - When the :meth:`unlock_later` coroutine function completes, it should be - expected to set the ``throttle_task`` to :obj:`~None`. This means you can - check if throttling is occurring by checking if ``throttle_task`` - is not :obj:`~None`. + When the `ManualRateLimiter.unlock_later` coroutine function + completes, it should be expected to set the `throttle_task` to + `None`. This means you can check if throttling is occurring by + checking if `throttle_task` is not `None`. """ self.logger.warning("you are being globally rate limited for %ss", retry_after) await asyncio.sleep(retry_after) @@ -404,8 +387,8 @@ class WindowedBurstRateLimiter(BurstRateLimiter): Rate limiter for rate limits that last fixed periods of time with a fixed number of times it can be used in that time frame. - To use this, you should call :meth:`acquire` and await the result - immediately before performing your rate-limited task. + To use this, you should call WindowedBurstRateLimiter.aquire` and await the + result immediately before performing your rate-limited task. If the rate limit has been hit, acquiring time will return an incomplete future that is placed on the internal queue. A throttle task is then spun up @@ -429,26 +412,21 @@ class WindowedBurstRateLimiter(BurstRateLimiter): __slots__ = ("reset_at", "remaining", "limit", "period") - #: The :func:`time.perf_counter` that the limit window ends at. - #: - #: :type: :obj:`~float` reset_at: float + """The `time.perf_counter` that the limit window ends at.""" - #: The number of :meth:`acquire`'s left in this window before you will get - #: rate limited. - #: - #: :type: :obj:`~int` remaining: int + """The number of `WindowedBurstRateLimiter.acquire`'s left in this window + before you will get rate limited. + """ - #: How long the window lasts for from the start in seconds. - #: - #: :type: :obj:`~float` period: float + """How long the window lasts for from the start in seconds.""" - #: The maximum number of :meth:`acquire`'s allowed in this time window. - #: - #: :type: :obj:`~int` limit: int + """The maximum number of `WindowedBurstRateLimiter.acquire`'s allowed in + this time window. + """ def __init__(self, name: str, period: float, limit: int) -> None: super().__init__(name) @@ -462,7 +440,7 @@ def acquire(self) -> more_typing.Future[None]: Returns ------- - :obj:`~asyncio.Future` + asyncio.Future A future that should be immediately awaited. Once the await completes, you are able to proceed with the operation that is under this rate limit. @@ -489,22 +467,21 @@ def get_time_until_reset(self, now: float) -> float: Parameters ---------- - now : :obj:`~float` - The monotonic :func:`time.perf_counter` timestamp. + now : float + The monotonic `time.perf_counter` timestamp. Returns ------- - :obj:`~float` + float The time left to sleep before the rate limit is reset. If no rate limit - is in effect, then this will return ``0.0`` instead. - - Warning - ------- - Invoking this method will update the internal state if we were - previously rate limited, but at the given time are no longer under that - limit. This makes it imperative that you only pass the current timestamp - to this function, and not past or future timestamps. The effects of - doing the latter are undefined behaviour. + is in effect, then this will return `0.0` instead. + + !!! warning + Invoking this method will update the internal state if we were + previously rate limited, but at the given time are no longer under + that limit. This makes it imperative that you only pass the current + timestamp to this function, and not past or future timestamps. The + effects of doing the latter are undefined behaviour. """ if not self.is_rate_limited(now): return 0.0 @@ -515,21 +492,20 @@ def is_rate_limited(self, now: float) -> bool: Parameters ---------- - now : :obj:`~float` - The monotonic :func:`time.perf_counter` timestamp. + now : float + The monotonic `time.perf_counter` timestamp. Returns ------- - :obj:`~bool` - :obj:`~True` if we are being rate limited. :obj:`~False` if we are not. - - Warning - ------- - Invoking this method will update the internal state if we were - previously rate limited, but at the given time are no longer under that - limit. This makes it imperative that you only pass the current timestamp - to this function, and not past or future timestamps. The effects of - doing the latter are undefined behaviour. + bool + `True` if we are being rate limited. `False` if we are not. + + !!! warning + Invoking this method will update the internal state if we were + previously rate limited, but at the given time are no longer under + that limit. This makes it imperative that you only pass the current + timestamp to this function, and not past or future timestamps. The + effects of doing the latter are undefined behaviour. """ if self.reset_at <= now: self.remaining = self.limit @@ -548,15 +524,14 @@ async def throttle(self) -> None: Iterates repeatedly while the queue is not empty, adhering to any rate limits that occur in the mean time. - Note - ---- - You should usually not need to invoke this directly, but if you do, - ensure to call it using :func:`asyncio.create_task`, and store the - task immediately in ``throttle_task``. + !!! note + You should usually not need to invoke this directly, but if you do, + ensure to call it using `asyncio.create_task`, and store the + task immediately in `throttle_task`. - When this coroutine function completes, it will set the - ``throttle_task`` to :obj:`~None`. This means you can check if throttling - is occurring by checking if ``throttle_task`` is not :obj:`~None`. + When this coroutine function completes, it will set the + `throttle_task` to `None`. This means you can check if throttling + is occurring by checking if `throttle_task` is not `None`. """ self.logger.debug( "you are being rate limited on bucket %s, backing off for %ss", @@ -581,7 +556,7 @@ class RESTBucket(WindowedBurstRateLimiter): Component to represent an active rate limit bucket on a specific REST route with a specific major parameter combo. - This is somewhat similar to the :obj:`~WindowedBurstRateLimiter` in how it + This is somewhat similar to the `WindowedBurstRateLimiter` in how it works. This algorithm will use fixed-period time windows that have a given limit @@ -593,16 +568,14 @@ class RESTBucket(WindowedBurstRateLimiter): capacity to zero, and tasks that are queued will start being able to drip again. - Additional logic is provided by the :meth:`update_rate_limit` call which - allows dynamically changing the enforced rate limits at any time. + Additional logic is provided by the `RESTBucket.update_rate_limit` call + which allows dynamically changing the enforced rate limits at any time. """ __slots__ = ("compiled_route",) - #: The compiled route that this rate limit is covering. - #: - #: :type: :obj:`~hikari.net.routes.CompiledRoute` compiled_route: typing.Final[routes.CompiledRoute] + """The compiled route that this rate limit is covering.""" def __init__(self, name: str, compiled_route: routes.CompiledRoute) -> None: super().__init__(name, 1, 1) @@ -612,7 +585,7 @@ def __init__(self, name: str, compiled_route: routes.CompiledRoute) -> None: @property def is_unknown(self) -> bool: - """Return :obj:`~True` if the bucket represents an ``UNKNOWN`` bucket.""" + """Return `True` if the bucket represents an `UNKNOWN` bucket.""" return self.name.startswith(UNKNOWN_HASH) def acquire(self) -> more_typing.Future[None]: @@ -620,14 +593,13 @@ def acquire(self) -> more_typing.Future[None]: Returns ------- - :obj:`~asyncio.Future` + asyncio.Future A future that should be awaited immediately. Once the future completes, you are allowed to proceed with your operation. - Note - ---- - You should afterwards invoke :meth:`update_rate_limit` to update any - rate limit information you are made aware of. + !!! note + You should afterwards invoke `RESTBucket.update_rate_limit` to + update any rate limit information you are made aware of. """ return more_asyncio.completed_future(None) if self.is_unknown else super().acquire() @@ -636,17 +608,16 @@ def update_rate_limit(self, remaining: int, limit: int, reset_at: float) -> None Parameters ---------- - remaining : :obj:`~int` + remaining : int The calls remaining in this time window. - limit : :obj:`~int` + limit : int The total calls allowed in this time window. - reset_at : :obj:`~float` + reset_at : float The epoch at which to reset the limit. - Note - ---- - The ``reset_at`` epoch is expected to be a :func:`time.perf_counter` - monotonic epoch, rather than a :func:`time.time` date-based epoch. + !!! note + The `reset_at` epoch is expected to be a `time.perf_counter` + monotonic epoch, rather than a `time.time` date-based epoch. """ self.remaining = remaining self.limit = limit @@ -656,10 +627,9 @@ def update_rate_limit(self, remaining: int, limit: int, reset_at: float) -> None def drip(self) -> None: """Decrement the remaining count for this bucket. - Note - ---- - If the bucket is marked as :attr:`is_unknown`, then this will not do - anything. ``Unknown`` buckets have infinite rate limits. + !!! note + If the bucket is marked as `RESTBucket.is_unknown`, then this will + not do anything. `Unknown` buckets have infinite rate limits. """ # We don't drip unknown buckets: we can't rate limit them as we don't know their real bucket hash or # the current rate limit values Discord put on them... @@ -671,7 +641,7 @@ class RESTBucketManager: """The main rate limiter implementation for REST clients. This is designed to provide bucketed rate limiting for Discord REST - endpoints that respects the ``X-RateLimit-Bucket`` rate limit header. To do + endpoints that respects the `X-RateLimit-Bucket` rate limit header. To do this, it makes the assumption that any limit can change at any time. """ @@ -683,32 +653,23 @@ class RESTBucketManager: "logger", ) - #: Maps compiled routes to their ``X-RateLimit-Bucket`` header being used. - #: - #: :type: :obj:`~typing.MutableMapping` [ :obj:`~hikari.net.routes.CompiledRoute`, :obj:`~str` ] routes_to_hashes: typing.Final[typing.MutableMapping[routes.CompiledRoute, str]] + """Maps compiled routes to their `X-RateLimit-Bucket` header being used.""" - #: Maps full bucket hashes (``X-RateLimit-Bucket`` appended with a hash of - #: major parameters used in that compiled route) to their corresponding rate - #: limiters. - #: - #: :type: :obj:`~typing.MutableMapping` [ :obj:`~str`, :obj:`~RESTBucket` ] real_hashes_to_buckets: typing.Final[typing.MutableMapping[str, RESTBucket]] + """Maps full bucket hashes (`X-RateLimit-Bucket` appended with a hash of + major parameters used in that compiled route) to their corresponding rate + limiters. + """ - #: An internal event that is set when the object is shut down. - #: - #: :type: :obj:`~asyncio.Event` closed_event: typing.Final[asyncio.Event] + """An internal event that is set when the object is shut down.""" - #: The internal garbage collector task. - #: - #: :type: :obj:`~asyncio.Task`, optional gc_task: typing.Optional[more_typing.Task[None]] + """The internal garbage collector task.""" - #: The logger to use for this object. - #: - #: :type: :obj:`~logging.Logger` logger: typing.Final[logging.Logger] + """The logger to use for this object.""" def __init__(self) -> None: self.routes_to_hashes = weakref.WeakKeyDictionary() @@ -735,9 +696,9 @@ def start(self, poll_period: float = 20) -> None: Parameters ---------- - poll_period : :obj:`~float` + poll_period : float Period to poll the garbage collector at in seconds. Defaults - to ``20`` seconds. + to `20` seconds. """ if not self.gc_task: self.gc_task = asyncio.get_running_loop().create_task(self.gc(poll_period)) @@ -761,18 +722,18 @@ async def gc(self, poll_period: float = 20) -> None: # noqa: D401 This is designed to run in the background and manage removing unused route references from the rate-limiter collection to save memory. - This will run forever until :attr:`closed_event` is set. This will - invoke :meth:`do_gc_pass` periodically. + This will run forever until `RESTBucketManager. closed_event` is set. + This will invoke `RESTBucketManager.do_gc_pass` periodically. Parameters ---------- - poll_period : :obj:`~float` - The period to poll at. This defaults to once every ``20`` seconds. + poll_period : float + The period to poll at. This defaults to once every `20` seconds. - Warnings - -------- - You generally have no need to invoke this directly. Use - :meth:`start` and :meth:`close` to control this instead. + !!! warning + You generally have no need to invoke this directly. Use + `RESTBucketManager.start` and `RESTBucketManager.close` to control + this instead. """ # Prevent filling memory increasingly until we run out by removing dead buckets every 20s # Allocations are somewhat cheap if we only do them every so-many seconds, after all. @@ -795,10 +756,10 @@ def do_gc_pass(self) -> None: If the removed routes are used again in the future, they will be re-cached automatically. - Warning - ------- - You generally have no need to invoke this directly. Use - :meth:`start` and :meth:`close` to control this instead. + !!! warning + You generally have no need to invoke this directly. Use + `RESTBucketManager.start` and `RESTBucketManager.close` to control + this instead. """ buckets_to_purge = [] @@ -820,21 +781,20 @@ def acquire(self, compiled_route: routes.CompiledRoute) -> more_typing.Future: Parameters ---------- - compiled_route : :obj:`~hikari.net.routes.CompiledRoute` + compiled_route : hikari.net.routes.CompiledRoute The route to get the bucket for. Returns ------- - :obj:`~asyncio.Future` + asyncio.Future A future to await that completes when you are allowed to run your request logic. - Note - ---- - The returned future MUST be awaited, and will complete when your turn to - make a call comes along. You are expected to await this and then - immediately make your REST call. The returned future may already be - completed if you can make the call immediately. + !!! note + The returned future MUST be awaited, and will complete when your + turn to make a call comes along. You are expected to await this and + then immediately make your REST call. The returned future may + already be completed if you can make the call immediately. """ # Returns a future to await on to wait to be allowed to send the request, and a # bucket hash to use to update rate limits later. @@ -868,20 +828,19 @@ def update_rate_limits( Parameters ---------- - compiled_route : :obj:`~hikari.net.routes.CompiledRoute` + compiled_route : hikari.net.routes.CompiledRoute The compiled route to get the bucket for. - bucket_header : :obj:`~str`, optional - The ``X-RateLimit-Bucket`` header that was provided in the response, - or :obj:`~None` if not present. - remaining_header : :obj:`~int` - The ``X-RateLimit-Remaining`` header cast to an :obj:`~int`. - limit_header : :obj:`~int` - The ``X-RateLimit-Limit`` header cast to an :obj:`~int`. - date_header : :obj:`~datetime.datetime` - The ``Date`` header value as a :obj:`~datetime.datetime`. - reset_at_header : :obj:`~datetime.datetime` - The ``X-RateLimit-Reset`` header value as a - :obj:`~datetime.datetime`. + bucket_header : str, optional + The `X-RateLimit-Bucket` header that was provided in the response, + or `None` if not present. + remaining_header : int + The `X-RateLimit-Remaining` header cast to an `int`. + limit_header : int + The `X-RateLimit-Limit`header cast to an `int`. + date_header : datetime.datetime + The `Date` header value as a `datetime.datetime`. + reset_at_header : datetime.datetime + The `X-RateLimit-Reset` header value as a `datetime.datetime`. """ self.routes_to_hashes[compiled_route] = bucket_header @@ -903,51 +862,46 @@ class ExponentialBackOff: .. math:: - t_{backoff} = b^{i} + m \\cdot rand() + t_{backoff} = b^{i} + m \cdot \mathrm{rand}() - Such that :math:`t_{backoff}` is the backoff time, :math:`b` is the base, - :math:`i` is the increment that increases by 1 for each invocation, and - :math:`m` is the jitter multiplier. :math:`rand()` returns a value in the - range :math:`[0,1)`. + Such that \(t_{backoff}\) is the backoff time, \(b\) is the base, + \(i\) is the increment that increases by 1 for each invocation, and + \(m\) is the jitter multiplier. \(\mathrm{rand}()\) returns a value in + the range \([0,1]\). Parameters ---------- - base : :obj:`~float` - The base to use. Defaults to ``2``. - maximum : :obj:`~float`, optional - If not :obj:`~None`, then this is the max value the backoff can be in a - single iteration before an :obj:`~asyncio.TimeoutError` is raised. - Defaults to ``64`` seconds. - jitter_multiplier : :obj:`~float` - The multiplier for the random jitter. Defaults to ``1``. - Set to ``0`` to disable jitter. - initial_increment : :obj:`~int` - The initial increment to start at. Defaults to ``0``. + base : float + The base to use. Defaults to `2`. + maximum : float, optional + If not `None`, then this is the max value the backoff can be in a + single iteration before an `asyncio.TimeoutError` is raised. + Defaults to `64` seconds. + jitter_multiplier : float + The multiplier for the random jitter. Defaults to `1`. + Set to `0` to disable jitter. + initial_increment : int + The initial increment to start at. Defaults to `0`. """ __slots__ = ("base", "increment", "maximum", "jitter_multiplier") - #: The base to use. Defaults to 2. - #: - #: :type: :obj:`~float` base: typing.Final[float] + """The base to use. Defaults to 2.""" - #: The current increment. - #: - #: :type: :obj:`~int` increment: int + """The current increment.""" - #: If not :obj:`~None`, then this is the max value the backoff can be in a - #: single iteration before an :obj:`~asyncio.TimeoutError` is raised. - #: - #: :type: :obj:`~float`, optional maximum: typing.Optional[float] + """If not `None`, then this is the max value the backoff can be in a + single iteration before an `asyncio.TimeoutError` is raised. + """ - #: The multiplier for the random jitter. Defaults to ``1``. - #: Set to ``0`` to disable jitter. - #: - #: :type: :obj:`~float` jitter_multiplier: typing.Final[float] + """The multiplier for the random jitter. + + This defaults to `1`. Set to `0` to disable jitter. + """ def __init__( self, diff --git a/hikari/net/rest.py b/hikari/net/rest.py index 934df23a70..35313a2e21 100644 --- a/hikari/net/rest.py +++ b/hikari/net/rest.py @@ -51,40 +51,38 @@ class REST: Parameters ---------- - base_url: :obj:`~str` + base_url: str The base URL and route for the discord API - allow_redirects: :obj:`~bool` + allow_redirects: bool Whether to allow redirects or not. - connector: :obj:`~aiohttp.BaseConnector`, optional + connector: aiohttp.BaseConnector, optional Optional aiohttp connector info for making an HTTP connection - proxy_headers: :obj:`~typing.Mapping` [ :obj:`~str`, :obj:`~str` ], optional + proxy_headers: typing.Mapping [ str, str ], optional Optional proxy headers to pass to HTTP requests. - proxy_auth: :obj:`~aiohttp.BasicAuth`, optional + proxy_auth: aiohttp.BasicAuth, optional Optional authorization to be used if using a proxy. - proxy_url: :obj:`~str`, optional + proxy_url: str, optional Optional proxy URL to use for HTTP requests. - ssl_context: :obj:`~ssl.SSLContext`, optional + ssl_context: ssl.SSLContext, optional The optional SSL context to be used. - verify_ssl: :obj:`~bool` + verify_ssl: bool Whether or not the client should enforce SSL signed certificate - verification. If :obj:`~False` it will ignore potentially malicious + verification. If 1 it will ignore potentially malicious SSL certificates. - timeout: :obj:`~float`, optional + timeout: float, optional The optional timeout for all HTTP requests. - json_deserialize: ``deserialization function`` - A custom JSON deserializer function to use. Defaults to - :func:`json.loads`. - json_serialize: ``serialization function`` - A custom JSON serializer function to use. Defaults to - :func:`json.dumps`. - token: :obj:`~string`, optional + json_deserialize: `deserialization function` + A custom JSON deserializer function to use. Defaults to `json.loads`. + json_serialize: `serialization function` + A custom JSON serializer function to use. Defaults to `json.dumps`. + token: string, optional The bot token for the client to use. You may start this with - a prefix of either ``Bot`` or ``Bearer`` to force the token type, or + a prefix of either `Bot` or `Bearer` to force the token type, or not provide this information if you want to have it auto-detected. - If this is passed as :obj:`~None`, then no token is used. - This will be passed as the ``Authorization`` header if not :obj:`~None` + If this is passed as `None`, then no token is used. + This will be passed as the `Authorization` header if not `None` for each request. - version: :obj:`~int` + version: int The version of the API to use. Defaults to the most recent stable version (v6). """ @@ -99,129 +97,112 @@ class REST: _AUTHENTICATION_SCHEMES: typing.Final[typing.Tuple[str, ...]] = ("Bearer", "Bot") - #: :obj:`~True` if HTTP redirects are enabled, or :obj:`~False` otherwise. - #: - #: :type: :obj:`~bool` allow_redirects: bool + """`True` if HTTP redirects are enabled, or `False` otherwise.""" - #: The base URL to send requests to. - #: - #: :type: :obj:`~str` base_url: str + """The base URL to send requests to.""" - #: The :mod:`aiohttp` client session to make requests with. - #: - #: This will be :obj:`~None` until the first request, due to limitations - #: with how :obj:`aiohttp.ClientSession` is able to be initialized outside - #: of a running event loop. - #: - #: :type: :obj:`~aiohttp.ClientSession` client_session: typing.Optional[aiohttp.ClientSession] + """The `aiohttp` client session to make requests with. + + This will be `None` until the first request, due to limitations with how + `aiohttp.ClientSession` is able to be initialized outside of a running + event loop. + """ - #: The base connector for the :obj:`~aiohttp.ClientSession`, if provided. - #: - #: :type: :obj:`~aiohttp.BaseConnector`, optional connector: typing.Optional[aiohttp.BaseConnector] + """The base connector for the `aiohttp.ClientSession`, if provided.""" - #: The internal correlation ID for the number of requests sent. This will - #: increase each time a REST request is sent. - #: - #: :type: :obj:`~int` in_count: int + """The internal correlation ID for the number of requests sent. + + This will increase each time a REST request is sent. + """ - #: The global ratelimiter. This is used if Discord declares a ratelimit - #: across the entire API, regardless of the endpoint. If this is set, then - #: any HTTP operation using this session will be paused. - #: - #: :type: :obj:`~hikari.net.ratelimits.ManualRateLimiter` global_ratelimiter: ratelimits.ManualRateLimiter + """The global ratelimiter. + + This is used if Discord declares a ratelimit across the entire API, + regardless of the endpoint. If this is set, then any HTTP operation using + this session will be paused. + """ - #: The logger to use for this object. - #: - #: :type: :obj:`~logging.Logger` logger: logging.Logger + """The logger to use for this object.""" - #: The JSON deserialization function. This consumes a JSON string and - #: produces some object. json_deserialize: typing.Callable[[typing.AnyStr], typing.Any] + """The JSON deserialization function. + + This consumes a JSON string and produces some object. + """ - #: The JSON deserialization function. This consumes an object and - #: produces some JSON string. json_serialize: typing.Callable[[typing.Any], typing.AnyStr] + """The JSON deserialization function. + + This consumes an object and produces some JSON string. + """ - #: Proxy authorization to use. - #: - #: :type: :obj:`~aiohttp.BasicAuth`, optional proxy_auth: typing.Optional[aiohttp.BasicAuth] + """Proxy authorization to use.""" - #: A set of headers to provide to a proxy server. - #: - #: :type: :obj:`~typing.Mapping` [ :obj:`~str`, :obj:`~str` ], optional proxy_headers: typing.Optional[typing.Mapping[str, str]] + """A set of headers to provide to a proxy server.""" - #: An optional proxy URL to send requests to. - #: - #: :type: :obj:`~str`, optional proxy_url: typing.Optional[str] + """An optional proxy URL to send requests to.""" - #: The per-route ratelimit manager. This handles tracking any ratelimits - #: for routes that have recently been used or are in active use, as well - #: as keeping memory usage to a minimum where possible for large numbers - #: of varying requests. This encapsulates a lot of complex rate limiting - #: rules to reduce the number of active ``429`` responses this client gets, - #: and thus reducing your chances of an API ban by Discord. - #: - #: You should not ever need to touch this implementation. - #: - #: :type: :obj:`~hikari.net.ratelimits.RESTBucketManager` bucket_ratelimiters: ratelimits.RESTBucketManager + """The per-route ratelimit manager. + + This handles tracking any ratelimits for routes that have recently been used + or are in active use, as well as keeping memory usage to a minimum where + possible for large numbers of varying requests. This encapsulates a lot of + complex rate limiting rules to reduce the number of active `429` responses + this client gets, and thus reducing your chances of an API ban by Discord. + + You should not ever need to touch this implementation. + """ - #: The custom SSL context to use. - #: - #: :type: :obj:`~ssl.SSLContext` ssl_context: typing.Optional[ssl.SSLContext] + """The custom SSL context to use.""" - #: The HTTP request timeout to abort requests after. - #: - #: :type: :obj:`~float` timeout: typing.Optional[float] + """The HTTP request timeout to abort requests after.""" - #: The bot token. This will be prefixed with either ``"Bearer "`` or - #: ``"Bot"`` depending on the format of the token passed to the constructor. - #: - #: This value will be used for the ``Authorization`` HTTP header on each - #: API request. - #: - #: If no token is set, then the value will be :obj:`~None`. In this case, - #: no ``Authorization`` header will be sent. - #: - #: :type: :obj:`~str`, optional token: typing.Optional[str] + """The bot token. + + This will be prefixed with either `"Bearer"` or `"Bot"` depending on the + format of the token passed to the constructor. + + This value will be used for the `Authorization` HTTP header on each + API request. + + If no token is set, then the value will be `None`. In this case, + no `Authorization` header will be sent. + """ - #: The ``User-Agent`` header to send to Discord. - #: - #: Warning - #: ------- - #: Changing this value may lead to undesirable results, as Discord document - #: that they can actively IP ban any client that does not have a valid - #: ``User-Agent`` header that conforms to specific requirements. - #: Your mileage may vary (YMMV). - #: - #: :type: :obj:`~str` user_agent: str + """The `User-Agent` header to send to Discord. + + !!! warning + Changing this value may lead to undesirable results, as Discord document + that they can actively IP ban any client that does not have a valid + `User-Agent` header that conforms to specific requirements. + Your mileage may vary (YMMV). + """ - #: If :obj:`~True`, SSL certificates are verified for each request, and - #: invalid SSL certificates are rejected, causing an exception. If - #: :obj:`~False`, then unrecognised certificates that may be illegitimate - #: are accepted and ignored. - #: - #: :type: :obj:`~bool` verify_ssl: bool + """Whether SSL certificates should be verified for each request. + + When this is `True` then an exception will be raised whenever invalid SSL + certificates are received. When this is `False` unrecognised certificates + that may be illegitimate are accepted and ignored. + """ - #: The API version number that is being used. - #: - #: :type: :obj:`~int` version: int + """The API version number that is being used.""" def __init__( self, @@ -472,12 +453,11 @@ async def get_gateway(self) -> str: Returns ------- - :obj:`~str` + str A static URL to use to connect to the gateway with. - Note - ---- - Users are expected to attempt to cache this result. + !!! note + Users are expected to attempt to cache this result. """ result = await self._request(routes.GATEWAY.compile(self.GET)) return result["url"] @@ -487,13 +467,14 @@ async def get_gateway_bot(self) -> typing.Dict[str, typing.Any]: Returns ------- - :obj:`~typing.Dict` [ :obj:`~str`, :obj:`~typing.Any` ] - An object containing a ``url`` to connect to, an :obj:`~int` number of shards recommended to use - for connecting, and a ``session_start_limit`` object. + typing.Dict [ str, typing.Any ] + An object containing a `url` to connect to, an `int` number of + shards recommended to use for connecting, and a + `session_start_limit` object. - Note - ---- - Unlike :meth:`get_gateway`, this requires a valid token to work. + !!! note + Unlike `LowLevelRestfulClient.get_gateway`, this requires a valid + token to work. """ return await self._request(routes.GATEWAY_BOT.compile(self.GET)) @@ -504,29 +485,29 @@ async def get_guild_audit_log( Parameters ---------- - guild_id : :obj:`~str` + guild_id : str The guild ID to look up. - user_id : :obj:`~str` + user_id : str If specified, the user ID to filter by. - action_type : :obj:`~int` + action_type : int If specified, the action type to look up. - limit : :obj:`~int` + limit : int If specified, the limit to apply to the number of records. - Defaults to ``50``. Must be between ``1`` and ``100`` inclusive. - before : :obj:`~str` + Defaults to `50`. Must be between `1` and `100` inclusive. + before : str If specified, the ID of the entry that all retrieved entries will have occurred before. Returns ------- - :obj:`~typing.Dict` [ :obj:`~str`, :obj:`~typing.Any` ] + typing.Dict [ str, typing.Any ] An audit log object. Raises ------ - :obj:`~hikari.errors.ForbiddenHTTPError` + hikari.errors.ForbiddenHTTPError If you lack the given permissions to view an audit log. - :obj:`~hikari.errors.NotFoundHTTPError` + hikari.errors.NotFoundHTTPError If the guild does not exist. """ query = {} @@ -542,19 +523,19 @@ async def get_channel(self, channel_id: str) -> typing.Dict[str, typing.Any]: Parameters ---------- - channel_id : :obj:`~str` + channel_id : str The channel ID to look up. Returns ------- - :obj:`~typing.Dict` [ :obj:`~str`, :obj:`~typing.Any` ] + typing.Dict [ str, typing.Any ] The channel object that has been found. Raises ------ - :obj:`~hikari.errors.ForbiddenHTTPError` + hikari.errors.ForbiddenHTTPError If you don't have access to the channel. - :obj:`~hikari.errors.NotFoundHTTPError` + hikari.errors.NotFoundHTTPError If the channel does not exist. """ route = routes.CHANNEL.compile(self.GET, channel_id=channel_id) @@ -579,58 +560,58 @@ async def modify_channel( # lgtm [py/similar-function] Parameters ---------- - channel_id : :obj:`~str` + channel_id : str The channel ID to update. - name : :obj:`~str` + name : str If specified, the new name for the channel. This must be - between ``2`` and ``100`` characters in length. - position : :obj:`~int` + between `2` and `100` characters in length. + position : int If specified, the position to change the channel to. - topic : :obj:`~str` + topic : str If specified, the topic to set. This is only applicable to - text channels. This must be between ``0`` and ``1024`` + text channels. This must be between `0` and `1024` characters in length. - nsfw : :obj:`~bool` + nsfw : bool If specified, whether the channel will be marked as NSFW. Only applicable to text channels. - rate_limit_per_user : :obj:`~int` + rate_limit_per_user : int If specified, the number of seconds the user has to wait before sending another message. This will not apply to bots, or to members with - ``MANAGE_MESSAGES`` or ``MANAGE_CHANNEL`` permissions. This must - be between ``0`` and ``21600`` seconds. - bitrate : :obj:`~int` + `MANAGE_MESSAGES` or `MANAGE_CHANNEL` permissions. This must + be between `0` and `21600` seconds. + bitrate : int If specified, the bitrate in bits per second allowable for the channel. - This only applies to voice channels and must be between ``8000`` - and ``96000`` for normal servers or ``8000`` and ``128000`` for + This only applies to voice channels and must be between `8000` + and `96000` for normal servers or `8000` and `128000` for VIP servers. - user_limit : :obj:`~int` + user_limit : int If specified, the new max number of users to allow in a voice channel. - This must be between ``0`` and ``99`` inclusive, where - ``0`` implies no limit. - permission_overwrites : :obj:`~typing.Sequence` [ :obj:`~typing.Dict` [ :obj:`~str`, :obj:`~typing.Any` ] ] + This must be between `0` and `99` inclusive, where + `0` implies no limit. + permission_overwrites : typing.Sequence [ typing.Dict [ str, typing.Any ] ] If specified, the new list of permission overwrites that are category specific to replace the existing overwrites with. - parent_id : :obj:`~str`, optional + parent_id : str, optional If specified, the new parent category ID to set for the channel., - pass :obj:`~None` to unset. - reason : :obj:`~str` + pass `None` to unset. + reason : str If specified, the audit log reason explaining why the operation was performed. Returns ------- - :obj:`~typing.Dict` [ :obj:`~str`, :obj:`~typing.Any` ] + typing.Dict [ str, typing.Any ] The channel object that has been modified. Raises ------ - :obj:`~hikari.errors.NotFoundHTTPError` + hikari.errors.NotFoundHTTPError If the channel does not exist. - :obj:`~hikari.errors.ForbiddenHTTPError` + hikari.errors.ForbiddenHTTPError If you lack the permission to make the change. - :obj:`~hikari.errors.BadRequestHTTPError` + hikari.errors.BadRequestHTTPError If you provide incorrect options for the corresponding channel type - (e.g. a ``bitrate`` for a text channel). + (e.g. a `bitrate` for a text channel). """ payload = {} conversions.put_if_specified(payload, "name", name) @@ -650,25 +631,25 @@ async def delete_close_channel(self, channel_id: str) -> None: Parameters ---------- - channel_id : :obj:`~str` + channel_id : str The channel ID to delete, or direct message channel to close. Returns ------- - :obj:`~None` + None Nothing, unlike what the API specifies. This is done to maintain consistency with other calls of a similar nature in this API wrapper. Raises ------ - :obj:`~hikari.errors.NotFoundHTTPError` + hikari.errors.NotFoundHTTPError If the channel does not exist. - :obj:`~hikari.errors.ForbiddenHTTPError` + hikari.errors.ForbiddenHTTPError If you do not have permission to delete the channel. - Warning - ------- - Deleted channels cannot be un-deleted. Deletion of DMs is able to be undone by reopening the DM. + !!! warning + Deleted channels cannot be un-deleted. Deletion of DMs is able to be + undone by reopening the DM. """ route = routes.CHANNEL.compile(self.DELETE, channel_id=channel_id) await self._request(route) @@ -680,47 +661,46 @@ async def get_channel_messages( Parameters ---------- - channel_id : :obj:`~str` + channel_id : str The ID of the channel to retrieve the messages from. - limit : :obj:`~int` + limit : int If specified, the number of messages to return. Must be - between ``1`` and ``100`` inclusive.Defaults to ``50`` + between `1` and `100` inclusive.Defaults to `50` if unspecified. - after : :obj:`~str` + after : str A message ID. If specified, only return messages sent AFTER this message. - before : :obj:`~str` + before : str A message ID. If specified, only return messages sent BEFORE this message. - around : :obj:`~str` + around : str A message ID. If specified, only return messages sent AROUND and including (if it still exists) this message. Returns ------- - :obj:`~typing.Sequence` [ :obj:`~typing.Dict` [ :obj:`~str`, :obj:`~typing.Any` ] ] + typing.Sequence [ typing.Dict [ str, typing.Any ] ] A list of message objects. Raises ------ - :obj:`~hikari.errors.ForbiddenHTTPError` + hikari.errors.ForbiddenHTTPError If you lack permission to read the channel. - :obj:`~hikari.errors.BadRequestHTTPError` - If your query is malformed, has an invalid value for ``limit``, - or contains more than one of ``after``, ``before`` and ``around``. - :obj:`~hikari.errors.NotFoundHTTPError` + hikari.errors.BadRequestHTTPError + If your query is malformed, has an invalid value for `limit`, + or contains more than one of `after`, `before` and `around`. + hikari.errors.NotFoundHTTPError If the channel is not found, or the message provided for one of the filter arguments is not found. - Note - ---- - If you are missing the ``VIEW_CHANNEL`` permission, you will receive a - :obj:`~hikari.errors.ForbiddenHTTPError`. If you are instead missing - the ``READ_MESSAGE_HISTORY`` permission, you will always receive - zero results, and thus an empty list will be returned instead. + !!! note + If you are missing the `VIEW_CHANNEL` permission, you will receive a + `hikari.errors.ForbiddenHTTPError`. If you are instead missing + the `READ_MESSAGE_HISTORY` permission, you will always receive + zero results, and thus an empty list will be returned instead. - Warning - ------- - You can only specify a maximum of one from ``before``, ``after``, and ``around``. - Specifying more than one will cause a :obj:`~hikari.errors.BadRequestHTTPError` to be raised. + !!! warning + You can only specify a maximum of one from `before`, `after`, and + `around`; specifying more than one will cause a + `hikari.errors.BadRequestHTTPError` to be raised. """ query = {} conversions.put_if_specified(query, "limit", limit) @@ -735,25 +715,24 @@ async def get_channel_message(self, channel_id: str, message_id: str) -> typing. Parameters ---------- - channel_id : :obj:`~str` + channel_id : str The ID of the channel to get the message from. - message_id : :obj:`~str` + message_id : str The ID of the message to retrieve. Returns ------- - :obj:`~typing.Dict` [ :obj:`~str`, :obj:`~typing.Any` ] + typing.Dict [ str, typing.Any ] A message object. - Note - ---- - This requires the ``READ_MESSAGE_HISTORY`` permission. + !!! note + This requires the `READ_MESSAGE_HISTORY` permission. Raises ------ - :obj:`~hikari.errors.ForbiddenHTTPError` + hikari.errors.ForbiddenHTTPError If you lack permission to see the message. - :obj:`~hikari.errors.NotFoundHTTPError` + hikari.errors.NotFoundHTTPError If the channel or message is not found. """ route = routes.CHANNEL_MESSAGE.compile(self.GET, channel_id=channel_id, message_id=message_id) @@ -774,45 +753,45 @@ async def create_message( Parameters ---------- - channel_id : :obj:`~str` + channel_id : str The ID of the channel to send to. - content : :obj:`~str` + content : str If specified, the message content to send with the message. - nonce : :obj:`~str` + nonce : str If specified, an optional ID to send for opportunistic message creation. This doesn't serve any real purpose for general use, and can usually be ignored. - tts : :obj:`~bool` + tts : bool If specified, whether the message will be sent as a TTS message. - files : :obj:`~typing.Sequence` [ :obj:`~typing.Tuple` [ :obj:`~str`, :obj:`~io.IOBase` ] ] - If specified, this should be a list of between ``1`` and ``5`` tuples. + files : typing.Sequence [ typing.Tuple [ str, io.IOBase ] ] + If specified, this should be a list of between `1` and `5` tuples. Each tuple should consist of the file name, and either - raw :obj:`~bytes` or an :obj:`~io.IOBase` derived object with + raw `bytes` or an `io.IOBase` derived object with a seek that points to a buffer containing said file. - embed : :obj:`~typing.Dict` [ :obj:`~str`, :obj:`~typing.Any` ] + embed : typing.Dict [ str, typing.Any ] If specified, the embed to send with the message. - allowed_mentions : :obj:`~typing.Dict` [ :obj:`~str`, :obj:`~typing.Any` ] - If specified, the mentions to parse from the ``content``. - If not specified, will parse all mentions from the ``content``. + allowed_mentions : typing.Dict [ str, typing.Any ] + If specified, the mentions to parse from the `content`. + If not specified, will parse all mentions from the `content`. Returns ------- - :obj:`~typing.Dict` [ :obj:`~str`, :obj:`~typing.Any` ] + typing.Dict [ str, typing.Any ] The created message object. Raises ------ - :obj:`~hikari.errors.NotFoundHTTPError` + hikari.errors.NotFoundHTTPError If the channel is not found. - :obj:`~hikari.errors.BadRequestHTTPError` + hikari.errors.BadRequestHTTPError This can be raised if the file is too large; if the embed exceeds the defined limits; if the message content is specified only and - empty or greater than ``2000`` characters; if neither content, file + empty or greater than `2000` characters; if neither content, file or embed are specified; if there is a duplicate id in only of the - fields in ``allowed_mentions``; if you specify to parse all + fields in `allowed_mentions`; if you specify to parse all users/roles mentions but also specify which users/roles to parse only. - :obj:`~hikari.errors.ForbiddenHTTPError` + hikari.errors.ForbiddenHTTPError If you lack permissions to send to this channel. """ form = aiohttp.FormData() @@ -841,24 +820,24 @@ async def create_reaction(self, channel_id: str, message_id: str, emoji: str) -> Parameters ---------- - channel_id : :obj:`~str` + channel_id : str The ID of the channel to add this reaction in. - message_id : :obj:`~str` + message_id : str The ID of the message to add the reaction in. - emoji : :obj:`~str` + emoji : str The emoji to add. This can either be a series of unicode characters making up a valid Discord emoji, or it can be a the url - representation of a custom emoji ``<{emoji.name}:{emoji.id}>``. + representation of a custom emoji `<{emoji.name}:{emoji.id}>`. Raises ------ - :obj:`~hikari.errors.ForbiddenHTTPError` + hikari.errors.ForbiddenHTTPError If this is the first reaction using this specific emoji on this - message and you lack the ``ADD_REACTIONS`` permission. If you lack - ``READ_MESSAGE_HISTORY``, this may also raise this error. - :obj:`~hikari.errors.NotFoundHTTPError` + message and you lack the `ADD_REACTIONS` permission. If you lack + `READ_MESSAGE_HISTORY`, this may also raise this error. + hikari.errors.NotFoundHTTPError If the channel or message is not found, or if the emoji is not found. - :obj:`~hikari.errors.BadRequestHTTPError` + hikari.errors.BadRequestHTTPError If the emoji is not valid, unknown, or formatted incorrectly. """ route = routes.OWN_REACTION.compile(self.PUT, channel_id=channel_id, message_id=message_id, emoji=emoji) @@ -869,20 +848,20 @@ async def delete_own_reaction(self, channel_id: str, message_id: str, emoji: str Parameters ---------- - channel_id : :obj:`~str` + channel_id : str The ID of the channel to get the message from. - message_id : :obj:`~str` + message_id : str The ID of the message to delete the reaction from. - emoji : :obj:`~str` + emoji : str The emoji to delete. This can either be a series of unicode characters making up a valid Discord emoji, or it can be a snowflake ID for a custom emoji. Raises ------ - :obj:`~hikari.errors.ForbiddenHTTPError` + hikari.errors.ForbiddenHTTPError If you lack permission to do this. - :obj:`~hikari.errors.NotFoundHTTPError` + hikari.errors.NotFoundHTTPError If the channel or message or emoji is not found. """ route = routes.OWN_REACTION.compile(self.DELETE, channel_id=channel_id, message_id=message_id, emoji=emoji) @@ -893,21 +872,21 @@ async def delete_all_reactions_for_emoji(self, channel_id: str, message_id: str, Parameters ---------- - channel_id : :obj:`~str` + channel_id : str The ID of the channel to get the message from. - message_id : :obj:`~str` + message_id : str The ID of the message to delete the reactions from. - emoji : :obj:`~str` + emoji : str The emoji to delete. This can either be a series of unicode characters making up a valid Discord emoji, or it can be a snowflake ID for a custom emoji. Raises ------ - :obj:`~hikari.errors.NotFoundHTTPError` + hikari.errors.NotFoundHTTPError If the channel or message or emoji or user is not found. - :obj:`~hikari.errors.ForbiddenHTTPError` - If you lack the ``MANAGE_MESSAGES`` permission, or are in DMs. + hikari.errors.ForbiddenHTTPError + If you lack the `MANAGE_MESSAGES` permission, or are in DMs. """ route = routes.REACTION_EMOJI.compile(self.DELETE, channel_id=channel_id, message_id=message_id, emoji=emoji) await self._request(route) @@ -917,23 +896,23 @@ async def delete_user_reaction(self, channel_id: str, message_id: str, emoji: st Parameters ---------- - channel_id : :obj:`~str` + channel_id : str The ID of the channel to get the message from. - message_id : :obj:`~str` + message_id : str The ID of the message to remove the reaction from. - emoji : :obj:`~str` + emoji : str The emoji to delete. This can either be a series of unicode characters making up a valid Discord emoji, or it can be a snowflake ID for a custom emoji. - user_id : :obj:`~str` + user_id : str The ID of the user who made the reaction that you wish to remove. Raises ------ - :obj:`~hikari.errors.NotFoundHTTPError` + hikari.errors.NotFoundHTTPError If the channel or message or emoji or user is not found. - :obj:`~hikari.errors.ForbiddenHTTPError` - If you lack the ``MANAGE_MESSAGES`` permission, or are in DMs. + hikari.errors.ForbiddenHTTPError + If you lack the `MANAGE_MESSAGES` permission, or are in DMs. """ route = routes.REACTION_EMOJI_USER.compile( self.DELETE, channel_id=channel_id, message_id=message_id, emoji=emoji, user_id=user_id, @@ -947,32 +926,32 @@ async def get_reactions( Parameters ---------- - channel_id : :obj:`~str` + channel_id : str The ID of the channel to get the message from. - message_id : :obj:`~str` + message_id : str The ID of the message to get the reactions from. - emoji : :obj:`~str` + emoji : str The emoji to get. This can either be a series of unicode characters making up a valid Discord emoji, or it can be a snowflake ID for a custom emoji. - after : :obj:`~str` + after : str If specified, the user ID. If specified, only users with a snowflake that is lexicographically greater than the value will be returned. - limit : :obj:`~str` + limit : str If specified, the limit of the number of values to return. Must be - between ``1`` and ``100`` inclusive. If unspecified, - defaults to ``25``. + between `1` and `100` inclusive. If unspecified, + defaults to `25`. Returns ------- - :obj:`~typing.Sequence` [ :obj:`~typing.Dict` [ :obj:`~str`, :obj:`~typing.Any` ] ] + typing.Sequence [ typing.Dict [ str, typing.Any ] ] A list of user objects. Raises ------ - :obj:`~hikari.errors.ForbiddenHTTPError` + hikari.errors.ForbiddenHTTPError If you lack access to the message. - :obj:`~hikari.errors.NotFoundHTTPError` + hikari.errors.NotFoundHTTPError If the channel or message is not found. """ query = {} @@ -986,17 +965,17 @@ async def delete_all_reactions(self, channel_id: str, message_id: str) -> None: Parameters ---------- - channel_id : :obj:`~str` + channel_id : str The ID of the channel to get the message from. - message_id : :obj:`~str` + message_id : str The ID of the message to remove all reactions from. Raises ------ - :obj:`~hikari.errors.NotFoundHTTPError` + hikari.errors.NotFoundHTTPError If the channel or message is not found. - :obj:`~hikari.errors.ForbiddenHTTPError` - If you lack the ``MANAGE_MESSAGES`` permission. + hikari.errors.ForbiddenHTTPError + If you lack the `MANAGE_MESSAGES` permission. """ route = routes.ALL_REACTIONS.compile(self.DELETE, channel_id=channel_id, message_id=message_id) await self._request(route) @@ -1015,41 +994,41 @@ async def edit_message( Parameters ---------- - channel_id : :obj:`~str` + channel_id : str The ID of the channel to get the message from. - message_id : :obj:`~str` + message_id : str The ID of the message to edit. - content : :obj:`~str`, optional + content : str, optional If specified, the string content to replace with in the message. - If :obj:`~None`, the content will be removed from the message. - embed : :obj:`~typing.Dict` [ :obj:`~str`, :obj:`~typing.Any` ], optional + If `None`, the content will be removed from the message. + embed : typing.Dict [ str, typing.Any ], optional If specified, the embed to replace with in the message. - If :obj:`~None`, the embed will be removed from the message. - flags : :obj:`~int` + If `None`, the embed will be removed from the message. + flags : int If specified, the integer to replace the message's current flags. - allowed_mentions : :obj:`~typing.Dict` [ :obj:`~str`, :obj:`~typing.Any` ] - If specified, the mentions to parse from the ``content``. - If not specified, will parse all mentions from the ``content``. + allowed_mentions : typing.Dict [ str, typing.Any ] + If specified, the mentions to parse from the `content`. + If not specified, will parse all mentions from the `content`. Returns ------- - :obj:`~typing.Dict` [ :obj:`~str`, :obj:`~typing.Any` ] + typing.Dict [ str, typing.Any ] The edited message object. Raises ------ - :obj:`~hikari.errors.NotFoundHTTPError` + hikari.errors.NotFoundHTTPError If the channel or message is not found. - :obj:`~hikari.errors.BadRequestHTTPError` + hikari.errors.BadRequestHTTPError This can be raised if the embed exceeds the defined limits; if the message content is specified only and empty or greater - than ``2000`` characters; if neither content, file or embed + than `2000` characters; if neither content, file or embed are specified. parse only. - :obj:`~hikari.errors.ForbiddenHTTPError` - If you try to edit ``content`` or ``embed`` or ``allowed_mentions` + hikari.errors.ForbiddenHTTPError + If you try to edit `content` or `embed` or `allowed_mentions` on a message you did not author or try to edit the flags on a - message you did not author without the ``MANAGE_MESSAGES`` + message you did not author without the `MANAGE_MESSAGES` permission. """ payload = {} @@ -1065,17 +1044,17 @@ async def delete_message(self, channel_id: str, message_id: str) -> None: Parameters ---------- - channel_id : :obj:`~str` + channel_id : str The ID of the channel to get the message from. - message_id : :obj:`~str` + message_id : str The ID of the message to delete. Raises ------ - :obj:`~hikari.errors.ForbiddenHTTPError` + hikari.errors.ForbiddenHTTPError If you did not author the message and are in a DM, or if you did not author the message and lack the - ``MANAGE_MESSAGES`` permission in a guild channel. - :obj:`~hikari.errors.NotFoundHTTPError` + `MANAGE_MESSAGES` permission in a guild channel. + hikari.errors.NotFoundHTTPError If the channel or message is not found. """ route = routes.CHANNEL_MESSAGE.compile(self.DELETE, channel_id=channel_id, message_id=message_id) @@ -1086,28 +1065,27 @@ async def bulk_delete_messages(self, channel_id: str, messages: typing.Sequence[ Parameters ---------- - channel_id : :obj:`~str` + channel_id : str The ID of the channel to get the message from. - messages : :obj:`~typing.Sequence` [ :obj:`~str` ] - A list of ``2-100`` message IDs to remove in the channel. + messages : typing.Sequence [ str ] + A list of `2-100` message IDs to remove in the channel. Raises ------ - :obj:`~hikari.errors.NotFoundHTTPError` + hikari.errors.NotFoundHTTPError If the channel is not found. - :obj:`~hikari.errors.ForbiddenHTTPError` - If you lack the ``MANAGE_MESSAGES`` permission in the channel. - :obj:`~hikari.errors.BadRequestHTTPError` - If any of the messages passed are older than ``2`` weeks in age or + hikari.errors.ForbiddenHTTPError + If you lack the `MANAGE_MESSAGES` permission in the channel. + hikari.errors.BadRequestHTTPError + If any of the messages passed are older than `2` weeks in age or any duplicate message IDs are passed. - Note - ---- - This can only be used on guild text channels. - Any message IDs that do not exist or are invalid still add towards the - total ``100`` max messages to remove. This can only delete messages that - are newer than ``2`` weeks in age. If any of the messages are older than - ``2`` weeks then this call will fail. + !!! note + This can only be used on guild text channels. Any message IDs that + do not exist or are invalid still add towards the total `100` max + messages to remove. This can only delete messages that are newer + than `2` weeks in age. If any of the messages are older than + `2` weeks then this call will fail. """ payload = {"messages": messages} route = routes.CHANNEL_MESSAGES_BULK_DELETE.compile(self.POST, channel_id=channel_id) @@ -1120,26 +1098,26 @@ async def edit_channel_permissions( Parameters ---------- - channel_id : :obj:`~str` + channel_id : str The ID of the channel to edit permissions for. - overwrite_id : :obj:`~str` + overwrite_id : str The overwrite ID to edit. - type_ : :obj:`~str` - The type of overwrite. ``"member"`` if it is for a member, - or ``"role"`` if it is for a role. - allow : :obj:`~int` + type_ : str + The type of overwrite. `"member"` if it is for a member, + or `"role"` if it is for a role. + allow : int If specified, the bitwise value of all permissions to set to be allowed. - deny : :obj:`~int` + deny : int If specified, the bitwise value of all permissions to set to be denied. - reason : :obj:`~str` + reason : str If specified, the audit log reason explaining why the operation was performed. Raises ------ - :obj:`~hikari.errors.NotFoundHTTPError` + hikari.errors.NotFoundHTTPError If the target channel or overwrite doesn't exist. - :obj:`~hikari.errors.ForbiddenHTTPError` + hikari.errors.ForbiddenHTTPError If you lack permission to do this. """ payload = {"type": type_} @@ -1153,19 +1131,19 @@ async def get_channel_invites(self, channel_id: str) -> typing.Sequence[typing.D Parameters ---------- - channel_id : :obj:`~str` + channel_id : str The ID of the channel to get invites for. Returns ------- - :obj:`~typing.Sequence` [ :obj:`~typing.Dict` [ :obj:`~str`, :obj:`~typing.Any` ] ] + typing.Sequence [ typing.Dict [ str, typing.Any ] ] A list of invite objects. Raises ------ - :obj:`~hikari.errors.ForbiddenHTTPError` - If you lack the ``MANAGE_CHANNELS`` permission. - :obj:`~hikari.errors.NotFoundHTTPError` + hikari.errors.ForbiddenHTTPError + If you lack the `MANAGE_CHANNELS` permission. + hikari.errors.NotFoundHTTPError If the channel does not exist. """ route = routes.CHANNEL_INVITES.compile(self.GET, channel_id=channel_id) @@ -1187,40 +1165,40 @@ async def create_channel_invite( Parameters ---------- - channel_id : :obj:`~str` + channel_id : str The ID of the channel to create the invite for. - max_age : :obj:`~int` + max_age : int If specified, the max age of the invite in seconds, defaults to - ``86400`` (``24`` hours). - Set to ``0`` to never expire. - max_uses : :obj:`~int` - If specified, the max number of uses this invite can have, or ``0`` + `86400` (`24` hours). + Set to `0` to never expire. + max_uses : int + If specified, the max number of uses this invite can have, or `0` for unlimited (as per the default). - temporary : :obj:`~bool` + temporary : bool If specified, whether to grant temporary membership, meaning the user is kicked when their session ends unless they are given a role. - unique : :obj:`~bool` + unique : bool If specified, whether to try to reuse a similar invite. - target_user : :obj:`~str` + target_user : str If specified, the ID of the user this invite should target. - target_user_type : :obj:`~int` + target_user_type : int If specified, the type of target for this invite. - reason : :obj:`~str` + reason : str If specified, the audit log reason explaining why the operation was performed. Returns ------- - :obj:`~typing.Dict` [ :obj:`~str`, :obj:`~typing.Any` ] + typing.Dict [ str, typing.Any ] An invite object. Raises ------ - :obj:`~hikari.errors.ForbiddenHTTPError` - If you lack the ``CREATE_INSTANT_MESSAGES`` permission. - :obj:`~hikari.errors.NotFoundHTTPError` + hikari.errors.ForbiddenHTTPError + If you lack the `CREATE_INSTANT_MESSAGES` permission. + hikari.errors.NotFoundHTTPError If the channel does not exist. - :obj:`~hikari.errors.BadRequestHTTPError` + hikari.errors.BadRequestHTTPError If the arguments provided are not valid (e.g. negative age, etc). """ payload = {} @@ -1238,34 +1216,34 @@ async def delete_channel_permission(self, channel_id: str, overwrite_id: str) -> Parameters ---------- - channel_id : :obj:`~str` + channel_id : str The ID of the channel to delete the overwrite from. - overwrite_id : :obj:`~str` + overwrite_id : str The ID of the overwrite to remove. Raises ------ - :obj:`~hikari.errors.NotFoundHTTPError` + hikari.errors.NotFoundHTTPError If the overwrite or channel do not exist. - :obj:`~hikari.errors.ForbiddenHTTPError` - If you lack the ``MANAGE_ROLES`` permission for that channel. + hikari.errors.ForbiddenHTTPError + If you lack the `MANAGE_ROLES` permission for that channel. """ route = routes.CHANNEL_PERMISSIONS.compile(self.DELETE, channel_id=channel_id, overwrite_id=overwrite_id) await self._request(route) async def trigger_typing_indicator(self, channel_id: str) -> None: - """Trigger the account to appear to be typing for the next ``10`` seconds in the given channel. + """Trigger the account to appear to be typing for the next `10` seconds in the given channel. Parameters ---------- - channel_id : :obj:`~str` + channel_id : str The ID of the channel to appear to be typing in. Raises ------ - :obj:`~hikari.errors.NotFoundHTTPError` + hikari.errors.NotFoundHTTPError If the channel is not found. - :obj:`~hikari.errors.ForbiddenHTTPError` + hikari.errors.ForbiddenHTTPError If you are not able to type in the channel. """ route = routes.CHANNEL_TYPING.compile(self.POST, channel_id=channel_id) @@ -1276,25 +1254,25 @@ async def get_pinned_messages(self, channel_id: str) -> typing.Sequence[typing.D Parameters ---------- - channel_id : :obj:`~str` + channel_id : str The channel ID to get messages from. Returns ------- - :obj:`~typing.Sequence` [ :obj:`~typing.Dict` [ :obj:`~str`, :obj:`~typing.Any` ] ] + typing.Sequence [ typing.Dict [ str, typing.Any ] ] A list of messages. Raises ------ - :obj:`~hikari.errors.NotFoundHTTPError` + hikari.errors.NotFoundHTTPError If the channel is not found. - :obj:`~hikari.errors.ForbiddenHTTPError` + hikari.errors.ForbiddenHTTPError If you are not able to see the channel. - Note - ---- - If you are not able to see the pinned message (eg. you are missing ``READ_MESSAGE_HISTORY`` - and the pinned message is an old message), it will not be returned. + !!! note + If you are not able to see the pinned message (eg. you are missing + `READ_MESSAGE_HISTORY` and the pinned message is an old message), it + will not be returned. """ route = routes.CHANNEL_PINS.compile(self.GET, channel_id=channel_id) return await self._request(route) @@ -1304,16 +1282,16 @@ async def add_pinned_channel_message(self, channel_id: str, message_id: str) -> Parameters ---------- - channel_id : :obj:`~str` + channel_id : str The ID of the channel to pin a message to. - message_id : :obj:`~str` + message_id : str The ID of the message to pin. Raises ------ - :obj:`~hikari.errors.ForbiddenHTTPError` - If you lack the ``MANAGE_MESSAGES`` permission. - :obj:`~hikari.errors.NotFoundHTTPError` + hikari.errors.ForbiddenHTTPError + If you lack the `MANAGE_MESSAGES` permission. + hikari.errors.NotFoundHTTPError If the message or channel do not exist. """ route = routes.CHANNEL_PINS.compile(self.PUT, channel_id=channel_id, message_id=message_id) @@ -1326,16 +1304,16 @@ async def delete_pinned_channel_message(self, channel_id: str, message_id: str) Parameters ---------- - channel_id : :obj:`~str` + channel_id : str The ID of the channel to remove a pin from. - message_id : :obj:`~str` + message_id : str The ID of the message to unpin. Raises ------ - :obj:`~hikari.errors.ForbiddenHTTPError` - If you lack the ``MANAGE_MESSAGES`` permission. - :obj:`~hikari.errors.NotFoundHTTPError` + hikari.errors.ForbiddenHTTPError + If you lack the `MANAGE_MESSAGES` permission. + hikari.errors.NotFoundHTTPError If the message or channel do not exist. """ route = routes.CHANNEL_PIN.compile(self.DELETE, channel_id=channel_id, message_id=message_id) @@ -1346,19 +1324,19 @@ async def list_guild_emojis(self, guild_id: str) -> typing.Sequence[typing.Dict[ Parameters ---------- - guild_id : :obj:`~str` + guild_id : str The ID of the guild to get the emojis for. Returns ------- - :obj:`~typing.Sequence` [ :obj:`~typing.Dict` [ :obj:`~str`, :obj:`~typing.Any` ] ] + typing.Sequence [ typing.Dict [ str, typing.Any ] ] A list of emoji objects. Raises ------ - :obj:`~hikari.errors.NotFoundHTTPError` + hikari.errors.NotFoundHTTPError If the guild is not found. - :obj:`~hikari.errors.ForbiddenHTTPError` + hikari.errors.ForbiddenHTTPError If you aren't a member of the guild. """ route = routes.GUILD_EMOJIS.compile(self.GET, guild_id=guild_id) @@ -1369,21 +1347,21 @@ async def get_guild_emoji(self, guild_id: str, emoji_id: str) -> typing.Dict[str Parameters ---------- - guild_id : :obj:`~str` + guild_id : str The ID of the guild to get the emoji from. - emoji_id : :obj:`~str` + emoji_id : str The ID of the emoji to get. Returns ------- - :obj:`~typing.Dict` [ :obj:`~str`, :obj:`~typing.Any` ] + typing.Dict [ str, typing.Any ] An emoji object. Raises ------ - :obj:`~hikari.errors.NotFoundHTTPError` + hikari.errors.NotFoundHTTPError If either the guild or the emoji aren't found. - :obj:`~hikari.errors.ForbiddenHTTPError` + hikari.errors.ForbiddenHTTPError If you aren't a member of said guild. """ route = routes.GUILD_EMOJI.compile(self.GET, guild_id=guild_id, emoji_id=emoji_id) @@ -1396,34 +1374,34 @@ async def create_guild_emoji( Parameters ---------- - guild_id : :obj:`~str` + guild_id : str The ID of the guild to create the emoji in. - name : :obj:`~str` + name : str The new emoji's name. - image : :obj:`~bytes` - The ``128x128`` image in bytes form. - roles : :obj:`~typing.Sequence` [ :obj:`~str` ] + image : bytes + The `128x128` image in bytes form. + roles : typing.Sequence [ str ] If specified, a list of roles for which the emoji will be whitelisted. If empty, all roles are whitelisted. - reason : :obj:`~str` + reason : str If specified, the audit log reason explaining why the operation was performed. Returns ------- - :obj:`~typing.Dict` [ :obj:`~str`, :obj:`~typing.Any` ] + typing.Dict [ str, typing.Any ] The newly created emoji object. Raises ------ - :obj:`~ValueError` - If ``image`` is :obj:`~None`. - :obj:`~hikari.errors.NotFoundHTTPError` + ValueError + If `image` is `None`. + hikari.errors.NotFoundHTTPError If the guild is not found. - :obj:`~hikari.errors.ForbiddenHTTPError` - If you either lack the ``MANAGE_EMOJIS`` permission or aren't a member of said guild. - :obj:`~hikari.errors.BadRequestHTTPError` - If you attempt to upload an image larger than ``256kb``, an empty image or an invalid image format. + hikari.errors.ForbiddenHTTPError + If you either lack the `MANAGE_EMOJIS` permission or aren't a member of said guild. + hikari.errors.BadRequestHTTPError + If you attempt to upload an image larger than `256kb`, an empty image or an invalid image format. """ assertions.assert_not_none(image, "image must be a valid image") payload = { @@ -1441,31 +1419,31 @@ async def modify_guild_emoji( Parameters ---------- - guild_id : :obj:`~str` + guild_id : str The ID of the guild to which the emoji to update belongs to. - emoji_id : :obj:`~str` + emoji_id : str The ID of the emoji to update. - name : :obj:`~str` + name : str If specified, a new emoji name string. Keep unspecified to keep the name the same. - roles : :obj:`~typing.Sequence` [ :obj:`~str` ] + roles : typing.Sequence [ str ] If specified, a list of IDs for the new whitelisted roles. Set to an empty list to whitelist all roles. Keep unspecified to leave the same roles already set. - reason : :obj:`~str` + reason : str If specified, the audit log reason explaining why the operation was performed. Returns ------- - :obj:`~typing.Dict` [ :obj:`~str`, :obj:`~typing.Any` ] + typing.Dict [ str, typing.Any ] The updated emoji object. Raises ------ - :obj:`~hikari.errors.NotFoundHTTPError` + hikari.errors.NotFoundHTTPError If either the guild or the emoji aren't found. - :obj:`~hikari.errors.ForbiddenHTTPError` - If you either lack the ``MANAGE_EMOJIS`` permission or are not a member of the given guild. + hikari.errors.ForbiddenHTTPError + If you either lack the `MANAGE_EMOJIS` permission or are not a member of the given guild. """ payload = {} conversions.put_if_specified(payload, "name", name) @@ -1478,17 +1456,17 @@ async def delete_guild_emoji(self, guild_id: str, emoji_id: str) -> None: Parameters ---------- - guild_id : :obj:`~str` + guild_id : str The ID of the guild to delete the emoji from. - emoji_id : :obj:`~str` + emoji_id : str The ID of the emoji to be deleted. Raises ------ - :obj:`~hikari.errors.NotFoundHTTPError` + hikari.errors.NotFoundHTTPError If either the guild or the emoji aren't found. - :obj:`~hikari.errors.ForbiddenHTTPError` - If you either lack the ``MANAGE_EMOJIS`` permission or aren't a member of said guild. + hikari.errors.ForbiddenHTTPError + If you either lack the `MANAGE_EMOJIS` permission or aren't a member of said guild. """ route = routes.GUILD_EMOJI.compile(self.DELETE, guild_id=guild_id, emoji_id=emoji_id) await self._request(route) @@ -1507,42 +1485,42 @@ async def create_guild( ) -> typing.Dict[str, typing.Any]: """Create a new guild. - Warning - ------- - Can only be used by bots in less than ``10`` guilds. + !!! warning + Can only be used by bots in less than `10` guilds. Parameters ---------- - name : :obj:`~str` - The name string for the new guild (``2-100`` characters). - region : :obj:`~str` + name : str + The name string for the new guild (`2-100` characters). + region : str If specified, the voice region ID for new guild. You can use - :meth:`list_voice_regions` to see which region IDs are available. - icon : :obj:`~bytes` + `LowLevelRestfulClient.list_voice_regions` to see which region IDs + are available. + icon : bytes If specified, the guild icon image in bytes form. - verification_level : :obj:`~int` - If specified, the verification level integer (``0-5``). - default_message_notifications : :obj:`~int` - If specified, the default notification level integer (``0-1``). - explicit_content_filter : :obj:`~int` - If specified, the explicit content filter integer (``0-2``). - roles : :obj:`~typing.Sequence` [ :obj:`~typing.Dict` [ :obj:`~str`, :obj:`~typing.Any` ] ] + verification_level : int + If specified, the verification level integer (`0-5`). + default_message_notifications : int + If specified, the default notification level integer (`0-1`). + explicit_content_filter : int + If specified, the explicit content filter integer (`0-2`). + roles : typing.Sequence [ typing.Dict [ str, typing.Any ] ] If specified, an array of role objects to be created alongside the - guild. First element changes the ``@everyone`` role. - channels : :obj:`~typing.Sequence` [ :obj:`~typing.Dict` [ :obj:`~str`, :obj:`~typing.Any` ] ] + guild. First element changes the `@everyone` role. + channels : typing.Sequence [ typing.Dict [ str, typing.Any ] ] If specified, an array of channel objects to be created alongside the guild. Returns ------- - :obj:`~typing.Dict` [ :obj:`~str`, :obj:`~typing.Any` ] + typing.Dict [ str, typing.Any ] The newly created guild object. Raises ------ - :obj:`~hikari.errors.ForbiddenHTTPError` - If you are on ``10`` or more guilds. - :obj:`~hikari.errors.BadRequestHTTPError` - If you provide unsupported fields like ``parent_id`` in channel objects. + hikari.errors.ForbiddenHTTPError + If you are on `10` or more guilds. + hikari.errors.BadRequestHTTPError + If you provide unsupported fields like `parent_id` in channel objects. """ payload = {"name": name} conversions.put_if_specified(payload, "region", region) @@ -1560,19 +1538,19 @@ async def get_guild(self, guild_id: str) -> typing.Dict[str, typing.Any]: Parameters ---------- - guild_id : :obj:`~str` + guild_id : str The ID of the guild to get. Returns ------- - :obj:`~typing.Dict` [ :obj:`~str`, :obj:`~typing.Any` ] + typing.Dict [ str, typing.Any ] The requested guild object. Raises ------ - :obj:`~hikari.errors.NotFoundHTTPError` + hikari.errors.NotFoundHTTPError If the guild is not found. - :obj:`~hikari.errors.ForbiddenHTTPError` + hikari.errors.ForbiddenHTTPError If you don't have access to the guild. """ route = routes.GUILD.compile(self.GET, guild_id=guild_id) @@ -1583,23 +1561,22 @@ async def get_guild_preview(self, guild_id: str) -> typing.Dict[str, typing.Any] Parameters ---------- - guild_id : :obj:`~str` + guild_id : str The ID of the guild to get the preview object of. Returns ------- - :obj:`~typing.Dict` [ :obj:`~str`, :obj:`~typing.Any` ] + typing.Dict [ str, typing.Any ] The requested guild preview object. - Note - ---- - Unlike other guild endpoints, the bot doesn't have to be in the target - guild to get it's preview. + !!! note + Unlike other guild endpoints, the bot doesn't have to be in the + target guild to get it's preview. Raises ------ - :obj:`~hikari.errors.NotFoundHTTPError` - If the guild is not found or it isn't ``PUBLIC``. + hikari.errors.NotFoundHTTPError + If the guild is not found or it isn't `PUBLIC`. """ route = routes.GUILD_PREVIEW.compile(self.GET, guild_id=guild_id) return await self._request(route) @@ -1626,46 +1603,47 @@ async def modify_guild( # lgtm [py/similar-function] Parameters ---------- - guild_id : :obj:`~str` + guild_id : str The ID of the guild to be edited. - name : :obj:`~str` - If specified, the new name string for the guild (``2-100`` characters). - region : :obj:`~str` + name : str + If specified, the new name string for the guild (`2-100` characters). + region : str If specified, the new voice region ID for guild. You can use - :meth:`list_voice_regions` to see which region IDs are available. - verification_level : :obj:`~int` - If specified, the new verification level integer (``0-5``). - default_message_notifications : :obj:`~int` - If specified, the new default notification level integer (``0-1``). - explicit_content_filter : :obj:`~int` - If specified, the new explicit content filter integer (``0-2``). - afk_channel_id : :obj:`~str` + `LowLevelRestfulClient.list_voice_regions` to see which region IDs + are available. + verification_level : int + If specified, the new verification level integer (`0-5`). + default_message_notifications : int + If specified, the new default notification level integer (`0-1`). + explicit_content_filter : int + If specified, the new explicit content filter integer (`0-2`). + afk_channel_id : str If specified, the new ID for the AFK voice channel. - afk_timeout : :obj:`~int` + afk_timeout : int If specified, the new AFK timeout period in seconds - icon : :obj:`~bytes` + icon : bytes If specified, the new guild icon image in bytes form. - owner_id : :obj:`~str` + owner_id : str If specified, the new ID of the new guild owner. - splash : :obj:`~bytes` + splash : bytes If specified, the new new splash image in bytes form. - system_channel_id : :obj:`~str` + system_channel_id : str If specified, the new ID of the new system channel. - reason : :obj:`~str` + reason : str If specified, the audit log reason explaining why the operation was performed. Returns ------- - :obj:`~typing.Dict` [ :obj:`~str`, :obj:`~typing.Any` ] + typing.Dict [ str, typing.Any ] The edited guild object. Raises ------ - :obj:`~hikari.errors.NotFoundHTTPError` + hikari.errors.NotFoundHTTPError If the guild is not found. - :obj:`~hikari.errors.ForbiddenHTTPError` - If you lack the ``MANAGE_GUILD`` permission or are not in the guild. + hikari.errors.ForbiddenHTTPError + If you lack the `MANAGE_GUILD` permission or are not in the guild. """ payload = {} conversions.put_if_specified(payload, "name", name) @@ -1691,14 +1669,14 @@ async def delete_guild(self, guild_id: str) -> None: Parameters ---------- - guild_id : :obj:`~str` + guild_id : str The ID of the guild to be deleted. Raises ------ - :obj:`~hikari.errors.NotFoundHTTPError` + hikari.errors.NotFoundHTTPError If the guild is not found. - :obj:`~hikari.errors.ForbiddenHTTPError` + hikari.errors.ForbiddenHTTPError If you are not the guild owner. """ route = routes.GUILD.compile(self.DELETE, guild_id=guild_id) @@ -1709,19 +1687,19 @@ async def list_guild_channels(self, guild_id: str) -> typing.Sequence[typing.Dic Parameters ---------- - guild_id : :obj:`~str` + guild_id : str The ID of the guild to get the channels from. Returns ------- - :obj:`~typing.Sequence` [ :obj:`~typing.Dict` [ :obj:`~str`, :obj:`~typing.Any` ] ] + typing.Sequence [ typing.Dict [ str, typing.Any ] ] A list of channel objects. Raises ------ - :obj:`~hikari.errors.NotFoundHTTPError` + hikari.errors.NotFoundHTTPError If the guild is not found. - :obj:`~hikari.errors.ForbiddenHTTPError` + hikari.errors.ForbiddenHTTPError If you are not in the guild. """ route = routes.GUILD_CHANNELS.compile(self.GET, guild_id=guild_id) @@ -1747,59 +1725,59 @@ async def create_guild_channel( Parameters ---------- - guild_id : :obj:`~str` + guild_id : str The ID of the guild to create the channel in. - name : :obj:`~str` + name : str If specified, the name for the channel.This must be - between ``2`` and ``100`` characters in length. - type_: :obj:`~int` - If specified, the channel type integer (``0-6``). - position : :obj:`~int` + between `2` and `100` characters in length. + type_: int + If specified, the channel type integer (`0-6`). + position : int If specified, the position to change the channel to. - topic : :obj:`~str` + topic : str If specified, the topic to set. This is only applicable to - text channels. This must be between ``0`` and ``1024`` + text channels. This must be between `0` and `1024` characters in length. - nsfw : :obj:`~bool` + nsfw : bool If specified, whether the channel will be marked as NSFW. Only applicable to text channels. - rate_limit_per_user : :obj:`~int` + rate_limit_per_user : int If specified, the number of seconds the user has to wait before sending another message. This will not apply to bots, or to members with - ``MANAGE_MESSAGES`` or ``MANAGE_CHANNEL`` permissions. This must - be between ``0`` and ``21600`` seconds. - bitrate : :obj:`~int` + `MANAGE_MESSAGES` or `MANAGE_CHANNEL` permissions. This must + be between `0` and `21600` seconds. + bitrate : int If specified, the bitrate in bits per second allowable for the channel. - This only applies to voice channels and must be between ``8000`` - and ``96000`` for normal servers or ``8000`` and ``128000`` for + This only applies to voice channels and must be between `8000` + and `96000` for normal servers or `8000` and `128000` for VIP servers. - user_limit : :obj:`~int` + user_limit : int If specified, the max number of users to allow in a voice channel. - This must be between ``0`` and ``99`` inclusive, where - ``0`` implies no limit. - permission_overwrites : :obj:`~typing.Sequence` [ :obj:`~typing.Dict` [ :obj:`~str`, :obj:`~typing.Any` ] ] + This must be between `0` and `99` inclusive, where + `0` implies no limit. + permission_overwrites : typing.Sequence [ typing.Dict [ str, typing.Any ] ] If specified, the list of permission overwrites that are category specific to replace the existing overwrites with. - parent_id : :obj:`~str` + parent_id : str If specified, the parent category ID to set for the channel. - reason : :obj:`~str` + reason : str If specified, the audit log reason explaining why the operation was performed. Returns ------- - :obj:`~typing.Dict` [ :obj:`~str`, :obj:`~typing.Any` ] + typing.Dict [ str, typing.Any ] The newly created channel object. Raises ------ - :obj:`~hikari.errors.NotFoundHTTPError` + hikari.errors.NotFoundHTTPError If the guild is not found. - :obj:`~hikari.errors.ForbiddenHTTPError` - If you lack the ``MANAGE_CHANNEL`` permission or are not in the guild. - :obj:`~hikari.errors.BadRequestHTTPError` + hikari.errors.ForbiddenHTTPError + If you lack the `MANAGE_CHANNEL` permission or are not in the guild. + hikari.errors.BadRequestHTTPError If you provide incorrect options for the corresponding channel type - (e.g. a ``bitrate`` for a text channel). + (e.g. a `bitrate` for a text channel). """ payload = {} conversions.put_if_specified(payload, "name", name) @@ -1822,23 +1800,23 @@ async def modify_guild_channel_positions( Parameters ---------- - guild_id : :obj:`~str` + guild_id : str The ID of the guild in which to edit the channels. - channel : :obj:`~typing.Tuple` [ :obj:`~str`, :obj:`~int` ] + channel : typing.Tuple [ str, int ] The first channel to change the position of. This is a tuple of the channel ID and the integer position. - *channels : :obj:`~typing.Tuple` [ :obj:`~str`, :obj:`~int` ] + *channels : typing.Tuple [ str, int ] Optional additional channels to change the position of. These must be tuples of the channel ID and the integer positions to change to. Raises ------ - :obj:`~hikari.errors.NotFoundHTTPError` + hikari.errors.NotFoundHTTPError If either the guild or any of the channels aren't found. - :obj:`~hikari.errors.ForbiddenHTTPError` - If you either lack the ``MANAGE_CHANNELS`` permission or are not a member of said guild or are not in + hikari.errors.ForbiddenHTTPError + If you either lack the `MANAGE_CHANNELS` permission or are not a member of said guild or are not in the guild. - :obj:`~hikari.errors.BadRequestHTTPError` - If you provide anything other than the ``id`` and ``position`` fields for the channels. + hikari.errors.BadRequestHTTPError + If you provide anything other than the `id` and `position` fields for the channels. """ payload = [{"id": ch[0], "position": ch[1]} for ch in (channel, *channels)] route = routes.GUILD_CHANNELS.compile(self.PATCH, guild_id=guild_id) @@ -1849,21 +1827,21 @@ async def get_guild_member(self, guild_id: str, user_id: str) -> typing.Dict[str Parameters ---------- - guild_id : :obj:`~str` + guild_id : str The ID of the guild to get the member from. - user_id : :obj:`~str` + user_id : str The ID of the member to get. Returns ------- - :obj:`~typing.Dict` [ :obj:`~str`, :obj:`~typing.Any` ] + typing.Dict [ str, typing.Any ] The requested member object. Raises ------ - :obj:`~hikari.errors.NotFoundHTTPError` + hikari.errors.NotFoundHTTPError If either the guild or the member aren't found. - :obj:`~hikari.errors.ForbiddenHTTPError` + hikari.errors.ForbiddenHTTPError If you don't have access to the target guild. """ route = routes.GUILD_MEMBER.compile(self.GET, guild_id=guild_id, user_id=user_id) @@ -1876,19 +1854,17 @@ async def list_guild_members( Parameters ---------- - guild_id : :obj:`~str` + guild_id : str The ID of the guild to get the members from. - limit : :obj:`~int` + limit : int If specified, the maximum number of members to return. This has to be between - ``1`` and ``1000`` inclusive. - after : :obj:`~str` + `1` and `1000` inclusive. + after : str If specified, the highest ID in the previous page. This is used for retrieving more - than ``1000`` members in a server using consecutive requests. - - Example - ------- - .. code-block:: python + than `1000` members in a server using consecutive requests. + Examples + -------- members = [] last_id = 0 @@ -1903,17 +1879,17 @@ async def list_guild_members( Returns ------- - :obj:`~typing.Dict` [ :obj:`~str`, :obj:`~typing.Any` ] + typing.Dict [ str, typing.Any ] A list of member objects. Raises ------ - :obj:`~hikari.errors.NotFoundHTTPError` + hikari.errors.NotFoundHTTPError If the guild is not found. - :obj:`~hikari.errors.ForbiddenHTTPError` + hikari.errors.ForbiddenHTTPError If you are not in the guild. - :obj:`~hikari.errors.BadRequestHTTPError` - If you provide invalid values for the ``limit`` or `after`` fields. + hikari.errors.BadRequestHTTPError + If you provide invalid values for the `limit` or `after` fields. """ query = {} conversions.put_if_specified(query, "limit", limit) @@ -1937,39 +1913,40 @@ async def modify_guild_member( # lgtm [py/similar-function] Parameters ---------- - guild_id : :obj:`~str` + guild_id : str The ID of the guild to edit the member from. - user_id : :obj:`~str` + user_id : str The ID of the member to edit. - nick : :obj:`~str`, optional - If specified, the new nickname string. Setting it to :obj:`~None` + nick : str, optional + If specified, the new nickname string. Setting it to None explicitly will clear the nickname. - roles : :obj:`~typing.Sequence` [ :obj:`~str` ] + roles : typing.Sequence [ str ] If specified, a list of role IDs the member should have. - mute : :obj:`~bool` + mute : bool If specified, whether the user should be muted in the voice channel or not. - deaf : :obj:`~bool` + deaf : bool If specified, whether the user should be deafen in the voice channel or not. - channel_id : :obj:`~str` + channel_id : str If specified, the ID of the channel to move the member to. Setting - it to :obj:`~None` explicitly will disconnect the user. - reason : :obj:`~str` + it to None explicitly will disconnect the user. + reason : str If specified, the audit log reason explaining why the operation was performed. Raises ------ - :obj:`~hikari.errors.NotFoundHTTPError` + hikari.errors.NotFoundHTTPError If either the guild, user, channel or any of the roles aren't found. - :obj:`~hikari.errors.ForbiddenHTTPError` - If you lack any of the applicable permissions - (``MANAGE_NICKNAMES``, ``MANAGE_ROLES``, ``MUTE_MEMBERS``, ``DEAFEN_MEMBERS`` or ``MOVE_MEMBERS``). - Note that to move a member you must also have permission to connect to the end channel. - This will also be raised if you're not in the guild. - :obj:`~hikari.errors.BadRequestHTTPError` - If you pass ```mute``, ``deaf`` or ``channel_id`` while the member is not connected to a voice channel. + hikari.errors.ForbiddenHTTPError + If you lack any of the applicable permissions (`MANAGE_NICKNAMES`, + `MANAGE_ROLES`, `MUTE_MEMBERS`,`DEAFEN_MEMBERS` or `MOVE_MEMBERS`). + Note that to move a member you must also have permission to connect + to the end channel. This will also be raised if you're not in the + guild. + hikari.errors.BadRequestHTTPError + If you pass `mute`, `deaf` or `channel_id` while the member is not connected to a voice channel. """ payload = {} conversions.put_if_specified(payload, "nick", nick) @@ -1985,21 +1962,21 @@ async def modify_current_user_nick(self, guild_id: str, nick: typing.Optional[st Parameters ---------- - guild_id : :obj:`~str` + guild_id : str The ID of the guild you want to change the nick on. - nick : :obj:`~str`, optional + nick : str, optional The new nick string. Setting this to `None` clears the nickname. - reason : :obj:`~str` + reason : str If specified, the audit log reason explaining why the operation was performed. Raises ------ - :obj:`~hikari.errors.NotFoundHTTPError` + hikari.errors.NotFoundHTTPError If the guild is not found. - :obj:`~hikari.errors.ForbiddenHTTPError` - If you lack the ``CHANGE_NICKNAME`` permission or are not in the guild. - :obj:`~hikari.errors.BadRequestHTTPError` + hikari.errors.ForbiddenHTTPError + If you lack the `CHANGE_NICKNAME` permission or are not in the guild. + hikari.errors.BadRequestHTTPError If you provide a disallowed nickname, one that is too long, or one that is empty. """ payload = {"nick": nick} @@ -2011,22 +1988,22 @@ async def add_guild_member_role(self, guild_id: str, user_id: str, role_id: str, Parameters ---------- - guild_id : :obj:`~str` + guild_id : str The ID of the guild the member belongs to. - user_id : :obj:`~str` + user_id : str The ID of the member you want to add the role to. - role_id : :obj:`~str` + role_id : str The ID of the role you want to add. - reason : :obj:`~str` + reason : str If specified, the audit log reason explaining why the operation was performed. Raises ------ - :obj:`~hikari.errors.NotFoundHTTPError` + hikari.errors.NotFoundHTTPError If either the guild, member or role aren't found. - :obj:`~hikari.errors.ForbiddenHTTPError` - If you lack the ``MANAGE_ROLES`` permission or are not in the guild. + hikari.errors.ForbiddenHTTPError + If you lack the `MANAGE_ROLES` permission or are not in the guild. """ route = routes.GUILD_MEMBER_ROLE.compile(self.PUT, guild_id=guild_id, user_id=user_id, role_id=role_id) await self._request(route, reason=reason) @@ -2036,22 +2013,22 @@ async def remove_guild_member_role(self, guild_id: str, user_id: str, role_id: s Parameters ---------- - guild_id : :obj:`~str` + guild_id : str The ID of the guild the member belongs to. - user_id : :obj:`~str` + user_id : str The ID of the member you want to remove the role from. - role_id : :obj:`~str` + role_id : str The ID of the role you want to remove. - reason : :obj:`~str` + reason : str If specified, the audit log reason explaining why the operation was performed. Raises ------ - :obj:`~hikari.errors.NotFoundHTTPError` + hikari.errors.NotFoundHTTPError If either the guild, member or role aren't found. - :obj:`~hikari.errors.ForbiddenHTTPError` - If you lack the ``MANAGE_ROLES`` permission or are not in the guild. + hikari.errors.ForbiddenHTTPError + If you lack the `MANAGE_ROLES` permission or are not in the guild. """ route = routes.GUILD_MEMBER_ROLE.compile(self.DELETE, guild_id=guild_id, user_id=user_id, role_id=role_id) await self._request(route, reason=reason) @@ -2061,20 +2038,20 @@ async def remove_guild_member(self, guild_id: str, user_id: str, *, reason: str Parameters ---------- - guild_id: :obj:`~str` + guild_id: str The ID of the guild the member belongs to. - user_id: :obj:`~str` + user_id: str The ID of the member you want to kick. - reason : :obj:`~str` + reason : str If specified, the audit log reason explaining why the operation was performed. Raises ------ - :obj:`~hikari.errors.NotFoundHTTPError` + hikari.errors.NotFoundHTTPError If either the guild or member aren't found. - :obj:`~hikari.errors.ForbiddenHTTPError` - If you lack the ``KICK_MEMBERS`` permission or are not in the guild. + hikari.errors.ForbiddenHTTPError + If you lack the `KICK_MEMBERS` permission or are not in the guild. """ route = routes.GUILD_MEMBER.compile(self.DELETE, guild_id=guild_id, user_id=user_id) await self._request(route, reason=reason) @@ -2084,20 +2061,20 @@ async def get_guild_bans(self, guild_id: str) -> typing.Sequence[typing.Dict[str Parameters ---------- - guild_id : :obj:`~str` + guild_id : str The ID of the guild you want to get the bans from. Returns ------- - :obj:`~typing.Sequence` [ :obj:`~typing.Dict` [ :obj:`~str`, :obj:`~typing.Any` ] ] + typing.Sequence [ typing.Dict [ str, typing.Any ] ] A list of ban objects. Raises ------ - :obj:`~hikari.errors.NotFoundHTTPError` + hikari.errors.NotFoundHTTPError If the guild is not found. - :obj:`~hikari.errors.ForbiddenHTTPError` - If you lack the ``BAN_MEMBERS`` permission or are not in the guild. + hikari.errors.ForbiddenHTTPError + If you lack the `BAN_MEMBERS` permission or are not in the guild. """ route = routes.GUILD_BANS.compile(self.GET, guild_id=guild_id) return await self._request(route) @@ -2107,22 +2084,22 @@ async def get_guild_ban(self, guild_id: str, user_id: str) -> typing.Dict[str, t Parameters ---------- - guild_id : :obj:`~str` + guild_id : str The ID of the guild you want to get the ban from. - user_id : :obj:`~str` + user_id : str The ID of the user to get the ban information for. Returns ------- - :obj:`~typing.Dict` [ :obj:`~str`, :obj:`~typing.Any` ] + typing.Dict [ str, typing.Any ] A ban object for the requested user. Raises ------ - :obj:`~hikari.errors.NotFoundHTTPError` + hikari.errors.NotFoundHTTPError If either the guild or the user aren't found, or if the user is not banned. - :obj:`~hikari.errors.ForbiddenHTTPError` - If you lack the ``BAN_MEMBERS`` permission or are not in the guild. + hikari.errors.ForbiddenHTTPError + If you lack the `BAN_MEMBERS` permission or are not in the guild. """ route = routes.GUILD_BAN.compile(self.GET, guild_id=guild_id, user_id=user_id) return await self._request(route) @@ -2134,23 +2111,23 @@ async def create_guild_ban( Parameters ---------- - guild_id : :obj:`~str` + guild_id : str The ID of the guild the member belongs to. - user_id : :obj:`~str` + user_id : str The ID of the member you want to ban. - delete_message_days : :obj:`~str` + delete_message_days : str If specified, how many days of messages from the user should be removed. - reason : :obj:`~str` + reason : str If specified, the audit log reason explaining why the operation was performed. Raises ------ - :obj:`~hikari.errors.NotFoundHTTPError` + hikari.errors.NotFoundHTTPError If either the guild or member aren't found. - :obj:`~hikari.errors.ForbiddenHTTPError` - If you lack the ``BAN_MEMBERS`` permission or are not in the guild. + hikari.errors.ForbiddenHTTPError + If you lack the `BAN_MEMBERS` permission or are not in the guild. """ query = {} conversions.put_if_specified(query, "delete-message-days", delete_message_days) @@ -2163,20 +2140,20 @@ async def remove_guild_ban(self, guild_id: str, user_id: str, *, reason: str = . Parameters ---------- - guild_id : :obj:`~str` + guild_id : str The ID of the guild to un-ban the user from. - user_id : :obj:`~str` + user_id : str The ID of the user you want to un-ban. - reason : :obj:`~str` + reason : str If specified, the audit log reason explaining why the operation was performed. Raises ------ - :obj:`~hikari.errors.NotFoundHTTPError` + hikari.errors.NotFoundHTTPError If either the guild or member aren't found, or the member is not banned. - :obj:`~hikari.errors.ForbiddenHTTPError` - If you lack the ``BAN_MEMBERS`` permission or are not a in the guild. + hikari.errors.ForbiddenHTTPError + If you lack the `BAN_MEMBERS` permission or are not a in the guild. """ route = routes.GUILD_BAN.compile(self.DELETE, guild_id=guild_id, user_id=user_id) await self._request(route, reason=reason) @@ -2186,19 +2163,19 @@ async def get_guild_roles(self, guild_id: str) -> typing.Sequence[typing.Dict[st Parameters ---------- - guild_id : :obj:`~str` + guild_id : str The ID of the guild you want to get the roles from. Returns ------- - :obj:`~typing.Sequence` [ :obj:`~typing.Dict` [ :obj:`~str`, :obj:`~typing.Any` ] ] + typing.Sequence [ typing.Dict [ str, typing.Any ] ] A list of role objects. Raises ------ - :obj:`~hikari.errors.NotFoundHTTPError` + hikari.errors.NotFoundHTTPError If the guild is not found. - :obj:`~hikari.errors.ForbiddenHTTPError` + hikari.errors.ForbiddenHTTPError If you're not in the guild. """ route = routes.GUILD_ROLES.compile(self.GET, guild_id=guild_id) @@ -2219,34 +2196,34 @@ async def create_guild_role( Parameters ---------- - guild_id : :obj:`~str` + guild_id : str The ID of the guild you want to create the role on. - name : :obj:`~str` + name : str If specified, the new role name string. - permissions : :obj:`~int` + permissions : int If specified, the permissions integer for the role. - color : :obj:`~int` + color : int If specified, the color for the role. - hoist : :obj:`~bool` + hoist : bool If specified, whether the role will be hoisted. - mentionable : :obj:`~bool` + mentionable : bool If specified, whether the role will be able to be mentioned by any user. - reason : :obj:`~str` + reason : str If specified, the audit log reason explaining why the operation was performed. Returns ------- - :obj:`~typing.Dict` [ :obj:`~str`, :obj:`~typing.Any` ] + typing.Dict [ str, typing.Any ] The newly created role object. Raises ------ - :obj:`~hikari.errors.NotFoundHTTPError` + hikari.errors.NotFoundHTTPError If the guild is not found. - :obj:`~hikari.errors.ForbiddenHTTPError` - If you lack the ``MANAGE_ROLES`` permission or you're not in the guild. - :obj:`~hikari.errors.BadRequestHTTPError` + hikari.errors.ForbiddenHTTPError + If you lack the `MANAGE_ROLES` permission or you're not in the guild. + hikari.errors.BadRequestHTTPError If you provide invalid values for the role attributes. """ payload = {} @@ -2267,25 +2244,25 @@ async def modify_guild_role_positions( ---------- guild_id : str The ID of the guild the roles belong to. - role : :obj:`~typing.Tuple` [ :obj:`~str`, :obj:`~int` ] + role : typing.Tuple [ str, int ] The first role to move. This is a tuple of the role ID and the integer position. - *roles : :obj:`~typing.Tuple` [ :obj:`~str`, :obj:`~int` ] + *roles : typing.Tuple [ str, int ] Optional extra roles to move. These must be tuples of the role ID and the integer position. Returns ------- - :obj:`~typing.Sequence` [ :obj:`~typing.Dict` [ :obj:`~str`, :obj:`~typing.Any` ] ] + typing.Sequence [ typing.Dict [ str, typing.Any ] ] A list of all the guild roles. Raises ------ - :obj:`~hikari.errors.NotFoundHTTPError` + hikari.errors.NotFoundHTTPError If either the guild or any of the roles aren't found. - :obj:`~hikari.errors.ForbiddenHTTPError` - If you lack the ``MANAGE_ROLES`` permission or you're not in the guild. - :obj:`~hikari.errors.BadRequestHTTPError` + hikari.errors.ForbiddenHTTPError + If you lack the `MANAGE_ROLES` permission or you're not in the guild. + hikari.errors.BadRequestHTTPError If you provide invalid values for the `position` fields. """ payload = [{"id": r[0], "position": r[1]} for r in (role, *roles)] @@ -2308,36 +2285,36 @@ async def modify_guild_role( # lgtm [py/similar-function] Parameters ---------- - guild_id : :obj:`~str` + guild_id : str The ID of the guild the role belong to. - role_id : :obj:`~str` + role_id : str The ID of the role you want to edit. - name : :obj:`~str` + name : str If specified, the new role's name string. - permissions : :obj:`~int` + permissions : int If specified, the new permissions integer for the role. - color : :obj:`~int` + color : int If specified, the new color for the new role. - hoist : :obj:`~bool` + hoist : bool If specified, whether the role should hoist or not. - mentionable : :obj:`~bool` + mentionable : bool If specified, whether the role should be mentionable or not. - reason : :obj:`~str` + reason : str If specified, the audit log reason explaining why the operation was performed. Returns ------- - :obj:`~typing.Dict` [ :obj:`~str`, :obj:`~typing.Any` ] + typing.Dict [ str, typing.Any ] The edited role object. Raises ------ - :obj:`~hikari.errors.NotFoundHTTPError` + hikari.errors.NotFoundHTTPError If either the guild or role aren't found. - :obj:`~hikari.errors.ForbiddenHTTPError` - If you lack the ``MANAGE_ROLES`` permission or you're not in the guild. - :obj:`~hikari.errors.BadRequestHTTPError` + hikari.errors.ForbiddenHTTPError + If you lack the `MANAGE_ROLES` permission or you're not in the guild. + hikari.errors.BadRequestHTTPError If you provide invalid values for the role attributes. """ payload = {} @@ -2354,17 +2331,17 @@ async def delete_guild_role(self, guild_id: str, role_id: str) -> None: Parameters ---------- - guild_id : :obj:`~str` + guild_id : str The ID of the guild you want to remove the role from. - role_id : :obj:`~str` + role_id : str The ID of the role you want to delete. Raises ------ - :obj:`~hikari.errors.NotFoundHTTPError` + hikari.errors.NotFoundHTTPError If either the guild or the role aren't found. - :obj:`~hikari.errors.ForbiddenHTTPError` - If you lack the ``MANAGE_ROLES`` permission or are not in the guild. + hikari.errors.ForbiddenHTTPError + If you lack the `MANAGE_ROLES` permission or are not in the guild. """ route = routes.GUILD_ROLE.compile(self.DELETE, guild_id=guild_id, role_id=role_id) await self._request(route) @@ -2374,23 +2351,23 @@ async def get_guild_prune_count(self, guild_id: str, days: int) -> int: Parameters ---------- - guild_id : :obj:`~str` + guild_id : str The ID of the guild you want to get the count for. - days : :obj:`~int` - The number of days to count prune for (at least ``1``). + days : int + The number of days to count prune for (at least `1`). Returns ------- - :obj:`~int` + int The number of members estimated to be pruned. Raises ------ - :obj:`~hikari.errors.NotFoundHTTPError` + hikari.errors.NotFoundHTTPError If the guild is not found. - :obj:`~hikari.errors.ForbiddenHTTPError` - If you lack the ``KICK_MEMBERS`` or you are not in the guild. - :obj:`~hikari.errors.BadRequestHTTPError` + hikari.errors.ForbiddenHTTPError + If you lack the `KICK_MEMBERS` or you are not in the guild. + hikari.errors.BadRequestHTTPError If you pass an invalid amount of days. """ payload = {"days": days} @@ -2405,31 +2382,31 @@ async def begin_guild_prune( Parameters ---------- - guild_id : :obj:`~str` + guild_id : str The ID of the guild you want to prune member of. - days : :obj:`~int` + days : int The number of inactivity days you want to use as filter. - compute_prune_count : :obj:`~bool` + compute_prune_count : bool Whether a count of pruned members is returned or not. Discouraged for large guilds out of politeness. - reason : :obj:`~str` + reason : str If specified, the audit log reason explaining why the operation was performed. Returns ------- - :obj:`~int`, optional - The number of members who were kicked if ``compute_prune_count`` - is :obj:`~True`, else :obj:`~None`. + int, optional + The number of members who were kicked if `compute_prune_count` + is True, else None. Raises ------ - :obj:`~hikari.errors.NotFoundHTTPError` + hikari.errors.NotFoundHTTPError If the guild is not found: - :obj:`~hikari.errors.ForbiddenHTTPError` - If you lack the ``KICK_MEMBER`` permission or are not in the guild. - :obj:`~hikari.errors.BadRequestHTTPError` - If you provide invalid values for the ``days`` or ``compute_prune_count`` fields. + hikari.errors.ForbiddenHTTPError + If you lack the `KICK_MEMBER` permission or are not in the guild. + hikari.errors.BadRequestHTTPError + If you provide invalid values for the `days` or `compute_prune_count` fields. """ query = {"days": days} conversions.put_if_specified(query, "compute_prune_count", compute_prune_count, str) @@ -2446,19 +2423,19 @@ async def get_guild_voice_regions(self, guild_id: str) -> typing.Sequence[typing Parameters ---------- - guild_id : :obj:`~str` + guild_id : str The ID of the guild to get the voice regions for. Returns ------- - :obj:`~typing.Sequence` [ :obj:`~typing.Dict` [ :obj:`~str`, :obj:`~typing.Any` ] ] + typing.Sequence [ typing.Dict [ str, typing.Any ] ] A list of voice region objects. Raises ------ - :obj:`~hikari.errors.NotFoundHTTPError` + hikari.errors.NotFoundHTTPError If the guild is not found. - :obj:`~hikari.errors.ForbiddenHTTPError` + hikari.errors.ForbiddenHTTPError If you are not in the guild. """ route = routes.GUILD_VOICE_REGIONS.compile(self.GET, guild_id=guild_id) @@ -2469,20 +2446,20 @@ async def get_guild_invites(self, guild_id: str) -> typing.Sequence[typing.Dict[ Parameters ---------- - guild_id : :obj:`~str` + guild_id : str The ID of the guild to get the invites for. Returns ------- - :obj:`~typing.Sequence` [ :obj:`~typing.Dict` [ :obj:`~str`, :obj:`~typing.Any` ] ] + typing.Sequence [ typing.Dict [ str, typing.Any ] ] A list of invite objects (with metadata). Raises ------ - :obj:`~hikari.errors.NotFoundHTTPError` + hikari.errors.NotFoundHTTPError If the guild is not found. - :obj:`~hikari.errors.ForbiddenHTTPError` - If you lack the ``MANAGE_GUILD`` permission or are not in the guild. + hikari.errors.ForbiddenHTTPError + If you lack the `MANAGE_GUILD` permission or are not in the guild. """ route = routes.GUILD_INVITES.compile(self.GET, guild_id=guild_id) return await self._request(route) @@ -2492,20 +2469,20 @@ async def get_guild_integrations(self, guild_id: str) -> typing.Sequence[typing. Parameters ---------- - guild_id : :obj:`~int` + guild_id : int The ID of the guild to get the integrations for. Returns ------- - :obj:`~typing.Sequence` [ :obj:`~typing.Dict` [ :obj:`~str`, :obj:`~typing.Any` ] ] + typing.Sequence [ typing.Dict [ str, typing.Any ] ] A list of integration objects. Raises ------ - :obj:`~hikari.errors.NotFoundHTTPError` + hikari.errors.NotFoundHTTPError If the guild is not found. - :obj:`~hikari.errors.ForbiddenHTTPError` - If you lack the ``MANAGE_GUILD`` permission or are not in the guild. + hikari.errors.ForbiddenHTTPError + If you lack the `MANAGE_GUILD` permission or are not in the guild. """ route = routes.GUILD_INTEGRATIONS.compile(self.GET, guild_id=guild_id) return await self._request(route) @@ -2524,28 +2501,28 @@ async def modify_guild_integration( Parameters ---------- - guild_id : :obj:`~str` + guild_id : str The ID of the guild to which the integration belongs to. - integration_id : :obj:`~str` + integration_id : str The ID of the integration. - expire_behaviour : :obj:`~int` + expire_behaviour : int If specified, the behaviour for when an integration subscription lapses. - expire_grace_period : :obj:`~int` + expire_grace_period : int If specified, time interval in seconds in which the integration will ignore lapsed subscriptions. - enable_emojis : :obj:`~bool` + enable_emojis : bool If specified, whether emojis should be synced for this integration. - reason : :obj:`~str` + reason : str If specified, the audit log reason explaining why the operation was performed. Raises ------ - :obj:`~hikari.errors.NotFoundHTTPError` + hikari.errors.NotFoundHTTPError If either the guild or the integration aren't found. - :obj:`~hikari.errors.ForbiddenHTTPError` - If you lack the ``MANAGE_GUILD`` permission or are not in the guild. + hikari.errors.ForbiddenHTTPError + If you lack the `MANAGE_GUILD` permission or are not in the guild. """ payload = {} conversions.put_if_specified(payload, "expire_behaviour", expire_behaviour) @@ -2560,19 +2537,19 @@ async def delete_guild_integration(self, guild_id: str, integration_id: str, *, Parameters ---------- - guild_id : :obj:`~str` + guild_id : str The ID of the guild to which the integration belongs to. - integration_id : :obj:`~str` + integration_id : str The ID of the integration to delete. - reason : :obj:`~str` + reason : str If specified, the audit log reason explaining why the operation was performed. Raises ------ - :obj:`~hikari.errors.NotFoundHTTPError` + hikari.errors.NotFoundHTTPError If either the guild or the integration aren't found. - :obj:`~hikari.errors.ForbiddenHTTPError` + hikari.errors.ForbiddenHTTPError If you lack the `MANAGE_GUILD` permission or are not in the guild. """ route = routes.GUILD_INTEGRATION.compile(self.DELETE, guild_id=guild_id, integration_id=integration_id) @@ -2583,17 +2560,17 @@ async def sync_guild_integration(self, guild_id: str, integration_id: str) -> No Parameters ---------- - guild_id : :obj:`~str` + guild_id : str The ID of the guild to which the integration belongs to. - integration_id : :obj:`~str` + integration_id : str The ID of the integration to sync. Raises ------ - :obj:`~hikari.errors.NotFoundHTTPError` + hikari.errors.NotFoundHTTPError If either the guild or the integration aren't found. - :obj:`~hikari.errors.ForbiddenHTTPError` - If you lack the ``MANAGE_GUILD`` permission or are not in the guild. + hikari.errors.ForbiddenHTTPError + If you lack the `MANAGE_GUILD` permission or are not in the guild. """ route = routes.GUILD_INTEGRATION_SYNC.compile(self.POST, guild_id=guild_id, integration_id=integration_id) await self._request(route) @@ -2603,20 +2580,20 @@ async def get_guild_embed(self, guild_id: str) -> typing.Dict[str, typing.Any]: Parameters ---------- - guild_id : :obj:`~str` + guild_id : str The ID of the guild to get the embed for. Returns ------- - :obj:`~typing.Dict` [ :obj:`~str`, :obj:`~typing.Any` ] + typing.Dict [ str, typing.Any ] A guild embed object. Raises ------ - :obj:`~hikari.errors.NotFoundHTTPError` + hikari.errors.NotFoundHTTPError If the guild is not found. - :obj:`~hikari.errors.ForbiddenHTTPError` - If you either lack the ``MANAGE_GUILD`` permission or are not in the guild. + hikari.errors.ForbiddenHTTPError + If you either lack the `MANAGE_GUILD` permission or are not in the guild. """ route = routes.GUILD_EMBED.compile(self.GET, guild_id=guild_id) return await self._request(route) @@ -2628,28 +2605,28 @@ async def modify_guild_embed( Parameters ---------- - guild_id : :obj:`~str` + guild_id : str The ID of the guild to edit the embed for. - channel_id : :obj:`~str`, optional + channel_id : str, optional If specified, the channel that this embed's invite should target. - Set to :obj:`~None` to disable invites for this embed. - enabled : :obj:`~bool` + Set to None to disable invites for this embed. + enabled : bool If specified, whether this embed should be enabled. - reason : :obj:`~str` + reason : str If specified, the audit log reason explaining why the operation was performed. Returns ------- - :obj:`~typing.Dict` [ :obj:`~str`, :obj:`~typing.Any` ] + typing.Dict [ str, typing.Any ] The updated embed object. Raises ------ - :obj:`~hikari.errors.NotFoundHTTPError` + hikari.errors.NotFoundHTTPError If the guild is not found. - :obj:`~hikari.errors.ForbiddenHTTPError` - If you either lack the ``MANAGE_GUILD`` permission or are not in the guild. + hikari.errors.ForbiddenHTTPError + If you either lack the `MANAGE_GUILD` permission or are not in the guild. """ payload = {} conversions.put_if_specified(payload, "channel_id", channel_id) @@ -2662,20 +2639,20 @@ async def get_guild_vanity_url(self, guild_id: str) -> typing.Dict[str, typing.A Parameters ---------- - guild_id : :obj:`~str` + guild_id : str The ID of the guild to get the vanity URL for. Returns ------- - :obj:`~typing.Dict` [ :obj:`~str`, :obj:`~typing.Any` ] - A partial invite object containing the vanity URL in the ``code`` field. + typing.Dict [ str, typing.Any ] + A partial invite object containing the vanity URL in the `code` field. Raises ------ - :obj:`~hikari.errors.NotFoundHTTPError` + hikari.errors.NotFoundHTTPError If the guild is not found. - :obj:`~hikari.errors.ForbiddenHTTPError` - If you either lack the ``MANAGE_GUILD`` permission or are not in the guild. + hikari.errors.ForbiddenHTTPError + If you either lack the `MANAGE_GUILD` permission or are not in the guild. """ route = routes.GUILD_VANITY_URL.compile(self.GET, guild_id=guild_id) return await self._request(route) @@ -2685,24 +2662,23 @@ def get_guild_widget_image_url(self, guild_id: str, *, style: str = ...,) -> str Parameters ---------- - guild_id : :obj:`~str` + guild_id : str The guild ID to use for the widget. - style : :obj:`~str` - If specified, the syle of the widget. + style : str + If specified, the style of the widget. Returns ------- - :obj:`~str` + str A URL to retrieve a PNG widget for your guild. - Note - ---- - This does not actually make any form of request, and shouldn't be awaited. - Thus, it doesn't have rate limits either. + !!! note + This does not actually make any form of request, and shouldn't be + awaited. Thus, it doesn't have rate limits either. - Warning - ------- - The guild must have the widget enabled in the guild settings for this to be valid. + !!! warning + The guild must have the widget enabled in the guild settings for + this to be valid. """ query = "" if style is ... else f"?style={style}" return f"{self.base_url}/guilds/{guild_id}/widget.png" + query @@ -2712,20 +2688,20 @@ async def get_invite(self, invite_code: str, *, with_counts: bool = ...) -> typi Parameters ---------- - invite_code : :obj:`~str` + invite_code : str The ID for wanted invite. - with_counts : :obj:`~bool` + with_counts : bool If specified, whether to attempt to count the number of times the invite has been used. Returns ------- - :obj:`~typing.Dict` [ :obj:`~str`, :obj:`~typing.Any` ] + typing.Dict [ str, typing.Any ] The requested invite object. Raises ------ - :obj:`~hikari.errors.NotFoundHTTPError` + hikari.errors.NotFoundHTTPError If the invite is not found. """ query = {} @@ -2738,22 +2714,22 @@ async def delete_invite(self, invite_code: str) -> None: Parameters ---------- - invite_code : :obj:`~str` + invite_code : str The ID for the invite to be deleted. Returns ------- - :obj:`~None` # Marker + None # Marker Nothing, unlike what the API specifies. This is done to maintain consistency with other calls of a similar nature in this API wrapper. Raises ------ - :obj:`~hikari.errors.NotFoundHTTPError` + hikari.errors.NotFoundHTTPError If the invite is not found. - :obj:`~hikari.errors.ForbiddenHTTPError` - If you lack either ``MANAGE_CHANNELS`` on the channel the invite - belongs to or ``MANAGE_GUILD`` for guild-global delete. + hikari.errors.ForbiddenHTTPError + If you lack either `MANAGE_CHANNELS` on the channel the invite + belongs to or `MANAGE_GUILD` for guild-global delete. """ route = routes.INVITE.compile(self.DELETE, invite_code=invite_code) return await self._request(route) @@ -2763,7 +2739,7 @@ async def get_current_user(self) -> typing.Dict[str, typing.Any]: Returns ------- - :obj:`~typing.Dict` [ :obj:`~str`, :obj:`~typing.Any` ] + typing.Dict [ str, typing.Any ] The current user object. """ route = routes.OWN_USER.compile(self.GET) @@ -2774,17 +2750,17 @@ async def get_user(self, user_id: str) -> typing.Dict[str, typing.Any]: Parameters ---------- - user_id : :obj:`~str` + user_id : str The ID of the user to get. Returns ------- - :obj:`~typing.Dict` [ :obj:`~str`, :obj:`~typing.Any` ] + typing.Dict [ str, typing.Any ] The requested user object. Raises ------ - :obj:`~hikari.errors.NotFoundHTTPError` + hikari.errors.NotFoundHTTPError If the user is not found. """ route = routes.USER.compile(self.GET, user_id=user_id) @@ -2797,21 +2773,21 @@ async def modify_current_user( Parameters ---------- - username : :obj:`~str` + username : str If specified, the new username string. - avatar : :obj:`~bytes`, optional + avatar : bytes, optional If specified, the new avatar image in bytes form. - If it is :obj:`~None`, the avatar is removed. + If it is None, the avatar is removed. Returns ------- - :obj:`~typing.Dict` [ :obj:`~str`, :obj:`~typing.Any` ] + typing.Dict [ str, typing.Any ] The updated user object. Raises ------ - :obj:`~hikari.errors.BadRequestHTTPError` - If you pass username longer than the limit (``2-32``) or an invalid image. + hikari.errors.BadRequestHTTPError + If you pass username longer than the limit (`2-32`) or an invalid image. """ payload = {} conversions.put_if_specified(payload, "username", username) @@ -2822,13 +2798,13 @@ async def modify_current_user( async def get_current_user_connections(self) -> typing.Sequence[typing.Dict[str, typing.Any]]: """Get the current user's connections. - This endpoint can be used with both ``Bearer`` and ``Bot`` tokens but + This endpoint can be used with both `Bearer` and `Bot` tokens but will usually return an empty list for bots (with there being some exceptions to this, like user accounts that have been converted to bots). Returns ------- - :obj:`~typing.Sequence` [ :obj:`~typing.Dict` [ :obj:`~str`, :obj:`~typing.Any` ] ] + typing.Sequence [ typing.Dict [ str, typing.Any ] ] A list of connection objects. """ route = routes.OWN_CONNECTIONS.compile(self.GET) @@ -2841,26 +2817,26 @@ async def get_current_user_guilds( Parameters ---------- - before : :obj:`~str` + before : str If specified, the guild ID to get guilds before it. - after : :obj:`~str` + after : str If specified, the guild ID to get guilds after it. - limit : :obj:`~int` + limit : int If specified, the limit of guilds to get. Has to be between - ``1`` and ``100``. + `1` and `100`. Returns ------- - :obj:`~typing.Sequence` [ :obj:`~typing.Dict` [ :obj:`~str`, :obj:`~typing.Any` ] ] + typing.Sequence [ typing.Dict [ str, typing.Any ] ] A list of partial guild objects. Raises ------ - :obj:`~hikari.errors.BadRequestHTTPError` - If you pass both ``before`` and ``after`` or an - invalid value for ``limit``. + hikari.errors.BadRequestHTTPError + If you pass both `before` and `after` or an + invalid value for `limit`. """ query = {} conversions.put_if_specified(query, "before", before) @@ -2874,12 +2850,12 @@ async def leave_guild(self, guild_id: str) -> None: Parameters ---------- - guild_id : :obj:`~str` + guild_id : str The ID of the guild to leave. Raises ------ - :obj:`~hikari.errors.NotFoundHTTPError` + hikari.errors.NotFoundHTTPError If the guild is not found. """ route = routes.LEAVE_GUILD.compile(self.DELETE, guild_id=guild_id) @@ -2890,17 +2866,17 @@ async def create_dm(self, recipient_id: str) -> typing.Dict[str, typing.Any]: Parameters ---------- - recipient_id : :obj:`~str` + recipient_id : str The ID of the user to create the new DM channel with. Returns ------- - :obj:`~typing.Dict` [ :obj:`~str`, :obj:`~typing.Any` ] + typing.Dict [ str, typing.Any ] The newly created DM channel object. Raises ------ - :obj:`~hikari.errors.NotFoundHTTPError` + hikari.errors.NotFoundHTTPError If the recipient is not found. """ payload = {"recipient_id": recipient_id} @@ -2912,12 +2888,11 @@ async def list_voice_regions(self) -> typing.Sequence[typing.Dict[str, typing.An Returns ------- - :obj:`~typing.Sequence` [ :obj:`~typing.Dict` [ :obj:`~str`, :obj:`~typing.Any` ] ] + typing.Sequence [ typing.Dict [ str, typing.Any ] ] A list of voice regions available - Note - ---- - This does not include VIP servers. + !!! note + This does not include VIP servers. """ route = routes.VOICE_REGIONS.compile(self.GET) return await self._request(route) @@ -2929,29 +2904,29 @@ async def create_webhook( Parameters ---------- - channel_id : :obj:`~str` + channel_id : str The ID of the channel for webhook to be created in. - name : :obj:`~str` + name : str The webhook's name string. - avatar : :obj:`~bytes` + avatar : bytes If specified, the avatar image in bytes form. - reason : :obj:`~str` + reason : str If specified, the audit log reason explaining why the operation was performed. Returns ------- - :obj:`~typing.Dict` [ :obj:`~str`, :obj:`~typing.Any` ] + typing.Dict [ str, typing.Any ] The newly created webhook object. Raises ------ - :obj:`~hikari.errors.NotFoundHTTPError` + hikari.errors.NotFoundHTTPError If the channel is not found. - :obj:`~hikari.errors.ForbiddenHTTPError` - If you either lack the ``MANAGE_WEBHOOKS`` permission or + hikari.errors.ForbiddenHTTPError + If you either lack the `MANAGE_WEBHOOKS` permission or can not see the given channel. - :obj:`~hikari.errors.BadRequestHTTPError` + hikari.errors.BadRequestHTTPError If the avatar image is too big or the format is invalid. """ payload = {"name": name} @@ -2964,20 +2939,20 @@ async def get_channel_webhooks(self, channel_id: str) -> typing.Sequence[typing. Parameters ---------- - channel_id : :obj:`~str` + channel_id : str The ID of the channel to get the webhooks from. Returns ------- - :obj:`~typing.Sequence` [ :obj:`~typing.Dict` [ :obj:`~str`, :obj:`~typing.Any` ] ] + typing.Sequence [ typing.Dict [ str, typing.Any ] ] A list of webhook objects for the give channel. Raises ------ - :obj:`~hikari.errors.NotFoundHTTPError` + hikari.errors.NotFoundHTTPError If the channel is not found. - :obj:`~hikari.errors.ForbiddenHTTPError` - If you either lack the ``MANAGE_WEBHOOKS`` permission or + hikari.errors.ForbiddenHTTPError + If you either lack the `MANAGE_WEBHOOKS` permission or can not see the given channel. """ route = routes.CHANNEL_WEBHOOKS.compile(self.GET, channel_id=channel_id) @@ -2988,20 +2963,20 @@ async def get_guild_webhooks(self, guild_id: str) -> typing.Sequence[typing.Dict Parameters ---------- - guild_id : :obj:`~str` + guild_id : str The ID for the guild to get the webhooks from. Returns ------- - :obj:`~typing.Sequence` [ :obj:`~typing.Dict` [ :obj:`~str`, :obj:`~typing.Any` ] ] + typing.Sequence [ typing.Dict [ str, typing.Any ] ] A list of webhook objects for the given guild. Raises ------ - :obj:`~hikari.errors.NotFoundHTTPError` + hikari.errors.NotFoundHTTPError If the guild is not found. - :obj:`~hikari.errors.ForbiddenHTTPError` - If you either lack the ``MANAGE_WEBHOOKS`` permission or + hikari.errors.ForbiddenHTTPError + If you either lack the `MANAGE_WEBHOOKS` permission or aren't a member of the given guild. """ route = routes.GUILD_WEBHOOKS.compile(self.GET, guild_id=guild_id) @@ -3012,24 +2987,24 @@ async def get_webhook(self, webhook_id: str, *, webhook_token: str = ...) -> typ Parameters ---------- - webhook_id : :obj:`~str` + webhook_id : str The ID of the webhook to get. - webhook_token : :obj:`~str` + webhook_token : str If specified, the webhook token to use to get it (bypassing bot authorization). Returns ------- - :obj:`~typing.Dict` [ :obj:`~str`, :obj:`~typing.Any` ] + typing.Dict [ str, typing.Any ] The requested webhook object. Raises ------ - :obj:`~hikari.errors.NotFoundHTTPError` + hikari.errors.NotFoundHTTPError If the webhook is not found. - :obj:`~hikari.errors.ForbiddenHTTPError` + hikari.errors.ForbiddenHTTPError If you're not in the guild that owns this webhook or - lack the ``MANAGE_WEBHOOKS`` permission. - :obj:`~hikari.errors.UnauthorizedHTTPError` + lack the `MANAGE_WEBHOOKS` permission. + hikari.errors.UnauthorizedHTTPError If you pass a token that's invalid for the target webhook. """ if webhook_token is ...: @@ -3052,35 +3027,35 @@ async def modify_webhook( Parameters ---------- - webhook_id : :obj:`~str` + webhook_id : str The ID of the webhook to edit. - webhook_token : :obj:`~str` + webhook_token : str If specified, the webhook token to use to modify it (bypassing bot authorization). - name : :obj:`~str` + name : str If specified, the new name string. - avatar : :obj:`~bytes` + avatar : bytes If specified, the new avatar image in bytes form. If None, then it is removed. - channel_id : :obj:`~str` + channel_id : str If specified, the ID of the new channel the given webhook should be moved to. - reason : :obj:`~str` + reason : str If specified, the audit log reason explaining why the operation was performed. Returns ------- - :obj:`~typing.Dict` [ :obj:`~str`, :obj:`~typing.Any` ] + typing.Dict [ str, typing.Any ] The updated webhook object. Raises ------ - :obj:`~hikari.errors.NotFoundHTTPError` + hikari.errors.NotFoundHTTPError If either the webhook or the channel aren't found. - :obj:`~hikari.errors.ForbiddenHTTPError` - If you either lack the ``MANAGE_WEBHOOKS`` permission or + hikari.errors.ForbiddenHTTPError + If you either lack the `MANAGE_WEBHOOKS` permission or aren't a member of the guild this webhook belongs to. - :obj:`~hikari.errors.UnauthorizedHTTPError` + hikari.errors.UnauthorizedHTTPError If you pass a token that's invalid for the target webhook. """ payload = {} @@ -3100,20 +3075,20 @@ async def delete_webhook(self, webhook_id: str, *, webhook_token: str = ...) -> Parameters ---------- - webhook_id : :obj:`~str` + webhook_id : str The ID of the webhook to delete - webhook_token : :obj:`~str` + webhook_token : str If specified, the webhook token to use to delete it (bypassing bot authorization). Raises ------ - :obj:`~hikari.errors.NotFoundHTTPError` + hikari.errors.NotFoundHTTPError If the webhook is not found. - :obj:`~hikari.errors.ForbiddenHTTPError` - If you either lack the ``MANAGE_WEBHOOKS`` permission or + hikari.errors.ForbiddenHTTPError + If you either lack the `MANAGE_WEBHOOKS` permission or aren't a member of the guild this webhook belongs to. - :obj:`~hikari.errors.UnauthorizedHTTPError` + hikari.errors.UnauthorizedHTTPError If you pass a token that's invalid for the target webhook. """ if webhook_token is ...: @@ -3140,55 +3115,55 @@ async def execute_webhook( Parameters ---------- - webhook_id : :obj:`~str` + webhook_id : str The ID of the webhook to execute. - webhook_token : :obj:`~str` + webhook_token : str The token of the webhook to execute. - content : :obj:`~str` + content : str If specified, the webhook message content to send. - username : :obj:`~str` + username : str If specified, the username to override the webhook's username for this request. - avatar_url : :obj:`~str` + avatar_url : str If specified, the url of an image to override the webhook's avatar with for this request. - tts : :obj:`~bool` + tts : bool If specified, whether this webhook should create a TTS message. - wait : :obj:`~bool` + wait : bool If specified, whether this request should wait for the webhook to be executed and return the resultant message object. - file : :obj:`~typing.Tuple` [ :obj:`~str`, :obj:`~io.IOBase` ] - If specified, a tuple of the file name and either raw :obj:`~bytes` - or a :obj:`~io.IOBase` derived object that points to a buffer + file : typing.Tuple [ str, io.IOBase ] + If specified, a tuple of the file name and either raw `bytes` + or a `io.IOBase` derived object that points to a buffer containing said file. - embeds : :obj:`~typing.Sequence` [:obj:`~typing.Dict` [ :obj:`~str`, :obj:`~typing.Any` ]] + embeds : typing.Sequence [typing.Dict [ str, typing.Any ]] If specified, the sequence of embed objects that will be sent with this message. - allowed_mentions : :obj:`~typing.Dict` [ :obj:`~str`, :obj:`~typing.Any` ] - If specified, the mentions to parse from the ``content``. - If not specified, will parse all mentions from the ``content``. + allowed_mentions : typing.Dict [ str, typing.Any ] + If specified, the mentions to parse from the `content`. + If not specified, will parse all mentions from the `content`. Returns ------- - :obj:`~typing.Dict` [ :obj:`~str`, :obj:`~typing.Any` ], optional - The created message object if ``wait`` is :obj:`~True`, else - :obj:`~None`. + typing.Dict [ str, typing.Any ], optional + The created message object if `wait` is `True`, else + `None`. Raises ------ - :obj:`~hikari.errors.NotFoundHTTPError` + hikari.errors.NotFoundHTTPError If the channel ID or webhook ID is not found. - :obj:`~hikari.errors.BadRequestHTTPError` + hikari.errors.BadRequestHTTPError This can be raised if the file is too large; if the embed exceeds the defined limits; if the message content is specified only and - empty or greater than ``2000`` characters; if neither content, file + empty or greater than `2000` characters; if neither content, file or embed are specified; if there is a duplicate id in only of the - fields in ``allowed_mentions``; if you specify to parse all + fields in `allowed_mentions`; if you specify to parse all users/roles mentions but also specify which users/roles to parse only. - :obj:`~hikari.errors.ForbiddenHTTPError` + hikari.errors.ForbiddenHTTPError If you lack permissions to send to this channel. - :obj:`~hikari.errors.UnauthorizedHTTPError` + hikari.errors.UnauthorizedHTTPError If you pass a token that's invalid for the target webhook. """ form = aiohttp.FormData() @@ -3232,7 +3207,7 @@ async def get_current_application_info(self) -> typing.Dict[str, typing.Any]: Returns ------- - :obj:`~typing.Dict` [ :obj:`~str`, :obj:`~typing.Any` ] + typing.Dict [ str, typing.Any ] An application info object. """ route = routes.OAUTH2_APPLICATIONS_ME.compile(self.GET) diff --git a/hikari/net/routes.py b/hikari/net/routes.py index 32908cb611..c2a1af8133 100644 --- a/hikari/net/routes.py +++ b/hikari/net/routes.py @@ -29,35 +29,27 @@ class CompiledRoute: Parameters ---------- - method : :obj:`~str` + method : str The HTTP method to use. - path : :obj:`~str` + path : str The path with any major parameters interpolated in. - major_params_hash : :obj:`~str` + major_params_hash : str The part of the hash identifier to use for the compiled set of major parameters. """ __slots__ = ("method", "major_params_hash", "compiled_path", "hash_code", "__weakref__") - #: The method to use on the route. - #: - #: :type: :obj:`~str` method: typing.Final[str] + """The method to use on the route.""" - #: The major parameters in a bucket hash-compatible representation. - #: - #: :type: :obj:`~str` major_params_hash: typing.Final[str] + """The major parameters in a bucket hash-compatible representation.""" - #: The compiled route path to use - #: - #: :type: :obj:`~str` compiled_path: typing.Final[str] + """The compiled route path to use.""" - #: The hash code - #: - #: :type: :obj:`~int` hash_code: typing.Final[int] + """The hash code.""" def __init__(self, method: str, path_template: str, path: str, major_params_hash: str) -> None: self.method = method @@ -70,12 +62,12 @@ def create_url(self, base_url: str) -> str: Parameters ---------- - base_url : :obj:`~str` + base_url : str The base of the URL to prepend to the compiled path. Returns ------- - :obj:`~str` + str The full URL for the route. """ return base_url + self.compiled_path @@ -88,13 +80,13 @@ def create_real_bucket_hash(self, initial_bucket_hash: str) -> str: Parameters ---------- - initial_bucket_hash: :obj:`~str` + initial_bucket_hash : str The initial bucket hash provided by Discord in the HTTP headers for a given response. Returns ------- - :obj:`~str` + str The input hash amalgamated with a hash code produced by the major parameters in this compiled route instance. """ @@ -128,9 +120,9 @@ class RouteTemplate: Parameters ---------- - path_template : :obj:`~str` + path_template : str The template string for the path to use. - major_params : :obj:`~str` + major_params : str A collection of major parameter names that appear in the template path. If not specified, the default major parameter names are extracted and used in-place. @@ -138,15 +130,11 @@ class RouteTemplate: __slots__ = ("path_template", "major_params") - #: The template string used for the path. - #: - #: :type: :obj:`~str` path_template: typing.Final[str] + """The template string used for the path.""" - #: Major parameter names that appear in the template path. - #: - #: :type: :obj:`~typing.FrozenSet` [ :obj:`~str` ] major_params: typing.Final[typing.FrozenSet[str]] + """Major parameter names that appear in the template path.""" def __init__(self, path_template: str, major_params: typing.Collection[str] = None) -> None: self.path_template = path_template @@ -156,21 +144,22 @@ def __init__(self, path_template: str, major_params: typing.Collection[str] = No self.major_params = frozenset(major_params) def compile(self, method: str, /, **kwargs: typing.Any) -> CompiledRoute: - """Generate a formatted :obj:`~CompiledRoute` for this route template. + """Generate a formatted `CompiledRoute` for this route template. - This takes into account any URL parameters that have been passed, and extracting - the :attr:major_params" for bucket hash operations accordingly. + This takes into account any URL parameters that have been passed, and + extracting the `RouteTemplate.major_params` for bucket hash operations + accordingly. Parameters ---------- - method : :obj:`~str` + method : str The method to use. - **kwargs : :obj:`~typing.Any` + **kwargs : typing.Any Any parameters to interpolate into the route path. Returns ------- - :obj:`~CompiledRoute` + CompiledRoute The compiled route. """ major_hash_part = "-".join((str(kwargs[p]) for p in self.major_params)) diff --git a/hikari/net/shards.py b/hikari/net/shards.py index 7554747749..b055d7e02d 100644 --- a/hikari/net/shards.py +++ b/hikari/net/shards.py @@ -26,9 +26,9 @@ See Also -------- -* IANA WS closure code standards: https://www.iana.org/assignments/websocket/websocket.xhtml -* Gateway documentation: https://discordapp.com/developers/docs/topics/gateway -* Opcode documentation: https://discordapp.com/developers/docs/topics/opcodes-and-status-codes +* [IANA WS closure code standards](https://www.iana.org/assignments/websocket/websocket.xhtml) +* [Gateway documentation](https://discordapp.com/developers/docs/topics/gateway) +* [Opcode documentation](https://discordapp.com/developers/docs/topics/opcodes-and-status-codes) """ __all__ = ["Shard"] @@ -53,8 +53,8 @@ from hikari.net import ratelimits from hikari.net import user_agents -#: The signature for an event dispatch callback. DispatchT = typing.Callable[["Shard", str, typing.Dict], None] +"""The signature for an event dispatch callback.""" VERSION_6: typing.Final[int] = 6 @@ -68,92 +68,88 @@ class Shard: application of events that occur, and to allow you to change your presence, amongst other real-time applications. - Each :obj:`~Shard` represents a single shard. + Each `Shard` represents a single shard. Expected events that may be passed to the event dispatcher are documented in the - `gateway event reference `_. + [gateway event reference](https://discordapp.com/developers/docs/topics/gateway#commands-and-events) . No normalization of the gateway event names occurs. In addition to this, some internal events can also be triggered to notify you of changes to the connection state. - * ``CONNECTED`` - fired on initial connection to Discord. - * ``DISCONNECTED`` - fired when the connection is closed for any reason. + * `CONNECTED` - fired on initial connection to Discord. + * `DISCONNECTED` - fired when the connection is closed for any reason. Parameters ---------- - compression: :obj:`~bool` - If True, then payload compression is enabled on the connection. - If False, no payloads are compressed. You usually want to keep this + compression : bool + If `True`, then payload compression is enabled on the connection. + If `False`, no payloads are compressed. You usually want to keep this enabled. - connector: :obj:`~aiohttp.BaseConnector`, optional - The :obj:`~aiohttp.BaseConnector` to use for the HTTP session that + connector : aiohttp.BaseConnector, optional + The `aiohttp.BaseConnector` to use for the HTTP session that gets upgraded to a websocket connection. You can use this to customise connection pooling, etc. - debug: :obj:`~bool` - If True, the client is configured to provide extra contextual + debug : bool + If `True`, the client is configured to provide extra contextual information to use when debugging this library or extending it. This includes logging every payload that is sent or received to the logger as debug entries. Generally it is best to keep this disabled. - dispatch: ``dispatch function`` + dispatch : `dispatch function` The function to invoke with any dispatched events. This must not be a coroutine function, and must take three arguments only. The first is - the reference to this :obj:`~Shard` The second is the + the reference to this `Shard` The second is the event name. - initial_presence: :obj:`~typing.Dict`, optional - A raw JSON object as a :obj:`~typing.Dict` that should be set as the - initial presence of the bot user once online. If :obj:`~None`, then it + initial_presence : typing.Dict, optional + A raw JSON object as a `typing.Dict` that should be set as the + initial presence of the bot user once online. If `None`, then it will be set to the default, which is showing up as online without a custom status message. - intents: :obj:`~hikari.intents.Intent`, optional + intents : hikari.intents.Intent, optional Bitfield of intents to use. If you use the V7 API, this is mandatory. This field will determine what events you will receive. - json_deserialize: ``deserialization function`` - A custom JSON deserializer function to use. Defaults to - :func:`json.loads`. - json_serialize: ``serialization function`` - A custom JSON serializer function to use. Defaults to - :func:`json.dumps`. - large_threshold: :obj:`~int` + json_deserialize : `deserialization function` + A custom JSON deserializer function to use. Defaults to `json.loads`. + json_serialize : `serialization function` + A custom JSON serializer function to use. Defaults to `json.dumps`. + large_threshold : int The number of members that have to be in a guild for it to be considered to be "large". Large guilds will not have member information sent automatically, and must manually request that member chunks be - sent using :meth:`request_guild_members`. - proxy_auth: :obj:`~aiohttp.BasicAuth`, optional - Optional :obj:`~aiohttp.BasicAuth` object that can be provided to - allow authenticating with a proxy if you use one. Leave :obj:`~None` to - ignore. - proxy_headers: :obj:`~typing.Mapping` [ :obj:`~str`, :obj:`~str` ], optional - Optional :obj:`~typing.Mapping` to provide as headers to allow the - connection through a proxy if you use one. Leave :obj:`~None` to ignore. - proxy_url: :obj:`~str`, optional - Optional :obj:`~str` to use for a proxy server. If :obj:`~None`, then it - is ignored. - session_id: :obj:`~str`, optional - The session ID to use. If specified along with ``seq``, then the - gateway client will attempt to ``RESUME`` an existing session rather than - re-``IDENTIFY``. Otherwise, it will be ignored. - seq: :obj:`~int`, optional - The sequence number to use. If specified along with ``session_id``, then - the gateway client will attempt to ``RESUME`` an existing session rather - than re-``IDENTIFY``. Otherwise, it will be ignored. - shard_id: :obj:`~int` - The shard ID of this gateway client. Defaults to ``0``. - shard_count: :obj:`~int` - The number of shards on this gateway. Defaults to ``1``, which implies no + sent using `ShardConnection.request_guild_members`. + proxy_auth : aiohttp.BasicAuth, optional + Optional `aiohttp.BasicAuth` object that can be provided to + allow authenticating with a proxy if you use one. Leave `None` to ignore. + proxy_headers : typing.Mapping [ str, str ], optional + Optional `typing.Mapping` to provide as headers to allow the + connection through a proxy if you use one. Leave `None` to ignore. + proxy_url : str, optional + Optional `str` to use for a proxy server. If `None`, then it is ignored. + session_id : str, optional + The session ID to use. If specified along with `seq`, then the + gateway client will attempt to `RESUME` an existing session rather than + re-`IDENTIFY`. Otherwise, it will be ignored. + seq : int, optional + The sequence number to use. If specified along with `session_id`, then + the gateway client will attempt to `RESUME` an existing session rather + than re-`IDENTIFY`. Otherwise, it will be ignored. + shard_id : int + The shard ID of this gateway client. Defaults to `0`. + shard_count : int + The number of shards on this gateway. Defaults to `1`, which implies no sharding is taking place. - ssl_context: :obj:`~ssl.SSLContext`, optional - An optional custom :obj:`~ssl.SSLContext` to provide to customise how + ssl_context : ssl.SSLContext, optional + An optional custom `ssl.SSLContext` to provide to customise how SSL works. - token: :obj:`~str` + token : str The mandatory bot token for the bot account to use, minus the "Bot" authentication prefix used elsewhere. - url: :obj:`~str` + url : str The websocket URL to use. - verify_ssl: :obj:`~bool` - If :obj:`~True`, SSL verification is enabled, which is generally what you + verify_ssl : bool + If `True`, SSL verification is enabled, which is generally what you want. If you get SSL issues, you can try turning this off at your own risk. - version: :obj:`~int` + version : int The version of the API to use. Defaults to the most recent stable version (v6). """ @@ -221,119 +217,103 @@ class Shard: _ws: typing.Optional[aiohttp.ClientWebSocketResponse] _zlib: typing.Optional[zlib.decompressobj] - #: An event that is set when the connection closes. - #: - #: :type: :obj:`~asyncio.Event` closed_event: typing.Final[asyncio.Event] + """An event that is set when the connection closes.""" - #: The number of times we have disconnected from the gateway on this - #: client instance. - #: - #: :type: :obj:`~int` disconnect_count: int + """The number of times we have disconnected from the gateway on this + client instance. + """ - #: The dispatch method to call when dispatching a new event. This is - #: the method passed in the constructor. dispatch: DispatchT + """The dispatch method to call when dispatching a new event. + + This is the method passed in the constructor. + """ - #: The heartbeat interval Discord instructed the client to beat at. - #: This is ``nan`` until this information is received. - #: - #: :type: :obj:`~float` heartbeat_interval: float + """The heartbeat interval Discord instructed the client to beat at. + + This is `nan` until this information is received. + """ - #: The most recent heartbeat latency measurement in seconds. This is - #: ``nan`` until this information is available. The latency is calculated - #: as the time between sending a ``HEARTBEAT`` payload and receiving a - #: ``HEARTBEAT_ACK`` response. - #: - #: :type: :obj:`~float` heartbeat_latency: float + """The most recent heartbeat latency measurement in seconds. + + This is `nan` until this information is available. The latency is calculated + as the time between sending a `HEARTBEAT` payload and receiving a + `HEARTBEAT_ACK` response. + """ - #: An event that is set when Discord sends a ``HELLO`` payload. This - #: indicates some sort of connection has successfully been made. - #: - #: :type: :obj:`~asyncio.Event` hello_event: typing.Final[asyncio.Event] + """An event that is set when Discord sends a `HELLO` payload. + + This indicates some sort of connection has successfully been made. + """ - #: An event that is set when the client has successfully ``IDENTIFY``ed - #: or ``RESUMED`` with the gateway. This indicates regular communication - #: can now take place on the connection and events can be expected to - #: be received. - #: - #: :type: :obj:`~asyncio.Event` handshake_event: typing.Final[asyncio.Event] + """An event that is set when the client has successfully `IDENTIFY`ed + or `RESUMED` with the gateway. + + This indicates regular communication can now take place on the connection + and events can be expected to be received. + """ - #: The monotonic timestamp that the last ``HEARTBEAT`` was sent at, or - #: ``nan`` if no ``HEARTBEAT`` has yet been sent. - #: - #: :type: :obj:`~float` last_heartbeat_sent: float + """The monotonic timestamp that the last `HEARTBEAT` was sent at. + + This will be `nan` if no `HEARTBEAT` has yet been sent. + """ - #: The monotonic timestamp at which the last payload was received from - #: Discord. If this was more than the ``heartbeat_interval`` from - #: the current time, then the connection is assumed to be zombied and - #: is shut down. If no messages have been received yet, this is ``nan``. - #: - #: :type: :obj:`~float` last_message_received: float + """The monotonic timestamp at which the last payload was received from Discord. + + If this was more than the `heartbeat_interval` from the current time, then + the connection is assumed to be zombied and is shut down. + If no messages have been received yet, this is `nan`. + """ - #: The logger used for dumping information about what this client is doing. - #: - #: :type: :obj:`~logging.Logger` logger: typing.Final[logging.Logger] + """The logger used for dumping information about what this client is doing.""" - #: An event that is triggered when a ``READY`` payload is received for the - #: shard. This indicates that it successfully started up and had a correct - #: sharding configuration. This is more appropriate to wait for than - #: :attr:`identify_event` since the former will still fire if starting - #: shards too closely together, for example. This would still lead to an - #: immediate invalid session being fired afterwards. - # - #: It is worth noting that this event is only set for the first ``READY`` - #: event after connecting with a fresh connection. For all other purposes, - #: you should wait for the event to be fired in the ``dispatch`` function - #: you provide. - #: - #: :type: :obj:`~asyncio.Event` ready_event: typing.Final[asyncio.Event] + """An event that is triggered when a `READY` payload is received for the shard. + + This indicates that it successfully started up and had a correct sharding + configuration. This is more appropriate to wait for than + `Shard.handshake_event` since the former will still fire if starting + shards too closely together, for example. This would still lead to an + immediate invalid session being fired afterwards. + + It is worth noting that this event is only set for the first `READY` event + after connecting with a fresh connection. For all other purposes, you should + wait for the event to be fired in the `dispatch` function you provide. + """ - #: An event that is triggered when a resume has succeeded on the gateway. - #: - #: :type: :obj:`~asyncio.Event` resumed_event: typing.Final[asyncio.Event] + """An event that is triggered when a resume has succeeded on the gateway.""" - #: An event that is set when something requests that the connection - #: should close somewhere. - #: - #: :type: :obj:`~asyncio.Event` requesting_close_event: typing.Final[asyncio.Event] + """An event that is set when something requests that the connection should + close somewhere. + """ - #: The current session ID, if known. - #: - #: :type: :obj:`~str`, optional session_id: typing.Optional[str] + """The current session ID, if known.""" - #: The current sequence number for state synchronization with the API, - #: if known. - #: - #: :type: :obj:`~int`, optional. seq: typing.Optional[int] + """The current sequence number for state synchronization with the API, + if known. + """ - #: The shard ID. - #: - #: :type: :obj:`~int` shard_id: typing.Final[int] + """The shard ID.""" - #: The number of shards in use for the bot. - #: - #: :type: :obj:`~int` shard_count: typing.Final[int] + """The number of shards in use for the bot.""" - #: The API version to use on Discord. - #: - #: :type: :obj:`~int` version: typing.Final[int] + """The API version to use on Discord.""" def __init__( self, @@ -415,54 +395,33 @@ def __init__( def uptime(self) -> datetime.timedelta: """Amount of time the connection has been running for. - Returns - ------- - :obj:`~datetime.timedelta` - The amount of time the connection has been running for. If it isn't - running, this will always return ``0`` seconds. + If this connection isn't running, this will always be `0` seconds. """ delta = time.perf_counter() - self._connected_at return datetime.timedelta(seconds=0 if math.isnan(delta) else delta) @property def is_connected(self) -> bool: - """Whether the gateway is connected or not. - - Returns - ------- - :obj:`~bool` - True if this gateway client is actively connected to something, or - False if it is not running. - """ + """Whether the gateway is connected or not.""" return not math.isnan(self._connected_at) @property def intents(self) -> typing.Optional[_intents.Intent]: """Intents being used. - If this is :obj:`~None`, no intent usage was being - used on this shard. On V6 this would be regular usage as prior to - the intents change in January 2020. If on V7, you just won't be - able to connect at all to the gateway. - - Returns - ------- - :obj:`~hikari.intents.Intent`, optional - The intents being used. + If this is `None`, no intent usage was being used on this shard. + On V6 this would be regular usage as prior to the intents change in + January 2020. If on V7, you just won't be able to connect at all to the + gateway. """ return self._intents @property def reconnect_count(self) -> int: - """Reconnection count for this shard connection instance. + """Amount of times the gateway has reconnected since initialization. This can be used as a debugging context, but is also used internally for exception management. - - Returns - ------- - :obj:`~int` - The amount of times the gateway has reconnected since initialization. """ # 0 disconnects + not is_connected => 0 # 0 disconnects + is_connected => 0 @@ -475,13 +434,7 @@ def reconnect_count(self) -> int: # Ignore docstring not starting in an imperative mood @property def current_presence(self) -> typing.Dict: # noqa: D401 - """Current presence for the gateway. - - Returns - ------- - :obj:`~typing.Dict` - The current presence for the gateway. - """ + """Current presence for the gateway.""" # Make a shallow copy to prevent mutation. return dict(self._presence or {}) @@ -497,35 +450,35 @@ async def request_guild_members(self, guild_id, *guild_ids, **kwargs): """Request the guild members for a guild or set of guilds. These guilds must be being served by this shard, and the results will be - provided to the dispatcher with ``GUILD_MEMBER_CHUNK`` events. + provided to the dispatcher with `GUILD_MEMBER_CHUNK` events. Parameters ---------- - guild_id : :obj:`~str` + guild_id : str The first guild to request members for. - *guild_ids : :obj:`~str` + *guild_ids : str Additional guilds to request members for. **kwargs Optional arguments. Keyword Args ------------ - limit : :obj:`~int` - Limit for the number of members to respond with. Set to ``0`` to be + limit : int + Limit for the number of members to respond with. Set to `0` to be unlimited. - query : :obj:`~str` + query : str An optional string to filter members with. If specified, only members who have a username starting with this string will be returned. - user_ids : :obj:`~typing.Sequence` [ :obj:`~str` ] + user_ids : typing.Sequence [ str ] An optional list of user IDs to return member info about. - Note - ---- - You may not specify ``user_ids`` at the same time as ``limit`` and - ``query``. Likewise, if you specify one of ``limit`` or ``query``, - the other must also be included. The default, if no optional arguments - are specified, is to use a ``limit = 0`` and a ``query = ""`` (empty-string). + !!! note + You may not specify `user_id` at the same time as `limit` and + `query`. Likewise, if you specify one of `limit` or `query`, the + other must also be included. The default, if no optional arguments + are specified, is to use a `limit = 0` and a `query = ""` + (empty-string). """ guilds = [guild_id, *guild_ids] constraints = {} @@ -550,7 +503,7 @@ async def update_presence(self, presence: typing.Dict) -> None: Parameters ---------- - presence : :obj:`~typing.Dict` + presence : typing.Dict The new presence payload to set. """ presence.setdefault("since", None) @@ -567,9 +520,9 @@ async def connect(self, client_session_type=aiohttp.ClientSession) -> None: Parameters ---------- - client_session_type + client_session_type : aiohttp.ClientSession The client session implementation to use. You generally do not want - to change this from the default, which is :obj:`~aiohttp.ClientSession`. + to change this from the default, which is `aiohttp.ClientSession`. """ if self.is_connected: raise RuntimeError("Already connected") @@ -666,8 +619,8 @@ async def close(self, close_code: int = 1000) -> None: Parameters ---------- - close_code : :obj:`~int` - The close code to use. Defaults to ``1000`` (normal closure). + close_code : int + The close code to use. Defaults to `1000` (normal closure). """ if not self.requesting_close_event.is_set(): self.logger.debug("closing websocket connection") diff --git a/hikari/net/user_agents.py b/hikari/net/user_agents.py index a36154ceea..fdbe395323 100644 --- a/hikari/net/user_agents.py +++ b/hikari/net/user_agents.py @@ -34,46 +34,41 @@ class UserAgent(metaclass=meta.SingletonMeta): """Platform version info. - Notes - ----- - This is a singleton. + !!! note + This is a singleton. """ - #: The version of the library. - #: - #: Example - #: ------- - #: ``"hikari 1.0.1"`` - #: - #: :type: :obj:`~typing.Final` [ :obj:`~str` ] library_version: typing.Final[str] + """The version of the library. + + Examples + -------- + `"hikari 1.0.1"` + """ - #: The platform version. - #: - #: Example - #: ------- - #: ``"CPython 3.8.2 GCC 9.2.0"`` - #: - #: :type: :obj:`~typing.Final` [ :obj:`~str` ] platform_version: typing.Final[str] + """The platform version. + + Examples + -------- + `"CPython 3.8.2 GCC 9.2.0"` + """ - #: The operating system type. - #: - #: Example - #: ------- - #: ``"Linux-5.4.15-2-MANJARO-x86_64-with-glibc2.2.5"`` - #: - #: :type: :obj:`~typing.Final` [ :obj:`~str` ] system_type: typing.Final[str] + """The operating system type. + + Examples + -------- + `"Linux-5.4.15-2-MANJARO-x86_64-with-glibc2.2.5"` + """ - #: The Hikari-specific user-agent to use in HTTP connections to Discord. - #: - #: Example - #: ------- - #: ``"DiscordBot (https://gitlab.com/nekokatt/hikari; 1.0.1; Nekokatt) CPython 3.8.2 GCC 9.2.0 Linux"`` - #: - #: :type: :obj:`~typing.Final` [ :obj:`~str` ] user_agent: typing.Final[str] + """The Hikari-specific user-agent to use in HTTP connections to Discord. + + Examples + -------- + `"DiscordBot (https://gitlab.com/nekokatt/hikari; 1.0.1; Nekokatt) CPython 3.8.2 GCC 9.2.0 Linux"` + """ def __init__(self): from hikari._about import __author__, __url__, __version__ @@ -96,16 +91,13 @@ def __attr__(_): def _join_strip(*args): return " ".join((arg.strip() for arg in args if arg.strip())) - # Ignore docstring not starting in an imperative mood + # Inore docstring not starting in an imperativge mood @property def websocket_triplet(self) -> typing.Dict[str, str]: # noqa: D401 """A dict representing device and library info. - Returns - ------- - :obj:`~typing.Dict` [ :obj:`~str`, :obj:`~str` ] - The object to send to Discord representing device info when - IDENTIFYing with the gateway. + This is the object to send to Discord representing device info when + IDENTIFYing with the gateway in the format `typing.Dict` [ `str`, `str` ] """ return { "$os": self.system_type, diff --git a/hikari/permissions.py b/hikari/permissions.py index 340b2ffab6..8d45245d3c 100644 --- a/hikari/permissions.py +++ b/hikari/permissions.py @@ -27,9 +27,7 @@ class Permission(enum.IntFlag): """Represents the permissions available in a given channel or guild. This is an int-flag enum. This means that you can **combine multiple - permissions together** into one value using the bitwise-OR operator (``|``). - - .. code-block:: python + permissions together** into one value using the bitwise-OR operator (`|`). my_perms = Permission.MANAGE_CHANNELS | Permission.MANAGE_GUILD @@ -41,10 +39,8 @@ class Permission(enum.IntFlag): ) You can **check if a permission is present** in a set of combined - permissions by using the bitwise-AND operator (``&``). This will return - the int-value of the permission if it is present, or ``0`` if not present. - - .. code-block:: python + permissions by using the bitwise-AND operator (`&`). This will return + the int-value of the permission if it is present, or `0` if not present. my_perms = Permission.MANAGE_CHANNELS | Permission.MANAGE_GUILD @@ -66,10 +62,8 @@ class Permission(enum.IntFlag): print("I don't have the permission to manage channels!") If you need to **check that a permission is not present**, you can use the - bitwise-XOR operator (``^``) to check. If the permission is not present, it - will return a non-zero value, otherwise if it is present, it will return ``0``. - - .. code-block:: python + bitwise-XOR operator (`^`) to check. If the permission is not present, it + will return a non-zero value, otherwise if it is present, it will return `0`. my_perms = Permission.MANAGE_CHANNELS | Permission.MANAGE_GUILD @@ -77,106 +71,106 @@ class Permission(enum.IntFlag): print("Please give me the MANAGE_CHANNELS permission!") Lastly, if you need all the permissions set except the permission you want, - you can use the inversion operator (``~``) to do that. - - .. code-block:: python + you can use the inversion operator (`~`) to do that. # All permissions except ADMINISTRATOR. my_perms = ~Permission.ADMINISTRATOR """ - #: Empty permission. NONE = 0 + """Empty permission.""" - #: Allows creation of instant invites. CREATE_INSTANT_INVITE = 1 << 0 + """Allows creation of instant invites.""" - #: Allows kicking members KICK_MEMBERS = 1 << 1 + """Allows kicking members""" - #: Allows banning members. BAN_MEMBERS = 1 << 2 + """Allows banning members.""" - #: Allows all permissions and bypasses channel permission overwrites. ADMINISTRATOR = 1 << 3 + """Allows all permissions and bypasses channel permission overwrites.""" - #: Allows management and editing of channels. MANAGE_CHANNELS = 1 << 4 + """Allows management and editing of channels.""" - #: Allows management and editing of the guild. MANAGE_GUILD = 1 << 5 + """Allows management and editing of the guild.""" - #: Allows for the addition of reactions to messages. ADD_REACTIONS = 1 << 6 + """Allows for the addition of reactions to messages.""" - #: Allows for viewing of audit logs. VIEW_AUDIT_LOG = 1 << 7 + """Allows for viewing of audit logs.""" - #: Allows for using priority speaker in a voice channel. PRIORITY_SPEAKER = 1 << 8 + """Allows for using priority speaker in a voice channel.""" - #: Allows the user to go live. STREAM = 1 << 9 + """Allows the user to go live.""" - #: Allows guild members to view a channel, which includes reading messages in text channels. VIEW_CHANNEL = 1 << 10 + """Allows guild members to view a channel, which includes reading messages in text channels.""" - #: Allows for sending messages in a channel. SEND_MESSAGES = 1 << 11 + """Allows for sending messages in a channel.""" - #: Allows for sending of ``/tts`` messages. SEND_TTS_MESSAGES = 1 << 12 + """Allows for sending of `/tts` messages.""" - #: Allows for deletion of other users messages. MANAGE_MESSAGES = 1 << 13 + """Allows for deletion of other users messages.""" - #: Links sent by users with this permission will be auto-embedded. EMBED_LINKS = 1 << 14 + """Links sent by users with this permission will be auto-embedded.""" - #: Allows for uploading images and files ATTACH_FILES = 1 << 15 + """Allows for uploading images and files.""" - #: Allows for reading of message history. READ_MESSAGE_HISTORY = 1 << 16 + """Allows for reading of message history.""" - #: Allows for using the ``@everyone`` tag to notify all users in a channel, and the - #: ``@here`` tag to notify all online users in a channel, and the ``@role`` tag (even - #: if the role is not mentionable) to notify all users with that role in a channel. MENTION_EVERYONE = 1 << 17 + """Allows for using the `@everyone` tag to notify all users in a channel, + and the `@here` tag to notify all online users in a channel, and the + `@role` tag (even if the role is not mentionable) to notify all users with + that role in a channel. + """ - #: Allows the usage of custom emojis from other servers. USE_EXTERNAL_EMOJIS = 1 << 18 + """Allows the usage of custom emojis from other servers.""" - #: Allows for joining of a voice channel. CONNECT = 1 << 20 + """Allows for joining of a voice channel.""" - #: Allows for speaking in a voice channel. SPEAK = 1 << 21 + """Allows for speaking in a voice channel.""" - #: Allows for muting members in a voice channel. MUTE_MEMBERS = 1 << 22 + """Allows for muting members in a voice channel.""" - #: Allows for deafening of members in a voice channel. DEAFEN_MEMBERS = 1 << 23 + """Allows for deafening of members in a voice channel.""" - #: Allows for moving of members between voice channels. MOVE_MEMBERS = 1 << 24 + """Allows for moving of members between voice channels.""" - #: Allows for using voice-activity-detection in a voice channel. USE_VAD = 1 << 25 + """Allows for using voice-activity-detection in a voice channel.""" - #: Allows for modification of own nickname. CHANGE_NICKNAME = 1 << 26 + """Allows for modification of own nickname.""" - #: Allows for modification of other users nicknames. MANAGE_NICKNAMES = 1 << 27 + """Allows for modification of other users nicknames.""" - #: Allows management and editing of roles. MANAGE_ROLES = 1 << 28 + """Allows management and editing of roles.""" - #: Allows management and editing of webhooks. MANAGE_WEBHOOKS = 1 << 29 + """Allows management and editing of webhooks.""" - #: Allows management and editing of emojis. MANAGE_EMOJIS = 1 << 30 + """Allows management and editing of emojis.""" diff --git a/hikari/snowflakes.py b/hikari/snowflakes.py index b124e3160e..dd5ae4b5df 100644 --- a/hikari/snowflakes.py +++ b/hikari/snowflakes.py @@ -39,15 +39,13 @@ class Snowflake(bases.HikariEntity, typing.SupportsInt): """A concrete representation of a unique identifier for an object on Discord. - This object can be treated as a regular :obj:`~int` for most purposes. + This object can be treated as a regular `int` for most purposes. """ __slots__ = ("_value",) - #: The integer value of this ID. - #: - #: :type: :obj:`~int` _value: int + """The integer value of this ID.""" # noinspection PyMissingConstructor def __init__(self, value: typing.Union[int, str]) -> None: # pylint:disable=super-init-not-called @@ -98,7 +96,7 @@ def serialize(self) -> str: @classmethod def deserialize(cls, value: str) -> "Snowflake": - """Take a :obj:`~str` ID and convert it into a Snowflake object.""" + """Take a `str` ID and convert it into a Snowflake object.""" return cls(value) @classmethod @@ -117,10 +115,8 @@ def from_timestamp(cls, timestamp: float) -> "Snowflake": class UniqueEntity(bases.HikariEntity, typing.SupportsInt): """An entity that has an integer ID of some sort.""" - #: The ID of this entity. - #: - #: :type: :obj:`~Snowflake` id: Snowflake = marshaller.attrib(hash=True, eq=True, repr=True, deserializer=Snowflake, serializer=str) + """The ID of this entity.""" def __int__(self): return int(self.id) diff --git a/hikari/state/__init__.py b/hikari/state/__init__.py index 608c413269..937c2e7faa 100644 --- a/hikari/state/__init__.py +++ b/hikari/state/__init__.py @@ -23,13 +23,5 @@ several key components to be implemented separately, in case you have a specific use case you want to provide (such as placing stuff on a message queue if you distribute your bot). - -The overall structure is as follows: - -.. inheritance-diagram:: - hikari.state.event_dispatchers - hikari.state.raw_event_consumers - hikari.state.event_managers - hikari.state.stateless_event_managers """ __all__ = [] diff --git a/hikari/state/consumers.py b/hikari/state/consumers.py index 184f458643..7c5f0e7636 100644 --- a/hikari/state/consumers.py +++ b/hikari/state/consumers.py @@ -49,10 +49,10 @@ def process_raw_event( Parameters ---------- - shard_client_obj : :obj:`~hikari.clients.shard_clients.ShardClient` + shard_client_obj : hikari.clients.shards.ShardClient The client for the shard that received the event. - name : :obj:`~str` + name : str The raw event name. - payload : :obj:`~typing.Any` + payload : typing.Any The raw event payload. Will be a JSON-compatible type. """ diff --git a/hikari/state/dispatchers.py b/hikari/state/dispatchers.py index e4984e8693..13cb9a494d 100644 --- a/hikari/state/dispatchers.py +++ b/hikari/state/dispatchers.py @@ -77,15 +77,15 @@ def add_listener(self, event_type: typing.Type[EventT], callback: EventCallbackT Parameters ---------- - event_type : :obj:`~typing.Type` [ :obj:`~hikari.events.HikariEvent` ] + event_type : typing.Type [ hikari.events.HikariEvent ] The event to register to. - callback : ``async def callback(event: HikariEvent) -> ...`` + callback : `async def callback(event: HikariEvent) -> ...` The event callback to invoke when this event is fired. Raises ------ - :obj:`~TypeError` - If ``coroutine_function`` is not a coroutine. + TypeError + If `coroutine_function` is not a coroutine. """ @abc.abstractmethod @@ -96,9 +96,9 @@ def remove_listener(self, event_type: typing.Type[EventT], callback: EventCallba Parameters ---------- - event_type : :obj:`~typing.Type` [ :obj:`~hikari.events.HikariEvent` ] + event_type : typing.Type [ hikari.events.HikariEvent ] The type of event to remove the callback from. - callback : ``async def callback(event: HikariEvent) -> ...`` + callback : `async def callback(event: HikariEvent) -> ...` The event callback to invoke when this event is fired. """ @@ -110,16 +110,16 @@ def wait_for( Parameters ---------- - event_type : :obj:`~typing.Type` [ :obj:`~hikari.events.HikariEvent` ] + event_type : typing.Type [ hikari.events.HikariEvent ] The name of the event to wait for. - timeout : :obj:`~float`, optional + timeout : float, optional The timeout to wait for before cancelling and raising an - :obj:`~asyncio.TimeoutError` instead. If this is :obj:`~None`, this - will wait forever. Care must be taken if you use :obj:`~None` as this + `asyncio.TimeoutError` instead. If this is `None`, this + will wait forever. Care must be taken if you use `None` as this may leak memory if you do this from an event listener that gets repeatedly called. If you want to do this, you should consider using an event listener instead of this function. - predicate : ``def predicate(event) -> bool`` or ``async def predicate(event) -> bool`` + predicate : `def predicate(event) -> bool` or `async def predicate(event) -> bool` A function that takes the arguments for the event and returns True if it is a match, or False if it should be ignored. This can be a coroutine function that returns a boolean, or a @@ -127,17 +127,16 @@ def wait_for( Returns ------- - :obj:`~asyncio.Future`: + asyncio.Future: A future to await. When the given event is matched, this will be completed with the corresponding event body. If the predicate throws an exception, or the timeout is reached, then this will be set as an exception on the returned future. - Notes - ----- - The event type is not expected to be considered in a polymorphic - lookup, but can be implemented this way optionally if documented. + !!! note + The event type is not expected to be considered in a polymorphic + lookup, but can be implemented this way optionally if documented. """ @typing.overload @@ -146,10 +145,8 @@ def on(self) -> typing.Callable[[EventCallbackT], EventCallbackT]: This considers the type hint on the signature to get the event type. - Example - ------- - .. code-block:: python - + Examples + -------- @bot.on() async def on_message(event: hikari.MessageCreatedEvent): print(event.content) @@ -161,21 +158,19 @@ def on(self, event_type: typing.Type[EventCallbackT]) -> typing.Callable[[EventC This considers the type given to the decorator. - Example - ------- - .. code-block:: python - + Examples + -------- @bot.on(hikari.MessageCreatedEvent) async def on_message(event): print(event.content) """ def on(self, event_type=None): - """Return a decorator equivalent to invoking :meth:`add_listener`. + """Return a decorator equivalent to invoking `EventDispatcher.add_listener`. Parameters ---------- - event_type : :obj:`~typing.Type` [ :obj:`~hikari.events.HikariEvent` ], optional + event_type : typing.Type [ hikari.events.HikariEvent ], optional The event type to register the produced decorator to. If this is not specified, then the given function is used instead and the type hint of the first argument is considered. If no type hint is present @@ -183,8 +178,6 @@ def on(self, event_type=None): Examples -------- - .. code-block:: python - # Type-hinted format. @bot.on() async def on_message(event: hikari.MessageCreatedEvent): @@ -197,7 +190,7 @@ async def on_message(event): Returns ------- - ``decorator(T) -> T`` + decorator(T) -> T A decorator for a coroutine function that registers the given event. """ @@ -242,12 +235,12 @@ def dispatch_event(self, event: events.HikariEvent) -> more_typing.Future[typing Parameters ---------- - event : :obj:`~hikari.events.HikariEvent` + event : hikari.events.HikariEvent The event to dispatch. Returns ------- - :obj:`~asyncio.Future`: + asyncio.Future: a future that can be optionally awaited if you need to wait for all listener callbacks and waiters to be processed. If this is not awaited, the invocation is invoked soon on the current event loop. @@ -273,15 +266,13 @@ class IntentAwareEventDispatcherImpl(EventDispatcher): Parameters ---------- - enabled_intents : :obj:`~hikari.intents.Intent`, optional - The intents that are enabled for the application. If ``None``, then no + enabled_intents : hikari.intents.Intent, optional + The intents that are enabled for the application. If `None`, then no intent checks are performed when subscribing a new event. """ - #: The logger used to write log messages. - #: - #: :type: :obj:`~logging.Logger` logger: logging.Logger + """The logger used to write log messages.""" def __init__(self, enabled_intents: typing.Optional[intents.Intent]) -> None: self._enabled_intents = enabled_intents @@ -304,15 +295,15 @@ def add_listener(self, event_type: typing.Type[events.HikariEvent], callback: Ev Parameters ---------- - event_type : :obj:`~typing.Type` [ :obj:`~hikari.events.HikariEvent` ] + event_type : typing.Type [ hikari.events.HikariEvent ] The event to register to. - callback : ``async def callback(event: HikariEvent) -> ...`` + callback : `async def callback(event: HikariEvent) -> ...` The event callback to invoke when this event is fired. Raises ------ - :obj:`~TypeError` - If ``coroutine_function`` is not a coroutine. + TypeError + If `coroutine_function` is not a coroutine. Note ---- @@ -356,9 +347,9 @@ def remove_listener(self, event_type: typing.Type[EventT], callback: EventCallba Parameters ---------- - event_type : :obj:`~typing.Type` [ :obj:`~hikari.events.HikariEvent` ] + event_type : typing.Type [ hikari.events.HikariEvent ] The type of event to remove the callback from. - callback : ``async def callback(event: HikariEvent) -> ...`` + callback : `async def callback(event: HikariEvent) -> ...` The event callback to remove. """ if event_type in self._listeners and callback in self._listeners[event_type]: @@ -374,12 +365,12 @@ def dispatch_event(self, event: events.HikariEvent): Parameters ---------- - event : :obj:`~hikari.events.HikariEvent` + event : hikari.events.HikariEvent The event to dispatch. Returns ------- - :obj:`~asyncio.Future` + asyncio.Future This may be a gathering future of the callbacks to invoke, or it may be a completed future object. Regardless, this result will be scheduled on the event loop automatically, and does not need to be @@ -452,17 +443,16 @@ def handle_exception( This allows users to override this with a custom implementation if desired. This implementation will check to see if the event that triggered the - exception is an :obj:`~hikari.events.ExceptionEvent`. If this - exception was caused by the :obj:`~hikari.events.ExceptionEvent`, - then nothing is dispatched (thus preventing an exception handler recursively - re-triggering itself). Otherwise, an :obj:`~hikari.events.ExceptionEvent` - is dispatched. + exception is an `hikari.events.ExceptionEvent`. If this exception was + caused by the `hikari.events.ExceptionEvent`, then nothing is dispatched + (thus preventing an exception handler recursively re-triggering itself). + Otherwise, an `hikari.events.ExceptionEvent` is dispatched. Parameters ---------- - exception: :obj:`~Exception` + exception : Exception The exception that triggered this call. - event: :obj:`~hikari.events.HikariEvent` + event : hikari.events.HikariEvent The event that was being dispatched. callback The callback that threw the exception. @@ -495,35 +485,34 @@ def wait_for( Parameters ---------- - event_type : :obj:`~typing.Type` [ :obj:`~hikari.events.HikariEvent` ] + event_type : typing.Type [ hikari.events.HikariEvent ] The name of the event to wait for. - timeout : :obj:`~float`, optional + timeout : float, optional The timeout to wait for before cancelling and raising an - :obj:`~asyncio.TimeoutError` instead. If this is `None`, this will - wait forever. Care must be taken if you use `None` as this may - leak memory if you do this from an event listener that gets - repeatedly called. If you want to do this, you should consider - using an event listener instead of this function. - predicate : ``def predicate(event) -> bool`` or ``async def predicate(event) -> bool`` - A function that takes the arguments for the event and returns True - if it is a match, or False if it should be ignored. + `asyncio.TimeoutError` instead. If this is `None`, this will wait + forever. Care must be taken if you use `None` as this may leak + memory if you do this from an event listener that gets repeatedly + called. If you want to do this, you should consider using an event + listener instead of this function. + predicate : `def predicate(event) -> bool` or `async def predicate(event) -> bool` + A function that takes the arguments for the event and returns `True` + if it is a match, or `False` if it should be ignored. This can be a coroutine function that returns a boolean, or a regular function. Returns ------- - :obj:`~asyncio.Future` + asyncio.Future A future that when awaited will provide a the arguments passed to the first matching event. If no arguments are passed to the event, then `None` is the result. If one argument is passed to the event, then that argument is the result, otherwise a tuple of arguments is the result instead. - Notes - ----- - Awaiting this result will raise an :obj:`~asyncio.TimeoutError` if the - timeout is hit and no match is found. If the predicate throws any - exception, this is raised immediately. + !!! note + Awaiting this result will raise an `asyncio.TimeoutError` if the + timeout is hit and no match is found. If the predicate throws any + exception, this is raised immediately. """ future = asyncio.get_event_loop().create_future() if event_type not in self._waiters: diff --git a/hikari/state/event_managers.py b/hikari/state/event_managers.py index 8177510d59..7ae5c4166d 100644 --- a/hikari/state/event_managers.py +++ b/hikari/state/event_managers.py @@ -44,7 +44,7 @@ def raw_event_mapper(name: str) -> typing.Callable[[EventConsumerT], EventConsum Returns ------- - ``decorator(T) -> T`` + decorator(T) -> T A decorator for a method. """ @@ -74,8 +74,8 @@ class EventManager(typing.Generic[EventDispatcherT], consumers.RawEventConsumer) """Abstract definition of the components for an event system for a bot. The class itself inherits from - :obj:`~hikari.state.raw_event_consumers.RawEventConsumer` (which allows - it to provide the ability to transform a raw payload into an event object). + `hikari.state.consumers.RawEventConsumer` (which allows it to provide the + ability to transform a raw payload into an event object). This is designed as a basis to enable transformation of raw incoming events from the websocket into more usable native Python objects, and to then @@ -84,53 +84,54 @@ class EventManager(typing.Generic[EventDispatcherT], consumers.RawEventConsumer) Parameters ---------- - event_dispatcher_impl: :obj:`~hikari.state.event_dispatchers.EventDispatcher` + event_dispatcher_impl: hikari.state.dispatchers.EventDispatcher An implementation of event dispatcher that will store individual events and manage dispatching them after this object creates them. - Notes - ----- - This object will detect internal event mapper functions by looking for - coroutine functions wrapped with :obj:`~raw_event_mapper`. + !!! note + This object will detect internal event mapper functions by looking for + coroutine functions wrapped with `raw_event_mapper`. - These methods are expected to have the following parameters: + These methods are expected to have the following parameters: - shard_obj: :obj:`~hikari.clients.shard_clients.ShardClient` - The shard client that emitted the event. - payload: :obj:`~typing.Any` - The received payload. This is expected to be a JSON-compatible type. + * shard_obj : `hikari.clients.shards.ShardClient` - For example, if you want to provide an implementation that can consume - and handle ``MESSAGE_CREATE`` events, you can do the following. + The shard client that emitted the event. - .. code-block:: python + * payload : `typing.Any` - class MyMappingEventConsumer(MappingEventConsumer): - @event_mapper("MESSAGE_CREATE") - def _process_message_create(self, shard, payload) -> MessageCreateEvent: - return MessageCreateEvent.deserialize(payload) + The received payload. This is expected to be a JSON-compatible type. - The decorator can be stacked if you wish to provide one mapper + For example, if you want to provide an implementation that can consume + and handle `MESSAGE_CREATE` events, you can do the following. - ... it is pretty simple. This is exposed in this way to enable you to write - code that may use a distributed system instead of a single-process bot. + class MyMappingEventConsumer(MappingEventConsumer): + @event_mapper("MESSAGE_CREATE") + def _process_message_create(self, shard, payload) -> MessageCreateEvent: + return MessageCreateEvent.deserialize(payload) - Writing to a message queue is pretty simple using this mechanism, as you can - choose when and how to place the event on a queue to be consumed by other - application components. + The decorator can be stacked if you wish to provide one mapper - For the sake of simplicity, Hikari only provides implementations for single - process bots, since most of what you will need will be fairly bespoke if you - want to implement anything more complicated; regardless, the tools are here - for you to use as you see fit. + ... it is pretty simple. This is exposed in this way to enable you to + write code that may use a distributed system instead of a single-process + bot. - Warnings - -------- - This class provides the scaffold for making an event consumer, but does not - physically implement the logic to deserialize and process specific events. + Writing to a message queue is pretty simple using this mechanism, as you + can choose when and how to place the event on a queue to be consumed by + other application components. - To provide this, use one of the provided implementations of this class, or - create your own as needed. + For the sake of simplicity, Hikari only provides implementations for + single process bots, since most of what you will need will be fairly + bespoke if you want to implement anything more complicated; regardless, + the tools are here for you to use as you see fit. + + !!! warning + This class provides the scaffold for making an event consumer, but doe + not physically implement the logic to deserialize and process specific + events. + + To provide this, use one of the provided implementations of this class, + or create your own as needed. """ def __init__(self, event_dispatcher_impl: EventDispatcherT) -> None: @@ -154,11 +155,11 @@ def process_raw_event( Parameters ---------- - shard_client_obj: :obj:`~hikari.clients.shard_clients.ShardClient` + shard_client_obj : hikari.clients.shards.ShardClient The shard that triggered this event. - name : :obj:`~str` + name : str The raw event name. - payload : :obj:`~dict` + payload : dict The payload that was sent. """ try: diff --git a/hikari/state/stateless.py b/hikari/state/stateless.py index 0731114793..7bac8536d1 100644 --- a/hikari/state/stateless.py +++ b/hikari/state/stateless.py @@ -34,181 +34,181 @@ class StatelessEventManagerImpl(event_managers.EventManager[dispatchers.EventDis """ @event_managers.raw_event_mapper("CONNECTED") - def on_connect(self, shard, _): + def on_connect(self, shard, _) -> None: """Handle CONNECTED events.""" event = events.ConnectedEvent(shard=shard) self.event_dispatcher.dispatch_event(event) @event_managers.raw_event_mapper("DISCONNECTED") - def on_disconnect(self, shard, _): + def on_disconnect(self, shard, _) -> None: """Handle DISCONNECTED events.""" event = events.DisconnectedEvent(shard=shard) self.event_dispatcher.dispatch_event(event) @event_managers.raw_event_mapper("RESUME") - def on_resume(self, shard, _): + def on_resume(self, shard, _) -> None: """Handle RESUME events.""" event = events.ResumedEvent(shard=shard) self.event_dispatcher.dispatch_event(event) @event_managers.raw_event_mapper("READY") - def on_ready(self, _, payload): + def on_ready(self, _, payload) -> None: """Handle READY events.""" event = events.ReadyEvent.deserialize(payload) self.event_dispatcher.dispatch_event(event) @event_managers.raw_event_mapper("CHANNEL_CREATE") - def on_channel_create(self, _, payload): + def on_channel_create(self, _, payload) -> None: """Handle CHANNEL_CREATE events.""" event = events.ChannelCreateEvent.deserialize(payload) self.event_dispatcher.dispatch_event(event) @event_managers.raw_event_mapper("CHANNEL_UPDATE") - def on_channel_update(self, _, payload): + def on_channel_update(self, _, payload) -> None: """Handle CHANNEL_UPDATE events.""" event = events.ChannelUpdateEvent.deserialize(payload) self.event_dispatcher.dispatch_event(event) @event_managers.raw_event_mapper("CHANNEL_DELETE") - def on_channel_delete(self, _, payload): + def on_channel_delete(self, _, payload) -> None: """Handle CHANNEL_DELETE events.""" event = events.ChannelDeleteEvent.deserialize(payload) self.event_dispatcher.dispatch_event(event) @event_managers.raw_event_mapper("CHANNEL_PIN_UPDATE") - def on_channel_pin_update(self, _, payload): + def on_channel_pin_update(self, _, payload) -> None: """Handle CHANNEL_PIN_UPDATE events.""" event = events.ChannelPinUpdateEvent.deserialize(payload) self.event_dispatcher.dispatch_event(event) @event_managers.raw_event_mapper("GUILD_CREATE") - def on_guild_create(self, _, payload): + def on_guild_create(self, _, payload) -> None: """Handle GUILD_CREATE events.""" event = events.GuildCreateEvent.deserialize(payload) self.event_dispatcher.dispatch_event(event) @event_managers.raw_event_mapper("GUILD_UPDATE") - def on_guild_update(self, _, payload): + def on_guild_update(self, _, payload) -> None: """Handle GUILD_UPDATE events.""" event = events.GuildUpdateEvent.deserialize(payload) self.event_dispatcher.dispatch_event(event) @event_managers.raw_event_mapper("GUILD_LEAVE") - def on_guild_leave(self, _, payload): + def on_guild_leave(self, _, payload) -> None: """Handle GUILD_LEAVE events.""" event = events.GuildLeaveEvent.deserialize(payload) self.event_dispatcher.dispatch_event(event) @event_managers.raw_event_mapper("GUILD_UNAVAILABLE") - def on_guild_unavailable(self, _, payload): + def on_guild_unavailable(self, _, payload) -> None: """Handle GUILD_UNAVAILABLE events.""" event = events.GuildUnavailableEvent.deserialize(payload) self.event_dispatcher.dispatch_event(event) @event_managers.raw_event_mapper("GUILD_BAN_ADD") - def on_guild_ban_add(self, _, payload): + def on_guild_ban_add(self, _, payload) -> None: """Handle GUILD_BAN_ADD events.""" event = events.GuildBanAddEvent.deserialize(payload) self.event_dispatcher.dispatch_event(event) @event_managers.raw_event_mapper("GUILD_BAN_REMOVE") - def on_guild_ban_remove(self, _, payload): + def on_guild_ban_remove(self, _, payload) -> None: """Handle GUILD_BAN_REMOVE events.""" event = events.GuildBanRemoveEvent.deserialize(payload) self.event_dispatcher.dispatch_event(event) @event_managers.raw_event_mapper("GUILD_EMOJIS_UPDATE") - def on_guild_emojis_update(self, _, payload): + def on_guild_emojis_update(self, _, payload) -> None: """Handle GUILD_EMOJIS_UPDATE events.""" event = events.GuildEmojisUpdateEvent.deserialize(payload) self.event_dispatcher.dispatch_event(event) @event_managers.raw_event_mapper("GUILD_INTEGRATIONS_UPDATE") - def on_guild_integrations_update(self, _, payload): + def on_guild_integrations_update(self, _, payload) -> None: """Handle GUILD_INTEGRATIONS_UPDATE events.""" event = events.GuildIntegrationsUpdateEvent.deserialize(payload) self.event_dispatcher.dispatch_event(event) @event_managers.raw_event_mapper("GUILD_MEMBER_ADD") - def on_guild_member_add(self, _, payload): + def on_guild_member_add(self, _, payload) -> None: """Handle GUILD_MEMBER_ADD events.""" event = events.GuildMemberAddEvent.deserialize(payload) self.event_dispatcher.dispatch_event(event) @event_managers.raw_event_mapper("GUILD_MEMBER_UPDATE") - def on_guild_member_update(self, _, payload): + def on_guild_member_update(self, _, payload) -> None: """Handle GUILD_MEMBER_UPDATE events.""" event = events.GuildMemberUpdateEvent.deserialize(payload) self.event_dispatcher.dispatch_event(event) @event_managers.raw_event_mapper("GUILD_MEMBER_REMOVE") - def on_guild_member_remove(self, _, payload): + def on_guild_member_remove(self, _, payload) -> None: """Handle GUILD_MEMBER_REMOVE events.""" event = events.GuildMemberRemoveEvent.deserialize(payload) self.event_dispatcher.dispatch_event(event) @event_managers.raw_event_mapper("GUILD_ROLE_CREATE") - def on_guild_role_create(self, _, payload): + def on_guild_role_create(self, _, payload) -> None: """Handle GUILD_ROLE_CREATE events.""" event = events.GuildRoleCreateEvent.deserialize(payload) self.event_dispatcher.dispatch_event(event) @event_managers.raw_event_mapper("GUILD_ROLE_UPDATE") - def on_guild_role_update(self, _, payload): + def on_guild_role_update(self, _, payload) -> None: """Handle GUILD_ROLE_UPDATE events.""" event = events.GuildRoleUpdateEvent.deserialize(payload) self.event_dispatcher.dispatch_event(event) @event_managers.raw_event_mapper("GUILD_ROLE_DELETE") - def on_guild_role_delete(self, _, payload): + def on_guild_role_delete(self, _, payload) -> None: """Handle GUILD_ROLE_DELETE events.""" event = events.GuildRoleDeleteEvent.deserialize(payload) self.event_dispatcher.dispatch_event(event) @event_managers.raw_event_mapper("INVITE_CREATE") - def on_invite_create(self, _, payload): + def on_invite_create(self, _, payload) -> None: """Handle INVITE_CREATE events.""" event = events.InviteCreateEvent.deserialize(payload) self.event_dispatcher.dispatch_event(event) @event_managers.raw_event_mapper("INVITE_DELETE") - def on_invite_delete(self, _, payload): + def on_invite_delete(self, _, payload) -> None: """Handle INVITE_DELETE events.""" event = events.InviteDeleteEvent.deserialize(payload) self.event_dispatcher.dispatch_event(event) @event_managers.raw_event_mapper("MESSAGE_CREATE") - def on_message_create(self, _, payload): + def on_message_create(self, _, payload) -> None: """Handle MESSAGE_CREATE events.""" event = events.MessageCreateEvent.deserialize(payload) self.event_dispatcher.dispatch_event(event) @event_managers.raw_event_mapper("MESSAGE_UPDATE") - def on_message_update(self, _, payload): + def on_message_update(self, _, payload) -> None: """Handle MESSAGE_UPDATE events.""" event = events.MessageUpdateEvent.deserialize(payload) self.event_dispatcher.dispatch_event(event) @event_managers.raw_event_mapper("MESSAGE_DELETE") - def on_message_delete(self, _, payload): + def on_message_delete(self, _, payload) -> None: """Handle MESSAGE_DELETE events.""" event = events.MessageDeleteEvent.deserialize(payload) self.event_dispatcher.dispatch_event(event) @event_managers.raw_event_mapper("MESSAGE_DELETE_BULK") - def on_message_delete_bulk(self, _, payload): + def on_message_delete_bulk(self, _, payload) -> None: """Handle MESSAGE_DELETE_BULK events.""" event = events.MessageDeleteBulkEvent.deserialize(payload) self.event_dispatcher.dispatch_event(event) @event_managers.raw_event_mapper("MESSAGE_REACTION_ADD") - def on_message_reaction_add(self, _, payload): + def on_message_reaction_add(self, _, payload) -> None: """Handle MESSAGE_REACTION_ADD events.""" event = events.MessageReactionAddEvent.deserialize(payload) self.event_dispatcher.dispatch_event(event) @event_managers.raw_event_mapper("MESSAGE_REACTION_REMOVE") - def on_message_reaction_remove(self, _, payload): + def on_message_reaction_remove(self, _, payload) -> None: """Handle MESSAGE_REACTION_REMOVE events.""" payload["emoji"].setdefault("animated", None) @@ -216,7 +216,7 @@ def on_message_reaction_remove(self, _, payload): self.event_dispatcher.dispatch_event(event) @event_managers.raw_event_mapper("MESSAGE_REACTION_REMOVE_EMOJI") - def on_message_reaction_remove_emoji(self, _, payload): + def on_message_reaction_remove_emoji(self, _, payload) -> None: """Handle MESSAGE_REACTION_REMOVE_EMOJI events.""" payload["emoji"].setdefault("animated", None) @@ -224,37 +224,37 @@ def on_message_reaction_remove_emoji(self, _, payload): self.event_dispatcher.dispatch_event(event) @event_managers.raw_event_mapper("PRESENCE_UPDATE") - def on_presence_update(self, _, payload): + def on_presence_update(self, _, payload) -> None: """Handle PRESENCE_UPDATE events.""" event = events.PresenceUpdateEvent.deserialize(payload) self.event_dispatcher.dispatch_event(event) @event_managers.raw_event_mapper("TYPING_START") - def on_typing_start(self, _, payload): + def on_typing_start(self, _, payload) -> None: """Handle TYPING_START events.""" event = events.TypingStartEvent.deserialize(payload) self.event_dispatcher.dispatch_event(event) @event_managers.raw_event_mapper("USER_UPDATE") - def on_user_update(self, _, payload): + def on_user_update(self, _, payload) -> None: """Handle USER_UPDATE events.""" event = events.UserUpdateEvent.deserialize(payload) self.event_dispatcher.dispatch_event(event) @event_managers.raw_event_mapper("VOICE_STATE_UPDATE") - def on_voice_state_update(self, _, payload): + def on_voice_state_update(self, _, payload) -> None: """Handle VOICE_STATE_UPDATE events.""" event = events.VoiceStateUpdateEvent.deserialize(payload) self.event_dispatcher.dispatch_event(event) @event_managers.raw_event_mapper("VOICE_SERVER_UPDATE") - def on_voice_server_update(self, _, payload): + def on_voice_server_update(self, _, payload) -> None: """Handle VOICE_SERVER_UPDATE events.""" event = events.VoiceStateUpdateEvent.deserialize(payload) self.event_dispatcher.dispatch_event(event) @event_managers.raw_event_mapper("WEBHOOK_UPDATE") - def on_webhook_update(self, _, payload): + def on_webhook_update(self, _, payload) -> None: """Handle WEBHOOK_UPDATE events.""" event = events.WebhookUpdateEvent.deserialize(payload) self.event_dispatcher.dispatch_event(event) diff --git a/hikari/unset.py b/hikari/unset.py index 36af4e339e..02f10b9671 100644 --- a/hikari/unset.py +++ b/hikari/unset.py @@ -50,20 +50,20 @@ def ___defer___(self, *_, **__) -> typing.NoReturn: T = typing.TypeVar("T") MayBeUnset = typing.Union[T, Unset] -#: A global instance of :class:`~Unset`. UNSET: typing.Final[Unset] = Unset() +"""A global instance of `Unset`.""" @typing.overload def is_unset(obj: UNSET) -> typing.Literal[True]: - """Return ``True`` always.""" + """Return `True` always.""" @typing.overload def is_unset(obj: typing.Any) -> typing.Literal[False]: - """Return ``False`` always.""" + """Return `False` always.""" def is_unset(obj): - """Return ``True`` if the object is an :obj:`~Unset` value.""" + """Return `True` if the object is an `Unset` value.""" return isinstance(obj, Unset) diff --git a/hikari/users.py b/hikari/users.py index 209f86a059..525fc788af 100644 --- a/hikari/users.py +++ b/hikari/users.py @@ -33,61 +33,61 @@ class UserFlag(enum.IntFlag): """The known user flags that represent account badges.""" - #: None NONE = 0 + """None""" - #: Discord Empoloyee DISCORD_EMPLOYEE = 1 << 0 + """Discord Employee""" - #: Discord Partner DISCORD_PARTNER = 1 << 1 + """Discord Partner""" - #: HypeSquad Events HYPESQUAD_EVENTS = 1 << 2 + """HypeSquad Events""" - #: Bug Hunter Level 1 BUG_HUNTER_LEVEL_1 = 1 << 3 + """Bug Hunter Level 1""" - #: House of Bravery HOUSE_BRAVERY = 1 << 6 + """House of Bravery""" - #: House of Brilliance HOUSE_BRILLIANCE = 1 << 7 + """House of Brilliance""" - #: House of Balance HOUSE_BALANCE = 1 << 8 + """House of Balance""" - #: Early Supporter EARLY_SUPPORTER = 1 << 9 + """Early Supporter""" - #: Team user TEAM_USER = 1 << 10 + """Team user""" - #: System SYSTEM = 1 << 12 + """System""" - #: Bug Hunter Level 2 BUG_HUNTER_LEVEL_2 = 1 << 14 + """Bug Hunter Level 2""" - #: Verified Bot VERIFIED_BOT = 1 << 16 + """Verified Bot""" - #: Verified Bot Developer VERIFIED_BOT_DEVELOPER = 1 << 17 + """Verified Bot Developer""" @enum.unique class PremiumType(enum.IntEnum): """The types of Nitro.""" - #: No premium. NONE = 0 + """No premium.""" - #: Premium including basic perks like animated emojis and avatars. NITRO_CLASSIC = 1 + """Premium including basic perks like animated emojis and avatars.""" - #: Premium including all perks (e.g. 2 server boosts). NITRO = 2 + """Premium including all perks (e.g. 2 server boosts).""" @marshaller.marshallable() @@ -95,42 +95,29 @@ class PremiumType(enum.IntEnum): class User(bases.UniqueEntity, marshaller.Deserializable): """Represents a user.""" - #: This user's discriminator. - #: - #: :type: :obj:`~str` discriminator: str = marshaller.attrib(deserializer=str) + """This user's discriminator.""" - #: This user's username. - #: - #: :type: :obj:`~str` username: str = marshaller.attrib(deserializer=str) + """This user's username.""" - #: This user's avatar hash, if set. - #: - #: :type: :obj:`~str`, optional avatar_hash: typing.Optional[str] = marshaller.attrib(raw_name="avatar", deserializer=str, if_none=None) + """This user's avatar hash, if set.""" - #: Whether this user is a bot account. - #: - #: :type: :obj:`~bool` is_bot: bool = marshaller.attrib(raw_name="bot", deserializer=bool, if_undefined=False, default=False) + """Whether this user is a bot account.""" - #: Whether this user is a system account. - #: - #: :type: :obj:`~bool` is_system: bool = marshaller.attrib(raw_name="system", deserializer=bool, if_undefined=False, default=False) + """Whether this user is a system account.""" - #: The public flags for this user. - #: - #: Note - #: ---- - #: This will be :obj:`~None` if it's a webhook user. - #: - #: - #: :type: :obj:`~UserFlag`, optional flags: typing.Optional[UserFlag] = marshaller.attrib( raw_name="public_flags", deserializer=UserFlag, if_undefined=None, default=None ) + """The public flags for this user. + + !!! info + This will be `None` if it's a webhook user. + """ @property def avatar_url(self) -> str: @@ -142,25 +129,25 @@ def format_avatar_url(self, fmt: typing.Optional[str] = None, size: int = 4096) Parameters ---------- - fmt : :obj:`~str` - The format to use for this URL, defaults to ``png`` or ``gif``. - Supports ``png``, ``jpeg``, ``jpg``, ``webp`` and ``gif`` (when + fmt : str + The format to use for this URL, defaults to `png` or `gif`. + Supports `png`, `jpeg`, `jpg`, `webp` and `gif` (when animated). Will be ignored for default avatars which can only be - ``png``. - size : :obj:`~int` - The size to set for the URL, defaults to ``4096``. + `png`. + size : int + The size to set for the URL, defaults to `4096`. Can be any power of two between 16 and 4096. Will be ignored for default avatars. Returns ------- - :obj:`~str` + str The string URL. Raises ------ - :obj:`~ValueError` - If ``size`` is not a power of two or not between 16 and 4096. + ValueError + If `size` is not a power of two or not between 16 and 4096. """ if not self.avatar_hash: return urls.generate_cdn_url("embed/avatars", str(self.default_avatar), fmt="png", size=None) @@ -181,41 +168,35 @@ def default_avatar(self) -> int: class MyUser(User): """Represents a user with extended oauth2 information.""" - #: Whether the user's account has 2fa enabled. - #: - #: :type: :obj:`~bool` is_mfa_enabled: bool = marshaller.attrib(raw_name="mfa_enabled", deserializer=bool) + """Whether the user's account has 2fa enabled.""" - #: The user's set language. This is not provided by the ``READY`` event. - #: - #: :type: :obj:`~str`, optional locale: typing.Optional[str] = marshaller.attrib(deserializer=str, if_none=None, if_undefined=None, default=None) + """The user's set language. This is not provided by the `READY` event.""" - #: Whether the email for this user's account has been verified. - #: Will be :obj:`~None` if retrieved through the oauth2 flow without the - #: ``email`` scope. - #: - #: :type: :obj:`~bool`, optional is_verified: typing.Optional[bool] = marshaller.attrib( raw_name="verified", deserializer=bool, if_undefined=None, default=None ) + """Whether the email for this user's account has been verified. + + Will be `None` if retrieved through the oauth2 flow without the `email` + scope. + """ - #: The user's set email. - #: Will be :obj:`~None` if retrieved through the oauth2 flow without the - #: ``email`` scope and for bot users. - #: - #: :type: :obj:`~str`, optional email: typing.Optional[str] = marshaller.attrib(deserializer=str, if_undefined=None, if_none=None, default=None) + """The user's set email. + + Will be `None` if retrieved through the oauth2 flow without the `email` + scope and for bot users. + """ - #: This user account's flags. - #: - #: :type: :obj:`~UserFlag` flags: UserFlag = marshaller.attrib(deserializer=UserFlag) + """This user account's flags.""" - #: The type of Nitro Subscription this user account had. - #: This will always be :obj:`~None` for bots. - #: - #: :type: :obj:`~PremiumType`, optional premium_type: typing.Optional[PremiumType] = marshaller.attrib( deserializer=PremiumType, if_undefined=None, default=None ) + """The type of Nitro Subscription this user account had. + + This will always be `None` for bots. + """ diff --git a/hikari/voices.py b/hikari/voices.py index 1d50583388..78d689e158 100644 --- a/hikari/voices.py +++ b/hikari/voices.py @@ -33,68 +33,47 @@ class VoiceState(bases.HikariEntity, marshaller.Deserializable): """Represents a user's voice connection status.""" - #: The ID of the guild this voice state is in, if applicable. - #: - #: :type: :obj:`~hikari.bases.Snowflake`, optional guild_id: typing.Optional[bases.Snowflake] = marshaller.attrib( deserializer=bases.Snowflake.deserialize, if_undefined=None, default=None ) + """The ID of the guild this voice state is in, if applicable.""" - #: The ID of the channel this user is connected to, will be :obj:`~None` if - #: they are leaving voice. - #: - #: :type: :obj:`~hikari.bases.Snowflake`, optional channel_id: typing.Optional[bases.Snowflake] = marshaller.attrib( deserializer=bases.Snowflake.deserialize, if_none=None ) + """The ID of the channel this user is connected to. + + This will be `None` if they are leaving voice. + """ - #: The ID of the user this voice state is for. - #: - #: :type: :obj:`~hikari.bases.Snowflake` user_id: bases.Snowflake = marshaller.attrib(deserializer=bases.Snowflake.deserialize) + """The ID of the user this voice state is for.""" - #: The guild member this voice state is for if the voice state is in a - #: guild. - #: - #: :type: :obj:`~hikari.guilds.GuildMember`, optional member: typing.Optional[guilds.GuildMember] = marshaller.attrib( deserializer=guilds.GuildMember.deserialize, if_undefined=None, default=None ) + """The guild member this voice state is for if the voice state is in a guild.""" - #: The ID of this voice state's session. - #: - #: :type: :obj:`~str` session_id: str = marshaller.attrib(deserializer=str) + """The string ID of this voice state's session.""" - #: Whether this user is deafened by the guild. - #: - #: :type: :obj:`~bool` is_guild_deafened: bool = marshaller.attrib(raw_name="deaf", deserializer=bool) + """Whether this user is deafened by the guild.""" - #: Whether this user is muted by the guild. - #: - #: :type: :obj:`~bool` is_guild_muted: bool = marshaller.attrib(raw_name="mute", deserializer=bool) + """Whether this user is muted by the guild.""" - #: Whether this user is deafened by their client. - #: - #: :type: :obj:`~bool` is_self_deafened: bool = marshaller.attrib(raw_name="self_deaf", deserializer=bool) + """Whether this user is deafened by their client.""" - #: Whether this user is muted by their client. - #: - #: :type: :obj:`~bool` is_self_muted: bool = marshaller.attrib(raw_name="self_mute", deserializer=bool) + """Whether this user is muted by their client.""" - #: Whether this user is streaming using "Go Live". - #: - #: :type: :obj:`~bool` is_streaming: bool = marshaller.attrib(raw_name="self_stream", deserializer=bool, if_undefined=False, default=False) + """Whether this user is streaming using "Go Live".""" - #: Whether this user is muted by the current user. - #: - #: :type: :obj:`~bool` is_suppressed: bool = marshaller.attrib(raw_name="suppress", deserializer=bool) + """Whether this user is muted by the current user.""" @marshaller.marshallable() @@ -102,32 +81,20 @@ class VoiceState(bases.HikariEntity, marshaller.Deserializable): class VoiceRegion(bases.HikariEntity, marshaller.Deserializable): """Represent's a voice region server.""" - #: The ID of this region - #: - #: :type: :obj:`~str` id: str = marshaller.attrib(deserializer=str) + """The string ID of this region.""" - #: The name of this region - #: - #: :type: :obj:`~str` name: str = marshaller.attrib(deserializer=str) + """The name of this region.""" - #: Whether this region is vip-only. - #: - #: :type: :obj:`~bool` is_vip: bool = marshaller.attrib(raw_name="vip", deserializer=bool) + """Whether this region is vip-only.""" - #: Whether this region's server is closest to the current user's client. - #: - #: :type: :obj:`~bool` is_optimal_location: bool = marshaller.attrib(raw_name="optimal", deserializer=bool) + """Whether this region's server is closest to the current user's client.""" - #: Whether this region is deprecated. - #: - #: :type: :obj:`~bool` is_deprecated: bool = marshaller.attrib(raw_name="deprecated", deserializer=bool) + """Whether this region is deprecated.""" - #: Whether this region is custom (e.g. used for events). - #: - #: :type: :obj:`~bool` is_custom: bool = marshaller.attrib(raw_name="custom", deserializer=bool) + """Whether this region is custom (e.g. used for events).""" diff --git a/hikari/webhooks.py b/hikari/webhooks.py index 39b2553ce2..bae5c916c1 100644 --- a/hikari/webhooks.py +++ b/hikari/webhooks.py @@ -33,11 +33,11 @@ class WebhookType(enum.IntEnum): """Types of webhook.""" - #: Incoming webhook. INCOMING = 1 + """Incoming webhook.""" - #: Channel Follower webhook. CHANNEL_FOLLOWER = 2 + """Channel Follower webhook.""" @marshaller.marshallable() @@ -50,52 +50,36 @@ class Webhook(bases.UniqueEntity, marshaller.Deserializable): send informational messages to specific channels. """ - #: The type of the webhook. - #: - #: :type: :obj:`~WebhookType` type: WebhookType = marshaller.attrib(deserializer=WebhookType) + """The type of the webhook.""" - #: The guild ID of the webhook. - #: - #: :type: :obj:`~hikari.bases.Snowflake`, optional guild_id: typing.Optional[bases.Snowflake] = marshaller.attrib( deserializer=bases.Snowflake.deserialize, if_undefined=None, default=None ) + """The guild ID of the webhook.""" - #: The channel ID this webhook is for. - #: - #: :type: :obj:`~hikari.bases.Snowflake` channel_id: bases.Snowflake = marshaller.attrib(deserializer=bases.Snowflake.deserialize) + """The channel ID this webhook is for.""" - #: The user that created the webhook - #: - #: Note - #: ---- - #: This will be :obj:`~None` when getting a webhook with bot authorization - #: rather than the webhook's token. - #: - #: - #: :type: :obj:`~hikari.users.User`, optional user: typing.Optional[users.User] = marshaller.attrib( deserializer=users.User.deserialize, if_undefined=None, default=None ) + """The user that created the webhook + + !!! info + This will be `None` when getting a webhook with bot authorization rather + than the webhook's token. + """ - #: The default name of the webhook. - #: - #: :type: :obj:`~str`, optional name: typing.Optional[str] = marshaller.attrib(deserializer=str, if_none=None) + """The default name of the webhook.""" - #: The default avatar hash of the webhook. - #: - #: :type: :obj:`~str`, optional avatar_hash: typing.Optional[str] = marshaller.attrib(raw_name="avatar", deserializer=str, if_none=None) + """The default avatar hash of the webhook.""" - #: The token of the webhook. - #: - #: Note - #: ---- - #: This is only available for Incoming webhooks. - #: - #: - #: :type: :obj:`~str`, optional token: typing.Optional[str] = marshaller.attrib(deserializer=str, if_undefined=None, default=None) + """The token of the webhook. + + !!! info + This is only available for Incoming webhooks. + """ From 7b1188e7763956fc5b4bd65733a228755752ad48 Mon Sep 17 00:00:00 2001 From: Faster Speeding Date: Tue, 21 Apr 2020 19:47:27 +0100 Subject: [PATCH 185/922] Add gitlab merge review app toolbar script integration --- ci/config.py | 1 + ci/pages.gitlab-ci.yml | 4 ++-- ci/pdoc.nox.py | 18 ++++++++++++++++++ docs/head.mako | 16 +++++++++++++++- docs/index.html | 31 +++++++++++++++++++++++++++++++ docs/logo.mako | 4 ++-- 6 files changed, 69 insertions(+), 5 deletions(-) create mode 100644 docs/index.html diff --git a/ci/config.py b/ci/config.py index c21b474495..ab19383161 100644 --- a/ci/config.py +++ b/ci/config.py @@ -32,6 +32,7 @@ # Generating documentation and artifacts. ARTIFACT_DIRECTORY = "public" DOCUMENTATION_DIRECTORY = "docs" +ROOT_INDEX_SOURCE = "index.html" # Linting and test configs. PYDOCSTYLE_INI = "pydocstyle.ini" diff --git a/ci/pages.gitlab-ci.yml b/ci/pages.gitlab-ci.yml index 8cad99b837..130cc79b8c 100644 --- a/ci/pages.gitlab-ci.yml +++ b/ci/pages.gitlab-ci.yml @@ -32,7 +32,7 @@ script: |+ set -x apt-get update - apt-get install -qy graphviz git + apt-get install -qy graphviz git # FIXME: Remove once in master. pip install nox git clone ${CI_REPOSITORY_URL} -b ${TARGET_BRANCH} hikari_docs_${TARGET_BRANCH} --depth=1 cd "hikari_docs_${TARGET_BRANCH}" @@ -95,7 +95,7 @@ set -e set -x apt-get update - apt-get install -qy graphviz + apt-get install -qy graphviz # FIXME: remove once in master pip install nox nox -s pdoc || nox -s documentation # FIXME: remove once in master. except: diff --git a/ci/pdoc.nox.py b/ci/pdoc.nox.py index a9cbdb512b..f1cb8ffa1c 100644 --- a/ci/pdoc.nox.py +++ b/ci/pdoc.nox.py @@ -17,6 +17,9 @@ # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . """Pdoc documentation generation.""" +import os +import shutil + from ci import config from ci import nox @@ -24,6 +27,21 @@ @nox.session(reuse_venv=True, default=True) def pdoc(session: nox.Session) -> None: """Generate documentation with pdoc.""" + + # Inherit environment GitLab CI vars, where appropriate. + for n, v in os.environ.items(): + if n.startswith(("GITLAB_", "CI")) or n == "CI": + session.env[n] = v + + #: Copy over the root index html file if it's set. + if config.ROOT_INDEX_SOURCE: + if not os.path.exists(config.ARTIFACT_DIRECTORY): + os.mkdir(config.ARTIFACT_DIRECTORY) + shutil.copy( + os.path.join(config.DOCUMENTATION_DIRECTORY, config.ROOT_INDEX_SOURCE), + os.path.join(config.ARTIFACT_DIRECTORY, "index.html") + ) + session.install("-r", config.REQUIREMENTS, "pdoc3==0.8.1") session.run( diff --git a/docs/head.mako b/docs/head.mako index 34a415341b..28682e2422 100644 --- a/docs/head.mako +++ b/docs/head.mako @@ -17,11 +17,25 @@ | along with Hikari. If not, see . !--> <%! + import os from pdoc.html_helpers import minify_css %> +% if "CI_MERGE_REQUEST_IID" in os.environ: + +% endif - +% if "." in module.name: + +% else: + +% endif diff --git a/docs/index.html b/docs/index.html new file mode 100644 index 0000000000..1a98c715e2 --- /dev/null +++ b/docs/index.html @@ -0,0 +1,31 @@ + + + + + + + + + + + Redirecting... + + + diff --git a/docs/logo.mako b/docs/logo.mako index 0bd3681aa1..809f1f9cd1 100644 --- a/docs/logo.mako +++ b/docs/logo.mako @@ -23,7 +23,7 @@ %>
- + Hikari @@ -31,7 +31,7 @@ % if version == "production": % endif diff --git a/pre-commit b/pre-commit deleted file mode 100755 index 38bc078553..0000000000 --- a/pre-commit +++ /dev/null @@ -1,20 +0,0 @@ -#!/bin/bash - -# Put this in .git/hooks to make sure all test and code formatting sessions run every time you commit. - -trap "exit" SIGINT SIGTERM SIGKILL - -if [[ -z ${SKIP_HOOK} ]]; then - command -v nox || pip install nox 'virtualenv<20.0.19' - echo -e '\e[1;32mRUNNING HOOKS - prepend your invocation of git commit with SKIP_HOOK=1 to skip this.\e[0m' - # Fix trailing whitespace in *.py files - find hikari tests -type f -name '*.py' -exec sed -i "s/[ ]*$//g" {} \; -exec git add {} \; - # Run black on specific files if needed. - nox -s check-formatting || nox -s reformat-code | grep -oP '^reformatted \K.*' | xargs -d'\n' -I '{}' git add -n '{}' - # Run pytest, pylint, pydocstyle, bandit, safety - nox --sessions pylint pydocstyle pytest bandit safety -else - echo -e '\e[1;31mSKIPPING COMMIT HOOKS. THIS IS HERESY!\e[0m' -fi - -trap - SIGINT SIGTERM SIGKILL diff --git a/scripts/fix-whitespace.sh b/scripts/fix-whitespace.sh new file mode 100755 index 0000000000..4f0a1fd6f3 --- /dev/null +++ b/scripts/fix-whitespace.sh @@ -0,0 +1,4 @@ +#!/usr/bin/env bash +# Removes trailing whitespace in Python source files. + +find hikari tests setup.py ci -type f -name '*.py' -exec sed -i "s/[ ]*$//g" {} \; -exec git add -v {} \; diff --git a/scripts/test_twemoji_mapping.py b/scripts/test_twemoji_mapping.py new file mode 100644 index 0000000000..51000f028d --- /dev/null +++ b/scripts/test_twemoji_mapping.py @@ -0,0 +1,96 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# Copyright © Nekoka.tt 2019-2020 +# +# This file is part of Hikari. +# +# Hikari is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Hikari is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Hikari. If not, see . +"""A CI-used script that tests all the Twemoji URLs generated +by Discord emojis are actually legitimate URLs, since Discord +does not map these on a 1-to-1 basis. +""" + +import asyncio +import time +import sys + +sys.path.append(".") + +import aiohttp +import hikari + + +valid_emojis = [] +invalid_emojis = [] + + +async def run(): + start = time.perf_counter() + + async with aiohttp.request("get", "https://static.emzi0767.com/misc/discordEmojiMap.json") as resp: + resp.raise_for_status() + mapping = (await resp.json(encoding="utf-8-sig"))["emojiDefinitions"] + + semaphore = asyncio.Semaphore(value=100) + + tasks = [] + + for i, emoji in enumerate(mapping): + await semaphore.acquire() + task = asyncio.create_task(try_fetch(i, len(mapping), emoji["surrogates"], emoji["primaryName"])) + task.add_done_callback(lambda _: semaphore.release()) + tasks.append(task) + + if i and i % 750 == 0: + print("Backing off so GitHub doesn't IP ban us") + await asyncio.gather(*tasks) + await asyncio.sleep(10) + tasks.clear() + + await asyncio.gather(*tasks) + + print("Results") + print("Valid emojis:", len(valid_emojis)) + print("Invalid emojis:", len(invalid_emojis)) + + for surrogates, name in invalid_emojis: + print(*map(hex, map(ord, surrogates)), name) + + print("Time taken", time.perf_counter() - start, "seconds") + + +async def try_fetch(i, n, emoji_surrogates, name): + emoji = hikari.UnicodeEmoji(name=emoji_surrogates) + ex = None + for _ in range(5): + try: + await emoji.__aiter__().__anext__() + except Exception as _ex: + ex = _ex + else: + ex = None + break + + if ex is None: + valid_emojis.append((emoji_surrogates, name)) + print("[ OK ]", f"{i}/{n}", name, *map(hex, map(ord, emoji_surrogates)), emoji.url) + else: + invalid_emojis.append((emoji_surrogates, name)) + print("[ FAIL ]", f"{i}/{n}", name, *map(hex, map(ord, emoji_surrogates)), type(ex), ex, emoji.url) + + +asyncio.run(run()) + +if invalid_emojis: + exit(1) diff --git a/tests/hikari/clients/test_rest/test_guild.py b/tests/hikari/clients/test_rest/test_guild.py index 6de84e3011..0414db954d 100644 --- a/tests/hikari/clients/test_rest/test_guild.py +++ b/tests/hikari/clients/test_rest/test_guild.py @@ -300,17 +300,17 @@ async def test_fetch_audit_log_handles_datetime_object(self, rest_guild_logic_im @pytest.mark.asyncio @_helpers.parametrize_valid_id_formats_for_models("guild", 93443949, guilds.Guild) - @_helpers.parametrize_valid_id_formats_for_models("emoji", 40404040404, emojis.GuildEmoji) + @_helpers.parametrize_valid_id_formats_for_models("emoji", 40404040404, emojis.KnownCustomEmoji) async def test_fetch_guild_emoji(self, rest_guild_logic_impl, guild, emoji): mock_emoji_payload = {"id": "92929", "name": "nyaa", "animated": True} - mock_emoji_obj = mock.MagicMock(emojis.GuildEmoji) + mock_emoji_obj = mock.MagicMock(emojis.KnownCustomEmoji) rest_guild_logic_impl._session.get_guild_emoji.return_value = mock_emoji_payload - with mock.patch.object(emojis.GuildEmoji, "deserialize", return_value=mock_emoji_obj): + with mock.patch.object(emojis.KnownCustomEmoji, "deserialize", return_value=mock_emoji_obj): assert await rest_guild_logic_impl.fetch_guild_emoji(guild=guild, emoji=emoji) is mock_emoji_obj rest_guild_logic_impl._session.get_guild_emoji.assert_called_once_with( guild_id="93443949", emoji_id="40404040404", ) - emojis.GuildEmoji.deserialize.assert_called_once_with( + emojis.KnownCustomEmoji.deserialize.assert_called_once_with( mock_emoji_payload, components=rest_guild_logic_impl._components ) @@ -318,12 +318,12 @@ async def test_fetch_guild_emoji(self, rest_guild_logic_impl, guild, emoji): @_helpers.parametrize_valid_id_formats_for_models("guild", 93443949, guilds.Guild) async def test_fetch_guild_emojis(self, rest_guild_logic_impl, guild): mock_emoji_payload = {"id": "92929", "name": "nyaa", "animated": True} - mock_emoji_obj = mock.MagicMock(emojis.GuildEmoji) + mock_emoji_obj = mock.MagicMock(emojis.KnownCustomEmoji) rest_guild_logic_impl._session.list_guild_emojis.return_value = [mock_emoji_payload] - with mock.patch.object(emojis.GuildEmoji, "deserialize", return_value=mock_emoji_obj): + with mock.patch.object(emojis.KnownCustomEmoji, "deserialize", return_value=mock_emoji_obj): assert await rest_guild_logic_impl.fetch_guild_emojis(guild=guild) == [mock_emoji_obj] rest_guild_logic_impl._session.list_guild_emojis.assert_called_once_with(guild_id="93443949",) - emojis.GuildEmoji.deserialize.assert_called_once_with( + emojis.KnownCustomEmoji.deserialize.assert_called_once_with( mock_emoji_payload, components=rest_guild_logic_impl._components ) @@ -332,19 +332,19 @@ async def test_fetch_guild_emojis(self, rest_guild_logic_impl, guild): @_helpers.parametrize_valid_id_formats_for_models("role", 537340989808050216, guilds.GuildRole) async def test_create_guild_emoji_with_optionals(self, rest_guild_logic_impl, guild, role): mock_emoji_payload = {"id": "229292929", "animated": True} - mock_emoji_obj = mock.MagicMock(emojis.GuildEmoji) + mock_emoji_obj = mock.MagicMock(emojis.KnownCustomEmoji) rest_guild_logic_impl._session.create_guild_emoji.return_value = mock_emoji_payload mock_image_data = mock.MagicMock(bytes) mock_image_obj = mock.MagicMock(files.BaseStream) mock_image_obj.read = mock.AsyncMock(return_value=mock_image_data) stack = contextlib.ExitStack() - stack.enter_context(mock.patch.object(emojis.GuildEmoji, "deserialize", return_value=mock_emoji_obj)) + stack.enter_context(mock.patch.object(emojis.KnownCustomEmoji, "deserialize", return_value=mock_emoji_obj)) with stack: result = await rest_guild_logic_impl.create_guild_emoji( guild=guild, name="fairEmoji", image=mock_image_obj, roles=[role], reason="hello", ) assert result is mock_emoji_obj - emojis.GuildEmoji.deserialize.assert_called_once_with( + emojis.KnownCustomEmoji.deserialize.assert_called_once_with( mock_emoji_payload, components=rest_guild_logic_impl._components ) mock_image_obj.read.assert_awaited_once() @@ -356,20 +356,20 @@ async def test_create_guild_emoji_with_optionals(self, rest_guild_logic_impl, gu @_helpers.parametrize_valid_id_formats_for_models("guild", 93443949, guilds.Guild) async def test_create_guild_emoji_without_optionals(self, rest_guild_logic_impl, guild): mock_emoji_payload = {"id": "229292929", "animated": True} - mock_emoji_obj = mock.MagicMock(emojis.GuildEmoji) + mock_emoji_obj = mock.MagicMock(emojis.KnownCustomEmoji) rest_guild_logic_impl._session.create_guild_emoji.return_value = mock_emoji_payload mock_image_obj = mock.MagicMock(files.BaseStream) mock_image_data = mock.MagicMock(bytes) mock_image_obj = mock.MagicMock(files.BaseStream) mock_image_obj.read = mock.AsyncMock(return_value=mock_image_data) stack = contextlib.ExitStack() - stack.enter_context(mock.patch.object(emojis.GuildEmoji, "deserialize", return_value=mock_emoji_obj)) + stack.enter_context(mock.patch.object(emojis.KnownCustomEmoji, "deserialize", return_value=mock_emoji_obj)) with stack: result = await rest_guild_logic_impl.create_guild_emoji( guild=guild, name="fairEmoji", image=mock_image_obj, ) assert result is mock_emoji_obj - emojis.GuildEmoji.deserialize.assert_called_once_with( + emojis.KnownCustomEmoji.deserialize.assert_called_once_with( mock_emoji_payload, components=rest_guild_logic_impl._components ) mock_image_obj.read.assert_awaited_once() @@ -379,29 +379,29 @@ async def test_create_guild_emoji_without_optionals(self, rest_guild_logic_impl, @pytest.mark.asyncio @_helpers.parametrize_valid_id_formats_for_models("guild", 93443949, guilds.Guild) - @_helpers.parametrize_valid_id_formats_for_models("emoji", 4123321, emojis.GuildEmoji) + @_helpers.parametrize_valid_id_formats_for_models("emoji", 4123321, emojis.KnownCustomEmoji) async def test_update_guild_emoji_without_optionals(self, rest_guild_logic_impl, guild, emoji): mock_emoji_payload = {"id": "202020", "name": "Nyaa", "animated": True} - mock_emoji_obj = mock.MagicMock(emojis.GuildEmoji) + mock_emoji_obj = mock.MagicMock(emojis.KnownCustomEmoji) rest_guild_logic_impl._session.modify_guild_emoji.return_value = mock_emoji_payload - with mock.patch.object(emojis.GuildEmoji, "deserialize", return_value=mock_emoji_obj): + with mock.patch.object(emojis.KnownCustomEmoji, "deserialize", return_value=mock_emoji_obj): assert await rest_guild_logic_impl.update_guild_emoji(guild, emoji) is mock_emoji_obj rest_guild_logic_impl._session.modify_guild_emoji.assert_called_once_with( guild_id="93443949", emoji_id="4123321", name=..., roles=..., reason=..., ) - emojis.GuildEmoji.deserialize.assert_called_once_with( + emojis.KnownCustomEmoji.deserialize.assert_called_once_with( mock_emoji_payload, components=rest_guild_logic_impl._components ) @pytest.mark.asyncio @_helpers.parametrize_valid_id_formats_for_models("guild", 93443949, guilds.Guild) - @_helpers.parametrize_valid_id_formats_for_models("emoji", 4123321, emojis.GuildEmoji) + @_helpers.parametrize_valid_id_formats_for_models("emoji", 4123321, emojis.KnownCustomEmoji) @_helpers.parametrize_valid_id_formats_for_models("role", 123123123, guilds.GuildRole) async def test_update_guild_emoji_with_optionals(self, rest_guild_logic_impl, guild, emoji, role): mock_emoji_payload = {"id": "202020", "name": "Nyaa", "animated": True} - mock_emoji_obj = mock.MagicMock(emojis.GuildEmoji) + mock_emoji_obj = mock.MagicMock(emojis.KnownCustomEmoji) rest_guild_logic_impl._session.modify_guild_emoji.return_value = mock_emoji_payload - with mock.patch.object(emojis.GuildEmoji, "deserialize", return_value=mock_emoji_obj): + with mock.patch.object(emojis.KnownCustomEmoji, "deserialize", return_value=mock_emoji_obj): result = await rest_guild_logic_impl.update_guild_emoji( guild, emoji, name="Nyaa", roles=[role], reason="Agent 42" ) @@ -409,13 +409,13 @@ async def test_update_guild_emoji_with_optionals(self, rest_guild_logic_impl, gu rest_guild_logic_impl._session.modify_guild_emoji.assert_called_once_with( guild_id="93443949", emoji_id="4123321", name="Nyaa", roles=["123123123"], reason="Agent 42", ) - emojis.GuildEmoji.deserialize.assert_called_once_with( + emojis.KnownCustomEmoji.deserialize.assert_called_once_with( mock_emoji_payload, components=rest_guild_logic_impl._components ) @pytest.mark.asyncio @_helpers.parametrize_valid_id_formats_for_models("guild", 93443949, guilds.Guild) - @_helpers.parametrize_valid_id_formats_for_models("emoji", 4123321, emojis.GuildEmoji) + @_helpers.parametrize_valid_id_formats_for_models("emoji", 4123321, emojis.KnownCustomEmoji) async def test_delete_guild_emoji(self, rest_guild_logic_impl, guild, emoji): rest_guild_logic_impl._session.delete_guild_emoji.return_value = ... assert await rest_guild_logic_impl.delete_guild_emoji(guild, emoji) is None diff --git a/tests/hikari/clients/test_rest/test_react.py b/tests/hikari/clients/test_rest/test_react.py index af6d9bc7aa..ba4bbf5333 100644 --- a/tests/hikari/clients/test_rest/test_react.py +++ b/tests/hikari/clients/test_rest/test_react.py @@ -38,7 +38,7 @@ [ "\N{OK HAND SIGN}", emojis.UnicodeEmoji(name="\N{OK HAND SIGN}"), - emojis.UnknownEmoji(id=bases.Snowflake(9876), name="foof"), + emojis.CustomEmoji(id=bases.Snowflake(9876), name="foof"), ], ids=lambda arg: str(arg), ) @@ -189,7 +189,7 @@ def __init__(self): @pytest.mark.asyncio @_helpers.parametrize_valid_id_formats_for_models("channel", 213123, channels.PartialChannel) @_helpers.parametrize_valid_id_formats_for_models("message", 987654321, messages.Message) - @pytest.mark.parametrize("emoji", ["blah:123", emojis.UnknownEmoji(name="blah", id=123, is_animated=False)]) + @pytest.mark.parametrize("emoji", ["blah:123", emojis.CustomEmoji(name="blah", id=123, is_animated=False)]) async def test_create_reaction(self, rest_reaction_logic_impl, channel, message, emoji): rest_reaction_logic_impl._session.create_reaction.return_value = ... assert await rest_reaction_logic_impl.add_reaction(channel=channel, message=message, emoji=emoji) is None @@ -200,7 +200,7 @@ async def test_create_reaction(self, rest_reaction_logic_impl, channel, message, @pytest.mark.asyncio @_helpers.parametrize_valid_id_formats_for_models("channel", 213123, channels.PartialChannel) @_helpers.parametrize_valid_id_formats_for_models("message", 987654321, messages.Message) - @pytest.mark.parametrize("emoji", ["blah:123", emojis.UnknownEmoji(name="blah", id=123, is_animated=False)]) + @pytest.mark.parametrize("emoji", ["blah:123", emojis.CustomEmoji(name="blah", id=123, is_animated=False)]) async def test_delete_reaction_for_bot_user(self, rest_reaction_logic_impl, channel, message, emoji): rest_reaction_logic_impl._session.delete_own_reaction.return_value = ... assert await rest_reaction_logic_impl.remove_reaction(channel=channel, message=message, emoji=emoji) is None @@ -212,7 +212,7 @@ async def test_delete_reaction_for_bot_user(self, rest_reaction_logic_impl, chan @_helpers.parametrize_valid_id_formats_for_models("channel", 213123, channels.PartialChannel) @_helpers.parametrize_valid_id_formats_for_models("message", 987654321, messages.Message) @_helpers.parametrize_valid_id_formats_for_models("user", 96969696, users.User) - @pytest.mark.parametrize("emoji", ["blah:123", emojis.UnknownEmoji(name="blah", id=123, is_animated=False)]) + @pytest.mark.parametrize("emoji", ["blah:123", emojis.CustomEmoji(name="blah", id=123, is_animated=False)]) async def test_delete_reaction_for_other_user(self, rest_reaction_logic_impl, channel, message, emoji, user): rest_reaction_logic_impl._session.delete_user_reaction.return_value = ... assert ( @@ -226,7 +226,7 @@ async def test_delete_reaction_for_other_user(self, rest_reaction_logic_impl, ch @pytest.mark.asyncio @_helpers.parametrize_valid_id_formats_for_models("channel", 213123, channels.PartialChannel) @_helpers.parametrize_valid_id_formats_for_models("message", 987654321, messages.Message) - @pytest.mark.parametrize("emoji", [None, "blah:123", emojis.UnknownEmoji(name="blah", id=123, is_animated=False)]) + @pytest.mark.parametrize("emoji", [None, "blah:123", emojis.CustomEmoji(name="blah", id=123, is_animated=False)]) async def test_delete_all_reactions(self, rest_reaction_logic_impl, channel, message, emoji): rest_reaction_logic_impl._session = mock.MagicMock(spec_set=rest.REST) assert ( diff --git a/tests/hikari/events/test_guild.py b/tests/hikari/events/test_guild.py index bd183a2aca..86ba22fc8f 100644 --- a/tests/hikari/events/test_guild.py +++ b/tests/hikari/events/test_guild.py @@ -110,10 +110,10 @@ def test_guild_emojis_update_payload(self, test_emoji_payload): return {"emojis": [test_emoji_payload], "guild_id": "696969"} def test_deserialize(self, test_guild_emojis_update_payload, test_emoji_payload): - mock_emoji = _helpers.mock_model(emojis.GuildEmoji, id=4242) - with mock.patch.object(emojis.GuildEmoji, "deserialize", return_value=mock_emoji): + mock_emoji = _helpers.mock_model(emojis.KnownCustomEmoji, id=4242) + with mock.patch.object(emojis.KnownCustomEmoji, "deserialize", return_value=mock_emoji): guild_emojis_update_obj = guild.GuildEmojisUpdateEvent.deserialize(test_guild_emojis_update_payload) - emojis.GuildEmoji.deserialize.assert_called_once_with(test_emoji_payload) + emojis.KnownCustomEmoji.deserialize.assert_called_once_with(test_emoji_payload) assert guild_emojis_update_obj.emojis == {mock_emoji.id: mock_emoji} assert guild_emojis_update_obj.guild_id == 696969 diff --git a/tests/hikari/events/test_message.py b/tests/hikari/events/test_message.py index 49976e2c97..2680db76c5 100644 --- a/tests/hikari/events/test_message.py +++ b/tests/hikari/events/test_message.py @@ -303,7 +303,7 @@ def test_message_reaction_add_payload(self, test_member_payload, test_emoji_payl def test_deserialize(self, test_message_reaction_add_payload, test_member_payload, test_emoji_payload): mock_member = mock.MagicMock(guilds.GuildMember) - mock_emoji = mock.MagicMock(emojis.UnknownEmoji) + mock_emoji = mock.MagicMock(emojis.CustomEmoji) stack = contextlib.ExitStack() patched_member_deserializer = stack.enter_context( _helpers.patch_marshal_attr( @@ -345,7 +345,7 @@ def test_message_reaction_remove_payload(self, test_emoji_payload): } def test_deserialize(self, test_message_reaction_remove_payload, test_emoji_payload): - mock_emoji = mock.MagicMock(emojis.UnknownEmoji) + mock_emoji = mock.MagicMock(emojis.CustomEmoji) with _helpers.patch_marshal_attr( message.MessageReactionRemoveEvent, "emoji", @@ -383,7 +383,7 @@ def test_message_reaction_remove_emoji_payload(self, test_emoji_payload): return {"channel_id": "4393939", "message_id": "2993993", "guild_id": "49494949", "emoji": test_emoji_payload} def test_deserialize(self, test_message_reaction_remove_emoji_payload, test_emoji_payload): - mock_emoji = mock.MagicMock(emojis.UnknownEmoji) + mock_emoji = mock.MagicMock(emojis.CustomEmoji) with _helpers.patch_marshal_attr( message.MessageReactionRemoveEmojiEvent, "emoji", diff --git a/tests/hikari/test_emojis.py b/tests/hikari/test_emojis.py index ef8b1be6b1..2b5ee26029 100644 --- a/tests/hikari/test_emojis.py +++ b/tests/hikari/test_emojis.py @@ -19,9 +19,12 @@ import mock import pytest +from hikari import bases from hikari import emojis +from hikari import files from hikari import users from hikari.clients import components +from hikari.internal import urls from tests.hikari import _helpers @@ -30,7 +33,43 @@ def mock_components(): return mock.MagicMock(components.Components) +class TestEmoji: + @pytest.fixture + def test_emoji(self): + class Impl(emojis.Emoji): + @property + def url(self): + return "http://example.com/test" + + @property + def url_name(self): + return "test:1234" + + @property + def mention(self): + return "<:test:1234>" + + @property + def filename(self) -> str: + return "test.png" + + return Impl() + + def test_is_mentionable(self, test_emoji): + assert test_emoji.is_mentionable + + def test_aiter(self, test_emoji): + aiter = mock.MagicMock() + stream = mock.MagicMock(__aiter__=mock.MagicMock(return_value=aiter)) + with mock.patch.object(files, "WebResourceStream", return_value=stream) as new: + assert test_emoji.__aiter__() is aiter + + new.assert_called_with("test.png", "http://example.com/test") + + class TestUnicodeEmoji: + england = [0x1F3F4, 0xE0067, 0xE0062, 0xE0065, 0xE006E, 0xE0067, 0xE007F] + def test_deserialize(self, mock_components): emoji_obj = emojis.UnicodeEmoji.deserialize({"name": "🤷"}, components=mock_components) @@ -42,10 +81,83 @@ def test_url_name(self): def test_mention(self): assert emojis.UnicodeEmoji(name="🤷").mention == "🤷" + def test_codepoints(self): + # :england: + codepoints = self.england.copy() + e = emojis.UnicodeEmoji(name="".join(map(chr, codepoints))) + assert e.codepoints == codepoints + + def test_from_codepoints(self): + # :england: + codepoints = self.england.copy() + e = emojis.UnicodeEmoji.from_codepoints(*codepoints) + assert e.codepoints == codepoints + + def test_from_emoji(self): + string = "\N{WHITE SMILING FACE}\N{VARIATION SELECTOR-16}" + assert emojis.UnicodeEmoji.from_emoji(string).codepoints == [0x263A, 0xFE0F] -class TestUnknownEmoji: + @pytest.mark.parametrize( + ["codepoints", "filename"], + [ + (england.copy(), "1f3f4-e0067-e0062-e0065-e006e-e0067-e007f.png"), # england + ([0x1F38C], "1f38c.png"), # crossed_flag + ([0x263A, 0xFE0F], "263a.png"), # relaxed + ([0x1F3F3, 0xFE0F, 0x200D, 0x1F308], "1f3f3-fe0f-200d-1f308.png"), # gay pride (outlier case) + ([0x1F3F4, 0x200D, 0x2620, 0xFE0F], "1f3f4-200d-2620-fe0f.png"), # pirate flag + ([0x1F3F3, 0xFE0F], "1f3f3.png"), # white flag + ([0x1F939, 0x1F3FE, 0x200D, 0x2642, 0xFE0F], "1f939-1f3fe-200d-2642-fe0f.png"), # man-juggling-tone-4 + ], + ) + def test_filename(self, codepoints, filename): + char = "".join(map(chr, codepoints)) + assert emojis.UnicodeEmoji(name=char).filename == filename + + @pytest.mark.parametrize( + ["codepoints", "filename"], + [ + (england.copy(), "1f3f4-e0067-e0062-e0065-e006e-e0067-e007f.png"), # england + ([0x1F38C], "1f38c.png"), # crossed_flag + ([0x263A, 0xFE0F], "263a.png"), # relaxed + ([0x1F3F3, 0xFE0F, 0x200D, 0x1F308], "1f3f3-fe0f-200d-1f308.png"), # gay pride (outlier case) + ([0x1F3F4, 0x200D, 0x2620, 0xFE0F], "1f3f4-200d-2620-fe0f.png"), # pirate flag + ([0x1F3F3, 0xFE0F], "1f3f3.png"), # white flag + ([0x1F939, 0x1F3FE, 0x200D, 0x2642, 0xFE0F], "1f939-1f3fe-200d-2642-fe0f.png"), # man-juggling-tone-4 + ], + ) + def test_url(self, codepoints, filename): + char = "".join(map(chr, codepoints)) + url = "https://github.com/twitter/twemoji/raw/master/assets/72x72/" + filename + assert emojis.UnicodeEmoji(name=char).url == url + + def test_unicode_names(self): + codepoints = [0x1F939, 0x1F3FE, 0x200D, 0x2642, 0xFE0F] + # https://unicode-table.com/en/ + names = [ + "JUGGLING", + "EMOJI MODIFIER FITZPATRICK TYPE-5", + "ZERO WIDTH JOINER", + "MALE SIGN", + "VARIATION SELECTOR-16", + ] + + char = "".join(map(chr, codepoints)) + assert emojis.UnicodeEmoji(name=char).unicode_names == names + + def test_from_unicode_escape(self): + input_string = r"\U0001f939\U0001f3fe\u200d\u2642\ufe0f" + codepoints = [0x1F939, 0x1F3FE, 0x200D, 0x2642, 0xFE0F] + assert emojis.UnicodeEmoji.from_unicode_escape(input_string).codepoints == codepoints + + def test_unicode_escape(self): + codepoints = [0x1F939, 0x1F3FE, 0x200D, 0x2642, 0xFE0F] + expected_string = r"\U0001f939\U0001f3fe\u200d\u2642\ufe0f" + assert emojis.UnicodeEmoji(name="".join(map(chr, codepoints))).unicode_escape == expected_string + + +class TestCustomEmoji: def test_deserialize(self, mock_components): - emoji_obj = emojis.UnknownEmoji.deserialize( + emoji_obj = emojis.CustomEmoji.deserialize( {"id": "1234", "name": "test", "animated": True}, components=mock_components ) @@ -54,19 +166,38 @@ def test_deserialize(self, mock_components): assert emoji_obj.is_animated is True def test_url_name(self): - name = emojis.UnknownEmoji(is_animated=True, id=650573534627758100, name="nyaa").url_name + name = emojis.CustomEmoji(is_animated=True, id=bases.Snowflake("650573534627758100"), name="nyaa").url_name assert name == "nyaa:650573534627758100" + @pytest.mark.parametrize(["animated", "extension"], [(True, ".gif"), (False, ".png")]) + def test_filename(self, animated, extension): + emoji = emojis.CustomEmoji(is_animated=animated, id=bases.Snowflake(9876543210), name="Foo") + assert emoji.filename == f"9876543210{extension}" + + @pytest.mark.parametrize(["animated", "is_mentionable"], [(True, True), (False, True), (None, False)]) + def test_is_mentionable(self, animated, is_mentionable): + emoji = emojis.CustomEmoji(is_animated=animated, id=bases.Snowflake(123), name="Foo") + assert emoji.is_mentionable is is_mentionable + + @pytest.mark.parametrize(["animated", "fmt"], [(True, "gif"), (False, "png")]) + def test_url(self, animated, fmt): + emoji = emojis.CustomEmoji(is_animated=animated, id=bases.Snowflake(98765), name="Foo") + mock_result = mock.MagicMock(spec_set=str) + with mock.patch.object(urls, "generate_cdn_url", return_value=mock_result) as generate_cdn_url: + assert emoji.url is mock_result + + generate_cdn_url.assert_called_once_with("emojis", "98765", fmt=fmt, size=None) + -class TestGuildEmoji: +class TestKnownCustomEmoji: def test_deserialize(self, mock_components): mock_user = mock.MagicMock(users.User) test_user_payload = {"id": "123456", "username": "hikari", "discriminator": "0000", "avatar": None} with _helpers.patch_marshal_attr( - emojis.GuildEmoji, "user", deserializer=users.User.deserialize, return_value=mock_user + emojis.KnownCustomEmoji, "user", deserializer=users.User.deserialize, return_value=mock_user ) as patched_user_deserializer: - emoji_obj = emojis.GuildEmoji.deserialize( + emoji_obj = emojis.KnownCustomEmoji.deserialize( { "id": "12345", "name": "testing", @@ -92,7 +223,7 @@ def test_deserialize(self, mock_components): @pytest.fixture() def mock_guild_emoji_obj(self): - return emojis.GuildEmoji( + return emojis.KnownCustomEmoji( is_animated=False, is_available=True, id=650573534627758100, @@ -114,7 +245,7 @@ def test_mention_when_not_animated(self, mock_guild_emoji_obj): @pytest.mark.parametrize( ["payload", "expected_type"], - [({"name": "🤷"}, emojis.UnicodeEmoji), ({"id": "1234", "name": "test"}, emojis.UnknownEmoji)], + [({"name": "🤷"}, emojis.UnicodeEmoji), ({"id": "1234", "name": "test"}, emojis.CustomEmoji)], ) def test_deserialize_reaction_emoji_returns_expected_type(payload, expected_type): assert isinstance(emojis.deserialize_reaction_emoji(payload), expected_type) diff --git a/tests/hikari/test_files.py b/tests/hikari/test_files.py index ecf949e083..b7943d8a22 100644 --- a/tests/hikari/test_files.py +++ b/tests/hikari/test_files.py @@ -45,13 +45,34 @@ async def __aiter__(self): yield b" " yield b"bork" - i = Impl("foobar") + @property + def filename(self) -> str: + return "poof" + + i = Impl() assert await i.read() == b"foo bar baz bork" + def test_repr(self): + class Impl(files.BaseStream): + @property + def filename(self) -> str: + return "poofs.gpg" + + async def __aiter__(self): + yield b"" + + i = Impl() + + assert repr(i) == "Impl(filename='poofs.gpg')" + @pytest.mark.asyncio class TestByteStream: + async def test_filename(self): + stream = files.ByteStream("foo.txt", b"(.) (.)") + assert stream.filename == "foo.txt" + @pytest.mark.parametrize( "chunks", [ @@ -251,6 +272,10 @@ def mock_request(self, stub_response): with mock.patch.object(aiohttp, "request", new=mock.MagicMock(return_value=stub_response)) as request: yield request + async def test_filename(self): + stream = files.WebResourceStream("cat.png", "http://http.cat") + assert stream.filename == "cat.png" + async def test_happy_path_reads_data_in_chunks(self, stub_content, stub_response, mock_request): stream = files.WebResourceStream("cat.png", "https://some-websi.te") @@ -352,6 +377,10 @@ def dummy_file(self, random_bytes): yield file + async def test_filename(self): + stream = files.FileStream("cat.png", "/root/cat.png") + assert stream.filename == "cat.png" + async def test_read_no_executor(self, random_bytes, dummy_file): stream = files.FileStream("xxx", dummy_file) diff --git a/tests/hikari/test_guilds.py b/tests/hikari/test_guilds.py index 65101a0fcb..edc51c9cf5 100644 --- a/tests/hikari/test_guilds.py +++ b/tests/hikari/test_guilds.py @@ -483,7 +483,7 @@ def test_deserialize( mock_components, ): mock_created_at = mock.MagicMock(datetime.datetime) - mock_emoji = mock.MagicMock(emojis.UnknownEmoji) + mock_emoji = mock.MagicMock(emojis.CustomEmoji) stack = contextlib.ExitStack() patched_created_at_deserializer = stack.enter_context( _helpers.patch_marshal_attr( @@ -846,10 +846,10 @@ def test_format_icon_url(self, partial_guild_obj): class TestGuildPreview: def test_deserialize(self, test_guild_preview_payload, test_emoji_payload, mock_components): - mock_emoji = mock.MagicMock(emojis.GuildEmoji, id=41771983429993937) - with mock.patch.object(emojis.GuildEmoji, "deserialize", return_value=mock_emoji): + mock_emoji = mock.MagicMock(emojis.KnownCustomEmoji, id=41771983429993937) + with mock.patch.object(emojis.KnownCustomEmoji, "deserialize", return_value=mock_emoji): guild_preview_obj = guilds.GuildPreview.deserialize(test_guild_preview_payload, components=mock_components) - emojis.GuildEmoji.deserialize.assert_called_once_with(test_emoji_payload, components=mock_components) + emojis.KnownCustomEmoji.deserialize.assert_called_once_with(test_emoji_payload, components=mock_components) assert guild_preview_obj.splash_hash == "dsa345tfcdg54b" assert guild_preview_obj.discovery_splash_hash == "lkodwaidi09239uid" assert guild_preview_obj.emojis == {41771983429993937: mock_emoji} @@ -930,10 +930,10 @@ def test_deserialize( test_channel_payload, test_guild_member_presence, ): - mock_emoji = mock.MagicMock(emojis.GuildEmoji, id=41771983429993937) + mock_emoji = mock.MagicMock(emojis.KnownCustomEmoji, id=41771983429993937) mock_guild_channel = mock.MagicMock(channels.GuildChannel, id=1234567) stack = contextlib.ExitStack() - stack.enter_context(mock.patch.object(emojis.GuildEmoji, "deserialize", return_value=mock_emoji)) + stack.enter_context(mock.patch.object(emojis.KnownCustomEmoji, "deserialize", return_value=mock_emoji)) stack.enter_context(mock.patch.object(channels, "deserialize_channel", return_value=mock_guild_channel)) stack.enter_context( _helpers.patch_marshal_attr( @@ -943,7 +943,7 @@ def test_deserialize( with stack: guild_obj = guilds.Guild.deserialize(test_guild_payload, components=mock_components) channels.deserialize_channel.assert_called_once_with(test_channel_payload, components=mock_components) - emojis.GuildEmoji.deserialize.assert_called_once_with(test_emoji_payload, components=mock_components) + emojis.KnownCustomEmoji.deserialize.assert_called_once_with(test_emoji_payload, components=mock_components) assert guild_obj.members == {123456: guilds.GuildMember.deserialize(test_member_payload)} assert guild_obj.members[123456]._components is mock_components assert guild_obj.presences == {123456: guilds.GuildMemberPresence.deserialize(test_guild_member_presence)} diff --git a/tests/hikari/test_messages.py b/tests/hikari/test_messages.py index ec98ad1b5c..e1d3e72c96 100644 --- a/tests/hikari/test_messages.py +++ b/tests/hikari/test_messages.py @@ -155,7 +155,7 @@ def test_deserialize(self, test_attachment_payload, mock_components): class TestReaction: def test_deserialize(self, test_reaction_payload, mock_components, test_emoji_payload): - mock_emoji = mock.MagicMock(emojis.UnknownEmoji) + mock_emoji = mock.MagicMock(emojis.CustomEmoji) with _helpers.patch_marshal_attr( messages.Reaction, "emoji", return_value=mock_emoji, deserializer=emojis.deserialize_reaction_emoji From d24b383859f59d11d1772ec3673dc5385b679587 Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Tue, 12 May 2020 20:59:26 +0100 Subject: [PATCH 312/922] Added CODEOWNERS --- .gitlab/CODEOWNERS | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 .gitlab/CODEOWNERS diff --git a/.gitlab/CODEOWNERS b/.gitlab/CODEOWNERS new file mode 100644 index 0000000000..696c6e9be9 --- /dev/null +++ b/.gitlab/CODEOWNERS @@ -0,0 +1,2 @@ +* @Nekoka.tt + From 138a5bca581bdb71241284f443a43e417ec4bfb8 Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Tue, 12 May 2020 20:22:46 +0000 Subject: [PATCH 313/922] Delete CODEOWNERS --- .gitlab/CODEOWNERS | 2 -- 1 file changed, 2 deletions(-) delete mode 100644 .gitlab/CODEOWNERS diff --git a/.gitlab/CODEOWNERS b/.gitlab/CODEOWNERS deleted file mode 100644 index 696c6e9be9..0000000000 --- a/.gitlab/CODEOWNERS +++ /dev/null @@ -1,2 +0,0 @@ -* @Nekoka.tt - From 689b377a2c17c60dbd7f8301f79f0f34a314fbfb Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Tue, 12 May 2020 20:42:53 +0000 Subject: [PATCH 314/922] Update black.nox.py to mute lgtm issue [skip ci] --- ci/black.nox.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/ci/black.nox.py b/ci/black.nox.py index 830af21908..993fc9c4b9 100644 --- a/ci/black.nox.py +++ b/ci/black.nox.py @@ -17,8 +17,6 @@ # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . """Black code-style jobs.""" -import subprocess - from ci import nox From 7f57bb52c68ac2ddb5980dccc026ac9254135754 Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Wed, 13 May 2020 10:05:51 +0000 Subject: [PATCH 315/922] Update _about.py --- hikari/_about.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hikari/_about.py b/hikari/_about.py index 5b39d5a811..f3a310275a 100644 --- a/hikari/_about.py +++ b/hikari/_about.py @@ -24,7 +24,7 @@ __ci__ = "https://gitlab.com/nekokatt/hikari/pipelines" __copyright__ = "© 2019-2020 Nekokatt" __discord_invite__ = "https://discord.gg/Jx4cNGG" -__docs__ = "https://nekokatt.gitlab.io/hikari/staging/index.html" +__docs__ = "https://nekokatt.gitlab.io/hikari/index.html" __email__ = "3903853-nekokatt@users.noreply.gitlab.com" __issue_tracker__ = "https://gitlab.com/nekokatt/hikari/issues" __license__ = "LGPL-3.0-ONLY" From ab93f0208e027b8f81f0015321349855f30f9728 Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Wed, 13 May 2020 17:01:51 +0100 Subject: [PATCH 316/922] Completed coverage in hikari/guilds.py --- tests/hikari/test_guilds.py | 34 ++++++++++++++++++++++++++++++---- 1 file changed, 30 insertions(+), 4 deletions(-) diff --git a/tests/hikari/test_guilds.py b/tests/hikari/test_guilds.py index edc51c9cf5..09b2989748 100644 --- a/tests/hikari/test_guilds.py +++ b/tests/hikari/test_guilds.py @@ -796,7 +796,7 @@ def test_deserialize(self, test_partial_guild_payload, mock_components): @pytest.fixture() def partial_guild_obj(self, test_partial_guild_payload): - return guilds.PartialGuild( + return _helpers.unslot_class(guilds.PartialGuild)( id=152559372126519269, icon_hash="d4a983885dsaa7691ce8bcaaf945a", name=None, features=None, ) @@ -836,13 +836,39 @@ def test_format_icon_url_returns_none(self, partial_guild_obj): urls.generate_cdn_url.assert_not_called() assert url is None - def test_format_icon_url(self, partial_guild_obj): + @pytest.mark.parametrize( + ["fmt", "expected_fmt", "icon_hash", "size"], + [ + ("png", "png", "a_1a2b3c", 1 << 4), + ("png", "png", "1a2b3c", 1 << 5), + ("jpeg", "jpeg", "a_1a2b3c", 1 << 6), + ("jpeg", "jpeg", "1a2b3c", 1 << 7), + ("jpg", "jpg", "a_1a2b3c", 1 << 8), + ("jpg", "jpg", "1a2b3c", 1 << 9), + ("webp", "webp", "a_1a2b3c", 1 << 10), + ("webp", "webp", "1a2b3c", 1 << 11), + ("gif", "gif", "a_1a2b3c", 1 << 12), + ("gif", "gif", "1a2b3c", 1 << 7), + (None, "gif", "a_1a2b3c", 1 << 5), + (None, "png", "1a2b3c", 1 << 10), + ], + ) + def test_format_icon_url(self, partial_guild_obj, fmt, expected_fmt, icon_hash, size): mock_url = "https://cdn.discordapp.com/icons/152559372126519269/d4a983885dsaa7691ce8bcaaf945a.png?size=20" + partial_guild_obj.icon_hash = icon_hash with mock.patch.object(urls, "generate_cdn_url", return_value=mock_url): - url = partial_guild_obj.icon_url - urls.generate_cdn_url.assert_called_once() + url = partial_guild_obj.format_icon_url(fmt, size) + urls.generate_cdn_url.assert_called_once_with( + "icons", str(partial_guild_obj.id), partial_guild_obj.icon_hash, fmt=expected_fmt, size=size + ) assert url == mock_url + def test_icon_url_default(self, partial_guild_obj): + result = mock.MagicMock() + partial_guild_obj.format_icon_url = mock.MagicMock(return_value=result) + assert partial_guild_obj.icon_url is result + partial_guild_obj.format_icon_url.assert_called_once_with() + class TestGuildPreview: def test_deserialize(self, test_guild_preview_payload, test_emoji_payload, mock_components): From 94606b97f10ffd1fed8927e7dbc8a524563646b5 Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Wed, 13 May 2020 17:30:38 +0100 Subject: [PATCH 317/922] Completed coverage in audit logs by removing redundant condition. --- hikari/audit_logs.py | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/hikari/audit_logs.py b/hikari/audit_logs.py index a3dc5a71dd..28e025d053 100644 --- a/hikari/audit_logs.py +++ b/hikari/audit_logs.py @@ -414,10 +414,7 @@ def deserialize(cls, payload: typing.Mapping[str, str], **_) -> UnrecognisedAudi return cls(payload) -_EntryInfoEntityT = typing.TypeVar("_EntryInfoEntityT", bound=BaseAuditLogEntryInfo) - - -def get_entry_info_entity(type_: int) -> typing.Type[_EntryInfoEntityT]: +def get_entry_info_entity(type_: int) -> typing.Type[BaseAuditLogEntryInfo]: """Get the entity that's registered for an entry's options. Parameters @@ -467,8 +464,8 @@ def deserialize(cls, payload: more_typing.JSONObject, **kwargs: typing.Any) -> A target_id = bases.Snowflake(target_id) if (options := payload.get("options")) is not None: - if option_converter := get_entry_info_entity(action_type): - options = option_converter.deserialize(options, **kwargs) + option_converter = get_entry_info_entity(action_type) + options = option_converter.deserialize(options, **kwargs) # noinspection PyArgumentList return cls( @@ -583,13 +580,13 @@ class AuditLogIterator(typing.AsyncIterator[AuditLogEntry]): "webhooks", ) - integrations: typing.Mapping[bases.Snowflake, guilds.GuildIntegration] + integrations: typing.MutableMapping[bases.Snowflake, guilds.GuildIntegration] """A mapping of the partial integrations objects found in this log so far.""" - users: typing.Mapping[bases.Snowflake, _users.User] + users: typing.MutableMapping[bases.Snowflake, _users.User] """A mapping of the objects of users found in this audit log so far.""" - webhooks: typing.Mapping[bases.Snowflake, _webhooks.Webhook] + webhooks: typing.MutableMapping[bases.Snowflake, _webhooks.Webhook] """A mapping of the objects of webhooks found in this audit log so far.""" def __init__( From a2cc55b4fb3edce4a65721f193d0b8cc9fcb2bd2 Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Wed, 13 May 2020 20:51:55 +0100 Subject: [PATCH 318/922] Added missing message paginator test suite and fixed bug with snowflake usage in message pagination. --- hikari/clients/rest/channel.py | 2 +- .../hikari/clients/test_rest/test_channel.py | 143 +++++++++++++++++- tests/hikari/clients/test_rest/test_guild.py | 8 + tests/hikari/clients/test_rest/test_me.py | 8 + tests/hikari/clients/test_rest/test_react.py | 8 + 5 files changed, 167 insertions(+), 2 deletions(-) diff --git a/hikari/clients/rest/channel.py b/hikari/clients/rest/channel.py index 2f2cb188a6..3f79c94b12 100644 --- a/hikari/clients/rest/channel.py +++ b/hikari/clients/rest/channel.py @@ -53,7 +53,7 @@ def __init__(self, channel, direction, first, components, session) -> None: self._channel_id = str(int(channel)) self._direction = direction self._first_id = ( - bases.Snowflake.from_datetime(first) if isinstance(first, datetime.datetime) else str(int(first)) + str(bases.Snowflake.from_datetime(first)) if isinstance(first, datetime.datetime) else str(int(first)) ) self._components = components self._session = session diff --git a/tests/hikari/clients/test_rest/test_channel.py b/tests/hikari/clients/test_rest/test_channel.py index 850cbbbf93..7afc5613fa 100644 --- a/tests/hikari/clients/test_rest/test_channel.py +++ b/tests/hikari/clients/test_rest/test_channel.py @@ -18,7 +18,9 @@ # along with Hikari. If not, see . import contextlib import datetime +import inspect +import attr import mock import pytest @@ -38,7 +40,146 @@ from tests.hikari import _helpers -class TestRESTChannelLogging: +@pytest.mark.asyncio +class TestMessagePaginator: + @pytest.fixture + def mock_session(self): + return mock.MagicMock(spec_set=rest.REST) + + @pytest.fixture + def mock_components(self): + return mock.MagicMock(spec_set=components.Components) + + @pytest.fixture + def message_cls(self): + with mock.patch.object(messages, "Message") as message_cls: + yield message_cls + + @pytest.mark.parametrize("direction", ["before", "after", "around"]) + def test_init_first_id_is_date(self, mock_session, mock_components, direction): + date = datetime.datetime(2015, 11, 15, 23, 13, 46, 709000, tzinfo=datetime.timezone.utc) + expected_id = 115590097100865536 + channel_id = 1234567 + pag = channel._MessagePaginator(channel_id, direction, date, mock_components, mock_session) + assert pag._first_id == str(expected_id) + assert pag._channel_id == str(channel_id) + assert pag._direction == direction + assert pag._session is mock_session + assert pag._components is mock_components + + @pytest.mark.parametrize("direction", ["before", "after", "around"]) + def test_init_first_id_is_id(self, mock_session, mock_components, direction): + expected_id = 115590097100865536 + channel_id = 1234567 + pag = channel._MessagePaginator(channel_id, direction, expected_id, mock_components, mock_session) + assert pag._first_id == str(expected_id) + assert pag._channel_id == str(channel_id) + assert pag._direction == direction + assert pag._session is mock_session + assert pag._components is mock_components + + @pytest.mark.parametrize("direction", ["before", "after", "around"]) + async def test_next_chunk_makes_api_call(self, mock_session, mock_components, message_cls, direction): + channel_obj = mock.MagicMock(__int__=lambda _: 55) + + mock_session.get_channel_messages = mock.AsyncMock(return_value=[]) + pag = channel._MessagePaginator(channel_obj, direction, "12345", mock_components, mock_session) + pag._first_id = "12345" + + await pag._next_chunk() + + mock_session.get_channel_messages.assert_awaited_once_with( + **{direction: "12345", "channel_id": "55", "limit": 100} + ) + + @pytest.mark.parametrize("direction", ["before", "after", "around"]) + async def test_next_chunk_empty_response_returns_None(self, mock_session, mock_components, message_cls, direction): + channel_obj = mock.MagicMock(__int__=lambda _: 55) + + pag = channel._MessagePaginator(channel_obj, direction, "12345", mock_components, mock_session) + pag._first_id = "12345" + + mock_session.get_channel_messages = mock.AsyncMock(return_value=[]) + + assert await pag._next_chunk() is None + + @pytest.mark.parametrize(["direction", "expect_reverse"], [("before", False), ("after", True), ("around", False)]) + async def test_next_chunk_updates_first_id( + self, mock_session, mock_components, message_cls, expect_reverse, direction + ): + return_payload = [ + {"id": "1234", ...: ...}, + {"id": "3456", ...: ...}, + {"id": "3333", ...: ...}, + {"id": "512", ...: ...}, + ] + + mock_session.get_channel_messages = mock.AsyncMock(return_value=return_payload) + + channel_obj = mock.MagicMock(__int__=lambda _: 99) + + pag = channel._MessagePaginator(channel_obj, direction, "12345", mock_components, mock_session) + pag._first_id = "12345" + + await pag._next_chunk() + + assert pag._first_id == "1234" if expect_reverse else "512" + + @pytest.mark.parametrize(["direction", "expect_reverse"], [("before", False), ("after", True), ("around", False)]) + async def test_next_chunk_returns_generator( + self, mock_session, mock_components, message_cls, expect_reverse, direction + ): + return_payload = [ + {"id": "1234", ...: ...}, + {"id": "3456", ...: ...}, + {"id": "3333", ...: ...}, + {"id": "512", ...: ...}, + ] + + @attr.s(auto_attribs=True) + class DummyResponse: + id: int + + real_values = [ + DummyResponse(1234), + DummyResponse(3456), + DummyResponse(3333), + DummyResponse(512), + ] + + if expect_reverse: + real_values.reverse() + + assert len(real_values) == len(return_payload) + + message_cls.deserialize = mock.MagicMock(side_effect=real_values.copy()) + mock_session.get_channel_messages = mock.AsyncMock(return_value=return_payload) + + channel_obj = mock.MagicMock(__int__=lambda _: 99) + + pag = channel._MessagePaginator(channel_obj, direction, "12345", mock_components, mock_session) + pag._first_id = "12345" + + generator = await pag._next_chunk() + + assert inspect.isgenerator(generator) + + for i, item in enumerate(generator, start=1): + assert item == real_values.pop(0) + + assert locals()["i"] == 4, "Not iterated correctly somehow" + assert not real_values + + # Clear the generator result. + # This doesn't test anything, but there is an issue with coverage not detecting generator + # exit conditions properly. This fixes something that would otherwise be marked as + # uncovered behaviour erroneously. + # https://stackoverflow.com/questions/35317757/python-unittest-branch-coverage-seems-to-miss-executed-generator-in-zip + with pytest.raises(StopIteration): + next(generator) + + +class TestRESTChannel: @pytest.fixture() def rest_channel_logic_impl(self): mock_components = mock.MagicMock(components.Components) diff --git a/tests/hikari/clients/test_rest/test_guild.py b/tests/hikari/clients/test_rest/test_guild.py index 0414db954d..6a3c28a4b2 100644 --- a/tests/hikari/clients/test_rest/test_guild.py +++ b/tests/hikari/clients/test_rest/test_guild.py @@ -153,6 +153,14 @@ async def test_next_chunk_deserializes_payload_in_generator_lazily(self, mock_se assert next(generator) is expected_value member_cls.deserialize.assert_called_with(input_payload, components=mock_components) + # Clear the generator result. + # This doesn't test anything, but there is an issue with coverage not detecting generator + # exit conditions properly. This fixes something that would otherwise be marked as + # uncovered behaviour erroneously. + # https://stackoverflow.com/questions/35317757/python-unittest-branch-coverage-seems-to-miss-executed-generator-in-zip + with pytest.raises(StopIteration): + next(generator) + assert locals()["i"] == len(return_payload) - 1, "Not iterated correctly somehow" diff --git a/tests/hikari/clients/test_rest/test_me.py b/tests/hikari/clients/test_rest/test_me.py index f74174aef3..6a327ea283 100644 --- a/tests/hikari/clients/test_rest/test_me.py +++ b/tests/hikari/clients/test_rest/test_me.py @@ -135,6 +135,14 @@ async def test_next_chunk_deserializes_payload_in_generator_lazily( assert next(generator) is expected_value ownguild_cls.deserialize.assert_called_with(input_payload, components=mock_components) + # Clear the generator result. + # This doesn't test anything, but there is an issue with coverage not detecting generator + # exit conditions properly. This fixes something that would otherwise be marked as + # uncovered behaviour erroneously. + # https://stackoverflow.com/questions/35317757/python-unittest-branch-coverage-seems-to-miss-executed-generator-in-zip + with pytest.raises(StopIteration): + next(generator) + assert locals()["i"] == len(return_payload) - 1, "Not iterated correctly somehow" diff --git a/tests/hikari/clients/test_rest/test_react.py b/tests/hikari/clients/test_rest/test_react.py index ba4bbf5333..9ed6930fe1 100644 --- a/tests/hikari/clients/test_rest/test_react.py +++ b/tests/hikari/clients/test_rest/test_react.py @@ -171,6 +171,14 @@ async def test_next_chunk_deserializes_payload_in_generator_lazily( assert next(generator) is expected_value user_cls.deserialize.assert_called_with(input_payload, components=mock_components) + # Clear the generator result. + # This doesn't test anything, but there is an issue with coverage not detecting generator + # exit conditions properly. This fixes something that would otherwise be marked as + # uncovered behaviour erroneously. + # https://stackoverflow.com/questions/35317757/python-unittest-branch-coverage-seems-to-miss-executed-generator-in-zip + with pytest.raises(StopIteration): + next(generator) + assert locals()["i"] == len(return_payload) - 1, "Not iterated correctly somehow" From 0c1be1baee0570c62a8451016108b840d022e798 Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Wed, 13 May 2020 21:01:02 +0100 Subject: [PATCH 319/922] Updated README and fixed typos in it [skip ci] --- README.md | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 0517eddb40..8742a97b46 100644 --- a/README.md +++ b/README.md @@ -28,13 +28,11 @@ async def ping(event): bot.run() ``` -_And if that is too verbose, this will be actively reduced with the -functionality provided by the Stateful bot implementation coming soon!_ - ## What does _hikari_ aim to do? - **Provide 100% documentation for the entire library.** Build your application - bottom-up or top-down with comprehensive documentation as standard. + bottom-up or top-down with comprehensive documentation as standard. Currently + more than 45% of this codebase consists of documentation. - **Ensure all components are reusable.** Most people want a basic framework for writing a bot, and _hikari_ will provide that. However, if you decide on a bespoke solution using custom components, such as a _Redis_ state cache, or @@ -62,8 +60,9 @@ ways. - Modular, reusable components. - Extensive documentation. -- Full type-checking. +- Support for using type hints to infer event types. - Minimal dependencies. +- Rapidly evolving codebase. - Full unit test suite. ### Network level components @@ -80,9 +79,10 @@ to utilize these components as a black box where necessary. - Low level gateway websocket shard implementation. - Rate limiting that complies with the `X-RateLimit-Bucket` header __properly__. - Gateway websocket ratelimiting (prevents your websocket getting completely invalidated). -- Intents +- Intents. - Proxy support for websockets and REST API. - File IO that doesn't block you. +- Fluent Pythonic API that does not limit your creativity. ### High level components @@ -95,7 +95,7 @@ to utilize these components as a black box where necessary. changes are required when a breaking API design is introduced. This reduces the amount of stuff you need to fix in your applications as a result. - REST only API functionality. Want to write a web dashboard? Feel free to just reuse the - REST client components to achive that! + REST client components to achieve that! ### Stuff coming soon @@ -112,7 +112,6 @@ to utilize these components as a black box where necessary. - Full voice transcoding support, natively in your application. Do not rely on invoking ffmpeg in a subprocess ever again! - ## Getting started This section is still very bare, and we are still actively writing this framework every day. From a9f07f5853db5a58c50ef6c2adf6c8274acee57f Mon Sep 17 00:00:00 2001 From: Faster Speeding Date: Wed, 13 May 2020 21:57:01 +0100 Subject: [PATCH 320/922] Add explicit attrs eq and hash for entities. This splits entities into 3 categories * Unique entities that compare by ID(s) and are hashable. * General entities that compare by all their fields (i.e. all their fields have to be the same for an equality check to return True) * Event entities that use default comparability (must be the same instance for an equality check to return True) and aren't hashable --- docs/credits.mako | 6 +- hikari/applications.py | 105 +++++---- hikari/audit_logs.py | 45 ++-- hikari/bases.py | 6 +- hikari/channels.py | 80 ++++--- hikari/embeds.py | 16 +- hikari/emojis.py | 39 ++-- hikari/events/base.py | 2 +- hikari/events/channel.py | 22 +- hikari/events/guild.py | 32 +-- hikari/events/message.py | 16 +- hikari/events/other.py | 20 +- hikari/gateway_entities.py | 6 +- hikari/guilds.py | 306 +++++++++++++++++---------- hikari/invites.py | 64 +++--- hikari/messages.py | 100 ++++++--- hikari/state/stateless.py | 10 + hikari/users.py | 40 ++-- hikari/voices.py | 40 ++-- hikari/webhooks.py | 26 ++- tests/hikari/clients/test_configs.py | 30 ++- tests/hikari/state/test_stateless.py | 44 +++- 22 files changed, 663 insertions(+), 392 deletions(-) diff --git a/docs/credits.mako b/docs/credits.mako index 395245cfcb..e66e0b4cb3 100644 --- a/docs/credits.mako +++ b/docs/credits.mako @@ -14,6 +14,8 @@ ## ## You should have received a copy of the GNU Lesser General Public License ## along with Hikari. If not, see . - +<%! + import hikari +%>

Licensed under GNU LGPLv3.

-

Copyright © Nekokatt 2019-2020.

+

${hikari.__copyright__}.

diff --git a/hikari/applications.py b/hikari/applications.py index a72d087ccd..81baa24768 100644 --- a/hikari/applications.py +++ b/hikari/applications.py @@ -190,57 +190,66 @@ def _deserialize_integrations( @marshaller.marshallable() -@attr.s(slots=True, kw_only=True) +@attr.s(eq=True, hash=True, kw_only=True, slots=True) class OwnConnection(bases.Entity, marshaller.Deserializable): """Represents a user's connection with a third party account. Returned by the `GET Current User Connections` endpoint. """ - id: str = marshaller.attrib(deserializer=str) + id: str = marshaller.attrib(deserializer=str, eq=True, hash=True, repr=True) """The string ID of the third party connected account. !!! warning Seeing as this is a third party ID, it will not be a snowflake. """ - name: str = marshaller.attrib(deserializer=str, repr=True) + name: str = marshaller.attrib(deserializer=str, eq=False, hash=False, repr=True) """The username of the connected account.""" - type: str = marshaller.attrib(deserializer=str, repr=True) + type: str = marshaller.attrib(deserializer=str, eq=False, hash=False, repr=True) """The type of service this connection is for.""" - is_revoked: bool = marshaller.attrib(raw_name="revoked", deserializer=bool, if_undefined=False, default=False) + is_revoked: bool = marshaller.attrib( + raw_name="revoked", deserializer=bool, if_undefined=False, default=False, eq=False, hash=False, + ) """Whether the connection has been revoked.""" integrations: typing.Sequence[guilds.PartialGuildIntegration] = marshaller.attrib( - deserializer=_deserialize_integrations, if_undefined=list, factory=list, inherit_kwargs=True, + deserializer=_deserialize_integrations, + if_undefined=list, + factory=list, + inherit_kwargs=True, + eq=False, + hash=False, ) """A sequence of the partial guild integration objects this connection has.""" - is_verified: bool = marshaller.attrib(raw_name="verified", deserializer=bool) + is_verified: bool = marshaller.attrib(raw_name="verified", deserializer=bool, eq=False, hash=False) """Whether the connection has been verified.""" - is_friend_syncing: bool = marshaller.attrib(raw_name="friend_sync", deserializer=bool) + is_friend_syncing: bool = marshaller.attrib(raw_name="friend_sync", deserializer=bool, eq=False, hash=False) """Whether friends should be added based on this connection.""" - is_showing_activity: bool = marshaller.attrib(raw_name="show_activity", deserializer=bool) + is_showing_activity: bool = marshaller.attrib(raw_name="show_activity", deserializer=bool, eq=False, hash=False) """Whether this connection's activities are shown in the user's presence.""" - visibility: ConnectionVisibility = marshaller.attrib(deserializer=ConnectionVisibility, repr=True) + visibility: ConnectionVisibility = marshaller.attrib( + deserializer=ConnectionVisibility, eq=False, hash=False, repr=True + ) """The visibility of the connection.""" @marshaller.marshallable() -@attr.s(slots=True, kw_only=True) +@attr.s(eq=True, hash=True, kw_only=True, slots=True) class OwnGuild(guilds.PartialGuild): """Represents a user bound partial guild object.""" - is_owner: bool = marshaller.attrib(raw_name="owner", deserializer=bool, repr=True) + is_owner: bool = marshaller.attrib(raw_name="owner", deserializer=bool, eq=False, hash=False, repr=True) """Whether the current user owns this guild.""" my_permissions: permissions.Permission = marshaller.attrib( - raw_name="permissions", deserializer=permissions.Permission + raw_name="permissions", deserializer=permissions.Permission, eq=False, hash=False ) """The guild level permissions that apply to the current user or bot.""" @@ -257,23 +266,25 @@ class TeamMembershipState(int, more_enums.Enum): @marshaller.marshallable() -@attr.s(slots=True, kw_only=True) +@attr.s(eq=True, hash=True, kw_only=True, slots=True) class TeamMember(bases.Entity, marshaller.Deserializable): """Represents a member of a Team.""" - membership_state: TeamMembershipState = marshaller.attrib(deserializer=TeamMembershipState) + membership_state: TeamMembershipState = marshaller.attrib(deserializer=TeamMembershipState, eq=False, hash=False) """The state of this user's membership.""" - permissions: typing.Set[str] = marshaller.attrib(deserializer=set) + permissions: typing.Set[str] = marshaller.attrib(deserializer=set, eq=False, hash=False) """This member's permissions within a team. Will always be `["*"]` until Discord starts using this. """ - team_id: bases.Snowflake = marshaller.attrib(deserializer=bases.Snowflake, repr=True) + team_id: bases.Snowflake = marshaller.attrib(deserializer=bases.Snowflake, eq=True, hash=True, repr=True) """The ID of the team this member belongs to.""" - user: users.User = marshaller.attrib(deserializer=users.User.deserialize, inherit_kwargs=True, repr=True) + user: users.User = marshaller.attrib( + deserializer=users.User.deserialize, inherit_kwargs=True, eq=True, hash=True, repr=True + ) """The user object of this team member.""" @@ -284,19 +295,19 @@ def _deserialize_members( @marshaller.marshallable() -@attr.s(slots=True, kw_only=True) +@attr.s(eq=True, hash=True, kw_only=True, slots=True) class Team(bases.Unique, marshaller.Deserializable): """Represents a development team, along with all its members.""" - icon_hash: typing.Optional[str] = marshaller.attrib(raw_name="icon", deserializer=str) + icon_hash: typing.Optional[str] = marshaller.attrib(raw_name="icon", deserializer=str, eq=False, hash=False) """The hash of this team's icon, if set.""" members: typing.Mapping[bases.Snowflake, TeamMember] = marshaller.attrib( - deserializer=_deserialize_members, inherit_kwargs=True + deserializer=_deserialize_members, inherit_kwargs=True, eq=False, hash=False ) """The member's that belong to this team.""" - owner_user_id: bases.Snowflake = marshaller.attrib(deserializer=bases.Snowflake, repr=True) + owner_user_id: bases.Snowflake = marshaller.attrib(deserializer=bases.Snowflake, eq=False, hash=False, repr=True) """The ID of this team's owner.""" @property @@ -332,11 +343,11 @@ def format_icon_url(self, fmt: str = "png", size: int = 4096) -> typing.Optional @marshaller.marshallable() -@attr.s(slots=True, kw_only=True) +@attr.s(eq=True, hash=True, kw_only=True, slots=True) class ApplicationOwner(users.User): """Represents the user who owns an application, may be a team user.""" - flags: int = marshaller.attrib(deserializer=users.UserFlag, repr=True) + flags: int = marshaller.attrib(deserializer=users.UserFlag, eq=False, hash=False, repr=True) """This user's flags.""" @property @@ -350,18 +361,18 @@ def _deserialize_verify_key(payload: str) -> bytes: @marshaller.marshallable() -@attr.s(slots=True, kw_only=True) +@attr.s(eq=True, hash=True, kw_only=True, slots=True) class Application(bases.Unique, marshaller.Deserializable): """Represents the information of an Oauth2 Application.""" - name: str = marshaller.attrib(deserializer=str, repr=True) + name: str = marshaller.attrib(deserializer=str, eq=False, hash=False, repr=True) """The name of this application.""" - description: str = marshaller.attrib(deserializer=str) + description: str = marshaller.attrib(deserializer=str, eq=False, hash=False) """The description of this application, will be an empty string if unset.""" is_bot_public: typing.Optional[bool] = marshaller.attrib( - raw_name="bot_public", deserializer=bool, if_undefined=None, default=None, repr=True + raw_name="bot_public", deserializer=bool, if_undefined=None, default=None, eq=False, hash=False, repr=True ) """Whether the bot associated with this application is public. @@ -369,7 +380,7 @@ class Application(bases.Unique, marshaller.Deserializable): """ is_bot_code_grant_required: typing.Optional[bool] = marshaller.attrib( - raw_name="bot_require_code_grant", deserializer=bool, if_undefined=None, default=None + raw_name="bot_require_code_grant", deserializer=bool, if_undefined=None, default=None, eq=False, hash=False ) """Whether this application's bot is requiring code grant for invites. @@ -377,7 +388,13 @@ class Application(bases.Unique, marshaller.Deserializable): """ owner: typing.Optional[ApplicationOwner] = marshaller.attrib( - deserializer=ApplicationOwner.deserialize, if_undefined=None, default=None, inherit_kwargs=True, repr=True + deserializer=ApplicationOwner.deserialize, + if_undefined=None, + default=None, + inherit_kwargs=True, + eq=False, + hash=False, + repr=True, ) """The object of this application's owner. @@ -385,48 +402,58 @@ class Application(bases.Unique, marshaller.Deserializable): Discord's oauth2 flow. """ - rpc_origins: typing.Optional[typing.Set[str]] = marshaller.attrib(deserializer=set, if_undefined=None, default=None) + rpc_origins: typing.Optional[typing.Set[str]] = marshaller.attrib( + deserializer=set, if_undefined=None, default=None, eq=False, hash=False + ) """A collection of this application's rpc origin URLs, if rpc is enabled.""" - summary: str = marshaller.attrib(deserializer=str) + summary: str = marshaller.attrib(deserializer=str, eq=False, hash=False) """This summary for this application's primary SKU if it's sold on Discord. Will be an empty string if unset. """ verify_key: typing.Optional[bytes] = marshaller.attrib( - deserializer=_deserialize_verify_key, if_undefined=None, default=None + deserializer=_deserialize_verify_key, if_undefined=None, default=None, eq=False, hash=False ) """The base64 encoded key used for the GameSDK's `GetTicket`.""" icon_hash: typing.Optional[str] = marshaller.attrib( - raw_name="icon", deserializer=str, if_undefined=None, default=None + raw_name="icon", deserializer=str, if_undefined=None, default=None, eq=False, hash=False ) """The hash of this application's icon, if set.""" team: typing.Optional[Team] = marshaller.attrib( - deserializer=Team.deserialize, if_undefined=None, if_none=None, default=None, inherit_kwargs=True + deserializer=Team.deserialize, + if_undefined=None, + if_none=None, + default=None, + eq=False, + hash=False, + inherit_kwargs=True, ) """This application's team if it belongs to one.""" guild_id: typing.Optional[bases.Snowflake] = marshaller.attrib( - deserializer=bases.Snowflake, if_undefined=None, default=None + deserializer=bases.Snowflake, if_undefined=None, default=None, eq=False, hash=False ) """The ID of the guild this application is linked to if sold on Discord.""" primary_sku_id: typing.Optional[bases.Snowflake] = marshaller.attrib( - deserializer=bases.Snowflake, if_undefined=None, default=None + deserializer=bases.Snowflake, if_undefined=None, default=None, eq=False, hash=False ) """The ID of the primary "Game SKU" of a game that's sold on Discord.""" - slug: typing.Optional[str] = marshaller.attrib(deserializer=str, if_undefined=None, default=None) + slug: typing.Optional[str] = marshaller.attrib( + deserializer=str, if_undefined=None, default=None, eq=False, hash=False + ) """The URL slug that links to this application's store page. Only applicable to applications sold on Discord. """ cover_image_hash: typing.Optional[str] = marshaller.attrib( - raw_name="cover_image", deserializer=str, if_undefined=None, default=None + raw_name="cover_image", deserializer=str, if_undefined=None, default=None, eq=False, hash=False ) """The hash of this application's cover image on it's store, if set.""" diff --git a/hikari/audit_logs.py b/hikari/audit_logs.py index 28e025d053..50fe7ab4f5 100644 --- a/hikari/audit_logs.py +++ b/hikari/audit_logs.py @@ -197,17 +197,17 @@ def _deserialize_max_age(seconds: int) -> typing.Optional[datetime.timedelta]: @marshaller.marshallable() -@attr.s(slots=True, kw_only=True) +@attr.s(eq=True, hash=False, kw_only=True, slots=True) class AuditLogChange(bases.Entity, marshaller.Deserializable): """Represents a change made to an audit log entry's target entity.""" - new_value: typing.Optional[typing.Any] = attr.attrib() + new_value: typing.Optional[typing.Any] = attr.attrib(repr=True) """The new value of the key, if something was added or changed.""" - old_value: typing.Optional[typing.Any] = attr.attrib() + old_value: typing.Optional[typing.Any] = attr.attrib(repr=True) """The old value of the key, if something was removed or changed.""" - key: typing.Union[AuditLogChangeKey, str] = attr.attrib() + key: typing.Union[AuditLogChangeKey, str] = attr.attrib(repr=True) """The name of the audit log change's key.""" @classmethod @@ -300,7 +300,7 @@ def decorator(cls): @marshaller.marshallable() -@attr.s(slots=True, kw_only=True) +@attr.s(eq=True, hash=False, kw_only=True, slots=True) class BaseAuditLogEntryInfo(bases.Entity, marshaller.Deserializable, abc.ABC): """A base object that all audit log entry info objects will inherit from.""" @@ -311,17 +311,14 @@ class BaseAuditLogEntryInfo(bases.Entity, marshaller.Deserializable, abc.ABC): AuditLogEventType.CHANNEL_OVERWRITE_DELETE, ) @marshaller.marshallable() -@attr.s(slots=True, kw_only=True) -class ChannelOverwriteEntryInfo(BaseAuditLogEntryInfo): +@attr.s(eq=True, hash=False, kw_only=True, slots=True) +class ChannelOverwriteEntryInfo(BaseAuditLogEntryInfo, bases.Unique): """Represents the extra information for overwrite related audit log entries. Will be attached to the overwrite create, update and delete audit log entries. """ - id: bases.Snowflake = marshaller.attrib(deserializer=bases.Snowflake) - """The ID of the overwrite being updated, added or removed.""" - type: channels.PermissionOverwriteType = marshaller.attrib(deserializer=channels.PermissionOverwriteType) """The type of entity this overwrite targets.""" @@ -331,7 +328,7 @@ class ChannelOverwriteEntryInfo(BaseAuditLogEntryInfo): @register_audit_log_entry_info(AuditLogEventType.MESSAGE_PIN, AuditLogEventType.MESSAGE_UNPIN) @marshaller.marshallable() -@attr.s(slots=True, kw_only=True) +@attr.s(eq=True, hash=False, kw_only=True, slots=True) class MessagePinEntryInfo(BaseAuditLogEntryInfo): """The extra information for message pin related audit log entries. @@ -347,7 +344,7 @@ class MessagePinEntryInfo(BaseAuditLogEntryInfo): @register_audit_log_entry_info(AuditLogEventType.MEMBER_PRUNE) @marshaller.marshallable() -@attr.s(slots=True, kw_only=True) +@attr.s(eq=True, hash=False, kw_only=True, slots=True) class MemberPruneEntryInfo(BaseAuditLogEntryInfo): """Represents the extra information attached to guild prune log entries.""" @@ -360,7 +357,7 @@ class MemberPruneEntryInfo(BaseAuditLogEntryInfo): @register_audit_log_entry_info(AuditLogEventType.MESSAGE_BULK_DELETE) @marshaller.marshallable() -@attr.s(slots=True, kw_only=True) +@attr.s(eq=True, hash=False, kw_only=True, slots=True) class MessageBulkDeleteEntryInfo(BaseAuditLogEntryInfo): """Represents extra information for the message bulk delete audit entry.""" @@ -370,7 +367,7 @@ class MessageBulkDeleteEntryInfo(BaseAuditLogEntryInfo): @register_audit_log_entry_info(AuditLogEventType.MESSAGE_DELETE) @marshaller.marshallable() -@attr.s(slots=True, kw_only=True) +@attr.s(eq=True, hash=False, kw_only=True, slots=True) class MessageDeleteEntryInfo(MessageBulkDeleteEntryInfo): """Represents extra information attached to the message delete audit entry.""" @@ -380,7 +377,7 @@ class MessageDeleteEntryInfo(MessageBulkDeleteEntryInfo): @register_audit_log_entry_info(AuditLogEventType.MEMBER_DISCONNECT) @marshaller.marshallable() -@attr.s(slots=True, kw_only=True) +@attr.s(eq=True, hash=False, kw_only=True, slots=True) class MemberDisconnectEntryInfo(BaseAuditLogEntryInfo): """Represents extra information for the voice chat member disconnect entry.""" @@ -390,7 +387,7 @@ class MemberDisconnectEntryInfo(BaseAuditLogEntryInfo): @register_audit_log_entry_info(AuditLogEventType.MEMBER_MOVE) @marshaller.marshallable() -@attr.s(slots=True, kw_only=True) +@attr.s(eq=True, hash=False, kw_only=True, slots=True) class MemberMoveEntryInfo(MemberDisconnectEntryInfo): """Represents extra information for the voice chat based member move entry.""" @@ -434,26 +431,26 @@ def get_entry_info_entity(type_: int) -> typing.Type[BaseAuditLogEntryInfo]: @marshaller.marshallable() -@attr.s(slots=True, kw_only=True) +@attr.s(eq=True, hash=True, kw_only=True, slots=True) class AuditLogEntry(bases.Unique, marshaller.Deserializable): """Represents an entry in a guild's audit log.""" - target_id: typing.Optional[bases.Snowflake] = attr.attrib() + target_id: typing.Optional[bases.Snowflake] = attr.attrib(eq=False, hash=False) """The ID of the entity affected by this change, if applicable.""" - changes: typing.Sequence[AuditLogChange] = attr.attrib(repr=False) + changes: typing.Sequence[AuditLogChange] = attr.attrib(eq=False, hash=False, repr=False) """A sequence of the changes made to `AuditLogEntry.target_id`.""" - user_id: bases.Snowflake = attr.attrib() + user_id: bases.Snowflake = attr.attrib(eq=False, hash=False) """The ID of the user who made this change.""" - action_type: typing.Union[AuditLogEventType, str] = attr.attrib() + action_type: typing.Union[AuditLogEventType, str] = attr.attrib(eq=False, hash=False) """The type of action this entry represents.""" - options: typing.Optional[BaseAuditLogEntryInfo] = attr.attrib(repr=False) + options: typing.Optional[BaseAuditLogEntryInfo] = attr.attrib(eq=False, hash=False, repr=False) """Extra information about this entry. Only be provided for certain `action_type`.""" - reason: typing.Optional[str] = attr.attrib(repr=False) + reason: typing.Optional[str] = attr.attrib(eq=False, hash=False, repr=False) """The reason for this change, if set (between 0-512 characters).""" @classmethod @@ -511,7 +508,7 @@ def _deserialize_webhooks( @marshaller.marshallable() -@attr.s(slots=True, kw_only=True) +@attr.s(eq=True, repr=False, kw_only=True, slots=True) class AuditLog(bases.Entity, marshaller.Deserializable): """Represents a guilds audit log.""" diff --git a/hikari/bases.py b/hikari/bases.py index 692e273099..83a8efb5d1 100644 --- a/hikari/bases.py +++ b/hikari/bases.py @@ -37,7 +37,7 @@ @marshaller.marshallable() -@attr.s(slots=True, kw_only=True, init=False) +@attr.s(eq=True, hash=False, init=False, kw_only=True, slots=True) class Entity(abc.ABC): """The base for any entity used in this API.""" @@ -45,7 +45,7 @@ class Entity(abc.ABC): """The client components that models may use for procedures.""" -class Snowflake(int): +class Snowflake(int, marshaller.Serializable): """A concrete representation of a unique identifier for an object on Discord. This object can be treated as a regular `int` for most purposes. @@ -111,7 +111,7 @@ def max(cls) -> Snowflake: @marshaller.marshallable() -@attr.s(slots=True, kw_only=True) +@attr.s(eq=True, hash=True, kw_only=True, slots=True) class Unique(Entity, typing.SupportsInt, abc.ABC): """A base for an entity that has an integer ID of some sort. diff --git a/hikari/channels.py b/hikari/channels.py index ca2e4f94dd..8e443cb1bc 100644 --- a/hikari/channels.py +++ b/hikari/channels.py @@ -94,20 +94,22 @@ def __str__(self) -> str: @marshaller.marshallable() -@attr.s(slots=True, kw_only=True) +@attr.s(eq=True, hash=True, kw_only=True, slots=True) class PermissionOverwrite(bases.Unique, marshaller.Deserializable, marshaller.Serializable): """Represents permission overwrites for a channel or role in a channel.""" - type: PermissionOverwriteType = marshaller.attrib(deserializer=PermissionOverwriteType, serializer=str) + type: PermissionOverwriteType = marshaller.attrib( + deserializer=PermissionOverwriteType, serializer=str, eq=True, hash=True + ) """The type of entity this overwrite targets.""" allow: permissions.Permission = marshaller.attrib( - deserializer=permissions.Permission, serializer=int, default=permissions.Permission(0) + deserializer=permissions.Permission, serializer=int, default=permissions.Permission(0), eq=False, hash=False ) """The permissions this overwrite allows.""" deny: permissions.Permission = marshaller.attrib( - deserializer=permissions.Permission, serializer=int, default=permissions.Permission(0) + deserializer=permissions.Permission, serializer=int, default=permissions.Permission(0), eq=False, hash=False ) """The permissions this overwrite denies.""" @@ -145,7 +147,7 @@ def decorator(cls): @marshaller.marshallable() -@attr.s(slots=True, kw_only=True) +@attr.s(eq=True, hash=True, kw_only=True, slots=True) class PartialChannel(bases.Unique, marshaller.Deserializable): """Represents a channel where we've only received it's basic information. @@ -153,11 +155,11 @@ class PartialChannel(bases.Unique, marshaller.Deserializable): """ name: typing.Optional[str] = marshaller.attrib( - deserializer=str, repr=True, default=None, if_undefined=None, if_none=None + deserializer=str, if_undefined=None, if_none=None, default=None, eq=False, hash=False, repr=True ) """The channel's name. This will be missing for DM channels.""" - type: ChannelType = marshaller.attrib(deserializer=ChannelType, repr=True) + type: ChannelType = marshaller.attrib(deserializer=ChannelType, eq=False, hash=False, repr=True) """The channel's type.""" @@ -167,11 +169,13 @@ def _deserialize_recipients(payload: more_typing.JSONArray, **kwargs: typing.Any @register_channel_type(ChannelType.DM) @marshaller.marshallable() -@attr.s(slots=True, kw_only=True) +@attr.s(eq=True, hash=True, kw_only=True, slots=True) class DMChannel(PartialChannel): """Represents a DM channel.""" - last_message_id: typing.Optional[bases.Snowflake] = marshaller.attrib(deserializer=bases.Snowflake, if_none=None) + last_message_id: typing.Optional[bases.Snowflake] = marshaller.attrib( + deserializer=bases.Snowflake, if_none=None, eq=False, hash=False + ) """The ID of the last message sent in this channel. !!! note @@ -179,25 +183,27 @@ class DMChannel(PartialChannel): """ recipients: typing.Mapping[bases.Snowflake, users.User] = marshaller.attrib( - deserializer=_deserialize_recipients, inherit_kwargs=True, + deserializer=_deserialize_recipients, inherit_kwargs=True, eq=False, hash=False, ) """The recipients of the DM.""" @register_channel_type(ChannelType.GROUP_DM) @marshaller.marshallable() -@attr.s(slots=True, kw_only=True) +@attr.s(eq=True, hash=True, kw_only=True, slots=True) class GroupDMChannel(DMChannel): """Represents a DM group channel.""" - owner_id: bases.Snowflake = marshaller.attrib(deserializer=bases.Snowflake, repr=True) + owner_id: bases.Snowflake = marshaller.attrib(deserializer=bases.Snowflake, eq=False, hash=False, repr=True) """The ID of the owner of the group.""" - icon_hash: typing.Optional[str] = marshaller.attrib(raw_name="icon", deserializer=str, if_none=None) + icon_hash: typing.Optional[str] = marshaller.attrib( + raw_name="icon", deserializer=str, if_none=None, eq=False, hash=False + ) """The hash of the icon of the group.""" application_id: typing.Optional[bases.Snowflake] = marshaller.attrib( - deserializer=bases.Snowflake, if_undefined=None, default=None + deserializer=bases.Snowflake, if_undefined=None, default=None, eq=False, hash=False ) """The ID of the application that created the group DM, if it's a bot based group DM.""" @@ -211,12 +217,12 @@ def _deserialize_overwrites( @marshaller.marshallable() -@attr.s(slots=True, kw_only=True) +@attr.s(eq=True, hash=True, kw_only=True, slots=True) class GuildChannel(PartialChannel): """The base for anything that is a guild channel.""" guild_id: typing.Optional[bases.Snowflake] = marshaller.attrib( - deserializer=bases.Snowflake, if_undefined=None, default=None, repr=True + deserializer=bases.Snowflake, if_undefined=None, default=None, eq=False, hash=False, repr=True ) """The ID of the guild the channel belongs to. @@ -224,16 +230,16 @@ class GuildChannel(PartialChannel): Guild Create). """ - position: int = marshaller.attrib(deserializer=int) + position: int = marshaller.attrib(deserializer=int, eq=False, hash=False) """The sorting position of the channel.""" permission_overwrites: PermissionOverwrite = marshaller.attrib( - deserializer=_deserialize_overwrites, inherit_kwargs=True + deserializer=_deserialize_overwrites, inherit_kwargs=True, eq=False, hash=False ) """The permission overwrites for the channel.""" is_nsfw: typing.Optional[bool] = marshaller.attrib( - raw_name="nsfw", deserializer=bool, if_undefined=None, default=None + raw_name="nsfw", deserializer=bool, if_undefined=None, default=None, eq=False, hash=False ) """Whether the channel is marked as NSFW. @@ -242,14 +248,14 @@ class GuildChannel(PartialChannel): """ parent_id: typing.Optional[bases.Snowflake] = marshaller.attrib( - deserializer=bases.Snowflake, if_none=None, if_undefined=None, repr=True + deserializer=bases.Snowflake, if_none=None, if_undefined=None, eq=False, hash=False, repr=True ) """The ID of the parent category the channel belongs to.""" @register_channel_type(ChannelType.GUILD_CATEGORY) @marshaller.marshallable() -@attr.s(slots=True, kw_only=True) +@attr.s(eq=True, hash=True, kw_only=True, slots=True) class GuildCategory(GuildChannel): """Represents a guild category.""" @@ -260,21 +266,25 @@ def _deserialize_rate_limit_per_user(payload: int) -> datetime.timedelta: @register_channel_type(ChannelType.GUILD_TEXT) @marshaller.marshallable() -@attr.s(slots=True, kw_only=True) +@attr.s(eq=True, hash=True, kw_only=True, slots=True) class GuildTextChannel(GuildChannel): """Represents a guild text channel.""" - topic: typing.Optional[str] = marshaller.attrib(deserializer=str, if_none=None) + topic: typing.Optional[str] = marshaller.attrib(deserializer=str, if_none=None, eq=False, hash=False) """The topic of the channel.""" - last_message_id: typing.Optional[bases.Snowflake] = marshaller.attrib(deserializer=bases.Snowflake, if_none=None) + last_message_id: typing.Optional[bases.Snowflake] = marshaller.attrib( + deserializer=bases.Snowflake, if_none=None, eq=False, hash=False + ) """The ID of the last message sent in this channel. !!! note This might point to an invalid or deleted message. """ - rate_limit_per_user: datetime.timedelta = marshaller.attrib(deserializer=_deserialize_rate_limit_per_user) + rate_limit_per_user: datetime.timedelta = marshaller.attrib( + deserializer=_deserialize_rate_limit_per_user, eq=False, hash=False + ) """The delay (in seconds) between a user can send a message to this channel. !!! note @@ -283,7 +293,7 @@ class GuildTextChannel(GuildChannel): """ last_pin_timestamp: typing.Optional[datetime.datetime] = marshaller.attrib( - deserializer=conversions.parse_iso_8601_ts, if_none=None, if_undefined=None + deserializer=conversions.parse_iso_8601_ts, if_none=None, if_undefined=None, eq=False, hash=False ) """The timestamp of the last-pinned message. @@ -294,14 +304,16 @@ class GuildTextChannel(GuildChannel): @register_channel_type(ChannelType.GUILD_NEWS) @marshaller.marshallable() -@attr.s(slots=True, kw_only=True) +@attr.s(eq=True, hash=True, slots=True, kw_only=True) class GuildNewsChannel(GuildChannel): """Represents an news channel.""" - topic: str = marshaller.attrib(deserializer=str, if_none=None) + topic: str = marshaller.attrib(deserializer=str, if_none=None, eq=False, hash=False) """The topic of the channel.""" - last_message_id: typing.Optional[bases.Snowflake] = marshaller.attrib(deserializer=bases.Snowflake, if_none=None) + last_message_id: typing.Optional[bases.Snowflake] = marshaller.attrib( + deserializer=bases.Snowflake, if_none=None, eq=False, hash=False + ) """The ID of the last message sent in this channel. !!! note @@ -309,7 +321,7 @@ class GuildNewsChannel(GuildChannel): """ last_pin_timestamp: typing.Optional[datetime.datetime] = marshaller.attrib( - deserializer=conversions.parse_iso_8601_ts, if_none=None, if_undefined=None + deserializer=conversions.parse_iso_8601_ts, if_none=None, if_undefined=None, eq=False, hash=False ) """The timestamp of the last-pinned message. @@ -320,21 +332,21 @@ class GuildNewsChannel(GuildChannel): @register_channel_type(ChannelType.GUILD_STORE) @marshaller.marshallable() -@attr.s(slots=True, kw_only=True) +@attr.s(eq=True, hash=True, kw_only=True, slots=True) class GuildStoreChannel(GuildChannel): """Represents a store channel.""" @register_channel_type(ChannelType.GUILD_VOICE) @marshaller.marshallable() -@attr.s(slots=True, kw_only=True) +@attr.s(eq=True, hash=True, kw_only=True, slots=True) class GuildVoiceChannel(GuildChannel): """Represents an voice channel.""" - bitrate: int = marshaller.attrib(deserializer=int, repr=True) + bitrate: int = marshaller.attrib(deserializer=int, eq=False, hash=False, repr=True) """The bitrate for the voice channel (in bits).""" - user_limit: int = marshaller.attrib(deserializer=int, repr=True) + user_limit: int = marshaller.attrib(deserializer=int, eq=False, hash=False, repr=True) """The user limit for the voice channel.""" diff --git a/hikari/embeds.py b/hikari/embeds.py index 0f37bc04a4..7f0ad52483 100644 --- a/hikari/embeds.py +++ b/hikari/embeds.py @@ -56,7 +56,7 @@ @marshaller.marshallable() -@attr.s(slots=True, kw_only=True) +@attr.s(eq=True, hash=False, kw_only=True, slots=True) class EmbedFooter(bases.Entity, marshaller.Deserializable, marshaller.Serializable): """Represents an embed footer.""" @@ -80,7 +80,7 @@ class EmbedFooter(bases.Entity, marshaller.Deserializable, marshaller.Serializab @marshaller.marshallable() -@attr.s(slots=True, kw_only=True) +@attr.s(eq=True, hash=False, kw_only=True, slots=True) class EmbedImage(bases.Entity, marshaller.Deserializable, marshaller.Serializable): """Represents an embed image.""" @@ -117,7 +117,7 @@ class EmbedImage(bases.Entity, marshaller.Deserializable, marshaller.Serializabl @marshaller.marshallable() -@attr.s(slots=True, kw_only=True) +@attr.s(eq=True, hash=False, kw_only=True, slots=True) class EmbedThumbnail(bases.Entity, marshaller.Deserializable, marshaller.Serializable): """Represents an embed thumbnail.""" @@ -154,7 +154,7 @@ class EmbedThumbnail(bases.Entity, marshaller.Deserializable, marshaller.Seriali @marshaller.marshallable() -@attr.s(slots=True, kw_only=True) +@attr.s(eq=True, hash=False, kw_only=True, slots=True) class EmbedVideo(bases.Entity, marshaller.Deserializable): """Represents an embed video. @@ -175,7 +175,7 @@ class EmbedVideo(bases.Entity, marshaller.Deserializable): @marshaller.marshallable() -@attr.s(slots=True, kw_only=True) +@attr.s(eq=True, hash=False, kw_only=True, slots=True) class EmbedProvider(bases.Entity, marshaller.Deserializable): """Represents an embed provider. @@ -195,7 +195,7 @@ class EmbedProvider(bases.Entity, marshaller.Deserializable): @marshaller.marshallable() -@attr.s(slots=True, kw_only=True) +@attr.s(eq=True, hash=False, kw_only=True, slots=True) class EmbedAuthor(bases.Entity, marshaller.Deserializable, marshaller.Serializable): """Represents an embed author.""" @@ -226,7 +226,7 @@ class EmbedAuthor(bases.Entity, marshaller.Deserializable, marshaller.Serializab @marshaller.marshallable() -@attr.s(slots=True, kw_only=True) +@attr.s(eq=True, hash=False, kw_only=True, slots=True) class EmbedField(bases.Entity, marshaller.Deserializable, marshaller.Serializable): """Represents a field in a embed.""" @@ -255,7 +255,7 @@ def _serialize_fields(fields: typing.Sequence[EmbedField]) -> more_typing.JSONAr @marshaller.marshallable() -@attr.s(slots=True, kw_only=True) +@attr.s(eq=True, hash=False, kw_only=True, slots=True) class Embed(bases.Entity, marshaller.Deserializable, marshaller.Serializable): """Represents an embed.""" diff --git a/hikari/emojis.py b/hikari/emojis.py index 817c03cb78..3f825be202 100644 --- a/hikari/emojis.py +++ b/hikari/emojis.py @@ -39,7 +39,7 @@ @marshaller.marshallable() -@attr.s(slots=True, kw_only=True) +@attr.s(eq=True, hash=True, kw_only=True, slots=True) class Emoji(bases.Entity, marshaller.Deserializable, files.BaseStream, abc.ABC): """Base class for all emojis. @@ -73,7 +73,7 @@ def __aiter__(self) -> typing.AsyncIterator[bytes]: @marshaller.marshallable() -@attr.s(slots=True, kw_only=True) +@attr.s(eq=True, hash=True, kw_only=True, slots=True) class UnicodeEmoji(Emoji): """Represents a unicode emoji. @@ -93,7 +93,7 @@ class UnicodeEmoji(Emoji): removed in a future release after a deprecation period. """ - name: str = marshaller.attrib(deserializer=str, repr=True) + name: str = marshaller.attrib(deserializer=str, eq=True, hash=True, repr=True) """The code points that form the emoji.""" @property @@ -176,7 +176,7 @@ def from_unicode_escape(cls, escape: str) -> UnicodeEmoji: @marshaller.marshallable() -@attr.s(slots=True, kw_only=True) +@attr.s(eq=True, hash=True, kw_only=True, slots=True) class CustomEmoji(Emoji, bases.Unique): """Represents a custom emoji. @@ -201,11 +201,18 @@ class CustomEmoji(Emoji, bases.Unique): https://github.com/discord/discord-api-docs/issues/1614 """ - name: typing.Optional[str] = marshaller.attrib(deserializer=str, if_none=None, repr=True) + name: typing.Optional[str] = marshaller.attrib(deserializer=str, if_none=None, eq=False, hash=False, repr=True) """The name of the emoji.""" is_animated: typing.Optional[bool] = marshaller.attrib( - raw_name="animated", deserializer=bool, if_undefined=False, if_none=None, default=False, repr=True + raw_name="animated", + deserializer=bool, + if_undefined=False, + if_none=None, + default=False, + eq=False, + hash=False, + repr=True, ) """Whether the emoji is animated. @@ -239,7 +246,7 @@ def _deserialize_role_ids(payload: more_typing.JSONArray) -> typing.Set[bases.Sn @marshaller.marshallable() -@attr.s(slots=True, kw_only=True) +@attr.s(eq=True, hash=True, kw_only=True, slots=True) class KnownCustomEmoji(CustomEmoji): """Represents an emoji that is known from a guild the bot is in. @@ -248,7 +255,7 @@ class KnownCustomEmoji(CustomEmoji): """ role_ids: typing.Set[bases.Snowflake] = marshaller.attrib( - raw_name="roles", deserializer=_deserialize_role_ids, if_undefined=set, factory=set, + raw_name="roles", deserializer=_deserialize_role_ids, if_undefined=set, eq=False, hash=False, factory=set, ) """The IDs of the roles that are whitelisted to use this emoji. @@ -256,7 +263,13 @@ class KnownCustomEmoji(CustomEmoji): """ user: typing.Optional[users.User] = marshaller.attrib( - deserializer=users.User.deserialize, if_none=None, if_undefined=None, default=None, inherit_kwargs=True + deserializer=users.User.deserialize, + if_none=None, + if_undefined=None, + inherit_kwargs=True, + default=None, + eq=False, + hash=False, ) """The user that created the emoji. @@ -266,20 +279,20 @@ class KnownCustomEmoji(CustomEmoji): """ is_animated: bool = marshaller.attrib( - raw_name="animated", deserializer=bool, if_undefined=False, default=False, repr=True + raw_name="animated", deserializer=bool, if_undefined=False, default=False, eq=False, hash=False, repr=True ) """Whether the emoji is animated. Unlike in `CustomEmoji`, this information is always known, and will thus never be `None`. """ - is_colons_required: bool = marshaller.attrib(raw_name="require_colons", deserializer=bool) + is_colons_required: bool = marshaller.attrib(raw_name="require_colons", deserializer=bool, eq=False, hash=False) """Whether this emoji must be wrapped in colons.""" - is_managed: bool = marshaller.attrib(raw_name="managed", deserializer=bool) + is_managed: bool = marshaller.attrib(raw_name="managed", deserializer=bool, eq=False, hash=False) """Whether the emoji is managed by an integration.""" - is_available: bool = marshaller.attrib(raw_name="available", deserializer=bool) + is_available: bool = marshaller.attrib(raw_name="available", deserializer=bool, eq=False, hash=False) """Whether this emoji can currently be used. May be `False` due to a loss of Sever Boosts on the emoji's guild. diff --git a/hikari/events/base.py b/hikari/events/base.py index 50902acdcd..06e5e6567f 100644 --- a/hikari/events/base.py +++ b/hikari/events/base.py @@ -36,7 +36,7 @@ # Base event, is not deserialized @marshaller.marshallable() -@attr.s(slots=True, kw_only=True) +@attr.s(eq=False, hash=False, kw_only=True, slots=True) class HikariEvent(bases.Entity, abc.ABC): """The base class that all events inherit from.""" diff --git a/hikari/events/channel.py b/hikari/events/channel.py index 7e0bdd18dc..ec7c653da3 100644 --- a/hikari/events/channel.py +++ b/hikari/events/channel.py @@ -75,7 +75,7 @@ def _recipients_deserializer( @base_events.requires_intents(intents.Intent.GUILDS) @marshaller.marshallable() -@attr.s(slots=True, kw_only=True) +@attr.s(eq=False, hash=False, kw_only=True, slots=True) class BaseChannelEvent(base_events.HikariEvent, base_entities.Unique, marshaller.Deserializable, abc.ABC): """A base object that Channel events will inherit from.""" @@ -164,7 +164,7 @@ class BaseChannelEvent(base_events.HikariEvent, base_entities.Unique, marshaller @base_events.requires_intents(intents.Intent.GUILDS) @marshaller.marshallable() -@attr.s(slots=True, kw_only=True) +@attr.s(eq=False, hash=False, kw_only=True, slots=True) class ChannelCreateEvent(BaseChannelEvent): """Represents Channel Create gateway events. @@ -175,21 +175,21 @@ class ChannelCreateEvent(BaseChannelEvent): @base_events.requires_intents(intents.Intent.GUILDS) @marshaller.marshallable() -@attr.s(slots=True, kw_only=True) +@attr.s(eq=False, hash=False, kw_only=True, slots=True) class ChannelUpdateEvent(BaseChannelEvent): """Represents Channel Update gateway events.""" @base_events.requires_intents(intents.Intent.GUILDS) @marshaller.marshallable() -@attr.s(slots=True, kw_only=True) +@attr.s(eq=False, hash=False, kw_only=True, slots=True) class ChannelDeleteEvent(BaseChannelEvent): """Represents Channel Delete gateway events.""" @base_events.requires_intents(intents.Intent.GUILDS) @marshaller.marshallable() -@attr.s(slots=True, kw_only=True) +@attr.s(eq=False, hash=False, kw_only=True, slots=True) class ChannelPinUpdateEvent(base_events.HikariEvent, marshaller.Deserializable): """Used to represent the Channel Pins Update gateway event. @@ -219,7 +219,7 @@ class ChannelPinUpdateEvent(base_events.HikariEvent, marshaller.Deserializable): @base_events.requires_intents(intents.Intent.GUILD_WEBHOOKS) @marshaller.marshallable() -@attr.s(slots=True, kw_only=True) +@attr.s(eq=False, hash=False, kw_only=True, slots=True) class WebhookUpdateEvent(base_events.HikariEvent, marshaller.Deserializable): """Used to represent webhook update gateway events. @@ -239,7 +239,7 @@ def _timestamp_deserializer(date: str) -> datetime.datetime: @base_events.requires_intents(intents.Intent.GUILD_MESSAGE_TYPING, intents.Intent.DIRECT_MESSAGE_TYPING) @marshaller.marshallable() -@attr.s(slots=True, kw_only=True) +@attr.s(eq=False, hash=False, kw_only=True, slots=True) class TypingStartEvent(base_events.HikariEvent, marshaller.Deserializable): """Used to represent typing start gateway events. @@ -282,7 +282,7 @@ def _max_uses_deserializer(count: int) -> typing.Union[int, float]: @base_events.requires_intents(intents.Intent.GUILD_INVITES) @marshaller.marshallable() -@attr.s(slots=True, kw_only=True) +@attr.s(eq=False, hash=False, kw_only=True, slots=True) class InviteCreateEvent(base_events.HikariEvent, marshaller.Deserializable): """Represents a gateway Invite Create event.""" @@ -339,7 +339,7 @@ class InviteCreateEvent(base_events.HikariEvent, marshaller.Deserializable): @base_events.requires_intents(intents.Intent.GUILD_INVITES) @marshaller.marshallable() -@attr.s(slots=True, kw_only=True) +@attr.s(eq=False, hash=False, kw_only=True, slots=True) class InviteDeleteEvent(base_events.HikariEvent, marshaller.Deserializable): """Used to represent Invite Delete gateway events. @@ -364,7 +364,7 @@ class InviteDeleteEvent(base_events.HikariEvent, marshaller.Deserializable): @base_events.requires_intents(intents.Intent.GUILD_VOICE_STATES) @marshaller.marshallable() -@attr.s(slots=True, kw_only=True) +@attr.s(eq=False, hash=False, kw_only=True, slots=True) class VoiceStateUpdateEvent(base_events.HikariEvent, voices.VoiceState): """Used to represent voice state update gateway events. @@ -373,7 +373,7 @@ class VoiceStateUpdateEvent(base_events.HikariEvent, voices.VoiceState): @marshaller.marshallable() -@attr.s(slots=True, kw_only=True) +@attr.s(eq=False, hash=False, kw_only=True, slots=True) class VoiceServerUpdateEvent(base_events.HikariEvent, marshaller.Deserializable): """Used to represent voice server update gateway events. diff --git a/hikari/events/guild.py b/hikari/events/guild.py index c22dd1e7a5..170fe7992b 100644 --- a/hikari/events/guild.py +++ b/hikari/events/guild.py @@ -62,7 +62,7 @@ @base_events.requires_intents(intents.Intent.GUILDS) @marshaller.marshallable() -@attr.s(slots=True, kw_only=True) +@attr.s(eq=False, hash=False, kw_only=True, slots=True) class GuildCreateEvent(base_events.HikariEvent, guilds.Guild): """Used to represent Guild Create gateway events. @@ -73,14 +73,14 @@ class GuildCreateEvent(base_events.HikariEvent, guilds.Guild): @base_events.requires_intents(intents.Intent.GUILDS) @marshaller.marshallable() -@attr.s(slots=True, kw_only=True) +@attr.s(eq=False, hash=False, kw_only=True, slots=True) class GuildUpdateEvent(base_events.HikariEvent, guilds.Guild): """Used to represent Guild Update gateway events.""" @base_events.requires_intents(intents.Intent.GUILDS) @marshaller.marshallable() -@attr.s(slots=True, kw_only=True) +@attr.s(eq=False, hash=False, kw_only=True, slots=True) class GuildLeaveEvent(base_events.HikariEvent, base_entities.Unique, marshaller.Deserializable): """Fired when the current user leaves the guild or is kicked/banned from it. @@ -91,7 +91,7 @@ class GuildLeaveEvent(base_events.HikariEvent, base_entities.Unique, marshaller. @base_events.requires_intents(intents.Intent.GUILDS) @marshaller.marshallable() -@attr.s(slots=True, kw_only=True) +@attr.s(eq=False, hash=False, kw_only=True, slots=True) class GuildUnavailableEvent(base_events.HikariEvent, base_entities.Unique, marshaller.Deserializable): """Fired when a guild becomes temporarily unavailable due to an outage. @@ -102,7 +102,7 @@ class GuildUnavailableEvent(base_events.HikariEvent, base_entities.Unique, marsh @base_events.requires_intents(intents.Intent.GUILD_BANS) @marshaller.marshallable() -@attr.s(slots=True, kw_only=True) +@attr.s(eq=False, hash=False, kw_only=True, slots=True) class BaseGuildBanEvent(base_events.HikariEvent, marshaller.Deserializable, abc.ABC): """A base object that guild ban events will inherit from.""" @@ -115,14 +115,14 @@ class BaseGuildBanEvent(base_events.HikariEvent, marshaller.Deserializable, abc. @base_events.requires_intents(intents.Intent.GUILD_BANS) @marshaller.marshallable() -@attr.s(slots=True, kw_only=True) +@attr.s(eq=False, hash=False, kw_only=True, slots=True) class GuildBanAddEvent(BaseGuildBanEvent): """Used to represent a Guild Ban Add gateway event.""" @base_events.requires_intents(intents.Intent.GUILD_BANS) @marshaller.marshallable() -@attr.s(slots=True, kw_only=True) +@attr.s(eq=False, hash=False, kw_only=True, slots=True) class GuildBanRemoveEvent(BaseGuildBanEvent): """Used to represent a Guild Ban Remove gateway event.""" @@ -137,7 +137,7 @@ def _deserialize_emojis( @base_events.requires_intents(intents.Intent.GUILD_EMOJIS) @marshaller.marshallable() -@attr.s(slots=True, kw_only=True) +@attr.s(eq=False, hash=False, kw_only=True, slots=True) class GuildEmojisUpdateEvent(base_events.HikariEvent, marshaller.Deserializable): """Represents a Guild Emoji Update gateway event.""" @@ -152,7 +152,7 @@ class GuildEmojisUpdateEvent(base_events.HikariEvent, marshaller.Deserializable) @base_events.requires_intents(intents.Intent.GUILD_INTEGRATIONS) @marshaller.marshallable() -@attr.s(slots=True, kw_only=True) +@attr.s(eq=False, hash=False, kw_only=True, slots=True) class GuildIntegrationsUpdateEvent(base_events.HikariEvent, marshaller.Deserializable): """Used to represent Guild Integration Update gateway events.""" @@ -162,7 +162,7 @@ class GuildIntegrationsUpdateEvent(base_events.HikariEvent, marshaller.Deseriali @base_events.requires_intents(intents.Intent.GUILD_MEMBERS) @marshaller.marshallable() -@attr.s(slots=True, kw_only=True) +@attr.s(eq=False, hash=False, kw_only=True, slots=True) class GuildMemberAddEvent(base_events.HikariEvent, guilds.GuildMember): """Used to represent a Guild Member Add gateway event.""" @@ -176,7 +176,7 @@ def _deserialize_role_ids(payload: more_typing.JSONArray) -> typing.Sequence[bas @base_events.requires_intents(intents.Intent.GUILD_MEMBERS) @marshaller.marshallable() -@attr.s(slots=True, kw_only=True) +@attr.s(eq=False, hash=False, kw_only=True, slots=True) class GuildMemberUpdateEvent(base_events.HikariEvent, marshaller.Deserializable): """Used to represent a Guild Member Update gateway event. @@ -214,7 +214,7 @@ class GuildMemberUpdateEvent(base_events.HikariEvent, marshaller.Deserializable) @base_events.requires_intents(intents.Intent.GUILD_MEMBERS) @marshaller.marshallable() -@attr.s(slots=True, kw_only=True) +@attr.s(eq=False, hash=False, kw_only=True, slots=True) class GuildMemberRemoveEvent(base_events.HikariEvent, marshaller.Deserializable): """Used to represent Guild Member Remove gateway events. @@ -231,7 +231,7 @@ class GuildMemberRemoveEvent(base_events.HikariEvent, marshaller.Deserializable) @base_events.requires_intents(intents.Intent.GUILDS) @marshaller.marshallable() -@attr.s(slots=True, kw_only=True) +@attr.s(eq=False, hash=False, kw_only=True, slots=True) class GuildRoleCreateEvent(base_events.HikariEvent, marshaller.Deserializable): """Used to represent a Guild Role Create gateway event.""" @@ -244,7 +244,7 @@ class GuildRoleCreateEvent(base_events.HikariEvent, marshaller.Deserializable): @base_events.requires_intents(intents.Intent.GUILDS) @marshaller.marshallable() -@attr.s(slots=True, kw_only=True) +@attr.s(eq=False, hash=False, kw_only=True, slots=True) class GuildRoleUpdateEvent(base_events.HikariEvent, marshaller.Deserializable): """Used to represent a Guild Role Create gateway event.""" @@ -261,7 +261,7 @@ class GuildRoleUpdateEvent(base_events.HikariEvent, marshaller.Deserializable): @base_events.requires_intents(intents.Intent.GUILDS) @marshaller.marshallable() -@attr.s(slots=True, kw_only=True) +@attr.s(eq=False, hash=False, kw_only=True, slots=True) class GuildRoleDeleteEvent(base_events.HikariEvent, marshaller.Deserializable): """Represents a gateway Guild Role Delete Event.""" @@ -274,7 +274,7 @@ class GuildRoleDeleteEvent(base_events.HikariEvent, marshaller.Deserializable): @base_events.requires_intents(intents.Intent.GUILD_PRESENCES) @marshaller.marshallable() -@attr.s(slots=True, kw_only=True) +@attr.s(eq=False, hash=False, kw_only=True, slots=True) class PresenceUpdateEvent(base_events.HikariEvent, guilds.GuildMemberPresence): """Used to represent Presence Update gateway events. diff --git a/hikari/events/message.py b/hikari/events/message.py index 06a3b9698e..409e145b82 100644 --- a/hikari/events/message.py +++ b/hikari/events/message.py @@ -56,7 +56,7 @@ @base_events.requires_intents(intents.Intent.GUILD_MESSAGES, intents.Intent.DIRECT_MESSAGES) @marshaller.marshallable() -@attr.s(slots=True, kw_only=True) +@attr.s(eq=False, hash=False, kw_only=True, slots=True) class MessageCreateEvent(base_events.HikariEvent, messages.Message): """Used to represent Message Create gateway events.""" @@ -86,7 +86,7 @@ def _deserialize_reaction(payload: more_typing.JSONArray, **kwargs: typing.Any) # This is an arbitrarily partial version of `messages.Message` @base_events.requires_intents(intents.Intent.GUILD_MESSAGES, intents.Intent.DIRECT_MESSAGES) @marshaller.marshallable() -@attr.s(slots=True, kw_only=True) +@attr.s(eq=False, hash=False, kw_only=True, slots=True) class MessageUpdateEvent(base_events.HikariEvent, base_entities.Unique, marshaller.Deserializable): """Represents Message Update gateway events. @@ -235,7 +235,7 @@ class MessageUpdateEvent(base_events.HikariEvent, base_entities.Unique, marshall @base_events.requires_intents(intents.Intent.GUILD_MESSAGES, intents.Intent.DIRECT_MESSAGES) @marshaller.marshallable() -@attr.s(slots=True, kw_only=True) +@attr.s(eq=False, hash=False, kw_only=True, slots=True) class MessageDeleteEvent(base_events.HikariEvent, marshaller.Deserializable): """Used to represent Message Delete gateway events. @@ -267,7 +267,7 @@ def _deserialize_message_ids(payload: more_typing.JSONArray) -> typing.Set[base_ @base_events.requires_intents(intents.Intent.GUILD_MESSAGES) @marshaller.marshallable() -@attr.s(slots=True, kw_only=True) +@attr.s(eq=False, hash=False, kw_only=True, slots=True) class MessageDeleteBulkEvent(base_events.HikariEvent, marshaller.Deserializable): """Used to represent Message Bulk Delete gateway events. @@ -293,7 +293,7 @@ class MessageDeleteBulkEvent(base_events.HikariEvent, marshaller.Deserializable) @base_events.requires_intents(intents.Intent.GUILD_MESSAGE_REACTIONS, intents.Intent.DIRECT_MESSAGE_REACTIONS) @marshaller.marshallable() -@attr.s(slots=True, kw_only=True) +@attr.s(eq=False, hash=False, kw_only=True, slots=True) class MessageReactionAddEvent(base_events.HikariEvent, marshaller.Deserializable): """Used to represent Message Reaction Add gateway events.""" @@ -333,7 +333,7 @@ class MessageReactionAddEvent(base_events.HikariEvent, marshaller.Deserializable @base_events.requires_intents(intents.Intent.GUILD_MESSAGE_REACTIONS, intents.Intent.DIRECT_MESSAGE_REACTIONS) @marshaller.marshallable() -@attr.s(slots=True, kw_only=True) +@attr.s(eq=False, hash=False, kw_only=True, slots=True) class MessageReactionRemoveEvent(base_events.HikariEvent, marshaller.Deserializable): """Used to represent Message Reaction Remove gateway events.""" @@ -362,7 +362,7 @@ class MessageReactionRemoveEvent(base_events.HikariEvent, marshaller.Deserializa @base_events.requires_intents(intents.Intent.GUILD_MESSAGE_REACTIONS, intents.Intent.DIRECT_MESSAGE_REACTIONS) @marshaller.marshallable() -@attr.s(slots=True, kw_only=True) +@attr.s(eq=False, hash=False, kw_only=True, slots=True) class MessageReactionRemoveAllEvent(base_events.HikariEvent, marshaller.Deserializable): """Used to represent Message Reaction Remove All gateway events. @@ -383,7 +383,7 @@ class MessageReactionRemoveAllEvent(base_events.HikariEvent, marshaller.Deserial @base_events.requires_intents(intents.Intent.GUILD_MESSAGE_REACTIONS, intents.Intent.DIRECT_MESSAGE_REACTIONS) @marshaller.marshallable() -@attr.s(slots=True, kw_only=True) +@attr.s(eq=False, hash=False, kw_only=True, slots=True) class MessageReactionRemoveEmojiEvent(base_events.HikariEvent, marshaller.Deserializable): """Represents Message Reaction Remove Emoji events. diff --git a/hikari/events/other.py b/hikari/events/other.py index 6dd1428c42..c068da8037 100644 --- a/hikari/events/other.py +++ b/hikari/events/other.py @@ -50,7 +50,7 @@ # Synthetic event, is not deserialized, and is produced by the dispatcher. @base_events.no_catch() -@attr.s(slots=True, auto_attribs=True) +@attr.s(auto_attribs=True, eq=False, hash=False, kw_only=True, slots=True) class ExceptionEvent(base_events.HikariEvent): """Descriptor for an exception thrown while processing an event.""" @@ -65,30 +65,30 @@ class ExceptionEvent(base_events.HikariEvent): # Synthetic event, is not deserialized -@attr.s(slots=True, auto_attribs=True) +@attr.s(auto_attribs=True, eq=False, hash=False, kw_only=True, slots=True) class StartingEvent(base_events.HikariEvent): """Event that is fired before the gateway client starts all shards.""" # Synthetic event, is not deserialized -@attr.s(slots=True, auto_attribs=True) +@attr.s(auto_attribs=True, eq=False, hash=False, kw_only=True, slots=True) class StartedEvent(base_events.HikariEvent): """Event that is fired when the gateway client starts all shards.""" # Synthetic event, is not deserialized -@attr.s(slots=True, auto_attribs=True) +@attr.s(auto_attribs=True, eq=False, hash=False, kw_only=True, slots=True) class StoppingEvent(base_events.HikariEvent): """Event that is fired when the gateway client is instructed to disconnect all shards.""" # Synthetic event, is not deserialized -@attr.s(slots=True, auto_attribs=True) +@attr.s(auto_attribs=True, eq=False, hash=False, kw_only=True, slots=True) class StoppedEvent(base_events.HikariEvent): """Event that is fired when the gateway client has finished disconnecting all shards.""" -@attr.s(slots=True, kw_only=True, auto_attribs=True) +@attr.s(auto_attribs=True, eq=False, hash=False, kw_only=True, slots=True) class ConnectedEvent(base_events.HikariEvent, marshaller.Deserializable): """Event invoked each time a shard connects.""" @@ -96,7 +96,7 @@ class ConnectedEvent(base_events.HikariEvent, marshaller.Deserializable): """The shard that connected.""" -@attr.s(slots=True, kw_only=True, auto_attribs=True) +@attr.s(auto_attribs=True, eq=False, hash=False, kw_only=True, slots=True) class DisconnectedEvent(base_events.HikariEvent, marshaller.Deserializable): """Event invoked each time a shard disconnects.""" @@ -104,7 +104,7 @@ class DisconnectedEvent(base_events.HikariEvent, marshaller.Deserializable): """The shard that disconnected.""" -@attr.s(slots=True, kw_only=True, auto_attribs=True) +@attr.s(auto_attribs=True, eq=False, hash=False, kw_only=True, slots=True) class ResumedEvent(base_events.HikariEvent): """Represents a gateway Resume event.""" @@ -121,7 +121,7 @@ def _deserialize_unavailable_guilds( @marshaller.marshallable() -@attr.s(slots=True, kw_only=True) +@attr.s(eq=False, hash=False, kw_only=True, slots=True) class ReadyEvent(base_events.HikariEvent, marshaller.Deserializable): """Represents the gateway Ready event. @@ -171,7 +171,7 @@ def shard_count(self) -> typing.Optional[int]: # TODO: rename to MyUserUpdateEvent @marshaller.marshallable() -@attr.s(slots=True, kw_only=True) +@attr.s(eq=False, hash=False, kw_only=True, slots=True) class UserUpdateEvent(base_events.HikariEvent, users.MyUser): """Used to represent User Update gateway events. diff --git a/hikari/gateway_entities.py b/hikari/gateway_entities.py index a3ca4c94c9..33cb4653bb 100644 --- a/hikari/gateway_entities.py +++ b/hikari/gateway_entities.py @@ -37,7 +37,7 @@ def _rest_after_deserializer(after: int) -> datetime.timedelta: @marshaller.marshallable() -@attr.s(slots=True, kw_only=True) +@attr.s(eq=True, hash=False, kw_only=True, slots=True) class SessionStartLimit(bases.Entity, marshaller.Deserializable): """Used to represent information about the current session start limits.""" @@ -55,7 +55,7 @@ class SessionStartLimit(bases.Entity, marshaller.Deserializable): @marshaller.marshallable() -@attr.s(slots=True, kw_only=True) +@attr.s(eq=True, hash=False, kw_only=True, slots=True) class GatewayBot(bases.Entity, marshaller.Deserializable): """Used to represent gateway information for the connected bot.""" @@ -76,7 +76,7 @@ def _undefined_type_default() -> typing.Literal[guilds.ActivityType.PLAYING]: @marshaller.marshallable() -@attr.s(slots=True, kw_only=True) +@attr.s(eq=True, hash=False, kw_only=True, slots=True) class Activity(marshaller.Deserializable, marshaller.Serializable): """An activity that the bot can set for one or more shards. diff --git a/hikari/guilds.py b/hikari/guilds.py index 4dd7d94097..e5f193aa37 100644 --- a/hikari/guilds.py +++ b/hikari/guilds.py @@ -216,7 +216,7 @@ class GuildVerificationLevel(int, more_enums.Enum): @marshaller.marshallable() -@attr.s(slots=True, kw_only=True) +@attr.s(eq=True, hash=False, kw_only=True, slots=True) class GuildEmbed(bases.Entity, marshaller.Deserializable): """Represents a guild embed.""" @@ -234,14 +234,14 @@ def _deserialize_role_ids(payload: more_typing.JSONArray) -> typing.Sequence[bas @marshaller.marshallable() -@attr.s(slots=True, kw_only=True) +@attr.s(eq=True, hash=True, kw_only=True, slots=True) class GuildMember(bases.Entity, marshaller.Deserializable): """Used to represent a guild bound member.""" # TODO: make GuildMember delegate to user and implement a common base class # this allows members and users to be used interchangeably. - user: typing.Optional[users.User] = marshaller.attrib( - deserializer=users.User.deserialize, if_undefined=None, default=None, inherit_kwargs=True, repr=True + user: users.User = marshaller.attrib( + deserializer=users.User.deserialize, inherit_kwargs=True, eq=True, hash=True, repr=True ) """This member's user object. @@ -249,74 +249,87 @@ class GuildMember(bases.Entity, marshaller.Deserializable): """ nickname: typing.Optional[str] = marshaller.attrib( - raw_name="nick", deserializer=str, if_none=None, if_undefined=None, default=None, repr=True + raw_name="nick", + deserializer=str, + if_none=None, + if_undefined=None, + default=None, + eq=False, + hash=False, + repr=True, ) """This member's nickname, if set.""" role_ids: typing.Sequence[bases.Snowflake] = marshaller.attrib( - raw_name="roles", deserializer=_deserialize_role_ids, + raw_name="roles", deserializer=_deserialize_role_ids, eq=False, hash=False, ) """A sequence of the IDs of the member's current roles.""" - joined_at: datetime.datetime = marshaller.attrib(deserializer=conversions.parse_iso_8601_ts) + joined_at: datetime.datetime = marshaller.attrib(deserializer=conversions.parse_iso_8601_ts, eq=False, hash=False) """The datetime of when this member joined the guild they belong to.""" premium_since: typing.Optional[datetime.datetime] = marshaller.attrib( - deserializer=conversions.parse_iso_8601_ts, if_none=None, if_undefined=None, default=None + deserializer=conversions.parse_iso_8601_ts, if_none=None, if_undefined=None, default=None, eq=False, hash=False ) """The datetime of when this member started "boosting" this guild. This will be `None` if they aren't boosting. """ - is_deaf: bool = marshaller.attrib(raw_name="deaf", deserializer=bool) + is_deaf: bool = marshaller.attrib(raw_name="deaf", deserializer=bool, eq=False, hash=False) """Whether this member is deafened by this guild in it's voice channels.""" - is_mute: bool = marshaller.attrib(raw_name="mute", deserializer=bool) + is_mute: bool = marshaller.attrib(raw_name="mute", deserializer=bool, eq=False, hash=False) """Whether this member is muted by this guild in it's voice channels.""" @marshaller.marshallable() -@attr.s(slots=True, kw_only=True) +@attr.s(eq=True, hash=True, kw_only=True, slots=True) class PartialGuildRole(bases.Unique, marshaller.Deserializable): """Represents a partial guild bound Role object.""" - name: str = marshaller.attrib(deserializer=str, serializer=str, repr=True) + name: str = marshaller.attrib(deserializer=str, serializer=str, eq=False, hash=False, repr=True) """The role's name.""" @marshaller.marshallable() -@attr.s(slots=True, kw_only=True) +@attr.s(eq=True, hash=True, kw_only=True, slots=True) class GuildRole(PartialGuildRole, marshaller.Serializable): """Represents a guild bound Role object.""" color: colors.Color = marshaller.attrib( - deserializer=colors.Color, serializer=int, default=colors.Color(0), repr=True, + deserializer=colors.Color, serializer=int, default=colors.Color(0), eq=False, hash=False, repr=True, ) """The colour of this role. This will be applied to a member's name in chat if it's their top coloured role.""" - is_hoisted: bool = marshaller.attrib(raw_name="hoist", deserializer=bool, serializer=bool, default=False, repr=True) + is_hoisted: bool = marshaller.attrib( + raw_name="hoist", deserializer=bool, serializer=bool, default=False, eq=False, hash=False, repr=True + ) """Whether this role is hoisting the members it's attached to in the member list. members will be hoisted under their highest role where this is set to `True`.""" - position: int = marshaller.attrib(deserializer=int, serializer=int, default=None, repr=True) + position: int = marshaller.attrib(deserializer=int, serializer=int, default=None, eq=False, hash=False, repr=True) """The position of this role in the role hierarchy.""" permissions: _permissions.Permission = marshaller.attrib( - deserializer=_permissions.Permission, serializer=int, default=_permissions.Permission(0) + deserializer=_permissions.Permission, serializer=int, default=_permissions.Permission(0), eq=False, hash=False ) """The guild wide permissions this role gives to the members it's attached to, This may be overridden by channel overwrites. """ - is_managed: bool = marshaller.attrib(raw_name="managed", deserializer=bool, serializer=None, default=None) + is_managed: bool = marshaller.attrib( + raw_name="managed", deserializer=bool, serializer=None, default=None, eq=False, hash=False + ) """Whether this role is managed by an integration.""" - is_mentionable: bool = marshaller.attrib(raw_name="mentionable", deserializer=bool, serializer=bool, default=False) + is_mentionable: bool = marshaller.attrib( + raw_name="mentionable", deserializer=bool, serializer=bool, default=False, eq=False, hash=False + ) """Whether this role can be mentioned by all regardless of permissions.""" @@ -349,7 +362,7 @@ class ActivityType(int, more_enums.Enum): @marshaller.marshallable() -@attr.s(slots=True, kw_only=True) +@attr.s(eq=True, hash=False, kw_only=True, slots=True) class ActivityTimestamps(bases.Entity, marshaller.Deserializable): """The datetimes for the start and/or end of an activity session.""" @@ -365,15 +378,17 @@ class ActivityTimestamps(bases.Entity, marshaller.Deserializable): @marshaller.marshallable() -@attr.s(slots=True, kw_only=True) +@attr.s(eq=True, hash=True, kw_only=True, slots=True) class ActivityParty(bases.Entity, marshaller.Deserializable): """Used to represent activity groups of users.""" - id: typing.Optional[str] = marshaller.attrib(deserializer=str, if_undefined=None, default=None, repr=True) + id: typing.Optional[str] = marshaller.attrib( + deserializer=str, if_undefined=None, default=None, eq=True, hash=True, repr=True + ) """The string id of this party instance, if set.""" _size_information: typing.Optional[typing.Tuple[int, int]] = marshaller.attrib( - raw_name="size", deserializer=tuple, if_undefined=None, default=None + raw_name="size", deserializer=tuple, if_undefined=None, default=None, eq=False, hash=False ) """The size metadata of this party, if applicable.""" @@ -390,7 +405,7 @@ def max_size(self) -> typing.Optional[int]: @marshaller.marshallable() -@attr.s(slots=True, kw_only=True) +@attr.s(eq=True, hash=False, kw_only=True, slots=True) class ActivityAssets(bases.Entity, marshaller.Deserializable): """Used to represent possible assets for an activity.""" @@ -408,7 +423,7 @@ class ActivityAssets(bases.Entity, marshaller.Deserializable): @marshaller.marshallable() -@attr.s(slots=True, kw_only=True) +@attr.s(eq=True, hash=False, kw_only=True, slots=True) class ActivitySecret(bases.Entity, marshaller.Deserializable): """The secrets used for interacting with an activity party.""" @@ -449,7 +464,7 @@ class ActivityFlag(more_enums.IntFlag): @marshaller.marshallable() -@attr.s(slots=True, kw_only=True) +@attr.s(eq=True, hash=False, kw_only=True, slots=True) class PresenceActivity(bases.Entity, marshaller.Deserializable): """Represents an activity that will be attached to a member's presence.""" @@ -536,7 +551,7 @@ def _default_status() -> typing.Literal[PresenceStatus.OFFLINE]: @marshaller.marshallable() -@attr.s(slots=True, kw_only=True) +@attr.s(eq=True, hash=False, kw_only=True, slots=True) class ClientStatus(bases.Entity, marshaller.Deserializable): """The client statuses for this member.""" @@ -558,7 +573,7 @@ class ClientStatus(bases.Entity, marshaller.Deserializable): # TODO: should this be an event instead? @marshaller.marshallable() -@attr.s(slots=True, kw_only=True) +@attr.s(eq=True, hash=True, kw_only=True, slots=True) class PresenceUser(users.User): """A user representation specifically used for presence updates. @@ -568,32 +583,45 @@ class PresenceUser(users.User): """ discriminator: typing.Union[str, unset.Unset] = marshaller.attrib( - deserializer=str, if_undefined=unset.Unset, default=unset.UNSET, repr=True + deserializer=str, if_undefined=unset.Unset, default=unset.UNSET, eq=False, hash=False, repr=True ) """This user's discriminator.""" username: typing.Union[str, unset.Unset] = marshaller.attrib( - deserializer=str, if_undefined=unset.Unset, default=unset.UNSET, repr=True + deserializer=str, if_undefined=unset.Unset, default=unset.UNSET, eq=False, hash=False, repr=True ) """This user's username.""" avatar_hash: typing.Union[None, str, unset.Unset] = marshaller.attrib( - raw_name="avatar", deserializer=str, if_none=None, if_undefined=unset.Unset, default=unset.UNSET, repr=True + raw_name="avatar", + deserializer=str, + if_none=None, + if_undefined=unset.Unset, + default=unset.UNSET, + eq=False, + hash=False, + repr=True, ) """This user's avatar hash, if set.""" is_bot: typing.Union[bool, unset.Unset] = marshaller.attrib( - raw_name="bot", deserializer=bool, if_undefined=unset.Unset, default=unset.UNSET, repr=True + raw_name="bot", + deserializer=bool, + if_undefined=unset.Unset, + default=unset.UNSET, + eq=False, + hash=False, + repr=True, ) """Whether this user is a bot account.""" is_system: typing.Union[bool, unset.Unset] = marshaller.attrib( - raw_name="system", deserializer=bool, if_undefined=unset.Unset, default=unset.UNSET + raw_name="system", deserializer=bool, if_undefined=unset.Unset, default=unset.UNSET, eq=False, hash=False, ) """Whether this user is a system account.""" flags: typing.Union[users.UserFlag, unset.Unset] = marshaller.attrib( - raw_name="public_flags", deserializer=users.UserFlag, if_undefined=unset.Unset + raw_name="public_flags", deserializer=users.UserFlag, if_undefined=unset.Unset, eq=False, hash=False ) """The public flags for this user.""" @@ -655,11 +683,13 @@ def _deserialize_activities(payload: more_typing.JSONArray, **kwargs: typing.Any @marshaller.marshallable() -@attr.s(slots=True, kw_only=True) +@attr.s(eq=True, hash=True, kw_only=True, slots=True) class GuildMemberPresence(bases.Entity, marshaller.Deserializable): """Used to represent a guild member's presence.""" - user: PresenceUser = marshaller.attrib(deserializer=PresenceUser.deserialize, inherit_kwargs=True, repr=True) + user: PresenceUser = marshaller.attrib( + deserializer=PresenceUser.deserialize, inherit_kwargs=True, eq=True, hash=True, repr=True + ) """The object of the user who this presence is for. !!! info @@ -669,7 +699,7 @@ class GuildMemberPresence(bases.Entity, marshaller.Deserializable): """ role_ids: typing.Optional[typing.Sequence[bases.Snowflake]] = marshaller.attrib( - raw_name="roles", deserializer=_deserialize_role_ids, if_undefined=None, default=None, + raw_name="roles", deserializer=_deserialize_role_ids, if_undefined=None, default=None, eq=False, hash=False, ) """The ids of the user's current roles in the guild this presence belongs to. @@ -678,7 +708,7 @@ class GuildMemberPresence(bases.Entity, marshaller.Deserializable): """ guild_id: typing.Optional[bases.Snowflake] = marshaller.attrib( - deserializer=bases.Snowflake, if_undefined=None, default=None, repr=True + deserializer=bases.Snowflake, if_undefined=None, default=None, eq=True, hash=True, repr=True ) """The ID of the guild this presence belongs to. @@ -686,21 +716,25 @@ class GuildMemberPresence(bases.Entity, marshaller.Deserializable): object (e.g on Guild Create). """ - visible_status: PresenceStatus = marshaller.attrib(raw_name="status", deserializer=PresenceStatus, repr=True) + visible_status: PresenceStatus = marshaller.attrib( + raw_name="status", deserializer=PresenceStatus, eq=False, hash=False, repr=True + ) """This user's current status being displayed by the client.""" activities: typing.Sequence[PresenceActivity] = marshaller.attrib( - deserializer=_deserialize_activities, inherit_kwargs=True + deserializer=_deserialize_activities, inherit_kwargs=True, eq=False, hash=False, ) """An array of the user's activities, with the top one will being prioritised by the client. """ - client_status: ClientStatus = marshaller.attrib(deserializer=ClientStatus.deserialize, inherit_kwargs=True) + client_status: ClientStatus = marshaller.attrib( + deserializer=ClientStatus.deserialize, inherit_kwargs=True, eq=False, hash=False, + ) """An object of the target user's client statuses.""" premium_since: typing.Optional[datetime.datetime] = marshaller.attrib( - deserializer=conversions.parse_iso_8601_ts, if_undefined=None, if_none=None, default=None + deserializer=conversions.parse_iso_8601_ts, if_undefined=None, if_none=None, default=None, eq=False, hash=False, ) """The datetime of when this member started "boosting" this guild. @@ -708,7 +742,14 @@ class GuildMemberPresence(bases.Entity, marshaller.Deserializable): """ nick: typing.Optional[str] = marshaller.attrib( - raw_name="nick", deserializer=str, if_undefined=None, if_none=None, default=None, repr=True + raw_name="nick", + deserializer=str, + if_undefined=None, + if_none=None, + default=None, + eq=False, + hash=False, + repr=True, ) """This member's nickname, if set.""" @@ -725,29 +766,31 @@ class IntegrationExpireBehaviour(int, more_enums.Enum): @marshaller.marshallable() -@attr.s(slots=True, kw_only=True) +@attr.s(eq=True, hash=True, kw_only=True, slots=True) class IntegrationAccount(bases.Entity, marshaller.Deserializable): """An account that's linked to an integration.""" - id: str = marshaller.attrib(deserializer=str) + id: str = marshaller.attrib(deserializer=str, eq=True, hash=True) """The string ID of this (likely) third party account.""" - name: str = marshaller.attrib(deserializer=str) + name: str = marshaller.attrib(deserializer=str, eq=False, hash=False) """The name of this account.""" @marshaller.marshallable() -@attr.s(slots=True, kw_only=True) +@attr.s(eq=True, hash=True, kw_only=True, slots=True) class PartialGuildIntegration(bases.Unique, marshaller.Deserializable): """A partial representation of an integration, found in audit logs.""" - name: str = marshaller.attrib(deserializer=str, repr=True) + name: str = marshaller.attrib(deserializer=str, eq=False, hash=False, repr=True) """The name of this integration.""" - type: str = marshaller.attrib(deserializer=str, repr=True) + type: str = marshaller.attrib(deserializer=str, eq=False, hash=False, repr=True) """The type of this integration.""" - account: IntegrationAccount = marshaller.attrib(deserializer=IntegrationAccount.deserialize, inherit_kwargs=True) + account: IntegrationAccount = marshaller.attrib( + deserializer=IntegrationAccount.deserialize, inherit_kwargs=True, eq=False, hash=False + ) """The account connected to this integration.""" @@ -756,47 +799,51 @@ def _deserialize_expire_grace_period(payload: int) -> datetime.timedelta: @marshaller.marshallable() -@attr.s(slots=True, kw_only=True) +@attr.s(eq=True, hash=True, kw_only=True, slots=True) class GuildIntegration(bases.Unique, marshaller.Deserializable): """Represents a guild integration object.""" - is_enabled: bool = marshaller.attrib(raw_name="enabled", deserializer=bool, repr=True) + is_enabled: bool = marshaller.attrib(raw_name="enabled", deserializer=bool, eq=False, hash=False, repr=True) """Whether this integration is enabled.""" - is_syncing: bool = marshaller.attrib(raw_name="syncing", deserializer=bool) + is_syncing: bool = marshaller.attrib(raw_name="syncing", deserializer=bool, eq=False, hash=False) """Whether this integration is syncing subscribers/emojis.""" - role_id: typing.Optional[bases.Snowflake] = marshaller.attrib(deserializer=bases.Snowflake) + role_id: typing.Optional[bases.Snowflake] = marshaller.attrib(deserializer=bases.Snowflake, eq=False, hash=False) """The ID of the managed role used for this integration's subscribers.""" is_emojis_enabled: typing.Optional[bool] = marshaller.attrib( - raw_name="enable_emoticons", deserializer=bool, if_undefined=None, default=None + raw_name="enable_emoticons", deserializer=bool, if_undefined=None, default=None, eq=False, hash=False ) """Whether users under this integration are allowed to use it's custom emojis.""" - expire_behavior: IntegrationExpireBehaviour = marshaller.attrib(deserializer=IntegrationExpireBehaviour) + expire_behavior: IntegrationExpireBehaviour = marshaller.attrib( + deserializer=IntegrationExpireBehaviour, eq=False, hash=False + ) """How members should be treated after their connected subscription expires. This won't be enacted until after `GuildIntegration.expire_grace_period` passes. """ - expire_grace_period: datetime.timedelta = marshaller.attrib(deserializer=_deserialize_expire_grace_period,) + expire_grace_period: datetime.timedelta = marshaller.attrib( + deserializer=_deserialize_expire_grace_period, eq=False, hash=False + ) """How many days users with expired subscriptions are given until `GuildIntegration.expire_behavior` is enacted out on them """ - user: users.User = marshaller.attrib(deserializer=users.User.deserialize, inherit_kwargs=True) + user: users.User = marshaller.attrib(deserializer=users.User.deserialize, inherit_kwargs=True, eq=False, hash=False) """The user this integration belongs to.""" last_synced_at: datetime.datetime = marshaller.attrib( - raw_name="synced_at", deserializer=conversions.parse_iso_8601_ts, if_none=None + raw_name="synced_at", deserializer=conversions.parse_iso_8601_ts, if_none=None, eq=False, hash=False ) """The datetime of when this integration's subscribers were last synced.""" @marshaller.marshallable() -@attr.s(slots=True, kw_only=True) +@attr.s(eq=True, hash=False, kw_only=True, slots=True) class GuildMemberBan(bases.Entity, marshaller.Deserializable): """Used to represent guild bans.""" @@ -808,7 +855,7 @@ class GuildMemberBan(bases.Entity, marshaller.Deserializable): @marshaller.marshallable() -@attr.s(slots=True, kw_only=True) +@attr.s(eq=True, hash=True, kw_only=True, slots=True) class UnavailableGuild(bases.Unique, marshaller.Deserializable): """An unavailable guild object, received during gateway events such as READY. @@ -831,17 +878,21 @@ def _deserialize_features(payload: more_typing.JSONArray) -> typing.Set[typing.U @marshaller.marshallable() -@attr.s(slots=True, kw_only=True) +@attr.s(eq=True, hash=True, kw_only=True, slots=True) class PartialGuild(bases.Unique, marshaller.Deserializable): """Base object for any partial guild objects.""" - name: str = marshaller.attrib(deserializer=str, repr=True) + name: str = marshaller.attrib(deserializer=str, eq=False, hash=False, repr=True) """The name of the guild.""" - icon_hash: typing.Optional[str] = marshaller.attrib(raw_name="icon", deserializer=str, if_none=None) + icon_hash: typing.Optional[str] = marshaller.attrib( + raw_name="icon", deserializer=str, if_none=None, eq=False, hash=False + ) """The hash for the guild icon, if there is one.""" - features: typing.Set[typing.Union[GuildFeature, str]] = marshaller.attrib(deserializer=_deserialize_features) + features: typing.Set[typing.Union[GuildFeature, str]] = marshaller.attrib( + deserializer=_deserialize_features, eq=False, hash=False + ) """A set of the features in this guild.""" def format_icon_url(self, fmt: typing.Optional[str] = None, size: int = 4096) -> typing.Optional[str]: @@ -886,30 +937,32 @@ def _deserialize_emojis( @marshaller.marshallable() -@attr.s(slots=True, kw_only=True) +@attr.s(eq=True, hash=True, kw_only=True, slots=True) class GuildPreview(PartialGuild): """A preview of a guild with the `GuildFeature.PUBLIC` feature.""" - splash_hash: typing.Optional[str] = marshaller.attrib(raw_name="splash", deserializer=str, if_none=None) + splash_hash: typing.Optional[str] = marshaller.attrib( + raw_name="splash", deserializer=str, if_none=None, eq=False, hash=False + ) """The hash of the splash for the guild, if there is one.""" discovery_splash_hash: typing.Optional[str] = marshaller.attrib( - raw_name="discovery_splash", deserializer=str, if_none=None + raw_name="discovery_splash", deserializer=str, if_none=None, eq=False, hash=False, ) """The hash of the discovery splash for the guild, if there is one.""" emojis: typing.Mapping[bases.Snowflake, _emojis.KnownCustomEmoji] = marshaller.attrib( - deserializer=_deserialize_emojis, inherit_kwargs=True + deserializer=_deserialize_emojis, inherit_kwargs=True, eq=False, hash=False, ) """The mapping of IDs to the emojis this guild provides.""" - approximate_presence_count: int = marshaller.attrib(deserializer=int, repr=True) + approximate_presence_count: int = marshaller.attrib(deserializer=int, eq=False, hash=False, repr=True) """The approximate amount of presences in guild.""" - approximate_member_count: int = marshaller.attrib(deserializer=int, repr=True) + approximate_member_count: int = marshaller.attrib(deserializer=int, eq=False, hash=False, repr=True) """The approximate amount of members in this guild.""" - description: typing.Optional[str] = marshaller.attrib(if_none=None, deserializer=str) + description: typing.Optional[str] = marshaller.attrib(deserializer=str, if_none=None, eq=False, hash=False) """The guild's description, if set.""" def format_splash_url(self, fmt: str = "png", size: int = 4096) -> typing.Optional[str]: @@ -1009,7 +1062,7 @@ def _deserialize_presences( @marshaller.marshallable() -@attr.s(slots=True, kw_only=True) +@attr.s(eq=True, hash=True, kw_only=True, slots=True) class Guild(PartialGuild): """A representation of a guild on Discord. @@ -1020,19 +1073,26 @@ class Guild(PartialGuild): any other fields should be ignored. """ - splash_hash: typing.Optional[str] = marshaller.attrib(raw_name="splash", deserializer=str, if_none=None) + splash_hash: typing.Optional[str] = marshaller.attrib( + raw_name="splash", deserializer=str, if_none=None, eq=False, hash=False + ) """The hash of the splash for the guild, if there is one.""" discovery_splash_hash: typing.Optional[str] = marshaller.attrib( - raw_name="discovery_splash", deserializer=str, if_none=None + raw_name="discovery_splash", deserializer=str, if_none=None, eq=False, hash=False ) """The hash of the discovery splash for the guild, if there is one.""" - owner_id: bases.Snowflake = marshaller.attrib(deserializer=bases.Snowflake, repr=True) + owner_id: bases.Snowflake = marshaller.attrib(deserializer=bases.Snowflake, eq=False, hash=False, repr=True) """The ID of the owner of this guild.""" my_permissions: _permissions.Permission = marshaller.attrib( - raw_name="permissions", deserializer=_permissions.Permission, if_undefined=None, default=None + raw_name="permissions", + deserializer=_permissions.Permission, + if_undefined=None, + default=None, + eq=False, + hash=False, ) """The guild-level permissions that apply to the bot user. @@ -1043,16 +1103,18 @@ class Guild(PartialGuild): rather than from the gateway. """ - region: str = marshaller.attrib(deserializer=str) + region: str = marshaller.attrib(deserializer=str, eq=False, hash=False) """The voice region for the guild.""" - afk_channel_id: typing.Optional[bases.Snowflake] = marshaller.attrib(deserializer=bases.Snowflake, if_none=None) + afk_channel_id: typing.Optional[bases.Snowflake] = marshaller.attrib( + deserializer=bases.Snowflake, if_none=None, eq=False, hash=False + ) """The ID for the channel that AFK voice users get sent to. If `None`, then no AFK channel is set up for this guild. """ - afk_timeout: datetime.timedelta = marshaller.attrib(deserializer=_deserialize_afk_timeout) + afk_timeout: datetime.timedelta = marshaller.attrib(deserializer=_deserialize_afk_timeout, eq=False, hash=False) """Timeout for activity before a member is classed as AFK. How long a voice user has to be AFK for before they are classed as being @@ -1060,7 +1122,7 @@ class Guild(PartialGuild): """ is_embed_enabled: typing.Optional[bool] = marshaller.attrib( - raw_name="embed_enabled", deserializer=bool, if_undefined=False, default=False + raw_name="embed_enabled", deserializer=bool, if_undefined=False, default=False, eq=False, hash=False ) """Defines if the guild embed is enabled or not. @@ -1069,47 +1131,51 @@ class Guild(PartialGuild): """ embed_channel_id: typing.Optional[bases.Snowflake] = marshaller.attrib( - deserializer=bases.Snowflake, if_undefined=None, if_none=None, default=None + deserializer=bases.Snowflake, if_undefined=None, if_none=None, default=None, eq=False, hash=False ) """The channel ID that the guild embed will generate an invite to. Will be `None` if invites are disabled for this guild's embed. """ - verification_level: GuildVerificationLevel = marshaller.attrib(deserializer=GuildVerificationLevel) + verification_level: GuildVerificationLevel = marshaller.attrib( + deserializer=GuildVerificationLevel, eq=False, hash=False + ) """The verification level required for a user to participate in this guild.""" default_message_notifications: GuildMessageNotificationsLevel = marshaller.attrib( - deserializer=GuildMessageNotificationsLevel + deserializer=GuildMessageNotificationsLevel, eq=False, hash=False ) """The default setting for message notifications in this guild.""" explicit_content_filter: GuildExplicitContentFilterLevel = marshaller.attrib( - deserializer=GuildExplicitContentFilterLevel + deserializer=GuildExplicitContentFilterLevel, eq=False, hash=False ) """The setting for the explicit content filter in this guild.""" roles: typing.Mapping[bases.Snowflake, GuildRole] = marshaller.attrib( - deserializer=_deserialize_roles, inherit_kwargs=True + deserializer=_deserialize_roles, inherit_kwargs=True, eq=False, hash=False, ) """The roles in this guild, represented as a mapping of ID to role object.""" emojis: typing.Mapping[bases.Snowflake, _emojis.KnownCustomEmoji] = marshaller.attrib( - deserializer=_deserialize_emojis, inherit_kwargs=True + deserializer=_deserialize_emojis, inherit_kwargs=True, eq=False, hash=False, ) """A mapping of IDs to the objects of the emojis this guild provides.""" - mfa_level: GuildMFALevel = marshaller.attrib(deserializer=GuildMFALevel) + mfa_level: GuildMFALevel = marshaller.attrib(deserializer=GuildMFALevel, eq=False, hash=False) """The required MFA level for users wishing to participate in this guild.""" - application_id: typing.Optional[bases.Snowflake] = marshaller.attrib(deserializer=bases.Snowflake, if_none=None) + application_id: typing.Optional[bases.Snowflake] = marshaller.attrib( + deserializer=bases.Snowflake, if_none=None, eq=False, hash=False + ) """The ID of the application that created this guild. This will always be `None` for guilds that weren't created by a bot. """ is_unavailable: typing.Optional[bool] = marshaller.attrib( - raw_name="unavailable", deserializer=bool, if_undefined=None, default=None + raw_name="unavailable", deserializer=bool, if_undefined=None, default=None, eq=False, hash=False ) """Whether the guild is unavailable or not. @@ -1122,7 +1188,7 @@ class Guild(PartialGuild): """ is_widget_enabled: typing.Optional[bool] = marshaller.attrib( - raw_name="widget_enabled", deserializer=bool, if_undefined=None, default=None + raw_name="widget_enabled", deserializer=bool, if_undefined=None, default=None, eq=False, hash=False ) """Describes whether the guild widget is enabled or not. @@ -1133,7 +1199,7 @@ class Guild(PartialGuild): """ widget_channel_id: typing.Optional[bases.Snowflake] = marshaller.attrib( - deserializer=bases.Snowflake, if_undefined=None, if_none=None, default=None + deserializer=bases.Snowflake, if_undefined=None, if_none=None, default=None, eq=False, hash=False ) """The channel ID that the widget's generated invite will send the user to. @@ -1141,16 +1207,22 @@ class Guild(PartialGuild): this will be `None`. """ - system_channel_id: typing.Optional[bases.Snowflake] = marshaller.attrib(if_none=None, deserializer=bases.Snowflake) + system_channel_id: typing.Optional[bases.Snowflake] = marshaller.attrib( + if_none=None, deserializer=bases.Snowflake, eq=False, hash=False + ) """The ID of the system channel or `None` if it is not enabled. Welcome messages and Nitro boost messages may be sent to this channel. """ - system_channel_flags: GuildSystemChannelFlag = marshaller.attrib(deserializer=GuildSystemChannelFlag) + system_channel_flags: GuildSystemChannelFlag = marshaller.attrib( + deserializer=GuildSystemChannelFlag, eq=False, hash=False + ) """Flags for the guild system channel to describe which notifications are suppressed.""" - rules_channel_id: typing.Optional[bases.Snowflake] = marshaller.attrib(if_none=None, deserializer=bases.Snowflake) + rules_channel_id: typing.Optional[bases.Snowflake] = marshaller.attrib( + if_none=None, deserializer=bases.Snowflake, eq=False, hash=False + ) """The ID of the channel where guilds with the `GuildFeature.PUBLIC` `features` display rules and guidelines. @@ -1158,7 +1230,7 @@ class Guild(PartialGuild): """ joined_at: typing.Optional[datetime.datetime] = marshaller.attrib( - deserializer=conversions.parse_iso_8601_ts, if_undefined=None, default=None + deserializer=conversions.parse_iso_8601_ts, if_undefined=None, default=None, eq=False, hash=False ) """The date and time that the bot user joined this guild. @@ -1168,7 +1240,7 @@ class Guild(PartialGuild): """ is_large: typing.Optional[bool] = marshaller.attrib( - raw_name="large", deserializer=bool, if_undefined=None, default=None + raw_name="large", deserializer=bool, if_undefined=None, default=None, eq=False, hash=False ) """Whether the guild is considered to be large or not. @@ -1180,7 +1252,9 @@ class Guild(PartialGuild): sent about members who are offline or invisible. """ - member_count: typing.Optional[int] = marshaller.attrib(deserializer=int, if_undefined=None, default=None) + member_count: typing.Optional[int] = marshaller.attrib( + deserializer=int, if_undefined=None, default=None, eq=False, hash=False + ) """The number of members in this guild. This information is only available if the guild was sent via a `GUILD_CREATE` @@ -1189,7 +1263,7 @@ class Guild(PartialGuild): """ members: typing.Optional[typing.Mapping[bases.Snowflake, GuildMember]] = marshaller.attrib( - deserializer=_deserialize_members, if_undefined=None, default=None, inherit_kwargs=True, + deserializer=_deserialize_members, if_undefined=None, inherit_kwargs=True, default=None, eq=False, hash=False ) """A mapping of ID to the corresponding guild members in this guild. @@ -1210,7 +1284,7 @@ class Guild(PartialGuild): """ channels: typing.Optional[typing.Mapping[bases.Snowflake, _channels.GuildChannel]] = marshaller.attrib( - deserializer=_deserialize_channels, if_undefined=None, default=None, inherit_kwargs=True, + deserializer=_deserialize_channels, if_undefined=None, inherit_kwargs=True, default=None, eq=False, hash=False, ) """A mapping of ID to the corresponding guild channels in this guild. @@ -1229,7 +1303,7 @@ class Guild(PartialGuild): """ presences: typing.Optional[typing.Mapping[bases.Snowflake, GuildMemberPresence]] = marshaller.attrib( - deserializer=_deserialize_presences, if_undefined=None, default=None, inherit_kwargs=True, + deserializer=_deserialize_presences, if_undefined=None, inherit_kwargs=True, default=None, eq=False, hash=False, ) """A mapping of member ID to the corresponding presence information for the given member, if available. @@ -1249,58 +1323,64 @@ class Guild(PartialGuild): """ max_presences: typing.Optional[int] = marshaller.attrib( - deserializer=int, if_undefined=None, if_none=None, default=None + deserializer=int, if_undefined=None, if_none=None, default=None, eq=False, hash=False ) """The maximum number of presences for the guild. If this is `None`, then the default value is used (currently 25000). """ - max_members: typing.Optional[int] = marshaller.attrib(deserializer=int, if_undefined=None, default=None) + max_members: typing.Optional[int] = marshaller.attrib( + deserializer=int, if_undefined=None, default=None, eq=False, hash=False + ) """The maximum number of members allowed in this guild. This information may not be present, in which case, it will be `None`. """ - max_video_channel_users: typing.Optional[int] = marshaller.attrib(deserializer=int, if_undefined=None, default=None) + max_video_channel_users: typing.Optional[int] = marshaller.attrib( + deserializer=int, if_undefined=None, default=None, eq=False, hash=False + ) """The maximum number of users allowed in a video channel together. If not available, this field will be `None`. """ - vanity_url_code: typing.Optional[str] = marshaller.attrib(deserializer=str, if_none=None) + vanity_url_code: typing.Optional[str] = marshaller.attrib(deserializer=str, if_none=None, eq=False, hash=False) """The vanity URL code for the guild's vanity URL. This is only present if `GuildFeature.VANITY_URL` is in `Guild.features` for this guild. If not, this will always be `None`. """ - description: typing.Optional[str] = marshaller.attrib(if_none=None, deserializer=str) + description: typing.Optional[str] = marshaller.attrib(if_none=None, deserializer=str, eq=False, hash=False) """The guild's description. This is only present if certain `GuildFeature`'s are set in `Guild.features` for this guild. Otherwise, this will always be `None`. """ - banner_hash: typing.Optional[str] = marshaller.attrib(raw_name="banner", if_none=None, deserializer=str) + banner_hash: typing.Optional[str] = marshaller.attrib( + raw_name="banner", if_none=None, deserializer=str, eq=False, hash=False + ) """The hash for the guild's banner. This is only present if the guild has `GuildFeature.BANNER` in `Guild.features` for this guild. For all other purposes, it is `None`. """ - premium_tier: GuildPremiumTier = marshaller.attrib(deserializer=GuildPremiumTier) + premium_tier: GuildPremiumTier = marshaller.attrib(deserializer=GuildPremiumTier, eq=False, hash=False) """The premium tier for this guild.""" premium_subscription_count: typing.Optional[int] = marshaller.attrib( - deserializer=int, if_undefined=None, if_none=None, default=None + deserializer=int, if_undefined=None, if_none=None, default=None, eq=False, hash=False ) """The number of nitro boosts that the server currently has. This information may not be present, in which case, it will be `None`. """ - preferred_locale: str = marshaller.attrib(deserializer=str) + preferred_locale: str = marshaller.attrib(deserializer=str, eq=False, hash=False) """The preferred locale to use for this guild. This can only be change if `GuildFeature.PUBLIC` is in `Guild.features` @@ -1308,7 +1388,7 @@ class Guild(PartialGuild): """ public_updates_channel_id: typing.Optional[bases.Snowflake] = marshaller.attrib( - if_none=None, deserializer=bases.Snowflake + if_none=None, deserializer=bases.Snowflake, eq=False, hash=False ) """The channel ID of the channel where admins and moderators receive notices from Discord. @@ -1319,7 +1399,7 @@ class Guild(PartialGuild): # TODO: if this is `None`, then should we attempt to look at the known member count if present? approximate_member_count: typing.Optional[int] = marshaller.attrib( - if_undefined=None, deserializer=int, default=None + if_undefined=None, deserializer=int, default=None, eq=False, hash=False ) """The approximate number of members in the guild. @@ -1329,7 +1409,7 @@ class Guild(PartialGuild): """ approximate_active_member_count: typing.Optional[int] = marshaller.attrib( - raw_name="approximate_presence_count", if_undefined=None, deserializer=int, default=None + raw_name="approximate_presence_count", if_undefined=None, deserializer=int, default=None, eq=False, hash=False ) """The approximate number of members in the guild that are not offline. diff --git a/hikari/invites.py b/hikari/invites.py index 83b489f0bf..1ffca174a1 100644 --- a/hikari/invites.py +++ b/hikari/invites.py @@ -46,43 +46,51 @@ class TargetUserType(int, more_enums.Enum): @marshaller.marshallable() -@attr.s(slots=True, kw_only=True) +@attr.s(eq=True, hash=True, kw_only=True, slots=True) class VanityUrl(bases.Entity, marshaller.Deserializable): """A special case invite object, that represents a guild's vanity url.""" - code: str = marshaller.attrib(deserializer=str, repr=True) + code: str = marshaller.attrib(deserializer=str, eq=True, hash=True, repr=True) """The code for this invite.""" - uses: int = marshaller.attrib(deserializer=int, repr=True) + uses: int = marshaller.attrib(deserializer=int, eq=False, hash=False, repr=True) """The amount of times this invite has been used.""" @marshaller.marshallable() -@attr.s(slots=True, kw_only=True) +@attr.s(eq=True, hash=True, kw_only=True, slots=True) class InviteGuild(guilds.PartialGuild): """Represents the partial data of a guild that'll be attached to invites.""" - splash_hash: typing.Optional[str] = marshaller.attrib(raw_name="splash", deserializer=str, if_none=None) + splash_hash: typing.Optional[str] = marshaller.attrib( + raw_name="splash", deserializer=str, if_none=None, eq=False, hash=False + ) """The hash of the splash for the guild, if there is one.""" - banner_hash: typing.Optional[str] = marshaller.attrib(raw_name="banner", if_none=None, deserializer=str) + banner_hash: typing.Optional[str] = marshaller.attrib( + raw_name="banner", deserializer=str, if_none=None, eq=False, hash=False + ) """The hash for the guild's banner. This is only present if `hikari.guilds.GuildFeature.BANNER` is in the `features` for this guild. For all other purposes, it is `None`. """ - description: typing.Optional[str] = marshaller.attrib(if_none=None, deserializer=str) + description: typing.Optional[str] = marshaller.attrib(deserializer=str, if_none=None, eq=False, hash=False) """The guild's description. This is only present if certain `features` are set in this guild. Otherwise, this will always be `None`. For all other purposes, it is `None`. """ - verification_level: guilds.GuildVerificationLevel = marshaller.attrib(deserializer=guilds.GuildVerificationLevel) + verification_level: guilds.GuildVerificationLevel = marshaller.attrib( + deserializer=guilds.GuildVerificationLevel, eq=False, hash=False + ) """The verification level required for a user to participate in this guild.""" - vanity_url_code: typing.Optional[str] = marshaller.attrib(if_none=None, deserializer=str, repr=True) + vanity_url_code: typing.Optional[str] = marshaller.attrib( + if_none=None, deserializer=str, eq=False, hash=False, repr=True + ) """The vanity URL code for the guild's vanity URL. This is only present if `hikari.guilds.GuildFeature.VANITY_URL` is in the @@ -153,15 +161,21 @@ def banner_url(self) -> typing.Optional[str]: @marshaller.marshallable() -@attr.s(slots=True, kw_only=True) +@attr.s(eq=True, hash=True, kw_only=True, slots=True) class Invite(bases.Entity, marshaller.Deserializable): """Represents an invite that's used to add users to a guild or group dm.""" - code: str = marshaller.attrib(deserializer=str, repr=True) + code: str = marshaller.attrib(deserializer=str, eq=True, hash=True, repr=True) """The code for this invite.""" guild: typing.Optional[InviteGuild] = marshaller.attrib( - deserializer=InviteGuild.deserialize, if_undefined=None, default=None, inherit_kwargs=True, repr=True + deserializer=InviteGuild.deserialize, + if_undefined=None, + inherit_kwargs=True, + default=None, + eq=False, + hash=False, + repr=True, ) """The partial object of the guild this dm belongs to. @@ -169,27 +183,27 @@ class Invite(bases.Entity, marshaller.Deserializable): """ channel: channels.PartialChannel = marshaller.attrib( - deserializer=channels.PartialChannel.deserialize, inherit_kwargs=True, repr=True + deserializer=channels.PartialChannel.deserialize, inherit_kwargs=True, eq=False, hash=False, repr=True, ) """The partial object of the channel this invite targets.""" inviter: typing.Optional[users.User] = marshaller.attrib( - deserializer=users.User.deserialize, if_undefined=None, default=None, inherit_kwargs=True, + deserializer=users.User.deserialize, if_undefined=None, inherit_kwargs=True, default=None, eq=False, hash=False, ) """The object of the user who created this invite.""" target_user: typing.Optional[users.User] = marshaller.attrib( - deserializer=users.User.deserialize, if_undefined=None, default=None, inherit_kwargs=True, + deserializer=users.User.deserialize, if_undefined=None, inherit_kwargs=True, default=None, eq=False, hash=False, ) """The object of the user who this invite targets, if set.""" target_user_type: typing.Optional[TargetUserType] = marshaller.attrib( - deserializer=TargetUserType, if_undefined=None, default=None + deserializer=TargetUserType, if_undefined=None, default=None, eq=False, hash=False, ) """The type of user target this invite is, if applicable.""" approximate_presence_count: typing.Optional[int] = marshaller.attrib( - deserializer=int, if_undefined=None, default=None + deserializer=int, if_undefined=None, default=None, eq=False, hash=False, ) """The approximate amount of presences in this invite's guild. @@ -198,7 +212,7 @@ class Invite(bases.Entity, marshaller.Deserializable): """ approximate_member_count: typing.Optional[int] = marshaller.attrib( - deserializer=int, if_undefined=None, default=None + deserializer=int, if_undefined=None, default=None, eq=False, hash=False, ) """The approximate amount of members in this invite's guild. @@ -212,7 +226,7 @@ def _max_age_deserializer(age: int) -> datetime.timedelta: @marshaller.marshallable() -@attr.s(slots=True, kw_only=True) +@attr.s(eq=True, hash=True, kw_only=True, slots=True) class InviteWithMetadata(Invite): """Extends the base `Invite` object with metadata. @@ -220,25 +234,27 @@ class InviteWithMetadata(Invite): guild permissions, rather than it's code. """ - uses: int = marshaller.attrib(deserializer=int, repr=True) + uses: int = marshaller.attrib(deserializer=int, eq=False, hash=False, repr=True) """The amount of times this invite has been used.""" - max_uses: int = marshaller.attrib(deserializer=int, repr=True) + max_uses: int = marshaller.attrib(deserializer=int, eq=False, hash=False, repr=True) """The limit for how many times this invite can be used before it expires. If set to `0` then this is unlimited. """ - max_age: typing.Optional[datetime.timedelta] = marshaller.attrib(deserializer=_max_age_deserializer) + max_age: typing.Optional[datetime.timedelta] = marshaller.attrib( + deserializer=_max_age_deserializer, eq=False, hash=False + ) """The timedelta of how long this invite will be valid for. If set to `None` then this is unlimited. """ - is_temporary: bool = marshaller.attrib(raw_name="temporary", deserializer=bool, repr=True) + is_temporary: bool = marshaller.attrib(raw_name="temporary", deserializer=bool, eq=False, hash=False, repr=True) """Whether this invite grants temporary membership.""" - created_at: datetime.datetime = marshaller.attrib(deserializer=conversions.parse_iso_8601_ts) + created_at: datetime.datetime = marshaller.attrib(deserializer=conversions.parse_iso_8601_ts, eq=False, hash=False) """When this invite was created.""" @property diff --git a/hikari/messages.py b/hikari/messages.py index 503e708c32..0ba2fb70b8 100644 --- a/hikari/messages.py +++ b/hikari/messages.py @@ -141,7 +141,7 @@ class MessageActivityType(int, more_enums.Enum): @marshaller.marshallable() -@attr.s(slots=True, kw_only=True) +@attr.s(eq=True, hash=False, kw_only=True, slots=True) class Attachment(bases.Unique, _files.BaseStream, marshaller.Deserializable): """Represents a file attached to a message. @@ -173,24 +173,24 @@ def __aiter__(self) -> typing.AsyncGenerator[bytes]: @marshaller.marshallable() -@attr.s(slots=True, kw_only=True) +@attr.s(eq=True, hash=True, kw_only=True, slots=True) class Reaction(bases.Entity, marshaller.Deserializable): """Represents a reaction in a message.""" - count: int = marshaller.attrib(deserializer=int, repr=True) + count: int = marshaller.attrib(deserializer=int, eq=False, hash=False, repr=True) """The amount of times the emoji has been used to react.""" emoji: typing.Union[_emojis.UnicodeEmoji, _emojis.CustomEmoji] = marshaller.attrib( - deserializer=_emojis.deserialize_reaction_emoji, inherit_kwargs=True, repr=True + deserializer=_emojis.deserialize_reaction_emoji, inherit_kwargs=True, eq=True, hash=True, repr=True ) """The emoji used to react.""" - is_reacted_by_me: bool = marshaller.attrib(raw_name="me", deserializer=bool) + is_reacted_by_me: bool = marshaller.attrib(raw_name="me", deserializer=bool, eq=False, hash=False) """Whether the current user reacted using this emoji.""" @marshaller.marshallable() -@attr.s(slots=True, kw_only=True) +@attr.s(eq=True, hash=False, kw_only=True, slots=True) class MessageActivity(bases.Entity, marshaller.Deserializable): """Represents the activity of a rich presence-enabled message.""" @@ -202,7 +202,7 @@ class MessageActivity(bases.Entity, marshaller.Deserializable): @marshaller.marshallable() -@attr.s(slots=True, kw_only=True) +@attr.s(eq=True, hash=False, kw_only=True, slots=True) class MessageCrosspost(bases.Unique, marshaller.Deserializable): """Represents information about a cross-posted message and the origin of the original message.""" @@ -253,104 +253,142 @@ def _deserialize_reactions(payload: more_typing.JSONArray, **kwargs: typing.Any) @marshaller.marshallable() -@attr.s(slots=True, kw_only=True) +@attr.s(eq=True, hash=True, kw_only=True, slots=True) class Message(bases.Unique, marshaller.Deserializable): """Represents a message.""" - channel_id: bases.Snowflake = marshaller.attrib(deserializer=bases.Snowflake, repr=True) + channel_id: bases.Snowflake = marshaller.attrib(deserializer=bases.Snowflake, eq=False, hash=False, repr=True) """The ID of the channel that the message was sent in.""" guild_id: typing.Optional[bases.Snowflake] = marshaller.attrib( - deserializer=bases.Snowflake, if_undefined=None, default=None, repr=True + deserializer=bases.Snowflake, if_undefined=None, default=None, eq=False, hash=False, repr=True ) """The ID of the guild that the message was sent in.""" - author: users.User = marshaller.attrib(deserializer=users.User.deserialize, inherit_kwargs=True, repr=True) + author: users.User = marshaller.attrib( + deserializer=users.User.deserialize, inherit_kwargs=True, eq=False, hash=False, repr=True + ) """The author of this message.""" member: typing.Optional[guilds.GuildMember] = marshaller.attrib( - deserializer=guilds.GuildMember.deserialize, if_undefined=None, default=None, inherit_kwargs=True, repr=True, + deserializer=guilds.GuildMember.deserialize, + if_undefined=None, + default=None, + inherit_kwargs=True, + eq=False, + hash=False, + repr=True, ) """The member properties for the message's author.""" - content: str = marshaller.attrib(deserializer=str) + content: str = marshaller.attrib(deserializer=str, eq=False, hash=False) """The content of the message.""" - timestamp: datetime.datetime = marshaller.attrib(deserializer=conversions.parse_iso_8601_ts, repr=True) + timestamp: datetime.datetime = marshaller.attrib( + deserializer=conversions.parse_iso_8601_ts, eq=False, hash=False, repr=True + ) """The timestamp that the message was sent at.""" edited_timestamp: typing.Optional[datetime.datetime] = marshaller.attrib( - deserializer=conversions.parse_iso_8601_ts, if_none=None + deserializer=conversions.parse_iso_8601_ts, if_none=None, eq=False, hash=False ) """The timestamp that the message was last edited at. Will be `None` if it wasn't ever edited. """ - is_tts: bool = marshaller.attrib(raw_name="tts", deserializer=bool) + is_tts: bool = marshaller.attrib(raw_name="tts", deserializer=bool, eq=False, hash=False) """Whether the message is a TTS message.""" - is_mentioning_everyone: bool = marshaller.attrib(raw_name="mention_everyone", deserializer=bool) + is_mentioning_everyone: bool = marshaller.attrib( + raw_name="mention_everyone", deserializer=bool, eq=False, hash=False + ) """Whether the message mentions `@everyone` or `@here`.""" user_mentions: typing.Set[bases.Snowflake] = marshaller.attrib( - raw_name="mentions", deserializer=_deserialize_object_mentions, + raw_name="mentions", deserializer=_deserialize_object_mentions, eq=False, hash=False, ) """The users the message mentions.""" role_mentions: typing.Set[bases.Snowflake] = marshaller.attrib( - raw_name="mention_roles", deserializer=_deserialize_mentions, + raw_name="mention_roles", deserializer=_deserialize_mentions, eq=False, hash=False, ) """The roles the message mentions.""" channel_mentions: typing.Set[bases.Snowflake] = marshaller.attrib( - raw_name="mention_channels", deserializer=_deserialize_object_mentions, if_undefined=set, factory=set, + raw_name="mention_channels", + deserializer=_deserialize_object_mentions, + if_undefined=set, + eq=False, + hash=False, + factory=set, ) """The channels the message mentions.""" attachments: typing.Sequence[Attachment] = marshaller.attrib( - deserializer=_deserialize_attachments, inherit_kwargs=True + deserializer=_deserialize_attachments, inherit_kwargs=True, eq=False, hash=False, ) """The message attachments.""" - embeds: typing.Sequence[_embeds.Embed] = marshaller.attrib(deserializer=_deserialize_embeds, inherit_kwargs=True) + embeds: typing.Sequence[_embeds.Embed] = marshaller.attrib( + deserializer=_deserialize_embeds, inherit_kwargs=True, eq=False, hash=False + ) """The message embeds.""" reactions: typing.Sequence[Reaction] = marshaller.attrib( - deserializer=_deserialize_reactions, if_undefined=list, factory=list, inherit_kwargs=True, + deserializer=_deserialize_reactions, if_undefined=list, inherit_kwargs=True, eq=False, hash=False, factory=list, ) """The message reactions.""" - is_pinned: bool = marshaller.attrib(raw_name="pinned", deserializer=bool) + is_pinned: bool = marshaller.attrib(raw_name="pinned", deserializer=bool, eq=False, hash=False) """Whether the message is pinned.""" webhook_id: typing.Optional[bases.Snowflake] = marshaller.attrib( - deserializer=bases.Snowflake, if_undefined=None, default=None + deserializer=bases.Snowflake, if_undefined=None, default=None, eq=False, hash=False, ) """If the message was generated by a webhook, the webhook's id.""" - type: MessageType = marshaller.attrib(deserializer=MessageType) + type: MessageType = marshaller.attrib(deserializer=MessageType, eq=False, hash=False) """The message type.""" activity: typing.Optional[MessageActivity] = marshaller.attrib( - deserializer=MessageActivity.deserialize, if_undefined=None, default=None, inherit_kwargs=True, + deserializer=MessageActivity.deserialize, + if_undefined=None, + inherit_kwargs=True, + default=None, + eq=False, + hash=False, ) """The message activity.""" application: typing.Optional[applications.Application] = marshaller.attrib( - deserializer=applications.Application.deserialize, if_undefined=None, default=None, inherit_kwargs=True, + deserializer=applications.Application.deserialize, + if_undefined=None, + inherit_kwargs=True, + default=None, + eq=False, + hash=False, ) """The message application.""" message_reference: typing.Optional[MessageCrosspost] = marshaller.attrib( - deserializer=MessageCrosspost.deserialize, if_undefined=None, default=None, inherit_kwargs=True, + deserializer=MessageCrosspost.deserialize, + if_undefined=None, + inherit_kwargs=True, + default=None, + eq=False, + hash=False, ) """The message crossposted reference data.""" - flags: typing.Optional[MessageFlag] = marshaller.attrib(deserializer=MessageFlag, if_undefined=None, default=None) + flags: typing.Optional[MessageFlag] = marshaller.attrib( + deserializer=MessageFlag, if_undefined=None, default=None, eq=False, hash=False + ) """The message flags.""" - nonce: typing.Optional[str] = marshaller.attrib(deserializer=str, if_undefined=None, default=None) + nonce: typing.Optional[str] = marshaller.attrib( + deserializer=str, if_undefined=None, default=None, eq=False, hash=False + ) """The message nonce. This is a string used for validating a message was sent.""" async def fetch_channel(self) -> channels.PartialChannel: diff --git a/hikari/state/stateless.py b/hikari/state/stateless.py index a81e868102..1e2f227d18 100644 --- a/hikari/state/stateless.py +++ b/hikari/state/stateless.py @@ -181,12 +181,22 @@ async def on_invite_delete(self, _, payload) -> None: @event_managers.raw_event_mapper("MESSAGE_CREATE") async def on_message_create(self, _, payload) -> None: """Handle MESSAGE_CREATE events.""" + # For consistency's sake and to keep Member.user as a non-nullable field, here we inject the attached user + # payload into the member payload when the member payload is present as discord decided not to duplicate the + # user object between Message.author and Message.member.user + if "member" in payload: + payload["member"]["user"] = payload["author"] event = message.MessageCreateEvent.deserialize(payload, components=self._components) await self._components.event_dispatcher.dispatch_event(event) @event_managers.raw_event_mapper("MESSAGE_UPDATE") async def on_message_update(self, _, payload) -> None: """Handle MESSAGE_UPDATE events.""" + # For consistency's sake and to keep Member.user as a non-nullable field, here we inject the attached user + # payload into the member payload when the member payload is present as discord decided not to duplicate the + # user object between Message.author and Message.member.user + if "member" in payload and "author" in payload: + payload["member"]["user"] = payload["author"] event = message.MessageUpdateEvent.deserialize(payload, components=self._components) await self._components.event_dispatcher.dispatch_event(event) diff --git a/hikari/users.py b/hikari/users.py index 3c3cbf8fb1..c68e527f5d 100644 --- a/hikari/users.py +++ b/hikari/users.py @@ -94,27 +94,35 @@ class PremiumType(int, more_enums.Enum): @marshaller.marshallable() -@attr.s(slots=True, kw_only=True) +@attr.s( + eq=True, hash=True, kw_only=True, slots=True, +) class User(bases.Unique, marshaller.Deserializable): """Represents a user.""" - discriminator: str = marshaller.attrib(deserializer=str, repr=True) + discriminator: str = marshaller.attrib(deserializer=str, eq=False, hash=False, repr=True) """This user's discriminator.""" - username: str = marshaller.attrib(deserializer=str, repr=True) + username: str = marshaller.attrib(deserializer=str, eq=False, hash=False, repr=True) """This user's username.""" - avatar_hash: typing.Optional[str] = marshaller.attrib(raw_name="avatar", deserializer=str, if_none=None) + avatar_hash: typing.Optional[str] = marshaller.attrib( + raw_name="avatar", deserializer=str, if_none=None, eq=False, hash=False + ) """This user's avatar hash, if set.""" - is_bot: bool = marshaller.attrib(raw_name="bot", deserializer=bool, if_undefined=False, default=False) + is_bot: bool = marshaller.attrib( + raw_name="bot", deserializer=bool, if_undefined=False, default=False, eq=False, hash=False + ) """Whether this user is a bot account.""" - is_system: bool = marshaller.attrib(raw_name="system", deserializer=bool, if_undefined=False, default=False) + is_system: bool = marshaller.attrib( + raw_name="system", deserializer=bool, if_undefined=False, default=False, eq=False, hash=False + ) """Whether this user is a system account.""" flags: typing.Optional[UserFlag] = marshaller.attrib( - raw_name="public_flags", deserializer=UserFlag, if_undefined=None, default=None + raw_name="public_flags", deserializer=UserFlag, if_undefined=None, default=None, eq=False, hash=False ) """The public flags for this user. @@ -167,18 +175,20 @@ def default_avatar(self) -> int: @marshaller.marshallable() -@attr.s(slots=True, kw_only=True) +@attr.s(eq=True, hash=True, kw_only=True, slots=True) class MyUser(User): """Represents a user with extended oauth2 information.""" - is_mfa_enabled: bool = marshaller.attrib(raw_name="mfa_enabled", deserializer=bool) + is_mfa_enabled: bool = marshaller.attrib(raw_name="mfa_enabled", deserializer=bool, eq=False, hash=False) """Whether the user's account has 2fa enabled.""" - locale: typing.Optional[str] = marshaller.attrib(deserializer=str, if_none=None, if_undefined=None, default=None) + locale: typing.Optional[str] = marshaller.attrib( + deserializer=str, if_none=None, if_undefined=None, default=None, eq=False, hash=False + ) """The user's set language. This is not provided by the `READY` event.""" is_verified: typing.Optional[bool] = marshaller.attrib( - raw_name="verified", deserializer=bool, if_undefined=None, default=None + raw_name="verified", deserializer=bool, if_undefined=None, default=None, eq=False, hash=False ) """Whether the email for this user's account has been verified. @@ -186,18 +196,20 @@ class MyUser(User): scope. """ - email: typing.Optional[str] = marshaller.attrib(deserializer=str, if_undefined=None, if_none=None, default=None) + email: typing.Optional[str] = marshaller.attrib( + deserializer=str, if_undefined=None, if_none=None, default=None, eq=False, hash=False + ) """The user's set email. Will be `None` if retrieved through the oauth2 flow without the `email` scope and for bot users. """ - flags: UserFlag = marshaller.attrib(deserializer=UserFlag) + flags: UserFlag = marshaller.attrib(deserializer=UserFlag, eq=False, hash=False) """This user account's flags.""" premium_type: typing.Optional[PremiumType] = marshaller.attrib( - deserializer=PremiumType, if_undefined=None, default=None + deserializer=PremiumType, if_undefined=None, default=None, eq=False, hash=False ) """The type of Nitro Subscription this user account had. diff --git a/hikari/voices.py b/hikari/voices.py index 2b0e289891..3c51e3bf2c 100644 --- a/hikari/voices.py +++ b/hikari/voices.py @@ -32,59 +32,61 @@ @marshaller.marshallable() -@attr.s(slots=True, kw_only=True) +@attr.s(eq=True, hash=True, kw_only=True, slots=True) class VoiceState(bases.Entity, marshaller.Deserializable): """Represents a user's voice connection status.""" guild_id: typing.Optional[bases.Snowflake] = marshaller.attrib( - deserializer=bases.Snowflake, if_undefined=None, default=None, repr=True + deserializer=bases.Snowflake, if_undefined=None, default=None, eq=False, hash=False, repr=True ) """The ID of the guild this voice state is in, if applicable.""" channel_id: typing.Optional[bases.Snowflake] = marshaller.attrib( - deserializer=bases.Snowflake, if_none=None, repr=True + deserializer=bases.Snowflake, if_none=None, eq=False, hash=False, repr=True ) """The ID of the channel this user is connected to. This will be `None` if they are leaving voice. """ - user_id: bases.Snowflake = marshaller.attrib(deserializer=bases.Snowflake, repr=True) + user_id: bases.Snowflake = marshaller.attrib(deserializer=bases.Snowflake, eq=False, hash=False, repr=True) """The ID of the user this voice state is for.""" member: typing.Optional[guilds.GuildMember] = marshaller.attrib( - deserializer=guilds.GuildMember.deserialize, if_undefined=None, default=None + deserializer=guilds.GuildMember.deserialize, if_undefined=None, default=None, eq=False, hash=False, ) """The guild member this voice state is for if the voice state is in a guild.""" - session_id: str = marshaller.attrib(deserializer=str, repr=True) + session_id: str = marshaller.attrib(deserializer=str, eq=True, hash=True, repr=True) """The string ID of this voice state's session.""" - is_guild_deafened: bool = marshaller.attrib(raw_name="deaf", deserializer=bool) + is_guild_deafened: bool = marshaller.attrib(raw_name="deaf", deserializer=bool, eq=False, hash=False) """Whether this user is deafened by the guild.""" - is_guild_muted: bool = marshaller.attrib(raw_name="mute", deserializer=bool) + is_guild_muted: bool = marshaller.attrib(raw_name="mute", deserializer=bool, eq=False, hash=False) """Whether this user is muted by the guild.""" - is_self_deafened: bool = marshaller.attrib(raw_name="self_deaf", deserializer=bool) + is_self_deafened: bool = marshaller.attrib(raw_name="self_deaf", deserializer=bool, eq=False, hash=False) """Whether this user is deafened by their client.""" - is_self_muted: bool = marshaller.attrib(raw_name="self_mute", deserializer=bool) + is_self_muted: bool = marshaller.attrib(raw_name="self_mute", deserializer=bool, eq=False, hash=False) """Whether this user is muted by their client.""" - is_streaming: bool = marshaller.attrib(raw_name="self_stream", deserializer=bool, if_undefined=False, default=False) + is_streaming: bool = marshaller.attrib( + raw_name="self_stream", deserializer=bool, if_undefined=False, default=False, eq=False, hash=False + ) """Whether this user is streaming using "Go Live".""" - is_suppressed: bool = marshaller.attrib(raw_name="suppress", deserializer=bool) + is_suppressed: bool = marshaller.attrib(raw_name="suppress", deserializer=bool, eq=False, hash=False) """Whether this user is muted by the current user.""" @marshaller.marshallable() -@attr.s(slots=True, kw_only=True) +@attr.s(eq=True, hash=True, kw_only=True, slots=True) class VoiceRegion(bases.Entity, marshaller.Deserializable): """Represents a voice region server.""" - id: str = marshaller.attrib(deserializer=str, repr=True) + id: str = marshaller.attrib(deserializer=str, eq=True, hash=True, repr=True) """The string ID of this region. !!! note @@ -92,17 +94,17 @@ class VoiceRegion(bases.Entity, marshaller.Deserializable): This is intentional. """ - name: str = marshaller.attrib(deserializer=str, repr=True) + name: str = marshaller.attrib(deserializer=str, eq=False, hash=False, repr=True) """The name of this region.""" - is_vip: bool = marshaller.attrib(raw_name="vip", deserializer=bool) + is_vip: bool = marshaller.attrib(raw_name="vip", deserializer=bool, eq=False, hash=False) """Whether this region is vip-only.""" - is_optimal_location: bool = marshaller.attrib(raw_name="optimal", deserializer=bool) + is_optimal_location: bool = marshaller.attrib(raw_name="optimal", deserializer=bool, eq=False, hash=False) """Whether this region's server is closest to the current user's client.""" - is_deprecated: bool = marshaller.attrib(raw_name="deprecated", deserializer=bool) + is_deprecated: bool = marshaller.attrib(raw_name="deprecated", deserializer=bool, eq=False, hash=False) """Whether this region is deprecated.""" - is_custom: bool = marshaller.attrib(raw_name="custom", deserializer=bool) + is_custom: bool = marshaller.attrib(raw_name="custom", deserializer=bool, eq=False, hash=False) """Whether this region is custom (e.g. used for events).""" diff --git a/hikari/webhooks.py b/hikari/webhooks.py index 6f2311e0d0..13fce50d39 100644 --- a/hikari/webhooks.py +++ b/hikari/webhooks.py @@ -44,7 +44,7 @@ class WebhookType(int, more_enums.Enum): @marshaller.marshallable() -@attr.s(slots=True, kw_only=True) +@attr.s(eq=True, hash=True, kw_only=True, slots=True) class Webhook(bases.Unique, marshaller.Deserializable): """Represents a webhook object on Discord. @@ -53,19 +53,25 @@ class Webhook(bases.Unique, marshaller.Deserializable): send informational messages to specific channels. """ - type: WebhookType = marshaller.attrib(deserializer=WebhookType, repr=True) + type: WebhookType = marshaller.attrib(deserializer=WebhookType, eq=False, hash=False, repr=True) """The type of the webhook.""" guild_id: typing.Optional[bases.Snowflake] = marshaller.attrib( - deserializer=bases.Snowflake, if_undefined=None, default=None, repr=True + deserializer=bases.Snowflake, if_undefined=None, default=None, eq=False, hash=False, repr=True ) """The guild ID of the webhook.""" - channel_id: bases.Snowflake = marshaller.attrib(deserializer=bases.Snowflake, repr=True) + channel_id: bases.Snowflake = marshaller.attrib(deserializer=bases.Snowflake, eq=False, hash=False, repr=True) """The channel ID this webhook is for.""" user: typing.Optional[users.User] = marshaller.attrib( - deserializer=users.User.deserialize, if_undefined=None, default=None, inherit_kwargs=True, repr=True + deserializer=users.User.deserialize, + if_undefined=None, + inherit_kwargs=True, + default=None, + eq=False, + hash=False, + repr=True, ) """The user that created the webhook @@ -74,13 +80,17 @@ class Webhook(bases.Unique, marshaller.Deserializable): than the webhook's token. """ - name: typing.Optional[str] = marshaller.attrib(deserializer=str, if_none=None, repr=True) + name: typing.Optional[str] = marshaller.attrib(deserializer=str, if_none=None, eq=False, hash=False, repr=True) """The name of the webhook.""" - avatar_hash: typing.Optional[str] = marshaller.attrib(raw_name="avatar", deserializer=str, if_none=None) + avatar_hash: typing.Optional[str] = marshaller.attrib( + raw_name="avatar", deserializer=str, if_none=None, eq=False, hash=False + ) """The avatar hash of the webhook.""" - token: typing.Optional[str] = marshaller.attrib(deserializer=str, if_undefined=None, default=None) + token: typing.Optional[str] = marshaller.attrib( + deserializer=str, if_undefined=None, default=None, eq=False, hash=False + ) """The token for the webhook. !!! info diff --git a/tests/hikari/clients/test_configs.py b/tests/hikari/clients/test_configs.py index f9a1c0a4bd..61f96c20f3 100644 --- a/tests/hikari/clients/test_configs.py +++ b/tests/hikari/clients/test_configs.py @@ -20,6 +20,7 @@ import ssl import aiohttp +import mock import pytest from hikari import gateway_entities @@ -145,13 +146,18 @@ class TestWebsocketConfig: def test_deserialize(self, test_websocket_config): datetime_obj = datetime.datetime.now() test_websocket_config["initial_idle_since"] = datetime_obj.timestamp() - websocket_config_obj = configs.GatewayConfig.deserialize(test_websocket_config) - + mock_activity = mock.MagicMock(gateway_entities.Activity) + with _helpers.patch_marshal_attr( + configs.GatewayConfig, + "initial_activity", + deserializer=gateway_entities.Activity.deserialize, + return_value=mock_activity, + ) as patched_activity_deserializer: + websocket_config_obj = configs.GatewayConfig.deserialize(test_websocket_config) + patched_activity_deserializer.assert_called_once_with({"name": "test", "url": "some_url", "type": 0}) assert websocket_config_obj.gateway_use_compression is False assert websocket_config_obj.gateway_version == 6 - assert websocket_config_obj.initial_activity == gateway_entities.Activity.deserialize( - {"name": "test", "url": "some_url", "type": 0} - ) + assert websocket_config_obj.initial_activity == mock_activity assert websocket_config_obj.initial_status == guilds.PresenceStatus.DND assert websocket_config_obj.initial_idle_since == datetime_obj assert websocket_config_obj.intents == intents.Intent.GUILD_MESSAGES | intents.Intent.GUILDS @@ -253,7 +259,15 @@ class TestBotConfig: def test_deserialize(self, test_bot_config): datetime_obj = datetime.datetime.now() test_bot_config["initial_idle_since"] = datetime_obj.timestamp() - bot_config_obj = configs.BotConfig.deserialize(test_bot_config) + mock_activity = mock.MagicMock(gateway_entities.Activity) + with _helpers.patch_marshal_attr( + configs.BotConfig, + "initial_activity", + deserializer=gateway_entities.Activity.deserialize, + return_value=mock_activity, + ) as patched_activity_deserializer: + bot_config_obj = configs.BotConfig.deserialize(test_bot_config) + patched_activity_deserializer.assert_called_once_with({"name": "test", "url": "some_url", "type": 0}) assert bot_config_obj.rest_version == 6 assert bot_config_obj.allow_redirects is True @@ -271,9 +285,7 @@ def test_deserialize(self, test_bot_config): assert bot_config_obj.shard_count == 17 assert bot_config_obj.gateway_use_compression is False assert bot_config_obj.gateway_version == 6 - assert bot_config_obj.initial_activity == gateway_entities.Activity.deserialize( - {"name": "test", "url": "some_url", "type": 0} - ) + assert bot_config_obj.initial_activity is mock_activity assert bot_config_obj.initial_status == guilds.PresenceStatus.DND assert bot_config_obj.initial_idle_since == datetime_obj assert bot_config_obj.intents == intents.Intent.GUILD_MESSAGES | intents.Intent.GUILDS diff --git a/tests/hikari/state/test_stateless.py b/tests/hikari/state/test_stateless.py index e029f2b7fc..1b58554ab7 100644 --- a/tests/hikari/state/test_stateless.py +++ b/tests/hikari/state/test_stateless.py @@ -315,7 +315,8 @@ async def test_on_invite_delete(self, event_manager_impl, mock_payload): event_manager_impl._components.event_dispatcher.dispatch_event.assert_called_once_with(mock_event) @pytest.mark.asyncio - async def test_on_message_create(self, event_manager_impl, mock_payload): + async def test_on_message_create_without_member_payload(self, event_manager_impl): + mock_payload = {"id": "424242", "user": {"id": "111", "username": "okokok", "discrim": "4242"}} mock_event = mock.MagicMock(message.MessageCreateEvent) with mock.patch("hikari.events.message.MessageCreateEvent.deserialize", return_value=mock_event) as event: @@ -326,7 +327,27 @@ async def test_on_message_create(self, event_manager_impl, mock_payload): event_manager_impl._components.event_dispatcher.dispatch_event.assert_called_once_with(mock_event) @pytest.mark.asyncio - async def test_on_message_update(self, event_manager_impl, mock_payload): + async def test_on_message_create_injects_user_into_member_payload(self, event_manager_impl): + mock_payload = {"id": "424242", "author": {"id": "111", "username": "okokok", "discrim": "4242"}, "member": {}} + mock_event = mock.MagicMock(message.MessageCreateEvent) + + with mock.patch("hikari.events.message.MessageCreateEvent.deserialize", return_value=mock_event) as event: + await event_manager_impl.on_message_create(None, mock_payload) + + assert event_manager_impl.on_message_create.___event_name___ == {"MESSAGE_CREATE"} + event.assert_called_once_with( + { + "id": "424242", + "author": {"id": "111", "username": "okokok", "discrim": "4242"}, + "member": {"user": {"id": "111", "username": "okokok", "discrim": "4242"}}, + }, + components=event_manager_impl._components, + ) + event_manager_impl._components.event_dispatcher.dispatch_event.assert_called_once_with(mock_event) + + @pytest.mark.asyncio + async def test_on_message_update_without_member_payload(self, event_manager_impl, mock_payload): + mock_payload = {"id": "424242", "user": {"id": "111", "username": "okokok", "discrim": "4242"}} mock_event = mock.MagicMock(message.MessageUpdateEvent) with mock.patch("hikari.events.message.MessageUpdateEvent.deserialize", return_value=mock_event) as event: @@ -336,6 +357,25 @@ async def test_on_message_update(self, event_manager_impl, mock_payload): event.assert_called_once_with(mock_payload, components=event_manager_impl._components) event_manager_impl._components.event_dispatcher.dispatch_event.assert_called_once_with(mock_event) + @pytest.mark.asyncio + async def test_on_message_update_injects_user_into_member_payload(self, event_manager_impl, mock_payload): + mock_payload = {"id": "424242", "author": {"id": "111", "username": "okokok", "discrim": "4242"}, "member": {}} + mock_event = mock.MagicMock(message.MessageUpdateEvent) + + with mock.patch("hikari.events.message.MessageUpdateEvent.deserialize", return_value=mock_event) as event: + await event_manager_impl.on_message_update(None, mock_payload) + + assert event_manager_impl.on_message_update.___event_name___ == {"MESSAGE_UPDATE"} + event.assert_called_once_with( + { + "id": "424242", + "author": {"id": "111", "username": "okokok", "discrim": "4242"}, + "member": {"user": {"id": "111", "username": "okokok", "discrim": "4242"}}, + }, + components=event_manager_impl._components, + ) + event_manager_impl._components.event_dispatcher.dispatch_event.assert_called_once_with(mock_event) + @pytest.mark.asyncio async def test_on_message_delete(self, event_manager_impl, mock_payload): mock_event = mock.MagicMock(message.MessageDeleteEvent) From 6f5bce078162bcabc5ffc8c906454d6637b717d1 Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Thu, 14 May 2020 10:26:55 +0100 Subject: [PATCH 321/922] Added `Attachment#__aiter__` test case --- tests/hikari/test_messages.py | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/tests/hikari/test_messages.py b/tests/hikari/test_messages.py index e1d3e72c96..b4d7cbfb84 100644 --- a/tests/hikari/test_messages.py +++ b/tests/hikari/test_messages.py @@ -23,6 +23,7 @@ import pytest from hikari import applications +from hikari import bases from hikari import channels from hikari import embeds from hikari import emojis @@ -152,6 +153,33 @@ def test_deserialize(self, test_attachment_payload, mock_components): assert attachment_obj.height == 2638 assert attachment_obj.width == 1844 + @pytest.mark.asyncio + async def test_aiter_yields_from_WebResourceStream(self): + attachment = messages.Attachment( + id=bases.Snowflake("1234"), + filename="foobar.png", + size=1_024_024, + url="https://example.com/foobar.png?x=4096", + proxy_url="https://example.com/foobar.png?x=4096", + ) + + async def __aiter__(_): + yield b"foo" + yield b"bar" + yield b"baz" + + web_resource = mock.MagicMock(spec_set=files.WebResourceStream) + web_resource.__aiter__ = __aiter__ + + with mock.patch.object(files, "WebResourceStream", return_value=web_resource): + async_iterator = attachment.__aiter__() + assert await async_iterator.__anext__() == b"foo" + assert await async_iterator.__anext__() == b"bar" + assert await async_iterator.__anext__() == b"baz" + + with pytest.raises(StopAsyncIteration): + await async_iterator.__anext__() + class TestReaction: def test_deserialize(self, test_reaction_payload, mock_components, test_emoji_payload): From 70ff042d79cc3008e5d67de021507ccc9def6edc Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Thu, 14 May 2020 10:49:43 +0100 Subject: [PATCH 322/922] Fixed more_typing tests, refactoring and testing errors. - HikariError has a new repr implementation that is slightly clearer and more consistent. - HTTPErrorResponse types now format the response name correctly (TOO_MANY_REQUESTS -> Too Many Requests) - HTTPErrorResponse now attempts to decode a bytes payload as UTF-8, falling back to using the raw str(payload) value if this fails due to not having a decode method or raising a UnicodeDecodeError. - tests/hikari/net/test_errors.py has been moved to tests/hikari/test_errors.py - tests/hikari/internal/more_typing.py has been renamed to tests/hikari/internal/test_more_typing.py --- hikari/errors.py | 19 +++++-- hikari/internal/more_typing.py | 6 +-- .../{more_typing.py => test_more_typing.py} | 2 + tests/hikari/{net => }/test_errors.py | 52 ++++++++++++++++--- 4 files changed, 63 insertions(+), 16 deletions(-) rename tests/hikari/internal/{more_typing.py => test_more_typing.py} (96%) rename tests/hikari/{net => }/test_errors.py (63%) diff --git a/hikari/errors.py b/hikari/errors.py index 20397498fd..81a997a23d 100644 --- a/hikari/errors.py +++ b/hikari/errors.py @@ -61,8 +61,8 @@ class HikariError(RuntimeError): __slots__ = () - def __repr__(self): - return str(self) + def __repr__(self) -> str: + return f"{type(self).__name__}({str(self)!r})" class HikariWarning(RuntimeWarning): @@ -289,7 +289,11 @@ class HTTPErrorResponse(HTTPError): """The response body.""" def __init__( - self, url: str, status: http.HTTPStatus, headers: aiohttp.typedefs.LooseHeaders, raw_body: typing.Any, + self, + url: str, + status: typing.Union[int, http.HTTPStatus], + headers: aiohttp.typedefs.LooseHeaders, + raw_body: typing.Any, ) -> None: super().__init__(url, f"{status}: {raw_body}") self.status = status @@ -297,9 +301,14 @@ def __init__( self.raw_body = raw_body def __str__(self) -> str: - raw_body = str(self.raw_body) + try: + raw_body = self.raw_body.decode("utf-8") + except (AttributeError, UnicodeDecodeError): + raw_body = str(self.raw_body) + chomped = len(raw_body) > 200 - return f"{self.status.value} {self.status.name}: {raw_body[:200]}{'...' if chomped else ''}" + name = self.status.name.replace("_", " ").title() + return f"{self.status.value} {name}: {raw_body[:200]}{'...' if chomped else ''}" class ClientHTTPErrorResponse(HTTPErrorResponse): diff --git a/hikari/internal/more_typing.py b/hikari/internal/more_typing.py index 5baa66159a..8e6b44e5ec 100644 --- a/hikari/internal/more_typing.py +++ b/hikari/internal/more_typing.py @@ -43,7 +43,7 @@ from typing import Mapping as _Mapping from typing import Optional as _Optional from typing import Protocol as _Protocol -from typing import runtime_checkable as _runtime_checkable +from typing import runtime_checkable as runtime_checkable from typing import Sequence as _Sequence from typing import TYPE_CHECKING as _TYPE_CHECKING from typing import TypeVar as _TypeVar @@ -99,7 +99,7 @@ """ -@_runtime_checkable +@runtime_checkable class Future(_Protocol[T_contra]): """Typed protocol representation of an `asyncio.Future`. @@ -142,7 +142,7 @@ def __await__(self) -> Coroutine[T_contra]: ... -@_runtime_checkable +@runtime_checkable class Task(_Protocol[T_contra]): """Typed protocol representation of an `asyncio.Task`. diff --git a/tests/hikari/internal/more_typing.py b/tests/hikari/internal/test_more_typing.py similarity index 96% rename from tests/hikari/internal/more_typing.py rename to tests/hikari/internal/test_more_typing.py index abf755a8ad..e07d6967fc 100644 --- a/tests/hikari/internal/more_typing.py +++ b/tests/hikari/internal/test_more_typing.py @@ -23,6 +23,7 @@ from hikari.internal import more_typing +# noinspection PyProtocol @pytest.mark.asyncio class TestFuture: async def test_is_instance(self, event_loop): @@ -34,6 +35,7 @@ async def nil(): assert isinstance(asyncio.create_task(nil()), more_typing.Future) +# noinspection PyProtocol @pytest.mark.asyncio class TestTask: async def test_is_instance(self, event_loop): diff --git a/tests/hikari/net/test_errors.py b/tests/hikari/test_errors.py similarity index 63% rename from tests/hikari/net/test_errors.py rename to tests/hikari/test_errors.py index 8e94337bc4..f2659e0bb5 100644 --- a/tests/hikari/net/test_errors.py +++ b/tests/hikari/test_errors.py @@ -24,6 +24,17 @@ from hikari.net import codes +class TestHikariError: + def test_repr(self): + class ErrorImpl(errors.HikariError): + def __str__(self): + return "program go boom!" + + inst = ErrorImpl() + + assert repr(inst) == "ErrorImpl('program go boom!')" + + class TestGatewayError: def test_init(self): err = errors.GatewayError("boom") @@ -94,16 +105,31 @@ def test_init(self): @pytest.mark.parametrize( - ("type", "expected_status"), + ("type", "expected_status", "expected_status_name"), [ - (errors.BadRequest, http.HTTPStatus.BAD_REQUEST), - (errors.Unauthorized, http.HTTPStatus.UNAUTHORIZED), - (errors.Forbidden, http.HTTPStatus.FORBIDDEN), - (errors.NotFound, http.HTTPStatus.NOT_FOUND), + (errors.BadRequest, http.HTTPStatus.BAD_REQUEST, "Bad Request"), + (errors.Unauthorized, http.HTTPStatus.UNAUTHORIZED, "Unauthorized"), + (errors.Forbidden, http.HTTPStatus.FORBIDDEN, "Forbidden"), + (errors.NotFound, http.HTTPStatus.NOT_FOUND, "Not Found"), + ( + lambda u, h, pl: errors.ClientHTTPErrorResponse(u, http.HTTPStatus.TOO_MANY_REQUESTS, h, pl), + http.HTTPStatus.TOO_MANY_REQUESTS, + "Too Many Requests", + ), + ( + lambda u, h, pl: errors.ServerHTTPErrorResponse(u, http.HTTPStatus.INTERNAL_SERVER_ERROR, h, pl), + http.HTTPStatus.INTERNAL_SERVER_ERROR, + "Internal Server Error", + ), + ( + lambda u, h, pl: errors.HTTPErrorResponse(u, http.HTTPStatus.CONTINUE, h, pl), + http.HTTPStatus.CONTINUE, + "Continue", + ), ], ) class TestHTTPClientErrors: - def test_init(self, type, expected_status): + def test_init(self, type, expected_status, expected_status_name): ex = type("http://foo.bar/api/v69/nice", {"foo": "bar"}, b"body") assert ex.status == expected_status @@ -111,7 +137,17 @@ def test_init(self, type, expected_status): assert ex.headers == {"foo": "bar"} assert ex.raw_body == b"body" - def test_str(self, type, expected_status): + def test_str_if_unicode_bytestring(self, type, expected_status, expected_status_name): ex = type("http://foo.bar/api/v69/nice", {"foo": "bar"}, b"body") - assert str(ex) == f"{expected_status} {expected_status.name}: {b'body'}" + assert str(ex) == f"{expected_status} {expected_status_name}: body" + + def test_str_if_not_unicode_bytestring(self, type, expected_status, expected_status_name): + ex = type("http://foo.bar/api/v69/nice", {"foo": "bar"}, b"\x1f\x0f\xff\xff\xff") + + assert str(ex) == f"{expected_status} {expected_status_name}: b'\\x1f\\x0f\\xff\\xff\\xff'" + + def test_str_if_payload(self, type, expected_status, expected_status_name): + ex = type("http://foo.bar/api/v69/nice", {"foo": "bar"}, {"code": 0, "message": "you broke it"}) + + assert str(ex) == f"{expected_status} {expected_status_name}: {{'code': 0, 'message': 'you broke it'}}" From 86a2de070e5ecdbf98a63f6c6122f1b47e398833 Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Thu, 14 May 2020 11:06:27 +0100 Subject: [PATCH 323/922] Fixed twemoji job failing if GitHub is down [skip deploy] --- scripts/test_twemoji_mapping.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/scripts/test_twemoji_mapping.py b/scripts/test_twemoji_mapping.py index 51000f028d..599b460d4b 100644 --- a/scripts/test_twemoji_mapping.py +++ b/scripts/test_twemoji_mapping.py @@ -31,6 +31,7 @@ import hikari +skipped_emojis = [] valid_emojis = [] invalid_emojis = [] @@ -64,6 +65,10 @@ async def run(): print("Valid emojis:", len(valid_emojis)) print("Invalid emojis:", len(invalid_emojis)) + if skipped_emojis: + print("Emojis may be skipped if persistent 5xx responses come from GitHub.") + print("Skipped emojis:", len(skipped_emojis)) + for surrogates, name in invalid_emojis: print(*map(hex, map(ord, surrogates)), name) @@ -82,6 +87,10 @@ async def try_fetch(i, n, emoji_surrogates, name): ex = None break + if isinstance(ex, hikari.errors.ServerHTTPErrorResponse): + skipped_emojis.append((emoji_surrogates, name)) + print("[ SKIP ]", f"{i}/{n}", name, *map(hex, map(ord, emoji_surrogates)), emoji.url, str(ex)) + if ex is None: valid_emojis.append((emoji_surrogates, name)) print("[ OK ]", f"{i}/{n}", name, *map(hex, map(ord, emoji_surrogates)), emoji.url) @@ -92,5 +101,5 @@ async def try_fetch(i, n, emoji_surrogates, name): asyncio.run(run()) -if invalid_emojis: +if invalid_emojis or not valid_emojis: exit(1) From 8b6fa02162fae90ed126d179b8f16d3f9d2f0db1 Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Thu, 14 May 2020 14:51:01 +0000 Subject: [PATCH 324/922] Fixed typo in URL that caused things to explode. The fire, it burns, it burns, put it out put it out. [skip ci] --- ci/gitlab/pages.yml | 1 - ci/gitlab/releases.yml | 4 ++-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/ci/gitlab/pages.yml b/ci/gitlab/pages.yml index ba6e50fe7c..f1a748373c 100644 --- a/ci/gitlab/pages.yml +++ b/ci/gitlab/pages.yml @@ -19,7 +19,6 @@ ### Base for generating documentation. ### .pages: - allow_failure: true # FIXME: remove once in master. artifacts: paths: - public/ diff --git a/ci/gitlab/releases.yml b/ci/gitlab/releases.yml index 248e2b9839..64e2ddb115 100644 --- a/ci/gitlab/releases.yml +++ b/ci/gitlab/releases.yml @@ -59,7 +59,7 @@ release:staging: environment: name: staging - url: https://nekokatt.ci.io/hikari/staging + url: https://nekokatt.gitlab.io/hikari # FIXME: change to staging URL when appropriate. extends: .release script: - |+ @@ -80,7 +80,7 @@ release:staging: release:master: environment: name: prod - url: https://nekokatt.ci.io/hikari + url: https://nekokatt.gitlab.io/hikari extends: .release script: - |+ From 1a5c9a27e4a58330d9bb04fc1c8eeddf26368446 Mon Sep 17 00:00:00 2001 From: Faster Speeding Date: Thu, 14 May 2020 01:46:34 +0100 Subject: [PATCH 325/922] [Fixes #366] urls.generate_cdn_url consistency fixes * Use consistent `format_` to avoid shadowing builtins rather than abbreviating style * enforce positional only optional arguments in methods that use urls.generate_cdn_url --- hikari/applications.py | 18 +++++------ hikari/channels.py | 32 ++++++++++++++++++++ hikari/emojis.py | 2 +- hikari/guilds.py | 48 ++++++++++++++++-------------- hikari/internal/urls.py | 8 ++--- hikari/invites.py | 12 ++++---- hikari/users.py | 16 +++++----- tests/hikari/internal/test_urls.py | 8 ++--- tests/hikari/test_applications.py | 18 +++++------ tests/hikari/test_channels.py | 37 +++++++++++++++++++++++ tests/hikari/test_emojis.py | 6 ++-- tests/hikari/test_guilds.py | 44 +++++++++++++-------------- tests/hikari/test_invites.py | 8 ++--- tests/hikari/test_users.py | 8 ++--- 14 files changed, 168 insertions(+), 97 deletions(-) diff --git a/hikari/applications.py b/hikari/applications.py index 81baa24768..6545a23a30 100644 --- a/hikari/applications.py +++ b/hikari/applications.py @@ -315,12 +315,12 @@ def icon_url(self) -> typing.Optional[str]: """URL of this team's icon, if set.""" return self.format_icon_url() - def format_icon_url(self, fmt: str = "png", size: int = 4096) -> typing.Optional[str]: + def format_icon_url(self, *, format_: str = "png", size: int = 4096) -> typing.Optional[str]: """Generate the icon URL for this team if set. Parameters ---------- - fmt : str + format_ : str The format to use for this URL, defaults to `png`. Supports `png`, `jpeg`, `jpg` and `webp`. size : int @@ -338,7 +338,7 @@ def format_icon_url(self, fmt: str = "png", size: int = 4096) -> typing.Optional If `size` is not a power of two or not between 16 and 4096. """ if self.icon_hash: - return urls.generate_cdn_url("team-icons", str(self.id), self.icon_hash, fmt=fmt, size=size) + return urls.generate_cdn_url("team-icons", str(self.id), self.icon_hash, format_=format_, size=size) return None @@ -462,12 +462,12 @@ def icon_url(self) -> typing.Optional[str]: """URL for this team's icon, if set.""" return self.format_icon_url() - def format_icon_url(self, fmt: str = "png", size: int = 4096) -> typing.Optional[str]: + def format_icon_url(self, *, format_: str = "png", size: int = 4096) -> typing.Optional[str]: """Generate the icon URL for this application if set. Parameters ---------- - fmt : str + format_ : str The format to use for this URL, defaults to `png`. Supports `png`, `jpeg`, `jpg` and `webp`. size : int @@ -485,7 +485,7 @@ def format_icon_url(self, fmt: str = "png", size: int = 4096) -> typing.Optional If `size` is not a power of two or not between 16 and 4096. """ if self.icon_hash: - return urls.generate_cdn_url("app-icons", str(self.id), self.icon_hash, fmt=fmt, size=size) + return urls.generate_cdn_url("app-icons", str(self.id), self.icon_hash, format_=format_, size=size) return None @property @@ -493,12 +493,12 @@ def cover_image_url(self) -> typing.Optional[str]: """URL for this icon's store cover image, if set.""" return self.format_cover_image_url() - def format_cover_image_url(self, fmt: str = "png", size: int = 4096) -> typing.Optional[str]: + def format_cover_image_url(self, *, format_: str = "png", size: int = 4096) -> typing.Optional[str]: """Generate the URL for this application's store page's cover image is set and applicable. Parameters ---------- - fmt : str + format_ : str The format to use for this URL, defaults to `png`. Supports `png`, `jpeg`, `jpg` and `webp`. size : int @@ -516,5 +516,5 @@ def format_cover_image_url(self, fmt: str = "png", size: int = 4096) -> typing.O If `size` is not a power of two or not between 16 and 4096. """ if self.cover_image_hash: - return urls.generate_cdn_url("app-assets", str(self.id), self.cover_image_hash, fmt=fmt, size=size) + return urls.generate_cdn_url("app-assets", str(self.id), self.cover_image_hash, format_=format_, size=size) return None diff --git a/hikari/channels.py b/hikari/channels.py index 8e443cb1bc..5fb29a651e 100644 --- a/hikari/channels.py +++ b/hikari/channels.py @@ -48,6 +48,7 @@ from hikari.internal import marshaller from hikari.internal import more_collections from hikari.internal import more_enums +from hikari.internal import urls if typing.TYPE_CHECKING: from hikari.internal import more_typing @@ -207,6 +208,37 @@ class GroupDMChannel(DMChannel): ) """The ID of the application that created the group DM, if it's a bot based group DM.""" + @property + def icon_url(self) -> typing.Optional[str]: + """URL for this DM channel's icon, if set.""" + return self.format_icon_url() + + def format_icon_url(self, *, format_: str = "png", size: int = 4096) -> typing.Optional[str]: + """Generate the URL for this group DM's icon, if set. + + Parameters + ---------- + format_ : str + The format to use for this URL, defaults to `png`. + Supports `png`, `jpeg`, `jpg` and `webp`. + size : int + The size to set for the URL, defaults to `4096`. + Can be any power of two between 16 and 4096. + + Returns + ------- + str, optional + The string URL. + + Raises + ------ + ValueError + If `size` is not a power of two or not between 16 and 4096. + """ + if self.icon_hash: + return urls.generate_cdn_url("channel-icons", str(self.id), self.icon_hash, format_=format_, size=size) + return None + def _deserialize_overwrites( payload: more_typing.JSONArray, **kwargs: typing.Any diff --git a/hikari/emojis.py b/hikari/emojis.py index 3f825be202..cb97105333 100644 --- a/hikari/emojis.py +++ b/hikari/emojis.py @@ -238,7 +238,7 @@ def is_mentionable(self) -> bool: @property def url(self) -> str: - return urls.generate_cdn_url("emojis", str(self.id), fmt="gif" if self.is_animated else "png", size=None) + return urls.generate_cdn_url("emojis", str(self.id), format_="gif" if self.is_animated else "png", size=None) def _deserialize_role_ids(payload: more_typing.JSONArray) -> typing.Set[bases.Snowflake]: diff --git a/hikari/guilds.py b/hikari/guilds.py index e5f193aa37..535f7c2760 100644 --- a/hikari/guilds.py +++ b/hikari/guilds.py @@ -635,12 +635,14 @@ def avatar_url(self) -> typing.Union[str, unset.Unset]: """ return self.format_avatar_url() - def format_avatar_url(self, fmt: typing.Optional[str] = None, size: int = 4096) -> typing.Union[str, unset.Unset]: + def format_avatar_url( + self, *, format_: typing.Optional[str] = None, size: int = 4096 + ) -> typing.Union[str, unset.Unset]: """Generate the avatar URL for this user's avatar if available. Parameters ---------- - fmt : str + format_ : str The format to use for this URL, defaults to `png` or `gif`. Supports `png`, `jpeg`, `jpg`, `webp` and `gif` (when animated). Will be ignored for default avatars which can only be `png`. @@ -663,7 +665,7 @@ def format_avatar_url(self, fmt: typing.Optional[str] = None, size: int = 4096) """ if self.discriminator is unset.UNSET and self.avatar_hash is unset.UNSET: return unset.UNSET - return super().format_avatar_url(fmt=fmt, size=size) + return super().format_avatar_url(format_=format_, size=size) @property def default_avatar(self) -> typing.Union[int, unset.Unset]: @@ -895,12 +897,12 @@ class PartialGuild(bases.Unique, marshaller.Deserializable): ) """A set of the features in this guild.""" - def format_icon_url(self, fmt: typing.Optional[str] = None, size: int = 4096) -> typing.Optional[str]: + def format_icon_url(self, *, format_: typing.Optional[str] = None, size: int = 4096) -> typing.Optional[str]: """Generate the URL for this guild's custom icon, if set. Parameters ---------- - fmt : str + format_ : str The format to use for this URL, defaults to `png` or `gif`. Supports `png`, `jpeg`, `jpg`, `webp` and `gif` (when animated). @@ -919,9 +921,9 @@ def format_icon_url(self, fmt: typing.Optional[str] = None, size: int = 4096) -> If `size` is not a power of two or not between 16 and 4096. """ if self.icon_hash: - if fmt is None: - fmt = "gif" if self.icon_hash.startswith("a_") else "png" - return urls.generate_cdn_url("icons", str(self.id), self.icon_hash, fmt=fmt, size=size) + if format_ is None: + format_ = "gif" if self.icon_hash.startswith("a_") else "png" + return urls.generate_cdn_url("icons", str(self.id), self.icon_hash, format_=format_, size=size) return None @property @@ -965,12 +967,12 @@ class GuildPreview(PartialGuild): description: typing.Optional[str] = marshaller.attrib(deserializer=str, if_none=None, eq=False, hash=False) """The guild's description, if set.""" - def format_splash_url(self, fmt: str = "png", size: int = 4096) -> typing.Optional[str]: + def format_splash_url(self, *, format_: str = "png", size: int = 4096) -> typing.Optional[str]: """Generate the URL for this guild's splash image, if set. Parameters ---------- - fmt : str + format_ : str The format to use for this URL, defaults to `png`. Supports `png`, `jpeg`, `jpg` and `webp`. size : int @@ -988,7 +990,7 @@ def format_splash_url(self, fmt: str = "png", size: int = 4096) -> typing.Option If `size` is not a power of two or not between 16 and 4096. """ if self.splash_hash: - return urls.generate_cdn_url("splashes", str(self.id), self.splash_hash, fmt=fmt, size=size) + return urls.generate_cdn_url("splashes", str(self.id), self.splash_hash, format_=format_, size=size) return None @property @@ -996,12 +998,12 @@ def splash_url(self) -> typing.Optional[str]: """URL for this guild's splash, if set.""" return self.format_splash_url() - def format_discovery_splash_url(self, fmt: str = "png", size: int = 4096) -> typing.Optional[str]: + def format_discovery_splash_url(self, *, format_: str = "png", size: int = 4096) -> typing.Optional[str]: """Generate the URL for this guild's discovery splash image, if set. Parameters ---------- - fmt : str + format_ : str The format to use for this URL, defaults to `png`. Supports `png`, `jpeg`, `jpg` and `webp`. size : int @@ -1020,7 +1022,7 @@ def format_discovery_splash_url(self, fmt: str = "png", size: int = 4096) -> typ """ if self.discovery_splash_hash: return urls.generate_cdn_url( - "discovery-splashes", str(self.id), self.discovery_splash_hash, fmt=fmt, size=size + "discovery-splashes", str(self.id), self.discovery_splash_hash, format_=format_, size=size ) return None @@ -1418,12 +1420,12 @@ class Guild(PartialGuild): remain `None`. """ - def format_splash_url(self, fmt: str = "png", size: int = 4096) -> typing.Optional[str]: + def format_splash_url(self, *, format_: str = "png", size: int = 4096) -> typing.Optional[str]: """Generate the URL for this guild's splash image, if set. Parameters ---------- - fmt : str + format_ : str The format to use for this URL, defaults to `png`. Supports `png`, `jpeg`, `jpg` and `webp`. size : int @@ -1441,7 +1443,7 @@ def format_splash_url(self, fmt: str = "png", size: int = 4096) -> typing.Option If `size` is not a power of two or not between 16 and 4096. """ if self.splash_hash: - return urls.generate_cdn_url("splashes", str(self.id), self.splash_hash, fmt=fmt, size=size) + return urls.generate_cdn_url("splashes", str(self.id), self.splash_hash, format_=format_, size=size) return None @property @@ -1449,12 +1451,12 @@ def splash_url(self) -> typing.Optional[str]: """URL for this guild's splash, if set.""" return self.format_splash_url() - def format_discovery_splash_url(self, fmt: str = "png", size: int = 4096) -> typing.Optional[str]: + def format_discovery_splash_url(self, *, format_: str = "png", size: int = 4096) -> typing.Optional[str]: """Generate the URL for this guild's discovery splash image, if set. Parameters ---------- - fmt : str + format_ : str The format to use for this URL, defaults to `png`. Supports `png`, `jpeg`, `jpg` and `webp`. size : int @@ -1473,7 +1475,7 @@ def format_discovery_splash_url(self, fmt: str = "png", size: int = 4096) -> typ """ if self.discovery_splash_hash: return urls.generate_cdn_url( - "discovery-splashes", str(self.id), self.discovery_splash_hash, fmt=fmt, size=size + "discovery-splashes", str(self.id), self.discovery_splash_hash, format_=format_, size=size ) return None @@ -1482,12 +1484,12 @@ def discovery_splash_url(self) -> typing.Optional[str]: """URL for this guild's discovery splash, if set.""" return self.format_discovery_splash_url() - def format_banner_url(self, fmt: str = "png", size: int = 4096) -> typing.Optional[str]: + def format_banner_url(self, *, format_: str = "png", size: int = 4096) -> typing.Optional[str]: """Generate the URL for this guild's banner image, if set. Parameters ---------- - fmt : str + format_ : str The format to use for this URL, defaults to `png`. Supports `png`, `jpeg`, `jpg` and `webp`. size : int @@ -1505,7 +1507,7 @@ def format_banner_url(self, fmt: str = "png", size: int = 4096) -> typing.Option If `size` is not a power of two or not between 16 and 4096. """ if self.banner_hash: - return urls.generate_cdn_url("banners", str(self.id), self.banner_hash, fmt=fmt, size=size) + return urls.generate_cdn_url("banners", str(self.id), self.banner_hash, format_=format_, size=size) return None @property diff --git a/hikari/internal/urls.py b/hikari/internal/urls.py index a0ac76d377..831fc0574b 100644 --- a/hikari/internal/urls.py +++ b/hikari/internal/urls.py @@ -46,14 +46,14 @@ """The URL for Twemoji SVG artwork for built-in emojis.""" -def generate_cdn_url(*route_parts: str, fmt: str, size: typing.Optional[int]) -> str: +def generate_cdn_url(*route_parts: str, format_: str, size: typing.Optional[int]) -> str: """Generate a link for a Discord CDN media resource. Parameters ---------- - route_parts : str + *route_parts : str The string route parts that will be used to form the link. - fmt : str + format_ : str The format to use for the wanted cdn entity, will usually be one of `webp`, `png`, `jpeg`, `jpg` or `gif` (which will be invalid if the target entity doesn't have an animated version available). @@ -78,6 +78,6 @@ def generate_cdn_url(*route_parts: str, fmt: str, size: typing.Optional[int]) -> raise ValueError("Size must be an integer power of 2") path = "/".join(urllib.parse.unquote(part) for part in route_parts) - url = urllib.parse.urljoin(BASE_CDN_URL, "/" + path) + "." + str(fmt) + url = urllib.parse.urljoin(BASE_CDN_URL, "/" + path) + "." + str(format_) query = urllib.parse.urlencode({"size": size}) if size is not None else None return f"{url}?{query}" if query else url diff --git a/hikari/invites.py b/hikari/invites.py index 1ffca174a1..554520644f 100644 --- a/hikari/invites.py +++ b/hikari/invites.py @@ -97,12 +97,12 @@ class InviteGuild(guilds.PartialGuild): `features` for this guild. If not, this will always be `None`. """ - def format_splash_url(self, fmt: str = "png", size: int = 4096) -> typing.Optional[str]: + def format_splash_url(self, *, format_: str = "png", size: int = 4096) -> typing.Optional[str]: """Generate the URL for this guild's splash, if set. Parameters ---------- - fmt : str + format_ : str The format to use for this URL, defaults to `png`. Supports `png`, `jpeg`, `jpg` and `webp`. size : int @@ -120,7 +120,7 @@ def format_splash_url(self, fmt: str = "png", size: int = 4096) -> typing.Option If `size` is not a power of two or not between 16 and 4096. """ if self.splash_hash: - return urls.generate_cdn_url("splashes", str(self.id), self.splash_hash, fmt=fmt, size=size) + return urls.generate_cdn_url("splashes", str(self.id), self.splash_hash, format_=format_, size=size) return None @property @@ -128,12 +128,12 @@ def splash_url(self) -> typing.Optional[str]: """URL for this guild's splash, if set.""" return self.format_splash_url() - def format_banner_url(self, fmt: str = "png", size: int = 4096) -> typing.Optional[str]: + def format_banner_url(self, *, format_: str = "png", size: int = 4096) -> typing.Optional[str]: """Generate the URL for this guild's banner, if set. Parameters ---------- - fmt : str + format_ : str The format to use for this URL, defaults to `png`. Supports `png`, `jpeg`, `jpg` and `webp`. size : int @@ -151,7 +151,7 @@ def format_banner_url(self, fmt: str = "png", size: int = 4096) -> typing.Option If `size` is not a power of two or not between 16 and 4096. """ if self.banner_hash: - return urls.generate_cdn_url("banners", str(self.id), self.banner_hash, fmt=fmt, size=size) + return urls.generate_cdn_url("banners", str(self.id), self.banner_hash, format_=format_, size=size) return None @property diff --git a/hikari/users.py b/hikari/users.py index c68e527f5d..fbde8864af 100644 --- a/hikari/users.py +++ b/hikari/users.py @@ -135,12 +135,12 @@ def avatar_url(self) -> str: """URL for this user's custom avatar if set, else default.""" return self.format_avatar_url() - def format_avatar_url(self, fmt: typing.Optional[str] = None, size: int = 4096) -> str: + def format_avatar_url(self, *, format_: typing.Optional[str] = None, size: int = 4096) -> str: """Generate the avatar URL for this user's custom avatar if set, else their default avatar. Parameters ---------- - fmt : str + format_ : str The format to use for this URL, defaults to `png` or `gif`. Supports `png`, `jpeg`, `jpg`, `webp` and `gif` (when animated). Will be ignored for default avatars which can only be @@ -161,12 +161,12 @@ def format_avatar_url(self, fmt: typing.Optional[str] = None, size: int = 4096) If `size` is not a power of two or not between 16 and 4096. """ if not self.avatar_hash: - return urls.generate_cdn_url("embed/avatars", str(self.default_avatar), fmt="png", size=None) - if fmt is None and self.avatar_hash.startswith("a_"): - fmt = "gif" - elif fmt is None: - fmt = "png" - return urls.generate_cdn_url("avatars", str(self.id), self.avatar_hash, fmt=fmt, size=size) + return urls.generate_cdn_url("embed/avatars", str(self.default_avatar), format_="png", size=None) + if format_ is None and self.avatar_hash.startswith("a_"): + format_ = "gif" + elif format_ is None: + format_ = "png" + return urls.generate_cdn_url("avatars", str(self.id), self.avatar_hash, format_=format_, size=size) @property def default_avatar(self) -> int: diff --git a/tests/hikari/internal/test_urls.py b/tests/hikari/internal/test_urls.py index ca62552e83..ec6216f623 100644 --- a/tests/hikari/internal/test_urls.py +++ b/tests/hikari/internal/test_urls.py @@ -21,20 +21,20 @@ def test_generate_cdn_url(): - url = urls.generate_cdn_url("not", "a", "path", fmt="neko", size=16) + url = urls.generate_cdn_url("not", "a", "path", format_="neko", size=16) assert url == "https://cdn.discordapp.com/not/a/path.neko?size=16" def test_generate_cdn_url_with_size_set_to_none(): - url = urls.generate_cdn_url("not", "a", "path", fmt="neko", size=None) + url = urls.generate_cdn_url("not", "a", "path", format_="neko", size=None) assert url == "https://cdn.discordapp.com/not/a/path.neko" @_helpers.assert_raises(type_=ValueError) def test_generate_cdn_url_with_invalid_size_out_of_limits(): - urls.generate_cdn_url("not", "a", "path", fmt="neko", size=11) + urls.generate_cdn_url("not", "a", "path", format_="neko", size=11) @_helpers.assert_raises(type_=ValueError) def test_generate_cdn_url_with_invalid_size_now_power_of_two(): - urls.generate_cdn_url("not", "a", "path", fmt="neko", size=111) + urls.generate_cdn_url("not", "a", "path", format_="neko", size=111) diff --git a/tests/hikari/test_applications.py b/tests/hikari/test_applications.py index 2209606c4c..c520e6fb48 100644 --- a/tests/hikari/test_applications.py +++ b/tests/hikari/test_applications.py @@ -189,14 +189,14 @@ def test_format_icon_url(self): mock_team = mock.MagicMock(applications.Team, icon_hash="3o2o32o", id=22323) mock_url = "https://cdn.discordapp.com/team-icons/22323/3o2o32o.jpg?size=64" with mock.patch.object(urls, "generate_cdn_url", return_value=mock_url): - url = applications.Team.format_icon_url(mock_team, fmt="jpg", size=64) - urls.generate_cdn_url.assert_called_once_with("team-icons", "22323", "3o2o32o", fmt="jpg", size=64) + url = applications.Team.format_icon_url(mock_team, format_="jpg", size=64) + urls.generate_cdn_url.assert_called_once_with("team-icons", "22323", "3o2o32o", format_="jpg", size=64) assert url == mock_url def test_format_icon_url_returns_none(self): mock_team = mock.MagicMock(applications.Team, icon_hash=None, id=22323) with mock.patch.object(urls, "generate_cdn_url", return_value=...): - url = applications.Team.format_icon_url(mock_team, fmt="jpg", size=64) + url = applications.Team.format_icon_url(mock_team, format_="jpg", size=64) urls.generate_cdn_url.assert_not_called() assert url is None @@ -266,14 +266,14 @@ def test_format_icon_url(self, mock_application): mock_application.icon_hash = "wosososoos" mock_url = "https://cdn.discordapp.com/app-icons/22222/wosososoos.jpg?size=4" with mock.patch.object(urls, "generate_cdn_url", return_value=mock_url): - url = applications.Application.format_icon_url(mock_application, fmt="jpg", size=4) - urls.generate_cdn_url.assert_called_once_with("app-icons", "22222", "wosososoos", fmt="jpg", size=4) + url = applications.Application.format_icon_url(mock_application, format_="jpg", size=4) + urls.generate_cdn_url.assert_called_once_with("app-icons", "22222", "wosososoos", format_="jpg", size=4) assert url == mock_url def test_format_icon_url_returns_none(self, mock_application): mock_application.icon_hash = None with mock.patch.object(urls, "generate_cdn_url", return_value=...): - url = applications.Application.format_icon_url(mock_application, fmt="jpg", size=4) + url = applications.Application.format_icon_url(mock_application, format_="jpg", size=4) urls.generate_cdn_url.assert_not_called() assert url is None @@ -288,13 +288,13 @@ def test_format_cover_image_url(self, mock_application): mock_application.cover_image_hash = "wowowowowo" mock_url = "https://cdn.discordapp.com/app-assets/22222/wowowowowo.jpg?size=42" with mock.patch.object(urls, "generate_cdn_url", return_value=mock_url): - url = applications.Application.format_cover_image_url(mock_application, fmt="jpg", size=42) - urls.generate_cdn_url.assert_called_once_with("app-assets", "22222", "wowowowowo", fmt="jpg", size=42) + url = applications.Application.format_cover_image_url(mock_application, format_="jpg", size=42) + urls.generate_cdn_url.assert_called_once_with("app-assets", "22222", "wowowowowo", format_="jpg", size=42) assert url == mock_url def test_format_cover_image_url_returns_none(self, mock_application): mock_application.cover_image_hash = None with mock.patch.object(urls, "generate_cdn_url", return_value=...): - url = applications.Application.format_cover_image_url(mock_application, fmt="jpg", size=42) + url = applications.Application.format_cover_image_url(mock_application, format_="jpg", size=42) urls.generate_cdn_url.assert_not_called() assert url is None diff --git a/tests/hikari/test_channels.py b/tests/hikari/test_channels.py index 9522cc8aed..6e8413eb65 100644 --- a/tests/hikari/test_channels.py +++ b/tests/hikari/test_channels.py @@ -26,6 +26,7 @@ from hikari import permissions from hikari import users from hikari.clients import components +from hikari.internal import urls @pytest.fixture() @@ -228,6 +229,42 @@ def test_deserialize(self, test_group_dm_channel_payload, test_recipient_payload assert channel_obj.owner_id == 456 assert channel_obj.application_id == 123789 + @pytest.fixture() + def group_dm_obj(self): + return channels.GroupDMChannel( + id=bases.Snowflake(123123123), + last_message_id=None, + type=None, + recipients=None, + name=None, + icon_hash="123asdf123adsf", + owner_id=None, + application_id=None, + ) + + def test_icon_url(self, group_dm_obj): + mock_url = "https://cdn.discordapp.com/channel-icons/209333111222/hashmebaby.png?size=4096" + with mock.patch.object(urls, "generate_cdn_url", return_value=mock_url): + url = group_dm_obj.icon_url + urls.generate_cdn_url.assert_called_once() + assert url == mock_url + + def test_format_icon_url(self, group_dm_obj): + mock_url = "https://cdn.discordapp.com/channel-icons/22222/wowowowowo.jpg?size=42" + with mock.patch.object(urls, "generate_cdn_url", return_value=mock_url): + url = channels.GroupDMChannel.format_icon_url(group_dm_obj, format_="jpg", size=42) + urls.generate_cdn_url.assert_called_once_with( + "channel-icons", "123123123", "123asdf123adsf", format_="jpg", size=42 + ) + assert url == mock_url + + def test_format_icon_url_returns_none(self, group_dm_obj): + group_dm_obj.icon_hash = None + with mock.patch.object(urls, "generate_cdn_url", return_value=...): + url = channels.GroupDMChannel.format_icon_url(group_dm_obj, format_="jpg", size=42) + urls.generate_cdn_url.assert_not_called() + assert url is None + class TestGuildCategory: def test_deserialize(self, test_guild_category_payload, test_permission_overwrite_payload, mock_components): diff --git a/tests/hikari/test_emojis.py b/tests/hikari/test_emojis.py index 2b5ee26029..c63b1ea7f7 100644 --- a/tests/hikari/test_emojis.py +++ b/tests/hikari/test_emojis.py @@ -179,14 +179,14 @@ def test_is_mentionable(self, animated, is_mentionable): emoji = emojis.CustomEmoji(is_animated=animated, id=bases.Snowflake(123), name="Foo") assert emoji.is_mentionable is is_mentionable - @pytest.mark.parametrize(["animated", "fmt"], [(True, "gif"), (False, "png")]) - def test_url(self, animated, fmt): + @pytest.mark.parametrize(["animated", "format_"], [(True, "gif"), (False, "png")]) + def test_url(self, animated, format_): emoji = emojis.CustomEmoji(is_animated=animated, id=bases.Snowflake(98765), name="Foo") mock_result = mock.MagicMock(spec_set=str) with mock.patch.object(urls, "generate_cdn_url", return_value=mock_result) as generate_cdn_url: assert emoji.url is mock_result - generate_cdn_url.assert_called_once_with("emojis", "98765", fmt=fmt, size=None) + generate_cdn_url.assert_called_once_with("emojis", "98765", format_=format_, size=None) class TestKnownCustomEmoji: diff --git a/tests/hikari/test_guilds.py b/tests/hikari/test_guilds.py index 09b2989748..e3fe06396d 100644 --- a/tests/hikari/test_guilds.py +++ b/tests/hikari/test_guilds.py @@ -587,7 +587,7 @@ def test_format_avatar_url_when_discriminator_or_avatar_hash_set_without_optiona mock_url = mock.MagicMock(str) with mock.patch.object(users.User, "format_avatar_url", return_value=mock_url): assert test_presence_user_obj.format_avatar_url() is mock_url - users.User.format_avatar_url.assert_called_once_with(fmt=None, size=4096) + users.User.format_avatar_url.assert_called_once_with(format_=None, size=4096) @pytest.mark.parametrize(["avatar_hash", "discriminator"], [("dwaea22", unset.UNSET), (unset.UNSET, "2929")]) def test_format_avatar_url_when_discriminator_or_avatar_hash_set_with_optionals( @@ -597,8 +597,8 @@ def test_format_avatar_url_when_discriminator_or_avatar_hash_set_with_optionals( test_presence_user_obj.discriminator = discriminator mock_url = mock.MagicMock(str) with mock.patch.object(users.User, "format_avatar_url", return_value=mock_url): - assert test_presence_user_obj.format_avatar_url(fmt="nyaapeg", size=2048) is mock_url - users.User.format_avatar_url.assert_called_once_with(fmt="nyaapeg", size=2048) + assert test_presence_user_obj.format_avatar_url(format_="nyaapeg", size=2048) is mock_url + users.User.format_avatar_url.assert_called_once_with(format_="nyaapeg", size=2048) def test_format_avatar_url_when_discriminator_and_avatar_hash_unset(self, test_presence_user_obj): test_presence_user_obj.avatar_hash = unset.UNSET @@ -803,9 +803,9 @@ def partial_guild_obj(self, test_partial_guild_payload): def test_format_icon_url(self, partial_guild_obj): mock_url = "https://cdn.discordapp.com/icons/152559372126519269/d4a983885dsaa7691ce8bcaaf945a.png?size=20" with mock.patch.object(urls, "generate_cdn_url", return_value=mock_url): - url = partial_guild_obj.format_icon_url(fmt="nyaapeg", size=42) + url = partial_guild_obj.format_icon_url(format_="nyaapeg", size=42) urls.generate_cdn_url.assert_called_once_with( - "icons", "152559372126519269", "d4a983885dsaa7691ce8bcaaf945a", fmt="nyaapeg", size=42 + "icons", "152559372126519269", "d4a983885dsaa7691ce8bcaaf945a", format_="nyaapeg", size=42 ) assert url == mock_url @@ -815,7 +815,7 @@ def test_format_icon_url_animated_default(self, partial_guild_obj): with mock.patch.object(urls, "generate_cdn_url", return_value=mock_url): url = partial_guild_obj.format_icon_url() urls.generate_cdn_url.assert_called_once_with( - "icons", "152559372126519269", "a_d4a983885dsaa7691ce8bcaaf945a", fmt="gif", size=4096 + "icons", "152559372126519269", "a_d4a983885dsaa7691ce8bcaaf945a", format_="gif", size=4096 ) assert url == mock_url @@ -825,19 +825,19 @@ def test_format_icon_url_none_animated_default(self, partial_guild_obj): with mock.patch.object(urls, "generate_cdn_url", return_value=mock_url): url = partial_guild_obj.format_icon_url() urls.generate_cdn_url.assert_called_once_with( - "icons", "152559372126519269", "d4a983885dsaa7691ce8bcaaf945a", fmt="png", size=4096 + "icons", "152559372126519269", "d4a983885dsaa7691ce8bcaaf945a", format_="png", size=4096 ) assert url == mock_url def test_format_icon_url_returns_none(self, partial_guild_obj): partial_guild_obj.icon_hash = None with mock.patch.object(urls, "generate_cdn_url", return_value=...): - url = partial_guild_obj.format_icon_url(fmt="nyaapeg", size=42) + url = partial_guild_obj.format_icon_url(format_="nyaapeg", size=42) urls.generate_cdn_url.assert_not_called() assert url is None @pytest.mark.parametrize( - ["fmt", "expected_fmt", "icon_hash", "size"], + ["format_", "expected_format", "icon_hash", "size"], [ ("png", "png", "a_1a2b3c", 1 << 4), ("png", "png", "1a2b3c", 1 << 5), @@ -853,13 +853,13 @@ def test_format_icon_url_returns_none(self, partial_guild_obj): (None, "png", "1a2b3c", 1 << 10), ], ) - def test_format_icon_url(self, partial_guild_obj, fmt, expected_fmt, icon_hash, size): + def test_format_icon_url(self, partial_guild_obj, format_, expected_format, icon_hash, size): mock_url = "https://cdn.discordapp.com/icons/152559372126519269/d4a983885dsaa7691ce8bcaaf945a.png?size=20" partial_guild_obj.icon_hash = icon_hash with mock.patch.object(urls, "generate_cdn_url", return_value=mock_url): - url = partial_guild_obj.format_icon_url(fmt, size) + url = partial_guild_obj.format_icon_url(format_=format_, size=size) urls.generate_cdn_url.assert_called_once_with( - "icons", str(partial_guild_obj.id), partial_guild_obj.icon_hash, fmt=expected_fmt, size=size + "icons", str(partial_guild_obj.id), partial_guild_obj.icon_hash, format_=expected_format, size=size ) assert url == mock_url @@ -901,9 +901,9 @@ def test_guild_preview_obj(self): def test_format_discovery_splash_url(self, test_guild_preview_obj): mock_url = "https://not-al" with mock.patch.object(urls, "generate_cdn_url", return_value=mock_url): - url = test_guild_preview_obj.format_discovery_splash_url(fmt="nyaapeg", size=4000) + url = test_guild_preview_obj.format_discovery_splash_url(format_="nyaapeg", size=4000) urls.generate_cdn_url.assert_called_once_with( - "discovery-splashes", "23123123123", "lkodwaidi09239uid", fmt="nyaapeg", size=4000 + "discovery-splashes", "23123123123", "lkodwaidi09239uid", format_="nyaapeg", size=4000 ) assert url == mock_url @@ -924,9 +924,9 @@ def test_discover_splash_url(self, test_guild_preview_obj): def test_format_splash_url(self, test_guild_preview_obj): mock_url = "https://not-al" with mock.patch.object(urls, "generate_cdn_url", return_value=mock_url): - url = test_guild_preview_obj.format_splash_url(fmt="nyaapeg", size=4000) + url = test_guild_preview_obj.format_splash_url(format_="nyaapeg", size=4000) urls.generate_cdn_url.assert_called_once_with( - "splashes", "23123123123", "dsa345tfcdg54b", fmt="nyaapeg", size=4000 + "splashes", "23123123123", "dsa345tfcdg54b", format_="nyaapeg", size=4000 ) assert url == mock_url @@ -1071,9 +1071,9 @@ def test_guild_obj(self): def test_format_banner_url(self, test_guild_obj): mock_url = "https://not-al" with mock.patch.object(urls, "generate_cdn_url", return_value=mock_url): - url = test_guild_obj.format_banner_url(fmt="nyaapeg", size=4000) + url = test_guild_obj.format_banner_url(format_="nyaapeg", size=4000) urls.generate_cdn_url.assert_called_once_with( - "banners", "265828729970753537", "1a2b3c", fmt="nyaapeg", size=4000 + "banners", "265828729970753537", "1a2b3c", format_="nyaapeg", size=4000 ) assert url == mock_url @@ -1094,9 +1094,9 @@ def test_banner_url(self, test_guild_obj): def test_format_discovery_splash_url(self, test_guild_obj): mock_url = "https://not-al" with mock.patch.object(urls, "generate_cdn_url", return_value=mock_url): - url = test_guild_obj.format_discovery_splash_url(fmt="nyaapeg", size=4000) + url = test_guild_obj.format_discovery_splash_url(format_="nyaapeg", size=4000) urls.generate_cdn_url.assert_called_once_with( - "discovery-splashes", "265828729970753537", "famfamFAMFAMfam", fmt="nyaapeg", size=4000 + "discovery-splashes", "265828729970753537", "famfamFAMFAMfam", format_="nyaapeg", size=4000 ) assert url == mock_url @@ -1117,9 +1117,9 @@ def test_discover_splash_url(self, test_guild_obj): def test_format_splash_url(self, test_guild_obj): mock_url = "https://not-al" with mock.patch.object(urls, "generate_cdn_url", return_value=mock_url): - url = test_guild_obj.format_splash_url(fmt="nyaapeg", size=4000) + url = test_guild_obj.format_splash_url(format_="nyaapeg", size=4000) urls.generate_cdn_url.assert_called_once_with( - "splashes", "265828729970753537", "0ff0ff0ff", fmt="nyaapeg", size=4000 + "splashes", "265828729970753537", "0ff0ff0ff", format_="nyaapeg", size=4000 ) assert url == mock_url diff --git a/tests/hikari/test_invites.py b/tests/hikari/test_invites.py index 7904ef2640..05bffcd9c3 100644 --- a/tests/hikari/test_invites.py +++ b/tests/hikari/test_invites.py @@ -119,9 +119,9 @@ def invite_guild_obj(self): def test_format_splash_url(self, invite_guild_obj): mock_url = "https://not-al" with mock.patch.object(urls, "generate_cdn_url", return_value=mock_url): - url = invite_guild_obj.format_splash_url(fmt="nyaapeg", size=4000) + url = invite_guild_obj.format_splash_url(format_="nyaapeg", size=4000) urls.generate_cdn_url.assert_called_once_with( - "splashes", "56188492224814744", "aSplashForSure", fmt="nyaapeg", size=4000 + "splashes", "56188492224814744", "aSplashForSure", format_="nyaapeg", size=4000 ) assert url == mock_url @@ -142,9 +142,9 @@ def test_splash_url(self, invite_guild_obj): def test_format_banner_url(self, invite_guild_obj): mock_url = "https://not-al" with mock.patch.object(urls, "generate_cdn_url", return_value=mock_url): - url = invite_guild_obj.format_banner_url(fmt="nyaapeg", size=4000) + url = invite_guild_obj.format_banner_url(format_="nyaapeg", size=4000) urls.generate_cdn_url.assert_called_once_with( - "banners", "56188492224814744", "aBannerForSure", fmt="nyaapeg", size=4000 + "banners", "56188492224814744", "aBannerForSure", format_="nyaapeg", size=4000 ) assert url == mock_url diff --git a/tests/hikari/test_users.py b/tests/hikari/test_users.py index 537e2b9e2f..ed0b3d9b64 100644 --- a/tests/hikari/test_users.py +++ b/tests/hikari/test_users.py @@ -98,7 +98,7 @@ def test_format_avatar_url_when_animated(self, user_obj): with mock.patch.object(urls, "generate_cdn_url", return_value=mock_url): url = user_obj.format_avatar_url(size=3232) urls.generate_cdn_url.assert_called_once_with( - "avatars", "115590097100865541", "a_820d0e50543216e812ad94e6ab7", fmt="gif", size=3232 + "avatars", "115590097100865541", "a_820d0e50543216e812ad94e6ab7", format_="gif", size=3232 ) assert url == mock_url @@ -107,15 +107,15 @@ def test_format_avatar_url_default(self, user_obj): mock_url = "https://cdn.discordapp.com/embed/avatars/2.png" with mock.patch.object(urls, "generate_cdn_url", return_value=mock_url): url = user_obj.format_avatar_url(size=3232) - urls.generate_cdn_url("embed/avatars", "115590097100865541", fmt="png", size=None) + urls.generate_cdn_url("embed/avatars", "115590097100865541", format_="png", size=None) assert url == mock_url def test_format_avatar_url_when_format_specified(self, user_obj): mock_url = "https://cdn.discordapp.com/avatars/115590097100865541/b3b24c6d7c37067061a8.nyaapeg?size=1024" with mock.patch.object(urls, "generate_cdn_url", return_value=mock_url): - url = user_obj.format_avatar_url(fmt="nyaapeg", size=1024) + url = user_obj.format_avatar_url(format_="nyaapeg", size=1024) urls.generate_cdn_url.assert_called_once_with( - "avatars", "115590097100865541", "b3b24c6d7cbcdec129d5d537067061a8", fmt="nyaapeg", size=1024 + "avatars", "115590097100865541", "b3b24c6d7cbcdec129d5d537067061a8", format_="nyaapeg", size=1024 ) assert url == mock_url From f237ecffa7b44a7814d7113e9e4ae428b5b83e71 Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Thu, 14 May 2020 20:48:55 +0100 Subject: [PATCH 326/922] Fixed voodoo type shit in the CI configuration --- ci/config.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/ci/config.py b/ci/config.py index 76d74be39f..56d8558a2f 100644 --- a/ci/config.py +++ b/ci/config.py @@ -16,7 +16,6 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . -import os import os as _os IS_CI = "CI" in _os.environ @@ -55,8 +54,8 @@ AUTHOR = "Nekokatt" ORIGINAL_REPO_URL = f"https://{GIT_SVC_HOST}/${AUTHOR}/{API_NAME}" SSH_DIR = "~/.ssh" -SSH_PRIVATE_KEY_PATH = os.path.join(SSH_DIR, "id_rsa") -SSH_KNOWN_HOSTS = os.path.join(SSH_DIR, "known_hosts") +SSH_PRIVATE_KEY_PATH = _os.path.join(SSH_DIR, "id_rsa") +SSH_KNOWN_HOSTS = _os.path.join(SSH_DIR, "known_hosts") CI_ROBOT_NAME = AUTHOR CI_ROBOT_EMAIL = "3903853-nekokatt@users.noreply.gitlab.com" SKIP_CI_PHRASE = "[skip ci]" From 21a30b0b82fb625ab0a8b56829521b863c87ba3e Mon Sep 17 00:00:00 2001 From: Faster Speeding Date: Thu, 14 May 2020 00:27:57 +0100 Subject: [PATCH 327/922] Handle activity timestamp with unix timestamp larger than the largest datetime python can handle --- hikari/events/event_managers.py | 2 +- hikari/internal/conversions.py | 15 +++++++++++++-- hikari/internal/marshaller.py | 8 ++++---- tests/hikari/internal/test_conversions.py | 8 ++++++++ 4 files changed, 26 insertions(+), 7 deletions(-) diff --git a/hikari/events/event_managers.py b/hikari/events/event_managers.py index 5b470679b8..74c1cf4b78 100644 --- a/hikari/events/event_managers.py +++ b/hikari/events/event_managers.py @@ -179,5 +179,5 @@ async def process_raw_event( await handler(shard_client_obj, payload) except Exception as ex: self.logger.exception( - "Failed to unmarshal %r event payload. This is likely a bug in the library itself.", exc_info=ex, + "Failed to unmarshal %r event payload. This is likely a bug in the library itself.", name, exc_info=ex, ) diff --git a/hikari/internal/conversions.py b/hikari/internal/conversions.py index cc1b10aef8..808ca3a962 100644 --- a/hikari/internal/conversions.py +++ b/hikari/internal/conversions.py @@ -47,7 +47,6 @@ if typing.TYPE_CHECKING: import enum - import types IntFlagT = typing.TypeVar("IntFlagT", bound=enum.IntFlag) RawIntFlagValueT = typing.Union[typing.AnyStr, typing.SupportsInt, int] @@ -252,8 +251,20 @@ def unix_epoch_to_datetime(epoch: int, /) -> datetime.datetime: ------- datetime.datetime Number of seconds since 1/1/1970 within a datetime object (UTC). + + !!! note + If an epoch that's outside the range of what this system can handle, + this will return `datetime.datetime.max` or `datetime.datetime.min`. """ - return datetime.datetime.fromtimestamp(epoch / 1000, datetime.timezone.utc) + try: + return datetime.datetime.fromtimestamp(epoch / 1000, datetime.timezone.utc) + # Datetime seems to raise an OSError when you try to convert an out of range timestamp on Windows and a ValueError + # if you try on a UNIX system so we want to catch both. + except (OSError, ValueError): + if epoch > 0: + return datetime.datetime.max + else: + return datetime.datetime.min def pluralize(count: int, name: str, suffix: str = "s") -> str: diff --git a/hikari/internal/marshaller.py b/hikari/internal/marshaller.py index f7c8a66d87..72fda0c32f 100644 --- a/hikari/internal/marshaller.py +++ b/hikari/internal/marshaller.py @@ -367,7 +367,7 @@ def deserialize( raise AttributeError( "Failed to deserialize data to instance of " f"{target_type.__module__}.{target_type.__qualname__} due to required field {a.field_name} " - f"(from raw key {a.raw_name}) not being included in the input payload\n\n{raw_data}" + f"(from raw key {a.raw_name}) not being included in the input payload\n\n{repr(raw_data)}" ) if a.if_undefined in _PASSED_THROUGH_SINGLETONS: kwargs[kwarg_name] = a.if_undefined @@ -379,8 +379,8 @@ def deserialize( if a.if_none is RAISE: raise AttributeError( "Failed to deserialize data to instance of " - f"{target_type.__module__}.{target_type.__qualname__} due to non-nullable field {a.field_name}" - f" (from raw key {a.raw_name}) being `None` in the input payload\n\n{raw_data}" + f"{target_type.__module__}.{target_type.__qualname__} due to non-nullable field " + f" (from raw key {a.raw_name!r}) being `None` in the input payload\n\n{raw_data!r}" ) if a.if_none in _PASSED_THROUGH_SINGLETONS: kwargs[kwarg_name] = a.if_none @@ -396,7 +396,7 @@ def deserialize( raise TypeError( "Failed to deserialize data to instance of " f"{target_type.__module__}.{target_type.__qualname__} because marshalling failed on " - f"attribute {a.field_name} (passed to constructor as {kwarg_name})" + f"attribute {a.field_name!r} (passed to constructor as {kwarg_name!r})\n\n{data!r}" ) from exc return target_type(**kwargs, **injected_kwargs) diff --git a/tests/hikari/internal/test_conversions.py b/tests/hikari/internal/test_conversions.py index f158423cfe..f2a69fb0a7 100644 --- a/tests/hikari/internal/test_conversions.py +++ b/tests/hikari/internal/test_conversions.py @@ -185,6 +185,14 @@ def test_parse_unix_epoch_to_datetime(): assert conversions.unix_epoch_to_datetime(unix_timestamp) == expected_timestamp +def test_unix_epoch_to_datetime_with_out_of_range_positive_timestamp(): + assert conversions.unix_epoch_to_datetime(996877846784536) == datetime.datetime.max + + +def test_unix_epoch_to_datetime_with_out_of_range_negative_timestamp(): + assert conversions.unix_epoch_to_datetime(-996877846784536) == datetime.datetime.min + + @pytest.mark.parametrize( ["count", "name", "kwargs", "expect"], [ From 3b6440d2fb7e94557999fbfce03130d19e7d9a07 Mon Sep 17 00:00:00 2001 From: Faster Speeding Date: Thu, 14 May 2020 22:09:55 +0100 Subject: [PATCH 328/922] [Closes 359] Add Webhook methods --- hikari/clients/rest/webhook.py | 4 +- hikari/guilds.py | 22 ++- hikari/users.py | 35 +++- hikari/webhooks.py | 328 ++++++++++++++++++++++++++++++++- tests/hikari/test_guilds.py | 20 +- tests/hikari/test_users.py | 54 +++++- tests/hikari/test_webhook.py | 320 +++++++++++++++++++++++++++++++- 7 files changed, 758 insertions(+), 25 deletions(-) diff --git a/hikari/clients/rest/webhook.py b/hikari/clients/rest/webhook.py index fa90d8f79d..0cdcac539e 100644 --- a/hikari/clients/rest/webhook.py +++ b/hikari/clients/rest/webhook.py @@ -239,7 +239,7 @@ async def execute_webhook( # pylint:disable=too-many-locals,line-too-long Raises ------ hikari.errors.NotFound - If the channel ID or webhook ID is not found. + If the webhook ID is not found. hikari.errors.BadRequest This can be raised if the file is too large; if the embed exceeds the defined limits; if the message content is specified only and @@ -247,8 +247,6 @@ async def execute_webhook( # pylint:disable=too-many-locals,line-too-long or embeds are specified. If any invalid snowflake IDs are passed; a snowflake may be invalid due to it being outside of the range of a 64 bit integer. - hikari.errors.Forbidden - If you lack permissions to send to this channel. hikari.errors.Unauthorized If you pass a token that's invalid for the target webhook. ValueError diff --git a/hikari/guilds.py b/hikari/guilds.py index 535f7c2760..54b0344fe1 100644 --- a/hikari/guilds.py +++ b/hikari/guilds.py @@ -663,12 +663,12 @@ def format_avatar_url( ValueError If `size` is not a power of two or not between 16 and 4096. """ - if self.discriminator is unset.UNSET and self.avatar_hash is unset.UNSET: - return unset.UNSET - return super().format_avatar_url(format_=format_, size=size) + if self.discriminator is not unset.UNSET or self.avatar_hash is not unset.UNSET: + return super().format_avatar_url(format_=format_, size=size) + return unset.UNSET @property - def default_avatar(self) -> typing.Union[int, unset.Unset]: + def default_avatar_index(self) -> typing.Union[int, unset.Unset]: """Integer representation of this user's default avatar. !!! note @@ -676,7 +676,19 @@ def default_avatar(self) -> typing.Union[int, unset.Unset]: `hikari.unset.UNSET`. """ if self.discriminator is not unset.UNSET: - return int(self.discriminator) % 5 + return super().default_avatar_index + return unset.UNSET + + @property + def default_avatar_url(self) -> typing.Union[str, unset.Unset]: + """URL for this user's default avatar. + + !!! note + This will be `hikari.unset.UNSET` if `PresenceUser.discriminator` is + `hikari.unset.UNSET`. + """ + if self.discriminator is not unset.UNSET: + return super().default_avatar_url return unset.UNSET diff --git a/hikari/users.py b/hikari/users.py index fbde8864af..29a9c95210 100644 --- a/hikari/users.py +++ b/hikari/users.py @@ -130,6 +130,21 @@ class User(bases.Unique, marshaller.Deserializable): This will be `None` if it's a webhook user. """ + async def fetch_self(self) -> User: + """Get this user's up-to-date object. + + Returns + ------- + hikari.users.User + The requested user object. + + Raises + ------ + hikari.errors.NotFound + If the user is not found. + """ + return await self._components.rest.fetch_user(user=self.id) + @property def avatar_url(self) -> str: """URL for this user's custom avatar if set, else default.""" @@ -161,7 +176,8 @@ def format_avatar_url(self, *, format_: typing.Optional[str] = None, size: int = If `size` is not a power of two or not between 16 and 4096. """ if not self.avatar_hash: - return urls.generate_cdn_url("embed/avatars", str(self.default_avatar), format_="png", size=None) + return self.default_avatar_url + if format_ is None and self.avatar_hash.startswith("a_"): format_ = "gif" elif format_ is None: @@ -169,10 +185,15 @@ def format_avatar_url(self, *, format_: typing.Optional[str] = None, size: int = return urls.generate_cdn_url("avatars", str(self.id), self.avatar_hash, format_=format_, size=size) @property - def default_avatar(self) -> int: + def default_avatar_index(self) -> int: """Integer representation of this user's default avatar.""" return int(self.discriminator) % 5 + @property + def default_avatar_url(self) -> str: + """URL for this user's default avatar.""" + return urls.generate_cdn_url("embed", "avatars", str(self.default_avatar_index), format_="png", size=None) + @marshaller.marshallable() @attr.s(eq=True, hash=True, kw_only=True, slots=True) @@ -215,3 +236,13 @@ class MyUser(User): This will always be `None` for bots. """ + + async def fetch_self(self) -> MyUser: + """Get this user's up-to-date object. + + Returns + ------- + hikari.users.User + The requested user object. + """ + return await self._components.rest.fetch_me() diff --git a/hikari/webhooks.py b/hikari/webhooks.py index 13fce50d39..c464dfe41f 100644 --- a/hikari/webhooks.py +++ b/hikari/webhooks.py @@ -30,6 +30,14 @@ from hikari import users from hikari.internal import marshaller from hikari.internal import more_enums +from hikari.internal import urls + +if typing.TYPE_CHECKING: + from hikari import channels as _channels + from hikari import embeds as _embeds + from hikari import files as _files + from hikari import guilds as _guilds + from hikari import messages as _messages @more_enums.must_be_unique @@ -64,7 +72,8 @@ class Webhook(bases.Unique, marshaller.Deserializable): channel_id: bases.Snowflake = marshaller.attrib(deserializer=bases.Snowflake, eq=False, hash=False, repr=True) """The channel ID this webhook is for.""" - user: typing.Optional[users.User] = marshaller.attrib( + author: typing.Optional[users.User] = marshaller.attrib( + raw_name="user", deserializer=users.User.deserialize, if_undefined=None, inherit_kwargs=True, @@ -97,3 +106,320 @@ class Webhook(bases.Unique, marshaller.Deserializable): This is only available for incoming webhooks that are created in the channel settings. """ + + async def execute( + self, + *, + content: str = ..., + username: str = ..., + avatar_url: str = ..., + tts: bool = ..., + wait: bool = False, + files: typing.Sequence[_files.BaseStream] = ..., + embeds: typing.Sequence[_embeds.Embed] = ..., + mentions_everyone: bool = True, + user_mentions: typing.Union[ + typing.Collection[typing.Union[bases.Snowflake, int, str, users.User]], bool + ] = True, + role_mentions: typing.Union[ + typing.Collection[typing.Union[bases.Snowflake, int, str, _guilds.GuildRole]], bool + ] = True, + ) -> typing.Optional[_messages.Message]: + """Execute the webhook to create a message. + + Parameters + ---------- + content : str + If specified, the message content to send with the message. + username : str + If specified, the username to override the webhook's username + for this request. + avatar_url : str + If specified, the url of an image to override the webhook's + avatar with for this request. + tts : bool + If specified, whether the message will be sent as a TTS message. + wait : bool + If specified, whether this request should wait for the webhook + to be executed and return the resultant message object. + files : typing.Sequence[hikari.files.BaseStream] + If specified, a sequence of files to upload. + embeds : typing.Sequence[hikari.embeds.Embed] + If specified, a sequence of between `1` to `10` embed objects + (inclusive) to send with the embed. + mentions_everyone : bool + Whether `@everyone` and `@here` mentions should be resolved by + discord and lead to actual pings, defaults to `True`. + user_mentions : typing.Union[typing.Collection[typing.Union[hikari.users.User, hikari.bases.Snowflake, int]], bool] + Either an array of user objects/IDs to allow mentions for, + `True` to allow all user mentions or `False` to block all + user mentions from resolving, defaults to `True`. + role_mentions : typing.Union[typing.Collection[typing.Union[hikari.guilds.GuildRole, hikari.bases.Snowflake, int]], bool] + Either an array of guild role objects/IDs to allow mentions for, + `True` to allow all role mentions or `False` to block all + role mentions from resolving, defaults to `True`. + + Returns + ------- + hikari.messages.Message, optional + The created message object, if `wait` is `True`, else `None`. + + Raises + ------ + hikari.errors.NotFound + If the current webhook is not found. + hikari.errors.BadRequest + This can be raised if the file is too large; if the embed exceeds + the defined limits; if the message content is specified only and + empty or greater than `2000` characters; if neither content, file + or embeds are specified. + If any invalid snowflake IDs are passed; a snowflake may be invalid + due to it being outside of the range of a 64 bit integer. + hikari.errors.Unauthorized + If you pass a token that's invalid for the target webhook. + ValueError + If either `Webhook.token` is `None` or more than 100 unique + objects/entities are passed for `role_mentions` or `user_mentions. + """ + if not self.token: + raise ValueError("Cannot send a message using a webhook where we don't know it's token.") + + return await self._components.rest.execute_webhook( + webhook=self.id, + webhook_token=self.token, + content=content, + username=username, + avatar_url=avatar_url, + tts=tts, + wait=wait, + files=files, + embeds=embeds, + mentions_everyone=mentions_everyone, + user_mentions=user_mentions, + role_mentions=role_mentions, + ) + + async def safe_execute( + self, + *, + content: str = ..., + username: str = ..., + avatar_url: str = ..., + tts: bool = ..., + wait: bool = False, + files: typing.Sequence[_files.BaseStream] = ..., + embeds: typing.Sequence[_embeds.Embed] = ..., + mentions_everyone: bool = False, + user_mentions: typing.Union[ + typing.Collection[typing.Union[bases.Snowflake, int, str, users.User]], bool + ] = False, + role_mentions: typing.Union[ + typing.Collection[typing.Union[bases.Snowflake, int, str, _guilds.GuildRole]], bool + ] = False, + ) -> typing.Optional[_messages.Message]: + """Execute the webhook to create a message with mention safety. + + This endpoint has the same signature as + `Webhook.execute_webhook` with the only difference being + that `mentions_everyone`, `user_mentions` and `role_mentions` default to + `False`. + """ + if not self.token: + raise ValueError("Cannot execute a webhook with a unknown token (set to `None`).") + + return await self._components.rest.safe_webhook_execute( + webhook=self.id, + webhook_token=self.token, + content=content, + username=username, + avatar_url=avatar_url, + tts=tts, + wait=wait, + files=files, + embeds=embeds, + mentions_everyone=mentions_everyone, + user_mentions=user_mentions, + role_mentions=role_mentions, + ) + + async def delete(self, *, use_token: typing.Optional[bool] = None,) -> None: + """Delete this webhook. + + Parameters + ---------- + use_token : bool, optional + If set to `True` then the webhook's token will be used for this + request; if set to `False` then bot authorization will be used; + if not specified then the webhook's token will be used for the + request if it's set else bot authorization. + + Raises + ------ + hikari.errors.NotFound + If this webhook is not found. + hikari.errors.Forbidden + If you either lack the `MANAGE_WEBHOOKS` permission or + aren't a member of the guild this webhook belongs to. + ValueError + If `use_token` is passed as `True` when `Webhook.token` is `None`. + """ + if use_token and not self.token: + raise ValueError("This webhook's token is unknown.") + + if use_token is None and self.token: + use_token = True + + await self._components.rest.delete_webhook(webhook=self.id, webhook_token=self.token if use_token else ...) + + async def edit( + self, + *, + name: str = ..., + avatar: typing.Optional[_files.BaseStream] = ..., + channel: typing.Union[bases.Snowflake, int, str, _channels.GuildChannel] = ..., + reason: str = ..., + use_token: typing.Optional[bool] = None, + ) -> Webhook: + """Edit this webhook. + + Parameters + ---------- + name : str + If specified, the new name string. + avatar : hikari.files.BaseStream, optional + If specified, the new avatar image. If `None`, then + it is removed. + channel : typing.Union[hikari.channels.GuildChannel, hikari.bases.Snowflake, int] + If specified, the object or ID of the new channel the given + webhook should be moved to. + reason : str + If specified, the audit log reason explaining why the operation + was performed. This field will be used when using the webhook's + token rather than bot authorization. + use_token : bool, optional + If set to `True` then the webhook's token will be used for this + request; if set to `False` then bot authorization will be used; + if not specified then the webhook's token will be used for the + request if it's set else bot authorization. + + Returns + ------- + hikari.webhooks.Webhook + The updated webhook object. + + Raises + ------ + hikari.errors.BadRequest + If any invalid snowflake IDs are passed; a snowflake may be invalid + due to it being outside of the range of a 64 bit integer. + hikari.errors.NotFound + If either the webhook or the channel aren't found. + hikari.errors.Forbidden + If you either lack the `MANAGE_WEBHOOKS` permission or + aren't a member of the guild this webhook belongs to. + hikari.errors.Unauthorized + If you pass a token that's invalid for the target webhook. + ValueError + If `use_token` is passed as `True` when `Webhook.token` is `None`. + """ + if use_token and not self.token: + raise ValueError("This webhook's token is unknown.") + + if use_token is None and self.token: + use_token = True + + return await self._components.rest.update_webhook( + webhook=self.id, + webhook_token=self.token if use_token else ..., + name=name, + avatar=avatar, + channel=channel, + reason=reason, + ) + + async def fetch_channel(self) -> _channels.PartialChannel: + """Fetch the channel this webhook is for. + + Returns + ------- + hikari.channels.PartialChannel + The object of the channel this webhook targets. + + Raises + ------ + hikari.errors.Forbidden + If you don't have access to the channel this webhook belongs to. + hikari.errors.NotFound + If the channel this message was created in does not exist. + """ + return await self._components.rest.fetch_channel(channel=self.channel_id) + + async def fetch_guild(self) -> _guilds.Guild: + """Fetch the guild this webhook belongs to. + + Returns + ------- + hikari.guilds.Guild + The object of the channel this message belongs to. + + Raises + ------ + hikari.errors.Forbidden + If you don't have access to the guild this webhook belongs to or it + doesn't exist. + """ + return await self._components.rest.fetch_guild(guild=self.guild_id) + + async def fetch_self(self, *, use_token: typing.Optional[bool] = None) -> Webhook: + if use_token and not self.token: + raise ValueError("This webhook's token is unknown.") + + if use_token is None and self.token: + use_token = True + + return await self._components.rest.fetch_webhook( + webhook=self.id, webhook_token=self.token if use_token else ... + ) + + @property + def avatar_url(self) -> str: + """URL for this webhook's custom avatar if set, else default.""" + return self.format_avatar_url() + + @property + def default_avatar_index(self) -> int: + """Integer representation of this webhook's default avatar.""" + return 0 + + @property + def default_avatar_url(self) -> str: + """URL for this webhook's default avatar.""" + return urls.generate_cdn_url("embed", "avatars", str(self.default_avatar_index), format_="png", size=None) + + def format_avatar_url(self, format_: str = "png", size: int = 4096) -> str: + """Generate the avatar URL for this webhook's custom avatar if set, else it's default avatar. + + Parameters + ---------- + format_ : str + The format to use for this URL, defaults to `png`. + Supports `png`, `jpeg`, `jpg`, `webp`. This will be ignored for + default avatars which can only be `png`. + size : int + The size to set for the URL, defaults to `4096`. + Can be any power of two between 16 and 4096. + Will be ignored for default avatars. + + Returns + ------- + str + The string URL. + + Raises + ------ + ValueError + If `size` is not a power of two or not between 16 and 4096. + """ + if not self.avatar_hash: + return self.default_avatar_url + return urls.generate_cdn_url("avatars", str(self.id), self.avatar_hash, format_=format_, size=size) diff --git a/tests/hikari/test_guilds.py b/tests/hikari/test_guilds.py index e3fe06396d..bf9e60bee7 100644 --- a/tests/hikari/test_guilds.py +++ b/tests/hikari/test_guilds.py @@ -607,13 +607,27 @@ def test_format_avatar_url_when_discriminator_and_avatar_hash_unset(self, test_p assert test_presence_user_obj.format_avatar_url() is unset.UNSET users.User.format_avatar_url.assert_not_called() - def test_default_avatar_when_discriminator_set(self, test_presence_user_obj): + def test_default_avatar_index_when_discriminator_set(self, test_presence_user_obj): test_presence_user_obj.discriminator = 4242 - assert test_presence_user_obj.default_avatar == 2 + assert test_presence_user_obj.default_avatar_index == 2 def test_default_avatar_when_discriminator_unset(self, test_presence_user_obj): test_presence_user_obj.discriminator = unset.UNSET - assert test_presence_user_obj.default_avatar is unset.UNSET + assert test_presence_user_obj.default_avatar_index is unset.UNSET + + def test_default_avatar_url_when_discriminator_is_set(self, test_presence_user_obj): + mock_url = "https://cdn.discordapp.com/embed/avatars/2.png" + test_presence_user_obj.discriminator = 4232 + with mock.patch.object(urls, "generate_cdn_url", return_value=mock_url): + url = test_presence_user_obj.default_avatar_url is mock_url + urls.generate_cdn_url.assert_called_once_with("embed", "avatars", "2", format_="png", size=None) + + def test_default_avatar_url_when_discriminator_is_unset(self, test_presence_user_obj): + mock_url = ... + test_presence_user_obj.discriminator = unset.UNSET + with mock.patch.object(urls, "generate_cdn_url", return_value=mock_url): + assert test_presence_user_obj.default_avatar_url is unset.UNSET + urls.generate_cdn_url.assert_not_called() @pytest.fixture() diff --git a/tests/hikari/test_users.py b/tests/hikari/test_users.py index ed0b3d9b64..3602c001a7 100644 --- a/tests/hikari/test_users.py +++ b/tests/hikari/test_users.py @@ -19,8 +19,10 @@ import mock import pytest +from hikari import bases from hikari import users from hikari.clients import components +from hikari.clients import rest from hikari.internal import urls @@ -55,8 +57,8 @@ def test_oauth_user_payload(): @pytest.fixture() -def mock_components(): - return mock.MagicMock(components.Components) +def mock_components() -> components.Components: + return mock.MagicMock(components.Components, rest=mock.AsyncMock(rest.RESTClient)) class TestUser: @@ -71,9 +73,10 @@ def test_deserialize(self, test_user_payload, mock_components): assert user_obj.flags == users.UserFlag.VERIFIED_BOT_DEVELOPER @pytest.fixture() - def user_obj(self, test_user_payload): + def user_obj(self, test_user_payload, mock_components): return users.User( - id="115590097100865541", + components=mock_components, + id=bases.Snowflake(115590097100865541), username=None, avatar_hash="b3b24c6d7cbcdec129d5d537067061a8", discriminator="6127", @@ -82,6 +85,13 @@ def user_obj(self, test_user_payload): flags=None, ) + @pytest.mark.asyncio + async def test_fetch_self(self, user_obj, mock_components): + mock_user = mock.MagicMock(users.User) + mock_components.rest.fetch_user.return_value = mock_user + assert await user_obj.fetch_self() is mock_user + mock_components.rest.fetch_user.assert_called_once_with(user=115590097100865541) + def test_avatar_url(self, user_obj): mock_url = "https://cdn.discordapp.com/avatars/115590097100865541" with mock.patch.object(urls, "generate_cdn_url", return_value=mock_url): @@ -89,8 +99,15 @@ def test_avatar_url(self, user_obj): urls.generate_cdn_url.assert_called_once() assert url == mock_url - def test_default_avatar(self, user_obj): - assert user_obj.default_avatar == 2 + def test_default_avatar_index(self, user_obj): + assert user_obj.default_avatar_index == 2 + + def test_default_avatar_url(self, user_obj): + mock_url = "https://cdn.discordapp.com/embed/avatars/2.png" + with mock.patch.object(urls, "generate_cdn_url", return_value=mock_url): + url = user_obj.default_avatar_url + urls.generate_cdn_url.assert_called_once_with("embed", "avatars", "2", format_="png", size=None) + assert url == mock_url def test_format_avatar_url_when_animated(self, user_obj): mock_url = "https://cdn.discordapp.com/avatars/115590097100865541/a_820d0e50543216e812ad94e6ab7.gif?size=3232" @@ -107,7 +124,7 @@ def test_format_avatar_url_default(self, user_obj): mock_url = "https://cdn.discordapp.com/embed/avatars/2.png" with mock.patch.object(urls, "generate_cdn_url", return_value=mock_url): url = user_obj.format_avatar_url(size=3232) - urls.generate_cdn_url("embed/avatars", "115590097100865541", format_="png", size=None) + urls.generate_cdn_url.assert_called_once_with("embed", "avatars", "2", format_="png", size=None) assert url == mock_url def test_format_avatar_url_when_format_specified(self, user_obj): @@ -133,3 +150,26 @@ def test_deserialize(self, test_oauth_user_payload, mock_components): assert my_user_obj.email == "blahblah@blah.blah" assert my_user_obj.flags == users.UserFlag.DISCORD_PARTNER | users.UserFlag.DISCORD_EMPLOYEE assert my_user_obj.premium_type is users.PremiumType.NITRO_CLASSIC + + @pytest.fixture() + def my_user_obj(self, mock_components): + return users.MyUser( + components=mock_components, + id=None, + username=None, + avatar_hash=None, + discriminator=None, + is_mfa_enabled=None, + locale=None, + is_verified=None, + email=None, + flags=None, + premium_type=None, + ) + + @pytest.mark.asyncio + async def test_fetch_me(self, my_user_obj, mock_components): + mock_user = mock.MagicMock(users.MyUser) + mock_components.rest.fetch_me.return_value = mock_user + assert await my_user_obj.fetch_self() is mock_user + mock_components.rest.fetch_me.assert_called_once() diff --git a/tests/hikari/test_webhook.py b/tests/hikari/test_webhook.py index 5e65960a36..e07064bf68 100644 --- a/tests/hikari/test_webhook.py +++ b/tests/hikari/test_webhook.py @@ -19,15 +19,22 @@ import mock import pytest +from hikari import bases +from hikari import channels +from hikari import embeds +from hikari import files +from hikari import messages from hikari import users from hikari import webhooks from hikari.clients import components +from hikari.clients import rest +from hikari.internal import urls from tests.hikari import _helpers @pytest.fixture() -def mock_components(): - return mock.MagicMock(components.Components) +def mock_components() -> components.Components: + return mock.MagicMock(components.Components, rest=mock.AsyncMock(rest.RESTClient)) class TestWebhook: @@ -46,7 +53,7 @@ def test_deserialize(self, mock_components): mock_user = mock.MagicMock(users.User) with _helpers.patch_marshal_attr( - webhooks.Webhook, "user", deserializer=users.User.deserialize, return_value=mock_user + webhooks.Webhook, "author", deserializer=users.User.deserialize, return_value=mock_user ) as mock_user_deserializer: webhook_obj = webhooks.Webhook.deserialize(payload, components=mock_components) mock_user_deserializer.assert_called_once_with(test_user_payload, components=mock_components) @@ -55,7 +62,312 @@ def test_deserialize(self, mock_components): assert webhook_obj.type == webhooks.WebhookType.INCOMING assert webhook_obj.guild_id == 123 assert webhook_obj.channel_id == 456 - assert webhook_obj.user == mock_user + assert webhook_obj.author is mock_user assert webhook_obj.name == "hikari webhook" assert webhook_obj.avatar_hash == "bb71f469c158984e265093a81b3397fb" assert webhook_obj.token == "ueoqrialsdfaKJLKfajslkdf" + + @pytest.fixture() + def webhook_obj(self, mock_components): + return webhooks.Webhook( + components=mock_components, + id=bases.Snowflake(123123), + type=None, + guild_id=None, + channel_id=None, + author=None, + name=None, + avatar_hash="b3b24c6d7cbcdec129d5d537067061a8", + token="blah.blah.blah", + ) + + @pytest.mark.asyncio + async def test_execute_without_optionals(self, webhook_obj, mock_components): + mock_webhook = mock.MagicMock(messages.Message) + mock_components.rest.execute_webhook.return_value = mock_webhook + assert await webhook_obj.execute() is mock_webhook + mock_components.rest.execute_webhook.assert_called_once_with( + webhook=123123, + webhook_token="blah.blah.blah", + content=..., + username=..., + avatar_url=..., + tts=..., + wait=False, + files=..., + embeds=..., + mentions_everyone=True, + user_mentions=True, + role_mentions=True, + ) + + @pytest.mark.asyncio + async def test_execute_with_optionals(self, webhook_obj, mock_components): + mock_webhook = mock.MagicMock(messages.Message) + mock_files = mock.MagicMock(files.BaseStream) + mock_embed = mock.MagicMock(embeds.Embed) + mock_components.rest.execute_webhook.return_value = mock_webhook + result = await webhook_obj.execute( + content="A CONTENT", + username="Name user", + avatar_url=">///<", + tts=True, + wait=True, + files=[mock_files], + embeds=[mock_embed], + mentions_everyone=False, + user_mentions=[123, 456], + role_mentions=[444], + ) + assert result is mock_webhook + mock_components.rest.execute_webhook.assert_called_once_with( + webhook=123123, + webhook_token="blah.blah.blah", + content="A CONTENT", + username="Name user", + avatar_url=">///<", + tts=True, + wait=True, + files=[mock_files], + embeds=[mock_embed], + mentions_everyone=False, + user_mentions=[123, 456], + role_mentions=[444], + ) + + @_helpers.assert_raises(type_=ValueError) + @pytest.mark.asyncio + async def test_execute_raises_value_error_without_token(self, webhook_obj): + webhook_obj.token = None + await webhook_obj.execute() + + @pytest.mark.asyncio + async def test_safe_execute_without_optionals(self, webhook_obj, mock_components): + mock_webhook = mock.MagicMock(messages.Message) + mock_components.rest.safe_webhook_execute = mock.AsyncMock(return_value=mock_webhook) + assert await webhook_obj.safe_execute() is mock_webhook + mock_components.rest.safe_webhook_execute.assert_called_once_with( + webhook=123123, + webhook_token="blah.blah.blah", + content=..., + username=..., + avatar_url=..., + tts=..., + wait=False, + files=..., + embeds=..., + mentions_everyone=False, + user_mentions=False, + role_mentions=False, + ) + + @pytest.mark.asyncio + async def test_safe_execute_with_optionals(self, webhook_obj, mock_components): + mock_webhook = mock.MagicMock(messages.Message) + mock_files = mock.MagicMock(files.BaseStream) + mock_embed = mock.MagicMock(embeds.Embed) + mock_components.rest.safe_webhook_execute = mock.AsyncMock(return_value=mock_webhook) + result = await webhook_obj.safe_execute( + content="A CONTENT", + username="Name user", + avatar_url=">///<", + tts=True, + wait=True, + files=[mock_files], + embeds=[mock_embed], + mentions_everyone=False, + user_mentions=[123, 456], + role_mentions=[444], + ) + assert result is mock_webhook + mock_components.rest.safe_webhook_execute.assert_called_once_with( + webhook=123123, + webhook_token="blah.blah.blah", + content="A CONTENT", + username="Name user", + avatar_url=">///<", + tts=True, + wait=True, + files=[mock_files], + embeds=[mock_embed], + mentions_everyone=False, + user_mentions=[123, 456], + role_mentions=[444], + ) + + @_helpers.assert_raises(type_=ValueError) + @pytest.mark.asyncio + async def test_safe_execute_raises_value_error_without_token(self, webhook_obj): + webhook_obj.token = None + await webhook_obj.safe_execute() + + @pytest.mark.asyncio + async def test_delete_with_token(self, webhook_obj, mock_components): + mock_components.rest.delete_webhook.return_value = ... + assert await webhook_obj.delete() is None + mock_components.rest.delete_webhook.assert_called_once_with(webhook=123123, webhook_token="blah.blah.blah") + + @pytest.mark.asyncio + async def test_delete_without_token(self, webhook_obj, mock_components): + webhook_obj.token = None + mock_components.rest.delete_webhook.return_value = ... + assert await webhook_obj.delete() is None + mock_components.rest.delete_webhook.assert_called_once_with(webhook=123123, webhook_token=...) + + @pytest.mark.asyncio + async def test_delete_with_use_token_set_to_true(self, webhook_obj, mock_components): + mock_components.rest.delete_webhook.return_value = ... + assert await webhook_obj.delete(use_token=True) is None + mock_components.rest.delete_webhook.assert_called_once_with(webhook=123123, webhook_token="blah.blah.blah") + + @pytest.mark.asyncio + async def test_delete_with_use_token_set_to_false(self, webhook_obj, mock_components): + mock_components.rest.delete_webhook.return_value = ... + assert await webhook_obj.delete(use_token=False) is None + mock_components.rest.delete_webhook.assert_called_once_with(webhook=123123, webhook_token=...) + + @_helpers.assert_raises(type_=ValueError) + @pytest.mark.asyncio + async def test_delete_raises_value_error_when_use_token_set_to_true_without_token( + self, webhook_obj, mock_components + ): + webhook_obj.token = None + await webhook_obj.delete(use_token=True) + + @pytest.mark.asyncio + async def test_edit_without_optionals_nor_token(self, webhook_obj, mock_components): + webhook_obj.token = None + mock_webhook = mock.MagicMock(webhooks.Webhook) + mock_components.rest.update_webhook.return_value = mock_webhook + assert await webhook_obj.edit() is mock_webhook + mock_components.rest.update_webhook.assert_called_once_with( + webhook=123123, webhook_token=..., name=..., avatar=..., channel=..., reason=... + ) + + @pytest.mark.asyncio + async def test_edit_with_optionals_and_token(self, webhook_obj, mock_components): + mock_webhook = mock.MagicMock(webhooks.Webhook) + mock_avatar = mock.MagicMock(files.BaseStream) + mock_channel = mock.MagicMock(channels.GuildChannel) + mock_components.rest.update_webhook.return_value = mock_webhook + result = await webhook_obj.edit(name="A name man", avatar=mock_avatar, channel=mock_channel, reason="xd420") + assert result is mock_webhook + mock_components.rest.update_webhook.assert_called_once_with( + webhook=123123, + webhook_token="blah.blah.blah", + name="A name man", + avatar=mock_avatar, + channel=mock_channel, + reason="xd420", + ) + + @pytest.mark.asyncio + async def test_edit_with_use_token_set_to_true(self, webhook_obj, mock_components): + mock_webhook = mock.MagicMock(webhooks.Webhook) + mock_components.rest.update_webhook.return_value = mock_webhook + assert await webhook_obj.edit(use_token=True) is mock_webhook + mock_components.rest.update_webhook.assert_called_once_with( + webhook=123123, webhook_token="blah.blah.blah", name=..., avatar=..., channel=..., reason=... + ) + + @pytest.mark.asyncio + async def test_edit_with_use_token_set_to_false(self, webhook_obj, mock_components): + mock_webhook = mock.MagicMock(webhooks.Webhook) + mock_components.rest.update_webhook.return_value = mock_webhook + assert await webhook_obj.edit(use_token=False) is mock_webhook + mock_components.rest.update_webhook.assert_called_once_with( + webhook=123123, webhook_token=..., name=..., avatar=..., channel=..., reason=... + ) + + @_helpers.assert_raises(type_=ValueError) + @pytest.mark.asyncio + async def test_edit_raises_value_error_when_use_token_set_to_true_without_token(self, webhook_obj, mock_components): + webhook_obj.token = None + await webhook_obj.edit(use_token=True) + + @pytest.mark.asyncio + async def test_fetch_channel(self, webhook_obj, mock_components): + webhook_obj.channel_id = bases.Snowflake(202020) + mock_channel = mock.MagicMock(channels.GuildChannel) + mock_components.rest.fetch_channel.return_value = mock_channel + assert await webhook_obj.fetch_channel() is mock_channel + mock_components.rest.fetch_channel.assert_called_once_with(channel=202020) + + @pytest.mark.asyncio + async def test_fetch_guild(self, webhook_obj, mock_components): + webhook_obj.guild_id = bases.Snowflake(202020) + mock_channel = mock.MagicMock(channels.GuildChannel) + mock_components.rest.fetch_guild.return_value = mock_channel + assert await webhook_obj.fetch_guild() is mock_channel + mock_components.rest.fetch_guild.assert_called_once_with(guild=202020) + + @pytest.mark.asyncio + async def test_fetch_self_with_token(self, webhook_obj, mock_components): + mock_webhook = mock.MagicMock(webhooks.Webhook) + mock_components.rest.fetch_webhook.return_value = mock_webhook + assert await webhook_obj.fetch_self() is mock_webhook + mock_components.rest.fetch_webhook.assert_called_once_with(webhook=123123, webhook_token="blah.blah.blah") + + @pytest.mark.asyncio + async def test_fetch_self_without_token(self, webhook_obj, mock_components): + webhook_obj.token = None + mock_webhook = mock.MagicMock(webhooks.Webhook) + mock_components.rest.fetch_webhook.return_value = mock_webhook + assert await webhook_obj.fetch_self() is mock_webhook + mock_components.rest.fetch_webhook.assert_called_once_with(webhook=123123, webhook_token=...) + + @pytest.mark.asyncio + async def test_fetch_self_with_use_token_set_to_true(self, webhook_obj, mock_components): + mock_webhook = mock.MagicMock(webhooks.Webhook) + mock_components.rest.fetch_webhook.return_value = mock_webhook + assert await webhook_obj.fetch_self(use_token=True) is mock_webhook + mock_components.rest.fetch_webhook.assert_called_once_with(webhook=123123, webhook_token="blah.blah.blah") + + @pytest.mark.asyncio + async def test_fetch_self_with_use_token_set_to_false(self, webhook_obj, mock_components): + mock_webhook = mock.MagicMock(webhooks.Webhook) + mock_components.rest.fetch_webhook.return_value = mock_webhook + assert await webhook_obj.fetch_self(use_token=False) is mock_webhook + mock_components.rest.fetch_webhook.assert_called_once_with(webhook=123123, webhook_token=...) + + @_helpers.assert_raises(type_=ValueError) + @pytest.mark.asyncio + async def test_fetch_self_raises_value_error_when_use_token_set_to_true_without_token( + self, webhook_obj, mock_components + ): + webhook_obj.token = None + assert await webhook_obj.fetch_self(use_token=True) + + def test_avatar_url(self, webhook_obj): + mock_url = "https://cdn.discordapp.com/avatars/115590097100865541" + with mock.patch.object(urls, "generate_cdn_url", return_value=mock_url): + url = webhook_obj.avatar_url + urls.generate_cdn_url.assert_called_once() + assert url == mock_url + + def test_test_default_avatar_index(self, webhook_obj): + assert webhook_obj.default_avatar_index == 0 + + def test_default_avatar_url(self, webhook_obj): + mock_url = "https://cdn.discordapp.com/embed/avatars/2.png" + with mock.patch.object(urls, "generate_cdn_url", return_value=mock_url): + url = webhook_obj.default_avatar_url + urls.generate_cdn_url.assert_called_once_with("embed", "avatars", "0", format_="png", size=None) + assert url == mock_url + + def test_format_avatar_url_default(self, webhook_obj): + webhook_obj.avatar_hash = None + mock_url = "https://cdn.discordapp.com/embed/avatars/2.png" + with mock.patch.object(urls, "generate_cdn_url", return_value=mock_url): + url = webhook_obj.format_avatar_url(size=3232) + urls.generate_cdn_url.assert_called_once_with("embed", "avatars", "0", format_="png", size=None) + assert url == mock_url + + def test_format_avatar_url_when_format_specified(self, webhook_obj): + mock_url = "https://cdn.discordapp.com/avatars/115590097100865541/b3b24c6d7c37067061a8.nyaapeg?size=1024" + with mock.patch.object(urls, "generate_cdn_url", return_value=mock_url): + url = webhook_obj.format_avatar_url(format_="nyaapeg", size=1024) + urls.generate_cdn_url.assert_called_once_with( + "avatars", "123123", "b3b24c6d7cbcdec129d5d537067061a8", format_="nyaapeg", size=1024 + ) + assert url == mock_url From 83abe6371bcfb9f592914dee093de3daaebc7e7b Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Fri, 15 May 2020 10:49:17 +0100 Subject: [PATCH 329/922] Removed dead function from configs.py --- hikari/clients/configs.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/hikari/clients/configs.py b/hikari/clients/configs.py index 6f4f4b4c05..3e26df60be 100644 --- a/hikari/clients/configs.py +++ b/hikari/clients/configs.py @@ -361,10 +361,6 @@ def _rest_url_default() -> str: return urls.REST_API_URL -def _cdn_url_default() -> str: - return urls.BASE_CDN_URL - - def _oauth2_url_default() -> str: return urls.OAUTH2_API_URL From 0629986fb90ed936e307eae4f4b0364c506eb6d2 Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Fri, 15 May 2020 11:18:30 +0100 Subject: [PATCH 330/922] Added logic to log 429's as a warning; those hopefully shouldn't occur regularly. --- hikari/net/rest.py | 11 +++++++++++ tests/hikari/net/test_rest.py | 4 ++++ 2 files changed, 15 insertions(+) diff --git a/hikari/net/rest.py b/hikari/net/rest.py index d32332b2ab..d3e2dff186 100644 --- a/hikari/net/rest.py +++ b/hikari/net/rest.py @@ -325,6 +325,17 @@ async def _handle_rate_limits_for_response(self, compiled_route, response): retry_after = float(body["retry_after"]) / 1_000 self.global_ratelimiter.throttle(retry_after) + self.logger.warning( + "you are being rate-limited globally - trying again after %ss", retry_after, + ) + else: + self.logger.warning( + "you are being rate-limited on bucket %s for route %s - trying again after %ss", + bucket, + compiled_route, + reset, + ) + raise _RateLimited() # We might find out Cloudflare causes this scenario to occur. diff --git a/tests/hikari/net/test_rest.py b/tests/hikari/net/test_rest.py index c3d0355f2a..feea9d715c 100644 --- a/tests/hikari/net/test_rest.py +++ b/tests/hikari/net/test_rest.py @@ -22,6 +22,7 @@ import email.utils import http import json +import logging import aiohttp import mock @@ -138,6 +139,7 @@ def rest_impl(self, bucket_ratelimiters, global_ratelimiter): stack.enter_context(mock.patch.object(ratelimits, "ManualRateLimiter", return_value=global_ratelimiter)) with stack: client = rest.REST(base_url="http://example.bloop.com", token="Bot blah.blah.blah") + client.logger = mock.MagicMock(spec_set=logging.Logger) client.json_deserialize = json.loads client.serialize = json.dumps client._perform_request = mock.AsyncMock(spec_set=client._perform_request, return_value=MockResponse(None)) @@ -376,6 +378,7 @@ def rest_impl(self, bucket_ratelimiters, global_ratelimiter): stack.enter_context(mock.patch.object(ratelimits, "ManualRateLimiter", return_value=global_ratelimiter)) with stack: client = rest.REST(base_url="http://example.bloop.com", token="Bot blah.blah.blah") + client.logger = mock.MagicMock(spec_set=logging.Logger) return client @pytest.mark.parametrize("status", [200, 201, 202, 203, 204, 400, 401, 403, 404, 429, 500]) @@ -481,6 +484,7 @@ def rest_impl(self): stack.enter_context(mock.patch.object(ratelimits, "ManualRateLimiter")) with stack: client = rest.REST(base_url="https://discord.com/api/v6", token="Bot blah.blah.blah") + client.logger = mock.MagicMock(spec_set=logging.Logger) client._request_json_response = mock.AsyncMock(return_value=...) client.client_session = mock.MagicMock(aiohttp.ClientSession, spec_set=True) From 32ae3fa6fcd0ec70e9c46f0ce52949e957ae3387 Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Fri, 15 May 2020 16:01:56 +0100 Subject: [PATCH 331/922] Added missing ratelimit tests. Pretty sure I already did this? --- tests/hikari/net/test_ratelimits.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/tests/hikari/net/test_ratelimits.py b/tests/hikari/net/test_ratelimits.py index 16fe3e4c02..63b79578b4 100644 --- a/tests/hikari/net/test_ratelimits.py +++ b/tests/hikari/net/test_ratelimits.py @@ -592,7 +592,7 @@ async def test_acquire_route_returns_acquired_future_for_new_bucket(self): route = mock.MagicMock() route.create_real_bucket_hash = mock.MagicMock(return_value="eat pant;bobs") bucket = mock.MagicMock() - mgr.routes_to_hashes[route] = "eat pant" + mgr.routes_to_hashes[route.route_template] = "eat pant" mgr.real_hashes_to_buckets["eat pant;bobs"] = bucket f = mgr.acquire(route) @@ -613,11 +613,11 @@ async def test_update_rate_limits_if_right_bucket_hash_does_nothing_to_hash(self with ratelimits.RESTBucketManager() as mgr: route = mock.MagicMock() route.create_real_bucket_hash = mock.MagicMock(wraps=lambda intial_hash: intial_hash + ";bobs") - mgr.routes_to_hashes[route] = "123" + mgr.routes_to_hashes[route.route_template] = "123" bucket = mock.MagicMock() mgr.real_hashes_to_buckets["123;bobs"] = bucket mgr.update_rate_limits(route, "123", 22, 23, datetime.datetime.now(), datetime.datetime.now()) - assert mgr.routes_to_hashes[route] == "123" + assert mgr.routes_to_hashes[route.route_template] == "123" assert mgr.real_hashes_to_buckets["123;bobs"] is bucket @pytest.mark.asyncio @@ -625,7 +625,7 @@ async def test_update_rate_limits_updates_params(self): with ratelimits.RESTBucketManager() as mgr: route = mock.MagicMock() route.create_real_bucket_hash = mock.MagicMock(wraps=lambda intial_hash: intial_hash + ";bobs") - mgr.routes_to_hashes[route] = "123" + mgr.routes_to_hashes[route.route_template] = "123" bucket = mock.MagicMock() mgr.real_hashes_to_buckets["123;bobs"] = bucket date = datetime.datetime.now().replace(year=2004) @@ -636,6 +636,12 @@ async def test_update_rate_limits_updates_params(self): mgr.update_rate_limits(route, "123", 22, 23, date, reset_at) bucket.update_rate_limit.assert_called_once_with(22, 23, expect_reset_at_monotonic) + @pytest.mark.parametrize(("gc_task", "is_started"), [(None, False), (object(), True)]) + def test_is_started(self, gc_task, is_started): + with ratelimits.RESTBucketManager() as mgr: + mgr.gc_task = gc_task + assert mgr.is_started is is_started + class TestExponentialBackOff: def test_reset(self): From 695591343b5d217ea93a258e4fc6976b3e246c41 Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Fri, 15 May 2020 16:09:25 +0100 Subject: [PATCH 332/922] Fixed broken colours tests that were causing decrease in test %age. --- hikari/colors.py | 38 ++++++++++++++----------------------- hikari/colours.py | 3 +-- tests/hikari/test_colors.py | 6 +++--- 3 files changed, 18 insertions(+), 29 deletions(-) diff --git a/hikari/colors.py b/hikari/colors.py index 841a59b13f..9cf6d494ac 100644 --- a/hikari/colors.py +++ b/hikari/colors.py @@ -19,7 +19,7 @@ """Model that represents a common RGB color and provides simple conversions to other common color systems.""" from __future__ import annotations -__all__ = ["Color", "ColorCompatibleT"] +__all__ = ["Color"] import string import typing @@ -157,8 +157,7 @@ def raw_hex_code(self) -> str: @property def is_web_safe(self) -> bool: # noqa: D401 """`True` if the color is web safe, `False` otherwise.""" - hex_code = self.raw_hex_code - return all(_all_same(*c) for c in (hex_code[:2], hex_code[2:4], hex_code[4:])) + return not (((self & 0xFF0000) % 0x110000) or ((self & 0xFF00) % 0x1100) or ((self & 0xFF) % 0x11)) @classmethod def from_rgb(cls, red: int, green: int, blue: int) -> Color: @@ -312,7 +311,18 @@ def from_bytes(cls, bytes_: typing.Sequence[int], byteorder: str, *, signed: boo return Color(int.from_bytes(bytes_, byteorder, signed=signed)) @classmethod - def of(cls, *values: ColorCompatibleT) -> Color: + def of( + cls, + *values: typing.Union[ + Color, + typing.SupportsInt, + typing.Tuple[typing.SupportsInt, typing.SupportsInt, typing.SupportsInt], + typing.Tuple[typing.SupportsFloat, typing.SupportsFloat, typing.SupportsFloat], + typing.Sequence[typing.SupportsInt], + typing.Sequence[typing.SupportsFloat], + str, + ], + ) -> Color: """Convert the value to a `Color`. Parameters @@ -371,23 +381,3 @@ def to_bytes(self, length: int, byteorder: str, *, signed: bool = True) -> bytes The bytes representation of the Color. """ return int(self).to_bytes(length, byteorder, signed=signed) - - -def _all_same(first, *rest): - for r in rest: - if r != first: - return False - - return True - - -ColorCompatibleT = typing.Union[ - Color, - typing.SupportsInt, - typing.Tuple[typing.SupportsInt, typing.SupportsInt, typing.SupportsInt], - typing.Tuple[typing.SupportsFloat, typing.SupportsFloat, typing.SupportsFloat], - typing.Sequence[typing.SupportsInt], - typing.Sequence[typing.SupportsFloat], - str, -] -"""Any type that can be converted into a color object.""" diff --git a/hikari/colours.py b/hikari/colours.py index 9861fe7ddd..1aec36fe62 100644 --- a/hikari/colours.py +++ b/hikari/colours.py @@ -20,7 +20,6 @@ from __future__ import annotations -__all__ = ["Colour", "ColourCompatibleT"] +__all__ = ["Colour"] from hikari.colors import Color as Colour -from hikari.colors import ColorCompatibleT as ColourCompatibleT diff --git a/tests/hikari/test_colors.py b/tests/hikari/test_colors.py index 820c63c019..91699254f6 100644 --- a/tests/hikari/test_colors.py +++ b/tests/hikari/test_colors.py @@ -105,15 +105,15 @@ def test_Color_from_rgb_float(self, r, g, b, expected): @_helpers.assert_raises(type_=ValueError) def test_color_from_rgb_float_raises_value_error_on_invalid_red(self): - colors.Color.from_rgb_float(float(0x999), 32.0, 32.0) + colors.Color.from_rgb_float(1.5, 0.5, 0.5) @_helpers.assert_raises(type_=ValueError) def test_color_from_rgb_float_raises_value_error_on_invalid_green(self): - colors.Color.from_rgb_float(32.0, float(0x999), 32.0) + colors.Color.from_rgb_float(0.5, 1.5, 0.5) @_helpers.assert_raises(type_=ValueError) def test_color_from_rgb_float_raises_value_error_on_invalid_blue(self): - colors.Color.from_rgb_float(32.0, 32.0, float(0x999)) + colors.Color.from_rgb_float(0.5, 0.5, 1.5) @pytest.mark.parametrize(["input", "r", "g", "b"], [(0x91827, 0x9, 0x18, 0x27), (0x551AFF, 0x55, 0x1A, 0xFF)]) def test_Color_rgb(self, input, r, g, b): From e73b4a6a8f2ac21ea0a5969510fc5fbaf3e3fb0a Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Fri, 15 May 2020 16:10:50 +0100 Subject: [PATCH 333/922] Removed utterly pointless string interpolations. --- hikari/colors.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/hikari/colors.py b/hikari/colors.py index 9cf6d494ac..137aa9d5ef 100644 --- a/hikari/colors.py +++ b/hikari/colors.py @@ -185,11 +185,11 @@ def from_rgb(cls, red: int, green: int, blue: int) -> Color: If red, green, or blue are outside the range [0x0, 0xFF]. """ if not 0 <= red <= 0xFF: - raise ValueError(f"red must be in the inclusive range of 0 and {0xFF}") + raise ValueError("red must be in the inclusive range of 0 and 255") if not 0 <= green <= 0xFF: - raise ValueError(f"green must be in the inclusive range of 0 and {0xFF}") + raise ValueError("green must be in the inclusive range of 0 and 255") if not 0 <= blue <= 0xFF: - raise ValueError(f"blue must be in the inclusive range of 0 and {0xFF}") + raise ValueError("blue must be in the inclusive range of 0 and 255") # noinspection PyTypeChecker return cls((red << 16) | (green << 8) | blue) From 8c75e977494db9389e398b01abde1846820f4345 Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Fri, 15 May 2020 16:15:15 +0100 Subject: [PATCH 334/922] Renamed RouteTemplate to Route. --- hikari/net/ratelimits.py | 14 +- hikari/net/routes.py | 211 ++++++++++++++-------------- tests/hikari/net/test_ratelimits.py | 16 +-- tests/hikari/net/test_rest.py | 6 +- tests/hikari/net/test_routes.py | 6 +- 5 files changed, 127 insertions(+), 126 deletions(-) diff --git a/hikari/net/ratelimits.py b/hikari/net/ratelimits.py index 9d14467a28..c031965b1e 100644 --- a/hikari/net/ratelimits.py +++ b/hikari/net/ratelimits.py @@ -27,10 +27,10 @@ In this module, we refer to a `hikari.net.routes.CompiledRoute` as a definition of a route with specific major parameter values included (e.g. -`POST /channels/123/messages`), and a `hikari.net.routes.RouteTemplate` as a +`POST /channels/123/messages`), and a `hikari.net.routes.Route` as a definition of a route without specific parameter values included (e.g. `POST /channels/{channel_id}/messages`). We can compile a -`hikari.net.routes.CompiledRoute` from a `hikari.net.routes.RouteTemplate` +`hikari.net.routes.CompiledRoute` from a `hikari.net.routes.Route` by providing the corresponding parameters as kwargs, as you may already know. In this module, a "bucket" is an internal data structure that tracks and @@ -73,7 +73,7 @@ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Each time you `BaseRateLimiter.acquire()` a request timeslice for a given -`hikari.net.routes.RouteTemplate`, several things happen. The first is that we +`hikari.net.routes.Route`, several things happen. The first is that we attempt to find the existing bucket for that route, if there is one, or get an unknown bucket otherwise. This is done by creating a real bucket hash from the compiled route. The initial hash is calculated using a lookup table that maps @@ -667,8 +667,8 @@ class RESTBucketManager: "logger", ) - routes_to_hashes: typing.Final[typing.MutableMapping[routes.RouteTemplate, str]] - """Maps route templates to their `X-RateLimit-Bucket` header being used.""" + routes_to_hashes: typing.Final[typing.MutableMapping[routes.Route, str]] + """Maps routes to their `X-RateLimit-Bucket` header being used.""" real_hashes_to_buckets: typing.Final[typing.MutableMapping[str, RESTBucket]] """Maps full bucket hashes (`X-RateLimit-Bucket` appended with a hash of @@ -849,7 +849,7 @@ def acquire(self, compiled_route: routes.CompiledRoute) -> more_typing.Future[No """ # Returns a future to await on to wait to be allowed to send the request, and a # bucket hash to use to update rate limits later. - template = compiled_route.route_template + template = compiled_route.route if template in self.routes_to_hashes: bucket_hash = self.routes_to_hashes[template] @@ -896,7 +896,7 @@ def update_rate_limits( reset_at_header : datetime.datetime The `X-RateLimit-Reset` header value as a `datetime.datetime`. """ - self.routes_to_hashes[compiled_route.route_template] = bucket_header + self.routes_to_hashes[compiled_route.route] = bucket_header real_bucket_hash = compiled_route.create_real_bucket_hash(bucket_header) diff --git a/hikari/net/routes.py b/hikari/net/routes.py index 62244d06d6..1800cd99aa 100644 --- a/hikari/net/routes.py +++ b/hikari/net/routes.py @@ -20,7 +20,7 @@ from __future__ import annotations -__all__ = ["CompiledRoute", "RouteTemplate"] +__all__ = ["CompiledRoute", "Route"] import re import typing @@ -34,18 +34,18 @@ class CompiledRoute: Parameters ---------- - route_template : RouteTemplate - The route template used to make this route. + route : Route + The route used to make this compiled route. path : str The path with any major parameters interpolated in. major_params_hash : str The part of the hash identifier to use for the compiled set of major parameters. """ - __slots__ = ("route_template", "major_param_hash", "compiled_path", "hash_code") + __slots__ = ("route", "major_param_hash", "compiled_path", "hash_code") - route_template: typing.Final[RouteTemplate] - """The route template this compiled route was created from.""" + route: typing.Final[Route] + """The route this compiled route was created from.""" major_param_hash: typing.Final[str] """The major parameters in a bucket hash-compatible representation.""" @@ -56,16 +56,16 @@ class CompiledRoute: hash_code: typing.Final[int] """The hash code.""" - def __init__(self, route_template: RouteTemplate, path: str, major_params_hash: str) -> None: - self.route_template = route_template + def __init__(self, route: Route, path: str, major_params_hash: str) -> None: + self.route = route self.major_param_hash = major_params_hash self.compiled_path = path - self.hash_code = hash((self.method, self.route_template.path_template, major_params_hash)) + self.hash_code = hash((self.method, self.route.path_template, major_params_hash)) @property def method(self) -> str: """Return the HTTP method of this compiled route.""" - return self.route_template.method + return self.route.method def create_url(self, base_url: str) -> str: """Create the full URL with which you can make a request. @@ -108,7 +108,7 @@ def __hash__(self) -> int: def __eq__(self, other) -> bool: return ( isinstance(other, CompiledRoute) - and self.route_template == other.route_template + and self.route == other.route and self.major_param_hash == other.major_param_hash and self.compiled_path == other.compiled_path and self.hash_code == other.hash_code @@ -129,7 +129,7 @@ def __str__(self) -> str: return f"{self.method} {self.compiled_path}" -class RouteTemplate: +class Route: """A template used to create compiled routes for specific parameters. These compiled routes are used to identify rate limit buckets. Compiled @@ -143,6 +143,7 @@ class RouteTemplate: The template string for the path to use. """ + # noinspection RegExpRedundantEscape _MAJOR_PARAM_REGEX = re.compile(r"\{(.*?)\}") __slots__ = ("method", "path_template", "major_param", "hash_code") @@ -171,7 +172,7 @@ def __init__(self, method: str, path_template: str) -> None: self.hash_code = hash((self.method, self.path_template)) def compile(self, **kwargs: typing.Any) -> CompiledRoute: - """Generate a formatted `CompiledRoute` for this route template. + """Generate a formatted `CompiledRoute` for this route. This takes into account any URL parameters that have been passed. @@ -202,7 +203,7 @@ def __hash__(self) -> int: def __eq__(self, other) -> bool: return ( - isinstance(other, RouteTemplate) + isinstance(other, Route) and self.method == other.method and self.major_param == other.major_param and self.path_template == other.path_template @@ -217,153 +218,153 @@ def __eq__(self, other) -> bool: POST = "POST" # Channels -GET_CHANNEL = RouteTemplate(GET, "/channels/{channel_id}") -PATCH_CHANNEL = RouteTemplate(PATCH, "/channels/{channel_id}") -DELETE_CHANNEL = RouteTemplate(DELETE, "/channels/{channel_id}") +GET_CHANNEL = Route(GET, "/channels/{channel_id}") +PATCH_CHANNEL = Route(PATCH, "/channels/{channel_id}") +DELETE_CHANNEL = Route(DELETE, "/channels/{channel_id}") -GET_CHANNEL_INVITES = RouteTemplate(GET, "/channels/{channel_id}/invites") -POST_CHANNEL_INVITES = RouteTemplate(POST, "/channels/{channel_id}/invites") +GET_CHANNEL_INVITES = Route(GET, "/channels/{channel_id}/invites") +POST_CHANNEL_INVITES = Route(POST, "/channels/{channel_id}/invites") -GET_CHANNEL_MESSAGE = RouteTemplate(GET, "/channels/{channel_id}/messages/{message_id}") -PATCH_CHANNEL_MESSAGE = RouteTemplate(PATCH, "/channels/{channel_id}/messages/{message_id}") -DELETE_CHANNEL_MESSAGE = RouteTemplate(DELETE, "/channels/{channel_id}/messages/{message_id}") +GET_CHANNEL_MESSAGE = Route(GET, "/channels/{channel_id}/messages/{message_id}") +PATCH_CHANNEL_MESSAGE = Route(PATCH, "/channels/{channel_id}/messages/{message_id}") +DELETE_CHANNEL_MESSAGE = Route(DELETE, "/channels/{channel_id}/messages/{message_id}") -GET_CHANNEL_MESSAGES = RouteTemplate(GET, "/channels/{channel_id}/messages") -POST_CHANNEL_MESSAGES = RouteTemplate(POST, "/channels/{channel_id}/messages") +GET_CHANNEL_MESSAGES = Route(GET, "/channels/{channel_id}/messages") +POST_CHANNEL_MESSAGES = Route(POST, "/channels/{channel_id}/messages") -POST_DELETE_CHANNEL_MESSAGES_BULK = RouteTemplate(POST, "/channels/{channel_id}/messages/bulk-delete") +POST_DELETE_CHANNEL_MESSAGES_BULK = Route(POST, "/channels/{channel_id}/messages/bulk-delete") -PATCH_CHANNEL_PERMISSIONS = RouteTemplate(PATCH, "/channels/{channel_id}/permissions/{overwrite_id}") -DELETE_CHANNEL_PERMISSIONS = RouteTemplate(DELETE, "/channels/{channel_id}/permissions/{overwrite_id}") +PATCH_CHANNEL_PERMISSIONS = Route(PATCH, "/channels/{channel_id}/permissions/{overwrite_id}") +DELETE_CHANNEL_PERMISSIONS = Route(DELETE, "/channels/{channel_id}/permissions/{overwrite_id}") -DELETE_CHANNEL_PIN = RouteTemplate(DELETE, "/channels/{channel_id}/pins/{message_id}") +DELETE_CHANNEL_PIN = Route(DELETE, "/channels/{channel_id}/pins/{message_id}") -GET_CHANNEL_PINS = RouteTemplate(GET, "/channels/{channel_id}/pins") -PUT_CHANNEL_PINS = RouteTemplate(PUT, "/channels/{channel_id}/pins/{message_id}") +GET_CHANNEL_PINS = Route(GET, "/channels/{channel_id}/pins") +PUT_CHANNEL_PINS = Route(PUT, "/channels/{channel_id}/pins/{message_id}") -POST_CHANNEL_TYPING = RouteTemplate(POST, "/channels/{channel_id}/typing") +POST_CHANNEL_TYPING = Route(POST, "/channels/{channel_id}/typing") -POST_CHANNEL_WEBHOOKS = RouteTemplate(POST, "/channels/{channel_id}/webhooks") -GET_CHANNEL_WEBHOOKS = RouteTemplate(GET, "/channels/{channel_id}/webhooks") +POST_CHANNEL_WEBHOOKS = Route(POST, "/channels/{channel_id}/webhooks") +GET_CHANNEL_WEBHOOKS = Route(GET, "/channels/{channel_id}/webhooks") # Reactions -DELETE_ALL_REACTIONS = RouteTemplate(DELETE, "/channels/{channel_id}/messages/{message_id}/reactions") +DELETE_ALL_REACTIONS = Route(DELETE, "/channels/{channel_id}/messages/{message_id}/reactions") -DELETE_REACTION_EMOJI = RouteTemplate(DELETE, "/channels/{channel_id}/messages/{message_id}/reactions/{emoji}") -DELETE_REACTION_USER = RouteTemplate(DELETE, "/channels/{channel_id}/messages/{message_id}/reactions/{emoji}/{used_id}") -GET_REACTIONS = RouteTemplate(GET, "/channels/{channel_id}/messages/{message_id}/reactions/{emoji}") +DELETE_REACTION_EMOJI = Route(DELETE, "/channels/{channel_id}/messages/{message_id}/reactions/{emoji}") +DELETE_REACTION_USER = Route(DELETE, "/channels/{channel_id}/messages/{message_id}/reactions/{emoji}/{used_id}") +GET_REACTIONS = Route(GET, "/channels/{channel_id}/messages/{message_id}/reactions/{emoji}") # Guilds -GET_GUILD = RouteTemplate(GET, "/guilds/{guild_id}") -PATCH_GUILD = RouteTemplate(PATCH, "/guilds/{guild_id}") -DELETE_GUILD = RouteTemplate(DELETE, "/guilds/{guild_id}") +GET_GUILD = Route(GET, "/guilds/{guild_id}") +PATCH_GUILD = Route(PATCH, "/guilds/{guild_id}") +DELETE_GUILD = Route(DELETE, "/guilds/{guild_id}") -POST_GUILDS = RouteTemplate(POST, "/guilds") +POST_GUILDS = Route(POST, "/guilds") -GET_GUILD_AUDIT_LOGS = RouteTemplate(GET, "/guilds/{guild_id}/audit-logs") +GET_GUILD_AUDIT_LOGS = Route(GET, "/guilds/{guild_id}/audit-logs") -GET_GUILD_BAN = RouteTemplate(GET, "/guilds/{guild_id}/bans/{user_id}") -PUT_GUILD_BAN = RouteTemplate(PUT, "/guilds/{guild_id}/bans/{user_id}") -DELETE_GUILD_BAN = RouteTemplate(DELETE, "/guilds/{guild_id}/bans/{user_id}") +GET_GUILD_BAN = Route(GET, "/guilds/{guild_id}/bans/{user_id}") +PUT_GUILD_BAN = Route(PUT, "/guilds/{guild_id}/bans/{user_id}") +DELETE_GUILD_BAN = Route(DELETE, "/guilds/{guild_id}/bans/{user_id}") -GET_GUILD_BANS = RouteTemplate(GET, "/guilds/{guild_id}/bans") +GET_GUILD_BANS = Route(GET, "/guilds/{guild_id}/bans") -GET_GUILD_CHANNELS = RouteTemplate(GET, "/guilds/{guild_id}/channels") -POST_GUILD_CHANNELS = RouteTemplate(POST, "/guilds/{guild_id}/channels") -PATCH_GUILD_CHANNELS = RouteTemplate(PATCH, "/guilds/{guild_id}/channels") +GET_GUILD_CHANNELS = Route(GET, "/guilds/{guild_id}/channels") +POST_GUILD_CHANNELS = Route(POST, "/guilds/{guild_id}/channels") +PATCH_GUILD_CHANNELS = Route(PATCH, "/guilds/{guild_id}/channels") -GET_GUILD_EMBED = RouteTemplate(GET, "/guilds/{guild_id}/embed") -PATCH_GUILD_EMBED = RouteTemplate(PATCH, "/guilds/{guild_id}/embed") +GET_GUILD_EMBED = Route(GET, "/guilds/{guild_id}/embed") +PATCH_GUILD_EMBED = Route(PATCH, "/guilds/{guild_id}/embed") -GET_GUILD_EMOJI = RouteTemplate(GET, "/guilds/{guild_id}/emojis/{emoji_id}") -PATCH_GUILD_EMOJI = RouteTemplate(PATCH, "/guilds/{guild_id}/emojis/{emoji_id}") -DELETE_GUILD_EMOJI = RouteTemplate(DELETE, "/guilds/{guild_id}/emojis/{emoji_id}") +GET_GUILD_EMOJI = Route(GET, "/guilds/{guild_id}/emojis/{emoji_id}") +PATCH_GUILD_EMOJI = Route(PATCH, "/guilds/{guild_id}/emojis/{emoji_id}") +DELETE_GUILD_EMOJI = Route(DELETE, "/guilds/{guild_id}/emojis/{emoji_id}") -GET_GUILD_EMOJIS = RouteTemplate(GET, "/guilds/{guild_id}/emojis") -POST_GUILD_EMOJIS = RouteTemplate(POST, "/guilds/{guild_id}/emojis") +GET_GUILD_EMOJIS = Route(GET, "/guilds/{guild_id}/emojis") +POST_GUILD_EMOJIS = Route(POST, "/guilds/{guild_id}/emojis") -PATCH_GUILD_INTEGRATION = RouteTemplate(PATCH, "/guilds/{guild_id}/integrations/{integration_id}") -DELETE_GUILD_INTEGRATION = RouteTemplate(DELETE, "/guilds/{guild_id}/integrations/{integration_id}") +PATCH_GUILD_INTEGRATION = Route(PATCH, "/guilds/{guild_id}/integrations/{integration_id}") +DELETE_GUILD_INTEGRATION = Route(DELETE, "/guilds/{guild_id}/integrations/{integration_id}") -GET_GUILD_INTEGRATIONS = RouteTemplate(GET, "/guilds/{guild_id}/integrations") +GET_GUILD_INTEGRATIONS = Route(GET, "/guilds/{guild_id}/integrations") -POST_GUILD_INTEGRATION_SYNC = RouteTemplate(POST, "/guilds/{guild_id}/integrations/{integration_id}") +POST_GUILD_INTEGRATION_SYNC = Route(POST, "/guilds/{guild_id}/integrations/{integration_id}") -GET_GUILD_INVITES = RouteTemplate(GET, "/guilds/{guild_id}/invites") +GET_GUILD_INVITES = Route(GET, "/guilds/{guild_id}/invites") -GET_GUILD_MEMBERS = RouteTemplate(GET, "/guilds/{guild_id}/members") +GET_GUILD_MEMBERS = Route(GET, "/guilds/{guild_id}/members") -GET_GUILD_MEMBER = RouteTemplate(GET, "/guilds/{guild_id}/members/{user_id}") -PATCH_GUILD_MEMBER = RouteTemplate(PATCH, "/guilds/{guild_id}/members/{user_id}") -DELETE_GUILD_MEMBER = RouteTemplate(DELETE, "/guilds/{guild_id}/members/{user_id}") +GET_GUILD_MEMBER = Route(GET, "/guilds/{guild_id}/members/{user_id}") +PATCH_GUILD_MEMBER = Route(PATCH, "/guilds/{guild_id}/members/{user_id}") +DELETE_GUILD_MEMBER = Route(DELETE, "/guilds/{guild_id}/members/{user_id}") -PUT_GUILD_MEMBER_ROLE = RouteTemplate(PUT, "/guilds/{guild_id}/members/{user_id}/roles/{role_id}") -DELETE_GUILD_MEMBER_ROLE = RouteTemplate(DELETE, "/guilds/{guild_id}/members/{user_id}/roles/{role_id}") +PUT_GUILD_MEMBER_ROLE = Route(PUT, "/guilds/{guild_id}/members/{user_id}/roles/{role_id}") +DELETE_GUILD_MEMBER_ROLE = Route(DELETE, "/guilds/{guild_id}/members/{user_id}/roles/{role_id}") -GET_GUILD_PREVIEW = RouteTemplate(GET, "/guilds/{guild_id}/preview") +GET_GUILD_PREVIEW = Route(GET, "/guilds/{guild_id}/preview") -GET_GUILD_PRUNE = RouteTemplate(GET, "/guilds/{guild_id}/prune") -POST_GUILD_PRUNE = RouteTemplate(POST, "/guilds/{guild_id}/prune") +GET_GUILD_PRUNE = Route(GET, "/guilds/{guild_id}/prune") +POST_GUILD_PRUNE = Route(POST, "/guilds/{guild_id}/prune") -PATCH_GUILD_ROLE = RouteTemplate(PATCH, "/guilds/{guild_id}/roles/{role_id}") -DELETE_GUILD_ROLE = RouteTemplate(DELETE, "/guilds/{guild_id}/roles/{role_id}") +PATCH_GUILD_ROLE = Route(PATCH, "/guilds/{guild_id}/roles/{role_id}") +DELETE_GUILD_ROLE = Route(DELETE, "/guilds/{guild_id}/roles/{role_id}") -GET_GUILD_ROLES = RouteTemplate(GET, "/guilds/{guild_id}/roles") -POST_GUILD_ROLES = RouteTemplate(POST, "/guilds/{guild_id}/roles") -PATCH_GUILD_ROLES = RouteTemplate(PATCH, "/guilds/{guild_id}/roles") +GET_GUILD_ROLES = Route(GET, "/guilds/{guild_id}/roles") +POST_GUILD_ROLES = Route(POST, "/guilds/{guild_id}/roles") +PATCH_GUILD_ROLES = Route(PATCH, "/guilds/{guild_id}/roles") -GET_GUILD_VANITY_URL = RouteTemplate(GET, "/guilds/{guild_id}/vanity-url") +GET_GUILD_VANITY_URL = Route(GET, "/guilds/{guild_id}/vanity-url") -GET_GUILD_VOICE_REGIONS = RouteTemplate(GET, "/guilds/{guild_id}/regions") +GET_GUILD_VOICE_REGIONS = Route(GET, "/guilds/{guild_id}/regions") -GET_GUILD_WEBHOOKS = RouteTemplate(GET, "/guilds/{guild_id}/webhooks") +GET_GUILD_WEBHOOKS = Route(GET, "/guilds/{guild_id}/webhooks") -GET_GUILD_WIDGET_IMAGE = RouteTemplate(GET, "/guilds/{guild_id}/widget.png") +GET_GUILD_WIDGET_IMAGE = Route(GET, "/guilds/{guild_id}/widget.png") # Invites -GET_INVITE = RouteTemplate(GET, "/invites/{invite_code}") -DELETE_INVITE = RouteTemplate(DELETE, "/invites/{invite_code}") +GET_INVITE = Route(GET, "/invites/{invite_code}") +DELETE_INVITE = Route(DELETE, "/invites/{invite_code}") # Users -GET_USER = RouteTemplate(GET, "/users/{user_id}") +GET_USER = Route(GET, "/users/{user_id}") # @me -DELETE_MY_GUILD = RouteTemplate(DELETE, "/users/@me/guilds/{guild_id}") +DELETE_MY_GUILD = Route(DELETE, "/users/@me/guilds/{guild_id}") -GET_MY_CONNECTIONS = RouteTemplate(GET, "/users/@me/connections") # OAuth2 only +GET_MY_CONNECTIONS = Route(GET, "/users/@me/connections") # OAuth2 only -POST_MY_CHANNELS = RouteTemplate(POST, "/users/@me/channels") +POST_MY_CHANNELS = Route(POST, "/users/@me/channels") -GET_MY_GUILDS = RouteTemplate(GET, "/users/@me/guilds") +GET_MY_GUILDS = Route(GET, "/users/@me/guilds") -PATCH_MY_GUILD_NICKNAME = RouteTemplate(PATCH, "/guilds/{guild_id}/members/@me/nick") +PATCH_MY_GUILD_NICKNAME = Route(PATCH, "/guilds/{guild_id}/members/@me/nick") -GET_MY_USER = RouteTemplate(GET, "/users/@me") -PATCH_MY_USER = RouteTemplate(PATCH, "/users/@me") +GET_MY_USER = Route(GET, "/users/@me") +PATCH_MY_USER = Route(PATCH, "/users/@me") -PUT_MY_REACTION = RouteTemplate(PUT, "/channels/{channel_id}/messages/{message_id}/reactions/{emoji}/@me") -DELETE_MY_REACTION = RouteTemplate(DELETE, "/channels/{channel_id}/messages/{message_id}/reactions/{emoji}/@me") +PUT_MY_REACTION = Route(PUT, "/channels/{channel_id}/messages/{message_id}/reactions/{emoji}/@me") +DELETE_MY_REACTION = Route(DELETE, "/channels/{channel_id}/messages/{message_id}/reactions/{emoji}/@me") # Voice -GET_VOICE_REGIONS = RouteTemplate(GET, "/voice/regions") +GET_VOICE_REGIONS = Route(GET, "/voice/regions") # Webhooks -GET_WEBHOOK = RouteTemplate(GET, "/webhooks/{webhook_id}") -PATCH_WEBHOOK = RouteTemplate(PATCH, "/webhooks/{webhook_id}") -POST_WEBHOOK = RouteTemplate(POST, "/webhooks/{webhook_id}") -DELETE_WEBHOOK = RouteTemplate(DELETE, "/webhooks/{webhook_id}") +GET_WEBHOOK = Route(GET, "/webhooks/{webhook_id}") +PATCH_WEBHOOK = Route(PATCH, "/webhooks/{webhook_id}") +POST_WEBHOOK = Route(POST, "/webhooks/{webhook_id}") +DELETE_WEBHOOK = Route(DELETE, "/webhooks/{webhook_id}") -GET_WEBHOOK_WITH_TOKEN = RouteTemplate(GET, "/webhooks/{webhook_id}/{webhook_token}") -PATCH_WEBHOOK_WITH_TOKEN = RouteTemplate(PATCH, "/webhooks/{webhook_id}/{webhook_token}") -DELETE_WEBHOOK_WITH_TOKEN = RouteTemplate(DELETE, "/webhooks/{webhook_id}/{webhook_token}") -POST_WEBHOOK_WITH_TOKEN = RouteTemplate(POST, "/webhooks/{webhook_id}/{webhook_token}") +GET_WEBHOOK_WITH_TOKEN = Route(GET, "/webhooks/{webhook_id}/{webhook_token}") +PATCH_WEBHOOK_WITH_TOKEN = Route(PATCH, "/webhooks/{webhook_id}/{webhook_token}") +DELETE_WEBHOOK_WITH_TOKEN = Route(DELETE, "/webhooks/{webhook_id}/{webhook_token}") +POST_WEBHOOK_WITH_TOKEN = Route(POST, "/webhooks/{webhook_id}/{webhook_token}") -POST_WEBHOOK_WITH_TOKEN_GITHUB = RouteTemplate(POST, "/webhooks/{webhook_id}/{webhook_token}/github") -POST_WEBHOOK_WITH_TOKEN_SLACK = RouteTemplate(POST, "/webhooks/{webhook_id}/{webhook_token}/slack") +POST_WEBHOOK_WITH_TOKEN_GITHUB = Route(POST, "/webhooks/{webhook_id}/{webhook_token}/github") +POST_WEBHOOK_WITH_TOKEN_SLACK = Route(POST, "/webhooks/{webhook_id}/{webhook_token}/slack") # OAuth2 API -GET_MY_APPLICATION = RouteTemplate(GET, "/oauth2/applications/@me") +GET_MY_APPLICATION = Route(GET, "/oauth2/applications/@me") # Gateway -GET_GATEWAY = RouteTemplate(GET, "/gateway") -GET_GATEWAY_BOT = RouteTemplate(GET, "/gateway/bot") +GET_GATEWAY = Route(GET, "/gateway") +GET_GATEWAY_BOT = Route(GET, "/gateway/bot") diff --git a/tests/hikari/net/test_ratelimits.py b/tests/hikari/net/test_ratelimits.py index 63b79578b4..2b0f2074d2 100644 --- a/tests/hikari/net/test_ratelimits.py +++ b/tests/hikari/net/test_ratelimits.py @@ -366,7 +366,7 @@ def test_is_rate_limited_when_rate_limit_not_expired_only_returns_expr(self, rem class TestRESTBucket: @pytest.fixture def template(self): - return routes.RouteTemplate("GET", "/foo/bar") + return routes.Route("GET", "/foo/bar") @pytest.fixture def compiled_route(self, template): @@ -556,7 +556,7 @@ async def test_acquire_route_when_not_in_routes_to_real_hashes_caches_route(self # noinspection PyAsyncCall mgr.acquire(route) - assert mgr.routes_to_hashes[route.route_template] == "UNKNOWN" + assert mgr.routes_to_hashes[route.route] == "UNKNOWN" @pytest.mark.asyncio async def test_acquire_route_when_route_cached_already_obtains_hash_from_route_and_bucket_from_hash(self): @@ -592,7 +592,7 @@ async def test_acquire_route_returns_acquired_future_for_new_bucket(self): route = mock.MagicMock() route.create_real_bucket_hash = mock.MagicMock(return_value="eat pant;bobs") bucket = mock.MagicMock() - mgr.routes_to_hashes[route.route_template] = "eat pant" + mgr.routes_to_hashes[route.route] = "eat pant" mgr.real_hashes_to_buckets["eat pant;bobs"] = bucket f = mgr.acquire(route) @@ -603,9 +603,9 @@ async def test_update_rate_limits_if_wrong_bucket_hash_reroutes_route(self): with ratelimits.RESTBucketManager() as mgr: route = mock.MagicMock() route.create_real_bucket_hash = mock.MagicMock(wraps=lambda intial_hash: intial_hash + ";bobs") - mgr.routes_to_hashes[route.route_template] = "123" + mgr.routes_to_hashes[route.route] = "123" mgr.update_rate_limits(route, "blep", 22, 23, datetime.datetime.now(), datetime.datetime.now()) - assert mgr.routes_to_hashes[route.route_template] == "blep" + assert mgr.routes_to_hashes[route.route] == "blep" assert isinstance(mgr.real_hashes_to_buckets["blep;bobs"], ratelimits.RESTBucket) @pytest.mark.asyncio @@ -613,11 +613,11 @@ async def test_update_rate_limits_if_right_bucket_hash_does_nothing_to_hash(self with ratelimits.RESTBucketManager() as mgr: route = mock.MagicMock() route.create_real_bucket_hash = mock.MagicMock(wraps=lambda intial_hash: intial_hash + ";bobs") - mgr.routes_to_hashes[route.route_template] = "123" + mgr.routes_to_hashes[route.route] = "123" bucket = mock.MagicMock() mgr.real_hashes_to_buckets["123;bobs"] = bucket mgr.update_rate_limits(route, "123", 22, 23, datetime.datetime.now(), datetime.datetime.now()) - assert mgr.routes_to_hashes[route.route_template] == "123" + assert mgr.routes_to_hashes[route.route] == "123" assert mgr.real_hashes_to_buckets["123;bobs"] is bucket @pytest.mark.asyncio @@ -625,7 +625,7 @@ async def test_update_rate_limits_updates_params(self): with ratelimits.RESTBucketManager() as mgr: route = mock.MagicMock() route.create_real_bucket_hash = mock.MagicMock(wraps=lambda intial_hash: intial_hash + ";bobs") - mgr.routes_to_hashes[route.route_template] = "123" + mgr.routes_to_hashes[route.route] = "123" bucket = mock.MagicMock() mgr.real_hashes_to_buckets["123;bobs"] = bucket date = datetime.datetime.now().replace(year=2004) diff --git a/tests/hikari/net/test_rest.py b/tests/hikari/net/test_rest.py index feea9d715c..65b2bd0807 100644 --- a/tests/hikari/net/test_rest.py +++ b/tests/hikari/net/test_rest.py @@ -60,8 +60,8 @@ async def json(self): @contextlib.contextmanager def mock_patch_route(real_route): compiled_route = mock.MagicMock(routes.CompiledRoute) - compile = mock.Mock(spec=routes.RouteTemplate.compile, spec_set=True, return_value=compiled_route) - route_template = mock.MagicMock(spec_set=routes.RouteTemplate, compile=compile) + compile = mock.Mock(spec=routes.Route.compile, spec_set=True, return_value=compiled_route) + route_template = mock.MagicMock(spec_set=routes.Route, compile=compile) with mock.patch.object(routes, real_route, new=route_template): yield route_template, compiled_route @@ -113,7 +113,7 @@ async def test_close_calls_ratelimiter_close(self, rest_impl, ratelimiter): @pytest.fixture def compiled_route(): - template = routes.RouteTemplate("POST", "/foo/{bar}/baz") + template = routes.Route("POST", "/foo/{bar}/baz") return routes.CompiledRoute(template, "/foo/bar/baz", "1a2a3b4b5c6d") diff --git a/tests/hikari/net/test_routes.py b/tests/hikari/net/test_routes.py index b87b23a365..603f2dd111 100644 --- a/tests/hikari/net/test_routes.py +++ b/tests/hikari/net/test_routes.py @@ -25,7 +25,7 @@ class TestCompiledRoute: @pytest.fixture def template_route(self): - return routes.RouteTemplate("get", "/somewhere/{channel_id}") + return routes.Route("get", "/somewhere/{channel_id}") @pytest.fixture def compiled_route(self, template_route): @@ -67,7 +67,7 @@ def test___eq___negative_hash(self): class TestRouteTemplate: @pytest.fixture def template_route(self): - return routes.RouteTemplate("post", "/somewhere/{channel_id}") + return routes.Route("post", "/somewhere/{channel_id}") def test__init___without_major_params_uses_default_major_params(self, template_route): assert template_route.major_param == "channel_id" @@ -79,7 +79,7 @@ def test_compile(self, template_route): assert actual_compiled_route == expected_compiled_route def test__repr__(self, template_route): - expected_repr = "RouteTemplate(path_template='/somewhere/{channel_id}', major_param='channel_id')" + expected_repr = "Route(path_template='/somewhere/{channel_id}', major_param='channel_id')" assert template_route.__repr__() == expected_repr From f11075cd36fbbb65c9b4ef467fdb5d1dd598b02c Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Fri, 15 May 2020 16:57:58 +0100 Subject: [PATCH 335/922] Added missing coverage on routes.py --- tests/hikari/net/test_routes.py | 54 ++++++++++++++++++++++++--------- 1 file changed, 39 insertions(+), 15 deletions(-) diff --git a/tests/hikari/net/test_routes.py b/tests/hikari/net/test_routes.py index 603f2dd111..b5f6535746 100644 --- a/tests/hikari/net/test_routes.py +++ b/tests/hikari/net/test_routes.py @@ -58,30 +58,54 @@ def test___eq___negative_path(self): ) def test___eq___negative_hash(self): - template = mock.MagicMock() - assert routes.CompiledRoute(template, "/foo/bar", "1a2b3d") != routes.CompiledRoute( - template, "/foo/bar", "1a2b3c" - ) + t = mock.MagicMock() + assert routes.CompiledRoute(t, "/foo/bar", "1a2b3d") != routes.CompiledRoute(t, "/foo/bar", "1a2b3c") + def test___hash___positive(self): + t = mock.MagicMock() + assert hash(routes.CompiledRoute(t, "/foo/bar", "1a2b3")) == hash(routes.CompiledRoute(t, "/foo/bar", "1a2b3")) -class TestRouteTemplate: + def test___hash___negative(self): + t = mock.MagicMock() + assert hash(routes.CompiledRoute(t, "/foo/bar", "1a2b3c")) != hash(routes.CompiledRoute(t, "/foo/bar", "1a2b3")) + + +class TestRoute: @pytest.fixture - def template_route(self): + def route(self): return routes.Route("post", "/somewhere/{channel_id}") - def test__init___without_major_params_uses_default_major_params(self, template_route): - assert template_route.major_param == "channel_id" + def test__init___without_major_params_uses_default_major_params(self, route): + assert route.major_param == "channel_id" - def test_compile(self, template_route): - expected_compiled_route = routes.CompiledRoute(template_route, "/somewhere/123", "123") + def test_compile(self, route): + expected_compiled_route = routes.CompiledRoute(route, "/somewhere/123", "123") - actual_compiled_route = template_route.compile(channel_id=123) + actual_compiled_route = route.compile(channel_id=123) assert actual_compiled_route == expected_compiled_route - def test__repr__(self, template_route): + def test__repr__(self, route): expected_repr = "Route(path_template='/somewhere/{channel_id}', major_param='channel_id')" - assert template_route.__repr__() == expected_repr + assert route.__repr__() == expected_repr + + def test__str__(self, route): + assert str(route) == "/somewhere/{channel_id}" + + def test___eq__(self): + assert routes.Route("foo", "bar") == routes.Route("foo", "bar") + + def test___ne___method(self): + assert routes.Route("foobar", "bar") != routes.Route("foo", "bar") + + def test___ne___path(self): + assert routes.Route("foo", "barbaz") != routes.Route("foo", "bar") + + def test___hash__when_equal(self): + assert hash(routes.Route("foo", "bar")) == hash(routes.Route("foo", "bar")) + + def test___hash___when_path_differs(self): + assert hash(routes.Route("foo", "barbaz")) != hash(routes.Route("foo", "bar")) - def test__str__(self, template_route): - assert str(template_route) == "/somewhere/{channel_id}" + def test___hash___when_method_differs(self): + assert hash(routes.Route("foobar", "baz")) != hash(routes.Route("foo", "baz")) From 58bde8892619b72fa353a1ef4310144e05ddb742 Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Fri, 15 May 2020 18:28:34 +0100 Subject: [PATCH 336/922] Removed dead 'serialize' method from Snowflake type. --- hikari/bases.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/hikari/bases.py b/hikari/bases.py index 83a8efb5d1..8e1ea208e8 100644 --- a/hikari/bases.py +++ b/hikari/bases.py @@ -45,7 +45,7 @@ class Entity(abc.ABC): """The client components that models may use for procedures.""" -class Snowflake(int, marshaller.Serializable): +class Snowflake(int): """A concrete representation of a unique identifier for an object on Discord. This object can be treated as a regular `int` for most purposes. @@ -81,10 +81,6 @@ def increment(self) -> int: """Increment of Discord's system when this object was made.""" return self & 0xFFF - def serialize(self) -> str: - """Generate a JSON-friendly representation of this object.""" - return str(self) - @classmethod def from_datetime(cls, date: datetime.datetime) -> Snowflake: """Get a snowflake object from a datetime object.""" From 847ca08cf98da9280765be6e5599f5539ae9434e Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Fri, 15 May 2020 19:18:44 +0100 Subject: [PATCH 337/922] Added missing test coverage for hikari.clients.shards --- hikari/clients/shards.py | 15 ++--------- tests/hikari/clients/test_shards.py | 39 ++++++++++++++++++++++++++++- 2 files changed, 40 insertions(+), 14 deletions(-) diff --git a/hikari/clients/shards.py b/hikari/clients/shards.py index 5804b5f798..1839c8a203 100644 --- a/hikari/clients/shards.py +++ b/hikari/clients/shards.py @@ -382,6 +382,8 @@ async def close(self) -> None: if self._task is not None: await self._task + self._shard_state = shard_states.ShardState.STOPPED + async def _keep_alive(self): # pylint: disable=too-many-branches back_off = ratelimits.ExponentialBackOff(base=1.85, maximum=600, initial_increment=2) last_start = time.perf_counter() @@ -553,16 +555,3 @@ def _create_presence_pl( "game": activity.serialize() if activity is not None else None, "afk": is_afk, } - - def __str__(self) -> str: - return f"Shard {self.shard_id} in pool of {self.shard_count} shards" - - def __repr__(self) -> str: - return ( - "ShardClient(" - + ", ".join( - f"{k}={getattr(self, k)!r}" - for k in ("shard_id", "shard_count", "connection_state", "heartbeat_interval", "heartbeat_latency") - ) - + ")" - ) diff --git a/tests/hikari/clients/test_shards.py b/tests/hikari/clients/test_shards.py index e20f744c81..5b426e85a1 100644 --- a/tests/hikari/clients/test_shards.py +++ b/tests/hikari/clients/test_shards.py @@ -20,6 +20,7 @@ import datetime import aiohttp +import async_timeout import mock import pytest @@ -29,9 +30,10 @@ from hikari.clients import components from hikari.clients import configs from hikari.clients import shards as high_level_shards +from hikari.events import consumers +from hikari.internal import more_asyncio from hikari.net import codes from hikari.net import shards as low_level_shards -from hikari.events import consumers from tests.hikari import _helpers @@ -109,6 +111,16 @@ def test_connection_is_set(self, shard_client_obj): class TestShardClientImplDelegateProperties: + def test_shard_id(self, shard_client_obj): + marker = object() + shard_client_obj._connection.shard_id = marker + assert shard_client_obj.shard_id is marker + + def test_shard_count(self, shard_client_obj): + marker = object() + shard_client_obj._connection.shard_count = marker + assert shard_client_obj.shard_count is marker + def test_status(self, shard_client_obj): marker = object() shard_client_obj._status = marker @@ -302,6 +314,27 @@ async def test_close_when_already_stopping(self, shard_client_obj): shard_client_obj._connection.close.assert_not_called() + @pytest.mark.asyncio + async def test_close_when_not_running_is_not_an_error(self, shard_client_obj): + shard_client_obj._shard_state = hikari.clients.shard_states.ShardState.NOT_RUNNING + shard_client_obj._task = None + + await shard_client_obj.close() + + shard_client_obj._connection.close.assert_called_once() + + @_helpers.timeout_after(5) + @pytest.mark.asyncio + async def test__keep_alive_repeats_silently_if_task_returns(self, shard_client_obj): + shard_client_obj._spin_up = mock.AsyncMock(return_value=more_asyncio.completed_future()) + + try: + async with async_timeout.timeout(1): + await shard_client_obj._keep_alive() + assert False + except asyncio.TimeoutError: + assert shard_client_obj._spin_up.await_count > 0 + @_helpers.assert_raises(type_=RuntimeError) @pytest.mark.parametrize( "error", @@ -315,6 +348,7 @@ async def test_close_when_already_stopping(self, shard_client_obj): ], ) @pytest.mark.asyncio + @_helpers.timeout_after(5) async def test__keep_alive_handles_errors(self, error, shard_client_obj): should_return = False @@ -332,6 +366,7 @@ def side_effect(*args): await shard_client_obj._keep_alive() @pytest.mark.asyncio + @_helpers.timeout_after(5) async def test__keep_alive_shuts_down_when_GatewayClientClosedError(self, shard_client_obj): shard_client_obj._spin_up = mock.AsyncMock( return_value=_helpers.AwaitableMock(return_value=errors.GatewayClientClosedError) @@ -354,6 +389,7 @@ async def test__keep_alive_shuts_down_when_GatewayClientClosedError(self, shard_ ], ) @pytest.mark.asyncio + @_helpers.timeout_after(5) async def test__keep_alive_shuts_down_when_GatewayServerClosedConnectionError(self, code, shard_client_obj): shard_client_obj._spin_up = mock.AsyncMock( return_value=_helpers.AwaitableMock(return_value=errors.GatewayServerClosedConnectionError(code)) @@ -364,6 +400,7 @@ async def test__keep_alive_shuts_down_when_GatewayServerClosedConnectionError(se @_helpers.assert_raises(type_=RuntimeError) @pytest.mark.asyncio + @_helpers.timeout_after(5) async def test__keep_alive_ignores_when_GatewayServerClosedConnectionError_with_other_code(self, shard_client_obj): should_return = False From a4edc3a3860dc313cc3a37db4cc677d20ca419c4 Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Fri, 15 May 2020 19:25:25 +0100 Subject: [PATCH 338/922] Removed dead code from hikari.net.user_agents --- hikari/net/user_agents.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/hikari/net/user_agents.py b/hikari/net/user_agents.py index 547bca32de..57d0131833 100644 --- a/hikari/net/user_agents.py +++ b/hikari/net/user_agents.py @@ -84,12 +84,6 @@ def __init__(self): self.system_type = platform() self.user_agent = f"DiscordBot ({__url__}; {__version__}; {__author__}) {python_version()} {self.system_type}" - def __attr__(_): - raise TypeError("cannot change attributes once set") - - self.__delattr__ = __attr__ - self.__setattr__ = __attr__ - @staticmethod def _join_strip(*args): return " ".join((arg.strip() for arg in args if arg.strip())) From 0bb6299f613bb3f49dc9c3c727c0d8e9be79fcee Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Fri, 15 May 2020 19:28:07 +0100 Subject: [PATCH 339/922] Added intents tests for 'is_privileged' test. --- tests/hikari/test_intents.py | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 tests/hikari/test_intents.py diff --git a/tests/hikari/test_intents.py b/tests/hikari/test_intents.py new file mode 100644 index 0000000000..a102e2eb81 --- /dev/null +++ b/tests/hikari/test_intents.py @@ -0,0 +1,27 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# Copyright © Nekoka.tt 2019-2020 +# +# This file is part of Hikari. +# +# Hikari is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Hikari is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Hikari. If not, see . +from hikari import intents + + +class TestIntent: + def test_is_privileged(self): + assert intents.Intent.GUILD_MEMBERS.is_privileged + + def test_not_is_privileged(self): + assert not intents.Intent.DIRECT_MESSAGE_TYPING.is_privileged From bb687640c6d11a1d41e1e354ec2271774fa71b1e Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Fri, 15 May 2020 19:29:33 +0100 Subject: [PATCH 340/922] Fixed incorrect enums test. --- tests/hikari/internal/test_more_enums.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/hikari/internal/test_more_enums.py b/tests/hikari/internal/test_more_enums.py index 680372134c..a662277162 100644 --- a/tests/hikari/internal/test_more_enums.py +++ b/tests/hikari/internal/test_more_enums.py @@ -22,7 +22,7 @@ class TestEnumMixin: def test_str(self): - class TestType(more_enums.IntFlag): + class TestType(more_enums.Enum): a = 1 b = 2 c = 4 From 6cc44736c2289c27c75e84a692a4f328bd6bf1a1 Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Fri, 15 May 2020 19:34:12 +0100 Subject: [PATCH 341/922] Removed dead code from 'shard_states' --- hikari/clients/shard_states.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/hikari/clients/shard_states.py b/hikari/clients/shard_states.py index eb3f0c2d17..fcd6787085 100644 --- a/hikari/clients/shard_states.py +++ b/hikari/clients/shard_states.py @@ -52,6 +52,3 @@ class ShardState(int, more_enums.Enum): STOPPED = more_enums.generated_value() """The shard has shut down and is no longer connected.""" - - def __str__(self) -> str: - return self.name From 893f08c8e39373e1fc24f265e8cd37b3e2114eb6 Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Fri, 15 May 2020 19:38:11 +0100 Subject: [PATCH 342/922] Trimmed imports. --- hikari/__main__.py | 1 + hikari/clients/bot_base.py | 4 +++- hikari/clients/rest/channel.py | 2 +- hikari/clients/rest/react.py | 2 +- hikari/events/event_managers.py | 3 ++- hikari/events/intent_aware_dispatchers.py | 2 +- hikari/state/stateless.py | 4 +++- tests/hikari/clients/test_components.py | 3 ++- tests/hikari/internal/test_conversions.py | 3 --- tests/hikari/internal/test_helpers.py | 1 - 10 files changed, 14 insertions(+), 11 deletions(-) diff --git a/hikari/__main__.py b/hikari/__main__.py index 677da594d7..2fc36d4a6e 100644 --- a/hikari/__main__.py +++ b/hikari/__main__.py @@ -23,6 +23,7 @@ import inspect import os import platform + import click from hikari import _about diff --git a/hikari/clients/bot_base.py b/hikari/clients/bot_base.py index 585390933f..57656e0997 100644 --- a/hikari/clients/bot_base.py +++ b/hikari/clients/bot_base.py @@ -38,7 +38,9 @@ from hikari.clients import configs from hikari.clients import runnable from hikari.clients import shard_states -from hikari.events import other, dispatchers, event_managers +from hikari.events import dispatchers +from hikari.events import event_managers +from hikari.events import other from hikari.internal import conversions if typing.TYPE_CHECKING: diff --git a/hikari/clients/rest/channel.py b/hikari/clients/rest/channel.py index 3f79c94b12..6df0ddacfd 100644 --- a/hikari/clients/rest/channel.py +++ b/hikari/clients/rest/channel.py @@ -26,11 +26,11 @@ import datetime import typing -from hikari import pagination from hikari import bases from hikari import channels as _channels from hikari import invites from hikari import messages as _messages +from hikari import pagination from hikari import webhooks from hikari.clients.rest import base from hikari.internal import helpers diff --git a/hikari/clients/rest/react.py b/hikari/clients/rest/react.py index 10a8232bf5..14aa8bc5b7 100644 --- a/hikari/clients/rest/react.py +++ b/hikari/clients/rest/react.py @@ -27,9 +27,9 @@ import typing from hikari import bases +from hikari import messages as _messages from hikari import pagination from hikari import users -from hikari import messages as _messages from hikari.clients.rest import base if typing.TYPE_CHECKING: diff --git a/hikari/events/event_managers.py b/hikari/events/event_managers.py index 74c1cf4b78..1851736684 100644 --- a/hikari/events/event_managers.py +++ b/hikari/events/event_managers.py @@ -26,7 +26,8 @@ import logging import typing -from hikari.events import dispatchers, consumers +from hikari.events import consumers +from hikari.events import dispatchers if typing.TYPE_CHECKING: from hikari.clients import components as _components diff --git a/hikari/events/intent_aware_dispatchers.py b/hikari/events/intent_aware_dispatchers.py index b5ec0bb3ba..957dd7517c 100644 --- a/hikari/events/intent_aware_dispatchers.py +++ b/hikari/events/intent_aware_dispatchers.py @@ -29,10 +29,10 @@ from hikari import errors from hikari import intents from hikari.events import base +from hikari.events import dispatchers from hikari.events import other from hikari.internal import more_asyncio from hikari.internal import more_collections -from hikari.events import dispatchers if typing.TYPE_CHECKING: from hikari.internal import more_typing diff --git a/hikari/state/stateless.py b/hikari/state/stateless.py index 1e2f227d18..33e48c5c4d 100644 --- a/hikari/state/stateless.py +++ b/hikari/state/stateless.py @@ -22,7 +22,9 @@ __all__ = ["StatelessEventManagerImpl"] -from hikari.events import channel, dispatchers, event_managers +from hikari.events import channel +from hikari.events import dispatchers +from hikari.events import event_managers from hikari.events import guild from hikari.events import message from hikari.events import other diff --git a/tests/hikari/clients/test_components.py b/tests/hikari/clients/test_components.py index 8d35b16781..ee75b33864 100644 --- a/tests/hikari/clients/test_components.py +++ b/tests/hikari/clients/test_components.py @@ -22,7 +22,8 @@ from hikari.clients import configs from hikari.clients import rest from hikari.clients import shards -from hikari.events import dispatchers, event_managers +from hikari.events import dispatchers +from hikari.events import event_managers class TestComponents: diff --git a/tests/hikari/internal/test_conversions.py b/tests/hikari/internal/test_conversions.py index f2a69fb0a7..76f0442a57 100644 --- a/tests/hikari/internal/test_conversions.py +++ b/tests/hikari/internal/test_conversions.py @@ -16,15 +16,12 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . -import concurrent.futures import datetime -import inspect import typing import pytest from hikari.internal import conversions -from tests.hikari import _helpers @pytest.mark.parametrize( diff --git a/tests/hikari/internal/test_helpers.py b/tests/hikari/internal/test_helpers.py index c71effc25c..b977948222 100644 --- a/tests/hikari/internal/test_helpers.py +++ b/tests/hikari/internal/test_helpers.py @@ -16,7 +16,6 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . -import mock import pytest from hikari import guilds From 49ef212f56e73b1e5a5fed276e569ae620371bd5 Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Fri, 15 May 2020 19:44:53 +0100 Subject: [PATCH 343/922] Added missing test case for BaseRESTComponent.close --- tests/hikari/clients/test_rest/test_base.py | 39 +++++++++++++-------- 1 file changed, 25 insertions(+), 14 deletions(-) diff --git a/tests/hikari/clients/test_rest/test_base.py b/tests/hikari/clients/test_rest/test_base.py index 0bc64341ab..08638a2611 100644 --- a/tests/hikari/clients/test_rest/test_base.py +++ b/tests/hikari/clients/test_rest/test_base.py @@ -25,23 +25,25 @@ from hikari.net import rest -class TestBaseRESTComponent: - @pytest.fixture() - def low_level_rest_impl(self) -> rest.REST: - return mock.MagicMock( - rest.REST, - global_ratelimiter=mock.create_autospec(ratelimits.ManualRateLimiter, spec_set=True), - bucket_ratelimiters=mock.create_autospec(ratelimits.RESTBucketManager, spec_set=True), - ) +@pytest.fixture() +def low_level_rest_impl() -> rest.REST: + return mock.MagicMock( + spec=rest.REST, + global_ratelimiter=mock.create_autospec(ratelimits.ManualRateLimiter, spec_set=True), + bucket_ratelimiters=mock.create_autospec(ratelimits.RESTBucketManager, spec_set=True), + ) - @pytest.fixture() - def rest_clients_impl(self, low_level_rest_impl) -> base.BaseRESTComponent: - class RestClientImpl(base.BaseRESTComponent): - def __init__(self): - super().__init__(mock.MagicMock(components.Components), low_level_rest_impl) - return RestClientImpl() +@pytest.fixture() +def rest_clients_impl(low_level_rest_impl) -> base.BaseRESTComponent: + class RestClientImpl(base.BaseRESTComponent): + def __init__(self): + super().__init__(mock.MagicMock(components.Components), low_level_rest_impl) + return RestClientImpl() + + +class TestBaseRESTComponentContextManager: @pytest.mark.asyncio async def test___aenter___and___aexit__(self, rest_clients_impl): rest_clients_impl.close = mock.AsyncMock() @@ -49,6 +51,15 @@ async def test___aenter___and___aexit__(self, rest_clients_impl): assert client is rest_clients_impl rest_clients_impl.close.assert_called_once_with() + +class TestBaseRESTComponentClose: + @pytest.mark.asyncio + async def test_close_delegates_to_low_level_rest_impl(self, rest_clients_impl): + await rest_clients_impl.close() + rest_clients_impl._session.close.assert_awaited_once_with() + + +class TestBaseRESTComponentQueueSizeProperties: def test_global_ratelimit_queue_size(self, rest_clients_impl, low_level_rest_impl): low_level_rest_impl.global_ratelimiter.queue = [object() for _ in range(107)] assert rest_clients_impl.global_ratelimit_queue_size == 107 From 48319f6b4871d34a8cac4bf474686a8a5c97c1ac Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Fri, 15 May 2020 19:53:49 +0100 Subject: [PATCH 344/922] Fixed missing unset test cases. --- tests/hikari/clients/test_rest/test___init__.py | 15 ++++++++++++--- tests/hikari/test_unset.py | 14 ++++++++++++++ 2 files changed, 26 insertions(+), 3 deletions(-) diff --git a/tests/hikari/clients/test_rest/test___init__.py b/tests/hikari/clients/test_rest/test___init__.py index f459aa00e8..0974dca040 100644 --- a/tests/hikari/clients/test_rest/test___init__.py +++ b/tests/hikari/clients/test_rest/test___init__.py @@ -19,6 +19,7 @@ import inspect import mock +import pytest from hikari.clients import components from hikari.clients import configs @@ -27,8 +28,16 @@ class TestRESTClient: - def test_init(self): - mock_config = configs.RESTConfig(token="blah.blah.blah", trust_env=True) + @pytest.mark.parametrize( + ["token", "token_type", "expected_token"], + [ + ("foobar.baz.bork", None, None), + ("foobar.baz.bork", "Bot", "Bot foobar.baz.bork"), + ("foobar.baz.bork", "Bearer", "Bearer foobar.baz.bork"), + ], + ) + def test_init(self, token, token_type, expected_token): + mock_config = configs.RESTConfig(token=token, token_type=token_type, trust_env=True) mock_components = mock.MagicMock(components.Components, config=mock_config) mock_low_level_rest_clients = mock.MagicMock(low_level_rest.REST) with mock.patch.object(low_level_rest, "REST", return_value=mock_low_level_rest_clients) as patched_init: @@ -43,7 +52,7 @@ def test_init(self): ssl_context=mock_config.ssl_context, verify_ssl=mock_config.verify_ssl, timeout=mock_config.request_timeout, - token=f"{mock_config.token_type} {mock_config.token}", + token=expected_token, trust_env=True, version=mock_config.rest_version, ) diff --git a/tests/hikari/test_unset.py b/tests/hikari/test_unset.py index 2beb505e29..4f4500baf7 100644 --- a/tests/hikari/test_unset.py +++ b/tests/hikari/test_unset.py @@ -16,7 +16,10 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . +import pytest + from hikari import unset +from tests.hikari import _helpers class TestUnset: @@ -32,3 +35,14 @@ def test_bool(self): def test_singleton_behaviour(self): assert unset.Unset() is unset.Unset() assert unset.UNSET is unset.Unset() + + @_helpers.assert_raises(type_=TypeError) + def test_cannot_subclass(self): + class _(unset.Unset): + pass + + +class TestIsUnset: + @pytest.mark.parametrize(["obj", "is_unset"], [(unset.UNSET, True), (object(), False),]) + def test_is_unset(self, obj, is_unset): + assert unset.is_unset(obj) is is_unset From 0b8684a5af5ecd489c6a47ffcddcd4466c25c5e4 Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Fri, 15 May 2020 19:56:12 +0100 Subject: [PATCH 345/922] De-implemented empty generator expression constant. --- hikari/internal/more_collections.py | 2 -- hikari/pagination.py | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/hikari/internal/more_collections.py b/hikari/internal/more_collections.py index a71abbb3d5..571936c8c2 100644 --- a/hikari/internal/more_collections.py +++ b/hikari/internal/more_collections.py @@ -25,7 +25,6 @@ "EMPTY_SET", "EMPTY_COLLECTION", "EMPTY_DICT", - "EMPTY_GENERATOR_EXPRESSION", "WeakKeyDictionary", ] @@ -42,7 +41,6 @@ EMPTY_SET: typing.Final[typing.AbstractSet[_T]] = frozenset() EMPTY_COLLECTION: typing.Final[typing.Collection[_T]] = tuple() EMPTY_DICT: typing.Final[typing.Mapping[_K, _V]] = types.MappingProxyType({}) -EMPTY_GENERATOR_EXPRESSION: typing.Final[typing.Iterator[_T]] = (_ for _ in EMPTY_COLLECTION) class WeakKeyDictionary(typing.Generic[_K, _V], weakref.WeakKeyDictionary, typing.MutableMapping[_K, _V]): diff --git a/hikari/pagination.py b/hikari/pagination.py index 2c568a9482..a3c3fe0846 100644 --- a/hikari/pagination.py +++ b/hikari/pagination.py @@ -241,7 +241,7 @@ class BufferedPaginatedResults(typing.Generic[_T], PaginatedResults[_T]): def __init__(self) -> None: # Start with an empty generator to force the paginator to get the next item. - self._buffer = more_collections.EMPTY_GENERATOR_EXPRESSION + self._buffer = (_ for _ in more_collections.EMPTY_COLLECTION) @abc.abstractmethod async def _next_chunk(self) -> typing.Optional[typing.Generator[typing.Any, None, _T]]: From 5b9bd36e211712203b8975ab51cdcced15e2c6f5 Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Fri, 15 May 2020 19:59:37 +0100 Subject: [PATCH 346/922] Removed duplicate pep563 test pack for marshaller as it was unnecesarry. --- .../hikari/internal/test_marshaller_pep563.py | 346 ------------------ 1 file changed, 346 deletions(-) delete mode 100644 tests/hikari/internal/test_marshaller_pep563.py diff --git a/tests/hikari/internal/test_marshaller_pep563.py b/tests/hikari/internal/test_marshaller_pep563.py deleted file mode 100644 index ee72334f4f..0000000000 --- a/tests/hikari/internal/test_marshaller_pep563.py +++ /dev/null @@ -1,346 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# Copyright © Nekoka.tt 2019-2020 -# -# This file is part of Hikari. -# -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . -""" -Same as the marshaller tests, but with PEP 563 POSTPONED TYPE ANNOTATIONS -future support enabled, to prove type hints do not interfere with this -mechanism if they are postponed and evaluated as string literals. -""" -from __future__ import annotations - -import attr -import mock -import pytest - -from hikari.internal import marshaller -from tests.hikari import _helpers - - -class TestAttribPep563: - def test_invokes_attrs(self): - deserializer = lambda _: _ - serializer = lambda _: _ - - mock_default_factory_1 = mock.MagicMock - mock_default_factory_2 = mock.MagicMock - - with mock.patch("attr.ib") as attrib: - marshaller.attrib( - deserializer=deserializer, - raw_name="foo", - if_none=mock_default_factory_1, - if_undefined=mock_default_factory_2, - inherit_kwargs=True, - serializer=serializer, - foo=12, - bar="hello, world", - ) - - attrib.assert_called_once_with( - foo=12, - bar="hello, world", - metadata={ - marshaller._RAW_NAME_ATTR: "foo", - marshaller._SERIALIZER_ATTR: serializer, - marshaller._DESERIALIZER_ATTR: deserializer, - marshaller._INHERIT_KWARGS: True, - marshaller._IF_UNDEFINED: mock_default_factory_2, - marshaller._IF_NONE: mock_default_factory_1, - marshaller._MARSHALLER_ATTRIB: True, - }, - repr=False, - ) - - -class TestAttrsPep563: - def test_invokes_attrs(self): - marshaller_mock = mock.MagicMock(marshaller.HikariEntityMarshaller) - - kwargs = {"marshaller": marshaller_mock} - - marshaller_mock.register = mock.MagicMock(wraps=lambda c: c) - - @marshaller.marshallable(**kwargs) - @attr.s() - class Foo: - bar = 69 - - assert Foo is not None - assert Foo.bar == 69 - - marshaller_mock.register.assert_called_once_with(Foo) - - -@pytest.mark.parametrize("data", [2, "d", bytes("ok", "utf-8"), [], {}, set()]) -@_helpers.assert_raises(type_=RuntimeError) -def test_default_validator_raises_runtime_error(data): - marshaller._default_validator(data) - - -def method_stub(value): - ... - - -@pytest.mark.parametrize( - "data", [lambda x: "ok", *marshaller._PASSED_THROUGH_SINGLETONS, marshaller.RAISE, dict, method_stub] -) -def test_default_validator(data): - marshaller._default_validator(data) - - -class TestMarshallerPep563: - @pytest.fixture() - def marshaller_impl(self): - return marshaller.HikariEntityMarshaller() - - def test_register_ignores_none_marshaller_attrs(self, marshaller_impl): - defaulted_foo = mock.MagicMock() - - @marshaller.marshallable(marshaller=marshaller_impl) - @attr.s() - class User: - id: int = marshaller.attrib(deserializer=int) - foo: list = attr.attrib(default=defaulted_foo) - - result = marshaller_impl.deserialize({"id": "123", "foo": "blah"}, User) - assert result.id == 123 - assert result.foo is defaulted_foo - - def test_deserialize(self, marshaller_impl): - deserialized_id = mock.MagicMock() - id_deserializer = mock.MagicMock(return_value=deserialized_id) - - @marshaller.marshallable(marshaller=marshaller_impl) - @attr.s() - class User: - id: int = marshaller.attrib(deserializer=id_deserializer) - some_list: list = marshaller.attrib(deserializer=lambda items: [str(i) for i in items]) - - result = marshaller_impl.deserialize({"id": "12345", "some_list": [True, False, "foo", 12, 3.4]}, User) - - assert isinstance(result, User) - assert result.id == deserialized_id - assert result.some_list == ["True", "False", "foo", "12", "3.4"] - - def test_deserialize_not_required_success_if_specified(self, marshaller_impl): - @marshaller.marshallable(marshaller=marshaller_impl) - @attr.s() - class User: - id: int = marshaller.attrib(if_undefined=None, deserializer=str) - - result = marshaller_impl.deserialize({"id": 12345}, User) - - assert isinstance(result, User) - assert result.id == "12345" - - @pytest.mark.parametrize("singleton", marshaller._PASSED_THROUGH_SINGLETONS) - def test_deserialize_not_required_success_if_not_specified(self, marshaller_impl, singleton): - @marshaller.marshallable(marshaller=marshaller_impl) - @attr.s() - class User: - id: int = marshaller.attrib(if_undefined=singleton, deserializer=str) - - result = marshaller_impl.deserialize({}, User) - - assert isinstance(result, User) - assert result.id is singleton - - def test_deserialize_calls_if_undefined_if_not_none_and_field_not_present(self, marshaller_impl): - mock_result = mock.MagicMock() - mock_callable = mock.MagicMock(return_value=mock_result) - - @marshaller.marshallable(marshaller=marshaller_impl) - @attr.s() - class User: - id: int = marshaller.attrib(if_undefined=mock_callable, deserializer=str) - - result = marshaller_impl.deserialize({}, User) - - assert isinstance(result, User) - assert result.id is mock_result - mock_callable.assert_called_once() - - @_helpers.assert_raises(type_=AttributeError) - def test_deserialize_fail_on_unspecified_if_required(self, marshaller_impl): - @marshaller.marshallable(marshaller=marshaller_impl) - @attr.s() - class User: - id: int = marshaller.attrib(deserializer=str) - - marshaller_impl.deserialize({}, User) - - def test_deserialize_nullable_success_if_not_null(self, marshaller_impl): - @marshaller.marshallable(marshaller=marshaller_impl) - @attr.s() - class User: - id: int = marshaller.attrib(if_none=None, deserializer=str) - - result = marshaller_impl.deserialize({"id": 12345}, User) - - assert isinstance(result, User) - assert result.id == "12345" - - @pytest.mark.parametrize("singleton", marshaller._PASSED_THROUGH_SINGLETONS) - def test_deserialize_nullable_success_if_null(self, marshaller_impl, singleton): - @marshaller.marshallable(marshaller=marshaller_impl) - @attr.s() - class User: - id: int = marshaller.attrib(if_none=singleton, deserializer=str) - - result = marshaller_impl.deserialize({"id": None}, User) - - assert isinstance(result, User) - assert result.id is singleton - - def test_deserialize_calls_if_none_if_not_none_and_data_is_none(self, marshaller_impl): - mock_result = mock.MagicMock() - mock_callable = mock.MagicMock(return_value=mock_result) - - @marshaller.marshallable(marshaller=marshaller_impl) - @attr.s() - class User: - id: int = marshaller.attrib(if_none=mock_callable, deserializer=str) - - result = marshaller_impl.deserialize({"id": None}, User) - - assert isinstance(result, User) - assert result.id is mock_result - mock_callable.assert_called_once() - - @_helpers.assert_raises(type_=AttributeError) - def test_deserialize_fail_on_None_if_not_nullable(self, marshaller_impl): - @marshaller.marshallable(marshaller=marshaller_impl) - @attr.s() - class User: - id: int = marshaller.attrib(deserializer=str) - - marshaller_impl.deserialize({"id": None}, User) - - @_helpers.assert_raises(type_=TypeError) - def test_deserialize_fail_on_Error(self, marshaller_impl): - die = mock.MagicMock(side_effect=RuntimeError) - - @marshaller.marshallable(marshaller=marshaller_impl) - @attr.s() - class User: - id: int = marshaller.attrib(deserializer=die) - - marshaller_impl.deserialize({"id": 123,}, User) - - def test_serialize(self, marshaller_impl): - @marshaller.marshallable(marshaller=marshaller_impl) - @attr.s() - class User: - id: int = marshaller.attrib(deserializer=..., serializer=str) - some_list: list = marshaller.attrib(deserializer=..., serializer=lambda i: list(map(int, i))) - - u = User(12, ["9", "18", "27", "36"]) - - assert marshaller_impl.serialize(u) == {"id": "12", "some_list": [9, 18, 27, 36]} - - def test_serialize_skips_fields_with_null_serializer(self, marshaller_impl): - @marshaller.marshallable(marshaller=marshaller_impl) - @attr.s() - class User: - id: int = marshaller.attrib(deserializer=..., serializer=str) - some_list: list = marshaller.attrib( - deserializer=..., serializer=None, - ) - - u = User(12, ["9", "18", "27", "36"]) - - assert marshaller_impl.serialize(u) == { - "id": "12", - } - - def test_deserialize_skips_fields_with_null_deserializer(self, marshaller_impl): - @marshaller.marshallable(marshaller=marshaller_impl) - @attr.s - class User: - username: str = marshaller.attrib(deserializer=str) - _component: object = marshaller.attrib(deserializer=None, default=None) - - u = marshaller_impl.deserialize({"_component": "OK", "component": "Nay", "username": "Nay"}, User) - assert u._component is None - assert u.username == "Nay" - - def test_deserialize_kwarg_gets_set_for_skip_unmarshalling_attr(self, marshaller_impl): - @marshaller.marshallable(marshaller=marshaller_impl) - @attr.s - class User: - _component: object = marshaller.attrib(deserializer=None, default=None) - - mock_component = mock.MagicMock() - u = marshaller_impl.deserialize({"_component": "OK", "component": "Nay"}, User, component=mock_component) - assert u._component is mock_component - - def test_deserialize_injects_kwargs_to_inheriting_child_entity(self, marshaller_impl): - @marshaller.marshallable(marshaller=marshaller_impl) - @attr.s() - class Child: - _components: object = marshaller.attrib(deserializer=None, serializer=None) - - @marshaller.marshallable(marshaller=marshaller_impl) - @attr.s() - class User(Child): - child: Child = marshaller.attrib( - deserializer=lambda *args, **kwargs: marshaller_impl.deserialize(*args, Child, **kwargs), - inherit_kwargs=True, - ) - - components = mock.MagicMock() - - user = marshaller_impl.deserialize({"child": {}}, User, components=components) - assert user._components is components - assert user.child._components is components - - @_helpers.assert_raises(type_=LookupError) - def test_deserialize_on_unregistered_class_raises_LookupError(self, marshaller_impl): - class Foo: - pass - - marshaller_impl.deserialize({}, Foo) - - @_helpers.assert_raises(type_=LookupError) - def test_serialize_on_unregistered_class_raises_LookupError(self, marshaller_impl): - class Foo: - pass - - f = Foo() - - marshaller_impl.serialize(f) - - def test_handling_underscores_correctly_during_deserialization(self, marshaller_impl): - @marshaller.marshallable(marshaller=marshaller_impl) - @attr.s() - class ClassWithUnderscores: - _foo = marshaller.attrib(deserializer=str) - - impl = marshaller_impl.deserialize({"_foo": 1234}, ClassWithUnderscores) - - assert impl._foo == "1234" - - def test_handling_underscores_correctly_during_serialization(self, marshaller_impl): - @marshaller.marshallable(marshaller=marshaller_impl) - @attr.s() - class ClassWithUnderscores: - _foo = marshaller.attrib(serializer=int) - - impl = ClassWithUnderscores(foo="1234") - - assert marshaller_impl.serialize(impl) == {"_foo": 1234} From 2db89142407addd73cbc221cabafc8328e304910 Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Fri, 15 May 2020 21:38:51 +0100 Subject: [PATCH 347/922] Added missing lines of coverage in marshaller for dereference_handle --- hikari/internal/marshaller.py | 11 +---------- tests/hikari/internal/test_marshaller.py | 15 +++++++++++++++ 2 files changed, 16 insertions(+), 10 deletions(-) diff --git a/hikari/internal/marshaller.py b/hikari/internal/marshaller.py index 72fda0c32f..8c0f9c9576 100644 --- a/hikari/internal/marshaller.py +++ b/hikari/internal/marshaller.py @@ -38,7 +38,6 @@ import importlib import typing -import weakref import attr @@ -104,7 +103,7 @@ def dereference_handle(handle_string: str) -> typing.Any: for attr_name in attribute_names: obj = getattr(obj, attr_name) - return weakref.proxy(obj) + return obj def attrib( @@ -161,14 +160,6 @@ class that this attribute is in will trigger a `TypeError` later. typing.Any The result of `attr.ib` internally being called with additional metadata. """ - # Sphinx decides to be really awkward and inject the wrong default values - # by default. Not helpful when it documents non-optional shit as defaulting - # to None. Hack to fix this seems to be to turn on autodoc's - # typing.TYPE_CHECKING mode, and then if that is enabled, always return - # some dummy class that has a repr that returns a literal "..." string. - if typing.TYPE_CHECKING: - return type("Literal", (), {"__repr__": lambda *_: "..."})() - metadata = kwargs.pop("metadata", {}) metadata[_RAW_NAME_ATTR] = raw_name metadata[_SERIALIZER_ATTR] = serializer diff --git a/tests/hikari/internal/test_marshaller.py b/tests/hikari/internal/test_marshaller.py index 0f0db269d6..87eec3631a 100644 --- a/tests/hikari/internal/test_marshaller.py +++ b/tests/hikari/internal/test_marshaller.py @@ -24,6 +24,21 @@ from tests.hikari import _helpers +class TestDereferenceHandle: + def test_dereference_handle_module_only(self): + from concurrent import futures + + assert marshaller.dereference_handle("concurrent.futures") is futures + + def test_dereference_handle_module_and_attribute(self): + from hikari.net import codes + + assert ( + marshaller.dereference_handle("hikari.net.codes#GatewayCloseCode.AUTHENTICATION_FAILED") + is codes.GatewayCloseCode.AUTHENTICATION_FAILED + ) + + class TestAttrib: def test_invokes_attrs(self): deserializer = lambda _: _ From 3a4a443273f7e3c1c7f4a1a0ebc96116c357eb28 Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Fri, 15 May 2020 22:01:21 +0100 Subject: [PATCH 348/922] Fixed nox descriptions [skip deploy] --- ci/clang-tidy.nox.py | 2 ++ ci/pylint.nox.py | 6 +----- ci/twemoji.nox.py | 4 +--- 3 files changed, 4 insertions(+), 8 deletions(-) diff --git a/ci/clang-tidy.nox.py b/ci/clang-tidy.nox.py index 790bf10dde..8625a9dbba 100644 --- a/ci/clang-tidy.nox.py +++ b/ci/clang-tidy.nox.py @@ -31,9 +31,11 @@ def _clang_tidy(*args): @nox.session(reuse_venv=True) def clang_tidy_check(session: nox.Session) -> None: + """Check C and C++ sources match the correct format for this library.""" _clang_tidy() @nox.session(reuse_venv=True) def clang_tidy_fix(session: nox.Session) -> None: + """Reformat C and C++ sources.""" _clang_tidy("--fix") diff --git a/ci/pylint.nox.py b/ci/pylint.nox.py index 1f5d812d2d..3095df7cbf 100644 --- a/ci/pylint.nox.py +++ b/ci/pylint.nox.py @@ -40,11 +40,7 @@ @nox.session(default=True, reuse_venv=True) def pylint(session: nox.Session) -> None: - f"""Run pylint against the code base and report any code smells or issues. - - Pass the {JUNIT_FLAG} flag to also produce a junit report. - Pass the {HTML_FLAG} flag to also produce an HTML report. - """ + """Run pylint against the code base and report any code smells or issues.""" tasks = [pylint_text] cpus_per_task = max(1, (os.cpu_count() or 1) // len(tasks)) diff --git a/ci/twemoji.nox.py b/ci/twemoji.nox.py index ad48b9564b..097b5b59d7 100644 --- a/ci/twemoji.nox.py +++ b/ci/twemoji.nox.py @@ -21,8 +21,6 @@ @nox.session() def twemoji_test(session: nox.Session): - """Brute-force test all possible Twemoji mappings to ensure the image URLs - are correct. - """ + """Brute-force test all possible Twemoji mappings for Discord unicode emojis.""" session.install("-e", ".") session.run("python", "scripts/test_twemoji_mapping.py") From e3b7e6f59b616f94564c2d85ef82a8ac7bed260a Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Fri, 15 May 2020 22:08:54 +0100 Subject: [PATCH 349/922] Updated coalesce_coverage noxfile job [skip deploy] --- ci/pytest.nox.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/ci/pytest.nox.py b/ci/pytest.nox.py index e2c90df630..5c8ff265bf 100644 --- a/ci/pytest.nox.py +++ b/ci/pytest.nox.py @@ -60,6 +60,7 @@ def coalesce_coverage(session: nox.Session) -> None: for file in os.listdir(config.ARTIFACT_DIRECTORY): if file.endswith(".coverage"): coverage_files.append(os.path.join(config.ARTIFACT_DIRECTORY, file)) + print("files for coverage:", coverage_files) - session.run("coverage", "combine", *coverage_files) - session.run("coverage", "report", "-i") + session.run("coverage", "combine", f"--rcfile={config.COVERAGE_INI}", *coverage_files) + session.run("coverage", "report", "-i", "-m", f"--rcfile={config.COVERAGE_INI}") From b364d5d77fbac37d6032717795a9301579b5437b Mon Sep 17 00:00:00 2001 From: davfsa Date: Sat, 16 May 2020 14:43:03 +0200 Subject: [PATCH 350/922] Fixed some bugs and inconsistencies - Fix events that werent getting fired - Passing `None` as `predicate` to `bot.wait_for` - Fix `deserializer` bugs - First line inconsistency - Fix some TODOs --- hikari/applications.py | 6 +++-- hikari/channels.py | 8 +++--- hikari/clients/bot_base.py | 4 +-- hikari/colors.py | 1 + hikari/events/base.py | 1 + hikari/events/channel.py | 5 ++-- hikari/events/consumers.py | 2 +- hikari/events/dispatchers.py | 12 ++++++--- hikari/events/event_managers.py | 3 ++- hikari/events/intent_aware_dispatchers.py | 17 ++++++++---- hikari/events/message.py | 2 -- hikari/events/other.py | 5 ++-- hikari/guilds.py | 6 ++--- hikari/internal/more_collections.py | 16 ++++++++++++ hikari/state/stateless.py | 12 ++++----- tests/hikari/events/test_channel.py | 6 ++--- .../events/test_intent_aware_dispatchers.py | 10 +++---- tests/hikari/events/test_other.py | 2 +- .../hikari/internal/test_more_collections.py | 26 +++++++++++++++++++ tests/hikari/state/test_stateless.py | 20 +++++++------- 20 files changed, 110 insertions(+), 54 deletions(-) diff --git a/hikari/applications.py b/hikari/applications.py index 6545a23a30..d4c9b758b9 100644 --- a/hikari/applications.py +++ b/hikari/applications.py @@ -299,7 +299,9 @@ def _deserialize_members( class Team(bases.Unique, marshaller.Deserializable): """Represents a development team, along with all its members.""" - icon_hash: typing.Optional[str] = marshaller.attrib(raw_name="icon", deserializer=str, eq=False, hash=False) + icon_hash: typing.Optional[str] = marshaller.attrib( + raw_name="icon", deserializer=str, if_none=None, eq=False, hash=False + ) """The hash of this team's icon, if set.""" members: typing.Mapping[bases.Snowflake, TeamMember] = marshaller.attrib( @@ -419,7 +421,7 @@ class Application(bases.Unique, marshaller.Deserializable): """The base64 encoded key used for the GameSDK's `GetTicket`.""" icon_hash: typing.Optional[str] = marshaller.attrib( - raw_name="icon", deserializer=str, if_undefined=None, default=None, eq=False, hash=False + raw_name="icon", deserializer=str, if_undefined=None, if_none=None, default=None, eq=False, hash=False ) """The hash of this application's icon, if set.""" diff --git a/hikari/channels.py b/hikari/channels.py index 5fb29a651e..9cea7abfff 100644 --- a/hikari/channels.py +++ b/hikari/channels.py @@ -280,7 +280,7 @@ class GuildChannel(PartialChannel): """ parent_id: typing.Optional[bases.Snowflake] = marshaller.attrib( - deserializer=bases.Snowflake, if_none=None, if_undefined=None, eq=False, hash=False, repr=True + deserializer=bases.Snowflake, if_none=None, if_undefined=None, default=None, eq=False, hash=False, repr=True ) """The ID of the parent category the channel belongs to.""" @@ -325,7 +325,7 @@ class GuildTextChannel(GuildChannel): """ last_pin_timestamp: typing.Optional[datetime.datetime] = marshaller.attrib( - deserializer=conversions.parse_iso_8601_ts, if_none=None, if_undefined=None, eq=False, hash=False + deserializer=conversions.parse_iso_8601_ts, if_none=None, if_undefined=None, default=None, eq=False, hash=False ) """The timestamp of the last-pinned message. @@ -340,7 +340,7 @@ class GuildTextChannel(GuildChannel): class GuildNewsChannel(GuildChannel): """Represents an news channel.""" - topic: str = marshaller.attrib(deserializer=str, if_none=None, eq=False, hash=False) + topic: typing.Optional[str] = marshaller.attrib(deserializer=str, if_none=None, eq=False, hash=False) """The topic of the channel.""" last_message_id: typing.Optional[bases.Snowflake] = marshaller.attrib( @@ -353,7 +353,7 @@ class GuildNewsChannel(GuildChannel): """ last_pin_timestamp: typing.Optional[datetime.datetime] = marshaller.attrib( - deserializer=conversions.parse_iso_8601_ts, if_none=None, if_undefined=None, eq=False, hash=False + deserializer=conversions.parse_iso_8601_ts, if_none=None, if_undefined=None, default=None, eq=False, hash=False ) """The timestamp of the last-pinned message. diff --git a/hikari/clients/bot_base.py b/hikari/clients/bot_base.py index 57656e0997..9d60879aa2 100644 --- a/hikari/clients/bot_base.py +++ b/hikari/clients/bot_base.py @@ -251,8 +251,8 @@ def wait_for( self, event_type: typing.Type[dispatchers.EventT], *, - timeout: typing.Optional[float], - predicate: dispatchers.PredicateT, + timeout: typing.Optional[float] = None, + predicate: typing.Optional[dispatchers.PredicateT] = None, ) -> more_typing.Future: return self.event_dispatcher.wait_for(event_type, timeout=timeout, predicate=predicate) diff --git a/hikari/colors.py b/hikari/colors.py index 137aa9d5ef..14a926c9ac 100644 --- a/hikari/colors.py +++ b/hikari/colors.py @@ -17,6 +17,7 @@ # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . """Model that represents a common RGB color and provides simple conversions to other common color systems.""" + from __future__ import annotations __all__ = ["Color"] diff --git a/hikari/events/base.py b/hikari/events/base.py index 06e5e6567f..d973ea35c9 100644 --- a/hikari/events/base.py +++ b/hikari/events/base.py @@ -17,6 +17,7 @@ # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . """Bases for components and entities that are used to describe Discord gateway events.""" + from __future__ import annotations __all__ = ["HikariEvent", "get_required_intents_for", "requires_intents", "no_catch", "is_no_catch_event"] diff --git a/hikari/events/channel.py b/hikari/events/channel.py index ec7c653da3..9a872de1d0 100644 --- a/hikari/events/channel.py +++ b/hikari/events/channel.py @@ -17,6 +17,7 @@ # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . """Components and entities that are used to describe Discord gateway channel events.""" + from __future__ import annotations __all__ = [ @@ -24,7 +25,7 @@ "ChannelCreateEvent", "ChannelUpdateEvent", "ChannelDeleteEvent", - "ChannelPinUpdateEvent", + "ChannelPinsUpdateEvent", "WebhookUpdateEvent", "TypingStartEvent", "InviteCreateEvent", @@ -190,7 +191,7 @@ class ChannelDeleteEvent(BaseChannelEvent): @base_events.requires_intents(intents.Intent.GUILDS) @marshaller.marshallable() @attr.s(eq=False, hash=False, kw_only=True, slots=True) -class ChannelPinUpdateEvent(base_events.HikariEvent, marshaller.Deserializable): +class ChannelPinsUpdateEvent(base_events.HikariEvent, marshaller.Deserializable): """Used to represent the Channel Pins Update gateway event. Sent when a message is pinned or unpinned in a channel but not diff --git a/hikari/events/consumers.py b/hikari/events/consumers.py index 046e0d4e4d..e73d1f03a5 100644 --- a/hikari/events/consumers.py +++ b/hikari/events/consumers.py @@ -44,7 +44,7 @@ class RawEventConsumer(abc.ABC): """ @abc.abstractmethod - def process_raw_event( + async def process_raw_event( self, shard_client_obj: shards.ShardClient, name: str, payload: typing.Mapping[str, str], ) -> None: """Consume a raw event that was received from a shard connection. diff --git a/hikari/events/dispatchers.py b/hikari/events/dispatchers.py index 0c9e060aed..1571fdb0cb 100644 --- a/hikari/events/dispatchers.py +++ b/hikari/events/dispatchers.py @@ -81,7 +81,11 @@ def remove_listener(self, event_type: typing.Type[EventT], callback: EventCallba @abc.abstractmethod def wait_for( - self, event_type: typing.Type[EventT], *, timeout: typing.Optional[float], predicate: PredicateT + self, + event_type: typing.Type[EventT], + *, + timeout: typing.Optional[float] = None, + predicate: typing.Optional[PredicateT] = None, ) -> more_typing.Future: """Wait for the given event type to occur. @@ -96,9 +100,9 @@ def wait_for( may leak memory if you do this from an event listener that gets repeatedly called. If you want to do this, you should consider using an event listener instead of this function. - predicate : `def predicate(event) -> bool` or `async def predicate(event) -> bool` - A function that takes the arguments for the event and returns True - if it is a match, or False if it should be ignored. + predicate : `def predicate(event) -> bool` or `async def predicate(event) -> bool`, optional + A function that takes the arguments for the event and returns `True` + if it is a match, or `False` if it should be ignored. This can be a coroutine function that returns a boolean, or a regular function. diff --git a/hikari/events/event_managers.py b/hikari/events/event_managers.py index 1851736684..01954967c0 100644 --- a/hikari/events/event_managers.py +++ b/hikari/events/event_managers.py @@ -32,6 +32,7 @@ if typing.TYPE_CHECKING: from hikari.clients import components as _components from hikari.clients import shards + from hikari.internal import more_typing EVENT_MARKER_ATTR: typing.Final[str] = "___event_name___" @@ -154,7 +155,7 @@ def __init__(self, components: _components.Components) -> None: self.raw_event_mappers[event_name] = member async def process_raw_event( - self, shard_client_obj: shards.ShardClient, name: str, payload: typing.Mapping[str, typing.Any], + self, shard_client_obj: shards.ShardClient, name: str, payload: more_typing.JSONObject, ) -> None: """Process a low level event. diff --git a/hikari/events/intent_aware_dispatchers.py b/hikari/events/intent_aware_dispatchers.py index 957dd7517c..0a7d930a3d 100644 --- a/hikari/events/intent_aware_dispatchers.py +++ b/hikari/events/intent_aware_dispatchers.py @@ -17,6 +17,7 @@ # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . """Event dispatcher implementations that are intent-aware.""" + from __future__ import annotations __all__ = ["IntentAwareEventDispatcherImpl"] @@ -192,6 +193,12 @@ def dispatch_event(self, event: base.HikariEvent) -> more_typing.Future[typing.A subtype_waiters = self._waiters.get(base_event_type, more_collections.EMPTY_DICT) for future, predicate in subtype_waiters.items(): + # If there is no predicate, there is nothing to check, so just return what we got. + if not predicate: + future.set_result(event) + futures_to_remove.append(future) + continue + # We execute async predicates differently to sync, because we hope most of the time # these checks will be synchronous only, as these will perform significantly faster. # I preferred execution speed over terseness here. @@ -288,8 +295,8 @@ def wait_for( self, event_type: typing.Type[dispatchers.EventT], *, - timeout: typing.Optional[float], - predicate: dispatchers.PredicateT, + timeout: typing.Optional[float] = None, + predicate: typing.Optional[dispatchers.PredicateT] = None, ) -> more_typing.Future: """Wait for a event to occur once and then return the arguments the event was called with. @@ -312,9 +319,9 @@ def wait_for( leak memory if you do this from an event listener that gets repeatedly called. If you want to do this, you should consider using an event listener instead of this function. - predicate : `def predicate(event) -> bool` - A function that takes the arguments for the event and returns True - if it is a match, or False if it should be ignored. This must be + predicate : `def predicate(event) -> bool` or `async def predicate(event) -> bool`, optional + A function that takes the arguments for the event and returns `True` + if it is a match, or `False` if it should be ignored. This must be a regular function. Returns diff --git a/hikari/events/message.py b/hikari/events/message.py index 409e145b82..13d82948bd 100644 --- a/hikari/events/message.py +++ b/hikari/events/message.py @@ -97,8 +97,6 @@ class MessageUpdateEvent(base_events.HikariEvent, base_entities.Unique, marshall alongside field nullability. """ - # FIXME: the id here is called "id", but in MessageDeleteEvent it is "message_id"... - channel_id: base_entities.Snowflake = marshaller.attrib(deserializer=base_entities.Snowflake, repr=True) """The ID of the channel that the message was sent in.""" diff --git a/hikari/events/other.py b/hikari/events/other.py index c068da8037..39f92ef5ec 100644 --- a/hikari/events/other.py +++ b/hikari/events/other.py @@ -30,7 +30,7 @@ "StoppedEvent", "ReadyEvent", "ResumedEvent", - "UserUpdateEvent", + "MyUserUpdateEvent", ] import typing @@ -169,10 +169,9 @@ def shard_count(self) -> typing.Optional[int]: return self._shard_information[1] if self._shard_information else None -# TODO: rename to MyUserUpdateEvent @marshaller.marshallable() @attr.s(eq=False, hash=False, kw_only=True, slots=True) -class UserUpdateEvent(base_events.HikariEvent, users.MyUser): +class MyUserUpdateEvent(base_events.HikariEvent, users.MyUser): """Used to represent User Update gateway events. Sent when the current user is updated. diff --git a/hikari/guilds.py b/hikari/guilds.py index 54b0344fe1..a89c5f5bfc 100644 --- a/hikari/guilds.py +++ b/hikari/guilds.py @@ -1077,7 +1077,7 @@ def _deserialize_presences( @marshaller.marshallable() @attr.s(eq=True, hash=True, kw_only=True, slots=True) -class Guild(PartialGuild): +class Guild(PartialGuild): # pylint:disable=too-many-instance-attributes """A representation of a guild on Discord. !!! note @@ -1259,7 +1259,7 @@ class Guild(PartialGuild): """Whether the guild is considered to be large or not. This information is only available if the guild was sent via a `GUILD_CREATE` - event. If the guild is received from any other place, this will always b + event. If the guild is received from any other place, this will always be `None`. The implications of a large guild are that presence information will not be @@ -1357,7 +1357,7 @@ class Guild(PartialGuild): ) """The maximum number of users allowed in a video channel together. - If not available, this field will be `None`. + This information may not be present, in which case, it will be `None`. """ vanity_url_code: typing.Optional[str] = marshaller.attrib(deserializer=str, if_none=None, eq=False, hash=False) diff --git a/hikari/internal/more_collections.py b/hikari/internal/more_collections.py index 571936c8c2..b7b7830e7c 100644 --- a/hikari/internal/more_collections.py +++ b/hikari/internal/more_collections.py @@ -58,3 +58,19 @@ class Commands: """ __slots__ = () + + +class WeakValueDictionary(typing.Generic[_K, _V], weakref.WeakValueDictionary, typing.MutableMapping[_K, _V]): + """A dictionary that has weak references to the values. + + This is a type-safe version of `weakref.WeakValueDictionary` which is + subscriptable. + + Examples + -------- + @attr.s(auto_attribs=True) + class Commands: + aliases: WeakValueDictionary[str, Command] + """ + + __slots__ = () diff --git a/hikari/state/stateless.py b/hikari/state/stateless.py index 33e48c5c4d..e1ef8452ef 100644 --- a/hikari/state/stateless.py +++ b/hikari/state/stateless.py @@ -81,10 +81,10 @@ async def on_channel_delete(self, _, payload) -> None: event = channel.ChannelDeleteEvent.deserialize(payload, components=self._components) await self._components.event_dispatcher.dispatch_event(event) - @event_managers.raw_event_mapper("CHANNEL_PIN_UPDATE") - async def on_channel_pin_update(self, _, payload) -> None: - """Handle CHANNEL_PIN_UPDATE events.""" - event = channel.ChannelPinUpdateEvent.deserialize(payload, components=self._components) + @event_managers.raw_event_mapper("CHANNEL_PINS_UPDATE") + async def on_channel_pins_update(self, _, payload) -> None: + """Handle CHANNEL_PINS_UPDATE events.""" + event = channel.ChannelPinsUpdateEvent.deserialize(payload, components=self._components) await self._components.event_dispatcher.dispatch_event(event) @event_managers.raw_event_mapper("GUILD_CREATE") @@ -249,9 +249,9 @@ async def on_typing_start(self, _, payload) -> None: await self._components.event_dispatcher.dispatch_event(event) @event_managers.raw_event_mapper("USER_UPDATE") - async def on_user_update(self, _, payload) -> None: + async def on_my_user_update(self, _, payload) -> None: """Handle USER_UPDATE events.""" - event = other.UserUpdateEvent.deserialize(payload, components=self._components) + event = other.MyUserUpdateEvent.deserialize(payload, components=self._components) await self._components.event_dispatcher.dispatch_event(event) @event_managers.raw_event_mapper("VOICE_STATE_UPDATE") diff --git a/tests/hikari/events/test_channel.py b/tests/hikari/events/test_channel.py index 9fe0f07554..6ea2c97126 100644 --- a/tests/hikari/events/test_channel.py +++ b/tests/hikari/events/test_channel.py @@ -118,7 +118,7 @@ class TestChannelDeleteEvent: ... -class TestChannelPinUpdateEvent: +class TestChannelPinsUpdateEvent: @pytest.fixture() def test_chanel_pin_update_payload(self): return { @@ -130,12 +130,12 @@ def test_chanel_pin_update_payload(self): def test_deserialize(self, test_chanel_pin_update_payload): mock_timestamp = mock.MagicMock(datetime.datetime) with _helpers.patch_marshal_attr( - channel.ChannelPinUpdateEvent, + channel.ChannelPinsUpdateEvent, "last_pin_timestamp", deserializer=conversions.parse_iso_8601_ts, return_value=mock_timestamp, ) as patched_iso_parser: - channel_pin_add_obj = channel.ChannelPinUpdateEvent.deserialize(test_chanel_pin_update_payload) + channel_pin_add_obj = channel.ChannelPinsUpdateEvent.deserialize(test_chanel_pin_update_payload) patched_iso_parser.assert_called_once_with("2020-03-20T16:08:25.412000+00:00") assert channel_pin_add_obj.guild_id == 424242 assert channel_pin_add_obj.channel_id == 29292929 diff --git a/tests/hikari/events/test_intent_aware_dispatchers.py b/tests/hikari/events/test_intent_aware_dispatchers.py index 4a695dc5f2..d41c892fc3 100644 --- a/tests/hikari/events/test_intent_aware_dispatchers.py +++ b/tests/hikari/events/test_intent_aware_dispatchers.py @@ -262,9 +262,9 @@ def truthy(event): return True intent_aware_dispatcher._waiters = { - Event1: {f1_1: truthy, f1_2: truthy, f1_3: truthy}, - Event2: {f2_1: truthy, f2_2: truthy, f2_3: truthy}, - Event3: {f3_1: truthy, f3_2: truthy, f3_3: truthy}, + Event1: {f1_1: None, f1_2: truthy, f1_3: None}, + Event2: {f2_1: truthy, f2_2: None, f2_3: truthy}, + Event3: {f3_1: None, f3_2: None, f3_3: None}, } inst = Event2() @@ -307,9 +307,9 @@ def truthy(event): return True intent_aware_dispatcher._waiters = { - A: {a_future: truthy}, + A: {a_future: None}, B: {b_future: truthy}, - C: {c_future: truthy}, + C: {c_future: None}, D: {d_future: truthy}, } diff --git a/tests/hikari/events/test_other.py b/tests/hikari/events/test_other.py index 561df1360e..08a491c652 100644 --- a/tests/hikari/events/test_other.py +++ b/tests/hikari/events/test_other.py @@ -124,5 +124,5 @@ class TestResumedEvent: # Doesn't declare any new fields. -class TestUserUpdateEvent: +class TestMyUserUpdateEvent: ... diff --git a/tests/hikari/internal/test_more_collections.py b/tests/hikari/internal/test_more_collections.py index cd7d0501da..b931f8a2b6 100644 --- a/tests/hikari/internal/test_more_collections.py +++ b/tests/hikari/internal/test_more_collections.py @@ -46,3 +46,29 @@ class Value: assert len([*d.keys()]) == 1 assert value1 in d.values() assert value2 not in d.values() + + +class TestWeakValueDictionary: + def test_is_weak(self): + class Key: + pass + + class Value: + pass + + d: more_collections.WeakValueDictionary[Key, Value] = more_collections.WeakValueDictionary() + + key1 = Key() + key2 = Key() + value1 = Value() + value2 = Value() + + d[key1] = value1 + d[key2] = value2 + + assert key1 in d + assert key2 in d + del value2 + assert len([*d.keys()]) == 1 + assert key1 in d + assert key2 not in d diff --git a/tests/hikari/state/test_stateless.py b/tests/hikari/state/test_stateless.py index 1b58554ab7..6401c794c3 100644 --- a/tests/hikari/state/test_stateless.py +++ b/tests/hikari/state/test_stateless.py @@ -124,13 +124,13 @@ async def test_on_channel_delete(self, event_manager_impl, mock_payload): event_manager_impl._components.event_dispatcher.dispatch_event.assert_called_once_with(mock_event) @pytest.mark.asyncio - async def test_on_channel_pin_update(self, event_manager_impl, mock_payload): - mock_event = mock.MagicMock(channel.ChannelPinUpdateEvent) + async def test_on_channel_pins_update(self, event_manager_impl, mock_payload): + mock_event = mock.MagicMock(channel.ChannelPinsUpdateEvent) - with mock.patch("hikari.events.channel.ChannelPinUpdateEvent.deserialize", return_value=mock_event) as event: - await event_manager_impl.on_channel_pin_update(None, mock_payload) + with mock.patch("hikari.events.channel.ChannelPinsUpdateEvent.deserialize", return_value=mock_event) as event: + await event_manager_impl.on_channel_pins_update(None, mock_payload) - assert event_manager_impl.on_channel_pin_update.___event_name___ == {"CHANNEL_PIN_UPDATE"} + assert event_manager_impl.on_channel_pins_update.___event_name___ == {"CHANNEL_PINS_UPDATE"} event.assert_called_once_with(mock_payload, components=event_manager_impl._components) event_manager_impl._components.event_dispatcher.dispatch_event.assert_called_once_with(mock_event) @@ -462,13 +462,13 @@ async def test_on_typing_start(self, event_manager_impl, mock_payload): event_manager_impl._components.event_dispatcher.dispatch_event.assert_called_once_with(mock_event) @pytest.mark.asyncio - async def test_on_user_update(self, event_manager_impl, mock_payload): - mock_event = mock.MagicMock(other.UserUpdateEvent) + async def test_on_my_user_update(self, event_manager_impl, mock_payload): + mock_event = mock.MagicMock(other.MyUserUpdateEvent) - with mock.patch("hikari.events.other.UserUpdateEvent.deserialize", return_value=mock_event) as event: - await event_manager_impl.on_user_update(None, mock_payload) + with mock.patch("hikari.events.other.MyUserUpdateEvent.deserialize", return_value=mock_event) as event: + await event_manager_impl.on_my_user_update(None, mock_payload) - assert event_manager_impl.on_user_update.___event_name___ == {"USER_UPDATE"} + assert event_manager_impl.on_my_user_update.___event_name___ == {"USER_UPDATE"} event.assert_called_once_with(mock_payload, components=event_manager_impl._components) event_manager_impl._components.event_dispatcher.dispatch_event.assert_called_once_with(mock_event) From 1e295cc15917a1675314e4ec21ae3193ab8dbdff Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Sat, 16 May 2020 15:00:13 +0100 Subject: [PATCH 351/922] Added stuff for voice server state. --- ci/gitlab/linting.yml | 2 +- ci/pylint.nox.py | 68 +--------------------------- hikari/events/__init__.py | 3 ++ hikari/events/channel.py | 32 ------------- hikari/events/voice.py | 60 ++++++++++++++++++++++++ hikari/net/shards.py | 34 ++++++++++++++ hikari/state/stateless.py | 7 ++- tests/hikari/events/test_channel.py | 17 ------- tests/hikari/events/test_voice.py | 33 ++++++++++++++ tests/hikari/net/test_shards.py | 20 ++++++++ tests/hikari/state/test_stateless.py | 9 ++-- 11 files changed, 162 insertions(+), 123 deletions(-) create mode 100644 hikari/events/voice.py create mode 100644 tests/hikari/events/test_voice.py diff --git a/ci/gitlab/linting.yml b/ci/gitlab/linting.yml index 6ca9747380..3c5a95aad1 100644 --- a/ci/gitlab/linting.yml +++ b/ci/gitlab/linting.yml @@ -45,7 +45,7 @@ pylint: junit: public/pylint.xml extends: .lint script: - - nox -s pylint --no-error-on-external-run -- --also-junit-report --also-html-report + - nox -s pylint --no-error-on-external-run ### ### Documentation linting. diff --git a/ci/pylint.nox.py b/ci/pylint.nox.py index 3095df7cbf..2058e21f46 100644 --- a/ci/pylint.nox.py +++ b/ci/pylint.nox.py @@ -17,10 +17,7 @@ # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . """Pylint support.""" -import os import traceback -from concurrent import futures - from ci import config from ci import nox @@ -33,80 +30,17 @@ SUCCESS_CODES = list(range(0, 256)) -JUNIT_FLAG = "--and-junit-report" -HTML_FLAG = "--and-html-report" -NON_PYLINT_FLAGS = [JUNIT_FLAG, HTML_FLAG] - @nox.session(default=True, reuse_venv=True) def pylint(session: nox.Session) -> None: """Run pylint against the code base and report any code smells or issues.""" - tasks = [pylint_text] - cpus_per_task = max(1, (os.cpu_count() or 1) // len(tasks)) - - if JUNIT_FLAG in session.posargs: - tasks.append(pylint_junit) - - if HTML_FLAG in session.posargs: - tasks.append(pylint_html) - session.install( "-r", config.REQUIREMENTS, "-r", config.DEV_REQUIREMENTS, ) - if "--jobs" not in session.posargs: - print("Using", cpus_per_task, "workers per task") - extra_flags = ["--jobs", str(cpus_per_task)] - else: - extra_flags = [] - - # Mapping concurrently halves the execution time (unless you have less than - # two CPU cores, but who cares). - with futures.ThreadPoolExecutor(max_workers=len(tasks)) as pool: - pool.map(lambda f: f(session, *extra_flags), tasks) - - -def pylint_text(session: nox.Session, *extra_flags) -> None: try: print("generating plaintext report") - args = [arg for arg in session.posargs if arg not in NON_PYLINT_FLAGS] - session.run(*FLAGS, *args, *extra_flags, success_codes=SUCCESS_CODES) - except Exception: - traceback.print_exc() - - -def pylint_junit(session: nox.Session, *extra_flags) -> None: - try: - print("generating junit report") - if not os.path.exists(config.ARTIFACT_DIRECTORY): - os.mkdir(config.ARTIFACT_DIRECTORY) - args = [arg for arg in session.posargs if arg not in NON_PYLINT_FLAGS] - with open(config.PYLINT_JUNIT_OUTPUT_PATH, "w") as fp: - session.run( - *FLAGS, - "--output-format", - "pylint_junit.JUnitReporter", - *args, - *extra_flags, - stdout=fp, - success_codes=SUCCESS_CODES - ) - except Exception: - traceback.print_exc() - - -def pylint_html(session: nox.Session, *extra_flags) -> None: - try: - print("generating json report") - args = [arg for arg in session.posargs if arg not in NON_PYLINT_FLAGS] - if not os.path.exists(config.ARTIFACT_DIRECTORY): - os.mkdir(config.ARTIFACT_DIRECTORY) - with open(config.PYLINT_JSON_OUTPUT_PATH, "w") as fp: - session.run(*FLAGS, "--output-format", "json", *args, *extra_flags, stdout=fp, success_codes=SUCCESS_CODES) - print("producing html report in", config.PYTEST_HTML_OUTPUT_PATH) - session.run("pylint-json2html", "-o", config.PYLINT_HTML_OUTPUT_PATH, config.PYLINT_JSON_OUTPUT_PATH) - print("artifacts:") - print(os.listdir(config.ARTIFACT_DIRECTORY)) + session.run(*FLAGS, *session.posargs, success_codes=SUCCESS_CODES) except Exception: traceback.print_exc() diff --git a/hikari/events/__init__.py b/hikari/events/__init__.py index 74de19715d..cbd6006e6a 100644 --- a/hikari/events/__init__.py +++ b/hikari/events/__init__.py @@ -25,11 +25,13 @@ from hikari.events import guild from hikari.events import message from hikari.events import other +from hikari.events import voice from hikari.events.base import * from hikari.events.channel import * from hikari.events.guild import * from hikari.events.message import * from hikari.events.other import * +from hikari.events.voice import * __all__ = [ *base.__all__, @@ -37,4 +39,5 @@ *guild.__all__, *message.__all__, *other.__all__, + *voice.__all__, ] diff --git a/hikari/events/channel.py b/hikari/events/channel.py index 9a872de1d0..16c7e24ff1 100644 --- a/hikari/events/channel.py +++ b/hikari/events/channel.py @@ -30,8 +30,6 @@ "TypingStartEvent", "InviteCreateEvent", "InviteDeleteEvent", - "VoiceStateUpdateEvent", - "VoiceServerUpdateEvent", ] import abc @@ -46,7 +44,6 @@ from hikari import intents from hikari import invites from hikari import users -from hikari import voices from hikari.events import base as base_events from hikari.internal import conversions from hikari.internal import marshaller @@ -361,32 +358,3 @@ class InviteDeleteEvent(base_events.HikariEvent, marshaller.Deserializable): This will be `None` if this invite belonged to a DM channel. """ - - -@base_events.requires_intents(intents.Intent.GUILD_VOICE_STATES) -@marshaller.marshallable() -@attr.s(eq=False, hash=False, kw_only=True, slots=True) -class VoiceStateUpdateEvent(base_events.HikariEvent, voices.VoiceState): - """Used to represent voice state update gateway events. - - Sent when a user joins, leaves or moves voice channel(s). - """ - - -@marshaller.marshallable() -@attr.s(eq=False, hash=False, kw_only=True, slots=True) -class VoiceServerUpdateEvent(base_events.HikariEvent, marshaller.Deserializable): - """Used to represent voice server update gateway events. - - Sent when initially connecting to voice and when the current voice instance - falls over to a new server. - """ - - token: str = marshaller.attrib(deserializer=str) - """The voice connection's string token.""" - - guild_id: base_entities.Snowflake = marshaller.attrib(deserializer=base_entities.Snowflake, repr=True) - """The ID of the guild this voice server update is for.""" - - endpoint: str = marshaller.attrib(deserializer=str, repr=True) - """The uri for this voice server host.""" diff --git a/hikari/events/voice.py b/hikari/events/voice.py new file mode 100644 index 0000000000..bcf3f2ece4 --- /dev/null +++ b/hikari/events/voice.py @@ -0,0 +1,60 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# Copyright © Nekoka.tt 2019-2020 +# +# This file is part of Hikari. +# +# Hikari is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Hikari is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Hikari. If not, see . +"""Voice server event types.""" + +from __future__ import annotations + +__all__ = ["VoiceStateUpdateEvent", "VoiceServerUpdateEvent"] + +import attr + +from hikari.events import base as base_events +from hikari import bases as base_entities +from hikari import intents +from hikari import voices +from hikari.internal import marshaller + + +@base_events.requires_intents(intents.Intent.GUILD_VOICE_STATES) +@marshaller.marshallable() +@attr.s(eq=False, hash=False, kw_only=True, slots=True) +class VoiceStateUpdateEvent(base_events.HikariEvent, voices.VoiceState): + """Used to represent voice state update gateway events. + + Sent when a user joins, leaves or moves voice channel(s). + """ + + +@marshaller.marshallable() +@attr.s(eq=False, hash=False, kw_only=True, slots=True) +class VoiceServerUpdateEvent(base_events.HikariEvent, marshaller.Deserializable): + """Used to represent voice server update gateway events. + + Sent when initially connecting to voice and when the current voice instance + falls over to a new server. + """ + + token: str = marshaller.attrib(deserializer=str) + """The voice connection's string token.""" + + guild_id: base_entities.Snowflake = marshaller.attrib(deserializer=base_entities.Snowflake, repr=True) + """The ID of the guild this voice server update is for.""" + + endpoint: str = marshaller.attrib(deserializer=str, repr=True) + """The URI for this voice server host.""" diff --git a/hikari/net/shards.py b/hikari/net/shards.py index b37e38eab3..96958e3fc1 100644 --- a/hikari/net/shards.py +++ b/hikari/net/shards.py @@ -507,6 +507,40 @@ async def update_presence(self, presence: typing.Dict) -> None: await self._send({"op": codes.GatewayOpcode.PRESENCE_UPDATE, "d": presence}) self._presence = presence + async def update_voice_state( + self, guild_id: str, channel_id: typing.Optional[str], self_mute: bool = False, self_deaf: bool = False, + ) -> None: + """Send a VOICE_STATE_UPDATE payload for voice control. + + After sending this payload, you should wait for a VOICE_STATE_UPDATE + event to be received for the corresponding guild and channel ID. This + will contain instructions where appropriate to continue with creating + a new voice connection, etc. + + This implementation will only request the initial voice payload. Any + logic for initializing a voice connection must be done separately. + + Parameters + ---------- + guild_id : str + The guild ID to change the voice state within. + channel_id : str, optional + The channel ID in the guild to change the voice state within. + If this is `None`, then this will behave as a disconnect opcode. + self_mute : bool + If `True`, then the bot user will be muted. Defaults to `False`. + self_deaf : bool + If `True`, then the bot user will be deafened. Defaults to `False`. + This doesn't currently have much effect, since receiving voice + data is undocumented for bots. + """ + payload = { + "op": codes.GatewayOpcode.VOICE_STATE_UPDATE, + "d": {"guild_id": guild_id, "channel_id": channel_id, "self_mute": self_mute, "self_deaf": self_deaf,}, + } + + await self._send(payload) + async def connect(self) -> None: """Connect to the gateway and return when it closes.""" if self.is_connected: diff --git a/hikari/state/stateless.py b/hikari/state/stateless.py index e1ef8452ef..f1703e2ff0 100644 --- a/hikari/state/stateless.py +++ b/hikari/state/stateless.py @@ -31,6 +31,9 @@ # pylint: disable=too-many-public-methods +from hikari.events import voice + + class StatelessEventManagerImpl(event_managers.EventManager[dispatchers.EventDispatcher]): """Stateless event manager implementation for stateless bots. @@ -257,13 +260,13 @@ async def on_my_user_update(self, _, payload) -> None: @event_managers.raw_event_mapper("VOICE_STATE_UPDATE") async def on_voice_state_update(self, _, payload) -> None: """Handle VOICE_STATE_UPDATE events.""" - event = channel.VoiceStateUpdateEvent.deserialize(payload, components=self._components) + event = voice.VoiceStateUpdateEvent.deserialize(payload, components=self._components) await self._components.event_dispatcher.dispatch_event(event) @event_managers.raw_event_mapper("VOICE_SERVER_UPDATE") async def on_voice_server_update(self, _, payload) -> None: """Handle VOICE_SERVER_UPDATE events.""" - event = channel.VoiceStateUpdateEvent.deserialize(payload, components=self._components) + event = voice.VoiceServerUpdateEvent.deserialize(payload, components=self._components) await self._components.event_dispatcher.dispatch_event(event) @event_managers.raw_event_mapper("WEBHOOK_UPDATE") diff --git a/tests/hikari/events/test_channel.py b/tests/hikari/events/test_channel.py index 6ea2c97126..972b72186b 100644 --- a/tests/hikari/events/test_channel.py +++ b/tests/hikari/events/test_channel.py @@ -276,20 +276,3 @@ def test_deserialize(self, test_invite_delete_payload): assert invite_delete_obj.channel_id == 393939 assert invite_delete_obj.code == "blahblahblah" assert invite_delete_obj.guild_id == 3834833 - - -# Doesn't declare any new fields. -class TestVoiceStateUpdateEvent: - ... - - -class TestVoiceServerUpdateEvent: - @pytest.fixture() - def test_voice_server_update_payload(self): - return {"token": "a_token", "guild_id": "303030300303", "endpoint": "smart.loyal.discord.gg"} - - def test_deserialize(self, test_voice_server_update_payload): - voice_server_update_obj = channel.VoiceServerUpdateEvent.deserialize(test_voice_server_update_payload) - assert voice_server_update_obj.token == "a_token" - assert voice_server_update_obj.guild_id == 303030300303 - assert voice_server_update_obj.endpoint == "smart.loyal.discord.gg" diff --git a/tests/hikari/events/test_voice.py b/tests/hikari/events/test_voice.py new file mode 100644 index 0000000000..048981627d --- /dev/null +++ b/tests/hikari/events/test_voice.py @@ -0,0 +1,33 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# Copyright © Nekoka.tt 2019-2020 +# +# This file is part of Hikari. +# +# Hikari is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Hikari is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Hikari. If not, see . +import pytest + +from hikari.events import voice + + +class TestVoiceServerUpdateEvent: + @pytest.fixture() + def test_voice_server_update_payload(self): + return {"token": "a_token", "guild_id": "303030300303", "endpoint": "smart.loyal.discord.gg"} + + def test_deserialize(self, test_voice_server_update_payload): + voice_server_update_obj = voice.VoiceServerUpdateEvent.deserialize(test_voice_server_update_payload) + assert voice_server_update_obj.token == "a_token" + assert voice_server_update_obj.guild_id == 303030300303 + assert voice_server_update_obj.endpoint == "smart.loyal.discord.gg" diff --git a/tests/hikari/net/test_shards.py b/tests/hikari/net/test_shards.py index 7df0f5ae7a..e73824a354 100644 --- a/tests/hikari/net/test_shards.py +++ b/tests/hikari/net/test_shards.py @@ -891,3 +891,23 @@ async def test_injects_default_fields(self, client): await client.update_presence({"foo": "bar"}) for k in ("foo", "afk", "game", "since", "status"): assert k in client._presence + + +@pytest.mark.asyncio +class TestUpdateVoiceState: + @pytest.fixture + def client(self, event_loop): + asyncio.set_event_loop(event_loop) + client = _helpers.unslot_class(shards.Shard)(token="1234", url="xxx") + client = _helpers.mock_methods_on(client, except_=("update_voice_state",)) + return client + + @pytest.mark.parametrize("channel_id", ["1234", None]) + async def test_sends_payload(self, client, channel_id): + await client.update_voice_state("9987", channel_id, True, False) + client._send.assert_awaited_once_with( + { + "op": codes.GatewayOpcode.VOICE_STATE_UPDATE, + "d": {"guild_id": "9987", "channel_id": channel_id, "self_mute": True, "self_deaf": False,}, + } + ) diff --git a/tests/hikari/state/test_stateless.py b/tests/hikari/state/test_stateless.py index 6401c794c3..7e4852fd82 100644 --- a/tests/hikari/state/test_stateless.py +++ b/tests/hikari/state/test_stateless.py @@ -25,6 +25,7 @@ from hikari.events import guild from hikari.events import message from hikari.events import other +from hikari.events import voice from hikari.state import stateless @@ -474,9 +475,9 @@ async def test_on_my_user_update(self, event_manager_impl, mock_payload): @pytest.mark.asyncio async def test_on_voice_state_update(self, event_manager_impl, mock_payload): - mock_event = mock.MagicMock(channel.VoiceStateUpdateEvent) + mock_event = mock.MagicMock(voice.VoiceStateUpdateEvent) - with mock.patch("hikari.events.channel.VoiceStateUpdateEvent.deserialize", return_value=mock_event) as event: + with mock.patch("hikari.events.voice.VoiceStateUpdateEvent.deserialize", return_value=mock_event) as event: await event_manager_impl.on_voice_state_update(None, mock_payload) assert event_manager_impl.on_voice_state_update.___event_name___ == {"VOICE_STATE_UPDATE"} @@ -485,9 +486,9 @@ async def test_on_voice_state_update(self, event_manager_impl, mock_payload): @pytest.mark.asyncio async def test_on_voice_server_update(self, event_manager_impl, mock_payload): - mock_event = mock.MagicMock(channel.VoiceStateUpdateEvent) + mock_event = mock.MagicMock(voice.VoiceStateUpdateEvent) - with mock.patch("hikari.events.channel.VoiceStateUpdateEvent.deserialize", return_value=mock_event) as event: + with mock.patch("hikari.events.voice.VoiceServerUpdateEvent.deserialize", return_value=mock_event) as event: await event_manager_impl.on_voice_server_update(None, mock_payload) assert event_manager_impl.on_voice_server_update.___event_name___ == {"VOICE_SERVER_UPDATE"} From 9122ac6e5d792abb57ccc84191a38ebfedc2db1c Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Sat, 16 May 2020 19:26:38 +0100 Subject: [PATCH 352/922] Reorganised a bulk of the code. Documentation still needs checking, and unit tests have not been fixed/updated yet. --- README.md | 8 +- hikari/__init__.py | 73 +--- hikari/__main__.py | 21 +- hikari/clients/__init__.py | 49 --- hikari/clients/rest/__init__.py | 98 ----- hikari/clients/test.py | 198 --------- hikari/{net => components}/__init__.py | 17 +- .../application.py} | 24 +- hikari/{clients => components}/bot_base.py | 89 ++-- hikari/{events => components}/consumers.py | 6 +- hikari/{events => components}/dispatchers.py | 31 +- .../{events => components}/event_managers.py | 48 +-- .../intent_aware_dispatchers.py | 29 +- hikari/{clients => components}/runnable.py | 0 hikari/{clients => }/configs.py | 18 +- hikari/errors.py | 2 +- hikari/events/__init__.py | 29 +- hikari/events/base.py | 8 +- hikari/events/channel.py | 83 ++-- hikari/events/guild.py | 54 +-- hikari/events/message.py | 113 +++-- hikari/events/other.py | 25 +- hikari/events/voice.py | 11 +- hikari/gateway/__init__.py | 26 ++ .../{clients/shards.py => gateway/client.py} | 320 ++++++-------- .../{net/shards.py => gateway/connection.py} | 13 +- .../gateway_state.py} | 4 +- hikari/internal/__init__.py | 2 + hikari/{net => internal}/codes.py | 2 +- hikari/internal/conversions.py | 9 +- hikari/internal/helpers.py | 14 +- hikari/{net => internal}/http_client.py | 4 +- hikari/internal/marshaller.py | 8 +- hikari/internal/meta.py | 2 +- hikari/internal/more_asyncio.py | 2 +- hikari/internal/more_typing.py | 6 +- hikari/{net => internal}/ratelimits.py | 398 +---------------- hikari/{net => internal}/tracing.py | 1 + hikari/internal/urls.py | 2 +- hikari/{net => internal}/user_agents.py | 2 +- hikari/models/__init__.py | 62 +++ hikari/{ => models}/applications.py | 49 ++- hikari/{ => models}/audit_logs.py | 56 ++- hikari/{ => models}/bases.py | 6 +- hikari/{ => models}/channels.py | 11 +- hikari/{ => models}/colors.py | 0 hikari/{ => models}/colours.py | 2 +- hikari/{ => models}/embeds.py | 9 +- hikari/{ => models}/emojis.py | 9 +- hikari/{ => models}/files.py | 4 +- .../gateway.py} | 5 +- hikari/{ => models}/guilds.py | 49 +-- hikari/{ => models}/intents.py | 0 hikari/{ => models}/invites.py | 11 +- hikari/{ => models}/messages.py | 50 +-- hikari/{ => models}/pagination.py | 5 +- hikari/{ => models}/permissions.py | 0 hikari/{ => models}/unset.py | 0 hikari/{ => models}/users.py | 4 +- hikari/{ => models}/voices.py | 6 +- hikari/{ => models}/webhooks.py | 49 +-- hikari/rest/__init__.py | 25 ++ hikari/{clients => }/rest/base.py | 17 +- hikari/rest/buckets.py | 405 ++++++++++++++++++ hikari/{clients => }/rest/channel.py | 193 ++++----- hikari/rest/client.py | 98 +++++ hikari/{clients => }/rest/gateway.py | 21 +- hikari/{clients => }/rest/guild.py | 65 +-- hikari/{clients => }/rest/invite.py | 7 +- hikari/{clients => }/rest/me.py | 23 +- hikari/{clients => }/rest/oauth2.py | 6 +- hikari/{clients => }/rest/react.py | 34 +- hikari/{net => rest}/routes.py | 0 hikari/{net/rest.py => rest/session.py} | 34 +- hikari/{clients => }/rest/user.py | 9 +- hikari/{clients => }/rest/voice.py | 6 +- hikari/{clients => }/rest/webhook.py | 39 +- hikari/stateful/.gitkeep | 0 hikari/{state => stateless}/__init__.py | 6 +- .../stateless.py => stateless/bot.py} | 29 +- .../stateless.py => stateless/manager.py} | 5 +- requirements.txt | 1 - tests/hikari/_helpers.py | 4 +- tests/hikari/clients/test_components.py | 16 +- tests/hikari/clients/test_configs.py | 14 +- .../hikari/clients/test_rest/test___init__.py | 38 +- tests/hikari/clients/test_rest/test_base.py | 14 +- .../hikari/clients/test_rest/test_channel.py | 32 +- .../hikari/clients/test_rest/test_gateway.py | 18 +- tests/hikari/clients/test_rest/test_guild.py | 38 +- tests/hikari/clients/test_rest/test_invite.py | 12 +- tests/hikari/clients/test_rest/test_me.py | 26 +- tests/hikari/clients/test_rest/test_oauth2.py | 14 +- tests/hikari/clients/test_rest/test_react.py | 26 +- tests/hikari/clients/test_rest/test_user.py | 12 +- tests/hikari/clients/test_rest/test_voice.py | 12 +- .../hikari/clients/test_rest/test_webhook.py | 18 +- tests/hikari/clients/test_shards.py | 36 +- tests/hikari/events/test_base.py | 2 +- tests/hikari/events/test_channel.py | 5 +- tests/hikari/events/test_dispatchers.py | 2 +- tests/hikari/events/test_guild.py | 5 +- .../events/test_intent_aware_dispatchers.py | 2 +- tests/hikari/events/test_message.py | 8 +- tests/hikari/events/test_other.py | 3 +- tests/hikari/internal/test_helpers.py | 3 +- tests/hikari/internal/test_marshaller.py | 4 +- tests/hikari/net/test_codes.py | 2 +- tests/hikari/net/test_http_client.py | 3 +- tests/hikari/net/test_ratelimits.py | 4 +- tests/hikari/net/test_rest.py | 38 +- tests/hikari/net/test_routes.py | 2 +- tests/hikari/net/test_shards.py | 56 +-- tests/hikari/net/test_user_agents.py | 2 +- tests/hikari/state/test_stateless.py | 12 +- tests/hikari/test_applications.py | 24 +- tests/hikari/test_audit_logs.py | 36 +- tests/hikari/test_bases.py | 10 +- tests/hikari/test_channels.py | 19 +- tests/hikari/test_colors.py | 2 +- tests/hikari/test_colours.py | 3 +- tests/hikari/test_embeds.py | 22 +- tests/hikari/test_emojis.py | 9 +- tests/hikari/test_errors.py | 2 +- tests/hikari/test_files.py | 2 +- tests/hikari/test_gateway_entities.py | 27 +- tests/hikari/test_guilds.py | 36 +- tests/hikari/test_intents.py | 2 +- tests/hikari/test_invites.py | 9 +- tests/hikari/test_messages.py | 30 +- tests/hikari/test_unset.py | 2 +- tests/hikari/test_users.py | 11 +- tests/hikari/test_voices.py | 6 +- tests/hikari/test_webhook.py | 16 +- 134 files changed, 1861 insertions(+), 2149 deletions(-) delete mode 100644 hikari/clients/__init__.py delete mode 100644 hikari/clients/rest/__init__.py delete mode 100644 hikari/clients/test.py rename hikari/{net => components}/__init__.py (59%) rename hikari/{clients/components.py => components/application.py} (67%) rename hikari/{clients => components}/bot_base.py (84%) rename hikari/{events => components}/consumers.py (89%) rename hikari/{events => components}/dispatchers.py (94%) rename hikari/{events => components}/event_managers.py (85%) rename hikari/{events => components}/intent_aware_dispatchers.py (94%) rename hikari/{clients => components}/runnable.py (100%) rename hikari/{clients => }/configs.py (97%) create mode 100644 hikari/gateway/__init__.py rename hikari/{clients/shards.py => gateway/client.py} (74%) rename hikari/{net/shards.py => gateway/connection.py} (99%) rename hikari/{clients/shard_states.py => gateway/gateway_state.py} (96%) rename hikari/{net => internal}/codes.py (99%) rename hikari/{net => internal}/http_client.py (99%) rename hikari/{net => internal}/ratelimits.py (61%) rename hikari/{net => internal}/tracing.py (99%) rename hikari/{net => internal}/user_agents.py (98%) create mode 100644 hikari/models/__init__.py rename hikari/{ => models}/applications.py (90%) rename hikari/{ => models}/audit_logs.py (94%) rename hikari/{ => models}/bases.py (94%) rename hikari/{ => models}/channels.py (98%) rename hikari/{ => models}/colors.py (100%) rename hikari/{ => models}/colours.py (95%) rename hikari/{ => models}/embeds.py (99%) rename hikari/{ => models}/emojis.py (98%) rename hikari/{ => models}/files.py (99%) rename hikari/{gateway_entities.py => models/gateway.py} (98%) rename hikari/{ => models}/guilds.py (97%) rename hikari/{ => models}/intents.py (100%) rename hikari/{ => models}/invites.py (98%) rename hikari/{ => models}/messages.py (95%) rename hikari/{ => models}/pagination.py (98%) rename hikari/{ => models}/permissions.py (100%) rename hikari/{ => models}/unset.py (100%) rename hikari/{ => models}/users.py (98%) rename hikari/{ => models}/voices.py (97%) rename hikari/{ => models}/webhooks.py (92%) create mode 100644 hikari/rest/__init__.py rename hikari/{clients => }/rest/base.py (79%) create mode 100644 hikari/rest/buckets.py rename hikari/{clients => }/rest/channel.py (88%) create mode 100644 hikari/rest/client.py rename hikari/{clients => }/rest/gateway.py (82%) rename hikari/{clients => }/rest/guild.py (98%) rename hikari/{clients => }/rest/invite.py (95%) rename hikari/{clients => }/rest/me.py (94%) rename hikari/{clients => }/rest/oauth2.py (91%) rename hikari/{clients => }/rest/react.py (91%) rename hikari/{net => rest}/routes.py (100%) rename hikari/{net/rest.py => rest/session.py} (99%) rename hikari/{clients => }/rest/user.py (91%) rename hikari/{clients => }/rest/voice.py (91%) rename hikari/{clients => }/rest/webhook.py (92%) create mode 100644 hikari/stateful/.gitkeep rename hikari/{state => stateless}/__init__.py (90%) rename hikari/{clients/stateless.py => stateless/bot.py} (61%) rename hikari/{state/stateless.py => stateless/manager.py} (99%) diff --git a/README.md b/README.md index 8742a97b46..a1dea046a4 100644 --- a/README.md +++ b/README.md @@ -75,12 +75,12 @@ Most mainstream Python Discord APIs lack one or more of the following features. implement each feature as part of the design, rather than an additional component. This enables you to utilize these components as a black box where necessary. -- Low level REST API implementation. +- Low level RESTSession API implementation. - Low level gateway websocket shard implementation. - Rate limiting that complies with the `X-RateLimit-Bucket` header __properly__. - Gateway websocket ratelimiting (prevents your websocket getting completely invalidated). - Intents. -- Proxy support for websockets and REST API. +- Proxy support for websockets and RESTSession API. - File IO that doesn't block you. - Fluent Pythonic API that does not limit your creativity. @@ -94,8 +94,8 @@ to utilize these components as a black box where necessary. to the original format of information provided by Discord as possible ensures that minimal changes are required when a breaking API design is introduced. This reduces the amount of stuff you need to fix in your applications as a result. -- REST only API functionality. Want to write a web dashboard? Feel free to just reuse the - REST client components to achieve that! +- RESTSession only API functionality. Want to write a web dashboard? Feel free to just reuse the + RESTSession client components to achieve that! ### Stuff coming soon diff --git a/hikari/__init__.py b/hikari/__init__.py index 3882e66496..e419e11cdf 100644 --- a/hikari/__init__.py +++ b/hikari/__init__.py @@ -20,60 +20,23 @@ from __future__ import annotations -from hikari import applications -from hikari import audit_logs -from hikari import bases -from hikari import channels -from hikari import clients -from hikari import colors -from hikari import colours -from hikari import embeds -from hikari import emojis -from hikari import errors -from hikari import events -from hikari import files -from hikari import gateway_entities -from hikari import guilds -from hikari import intents -from hikari import invites -from hikari import messages -from hikari import net -from hikari import permissions -from hikari import state -from hikari import users -from hikari import voices -from hikari import webhooks -from hikari._about import __author__ -from hikari._about import __ci__ -from hikari._about import __copyright__ -from hikari._about import __discord_invite__ -from hikari._about import __docs__ -from hikari._about import __email__ -from hikari._about import __issue_tracker__ -from hikari._about import __license__ -from hikari._about import __url__ -from hikari._about import __version__ -from hikari.applications import * -from hikari.audit_logs import * -from hikari.bases import * -from hikari.channels import * -from hikari.clients import * -from hikari.colors import * -from hikari.colours import * -from hikari.embeds import * -from hikari.emojis import * -from hikari.events import * -from hikari.files import * -from hikari.gateway_entities import * -from hikari.guilds import * -from hikari.intents import * -from hikari.invites import * -from hikari.messages import * -from hikari.permissions import * -from hikari.unset import * -from hikari.users import * -from hikari.voices import * -from hikari.webhooks import * +from ._about import __author__ +from ._about import __ci__ +from ._about import __copyright__ +from ._about import __discord_invite__ +from ._about import __docs__ +from ._about import __email__ +from ._about import __issue_tracker__ +from ._about import __license__ +from ._about import __url__ +from ._about import __version__ +from .configs import * +from .events import * +from .errors import * +from .gateway import * +from .models import * +from .rest import * +from .stateful import * +from .stateless import * -# Adding everything to `__all__` pollutes the top level index in our documentation, therefore this is left empty. __all__ = [] diff --git a/hikari/__main__.py b/hikari/__main__.py index 2fc36d4a6e..3da0fae356 100644 --- a/hikari/__main__.py +++ b/hikari/__main__.py @@ -24,20 +24,11 @@ import os import platform -import click - from hikari import _about - -@click.command() -def main(): - """Show the application version, then exit.""" - version = _about.__version__ - path = os.path.abspath(os.path.dirname(inspect.getsourcefile(_about))) - py_impl = platform.python_implementation() - py_ver = platform.python_version() - py_compiler = platform.python_compiler() - print(f"hikari v{version} (installed in {path}) ({py_impl} {py_ver} {py_compiler})") - - -main() +version = _about.__version__ +path = os.path.abspath(os.path.dirname(inspect.getsourcefile(_about))) +py_impl = platform.python_implementation() +py_ver = platform.python_version() +py_compiler = platform.python_compiler() +print(f"hikari v{version} (installed in {path}) ({py_impl} {py_ver} {py_compiler})") diff --git a/hikari/clients/__init__.py b/hikari/clients/__init__.py deleted file mode 100644 index de371f494e..0000000000 --- a/hikari/clients/__init__.py +++ /dev/null @@ -1,49 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# Copyright © Nekokatt 2019-2020 -# -# This file is part of Hikari. -# -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . -"""The models API for interacting with Discord directly.""" - -from __future__ import annotations - -from hikari.clients import bot_base -from hikari.clients import components -from hikari.clients import configs -from hikari.clients import rest -from hikari.clients import runnable -from hikari.clients import shard_states -from hikari.clients import shards -from hikari.clients import stateless -from hikari.clients.bot_base import * -from hikari.clients.components import * -from hikari.clients.configs import * -from hikari.clients.rest import * -from hikari.clients.runnable import * -from hikari.clients.shard_states import * -from hikari.clients.shards import * -from hikari.clients.stateless import * - -__all__ = [ - *bot_base.__all__, - *components.__all__, - *configs.__all__, - *rest.__all__, - *shard_states.__all__, - *shards.__all__, - *runnable.__all__, - *stateless.__all__, -] diff --git a/hikari/clients/rest/__init__.py b/hikari/clients/rest/__init__.py deleted file mode 100644 index 504a89f64f..0000000000 --- a/hikari/clients/rest/__init__.py +++ /dev/null @@ -1,98 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# Copyright © Nekoka.tt 2019-2020 -# -# This file is part of Hikari. -# -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . -"""Marshall wrappings for the REST implementation in `hikari.net.rest`. - -This provides an object-oriented interface for interacting with discord's REST -API. -""" - -from __future__ import annotations - -__all__ = ["RESTClient"] - -import typing - -from hikari.clients.rest import channel -from hikari.clients.rest import gateway -from hikari.clients.rest import guild -from hikari.clients.rest import invite -from hikari.clients.rest import me -from hikari.clients.rest import oauth2 -from hikari.clients.rest import react -from hikari.clients.rest import user -from hikari.clients.rest import voice -from hikari.clients.rest import webhook -from hikari.net import rest as low_level_rest - -if typing.TYPE_CHECKING: - from hikari.clients import components as _components - - -class RESTClient( - channel.RESTChannelComponent, - me.RESTCurrentUserComponent, - gateway.RESTGatewayComponent, - guild.RESTGuildComponent, - invite.RESTInviteComponent, - oauth2.RESTOAuth2Component, - react.RESTReactionComponent, - user.RESTUserComponent, - voice.RESTVoiceComponent, - webhook.RESTWebhookComponent, -): - """ - A marshalling object-oriented REST API client. - - This client bridges the basic REST API exposed by - `hikari.net.rest.REST` and wraps it in a unit of processing that can handle - handle parsing API objects into Hikari entity objects. - - Parameters - ---------- - components : hikari.clients.components.Components - The client components that this rest client should be bound by. - Includes the rest config. - - !!! note - For all endpoints where a `reason` argument is provided, this may be a - string inclusively between `0` and `512` characters length, with any - additional characters being cut off. - """ - - def __init__(self, components: _components.Components) -> None: - token = None - if components.config.token_type is not None: - token = f"{components.config.token_type} {components.config.token}" - super().__init__( - components, - low_level_rest.REST( - allow_redirects=components.config.allow_redirects, - base_url=components.config.rest_url, - connector=components.config.tcp_connector, - debug=components.config.debug, - proxy_headers=components.config.proxy_headers, - proxy_auth=components.config.proxy_auth, - ssl_context=components.config.ssl_context, - verify_ssl=components.config.verify_ssl, - timeout=components.config.request_timeout, - token=token, - trust_env=components.config.trust_env, - version=components.config.rest_version, - ), - ) diff --git a/hikari/clients/test.py b/hikari/clients/test.py deleted file mode 100644 index aaedc795d8..0000000000 --- a/hikari/clients/test.py +++ /dev/null @@ -1,198 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# Copyright © Nekoka.tt 2019-2020 -# -# This file is part of Hikari. -# -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . -"""An executable module to be used to test that the gateway works as intended. - -This is only for use by developers of this library, regular users do not need -to use this. -""" - -from __future__ import annotations - -import datetime -import logging -import math -import os -import re -import sys -import time -import typing - -import click - -import hikari -from hikari.internal import conversions - -_LOGGER_LEVELS: typing.Final[typing.Sequence[str]] = ["DEBUG", "INFO", "WARNING", "ERROR", "NOTSET"] - - -def _supports_color(): - plat = sys.platform - supported_platform = plat != "Pocket PC" and (plat != "win32" or "ANSICON" in os.environ) - # isatty is not always implemented, #6223. - is_a_tty = hasattr(sys.stdout, "isatty") and sys.stdout.isatty() - return supported_platform and is_a_tty - - -_COLOR_FORMAT: typing.Final[str] = ( - "\033[1;35m%(levelname)1.1s \033[0;37m%(name)45.45s \033[0;31m%(asctime)23.23s \033[1;34m%(module)-15.15s " - "\033[1;32m#%(lineno)-4d \033[0m:: \033[0;33m%(message)s\033[0m" -) - -_REGULAR_FORMAT: typing.Final[str] = ( - "%(levelname)1.1s %(name)45.45s %(asctime)23.23s %(module)-15.15s #%(lineno)-4d :: %(message)s" -) - - -@click.command() -@click.option("--compression", default=True, type=click.BOOL, help="Enable or disable gateway compression.") -@click.option("--color", default=_supports_color(), type=click.BOOL, help="Whether to enable or disable color.") -@click.option("--debug", default=False, type=click.BOOL, help="Enable or disable debug mode.") -@click.option("--intents", default=None, type=click.STRING, help="Intent names to enable (comma separated)") -@click.option("--logger", envvar="LOGGER", default="INFO", type=click.Choice(_LOGGER_LEVELS), help="Logger verbosity.") -@click.option("--shards", default=1, type=click.IntRange(min=1), help="The number of shards to explicitly use.") -@click.option("--token", required=True, envvar="TOKEN", help="The token to use to authenticate with Discord.") -@click.option("--verify-ssl", default=True, type=click.BOOL, help="Enable or disable SSL verification.") -@click.option("--gateway-version", default=6, type=click.IntRange(min=6), help="Version of the gateway to use.") -@click.option("--rest-version", default=6, type=click.IntRange(min=6), help="Version of the gateway to use.") -def main(compression, color, debug, intents, logger, shards, token, verify_ssl, gateway_version, rest_version): - """`click` command line client for running a test gateway connection. - - This is provided for internal testing purposes for benchmarking API - stability, etc. - """ - if intents is not None: - intents = intents.split(",") - intents = conversions.dereference_int_flag(hikari.Intent, intents) - - logging.captureWarnings(True) - - logging.basicConfig(level=logger, format=_COLOR_FORMAT if color else _REGULAR_FORMAT, stream=sys.stdout) - - client = hikari.StatelessBot( - token=token, - gateway_version=gateway_version, - rest_version=rest_version, - debug=debug, - gateway_use_compression=compression, - intents=intents, - verify_ssl=verify_ssl, - shard_count=shards, - initial_activity=hikari.Activity(name="people mention me", type=hikari.ActivityType.LISTENING), - ) - - bot_id = 0 - bot_avatar_url = "about:blank" - startup_time = 0 - - @client.event(hikari.StartingEvent) - async def on_start(_): - nonlocal startup_time - startup_time = time.perf_counter() - - @client.event(hikari.ReadyEvent) - async def on_ready(event): - nonlocal bot_id, bot_avatar_url - bot_id = event.my_user.id - bot_avatar_url = event.my_user.avatar_url - - def since(epoch): - if math.isnan(epoch): - return "never" - return datetime.timedelta(seconds=time.perf_counter() - epoch) - - @client.event(hikari.MessageCreateEvent) - async def on_message(event): - if not event.author.is_bot and re.match(f"^<@!?{bot_id}>$", event.content): - start = time.perf_counter() - message = await client.rest.create_message(event.channel_id, content="Pong!") - rest_time = time.perf_counter() - start - - shard_infos = [] - for shard_id, shard in client.shards.items(): - shard_info = ( - f"latency: {shard.heartbeat_latency * 1_000:.0f} ms\n" - f"seq: {shard.seq}\n" - f"session id: {shard.session_id}\n" - f"reconnects: {shard.reconnect_count}\n" - f"heartbeat interval: {shard.heartbeat_interval} s\n" - f"state: {shard.connection_state.name}\n" - ) - - shard_infos.append(hikari.EmbedField(name=f"Shard {shard_id}", value=shard_info, is_inline=False)) - - gw_info = ( - f"intents: {client.intents}\n" - f"version: {client.version}\n" - f"average latency: {client.heartbeat_latency * 1_000:.0f} ms\n" - f"shards: {len(client.shards)}\n" - f"compression: {compression}\n" - f"debug: {debug}\n" - ) - - actively_limited_routes = sum( - 1 - for b in client.rest._session.bucket_ratelimiters.real_hashes_to_buckets.values() - if b.throttle_task is not None - ) - - actively_limited_calls = sum( - len(b.queue) - for b in client.rest._session.bucket_ratelimiters.real_hashes_to_buckets.values() - if b.throttle_task is not None - ) - - rest_info = ( - f"message send time: {rest_time * 1_000:.0f} ms\n" - f"global ratelimiter backlog: {len(client.rest._session.global_ratelimiter.queue)}\n" - f"cached limiter routes: {len(client.rest._session.bucket_ratelimiters.routes_to_hashes)}\n" - f"cached limiter buckets: {len(client.rest._session.bucket_ratelimiters.real_hashes_to_buckets)}\n" - f"actively limited routes: {actively_limited_routes}\n" - f"actively limited calls: {actively_limited_calls}" - ) - - embed = hikari.Embed( - author=hikari.EmbedAuthor(name=hikari.__copyright__), - url=hikari.__url__, - title=f"Hikari {hikari.__version__} debugging test client", - footer=hikari.EmbedFooter(text=hikari.__license__), - description=f"Uptime: {since(startup_time)}", - fields=[ - hikari.EmbedField(name="REST", value=rest_info, is_inline=False), - hikari.EmbedField(name="Gateway Manager", value=gw_info, is_inline=False), - *shard_infos[:3], - ], - thumbnail=hikari.EmbedThumbnail(url=bot_avatar_url), - color=hikari.Color.of("#F660AB"), - ) - - content = ( - "Pong!\n" - "\n" - f"Documentation: <{hikari.__docs__}>\n" - f"Repository: <{hikari.__url__}>\n" - f"PyPI: \n" - ) - - await client.rest.update_message(message, message.channel_id, content=content, embed=embed) - - client.run() - - -if __name__ == "__main__": - main() # pylint:disable=no-value-for-parameter diff --git a/hikari/net/__init__.py b/hikari/components/__init__.py similarity index 59% rename from hikari/net/__init__.py rename to hikari/components/__init__.py index df0f56de89..221c299d0b 100644 --- a/hikari/net/__init__.py +++ b/hikari/components/__init__.py @@ -16,22 +16,9 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . -"""Network components for the Hikari Discord API. +"""The models API for interacting with Discord directly.""" -These components describe the low level parts of Hikari. No model classes exist -for these; the majority of communication is done via JSON arrays and objects. -""" from __future__ import annotations -from hikari.net import codes -from hikari.net import ratelimits -from hikari.net import rest -from hikari.net import routes -from hikari.net import shards -from hikari.net import user_agents -from hikari.net.codes import * -from hikari.net.rest import * -from hikari.net.shards import * - -__all__ = codes.__all__ + shards.__all__ + rest.__all__ +__all__ = [] diff --git a/hikari/clients/components.py b/hikari/components/application.py similarity index 67% rename from hikari/clients/components.py rename to hikari/components/application.py index e9df108afb..221ba144a9 100644 --- a/hikari/clients/components.py +++ b/hikari/components/application.py @@ -16,26 +16,28 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . -"""A library wide base for storing client components.""" +"""A library wide base for storing client application.""" from __future__ import annotations -__all__ = ["Components"] +__all__ = ["Application"] import typing import attr if typing.TYPE_CHECKING: - from hikari.clients import configs # pylint: disable=cyclic-import - from hikari.clients import rest # pylint: disable=cyclic-import - from hikari.clients import shards # pylint: disable=cyclic-import - from hikari.events import dispatchers, event_managers + from hikari import configs + from hikari.rest import client as rest_client + from hikari.gateway import client as gateway_client + + from . import dispatchers + from . import event_managers @attr.s(repr=False) -class Components: - """A base that defines placement for set of components used in the library.""" +class Application: + """A base that defines placement for set of application used in the library.""" config: configs.BotConfig = attr.attrib(default=None) """The config used for this bot.""" @@ -46,10 +48,10 @@ class Components: event_manager: event_managers.EventManager = attr.attrib(default=None) """The event manager for this bot.""" - rest: rest.RESTClient = attr.attrib(default=None) - """The REST HTTP client to use for this bot.""" + rest: rest_client.RESTClient = attr.attrib(default=None) + """The RESTSession HTTP client to use for this bot.""" - shards: typing.Mapping[int, shards.ShardClient] = attr.attrib(default=None) + shards: typing.Mapping[int, gateway_client.GatewayClient] = attr.attrib(default=None) """Shards registered to this bot. These will be created once the bot has started execution. diff --git a/hikari/clients/bot_base.py b/hikari/components/bot_base.py similarity index 84% rename from hikari/clients/bot_base.py rename to hikari/components/bot_base.py index 9d60879aa2..45653f8127 100644 --- a/hikari/clients/bot_base.py +++ b/hikari/components/bot_base.py @@ -34,37 +34,38 @@ import typing from hikari import _about -from hikari.clients import components as _components -from hikari.clients import configs -from hikari.clients import runnable -from hikari.clients import shard_states -from hikari.events import dispatchers -from hikari.events import event_managers -from hikari.events import other +from hikari import configs +from hikari.events import other as other_events from hikari.internal import conversions +from hikari.internal import helpers + +from . import application +from . import dispatchers +from . import event_managers +from . import runnable if typing.TYPE_CHECKING: - from hikari import gateway_entities - from hikari import guilds - from hikari import intents - from hikari.clients import rest as _rest - from hikari.clients import shards as _shards from hikari.events import base as event_base + from hikari.gateway import client as gateway_client from hikari.internal import more_typing + from hikari.models import gateway as gateway_models + from hikari.models import guilds + from hikari.models import intents + from hikari.rest import client as rest_client class BotBase( - _components.Components, runnable.RunnableClient, dispatchers.EventDispatcher, abc.ABC, + application.Application, runnable.RunnableClient, dispatchers.EventDispatcher, abc.ABC, ): """An abstract base class for a bot implementation. Parameters ---------- - config : hikari.clients.configs.BotConfig + config : hikari.app.configs.BotConfig The config object to use. **kwargs - Parameters to use to create a hikari.clients.configs.BotConfig from, - instead of passing a raw config object. + Parameters to use to create a `hikari.app.configs.BotConfig` + from, instead of passing a raw config object. Examples -------- @@ -84,10 +85,10 @@ def __init__(self, *, config: typing.Optional[configs.BotConfig] = None, **kwarg if not bool(config) ^ bool(kwargs): raise TypeError("You must only specify a config object OR kwargs.") - runnable.RunnableClient.__init__(self, logging.getLogger(f"hikari.{type(self).__qualname__}")) + runnable.RunnableClient.__init__(self, helpers.get_logger(self)) # noinspection PyArgumentList - _components.Components.__init__( + application.Application.__init__( self, config=None, event_dispatcher=None, event_manager=None, rest=None, shards={}, ) @@ -103,7 +104,7 @@ def heartbeat_latency(self) -> float: This will return a mean of all the heartbeat intervals for all shards with a valid heartbeat latency that are in the - `hikari.clients.shard_states.ShardState.READY` state. + `hikari.clients.shard_states.GatewayState.READY` state. If no shards are in this state, this will return `float("nan")` instead. @@ -192,7 +193,7 @@ async def start(self) -> None: self.logger.info("started %s shard(s) in approx %.2fs", len(self.shards), finish_time - start_time) if self.event_manager is not None: - await self.dispatch_event(other.StartedEvent()) + await self.dispatch_event(other_events.StartedEvent()) async def _calculate_shards(self): gateway_bot = await self.rest.fetch_gateway_bot() @@ -210,7 +211,7 @@ async def _calculate_shards(self): self.logger.info("will connect shards to %s", url) - shard_clients = {} + shard_clients: typing.Dict[int, gateway_client.GatewayClient] = {} for shard_id in shard_ids: shard = self._create_shard(self, shard_id, shard_count, url) shard_clients[shard_id] = shard @@ -228,12 +229,12 @@ async def close(self) -> None: self.logger.info("stopping %s shard(s)", len(self.shards)) start_time = time.perf_counter() try: - await self.dispatch_event(other.StoppingEvent()) + await self.dispatch_event(other_events.StoppingEvent()) await asyncio.gather(*(shard_obj.close() for shard_obj in self.shards.values())) finally: finish_time = time.perf_counter() self.logger.info("stopped %s shard(s) in approx %.2fs", len(self.shards), finish_time - start_time) - await self.dispatch_event(other.StoppedEvent()) + await self.dispatch_event(other_events.StoppedEvent()) finally: await self.rest.close() @@ -253,7 +254,7 @@ def wait_for( *, timeout: typing.Optional[float] = None, predicate: typing.Optional[dispatchers.PredicateT] = None, - ) -> more_typing.Future: + ) -> more_typing.Future[typing.Any]: return self.event_dispatcher.wait_for(event_type, timeout=timeout, predicate=predicate) def dispatch_event(self, event: event_base.HikariEvent) -> more_typing.Future[typing.Any]: @@ -263,7 +264,7 @@ async def update_presence( self, *, status: guilds.PresenceStatus = ..., - activity: typing.Optional[gateway_entities.Activity] = ..., + activity: typing.Optional[gateway_models.Activity] = ..., idle_since: typing.Optional[datetime.datetime] = ..., is_afk: bool = ..., ) -> None: @@ -298,21 +299,21 @@ async def update_presence( *( s.update_presence(status=status, activity=activity, idle_since=idle_since, is_afk=is_afk) for s in self.shards.values() - if s.connection_state in (shard_states.ShardState.WAITING_FOR_READY, shard_states.ShardState.READY) + if s.connection_state in (status.ShardState.WAITING_FOR_READY, status.ShardState.READY) ) ) @staticmethod @abc.abstractmethod def _create_shard( - components: _components.Components, shard_id: int, shard_count: int, url: str - ) -> _shards.ShardClient: + app: application.Application, shard_id: int, shard_count: int, url: str + ) -> gateway_client.GatewayClient: """Return a new shard for the given parameters. Parameters ---------- - components : hikari.clients.components.Components - The components to register. + app : hikari.components.application.Application + The app to register. shard_id : int The shard ID to use. shard_count : int @@ -320,44 +321,44 @@ def _create_shard( url : str The gateway URL to connect to. - Returns - ------- - hikari.clients.shards.ShardClient - The shard client implementation to use for the given shard ID. - !!! note The `shard_id` and `shard_count` may be set within the `config` object passed, but any conforming implementations are expected to use the value passed in the `shard_id` and `shard_count` parameters regardless. Failure to do so may result in an invalid sharding configuration being used. + + Returns + ------- + hikari.clients.client.GatewayClient + The shard client implementation to use for the given shard ID. """ @staticmethod @abc.abstractmethod - def _create_rest(components: _components.Components) -> _rest.RESTClient: - """Return a new REST client from the given configuration. + def _create_rest(app: application.Application) -> rest_client.RESTClient: + """Return a new RESTSession client from the given configuration. Parameters ---------- - components : hikari.clients.components.Components - The components to register. + app : hikari.components.application.Application + The application to register. Returns ------- hikari.clients.rest.RESTClient - The REST client to use. + The RESTSession client to use. """ @staticmethod @abc.abstractmethod - def _create_event_manager(components: _components.Components) -> event_managers.EventManager: + def _create_event_manager(app: application.Application) -> event_managers.EventManager: """Return a new instance of an event manager implementation. Parameters ---------- - components : hikari.clients.components.Components - The components to register. + app : hikari.components.application.Application + The application to register. Returns ------- @@ -372,7 +373,7 @@ def _create_event_dispatcher(config: configs.BotConfig) -> dispatchers.EventDisp Parameters ---------- - config : hikari.clients.configs.BotConfig + config : hikari.app.configs.BotConfig The bot config to use. Returns diff --git a/hikari/events/consumers.py b/hikari/components/consumers.py similarity index 89% rename from hikari/events/consumers.py rename to hikari/components/consumers.py index e73d1f03a5..0c154e7f80 100644 --- a/hikari/events/consumers.py +++ b/hikari/components/consumers.py @@ -29,7 +29,7 @@ import typing if typing.TYPE_CHECKING: - from hikari.clients import shards + from hikari.gateway import client as gateway_client class RawEventConsumer(abc.ABC): @@ -45,13 +45,13 @@ class RawEventConsumer(abc.ABC): @abc.abstractmethod async def process_raw_event( - self, shard_client_obj: shards.ShardClient, name: str, payload: typing.Mapping[str, str], + self, shard_client_obj: gateway_client.GatewayClient, name: str, payload: typing.Mapping[str, str], ) -> None: """Consume a raw event that was received from a shard connection. Parameters ---------- - shard_client_obj : hikari.clients.shards.ShardClient + shard_client_obj : hikari.clients.client.GatewayClient The client for the shard that received the event. name : str The raw event name. diff --git a/hikari/events/dispatchers.py b/hikari/components/dispatchers.py similarity index 94% rename from hikari/events/dispatchers.py rename to hikari/components/dispatchers.py index 1571fdb0cb..80e994b218 100644 --- a/hikari/events/dispatchers.py +++ b/hikari/components/dispatchers.py @@ -28,10 +28,10 @@ from hikari.internal import conversions if typing.TYPE_CHECKING: - from hikari.events import bases + from hikari.events import base as event_base from hikari.internal import more_typing - EventT = typing.TypeVar("EventT", bound=bases.HikariEvent) + EventT = typing.TypeVar("EventT", bound=event_base.HikariEvent) PredicateT = typing.Callable[[EventT], typing.Union[more_typing.Coroutine[bool], bool]] EventCallbackT = typing.Callable[[EventT], typing.Union[more_typing.Coroutine[typing.Any], typing.Any]] @@ -68,7 +68,8 @@ def add_listener(self, event_type: typing.Type[EventT], callback: EventCallbackT def remove_listener(self, event_type: typing.Type[EventT], callback: EventCallbackT) -> EventCallbackT: """Remove the given function from the handlers for the given event. - The name is mandatory to enable supportinng registering the same event callback for multiple event types. + The name is mandatory to enable supporting registering the same event + callback for multiple event types. Parameters ---------- @@ -114,10 +115,6 @@ def wait_for( If the predicate throws an exception, or the timeout is reached, then this will be set as an exception on the returned future. - - !!! note - The event type is not expected to be considered in a polymorphic - lookup, but can be implemented this way optionally if documented. """ def event( @@ -125,16 +122,6 @@ def event( ) -> typing.Callable[[EventCallbackT], EventCallbackT]: """Return a decorator equivalent to invoking `EventDispatcher.add_listener`. - Parameters - ---------- - event_type : typing.Type[hikari.events.base.HikariEvent], optional - The event type to register the produced decorator to. If this is not - specified, then the given function is used instead and the type hint - of the first argument is considered. If no type hint is present - there either, then the call will fail. - - Usage - ----- This can be used in two ways. The first is to pass the event type to the decorator directly. @@ -173,6 +160,14 @@ async def on_event(event): async def on_create_delete(event): print(event) + Parameters + ---------- + event_type : typing.Type[hikari.events.base.HikariEvent], optional + The event type to register the produced decorator to. If this is not + specified, then the given function is used instead and the type hint + of the first argument is considered. If no type hint is present + there either, then the call will fail. + Returns ------- decorator(T) -> T @@ -208,7 +203,7 @@ def decorator(callback: EventCallbackT) -> EventCallbackT: # Do not add an annotation here, it will mess with type hints in PyCharm which can lead to # confusing telepathy comments to the user. @abc.abstractmethod - def dispatch_event(self, event: bases.HikariEvent) -> more_typing.Future[typing.Any]: + def dispatch_event(self, event: event_base.HikariEvent) -> more_typing.Future[typing.Any]: """Dispatch a given event to any listeners and waiters. Parameters diff --git a/hikari/events/event_managers.py b/hikari/components/event_managers.py similarity index 85% rename from hikari/events/event_managers.py rename to hikari/components/event_managers.py index 01954967c0..aa4d4ee36e 100644 --- a/hikari/events/event_managers.py +++ b/hikari/components/event_managers.py @@ -26,14 +26,15 @@ import logging import typing -from hikari.events import consumers -from hikari.events import dispatchers +from . import consumers +from . import dispatchers if typing.TYPE_CHECKING: - from hikari.clients import components as _components - from hikari.clients import shards + from hikari.gateway import client as gateway_client from hikari.internal import more_typing + from . import application + EVENT_MARKER_ATTR: typing.Final[str] = "___event_name___" EventConsumerT = typing.Callable[[str, typing.Mapping[str, str]], typing.Awaitable[None]] @@ -52,17 +53,16 @@ def raw_event_mapper(name: str) -> typing.Callable[[EventConsumerT], EventConsum ------- decorator(T) -> T A decorator for a method. - """ - def decorator(callable: EventConsumerT) -> EventConsumerT: - if not inspect.iscoroutinefunction(callable): + def decorator(callable_: EventConsumerT) -> EventConsumerT: + if not inspect.iscoroutinefunction(callable_): raise ValueError("Annotated element must be a coroutine function") - event_set = getattr(callable, EVENT_MARKER_ATTR, set()) + event_set = getattr(callable_, EVENT_MARKER_ATTR, set()) event_set.add(name) - setattr(callable, EVENT_MARKER_ATTR, event_set) - return callable + setattr(callable_, EVENT_MARKER_ATTR, event_set) + return callable_ return decorator @@ -79,7 +79,7 @@ def _get_event_marker(obj: typing.Any) -> typing.Set[str]: class EventManager(typing.Generic[EventDispatcherT], consumers.RawEventConsumer): - """Abstract definition of the components for an event system for a bot. + """Abstract definition of the application for an event system for a bot. The class itself inherits from `hikari.state.consumers.RawEventConsumer` (which allows it to provide the @@ -90,20 +90,13 @@ class EventManager(typing.Generic[EventDispatcherT], consumers.RawEventConsumer) dispatch them to a given event dispatcher. It does not provide the logic for how to specifically parse each event however. - Parameters - ---------- - components: hikari.clients.components.Components - The client components that this event manager should be bound to. - Includes the event dispatcher that will store individual events and - manage dispatching them after this object creates them. - - !!! note + !!! note This object will detect internal event mapper functions by looking for coroutine functions wrapped with `raw_event_mapper`. These methods are expected to have the following parameters: - * shard_obj : `hikari.clients.shards.ShardClient` + * shard_obj : `hikari.clients.client.GatewayClient` The shard client that emitted the event. @@ -127,7 +120,7 @@ def _process_message_create(self, shard, payload) -> MessageCreateEvent: Writing to a message queue is pretty simple using this mechanism, as you can choose when and how to place the event on a queue to be consumed by - other application components. + other application application. For the sake of simplicity, Hikari only provides implementations for single process bots, since most of what you will need will be fairly @@ -141,9 +134,16 @@ def _process_message_create(self, shard, payload) -> MessageCreateEvent: To provide this, use one of the provided implementations of this class, or create your own as needed. + + Parameters + ---------- + components: hikari.clients.application.Application + The client application that this event manager should be bound to. + Includes the event dispatcher that will store individual events and + manage dispatching them after this object creates them. """ - def __init__(self, components: _components.Components) -> None: + def __init__(self, components: application.Application) -> None: self.logger = logging.getLogger(type(self).__qualname__) self._components = components self.raw_event_mappers = {} @@ -155,7 +155,7 @@ def __init__(self, components: _components.Components) -> None: self.raw_event_mappers[event_name] = member async def process_raw_event( - self, shard_client_obj: shards.ShardClient, name: str, payload: more_typing.JSONObject, + self, shard_client_obj: gateway_client.GatewayClient, name: str, payload: more_typing.JSONObject, ) -> None: """Process a low level event. @@ -164,7 +164,7 @@ async def process_raw_event( Parameters ---------- - shard_client_obj : hikari.clients.shards.ShardClient + shard_client_obj : hikari.gateway.client.GatewayClient The shard that triggered this event. name : str The raw event name. diff --git a/hikari/events/intent_aware_dispatchers.py b/hikari/components/intent_aware_dispatchers.py similarity index 94% rename from hikari/events/intent_aware_dispatchers.py rename to hikari/components/intent_aware_dispatchers.py index 0a7d930a3d..99e9180e99 100644 --- a/hikari/events/intent_aware_dispatchers.py +++ b/hikari/components/intent_aware_dispatchers.py @@ -27,13 +27,14 @@ import typing import warnings -from hikari import errors -from hikari import intents -from hikari.events import base -from hikari.events import dispatchers -from hikari.events import other +from hikari.events import base as event_base +from hikari.events import other as other_events from hikari.internal import more_asyncio from hikari.internal import more_collections +from .. import errors +from hikari.models import intents + +from . import dispatchers if typing.TYPE_CHECKING: from hikari.internal import more_typing @@ -81,13 +82,13 @@ def close(self) -> None: self._waiters.clear() def add_listener( - self, event_type: typing.Type[base.HikariEvent], callback: dispatchers.EventCallbackT, **kwargs + self, event_type: typing.Type[event_base.HikariEvent], callback: dispatchers.EventCallbackT, **kwargs ) -> dispatchers.EventCallbackT: """Register a new event callback to a given event name. Parameters ---------- - event_type : typing.Type[hikari.events.HikariEvent] + event_type : typing.Type[hikari.events.base.HikariEvent] The event to register to. callback : `async def callback(event: HikariEvent) -> ...` The event callback to invoke when this event is fired; this can be @@ -95,7 +96,7 @@ def add_listener( Returns ------- - async def callback(event: HikariEvent) -> ... + async def callback(event: hikari.events.base.HikariEvent) -> ... The callback that was registered. Note @@ -103,10 +104,10 @@ async def callback(event: HikariEvent) -> ... If you subscribe to an event that requires intents that you do not have set, you will receive a warning. """ - if not issubclass(event_type, base.HikariEvent): + if not issubclass(event_type, event_base.HikariEvent): raise TypeError("Events must subclass hikari.events.HikariEvent") - required_intents = base.get_required_intents_for(event_type) + required_intents = event_base.get_required_intents_for(event_type) enabled_intents = self._enabled_intents if self._enabled_intents is not None else 0 any_intent_match = any(enabled_intents & i == i for i in required_intents) @@ -157,7 +158,7 @@ def remove_listener( # Do not add an annotation here, it will mess with type hints in PyCharm which can lead to # confusing telepathy comments to the user. # Additionally, this MUST NOT BE A COROUTINE ITSELF. THIS IS NOT TYPESAFE! - def dispatch_event(self, event: base.HikariEvent) -> more_typing.Future[typing.Any]: + def dispatch_event(self, event: event_base.HikariEvent) -> more_typing.Future[typing.Any]: """Dispatch a given event to all listeners and waiters that are applicable. Parameters @@ -254,7 +255,7 @@ async def _failsafe_invoke(self, event, callback) -> None: def handle_exception( self, exception: Exception, - event: base.HikariEvent, + event: event_base.HikariEvent, callback: typing.Callable[..., typing.Union[typing.Awaitable[None]]], ) -> None: """Handle raised exception. @@ -279,11 +280,11 @@ def handle_exception( callback, or a `wait_for` predicate that threw an exception. """ # Do not recurse if a dodgy exception handler is added. - if not base.is_no_catch_event(event): + if not event_base.is_no_catch_event(event): self.logger.exception( 'Exception occurred in handler for event "%s"', type(event).__name__, exc_info=exception ) - self.dispatch_event(other.ExceptionEvent(exception=exception, event=event, callback=callback)) + self.dispatch_event(other_events.ExceptionEvent(exception=exception, event=event, callback=callback)) else: self.logger.exception( 'Exception occurred in handler for event "%s", and the exception has been dropped', diff --git a/hikari/clients/runnable.py b/hikari/components/runnable.py similarity index 100% rename from hikari/clients/runnable.py rename to hikari/components/runnable.py diff --git a/hikari/clients/configs.py b/hikari/configs.py similarity index 97% rename from hikari/clients/configs.py rename to hikari/configs.py index 3e26df60be..a9b4b984fd 100644 --- a/hikari/clients/configs.py +++ b/hikari/configs.py @@ -37,12 +37,12 @@ import aiohttp import attr -from hikari import gateway_entities -from hikari import guilds -from hikari import intents as _intents from hikari.internal import conversions from hikari.internal import marshaller from hikari.internal import urls +from hikari.models import gateway as gateway_models +from hikari.models import guilds +from hikari.models import intents as intents_ if typing.TYPE_CHECKING: import ssl @@ -71,7 +71,7 @@ class DebugConfig(BaseConfig): @marshaller.marshallable() @attr.s(kw_only=True, repr=False) class AIOHTTPConfig(BaseConfig): - """Config for components that use AIOHTTP somewhere. + """Config for application that use AIOHTTP somewhere. Attributes ---------- @@ -196,8 +196,8 @@ def _initial_status_default() -> typing.Literal[guilds.PresenceStatus.ONLINE]: return guilds.PresenceStatus.ONLINE -def _deserialize_intents(value) -> _intents.Intent: - return conversions.dereference_int_flag(_intents.Intent, value) +def _deserialize_intents(value) -> intents_.Intent: + return conversions.dereference_int_flag(intents_.Intent, value) def _large_threshold_default() -> int: @@ -322,8 +322,8 @@ class GatewayConfig(AIOHTTPConfig, DebugConfig, TokenConfig): gateway_version: int = marshaller.attrib(deserializer=int, if_undefined=_gateway_version_default, default=6) - initial_activity: typing.Optional[gateway_entities.Activity] = marshaller.attrib( - deserializer=gateway_entities.Activity.deserialize, if_none=None, if_undefined=None, default=None + initial_activity: typing.Optional[gateway_models.Activity] = marshaller.attrib( + deserializer=gateway_models.Activity.deserialize, if_none=None, if_undefined=None, default=None ) initial_status: guilds.PresenceStatus = marshaller.attrib( @@ -336,7 +336,7 @@ class GatewayConfig(AIOHTTPConfig, DebugConfig, TokenConfig): deserializer=datetime.datetime.fromtimestamp, if_none=None, if_undefined=None, default=None ) - intents: typing.Optional[_intents.Intent] = marshaller.attrib( + intents: typing.Optional[intents_.Intent] = marshaller.attrib( deserializer=_deserialize_intents, if_undefined=None, default=None, ) diff --git a/hikari/errors.py b/hikari/errors.py index 81a997a23d..5ca1db9d94 100644 --- a/hikari/errors.py +++ b/hikari/errors.py @@ -47,7 +47,7 @@ import aiohttp.typedefs -from hikari.net import codes +from hikari.internal import codes class HikariError(RuntimeError): diff --git a/hikari/events/__init__.py b/hikari/events/__init__.py index cbd6006e6a..0a74928018 100644 --- a/hikari/events/__init__.py +++ b/hikari/events/__init__.py @@ -16,28 +16,15 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . -"""Components and entities that are used to describe Discord gateway events.""" +"""Application and entities that are used to describe Discord gateway events.""" from __future__ import annotations -from hikari.events import base -from hikari.events import channel -from hikari.events import guild -from hikari.events import message -from hikari.events import other -from hikari.events import voice -from hikari.events.base import * -from hikari.events.channel import * -from hikari.events.guild import * -from hikari.events.message import * -from hikari.events.other import * -from hikari.events.voice import * +from .base import * +from .channel import * +from .guild import * +from .message import * +from .other import * +from .voice import * -__all__ = [ - *base.__all__, - *channel.__all__, - *guild.__all__, - *message.__all__, - *other.__all__, - *voice.__all__, -] +__all__ = base.__all__ + channel.__all__ + guild.__all__ + message.__all__ + other.__all__ + voice.__all__ diff --git a/hikari/events/base.py b/hikari/events/base.py index d973ea35c9..160c20b20b 100644 --- a/hikari/events/base.py +++ b/hikari/events/base.py @@ -16,7 +16,7 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . -"""Bases for components and entities that are used to describe Discord gateway events.""" +"""Bases for application and entities that are used to describe Discord gateway events.""" from __future__ import annotations @@ -27,18 +27,18 @@ import attr -from hikari import bases from hikari.internal import marshaller from hikari.internal import more_collections +from hikari.models import bases as base_models if typing.TYPE_CHECKING: - from hikari import intents + from hikari.models import intents # Base event, is not deserialized @marshaller.marshallable() @attr.s(eq=False, hash=False, kw_only=True, slots=True) -class HikariEvent(bases.Entity, abc.ABC): +class HikariEvent(base_models.Entity, abc.ABC): """The base class that all events inherit from.""" diff --git a/hikari/events/channel.py b/hikari/events/channel.py index 16c7e24ff1..2b33c14f65 100644 --- a/hikari/events/channel.py +++ b/hikari/events/channel.py @@ -16,7 +16,7 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . -"""Components and entities that are used to describe Discord gateway channel events.""" +"""Application and entities that are used to describe Discord gateway channel events.""" from __future__ import annotations @@ -38,15 +38,16 @@ import attr -from hikari import bases as base_entities -from hikari import channels -from hikari import guilds -from hikari import intents -from hikari import invites -from hikari import users -from hikari.events import base as base_events from hikari.internal import conversions from hikari.internal import marshaller +from hikari.models import bases as base_models +from hikari.models import channels +from hikari.models import guilds +from hikari.models import intents +from hikari.models import invites +from hikari.models import users + +from . import base as base_events if typing.TYPE_CHECKING: from hikari.internal import more_typing @@ -54,9 +55,9 @@ def _overwrite_deserializer( payload: more_typing.JSONArray, **kwargs: typing.Any -) -> typing.Mapping[base_entities.Snowflake, channels.PermissionOverwrite]: +) -> typing.Mapping[base_models.Snowflake, channels.PermissionOverwrite]: return { - base_entities.Snowflake(overwrite["id"]): channels.PermissionOverwrite.deserialize(overwrite, **kwargs) + base_models.Snowflake(overwrite["id"]): channels.PermissionOverwrite.deserialize(overwrite, **kwargs) for overwrite in payload } @@ -67,21 +68,21 @@ def _rate_limit_per_user_deserializer(seconds: int) -> datetime.timedelta: def _recipients_deserializer( payload: more_typing.JSONArray, **kwargs: typing.Any -) -> typing.Mapping[base_entities.Snowflake, users.User]: - return {base_entities.Snowflake(user["id"]): users.User.deserialize(user, **kwargs) for user in payload} +) -> typing.Mapping[base_models.Snowflake, users.User]: + return {base_models.Snowflake(user["id"]): users.User.deserialize(user, **kwargs) for user in payload} @base_events.requires_intents(intents.Intent.GUILDS) @marshaller.marshallable() @attr.s(eq=False, hash=False, kw_only=True, slots=True) -class BaseChannelEvent(base_events.HikariEvent, base_entities.Unique, marshaller.Deserializable, abc.ABC): +class BaseChannelEvent(base_events.HikariEvent, base_models.Unique, marshaller.Deserializable, abc.ABC): """A base object that Channel events will inherit from.""" type: channels.ChannelType = marshaller.attrib(deserializer=channels.ChannelType, repr=True) """The channel's type.""" - guild_id: typing.Optional[base_entities.Snowflake] = marshaller.attrib( - deserializer=base_entities.Snowflake, if_undefined=None, default=None, repr=True + guild_id: typing.Optional[base_models.Snowflake] = marshaller.attrib( + deserializer=base_models.Snowflake, if_undefined=None, default=None, repr=True ) """The ID of the guild this channel is in, will be `None` for DMs.""" @@ -92,7 +93,7 @@ class BaseChannelEvent(base_events.HikariEvent, base_entities.Unique, marshaller """ permission_overwrites: typing.Optional[ - typing.Mapping[base_entities.Snowflake, channels.PermissionOverwrite] + typing.Mapping[base_models.Snowflake, channels.PermissionOverwrite] ] = marshaller.attrib(deserializer=_overwrite_deserializer, if_undefined=None, default=None, inherit_kwargs=True) """An mapping of the set permission overwrites for this channel, if applicable.""" @@ -107,8 +108,8 @@ class BaseChannelEvent(base_events.HikariEvent, base_entities.Unique, marshaller ) """Whether this channel is nsfw, will be `None` if not applicable.""" - last_message_id: typing.Optional[base_entities.Snowflake] = marshaller.attrib( - deserializer=base_entities.Snowflake, if_none=None, if_undefined=None, default=None + last_message_id: typing.Optional[base_models.Snowflake] = marshaller.attrib( + deserializer=base_models.Snowflake, if_none=None, if_undefined=None, default=None ) """The ID of the last message sent, if it's a text type channel.""" @@ -126,7 +127,7 @@ class BaseChannelEvent(base_events.HikariEvent, base_entities.Unique, marshaller This is only applicable to a guild text like channel. """ - recipients: typing.Optional[typing.Mapping[base_entities.Snowflake, users.User]] = marshaller.attrib( + recipients: typing.Optional[typing.Mapping[base_models.Snowflake, users.User]] = marshaller.attrib( deserializer=_recipients_deserializer, if_undefined=None, default=None, inherit_kwargs=True, ) """A mapping of this channel's recipient users, if it's a DM or group DM.""" @@ -136,21 +137,21 @@ class BaseChannelEvent(base_events.HikariEvent, base_entities.Unique, marshaller ) """The hash of this channel's icon, if it's a group DM channel and is set.""" - owner_id: typing.Optional[base_entities.Snowflake] = marshaller.attrib( - deserializer=base_entities.Snowflake, if_undefined=None, default=None + owner_id: typing.Optional[base_models.Snowflake] = marshaller.attrib( + deserializer=base_models.Snowflake, if_undefined=None, default=None ) """The ID of this channel's creator, if it's a DM channel.""" - application_id: typing.Optional[base_entities.Snowflake] = marshaller.attrib( - deserializer=base_entities.Snowflake, if_undefined=None, default=None + application_id: typing.Optional[base_models.Snowflake] = marshaller.attrib( + deserializer=base_models.Snowflake, if_undefined=None, default=None ) """The ID of the application that created the group DM. This is only applicable to bot based group DMs. """ - parent_id: typing.Optional[base_entities.Snowflake] = marshaller.attrib( - deserializer=base_entities.Snowflake, if_undefined=None, if_none=None, default=None + parent_id: typing.Optional[base_models.Snowflake] = marshaller.attrib( + deserializer=base_models.Snowflake, if_undefined=None, if_none=None, default=None ) """The ID of this channels's parent category within guild, if set.""" @@ -195,15 +196,15 @@ class ChannelPinsUpdateEvent(base_events.HikariEvent, marshaller.Deserializable) when a pinned message is deleted. """ - guild_id: typing.Optional[base_entities.Snowflake] = marshaller.attrib( - deserializer=base_entities.Snowflake, if_undefined=None, default=None, repr=True + guild_id: typing.Optional[base_models.Snowflake] = marshaller.attrib( + deserializer=base_models.Snowflake, if_undefined=None, default=None, repr=True ) """The ID of the guild where this event happened. Will be `None` if this happened in a DM channel. """ - channel_id: base_entities.Snowflake = marshaller.attrib(deserializer=base_entities.Snowflake, repr=True) + channel_id: base_models.Snowflake = marshaller.attrib(deserializer=base_models.Snowflake, repr=True) """The ID of the channel where the message was pinned or unpinned.""" last_pin_timestamp: typing.Optional[datetime.datetime] = marshaller.attrib( @@ -224,15 +225,15 @@ class WebhookUpdateEvent(base_events.HikariEvent, marshaller.Deserializable): Sent when a webhook is updated, created or deleted in a guild. """ - guild_id: base_entities.Snowflake = marshaller.attrib(deserializer=base_entities.Snowflake, repr=True) + guild_id: base_models.Snowflake = marshaller.attrib(deserializer=base_models.Snowflake, repr=True) """The ID of the guild this webhook is being updated in.""" - channel_id: base_entities.Snowflake = marshaller.attrib(deserializer=base_entities.Snowflake, repr=True) + channel_id: base_models.Snowflake = marshaller.attrib(deserializer=base_models.Snowflake, repr=True) """The ID of the channel this webhook is being updated in.""" def _timestamp_deserializer(date: str) -> datetime.datetime: - return datetime.datetime.fromtimestamp(date, datetime.timezone.utc) + return datetime.datetime.fromtimestamp(float(date), datetime.timezone.utc) @base_events.requires_intents(intents.Intent.GUILD_MESSAGE_TYPING, intents.Intent.DIRECT_MESSAGE_TYPING) @@ -244,18 +245,18 @@ class TypingStartEvent(base_events.HikariEvent, marshaller.Deserializable): Received when a user or bot starts "typing" in a channel. """ - channel_id: base_entities.Snowflake = marshaller.attrib(deserializer=base_entities.Snowflake, repr=True) + channel_id: base_models.Snowflake = marshaller.attrib(deserializer=base_models.Snowflake, repr=True) """The ID of the channel this typing event is occurring in.""" - guild_id: typing.Optional[base_entities.Snowflake] = marshaller.attrib( - deserializer=base_entities.Snowflake, if_undefined=None, default=None, repr=True + guild_id: typing.Optional[base_models.Snowflake] = marshaller.attrib( + deserializer=base_models.Snowflake, if_undefined=None, default=None, repr=True ) """The ID of the guild this typing event is occurring in. Will be `None` if this event is happening in a DM channel. """ - user_id: base_entities.Snowflake = marshaller.attrib(deserializer=base_entities.Snowflake, repr=True) + user_id: base_models.Snowflake = marshaller.attrib(deserializer=base_models.Snowflake, repr=True) """The ID of the user who triggered this typing event.""" timestamp: datetime.datetime = marshaller.attrib(deserializer=_timestamp_deserializer) @@ -284,7 +285,7 @@ def _max_uses_deserializer(count: int) -> typing.Union[int, float]: class InviteCreateEvent(base_events.HikariEvent, marshaller.Deserializable): """Represents a gateway Invite Create event.""" - channel_id: base_entities.Snowflake = marshaller.attrib(deserializer=base_entities.Snowflake, repr=True) + channel_id: base_models.Snowflake = marshaller.attrib(deserializer=base_models.Snowflake, repr=True) """The ID of the channel this invite targets.""" code: str = marshaller.attrib(deserializer=str, repr=True) @@ -293,8 +294,8 @@ class InviteCreateEvent(base_events.HikariEvent, marshaller.Deserializable): created_at: datetime.datetime = marshaller.attrib(deserializer=conversions.parse_iso_8601_ts) """The datetime of when this invite was created.""" - guild_id: typing.Optional[base_entities.Snowflake] = marshaller.attrib( - deserializer=base_entities.Snowflake, if_undefined=None, default=None, repr=True + guild_id: typing.Optional[base_models.Snowflake] = marshaller.attrib( + deserializer=base_models.Snowflake, if_undefined=None, default=None, repr=True ) """The ID of the guild this invite was created in, if applicable. @@ -344,15 +345,15 @@ class InviteDeleteEvent(base_events.HikariEvent, marshaller.Deserializable): Sent when an invite is deleted for a channel we can access. """ - channel_id: base_entities.Snowflake = marshaller.attrib(deserializer=base_entities.Snowflake, repr=True) + channel_id: base_models.Snowflake = marshaller.attrib(deserializer=base_models.Snowflake, repr=True) """The ID of the channel this ID was attached to.""" # TODO: move common fields with InviteCreateEvent into base class. code: str = marshaller.attrib(deserializer=str, repr=True) """The code of this invite.""" - guild_id: typing.Optional[base_entities.Snowflake] = marshaller.attrib( - deserializer=base_entities.Snowflake, if_undefined=None, default=None, repr=True + guild_id: typing.Optional[base_models.Snowflake] = marshaller.attrib( + deserializer=base_models.Snowflake, if_undefined=None, default=None, repr=True ) """The ID of the guild this invite was deleted in. diff --git a/hikari/events/guild.py b/hikari/events/guild.py index 170fe7992b..24859c4cfc 100644 --- a/hikari/events/guild.py +++ b/hikari/events/guild.py @@ -16,7 +16,7 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . -"""Components and entities that are used to describe Discord gateway guild events.""" +"""Application and entities that are used to describe Discord gateway guild events.""" from __future__ import annotations @@ -44,15 +44,16 @@ import attr -from hikari import bases as base_entities -from hikari import emojis as _emojis -from hikari import guilds -from hikari import intents -from hikari import unset -from hikari import users -from hikari.events import base as base_events from hikari.internal import conversions from hikari.internal import marshaller +from hikari.models import bases as base_models +from hikari.models import emojis as emojis_models +from hikari.models import guilds +from hikari.models import intents +from hikari.models import unset +from hikari.models import users + +from . import base as base_events if typing.TYPE_CHECKING: import datetime @@ -81,7 +82,7 @@ class GuildUpdateEvent(base_events.HikariEvent, guilds.Guild): @base_events.requires_intents(intents.Intent.GUILDS) @marshaller.marshallable() @attr.s(eq=False, hash=False, kw_only=True, slots=True) -class GuildLeaveEvent(base_events.HikariEvent, base_entities.Unique, marshaller.Deserializable): +class GuildLeaveEvent(base_events.HikariEvent, base_models.Unique, marshaller.Deserializable): """Fired when the current user leaves the guild or is kicked/banned from it. !!! note @@ -92,7 +93,7 @@ class GuildLeaveEvent(base_events.HikariEvent, base_entities.Unique, marshaller. @base_events.requires_intents(intents.Intent.GUILDS) @marshaller.marshallable() @attr.s(eq=False, hash=False, kw_only=True, slots=True) -class GuildUnavailableEvent(base_events.HikariEvent, base_entities.Unique, marshaller.Deserializable): +class GuildUnavailableEvent(base_events.HikariEvent, base_models.Unique, marshaller.Deserializable): """Fired when a guild becomes temporarily unavailable due to an outage. !!! note @@ -106,7 +107,7 @@ class GuildUnavailableEvent(base_events.HikariEvent, base_entities.Unique, marsh class BaseGuildBanEvent(base_events.HikariEvent, marshaller.Deserializable, abc.ABC): """A base object that guild ban events will inherit from.""" - guild_id: base_entities.Snowflake = marshaller.attrib(deserializer=base_entities.Snowflake, repr=True) + guild_id: base_models.Snowflake = marshaller.attrib(deserializer=base_models.Snowflake, repr=True) """The ID of the guild this ban is in.""" user: users.User = marshaller.attrib(deserializer=users.User.deserialize, inherit_kwargs=True, repr=True) @@ -129,9 +130,10 @@ class GuildBanRemoveEvent(BaseGuildBanEvent): def _deserialize_emojis( payload: more_typing.JSONArray, **kwargs: typing.Any -) -> typing.Mapping[base_entities.Snowflake, _emojis.KnownCustomEmoji]: +) -> typing.Mapping[base_models.Snowflake, emojis_models.KnownCustomEmoji]: return { - base_entities.Snowflake(emoji["id"]): _emojis.KnownCustomEmoji.deserialize(emoji, **kwargs) for emoji in payload + base_models.Snowflake(emoji["id"]): emojis_models.KnownCustomEmoji.deserialize(emoji, **kwargs) + for emoji in payload } @@ -141,10 +143,10 @@ def _deserialize_emojis( class GuildEmojisUpdateEvent(base_events.HikariEvent, marshaller.Deserializable): """Represents a Guild Emoji Update gateway event.""" - guild_id: base_entities.Snowflake = marshaller.attrib(deserializer=base_entities.Snowflake) + guild_id: base_models.Snowflake = marshaller.attrib(deserializer=base_models.Snowflake) """The ID of the guild this emoji was updated in.""" - emojis: typing.Mapping[base_entities.Snowflake, _emojis.KnownCustomEmoji] = marshaller.attrib( + emojis: typing.Mapping[base_models.Snowflake, emojis_models.KnownCustomEmoji] = marshaller.attrib( deserializer=_deserialize_emojis, inherit_kwargs=True, repr=True ) """The updated mapping of emojis by their ID.""" @@ -156,7 +158,7 @@ class GuildEmojisUpdateEvent(base_events.HikariEvent, marshaller.Deserializable) class GuildIntegrationsUpdateEvent(base_events.HikariEvent, marshaller.Deserializable): """Used to represent Guild Integration Update gateway events.""" - guild_id: base_entities.Snowflake = marshaller.attrib(deserializer=base_entities.Snowflake, repr=True) + guild_id: base_models.Snowflake = marshaller.attrib(deserializer=base_models.Snowflake, repr=True) """The ID of the guild the integration was updated in.""" @@ -166,12 +168,12 @@ class GuildIntegrationsUpdateEvent(base_events.HikariEvent, marshaller.Deseriali class GuildMemberAddEvent(base_events.HikariEvent, guilds.GuildMember): """Used to represent a Guild Member Add gateway event.""" - guild_id: base_entities.Snowflake = marshaller.attrib(deserializer=base_entities.Snowflake, repr=True) + guild_id: base_models.Snowflake = marshaller.attrib(deserializer=base_models.Snowflake, repr=True) """The ID of the guild where this member was added.""" -def _deserialize_role_ids(payload: more_typing.JSONArray) -> typing.Sequence[base_entities.Snowflake]: - return [base_entities.Snowflake(role_id) for role_id in payload] +def _deserialize_role_ids(payload: more_typing.JSONArray) -> typing.Sequence[base_models.Snowflake]: + return [base_models.Snowflake(role_id) for role_id in payload] @base_events.requires_intents(intents.Intent.GUILD_MEMBERS) @@ -183,10 +185,10 @@ class GuildMemberUpdateEvent(base_events.HikariEvent, marshaller.Deserializable) Sent when a guild member or their inner user object is updated. """ - guild_id: base_entities.Snowflake = marshaller.attrib(deserializer=base_entities.Snowflake, repr=True) + guild_id: base_models.Snowflake = marshaller.attrib(deserializer=base_models.Snowflake, repr=True) """The ID of the guild this member was updated in.""" - role_ids: typing.Sequence[base_entities.Snowflake] = marshaller.attrib( + role_ids: typing.Sequence[base_models.Snowflake] = marshaller.attrib( raw_name="roles", deserializer=_deserialize_role_ids, ) """A sequence of the IDs of the member's current roles.""" @@ -222,7 +224,7 @@ class GuildMemberRemoveEvent(base_events.HikariEvent, marshaller.Deserializable) """ # TODO: make GuildMember event into common base class. - guild_id: base_entities.Snowflake = marshaller.attrib(deserializer=base_entities.Snowflake, repr=True) + guild_id: base_models.Snowflake = marshaller.attrib(deserializer=base_models.Snowflake, repr=True) """The ID of the guild this user was removed from.""" user: users.User = marshaller.attrib(deserializer=users.User.deserialize, inherit_kwargs=True, repr=True) @@ -235,7 +237,7 @@ class GuildMemberRemoveEvent(base_events.HikariEvent, marshaller.Deserializable) class GuildRoleCreateEvent(base_events.HikariEvent, marshaller.Deserializable): """Used to represent a Guild Role Create gateway event.""" - guild_id: base_entities.Snowflake = marshaller.attrib(deserializer=base_entities.Snowflake, repr=True) + guild_id: base_models.Snowflake = marshaller.attrib(deserializer=base_models.Snowflake, repr=True) """The ID of the guild where this role was created.""" role: guilds.GuildRole = marshaller.attrib(deserializer=guilds.GuildRole.deserialize, inherit_kwargs=True) @@ -250,7 +252,7 @@ class GuildRoleUpdateEvent(base_events.HikariEvent, marshaller.Deserializable): # TODO: make any event with a guild ID into a custom base event. # https://pypi.org/project/stupid/ could this work around the multiple inheritance problem? - guild_id: base_entities.Snowflake = marshaller.attrib(deserializer=base_entities.Snowflake, repr=True) + guild_id: base_models.Snowflake = marshaller.attrib(deserializer=base_models.Snowflake, repr=True) """The ID of the guild where this role was updated.""" role: guilds.GuildRole = marshaller.attrib( @@ -265,10 +267,10 @@ class GuildRoleUpdateEvent(base_events.HikariEvent, marshaller.Deserializable): class GuildRoleDeleteEvent(base_events.HikariEvent, marshaller.Deserializable): """Represents a gateway Guild Role Delete Event.""" - guild_id: base_entities.Snowflake = marshaller.attrib(deserializer=base_entities.Snowflake, repr=True) + guild_id: base_models.Snowflake = marshaller.attrib(deserializer=base_models.Snowflake, repr=True) """The ID of the guild where this role is being deleted.""" - role_id: base_entities.Snowflake = marshaller.attrib(deserializer=base_entities.Snowflake, repr=True) + role_id: base_models.Snowflake = marshaller.attrib(deserializer=base_models.Snowflake, repr=True) """The ID of the role being deleted.""" diff --git a/hikari/events/message.py b/hikari/events/message.py index 13d82948bd..c2923dbec3 100644 --- a/hikari/events/message.py +++ b/hikari/events/message.py @@ -16,7 +16,7 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . -"""Components and entities that are used to describe Discord gateway message events.""" +"""Application and entities that are used to describe Discord gateway message events.""" from __future__ import annotations @@ -35,18 +35,19 @@ import attr -from hikari import applications -from hikari import bases as base_entities -from hikari import embeds as _embeds -from hikari import emojis -from hikari import guilds -from hikari import intents -from hikari import messages -from hikari import unset -from hikari import users -from hikari.events import base as base_events from hikari.internal import conversions from hikari.internal import marshaller +from hikari.models import applications +from hikari.models import bases as base_models +from hikari.models import embeds as embed_models +from hikari.models import emojis +from hikari.models import guilds +from hikari.models import intents +from hikari.models import messages +from hikari.models import unset +from hikari.models import users + +from . import base as base_events if typing.TYPE_CHECKING: import datetime @@ -61,12 +62,12 @@ class MessageCreateEvent(base_events.HikariEvent, messages.Message): """Used to represent Message Create gateway events.""" -def _deserialize_object_mentions(payload: more_typing.JSONArray) -> typing.Set[base_entities.Snowflake]: - return {base_entities.Snowflake(mention["id"]) for mention in payload} +def _deserialize_object_mentions(payload: more_typing.JSONArray) -> typing.Set[base_models.Snowflake]: + return {base_models.Snowflake(mention["id"]) for mention in payload} -def _deserialize_mentions(payload: more_typing.JSONArray) -> typing.Set[base_entities.Snowflake]: - return {base_entities.Snowflake(mention) for mention in payload} +def _deserialize_mentions(payload: more_typing.JSONArray) -> typing.Set[base_models.Snowflake]: + return {base_models.Snowflake(mention) for mention in payload} def _deserialize_attachments( @@ -75,8 +76,8 @@ def _deserialize_attachments( return [messages.Attachment.deserialize(attachment, **kwargs) for attachment in payload] -def _deserialize_embeds(payload: more_typing.JSONArray, **kwargs: typing.Any) -> typing.Sequence[_embeds.Embed]: - return [_embeds.Embed.deserialize(embed, **kwargs) for embed in payload] +def _deserialize_embeds(payload: more_typing.JSONArray, **kwargs: typing.Any) -> typing.Sequence[embed_models.Embed]: + return [embed_models.Embed.deserialize(embed, **kwargs) for embed in payload] def _deserialize_reaction(payload: more_typing.JSONArray, **kwargs: typing.Any) -> typing.Sequence[messages.Reaction]: @@ -87,7 +88,7 @@ def _deserialize_reaction(payload: more_typing.JSONArray, **kwargs: typing.Any) @base_events.requires_intents(intents.Intent.GUILD_MESSAGES, intents.Intent.DIRECT_MESSAGES) @marshaller.marshallable() @attr.s(eq=False, hash=False, kw_only=True, slots=True) -class MessageUpdateEvent(base_events.HikariEvent, base_entities.Unique, marshaller.Deserializable): +class MessageUpdateEvent(base_events.HikariEvent, base_models.Unique, marshaller.Deserializable): """Represents Message Update gateway events. !!! note @@ -97,11 +98,11 @@ class MessageUpdateEvent(base_events.HikariEvent, base_entities.Unique, marshall alongside field nullability. """ - channel_id: base_entities.Snowflake = marshaller.attrib(deserializer=base_entities.Snowflake, repr=True) + channel_id: base_models.Snowflake = marshaller.attrib(deserializer=base_models.Snowflake, repr=True) """The ID of the channel that the message was sent in.""" - guild_id: typing.Union[base_entities.Snowflake, unset.Unset] = marshaller.attrib( - deserializer=base_entities.Snowflake, if_undefined=unset.Unset, default=unset.UNSET, repr=True + guild_id: typing.Union[base_models.Snowflake, unset.Unset] = marshaller.attrib( + deserializer=base_models.Snowflake, if_undefined=unset.Unset, default=unset.UNSET, repr=True ) """The ID of the guild that the message was sent in.""" @@ -145,17 +146,17 @@ class MessageUpdateEvent(base_events.HikariEvent, base_entities.Unique, marshall ) """Whether the message mentions `@everyone` or `@here`.""" - user_mentions: typing.Union[typing.Set[base_entities.Snowflake], unset.Unset] = marshaller.attrib( + user_mentions: typing.Union[typing.Set[base_models.Snowflake], unset.Unset] = marshaller.attrib( raw_name="mentions", deserializer=_deserialize_object_mentions, if_undefined=unset.Unset, default=unset.UNSET, ) """The users the message mentions.""" - role_mentions: typing.Union[typing.Set[base_entities.Snowflake], unset.Unset] = marshaller.attrib( + role_mentions: typing.Union[typing.Set[base_models.Snowflake], unset.Unset] = marshaller.attrib( raw_name="mention_roles", deserializer=_deserialize_mentions, if_undefined=unset.Unset, default=unset.UNSET, ) """The roles the message mentions.""" - channel_mentions: typing.Union[typing.Set[base_entities.Snowflake], unset.Unset] = marshaller.attrib( + channel_mentions: typing.Union[typing.Set[base_models.Snowflake], unset.Unset] = marshaller.attrib( raw_name="mention_channels", deserializer=_deserialize_object_mentions, if_undefined=unset.Unset, @@ -168,7 +169,7 @@ class MessageUpdateEvent(base_events.HikariEvent, base_entities.Unique, marshall ) """The message attachments.""" - embeds: typing.Union[typing.Sequence[_embeds.Embed], unset.Unset] = marshaller.attrib( + embeds: typing.Union[typing.Sequence[embed_models.Embed], unset.Unset] = marshaller.attrib( deserializer=_deserialize_embeds, if_undefined=unset.Unset, default=unset.UNSET, inherit_kwargs=True, ) """The message's embeds.""" @@ -183,8 +184,8 @@ class MessageUpdateEvent(base_events.HikariEvent, base_entities.Unique, marshall ) """Whether the message is pinned.""" - webhook_id: typing.Union[base_entities.Snowflake, unset.Unset] = marshaller.attrib( - deserializer=base_entities.Snowflake, if_undefined=unset.Unset, default=unset.UNSET + webhook_id: typing.Union[base_models.Snowflake, unset.Unset] = marshaller.attrib( + deserializer=base_models.Snowflake, if_undefined=unset.Unset, default=unset.UNSET ) """If the message was generated by a webhook, the webhook's ID.""" @@ -242,25 +243,23 @@ class MessageDeleteEvent(base_events.HikariEvent, marshaller.Deserializable): # TODO: common base class for Message events. - channel_id: base_entities.Snowflake = marshaller.attrib(deserializer=base_entities.Snowflake, repr=True) + channel_id: base_models.Snowflake = marshaller.attrib(deserializer=base_models.Snowflake, repr=True) """The ID of the channel where this message was deleted.""" - guild_id: typing.Optional[base_entities.Snowflake] = marshaller.attrib( - deserializer=base_entities.Snowflake, if_undefined=None, default=None, repr=True + guild_id: typing.Optional[base_models.Snowflake] = marshaller.attrib( + deserializer=base_models.Snowflake, if_undefined=None, default=None, repr=True ) """The ID of the guild where this message was deleted. This will be `None` if this message was deleted in a DM channel. """ - message_id: base_entities.Snowflake = marshaller.attrib( - raw_name="id", deserializer=base_entities.Snowflake, repr=True - ) + message_id: base_models.Snowflake = marshaller.attrib(raw_name="id", deserializer=base_models.Snowflake, repr=True) """The ID of the message that was deleted.""" -def _deserialize_message_ids(payload: more_typing.JSONArray) -> typing.Set[base_entities.Snowflake]: - return {base_entities.Snowflake(message_id) for message_id in payload} +def _deserialize_message_ids(payload: more_typing.JSONArray) -> typing.Set[base_models.Snowflake]: + return {base_models.Snowflake(message_id) for message_id in payload} @base_events.requires_intents(intents.Intent.GUILD_MESSAGES) @@ -272,18 +271,18 @@ class MessageDeleteBulkEvent(base_events.HikariEvent, marshaller.Deserializable) Sent when multiple messages are deleted in a channel at once. """ - channel_id: base_entities.Snowflake = marshaller.attrib(deserializer=base_entities.Snowflake, repr=True) + channel_id: base_models.Snowflake = marshaller.attrib(deserializer=base_models.Snowflake, repr=True) """The ID of the channel these messages have been deleted in.""" - guild_id: typing.Optional[base_entities.Snowflake] = marshaller.attrib( - deserializer=base_entities.Snowflake, if_none=None, repr=True, + guild_id: typing.Optional[base_models.Snowflake] = marshaller.attrib( + deserializer=base_models.Snowflake, if_none=None, repr=True, ) """The ID of the channel these messages have been deleted in. This will be `None` if these messages were bulk deleted in a DM channel. """ - message_ids: typing.Set[base_entities.Snowflake] = marshaller.attrib( + message_ids: typing.Set[base_models.Snowflake] = marshaller.attrib( raw_name="ids", deserializer=_deserialize_message_ids ) """A collection of the IDs of the messages that were deleted.""" @@ -297,17 +296,17 @@ class MessageReactionAddEvent(base_events.HikariEvent, marshaller.Deserializable # TODO: common base classes! - user_id: base_entities.Snowflake = marshaller.attrib(deserializer=base_entities.Snowflake, repr=True) + user_id: base_models.Snowflake = marshaller.attrib(deserializer=base_models.Snowflake, repr=True) """The ID of the user adding the reaction.""" - channel_id: base_entities.Snowflake = marshaller.attrib(deserializer=base_entities.Snowflake, repr=True) + channel_id: base_models.Snowflake = marshaller.attrib(deserializer=base_models.Snowflake, repr=True) """The ID of the channel where this reaction is being added.""" - message_id: base_entities.Snowflake = marshaller.attrib(deserializer=base_entities.Snowflake, repr=True) + message_id: base_models.Snowflake = marshaller.attrib(deserializer=base_models.Snowflake, repr=True) """The ID of the message this reaction is being added to.""" - guild_id: typing.Optional[base_entities.Snowflake] = marshaller.attrib( - deserializer=base_entities.Snowflake, if_undefined=None, default=None, repr=True + guild_id: typing.Optional[base_models.Snowflake] = marshaller.attrib( + deserializer=base_models.Snowflake, if_undefined=None, default=None, repr=True ) """The ID of the guild where this reaction is being added. @@ -335,17 +334,17 @@ class MessageReactionAddEvent(base_events.HikariEvent, marshaller.Deserializable class MessageReactionRemoveEvent(base_events.HikariEvent, marshaller.Deserializable): """Used to represent Message Reaction Remove gateway events.""" - user_id: base_entities.Snowflake = marshaller.attrib(deserializer=base_entities.Snowflake, repr=True) + user_id: base_models.Snowflake = marshaller.attrib(deserializer=base_models.Snowflake, repr=True) """The ID of the user who is removing their reaction.""" - channel_id: base_entities.Snowflake = marshaller.attrib(deserializer=base_entities.Snowflake, repr=True) + channel_id: base_models.Snowflake = marshaller.attrib(deserializer=base_models.Snowflake, repr=True) """The ID of the channel where this reaction is being removed.""" - message_id: base_entities.Snowflake = marshaller.attrib(deserializer=base_entities.Snowflake, repr=True) + message_id: base_models.Snowflake = marshaller.attrib(deserializer=base_models.Snowflake, repr=True) """The ID of the message this reaction is being removed from.""" - guild_id: typing.Optional[base_entities.Snowflake] = marshaller.attrib( - deserializer=base_entities.Snowflake, if_undefined=None, default=None, repr=True + guild_id: typing.Optional[base_models.Snowflake] = marshaller.attrib( + deserializer=base_models.Snowflake, if_undefined=None, default=None, repr=True ) """The ID of the guild where this reaction is being removed @@ -367,14 +366,14 @@ class MessageReactionRemoveAllEvent(base_events.HikariEvent, marshaller.Deserial Sent when all the reactions are removed from a message, regardless of emoji. """ - channel_id: base_entities.Snowflake = marshaller.attrib(deserializer=base_entities.Snowflake, repr=True) + channel_id: base_models.Snowflake = marshaller.attrib(deserializer=base_models.Snowflake, repr=True) """The ID of the channel where the targeted message is.""" - message_id: base_entities.Snowflake = marshaller.attrib(deserializer=base_entities.Snowflake, repr=True) + message_id: base_models.Snowflake = marshaller.attrib(deserializer=base_models.Snowflake, repr=True) """The ID of the message all reactions are being removed from.""" - guild_id: typing.Optional[base_entities.Snowflake] = marshaller.attrib( - deserializer=base_entities.Snowflake, if_undefined=None, default=None, repr=True, + guild_id: typing.Optional[base_models.Snowflake] = marshaller.attrib( + deserializer=base_models.Snowflake, if_undefined=None, default=None, repr=True, ) """The ID of the guild where the targeted message is, if applicable.""" @@ -388,15 +387,15 @@ class MessageReactionRemoveEmojiEvent(base_events.HikariEvent, marshaller.Deseri Sent when all the reactions for a single emoji are removed from a message. """ - channel_id: base_entities.Snowflake = marshaller.attrib(deserializer=base_entities.Snowflake, repr=True) + channel_id: base_models.Snowflake = marshaller.attrib(deserializer=base_models.Snowflake, repr=True) """The ID of the channel where the targeted message is.""" - guild_id: typing.Optional[base_entities.Snowflake] = marshaller.attrib( - deserializer=base_entities.Snowflake, if_undefined=None, default=None, repr=True + guild_id: typing.Optional[base_models.Snowflake] = marshaller.attrib( + deserializer=base_models.Snowflake, if_undefined=None, default=None, repr=True ) """The ID of the guild where the targeted message is, if applicable.""" - message_id: base_entities.Snowflake = marshaller.attrib(deserializer=base_entities.Snowflake, repr=True) + message_id: base_models.Snowflake = marshaller.attrib(deserializer=base_models.Snowflake, repr=True) """The ID of the message the reactions are being removed from.""" emoji: typing.Union[emojis.UnicodeEmoji, emojis.CustomEmoji] = marshaller.attrib( diff --git a/hikari/events/other.py b/hikari/events/other.py index 39f92ef5ec..089e9dafb4 100644 --- a/hikari/events/other.py +++ b/hikari/events/other.py @@ -16,7 +16,7 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . -"""Components and entities that are used to describe Discord gateway other events.""" +"""Application and entities that are used to describe Discord gateway other events.""" from __future__ import annotations @@ -37,14 +37,15 @@ import attr -from hikari import bases as base_entities -from hikari import guilds -from hikari import users -from hikari.events import base as base_events from hikari.internal import marshaller +from hikari.models import bases as base_models +from hikari.models import guilds +from hikari.models import users + +from . import base as base_events if typing.TYPE_CHECKING: - from hikari.clients import shards # pylint: disable=cyclic-import + from hikari.gateway import client as gateway_client from hikari.internal import more_typing @@ -92,7 +93,7 @@ class StoppedEvent(base_events.HikariEvent): class ConnectedEvent(base_events.HikariEvent, marshaller.Deserializable): """Event invoked each time a shard connects.""" - shard: shards.ShardClient + shard: gateway_client.GatewayClient """The shard that connected.""" @@ -100,7 +101,7 @@ class ConnectedEvent(base_events.HikariEvent, marshaller.Deserializable): class DisconnectedEvent(base_events.HikariEvent, marshaller.Deserializable): """Event invoked each time a shard disconnects.""" - shard: shards.ShardClient + shard: gateway_client.GatewayClient """The shard that disconnected.""" @@ -108,15 +109,15 @@ class DisconnectedEvent(base_events.HikariEvent, marshaller.Deserializable): class ResumedEvent(base_events.HikariEvent): """Represents a gateway Resume event.""" - shard: shards.ShardClient + shard: gateway_client.GatewayClient """The shard that reconnected.""" def _deserialize_unavailable_guilds( payload: more_typing.JSONArray, **kwargs: typing.Any -) -> typing.Mapping[base_entities.Snowflake, guilds.UnavailableGuild]: +) -> typing.Mapping[base_models.Snowflake, guilds.UnavailableGuild]: return { - base_entities.Snowflake(guild["id"]): guilds.UnavailableGuild.deserialize(guild, **kwargs) for guild in payload + base_models.Snowflake(guild["id"]): guilds.UnavailableGuild.deserialize(guild, **kwargs) for guild in payload } @@ -136,7 +137,7 @@ class ReadyEvent(base_events.HikariEvent, marshaller.Deserializable): ) """The object of the current bot account this connection is for.""" - unavailable_guilds: typing.Mapping[base_entities.Snowflake, guilds.UnavailableGuild] = marshaller.attrib( + unavailable_guilds: typing.Mapping[base_models.Snowflake, guilds.UnavailableGuild] = marshaller.attrib( raw_name="guilds", deserializer=_deserialize_unavailable_guilds, inherit_kwargs=True ) """A mapping of the guilds this bot is currently in. diff --git a/hikari/events/voice.py b/hikari/events/voice.py index bcf3f2ece4..76bfbeecda 100644 --- a/hikari/events/voice.py +++ b/hikari/events/voice.py @@ -24,11 +24,12 @@ import attr -from hikari.events import base as base_events -from hikari import bases as base_entities -from hikari import intents -from hikari import voices from hikari.internal import marshaller +from hikari.models import bases as base_models +from hikari.models import intents +from hikari.models import voices + +from . import base as base_events @base_events.requires_intents(intents.Intent.GUILD_VOICE_STATES) @@ -53,7 +54,7 @@ class VoiceServerUpdateEvent(base_events.HikariEvent, marshaller.Deserializable) token: str = marshaller.attrib(deserializer=str) """The voice connection's string token.""" - guild_id: base_entities.Snowflake = marshaller.attrib(deserializer=base_entities.Snowflake, repr=True) + guild_id: base_models.Snowflake = marshaller.attrib(deserializer=base_models.Snowflake, repr=True) """The ID of the guild this voice server update is for.""" endpoint: str = marshaller.attrib(deserializer=str, repr=True) diff --git a/hikari/gateway/__init__.py b/hikari/gateway/__init__.py new file mode 100644 index 0000000000..60449679d9 --- /dev/null +++ b/hikari/gateway/__init__.py @@ -0,0 +1,26 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# Copyright © Nekoka.tt 2019-2020 +# +# This file is part of Hikari. +# +# Hikari is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Hikari is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Hikari. If not, see . + +from __future__ import annotations + +from .client import * +from .connection import * +from .gateway_state import * + +__all__ = client.__all__ + connection.__all__ + gateway_state.__all__ diff --git a/hikari/clients/shards.py b/hikari/gateway/client.py similarity index 74% rename from hikari/clients/shards.py rename to hikari/gateway/client.py index 1839c8a203..d350467ce2 100644 --- a/hikari/clients/shards.py +++ b/hikari/gateway/client.py @@ -27,176 +27,33 @@ from __future__ import annotations -__all__ = ["ShardClient", "ShardClientImpl"] +__all__ = ["GatewayClient"] -import abc import asyncio -import logging import time import typing import aiohttp -from hikari import errors -from hikari.clients import runnable -from hikari.clients import shard_states -from hikari.net import codes -from hikari.net import ratelimits -from hikari.net import shards +from hikari.components import runnable +from hikari.internal import codes +from hikari.internal import helpers +from hikari.internal import ratelimits +from .. import errors + +from . import connection as gateway_connection +from . import gateway_state if typing.TYPE_CHECKING: import datetime - from hikari import gateway_entities - from hikari import guilds - from hikari import intents as _intents - from hikari.clients import components as _components - - -class ShardClient(runnable.RunnableClient, abc.ABC): - """Definition of the interface for a conforming shard client.""" - - __slots__ = () - - @property - @abc.abstractmethod - def shard_id(self) -> int: - """Shard ID (this is 0-indexed).""" - - @property - @abc.abstractmethod - def shard_count(self) -> int: - """Count of how many shards make up this bot.""" - - @property - @abc.abstractmethod - def status(self) -> guilds.PresenceStatus: - """User status for this shard.""" - - @property - @abc.abstractmethod - def activity(self) -> typing.Optional[gateway_entities.Activity]: - """Activity for the user status for this shard. - - This will be `None` if there is no activity. - """ - - @property - @abc.abstractmethod - def idle_since(self) -> typing.Optional[datetime.datetime]: - """Timestamp of when the user of this shard appeared to be idle. - - This will be `None` if not applicable. - """ - - @property - @abc.abstractmethod - def is_afk(self) -> bool: - """Whether the user is appearing as AFK or not..""" - - @property - @abc.abstractmethod - def heartbeat_latency(self) -> float: - """Latency between sending a HEARTBEAT and receiving an ACK in seconds. - - This will be `float("nan")` until the first heartbeat is performed. - """ - - @property - @abc.abstractmethod - def heartbeat_interval(self) -> float: - """Time period to wait between sending HEARTBEAT payloads in seconds. - - This will be `float("nan")` until the connection has received a `HELLO` - payload. - """ - - @property - @abc.abstractmethod - def disconnect_count(self) -> int: - """Count of number of times this shard's connection has disconnected.""" - - @property - @abc.abstractmethod - def reconnect_count(self) -> int: - """Count of number of times this shard's connection has reconnected. + from hikari.models import gateway + from hikari.models import guilds + from hikari.models import intents as intents_ + from hikari.components import application - This includes RESUME and re-IDENTIFY events. - """ - - @property - @abc.abstractmethod - def connection_state(self) -> shard_states.ShardState: - """State of this shard's connection.""" - - @property - @abc.abstractmethod - def is_connected(self) -> bool: - """Whether the shard is connected or not.""" - - @property - @abc.abstractmethod - def seq(self) -> typing.Optional[int]: - """Sequence ID of the shard. - This is the number of payloads that have been received since the last - `IDENTIFY` was sent. - """ - - @property - @abc.abstractmethod - def session_id(self) -> typing.Optional[str]: - """Session ID of the shard connection. - - Will be `None` if there is no session. - """ - - @property - @abc.abstractmethod - def version(self) -> float: - """Version being used for the gateway API.""" - - @property - @abc.abstractmethod - def intents(self) -> typing.Optional[_intents.Intent]: - """Intents that are in use for the shard connection. - - If intents are not being used at all, then this will be `None` instead. - """ - - @abc.abstractmethod - async def update_presence( - self, - *, - status: guilds.PresenceStatus = ..., - activity: typing.Optional[gateway_entities.Activity] = ..., - idle_since: typing.Optional[datetime.datetime] = ..., - is_afk: bool = ..., - ) -> None: - """Update the presence of the user for the shard. - - This will only update arguments that you explicitly specify a value for. - Any arguments that you do not explicitly provide some value for will - not be changed. - - !!! warning - This will fail if the shard is not online. - - Parameters - ---------- - status : hikari.guilds.PresenceStatus - If specified, the new status to set. - activity : hikari.gateway_entities.Activity, optional - If specified, the new activity to set. - idle_since : datetime.datetime, optional - If specified, the time to show up as being idle since, or - `None` if not applicable. - is_afk : bool - If specified, whether the user should be marked as AFK. - """ - - -class ShardClientImpl(ShardClient): +class GatewayClient(runnable.RunnableClient): """The primary interface for a single shard connection. This contains several abstractions to enable usage of the low @@ -209,8 +66,8 @@ class ShardClientImpl(ShardClient): The ID of this specific shard. shard_id : int The number of shards that make up this distributed application. - components : hikari.clients.components.Components - The client components that this shard client should be bound by. + app : hikari.clients.application.Application + The client application that this shard client should be bound by. Includes the the gateway configuration to use to initialize this shard and the consumer of a raw event. url : str @@ -238,110 +95,152 @@ class ShardClientImpl(ShardClient): "logger", ) - def __init__(self, shard_id: int, shard_count: int, components: _components.Components, url: str,) -> None: - super().__init__(logging.getLogger(f"hikari.{type(self).__qualname__}.{shard_id}")) - self._components = components - self._raw_event_consumer = components.event_manager - self._activity = components.config.initial_activity - self._idle_since = components.config.initial_idle_since - self._is_afk = components.config.initial_is_afk - self._status = components.config.initial_status - self._shard_state = shard_states.ShardState.NOT_RUNNING + def __init__(self, shard_id: int, shard_count: int, app: application.Application, url: str) -> None: + super().__init__(helpers.get_logger(self, str(shard_id))) + self._components = app + self._raw_event_consumer = app.event_manager + self._activity = app.config.initial_activity + self._idle_since = app.config.initial_idle_since + self._is_afk = app.config.initial_is_afk + self._status = app.config.initial_status + self._shard_state = gateway_state.GatewayState.NOT_RUNNING self._task = None - self._connection = shards.Shard( - compression=components.config.gateway_use_compression, - connector=components.config.tcp_connector, - debug=components.config.debug, + self._connection = gateway_connection.Shard( + compression=app.config.gateway_use_compression, + connector=app.config.tcp_connector, + debug=app.config.debug, # This is a bit of a cheat, we should pass a coroutine function here, but # instead we just use a lambda that does the transformation we want (replaces the # low-level shard argument with the reference to this class object), then return # the result of that coroutine. To the low level client, it looks the same :-) # (also hides a useless stack frame from tracebacks, I guess). - dispatcher=lambda c, n, pl: components.event_manager.process_raw_event(self, n, pl), + dispatcher=lambda c, n, pl: app.event_manager.process_raw_event(self, n, pl), initial_presence=self._create_presence_pl( - status=components.config.initial_status, - activity=components.config.initial_activity, - idle_since=components.config.initial_idle_since, - is_afk=components.config.initial_is_afk, + status=app.config.initial_status, + activity=app.config.initial_activity, + idle_since=app.config.initial_idle_since, + is_afk=app.config.initial_is_afk, ), - intents=components.config.intents, - large_threshold=components.config.large_threshold, - proxy_auth=components.config.proxy_auth, - proxy_headers=components.config.proxy_headers, - proxy_url=components.config.proxy_url, + intents=app.config.intents, + large_threshold=app.config.large_threshold, + proxy_auth=app.config.proxy_auth, + proxy_headers=app.config.proxy_headers, + proxy_url=app.config.proxy_url, session_id=None, seq=None, shard_id=shard_id, shard_count=shard_count, - ssl_context=components.config.ssl_context, - token=components.config.token, + ssl_context=app.config.ssl_context, + token=app.config.token, url=url, - verify_ssl=components.config.verify_ssl, - version=components.config.gateway_version, + verify_ssl=app.config.verify_ssl, + version=app.config.gateway_version, ) @property def shard_id(self) -> int: + """Shard ID (this is 0-indexed).""" return self._connection.shard_id @property def shard_count(self) -> int: + """Count of how many shards make up this bot.""" return self._connection.shard_count @property def status(self) -> guilds.PresenceStatus: + """User status for this shard.""" return self._status @property - def activity(self) -> typing.Optional[gateway_entities.Activity]: + def activity(self) -> typing.Optional[gateway.Activity]: + """Activity for the user status for this shard. + + This will be `None` if there is no activity. + """ return self._activity @property def idle_since(self) -> typing.Optional[datetime.datetime]: + """Timestamp of when the user of this shard appeared to be idle. + + This will be `None` if not applicable. + """ return self._idle_since @property def is_afk(self) -> bool: + """Whether the user is appearing as AFK or not.""" return self._is_afk @property def heartbeat_latency(self) -> float: + """Latency between sending a HEARTBEAT and receiving an ACK in seconds. + + This will be `float("nan")` until the first heartbeat is performed. + """ return self._connection.heartbeat_latency @property def heartbeat_interval(self) -> float: + """Time period to wait between sending HEARTBEAT payloads in seconds. + + This will be `float("nan")` until the connection has received a `HELLO` + payload. + """ return self._connection.heartbeat_interval @property def disconnect_count(self) -> int: + """Count of number of times this shard's connection has disconnected.""" return self._connection.disconnect_count @property def reconnect_count(self) -> int: + """Count of number of times this shard's connection has reconnected. + + This includes RESUME and re-IDENTIFY events. + """ return self._connection.reconnect_count @property - def connection_state(self) -> shard_states.ShardState: + def connection_state(self) -> gateway_state.GatewayState: + """State of this shard's connection.""" return self._shard_state @property def is_connected(self) -> bool: + """Whether the shard is connected or not.""" return self._connection.is_connected @property def seq(self) -> typing.Optional[int]: + """Sequence ID of the shard. + + This is the number of payloads that have been received since the last + `IDENTIFY` was sent. If not yet identified, this will be `None`. + """ return self._connection.seq @property def session_id(self) -> typing.Optional[str]: + """Session ID of the shard connection. + + Will be `None` if there is no session. + """ return self._connection.session_id @property def version(self) -> float: + """Version being used for the gateway API.""" return self._connection.version @property - def intents(self) -> typing.Optional[_intents.Intent]: + def intents(self) -> typing.Optional[intents_.Intent]: + """Intents that are in use for the shard connection. + + If intents are not being used at all, then this will be `None` instead. + """ return self._connection.intents async def start(self): @@ -350,10 +249,10 @@ async def start(self): This will wait for the shard to dispatch a `READY` event, and then return. """ - if self._shard_state not in (shard_states.ShardState.NOT_RUNNING, shard_states.ShardState.STOPPED): + if self._shard_state not in (gateway_state.GatewayState.NOT_RUNNING, gateway_state.GatewayState.STOPPED): raise RuntimeError("Cannot start a shard twice") - self._task = asyncio.create_task(self._keep_alive(), name="ShardClient#keep_alive") + self._task = asyncio.create_task(self._keep_alive(), name="GatewayClient#keep_alive") completed, _ = await asyncio.wait( [self._task, self._connection.ready_event.wait()], return_when=asyncio.FIRST_COMPLETED @@ -373,8 +272,8 @@ async def close(self) -> None: This will wait for the client to shut down before returning. """ - if self._shard_state != shard_states.ShardState.STOPPING: - self._shard_state = shard_states.ShardState.STOPPING + if self._shard_state != gateway_state.GatewayState.STOPPING: + self._shard_state = gateway_state.GatewayState.STOPPING self.logger.debug("stopping shard") await self._connection.close() @@ -382,7 +281,7 @@ async def close(self) -> None: if self._task is not None: await self._task - self._shard_state = shard_states.ShardState.STOPPED + self._shard_state = gateway_state.GatewayState.STOPPED async def _keep_alive(self): # pylint: disable=too-many-branches back_off = ratelimits.ExponentialBackOff(base=1.85, maximum=600, initial_increment=2) @@ -459,7 +358,7 @@ async def _keep_alive(self): # pylint: disable=too-many-branches async def _spin_up(self) -> asyncio.Task: self.logger.debug("initializing shard") - self._shard_state = shard_states.ShardState.CONNECTING + self._shard_state = gateway_state.GatewayState.CONNECTING is_resume = self._connection.seq is not None and self._connection.session_id is not None @@ -485,7 +384,7 @@ async def _spin_up(self) -> asyncio.Task: if is_resume: self.logger.info("sent RESUME, waiting for RESUMED event") - self._shard_state = shard_states.ShardState.RESUMING + self._shard_state = gateway_state.GatewayState.RESUMING completed, _ = await asyncio.wait( [connect_task, self._connection.resumed_event.wait()], return_when=asyncio.FIRST_COMPLETED @@ -500,7 +399,7 @@ async def _spin_up(self) -> asyncio.Task: else: self.logger.info("sent IDENTIFY, waiting for READY event") - self._shard_state = shard_states.ShardState.WAITING_FOR_READY + self._shard_state = gateway_state.GatewayState.WAITING_FOR_READY completed, _ = await asyncio.wait( [connect_task, self._connection.ready_event.wait()], return_when=asyncio.FIRST_COMPLETED @@ -512,7 +411,7 @@ async def _spin_up(self) -> asyncio.Task: self.logger.info("now READY") - self._shard_state = shard_states.ShardState.READY + self._shard_state = gateway_state.GatewayState.READY return connect_task @@ -520,10 +419,31 @@ async def update_presence( self, *, status: guilds.PresenceStatus = ..., - activity: typing.Optional[gateway_entities.Activity] = ..., + activity: typing.Optional[gateway.Activity] = ..., idle_since: typing.Optional[datetime.datetime] = ..., is_afk: bool = ..., ) -> None: + """Update the presence of the user for the shard. + + This will only update arguments that you explicitly specify a value for. + Any arguments that you do not explicitly provide some value for will + not be changed. + + !!! warning + This will fail if the shard is not online. + + Parameters + ---------- + status : hikari.guilds.PresenceStatus + If specified, the new status to set. + activity : hikari.gateway_entities.Activity, optional + If specified, the new activity to set. + idle_since : datetime.datetime, optional + If specified, the time to show up as being idle since, or + `None` if not applicable. + is_afk : bool + If specified, whether the user should be marked as AFK. + """ # We wouldn't ever want to do this, so throw an error if it happens. if status is ... and activity is ... and idle_since is ... and is_afk is ...: raise ValueError("update_presence requires at least one argument to be passed") @@ -545,7 +465,7 @@ async def update_presence( @staticmethod def _create_presence_pl( status: guilds.PresenceStatus, - activity: typing.Optional[gateway_entities.Activity], + activity: typing.Optional[gateway.Activity], idle_since: typing.Optional[datetime.datetime], is_afk: bool, ) -> typing.Dict[str, typing.Any]: diff --git a/hikari/net/shards.py b/hikari/gateway/connection.py similarity index 99% rename from hikari/net/shards.py rename to hikari/gateway/connection.py index 96958e3fc1..2bd9318e06 100644 --- a/hikari/net/shards.py +++ b/hikari/gateway/connection.py @@ -47,19 +47,18 @@ import aiohttp.typedefs -from hikari import errors +from hikari.internal import codes +from hikari.internal import http_client from hikari.internal import more_asyncio from hikari.internal import more_typing -from hikari.net import codes -from hikari.net import http_client -from hikari.net import ratelimits -from hikari.net import user_agents +from hikari.internal import ratelimits +from hikari.internal import user_agents +from hikari import errors if typing.TYPE_CHECKING: import ssl - from hikari import intents as _intents - + from hikari.models import intents as _intents DispatcherT = typing.Callable[["Shard", str, typing.Dict], more_typing.Coroutine[None]] """The signature for an event dispatch callback.""" diff --git a/hikari/clients/shard_states.py b/hikari/gateway/gateway_state.py similarity index 96% rename from hikari/clients/shard_states.py rename to hikari/gateway/gateway_state.py index fcd6787085..2ea245a81d 100644 --- a/hikari/clients/shard_states.py +++ b/hikari/gateway/gateway_state.py @@ -20,13 +20,13 @@ from __future__ import annotations -__all__ = ["ShardState"] +__all__ = ["GatewayState"] from hikari.internal import more_enums @more_enums.must_be_unique -class ShardState(int, more_enums.Enum): +class GatewayState(int, more_enums.Enum): """Describes the state of a shard.""" NOT_RUNNING = 0 diff --git a/hikari/internal/__init__.py b/hikari/internal/__init__.py index 062aee7180..1798e98493 100644 --- a/hikari/internal/__init__.py +++ b/hikari/internal/__init__.py @@ -19,3 +19,5 @@ """Various utilities used internally within this API.""" from __future__ import annotations + +__all__ = [] diff --git a/hikari/net/codes.py b/hikari/internal/codes.py similarity index 99% rename from hikari/net/codes.py rename to hikari/internal/codes.py index 3fec6b471d..42aad58a44 100644 --- a/hikari/net/codes.py +++ b/hikari/internal/codes.py @@ -22,7 +22,7 @@ __all__ = ["GatewayCloseCode", "GatewayOpcode", "JSONErrorCode"] -from hikari.internal import more_enums +from . import more_enums @more_enums.must_be_unique diff --git a/hikari/internal/conversions.py b/hikari/internal/conversions.py index 808ca3a962..f2239bacfe 100644 --- a/hikari/internal/conversions.py +++ b/hikari/internal/conversions.py @@ -328,10 +328,6 @@ def dereference_int_flag( raw_value : Castable Value The raw value to convert. - Returns - ------- - enum.IntFlag - The cast value as a flag. !!! note Types that are a `Castable Value` include: @@ -342,6 +338,11 @@ def dereference_int_flag( When a collection is passed, values will be combined using functional reduction via the `operator.or_` operator. + + Returns + ------- + enum.IntFlag + The cast value as a flag. """ if isinstance(raw_value, str) and raw_value.isdigit(): raw_value = int(raw_value) diff --git a/hikari/internal/helpers.py b/hikari/internal/helpers.py index fdfb48f08e..5e368a30dc 100644 --- a/hikari/internal/helpers.py +++ b/hikari/internal/helpers.py @@ -22,14 +22,20 @@ __all__ = [] +import logging import typing -from hikari import bases -from hikari.internal import more_collections +from . import more_collections if typing.TYPE_CHECKING: - from hikari import guilds - from hikari import users + from hikari.models import bases + from hikari.models import guilds + from hikari.models import users + + +def get_logger(cls: typing.Union[typing.Type, typing.Any], *additional_args: str) -> logging.Logger: + cls = cls if isinstance(cls, type) else type(cls) + return logging.getLogger(".".join((cls.__module__, cls.__qualname__, *additional_args))) def generate_allowed_mentions( # pylint:disable=line-too-long diff --git a/hikari/net/http_client.py b/hikari/internal/http_client.py similarity index 99% rename from hikari/net/http_client.py rename to hikari/internal/http_client.py index 33ffdd119b..a6c717e1b0 100644 --- a/hikari/net/http_client.py +++ b/hikari/internal/http_client.py @@ -29,14 +29,14 @@ import aiohttp.typedefs -from hikari.net import tracing +from . import tracing class HTTPClient(abc.ABC): # pylint:disable=too-many-instance-attributes """An HTTP client base for Hikari. The purpose of this is to provide a consistent interface for any network - facing components that need an HTTP connection or websocket connection. + facing application that need an HTTP connection or websocket connection. This class takes care of initializing the underlying client session, etc. diff --git a/hikari/internal/marshaller.py b/hikari/internal/marshaller.py index 8c0f9c9576..1d8f467a83 100644 --- a/hikari/internal/marshaller.py +++ b/hikari/internal/marshaller.py @@ -16,7 +16,7 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . -"""An internal marshalling utility used by internal API components. +"""An internal marshalling utility used by internal API application. !!! warning You should not change anything in this file, if you do, you will likely get @@ -41,10 +41,10 @@ import attr -from hikari.internal import more_collections +from . import more_collections if typing.TYPE_CHECKING: - from hikari.internal import more_typing + from . import more_typing _RAW_NAME_ATTR: typing.Final[str] = __name__ + "_RAW_NAME" _SERIALIZER_ATTR: typing.Final[str] = __name__ + "_SERIALIZER" @@ -277,7 +277,7 @@ def _construct_entity_descriptor(entity: typing.Any) -> _EntityDescriptor: class HikariEntityMarshaller: """Hikari's utility to manage automated serialization and deserialization. - It can deserialize and serialize any internal components that that are + It can deserialize and serialize any internal application that that are decorated with the `marshallable` decorator, and that are `attr.s` classes using fields with the`attrib` function call descriptor. """ diff --git a/hikari/internal/meta.py b/hikari/internal/meta.py index 735d4135a9..905eeff5d2 100644 --- a/hikari/internal/meta.py +++ b/hikari/internal/meta.py @@ -26,7 +26,7 @@ import inspect import typing -from hikari.internal import more_collections +from . import more_collections class SingletonMeta(type): diff --git a/hikari/internal/more_asyncio.py b/hikari/internal/more_asyncio.py index 56598129cd..f609795261 100644 --- a/hikari/internal/more_asyncio.py +++ b/hikari/internal/more_asyncio.py @@ -27,7 +27,7 @@ import typing if typing.TYPE_CHECKING: - from hikari.internal import more_typing + from . import more_typing @typing.overload diff --git a/hikari/internal/more_typing.py b/hikari/internal/more_typing.py index 8e6b44e5ec..74b899acfd 100644 --- a/hikari/internal/more_typing.py +++ b/hikari/internal/more_typing.py @@ -50,11 +50,13 @@ from typing import Union as _Union if _TYPE_CHECKING: + import asyncio + import contextvars + from types import FrameType as _FrameType from typing import Callable as _Callable from typing import IO as _IO - import asyncio - import contextvars + # pylint: enable=ungrouped-imports T_contra = _TypeVar("T_contra", contravariant=True) diff --git a/hikari/net/ratelimits.py b/hikari/internal/ratelimits.py similarity index 61% rename from hikari/net/ratelimits.py rename to hikari/internal/ratelimits.py index c031965b1e..29968e4177 100644 --- a/hikari/net/ratelimits.py +++ b/hikari/internal/ratelimits.py @@ -163,8 +163,6 @@ "BurstRateLimiter", "ManualRateLimiter", "WindowedBurstRateLimiter", - "RESTBucket", - "RESTBucketManager", "ExponentialBackOff", ] @@ -175,14 +173,8 @@ import time import typing -from hikari.internal import more_asyncio - if typing.TYPE_CHECKING: - import datetime - import types - - from hikari.internal import more_typing - from hikari.net import routes + from . import more_typing UNKNOWN_HASH: typing.Final[str] = "UNKNOWN" @@ -295,7 +287,7 @@ def is_empty(self) -> bool: class ManualRateLimiter(BurstRateLimiter): - """Rate limit handler for the global REST rate limit. + """Rate limit handler for the global RESTSession rate limit. This is a non-preemptive rate limiting algorithm that will always return completed futures until `ManualRateLimiter.throttle` is invoked. Once this @@ -308,8 +300,8 @@ class ManualRateLimiter(BurstRateLimiter): Triggering a throttle when it is already set will cancel the current throttle task that is sleeping and replace it. - This is used to enforce the global REST rate limit that will occur - "randomly" during REST API interaction. + This is used to enforce the global RESTSession rate limit that will occur + "randomly" during RESTSession API interaction. Expect random occurrences. """ @@ -483,18 +475,18 @@ def get_time_until_reset(self, now: float) -> float: now : float The monotonic `time.perf_counter` timestamp. - Returns - ------- - float - The time left to sleep before the rate limit is reset. If no rate limit - is in effect, then this will return `0.0` instead. - !!! warning Invoking this method will update the internal state if we were previously rate limited, but at the given time are no longer under that limit. This makes it imperative that you only pass the current timestamp to this function, and not past or future timestamps. The effects of doing the latter are undefined behaviour. + + Returns + ------- + float + The time left to sleep before the rate limit is reset. If no rate limit + is in effect, then this will return `0.0` instead. """ if not self.is_rate_limited(now): return 0.0 @@ -563,376 +555,6 @@ async def throttle(self) -> None: self.throttle_task = None -class RESTBucket(WindowedBurstRateLimiter): - """Represents a rate limit for an REST endpoint. - - Component to represent an active rate limit bucket on a specific REST route - with a specific major parameter combo. - - This is somewhat similar to the `WindowedBurstRateLimiter` in how it - works. - - This algorithm will use fixed-period time windows that have a given limit - (capacity). Each time a task requests processing time, it will drip another - unit into the bucket. Once the bucket has reached its limit, nothing can - drip and new tasks will be queued until the time window finishes. - - Once the time window finishes, the bucket will empty, returning the current - capacity to zero, and tasks that are queued will start being able to drip - again. - - Additional logic is provided by the `RESTBucket.update_rate_limit` call - which allows dynamically changing the enforced rate limits at any time. - """ - - __slots__ = ("compiled_route",) - - compiled_route: typing.Final[routes.CompiledRoute] - """The compiled route that this rate limit is covering.""" - - def __init__(self, name: str, compiled_route: routes.CompiledRoute) -> None: - super().__init__(name, 1, 1) - self.compiled_route = compiled_route - - @property - def is_unknown(self) -> bool: - """Return `True` if the bucket represents an `UNKNOWN` bucket.""" - return self.name.startswith(UNKNOWN_HASH) - - def acquire(self) -> more_typing.Future[None]: - """Acquire time on this rate limiter. - - Returns - ------- - asyncio.Future - A future that should be awaited immediately. Once the future completes, - you are allowed to proceed with your operation. - - !!! note - You should afterwards invoke `RESTBucket.update_rate_limit` to - update any rate limit information you are made aware of. - """ - return more_asyncio.completed_future(None) if self.is_unknown else super().acquire() - - def update_rate_limit(self, remaining: int, limit: int, reset_at: float) -> None: - """Amend the rate limit. - - Parameters - ---------- - remaining : int - The calls remaining in this time window. - limit : int - The total calls allowed in this time window. - reset_at : float - The epoch at which to reset the limit. - - !!! note - The `reset_at` epoch is expected to be a `time.perf_counter` - monotonic epoch, rather than a `time.time` date-based epoch. - """ - self.remaining = remaining - self.limit = limit - self.reset_at = reset_at - self.period = max(0.0, self.reset_at - time.perf_counter()) - - def drip(self) -> None: - """Decrement the remaining count for this bucket. - - !!! note - If the bucket is marked as `RESTBucket.is_unknown`, then this will - not do anything. `Unknown` buckets have infinite rate limits. - """ - # We don't drip unknown buckets: we can't rate limit them as we don't know their real bucket hash or - # the current rate limit values Discord put on them... - if not self.is_unknown: - self.remaining -= 1 - - -class RESTBucketManager: - """The main rate limiter implementation for REST clients. - - This is designed to provide bucketed rate limiting for Discord REST - endpoints that respects the `X-RateLimit-Bucket` rate limit header. To do - this, it makes the assumption that any limit can change at any time. - """ - - _POLL_PERIOD: typing.Final[typing.ClassVar[int]] = 20 - _EXPIRE_PERIOD: typing.Final[typing.ClassVar[int]] = 10 - - __slots__ = ( - "routes_to_hashes", - "real_hashes_to_buckets", - "closed_event", - "gc_task", - "logger", - ) - - routes_to_hashes: typing.Final[typing.MutableMapping[routes.Route, str]] - """Maps routes to their `X-RateLimit-Bucket` header being used.""" - - real_hashes_to_buckets: typing.Final[typing.MutableMapping[str, RESTBucket]] - """Maps full bucket hashes (`X-RateLimit-Bucket` appended with a hash of - major parameters used in that compiled route) to their corresponding rate - limiters. - """ - - closed_event: typing.Final[asyncio.Event] - """An internal event that is set when the object is shut down.""" - - gc_task: typing.Optional[more_typing.Task[None]] - """The internal garbage collector task.""" - - logger: typing.Final[logging.Logger] - """The logger to use for this object.""" - - def __init__(self) -> None: - self.routes_to_hashes = {} - self.real_hashes_to_buckets = {} - self.closed_event: asyncio.Event = asyncio.Event() - self.gc_task: typing.Optional[asyncio.Task] = None - self.logger = logging.getLogger(f"hikari.net.{type(self).__qualname__}") - - def __enter__(self) -> RESTBucketManager: - return self - - def __exit__(self, exc_type: typing.Type[Exception], exc_val: Exception, exc_tb: types.TracebackType) -> None: - self.close() - - def __del__(self) -> None: - self.close() - - def start(self, poll_period: float = _POLL_PERIOD, expire_after: float = _EXPIRE_PERIOD) -> None: - """Start this ratelimiter up. - - This spins up internal garbage collection logic in the background to - keep memory usage to an optimal level as old routes and bucket hashes - get discarded and replaced. - - Parameters - ---------- - poll_period : float - Period to poll the garbage collector at in seconds. Defaults - to `20` seconds. - expire_after : float - Time after which the last `reset_at` was hit for a bucket to - remove it. Higher values will retain unneeded ratelimit info for - longer, but may produce more effective ratelimiting logic as a - result. Using `0` will make the bucket get garbage collected as soon - as the rate limit has reset. Defaults to `10` seconds. - """ - if not self.gc_task: - self.gc_task = asyncio.get_running_loop().create_task(self.gc(poll_period, expire_after)) - - def close(self) -> None: - """Close the garbage collector and kill any tasks waiting on ratelimits. - - Once this has been called, this object is considered to be effectively - dead. To reuse it, one should create a new instance. - """ - self.closed_event.set() - for bucket in self.real_hashes_to_buckets.values(): - bucket.close() - self.real_hashes_to_buckets.clear() - self.routes_to_hashes.clear() - - # Ignore docstring not starting in an imperative mood - async def gc(self, poll_period: float, expire_after: float) -> None: # noqa: D401 - """The garbage collector loop. - - This is designed to run in the background and manage removing unused - route references from the rate-limiter collection to save memory. - - This will run forever until `RESTBucketManager. closed_event` is set. - This will invoke `RESTBucketManager.do_gc_pass` periodically. - - Parameters - ---------- - poll_period : float - The period to poll at. - expire_after : float - Time after which the last `reset_at` was hit for a bucket to - remove it. Higher values will retain unneeded ratelimit info for - longer, but may produce more effective ratelimiting logic as a - result. Using `0` will make the bucket get garbage collected as soon - as the rate limit has reset. - - !!! warning - You generally have no need to invoke this directly. Use - `RESTBucketManager.start` and `RESTBucketManager.close` to control - this instead. - """ - # Prevent filling memory increasingly until we run out by removing dead buckets every 20s - # Allocations are somewhat cheap if we only do them every so-many seconds, after all. - self.logger.debug("rate limit garbage collector started") - while not self.closed_event.is_set(): - try: - await asyncio.wait_for(self.closed_event.wait(), timeout=poll_period) - except asyncio.TimeoutError: - self.logger.debug("performing rate limit garbage collection pass") - self.do_gc_pass(expire_after) - self.gc_task = None - - def do_gc_pass(self, expire_after: float) -> None: - """Perform a single garbage collection pass. - - This will assess any routes stored in the internal mappings of this - object and remove any that are deemed to be inactive or dead in order - to save memory. - - If the removed routes are used again in the future, they will be - re-cached automatically. - - Parameters - ---------- - expire_after : float - Time after which the last `reset_at` was hit for a bucket to - remove it. Defaults to `reset_at` + 20 seconds. Higher values will - retain unneeded ratelimit info for longer, but may produce more - effective ratelimiting logic as a result. - - !!! warning - You generally have no need to invoke this directly. Use - `RESTBucketManager.start` and `RESTBucketManager.close` to control - this instead. - """ - buckets_to_purge = [] - - now = time.perf_counter() - - # We have three main states that a bucket can be in: - # 1. active - the bucket is active and is not at risk of deallocation - # 2. survival - the bucket is inactive but is still fresh enough to be kept alive. - # 3. death - the bucket has been inactive for too long. - active = 0 - - # Discover and purge - bucket_pairs = self.real_hashes_to_buckets.items() - - for full_hash, bucket in bucket_pairs: - if bucket.is_empty and bucket.reset_at + expire_after < now: - # If it is still running a throttle and is in memory, it will remain in memory - # but we won't know about it. - buckets_to_purge.append(full_hash) - - if bucket.reset_at >= now: - active += 1 - - dead = len(buckets_to_purge) - total = len(bucket_pairs) - survival = total - active - dead - - for full_hash in buckets_to_purge: - self.real_hashes_to_buckets[full_hash].close() - del self.real_hashes_to_buckets[full_hash] - - self.logger.debug("purged %s stale buckets, %s remain in survival, %s active", dead, survival, active) - - def acquire(self, compiled_route: routes.CompiledRoute) -> more_typing.Future[None]: - """Acquire a bucket for the given route. - - Parameters - ---------- - compiled_route : hikari.net.routes.CompiledRoute - The route to get the bucket for. - - Returns - ------- - asyncio.Future - A future to await that completes when you are allowed to run - your request logic. - - !!! note - The returned future MUST be awaited, and will complete when your - turn to make a call comes along. You are expected to await this and - then immediately make your REST call. The returned future may - already be completed if you can make the call immediately. - """ - # Returns a future to await on to wait to be allowed to send the request, and a - # bucket hash to use to update rate limits later. - template = compiled_route.route - - if template in self.routes_to_hashes: - bucket_hash = self.routes_to_hashes[template] - else: - bucket_hash = UNKNOWN_HASH - self.routes_to_hashes[template] = bucket_hash - - real_bucket_hash = compiled_route.create_real_bucket_hash(bucket_hash) - - try: - bucket = self.real_hashes_to_buckets[real_bucket_hash] - self.logger.debug("%s is being mapped to existing bucket %s", compiled_route, real_bucket_hash) - except KeyError: - self.logger.debug("%s is being mapped to new bucket %s", compiled_route, real_bucket_hash) - bucket = RESTBucket(real_bucket_hash, compiled_route) - self.real_hashes_to_buckets[real_bucket_hash] = bucket - - return bucket.acquire() - - def update_rate_limits( - self, - compiled_route: routes.CompiledRoute, - bucket_header: typing.Optional[str], - remaining_header: int, - limit_header: int, - date_header: datetime.datetime, - reset_at_header: datetime.datetime, - ) -> None: - """Update the rate limits for a bucket using info from a response. - - Parameters - ---------- - compiled_route : hikari.net.routes.CompiledRoute - The compiled route to get the bucket for. - bucket_header : str, optional - The `X-RateLimit-Bucket` header that was provided in the response, - or `None` if not present. - remaining_header : int - The `X-RateLimit-Remaining` header cast to an `int`. - limit_header : int - The `X-RateLimit-Limit`header cast to an `int`. - date_header : datetime.datetime - The `Date` header value as a `datetime.datetime`. - reset_at_header : datetime.datetime - The `X-RateLimit-Reset` header value as a `datetime.datetime`. - """ - self.routes_to_hashes[compiled_route.route] = bucket_header - - real_bucket_hash = compiled_route.create_real_bucket_hash(bucket_header) - - reset_after = (reset_at_header - date_header).total_seconds() - reset_at_monotonic = time.perf_counter() + reset_after - - if real_bucket_hash in self.real_hashes_to_buckets: - bucket = self.real_hashes_to_buckets[real_bucket_hash] - self.logger.debug( - "updating %s with bucket %s [reset-after:%ss, limit:%s, remaining:%s]", - compiled_route, - real_bucket_hash, - reset_after, - limit_header, - remaining_header, - ) - else: - bucket = RESTBucket(real_bucket_hash, compiled_route) - self.real_hashes_to_buckets[real_bucket_hash] = bucket - self.logger.debug( - "remapping %s with bucket %s [reset-after:%ss, limit:%s, remaining:%s]", - compiled_route, - real_bucket_hash, - reset_after, - limit_header, - remaining_header, - ) - - bucket.update_rate_limit(remaining_header, limit_header, reset_at_monotonic) - - @property - def is_started(self) -> bool: - """Return `True` if the rate limiter GC task is started.""" - return self.gc_task is not None - - class ExponentialBackOff: r"""Implementation of an asyncio-compatible exponential back-off algorithm with random jitter. diff --git a/hikari/net/tracing.py b/hikari/internal/tracing.py similarity index 99% rename from hikari/net/tracing.py rename to hikari/internal/tracing.py index d9efe986f8..7453e85f9f 100644 --- a/hikari/net/tracing.py +++ b/hikari/internal/tracing.py @@ -153,6 +153,7 @@ async def on_dns_cache_miss(self, _, ctx, params): """Log when we have to query a DNS server for an IP address.""" self.logger.debug("[%s] will perform DNS lookup of new host %s", ctx.identifier, params.host) + # noinspection PyMethodMayBeStatic async def on_dns_resolvehost_start(self, _, ctx, __): """Store the time the DNS lookup started at.""" ctx.dns_start_time = time.perf_counter() diff --git a/hikari/internal/urls.py b/hikari/internal/urls.py index 831fc0574b..50e73a5659 100644 --- a/hikari/internal/urls.py +++ b/hikari/internal/urls.py @@ -32,7 +32,7 @@ """The URL for the CDN.""" REST_API_URL: typing.Final[str] = "https://discord.com/api/v{0.version}" -"""The URL for the REST API. This contains a version number parameter that +"""The URL for the RESTSession API. This contains a version number parameter that should be interpolated. """ diff --git a/hikari/net/user_agents.py b/hikari/internal/user_agents.py similarity index 98% rename from hikari/net/user_agents.py rename to hikari/internal/user_agents.py index 57d0131833..8a7d6ebe84 100644 --- a/hikari/net/user_agents.py +++ b/hikari/internal/user_agents.py @@ -31,7 +31,7 @@ import typing -from hikari.internal import meta +from . import meta class UserAgent(metaclass=meta.SingletonMeta): diff --git a/hikari/models/__init__.py b/hikari/models/__init__.py new file mode 100644 index 0000000000..ff23df643d --- /dev/null +++ b/hikari/models/__init__.py @@ -0,0 +1,62 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# Copyright © Nekoka.tt 2019-2020 +# +# This file is part of Hikari. +# +# Hikari is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Hikari is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Hikari. If not, see . + +from __future__ import annotations + +from .applications import * +from .audit_logs import * +from .bases import * +from .channels import * +from .colors import * +from .colours import * +from .embeds import * +from .emojis import * +from .files import * +from .gateway import * +from .guilds import * +from .intents import * +from .invites import * +from .messages import * +from .permissions import * +from .unset import * +from .users import * +from .voices import * +from .webhooks import * + +__all__ = ( + applications.__all__ + + audit_logs.__all__ + + bases.__all__ + + channels.__all__ + + colors.__all__ + + colours.__all__ + + embeds.__all__ + + emojis.__all__ + + files.__all__ + + gateway.__all__ + + guilds.__all__ + + intents.__all__ + + invites.__all__ + + messages.__all__ + + permissions.__all__ + + unset.__all__ + + users.__all__ + + voices.__all__ + + webhooks.__all__ +) diff --git a/hikari/applications.py b/hikari/models/applications.py similarity index 90% rename from hikari/applications.py rename to hikari/models/applications.py index d4c9b758b9..54cd930ac6 100644 --- a/hikari/applications.py +++ b/hikari/models/applications.py @@ -16,7 +16,7 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . -"""Components and entities related to discord's Oauth2 flow.""" +"""Application and entities related to discord's Oauth2 flow.""" from __future__ import annotations @@ -36,14 +36,15 @@ import attr -from hikari import bases -from hikari import guilds -from hikari import permissions -from hikari import users from hikari.internal import marshaller from hikari.internal import more_enums from hikari.internal import urls +from . import bases +from . import guilds +from . import permissions +from . import users + if typing.TYPE_CHECKING: from hikari.internal import more_typing @@ -58,14 +59,14 @@ class OAuth2Scope(str, more_enums.Enum): """ ACTIVITIES_READ = "activities.read" - """Enable the app to fetch a user's "Now Playing/Recently Played" list. + """Enable the application to fetch a user's "Now Playing/Recently Played" list. !!! note You must be whitelisted to use this scope. """ ACTIVITIES_WRITE = "activities.write" - """Enable the app to update a user's activity. + """Enable the application to update a user's activity. !!! note You must be whitelisted to use this scope. @@ -75,24 +76,24 @@ class OAuth2Scope(str, more_enums.Enum): """ APPLICATIONS_BUILDS_READ = "applications.builds.read" - """Enable the app to read build data for a user's applications. + """Enable the application to read build data for a user's applications. !!! note You must be whitelisted to use this scope. """ APPLICATIONS_BUILDS_UPLOAD = "applications.builds.upload" - """Enable the app to upload/update builds for a user's applications. + """Enable the application to upload/update builds for a user's applications. !!! note You must be whitelisted to use this scope. """ APPLICATIONS_ENTITLEMENTS = "applications.entitlements" - """Enable the app to read entitlements for a user's applications.""" + """Enable the application to read entitlements for a user's applications.""" APPLICATIONS_STORE_UPDATE = "applications.store.update" - """Enable the app to read and update store data for the user's applications. + """Enable the application to read and update store data for the user's applications. This includes store listings, achievements, SKU's, etc. @@ -108,26 +109,26 @@ class OAuth2Scope(str, more_enums.Enum): """ CONNECTIONS = "connections" - """Enable the app to view third-party linked accounts such as Twitch.""" + """Enable the application to view third-party linked accounts such as Twitch.""" EMAIL = "email" - """Enable the app to view the user's email and application info.""" + """Enable the application to view the user's email and application info.""" GROUP_DM_JOIN = "gdm.join" """Enable the application to join users into a group DM.""" GUILDS = "guilds" - """Enable the app to view the guilds the user is in.""" + """Enable the application to view the guilds the user is in.""" GUILDS_JOIN = "guilds.join" - """Enable the app to add the user to a specific guild. + """Enable the application to add the user to a specific guild. !!! note This requires you to have set up a bot account for your application. """ IDENTIFY = "identify" - """Enable the app to view info about itself. + """Enable the application to view info about itself. !!! note This does not include email address info. Use the `EMAIL` scope instead @@ -135,31 +136,31 @@ class OAuth2Scope(str, more_enums.Enum): """ RELATIONSHIPS_READ = "relationships.read" - """Enable the app to view a user's friend list. + """Enable the application to view a user's friend list. !!! note You must be whitelisted to use this scope. """ RPC = "rpc" - """Enable the RPC app to control the local user's Discord client. + """Enable the RPC application to control the local user's Discord client. !!! note You must be whitelisted to use this scope. """ RPC_API = "rpc.api" - """Enable the RPC app to access the RPC API as the local user. + """Enable the RPC application to access the RPC API as the local user. !!! note You must be whitelisted to use this scope. """ RPC_MESSAGES_READ = "messages.read" - """Enable the RPC app to read messages from all channels the user is in.""" + """Enable the RPC application to read messages from all channels the user is in.""" RPC_NOTIFICATIONS_READ = "rpc.notifications.read" - """Enable the RPC app to read from all channels the user is in. + """Enable the RPC application to read from all channels the user is in. !!! note You must be whitelisted to use this scope. @@ -487,7 +488,7 @@ def format_icon_url(self, *, format_: str = "png", size: int = 4096) -> typing.O If `size` is not a power of two or not between 16 and 4096. """ if self.icon_hash: - return urls.generate_cdn_url("app-icons", str(self.id), self.icon_hash, format_=format_, size=size) + return urls.generate_cdn_url("application-icons", str(self.id), self.icon_hash, format_=format_, size=size) return None @property @@ -518,5 +519,7 @@ def format_cover_image_url(self, *, format_: str = "png", size: int = 4096) -> t If `size` is not a power of two or not between 16 and 4096. """ if self.cover_image_hash: - return urls.generate_cdn_url("app-assets", str(self.id), self.cover_image_hash, format_=format_, size=size) + return urls.generate_cdn_url( + "application-assets", str(self.id), self.cover_image_hash, format_=format_, size=size + ) return None diff --git a/hikari/audit_logs.py b/hikari/models/audit_logs.py similarity index 94% rename from hikari/audit_logs.py rename to hikari/models/audit_logs.py index 50fe7ab4f5..28db51c800 100644 --- a/hikari/audit_logs.py +++ b/hikari/models/audit_logs.py @@ -16,7 +16,7 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . -"""Components and entities that are used to describe audit logs on Discord.""" +"""Application and entities that are used to describe audit logs on Discord.""" from __future__ import annotations @@ -46,20 +46,21 @@ import attr -from hikari import bases -from hikari import channels -from hikari import colors -from hikari import guilds -from hikari import permissions -from hikari import users as _users -from hikari import webhooks as _webhooks from hikari.internal import conversions from hikari.internal import marshaller from hikari.internal import more_collections from hikari.internal import more_enums +from . import bases +from . import channels +from . import colors +from . import guilds +from . import permissions +from . import users as users_ +from . import webhooks as webhooks_ + if typing.TYPE_CHECKING: - from hikari.clients import components as _components + from hikari.components import application from hikari.internal import more_typing @@ -497,14 +498,14 @@ def _deserialize_integrations( def _deserialize_users( payload: more_typing.JSONArray, **kwargs: typing.Any -) -> typing.Mapping[bases.Snowflake, _users.User]: - return {bases.Snowflake(user["id"]): _users.User.deserialize(user, **kwargs) for user in payload} +) -> typing.Mapping[bases.Snowflake, users_.User]: + return {bases.Snowflake(user["id"]): users_.User.deserialize(user, **kwargs) for user in payload} def _deserialize_webhooks( payload: more_typing.JSONArray, **kwargs: typing.Any -) -> typing.Mapping[bases.Snowflake, _webhooks.Webhook]: - return {bases.Snowflake(webhook["id"]): _webhooks.Webhook.deserialize(webhook, **kwargs) for webhook in payload} +) -> typing.Mapping[bases.Snowflake, webhooks_.Webhook]: + return {bases.Snowflake(webhook["id"]): webhooks_.Webhook.deserialize(webhook, **kwargs) for webhook in payload} @marshaller.marshallable() @@ -522,12 +523,12 @@ class AuditLog(bases.Entity, marshaller.Deserializable): ) """A mapping of the partial objects of integrations found in this audit log.""" - users: typing.Mapping[bases.Snowflake, _users.User] = marshaller.attrib( + users: typing.Mapping[bases.Snowflake, users_.User] = marshaller.attrib( deserializer=_deserialize_users, inherit_kwargs=True ) """A mapping of the objects of users found in this audit log.""" - webhooks: typing.Mapping[bases.Snowflake, _webhooks.Webhook] = marshaller.attrib( + webhooks: typing.Mapping[bases.Snowflake, webhooks_.Webhook] = marshaller.attrib( deserializer=_deserialize_webhooks, inherit_kwargs=True, ) """A mapping of the objects of webhooks found in this audit log.""" @@ -541,8 +542,8 @@ class AuditLogIterator(typing.AsyncIterator[AuditLogEntry]): Parameters ---------- - components : hikari.clients.components.Components - The `hikari.clients.components.Components` that this should pass through + app : hikari.clients.application.Application + The `hikari.clients.application.Application` that this should pass through to the generated entities. request : typing.Callable[..., typing.Coroutine[typing.Any, typing.Any, typing.Any]] The prepared session bound partial function that this iterator should @@ -568,7 +569,7 @@ class AuditLogIterator(typing.AsyncIterator[AuditLogEntry]): __slots__ = ( "_buffer", - "_components", + "_app", "_front", "_limit", "_request", @@ -580,20 +581,20 @@ class AuditLogIterator(typing.AsyncIterator[AuditLogEntry]): integrations: typing.MutableMapping[bases.Snowflake, guilds.GuildIntegration] """A mapping of the partial integrations objects found in this log so far.""" - users: typing.MutableMapping[bases.Snowflake, _users.User] + users: typing.MutableMapping[bases.Snowflake, users_.User] """A mapping of the objects of users found in this audit log so far.""" - webhooks: typing.MutableMapping[bases.Snowflake, _webhooks.Webhook] + webhooks: typing.MutableMapping[bases.Snowflake, webhooks_.Webhook] """A mapping of the objects of webhooks found in this audit log so far.""" def __init__( self, - components: _components.Components, + app: application.Application, request: typing.Callable[..., more_typing.Coroutine[typing.Any]], before: typing.Optional[str] = str(bases.Snowflake.max()), limit: typing.Optional[int] = None, ) -> None: - self._components = components + self._app = app self._limit = limit self._buffer = [] self._request = request @@ -609,7 +610,7 @@ async def __anext__(self) -> AuditLogEntry: if not self._buffer and self._limit != 0: await self._fill() try: - entry = AuditLogEntry.deserialize(self._buffer.pop(), components=self._components) + entry = AuditLogEntry.deserialize(self._buffer.pop(), components=self._app) self._front = str(entry.id) return entry except IndexError: @@ -629,21 +630,18 @@ async def _fill(self) -> None: if users := payload.get("users"): self.users = copy.copy(self.users) self.users.update( - {bases.Snowflake(u["id"]): _users.User.deserialize(u, components=self._components) for u in users} + {bases.Snowflake(u["id"]): users_.User.deserialize(u, components=self._app) for u in users} ) if webhooks := payload.get("webhooks"): self.webhooks = copy.copy(self.webhooks) self.webhooks.update( - { - bases.Snowflake(w["id"]): _webhooks.Webhook.deserialize(w, components=self._components) - for w in webhooks - } + {bases.Snowflake(w["id"]): webhooks_.Webhook.deserialize(w, components=self._app) for w in webhooks} ) if integrations := payload.get("integrations"): self.integrations = copy.copy(self.integrations) self.integrations.update( { - bases.Snowflake(i["id"]): guilds.PartialGuildIntegration.deserialize(i, components=self._components) + bases.Snowflake(i["id"]): guilds.PartialGuildIntegration.deserialize(i, components=self._app) for i in integrations } ) diff --git a/hikari/bases.py b/hikari/models/bases.py similarity index 94% rename from hikari/bases.py rename to hikari/models/bases.py index 8e1ea208e8..edd005366b 100644 --- a/hikari/bases.py +++ b/hikari/models/bases.py @@ -33,7 +33,7 @@ if typing.TYPE_CHECKING: import datetime - from hikari.clients import components + from hikari.components import application @marshaller.marshallable() @@ -41,8 +41,8 @@ class Entity(abc.ABC): """The base for any entity used in this API.""" - _components: typing.Optional[components.Components] = attr.attrib(default=None, repr=False, eq=False, hash=False) - """The client components that models may use for procedures.""" + _components: typing.Optional[application.Application] = attr.attrib(default=None, repr=False, eq=False, hash=False) + """The client application that models may use for procedures.""" class Snowflake(int): diff --git a/hikari/channels.py b/hikari/models/channels.py similarity index 98% rename from hikari/channels.py rename to hikari/models/channels.py index 9cea7abfff..3242387c72 100644 --- a/hikari/channels.py +++ b/hikari/models/channels.py @@ -16,7 +16,7 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . -"""Components and entities that are used to describe both DMs and guild channels on Discord.""" +"""Application and entities that are used to describe both DMs and guild channels on Discord.""" from __future__ import annotations @@ -41,15 +41,16 @@ import attr -from hikari import bases -from hikari import permissions -from hikari import users from hikari.internal import conversions from hikari.internal import marshaller from hikari.internal import more_collections from hikari.internal import more_enums from hikari.internal import urls +from . import bases +from . import permissions +from . import users + if typing.TYPE_CHECKING: from hikari.internal import more_typing @@ -152,7 +153,7 @@ def decorator(cls): class PartialChannel(bases.Unique, marshaller.Deserializable): """Represents a channel where we've only received it's basic information. - This is commonly received in REST responses. + This is commonly received in RESTSession responses. """ name: typing.Optional[str] = marshaller.attrib( diff --git a/hikari/colors.py b/hikari/models/colors.py similarity index 100% rename from hikari/colors.py rename to hikari/models/colors.py diff --git a/hikari/colours.py b/hikari/models/colours.py similarity index 95% rename from hikari/colours.py rename to hikari/models/colours.py index 1aec36fe62..f72fbf2563 100644 --- a/hikari/colours.py +++ b/hikari/models/colours.py @@ -22,4 +22,4 @@ __all__ = ["Colour"] -from hikari.colors import Color as Colour +from .colors import Color as Colour diff --git a/hikari/embeds.py b/hikari/models/embeds.py similarity index 99% rename from hikari/embeds.py rename to hikari/models/embeds.py index 7f0ad52483..a9b6943313 100644 --- a/hikari/embeds.py +++ b/hikari/models/embeds.py @@ -16,7 +16,7 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . -"""Components and entities that are used to describe message embeds on Discord.""" +"""Application and entities that are used to describe message embeds on Discord.""" from __future__ import annotations @@ -36,12 +36,13 @@ import attr -from hikari import bases -from hikari import colors -from hikari import files from hikari.internal import conversions from hikari.internal import marshaller +from . import bases +from . import colors +from . import files + if typing.TYPE_CHECKING: from hikari.internal import more_typing diff --git a/hikari/emojis.py b/hikari/models/emojis.py similarity index 98% rename from hikari/emojis.py rename to hikari/models/emojis.py index cb97105333..ede4b81b02 100644 --- a/hikari/emojis.py +++ b/hikari/models/emojis.py @@ -16,7 +16,7 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . -"""Components and entities that are used to describe both custom and Unicode emojis on Discord.""" +"""Application and entities that are used to describe both custom and Unicode emojis on Discord.""" from __future__ import annotations @@ -28,12 +28,13 @@ import attr -from hikari import bases -from hikari import files -from hikari import users from hikari.internal import marshaller from hikari.internal import urls +from . import bases +from . import files +from . import users + if typing.TYPE_CHECKING: from hikari.internal import more_typing diff --git a/hikari/files.py b/hikari/models/files.py similarity index 99% rename from hikari/files.py rename to hikari/models/files.py index 559f3f6046..9d7fb5dfa5 100644 --- a/hikari/files.py +++ b/hikari/models/files.py @@ -16,7 +16,7 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . -"""Components used to make uploading data simpler. +"""Application used to make uploading data simpler. What should I use? ------------------ @@ -66,8 +66,8 @@ import aiohttp -from hikari import errors from hikari.internal import more_asyncio +from hikari import errors # XXX: find optimal size. MAGIC_NUMBER: typing.Final[int] = 128 * 1024 diff --git a/hikari/gateway_entities.py b/hikari/models/gateway.py similarity index 98% rename from hikari/gateway_entities.py rename to hikari/models/gateway.py index 33cb4653bb..19de548588 100644 --- a/hikari/gateway_entities.py +++ b/hikari/models/gateway.py @@ -27,10 +27,11 @@ import attr -from hikari import bases -from hikari import guilds from hikari.internal import marshaller +from . import bases +from . import guilds + def _rest_after_deserializer(after: int) -> datetime.timedelta: return datetime.timedelta(milliseconds=after) diff --git a/hikari/guilds.py b/hikari/models/guilds.py similarity index 97% rename from hikari/guilds.py rename to hikari/models/guilds.py index a89c5f5bfc..a9b950dc8a 100644 --- a/hikari/guilds.py +++ b/hikari/models/guilds.py @@ -16,7 +16,7 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . -"""Components and entities that are used to describe guilds on Discord.""" +"""Application and entities that are used to describe guilds on Discord.""" from __future__ import annotations @@ -59,18 +59,19 @@ import attr -from hikari import bases -from hikari import channels as _channels -from hikari import colors -from hikari import emojis as _emojis -from hikari import permissions as _permissions -from hikari import unset -from hikari import users from hikari.internal import conversions from hikari.internal import marshaller from hikari.internal import more_enums from hikari.internal import urls +from . import bases +from . import channels as channels_ +from . import colors +from . import emojis as emojis_ +from . import permissions as permissions_ +from . import unset +from . import users + if typing.TYPE_CHECKING: from hikari.internal import more_typing @@ -314,8 +315,8 @@ class GuildRole(PartialGuildRole, marshaller.Serializable): position: int = marshaller.attrib(deserializer=int, serializer=int, default=None, eq=False, hash=False, repr=True) """The position of this role in the role hierarchy.""" - permissions: _permissions.Permission = marshaller.attrib( - deserializer=_permissions.Permission, serializer=int, default=_permissions.Permission(0), eq=False, hash=False + permissions: permissions_.Permission = marshaller.attrib( + deserializer=permissions_.Permission, serializer=int, default=permissions_.Permission(0), eq=False, hash=False ) """The guild wide permissions this role gives to the members it's attached to, @@ -498,8 +499,8 @@ class PresenceActivity(bases.Entity, marshaller.Deserializable): state: typing.Optional[str] = marshaller.attrib(deserializer=str, if_undefined=None, if_none=None, default=None) """The current status of this activity's target, if set.""" - emoji: typing.Union[None, _emojis.UnicodeEmoji, _emojis.CustomEmoji] = marshaller.attrib( - deserializer=_emojis.deserialize_reaction_emoji, if_undefined=None, default=None, inherit_kwargs=True + emoji: typing.Union[None, emojis_.UnicodeEmoji, emojis_.CustomEmoji] = marshaller.attrib( + deserializer=emojis_.deserialize_reaction_emoji, if_undefined=None, default=None, inherit_kwargs=True ) """The emoji of this activity, if it is a custom status and set.""" @@ -946,8 +947,8 @@ def icon_url(self) -> typing.Optional[str]: def _deserialize_emojis( payload: more_typing.JSONArray, **kwargs: typing.Any -) -> typing.Mapping[bases.Snowflake, _emojis.KnownCustomEmoji]: - return {bases.Snowflake(emoji["id"]): _emojis.KnownCustomEmoji.deserialize(emoji, **kwargs) for emoji in payload} +) -> typing.Mapping[bases.Snowflake, emojis_.KnownCustomEmoji]: + return {bases.Snowflake(emoji["id"]): emojis_.KnownCustomEmoji.deserialize(emoji, **kwargs) for emoji in payload} @marshaller.marshallable() @@ -965,7 +966,7 @@ class GuildPreview(PartialGuild): ) """The hash of the discovery splash for the guild, if there is one.""" - emojis: typing.Mapping[bases.Snowflake, _emojis.KnownCustomEmoji] = marshaller.attrib( + emojis: typing.Mapping[bases.Snowflake, emojis_.KnownCustomEmoji] = marshaller.attrib( deserializer=_deserialize_emojis, inherit_kwargs=True, eq=False, hash=False, ) """The mapping of IDs to the emojis this guild provides.""" @@ -1062,8 +1063,8 @@ def _deserialize_members( def _deserialize_channels( payload: more_typing.JSONArray, **kwargs: typing.Any -) -> typing.Mapping[bases.Snowflake, _channels.GuildChannel]: - return {bases.Snowflake(channel["id"]): _channels.deserialize_channel(channel, **kwargs) for channel in payload} +) -> typing.Mapping[bases.Snowflake, channels_.GuildChannel]: + return {bases.Snowflake(channel["id"]): channels_.deserialize_channel(channel, **kwargs) for channel in payload} def _deserialize_presences( @@ -1100,9 +1101,9 @@ class Guild(PartialGuild): # pylint:disable=too-many-instance-attributes owner_id: bases.Snowflake = marshaller.attrib(deserializer=bases.Snowflake, eq=False, hash=False, repr=True) """The ID of the owner of this guild.""" - my_permissions: _permissions.Permission = marshaller.attrib( + my_permissions: permissions_.Permission = marshaller.attrib( raw_name="permissions", - deserializer=_permissions.Permission, + deserializer=permissions_.Permission, if_undefined=None, default=None, eq=False, @@ -1113,7 +1114,7 @@ class Guild(PartialGuild): # pylint:disable=too-many-instance-attributes This will not take into account permission overwrites or implied permissions (for example, ADMINISTRATOR implies all other permissions). - This will be `None` when this object is retrieved through a REST request + This will be `None` when this object is retrieved through a RESTSession request rather than from the gateway. """ @@ -1172,7 +1173,7 @@ class Guild(PartialGuild): # pylint:disable=too-many-instance-attributes ) """The roles in this guild, represented as a mapping of ID to role object.""" - emojis: typing.Mapping[bases.Snowflake, _emojis.KnownCustomEmoji] = marshaller.attrib( + emojis: typing.Mapping[bases.Snowflake, emojis_.KnownCustomEmoji] = marshaller.attrib( deserializer=_deserialize_emojis, inherit_kwargs=True, eq=False, hash=False, ) """A mapping of IDs to the objects of the emojis this guild provides.""" @@ -1297,7 +1298,7 @@ class Guild(PartialGuild): # pylint:disable=too-many-instance-attributes query the members using the appropriate API call instead. """ - channels: typing.Optional[typing.Mapping[bases.Snowflake, _channels.GuildChannel]] = marshaller.attrib( + channels: typing.Optional[typing.Mapping[bases.Snowflake, channels_.GuildChannel]] = marshaller.attrib( deserializer=_deserialize_channels, if_undefined=None, inherit_kwargs=True, default=None, eq=False, hash=False, ) """A mapping of ID to the corresponding guild channels in this guild. @@ -1417,7 +1418,7 @@ class Guild(PartialGuild): # pylint:disable=too-many-instance-attributes ) """The approximate number of members in the guild. - This information will be provided by REST API calls fetching the guilds that + This information will be provided by RESTSession API calls fetching the guilds that a bot account is in. For all other purposes, this should be expected to remain `None`. """ @@ -1427,7 +1428,7 @@ class Guild(PartialGuild): # pylint:disable=too-many-instance-attributes ) """The approximate number of members in the guild that are not offline. - This information will be provided by REST API calls fetching the guilds that + This information will be provided by RESTSession API calls fetching the guilds that a bot account is in. For all other purposes, this should be expected to remain `None`. """ diff --git a/hikari/intents.py b/hikari/models/intents.py similarity index 100% rename from hikari/intents.py rename to hikari/models/intents.py diff --git a/hikari/invites.py b/hikari/models/invites.py similarity index 98% rename from hikari/invites.py rename to hikari/models/invites.py index 554520644f..aae9ee3147 100644 --- a/hikari/invites.py +++ b/hikari/models/invites.py @@ -16,7 +16,7 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . -"""Components and entities that are used to describe invites on Discord.""" +"""Application and entities that are used to describe invites on Discord.""" from __future__ import annotations @@ -27,15 +27,16 @@ import attr -from hikari import bases -from hikari import channels -from hikari import guilds -from hikari import users from hikari.internal import conversions from hikari.internal import marshaller from hikari.internal import more_enums from hikari.internal import urls +from . import bases +from . import channels +from . import guilds +from . import users + @more_enums.must_be_unique class TargetUserType(int, more_enums.Enum): diff --git a/hikari/messages.py b/hikari/models/messages.py similarity index 95% rename from hikari/messages.py rename to hikari/models/messages.py index 0ba2fb70b8..684f4e4997 100644 --- a/hikari/messages.py +++ b/hikari/models/messages.py @@ -16,7 +16,7 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . -"""Components and entities that are used to describe messages on Discord.""" +"""Application and entities that are used to describe messages on Discord.""" from __future__ import annotations @@ -35,21 +35,21 @@ import attr -from hikari import applications -from hikari import bases -from hikari import embeds as _embeds -from hikari import emojis as _emojis -from hikari import files as _files -from hikari import guilds -from hikari import users from hikari.internal import conversions from hikari.internal import marshaller from hikari.internal import more_enums +from . import applications +from . import bases +from . import channels +from . import embeds as embeds_ +from . import emojis as emojis_ +from . import files as files_ +from . import guilds +from . import users if typing.TYPE_CHECKING: import datetime - from hikari import channels from hikari.internal import more_typing @@ -142,7 +142,7 @@ class MessageActivityType(int, more_enums.Enum): @marshaller.marshallable() @attr.s(eq=True, hash=False, kw_only=True, slots=True) -class Attachment(bases.Unique, _files.BaseStream, marshaller.Deserializable): +class Attachment(bases.Unique, files_.BaseStream, marshaller.Deserializable): """Represents a file attached to a message. You can use this object in the same way as a @@ -169,7 +169,7 @@ class Attachment(bases.Unique, _files.BaseStream, marshaller.Deserializable): """The width of the image (if the file is an image).""" def __aiter__(self) -> typing.AsyncGenerator[bytes]: - return _files.WebResourceStream(self.filename, self.url).__aiter__() + return files_.WebResourceStream(self.filename, self.url).__aiter__() @marshaller.marshallable() @@ -180,8 +180,8 @@ class Reaction(bases.Entity, marshaller.Deserializable): count: int = marshaller.attrib(deserializer=int, eq=False, hash=False, repr=True) """The amount of times the emoji has been used to react.""" - emoji: typing.Union[_emojis.UnicodeEmoji, _emojis.CustomEmoji] = marshaller.attrib( - deserializer=_emojis.deserialize_reaction_emoji, inherit_kwargs=True, eq=True, hash=True, repr=True + emoji: typing.Union[emojis_.UnicodeEmoji, emojis_.CustomEmoji] = marshaller.attrib( + deserializer=emojis_.deserialize_reaction_emoji, inherit_kwargs=True, eq=True, hash=True, repr=True ) """The emoji used to react.""" @@ -244,8 +244,8 @@ def _deserialize_attachments(payload: more_typing.JSONArray, **kwargs: typing.An return [Attachment.deserialize(attachment, **kwargs) for attachment in payload] -def _deserialize_embeds(payload: more_typing.JSONArray, **kwargs: typing.Any) -> typing.Sequence[_embeds.Embed]: - return [_embeds.Embed.deserialize(embed, **kwargs) for embed in payload] +def _deserialize_embeds(payload: more_typing.JSONArray, **kwargs: typing.Any) -> typing.Sequence[embeds_.Embed]: + return [embeds_.Embed.deserialize(embed, **kwargs) for embed in payload] def _deserialize_reactions(payload: more_typing.JSONArray, **kwargs: typing.Any) -> typing.Sequence[Reaction]: @@ -330,7 +330,7 @@ class Message(bases.Unique, marshaller.Deserializable): ) """The message attachments.""" - embeds: typing.Sequence[_embeds.Embed] = marshaller.attrib( + embeds: typing.Sequence[embeds_.Embed] = marshaller.attrib( deserializer=_deserialize_embeds, inherit_kwargs=True, eq=False, hash=False ) """The message embeds.""" @@ -415,7 +415,7 @@ async def edit( # pylint:disable=line-too-long self, *, content: str = ..., - embed: _embeds.Embed = ..., + embed: embeds_.Embed = ..., mentions_everyone: bool = True, user_mentions: typing.Union[ typing.Collection[typing.Union[bases.Snowflake, int, str, users.User]], bool @@ -486,7 +486,7 @@ async def safe_edit( self, *, content: str = ..., - embed: _embeds.Embed = ..., + embed: embeds_.Embed = ..., mentions_everyone: bool = False, user_mentions: typing.Union[ typing.Collection[typing.Union[bases.Snowflake, int, str, users.User]], bool @@ -514,8 +514,8 @@ async def reply( # pylint:disable=line-too-long self, *, content: str = ..., - embed: _embeds.Embed = ..., - files: typing.Sequence[_files.BaseStream] = ..., + embed: embeds_.Embed = ..., + files: typing.Sequence[files_.BaseStream] = ..., mentions_everyone: bool = True, user_mentions: typing.Union[ typing.Collection[typing.Union[bases.Snowflake, int, str, users.User]], bool @@ -597,8 +597,8 @@ async def safe_reply( self, *, content: str = ..., - embed: _embeds.Embed = ..., - files: typing.Sequence[_files.BaseStream] = ..., + embed: embeds_.Embed = ..., + files: typing.Sequence[files_.BaseStream] = ..., mentions_everyone: bool = False, user_mentions: typing.Union[ typing.Collection[typing.Union[bases.Snowflake, int, str, users.User]], bool @@ -639,7 +639,7 @@ async def delete(self) -> None: """ await self._components.rest.delete_messages(channel=self.channel_id, message=self.id) - async def add_reaction(self, emoji: typing.Union[str, _emojis.Emoji]) -> None: + async def add_reaction(self, emoji: typing.Union[str, emojis_.Emoji]) -> None: r"""Add a reaction to this message. Parameters @@ -678,7 +678,7 @@ async def add_reaction(self, emoji: typing.Union[str, _emojis.Emoji]) -> None: await self._components.rest.add_reaction(channel=self.channel_id, message=self.id, emoji=emoji) async def remove_reaction( - self, emoji: typing.Union[str, _emojis.Emoji], *, user: typing.Optional[users.User] = None + self, emoji: typing.Union[str, emojis_.Emoji], *, user: typing.Optional[users.User] = None ) -> None: r"""Remove a reaction from this message. @@ -721,7 +721,7 @@ async def remove_reaction( """ await self._components.rest.remove_reaction(channel=self.channel_id, message=self.id, emoji=emoji, user=user) - async def remove_all_reactions(self, emoji: typing.Optional[typing.Union[str, _emojis.Emoji]] = None) -> None: + async def remove_all_reactions(self, emoji: typing.Optional[typing.Union[str, emojis_.Emoji]] = None) -> None: r"""Remove all users' reactions for a specific emoji from the message. Parameters diff --git a/hikari/pagination.py b/hikari/models/pagination.py similarity index 98% rename from hikari/pagination.py rename to hikari/models/pagination.py index a3c3fe0846..ff8a8075ab 100644 --- a/hikari/pagination.py +++ b/hikari/models/pagination.py @@ -23,9 +23,10 @@ import datetime import typing -from hikari import bases from hikari.internal import more_collections +from . import bases + _T = typing.TypeVar("_T") @@ -161,7 +162,7 @@ def _prepare_first_id(value, if_none=bases.Snowflake.min()) -> str: Given an object with an ID, a datetime, an int, a snowflake, or a string type, convert the element to the string ID snowflake it represents - that can be passed to the underlying REST API safely. + that can be passed to the underlying RESTSession API safely. Parameters ---------- diff --git a/hikari/permissions.py b/hikari/models/permissions.py similarity index 100% rename from hikari/permissions.py rename to hikari/models/permissions.py diff --git a/hikari/unset.py b/hikari/models/unset.py similarity index 100% rename from hikari/unset.py rename to hikari/models/unset.py diff --git a/hikari/users.py b/hikari/models/users.py similarity index 98% rename from hikari/users.py rename to hikari/models/users.py index 29a9c95210..60f9983379 100644 --- a/hikari/users.py +++ b/hikari/models/users.py @@ -16,7 +16,7 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . -"""Components and entities that are used to describe Users on Discord.""" +"""Application and entities that are used to describe Users on Discord.""" from __future__ import annotations @@ -26,10 +26,10 @@ import attr -from hikari import bases from hikari.internal import marshaller from hikari.internal import more_enums from hikari.internal import urls +from . import bases @more_enums.must_be_unique diff --git a/hikari/voices.py b/hikari/models/voices.py similarity index 97% rename from hikari/voices.py rename to hikari/models/voices.py index 3c51e3bf2c..0f866ec4bd 100644 --- a/hikari/voices.py +++ b/hikari/models/voices.py @@ -16,7 +16,7 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . -"""Components and entities that are used to describe voice states on Discord.""" +"""Application and entities that are used to describe voice states on Discord.""" from __future__ import annotations @@ -26,9 +26,9 @@ import attr -from hikari import bases -from hikari import guilds from hikari.internal import marshaller +from . import bases +from . import guilds @marshaller.marshallable() diff --git a/hikari/webhooks.py b/hikari/models/webhooks.py similarity index 92% rename from hikari/webhooks.py rename to hikari/models/webhooks.py index c464dfe41f..3e99cc32e9 100644 --- a/hikari/webhooks.py +++ b/hikari/models/webhooks.py @@ -16,7 +16,7 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . -"""Components and entities that are used to describe webhooks on Discord.""" +"""Application and entities that are used to describe webhooks on Discord.""" from __future__ import annotations @@ -26,18 +26,19 @@ import attr -from hikari import bases -from hikari import users from hikari.internal import marshaller from hikari.internal import more_enums from hikari.internal import urls +from . import bases +from . import users as users_ + if typing.TYPE_CHECKING: - from hikari import channels as _channels - from hikari import embeds as _embeds - from hikari import files as _files - from hikari import guilds as _guilds - from hikari import messages as _messages + from . import channels as channels_ + from . import embeds as embeds_ + from . import files as files_ + from . import guilds as guilds_ + from . import messages as messages_ @more_enums.must_be_unique @@ -72,9 +73,9 @@ class Webhook(bases.Unique, marshaller.Deserializable): channel_id: bases.Snowflake = marshaller.attrib(deserializer=bases.Snowflake, eq=False, hash=False, repr=True) """The channel ID this webhook is for.""" - author: typing.Optional[users.User] = marshaller.attrib( + author: typing.Optional[users_.User] = marshaller.attrib( raw_name="user", - deserializer=users.User.deserialize, + deserializer=users_.User.deserialize, if_undefined=None, inherit_kwargs=True, default=None, @@ -115,16 +116,16 @@ async def execute( avatar_url: str = ..., tts: bool = ..., wait: bool = False, - files: typing.Sequence[_files.BaseStream] = ..., - embeds: typing.Sequence[_embeds.Embed] = ..., + files: typing.Sequence[files_.BaseStream] = ..., + embeds: typing.Sequence[embeds_.Embed] = ..., mentions_everyone: bool = True, user_mentions: typing.Union[ - typing.Collection[typing.Union[bases.Snowflake, int, str, users.User]], bool + typing.Collection[typing.Union[bases.Snowflake, int, str, users_.User]], bool ] = True, role_mentions: typing.Union[ - typing.Collection[typing.Union[bases.Snowflake, int, str, _guilds.GuildRole]], bool + typing.Collection[typing.Union[bases.Snowflake, int, str, guilds_.GuildRole]], bool ] = True, - ) -> typing.Optional[_messages.Message]: + ) -> typing.Optional[messages_.Message]: """Execute the webhook to create a message. Parameters @@ -207,16 +208,16 @@ async def safe_execute( avatar_url: str = ..., tts: bool = ..., wait: bool = False, - files: typing.Sequence[_files.BaseStream] = ..., - embeds: typing.Sequence[_embeds.Embed] = ..., + files: typing.Sequence[files_.BaseStream] = ..., + embeds: typing.Sequence[embeds_.Embed] = ..., mentions_everyone: bool = False, user_mentions: typing.Union[ - typing.Collection[typing.Union[bases.Snowflake, int, str, users.User]], bool + typing.Collection[typing.Union[bases.Snowflake, int, str, users_.User]], bool ] = False, role_mentions: typing.Union[ - typing.Collection[typing.Union[bases.Snowflake, int, str, _guilds.GuildRole]], bool + typing.Collection[typing.Union[bases.Snowflake, int, str, guilds_.GuildRole]], bool ] = False, - ) -> typing.Optional[_messages.Message]: + ) -> typing.Optional[messages_.Message]: """Execute the webhook to create a message with mention safety. This endpoint has the same signature as @@ -275,8 +276,8 @@ async def edit( self, *, name: str = ..., - avatar: typing.Optional[_files.BaseStream] = ..., - channel: typing.Union[bases.Snowflake, int, str, _channels.GuildChannel] = ..., + avatar: typing.Optional[files_.BaseStream] = ..., + channel: typing.Union[bases.Snowflake, int, str, channels_.GuildChannel] = ..., reason: str = ..., use_token: typing.Optional[bool] = None, ) -> Webhook: @@ -337,7 +338,7 @@ async def edit( reason=reason, ) - async def fetch_channel(self) -> _channels.PartialChannel: + async def fetch_channel(self) -> channels_.PartialChannel: """Fetch the channel this webhook is for. Returns @@ -354,7 +355,7 @@ async def fetch_channel(self) -> _channels.PartialChannel: """ return await self._components.rest.fetch_channel(channel=self.channel_id) - async def fetch_guild(self) -> _guilds.Guild: + async def fetch_guild(self) -> guilds_.Guild: """Fetch the guild this webhook belongs to. Returns diff --git a/hikari/rest/__init__.py b/hikari/rest/__init__.py new file mode 100644 index 0000000000..4a92641af1 --- /dev/null +++ b/hikari/rest/__init__.py @@ -0,0 +1,25 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# Copyright © Nekoka.tt 2019-2020 +# +# This file is part of Hikari. +# +# Hikari is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Hikari is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Hikari. If not, see . + +from __future__ import annotations + +from .client import * +from .session import * + +__all__ = client.__all__ + session.__all__ diff --git a/hikari/clients/rest/base.py b/hikari/rest/base.py similarity index 79% rename from hikari/clients/rest/base.py rename to hikari/rest/base.py index 9177dce3e9..a0d462bc75 100644 --- a/hikari/clients/rest/base.py +++ b/hikari/rest/base.py @@ -16,7 +16,7 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . -"""The abstract class that all REST client logic classes should inherit from.""" +"""The abstract class that all RESTSession client logic classes should inherit from.""" from __future__ import annotations @@ -30,22 +30,23 @@ if typing.TYPE_CHECKING: import types - from hikari.clients import components as _components - from hikari.net import rest + from hikari.components import application + + from . import session as rest_session class BaseRESTComponent(abc.ABC, metaclass=meta.UniqueFunctionMeta): - """An abstract class that all REST client logic classes should inherit from. + """An abstract class that all RESTSession client logic classes should inherit from. This defines the abstract method `__init__` which will assign an instance - of `hikari.net.rest.REST` to the attribute that all components will expect + of `hikari.net.rest.RESTSession` to the attribute that all application will expect to make calls to. """ @abc.abstractmethod - def __init__(self, components: _components.Components, session: rest.REST) -> None: + def __init__(self, components: application.Application, session: rest_session.RESTSession) -> None: self._components = components - self._session: rest.REST = session + self._session = session async def __aenter__(self) -> BaseRESTComponent: return self @@ -56,7 +57,7 @@ async def __aexit__( await self.close() async def close(self) -> None: - """Shut down the REST client safely.""" + """Shut down the RESTSession client safely.""" await self._session.close() @property diff --git a/hikari/rest/buckets.py b/hikari/rest/buckets.py new file mode 100644 index 0000000000..74c3a8de12 --- /dev/null +++ b/hikari/rest/buckets.py @@ -0,0 +1,405 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# Copyright © Nekoka.tt 2019-2020 +# +# This file is part of Hikari. +# +# Hikari is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Hikari is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Hikari. If not, see . +from __future__ import annotations + +import asyncio +import datetime +import logging +import time +import types +import typing + +from hikari.internal import more_asyncio +from hikari.internal import more_typing +from hikari.internal import ratelimits + +from . import routes + +UNKNOWN_HASH: typing.Final[str] = "UNKNOWN" +"""The hash used for an unknown bucket that has not yet been resolved.""" + + +class RESTBucket(ratelimits.WindowedBurstRateLimiter): + """Represents a rate limit for an RESTSession endpoint. + + Component to represent an active rate limit bucket on a specific RESTSession route + with a specific major parameter combo. + + This is somewhat similar to the `WindowedBurstRateLimiter` in how it + works. + + This algorithm will use fixed-period time windows that have a given limit + (capacity). Each time a task requests processing time, it will drip another + unit into the bucket. Once the bucket has reached its limit, nothing can + drip and new tasks will be queued until the time window finishes. + + Once the time window finishes, the bucket will empty, returning the current + capacity to zero, and tasks that are queued will start being able to drip + again. + + Additional logic is provided by the `RESTBucket.update_rate_limit` call + which allows dynamically changing the enforced rate limits at any time. + """ + + __slots__ = ("compiled_route",) + + compiled_route: typing.Final[routes.CompiledRoute] + """The compiled route that this rate limit is covering.""" + + def __init__(self, name: str, compiled_route: routes.CompiledRoute) -> None: + super().__init__(name, 1, 1) + self.compiled_route = compiled_route + + @property + def is_unknown(self) -> bool: + """Return `True` if the bucket represents an `UNKNOWN` bucket.""" + return self.name.startswith(UNKNOWN_HASH) + + def acquire(self) -> more_typing.Future[None]: + """Acquire time on this rate limiter. + + !!! note + You should afterwards invoke `RESTBucket.update_rate_limit` to + update any rate limit information you are made aware of. + + Returns + ------- + asyncio.Future + A future that should be awaited immediately. Once the future completes, + you are allowed to proceed with your operation. + """ + return more_asyncio.completed_future(None) if self.is_unknown else super().acquire() + + def update_rate_limit(self, remaining: int, limit: int, reset_at: float) -> None: + """Amend the rate limit. + + Parameters + ---------- + remaining : int + The calls remaining in this time window. + limit : int + The total calls allowed in this time window. + reset_at : float + The epoch at which to reset the limit. + + !!! note + The `reset_at` epoch is expected to be a `time.perf_counter` + monotonic epoch, rather than a `time.time` date-based epoch. + """ + self.remaining = remaining + self.limit = limit + self.reset_at = reset_at + self.period = max(0.0, self.reset_at - time.perf_counter()) + + def drip(self) -> None: + """Decrement the remaining count for this bucket. + + !!! note + If the bucket is marked as `RESTBucket.is_unknown`, then this will + not do anything. `Unknown` buckets have infinite rate limits. + """ + # We don't drip unknown buckets: we can't rate limit them as we don't know their real bucket hash or + # the current rate limit values Discord put on them... + if not self.is_unknown: + self.remaining -= 1 + + +class RESTBucketManager: + """The main rate limiter implementation for RESTSession clients. + + This is designed to provide bucketed rate limiting for Discord RESTSession + endpoints that respects the `X-RateLimit-Bucket` rate limit header. To do + this, it makes the assumption that any limit can change at any time. + """ + + _POLL_PERIOD: typing.Final[typing.ClassVar[int]] = 20 + _EXPIRE_PERIOD: typing.Final[typing.ClassVar[int]] = 10 + + __slots__ = ( + "routes_to_hashes", + "real_hashes_to_buckets", + "closed_event", + "gc_task", + "logger", + ) + + routes_to_hashes: typing.Final[typing.MutableMapping[routes.Route, str]] + """Maps routes to their `X-RateLimit-Bucket` header being used.""" + + real_hashes_to_buckets: typing.Final[typing.MutableMapping[str, RESTBucket]] + """Maps full bucket hashes (`X-RateLimit-Bucket` appended with a hash of + major parameters used in that compiled route) to their corresponding rate + limiters. + """ + + closed_event: typing.Final[asyncio.Event] + """An internal event that is set when the object is shut down.""" + + gc_task: typing.Optional[more_typing.Task[None]] + """The internal garbage collector task.""" + + logger: typing.Final[logging.Logger] + """The logger to use for this object.""" + + def __init__(self) -> None: + self.routes_to_hashes = {} + self.real_hashes_to_buckets = {} + self.closed_event: asyncio.Event = asyncio.Event() + self.gc_task: typing.Optional[asyncio.Task] = None + self.logger = logging.getLogger("hikari.rest.ratelimits.RESTBucketManager") + + def __enter__(self) -> RESTBucketManager: + return self + + def __exit__(self, exc_type: typing.Type[Exception], exc_val: Exception, exc_tb: types.TracebackType) -> None: + self.close() + + def __del__(self) -> None: + self.close() + + def start(self, poll_period: float = _POLL_PERIOD, expire_after: float = _EXPIRE_PERIOD) -> None: + """Start this ratelimiter up. + + This spins up internal garbage collection logic in the background to + keep memory usage to an optimal level as old routes and bucket hashes + get discarded and replaced. + + Parameters + ---------- + poll_period : float + Period to poll the garbage collector at in seconds. Defaults + to `20` seconds. + expire_after : float + Time after which the last `reset_at` was hit for a bucket to + remove it. Higher values will retain unneeded ratelimit info for + longer, but may produce more effective ratelimiting logic as a + result. Using `0` will make the bucket get garbage collected as soon + as the rate limit has reset. Defaults to `10` seconds. + """ + if not self.gc_task: + self.gc_task = asyncio.get_running_loop().create_task(self.gc(poll_period, expire_after)) + + def close(self) -> None: + """Close the garbage collector and kill any tasks waiting on ratelimits. + + Once this has been called, this object is considered to be effectively + dead. To reuse it, one should create a new instance. + """ + self.closed_event.set() + for bucket in self.real_hashes_to_buckets.values(): + bucket.close() + self.real_hashes_to_buckets.clear() + self.routes_to_hashes.clear() + + # Ignore docstring not starting in an imperative mood + async def gc(self, poll_period: float, expire_after: float) -> None: # noqa: D401 + """The garbage collector loop. + + This is designed to run in the background and manage removing unused + route references from the rate-limiter collection to save memory. + + This will run forever until `RESTBucketManager. closed_event` is set. + This will invoke `RESTBucketManager.do_gc_pass` periodically. + + Parameters + ---------- + poll_period : float + The period to poll at. + expire_after : float + Time after which the last `reset_at` was hit for a bucket to + remove it. Higher values will retain unneeded ratelimit info for + longer, but may produce more effective ratelimiting logic as a + result. Using `0` will make the bucket get garbage collected as soon + as the rate limit has reset. + + !!! warning + You generally have no need to invoke this directly. Use + `RESTBucketManager.start` and `RESTBucketManager.close` to control + this instead. + """ + # Prevent filling memory increasingly until we run out by removing dead buckets every 20s + # Allocations are somewhat cheap if we only do them every so-many seconds, after all. + self.logger.debug("rate limit garbage collector started") + while not self.closed_event.is_set(): + try: + await asyncio.wait_for(self.closed_event.wait(), timeout=poll_period) + except asyncio.TimeoutError: + self.logger.debug("performing rate limit garbage collection pass") + self.do_gc_pass(expire_after) + self.gc_task = None + + def do_gc_pass(self, expire_after: float) -> None: + """Perform a single garbage collection pass. + + This will assess any routes stored in the internal mappings of this + object and remove any that are deemed to be inactive or dead in order + to save memory. + + If the removed routes are used again in the future, they will be + re-cached automatically. + + Parameters + ---------- + expire_after : float + Time after which the last `reset_at` was hit for a bucket to + remove it. Defaults to `reset_at` + 20 seconds. Higher values will + retain unneeded ratelimit info for longer, but may produce more + effective ratelimiting logic as a result. + + !!! warning + You generally have no need to invoke this directly. Use + `RESTBucketManager.start` and `RESTBucketManager.close` to control + this instead. + """ + buckets_to_purge = [] + + now = time.perf_counter() + + # We have three main states that a bucket can be in: + # 1. active - the bucket is active and is not at risk of deallocation + # 2. survival - the bucket is inactive but is still fresh enough to be kept alive. + # 3. death - the bucket has been inactive for too long. + active = 0 + + # Discover and purge + bucket_pairs = self.real_hashes_to_buckets.items() + + for full_hash, bucket in bucket_pairs: + if bucket.is_empty and bucket.reset_at + expire_after < now: + # If it is still running a throttle and is in memory, it will remain in memory + # but we won't know about it. + buckets_to_purge.append(full_hash) + + if bucket.reset_at >= now: + active += 1 + + dead = len(buckets_to_purge) + total = len(bucket_pairs) + survival = total - active - dead + + for full_hash in buckets_to_purge: + self.real_hashes_to_buckets[full_hash].close() + del self.real_hashes_to_buckets[full_hash] + + self.logger.debug("purged %s stale buckets, %s remain in survival, %s active", dead, survival, active) + + def acquire(self, compiled_route: routes.CompiledRoute) -> more_typing.Future[None]: + """Acquire a bucket for the given route. + + Parameters + ---------- + compiled_route : hikari.net.routes.CompiledRoute + The route to get the bucket for. + + Returns + ------- + asyncio.Future + A future to await that completes when you are allowed to run + your request logic. + + !!! note + The returned future MUST be awaited, and will complete when your + turn to make a call comes along. You are expected to await this and + then immediately make your RESTSession call. The returned future may + already be completed if you can make the call immediately. + """ + # Returns a future to await on to wait to be allowed to send the request, and a + # bucket hash to use to update rate limits later. + template = compiled_route.route + + if template in self.routes_to_hashes: + bucket_hash = self.routes_to_hashes[template] + else: + bucket_hash = UNKNOWN_HASH + self.routes_to_hashes[template] = bucket_hash + + real_bucket_hash = compiled_route.create_real_bucket_hash(bucket_hash) + + try: + bucket = self.real_hashes_to_buckets[real_bucket_hash] + self.logger.debug("%s is being mapped to existing bucket %s", compiled_route, real_bucket_hash) + except KeyError: + self.logger.debug("%s is being mapped to new bucket %s", compiled_route, real_bucket_hash) + bucket = RESTBucket(real_bucket_hash, compiled_route) + self.real_hashes_to_buckets[real_bucket_hash] = bucket + + return bucket.acquire() + + def update_rate_limits( + self, + compiled_route: routes.CompiledRoute, + bucket_header: typing.Optional[str], + remaining_header: int, + limit_header: int, + date_header: datetime.datetime, + reset_at_header: datetime.datetime, + ) -> None: + """Update the rate limits for a bucket using info from a response. + + Parameters + ---------- + compiled_route : hikari.net.routes.CompiledRoute + The compiled route to get the bucket for. + bucket_header : str, optional + The `X-RateLimit-Bucket` header that was provided in the response, + or `None` if not present. + remaining_header : int + The `X-RateLimit-Remaining` header cast to an `int`. + limit_header : int + The `X-RateLimit-Limit`header cast to an `int`. + date_header : datetime.datetime + The `Date` header value as a `datetime.datetime`. + reset_at_header : datetime.datetime + The `X-RateLimit-Reset` header value as a `datetime.datetime`. + """ + self.routes_to_hashes[compiled_route.route] = bucket_header + + real_bucket_hash = compiled_route.create_real_bucket_hash(bucket_header) + + reset_after = (reset_at_header - date_header).total_seconds() + reset_at_monotonic = time.perf_counter() + reset_after + + if real_bucket_hash in self.real_hashes_to_buckets: + bucket = self.real_hashes_to_buckets[real_bucket_hash] + self.logger.debug( + "updating %s with bucket %s [reset-after:%ss, limit:%s, remaining:%s]", + compiled_route, + real_bucket_hash, + reset_after, + limit_header, + remaining_header, + ) + else: + bucket = RESTBucket(real_bucket_hash, compiled_route) + self.real_hashes_to_buckets[real_bucket_hash] = bucket + self.logger.debug( + "remapping %s with bucket %s [reset-after:%ss, limit:%s, remaining:%s]", + compiled_route, + real_bucket_hash, + reset_after, + limit_header, + remaining_header, + ) + + bucket.update_rate_limit(remaining_header, limit_header, reset_at_monotonic) + + @property + def is_started(self) -> bool: + """Return `True` if the rate limiter GC task is started.""" + return self.gc_task is not None diff --git a/hikari/clients/rest/channel.py b/hikari/rest/channel.py similarity index 88% rename from hikari/clients/rest/channel.py rename to hikari/rest/channel.py index 6df0ddacfd..ba819e62c2 100644 --- a/hikari/clients/rest/channel.py +++ b/hikari/rest/channel.py @@ -26,26 +26,27 @@ import datetime import typing -from hikari import bases -from hikari import channels as _channels -from hikari import invites -from hikari import messages as _messages -from hikari import pagination -from hikari import webhooks -from hikari.clients.rest import base from hikari.internal import helpers +from hikari.models import bases +from hikari.models import channels as channels_ +from hikari.models import invites +from hikari.models import messages as messages_ +from hikari.models import pagination +from hikari.models import webhooks + +from . import base if typing.TYPE_CHECKING: - from hikari import embeds as _embeds - from hikari import files as _files - from hikari import guilds - from hikari import permissions as _permissions - from hikari import users + from hikari.models import embeds as embeds_ + from hikari.models import files as files_ + from hikari.models import guilds + from hikari.models import permissions as permissions_ + from hikari.models import users from hikari.internal import more_typing -class _MessagePaginator(pagination.BufferedPaginatedResults[_messages.Message]): +class _MessagePaginator(pagination.BufferedPaginatedResults[messages_.Message]): __slots__ = ("_channel_id", "_direction", "_first_id", "_components", "_session") def __init__(self, channel, direction, first, components, session) -> None: @@ -65,7 +66,7 @@ async def _next_chunk(self): "limit": 100, } - chunk = await self._session.get_channel_messages(**kwargs) + chunk = await self._session.get_channelmessages_(**kwargs) if not chunk: return None @@ -74,15 +75,15 @@ async def _next_chunk(self): self._first_id = chunk[-1]["id"] - return (_messages.Message.deserialize(m, components=self._components) for m in chunk) + return (messages_.Message.deserialize(m, components=self._components) for m in chunk) class RESTChannelComponent(base.BaseRESTComponent, abc.ABC): # pylint: disable=abstract-method, too-many-public-methods - """The REST client component for handling requests to channel endpoints.""" + """The RESTSession client component for handling requests to channel endpoints.""" async def fetch_channel( - self, channel: typing.Union[bases.Snowflake, int, str, _channels.PartialChannel] - ) -> _channels.PartialChannel: + self, channel: typing.Union[bases.Snowflake, int, str, channels_.PartialChannel] + ) -> channels_.PartialChannel: """Get an up to date channel object from a given channel object or ID. Parameters @@ -108,11 +109,11 @@ async def fetch_channel( payload = await self._session.get_channel( channel_id=str(channel.id if isinstance(channel, bases.Unique) else int(channel)) ) - return _channels.deserialize_channel(payload, components=self._components) + return channels_.deserialize_channel(payload, components=self._components) async def update_channel( self, - channel: typing.Union[bases.Snowflake, int, str, _channels.PartialChannel], + channel: typing.Union[bases.Snowflake, int, str, channels_.PartialChannel], *, name: str = ..., position: int = ..., @@ -121,10 +122,10 @@ async def update_channel( bitrate: int = ..., user_limit: int = ..., rate_limit_per_user: typing.Union[int, datetime.timedelta] = ..., - permission_overwrites: typing.Sequence[_channels.PermissionOverwrite] = ..., - parent_category: typing.Optional[typing.Union[bases.Snowflake, int, str, _channels.GuildCategory]] = ..., + permission_overwrites: typing.Sequence[channels_.PermissionOverwrite] = ..., + parent_category: typing.Optional[typing.Union[bases.Snowflake, int, str, channels_.GuildCategory]] = ..., reason: str = ..., - ) -> _channels.PartialChannel: + ) -> channels_.PartialChannel: """Update one or more aspects of a given channel ID. Parameters @@ -208,9 +209,9 @@ async def update_channel( ), reason=reason, ) - return _channels.deserialize_channel(payload, components=self._components) + return channels_.deserialize_channel(payload, components=self._components) - async def delete_channel(self, channel: typing.Union[bases.Snowflake, int, str, _channels.PartialChannel]) -> None: + async def delete_channel(self, channel: typing.Union[bases.Snowflake, int, str, channels_.PartialChannel]) -> None: """Delete the given channel ID, or if it is a DM, close it. Parameters @@ -248,40 +249,40 @@ async def delete_channel(self, channel: typing.Union[bases.Snowflake, int, str, ) @typing.overload - def fetch_messages( - self, channel: typing.Union[bases.Snowflake, int, str, _channels.PartialChannel] - ) -> pagination.PaginatedResults[_messages.Message]: + def fetchmessages_( + self, channel: typing.Union[bases.Snowflake, int, str, channels_.PartialChannel] + ) -> pagination.PaginatedResults[messages_.Message]: """Fetch the channel history, starting with the newest messages.""" @typing.overload - def fetch_messages( + def fetchmessages_( self, - channel: typing.Union[bases.Snowflake, int, str, _channels.PartialChannel], + channel: typing.Union[bases.Snowflake, int, str, channels_.PartialChannel], before: typing.Union[datetime.datetime, int, str, bases.Unique, bases.Snowflake], - ) -> pagination.PaginatedResults[_messages.Message]: + ) -> pagination.PaginatedResults[messages_.Message]: """Fetch the channel history before a given message/time.""" @typing.overload - def fetch_messages( + def fetchmessages_( self, - channel: typing.Union[bases.Snowflake, int, str, _channels.PartialChannel], + channel: typing.Union[bases.Snowflake, int, str, channels_.PartialChannel], after: typing.Union[datetime.datetime, int, str, bases.Unique, bases.Snowflake], - ) -> pagination.PaginatedResults[_messages.Message]: + ) -> pagination.PaginatedResults[messages_.Message]: """Fetch the channel history after a given message/time.""" @typing.overload - def fetch_messages( + def fetchmessages_( self, - channel: typing.Union[bases.Snowflake, int, str, _channels.PartialChannel], + channel: typing.Union[bases.Snowflake, int, str, channels_.PartialChannel], around: typing.Union[datetime.datetime, int, str, bases.Unique, bases.Snowflake], - ) -> pagination.PaginatedResults[_messages.Message]: + ) -> pagination.PaginatedResults[messages_.Message]: """Fetch the channel history around a given message/time.""" - def fetch_messages( + def fetchmessages_( self, - channel: typing.Union[bases.Snowflake, int, str, _channels.PartialChannel], + channel: typing.Union[bases.Snowflake, int, str, channels_.PartialChannel], **kwargs: typing.Union[datetime.datetime, int, str, bases.Unique, bases.Snowflake], - ) -> pagination.PaginatedResults[_messages.Message]: + ) -> pagination.PaginatedResults[messages_.Message]: """Fetch messages from the channel's history. Parameters @@ -327,33 +328,33 @@ def fetch_messages( timestamp = datetime.datetime(2020, 5, 2) - async for message in rest.fetch_messages(channel, before=timestamp).limit(20): + async for message in rest.fetchmessages_(channel, before=timestamp).limit(20): print(message.author, message.content) Fetching messages sent around the same time as a given message. - async for message in rest.fetch_messages(channel, around=event.message): + async for message in rest.fetchmessages_(channel, around=event.message): print(message.author, message.content) Fetching messages after May 3rd, 2020 at 15:33 UTC. timestamp = datetime.datetime(2020, 5, 3, 15, 33, tzinfo=datetime.timezone.utc) - async for message in rest.fetch_messages(channel, after=timestamp): + async for message in rest.fetchmessages_(channel, after=timestamp): print(message.author, message.content) Fetching all messages, newest to oldest: - async for message in rest.fetch_messages(channel, before=datetime.datetime.utcnow()): + async for message in rest.fetchmessages_(channel, before=datetime.datetime.utcnow()): print(message) # More efficient alternative - async for message in rest.fetch_messages(channel): + async for message in rest.fetchmessages_(channel): print(message) Fetching all messages, oldest to newest: - async for message in rest.fetch_messages(channel, after=): + async for message in rest.fetchmessages_(channel, after=): print(message) !!! warning @@ -370,7 +371,7 @@ def fetch_messages( (e.g. older/newer/both) is not overly intuitive. Thus, this specific functionality may be deprecated in the future in favour of a cleaner Python API until a time comes where this information is - documented at a REST API level by Discord. + documented at a RESTSession API level by Discord. Returns ------- @@ -399,9 +400,9 @@ def fetch_messages( async def fetch_message( self, - channel: typing.Union[bases.Snowflake, int, str, _channels.PartialChannel], - message: typing.Union[bases.Snowflake, int, str, _messages.Message], - ) -> _messages.Message: + channel: typing.Union[bases.Snowflake, int, str, channels_.PartialChannel], + message: typing.Union[bases.Snowflake, int, str, messages_.Message], + ) -> messages_.Message: """Get a message from known channel that we can access. Parameters @@ -433,17 +434,17 @@ async def fetch_message( channel_id=str(channel.id if isinstance(channel, bases.Unique) else int(channel)), message_id=str(message.id if isinstance(message, bases.Unique) else int(message)), ) - return _messages.Message.deserialize(payload, components=self._components) + return messages_.Message.deserialize(payload, components=self._components) async def create_message( # pylint: disable=line-too-long self, - channel: typing.Union[bases.Snowflake, int, str, _channels.PartialChannel], + channel: typing.Union[bases.Snowflake, int, str, channels_.PartialChannel], *, content: str = ..., nonce: str = ..., tts: bool = ..., - files: typing.Sequence[_files.BaseStream] = ..., - embed: _embeds.Embed = ..., + files: typing.Sequence[files_.BaseStream] = ..., + embed: embeds_.Embed = ..., mentions_everyone: bool = True, user_mentions: typing.Union[ typing.Collection[typing.Union[bases.Snowflake, int, str, users.User]], bool @@ -451,7 +452,7 @@ async def create_message( # pylint: disable=line-too-long role_mentions: typing.Union[ typing.Collection[typing.Union[bases.Snowflake, int, str, guilds.GuildRole]], bool ] = True, - ) -> _messages.Message: + ) -> messages_.Message: """Create a message in the given channel. Parameters @@ -525,17 +526,17 @@ async def create_message( # pylint: disable=line-too-long mentions_everyone=mentions_everyone, user_mentions=user_mentions, role_mentions=role_mentions ), ) - return _messages.Message.deserialize(payload, components=self._components) + return messages_.Message.deserialize(payload, components=self._components) def safe_create_message( self, - channel: typing.Union[bases.Snowflake, int, str, _channels.PartialChannel], + channel: typing.Union[bases.Snowflake, int, str, channels_.PartialChannel], *, content: str = ..., nonce: str = ..., tts: bool = ..., - files: typing.Sequence[_files.BaseStream] = ..., - embed: _embeds.Embed = ..., + files: typing.Sequence[files_.BaseStream] = ..., + embed: embeds_.Embed = ..., mentions_everyone: bool = False, user_mentions: typing.Union[ typing.Collection[typing.Union[bases.Snowflake, int, str, users.User]], bool @@ -543,7 +544,7 @@ def safe_create_message( role_mentions: typing.Union[ typing.Collection[typing.Union[bases.Snowflake, int, str, guilds.GuildRole]], bool ] = False, - ) -> more_typing.Coroutine[_messages.Message]: + ) -> more_typing.Coroutine[messages_.Message]: """Create a message in the given channel with mention safety. This endpoint has the same signature as @@ -565,11 +566,11 @@ def safe_create_message( async def update_message( # pylint: disable=line-too-long self, - message: typing.Union[bases.Snowflake, int, str, _messages.Message], - channel: typing.Union[bases.Snowflake, int, str, _channels.PartialChannel], + message: typing.Union[bases.Snowflake, int, str, messages_.Message], + channel: typing.Union[bases.Snowflake, int, str, channels_.PartialChannel], *, content: typing.Optional[str] = ..., - embed: typing.Optional[_embeds.Embed] = ..., + embed: typing.Optional[embeds_.Embed] = ..., flags: int = ..., mentions_everyone: bool = True, user_mentions: typing.Union[ @@ -578,7 +579,7 @@ async def update_message( # pylint: disable=line-too-long role_mentions: typing.Union[ typing.Collection[typing.Union[bases.Snowflake, int, str, guilds.GuildRole]], bool ] = True, - ) -> _messages.Message: + ) -> messages_.Message: """Update the given message. Parameters @@ -644,15 +645,15 @@ async def update_message( # pylint: disable=line-too-long mentions_everyone=mentions_everyone, user_mentions=user_mentions, role_mentions=role_mentions, ), ) - return _messages.Message.deserialize(payload, components=self._components) + return messages_.Message.deserialize(payload, components=self._components) def safe_update_message( self, - message: typing.Union[bases.Snowflake, int, str, _messages.Message], - channel: typing.Union[bases.Snowflake, int, str, _channels.PartialChannel], + message: typing.Union[bases.Snowflake, int, str, messages_.Message], + channel: typing.Union[bases.Snowflake, int, str, channels_.PartialChannel], *, content: typing.Optional[str] = ..., - embed: typing.Optional[_embeds.Embed] = ..., + embed: typing.Optional[embeds_.Embed] = ..., flags: int = ..., mentions_everyone: bool = False, user_mentions: typing.Union[ @@ -661,7 +662,7 @@ def safe_update_message( role_mentions: typing.Union[ typing.Collection[typing.Union[bases.Snowflake, int, str, guilds.GuildRole]], bool ] = False, - ) -> more_typing.Coroutine[_messages.Message]: + ) -> more_typing.Coroutine[messages_.Message]: """Update a message in the given channel with mention safety. This endpoint has the same signature as @@ -680,11 +681,11 @@ def safe_update_message( role_mentions=role_mentions, ) - async def delete_messages( + async def deletemessages_( self, - channel: typing.Union[bases.Snowflake, int, str, _channels.PartialChannel], - message: typing.Union[bases.Snowflake, int, str, _messages.Message], - *additional_messages: typing.Union[bases.Snowflake, int, str, _messages.Message], + channel: typing.Union[bases.Snowflake, int, str, channels_.PartialChannel], + message: typing.Union[bases.Snowflake, int, str, messages_.Message], + *additionalmessages_: typing.Union[bases.Snowflake, int, str, messages_.Message], ) -> None: """Delete a message in a given channel. @@ -694,7 +695,7 @@ async def delete_messages( The object or ID of the channel to get the message from. message : typing.Union[hikari.messages.Message, hikari.bases.Snowflake, int] The object or ID of the message to delete. - *additional_messages : typing.Union[hikari.messages.Message, hikari.bases.Snowflake, int] + *additionalmessages_ : typing.Union[hikari.messages.Message, hikari.bases.Snowflake, int] Objects and/or IDs of additional messages to delete in the same channel, in total you can delete up to 100 messages in a request. @@ -719,11 +720,11 @@ async def delete_messages( messages that are newer than `2` weeks in age. If any of the messages are older than `2` weeks then this call will fail. """ - if additional_messages: + if additionalmessages_: messages = list( # dict.fromkeys is used to remove duplicate entries that would cause discord to return an error. dict.fromkeys( - str(m.id if isinstance(m, bases.Unique) else int(m)) for m in (message, *additional_messages) + str(m.id if isinstance(m, bases.Unique) else int(m)) for m in (message, *additionalmessages_) ) ) if len(messages) > 100: @@ -743,12 +744,12 @@ async def delete_messages( async def update_channel_overwrite( # pylint: disable=line-too-long self, - channel: typing.Union[bases.Snowflake, int, str, _messages.Message], - overwrite: typing.Union[_channels.PermissionOverwrite, users.User, guilds.GuildRole, bases.Snowflake, int], - target_type: typing.Union[_channels.PermissionOverwriteType, str], + channel: typing.Union[bases.Snowflake, int, str, messages_.Message], + overwrite: typing.Union[channels_.PermissionOverwrite, users.User, guilds.GuildRole, bases.Snowflake, int], + target_type: typing.Union[channels_.PermissionOverwriteType, str], *, - allow: typing.Union[_permissions.Permission, int] = ..., - deny: typing.Union[_permissions.Permission, int] = ..., + allow: typing.Union[permissions_.Permission, int] = ..., + deny: typing.Union[permissions_.Permission, int] = ..., reason: str = ..., ) -> None: """Edit permissions for a given channel. @@ -794,7 +795,7 @@ async def update_channel_overwrite( # pylint: disable=line-too-long ) async def fetch_invites_for_channel( - self, channel: typing.Union[bases.Snowflake, int, str, _channels.PartialChannel] + self, channel: typing.Union[bases.Snowflake, int, str, channels_.PartialChannel] ) -> typing.Sequence[invites.InviteWithMetadata]: """Get invites for a given channel. @@ -825,7 +826,7 @@ async def fetch_invites_for_channel( async def create_invite_for_channel( self, - channel: typing.Union[bases.Snowflake, int, str, _channels.PartialChannel], + channel: typing.Union[bases.Snowflake, int, str, channels_.PartialChannel], *, max_age: typing.Union[int, datetime.timedelta] = ..., max_uses: int = ..., @@ -897,8 +898,8 @@ async def create_invite_for_channel( async def delete_channel_overwrite( # pylint: disable=line-too-long self, - channel: typing.Union[bases.Snowflake, int, str, _channels.PartialChannel], - overwrite: typing.Union[_channels.PermissionOverwrite, guilds.GuildRole, users.User, bases.Snowflake, int], + channel: typing.Union[bases.Snowflake, int, str, channels_.PartialChannel], + overwrite: typing.Union[channels_.PermissionOverwrite, guilds.GuildRole, users.User, bases.Snowflake, int], ) -> None: """Delete a channel permission overwrite for a user or a role. @@ -925,7 +926,7 @@ async def delete_channel_overwrite( # pylint: disable=line-too-long overwrite_id=str(overwrite.id if isinstance(overwrite, bases.Unique) else int(overwrite)), ) - async def trigger_typing(self, channel: typing.Union[bases.Snowflake, int, str, _channels.PartialChannel]) -> None: + async def trigger_typing(self, channel: typing.Union[bases.Snowflake, int, str, channels_.PartialChannel]) -> None: """Trigger the typing indicator for `10` seconds in a channel. Parameters @@ -948,8 +949,8 @@ async def trigger_typing(self, channel: typing.Union[bases.Snowflake, int, str, ) async def fetch_pins( - self, channel: typing.Union[bases.Snowflake, int, str, _channels.PartialChannel] - ) -> typing.Mapping[bases.Snowflake, _messages.Message]: + self, channel: typing.Union[bases.Snowflake, int, str, channels_.PartialChannel] + ) -> typing.Mapping[bases.Snowflake, messages_.Message]: """Get pinned messages for a given channel. Parameters @@ -981,14 +982,14 @@ async def fetch_pins( channel_id=str(channel.id if isinstance(channel, bases.Unique) else int(channel)) ) return { - bases.Snowflake(message["id"]): _messages.Message.deserialize(message, components=self._components) + bases.Snowflake(message["id"]): messages_.Message.deserialize(message, components=self._components) for message in payload } async def pin_message( self, - channel: typing.Union[bases.Snowflake, int, str, _channels.PartialChannel], - message: typing.Union[bases.Snowflake, int, str, _messages.Message], + channel: typing.Union[bases.Snowflake, int, str, channels_.PartialChannel], + message: typing.Union[bases.Snowflake, int, str, messages_.Message], ) -> None: """Add a pinned message to the channel. @@ -1016,8 +1017,8 @@ async def pin_message( async def unpin_message( self, - channel: typing.Union[bases.Snowflake, int, str, _channels.PartialChannel], - message: typing.Union[bases.Snowflake, int, str, _messages.Message], + channel: typing.Union[bases.Snowflake, int, str, channels_.PartialChannel], + message: typing.Union[bases.Snowflake, int, str, messages_.Message], ) -> None: """Remove a pinned message from the channel. @@ -1047,10 +1048,10 @@ async def unpin_message( async def create_webhook( self, - channel: typing.Union[bases.Snowflake, int, str, _channels.GuildChannel], + channel: typing.Union[bases.Snowflake, int, str, channels_.GuildChannel], name: str, *, - avatar: _files.BaseStream = ..., + avatar: files_.BaseStream = ..., reason: str = ..., ) -> webhooks.Webhook: """Create a webhook for a given channel. @@ -1093,7 +1094,7 @@ async def create_webhook( return webhooks.Webhook.deserialize(payload, components=self._components) async def fetch_channel_webhooks( - self, channel: typing.Union[bases.Snowflake, int, str, _channels.GuildChannel] + self, channel: typing.Union[bases.Snowflake, int, str, channels_.GuildChannel] ) -> typing.Sequence[webhooks.Webhook]: """Get all webhooks from a given channel. diff --git a/hikari/rest/client.py b/hikari/rest/client.py new file mode 100644 index 0000000000..8290f5a3bb --- /dev/null +++ b/hikari/rest/client.py @@ -0,0 +1,98 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# Copyright © Nekoka.tt 2019-2020 +# +# This file is part of Hikari. +# +# Hikari is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Hikari is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Hikari. If not, see . +"""Marshall wrappings for the RESTSession implementation in `hikari.net.rest`. + +This provides an object-oriented interface for interacting with discord's RESTSession +API. +""" + +from __future__ import annotations + +__all__ = ["RESTClient"] + +import typing + +from . import channel +from . import gateway +from . import guild +from . import invite +from . import me +from . import oauth2 +from . import react +from . import session +from . import user +from . import voice +from . import webhook + +if typing.TYPE_CHECKING: + from hikari.components import application + + +class RESTClient( + channel.RESTChannelComponent, + me.RESTCurrentUserComponent, + gateway.RESTGatewayComponent, + guild.RESTGuildComponent, + invite.RESTInviteComponent, + oauth2.RESTOAuth2Component, + react.RESTReactionComponent, + user.RESTUserComponent, + voice.RESTVoiceComponent, + webhook.RESTWebhookComponent, +): + """ + A marshalling object-oriented RESTSession API client. + + This client bridges the basic RESTSession API exposed by + `hikari.net.rest.RESTSession` and wraps it in a unit of processing that can handle + handle parsing API objects into Hikari entity objects. + + Parameters + ---------- + app : hikari.clients.application.Application + The client application that this rest client should be bound by. + Includes the rest config. + + !!! note + For all endpoints where a `reason` argument is provided, this may be a + string inclusively between `0` and `512` characters length, with any + additional characters being cut off. + """ + + def __init__(self, app: application.Application) -> None: + token = None + if app.config.token_type is not None: + token = f"{app.config.token_type} {app.config.token}" + super().__init__( + app, + session.RESTSession( + allow_redirects=app.config.allow_redirects, + base_url=app.config.rest_url, + connector=app.config.tcp_connector, + debug=app.config.debug, + proxy_headers=app.config.proxy_headers, + proxy_auth=app.config.proxy_auth, + ssl_context=app.config.ssl_context, + verify_ssl=app.config.verify_ssl, + timeout=app.config.request_timeout, + token=token, + trust_env=app.config.trust_env, + version=app.config.rest_version, + ), + ) diff --git a/hikari/clients/rest/gateway.py b/hikari/rest/gateway.py similarity index 82% rename from hikari/clients/rest/gateway.py rename to hikari/rest/gateway.py index 1f91bb92e8..6c093beaf6 100644 --- a/hikari/clients/rest/gateway.py +++ b/hikari/rest/gateway.py @@ -24,12 +24,13 @@ import abc -from hikari import gateway_entities -from hikari.clients.rest import base +from hikari.models import gateway + +from . import base class RESTGatewayComponent(base.BaseRESTComponent, abc.ABC): # pylint: disable=abstract-method - """The REST client component for handling requests to gateway endpoints.""" + """The RESTSession client component for handling requests to gateway endpoints.""" async def fetch_gateway_url(self) -> str: """Get a generic url used for establishing a Discord gateway connection. @@ -45,17 +46,17 @@ async def fetch_gateway_url(self) -> str: # noinspection PyTypeChecker return await self._session.get_gateway() - async def fetch_gateway_bot(self) -> gateway_entities.GatewayBot: + async def fetch_gateway_bot(self) -> gateway.GatewayBot: """Get bot specific gateway information. - Returns - ------- - hikari.gateway_entities.GatewayBot - The bot specific gateway information object. - !!! note Unlike `RESTGatewayComponent.fetch_gateway_url`, this requires a valid token to work. + + Returns + ------- + hikari.models.gateway.GatewayBot + The bot specific gateway information object. """ payload = await self._session.get_gateway_bot() - return gateway_entities.GatewayBot.deserialize(payload, components=self._components) + return gateway.GatewayBot.deserialize(payload, components=self._components) diff --git a/hikari/clients/rest/guild.py b/hikari/rest/guild.py similarity index 98% rename from hikari/clients/rest/guild.py rename to hikari/rest/guild.py index 20e60b0180..1b59edb67b 100644 --- a/hikari/clients/rest/guild.py +++ b/hikari/rest/guild.py @@ -27,22 +27,23 @@ import functools import typing -from hikari import audit_logs -from hikari import bases -from hikari import channels as _channels -from hikari import emojis -from hikari import guilds -from hikari import invites -from hikari import pagination -from hikari import voices -from hikari import webhooks -from hikari.clients.rest import base +from hikari.models import audit_logs +from hikari.models import bases +from hikari.models import channels as channels_ +from hikari.models import emojis +from hikari.models import guilds +from hikari.models import invites +from hikari.models import pagination +from hikari.models import voices +from hikari.models import webhooks + +from . import base if typing.TYPE_CHECKING: - from hikari import colors - from hikari import files - from hikari import permissions as _permissions - from hikari import users + from hikari.models import colors + from hikari.models import files + from hikari.models import permissions as permissions_ + from hikari.models import users class _MemberPaginator(pagination.BufferedPaginatedResults[guilds.GuildMember]): @@ -67,7 +68,7 @@ async def _next_chunk(self): class RESTGuildComponent(base.BaseRESTComponent, abc.ABC): # pylint: disable=abstract-method, too-many-public-methods - """The REST client component for handling requests to guild endpoints.""" + """The RESTSession client component for handling requests to guild endpoints.""" async def fetch_audit_log( self, @@ -193,7 +194,7 @@ def fetch_audit_log_entries_before( user_id=(str(user.id if isinstance(user, bases.Unique) else int(user)) if user is not ... else ...), action_type=action_type, ) - return audit_logs.AuditLogIterator(components=self._components, request=request, before=before, limit=limit) + return audit_logs.AuditLogIterator(app=self._components, request=request, before=before, limit=limit) async def fetch_guild_emoji( self, @@ -412,7 +413,7 @@ async def create_guild( default_message_notifications: typing.Union[guilds.GuildMessageNotificationsLevel, int] = ..., explicit_content_filter: typing.Union[guilds.GuildExplicitContentFilterLevel, int] = ..., roles: typing.Sequence[guilds.GuildRole] = ..., - channels: typing.Sequence[_channels.GuildChannelBuilder] = ..., + channels: typing.Sequence[channels_.GuildChannelBuilder] = ..., ) -> guilds.Guild: """Create a new guild. @@ -543,12 +544,12 @@ async def update_guild( verification_level: typing.Union[guilds.GuildVerificationLevel, int] = ..., default_message_notifications: typing.Union[guilds.GuildMessageNotificationsLevel, int] = ..., explicit_content_filter: typing.Union[guilds.GuildExplicitContentFilterLevel, int] = ..., - afk_channel: typing.Union[bases.Snowflake, int, str, _channels.GuildVoiceChannel] = ..., + afk_channel: typing.Union[bases.Snowflake, int, str, channels_.GuildVoiceChannel] = ..., afk_timeout: typing.Union[datetime.timedelta, int] = ..., icon: files.BaseStream = ..., owner: typing.Union[bases.Snowflake, int, str, users.User] = ..., splash: files.BaseStream = ..., - system_channel: typing.Union[bases.Snowflake, int, str, _channels.PartialChannel] = ..., + system_channel: typing.Union[bases.Snowflake, int, str, channels_.PartialChannel] = ..., reason: str = ..., ) -> guilds.Guild: """Edit a given guild. @@ -652,7 +653,7 @@ async def delete_guild(self, guild: typing.Union[bases.Snowflake, int, str, guil async def fetch_guild_channels( self, guild: typing.Union[bases.Snowflake, int, str, guilds.Guild] - ) -> typing.Sequence[_channels.GuildChannel]: + ) -> typing.Sequence[channels_.GuildChannel]: """Get all the channels for a given guild. Parameters @@ -678,23 +679,23 @@ async def fetch_guild_channels( payload = await self._session.list_guild_channels( guild_id=str(guild.id if isinstance(guild, bases.Unique) else int(guild)) ) - return [_channels.deserialize_channel(channel, components=self._components) for channel in payload] + return [channels_.deserialize_channel(channel, components=self._components) for channel in payload] async def create_guild_channel( # pylint: disable=too-many-arguments self, guild: typing.Union[bases.Snowflake, int, str, guilds.Guild], name: str, - channel_type: typing.Union[_channels.ChannelType, int] = ..., + channel_type: typing.Union[channels_.ChannelType, int] = ..., position: int = ..., topic: str = ..., nsfw: bool = ..., rate_limit_per_user: typing.Union[datetime.timedelta, int] = ..., bitrate: int = ..., user_limit: int = ..., - permission_overwrites: typing.Sequence[_channels.PermissionOverwrite] = ..., - parent_category: typing.Union[bases.Snowflake, int, str, _channels.GuildCategory] = ..., + permission_overwrites: typing.Sequence[channels_.PermissionOverwrite] = ..., + parent_category: typing.Union[bases.Snowflake, int, str, channels_.GuildCategory] = ..., reason: str = ..., - ) -> _channels.GuildChannel: + ) -> channels_.GuildChannel: """Create a channel in a given guild. Parameters @@ -782,13 +783,13 @@ async def create_guild_channel( # pylint: disable=too-many-arguments ), reason=reason, ) - return _channels.deserialize_channel(payload, components=self._components) + return channels_.deserialize_channel(payload, components=self._components) async def reposition_guild_channels( self, guild: typing.Union[bases.Snowflake, int, str, guilds.Guild], - channel: typing.Tuple[int, typing.Union[bases.Snowflake, int, str, _channels.GuildChannel]], - *additional_channels: typing.Tuple[int, typing.Union[bases.Snowflake, int, str, _channels.GuildChannel]], + channel: typing.Tuple[int, typing.Union[bases.Snowflake, int, str, channels_.GuildChannel]], + *additional_channels: typing.Tuple[int, typing.Union[bases.Snowflake, int, str, channels_.GuildChannel]], ) -> None: """Edits the position of one or more given channels. @@ -901,7 +902,7 @@ async def update_member( # pylint: disable=too-many-arguments roles: typing.Sequence[typing.Union[bases.Snowflake, int, str, guilds.GuildRole]] = ..., mute: bool = ..., deaf: bool = ..., - voice_channel: typing.Optional[typing.Union[bases.Snowflake, int, str, _channels.GuildVoiceChannel]] = ..., + voice_channel: typing.Optional[typing.Union[bases.Snowflake, int, str, channels_.GuildVoiceChannel]] = ..., reason: str = ..., ) -> None: """Edits a guild's member, any unspecified fields will not be changed. @@ -1295,7 +1296,7 @@ async def create_role( guild: typing.Union[bases.Snowflake, int, str, guilds.Guild], *, name: str = ..., - permissions: typing.Union[_permissions.Permission, int] = ..., + permissions: typing.Union[permissions_.Permission, int] = ..., color: typing.Union[colors.Color, int] = ..., hoist: bool = ..., mentionable: bool = ..., @@ -1402,7 +1403,7 @@ async def update_role( role: typing.Union[bases.Snowflake, int, str, guilds.GuildRole], *, name: str = ..., - permissions: typing.Union[_permissions.Permission, int] = ..., + permissions: typing.Union[permissions_.Permission, int] = ..., color: typing.Union[colors.Color, int] = ..., hoist: bool = ..., mentionable: bool = ..., @@ -1811,7 +1812,7 @@ async def update_guild_embed( self, guild: typing.Union[bases.Snowflake, int, str, guilds.Guild], *, - channel: typing.Union[bases.Snowflake, int, str, _channels.GuildChannel] = ..., + channel: typing.Union[bases.Snowflake, int, str, channels_.GuildChannel] = ..., enabled: bool = ..., reason: str = ..., ) -> guilds.GuildEmbed: diff --git a/hikari/clients/rest/invite.py b/hikari/rest/invite.py similarity index 95% rename from hikari/clients/rest/invite.py rename to hikari/rest/invite.py index ebe2520faa..980e63bbf4 100644 --- a/hikari/clients/rest/invite.py +++ b/hikari/rest/invite.py @@ -25,12 +25,13 @@ import abc import typing -from hikari import invites -from hikari.clients.rest import base +from hikari.models import invites + +from . import base class RESTInviteComponent(base.BaseRESTComponent, abc.ABC): # pylint: disable=abstract-method - """The REST client component for handling requests to invite endpoints.""" + """The RESTSession client component for handling requests to invite endpoints.""" async def fetch_invite( self, invite: typing.Union[invites.Invite, str], *, with_counts: bool = ... diff --git a/hikari/clients/rest/me.py b/hikari/rest/me.py similarity index 94% rename from hikari/clients/rest/me.py rename to hikari/rest/me.py index 6aeacc5f84..430aaa5e3c 100644 --- a/hikari/clients/rest/me.py +++ b/hikari/rest/me.py @@ -26,16 +26,17 @@ import datetime import typing -from hikari import applications -from hikari import bases -from hikari import channels as _channels -from hikari import guilds -from hikari import pagination -from hikari import users -from hikari.clients.rest import base +from hikari.models import applications +from hikari.models import bases +from hikari.models import channels as channels_ +from hikari.models import guilds +from hikari.models import pagination +from hikari.models import users + +from . import base if typing.TYPE_CHECKING: - from hikari import files + from hikari.models import files class _GuildPaginator(pagination.BufferedPaginatedResults[guilds.Guild]): @@ -64,7 +65,7 @@ async def _next_chunk(self): class RESTCurrentUserComponent(base.BaseRESTComponent, abc.ABC): # pylint: disable=abstract-method - """The REST client component for handling requests to `@me` endpoints.""" + """The RESTSession client component for handling requests to `@me` endpoints.""" async def fetch_me(self) -> users.MyUser: """Get the current user that of the token given to the client. @@ -181,7 +182,7 @@ async def leave_guild(self, guild: typing.Union[bases.Snowflake, int, str, guild async def create_dm_channel( self, recipient: typing.Union[bases.Snowflake, int, str, users.User] - ) -> _channels.DMChannel: + ) -> channels_.DMChannel: """Create a new DM channel with a given user. Parameters @@ -205,4 +206,4 @@ async def create_dm_channel( payload = await self._session.create_dm( recipient_id=str(recipient.id if isinstance(recipient, bases.Unique) else int(recipient)) ) - return _channels.DMChannel.deserialize(payload, components=self._components) + return channels_.DMChannel.deserialize(payload, components=self._components) diff --git a/hikari/clients/rest/oauth2.py b/hikari/rest/oauth2.py similarity index 91% rename from hikari/clients/rest/oauth2.py rename to hikari/rest/oauth2.py index a50aa80703..336b3cae8a 100644 --- a/hikari/clients/rest/oauth2.py +++ b/hikari/rest/oauth2.py @@ -24,12 +24,12 @@ import abc -from hikari import applications -from hikari.clients.rest import base +from hikari.models import applications +from . import base class RESTOAuth2Component(base.BaseRESTComponent, abc.ABC): # pylint: disable=abstract-method - """The REST client component for handling requests to oauth2 endpoints.""" + """The RESTSession client component for handling requests to oauth2 endpoints.""" async def fetch_my_application_info(self) -> applications.Application: """Get the current application information. diff --git a/hikari/clients/rest/react.py b/hikari/rest/react.py similarity index 91% rename from hikari/clients/rest/react.py rename to hikari/rest/react.py index 14aa8bc5b7..d50a974c8e 100644 --- a/hikari/clients/rest/react.py +++ b/hikari/rest/react.py @@ -26,18 +26,18 @@ import datetime import typing -from hikari import bases -from hikari import messages as _messages -from hikari import pagination -from hikari import users -from hikari.clients.rest import base +from hikari.models import bases +from hikari.models import messages as messages_ +from hikari.models import pagination +from hikari.models import users +from . import base if typing.TYPE_CHECKING: - from hikari import channels as _channels - from hikari import emojis + from hikari.models import channels as channels_ + from hikari.models import emojis -class _ReactionPaginator(pagination.BufferedPaginatedResults[_messages.Reaction]): +class _ReactionPaginator(pagination.BufferedPaginatedResults[messages_.Reaction]): __slots__ = ("_channel_id", "_message_id", "_first_id", "_emoji", "_components", "_session") def __init__(self, channel, message, emoji, users_after, components, session) -> None: @@ -63,12 +63,12 @@ async def _next_chunk(self): class RESTReactionComponent(base.BaseRESTComponent, abc.ABC): # pylint: disable=abstract-method - """The REST client component for handling requests to reaction endpoints.""" + """The RESTSession client component for handling requests to reaction endpoints.""" async def add_reaction( self, - channel: typing.Union[bases.Snowflake, int, str, _channels.PartialChannel], - message: typing.Union[bases.Snowflake, int, str, _messages.Message], + channel: typing.Union[bases.Snowflake, int, str, channels_.PartialChannel], + message: typing.Union[bases.Snowflake, int, str, messages_.Message], emoji: typing.Union[emojis.Emoji, str], ) -> None: """Add a reaction to the given message in the given channel. @@ -106,8 +106,8 @@ async def add_reaction( async def remove_reaction( self, - channel: typing.Union[bases.Snowflake, int, str, _channels.PartialChannel], - message: typing.Union[bases.Snowflake, int, str, _messages.Message], + channel: typing.Union[bases.Snowflake, int, str, channels_.PartialChannel], + message: typing.Union[bases.Snowflake, int, str, messages_.Message], emoji: typing.Union[emojis.Emoji, str], *, user: typing.Optional[typing.Hashable[users.User]] = None, @@ -164,8 +164,8 @@ async def remove_reaction( async def remove_all_reactions( self, - channel: typing.Union[bases.Snowflake, int, str, _channels.PartialChannel], - message: typing.Union[bases.Snowflake, int, str, _messages.Message], + channel: typing.Union[bases.Snowflake, int, str, channels_.PartialChannel], + message: typing.Union[bases.Snowflake, int, str, messages_.Message], *, emoji: typing.Optional[typing.Union[emojis.Emoji, str]] = None, ) -> None: @@ -208,8 +208,8 @@ async def remove_all_reactions( def fetch_reactors( self, - channel: typing.Union[bases.Snowflake, int, str, _channels.PartialChannel], - message: typing.Union[bases.Snowflake, int, str, _messages.Message], + channel: typing.Union[bases.Snowflake, int, str, channels_.PartialChannel], + message: typing.Union[bases.Snowflake, int, str, messages_.Message], emoji: typing.Union[emojis.Emoji, str], after: typing.Optional[typing.Union[datetime.datetime, bases.Unique, bases.Snowflake, int, str]] = None, ) -> pagination.PaginatedResults[users.User]: diff --git a/hikari/net/routes.py b/hikari/rest/routes.py similarity index 100% rename from hikari/net/routes.py rename to hikari/rest/routes.py diff --git a/hikari/net/rest.py b/hikari/rest/session.py similarity index 99% rename from hikari/net/rest.py rename to hikari/rest/session.py index d3e2dff186..f4b1f49242 100644 --- a/hikari/net/rest.py +++ b/hikari/rest/session.py @@ -20,7 +20,7 @@ from __future__ import annotations -__all__ = ["REST"] +__all__ = ["RESTSession"] import asyncio import contextlib @@ -31,19 +31,21 @@ import aiohttp.typedefs -from hikari import errors +from hikari.internal import user_agents from hikari.internal import conversions +from hikari.internal import http_client from hikari.internal import more_collections +from hikari.internal import ratelimits from hikari.internal import urls -from hikari.net import http_client -from hikari.net import ratelimits -from hikari.net import routes -from hikari.net import user_agents +from hikari import errors + +from . import buckets +from . import routes if typing.TYPE_CHECKING: import ssl - from hikari import files as _files + from hikari.models import files as files_ from hikari.internal import more_typing VERSION_6: typing.Final[int] = 6 @@ -54,7 +56,7 @@ class _RateLimited(RuntimeError): __slots__ = () -class REST(http_client.HTTPClient): # pylint: disable=too-many-public-methods, too-many-instance-attributes +class RESTSession(http_client.HTTPClient): # pylint: disable=too-many-public-methods, too-many-instance-attributes """A low-level RESTful client to allow you to interact with the Discord API. Parameters @@ -119,7 +121,7 @@ class REST(http_client.HTTPClient): # pylint: disable=too-many-public-methods, this session will be paused. """ - bucket_ratelimiters: ratelimits.RESTBucketManager + bucket_ratelimiters: buckets.RESTBucketManager """The per-route ratelimit manager. This handles tracking any ratelimits for routes that have recently been used @@ -188,7 +190,7 @@ def __init__( # pylint: disable=too-many-locals self.user_agent = user_agents.UserAgent().user_agent self.version = version self.global_ratelimiter = ratelimits.ManualRateLimiter() - self.bucket_ratelimiters = ratelimits.RESTBucketManager() + self.bucket_ratelimiters = buckets.RESTBucketManager() if token is not None and not token.startswith(self._AUTHENTICATION_SCHEMES): this_type = type(self).__name__ @@ -199,7 +201,7 @@ def __init__( # pylint: disable=too-many-locals self.base_url = base_url.format(self) async def close(self) -> None: - """Shut down the REST client safely, and terminate any rate limiters executing in the background.""" + """Shut down the RESTSession client safely, and terminate any rate limiters executing in the background.""" with contextlib.suppress(Exception): self.bucket_ratelimiters.close() with contextlib.suppress(Exception): @@ -370,7 +372,7 @@ async def get_gateway_bot(self) -> more_typing.JSONObject: `session_start_limit` object. !!! note - Unlike `REST.get_gateway`, this requires a valid token to work. + Unlike `RESTSession.get_gateway`, this requires a valid token to work. """ return await self._request_json_response(routes.GET_GATEWAY_BOT.compile()) @@ -641,7 +643,7 @@ async def create_message( content: str = ..., nonce: str = ..., tts: bool = ..., - files: typing.Sequence[_files.BaseStream] = ..., + files: typing.Sequence[files_.BaseStream] = ..., embed: more_typing.JSONObject = ..., allowed_mentions: more_typing.JSONObject = ..., ) -> more_typing.JSONObject: @@ -1387,7 +1389,7 @@ async def create_guild( The name string for the new guild (`2-100` characters). region : str If specified, the voice region ID for new guild. You can use - `REST.list_voice_regions` to see which region IDs are available. + `RESTSession.list_voice_regions` to see which region IDs are available. icon : bytes If specified, the guild icon image in bytes form. verification_level : int @@ -1506,7 +1508,7 @@ async def modify_guild( # lgtm [py/similar-function] If specified, the new name string for the guild (`2-100` characters). region : str If specified, the new voice region ID for guild. You can use - `REST.list_voice_regions` to see which region IDs are available. + `RESTSession.list_voice_regions` to see which region IDs are available. verification_level : int If specified, the new verification level integer (`0-5`). default_message_notifications : int @@ -3009,7 +3011,7 @@ async def execute_webhook( # pylint:disable=too-many-locals avatar_url: str = ..., tts: bool = ..., wait: bool = ..., - files: typing.Sequence[_files.BaseStream] = ..., + files: typing.Sequence[files_.BaseStream] = ..., embeds: typing.Sequence[more_typing.JSONObject] = ..., allowed_mentions: more_typing.JSONObject = ..., ) -> typing.Optional[more_typing.JSONObject]: diff --git a/hikari/clients/rest/user.py b/hikari/rest/user.py similarity index 91% rename from hikari/clients/rest/user.py rename to hikari/rest/user.py index 0c12bc7ddb..eb32aeda81 100644 --- a/hikari/clients/rest/user.py +++ b/hikari/rest/user.py @@ -25,13 +25,14 @@ import abc import typing -from hikari import bases -from hikari import users -from hikari.clients.rest import base +from hikari.models import bases +from hikari.models import users + +from . import base class RESTUserComponent(base.BaseRESTComponent, abc.ABC): # pylint: disable=abstract-method - """The REST client component for handling requests to user endpoints.""" + """The RESTSession client component for handling requests to user endpoints.""" async def fetch_user(self, user: typing.Union[bases.Snowflake, int, str, users.User]) -> users.User: """Get a given user. diff --git a/hikari/clients/rest/voice.py b/hikari/rest/voice.py similarity index 91% rename from hikari/clients/rest/voice.py rename to hikari/rest/voice.py index 9554bb4e34..b81ecaef51 100644 --- a/hikari/clients/rest/voice.py +++ b/hikari/rest/voice.py @@ -25,12 +25,12 @@ import abc import typing -from hikari import voices -from hikari.clients.rest import base +from hikari.models import voices +from . import base class RESTVoiceComponent(base.BaseRESTComponent, abc.ABC): # pylint: disable=abstract-method - """The REST client component for handling requests to voice endpoints.""" + """The RESTSession client component for handling requests to voice endpoints.""" async def fetch_voice_regions(self) -> typing.Sequence[voices.VoiceRegion]: """Get the voice regions that are available. diff --git a/hikari/clients/rest/webhook.py b/hikari/rest/webhook.py similarity index 92% rename from hikari/clients/rest/webhook.py rename to hikari/rest/webhook.py index 0cdcac539e..65c77620cf 100644 --- a/hikari/clients/rest/webhook.py +++ b/hikari/rest/webhook.py @@ -25,22 +25,23 @@ import abc import typing -from hikari import bases -from hikari import messages as _messages -from hikari import webhooks -from hikari.clients.rest import base from hikari.internal import helpers +from hikari.models import bases +from hikari.models import messages as messages_ +from hikari.models import webhooks + +from . import base if typing.TYPE_CHECKING: - from hikari import channels as _channels - from hikari import embeds as _embeds - from hikari import files as _files - from hikari import guilds - from hikari import users + from hikari.models import channels as channels_ + from hikari.models import embeds as embeds_ + from hikari.models import files as files_ + from hikari.models import guilds + from hikari.models import users class RESTWebhookComponent(base.BaseRESTComponent, abc.ABC): # pylint: disable=abstract-method - """The REST client component for handling requests to webhook endpoints.""" + """The RESTSession client component for handling requests to webhook endpoints.""" async def fetch_webhook( self, webhook: typing.Union[bases.Snowflake, int, str, webhooks.Webhook], *, webhook_token: str = ... @@ -85,8 +86,8 @@ async def update_webhook( *, webhook_token: str = ..., name: str = ..., - avatar: typing.Optional[_files.BaseStream] = ..., - channel: typing.Union[bases.Snowflake, int, str, _channels.GuildChannel] = ..., + avatar: typing.Optional[files_.BaseStream] = ..., + channel: typing.Union[bases.Snowflake, int, str, channels_.GuildChannel] = ..., reason: str = ..., ) -> webhooks.Webhook: """Edit a given webhook. @@ -183,8 +184,8 @@ async def execute_webhook( # pylint:disable=too-many-locals,line-too-long avatar_url: str = ..., tts: bool = ..., wait: bool = False, - files: typing.Sequence[_files.BaseStream] = ..., - embeds: typing.Sequence[_embeds.Embed] = ..., + files: typing.Sequence[files_.BaseStream] = ..., + embeds: typing.Sequence[embeds_.Embed] = ..., mentions_everyone: bool = True, user_mentions: typing.Union[ typing.Collection[typing.Union[bases.Snowflake, int, str, users.User]], bool @@ -192,7 +193,7 @@ async def execute_webhook( # pylint:disable=too-many-locals,line-too-long role_mentions: typing.Union[ typing.Collection[typing.Union[bases.Snowflake, int, str, guilds.GuildRole]], bool ] = True, - ) -> typing.Optional[_messages.Message]: + ) -> typing.Optional[messages_.Message]: """Execute a webhook to create a message. Parameters @@ -275,7 +276,7 @@ async def execute_webhook( # pylint:disable=too-many-locals,line-too-long ), ) if wait is True: - return _messages.Message.deserialize(payload, components=self._components) + return messages_.Message.deserialize(payload, components=self._components) return None def safe_webhook_execute( @@ -288,8 +289,8 @@ def safe_webhook_execute( avatar_url: str = ..., tts: bool = ..., wait: bool = False, - files: typing.Sequence[_files.BaseStream] = ..., - embeds: typing.Sequence[_embeds.Embed] = ..., + files: typing.Sequence[files_.BaseStream] = ..., + embeds: typing.Sequence[embeds_.Embed] = ..., mentions_everyone: bool = False, user_mentions: typing.Union[ typing.Collection[typing.Union[bases.Snowflake, int, str, users.User]], bool @@ -297,7 +298,7 @@ def safe_webhook_execute( role_mentions: typing.Union[ typing.Collection[typing.Union[bases.Snowflake, int, str, guilds.GuildRole]], bool ] = False, - ) -> typing.Coroutine[typing.Any, typing.Any, typing.Optional[_messages.Message]]: + ) -> typing.Coroutine[typing.Any, typing.Any, typing.Optional[messages_.Message]]: """Execute a webhook to create a message with mention safety. This endpoint has the same signature as diff --git a/hikari/stateful/.gitkeep b/hikari/stateful/.gitkeep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/hikari/state/__init__.py b/hikari/stateless/__init__.py similarity index 90% rename from hikari/state/__init__.py rename to hikari/stateless/__init__.py index 6b4bb436f2..5e0dd2c85c 100644 --- a/hikari/state/__init__.py +++ b/hikari/stateless/__init__.py @@ -20,11 +20,13 @@ The API for this part of the framework has been split into groups of abstract base classes, and corresponding implementations. This allows -several key components to be implemented separately, in case you have a +several key application to be implemented separately, in case you have a specific use case you want to provide (such as placing stuff on a message queue if you distribute your bot). """ from __future__ import annotations -__all__ = [] +from .bot import * + +__all__ = bot.__all__ diff --git a/hikari/clients/stateless.py b/hikari/stateless/bot.py similarity index 61% rename from hikari/clients/stateless.py rename to hikari/stateless/bot.py index 55549fec3b..f318c4a27b 100644 --- a/hikari/clients/stateless.py +++ b/hikari/stateless/bot.py @@ -24,15 +24,16 @@ import typing -from hikari.clients import bot_base -from hikari.clients import rest -from hikari.clients import shards -from hikari.events import intent_aware_dispatchers -from hikari.state import stateless +from hikari import rest +from hikari.components import bot_base +from hikari.components import intent_aware_dispatchers +from hikari.gateway import client + +from . import manager if typing.TYPE_CHECKING: - from hikari.clients import components as _components - from hikari.clients import configs + from hikari.components import application + from hikari import configs class StatelessBot(bot_base.BotBase): @@ -42,18 +43,16 @@ class StatelessBot(bot_base.BotBase): """ @staticmethod - def _create_shard( - components: _components.Components, shard_id: int, shard_count: int, url: str - ) -> shards.ShardClientImpl: - return shards.ShardClientImpl(components=components, shard_id=shard_id, shard_count=shard_count, url=url) + def _create_shard(app: application.Application, shard_id: int, shard_count: int, url: str) -> client.GatewayClient: + return client.GatewayClient(app=app, shard_id=shard_id, shard_count=shard_count, url=url) @staticmethod - def _create_rest(components: _components.Components) -> rest.RESTClient: - return rest.RESTClient(components) + def _create_rest(app: application.Application) -> rest.RESTClient: + return rest.RESTClient(app) @staticmethod - def _create_event_manager(components: _components.Components) -> stateless.StatelessEventManagerImpl: - return stateless.StatelessEventManagerImpl(components) + def _create_event_manager(app: application.Application) -> manager.StatelessEventManagerImpl: + return manager.StatelessEventManagerImpl(app) @staticmethod def _create_event_dispatcher(config: configs.BotConfig) -> intent_aware_dispatchers.IntentAwareEventDispatcherImpl: diff --git a/hikari/state/stateless.py b/hikari/stateless/manager.py similarity index 99% rename from hikari/state/stateless.py rename to hikari/stateless/manager.py index f1703e2ff0..2e26d3d135 100644 --- a/hikari/state/stateless.py +++ b/hikari/stateless/manager.py @@ -22,14 +22,13 @@ __all__ = ["StatelessEventManagerImpl"] +from hikari.components import dispatchers +from hikari.components import event_managers from hikari.events import channel -from hikari.events import dispatchers -from hikari.events import event_managers from hikari.events import guild from hikari.events import message from hikari.events import other - # pylint: disable=too-many-public-methods from hikari.events import voice diff --git a/requirements.txt b/requirements.txt index e69acd3e63..9f2c046784 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,2 @@ aiohttp~=3.6.2 attrs~=19.3.0 -click~=7.1.2 diff --git a/tests/hikari/_helpers.py b/tests/hikari/_helpers.py index adb8b1b1b0..fe23d8aed5 100644 --- a/tests/hikari/_helpers.py +++ b/tests/hikari/_helpers.py @@ -35,7 +35,7 @@ import mock import pytest -from hikari import bases +from hikari.models import bases from hikari.internal import marshaller _LOGGER = logging.getLogger(__name__) @@ -57,7 +57,7 @@ def purge_loop(): def mock_methods_on(obj, except_=(), also_mock=()): # Mock any methods we don't care about. also_mock is a collection of attribute names that we can eval to access - # and mock specific components with a coroutine mock to mock other external components quickly :) + # and mock specific application with a coroutine mock to mock other external application quickly :) magics = ["__enter__", "__exit__", "__aenter__", "__aexit__", "__iter__", "__aiter__"] except_ = set(except_) diff --git a/tests/hikari/clients/test_components.py b/tests/hikari/clients/test_components.py index ee75b33864..b332e3e779 100644 --- a/tests/hikari/clients/test_components.py +++ b/tests/hikari/clients/test_components.py @@ -18,12 +18,12 @@ # along ith Hikari. If not, see . import mock -from hikari.clients import components -from hikari.clients import configs -from hikari.clients import rest -from hikari.clients import shards -from hikari.events import dispatchers -from hikari.events import event_managers +from hikari.components import application +from hikari import configs +from hikari import rest +from hikari.gateway import client +from hikari.components import dispatchers +from hikari.components import event_managers class TestComponents: @@ -32,8 +32,8 @@ def test___init__(self): mock_event_dispatcher = mock.MagicMock(dispatchers.EventDispatcher) mock_event_manager = mock.MagicMock(event_managers.EventManager) mock_rest = mock.MagicMock(rest.RESTClient) - mock_shards = mock.MagicMock(shards.ShardClient) - component = components.Components( + mock_shards = mock.MagicMock(client.GatewayClient) + component = application.Application( config=mock_config, event_manager=mock_event_manager, event_dispatcher=mock_event_dispatcher, diff --git a/tests/hikari/clients/test_configs.py b/tests/hikari/clients/test_configs.py index 61f96c20f3..0b9046a9d9 100644 --- a/tests/hikari/clients/test_configs.py +++ b/tests/hikari/clients/test_configs.py @@ -23,10 +23,8 @@ import mock import pytest -from hikari import gateway_entities -from hikari import guilds -from hikari import intents -from hikari.clients import configs +from hikari.models import guilds, intents, gateway +from hikari import configs from hikari.internal import urls from tests.hikari import _helpers @@ -146,11 +144,11 @@ class TestWebsocketConfig: def test_deserialize(self, test_websocket_config): datetime_obj = datetime.datetime.now() test_websocket_config["initial_idle_since"] = datetime_obj.timestamp() - mock_activity = mock.MagicMock(gateway_entities.Activity) + mock_activity = mock.MagicMock(gateway.Activity) with _helpers.patch_marshal_attr( configs.GatewayConfig, "initial_activity", - deserializer=gateway_entities.Activity.deserialize, + deserializer=gateway.Activity.deserialize, return_value=mock_activity, ) as patched_activity_deserializer: websocket_config_obj = configs.GatewayConfig.deserialize(test_websocket_config) @@ -259,11 +257,11 @@ class TestBotConfig: def test_deserialize(self, test_bot_config): datetime_obj = datetime.datetime.now() test_bot_config["initial_idle_since"] = datetime_obj.timestamp() - mock_activity = mock.MagicMock(gateway_entities.Activity) + mock_activity = mock.MagicMock(gateway.Activity) with _helpers.patch_marshal_attr( configs.BotConfig, "initial_activity", - deserializer=gateway_entities.Activity.deserialize, + deserializer=gateway.Activity.deserialize, return_value=mock_activity, ) as patched_activity_deserializer: bot_config_obj = configs.BotConfig.deserialize(test_bot_config) diff --git a/tests/hikari/clients/test_rest/test___init__.py b/tests/hikari/clients/test_rest/test___init__.py index 0974dca040..aa2a5e8faa 100644 --- a/tests/hikari/clients/test_rest/test___init__.py +++ b/tests/hikari/clients/test_rest/test___init__.py @@ -21,10 +21,10 @@ import mock import pytest -from hikari.clients import components -from hikari.clients import configs -from hikari.clients import rest as high_level_rest -from hikari.net import rest as low_level_rest +from hikari.components import application +from hikari import configs +from hikari import rest as high_level_rest +from hikari.rest import session as low_level_rest class TestRESTClient: @@ -38,9 +38,9 @@ class TestRESTClient: ) def test_init(self, token, token_type, expected_token): mock_config = configs.RESTConfig(token=token, token_type=token_type, trust_env=True) - mock_components = mock.MagicMock(components.Components, config=mock_config) - mock_low_level_rest_clients = mock.MagicMock(low_level_rest.REST) - with mock.patch.object(low_level_rest, "REST", return_value=mock_low_level_rest_clients) as patched_init: + mock_components = mock.MagicMock(application.Application, config=mock_config) + mock_low_level_rest_clients = mock.MagicMock(low_level_rest.RESTSession) + with mock.patch.object(low_level_rest, "RESTSession", return_value=mock_low_level_rest_clients) as patched_init: client = high_level_rest.RESTClient(mock_components) patched_init.assert_called_once_with( allow_redirects=mock_config.allow_redirects, @@ -57,22 +57,22 @@ def test_init(self, token, token_type, expected_token): version=mock_config.rest_version, ) assert client._session is mock_low_level_rest_clients - assert client._components is mock_components + assert client._app is mock_components def test_inheritance(self): for attr, routine in ( member for component in [ - high_level_rest.channel.RESTChannelComponent, - high_level_rest.me.RESTCurrentUserComponent, - high_level_rest.gateway.RESTGatewayComponent, - high_level_rest.guild.RESTGuildComponent, - high_level_rest.invite.RESTInviteComponent, - high_level_rest.oauth2.RESTOAuth2Component, - high_level_rest.react.RESTReactionComponent, - high_level_rest.user.RESTUserComponent, - high_level_rest.voice.RESTVoiceComponent, - high_level_rest.webhook.RESTWebhookComponent, + hikari.rest.channel.RESTChannelComponent, + hikari.rest.me.RESTCurrentUserComponent, + hikari.rest.gateway.RESTGatewayComponent, + hikari.rest.guild.RESTGuildComponent, + hikari.rest.invite.RESTInviteComponent, + hikari.rest.oauth2.RESTOAuth2Component, + hikari.rest.react.RESTReactionComponent, + hikari.rest.user.RESTUserComponent, + hikari.rest.voice.RESTVoiceComponent, + hikari.rest.webhook.RESTWebhookComponent, ] for member in inspect.getmembers(component, inspect.isroutine) ): @@ -84,5 +84,5 @@ def test_inheritance(self): assert getattr(high_level_rest.RESTClient, attr) == routine, ( f"Mismatching method found on RestClient; expected {routine.__qualname__} but got " f"{getattr(high_level_rest.RESTClient, attr).__qualname__}. `{attr}` is most likely being declared on" - "multiple components." + "multiple application." ) diff --git a/tests/hikari/clients/test_rest/test_base.py b/tests/hikari/clients/test_rest/test_base.py index 08638a2611..aef82937cf 100644 --- a/tests/hikari/clients/test_rest/test_base.py +++ b/tests/hikari/clients/test_rest/test_base.py @@ -19,16 +19,16 @@ import mock import pytest -from hikari.clients import components -from hikari.clients.rest import base -from hikari.net import ratelimits -from hikari.net import rest +from hikari.components import application +from hikari.rest import base +from hikari.internal import ratelimits +from hikari.rest import session @pytest.fixture() -def low_level_rest_impl() -> rest.REST: +def low_level_rest_impl() -> session.RESTSession: return mock.MagicMock( - spec=rest.REST, + spec=session.RESTSession, global_ratelimiter=mock.create_autospec(ratelimits.ManualRateLimiter, spec_set=True), bucket_ratelimiters=mock.create_autospec(ratelimits.RESTBucketManager, spec_set=True), ) @@ -38,7 +38,7 @@ def low_level_rest_impl() -> rest.REST: def rest_clients_impl(low_level_rest_impl) -> base.BaseRESTComponent: class RestClientImpl(base.BaseRESTComponent): def __init__(self): - super().__init__(mock.MagicMock(components.Components), low_level_rest_impl) + super().__init__(mock.MagicMock(application.Application), low_level_rest_impl) return RestClientImpl() diff --git a/tests/hikari/clients/test_rest/test_channel.py b/tests/hikari/clients/test_rest/test_channel.py index 7afc5613fa..d396ab33a1 100644 --- a/tests/hikari/clients/test_rest/test_channel.py +++ b/tests/hikari/clients/test_rest/test_channel.py @@ -24,19 +24,19 @@ import mock import pytest -from hikari import bases -from hikari import channels -from hikari import embeds -from hikari import files -from hikari import guilds -from hikari import invites -from hikari import messages -from hikari import users -from hikari import webhooks -from hikari.clients import components -from hikari.clients.rest import channel +from hikari.models import bases +from hikari.models import channels +from hikari.models import embeds +from hikari.models import files +from hikari.models import guilds +from hikari.models import invites +from hikari.models import messages +from hikari.models import users +from hikari.models import webhooks +from hikari.components import application +from hikari.rest import channel from hikari.internal import helpers -from hikari.net import rest +from hikari.rest import session from tests.hikari import _helpers @@ -44,11 +44,11 @@ class TestMessagePaginator: @pytest.fixture def mock_session(self): - return mock.MagicMock(spec_set=rest.REST) + return mock.MagicMock(spec_set=session.RESTSession) @pytest.fixture def mock_components(self): - return mock.MagicMock(spec_set=components.Components) + return mock.MagicMock(spec_set=application.Application) @pytest.fixture def message_cls(self): @@ -182,8 +182,8 @@ class DummyResponse: class TestRESTChannel: @pytest.fixture() def rest_channel_logic_impl(self): - mock_components = mock.MagicMock(components.Components) - mock_low_level_restful_client = mock.MagicMock(rest.REST) + mock_components = mock.MagicMock(application.Application) + mock_low_level_restful_client = mock.MagicMock(session.RESTSession) class RESTChannelLogicImpl(channel.RESTChannelComponent): def __init__(self): diff --git a/tests/hikari/clients/test_rest/test_gateway.py b/tests/hikari/clients/test_rest/test_gateway.py index 11f2a7a26c..afb66369bf 100644 --- a/tests/hikari/clients/test_rest/test_gateway.py +++ b/tests/hikari/clients/test_rest/test_gateway.py @@ -20,17 +20,17 @@ import mock import pytest -from hikari import gateway_entities -from hikari.clients import components -from hikari.clients.rest import gateway -from hikari.net import rest +from hikari.models import gateway +from hikari.components import application +from hikari.rest import gateway +from hikari.rest import session class TestRESTReactionLogic: @pytest.fixture() def rest_gateway_logic_impl(self): - mock_components = mock.MagicMock(components.Components) - mock_low_level_restful_client = mock.MagicMock(rest.REST) + mock_components = mock.MagicMock(application.Application) + mock_low_level_restful_client = mock.MagicMock(session.RESTSession) class RESTGatewayLogicImpl(gateway.RESTGatewayComponent): def __init__(self): @@ -48,11 +48,11 @@ async def test_fetch_gateway_url(self, rest_gateway_logic_impl): @pytest.mark.asyncio async def test_fetch_gateway_bot(self, rest_gateway_logic_impl): mock_payload = {"url": "wss://gateway.discord.gg/", "shards": 9, "session_start_limit": {}} - mock_gateway_bot_obj = mock.MagicMock(gateway_entities.GatewayBot) + mock_gateway_bot_obj = mock.MagicMock(gateway.GatewayBot) rest_gateway_logic_impl._session.get_gateway_bot.return_value = mock_payload - with mock.patch.object(gateway_entities.GatewayBot, "deserialize", return_value=mock_gateway_bot_obj): + with mock.patch.object(gateway.GatewayBot, "deserialize", return_value=mock_gateway_bot_obj): assert await rest_gateway_logic_impl.fetch_gateway_bot() is mock_gateway_bot_obj rest_gateway_logic_impl._session.get_gateway_bot.assert_called_once() - gateway_entities.GatewayBot.deserialize.assert_called_once_with( + gateway.GatewayBot.deserialize.assert_called_once_with( mock_payload, components=rest_gateway_logic_impl._components ) diff --git a/tests/hikari/clients/test_rest/test_guild.py b/tests/hikari/clients/test_rest/test_guild.py index 6a3c28a4b2..e7f9f28586 100644 --- a/tests/hikari/clients/test_rest/test_guild.py +++ b/tests/hikari/clients/test_rest/test_guild.py @@ -23,32 +23,32 @@ import mock import pytest -from hikari import audit_logs -from hikari import bases -from hikari import channels -from hikari import colors -from hikari import emojis -from hikari import files -from hikari import guilds -from hikari import invites -from hikari import permissions -from hikari import users -from hikari import voices -from hikari import webhooks -from hikari.clients import components -from hikari.clients.rest import guild as _guild -from hikari.net import rest +from hikari.models import audit_logs +from hikari.models import bases +from hikari.models import channels +from hikari.models import colors +from hikari.models import emojis +from hikari.models import files +from hikari.models import guilds +from hikari.models import invites +from hikari.models import permissions +from hikari.models import users +from hikari.models import voices +from hikari.models import webhooks +from hikari.components import application +from hikari.rest import guild as _guild +from hikari.rest import session from tests.hikari import _helpers class TestMemberPaginator: @pytest.fixture() def mock_session(self): - return mock.MagicMock(spec_set=rest.REST) + return mock.MagicMock(spec_set=session.RESTSession) @pytest.fixture() def mock_components(self): - return mock.MagicMock(spec_set=components.Components) + return mock.MagicMock(spec_set=application.Application) @pytest.fixture() def member_cls(self): @@ -167,8 +167,8 @@ async def test_next_chunk_deserializes_payload_in_generator_lazily(self, mock_se class TestRESTGuildLogic: @pytest.fixture() def rest_guild_logic_impl(self): - mock_components = mock.MagicMock(components.Components) - mock_low_level_restful_client = mock.MagicMock(rest.REST) + mock_components = mock.MagicMock(application.Application) + mock_low_level_restful_client = mock.MagicMock(session.RESTSession) class RESTGuildLogicImpl(_guild.RESTGuildComponent): def __init__(self): diff --git a/tests/hikari/clients/test_rest/test_invite.py b/tests/hikari/clients/test_rest/test_invite.py index 54d9193ea3..a555a1da18 100644 --- a/tests/hikari/clients/test_rest/test_invite.py +++ b/tests/hikari/clients/test_rest/test_invite.py @@ -19,17 +19,17 @@ import mock import pytest -from hikari import invites -from hikari.clients import components -from hikari.clients.rest import invite -from hikari.net import rest +from hikari.models import invites +from hikari.components import application +from hikari.rest import invite +from hikari.rest import session class TestRESTInviteLogic: @pytest.fixture() def rest_invite_logic_impl(self): - mock_components = mock.MagicMock(components.Components) - mock_low_level_restful_client = mock.MagicMock(rest.REST) + mock_components = mock.MagicMock(application.Application) + mock_low_level_restful_client = mock.MagicMock(session.RESTSession) class RESTInviteLogicImpl(invite.RESTInviteComponent): def __init__(self): diff --git a/tests/hikari/clients/test_rest/test_me.py b/tests/hikari/clients/test_rest/test_me.py index 6a327ea283..420f6ce7e0 100644 --- a/tests/hikari/clients/test_rest/test_me.py +++ b/tests/hikari/clients/test_rest/test_me.py @@ -22,26 +22,26 @@ import mock import pytest -from hikari import applications -from hikari import bases -from hikari import channels -from hikari import files -from hikari import guilds -from hikari import users -from hikari.clients import components -from hikari.clients.rest import me -from hikari.net import rest +from hikari.models import applications +from hikari.models import bases +from hikari.models import channels +from hikari.models import files +from hikari.models import guilds +from hikari.models import users +from hikari.components import application +from hikari.rest import me +from hikari.rest import session from tests.hikari import _helpers class TestGuildPaginator: @pytest.fixture() def mock_session(self): - return mock.MagicMock(spec_set=rest.REST) + return mock.MagicMock(spec_set=session.RESTSession) @pytest.fixture() def mock_components(self): - return mock.MagicMock(spec_set=components.Components) + return mock.MagicMock(spec_set=application.Application) @pytest.fixture() def ownguild_cls(self): @@ -149,8 +149,8 @@ async def test_next_chunk_deserializes_payload_in_generator_lazily( class TestRESTInviteLogic: @pytest.fixture() def rest_clients_impl(self): - mock_components = mock.MagicMock(components.Components) - mock_low_level_restful_client = mock.MagicMock(rest.REST) + mock_components = mock.MagicMock(application.Application) + mock_low_level_restful_client = mock.MagicMock(session.RESTSession) class RESTCurrentUserLogicImpl(me.RESTCurrentUserComponent): def __init__(self): diff --git a/tests/hikari/clients/test_rest/test_oauth2.py b/tests/hikari/clients/test_rest/test_oauth2.py index b9f2d1bbae..3d9d800d4c 100644 --- a/tests/hikari/clients/test_rest/test_oauth2.py +++ b/tests/hikari/clients/test_rest/test_oauth2.py @@ -20,17 +20,17 @@ import mock import pytest -from hikari import applications -from hikari.clients import components -from hikari.clients.rest import oauth2 -from hikari.net import rest +from hikari.models import applications +from hikari.components import application +from hikari.rest import oauth2 +from hikari.rest import session class TestRESTReactionLogic: @pytest.fixture() def rest_oauth2_logic_impl(self): - mock_components = mock.MagicMock(components.Components) - mock_low_level_restful_client = mock.MagicMock(rest.REST) + mock_components = mock.MagicMock(application.Application) + mock_low_level_restful_client = mock.MagicMock(session.RESTSession) class RESTOauth2LogicImpl(oauth2.RESTOAuth2Component): def __init__(self): @@ -40,7 +40,7 @@ def __init__(self): @pytest.mark.asyncio async def test_fetch_my_application_info(self, rest_oauth2_logic_impl): - mock_application_payload = {"id": "2929292", "name": "blah blah", "description": "an app"} + mock_application_payload = {"id": "2929292", "name": "blah blah", "description": "an application"} mock_application_obj = mock.MagicMock(applications.Application) rest_oauth2_logic_impl._session.get_current_application_info.return_value = mock_application_payload with mock.patch.object(applications.Application, "deserialize", return_value=mock_application_obj): diff --git a/tests/hikari/clients/test_rest/test_react.py b/tests/hikari/clients/test_rest/test_react.py index 9ed6930fe1..60ac20d729 100644 --- a/tests/hikari/clients/test_rest/test_react.py +++ b/tests/hikari/clients/test_rest/test_react.py @@ -22,14 +22,14 @@ import mock import pytest -from hikari import bases -from hikari import channels -from hikari import emojis -from hikari import messages -from hikari import users -from hikari.clients import components -from hikari.clients.rest import react -from hikari.net import rest +from hikari.models import bases +from hikari.models import channels +from hikari.models import emojis +from hikari.models import messages +from hikari.models import users +from hikari.components import application +from hikari.rest import react +from hikari.rest import session from tests.hikari import _helpers @@ -45,11 +45,11 @@ class TestMemberPaginator: @pytest.fixture() def mock_session(self): - return mock.MagicMock(spec_set=rest.REST) + return mock.MagicMock(spec_set=session.RESTSession) @pytest.fixture() def mock_components(self): - return mock.MagicMock(spec_set=components.Components) + return mock.MagicMock(spec_set=application.Application) @pytest.fixture() def user_cls(self): @@ -185,8 +185,8 @@ async def test_next_chunk_deserializes_payload_in_generator_lazily( class TestRESTReactionLogic: @pytest.fixture() def rest_reaction_logic_impl(self): - mock_components = mock.MagicMock(components.Components) - mock_low_level_restful_client = mock.MagicMock(rest.REST) + mock_components = mock.MagicMock(application.Application) + mock_low_level_restful_client = mock.MagicMock(session.RESTSession) class RESTReactionLogicImpl(react.RESTReactionComponent): def __init__(self): @@ -236,7 +236,7 @@ async def test_delete_reaction_for_other_user(self, rest_reaction_logic_impl, ch @_helpers.parametrize_valid_id_formats_for_models("message", 987654321, messages.Message) @pytest.mark.parametrize("emoji", [None, "blah:123", emojis.CustomEmoji(name="blah", id=123, is_animated=False)]) async def test_delete_all_reactions(self, rest_reaction_logic_impl, channel, message, emoji): - rest_reaction_logic_impl._session = mock.MagicMock(spec_set=rest.REST) + rest_reaction_logic_impl._session = mock.MagicMock(spec_set=session.RESTSession) assert ( await rest_reaction_logic_impl.remove_all_reactions(channel=channel, message=message, emoji=emoji) is None ) diff --git a/tests/hikari/clients/test_rest/test_user.py b/tests/hikari/clients/test_rest/test_user.py index e73b1f25a1..aa9932c98e 100644 --- a/tests/hikari/clients/test_rest/test_user.py +++ b/tests/hikari/clients/test_rest/test_user.py @@ -19,18 +19,18 @@ import mock import pytest -from hikari import users -from hikari.clients import components -from hikari.clients.rest import user -from hikari.net import rest +from hikari.models import users +from hikari.components import application +from hikari.rest import user +from hikari.rest import session from tests.hikari import _helpers class TestRESTUserLogic: @pytest.fixture() def rest_user_logic_impl(self): - mock_components = mock.MagicMock(components.Components) - mock_low_level_restful_client = mock.MagicMock(rest.REST) + mock_components = mock.MagicMock(application.Application) + mock_low_level_restful_client = mock.MagicMock(session.RESTSession) class RESTUserLogicImpl(user.RESTUserComponent): def __init__(self): diff --git a/tests/hikari/clients/test_rest/test_voice.py b/tests/hikari/clients/test_rest/test_voice.py index 3ebf760bd5..659e4dbfb9 100644 --- a/tests/hikari/clients/test_rest/test_voice.py +++ b/tests/hikari/clients/test_rest/test_voice.py @@ -19,17 +19,17 @@ import mock import pytest -from hikari import voices -from hikari.clients import components -from hikari.clients.rest import voice -from hikari.net import rest +from hikari.models import voices +from hikari.components import application +from hikari.rest import voice +from hikari.rest import session class TestRESTUserLogic: @pytest.fixture() def rest_voice_logic_impl(self): - mock_components = mock.MagicMock(components.Components) - mock_low_level_restful_client = mock.MagicMock(rest.REST) + mock_components = mock.MagicMock(application.Application) + mock_low_level_restful_client = mock.MagicMock(session.RESTSession) class RESTVoiceLogicImpl(voice.RESTVoiceComponent): def __init__(self): diff --git a/tests/hikari/clients/test_rest/test_webhook.py b/tests/hikari/clients/test_rest/test_webhook.py index 91d9795790..75e7d4e830 100644 --- a/tests/hikari/clients/test_rest/test_webhook.py +++ b/tests/hikari/clients/test_rest/test_webhook.py @@ -21,22 +21,22 @@ import mock import pytest -from hikari import embeds -from hikari import files -from hikari import messages -from hikari import webhooks -from hikari.clients import components -from hikari.clients.rest import webhook +from hikari.models import embeds +from hikari.models import files +from hikari.models import messages +from hikari.models import webhooks +from hikari.components import application +from hikari.rest import webhook from hikari.internal import helpers -from hikari.net import rest +from hikari.rest import session from tests.hikari import _helpers class TestRESTUserLogic: @pytest.fixture() def rest_webhook_logic_impl(self): - mock_components = mock.MagicMock(components.Components) - mock_low_level_restful_client = mock.MagicMock(rest.REST) + mock_components = mock.MagicMock(application.Application) + mock_low_level_restful_client = mock.MagicMock(session.RESTSession) class RESTWebhookLogicImpl(webhook.RESTWebhookComponent): def __init__(self): diff --git a/tests/hikari/clients/test_shards.py b/tests/hikari/clients/test_shards.py index 5b426e85a1..5f95dd3f7a 100644 --- a/tests/hikari/clients/test_shards.py +++ b/tests/hikari/clients/test_shards.py @@ -24,16 +24,16 @@ import mock import pytest -import hikari.clients.shard_states +import hikari.gateway.gateway_state +from hikari.models import guilds from hikari import errors -from hikari import guilds -from hikari.clients import components -from hikari.clients import configs -from hikari.clients import shards as high_level_shards -from hikari.events import consumers +from hikari.components import application +from hikari import configs +from hikari.gateway import client as high_level_shards +from hikari.components import consumers from hikari.internal import more_asyncio -from hikari.net import codes -from hikari.net import shards as low_level_shards +from hikari.internal import codes +from hikari.gateway import connection as low_level_shards from tests.hikari import _helpers @@ -54,7 +54,7 @@ def done(self): @pytest.fixture def mock_components(): - class ComponentsImpl(components.Components): + class ApplicationImpl(application.Application): def __init__(self): super().__init__( config=mock.MagicMock(), @@ -64,7 +64,7 @@ def __init__(self): shards=mock.MagicMock(), ) - return ComponentsImpl() + return ApplicationImpl() @pytest.fixture @@ -78,7 +78,7 @@ def shard_client_obj(mock_components): session_id=None, ) with mock.patch("hikari.net.shards.Shard", return_value=mock_shard_connection): - return _helpers.unslot_class(high_level_shards.ShardClientImpl)(0, 1, mock_components, "some_url") + return _helpers.unslot_class(high_level_shards.GatewayClient)(0, 1, mock_components, "some_url") class TestShardClientImpl: @@ -87,10 +87,10 @@ class DummyConsumer(consumers.RawEventConsumer): def process_raw_event(self, _client, name, payload): return "ASSERT TRUE" - shard_client_obj = high_level_shards.ShardClientImpl( + shard_client_obj = high_level_shards.GatewayClient( 0, 1, - mock.MagicMock(components.Components, config=configs.GatewayConfig(), event_manager=DummyConsumer()), + mock.MagicMock(application.Application, config=configs.GatewayConfig(), event_manager=DummyConsumer()), "some_url", ) @@ -100,10 +100,10 @@ def test_connection_is_set(self, shard_client_obj): mock_shard_connection = mock.MagicMock(low_level_shards.Shard) with mock.patch("hikari.net.shards.Shard", return_value=mock_shard_connection): - shard_client_obj = high_level_shards.ShardClientImpl( + shard_client_obj = high_level_shards.GatewayClient( 0, 1, - mock.MagicMock(components.Components, event_manager=None, config=configs.GatewayConfig()), + mock.MagicMock(application.Application, event_manager=None, config=configs.GatewayConfig()), "some_url", ) @@ -278,7 +278,7 @@ async def forever(): @_helpers.assert_raises(type_=RuntimeError) @pytest.mark.asyncio async def test_start_when_already_started(self, shard_client_obj): - shard_client_obj._shard_state = hikari.clients.shard_states.ShardState.READY + shard_client_obj._shard_state = hikari.gateway.gateway_state.GatewayState.READY await shard_client_obj.start() @@ -308,7 +308,7 @@ async def test_close(self, shard_client_obj): @pytest.mark.asyncio async def test_close_when_already_stopping(self, shard_client_obj): - shard_client_obj._shard_state = hikari.clients.shard_states.ShardState.STOPPING + shard_client_obj._shard_state = hikari.gateway.gateway_state.GatewayState.STOPPING await shard_client_obj.close() @@ -316,7 +316,7 @@ async def test_close_when_already_stopping(self, shard_client_obj): @pytest.mark.asyncio async def test_close_when_not_running_is_not_an_error(self, shard_client_obj): - shard_client_obj._shard_state = hikari.clients.shard_states.ShardState.NOT_RUNNING + shard_client_obj._shard_state = hikari.gateway.gateway_state.GatewayState.NOT_RUNNING shard_client_obj._task = None await shard_client_obj.close() diff --git a/tests/hikari/events/test_base.py b/tests/hikari/events/test_base.py index ce2b58ee7c..1ecfa1cb63 100644 --- a/tests/hikari/events/test_base.py +++ b/tests/hikari/events/test_base.py @@ -16,7 +16,7 @@ # # You should have received a copy of the GNU Lesser General Public License # along ith Hikari. If not, see . -from hikari import intents +from hikari.models import intents from hikari.events import base from hikari.internal import more_collections diff --git a/tests/hikari/events/test_channel.py b/tests/hikari/events/test_channel.py index 972b72186b..507702ac62 100644 --- a/tests/hikari/events/test_channel.py +++ b/tests/hikari/events/test_channel.py @@ -22,10 +22,7 @@ import mock import pytest -from hikari import channels -from hikari import guilds -from hikari import invites -from hikari import users +from hikari.models import guilds, users, channels, invites from hikari.events import channel from hikari.internal import conversions from tests.hikari import _helpers diff --git a/tests/hikari/events/test_dispatchers.py b/tests/hikari/events/test_dispatchers.py index 12457fea91..1b4532dcaf 100644 --- a/tests/hikari/events/test_dispatchers.py +++ b/tests/hikari/events/test_dispatchers.py @@ -20,7 +20,7 @@ import pytest from hikari import events -from hikari.events import dispatchers +from hikari.components import dispatchers from tests.hikari import _helpers diff --git a/tests/hikari/events/test_guild.py b/tests/hikari/events/test_guild.py index 86ba22fc8f..305f071fbf 100644 --- a/tests/hikari/events/test_guild.py +++ b/tests/hikari/events/test_guild.py @@ -22,10 +22,7 @@ import mock import pytest -from hikari import emojis -from hikari import guilds -from hikari import unset -from hikari import users +from hikari.models import unset, guilds, users, emojis from hikari.events import guild from hikari.internal import conversions from tests.hikari import _helpers diff --git a/tests/hikari/events/test_intent_aware_dispatchers.py b/tests/hikari/events/test_intent_aware_dispatchers.py index d41c892fc3..06c24bb453 100644 --- a/tests/hikari/events/test_intent_aware_dispatchers.py +++ b/tests/hikari/events/test_intent_aware_dispatchers.py @@ -24,7 +24,7 @@ import pytest from hikari import events -from hikari.events import intent_aware_dispatchers +from hikari.components import intent_aware_dispatchers from tests.hikari import _helpers diff --git a/tests/hikari/events/test_message.py b/tests/hikari/events/test_message.py index 2680db76c5..f134e665a3 100644 --- a/tests/hikari/events/test_message.py +++ b/tests/hikari/events/test_message.py @@ -22,13 +22,7 @@ import mock import pytest -from hikari import applications -from hikari import embeds -from hikari import emojis -from hikari import guilds -from hikari import messages -from hikari import unset -from hikari import users +from hikari.models import unset, embeds, messages, guilds, applications, users, emojis from hikari.events import message from hikari.internal import conversions from tests.hikari import _helpers diff --git a/tests/hikari/events/test_other.py b/tests/hikari/events/test_other.py index 08a491c652..0d63a574c5 100644 --- a/tests/hikari/events/test_other.py +++ b/tests/hikari/events/test_other.py @@ -21,8 +21,7 @@ import mock import pytest -from hikari import guilds -from hikari import users +from hikari.models import guilds, users from hikari.events import other from tests.hikari import _helpers diff --git a/tests/hikari/internal/test_helpers.py b/tests/hikari/internal/test_helpers.py index b977948222..3636ad1685 100644 --- a/tests/hikari/internal/test_helpers.py +++ b/tests/hikari/internal/test_helpers.py @@ -18,8 +18,7 @@ # along with Hikari. If not, see . import pytest -from hikari import guilds -from hikari import users +from hikari.models import guilds, users from hikari.internal import helpers from tests.hikari import _helpers diff --git a/tests/hikari/internal/test_marshaller.py b/tests/hikari/internal/test_marshaller.py index 87eec3631a..f8dbfb2b0b 100644 --- a/tests/hikari/internal/test_marshaller.py +++ b/tests/hikari/internal/test_marshaller.py @@ -20,7 +20,7 @@ import mock import pytest -from hikari.internal import marshaller +from hikari.internal import marshaller, codes from tests.hikari import _helpers @@ -31,8 +31,6 @@ def test_dereference_handle_module_only(self): assert marshaller.dereference_handle("concurrent.futures") is futures def test_dereference_handle_module_and_attribute(self): - from hikari.net import codes - assert ( marshaller.dereference_handle("hikari.net.codes#GatewayCloseCode.AUTHENTICATION_FAILED") is codes.GatewayCloseCode.AUTHENTICATION_FAILED diff --git a/tests/hikari/net/test_codes.py b/tests/hikari/net/test_codes.py index d621f02e88..38e3ddd34c 100644 --- a/tests/hikari/net/test_codes.py +++ b/tests/hikari/net/test_codes.py @@ -17,7 +17,7 @@ # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . -from hikari.net import codes +from hikari.internal import codes def test_str_GatewayCloseCode(): diff --git a/tests/hikari/net/test_http_client.py b/tests/hikari/net/test_http_client.py index 0595c9dfc3..5b04e43952 100644 --- a/tests/hikari/net/test_http_client.py +++ b/tests/hikari/net/test_http_client.py @@ -23,8 +23,7 @@ import mock import pytest -from hikari.net import http_client -from hikari.net import tracing +from hikari.internal import http_client, tracing from tests.hikari import _helpers diff --git a/tests/hikari/net/test_ratelimits.py b/tests/hikari/net/test_ratelimits.py index 2b0f2074d2..7788bdc1e7 100644 --- a/tests/hikari/net/test_ratelimits.py +++ b/tests/hikari/net/test_ratelimits.py @@ -27,8 +27,8 @@ import mock import pytest -from hikari.net import ratelimits -from hikari.net import routes +from hikari.internal import ratelimits +from hikari.rest import routes from tests.hikari import _helpers diff --git a/tests/hikari/net/test_rest.py b/tests/hikari/net/test_rest.py index 65b2bd0807..6eec831dff 100644 --- a/tests/hikari/net/test_rest.py +++ b/tests/hikari/net/test_rest.py @@ -29,12 +29,12 @@ import pytest from hikari import errors -from hikari import files +from hikari.models import files from hikari.internal import conversions -from hikari.net import http_client -from hikari.net import ratelimits -from hikari.net import rest -from hikari.net import routes +from hikari.internal import http_client +from hikari.internal import ratelimits +from hikari.rest import session +from hikari.rest import routes from tests.hikari import _helpers @@ -70,27 +70,29 @@ def mock_patch_route(real_route): @pytest.mark.asyncio class TestRESTInit: async def test_base_url_is_formatted_correctly(self): - async with rest.REST(base_url="http://example.com/api/v{0.version}/test", token=None, version=69) as client: + async with session.RESTSession( + base_url="http://example.com/api/v{0.version}/test", token=None, version=69 + ) as client: assert client.base_url == "http://example.com/api/v69/test" async def test_no_token_sets_field_to_None(self): - async with rest.REST(token=None) as client: + async with session.RESTSession(token=None) as client: assert client._token is None @_helpers.assert_raises(type_=RuntimeError) async def test_bare_old_token_without_auth_scheme_raises_error(self): - async with rest.REST(token="1a2b.3c4d") as client: + async with session.RESTSession(token="1a2b.3c4d") as client: pass @_helpers.assert_raises(type_=RuntimeError) async def test_bare_old_token_without_recognised_auth_scheme_raises_error(self): - async with rest.REST(token="Token 1a2b.3c4d") as client: + async with session.RESTSession(token="Token 1a2b.3c4d") as client: pass @pytest.mark.parametrize("auth_type", ["Bot", "Bearer"]) async def test_known_auth_type_is_allowed(self, auth_type): token = f"{auth_type} 1a2b.3c4d" - async with rest.REST(token=token) as client: + async with session.RESTSession(token=token) as client: assert client._token == token @@ -98,9 +100,9 @@ async def test_known_auth_type_is_allowed(self, auth_type): class TestRESTClose: @pytest.fixture def rest_impl(self, event_loop): - rest_impl = rest.REST(token="Bot token") + rest_impl = session.RESTSession(token="Bot token") yield rest_impl - event_loop.run_until_complete(super(rest.REST, rest_impl).close()) + event_loop.run_until_complete(super(session.RESTSession, rest_impl).close()) rest_impl.bucket_ratelimiters.close() rest_impl.global_ratelimiter.close() @@ -138,7 +140,7 @@ def rest_impl(self, bucket_ratelimiters, global_ratelimiter): stack.enter_context(mock.patch.object(ratelimits, "RESTBucketManager", return_value=bucket_ratelimiters)) stack.enter_context(mock.patch.object(ratelimits, "ManualRateLimiter", return_value=global_ratelimiter)) with stack: - client = rest.REST(base_url="http://example.bloop.com", token="Bot blah.blah.blah") + client = session.RESTSession(base_url="http://example.bloop.com", token="Bot blah.blah.blah") client.logger = mock.MagicMock(spec_set=logging.Logger) client.json_deserialize = json.loads client.serialize = json.dumps @@ -340,7 +342,7 @@ async def test_ratelimited_429_retries_request_until_it_works(self, compiled_rou rest_impl._handle_rate_limits_for_response = mock.AsyncMock( # In reality, the ratelimiting logic will ensure we wait before retrying, but this # is a test for a spammy edge-case scenario. - side_effect=[rest._RateLimited, rest._RateLimited, rest._RateLimited, None] + side_effect=[session._RateLimited, session._RateLimited, session._RateLimited, None] ) # when await rest_impl._request_json_response(compiled_route) @@ -377,7 +379,7 @@ def rest_impl(self, bucket_ratelimiters, global_ratelimiter): stack.enter_context(mock.patch.object(ratelimits, "RESTBucketManager", return_value=bucket_ratelimiters)) stack.enter_context(mock.patch.object(ratelimits, "ManualRateLimiter", return_value=global_ratelimiter)) with stack: - client = rest.REST(base_url="http://example.bloop.com", token="Bot blah.blah.blah") + client = session.RESTSession(base_url="http://example.bloop.com", token="Bot blah.blah.blah") client.logger = mock.MagicMock(spec_set=logging.Logger) return client @@ -414,7 +416,7 @@ async def test_bucket_ratelimiter_updated( ) @pytest.mark.parametrize("body", [b"{}", b'{"global": false}']) - @_helpers.assert_raises(type_=rest._RateLimited) + @_helpers.assert_raises(type_=session._RateLimited) async def test_non_global_429_raises_Ratelimited(self, rest_impl, compiled_route, body): response = MockResponse( headers={ @@ -448,7 +450,7 @@ async def test_global_429_throttles_then_raises_Ratelimited(self, global_ratelim try: await rest_impl._handle_rate_limits_for_response(compiled_route, response) assert False - except rest._RateLimited: + except session._RateLimited: global_ratelimiter.throttle.assert_called_once_with(1024.768) async def test_non_json_429_causes_httperror(self, rest_impl, compiled_route): @@ -483,7 +485,7 @@ def rest_impl(self): stack.enter_context(mock.patch.object(ratelimits, "RESTBucketManager")) stack.enter_context(mock.patch.object(ratelimits, "ManualRateLimiter")) with stack: - client = rest.REST(base_url="https://discord.com/api/v6", token="Bot blah.blah.blah") + client = session.RESTSession(base_url="https://discord.com/api/v6", token="Bot blah.blah.blah") client.logger = mock.MagicMock(spec_set=logging.Logger) client._request_json_response = mock.AsyncMock(return_value=...) client.client_session = mock.MagicMock(aiohttp.ClientSession, spec_set=True) diff --git a/tests/hikari/net/test_routes.py b/tests/hikari/net/test_routes.py index b5f6535746..a2635e51a6 100644 --- a/tests/hikari/net/test_routes.py +++ b/tests/hikari/net/test_routes.py @@ -19,7 +19,7 @@ import mock import pytest -from hikari.net import routes +from hikari.rest import routes class TestCompiledRoute: diff --git a/tests/hikari/net/test_shards.py b/tests/hikari/net/test_shards.py index e73824a354..4028c60ee5 100644 --- a/tests/hikari/net/test_shards.py +++ b/tests/hikari/net/test_shards.py @@ -30,9 +30,9 @@ from hikari import errors from hikari.internal import more_collections -from hikari.net import codes -from hikari.net import shards -from hikari.net import user_agents +from hikari.internal import codes +from hikari.gateway import connection +from hikari.internal import user_agents from tests.hikari import _helpers @@ -86,12 +86,12 @@ async def ws_connect(self, *args, **kwargs): class TestShardConstructor: async def test_init_sets_shard_numbers_correctly(self): input_shard_id, input_shard_count, expected_shard_id, expected_shard_count = 1, 2, 1, 2 - client = shards.Shard(shard_id=input_shard_id, shard_count=input_shard_count, token="xxx", url="yyy") + client = connection.Shard(shard_id=input_shard_id, shard_count=input_shard_count, token="xxx", url="yyy") assert client.shard_id == expected_shard_id assert client.shard_count == expected_shard_count async def test_dispatch_is_callable(self): - client = shards.Shard(token="xxx", url="yyy") + client = connection.Shard(token="xxx", url="yyy") client.dispatcher(client, "ping", "pong") @pytest.mark.parametrize( @@ -103,7 +103,7 @@ async def test_dispatch_is_callable(self): ) async def test_compression(self, compression, expected_url_query): url = "ws://baka-im-not-a-http-url:49620/locate/the/bloody/websocket?ayyyyy=lmao" - client = shards.Shard(token="xxx", url=url, compression=compression) + client = connection.Shard(token="xxx", url=url, compression=compression) scheme, netloc, path, params, query, fragment = urllib.parse.urlparse(client._url) assert scheme == "ws" assert netloc == "baka-im-not-a-http-url:49620" @@ -114,13 +114,13 @@ async def test_compression(self, compression, expected_url_query): assert fragment == "" async def test_init_hearbeat_defaults_before_startup(self): - client = shards.Shard(token="xxx", url="yyy") + client = connection.Shard(token="xxx", url="yyy") assert math.isnan(client.last_heartbeat_sent) assert math.isnan(client.heartbeat_latency) assert math.isnan(client.last_message_received) async def test_init_connected_at_is_nan(self): - client = shards.Shard(token="xxx", url="yyy") + client = connection.Shard(token="xxx", url="yyy") assert math.isnan(client._connected_at) @@ -132,7 +132,7 @@ class TestShardUptimeProperty: ) async def test_uptime(self, connected_at, now, expected_uptime): with mock.patch("time.perf_counter", return_value=now): - client = shards.Shard(token="xxx", url="yyy") + client = connection.Shard(token="xxx", url="yyy") client._connected_at = connected_at assert client.up_time == expected_uptime @@ -141,7 +141,7 @@ async def test_uptime(self, connected_at, now, expected_uptime): class TestShardIsConnectedProperty: @pytest.mark.parametrize(["connected_at", "is_connected"], [(float("nan"), False), (15, True), (2500.0, True),]) async def test_is_connected(self, connected_at, is_connected): - client = shards.Shard(token="xxx", url="yyy") + client = connection.Shard(token="xxx", url="yyy") client._connected_at = connected_at assert client.is_connected is is_connected @@ -162,7 +162,7 @@ class TestGatewayReconnectCountProperty: ], ) async def test_value(self, disconnect_count, is_connected, expected_reconnect_count): - client = shards.Shard(token="xxx", url="yyy") + client = connection.Shard(token="xxx", url="yyy") client.disconnect_count = disconnect_count client._connected_at = 420 if is_connected else float("nan") assert client.reconnect_count == expected_reconnect_count @@ -171,12 +171,12 @@ async def test_value(self, disconnect_count, is_connected, expected_reconnect_co @pytest.mark.asyncio class TestGatewayCurrentPresenceProperty: async def test_returns_presence(self): - client = shards.Shard(token="xxx", url="yyy") + client = connection.Shard(token="xxx", url="yyy") client._presence = {"foo": "bar"} assert client.current_presence == {"foo": "bar"} async def test_returns_copy(self): - client = shards.Shard(token="xxx", url="yyy") + client = connection.Shard(token="xxx", url="yyy") client._presence = {"foo": "bar"} assert client.current_presence is not client._presence @@ -194,7 +194,7 @@ def non_hello_payload(self): @pytest.fixture def client(self, event_loop): asyncio.set_event_loop(event_loop) - client = _helpers.unslot_class(shards.Shard)(url="ws://localhost", token="xxx") + client = _helpers.unslot_class(connection.Shard)(url="ws://localhost", token="xxx") client = _helpers.mock_methods_on(client, except_=("connect",)) client._receive = mock.AsyncMock(return_value=self.hello_payload) return client @@ -465,7 +465,7 @@ class TestShardRun: @pytest.fixture def client(self, event_loop): asyncio.set_event_loop(event_loop) - client = _helpers.unslot_class(shards.Shard)(token="1234", url="xxx") + client = _helpers.unslot_class(connection.Shard)(token="1234", url="xxx") client = _helpers.mock_methods_on(client, except_=("_run",)) def receive(): @@ -477,9 +477,9 @@ def identify(): def resume(): client.resume_time = time.perf_counter() - client._identify = mock.AsyncMock(spec=shards.Shard._identify, wraps=identify) - client._resume = mock.AsyncMock(spec=shards.Shard._resume, wraps=resume) - client._receive = mock.AsyncMock(spec=shards.Shard._receive, wraps=receive) + client._identify = mock.AsyncMock(spec=connection.Shard._identify, wraps=identify) + client._resume = mock.AsyncMock(spec=connection.Shard._resume, wraps=resume) + client._receive = mock.AsyncMock(spec=connection.Shard._receive, wraps=receive) return client async def test_no_session_id_sends_identify_then_polls_events(self, client): @@ -512,7 +512,7 @@ class TestIdentify: @pytest.fixture def client(self, event_loop): asyncio.set_event_loop(event_loop) - client = _helpers.unslot_class(shards.Shard)(token="1234", url="xxx") + client = _helpers.unslot_class(connection.Shard)(token="1234", url="xxx") client = _helpers.mock_methods_on(client, except_=("_identify",)) return client @@ -626,7 +626,7 @@ class TestResume: @pytest.fixture def client(self, event_loop): asyncio.set_event_loop(event_loop) - client = _helpers.unslot_class(shards.Shard)(token="1234", url="xxx") + client = _helpers.unslot_class(connection.Shard)(token="1234", url="xxx") client = _helpers.mock_methods_on(client, except_=("_resume",)) return client @@ -649,7 +649,7 @@ class TestHeartbeatKeepAlive: @pytest.fixture def client(self, event_loop): asyncio.set_event_loop(event_loop) - client = _helpers.unslot_class(shards.Shard)(token="1234", url="xxx") + client = _helpers.unslot_class(connection.Shard)(token="1234", url="xxx") client = _helpers.mock_methods_on(client, except_=("_heartbeat_keep_alive", "_zombie_detector")) client._send = mock.AsyncMock() # This won't get set on the right event loop if we are not careful @@ -709,14 +709,14 @@ async def test_zombie_detector_not_a_zombie(self): client = mock.MagicMock() client.last_message_received = time.perf_counter() - 5 heartbeat_interval = 41.25 - shards.Shard._zombie_detector(client, heartbeat_interval) + connection.Shard._zombie_detector(client, heartbeat_interval) @_helpers.assert_raises(type_=asyncio.TimeoutError) async def test_zombie_detector_is_a_zombie(self): client = mock.MagicMock() client.last_message_received = time.perf_counter() - 500000 heartbeat_interval = 41.25 - shards.Shard._zombie_detector(client, heartbeat_interval) + connection.Shard._zombie_detector(client, heartbeat_interval) @pytest.mark.asyncio @@ -724,7 +724,7 @@ class TestClose: @pytest.fixture def client(self, event_loop): asyncio.set_event_loop(event_loop) - client = _helpers.unslot_class(shards.Shard)(token="1234", url="xxx") + client = _helpers.unslot_class(connection.Shard)(token="1234", url="xxx") client = _helpers.mock_methods_on(client, except_=("close",)) client.ws = mock.MagicMock(aiohttp.ClientWebSocketResponse) client.session = mock.MagicMock(aiohttp.ClientSession) @@ -795,7 +795,7 @@ class TestPollEvents: @pytest.fixture def client(self, event_loop): asyncio.set_event_loop(event_loop) - client = _helpers.unslot_class(shards.Shard)(token="1234", url="xxx") + client = _helpers.unslot_class(connection.Shard)(token="1234", url="xxx") client = _helpers.mock_methods_on(client, except_=("_run",)) return client @@ -835,7 +835,7 @@ class TestRequestGuildMembers: @pytest.fixture def client(self, event_loop): asyncio.set_event_loop(event_loop) - client = _helpers.unslot_class(shards.Shard)(token="1234", url="xxx") + client = _helpers.unslot_class(connection.Shard)(token="1234", url="xxx") client = _helpers.mock_methods_on(client, except_=("request_guild_members",)) return client @@ -872,7 +872,7 @@ class TestUpdatePresence: @pytest.fixture def client(self, event_loop): asyncio.set_event_loop(event_loop) - client = _helpers.unslot_class(shards.Shard)(token="1234", url="xxx") + client = _helpers.unslot_class(connection.Shard)(token="1234", url="xxx") client = _helpers.mock_methods_on(client, except_=("update_presence",)) return client @@ -898,7 +898,7 @@ class TestUpdateVoiceState: @pytest.fixture def client(self, event_loop): asyncio.set_event_loop(event_loop) - client = _helpers.unslot_class(shards.Shard)(token="1234", url="xxx") + client = _helpers.unslot_class(connection.Shard)(token="1234", url="xxx") client = _helpers.mock_methods_on(client, except_=("update_voice_state",)) return client diff --git a/tests/hikari/net/test_user_agents.py b/tests/hikari/net/test_user_agents.py index c10e2388b9..4de6ed8779 100644 --- a/tests/hikari/net/test_user_agents.py +++ b/tests/hikari/net/test_user_agents.py @@ -16,7 +16,7 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . -from hikari.net import user_agents +from hikari.internal import user_agents def test_library_version_is_callable_and_produces_string(): diff --git a/tests/hikari/state/test_stateless.py b/tests/hikari/state/test_stateless.py index 7e4852fd82..9f7256e43e 100644 --- a/tests/hikari/state/test_stateless.py +++ b/tests/hikari/state/test_stateless.py @@ -19,14 +19,14 @@ import mock import pytest -from hikari.clients import components -from hikari.clients import shards +from hikari.components import application +from hikari.gateway import client from hikari.events import channel from hikari.events import guild from hikari.events import message from hikari.events import other from hikari.events import voice -from hikari.state import stateless +from hikari.stateless import manager class TestStatelessEventManagerImpl: @@ -39,13 +39,13 @@ def event_manager_impl(self): class MockDispatcher: dispatch_event = mock.AsyncMock() - return stateless.StatelessEventManagerImpl( - components=mock.MagicMock(components.Components, event_dispatcher=MockDispatcher()) + return manager.StatelessEventManagerImpl( + components=mock.MagicMock(application.Application, event_dispatcher=MockDispatcher()) ) @pytest.fixture def mock_shard(self): - return mock.MagicMock(shards.ShardClient) + return mock.MagicMock(client.GatewayClient) @pytest.mark.asyncio async def test_on_connect(self, event_manager_impl, mock_shard): diff --git a/tests/hikari/test_applications.py b/tests/hikari/test_applications.py index c520e6fb48..c2ced4d0af 100644 --- a/tests/hikari/test_applications.py +++ b/tests/hikari/test_applications.py @@ -19,17 +19,15 @@ import mock import pytest -from hikari import applications -from hikari import guilds -from hikari import users -from hikari.clients import components +from hikari.models import guilds, applications, users +from hikari.components import application from hikari.internal import urls from tests.hikari import _helpers @pytest.fixture() def mock_components(): - return mock.MagicMock(components.Components) + return mock.MagicMock(application.Application) @pytest.fixture() @@ -95,7 +93,7 @@ def application_information_payload(owner_payload, team_payload): "id": "209333111222", "name": "Dream Sweet in Sea Major", "icon": "iwiwiwiwiw", - "description": "I am an app", + "description": "I am an application", "rpc_origins": ["127.0.0.0"], "bot_public": True, "bot_require_code_grant": False, @@ -214,13 +212,13 @@ def test_deserialize(self, application_information_payload, team_payload, owner_ application_information_payload, components=mock_components ) assert application_obj.team == applications.Team.deserialize(team_payload) - assert application_obj.team._components is mock_components + assert application_obj.team._app is mock_components assert application_obj.owner == applications.ApplicationOwner.deserialize(owner_payload) - assert application_obj.owner._components is mock_components + assert application_obj.owner._app is mock_components assert application_obj.id == 209333111222 assert application_obj.name == "Dream Sweet in Sea Major" assert application_obj.icon_hash == "iwiwiwiwiw" - assert application_obj.description == "I am an app" + assert application_obj.description == "I am an application" assert application_obj.rpc_origins == {"127.0.0.0"} assert application_obj.is_bot_public is True assert application_obj.is_bot_code_grant_required is False @@ -267,7 +265,9 @@ def test_format_icon_url(self, mock_application): mock_url = "https://cdn.discordapp.com/app-icons/22222/wosososoos.jpg?size=4" with mock.patch.object(urls, "generate_cdn_url", return_value=mock_url): url = applications.Application.format_icon_url(mock_application, format_="jpg", size=4) - urls.generate_cdn_url.assert_called_once_with("app-icons", "22222", "wosososoos", format_="jpg", size=4) + urls.generate_cdn_url.assert_called_once_with( + "application-icons", "22222", "wosososoos", format_="jpg", size=4 + ) assert url == mock_url def test_format_icon_url_returns_none(self, mock_application): @@ -289,7 +289,9 @@ def test_format_cover_image_url(self, mock_application): mock_url = "https://cdn.discordapp.com/app-assets/22222/wowowowowo.jpg?size=42" with mock.patch.object(urls, "generate_cdn_url", return_value=mock_url): url = applications.Application.format_cover_image_url(mock_application, format_="jpg", size=42) - urls.generate_cdn_url.assert_called_once_with("app-assets", "22222", "wowowowowo", format_="jpg", size=42) + urls.generate_cdn_url.assert_called_once_with( + "application-assets", "22222", "wowowowowo", format_="jpg", size=42 + ) assert url == mock_url def test_format_cover_image_url_returns_none(self, mock_application): diff --git a/tests/hikari/test_audit_logs.py b/tests/hikari/test_audit_logs.py index a6e502f496..510694e83b 100644 --- a/tests/hikari/test_audit_logs.py +++ b/tests/hikari/test_audit_logs.py @@ -22,17 +22,13 @@ import mock import pytest -from hikari import audit_logs -from hikari import channels -from hikari import guilds -from hikari import users -from hikari import webhooks -from hikari.clients import components +from hikari.models import audit_logs, guilds, users, webhooks, channels +from hikari.components import application @pytest.fixture() def mock_components(): - return mock.MagicMock(components.Components) + return mock.MagicMock(application.Application) class TestAuditLogChangeKey: @@ -478,7 +474,7 @@ def test_deserialize( assert audit_log_obj.entries == { 694026906592477214: audit_logs.AuditLogEntry.deserialize(test_audit_log_entry_payload) } - assert audit_log_obj.entries[694026906592477214]._components is mock_components + assert audit_log_obj.entries[694026906592477214]._app is mock_components assert audit_log_obj.webhooks == {424242: mock_webhook_obj} assert audit_log_obj.users == {92929292: mock_user_obj} assert audit_log_obj.integrations == {33590653072239123: mock_integration_obj} @@ -503,7 +499,7 @@ async def test__fill_when_entities_returned(self, mock_components): } ) audit_log_iterator = audit_logs.AuditLogIterator( - components=mock_components, request=mock_request, before="123", limit=None, + app=mock_components, request=mock_request, before="123", limit=None, ) stack = contextlib.ExitStack() stack.enter_context(mock.patch.object(users.User, "deserialize", return_value=mock_user_obj)) @@ -523,7 +519,7 @@ async def test__fill_when_entities_returned(self, mock_components): assert audit_log_iterator.users == {929292: mock_user_obj} assert audit_log_iterator.integrations == {123123123: mock_integration_obj} assert audit_log_iterator._buffer == [mock_audit_log_entry_payload] - assert audit_log_iterator._components is mock_components + assert audit_log_iterator._app is mock_components mock_request.assert_called_once_with( before="123", limit=100, ) @@ -534,7 +530,7 @@ async def test__fill_when_resource_exhausted(self, mock_components): return_value={"webhooks": [], "users": [], "audit_log_entries": [], "integrations": []} ) audit_log_iterator = audit_logs.AuditLogIterator( - components=mock_components, request=mock_request, before="222222222", limit=None, + app=mock_components, request=mock_request, before="222222222", limit=None, ) stack = contextlib.ExitStack() stack.enter_context(mock.patch.object(users.User, "deserialize", return_value=...)) @@ -565,7 +561,7 @@ async def test__fill_when_before_and_limit_not_set(self, mock_components): } ) audit_log_iterator = audit_logs.AuditLogIterator( - components=mock_components, request=mock_request, before="123", limit=None, + app=mock_components, request=mock_request, before="123", limit=None, ) stack = contextlib.ExitStack() stack.enter_context(mock.patch.object(users.User, "deserialize", return_value=...)) @@ -590,7 +586,7 @@ async def test__fill_when_before_and_limit_set(self, mock_components): } ) audit_log_iterator = audit_logs.AuditLogIterator( - components=mock_components, request=mock_request, before="222222222", limit=44, + app=mock_components, request=mock_request, before="222222222", limit=44, ) stack = contextlib.ExitStack() stack.enter_context(mock.patch.object(users.User, "deserialize", return_value=...)) @@ -609,9 +605,7 @@ async def test___anext___when_not_filled_and_resource_is_exhausted(self, mock_co mock_request = mock.AsyncMock( return_value={"webhooks": [], "users": [], "audit_log_entries": [], "integrations": []} ) - iterator = audit_logs.AuditLogIterator( - components=mock_components, request=mock_request, before="123", limit=None - ) + iterator = audit_logs.AuditLogIterator(app=mock_components, request=mock_request, before="123", limit=None) with mock.patch.object(audit_logs.AuditLogEntry, "deserialize", return_value=...): async for _ in iterator: assert False, "Iterator shouldn't have yielded anything." @@ -624,9 +618,7 @@ async def test___anext___when_not_filled(self, mock_components): side_effect=[{"webhooks": [], "users": [], "audit_log_entries": [{"id": "666666"}], "integrations": []}] ) mock_audit_log_entry = mock.MagicMock(audit_logs.AuditLogEntry, id=666666) - iterator = audit_logs.AuditLogIterator( - components=mock_components, request=mock_request, before="123", limit=None - ) + iterator = audit_logs.AuditLogIterator(app=mock_components, request=mock_request, before="123", limit=None) with mock.patch.object(audit_logs.AuditLogEntry, "deserialize", side_effect=[mock_audit_log_entry]): async for result in iterator: assert result is mock_audit_log_entry @@ -643,9 +635,7 @@ async def test___anext___when_not_filled_and_limit_exhausted(self, mock_componen side_effect=[{"webhooks": [], "users": [], "audit_log_entries": [], "integrations": []}] ) mock_audit_log_entry = mock.MagicMock(audit_logs.AuditLogEntry, id=666666) - iterator = audit_logs.AuditLogIterator( - components=mock_components, request=mock_request, before="123", limit=None - ) + iterator = audit_logs.AuditLogIterator(app=mock_components, request=mock_request, before="123", limit=None) with mock.patch.object(audit_logs.AuditLogEntry, "deserialize", side_effect=[mock_audit_log_entry]): async for _ in iterator: assert False, "Iterator shouldn't have yielded anything." @@ -659,7 +649,7 @@ async def test___anext___when_not_filled_and_limit_exhausted(self, mock_componen async def test___anext___when_filled(self, mock_components): mock_request = mock.AsyncMock(side_effect=[]) mock_audit_log_entry = mock.MagicMock(audit_logs.AuditLogEntry, id=4242) - iterator = audit_logs.AuditLogIterator(components=mock_components, request=mock_request, before="123",) + iterator = audit_logs.AuditLogIterator(app=mock_components, request=mock_request, before="123",) iterator._buffer = [{"id": "123123"}] with mock.patch.object(audit_logs.AuditLogEntry, "deserialize", side_effect=[mock_audit_log_entry]): async for result in iterator: diff --git a/tests/hikari/test_bases.py b/tests/hikari/test_bases.py index 339b2eeee9..98a89b997a 100644 --- a/tests/hikari/test_bases.py +++ b/tests/hikari/test_bases.py @@ -23,8 +23,8 @@ import mock import pytest -from hikari import bases -from hikari.clients import components +from hikari.models import bases +from hikari.components import application from hikari.internal import marshaller @@ -39,12 +39,12 @@ class StubEntity(bases.Entity, marshaller.Deserializable, marshaller.Serializabl return StubEntity def test_deserialize(self, stub_entity): - mock_components = mock.MagicMock(components.Components) + mock_components = mock.MagicMock(application.Application) entity = stub_entity.deserialize({}, components=mock_components) - assert entity._components is mock_components + assert entity._app is mock_components def test_serialize(self, stub_entity): - mock_components = mock.MagicMock(components.Components) + mock_components = mock.MagicMock(application.Application) assert stub_entity(components=mock_components).serialize() == {} diff --git a/tests/hikari/test_channels.py b/tests/hikari/test_channels.py index 6e8413eb65..07d5de5309 100644 --- a/tests/hikari/test_channels.py +++ b/tests/hikari/test_channels.py @@ -21,11 +21,8 @@ import mock import pytest -from hikari import bases -from hikari import channels -from hikari import permissions -from hikari import users -from hikari.clients import components +from hikari.models import permissions, users, channels, bases +from hikari.components import application from hikari.internal import urls @@ -142,7 +139,7 @@ def test_guild_voice_channel_payload(test_permission_overwrite_payload): @pytest.fixture() def mock_components(): - return mock.MagicMock(components.Components) + return mock.MagicMock(application.Application) class TestPartialChannel: @@ -274,7 +271,7 @@ def test_deserialize(self, test_guild_category_payload, test_permission_overwrit assert channel_obj.permission_overwrites == { 4242: channels.PermissionOverwrite.deserialize(test_permission_overwrite_payload) } - assert channel_obj.permission_overwrites[4242]._components is mock_components + assert channel_obj.permission_overwrites[4242]._app is mock_components assert channel_obj.guild_id == 9876 assert channel_obj.position == 3 assert channel_obj.name == "Test" @@ -291,7 +288,7 @@ def test_deserialize(self, test_guild_text_channel_payload, test_permission_over assert channel_obj.permission_overwrites == { 4242: channels.PermissionOverwrite.deserialize(test_permission_overwrite_payload) } - assert channel_obj.permission_overwrites[4242]._components is mock_components + assert channel_obj.permission_overwrites[4242]._app is mock_components assert channel_obj.guild_id == 567 assert channel_obj.position == 6 assert channel_obj.name == "general" @@ -311,7 +308,7 @@ def test_deserialize(self, test_guild_news_channel_payload, test_permission_over assert channel_obj.permission_overwrites == { 4242: channels.PermissionOverwrite.deserialize(test_permission_overwrite_payload) } - assert channel_obj.permission_overwrites[4242]._components is mock_components + assert channel_obj.permission_overwrites[4242]._app is mock_components assert channel_obj.guild_id == 123 assert channel_obj.position == 0 assert channel_obj.name == "Important Announcements" @@ -332,7 +329,7 @@ def test_deserialize(self, test_guild_store_channel_payload, test_permission_ove assert channel_obj.permission_overwrites == { 4242: channels.PermissionOverwrite.deserialize(test_permission_overwrite_payload) } - assert channel_obj.permission_overwrites[4242]._components is mock_components + assert channel_obj.permission_overwrites[4242]._app is mock_components assert channel_obj.guild_id == 1234 assert channel_obj.position == 2 assert channel_obj.name == "Half Life 3" @@ -351,7 +348,7 @@ def test_deserialize(self, test_guild_voice_channel_payload, test_permission_ove assert channel_obj.permission_overwrites == { 4242: channels.PermissionOverwrite.deserialize(test_permission_overwrite_payload) } - assert channel_obj.permission_overwrites[4242]._components is mock_components + assert channel_obj.permission_overwrites[4242]._app is mock_components assert channel_obj.guild_id == 789 assert channel_obj.position == 4 assert channel_obj.name == "Secret Developer Discussions" diff --git a/tests/hikari/test_colors.py b/tests/hikari/test_colors.py index 91699254f6..591c1c9666 100644 --- a/tests/hikari/test_colors.py +++ b/tests/hikari/test_colors.py @@ -20,7 +20,7 @@ import pytest -from hikari import colors +from hikari.models import colors from tests.hikari import _helpers diff --git a/tests/hikari/test_colours.py b/tests/hikari/test_colours.py index 0fe10e273a..1d888242b8 100644 --- a/tests/hikari/test_colours.py +++ b/tests/hikari/test_colours.py @@ -18,8 +18,7 @@ # along with Hikari. If not, see . import pytest -from hikari import colors -from hikari import colours +from hikari.models import colors, colours @pytest.mark.model diff --git a/tests/hikari/test_embeds.py b/tests/hikari/test_embeds.py index 7ef6691674..a408852aa0 100644 --- a/tests/hikari/test_embeds.py +++ b/tests/hikari/test_embeds.py @@ -21,17 +21,15 @@ import mock import pytest -from hikari import colors -from hikari import embeds -from hikari import files -from hikari.clients import components +from hikari.models import embeds, colors, files +from hikari.components import application from hikari.internal import conversions from tests.hikari import _helpers @pytest.fixture() def mock_components(): - return mock.MagicMock(components.Components) + return mock.MagicMock(application.Application) @pytest.fixture @@ -264,19 +262,19 @@ def test_deserialize( assert embed_obj.timestamp == mock_datetime assert embed_obj.color == colors.Color(14014915) assert embed_obj.footer == embeds.EmbedFooter.deserialize(test_footer_payload) - assert embed_obj.footer._components is mock_components + assert embed_obj.footer._app is mock_components assert embed_obj.image == embeds.EmbedImage.deserialize(test_image_payload) - assert embed_obj.image._components is mock_components + assert embed_obj.image._app is mock_components assert embed_obj.thumbnail == embeds.EmbedThumbnail.deserialize(test_thumbnail_payload) - assert embed_obj.thumbnail._components is mock_components + assert embed_obj.thumbnail._app is mock_components assert embed_obj.video == embeds.EmbedVideo.deserialize(test_video_payload) - assert embed_obj.video._components is mock_components + assert embed_obj.video._app is mock_components assert embed_obj.provider == embeds.EmbedProvider.deserialize(test_provider_payload) - assert embed_obj.provider._components is mock_components + assert embed_obj.provider._app is mock_components assert embed_obj.author == embeds.EmbedAuthor.deserialize(test_author_payload) - assert embed_obj.author._components is mock_components + assert embed_obj.author._app is mock_components assert embed_obj.fields == [embeds.EmbedField.deserialize(test_field_payload)] - assert embed_obj.fields[0]._components is mock_components + assert embed_obj.fields[0]._app is mock_components def test_serialize_full_embed(self): embed_obj = embeds.Embed( diff --git a/tests/hikari/test_emojis.py b/tests/hikari/test_emojis.py index c63b1ea7f7..c28d6cd02d 100644 --- a/tests/hikari/test_emojis.py +++ b/tests/hikari/test_emojis.py @@ -19,18 +19,15 @@ import mock import pytest -from hikari import bases -from hikari import emojis -from hikari import files -from hikari import users -from hikari.clients import components +from hikari.models import users, emojis, bases, files +from hikari.components import application from hikari.internal import urls from tests.hikari import _helpers @pytest.fixture() def mock_components(): - return mock.MagicMock(components.Components) + return mock.MagicMock(application.Application) class TestEmoji: diff --git a/tests/hikari/test_errors.py b/tests/hikari/test_errors.py index f2659e0bb5..156905a935 100644 --- a/tests/hikari/test_errors.py +++ b/tests/hikari/test_errors.py @@ -21,7 +21,7 @@ import pytest from hikari import errors -from hikari.net import codes +from hikari.internal import codes class TestHikariError: diff --git a/tests/hikari/test_files.py b/tests/hikari/test_files.py index b7943d8a22..148c3dfcd8 100644 --- a/tests/hikari/test_files.py +++ b/tests/hikari/test_files.py @@ -27,8 +27,8 @@ import mock import pytest +from hikari.models import files from hikari import errors -from hikari import files from tests.hikari import _helpers diff --git a/tests/hikari/test_gateway_entities.py b/tests/hikari/test_gateway_entities.py index 90329bf5c0..afeab904a6 100644 --- a/tests/hikari/test_gateway_entities.py +++ b/tests/hikari/test_gateway_entities.py @@ -21,15 +21,14 @@ import mock import pytest -from hikari import gateway_entities -from hikari import guilds -from hikari.clients import components +from hikari.models import guilds, gateway +from hikari.components import application from tests.hikari import _helpers @pytest.fixture() def mock_components(): - return mock.MagicMock(components.Components) + return mock.MagicMock(application.Application) @pytest.fixture() @@ -39,7 +38,7 @@ def test_session_start_limit_payload(): class TestSessionStartLimit: def test_deserialize(self, test_session_start_limit_payload, mock_components): - session_start_limit_obj = gateway_entities.SessionStartLimit.deserialize( + session_start_limit_obj = gateway.SessionStartLimit.deserialize( test_session_start_limit_payload, components=mock_components ) assert session_start_limit_obj.total == 1000 @@ -53,16 +52,14 @@ def test_gateway_bot_payload(self, test_session_start_limit_payload): return {"url": "wss://gateway.discord.gg", "shards": 1, "session_start_limit": test_session_start_limit_payload} def test_deserialize(self, test_gateway_bot_payload, test_session_start_limit_payload, mock_components): - mock_session_start_limit = mock.MagicMock(gateway_entities.SessionStartLimit) + mock_session_start_limit = mock.MagicMock(gateway.SessionStartLimit) with _helpers.patch_marshal_attr( - gateway_entities.GatewayBot, + gateway.GatewayBot, "session_start_limit", - deserializer=gateway_entities.SessionStartLimit.deserialize, + deserializer=gateway.SessionStartLimit.deserialize, return_value=mock_session_start_limit, ) as patched_start_limit_deserializer: - gateway_bot_obj = gateway_entities.GatewayBot.deserialize( - test_gateway_bot_payload, components=mock_components - ) + gateway_bot_obj = gateway.GatewayBot.deserialize(test_gateway_bot_payload, components=mock_components) patched_start_limit_deserializer.assert_called_once_with( test_session_start_limit_payload, components=mock_components ) @@ -77,19 +74,19 @@ def test_gateway_activity_config(self): return {"name": "Presence me baby", "url": "http://a-url-name", "type": 1} def test_deserialize_full_config(self, test_gateway_activity_config): - gateway_activity_obj = gateway_entities.Activity.deserialize(test_gateway_activity_config) + gateway_activity_obj = gateway.Activity.deserialize(test_gateway_activity_config) assert gateway_activity_obj.name == "Presence me baby" assert gateway_activity_obj.url == "http://a-url-name" assert gateway_activity_obj.type is guilds.ActivityType.STREAMING def test_deserialize_partial_config(self): - gateway_activity_obj = gateway_entities.Activity.deserialize({"name": "Presence me baby"}) + gateway_activity_obj = gateway.Activity.deserialize({"name": "Presence me baby"}) assert gateway_activity_obj.name == "Presence me baby" assert gateway_activity_obj.url == None assert gateway_activity_obj.type is guilds.ActivityType.PLAYING def test_serialize_full_activity(self): - gateway_activity_obj = gateway_entities.Activity( + gateway_activity_obj = gateway.Activity( name="Presence me baby", url="http://a-url-name", type=guilds.ActivityType.STREAMING ) assert gateway_activity_obj.serialize() == { @@ -99,7 +96,7 @@ def test_serialize_full_activity(self): } def test_serialize_partial_activity(self): - gateway_activity_obj = gateway_entities.Activity(name="Presence me baby",) + gateway_activity_obj = gateway.Activity(name="Presence me baby",) assert gateway_activity_obj.serialize() == { "name": "Presence me baby", "type": 0, diff --git a/tests/hikari/test_guilds.py b/tests/hikari/test_guilds.py index bf9e60bee7..77b6b6c51f 100644 --- a/tests/hikari/test_guilds.py +++ b/tests/hikari/test_guilds.py @@ -22,14 +22,8 @@ import mock import pytest -from hikari import channels -from hikari import colors -from hikari import emojis -from hikari import guilds -from hikari import permissions -from hikari import unset -from hikari import users -from hikari.clients import components +from hikari.models import unset, permissions, guilds, users, colors, channels, emojis +from hikari.components import application from hikari.internal import conversions from hikari.internal import urls from tests.hikari import _helpers @@ -270,7 +264,7 @@ def test_guild_payload( @pytest.fixture() def mock_components(): - return mock.MagicMock(components.Components) + return mock.MagicMock(application.Application) class TestGuildEmbed: @@ -514,17 +508,17 @@ def test_deserialize( assert presence_activity_obj.timestamps == guilds.ActivityTimestamps.deserialize( test_activity_timestamps_payload ) - assert presence_activity_obj.timestamps._components is mock_components + assert presence_activity_obj.timestamps._app is mock_components assert presence_activity_obj.application_id == 40404040404040 assert presence_activity_obj.details == "They are doing stuff" assert presence_activity_obj.state == "STATED" assert presence_activity_obj.emoji is mock_emoji assert presence_activity_obj.party == guilds.ActivityParty.deserialize(test_activity_party_payload) - assert presence_activity_obj.party._components is mock_components + assert presence_activity_obj.party._app is mock_components assert presence_activity_obj.assets == guilds.ActivityAssets.deserialize(test_activity_assets_payload) - assert presence_activity_obj.assets._components is mock_components + assert presence_activity_obj.assets._app is mock_components assert presence_activity_obj.secrets == guilds.ActivitySecret.deserialize(test_activity_secrets_payload) - assert presence_activity_obj.secrets._components is mock_components + assert presence_activity_obj.secrets._app is mock_components assert presence_activity_obj.is_instance is True assert presence_activity_obj.flags == guilds.ActivityFlag.INSTANCE | guilds.ActivityFlag.JOIN @@ -556,7 +550,7 @@ def test_deserialize_partial_presence_user(self, mock_components): presence_user_obj = guilds.PresenceUser.deserialize({"id": "115590097100865541"}, components=mock_components) assert presence_user_obj.id == 115590097100865541 for attr in presence_user_obj.__slots__: - if attr not in ("id", "_components"): + if attr not in ("id", "_app"): assert getattr(presence_user_obj, attr) is unset.UNSET @pytest.fixture() @@ -666,16 +660,16 @@ def test_deserialize( ) patched_since_deserializer.assert_called_once_with("2015-04-26T06:26:56.936000+00:00") assert guild_member_presence_obj.user == guilds.PresenceUser.deserialize(test_user_payload) - assert guild_member_presence_obj.user._components is mock_components + assert guild_member_presence_obj.user._app is mock_components assert guild_member_presence_obj.role_ids == [49494949] assert guild_member_presence_obj.guild_id == 44004040 assert guild_member_presence_obj.visible_status is guilds.PresenceStatus.DND assert guild_member_presence_obj.activities == [ guilds.PresenceActivity.deserialize(test_presence_activity_payload) ] - assert guild_member_presence_obj.activities[0]._components is mock_components + assert guild_member_presence_obj.activities[0]._app is mock_components assert guild_member_presence_obj.client_status == guilds.ClientStatus.deserialize(test_client_status_payload) - assert guild_member_presence_obj.client_status._components is mock_components + assert guild_member_presence_obj.client_status._app is mock_components assert guild_member_presence_obj.premium_since is mock_since assert guild_member_presence_obj.nick == "Nick" @@ -734,7 +728,7 @@ def test_deserialise( assert partial_guild_integration_obj.account == guilds.IntegrationAccount.deserialize( test_integration_account_payload ) - assert partial_guild_integration_obj.account._components is mock_components + assert partial_guild_integration_obj.account._app is mock_components class TestGuildIntegration: @@ -985,9 +979,9 @@ def test_deserialize( channels.deserialize_channel.assert_called_once_with(test_channel_payload, components=mock_components) emojis.KnownCustomEmoji.deserialize.assert_called_once_with(test_emoji_payload, components=mock_components) assert guild_obj.members == {123456: guilds.GuildMember.deserialize(test_member_payload)} - assert guild_obj.members[123456]._components is mock_components + assert guild_obj.members[123456]._app is mock_components assert guild_obj.presences == {123456: guilds.GuildMemberPresence.deserialize(test_guild_member_presence)} - assert guild_obj.presences[123456]._components is mock_components + assert guild_obj.presences[123456]._app is mock_components assert guild_obj.splash_hash == "0ff0ff0ff" assert guild_obj.discovery_splash_hash == "famfamFAMFAMfam" assert guild_obj.owner_id == 6969696 @@ -1003,7 +997,7 @@ def test_deserialize( assert guild_obj.default_message_notifications is guilds.GuildMessageNotificationsLevel.ONLY_MENTIONS assert guild_obj.explicit_content_filter is guilds.GuildExplicitContentFilterLevel.ALL_MEMBERS assert guild_obj.roles == {41771983423143936: guilds.GuildRole.deserialize(test_roles_payload)} - assert guild_obj.roles[41771983423143936]._components is mock_components + assert guild_obj.roles[41771983423143936]._app is mock_components assert guild_obj.emojis == {41771983429993937: mock_emoji} assert guild_obj.mfa_level is guilds.GuildMFALevel.ELEVATED assert guild_obj.application_id == 39494949 diff --git a/tests/hikari/test_intents.py b/tests/hikari/test_intents.py index a102e2eb81..a47141aaf0 100644 --- a/tests/hikari/test_intents.py +++ b/tests/hikari/test_intents.py @@ -16,7 +16,7 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . -from hikari import intents +from hikari.models import intents class TestIntent: diff --git a/tests/hikari/test_invites.py b/tests/hikari/test_invites.py index 05bffcd9c3..4938aa3d67 100644 --- a/tests/hikari/test_invites.py +++ b/tests/hikari/test_invites.py @@ -22,11 +22,8 @@ import mock import pytest -from hikari import channels -from hikari import guilds -from hikari import invites -from hikari import users -from hikari.clients import components +from hikari.models import guilds, users, channels, invites +from hikari.components import application from hikari.internal import conversions from hikari.internal import urls from tests.hikari import _helpers @@ -90,7 +87,7 @@ def test_invite_with_metadata_payload(test_invite_payload): @pytest.fixture() def mock_components(): - return mock.MagicMock(components.Components) + return mock.MagicMock(application.Application) class TestInviteGuild: diff --git a/tests/hikari/test_messages.py b/tests/hikari/test_messages.py index b4d7cbfb84..203e41722e 100644 --- a/tests/hikari/test_messages.py +++ b/tests/hikari/test_messages.py @@ -22,16 +22,8 @@ import mock import pytest -from hikari import applications -from hikari import bases -from hikari import channels -from hikari import embeds -from hikari import emojis -from hikari import files -from hikari import guilds -from hikari import messages -from hikari import users -from hikari.clients import components +from hikari.models import embeds, messages, guilds, applications, users, channels, emojis, bases, files +from hikari.components import application from hikari.internal import conversions from tests.hikari import _helpers @@ -74,7 +66,7 @@ def test_application_payload(): return { "id": "456", "name": "hikari", - "description": "The best app", + "description": "The best application", "icon": "2658b3029e775a931ffb49380073fa63", "cover_image": "58982a23790c4f22787b05d3be38a026", } @@ -138,7 +130,7 @@ def test_message_payload( @pytest.fixture() def mock_components(): - return mock.MagicMock(components.Components) + return mock.MagicMock(application.Application) class TestAttachment: @@ -280,7 +272,7 @@ def test_deserialize( test_reaction_payload["emoji"], components=mock_components ) assert message_obj.reactions == [messages.Reaction.deserialize(test_reaction_payload)] - assert message_obj.reactions[0]._components is mock_components + assert message_obj.reactions[0]._app is mock_components patched_application_deserializer.assert_called_once_with( test_application_payload, components=mock_components ) @@ -303,23 +295,23 @@ def test_deserialize( assert message_obj.role_mentions == {987} assert message_obj.channel_mentions == {456} assert message_obj.attachments == [messages.Attachment.deserialize(test_attachment_payload)] - assert message_obj.attachments[0]._components is mock_components + assert message_obj.attachments[0]._app is mock_components assert message_obj.embeds == [embeds.Embed.deserialize({})] - assert message_obj.embeds[0]._components is mock_components + assert message_obj.embeds[0]._app is mock_components assert message_obj.is_pinned is True assert message_obj.webhook_id == 1234 assert message_obj.type == messages.MessageType.DEFAULT assert message_obj.activity == messages.MessageActivity.deserialize(test_message_activity_payload) - assert message_obj.activity._components is mock_components + assert message_obj.activity._app is mock_components assert message_obj.application == mock_app assert message_obj.message_reference == messages.MessageCrosspost.deserialize(test_message_crosspost_payload) - assert message_obj.message_reference._components is mock_components + assert message_obj.message_reference._app is mock_components assert message_obj.flags == messages.MessageFlag.IS_CROSSPOST assert message_obj.nonce == "171000788183678976" @pytest.fixture() - def components_impl(self) -> components.Components: - return mock.MagicMock(components.Components, rest=mock.AsyncMock()) + def components_impl(self) -> application.Application: + return mock.MagicMock(application.Application, rest=mock.AsyncMock()) @pytest.fixture() def message_obj(self, components_impl): diff --git a/tests/hikari/test_unset.py b/tests/hikari/test_unset.py index 4f4500baf7..8dc6dcb135 100644 --- a/tests/hikari/test_unset.py +++ b/tests/hikari/test_unset.py @@ -18,7 +18,7 @@ # along with Hikari. If not, see . import pytest -from hikari import unset +from hikari.models import unset from tests.hikari import _helpers diff --git a/tests/hikari/test_users.py b/tests/hikari/test_users.py index 3602c001a7..02c903e972 100644 --- a/tests/hikari/test_users.py +++ b/tests/hikari/test_users.py @@ -19,10 +19,9 @@ import mock import pytest -from hikari import bases -from hikari import users -from hikari.clients import components -from hikari.clients import rest +from hikari import rest +from hikari.models import users, bases +from hikari.components import application from hikari.internal import urls @@ -57,8 +56,8 @@ def test_oauth_user_payload(): @pytest.fixture() -def mock_components() -> components.Components: - return mock.MagicMock(components.Components, rest=mock.AsyncMock(rest.RESTClient)) +def mock_components() -> application.Application: + return mock.MagicMock(application.Application, rest=mock.AsyncMock(rest.RESTClient)) class TestUser: diff --git a/tests/hikari/test_voices.py b/tests/hikari/test_voices.py index 156c5dbbdc..753003b93b 100644 --- a/tests/hikari/test_voices.py +++ b/tests/hikari/test_voices.py @@ -19,8 +19,8 @@ import mock import pytest -from hikari import voices -from hikari.clients import components +from hikari.models import voices +from hikari.components import application @pytest.fixture() @@ -45,7 +45,7 @@ def voice_region_payload(): @pytest.fixture() def mock_components(): - return mock.MagicMock(components.Components) + return mock.MagicMock(application.Application) class TestVoiceState: diff --git a/tests/hikari/test_webhook.py b/tests/hikari/test_webhook.py index e07064bf68..0735651039 100644 --- a/tests/hikari/test_webhook.py +++ b/tests/hikari/test_webhook.py @@ -19,22 +19,16 @@ import mock import pytest -from hikari import bases -from hikari import channels -from hikari import embeds -from hikari import files -from hikari import messages -from hikari import users -from hikari import webhooks -from hikari.clients import components -from hikari.clients import rest +from hikari import rest +from hikari.models import embeds, messages, users, webhooks, channels, bases, files +from hikari.components import application from hikari.internal import urls from tests.hikari import _helpers @pytest.fixture() -def mock_components() -> components.Components: - return mock.MagicMock(components.Components, rest=mock.AsyncMock(rest.RESTClient)) +def mock_components() -> application.Application: + return mock.MagicMock(application.Application, rest=mock.AsyncMock(rest.RESTClient)) class TestWebhook: From ab5eeff878a64f36981eda6d6051c313fc03cb89 Mon Sep 17 00:00:00 2001 From: Faster Speeding Date: Sun, 17 May 2020 13:02:48 +0100 Subject: [PATCH 353/922] Update the test layout * rename places where the application object was being referred to as component * Bug fixes --- hikari/components/bot_base.py | 4 +- hikari/components/event_managers.py | 6 +- hikari/gateway/__init__.py | 1 + hikari/gateway/client.py | 4 +- hikari/gateway/connection.py | 2 +- hikari/internal/helpers.py | 3 +- hikari/models/__init__.py | 1 + hikari/models/audit_logs.py | 10 +- hikari/models/bases.py | 2 +- hikari/models/messages.py | 18 +- hikari/models/users.py | 4 +- hikari/models/webhooks.py | 16 +- hikari/rest/__init__.py | 1 + hikari/rest/base.py | 4 +- hikari/rest/channel.py | 65 ++-- hikari/rest/gateway.py | 2 +- hikari/rest/guild.py | 67 ++-- hikari/rest/invite.py | 2 +- hikari/rest/me.py | 22 +- hikari/rest/oauth2.py | 2 +- hikari/rest/react.py | 15 +- hikari/rest/user.py | 2 +- hikari/rest/voice.py | 2 +- hikari/rest/webhook.py | 6 +- hikari/stateless/manager.py | 140 ++++---- tests/hikari/_helpers.py | 2 +- .../{clients => components}/__init__.py | 0 .../test_application.py} | 4 +- .../test_dispatchers.py | 0 .../test_intent_aware_dispatchers.py | 0 tests/hikari/events/test_base.py | 5 +- tests/hikari/events/test_channel.py | 7 +- tests/hikari/events/test_guild.py | 7 +- tests/hikari/events/test_message.py | 10 +- tests/hikari/events/test_other.py | 5 +- .../test_rest => gateway}/__init__.py | 0 .../test_shards.py => gateway/test_client.py} | 30 +- .../test_connection.py} | 4 +- tests/hikari/{net => internal}/test_codes.py | 0 .../{net => internal}/test_http_client.py | 0 tests/hikari/internal/test_marshaller.py | 2 +- .../{net => internal}/test_ratelimits.py | 282 ---------------- tests/hikari/internal/test_urls.py | 2 +- .../{net => internal}/test_user_agents.py | 0 tests/hikari/{net => models}/__init__.py | 0 .../hikari/{ => models}/test_applications.py | 48 ++- tests/hikari/{ => models}/test_audit_logs.py | 222 +++++-------- tests/hikari/{ => models}/test_bases.py | 14 +- tests/hikari/{ => models}/test_channels.py | 71 ++-- tests/hikari/{ => models}/test_colors.py | 0 tests/hikari/{ => models}/test_colours.py | 3 +- tests/hikari/{ => models}/test_embeds.py | 58 ++-- tests/hikari/{ => models}/test_emojis.py | 31 +- tests/hikari/{ => models}/test_files.py | 4 +- .../test_gateway.py} | 19 +- tests/hikari/{ => models}/test_guilds.py | 150 ++++----- tests/hikari/{ => models}/test_intents.py | 0 tests/hikari/{ => models}/test_invites.py | 33 +- tests/hikari/{ => models}/test_messages.py | 66 ++-- tests/hikari/{ => models}/test_unset.py | 0 tests/hikari/{ => models}/test_users.py | 35 +- tests/hikari/{ => models}/test_voices.py | 14 +- tests/hikari/{ => models}/test_webhook.py | 140 ++++---- tests/hikari/{state => rest}/__init__.py | 0 .../{clients/test_rest => rest}/test_base.py | 13 +- tests/hikari/rest/test_buckets.py | 309 ++++++++++++++++++ .../test_rest => rest}/test_channel.py | 98 ++---- .../test___init__.py => rest/test_client.py} | 38 ++- .../test_rest => rest}/test_gateway.py | 14 +- .../{clients/test_rest => rest}/test_guild.py | 165 ++++------ .../test_rest => rest}/test_invite.py | 14 +- .../{clients/test_rest => rest}/test_me.py | 65 ++-- .../test_rest => rest}/test_oauth2.py | 8 +- .../{clients/test_rest => rest}/test_react.py | 42 ++- tests/hikari/{net => rest}/test_routes.py | 0 .../test_rest.py => rest/test_session.py} | 13 +- .../{clients/test_rest => rest}/test_user.py | 12 +- .../{clients/test_rest => rest}/test_voice.py | 10 +- .../test_rest => rest}/test_webhook.py | 30 +- tests/hikari/stateless/__init__.py | 18 + .../test_manager.py} | 154 ++++----- tests/hikari/{clients => }/test_configs.py | 6 +- 82 files changed, 1284 insertions(+), 1394 deletions(-) rename tests/hikari/{clients => components}/__init__.py (100%) rename tests/hikari/{clients/test_components.py => components/test_application.py} (96%) rename tests/hikari/{events => components}/test_dispatchers.py (100%) rename tests/hikari/{events => components}/test_intent_aware_dispatchers.py (100%) rename tests/hikari/{clients/test_rest => gateway}/__init__.py (100%) rename tests/hikari/{clients/test_shards.py => gateway/test_client.py} (97%) rename tests/hikari/{net/test_shards.py => gateway/test_connection.py} (100%) rename tests/hikari/{net => internal}/test_codes.py (100%) rename tests/hikari/{net => internal}/test_http_client.py (100%) rename tests/hikari/{net => internal}/test_ratelimits.py (57%) rename tests/hikari/{net => internal}/test_user_agents.py (100%) rename tests/hikari/{net => models}/__init__.py (100%) rename tests/hikari/{ => models}/test_applications.py (91%) rename tests/hikari/{ => models}/test_audit_logs.py (80%) rename tests/hikari/{ => models}/test_bases.py (91%) rename tests/hikari/{ => models}/test_channels.py (91%) rename tests/hikari/{ => models}/test_colors.py (100%) rename tests/hikari/{ => models}/test_colours.py (92%) rename tests/hikari/{ => models}/test_embeds.py (92%) rename tests/hikari/{ => models}/test_emojis.py (92%) rename tests/hikari/{ => models}/test_files.py (99%) rename tests/hikari/{test_gateway_entities.py => models/test_gateway.py} (91%) rename tests/hikari/{ => models}/test_guilds.py (93%) rename tests/hikari/{ => models}/test_intents.py (100%) rename tests/hikari/{ => models}/test_invites.py (93%) rename tests/hikari/{ => models}/test_messages.py (92%) rename tests/hikari/{ => models}/test_unset.py (100%) rename tests/hikari/{ => models}/test_users.py (85%) rename tests/hikari/{ => models}/test_voices.py (88%) rename tests/hikari/{ => models}/test_webhook.py (75%) rename tests/hikari/{state => rest}/__init__.py (100%) rename tests/hikari/{clients/test_rest => rest}/test_base.py (85%) create mode 100644 tests/hikari/rest/test_buckets.py rename tests/hikari/{clients/test_rest => rest}/test_channel.py (93%) rename tests/hikari/{clients/test_rest/test___init__.py => rest/test_client.py} (77%) rename tests/hikari/{clients/test_rest => rest}/test_gateway.py (80%) rename tests/hikari/{clients/test_rest => rest}/test_guild.py (93%) rename tests/hikari/{clients/test_rest => rest}/test_invite.py (87%) rename tests/hikari/{clients/test_rest => rest}/test_me.py (83%) rename tests/hikari/{clients/test_rest => rest}/test_oauth2.py (89%) rename tests/hikari/{clients/test_rest => rest}/test_react.py (88%) rename tests/hikari/{net => rest}/test_routes.py (100%) rename tests/hikari/{net/test_rest.py => rest/test_session.py} (99%) rename tests/hikari/{clients/test_rest => rest}/test_user.py (86%) rename tests/hikari/{clients/test_rest => rest}/test_voice.py (88%) rename tests/hikari/{clients/test_rest => rest}/test_webhook.py (95%) create mode 100644 tests/hikari/stateless/__init__.py rename tests/hikari/{state/test_stateless.py => stateless/test_manager.py} (72%) rename tests/hikari/{clients => }/test_configs.py (98%) diff --git a/hikari/components/bot_base.py b/hikari/components/bot_base.py index 45653f8127..bb76cbd0d2 100644 --- a/hikari/components/bot_base.py +++ b/hikari/components/bot_base.py @@ -50,7 +50,7 @@ from hikari.internal import more_typing from hikari.models import gateway as gateway_models from hikari.models import guilds - from hikari.models import intents + from hikari.models import intents as intents_ from hikari.rest import client as rest_client @@ -135,7 +135,7 @@ def total_reconnect_count(self) -> int: return sum(s.reconnect_count for s in self.shards.values()) @property - def intents(self) -> typing.Optional[intents.Intent]: # noqa: D401 + def intents(self) -> typing.Optional[intents_.Intent]: # noqa: D401 """Intents that are in use for the connection. If intents are not being used at all, then this will be `None` instead. diff --git a/hikari/components/event_managers.py b/hikari/components/event_managers.py index aa4d4ee36e..665d805f65 100644 --- a/hikari/components/event_managers.py +++ b/hikari/components/event_managers.py @@ -137,15 +137,15 @@ def _process_message_create(self, shard, payload) -> MessageCreateEvent: Parameters ---------- - components: hikari.clients.application.Application + app: hikari.clients.application.Application The client application that this event manager should be bound to. Includes the event dispatcher that will store individual events and manage dispatching them after this object creates them. """ - def __init__(self, components: application.Application) -> None: + def __init__(self, app: application.Application) -> None: + self._app = app self.logger = logging.getLogger(type(self).__qualname__) - self._components = components self.raw_event_mappers = {} # Look for events and register them. diff --git a/hikari/gateway/__init__.py b/hikari/gateway/__init__.py index 60449679d9..a3c200478e 100644 --- a/hikari/gateway/__init__.py +++ b/hikari/gateway/__init__.py @@ -16,6 +16,7 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . +"""Components used for interacting with discord's gateway API.""" from __future__ import annotations diff --git a/hikari/gateway/client.py b/hikari/gateway/client.py index d350467ce2..812da3cf82 100644 --- a/hikari/gateway/client.py +++ b/hikari/gateway/client.py @@ -84,7 +84,7 @@ class GatewayClient(runnable.RunnableClient): __slots__ = ( "_activity", - "_components", + "_app", "_connection", "_idle_since", "_is_afk", @@ -97,7 +97,7 @@ class GatewayClient(runnable.RunnableClient): def __init__(self, shard_id: int, shard_count: int, app: application.Application, url: str) -> None: super().__init__(helpers.get_logger(self, str(shard_id))) - self._components = app + self._app = app self._raw_event_consumer = app.event_manager self._activity = app.config.initial_activity self._idle_since = app.config.initial_idle_since diff --git a/hikari/gateway/connection.py b/hikari/gateway/connection.py index 2bd9318e06..62bfdc71be 100644 --- a/hikari/gateway/connection.py +++ b/hikari/gateway/connection.py @@ -535,7 +535,7 @@ async def update_voice_state( """ payload = { "op": codes.GatewayOpcode.VOICE_STATE_UPDATE, - "d": {"guild_id": guild_id, "channel_id": channel_id, "self_mute": self_mute, "self_deaf": self_deaf,}, + "d": {"guild_id": guild_id, "channel_id": channel_id, "self_mute": self_mute, "self_deaf": self_deaf}, } await self._send(payload) diff --git a/hikari/internal/helpers.py b/hikari/internal/helpers.py index 5e368a30dc..7cb6a6d25d 100644 --- a/hikari/internal/helpers.py +++ b/hikari/internal/helpers.py @@ -25,10 +25,11 @@ import logging import typing +from hikari.models import bases + from . import more_collections if typing.TYPE_CHECKING: - from hikari.models import bases from hikari.models import guilds from hikari.models import users diff --git a/hikari/models/__init__.py b/hikari/models/__init__.py index ff23df643d..60fb41c6f7 100644 --- a/hikari/models/__init__.py +++ b/hikari/models/__init__.py @@ -16,6 +16,7 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . +"""Marshall classes used for describing discord entities.""" from __future__ import annotations diff --git a/hikari/models/audit_logs.py b/hikari/models/audit_logs.py index 28db51c800..5561baf970 100644 --- a/hikari/models/audit_logs.py +++ b/hikari/models/audit_logs.py @@ -610,7 +610,7 @@ async def __anext__(self) -> AuditLogEntry: if not self._buffer and self._limit != 0: await self._fill() try: - entry = AuditLogEntry.deserialize(self._buffer.pop(), components=self._app) + entry = AuditLogEntry.deserialize(self._buffer.pop(), app=self._app) self._front = str(entry.id) return entry except IndexError: @@ -629,19 +629,17 @@ async def _fill(self) -> None: self._buffer.extend(payload["audit_log_entries"]) if users := payload.get("users"): self.users = copy.copy(self.users) - self.users.update( - {bases.Snowflake(u["id"]): users_.User.deserialize(u, components=self._app) for u in users} - ) + self.users.update({bases.Snowflake(u["id"]): users_.User.deserialize(u, app=self._app) for u in users}) if webhooks := payload.get("webhooks"): self.webhooks = copy.copy(self.webhooks) self.webhooks.update( - {bases.Snowflake(w["id"]): webhooks_.Webhook.deserialize(w, components=self._app) for w in webhooks} + {bases.Snowflake(w["id"]): webhooks_.Webhook.deserialize(w, app=self._app) for w in webhooks} ) if integrations := payload.get("integrations"): self.integrations = copy.copy(self.integrations) self.integrations.update( { - bases.Snowflake(i["id"]): guilds.PartialGuildIntegration.deserialize(i, components=self._app) + bases.Snowflake(i["id"]): guilds.PartialGuildIntegration.deserialize(i, app=self._app) for i in integrations } ) diff --git a/hikari/models/bases.py b/hikari/models/bases.py index edd005366b..3c60088f5f 100644 --- a/hikari/models/bases.py +++ b/hikari/models/bases.py @@ -41,7 +41,7 @@ class Entity(abc.ABC): """The base for any entity used in this API.""" - _components: typing.Optional[application.Application] = attr.attrib(default=None, repr=False, eq=False, hash=False) + _app: typing.Optional[application.Application] = attr.attrib(default=None, repr=False, eq=False, hash=False) """The client application that models may use for procedures.""" diff --git a/hikari/models/messages.py b/hikari/models/messages.py index 684f4e4997..36dd741b59 100644 --- a/hikari/models/messages.py +++ b/hikari/models/messages.py @@ -409,7 +409,7 @@ async def fetch_channel(self) -> channels.PartialChannel: hikari.errors.NotFound If the channel this message was created in does not exist. """ - return await self._components.rest.fetch_channel(channel=self.channel_id) + return await self._app.rest.fetch_channel(channel=self.channel_id) async def edit( # pylint:disable=line-too-long self, @@ -472,7 +472,7 @@ async def edit( # pylint:disable=line-too-long If more than 100 unique objects/entities are passed for `role_mentions` or `user_mentions`. """ - return await self._components.rest.update_message( + return await self._app.rest.update_message( message=self.id, channel=self.channel_id, content=content, @@ -500,7 +500,7 @@ async def safe_edit( This is the same as `edit`, but with all defaults set to prevent any mentions from working by default. """ - return await self._components.rest.safe_update_message( + return await self._app.rest.safe_update_message( message=self.id, channel=self.channel_id, content=content, @@ -581,7 +581,7 @@ async def reply( # pylint:disable=line-too-long If more than 100 unique objects/entities are passed for `role_mentions` or `user_mentions`. """ - return await self._components.rest.create_message( + return await self._app.rest.create_message( channel=self.channel_id, content=content, nonce=nonce, @@ -614,7 +614,7 @@ async def safe_reply( This is the same as `reply`, but with all defaults set to prevent any mentions from working by default. """ - return await self._components.rest.safe_create_message( + return await self._app.rest.safe_create_message( channel=self.channel_id, content=content, nonce=nonce, @@ -637,7 +637,7 @@ async def delete(self) -> None: hikari.errors.Forbidden If you lack the permissions to delete the message. """ - await self._components.rest.delete_messages(channel=self.channel_id, message=self.id) + await self._app.rest.delete_messages(channel=self.channel_id, message=self.id) async def add_reaction(self, emoji: typing.Union[str, emojis_.Emoji]) -> None: r"""Add a reaction to this message. @@ -675,7 +675,7 @@ async def add_reaction(self, emoji: typing.Union[str, emojis_.Emoji]) -> None: due to it being outside of the range of a 64 bit integer. """ - await self._components.rest.add_reaction(channel=self.channel_id, message=self.id, emoji=emoji) + await self._app.rest.add_reaction(channel=self.channel_id, message=self.id, emoji=emoji) async def remove_reaction( self, emoji: typing.Union[str, emojis_.Emoji], *, user: typing.Optional[users.User] = None @@ -719,7 +719,7 @@ async def remove_reaction( If any invalid snowflake IDs are passed; a snowflake may be invalid due to it being outside of the range of a 64 bit integer. """ - await self._components.rest.remove_reaction(channel=self.channel_id, message=self.id, emoji=emoji, user=user) + await self._app.rest.remove_reaction(channel=self.channel_id, message=self.id, emoji=emoji, user=user) async def remove_all_reactions(self, emoji: typing.Optional[typing.Union[str, emojis_.Emoji]] = None) -> None: r"""Remove all users' reactions for a specific emoji from the message. @@ -751,4 +751,4 @@ async def remove_all_reactions(self, emoji: typing.Optional[typing.Union[str, em If any invalid snowflake IDs are passed; a snowflake may be invalid due to it being outside of the range of a 64 bit integer. """ - await self._components.rest.remove_all_reactions(channel=self.channel_id, message=self.id, emoji=emoji) + await self._app.rest.remove_all_reactions(channel=self.channel_id, message=self.id, emoji=emoji) diff --git a/hikari/models/users.py b/hikari/models/users.py index 60f9983379..70cab00097 100644 --- a/hikari/models/users.py +++ b/hikari/models/users.py @@ -143,7 +143,7 @@ async def fetch_self(self) -> User: hikari.errors.NotFound If the user is not found. """ - return await self._components.rest.fetch_user(user=self.id) + return await self._app.rest.fetch_user(user=self.id) @property def avatar_url(self) -> str: @@ -245,4 +245,4 @@ async def fetch_self(self) -> MyUser: hikari.users.User The requested user object. """ - return await self._components.rest.fetch_me() + return await self._app.rest.fetch_me() diff --git a/hikari/models/webhooks.py b/hikari/models/webhooks.py index 3e99cc32e9..89cd78692b 100644 --- a/hikari/models/webhooks.py +++ b/hikari/models/webhooks.py @@ -185,7 +185,7 @@ async def execute( if not self.token: raise ValueError("Cannot send a message using a webhook where we don't know it's token.") - return await self._components.rest.execute_webhook( + return await self._app.rest.execute_webhook( webhook=self.id, webhook_token=self.token, content=content, @@ -228,7 +228,7 @@ async def safe_execute( if not self.token: raise ValueError("Cannot execute a webhook with a unknown token (set to `None`).") - return await self._components.rest.safe_webhook_execute( + return await self._app.rest.safe_webhook_execute( webhook=self.id, webhook_token=self.token, content=content, @@ -270,7 +270,7 @@ async def delete(self, *, use_token: typing.Optional[bool] = None,) -> None: if use_token is None and self.token: use_token = True - await self._components.rest.delete_webhook(webhook=self.id, webhook_token=self.token if use_token else ...) + await self._app.rest.delete_webhook(webhook=self.id, webhook_token=self.token if use_token else ...) async def edit( self, @@ -329,7 +329,7 @@ async def edit( if use_token is None and self.token: use_token = True - return await self._components.rest.update_webhook( + return await self._app.rest.update_webhook( webhook=self.id, webhook_token=self.token if use_token else ..., name=name, @@ -353,7 +353,7 @@ async def fetch_channel(self) -> channels_.PartialChannel: hikari.errors.NotFound If the channel this message was created in does not exist. """ - return await self._components.rest.fetch_channel(channel=self.channel_id) + return await self._app.rest.fetch_channel(channel=self.channel_id) async def fetch_guild(self) -> guilds_.Guild: """Fetch the guild this webhook belongs to. @@ -369,7 +369,7 @@ async def fetch_guild(self) -> guilds_.Guild: If you don't have access to the guild this webhook belongs to or it doesn't exist. """ - return await self._components.rest.fetch_guild(guild=self.guild_id) + return await self._app.rest.fetch_guild(guild=self.guild_id) async def fetch_self(self, *, use_token: typing.Optional[bool] = None) -> Webhook: if use_token and not self.token: @@ -378,9 +378,7 @@ async def fetch_self(self, *, use_token: typing.Optional[bool] = None) -> Webhoo if use_token is None and self.token: use_token = True - return await self._components.rest.fetch_webhook( - webhook=self.id, webhook_token=self.token if use_token else ... - ) + return await self._app.rest.fetch_webhook(webhook=self.id, webhook_token=self.token if use_token else ...) @property def avatar_url(self) -> str: diff --git a/hikari/rest/__init__.py b/hikari/rest/__init__.py index 4a92641af1..c1f911ae6a 100644 --- a/hikari/rest/__init__.py +++ b/hikari/rest/__init__.py @@ -16,6 +16,7 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . +"""Components used for interacting with Discord's RESTful api.""" from __future__ import annotations diff --git a/hikari/rest/base.py b/hikari/rest/base.py index a0d462bc75..604aa388d4 100644 --- a/hikari/rest/base.py +++ b/hikari/rest/base.py @@ -44,8 +44,8 @@ class BaseRESTComponent(abc.ABC, metaclass=meta.UniqueFunctionMeta): """ @abc.abstractmethod - def __init__(self, components: application.Application, session: rest_session.RESTSession) -> None: - self._components = components + def __init__(self, app: application.Application, session: rest_session.RESTSession) -> None: + self._app = app self._session = session async def __aenter__(self) -> BaseRESTComponent: diff --git a/hikari/rest/channel.py b/hikari/rest/channel.py index ba819e62c2..9248ffd4df 100644 --- a/hikari/rest/channel.py +++ b/hikari/rest/channel.py @@ -47,16 +47,16 @@ class _MessagePaginator(pagination.BufferedPaginatedResults[messages_.Message]): - __slots__ = ("_channel_id", "_direction", "_first_id", "_components", "_session") + __slots__ = ("_app", "_channel_id", "_direction", "_first_id", "_session") - def __init__(self, channel, direction, first, components, session) -> None: + def __init__(self, app, channel, direction, first, session) -> None: super().__init__() + self._app = app self._channel_id = str(int(channel)) self._direction = direction self._first_id = ( str(bases.Snowflake.from_datetime(first)) if isinstance(first, datetime.datetime) else str(int(first)) ) - self._components = components self._session = session async def _next_chunk(self): @@ -66,7 +66,7 @@ async def _next_chunk(self): "limit": 100, } - chunk = await self._session.get_channelmessages_(**kwargs) + chunk = await self._session.get_channel_messages(**kwargs) if not chunk: return None @@ -75,7 +75,7 @@ async def _next_chunk(self): self._first_id = chunk[-1]["id"] - return (messages_.Message.deserialize(m, components=self._components) for m in chunk) + return (messages_.Message.deserialize(m, app=self._app) for m in chunk) class RESTChannelComponent(base.BaseRESTComponent, abc.ABC): # pylint: disable=abstract-method, too-many-public-methods @@ -109,7 +109,7 @@ async def fetch_channel( payload = await self._session.get_channel( channel_id=str(channel.id if isinstance(channel, bases.Unique) else int(channel)) ) - return channels_.deserialize_channel(payload, components=self._components) + return channels_.deserialize_channel(payload, app=self._app) async def update_channel( self, @@ -209,7 +209,7 @@ async def update_channel( ), reason=reason, ) - return channels_.deserialize_channel(payload, components=self._components) + return channels_.deserialize_channel(payload, app=self._app) async def delete_channel(self, channel: typing.Union[bases.Snowflake, int, str, channels_.PartialChannel]) -> None: """Delete the given channel ID, or if it is a DM, close it. @@ -249,13 +249,13 @@ async def delete_channel(self, channel: typing.Union[bases.Snowflake, int, str, ) @typing.overload - def fetchmessages_( + def fetch_messages( self, channel: typing.Union[bases.Snowflake, int, str, channels_.PartialChannel] ) -> pagination.PaginatedResults[messages_.Message]: """Fetch the channel history, starting with the newest messages.""" @typing.overload - def fetchmessages_( + def fetch_messages( self, channel: typing.Union[bases.Snowflake, int, str, channels_.PartialChannel], before: typing.Union[datetime.datetime, int, str, bases.Unique, bases.Snowflake], @@ -263,7 +263,7 @@ def fetchmessages_( """Fetch the channel history before a given message/time.""" @typing.overload - def fetchmessages_( + def fetch_messages( self, channel: typing.Union[bases.Snowflake, int, str, channels_.PartialChannel], after: typing.Union[datetime.datetime, int, str, bases.Unique, bases.Snowflake], @@ -271,14 +271,14 @@ def fetchmessages_( """Fetch the channel history after a given message/time.""" @typing.overload - def fetchmessages_( + def fetch_messages( self, channel: typing.Union[bases.Snowflake, int, str, channels_.PartialChannel], around: typing.Union[datetime.datetime, int, str, bases.Unique, bases.Snowflake], ) -> pagination.PaginatedResults[messages_.Message]: """Fetch the channel history around a given message/time.""" - def fetchmessages_( + def fetch_messages( self, channel: typing.Union[bases.Snowflake, int, str, channels_.PartialChannel], **kwargs: typing.Union[datetime.datetime, int, str, bases.Unique, bases.Snowflake], @@ -328,33 +328,33 @@ def fetchmessages_( timestamp = datetime.datetime(2020, 5, 2) - async for message in rest.fetchmessages_(channel, before=timestamp).limit(20): + async for message in rest.fetch_messages(channel, before=timestamp).limit(20): print(message.author, message.content) Fetching messages sent around the same time as a given message. - async for message in rest.fetchmessages_(channel, around=event.message): + async for message in rest.fetch_messages(channel, around=event.message): print(message.author, message.content) Fetching messages after May 3rd, 2020 at 15:33 UTC. timestamp = datetime.datetime(2020, 5, 3, 15, 33, tzinfo=datetime.timezone.utc) - async for message in rest.fetchmessages_(channel, after=timestamp): + async for message in rest.fetch_messages(channel, after=timestamp): print(message.author, message.content) Fetching all messages, newest to oldest: - async for message in rest.fetchmessages_(channel, before=datetime.datetime.utcnow()): + async for message in rest.fetch_messages(channel, before=datetime.datetime.utcnow()): print(message) # More efficient alternative - async for message in rest.fetchmessages_(channel): + async for message in rest.fetch_messages(channel): print(message) Fetching all messages, oldest to newest: - async for message in rest.fetchmessages_(channel, after=): + async for message in rest.fetch_messages(channel, after=): print(message) !!! warning @@ -395,7 +395,7 @@ def fetchmessages_( direction, first = "before", bases.Snowflake.max() return _MessagePaginator( - channel=channel, direction=direction, first=first, components=self._components, session=self._session, + app=self._app, channel=channel, direction=direction, first=first, session=self._session, ) async def fetch_message( @@ -434,7 +434,7 @@ async def fetch_message( channel_id=str(channel.id if isinstance(channel, bases.Unique) else int(channel)), message_id=str(message.id if isinstance(message, bases.Unique) else int(message)), ) - return messages_.Message.deserialize(payload, components=self._components) + return messages_.Message.deserialize(payload, app=self._app) async def create_message( # pylint: disable=line-too-long self, @@ -526,7 +526,7 @@ async def create_message( # pylint: disable=line-too-long mentions_everyone=mentions_everyone, user_mentions=user_mentions, role_mentions=role_mentions ), ) - return messages_.Message.deserialize(payload, components=self._components) + return messages_.Message.deserialize(payload, app=self._app) def safe_create_message( self, @@ -645,7 +645,7 @@ async def update_message( # pylint: disable=line-too-long mentions_everyone=mentions_everyone, user_mentions=user_mentions, role_mentions=role_mentions, ), ) - return messages_.Message.deserialize(payload, components=self._components) + return messages_.Message.deserialize(payload, app=self._app) def safe_update_message( self, @@ -681,11 +681,11 @@ def safe_update_message( role_mentions=role_mentions, ) - async def deletemessages_( + async def delete_messages( self, channel: typing.Union[bases.Snowflake, int, str, channels_.PartialChannel], message: typing.Union[bases.Snowflake, int, str, messages_.Message], - *additionalmessages_: typing.Union[bases.Snowflake, int, str, messages_.Message], + *additional_messages: typing.Union[bases.Snowflake, int, str, messages_.Message], ) -> None: """Delete a message in a given channel. @@ -695,7 +695,7 @@ async def deletemessages_( The object or ID of the channel to get the message from. message : typing.Union[hikari.messages.Message, hikari.bases.Snowflake, int] The object or ID of the message to delete. - *additionalmessages_ : typing.Union[hikari.messages.Message, hikari.bases.Snowflake, int] + *additional_messages : typing.Union[hikari.messages.Message, hikari.bases.Snowflake, int] Objects and/or IDs of additional messages to delete in the same channel, in total you can delete up to 100 messages in a request. @@ -720,11 +720,11 @@ async def deletemessages_( messages that are newer than `2` weeks in age. If any of the messages are older than `2` weeks then this call will fail. """ - if additionalmessages_: + if additional_messages: messages = list( # dict.fromkeys is used to remove duplicate entries that would cause discord to return an error. dict.fromkeys( - str(m.id if isinstance(m, bases.Unique) else int(m)) for m in (message, *additionalmessages_) + str(m.id if isinstance(m, bases.Unique) else int(m)) for m in (message, *additional_messages) ) ) if len(messages) > 100: @@ -822,7 +822,7 @@ async def fetch_invites_for_channel( payload = await self._session.get_channel_invites( channel_id=str(channel.id if isinstance(channel, bases.Unique) else int(channel)) ) - return [invites.InviteWithMetadata.deserialize(invite, components=self._components) for invite in payload] + return [invites.InviteWithMetadata.deserialize(invite, app=self._app) for invite in payload] async def create_invite_for_channel( self, @@ -894,7 +894,7 @@ async def create_invite_for_channel( target_user_type=target_user_type, reason=reason, ) - return invites.InviteWithMetadata.deserialize(payload, components=self._components) + return invites.InviteWithMetadata.deserialize(payload, app=self._app) async def delete_channel_overwrite( # pylint: disable=line-too-long self, @@ -982,8 +982,7 @@ async def fetch_pins( channel_id=str(channel.id if isinstance(channel, bases.Unique) else int(channel)) ) return { - bases.Snowflake(message["id"]): messages_.Message.deserialize(message, components=self._components) - for message in payload + bases.Snowflake(message["id"]): messages_.Message.deserialize(message, app=self._app) for message in payload } async def pin_message( @@ -1091,7 +1090,7 @@ async def create_webhook( avatar=await avatar.read() if avatar is not ... else ..., reason=reason, ) - return webhooks.Webhook.deserialize(payload, components=self._components) + return webhooks.Webhook.deserialize(payload, app=self._app) async def fetch_channel_webhooks( self, channel: typing.Union[bases.Snowflake, int, str, channels_.GuildChannel] @@ -1122,4 +1121,4 @@ async def fetch_channel_webhooks( payload = await self._session.get_channel_webhooks( channel_id=str(channel.id if isinstance(channel, bases.Unique) else int(channel)) ) - return [webhooks.Webhook.deserialize(webhook, components=self._components) for webhook in payload] + return [webhooks.Webhook.deserialize(webhook, app=self._app) for webhook in payload] diff --git a/hikari/rest/gateway.py b/hikari/rest/gateway.py index 6c093beaf6..d6810929ea 100644 --- a/hikari/rest/gateway.py +++ b/hikari/rest/gateway.py @@ -59,4 +59,4 @@ async def fetch_gateway_bot(self) -> gateway.GatewayBot: The bot specific gateway information object. """ payload = await self._session.get_gateway_bot() - return gateway.GatewayBot.deserialize(payload, components=self._components) + return gateway.GatewayBot.deserialize(payload, app=self._app) diff --git a/hikari/rest/guild.py b/hikari/rest/guild.py index 1b59edb67b..25647c7282 100644 --- a/hikari/rest/guild.py +++ b/hikari/rest/guild.py @@ -47,13 +47,13 @@ class _MemberPaginator(pagination.BufferedPaginatedResults[guilds.GuildMember]): - __slots__ = ("_guild_id", "_first_id", "_components", "_session") + __slots__ = ("_app", "_guild_id", "_first_id", "_session") - def __init__(self, guild, created_after, components, session): + def __init__(self, app, guild, created_after, session): super().__init__() + self._app = app self._guild_id = str(int(guild)) self._first_id = self._prepare_first_id(created_after) - self._components = components self._session = session async def _next_chunk(self): @@ -64,7 +64,7 @@ async def _next_chunk(self): self._first_id = chunk[-1]["id"] - return (guilds.GuildMember.deserialize(m, components=self._components) for m in chunk) + return (guilds.GuildMember.deserialize(m, app=self._app) for m in chunk) class RESTGuildComponent(base.BaseRESTComponent, abc.ABC): # pylint: disable=abstract-method, too-many-public-methods @@ -126,7 +126,7 @@ async def fetch_audit_log( limit=limit, before=before, ) - return audit_logs.AuditLog.deserialize(payload, components=self._components) + return audit_logs.AuditLog.deserialize(payload, app=self._app) def fetch_audit_log_entries_before( self, @@ -194,7 +194,7 @@ def fetch_audit_log_entries_before( user_id=(str(user.id if isinstance(user, bases.Unique) else int(user)) if user is not ... else ...), action_type=action_type, ) - return audit_logs.AuditLogIterator(app=self._components, request=request, before=before, limit=limit) + return audit_logs.AuditLogIterator(app=self._app, request=request, before=before, limit=limit) async def fetch_guild_emoji( self, @@ -229,7 +229,7 @@ async def fetch_guild_emoji( guild_id=str(guild.id if isinstance(guild, bases.Unique) else int(guild)), emoji_id=str(emoji.id if isinstance(emoji, bases.Unique) else int(emoji)), ) - return emojis.KnownCustomEmoji.deserialize(payload, components=self._components) + return emojis.KnownCustomEmoji.deserialize(payload, app=self._app) async def fetch_guild_emojis( self, guild: typing.Union[bases.Snowflake, int, str, guilds.Guild] @@ -259,7 +259,7 @@ async def fetch_guild_emojis( payload = await self._session.list_guild_emojis( guild_id=str(guild.id if isinstance(guild, bases.Unique) else int(guild)) ) - return [emojis.KnownCustomEmoji.deserialize(emoji, components=self._components) for emoji in payload] + return [emojis.KnownCustomEmoji.deserialize(emoji, app=self._app) for emoji in payload] async def create_guild_emoji( self, @@ -316,7 +316,7 @@ async def create_guild_emoji( else ..., reason=reason, ) - return emojis.KnownCustomEmoji.deserialize(payload, components=self._components) + return emojis.KnownCustomEmoji.deserialize(payload, app=self._app) async def update_guild_emoji( self, @@ -371,7 +371,7 @@ async def update_guild_emoji( else ..., reason=reason, ) - return emojis.KnownCustomEmoji.deserialize(payload, components=self._components) + return emojis.KnownCustomEmoji.deserialize(payload, app=self._app) async def delete_guild_emoji( self, @@ -471,7 +471,7 @@ async def create_guild( roles=[role.serialize() for role in roles] if roles is not ... else ..., channels=[channel.serialize() for channel in channels] if channels is not ... else ..., ) - return guilds.Guild.deserialize(payload, components=self._components) + return guilds.Guild.deserialize(payload, app=self._app) async def fetch_guild(self, guild: typing.Union[bases.Snowflake, int, str, guilds.Guild]) -> guilds.Guild: """Get a given guild's object. @@ -501,7 +501,7 @@ async def fetch_guild(self, guild: typing.Union[bases.Snowflake, int, str, guild # Always get counts. There is no reason you would _not_ want this info, right? with_counts=True, ) - return guilds.Guild.deserialize(payload, components=self._components) + return guilds.Guild.deserialize(payload, app=self._app) async def fetch_guild_preview( self, guild: typing.Union[bases.Snowflake, int, str, guilds.Guild] @@ -533,7 +533,7 @@ async def fetch_guild_preview( payload = await self._session.get_guild_preview( guild_id=str(guild.id if isinstance(guild, bases.Unique) else int(guild)) ) - return guilds.GuildPreview.deserialize(payload, components=self._components) + return guilds.GuildPreview.deserialize(payload, app=self._app) async def update_guild( self, @@ -627,7 +627,7 @@ async def update_guild( ), reason=reason, ) - return guilds.Guild.deserialize(payload, components=self._components) + return guilds.Guild.deserialize(payload, app=self._app) async def delete_guild(self, guild: typing.Union[bases.Snowflake, int, str, guilds.Guild]) -> None: """Permanently deletes the given guild. @@ -679,7 +679,7 @@ async def fetch_guild_channels( payload = await self._session.list_guild_channels( guild_id=str(guild.id if isinstance(guild, bases.Unique) else int(guild)) ) - return [channels_.deserialize_channel(channel, components=self._components) for channel in payload] + return [channels_.deserialize_channel(channel, app=self._app) for channel in payload] async def create_guild_channel( # pylint: disable=too-many-arguments self, @@ -783,7 +783,7 @@ async def create_guild_channel( # pylint: disable=too-many-arguments ), reason=reason, ) - return channels_.deserialize_channel(payload, components=self._components) + return channels_.deserialize_channel(payload, app=self._app) async def reposition_guild_channels( self, @@ -859,7 +859,7 @@ async def fetch_member( guild_id=str(guild.id if isinstance(guild, bases.Unique) else int(guild)), user_id=str(user.id if isinstance(user, bases.Unique) else int(user)), ) - return guilds.GuildMember.deserialize(payload, components=self._components) + return guilds.GuildMember.deserialize(payload, app=self._app) def fetch_members( self, guild: typing.Union[bases.Snowflake, int, str, guilds.Guild], @@ -892,7 +892,7 @@ def fetch_members( hikari.errors.Forbidden If you are not in the guild. """ - return _MemberPaginator(guild=guild, created_after=None, components=self._components, session=self._session) + return _MemberPaginator(app=self._app, guild=guild, created_after=None, session=self._session) async def update_member( # pylint: disable=too-many-arguments self, @@ -1149,7 +1149,7 @@ async def fetch_ban( guild_id=str(guild.id if isinstance(guild, bases.Unique) else int(guild)), user_id=str(user.id if isinstance(user, bases.Unique) else int(user)), ) - return guilds.GuildMemberBan.deserialize(payload, components=self._components) + return guilds.GuildMemberBan.deserialize(payload, app=self._app) async def fetch_bans( self, guild: typing.Union[bases.Snowflake, int, str, guilds.Guild] @@ -1179,7 +1179,7 @@ async def fetch_bans( payload = await self._session.get_guild_bans( guild_id=str(guild.id if isinstance(guild, bases.Unique) else int(guild)) ) - return [guilds.GuildMemberBan.deserialize(ban, components=self._components) for ban in payload] + return [guilds.GuildMemberBan.deserialize(ban, app=self._app) for ban in payload] async def ban_member( self, @@ -1286,10 +1286,7 @@ async def fetch_roles( payload = await self._session.get_guild_roles( guild_id=str(guild.id if isinstance(guild, bases.Unique) else int(guild)) ) - return { - bases.Snowflake(role["id"]): guilds.GuildRole.deserialize(role, components=self._components) - for role in payload - } + return {bases.Snowflake(role["id"]): guilds.GuildRole.deserialize(role, app=self._app) for role in payload} async def create_role( self, @@ -1350,7 +1347,7 @@ async def create_role( mentionable=mentionable, reason=reason, ) - return guilds.GuildRole.deserialize(payload, components=self._components) + return guilds.GuildRole.deserialize(payload, app=self._app) async def reposition_roles( self, @@ -1395,7 +1392,7 @@ async def reposition_roles( for position, channel in [role, *additional_roles] ], ) - return [guilds.GuildRole.deserialize(role, components=self._components) for role in payload] + return [guilds.GuildRole.deserialize(role, app=self._app) for role in payload] async def update_role( self, @@ -1460,7 +1457,7 @@ async def update_role( mentionable=mentionable, reason=reason, ) - return guilds.GuildRole.deserialize(payload, components=self._components) + return guilds.GuildRole.deserialize(payload, app=self._app) async def delete_role( self, @@ -1599,7 +1596,7 @@ async def fetch_guild_voice_regions( payload = await self._session.get_guild_voice_regions( guild_id=str(guild.id if isinstance(guild, bases.Unique) else int(guild)) ) - return [voices.VoiceRegion.deserialize(region, components=self._components) for region in payload] + return [voices.VoiceRegion.deserialize(region, app=self._app) for region in payload] async def fetch_guild_invites( self, guild: typing.Union[bases.Snowflake, int, str, guilds.Guild], @@ -1629,7 +1626,7 @@ async def fetch_guild_invites( payload = await self._session.get_guild_invites( guild_id=str(guild.id if isinstance(guild, bases.Unique) else int(guild)) ) - return [invites.InviteWithMetadata.deserialize(invite, components=self._components) for invite in payload] + return [invites.InviteWithMetadata.deserialize(invite, app=self._app) for invite in payload] async def fetch_integrations( self, guild: typing.Union[bases.Snowflake, int, str, guilds.Guild] @@ -1659,9 +1656,7 @@ async def fetch_integrations( payload = await self._session.get_guild_integrations( guild_id=str(guild.id if isinstance(guild, bases.Unique) else int(guild)) ) - return [ - guilds.GuildIntegration.deserialize(integration, components=self._components) for integration in payload - ] + return [guilds.GuildIntegration.deserialize(integration, app=self._app) for integration in payload] async def update_integration( self, @@ -1806,7 +1801,7 @@ async def fetch_guild_embed( payload = await self._session.get_guild_embed( guild_id=str(guild.id if isinstance(guild, bases.Unique) else int(guild)) ) - return guilds.GuildEmbed.deserialize(payload, components=self._components) + return guilds.GuildEmbed.deserialize(payload, app=self._app) async def update_guild_embed( self, @@ -1856,7 +1851,7 @@ async def update_guild_embed( enabled=enabled, reason=reason, ) - return guilds.GuildEmbed.deserialize(payload, components=self._components) + return guilds.GuildEmbed.deserialize(payload, app=self._app) async def fetch_guild_vanity_url( self, guild: typing.Union[bases.Snowflake, int, str, guilds.Guild] @@ -1889,7 +1884,7 @@ async def fetch_guild_vanity_url( payload = await self._session.get_guild_vanity_url( guild_id=str(guild.id if isinstance(guild, bases.Unique) else int(guild)) ) - return invites.VanityUrl.deserialize(payload, components=self._components) + return invites.VanityUrl.deserialize(payload, app=self._app) def format_guild_widget_image( self, guild: typing.Union[bases.Snowflake, int, str, guilds.Guild], *, style: str = ... @@ -1950,4 +1945,4 @@ async def fetch_guild_webhooks( payload = await self._session.get_guild_webhooks( guild_id=str(guild.id if isinstance(guild, bases.Unique) else int(guild)) ) - return [webhooks.Webhook.deserialize(webhook, components=self._components) for webhook in payload] + return [webhooks.Webhook.deserialize(webhook, app=self._app) for webhook in payload] diff --git a/hikari/rest/invite.py b/hikari/rest/invite.py index 980e63bbf4..65c486ecae 100644 --- a/hikari/rest/invite.py +++ b/hikari/rest/invite.py @@ -60,7 +60,7 @@ async def fetch_invite( If the invite is not found. """ payload = await self._session.get_invite(invite_code=getattr(invite, "code", invite), with_counts=with_counts) - return invites.Invite.deserialize(payload, components=self._components) + return invites.Invite.deserialize(payload, app=self._app) async def delete_invite(self, invite: typing.Union[invites.Invite, str]) -> None: """Delete a given invite. diff --git a/hikari/rest/me.py b/hikari/rest/me.py index 430aaa5e3c..2fa1280dfe 100644 --- a/hikari/rest/me.py +++ b/hikari/rest/me.py @@ -40,15 +40,15 @@ class _GuildPaginator(pagination.BufferedPaginatedResults[guilds.Guild]): - __slots__ = ("_session", "_components", "_newest_first", "_first_id") + __slots__ = ("_app", "_session", "_newest_first", "_first_id") - def __init__(self, newest_first, first_item, components, session): + def __init__(self, app, newest_first, first_item, session): super().__init__() + self._app = app self._newest_first = newest_first self._first_id = self._prepare_first_id( first_item, bases.Snowflake.max() if newest_first else bases.Snowflake.min(), ) - self._components = components self._session = session async def _next_chunk(self): @@ -61,7 +61,7 @@ async def _next_chunk(self): self._first_id = chunk[-1]["id"] - return (applications.OwnGuild.deserialize(g, components=self._components) for g in chunk) + return (applications.OwnGuild.deserialize(g, app=self._app) for g in chunk) class RESTCurrentUserComponent(base.BaseRESTComponent, abc.ABC): # pylint: disable=abstract-method @@ -76,7 +76,7 @@ async def fetch_me(self) -> users.MyUser: The current user object. """ payload = await self._session.get_current_user() - return users.MyUser.deserialize(payload, components=self._components) + return users.MyUser.deserialize(payload, app=self._app) async def update_me(self, *, username: str = ..., avatar: typing.Optional[files.BaseStream] = ...) -> users.MyUser: """Edit the current user. @@ -107,7 +107,7 @@ async def update_me(self, *, username: str = ..., avatar: typing.Optional[files. payload = await self._session.modify_current_user( username=username, avatar=await avatar.read() if avatar is not ... else ..., ) - return users.MyUser.deserialize(payload, components=self._components) + return users.MyUser.deserialize(payload, app=self._app) async def fetch_my_connections(self) -> typing.Sequence[applications.OwnConnection]: """ @@ -125,9 +125,7 @@ async def fetch_my_connections(self) -> typing.Sequence[applications.OwnConnecti A list of connection objects. """ payload = await self._session.get_current_user_connections() - return [ - applications.OwnConnection.deserialize(connection, components=self._components) for connection in payload - ] + return [applications.OwnConnection.deserialize(connection, app=self._app) for connection in payload] def fetch_my_guilds( self, @@ -158,9 +156,7 @@ def fetch_my_guilds( hikari.errors.NotFound If the guild is not found. """ - return _GuildPaginator( - newest_first=newest_first, first_item=start_at, components=self._components, session=self._session - ) + return _GuildPaginator(app=self._app, newest_first=newest_first, first_item=start_at, session=self._session) async def leave_guild(self, guild: typing.Union[bases.Snowflake, int, str, guilds.Guild]) -> None: """Make the current user leave a given guild. @@ -206,4 +202,4 @@ async def create_dm_channel( payload = await self._session.create_dm( recipient_id=str(recipient.id if isinstance(recipient, bases.Unique) else int(recipient)) ) - return channels_.DMChannel.deserialize(payload, components=self._components) + return channels_.DMChannel.deserialize(payload, app=self._app) diff --git a/hikari/rest/oauth2.py b/hikari/rest/oauth2.py index 336b3cae8a..d4b1780b79 100644 --- a/hikari/rest/oauth2.py +++ b/hikari/rest/oauth2.py @@ -40,7 +40,7 @@ async def fetch_my_application_info(self) -> applications.Application: An application info object. """ payload = await self._session.get_current_application_info() - return applications.Application.deserialize(payload, components=self._components) + return applications.Application.deserialize(payload, app=self._app) async def add_guild_member(self, *_, **__): # TODO: implement and document this. diff --git a/hikari/rest/react.py b/hikari/rest/react.py index d50a974c8e..6cf5611c3a 100644 --- a/hikari/rest/react.py +++ b/hikari/rest/react.py @@ -38,15 +38,15 @@ class _ReactionPaginator(pagination.BufferedPaginatedResults[messages_.Reaction]): - __slots__ = ("_channel_id", "_message_id", "_first_id", "_emoji", "_components", "_session") + __slots__ = ("_app", "_channel_id", "_message_id", "_first_id", "_emoji", "_session") - def __init__(self, channel, message, emoji, users_after, components, session) -> None: + def __init__(self, app, channel, message, emoji, users_after, session) -> None: super().__init__() + self._app = app self._channel_id = str(int(channel)) self._message_id = str(int(message)) self._emoji = getattr(emoji, "url_name", emoji) self._first_id = self._prepare_first_id(users_after) - self._components = components self._session = session async def _next_chunk(self): @@ -59,7 +59,7 @@ async def _next_chunk(self): self._first_id = chunk[-1]["id"] - return (users.User.deserialize(u, components=self._components) for u in chunk) + return (users.User.deserialize(u, app=self._app) for u in chunk) class RESTReactionComponent(base.BaseRESTComponent, abc.ABC): # pylint: disable=abstract-method @@ -259,10 +259,5 @@ def fetch_reactors( If the channel or message is not found. """ return _ReactionPaginator( - channel=channel, - message=message, - emoji=emoji, - users_after=after, - components=self._components, - session=self._session, + app=self._app, channel=channel, message=message, emoji=emoji, users_after=after, session=self._session, ) diff --git a/hikari/rest/user.py b/hikari/rest/user.py index eb32aeda81..12fae68183 100644 --- a/hikari/rest/user.py +++ b/hikari/rest/user.py @@ -56,4 +56,4 @@ async def fetch_user(self, user: typing.Union[bases.Snowflake, int, str, users.U If the user is not found. """ payload = await self._session.get_user(user_id=str(user.id if isinstance(user, bases.Unique) else int(user))) - return users.User.deserialize(payload, components=self._components) + return users.User.deserialize(payload, app=self._app) diff --git a/hikari/rest/voice.py b/hikari/rest/voice.py index b81ecaef51..9f0e962f02 100644 --- a/hikari/rest/voice.py +++ b/hikari/rest/voice.py @@ -44,4 +44,4 @@ async def fetch_voice_regions(self) -> typing.Sequence[voices.VoiceRegion]: This does not include VIP servers. """ payload = await self._session.list_voice_regions() - return [voices.VoiceRegion.deserialize(region, components=self._components) for region in payload] + return [voices.VoiceRegion.deserialize(region, app=self._app) for region in payload] diff --git a/hikari/rest/webhook.py b/hikari/rest/webhook.py index 65c77620cf..c19caaec83 100644 --- a/hikari/rest/webhook.py +++ b/hikari/rest/webhook.py @@ -78,7 +78,7 @@ async def fetch_webhook( webhook_id=str(webhook.id if isinstance(webhook, bases.Unique) else int(webhook)), webhook_token=webhook_token, ) - return webhooks.Webhook.deserialize(payload, components=self._components) + return webhooks.Webhook.deserialize(payload, app=self._app) async def update_webhook( self, @@ -141,7 +141,7 @@ async def update_webhook( ), reason=reason, ) - return webhooks.Webhook.deserialize(payload, components=self._components) + return webhooks.Webhook.deserialize(payload, app=self._app) async def delete_webhook( self, webhook: typing.Union[bases.Snowflake, int, str, webhooks.Webhook], *, webhook_token: str = ... @@ -276,7 +276,7 @@ async def execute_webhook( # pylint:disable=too-many-locals,line-too-long ), ) if wait is True: - return messages_.Message.deserialize(payload, components=self._components) + return messages_.Message.deserialize(payload, app=self._app) return None def safe_webhook_execute( diff --git a/hikari/stateless/manager.py b/hikari/stateless/manager.py index 2e26d3d135..8f07c82cae 100644 --- a/hikari/stateless/manager.py +++ b/hikari/stateless/manager.py @@ -45,142 +45,142 @@ class StatelessEventManagerImpl(event_managers.EventManager[dispatchers.EventDis async def on_connect(self, shard, _) -> None: """Handle CONNECTED events.""" event = other.ConnectedEvent(shard=shard) - await self._components.event_dispatcher.dispatch_event(event) + await self._app.event_dispatcher.dispatch_event(event) @event_managers.raw_event_mapper("DISCONNECTED") async def on_disconnect(self, shard, _) -> None: """Handle DISCONNECTED events.""" event = other.DisconnectedEvent(shard=shard) - await self._components.event_dispatcher.dispatch_event(event) + await self._app.event_dispatcher.dispatch_event(event) @event_managers.raw_event_mapper("RESUME") async def on_resume(self, shard, _) -> None: """Handle RESUME events.""" event = other.ResumedEvent(shard=shard) - await self._components.event_dispatcher.dispatch_event(event) + await self._app.event_dispatcher.dispatch_event(event) @event_managers.raw_event_mapper("READY") async def on_ready(self, _, payload) -> None: """Handle READY events.""" - event = other.ReadyEvent.deserialize(payload, components=self._components) - await self._components.event_dispatcher.dispatch_event(event) + event = other.ReadyEvent.deserialize(payload, app=self._app) + await self._app.event_dispatcher.dispatch_event(event) @event_managers.raw_event_mapper("CHANNEL_CREATE") async def on_channel_create(self, _, payload) -> None: """Handle CHANNEL_CREATE events.""" - event = channel.ChannelCreateEvent.deserialize(payload, components=self._components) - await self._components.event_dispatcher.dispatch_event(event) + event = channel.ChannelCreateEvent.deserialize(payload, app=self._app) + await self._app.event_dispatcher.dispatch_event(event) @event_managers.raw_event_mapper("CHANNEL_UPDATE") async def on_channel_update(self, _, payload) -> None: """Handle CHANNEL_UPDATE events.""" - event = channel.ChannelUpdateEvent.deserialize(payload, components=self._components) - await self._components.event_dispatcher.dispatch_event(event) + event = channel.ChannelUpdateEvent.deserialize(payload, app=self._app) + await self._app.event_dispatcher.dispatch_event(event) @event_managers.raw_event_mapper("CHANNEL_DELETE") async def on_channel_delete(self, _, payload) -> None: """Handle CHANNEL_DELETE events.""" - event = channel.ChannelDeleteEvent.deserialize(payload, components=self._components) - await self._components.event_dispatcher.dispatch_event(event) + event = channel.ChannelDeleteEvent.deserialize(payload, app=self._app) + await self._app.event_dispatcher.dispatch_event(event) @event_managers.raw_event_mapper("CHANNEL_PINS_UPDATE") async def on_channel_pins_update(self, _, payload) -> None: """Handle CHANNEL_PINS_UPDATE events.""" - event = channel.ChannelPinsUpdateEvent.deserialize(payload, components=self._components) - await self._components.event_dispatcher.dispatch_event(event) + event = channel.ChannelPinsUpdateEvent.deserialize(payload, app=self._app) + await self._app.event_dispatcher.dispatch_event(event) @event_managers.raw_event_mapper("GUILD_CREATE") async def on_guild_create(self, _, payload) -> None: """Handle GUILD_CREATE events.""" - event = guild.GuildCreateEvent.deserialize(payload, components=self._components) - await self._components.event_dispatcher.dispatch_event(event) + event = guild.GuildCreateEvent.deserialize(payload, app=self._app) + await self._app.event_dispatcher.dispatch_event(event) @event_managers.raw_event_mapper("GUILD_UPDATE") async def on_guild_update(self, _, payload) -> None: """Handle GUILD_UPDATE events.""" - event = guild.GuildUpdateEvent.deserialize(payload, components=self._components) - await self._components.event_dispatcher.dispatch_event(event) + event = guild.GuildUpdateEvent.deserialize(payload, app=self._app) + await self._app.event_dispatcher.dispatch_event(event) @event_managers.raw_event_mapper("GUILD_DELETE") async def on_guild_delete(self, _, payload) -> None: """Handle GUILD_DELETE events.""" if payload.get("unavailable", False): - event = guild.GuildUnavailableEvent.deserialize(payload, components=self._components) + event = guild.GuildUnavailableEvent.deserialize(payload, app=self._app) else: - event = guild.GuildLeaveEvent.deserialize(payload, components=self._components) - await self._components.event_dispatcher.dispatch_event(event) + event = guild.GuildLeaveEvent.deserialize(payload, app=self._app) + await self._app.event_dispatcher.dispatch_event(event) @event_managers.raw_event_mapper("GUILD_BAN_ADD") async def on_guild_ban_add(self, _, payload) -> None: """Handle GUILD_BAN_ADD events.""" - event = guild.GuildBanAddEvent.deserialize(payload, components=self._components) - await self._components.event_dispatcher.dispatch_event(event) + event = guild.GuildBanAddEvent.deserialize(payload, app=self._app) + await self._app.event_dispatcher.dispatch_event(event) @event_managers.raw_event_mapper("GUILD_BAN_REMOVE") async def on_guild_ban_remove(self, _, payload) -> None: """Handle GUILD_BAN_REMOVE events.""" - event = guild.GuildBanRemoveEvent.deserialize(payload, components=self._components) - await self._components.event_dispatcher.dispatch_event(event) + event = guild.GuildBanRemoveEvent.deserialize(payload, app=self._app) + await self._app.event_dispatcher.dispatch_event(event) @event_managers.raw_event_mapper("GUILD_EMOJIS_UPDATE") async def on_guild_emojis_update(self, _, payload) -> None: """Handle GUILD_EMOJIS_UPDATE events.""" - event = guild.GuildEmojisUpdateEvent.deserialize(payload, components=self._components) - await self._components.event_dispatcher.dispatch_event(event) + event = guild.GuildEmojisUpdateEvent.deserialize(payload, app=self._app) + await self._app.event_dispatcher.dispatch_event(event) @event_managers.raw_event_mapper("GUILD_INTEGRATIONS_UPDATE") async def on_guild_integrations_update(self, _, payload) -> None: """Handle GUILD_INTEGRATIONS_UPDATE events.""" - event = guild.GuildIntegrationsUpdateEvent.deserialize(payload, components=self._components) - await self._components.event_dispatcher.dispatch_event(event) + event = guild.GuildIntegrationsUpdateEvent.deserialize(payload, app=self._app) + await self._app.event_dispatcher.dispatch_event(event) @event_managers.raw_event_mapper("GUILD_MEMBER_ADD") async def on_guild_member_add(self, _, payload) -> None: """Handle GUILD_MEMBER_ADD events.""" - event = guild.GuildMemberAddEvent.deserialize(payload, components=self._components) - await self._components.event_dispatcher.dispatch_event(event) + event = guild.GuildMemberAddEvent.deserialize(payload, app=self._app) + await self._app.event_dispatcher.dispatch_event(event) @event_managers.raw_event_mapper("GUILD_MEMBER_UPDATE") async def on_guild_member_update(self, _, payload) -> None: """Handle GUILD_MEMBER_UPDATE events.""" - event = guild.GuildMemberUpdateEvent.deserialize(payload, components=self._components) - await self._components.event_dispatcher.dispatch_event(event) + event = guild.GuildMemberUpdateEvent.deserialize(payload, app=self._app) + await self._app.event_dispatcher.dispatch_event(event) @event_managers.raw_event_mapper("GUILD_MEMBER_REMOVE") async def on_guild_member_remove(self, _, payload) -> None: """Handle GUILD_MEMBER_REMOVE events.""" - event = guild.GuildMemberRemoveEvent.deserialize(payload, components=self._components) - await self._components.event_dispatcher.dispatch_event(event) + event = guild.GuildMemberRemoveEvent.deserialize(payload, app=self._app) + await self._app.event_dispatcher.dispatch_event(event) @event_managers.raw_event_mapper("GUILD_ROLE_CREATE") async def on_guild_role_create(self, _, payload) -> None: """Handle GUILD_ROLE_CREATE events.""" - event = guild.GuildRoleCreateEvent.deserialize(payload, components=self._components) - await self._components.event_dispatcher.dispatch_event(event) + event = guild.GuildRoleCreateEvent.deserialize(payload, app=self._app) + await self._app.event_dispatcher.dispatch_event(event) @event_managers.raw_event_mapper("GUILD_ROLE_UPDATE") async def on_guild_role_update(self, _, payload) -> None: """Handle GUILD_ROLE_UPDATE events.""" - event = guild.GuildRoleUpdateEvent.deserialize(payload, components=self._components) - await self._components.event_dispatcher.dispatch_event(event) + event = guild.GuildRoleUpdateEvent.deserialize(payload, app=self._app) + await self._app.event_dispatcher.dispatch_event(event) @event_managers.raw_event_mapper("GUILD_ROLE_DELETE") async def on_guild_role_delete(self, _, payload) -> None: """Handle GUILD_ROLE_DELETE events.""" - event = guild.GuildRoleDeleteEvent.deserialize(payload, components=self._components) - await self._components.event_dispatcher.dispatch_event(event) + event = guild.GuildRoleDeleteEvent.deserialize(payload, app=self._app) + await self._app.event_dispatcher.dispatch_event(event) @event_managers.raw_event_mapper("INVITE_CREATE") async def on_invite_create(self, _, payload) -> None: """Handle INVITE_CREATE events.""" - event = channel.InviteCreateEvent.deserialize(payload, components=self._components) - await self._components.event_dispatcher.dispatch_event(event) + event = channel.InviteCreateEvent.deserialize(payload, app=self._app) + await self._app.event_dispatcher.dispatch_event(event) @event_managers.raw_event_mapper("INVITE_DELETE") async def on_invite_delete(self, _, payload) -> None: """Handle INVITE_DELETE events.""" - event = channel.InviteDeleteEvent.deserialize(payload, components=self._components) - await self._components.event_dispatcher.dispatch_event(event) + event = channel.InviteDeleteEvent.deserialize(payload, app=self._app) + await self._app.event_dispatcher.dispatch_event(event) @event_managers.raw_event_mapper("MESSAGE_CREATE") async def on_message_create(self, _, payload) -> None: @@ -190,8 +190,8 @@ async def on_message_create(self, _, payload) -> None: # user object between Message.author and Message.member.user if "member" in payload: payload["member"]["user"] = payload["author"] - event = message.MessageCreateEvent.deserialize(payload, components=self._components) - await self._components.event_dispatcher.dispatch_event(event) + event = message.MessageCreateEvent.deserialize(payload, app=self._app) + await self._app.event_dispatcher.dispatch_event(event) @event_managers.raw_event_mapper("MESSAGE_UPDATE") async def on_message_update(self, _, payload) -> None: @@ -201,78 +201,78 @@ async def on_message_update(self, _, payload) -> None: # user object between Message.author and Message.member.user if "member" in payload and "author" in payload: payload["member"]["user"] = payload["author"] - event = message.MessageUpdateEvent.deserialize(payload, components=self._components) - await self._components.event_dispatcher.dispatch_event(event) + event = message.MessageUpdateEvent.deserialize(payload, app=self._app) + await self._app.event_dispatcher.dispatch_event(event) @event_managers.raw_event_mapper("MESSAGE_DELETE") async def on_message_delete(self, _, payload) -> None: """Handle MESSAGE_DELETE events.""" - event = message.MessageDeleteEvent.deserialize(payload, components=self._components) - await self._components.event_dispatcher.dispatch_event(event) + event = message.MessageDeleteEvent.deserialize(payload, app=self._app) + await self._app.event_dispatcher.dispatch_event(event) @event_managers.raw_event_mapper("MESSAGE_DELETE_BULK") async def on_message_delete_bulk(self, _, payload) -> None: """Handle MESSAGE_DELETE_BULK events.""" - event = message.MessageDeleteBulkEvent.deserialize(payload, components=self._components) - await self._components.event_dispatcher.dispatch_event(event) + event = message.MessageDeleteBulkEvent.deserialize(payload, app=self._app) + await self._app.event_dispatcher.dispatch_event(event) @event_managers.raw_event_mapper("MESSAGE_REACTION_ADD") async def on_message_reaction_add(self, _, payload) -> None: """Handle MESSAGE_REACTION_ADD events.""" - event = message.MessageReactionAddEvent.deserialize(payload, components=self._components) - await self._components.event_dispatcher.dispatch_event(event) + event = message.MessageReactionAddEvent.deserialize(payload, app=self._app) + await self._app.event_dispatcher.dispatch_event(event) @event_managers.raw_event_mapper("MESSAGE_REACTION_REMOVE") async def on_message_reaction_remove(self, _, payload) -> None: """Handle MESSAGE_REACTION_REMOVE events.""" payload["emoji"].setdefault("animated", None) - event = message.MessageReactionRemoveEvent.deserialize(payload, components=self._components) - await self._components.event_dispatcher.dispatch_event(event) + event = message.MessageReactionRemoveEvent.deserialize(payload, app=self._app) + await self._app.event_dispatcher.dispatch_event(event) @event_managers.raw_event_mapper("MESSAGE_REACTION_REMOVE_EMOJI") async def on_message_reaction_remove_emoji(self, _, payload) -> None: """Handle MESSAGE_REACTION_REMOVE_EMOJI events.""" payload["emoji"].setdefault("animated", None) - event = message.MessageReactionRemoveEmojiEvent.deserialize(payload, components=self._components) - await self._components.event_dispatcher.dispatch_event(event) + event = message.MessageReactionRemoveEmojiEvent.deserialize(payload, app=self._app) + await self._app.event_dispatcher.dispatch_event(event) @event_managers.raw_event_mapper("PRESENCE_UPDATE") async def on_presence_update(self, _, payload) -> None: """Handle PRESENCE_UPDATE events.""" - event = guild.PresenceUpdateEvent.deserialize(payload, components=self._components) - await self._components.event_dispatcher.dispatch_event(event) + event = guild.PresenceUpdateEvent.deserialize(payload, app=self._app) + await self._app.event_dispatcher.dispatch_event(event) @event_managers.raw_event_mapper("TYPING_START") async def on_typing_start(self, _, payload) -> None: """Handle TYPING_START events.""" - event = channel.TypingStartEvent.deserialize(payload, components=self._components) - await self._components.event_dispatcher.dispatch_event(event) + event = channel.TypingStartEvent.deserialize(payload, app=self._app) + await self._app.event_dispatcher.dispatch_event(event) @event_managers.raw_event_mapper("USER_UPDATE") async def on_my_user_update(self, _, payload) -> None: """Handle USER_UPDATE events.""" - event = other.MyUserUpdateEvent.deserialize(payload, components=self._components) - await self._components.event_dispatcher.dispatch_event(event) + event = other.MyUserUpdateEvent.deserialize(payload, app=self._app) + await self._app.event_dispatcher.dispatch_event(event) @event_managers.raw_event_mapper("VOICE_STATE_UPDATE") async def on_voice_state_update(self, _, payload) -> None: """Handle VOICE_STATE_UPDATE events.""" - event = voice.VoiceStateUpdateEvent.deserialize(payload, components=self._components) - await self._components.event_dispatcher.dispatch_event(event) + event = voice.VoiceStateUpdateEvent.deserialize(payload, app=self._app) + await self._app.event_dispatcher.dispatch_event(event) @event_managers.raw_event_mapper("VOICE_SERVER_UPDATE") async def on_voice_server_update(self, _, payload) -> None: """Handle VOICE_SERVER_UPDATE events.""" - event = voice.VoiceServerUpdateEvent.deserialize(payload, components=self._components) - await self._components.event_dispatcher.dispatch_event(event) + event = voice.VoiceServerUpdateEvent.deserialize(payload, app=self._app) + await self._app.event_dispatcher.dispatch_event(event) @event_managers.raw_event_mapper("WEBHOOK_UPDATE") async def on_webhook_update(self, _, payload) -> None: """Handle WEBHOOK_UPDATE events.""" - event = channel.WebhookUpdateEvent.deserialize(payload, components=self._components) - await self._components.event_dispatcher.dispatch_event(event) + event = channel.WebhookUpdateEvent.deserialize(payload, app=self._app) + await self._app.event_dispatcher.dispatch_event(event) # pylint: enable=too-many-public-methods diff --git a/tests/hikari/_helpers.py b/tests/hikari/_helpers.py index fe23d8aed5..f852cc2c89 100644 --- a/tests/hikari/_helpers.py +++ b/tests/hikari/_helpers.py @@ -35,8 +35,8 @@ import mock import pytest -from hikari.models import bases from hikari.internal import marshaller +from hikari.models import bases _LOGGER = logging.getLogger(__name__) diff --git a/tests/hikari/clients/__init__.py b/tests/hikari/components/__init__.py similarity index 100% rename from tests/hikari/clients/__init__.py rename to tests/hikari/components/__init__.py diff --git a/tests/hikari/clients/test_components.py b/tests/hikari/components/test_application.py similarity index 96% rename from tests/hikari/clients/test_components.py rename to tests/hikari/components/test_application.py index b332e3e779..394390a113 100644 --- a/tests/hikari/clients/test_components.py +++ b/tests/hikari/components/test_application.py @@ -15,13 +15,13 @@ # GNU Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public License -# along ith Hikari. If not, see . +# along with Hikari. If not, see . import mock -from hikari.components import application from hikari import configs from hikari import rest from hikari.gateway import client +from hikari.components import application from hikari.components import dispatchers from hikari.components import event_managers diff --git a/tests/hikari/events/test_dispatchers.py b/tests/hikari/components/test_dispatchers.py similarity index 100% rename from tests/hikari/events/test_dispatchers.py rename to tests/hikari/components/test_dispatchers.py diff --git a/tests/hikari/events/test_intent_aware_dispatchers.py b/tests/hikari/components/test_intent_aware_dispatchers.py similarity index 100% rename from tests/hikari/events/test_intent_aware_dispatchers.py rename to tests/hikari/components/test_intent_aware_dispatchers.py diff --git a/tests/hikari/events/test_base.py b/tests/hikari/events/test_base.py index 1ecfa1cb63..951295a57d 100644 --- a/tests/hikari/events/test_base.py +++ b/tests/hikari/events/test_base.py @@ -15,10 +15,11 @@ # GNU Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public License -# along ith Hikari. If not, see . -from hikari.models import intents +# along with Hikari. If not, see . from hikari.events import base from hikari.internal import more_collections +from hikari.models import intents + # Base event, is not deserialized class TestHikariEvent: diff --git a/tests/hikari/events/test_channel.py b/tests/hikari/events/test_channel.py index 507702ac62..7099a633a2 100644 --- a/tests/hikari/events/test_channel.py +++ b/tests/hikari/events/test_channel.py @@ -15,16 +15,19 @@ # GNU Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public License -# along ith Hikari. If not, see . +# along with Hikari. If not, see . import contextlib import datetime import mock import pytest -from hikari.models import guilds, users, channels, invites from hikari.events import channel from hikari.internal import conversions +from hikari.models import channels +from hikari.models import guilds +from hikari.models import invites +from hikari.models import users from tests.hikari import _helpers diff --git a/tests/hikari/events/test_guild.py b/tests/hikari/events/test_guild.py index 305f071fbf..10f882ea2e 100644 --- a/tests/hikari/events/test_guild.py +++ b/tests/hikari/events/test_guild.py @@ -15,16 +15,19 @@ # GNU Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public License -# along ith Hikari. If not, see . +# along with Hikari. If not, see . import contextlib import datetime import mock import pytest -from hikari.models import unset, guilds, users, emojis from hikari.events import guild from hikari.internal import conversions +from hikari.models import emojis +from hikari.models import guilds +from hikari.models import unset +from hikari.models import users from tests.hikari import _helpers diff --git a/tests/hikari/events/test_message.py b/tests/hikari/events/test_message.py index f134e665a3..b348a3b86f 100644 --- a/tests/hikari/events/test_message.py +++ b/tests/hikari/events/test_message.py @@ -15,16 +15,22 @@ # GNU Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public License -# along ith Hikari. If not, see . +# along with Hikari. If not, see . import contextlib import datetime import mock import pytest -from hikari.models import unset, embeds, messages, guilds, applications, users, emojis from hikari.events import message from hikari.internal import conversions +from hikari.models import applications +from hikari.models import embeds +from hikari.models import emojis +from hikari.models import guilds +from hikari.models import messages +from hikari.models import unset +from hikari.models import users from tests.hikari import _helpers diff --git a/tests/hikari/events/test_other.py b/tests/hikari/events/test_other.py index 0d63a574c5..9bb763b15b 100644 --- a/tests/hikari/events/test_other.py +++ b/tests/hikari/events/test_other.py @@ -15,14 +15,15 @@ # GNU Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public License -# along ith Hikari. If not, see . +# along with Hikari. If not, see . import contextlib import mock import pytest -from hikari.models import guilds, users from hikari.events import other +from hikari.models import guilds +from hikari.models import users from tests.hikari import _helpers diff --git a/tests/hikari/clients/test_rest/__init__.py b/tests/hikari/gateway/__init__.py similarity index 100% rename from tests/hikari/clients/test_rest/__init__.py rename to tests/hikari/gateway/__init__.py diff --git a/tests/hikari/clients/test_shards.py b/tests/hikari/gateway/test_client.py similarity index 97% rename from tests/hikari/clients/test_shards.py rename to tests/hikari/gateway/test_client.py index 5f95dd3f7a..3cb4292182 100644 --- a/tests/hikari/clients/test_shards.py +++ b/tests/hikari/gateway/test_client.py @@ -15,7 +15,7 @@ # GNU Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public License -# along ith Hikari. If not, see . +# along with Hikari. If not, see . import asyncio import datetime @@ -24,16 +24,16 @@ import mock import pytest -import hikari.gateway.gateway_state -from hikari.models import guilds +from hikari import configs from hikari import errors from hikari.components import application -from hikari import configs -from hikari.gateway import client as high_level_shards from hikari.components import consumers -from hikari.internal import more_asyncio -from hikari.internal import codes +from hikari.gateway import client as high_level_shards from hikari.gateway import connection as low_level_shards +from hikari.gateway import gateway_state +from hikari.internal import codes +from hikari.internal import more_asyncio +from hikari.models import guilds from tests.hikari import _helpers @@ -53,7 +53,7 @@ def done(self): @pytest.fixture -def mock_components(): +def mock_app(): class ApplicationImpl(application.Application): def __init__(self): super().__init__( @@ -68,7 +68,7 @@ def __init__(self): @pytest.fixture -def shard_client_obj(mock_components): +def shard_client_obj(mock_app): mock_shard_connection = mock.MagicMock( low_level_shards.Shard, heartbeat_latency=float("nan"), @@ -77,8 +77,8 @@ def shard_client_obj(mock_components): seq=None, session_id=None, ) - with mock.patch("hikari.net.shards.Shard", return_value=mock_shard_connection): - return _helpers.unslot_class(high_level_shards.GatewayClient)(0, 1, mock_components, "some_url") + with mock.patch.object(low_level_shards, "Shard", return_value=mock_shard_connection): + return _helpers.unslot_class(high_level_shards.GatewayClient)(0, 1, mock_app, "some_url") class TestShardClientImpl: @@ -99,7 +99,7 @@ def process_raw_event(self, _client, name, payload): def test_connection_is_set(self, shard_client_obj): mock_shard_connection = mock.MagicMock(low_level_shards.Shard) - with mock.patch("hikari.net.shards.Shard", return_value=mock_shard_connection): + with mock.patch.object(low_level_shards, "Shard", return_value=mock_shard_connection): shard_client_obj = high_level_shards.GatewayClient( 0, 1, @@ -278,7 +278,7 @@ async def forever(): @_helpers.assert_raises(type_=RuntimeError) @pytest.mark.asyncio async def test_start_when_already_started(self, shard_client_obj): - shard_client_obj._shard_state = hikari.gateway.gateway_state.GatewayState.READY + shard_client_obj._shard_state = gateway_state.GatewayState.READY await shard_client_obj.start() @@ -308,7 +308,7 @@ async def test_close(self, shard_client_obj): @pytest.mark.asyncio async def test_close_when_already_stopping(self, shard_client_obj): - shard_client_obj._shard_state = hikari.gateway.gateway_state.GatewayState.STOPPING + shard_client_obj._shard_state = gateway_state.GatewayState.STOPPING await shard_client_obj.close() @@ -316,7 +316,7 @@ async def test_close_when_already_stopping(self, shard_client_obj): @pytest.mark.asyncio async def test_close_when_not_running_is_not_an_error(self, shard_client_obj): - shard_client_obj._shard_state = hikari.gateway.gateway_state.GatewayState.NOT_RUNNING + shard_client_obj._shard_state = gateway_state.GatewayState.NOT_RUNNING shard_client_obj._task = None await shard_client_obj.close() diff --git a/tests/hikari/net/test_shards.py b/tests/hikari/gateway/test_connection.py similarity index 100% rename from tests/hikari/net/test_shards.py rename to tests/hikari/gateway/test_connection.py index 4028c60ee5..f036d261bc 100644 --- a/tests/hikari/net/test_shards.py +++ b/tests/hikari/gateway/test_connection.py @@ -29,9 +29,9 @@ import pytest from hikari import errors -from hikari.internal import more_collections -from hikari.internal import codes from hikari.gateway import connection +from hikari.internal import codes +from hikari.internal import more_collections from hikari.internal import user_agents from tests.hikari import _helpers diff --git a/tests/hikari/net/test_codes.py b/tests/hikari/internal/test_codes.py similarity index 100% rename from tests/hikari/net/test_codes.py rename to tests/hikari/internal/test_codes.py diff --git a/tests/hikari/net/test_http_client.py b/tests/hikari/internal/test_http_client.py similarity index 100% rename from tests/hikari/net/test_http_client.py rename to tests/hikari/internal/test_http_client.py diff --git a/tests/hikari/internal/test_marshaller.py b/tests/hikari/internal/test_marshaller.py index f8dbfb2b0b..646f56c1c2 100644 --- a/tests/hikari/internal/test_marshaller.py +++ b/tests/hikari/internal/test_marshaller.py @@ -32,7 +32,7 @@ def test_dereference_handle_module_only(self): def test_dereference_handle_module_and_attribute(self): assert ( - marshaller.dereference_handle("hikari.net.codes#GatewayCloseCode.AUTHENTICATION_FAILED") + marshaller.dereference_handle("hikari.internal.codes#GatewayCloseCode.AUTHENTICATION_FAILED") is codes.GatewayCloseCode.AUTHENTICATION_FAILED ) diff --git a/tests/hikari/net/test_ratelimits.py b/tests/hikari/internal/test_ratelimits.py similarity index 57% rename from tests/hikari/net/test_ratelimits.py rename to tests/hikari/internal/test_ratelimits.py index 7788bdc1e7..aa5002b0d8 100644 --- a/tests/hikari/net/test_ratelimits.py +++ b/tests/hikari/internal/test_ratelimits.py @@ -18,7 +18,6 @@ # along with Hikari. If not, see . import asyncio import contextlib -import datetime import logging import math import statistics @@ -28,7 +27,6 @@ import pytest from hikari.internal import ratelimits -from hikari.rest import routes from tests.hikari import _helpers @@ -363,286 +361,6 @@ def test_is_rate_limited_when_rate_limit_not_expired_only_returns_expr(self, rem assert rl.is_rate_limited(now) is (remaining <= 0) -class TestRESTBucket: - @pytest.fixture - def template(self): - return routes.Route("GET", "/foo/bar") - - @pytest.fixture - def compiled_route(self, template): - return routes.CompiledRoute(template, "/foo/bar", "1a2b3c") - - @pytest.mark.parametrize("name", ["spaghetti", ratelimits.UNKNOWN_HASH]) - def test_is_unknown(self, name, compiled_route): - with ratelimits.RESTBucket(name, compiled_route) as rl: - assert rl.is_unknown is (name == ratelimits.UNKNOWN_HASH) - - def test_update_rate_limit(self, compiled_route): - with ratelimits.RESTBucket(__name__, compiled_route) as rl: - rl.remaining = 1 - rl.limit = 2 - rl.reset_at = 3 - rl.period = 2 - - with mock.patch("time.perf_counter", return_value=4.20): - rl.update_rate_limit(9, 18, 27) - - assert rl.remaining == 9 - assert rl.limit == 18 - assert rl.reset_at == 27 - assert rl.period == 27 - 4.20 - - @pytest.mark.parametrize("name", ["spaghetti", ratelimits.UNKNOWN_HASH]) - def test_drip(self, name, compiled_route): - with ratelimits.RESTBucket(name, compiled_route) as rl: - rl.remaining = 1 - rl.drip() - assert rl.remaining == 0 if name != ratelimits.UNKNOWN_HASH else 1 - - -class TestRESTBucketManager: - @pytest.mark.asyncio - async def test_close_closes_all_buckets(self): - class MockBucket: - def __init__(self): - self.close = mock.MagicMock() - - buckets = [MockBucket() for _ in range(30)] - - mgr = ratelimits.RESTBucketManager() - # noinspection PyFinal - mgr.real_hashes_to_buckets = {f"blah{i}": bucket for i, bucket in enumerate(buckets)} - - mgr.close() - - for i, bucket in enumerate(buckets): - bucket.close.assert_called_once(), i - - @pytest.mark.asyncio - async def test_close_sets_closed_event(self): - mgr = ratelimits.RESTBucketManager() - assert not mgr.closed_event.is_set() - mgr.close() - assert mgr.closed_event.is_set() - - @pytest.mark.asyncio - async def test_start(self): - with ratelimits.RESTBucketManager() as mgr: - assert mgr.gc_task is None - mgr.start() - mgr.start() - mgr.start() - assert mgr.gc_task is not None - - @pytest.mark.asyncio - async def test_exit_closes(self): - with mock.patch("hikari.net.ratelimits.RESTBucketManager.close") as close: - with mock.patch("hikari.net.ratelimits.RESTBucketManager.gc") as gc: - with ratelimits.RESTBucketManager() as mgr: - mgr.start(0.01, 32) - gc.assert_called_once_with(0.01, 32) - close.assert_called() - - @pytest.mark.asyncio - async def test_gc_polls_until_closed_event_set(self): - # This is shit, but it is good shit. - with ratelimits.RESTBucketManager() as mgr: - mgr.start(0.01) - assert mgr.gc_task is not None - assert not mgr.gc_task.done() - await asyncio.sleep(0.1) - assert mgr.gc_task is not None - assert not mgr.gc_task.done() - await asyncio.sleep(0.1) - mgr.closed_event.set() - assert mgr.gc_task is not None - assert not mgr.gc_task.done() - task = mgr.gc_task - await asyncio.sleep(0.1) - assert mgr.gc_task is None - assert task.done() - - @pytest.mark.asyncio - async def test_gc_calls_do_pass(self): - with _helpers.unslot_class(ratelimits.RESTBucketManager)() as mgr: - mgr.do_gc_pass = mock.MagicMock() - mgr.start(0.01, 33) - try: - await asyncio.sleep(0.1) - mgr.do_gc_pass.assert_called_with(33) - finally: - mgr.gc_task.cancel() - - @pytest.mark.asyncio - async def test_do_gc_pass_any_buckets_that_are_empty_but_still_rate_limited_are_kept_alive(self): - with _helpers.unslot_class(ratelimits.RESTBucketManager)() as mgr: - bucket = mock.MagicMock() - bucket.is_empty = True - bucket.is_unknown = False - bucket.reset_at = time.perf_counter() + 999999999999999999999999999 - - mgr.real_hashes_to_buckets["foobar"] = bucket - - mgr.do_gc_pass(0) - - assert "foobar" in mgr.real_hashes_to_buckets - bucket.close.assert_not_called() - - @pytest.mark.asyncio - async def test_do_gc_pass_any_buckets_that_are_empty_but_not_rate_limited_and_not_expired_are_kept_alive(self): - with _helpers.unslot_class(ratelimits.RESTBucketManager)() as mgr: - bucket = mock.MagicMock() - bucket.is_empty = True - bucket.is_unknown = False - bucket.reset_at = time.perf_counter() - - mgr.real_hashes_to_buckets["foobar"] = bucket - - mgr.do_gc_pass(10) - - assert "foobar" in mgr.real_hashes_to_buckets - bucket.close.assert_not_called() - - @pytest.mark.asyncio - async def test_do_gc_pass_any_buckets_that_are_empty_but_not_rate_limited_and_expired_are_closed(self): - with _helpers.unslot_class(ratelimits.RESTBucketManager)() as mgr: - bucket = mock.MagicMock() - bucket.is_empty = True - bucket.is_unknown = False - bucket.reset_at = time.perf_counter() - 999999999999999999999999999 - - mgr.real_hashes_to_buckets["foobar"] = bucket - - mgr.do_gc_pass(0) - - assert "foobar" not in mgr.real_hashes_to_buckets - bucket.close.assert_called_once() - - @pytest.mark.asyncio - async def test_do_gc_pass_any_buckets_that_are_not_empty_are_kept_alive(self): - with _helpers.unslot_class(ratelimits.RESTBucketManager)() as mgr: - bucket = mock.MagicMock() - bucket.is_empty = False - bucket.is_unknown = True - bucket.reset_at = time.perf_counter() - - mgr.real_hashes_to_buckets["foobar"] = bucket - - mgr.do_gc_pass(0) - - assert "foobar" in mgr.real_hashes_to_buckets - bucket.close.assert_not_called() - - @pytest.mark.asyncio - async def test_acquire_route_when_not_in_routes_to_real_hashes_makes_new_bucket_using_initial_hash(self): - with ratelimits.RESTBucketManager() as mgr: - route = mock.MagicMock() - route.create_real_bucket_hash = mock.MagicMock(wraps=lambda intial_hash: intial_hash + ";bobs") - - # This isn't a coroutine; why would I await it? - # noinspection PyAsyncCall - mgr.acquire(route) - - assert "UNKNOWN;bobs" in mgr.real_hashes_to_buckets - assert isinstance(mgr.real_hashes_to_buckets["UNKNOWN;bobs"], ratelimits.RESTBucket) - - @pytest.mark.asyncio - async def test_acquire_route_when_not_in_routes_to_real_hashes_caches_route(self): - with ratelimits.RESTBucketManager() as mgr: - route = mock.MagicMock() - route.create_real_bucket_hash = mock.MagicMock(wraps=lambda intial_hash: intial_hash + ";bobs") - - # This isn't a coroutine; why would I await it? - # noinspection PyAsyncCall - mgr.acquire(route) - - assert mgr.routes_to_hashes[route.route] == "UNKNOWN" - - @pytest.mark.asyncio - async def test_acquire_route_when_route_cached_already_obtains_hash_from_route_and_bucket_from_hash(self): - with ratelimits.RESTBucketManager() as mgr: - route = mock.MagicMock() - route.create_real_bucket_hash = mock.MagicMock(return_value="eat pant;1234") - bucket = mock.MagicMock() - mgr.routes_to_hashes[route] = "eat pant" - mgr.real_hashes_to_buckets["eat pant;1234"] = bucket - - # This isn't a coroutine; why would I await it? - # noinspection PyAsyncCall - mgr.acquire(route) - - # yes i test this twice, sort of. no, there isn't another way to verify this. sue me. - bucket.acquire.assert_called_once() - - @pytest.mark.asyncio - async def test_acquire_route_returns_acquired_future(self): - with ratelimits.RESTBucketManager() as mgr: - route = mock.MagicMock() - - bucket = mock.MagicMock() - with mock.patch("hikari.net.ratelimits.RESTBucket", return_value=bucket): - route.create_real_bucket_hash = mock.MagicMock(wraps=lambda intial_hash: intial_hash + ";bobs") - - f = mgr.acquire(route) - assert f is bucket.acquire() - - @pytest.mark.asyncio - async def test_acquire_route_returns_acquired_future_for_new_bucket(self): - with ratelimits.RESTBucketManager() as mgr: - route = mock.MagicMock() - route.create_real_bucket_hash = mock.MagicMock(return_value="eat pant;bobs") - bucket = mock.MagicMock() - mgr.routes_to_hashes[route.route] = "eat pant" - mgr.real_hashes_to_buckets["eat pant;bobs"] = bucket - - f = mgr.acquire(route) - assert f is bucket.acquire() - - @pytest.mark.asyncio - async def test_update_rate_limits_if_wrong_bucket_hash_reroutes_route(self): - with ratelimits.RESTBucketManager() as mgr: - route = mock.MagicMock() - route.create_real_bucket_hash = mock.MagicMock(wraps=lambda intial_hash: intial_hash + ";bobs") - mgr.routes_to_hashes[route.route] = "123" - mgr.update_rate_limits(route, "blep", 22, 23, datetime.datetime.now(), datetime.datetime.now()) - assert mgr.routes_to_hashes[route.route] == "blep" - assert isinstance(mgr.real_hashes_to_buckets["blep;bobs"], ratelimits.RESTBucket) - - @pytest.mark.asyncio - async def test_update_rate_limits_if_right_bucket_hash_does_nothing_to_hash(self): - with ratelimits.RESTBucketManager() as mgr: - route = mock.MagicMock() - route.create_real_bucket_hash = mock.MagicMock(wraps=lambda intial_hash: intial_hash + ";bobs") - mgr.routes_to_hashes[route.route] = "123" - bucket = mock.MagicMock() - mgr.real_hashes_to_buckets["123;bobs"] = bucket - mgr.update_rate_limits(route, "123", 22, 23, datetime.datetime.now(), datetime.datetime.now()) - assert mgr.routes_to_hashes[route.route] == "123" - assert mgr.real_hashes_to_buckets["123;bobs"] is bucket - - @pytest.mark.asyncio - async def test_update_rate_limits_updates_params(self): - with ratelimits.RESTBucketManager() as mgr: - route = mock.MagicMock() - route.create_real_bucket_hash = mock.MagicMock(wraps=lambda intial_hash: intial_hash + ";bobs") - mgr.routes_to_hashes[route.route] = "123" - bucket = mock.MagicMock() - mgr.real_hashes_to_buckets["123;bobs"] = bucket - date = datetime.datetime.now().replace(year=2004) - reset_at = datetime.datetime.now() - - with mock.patch("time.perf_counter", return_value=27): - expect_reset_at_monotonic = 27 + (reset_at - date).total_seconds() - mgr.update_rate_limits(route, "123", 22, 23, date, reset_at) - bucket.update_rate_limit.assert_called_once_with(22, 23, expect_reset_at_monotonic) - - @pytest.mark.parametrize(("gc_task", "is_started"), [(None, False), (object(), True)]) - def test_is_started(self, gc_task, is_started): - with ratelimits.RESTBucketManager() as mgr: - mgr.gc_task = gc_task - assert mgr.is_started is is_started - - class TestExponentialBackOff: def test_reset(self): eb = ratelimits.ExponentialBackOff() diff --git a/tests/hikari/internal/test_urls.py b/tests/hikari/internal/test_urls.py index ec6216f623..40e774cc00 100644 --- a/tests/hikari/internal/test_urls.py +++ b/tests/hikari/internal/test_urls.py @@ -15,7 +15,7 @@ # GNU Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public License -# along ith Hikari. If not, see . +# along with Hikari. If not, see . from hikari.internal import urls from tests.hikari import _helpers diff --git a/tests/hikari/net/test_user_agents.py b/tests/hikari/internal/test_user_agents.py similarity index 100% rename from tests/hikari/net/test_user_agents.py rename to tests/hikari/internal/test_user_agents.py diff --git a/tests/hikari/net/__init__.py b/tests/hikari/models/__init__.py similarity index 100% rename from tests/hikari/net/__init__.py rename to tests/hikari/models/__init__.py diff --git a/tests/hikari/test_applications.py b/tests/hikari/models/test_applications.py similarity index 91% rename from tests/hikari/test_applications.py rename to tests/hikari/models/test_applications.py index c2ced4d0af..bf3c15f32f 100644 --- a/tests/hikari/test_applications.py +++ b/tests/hikari/models/test_applications.py @@ -15,18 +15,20 @@ # GNU Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public License -# along ith Hikari. If not, see . +# along with Hikari. If not, see . import mock import pytest -from hikari.models import guilds, applications, users from hikari.components import application from hikari.internal import urls +from hikari.models import applications +from hikari.models import guilds +from hikari.models import users from tests.hikari import _helpers @pytest.fixture() -def mock_components(): +def mock_app(): return mock.MagicMock(application.Application) @@ -109,13 +111,11 @@ def application_information_payload(owner_payload, team_payload): class TestOwnConnection: - def test_deserialize(self, own_connection_payload, test_partial_integration, mock_components): + def test_deserialize(self, own_connection_payload, test_partial_integration, mock_app): mock_integration_obj = mock.MagicMock(guilds.PartialGuildIntegration) with mock.patch.object(guilds.PartialGuildIntegration, "deserialize", return_value=mock_integration_obj): - connection_obj = applications.OwnConnection.deserialize(own_connection_payload, components=mock_components) - guilds.PartialGuildIntegration.deserialize.assert_called_once_with( - test_partial_integration, components=mock_components - ) + connection_obj = applications.OwnConnection.deserialize(own_connection_payload, app=mock_app) + guilds.PartialGuildIntegration.deserialize.assert_called_once_with(test_partial_integration, app=mock_app) assert connection_obj.id == "2513849648" assert connection_obj.name == "FS" assert connection_obj.type == "twitter" @@ -128,15 +128,15 @@ def test_deserialize(self, own_connection_payload, test_partial_integration, moc class TestOwnGuild: - def test_deserialize(self, own_guild_payload, mock_components): - own_guild_obj = applications.OwnGuild.deserialize(own_guild_payload, components=mock_components) + def test_deserialize(self, own_guild_payload, mock_app): + own_guild_obj = applications.OwnGuild.deserialize(own_guild_payload, app=mock_app) assert own_guild_obj.is_owner is False assert own_guild_obj.my_permissions == 2147483647 class TestApplicationOwner: - def test_deserialize(self, owner_payload, mock_components): - owner_obj = applications.ApplicationOwner.deserialize(owner_payload, components=mock_components) + def test_deserialize(self, owner_payload, mock_app): + owner_obj = applications.ApplicationOwner.deserialize(owner_payload, app=mock_app) assert owner_obj.username == "agent 47" assert owner_obj.discriminator == "4747" assert owner_obj.id == 474747474 @@ -144,7 +144,7 @@ def test_deserialize(self, owner_payload, mock_components): assert owner_obj.avatar_hash == "hashed" @pytest.fixture() - def owner_obj(self, owner_payload, mock_components): + def owner_obj(self, owner_payload, mock_app): return applications.ApplicationOwner(username=None, discriminator=None, id=None, flags=None, avatar_hash=None) def test_is_team_user(self, owner_obj): @@ -155,13 +155,13 @@ def test_is_team_user(self, owner_obj): class TestTeamMember: - def test_deserialize(self, member_payload, team_user_payload, mock_components): + def test_deserialize(self, member_payload, team_user_payload, mock_app): mock_team_user = mock.MagicMock(users.User) with _helpers.patch_marshal_attr( applications.TeamMember, "user", deserializer=users.User.deserialize, return_value=mock_team_user ) as patched_deserializer: - member_obj = applications.TeamMember.deserialize(member_payload, components=mock_components) - patched_deserializer.assert_called_once_with(team_user_payload, components=mock_components) + member_obj = applications.TeamMember.deserialize(member_payload, app=mock_app) + patched_deserializer.assert_called_once_with(team_user_payload, app=mock_app) assert member_obj.user is mock_team_user assert member_obj.membership_state is applications.TeamMembershipState.INVITED assert member_obj.permissions == {"*"} @@ -169,11 +169,11 @@ def test_deserialize(self, member_payload, team_user_payload, mock_components): class TestTeam: - def test_deserialize(self, team_payload, member_payload, mock_components): + def test_deserialize(self, team_payload, member_payload, mock_app): mock_member = mock.MagicMock(applications.Team, user=mock.MagicMock(id=202292292)) with mock.patch.object(applications.TeamMember, "deserialize", return_value=mock_member): - team_obj = applications.Team.deserialize(team_payload, components=mock_components) - applications.TeamMember.deserialize.assert_called_once_with(member_payload, components=mock_components) + team_obj = applications.Team.deserialize(team_payload, app=mock_app) + applications.TeamMember.deserialize.assert_called_once_with(member_payload, app=mock_app) assert team_obj.members == {202292292: mock_member} assert team_obj.icon_hash == "hashtag" assert team_obj.id == 202020202 @@ -207,14 +207,12 @@ def test_icon_url(self, team_obj): class TestApplication: - def test_deserialize(self, application_information_payload, team_payload, owner_payload, mock_components): - application_obj = applications.Application.deserialize( - application_information_payload, components=mock_components - ) + def test_deserialize(self, application_information_payload, team_payload, owner_payload, mock_app): + application_obj = applications.Application.deserialize(application_information_payload, app=mock_app) assert application_obj.team == applications.Team.deserialize(team_payload) - assert application_obj.team._app is mock_components + assert application_obj.team._app is mock_app assert application_obj.owner == applications.ApplicationOwner.deserialize(owner_payload) - assert application_obj.owner._app is mock_components + assert application_obj.owner._app is mock_app assert application_obj.id == 209333111222 assert application_obj.name == "Dream Sweet in Sea Major" assert application_obj.icon_hash == "iwiwiwiwiw" diff --git a/tests/hikari/test_audit_logs.py b/tests/hikari/models/test_audit_logs.py similarity index 80% rename from tests/hikari/test_audit_logs.py rename to tests/hikari/models/test_audit_logs.py index 510694e83b..27d8e6494f 100644 --- a/tests/hikari/test_audit_logs.py +++ b/tests/hikari/models/test_audit_logs.py @@ -15,19 +15,23 @@ # GNU Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public License -# along ith Hikari. If not, see . +# along with Hikari. If not, see . import contextlib import datetime import mock import pytest -from hikari.models import audit_logs, guilds, users, webhooks, channels from hikari.components import application +from hikari.models import audit_logs +from hikari.models import channels +from hikari.models import guilds +from hikari.models import users +from hikari.models import webhooks @pytest.fixture() -def mock_components(): +def mock_app(): return mock.MagicMock(application.Application) @@ -43,20 +47,17 @@ def test__deserialize_seconds_timedelta(): assert audit_logs._deserialize_seconds_timedelta(30) == datetime.timedelta(seconds=30) -def test__deserialize_partial_roles(mock_components): +def test__deserialize_partial_roles(mock_app): test_role_payloads = [ {"id": "24", "name": "roleA", "hoisted": True}, {"id": "48", "name": "roleA", "hoisted": True}, ] mock_role_objs = [mock.MagicMock(guilds.PartialGuildRole, id=24), mock.MagicMock(guilds.PartialGuildRole, id=48)] with mock.patch.object(guilds.PartialGuildRole, "deserialize", side_effect=mock_role_objs): - result = audit_logs._deserialize_partial_roles(test_role_payloads, components=mock_components) + result = audit_logs._deserialize_partial_roles(test_role_payloads, app=mock_app) assert result == {24: mock_role_objs[0], 48: mock_role_objs[1]} guilds.PartialGuildRole.deserialize.assert_has_calls( - [ - mock.call(test_role_payloads[0], components=mock_components), - mock.call(test_role_payloads[1], components=mock_components), - ] + [mock.call(test_role_payloads[0], app=mock_app), mock.call(test_role_payloads[1], app=mock_app),] ) @@ -64,20 +65,17 @@ def test__deserialize_day_timedelta(): assert audit_logs._deserialize_day_timedelta("4") == datetime.timedelta(days=4) -def test__deserialize_overwrites(mock_components): +def test__deserialize_overwrites(mock_app): test_overwrite_payloads = [{"id": "24", "allow": 21, "deny": 0}, {"id": "48", "deny": 42, "allow": 0}] mock_overwrite_objs = [ mock.MagicMock(guilds.PartialGuildRole, id=24), mock.MagicMock(guilds.PartialGuildRole, id=48), ] with mock.patch.object(channels.PermissionOverwrite, "deserialize", side_effect=mock_overwrite_objs): - result = audit_logs._deserialize_overwrites(test_overwrite_payloads, components=mock_components) + result = audit_logs._deserialize_overwrites(test_overwrite_payloads, app=mock_app) assert result == {24: mock_overwrite_objs[0], 48: mock_overwrite_objs[1]} channels.PermissionOverwrite.deserialize.assert_has_calls( - [ - mock.call(test_overwrite_payloads[0], components=mock_components), - mock.call(test_overwrite_payloads[1], components=mock_components), - ] + [mock.call(test_overwrite_payloads[0], app=mock_app), mock.call(test_overwrite_payloads[1], app=mock_app),] ) @@ -107,85 +105,71 @@ def test_audit_log_change_payload(): class TestAuditLogChange: - def test_deserialize_with_known_component_less_converter_and_values(self, mock_components): + def test_deserialize_with_known_component_less_converter_and_values(self, mock_app): test_audit_log_change_payload = {"key": "rate_limit_per_user", "old_value": "0", "new_value": "60"} mock_role_zero = mock.MagicMock(guilds.PartialGuildRole, id=123123123312312) mock_role_one = mock.MagicMock(guilds.PartialGuildRole, id=568651298858074123) with mock.patch.object(guilds.PartialGuildRole, "deserialize", side_effect=[mock_role_zero, mock_role_one]): - audit_log_change_obj = audit_logs.AuditLogChange.deserialize( - test_audit_log_change_payload, components=mock_components - ) - assert audit_log_change_obj._components is mock_components + audit_log_change_obj = audit_logs.AuditLogChange.deserialize(test_audit_log_change_payload, app=mock_app) + assert audit_log_change_obj._app is mock_app assert audit_log_change_obj.key is audit_logs.AuditLogChangeKey.RATE_LIMIT_PER_USER assert audit_log_change_obj.old_value == datetime.timedelta(seconds=0) assert audit_log_change_obj.new_value == datetime.timedelta(seconds=60) - def test_deserialize_with_known_component_full_converter_and_values( - self, test_audit_log_change_payload, mock_components - ): + def test_deserialize_with_known_component_full_converter_and_values(self, test_audit_log_change_payload, mock_app): mock_role_zero = mock.MagicMock(guilds.PartialGuildRole, id=123123123312312) mock_role_one = mock.MagicMock(guilds.PartialGuildRole, id=568651298858074123) with mock.patch.object(guilds.PartialGuildRole, "deserialize", side_effect=[mock_role_zero, mock_role_one]): - audit_log_change_obj = audit_logs.AuditLogChange.deserialize( - test_audit_log_change_payload, components=mock_components - ) + audit_log_change_obj = audit_logs.AuditLogChange.deserialize(test_audit_log_change_payload, app=mock_app) guilds.PartialGuildRole.deserialize.assert_has_calls( [ - mock.call({"id": "123123123312312", "name": "aRole"}, components=mock_components), - mock.call({"id": "568651298858074123", "name": "Casual"}, components=mock_components), + mock.call({"id": "123123123312312", "name": "aRole"}, app=mock_app), + mock.call({"id": "568651298858074123", "name": "Casual"}, app=mock_app), ] ) - assert audit_log_change_obj._components is mock_components + assert audit_log_change_obj._app is mock_app assert audit_log_change_obj.key is audit_logs.AuditLogChangeKey.ADD_ROLE_TO_MEMBER assert audit_log_change_obj.old_value == {568651298858074123: mock_role_one} assert audit_log_change_obj.new_value == {123123123312312: mock_role_zero} def test_deserialize_with_known_component_full_converter_and_no_values( - self, test_audit_log_change_payload, mock_components + self, test_audit_log_change_payload, mock_app ): test_audit_log_change_payload = {"key": "$add"} with mock.patch.object(guilds.PartialGuildRole, "deserialize"): - audit_log_change_obj = audit_logs.AuditLogChange.deserialize( - test_audit_log_change_payload, components=mock_components - ) + audit_log_change_obj = audit_logs.AuditLogChange.deserialize(test_audit_log_change_payload, app=mock_app) guilds.PartialGuildRole.deserialize.assert_not_called() - assert audit_log_change_obj._components is mock_components + assert audit_log_change_obj._app is mock_app assert audit_log_change_obj.key is audit_logs.AuditLogChangeKey.ADD_ROLE_TO_MEMBER assert audit_log_change_obj.old_value is None assert audit_log_change_obj.new_value is None def test_deserialize_with_known_component_less_converter_and_no_values( - self, test_audit_log_change_payload, mock_components + self, test_audit_log_change_payload, mock_app ): test_audit_log_change_payload = {"key": "rate_limit_per_user"} with mock.patch.object(guilds.PartialGuildRole, "deserialize"): - audit_log_change_obj = audit_logs.AuditLogChange.deserialize( - test_audit_log_change_payload, components=mock_components - ) + audit_log_change_obj = audit_logs.AuditLogChange.deserialize(test_audit_log_change_payload, app=mock_app) guilds.PartialGuildRole.deserialize.assert_not_called() - assert audit_log_change_obj._components is mock_components + assert audit_log_change_obj._app is mock_app assert audit_log_change_obj.key is audit_logs.AuditLogChangeKey.RATE_LIMIT_PER_USER assert audit_log_change_obj.old_value is None assert audit_log_change_obj.new_value is None - def test_deserialize_with_unknown_converter_and_values(self, test_audit_log_change_payload, mock_components): + def test_deserialize_with_unknown_converter_and_values(self, test_audit_log_change_payload, mock_app): test_audit_log_change_payload["key"] = "aUnknownKey" - audit_log_change_obj = audit_logs.AuditLogChange.deserialize( - test_audit_log_change_payload, components=mock_components - ) - assert audit_log_change_obj._components is mock_components + audit_log_change_obj = audit_logs.AuditLogChange.deserialize(test_audit_log_change_payload, app=mock_app) + assert audit_log_change_obj._app is mock_app assert audit_log_change_obj.key == "aUnknownKey" assert audit_log_change_obj.old_value == test_audit_log_change_payload["old_value"] assert audit_log_change_obj.new_value == test_audit_log_change_payload["new_value"] - def test_deserialize_with_unknown_converter_and_no_values(self, test_audit_log_change_payload, mock_components): + def test_deserialize_with_unknown_converter_and_no_values(self, test_audit_log_change_payload, mock_app): test_audit_log_change_payload["key"] = "aUnknownKey" del test_audit_log_change_payload["old_value"] del test_audit_log_change_payload["new_value"] - audit_log_change_obj = audit_logs.AuditLogChange.deserialize( - test_audit_log_change_payload, components=mock_components - ) - assert audit_log_change_obj._components is mock_components + audit_log_change_obj = audit_logs.AuditLogChange.deserialize(test_audit_log_change_payload, app=mock_app) + assert audit_log_change_obj._app is mock_app assert audit_log_change_obj.key == "aUnknownKey" assert audit_log_change_obj.old_value is None assert audit_log_change_obj.new_value is None @@ -196,9 +180,9 @@ class TestChannelOverwriteEntryInfo: def test_overwrite_info_payload(self): return {"id": "123123123", "type": "role", "role_name": "aRole"} - def test_deserialize(self, test_overwrite_info_payload, mock_components): + def test_deserialize(self, test_overwrite_info_payload, mock_app): overwrite_entry_info = audit_logs.ChannelOverwriteEntryInfo.deserialize( - test_overwrite_info_payload, components=mock_components + test_overwrite_info_payload, app=mock_app ) assert overwrite_entry_info.id == 123123123 assert overwrite_entry_info.type is channels.PermissionOverwriteType.ROLE @@ -213,10 +197,8 @@ def test_message_pin_info_payload(self): "message_id": "69696969", } - def test_deserialize(self, test_message_pin_info_payload, mock_components): - message_pin_info_obj = audit_logs.MessagePinEntryInfo.deserialize( - test_message_pin_info_payload, components=mock_components - ) + def test_deserialize(self, test_message_pin_info_payload, mock_app): + message_pin_info_obj = audit_logs.MessagePinEntryInfo.deserialize(test_message_pin_info_payload, app=mock_app) assert message_pin_info_obj.channel_id == 123123123 assert message_pin_info_obj.message_id == 69696969 @@ -229,9 +211,9 @@ def test_member_prune_info_payload(self): "members_removed": "1", } - def test_deserialize(self, test_member_prune_info_payload, mock_components): + def test_deserialize(self, test_member_prune_info_payload, mock_app): member_prune_info_obj = audit_logs.MemberPruneEntryInfo.deserialize( - test_member_prune_info_payload, components=mock_components + test_member_prune_info_payload, app=mock_app ) assert member_prune_info_obj.delete_member_days == datetime.timedelta(days=7) assert member_prune_info_obj.members_removed == 1 @@ -242,9 +224,9 @@ class TestMessageDeleteEntryInfo: def test_message_delete_info_payload(self): return {"count": "42", "channel_id": "4206942069"} - def test_deserialize(self, test_message_delete_info_payload, mock_components): + def test_deserialize(self, test_message_delete_info_payload, mock_app): message_delete_entry_info = audit_logs.MessageDeleteEntryInfo.deserialize( - test_message_delete_info_payload, components=mock_components + test_message_delete_info_payload, app=mock_app ) assert message_delete_entry_info.channel_id == 4206942069 @@ -254,9 +236,9 @@ class TestMessageBulkDeleteEntryInfo: def test_message_bulk_delete_info_payload(self): return {"count": "42"} - def test_deserialize(self, test_message_bulk_delete_info_payload, mock_components): + def test_deserialize(self, test_message_bulk_delete_info_payload, mock_app): message_bulk_delete_entry_info = audit_logs.MessageBulkDeleteEntryInfo.deserialize( - test_message_bulk_delete_info_payload, components=mock_components + test_message_bulk_delete_info_payload, app=mock_app ) assert message_bulk_delete_entry_info.count == 42 @@ -266,9 +248,9 @@ class TestMemberDisconnectEntryInfo: def test_member_disconnect_info_payload(self): return {"count": "42"} - def test_deserialize(self, test_member_disconnect_info_payload, mock_components): + def test_deserialize(self, test_member_disconnect_info_payload, mock_app): member_disconnect_entry_info = audit_logs.MemberDisconnectEntryInfo.deserialize( - test_member_disconnect_info_payload, components=mock_components + test_member_disconnect_info_payload, app=mock_app ) assert member_disconnect_entry_info.count == 42 @@ -278,10 +260,8 @@ class TestMemberMoveEntryInfo: def test_member_move_info_payload(self): return {"count": "42", "channel_id": "22222222"} - def test_deserialize(self, test_member_move_info_payload, mock_components): - member_move_entry_info = audit_logs.MemberMoveEntryInfo.deserialize( - test_member_move_info_payload, components=mock_components - ) + def test_deserialize(self, test_member_move_info_payload, mock_app): + member_move_entry_info = audit_logs.MemberMoveEntryInfo.deserialize(test_member_move_info_payload, app=mock_app) assert member_move_entry_info.channel_id == 22222222 @@ -290,9 +270,9 @@ class TestUnrecognisedAuditLogEntryInfo: def test_unrecognised_audit_log_entry(self): return {"count": "5412", "action": "nyaa'd"} - def test_deserialize(self, test_unrecognised_audit_log_entry, mock_components): + def test_deserialize(self, test_unrecognised_audit_log_entry, mock_app): unrecognised_info_obj = audit_logs.UnrecognisedAuditLogEntryInfo.deserialize( - test_unrecognised_audit_log_entry, components=mock_components + test_unrecognised_audit_log_entry, app=mock_app ) assert unrecognised_info_obj.count == "5412" assert unrecognised_info_obj.action == "nyaa'd" @@ -340,21 +320,15 @@ def test_audit_log_entry_payload(test_audit_log_change_payload, test_audit_log_o class TestAuditLogEntry: def test_deserialize_with_options_and_target_id_and_known_type( - self, - test_audit_log_entry_payload, - test_audit_log_option_payload, - test_audit_log_change_payload, - mock_components, + self, test_audit_log_entry_payload, test_audit_log_option_payload, test_audit_log_change_payload, mock_app, ): - audit_log_entry_obj = audit_logs.AuditLogEntry.deserialize( - test_audit_log_entry_payload, components=mock_components - ) - assert audit_log_entry_obj._components is mock_components + audit_log_entry_obj = audit_logs.AuditLogEntry.deserialize(test_audit_log_entry_payload, app=mock_app) + assert audit_log_entry_obj._app is mock_app assert audit_log_entry_obj.changes == [audit_logs.AuditLogChange.deserialize(test_audit_log_change_payload)] assert audit_log_entry_obj.options == audit_logs.ChannelOverwriteEntryInfo.deserialize( test_audit_log_option_payload ) - assert audit_log_entry_obj.options._components is mock_components + assert audit_log_entry_obj.options._app is mock_app assert audit_log_entry_obj.target_id == 115590097100865541 assert audit_log_entry_obj.user_id == 560984860634644482 assert audit_log_entry_obj.id == 694026906592477214 @@ -362,13 +336,11 @@ def test_deserialize_with_options_and_target_id_and_known_type( assert audit_log_entry_obj.reason == "An artificial insanity." def test_deserialize_with_known_type_without_options_or_target_( - self, test_audit_log_entry_payload, test_audit_log_change_payload, mock_components + self, test_audit_log_entry_payload, test_audit_log_change_payload, mock_app ): del test_audit_log_entry_payload["options"] del test_audit_log_entry_payload["target_id"] - audit_log_entry_obj = audit_logs.AuditLogEntry.deserialize( - test_audit_log_entry_payload, components=mock_components - ) + audit_log_entry_obj = audit_logs.AuditLogEntry.deserialize(test_audit_log_entry_payload, app=mock_app) assert audit_log_entry_obj.changes == [audit_logs.AuditLogChange.deserialize(test_audit_log_change_payload)] assert audit_log_entry_obj.options is None assert audit_log_entry_obj.target_id is None @@ -378,16 +350,10 @@ def test_deserialize_with_known_type_without_options_or_target_( assert audit_log_entry_obj.reason == "An artificial insanity." def test_deserialize_with_options_and_target_id_and_unknown_type( - self, - test_audit_log_entry_payload, - test_audit_log_option_payload, - test_audit_log_change_payload, - mock_components, + self, test_audit_log_entry_payload, test_audit_log_option_payload, test_audit_log_change_payload, mock_app, ): test_audit_log_entry_payload["action_type"] = 123123123 - audit_log_entry_obj = audit_logs.AuditLogEntry.deserialize( - test_audit_log_entry_payload, components=mock_components - ) + audit_log_entry_obj = audit_logs.AuditLogEntry.deserialize(test_audit_log_entry_payload, app=mock_app) assert audit_log_entry_obj.changes == [audit_logs.AuditLogChange.deserialize(test_audit_log_change_payload)] assert audit_log_entry_obj.options == audit_logs.UnrecognisedAuditLogEntryInfo.deserialize( test_audit_log_option_payload @@ -399,18 +365,12 @@ def test_deserialize_with_options_and_target_id_and_unknown_type( assert audit_log_entry_obj.reason == "An artificial insanity." def test_deserialize_without_options_or_target_id_and_unknown_type( - self, - test_audit_log_entry_payload, - test_audit_log_option_payload, - test_audit_log_change_payload, - mock_components, + self, test_audit_log_entry_payload, test_audit_log_option_payload, test_audit_log_change_payload, mock_app, ): del test_audit_log_entry_payload["options"] del test_audit_log_entry_payload["target_id"] test_audit_log_entry_payload["action_type"] = 123123123 - audit_log_entry_obj = audit_logs.AuditLogEntry.deserialize( - test_audit_log_entry_payload, components=mock_components - ) + audit_log_entry_obj = audit_logs.AuditLogEntry.deserialize(test_audit_log_entry_payload, app=mock_app) assert audit_log_entry_obj.changes == [audit_logs.AuditLogChange.deserialize(test_audit_log_change_payload)] assert audit_log_entry_obj.options is None assert audit_log_entry_obj.target_id is None @@ -451,7 +411,7 @@ def test_deserialize( test_integration_payload, test_user_payload, test_webhook_payload, - mock_components, + mock_app, ): mock_webhook_obj = mock.MagicMock(webhooks.Webhook, id=424242) mock_user_obj = mock.MagicMock(users.User, id=92929292) @@ -465,16 +425,14 @@ def test_deserialize( ) with stack: - audit_log_obj = audit_logs.AuditLog.deserialize(test_audit_log_payload, components=mock_components) - webhooks.Webhook.deserialize.assert_called_once_with(test_webhook_payload, components=mock_components) - users.User.deserialize.assert_called_once_with(test_user_payload, components=mock_components) - guilds.PartialGuildIntegration.deserialize.assert_called_once_with( - test_integration_payload, components=mock_components - ) + audit_log_obj = audit_logs.AuditLog.deserialize(test_audit_log_payload, app=mock_app) + webhooks.Webhook.deserialize.assert_called_once_with(test_webhook_payload, app=mock_app) + users.User.deserialize.assert_called_once_with(test_user_payload, app=mock_app) + guilds.PartialGuildIntegration.deserialize.assert_called_once_with(test_integration_payload, app=mock_app) assert audit_log_obj.entries == { 694026906592477214: audit_logs.AuditLogEntry.deserialize(test_audit_log_entry_payload) } - assert audit_log_obj.entries[694026906592477214]._app is mock_components + assert audit_log_obj.entries[694026906592477214]._app is mock_app assert audit_log_obj.webhooks == {424242: mock_webhook_obj} assert audit_log_obj.users == {92929292: mock_user_obj} assert audit_log_obj.integrations == {33590653072239123: mock_integration_obj} @@ -482,7 +440,7 @@ def test_deserialize( class TestAuditLogIterator: @pytest.mark.asyncio - async def test__fill_when_entities_returned(self, mock_components): + async def test__fill_when_entities_returned(self, mock_app): mock_webhook_payload = {"id": "292393993", "channel_id": "43242"} mock_webhook_obj = mock.MagicMock(webhooks.Webhook, id=292393993) mock_user_payload = {"id": "929292", "public_flags": "22222"} @@ -498,9 +456,7 @@ async def test__fill_when_entities_returned(self, mock_components): "integrations": [mock_integration_payload], } ) - audit_log_iterator = audit_logs.AuditLogIterator( - app=mock_components, request=mock_request, before="123", limit=None, - ) + audit_log_iterator = audit_logs.AuditLogIterator(app=mock_app, request=mock_request, before="123", limit=None,) stack = contextlib.ExitStack() stack.enter_context(mock.patch.object(users.User, "deserialize", return_value=mock_user_obj)) stack.enter_context(mock.patch.object(webhooks.Webhook, "deserialize", return_value=mock_webhook_obj)) @@ -510,27 +466,25 @@ async def test__fill_when_entities_returned(self, mock_components): with stack: assert await audit_log_iterator._fill() is None - users.User.deserialize.assert_called_once_with(mock_user_payload, components=mock_components) - webhooks.Webhook.deserialize.assert_called_once_with(mock_webhook_payload, components=mock_components) - guilds.PartialGuildIntegration.deserialize.assert_called_once_with( - mock_integration_payload, components=mock_components - ) + users.User.deserialize.assert_called_once_with(mock_user_payload, app=mock_app) + webhooks.Webhook.deserialize.assert_called_once_with(mock_webhook_payload, app=mock_app) + guilds.PartialGuildIntegration.deserialize.assert_called_once_with(mock_integration_payload, app=mock_app) assert audit_log_iterator.webhooks == {292393993: mock_webhook_obj} assert audit_log_iterator.users == {929292: mock_user_obj} assert audit_log_iterator.integrations == {123123123: mock_integration_obj} assert audit_log_iterator._buffer == [mock_audit_log_entry_payload] - assert audit_log_iterator._app is mock_components + assert audit_log_iterator._app is mock_app mock_request.assert_called_once_with( before="123", limit=100, ) @pytest.mark.asyncio - async def test__fill_when_resource_exhausted(self, mock_components): + async def test__fill_when_resource_exhausted(self, mock_app): mock_request = mock.AsyncMock( return_value={"webhooks": [], "users": [], "audit_log_entries": [], "integrations": []} ) audit_log_iterator = audit_logs.AuditLogIterator( - app=mock_components, request=mock_request, before="222222222", limit=None, + app=mock_app, request=mock_request, before="222222222", limit=None, ) stack = contextlib.ExitStack() stack.enter_context(mock.patch.object(users.User, "deserialize", return_value=...)) @@ -551,7 +505,7 @@ async def test__fill_when_resource_exhausted(self, mock_components): ) @pytest.mark.asyncio - async def test__fill_when_before_and_limit_not_set(self, mock_components): + async def test__fill_when_before_and_limit_not_set(self, mock_app): mock_request = mock.AsyncMock( return_value={ "webhooks": [], @@ -560,9 +514,7 @@ async def test__fill_when_before_and_limit_not_set(self, mock_components): "integrations": [], } ) - audit_log_iterator = audit_logs.AuditLogIterator( - app=mock_components, request=mock_request, before="123", limit=None, - ) + audit_log_iterator = audit_logs.AuditLogIterator(app=mock_app, request=mock_request, before="123", limit=None,) stack = contextlib.ExitStack() stack.enter_context(mock.patch.object(users.User, "deserialize", return_value=...)) stack.enter_context(mock.patch.object(webhooks.Webhook, "deserialize", return_value=...)) @@ -576,7 +528,7 @@ async def test__fill_when_before_and_limit_not_set(self, mock_components): assert audit_log_iterator._limit is None @pytest.mark.asyncio - async def test__fill_when_before_and_limit_set(self, mock_components): + async def test__fill_when_before_and_limit_set(self, mock_app): mock_request = mock.AsyncMock( return_value={ "webhooks": [], @@ -586,7 +538,7 @@ async def test__fill_when_before_and_limit_set(self, mock_components): } ) audit_log_iterator = audit_logs.AuditLogIterator( - app=mock_components, request=mock_request, before="222222222", limit=44, + app=mock_app, request=mock_request, before="222222222", limit=44, ) stack = contextlib.ExitStack() stack.enter_context(mock.patch.object(users.User, "deserialize", return_value=...)) @@ -601,11 +553,11 @@ async def test__fill_when_before_and_limit_set(self, mock_components): assert audit_log_iterator._limit == 42 @pytest.mark.asyncio - async def test___anext___when_not_filled_and_resource_is_exhausted(self, mock_components): + async def test___anext___when_not_filled_and_resource_is_exhausted(self, mock_app): mock_request = mock.AsyncMock( return_value={"webhooks": [], "users": [], "audit_log_entries": [], "integrations": []} ) - iterator = audit_logs.AuditLogIterator(app=mock_components, request=mock_request, before="123", limit=None) + iterator = audit_logs.AuditLogIterator(app=mock_app, request=mock_request, before="123", limit=None) with mock.patch.object(audit_logs.AuditLogEntry, "deserialize", return_value=...): async for _ in iterator: assert False, "Iterator shouldn't have yielded anything." @@ -613,29 +565,29 @@ async def test___anext___when_not_filled_and_resource_is_exhausted(self, mock_co assert iterator._front == "123" @pytest.mark.asyncio - async def test___anext___when_not_filled(self, mock_components): + async def test___anext___when_not_filled(self, mock_app): mock_request = mock.AsyncMock( side_effect=[{"webhooks": [], "users": [], "audit_log_entries": [{"id": "666666"}], "integrations": []}] ) mock_audit_log_entry = mock.MagicMock(audit_logs.AuditLogEntry, id=666666) - iterator = audit_logs.AuditLogIterator(app=mock_components, request=mock_request, before="123", limit=None) + iterator = audit_logs.AuditLogIterator(app=mock_app, request=mock_request, before="123", limit=None) with mock.patch.object(audit_logs.AuditLogEntry, "deserialize", side_effect=[mock_audit_log_entry]): async for result in iterator: assert result is mock_audit_log_entry break - audit_logs.AuditLogEntry.deserialize.assert_called_once_with({"id": "666666"}, components=mock_components) + audit_logs.AuditLogEntry.deserialize.assert_called_once_with({"id": "666666"}, app=mock_app) mock_request.assert_called_once_with( before="123", limit=100, ) assert iterator._front == "666666" @pytest.mark.asyncio - async def test___anext___when_not_filled_and_limit_exhausted(self, mock_components): + async def test___anext___when_not_filled_and_limit_exhausted(self, mock_app): mock_request = mock.AsyncMock( side_effect=[{"webhooks": [], "users": [], "audit_log_entries": [], "integrations": []}] ) mock_audit_log_entry = mock.MagicMock(audit_logs.AuditLogEntry, id=666666) - iterator = audit_logs.AuditLogIterator(app=mock_components, request=mock_request, before="123", limit=None) + iterator = audit_logs.AuditLogIterator(app=mock_app, request=mock_request, before="123", limit=None) with mock.patch.object(audit_logs.AuditLogEntry, "deserialize", side_effect=[mock_audit_log_entry]): async for _ in iterator: assert False, "Iterator shouldn't have yielded anything." @@ -646,15 +598,15 @@ async def test___anext___when_not_filled_and_limit_exhausted(self, mock_componen assert iterator._front == "123" @pytest.mark.asyncio - async def test___anext___when_filled(self, mock_components): + async def test___anext___when_filled(self, mock_app): mock_request = mock.AsyncMock(side_effect=[]) mock_audit_log_entry = mock.MagicMock(audit_logs.AuditLogEntry, id=4242) - iterator = audit_logs.AuditLogIterator(app=mock_components, request=mock_request, before="123",) + iterator = audit_logs.AuditLogIterator(app=mock_app, request=mock_request, before="123",) iterator._buffer = [{"id": "123123"}] with mock.patch.object(audit_logs.AuditLogEntry, "deserialize", side_effect=[mock_audit_log_entry]): async for result in iterator: assert result is mock_audit_log_entry break - audit_logs.AuditLogEntry.deserialize.assert_called_once_with({"id": "123123"}, components=mock_components) + audit_logs.AuditLogEntry.deserialize.assert_called_once_with({"id": "123123"}, app=mock_app) mock_request.assert_not_called() assert iterator._front == "4242" diff --git a/tests/hikari/test_bases.py b/tests/hikari/models/test_bases.py similarity index 91% rename from tests/hikari/test_bases.py rename to tests/hikari/models/test_bases.py index 98a89b997a..4ed5a66741 100644 --- a/tests/hikari/test_bases.py +++ b/tests/hikari/models/test_bases.py @@ -15,7 +15,7 @@ # GNU Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public License -# along ith Hikari. If not, see . +# along with Hikari. If not, see . import datetime import typing @@ -23,9 +23,9 @@ import mock import pytest -from hikari.models import bases from hikari.components import application from hikari.internal import marshaller +from hikari.models import bases class TestHikariEntity: @@ -39,13 +39,13 @@ class StubEntity(bases.Entity, marshaller.Deserializable, marshaller.Serializabl return StubEntity def test_deserialize(self, stub_entity): - mock_components = mock.MagicMock(application.Application) - entity = stub_entity.deserialize({}, components=mock_components) - assert entity._app is mock_components + mock_app = mock.MagicMock(application.Application) + entity = stub_entity.deserialize({}, app=mock_app) + assert entity._app is mock_app def test_serialize(self, stub_entity): - mock_components = mock.MagicMock(application.Application) - assert stub_entity(components=mock_components).serialize() == {} + mock_app = mock.MagicMock(application.Application) + assert stub_entity(app=mock_app).serialize() == {} class TestSnowflake: diff --git a/tests/hikari/test_channels.py b/tests/hikari/models/test_channels.py similarity index 91% rename from tests/hikari/test_channels.py rename to tests/hikari/models/test_channels.py index 07d5de5309..373f54ffad 100644 --- a/tests/hikari/test_channels.py +++ b/tests/hikari/models/test_channels.py @@ -15,13 +15,16 @@ # GNU Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public License -# along ith Hikari. If not, see . +# along with Hikari. If not, see . import datetime import mock import pytest -from hikari.models import permissions, users, channels, bases +from hikari.models import bases +from hikari.models import channels +from hikari.models import permissions +from hikari.models import users from hikari.components import application from hikari.internal import urls @@ -138,7 +141,7 @@ def test_guild_voice_channel_payload(test_permission_overwrite_payload): @pytest.fixture() -def mock_components(): +def mock_app(): return mock.MagicMock(application.Application) @@ -147,10 +150,8 @@ class TestPartialChannel: def test_partial_channel_payload(self): return {"id": "561884984214814750", "name": "general", "type": 0} - def test_deserialize(self, test_partial_channel_payload, mock_components): - partial_channel_obj = channels.PartialChannel.deserialize( - test_partial_channel_payload, components=mock_components - ) + def test_deserialize(self, test_partial_channel_payload, mock_app): + partial_channel_obj = channels.PartialChannel.deserialize(test_partial_channel_payload, app=mock_app) assert partial_channel_obj.id == 561884984214814750 assert partial_channel_obj.name == "general" assert partial_channel_obj.type is channels.ChannelType.GUILD_TEXT @@ -162,9 +163,9 @@ def test___int__(self): class TestPermissionOverwrite: - def test_deserialize(self, test_permission_overwrite_payload, mock_components): + def test_deserialize(self, test_permission_overwrite_payload, mock_app): permission_overwrite_obj = channels.PermissionOverwrite.deserialize( - test_permission_overwrite_payload, components=mock_components + test_permission_overwrite_payload, app=mock_app ) assert ( permission_overwrite_obj.allow @@ -196,12 +197,12 @@ def test_unset(self): class TestDMChannel: - def test_deserialize(self, test_dm_channel_payload, test_recipient_payload, mock_components): + def test_deserialize(self, test_dm_channel_payload, test_recipient_payload, mock_app): mock_user = mock.MagicMock(users.User, id=987) with mock.patch.object(users.User, "deserialize", return_value=mock_user) as patched_user_deserialize: - channel_obj = channels.DMChannel.deserialize(test_dm_channel_payload, components=mock_components) - patched_user_deserialize.assert_called_once_with(test_recipient_payload, components=mock_components) + channel_obj = channels.DMChannel.deserialize(test_dm_channel_payload, app=mock_app) + patched_user_deserialize.assert_called_once_with(test_recipient_payload, app=mock_app) assert channel_obj.id == 123 assert channel_obj.last_message_id == 456 @@ -210,12 +211,12 @@ def test_deserialize(self, test_dm_channel_payload, test_recipient_payload, mock class TestGroupDMChannel: - def test_deserialize(self, test_group_dm_channel_payload, test_recipient_payload, mock_components): + def test_deserialize(self, test_group_dm_channel_payload, test_recipient_payload, mock_app): mock_user = mock.MagicMock(users.User, id=987) with mock.patch.object(users.User, "deserialize", return_value=mock_user) as patched_user_deserialize: - channel_obj = channels.GroupDMChannel.deserialize(test_group_dm_channel_payload, components=mock_components) - patched_user_deserialize.assert_called_once_with(test_recipient_payload, components=mock_components) + channel_obj = channels.GroupDMChannel.deserialize(test_group_dm_channel_payload, app=mock_app) + patched_user_deserialize.assert_called_once_with(test_recipient_payload, app=mock_app) assert channel_obj.id == 123 assert channel_obj.last_message_id == 456 @@ -264,14 +265,14 @@ def test_format_icon_url_returns_none(self, group_dm_obj): class TestGuildCategory: - def test_deserialize(self, test_guild_category_payload, test_permission_overwrite_payload, mock_components): - channel_obj = channels.GuildCategory.deserialize(test_guild_category_payload, components=mock_components) + def test_deserialize(self, test_guild_category_payload, test_permission_overwrite_payload, mock_app): + channel_obj = channels.GuildCategory.deserialize(test_guild_category_payload, app=mock_app) assert channel_obj.id == 123 assert channel_obj.permission_overwrites == { 4242: channels.PermissionOverwrite.deserialize(test_permission_overwrite_payload) } - assert channel_obj.permission_overwrites[4242]._app is mock_components + assert channel_obj.permission_overwrites[4242]._app is mock_app assert channel_obj.guild_id == 9876 assert channel_obj.position == 3 assert channel_obj.name == "Test" @@ -281,14 +282,14 @@ def test_deserialize(self, test_guild_category_payload, test_permission_overwrit class TestGuildTextChannel: - def test_deserialize(self, test_guild_text_channel_payload, test_permission_overwrite_payload, mock_components): - channel_obj = channels.GuildTextChannel.deserialize(test_guild_text_channel_payload, components=mock_components) + def test_deserialize(self, test_guild_text_channel_payload, test_permission_overwrite_payload, mock_app): + channel_obj = channels.GuildTextChannel.deserialize(test_guild_text_channel_payload, app=mock_app) assert channel_obj.id == 123 assert channel_obj.permission_overwrites == { 4242: channels.PermissionOverwrite.deserialize(test_permission_overwrite_payload) } - assert channel_obj.permission_overwrites[4242]._app is mock_components + assert channel_obj.permission_overwrites[4242]._app is mock_app assert channel_obj.guild_id == 567 assert channel_obj.position == 6 assert channel_obj.name == "general" @@ -301,14 +302,14 @@ def test_deserialize(self, test_guild_text_channel_payload, test_permission_over class TestGuildNewsChannel: - def test_deserialize(self, test_guild_news_channel_payload, test_permission_overwrite_payload, mock_components): - channel_obj = channels.GuildNewsChannel.deserialize(test_guild_news_channel_payload, components=mock_components) + def test_deserialize(self, test_guild_news_channel_payload, test_permission_overwrite_payload, mock_app): + channel_obj = channels.GuildNewsChannel.deserialize(test_guild_news_channel_payload, app=mock_app) assert channel_obj.id == 567 assert channel_obj.permission_overwrites == { 4242: channels.PermissionOverwrite.deserialize(test_permission_overwrite_payload) } - assert channel_obj.permission_overwrites[4242]._app is mock_components + assert channel_obj.permission_overwrites[4242]._app is mock_app assert channel_obj.guild_id == 123 assert channel_obj.position == 0 assert channel_obj.name == "Important Announcements" @@ -320,16 +321,14 @@ def test_deserialize(self, test_guild_news_channel_payload, test_permission_over class TestGuildStoreChannel: - def test_deserialize(self, test_guild_store_channel_payload, test_permission_overwrite_payload, mock_components): - channel_obj = channels.GuildStoreChannel.deserialize( - test_guild_store_channel_payload, components=mock_components - ) + def test_deserialize(self, test_guild_store_channel_payload, test_permission_overwrite_payload, mock_app): + channel_obj = channels.GuildStoreChannel.deserialize(test_guild_store_channel_payload, app=mock_app) assert channel_obj.id == 123 assert channel_obj.permission_overwrites == { 4242: channels.PermissionOverwrite.deserialize(test_permission_overwrite_payload) } - assert channel_obj.permission_overwrites[4242]._app is mock_components + assert channel_obj.permission_overwrites[4242]._app is mock_app assert channel_obj.guild_id == 1234 assert channel_obj.position == 2 assert channel_obj.name == "Half Life 3" @@ -339,16 +338,14 @@ def test_deserialize(self, test_guild_store_channel_payload, test_permission_ove class TestGuildVoiceChannell: - def test_deserialize(self, test_guild_voice_channel_payload, test_permission_overwrite_payload, mock_components): - channel_obj = channels.GuildVoiceChannel.deserialize( - test_guild_voice_channel_payload, components=mock_components - ) + def test_deserialize(self, test_guild_voice_channel_payload, test_permission_overwrite_payload, mock_app): + channel_obj = channels.GuildVoiceChannel.deserialize(test_guild_voice_channel_payload, app=mock_app) assert channel_obj.id == 123 assert channel_obj.permission_overwrites == { 4242: channels.PermissionOverwrite.deserialize(test_permission_overwrite_payload) } - assert channel_obj.permission_overwrites[4242]._app is mock_components + assert channel_obj.permission_overwrites[4242]._app is mock_app assert channel_obj.guild_id == 789 assert channel_obj.position == 4 assert channel_obj.name == "Secret Developer Discussions" @@ -448,6 +445,6 @@ def test_deserialize_channel_returns_correct_type( assert isinstance(channels.deserialize_channel(test_guild_voice_channel_payload), channels.GuildVoiceChannel) -def test_deserialize_channel_type_passes_kwargs(test_dm_channel_payload, mock_components): - channel_obj = channels.deserialize_channel(test_dm_channel_payload, components=mock_components) - assert channel_obj._components is mock_components +def test_deserialize_channel_type_passes_kwargs(test_dm_channel_payload, mock_app): + channel_obj = channels.deserialize_channel(test_dm_channel_payload, app=mock_app) + assert channel_obj._app is mock_app diff --git a/tests/hikari/test_colors.py b/tests/hikari/models/test_colors.py similarity index 100% rename from tests/hikari/test_colors.py rename to tests/hikari/models/test_colors.py diff --git a/tests/hikari/test_colours.py b/tests/hikari/models/test_colours.py similarity index 92% rename from tests/hikari/test_colours.py rename to tests/hikari/models/test_colours.py index 1d888242b8..66d984c080 100644 --- a/tests/hikari/test_colours.py +++ b/tests/hikari/models/test_colours.py @@ -18,7 +18,8 @@ # along with Hikari. If not, see . import pytest -from hikari.models import colors, colours +from hikari.models import colors +from hikari.models import colours @pytest.mark.model diff --git a/tests/hikari/test_embeds.py b/tests/hikari/models/test_embeds.py similarity index 92% rename from tests/hikari/test_embeds.py rename to tests/hikari/models/test_embeds.py index a408852aa0..804dd74f2d 100644 --- a/tests/hikari/test_embeds.py +++ b/tests/hikari/models/test_embeds.py @@ -15,20 +15,22 @@ # GNU Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public License -# along ith Hikari. If not, see . +# along with Hikari. If not, see . import datetime import mock import pytest -from hikari.models import embeds, colors, files from hikari.components import application from hikari.internal import conversions +from hikari.models import colors +from hikari.models import embeds +from hikari.models import files from tests.hikari import _helpers @pytest.fixture() -def mock_components(): +def mock_app(): return mock.MagicMock(application.Application) @@ -118,8 +120,8 @@ def test_embed_payload( class TestEmbedFooter: - def test_deserialize(self, test_footer_payload, mock_components): - footer_obj = embeds.EmbedFooter.deserialize(test_footer_payload, components=mock_components) + def test_deserialize(self, test_footer_payload, mock_app): + footer_obj = embeds.EmbedFooter.deserialize(test_footer_payload, app=mock_app) assert footer_obj.text == "footer text" assert footer_obj.icon_url == "https://somewhere.com/footer.png" @@ -137,8 +139,8 @@ def test_serialize_partial_footer(self): class TestEmbedImage: - def test_deserialize(self, test_image_payload, mock_components): - image_obj = embeds.EmbedImage.deserialize(test_image_payload, components=mock_components) + def test_deserialize(self, test_image_payload, mock_app): + image_obj = embeds.EmbedImage.deserialize(test_image_payload, app=mock_app) assert image_obj.url == "https://somewhere.com/image.png" assert image_obj.proxy_url == "https://media.somewhere.com/image.png" @@ -155,8 +157,8 @@ def test_serialize_empty_image(self): class TestEmbedThumbnail: - def test_deserialize(self, test_thumbnail_payload, mock_components): - thumbnail_obj = embeds.EmbedThumbnail.deserialize(test_thumbnail_payload, components=mock_components) + def test_deserialize(self, test_thumbnail_payload, mock_app): + thumbnail_obj = embeds.EmbedThumbnail.deserialize(test_thumbnail_payload, app=mock_app) assert thumbnail_obj.url == "https://somewhere.com/thumbnail.png" assert thumbnail_obj.proxy_url == "https://media.somewhere.com/thumbnail.png" @@ -173,8 +175,8 @@ def test_serialize_empty_thumbnail(self): class TestEmbedVideo: - def test_deserialize(self, test_video_payload, mock_components): - video_obj = embeds.EmbedVideo.deserialize(test_video_payload, components=mock_components) + def test_deserialize(self, test_video_payload, mock_app): + video_obj = embeds.EmbedVideo.deserialize(test_video_payload, app=mock_app) assert video_obj.url == "https://somewhere.com/video.mp4" assert video_obj.height == 1234 @@ -182,16 +184,16 @@ def test_deserialize(self, test_video_payload, mock_components): class TestEmbedProvider: - def test_deserialize(self, test_provider_payload, mock_components): - provider_obj = embeds.EmbedProvider.deserialize(test_provider_payload, components=mock_components) + def test_deserialize(self, test_provider_payload, mock_app): + provider_obj = embeds.EmbedProvider.deserialize(test_provider_payload, app=mock_app) assert provider_obj.name == "some name" assert provider_obj.url == "https://somewhere.com/provider" class TestEmbedAuthor: - def test_deserialize(self, test_author_payload, mock_components): - author_obj = embeds.EmbedAuthor.deserialize(test_author_payload, components=mock_components) + def test_deserialize(self, test_author_payload, mock_app): + author_obj = embeds.EmbedAuthor.deserialize(test_author_payload, app=mock_app) assert author_obj.name == "some name" assert author_obj.url == "https://somewhere.com/author" @@ -214,8 +216,8 @@ def test_serialize_empty_author(self): class TestEmbedField: - def test_deserialize(self, mock_components): - field_obj = embeds.EmbedField.deserialize({"name": "title", "value": "some value"}, components=mock_components) + def test_deserialize(self, mock_app): + field_obj = embeds.EmbedField.deserialize({"name": "title", "value": "some value"}, app=mock_app) assert field_obj.name == "title" assert field_obj.value == "some value" @@ -246,14 +248,14 @@ def test_deserialize( test_provider_payload, test_author_payload, test_field_payload, - mock_components, + mock_app, ): mock_datetime = mock.MagicMock(datetime.datetime) with _helpers.patch_marshal_attr( embeds.Embed, "timestamp", deserializer=conversions.parse_iso_8601_ts, return_value=mock_datetime, ) as patched_timestamp_deserializer: - embed_obj = embeds.Embed.deserialize(test_embed_payload, components=mock_components) + embed_obj = embeds.Embed.deserialize(test_embed_payload, app=mock_app) patched_timestamp_deserializer.assert_called_once_with("2020-03-22T16:40:39.218000+00:00") assert embed_obj.title == "embed title" @@ -262,19 +264,19 @@ def test_deserialize( assert embed_obj.timestamp == mock_datetime assert embed_obj.color == colors.Color(14014915) assert embed_obj.footer == embeds.EmbedFooter.deserialize(test_footer_payload) - assert embed_obj.footer._app is mock_components + assert embed_obj.footer._app is mock_app assert embed_obj.image == embeds.EmbedImage.deserialize(test_image_payload) - assert embed_obj.image._app is mock_components + assert embed_obj.image._app is mock_app assert embed_obj.thumbnail == embeds.EmbedThumbnail.deserialize(test_thumbnail_payload) - assert embed_obj.thumbnail._app is mock_components + assert embed_obj.thumbnail._app is mock_app assert embed_obj.video == embeds.EmbedVideo.deserialize(test_video_payload) - assert embed_obj.video._app is mock_components + assert embed_obj.video._app is mock_app assert embed_obj.provider == embeds.EmbedProvider.deserialize(test_provider_payload) - assert embed_obj.provider._app is mock_components + assert embed_obj.provider._app is mock_app assert embed_obj.author == embeds.EmbedAuthor.deserialize(test_author_payload) - assert embed_obj.author._app is mock_components + assert embed_obj.author._app is mock_app assert embed_obj.fields == [embeds.EmbedField.deserialize(test_field_payload)] - assert embed_obj.fields[0]._app is mock_components + assert embed_obj.fields[0]._app is mock_app def test_serialize_full_embed(self): embed_obj = embeds.Embed( @@ -290,7 +292,7 @@ def test_serialize_full_embed(self): fields=[embeds.EmbedField(name="aField", value="agent69", is_inline=True)], ) - with mock.patch("hikari.embeds.Embed._check_total_length") as mock_check: + with mock.patch.object(embeds.Embed, "_check_total_length") as mock_check: assert embed_obj.serialize() == { "title": "Nyaa me pls >////<", "description": "Nyan >////<", @@ -453,7 +455,7 @@ def test_add_field_with_optionals(self): field_obj2 = embeds.EmbedField(name="test_name", value="test_value", is_inline=True) em = embeds.Embed() em.fields = [field_obj1] - with mock.patch("hikari.embeds.EmbedField", return_value=field_obj2) as mock_embed_field: + with mock.patch.object(embeds, "EmbedField", return_value=field_obj2) as mock_embed_field: assert em.add_field(name="test_name", value="test_value", inline=True, index=0) == em mock_embed_field.assert_called_once_with(name="test_name", value="test_value", is_inline=True) assert em.fields == [field_obj2, field_obj1] diff --git a/tests/hikari/test_emojis.py b/tests/hikari/models/test_emojis.py similarity index 92% rename from tests/hikari/test_emojis.py rename to tests/hikari/models/test_emojis.py index c28d6cd02d..5cea94c747 100644 --- a/tests/hikari/test_emojis.py +++ b/tests/hikari/models/test_emojis.py @@ -15,18 +15,21 @@ # GNU Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public License -# along ith Hikari. If not, see . +# along with Hikari. If not, see . import mock import pytest -from hikari.models import users, emojis, bases, files from hikari.components import application from hikari.internal import urls +from hikari.models import bases +from hikari.models import emojis +from hikari.models import files +from hikari.models import users from tests.hikari import _helpers @pytest.fixture() -def mock_components(): +def mock_app(): return mock.MagicMock(application.Application) @@ -67,8 +70,8 @@ def test_aiter(self, test_emoji): class TestUnicodeEmoji: england = [0x1F3F4, 0xE0067, 0xE0062, 0xE0065, 0xE006E, 0xE0067, 0xE007F] - def test_deserialize(self, mock_components): - emoji_obj = emojis.UnicodeEmoji.deserialize({"name": "🤷"}, components=mock_components) + def test_deserialize(self, mock_app): + emoji_obj = emojis.UnicodeEmoji.deserialize({"name": "🤷"}, app=mock_app) assert emoji_obj.name == "🤷" @@ -153,10 +156,8 @@ def test_unicode_escape(self): class TestCustomEmoji: - def test_deserialize(self, mock_components): - emoji_obj = emojis.CustomEmoji.deserialize( - {"id": "1234", "name": "test", "animated": True}, components=mock_components - ) + def test_deserialize(self, mock_app): + emoji_obj = emojis.CustomEmoji.deserialize({"id": "1234", "name": "test", "animated": True}, app=mock_app) assert emoji_obj.id == 1234 assert emoji_obj.name == "test" @@ -187,7 +188,7 @@ def test_url(self, animated, format_): class TestKnownCustomEmoji: - def test_deserialize(self, mock_components): + def test_deserialize(self, mock_app): mock_user = mock.MagicMock(users.User) test_user_payload = {"id": "123456", "username": "hikari", "discriminator": "0000", "avatar": None} @@ -205,9 +206,9 @@ def test_deserialize(self, mock_components): "require_colons": True, "managed": False, }, - components=mock_components, + app=mock_app, ) - patched_user_deserializer.assert_called_once_with(test_user_payload, components=mock_components) + patched_user_deserializer.assert_called_once_with(test_user_payload, app=mock_app) assert emoji_obj.id == 12345 assert emoji_obj.name == "testing" @@ -248,6 +249,6 @@ def test_deserialize_reaction_emoji_returns_expected_type(payload, expected_type assert isinstance(emojis.deserialize_reaction_emoji(payload), expected_type) -def test_deserialize_reaction_emoji_passes_kwargs(mock_components): - emoji_obj = emojis.deserialize_reaction_emoji({"id": "1234", "name": "test"}, components=mock_components) - assert emoji_obj._components is mock_components +def test_deserialize_reaction_emoji_passes_kwargs(mock_app): + emoji_obj = emojis.deserialize_reaction_emoji({"id": "1234", "name": "test"}, app=mock_app) + assert emoji_obj._app is mock_app diff --git a/tests/hikari/test_files.py b/tests/hikari/models/test_files.py similarity index 99% rename from tests/hikari/test_files.py rename to tests/hikari/models/test_files.py index 148c3dfcd8..02d080af39 100644 --- a/tests/hikari/test_files.py +++ b/tests/hikari/models/test_files.py @@ -15,7 +15,7 @@ # GNU Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public License -# along ith Hikari. If not, see . +# along with Hikari. If not, see . import concurrent.futures import io import os @@ -27,8 +27,8 @@ import mock import pytest -from hikari.models import files from hikari import errors +from hikari.models import files from tests.hikari import _helpers diff --git a/tests/hikari/test_gateway_entities.py b/tests/hikari/models/test_gateway.py similarity index 91% rename from tests/hikari/test_gateway_entities.py rename to tests/hikari/models/test_gateway.py index afeab904a6..3157783a93 100644 --- a/tests/hikari/test_gateway_entities.py +++ b/tests/hikari/models/test_gateway.py @@ -21,13 +21,14 @@ import mock import pytest -from hikari.models import guilds, gateway from hikari.components import application +from hikari.models import gateway +from hikari.models import guilds from tests.hikari import _helpers @pytest.fixture() -def mock_components(): +def mock_app(): return mock.MagicMock(application.Application) @@ -37,10 +38,8 @@ def test_session_start_limit_payload(): class TestSessionStartLimit: - def test_deserialize(self, test_session_start_limit_payload, mock_components): - session_start_limit_obj = gateway.SessionStartLimit.deserialize( - test_session_start_limit_payload, components=mock_components - ) + def test_deserialize(self, test_session_start_limit_payload, mock_app): + session_start_limit_obj = gateway.SessionStartLimit.deserialize(test_session_start_limit_payload, app=mock_app) assert session_start_limit_obj.total == 1000 assert session_start_limit_obj.remaining == 991 assert session_start_limit_obj.reset_after == datetime.timedelta(milliseconds=14170186) @@ -51,7 +50,7 @@ class TestGatewayBot: def test_gateway_bot_payload(self, test_session_start_limit_payload): return {"url": "wss://gateway.discord.gg", "shards": 1, "session_start_limit": test_session_start_limit_payload} - def test_deserialize(self, test_gateway_bot_payload, test_session_start_limit_payload, mock_components): + def test_deserialize(self, test_gateway_bot_payload, test_session_start_limit_payload, mock_app): mock_session_start_limit = mock.MagicMock(gateway.SessionStartLimit) with _helpers.patch_marshal_attr( gateway.GatewayBot, @@ -59,10 +58,8 @@ def test_deserialize(self, test_gateway_bot_payload, test_session_start_limit_pa deserializer=gateway.SessionStartLimit.deserialize, return_value=mock_session_start_limit, ) as patched_start_limit_deserializer: - gateway_bot_obj = gateway.GatewayBot.deserialize(test_gateway_bot_payload, components=mock_components) - patched_start_limit_deserializer.assert_called_once_with( - test_session_start_limit_payload, components=mock_components - ) + gateway_bot_obj = gateway.GatewayBot.deserialize(test_gateway_bot_payload, app=mock_app) + patched_start_limit_deserializer.assert_called_once_with(test_session_start_limit_payload, app=mock_app) assert gateway_bot_obj.session_start_limit is mock_session_start_limit assert gateway_bot_obj.url == "wss://gateway.discord.gg" assert gateway_bot_obj.shard_count == 1 diff --git a/tests/hikari/test_guilds.py b/tests/hikari/models/test_guilds.py similarity index 93% rename from tests/hikari/test_guilds.py rename to tests/hikari/models/test_guilds.py index 77b6b6c51f..666473a9bb 100644 --- a/tests/hikari/test_guilds.py +++ b/tests/hikari/models/test_guilds.py @@ -15,17 +15,23 @@ # GNU Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public License -# along ith Hikari. If not, see . +# along with Hikari. If not, see . import contextlib import datetime import mock import pytest -from hikari.models import unset, permissions, guilds, users, colors, channels, emojis from hikari.components import application from hikari.internal import conversions from hikari.internal import urls +from hikari.models import channels +from hikari.models import colors +from hikari.models import emojis +from hikari.models import guilds +from hikari.models import permissions +from hikari.models import unset +from hikari.models import users from tests.hikari import _helpers @@ -263,7 +269,7 @@ def test_guild_payload( @pytest.fixture() -def mock_components(): +def mock_app(): return mock.MagicMock(application.Application) @@ -272,14 +278,14 @@ class TestGuildEmbed: def test_guild_embed_payload(self): return {"channel_id": "123123123", "enabled": True} - def test_deserialize(self, test_guild_embed_payload, mock_components): - guild_embed_obj = guilds.GuildEmbed.deserialize(test_guild_embed_payload, components=mock_components) + def test_deserialize(self, test_guild_embed_payload, mock_app): + guild_embed_obj = guilds.GuildEmbed.deserialize(test_guild_embed_payload, app=mock_app) assert guild_embed_obj.channel_id == 123123123 assert guild_embed_obj.is_enabled is True class TestGuildMember: - def test_deserialize(self, test_member_payload, test_user_payload, mock_components): + def test_deserialize(self, test_member_payload, test_user_payload, mock_app): mock_user = mock.MagicMock(users.User) mock_datetime_1 = mock.MagicMock(datetime.datetime) mock_datetime_2 = mock.MagicMock(datetime.datetime) @@ -306,10 +312,10 @@ def test_deserialize(self, test_member_payload, test_user_payload, mock_componen ) ) with stack: - guild_member_obj = guilds.GuildMember.deserialize(test_member_payload, components=mock_components) + guild_member_obj = guilds.GuildMember.deserialize(test_member_payload, app=mock_app) patched_premium_since_deserializer.assert_called_once_with("2019-05-17T06:26:56.936000+00:00") patched_joined_at_deserializer.assert_called_once_with("2015-04-26T06:26:56.936000+00:00") - patched_user_deserializer.assert_called_once_with(test_user_payload, components=mock_components) + patched_user_deserializer.assert_called_once_with(test_user_payload, app=mock_app) assert guild_member_obj.user is mock_user assert guild_member_obj.nickname == "foobarbaz" assert guild_member_obj.role_ids == [11111, 22222, 33333, 44444] @@ -327,10 +333,8 @@ def test_partial_guild_role_payload(self): "name": "WE DEM BOYZZ!!!!!!", } - def test_deserialize(self, test_partial_guild_role_payload, mock_components): - partial_guild_role_obj = guilds.PartialGuildRole.deserialize( - test_partial_guild_role_payload, components=mock_components - ) + def test_deserialize(self, test_partial_guild_role_payload, mock_app): + partial_guild_role_obj = guilds.PartialGuildRole.deserialize(test_partial_guild_role_payload, app=mock_app) assert partial_guild_role_obj.name == "WE DEM BOYZZ!!!!!!" @@ -348,8 +352,8 @@ def test_guild_role_payload(self): "mentionable": False, } - def test_deserialize(self, test_guild_role_payload, mock_components): - guild_role_obj = guilds.GuildRole.deserialize(test_guild_role_payload, components=mock_components) + def test_deserialize(self, test_guild_role_payload, mock_app): + guild_role_obj = guilds.GuildRole.deserialize(test_guild_role_payload, app=mock_app) assert guild_role_obj.color == 3_447_003 assert guild_role_obj.is_hoisted is True assert guild_role_obj.position == 0 @@ -390,7 +394,7 @@ def test_serialize_partial_role(self): class TestActivityTimestamps: - def test_deserialize(self, test_activity_timestamps_payload, mock_components): + def test_deserialize(self, test_activity_timestamps_payload, mock_app): mock_start_date = mock.MagicMock(datetime.datetime) mock_end_date = mock.MagicMock(datetime.datetime) stack = contextlib.ExitStack() @@ -412,7 +416,7 @@ def test_deserialize(self, test_activity_timestamps_payload, mock_components): ) with stack: activity_timestamps_obj = guilds.ActivityTimestamps.deserialize( - test_activity_timestamps_payload, components=mock_components + test_activity_timestamps_payload, app=mock_app ) patched_end_deserializer.assert_called_once_with(1999999792798) patched_start_deserializer.assert_called_once_with(1584996792798) @@ -445,10 +449,8 @@ def test_max_size_when_null(self, test_activity_party_obj): class TestActivityAssets: - def test_deserialize(self, test_activity_assets_payload, mock_components): - activity_assets_obj = guilds.ActivityAssets.deserialize( - test_activity_assets_payload, components=mock_components - ) + def test_deserialize(self, test_activity_assets_payload, mock_app): + activity_assets_obj = guilds.ActivityAssets.deserialize(test_activity_assets_payload, app=mock_app) assert activity_assets_obj.large_image == "34234234234243" assert activity_assets_obj.large_text == "LARGE TEXT" assert activity_assets_obj.small_image == "3939393" @@ -456,10 +458,8 @@ def test_deserialize(self, test_activity_assets_payload, mock_components): class TestActivitySecret: - def test_deserialize(self, test_activity_secrets_payload, mock_components): - activity_secret_obj = guilds.ActivitySecret.deserialize( - test_activity_secrets_payload, components=mock_components - ) + def test_deserialize(self, test_activity_secrets_payload, mock_app): + activity_secret_obj = guilds.ActivitySecret.deserialize(test_activity_secrets_payload, app=mock_app) assert activity_secret_obj.join == "who's a good secret?" assert activity_secret_obj.spectate == "I'm a good secret" assert activity_secret_obj.match == "No." @@ -474,7 +474,7 @@ def test_deserialize( test_activity_party_payload, test_emoji_payload, test_activity_timestamps_payload, - mock_components, + mock_app, ): mock_created_at = mock.MagicMock(datetime.datetime) mock_emoji = mock.MagicMock(emojis.CustomEmoji) @@ -496,10 +496,8 @@ def test_deserialize( ) ) with stack: - presence_activity_obj = guilds.PresenceActivity.deserialize( - test_presence_activity_payload, components=mock_components - ) - patched_emoji_deserializer.assert_called_once_with(test_emoji_payload, components=mock_components) + presence_activity_obj = guilds.PresenceActivity.deserialize(test_presence_activity_payload, app=mock_app) + patched_emoji_deserializer.assert_called_once_with(test_emoji_payload, app=mock_app) patched_created_at_deserializer.assert_called_once_with(1584996792798) assert presence_activity_obj.name == "an activity" assert presence_activity_obj.type is guilds.ActivityType.STREAMING @@ -508,17 +506,17 @@ def test_deserialize( assert presence_activity_obj.timestamps == guilds.ActivityTimestamps.deserialize( test_activity_timestamps_payload ) - assert presence_activity_obj.timestamps._app is mock_components + assert presence_activity_obj.timestamps._app is mock_app assert presence_activity_obj.application_id == 40404040404040 assert presence_activity_obj.details == "They are doing stuff" assert presence_activity_obj.state == "STATED" assert presence_activity_obj.emoji is mock_emoji assert presence_activity_obj.party == guilds.ActivityParty.deserialize(test_activity_party_payload) - assert presence_activity_obj.party._app is mock_components + assert presence_activity_obj.party._app is mock_app assert presence_activity_obj.assets == guilds.ActivityAssets.deserialize(test_activity_assets_payload) - assert presence_activity_obj.assets._app is mock_components + assert presence_activity_obj.assets._app is mock_app assert presence_activity_obj.secrets == guilds.ActivitySecret.deserialize(test_activity_secrets_payload) - assert presence_activity_obj.secrets._app is mock_components + assert presence_activity_obj.secrets._app is mock_app assert presence_activity_obj.is_instance is True assert presence_activity_obj.flags == guilds.ActivityFlag.INSTANCE | guilds.ActivityFlag.JOIN @@ -529,16 +527,16 @@ def test_client_status_payload(): class TestClientStatus: - def test_deserialize(self, test_client_status_payload, mock_components): - client_status_obj = guilds.ClientStatus.deserialize(test_client_status_payload, components=mock_components) + def test_deserialize(self, test_client_status_payload, mock_app): + client_status_obj = guilds.ClientStatus.deserialize(test_client_status_payload, app=mock_app) assert client_status_obj.desktop is guilds.PresenceStatus.ONLINE assert client_status_obj.mobile is guilds.PresenceStatus.IDLE assert client_status_obj.web is guilds.PresenceStatus.OFFLINE class TestPresenceUser: - def test_deserialize_filled_presence_user(self, test_user_payload, mock_components): - presence_user_obj = guilds.PresenceUser.deserialize(test_user_payload, components=mock_components) + def test_deserialize_filled_presence_user(self, test_user_payload, mock_app): + presence_user_obj = guilds.PresenceUser.deserialize(test_user_payload, app=mock_app) assert presence_user_obj.username == "Boris Johnson" assert presence_user_obj.discriminator == "6969" assert presence_user_obj.avatar_hash == "1a2b3c4d" @@ -546,8 +544,8 @@ def test_deserialize_filled_presence_user(self, test_user_payload, mock_componen assert presence_user_obj.is_bot is True assert presence_user_obj.flags == users.UserFlag(0b0001101) - def test_deserialize_partial_presence_user(self, mock_components): - presence_user_obj = guilds.PresenceUser.deserialize({"id": "115590097100865541"}, components=mock_components) + def test_deserialize_partial_presence_user(self, mock_app): + presence_user_obj = guilds.PresenceUser.deserialize({"id": "115590097100865541"}, app=mock_app) assert presence_user_obj.id == 115590097100865541 for attr in presence_user_obj.__slots__: if attr not in ("id", "_app"): @@ -646,7 +644,7 @@ def test_deserialize( test_user_payload, test_presence_activity_payload, test_client_status_payload, - mock_components, + mock_app, ): mock_since = mock.MagicMock(datetime.datetime) with _helpers.patch_marshal_attr( @@ -655,21 +653,19 @@ def test_deserialize( deserializer=conversions.parse_iso_8601_ts, return_value=mock_since, ) as patched_since_deserializer: - guild_member_presence_obj = guilds.GuildMemberPresence.deserialize( - test_guild_member_presence, components=mock_components - ) + guild_member_presence_obj = guilds.GuildMemberPresence.deserialize(test_guild_member_presence, app=mock_app) patched_since_deserializer.assert_called_once_with("2015-04-26T06:26:56.936000+00:00") assert guild_member_presence_obj.user == guilds.PresenceUser.deserialize(test_user_payload) - assert guild_member_presence_obj.user._app is mock_components + assert guild_member_presence_obj.user._app is mock_app assert guild_member_presence_obj.role_ids == [49494949] assert guild_member_presence_obj.guild_id == 44004040 assert guild_member_presence_obj.visible_status is guilds.PresenceStatus.DND assert guild_member_presence_obj.activities == [ guilds.PresenceActivity.deserialize(test_presence_activity_payload) ] - assert guild_member_presence_obj.activities[0]._app is mock_components + assert guild_member_presence_obj.activities[0]._app is mock_app assert guild_member_presence_obj.client_status == guilds.ClientStatus.deserialize(test_client_status_payload) - assert guild_member_presence_obj.client_status._app is mock_components + assert guild_member_presence_obj.client_status._app is mock_app assert guild_member_presence_obj.premium_since is mock_since assert guild_member_presence_obj.nick == "Nick" @@ -679,15 +675,13 @@ class TestGuildMemberBan: def test_guild_member_ban_payload(self, test_user_payload): return {"reason": "Get Nyaa'ed", "user": test_user_payload} - def test_deserializer(self, test_guild_member_ban_payload, test_user_payload, mock_components): + def test_deserializer(self, test_guild_member_ban_payload, test_user_payload, mock_app): mock_user = mock.MagicMock(users.User) with _helpers.patch_marshal_attr( guilds.GuildMemberBan, "user", deserializer=users.User.deserialize, return_value=mock_user ) as patched_user_deserializer: - guild_member_ban_obj = guilds.GuildMemberBan.deserialize( - test_guild_member_ban_payload, components=mock_components - ) - patched_user_deserializer.assert_called_once_with(test_user_payload, components=mock_components) + guild_member_ban_obj = guilds.GuildMemberBan.deserialize(test_guild_member_ban_payload, app=mock_app) + patched_user_deserializer.assert_called_once_with(test_user_payload, app=mock_app) assert guild_member_ban_obj.reason == "Get Nyaa'ed" assert guild_member_ban_obj.user is mock_user @@ -698,10 +692,8 @@ def test_integration_account_payload(): class TestIntegrationAccount: - def test_deserializer(self, test_integration_account_payload, mock_components): - integration_account_obj = guilds.IntegrationAccount.deserialize( - test_integration_account_payload, components=mock_components - ) + def test_deserializer(self, test_integration_account_payload, mock_app): + integration_account_obj = guilds.IntegrationAccount.deserialize(test_integration_account_payload, app=mock_app) assert integration_account_obj.id == "543453" assert integration_account_obj.name == "Blah Blah" @@ -717,18 +709,16 @@ def test_partial_guild_integration_payload(test_integration_account_payload): class TestPartialGuildIntegration: - def test_deserialise( - self, test_partial_guild_integration_payload, test_integration_account_payload, mock_components - ): + def test_deserialise(self, test_partial_guild_integration_payload, test_integration_account_payload, mock_app): partial_guild_integration_obj = guilds.PartialGuildIntegration.deserialize( - test_partial_guild_integration_payload, components=mock_components + test_partial_guild_integration_payload, app=mock_app ) assert partial_guild_integration_obj.name == "Blah blah" assert partial_guild_integration_obj.type == "twitch" assert partial_guild_integration_obj.account == guilds.IntegrationAccount.deserialize( test_integration_account_payload ) - assert partial_guild_integration_obj.account._app is mock_components + assert partial_guild_integration_obj.account._app is mock_app class TestGuildIntegration: @@ -747,7 +737,7 @@ def test_guild_integration_payload(self, test_user_payload, test_partial_guild_i } def test_deserialize( - self, test_guild_integration_payload, test_user_payload, test_integration_account_payload, mock_components + self, test_guild_integration_payload, test_user_payload, test_integration_account_payload, mock_app ): mock_user = mock.MagicMock(users.User) mock_sync_date = mock.MagicMock(datetime.datetime) @@ -766,10 +756,8 @@ def test_deserialize( ) ) with stack: - guild_integration_obj = guilds.GuildIntegration.deserialize( - test_guild_integration_payload, components=mock_components - ) - patched_user_deserializer.assert_called_once_with(test_user_payload, components=mock_components) + guild_integration_obj = guilds.GuildIntegration.deserialize(test_guild_integration_payload, app=mock_app) + patched_user_deserializer.assert_called_once_with(test_user_payload, app=mock_app) patched_sync_at_deserializer.assert_called_once_with("2015-04-26T06:26:56.936000+00:00") assert guild_integration_obj.is_enabled is True @@ -783,20 +771,20 @@ def test_deserialize( class TestUnavailableGuild: - def test_deserialize_when_unavailable_is_defined(self, mock_components): + def test_deserialize_when_unavailable_is_defined(self, mock_app): guild_delete_event_obj = guilds.UnavailableGuild.deserialize( - {"id": "293293939", "unavailable": True}, components=mock_components + {"id": "293293939", "unavailable": True}, app=mock_app ) assert guild_delete_event_obj.is_unavailable is True - def test_deserialize_when_unavailable_is_undefined(self, mock_components): - guild_delete_event_obj = guilds.UnavailableGuild.deserialize({"id": "293293939"}, components=mock_components) + def test_deserialize_when_unavailable_is_undefined(self, mock_app): + guild_delete_event_obj = guilds.UnavailableGuild.deserialize({"id": "293293939"}, app=mock_app) assert guild_delete_event_obj.is_unavailable is True class TestPartialGuild: - def test_deserialize(self, test_partial_guild_payload, mock_components): - partial_guild_obj = guilds.PartialGuild.deserialize(test_partial_guild_payload, components=mock_components) + def test_deserialize(self, test_partial_guild_payload, mock_app): + partial_guild_obj = guilds.PartialGuild.deserialize(test_partial_guild_payload, app=mock_app) assert partial_guild_obj.id == 152559372126519269 assert partial_guild_obj.name == "Isopropyl" assert partial_guild_obj.icon_hash == "d4a983885dsaa7691ce8bcaaf945a" @@ -879,11 +867,11 @@ def test_icon_url_default(self, partial_guild_obj): class TestGuildPreview: - def test_deserialize(self, test_guild_preview_payload, test_emoji_payload, mock_components): + def test_deserialize(self, test_guild_preview_payload, test_emoji_payload, mock_app): mock_emoji = mock.MagicMock(emojis.KnownCustomEmoji, id=41771983429993937) with mock.patch.object(emojis.KnownCustomEmoji, "deserialize", return_value=mock_emoji): - guild_preview_obj = guilds.GuildPreview.deserialize(test_guild_preview_payload, components=mock_components) - emojis.KnownCustomEmoji.deserialize.assert_called_once_with(test_emoji_payload, components=mock_components) + guild_preview_obj = guilds.GuildPreview.deserialize(test_guild_preview_payload, app=mock_app) + emojis.KnownCustomEmoji.deserialize.assert_called_once_with(test_emoji_payload, app=mock_app) assert guild_preview_obj.splash_hash == "dsa345tfcdg54b" assert guild_preview_obj.discovery_splash_hash == "lkodwaidi09239uid" assert guild_preview_obj.emojis == {41771983429993937: mock_emoji} @@ -956,7 +944,7 @@ def test_splash_url(self, test_guild_preview_obj): class TestGuild: def test_deserialize( self, - mock_components, + mock_app, test_guild_payload, test_roles_payload, test_emoji_payload, @@ -975,13 +963,13 @@ def test_deserialize( ) ) with stack: - guild_obj = guilds.Guild.deserialize(test_guild_payload, components=mock_components) - channels.deserialize_channel.assert_called_once_with(test_channel_payload, components=mock_components) - emojis.KnownCustomEmoji.deserialize.assert_called_once_with(test_emoji_payload, components=mock_components) + guild_obj = guilds.Guild.deserialize(test_guild_payload, app=mock_app) + channels.deserialize_channel.assert_called_once_with(test_channel_payload, app=mock_app) + emojis.KnownCustomEmoji.deserialize.assert_called_once_with(test_emoji_payload, app=mock_app) assert guild_obj.members == {123456: guilds.GuildMember.deserialize(test_member_payload)} - assert guild_obj.members[123456]._app is mock_components + assert guild_obj.members[123456]._app is mock_app assert guild_obj.presences == {123456: guilds.GuildMemberPresence.deserialize(test_guild_member_presence)} - assert guild_obj.presences[123456]._app is mock_components + assert guild_obj.presences[123456]._app is mock_app assert guild_obj.splash_hash == "0ff0ff0ff" assert guild_obj.discovery_splash_hash == "famfamFAMFAMfam" assert guild_obj.owner_id == 6969696 @@ -997,7 +985,7 @@ def test_deserialize( assert guild_obj.default_message_notifications is guilds.GuildMessageNotificationsLevel.ONLY_MENTIONS assert guild_obj.explicit_content_filter is guilds.GuildExplicitContentFilterLevel.ALL_MEMBERS assert guild_obj.roles == {41771983423143936: guilds.GuildRole.deserialize(test_roles_payload)} - assert guild_obj.roles[41771983423143936]._app is mock_components + assert guild_obj.roles[41771983423143936]._app is mock_app assert guild_obj.emojis == {41771983429993937: mock_emoji} assert guild_obj.mfa_level is guilds.GuildMFALevel.ELEVATED assert guild_obj.application_id == 39494949 diff --git a/tests/hikari/test_intents.py b/tests/hikari/models/test_intents.py similarity index 100% rename from tests/hikari/test_intents.py rename to tests/hikari/models/test_intents.py diff --git a/tests/hikari/test_invites.py b/tests/hikari/models/test_invites.py similarity index 93% rename from tests/hikari/test_invites.py rename to tests/hikari/models/test_invites.py index 4938aa3d67..4a5911cfd5 100644 --- a/tests/hikari/test_invites.py +++ b/tests/hikari/models/test_invites.py @@ -15,17 +15,20 @@ # GNU Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public License -# along ith Hikari. If not, see . +# along with Hikari. If not, see . import contextlib import datetime import mock import pytest -from hikari.models import guilds, users, channels, invites from hikari.components import application from hikari.internal import conversions from hikari.internal import urls +from hikari.models import channels +from hikari.models import guilds +from hikari.models import invites +from hikari.models import users from tests.hikari import _helpers @@ -86,13 +89,13 @@ def test_invite_with_metadata_payload(test_invite_payload): @pytest.fixture() -def mock_components(): +def mock_app(): return mock.MagicMock(application.Application) class TestInviteGuild: - def test_deserialize(self, test_invite_guild_payload, mock_components): - invite_guild_obj = invites.InviteGuild.deserialize(test_invite_guild_payload, components=mock_components) + def test_deserialize(self, test_invite_guild_payload, mock_app): + invite_guild_obj = invites.InviteGuild.deserialize(test_invite_guild_payload, app=mock_app) assert invite_guild_obj.splash_hash == "aSplashForSure" assert invite_guild_obj.banner_hash == "aBannerForSure" assert invite_guild_obj.description == "Describe me cute kitty." @@ -165,8 +168,8 @@ class TestVanityUrl: def vanity_url_payload(self): return {"code": "iamacode", "uses": 42} - def test_deserialize(self, vanity_url_payload, mock_components): - vanity_url_obj = invites.VanityUrl.deserialize(vanity_url_payload, components=mock_components) + def test_deserialize(self, vanity_url_payload, mock_app): + vanity_url_obj = invites.VanityUrl.deserialize(vanity_url_payload, app=mock_app) assert vanity_url_obj.code == "iamacode" assert vanity_url_obj.uses == 42 @@ -179,7 +182,7 @@ def test_deserialize( test_2nd_user_payload, test_partial_channel, test_invite_guild_payload, - mock_components, + mock_app, ): mock_guild = mock.MagicMock(invites.InviteGuild) mock_channel = mock.MagicMock(channels.PartialChannel) @@ -207,11 +210,11 @@ def test_deserialize( ) ) with stack: - invite_obj = invites.Invite.deserialize(test_invite_payload, components=mock_components) - mock_target_user_deseralize.assert_called_once_with(test_2nd_user_payload, components=mock_components) - mock_inviter_deseralize.assert_called_once_with(test_user_payload, components=mock_components) - mock_channel_deseralize.assert_called_once_with(test_partial_channel, components=mock_components) - mock_guld_deseralize.assert_called_once_with(test_invite_guild_payload, components=mock_components) + invite_obj = invites.Invite.deserialize(test_invite_payload, app=mock_app) + mock_target_user_deseralize.assert_called_once_with(test_2nd_user_payload, app=mock_app) + mock_inviter_deseralize.assert_called_once_with(test_user_payload, app=mock_app) + mock_channel_deseralize.assert_called_once_with(test_partial_channel, app=mock_app) + mock_guld_deseralize.assert_called_once_with(test_invite_guild_payload, app=mock_app) assert invite_obj.code == "aCode" assert invite_obj.guild is mock_guild assert invite_obj.channel is mock_channel @@ -223,7 +226,7 @@ def test_deserialize( class TestInviteWithMetadata: - def test_deserialize(self, test_invite_with_metadata_payload, mock_components): + def test_deserialize(self, test_invite_with_metadata_payload, mock_app): mock_datetime = mock.MagicMock(datetime.datetime) stack = contextlib.ExitStack() stack.enter_context( @@ -252,7 +255,7 @@ def test_deserialize(self, test_invite_with_metadata_payload, mock_components): ) with stack: invite_with_metadata_obj = invites.InviteWithMetadata.deserialize( - test_invite_with_metadata_payload, components=mock_components + test_invite_with_metadata_payload, app=mock_app ) mock_created_at_deserializer.assert_called_once_with("2015-04-26T06:26:56.936000+00:00") assert invite_with_metadata_obj.uses == 3 diff --git a/tests/hikari/test_messages.py b/tests/hikari/models/test_messages.py similarity index 92% rename from tests/hikari/test_messages.py rename to tests/hikari/models/test_messages.py index 203e41722e..8e63b1a28b 100644 --- a/tests/hikari/test_messages.py +++ b/tests/hikari/models/test_messages.py @@ -15,16 +15,24 @@ # GNU Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public License -# along ith Hikari. If not, see . +# along with Hikari. If not, see . import contextlib import datetime import mock import pytest -from hikari.models import embeds, messages, guilds, applications, users, channels, emojis, bases, files from hikari.components import application from hikari.internal import conversions +from hikari.models import applications +from hikari.models import bases +from hikari.models import channels +from hikari.models import embeds +from hikari.models import emojis +from hikari.models import files +from hikari.models import guilds +from hikari.models import messages +from hikari.models import users from tests.hikari import _helpers @@ -129,13 +137,13 @@ def test_message_payload( @pytest.fixture() -def mock_components(): +def mock_app(): return mock.MagicMock(application.Application) class TestAttachment: - def test_deserialize(self, test_attachment_payload, mock_components): - attachment_obj = messages.Attachment.deserialize(test_attachment_payload, components=mock_components) + def test_deserialize(self, test_attachment_payload, mock_app): + attachment_obj = messages.Attachment.deserialize(test_attachment_payload, app=mock_app) assert attachment_obj.id == 690922406474154014 assert attachment_obj.filename == "IMG.jpg" @@ -174,14 +182,14 @@ async def __aiter__(_): class TestReaction: - def test_deserialize(self, test_reaction_payload, mock_components, test_emoji_payload): + def test_deserialize(self, test_reaction_payload, mock_app, test_emoji_payload): mock_emoji = mock.MagicMock(emojis.CustomEmoji) with _helpers.patch_marshal_attr( messages.Reaction, "emoji", return_value=mock_emoji, deserializer=emojis.deserialize_reaction_emoji ) as patched_emoji_deserializer: - reaction_obj = messages.Reaction.deserialize(test_reaction_payload, components=mock_components) - patched_emoji_deserializer.assert_called_once_with(test_emoji_payload, components=mock_components) + reaction_obj = messages.Reaction.deserialize(test_reaction_payload, app=mock_app) + patched_emoji_deserializer.assert_called_once_with(test_emoji_payload, app=mock_app) assert reaction_obj.count == 100 assert reaction_obj.emoji == mock_emoji @@ -189,20 +197,16 @@ def test_deserialize(self, test_reaction_payload, mock_components, test_emoji_pa class TestMessageActivity: - def test_deserialize(self, test_message_activity_payload, mock_components): - message_activity_obj = messages.MessageActivity.deserialize( - test_message_activity_payload, components=mock_components - ) + def test_deserialize(self, test_message_activity_payload, mock_app): + message_activity_obj = messages.MessageActivity.deserialize(test_message_activity_payload, app=mock_app) assert message_activity_obj.type == messages.MessageActivityType.JOIN_REQUEST assert message_activity_obj.party_id == "ae488379-351d-4a4f-ad32-2b9b01c91657" class TestMessageCrosspost: - def test_deserialize(self, test_message_crosspost_payload, mock_components): - message_crosspost_obj = messages.MessageCrosspost.deserialize( - test_message_crosspost_payload, components=mock_components - ) + def test_deserialize(self, test_message_crosspost_payload, mock_app): + message_crosspost_obj = messages.MessageCrosspost.deserialize(test_message_crosspost_payload, app=mock_app) assert message_crosspost_obj.id == 306588351130107906 assert message_crosspost_obj.channel_id == 278325129692446722 @@ -220,13 +224,13 @@ def test_deserialize( test_member_payload, test_message_activity_payload, test_message_crosspost_payload, - mock_components, + mock_app, ): mock_user = mock.MagicMock(users.User) mock_member = mock.MagicMock(guilds.GuildMember) mock_datetime = mock.MagicMock(datetime.datetime) mock_datetime2 = mock.MagicMock(datetime.datetime) - mock_emoji = mock.MagicMock(messages._emojis) + mock_emoji = mock.MagicMock(emojis.Emoji) mock_app = mock.MagicMock(applications.Application) stack = contextlib.ExitStack() @@ -267,19 +271,15 @@ def test_deserialize( ) ) with stack: - message_obj = messages.Message.deserialize(test_message_payload, components=mock_components) - patched_emoji_deserializer.assert_called_once_with( - test_reaction_payload["emoji"], components=mock_components - ) + message_obj = messages.Message.deserialize(test_message_payload, app=mock_app) + patched_emoji_deserializer.assert_called_once_with(test_reaction_payload["emoji"], app=mock_app) assert message_obj.reactions == [messages.Reaction.deserialize(test_reaction_payload)] - assert message_obj.reactions[0]._app is mock_components - patched_application_deserializer.assert_called_once_with( - test_application_payload, components=mock_components - ) + assert message_obj.reactions[0]._app is mock_app + patched_application_deserializer.assert_called_once_with(test_application_payload, app=mock_app) patched_edited_timestamp_deserializer.assert_called_once_with("2020-04-21T21:20:16.510000+00:00") patched_timestamp_deserializer.assert_called_once_with("2020-03-21T21:20:16.510000+00:00") - patched_member_deserializer.assert_called_once_with(test_member_payload, components=mock_components) - patched_author_deserializer.assert_called_once_with(test_user_payload, components=mock_components) + patched_member_deserializer.assert_called_once_with(test_member_payload, app=mock_app) + patched_author_deserializer.assert_called_once_with(test_user_payload, app=mock_app) assert message_obj.id == 123 assert message_obj.channel_id == 456 @@ -295,17 +295,17 @@ def test_deserialize( assert message_obj.role_mentions == {987} assert message_obj.channel_mentions == {456} assert message_obj.attachments == [messages.Attachment.deserialize(test_attachment_payload)] - assert message_obj.attachments[0]._app is mock_components + assert message_obj.attachments[0]._app is mock_app assert message_obj.embeds == [embeds.Embed.deserialize({})] - assert message_obj.embeds[0]._app is mock_components + assert message_obj.embeds[0]._app is mock_app assert message_obj.is_pinned is True assert message_obj.webhook_id == 1234 assert message_obj.type == messages.MessageType.DEFAULT assert message_obj.activity == messages.MessageActivity.deserialize(test_message_activity_payload) - assert message_obj.activity._app is mock_components + assert message_obj.activity._app is mock_app assert message_obj.application == mock_app assert message_obj.message_reference == messages.MessageCrosspost.deserialize(test_message_crosspost_payload) - assert message_obj.message_reference._app is mock_components + assert message_obj.message_reference._app is mock_app assert message_obj.flags == messages.MessageFlag.IS_CROSSPOST assert message_obj.nonce == "171000788183678976" @@ -316,7 +316,7 @@ def components_impl(self) -> application.Application: @pytest.fixture() def message_obj(self, components_impl): return messages.Message( - components=components_impl, + app=components_impl, id=123, channel_id=44444, guild_id=44334, diff --git a/tests/hikari/test_unset.py b/tests/hikari/models/test_unset.py similarity index 100% rename from tests/hikari/test_unset.py rename to tests/hikari/models/test_unset.py diff --git a/tests/hikari/test_users.py b/tests/hikari/models/test_users.py similarity index 85% rename from tests/hikari/test_users.py rename to tests/hikari/models/test_users.py index 02c903e972..e04b9f77cc 100644 --- a/tests/hikari/test_users.py +++ b/tests/hikari/models/test_users.py @@ -15,13 +15,14 @@ # GNU Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public License -# along ith Hikari. If not, see . +# along with Hikari. If not, see . import mock import pytest from hikari import rest -from hikari.models import users, bases from hikari.components import application +from hikari.models import bases +from hikari.models import users from hikari.internal import urls @@ -56,13 +57,13 @@ def test_oauth_user_payload(): @pytest.fixture() -def mock_components() -> application.Application: +def mock_app() -> application.Application: return mock.MagicMock(application.Application, rest=mock.AsyncMock(rest.RESTClient)) class TestUser: - def test_deserialize(self, test_user_payload, mock_components): - user_obj = users.User.deserialize(test_user_payload, components=mock_components) + def test_deserialize(self, test_user_payload, mock_app): + user_obj = users.User.deserialize(test_user_payload, app=mock_app) assert user_obj.id == 115590097100865541 assert user_obj.username == "nyaa" assert user_obj.avatar_hash == "b3b24c6d7cbcdec129d5d537067061a8" @@ -72,9 +73,9 @@ def test_deserialize(self, test_user_payload, mock_components): assert user_obj.flags == users.UserFlag.VERIFIED_BOT_DEVELOPER @pytest.fixture() - def user_obj(self, test_user_payload, mock_components): + def user_obj(self, test_user_payload, mock_app): return users.User( - components=mock_components, + app=mock_app, id=bases.Snowflake(115590097100865541), username=None, avatar_hash="b3b24c6d7cbcdec129d5d537067061a8", @@ -85,11 +86,11 @@ def user_obj(self, test_user_payload, mock_components): ) @pytest.mark.asyncio - async def test_fetch_self(self, user_obj, mock_components): + async def test_fetch_self(self, user_obj, mock_app): mock_user = mock.MagicMock(users.User) - mock_components.rest.fetch_user.return_value = mock_user + mock_app.rest.fetch_user.return_value = mock_user assert await user_obj.fetch_self() is mock_user - mock_components.rest.fetch_user.assert_called_once_with(user=115590097100865541) + mock_app.rest.fetch_user.assert_called_once_with(user=115590097100865541) def test_avatar_url(self, user_obj): mock_url = "https://cdn.discordapp.com/avatars/115590097100865541" @@ -137,8 +138,8 @@ def test_format_avatar_url_when_format_specified(self, user_obj): class TestMyUser: - def test_deserialize(self, test_oauth_user_payload, mock_components): - my_user_obj = users.MyUser.deserialize(test_oauth_user_payload, components=mock_components) + def test_deserialize(self, test_oauth_user_payload, mock_app): + my_user_obj = users.MyUser.deserialize(test_oauth_user_payload, app=mock_app) assert my_user_obj.id == 379953393319542784 assert my_user_obj.username == "qt pi" assert my_user_obj.avatar_hash == "820d0e50543216e812ad94e6ab7" @@ -151,9 +152,9 @@ def test_deserialize(self, test_oauth_user_payload, mock_components): assert my_user_obj.premium_type is users.PremiumType.NITRO_CLASSIC @pytest.fixture() - def my_user_obj(self, mock_components): + def my_user_obj(self, mock_app): return users.MyUser( - components=mock_components, + app=mock_app, id=None, username=None, avatar_hash=None, @@ -167,8 +168,8 @@ def my_user_obj(self, mock_components): ) @pytest.mark.asyncio - async def test_fetch_me(self, my_user_obj, mock_components): + async def test_fetch_me(self, my_user_obj, mock_app): mock_user = mock.MagicMock(users.MyUser) - mock_components.rest.fetch_me.return_value = mock_user + mock_app.rest.fetch_me.return_value = mock_user assert await my_user_obj.fetch_self() is mock_user - mock_components.rest.fetch_me.assert_called_once() + mock_app.rest.fetch_me.assert_called_once() diff --git a/tests/hikari/test_voices.py b/tests/hikari/models/test_voices.py similarity index 88% rename from tests/hikari/test_voices.py rename to tests/hikari/models/test_voices.py index 753003b93b..5d6a89599c 100644 --- a/tests/hikari/test_voices.py +++ b/tests/hikari/models/test_voices.py @@ -15,12 +15,12 @@ # GNU Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public License -# along ith Hikari. If not, see . +# along with Hikari. If not, see . import mock import pytest -from hikari.models import voices from hikari.components import application +from hikari.models import voices @pytest.fixture() @@ -44,13 +44,13 @@ def voice_region_payload(): @pytest.fixture() -def mock_components(): +def mock_app(): return mock.MagicMock(application.Application) class TestVoiceState: - def test_deserialize(self, voice_state_payload, mock_components): - voice_state_obj = voices.VoiceState.deserialize(voice_state_payload, components=mock_components) + def test_deserialize(self, voice_state_payload, mock_app): + voice_state_obj = voices.VoiceState.deserialize(voice_state_payload, app=mock_app) assert voice_state_obj.guild_id == 929292929292992 assert voice_state_obj.channel_id == 157733188964188161 assert voice_state_obj.user_id == 80351110224678912 @@ -63,8 +63,8 @@ def test_deserialize(self, voice_state_payload, mock_components): class TestVoiceRegion: - def test_deserialize(self, voice_region_payload, mock_components): - voice_region_obj = voices.VoiceRegion.deserialize(voice_region_payload, components=mock_components) + def test_deserialize(self, voice_region_payload, mock_app): + voice_region_obj = voices.VoiceRegion.deserialize(voice_region_payload, app=mock_app) assert voice_region_obj.id == "london" assert voice_region_obj.name == "LONDON" assert voice_region_obj.is_vip is True diff --git a/tests/hikari/test_webhook.py b/tests/hikari/models/test_webhook.py similarity index 75% rename from tests/hikari/test_webhook.py rename to tests/hikari/models/test_webhook.py index 0735651039..4e70df14f1 100644 --- a/tests/hikari/test_webhook.py +++ b/tests/hikari/models/test_webhook.py @@ -15,24 +15,30 @@ # GNU Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public License -# along ith Hikari. If not, see . +# along with Hikari. If not, see . import mock import pytest from hikari import rest -from hikari.models import embeds, messages, users, webhooks, channels, bases, files from hikari.components import application from hikari.internal import urls +from hikari.models import bases +from hikari.models import channels +from hikari.models import embeds +from hikari.models import files +from hikari.models import messages +from hikari.models import users +from hikari.models import webhooks from tests.hikari import _helpers @pytest.fixture() -def mock_components() -> application.Application: +def mock_app() -> application.Application: return mock.MagicMock(application.Application, rest=mock.AsyncMock(rest.RESTClient)) class TestWebhook: - def test_deserialize(self, mock_components): + def test_deserialize(self, mock_app): test_user_payload = {"id": "123456", "username": "hikari", "discriminator": "0000", "avatar": None} payload = { "id": "1234", @@ -49,8 +55,8 @@ def test_deserialize(self, mock_components): with _helpers.patch_marshal_attr( webhooks.Webhook, "author", deserializer=users.User.deserialize, return_value=mock_user ) as mock_user_deserializer: - webhook_obj = webhooks.Webhook.deserialize(payload, components=mock_components) - mock_user_deserializer.assert_called_once_with(test_user_payload, components=mock_components) + webhook_obj = webhooks.Webhook.deserialize(payload, app=mock_app) + mock_user_deserializer.assert_called_once_with(test_user_payload, app=mock_app) assert webhook_obj.id == 1234 assert webhook_obj.type == webhooks.WebhookType.INCOMING @@ -62,9 +68,9 @@ def test_deserialize(self, mock_components): assert webhook_obj.token == "ueoqrialsdfaKJLKfajslkdf" @pytest.fixture() - def webhook_obj(self, mock_components): + def webhook_obj(self, mock_app): return webhooks.Webhook( - components=mock_components, + app=mock_app, id=bases.Snowflake(123123), type=None, guild_id=None, @@ -76,11 +82,11 @@ def webhook_obj(self, mock_components): ) @pytest.mark.asyncio - async def test_execute_without_optionals(self, webhook_obj, mock_components): + async def test_execute_without_optionals(self, webhook_obj, mock_app): mock_webhook = mock.MagicMock(messages.Message) - mock_components.rest.execute_webhook.return_value = mock_webhook + mock_app.rest.execute_webhook.return_value = mock_webhook assert await webhook_obj.execute() is mock_webhook - mock_components.rest.execute_webhook.assert_called_once_with( + mock_app.rest.execute_webhook.assert_called_once_with( webhook=123123, webhook_token="blah.blah.blah", content=..., @@ -96,11 +102,11 @@ async def test_execute_without_optionals(self, webhook_obj, mock_components): ) @pytest.mark.asyncio - async def test_execute_with_optionals(self, webhook_obj, mock_components): + async def test_execute_with_optionals(self, webhook_obj, mock_app): mock_webhook = mock.MagicMock(messages.Message) mock_files = mock.MagicMock(files.BaseStream) mock_embed = mock.MagicMock(embeds.Embed) - mock_components.rest.execute_webhook.return_value = mock_webhook + mock_app.rest.execute_webhook.return_value = mock_webhook result = await webhook_obj.execute( content="A CONTENT", username="Name user", @@ -114,7 +120,7 @@ async def test_execute_with_optionals(self, webhook_obj, mock_components): role_mentions=[444], ) assert result is mock_webhook - mock_components.rest.execute_webhook.assert_called_once_with( + mock_app.rest.execute_webhook.assert_called_once_with( webhook=123123, webhook_token="blah.blah.blah", content="A CONTENT", @@ -136,11 +142,11 @@ async def test_execute_raises_value_error_without_token(self, webhook_obj): await webhook_obj.execute() @pytest.mark.asyncio - async def test_safe_execute_without_optionals(self, webhook_obj, mock_components): + async def test_safe_execute_without_optionals(self, webhook_obj, mock_app): mock_webhook = mock.MagicMock(messages.Message) - mock_components.rest.safe_webhook_execute = mock.AsyncMock(return_value=mock_webhook) + mock_app.rest.safe_webhook_execute = mock.AsyncMock(return_value=mock_webhook) assert await webhook_obj.safe_execute() is mock_webhook - mock_components.rest.safe_webhook_execute.assert_called_once_with( + mock_app.rest.safe_webhook_execute.assert_called_once_with( webhook=123123, webhook_token="blah.blah.blah", content=..., @@ -156,11 +162,11 @@ async def test_safe_execute_without_optionals(self, webhook_obj, mock_components ) @pytest.mark.asyncio - async def test_safe_execute_with_optionals(self, webhook_obj, mock_components): + async def test_safe_execute_with_optionals(self, webhook_obj, mock_app): mock_webhook = mock.MagicMock(messages.Message) mock_files = mock.MagicMock(files.BaseStream) mock_embed = mock.MagicMock(embeds.Embed) - mock_components.rest.safe_webhook_execute = mock.AsyncMock(return_value=mock_webhook) + mock_app.rest.safe_webhook_execute = mock.AsyncMock(return_value=mock_webhook) result = await webhook_obj.safe_execute( content="A CONTENT", username="Name user", @@ -174,7 +180,7 @@ async def test_safe_execute_with_optionals(self, webhook_obj, mock_components): role_mentions=[444], ) assert result is mock_webhook - mock_components.rest.safe_webhook_execute.assert_called_once_with( + mock_app.rest.safe_webhook_execute.assert_called_once_with( webhook=123123, webhook_token="blah.blah.blah", content="A CONTENT", @@ -196,57 +202,55 @@ async def test_safe_execute_raises_value_error_without_token(self, webhook_obj): await webhook_obj.safe_execute() @pytest.mark.asyncio - async def test_delete_with_token(self, webhook_obj, mock_components): - mock_components.rest.delete_webhook.return_value = ... + async def test_delete_with_token(self, webhook_obj, mock_app): + mock_app.rest.delete_webhook.return_value = ... assert await webhook_obj.delete() is None - mock_components.rest.delete_webhook.assert_called_once_with(webhook=123123, webhook_token="blah.blah.blah") + mock_app.rest.delete_webhook.assert_called_once_with(webhook=123123, webhook_token="blah.blah.blah") @pytest.mark.asyncio - async def test_delete_without_token(self, webhook_obj, mock_components): + async def test_delete_without_token(self, webhook_obj, mock_app): webhook_obj.token = None - mock_components.rest.delete_webhook.return_value = ... + mock_app.rest.delete_webhook.return_value = ... assert await webhook_obj.delete() is None - mock_components.rest.delete_webhook.assert_called_once_with(webhook=123123, webhook_token=...) + mock_app.rest.delete_webhook.assert_called_once_with(webhook=123123, webhook_token=...) @pytest.mark.asyncio - async def test_delete_with_use_token_set_to_true(self, webhook_obj, mock_components): - mock_components.rest.delete_webhook.return_value = ... + async def test_delete_with_use_token_set_to_true(self, webhook_obj, mock_app): + mock_app.rest.delete_webhook.return_value = ... assert await webhook_obj.delete(use_token=True) is None - mock_components.rest.delete_webhook.assert_called_once_with(webhook=123123, webhook_token="blah.blah.blah") + mock_app.rest.delete_webhook.assert_called_once_with(webhook=123123, webhook_token="blah.blah.blah") @pytest.mark.asyncio - async def test_delete_with_use_token_set_to_false(self, webhook_obj, mock_components): - mock_components.rest.delete_webhook.return_value = ... + async def test_delete_with_use_token_set_to_false(self, webhook_obj, mock_app): + mock_app.rest.delete_webhook.return_value = ... assert await webhook_obj.delete(use_token=False) is None - mock_components.rest.delete_webhook.assert_called_once_with(webhook=123123, webhook_token=...) + mock_app.rest.delete_webhook.assert_called_once_with(webhook=123123, webhook_token=...) @_helpers.assert_raises(type_=ValueError) @pytest.mark.asyncio - async def test_delete_raises_value_error_when_use_token_set_to_true_without_token( - self, webhook_obj, mock_components - ): + async def test_delete_raises_value_error_when_use_token_set_to_true_without_token(self, webhook_obj, mock_app): webhook_obj.token = None await webhook_obj.delete(use_token=True) @pytest.mark.asyncio - async def test_edit_without_optionals_nor_token(self, webhook_obj, mock_components): + async def test_edit_without_optionals_nor_token(self, webhook_obj, mock_app): webhook_obj.token = None mock_webhook = mock.MagicMock(webhooks.Webhook) - mock_components.rest.update_webhook.return_value = mock_webhook + mock_app.rest.update_webhook.return_value = mock_webhook assert await webhook_obj.edit() is mock_webhook - mock_components.rest.update_webhook.assert_called_once_with( + mock_app.rest.update_webhook.assert_called_once_with( webhook=123123, webhook_token=..., name=..., avatar=..., channel=..., reason=... ) @pytest.mark.asyncio - async def test_edit_with_optionals_and_token(self, webhook_obj, mock_components): + async def test_edit_with_optionals_and_token(self, webhook_obj, mock_app): mock_webhook = mock.MagicMock(webhooks.Webhook) mock_avatar = mock.MagicMock(files.BaseStream) mock_channel = mock.MagicMock(channels.GuildChannel) - mock_components.rest.update_webhook.return_value = mock_webhook + mock_app.rest.update_webhook.return_value = mock_webhook result = await webhook_obj.edit(name="A name man", avatar=mock_avatar, channel=mock_channel, reason="xd420") assert result is mock_webhook - mock_components.rest.update_webhook.assert_called_once_with( + mock_app.rest.update_webhook.assert_called_once_with( webhook=123123, webhook_token="blah.blah.blah", name="A name man", @@ -256,79 +260,77 @@ async def test_edit_with_optionals_and_token(self, webhook_obj, mock_components) ) @pytest.mark.asyncio - async def test_edit_with_use_token_set_to_true(self, webhook_obj, mock_components): + async def test_edit_with_use_token_set_to_true(self, webhook_obj, mock_app): mock_webhook = mock.MagicMock(webhooks.Webhook) - mock_components.rest.update_webhook.return_value = mock_webhook + mock_app.rest.update_webhook.return_value = mock_webhook assert await webhook_obj.edit(use_token=True) is mock_webhook - mock_components.rest.update_webhook.assert_called_once_with( + mock_app.rest.update_webhook.assert_called_once_with( webhook=123123, webhook_token="blah.blah.blah", name=..., avatar=..., channel=..., reason=... ) @pytest.mark.asyncio - async def test_edit_with_use_token_set_to_false(self, webhook_obj, mock_components): + async def test_edit_with_use_token_set_to_false(self, webhook_obj, mock_app): mock_webhook = mock.MagicMock(webhooks.Webhook) - mock_components.rest.update_webhook.return_value = mock_webhook + mock_app.rest.update_webhook.return_value = mock_webhook assert await webhook_obj.edit(use_token=False) is mock_webhook - mock_components.rest.update_webhook.assert_called_once_with( + mock_app.rest.update_webhook.assert_called_once_with( webhook=123123, webhook_token=..., name=..., avatar=..., channel=..., reason=... ) @_helpers.assert_raises(type_=ValueError) @pytest.mark.asyncio - async def test_edit_raises_value_error_when_use_token_set_to_true_without_token(self, webhook_obj, mock_components): + async def test_edit_raises_value_error_when_use_token_set_to_true_without_token(self, webhook_obj, mock_app): webhook_obj.token = None await webhook_obj.edit(use_token=True) @pytest.mark.asyncio - async def test_fetch_channel(self, webhook_obj, mock_components): + async def test_fetch_channel(self, webhook_obj, mock_app): webhook_obj.channel_id = bases.Snowflake(202020) mock_channel = mock.MagicMock(channels.GuildChannel) - mock_components.rest.fetch_channel.return_value = mock_channel + mock_app.rest.fetch_channel.return_value = mock_channel assert await webhook_obj.fetch_channel() is mock_channel - mock_components.rest.fetch_channel.assert_called_once_with(channel=202020) + mock_app.rest.fetch_channel.assert_called_once_with(channel=202020) @pytest.mark.asyncio - async def test_fetch_guild(self, webhook_obj, mock_components): + async def test_fetch_guild(self, webhook_obj, mock_app): webhook_obj.guild_id = bases.Snowflake(202020) mock_channel = mock.MagicMock(channels.GuildChannel) - mock_components.rest.fetch_guild.return_value = mock_channel + mock_app.rest.fetch_guild.return_value = mock_channel assert await webhook_obj.fetch_guild() is mock_channel - mock_components.rest.fetch_guild.assert_called_once_with(guild=202020) + mock_app.rest.fetch_guild.assert_called_once_with(guild=202020) @pytest.mark.asyncio - async def test_fetch_self_with_token(self, webhook_obj, mock_components): + async def test_fetch_self_with_token(self, webhook_obj, mock_app): mock_webhook = mock.MagicMock(webhooks.Webhook) - mock_components.rest.fetch_webhook.return_value = mock_webhook + mock_app.rest.fetch_webhook.return_value = mock_webhook assert await webhook_obj.fetch_self() is mock_webhook - mock_components.rest.fetch_webhook.assert_called_once_with(webhook=123123, webhook_token="blah.blah.blah") + mock_app.rest.fetch_webhook.assert_called_once_with(webhook=123123, webhook_token="blah.blah.blah") @pytest.mark.asyncio - async def test_fetch_self_without_token(self, webhook_obj, mock_components): + async def test_fetch_self_without_token(self, webhook_obj, mock_app): webhook_obj.token = None mock_webhook = mock.MagicMock(webhooks.Webhook) - mock_components.rest.fetch_webhook.return_value = mock_webhook + mock_app.rest.fetch_webhook.return_value = mock_webhook assert await webhook_obj.fetch_self() is mock_webhook - mock_components.rest.fetch_webhook.assert_called_once_with(webhook=123123, webhook_token=...) + mock_app.rest.fetch_webhook.assert_called_once_with(webhook=123123, webhook_token=...) @pytest.mark.asyncio - async def test_fetch_self_with_use_token_set_to_true(self, webhook_obj, mock_components): + async def test_fetch_self_with_use_token_set_to_true(self, webhook_obj, mock_app): mock_webhook = mock.MagicMock(webhooks.Webhook) - mock_components.rest.fetch_webhook.return_value = mock_webhook + mock_app.rest.fetch_webhook.return_value = mock_webhook assert await webhook_obj.fetch_self(use_token=True) is mock_webhook - mock_components.rest.fetch_webhook.assert_called_once_with(webhook=123123, webhook_token="blah.blah.blah") + mock_app.rest.fetch_webhook.assert_called_once_with(webhook=123123, webhook_token="blah.blah.blah") @pytest.mark.asyncio - async def test_fetch_self_with_use_token_set_to_false(self, webhook_obj, mock_components): + async def test_fetch_self_with_use_token_set_to_false(self, webhook_obj, mock_app): mock_webhook = mock.MagicMock(webhooks.Webhook) - mock_components.rest.fetch_webhook.return_value = mock_webhook + mock_app.rest.fetch_webhook.return_value = mock_webhook assert await webhook_obj.fetch_self(use_token=False) is mock_webhook - mock_components.rest.fetch_webhook.assert_called_once_with(webhook=123123, webhook_token=...) + mock_app.rest.fetch_webhook.assert_called_once_with(webhook=123123, webhook_token=...) @_helpers.assert_raises(type_=ValueError) @pytest.mark.asyncio - async def test_fetch_self_raises_value_error_when_use_token_set_to_true_without_token( - self, webhook_obj, mock_components - ): + async def test_fetch_self_raises_value_error_when_use_token_set_to_true_without_token(self, webhook_obj, mock_app): webhook_obj.token = None assert await webhook_obj.fetch_self(use_token=True) diff --git a/tests/hikari/state/__init__.py b/tests/hikari/rest/__init__.py similarity index 100% rename from tests/hikari/state/__init__.py rename to tests/hikari/rest/__init__.py diff --git a/tests/hikari/clients/test_rest/test_base.py b/tests/hikari/rest/test_base.py similarity index 85% rename from tests/hikari/clients/test_rest/test_base.py rename to tests/hikari/rest/test_base.py index aef82937cf..b0df084d85 100644 --- a/tests/hikari/clients/test_rest/test_base.py +++ b/tests/hikari/rest/test_base.py @@ -20,8 +20,9 @@ import pytest from hikari.components import application -from hikari.rest import base from hikari.internal import ratelimits +from hikari.rest import base +from hikari.rest import buckets from hikari.rest import session @@ -30,7 +31,7 @@ def low_level_rest_impl() -> session.RESTSession: return mock.MagicMock( spec=session.RESTSession, global_ratelimiter=mock.create_autospec(ratelimits.ManualRateLimiter, spec_set=True), - bucket_ratelimiters=mock.create_autospec(ratelimits.RESTBucketManager, spec_set=True), + bucket_ratelimiters=mock.create_autospec(buckets.RESTBucketManager, spec_set=True), ) @@ -67,16 +68,16 @@ def test_global_ratelimit_queue_size(self, rest_clients_impl, low_level_rest_imp def test_route_ratelimit_queue_size(self, rest_clients_impl, low_level_rest_impl): low_level_rest_impl.bucket_ratelimiters.real_hashes_to_buckets = { "aaaaa;1234;5678;9101123": mock.create_autospec( - ratelimits.RESTBucket, spec_set=True, queue=[object() for _ in range(30)] + buckets.RESTBucket, spec_set=True, queue=[object() for _ in range(30)] ), "aaaaa;1234;5678;9101122": mock.create_autospec( - ratelimits.RESTBucket, spec_set=True, queue=[object() for _ in range(29)] + buckets.RESTBucket, spec_set=True, queue=[object() for _ in range(29)] ), "aaaab;1234;5678;9101123": mock.create_autospec( - ratelimits.RESTBucket, spec_set=True, queue=[object() for _ in range(28)] + buckets.RESTBucket, spec_set=True, queue=[object() for _ in range(28)] ), "zzzzz;1234;5678;9101123": mock.create_autospec( - ratelimits.RESTBucket, spec_set=True, queue=[object() for _ in range(20)] + buckets.RESTBucket, spec_set=True, queue=[object() for _ in range(20)] ), } diff --git a/tests/hikari/rest/test_buckets.py b/tests/hikari/rest/test_buckets.py new file mode 100644 index 0000000000..36ff1059a2 --- /dev/null +++ b/tests/hikari/rest/test_buckets.py @@ -0,0 +1,309 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# Copyright © Nekoka.tt 2019-2020 +# +# This file is part of Hikari. +# +# Hikari is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Hikari is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Hikari. If not, see . +import asyncio +import datetime +import time + +import mock +import pytest + +from hikari.internal import ratelimits +from hikari.rest import buckets +from hikari.rest import routes +from tests.hikari import _helpers + + +class TestRESTBucket: + @pytest.fixture + def template(self): + return routes.Route("GET", "/foo/bar") + + @pytest.fixture + def compiled_route(self, template): + return routes.CompiledRoute(template, "/foo/bar", "1a2b3c") + + @pytest.mark.parametrize("name", ["spaghetti", ratelimits.UNKNOWN_HASH]) + def test_is_unknown(self, name, compiled_route): + with buckets.RESTBucket(name, compiled_route) as rl: + assert rl.is_unknown is (name == ratelimits.UNKNOWN_HASH) + + def test_update_rate_limit(self, compiled_route): + with buckets.RESTBucket(__name__, compiled_route) as rl: + rl.remaining = 1 + rl.limit = 2 + rl.reset_at = 3 + rl.period = 2 + + with mock.patch("time.perf_counter", return_value=4.20): + rl.update_rate_limit(9, 18, 27) + + assert rl.remaining == 9 + assert rl.limit == 18 + assert rl.reset_at == 27 + assert rl.period == 27 - 4.20 + + @pytest.mark.parametrize("name", ["spaghetti", ratelimits.UNKNOWN_HASH]) + def test_drip(self, name, compiled_route): + with buckets.RESTBucket(name, compiled_route) as rl: + rl.remaining = 1 + rl.drip() + assert rl.remaining == 0 if name != ratelimits.UNKNOWN_HASH else 1 + + +class TestRESTBucketManager: + @pytest.mark.asyncio + async def test_close_closes_all_buckets(self): + class MockBucket: + def __init__(self): + self.close = mock.MagicMock() + + buckets_array = [MockBucket() for _ in range(30)] + + mgr = buckets.RESTBucketManager() + # noinspection PyFinal + mgr.real_hashes_to_buckets = {f"blah{i}": bucket for i, bucket in enumerate(buckets_array)} + + mgr.close() + + for i, bucket in enumerate(buckets_array): + bucket.close.assert_called_once(), i + + @pytest.mark.asyncio + async def test_close_sets_closed_event(self): + mgr = buckets.RESTBucketManager() + assert not mgr.closed_event.is_set() + mgr.close() + assert mgr.closed_event.is_set() + + @pytest.mark.asyncio + async def test_start(self): + with buckets.RESTBucketManager() as mgr: + assert mgr.gc_task is None + mgr.start() + mgr.start() + mgr.start() + assert mgr.gc_task is not None + + @pytest.mark.asyncio + async def test_exit_closes(self): + with mock.patch.object(buckets.RESTBucketManager, "close") as close: + with mock.patch.object(buckets.RESTBucketManager, "gc") as gc: + with buckets.RESTBucketManager() as mgr: + mgr.start(0.01, 32) + gc.assert_called_once_with(0.01, 32) + close.assert_called() + + @pytest.mark.asyncio + async def test_gc_polls_until_closed_event_set(self): + # This is shit, but it is good shit. + with buckets.RESTBucketManager() as mgr: + mgr.start(0.01) + assert mgr.gc_task is not None + assert not mgr.gc_task.done() + await asyncio.sleep(0.1) + assert mgr.gc_task is not None + assert not mgr.gc_task.done() + await asyncio.sleep(0.1) + mgr.closed_event.set() + assert mgr.gc_task is not None + assert not mgr.gc_task.done() + task = mgr.gc_task + await asyncio.sleep(0.1) + assert mgr.gc_task is None + assert task.done() + + @pytest.mark.asyncio + async def test_gc_calls_do_pass(self): + with _helpers.unslot_class(buckets.RESTBucketManager)() as mgr: + mgr.do_gc_pass = mock.MagicMock() + mgr.start(0.01, 33) + try: + await asyncio.sleep(0.1) + mgr.do_gc_pass.assert_called_with(33) + finally: + mgr.gc_task.cancel() + + @pytest.mark.asyncio + async def test_do_gc_pass_any_buckets_that_are_empty_but_still_rate_limited_are_kept_alive(self): + with _helpers.unslot_class(buckets.RESTBucketManager)() as mgr: + bucket = mock.MagicMock() + bucket.is_empty = True + bucket.is_unknown = False + bucket.reset_at = time.perf_counter() + 999999999999999999999999999 + + mgr.real_hashes_to_buckets["foobar"] = bucket + + mgr.do_gc_pass(0) + + assert "foobar" in mgr.real_hashes_to_buckets + bucket.close.assert_not_called() + + @pytest.mark.asyncio + async def test_do_gc_pass_any_buckets_that_are_empty_but_not_rate_limited_and_not_expired_are_kept_alive(self): + with _helpers.unslot_class(buckets.RESTBucketManager)() as mgr: + bucket = mock.MagicMock() + bucket.is_empty = True + bucket.is_unknown = False + bucket.reset_at = time.perf_counter() + + mgr.real_hashes_to_buckets["foobar"] = bucket + + mgr.do_gc_pass(10) + + assert "foobar" in mgr.real_hashes_to_buckets + bucket.close.assert_not_called() + + @pytest.mark.asyncio + async def test_do_gc_pass_any_buckets_that_are_empty_but_not_rate_limited_and_expired_are_closed(self): + with _helpers.unslot_class(buckets.RESTBucketManager)() as mgr: + bucket = mock.MagicMock() + bucket.is_empty = True + bucket.is_unknown = False + bucket.reset_at = time.perf_counter() - 999999999999999999999999999 + + mgr.real_hashes_to_buckets["foobar"] = bucket + + mgr.do_gc_pass(0) + + assert "foobar" not in mgr.real_hashes_to_buckets + bucket.close.assert_called_once() + + @pytest.mark.asyncio + async def test_do_gc_pass_any_buckets_that_are_not_empty_are_kept_alive(self): + with _helpers.unslot_class(buckets.RESTBucketManager)() as mgr: + bucket = mock.MagicMock() + bucket.is_empty = False + bucket.is_unknown = True + bucket.reset_at = time.perf_counter() + + mgr.real_hashes_to_buckets["foobar"] = bucket + + mgr.do_gc_pass(0) + + assert "foobar" in mgr.real_hashes_to_buckets + bucket.close.assert_not_called() + + @pytest.mark.asyncio + async def test_acquire_route_when_not_in_routes_to_real_hashes_makes_new_bucket_using_initial_hash(self): + with buckets.RESTBucketManager() as mgr: + route = mock.MagicMock() + route.create_real_bucket_hash = mock.MagicMock(wraps=lambda intial_hash: intial_hash + ";bobs") + + # This isn't a coroutine; why would I await it? + # noinspection PyAsyncCall + mgr.acquire(route) + + assert "UNKNOWN;bobs" in mgr.real_hashes_to_buckets + assert isinstance(mgr.real_hashes_to_buckets["UNKNOWN;bobs"], buckets.RESTBucket) + + @pytest.mark.asyncio + async def test_acquire_route_when_not_in_routes_to_real_hashes_caches_route(self): + with buckets.RESTBucketManager() as mgr: + route = mock.MagicMock() + route.create_real_bucket_hash = mock.MagicMock(wraps=lambda intial_hash: intial_hash + ";bobs") + + # This isn't a coroutine; why would I await it? + # noinspection PyAsyncCall + mgr.acquire(route) + + assert mgr.routes_to_hashes[route.route] == "UNKNOWN" + + @pytest.mark.asyncio + async def test_acquire_route_when_route_cached_already_obtains_hash_from_route_and_bucket_from_hash(self): + with buckets.RESTBucketManager() as mgr: + route = mock.MagicMock() + route.create_real_bucket_hash = mock.MagicMock(return_value="eat pant;1234") + bucket = mock.MagicMock() + mgr.routes_to_hashes[route] = "eat pant" + mgr.real_hashes_to_buckets["eat pant;1234"] = bucket + + # This isn't a coroutine; why would I await it? + # noinspection PyAsyncCall + mgr.acquire(route) + + # yes i test this twice, sort of. no, there isn't another way to verify this. sue me. + bucket.acquire.assert_called_once() + + @pytest.mark.asyncio + async def test_acquire_route_returns_acquired_future(self): + with buckets.RESTBucketManager() as mgr: + route = mock.MagicMock() + + bucket = mock.MagicMock() + with mock.patch.object(buckets, "RESTBucket", return_value=bucket): + route.create_real_bucket_hash = mock.MagicMock(wraps=lambda intial_hash: intial_hash + ";bobs") + + f = mgr.acquire(route) + assert f is bucket.acquire() + + @pytest.mark.asyncio + async def test_acquire_route_returns_acquired_future_for_new_bucket(self): + with buckets.RESTBucketManager() as mgr: + route = mock.MagicMock() + route.create_real_bucket_hash = mock.MagicMock(return_value="eat pant;bobs") + bucket = mock.MagicMock() + mgr.routes_to_hashes[route.route] = "eat pant" + mgr.real_hashes_to_buckets["eat pant;bobs"] = bucket + + f = mgr.acquire(route) + assert f is bucket.acquire() + + @pytest.mark.asyncio + async def test_update_rate_limits_if_wrong_bucket_hash_reroutes_route(self): + with buckets.RESTBucketManager() as mgr: + route = mock.MagicMock() + route.create_real_bucket_hash = mock.MagicMock(wraps=lambda intial_hash: intial_hash + ";bobs") + mgr.routes_to_hashes[route.route] = "123" + mgr.update_rate_limits(route, "blep", 22, 23, datetime.datetime.now(), datetime.datetime.now()) + assert mgr.routes_to_hashes[route.route] == "blep" + assert isinstance(mgr.real_hashes_to_buckets["blep;bobs"], buckets.RESTBucket) + + @pytest.mark.asyncio + async def test_update_rate_limits_if_right_bucket_hash_does_nothing_to_hash(self): + with buckets.RESTBucketManager() as mgr: + route = mock.MagicMock() + route.create_real_bucket_hash = mock.MagicMock(wraps=lambda intial_hash: intial_hash + ";bobs") + mgr.routes_to_hashes[route.route] = "123" + bucket = mock.MagicMock() + mgr.real_hashes_to_buckets["123;bobs"] = bucket + mgr.update_rate_limits(route, "123", 22, 23, datetime.datetime.now(), datetime.datetime.now()) + assert mgr.routes_to_hashes[route.route] == "123" + assert mgr.real_hashes_to_buckets["123;bobs"] is bucket + + @pytest.mark.asyncio + async def test_update_rate_limits_updates_params(self): + with buckets.RESTBucketManager() as mgr: + route = mock.MagicMock() + route.create_real_bucket_hash = mock.MagicMock(wraps=lambda intial_hash: intial_hash + ";bobs") + mgr.routes_to_hashes[route.route] = "123" + bucket = mock.MagicMock() + mgr.real_hashes_to_buckets["123;bobs"] = bucket + date = datetime.datetime.now().replace(year=2004) + reset_at = datetime.datetime.now() + + with mock.patch("time.perf_counter", return_value=27): + expect_reset_at_monotonic = 27 + (reset_at - date).total_seconds() + mgr.update_rate_limits(route, "123", 22, 23, date, reset_at) + bucket.update_rate_limit.assert_called_once_with(22, 23, expect_reset_at_monotonic) + + @pytest.mark.parametrize(("gc_task", "is_started"), [(None, False), (object(), True)]) + def test_is_started(self, gc_task, is_started): + with buckets.RESTBucketManager() as mgr: + mgr.gc_task = gc_task + assert mgr.is_started is is_started diff --git a/tests/hikari/clients/test_rest/test_channel.py b/tests/hikari/rest/test_channel.py similarity index 93% rename from tests/hikari/clients/test_rest/test_channel.py rename to tests/hikari/rest/test_channel.py index d396ab33a1..a6c07ca4b3 100644 --- a/tests/hikari/clients/test_rest/test_channel.py +++ b/tests/hikari/rest/test_channel.py @@ -24,6 +24,8 @@ import mock import pytest +from hikari.components import application +from hikari.internal import helpers from hikari.models import bases from hikari.models import channels from hikari.models import embeds @@ -33,9 +35,7 @@ from hikari.models import messages from hikari.models import users from hikari.models import webhooks -from hikari.components import application from hikari.rest import channel -from hikari.internal import helpers from hikari.rest import session from tests.hikari import _helpers @@ -47,7 +47,7 @@ def mock_session(self): return mock.MagicMock(spec_set=session.RESTSession) @pytest.fixture - def mock_components(self): + def mock_app(self): return mock.MagicMock(spec_set=application.Application) @pytest.fixture @@ -56,34 +56,34 @@ def message_cls(self): yield message_cls @pytest.mark.parametrize("direction", ["before", "after", "around"]) - def test_init_first_id_is_date(self, mock_session, mock_components, direction): + def test_init_first_id_is_date(self, mock_session, mock_app, direction): date = datetime.datetime(2015, 11, 15, 23, 13, 46, 709000, tzinfo=datetime.timezone.utc) expected_id = 115590097100865536 channel_id = 1234567 - pag = channel._MessagePaginator(channel_id, direction, date, mock_components, mock_session) + pag = channel._MessagePaginator(mock_app, channel_id, direction, date, mock_session) assert pag._first_id == str(expected_id) assert pag._channel_id == str(channel_id) assert pag._direction == direction assert pag._session is mock_session - assert pag._components is mock_components + assert pag._app is mock_app @pytest.mark.parametrize("direction", ["before", "after", "around"]) - def test_init_first_id_is_id(self, mock_session, mock_components, direction): + def test_init_first_id_is_id(self, mock_session, mock_app, direction): expected_id = 115590097100865536 channel_id = 1234567 - pag = channel._MessagePaginator(channel_id, direction, expected_id, mock_components, mock_session) + pag = channel._MessagePaginator(mock_app, channel_id, direction, expected_id, mock_session) assert pag._first_id == str(expected_id) assert pag._channel_id == str(channel_id) assert pag._direction == direction assert pag._session is mock_session - assert pag._components is mock_components + assert pag._app is mock_app @pytest.mark.parametrize("direction", ["before", "after", "around"]) - async def test_next_chunk_makes_api_call(self, mock_session, mock_components, message_cls, direction): + async def test_next_chunk_makes_api_call(self, mock_session, mock_app, message_cls, direction): channel_obj = mock.MagicMock(__int__=lambda _: 55) mock_session.get_channel_messages = mock.AsyncMock(return_value=[]) - pag = channel._MessagePaginator(channel_obj, direction, "12345", mock_components, mock_session) + pag = channel._MessagePaginator(mock_app, channel_obj, direction, "12345", mock_session) pag._first_id = "12345" await pag._next_chunk() @@ -93,10 +93,10 @@ async def test_next_chunk_makes_api_call(self, mock_session, mock_components, me ) @pytest.mark.parametrize("direction", ["before", "after", "around"]) - async def test_next_chunk_empty_response_returns_None(self, mock_session, mock_components, message_cls, direction): + async def test_next_chunk_empty_response_returns_None(self, mock_session, mock_app, message_cls, direction): channel_obj = mock.MagicMock(__int__=lambda _: 55) - pag = channel._MessagePaginator(channel_obj, direction, "12345", mock_components, mock_session) + pag = channel._MessagePaginator(mock_app, channel_obj, direction, "12345", mock_session) pag._first_id = "12345" mock_session.get_channel_messages = mock.AsyncMock(return_value=[]) @@ -104,9 +104,7 @@ async def test_next_chunk_empty_response_returns_None(self, mock_session, mock_c assert await pag._next_chunk() is None @pytest.mark.parametrize(["direction", "expect_reverse"], [("before", False), ("after", True), ("around", False)]) - async def test_next_chunk_updates_first_id( - self, mock_session, mock_components, message_cls, expect_reverse, direction - ): + async def test_next_chunk_updates_first_id(self, mock_session, mock_app, message_cls, expect_reverse, direction): return_payload = [ {"id": "1234", ...: ...}, {"id": "3456", ...: ...}, @@ -118,7 +116,7 @@ async def test_next_chunk_updates_first_id( channel_obj = mock.MagicMock(__int__=lambda _: 99) - pag = channel._MessagePaginator(channel_obj, direction, "12345", mock_components, mock_session) + pag = channel._MessagePaginator(mock_app, channel_obj, direction, "12345", mock_session) pag._first_id = "12345" await pag._next_chunk() @@ -126,9 +124,7 @@ async def test_next_chunk_updates_first_id( assert pag._first_id == "1234" if expect_reverse else "512" @pytest.mark.parametrize(["direction", "expect_reverse"], [("before", False), ("after", True), ("around", False)]) - async def test_next_chunk_returns_generator( - self, mock_session, mock_components, message_cls, expect_reverse, direction - ): + async def test_next_chunk_returns_generator(self, mock_session, mock_app, message_cls, expect_reverse, direction): return_payload = [ {"id": "1234", ...: ...}, {"id": "3456", ...: ...}, @@ -157,7 +153,7 @@ class DummyResponse: channel_obj = mock.MagicMock(__int__=lambda _: 99) - pag = channel._MessagePaginator(channel_obj, direction, "12345", mock_components, mock_session) + pag = channel._MessagePaginator(mock_app, channel_obj, direction, "12345", mock_session) pag._first_id = "12345" generator = await pag._next_chunk() @@ -182,12 +178,12 @@ class DummyResponse: class TestRESTChannel: @pytest.fixture() def rest_channel_logic_impl(self): - mock_components = mock.MagicMock(application.Application) + mock_app = mock.MagicMock(application.Application) mock_low_level_restful_client = mock.MagicMock(session.RESTSession) class RESTChannelLogicImpl(channel.RESTChannelComponent): def __init__(self): - super().__init__(mock_components, mock_low_level_restful_client) + super().__init__(mock_app, mock_low_level_restful_client) return RESTChannelLogicImpl() @@ -200,9 +196,7 @@ async def test_fetch_channel(self, rest_channel_logic_impl, channel): with mock.patch.object(channels, "deserialize_channel", return_value=mock_channel_obj): assert await rest_channel_logic_impl.fetch_channel(channel) is mock_channel_obj rest_channel_logic_impl._session.get_channel.assert_called_once_with(channel_id="1234") - channels.deserialize_channel.assert_called_once_with( - mock_payload, components=rest_channel_logic_impl._components - ) + channels.deserialize_channel.assert_called_once_with(mock_payload, app=rest_channel_logic_impl._app) @pytest.mark.asyncio @_helpers.parametrize_valid_id_formats_for_models("channel", 379953393319542784, channels.PartialChannel) @@ -246,9 +240,7 @@ async def test_update_channel_with_optionals( reason="Get Nyaa'd.", ) mock_overwrite_obj.serialize.assert_called_once() - channels.deserialize_channel.assert_called_once_with( - mock_payload, components=rest_channel_logic_impl._components - ) + channels.deserialize_channel.assert_called_once_with(mock_payload, app=rest_channel_logic_impl._app) @pytest.mark.asyncio @_helpers.parametrize_valid_id_formats_for_models("channel", 379953393319542784, channels.PartialChannel) @@ -274,9 +266,7 @@ async def test_update_channel_without_optionals( parent_id=..., reason=..., ) - channels.deserialize_channel.assert_called_once_with( - mock_payload, components=rest_channel_logic_impl._components - ) + channels.deserialize_channel.assert_called_once_with(mock_payload, app=rest_channel_logic_impl._app) @pytest.mark.asyncio @_helpers.parametrize_valid_id_formats_for_models("channel", 55555, channels.PartialChannel) @@ -309,7 +299,7 @@ def test_fetch_messages(self, rest_channel_logic_impl, direction, expected_direc channel=mock_channel, direction=expected_direction, first=expected_first, - components=rest_channel_logic_impl._components, + app=rest_channel_logic_impl._app, session=rest_channel_logic_impl._session, ) @@ -338,9 +328,7 @@ async def test_fetch_message(self, rest_channel_logic_impl, channel, message): rest_channel_logic_impl._session.get_channel_message.assert_called_once_with( channel_id="55555", message_id="565656", ) - messages.Message.deserialize.assert_called_once_with( - mock_payload, components=rest_channel_logic_impl._components - ) + messages.Message.deserialize.assert_called_once_with(mock_payload, app=rest_channel_logic_impl._app) @pytest.mark.asyncio @_helpers.parametrize_valid_id_formats_for_models("channel", 694463529998352394, channels.PartialChannel) @@ -373,9 +361,7 @@ async def test_create_message_with_optionals(self, rest_channel_logic_impl, chan role_mentions=False, ) assert result is mock_message_obj - messages.Message.deserialize.assert_called_once_with( - mock_message_payload, components=rest_channel_logic_impl._components - ) + messages.Message.deserialize.assert_called_once_with(mock_message_payload, app=rest_channel_logic_impl._app) helpers.generate_allowed_mentions.assert_called_once_with( mentions_everyone=False, user_mentions=False, role_mentions=False ) @@ -404,9 +390,7 @@ async def test_create_message_without_optionals(self, rest_channel_logic_impl, c stack.enter_context(mock.patch.object(messages.Message, "deserialize", return_value=mock_message_obj)) with stack: assert await rest_channel_logic_impl.create_message(channel) is mock_message_obj - messages.Message.deserialize.assert_called_once_with( - mock_message_payload, components=rest_channel_logic_impl._components - ) + messages.Message.deserialize.assert_called_once_with(mock_message_payload, app=rest_channel_logic_impl._app) helpers.generate_allowed_mentions.assert_called_once_with( mentions_everyone=True, user_mentions=True, role_mentions=True ) @@ -508,9 +492,7 @@ async def test_update_message_with_optionals(self, rest_channel_logic_impl, mess allowed_mentions=mock_allowed_mentions_payload, ) mock_embed.serialize.assert_called_once() - messages.Message.deserialize.assert_called_once_with( - mock_payload, components=rest_channel_logic_impl._components - ) + messages.Message.deserialize.assert_called_once_with(mock_payload, app=rest_channel_logic_impl._app) helpers.generate_allowed_mentions.assert_called_once_with( mentions_everyone=False, role_mentions=False, user_mentions=[123123123] ) @@ -538,9 +520,7 @@ async def test_update_message_without_optionals(self, rest_channel_logic_impl, m flags=..., allowed_mentions=mock_allowed_mentions_payload, ) - messages.Message.deserialize.assert_called_once_with( - mock_payload, components=rest_channel_logic_impl._components - ) + messages.Message.deserialize.assert_called_once_with(mock_payload, app=rest_channel_logic_impl._app) helpers.generate_allowed_mentions.assert_called_once_with( mentions_everyone=True, user_mentions=True, role_mentions=True ) @@ -698,7 +678,7 @@ async def test_fetch_invites_for_channel(self, rest_channel_logic_impl, channel) assert await rest_channel_logic_impl.fetch_invites_for_channel(channel=channel) == [mock_invite_obj] rest_channel_logic_impl._session.get_channel_invites.assert_called_once_with(channel_id="123123123") invites.InviteWithMetadata.deserialize.assert_called_once_with( - mock_invite_payload, components=rest_channel_logic_impl._components + mock_invite_payload, app=rest_channel_logic_impl._app ) @pytest.mark.asyncio @@ -732,7 +712,7 @@ async def test_create_invite_for_channel_with_optionals(self, rest_channel_logic reason="Hello there.", ) invites.InviteWithMetadata.deserialize.assert_called_once_with( - mock_invite_payload, components=rest_channel_logic_impl._components + mock_invite_payload, app=rest_channel_logic_impl._app ) @pytest.mark.asyncio @@ -754,7 +734,7 @@ async def test_create_invite_for_channel_without_optionals(self, rest_channel_lo reason=..., ) invites.InviteWithMetadata.deserialize.assert_called_once_with( - mock_invite_payload, components=rest_channel_logic_impl._components + mock_invite_payload, app=rest_channel_logic_impl._app ) @pytest.mark.asyncio @@ -802,9 +782,7 @@ async def test_fetch_pins(self, rest_channel_logic_impl, channel): with mock.patch.object(messages.Message, "deserialize", return_value=mock_message_obj): assert await rest_channel_logic_impl.fetch_pins(channel) == {21232: mock_message_obj} rest_channel_logic_impl._session.get_pinned_messages.assert_called_once_with(channel_id="123123123") - messages.Message.deserialize.assert_called_once_with( - mock_message_payload, components=rest_channel_logic_impl._components - ) + messages.Message.deserialize.assert_called_once_with(mock_message_payload, app=rest_channel_logic_impl._app) @pytest.mark.asyncio @_helpers.parametrize_valid_id_formats_for_models("channel", 292929, channels.PartialChannel) @@ -843,9 +821,7 @@ async def test_create_webhook_with_optionals(self, rest_channel_logic_impl, chan ) assert result is mock_webhook_obj mock_image_obj.read.assert_awaited_once() - webhooks.Webhook.deserialize.assert_called_once_with( - mock_webhook_payload, components=rest_channel_logic_impl._components - ) + webhooks.Webhook.deserialize.assert_called_once_with(mock_webhook_payload, app=rest_channel_logic_impl._app) rest_channel_logic_impl._session.create_webhook.assert_called_once_with( channel_id="115590097100865541", name="aWebhook", avatar=mock_image_data, reason="And a webhook is born." ) @@ -858,9 +834,7 @@ async def test_create_webhook_without_optionals(self, rest_channel_logic_impl, c rest_channel_logic_impl._session.create_webhook.return_value = mock_webhook_payload with mock.patch.object(webhooks.Webhook, "deserialize", return_value=mock_webhook_obj): assert await rest_channel_logic_impl.create_webhook(channel=channel, name="aWebhook") is mock_webhook_obj - webhooks.Webhook.deserialize.assert_called_once_with( - mock_webhook_payload, components=rest_channel_logic_impl._components - ) + webhooks.Webhook.deserialize.assert_called_once_with(mock_webhook_payload, app=rest_channel_logic_impl._app) rest_channel_logic_impl._session.create_webhook.assert_called_once_with( channel_id="115590097100865541", name="aWebhook", avatar=..., reason=... ) @@ -876,6 +850,4 @@ async def test_fetch_channel_webhooks(self, rest_channel_logic_impl, channel): rest_channel_logic_impl._session.get_channel_webhooks.assert_called_once_with( channel_id="115590097100865541" ) - webhooks.Webhook.deserialize.assert_called_once_with( - mock_webhook_payload, components=rest_channel_logic_impl._components - ) + webhooks.Webhook.deserialize.assert_called_once_with(mock_webhook_payload, app=rest_channel_logic_impl._app) diff --git a/tests/hikari/clients/test_rest/test___init__.py b/tests/hikari/rest/test_client.py similarity index 77% rename from tests/hikari/clients/test_rest/test___init__.py rename to tests/hikari/rest/test_client.py index aa2a5e8faa..c43678c6d2 100644 --- a/tests/hikari/clients/test_rest/test___init__.py +++ b/tests/hikari/rest/test_client.py @@ -21,10 +21,20 @@ import mock import pytest -from hikari.components import application from hikari import configs from hikari import rest as high_level_rest +from hikari.components import application +from hikari.rest import channel +from hikari.rest import gateway +from hikari.rest import guild +from hikari.rest import invite +from hikari.rest import me +from hikari.rest import oauth2 +from hikari.rest import react from hikari.rest import session as low_level_rest +from hikari.rest import user +from hikari.rest import voice +from hikari.rest import webhook class TestRESTClient: @@ -38,10 +48,10 @@ class TestRESTClient: ) def test_init(self, token, token_type, expected_token): mock_config = configs.RESTConfig(token=token, token_type=token_type, trust_env=True) - mock_components = mock.MagicMock(application.Application, config=mock_config) + mock_app = mock.MagicMock(application.Application, config=mock_config) mock_low_level_rest_clients = mock.MagicMock(low_level_rest.RESTSession) with mock.patch.object(low_level_rest, "RESTSession", return_value=mock_low_level_rest_clients) as patched_init: - client = high_level_rest.RESTClient(mock_components) + client = high_level_rest.RESTClient(mock_app) patched_init.assert_called_once_with( allow_redirects=mock_config.allow_redirects, base_url=mock_config.rest_url, @@ -57,22 +67,22 @@ def test_init(self, token, token_type, expected_token): version=mock_config.rest_version, ) assert client._session is mock_low_level_rest_clients - assert client._app is mock_components + assert client._app is mock_app def test_inheritance(self): for attr, routine in ( member for component in [ - hikari.rest.channel.RESTChannelComponent, - hikari.rest.me.RESTCurrentUserComponent, - hikari.rest.gateway.RESTGatewayComponent, - hikari.rest.guild.RESTGuildComponent, - hikari.rest.invite.RESTInviteComponent, - hikari.rest.oauth2.RESTOAuth2Component, - hikari.rest.react.RESTReactionComponent, - hikari.rest.user.RESTUserComponent, - hikari.rest.voice.RESTVoiceComponent, - hikari.rest.webhook.RESTWebhookComponent, + channel.RESTChannelComponent, + me.RESTCurrentUserComponent, + gateway.RESTGatewayComponent, + guild.RESTGuildComponent, + invite.RESTInviteComponent, + oauth2.RESTOAuth2Component, + react.RESTReactionComponent, + user.RESTUserComponent, + voice.RESTVoiceComponent, + webhook.RESTWebhookComponent, ] for member in inspect.getmembers(component, inspect.isroutine) ): diff --git a/tests/hikari/clients/test_rest/test_gateway.py b/tests/hikari/rest/test_gateway.py similarity index 80% rename from tests/hikari/clients/test_rest/test_gateway.py rename to tests/hikari/rest/test_gateway.py index afb66369bf..52415b532a 100644 --- a/tests/hikari/clients/test_rest/test_gateway.py +++ b/tests/hikari/rest/test_gateway.py @@ -20,8 +20,8 @@ import mock import pytest -from hikari.models import gateway from hikari.components import application +from hikari.models import gateway as gateway_models from hikari.rest import gateway from hikari.rest import session @@ -29,12 +29,12 @@ class TestRESTReactionLogic: @pytest.fixture() def rest_gateway_logic_impl(self): - mock_components = mock.MagicMock(application.Application) + mock_app = mock.MagicMock(application.Application) mock_low_level_restful_client = mock.MagicMock(session.RESTSession) class RESTGatewayLogicImpl(gateway.RESTGatewayComponent): def __init__(self): - super().__init__(mock_components, mock_low_level_restful_client) + super().__init__(mock_app, mock_low_level_restful_client) return RESTGatewayLogicImpl() @@ -48,11 +48,11 @@ async def test_fetch_gateway_url(self, rest_gateway_logic_impl): @pytest.mark.asyncio async def test_fetch_gateway_bot(self, rest_gateway_logic_impl): mock_payload = {"url": "wss://gateway.discord.gg/", "shards": 9, "session_start_limit": {}} - mock_gateway_bot_obj = mock.MagicMock(gateway.GatewayBot) + mock_gateway_bot_obj = mock.MagicMock(gateway_models.GatewayBot) rest_gateway_logic_impl._session.get_gateway_bot.return_value = mock_payload - with mock.patch.object(gateway.GatewayBot, "deserialize", return_value=mock_gateway_bot_obj): + with mock.patch.object(gateway_models.GatewayBot, "deserialize", return_value=mock_gateway_bot_obj): assert await rest_gateway_logic_impl.fetch_gateway_bot() is mock_gateway_bot_obj rest_gateway_logic_impl._session.get_gateway_bot.assert_called_once() - gateway.GatewayBot.deserialize.assert_called_once_with( - mock_payload, components=rest_gateway_logic_impl._components + gateway_models.GatewayBot.deserialize.assert_called_once_with( + mock_payload, app=rest_gateway_logic_impl._app ) diff --git a/tests/hikari/clients/test_rest/test_guild.py b/tests/hikari/rest/test_guild.py similarity index 93% rename from tests/hikari/clients/test_rest/test_guild.py rename to tests/hikari/rest/test_guild.py index e7f9f28586..9f80e9ab0a 100644 --- a/tests/hikari/clients/test_rest/test_guild.py +++ b/tests/hikari/rest/test_guild.py @@ -23,6 +23,7 @@ import mock import pytest +from hikari.components import application from hikari.models import audit_logs from hikari.models import bases from hikari.models import channels @@ -35,7 +36,6 @@ from hikari.models import users from hikari.models import voices from hikari.models import webhooks -from hikari.components import application from hikari.rest import guild as _guild from hikari.rest import session from tests.hikari import _helpers @@ -47,7 +47,7 @@ def mock_session(self): return mock.MagicMock(spec_set=session.RESTSession) @pytest.fixture() - def mock_components(self): + def mock_app(self): return mock.MagicMock(spec_set=application.Application) @pytest.fixture() @@ -55,12 +55,12 @@ def member_cls(self): with mock.patch.object(guilds, "GuildMember") as member_cls: yield member_cls - def test_init_no_start_bounds(self, mock_session, mock_components): + def test_init_no_start_bounds(self, mock_session, mock_app): guild = mock.MagicMock(__int__=lambda _: 22) - pag = _guild._MemberPaginator(guild, None, mock_components, mock_session) + pag = _guild._MemberPaginator(mock_app, guild, None, mock_session) assert pag._first_id == "0" assert pag._guild_id == "22" - assert pag._components is mock_components + assert pag._app is mock_app assert pag._session is mock_session @pytest.mark.parametrize( @@ -73,18 +73,18 @@ def test_init_no_start_bounds(self, mock_session, mock_components): (datetime.datetime(2019, 1, 22, 18, 41, 15, 283000, tzinfo=datetime.timezone.utc), "537340989807788032"), ], ) - def test_init_with_start_bounds(self, mock_session, mock_components, start_at, expected): + def test_init_with_start_bounds(self, mock_session, mock_app, start_at, expected): guild = mock.MagicMock(__int__=lambda _: 25) - pag = _guild._MemberPaginator(guild, start_at, mock_components, mock_session) + pag = _guild._MemberPaginator(mock_app, guild, start_at, mock_session) assert pag._first_id == expected assert pag._guild_id == "25" - assert pag._components is mock_components + assert pag._app is mock_app assert pag._session is mock_session @pytest.mark.asyncio - async def test_next_chunk_performs_correct_api_call(self, mock_session, mock_components, member_cls): + async def test_next_chunk_performs_correct_api_call(self, mock_session, mock_app, member_cls): guild = mock.MagicMock(__int__=lambda _: 34) - pag = _guild._MemberPaginator(guild, None, mock_components, mock_session) + pag = _guild._MemberPaginator(mock_app, guild, None, mock_session) pag._first_id = "123456" await pag._next_chunk() @@ -92,15 +92,15 @@ async def test_next_chunk_performs_correct_api_call(self, mock_session, mock_com mock_session.list_guild_members.assert_awaited_once_with("34", after="123456") @pytest.mark.asyncio - async def test_next_chunk_when_empty_returns_None(self, mock_session, mock_components, member_cls): + async def test_next_chunk_when_empty_returns_None(self, mock_session, mock_app, member_cls): mock_session.list_guild_members = mock.AsyncMock(return_value=[]) guild = mock.MagicMock(__int__=lambda _: 36) - pag = _guild._MemberPaginator(guild, None, mock_components, mock_session) + pag = _guild._MemberPaginator(mock_app, guild, None, mock_session) assert await pag._next_chunk() is None @pytest.mark.asyncio - async def test_next_chunk_updates_first_id_to_last_item(self, mock_session, mock_components, member_cls): + async def test_next_chunk_updates_first_id_to_last_item(self, mock_session, mock_app, member_cls): return_payload = [ {"id": "1234", ...: ...}, {"id": "3456", ...: ...}, @@ -111,16 +111,16 @@ async def test_next_chunk_updates_first_id_to_last_item(self, mock_session, mock mock_session.list_guild_members = mock.AsyncMock(return_value=return_payload) guild = mock.MagicMock(__int__=lambda _: 42) - pag = _guild._MemberPaginator(guild, None, mock_components, mock_session) + pag = _guild._MemberPaginator(mock_app, guild, None, mock_session) await pag._next_chunk() assert pag._first_id == "512" @pytest.mark.asyncio - async def test_next_chunk_deserializes_payload_in_generator_lazily(self, mock_session, mock_components, member_cls): + async def test_next_chunk_deserializes_payload_in_generator_lazily(self, mock_session, mock_app, member_cls): guild = mock.MagicMock(__int__=lambda _: 69) - pag = _guild._MemberPaginator(guild, None, mock_components, mock_session) + pag = _guild._MemberPaginator(mock_app, guild, None, mock_session) return_payload = [ {"id": "1234", ...: ...}, @@ -151,7 +151,7 @@ async def test_next_chunk_deserializes_payload_in_generator_lazily(self, mock_se for i, input_payload in enumerate(return_payload): expected_value = real_values[i] assert next(generator) is expected_value - member_cls.deserialize.assert_called_with(input_payload, components=mock_components) + member_cls.deserialize.assert_called_with(input_payload, app=mock_app) # Clear the generator result. # This doesn't test anything, but there is an issue with coverage not detecting generator @@ -167,12 +167,12 @@ async def test_next_chunk_deserializes_payload_in_generator_lazily(self, mock_se class TestRESTGuildLogic: @pytest.fixture() def rest_guild_logic_impl(self): - mock_components = mock.MagicMock(application.Application) + mock_app = mock.MagicMock(application.Application) mock_low_level_restful_client = mock.MagicMock(session.RESTSession) class RESTGuildLogicImpl(_guild.RESTGuildComponent): def __init__(self): - super().__init__(mock_components, mock_low_level_restful_client) + super().__init__(mock_app, mock_low_level_restful_client) return RESTGuildLogicImpl() @@ -199,7 +199,7 @@ async def test_fetch_audit_log_entries_before_with_optionals_integration( assert entry is mock_entry break audit_logs.AuditLogEntry.deserialize.assert_called_once_with( - mock_entry_payload, components=rest_guild_logic_impl._components + mock_entry_payload, app=rest_guild_logic_impl._app ) rest_guild_logic_impl._session.get_guild_audit_log.assert_called_once_with( guild_id="379953393319542784", user_id="22222222", action_type=26, before="9223372036854775807", limit=42 @@ -222,7 +222,7 @@ async def test_fetch_audit_log_entries_before_without_optionals_integration(self assert entry is mock_entry break audit_logs.AuditLogEntry.deserialize.assert_called_once_with( - mock_entry_payload, components=rest_guild_logic_impl._components + mock_entry_payload, app=rest_guild_logic_impl._app ) rest_guild_logic_impl._session.get_guild_audit_log.assert_called_once_with( guild_id="379953393319542784", user_id=..., action_type=..., before="9223372036854775807", limit=100 @@ -245,7 +245,7 @@ async def test_fetch_audit_log_entries_before_with_datetime_object_integration(s assert entry is mock_entry break audit_logs.AuditLogEntry.deserialize.assert_called_once_with( - mock_entry_payload, components=rest_guild_logic_impl._components + mock_entry_payload, app=rest_guild_logic_impl._app ) rest_guild_logic_impl._session.get_guild_audit_log.assert_called_once_with( guild_id="123123123", user_id=..., action_type=..., before="537340989807788032", limit=100 @@ -272,7 +272,7 @@ async def test_fetch_audit_log_with_optionals(self, rest_guild_logic_impl, guild before="1231231123", ) audit_logs.AuditLog.deserialize.assert_called_once_with( - mock_audit_log_payload, components=rest_guild_logic_impl._components + mock_audit_log_payload, app=rest_guild_logic_impl._app ) @pytest.mark.asyncio @@ -287,7 +287,7 @@ async def test_fetch_audit_log_without_optionals(self, rest_guild_logic_impl, gu guild_id="379953393319542784", user_id=..., action_type=..., limit=..., before=... ) audit_logs.AuditLog.deserialize.assert_called_once_with( - mock_audit_log_payload, components=rest_guild_logic_impl._components + mock_audit_log_payload, app=rest_guild_logic_impl._app ) @pytest.mark.asyncio @@ -303,7 +303,7 @@ async def test_fetch_audit_log_handles_datetime_object(self, rest_guild_logic_im guild_id="379953393319542784", user_id=..., action_type=..., limit=..., before="537340989807788032" ) audit_logs.AuditLog.deserialize.assert_called_once_with( - mock_audit_log_payload, components=rest_guild_logic_impl._components + mock_audit_log_payload, app=rest_guild_logic_impl._app ) @pytest.mark.asyncio @@ -319,7 +319,7 @@ async def test_fetch_guild_emoji(self, rest_guild_logic_impl, guild, emoji): guild_id="93443949", emoji_id="40404040404", ) emojis.KnownCustomEmoji.deserialize.assert_called_once_with( - mock_emoji_payload, components=rest_guild_logic_impl._components + mock_emoji_payload, app=rest_guild_logic_impl._app ) @pytest.mark.asyncio @@ -332,7 +332,7 @@ async def test_fetch_guild_emojis(self, rest_guild_logic_impl, guild): assert await rest_guild_logic_impl.fetch_guild_emojis(guild=guild) == [mock_emoji_obj] rest_guild_logic_impl._session.list_guild_emojis.assert_called_once_with(guild_id="93443949",) emojis.KnownCustomEmoji.deserialize.assert_called_once_with( - mock_emoji_payload, components=rest_guild_logic_impl._components + mock_emoji_payload, app=rest_guild_logic_impl._app ) @pytest.mark.asyncio @@ -353,7 +353,7 @@ async def test_create_guild_emoji_with_optionals(self, rest_guild_logic_impl, gu ) assert result is mock_emoji_obj emojis.KnownCustomEmoji.deserialize.assert_called_once_with( - mock_emoji_payload, components=rest_guild_logic_impl._components + mock_emoji_payload, app=rest_guild_logic_impl._app ) mock_image_obj.read.assert_awaited_once() rest_guild_logic_impl._session.create_guild_emoji.assert_called_once_with( @@ -378,7 +378,7 @@ async def test_create_guild_emoji_without_optionals(self, rest_guild_logic_impl, ) assert result is mock_emoji_obj emojis.KnownCustomEmoji.deserialize.assert_called_once_with( - mock_emoji_payload, components=rest_guild_logic_impl._components + mock_emoji_payload, app=rest_guild_logic_impl._app ) mock_image_obj.read.assert_awaited_once() rest_guild_logic_impl._session.create_guild_emoji.assert_called_once_with( @@ -398,7 +398,7 @@ async def test_update_guild_emoji_without_optionals(self, rest_guild_logic_impl, guild_id="93443949", emoji_id="4123321", name=..., roles=..., reason=..., ) emojis.KnownCustomEmoji.deserialize.assert_called_once_with( - mock_emoji_payload, components=rest_guild_logic_impl._components + mock_emoji_payload, app=rest_guild_logic_impl._app ) @pytest.mark.asyncio @@ -418,7 +418,7 @@ async def test_update_guild_emoji_with_optionals(self, rest_guild_logic_impl, gu guild_id="93443949", emoji_id="4123321", name="Nyaa", roles=["123123123"], reason="Agent 42", ) emojis.KnownCustomEmoji.deserialize.assert_called_once_with( - mock_emoji_payload, components=rest_guild_logic_impl._components + mock_emoji_payload, app=rest_guild_logic_impl._app ) @pytest.mark.asyncio @@ -461,9 +461,7 @@ async def test_create_guild_with_optionals(self, rest_guild_logic_impl, region): ) assert result is mock_guild_obj mock_image_obj.read.assert_awaited_once() - guilds.Guild.deserialize.assert_called_once_with( - mock_guild_payload, components=rest_guild_logic_impl._components - ) + guilds.Guild.deserialize.assert_called_once_with(mock_guild_payload, app=rest_guild_logic_impl._app) mock_channel_obj.serialize.assert_called_once() mock_role_obj.serialize.assert_called_once() rest_guild_logic_impl._session.create_guild.assert_called_once_with( @@ -484,9 +482,7 @@ async def test_create_guild_without_optionals(self, rest_guild_logic_impl): rest_guild_logic_impl._session.create_guild.return_value = mock_guild_payload with mock.patch.object(guilds.Guild, "deserialize", return_value=mock_guild_obj): assert await rest_guild_logic_impl.create_guild(name="OK") is mock_guild_obj - guilds.Guild.deserialize.assert_called_once_with( - mock_guild_payload, components=rest_guild_logic_impl._components - ) + guilds.Guild.deserialize.assert_called_once_with(mock_guild_payload, app=rest_guild_logic_impl._app) rest_guild_logic_impl._session.create_guild.assert_called_once_with( name="OK", region=..., @@ -509,9 +505,7 @@ async def test_fetch_guild(self, rest_guild_logic_impl, guild): rest_guild_logic_impl._session.get_guild.assert_called_once_with( guild_id="379953393319542784", with_counts=True ) - guilds.Guild.deserialize.assert_called_once_with( - mock_guild_payload, components=rest_guild_logic_impl._components - ) + guilds.Guild.deserialize.assert_called_once_with(mock_guild_payload, app=rest_guild_logic_impl._app) @pytest.mark.asyncio @_helpers.parametrize_valid_id_formats_for_models("guild", 379953393319542784, guilds.Guild) @@ -523,7 +517,7 @@ async def test_fetch_guild_preview(self, rest_guild_logic_impl, guild): assert await rest_guild_logic_impl.fetch_guild_preview(guild) is mock_guild_preview_obj rest_guild_logic_impl._session.get_guild_preview.assert_called_once_with(guild_id="379953393319542784") guilds.GuildPreview.deserialize.assert_called_once_with( - mock_guild_preview_payload, components=rest_guild_logic_impl._components + mock_guild_preview_payload, app=rest_guild_logic_impl._app ) @pytest.mark.asyncio @@ -564,9 +558,7 @@ async def test_update_guild_with_optionals( reason="A good reason", ) assert result is mock_guild_obj - guilds.Guild.deserialize.assert_called_once_with( - mock_guild_payload, components=rest_guild_logic_impl._components - ) + guilds.Guild.deserialize.assert_called_once_with(mock_guild_payload, app=rest_guild_logic_impl._app) mock_icon_obj.read.assert_awaited_once() mock_splash_obj.read.assert_awaited_once() rest_guild_logic_impl._session.modify_guild.assert_called_once_with( @@ -608,9 +600,7 @@ async def test_update_guild_without_optionals(self, rest_guild_logic_impl, guild system_channel_id=..., reason=..., ) - guilds.Guild.deserialize.assert_called_once_with( - mock_guild_payload, components=rest_guild_logic_impl._components - ) + guilds.Guild.deserialize.assert_called_once_with(mock_guild_payload, app=rest_guild_logic_impl._app) @pytest.mark.asyncio @_helpers.parametrize_valid_id_formats_for_models("guild", 379953393319542784, guilds.Guild) @@ -628,9 +618,7 @@ async def test_fetch_guild_channels(self, rest_guild_logic_impl, guild): with mock.patch.object(channels, "deserialize_channel", return_value=mock_channel_obj): assert await rest_guild_logic_impl.fetch_guild_channels(guild) == [mock_channel_obj] rest_guild_logic_impl._session.list_guild_channels.assert_called_once_with(guild_id="379953393319542784") - channels.deserialize_channel.assert_called_once_with( - mock_channel_payload, components=rest_guild_logic_impl._components - ) + channels.deserialize_channel.assert_called_once_with(mock_channel_payload, app=rest_guild_logic_impl._app) @pytest.mark.asyncio @_helpers.parametrize_valid_id_formats_for_models("guild", 123123123, guilds.Guild) @@ -677,9 +665,7 @@ async def test_create_guild_channel_with_optionals( parent_id="5555", reason="A GOOD REASON!", ) - channels.deserialize_channel.assert_called_once_with( - mock_channel_payload, components=rest_guild_logic_impl._components - ) + channels.deserialize_channel.assert_called_once_with(mock_channel_payload, app=rest_guild_logic_impl._app) @pytest.mark.asyncio @_helpers.parametrize_valid_id_formats_for_models("guild", 123123123, guilds.Guild) @@ -703,9 +689,7 @@ async def test_create_guild_channel_without_optionals(self, rest_guild_logic_imp parent_id=..., reason=..., ) - channels.deserialize_channel.assert_called_once_with( - mock_channel_payload, components=rest_guild_logic_impl._components - ) + channels.deserialize_channel.assert_called_once_with(mock_channel_payload, app=rest_guild_logic_impl._app) @pytest.mark.asyncio @_helpers.parametrize_valid_id_formats_for_models("guild", 123123123, guilds.Guild) @@ -730,9 +714,7 @@ async def test_fetch_member(self, rest_guild_logic_impl, guild, user): rest_guild_logic_impl._session.get_guild_member.assert_called_once_with( guild_id="444444", user_id="123123123123" ) - guilds.GuildMember.deserialize.assert_called_once_with( - mock_member_payload, components=rest_guild_logic_impl._components - ) + guilds.GuildMember.deserialize.assert_called_once_with(mock_member_payload, app=rest_guild_logic_impl._app) def test_fetch_members(self, rest_guild_logic_impl): guild = mock.MagicMock() @@ -742,10 +724,7 @@ def test_fetch_members(self, rest_guild_logic_impl): assert isinstance(result, _guild._MemberPaginator) init.assert_called_once_with( - guild=guild, - created_after=None, - components=rest_guild_logic_impl._components, - session=rest_guild_logic_impl._session, + guild=guild, created_after=None, app=rest_guild_logic_impl._app, session=rest_guild_logic_impl._session, ) @pytest.mark.asyncio @@ -891,9 +870,7 @@ async def test_fetch_ban(self, rest_guild_logic_impl, guild, user): rest_guild_logic_impl._session.get_guild_ban.assert_called_once_with( guild_id="123123123", user_id="4444444" ) - guilds.GuildMemberBan.deserialize.assert_called_once_with( - mock_ban_payload, components=rest_guild_logic_impl._components - ) + guilds.GuildMemberBan.deserialize.assert_called_once_with(mock_ban_payload, app=rest_guild_logic_impl._app) @pytest.mark.asyncio @_helpers.parametrize_valid_id_formats_for_models("guild", 123123123, guilds.Guild) @@ -904,9 +881,7 @@ async def test_fetch_bans(self, rest_guild_logic_impl, guild): with mock.patch.object(guilds.GuildMemberBan, "deserialize", return_value=mock_ban_obj): assert await rest_guild_logic_impl.fetch_bans(guild) == [mock_ban_obj] rest_guild_logic_impl._session.get_guild_bans.assert_called_once_with(guild_id="123123123") - guilds.GuildMemberBan.deserialize.assert_called_once_with( - mock_ban_payload, components=rest_guild_logic_impl._components - ) + guilds.GuildMemberBan.deserialize.assert_called_once_with(mock_ban_payload, app=rest_guild_logic_impl._app) @pytest.mark.asyncio @_helpers.parametrize_valid_id_formats_for_models("guild", 123123123, guilds.Guild) @@ -962,9 +937,7 @@ async def test_fetch_roles(self, rest_guild_logic_impl, guild): with mock.patch.object(guilds.GuildRole, "deserialize", return_value=mock_role_obj): assert await rest_guild_logic_impl.fetch_roles(guild) == {33030: mock_role_obj} rest_guild_logic_impl._session.get_guild_roles.assert_called_once_with(guild_id="574921006817476608") - guilds.GuildRole.deserialize.assert_called_once_with( - mock_role_payload, components=rest_guild_logic_impl._components - ) + guilds.GuildRole.deserialize.assert_called_once_with(mock_role_payload, app=rest_guild_logic_impl._app) @pytest.mark.asyncio @_helpers.parametrize_valid_id_formats_for_models("guild", 574921006817476608, guilds.Guild) @@ -992,9 +965,7 @@ async def test_create_role_with_optionals(self, rest_guild_logic_impl, guild): mentionable=False, reason="And then there was a role.", ) - guilds.GuildRole.deserialize.assert_called_once_with( - mock_role_payload, components=rest_guild_logic_impl._components - ) + guilds.GuildRole.deserialize.assert_called_once_with(mock_role_payload, app=rest_guild_logic_impl._app) @pytest.mark.asyncio @_helpers.parametrize_valid_id_formats_for_models("guild", 574921006817476608, guilds.Guild) @@ -1014,9 +985,7 @@ async def test_create_role_without_optionals(self, rest_guild_logic_impl, guild) mentionable=..., reason=..., ) - guilds.GuildRole.deserialize.assert_called_once_with( - mock_role_payload, components=rest_guild_logic_impl._components - ) + guilds.GuildRole.deserialize.assert_called_once_with(mock_role_payload, app=rest_guild_logic_impl._app) @pytest.mark.asyncio @_helpers.parametrize_valid_id_formats_for_models("guild", 574921006817476608, guilds.Guild) @@ -1032,9 +1001,7 @@ async def test_reposition_roles(self, rest_guild_logic_impl, guild, role, additi rest_guild_logic_impl._session.modify_guild_role_positions.assert_called_once_with( "574921006817476608", ("123123", 1), ("123456", 2) ) - guilds.GuildRole.deserialize.assert_called_once_with( - mock_role_payload, components=rest_guild_logic_impl._components - ) + guilds.GuildRole.deserialize.assert_called_once_with(mock_role_payload, app=rest_guild_logic_impl._app) @pytest.mark.asyncio @_helpers.parametrize_valid_id_formats_for_models("guild", 574921006817476608, guilds.Guild) @@ -1065,9 +1032,7 @@ async def test_update_role_with_optionals(self, rest_guild_logic_impl, guild, ro mentionable=False, reason="Why not?", ) - guilds.GuildRole.deserialize.assert_called_once_with( - mock_role_payload, components=rest_guild_logic_impl._components - ) + guilds.GuildRole.deserialize.assert_called_once_with(mock_role_payload, app=rest_guild_logic_impl._app) @pytest.mark.asyncio @_helpers.parametrize_valid_id_formats_for_models("guild", 574921006817476608, guilds.Guild) @@ -1088,9 +1053,7 @@ async def test_update_role_without_optionals(self, rest_guild_logic_impl, guild, mentionable=..., reason=..., ) - guilds.GuildRole.deserialize.assert_called_once_with( - mock_role_payload, components=rest_guild_logic_impl._components - ) + guilds.GuildRole.deserialize.assert_called_once_with(mock_role_payload, app=rest_guild_logic_impl._app) @pytest.mark.asyncio @_helpers.parametrize_valid_id_formats_for_models("guild", 574921006817476608, guilds.Guild) @@ -1146,9 +1109,7 @@ async def test_fetch_guild_voice_regions(self, rest_guild_logic_impl, guild): rest_guild_logic_impl._session.get_guild_voice_regions.assert_called_once_with( guild_id="574921006817476608" ) - voices.VoiceRegion.deserialize.assert_called_once_with( - mock_voice_payload, components=rest_guild_logic_impl._components - ) + voices.VoiceRegion.deserialize.assert_called_once_with(mock_voice_payload, app=rest_guild_logic_impl._app) @pytest.mark.asyncio @_helpers.parametrize_valid_id_formats_for_models("guild", 574921006817476608, guilds.Guild) @@ -1159,7 +1120,7 @@ async def test_fetch_guild_invites(self, rest_guild_logic_impl, guild): with mock.patch.object(invites.InviteWithMetadata, "deserialize", return_value=mock_invite_obj): assert await rest_guild_logic_impl.fetch_guild_invites(guild) == [mock_invite_obj] invites.InviteWithMetadata.deserialize.assert_called_once_with( - mock_invite_payload, components=rest_guild_logic_impl._components + mock_invite_payload, app=rest_guild_logic_impl._app ) @pytest.mark.asyncio @@ -1172,7 +1133,7 @@ async def test_fetch_integrations(self, rest_guild_logic_impl, guild): assert await rest_guild_logic_impl.fetch_integrations(guild) == [mock_integration_obj] rest_guild_logic_impl._session.get_guild_integrations.assert_called_once_with(guild_id="574921006817476608") guilds.GuildIntegration.deserialize.assert_called_once_with( - mock_integration_payload, components=rest_guild_logic_impl._components + mock_integration_payload, app=rest_guild_logic_impl._app ) @pytest.mark.asyncio @@ -1253,9 +1214,7 @@ async def test_fetch_guild_embed(self, rest_guild_logic_impl, guild): with mock.patch.object(guilds.GuildEmbed, "deserialize", return_value=mock_embed_obj): assert await rest_guild_logic_impl.fetch_guild_embed(guild) is mock_embed_obj rest_guild_logic_impl._session.get_guild_embed.assert_called_once_with(guild_id="574921006817476608") - guilds.GuildEmbed.deserialize.assert_called_once_with( - mock_embed_payload, components=rest_guild_logic_impl._components - ) + guilds.GuildEmbed.deserialize.assert_called_once_with(mock_embed_payload, app=rest_guild_logic_impl._app) @pytest.mark.asyncio @_helpers.parametrize_valid_id_formats_for_models("guild", 574921006817476608, guilds.Guild) @@ -1272,9 +1231,7 @@ async def test_update_guild_embed_with_optionnal(self, rest_guild_logic_impl, gu rest_guild_logic_impl._session.modify_guild_embed.assert_called_once_with( guild_id="574921006817476608", channel_id="123123", enabled=True, reason="Nyaa!!!" ) - guilds.GuildEmbed.deserialize.assert_called_once_with( - mock_embed_payload, components=rest_guild_logic_impl._components - ) + guilds.GuildEmbed.deserialize.assert_called_once_with(mock_embed_payload, app=rest_guild_logic_impl._app) @pytest.mark.asyncio @_helpers.parametrize_valid_id_formats_for_models("guild", 574921006817476608, guilds.Guild) @@ -1287,9 +1244,7 @@ async def test_update_guild_embed_without_optionnal(self, rest_guild_logic_impl, rest_guild_logic_impl._session.modify_guild_embed.assert_called_once_with( guild_id="574921006817476608", channel_id=..., enabled=..., reason=... ) - guilds.GuildEmbed.deserialize.assert_called_once_with( - mock_embed_payload, components=rest_guild_logic_impl._components - ) + guilds.GuildEmbed.deserialize.assert_called_once_with(mock_embed_payload, app=rest_guild_logic_impl._app) @pytest.mark.asyncio @_helpers.parametrize_valid_id_formats_for_models("guild", 574921006817476608, guilds.Guild) @@ -1300,9 +1255,7 @@ async def test_fetch_guild_vanity_url(self, rest_guild_logic_impl, guild): with mock.patch.object(invites.VanityUrl, "deserialize", return_value=mock_vanity_obj): assert await rest_guild_logic_impl.fetch_guild_vanity_url(guild) is mock_vanity_obj rest_guild_logic_impl._session.get_guild_vanity_url.assert_called_once_with(guild_id="574921006817476608") - invites.VanityUrl.deserialize.assert_called_once_with( - mock_vanity_payload, components=rest_guild_logic_impl._components - ) + invites.VanityUrl.deserialize.assert_called_once_with(mock_vanity_payload, app=rest_guild_logic_impl._app) @_helpers.parametrize_valid_id_formats_for_models("guild", 574921006817476608, guilds.Guild) def test_fetch_guild_widget_image_with_style(self, rest_guild_logic_impl, guild): @@ -1331,6 +1284,4 @@ async def test_fetch_guild_webhooks(self, rest_guild_logic_impl, channel): with mock.patch.object(webhooks.Webhook, "deserialize", return_value=mock_webhook_obj): assert await rest_guild_logic_impl.fetch_guild_webhooks(channel) == [mock_webhook_obj] rest_guild_logic_impl._session.get_guild_webhooks.assert_called_once_with(guild_id="115590097100865541") - webhooks.Webhook.deserialize.assert_called_once_with( - mock_webhook_payload, components=rest_guild_logic_impl._components - ) + webhooks.Webhook.deserialize.assert_called_once_with(mock_webhook_payload, app=rest_guild_logic_impl._app) diff --git a/tests/hikari/clients/test_rest/test_invite.py b/tests/hikari/rest/test_invite.py similarity index 87% rename from tests/hikari/clients/test_rest/test_invite.py rename to tests/hikari/rest/test_invite.py index a555a1da18..e7d3b86180 100644 --- a/tests/hikari/clients/test_rest/test_invite.py +++ b/tests/hikari/rest/test_invite.py @@ -19,8 +19,8 @@ import mock import pytest -from hikari.models import invites from hikari.components import application +from hikari.models import invites from hikari.rest import invite from hikari.rest import session @@ -28,12 +28,12 @@ class TestRESTInviteLogic: @pytest.fixture() def rest_invite_logic_impl(self): - mock_components = mock.MagicMock(application.Application) + mock_app = mock.MagicMock(application.Application) mock_low_level_restful_client = mock.MagicMock(session.RESTSession) class RESTInviteLogicImpl(invite.RESTInviteComponent): def __init__(self): - super().__init__(mock_components, mock_low_level_restful_client) + super().__init__(mock_app, mock_low_level_restful_client) return RESTInviteLogicImpl() @@ -48,9 +48,7 @@ async def test_fetch_invite_with_counts(self, rest_invite_logic_impl, invite): rest_invite_logic_impl._session.get_invite.assert_called_once_with( invite_code="AAAAAAAAAAAAAAAA", with_counts=True, ) - invites.Invite.deserialize.assert_called_once_with( - mock_invite_payload, components=rest_invite_logic_impl._components - ) + invites.Invite.deserialize.assert_called_once_with(mock_invite_payload, app=rest_invite_logic_impl._app) @pytest.mark.asyncio @pytest.mark.parametrize("invite", [mock.MagicMock(invites.Invite, code="AAAAAAAAAAAAAAAA"), "AAAAAAAAAAAAAAAA"]) @@ -63,9 +61,7 @@ async def test_fetch_invite_without_counts(self, rest_invite_logic_impl, invite) rest_invite_logic_impl._session.get_invite.assert_called_once_with( invite_code="AAAAAAAAAAAAAAAA", with_counts=..., ) - invites.Invite.deserialize.assert_called_once_with( - mock_invite_payload, components=rest_invite_logic_impl._components - ) + invites.Invite.deserialize.assert_called_once_with(mock_invite_payload, app=rest_invite_logic_impl._app) @pytest.mark.asyncio @pytest.mark.parametrize("invite", [mock.MagicMock(invites.Invite, code="AAAAAAAAAAAAAAAA"), "AAAAAAAAAAAAAAAA"]) diff --git a/tests/hikari/clients/test_rest/test_me.py b/tests/hikari/rest/test_me.py similarity index 83% rename from tests/hikari/clients/test_rest/test_me.py rename to tests/hikari/rest/test_me.py index 420f6ce7e0..e246cb3e93 100644 --- a/tests/hikari/clients/test_rest/test_me.py +++ b/tests/hikari/rest/test_me.py @@ -22,13 +22,13 @@ import mock import pytest +from hikari.components import application from hikari.models import applications from hikari.models import bases from hikari.models import channels from hikari.models import files from hikari.models import guilds from hikari.models import users -from hikari.components import application from hikari.rest import me from hikari.rest import session from tests.hikari import _helpers @@ -40,7 +40,7 @@ def mock_session(self): return mock.MagicMock(spec_set=session.RESTSession) @pytest.fixture() - def mock_components(self): + def mock_app(self): return mock.MagicMock(spec_set=application.Application) @pytest.fixture() @@ -51,26 +51,26 @@ def ownguild_cls(self): @pytest.mark.parametrize( ["newest_first", "expected_first_id"], [(True, str(bases.Snowflake.max())), (False, str(bases.Snowflake.min()))] ) - def test_init_with_no_explicit_first_element(self, newest_first, expected_first_id, mock_components, mock_session): - pag = me._GuildPaginator(newest_first, None, mock_components, mock_session) + def test_init_with_no_explicit_first_element(self, newest_first, expected_first_id, mock_app, mock_session): + pag = me._GuildPaginator(mock_app, newest_first, None, mock_session) assert pag._first_id == expected_first_id assert pag._newest_first is newest_first - assert pag._components is mock_components + assert pag._app is mock_app assert pag._session is mock_session - def test_init_with_explicit_first_element(self, mock_components, mock_session): - pag = me._GuildPaginator(False, 12345, mock_components, mock_session) + def test_init_with_explicit_first_element(self, mock_app, mock_session): + pag = me._GuildPaginator(mock_app, False, 12345, mock_session) assert pag._first_id == "12345" assert pag._newest_first is False - assert pag._components is mock_components + assert pag._app is mock_app assert pag._session is mock_session @pytest.mark.parametrize(["newest_first", "direction"], [(True, "before"), (False, "after"),]) @pytest.mark.asyncio async def test_next_chunk_performs_correct_api_call( - self, mock_session, mock_components, newest_first, direction, ownguild_cls + self, mock_session, mock_app, newest_first, direction, ownguild_cls ): - pag = me._GuildPaginator(newest_first, None, mock_components, mock_session) + pag = me._GuildPaginator(mock_app, newest_first, None, mock_session) pag._first_id = "123456" await pag._next_chunk() @@ -78,14 +78,14 @@ async def test_next_chunk_performs_correct_api_call( mock_session.get_current_user_guilds.assert_awaited_once_with(**{direction: "123456"}) @pytest.mark.asyncio - async def test_next_chunk_returns_None_if_no_items_returned(self, mock_session, mock_components, ownguild_cls): - pag = me._GuildPaginator(False, None, mock_components, mock_session) + async def test_next_chunk_returns_None_if_no_items_returned(self, mock_session, mock_app, ownguild_cls): + pag = me._GuildPaginator(mock_app, False, None, mock_session) mock_session.get_current_user_guilds = mock.AsyncMock(return_value=[]) assert await pag._next_chunk() is None @pytest.mark.asyncio - async def test_next_chunk_updates_first_id_to_last_item(self, mock_session, mock_components, ownguild_cls): - pag = me._GuildPaginator(False, None, mock_components, mock_session) + async def test_next_chunk_updates_first_id_to_last_item(self, mock_session, mock_app, ownguild_cls): + pag = me._GuildPaginator(mock_app, False, None, mock_session) return_payload = [ {"id": "1234", ...: ...}, @@ -99,10 +99,8 @@ async def test_next_chunk_updates_first_id_to_last_item(self, mock_session, mock assert pag._first_id == "512" @pytest.mark.asyncio - async def test_next_chunk_deserializes_payload_in_generator_lazily( - self, mock_session, mock_components, ownguild_cls - ): - pag = me._GuildPaginator(False, None, mock_components, mock_session) + async def test_next_chunk_deserializes_payload_in_generator_lazily(self, mock_session, mock_app, ownguild_cls): + pag = me._GuildPaginator(mock_app, False, None, mock_session) return_payload = [ {"id": "1234", ...: ...}, @@ -133,7 +131,7 @@ async def test_next_chunk_deserializes_payload_in_generator_lazily( for i, input_payload in enumerate(return_payload): expected_value = real_values[i] assert next(generator) is expected_value - ownguild_cls.deserialize.assert_called_with(input_payload, components=mock_components) + ownguild_cls.deserialize.assert_called_with(input_payload, app=mock_app) # Clear the generator result. # This doesn't test anything, but there is an issue with coverage not detecting generator @@ -149,12 +147,12 @@ async def test_next_chunk_deserializes_payload_in_generator_lazily( class TestRESTInviteLogic: @pytest.fixture() def rest_clients_impl(self): - mock_components = mock.MagicMock(application.Application) + mock_app = mock.MagicMock(application.Application) mock_low_level_restful_client = mock.MagicMock(session.RESTSession) class RESTCurrentUserLogicImpl(me.RESTCurrentUserComponent): def __init__(self): - super().__init__(mock_components, mock_low_level_restful_client) + super().__init__(mock_app, mock_low_level_restful_client) return RESTCurrentUserLogicImpl() @@ -166,9 +164,7 @@ async def test_fetch_me(self, rest_clients_impl): with mock.patch.object(users.MyUser, "deserialize", return_value=mock_user_obj): assert await rest_clients_impl.fetch_me() is mock_user_obj rest_clients_impl._session.get_current_user.assert_called_once() - users.MyUser.deserialize.assert_called_once_with( - mock_user_payload, components=rest_clients_impl._components - ) + users.MyUser.deserialize.assert_called_once_with(mock_user_payload, app=rest_clients_impl._app) @pytest.mark.asyncio async def test_update_me_with_optionals(self, rest_clients_impl): @@ -186,9 +182,7 @@ async def test_update_me_with_optionals(self, rest_clients_impl): username="aNewName", avatar=mock_avatar_data ) mock_avatar_obj.read.assert_awaited_once() - users.MyUser.deserialize.assert_called_once_with( - mock_user_payload, components=rest_clients_impl._components - ) + users.MyUser.deserialize.assert_called_once_with(mock_user_payload, app=rest_clients_impl._app) @pytest.mark.asyncio async def test_update_me_without_optionals(self, rest_clients_impl): @@ -198,9 +192,7 @@ async def test_update_me_without_optionals(self, rest_clients_impl): with mock.patch.object(users.MyUser, "deserialize", return_value=mock_user_obj): assert await rest_clients_impl.update_me() is mock_user_obj rest_clients_impl._session.modify_current_user.assert_called_once_with(username=..., avatar=...) - users.MyUser.deserialize.assert_called_once_with( - mock_user_payload, components=rest_clients_impl._components - ) + users.MyUser.deserialize.assert_called_once_with(mock_user_payload, app=rest_clients_impl._app) @pytest.mark.asyncio async def test_fetch_my_connections(self, rest_clients_impl): @@ -211,7 +203,7 @@ async def test_fetch_my_connections(self, rest_clients_impl): assert await rest_clients_impl.fetch_my_connections() == [mock_connection_obj] rest_clients_impl._session.get_current_user_connections.assert_called_once() applications.OwnConnection.deserialize.assert_called_once_with( - mock_connection_payload, components=rest_clients_impl._components + mock_connection_payload, app=rest_clients_impl._app ) @pytest.mark.parametrize("newest_first", [True, False]) @@ -223,7 +215,7 @@ def test_fetch_my_guilds(self, rest_clients_impl, newest_first, start_at): init.assert_called_once_with( newest_first=newest_first, first_item=start_at, - components=rest_clients_impl._components, + app=rest_clients_impl._app, session=rest_clients_impl._session, ) @@ -232,10 +224,7 @@ def test_fetch_my_guilds_default_directionality(self, rest_clients_impl): result = rest_clients_impl.fetch_my_guilds() assert isinstance(result, me._GuildPaginator) init.assert_called_once_with( - newest_first=False, - first_item=None, - components=rest_clients_impl._components, - session=rest_clients_impl._session, + newest_first=False, first_item=None, app=rest_clients_impl._app, session=rest_clients_impl._session, ) @pytest.mark.asyncio @@ -254,6 +243,4 @@ async def test_create_dm_channel(self, rest_clients_impl, recipient): with mock.patch.object(channels.DMChannel, "deserialize", return_value=mock_dm_obj): assert await rest_clients_impl.create_dm_channel(recipient) is mock_dm_obj rest_clients_impl._session.create_dm.assert_called_once_with(recipient_id="115590097100865541") - channels.DMChannel.deserialize.assert_called_once_with( - mock_dm_payload, components=rest_clients_impl._components - ) + channels.DMChannel.deserialize.assert_called_once_with(mock_dm_payload, app=rest_clients_impl._app) diff --git a/tests/hikari/clients/test_rest/test_oauth2.py b/tests/hikari/rest/test_oauth2.py similarity index 89% rename from tests/hikari/clients/test_rest/test_oauth2.py rename to tests/hikari/rest/test_oauth2.py index 3d9d800d4c..be4f86927a 100644 --- a/tests/hikari/clients/test_rest/test_oauth2.py +++ b/tests/hikari/rest/test_oauth2.py @@ -20,8 +20,8 @@ import mock import pytest -from hikari.models import applications from hikari.components import application +from hikari.models import applications from hikari.rest import oauth2 from hikari.rest import session @@ -29,12 +29,12 @@ class TestRESTReactionLogic: @pytest.fixture() def rest_oauth2_logic_impl(self): - mock_components = mock.MagicMock(application.Application) + mock_app = mock.MagicMock(application.Application) mock_low_level_restful_client = mock.MagicMock(session.RESTSession) class RESTOauth2LogicImpl(oauth2.RESTOAuth2Component): def __init__(self): - super().__init__(mock_components, mock_low_level_restful_client) + super().__init__(mock_app, mock_low_level_restful_client) return RESTOauth2LogicImpl() @@ -47,5 +47,5 @@ async def test_fetch_my_application_info(self, rest_oauth2_logic_impl): assert await rest_oauth2_logic_impl.fetch_my_application_info() is mock_application_obj rest_oauth2_logic_impl._session.get_current_application_info.assert_called_once_with() applications.Application.deserialize.assert_called_once_with( - mock_application_payload, components=rest_oauth2_logic_impl._components + mock_application_payload, app=rest_oauth2_logic_impl._app ) diff --git a/tests/hikari/clients/test_rest/test_react.py b/tests/hikari/rest/test_react.py similarity index 88% rename from tests/hikari/clients/test_rest/test_react.py rename to tests/hikari/rest/test_react.py index 60ac20d729..2f54500a31 100644 --- a/tests/hikari/clients/test_rest/test_react.py +++ b/tests/hikari/rest/test_react.py @@ -22,12 +22,12 @@ import mock import pytest +from hikari.components import application from hikari.models import bases from hikari.models import channels from hikari.models import emojis from hikari.models import messages from hikari.models import users -from hikari.components import application from hikari.rest import react from hikari.rest import session from tests.hikari import _helpers @@ -48,7 +48,7 @@ def mock_session(self): return mock.MagicMock(spec_set=session.RESTSession) @pytest.fixture() - def mock_components(self): + def mock_app(self): return mock.MagicMock(spec_set=application.Application) @pytest.fixture() @@ -56,14 +56,14 @@ def user_cls(self): with mock.patch.object(users, "User") as user_cls: yield user_cls - def test_init_no_start_bounds(self, mock_session, mock_components, emoji): + def test_init_no_start_bounds(self, mock_session, mock_app, emoji): message = mock.MagicMock(__int__=lambda _: 22) channel = mock.MagicMock(__int__=lambda _: 33) - pag = react._ReactionPaginator(channel, message, emoji, None, mock_components, mock_session) + pag = react._ReactionPaginator(mock_app, channel, message, emoji, None, mock_session) + assert pag._app is mock_app assert pag._first_id == "0" assert pag._message_id == "22" - assert pag._components is mock_components assert pag._session is mock_session @pytest.mark.parametrize( @@ -76,23 +76,23 @@ def test_init_no_start_bounds(self, mock_session, mock_components, emoji): (datetime.datetime(2019, 1, 22, 18, 41, 15, 283000, tzinfo=datetime.timezone.utc), "537340989807788032"), ], ) - def test_init_with_start_bounds(self, mock_session, mock_components, start_at, expected, emoji): + def test_init_with_start_bounds(self, mock_session, mock_app, start_at, expected, emoji): message = mock.MagicMock(__int__=lambda _: 22) channel = mock.MagicMock(__int__=lambda _: 33) - pag = react._ReactionPaginator(channel, message, emoji, start_at, mock_components, mock_session) + pag = react._ReactionPaginator(mock_app, channel, message, emoji, start_at, mock_session) assert pag._first_id == expected assert pag._message_id == "22" assert pag._channel_id == "33" - assert pag._components is mock_components + assert pag._app is mock_app assert pag._session is mock_session @pytest.mark.asyncio - async def test_next_chunk_performs_correct_api_call(self, mock_session, mock_components, user_cls, emoji): + async def test_next_chunk_performs_correct_api_call(self, mock_session, mock_app, user_cls, emoji): message = mock.MagicMock(__int__=lambda _: 44) channel = mock.MagicMock(__int__=lambda _: 55) - pag = react._ReactionPaginator(channel, message, emoji, None, mock_components, mock_session) + pag = react._ReactionPaginator(mock_app, channel, message, emoji, None, mock_session) pag._first_id = "123456" await pag._next_chunk() @@ -102,17 +102,17 @@ async def test_next_chunk_performs_correct_api_call(self, mock_session, mock_com ) @pytest.mark.asyncio - async def test_next_chunk_when_empty_returns_None(self, mock_session, mock_components, user_cls, emoji): + async def test_next_chunk_when_empty_returns_None(self, mock_session, mock_app, user_cls, emoji): mock_session.get_reactions = mock.AsyncMock(return_value=[]) message = mock.MagicMock(__int__=lambda _: 66) channel = mock.MagicMock(__int__=lambda _: 77) - pag = react._ReactionPaginator(channel, message, emoji, None, mock_components, mock_session) + pag = react._ReactionPaginator(mock_app, channel, message, emoji, None, mock_session) assert await pag._next_chunk() is None @pytest.mark.asyncio - async def test_next_chunk_updates_first_id_to_last_item(self, mock_session, mock_components, user_cls, emoji): + async def test_next_chunk_updates_first_id_to_last_item(self, mock_session, mock_app, user_cls, emoji): return_payload = [ {"id": "1234", ...: ...}, {"id": "3456", ...: ...}, @@ -125,20 +125,18 @@ async def test_next_chunk_updates_first_id_to_last_item(self, mock_session, mock message = mock.MagicMock(__int__=lambda _: 88) channel = mock.MagicMock(__int__=lambda _: 99) - pag = react._ReactionPaginator(channel, message, emoji, None, mock_components, mock_session) + pag = react._ReactionPaginator(mock_app, channel, message, emoji, None, mock_session) await pag._next_chunk() assert pag._first_id == "512" @pytest.mark.asyncio - async def test_next_chunk_deserializes_payload_in_generator_lazily( - self, mock_session, mock_components, user_cls, emoji - ): + async def test_next_chunk_deserializes_payload_in_generator_lazily(self, mock_session, mock_app, user_cls, emoji): message = mock.MagicMock(__int__=lambda _: 91210) channel = mock.MagicMock(__int__=lambda _: 8008135) - pag = react._ReactionPaginator(channel, message, emoji, None, mock_components, mock_session) + pag = react._ReactionPaginator(mock_app, channel, message, emoji, None, mock_session) return_payload = [ {"id": "1234", ...: ...}, @@ -169,7 +167,7 @@ async def test_next_chunk_deserializes_payload_in_generator_lazily( for i, input_payload in enumerate(return_payload): expected_value = real_values[i] assert next(generator) is expected_value - user_cls.deserialize.assert_called_with(input_payload, components=mock_components) + user_cls.deserialize.assert_called_with(input_payload, app=mock_app) # Clear the generator result. # This doesn't test anything, but there is an issue with coverage not detecting generator @@ -185,12 +183,12 @@ async def test_next_chunk_deserializes_payload_in_generator_lazily( class TestRESTReactionLogic: @pytest.fixture() def rest_reaction_logic_impl(self): - mock_components = mock.MagicMock(application.Application) + mock_app = mock.MagicMock(application.Application) mock_low_level_restful_client = mock.MagicMock(session.RESTSession) class RESTReactionLogicImpl(react.RESTReactionComponent): def __init__(self): - super().__init__(mock_components, mock_low_level_restful_client) + super().__init__(mock_app, mock_low_level_restful_client) return RESTReactionLogicImpl() @@ -263,6 +261,6 @@ def test_fetch_reactors(self, rest_reaction_logic_impl): message=bases.Snowflake("3456"), users_after=None, emoji="\N{OK HAND SIGN}", - components=rest_reaction_logic_impl._components, + app=rest_reaction_logic_impl._app, session=rest_reaction_logic_impl._session, ) diff --git a/tests/hikari/net/test_routes.py b/tests/hikari/rest/test_routes.py similarity index 100% rename from tests/hikari/net/test_routes.py rename to tests/hikari/rest/test_routes.py diff --git a/tests/hikari/net/test_rest.py b/tests/hikari/rest/test_session.py similarity index 99% rename from tests/hikari/net/test_rest.py rename to tests/hikari/rest/test_session.py index 6eec831dff..4b5a053f5b 100644 --- a/tests/hikari/net/test_rest.py +++ b/tests/hikari/rest/test_session.py @@ -29,10 +29,11 @@ import pytest from hikari import errors -from hikari.models import files from hikari.internal import conversions from hikari.internal import http_client from hikari.internal import ratelimits +from hikari.models import files +from hikari.rest import buckets from hikari.rest import session from hikari.rest import routes from tests.hikari import _helpers @@ -123,7 +124,7 @@ def compiled_route(): class TestRESTRequestJsonResponse: @pytest.fixture def bucket_ratelimiters(self): - limiter = mock.MagicMock(spec_set=ratelimits.RESTBucketManager) + limiter = mock.MagicMock(spec_set=buckets.RESTBucketManager) limiter.acquire = mock.MagicMock(return_value=_helpers.AwaitableMock()) return limiter @@ -137,7 +138,7 @@ def global_ratelimiter(self): def rest_impl(self, bucket_ratelimiters, global_ratelimiter): stack = contextlib.ExitStack() stack.enter_context(mock.patch.object(http_client.HTTPClient, "__init__", new=lambda *_, **__: None)) - stack.enter_context(mock.patch.object(ratelimits, "RESTBucketManager", return_value=bucket_ratelimiters)) + stack.enter_context(mock.patch.object(buckets, "RESTBucketManager", return_value=bucket_ratelimiters)) stack.enter_context(mock.patch.object(ratelimits, "ManualRateLimiter", return_value=global_ratelimiter)) with stack: client = session.RESTSession(base_url="http://example.bloop.com", token="Bot blah.blah.blah") @@ -362,7 +363,7 @@ async def test_ratelimited_429_retries_request_until_it_works(self, compiled_rou class TestHandleRateLimitsForResponse: @pytest.fixture def bucket_ratelimiters(self): - limiter = mock.MagicMock(spec_set=ratelimits.RESTBucketManager) + limiter = mock.MagicMock(spec_set=buckets.RESTBucketManager) limiter.update_rate_limits = mock.MagicMock() return limiter @@ -376,7 +377,7 @@ def global_ratelimiter(self): def rest_impl(self, bucket_ratelimiters, global_ratelimiter): stack = contextlib.ExitStack() stack.enter_context(mock.patch.object(http_client.HTTPClient, "__init__", new=lambda *_, **__: None)) - stack.enter_context(mock.patch.object(ratelimits, "RESTBucketManager", return_value=bucket_ratelimiters)) + stack.enter_context(mock.patch.object(buckets, "RESTBucketManager", return_value=bucket_ratelimiters)) stack.enter_context(mock.patch.object(ratelimits, "ManualRateLimiter", return_value=global_ratelimiter)) with stack: client = session.RESTSession(base_url="http://example.bloop.com", token="Bot blah.blah.blah") @@ -482,7 +483,7 @@ class TestRESTEndpoints: def rest_impl(self): stack = contextlib.ExitStack() stack.enter_context(mock.patch.object(http_client.HTTPClient, "__init__", new=lambda *_, **__: None)) - stack.enter_context(mock.patch.object(ratelimits, "RESTBucketManager")) + stack.enter_context(mock.patch.object(buckets, "RESTBucketManager")) stack.enter_context(mock.patch.object(ratelimits, "ManualRateLimiter")) with stack: client = session.RESTSession(base_url="https://discord.com/api/v6", token="Bot blah.blah.blah") diff --git a/tests/hikari/clients/test_rest/test_user.py b/tests/hikari/rest/test_user.py similarity index 86% rename from tests/hikari/clients/test_rest/test_user.py rename to tests/hikari/rest/test_user.py index aa9932c98e..93b182c3a2 100644 --- a/tests/hikari/clients/test_rest/test_user.py +++ b/tests/hikari/rest/test_user.py @@ -19,22 +19,22 @@ import mock import pytest -from hikari.models import users from hikari.components import application -from hikari.rest import user +from hikari.models import users from hikari.rest import session +from hikari.rest import user from tests.hikari import _helpers class TestRESTUserLogic: @pytest.fixture() def rest_user_logic_impl(self): - mock_components = mock.MagicMock(application.Application) + mock_app = mock.MagicMock(application.Application) mock_low_level_restful_client = mock.MagicMock(session.RESTSession) class RESTUserLogicImpl(user.RESTUserComponent): def __init__(self): - super().__init__(mock_components, mock_low_level_restful_client) + super().__init__(mock_app, mock_low_level_restful_client) return RESTUserLogicImpl() @@ -47,6 +47,4 @@ async def test_fetch_user(self, rest_user_logic_impl, user): with mock.patch.object(users.User, "deserialize", return_value=mock_user_obj): assert await rest_user_logic_impl.fetch_user(user) is mock_user_obj rest_user_logic_impl._session.get_user.assert_called_once_with(user_id="123123123") - users.User.deserialize.assert_called_once_with( - mock_user_payload, components=rest_user_logic_impl._components - ) + users.User.deserialize.assert_called_once_with(mock_user_payload, app=rest_user_logic_impl._app) diff --git a/tests/hikari/clients/test_rest/test_voice.py b/tests/hikari/rest/test_voice.py similarity index 88% rename from tests/hikari/clients/test_rest/test_voice.py rename to tests/hikari/rest/test_voice.py index 659e4dbfb9..d21468a5fe 100644 --- a/tests/hikari/clients/test_rest/test_voice.py +++ b/tests/hikari/rest/test_voice.py @@ -19,8 +19,8 @@ import mock import pytest -from hikari.models import voices from hikari.components import application +from hikari.models import voices from hikari.rest import voice from hikari.rest import session @@ -28,12 +28,12 @@ class TestRESTUserLogic: @pytest.fixture() def rest_voice_logic_impl(self): - mock_components = mock.MagicMock(application.Application) + mock_app = mock.MagicMock(application.Application) mock_low_level_restful_client = mock.MagicMock(session.RESTSession) class RESTVoiceLogicImpl(voice.RESTVoiceComponent): def __init__(self): - super().__init__(mock_components, mock_low_level_restful_client) + super().__init__(mock_app, mock_low_level_restful_client) return RESTVoiceLogicImpl() @@ -45,6 +45,4 @@ async def test_fetch_voice_regions(self, rest_voice_logic_impl): with mock.patch.object(voices.VoiceRegion, "deserialize", return_value=mock_voice_obj): assert await rest_voice_logic_impl.fetch_voice_regions() == [mock_voice_obj] rest_voice_logic_impl._session.list_voice_regions.assert_called_once() - voices.VoiceRegion.deserialize.assert_called_once_with( - mock_voice_payload, components=rest_voice_logic_impl._components - ) + voices.VoiceRegion.deserialize.assert_called_once_with(mock_voice_payload, app=rest_voice_logic_impl._app) diff --git a/tests/hikari/clients/test_rest/test_webhook.py b/tests/hikari/rest/test_webhook.py similarity index 95% rename from tests/hikari/clients/test_rest/test_webhook.py rename to tests/hikari/rest/test_webhook.py index 75e7d4e830..e37a3f2baa 100644 --- a/tests/hikari/clients/test_rest/test_webhook.py +++ b/tests/hikari/rest/test_webhook.py @@ -21,26 +21,26 @@ import mock import pytest +from hikari.components import application +from hikari.internal import helpers from hikari.models import embeds from hikari.models import files from hikari.models import messages from hikari.models import webhooks -from hikari.components import application -from hikari.rest import webhook -from hikari.internal import helpers from hikari.rest import session +from hikari.rest import webhook from tests.hikari import _helpers class TestRESTUserLogic: @pytest.fixture() def rest_webhook_logic_impl(self): - mock_components = mock.MagicMock(application.Application) + mock_app = mock.MagicMock(application.Application) mock_low_level_restful_client = mock.MagicMock(session.RESTSession) class RESTWebhookLogicImpl(webhook.RESTWebhookComponent): def __init__(self): - super().__init__(mock_components, mock_low_level_restful_client) + super().__init__(mock_app, mock_low_level_restful_client) return RESTWebhookLogicImpl() @@ -58,9 +58,7 @@ async def test_fetch_webhook_with_webhook_token(self, rest_webhook_logic_impl, w rest_webhook_logic_impl._session.get_webhook.assert_called_once_with( webhook_id="379953393319542784", webhook_token="dsawqoepql.kmsdao", ) - webhooks.Webhook.deserialize.assert_called_once_with( - mock_webhook_payload, components=rest_webhook_logic_impl._components - ) + webhooks.Webhook.deserialize.assert_called_once_with(mock_webhook_payload, app=rest_webhook_logic_impl._app) @pytest.mark.asyncio @_helpers.parametrize_valid_id_formats_for_models("webhook", 379953393319542784, webhooks.Webhook) @@ -73,9 +71,7 @@ async def test_fetch_webhook_without_webhook_token(self, rest_webhook_logic_impl rest_webhook_logic_impl._session.get_webhook.assert_called_once_with( webhook_id="379953393319542784", webhook_token=..., ) - webhooks.Webhook.deserialize.assert_called_once_with( - mock_webhook_payload, components=rest_webhook_logic_impl._components - ) + webhooks.Webhook.deserialize.assert_called_once_with(mock_webhook_payload, app=rest_webhook_logic_impl._app) @pytest.mark.asyncio @_helpers.parametrize_valid_id_formats_for_models("webhook", 379953393319542784, webhooks.Webhook) @@ -107,9 +103,7 @@ async def test_update_webhook_with_optionals(self, rest_webhook_logic_impl, webh channel_id="115590097100865541", reason="A reason", ) - webhooks.Webhook.deserialize.assert_called_once_with( - mock_webhook_payload, components=rest_webhook_logic_impl._components - ) + webhooks.Webhook.deserialize.assert_called_once_with(mock_webhook_payload, app=rest_webhook_logic_impl._app) mock_image_obj.read.assert_awaited_once() @pytest.mark.asyncio @@ -123,9 +117,7 @@ async def test_update_webhook_without_optionals(self, rest_webhook_logic_impl, w rest_webhook_logic_impl._session.modify_webhook.assert_called_once_with( webhook_id="379953393319542784", webhook_token=..., name=..., avatar=..., channel_id=..., reason=..., ) - webhooks.Webhook.deserialize.assert_called_once_with( - mock_webhook_payload, components=rest_webhook_logic_impl._components - ) + webhooks.Webhook.deserialize.assert_called_once_with(mock_webhook_payload, app=rest_webhook_logic_impl._app) @pytest.mark.asyncio @_helpers.parametrize_valid_id_formats_for_models("webhook", 379953393319542784, webhooks.Webhook) @@ -232,9 +224,7 @@ async def test_execute_webhook_returns_message_when_wait_is_true(self, rest_webh assert ( await rest_webhook_logic_impl.execute_webhook(webhook, "a.webhook.token", wait=True) is mock_message_obj ) - messages.Message.deserialize.assert_called_once_with( - mock_message_payload, components=rest_webhook_logic_impl._components - ) + messages.Message.deserialize.assert_called_once_with(mock_message_payload, app=rest_webhook_logic_impl._app) @pytest.mark.asyncio async def test_safe_execute_webhook_without_optionals(self, rest_webhook_logic_impl): diff --git a/tests/hikari/stateless/__init__.py b/tests/hikari/stateless/__init__.py new file mode 100644 index 0000000000..1c1502a5ca --- /dev/null +++ b/tests/hikari/stateless/__init__.py @@ -0,0 +1,18 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# Copyright © Nekoka.tt 2019-2020 +# +# This file is part of Hikari. +# +# Hikari is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Hikari is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Hikari. If not, see . diff --git a/tests/hikari/state/test_stateless.py b/tests/hikari/stateless/test_manager.py similarity index 72% rename from tests/hikari/state/test_stateless.py rename to tests/hikari/stateless/test_manager.py index 9f7256e43e..9d0ca13719 100644 --- a/tests/hikari/state/test_stateless.py +++ b/tests/hikari/stateless/test_manager.py @@ -20,12 +20,12 @@ import pytest from hikari.components import application -from hikari.gateway import client from hikari.events import channel from hikari.events import guild from hikari.events import message from hikari.events import other from hikari.events import voice +from hikari.gateway import client from hikari.stateless import manager @@ -40,7 +40,7 @@ class MockDispatcher: dispatch_event = mock.AsyncMock() return manager.StatelessEventManagerImpl( - components=mock.MagicMock(application.Application, event_dispatcher=MockDispatcher()) + app=mock.MagicMock(application.Application, event_dispatcher=MockDispatcher()) ) @pytest.fixture @@ -56,7 +56,7 @@ async def test_on_connect(self, event_manager_impl, mock_shard): assert event_manager_impl.on_connect.___event_name___ == {"CONNECTED"} event.assert_called_once_with(shard=mock_shard) - event_manager_impl._components.event_dispatcher.dispatch_event.assert_called_once_with(mock_event) + event_manager_impl._app.event_dispatcher.dispatch_event.assert_called_once_with(mock_event) @pytest.mark.asyncio async def test_on_disconnect(self, event_manager_impl, mock_shard): @@ -67,7 +67,7 @@ async def test_on_disconnect(self, event_manager_impl, mock_shard): assert event_manager_impl.on_disconnect.___event_name___ == {"DISCONNECTED"} event.assert_called_once_with(shard=mock_shard) - event_manager_impl._components.event_dispatcher.dispatch_event.assert_called_once_with(mock_event) + event_manager_impl._app.event_dispatcher.dispatch_event.assert_called_once_with(mock_event) @pytest.mark.asyncio async def test_on_resume(self, event_manager_impl, mock_shard): @@ -78,7 +78,7 @@ async def test_on_resume(self, event_manager_impl, mock_shard): assert event_manager_impl.on_resume.___event_name___ == {"RESUME"} event.assert_called_once_with(shard=mock_shard) - event_manager_impl._components.event_dispatcher.dispatch_event.assert_called_once_with(mock_event) + event_manager_impl._app.event_dispatcher.dispatch_event.assert_called_once_with(mock_event) @pytest.mark.asyncio async def test_on_ready(self, event_manager_impl, mock_payload): @@ -88,8 +88,8 @@ async def test_on_ready(self, event_manager_impl, mock_payload): await event_manager_impl.on_ready(None, mock_payload) assert event_manager_impl.on_ready.___event_name___ == {"READY"} - event.assert_called_once_with(mock_payload, components=event_manager_impl._components) - event_manager_impl._components.event_dispatcher.dispatch_event.assert_called_once_with(mock_event) + event.assert_called_once_with(mock_payload, app=event_manager_impl._app) + event_manager_impl._app.event_dispatcher.dispatch_event.assert_called_once_with(mock_event) @pytest.mark.asyncio async def test_on_channel_create(self, event_manager_impl, mock_payload): @@ -99,8 +99,8 @@ async def test_on_channel_create(self, event_manager_impl, mock_payload): await event_manager_impl.on_channel_create(None, mock_payload) assert event_manager_impl.on_channel_create.___event_name___ == {"CHANNEL_CREATE"} - event.assert_called_once_with(mock_payload, components=event_manager_impl._components) - event_manager_impl._components.event_dispatcher.dispatch_event.assert_called_once_with(mock_event) + event.assert_called_once_with(mock_payload, app=event_manager_impl._app) + event_manager_impl._app.event_dispatcher.dispatch_event.assert_called_once_with(mock_event) @pytest.mark.asyncio async def test_on_channel_update(self, event_manager_impl, mock_payload): @@ -110,8 +110,8 @@ async def test_on_channel_update(self, event_manager_impl, mock_payload): await event_manager_impl.on_channel_update(None, mock_payload) assert event_manager_impl.on_channel_update.___event_name___ == {"CHANNEL_UPDATE"} - event.assert_called_once_with(mock_payload, components=event_manager_impl._components) - event_manager_impl._components.event_dispatcher.dispatch_event.assert_called_once_with(mock_event) + event.assert_called_once_with(mock_payload, app=event_manager_impl._app) + event_manager_impl._app.event_dispatcher.dispatch_event.assert_called_once_with(mock_event) @pytest.mark.asyncio async def test_on_channel_delete(self, event_manager_impl, mock_payload): @@ -121,8 +121,8 @@ async def test_on_channel_delete(self, event_manager_impl, mock_payload): await event_manager_impl.on_channel_delete(None, mock_payload) assert event_manager_impl.on_channel_delete.___event_name___ == {"CHANNEL_DELETE"} - event.assert_called_once_with(mock_payload, components=event_manager_impl._components) - event_manager_impl._components.event_dispatcher.dispatch_event.assert_called_once_with(mock_event) + event.assert_called_once_with(mock_payload, app=event_manager_impl._app) + event_manager_impl._app.event_dispatcher.dispatch_event.assert_called_once_with(mock_event) @pytest.mark.asyncio async def test_on_channel_pins_update(self, event_manager_impl, mock_payload): @@ -132,8 +132,8 @@ async def test_on_channel_pins_update(self, event_manager_impl, mock_payload): await event_manager_impl.on_channel_pins_update(None, mock_payload) assert event_manager_impl.on_channel_pins_update.___event_name___ == {"CHANNEL_PINS_UPDATE"} - event.assert_called_once_with(mock_payload, components=event_manager_impl._components) - event_manager_impl._components.event_dispatcher.dispatch_event.assert_called_once_with(mock_event) + event.assert_called_once_with(mock_payload, app=event_manager_impl._app) + event_manager_impl._app.event_dispatcher.dispatch_event.assert_called_once_with(mock_event) @pytest.mark.asyncio async def test_on_guild_create(self, event_manager_impl, mock_payload): @@ -143,8 +143,8 @@ async def test_on_guild_create(self, event_manager_impl, mock_payload): await event_manager_impl.on_guild_create(None, mock_payload) assert event_manager_impl.on_guild_create.___event_name___ == {"GUILD_CREATE"} - event.assert_called_once_with(mock_payload, components=event_manager_impl._components) - event_manager_impl._components.event_dispatcher.dispatch_event.assert_called_once_with(mock_event) + event.assert_called_once_with(mock_payload, app=event_manager_impl._app) + event_manager_impl._app.event_dispatcher.dispatch_event.assert_called_once_with(mock_event) @pytest.mark.asyncio async def test_on_guild_update(self, event_manager_impl, mock_payload): @@ -154,8 +154,8 @@ async def test_on_guild_update(self, event_manager_impl, mock_payload): await event_manager_impl.on_guild_update(None, mock_payload) assert event_manager_impl.on_guild_update.___event_name___ == {"GUILD_UPDATE"} - event.assert_called_once_with(mock_payload, components=event_manager_impl._components) - event_manager_impl._components.event_dispatcher.dispatch_event.assert_called_once_with(mock_event) + event.assert_called_once_with(mock_payload, app=event_manager_impl._app) + event_manager_impl._app.event_dispatcher.dispatch_event.assert_called_once_with(mock_event) @pytest.mark.asyncio async def test_on_guild_delete_handles_guild_leave(self, event_manager_impl, mock_payload): @@ -166,8 +166,8 @@ async def test_on_guild_delete_handles_guild_leave(self, event_manager_impl, moc await event_manager_impl.on_guild_delete(None, mock_payload) assert event_manager_impl.on_guild_delete.___event_name___ == {"GUILD_DELETE"} - event.assert_called_once_with(mock_payload, components=event_manager_impl._components) - event_manager_impl._components.event_dispatcher.dispatch_event.assert_called_once_with(mock_event) + event.assert_called_once_with(mock_payload, app=event_manager_impl._app) + event_manager_impl._app.event_dispatcher.dispatch_event.assert_called_once_with(mock_event) @pytest.mark.asyncio async def test_on_guild_delete_handles_guild_unavailable(self, event_manager_impl, mock_payload): @@ -178,8 +178,8 @@ async def test_on_guild_delete_handles_guild_unavailable(self, event_manager_imp await event_manager_impl.on_guild_delete(None, mock_payload) assert event_manager_impl.on_guild_delete.___event_name___ == {"GUILD_DELETE"} - event.assert_called_once_with(mock_payload, components=event_manager_impl._components) - event_manager_impl._components.event_dispatcher.dispatch_event.assert_called_once_with(mock_event) + event.assert_called_once_with(mock_payload, app=event_manager_impl._app) + event_manager_impl._app.event_dispatcher.dispatch_event.assert_called_once_with(mock_event) @pytest.mark.asyncio async def test_on_guild_ban_add(self, event_manager_impl, mock_payload): @@ -189,8 +189,8 @@ async def test_on_guild_ban_add(self, event_manager_impl, mock_payload): await event_manager_impl.on_guild_ban_add(None, mock_payload) assert event_manager_impl.on_guild_ban_add.___event_name___ == {"GUILD_BAN_ADD"} - event.assert_called_once_with(mock_payload, components=event_manager_impl._components) - event_manager_impl._components.event_dispatcher.dispatch_event.assert_called_once_with(mock_event) + event.assert_called_once_with(mock_payload, app=event_manager_impl._app) + event_manager_impl._app.event_dispatcher.dispatch_event.assert_called_once_with(mock_event) @pytest.mark.asyncio async def test_on_guild_ban_remove(self, event_manager_impl, mock_payload): @@ -200,8 +200,8 @@ async def test_on_guild_ban_remove(self, event_manager_impl, mock_payload): await event_manager_impl.on_guild_ban_remove(None, mock_payload) assert event_manager_impl.on_guild_ban_remove.___event_name___ == {"GUILD_BAN_REMOVE"} - event.assert_called_once_with(mock_payload, components=event_manager_impl._components) - event_manager_impl._components.event_dispatcher.dispatch_event.assert_called_once_with(mock_event) + event.assert_called_once_with(mock_payload, app=event_manager_impl._app) + event_manager_impl._app.event_dispatcher.dispatch_event.assert_called_once_with(mock_event) @pytest.mark.asyncio async def test_on_guild_emojis_update(self, event_manager_impl, mock_payload): @@ -211,8 +211,8 @@ async def test_on_guild_emojis_update(self, event_manager_impl, mock_payload): await event_manager_impl.on_guild_emojis_update(None, mock_payload) assert event_manager_impl.on_guild_emojis_update.___event_name___ == {"GUILD_EMOJIS_UPDATE"} - event.assert_called_once_with(mock_payload, components=event_manager_impl._components) - event_manager_impl._components.event_dispatcher.dispatch_event.assert_called_once_with(mock_event) + event.assert_called_once_with(mock_payload, app=event_manager_impl._app) + event_manager_impl._app.event_dispatcher.dispatch_event.assert_called_once_with(mock_event) @pytest.mark.asyncio async def test_on_guild_integrations_update(self, event_manager_impl, mock_payload): @@ -224,8 +224,8 @@ async def test_on_guild_integrations_update(self, event_manager_impl, mock_paylo await event_manager_impl.on_guild_integrations_update(None, mock_payload) assert event_manager_impl.on_guild_integrations_update.___event_name___ == {"GUILD_INTEGRATIONS_UPDATE"} - event.assert_called_once_with(mock_payload, components=event_manager_impl._components) - event_manager_impl._components.event_dispatcher.dispatch_event.assert_called_once_with(mock_event) + event.assert_called_once_with(mock_payload, app=event_manager_impl._app) + event_manager_impl._app.event_dispatcher.dispatch_event.assert_called_once_with(mock_event) @pytest.mark.asyncio async def test_on_guild_member_add(self, event_manager_impl, mock_payload): @@ -235,8 +235,8 @@ async def test_on_guild_member_add(self, event_manager_impl, mock_payload): await event_manager_impl.on_guild_member_add(None, mock_payload) assert event_manager_impl.on_guild_member_add.___event_name___ == {"GUILD_MEMBER_ADD"} - event.assert_called_once_with(mock_payload, components=event_manager_impl._components) - event_manager_impl._components.event_dispatcher.dispatch_event.assert_called_once_with(mock_event) + event.assert_called_once_with(mock_payload, app=event_manager_impl._app) + event_manager_impl._app.event_dispatcher.dispatch_event.assert_called_once_with(mock_event) @pytest.mark.asyncio async def test_on_guild_member_update(self, event_manager_impl, mock_payload): @@ -246,8 +246,8 @@ async def test_on_guild_member_update(self, event_manager_impl, mock_payload): await event_manager_impl.on_guild_member_update(None, mock_payload) assert event_manager_impl.on_guild_member_update.___event_name___ == {"GUILD_MEMBER_UPDATE"} - event.assert_called_once_with(mock_payload, components=event_manager_impl._components) - event_manager_impl._components.event_dispatcher.dispatch_event.assert_called_once_with(mock_event) + event.assert_called_once_with(mock_payload, app=event_manager_impl._app) + event_manager_impl._app.event_dispatcher.dispatch_event.assert_called_once_with(mock_event) @pytest.mark.asyncio async def test_on_guild_member_remove(self, event_manager_impl, mock_payload): @@ -257,8 +257,8 @@ async def test_on_guild_member_remove(self, event_manager_impl, mock_payload): await event_manager_impl.on_guild_member_remove(None, mock_payload) assert event_manager_impl.on_guild_member_remove.___event_name___ == {"GUILD_MEMBER_REMOVE"} - event.assert_called_once_with(mock_payload, components=event_manager_impl._components) - event_manager_impl._components.event_dispatcher.dispatch_event.assert_called_once_with(mock_event) + event.assert_called_once_with(mock_payload, app=event_manager_impl._app) + event_manager_impl._app.event_dispatcher.dispatch_event.assert_called_once_with(mock_event) @pytest.mark.asyncio async def test_on_guild_role_create(self, event_manager_impl, mock_payload): @@ -268,8 +268,8 @@ async def test_on_guild_role_create(self, event_manager_impl, mock_payload): await event_manager_impl.on_guild_role_create(None, mock_payload) assert event_manager_impl.on_guild_role_create.___event_name___ == {"GUILD_ROLE_CREATE"} - event.assert_called_once_with(mock_payload, components=event_manager_impl._components) - event_manager_impl._components.event_dispatcher.dispatch_event.assert_called_once_with(mock_event) + event.assert_called_once_with(mock_payload, app=event_manager_impl._app) + event_manager_impl._app.event_dispatcher.dispatch_event.assert_called_once_with(mock_event) @pytest.mark.asyncio async def test_on_guild_role_update(self, event_manager_impl, mock_payload): @@ -279,8 +279,8 @@ async def test_on_guild_role_update(self, event_manager_impl, mock_payload): await event_manager_impl.on_guild_role_update(None, mock_payload) assert event_manager_impl.on_guild_role_update.___event_name___ == {"GUILD_ROLE_UPDATE"} - event.assert_called_once_with(mock_payload, components=event_manager_impl._components) - event_manager_impl._components.event_dispatcher.dispatch_event.assert_called_once_with(mock_event) + event.assert_called_once_with(mock_payload, app=event_manager_impl._app) + event_manager_impl._app.event_dispatcher.dispatch_event.assert_called_once_with(mock_event) @pytest.mark.asyncio async def test_on_guild_role_delete(self, event_manager_impl, mock_payload): @@ -290,8 +290,8 @@ async def test_on_guild_role_delete(self, event_manager_impl, mock_payload): await event_manager_impl.on_guild_role_delete(None, mock_payload) assert event_manager_impl.on_guild_role_delete.___event_name___ == {"GUILD_ROLE_DELETE"} - event.assert_called_once_with(mock_payload, components=event_manager_impl._components) - event_manager_impl._components.event_dispatcher.dispatch_event.assert_called_once_with(mock_event) + event.assert_called_once_with(mock_payload, app=event_manager_impl._app) + event_manager_impl._app.event_dispatcher.dispatch_event.assert_called_once_with(mock_event) @pytest.mark.asyncio async def test_on_invite_create(self, event_manager_impl, mock_payload): @@ -301,8 +301,8 @@ async def test_on_invite_create(self, event_manager_impl, mock_payload): await event_manager_impl.on_invite_create(None, mock_payload) assert event_manager_impl.on_invite_create.___event_name___ == {"INVITE_CREATE"} - event.assert_called_once_with(mock_payload, components=event_manager_impl._components) - event_manager_impl._components.event_dispatcher.dispatch_event.assert_called_once_with(mock_event) + event.assert_called_once_with(mock_payload, app=event_manager_impl._app) + event_manager_impl._app.event_dispatcher.dispatch_event.assert_called_once_with(mock_event) @pytest.mark.asyncio async def test_on_invite_delete(self, event_manager_impl, mock_payload): @@ -312,8 +312,8 @@ async def test_on_invite_delete(self, event_manager_impl, mock_payload): await event_manager_impl.on_invite_delete(None, mock_payload) assert event_manager_impl.on_invite_delete.___event_name___ == {"INVITE_DELETE"} - event.assert_called_once_with(mock_payload, components=event_manager_impl._components) - event_manager_impl._components.event_dispatcher.dispatch_event.assert_called_once_with(mock_event) + event.assert_called_once_with(mock_payload, app=event_manager_impl._app) + event_manager_impl._app.event_dispatcher.dispatch_event.assert_called_once_with(mock_event) @pytest.mark.asyncio async def test_on_message_create_without_member_payload(self, event_manager_impl): @@ -324,8 +324,8 @@ async def test_on_message_create_without_member_payload(self, event_manager_impl await event_manager_impl.on_message_create(None, mock_payload) assert event_manager_impl.on_message_create.___event_name___ == {"MESSAGE_CREATE"} - event.assert_called_once_with(mock_payload, components=event_manager_impl._components) - event_manager_impl._components.event_dispatcher.dispatch_event.assert_called_once_with(mock_event) + event.assert_called_once_with(mock_payload, app=event_manager_impl._app) + event_manager_impl._app.event_dispatcher.dispatch_event.assert_called_once_with(mock_event) @pytest.mark.asyncio async def test_on_message_create_injects_user_into_member_payload(self, event_manager_impl): @@ -342,9 +342,9 @@ async def test_on_message_create_injects_user_into_member_payload(self, event_ma "author": {"id": "111", "username": "okokok", "discrim": "4242"}, "member": {"user": {"id": "111", "username": "okokok", "discrim": "4242"}}, }, - components=event_manager_impl._components, + app=event_manager_impl._app, ) - event_manager_impl._components.event_dispatcher.dispatch_event.assert_called_once_with(mock_event) + event_manager_impl._app.event_dispatcher.dispatch_event.assert_called_once_with(mock_event) @pytest.mark.asyncio async def test_on_message_update_without_member_payload(self, event_manager_impl, mock_payload): @@ -355,8 +355,8 @@ async def test_on_message_update_without_member_payload(self, event_manager_impl await event_manager_impl.on_message_update(None, mock_payload) assert event_manager_impl.on_message_update.___event_name___ == {"MESSAGE_UPDATE"} - event.assert_called_once_with(mock_payload, components=event_manager_impl._components) - event_manager_impl._components.event_dispatcher.dispatch_event.assert_called_once_with(mock_event) + event.assert_called_once_with(mock_payload, app=event_manager_impl._app) + event_manager_impl._app.event_dispatcher.dispatch_event.assert_called_once_with(mock_event) @pytest.mark.asyncio async def test_on_message_update_injects_user_into_member_payload(self, event_manager_impl, mock_payload): @@ -373,9 +373,9 @@ async def test_on_message_update_injects_user_into_member_payload(self, event_ma "author": {"id": "111", "username": "okokok", "discrim": "4242"}, "member": {"user": {"id": "111", "username": "okokok", "discrim": "4242"}}, }, - components=event_manager_impl._components, + app=event_manager_impl._app, ) - event_manager_impl._components.event_dispatcher.dispatch_event.assert_called_once_with(mock_event) + event_manager_impl._app.event_dispatcher.dispatch_event.assert_called_once_with(mock_event) @pytest.mark.asyncio async def test_on_message_delete(self, event_manager_impl, mock_payload): @@ -385,8 +385,8 @@ async def test_on_message_delete(self, event_manager_impl, mock_payload): await event_manager_impl.on_message_delete(None, mock_payload) assert event_manager_impl.on_message_delete.___event_name___ == {"MESSAGE_DELETE"} - event.assert_called_once_with(mock_payload, components=event_manager_impl._components) - event_manager_impl._components.event_dispatcher.dispatch_event.assert_called_once_with(mock_event) + event.assert_called_once_with(mock_payload, app=event_manager_impl._app) + event_manager_impl._app.event_dispatcher.dispatch_event.assert_called_once_with(mock_event) @pytest.mark.asyncio async def test_on_message_delete_bulk(self, event_manager_impl, mock_payload): @@ -396,8 +396,8 @@ async def test_on_message_delete_bulk(self, event_manager_impl, mock_payload): await event_manager_impl.on_message_delete_bulk(None, mock_payload) assert event_manager_impl.on_message_delete_bulk.___event_name___ == {"MESSAGE_DELETE_BULK"} - event.assert_called_once_with(mock_payload, components=event_manager_impl._components) - event_manager_impl._components.event_dispatcher.dispatch_event.assert_called_once_with(mock_event) + event.assert_called_once_with(mock_payload, app=event_manager_impl._app) + event_manager_impl._app.event_dispatcher.dispatch_event.assert_called_once_with(mock_event) @pytest.mark.asyncio async def test_on_message_reaction_add(self, event_manager_impl, mock_payload): @@ -407,8 +407,8 @@ async def test_on_message_reaction_add(self, event_manager_impl, mock_payload): await event_manager_impl.on_message_reaction_add(None, mock_payload) assert event_manager_impl.on_message_reaction_add.___event_name___ == {"MESSAGE_REACTION_ADD"} - event.assert_called_once_with(mock_payload, components=event_manager_impl._components) - event_manager_impl._components.event_dispatcher.dispatch_event.assert_called_once_with(mock_event) + event.assert_called_once_with(mock_payload, app=event_manager_impl._app) + event_manager_impl._app.event_dispatcher.dispatch_event.assert_called_once_with(mock_event) @pytest.mark.asyncio async def test_on_message_reaction_remove(self, event_manager_impl, mock_payload): @@ -421,8 +421,8 @@ async def test_on_message_reaction_remove(self, event_manager_impl, mock_payload await event_manager_impl.on_message_reaction_remove(None, mock_payload) assert event_manager_impl.on_message_reaction_remove.___event_name___ == {"MESSAGE_REACTION_REMOVE"} - event.assert_called_once_with({"emoji": {"animated": None}}, components=event_manager_impl._components) - event_manager_impl._components.event_dispatcher.dispatch_event.assert_called_once_with(mock_event) + event.assert_called_once_with({"emoji": {"animated": None}}, app=event_manager_impl._app) + event_manager_impl._app.event_dispatcher.dispatch_event.assert_called_once_with(mock_event) @pytest.mark.asyncio async def test_on_message_reaction_remove_emoji(self, event_manager_impl, mock_payload): @@ -437,8 +437,8 @@ async def test_on_message_reaction_remove_emoji(self, event_manager_impl, mock_p assert event_manager_impl.on_message_reaction_remove_emoji.___event_name___ == { "MESSAGE_REACTION_REMOVE_EMOJI" } - event.assert_called_once_with({"emoji": {"animated": None}}, components=event_manager_impl._components) - event_manager_impl._components.event_dispatcher.dispatch_event.assert_called_once_with(mock_event) + event.assert_called_once_with({"emoji": {"animated": None}}, app=event_manager_impl._app) + event_manager_impl._app.event_dispatcher.dispatch_event.assert_called_once_with(mock_event) @pytest.mark.asyncio async def test_on_presence_update(self, event_manager_impl, mock_payload): @@ -448,8 +448,8 @@ async def test_on_presence_update(self, event_manager_impl, mock_payload): await event_manager_impl.on_presence_update(None, mock_payload) assert event_manager_impl.on_presence_update.___event_name___ == {"PRESENCE_UPDATE"} - event.assert_called_once_with(mock_payload, components=event_manager_impl._components) - event_manager_impl._components.event_dispatcher.dispatch_event.assert_called_once_with(mock_event) + event.assert_called_once_with(mock_payload, app=event_manager_impl._app) + event_manager_impl._app.event_dispatcher.dispatch_event.assert_called_once_with(mock_event) @pytest.mark.asyncio async def test_on_typing_start(self, event_manager_impl, mock_payload): @@ -459,8 +459,8 @@ async def test_on_typing_start(self, event_manager_impl, mock_payload): await event_manager_impl.on_typing_start(None, mock_payload) assert event_manager_impl.on_typing_start.___event_name___ == {"TYPING_START"} - event.assert_called_once_with(mock_payload, components=event_manager_impl._components) - event_manager_impl._components.event_dispatcher.dispatch_event.assert_called_once_with(mock_event) + event.assert_called_once_with(mock_payload, app=event_manager_impl._app) + event_manager_impl._app.event_dispatcher.dispatch_event.assert_called_once_with(mock_event) @pytest.mark.asyncio async def test_on_my_user_update(self, event_manager_impl, mock_payload): @@ -470,8 +470,8 @@ async def test_on_my_user_update(self, event_manager_impl, mock_payload): await event_manager_impl.on_my_user_update(None, mock_payload) assert event_manager_impl.on_my_user_update.___event_name___ == {"USER_UPDATE"} - event.assert_called_once_with(mock_payload, components=event_manager_impl._components) - event_manager_impl._components.event_dispatcher.dispatch_event.assert_called_once_with(mock_event) + event.assert_called_once_with(mock_payload, app=event_manager_impl._app) + event_manager_impl._app.event_dispatcher.dispatch_event.assert_called_once_with(mock_event) @pytest.mark.asyncio async def test_on_voice_state_update(self, event_manager_impl, mock_payload): @@ -481,8 +481,8 @@ async def test_on_voice_state_update(self, event_manager_impl, mock_payload): await event_manager_impl.on_voice_state_update(None, mock_payload) assert event_manager_impl.on_voice_state_update.___event_name___ == {"VOICE_STATE_UPDATE"} - event.assert_called_once_with(mock_payload, components=event_manager_impl._components) - event_manager_impl._components.event_dispatcher.dispatch_event.assert_called_once_with(mock_event) + event.assert_called_once_with(mock_payload, app=event_manager_impl._app) + event_manager_impl._app.event_dispatcher.dispatch_event.assert_called_once_with(mock_event) @pytest.mark.asyncio async def test_on_voice_server_update(self, event_manager_impl, mock_payload): @@ -492,8 +492,8 @@ async def test_on_voice_server_update(self, event_manager_impl, mock_payload): await event_manager_impl.on_voice_server_update(None, mock_payload) assert event_manager_impl.on_voice_server_update.___event_name___ == {"VOICE_SERVER_UPDATE"} - event.assert_called_once_with(mock_payload, components=event_manager_impl._components) - event_manager_impl._components.event_dispatcher.dispatch_event.assert_called_once_with(mock_event) + event.assert_called_once_with(mock_payload, app=event_manager_impl._app) + event_manager_impl._app.event_dispatcher.dispatch_event.assert_called_once_with(mock_event) @pytest.mark.asyncio async def test_on_webhook_update(self, event_manager_impl, mock_payload): @@ -503,5 +503,5 @@ async def test_on_webhook_update(self, event_manager_impl, mock_payload): await event_manager_impl.on_webhook_update(None, mock_payload) assert event_manager_impl.on_webhook_update.___event_name___ == {"WEBHOOK_UPDATE"} - event.assert_called_once_with(mock_payload, components=event_manager_impl._components) - event_manager_impl._components.event_dispatcher.dispatch_event.assert_called_once_with(mock_event) + event.assert_called_once_with(mock_payload, app=event_manager_impl._app) + event_manager_impl._app.event_dispatcher.dispatch_event.assert_called_once_with(mock_event) diff --git a/tests/hikari/clients/test_configs.py b/tests/hikari/test_configs.py similarity index 98% rename from tests/hikari/clients/test_configs.py rename to tests/hikari/test_configs.py index 0b9046a9d9..e5149bf294 100644 --- a/tests/hikari/clients/test_configs.py +++ b/tests/hikari/test_configs.py @@ -15,7 +15,7 @@ # GNU Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public License -# along ith Hikari. If not, see . +# along with Hikari. If not, see . import datetime import ssl @@ -23,9 +23,11 @@ import mock import pytest -from hikari.models import guilds, intents, gateway from hikari import configs from hikari.internal import urls +from hikari.models import gateway +from hikari.models import guilds +from hikari.models import intents from tests.hikari import _helpers From 495fdf81bc850c642296549461b3ad0cd56f74ce Mon Sep 17 00:00:00 2001 From: Faster Speeding Date: Mon, 18 May 2020 04:23:34 +0100 Subject: [PATCH 354/922] Bump version to 2.0.0.dev0 in-preparation for marshaller removal and entity parsing rework. --- hikari/_about.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hikari/_about.py b/hikari/_about.py index f3a310275a..1faea49cd3 100644 --- a/hikari/_about.py +++ b/hikari/_about.py @@ -29,4 +29,4 @@ __issue_tracker__ = "https://gitlab.com/nekokatt/hikari/issues" __license__ = "LGPL-3.0-ONLY" __url__ = "https://gitlab.com/nekokatt/hikari" -__version__ = "1.0.1.dev" +__version__ = "2.0.0.dev0" From 724c0ab40d5e2a34c5313ddea7dc5d70575e7148 Mon Sep 17 00:00:00 2001 From: Faster Speeding Date: Mon, 18 May 2020 08:24:21 +0100 Subject: [PATCH 355/922] Fix outdated doc links --- hikari/components/bot_base.py | 16 +- hikari/components/consumers.py | 2 +- hikari/components/event_managers.py | 6 +- hikari/components/intent_aware_dispatchers.py | 2 +- hikari/configs.py | 16 +- hikari/errors.py | 2 +- hikari/events/base.py | 6 +- hikari/events/guild.py | 2 +- hikari/events/message.py | 2 +- hikari/gateway/client.py | 10 +- hikari/gateway/connection.py | 2 +- hikari/internal/helpers.py | 4 +- hikari/internal/marshaller.py | 4 +- hikari/internal/ratelimits.py | 20 +- hikari/models/audit_logs.py | 4 +- hikari/models/channels.py | 4 +- hikari/models/colours.py | 2 +- hikari/models/embeds.py | 16 +- hikari/models/emojis.py | 6 +- hikari/models/guilds.py | 18 +- hikari/models/invites.py | 4 +- hikari/models/messages.py | 30 +-- hikari/models/users.py | 4 +- hikari/models/webhooks.py | 22 +- hikari/rest/base.py | 2 +- hikari/rest/buckets.py | 6 +- hikari/rest/channel.py | 112 ++++---- hikari/rest/client.py | 6 +- hikari/rest/guild.py | 252 +++++++++--------- hikari/rest/invite.py | 6 +- hikari/rest/me.py | 18 +- hikari/rest/oauth2.py | 2 +- hikari/rest/react.py | 30 +-- hikari/rest/session.py | 4 +- hikari/rest/user.py | 4 +- hikari/rest/voice.py | 2 +- hikari/rest/webhook.py | 26 +- 37 files changed, 337 insertions(+), 337 deletions(-) diff --git a/hikari/components/bot_base.py b/hikari/components/bot_base.py index bb76cbd0d2..2582fe9a48 100644 --- a/hikari/components/bot_base.py +++ b/hikari/components/bot_base.py @@ -61,10 +61,10 @@ class BotBase( Parameters ---------- - config : hikari.app.configs.BotConfig + config : hikari.configs.BotConfig The config object to use. **kwargs - Parameters to use to create a `hikari.app.configs.BotConfig` + Parameters to use to create a `hikari.configs.BotConfig` from, instead of passing a raw config object. Examples @@ -104,7 +104,7 @@ def heartbeat_latency(self) -> float: This will return a mean of all the heartbeat intervals for all shards with a valid heartbeat latency that are in the - `hikari.clients.shard_states.GatewayState.READY` state. + `hikari.gateway.gateway_state.GatewayState.READY` state. If no shards are in this state, this will return `float("nan")` instead. @@ -284,9 +284,9 @@ async def update_presence( Parameters ---------- - status : hikari.guilds.PresenceStatus + status : hikari.models.guilds.PresenceStatus If specified, the new status to set. - activity : hikari.gateway_entities.Activity, optional + activity : hikari.models.gateway.Activity, optional If specified, the new activity to set. idle_since : datetime.datetime, optional If specified, the time to show up as being idle since, @@ -330,7 +330,7 @@ def _create_shard( Returns ------- - hikari.clients.client.GatewayClient + hikari.gateway.client.GatewayClient The shard client implementation to use for the given shard ID. """ @@ -346,7 +346,7 @@ def _create_rest(app: application.Application) -> rest_client.RESTClient: Returns ------- - hikari.clients.rest.RESTClient + hikari.components.rest.RESTClient The RESTSession client to use. """ @@ -373,7 +373,7 @@ def _create_event_dispatcher(config: configs.BotConfig) -> dispatchers.EventDisp Parameters ---------- - config : hikari.app.configs.BotConfig + config : hikari.configs.BotConfig The bot config to use. Returns diff --git a/hikari/components/consumers.py b/hikari/components/consumers.py index 0c154e7f80..1b15affbd2 100644 --- a/hikari/components/consumers.py +++ b/hikari/components/consumers.py @@ -51,7 +51,7 @@ async def process_raw_event( Parameters ---------- - shard_client_obj : hikari.clients.client.GatewayClient + shard_client_obj : hikari.gateway.client.GatewayClient The client for the shard that received the event. name : str The raw event name. diff --git a/hikari/components/event_managers.py b/hikari/components/event_managers.py index 665d805f65..b9faf8da17 100644 --- a/hikari/components/event_managers.py +++ b/hikari/components/event_managers.py @@ -82,7 +82,7 @@ class EventManager(typing.Generic[EventDispatcherT], consumers.RawEventConsumer) """Abstract definition of the application for an event system for a bot. The class itself inherits from - `hikari.state.consumers.RawEventConsumer` (which allows it to provide the + `hikari.components.consumers.RawEventConsumer` (which allows it to provide the ability to transform a raw payload into an event object). This is designed as a basis to enable transformation of raw incoming events @@ -96,7 +96,7 @@ class EventManager(typing.Generic[EventDispatcherT], consumers.RawEventConsumer) These methods are expected to have the following parameters: - * shard_obj : `hikari.clients.client.GatewayClient` + * shard_obj : `hikari.gateway.client.GatewayClient` The shard client that emitted the event. @@ -137,7 +137,7 @@ def _process_message_create(self, shard, payload) -> MessageCreateEvent: Parameters ---------- - app: hikari.clients.application.Application + app: hikari.components.application.Application The client application that this event manager should be bound to. Includes the event dispatcher that will store individual events and manage dispatching them after this object creates them. diff --git a/hikari/components/intent_aware_dispatchers.py b/hikari/components/intent_aware_dispatchers.py index 99e9180e99..bfb3f67943 100644 --- a/hikari/components/intent_aware_dispatchers.py +++ b/hikari/components/intent_aware_dispatchers.py @@ -59,7 +59,7 @@ class IntentAwareEventDispatcherImpl(dispatchers.EventDispatcher): Parameters ---------- - enabled_intents : hikari.intents.Intent, optional + enabled_intents : hikari.models.intents.Intent, optional The intents that are enabled for the application. If `None`, then no intent checks are performed when subscribing a new event. """ diff --git a/hikari/configs.py b/hikari/configs.py index a9b4b984fd..f1e8b3d163 100644 --- a/hikari/configs.py +++ b/hikari/configs.py @@ -222,10 +222,10 @@ class GatewayConfig(AIOHTTPConfig, DebugConfig, TokenConfig): Usually you want this turned on. gateway_version : int The gateway API version to use. Defaults to v6 - initial_activity : hikari.gateway_entities.Activity, optional + initial_activity : hikari.models.gateway.Activity, optional The initial activity to set all shards to when starting the gateway. If this is `None` then no activity will be set, this is the default. - initial_status : hikari.guilds.PresenceStatus + initial_status : hikari.models.guilds.PresenceStatus The initial status to set the shards to when starting the gateway. Defaults to `ONLINE`. initial_is_afk : bool @@ -233,7 +233,7 @@ class GatewayConfig(AIOHTTPConfig, DebugConfig, TokenConfig): initial_idle_since : datetime.datetime, optional The idle time to show on signing in. If set to `None` to not show an idle time, this is the default. - intents : hikari.intents.Intent + intents : hikari.models.intents.Intent The intents to use for the connection. If being deserialized, this can be an integer bitfield, or a sequence of intent names. If unspecified, this will be set to `None`. @@ -305,7 +305,7 @@ class GatewayConfig(AIOHTTPConfig, DebugConfig, TokenConfig): sequence of intent names. If unspecified, `intents` will be set to `None`. - See `hikari.intents.Intent` for valid names of intents you + See `hikari.models.intents.Intent` for valid names of intents you can use. Integer values are as documented on Discord's developer portal. !!! warning @@ -466,10 +466,10 @@ class BotConfig(RESTConfig, GatewayConfig): Usually you want this turned on. gateway_version : int The gateway API version to use. Defaults to v6 - initial_activity : hikari.gateway_entities.Activity, optional + initial_activity : hikari.models.gateway.Activity, optional The initial activity to set all shards to when starting the gateway. If this is `None` then no activity will be set, this is the default. - initial_status : hikari.guilds.PresenceStatus + initial_status : hikari.models.guilds.PresenceStatus The initial status to set the shards to when starting the gateway. Defaults to `ONLINE`. initial_is_afk : bool @@ -477,7 +477,7 @@ class BotConfig(RESTConfig, GatewayConfig): initial_idle_since : datetime.datetime, optional The idle time to show on signing in. If set to `None` to not show an idle time, this is the default. - intents : hikari.intents.Intent + intents : hikari.models.intents.Intent The intents to use for the connection. If being deserialized, this can be an integer bitfield, or a sequence of intent names. If unspecified, this will be set to `None`. @@ -564,7 +564,7 @@ class BotConfig(RESTConfig, GatewayConfig): sequence of intent names. If unspecified, `intents` will be set to `None`. - See `hikari.intents.Intent` for valid names of intents you + See `hikari.models.intents.Intent` for valid names of intents you can use. Integer values are as documented on Discord's developer portal. !!! warning diff --git a/hikari/errors.py b/hikari/errors.py index 5ca1db9d94..d86a143bfc 100644 --- a/hikari/errors.py +++ b/hikari/errors.py @@ -134,7 +134,7 @@ class GatewayServerClosedConnectionError(GatewayError): Parameters ---------- - close_code : typing.Union[hikari.net.codes.GatewayCloseCode, int], optional + close_code : typing.Union[hikari.internal.codes.GatewayCloseCode, int], optional The close code provided by the server, if there was one. reason : str, optional A string explaining the issue. diff --git a/hikari/events/base.py b/hikari/events/base.py index 160c20b20b..2d4a804d2a 100644 --- a/hikari/events/base.py +++ b/hikari/events/base.py @@ -57,7 +57,7 @@ def get_required_intents_for(event_type: typing.Type[HikariEvent]) -> typing.Col Returns ------- - typing.Collection[hikari.intents.Intent] + typing.Collection[hikari.models.intents.Intent] Collection of acceptable subset combinations of intent needed to be able to receive the given event type. """ @@ -71,10 +71,10 @@ def requires_intents( Parameters ---------- - first : hikari.intents.Intent + first : hikari.models.intents.Intent First combination of intents that are acceptable in order to receive the decorated event type. - *rest : hikari.intents.Intent + *rest : hikari.models.intents.Intent Zero or more additional combinations of intents to require for this event to be subscribed to. diff --git a/hikari/events/guild.py b/hikari/events/guild.py index 24859c4cfc..101c2bc70c 100644 --- a/hikari/events/guild.py +++ b/hikari/events/guild.py @@ -202,7 +202,7 @@ class GuildMemberUpdateEvent(base_events.HikariEvent, marshaller.Deserializable) """This member's nickname. When set to `None`, this has been removed and when set to - `hikari.unset.UNSET` this hasn't been acted on. + `hikari.models.unset.UNSET` this hasn't been acted on. """ premium_since: typing.Union[None, datetime.datetime, unset.Unset] = marshaller.attrib( diff --git a/hikari/events/message.py b/hikari/events/message.py index c2923dbec3..fcf5e988fe 100644 --- a/hikari/events/message.py +++ b/hikari/events/message.py @@ -93,7 +93,7 @@ class MessageUpdateEvent(base_events.HikariEvent, base_models.Unique, marshaller !!! note All fields on this model except `MessageUpdateEvent.channel_id` and - `MessageUpdateEvent.id` may be set to `hikari.unset.UNSET` (a singleton) + `MessageUpdateEvent.id` may be set to `hikari.models.unset.UNSET` (a singleton) we have not received information about their state from Discord alongside field nullability. """ diff --git a/hikari/gateway/client.py b/hikari/gateway/client.py index 812da3cf82..7839ce3a5e 100644 --- a/hikari/gateway/client.py +++ b/hikari/gateway/client.py @@ -16,7 +16,7 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . -"""Provides a facade around `hikari.net.shards.Shard`. +"""Provides a facade around `hikari.gateway.connection.Shard`. This handles parsing and initializing the object from a configuration, as well as restarting it if it disconnects. @@ -66,7 +66,7 @@ class GatewayClient(runnable.RunnableClient): The ID of this specific shard. shard_id : int The number of shards that make up this distributed application. - app : hikari.clients.application.Application + app : hikari.components.application.Application The client application that this shard client should be bound by. Includes the the gateway configuration to use to initialize this shard and the consumer of a raw event. @@ -75,7 +75,7 @@ class GatewayClient(runnable.RunnableClient): !!! note Generally, you want to use - `hikari.clients.bot_base.BotBase` rather than this class + `hikari.components.bot_base.BotBase` rather than this class directly, as that will handle sharding where enabled and applicable, and provides a few more bits and pieces that may be useful such as state management and event dispatcher integration. and If you want to customize @@ -434,9 +434,9 @@ async def update_presence( Parameters ---------- - status : hikari.guilds.PresenceStatus + status : hikari.models.guilds.PresenceStatus If specified, the new status to set. - activity : hikari.gateway_entities.Activity, optional + activity : hikari.models.gateway.Activity, optional If specified, the new activity to set. idle_since : datetime.datetime, optional If specified, the time to show up as being idle since, or diff --git a/hikari/gateway/connection.py b/hikari/gateway/connection.py index 62bfdc71be..6ef597860e 100644 --- a/hikari/gateway/connection.py +++ b/hikari/gateway/connection.py @@ -113,7 +113,7 @@ class Shard(http_client.HTTPClient): # pylint: disable=too-many-instance-attrib initial presence of the bot user once online. If `None`, then it will be set to the default, which is showing up as online without a custom status message. - intents : hikari.intents.Intent, optional + intents : hikari.models.intents.Intent, optional Bitfield of intents to use. If you use the V7 API, this is mandatory. This field will determine what events you will receive. json_deserialize : `deserialization function` diff --git a/hikari/internal/helpers.py b/hikari/internal/helpers.py index 7cb6a6d25d..234e760caf 100644 --- a/hikari/internal/helpers.py +++ b/hikari/internal/helpers.py @@ -51,11 +51,11 @@ def generate_allowed_mentions( # pylint:disable=line-too-long mentions_everyone : bool Whether `@everyone` and `@here` mentions should be resolved by discord and lead to actual pings. - user_mentions : typing.Union[typing.Collection[typing.Union[hikari.users.User, hikari.bases.Snowflake, int]], bool] + user_mentions : typing.Union[typing.Collection[typing.Union[hikari.models.users.User, hikari.models.bases.Snowflake, int]], bool] Either an array of user objects/IDs to allow mentions for, `True` to allow all user mentions or `False` to block all user mentions from resolving. - role_mentions : typing.Union[typing.Collection[typing.Union[hikari.guilds.GuildRole, hikari.bases.Snowflake, int]], bool] + role_mentions : typing.Union[typing.Collection[typing.Union[hikari.models.guilds.GuildRole, hikari.models.bases.Snowflake, int]], bool] Either an array of guild role objects/IDs to allow mentions for, `True` to allow all role mentions or `False` to block all role mentions from resolving. diff --git a/hikari/internal/marshaller.py b/hikari/internal/marshaller.py index 1d8f467a83..1600c30c5b 100644 --- a/hikari/internal/marshaller.py +++ b/hikari/internal/marshaller.py @@ -84,9 +84,9 @@ def dereference_handle(handle_string: str) -> typing.Any: Refers to `asyncio.tasks.Task` - * `"hikari.net"`: + * `"hikari.rest"`: - Refers to `hikari.net` + Refers to `hikari.rest` * `"foo.bar#baz.bork.qux"`: diff --git a/hikari/internal/ratelimits.py b/hikari/internal/ratelimits.py index 29968e4177..37305da34c 100644 --- a/hikari/internal/ratelimits.py +++ b/hikari/internal/ratelimits.py @@ -25,16 +25,16 @@ What is the theory behind this implementation? ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -In this module, we refer to a `hikari.net.routes.CompiledRoute` as a definition +In this module, we refer to a `hikari.rest.routes.CompiledRoute` as a definition of a route with specific major parameter values included (e.g. -`POST /channels/123/messages`), and a `hikari.net.routes.Route` as a +`POST /channels/123/messages`), and a `hikari.rest.routes.Route` as a definition of a route without specific parameter values included (e.g. `POST /channels/{channel_id}/messages`). We can compile a -`hikari.net.routes.CompiledRoute` from a `hikari.net.routes.Route` +`hikari.rest.routes.CompiledRoute` from a `hikari.rest.routes.Route` by providing the corresponding parameters as kwargs, as you may already know. In this module, a "bucket" is an internal data structure that tracks and -enforces the rate limit state for a specific `hikari.net.routes.CompiledRoute`, +enforces the rate limit state for a specific `hikari.rest.routes.CompiledRoute`, and can manage delaying tasks in the event that we begin to get rate limited. It also supports providing in-order execution of queued tasks. @@ -59,12 +59,12 @@ module. One issue that occurs from this is that we cannot effectively hash a -`hikari.net.routes.CompiledRoute` that has not yet been hit, meaning that +`hikari.rest.routes.CompiledRoute` that has not yet been hit, meaning that until we receive a response from this endpoint, we have no idea what our rate limits could be, nor the bucket that they sit in. This is usually not problematic, as the first request to an endpoint should never be rate limited unless you are hitting it from elsewhere in the same time window outside your -hikari.applications. To manage this situation, unknown endpoints are allocated to +hikari.models.applications. To manage this situation, unknown endpoints are allocated to a special unlimited bucket until they have an initial bucket hash code allocated from a response. Once this happens, the route is reallocated a dedicated bucket. Unknown buckets have a hardcoded initial hash code internally. @@ -73,13 +73,13 @@ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Each time you `BaseRateLimiter.acquire()` a request timeslice for a given -`hikari.net.routes.Route`, several things happen. The first is that we +`hikari.rest.routes.Route`, several things happen. The first is that we attempt to find the existing bucket for that route, if there is one, or get an unknown bucket otherwise. This is done by creating a real bucket hash from the compiled route. The initial hash is calculated using a lookup table that maps -`hikari.net.routes.CompiledRoute` objects to their corresponding initial hash +`hikari.rest.routes.CompiledRoute` objects to their corresponding initial hash codes, or to the unknown bucket hash code if not yet known. This initial hash is -processed by the `hikari.net.routes.CompiledRoute` to provide the real bucket +processed by the `hikari.rest.routes.CompiledRoute` to provide the real bucket hash we need to get the route's bucket object internally. The `acquire` method will take the bucket and acquire a new timeslice on @@ -239,7 +239,7 @@ def __init__(self, name: str) -> None: self.name = name self.throttle_task = None self.queue = [] - self.logger = logging.getLogger(f"hikari.net.{type(self).__qualname__}.{name}") + self.logger = logging.getLogger(f"hikari.internal.ratelimits.{type(self).__qualname__}.{name}") self._closed = False @abc.abstractmethod diff --git a/hikari/models/audit_logs.py b/hikari/models/audit_logs.py index 5561baf970..763558136c 100644 --- a/hikari/models/audit_logs.py +++ b/hikari/models/audit_logs.py @@ -542,8 +542,8 @@ class AuditLogIterator(typing.AsyncIterator[AuditLogEntry]): Parameters ---------- - app : hikari.clients.application.Application - The `hikari.clients.application.Application` that this should pass through + app : hikari.components.application.Application + The `hikari.components.application.Application` that this should pass through to the generated entities. request : typing.Callable[..., typing.Coroutine[typing.Any, typing.Any, typing.Any]] The prepared session bound partial function that this iterator should diff --git a/hikari/models/channels.py b/hikari/models/channels.py index 3242387c72..b5f7be078d 100644 --- a/hikari/models/channels.py +++ b/hikari/models/channels.py @@ -494,7 +494,7 @@ def with_parent_category(self, category: typing.Union[bases.Snowflake, int]) -> Parameters ---------- - category : typing.Union[hikari.bases.Snowflake, int] + category : typing.Union[hikari.models.bases.Snowflake, int] The placeholder ID of the category channel that should be this channel's parent. """ @@ -506,7 +506,7 @@ def with_id(self, channel_id: typing.Union[bases.Snowflake, int]) -> GuildChanne Parameters ---------- - channel_id : typing.Union[hikari.bases.Snowflake, int] + channel_id : typing.Union[hikari.models.bases.Snowflake, int] The placeholder ID to use. !!! note diff --git a/hikari/models/colours.py b/hikari/models/colours.py index f72fbf2563..a39be2dce5 100644 --- a/hikari/models/colours.py +++ b/hikari/models/colours.py @@ -16,7 +16,7 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . -"""Alias for the `hikari.colors` module.""" +"""Alias for the `hikari.models.colors` module.""" from __future__ import annotations diff --git a/hikari/models/embeds.py b/hikari/models/embeds.py index a9b6943313..64b387e7b1 100644 --- a/hikari/models/embeds.py +++ b/hikari/models/embeds.py @@ -392,8 +392,8 @@ def set_footer(self, *, text: str, icon: typing.Optional[str, files.BaseStream] ---------- text: str The optional text to set for the footer. - icon: typing.Union[str, hikari.files.BaseStream], optional - The optional `hikari.files.BaseStream` or URL to the image to set. + icon: typing.Union[str, hikari.models.files.BaseStream], optional + The optional `hikari.models.files.BaseStream` or URL to the image to set. Returns ------- @@ -420,8 +420,8 @@ def set_image(self, image: typing.Optional[str, files.BaseStream] = None) -> Emb Parameters ---------- - image: typing.Union[str, hikari.files.BaseStream], optional - The optional `hikari.files.BaseStream` or URL to the image to set. + image: typing.Union[str, hikari.models.files.BaseStream], optional + The optional `hikari.models.files.BaseStream` or URL to the image to set. Returns ------- @@ -438,8 +438,8 @@ def set_thumbnail(self, image: typing.Optional[str, files.BaseStream] = None) -> Parameters ---------- - image: typing.Union[str, hikari.files.BaseStream], optional - The optional `hikari.files.BaseStream` or URL to the image to set. + image: typing.Union[str, hikari.models.files.BaseStream], optional + The optional `hikari.models.files.BaseStream` or URL to the image to set. Returns ------- @@ -466,8 +466,8 @@ def set_author( The optional authors name. url: str, optional The optional URL to make the author text link to. - icon: typing.Union[str, hikari.files.BaseStream], optional - The optional `hikari.files.BaseStream` or URL to the icon to set. + icon: typing.Union[str, hikari.models.files.BaseStream], optional + The optional `hikari.models.files.BaseStream` or URL to the icon to set. Returns ------- diff --git a/hikari/models/emojis.py b/hikari/models/emojis.py index ede4b81b02..5e2988662c 100644 --- a/hikari/models/emojis.py +++ b/hikari/models/emojis.py @@ -44,9 +44,9 @@ class Emoji(bases.Entity, marshaller.Deserializable, files.BaseStream, abc.ABC): """Base class for all emojis. - Any emoji implementation supports being used as a `hikari.files.BaseStream` + Any emoji implementation supports being used as a `hikari.models.files.BaseStream` when uploading an attachment to the API. This is achieved in the same - way as using a `hikari.files.WebResourceStream` would achieve this. + way as using a `hikari.models.files.WebResourceStream` would achieve this. """ @property @@ -184,7 +184,7 @@ class CustomEmoji(Emoji, bases.Unique): This is a custom emoji that is from a guild you might not be part of. All CustomEmoji objects and their derivatives act as valid - `hikari.files.BaseStream` objects. This means you can use them as a + `hikari.models.files.BaseStream` objects. This means you can use them as a file when sending a message. >>> emojis = await bot.rest.fetch_guild_emojis(12345) diff --git a/hikari/models/guilds.py b/hikari/models/guilds.py index a9b950dc8a..0313d3aa29 100644 --- a/hikari/models/guilds.py +++ b/hikari/models/guilds.py @@ -579,7 +579,7 @@ class PresenceUser(users.User): """A user representation specifically used for presence updates. !!! warning - Every attribute except `PresenceUser.id` may be as `hikari.unset.UNSET` + Every attribute except `PresenceUser.id` may be as `hikari.models.unset.UNSET` unless it is specifically being modified for this update. """ @@ -631,8 +631,8 @@ def avatar_url(self) -> typing.Union[str, unset.Unset]: """URL for this user's avatar if the relevant info is available. !!! note - This will be `hikari.unset.UNSET` if both `PresenceUser.avatar_hash` - and `PresenceUser.discriminator` are `hikari.unset.UNSET`. + This will be `hikari.models.unset.UNSET` if both `PresenceUser.avatar_hash` + and `PresenceUser.discriminator` are `hikari.models.unset.UNSET`. """ return self.format_avatar_url() @@ -654,10 +654,10 @@ def format_avatar_url( Returns ------- - typing.Union[str, hikari.unset.UNSET] + typing.Union[str, hikari.models.unset.UNSET] The string URL of the user's custom avatar if either `PresenceUser.avatar_hash` is set or their default avatar if - `PresenceUser.discriminator` is set, else `hikari.unset.UNSET`. + `PresenceUser.discriminator` is set, else `hikari.models.unset.UNSET`. Raises ------ @@ -673,8 +673,8 @@ def default_avatar_index(self) -> typing.Union[int, unset.Unset]: """Integer representation of this user's default avatar. !!! note - This will be `hikari.unset.UNSET` if `PresenceUser.discriminator` is - `hikari.unset.UNSET`. + This will be `hikari.models.unset.UNSET` if `PresenceUser.discriminator` is + `hikari.models.unset.UNSET`. """ if self.discriminator is not unset.UNSET: return super().default_avatar_index @@ -685,8 +685,8 @@ def default_avatar_url(self) -> typing.Union[str, unset.Unset]: """URL for this user's default avatar. !!! note - This will be `hikari.unset.UNSET` if `PresenceUser.discriminator` is - `hikari.unset.UNSET`. + This will be `hikari.models.unset.UNSET` if `PresenceUser.discriminator` is + `hikari.models.unset.UNSET`. """ if self.discriminator is not unset.UNSET: return super().default_avatar_url diff --git a/hikari/models/invites.py b/hikari/models/invites.py index aae9ee3147..8f57d17941 100644 --- a/hikari/models/invites.py +++ b/hikari/models/invites.py @@ -73,7 +73,7 @@ class InviteGuild(guilds.PartialGuild): ) """The hash for the guild's banner. - This is only present if `hikari.guilds.GuildFeature.BANNER` is in the + This is only present if `hikari.models.guilds.GuildFeature.BANNER` is in the `features` for this guild. For all other purposes, it is `None`. """ @@ -94,7 +94,7 @@ class InviteGuild(guilds.PartialGuild): ) """The vanity URL code for the guild's vanity URL. - This is only present if `hikari.guilds.GuildFeature.VANITY_URL` is in the + This is only present if `hikari.models.guilds.GuildFeature.VANITY_URL` is in the `features` for this guild. If not, this will always be `None`. """ diff --git a/hikari/models/messages.py b/hikari/models/messages.py index 36dd741b59..ed2ebee57e 100644 --- a/hikari/models/messages.py +++ b/hikari/models/messages.py @@ -146,7 +146,7 @@ class Attachment(bases.Unique, files_.BaseStream, marshaller.Deserializable): """Represents a file attached to a message. You can use this object in the same way as a - `hikari.files.BaseStream`, by passing it as an attached file when creating a + `hikari.models.files.BaseStream`, by passing it as an attached file when creating a message, etc. """ @@ -396,7 +396,7 @@ async def fetch_channel(self) -> channels.PartialChannel: Returns ------- - hikari.channels.PartialChannel + hikari.models.channels.PartialChannel The object of the channel this message belongs to. Raises @@ -433,23 +433,23 @@ async def edit( # pylint:disable=line-too-long ---------- content : str If specified, the message content to set on the message. - embed : hikari.embeds.Embed + embed : hikari.models.embeds.Embed If specified, the embed object to set on the message. mentions_everyone : bool Whether `@everyone` and `@here` mentions should be resolved by discord and lead to actual pings, defaults to `True`. - user_mentions : typing.Union[typing.Collection[typing.Union[hikari.users.User, hikari.bases.Snowflake, int]], bool] + user_mentions : typing.Union[typing.Collection[typing.Union[hikari.models.users.User, hikari.models.bases.Snowflake, int]], bool] Either an array of user objects/IDs to allow mentions for, `True` to allow all user mentions or `False` to block all user mentions from resolving, defaults to `True`. - role_mentions: typing.Union[typing.Collection[typing.Union[hikari.guilds.GuildRole, hikari.bases.Snowflake, int]], bool] + role_mentions: typing.Union[typing.Collection[typing.Union[hikari.models.guilds.GuildRole, hikari.models.bases.Snowflake, int]], bool] Either an array of guild role objects/IDs to allow mentions for, `True` to allow all role mentions or `False` to block all role mentions from resolving, defaults to `True`. Returns ------- - hikari.messages.Message + hikari.models.messages.Message The edited message. Raises @@ -538,27 +538,27 @@ async def reply( # pylint:disable=line-too-long and can usually be ignored. tts : bool If specified, whether the message will be sent as a TTS message. - files : typing.Sequence[hikari.files.BaseStream] + files : typing.Sequence[hikari.models.files.BaseStream] If specified, a sequence of files to upload, if desired. Should be between 1 and 10 objects in size (inclusive), also including embed attachments. - embed : hikari.embeds.Embed + embed : hikari.models.embeds.Embed If specified, the embed object to send with the message. mentions_everyone : bool Whether `@everyone` and `@here` mentions should be resolved by discord and lead to actual pings, defaults to `True`. - user_mentions : typing.Union[typing.Collection[typing.Union[hikari.users.User, hikari.bases.Snowflake, int]], bool] + user_mentions : typing.Union[typing.Collection[typing.Union[hikari.models.users.User, hikari.models.bases.Snowflake, int]], bool] Either an array of user objects/IDs to allow mentions for, `True` to allow all user mentions or `False` to block all user mentions from resolving, defaults to `True`. - role_mentions: typing.Union[typing.Collection[typing.Union[hikari.guilds.GuildRole, hikari.bases.Snowflake, int]], bool] + role_mentions: typing.Union[typing.Collection[typing.Union[hikari.models.guilds.GuildRole, hikari.models.bases.Snowflake, int]], bool] Either an array of guild role objects/IDs to allow mentions for, `True` to allow all role mentions or `False` to block all role mentions from resolving, defaults to `True`. Returns ------- - hikari.messages.Message + hikari.models.messages.Message The created message object. Raises @@ -644,7 +644,7 @@ async def add_reaction(self, emoji: typing.Union[str, emojis_.Emoji]) -> None: Parameters ---------- - emoji : str OR hikari.emojis.Emoji + emoji : str OR hikari.models.emojis.Emoji The emoji to add. Examples @@ -684,9 +684,9 @@ async def remove_reaction( Parameters ---------- - emoji : str OR hikari.emojis.Emoji + emoji : str OR hikari.models.emojis.Emoji The emoji to remove. - user : hikari.users.User, optional + user : hikari.models.users.User, optional The user of the reaction to remove. If `None`, then the bot's reaction is removed instead. @@ -726,7 +726,7 @@ async def remove_all_reactions(self, emoji: typing.Optional[typing.Union[str, em Parameters ---------- - emoji : str OR hikari.emojis.Emoji, optional + emoji : str OR hikari.models.emojis.Emoji, optional The emoji to remove all reactions for. If not specified, or `None`, then all emojis are removed. diff --git a/hikari/models/users.py b/hikari/models/users.py index 70cab00097..1b89e724af 100644 --- a/hikari/models/users.py +++ b/hikari/models/users.py @@ -135,7 +135,7 @@ async def fetch_self(self) -> User: Returns ------- - hikari.users.User + hikari.models.users.User The requested user object. Raises @@ -242,7 +242,7 @@ async def fetch_self(self) -> MyUser: Returns ------- - hikari.users.User + hikari.models.users.User The requested user object. """ return await self._app.rest.fetch_me() diff --git a/hikari/models/webhooks.py b/hikari/models/webhooks.py index 89cd78692b..ff312d5a85 100644 --- a/hikari/models/webhooks.py +++ b/hikari/models/webhooks.py @@ -143,26 +143,26 @@ async def execute( wait : bool If specified, whether this request should wait for the webhook to be executed and return the resultant message object. - files : typing.Sequence[hikari.files.BaseStream] + files : typing.Sequence[hikari.models.files.BaseStream] If specified, a sequence of files to upload. - embeds : typing.Sequence[hikari.embeds.Embed] + embeds : typing.Sequence[hikari.models.embeds.Embed] If specified, a sequence of between `1` to `10` embed objects (inclusive) to send with the embed. mentions_everyone : bool Whether `@everyone` and `@here` mentions should be resolved by discord and lead to actual pings, defaults to `True`. - user_mentions : typing.Union[typing.Collection[typing.Union[hikari.users.User, hikari.bases.Snowflake, int]], bool] + user_mentions : typing.Union[typing.Collection[typing.Union[hikari.models.users.User, hikari.models.bases.Snowflake, int]], bool] Either an array of user objects/IDs to allow mentions for, `True` to allow all user mentions or `False` to block all user mentions from resolving, defaults to `True`. - role_mentions : typing.Union[typing.Collection[typing.Union[hikari.guilds.GuildRole, hikari.bases.Snowflake, int]], bool] + role_mentions : typing.Union[typing.Collection[typing.Union[hikari.models.guilds.GuildRole, hikari.models.bases.Snowflake, int]], bool] Either an array of guild role objects/IDs to allow mentions for, `True` to allow all role mentions or `False` to block all role mentions from resolving, defaults to `True`. Returns ------- - hikari.messages.Message, optional + hikari.models.messages.Message, optional The created message object, if `wait` is `True`, else `None`. Raises @@ -221,7 +221,7 @@ async def safe_execute( """Execute the webhook to create a message with mention safety. This endpoint has the same signature as - `Webhook.execute_webhook` with the only difference being + `Webhook.execute` with the only difference being that `mentions_everyone`, `user_mentions` and `role_mentions` default to `False`. """ @@ -287,10 +287,10 @@ async def edit( ---------- name : str If specified, the new name string. - avatar : hikari.files.BaseStream, optional + avatar : hikari.models.files.BaseStream, optional If specified, the new avatar image. If `None`, then it is removed. - channel : typing.Union[hikari.channels.GuildChannel, hikari.bases.Snowflake, int] + channel : typing.Union[hikari.models.channels.GuildChannel, hikari.models.bases.Snowflake, int] If specified, the object or ID of the new channel the given webhook should be moved to. reason : str @@ -305,7 +305,7 @@ async def edit( Returns ------- - hikari.webhooks.Webhook + hikari.models.webhooks.Webhook The updated webhook object. Raises @@ -343,7 +343,7 @@ async def fetch_channel(self) -> channels_.PartialChannel: Returns ------- - hikari.channels.PartialChannel + hikari.models.channels.PartialChannel The object of the channel this webhook targets. Raises @@ -360,7 +360,7 @@ async def fetch_guild(self) -> guilds_.Guild: Returns ------- - hikari.guilds.Guild + hikari.models.guilds.Guild The object of the channel this message belongs to. Raises diff --git a/hikari/rest/base.py b/hikari/rest/base.py index 604aa388d4..f06aae1401 100644 --- a/hikari/rest/base.py +++ b/hikari/rest/base.py @@ -39,7 +39,7 @@ class BaseRESTComponent(abc.ABC, metaclass=meta.UniqueFunctionMeta): """An abstract class that all RESTSession client logic classes should inherit from. This defines the abstract method `__init__` which will assign an instance - of `hikari.net.rest.RESTSession` to the attribute that all application will expect + of `hikari.rest.session.RESTSession` to the attribute that all application will expect to make calls to. """ diff --git a/hikari/rest/buckets.py b/hikari/rest/buckets.py index 74c3a8de12..50d5258150 100644 --- a/hikari/rest/buckets.py +++ b/hikari/rest/buckets.py @@ -162,7 +162,7 @@ def __init__(self) -> None: self.real_hashes_to_buckets = {} self.closed_event: asyncio.Event = asyncio.Event() self.gc_task: typing.Optional[asyncio.Task] = None - self.logger = logging.getLogger("hikari.rest.ratelimits.RESTBucketManager") + self.logger = logging.getLogger("hikari.rest.buckets.RESTBucketManager") def __enter__(self) -> RESTBucketManager: return self @@ -304,7 +304,7 @@ def acquire(self, compiled_route: routes.CompiledRoute) -> more_typing.Future[No Parameters ---------- - compiled_route : hikari.net.routes.CompiledRoute + compiled_route : hikari.rest.routes.CompiledRoute The route to get the bucket for. Returns @@ -354,7 +354,7 @@ def update_rate_limits( Parameters ---------- - compiled_route : hikari.net.routes.CompiledRoute + compiled_route : hikari.rest.routes.CompiledRoute The compiled route to get the bucket for. bucket_header : str, optional The `X-RateLimit-Bucket` header that was provided in the response, diff --git a/hikari/rest/channel.py b/hikari/rest/channel.py index 9248ffd4df..35ab323125 100644 --- a/hikari/rest/channel.py +++ b/hikari/rest/channel.py @@ -88,12 +88,12 @@ async def fetch_channel( Parameters ---------- - channel : typing.Union[hikari.channels.PartialChannel, hikari.bases.Snowflake, int] + channel : typing.Union[hikari.models.channels.PartialChannel, hikari.models.bases.Snowflake, int] The object ID of the channel to look up. Returns ------- - hikari.channels.PartialChannel + hikari.models.channels.PartialChannel The channel object that has been found. Raises @@ -130,7 +130,7 @@ async def update_channel( Parameters ---------- - channel : typing.Union[hikari.channels.PartialChannel, hikari.bases.Snowflake, int] + channel : typing.Union[hikari.models.channels.PartialChannel, hikari.models.bases.Snowflake, int] The channel ID to update. name : str If specified, the new name for the channel. This must be @@ -159,10 +159,10 @@ async def update_channel( If specified, the new max number of users to allow in a voice channel. This must be between `0` and `99` inclusive, where `0` implies no limit. - permission_overwrites : typing.Sequence[hikari.channels.PermissionOverwrite] + permission_overwrites : typing.Sequence[hikari.models.channels.PermissionOverwrite] If specified, the new list of permission overwrites that are category specific to replace the existing overwrites with. - parent_category : typing.Union[hikari.channels.PartialChannel, hikari.bases.Snowflake, int], optional + parent_category : typing.Union[hikari.models.channels.PartialChannel, hikari.models.bases.Snowflake, int], optional If specified, the new parent category ID to set for the channel, pass `None` to unset. reason : str @@ -171,7 +171,7 @@ async def update_channel( Returns ------- - hikari.channels.PartialChannel + hikari.models.channels.PartialChannel The channel object that has been modified. Raises @@ -216,7 +216,7 @@ async def delete_channel(self, channel: typing.Union[bases.Snowflake, int, str, Parameters ---------- - channel : typing.Union[hikari.channels.PartialChannel, hikari.bases.Snowflake str] + channel : typing.Union[hikari.models.channels.PartialChannel, hikari.models.bases.Snowflake str] The object or ID of the channel to delete. Returns @@ -287,22 +287,22 @@ def fetch_messages( Parameters ---------- - channel : hikari.channels.PartialChannel OR hikari.bases.Snowflake OR int OR str + channel : hikari.models.channels.PartialChannel OR hikari.models.bases.Snowflake OR int OR str The channel to fetch messages from. Keyword Arguments ----------------- - before : datetime.datetime OR int OR str OR hikari.bases.Unique OR hikari.bases.Snowflake + before : datetime.datetime OR int OR str OR hikari.models.bases.Unique OR hikari.models.bases.Snowflake If a unique object (like a message), then message created before this object will be returned. If a datetime, then messages before that datetime will be returned. If unspecified or None, the filter is not used. - after : datetime.datetime OR int OR str OR hikari.bases.Unique OR hikari.bases.Snowflake + after : datetime.datetime OR int OR str OR hikari.models.bases.Unique OR hikari.models.bases.Snowflake If a unique object (like a message), then message created after this object will be returned. If a datetime, then messages after that datetime will be returned. If unspecified or None, the filter is not used. - around : datetime.datetime OR int OR str OR hikari.bases.Unique OR hikari.bases.Snowflake + around : datetime.datetime OR int OR str OR hikari.models.bases.Unique OR hikari.models.bases.Snowflake If a unique object (like a message), then message created around the same time as this object will be returned. If a datetime, then messages around that datetime will be returned. If unspecified or @@ -319,7 +319,7 @@ def fetch_messages( !!! note Passing no value for `before`, `after`, or `around` will have the - same effect as passing `before=hikari.bases.Snowflake.max()`. This + same effect as passing `before=hikari.models.bases.Snowflake.max()`. This will return all messages that can be found, newest to oldest. Examples @@ -375,7 +375,7 @@ def fetch_messages( Returns ------- - hikari.pagination.PaginatedResults[hikari.messages.Message] + hikari.models.pagination.PaginatedResults[hikari.models.messages.Message] An async iterator of message objects. Raises @@ -407,14 +407,14 @@ async def fetch_message( Parameters ---------- - channel : typing.Union[hikari.channels.PartialChannel, hikari.bases.Snowflake, int] + channel : typing.Union[hikari.models.channels.PartialChannel, hikari.models.bases.Snowflake, int] The object or ID of the channel to get the message from. - message : typing.Union[hikari.messages.Message, hikari.bases.Snowflake, int] + message : typing.Union[hikari.models.messages.Message, hikari.models.bases.Snowflake, int] The object or ID of the message to retrieve. Returns ------- - hikari.messages.Message + hikari.models.messages.Message The found message object. !!! note @@ -457,7 +457,7 @@ async def create_message( # pylint: disable=line-too-long Parameters ---------- - channel : typing.Union[hikari.channels.PartialChannel, hikari.bases.Snowflake, int] + channel : typing.Union[hikari.models.channels.PartialChannel, hikari.models.bases.Snowflake, int] The channel or ID of the channel to send to. content : str If specified, the message content to send with the message. @@ -467,27 +467,27 @@ async def create_message( # pylint: disable=line-too-long Nonces are limited to 32 bytes in size. tts : bool If specified, whether the message will be sent as a TTS message. - files : typing.Sequence[hikari.files.BaseStream] + files : typing.Sequence[hikari.models.files.BaseStream] If specified, a sequence of files to upload, if desired. Should be between 1 and 10 objects in size (inclusive), also including embed attachments. - embed : hikari.embeds.Embed + embed : hikari.models.embeds.Embed If specified, the embed object to send with the message. mentions_everyone : bool Whether `@everyone` and `@here` mentions should be resolved by discord and lead to actual pings, defaults to `True`. - user_mentions : typing.Union[typing.Collection[typing.Union[hikari.users.User, hikari.bases.Snowflake, int]], bool] + user_mentions : typing.Union[typing.Collection[typing.Union[hikari.models.users.User, hikari.models.bases.Snowflake, int]], bool] Either an array of user objects/IDs to allow mentions for, `True` to allow all user mentions or `False` to block all user mentions from resolving, defaults to `True`. - role_mentions: typing.Union[typing.Collection[typing.Union[hikari.guilds.GuildRole, hikari.bases.Snowflake, int]], bool] + role_mentions: typing.Union[typing.Collection[typing.Union[hikari.models.guilds.GuildRole, hikari.models.bases.Snowflake, int]], bool] Either an array of guild role objects/IDs to allow mentions for, `True` to allow all role mentions or `False` to block all role mentions from resolving, defaults to `True`. Returns ------- - hikari.messages.Message + hikari.models.messages.Message The created message object. Raises @@ -584,35 +584,35 @@ async def update_message( # pylint: disable=line-too-long Parameters ---------- - channel : typing.Union[hikari.channels.PartialChannel, hikari.bases.Snowflake, int] + channel : typing.Union[hikari.models.channels.PartialChannel, hikari.models.bases.Snowflake, int] The object or ID of the channel to get the message from. - message : typing.Union[hikari.messages.Message, hikari.bases.Snowflake, int] + message : typing.Union[hikari.models.messages.Message, hikari.models.bases.Snowflake, int] The object or ID of the message to edit. content : str, optional If specified, the string content to replace with in the message. If `None`, then the content will be removed from the message. - embed : hikari.embeds.Embed, optional + embed : hikari.models.embeds.Embed, optional If specified, then the embed to replace with in the message. If `None`, then the embed will be removed from the message. - flags : hikari.messages.MessageFlag + flags : hikari.models.messages.MessageFlag If specified, the new flags for this message, while a raw int may be passed for this, this can lead to unexpected behaviour if it's outside the range of the MessageFlag int flag. mentions_everyone : bool Whether `@everyone` and `@here` mentions should be resolved by discord and lead to actual pings, defaults to `True`. - user_mentions: typing.Union[typing.Collection[typing.Union[hikari.users.User, hikari.bases.Snowflake, int]], bool] + user_mentions: typing.Union[typing.Collection[typing.Union[hikari.models.users.User, hikari.models.bases.Snowflake, int]], bool] Either an array of user objects/IDs to allow mentions for, `True` to allow all user mentions or `False` to block all user mentions from resolving, defaults to `True`. - role_mentions: typing.Union[typing.Collection[typing.Union[hikari.guilds.GuildRole, hikari.bases.Snowflake, int]], bool] + role_mentions: typing.Union[typing.Collection[typing.Union[hikari.models.guilds.GuildRole, hikari.models.bases.Snowflake, int]], bool] Either an array of guild role objects/IDs to allow mentions for, `True` to allow all role mentions or `False` to block all role mentions from resolving, defaults to `True`. Returns ------- - hikari.messages.Message + hikari.models.messages.Message The edited message object. Raises @@ -691,11 +691,11 @@ async def delete_messages( Parameters ---------- - channel : typing.Union[hikari.channels.PartialChannel, hikari.bases.Snowflake, int] + channel : typing.Union[hikari.models.channels.PartialChannel, hikari.models.bases.Snowflake, int] The object or ID of the channel to get the message from. - message : typing.Union[hikari.messages.Message, hikari.bases.Snowflake, int] + message : typing.Union[hikari.models.messages.Message, hikari.models.bases.Snowflake, int] The object or ID of the message to delete. - *additional_messages : typing.Union[hikari.messages.Message, hikari.bases.Snowflake, int] + *additional_messages : typing.Union[hikari.models.messages.Message, hikari.models.bases.Snowflake, int] Objects and/or IDs of additional messages to delete in the same channel, in total you can delete up to 100 messages in a request. @@ -756,18 +756,18 @@ async def update_channel_overwrite( # pylint: disable=line-too-long Parameters ---------- - channel : typing.Union[hikari.messages.Message, hikari.bases.Snowflake, int] + channel : typing.Union[hikari.models.messages.Message, hikari.models.bases.Snowflake, int] The object or ID of the channel to edit permissions for. - overwrite : typing.Union[hikari.channels.PermissionOverwrite, hikari.guilds.GuildRole, hikari.users.User, hikari.bases.Snowflake , int] + overwrite : typing.Union[hikari.models.channels.PermissionOverwrite, hikari.models.guilds.GuildRole, hikari.models.users.User, hikari.models.bases.Snowflake , int] The object or ID of the target member or role to edit/create the overwrite for. - target_type : typing.Union[hikari.channels.PermissionOverwriteType, int] + target_type : typing.Union[hikari.models.channels.PermissionOverwriteType, int] The type of overwrite, passing a raw string that's outside of the enum's range for this may lead to unexpected behaviour. - allow : typing.Union[hikari.permissions.Permission, int] + allow : typing.Union[hikari.models.permissions.Permission, int] If specified, the value of all permissions to set to be allowed, passing a raw integer for this may lead to unexpected behaviour. - deny : typing.Union[hikari.permissions.Permission, int] + deny : typing.Union[hikari.models.permissions.Permission, int] If specified, the value of all permissions to set to be denied, passing a raw integer for this may lead to unexpected behaviour. reason : str @@ -801,12 +801,12 @@ async def fetch_invites_for_channel( Parameters ---------- - channel : typing.Union[hikari.channels.PartialChannel, hikari.bases.Snowflake, int] + channel : typing.Union[hikari.models.channels.PartialChannel, hikari.models.bases.Snowflake, int] The object or ID of the channel to get invites for. Returns ------- - typing.Sequence[hikari.invites.InviteWithMetadata] + typing.Sequence[hikari.models.invites.InviteWithMetadata] A list of invite objects. Raises @@ -854,10 +854,10 @@ async def create_invite_for_channel( user is kicked when their session ends unless they are given a role. unique : bool If specified, whether to try to reuse a similar invite. - target_user : typing.Union[hikari.users.User, hikari.bases.Snowflake, int] + target_user : typing.Union[hikari.models.users.User, hikari.models.bases.Snowflake, int] If specified, the object or ID of the user this invite should target. - target_user_type : typing.Union[hikari.invites.TargetUserType, int] + target_user_type : typing.Union[hikari.models.invites.TargetUserType, int] If specified, the type of target for this invite, passing a raw integer for this may lead to unexpected results. reason : str @@ -866,7 +866,7 @@ async def create_invite_for_channel( Returns ------- - hikari.invites.InviteWithMetadata + hikari.models.invites.InviteWithMetadata The created invite object. Raises @@ -905,9 +905,9 @@ async def delete_channel_overwrite( # pylint: disable=line-too-long Parameters ---------- - channel : typing.Union[hikari.channels.PartialChannel, hikari.bases.Snowflake, int] + channel : typing.Union[hikari.models.channels.PartialChannel, hikari.models.bases.Snowflake, int] The object or ID of the channel to delete the overwrite from. - overwrite : typing.Union[hikari.channels.PermissionOverwrite, hikari.guilds.GuildRole, hikari.users.User, hikari.bases.Snowflake, int] + overwrite : typing.Union[hikari.models.channels.PermissionOverwrite, hikari.models.guilds.GuildRole, hikari.models.users.User, hikari.models.bases.Snowflake, int] The ID of the entity this overwrite targets. Raises @@ -931,7 +931,7 @@ async def trigger_typing(self, channel: typing.Union[bases.Snowflake, int, str, Parameters ---------- - channel : typing.Union[hikari.channels.PartialChannel, hikari.bases.Snowflake, int] + channel : typing.Union[hikari.models.channels.PartialChannel, hikari.models.bases.Snowflake, int] The object or ID of the channel to appear to be typing in. Raises @@ -955,12 +955,12 @@ async def fetch_pins( Parameters ---------- - channel : typing.Union[hikari.channels.PartialChannel, hikari.bases.Snowflake, int] + channel : typing.Union[hikari.models.channels.PartialChannel, hikari.models.bases.Snowflake, int] The object or ID of the channel to get messages from. Returns ------- - typing.Mapping[hikari.bases.Snowflake, hikari.messages.Message] + typing.Mapping[hikari.models.bases.Snowflake, hikari.models.messages.Message] A list of message objects. Raises @@ -994,9 +994,9 @@ async def pin_message( Parameters ---------- - channel : typing.Union[hikari.channels.PartialChannel, hikari.bases.Snowflake, int] + channel : typing.Union[hikari.models.channels.PartialChannel, hikari.models.bases.Snowflake, int] The object or ID of the channel to pin a message to. - message : typing.Union[hikari.messages.Message, hikari.bases.Snowflake, int] + message : typing.Union[hikari.models.messages.Message, hikari.models.bases.Snowflake, int] The object or ID of the message to pin. Raises @@ -1025,9 +1025,9 @@ async def unpin_message( Parameters ---------- - channel : typing.Union[hikari.channels.PartialChannel, hikari.bases.Snowflake, int] + channel : typing.Union[hikari.models.channels.PartialChannel, hikari.models.bases.Snowflake, int] The ID of the channel to remove a pin from. - message : typing.Union[hikari.messages.Message, hikari.bases.Snowflake, int] + message : typing.Union[hikari.models.messages.Message, hikari.models.bases.Snowflake, int] The object or ID of the message to unpin. Raises @@ -1057,11 +1057,11 @@ async def create_webhook( Parameters ---------- - channel : typing.Union[hikari.channels.GuildChannel, hikari.bases.Snowflake, int] + channel : typing.Union[hikari.models.channels.GuildChannel, hikari.models.bases.Snowflake, int] The object or ID of the channel for webhook to be created in. name : str The webhook's name string. - avatar : hikari.files.BaseStream + avatar : hikari.models.files.BaseStream If specified, the avatar image to use. reason : str If specified, the audit log reason explaining why the operation @@ -1069,7 +1069,7 @@ async def create_webhook( Returns ------- - hikari.webhooks.Webhook + hikari.models.webhooks.Webhook The newly created webhook object. Raises @@ -1099,12 +1099,12 @@ async def fetch_channel_webhooks( Parameters ---------- - channel : typing.Union[hikari.channels.GuildChannel, hikari.bases.Snowflake, int] + channel : typing.Union[hikari.models.channels.GuildChannel, hikari.models.bases.Snowflake, int] The object or ID of the guild channel to get the webhooks from. Returns ------- - typing.Sequence[hikari.webhooks.Webhook] + typing.Sequence[hikari.models.webhooks.Webhook] A list of webhook objects for the give channel. Raises diff --git a/hikari/rest/client.py b/hikari/rest/client.py index 8290f5a3bb..9cb098e804 100644 --- a/hikari/rest/client.py +++ b/hikari/rest/client.py @@ -16,7 +16,7 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . -"""Marshall wrappings for the RESTSession implementation in `hikari.net.rest`. +"""Marshall wrappings for the RESTSession implementation in `hikari.rest.session`. This provides an object-oriented interface for interacting with discord's RESTSession API. @@ -60,12 +60,12 @@ class RESTClient( A marshalling object-oriented RESTSession API client. This client bridges the basic RESTSession API exposed by - `hikari.net.rest.RESTSession` and wraps it in a unit of processing that can handle + `hikari.rest.session.RESTSession` and wraps it in a unit of processing that can handle handle parsing API objects into Hikari entity objects. Parameters ---------- - app : hikari.clients.application.Application + app : hikari.components.application.Application The client application that this rest client should be bound by. Includes the rest config. diff --git a/hikari/rest/guild.py b/hikari/rest/guild.py index 25647c7282..a0199c5409 100644 --- a/hikari/rest/guild.py +++ b/hikari/rest/guild.py @@ -85,23 +85,23 @@ async def fetch_audit_log( Parameters ---------- - guild : typing.Union[hikari.guilds.Guild, hikari.bases.Snowflake, int] + guild : typing.Union[hikari.models.guilds.Guild, hikari.models.bases.Snowflake, int] The object or ID of the guild to get the audit logs for. - user : typing.Union[hikari.users.User, hikari.bases.Snowflake, int] + user : typing.Union[hikari.models.users.User, hikari.models.bases.Snowflake, int] If specified, the object or ID of the user to filter by. - action_type : typing.Union[hikari.audit_logs.AuditLogEventType, int] + action_type : typing.Union[hikari.models.audit_logs.AuditLogEventType, int] If specified, the action type to look up. Passing a raw integer for this may lead to unexpected behaviour. limit : int If specified, the limit to apply to the number of records. Defaults to `50`. Must be between `1` and `100` inclusive. - before : typing.Union[datetime.datetime, hikari.audit_logs.AuditLogEntry, hikari.bases.Snowflake, int] + before : typing.Union[datetime.datetime, hikari.models.audit_logs.AuditLogEntry, hikari.models.bases.Snowflake, int] If specified, the object or ID of the entry that all retrieved entries should have occurred before. Returns ------- - hikari.audit_logs.AuditLog + hikari.models.audit_logs.AuditLog An audit log object. Raises @@ -146,15 +146,15 @@ def fetch_audit_log_entries_before( Parameters ---------- - guild : typing.Union[hikari.guilds.Guild, hikari.bases.Snowflake, int] + guild : typing.Union[hikari.models.guilds.Guild, hikari.models.bases.Snowflake, int] The ID or object of the guild to get audit log entries for - before : typing.Union[datetime.datetime, hikari.audit_logs.AuditLogEntry, hikari.bases.Snowflake, int], optional + before : typing.Union[datetime.datetime, hikari.models.audit_logs.AuditLogEntry, hikari.models.bases.Snowflake, int], optional If specified, the ID or object of the entry or datetime to get entries that happened before otherwise this will start from the newest entry. - user : typing.Union[hikari.users.User, hikari.bases.Snowflake, int] + user : typing.Union[hikari.models.users.User, hikari.models.bases.Snowflake, int] If specified, the object or ID of the user to filter by. - action_type : typing.Union[hikari.audit_logs.AuditLogEventType, int] + action_type : typing.Union[hikari.models.audit_logs.AuditLogEventType, int] If specified, the action type to look up. Passing a raw integer for this may lead to unexpected behaviour. limit : int, optional @@ -180,7 +180,7 @@ def fetch_audit_log_entries_before( hikari.errors.BadRequest If any invalid snowflake IDs are passed; a snowflake may be invalid due to it being outside of the range of a 64 bit integer. - hikari.audit_logs.AuditLogIterator + hikari.models.audit_logs.AuditLogIterator An async iterator of the audit log entries in a guild (from newest to oldest). """ @@ -205,14 +205,14 @@ async def fetch_guild_emoji( Parameters ---------- - guild : typing.Union[hikari.guilds.Guild, hikari.bases.Snowflake, int] + guild : typing.Union[hikari.models.guilds.Guild, hikari.models.bases.Snowflake, int] The object or ID of the guild to get the emoji from. - emoji : typing.Union[hikari.emojis.GuildEmoji, hikari.bases.Snowflake, int] + emoji : typing.Union[hikari.models.emojis.KnownCustomEmoji, hikari.models.bases.Snowflake, int] The object or ID of the emoji to get. Returns ------- - hikari.emojis.KnownCustomEmoji + hikari.models.emojis.KnownCustomEmoji A guild emoji object. Raises @@ -238,12 +238,12 @@ async def fetch_guild_emojis( Parameters ---------- - guild : typing.Union[hikari.guilds.Guild, hikari.bases.Snowflake, int] + guild : typing.Union[hikari.models.guilds.Guild, hikari.models.bases.Snowflake, int] The object or ID of the guild to get the emojis for. Returns ------- - typing.Sequence[hikari.emojis.KnownCustomEmoji] + typing.Sequence[hikari.models.emojis.KnownCustomEmoji] A list of guild emoji objects. Raises @@ -274,13 +274,13 @@ async def create_guild_emoji( Parameters ---------- - guild : typing.Union[hikari.guilds.GuildRole, hikari.bases.Snowflake, int] + guild : typing.Union[hikari.models.guilds.GuildRole, hikari.models.bases.Snowflake, int] The object or ID of the guild to create the emoji in. name : str The new emoji's name. - image : hikari.files.BaseStream + image : hikari.models.files.BaseStream The `128x128` image data. - roles : typing.Sequence[typing.Union[hikari.guilds.GuildRole, hikari.bases.Snowflake, int]] + roles : typing.Sequence[typing.Union[hikari.models.guilds.GuildRole, hikari.models.bases.Snowflake, int]] If specified, a list of role objects or IDs for which the emoji will be whitelisted. If empty, all roles are whitelisted. reason : str @@ -289,7 +289,7 @@ async def create_guild_emoji( Returns ------- - hikari.emojis.KnownCustomEmoji + hikari.models.emojis.KnownCustomEmoji The newly created emoji object. Raises @@ -331,14 +331,14 @@ async def update_guild_emoji( Parameters ---------- - guild : typing.Union[hikari.guilds.Guild, hikari.bases.Snowflake, int] + guild : typing.Union[hikari.models.guilds.Guild, hikari.models.bases.Snowflake, int] The object or ID of the guild to which the emoji to edit belongs to. - emoji : typing.Union[hikari.emojis.GuildEmoji, hikari.bases.Snowflake, int] + emoji : typing.Union[hikari.models.emojis.KnownCustomEmoji, hikari.models.bases.Snowflake, int] The object or ID of the emoji to edit. name : str If specified, a new emoji name string. Keep unspecified to leave the name unchanged. - roles : typing.Sequence[typing.Union[hikari.guilds.GuildRole, hikari.bases.Snowflake, int]] + roles : typing.Sequence[typing.Union[hikari.models.guilds.GuildRole, hikari.models.bases.Snowflake, int]] If specified, a list of objects or IDs for the new whitelisted roles. Set to an empty list to whitelist all roles. Keep unspecified to leave the same roles already set. @@ -348,7 +348,7 @@ async def update_guild_emoji( Returns ------- - hikari.emojis.KnownCustomEmoji + hikari.models.emojis.KnownCustomEmoji The updated emoji object. Raises @@ -382,9 +382,9 @@ async def delete_guild_emoji( Parameters ---------- - guild : typing.Union[hikari.guilds.Guild, hikari.bases.Snowflake, int] + guild : typing.Union[hikari.models.guilds.Guild, hikari.models.bases.Snowflake, int] The object or ID of the guild to delete the emoji from. - emoji : typing.Union[hikari.emojis.KnownCustomEmoji, hikari.bases.Snowflake, int] + emoji : typing.Union[hikari.models.emojis.KnownCustomEmoji, hikari.models.bases.Snowflake, int] The object or ID of the guild emoji to be deleted. Raises @@ -428,27 +428,27 @@ async def create_guild( If specified, the voice region ID for new guild. You can use `RESTGuildComponent.fetch_guild_voice_regions` to see which region IDs are available. - icon : hikari.files.BaseStream + icon : hikari.models.files.BaseStream If specified, the guild icon image data. - verification_level : typing.Union[hikari.guilds.GuildVerificationLevel, int] + verification_level : typing.Union[hikari.models.guilds.GuildVerificationLevel, int] If specified, the verification level. Passing a raw int for this may lead to unexpected behaviour. - default_message_notifications : typing.Union[hikari.guilds.GuildMessageNotificationsLevel, int] + default_message_notifications : typing.Union[hikari.models.guilds.GuildMessageNotificationsLevel, int] If specified, the default notification level. Passing a raw int for this may lead to unexpected behaviour. - explicit_content_filter : typing.Union[hikari.guilds.GuildExplicitContentFilterLevel, int] + explicit_content_filter : typing.Union[hikari.models.guilds.GuildExplicitContentFilterLevel, int] If specified, the explicit content filter. Passing a raw int for this may lead to unexpected behaviour. - roles : typing.Sequence[hikari.guilds.GuildRole] + roles : typing.Sequence[hikari.models.guilds.GuildRole] If specified, an array of role objects to be created alongside the guild. First element changes the `@everyone` role. - channels : typing.Sequence[hikari.channels.GuildChannelBuilder] + channels : typing.Sequence[hikari.models.channels.GuildChannelBuilder] If specified, an array of guild channel builder objects to be created within the guild. Returns ------- - hikari.guilds.Guild + hikari.models.guilds.Guild The newly created guild object. Raises @@ -478,12 +478,12 @@ async def fetch_guild(self, guild: typing.Union[bases.Snowflake, int, str, guild Parameters ---------- - guild : typing.Union[hikari.guilds.Guild, hikari.bases.Snowflake, int] + guild : typing.Union[hikari.models.guilds.Guild, hikari.models.bases.Snowflake, int] The object or ID of the guild to get. Returns ------- - hikari.guilds.Guild + hikari.models.guilds.Guild The requested guild object. Raises @@ -510,12 +510,12 @@ async def fetch_guild_preview( Parameters ---------- - guild : typing.Union[hikari.guilds.Guild, hikari.bases.Snowflake, int] + guild : typing.Union[hikari.models.guilds.Guild, hikari.models.bases.Snowflake, int] The object or ID of the guild to get the preview object for. Returns ------- - hikari.guilds.GuildPreview + hikari.models.guilds.GuildPreview The requested guild preview object. !!! note @@ -556,7 +556,7 @@ async def update_guild( Parameters ---------- - guild : typing.Union[hikari.guilds.Guild, hikari.bases.Snowflake, int] + guild : typing.Union[hikari.models.guilds.Guild, hikari.models.bases.Snowflake, int] The object or ID of the guild to be edited. name : str If specified, the new name string for the guild (`2-100` characters). @@ -564,26 +564,26 @@ async def update_guild( If specified, the new voice region ID for guild. You can use `RESTGuildComponent.fetch_guild_voice_regions` to see which region IDs are available. - verification_level : typing.Union[hikari.guilds.GuildVerificationLevel, int] + verification_level : typing.Union[hikari.models.guilds.GuildVerificationLevel, int] If specified, the new verification level. Passing a raw int for this may lead to unexpected behaviour. - default_message_notifications : typing.Union[hikari.guilds.GuildMessageNotificationsLevel, int] + default_message_notifications : typing.Union[hikari.models.guilds.GuildMessageNotificationsLevel, int] If specified, the new default notification level. Passing a raw int for this may lead to unexpected behaviour. - explicit_content_filter : typing.Union[hikari.guilds.GuildExplicitContentFilterLevel, int] + explicit_content_filter : typing.Union[hikari.models.guilds.GuildExplicitContentFilterLevel, int] If specified, the new explicit content filter. Passing a raw int for this may lead to unexpected behaviour. - afk_channel : typing.Union[hikari.channels.GuildVoiceChannel, hikari.bases.Snowflake, int] + afk_channel : typing.Union[hikari.models.channels.GuildVoiceChannel, hikari.models.bases.Snowflake, int] If specified, the object or ID for the new AFK voice channel. afk_timeout : typing.Union[datetime.timedelta, int] If specified, the new AFK timeout seconds timedelta. - icon : hikari.files.BaseStream + icon : hikari.models.files.BaseStream If specified, the new guild icon image file. - owner : typing.Union[hikari.users.User, hikari.bases.Snowflake, int] + owner : typing.Union[hikari.models.users.User, hikari.models.bases.Snowflake, int] If specified, the object or ID of the new guild owner. - splash : hikari.files.BaseStream + splash : hikari.models.files.BaseStream If specified, the new new splash image file. - system_channel : typing.Union[hikari.channels.GuildVoiceChannel, hikari.bases.Snowflake, int] + system_channel : typing.Union[hikari.models.channels.GuildVoiceChannel, hikari.models.bases.Snowflake, int] If specified, the object or ID of the new system channel. reason : str If specified, the audit log reason explaining why the operation @@ -591,7 +591,7 @@ async def update_guild( Returns ------- - hikari.guilds.Guild + hikari.models.guilds.Guild The edited guild object. Raises @@ -636,7 +636,7 @@ async def delete_guild(self, guild: typing.Union[bases.Snowflake, int, str, guil Parameters ---------- - guild : typing.Union[hikari.guilds.Guild, hikari.bases.Snowflake, int] + guild : typing.Union[hikari.models.guilds.Guild, hikari.models.bases.Snowflake, int] The object or ID of the guild to be deleted. Raises @@ -658,12 +658,12 @@ async def fetch_guild_channels( Parameters ---------- - guild : typing.Union[hikari.guilds.Guild, hikari.bases.Snowflake, int] + guild : typing.Union[hikari.models.guilds.Guild, hikari.models.bases.Snowflake, int] The object or ID of the guild to get the channels from. Returns ------- - typing.Sequence[hikari.channels.GuildChannel] + typing.Sequence[hikari.models.channels.GuildChannel] A list of guild channel objects. Raises @@ -700,12 +700,12 @@ async def create_guild_channel( # pylint: disable=too-many-arguments Parameters ---------- - guild : typing.Union[hikari.guilds.Guild, hikari.bases.Snowflake, int] + guild : typing.Union[hikari.models.guilds.Guild, hikari.models.bases.Snowflake, int] The object or ID of the guild to create the channel in. name : str If specified, the name for the channel. This must be inclusively between `1` and `100` characters in length. - channel_type : typing.Union[hikari.channels.ChannelType, int] + channel_type : typing.Union[hikari.models.channels.ChannelType, int] If specified, the channel type, passing through a raw integer here may lead to unexpected behaviour. position : int @@ -731,10 +731,10 @@ async def create_guild_channel( # pylint: disable=too-many-arguments If specified, the max number of users to allow in a voice channel. This must be between `0` and `99` inclusive, where `0` implies no limit. - permission_overwrites : typing.Sequence[hikari.channels.PermissionOverwrite] + permission_overwrites : typing.Sequence[hikari.models.channels.PermissionOverwrite] If specified, the list of permission overwrite objects that are category specific to replace the existing overwrites with. - parent_category : typing.Union[hikari.channels.GuildCategory, hikari.bases.Snowflake, int] + parent_category : typing.Union[hikari.models.channels.GuildCategory, hikari.models.bases.Snowflake, int] If specified, the object or ID of the parent category to set for the channel. reason : str @@ -743,7 +743,7 @@ async def create_guild_channel( # pylint: disable=too-many-arguments Returns ------- - hikari.channels.GuildChannel + hikari.models.channels.GuildChannel The newly created channel object. Raises @@ -795,12 +795,12 @@ async def reposition_guild_channels( Parameters ---------- - guild : typing.Union[hikari.guilds.Guild, hikari.bases.Snowflake, int] + guild : typing.Union[hikari.models.guilds.Guild, hikari.models.bases.Snowflake, int] The object or ID of the guild in which to edit the channels. - channel : typing.Tuple[int , typing.Union[hikari.channels.GuildChannel, hikari.bases.Snowflake, int]] + channel : typing.Tuple[int , typing.Union[hikari.models.channels.GuildChannel, hikari.models.bases.Snowflake, int]] The first channel to change the position of. This is a tuple of the integer position the channel object or ID. - *additional_channels: typing.Tuple[int, typing.Union[hikari.channels.GuildChannel, hikari.bases.Snowflake, int]] + *additional_channels: typing.Tuple[int, typing.Union[hikari.models.channels.GuildChannel, hikari.models.bases.Snowflake, int]] Optional additional channels to change the position of. These must be tuples of integer positions to change to and the channel object or ID and the. @@ -835,14 +835,14 @@ async def fetch_member( Parameters ---------- - guild : typing.Union[hikari.guilds.Guild, hikari.bases.Snowflake, int] + guild : typing.Union[hikari.models.guilds.Guild, hikari.models.bases.Snowflake, int] The object or ID of the guild to get the member from. - user : typing.Union[hikari.users.User, hikari.bases.Snowflake, int] + user : typing.Union[hikari.models.users.User, hikari.models.bases.Snowflake, int] The object or ID of the member to get. Returns ------- - hikari.guilds.GuildMember + hikari.models.guilds.GuildMember The requested member object. Raises @@ -868,7 +868,7 @@ def fetch_members( Parameters ---------- - guild : typing.Union[hikari.guilds.Guild, hikari.bases.Snowflake, int] + guild : typing.Union[hikari.models.guilds.Guild, hikari.models.bases.Snowflake, int] The object or ID of the guild to get the members from. Examples @@ -879,7 +879,7 @@ def fetch_members( Returns ------- - hikari.pagination.PaginatedResults[[hikari.guilds.GuildMember] + hikari.models.pagination.PaginatedResults[[hikari.models.guilds.GuildMember] An async iterator of member objects. Raises @@ -909,14 +909,14 @@ async def update_member( # pylint: disable=too-many-arguments Parameters ---------- - guild : typing.Union[hikari.guilds.Guild, hikari.bases.Snowflake, int] + guild : typing.Union[hikari.models.guilds.Guild, hikari.models.bases.Snowflake, int] The object or ID of the guild to edit the member from. - user : typing.Union[hikari.guilds.GuildMember, hikari.bases.Snowflake, int] + user : typing.Union[hikari.models.guilds.GuildMember, hikari.models.bases.Snowflake, int] The object or ID of the member to edit. nickname : str, optional If specified, the new nickname string. Setting it to `None` explicitly will clear the nickname. - roles : typing.Sequence[typing.Union[hikari.guilds.GuildRole, hikari.bases.Snowflake, int]] + roles : typing.Sequence[typing.Union[hikari.models.guilds.GuildRole, hikari.models.bases.Snowflake, int]] If specified, a list of role IDs the member should have. mute : bool If specified, whether the user should be muted in the voice channel @@ -924,7 +924,7 @@ async def update_member( # pylint: disable=too-many-arguments deaf : bool If specified, whether the user should be deafen in the voice channel or not. - voice_channel : typing.Union[hikari.channels.GuildVoiceChannel, hikari.bases.Snowflake, int], optional + voice_channel : typing.Union[hikari.models.channels.GuildVoiceChannel, hikari.models.bases.Snowflake, int], optional If specified, the ID of the channel to move the member to. Setting it to `None` explicitly will disconnect the user. reason : str @@ -977,7 +977,7 @@ async def update_my_member_nickname( Parameters ---------- - guild : typing.Union[hikari.guilds.Guild, hikari.bases.Snowflake, int] + guild : typing.Union[hikari.models.guilds.Guild, hikari.models.bases.Snowflake, int] The object or ID of the guild you want to change the nick on. nickname : str, optional The new nick string. Setting this to `None` clears the nickname. @@ -1014,11 +1014,11 @@ async def add_role_to_member( Parameters ---------- - guild : typing.Union[hikari.guilds.Guild, hikari.bases.Snowflake, int] + guild : typing.Union[hikari.models.guilds.Guild, hikari.models.bases.Snowflake, int] The object or ID of the guild the member belongs to. - user : typing.Union[hikari.users.User, hikari.bases.Snowflake, int] + user : typing.Union[hikari.models.users.User, hikari.models.bases.Snowflake, int] The object or ID of the member you want to add the role to. - role : typing.Union[hikari.guilds.GuildRole, hikari.bases.Snowflake, int] + role : typing.Union[hikari.models.guilds.GuildRole, hikari.models.bases.Snowflake, int] The object or ID of the role you want to add. reason : str If specified, the audit log reason explaining why the operation @@ -1053,11 +1053,11 @@ async def remove_role_from_member( Parameters ---------- - guild : typing.Union[hikari.guilds.Guild, hikari.bases.Snowflake, int] + guild : typing.Union[hikari.models.guilds.Guild, hikari.models.bases.Snowflake, int] The object or ID of the guild the member belongs to. - user : typing.Union[hikari.users.User, hikari.bases.Snowflake, int] + user : typing.Union[hikari.models.users.User, hikari.models.bases.Snowflake, int] The object or ID of the member you want to remove the role from. - role : typing.Union[hikari.guilds.GuildRole, hikari.bases.Snowflake, int] + role : typing.Union[hikari.models.guilds.GuildRole, hikari.models.bases.Snowflake, int] The object or ID of the role you want to remove. reason : str If specified, the audit log reason explaining why the operation @@ -1091,9 +1091,9 @@ async def kick_member( Parameters ---------- - guild : typing.Union[hikari.guilds.Guild, hikari.bases.Snowflake, int] + guild : typing.Union[hikari.models.guilds.Guild, hikari.models.bases.Snowflake, int] The object or ID of the guild the member belongs to. - user : typing.Union[hikari.users.User, hikari.bases.Snowflake, int] + user : typing.Union[hikari.models.users.User, hikari.models.bases.Snowflake, int] The object or ID of the member you want to kick. reason : str If specified, the audit log reason explaining why the operation @@ -1124,14 +1124,14 @@ async def fetch_ban( Parameters ---------- - guild : typing.Union[hikari.guilds.Guild, hikari.bases.Snowflake, int] + guild : typing.Union[hikari.models.guilds.Guild, hikari.models.bases.Snowflake, int] The object or ID of the guild you want to get the ban from. - user : typing.Union[hikari.users.User, hikari.bases.Snowflake, int] + user : typing.Union[hikari.models.users.User, hikari.models.bases.Snowflake, int] The object or ID of the user to get the ban information for. Returns ------- - hikari.guilds.GuildMemberBan + hikari.models.guilds.GuildMemberBan A ban object for the requested user. Raises @@ -1158,12 +1158,12 @@ async def fetch_bans( Parameters ---------- - guild : typing.Union[hikari.guilds.Guild, hikari.bases.Snowflake, int] + guild : typing.Union[hikari.models.guilds.Guild, hikari.models.bases.Snowflake, int] The object or ID of the guild you want to get the bans from. Returns ------- - typing.Sequence[hikari.guilds.GuildMemberBan] + typing.Sequence[hikari.models.guilds.GuildMemberBan] A list of ban objects. Raises @@ -1193,9 +1193,9 @@ async def ban_member( Parameters ---------- - guild : typing.Union[hikari.guilds.Guild, hikari.bases.Snowflake, int] + guild : typing.Union[hikari.models.guilds.Guild, hikari.models.bases.Snowflake, int] The object or ID of the guild the member belongs to. - user : typing.Union[hikari.users.User, hikari.bases.Snowflake, int] + user : typing.Union[hikari.models.users.User, hikari.models.bases.Snowflake, int] The object or ID of the member you want to ban. delete_message_days : typing.Union[datetime.timedelta, int] If specified, the tim delta of how many days of messages from the @@ -1232,9 +1232,9 @@ async def unban_member( Parameters ---------- - guild : typing.Union[hikari.guilds.Guild, hikari.bases.Snowflake, int] + guild : typing.Union[hikari.models.guilds.Guild, hikari.models.bases.Snowflake, int] The object or ID of the guild to un-ban the user from. - user : typing.Union[hikari.users.User, hikari.bases.Snowflake, int] + user : typing.Union[hikari.models.users.User, hikari.models.bases.Snowflake, int] The ID of the user you want to un-ban. reason : str If specified, the audit log reason explaining why the operation @@ -1265,12 +1265,12 @@ async def fetch_roles( Parameters ---------- - guild : typing.Union[hikari.guilds.Guild, hikari.bases.Snowflake, int] + guild : typing.Union[hikari.models.guilds.Guild, hikari.models.bases.Snowflake, int] The object or ID of the guild you want to get the roles from. Returns ------- - typing.Mapping[hikari.bases.Snowflake, hikari.guilds.GuildRole] + typing.Mapping[hikari.models.bases.Snowflake, hikari.models.guilds.GuildRole] A list of role objects. Raises @@ -1303,14 +1303,14 @@ async def create_role( Parameters ---------- - guild : typing.Union[hikari.guilds.Guild, hikari.bases.Snowflake, int] + guild : typing.Union[hikari.models.guilds.Guild, hikari.models.bases.Snowflake, int] The object or ID of the guild you want to create the role on. name : str If specified, the new role name string. - permissions : typing.Union[hikari.permissions.Permission, int] + permissions : typing.Union[hikari.models.permissions.Permission, int] If specified, the permissions integer for the role, passing a raw integer rather than the int flag may lead to unexpected results. - color : typing.Union[hikari.colors.Color, int] + color : typing.Union[hikari.models.colors.Color, int] If specified, the color for the role. hoist : bool If specified, whether the role will be hoisted. @@ -1323,7 +1323,7 @@ async def create_role( Returns ------- - hikari.guilds.GuildRole + hikari.models.guilds.GuildRole The newly created role object. Raises @@ -1359,18 +1359,18 @@ async def reposition_roles( Parameters ---------- - guild : typing.Union[hikari.guilds.Guild, hikari.bases.Snowflake, int] + guild : typing.Union[hikari.models.guilds.Guild, hikari.models.bases.Snowflake, int] The ID of the guild the roles belong to. - role : typing.Tuple[int, typing.Union[hikari.guilds.GuildRole, hikari.bases.Snowflake, int]] + role : typing.Tuple[int, typing.Union[hikari.models.guilds.GuildRole, hikari.models.bases.Snowflake, int]] The first role to move. This is a tuple of the integer position and the role object or ID. - *additional_roles : typing.Tuple[int, typing.Union[hikari.guilds.GuildRole, hikari.bases.Snowflake, int]] + *additional_roles : typing.Tuple[int, typing.Union[hikari.models.guilds.GuildRole, hikari.models.bases.Snowflake, int]] Optional extra roles to move. These must be tuples of the integer position and the role object or ID. Returns ------- - typing.Sequence[hikari.guilds.GuildRole] + typing.Sequence[hikari.models.guilds.GuildRole] A list of all the guild roles. Raises @@ -1410,16 +1410,16 @@ async def update_role( Parameters ---------- - guild : typing.Union[hikari.guilds.Guild, hikari.bases.Snowflake, int] + guild : typing.Union[hikari.models.guilds.Guild, hikari.models.bases.Snowflake, int] The object or ID of the guild the role belong to. - role : typing.Union[hikari.guilds.GuildRole, hikari.bases.Snowflake, int] + role : typing.Union[hikari.models.guilds.GuildRole, hikari.models.bases.Snowflake, int] The object or ID of the role you want to edit. name : str If specified, the new role's name string. - permissions : typing.Union[hikari.permissions.Permission, int] + permissions : typing.Union[hikari.models.permissions.Permission, int] If specified, the new permissions integer for the role, passing a raw integer for this may lead to unexpected behaviour. - color : typing.Union[hikari.colors.Color, int] + color : typing.Union[hikari.models.colors.Color, int] If specified, the new color for the new role passing a raw integer for this may lead to unexpected behaviour. hoist : bool @@ -1432,7 +1432,7 @@ async def update_role( Returns ------- - hikari.guilds.GuildRole + hikari.models.guilds.GuildRole The edited role object. Raises @@ -1468,9 +1468,9 @@ async def delete_role( Parameters ---------- - guild : typing.Union[hikari.guilds.Guild, hikari.bases.Snowflake, int] + guild : typing.Union[hikari.models.guilds.Guild, hikari.models.bases.Snowflake, int] The object or ID of the guild you want to remove the role from. - role : typing.Union[hikari.guilds.GuildRole, hikari.bases.Snowflake, int] + role : typing.Union[hikari.models.guilds.GuildRole, hikari.models.bases.Snowflake, int] The object or ID of the role you want to delete. Raises @@ -1495,7 +1495,7 @@ async def estimate_guild_prune_count( Parameters ---------- - guild : typing.Union[hikari.guilds.Guild, hikari.bases.Snowflake, int] + guild : typing.Union[hikari.models.guilds.Guild, hikari.models.bases.Snowflake, int] The object or ID of the guild you want to get the count for. days : typing.Union[datetime.timedelta, int] The time delta of days to count prune for (at least `1`). @@ -1532,7 +1532,7 @@ async def begin_guild_prune( Parameters ---------- - guild : typing.Union[hikari.guilds.Guild, hikari.bases.Snowflake, int] + guild : typing.Union[hikari.models.guilds.Guild, hikari.models.bases.Snowflake, int] The object or ID of the guild you want to prune member of. days : typing.Union[datetime.timedelta, int] The time delta of inactivity days you want to use as filter. @@ -1575,12 +1575,12 @@ async def fetch_guild_voice_regions( Parameters ---------- - guild : typing.Union[hikari.guilds.Guild, hikari.bases.Snowflake, int] + guild : typing.Union[hikari.models.guilds.Guild, hikari.models.bases.Snowflake, int] The object or ID of the guild to get the voice regions for. Returns ------- - typing.Sequence[hikari.voices.VoiceRegion] + typing.Sequence[hikari.models.voices.VoiceRegion] A list of voice region objects. Raises @@ -1605,12 +1605,12 @@ async def fetch_guild_invites( Parameters ---------- - guild : typing.Union[hikari.guilds.Guild, hikari.bases.Snowflake, int] + guild : typing.Union[hikari.models.guilds.Guild, hikari.models.bases.Snowflake, int] The object or ID of the guild to get the invites for. Returns ------- - typing.Sequence[hikari.invites.InviteWithMetadata] + typing.Sequence[hikari.models.invites.InviteWithMetadata] A list of invite objects (with metadata). Raises @@ -1635,12 +1635,12 @@ async def fetch_integrations( Parameters ---------- - guild : typing.Union[hikari.guilds.Guild, hikari.bases.Snowflake, int] + guild : typing.Union[hikari.models.guilds.Guild, hikari.models.bases.Snowflake, int] The object or ID of the guild to get the integrations for. Returns ------- - typing.Sequence[hikari.guilds.GuildIntegration] + typing.Sequence[hikari.models.guilds.GuildIntegration] A list of integration objects. Raises @@ -1672,11 +1672,11 @@ async def update_integration( Parameters ---------- - guild : typing.Union[hikari.guilds.Guild, hikari.bases.Snowflake, int] + guild : typing.Union[hikari.models.guilds.Guild, hikari.models.bases.Snowflake, int] The object or ID of the guild to which the integration belongs to. - integration : typing.Union[hikari.guilds.GuildIntegration, hikari.bases.Snowflake, int] + integration : typing.Union[hikari.models.guilds.GuildIntegration, hikari.models.bases.Snowflake, int] The object or ID of the integration to update. - expire_behaviour : typing.Union[hikari.guilds.IntegrationExpireBehaviour, int] + expire_behaviour : typing.Union[hikari.models.guilds.IntegrationExpireBehaviour, int] If specified, the behaviour for when an integration subscription expires (passing a raw integer for this may lead to unexpected behaviour). @@ -1719,9 +1719,9 @@ async def delete_integration( Parameters ---------- - guild : typing.Union[hikari.guilds.Guild, hikari.bases.Snowflake, int] + guild : typing.Union[hikari.models.guilds.Guild, hikari.models.bases.Snowflake, int] The object or ID of the guild to which the integration belongs to. - integration : typing.Union[hikari.guilds.GuildIntegration, hikari.bases.Snowflake, int] + integration : typing.Union[hikari.models.guilds.GuildIntegration, hikari.models.bases.Snowflake, int] The object or ID of the integration to delete. reason : str If specified, the audit log reason explaining why the operation @@ -1752,9 +1752,9 @@ async def sync_guild_integration( Parameters ---------- - guild : typing.Union[hikari.guilds.Guild, hikari.bases.Snowflake, int] + guild : typing.Union[hikari.models.guilds.Guild, hikari.models.bases.Snowflake, int] The object or ID of the guild to which the integration belongs to. - integration : typing.Union[hikari.guilds.GuildIntegration, hikari.bases.Snowflake, int] + integration : typing.Union[hikari.models.guilds.GuildIntegration, hikari.models.bases.Snowflake, int] The ID of the integration to sync. Raises @@ -1779,12 +1779,12 @@ async def fetch_guild_embed( Parameters ---------- - guild : typing.Union[hikari.guilds.Guild, hikari.bases.Snowflake, int] + guild : typing.Union[hikari.models.guilds.Guild, hikari.models.bases.Snowflake, int] The object or ID of the guild to get the embed for. Returns ------- - hikari.guilds.GuildEmbed + hikari.models.guilds.GuildEmbed A guild embed object. Raises @@ -1815,9 +1815,9 @@ async def update_guild_embed( Parameters ---------- - guild : typing.Union[hikari.guilds.Guild, hikari.bases.Snowflake, int] + guild : typing.Union[hikari.models.guilds.Guild, hikari.models.bases.Snowflake, int] The object or ID of the guild to edit the embed for. - channel : typing.Union[hikari.channels.GuildChannel, hikari.bases.Snowflake, int], optional + channel : typing.Union[hikari.models.channels.GuildChannel, hikari.models.bases.Snowflake, int], optional If specified, the object or ID of the channel that this embed's invite should target. Set to `None` to disable invites for this embed. @@ -1829,7 +1829,7 @@ async def update_guild_embed( Returns ------- - hikari.guilds.GuildEmbed + hikari.models.guilds.GuildEmbed The updated embed object. Raises @@ -1861,12 +1861,12 @@ async def fetch_guild_vanity_url( Parameters ---------- - guild : typing.Union[hikari.guilds.Guild, hikari.bases.Snowflake, int] + guild : typing.Union[hikari.models.guilds.Guild, hikari.models.bases.Snowflake, int] The object or ID of the guild to get the vanity URL for. Returns ------- - hikari.invites.VanityUrl + hikari.models.invites.VanityUrl A partial invite object containing the vanity URL in the `code` field. @@ -1893,7 +1893,7 @@ def format_guild_widget_image( Parameters ---------- - guild : typing.Union[hikari.guilds.Guild, hikari.bases.Snowflake, int] + guild : typing.Union[hikari.models.guilds.Guild, hikari.models.bases.Snowflake, int] The object or ID of the guild to form the widget. style : str If specified, the style of the widget. @@ -1923,12 +1923,12 @@ async def fetch_guild_webhooks( Parameters ---------- - guild : typing.Union[hikari.guilds.Guild, hikari.bases.Snowflake, int] + guild : typing.Union[hikari.models.guilds.Guild, hikari.models.bases.Snowflake, int] The object or ID for the guild to get the webhooks from. Returns ------- - typing.Sequence[hikari.webhooks.Webhook] + typing.Sequence[hikari.models.webhooks.Webhook] A list of webhook objects for the given guild. Raises diff --git a/hikari/rest/invite.py b/hikari/rest/invite.py index 65c486ecae..22f04b443d 100644 --- a/hikari/rest/invite.py +++ b/hikari/rest/invite.py @@ -40,7 +40,7 @@ async def fetch_invite( Parameters ---------- - invite : typing.Union[hikari.invites.Invite, str] + invite : typing.Union[hikari.models.invites.Invite, str] The object or code of the wanted invite. with_counts : bool If specified, whether to attempt to count the number of @@ -48,7 +48,7 @@ async def fetch_invite( Returns ------- - hikari.invites.Invite + hikari.models.invites.Invite The requested invite object. Raises @@ -67,7 +67,7 @@ async def delete_invite(self, invite: typing.Union[invites.Invite, str]) -> None Parameters ---------- - invite : typing.Union[hikari.invites.Invite, str] + invite : typing.Union[hikari.models.invites.Invite, str] The object or ID for the invite to be deleted. Returns diff --git a/hikari/rest/me.py b/hikari/rest/me.py index 2fa1280dfe..d2731106d5 100644 --- a/hikari/rest/me.py +++ b/hikari/rest/me.py @@ -72,7 +72,7 @@ async def fetch_me(self) -> users.MyUser: Returns ------- - hikari.users.MyUser + hikari.models.users.MyUser The current user object. """ payload = await self._session.get_current_user() @@ -85,7 +85,7 @@ async def update_me(self, *, username: str = ..., avatar: typing.Optional[files. ---------- username : str If specified, the new username string. - avatar : hikari.files.BaseStream, optional + avatar : hikari.models.files.BaseStream, optional If specified, the new avatar image data. If it is None, the avatar is removed. @@ -96,7 +96,7 @@ async def update_me(self, *, username: str = ..., avatar: typing.Optional[files. Returns ------- - hikari.users.MyUser + hikari.models.users.MyUser The updated user object. Raises @@ -121,7 +121,7 @@ async def fetch_my_connections(self) -> typing.Sequence[applications.OwnConnecti Returns ------- - typing.Sequence[hikari.applications.OwnConnection] + typing.Sequence[hikari.models.applications.OwnConnection] A list of connection objects. """ payload = await self._session.get_current_user_connections() @@ -140,7 +140,7 @@ def fetch_my_guilds( newest_first : bool If specified and `True`, the guilds are returned in the order of newest to oldest. The default is to return oldest guilds first. - start_at : datetime.datetime OR hikari.bases.UniqueEntity OR hikari.bases.Snowflake or int, optional + start_at : datetime.datetime OR hikari.models.bases.Unique OR hikari.models.bases.Snowflake or int, optional The optional first item to start at, if you want to limit your results. This will be interpreted as the date of creation for a guild. If unspecified, the newest or older possible snowflake is @@ -148,7 +148,7 @@ def fetch_my_guilds( Returns ------- - hikari.pagination.PaginatedResults[hikari.applications.OwnGuild] + hikari.models.pagination.PaginatedResults[hikari.models.applications.OwnGuild] An async iterable of partial guild objects. Raises @@ -163,7 +163,7 @@ async def leave_guild(self, guild: typing.Union[bases.Snowflake, int, str, guild Parameters ---------- - guild : typing.Union[hikari.guilds.Guild, hikari.bases.Snowflake, int] + guild : typing.Union[hikari.models.guilds.Guild, hikari.models.bases.Snowflake, int] The object or ID of the guild to leave. Raises @@ -183,12 +183,12 @@ async def create_dm_channel( Parameters ---------- - recipient : typing.Union[hikari.users.User, hikari.bases.Snowflake, int] + recipient : typing.Union[hikari.models.users.User, hikari.models.bases.Snowflake, int] The object or ID of the user to create the new DM channel with. Returns ------- - hikari.channels.DMChannel + hikari.models.channels.DMChannel The newly created DM channel object. Raises diff --git a/hikari/rest/oauth2.py b/hikari/rest/oauth2.py index d4b1780b79..c813ccde30 100644 --- a/hikari/rest/oauth2.py +++ b/hikari/rest/oauth2.py @@ -36,7 +36,7 @@ async def fetch_my_application_info(self) -> applications.Application: Returns ------- - hikari.applications.Application + hikari.models.applications.Application An application info object. """ payload = await self._session.get_current_application_info() diff --git a/hikari/rest/react.py b/hikari/rest/react.py index 6cf5611c3a..fac9812a0a 100644 --- a/hikari/rest/react.py +++ b/hikari/rest/react.py @@ -75,11 +75,11 @@ async def add_reaction( Parameters ---------- - channel : typing.Union[hikari.channels.PartialChannel, hikari.bases.Snowflake, int] + channel : typing.Union[hikari.models.channels.PartialChannel, hikari.models.bases.Snowflake, int] The object or ID of the channel to add this reaction in. - message : typing.Union[hikari.messages.Message, hikari.bases.Snowflake, int] + message : typing.Union[hikari.models.messages.Message, hikari.models.bases.Snowflake, int] The object or ID of the message to add the reaction in. - emoji : typing.Union[hikari.emojis.Emoji, str] + emoji : typing.Union[hikari.models.emojis.Emoji, str] The emoji to add. This can either be an emoji object or a string representation of an emoji. The string representation will be either `"name:id"` for custom emojis, or it's unicode character(s) for @@ -119,16 +119,16 @@ async def remove_reaction( Parameters ---------- - channel : typing.Union[hikari.channels.PartialChannel, hikari.bases.Snowflake, int] + channel : typing.Union[hikari.models.channels.PartialChannel, hikari.models.bases.Snowflake, int] The object or ID of the channel to add this reaction in. - message : typing.Union[hikari.messages.Message, hikari.bases.Snowflake, int] + message : typing.Union[hikari.models.messages.Message, hikari.models.bases.Snowflake, int] The object or ID of the message to add the reaction in. - emoji : typing.Union[hikari.emojis.Emoji, str] + emoji : typing.Union[hikari.models.emojis.Emoji, str] The emoji to add. This can either be an emoji object or a string representation of an emoji. The string representation will be either `"name:id"` for custom emojis else it's unicode character(s) (can be UTF-32). - user : typing.Union[hikari.users.User, hikari.bases.Snowflake, int], optional + user : typing.Union[hikari.models.users.User, hikari.models.bases.Snowflake, int], optional The user to remove the reaction of. If this is `None`, then the bot's own reaction is removed instead. @@ -173,11 +173,11 @@ async def remove_all_reactions( Parameters ---------- - channel : typing.Union[hikari.channels.PartialChannel, hikari.bases.Snowflake, int] + channel : typing.Union[hikari.models.channels.PartialChannel, hikari.models.bases.Snowflake, int] The object or ID of the channel to get the message from. - message : typing.Union[hikari.messages.Message, hikari.bases.Snowflake, int] + message : typing.Union[hikari.models.messages.Message, hikari.models.bases.Snowflake, int] The object or ID of the message to delete the reactions from. - emoji : typing.Union[hikari.emojis.Emoji, str], optional + emoji : typing.Union[hikari.models.emojis.Emoji, str], optional The object or string representation of the emoji to delete. The string representation will be either `"name:id"` for custom emojis else it's unicode character(s) (can be UTF-32). @@ -220,16 +220,16 @@ def fetch_reactors( Parameters ---------- - channel : typing.Union[hikari.channels.PartialChannel, hikari.bases.Snowflake, int] + channel : typing.Union[hikari.models.channels.PartialChannel, hikari.models.bases.Snowflake, int] The object or ID of the channel to get the message from. - message : typing.Union[hikari.messages.Message, hikari.bases.Snowflake, int] + message : typing.Union[hikari.models.messages.Message, hikari.models.bases.Snowflake, int] The object or ID of the message to get the reactions from. - emoji : typing.Union[hikari.emojis.Emoji, str] + emoji : typing.Union[hikari.models.emojis.Emoji, str] The emoji to get. This can either be it's object or the string representation of the emoji. The string representation will be either `"name:id"` for custom emojis else it's unicode character(s) (can be UTF-32). - after : datetime.datetime OR hikari.bases.Unique OR hikari.bases.Snowflake OR int OR str, optional + after : datetime.datetime OR hikari.models.bases.Unique OR hikari.models.bases.Snowflake OR int OR str, optional A limit to the users returned. This allows you to only receive users that were created after the given object, if desired. If a snowflake/int/str/unique object, then this will use the @@ -245,7 +245,7 @@ def fetch_reactors( Returns ------- - hikari.pagination.PaginatedResults[hikari.users.User] + hikari.models.pagination.PaginatedResults[hikari.models.users.User] An async iterator of user objects. Raises diff --git a/hikari/rest/session.py b/hikari/rest/session.py index f4b1f49242..1200e2f950 100644 --- a/hikari/rest/session.py +++ b/hikari/rest/session.py @@ -661,7 +661,7 @@ async def create_message( Nonces are limited to 32 bytes in size. tts : bool If specified, whether the message will be sent as a TTS message. - files : typing.Sequence[hikari.files.BaseStream] + files : typing.Sequence[hikari.models.files.BaseStream] If specified, this should be a list of between `1` and `5` file objects to upload. Each should have a unique name. embed : more_typing.JSONObject @@ -3036,7 +3036,7 @@ async def execute_webhook( # pylint:disable=too-many-locals wait : bool If specified, whether this request should wait for the webhook to be executed and return the resultant message object. - files : typing.Sequence[hikari.files.BaseStream] + files : typing.Sequence[hikari.models.files.BaseStream] If specified, the optional file objects to upload. embeds : typing.Sequence[more_typing.JSONObject] If specified, the sequence of embed objects that will be sent diff --git a/hikari/rest/user.py b/hikari/rest/user.py index 12fae68183..8b7e9686ea 100644 --- a/hikari/rest/user.py +++ b/hikari/rest/user.py @@ -39,12 +39,12 @@ async def fetch_user(self, user: typing.Union[bases.Snowflake, int, str, users.U Parameters ---------- - user : typing.Union[hikari.users.User, hikari.bases.Snowflake, int] + user : typing.Union[hikari.models.users.User, hikari.models.bases.Snowflake, int] The object or ID of the user to get. Returns ------- - hikari.users.User + hikari.models.users.User The requested user object. Raises diff --git a/hikari/rest/voice.py b/hikari/rest/voice.py index 9f0e962f02..400e645f78 100644 --- a/hikari/rest/voice.py +++ b/hikari/rest/voice.py @@ -37,7 +37,7 @@ async def fetch_voice_regions(self) -> typing.Sequence[voices.VoiceRegion]: Returns ------- - typing.Sequence[hikari.voices.VoiceRegion] + typing.Sequence[hikari.models.voices.VoiceRegion] A list of voice regions available !!! note diff --git a/hikari/rest/webhook.py b/hikari/rest/webhook.py index c19caaec83..3fcfb2bf28 100644 --- a/hikari/rest/webhook.py +++ b/hikari/rest/webhook.py @@ -50,7 +50,7 @@ async def fetch_webhook( Parameters ---------- - webhook : typing.Union[hikari.webhooks.Webhook, hikari.bases.Snowflake, int] + webhook : typing.Union[hikari.models.webhooks.Webhook, hikari.models.bases.Snowflake, int] The object or ID of the webhook to get. webhook_token : str If specified, the webhook token to use to get it (bypassing this @@ -58,7 +58,7 @@ async def fetch_webhook( Returns ------- - hikari.webhooks.Webhook + hikari.models.webhooks.Webhook The requested webhook object. Raises @@ -94,17 +94,17 @@ async def update_webhook( Parameters ---------- - webhook : typing.Union[hikari.webhooks.Webhook, hikari.bases.Snowflake, int] + webhook : typing.Union[hikari.models.webhooks.Webhook, hikari.models.bases.Snowflake, int] The object or ID of the webhook to edit. webhook_token : str If specified, the webhook token to use to modify it (bypassing this session's provided authorization `token`). name : str If specified, the new name string. - avatar : hikari.files.BaseStream, optional + avatar : hikari.models.files.BaseStream, optional If specified, the new avatar image. If `None`, then it is removed. - channel : typing.Union[hikari.channels.GuildChannel, hikari.bases.Snowflake, int] + channel : typing.Union[hikari.models.channels.GuildChannel, hikari.models.bases.Snowflake, int] If specified, the object or ID of the new channel the given webhook should be moved to. reason : str @@ -113,7 +113,7 @@ async def update_webhook( Returns ------- - hikari.webhooks.Webhook + hikari.models.webhooks.Webhook The updated webhook object. Raises @@ -150,7 +150,7 @@ async def delete_webhook( Parameters ---------- - webhook : typing.Union[hikari.webhooks.Webhook, hikari.bases.Snowflake, int] + webhook : typing.Union[hikari.models.webhooks.Webhook, hikari.models.bases.Snowflake, int] The object or ID of the webhook to delete webhook_token : str If specified, the webhook token to use to delete it (bypassing this @@ -198,7 +198,7 @@ async def execute_webhook( # pylint:disable=too-many-locals,line-too-long Parameters ---------- - webhook : typing.Union[hikari.webhooks.Webhook, hikari.bases.Snowflake, int] + webhook : typing.Union[hikari.models.webhooks.Webhook, hikari.models.bases.Snowflake, int] The object or ID of the webhook to execute. webhook_token : str The token of the webhook to execute. @@ -215,26 +215,26 @@ async def execute_webhook( # pylint:disable=too-many-locals,line-too-long wait : bool If specified, whether this request should wait for the webhook to be executed and return the resultant message object. - files : typing.Sequence[hikari.files.BaseStream] + files : typing.Sequence[hikari.models.files.BaseStream] If specified, a sequence of files to upload. - embeds : typing.Sequence[hikari.embeds.Embed] + embeds : typing.Sequence[hikari.models.embeds.Embed] If specified, a sequence of between `1` to `10` embed objects (inclusive) to send with the embed. mentions_everyone : bool Whether `@everyone` and `@here` mentions should be resolved by discord and lead to actual pings, defaults to `True`. - user_mentions : typing.Union[typing.Collection[typing.Union[hikari.users.User, hikari.bases.Snowflake, int]], bool] + user_mentions : typing.Union[typing.Collection[typing.Union[hikari.models.users.User, hikari.models.bases.Snowflake, int]], bool] Either an array of user objects/IDs to allow mentions for, `True` to allow all user mentions or `False` to block all user mentions from resolving, defaults to `True`. - role_mentions : typing.Union[typing.Collection[typing.Union[hikari.guilds.GuildRole, hikari.bases.Snowflake, int]], bool] + role_mentions : typing.Union[typing.Collection[typing.Union[hikari.models.guilds.GuildRole, hikari.models.bases.Snowflake, int]], bool] Either an array of guild role objects/IDs to allow mentions for, `True` to allow all role mentions or `False` to block all role mentions from resolving, defaults to `True`. Returns ------- - hikari.messages.Message, optional + hikari.models.messages.Message, optional The created message object, if `wait` is `True`, else `None`. Raises From 0e31be725ba50b6efcb4250357bb3e7391139b88 Mon Sep 17 00:00:00 2001 From: davfsa Date: Mon, 18 May 2020 12:48:31 +0200 Subject: [PATCH 356/922] Documentation fixes - Reference errors - Replace "typing.Union" and "OR" with "typehint1 | typehint2" - Replace ", optional" with " | None" --- docs/config.mako | 2 +- hikari/components/bot_base.py | 4 +- hikari/components/dispatchers.py | 6 +- hikari/components/event_managers.py | 8 +- hikari/components/intent_aware_dispatchers.py | 6 +- hikari/configs.py | 76 +++---- hikari/errors.py | 4 +- hikari/gateway/connection.py | 22 +- hikari/internal/conversions.py | 6 +- hikari/internal/helpers.py | 4 +- hikari/internal/http_client.py | 14 +- hikari/internal/marshaller.py | 14 +- hikari/internal/ratelimits.py | 2 +- hikari/internal/urls.py | 2 +- hikari/models/applications.py | 6 +- hikari/models/channels.py | 8 +- hikari/models/embeds.py | 14 +- hikari/models/files.py | 6 +- hikari/models/guilds.py | 14 +- hikari/models/invites.py | 4 +- hikari/models/messages.py | 16 +- hikari/models/webhooks.py | 44 +++- hikari/rest/buckets.py | 2 + hikari/rest/channel.py | 94 ++++---- hikari/rest/guild.py | 206 +++++++++--------- hikari/rest/invite.py | 4 +- hikari/rest/me.py | 8 +- hikari/rest/react.py | 36 +-- hikari/rest/session.py | 36 +-- hikari/rest/user.py | 2 +- hikari/rest/webhook.py | 18 +- tests/hikari/models/test_bases.py | 2 +- 32 files changed, 361 insertions(+), 329 deletions(-) diff --git a/docs/config.mako b/docs/config.mako index c24360cb52..407bd68d5a 100644 --- a/docs/config.mako +++ b/docs/config.mako @@ -53,7 +53,7 @@ # If set, insert Google Analytics tracking code. Value is GA # tracking id (UA-XXXXXX-Y). google_analytics = "" - # If set, insert Google CustomEmoji Search search bar widget above the sidebar index. + # If set, insert Google Custom Search search bar widget above the sidebar index. # The whitespace-separated tokens represent arbitrary extra queries (at least one # must match) passed to regular Google search. Example: #search_query = 'inurl:github.com/USER/PROJECT site:PROJECT.github.io site:PROJECT.website' diff --git a/hikari/components/bot_base.py b/hikari/components/bot_base.py index 2582fe9a48..3df9b6220b 100644 --- a/hikari/components/bot_base.py +++ b/hikari/components/bot_base.py @@ -286,9 +286,9 @@ async def update_presence( ---------- status : hikari.models.guilds.PresenceStatus If specified, the new status to set. - activity : hikari.models.gateway.Activity, optional + activity : hikari.models.gateway.Activity | None If specified, the new activity to set. - idle_since : datetime.datetime, optional + idle_since : datetime.datetime | None If specified, the time to show up as being idle since, or `None` if not applicable. is_afk : bool diff --git a/hikari/components/dispatchers.py b/hikari/components/dispatchers.py index 80e994b218..385af995d3 100644 --- a/hikari/components/dispatchers.py +++ b/hikari/components/dispatchers.py @@ -94,14 +94,14 @@ def wait_for( ---------- event_type : typing.Type[hikari.events.base.HikariEvent] The name of the event to wait for. - timeout : float, optional + timeout : float | None The timeout to wait for before cancelling and raising an `asyncio.TimeoutError` instead. If this is `None`, this will wait forever. Care must be taken if you use `None` as this may leak memory if you do this from an event listener that gets repeatedly called. If you want to do this, you should consider using an event listener instead of this function. - predicate : `def predicate(event) -> bool` or `async def predicate(event) -> bool`, optional + predicate : `def predicate(event) -> bool` | `async def predicate(event) -> bool` | None A function that takes the arguments for the event and returns `True` if it is a match, or `False` if it should be ignored. This can be a coroutine function that returns a boolean, or a @@ -162,7 +162,7 @@ async def on_create_delete(event): Parameters ---------- - event_type : typing.Type[hikari.events.base.HikariEvent], optional + event_type : typing.Type[hikari.events.base.HikariEvent] | None The event type to register the produced decorator to. If this is not specified, then the given function is used instead and the type hint of the first argument is considered. If no type hint is present diff --git a/hikari/components/event_managers.py b/hikari/components/event_managers.py index b9faf8da17..98592318f2 100644 --- a/hikari/components/event_managers.py +++ b/hikari/components/event_managers.py @@ -81,16 +81,16 @@ def _get_event_marker(obj: typing.Any) -> typing.Set[str]: class EventManager(typing.Generic[EventDispatcherT], consumers.RawEventConsumer): """Abstract definition of the application for an event system for a bot. - The class itself inherits from - `hikari.components.consumers.RawEventConsumer` (which allows it to provide the - ability to transform a raw payload into an event object). + The class itself inherits from `hikari.components.consumers.RawEventConsumer` + (which allows it to provide the ability to transform a raw payload into an + event object). This is designed as a basis to enable transformation of raw incoming events from the websocket into more usable native Python objects, and to then dispatch them to a given event dispatcher. It does not provide the logic for how to specifically parse each event however. - !!! note + !!! note This object will detect internal event mapper functions by looking for coroutine functions wrapped with `raw_event_mapper`. diff --git a/hikari/components/intent_aware_dispatchers.py b/hikari/components/intent_aware_dispatchers.py index bfb3f67943..1f1932ed87 100644 --- a/hikari/components/intent_aware_dispatchers.py +++ b/hikari/components/intent_aware_dispatchers.py @@ -59,7 +59,7 @@ class IntentAwareEventDispatcherImpl(dispatchers.EventDispatcher): Parameters ---------- - enabled_intents : hikari.models.intents.Intent, optional + enabled_intents : hikari.models.intents.Intent | None The intents that are enabled for the application. If `None`, then no intent checks are performed when subscribing a new event. """ @@ -313,14 +313,14 @@ def wait_for( ---------- event_type : typing.Type[hikari.events.HikariEvent] The name of the event to wait for. - timeout : float, optional + timeout : float | None The timeout to wait for before cancelling and raising an `asyncio.TimeoutError` instead. If this is `None`, this will wait forever. Care must be taken if you use `None` as this may leak memory if you do this from an event listener that gets repeatedly called. If you want to do this, you should consider using an event listener instead of this function. - predicate : `def predicate(event) -> bool` or `async def predicate(event) -> bool`, optional + predicate : `def predicate(event) -> bool` or `async def predicate(event) -> bool` | None A function that takes the arguments for the event and returns `True` if it is a match, or `False` if it should be ignored. This must be a regular function. diff --git a/hikari/configs.py b/hikari/configs.py index f1e8b3d163..997cf77afc 100644 --- a/hikari/configs.py +++ b/hikari/configs.py @@ -79,29 +79,29 @@ class AIOHTTPConfig(BaseConfig): If `True`, allow following redirects from `3xx` HTTP responses. Generally you do not want to enable this unless you have a good reason to. Defaults to `False` if unspecified during deserialization. - proxy_auth : aiohttp.BasicAuth, optional + proxy_auth : aiohttp.BasicAuth | None Optional proxy authorization to provide in any HTTP requests. This is deserialized using the format `"basic {{base 64 string here}}"`. Defaults to `None` if unspecified during deserialization. - proxy_headers : typing.Mapping[str, str], optional + proxy_headers : typing.Mapping[str, str] | None Optional proxy headers to provide in any HTTP requests. Defaults to `None` if unspecified during deserialization. - proxy_url : str, optional + proxy_url : str | None The optional URL of the proxy to send requests via. Defaults to `None` if unspecified during deserialization. - request_timeout : float, optional + request_timeout : float | None Optional request timeout to use. If an HTTP request takes longer than this, it will be aborted. If not `None`, the value represents a number of seconds as a floating point number. Defaults to `None` if unspecified during deserialization. - ssl_context : ssl.SSLContext, optional + ssl_context : ssl.SSLContext | None The optional SSL context to use. This is deserialized as an object reference in the format `package.module#object.attribute` that is expected to point to the desired value. Defaults to `None` if unspecified during deserialization. - tcp_connector : aiohttp.TCPConnector, optional + tcp_connector : aiohttp.TCPConnector | None This may otherwise be `None` to use the default settings provided by `aiohttp`. This is deserialized as an object reference in the format @@ -160,7 +160,7 @@ class TokenConfig(BaseConfig): Attributes ---------- - token : str, optional + token : str | None The token to use. """ @@ -222,7 +222,7 @@ class GatewayConfig(AIOHTTPConfig, DebugConfig, TokenConfig): Usually you want this turned on. gateway_version : int The gateway API version to use. Defaults to v6 - initial_activity : hikari.models.gateway.Activity, optional + initial_activity : hikari.models.gateway.Activity | None The initial activity to set all shards to when starting the gateway. If this is `None` then no activity will be set, this is the default. initial_status : hikari.models.guilds.PresenceStatus @@ -230,7 +230,7 @@ class GatewayConfig(AIOHTTPConfig, DebugConfig, TokenConfig): Defaults to `ONLINE`. initial_is_afk : bool Whether to show up as AFK or not on sign-in. - initial_idle_since : datetime.datetime, optional + initial_idle_since : datetime.datetime | None The idle time to show on signing in. If set to `None` to not show an idle time, this is the default. intents : hikari.models.intents.Intent @@ -239,44 +239,44 @@ class GatewayConfig(AIOHTTPConfig, DebugConfig, TokenConfig): intent names. If unspecified, this will be set to `None`. large_threshold : int The large threshold to use. - proxy_headers : typing.Mapping[str, str], optional + proxy_headers : typing.Mapping[str, str] | None Optional proxy headers to provide in any HTTP requests. Defaults to `None` if unspecified during deserialization. - proxy_auth : aiohttp.BasicAuth, optional + proxy_auth : aiohttp.BasicAuth | None Optional proxy authorization to provide in any HTTP requests. This is deserialized using the format `"basic {{base 64 string here}}"`. Defaults to `None` if unspecified during deserialization. - proxy_url : str, optional + proxy_url : str | None The optional URL of the proxy to send requests via. Defaults to `None` if unspecified during deserialization. - request_timeout : float, optional + request_timeout : float | None Optional request timeout to use. If an HTTP request takes longer than this, it will be aborted. If not `None`, the value represents a number of seconds as a floating point number. Defaults to `None` if unspecified during deserialization. - shard_count : int, optional + shard_count : int | None The number of shards the entire distributed application should consists of. If you run multiple distributed instances of the bot, you should ensure this value is consistent. This can be set to `None` to enable auto-sharding. This is the default. - shard_id : typing.Sequence[int], optional + shard_id : typing.Sequence[int] | None The shard IDs to produce shard connections for. If being deserialized, this can be several formats shown in `notes`. - ssl_context : ssl.SSLContext, optional + ssl_context : ssl.SSLContext | None The optional SSL context to use. This is deserialized as an object reference in the format `package.module#object.attribute` that is expected to point to the desired value. Defaults to `None` if unspecified during deserialization. - tcp_connector : aiohttp.TCPConnector, optional + tcp_connector : aiohttp.TCPConnector | None This may otherwise be `None` to use the default settings provided by `aiohttp`. This is deserialized as an object reference in the format `package.module#object.attribute` that is expected to point to the desired value. Defaults to `None` if unspecified during deserialization. - token : str, optional + token : str | None The token to use. verify_ssl : bool If `True`, then responses with invalid SSL certificates will be @@ -386,29 +386,29 @@ class RESTConfig(AIOHTTPConfig, DebugConfig, TokenConfig): useful for testing, amongst other things. You can put format-string placeholders in the URL such as `{0.version}` to interpolate the chosen API version to use. - proxy_headers : typing.Mapping[str, str], optional + proxy_headers : typing.Mapping[str, str] | None Optional proxy headers to provide in any HTTP requests. Defaults to `None` if unspecified during deserialization. - proxy_auth : aiohttp.BasicAuth, optional + proxy_auth : aiohttp.BasicAuth | None Optional proxy authorization to provide in any HTTP requests. This is deserialized using the format `"basic {{base 64 string here}}"`. Defaults to `None` if unspecified during deserialization. - proxy_url : str, optional + proxy_url : str | None The optional URL of the proxy to send requests via. Defaults to `None` if unspecified during deserialization. - request_timeout : float, optional + request_timeout : float | None Optional request timeout to use. If an HTTP request takes longer than this, it will be aborted. If not `None`, the value represents a number of seconds as a floating point number. Defaults to `None` if unspecified during deserialization. - ssl_context : ssl.SSLContext, optional + ssl_context : ssl.SSLContext | None The optional SSL context to use. This is deserialized as an object reference in the format `package.module#object.attribute` that is expected to point to the desired value. Defaults to `None` if unspecified during deserialization. - tcp_connector : aiohttp.TCPConnector, optional + tcp_connector : aiohttp.TCPConnector | None This may otherwise be `None` to use the default settings provided by `aiohttp`. This is deserialized as an object reference in the format @@ -422,11 +422,11 @@ class RESTConfig(AIOHTTPConfig, DebugConfig, TokenConfig): this. Disabling SSL verification can have major security implications. You turn this off at your own risk. Defaults to `True` if unspecified during deserialization. - token : str, optional + token : str | None The token to use. debug : bool Whether to enable debugging mode. Usually you don't want to enable this. - token_type : str, optional + token_type : str | None Token authentication scheme, this defaults to `"Bot"` and should be one of `"Bot"` or `"Bearer"`, or `None` if not relevant. rest_version : int @@ -466,7 +466,7 @@ class BotConfig(RESTConfig, GatewayConfig): Usually you want this turned on. gateway_version : int The gateway API version to use. Defaults to v6 - initial_activity : hikari.models.gateway.Activity, optional + initial_activity : hikari.models.gateway.Activity | None The initial activity to set all shards to when starting the gateway. If this is `None` then no activity will be set, this is the default. initial_status : hikari.models.guilds.PresenceStatus @@ -474,7 +474,7 @@ class BotConfig(RESTConfig, GatewayConfig): Defaults to `ONLINE`. initial_is_afk : bool Whether to show up as AFK or not on sign-in. - initial_idle_since : datetime.datetime, optional + initial_idle_since : datetime.datetime | None The idle time to show on signing in. If set to `None` to not show an idle time, this is the default. intents : hikari.models.intents.Intent @@ -487,17 +487,17 @@ class BotConfig(RESTConfig, GatewayConfig): Can be specified to override the default URL for the Discord OAuth2 API. Generally there is no reason to need to specify this, but it can be useful for testing, amongst other things. - proxy_headers : typing.Mapping[str, str], optional + proxy_headers : typing.Mapping[str, str] | None Optional proxy headers to provide in any HTTP requests. Defaults to `None` if unspecified during deserialization. - proxy_auth : aiohttp.BasicAuth, optional + proxy_auth : aiohttp.BasicAuth | None Optional proxy authorization to provide in any HTTP requests. This is deserialized using the format `"basic {{base 64 string here}}"`. Defaults to `None` if unspecified during deserialization. - proxy_url : str, optional + proxy_url : str | None The optional URL of the proxy to send requests via. Defaults to `None` if unspecified during deserialization. - request_timeout : float, optional + request_timeout : float | None Optional request timeout to use. If an HTTP request takes longer than this, it will be aborted. If not `None`, the value represents a number of seconds as a floating @@ -511,30 +511,30 @@ class BotConfig(RESTConfig, GatewayConfig): to interpolate the chosen API version to use. rest_version : int The HTTP API version to use. If unspecified, then V7 is used. - shard_count : int, optional + shard_count : int | None The number of shards the entire distributed application should consists of. If you run multiple distributed instances of the bot, you should ensure this value is consistent. This can be set to `None` to enable auto-sharding. This is the default. - shard_id : typing.Sequence[int], optional + shard_id : typing.Sequence[int] | None The shard IDs to produce shard connections for. If being deserialized, this can be several formats shown in `notes`. - ssl_context : ssl.SSLContext, optional + ssl_context : ssl.SSLContext | None The optional SSL context to use. This is deserialized as an object reference in the format `package.module#object.attribute` that is expected to point to the desired value. Defaults to `None` if unspecified during deserialization. - tcp_connector : aiohttp.TCPConnector, optional + tcp_connector : aiohttp.TCPConnector | None This may otherwise be `None` to use the default settings provided by `aiohttp`. This is deserialized as an object reference in the format `package.module#object.attribute` that is expected to point to the desired value. Defaults to `None` if unspecified during deserialization. - token : str, optional + token : str | None The token to use. - token_type : str, optional + token_type : str | None Token authentication scheme, this defaults to `"Bot"` and should be one of `"Bot"` or `"Bearer"`, or `None` if not relevant. verify_ssl : bool diff --git a/hikari/errors.py b/hikari/errors.py index d86a143bfc..b3d16964f7 100644 --- a/hikari/errors.py +++ b/hikari/errors.py @@ -134,9 +134,9 @@ class GatewayServerClosedConnectionError(GatewayError): Parameters ---------- - close_code : typing.Union[hikari.internal.codes.GatewayCloseCode, int], optional + close_code : hikari.internal.codes.GatewayCloseCode | int | None The close code provided by the server, if there was one. - reason : str, optional + reason : str | None A string explaining the issue. """ diff --git a/hikari/gateway/connection.py b/hikari/gateway/connection.py index 6ef597860e..bc538e3715 100644 --- a/hikari/gateway/connection.py +++ b/hikari/gateway/connection.py @@ -94,7 +94,7 @@ class Shard(http_client.HTTPClient): # pylint: disable=too-many-instance-attrib If `True`, then payload compression is enabled on the connection. If `False`, no payloads are compressed. You usually want to keep this enabled. - connector : aiohttp.BaseConnector, optional + connector : aiohttp.BaseConnector | None The `aiohttp.BaseConnector` to use for the HTTP session that gets upgraded to a websocket connection. You can use this to customise connection pooling, etc. @@ -108,12 +108,12 @@ class Shard(http_client.HTTPClient): # pylint: disable=too-many-instance-attrib coroutine function, and must take three arguments only. The first is the reference to this `Shard` The second is the event name. - initial_presence : typing.Dict, optional + initial_presence : typing.Dict | None A raw JSON object as a `typing.Dict` that should be set as the initial presence of the bot user once online. If `None`, then it will be set to the default, which is showing up as online without a custom status message. - intents : hikari.models.intents.Intent, optional + intents : hikari.models.intents.Intent | None Bitfield of intents to use. If you use the V7 API, this is mandatory. This field will determine what events you will receive. json_deserialize : `deserialization function` @@ -125,19 +125,19 @@ class Shard(http_client.HTTPClient): # pylint: disable=too-many-instance-attrib considered to be "large". Large guilds will not have member information sent automatically, and must manually request that member chunks be sent using `ShardConnection.request_guild_members`. - proxy_auth : aiohttp.BasicAuth, optional + proxy_auth : aiohttp.BasicAuth | None Optional `aiohttp.BasicAuth` object that can be provided to allow authenticating with a proxy if you use one. Leave `None` to ignore. - proxy_headers : typing.Mapping[str, str], optional + proxy_headers : typing.Mapping[str, str] | None Optional `typing.Mapping` to provide as headers to allow the connection through a proxy if you use one. Leave `None` to ignore. - proxy_url : str, optional + proxy_url : str | None Optional `str` to use for a proxy server. If `None`, then it is ignored. - session_id : str, optional + session_id : str | None The session ID to use. If specified along with `seq`, then the gateway client will attempt to `RESUME` an existing session rather than re-`IDENTIFY`. Otherwise, it will be ignored. - seq : int, optional + seq : int | None The sequence number to use. If specified along with `session_id`, then the gateway client will attempt to `RESUME` an existing session rather than re-`IDENTIFY`. Otherwise, it will be ignored. @@ -146,10 +146,10 @@ class Shard(http_client.HTTPClient): # pylint: disable=too-many-instance-attrib shard_count : int The number of shards on this gateway. Defaults to `1`, which implies no sharding is taking place. - ssl_context : ssl.SSLContext, optional + ssl_context : ssl.SSLContext | None An optional custom `ssl.SSLContext` to provide to customise how SSL works. - timeout : float, optional + timeout : float | None The optional timeout for all HTTP requests. token : str The mandatory bot token for the bot account to use, minus the "Bot" @@ -523,7 +523,7 @@ async def update_voice_state( ---------- guild_id : str The guild ID to change the voice state within. - channel_id : str, optional + channel_id : str | None The channel ID in the guild to change the voice state within. If this is `None`, then this will behave as a disconnect opcode. self_mute : bool diff --git a/hikari/internal/conversions.py b/hikari/internal/conversions.py index f2239bacfe..a55ff65207 100644 --- a/hikari/internal/conversions.py +++ b/hikari/internal/conversions.py @@ -112,7 +112,7 @@ def put_if_specified( The key to add the value under. value : typing.Any The value to add. - type_after : typing.Callable[[`input type`], `output type`], optional + type_after : typing.Callable[[`input type`], `output type`] | None Type to apply to the value when added. """ if value is not ...: @@ -127,7 +127,7 @@ def image_bytes_to_image_data(img_bytes: typing.Optional[bytes] = None, /) -> ty Parameters ---------- - img_bytes : bytes, optional + img_bytes : bytes | None The image bytes. Raises @@ -137,7 +137,7 @@ def image_bytes_to_image_data(img_bytes: typing.Optional[bytes] = None, /) -> ty Returns ------- - str, optional + str | None The `image_bytes` given encoded into an image data string or `None`. diff --git a/hikari/internal/helpers.py b/hikari/internal/helpers.py index 234e760caf..68cebcc716 100644 --- a/hikari/internal/helpers.py +++ b/hikari/internal/helpers.py @@ -51,11 +51,11 @@ def generate_allowed_mentions( # pylint:disable=line-too-long mentions_everyone : bool Whether `@everyone` and `@here` mentions should be resolved by discord and lead to actual pings. - user_mentions : typing.Union[typing.Collection[typing.Union[hikari.models.users.User, hikari.models.bases.Snowflake, int]], bool] + user_mentions : typing.Collection[hikari.models.users.User | hikari.models.bases.Snowflake | int | str] | bool Either an array of user objects/IDs to allow mentions for, `True` to allow all user mentions or `False` to block all user mentions from resolving. - role_mentions : typing.Union[typing.Collection[typing.Union[hikari.models.guilds.GuildRole, hikari.models.bases.Snowflake, int]], bool] + role_mentions : typing.Collection[hikari.models.guilds.GuildRole | hikari.models.bases.Snowflake | int | str] | bool Either an array of guild role objects/IDs to allow mentions for, `True` to allow all role mentions or `False` to block all role mentions from resolving. diff --git a/hikari/internal/http_client.py b/hikari/internal/http_client.py index a6c717e1b0..4a80b2553d 100644 --- a/hikari/internal/http_client.py +++ b/hikari/internal/http_client.py @@ -47,7 +47,7 @@ class HTTPClient(abc.ABC): # pylint:disable=too-many-instance-attributes ---------- allow_redirects : bool Whether to allow redirects or not. Defaults to `False`. - connector : aiohttp.BaseConnector, optional + connector : aiohttp.BaseConnector | None Optional aiohttp connector info for making an HTTP connection debug : bool Defaults to `False`. If `True`, then a lot of contextual information @@ -57,19 +57,19 @@ class HTTPClient(abc.ABC): # pylint:disable=too-many-instance-attributes A custom JSON deserializer function to use. Defaults to `json.loads`. json_serialize : serialization function A custom JSON serializer function to use. Defaults to `json.dumps`. - proxy_headers : typing.Mapping[str, str], optional + proxy_headers : typing.Mapping[str, str] | None Optional proxy headers to pass to HTTP requests. - proxy_auth : aiohttp.BasicAuth, optional + proxy_auth : aiohttp.BasicAuth | None Optional authorization to be used if using a proxy. - proxy_url : str, optional + proxy_url : str | None Optional proxy URL to use for HTTP requests. - ssl_context : ssl.SSLContext, optional + ssl_context : ssl.SSLContext | None The optional SSL context to be used. verify_ssl : bool Whether or not the client should enforce SSL signed certificate verification. If 1 it will ignore potentially malicious SSL certificates. - timeout : float, optional + timeout : float | None The optional timeout for all HTTP requests. trust_env : bool If `True`, and no proxy info is given, then `HTTP_PROXY` and @@ -266,7 +266,7 @@ async def _perform_request( The URL to hit. headers : typing.Dict[str, str] Headers to use when making the request. - body : typing.Union[aiohttp.FormData, dict, list, None] + body : aiohttp.FormData | dict | list | None The body to send. Currently this will send the content in a form body if you pass an instance of `aiohttp.FormData`, or as a JSON body if you pass a `list` or `dict`. Any other types diff --git a/hikari/internal/marshaller.py b/hikari/internal/marshaller.py index 1600c30c5b..7b02a60d8d 100644 --- a/hikari/internal/marshaller.py +++ b/hikari/internal/marshaller.py @@ -126,29 +126,29 @@ def attrib( Parameters ---------- - deserializer : typing.Callable[[...], typing.Any], optional + deserializer : typing.Callable[[...], typing.Any] | None The deserializer to use to deserialize raw elements. If `None` then this field will never be deserialized from a payload and will have to be attached to the object after generation or passed through to `deserialize` as a kwarg. - raw_name : str, optional + raw_name : str | None The raw name of the element in its raw serialized form. If not provided, then this will use the field's default name later. inherit_kwargs : bool If `True` then any fields passed to deserialize for the entity this attribute is attached to as kwargs will also be passed through to this entity's deserializer as kwargs. Defaults to `False`. - if_none : typing.Union[typing.Callable[[], typing.Any], None] + if_none : typing.Callable[[], typing.Any] | None Either a default factory function called to get the default for when this field is `None` or one of `None`, `False` or `True` to specify that this should default to the given singleton. Will raise an exception when `None` is received for this field later if this isn't specified. - if_undefined : typing.Union[typing.Callable[[], typing.Any], None] + if_undefined : typing.Callable[[], typing.Any] | None Either a default factory function called to get the default for when this field isn't defined or one of `None`, `False` or `True` to specify that this should default to the given singleton. Will raise an exception when this field is undefined later on if this isn't specified. - serializer : typing.Callable[[typing.Any], typing.Any], optional + serializer : typing.Callable[[typing.Any], typing.Any] | None The serializer to use. If not specified, then serializing the entire class that this attribute is in will trigger a `TypeError` later. If `None` then the field will not be serialized. @@ -397,12 +397,12 @@ def serialize(self, obj: typing.Optional[typing.Any]) -> more_typing.NullableJSO Parameters ---------- - obj : typing.Any, optional + obj : typing.Any | None The entity to serialize. Returns ------- - typing.Mapping[str, typing.Any], optional + typing.Mapping[str, typing.Any] | None The serialized raw data item. Raises diff --git a/hikari/internal/ratelimits.py b/hikari/internal/ratelimits.py index 37305da34c..dfacfd8c0f 100644 --- a/hikari/internal/ratelimits.py +++ b/hikari/internal/ratelimits.py @@ -571,7 +571,7 @@ class ExponentialBackOff: ---------- base : float The base to use. Defaults to `2`. - maximum : float, optional + maximum : float | None If not `None`, then this is the max value the backoff can be in a single iteration before an `asyncio.TimeoutError` is raised. Defaults to `64` seconds. diff --git a/hikari/internal/urls.py b/hikari/internal/urls.py index 50e73a5659..c0902476c3 100644 --- a/hikari/internal/urls.py +++ b/hikari/internal/urls.py @@ -57,7 +57,7 @@ def generate_cdn_url(*route_parts: str, format_: str, size: typing.Optional[int] The format to use for the wanted cdn entity, will usually be one of `webp`, `png`, `jpeg`, `jpg` or `gif` (which will be invalid if the target entity doesn't have an animated version available). - size : int, optional + size : int | None The size to specify for the image in the query string if applicable, should be passed through as None to avoid the param being set. Must be any power of two between 16 and 4096. diff --git a/hikari/models/applications.py b/hikari/models/applications.py index 54cd930ac6..a2c0616545 100644 --- a/hikari/models/applications.py +++ b/hikari/models/applications.py @@ -332,7 +332,7 @@ def format_icon_url(self, *, format_: str = "png", size: int = 4096) -> typing.O Returns ------- - str, optional + str | None The string URL. Raises @@ -479,7 +479,7 @@ def format_icon_url(self, *, format_: str = "png", size: int = 4096) -> typing.O Returns ------- - str, optional + str | None The string URL. Raises @@ -510,7 +510,7 @@ def format_cover_image_url(self, *, format_: str = "png", size: int = 4096) -> t Returns ------- - str, optional + str | None The string URL. Raises diff --git a/hikari/models/channels.py b/hikari/models/channels.py index b5f7be078d..0701f03e9e 100644 --- a/hikari/models/channels.py +++ b/hikari/models/channels.py @@ -228,7 +228,7 @@ def format_icon_url(self, *, format_: str = "png", size: int = 4096) -> typing.O Returns ------- - str, optional + str | None The string URL. Raises @@ -478,7 +478,7 @@ def with_rate_limit_per_user( Parameters ---------- - rate_limit_per_user : typing.Union[datetime.timedelta, int] + rate_limit_per_user : datetime.timedelta | int The amount of seconds users will have to wait before sending another message in the channel to set. """ @@ -494,7 +494,7 @@ def with_parent_category(self, category: typing.Union[bases.Snowflake, int]) -> Parameters ---------- - category : typing.Union[hikari.models.bases.Snowflake, int] + category : hikari.models.bases.Snowflake | int The placeholder ID of the category channel that should be this channel's parent. """ @@ -506,7 +506,7 @@ def with_id(self, channel_id: typing.Union[bases.Snowflake, int]) -> GuildChanne Parameters ---------- - channel_id : typing.Union[hikari.models.bases.Snowflake, int] + channel_id : hikari.models.bases.Snowflake | int The placeholder ID to use. !!! note diff --git a/hikari/models/embeds.py b/hikari/models/embeds.py index 64b387e7b1..b393c35550 100644 --- a/hikari/models/embeds.py +++ b/hikari/models/embeds.py @@ -392,7 +392,7 @@ def set_footer(self, *, text: str, icon: typing.Optional[str, files.BaseStream] ---------- text: str The optional text to set for the footer. - icon: typing.Union[str, hikari.models.files.BaseStream], optional + icon: hikari.models.files.BaseStream | str | None The optional `hikari.models.files.BaseStream` or URL to the image to set. Returns @@ -420,7 +420,7 @@ def set_image(self, image: typing.Optional[str, files.BaseStream] = None) -> Emb Parameters ---------- - image: typing.Union[str, hikari.models.files.BaseStream], optional + image: hikari.models.files.BaseStream | str | None The optional `hikari.models.files.BaseStream` or URL to the image to set. Returns @@ -438,7 +438,7 @@ def set_thumbnail(self, image: typing.Optional[str, files.BaseStream] = None) -> Parameters ---------- - image: typing.Union[str, hikari.models.files.BaseStream], optional + image: hikari.models.files.BaseStream | str | None The optional `hikari.models.files.BaseStream` or URL to the image to set. Returns @@ -462,11 +462,11 @@ def set_author( Parameters ---------- - name: str, optional + name: str | None The optional authors name. - url: str, optional + url: str | None The optional URL to make the author text link to. - icon: typing.Union[str, hikari.models.files.BaseStream], optional + icon: hikari.models.files.BaseStream | str | None The optional `hikari.models.files.BaseStream` or URL to the icon to set. Returns @@ -500,7 +500,7 @@ def add_field(self, *, name: str, value: str, inline: bool = False, index: typin The fields value. inline: bool Whether to set the field to behave as if it were inline or not. Defaults to `False`. - index: int, optional + index: int | None The optional index to insert the field at. If `None`, it will append to the end. Returns diff --git a/hikari/models/files.py b/hikari/models/files.py index 9d7fb5dfa5..66a5d53a4d 100644 --- a/hikari/models/files.py +++ b/hikari/models/files.py @@ -395,13 +395,13 @@ class FileStream(BaseStream): Parameters ---------- - filename : str, optional + filename : str | None The custom file name to give the file when uploading it. May be omitted. - path : str OR os.PathLike + path : os.PathLike | str The path-like object that describes the file to upload. - executor : concurrent.futures.Executor, optional + executor : concurrent.futures.Executor | None An optional executor to run the IO operations in. If not specified, the default executor for this loop will be used instead. diff --git a/hikari/models/guilds.py b/hikari/models/guilds.py index 0313d3aa29..a94a55f8e0 100644 --- a/hikari/models/guilds.py +++ b/hikari/models/guilds.py @@ -654,7 +654,7 @@ def format_avatar_url( Returns ------- - typing.Union[str, hikari.models.unset.UNSET] + hikari.models.unset.UNSET | str The string URL of the user's custom avatar if either `PresenceUser.avatar_hash` is set or their default avatar if `PresenceUser.discriminator` is set, else `hikari.models.unset.UNSET`. @@ -925,7 +925,7 @@ def format_icon_url(self, *, format_: typing.Optional[str] = None, size: int = 4 Returns ------- - str, optional + str | None The string URL. Raises @@ -994,7 +994,7 @@ def format_splash_url(self, *, format_: str = "png", size: int = 4096) -> typing Returns ------- - str, optional + str | None The string URL. Raises @@ -1025,7 +1025,7 @@ def format_discovery_splash_url(self, *, format_: str = "png", size: int = 4096) Returns ------- - str, optional + str | None The string URL. Raises @@ -1447,7 +1447,7 @@ def format_splash_url(self, *, format_: str = "png", size: int = 4096) -> typing Returns ------- - str, optional + str | None The string URL. Raises @@ -1478,7 +1478,7 @@ def format_discovery_splash_url(self, *, format_: str = "png", size: int = 4096) Returns ------- - str, optional + str | None The string URL. Raises @@ -1511,7 +1511,7 @@ def format_banner_url(self, *, format_: str = "png", size: int = 4096) -> typing Returns ------- - str, optional + str | None The string URL. Raises diff --git a/hikari/models/invites.py b/hikari/models/invites.py index 8f57d17941..19a826ee3c 100644 --- a/hikari/models/invites.py +++ b/hikari/models/invites.py @@ -112,7 +112,7 @@ def format_splash_url(self, *, format_: str = "png", size: int = 4096) -> typing Returns ------- - str, optional + str | None The string URL. Raises @@ -143,7 +143,7 @@ def format_banner_url(self, *, format_: str = "png", size: int = 4096) -> typing Returns ------- - str, optional + str | None The string URL. Raises diff --git a/hikari/models/messages.py b/hikari/models/messages.py index ed2ebee57e..0a23e34f95 100644 --- a/hikari/models/messages.py +++ b/hikari/models/messages.py @@ -438,11 +438,11 @@ async def edit( # pylint:disable=line-too-long mentions_everyone : bool Whether `@everyone` and `@here` mentions should be resolved by discord and lead to actual pings, defaults to `True`. - user_mentions : typing.Union[typing.Collection[typing.Union[hikari.models.users.User, hikari.models.bases.Snowflake, int]], bool] + user_mentions : typing.Collection[hikari.models.users.User | hikari.models.bases.Snowflake | int | str] | bool Either an array of user objects/IDs to allow mentions for, `True` to allow all user mentions or `False` to block all user mentions from resolving, defaults to `True`. - role_mentions: typing.Union[typing.Collection[typing.Union[hikari.models.guilds.GuildRole, hikari.models.bases.Snowflake, int]], bool] + role_mentions: typing.Collection[hikari.models.guilds.GuildRole | hikari.models.bases.Snowflake | int | str] | bool Either an array of guild role objects/IDs to allow mentions for, `True` to allow all role mentions or `False` to block all role mentions from resolving, defaults to `True`. @@ -547,11 +547,11 @@ async def reply( # pylint:disable=line-too-long mentions_everyone : bool Whether `@everyone` and `@here` mentions should be resolved by discord and lead to actual pings, defaults to `True`. - user_mentions : typing.Union[typing.Collection[typing.Union[hikari.models.users.User, hikari.models.bases.Snowflake, int]], bool] + user_mentions : typing.Collection[hikari.models.users.User | hikari.models.bases.Snowflake | int | str] | bool Either an array of user objects/IDs to allow mentions for, `True` to allow all user mentions or `False` to block all user mentions from resolving, defaults to `True`. - role_mentions: typing.Union[typing.Collection[typing.Union[hikari.models.guilds.GuildRole, hikari.models.bases.Snowflake, int]], bool] + role_mentions: typing.Collection[hikari.models.guilds.GuildRole | hikari.models.bases.Snowflake | int | str] | bool Either an array of guild role objects/IDs to allow mentions for, `True` to allow all role mentions or `False` to block all role mentions from resolving, defaults to `True`. @@ -644,7 +644,7 @@ async def add_reaction(self, emoji: typing.Union[str, emojis_.Emoji]) -> None: Parameters ---------- - emoji : str OR hikari.models.emojis.Emoji + emoji : hikari.models.emojis.Emoji | str The emoji to add. Examples @@ -684,9 +684,9 @@ async def remove_reaction( Parameters ---------- - emoji : str OR hikari.models.emojis.Emoji + emoji : hikari.models.emojis.Emoji | str The emoji to remove. - user : hikari.models.users.User, optional + user : hikari.models.users.User | None The user of the reaction to remove. If `None`, then the bot's reaction is removed instead. @@ -726,7 +726,7 @@ async def remove_all_reactions(self, emoji: typing.Optional[typing.Union[str, em Parameters ---------- - emoji : str OR hikari.models.emojis.Emoji, optional + emoji : hikari.models.emojis.Emoji | str | None The emoji to remove all reactions for. If not specified, or `None`, then all emojis are removed. diff --git a/hikari/models/webhooks.py b/hikari/models/webhooks.py index ff312d5a85..688ab7e637 100644 --- a/hikari/models/webhooks.py +++ b/hikari/models/webhooks.py @@ -151,18 +151,18 @@ async def execute( mentions_everyone : bool Whether `@everyone` and `@here` mentions should be resolved by discord and lead to actual pings, defaults to `True`. - user_mentions : typing.Union[typing.Collection[typing.Union[hikari.models.users.User, hikari.models.bases.Snowflake, int]], bool] + user_mentions : typing.Collection[hikari.models.users.User | hikari.models.bases.Snowflake | int | str] | bool Either an array of user objects/IDs to allow mentions for, `True` to allow all user mentions or `False` to block all user mentions from resolving, defaults to `True`. - role_mentions : typing.Union[typing.Collection[typing.Union[hikari.models.guilds.GuildRole, hikari.models.bases.Snowflake, int]], bool] + role_mentions: typing.Collection[hikari.models.guilds.GuildRole | hikari.models.bases.Snowflake | int | str] | bool Either an array of guild role objects/IDs to allow mentions for, `True` to allow all role mentions or `False` to block all role mentions from resolving, defaults to `True`. Returns ------- - hikari.models.messages.Message, optional + hikari.models.messages.Message | None The created message object, if `wait` is `True`, else `None`. Raises @@ -248,7 +248,7 @@ async def delete(self, *, use_token: typing.Optional[bool] = None,) -> None: Parameters ---------- - use_token : bool, optional + use_token : bool | None If set to `True` then the webhook's token will be used for this request; if set to `False` then bot authorization will be used; if not specified then the webhook's token will be used for the @@ -287,17 +287,17 @@ async def edit( ---------- name : str If specified, the new name string. - avatar : hikari.models.files.BaseStream, optional + avatar : hikari.models.files.BaseStream | None If specified, the new avatar image. If `None`, then it is removed. - channel : typing.Union[hikari.models.channels.GuildChannel, hikari.models.bases.Snowflake, int] + channel : hikari.models.channels.GuildChannel | hikari.models.bases.Snowflake | int If specified, the object or ID of the new channel the given webhook should be moved to. reason : str If specified, the audit log reason explaining why the operation was performed. This field will be used when using the webhook's token rather than bot authorization. - use_token : bool, optional + use_token : bool | None If set to `True` then the webhook's token will be used for this request; if set to `False` then bot authorization will be used; if not specified then the webhook's token will be used for the @@ -372,6 +372,36 @@ async def fetch_guild(self) -> guilds_.Guild: return await self._app.rest.fetch_guild(guild=self.guild_id) async def fetch_self(self, *, use_token: typing.Optional[bool] = None) -> Webhook: + """Fetch this webhook. + + Parameters + ---------- + use_token : bool | None + If set to `True` then the webhook's token will be used for this + request; if set to `False` then bot authorization will be used; + if not specified then the webhook's token will be used for the + request if it's set else bot authorization. + + Returns + ------- + hikari.models.webhooks.Webhook + The requested webhook object. + + Raises + ------ + hikari.errors.BadRequest + If any invalid snowflake IDs are passed; a snowflake may be invalid + due to it being outside of the range of a 64 bit integer. + hikari.errors.NotFound + If the webhook is not found. + hikari.errors.Forbidden + If you're not in the guild that owns this webhook or + lack the `MANAGE_WEBHOOKS` permission. + hikari.errors.Unauthorized + If you pass a token that's invalid for the target webhook. + ValueError + If `use_token` is passed as `True` when `Webhook.token` is `None`. + """ if use_token and not self.token: raise ValueError("This webhook's token is unknown.") diff --git a/hikari/rest/buckets.py b/hikari/rest/buckets.py index 50d5258150..737b7df46e 100644 --- a/hikari/rest/buckets.py +++ b/hikari/rest/buckets.py @@ -16,6 +16,8 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . +"""Rate-limit implementations for the RESTSession clients.""" + from __future__ import annotations import asyncio diff --git a/hikari/rest/channel.py b/hikari/rest/channel.py index 35ab323125..a7436679b5 100644 --- a/hikari/rest/channel.py +++ b/hikari/rest/channel.py @@ -88,7 +88,7 @@ async def fetch_channel( Parameters ---------- - channel : typing.Union[hikari.models.channels.PartialChannel, hikari.models.bases.Snowflake, int] + channel : hikari.models.channels.PartialChannel | hikari.models.bases.Snowflake | int | str The object ID of the channel to look up. Returns @@ -130,7 +130,7 @@ async def update_channel( Parameters ---------- - channel : typing.Union[hikari.models.channels.PartialChannel, hikari.models.bases.Snowflake, int] + channel : hikari.models.channels.PartialChannel | hikari.models.bases.Snowflake | int | str The channel ID to update. name : str If specified, the new name for the channel. This must be @@ -145,7 +145,7 @@ async def update_channel( Mark the channel as being not safe for work (NSFW) if `True`. If `False` or unspecified, then the channel is not marked as NSFW. Will have no visible effect for non-text guild channels. - rate_limit_per_user : typing.Union[int, datetime.timedelta] + rate_limit_per_user : datetime.timedelta | int If specified, the time delta of seconds the user has to wait before sending another message. This will not apply to bots, or to members with `MANAGE_MESSAGES` or `MANAGE_CHANNEL` permissions. @@ -162,7 +162,7 @@ async def update_channel( permission_overwrites : typing.Sequence[hikari.models.channels.PermissionOverwrite] If specified, the new list of permission overwrites that are category specific to replace the existing overwrites with. - parent_category : typing.Union[hikari.models.channels.PartialChannel, hikari.models.bases.Snowflake, int], optional + parent_category : hikari.models.channels.GuildCategory | hikari.models.bases.Snowflake | int | None If specified, the new parent category ID to set for the channel, pass `None` to unset. reason : str @@ -216,7 +216,7 @@ async def delete_channel(self, channel: typing.Union[bases.Snowflake, int, str, Parameters ---------- - channel : typing.Union[hikari.models.channels.PartialChannel, hikari.models.bases.Snowflake str] + channel : hikari.models.channels.PartialChannel | hikari.models.bases.Snowflake | int | str The object or ID of the channel to delete. Returns @@ -287,26 +287,26 @@ def fetch_messages( Parameters ---------- - channel : hikari.models.channels.PartialChannel OR hikari.models.bases.Snowflake OR int OR str + channel : hikari.models.channels.PartialChannel | hikari.models.bases.Snowflake | int | str The channel to fetch messages from. Keyword Arguments ----------------- - before : datetime.datetime OR int OR str OR hikari.models.bases.Unique OR hikari.models.bases.Snowflake + before : datetime.datetime | hikari.models.bases.Unique | hikari.models.bases.Snowflake | int | str If a unique object (like a message), then message created before this object will be returned. If a datetime, then messages before that datetime will be returned. If unspecified or None, the filter is not used. - after : datetime.datetime OR int OR str OR hikari.models.bases.Unique OR hikari.models.bases.Snowflake + after : datetime.datetime | hikari.models.bases.Unique | hikari.models.bases.Snowflake | int | str If a unique object (like a message), then message created after this object will be returned. If a datetime, then messages after that datetime will be returned. If unspecified or None, the filter is not used. - around : datetime.datetime OR int OR str OR hikari.models.bases.Unique OR hikari.models.bases.Snowflake + around : datetime.datetime | hikari.models.bases.Unique | hikari.models.bases.Snowflake | int | str If a unique object (like a message), then message created around the same time as this object will be returned. If a datetime, then messages around that datetime will be returned. If unspecified or - None, the filter is not used. + `None`, the filter is not used. !!! info Using `before` or no filter will return messages in the order @@ -407,9 +407,9 @@ async def fetch_message( Parameters ---------- - channel : typing.Union[hikari.models.channels.PartialChannel, hikari.models.bases.Snowflake, int] + channel : hikari.models.channels.PartialChannel | hikari.models.bases.Snowflake | int | str The object or ID of the channel to get the message from. - message : typing.Union[hikari.models.messages.Message, hikari.models.bases.Snowflake, int] + message : hikari.models.messages.Message | hikari.models.bases.Snowflake | int | str The object or ID of the message to retrieve. Returns @@ -457,7 +457,7 @@ async def create_message( # pylint: disable=line-too-long Parameters ---------- - channel : typing.Union[hikari.models.channels.PartialChannel, hikari.models.bases.Snowflake, int] + channel : hikari.models.channels.PartialChannel | hikari.models.bases.Snowflake | int | str The channel or ID of the channel to send to. content : str If specified, the message content to send with the message. @@ -476,11 +476,11 @@ async def create_message( # pylint: disable=line-too-long mentions_everyone : bool Whether `@everyone` and `@here` mentions should be resolved by discord and lead to actual pings, defaults to `True`. - user_mentions : typing.Union[typing.Collection[typing.Union[hikari.models.users.User, hikari.models.bases.Snowflake, int]], bool] + user_mentions : typing.Collection[hikari.models.users.User | hikari.models.bases.Snowflake | int | str] | bool Either an array of user objects/IDs to allow mentions for, `True` to allow all user mentions or `False` to block all user mentions from resolving, defaults to `True`. - role_mentions: typing.Union[typing.Collection[typing.Union[hikari.models.guilds.GuildRole, hikari.models.bases.Snowflake, int]], bool] + role_mentions : typing.Collection[hikari.models.guilds.GuildRole | hikari.models.bases.Snowflake | int | str] | bool Either an array of guild role objects/IDs to allow mentions for, `True` to allow all role mentions or `False` to block all role mentions from resolving, defaults to `True`. @@ -566,8 +566,8 @@ def safe_create_message( async def update_message( # pylint: disable=line-too-long self, - message: typing.Union[bases.Snowflake, int, str, messages_.Message], channel: typing.Union[bases.Snowflake, int, str, channels_.PartialChannel], + message: typing.Union[bases.Snowflake, int, str, messages_.Message], *, content: typing.Optional[str] = ..., embed: typing.Optional[embeds_.Embed] = ..., @@ -584,14 +584,14 @@ async def update_message( # pylint: disable=line-too-long Parameters ---------- - channel : typing.Union[hikari.models.channels.PartialChannel, hikari.models.bases.Snowflake, int] + channel : hikari.models.channels.PartialChannel | hikari.models.bases.Snowflake | int | str The object or ID of the channel to get the message from. - message : typing.Union[hikari.models.messages.Message, hikari.models.bases.Snowflake, int] + message : hikari.models.messages.Message | hikari.models.bases.Snowflake | int | str The object or ID of the message to edit. - content : str, optional + content : str | None If specified, the string content to replace with in the message. If `None`, then the content will be removed from the message. - embed : hikari.models.embeds.Embed, optional + embed : hikari.models.embeds.Embed | None If specified, then the embed to replace with in the message. If `None`, then the embed will be removed from the message. flags : hikari.models.messages.MessageFlag @@ -601,11 +601,11 @@ async def update_message( # pylint: disable=line-too-long mentions_everyone : bool Whether `@everyone` and `@here` mentions should be resolved by discord and lead to actual pings, defaults to `True`. - user_mentions: typing.Union[typing.Collection[typing.Union[hikari.models.users.User, hikari.models.bases.Snowflake, int]], bool] + user_mentions : typing.Collection[hikari.models.users.User | hikari.models.bases.Snowflake | int | str] | bool Either an array of user objects/IDs to allow mentions for, `True` to allow all user mentions or `False` to block all user mentions from resolving, defaults to `True`. - role_mentions: typing.Union[typing.Collection[typing.Union[hikari.models.guilds.GuildRole, hikari.models.bases.Snowflake, int]], bool] + role_mentions : typing.Collection[hikari.models.guilds.GuildRole | hikari.models.bases.Snowflake | int | str] | bool Either an array of guild role objects/IDs to allow mentions for, `True` to allow all role mentions or `False` to block all role mentions from resolving, defaults to `True`. @@ -691,11 +691,11 @@ async def delete_messages( Parameters ---------- - channel : typing.Union[hikari.models.channels.PartialChannel, hikari.models.bases.Snowflake, int] + channel : hikari.models.channels.PartialChannel | hikari.models.bases.Snowflake | int | str The object or ID of the channel to get the message from. - message : typing.Union[hikari.models.messages.Message, hikari.models.bases.Snowflake, int] + message : message : hikari.models.messages.Message | hikari.models.bases.Snowflake | int | str The object or ID of the message to delete. - *additional_messages : typing.Union[hikari.models.messages.Message, hikari.models.bases.Snowflake, int] + *additional_messages : message : hikari.models.messages.Message | hikari.models.bases.Snowflake | int | str Objects and/or IDs of additional messages to delete in the same channel, in total you can delete up to 100 messages in a request. @@ -744,7 +744,7 @@ async def delete_messages( async def update_channel_overwrite( # pylint: disable=line-too-long self, - channel: typing.Union[bases.Snowflake, int, str, messages_.Message], + channel: typing.Union[bases.Snowflake, int, str, channels_.PartialChannel], overwrite: typing.Union[channels_.PermissionOverwrite, users.User, guilds.GuildRole, bases.Snowflake, int], target_type: typing.Union[channels_.PermissionOverwriteType, str], *, @@ -756,18 +756,18 @@ async def update_channel_overwrite( # pylint: disable=line-too-long Parameters ---------- - channel : typing.Union[hikari.models.messages.Message, hikari.models.bases.Snowflake, int] + channel : hikari.models.channels.PartialChannel | hikari.models.bases.Snowflake | int | str The object or ID of the channel to edit permissions for. - overwrite : typing.Union[hikari.models.channels.PermissionOverwrite, hikari.models.guilds.GuildRole, hikari.models.users.User, hikari.models.bases.Snowflake , int] - The object or ID of the target member or role to edit/create the + overwrite : hikari.models.channels.PermissionOverwrite | hikari.models.guilds.GuildRole | hikari.models.users.User | hikari.models.bases.Snowflake | int + The object or ID of the target member or role to create/edit the overwrite for. - target_type : typing.Union[hikari.models.channels.PermissionOverwriteType, int] + target_type : hikari.models.channels.PermissionOverwriteType | int The type of overwrite, passing a raw string that's outside of the enum's range for this may lead to unexpected behaviour. - allow : typing.Union[hikari.models.permissions.Permission, int] + allow : hikari.permissions.Permission | int If specified, the value of all permissions to set to be allowed, passing a raw integer for this may lead to unexpected behaviour. - deny : typing.Union[hikari.models.permissions.Permission, int] + deny : hikari.permissions.Permission | int If specified, the value of all permissions to set to be denied, passing a raw integer for this may lead to unexpected behaviour. reason : str @@ -801,7 +801,7 @@ async def fetch_invites_for_channel( Parameters ---------- - channel : typing.Union[hikari.models.channels.PartialChannel, hikari.models.bases.Snowflake, int] + channel : hikari.models.channels.PartialChannel | hikari.models.bases.Snowflake | int | str The object or ID of the channel to get invites for. Returns @@ -840,9 +840,9 @@ async def create_invite_for_channel( Parameters ---------- - channel : typing.Union[datetime.timedelta, str] + channel : hikari.models.channels.PartialChannel | hikari.models.bases.Snowflake | int | str The object or ID of the channel to create the invite for. - max_age : int + max_age : datetime.timedelta | int If specified, the seconds time delta for the max age of the invite, defaults to `86400` seconds (`24` hours). Set to `0` seconds to never expire. @@ -854,10 +854,10 @@ async def create_invite_for_channel( user is kicked when their session ends unless they are given a role. unique : bool If specified, whether to try to reuse a similar invite. - target_user : typing.Union[hikari.models.users.User, hikari.models.bases.Snowflake, int] + target_user : hikari.models.users.User | hikari.models.bases.Snowflake | int | str If specified, the object or ID of the user this invite should target. - target_user_type : typing.Union[hikari.models.invites.TargetUserType, int] + target_user_type : hikari.invites.TargetUserType | int If specified, the type of target for this invite, passing a raw integer for this may lead to unexpected results. reason : str @@ -905,9 +905,9 @@ async def delete_channel_overwrite( # pylint: disable=line-too-long Parameters ---------- - channel : typing.Union[hikari.models.channels.PartialChannel, hikari.models.bases.Snowflake, int] + channel : hikari.models.channels.PartialChannel | hikari.models.bases.Snowflake | int | str The object or ID of the channel to delete the overwrite from. - overwrite : typing.Union[hikari.models.channels.PermissionOverwrite, hikari.models.guilds.GuildRole, hikari.models.users.User, hikari.models.bases.Snowflake, int] + overwrite : hikari.models.channels.PermissionOverwrite | hikari.models.guilds.GuildRole | hikari.models.users.User| hikari.models.bases.Snowflake | int The ID of the entity this overwrite targets. Raises @@ -931,7 +931,7 @@ async def trigger_typing(self, channel: typing.Union[bases.Snowflake, int, str, Parameters ---------- - channel : typing.Union[hikari.models.channels.PartialChannel, hikari.models.bases.Snowflake, int] + channel : hikari.models.channels.PartialChannel | hikari.models.bases.Snowflake | int | str The object or ID of the channel to appear to be typing in. Raises @@ -955,7 +955,7 @@ async def fetch_pins( Parameters ---------- - channel : typing.Union[hikari.models.channels.PartialChannel, hikari.models.bases.Snowflake, int] + channel : hikari.models.channels.PartialChannel | hikari.models.bases.Snowflake | int | str The object or ID of the channel to get messages from. Returns @@ -994,9 +994,9 @@ async def pin_message( Parameters ---------- - channel : typing.Union[hikari.models.channels.PartialChannel, hikari.models.bases.Snowflake, int] + channel : hikari.models.channels.PartialChannel | hikari.models.bases.Snowflake | int | str The object or ID of the channel to pin a message to. - message : typing.Union[hikari.models.messages.Message, hikari.models.bases.Snowflake, int] + message : hikari.models.channels.PartialChannel | hikari.models.bases.Snowflake | int | str The object or ID of the message to pin. Raises @@ -1025,9 +1025,9 @@ async def unpin_message( Parameters ---------- - channel : typing.Union[hikari.models.channels.PartialChannel, hikari.models.bases.Snowflake, int] + channel : hikari.models.channels.PartialChannel | hikari.models.bases.Snowflake | int | str The ID of the channel to remove a pin from. - message : typing.Union[hikari.models.messages.Message, hikari.models.bases.Snowflake, int] + message : hikari.models.messages.Message | hikari.models.bases.Snowflake | int | str The object or ID of the message to unpin. Raises @@ -1057,7 +1057,7 @@ async def create_webhook( Parameters ---------- - channel : typing.Union[hikari.models.channels.GuildChannel, hikari.models.bases.Snowflake, int] + channel : hikari.models.channels.PartialChannel | hikari.models.bases.Snowflake | int | str The object or ID of the channel for webhook to be created in. name : str The webhook's name string. @@ -1099,7 +1099,7 @@ async def fetch_channel_webhooks( Parameters ---------- - channel : typing.Union[hikari.models.channels.GuildChannel, hikari.models.bases.Snowflake, int] + channel : hikari.models.channels.GuildChannel | hikari.models.bases.Snowflake | int | str The object or ID of the guild channel to get the webhooks from. Returns diff --git a/hikari/rest/guild.py b/hikari/rest/guild.py index a0199c5409..847c46501a 100644 --- a/hikari/rest/guild.py +++ b/hikari/rest/guild.py @@ -85,17 +85,17 @@ async def fetch_audit_log( Parameters ---------- - guild : typing.Union[hikari.models.guilds.Guild, hikari.models.bases.Snowflake, int] + guild : hikari.models.guilds.Guild | hikari.models.bases.Snowflake | int | str The object or ID of the guild to get the audit logs for. - user : typing.Union[hikari.models.users.User, hikari.models.bases.Snowflake, int] + user : hikari.models.users.User | hikari.models.bases.Snowflake | int | str If specified, the object or ID of the user to filter by. - action_type : typing.Union[hikari.models.audit_logs.AuditLogEventType, int] + action_type : hikari.models.audit_logs.AuditLogEventType | int If specified, the action type to look up. Passing a raw integer for this may lead to unexpected behaviour. limit : int If specified, the limit to apply to the number of records. Defaults to `50`. Must be between `1` and `100` inclusive. - before : typing.Union[datetime.datetime, hikari.models.audit_logs.AuditLogEntry, hikari.models.bases.Snowflake, int] + before : datetime.datetime | hikari.models.audit_logs.AuditLogEntry | hikari.models.bases.Snowflake | int | str If specified, the object or ID of the entry that all retrieved entries should have occurred before. @@ -146,18 +146,18 @@ def fetch_audit_log_entries_before( Parameters ---------- - guild : typing.Union[hikari.models.guilds.Guild, hikari.models.bases.Snowflake, int] + guild : hikari.models.guilds.Guild | hikari.models.bases.Snowflake | int | str The ID or object of the guild to get audit log entries for - before : typing.Union[datetime.datetime, hikari.models.audit_logs.AuditLogEntry, hikari.models.bases.Snowflake, int], optional + before : datetime.datetime | hikari.models.audit_logs.AuditLogEntry | hikari.models.bases.Snowflake | int | str | None If specified, the ID or object of the entry or datetime to get entries that happened before otherwise this will start from the newest entry. - user : typing.Union[hikari.models.users.User, hikari.models.bases.Snowflake, int] + user : hikari.models.users.User | hikari.models.bases.Snowflake | int | str If specified, the object or ID of the user to filter by. - action_type : typing.Union[hikari.models.audit_logs.AuditLogEventType, int] + action_type : hikari.models.audit_logs.AuditLogEventType | int If specified, the action type to look up. Passing a raw integer for this may lead to unexpected behaviour. - limit : int, optional + limit : int | None If specified, the limit for how many entries this iterator should return, defaults to unlimited. @@ -205,9 +205,9 @@ async def fetch_guild_emoji( Parameters ---------- - guild : typing.Union[hikari.models.guilds.Guild, hikari.models.bases.Snowflake, int] + guild : hikari.models.guilds.Guild | hikari.models.bases.Snowflake | int | str The object or ID of the guild to get the emoji from. - emoji : typing.Union[hikari.models.emojis.KnownCustomEmoji, hikari.models.bases.Snowflake, int] + emoji : hikari.models.emojis.KnownCustomEmoji | hikari.models.bases.Snowflake | int | str The object or ID of the emoji to get. Returns @@ -238,7 +238,7 @@ async def fetch_guild_emojis( Parameters ---------- - guild : typing.Union[hikari.models.guilds.Guild, hikari.models.bases.Snowflake, int] + guild : hikari.models.guilds.Guild | hikari.models.bases.Snowflake | int | str The object or ID of the guild to get the emojis for. Returns @@ -274,13 +274,13 @@ async def create_guild_emoji( Parameters ---------- - guild : typing.Union[hikari.models.guilds.GuildRole, hikari.models.bases.Snowflake, int] + guild : hikari.models.guilds.Guild | hikari.models.bases.Snowflake | int | str The object or ID of the guild to create the emoji in. name : str The new emoji's name. image : hikari.models.files.BaseStream The `128x128` image data. - roles : typing.Sequence[typing.Union[hikari.models.guilds.GuildRole, hikari.models.bases.Snowflake, int]] + roles : typing.Sequence[hikari.models.guilds.GuildRole | hikari.models.bases.Snowflake | int | str] If specified, a list of role objects or IDs for which the emoji will be whitelisted. If empty, all roles are whitelisted. reason : str @@ -331,14 +331,14 @@ async def update_guild_emoji( Parameters ---------- - guild : typing.Union[hikari.models.guilds.Guild, hikari.models.bases.Snowflake, int] + guild : hikari.models.guilds.Guild | hikari.models.bases.Snowflake | int | str The object or ID of the guild to which the emoji to edit belongs to. - emoji : typing.Union[hikari.models.emojis.KnownCustomEmoji, hikari.models.bases.Snowflake, int] + emoji : hikari.models.emojis.KnownCustomEmoji | hikari.models.bases.Snowflake | int | str The object or ID of the emoji to edit. name : str If specified, a new emoji name string. Keep unspecified to leave the name unchanged. - roles : typing.Sequence[typing.Union[hikari.models.guilds.GuildRole, hikari.models.bases.Snowflake, int]] + roles : typing.Sequence[hikari.models.guilds.GuildRole | hikari.models.bases.Snowflake | int | str] If specified, a list of objects or IDs for the new whitelisted roles. Set to an empty list to whitelist all roles. Keep unspecified to leave the same roles already set. @@ -382,9 +382,9 @@ async def delete_guild_emoji( Parameters ---------- - guild : typing.Union[hikari.models.guilds.Guild, hikari.models.bases.Snowflake, int] + guild : hikari.models.guilds.Guild | hikari.models.bases.Snowflake | int | str The object or ID of the guild to delete the emoji from. - emoji : typing.Union[hikari.models.emojis.KnownCustomEmoji, hikari.models.bases.Snowflake, int] + emoji : hikari.models.emojis.KnownCustomEmoji | hikari.models.bases.Snowflake | int | str The object or ID of the guild emoji to be deleted. Raises @@ -430,13 +430,13 @@ async def create_guild( IDs are available. icon : hikari.models.files.BaseStream If specified, the guild icon image data. - verification_level : typing.Union[hikari.models.guilds.GuildVerificationLevel, int] + verification_level : hikari.models.guilds.GuildVerificationLevel | int If specified, the verification level. Passing a raw int for this may lead to unexpected behaviour. - default_message_notifications : typing.Union[hikari.models.guilds.GuildMessageNotificationsLevel, int] + default_message_notifications : hikari.models.guilds.GuildMessageNotificationsLevel | int If specified, the default notification level. Passing a raw int for this may lead to unexpected behaviour. - explicit_content_filter : typing.Union[hikari.models.guilds.GuildExplicitContentFilterLevel, int] + explicit_content_filter : hikari.models.guilds.GuildExplicitContentFilterLevel | int If specified, the explicit content filter. Passing a raw int for this may lead to unexpected behaviour. roles : typing.Sequence[hikari.models.guilds.GuildRole] @@ -478,7 +478,7 @@ async def fetch_guild(self, guild: typing.Union[bases.Snowflake, int, str, guild Parameters ---------- - guild : typing.Union[hikari.models.guilds.Guild, hikari.models.bases.Snowflake, int] + guild : hikari.models.guilds.Guild | hikari.models.bases.Snowflake | int | str The object or ID of the guild to get. Returns @@ -510,7 +510,7 @@ async def fetch_guild_preview( Parameters ---------- - guild : typing.Union[hikari.models.guilds.Guild, hikari.models.bases.Snowflake, int] + guild : hikari.models.guilds.Guild | hikari.models.bases.Snowflake | int | str The object or ID of the guild to get the preview object for. Returns @@ -556,7 +556,7 @@ async def update_guild( Parameters ---------- - guild : typing.Union[hikari.models.guilds.Guild, hikari.models.bases.Snowflake, int] + guild : hikari.models.guilds.Guild | hikari.models.bases.Snowflake | int | str The object or ID of the guild to be edited. name : str If specified, the new name string for the guild (`2-100` characters). @@ -564,26 +564,26 @@ async def update_guild( If specified, the new voice region ID for guild. You can use `RESTGuildComponent.fetch_guild_voice_regions` to see which region IDs are available. - verification_level : typing.Union[hikari.models.guilds.GuildVerificationLevel, int] + verification_level : hikari.models.guilds.GuildVerificationLevel | int If specified, the new verification level. Passing a raw int for this may lead to unexpected behaviour. - default_message_notifications : typing.Union[hikari.models.guilds.GuildMessageNotificationsLevel, int] + default_message_notifications : hikari.models.guilds.GuildMessageNotificationsLevel | int If specified, the new default notification level. Passing a raw int for this may lead to unexpected behaviour. - explicit_content_filter : typing.Union[hikari.models.guilds.GuildExplicitContentFilterLevel, int] + explicit_content_filter : hikari.models.guilds.GuildExplicitContentFilterLevel | int If specified, the new explicit content filter. Passing a raw int for this may lead to unexpected behaviour. - afk_channel : typing.Union[hikari.models.channels.GuildVoiceChannel, hikari.models.bases.Snowflake, int] + afk_channel : hikari.models.channels.GuildVoiceChannel | hikari.models.bases.Snowflake | int | str If specified, the object or ID for the new AFK voice channel. - afk_timeout : typing.Union[datetime.timedelta, int] + afk_timeout : datetime.timedelta | int If specified, the new AFK timeout seconds timedelta. icon : hikari.models.files.BaseStream If specified, the new guild icon image file. - owner : typing.Union[hikari.models.users.User, hikari.models.bases.Snowflake, int] + owner : hikari.models.users.User | hikari.models.bases.Snowflake | int | str If specified, the object or ID of the new guild owner. splash : hikari.models.files.BaseStream If specified, the new new splash image file. - system_channel : typing.Union[hikari.models.channels.GuildVoiceChannel, hikari.models.bases.Snowflake, int] + system_channel : hikari.models.channels.PartialChannel | hikari.models.bases.Snowflake | int | str If specified, the object or ID of the new system channel. reason : str If specified, the audit log reason explaining why the operation @@ -636,7 +636,7 @@ async def delete_guild(self, guild: typing.Union[bases.Snowflake, int, str, guil Parameters ---------- - guild : typing.Union[hikari.models.guilds.Guild, hikari.models.bases.Snowflake, int] + guild : hikari.models.guilds.Guild | hikari.models.bases.Snowflake | int | str The object or ID of the guild to be deleted. Raises @@ -658,7 +658,7 @@ async def fetch_guild_channels( Parameters ---------- - guild : typing.Union[hikari.models.guilds.Guild, hikari.models.bases.Snowflake, int] + guild :hikari.models.guilds.Guild | hikari.models.bases.Snowflake | int | str The object or ID of the guild to get the channels from. Returns @@ -700,12 +700,12 @@ async def create_guild_channel( # pylint: disable=too-many-arguments Parameters ---------- - guild : typing.Union[hikari.models.guilds.Guild, hikari.models.bases.Snowflake, int] + guild : hikari.models.guilds.Guild | hikari.models.bases.Snowflake | int | str The object or ID of the guild to create the channel in. name : str If specified, the name for the channel. This must be inclusively between `1` and `100` characters in length. - channel_type : typing.Union[hikari.models.channels.ChannelType, int] + channel_type : hikari.models.channels.ChannelType | int If specified, the channel type, passing through a raw integer here may lead to unexpected behaviour. position : int @@ -717,7 +717,7 @@ async def create_guild_channel( # pylint: disable=too-many-arguments nsfw : bool If specified, whether the channel will be marked as NSFW. Only applicable for text channels. - rate_limit_per_user : typing.Union[datetime.timedelta, int] + rate_limit_per_user : datetime.timedelta | int If specified, the second time delta the user has to wait before sending another message. This will not apply to bots, or to members with `MANAGE_MESSAGES` or `MANAGE_CHANNEL` permissions. @@ -734,9 +734,9 @@ async def create_guild_channel( # pylint: disable=too-many-arguments permission_overwrites : typing.Sequence[hikari.models.channels.PermissionOverwrite] If specified, the list of permission overwrite objects that are category specific to replace the existing overwrites with. - parent_category : typing.Union[hikari.models.channels.GuildCategory, hikari.models.bases.Snowflake, int] + parent_category : hikari.models.guilds.GuildCategory | hikari.models.bases.Snowflake | int | str If specified, the object or ID of the parent category to set for - the channel. + the channel. reason : str If specified, the audit log reason explaining why the operation was performed. @@ -795,12 +795,12 @@ async def reposition_guild_channels( Parameters ---------- - guild : typing.Union[hikari.models.guilds.Guild, hikari.models.bases.Snowflake, int] + guild : hikari.models.guilds.Guild | hikari.models.bases.Snowflake | int | str The object or ID of the guild in which to edit the channels. - channel : typing.Tuple[int , typing.Union[hikari.models.channels.GuildChannel, hikari.models.bases.Snowflake, int]] + channel : typing.Tuple[int , hikari.models.channels.GuildChannel | hikari.models.bases.Snowflake | int | str] The first channel to change the position of. This is a tuple of the integer position the channel object or ID. - *additional_channels: typing.Tuple[int, typing.Union[hikari.models.channels.GuildChannel, hikari.models.bases.Snowflake, int]] + *additional_channels: typing.Tuple[int, hikari.models.channels.GuildChannel | hikari.models.bases.Snowflake | int | str] Optional additional channels to change the position of. These must be tuples of integer positions to change to and the channel object or ID and the. @@ -835,9 +835,9 @@ async def fetch_member( Parameters ---------- - guild : typing.Union[hikari.models.guilds.Guild, hikari.models.bases.Snowflake, int] + guild : hikari.models.guilds.Guild | hikari.models.bases.Snowflake | int | str The object or ID of the guild to get the member from. - user : typing.Union[hikari.models.users.User, hikari.models.bases.Snowflake, int] + user : hikari.models.users.User | hikari.models.bases.Snowflake | int | str The object or ID of the member to get. Returns @@ -868,7 +868,7 @@ def fetch_members( Parameters ---------- - guild : typing.Union[hikari.models.guilds.Guild, hikari.models.bases.Snowflake, int] + guild : hikari.models.guilds.Guild | hikari.models.bases.Snowflake | int | str The object or ID of the guild to get the members from. Examples @@ -909,14 +909,14 @@ async def update_member( # pylint: disable=too-many-arguments Parameters ---------- - guild : typing.Union[hikari.models.guilds.Guild, hikari.models.bases.Snowflake, int] + guild : hikari.models.guilds.Guild | hikari.models.bases.Snowflake | int | str The object or ID of the guild to edit the member from. - user : typing.Union[hikari.models.guilds.GuildMember, hikari.models.bases.Snowflake, int] + user : hikari.models.users.User | hikari.models.bases.Snowflake | int | str The object or ID of the member to edit. - nickname : str, optional + nickname : str | None If specified, the new nickname string. Setting it to `None` explicitly will clear the nickname. - roles : typing.Sequence[typing.Union[hikari.models.guilds.GuildRole, hikari.models.bases.Snowflake, int]] + roles : typing.Sequence[hikari.models.guilds.GuildRole | hikari.models.bases.Snowflake | int | str] If specified, a list of role IDs the member should have. mute : bool If specified, whether the user should be muted in the voice channel @@ -924,7 +924,7 @@ async def update_member( # pylint: disable=too-many-arguments deaf : bool If specified, whether the user should be deafen in the voice channel or not. - voice_channel : typing.Union[hikari.models.channels.GuildVoiceChannel, hikari.models.bases.Snowflake, int], optional + voice_channel : hikari.models.channels.GuildVoiceChannel | hikari.models.bases.Snowflake | int | str | None If specified, the ID of the channel to move the member to. Setting it to `None` explicitly will disconnect the user. reason : str @@ -977,9 +977,9 @@ async def update_my_member_nickname( Parameters ---------- - guild : typing.Union[hikari.models.guilds.Guild, hikari.models.bases.Snowflake, int] + guild : hikari.models.guilds.Guild | hikari.models.bases.Snowflake | int | str The object or ID of the guild you want to change the nick on. - nickname : str, optional + nickname : str | None The new nick string. Setting this to `None` clears the nickname. reason : str If specified, the audit log reason explaining why the operation @@ -1014,11 +1014,11 @@ async def add_role_to_member( Parameters ---------- - guild : typing.Union[hikari.models.guilds.Guild, hikari.models.bases.Snowflake, int] + guild : hikari.models.guilds.Guild | hikari.models.bases.Snowflake | int | str The object or ID of the guild the member belongs to. - user : typing.Union[hikari.models.users.User, hikari.models.bases.Snowflake, int] + user : hikari.models.users.User | hikari.models.bases.Snowflake | int | str The object or ID of the member you want to add the role to. - role : typing.Union[hikari.models.guilds.GuildRole, hikari.models.bases.Snowflake, int] + role : hikari.models.guilds.GuildRole | hikari.models.bases.Snowflake | int | str The object or ID of the role you want to add. reason : str If specified, the audit log reason explaining why the operation @@ -1053,11 +1053,11 @@ async def remove_role_from_member( Parameters ---------- - guild : typing.Union[hikari.models.guilds.Guild, hikari.models.bases.Snowflake, int] + guild : hikari.models.guilds.Guild | hikari.models.bases.Snowflake | int | str The object or ID of the guild the member belongs to. - user : typing.Union[hikari.models.users.User, hikari.models.bases.Snowflake, int] + user : hikari.models.users.User | hikari.models.bases.Snowflake | int | str The object or ID of the member you want to remove the role from. - role : typing.Union[hikari.models.guilds.GuildRole, hikari.models.bases.Snowflake, int] + role : hikari.models.guilds.GuildRole | hikari.models.bases.Snowflake | int | str The object or ID of the role you want to remove. reason : str If specified, the audit log reason explaining why the operation @@ -1091,9 +1091,9 @@ async def kick_member( Parameters ---------- - guild : typing.Union[hikari.models.guilds.Guild, hikari.models.bases.Snowflake, int] + guild : hikari.models.guilds.Guild | hikari.models.bases.Snowflake | int | str The object or ID of the guild the member belongs to. - user : typing.Union[hikari.models.users.User, hikari.models.bases.Snowflake, int] + user : hikari.models.users.User | hikari.models.bases.Snowflake | int | str The object or ID of the member you want to kick. reason : str If specified, the audit log reason explaining why the operation @@ -1124,9 +1124,9 @@ async def fetch_ban( Parameters ---------- - guild : typing.Union[hikari.models.guilds.Guild, hikari.models.bases.Snowflake, int] + guild : hikari.models.guilds.Guild | hikari.models.bases.Snowflake | int | str The object or ID of the guild you want to get the ban from. - user : typing.Union[hikari.models.users.User, hikari.models.bases.Snowflake, int] + user : hikari.models.users.User | hikari.models.bases.Snowflake | int | str The object or ID of the user to get the ban information for. Returns @@ -1158,7 +1158,7 @@ async def fetch_bans( Parameters ---------- - guild : typing.Union[hikari.models.guilds.Guild, hikari.models.bases.Snowflake, int] + guild : hikari.models.guilds.Guild | hikari.models.bases.Snowflake | int | str The object or ID of the guild you want to get the bans from. Returns @@ -1193,11 +1193,11 @@ async def ban_member( Parameters ---------- - guild : typing.Union[hikari.models.guilds.Guild, hikari.models.bases.Snowflake, int] + guild : hikari.models.guilds.Guild | hikari.models.bases.Snowflake | int | str The object or ID of the guild the member belongs to. - user : typing.Union[hikari.models.users.User, hikari.models.bases.Snowflake, int] + user : hikari.models.users.User | hikari.models.bases.Snowflake | int | str The object or ID of the member you want to ban. - delete_message_days : typing.Union[datetime.timedelta, int] + delete_message_days : datetime.timedelta | int If specified, the tim delta of how many days of messages from the user should be removed. reason : str @@ -1232,9 +1232,9 @@ async def unban_member( Parameters ---------- - guild : typing.Union[hikari.models.guilds.Guild, hikari.models.bases.Snowflake, int] + guild : hikari.models.guilds.Guild | hikari.models.bases.Snowflake | int | str The object or ID of the guild to un-ban the user from. - user : typing.Union[hikari.models.users.User, hikari.models.bases.Snowflake, int] + user : hikari.models.users.User | hikari.models.bases.Snowflake | int | str The ID of the user you want to un-ban. reason : str If specified, the audit log reason explaining why the operation @@ -1265,7 +1265,7 @@ async def fetch_roles( Parameters ---------- - guild : typing.Union[hikari.models.guilds.Guild, hikari.models.bases.Snowflake, int] + guild : hikari.models.guilds.Guild | hikari.models.bases.Snowflake | int | str The object or ID of the guild you want to get the roles from. Returns @@ -1303,14 +1303,14 @@ async def create_role( Parameters ---------- - guild : typing.Union[hikari.models.guilds.Guild, hikari.models.bases.Snowflake, int] + guild : hikari.models.guilds.Guild | hikari.models.bases.Snowflake | int | str The object or ID of the guild you want to create the role on. name : str If specified, the new role name string. - permissions : typing.Union[hikari.models.permissions.Permission, int] + permissions : hikari.permissions.Permission | int If specified, the permissions integer for the role, passing a raw integer rather than the int flag may lead to unexpected results. - color : typing.Union[hikari.models.colors.Color, int] + color : hikari.colors.Color | int If specified, the color for the role. hoist : bool If specified, whether the role will be hoisted. @@ -1359,12 +1359,12 @@ async def reposition_roles( Parameters ---------- - guild : typing.Union[hikari.models.guilds.Guild, hikari.models.bases.Snowflake, int] + guild : hikari.models.guilds.Guild | hikari.models.bases.Snowflake | int | str The ID of the guild the roles belong to. - role : typing.Tuple[int, typing.Union[hikari.models.guilds.GuildRole, hikari.models.bases.Snowflake, int]] + role : typing.Tuple[int, hikari.models.guilds.GuildRole | hikari.models.bases.Snowflake | int | str] The first role to move. This is a tuple of the integer position and the role object or ID. - *additional_roles : typing.Tuple[int, typing.Union[hikari.models.guilds.GuildRole, hikari.models.bases.Snowflake, int]] + *additional_roles : typing.Tuple[int, thikari.guilds.GuildRole | hikari.models.bases.Snowflake | int | str] Optional extra roles to move. These must be tuples of the integer position and the role object or ID. @@ -1410,16 +1410,16 @@ async def update_role( Parameters ---------- - guild : typing.Union[hikari.models.guilds.Guild, hikari.models.bases.Snowflake, int] + guild : hikari.models.guilds.Guild | hikari.models.bases.Snowflake | int | str The object or ID of the guild the role belong to. - role : typing.Union[hikari.models.guilds.GuildRole, hikari.models.bases.Snowflake, int] + role : hikari.models.guilds.GuildRole | hikari.models.bases.Snowflake | int | str The object or ID of the role you want to edit. name : str If specified, the new role's name string. - permissions : typing.Union[hikari.models.permissions.Permission, int] + permissions : hikari.permissions.Permission | int If specified, the new permissions integer for the role, passing a raw integer for this may lead to unexpected behaviour. - color : typing.Union[hikari.models.colors.Color, int] + color : hikari.colors.Color | int If specified, the new color for the new role passing a raw integer for this may lead to unexpected behaviour. hoist : bool @@ -1468,9 +1468,9 @@ async def delete_role( Parameters ---------- - guild : typing.Union[hikari.models.guilds.Guild, hikari.models.bases.Snowflake, int] + guild : hikari.models.guilds.Guild | hikari.models.bases.Snowflake | int | str The object or ID of the guild you want to remove the role from. - role : typing.Union[hikari.models.guilds.GuildRole, hikari.models.bases.Snowflake, int] + role : hikari.models.guilds.GuildRole | hikari.models.bases.Snowflake | int | str The object or ID of the role you want to delete. Raises @@ -1495,9 +1495,9 @@ async def estimate_guild_prune_count( Parameters ---------- - guild : typing.Union[hikari.models.guilds.Guild, hikari.models.bases.Snowflake, int] + guild : hikari.models.guilds.Guild | hikari.models.bases.Snowflake | int | str The object or ID of the guild you want to get the count for. - days : typing.Union[datetime.timedelta, int] + days : datetime.timedelta | int The time delta of days to count prune for (at least `1`). Returns @@ -1532,9 +1532,9 @@ async def begin_guild_prune( Parameters ---------- - guild : typing.Union[hikari.models.guilds.Guild, hikari.models.bases.Snowflake, int] + guild : hikari.models.guilds.Guild | hikari.models.bases.Snowflake | int | str The object or ID of the guild you want to prune member of. - days : typing.Union[datetime.timedelta, int] + days : datetime.timedelta | int The time delta of inactivity days you want to use as filter. compute_prune_count : bool Whether a count of pruned members is returned or not. @@ -1545,7 +1545,7 @@ async def begin_guild_prune( Returns ------- - int, optional + int | None The number of members who were kicked if `compute_prune_count` is `True`, else `None`. @@ -1575,7 +1575,7 @@ async def fetch_guild_voice_regions( Parameters ---------- - guild : typing.Union[hikari.models.guilds.Guild, hikari.models.bases.Snowflake, int] + guild : hikari.models.guilds.Guild | hikari.models.bases.Snowflake | int | str The object or ID of the guild to get the voice regions for. Returns @@ -1605,7 +1605,7 @@ async def fetch_guild_invites( Parameters ---------- - guild : typing.Union[hikari.models.guilds.Guild, hikari.models.bases.Snowflake, int] + guild : hikari.models.guilds.Guild | hikari.models.bases.Snowflake | int | str The object or ID of the guild to get the invites for. Returns @@ -1635,7 +1635,7 @@ async def fetch_integrations( Parameters ---------- - guild : typing.Union[hikari.models.guilds.Guild, hikari.models.bases.Snowflake, int] + guild : hikari.models.guilds.Guild | hikari.models.bases.Snowflake | int | str The object or ID of the guild to get the integrations for. Returns @@ -1672,15 +1672,15 @@ async def update_integration( Parameters ---------- - guild : typing.Union[hikari.models.guilds.Guild, hikari.models.bases.Snowflake, int] + guild : hikari.models.guilds.Guild | hikari.models.bases.Snowflake | int | str The object or ID of the guild to which the integration belongs to. - integration : typing.Union[hikari.models.guilds.GuildIntegration, hikari.models.bases.Snowflake, int] + integration : hikari.models.guilds.GuildIntegration | hikari.models.bases.Snowflake | int | str The object or ID of the integration to update. - expire_behaviour : typing.Union[hikari.models.guilds.IntegrationExpireBehaviour, int] + expire_behaviour : hikari.models.guilds.IntegrationExpireBehaviour | int If specified, the behaviour for when an integration subscription expires (passing a raw integer for this may lead to unexpected behaviour). - expire_grace_period : typing.Union[datetime.timedelta, int] + expire_grace_period : datetime.timedelta | int If specified, time time delta of how many days the integration will ignore lapsed subscriptions for. enable_emojis : bool @@ -1719,9 +1719,9 @@ async def delete_integration( Parameters ---------- - guild : typing.Union[hikari.models.guilds.Guild, hikari.models.bases.Snowflake, int] + guild : hikari.models.guilds.Guild | hikari.models.bases.Snowflake | int | str The object or ID of the guild to which the integration belongs to. - integration : typing.Union[hikari.models.guilds.GuildIntegration, hikari.models.bases.Snowflake, int] + integration : hikari.models.guilds.GuildIntegration | hikari.models.bases.Snowflake | int | str The object or ID of the integration to delete. reason : str If specified, the audit log reason explaining why the operation @@ -1752,9 +1752,9 @@ async def sync_guild_integration( Parameters ---------- - guild : typing.Union[hikari.models.guilds.Guild, hikari.models.bases.Snowflake, int] + guild : hikari.models.guilds.Guild | hikari.models.bases.Snowflake | int | str The object or ID of the guild to which the integration belongs to. - integration : typing.Union[hikari.models.guilds.GuildIntegration, hikari.models.bases.Snowflake, int] + integration : hikari.models.guilds.GuildIntegration | hikari.models.bases.Snowflake | int | str The ID of the integration to sync. Raises @@ -1779,7 +1779,7 @@ async def fetch_guild_embed( Parameters ---------- - guild : typing.Union[hikari.models.guilds.Guild, hikari.models.bases.Snowflake, int] + guild : hikari.models.guilds.Guild | hikari.models.bases.Snowflake | int | str The object or ID of the guild to get the embed for. Returns @@ -1815,9 +1815,9 @@ async def update_guild_embed( Parameters ---------- - guild : typing.Union[hikari.models.guilds.Guild, hikari.models.bases.Snowflake, int] + guild : hikari.models.guilds.Guild | hikari.models.bases.Snowflake | int | str The object or ID of the guild to edit the embed for. - channel : typing.Union[hikari.models.channels.GuildChannel, hikari.models.bases.Snowflake, int], optional + channel : hikari.models.guilds.GuildChannel | hikari.models.bases.Snowflake | int | str | None If specified, the object or ID of the channel that this embed's invite should target. Set to `None` to disable invites for this embed. @@ -1861,7 +1861,7 @@ async def fetch_guild_vanity_url( Parameters ---------- - guild : typing.Union[hikari.models.guilds.Guild, hikari.models.bases.Snowflake, int] + guild : hikari.models.guilds.Guild | hikari.models.bases.Snowflake | int | str The object or ID of the guild to get the vanity URL for. Returns @@ -1893,7 +1893,7 @@ def format_guild_widget_image( Parameters ---------- - guild : typing.Union[hikari.models.guilds.Guild, hikari.models.bases.Snowflake, int] + guild : hikari.models.guilds.Guild | hikari.models.bases.Snowflake | int | str The object or ID of the guild to form the widget. style : str If specified, the style of the widget. @@ -1923,7 +1923,7 @@ async def fetch_guild_webhooks( Parameters ---------- - guild : typing.Union[hikari.models.guilds.Guild, hikari.models.bases.Snowflake, int] + guild : hikari.models.guilds.Guild | hikari.models.bases.Snowflake | int | str The object or ID for the guild to get the webhooks from. Returns diff --git a/hikari/rest/invite.py b/hikari/rest/invite.py index 22f04b443d..c1207ca685 100644 --- a/hikari/rest/invite.py +++ b/hikari/rest/invite.py @@ -40,7 +40,7 @@ async def fetch_invite( Parameters ---------- - invite : typing.Union[hikari.models.invites.Invite, str] + invite : hikari.models.invites.Invite | str The object or code of the wanted invite. with_counts : bool If specified, whether to attempt to count the number of @@ -67,7 +67,7 @@ async def delete_invite(self, invite: typing.Union[invites.Invite, str]) -> None Parameters ---------- - invite : typing.Union[hikari.models.invites.Invite, str] + invite : hikari.models.invites.Invite | str The object or ID for the invite to be deleted. Returns diff --git a/hikari/rest/me.py b/hikari/rest/me.py index d2731106d5..9391194998 100644 --- a/hikari/rest/me.py +++ b/hikari/rest/me.py @@ -85,7 +85,7 @@ async def update_me(self, *, username: str = ..., avatar: typing.Optional[files. ---------- username : str If specified, the new username string. - avatar : hikari.models.files.BaseStream, optional + avatar : hikari.models.files.BaseStream | None If specified, the new avatar image data. If it is None, the avatar is removed. @@ -140,7 +140,7 @@ def fetch_my_guilds( newest_first : bool If specified and `True`, the guilds are returned in the order of newest to oldest. The default is to return oldest guilds first. - start_at : datetime.datetime OR hikari.models.bases.Unique OR hikari.models.bases.Snowflake or int, optional + start_at : datetime.datetime | hikari.models.bases.Unique | hikari.models.bases.Snowflake | int | None The optional first item to start at, if you want to limit your results. This will be interpreted as the date of creation for a guild. If unspecified, the newest or older possible snowflake is @@ -163,7 +163,7 @@ async def leave_guild(self, guild: typing.Union[bases.Snowflake, int, str, guild Parameters ---------- - guild : typing.Union[hikari.models.guilds.Guild, hikari.models.bases.Snowflake, int] + guild : hikari.models.guilds.Guild | hikari.models.bases.Snowflake | int | str The object or ID of the guild to leave. Raises @@ -183,7 +183,7 @@ async def create_dm_channel( Parameters ---------- - recipient : typing.Union[hikari.models.users.User, hikari.models.bases.Snowflake, int] + recipient : hikari.models.users.User | hikari.models.bases.Snowflake | int The object or ID of the user to create the new DM channel with. Returns diff --git a/hikari/rest/react.py b/hikari/rest/react.py index fac9812a0a..f945f3a827 100644 --- a/hikari/rest/react.py +++ b/hikari/rest/react.py @@ -75,11 +75,11 @@ async def add_reaction( Parameters ---------- - channel : typing.Union[hikari.models.channels.PartialChannel, hikari.models.bases.Snowflake, int] + channel : hikari.models.channels.PartialChannel | hikari.models.bases.Snowflake | int | str The object or ID of the channel to add this reaction in. - message : typing.Union[hikari.models.messages.Message, hikari.models.bases.Snowflake, int] + message : hikari.models.messages.Message | hikari.models.bases.Snowflake | int | str The object or ID of the message to add the reaction in. - emoji : typing.Union[hikari.models.emojis.Emoji, str] + emoji : hikari.models.emojis.Emoji | str The emoji to add. This can either be an emoji object or a string representation of an emoji. The string representation will be either `"name:id"` for custom emojis, or it's unicode character(s) for @@ -119,16 +119,16 @@ async def remove_reaction( Parameters ---------- - channel : typing.Union[hikari.models.channels.PartialChannel, hikari.models.bases.Snowflake, int] + channel : hikari.models.channels.PartialChannel | hikari.models.bases.Snowflake | int | str The object or ID of the channel to add this reaction in. - message : typing.Union[hikari.models.messages.Message, hikari.models.bases.Snowflake, int] + message : hikari.models.messages.Message | hikari.models.bases.Snowflake | int | str The object or ID of the message to add the reaction in. - emoji : typing.Union[hikari.models.emojis.Emoji, str] + emoji : hikari.models.emojis.Emoji | str The emoji to add. This can either be an emoji object or a string representation of an emoji. The string representation will be either `"name:id"` for custom emojis else it's unicode character(s) (can be UTF-32). - user : typing.Union[hikari.models.users.User, hikari.models.bases.Snowflake, int], optional + user : hikari.models.users.User | hikari.models.bases.Snowflake | int | str | None The user to remove the reaction of. If this is `None`, then the bot's own reaction is removed instead. @@ -173,11 +173,11 @@ async def remove_all_reactions( Parameters ---------- - channel : typing.Union[hikari.models.channels.PartialChannel, hikari.models.bases.Snowflake, int] + channel : hikari.models.channels.PartialChannel | hikari.models.bases.Snowflake | int | str The object or ID of the channel to get the message from. - message : typing.Union[hikari.models.messages.Message, hikari.models.bases.Snowflake, int] + message : hikari.models.messages.Message | hikari.models.bases.Snowflake | int | str The object or ID of the message to delete the reactions from. - emoji : typing.Union[hikari.models.emojis.Emoji, str], optional + emoji : hikari.models.emojis.Emoji | str | None The object or string representation of the emoji to delete. The string representation will be either `"name:id"` for custom emojis else it's unicode character(s) (can be UTF-32). @@ -220,22 +220,22 @@ def fetch_reactors( Parameters ---------- - channel : typing.Union[hikari.models.channels.PartialChannel, hikari.models.bases.Snowflake, int] + channel : hikari.models.channels.PartialChannel | hikari.models.bases.Snowflake | int | str The object or ID of the channel to get the message from. - message : typing.Union[hikari.models.messages.Message, hikari.models.bases.Snowflake, int] + message : hikari.models.messages.Message | hikari.models.bases.Snowflake | int | str The object or ID of the message to get the reactions from. - emoji : typing.Union[hikari.models.emojis.Emoji, str] + emoji : hikari.models.emojis.Emoji | str The emoji to get. This can either be it's object or the string representation of the emoji. The string representation will be either `"name:id"` for custom emojis else it's unicode character(s) (can be UTF-32). - after : datetime.datetime OR hikari.models.bases.Unique OR hikari.models.bases.Snowflake OR int OR str, optional + after : datetime.datetime | hikari.models.bases.Unique | hikari.models.bases.Snowflake | int | str | None A limit to the users returned. This allows you to only receive users that were created after the given object, if desired. - If a snowflake/int/str/unique object, then this will use the - corresponding user creation date. If a datetime, the date will - be the limit. If unspecified/None, per the default, then all - valid users will be returned instead. + If a unique object (like a message), then message created after this + object will be returned. If a datetime, then messages after that + datetime will be returned. If unspecified or `None`, the filter is not + used. Examples -------- diff --git a/hikari/rest/session.py b/hikari/rest/session.py index 1200e2f950..bd96b46044 100644 --- a/hikari/rest/session.py +++ b/hikari/rest/session.py @@ -65,7 +65,7 @@ class RESTSession(http_client.HTTPClient): # pylint: disable=too-many-public-me Whether to allow redirects or not. Defaults to `False`. base_url : str The base URL and route for the discord API - connector : aiohttp.BaseConnector, optional + connector : aiohttp.BaseConnector | None Optional aiohttp connector info for making an HTTP connection debug : bool Defaults to `False`. If `True`, then a lot of contextual information @@ -75,21 +75,21 @@ class RESTSession(http_client.HTTPClient): # pylint: disable=too-many-public-me A custom JSON deserializer function to use. Defaults to `json.loads`. json_serialize : serialization function A custom JSON serializer function to use. Defaults to `json.dumps`. - proxy_headers : typing.Mapping[str, str], optional + proxy_headers : typing.Mapping[str, str] | None Optional proxy headers to pass to HTTP requests. - proxy_auth : aiohttp.BasicAuth, optional + proxy_auth : aiohttp.BasicAuth | None Optional authorization to be used if using a proxy. - proxy_url : str, optional + proxy_url : str | None Optional proxy URL to use for HTTP requests. - ssl_context : ssl.SSLContext, optional + ssl_context : ssl.SSLContext | None The optional SSL context to be used. verify_ssl : bool Whether or not the client should enforce SSL signed certificate verification. If 1 it will ignore potentially malicious SSL certificates. - timeout : float, optional + timeout : float | None The optional timeout for all HTTP requests. - token : string, optional + token : string | None The bot token for the client to use. You may start this with a prefix of either `Bot` or `Bearer` to force the token type, or not provide this information if you want to have it auto-detected. @@ -489,9 +489,9 @@ async def modify_channel( # lgtm [py/similar-function] permission_overwrites : typing.Sequence[more_typing.JSONObject] If specified, the new list of permission overwrites that are category specific to replace the existing overwrites with. - parent_id : str, optional - If specified, the new parent category ID to set for the channel., - pass `None` to unset. + parent_id : str | None + If specified, the new parent category ID to set for the channel. + Pass `None` to unset. reason : str If specified, the audit log reason explaining why the operation was performed. @@ -894,10 +894,10 @@ async def edit_message( The ID of the channel to get the message from. message_id : str The ID of the message to edit. - content : str, optional + content : str | None If specified, the string content to replace with in the message. If `None`, the content will be removed from the message. - embed : more_typing.JSONObject, optional + embed : more_typing.JSONObject | None If specified, the embed to replace with in the message. If `None`, the embed will be removed from the message. flags : int @@ -1815,7 +1815,7 @@ async def modify_guild_member( # lgtm [py/similar-function] The ID of the guild to edit the member from. user_id : str The ID of the member to edit. - nick : str, optional + nick : str | None If specified, the new nickname string. Setting it to None explicitly will clear the nickname. roles : typing.Sequence[str] @@ -1862,7 +1862,7 @@ async def modify_current_user_nick(self, guild_id: str, nick: typing.Optional[st ---------- guild_id : str The ID of the guild you want to change the nick on. - nick : str, optional + nick : str | None The new nick string. Setting this to `None` clears the nickname. reason : str If specified, the audit log reason explaining why the operation @@ -2293,7 +2293,7 @@ async def begin_guild_prune( Returns ------- - int, optional + int | None The number of members who were kicked if `compute_prune_count` is True, else None. @@ -2505,7 +2505,7 @@ async def modify_guild_embed( ---------- guild_id : str The ID of the guild to edit the embed for. - channel_id : str, optional + channel_id : str | None If specified, the channel that this embed's invite should target. Set to None to disable invites for this embed. enabled : bool @@ -2674,7 +2674,7 @@ async def modify_current_user( ---------- username : str If specified, the new username string. - avatar : bytes, optional + avatar : bytes | None If specified, the new avatar image in bytes form. If it is None, the avatar is removed. @@ -3047,7 +3047,7 @@ async def execute_webhook( # pylint:disable=too-many-locals Returns ------- - more_typing.JSONObject, optional + more_typing.JSONObject | None The created message object if `wait` is `True`, else `None`. diff --git a/hikari/rest/user.py b/hikari/rest/user.py index 8b7e9686ea..bca7870beb 100644 --- a/hikari/rest/user.py +++ b/hikari/rest/user.py @@ -39,7 +39,7 @@ async def fetch_user(self, user: typing.Union[bases.Snowflake, int, str, users.U Parameters ---------- - user : typing.Union[hikari.models.users.User, hikari.models.bases.Snowflake, int] + user : hikari.models.users.User | hikari.models.bases.Snowflake | int | str The object or ID of the user to get. Returns diff --git a/hikari/rest/webhook.py b/hikari/rest/webhook.py index 3fcfb2bf28..73404e346f 100644 --- a/hikari/rest/webhook.py +++ b/hikari/rest/webhook.py @@ -50,7 +50,7 @@ async def fetch_webhook( Parameters ---------- - webhook : typing.Union[hikari.models.webhooks.Webhook, hikari.models.bases.Snowflake, int] + webhook : hikari.models.webhooks.Webhook | hikari.models.bases.Snowflake | int | str The object or ID of the webhook to get. webhook_token : str If specified, the webhook token to use to get it (bypassing this @@ -94,17 +94,17 @@ async def update_webhook( Parameters ---------- - webhook : typing.Union[hikari.models.webhooks.Webhook, hikari.models.bases.Snowflake, int] + webhook : hikari.models.webhooks.Webhook | hikari.models.bases.Snowflake | int | str The object or ID of the webhook to edit. webhook_token : str If specified, the webhook token to use to modify it (bypassing this session's provided authorization `token`). name : str If specified, the new name string. - avatar : hikari.models.files.BaseStream, optional + avatar : hikari.models.files.BaseStream | None If specified, the new avatar image. If `None`, then it is removed. - channel : typing.Union[hikari.models.channels.GuildChannel, hikari.models.bases.Snowflake, int] + channel : hikari.models.channels.GuildChannel | hikari.models.bases.Snowflake | int | str If specified, the object or ID of the new channel the given webhook should be moved to. reason : str @@ -150,7 +150,7 @@ async def delete_webhook( Parameters ---------- - webhook : typing.Union[hikari.models.webhooks.Webhook, hikari.models.bases.Snowflake, int] + webhook : hikari.models.webhooks.Webhook | hikari.models.bases.Snowflake | int | str The object or ID of the webhook to delete webhook_token : str If specified, the webhook token to use to delete it (bypassing this @@ -198,7 +198,7 @@ async def execute_webhook( # pylint:disable=too-many-locals,line-too-long Parameters ---------- - webhook : typing.Union[hikari.models.webhooks.Webhook, hikari.models.bases.Snowflake, int] + webhook : hikari.models.webhooks.Webhook | hikari.models.bases.Snowflake | int | str The object or ID of the webhook to execute. webhook_token : str The token of the webhook to execute. @@ -223,18 +223,18 @@ async def execute_webhook( # pylint:disable=too-many-locals,line-too-long mentions_everyone : bool Whether `@everyone` and `@here` mentions should be resolved by discord and lead to actual pings, defaults to `True`. - user_mentions : typing.Union[typing.Collection[typing.Union[hikari.models.users.User, hikari.models.bases.Snowflake, int]], bool] + user_mentions : typing.Collection[hikari.models.users.User | hikari.models.bases.Snowflake | int | str] | bool Either an array of user objects/IDs to allow mentions for, `True` to allow all user mentions or `False` to block all user mentions from resolving, defaults to `True`. - role_mentions : typing.Union[typing.Collection[typing.Union[hikari.models.guilds.GuildRole, hikari.models.bases.Snowflake, int]], bool] + role_mentions : typing.Collection[hikari.models.guilds.GuildRole | hikari.models.bases.Snowflake | int | str] | bool Either an array of guild role objects/IDs to allow mentions for, `True` to allow all role mentions or `False` to block all role mentions from resolving, defaults to `True`. Returns ------- - hikari.models.messages.Message, optional + hikari.models.messages.Message | None The created message object, if `wait` is `True`, else `None`. Raises diff --git a/tests/hikari/models/test_bases.py b/tests/hikari/models/test_bases.py index 4ed5a66741..f26ed5598e 100644 --- a/tests/hikari/models/test_bases.py +++ b/tests/hikari/models/test_bases.py @@ -123,7 +123,7 @@ class StubEntity(bases.Unique, marshaller.Deserializable, marshaller.Serializabl ... -class TestUniqueEntity: +class TestUnique: def test_created_at(self): entity = bases.Unique(id=bases.Snowflake("9217346714023428234")) assert entity.created_at == entity.id.created_at From 59eac305aee0d40f7b03fd2de4978eaf8fcc1710 Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Sun, 17 May 2020 16:01:43 +0100 Subject: [PATCH 357/922] Removed bot-base --- .../bot_base.py => application.py} | 27 +++------ hikari/components/__init__.py | 24 -------- hikari/components/application.py | 58 ------------------- hikari/gateway/client.py | 9 +-- hikari/{components => gateway}/consumers.py | 2 +- hikari/{components => gateway}/dispatchers.py | 0 .../{components => gateway}/event_managers.py | 5 +- .../intent_aware_dispatchers.py | 3 +- hikari/{components => gateway}/runnable.py | 0 .../__init__.py => hikari/model_manager.py | 0 hikari/models/audit_logs.py | 2 +- hikari/models/bases.py | 2 +- hikari/rest/base.py | 3 +- hikari/rest/client.py | 2 +- hikari/stateless/bot.py | 8 +-- hikari/stateless/manager.py | 4 +- tests/hikari/components/test_application.py | 47 --------------- tests/hikari/gateway/test_client.py | 22 +++---- .../test_dispatchers.py | 2 +- .../test_intent_aware_dispatchers.py | 2 +- tests/hikari/models/test_applications.py | 2 +- tests/hikari/models/test_audit_logs.py | 2 +- tests/hikari/models/test_bases.py | 2 +- tests/hikari/models/test_channels.py | 2 +- tests/hikari/models/test_embeds.py | 2 +- tests/hikari/models/test_emojis.py | 2 +- tests/hikari/models/test_gateway.py | 2 +- tests/hikari/models/test_guilds.py | 2 +- tests/hikari/models/test_invites.py | 2 +- tests/hikari/models/test_messages.py | 2 +- tests/hikari/models/test_users.py | 2 +- tests/hikari/models/test_voices.py | 2 +- tests/hikari/models/test_webhook.py | 2 +- tests/hikari/rest/test_base.py | 2 +- tests/hikari/rest/test_channel.py | 2 +- tests/hikari/rest/test_client.py | 2 +- tests/hikari/rest/test_gateway.py | 2 +- tests/hikari/rest/test_guild.py | 2 +- tests/hikari/rest/test_invite.py | 2 +- tests/hikari/rest/test_me.py | 2 +- tests/hikari/rest/test_oauth2.py | 2 +- tests/hikari/rest/test_react.py | 2 +- tests/hikari/rest/test_user.py | 2 +- tests/hikari/rest/test_voice.py | 2 +- tests/hikari/rest/test_webhook.py | 2 +- tests/hikari/stateless/test_manager.py | 2 +- 46 files changed, 67 insertions(+), 207 deletions(-) rename hikari/{components/bot_base.py => application.py} (94%) delete mode 100644 hikari/components/__init__.py delete mode 100644 hikari/components/application.py rename hikari/{components => gateway}/consumers.py (97%) rename hikari/{components => gateway}/dispatchers.py (100%) rename hikari/{components => gateway}/event_managers.py (98%) rename hikari/{components => gateway}/intent_aware_dispatchers.py (99%) rename hikari/{components => gateway}/runnable.py (100%) rename tests/hikari/components/__init__.py => hikari/model_manager.py (100%) delete mode 100644 tests/hikari/components/test_application.py rename tests/hikari/{components => gateway}/test_dispatchers.py (99%) rename tests/hikari/{components => gateway}/test_intent_aware_dispatchers.py (99%) diff --git a/hikari/components/bot_base.py b/hikari/application.py similarity index 94% rename from hikari/components/bot_base.py rename to hikari/application.py index 3df9b6220b..771a3626ae 100644 --- a/hikari/components/bot_base.py +++ b/hikari/application.py @@ -20,7 +20,7 @@ from __future__ import annotations -__all__ = ["BotBase"] +__all__ = ["Application"] import abc import asyncio @@ -39,10 +39,9 @@ from hikari.internal import conversions from hikari.internal import helpers -from . import application -from . import dispatchers -from . import event_managers -from . import runnable +from hikari.gateway import dispatchers +from hikari.gateway import event_managers +from hikari.gateway import runnable if typing.TYPE_CHECKING: from hikari.events import base as event_base @@ -54,9 +53,7 @@ from hikari.rest import client as rest_client -class BotBase( - application.Application, runnable.RunnableClient, dispatchers.EventDispatcher, abc.ABC, -): +class Application(runnable.RunnableClient, dispatchers.EventDispatcher, abc.ABC): """An abstract base class for a bot implementation. Parameters @@ -87,16 +84,12 @@ def __init__(self, *, config: typing.Optional[configs.BotConfig] = None, **kwarg runnable.RunnableClient.__init__(self, helpers.get_logger(self)) - # noinspection PyArgumentList - application.Application.__init__( - self, config=None, event_dispatcher=None, event_manager=None, rest=None, shards={}, - ) - self._is_shutting_down = False self.config = configs.BotConfig(**kwargs) if config is None else config self.event_dispatcher = self._create_event_dispatcher(self.config) self.event_manager = self._create_event_manager(self) self.rest = self._create_rest(self) + self.shards = None @property def heartbeat_latency(self) -> float: @@ -305,9 +298,7 @@ async def update_presence( @staticmethod @abc.abstractmethod - def _create_shard( - app: application.Application, shard_id: int, shard_count: int, url: str - ) -> gateway_client.GatewayClient: + def _create_shard(app: Application, shard_id: int, shard_count: int, url: str) -> gateway_client.GatewayClient: """Return a new shard for the given parameters. Parameters @@ -336,7 +327,7 @@ def _create_shard( @staticmethod @abc.abstractmethod - def _create_rest(app: application.Application) -> rest_client.RESTClient: + def _create_rest(app: Application) -> rest_client.RESTClient: """Return a new RESTSession client from the given configuration. Parameters @@ -352,7 +343,7 @@ def _create_rest(app: application.Application) -> rest_client.RESTClient: @staticmethod @abc.abstractmethod - def _create_event_manager(app: application.Application) -> event_managers.EventManager: + def _create_event_manager(app: Application) -> event_managers.EventManager: """Return a new instance of an event manager implementation. Parameters diff --git a/hikari/components/__init__.py b/hikari/components/__init__.py deleted file mode 100644 index 221c299d0b..0000000000 --- a/hikari/components/__init__.py +++ /dev/null @@ -1,24 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# Copyright © Nekokatt 2019-2020 -# -# This file is part of Hikari. -# -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . -"""The models API for interacting with Discord directly.""" - - -from __future__ import annotations - -__all__ = [] diff --git a/hikari/components/application.py b/hikari/components/application.py deleted file mode 100644 index 221ba144a9..0000000000 --- a/hikari/components/application.py +++ /dev/null @@ -1,58 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# Copyright © Nekoka.tt 2019-2020 -# -# This file is part of Hikari. -# -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . -"""A library wide base for storing client application.""" - -from __future__ import annotations - -__all__ = ["Application"] - -import typing - -import attr - -if typing.TYPE_CHECKING: - from hikari import configs - from hikari.rest import client as rest_client - from hikari.gateway import client as gateway_client - - from . import dispatchers - from . import event_managers - - -@attr.s(repr=False) -class Application: - """A base that defines placement for set of application used in the library.""" - - config: configs.BotConfig = attr.attrib(default=None) - """The config used for this bot.""" - - event_dispatcher: dispatchers.EventDispatcher = attr.attrib(default=None) - """The event dispatcher for this bot.""" - - event_manager: event_managers.EventManager = attr.attrib(default=None) - """The event manager for this bot.""" - - rest: rest_client.RESTClient = attr.attrib(default=None) - """The RESTSession HTTP client to use for this bot.""" - - shards: typing.Mapping[int, gateway_client.GatewayClient] = attr.attrib(default=None) - """Shards registered to this bot. - - These will be created once the bot has started execution. - """ diff --git a/hikari/gateway/client.py b/hikari/gateway/client.py index 7839ce3a5e..416f521550 100644 --- a/hikari/gateway/client.py +++ b/hikari/gateway/client.py @@ -35,22 +35,23 @@ import aiohttp -from hikari.components import runnable +from hikari import errors from hikari.internal import codes from hikari.internal import helpers from hikari.internal import ratelimits -from .. import errors from . import connection as gateway_connection from . import gateway_state +from . import runnable + if typing.TYPE_CHECKING: import datetime + from hikari import application from hikari.models import gateway from hikari.models import guilds from hikari.models import intents as intents_ - from hikari.components import application class GatewayClient(runnable.RunnableClient): @@ -75,7 +76,7 @@ class GatewayClient(runnable.RunnableClient): !!! note Generally, you want to use - `hikari.components.bot_base.BotBase` rather than this class + `hikari.clients.bot_base.Application` rather than this class directly, as that will handle sharding where enabled and applicable, and provides a few more bits and pieces that may be useful such as state management and event dispatcher integration. and If you want to customize diff --git a/hikari/components/consumers.py b/hikari/gateway/consumers.py similarity index 97% rename from hikari/components/consumers.py rename to hikari/gateway/consumers.py index 1b15affbd2..bf6b564814 100644 --- a/hikari/components/consumers.py +++ b/hikari/gateway/consumers.py @@ -29,7 +29,7 @@ import typing if typing.TYPE_CHECKING: - from hikari.gateway import client as gateway_client + from . import client as gateway_client class RawEventConsumer(abc.ABC): diff --git a/hikari/components/dispatchers.py b/hikari/gateway/dispatchers.py similarity index 100% rename from hikari/components/dispatchers.py rename to hikari/gateway/dispatchers.py diff --git a/hikari/components/event_managers.py b/hikari/gateway/event_managers.py similarity index 98% rename from hikari/components/event_managers.py rename to hikari/gateway/event_managers.py index 98592318f2..a6e200f89f 100644 --- a/hikari/components/event_managers.py +++ b/hikari/gateway/event_managers.py @@ -30,10 +30,11 @@ from . import dispatchers if typing.TYPE_CHECKING: - from hikari.gateway import client as gateway_client + from hikari import application + from hikari.internal import more_typing - from . import application + from . import client as gateway_client EVENT_MARKER_ATTR: typing.Final[str] = "___event_name___" diff --git a/hikari/components/intent_aware_dispatchers.py b/hikari/gateway/intent_aware_dispatchers.py similarity index 99% rename from hikari/components/intent_aware_dispatchers.py rename to hikari/gateway/intent_aware_dispatchers.py index 1f1932ed87..528f8e530d 100644 --- a/hikari/components/intent_aware_dispatchers.py +++ b/hikari/gateway/intent_aware_dispatchers.py @@ -27,11 +27,12 @@ import typing import warnings +from hikari import errors + from hikari.events import base as event_base from hikari.events import other as other_events from hikari.internal import more_asyncio from hikari.internal import more_collections -from .. import errors from hikari.models import intents from . import dispatchers diff --git a/hikari/components/runnable.py b/hikari/gateway/runnable.py similarity index 100% rename from hikari/components/runnable.py rename to hikari/gateway/runnable.py diff --git a/tests/hikari/components/__init__.py b/hikari/model_manager.py similarity index 100% rename from tests/hikari/components/__init__.py rename to hikari/model_manager.py diff --git a/hikari/models/audit_logs.py b/hikari/models/audit_logs.py index 763558136c..2366ae292b 100644 --- a/hikari/models/audit_logs.py +++ b/hikari/models/audit_logs.py @@ -60,7 +60,7 @@ from . import webhooks as webhooks_ if typing.TYPE_CHECKING: - from hikari.components import application + from hikari import application from hikari.internal import more_typing diff --git a/hikari/models/bases.py b/hikari/models/bases.py index 3c60088f5f..41823514bd 100644 --- a/hikari/models/bases.py +++ b/hikari/models/bases.py @@ -33,7 +33,7 @@ if typing.TYPE_CHECKING: import datetime - from hikari.components import application + from hikari import application @marshaller.marshallable() diff --git a/hikari/rest/base.py b/hikari/rest/base.py index f06aae1401..1040ce93d0 100644 --- a/hikari/rest/base.py +++ b/hikari/rest/base.py @@ -30,8 +30,7 @@ if typing.TYPE_CHECKING: import types - from hikari.components import application - + from hikari import application from . import session as rest_session diff --git a/hikari/rest/client.py b/hikari/rest/client.py index 9cb098e804..7b101a290d 100644 --- a/hikari/rest/client.py +++ b/hikari/rest/client.py @@ -41,7 +41,7 @@ from . import webhook if typing.TYPE_CHECKING: - from hikari.components import application + from hikari import application class RESTClient( diff --git a/hikari/stateless/bot.py b/hikari/stateless/bot.py index f318c4a27b..3bd8638730 100644 --- a/hikari/stateless/bot.py +++ b/hikari/stateless/bot.py @@ -24,19 +24,19 @@ import typing +from hikari import application from hikari import rest -from hikari.components import bot_base -from hikari.components import intent_aware_dispatchers from hikari.gateway import client +from hikari.gateway import intent_aware_dispatchers from . import manager if typing.TYPE_CHECKING: - from hikari.components import application + from hikari import application from hikari import configs -class StatelessBot(bot_base.BotBase): +class StatelessBot(application.Application): """Bot client without any state internals. This is the most basic type of bot you can create. diff --git a/hikari/stateless/manager.py b/hikari/stateless/manager.py index 8f07c82cae..c136f1079f 100644 --- a/hikari/stateless/manager.py +++ b/hikari/stateless/manager.py @@ -22,8 +22,8 @@ __all__ = ["StatelessEventManagerImpl"] -from hikari.components import dispatchers -from hikari.components import event_managers +from hikari.gateway import dispatchers +from hikari.gateway import event_managers from hikari.events import channel from hikari.events import guild from hikari.events import message diff --git a/tests/hikari/components/test_application.py b/tests/hikari/components/test_application.py deleted file mode 100644 index 394390a113..0000000000 --- a/tests/hikari/components/test_application.py +++ /dev/null @@ -1,47 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# Copyright © Nekoka.tt 2019-2020 -# -# This file is part of Hikari. -# -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . -import mock - -from hikari import configs -from hikari import rest -from hikari.gateway import client -from hikari.components import application -from hikari.components import dispatchers -from hikari.components import event_managers - - -class TestComponents: - def test___init__(self): - mock_config = mock.MagicMock(configs.BotConfig) - mock_event_dispatcher = mock.MagicMock(dispatchers.EventDispatcher) - mock_event_manager = mock.MagicMock(event_managers.EventManager) - mock_rest = mock.MagicMock(rest.RESTClient) - mock_shards = mock.MagicMock(client.GatewayClient) - component = application.Application( - config=mock_config, - event_manager=mock_event_manager, - event_dispatcher=mock_event_dispatcher, - rest=mock_rest, - shards=mock_shards, - ) - assert component.config is mock_config - assert component.event_dispatcher is mock_event_dispatcher - assert component.event_manager is mock_event_manager - assert component.rest is mock_rest - assert component.shards is mock_shards diff --git a/tests/hikari/gateway/test_client.py b/tests/hikari/gateway/test_client.py index 3cb4292182..de1de6791b 100644 --- a/tests/hikari/gateway/test_client.py +++ b/tests/hikari/gateway/test_client.py @@ -26,8 +26,8 @@ from hikari import configs from hikari import errors -from hikari.components import application -from hikari.components import consumers +from hikari import application +from hikari.gateway import consumers from hikari.gateway import client as high_level_shards from hikari.gateway import connection as low_level_shards from hikari.gateway import gateway_state @@ -54,17 +54,13 @@ def done(self): @pytest.fixture def mock_app(): - class ApplicationImpl(application.Application): - def __init__(self): - super().__init__( - config=mock.MagicMock(), - event_dispatcher=mock.MagicMock(dispatch_event=mock.MagicMock(return_value=_helpers.AwaitableMock())), - event_manager=mock.MagicMock(), - rest=mock.MagicMock(), - shards=mock.MagicMock(), - ) - - return ApplicationImpl() + app = mock.MagicMock() + app.config = mock.MagicMock() + app.event_dispatcher = mock.MagicMock(dispatch_event=mock.MagicMock(return_value=_helpers.AwaitableMock())) + app.event_manager = mock.MagicMock() + app.rest = mock.MagicMock() + app.shards = mock.MagicMock() + return app @pytest.fixture diff --git a/tests/hikari/components/test_dispatchers.py b/tests/hikari/gateway/test_dispatchers.py similarity index 99% rename from tests/hikari/components/test_dispatchers.py rename to tests/hikari/gateway/test_dispatchers.py index 1b4532dcaf..5869785b15 100644 --- a/tests/hikari/components/test_dispatchers.py +++ b/tests/hikari/gateway/test_dispatchers.py @@ -20,7 +20,7 @@ import pytest from hikari import events -from hikari.components import dispatchers +from hikari.gateway import dispatchers from tests.hikari import _helpers diff --git a/tests/hikari/components/test_intent_aware_dispatchers.py b/tests/hikari/gateway/test_intent_aware_dispatchers.py similarity index 99% rename from tests/hikari/components/test_intent_aware_dispatchers.py rename to tests/hikari/gateway/test_intent_aware_dispatchers.py index 06c24bb453..39487cdccc 100644 --- a/tests/hikari/components/test_intent_aware_dispatchers.py +++ b/tests/hikari/gateway/test_intent_aware_dispatchers.py @@ -24,7 +24,7 @@ import pytest from hikari import events -from hikari.components import intent_aware_dispatchers +from hikari.gateway import intent_aware_dispatchers from tests.hikari import _helpers diff --git a/tests/hikari/models/test_applications.py b/tests/hikari/models/test_applications.py index bf3c15f32f..0df06754e2 100644 --- a/tests/hikari/models/test_applications.py +++ b/tests/hikari/models/test_applications.py @@ -19,7 +19,7 @@ import mock import pytest -from hikari.components import application +from hikari import application from hikari.internal import urls from hikari.models import applications from hikari.models import guilds diff --git a/tests/hikari/models/test_audit_logs.py b/tests/hikari/models/test_audit_logs.py index 27d8e6494f..081c5e2a5e 100644 --- a/tests/hikari/models/test_audit_logs.py +++ b/tests/hikari/models/test_audit_logs.py @@ -22,7 +22,7 @@ import mock import pytest -from hikari.components import application +from hikari import application from hikari.models import audit_logs from hikari.models import channels from hikari.models import guilds diff --git a/tests/hikari/models/test_bases.py b/tests/hikari/models/test_bases.py index f26ed5598e..00e11ce169 100644 --- a/tests/hikari/models/test_bases.py +++ b/tests/hikari/models/test_bases.py @@ -23,7 +23,7 @@ import mock import pytest -from hikari.components import application +from hikari import application from hikari.internal import marshaller from hikari.models import bases diff --git a/tests/hikari/models/test_channels.py b/tests/hikari/models/test_channels.py index 373f54ffad..52394449ec 100644 --- a/tests/hikari/models/test_channels.py +++ b/tests/hikari/models/test_channels.py @@ -25,7 +25,7 @@ from hikari.models import channels from hikari.models import permissions from hikari.models import users -from hikari.components import application +from hikari import application from hikari.internal import urls diff --git a/tests/hikari/models/test_embeds.py b/tests/hikari/models/test_embeds.py index 804dd74f2d..fb57d6c787 100644 --- a/tests/hikari/models/test_embeds.py +++ b/tests/hikari/models/test_embeds.py @@ -21,7 +21,7 @@ import mock import pytest -from hikari.components import application +from hikari import application from hikari.internal import conversions from hikari.models import colors from hikari.models import embeds diff --git a/tests/hikari/models/test_emojis.py b/tests/hikari/models/test_emojis.py index 5cea94c747..6704e133a3 100644 --- a/tests/hikari/models/test_emojis.py +++ b/tests/hikari/models/test_emojis.py @@ -19,7 +19,7 @@ import mock import pytest -from hikari.components import application +from hikari import application from hikari.internal import urls from hikari.models import bases from hikari.models import emojis diff --git a/tests/hikari/models/test_gateway.py b/tests/hikari/models/test_gateway.py index 3157783a93..181f275bd9 100644 --- a/tests/hikari/models/test_gateway.py +++ b/tests/hikari/models/test_gateway.py @@ -21,7 +21,7 @@ import mock import pytest -from hikari.components import application +from hikari import application from hikari.models import gateway from hikari.models import guilds from tests.hikari import _helpers diff --git a/tests/hikari/models/test_guilds.py b/tests/hikari/models/test_guilds.py index 666473a9bb..f37e0f30ab 100644 --- a/tests/hikari/models/test_guilds.py +++ b/tests/hikari/models/test_guilds.py @@ -22,7 +22,7 @@ import mock import pytest -from hikari.components import application +from hikari import application from hikari.internal import conversions from hikari.internal import urls from hikari.models import channels diff --git a/tests/hikari/models/test_invites.py b/tests/hikari/models/test_invites.py index 4a5911cfd5..51b8571a1d 100644 --- a/tests/hikari/models/test_invites.py +++ b/tests/hikari/models/test_invites.py @@ -22,7 +22,7 @@ import mock import pytest -from hikari.components import application +from hikari import application from hikari.internal import conversions from hikari.internal import urls from hikari.models import channels diff --git a/tests/hikari/models/test_messages.py b/tests/hikari/models/test_messages.py index 8e63b1a28b..e4c0504a48 100644 --- a/tests/hikari/models/test_messages.py +++ b/tests/hikari/models/test_messages.py @@ -22,7 +22,7 @@ import mock import pytest -from hikari.components import application +from hikari import application from hikari.internal import conversions from hikari.models import applications from hikari.models import bases diff --git a/tests/hikari/models/test_users.py b/tests/hikari/models/test_users.py index e04b9f77cc..f76d0bea79 100644 --- a/tests/hikari/models/test_users.py +++ b/tests/hikari/models/test_users.py @@ -20,7 +20,7 @@ import pytest from hikari import rest -from hikari.components import application +from hikari import application from hikari.models import bases from hikari.models import users from hikari.internal import urls diff --git a/tests/hikari/models/test_voices.py b/tests/hikari/models/test_voices.py index 5d6a89599c..7752137613 100644 --- a/tests/hikari/models/test_voices.py +++ b/tests/hikari/models/test_voices.py @@ -19,7 +19,7 @@ import mock import pytest -from hikari.components import application +from hikari import application from hikari.models import voices diff --git a/tests/hikari/models/test_webhook.py b/tests/hikari/models/test_webhook.py index 4e70df14f1..1368741ade 100644 --- a/tests/hikari/models/test_webhook.py +++ b/tests/hikari/models/test_webhook.py @@ -20,7 +20,7 @@ import pytest from hikari import rest -from hikari.components import application +from hikari import application from hikari.internal import urls from hikari.models import bases from hikari.models import channels diff --git a/tests/hikari/rest/test_base.py b/tests/hikari/rest/test_base.py index b0df084d85..a9a61422bb 100644 --- a/tests/hikari/rest/test_base.py +++ b/tests/hikari/rest/test_base.py @@ -19,7 +19,7 @@ import mock import pytest -from hikari.components import application +from hikari import application from hikari.internal import ratelimits from hikari.rest import base from hikari.rest import buckets diff --git a/tests/hikari/rest/test_channel.py b/tests/hikari/rest/test_channel.py index a6c07ca4b3..be2c082ad8 100644 --- a/tests/hikari/rest/test_channel.py +++ b/tests/hikari/rest/test_channel.py @@ -24,7 +24,7 @@ import mock import pytest -from hikari.components import application +from hikari import application from hikari.internal import helpers from hikari.models import bases from hikari.models import channels diff --git a/tests/hikari/rest/test_client.py b/tests/hikari/rest/test_client.py index c43678c6d2..4c7c06ed75 100644 --- a/tests/hikari/rest/test_client.py +++ b/tests/hikari/rest/test_client.py @@ -23,7 +23,7 @@ from hikari import configs from hikari import rest as high_level_rest -from hikari.components import application +from hikari import application from hikari.rest import channel from hikari.rest import gateway from hikari.rest import guild diff --git a/tests/hikari/rest/test_gateway.py b/tests/hikari/rest/test_gateway.py index 52415b532a..5d914a4f69 100644 --- a/tests/hikari/rest/test_gateway.py +++ b/tests/hikari/rest/test_gateway.py @@ -20,7 +20,7 @@ import mock import pytest -from hikari.components import application +from hikari import application from hikari.models import gateway as gateway_models from hikari.rest import gateway from hikari.rest import session diff --git a/tests/hikari/rest/test_guild.py b/tests/hikari/rest/test_guild.py index 9f80e9ab0a..19585417bc 100644 --- a/tests/hikari/rest/test_guild.py +++ b/tests/hikari/rest/test_guild.py @@ -23,7 +23,7 @@ import mock import pytest -from hikari.components import application +from hikari import application from hikari.models import audit_logs from hikari.models import bases from hikari.models import channels diff --git a/tests/hikari/rest/test_invite.py b/tests/hikari/rest/test_invite.py index e7d3b86180..4a197ab965 100644 --- a/tests/hikari/rest/test_invite.py +++ b/tests/hikari/rest/test_invite.py @@ -19,7 +19,7 @@ import mock import pytest -from hikari.components import application +from hikari import application from hikari.models import invites from hikari.rest import invite from hikari.rest import session diff --git a/tests/hikari/rest/test_me.py b/tests/hikari/rest/test_me.py index e246cb3e93..06ac10007c 100644 --- a/tests/hikari/rest/test_me.py +++ b/tests/hikari/rest/test_me.py @@ -22,7 +22,7 @@ import mock import pytest -from hikari.components import application +from hikari import application from hikari.models import applications from hikari.models import bases from hikari.models import channels diff --git a/tests/hikari/rest/test_oauth2.py b/tests/hikari/rest/test_oauth2.py index be4f86927a..83a149852c 100644 --- a/tests/hikari/rest/test_oauth2.py +++ b/tests/hikari/rest/test_oauth2.py @@ -20,7 +20,7 @@ import mock import pytest -from hikari.components import application +from hikari import application from hikari.models import applications from hikari.rest import oauth2 from hikari.rest import session diff --git a/tests/hikari/rest/test_react.py b/tests/hikari/rest/test_react.py index 2f54500a31..ae0b34b492 100644 --- a/tests/hikari/rest/test_react.py +++ b/tests/hikari/rest/test_react.py @@ -22,7 +22,7 @@ import mock import pytest -from hikari.components import application +from hikari import application from hikari.models import bases from hikari.models import channels from hikari.models import emojis diff --git a/tests/hikari/rest/test_user.py b/tests/hikari/rest/test_user.py index 93b182c3a2..cd66c90232 100644 --- a/tests/hikari/rest/test_user.py +++ b/tests/hikari/rest/test_user.py @@ -19,7 +19,7 @@ import mock import pytest -from hikari.components import application +from hikari import application from hikari.models import users from hikari.rest import session from hikari.rest import user diff --git a/tests/hikari/rest/test_voice.py b/tests/hikari/rest/test_voice.py index d21468a5fe..2b4ede3d3d 100644 --- a/tests/hikari/rest/test_voice.py +++ b/tests/hikari/rest/test_voice.py @@ -19,7 +19,7 @@ import mock import pytest -from hikari.components import application +from hikari import application from hikari.models import voices from hikari.rest import voice from hikari.rest import session diff --git a/tests/hikari/rest/test_webhook.py b/tests/hikari/rest/test_webhook.py index e37a3f2baa..779b87ae2f 100644 --- a/tests/hikari/rest/test_webhook.py +++ b/tests/hikari/rest/test_webhook.py @@ -21,7 +21,7 @@ import mock import pytest -from hikari.components import application +from hikari import application from hikari.internal import helpers from hikari.models import embeds from hikari.models import files diff --git a/tests/hikari/stateless/test_manager.py b/tests/hikari/stateless/test_manager.py index 9d0ca13719..b29c0e8c8f 100644 --- a/tests/hikari/stateless/test_manager.py +++ b/tests/hikari/stateless/test_manager.py @@ -19,7 +19,7 @@ import mock import pytest -from hikari.components import application +from hikari import application from hikari.events import channel from hikari.events import guild from hikari.events import message From ecd3b5962bace96781ae791302c57bc60e5cbd93 Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Sun, 17 May 2020 18:20:05 +0100 Subject: [PATCH 358/922] Added model stuff for cache. --- hikari/crud.py | 316 ++++++++++++++++++++++++++++++++++++ hikari/model_manager.py | 18 -- hikari/models/audit_logs.py | 1 + 3 files changed, 317 insertions(+), 18 deletions(-) create mode 100644 hikari/crud.py delete mode 100644 hikari/model_manager.py diff --git a/hikari/crud.py b/hikari/crud.py new file mode 100644 index 0000000000..e3411ff7e4 --- /dev/null +++ b/hikari/crud.py @@ -0,0 +1,316 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# Copyright © Nekoka.tt 2019-2020 +# +# This file is part of Hikari. +# +# Hikari is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Hikari is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Hikari. If not, see . + +import abc +import typing + +if typing.TYPE_CHECKING: + from hikari.models import applications + from hikari.models import audit_logs + from hikari.models import channels + from hikari.models import embeds + from hikari.models import emojis + from hikari.models import gateway + from hikari.models import guilds + from hikari.models import invites + from hikari.models import messages + from hikari.models import users + from hikari.models import voices + from hikari.models import webhooks + from hikari.internal import more_typing + + +class CRUD(abc.ABC): + """CRUD interface for model caching. + + This is designed to be interfaced with using the event manager. + """ + + ################ + # APPLICATIONS # + ################ + @abc.abstractmethod + async def create_application(self, payload: more_typing.JSONObject) -> applications.Application: + ... + + @abc.abstractmethod + async def create_own_guild(self, payload: more_typing.JSONObject) -> applications.OwnGuild: + ... + + @abc.abstractmethod + async def create_own_connection(self, payload: more_typing.JSONObject) -> applications.OwnConnection: + ... + + ############## + # AUDIT LOGS # + ############## + + @abc.abstractmethod + async def create_audit_log_change(self, payload: more_typing.JSONObject) -> audit_logs.AuditLogChange: + ... + + @abc.abstractmethod + async def create_audit_log_entry_info(self, payload: more_typing.JSONObject) -> audit_logs.BaseAuditLogEntryInfo: + ... + + @abc.abstractmethod + async def create_audit_log_entry(self, payload: more_typing.JSONObject) -> audit_logs.AuditLogEntry: + ... + + @abc.abstractmethod + async def create_audit_log(self, payload: more_typing.JSONObject) -> audit_logs.AuditLog: + ... + + ############ + # CHANNELS # + ############ + + @abc.abstractmethod + async def create_channel(self, payload: more_typing.JSONObject, can_cache: bool = False) -> channels.PartialChannel: + ... + + @abc.abstractmethod + async def update_channel( + self, channel: channels.PartialChannel, payload: more_typing.JSONObject, + ) -> channels.PartialChannel: + ... + + @abc.abstractmethod + async def get_channel(self, channel_id: int) -> typing.Optional[channels.PartialChannel]: + ... + + @abc.abstractmethod + async def delete_channel(self, channel_id: int) -> typing.Optional[channels.PartialChannel]: + ... + + ########## + # EMBEDS # + ########## + + @abc.abstractmethod + async def create_embed(self, payload: more_typing.JSONObject) -> embeds.Embed: + ... + + ########## + # EMOJIS # + ########## + + @abc.abstractmethod + async def create_emoji(self, payload: more_typing.JSONObject, can_cache: bool = False) -> emojis.Emoji: + ... + + @abc.abstractmethod + async def update_emoji(self, payload: more_typing.JSONObject) -> emojis.Emoji: + ... + + @abc.abstractmethod + async def get_emoji(self, emoji_id: int) -> typing.Optional[emojis.KnownCustomEmoji]: + ... + + @abc.abstractmethod + async def delete_emoji(self, emoji_id: int) -> typing.Optional[emojis.KnownCustomEmoji]: + ... + + ########### + # GATEWAY # + ########### + + @abc.abstractmethod + async def create_gateway_bot(self, payload: more_typing.JSONObject) -> gateway.GatewayBot: + ... + + ########## + # GUILDS # + ########## + + @abc.abstractmethod + async def create_member(self, payload: more_typing.JSONObject, can_cache: bool = False) -> guilds.GuildMember: + # TODO: revisit for the voodoo to make a member into a special user. + ... + + @abc.abstractmethod + async def update_member(self, member: guilds.GuildMember, payload: more_typing.JSONObject) -> guilds.GuildMember: + ... + + @abc.abstractmethod + async def get_member(self, guild_id: int, user_id: int) -> typing.Optional[guilds.GuildMember]: + ... + + @abc.abstractmethod + async def delete_member(self, guild_id: int, user_id: int) -> typing.Optional[guilds.GuildMember]: + ... + + @abc.abstractmethod + async def create_role(self, payload: more_typing.JSONObject, can_cache: bool = False) -> guilds.PartialGuildRole: + ... + + @abc.abstractmethod + async def update_role( + self, role: guilds.PartialGuildRole, payload: more_typing.JSONObject + ) -> guilds.PartialGuildRole: + ... + + @abc.abstractmethod + async def get_role(self, guild_id: int, role_id: int) -> typing.Optional[guilds.PartialGuildRole]: + ... + + @abc.abstractmethod + async def delete_role(self, guild_id: int, role_id: int) -> typing.Optional[guilds.PartialGuildRole]: + ... + + @abc.abstractmethod + async def create_presence( + self, payload: more_typing.JSONObject, can_cache: bool = False + ) -> guilds.GuildMemberPresence: + ... + + @abc.abstractmethod + async def update_presence( + self, role: guilds.GuildMemberPresence, payload: more_typing.JSONObject + ) -> guilds.GuildMemberPresence: + ... + + @abc.abstractmethod + async def get_presence(self, guild_id: int, user_id: int) -> typing.Optional[guilds.GuildMemberPresence]: + ... + + # TODO: do we need this? + @abc.abstractmethod + async def delete_presence(self, guild_id: int, user_id: int) -> typing.Optional[guilds.GuildMemberPresence]: + ... + + @abc.abstractmethod + async def create_guild_ban(self, payload: more_typing.JSONObject) -> guilds.GuildMemberBan: + ... + + @abc.abstractmethod + async def create_guild_integration(self, payload: more_typing.JSONObject) -> guilds.PartialGuildIntegration: + ... + + # FIXME: should this be Guild instead of PartialGuild + @abc.abstractmethod + async def create_guild(self, payload: more_typing.JSONObject, can_cache: bool = False) -> guilds.PartialGuild: + ... + + @abc.abstractmethod + async def update_guild(self, guild: guilds.PartialGuild, payload: more_typing.JSONObject) -> guilds.PartialGuild: + ... + + @abc.abstractmethod + async def get_guild(self, guild_id: int) -> typing.Optional[guilds.PartialGuild]: + ... + + @abc.abstractmethod + async def delete_guild(self, guild_id: int) -> typing.Optional[guilds.PartialGuild]: + ... + + @abc.abstractmethod + async def create_guild_preview(self, payload: more_typing.JSONObject) -> guilds.GuildPreview: + ... + + ########### + # INVITES # + ########### + @abc.abstractmethod + async def create_invite(self, payload: more_typing.JSONObject) -> invites.Invite: + ... + + ############ + # MESSAGES # + ############ + @abc.abstractmethod + async def create_reaction(self, payload: more_typing.JSONObject) -> messages.Reaction: + ... + + @abc.abstractmethod + async def create_message(self, payload: more_typing.JSONObject, can_cache: bool = False) -> messages.Message: + ... + + @abc.abstractmethod + async def update_message(self, message: messages.Message, payload: more_typing.JSONObject) -> messages.Message: + ... + + @abc.abstractmethod + async def get_message(self, channel_id: int, message_id: int) -> typing.Optional[messages.Message]: + ... + + @abc.abstractmethod + async def delete_message(self, channel_id: int, message_id: int) -> typing.Optional[messages.Message]: + ... + + ######### + # USERS # + ######### + @abc.abstractmethod + async def create_user(self, payload: more_typing.JSONObject, can_cache: bool = False) -> users.User: + ... + + @abc.abstractmethod + async def update_user(self, user: users.User, payload: more_typing.JSONObject) -> users.User: + ... + + @abc.abstractmethod + async def get_user(self, user_id: int) -> typing.Optional[users.User]: + ... + + @abc.abstractmethod + async def delete_user(self, user_id: int) -> typing.Optional[users.User]: + ... + + @abc.abstractmethod + async def create_my_user(self, payload: more_typing.JSONObject, can_cache: bool = False) -> users.MyUser: + ... + + @abc.abstractmethod + async def update_my_user(self, my_user: users.MyUser, payload: more_typing.JSONObject) -> users.MyUser: + ... + + @abc.abstractmethod + async def get_my_user(self) -> typing.Optional[users.User]: + ... + + ########## + # VOICES # + ########## + @abc.abstractmethod + async def create_voice_state(self, payload: more_typing.JSONObject, can_cache: bool = False) -> voices.VoiceState: + ... + + @abc.abstractmethod + async def update_voice_state(self, payload: more_typing.JSONObject) -> voices.VoiceState: + ... + + @abc.abstractmethod + async def get_voice_state(self, guild_id: int, channel_id: int) -> typing.Optional[voices.VoiceState]: + ... + + @abc.abstractmethod + async def delete_voice_state(self, guild_id: int, channel_id: int) -> typing.Optional[voices.VoiceState]: + ... + + @abc.abstractmethod + async def create_voice_region(self, payload: more_typing.JSONObject) -> voices.VoiceRegion: + ... + + ############ + # WEBHOOKS # + ############ + @abc.abstractmethod + async def create_webhook(self, payload: more_typing.JSONObject) -> webhooks.Webhook: + ... diff --git a/hikari/model_manager.py b/hikari/model_manager.py deleted file mode 100644 index 1c1502a5ca..0000000000 --- a/hikari/model_manager.py +++ /dev/null @@ -1,18 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# Copyright © Nekoka.tt 2019-2020 -# -# This file is part of Hikari. -# -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . diff --git a/hikari/models/audit_logs.py b/hikari/models/audit_logs.py index 2366ae292b..1adb26286a 100644 --- a/hikari/models/audit_logs.py +++ b/hikari/models/audit_logs.py @@ -508,6 +508,7 @@ def _deserialize_webhooks( return {bases.Snowflake(webhook["id"]): webhooks_.Webhook.deserialize(webhook, **kwargs) for webhook in payload} +# TODO: can we remove this? it is used by a seemingly duplicated endpoint that can just use the iterator. @marshaller.marshallable() @attr.s(eq=True, repr=False, kw_only=True, slots=True) class AuditLog(bases.Entity, marshaller.Deserializable): From dcbdc02d20741d81cc3fbebc4fc9933a39f6273a Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Sun, 17 May 2020 19:55:35 +0100 Subject: [PATCH 359/922] Applications implementation and renaming crud. --- hikari/models/applications.py | 19 +---- hikari/models/audit_logs.py | 2 +- hikari/{crud.py => object_factory.py} | 110 ++++++++++++++------------ 3 files changed, 64 insertions(+), 67 deletions(-) rename hikari/{crud.py => object_factory.py} (77%) diff --git a/hikari/models/applications.py b/hikari/models/applications.py index a2c0616545..c6b04542c0 100644 --- a/hikari/models/applications.py +++ b/hikari/models/applications.py @@ -22,7 +22,6 @@ __all__ = [ "Application", - "ApplicationOwner", "ConnectionVisibility", "OAuth2Scope", "OwnConnection", @@ -345,20 +344,6 @@ def format_icon_url(self, *, format_: str = "png", size: int = 4096) -> typing.O return None -@marshaller.marshallable() -@attr.s(eq=True, hash=True, kw_only=True, slots=True) -class ApplicationOwner(users.User): - """Represents the user who owns an application, may be a team user.""" - - flags: int = marshaller.attrib(deserializer=users.UserFlag, eq=False, hash=False, repr=True) - """This user's flags.""" - - @property - def is_team_user(self) -> bool: - """If this user is a Team user (the owner of an application that's owned by a team).""" - return bool((self.flags >> 10) & 1) - - def _deserialize_verify_key(payload: str) -> bytes: return bytes(payload, "utf-8") @@ -390,8 +375,8 @@ class Application(bases.Unique, marshaller.Deserializable): Will be `None` if this application doesn't have a bot. """ - owner: typing.Optional[ApplicationOwner] = marshaller.attrib( - deserializer=ApplicationOwner.deserialize, + owner: typing.Optional[users.User] = marshaller.attrib( + deserializer=users.User.deserialize, if_undefined=None, default=None, inherit_kwargs=True, diff --git a/hikari/models/audit_logs.py b/hikari/models/audit_logs.py index 1adb26286a..94060a374e 100644 --- a/hikari/models/audit_logs.py +++ b/hikari/models/audit_logs.py @@ -505,7 +505,7 @@ def _deserialize_users( def _deserialize_webhooks( payload: more_typing.JSONArray, **kwargs: typing.Any ) -> typing.Mapping[bases.Snowflake, webhooks_.Webhook]: - return {bases.Snowflake(webhook["id"]): webhooks_.Webhook.deserialize(webhook, **kwargs) for webhook in payload} + return {bases.Snowflake(webhook["id"]): webhooks_.Webhook.deserialize(we1bhook, **kwargs) for webhook in payload} # TODO: can we remove this? it is used by a seemingly duplicated endpoint that can just use the iterator. diff --git a/hikari/crud.py b/hikari/object_factory.py similarity index 77% rename from hikari/crud.py rename to hikari/object_factory.py index e3411ff7e4..ed3cf80f01 100644 --- a/hikari/crud.py +++ b/hikari/object_factory.py @@ -16,10 +16,14 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . +"""Utilities to handle cache management and object deserialization.""" +from __future__ import annotations import abc import typing +from hikari.models import bases + if typing.TYPE_CHECKING: from hikari.models import applications from hikari.models import audit_logs @@ -36,24 +40,63 @@ from hikari.internal import more_typing -class CRUD(abc.ABC): - """CRUD interface for model caching. +class ObjectFactory: + """Class that handles deserialization and cache operations. This is designed to be interfaced with using the event manager. """ + def __init__(self, app): + self.app = app + ################ # APPLICATIONS # ################ - @abc.abstractmethod async def create_application(self, payload: more_typing.JSONObject) -> applications.Application: - ... + application = applications.Application() + + application.name = payload["name"] + application.description = payload["description"] + application.is_bot_public = payload.get("bot_public") + application.is_bot_code_grant_required = payload.get("bot_require_code_grant") + application.summary = payload["summary"] + application.slug = payload.get("slug") + application.cover_image_hash = payload.get("cover_image") + + if "guild_id" in payload: + application.guild_id = bases.Snowflake(application["guild_id"]) + + if "primary_sku_id" in payload: + application.primary_sku_id = bases.Snowflake(application["primary_sku_id"]) + + if "rpc_origins" in payload: + application.rpc_origins = set(payload.get("rpc_origins")) + + if "verify_key" in payload: + application.verify_key = bytes(payload["verify_key"], "utf-8") + + if "owner" in payload: + application.owner = self._make_user(payload["owner"]) + + if (raw_team := payload.get("team")) is not None: + team = applications.Team() + team.id = bases.Snowflake(raw_team["id"]) + team.icon_hash = raw_team.get("icon_hash") + team.owner_user_id = bases.Snowflake(raw_team["owner_user_id"]) + + team.members = {} + for raw_member in raw_team["members"]: + member = applications.TeamMember() + member.team_id = team.id + member.user = self._make_user(raw_member["user"]) + member.permissions = set(raw_member["permissions"]) + member.membership_state = applications.TeamMembershipState(raw_member["membership_state"]) + + return application - @abc.abstractmethod async def create_own_guild(self, payload: more_typing.JSONObject) -> applications.OwnGuild: ... - @abc.abstractmethod async def create_own_connection(self, payload: more_typing.JSONObject) -> applications.OwnConnection: ... @@ -61,19 +104,15 @@ async def create_own_connection(self, payload: more_typing.JSONObject) -> applic # AUDIT LOGS # ############## - @abc.abstractmethod async def create_audit_log_change(self, payload: more_typing.JSONObject) -> audit_logs.AuditLogChange: ... - @abc.abstractmethod async def create_audit_log_entry_info(self, payload: more_typing.JSONObject) -> audit_logs.BaseAuditLogEntryInfo: ... - @abc.abstractmethod async def create_audit_log_entry(self, payload: more_typing.JSONObject) -> audit_logs.AuditLogEntry: ... - @abc.abstractmethod async def create_audit_log(self, payload: more_typing.JSONObject) -> audit_logs.AuditLog: ... @@ -81,21 +120,17 @@ async def create_audit_log(self, payload: more_typing.JSONObject) -> audit_logs. # CHANNELS # ############ - @abc.abstractmethod async def create_channel(self, payload: more_typing.JSONObject, can_cache: bool = False) -> channels.PartialChannel: ... - @abc.abstractmethod async def update_channel( self, channel: channels.PartialChannel, payload: more_typing.JSONObject, ) -> channels.PartialChannel: ... - @abc.abstractmethod async def get_channel(self, channel_id: int) -> typing.Optional[channels.PartialChannel]: ... - @abc.abstractmethod async def delete_channel(self, channel_id: int) -> typing.Optional[channels.PartialChannel]: ... @@ -103,7 +138,6 @@ async def delete_channel(self, channel_id: int) -> typing.Optional[channels.Part # EMBEDS # ########## - @abc.abstractmethod async def create_embed(self, payload: more_typing.JSONObject) -> embeds.Embed: ... @@ -111,19 +145,15 @@ async def create_embed(self, payload: more_typing.JSONObject) -> embeds.Embed: # EMOJIS # ########## - @abc.abstractmethod async def create_emoji(self, payload: more_typing.JSONObject, can_cache: bool = False) -> emojis.Emoji: ... - @abc.abstractmethod async def update_emoji(self, payload: more_typing.JSONObject) -> emojis.Emoji: ... - @abc.abstractmethod async def get_emoji(self, emoji_id: int) -> typing.Optional[emojis.KnownCustomEmoji]: ... - @abc.abstractmethod async def delete_emoji(self, emoji_id: int) -> typing.Optional[emojis.KnownCustomEmoji]: ... @@ -131,7 +161,6 @@ async def delete_emoji(self, emoji_id: int) -> typing.Optional[emojis.KnownCusto # GATEWAY # ########### - @abc.abstractmethod async def create_gateway_bot(self, payload: more_typing.JSONObject) -> gateway.GatewayBot: ... @@ -139,127 +168,99 @@ async def create_gateway_bot(self, payload: more_typing.JSONObject) -> gateway.G # GUILDS # ########## - @abc.abstractmethod async def create_member(self, payload: more_typing.JSONObject, can_cache: bool = False) -> guilds.GuildMember: # TODO: revisit for the voodoo to make a member into a special user. ... - @abc.abstractmethod async def update_member(self, member: guilds.GuildMember, payload: more_typing.JSONObject) -> guilds.GuildMember: ... - @abc.abstractmethod async def get_member(self, guild_id: int, user_id: int) -> typing.Optional[guilds.GuildMember]: ... - @abc.abstractmethod async def delete_member(self, guild_id: int, user_id: int) -> typing.Optional[guilds.GuildMember]: ... - @abc.abstractmethod async def create_role(self, payload: more_typing.JSONObject, can_cache: bool = False) -> guilds.PartialGuildRole: ... - @abc.abstractmethod async def update_role( self, role: guilds.PartialGuildRole, payload: more_typing.JSONObject ) -> guilds.PartialGuildRole: ... - @abc.abstractmethod async def get_role(self, guild_id: int, role_id: int) -> typing.Optional[guilds.PartialGuildRole]: ... - @abc.abstractmethod async def delete_role(self, guild_id: int, role_id: int) -> typing.Optional[guilds.PartialGuildRole]: ... - @abc.abstractmethod async def create_presence( self, payload: more_typing.JSONObject, can_cache: bool = False ) -> guilds.GuildMemberPresence: ... - @abc.abstractmethod async def update_presence( self, role: guilds.GuildMemberPresence, payload: more_typing.JSONObject ) -> guilds.GuildMemberPresence: ... - @abc.abstractmethod async def get_presence(self, guild_id: int, user_id: int) -> typing.Optional[guilds.GuildMemberPresence]: ... - # TODO: do we need this? - @abc.abstractmethod async def delete_presence(self, guild_id: int, user_id: int) -> typing.Optional[guilds.GuildMemberPresence]: ... - @abc.abstractmethod async def create_guild_ban(self, payload: more_typing.JSONObject) -> guilds.GuildMemberBan: ... - @abc.abstractmethod async def create_guild_integration(self, payload: more_typing.JSONObject) -> guilds.PartialGuildIntegration: ... - # FIXME: should this be Guild instead of PartialGuild - @abc.abstractmethod async def create_guild(self, payload: more_typing.JSONObject, can_cache: bool = False) -> guilds.PartialGuild: ... - @abc.abstractmethod async def update_guild(self, guild: guilds.PartialGuild, payload: more_typing.JSONObject) -> guilds.PartialGuild: ... - @abc.abstractmethod async def get_guild(self, guild_id: int) -> typing.Optional[guilds.PartialGuild]: ... - @abc.abstractmethod async def delete_guild(self, guild_id: int) -> typing.Optional[guilds.PartialGuild]: ... - @abc.abstractmethod async def create_guild_preview(self, payload: more_typing.JSONObject) -> guilds.GuildPreview: ... ########### # INVITES # ########### - @abc.abstractmethod async def create_invite(self, payload: more_typing.JSONObject) -> invites.Invite: ... ############ # MESSAGES # ############ - @abc.abstractmethod async def create_reaction(self, payload: more_typing.JSONObject) -> messages.Reaction: ... - @abc.abstractmethod async def create_message(self, payload: more_typing.JSONObject, can_cache: bool = False) -> messages.Message: ... - @abc.abstractmethod async def update_message(self, message: messages.Message, payload: more_typing.JSONObject) -> messages.Message: ... - @abc.abstractmethod async def get_message(self, channel_id: int, message_id: int) -> typing.Optional[messages.Message]: ... - @abc.abstractmethod async def delete_message(self, channel_id: int, message_id: int) -> typing.Optional[messages.Message]: ... ######### # USERS # ######### - @abc.abstractmethod async def create_user(self, payload: more_typing.JSONObject, can_cache: bool = False) -> users.User: - ... + return self._make_user(payload) @abc.abstractmethod async def update_user(self, user: users.User, payload: more_typing.JSONObject) -> users.User: @@ -314,3 +315,14 @@ async def create_voice_region(self, payload: more_typing.JSONObject) -> voices.V @abc.abstractmethod async def create_webhook(self, payload: more_typing.JSONObject) -> webhooks.Webhook: ... + + def _make_user(self, payload): + user_obj = users.User(app=self.app) + user_obj.id = bases.Snowflake(payload["id"]) + user_obj.discriminator = payload["discriminator"] + user_obj.username = payload["username"] + user_obj.avatar_hash = payload["avatar"] + user_obj.is_bot = payload.get("bot", False) + user_obj.is_system = payload.get("system", False) + user_obj.flags = payload.get("bot", False) + return user_obj From d0624770c11631e49faf4c6346879ed13fe75c10 Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Mon, 18 May 2020 18:54:29 +0100 Subject: [PATCH 360/922] Added new application structure. --- hikari/__init__.py | 6 +- hikari/aiohttp_config.py | 85 +++ hikari/api/__init__.py | 33 ++ hikari/api/base_app.py | 61 +++ hikari/api/bot.py | 39 ++ hikari/{object_factory.py => api/cache.py} | 119 ++--- hikari/api/entity_factory.py | 30 ++ hikari/api/event_consumer.py | 33 ++ hikari/api/event_dispatcher.py | 36 ++ hikari/api/gateway_dispatcher.py | 48 ++ hikari/api/gateway_zookeeper.py | 152 ++++++ hikari/api/rest_app.py | 42 ++ hikari/application.py | 373 ------------- hikari/configs.py | 578 --------------------- hikari/events/channel.py | 1 - hikari/events/guild.py | 1 - hikari/events/message.py | 1 - hikari/events/other.py | 1 - hikari/events/voice.py | 1 - hikari/gateway/client.py | 76 +-- hikari/gateway/connection.py | 5 +- hikari/gateway/dispatchers.py | 2 +- hikari/gateway/event_managers.py | 10 +- hikari/gateway/intent_aware_dispatchers.py | 2 - hikari/impl/__init__.py | 18 + hikari/impl/bot.py | 130 +++++ hikari/impl/cache.py | 205 ++++++++ hikari/impl/entity_factory.py | 26 + hikari/impl/event_manager.py | 31 ++ hikari/impl/gateway_zookeeper.py | 151 ++++++ hikari/impl/rest_app.py | 84 +++ hikari/internal/conversions.py | 4 +- hikari/internal/helpers.py | 1 - hikari/internal/ratelimits.py | 1 - hikari/internal/urls.py | 1 - hikari/models/applications.py | 1 - hikari/models/audit_logs.py | 3 +- hikari/models/bases.py | 2 +- hikari/models/channels.py | 3 +- hikari/models/embeds.py | 1 - hikari/models/emojis.py | 1 - hikari/models/files.py | 2 +- hikari/models/gateway.py | 1 - hikari/models/guilds.py | 1 - hikari/models/invites.py | 1 - hikari/models/pagination.py | 5 +- hikari/models/unset.py | 2 +- hikari/models/webhooks.py | 1 - hikari/rest/base.py | 13 +- hikari/rest/buckets.py | 3 +- hikari/rest/channel.py | 3 +- hikari/rest/client.py | 66 +-- hikari/rest/gateway.py | 3 +- hikari/rest/guild.py | 3 +- hikari/rest/invite.py | 3 +- hikari/rest/me.py | 3 +- hikari/rest/oauth2.py | 2 +- hikari/rest/react.py | 2 +- hikari/rest/session.py | 13 +- hikari/rest/user.py | 3 +- hikari/rest/voice.py | 2 +- hikari/rest/webhook.py | 3 +- hikari/stateful/.gitkeep | 0 hikari/stateless/bot.py | 7 +- hikari/stateless/manager.py | 4 +- tests/hikari/_helpers.py | 2 +- tests/hikari/gateway/test_client.py | 8 +- tests/hikari/models/test_applications.py | 4 +- tests/hikari/models/test_audit_logs.py | 2 +- tests/hikari/models/test_bases.py | 2 +- tests/hikari/models/test_channels.py | 10 +- tests/hikari/models/test_embeds.py | 14 +- tests/hikari/models/test_guilds.py | 24 +- tests/hikari/models/test_messages.py | 10 +- tests/hikari/rest/test_channel.py | 8 +- tests/hikari/rest/test_client.py | 4 +- tests/hikari/test_configs.py | 40 +- 77 files changed, 1455 insertions(+), 1217 deletions(-) create mode 100644 hikari/aiohttp_config.py create mode 100644 hikari/api/__init__.py create mode 100644 hikari/api/base_app.py create mode 100644 hikari/api/bot.py rename hikari/{object_factory.py => api/cache.py} (76%) create mode 100644 hikari/api/entity_factory.py create mode 100644 hikari/api/event_consumer.py create mode 100644 hikari/api/event_dispatcher.py create mode 100644 hikari/api/gateway_dispatcher.py create mode 100644 hikari/api/gateway_zookeeper.py create mode 100644 hikari/api/rest_app.py delete mode 100644 hikari/application.py delete mode 100644 hikari/configs.py create mode 100644 hikari/impl/__init__.py create mode 100644 hikari/impl/bot.py create mode 100644 hikari/impl/cache.py create mode 100644 hikari/impl/entity_factory.py create mode 100644 hikari/impl/event_manager.py create mode 100644 hikari/impl/gateway_zookeeper.py create mode 100644 hikari/impl/rest_app.py delete mode 100644 hikari/stateful/.gitkeep diff --git a/hikari/__init__.py b/hikari/__init__.py index e419e11cdf..d12288a5c1 100644 --- a/hikari/__init__.py +++ b/hikari/__init__.py @@ -30,13 +30,11 @@ from ._about import __license__ from ._about import __url__ from ._about import __version__ -from .configs import * -from .events import * +from .aiohttp_config import * from .errors import * +from .events import * from .gateway import * from .models import * from .rest import * -from .stateful import * -from .stateless import * __all__ = [] diff --git a/hikari/aiohttp_config.py b/hikari/aiohttp_config.py new file mode 100644 index 0000000000..906ae907ba --- /dev/null +++ b/hikari/aiohttp_config.py @@ -0,0 +1,85 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# Copyright © Nekoka.tt 2019-2020 +# +# This file is part of Hikari. +# +# Hikari is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Hikari is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Hikari. If not, see . +"""Configuration data classes.""" + +from __future__ import annotations + +__all__ = ["AIOHTTPConfig"] + +import typing + +import aiohttp +import attr + +if typing.TYPE_CHECKING: + import ssl + + +@attr.s(kw_only=True, repr=False, auto_attribs=True) +class AIOHTTPConfig: + """Config for application that use AIOHTTP.""" + + allow_redirects: bool = False + """If `True`, allow following redirects from `3xx` HTTP responses. + + Generally you do not want to enable this unless you have a good reason to. + """ + + proxy_auth: typing.Optional[aiohttp.BasicAuth] = None + """Optional proxy authorization to provide in any HTTP requests.""" + + proxy_headers: typing.Optional[typing.Mapping[str, str]] = None + """Optional proxy headers to provide in any HTTP requests.""" + + proxy_url: typing.Optional[str] = None + """The optional URL of the proxy to send requests via.""" + + request_timeout: typing.Optional[float] = None + """Optional request timeout to use. If an HTTP request takes longer than + this, it will be aborted. + + If not `None`, the value represents a number of seconds as a floating + point number. + """ + + ssl_context: typing.Optional[ssl.SSLContext] = None + """The optional SSL context to use.""" + + tcp_connector: typing.Optional[aiohttp.TCPConnector] = None + """This may otherwise be `None` to use the default settings provided by + `aiohttp`. + """ + + trust_env: bool = False + """If `True`, and no proxy info is given, then `HTTP_PROXY` and + `HTTPS_PROXY` will be used from the environment variables if present. + + Any proxy credentials will be read from the user's `netrc` file + (https://www.gnu.org/software/inetutils/manual/html_node/The-_002enetrc-file.html) + If `False`, then this information is instead ignored. + Defaults to `False` if unspecified. + """ + + verify_ssl: bool = True + """If `True`, then responses with invalid SSL certificates will be + rejected. Generally you want to keep this enabled unless you have a + problem with SSL and you know exactly what you are doing by disabling + this. Disabling SSL verification can have major security implications. + You turn this off at your own risk. + """ diff --git a/hikari/api/__init__.py b/hikari/api/__init__.py new file mode 100644 index 0000000000..dfe1dfe460 --- /dev/null +++ b/hikari/api/__init__.py @@ -0,0 +1,33 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# Copyright © Nekoka.tt 2019-2020 +# +# This file is part of Hikari. +# +# Hikari is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Hikari is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Hikari. If not, see . +"""ABCs that describe the core Hikari API for writing Discord applications.""" + +from __future__ import annotations + +__all__ = [] + +from . import base_app +from . import bot +from . import cache +from . import entity_factory +from . import event_consumer +from . import event_dispatcher +from . import gateway_dispatcher +from . import gateway_zookeeper +from . import rest_app diff --git a/hikari/api/base_app.py b/hikari/api/base_app.py new file mode 100644 index 0000000000..2294450d02 --- /dev/null +++ b/hikari/api/base_app.py @@ -0,0 +1,61 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# Copyright © Nekoka.tt 2019-2020 +# +# This file is part of Hikari. +# +# Hikari is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Hikari is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Hikari. If not, see . +from __future__ import annotations + +__all__ = ["IBaseApp"] + +import abc +import logging +import typing + +from concurrent import futures + +if typing.TYPE_CHECKING: + from hikari.api import cache as cache_ + from hikari.api import entity_factory as entity_factory_ + + +class IBaseApp(abc.ABC): + """Core components that any Hikari-based application will usually need.""" + + __slots__ = () + + @property + @abc.abstractmethod + def logger(self) -> logging.Logger: + """Logger for logging messages.""" + + @property + @abc.abstractmethod + def cache(self) -> cache_.ICache: + """Entity cache.""" + + @property + @abc.abstractmethod + def entity_factory(self) -> entity_factory_.IEntityFactory: + """Entity creator and updater facility.""" + + @property + @abc.abstractmethod + def thread_pool(self) -> typing.Optional[futures.ThreadPoolExecutor]: + """The optional library-wide thread-pool to utilise for file IO.""" + + @abc.abstractmethod + async def close(self) -> None: + """Safely shut down all resources.""" diff --git a/hikari/api/bot.py b/hikari/api/bot.py new file mode 100644 index 0000000000..41b3bb405e --- /dev/null +++ b/hikari/api/bot.py @@ -0,0 +1,39 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# Copyright © Nekoka.tt 2019-2020 +# +# This file is part of Hikari. +# +# Hikari is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Hikari is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Hikari. If not, see . +from __future__ import annotations + +__all__ = ["IBot"] + +import abc + +from hikari.api import gateway_dispatcher +from hikari.api import gateway_zookeeper +from hikari.api import rest_app + + +class IBot(rest_app.IRESTApp, gateway_zookeeper.IGatewayZookeeper, gateway_dispatcher.IGatewayDispatcher, abc.ABC): + """Component for single-process bots. + + Bots are components that have access to a REST API, an event dispatcher, + and an event consumer. + + Additionally, bots will contain a collection of Gateway client objects. + """ + + __slots__ = () diff --git a/hikari/object_factory.py b/hikari/api/cache.py similarity index 76% rename from hikari/object_factory.py rename to hikari/api/cache.py index ed3cf80f01..3453a6ba12 100644 --- a/hikari/object_factory.py +++ b/hikari/api/cache.py @@ -19,11 +19,11 @@ """Utilities to handle cache management and object deserialization.""" from __future__ import annotations +__all__ = ["ICache"] + import abc import typing -from hikari.models import bases - if typing.TYPE_CHECKING: from hikari.models import applications from hikari.models import audit_logs @@ -36,67 +36,26 @@ from hikari.models import messages from hikari.models import users from hikari.models import voices - from hikari.models import webhooks from hikari.internal import more_typing -class ObjectFactory: - """Class that handles deserialization and cache operations. +class ICache(abc.ABC): + """Interface for a cache implementation.""" - This is designed to be interfaced with using the event manager. - """ - - def __init__(self, app): - self.app = app + __slots__ = () ################ # APPLICATIONS # ################ + @abc.abstractmethod async def create_application(self, payload: more_typing.JSONObject) -> applications.Application: - application = applications.Application() - - application.name = payload["name"] - application.description = payload["description"] - application.is_bot_public = payload.get("bot_public") - application.is_bot_code_grant_required = payload.get("bot_require_code_grant") - application.summary = payload["summary"] - application.slug = payload.get("slug") - application.cover_image_hash = payload.get("cover_image") - - if "guild_id" in payload: - application.guild_id = bases.Snowflake(application["guild_id"]) - - if "primary_sku_id" in payload: - application.primary_sku_id = bases.Snowflake(application["primary_sku_id"]) - - if "rpc_origins" in payload: - application.rpc_origins = set(payload.get("rpc_origins")) - - if "verify_key" in payload: - application.verify_key = bytes(payload["verify_key"], "utf-8") - - if "owner" in payload: - application.owner = self._make_user(payload["owner"]) - - if (raw_team := payload.get("team")) is not None: - team = applications.Team() - team.id = bases.Snowflake(raw_team["id"]) - team.icon_hash = raw_team.get("icon_hash") - team.owner_user_id = bases.Snowflake(raw_team["owner_user_id"]) - - team.members = {} - for raw_member in raw_team["members"]: - member = applications.TeamMember() - member.team_id = team.id - member.user = self._make_user(raw_member["user"]) - member.permissions = set(raw_member["permissions"]) - member.membership_state = applications.TeamMembershipState(raw_member["membership_state"]) - - return application + ... + @abc.abstractmethod async def create_own_guild(self, payload: more_typing.JSONObject) -> applications.OwnGuild: ... + @abc.abstractmethod async def create_own_connection(self, payload: more_typing.JSONObject) -> applications.OwnConnection: ... @@ -104,15 +63,19 @@ async def create_own_connection(self, payload: more_typing.JSONObject) -> applic # AUDIT LOGS # ############## + @abc.abstractmethod async def create_audit_log_change(self, payload: more_typing.JSONObject) -> audit_logs.AuditLogChange: ... + @abc.abstractmethod async def create_audit_log_entry_info(self, payload: more_typing.JSONObject) -> audit_logs.BaseAuditLogEntryInfo: ... + @abc.abstractmethod async def create_audit_log_entry(self, payload: more_typing.JSONObject) -> audit_logs.AuditLogEntry: ... + @abc.abstractmethod async def create_audit_log(self, payload: more_typing.JSONObject) -> audit_logs.AuditLog: ... @@ -120,17 +83,21 @@ async def create_audit_log(self, payload: more_typing.JSONObject) -> audit_logs. # CHANNELS # ############ + @abc.abstractmethod async def create_channel(self, payload: more_typing.JSONObject, can_cache: bool = False) -> channels.PartialChannel: ... + @abc.abstractmethod async def update_channel( self, channel: channels.PartialChannel, payload: more_typing.JSONObject, ) -> channels.PartialChannel: ... + @abc.abstractmethod async def get_channel(self, channel_id: int) -> typing.Optional[channels.PartialChannel]: ... + @abc.abstractmethod async def delete_channel(self, channel_id: int) -> typing.Optional[channels.PartialChannel]: ... @@ -138,6 +105,7 @@ async def delete_channel(self, channel_id: int) -> typing.Optional[channels.Part # EMBEDS # ########## + @abc.abstractmethod async def create_embed(self, payload: more_typing.JSONObject) -> embeds.Embed: ... @@ -145,15 +113,19 @@ async def create_embed(self, payload: more_typing.JSONObject) -> embeds.Embed: # EMOJIS # ########## + @abc.abstractmethod async def create_emoji(self, payload: more_typing.JSONObject, can_cache: bool = False) -> emojis.Emoji: ... + @abc.abstractmethod async def update_emoji(self, payload: more_typing.JSONObject) -> emojis.Emoji: ... + @abc.abstractmethod async def get_emoji(self, emoji_id: int) -> typing.Optional[emojis.KnownCustomEmoji]: ... + @abc.abstractmethod async def delete_emoji(self, emoji_id: int) -> typing.Optional[emojis.KnownCustomEmoji]: ... @@ -161,6 +133,7 @@ async def delete_emoji(self, emoji_id: int) -> typing.Optional[emojis.KnownCusto # GATEWAY # ########### + @abc.abstractmethod async def create_gateway_bot(self, payload: more_typing.JSONObject) -> gateway.GatewayBot: ... @@ -168,99 +141,125 @@ async def create_gateway_bot(self, payload: more_typing.JSONObject) -> gateway.G # GUILDS # ########## + @abc.abstractmethod async def create_member(self, payload: more_typing.JSONObject, can_cache: bool = False) -> guilds.GuildMember: # TODO: revisit for the voodoo to make a member into a special user. ... + @abc.abstractmethod async def update_member(self, member: guilds.GuildMember, payload: more_typing.JSONObject) -> guilds.GuildMember: ... + @abc.abstractmethod async def get_member(self, guild_id: int, user_id: int) -> typing.Optional[guilds.GuildMember]: ... + @abc.abstractmethod async def delete_member(self, guild_id: int, user_id: int) -> typing.Optional[guilds.GuildMember]: ... + @abc.abstractmethod async def create_role(self, payload: more_typing.JSONObject, can_cache: bool = False) -> guilds.PartialGuildRole: ... + @abc.abstractmethod async def update_role( self, role: guilds.PartialGuildRole, payload: more_typing.JSONObject ) -> guilds.PartialGuildRole: ... + @abc.abstractmethod async def get_role(self, guild_id: int, role_id: int) -> typing.Optional[guilds.PartialGuildRole]: ... + @abc.abstractmethod async def delete_role(self, guild_id: int, role_id: int) -> typing.Optional[guilds.PartialGuildRole]: ... + @abc.abstractmethod async def create_presence( self, payload: more_typing.JSONObject, can_cache: bool = False ) -> guilds.GuildMemberPresence: ... + @abc.abstractmethod async def update_presence( self, role: guilds.GuildMemberPresence, payload: more_typing.JSONObject ) -> guilds.GuildMemberPresence: ... + @abc.abstractmethod async def get_presence(self, guild_id: int, user_id: int) -> typing.Optional[guilds.GuildMemberPresence]: ... + @abc.abstractmethod async def delete_presence(self, guild_id: int, user_id: int) -> typing.Optional[guilds.GuildMemberPresence]: ... + @abc.abstractmethod async def create_guild_ban(self, payload: more_typing.JSONObject) -> guilds.GuildMemberBan: ... + @abc.abstractmethod async def create_guild_integration(self, payload: more_typing.JSONObject) -> guilds.PartialGuildIntegration: ... + @abc.abstractmethod async def create_guild(self, payload: more_typing.JSONObject, can_cache: bool = False) -> guilds.PartialGuild: ... + @abc.abstractmethod async def update_guild(self, guild: guilds.PartialGuild, payload: more_typing.JSONObject) -> guilds.PartialGuild: ... + @abc.abstractmethod async def get_guild(self, guild_id: int) -> typing.Optional[guilds.PartialGuild]: ... + @abc.abstractmethod async def delete_guild(self, guild_id: int) -> typing.Optional[guilds.PartialGuild]: ... + @abc.abstractmethod async def create_guild_preview(self, payload: more_typing.JSONObject) -> guilds.GuildPreview: ... ########### # INVITES # ########### + @abc.abstractmethod async def create_invite(self, payload: more_typing.JSONObject) -> invites.Invite: ... ############ # MESSAGES # ############ + @abc.abstractmethod async def create_reaction(self, payload: more_typing.JSONObject) -> messages.Reaction: ... + @abc.abstractmethod async def create_message(self, payload: more_typing.JSONObject, can_cache: bool = False) -> messages.Message: ... + @abc.abstractmethod async def update_message(self, message: messages.Message, payload: more_typing.JSONObject) -> messages.Message: ... + @abc.abstractmethod async def get_message(self, channel_id: int, message_id: int) -> typing.Optional[messages.Message]: ... + @abc.abstractmethod async def delete_message(self, channel_id: int, message_id: int) -> typing.Optional[messages.Message]: ... ######### # USERS # ######### + @abc.abstractmethod async def create_user(self, payload: more_typing.JSONObject, can_cache: bool = False) -> users.User: - return self._make_user(payload) + ... @abc.abstractmethod async def update_user(self, user: users.User, payload: more_typing.JSONObject) -> users.User: @@ -308,21 +307,3 @@ async def delete_voice_state(self, guild_id: int, channel_id: int) -> typing.Opt @abc.abstractmethod async def create_voice_region(self, payload: more_typing.JSONObject) -> voices.VoiceRegion: ... - - ############ - # WEBHOOKS # - ############ - @abc.abstractmethod - async def create_webhook(self, payload: more_typing.JSONObject) -> webhooks.Webhook: - ... - - def _make_user(self, payload): - user_obj = users.User(app=self.app) - user_obj.id = bases.Snowflake(payload["id"]) - user_obj.discriminator = payload["discriminator"] - user_obj.username = payload["username"] - user_obj.avatar_hash = payload["avatar"] - user_obj.is_bot = payload.get("bot", False) - user_obj.is_system = payload.get("system", False) - user_obj.flags = payload.get("bot", False) - return user_obj diff --git a/hikari/api/entity_factory.py b/hikari/api/entity_factory.py new file mode 100644 index 0000000000..093f7e3692 --- /dev/null +++ b/hikari/api/entity_factory.py @@ -0,0 +1,30 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# Copyright © Nekoka.tt 2019-2020 +# +# This file is part of Hikari. +# +# Hikari is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Hikari is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Hikari. If not, see . +"""Contains an interface for components wishing to build entities.""" +from __future__ import annotations + +__all__ = ["IEntityFactory"] + +import abc + + +class IEntityFactory(abc.ABC): + """Interface for an entity factory implementation.""" + + __slots__ = () diff --git a/hikari/api/event_consumer.py b/hikari/api/event_consumer.py new file mode 100644 index 0000000000..1c40e88e09 --- /dev/null +++ b/hikari/api/event_consumer.py @@ -0,0 +1,33 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# Copyright © Nekoka.tt 2019-2020 +# +# This file is part of Hikari. +# +# Hikari is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Hikari is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Hikari. If not, see . +from __future__ import annotations + +__all__ = ["IEventConsumer"] + +import abc + +from hikari.gateway import client +from hikari.internal import more_typing + + +class IEventConsumer(abc.ABC): + __slots__ = () + + async def consume_raw_event(self, shard: client.GatewayClient, event_name: str, payload: more_typing.JSONType): + ... diff --git a/hikari/api/event_dispatcher.py b/hikari/api/event_dispatcher.py new file mode 100644 index 0000000000..798a6976f6 --- /dev/null +++ b/hikari/api/event_dispatcher.py @@ -0,0 +1,36 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# Copyright © Nekoka.tt 2019-2020 +# +# This file is part of Hikari. +# +# Hikari is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Hikari is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Hikari. If not, see . +from __future__ import annotations + +__all__ = ["IEventDispatcher"] + +import abc +import typing + +if typing.TYPE_CHECKING: + from hikari.events import base + from hikari.internal import more_typing + + +class IEventDispatcher(abc.ABC): + __slots__ = () + + @abc.abstractmethod + def dispatch(self, event: base.HikariEvent) -> more_typing.Future[typing.Any]: + ... diff --git a/hikari/api/gateway_dispatcher.py b/hikari/api/gateway_dispatcher.py new file mode 100644 index 0000000000..59f45cdc3c --- /dev/null +++ b/hikari/api/gateway_dispatcher.py @@ -0,0 +1,48 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# Copyright © Nekoka.tt 2019-2020 +# +# This file is part of Hikari. +# +# Hikari is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Hikari is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Hikari. If not, see . +from __future__ import annotations + +__all__ = ["IGatewayDispatcher"] + +import abc +import typing + +from hikari.api import base_app + +if typing.TYPE_CHECKING: + from hikari.api import event_dispatcher + + +class IGatewayDispatcher(base_app.IBaseApp, abc.ABC): + """Component specialization that supports dispatching of events. + + These events are expected to be instances of + `hikari.events.base.HikariEvent`. + + This may be combined with `IGatewayZookeeper` for most single-process + bots, or may be a specific component for large distributed applications + that consume events from a message queue, for example. + """ + + __slots__ = () + + @property + @abc.abstractmethod + def event_dispatcher(self) -> event_dispatcher.IEventDispatcher: + """Event dispatcher and waiter.""" diff --git a/hikari/api/gateway_zookeeper.py b/hikari/api/gateway_zookeeper.py new file mode 100644 index 0000000000..89fb189279 --- /dev/null +++ b/hikari/api/gateway_zookeeper.py @@ -0,0 +1,152 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# Copyright © Nekoka.tt 2019-2020 +# +# This file is part of Hikari. +# +# Hikari is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Hikari is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Hikari. If not, see . +from __future__ import annotations + +__all__ = ["IGatewayZookeeper"] + +import abc +import asyncio +import contextlib +import datetime +import signal +import typing + +from hikari.api import base_app +from hikari.models import gateway +from hikari.models import guilds + +if typing.TYPE_CHECKING: + from hikari.api import event_consumer + from hikari.gateway import client + + +class IGatewayZookeeper(base_app.IBaseApp, abc.ABC): + """Component specialization that looks after a set of shards. + + These events will be produced by a low-level gateway implementation, and + will produce `list` and `dict` types only. + q + This may be combined with `IGatewayDispatcher` for most single-process + bots, or may be a specific component for large distributed applications + that feed new events into a message queue, for example. + """ + + __slots__ = () + + @property + @abc.abstractmethod + def event_consumer(self) -> event_consumer.IEventConsumer: + """Raw event consumer.""" + + @property + @abc.abstractmethod + def gateway_shards(self) -> typing.Mapping[int, client.GatewayClient]: + """Mapping of each shard ID to the corresponding client for it.""" + + @property + @abc.abstractmethod + def shard_count(self) -> int: + """The number of shards in the entire distributed application.""" + + @abc.abstractmethod + async def start(self) -> None: + """Start all shards and wait for them to be READY.""" + + @abc.abstractmethod + async def join(self) -> None: + """Wait for all shards to shut down.""" + + @abc.abstractmethod + async def update_presence( + self, + *, + status: guilds.PresenceStatus = ..., + activity: typing.Optional[gateway.Activity] = ..., + idle_since: typing.Optional[datetime.datetime] = ..., + is_afk: bool = ..., + ) -> None: + """Update the presence of the user for all shards. + + This will only update arguments that you explicitly specify a value for. + Any arguments that you do not explicitly provide some value for will + not be changed. + + !!! warning + This will only apply to connected shards. + + !!! note + If you wish to update a presence for a specific shard, you can do + this by using the `gateway_shards` `typing.Mapping` to find the + shard you wish to update. + + Parameters + ---------- + status : hikari.models.guilds.PresenceStatus + If specified, the new status to set. + activity : hikari.models.gateway.Activity | None + If specified, the new activity to set. + idle_since : datetime.datetime | None + If specified, the time to show up as being idle since, + or `None` if not applicable. + is_afk : bool + If specified, `True` if the user should be marked as AFK, + or `False` otherwise. + """ + + def run(self) -> None: + """Execute this component on an event loop. + + Performs the same job as `RunnableClient.start`, but provides additional + preparation such as registering OS signal handlers for interrupts, + and preparing the initial event loop. + + This enables the client to be run immediately without having to + set up the `asyncio` event loop manually first. + """ + loop = asyncio.get_event_loop() + + def sigterm_handler(*_): + raise KeyboardInterrupt() + + ex = None + + try: + with contextlib.suppress(NotImplementedError): + # Not implemented on Windows + loop.add_signal_handler(signal.SIGTERM, sigterm_handler) + + loop.run_until_complete(self.start()) + loop.run_until_complete(self.join()) + + self.logger.info("client has shut down") + + except KeyboardInterrupt as _ex: + self.logger.info("received signal to shut down client") + loop.run_until_complete(self.close()) + # Apparently you have to alias except clauses or you get an + # UnboundLocalError. + ex = _ex + finally: + loop.run_until_complete(self.close()) + with contextlib.suppress(NotImplementedError): + # Not implemented on Windows + loop.remove_signal_handler(signal.SIGTERM) + + if ex: + raise ex from ex diff --git a/hikari/api/rest_app.py b/hikari/api/rest_app.py new file mode 100644 index 0000000000..2249ed4120 --- /dev/null +++ b/hikari/api/rest_app.py @@ -0,0 +1,42 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# Copyright © Nekoka.tt 2019-2020 +# +# This file is part of Hikari. +# +# Hikari is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Hikari is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Hikari. If not, see . +from __future__ import annotations + +__all__ = ["IRESTApp"] + +import abc + +from hikari.api import base_app +from hikari.rest import client + + +class IRESTApp(base_app.IBaseApp, abc.ABC): + """Component specialization that is used for REST-only applications. + + Examples may include web dashboards, or applications where no gateway + connection is required. As a result, no event conduit is provided by + these implementations. + """ + + __slots__ = () + + @property + @abc.abstractmethod + def rest(self) -> client.RESTClient: + """REST API.""" diff --git a/hikari/application.py b/hikari/application.py deleted file mode 100644 index 771a3626ae..0000000000 --- a/hikari/application.py +++ /dev/null @@ -1,373 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# Copyright © Nekoka.tt 2019-2020 -# -# This file is part of Hikari. -# -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . -"""A bot client might go here... eventually...""" - -from __future__ import annotations - -__all__ = ["Application"] - -import abc -import asyncio -import datetime -import inspect -import logging -import math -import os -import platform -import time -import typing - -from hikari import _about -from hikari import configs -from hikari.events import other as other_events -from hikari.internal import conversions -from hikari.internal import helpers - -from hikari.gateway import dispatchers -from hikari.gateway import event_managers -from hikari.gateway import runnable - -if typing.TYPE_CHECKING: - from hikari.events import base as event_base - from hikari.gateway import client as gateway_client - from hikari.internal import more_typing - from hikari.models import gateway as gateway_models - from hikari.models import guilds - from hikari.models import intents as intents_ - from hikari.rest import client as rest_client - - -class Application(runnable.RunnableClient, dispatchers.EventDispatcher, abc.ABC): - """An abstract base class for a bot implementation. - - Parameters - ---------- - config : hikari.configs.BotConfig - The config object to use. - **kwargs - Parameters to use to create a `hikari.configs.BotConfig` - from, instead of passing a raw config object. - - Examples - -------- - # You can use it like this: - config = hikari.BotConfig(token="...", ...) - bot = hikari.StatelessBot(config=config) - - # Or like this: - bot = hikari.StatelessBot(token="...", ...) - - """ - - logger: logging.Logger - """The logger to use for this bot.""" - - def __init__(self, *, config: typing.Optional[configs.BotConfig] = None, **kwargs: typing.Any) -> None: - if not bool(config) ^ bool(kwargs): - raise TypeError("You must only specify a config object OR kwargs.") - - runnable.RunnableClient.__init__(self, helpers.get_logger(self)) - - self._is_shutting_down = False - self.config = configs.BotConfig(**kwargs) if config is None else config - self.event_dispatcher = self._create_event_dispatcher(self.config) - self.event_manager = self._create_event_manager(self) - self.rest = self._create_rest(self) - self.shards = None - - @property - def heartbeat_latency(self) -> float: - """Average heartbeat latency for all valid shards. - - This will return a mean of all the heartbeat intervals for all shards - with a valid heartbeat latency that are in the - `hikari.gateway.gateway_state.GatewayState.READY` state. - - If no shards are in this state, this will return `float("nan")` - instead. - - Returns - ------- - float - The mean latency for all `READY` shards that have sent at least - one acknowledged `HEARTBEAT` payload. If there is not at least - one shard that meets this criteria, this will instead return - `float("nan")`. - """ - latencies = [] - for shard in self.shards.values(): - if not math.isnan(shard.heartbeat_latency): - latencies.append(shard.heartbeat_latency) - - return sum(latencies) / len(latencies) if latencies else float("nan") - - @property - def total_disconnect_count(self) -> int: - """Total number of times any shard has disconnected.""" - return sum(s.disconnect_count for s in self.shards.values()) - - @property - def total_reconnect_count(self) -> int: - """Total number of times any shard has reconnected.""" - return sum(s.reconnect_count for s in self.shards.values()) - - @property - def intents(self) -> typing.Optional[intents_.Intent]: # noqa: D401 - """Intents that are in use for the connection. - - If intents are not being used at all, then this will be `None` instead. - """ - return self.config.intents - - @property - def version(self) -> float: - """Version being used for the gateway API.""" - return self.config.gateway_version - - # noinspection PyTypeChecker,PyUnresolvedReferences - async def start(self) -> None: - """Start the bot. - - This will query Discord for the optimal number of shards to use if - you did not provide an explicit sharding configuration. - - Each required shard is then started up incrementally at a rate that - reduces the chance of `INVALID_SESSION` spam occurring. After each - shard websocket has fired the `READY` event, this coroutine will return. - - After invoking this coroutine, you should keep the application alive - by awaiting the `join` coroutine in this class. - """ - if self.shards: # pylint: disable=access-member-before-definition - raise RuntimeError("Bot is already running.") - self._is_shutting_down = False - - version = _about.__version__ - path = os.path.abspath(os.path.dirname(inspect.getsourcefile(_about))) - py_impl = platform.python_implementation() - py_ver = platform.python_version() - py_compiler = platform.python_compiler() - self.logger.info( - "hikari v%s (installed in %s) (%s %s %s)", version, path, py_impl, py_ver, py_compiler, - ) - - await self._calculate_shards() - - self.logger.info("starting %s", conversions.pluralize(len(self.shards), "shard")) - - start_time = time.perf_counter() - - for i, shard_id in enumerate(self.shards): - if i > 0: - self.logger.info("idling for 5 seconds to avoid an invalid session") - await asyncio.sleep(5) - - shard_obj = self.shards[shard_id] - await shard_obj.start() - - finish_time = time.perf_counter() - - self.logger.info("started %s shard(s) in approx %.2fs", len(self.shards), finish_time - start_time) - - if self.event_manager is not None: - await self.dispatch_event(other_events.StartedEvent()) - - async def _calculate_shards(self): - gateway_bot = await self.rest.fetch_gateway_bot() - - self.logger.info( - "you have sent an IDENTIFY %s time(s) before now, and have %s remaining. This will reset at %s.", - gateway_bot.session_start_limit.total - gateway_bot.session_start_limit.remaining, - gateway_bot.session_start_limit.remaining, - datetime.datetime.now() + gateway_bot.session_start_limit.reset_after, - ) - - shard_count = self.config.shard_count if self.config.shard_count else gateway_bot.shard_count - shard_ids = self.config.shard_ids if self.config.shard_ids else range(shard_count) - url = gateway_bot.url - - self.logger.info("will connect shards to %s", url) - - shard_clients: typing.Dict[int, gateway_client.GatewayClient] = {} - for shard_id in shard_ids: - shard = self._create_shard(self, shard_id, shard_count, url) - shard_clients[shard_id] = shard - - self.shards = shard_clients # pylint: disable=attribute-defined-outside-init - - async def join(self) -> None: - """Wait for each shard to terminate, then return.""" - await asyncio.gather(*(shard_obj.join() for shard_obj in self.shards.values())) - - async def close(self) -> None: - try: - if self.shards and not self._is_shutting_down: - self._is_shutting_down = True - self.logger.info("stopping %s shard(s)", len(self.shards)) - start_time = time.perf_counter() - try: - await self.dispatch_event(other_events.StoppingEvent()) - await asyncio.gather(*(shard_obj.close() for shard_obj in self.shards.values())) - finally: - finish_time = time.perf_counter() - self.logger.info("stopped %s shard(s) in approx %.2fs", len(self.shards), finish_time - start_time) - await self.dispatch_event(other_events.StoppedEvent()) - finally: - await self.rest.close() - - def add_listener( - self, event_type: typing.Type[dispatchers.EventT], callback: dispatchers.EventCallbackT, **kwargs - ) -> dispatchers.EventCallbackT: - return self.event_dispatcher.add_listener(event_type, callback, _stack_level=4) - - def remove_listener( - self, event_type: typing.Type[dispatchers.EventT], callback: dispatchers.EventCallbackT - ) -> dispatchers.EventCallbackT: - return self.event_dispatcher.remove_listener(event_type, callback) - - def wait_for( - self, - event_type: typing.Type[dispatchers.EventT], - *, - timeout: typing.Optional[float] = None, - predicate: typing.Optional[dispatchers.PredicateT] = None, - ) -> more_typing.Future[typing.Any]: - return self.event_dispatcher.wait_for(event_type, timeout=timeout, predicate=predicate) - - def dispatch_event(self, event: event_base.HikariEvent) -> more_typing.Future[typing.Any]: - return self.event_dispatcher.dispatch_event(event) - - async def update_presence( - self, - *, - status: guilds.PresenceStatus = ..., - activity: typing.Optional[gateway_models.Activity] = ..., - idle_since: typing.Optional[datetime.datetime] = ..., - is_afk: bool = ..., - ) -> None: - """Update the presence of the user for all shards. - - This will only update arguments that you explicitly specify a value for. - Any arguments that you do not explicitly provide some value for will - not be changed. - - !!! warning - This will only apply to connected shards. - - !!! note - If you wish to update a presence for a specific shard, you can do this - by using the `shards` `typing.Mapping` to find the shard you wish to - update. - - Parameters - ---------- - status : hikari.models.guilds.PresenceStatus - If specified, the new status to set. - activity : hikari.models.gateway.Activity | None - If specified, the new activity to set. - idle_since : datetime.datetime | None - If specified, the time to show up as being idle since, - or `None` if not applicable. - is_afk : bool - If specified, `True` if the user should be marked as AFK, - or `False` otherwise. - """ - await asyncio.gather( - *( - s.update_presence(status=status, activity=activity, idle_since=idle_since, is_afk=is_afk) - for s in self.shards.values() - if s.connection_state in (status.ShardState.WAITING_FOR_READY, status.ShardState.READY) - ) - ) - - @staticmethod - @abc.abstractmethod - def _create_shard(app: Application, shard_id: int, shard_count: int, url: str) -> gateway_client.GatewayClient: - """Return a new shard for the given parameters. - - Parameters - ---------- - app : hikari.components.application.Application - The app to register. - shard_id : int - The shard ID to use. - shard_count : int - The shard count to use. - url : str - The gateway URL to connect to. - - !!! note - The `shard_id` and `shard_count` may be set within the `config` - object passed, but any conforming implementations are expected to - use the value passed in the `shard_id` and `shard_count` parameters - regardless. Failure to do so may result in an invalid sharding - configuration being used. - - Returns - ------- - hikari.gateway.client.GatewayClient - The shard client implementation to use for the given shard ID. - """ - - @staticmethod - @abc.abstractmethod - def _create_rest(app: Application) -> rest_client.RESTClient: - """Return a new RESTSession client from the given configuration. - - Parameters - ---------- - app : hikari.components.application.Application - The application to register. - - Returns - ------- - hikari.components.rest.RESTClient - The RESTSession client to use. - """ - - @staticmethod - @abc.abstractmethod - def _create_event_manager(app: Application) -> event_managers.EventManager: - """Return a new instance of an event manager implementation. - - Parameters - ---------- - app : hikari.components.application.Application - The application to register. - - Returns - ------- - hikari.state.event_managers.EventManager - The event manager to use internally. - """ - - @staticmethod - @abc.abstractmethod - def _create_event_dispatcher(config: configs.BotConfig) -> dispatchers.EventDispatcher: - """Return a new instance of an event dispatcher implementation. - - Parameters - ---------- - config : hikari.configs.BotConfig - The bot config to use. - - Returns - ------- - hikari.state.dispatchers.EventDispatcher - """ diff --git a/hikari/configs.py b/hikari/configs.py deleted file mode 100644 index 997cf77afc..0000000000 --- a/hikari/configs.py +++ /dev/null @@ -1,578 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# Copyright © Nekoka.tt 2019-2020 -# -# This file is part of Hikari. -# -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . -"""Configuration data classes.""" - -from __future__ import annotations - -__all__ = [ - "BaseConfig", - "DebugConfig", - "AIOHTTPConfig", - "TokenConfig", - "GatewayConfig", - "RESTConfig", - "BotConfig", -] - -import datetime -import re -import typing - -import aiohttp -import attr - -from hikari.internal import conversions -from hikari.internal import marshaller -from hikari.internal import urls -from hikari.models import gateway as gateway_models -from hikari.models import guilds -from hikari.models import intents as intents_ - -if typing.TYPE_CHECKING: - import ssl - - -@marshaller.marshallable() -@attr.s(kw_only=True, repr=False) -class BaseConfig(marshaller.Deserializable): - """Base class for any configuration data class.""" - - -@marshaller.marshallable() -@attr.s(kw_only=True, repr=False) -class DebugConfig(BaseConfig): - """Configuration for anything with a debugging mode. - - Attributes - ---------- - debug : bool - Whether to enable debugging mode. Usually you don't want to enable this. - """ - - debug: bool = marshaller.attrib(deserializer=bool, if_undefined=False, default=False) - - -@marshaller.marshallable() -@attr.s(kw_only=True, repr=False) -class AIOHTTPConfig(BaseConfig): - """Config for application that use AIOHTTP somewhere. - - Attributes - ---------- - allow_redirects : bool - If `True`, allow following redirects from `3xx` HTTP responses. - Generally you do not want to enable this unless you have a good reason to. - Defaults to `False` if unspecified during deserialization. - proxy_auth : aiohttp.BasicAuth | None - Optional proxy authorization to provide in any HTTP requests. - This is deserialized using the format `"basic {{base 64 string here}}"`. - Defaults to `None` if unspecified during deserialization. - proxy_headers : typing.Mapping[str, str] | None - Optional proxy headers to provide in any HTTP requests. - Defaults to `None` if unspecified during deserialization. - proxy_url : str | None - The optional URL of the proxy to send requests via. - Defaults to `None` if unspecified during deserialization. - request_timeout : float | None - Optional request timeout to use. If an HTTP request takes longer than - this, it will be aborted. - If not `None`, the value represents a number of seconds as a floating - point number. - Defaults to `None` if unspecified during deserialization. - ssl_context : ssl.SSLContext | None - The optional SSL context to use. - This is deserialized as an object reference in the format - `package.module#object.attribute` that is expected to point to the - desired value. - Defaults to `None` if unspecified during deserialization. - tcp_connector : aiohttp.TCPConnector | None - This may otherwise be `None` to use the default settings provided by - `aiohttp`. - This is deserialized as an object reference in the format - `package.module#object.attribute` that is expected to point to the - desired value. - Defaults to `None` if unspecified during deserialization. - trust_env: bool - If `True`, and no proxy info is given, then `HTTP_PROXY` and - `HTTPS_PROXY` will be used from the environment variables if present. - Any proxy credentials will be read from the user's `netrc` file - (https://www.gnu.org/software/inetutils/manual/html_node/The-_002enetrc-file.html) - If `False`, then this information is instead ignored. - Defaults to `False` if unspecified. - verify_ssl : bool - If `True`, then responses with invalid SSL certificates will be - rejected. Generally you want to keep this enabled unless you have a - problem with SSL and you know exactly what you are doing by disabling - this. Disabling SSL verification can have major security implications. - You turn this off at your own risk. - Defaults to `True` if unspecified during deserialization. - """ - - allow_redirects: bool = marshaller.attrib(deserializer=bool, if_undefined=False, default=False) - - tcp_connector: typing.Optional[aiohttp.TCPConnector] = marshaller.attrib( - deserializer=marshaller.dereference_handle, if_none=None, if_undefined=None, default=None - ) - - proxy_auth: typing.Optional[aiohttp.BasicAuth] = marshaller.attrib( - deserializer=aiohttp.BasicAuth.decode, if_none=None, if_undefined=None, default=None - ) - - proxy_headers: typing.Optional[typing.Mapping[str, str]] = marshaller.attrib( - deserializer=dict, if_none=None, if_undefined=None, default=None - ) - - proxy_url: typing.Optional[str] = marshaller.attrib(deserializer=str, if_undefined=None, if_none=None, default=None) - - request_timeout: typing.Optional[float] = marshaller.attrib( - deserializer=float, if_undefined=None, if_none=None, default=None - ) - - ssl_context: typing.Optional[ssl.SSLContext] = marshaller.attrib( - deserializer=marshaller.dereference_handle, if_none=None, if_undefined=None, default=None - ) - - trust_env: bool = marshaller.attrib(deserializer=bool, if_undefined=True, default=False) - - verify_ssl: bool = marshaller.attrib(deserializer=bool, if_undefined=True, default=True) - - -@marshaller.marshallable() -@attr.s(kw_only=True, repr=False) -class TokenConfig(BaseConfig): - """Token config options. - - Attributes - ---------- - token : str | None - The token to use. - """ - - token: typing.Optional[str] = marshaller.attrib(deserializer=str, if_none=None, if_undefined=None, default=None) - """The token to use.""" - - -def _parse_shard_info(payload): - range_matcher = re.search(r"(\d+)\s*(\.{2,3})\s*(\d+)", payload) if isinstance(payload, str) else None - - if not range_matcher: - if isinstance(payload, int): - return [payload] - - if isinstance(payload, list): - return payload - - raise ValueError('expected shard_ids to be one of int, list of int, or range string ("x..y" or "x...y")') - - minimum, range_mod, maximum = range_matcher.groups() - minimum, maximum = int(minimum), int(maximum) - if len(range_mod) == 3: - maximum += 1 - - return [*range(minimum, maximum)] - - -def _gateway_version_default() -> int: - return 6 - - -def _initial_status_default() -> typing.Literal[guilds.PresenceStatus.ONLINE]: - return guilds.PresenceStatus.ONLINE - - -def _deserialize_intents(value) -> intents_.Intent: - return conversions.dereference_int_flag(intents_.Intent, value) - - -def _large_threshold_default() -> int: - return 250 - - -@marshaller.marshallable() -@attr.s(kw_only=True, repr=False) -class GatewayConfig(AIOHTTPConfig, DebugConfig, TokenConfig): - """Single-websocket specific configuration options. - - Attributes - ---------- - allow_redirects : bool - If `True`, allow following redirects from `3xx` HTTP responses. - Generally you do not want to enable this unless you have a good reason to. - Defaults to `False` if unspecified during deserialization. - debug : bool - Whether to enable debugging mode. Usually you don't want to enable this. - gateway_use_compression : bool - Whether to use zlib compression on the gateway for inbound messages. - Usually you want this turned on. - gateway_version : int - The gateway API version to use. Defaults to v6 - initial_activity : hikari.models.gateway.Activity | None - The initial activity to set all shards to when starting the gateway. - If this is `None` then no activity will be set, this is the default. - initial_status : hikari.models.guilds.PresenceStatus - The initial status to set the shards to when starting the gateway. - Defaults to `ONLINE`. - initial_is_afk : bool - Whether to show up as AFK or not on sign-in. - initial_idle_since : datetime.datetime | None - The idle time to show on signing in. - If set to `None` to not show an idle time, this is the default. - intents : hikari.models.intents.Intent - The intents to use for the connection. - If being deserialized, this can be an integer bitfield, or a sequence of - intent names. If unspecified, this will be set to `None`. - large_threshold : int - The large threshold to use. - proxy_headers : typing.Mapping[str, str] | None - Optional proxy headers to provide in any HTTP requests. - Defaults to `None` if unspecified during deserialization. - proxy_auth : aiohttp.BasicAuth | None - Optional proxy authorization to provide in any HTTP requests. - This is deserialized using the format `"basic {{base 64 string here}}"`. - Defaults to `None` if unspecified during deserialization. - proxy_url : str | None - The optional URL of the proxy to send requests via. - Defaults to `None` if unspecified during deserialization. - request_timeout : float | None - Optional request timeout to use. If an HTTP request takes longer than - this, it will be aborted. - If not `None`, the value represents a number of seconds as a floating - point number. - Defaults to `None` if unspecified during deserialization. - shard_count : int | None - The number of shards the entire distributed application should consists - of. If you run multiple distributed instances of the bot, you should - ensure this value is consistent. - This can be set to `None` to enable auto-sharding. This is the default. - shard_id : typing.Sequence[int] | None - The shard IDs to produce shard connections for. - If being deserialized, this can be several formats shown in `notes`. - ssl_context : ssl.SSLContext | None - The optional SSL context to use. - This is deserialized as an object reference in the format - `package.module#object.attribute` that is expected to point to the - desired value. - Defaults to `None` if unspecified during deserialization. - tcp_connector : aiohttp.TCPConnector | None - This may otherwise be `None` to use the default settings provided by - `aiohttp`. - This is deserialized as an object reference in the format - `package.module#object.attribute` that is expected to point to the - desired value. - Defaults to `None` if unspecified during deserialization. - token : str | None - The token to use. - verify_ssl : bool - If `True`, then responses with invalid SSL certificates will be - rejected. Generally you want to keep this enabled unless you have a - problem with SSL and you know exactly what you are doing by disabling - this. Disabling SSL verification can have major security implications. - You turn this off at your own risk. - Defaults to `True` if unspecified during deserialization. - - !!! note - The several formats for `shard_id` are as follows: - - * A specific shard ID (e.g. `12`); - * A sequence of shard IDs (e.g. `[0, 1, 2, 3, 8, 9, 10]`); - * A range string. Two periods indicate a range of `[5, 16]` - (inclusive beginning, exclusive end). - * A range string. Three periods indicate a range of - `[5, 17]` (inclusive beginning, inclusive end); - * `None` this means `shard_count` will be considered and that many - shards will be created for you. If the `shard_count` is also - `None` then auto-sharding will be performed for you. - - !!! note - - If being deserialized, `intents` can be an integer bitfield, or a - sequence of intent names. If unspecified, `intents` will be set to - `None`. - - See `hikari.models.intents.Intent` for valid names of intents you - can use. Integer values are as documented on Discord's developer portal. - - !!! warning - If you are using the V7 gateway implementation, you will NEED to provide - explicit `intents` values for this field in order to get online. - Additionally, intents that are classed by Discord as being privileged - will require you to whitelist your application in order to use them. - - If you are using the V6 gateway implementation, setting `intents` to - `None` will simply opt you into every event you can subscribe to. - """ - - gateway_use_compression: bool = marshaller.attrib(deserializer=bool, if_undefined=True, default=True) - - gateway_version: int = marshaller.attrib(deserializer=int, if_undefined=_gateway_version_default, default=6) - - initial_activity: typing.Optional[gateway_models.Activity] = marshaller.attrib( - deserializer=gateway_models.Activity.deserialize, if_none=None, if_undefined=None, default=None - ) - - initial_status: guilds.PresenceStatus = marshaller.attrib( - deserializer=guilds.PresenceStatus, if_undefined=_initial_status_default, default=guilds.PresenceStatus.ONLINE, - ) - - initial_is_afk: bool = marshaller.attrib(deserializer=bool, if_undefined=False, default=False) - - initial_idle_since: typing.Optional[datetime.datetime] = marshaller.attrib( - deserializer=datetime.datetime.fromtimestamp, if_none=None, if_undefined=None, default=None - ) - - intents: typing.Optional[intents_.Intent] = marshaller.attrib( - deserializer=_deserialize_intents, if_undefined=None, default=None, - ) - - large_threshold: int = marshaller.attrib(deserializer=int, if_undefined=_large_threshold_default, default=250) - - shard_ids: typing.Optional[typing.Sequence[int]] = marshaller.attrib( - deserializer=_parse_shard_info, if_none=None, if_undefined=None, default=None - ) - - shard_count: typing.Optional[int] = marshaller.attrib(deserializer=int, if_undefined=None, default=None) - - -def _token_type_default() -> str: - return "Bot" - - -def _rest_version_default() -> int: - return 6 - - -def _rest_url_default() -> str: - return urls.REST_API_URL - - -def _oauth2_url_default() -> str: - return urls.OAUTH2_API_URL - - -@marshaller.marshallable() -@attr.s(kw_only=True, repr=False) -class RESTConfig(AIOHTTPConfig, DebugConfig, TokenConfig): - """Single-websocket specific configuration options. - - Attributes - ---------- - allow_redirects : bool - If `True`, allow following redirects from `3xx` HTTP responses. - Generally you do not want to enable this unless you have a good reason to. - Defaults to `False` if unspecified during deserialization. - oauth2_url : str - Can be specified to override the default URL for the Discord OAuth2 API. - Generally there is no reason to need to specify this, but it can be - useful for testing, amongst other things. - rest_url : str - Can be specified to override the default URL for the Discord API itself. - Generally there is no reason to need to specify this, but it can be - useful for testing, amongst other things. - You can put format-string placeholders in the URL such as `{0.version}` - to interpolate the chosen API version to use. - proxy_headers : typing.Mapping[str, str] | None - Optional proxy headers to provide in any HTTP requests. - Defaults to `None` if unspecified during deserialization. - proxy_auth : aiohttp.BasicAuth | None - Optional proxy authorization to provide in any HTTP requests. - This is deserialized using the format `"basic {{base 64 string here}}"`. - Defaults to `None` if unspecified during deserialization. - proxy_url : str | None - The optional URL of the proxy to send requests via. - Defaults to `None` if unspecified during deserialization. - request_timeout : float | None - Optional request timeout to use. If an HTTP request takes longer than - this, it will be aborted. - If not `None`, the value represents a number of seconds as a floating - point number. - Defaults to `None` if unspecified during deserialization. - ssl_context : ssl.SSLContext | None - The optional SSL context to use. - This is deserialized as an object reference in the format - `package.module#object.attribute` that is expected to point to the - desired value. - Defaults to `None` if unspecified during deserialization. - tcp_connector : aiohttp.TCPConnector | None - This may otherwise be `None` to use the default settings provided by - `aiohttp`. - This is deserialized as an object reference in the format - `package.module#object.attribute` that is expected to point to the - desired value. - Defaults to `None` if unspecified during deserialization. - verify_ssl : bool - If `True`, then responses with invalid SSL certificates will be - rejected. Generally you want to keep this enabled unless you have a - problem with SSL and you know exactly what you are doing by disabling - this. Disabling SSL verification can have major security implications. - You turn this off at your own risk. - Defaults to `True` if unspecified during deserialization. - token : str | None - The token to use. - debug : bool - Whether to enable debugging mode. Usually you don't want to enable this. - token_type : str | None - Token authentication scheme, this defaults to `"Bot"` and should be - one of `"Bot"` or `"Bearer"`, or `None` if not relevant. - rest_version : int - The HTTP API version to use. If unspecified, then V7 is used. - """ - - oauth2_url: str = marshaller.attrib( - deserializer=str, if_undefined=_oauth2_url_default, default=_oauth2_url_default() - ) - - rest_url: str = marshaller.attrib(deserializer=str, if_undefined=_rest_url_default, default=_rest_url_default()) - - rest_version: int = marshaller.attrib( - deserializer=int, if_undefined=_rest_version_default, default=_rest_version_default() - ) - - token_type: typing.Optional[str] = marshaller.attrib( - deserializer=str, if_undefined=_token_type_default, if_none=None, default="Bot" - ) - - -@marshaller.marshallable() -@attr.s(kw_only=True, repr=False) -class BotConfig(RESTConfig, GatewayConfig): - """Configuration for a standard bot. - - Attributes - ---------- - allow_redirects : bool - If `True`, allow following redirects from `3xx` HTTP responses. - Generally you do not want to enable this unless you have a good reason to. - Defaults to `False` if unspecified during deserialization. - debug : bool - Whether to enable debugging mode. Usually you don't want to enable this. - gateway_use_compression : bool - Whether to use zlib compression on the gateway for inbound messages. - Usually you want this turned on. - gateway_version : int - The gateway API version to use. Defaults to v6 - initial_activity : hikari.models.gateway.Activity | None - The initial activity to set all shards to when starting the gateway. - If this is `None` then no activity will be set, this is the default. - initial_status : hikari.models.guilds.PresenceStatus - The initial status to set the shards to when starting the gateway. - Defaults to `ONLINE`. - initial_is_afk : bool - Whether to show up as AFK or not on sign-in. - initial_idle_since : datetime.datetime | None - The idle time to show on signing in. - If set to `None` to not show an idle time, this is the default. - intents : hikari.models.intents.Intent - The intents to use for the connection. - If being deserialized, this can be an integer bitfield, or a sequence of - intent names. If unspecified, this will be set to `None`. - large_threshold : int - The large threshold to use. - oauth2_url : str - Can be specified to override the default URL for the Discord OAuth2 API. - Generally there is no reason to need to specify this, but it can be - useful for testing, amongst other things. - proxy_headers : typing.Mapping[str, str] | None - Optional proxy headers to provide in any HTTP requests. - Defaults to `None` if unspecified during deserialization. - proxy_auth : aiohttp.BasicAuth | None - Optional proxy authorization to provide in any HTTP requests. - This is deserialized using the format `"basic {{base 64 string here}}"`. - Defaults to `None` if unspecified during deserialization. - proxy_url : str | None - The optional URL of the proxy to send requests via. - Defaults to `None` if unspecified during deserialization. - request_timeout : float | None - Optional request timeout to use. If an HTTP request takes longer than - this, it will be aborted. - If not `None`, the value represents a number of seconds as a floating - point number. - Defaults to `None` if unspecified during deserialization. - rest_url : str - Can be specified to override the default URL for the Discord API itself. - Generally there is no reason to need to specify this, but it can be - useful for testing, amongst other things. - You can put format-string placeholders in the URL such as `{0.version}` - to interpolate the chosen API version to use. - rest_version : int - The HTTP API version to use. If unspecified, then V7 is used. - shard_count : int | None - The number of shards the entire distributed application should consists - of. If you run multiple distributed instances of the bot, you should - ensure this value is consistent. - This can be set to `None` to enable auto-sharding. This is the default. - shard_id : typing.Sequence[int] | None - The shard IDs to produce shard connections for. - If being deserialized, this can be several formats shown in `notes`. - ssl_context : ssl.SSLContext | None - The optional SSL context to use. - This is deserialized as an object reference in the format - `package.module#object.attribute` that is expected to point to the - desired value. - Defaults to `None` if unspecified during deserialization. - tcp_connector : aiohttp.TCPConnector | None - This may otherwise be `None` to use the default settings provided by - `aiohttp`. - This is deserialized as an object reference in the format - `package.module#object.attribute` that is expected to point to the - desired value. - Defaults to `None` if unspecified during deserialization. - token : str | None - The token to use. - token_type : str | None - Token authentication scheme, this defaults to `"Bot"` and should be - one of `"Bot"` or `"Bearer"`, or `None` if not relevant. - verify_ssl : bool - If `True`, then responses with invalid SSL certificates will be - rejected. Generally you want to keep this enabled unless you have a - problem with SSL and you know exactly what you are doing by disabling - this. Disabling SSL verification can have major security implications. - You turn this off at your own risk. - Defaults to `True` if unspecified during deserialization. - - !!! note - The several formats for `shard_id` are as follows: - - * A specific shard ID (e.g. `12`); - * A sequence of shard IDs (e.g. `[0, 1, 2, 3, 8, 9, 10]`); - * A range string. Two periods indicate a range of `[5, 16]` - (inclusive beginning, exclusive end). - * A range string. Three periods indicate a range of - `[5, 17]` (inclusive beginning, inclusive end); - * `None` this means `shard_count` will be considered and that many - shards will be created for you. If the `shard_count` is also - `None` then auto-sharding will be performed for you. - - !!! note - - If being deserialized, `intents` can be an integer bitfield, or a - sequence of intent names. If unspecified, `intents` will be set to - `None`. - - See `hikari.models.intents.Intent` for valid names of intents you - can use. Integer values are as documented on Discord's developer portal. - - !!! warning - If you are using the V7 gateway implementation, you will NEED to provide - explicit `intents` values for this field in order to get online. - Additionally, intents that are classed by Discord as being privileged - will require you to whitelist your application in order to use them. - - If you are using the V6 gateway implementation, setting `intents` to - `None` will simply opt you into every event you can subscribe to. - """ diff --git a/hikari/events/channel.py b/hikari/events/channel.py index 2b33c14f65..3b71bf8b79 100644 --- a/hikari/events/channel.py +++ b/hikari/events/channel.py @@ -46,7 +46,6 @@ from hikari.models import intents from hikari.models import invites from hikari.models import users - from . import base as base_events if typing.TYPE_CHECKING: diff --git a/hikari/events/guild.py b/hikari/events/guild.py index 101c2bc70c..6f2fb49264 100644 --- a/hikari/events/guild.py +++ b/hikari/events/guild.py @@ -52,7 +52,6 @@ from hikari.models import intents from hikari.models import unset from hikari.models import users - from . import base as base_events if typing.TYPE_CHECKING: diff --git a/hikari/events/message.py b/hikari/events/message.py index fcf5e988fe..eba3fdaf76 100644 --- a/hikari/events/message.py +++ b/hikari/events/message.py @@ -46,7 +46,6 @@ from hikari.models import messages from hikari.models import unset from hikari.models import users - from . import base as base_events if typing.TYPE_CHECKING: diff --git a/hikari/events/other.py b/hikari/events/other.py index 089e9dafb4..3e61e17928 100644 --- a/hikari/events/other.py +++ b/hikari/events/other.py @@ -41,7 +41,6 @@ from hikari.models import bases as base_models from hikari.models import guilds from hikari.models import users - from . import base as base_events if typing.TYPE_CHECKING: diff --git a/hikari/events/voice.py b/hikari/events/voice.py index 76bfbeecda..f7c2c3c588 100644 --- a/hikari/events/voice.py +++ b/hikari/events/voice.py @@ -28,7 +28,6 @@ from hikari.models import bases as base_models from hikari.models import intents from hikari.models import voices - from . import base as base_events diff --git a/hikari/gateway/client.py b/hikari/gateway/client.py index 416f521550..9a785744c9 100644 --- a/hikari/gateway/client.py +++ b/hikari/gateway/client.py @@ -35,20 +35,19 @@ import aiohttp +from hikari import aiohttp_config from hikari import errors +from hikari.api import gateway_zookeeper +from hikari.gateway import connection as gateway_connection +from hikari.gateway import gateway_state +from hikari.gateway import runnable from hikari.internal import codes from hikari.internal import helpers from hikari.internal import ratelimits -from . import connection as gateway_connection -from . import gateway_state -from . import runnable - - if typing.TYPE_CHECKING: import datetime - from hikari import application from hikari.models import gateway from hikari.models import guilds from hikari.models import intents as intents_ @@ -85,7 +84,7 @@ class GatewayClient(runnable.RunnableClient): __slots__ = ( "_activity", - "_app", + "_zookeeper", "_connection", "_idle_since", "_is_afk", @@ -96,46 +95,61 @@ class GatewayClient(runnable.RunnableClient): "logger", ) - def __init__(self, shard_id: int, shard_count: int, app: application.Application, url: str) -> None: + def __init__( + self, + *, + config: aiohttp_config.AIOHTTPConfig, + debug: bool, + initial_activity: typing.Optional[gateway.Activity], + initial_idle_since: typing.Optional[datetime.datetime], + initial_is_afk: bool, + initial_status: guilds.PresenceStatus, + intents: typing.Optional[intents_.Intent], + large_threshold: int, + shard_id: int, + shard_count: int, + token: str, + url: str, + use_compression: bool, + version: int, + zookeeper: gateway_zookeeper.IGatewayZookeeper, + ) -> None: super().__init__(helpers.get_logger(self, str(shard_id))) - self._app = app - self._raw_event_consumer = app.event_manager - self._activity = app.config.initial_activity - self._idle_since = app.config.initial_idle_since - self._is_afk = app.config.initial_is_afk - self._status = app.config.initial_status + self._zookeeper = zookeeper + self._activity = initial_activity + self._idle_since = initial_idle_since + self._is_afk = initial_is_afk + self._status = initial_status self._shard_state = gateway_state.GatewayState.NOT_RUNNING self._task = None self._connection = gateway_connection.Shard( - compression=app.config.gateway_use_compression, - connector=app.config.tcp_connector, - debug=app.config.debug, + compression=use_compression, + connector=config.tcp_connector, + debug=debug, # This is a bit of a cheat, we should pass a coroutine function here, but # instead we just use a lambda that does the transformation we want (replaces the # low-level shard argument with the reference to this class object), then return # the result of that coroutine. To the low level client, it looks the same :-) # (also hides a useless stack frame from tracebacks, I guess). - dispatcher=lambda c, n, pl: app.event_manager.process_raw_event(self, n, pl), + # FIXME: implement dispatch. + dispatcher=lambda c, n, pl: self._zookeeper.event_consumer.consume_raw_event(self, n, pl), initial_presence=self._create_presence_pl( - status=app.config.initial_status, - activity=app.config.initial_activity, - idle_since=app.config.initial_idle_since, - is_afk=app.config.initial_is_afk, + status=initial_status, activity=initial_activity, idle_since=initial_idle_since, is_afk=initial_is_afk, ), - intents=app.config.intents, - large_threshold=app.config.large_threshold, - proxy_auth=app.config.proxy_auth, - proxy_headers=app.config.proxy_headers, - proxy_url=app.config.proxy_url, + intents=intents, + large_threshold=large_threshold, + proxy_auth=config.proxy_auth, + proxy_headers=config.proxy_headers, + proxy_url=config.proxy_url, session_id=None, seq=None, shard_id=shard_id, shard_count=shard_count, - ssl_context=app.config.ssl_context, - token=app.config.token, + ssl_context=config.ssl_context, + token=token, url=url, - verify_ssl=app.config.verify_ssl, - version=app.config.gateway_version, + verify_ssl=config.verify_ssl, + version=version, ) @property diff --git a/hikari/gateway/connection.py b/hikari/gateway/connection.py index bc538e3715..96b8bbfa9a 100644 --- a/hikari/gateway/connection.py +++ b/hikari/gateway/connection.py @@ -47,13 +47,13 @@ import aiohttp.typedefs +from hikari import errors from hikari.internal import codes from hikari.internal import http_client from hikari.internal import more_asyncio from hikari.internal import more_typing from hikari.internal import ratelimits from hikari.internal import user_agents -from hikari import errors if typing.TYPE_CHECKING: import ssl @@ -63,7 +63,6 @@ DispatcherT = typing.Callable[["Shard", str, typing.Dict], more_typing.Coroutine[None]] """The signature for an event dispatch callback.""" - VERSION_6: typing.Final[int] = 6 VERSION_7: typing.Final[int] = 7 @@ -305,7 +304,7 @@ def __init__( # pylint: disable=too-many-locals compression: bool = True, connector: typing.Optional[aiohttp.BaseConnector] = None, debug: bool = False, - dispatcher: DispatcherT = lambda gw, e, p: None, + dispatcher: DispatcherT, initial_presence: typing.Optional[typing.Dict] = None, intents: typing.Optional[_intents.Intent] = None, json_deserialize: typing.Callable[[typing.AnyStr], typing.Dict] = json.loads, diff --git a/hikari/gateway/dispatchers.py b/hikari/gateway/dispatchers.py index 385af995d3..74585c0697 100644 --- a/hikari/gateway/dispatchers.py +++ b/hikari/gateway/dispatchers.py @@ -170,7 +170,7 @@ async def on_create_delete(event): Returns ------- - decorator(T) -> T + decorator(ComponentImplT) -> ComponentImplT A decorator for a function that registers the given event. Raises diff --git a/hikari/gateway/event_managers.py b/hikari/gateway/event_managers.py index a6e200f89f..fa7c4867fa 100644 --- a/hikari/gateway/event_managers.py +++ b/hikari/gateway/event_managers.py @@ -26,16 +26,14 @@ import logging import typing -from . import consumers -from . import dispatchers +from hikari.gateway import consumers +from hikari.gateway import dispatchers if typing.TYPE_CHECKING: from hikari import application - + from hikari.gateway import client as gateway_client from hikari.internal import more_typing - from . import client as gateway_client - EVENT_MARKER_ATTR: typing.Final[str] = "___event_name___" EventConsumerT = typing.Callable[[str, typing.Mapping[str, str]], typing.Awaitable[None]] @@ -52,7 +50,7 @@ def raw_event_mapper(name: str) -> typing.Callable[[EventConsumerT], EventConsum Returns ------- - decorator(T) -> T + decorator(ComponentImplT) -> ComponentImplT A decorator for a method. """ diff --git a/hikari/gateway/intent_aware_dispatchers.py b/hikari/gateway/intent_aware_dispatchers.py index 528f8e530d..6430613a64 100644 --- a/hikari/gateway/intent_aware_dispatchers.py +++ b/hikari/gateway/intent_aware_dispatchers.py @@ -28,13 +28,11 @@ import warnings from hikari import errors - from hikari.events import base as event_base from hikari.events import other as other_events from hikari.internal import more_asyncio from hikari.internal import more_collections from hikari.models import intents - from . import dispatchers if typing.TYPE_CHECKING: diff --git a/hikari/impl/__init__.py b/hikari/impl/__init__.py new file mode 100644 index 0000000000..1c1502a5ca --- /dev/null +++ b/hikari/impl/__init__.py @@ -0,0 +1,18 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# Copyright © Nekoka.tt 2019-2020 +# +# This file is part of Hikari. +# +# Hikari is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Hikari is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Hikari. If not, see . diff --git a/hikari/impl/bot.py b/hikari/impl/bot.py new file mode 100644 index 0000000000..fdeb3a68fc --- /dev/null +++ b/hikari/impl/bot.py @@ -0,0 +1,130 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# Copyright © Nekoka.tt 2019-2020 +# +# This file is part of Hikari. +# +# Hikari is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Hikari is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Hikari. If not, see . + +from __future__ import annotations + +import logging +import typing +from concurrent import futures + +from hikari.api import bot +from hikari.impl import cache as cache_impl +from hikari.impl import entity_factory as entity_factory_impl +from hikari.impl import event_manager +from hikari.impl import gateway_zookeeper +from hikari.internal import helpers +from hikari.internal import urls +from hikari.models import guilds +from hikari.rest import client as rest_client_ + +if typing.TYPE_CHECKING: + import datetime + + from hikari.api import cache as cache_ + from hikari.api import entity_factory as entity_factory_ + from hikari.api import event_consumer as event_consumer_ + from hikari import aiohttp_config + from hikari.api import event_dispatcher + from hikari.api import gateway_zookeeper + from hikari.models import gateway + from hikari.models import intents as intents_ + + +class BotImpl(gateway_zookeeper.AbstractGatewayZookeeper, bot.IBot): + def __init__( + self, + *, + config: aiohttp_config.AIOHTTPConfig, + debug: bool = False, + gateway_url: str, + gateway_version: int = 6, + initial_activity: typing.Optional[gateway.Activity] = None, + initial_idle_since: typing.Optional[datetime.datetime] = None, + initial_is_afk: bool = False, + initial_status: guilds.PresenceStatus = guilds.PresenceStatus.ONLINE, + intents: typing.Optional[intents_.Intent] = None, + large_threshold: int = 250, + rest_version: int = 6, + rest_url: str = urls.REST_API_URL, + shard_ids: typing.Set[int], + shard_count: int, + token: str, + use_compression: bool = True, + ): + self._logger = helpers.get_logger(self) + + super().__init__( + config=config, + debug=debug, + initial_activity=initial_activity, + initial_idle_since=initial_idle_since, + initial_is_afk=initial_is_afk, + initial_status=initial_status, + intents=intents, + large_threshold=large_threshold, + shard_ids=shard_ids, + shard_count=shard_count, + token=token, + url=gateway_url, + use_compression=use_compression, + version=gateway_version, + ) + + self._rest = rest_client_.RESTClient( + app=self, + config=config, + debug=debug, + token=token, + token_type="Bot", + rest_url=rest_url, + version=rest_version, + ) + + self._cache = cache_impl.CacheImpl() + self._event_manager = event_manager.EventManagerImpl() + self._entity_factory = entity_factory_impl.EntityFactoryImpl() + + @property + def event_dispatcher(self) -> event_dispatcher.IEventDispatcher: + return self._event_manager + + @property + def logger(self) -> logging.Logger: + return self._logger + + @property + def cache(self) -> cache_.ICache: + return self._cache + + @property + def entity_factory(self) -> entity_factory_.IEntityFactory: + return self._entity_factory + + @property + def thread_pool(self) -> typing.Optional[futures.ThreadPoolExecutor]: + # XXX: fixme + return None + + @property + def rest(self) -> rest_client_.RESTClient: + return self._rest + + @property + def event_consumer(self) -> event_consumer_.IEventConsumer: + return self._event_manager diff --git a/hikari/impl/cache.py b/hikari/impl/cache.py new file mode 100644 index 0000000000..cf482e7d99 --- /dev/null +++ b/hikari/impl/cache.py @@ -0,0 +1,205 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# Copyright © Nekoka.tt 2019-2020 +# +# This file is part of Hikari. +# +# Hikari is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Hikari is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Hikari. If not, see . +import typing + +from hikari.api import cache +from hikari.internal import more_typing +from hikari.models import applications +from hikari.models import audit_logs +from hikari.models import channels +from hikari.models import embeds +from hikari.models import emojis +from hikari.models import gateway +from hikari.models import guilds +from hikari.models import invites +from hikari.models import messages +from hikari.models import users +from hikari.models import voices + + +class CacheImpl(cache.ICache): + async def create_application(self, payload: more_typing.JSONObject) -> applications.Application: + pass + + async def create_own_guild(self, payload: more_typing.JSONObject) -> applications.OwnGuild: + pass + + async def create_own_connection(self, payload: more_typing.JSONObject) -> applications.OwnConnection: + pass + + async def create_audit_log_change(self, payload: more_typing.JSONObject) -> audit_logs.AuditLogChange: + pass + + async def create_audit_log_entry_info(self, payload: more_typing.JSONObject) -> audit_logs.BaseAuditLogEntryInfo: + pass + + async def create_audit_log_entry(self, payload: more_typing.JSONObject) -> audit_logs.AuditLogEntry: + pass + + async def create_audit_log(self, payload: more_typing.JSONObject) -> audit_logs.AuditLog: + pass + + async def create_channel(self, payload: more_typing.JSONObject, can_cache: bool = False) -> channels.PartialChannel: + pass + + async def update_channel( + self, channel: channels.PartialChannel, payload: more_typing.JSONObject + ) -> channels.PartialChannel: + pass + + async def get_channel(self, channel_id: int) -> typing.Optional[channels.PartialChannel]: + pass + + async def delete_channel(self, channel_id: int) -> typing.Optional[channels.PartialChannel]: + pass + + async def create_embed(self, payload: more_typing.JSONObject) -> embeds.Embed: + pass + + async def create_emoji(self, payload: more_typing.JSONObject, can_cache: bool = False) -> emojis.Emoji: + pass + + async def update_emoji(self, payload: more_typing.JSONObject) -> emojis.Emoji: + pass + + async def get_emoji(self, emoji_id: int) -> typing.Optional[emojis.KnownCustomEmoji]: + pass + + async def delete_emoji(self, emoji_id: int) -> typing.Optional[emojis.KnownCustomEmoji]: + pass + + async def create_gateway_bot(self, payload: more_typing.JSONObject) -> gateway.GatewayBot: + pass + + async def create_member(self, payload: more_typing.JSONObject, can_cache: bool = False) -> guilds.GuildMember: + pass + + async def update_member(self, member: guilds.GuildMember, payload: more_typing.JSONObject) -> guilds.GuildMember: + pass + + async def get_member(self, guild_id: int, user_id: int) -> typing.Optional[guilds.GuildMember]: + pass + + async def delete_member(self, guild_id: int, user_id: int) -> typing.Optional[guilds.GuildMember]: + pass + + async def create_role(self, payload: more_typing.JSONObject, can_cache: bool = False) -> guilds.PartialGuildRole: + pass + + async def update_role( + self, role: guilds.PartialGuildRole, payload: more_typing.JSONObject + ) -> guilds.PartialGuildRole: + pass + + async def get_role(self, guild_id: int, role_id: int) -> typing.Optional[guilds.PartialGuildRole]: + pass + + async def delete_role(self, guild_id: int, role_id: int) -> typing.Optional[guilds.PartialGuildRole]: + pass + + async def create_presence( + self, payload: more_typing.JSONObject, can_cache: bool = False + ) -> guilds.GuildMemberPresence: + pass + + async def update_presence( + self, role: guilds.GuildMemberPresence, payload: more_typing.JSONObject + ) -> guilds.GuildMemberPresence: + pass + + async def get_presence(self, guild_id: int, user_id: int) -> typing.Optional[guilds.GuildMemberPresence]: + pass + + async def delete_presence(self, guild_id: int, user_id: int) -> typing.Optional[guilds.GuildMemberPresence]: + pass + + async def create_guild_ban(self, payload: more_typing.JSONObject) -> guilds.GuildMemberBan: + pass + + async def create_guild_integration(self, payload: more_typing.JSONObject) -> guilds.PartialGuildIntegration: + pass + + async def create_guild(self, payload: more_typing.JSONObject, can_cache: bool = False) -> guilds.PartialGuild: + pass + + async def update_guild(self, guild: guilds.PartialGuild, payload: more_typing.JSONObject) -> guilds.PartialGuild: + pass + + async def get_guild(self, guild_id: int) -> typing.Optional[guilds.PartialGuild]: + pass + + async def delete_guild(self, guild_id: int) -> typing.Optional[guilds.PartialGuild]: + pass + + async def create_guild_preview(self, payload: more_typing.JSONObject) -> guilds.GuildPreview: + pass + + async def create_invite(self, payload: more_typing.JSONObject) -> invites.Invite: + pass + + async def create_reaction(self, payload: more_typing.JSONObject) -> messages.Reaction: + pass + + async def create_message(self, payload: more_typing.JSONObject, can_cache: bool = False) -> messages.Message: + pass + + async def update_message(self, message: messages.Message, payload: more_typing.JSONObject) -> messages.Message: + pass + + async def get_message(self, channel_id: int, message_id: int) -> typing.Optional[messages.Message]: + pass + + async def delete_message(self, channel_id: int, message_id: int) -> typing.Optional[messages.Message]: + pass + + async def create_user(self, payload: more_typing.JSONObject, can_cache: bool = False) -> users.User: + pass + + async def update_user(self, user: users.User, payload: more_typing.JSONObject) -> users.User: + pass + + async def get_user(self, user_id: int) -> typing.Optional[users.User]: + pass + + async def delete_user(self, user_id: int) -> typing.Optional[users.User]: + pass + + async def create_my_user(self, payload: more_typing.JSONObject, can_cache: bool = False) -> users.MyUser: + pass + + async def update_my_user(self, my_user: users.MyUser, payload: more_typing.JSONObject) -> users.MyUser: + pass + + async def get_my_user(self) -> typing.Optional[users.User]: + pass + + async def create_voice_state(self, payload: more_typing.JSONObject, can_cache: bool = False) -> voices.VoiceState: + pass + + async def update_voice_state(self, payload: more_typing.JSONObject) -> voices.VoiceState: + pass + + async def get_voice_state(self, guild_id: int, channel_id: int) -> typing.Optional[voices.VoiceState]: + pass + + async def delete_voice_state(self, guild_id: int, channel_id: int) -> typing.Optional[voices.VoiceState]: + pass + + async def create_voice_region(self, payload: more_typing.JSONObject) -> voices.VoiceRegion: + pass diff --git a/hikari/impl/entity_factory.py b/hikari/impl/entity_factory.py new file mode 100644 index 0000000000..7d662dbe9f --- /dev/null +++ b/hikari/impl/entity_factory.py @@ -0,0 +1,26 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# Copyright © Nekoka.tt 2019-2020 +# +# This file is part of Hikari. +# +# Hikari is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Hikari is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Hikari. If not, see . + +from __future__ import annotations + +from hikari.api import entity_factory + + +class EntityFactoryImpl(entity_factory.IEntityFactory): + pass diff --git a/hikari/impl/event_manager.py b/hikari/impl/event_manager.py new file mode 100644 index 0000000000..5e6607be9f --- /dev/null +++ b/hikari/impl/event_manager.py @@ -0,0 +1,31 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# Copyright © Nekoka.tt 2019-2020 +# +# This file is part of Hikari. +# +# Hikari is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Hikari is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Hikari. If not, see . +import typing + +from hikari.api import event_consumer +from hikari.api import event_dispatcher +from hikari.events import base +from hikari.internal import more_asyncio +from hikari.internal import more_typing + + +class EventManagerImpl(event_dispatcher.IEventDispatcher, event_consumer.IEventConsumer): + def dispatch(self, event: base.HikariEvent) -> more_typing.Future[typing.Any]: + # TODO: this + return more_asyncio.completed_future() diff --git a/hikari/impl/gateway_zookeeper.py b/hikari/impl/gateway_zookeeper.py new file mode 100644 index 0000000000..18db398012 --- /dev/null +++ b/hikari/impl/gateway_zookeeper.py @@ -0,0 +1,151 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# Copyright © Nekoka.tt 2019-2020 +# +# This file is part of Hikari. +# +# Hikari is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Hikari is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Hikari. If not, see . +from __future__ import annotations + +import abc +import asyncio +import time +import typing + +from hikari.api import event_dispatcher +from hikari.api import gateway_zookeeper +from hikari.events import other +from hikari.gateway import client +from hikari.internal import conversions + +if typing.TYPE_CHECKING: + import datetime + + from hikari import aiohttp_config + from hikari.models import gateway + from hikari.models import guilds + from hikari.models import intents as intents_ + + +class AbstractGatewayZookeeper(gateway_zookeeper.IGatewayZookeeper, abc.ABC): + def __init__( + self, + *, + config: aiohttp_config.AIOHTTPConfig, + debug: bool, + initial_activity: typing.Optional[gateway.Activity], + initial_idle_since: typing.Optional[datetime.datetime], + initial_is_afk: bool, + initial_status: guilds.PresenceStatus, + intents: typing.Optional[intents_.Intent], + large_threshold: int, + shard_ids: typing.Set[int], + shard_count: int, + token: str, + url: str, + use_compression: bool, + version: int, + ) -> None: + self._aiohttp_config = config + self._url = url + self._shard_count = shard_count + self._shards = { + shard_id: client.GatewayClient( + config=config, + debug=debug, + initial_activity=initial_activity, + initial_idle_since=initial_idle_since, + initial_is_afk=initial_is_afk, + initial_status=initial_status, + intents=intents, + large_threshold=large_threshold, + shard_id=shard_id, + shard_count=shard_count, + token=token, + url=url, + use_compression=use_compression, + version=version, + zookeeper=self, + ) + for shard_id in shard_ids + } + + @property + def gateway_shards(self) -> typing.Mapping[int, client.GatewayClient]: + return self._shards + + @property + def shard_count(self) -> int: + return self._shard_count + + async def start(self) -> None: + self.logger.info("starting %s", conversions.pluralize(len(self._shards), "shard")) + + start_time = time.perf_counter() + + for i, shard_id in enumerate(self._shards): + if i > 0: + self.logger.info("idling for 5 seconds to avoid an invalid session") + await asyncio.sleep(5) + + shard_obj = self._shards[shard_id] + await shard_obj.start() + + finish_time = time.perf_counter() + + self.logger.info("started %s shard(s) in approx %.2fs", len(self._shards), finish_time - start_time) + + if hasattr(self, "event_dispatcher") and isinstance(self.event_dispatcher, event_dispatcher.IEventDispatcher): + await self.event_dispatcher.dispatch(other.StartedEvent()) + + async def join(self) -> None: + await asyncio.gather(*(shard_obj.join() for shard_obj in self._shards.values())) + + async def close(self) -> None: + self.logger.info("stopping %s shard(s)", len(self._shards)) + start_time = time.perf_counter() + + has_event_dispatcher = hasattr(self, "event_dispatcher") and isinstance( + self.event_dispatcher, event_dispatcher.IEventDispatcher + ) + + try: + if has_event_dispatcher: + # noinspection PyUnresolvedReferences + await self.event_dispatcher.dispatch(other.StoppingEvent()) + + await asyncio.gather(*(shard_obj.close() for shard_obj in self._shards.values())) + finally: + finish_time = time.perf_counter() + self.logger.info("stopped %s shard(s) in approx %.2fs", len(self._shards), finish_time - start_time) + + if has_event_dispatcher: + # noinspection PyUnresolvedReferences + await self.event_dispatcher.dispatch(other.StoppedEvent()) + + async def update_presence( + self, + *, + status: guilds.PresenceStatus = ..., + activity: typing.Optional[gateway.Activity] = ..., + idle_since: typing.Optional[datetime.datetime] = ..., + is_afk: bool = ..., + ) -> None: + await asyncio.gather( + *( + s.update_presence(status=status, activity=activity, idle_since=idle_since, is_afk=is_afk) + for s in self._shards.values() + if s.connection_state in (status.ShardState.WAITING_FOR_READY, status.ShardState.READY) + ) + ) diff --git a/hikari/impl/rest_app.py b/hikari/impl/rest_app.py new file mode 100644 index 0000000000..7be06a3e9b --- /dev/null +++ b/hikari/impl/rest_app.py @@ -0,0 +1,84 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# Copyright © Nekoka.tt 2019-2020 +# +# This file is part of Hikari. +# +# Hikari is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Hikari is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Hikari. If not, see . + +from __future__ import annotations + +import logging +import typing +from concurrent import futures + +from hikari import aiohttp_config +from hikari.api import rest_app +from hikari.impl import cache as cache_impl +from hikari.impl import entity_factory as entity_factory_impl +from hikari.internal import helpers +from hikari.internal import urls +from hikari.rest import client as rest_client_ + +if typing.TYPE_CHECKING: + from hikari.api import cache as cache_ + from hikari.api import entity_factory as entity_factory_ + + +class RESTAppImpl(rest_app.IRESTApp): + def __init__( + self, + config: aiohttp_config.AIOHTTPConfig, + debug: bool = False, + token: typing.Optional[str] = None, + token_type: typing.Optional[str] = None, + rest_url: str = urls.REST_API_URL, + version: int = 6, + ) -> None: + self._logger = helpers.get_logger(self) + self._rest = rest_client_.RESTClient( + app=self, + config=config, + debug=debug, + token=token, + token_type=token_type, + rest_url=rest_url, + version=version, + ) + self._cache = cache_impl.CacheImpl() + self._entity_factory = entity_factory_impl.EntityFactoryImpl() + + @property + def logger(self) -> logging.Logger: + return self._logger + + async def close(self) -> None: + await self._rest.close() + + @property + def thread_pool(self) -> typing.Optional[futures.ThreadPoolExecutor]: + # XXX: fixme + return None + + @property + def rest(self) -> rest_client_.RESTClient: + return self._rest + + @property + def cache(self) -> cache_.ICache: + return self._cache + + @property + def entity_factory(self) -> entity_factory_.IEntityFactory: + return self._entity_factory diff --git a/hikari/internal/conversions.py b/hikari/internal/conversions.py index a55ff65207..36132431aa 100644 --- a/hikari/internal/conversions.py +++ b/hikari/internal/conversions.py @@ -58,7 +58,9 @@ DISCORD_EPOCH: typing.Final[int] = 1_420_070_400 ISO_8601_DATE_PART: typing.Final[typing.Pattern] = re.compile(r"^(\d{4})-(\d{2})-(\d{2})") -ISO_8601_TIME_PART: typing.Final[typing.Pattern] = re.compile(r"T(\d{2}):(\d{2}):(\d{2})(?:\.(\d{1,6}))?", re.I) +ISO_8601_TIME_PART: typing.Final[typing.Pattern] = re.compile( + r"ComponentImplT(\d{2}):(\d{2}):(\d{2})(?:\.(\d{1,6}))?", re.I +) ISO_8601_TZ_PART: typing.Final[typing.Pattern] = re.compile(r"([+-])(\d{2}):(\d{2})$") diff --git a/hikari/internal/helpers.py b/hikari/internal/helpers.py index 68cebcc716..9d4288ee96 100644 --- a/hikari/internal/helpers.py +++ b/hikari/internal/helpers.py @@ -26,7 +26,6 @@ import typing from hikari.models import bases - from . import more_collections if typing.TYPE_CHECKING: diff --git a/hikari/internal/ratelimits.py b/hikari/internal/ratelimits.py index dfacfd8c0f..92253ca974 100644 --- a/hikari/internal/ratelimits.py +++ b/hikari/internal/ratelimits.py @@ -176,7 +176,6 @@ if typing.TYPE_CHECKING: from . import more_typing - UNKNOWN_HASH: typing.Final[str] = "UNKNOWN" """The hash used for an unknown bucket that has not yet been resolved.""" diff --git a/hikari/internal/urls.py b/hikari/internal/urls.py index c0902476c3..572a175429 100644 --- a/hikari/internal/urls.py +++ b/hikari/internal/urls.py @@ -27,7 +27,6 @@ import typing import urllib.parse - BASE_CDN_URL: typing.Final[str] = "https://cdn.discordapp.com" """The URL for the CDN.""" diff --git a/hikari/models/applications.py b/hikari/models/applications.py index c6b04542c0..bb248ede02 100644 --- a/hikari/models/applications.py +++ b/hikari/models/applications.py @@ -38,7 +38,6 @@ from hikari.internal import marshaller from hikari.internal import more_enums from hikari.internal import urls - from . import bases from . import guilds from . import permissions diff --git a/hikari/models/audit_logs.py b/hikari/models/audit_logs.py index 94060a374e..d9287027ca 100644 --- a/hikari/models/audit_logs.py +++ b/hikari/models/audit_logs.py @@ -50,7 +50,6 @@ from hikari.internal import marshaller from hikari.internal import more_collections from hikari.internal import more_enums - from . import bases from . import channels from . import colors @@ -286,7 +285,7 @@ def register_audit_log_entry_info( Returns ------- - decorator(T) -> T + decorator(ComponentImplT) -> ComponentImplT The decorator to decorate the class with. """ diff --git a/hikari/models/bases.py b/hikari/models/bases.py index 41823514bd..cd8cfee38b 100644 --- a/hikari/models/bases.py +++ b/hikari/models/bases.py @@ -127,4 +127,4 @@ def __int__(self) -> int: return int(self.id) -T = typing.TypeVar("T", bound=Unique) +T = typing.TypeVar("ComponentImplT", bound=Unique) diff --git a/hikari/models/channels.py b/hikari/models/channels.py index 0701f03e9e..b1d96c42ef 100644 --- a/hikari/models/channels.py +++ b/hikari/models/channels.py @@ -46,7 +46,6 @@ from hikari.internal import more_collections from hikari.internal import more_enums from hikari.internal import urls - from . import bases from . import permissions from . import users @@ -135,7 +134,7 @@ def register_channel_type( Returns ------- - decorator(T) -> T + decorator(ComponentImplT) -> ComponentImplT The decorator to decorate the class with. """ diff --git a/hikari/models/embeds.py b/hikari/models/embeds.py index b393c35550..ff7826a4e6 100644 --- a/hikari/models/embeds.py +++ b/hikari/models/embeds.py @@ -38,7 +38,6 @@ from hikari.internal import conversions from hikari.internal import marshaller - from . import bases from . import colors from . import files diff --git a/hikari/models/emojis.py b/hikari/models/emojis.py index 5e2988662c..632f7fafe9 100644 --- a/hikari/models/emojis.py +++ b/hikari/models/emojis.py @@ -30,7 +30,6 @@ from hikari.internal import marshaller from hikari.internal import urls - from . import bases from . import files from . import users diff --git a/hikari/models/files.py b/hikari/models/files.py index 66a5d53a4d..e69d335b61 100644 --- a/hikari/models/files.py +++ b/hikari/models/files.py @@ -66,8 +66,8 @@ import aiohttp -from hikari.internal import more_asyncio from hikari import errors +from hikari.internal import more_asyncio # XXX: find optimal size. MAGIC_NUMBER: typing.Final[int] = 128 * 1024 diff --git a/hikari/models/gateway.py b/hikari/models/gateway.py index 19de548588..f9a9ed6cd9 100644 --- a/hikari/models/gateway.py +++ b/hikari/models/gateway.py @@ -28,7 +28,6 @@ import attr from hikari.internal import marshaller - from . import bases from . import guilds diff --git a/hikari/models/guilds.py b/hikari/models/guilds.py index a94a55f8e0..2639539cb8 100644 --- a/hikari/models/guilds.py +++ b/hikari/models/guilds.py @@ -63,7 +63,6 @@ from hikari.internal import marshaller from hikari.internal import more_enums from hikari.internal import urls - from . import bases from . import channels as channels_ from . import colors diff --git a/hikari/models/invites.py b/hikari/models/invites.py index 19a826ee3c..0ea067b24e 100644 --- a/hikari/models/invites.py +++ b/hikari/models/invites.py @@ -31,7 +31,6 @@ from hikari.internal import marshaller from hikari.internal import more_enums from hikari.internal import urls - from . import bases from . import channels from . import guilds diff --git a/hikari/models/pagination.py b/hikari/models/pagination.py index ff8a8075ab..9fdf99e6c0 100644 --- a/hikari/models/pagination.py +++ b/hikari/models/pagination.py @@ -24,7 +24,6 @@ import typing from hikari.internal import more_collections - from . import bases _T = typing.TypeVar("_T") @@ -118,7 +117,7 @@ def enumerate(self, *, start: int = 0) -> PaginatedResults[typing.Tuple[int, _T] Returns ------- - PaginatedResults[typing.Tuple[int, T]] + PaginatedResults[typing.Tuple[int, ComponentImplT]] A paginated results view that asynchronously yields an increasing counter in a tuple with each result, lazily. """ @@ -140,7 +139,7 @@ def limit(self, limit: int) -> PaginatedResults[_T]: Returns ------- - PaginatedResults[T] + PaginatedResults[ComponentImplT] A paginated results view that asynchronously yields a maximum of the given number of items before completing. """ diff --git a/hikari/models/unset.py b/hikari/models/unset.py index 6b38d07de4..55901cd617 100644 --- a/hikari/models/unset.py +++ b/hikari/models/unset.py @@ -47,7 +47,7 @@ def __init_subclass__(cls, **kwargs: typing.Any) -> typing.NoReturn: raise TypeError("Cannot subclass Unset type") -T = typing.TypeVar("T") +T = typing.TypeVar("ComponentImplT") MayBeUnset = typing.Union[T, Unset] UNSET: typing.Final[Unset] = Unset() diff --git a/hikari/models/webhooks.py b/hikari/models/webhooks.py index 688ab7e637..c80329d569 100644 --- a/hikari/models/webhooks.py +++ b/hikari/models/webhooks.py @@ -29,7 +29,6 @@ from hikari.internal import marshaller from hikari.internal import more_enums from hikari.internal import urls - from . import bases from . import users as users_ diff --git a/hikari/rest/base.py b/hikari/rest/base.py index 1040ce93d0..a65872aaf3 100644 --- a/hikari/rest/base.py +++ b/hikari/rest/base.py @@ -30,8 +30,8 @@ if typing.TYPE_CHECKING: import types - from hikari import application - from . import session as rest_session + from hikari import api + from hikari.rest import session as rest_session class BaseRESTComponent(abc.ABC, metaclass=meta.UniqueFunctionMeta): @@ -43,7 +43,7 @@ class BaseRESTComponent(abc.ABC, metaclass=meta.UniqueFunctionMeta): """ @abc.abstractmethod - def __init__(self, app: application.Application, session: rest_session.RESTSession) -> None: + def __init__(self, app: api.IRESTApp, session: rest_session.RESTSession) -> None: self._app = app self._session = session @@ -59,6 +59,13 @@ async def close(self) -> None: """Shut down the RESTSession client safely.""" await self._session.close() + # XXX: my plan is to abolish the low level objects soon and integrate their logic in here + # directly, thus I want this gone before 2.0.0. + @property + def session(self) -> rest_session.RESTSession: + """The low-level REST API session.""" + return self._session + @property def global_ratelimit_queue_size(self) -> int: """Count of API calls waiting for the global ratelimiter to release. diff --git a/hikari/rest/buckets.py b/hikari/rest/buckets.py index 737b7df46e..3d57e535af 100644 --- a/hikari/rest/buckets.py +++ b/hikari/rest/buckets.py @@ -30,8 +30,7 @@ from hikari.internal import more_asyncio from hikari.internal import more_typing from hikari.internal import ratelimits - -from . import routes +from hikari.rest import routes UNKNOWN_HASH: typing.Final[str] = "UNKNOWN" """The hash used for an unknown bucket that has not yet been resolved.""" diff --git a/hikari/rest/channel.py b/hikari/rest/channel.py index a7436679b5..4655a668a2 100644 --- a/hikari/rest/channel.py +++ b/hikari/rest/channel.py @@ -33,8 +33,7 @@ from hikari.models import messages as messages_ from hikari.models import pagination from hikari.models import webhooks - -from . import base +from hikari.rest import base if typing.TYPE_CHECKING: from hikari.models import embeds as embeds_ diff --git a/hikari/rest/client.py b/hikari/rest/client.py index 7b101a290d..6a4fc55d77 100644 --- a/hikari/rest/client.py +++ b/hikari/rest/client.py @@ -28,20 +28,19 @@ import typing -from . import channel -from . import gateway -from . import guild -from . import invite -from . import me -from . import oauth2 -from . import react -from . import session -from . import user -from . import voice -from . import webhook - -if typing.TYPE_CHECKING: - from hikari import application +from hikari import aiohttp_config +from hikari.api import rest_app +from hikari.rest import channel +from hikari.rest import gateway +from hikari.rest import guild +from hikari.rest import invite +from hikari.rest import me +from hikari.rest import oauth2 +from hikari.rest import react +from hikari.rest import session +from hikari.rest import user +from hikari.rest import voice +from hikari.rest import webhook class RESTClient( @@ -75,24 +74,33 @@ class RESTClient( additional characters being cut off. """ - def __init__(self, app: application.Application) -> None: - token = None - if app.config.token_type is not None: - token = f"{app.config.token_type} {app.config.token}" + def __init__( + self, + *, + app: rest_app.IRESTApp, + config: aiohttp_config.AIOHTTPConfig, + debug: bool, + token: typing.Optional[str], + token_type: typing.Optional[str], + rest_url, + version, + ) -> None: + if token_type is not None: + token = f"{token_type} {token}" super().__init__( app, session.RESTSession( - allow_redirects=app.config.allow_redirects, - base_url=app.config.rest_url, - connector=app.config.tcp_connector, - debug=app.config.debug, - proxy_headers=app.config.proxy_headers, - proxy_auth=app.config.proxy_auth, - ssl_context=app.config.ssl_context, - verify_ssl=app.config.verify_ssl, - timeout=app.config.request_timeout, + allow_redirects=config.allow_redirects, + base_url=rest_url, + connector=config.tcp_connector, + debug=debug, + proxy_headers=config.proxy_headers, + proxy_auth=config.proxy_auth, + ssl_context=config.ssl_context, + verify_ssl=config.verify_ssl, + timeout=config.request_timeout, token=token, - trust_env=app.config.trust_env, - version=app.config.rest_version, + trust_env=config.trust_env, + version=version, ), ) diff --git a/hikari/rest/gateway.py b/hikari/rest/gateway.py index d6810929ea..bb48f21e73 100644 --- a/hikari/rest/gateway.py +++ b/hikari/rest/gateway.py @@ -25,8 +25,7 @@ import abc from hikari.models import gateway - -from . import base +from hikari.rest import base class RESTGatewayComponent(base.BaseRESTComponent, abc.ABC): # pylint: disable=abstract-method diff --git a/hikari/rest/guild.py b/hikari/rest/guild.py index 847c46501a..b5e1930907 100644 --- a/hikari/rest/guild.py +++ b/hikari/rest/guild.py @@ -36,8 +36,7 @@ from hikari.models import pagination from hikari.models import voices from hikari.models import webhooks - -from . import base +from hikari.rest import base if typing.TYPE_CHECKING: from hikari.models import colors diff --git a/hikari/rest/invite.py b/hikari/rest/invite.py index c1207ca685..f27e8b8064 100644 --- a/hikari/rest/invite.py +++ b/hikari/rest/invite.py @@ -26,8 +26,7 @@ import typing from hikari.models import invites - -from . import base +from hikari.rest import base class RESTInviteComponent(base.BaseRESTComponent, abc.ABC): # pylint: disable=abstract-method diff --git a/hikari/rest/me.py b/hikari/rest/me.py index 9391194998..75d6060c8e 100644 --- a/hikari/rest/me.py +++ b/hikari/rest/me.py @@ -32,8 +32,7 @@ from hikari.models import guilds from hikari.models import pagination from hikari.models import users - -from . import base +from hikari.rest import base if typing.TYPE_CHECKING: from hikari.models import files diff --git a/hikari/rest/oauth2.py b/hikari/rest/oauth2.py index c813ccde30..ff814e5b30 100644 --- a/hikari/rest/oauth2.py +++ b/hikari/rest/oauth2.py @@ -25,7 +25,7 @@ import abc from hikari.models import applications -from . import base +from hikari.rest import base class RESTOAuth2Component(base.BaseRESTComponent, abc.ABC): # pylint: disable=abstract-method diff --git a/hikari/rest/react.py b/hikari/rest/react.py index f945f3a827..11490e8c78 100644 --- a/hikari/rest/react.py +++ b/hikari/rest/react.py @@ -30,7 +30,7 @@ from hikari.models import messages as messages_ from hikari.models import pagination from hikari.models import users -from . import base +from hikari.rest import base if typing.TYPE_CHECKING: from hikari.models import channels as channels_ diff --git a/hikari/rest/session.py b/hikari/rest/session.py index bd96b46044..9d1702eeda 100644 --- a/hikari/rest/session.py +++ b/hikari/rest/session.py @@ -20,6 +20,10 @@ from __future__ import annotations +#### +#### DELETE +#### + __all__ = ["RESTSession"] import asyncio @@ -31,16 +35,15 @@ import aiohttp.typedefs -from hikari.internal import user_agents +from hikari import errors from hikari.internal import conversions from hikari.internal import http_client from hikari.internal import more_collections from hikari.internal import ratelimits from hikari.internal import urls -from hikari import errors - -from . import buckets -from . import routes +from hikari.internal import user_agents +from hikari.rest import buckets +from hikari.rest import routes if typing.TYPE_CHECKING: import ssl diff --git a/hikari/rest/user.py b/hikari/rest/user.py index bca7870beb..a75ea972f4 100644 --- a/hikari/rest/user.py +++ b/hikari/rest/user.py @@ -27,8 +27,7 @@ from hikari.models import bases from hikari.models import users - -from . import base +from hikari.rest import base class RESTUserComponent(base.BaseRESTComponent, abc.ABC): # pylint: disable=abstract-method diff --git a/hikari/rest/voice.py b/hikari/rest/voice.py index 400e645f78..da6a46fb48 100644 --- a/hikari/rest/voice.py +++ b/hikari/rest/voice.py @@ -26,7 +26,7 @@ import typing from hikari.models import voices -from . import base +from hikari.rest import base class RESTVoiceComponent(base.BaseRESTComponent, abc.ABC): # pylint: disable=abstract-method diff --git a/hikari/rest/webhook.py b/hikari/rest/webhook.py index 73404e346f..2a806e263b 100644 --- a/hikari/rest/webhook.py +++ b/hikari/rest/webhook.py @@ -29,8 +29,7 @@ from hikari.models import bases from hikari.models import messages as messages_ from hikari.models import webhooks - -from . import base +from hikari.rest import base if typing.TYPE_CHECKING: from hikari.models import channels as channels_ diff --git a/hikari/stateful/.gitkeep b/hikari/stateful/.gitkeep deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/hikari/stateless/bot.py b/hikari/stateless/bot.py index 3bd8638730..f492b94d27 100644 --- a/hikari/stateless/bot.py +++ b/hikari/stateless/bot.py @@ -28,12 +28,11 @@ from hikari import rest from hikari.gateway import client from hikari.gateway import intent_aware_dispatchers - from . import manager if typing.TYPE_CHECKING: from hikari import application - from hikari import configs + from hikari import aiohttp_config class StatelessBot(application.Application): @@ -55,5 +54,7 @@ def _create_event_manager(app: application.Application) -> manager.StatelessEven return manager.StatelessEventManagerImpl(app) @staticmethod - def _create_event_dispatcher(config: configs.BotConfig) -> intent_aware_dispatchers.IntentAwareEventDispatcherImpl: + def _create_event_dispatcher( + config: aiohttp_config.BotConfig, + ) -> intent_aware_dispatchers.IntentAwareEventDispatcherImpl: return intent_aware_dispatchers.IntentAwareEventDispatcherImpl(config.intents) diff --git a/hikari/stateless/manager.py b/hikari/stateless/manager.py index c136f1079f..005a43e9a8 100644 --- a/hikari/stateless/manager.py +++ b/hikari/stateless/manager.py @@ -22,8 +22,6 @@ __all__ = ["StatelessEventManagerImpl"] -from hikari.gateway import dispatchers -from hikari.gateway import event_managers from hikari.events import channel from hikari.events import guild from hikari.events import message @@ -31,6 +29,8 @@ # pylint: disable=too-many-public-methods from hikari.events import voice +from hikari.gateway import dispatchers +from hikari.gateway import event_managers class StatelessEventManagerImpl(event_managers.EventManager[dispatchers.EventDispatcher]): diff --git a/tests/hikari/_helpers.py b/tests/hikari/_helpers.py index f852cc2c89..755701f760 100644 --- a/tests/hikari/_helpers.py +++ b/tests/hikari/_helpers.py @@ -157,7 +157,7 @@ def fqn2(module, item_identifier): return module.__name__ + "." + item_identifier -T = typing.TypeVar("T") +T = typing.TypeVar("ComponentImplT") def _can_weakref(spec_set): diff --git a/tests/hikari/gateway/test_client.py b/tests/hikari/gateway/test_client.py index de1de6791b..f1710bf54f 100644 --- a/tests/hikari/gateway/test_client.py +++ b/tests/hikari/gateway/test_client.py @@ -24,7 +24,7 @@ import mock import pytest -from hikari import configs +from hikari import aiohttp_config from hikari import errors from hikari import application from hikari.gateway import consumers @@ -86,7 +86,9 @@ def process_raw_event(self, _client, name, payload): shard_client_obj = high_level_shards.GatewayClient( 0, 1, - mock.MagicMock(application.Application, config=configs.GatewayConfig(), event_manager=DummyConsumer()), + mock.MagicMock( + application.Application, config=aiohttp_config.GatewayConfig(), event_manager=DummyConsumer() + ), "some_url", ) @@ -99,7 +101,7 @@ def test_connection_is_set(self, shard_client_obj): shard_client_obj = high_level_shards.GatewayClient( 0, 1, - mock.MagicMock(application.Application, event_manager=None, config=configs.GatewayConfig()), + mock.MagicMock(application.Application, event_manager=None, config=aiohttp_config.GatewayConfig()), "some_url", ) diff --git a/tests/hikari/models/test_applications.py b/tests/hikari/models/test_applications.py index 0df06754e2..b379d15d0e 100644 --- a/tests/hikari/models/test_applications.py +++ b/tests/hikari/models/test_applications.py @@ -210,9 +210,9 @@ class TestApplication: def test_deserialize(self, application_information_payload, team_payload, owner_payload, mock_app): application_obj = applications.Application.deserialize(application_information_payload, app=mock_app) assert application_obj.team == applications.Team.deserialize(team_payload) - assert application_obj.team._app is mock_app + assert application_obj.team._zookeeper is mock_app assert application_obj.owner == applications.ApplicationOwner.deserialize(owner_payload) - assert application_obj.owner._app is mock_app + assert application_obj.owner._zookeeper is mock_app assert application_obj.id == 209333111222 assert application_obj.name == "Dream Sweet in Sea Major" assert application_obj.icon_hash == "iwiwiwiwiw" diff --git a/tests/hikari/models/test_audit_logs.py b/tests/hikari/models/test_audit_logs.py index 081c5e2a5e..95a69b459e 100644 --- a/tests/hikari/models/test_audit_logs.py +++ b/tests/hikari/models/test_audit_logs.py @@ -432,7 +432,7 @@ def test_deserialize( assert audit_log_obj.entries == { 694026906592477214: audit_logs.AuditLogEntry.deserialize(test_audit_log_entry_payload) } - assert audit_log_obj.entries[694026906592477214]._app is mock_app + assert audit_log_obj.entries[694026906592477214]._zookeeper is mock_app assert audit_log_obj.webhooks == {424242: mock_webhook_obj} assert audit_log_obj.users == {92929292: mock_user_obj} assert audit_log_obj.integrations == {33590653072239123: mock_integration_obj} diff --git a/tests/hikari/models/test_bases.py b/tests/hikari/models/test_bases.py index 00e11ce169..6010a158ff 100644 --- a/tests/hikari/models/test_bases.py +++ b/tests/hikari/models/test_bases.py @@ -41,7 +41,7 @@ class StubEntity(bases.Entity, marshaller.Deserializable, marshaller.Serializabl def test_deserialize(self, stub_entity): mock_app = mock.MagicMock(application.Application) entity = stub_entity.deserialize({}, app=mock_app) - assert entity._app is mock_app + assert entity._zookeeper is mock_app def test_serialize(self, stub_entity): mock_app = mock.MagicMock(application.Application) diff --git a/tests/hikari/models/test_channels.py b/tests/hikari/models/test_channels.py index 52394449ec..2e94d73bf9 100644 --- a/tests/hikari/models/test_channels.py +++ b/tests/hikari/models/test_channels.py @@ -272,7 +272,7 @@ def test_deserialize(self, test_guild_category_payload, test_permission_overwrit assert channel_obj.permission_overwrites == { 4242: channels.PermissionOverwrite.deserialize(test_permission_overwrite_payload) } - assert channel_obj.permission_overwrites[4242]._app is mock_app + assert channel_obj.permission_overwrites[4242]._zookeeper is mock_app assert channel_obj.guild_id == 9876 assert channel_obj.position == 3 assert channel_obj.name == "Test" @@ -289,7 +289,7 @@ def test_deserialize(self, test_guild_text_channel_payload, test_permission_over assert channel_obj.permission_overwrites == { 4242: channels.PermissionOverwrite.deserialize(test_permission_overwrite_payload) } - assert channel_obj.permission_overwrites[4242]._app is mock_app + assert channel_obj.permission_overwrites[4242]._zookeeper is mock_app assert channel_obj.guild_id == 567 assert channel_obj.position == 6 assert channel_obj.name == "general" @@ -309,7 +309,7 @@ def test_deserialize(self, test_guild_news_channel_payload, test_permission_over assert channel_obj.permission_overwrites == { 4242: channels.PermissionOverwrite.deserialize(test_permission_overwrite_payload) } - assert channel_obj.permission_overwrites[4242]._app is mock_app + assert channel_obj.permission_overwrites[4242]._zookeeper is mock_app assert channel_obj.guild_id == 123 assert channel_obj.position == 0 assert channel_obj.name == "Important Announcements" @@ -328,7 +328,7 @@ def test_deserialize(self, test_guild_store_channel_payload, test_permission_ove assert channel_obj.permission_overwrites == { 4242: channels.PermissionOverwrite.deserialize(test_permission_overwrite_payload) } - assert channel_obj.permission_overwrites[4242]._app is mock_app + assert channel_obj.permission_overwrites[4242]._zookeeper is mock_app assert channel_obj.guild_id == 1234 assert channel_obj.position == 2 assert channel_obj.name == "Half Life 3" @@ -345,7 +345,7 @@ def test_deserialize(self, test_guild_voice_channel_payload, test_permission_ove assert channel_obj.permission_overwrites == { 4242: channels.PermissionOverwrite.deserialize(test_permission_overwrite_payload) } - assert channel_obj.permission_overwrites[4242]._app is mock_app + assert channel_obj.permission_overwrites[4242]._zookeeper is mock_app assert channel_obj.guild_id == 789 assert channel_obj.position == 4 assert channel_obj.name == "Secret Developer Discussions" diff --git a/tests/hikari/models/test_embeds.py b/tests/hikari/models/test_embeds.py index fb57d6c787..154dfaf569 100644 --- a/tests/hikari/models/test_embeds.py +++ b/tests/hikari/models/test_embeds.py @@ -264,19 +264,19 @@ def test_deserialize( assert embed_obj.timestamp == mock_datetime assert embed_obj.color == colors.Color(14014915) assert embed_obj.footer == embeds.EmbedFooter.deserialize(test_footer_payload) - assert embed_obj.footer._app is mock_app + assert embed_obj.footer._zookeeper is mock_app assert embed_obj.image == embeds.EmbedImage.deserialize(test_image_payload) - assert embed_obj.image._app is mock_app + assert embed_obj.image._zookeeper is mock_app assert embed_obj.thumbnail == embeds.EmbedThumbnail.deserialize(test_thumbnail_payload) - assert embed_obj.thumbnail._app is mock_app + assert embed_obj.thumbnail._zookeeper is mock_app assert embed_obj.video == embeds.EmbedVideo.deserialize(test_video_payload) - assert embed_obj.video._app is mock_app + assert embed_obj.video._zookeeper is mock_app assert embed_obj.provider == embeds.EmbedProvider.deserialize(test_provider_payload) - assert embed_obj.provider._app is mock_app + assert embed_obj.provider._zookeeper is mock_app assert embed_obj.author == embeds.EmbedAuthor.deserialize(test_author_payload) - assert embed_obj.author._app is mock_app + assert embed_obj.author._zookeeper is mock_app assert embed_obj.fields == [embeds.EmbedField.deserialize(test_field_payload)] - assert embed_obj.fields[0]._app is mock_app + assert embed_obj.fields[0]._zookeeper is mock_app def test_serialize_full_embed(self): embed_obj = embeds.Embed( diff --git a/tests/hikari/models/test_guilds.py b/tests/hikari/models/test_guilds.py index f37e0f30ab..b8b596e760 100644 --- a/tests/hikari/models/test_guilds.py +++ b/tests/hikari/models/test_guilds.py @@ -506,17 +506,17 @@ def test_deserialize( assert presence_activity_obj.timestamps == guilds.ActivityTimestamps.deserialize( test_activity_timestamps_payload ) - assert presence_activity_obj.timestamps._app is mock_app + assert presence_activity_obj.timestamps._zookeeper is mock_app assert presence_activity_obj.application_id == 40404040404040 assert presence_activity_obj.details == "They are doing stuff" assert presence_activity_obj.state == "STATED" assert presence_activity_obj.emoji is mock_emoji assert presence_activity_obj.party == guilds.ActivityParty.deserialize(test_activity_party_payload) - assert presence_activity_obj.party._app is mock_app + assert presence_activity_obj.party._zookeeper is mock_app assert presence_activity_obj.assets == guilds.ActivityAssets.deserialize(test_activity_assets_payload) - assert presence_activity_obj.assets._app is mock_app + assert presence_activity_obj.assets._zookeeper is mock_app assert presence_activity_obj.secrets == guilds.ActivitySecret.deserialize(test_activity_secrets_payload) - assert presence_activity_obj.secrets._app is mock_app + assert presence_activity_obj.secrets._zookeeper is mock_app assert presence_activity_obj.is_instance is True assert presence_activity_obj.flags == guilds.ActivityFlag.INSTANCE | guilds.ActivityFlag.JOIN @@ -548,7 +548,7 @@ def test_deserialize_partial_presence_user(self, mock_app): presence_user_obj = guilds.PresenceUser.deserialize({"id": "115590097100865541"}, app=mock_app) assert presence_user_obj.id == 115590097100865541 for attr in presence_user_obj.__slots__: - if attr not in ("id", "_app"): + if attr not in ("id", "_zookeeper"): assert getattr(presence_user_obj, attr) is unset.UNSET @pytest.fixture() @@ -656,16 +656,16 @@ def test_deserialize( guild_member_presence_obj = guilds.GuildMemberPresence.deserialize(test_guild_member_presence, app=mock_app) patched_since_deserializer.assert_called_once_with("2015-04-26T06:26:56.936000+00:00") assert guild_member_presence_obj.user == guilds.PresenceUser.deserialize(test_user_payload) - assert guild_member_presence_obj.user._app is mock_app + assert guild_member_presence_obj.user._zookeeper is mock_app assert guild_member_presence_obj.role_ids == [49494949] assert guild_member_presence_obj.guild_id == 44004040 assert guild_member_presence_obj.visible_status is guilds.PresenceStatus.DND assert guild_member_presence_obj.activities == [ guilds.PresenceActivity.deserialize(test_presence_activity_payload) ] - assert guild_member_presence_obj.activities[0]._app is mock_app + assert guild_member_presence_obj.activities[0]._zookeeper is mock_app assert guild_member_presence_obj.client_status == guilds.ClientStatus.deserialize(test_client_status_payload) - assert guild_member_presence_obj.client_status._app is mock_app + assert guild_member_presence_obj.client_status._zookeeper is mock_app assert guild_member_presence_obj.premium_since is mock_since assert guild_member_presence_obj.nick == "Nick" @@ -718,7 +718,7 @@ def test_deserialise(self, test_partial_guild_integration_payload, test_integrat assert partial_guild_integration_obj.account == guilds.IntegrationAccount.deserialize( test_integration_account_payload ) - assert partial_guild_integration_obj.account._app is mock_app + assert partial_guild_integration_obj.account._zookeeper is mock_app class TestGuildIntegration: @@ -967,9 +967,9 @@ def test_deserialize( channels.deserialize_channel.assert_called_once_with(test_channel_payload, app=mock_app) emojis.KnownCustomEmoji.deserialize.assert_called_once_with(test_emoji_payload, app=mock_app) assert guild_obj.members == {123456: guilds.GuildMember.deserialize(test_member_payload)} - assert guild_obj.members[123456]._app is mock_app + assert guild_obj.members[123456]._zookeeper is mock_app assert guild_obj.presences == {123456: guilds.GuildMemberPresence.deserialize(test_guild_member_presence)} - assert guild_obj.presences[123456]._app is mock_app + assert guild_obj.presences[123456]._zookeeper is mock_app assert guild_obj.splash_hash == "0ff0ff0ff" assert guild_obj.discovery_splash_hash == "famfamFAMFAMfam" assert guild_obj.owner_id == 6969696 @@ -985,7 +985,7 @@ def test_deserialize( assert guild_obj.default_message_notifications is guilds.GuildMessageNotificationsLevel.ONLY_MENTIONS assert guild_obj.explicit_content_filter is guilds.GuildExplicitContentFilterLevel.ALL_MEMBERS assert guild_obj.roles == {41771983423143936: guilds.GuildRole.deserialize(test_roles_payload)} - assert guild_obj.roles[41771983423143936]._app is mock_app + assert guild_obj.roles[41771983423143936]._zookeeper is mock_app assert guild_obj.emojis == {41771983429993937: mock_emoji} assert guild_obj.mfa_level is guilds.GuildMFALevel.ELEVATED assert guild_obj.application_id == 39494949 diff --git a/tests/hikari/models/test_messages.py b/tests/hikari/models/test_messages.py index e4c0504a48..0a3a1c3f71 100644 --- a/tests/hikari/models/test_messages.py +++ b/tests/hikari/models/test_messages.py @@ -274,7 +274,7 @@ def test_deserialize( message_obj = messages.Message.deserialize(test_message_payload, app=mock_app) patched_emoji_deserializer.assert_called_once_with(test_reaction_payload["emoji"], app=mock_app) assert message_obj.reactions == [messages.Reaction.deserialize(test_reaction_payload)] - assert message_obj.reactions[0]._app is mock_app + assert message_obj.reactions[0]._zookeeper is mock_app patched_application_deserializer.assert_called_once_with(test_application_payload, app=mock_app) patched_edited_timestamp_deserializer.assert_called_once_with("2020-04-21T21:20:16.510000+00:00") patched_timestamp_deserializer.assert_called_once_with("2020-03-21T21:20:16.510000+00:00") @@ -295,17 +295,17 @@ def test_deserialize( assert message_obj.role_mentions == {987} assert message_obj.channel_mentions == {456} assert message_obj.attachments == [messages.Attachment.deserialize(test_attachment_payload)] - assert message_obj.attachments[0]._app is mock_app + assert message_obj.attachments[0]._zookeeper is mock_app assert message_obj.embeds == [embeds.Embed.deserialize({})] - assert message_obj.embeds[0]._app is mock_app + assert message_obj.embeds[0]._zookeeper is mock_app assert message_obj.is_pinned is True assert message_obj.webhook_id == 1234 assert message_obj.type == messages.MessageType.DEFAULT assert message_obj.activity == messages.MessageActivity.deserialize(test_message_activity_payload) - assert message_obj.activity._app is mock_app + assert message_obj.activity._zookeeper is mock_app assert message_obj.application == mock_app assert message_obj.message_reference == messages.MessageCrosspost.deserialize(test_message_crosspost_payload) - assert message_obj.message_reference._app is mock_app + assert message_obj.message_reference._zookeeper is mock_app assert message_obj.flags == messages.MessageFlag.IS_CROSSPOST assert message_obj.nonce == "171000788183678976" diff --git a/tests/hikari/rest/test_channel.py b/tests/hikari/rest/test_channel.py index be2c082ad8..08897c8bcc 100644 --- a/tests/hikari/rest/test_channel.py +++ b/tests/hikari/rest/test_channel.py @@ -475,7 +475,7 @@ async def test_update_message_with_optionals(self, rest_channel_logic_impl, mess result = await rest_channel_logic_impl.update_message( message=message, channel=channel, - content="C O N T E N T", + content="C O N ComponentImplT E N ComponentImplT", embed=mock_embed, flags=messages.MessageFlag.IS_CROSSPOST | messages.MessageFlag.SUPPRESS_EMBEDS, mentions_everyone=False, @@ -486,7 +486,7 @@ async def test_update_message_with_optionals(self, rest_channel_logic_impl, mess rest_channel_logic_impl._session.edit_message.assert_called_once_with( channel_id="123", message_id="432", - content="C O N T E N T", + content="C O N ComponentImplT E N ComponentImplT", embed=mock_embed_payload, flags=6, allowed_mentions=mock_allowed_mentions_payload, @@ -554,7 +554,7 @@ async def test_safe_update_message_with_optionals(self, rest_channel_logic_impl) result = await rest_channel_logic_impl.safe_update_message( message=message, channel=channel, - content="C O N T E N T", + content="C O N ComponentImplT E N ComponentImplT", embed=mock_embed, flags=messages.MessageFlag.IS_CROSSPOST | messages.MessageFlag.SUPPRESS_EMBEDS, mentions_everyone=True, @@ -565,7 +565,7 @@ async def test_safe_update_message_with_optionals(self, rest_channel_logic_impl) rest_channel_logic_impl.update_message.assert_called_once_with( message=message, channel=channel, - content="C O N T E N T", + content="C O N ComponentImplT E N ComponentImplT", embed=mock_embed, flags=messages.MessageFlag.IS_CROSSPOST | messages.MessageFlag.SUPPRESS_EMBEDS, mentions_everyone=True, diff --git a/tests/hikari/rest/test_client.py b/tests/hikari/rest/test_client.py index 4c7c06ed75..ef57a1d732 100644 --- a/tests/hikari/rest/test_client.py +++ b/tests/hikari/rest/test_client.py @@ -21,7 +21,7 @@ import mock import pytest -from hikari import configs +from hikari import aiohttp_config from hikari import rest as high_level_rest from hikari import application from hikari.rest import channel @@ -47,7 +47,7 @@ class TestRESTClient: ], ) def test_init(self, token, token_type, expected_token): - mock_config = configs.RESTConfig(token=token, token_type=token_type, trust_env=True) + mock_config = aiohttp_config.RESTConfig(token=token, token_type=token_type, trust_env=True) mock_app = mock.MagicMock(application.Application, config=mock_config) mock_low_level_rest_clients = mock.MagicMock(low_level_rest.RESTSession) with mock.patch.object(low_level_rest, "RESTSession", return_value=mock_low_level_rest_clients) as patched_init: diff --git a/tests/hikari/test_configs.py b/tests/hikari/test_configs.py index e5149bf294..ec48b1f51c 100644 --- a/tests/hikari/test_configs.py +++ b/tests/hikari/test_configs.py @@ -23,7 +23,7 @@ import mock import pytest -from hikari import configs +from hikari import aiohttp_config from hikari.internal import urls from hikari.models import gateway from hikari.models import guilds @@ -92,19 +92,19 @@ def test_bot_config(test_rest_config, test_websocket_config): class TestDebugConfig: def test_deserialize(self, test_debug_config): - debug_config_obj = configs.DebugConfig.deserialize(test_debug_config) + debug_config_obj = aiohttp_config.DebugConfig.deserialize(test_debug_config) assert debug_config_obj.debug is True def test_empty_deserialize(self): - debug_config_obj = configs.DebugConfig.deserialize({}) + debug_config_obj = aiohttp_config.DebugConfig.deserialize({}) assert debug_config_obj.debug is False class TestAIOHTTPConfig: def test_deserialize(self, test_aiohttp_config): - aiohttp_config_obj = configs.AIOHTTPConfig.deserialize(test_aiohttp_config) + aiohttp_config_obj = aiohttp_config.AIOHTTPConfig.deserialize(test_aiohttp_config) assert aiohttp_config_obj.allow_redirects is True assert aiohttp_config_obj.tcp_connector == aiohttp.TCPConnector @@ -118,7 +118,7 @@ def test_deserialize(self, test_aiohttp_config): assert aiohttp_config_obj.verify_ssl is False def test_empty_deserialize(self): - aiohttp_config_obj = configs.AIOHTTPConfig.deserialize({}) + aiohttp_config_obj = aiohttp_config.AIOHTTPConfig.deserialize({}) assert aiohttp_config_obj.allow_redirects is False assert aiohttp_config_obj.tcp_connector is None @@ -132,12 +132,12 @@ def test_empty_deserialize(self): class TestTokenConfig: def test_deserialize(self, test_token_config): - token_config_obj = configs.TokenConfig.deserialize(test_token_config) + token_config_obj = aiohttp_config.TokenConfig.deserialize(test_token_config) assert token_config_obj.token == "token" def test_empty_deserialize(self): - token_config_obj = configs.TokenConfig.deserialize({}) + token_config_obj = aiohttp_config.TokenConfig.deserialize({}) assert token_config_obj.token is None @@ -148,12 +148,12 @@ def test_deserialize(self, test_websocket_config): test_websocket_config["initial_idle_since"] = datetime_obj.timestamp() mock_activity = mock.MagicMock(gateway.Activity) with _helpers.patch_marshal_attr( - configs.GatewayConfig, + aiohttp_config.GatewayConfig, "initial_activity", deserializer=gateway.Activity.deserialize, return_value=mock_activity, ) as patched_activity_deserializer: - websocket_config_obj = configs.GatewayConfig.deserialize(test_websocket_config) + websocket_config_obj = aiohttp_config.GatewayConfig.deserialize(test_websocket_config) patched_activity_deserializer.assert_called_once_with({"name": "test", "url": "some_url", "type": 0}) assert websocket_config_obj.gateway_use_compression is False assert websocket_config_obj.gateway_version == 6 @@ -178,7 +178,7 @@ def test_deserialize(self, test_websocket_config): assert websocket_config_obj.shard_count == 17 def test_empty_deserialize(self): - websocket_config_obj = configs.GatewayConfig.deserialize({}) + websocket_config_obj = aiohttp_config.GatewayConfig.deserialize({}) assert websocket_config_obj.gateway_use_compression is True assert websocket_config_obj.gateway_version == 6 @@ -203,25 +203,25 @@ def test_empty_deserialize(self): class TestParseShardInfo: def test__parse_shard_info_when_exclusive_range(self): - assert configs._parse_shard_info("0..2") == [0, 1] + assert aiohttp_config._parse_shard_info("0..2") == [0, 1] def test__parse_shard_info_when_inclusive_range(self): - assert configs._parse_shard_info("0...2") == [0, 1, 2] + assert aiohttp_config._parse_shard_info("0...2") == [0, 1, 2] def test__parse_shard_info_when_specific_id(self): - assert configs._parse_shard_info(2) == [2] + assert aiohttp_config._parse_shard_info(2) == [2] def test__parse_shard_info_when_list(self): - assert configs._parse_shard_info([2, 5, 6]) == [2, 5, 6] + assert aiohttp_config._parse_shard_info([2, 5, 6]) == [2, 5, 6] @_helpers.assert_raises(type_=ValueError) def test__parse_shard_info_when_invalid(self): - configs._parse_shard_info("something invalid") + aiohttp_config._parse_shard_info("something invalid") class TestRESTConfig: def test_deserialize(self, test_rest_config): - rest_config_obj = configs.RESTConfig.deserialize(test_rest_config) + rest_config_obj = aiohttp_config.RESTConfig.deserialize(test_rest_config) assert rest_config_obj.rest_version == 6 assert rest_config_obj.allow_redirects is True @@ -239,7 +239,7 @@ def test_deserialize(self, test_rest_config): assert rest_config_obj.oauth2_url == "quxquxx" def test_empty_deserialize(self): - rest_config_obj = configs.RESTConfig.deserialize({}) + rest_config_obj = aiohttp_config.RESTConfig.deserialize({}) assert rest_config_obj.rest_version == 6 assert rest_config_obj.allow_redirects is False @@ -261,12 +261,12 @@ def test_deserialize(self, test_bot_config): test_bot_config["initial_idle_since"] = datetime_obj.timestamp() mock_activity = mock.MagicMock(gateway.Activity) with _helpers.patch_marshal_attr( - configs.BotConfig, + aiohttp_config.BotConfig, "initial_activity", deserializer=gateway.Activity.deserialize, return_value=mock_activity, ) as patched_activity_deserializer: - bot_config_obj = configs.BotConfig.deserialize(test_bot_config) + bot_config_obj = aiohttp_config.BotConfig.deserialize(test_bot_config) patched_activity_deserializer.assert_called_once_with({"name": "test", "url": "some_url", "type": 0}) assert bot_config_obj.rest_version == 6 @@ -295,7 +295,7 @@ def test_deserialize(self, test_bot_config): assert bot_config_obj.oauth2_url == "quxquxx" def test_empty_deserialize(self): - bot_config_obj = configs.BotConfig.deserialize({}) + bot_config_obj = aiohttp_config.BotConfig.deserialize({}) assert bot_config_obj.rest_version == 6 assert bot_config_obj.allow_redirects is False From 6e0b23c322664a990767b2c72c461b1fcf7e0726 Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Mon, 18 May 2020 19:07:59 +0100 Subject: [PATCH 361/922] De-q-ed docstring --- hikari/api/gateway_zookeeper.py | 2 +- hikari/gateway/dispatchers.py | 2 +- hikari/gateway/event_managers.py | 2 +- hikari/internal/conversions.py | 2 +- hikari/models/audit_logs.py | 4 ++-- hikari/models/bases.py | 2 +- hikari/models/channels.py | 2 +- hikari/models/pagination.py | 4 ++-- hikari/models/unset.py | 2 +- tests/hikari/_helpers.py | 2 +- tests/hikari/rest/test_channel.py | 8 ++++---- 11 files changed, 16 insertions(+), 16 deletions(-) diff --git a/hikari/api/gateway_zookeeper.py b/hikari/api/gateway_zookeeper.py index 89fb189279..78fbe3f760 100644 --- a/hikari/api/gateway_zookeeper.py +++ b/hikari/api/gateway_zookeeper.py @@ -41,7 +41,7 @@ class IGatewayZookeeper(base_app.IBaseApp, abc.ABC): These events will be produced by a low-level gateway implementation, and will produce `list` and `dict` types only. - q + This may be combined with `IGatewayDispatcher` for most single-process bots, or may be a specific component for large distributed applications that feed new events into a message queue, for example. diff --git a/hikari/gateway/dispatchers.py b/hikari/gateway/dispatchers.py index 74585c0697..385af995d3 100644 --- a/hikari/gateway/dispatchers.py +++ b/hikari/gateway/dispatchers.py @@ -170,7 +170,7 @@ async def on_create_delete(event): Returns ------- - decorator(ComponentImplT) -> ComponentImplT + decorator(T) -> T A decorator for a function that registers the given event. Raises diff --git a/hikari/gateway/event_managers.py b/hikari/gateway/event_managers.py index fa7c4867fa..319dd22035 100644 --- a/hikari/gateway/event_managers.py +++ b/hikari/gateway/event_managers.py @@ -50,7 +50,7 @@ def raw_event_mapper(name: str) -> typing.Callable[[EventConsumerT], EventConsum Returns ------- - decorator(ComponentImplT) -> ComponentImplT + decorator(T) -> T A decorator for a method. """ diff --git a/hikari/internal/conversions.py b/hikari/internal/conversions.py index 36132431aa..9bcfbb0a7b 100644 --- a/hikari/internal/conversions.py +++ b/hikari/internal/conversions.py @@ -59,7 +59,7 @@ DISCORD_EPOCH: typing.Final[int] = 1_420_070_400 ISO_8601_DATE_PART: typing.Final[typing.Pattern] = re.compile(r"^(\d{4})-(\d{2})-(\d{2})") ISO_8601_TIME_PART: typing.Final[typing.Pattern] = re.compile( - r"ComponentImplT(\d{2}):(\d{2}):(\d{2})(?:\.(\d{1,6}))?", re.I + r"T(\d{2}):(\d{2}):(\d{2})(?:\.(\d{1,6}))?", re.I ) ISO_8601_TZ_PART: typing.Final[typing.Pattern] = re.compile(r"([+-])(\d{2}):(\d{2})$") diff --git a/hikari/models/audit_logs.py b/hikari/models/audit_logs.py index d9287027ca..881bb8ff14 100644 --- a/hikari/models/audit_logs.py +++ b/hikari/models/audit_logs.py @@ -285,7 +285,7 @@ def register_audit_log_entry_info( Returns ------- - decorator(ComponentImplT) -> ComponentImplT + decorator(T) -> T The decorator to decorate the class with. """ @@ -504,7 +504,7 @@ def _deserialize_users( def _deserialize_webhooks( payload: more_typing.JSONArray, **kwargs: typing.Any ) -> typing.Mapping[bases.Snowflake, webhooks_.Webhook]: - return {bases.Snowflake(webhook["id"]): webhooks_.Webhook.deserialize(we1bhook, **kwargs) for webhook in payload} + return {bases.Snowflake(webhook["id"]): webhooks_.Webhook.deserialize(webhook, **kwargs) for webhook in payload} # TODO: can we remove this? it is used by a seemingly duplicated endpoint that can just use the iterator. diff --git a/hikari/models/bases.py b/hikari/models/bases.py index cd8cfee38b..41823514bd 100644 --- a/hikari/models/bases.py +++ b/hikari/models/bases.py @@ -127,4 +127,4 @@ def __int__(self) -> int: return int(self.id) -T = typing.TypeVar("ComponentImplT", bound=Unique) +T = typing.TypeVar("T", bound=Unique) diff --git a/hikari/models/channels.py b/hikari/models/channels.py index b1d96c42ef..2ea6a6e8dc 100644 --- a/hikari/models/channels.py +++ b/hikari/models/channels.py @@ -134,7 +134,7 @@ def register_channel_type( Returns ------- - decorator(ComponentImplT) -> ComponentImplT + decorator(T) -> T The decorator to decorate the class with. """ diff --git a/hikari/models/pagination.py b/hikari/models/pagination.py index 9fdf99e6c0..46b5ca6607 100644 --- a/hikari/models/pagination.py +++ b/hikari/models/pagination.py @@ -117,7 +117,7 @@ def enumerate(self, *, start: int = 0) -> PaginatedResults[typing.Tuple[int, _T] Returns ------- - PaginatedResults[typing.Tuple[int, ComponentImplT]] + PaginatedResults[typing.Tuple[int, T]] A paginated results view that asynchronously yields an increasing counter in a tuple with each result, lazily. """ @@ -139,7 +139,7 @@ def limit(self, limit: int) -> PaginatedResults[_T]: Returns ------- - PaginatedResults[ComponentImplT] + PaginatedResults[T] A paginated results view that asynchronously yields a maximum of the given number of items before completing. """ diff --git a/hikari/models/unset.py b/hikari/models/unset.py index 55901cd617..6b38d07de4 100644 --- a/hikari/models/unset.py +++ b/hikari/models/unset.py @@ -47,7 +47,7 @@ def __init_subclass__(cls, **kwargs: typing.Any) -> typing.NoReturn: raise TypeError("Cannot subclass Unset type") -T = typing.TypeVar("ComponentImplT") +T = typing.TypeVar("T") MayBeUnset = typing.Union[T, Unset] UNSET: typing.Final[Unset] = Unset() diff --git a/tests/hikari/_helpers.py b/tests/hikari/_helpers.py index 755701f760..f852cc2c89 100644 --- a/tests/hikari/_helpers.py +++ b/tests/hikari/_helpers.py @@ -157,7 +157,7 @@ def fqn2(module, item_identifier): return module.__name__ + "." + item_identifier -T = typing.TypeVar("ComponentImplT") +T = typing.TypeVar("T") def _can_weakref(spec_set): diff --git a/tests/hikari/rest/test_channel.py b/tests/hikari/rest/test_channel.py index 08897c8bcc..be2c082ad8 100644 --- a/tests/hikari/rest/test_channel.py +++ b/tests/hikari/rest/test_channel.py @@ -475,7 +475,7 @@ async def test_update_message_with_optionals(self, rest_channel_logic_impl, mess result = await rest_channel_logic_impl.update_message( message=message, channel=channel, - content="C O N ComponentImplT E N ComponentImplT", + content="C O N T E N T", embed=mock_embed, flags=messages.MessageFlag.IS_CROSSPOST | messages.MessageFlag.SUPPRESS_EMBEDS, mentions_everyone=False, @@ -486,7 +486,7 @@ async def test_update_message_with_optionals(self, rest_channel_logic_impl, mess rest_channel_logic_impl._session.edit_message.assert_called_once_with( channel_id="123", message_id="432", - content="C O N ComponentImplT E N ComponentImplT", + content="C O N T E N T", embed=mock_embed_payload, flags=6, allowed_mentions=mock_allowed_mentions_payload, @@ -554,7 +554,7 @@ async def test_safe_update_message_with_optionals(self, rest_channel_logic_impl) result = await rest_channel_logic_impl.safe_update_message( message=message, channel=channel, - content="C O N ComponentImplT E N ComponentImplT", + content="C O N T E N T", embed=mock_embed, flags=messages.MessageFlag.IS_CROSSPOST | messages.MessageFlag.SUPPRESS_EMBEDS, mentions_everyone=True, @@ -565,7 +565,7 @@ async def test_safe_update_message_with_optionals(self, rest_channel_logic_impl) rest_channel_logic_impl.update_message.assert_called_once_with( message=message, channel=channel, - content="C O N ComponentImplT E N ComponentImplT", + content="C O N T E N T", embed=mock_embed, flags=messages.MessageFlag.IS_CROSSPOST | messages.MessageFlag.SUPPRESS_EMBEDS, mentions_everyone=True, From 36fe50f25db7e08d68cb75ad6868c1f2d9b7da08 Mon Sep 17 00:00:00 2001 From: davfsa Date: Tue, 19 May 2020 10:44:25 +0200 Subject: [PATCH 362/922] Deprecate "is_embed_enabled" and "embed_channel_id" [skip ci] --- hikari/models/guilds.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/hikari/models/guilds.py b/hikari/models/guilds.py index 2639539cb8..45467399e7 100644 --- a/hikari/models/guilds.py +++ b/hikari/models/guilds.py @@ -1142,6 +1142,9 @@ class Guild(PartialGuild): # pylint:disable=too-many-instance-attributes This information may not be present, in which case, it will be `None` instead. This will be `None` for guilds that the bot is not a member in. + + !!! deprecated + Use `is_widget_enabled` instead. """ embed_channel_id: typing.Optional[bases.Snowflake] = marshaller.attrib( @@ -1150,6 +1153,9 @@ class Guild(PartialGuild): # pylint:disable=too-many-instance-attributes """The channel ID that the guild embed will generate an invite to. Will be `None` if invites are disabled for this guild's embed. + + !!! deprecated + Use `widget_channel_id` instead. """ verification_level: GuildVerificationLevel = marshaller.attrib( @@ -1207,9 +1213,6 @@ class Guild(PartialGuild): # pylint:disable=too-many-instance-attributes """Describes whether the guild widget is enabled or not. If this information is not present, this will be `None`. - - This will only be provided for guilds that the application user is a member - of. For all other purposes, this should be ignored. """ widget_channel_id: typing.Optional[bases.Snowflake] = marshaller.attrib( From 087c53597623686cc16edffb24eff692630f173f Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Tue, 19 May 2020 15:27:06 +0100 Subject: [PATCH 363/922] Removed a hell of a lot of clutter code, reduced all gateway stuff to one 560 line long file --- hikari/__init__.py | 2 +- hikari/api/event_consumer.py | 4 +- hikari/api/gateway_zookeeper.py | 5 +- hikari/errors.py | 113 +-- hikari/events/message.py | 2 +- hikari/events/other.py | 8 +- hikari/gateway.py | 564 +++++++++++ hikari/gateway/__init__.py | 27 - hikari/gateway/client.py | 492 ---------- hikari/gateway/connection.py | 891 ------------------ hikari/gateway/consumers.py | 60 -- hikari/gateway/dispatchers.py | 220 ----- hikari/gateway/event_managers.py | 184 ---- hikari/gateway/gateway_state.py | 54 -- hikari/gateway/intent_aware_dispatchers.py | 348 ------- hikari/gateway/runnable.py | 99 -- .../{aiohttp_config.py => http_settings.py} | 6 +- hikari/impl/bot.py | 36 +- hikari/impl/gateway_zookeeper.py | 14 +- hikari/impl/rest_app.py | 4 +- hikari/internal/codes.py | 362 ------- hikari/internal/conversions.py | 4 +- hikari/internal/http_client.py | 173 ++-- hikari/internal/more_asyncio.py | 2 +- hikari/internal/ratelimits.py | 2 +- hikari/internal/tracing.py | 6 +- hikari/models/audit_logs.py | 2 +- hikari/rest/channel.py | 2 +- hikari/rest/client.py | 4 +- hikari/rest/guild.py | 4 +- hikari/rest/routes.py | 52 +- hikari/rest/session.py | 30 +- hikari/stateless/bot.py | 10 +- tests/hikari/events/test_channel.py | 10 +- tests/hikari/events/test_message.py | 22 +- tests/hikari/gateway/test_client.py | 14 +- tests/hikari/gateway/test_connection.py | 32 +- tests/hikari/internal/test_http_client.py | 100 +- tests/hikari/internal/test_marshaller.py | 2 +- tests/hikari/models/test_applications.py | 4 +- tests/hikari/models/test_audit_logs.py | 12 +- tests/hikari/models/test_bases.py | 2 +- tests/hikari/models/test_channels.py | 10 +- tests/hikari/models/test_embeds.py | 22 +- tests/hikari/models/test_guilds.py | 28 +- tests/hikari/models/test_messages.py | 18 +- tests/hikari/models/test_voices.py | 2 +- tests/hikari/models/test_webhook.py | 2 +- tests/hikari/rest/test_channel.py | 8 +- tests/hikari/rest/test_client.py | 14 +- tests/hikari/rest/test_guild.py | 8 +- tests/hikari/rest/test_routes.py | 10 +- tests/hikari/rest/test_session.py | 32 +- tests/hikari/rest/test_webhook.py | 4 +- tests/hikari/stateless/test_manager.py | 4 +- tests/hikari/test_configs.py | 162 ++-- 56 files changed, 1006 insertions(+), 3302 deletions(-) create mode 100644 hikari/gateway.py delete mode 100644 hikari/gateway/__init__.py delete mode 100644 hikari/gateway/client.py delete mode 100644 hikari/gateway/connection.py delete mode 100644 hikari/gateway/consumers.py delete mode 100644 hikari/gateway/dispatchers.py delete mode 100644 hikari/gateway/event_managers.py delete mode 100644 hikari/gateway/gateway_state.py delete mode 100644 hikari/gateway/intent_aware_dispatchers.py delete mode 100644 hikari/gateway/runnable.py rename hikari/{aiohttp_config.py => http_settings.py} (95%) delete mode 100644 hikari/internal/codes.py diff --git a/hikari/__init__.py b/hikari/__init__.py index d12288a5c1..bf7df8a88a 100644 --- a/hikari/__init__.py +++ b/hikari/__init__.py @@ -30,7 +30,7 @@ from ._about import __license__ from ._about import __url__ from ._about import __version__ -from .aiohttp_config import * +from .http_settings import * from .errors import * from .events import * from .gateway import * diff --git a/hikari/api/event_consumer.py b/hikari/api/event_consumer.py index 1c40e88e09..9571eef0c6 100644 --- a/hikari/api/event_consumer.py +++ b/hikari/api/event_consumer.py @@ -22,12 +22,12 @@ import abc -from hikari.gateway import client +from hikari import gateway from hikari.internal import more_typing class IEventConsumer(abc.ABC): __slots__ = () - async def consume_raw_event(self, shard: client.GatewayClient, event_name: str, payload: more_typing.JSONType): + async def consume_raw_event(self, shard: gateway.Gateway, event_name: str, payload: more_typing.JSONType): ... diff --git a/hikari/api/gateway_zookeeper.py b/hikari/api/gateway_zookeeper.py index 78fbe3f760..e2225f4774 100644 --- a/hikari/api/gateway_zookeeper.py +++ b/hikari/api/gateway_zookeeper.py @@ -28,12 +28,11 @@ import typing from hikari.api import base_app -from hikari.models import gateway from hikari.models import guilds if typing.TYPE_CHECKING: from hikari.api import event_consumer - from hikari.gateway import client + from hikari import gateway class IGatewayZookeeper(base_app.IBaseApp, abc.ABC): @@ -56,7 +55,7 @@ def event_consumer(self) -> event_consumer.IEventConsumer: @property @abc.abstractmethod - def gateway_shards(self) -> typing.Mapping[int, client.GatewayClient]: + def gateway_shards(self) -> typing.Mapping[int, gateway.Gateway]: """Mapping of each shard ID to the corresponding client for it.""" @property diff --git a/hikari/errors.py b/hikari/errors.py index b3d16964f7..6f7e4f56c4 100644 --- a/hikari/errors.py +++ b/hikari/errors.py @@ -31,11 +31,6 @@ "HTTPErrorResponse", "ClientHTTPErrorResponse", "ServerHTTPErrorResponse", - "GatewayZombiedError", - "GatewayNeedsShardingError", - "GatewayMustReconnectError", - "GatewayInvalidSessionError", - "GatewayInvalidTokenError", "GatewayServerClosedConnectionError", "GatewayClientClosedError", "GatewayClientDisconnectedError", @@ -47,8 +42,6 @@ import aiohttp.typedefs -from hikari.internal import codes - class HikariError(RuntimeError): """Base for an error raised by this API. @@ -134,107 +127,31 @@ class GatewayServerClosedConnectionError(GatewayError): Parameters ---------- - close_code : hikari.internal.codes.GatewayCloseCode | int | None - The close code provided by the server, if there was one. reason : str | None A string explaining the issue. - """ + code : int | None + The close code. - __slots__ = ("close_code",) + """ - close_code: typing.Union[codes.GatewayCloseCode, int, None] + __slots__ = ("code", "can_reconnect", "can_resume", "should_backoff") def __init__( self, - close_code: typing.Optional[typing.Union[codes.GatewayCloseCode, int]] = None, - reason: typing.Optional[str] = None, + reason: str, + code: typing.Optional[int] = None, + can_reconnect: bool = False, + can_resume: bool = False, + should_backoff: bool = True, ) -> None: - try: - name = close_code.name - except AttributeError: - name = str(close_code) if close_code is not None else "no reason" - - if reason is None: - reason = f"Gateway connection closed by server ({name})" - - self.close_code = close_code - super().__init__(reason) - - -class GatewayInvalidTokenError(GatewayServerClosedConnectionError): - """An exception that is raised if you failed to authenticate with a valid token to the Gateway.""" - - __slots__ = () - - def __init__(self) -> None: - super().__init__( - codes.GatewayCloseCode.AUTHENTICATION_FAILED, - "The account token specified is invalid for the gateway connection", - ) - - -class GatewayInvalidSessionError(GatewayServerClosedConnectionError): - """An exception raised if a Gateway session becomes invalid. - - Parameters - ---------- - can_resume : bool - `True` if the connection will be able to RESUME next time it starts - rather than re-IDENTIFYing, or `False` if you need to IDENTIFY - again instead. - """ - - __slots__ = ("can_resume",) - - can_resume: bool - """`True` if the next reconnection can be RESUMED, - `False` if it has to be coordinated by re-IDENFITYing. - """ - - def __init__(self, can_resume: bool) -> None: + self.code = code + self.can_reconnect = can_reconnect self.can_resume = can_resume - instruction = "restart the shard and RESUME" if can_resume else "restart the shard with a fresh session" - super().__init__(reason=f"The session has been invalidated; {instruction}") - - -class GatewayMustReconnectError(GatewayServerClosedConnectionError): - """An exception raised when the Gateway has to re-connect with a new session. - - This will cause a re-IDENTIFY. - """ - - __slots__ = () - - def __init__(self) -> None: - super().__init__(reason="The gateway server has requested that the client reconnects with a new session") - - -class GatewayNeedsShardingError(GatewayServerClosedConnectionError): - """An exception raised if you have too many guilds on one of the current Gateway shards. - - This is a sign you need to increase the number of shards that your bot is - running with in order to connect to Discord. - """ - - __slots__ = () - - def __init__(self) -> None: - super().__init__( - codes.GatewayCloseCode.SHARDING_REQUIRED, "You are in too many guilds. Shard the bot to connect", - ) - - -class GatewayZombiedError(GatewayClientClosedError): - """An exception raised if a shard becomes zombied. - - This means that Discord is no longer responding to us, and we have - disconnected due to a timeout. - """ - - __slots__ = () + self.should_backoff = should_backoff + super().__init__(reason) - def __init__(self) -> None: - super().__init__("No heartbeat was received, the connection has been closed") + def __str__(self) -> str: + return f"Server closed connection with code {self.code} because {self.reason}" class HTTPError(HikariError): diff --git a/hikari/events/message.py b/hikari/events/message.py index eba3fdaf76..38883a03d0 100644 --- a/hikari/events/message.py +++ b/hikari/events/message.py @@ -91,7 +91,7 @@ class MessageUpdateEvent(base_events.HikariEvent, base_models.Unique, marshaller """Represents Message Update gateway events. !!! note - All fields on this model except `MessageUpdateEvent.channel_id` and + All fields on this model except `MessageUpdateEvent.channel` and `MessageUpdateEvent.id` may be set to `hikari.models.unset.UNSET` (a singleton) we have not received information about their state from Discord alongside field nullability. diff --git a/hikari/events/other.py b/hikari/events/other.py index 3e61e17928..862cd12142 100644 --- a/hikari/events/other.py +++ b/hikari/events/other.py @@ -44,7 +44,7 @@ from . import base as base_events if typing.TYPE_CHECKING: - from hikari.gateway import client as gateway_client + from .. import gateway as gateway_client from hikari.internal import more_typing @@ -92,7 +92,7 @@ class StoppedEvent(base_events.HikariEvent): class ConnectedEvent(base_events.HikariEvent, marshaller.Deserializable): """Event invoked each time a shard connects.""" - shard: gateway_client.GatewayClient + shard: gateway_client.Gateway """The shard that connected.""" @@ -100,7 +100,7 @@ class ConnectedEvent(base_events.HikariEvent, marshaller.Deserializable): class DisconnectedEvent(base_events.HikariEvent, marshaller.Deserializable): """Event invoked each time a shard disconnects.""" - shard: gateway_client.GatewayClient + shard: gateway_client.Gateway """The shard that disconnected.""" @@ -108,7 +108,7 @@ class DisconnectedEvent(base_events.HikariEvent, marshaller.Deserializable): class ResumedEvent(base_events.HikariEvent): """Represents a gateway Resume event.""" - shard: gateway_client.GatewayClient + shard: gateway_client.Gateway """The shard that reconnected.""" diff --git a/hikari/gateway.py b/hikari/gateway.py new file mode 100644 index 0000000000..dad9f9ed71 --- /dev/null +++ b/hikari/gateway.py @@ -0,0 +1,564 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# Copyright © Nekoka.tt 2019-2020 +# +# This file is part of Hikari. +# +# Hikari is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Hikari is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Hikari. If not, see . +"""Provides a facade around `hikari.gateway.connection.Shard`. + +This handles parsing and initializing the object from a configuration, as +well as restarting it if it disconnects. + +Additional functions and coroutines are provided to update the presence on the +shard using models defined in `hikari`. +""" + +from __future__ import annotations + +__all__ = ["Gateway"] + +import asyncio +import contextlib +import json +import time +import typing +import urllib.parse +import zlib + +import aiohttp + +from hikari import errors +from hikari import http_settings +from hikari.internal import http_client +from hikari.internal import more_asyncio +from hikari.internal import more_enums +from hikari.internal import ratelimits +from hikari.internal import user_agents +from hikari.models import bases +from hikari.models import channels +from hikari.models import unset +from hikari.internal import more_typing + + +if typing.TYPE_CHECKING: + import datetime + + from hikari.models import gateway + from hikari.models import guilds + from hikari.models import intents as intents_ + + +@more_enums.must_be_unique +class _GatewayCloseCode(int, more_enums.Enum): + """Reasons for closing a gateway connection.""" + + NORMAL_CLOSURE = 1000 + UNKNOWN_ERROR = 4000 + UNKNOWN_OPCODE = 4001 + DECODE_ERROR = 4002 + NOT_AUTHENTICATED = 4003 + AUTHENTICATION_FAILED = 4004 + ALREADY_AUTHENTICATED = 4005 + INVALID_SEQ = 4007 + RATE_LIMITED = 4008 + SESSION_TIMEOUT = 4009 + INVALID_SHARD = 4010 + SHARDING_REQUIRED = 4011 + INVALID_VERSION = 4012 + INVALID_INTENT = 4013 + DISALLOWED_INTENT = 4014 + + def __str__(self) -> str: + name = self.name.replace("_", " ").title() + return f"{self.value} {name}" + + +@more_enums.must_be_unique +class _GatewayOpcode(int, more_enums.Enum): + """Opcodes that the gateway uses internally.""" + + DISPATCH = 0 + HEARTBEAT = 1 + IDENTIFY = 2 + PRESENCE_UPDATE = 3 + VOICE_STATE_UPDATE = 4 + RESUME = 6 + RECONNECT = 7 + REQUEST_GUILD_MEMBERS = 8 + INVALID_SESSION = 9 + HELLO = 10 + HEARTBEAT_ACK = 11 + + def __str__(self) -> str: + name = self.name.replace("_", " ").title() + return f"{self.value} {name}" + + +RawDispatchT = typing.Callable[["Gateway", str, more_typing.JSONObject], more_typing.Coroutine[None]] + + +class _Reconnect(RuntimeError): + __slots__ = () + + +class _Zombie(RuntimeError): + __slots__ = () + + +class _InvalidSession(RuntimeError): + __slots__ = ("can_resume",) + + def __init__(self, can_resume: bool) -> None: + self.can_resume = can_resume + + +class Gateway(http_client.HTTPClient): + """Blah blah + """ + + def __init__( + self, + *, + config: http_settings.HTTPSettings, + dispatch: RawDispatchT, + debug: bool, + initial_activity: typing.Optional[gateway.Activity] = None, + initial_idle_since: typing.Optional[datetime.datetime] = None, + initial_is_afk: typing.Optional[bool] = None, + initial_status: typing.Optional[guilds.PresenceStatus] = None, + intents: typing.Optional[intents_.Intent] = None, + large_threshold: int = 250, + shard_id: int, + shard_count: int, + token: str, + url: str, + use_compression: bool = True, + version: int, + ) -> None: + super().__init__( + allow_redirects=config.allow_redirects, + connector=config.tcp_connector, + debug=debug, + logger_name=f"{type(self).__module__}.{type(self).__qualname__}.{shard_id}", + proxy_auth=config.proxy_auth, + proxy_headers=config.proxy_headers, + proxy_url=config.proxy_url, + ssl_context=config.ssl_context, + verify_ssl=config.verify_ssl, + timeout=config.request_timeout, + trust_env=config.trust_env, + ) + self._activity = initial_activity + self._dead_event = asyncio.Event() + self._dispatch = dispatch + self._heartbeat_task = None + self._idle_since = initial_idle_since + self._intents = intents + self._is_afk = initial_is_afk + self._ready_event = asyncio.Event() + self._request_close_event = asyncio.Event() + self._resumed_event = asyncio.Event() + self._run_task = None + self._running = False + self._seq = None + self._session_id = None + self._shard_id = shard_id + self._shard_count = shard_count + self._status = initial_status + self._token = token + self._use_compression = use_compression + self._version = version + self._ws = None + self._zlib = None + + self.connected_at = float("nan") + self.disconnect_count = 0 + self.heartbeat_interval = float("nan") + self.heartbeat_latency = float("nan") + self.last_heartbeat_sent = float("nan") + self.last_message_received = float("nan") + self.large_threshold = large_threshold + self.ratelimiter = ratelimits.WindowedBurstRateLimiter(str(shard_id), 60.0, 120) + + scheme, netloc, path, params, _, _ = urllib.parse.urlparse(url, allow_fragments=True) + + new_query = dict(v=int(version), encoding="json") + if use_compression: + # payload compression + new_query["compress"] = "zlib-stream" + + new_query = urllib.parse.urlencode(new_query) + + self.url = urllib.parse.urlunparse((scheme, netloc, path, params, new_query, "")) + + async def close(self) -> None: + self._request_close_event.set() + await self._dead_event.wait() + + async def update_presence( + self, + *, + idle_since: unset.MayBeUnset[typing.Optional[datetime.datetime]] = unset.UNSET, + is_afk: unset.MayBeUnset[bool] = unset.UNSET, + activity: unset.MayBeUnset[typing.Optional[gateway.Activity]] = unset.UNSET, + status: unset.MayBeUnset[guilds.PresenceStatus] = unset.UNSET, + ) -> None: + payload = self._build_presence_payload(idle_since, is_afk, activity, status) + await self._send_json({"op": _GatewayOpcode.PRESENCE_UPDATE, "d": payload}) + self._idle_since = idle_since if not unset.is_unset(idle_since) else self._idle_since + self._is_afk = is_afk if not unset.is_unset(is_afk) else self._is_afk + self._activity = activity if not unset.is_unset(activity) else self._activity + self._status = status if not unset.is_unset(status) else self._status + + async def update_voice_state( + self, + guild: typing.Union[guilds.PartialGuild, bases.Snowflake, int, str], + channel: typing.Union[channels.GuildVoiceChannel, bases.Snowflake, int, str, None], + *, + self_mute: bool = False, + self_deaf: bool = False, + ) -> None: + payload = { + "op": _GatewayOpcode.VOICE_STATE_UPDATE, + "d": { + "guild_id": str(int(guild)), + "channel": str(int(channel)) if channel is not None else None, + "self_mute": self_mute, + "self_deaf": self_deaf, + }, + } + await self._send_json(payload) + + async def run(self): + self._run_task = asyncio.Task.current_task() + self._dead_event.clear() + + back_off = ratelimits.ExponentialBackOff(base=1.85, maximum=600, initial_increment=2) + last_start = self._now() + do_not_back_off = True + + try: + while True: + try: + if not do_not_back_off and self._now() - last_start < 30: + next_back_off = next(back_off) + self.logger.info( + "restarted within 30 seconds, will back off for %.2fs", next_back_off, + ) + await asyncio.sleep(next_back_off) + else: + back_off.reset() + + last_start = self._now() + do_not_back_off = False + + await self._run_once() + + raise RuntimeError("This shouldn't be reached.") + + except aiohttp.ClientConnectorError as ex: + self.logger.exception( + "failed to connect to Discord to initialize a websocket connection", exc_info=ex, + ) + + except _Zombie: + self.logger.warning("entered a zombie state and will be restarted") + + except _InvalidSession as ex: + if ex.can_resume: + self.logger.warning("invalid session, so will attempt to resume") + else: + self.logger.warning("invalid session, so will attempt to reconnect") + self._seq = None + self._session_id = None + + do_not_back_off = True + await asyncio.sleep(5) + + except _Reconnect: + self.logger.warning("instructed by Discord to reconnect") + do_not_back_off = True + await asyncio.sleep(5) + + except errors.GatewayClientDisconnectedError: + self.logger.warning("unexpected connection close, will attempt to reconnect") + + except errors.GatewayClientClosedError: + self.logger.warning("gateway client closed by user, will not attempt to restart") + return + finally: + self._dead_event.set() + + async def _run_once(self) -> None: + try: + self.logger.debug("creating websocket connection to %s", self.url) + self._ws = await self._create_ws(self.url) + self._zlib = zlib.decompressobj() + + self._ready_event.clear() + self._resumed_event.clear() + self._request_close_event.clear() + self._running = True + + await self._handshake() + + # We should ideally set this after HELLO, but it should be fine + # here as well. If we don't heartbeat in time, something probably + # went majorly wrong anyway. + heartbeat_task = asyncio.create_task( + self._maintain_heartbeat(), name=f"gateway shard {self._shard_id} heartbeat" + ) + + poll_events_task = asyncio.create_task(self._poll_events(), name=f"gateway shard {self._shard_id} poll") + completed, pending = await more_asyncio.wait( + [heartbeat_task, poll_events_task], return_when=asyncio.FIRST_COMPLETED + ) + + for pending_task in pending: + pending_task.cancel() + with contextlib.suppress(Exception): + # Clear any pending exception to prevent a nasty console message. + pending_task.result() + + ex = None + while len(completed) > 0 and ex is None: + ex = completed.pop().exception() + + # If the heartbeat call closes normally, then we want to get the exception + # raised by the identify call if it raises anything. This prevents spammy + # exceptions being thrown if the client shuts down during the handshake, + # which becomes more and more likely when we consider bots may have many + # shards running, each taking min of 5s to start up after the first. + ex = None + + while len(completed) > 0 and ex is None: + ex = completed.pop().exception() + + if isinstance(ex, asyncio.TimeoutError): + # If we get _request_timeout errors receiving stuff, propagate as a zombied connection. This + # is already done by the ping keepalive and heartbeat keepalive partially, but this + # is a second edge case. + raise _Zombie() + + if ex is not None: + raise ex + finally: + asyncio.create_task( + self._dispatch(self, "DISCONNECTED", {}), name=f"shard {self._shard_id} dispatch DISCONNECTED" + ) + self._running = False + + async def _handshake(self) -> None: + # HELLO! + message = await self._recv_json() + op = message["op"] + if message["op"] != _GatewayOpcode.HELLO: + raise errors.GatewayError(f"Expected HELLO opcode 10 but received {op}") + + self.heartbeat_interval = message["d"]["heartbeat_interval"] / 1_000.0 + + asyncio.create_task(self._dispatch(self, "CONNECTED", {}), name=f"shard {self._shard_id} dispatch CONNECTED") + self.logger.debug("received HELLO") + + if self._session_id is not None: + # RESUME! + await self._send_json( + { + "op": _GatewayOpcode.RESUME, + "d": {"token": self._token, "seq": self._seq, "session_id": self._session_id}, + } + ) + + else: + # IDENTIFY! + # noinspection PyArgumentList + payload = { + "op": _GatewayOpcode.IDENTIFY, + "d": { + "token": self._token, + "compress": False, + "large_threshold": self.large_threshold, + "properties": user_agents.UserAgent().websocket_triplet, + "shard": [self._shard_id, self._shard_count], + }, + } + + if self._intents is not None: + payload["d"]["intents"] = self._intents + + if any(item is not None for item in (self._activity, self._idle_since, self._is_afk, self._status)): + # noinspection PyTypeChecker + payload["d"]["presence"] = self._build_presence_payload() + + await self._send_json(payload) + + async def _maintain_heartbeat(self) -> None: + while not self._request_close_event.is_set(): + time_since_message = self._now() - self.last_message_received + if self.heartbeat_interval < time_since_message: + self.logger.error("connection is a zombie, haven't received any message for %ss", time_since_message) + raise _Zombie() + + self.logger.debug("preparing to send HEARTBEAT [s:%s, interval:%ss]", self._seq, self.heartbeat_interval) + await self._send_json({"op": _GatewayOpcode.HEARTBEAT, "d": self._seq}) + self.last_heartbeat_sent = self._now() + + try: + await asyncio.wait_for(self._request_close_event.wait(), timeout=self.heartbeat_interval) + except asyncio.TimeoutError: + pass + + async def _poll_events(self) -> None: + while not self._request_close_event.is_set(): + message = await self._recv_json() + + op = message["op"] + data = message["d"] + + if op == _GatewayOpcode.DISPATCH: + event = message["t"] + self._seq = message["s"] + if event == "READY": + self._session_id = data["session_id"] + self.logger.info("connection is ready [session:%s]", self._session_id) + self._ready_event.set() + elif event == "RESUME": + self.logger.info("connection has resumed [session:%s, seq:%s]", self._session_id, self._seq) + self._resumed_event.set() + + asyncio.create_task(self._dispatch(self, event, data), name=f"shard {self._shard_id} dispatch {event}") + + elif op == _GatewayOpcode.HEARTBEAT: + self.logger.debug("received HEARTBEAT; sending HEARTBEAT ACK") + await self._send_json({"op": _GatewayOpcode.HEARTBEAT_ACK}) + + elif op == _GatewayOpcode.HEARTBEAT_ACK: + self.heartbeat_latency = self._now() - self.last_heartbeat_sent + self.logger.debug("received HEARTBEAT ACK [latency:%ss]", self.heartbeat_latency) + + elif op == _GatewayOpcode.RECONNECT: + self.logger.debug("RECONNECT") + + # 4000 close code allows us to resume without the session being invalided + await self._ws.close(code=4000, message=b"processing RECONNECT") + raise _Reconnect() + + elif op == _GatewayOpcode.INVALID_SESSION: + self.logger.debug("INVALID SESSION [resume:%s]", data) + await self._ws.close(code=4000, message=b"processing INVALID SESSION") + raise _InvalidSession(data) + + else: + self.logger.debug("ignoring unrecognised opcode %s", op) + + async def _recv_json(self) -> more_typing.JSONObject: + message = await self._recv_raw() + + if message.type == aiohttp.WSMsgType.BINARY: + n, string = await self._recv_zlib_str(message.data) + self._log_pl_debug(string, "received %s zlib encoded packets", n) + elif message.type == aiohttp.WSMsgType.TEXT: + string = message.data + self._log_pl_debug(string, "received text payload") + elif message.type == aiohttp.WSMsgType.CLOSE: + close_code = self._ws.close_code + self.logger.debug("connection closed with code %s", close_code) + + reason = _GatewayCloseCode(close_code).name if close_code in _GatewayCloseCode else "unknown close code" + + can_reconnect = close_code in ( + _GatewayCloseCode.DECODE_ERROR, + _GatewayCloseCode.INVALID_SEQ, + _GatewayCloseCode.UNKNOWN_ERROR, + _GatewayCloseCode.SESSION_TIMEOUT, + _GatewayCloseCode.RATE_LIMITED, + ) + + raise errors.GatewayServerClosedConnectionError(reason, close_code, can_reconnect, False, True) + elif message.type == aiohttp.WSMsgType.CLOSING or message.type == aiohttp.WSMsgType.CLOSED: + if self._request_close_event.is_set(): + self.logger.debug("user has requested the gateway to close") + raise errors.GatewayClientClosedError() + self.logger.debug("connection has been closed unexpectedly, probably a network issue") + raise errors.GatewayClientDisconnectedError() + else: + # Assume exception for now. + ex = self._ws.exception() + self.logger.debug("encountered unexpected error", exc_info=ex) + raise errors.GatewayError("Unexpected websocket exception from gateway") from ex + + return json.loads(string) + + async def _recv_zlib_str(self, first_packet: bytes) -> typing.Tuple[int, str]: + buff = bytearray(first_packet) + + packets = 1 + + while not buff.endswith(b"\x00\x00\xff\xff"): + message = await self._recv_raw() + if message.type != aiohttp.WSMsgType.BINARY: + raise errors.GatewayError(f"Expected a binary message but got {message.type}") + buff.append(message.data) + packets += 1 + + return packets, self._zlib.decompress(buff).decode("utf-8") + + async def _recv_raw(self) -> aiohttp.WSMessage: + packet = await self._ws.receive() + self.last_message_received = self._now() + return packet + + async def _send_json(self, payload: more_typing.JSONObject) -> None: + await self.ratelimiter.acquire() + message = json.dumps(payload) + self._log_pl_debug(message, "sending json payload") + await self._ws.send_str(message) + + @staticmethod + def _now() -> float: + return time.perf_counter() + + def _log_pl_debug(self, payload: str, message: str, *args: typing.Any) -> None: + message = f"{message} [seq:%s, session:%s, size:%s]" + if self._debug: + message = f"{message} with raw payload: %s" + args = (*args, self._seq, self._session_id, len(payload), payload) + else: + args = (*args, self._seq, self._session_id, len(payload)) + + self.logger.debug(message, *args) + + def _build_presence_payload( + self, + idle_since: unset.MayBeUnset[typing.Optional[datetime.datetime]] = unset.UNSET, + is_afk: unset.MayBeUnset[bool] = unset.UNSET, + status: unset.MayBeUnset[guilds.PresenceStatus] = unset.UNSET, + activity: unset.MayBeUnset[typing.Optional[gateway.Activity]] = unset.UNSET, + ) -> more_typing.JSONObject: + if unset.is_unset(idle_since): + idle_since = self._idle_since + if unset.is_unset(is_afk): + is_afk = self._is_afk + if unset.is_unset(status): + status = self._status + if unset.is_unset(activity): + activity = self._activity + + return { + "since": idle_since.timestamp() if idle_since is not None else None, + "afk": is_afk if is_afk is not None else False, + "status": status.value if status is not None else guilds.PresenceStatus.ONLINE.value, + "game": activity.serialize() if activity is not None else None, + } diff --git a/hikari/gateway/__init__.py b/hikari/gateway/__init__.py deleted file mode 100644 index a3c200478e..0000000000 --- a/hikari/gateway/__init__.py +++ /dev/null @@ -1,27 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# Copyright © Nekoka.tt 2019-2020 -# -# This file is part of Hikari. -# -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . -"""Components used for interacting with discord's gateway API.""" - -from __future__ import annotations - -from .client import * -from .connection import * -from .gateway_state import * - -__all__ = client.__all__ + connection.__all__ + gateway_state.__all__ diff --git a/hikari/gateway/client.py b/hikari/gateway/client.py deleted file mode 100644 index 9a785744c9..0000000000 --- a/hikari/gateway/client.py +++ /dev/null @@ -1,492 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# Copyright © Nekoka.tt 2019-2020 -# -# This file is part of Hikari. -# -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . -"""Provides a facade around `hikari.gateway.connection.Shard`. - -This handles parsing and initializing the object from a configuration, as -well as restarting it if it disconnects. - -Additional functions and coroutines are provided to update the presence on the -shard using models defined in `hikari`. -""" - -from __future__ import annotations - -__all__ = ["GatewayClient"] - -import asyncio -import time -import typing - -import aiohttp - -from hikari import aiohttp_config -from hikari import errors -from hikari.api import gateway_zookeeper -from hikari.gateway import connection as gateway_connection -from hikari.gateway import gateway_state -from hikari.gateway import runnable -from hikari.internal import codes -from hikari.internal import helpers -from hikari.internal import ratelimits - -if typing.TYPE_CHECKING: - import datetime - - from hikari.models import gateway - from hikari.models import guilds - from hikari.models import intents as intents_ - - -class GatewayClient(runnable.RunnableClient): - """The primary interface for a single shard connection. - - This contains several abstractions to enable usage of the low - level gateway network interface with the higher level constructs - in `hikari`. - - Parameters - ---------- - shard_id : int - The ID of this specific shard. - shard_id : int - The number of shards that make up this distributed application. - app : hikari.components.application.Application - The client application that this shard client should be bound by. - Includes the the gateway configuration to use to initialize this shard - and the consumer of a raw event. - url : str - The URL to connect the gateway to. - - !!! note - Generally, you want to use - `hikari.clients.bot_base.Application` rather than this class - directly, as that will handle sharding where enabled and applicable, - and provides a few more bits and pieces that may be useful such as state - management and event dispatcher integration. and If you want to customize - this, you can subclass it and simply override anything you want. - """ - - __slots__ = ( - "_activity", - "_zookeeper", - "_connection", - "_idle_since", - "_is_afk", - "_raw_event_consumer", - "_shard_state", - "_status", - "_task", - "logger", - ) - - def __init__( - self, - *, - config: aiohttp_config.AIOHTTPConfig, - debug: bool, - initial_activity: typing.Optional[gateway.Activity], - initial_idle_since: typing.Optional[datetime.datetime], - initial_is_afk: bool, - initial_status: guilds.PresenceStatus, - intents: typing.Optional[intents_.Intent], - large_threshold: int, - shard_id: int, - shard_count: int, - token: str, - url: str, - use_compression: bool, - version: int, - zookeeper: gateway_zookeeper.IGatewayZookeeper, - ) -> None: - super().__init__(helpers.get_logger(self, str(shard_id))) - self._zookeeper = zookeeper - self._activity = initial_activity - self._idle_since = initial_idle_since - self._is_afk = initial_is_afk - self._status = initial_status - self._shard_state = gateway_state.GatewayState.NOT_RUNNING - self._task = None - self._connection = gateway_connection.Shard( - compression=use_compression, - connector=config.tcp_connector, - debug=debug, - # This is a bit of a cheat, we should pass a coroutine function here, but - # instead we just use a lambda that does the transformation we want (replaces the - # low-level shard argument with the reference to this class object), then return - # the result of that coroutine. To the low level client, it looks the same :-) - # (also hides a useless stack frame from tracebacks, I guess). - # FIXME: implement dispatch. - dispatcher=lambda c, n, pl: self._zookeeper.event_consumer.consume_raw_event(self, n, pl), - initial_presence=self._create_presence_pl( - status=initial_status, activity=initial_activity, idle_since=initial_idle_since, is_afk=initial_is_afk, - ), - intents=intents, - large_threshold=large_threshold, - proxy_auth=config.proxy_auth, - proxy_headers=config.proxy_headers, - proxy_url=config.proxy_url, - session_id=None, - seq=None, - shard_id=shard_id, - shard_count=shard_count, - ssl_context=config.ssl_context, - token=token, - url=url, - verify_ssl=config.verify_ssl, - version=version, - ) - - @property - def shard_id(self) -> int: - """Shard ID (this is 0-indexed).""" - return self._connection.shard_id - - @property - def shard_count(self) -> int: - """Count of how many shards make up this bot.""" - return self._connection.shard_count - - @property - def status(self) -> guilds.PresenceStatus: - """User status for this shard.""" - return self._status - - @property - def activity(self) -> typing.Optional[gateway.Activity]: - """Activity for the user status for this shard. - - This will be `None` if there is no activity. - """ - return self._activity - - @property - def idle_since(self) -> typing.Optional[datetime.datetime]: - """Timestamp of when the user of this shard appeared to be idle. - - This will be `None` if not applicable. - """ - return self._idle_since - - @property - def is_afk(self) -> bool: - """Whether the user is appearing as AFK or not.""" - return self._is_afk - - @property - def heartbeat_latency(self) -> float: - """Latency between sending a HEARTBEAT and receiving an ACK in seconds. - - This will be `float("nan")` until the first heartbeat is performed. - """ - return self._connection.heartbeat_latency - - @property - def heartbeat_interval(self) -> float: - """Time period to wait between sending HEARTBEAT payloads in seconds. - - This will be `float("nan")` until the connection has received a `HELLO` - payload. - """ - return self._connection.heartbeat_interval - - @property - def disconnect_count(self) -> int: - """Count of number of times this shard's connection has disconnected.""" - return self._connection.disconnect_count - - @property - def reconnect_count(self) -> int: - """Count of number of times this shard's connection has reconnected. - - This includes RESUME and re-IDENTIFY events. - """ - return self._connection.reconnect_count - - @property - def connection_state(self) -> gateway_state.GatewayState: - """State of this shard's connection.""" - return self._shard_state - - @property - def is_connected(self) -> bool: - """Whether the shard is connected or not.""" - return self._connection.is_connected - - @property - def seq(self) -> typing.Optional[int]: - """Sequence ID of the shard. - - This is the number of payloads that have been received since the last - `IDENTIFY` was sent. If not yet identified, this will be `None`. - """ - return self._connection.seq - - @property - def session_id(self) -> typing.Optional[str]: - """Session ID of the shard connection. - - Will be `None` if there is no session. - """ - return self._connection.session_id - - @property - def version(self) -> float: - """Version being used for the gateway API.""" - return self._connection.version - - @property - def intents(self) -> typing.Optional[intents_.Intent]: - """Intents that are in use for the shard connection. - - If intents are not being used at all, then this will be `None` instead. - """ - return self._connection.intents - - async def start(self): - """Connect to the gateway on this shard and keep the connection alive. - - This will wait for the shard to dispatch a `READY` event, and - then return. - """ - if self._shard_state not in (gateway_state.GatewayState.NOT_RUNNING, gateway_state.GatewayState.STOPPED): - raise RuntimeError("Cannot start a shard twice") - - self._task = asyncio.create_task(self._keep_alive(), name="GatewayClient#keep_alive") - - completed, _ = await asyncio.wait( - [self._task, self._connection.ready_event.wait()], return_when=asyncio.FIRST_COMPLETED - ) - - for task in completed: - if ex := task.exception(): - raise ex - - async def join(self) -> None: - """Wait for the shard to shut down fully.""" - if self._task: - await self._task - - async def close(self) -> None: - """Request that the shard shuts down. - - This will wait for the client to shut down before returning. - """ - if self._shard_state != gateway_state.GatewayState.STOPPING: - self._shard_state = gateway_state.GatewayState.STOPPING - self.logger.debug("stopping shard") - - await self._connection.close() - - if self._task is not None: - await self._task - - self._shard_state = gateway_state.GatewayState.STOPPED - - async def _keep_alive(self): # pylint: disable=too-many-branches - back_off = ratelimits.ExponentialBackOff(base=1.85, maximum=600, initial_increment=2) - last_start = time.perf_counter() - do_not_back_off = True - - while True: - try: - if not do_not_back_off and time.perf_counter() - last_start < 30: - next_back_off = next(back_off) - self.logger.info( - "restarted within 30 seconds, will backoff for %.2fs", next_back_off, - ) - await asyncio.sleep(next_back_off) - else: - back_off.reset() - - last_start = time.perf_counter() - do_not_back_off = False - - connect_task = await self._spin_up() - - await connect_task - self.logger.critical("shut down silently! this shouldn't happen!") - - except aiohttp.ClientConnectorError as ex: - self.logger.exception( - "failed to connect to Discord to initialize a websocket connection", exc_info=ex, - ) - - except errors.GatewayZombiedError: - self.logger.warning("entered a zombie state and will be restarted") - - except errors.GatewayInvalidSessionError as ex: - if ex.can_resume: - self.logger.warning("invalid session, so will attempt to resume") - else: - self.logger.warning("invalid session, so will attempt to reconnect") - self._connection.seq = None - self._connection.session_id = None - - do_not_back_off = True - await asyncio.sleep(5) - - except errors.GatewayMustReconnectError: - self.logger.warning("instructed by Discord to reconnect") - do_not_back_off = True - await asyncio.sleep(5) - - except errors.GatewayServerClosedConnectionError as ex: - if ex.close_code in ( - codes.GatewayCloseCode.NOT_AUTHENTICATED, - codes.GatewayCloseCode.AUTHENTICATION_FAILED, - codes.GatewayCloseCode.ALREADY_AUTHENTICATED, - codes.GatewayCloseCode.SHARDING_REQUIRED, - codes.GatewayCloseCode.INVALID_VERSION, - codes.GatewayCloseCode.INVALID_INTENT, - codes.GatewayCloseCode.DISALLOWED_INTENT, - ): - self.logger.error("disconnected by Discord, %s: %s", type(ex).__name__, ex.reason) - raise ex from None - - self.logger.warning("disconnected by Discord, will attempt to reconnect") - - except errors.GatewayClientDisconnectedError: - self.logger.warning("unexpected connection close, will attempt to reconnect") - - except errors.GatewayClientClosedError: - self.logger.warning("gateway client closed by user, will not attempt to restart") - return - except Exception as ex: - self.logger.debug("propagating unexpected exception", exc_info=ex) - raise ex - - async def _spin_up(self) -> asyncio.Task: - self.logger.debug("initializing shard") - self._shard_state = gateway_state.GatewayState.CONNECTING - - is_resume = self._connection.seq is not None and self._connection.session_id is not None - - connect_task = asyncio.create_task(self._connection.connect(), name="Shard#connect") - - completed, _ = await asyncio.wait( - [connect_task, self._connection.hello_event.wait()], return_when=asyncio.FIRST_COMPLETED - ) - - for task in completed: - if ex := task.exception(): - raise ex - - self.logger.info("received HELLO, interval is %ss", self._connection.heartbeat_interval) - - completed, _ = await asyncio.wait( - [connect_task, self._connection.handshake_event.wait()], return_when=asyncio.FIRST_COMPLETED - ) - - for task in completed: - if ex := task.exception(): - raise ex - - if is_resume: - self.logger.info("sent RESUME, waiting for RESUMED event") - self._shard_state = gateway_state.GatewayState.RESUMING - - completed, _ = await asyncio.wait( - [connect_task, self._connection.resumed_event.wait()], return_when=asyncio.FIRST_COMPLETED - ) - - for task in completed: - if ex := task.exception(): - raise ex - - self.logger.info("now RESUMED") - - else: - self.logger.info("sent IDENTIFY, waiting for READY event") - - self._shard_state = gateway_state.GatewayState.WAITING_FOR_READY - - completed, _ = await asyncio.wait( - [connect_task, self._connection.ready_event.wait()], return_when=asyncio.FIRST_COMPLETED - ) - - for task in completed: - if ex := task.exception(): - raise ex - - self.logger.info("now READY") - - self._shard_state = gateway_state.GatewayState.READY - - return connect_task - - async def update_presence( - self, - *, - status: guilds.PresenceStatus = ..., - activity: typing.Optional[gateway.Activity] = ..., - idle_since: typing.Optional[datetime.datetime] = ..., - is_afk: bool = ..., - ) -> None: - """Update the presence of the user for the shard. - - This will only update arguments that you explicitly specify a value for. - Any arguments that you do not explicitly provide some value for will - not be changed. - - !!! warning - This will fail if the shard is not online. - - Parameters - ---------- - status : hikari.models.guilds.PresenceStatus - If specified, the new status to set. - activity : hikari.models.gateway.Activity, optional - If specified, the new activity to set. - idle_since : datetime.datetime, optional - If specified, the time to show up as being idle since, or - `None` if not applicable. - is_afk : bool - If specified, whether the user should be marked as AFK. - """ - # We wouldn't ever want to do this, so throw an error if it happens. - if status is ... and activity is ... and idle_since is ... and is_afk is ...: - raise ValueError("update_presence requires at least one argument to be passed") - - status = self._status if status is ... else status - activity = self._activity if activity is ... else activity - idle_since = self._idle_since if idle_since is ... else idle_since - is_afk = self._is_afk if is_afk is ... else is_afk - - presence = self._create_presence_pl(status=status, activity=activity, idle_since=idle_since, is_afk=is_afk) - await self._connection.update_presence(presence) - - # If we get this far, the update succeeded probably, or the gateway just died. Whatever. - self._status = status - self._activity = activity - self._idle_since = idle_since - self._is_afk = is_afk - - @staticmethod - def _create_presence_pl( - status: guilds.PresenceStatus, - activity: typing.Optional[gateway.Activity], - idle_since: typing.Optional[datetime.datetime], - is_afk: bool, - ) -> typing.Dict[str, typing.Any]: - return { - "status": status, - "idle_since": idle_since.timestamp() * 1000 if idle_since is not None else None, - "game": activity.serialize() if activity is not None else None, - "afk": is_afk, - } diff --git a/hikari/gateway/connection.py b/hikari/gateway/connection.py deleted file mode 100644 index 96b8bbfa9a..0000000000 --- a/hikari/gateway/connection.py +++ /dev/null @@ -1,891 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# Copyright © Nekokatt 2019-2020 -# -# This file is part of Hikari. -# -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . -"""Single-threaded asyncio Gateway implementation. - -Handles regular heartbeating in a background task -on the same event loop. Implements zlib transport compression only. - -Can be used as the main gateway connection for a single-sharded bot, or the gateway connection for a -specific shard in a swarm of shards making up a larger bot. - -References ----------- -* [IANA WS closure code standards](https://www.iana.org/assignments/websocket/websocket.xhtml) -* [Gateway documentation](https://discord.com/developers/docs/topics/gateway) -* [Opcode documentation](https://discord.com/developers/docs/topics/opcodes-and-status-codes) -""" - -from __future__ import annotations - -__all__ = ["Shard"] - -import asyncio -import contextlib -import datetime -import json -import math -import time -import typing -import urllib.parse -import zlib - -import aiohttp.typedefs - -from hikari import errors -from hikari.internal import codes -from hikari.internal import http_client -from hikari.internal import more_asyncio -from hikari.internal import more_typing -from hikari.internal import ratelimits -from hikari.internal import user_agents - -if typing.TYPE_CHECKING: - import ssl - - from hikari.models import intents as _intents - -DispatcherT = typing.Callable[["Shard", str, typing.Dict], more_typing.Coroutine[None]] -"""The signature for an event dispatch callback.""" - -VERSION_6: typing.Final[int] = 6 -VERSION_7: typing.Final[int] = 7 - - -class Shard(http_client.HTTPClient): # pylint: disable=too-many-instance-attributes - """Implementation of a client for the Discord Gateway. - - This is a websocket connection to Discord that is used to inform your - application of events that occur, and to allow you to change your presence, - amongst other real-time applications. - - Each `Shard` represents a single shard. - - Expected events that may be passed to the event dispatcher are documented in the - [gateway event reference](https://discord.com/developers/docs/topics/gateway#commands-and-events) . - No normalization of the gateway event names occurs. In addition to this, - some internal events can also be triggered to notify you of changes to - the connection state. - - * `CONNECTED` - fired on initial connection to Discord. - * `DISCONNECTED` - fired when the connection is closed for any reason. - - Parameters - ---------- - allow_redirects : bool - Whether to allow redirects or not. Defaults to `False`. - compression : bool - If `True`, then payload compression is enabled on the connection. - If `False`, no payloads are compressed. You usually want to keep this - enabled. - connector : aiohttp.BaseConnector | None - The `aiohttp.BaseConnector` to use for the HTTP session that - gets upgraded to a websocket connection. You can use this to customise - connection pooling, etc. - debug : bool - If `True`, the client is configured to provide extra contextual - information to use when debugging this library or extending it. This - includes logging every payload that is sent or received to the logger - as debug entries. Generally it is best to keep this disabled. - dispatcher : `async def dispatch(shard, event_name, payload)` - The function to invoke with any dispatched events. This must be a - coroutine function, and must take three arguments only. The first is - the reference to this `Shard` The second is the - event name. - initial_presence : typing.Dict | None - A raw JSON object as a `typing.Dict` that should be set as the - initial presence of the bot user once online. If `None`, then it - will be set to the default, which is showing up as online without a - custom status message. - intents : hikari.models.intents.Intent | None - Bitfield of intents to use. If you use the V7 API, this is mandatory. - This field will determine what events you will receive. - json_deserialize : `deserialization function` - A custom JSON deserializer function to use. Defaults to `json.loads`. - json_serialize : `serialization function` - A custom JSON serializer function to use. Defaults to `json.dumps`. - large_threshold : int - The number of members that have to be in a guild for it to be - considered to be "large". Large guilds will not have member information - sent automatically, and must manually request that member chunks be - sent using `ShardConnection.request_guild_members`. - proxy_auth : aiohttp.BasicAuth | None - Optional `aiohttp.BasicAuth` object that can be provided to - allow authenticating with a proxy if you use one. Leave `None` to ignore. - proxy_headers : typing.Mapping[str, str] | None - Optional `typing.Mapping` to provide as headers to allow the - connection through a proxy if you use one. Leave `None` to ignore. - proxy_url : str | None - Optional `str` to use for a proxy server. If `None`, then it is ignored. - session_id : str | None - The session ID to use. If specified along with `seq`, then the - gateway client will attempt to `RESUME` an existing session rather than - re-`IDENTIFY`. Otherwise, it will be ignored. - seq : int | None - The sequence number to use. If specified along with `session_id`, then - the gateway client will attempt to `RESUME` an existing session rather - than re-`IDENTIFY`. Otherwise, it will be ignored. - shard_id : int - The shard ID of this gateway client. Defaults to `0`. - shard_count : int - The number of shards on this gateway. Defaults to `1`, which implies no - sharding is taking place. - ssl_context : ssl.SSLContext | None - An optional custom `ssl.SSLContext` to provide to customise how - SSL works. - timeout : float | None - The optional timeout for all HTTP requests. - token : str - The mandatory bot token for the bot account to use, minus the "Bot" - authentication prefix used elsewhere. - trust_env : bool - If `True`, and no proxy info is given, then `HTTP_PROXY` and - `HTTPS_PROXY` will be used from the environment variables if present. - Any proxy credentials will be read from the user's `netrc` file - (https://www.gnu.org/software/inetutils/manual/html_node/The-_002enetrc-file.html) - If `False`, then this information is instead ignored. - Defaults to `False` if unspecified. - url : str - The websocket URL to use. - verify_ssl : bool - If `True`, SSL verification is enabled, which is generally what you - want. If you get SSL issues, you can try turning this off at your own - risk. - version : int - The version of the API to use. Defaults to the most recent stable - version (v6). - """ - - __slots__ = ( - "_compression", - "_connected_at", - "_intents", - "_large_threshold", - "_presence", - "_ratelimiter", - "_token", - "_url", - "_ws", - "_zlib", - "closed_event", - "disconnect_count", - "dispatcher", - "handshake_event", - "heartbeat_interval", - "heartbeat_latency", - "hello_event", - "last_heartbeat_sent", - "last_message_received", - "ready_event", - "requesting_close_event", - "resumed_event", - "seq", - "session_id", - "shard_count", - "shard_id", - "status", - "version", - ) - - closed_event: typing.Final[asyncio.Event] - """An event that is set when the connection closes.""" - - disconnect_count: int - """The number of times we have disconnected from the gateway on this - client instance. - """ - - dispatcher: DispatcherT - """The dispatch method to call when dispatching a new event. - - This is the method passed in the constructor. - """ - - heartbeat_interval: float - """The heartbeat interval Discord instructed the client to beat at. - - This is `nan` until this information is received. - """ - - heartbeat_latency: float - """The most recent heartbeat latency measurement in seconds. - - This is `nan` until this information is available. The latency is calculated - as the time between sending a `HEARTBEAT` payload and receiving a - `HEARTBEAT_ACK` response. - """ - - hello_event: typing.Final[asyncio.Event] - """An event that is set when Discord sends a `HELLO` payload. - - This indicates some sort of connection has successfully been made. - """ - - handshake_event: typing.Final[asyncio.Event] - """An event that is set when the client has successfully `IDENTIFY`ed - or `RESUMED` with the gateway. - - This indicates regular communication can now take place on the connection - and events can be expected to be received. - """ - - last_heartbeat_sent: float - """The monotonic timestamp that the last `HEARTBEAT` was sent at. - - This will be `nan` if no `HEARTBEAT` has yet been sent. - """ - - last_message_received: float - """The monotonic timestamp at which the last payload was received from Discord. - - If this was more than the `heartbeat_interval` from the current time, then - the connection is assumed to be zombied and is shut down. - If no messages have been received yet, this is `nan`. - """ - - ready_event: typing.Final[asyncio.Event] - """An event that is triggered when a `READY` payload is received for the shard. - - This indicates that it successfully started up and had a correct sharding - configuration. This is more appropriate to wait for than - `Shard.handshake_event` since the former will still fire if starting - shards too closely together, for example. This would still lead to an - immediate invalid session being fired afterwards. - - It is worth noting that this event is only set for the first `READY` event - after connecting with a fresh connection. For all other purposes, you should - wait for the event to be fired in the `dispatch` function you provide. - """ - - resumed_event: typing.Final[asyncio.Event] - """An event that is triggered when a resume has succeeded on the gateway.""" - - requesting_close_event: typing.Final[asyncio.Event] - """An event that is set when something requests that the connection should - close somewhere. - """ - - session_id: typing.Optional[str] - """The current session ID, if known.""" - - seq: typing.Optional[int] - """The current sequence number for state synchronization with the API, - if known. - """ - - shard_id: typing.Final[int] - """The shard ID.""" - - shard_count: typing.Final[int] - """The number of shards in use for the bot.""" - - version: typing.Final[int] - """The API version to use on Discord.""" - - def __init__( # pylint: disable=too-many-locals - self, - *, - allow_redirects: bool = False, - compression: bool = True, - connector: typing.Optional[aiohttp.BaseConnector] = None, - debug: bool = False, - dispatcher: DispatcherT, - initial_presence: typing.Optional[typing.Dict] = None, - intents: typing.Optional[_intents.Intent] = None, - json_deserialize: typing.Callable[[typing.AnyStr], typing.Dict] = json.loads, - json_serialize: typing.Callable[[typing.Dict], typing.AnyStr] = json.dumps, - large_threshold: int = 250, - proxy_auth: typing.Optional[aiohttp.BasicAuth] = None, - proxy_headers: typing.Optional[aiohttp.typedefs.LooseHeaders] = None, - proxy_url: typing.Optional[str] = None, - session_id: typing.Optional[str] = None, - seq: typing.Optional[int] = None, - shard_id: int = 0, - shard_count: int = 1, - ssl_context: typing.Optional[ssl.SSLContext] = None, - timeout: typing.Optional[float] = None, - token: str, - trust_env: bool = False, - url: str, - verify_ssl: bool = True, - version: int = VERSION_6, - ) -> None: - # Sanitise the URL... - scheme, netloc, path, params, _, _ = urllib.parse.urlparse(url, allow_fragments=True) - - new_query = dict(v=int(version), encoding="json") - if compression: - # payload compression - new_query["compress"] = "zlib-stream" - - new_query = urllib.parse.urlencode(new_query) - - url = urllib.parse.urlunparse((scheme, netloc, path, params, new_query, "")) - - super().__init__( - allow_redirects=allow_redirects, - connector=connector, - debug=debug, - json_deserialize=json_deserialize, - json_serialize=json_serialize, - logger_name=f"{type(self).__module__}.{type(self).__qualname__}.{shard_id}", - proxy_auth=proxy_auth, - proxy_headers=proxy_headers, - proxy_url=proxy_url, - ssl_context=ssl_context, - verify_ssl=verify_ssl, - timeout=timeout, - trust_env=trust_env, - ) - - self._compression = compression - self._connected_at = float("nan") - self._intents = intents - self._large_threshold = large_threshold - self._presence = initial_presence - self._ratelimiter = ratelimits.WindowedBurstRateLimiter(str(shard_id), 60.0, 120) - self._token = token - self._url = url - self._ws = None - self._zlib = None - self.closed_event = asyncio.Event() - self.disconnect_count = 0 - self.dispatcher = dispatcher - self.heartbeat_interval = float("nan") - self.heartbeat_latency = float("nan") - self.hello_event = asyncio.Event() - self.handshake_event = asyncio.Event() - self.last_heartbeat_sent = float("nan") - self.last_message_received = float("nan") - self.requesting_close_event = asyncio.Event() - self.ready_event = asyncio.Event() - self.resumed_event = asyncio.Event() - self.session_id = session_id - self.seq = seq - self.shard_id = shard_id - self.shard_count = shard_count - self.version = version - - @property - def up_time(self) -> datetime.timedelta: - """Amount of time the connection has been running for. - - If this connection isn't running, this will always be `0` seconds. - """ - delta = time.perf_counter() - self._connected_at - return datetime.timedelta(seconds=0 if math.isnan(delta) else delta) - - @property - def is_connected(self) -> bool: - """Whether the gateway is connected or not.""" - return not math.isnan(self._connected_at) - - @property - def intents(self) -> typing.Optional[_intents.Intent]: - """Intents being used. - - If this is `None`, no intent usage was being used on this shard. - On V6 this would be regular usage as prior to the intents change in - January 2020. If on V7, you just won't be able to connect at all to the - gateway. - """ - return self._intents - - @property - def reconnect_count(self) -> int: - """Amount of times the gateway has reconnected since initialization. - - This can be used as a debugging context, but is also used internally - for exception management. - """ - # 0 disconnects + not is_connected => 0 - # 0 disconnects + is_connected => 0 - # 1 disconnects + not is_connected = 0 - # 1 disconnects + is_connected = 1 - # 2 disconnects + not is_connected = 1 - # 2 disconnects + is_connected = 2 - return max(0, self.disconnect_count - int(not self.is_connected)) - - # Ignore docstring not starting in an imperative mood - @property - def current_presence(self) -> typing.Dict: # noqa: D401 - """Current presence for the gateway.""" - # Make a shallow copy to prevent mutation. - return dict(self._presence or {}) - - @typing.overload - async def request_guild_members(self, guild_id: str, *guild_ids: str, limit: int = 0, query: str = "") -> None: - """Request guild members in the given guilds using a query string and an optional limit.""" - - @typing.overload - async def request_guild_members(self, guild_id: str, *guild_ids: str, user_ids: typing.Sequence[str]) -> None: - """Request guild members in the given guilds using a set of user IDs to resolve.""" - - async def request_guild_members(self, guild_id, *guild_ids, **kwargs): - """Request the guild members for a guild or set of guilds. - - These guilds must be being served by this shard, and the results will be - provided to the dispatcher with `GUILD_MEMBER_CHUNK` events. - - Parameters - ---------- - guild_id : str - The first guild to request members for. - *guild_ids : str - Additional guilds to request members for. - **kwargs - Optional arguments. - - Keyword Args - ------------ - limit : int - Limit for the number of members to respond with. Set to `0` to be - unlimited. - query : str - An optional string to filter members with. If specified, only - members who have a username starting with this string will be - returned. - user_ids : typing.Sequence[str] - An optional list of user IDs to return member info about. - - !!! note - You may not specify `user_id` at the same time as `limit` and - `query`. Likewise, if you specify one of `limit` or `query`, the - other must also be included. The default, if no optional arguments - are specified, is to use a `limit = 0` and a `query = ""` - (empty-string). - """ - guilds = [guild_id, *guild_ids] - constraints = {} - - if "presences" in kwargs: - constraints["presences"] = kwargs["presences"] - - if "user_ids" in kwargs: - constraints["user_ids"] = kwargs["user_ids"] - else: - constraints["query"] = kwargs.get("query", "") - constraints["limit"] = kwargs.get("limit", 0) - - self.logger.debug( - "requesting guild members for guilds %s with constraints %s", guilds, constraints, - ) - - await self._send({"op": codes.GatewayOpcode.REQUEST_GUILD_MEMBERS, "d": {"guild_id": guilds, **constraints}}) - - async def update_presence(self, presence: typing.Dict) -> None: - """Change the presence of the bot user for this shard. - - Parameters - ---------- - presence : typing.Dict - The new presence payload to set. - """ - presence.setdefault("since", None) - presence.setdefault("game", None) - presence.setdefault("status", "online") - presence.setdefault("afk", False) - - self.logger.debug("updating presence to %r", presence) - await self._send({"op": codes.GatewayOpcode.PRESENCE_UPDATE, "d": presence}) - self._presence = presence - - async def update_voice_state( - self, guild_id: str, channel_id: typing.Optional[str], self_mute: bool = False, self_deaf: bool = False, - ) -> None: - """Send a VOICE_STATE_UPDATE payload for voice control. - - After sending this payload, you should wait for a VOICE_STATE_UPDATE - event to be received for the corresponding guild and channel ID. This - will contain instructions where appropriate to continue with creating - a new voice connection, etc. - - This implementation will only request the initial voice payload. Any - logic for initializing a voice connection must be done separately. - - Parameters - ---------- - guild_id : str - The guild ID to change the voice state within. - channel_id : str | None - The channel ID in the guild to change the voice state within. - If this is `None`, then this will behave as a disconnect opcode. - self_mute : bool - If `True`, then the bot user will be muted. Defaults to `False`. - self_deaf : bool - If `True`, then the bot user will be deafened. Defaults to `False`. - This doesn't currently have much effect, since receiving voice - data is undocumented for bots. - """ - payload = { - "op": codes.GatewayOpcode.VOICE_STATE_UPDATE, - "d": {"guild_id": guild_id, "channel_id": channel_id, "self_mute": self_mute, "self_deaf": self_deaf}, - } - - await self._send(payload) - - async def connect(self) -> None: - """Connect to the gateway and return when it closes.""" - if self.is_connected: - raise RuntimeError("Already connected") - - self.closed_event.clear() - self.hello_event.clear() - self.handshake_event.clear() - self.ready_event.clear() - self.requesting_close_event.clear() - self.resumed_event.clear() - - # 1000 and 1001 will invalidate sessions, 1006 (used here before) - # is a sketchy area as to the intent. 4000 is known to work normally. - close_code = codes.GatewayCloseCode.UNKNOWN_ERROR - - try: - self._ws = await self._create_ws(self._url, compress=0, autoping=True, max_msg_size=0) - self._zlib = zlib.decompressobj() - self.logger.debug("expecting HELLO") - pl = await self._receive() - - self._connected_at = time.perf_counter() - - op = pl["op"] - if op != 10: - raise errors.GatewayError(f"Expected HELLO opcode 10 but received {op}") - - self.heartbeat_interval = pl["d"]["heartbeat_interval"] / 1_000.0 - - self.hello_event.set() - - self.do_dispatch("CONNECTED", {}) - self.logger.debug("received HELLO [interval:%ss]", self.heartbeat_interval) - - # noinspection PyTypeChecker - completed, pending_tasks = await more_asyncio.wait( - [self._heartbeat_keep_alive(self.heartbeat_interval), self._run()], return_when=asyncio.FIRST_COMPLETED, - ) - - # Kill other running tasks now. - for pending_task in pending_tasks: - pending_task.cancel() - with contextlib.suppress(Exception): - # Clear any pending exception to prevent a nasty console message. - pending_task.result() - - # If the heartbeat call closes normally, then we want to get the exception - # raised by the identify call if it raises anything. This prevents spammy - # exceptions being thrown if the client shuts down during the handshake, - # which becomes more and more likely when we consider bots may have many - # shards running, each taking min of 5s to start up after the first. - ex = None - while len(completed) > 0 and ex is None: - ex = completed.pop().exception() - - if ex is None: - # If no exception occurred, we must have exited non-exceptionally, indicating - # the close event was set without an error causing that flag to be changed. - ex = errors.GatewayClientClosedError() - close_code = codes.GatewayCloseCode.NORMAL_CLOSURE - - elif isinstance(ex, asyncio.TimeoutError): - # If we get timeout errors receiving stuff, propagate as a zombied connection. This - # is already done by the ping keepalive and heartbeat keepalive partially, but this - # is a second edge case. - ex = errors.GatewayZombiedError() - - if hasattr(ex, "close_code"): - close_code = ex.close_code - - raise ex - - finally: - self.closed_event.set() - - if not math.isnan(self._connected_at): - await self.close(close_code) - self.do_dispatch("DISCONNECTED", {}) - self.disconnect_count += 1 - - self._ws = None - - self._connected_at = float("nan") - self.last_heartbeat_sent = float("nan") - self.heartbeat_latency = float("nan") - self.last_message_received = float("nan") - - await super().close() - - async def close(self, close_code: int = 1000) -> None: - """Request this gateway connection closes. - - Parameters - ---------- - close_code : int - The close code to use. Defaults to `1000` (normal closure). - """ - if not self.requesting_close_event.is_set(): - self.logger.debug("closing websocket connection") - self.requesting_close_event.set() - # These will attribute error if they are not set; in this case we don't care, just ignore it. - with contextlib.suppress(asyncio.TimeoutError, AttributeError): - await asyncio.wait_for(self._ws.close(code=close_code), timeout=2.0) - with contextlib.suppress(asyncio.TimeoutError, AttributeError): - await asyncio.wait_for(super().close(), timeout=2.0) - self.closed_event.set() - elif self.debug: - self.logger.debug("websocket connection already requested to be closed, will not do anything else") - - async def _heartbeat_keep_alive(self, heartbeat_interval): - while not self.requesting_close_event.is_set(): - self._zombie_detector(heartbeat_interval) - self.logger.debug("preparing to send HEARTBEAT [s:%s, interval:%ss]", self.seq, self.heartbeat_interval) - await self._send({"op": codes.GatewayOpcode.HEARTBEAT, "d": self.seq}) - self.last_heartbeat_sent = time.perf_counter() - - try: - await asyncio.wait_for(self.requesting_close_event.wait(), timeout=heartbeat_interval) - except asyncio.TimeoutError: - pass - - def _zombie_detector(self, heartbeat_interval): - time_since_message = time.perf_counter() - self.last_message_received - if heartbeat_interval < time_since_message: - raise asyncio.TimeoutError( - f"{self.shard_id}: connection is a zombie, haven't received any message for {time_since_message}s" - ) - - async def _identify(self): - self.logger.debug("preparing to send IDENTIFY") - - # noinspection PyArgumentList - pl = { - "op": codes.GatewayOpcode.IDENTIFY, - "d": { - "token": self._token, - "compress": False, - "large_threshold": self._large_threshold, - "properties": user_agents.UserAgent().websocket_triplet, - "shard": [self.shard_id, self.shard_count], - }, - } - - # From october 2020, we will likely just make this always passed - if self._intents is not None: - pl["d"]["intents"] = self._intents - - if self._presence: - # noinspection PyTypeChecker - pl["d"]["presence"] = self._presence - await self._send(pl) - self.logger.debug("sent IDENTIFY") - self.handshake_event.set() - - async def _resume(self): - self.logger.debug("preparing to send RESUME") - pl = { - "op": codes.GatewayOpcode.RESUME, - "d": {"token": self._token, "seq": self.seq, "session_id": self.session_id}, - } - await self._send(pl) - self.logger.debug("sent RESUME") - - async def _run(self): - if self.session_id is None: - await self._identify() - else: - await self._resume() - - self.handshake_event.set() - - while not self.requesting_close_event.is_set(): - next_pl = await self._receive() - - op = next_pl["op"] - d = next_pl["d"] - - if op == codes.GatewayOpcode.DISPATCH: - self.seq = next_pl["s"] - event_name = next_pl["t"] - - if event_name == "READY": - self.session_id = d["session_id"] - version = d["v"] - - self.logger.debug( - "connection is READY [session:%s, version:%s]", self.session_id, version, - ) - - self.ready_event.set() - - elif event_name == "RESUMED": - self.resumed_event.set() - - self.logger.debug("connection has RESUMED [session:%s, s:%s])", self.session_id, self.seq) - - self.do_dispatch(event_name, d) - - elif op == codes.GatewayOpcode.HEARTBEAT: - self.logger.debug("received HEARTBEAT, preparing to send HEARTBEAT ACK to server in response") - await self._send({"op": codes.GatewayOpcode.HEARTBEAT_ACK}) - - elif op == codes.GatewayOpcode.RECONNECT: - self.logger.debug("instructed by gateway server to restart connection") - raise errors.GatewayMustReconnectError() - - elif op == codes.GatewayOpcode.INVALID_SESSION: - can_resume = bool(d) - self.logger.debug( - "instructed by gateway server to %s session", "resume" if can_resume else "restart", - ) - raise errors.GatewayInvalidSessionError(can_resume) - - elif op == codes.GatewayOpcode.HEARTBEAT_ACK: - now = time.perf_counter() - self.heartbeat_latency = now - self.last_heartbeat_sent - self.logger.debug("received HEARTBEAT ACK [latency:%ss]", self.heartbeat_latency) - - else: - self.logger.debug("ignoring opcode %s with data %r", op, d) - - async def _receive(self): # pylint: disable=too-many-branches - while True: - message = await self._receive_one_packet() - - if message.type == aiohttp.WSMsgType.TEXT: - obj = self.json_deserialize(message.data) - - if self.debug: - self.logger.debug("receive text payload %r", message.data) - else: - self.logger.debug( - "receive text payload (op:%s, t:%s, s:%s, size:%s)", - obj.get("op"), - obj.get("t"), - obj.get("s"), - len(message.data), - ) - return obj - - if message.type == aiohttp.WSMsgType.BINARY: - buffer = bytearray(message.data) - packets = 1 - while not buffer.endswith(b"\x00\x00\xff\xff"): - packets += 1 - message = await self._receive_one_packet() - if message.type != aiohttp.WSMsgType.BINARY: - raise errors.GatewayError(f"Expected a binary message but got {message.type}") - buffer.extend(message.data) - - pl = self._zlib.decompress(buffer) - obj = self.json_deserialize(pl) - - if self.debug: - self.logger.debug("receive %s zlib-encoded packets\n inbound payload: %r", packets, pl) - - else: - self.logger.debug( - "receive zlib payload [op:%s, t:%s, s:%s, size:%s, packets:%s]", - obj.get("op"), - obj.get("t"), - obj.get("s"), - len(pl), - packets, - ) - return obj - - if message.type == aiohttp.WSMsgType.CLOSE: - close_code = self._ws.close_code - try: - # noinspection PyArgumentList - close_code = codes.GatewayCloseCode(close_code) - except ValueError: - pass - - self.logger.debug("connection closed with code %s", close_code) - - if close_code == codes.GatewayCloseCode.AUTHENTICATION_FAILED: - raise errors.GatewayInvalidTokenError() - - if close_code in (codes.GatewayCloseCode.SESSION_TIMEOUT, codes.GatewayCloseCode.INVALID_SEQ): - raise errors.GatewayInvalidSessionError(False) - - if close_code == codes.GatewayCloseCode.SHARDING_REQUIRED: - raise errors.GatewayNeedsShardingError() - - raise errors.GatewayServerClosedConnectionError(close_code) - - if message.type in (aiohttp.WSMsgType.CLOSING, aiohttp.WSMsgType.CLOSED): - if self.requesting_close_event.is_set(): - self.logger.debug("connection has been marked as closed") - raise errors.GatewayClientClosedError() - - self.logger.debug("connection has been marked as closed unexpectedly") - raise errors.GatewayClientDisconnectedError() - - if message.type == aiohttp.WSMsgType.ERROR: - ex = self._ws.exception() - self.logger.debug("connection encountered some error", exc_info=ex) - raise errors.GatewayError("Unexpected exception occurred") from ex - - async def _receive_one_packet(self): - packet = await self._ws.receive() - self.last_message_received = time.perf_counter() - return packet - - async def _send(self, payload): - payload_str = self.json_serialize(payload) - - if len(payload_str) > 4096: - raise errors.GatewayError( - f"Tried to send a payload greater than 4096 bytes in size (was actually {len(payload_str)}" - ) - - await self._ratelimiter.acquire() - await self._ws.send_str(payload_str) - - if self.debug: - self.logger.debug("send payload\n outbound payload: %s", payload_str) - else: - self.logger.debug("sent payload [op:%s, size:%s]", payload.get("op"), len(payload_str)) - - def do_dispatch(self, event_name, payload): - asyncio.create_task(self.dispatcher(self, event_name, payload), name=f"dispatch {event_name} from websocket") - - def __str__(self): - state = "Connected" if self.is_connected else "Disconnected" - return f"{state} gateway connection to {self._url} at shard {self.shard_id}/{self.shard_count}" - - def __repr__(self): - this_type = type(self).__name__ - major_attributes = ", ".join( - ( - f"is_connected={self.is_connected!r}", - f"heartbeat_latency={self.heartbeat_latency!r}", - f"presence={self._presence!r}", - f"shard_id={self.shard_id!r}", - f"shard_count={self.shard_count!r}", - f"seq={self.seq!r}", - f"session_id={self.session_id!r}", - f"up_time={self.up_time!r}", - f"url={self._url!r}", - ) - ) - - return f"{this_type}({major_attributes})" - - def __bool__(self): - return self.is_connected diff --git a/hikari/gateway/consumers.py b/hikari/gateway/consumers.py deleted file mode 100644 index bf6b564814..0000000000 --- a/hikari/gateway/consumers.py +++ /dev/null @@ -1,60 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# Copyright © Nekoka.tt 2019-2020 -# -# This file is part of Hikari. -# -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . -"""Definition of the interface a compliant weaving implementation should provide. - -State object handle decoding events and managing application state. -""" - -from __future__ import annotations - -__all__ = ["RawEventConsumer"] - -import abc -import typing - -if typing.TYPE_CHECKING: - from . import client as gateway_client - - -class RawEventConsumer(abc.ABC): - """Consumer of raw events from Discord. - - RawEventConsumer describes an object that takes any event payloads that - Discord dispatches over a websocket and decides how to process it further. - This is used as the core base for any form of event manager type. - - This base may also be used by users to dispatch the event to a completely - different medium, such as a message queue for distributed applications. - """ - - @abc.abstractmethod - async def process_raw_event( - self, shard_client_obj: gateway_client.GatewayClient, name: str, payload: typing.Mapping[str, str], - ) -> None: - """Consume a raw event that was received from a shard connection. - - Parameters - ---------- - shard_client_obj : hikari.gateway.client.GatewayClient - The client for the shard that received the event. - name : str - The raw event name. - payload : typing.Any - The raw event payload. Will be a JSON-compatible type. - """ diff --git a/hikari/gateway/dispatchers.py b/hikari/gateway/dispatchers.py deleted file mode 100644 index 385af995d3..0000000000 --- a/hikari/gateway/dispatchers.py +++ /dev/null @@ -1,220 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# Copyright © Nekoka.tt 2019-2020 -# -# This file is part of Hikari. -# -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . -"""Event dispatcher implementation.""" - -from __future__ import annotations - -__all__ = ["EventDispatcher"] - -import abc -import typing - -from hikari.internal import conversions - -if typing.TYPE_CHECKING: - from hikari.events import base as event_base - from hikari.internal import more_typing - - EventT = typing.TypeVar("EventT", bound=event_base.HikariEvent) - PredicateT = typing.Callable[[EventT], typing.Union[more_typing.Coroutine[bool], bool]] - EventCallbackT = typing.Callable[[EventT], typing.Union[more_typing.Coroutine[typing.Any], typing.Any]] - - -class EventDispatcher(abc.ABC): - """Base definition for a conforming event dispatcher implementation. - - This enables users to implement their own event dispatching strategies - if the base implementation is not suitable. This could be used to write - a distributed bot dispatcher, for example, or could handle dispatching - to a set of micro-interpreter instances to achieve greater concurrency. - """ - - __slots__ = () - - @abc.abstractmethod - def close(self) -> None: - """Cancel anything that is waiting for an event to be dispatched.""" - - @abc.abstractmethod - def add_listener(self, event_type: typing.Type[EventT], callback: EventCallbackT, **kwargs) -> EventCallbackT: - """Register a new event callback to a given event name. - - Parameters - ---------- - event_type : typing.Type[hikari.events.base.HikariEvent] - The event to register to. - callback : `async def callback(event: HikariEvent) -> ...` - The event callback to invoke when this event is fired; this can be - async or non-async. - """ - - @abc.abstractmethod - def remove_listener(self, event_type: typing.Type[EventT], callback: EventCallbackT) -> EventCallbackT: - """Remove the given function from the handlers for the given event. - - The name is mandatory to enable supporting registering the same event - callback for multiple event types. - - Parameters - ---------- - event_type : typing.Type[hikari.events.base.HikariEvent] - The type of event to remove the callback from. - callback : `async def callback(event: HikariEvent) -> ...` - The event callback to invoke when this event is fired; this can be - async or non-async. - """ - - @abc.abstractmethod - def wait_for( - self, - event_type: typing.Type[EventT], - *, - timeout: typing.Optional[float] = None, - predicate: typing.Optional[PredicateT] = None, - ) -> more_typing.Future: - """Wait for the given event type to occur. - - Parameters - ---------- - event_type : typing.Type[hikari.events.base.HikariEvent] - The name of the event to wait for. - timeout : float | None - The timeout to wait for before cancelling and raising an - `asyncio.TimeoutError` instead. If this is `None`, this - will wait forever. Care must be taken if you use `None` as this - may leak memory if you do this from an event listener that gets - repeatedly called. If you want to do this, you should consider - using an event listener instead of this function. - predicate : `def predicate(event) -> bool` | `async def predicate(event) -> bool` | None - A function that takes the arguments for the event and returns `True` - if it is a match, or `False` if it should be ignored. - This can be a coroutine function that returns a boolean, or a - regular function. - - Returns - ------- - asyncio.Future - A future to await. When the given event is matched, this will be - completed with the corresponding event body. - - If the predicate throws an exception, or the timeout is reached, - then this will be set as an exception on the returned future. - """ - - def event( - self, event_type: typing.Optional[typing.Type[EventT]] = None # pylint:disable=unused-argument - ) -> typing.Callable[[EventCallbackT], EventCallbackT]: - """Return a decorator equivalent to invoking `EventDispatcher.add_listener`. - - This can be used in two ways. The first is to pass the event type - to the decorator directly. - - @bot.event(hikari.MessageCreatedEvent) - async def on_message(event): - print(event.content) - - The second method is to provide typehints instead. This allows you - to write code that works with a static type checker without needing - to specify the event type twice. - - # Type-hinted format. - @bot.event() - async def on_message(event: hikari.MessageCreatedEvent) -> None: - print(event.content) - - If you do not provide the event type in the decorator, and you do not - provide a valid type hint, then a `TypeError` will be raised. - - You can subscribe to multiple events in two ways. - - The first way is to subscribe to a base class of each event you want - to listen to. Base classes are described in this documentation on each - event type. - - An example would be listening to every event that occurs. - - @bot.event(hikari.HikariEvent) - async def on_event(event): - print(event) - - The other method is to apply the decorator more than once. - - @bot.event(hikari.MessageCreateEvent) - @bot.event(hikari.MessageDeleteEvent) - async def on_create_delete(event): - print(event) - - Parameters - ---------- - event_type : typing.Type[hikari.events.base.HikariEvent] | None - The event type to register the produced decorator to. If this is not - specified, then the given function is used instead and the type hint - of the first argument is considered. If no type hint is present - there either, then the call will fail. - - Returns - ------- - decorator(T) -> T - A decorator for a function that registers the given event. - - Raises - ------ - TypeError: - If a function with an invalid signature is passed, or if no event - type is used in the decorator or as a type hint. - """ - - def decorator(callback: EventCallbackT) -> EventCallbackT: - nonlocal event_type - - if event_type is None: - signature = conversions.resolve_signature(callback) - - # Seems that the `self` gets unbound for methods automatically by - # inspect.signature. That makes my life two lines easier. - if len(signature.parameters) == 1: - event_type = next(iter(signature.parameters.values())).annotation - else: - raise TypeError(f"Invalid signature for event: async def {callback.__name__}({signature}): ...") - - if event_type is conversions.EMPTY: - raise TypeError(f"No param type hint given for: async def {callback}({signature}): ...") - - return self.add_listener(event_type, callback, _stack_level=3) - - return decorator - - # Do not add an annotation here, it will mess with type hints in PyCharm which can lead to - # confusing telepathy comments to the user. - @abc.abstractmethod - def dispatch_event(self, event: event_base.HikariEvent) -> more_typing.Future[typing.Any]: - """Dispatch a given event to any listeners and waiters. - - Parameters - ---------- - event : hikari.events.base.HikariEvent - The event to dispatch. - - Returns - ------- - asyncio.Future: - A future that can be optionally awaited if you need to wait for all - listener callbacks and waiters to be processed. If this is not - awaited, the invocation is invoked soon on the current event loop. - """ diff --git a/hikari/gateway/event_managers.py b/hikari/gateway/event_managers.py deleted file mode 100644 index 319dd22035..0000000000 --- a/hikari/gateway/event_managers.py +++ /dev/null @@ -1,184 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# Copyright © Nekoka.tt 2019-2020 -# -# This file is part of Hikari. -# -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . -"""Basic single-application weaving manager.""" - -from __future__ import annotations - -__all__ = ["raw_event_mapper", "EventManager"] - -import inspect -import logging -import typing - -from hikari.gateway import consumers -from hikari.gateway import dispatchers - -if typing.TYPE_CHECKING: - from hikari import application - from hikari.gateway import client as gateway_client - from hikari.internal import more_typing - -EVENT_MARKER_ATTR: typing.Final[str] = "___event_name___" - -EventConsumerT = typing.Callable[[str, typing.Mapping[str, str]], typing.Awaitable[None]] - - -def raw_event_mapper(name: str) -> typing.Callable[[EventConsumerT], EventConsumerT]: - """Create a decorator for a coroutine function to register it as an event handler. - - Parameters - ---------- - name: str - The case sensitive name of the event to associate the annotated method - with. - - Returns - ------- - decorator(T) -> T - A decorator for a method. - """ - - def decorator(callable_: EventConsumerT) -> EventConsumerT: - if not inspect.iscoroutinefunction(callable_): - raise ValueError("Annotated element must be a coroutine function") - - event_set = getattr(callable_, EVENT_MARKER_ATTR, set()) - event_set.add(name) - setattr(callable_, EVENT_MARKER_ATTR, event_set) - return callable_ - - return decorator - - -def _has_event_marker(obj: typing.Any) -> bool: - return hasattr(obj, EVENT_MARKER_ATTR) - - -def _get_event_marker(obj: typing.Any) -> typing.Set[str]: - return getattr(obj, EVENT_MARKER_ATTR) - - -EventDispatcherT = typing.TypeVar("EventDispatcherT", bound=dispatchers.EventDispatcher) - - -class EventManager(typing.Generic[EventDispatcherT], consumers.RawEventConsumer): - """Abstract definition of the application for an event system for a bot. - - The class itself inherits from `hikari.components.consumers.RawEventConsumer` - (which allows it to provide the ability to transform a raw payload into an - event object). - - This is designed as a basis to enable transformation of raw incoming events - from the websocket into more usable native Python objects, and to then - dispatch them to a given event dispatcher. It does not provide the logic for - how to specifically parse each event however. - - !!! note - This object will detect internal event mapper functions by looking for - coroutine functions wrapped with `raw_event_mapper`. - - These methods are expected to have the following parameters: - - * shard_obj : `hikari.gateway.client.GatewayClient` - - The shard client that emitted the event. - - * payload : `typing.Any` - - The received payload. This is expected to be a JSON-compatible type. - - For example, if you want to provide an implementation that can consume - and handle `MESSAGE_CREATE` events, you can do the following. - - class MyMappingEventConsumer(MappingEventConsumer): - @event_mapper("MESSAGE_CREATE") - def _process_message_create(self, shard, payload) -> MessageCreateEvent: - return MessageCreateEvent.deserialize(payload) - - The decorator can be stacked if you wish to provide one mapper - - ... it is pretty simple. This is exposed in this way to enable you to - write code that may use a distributed system instead of a single-process - bot. - - Writing to a message queue is pretty simple using this mechanism, as you - can choose when and how to place the event on a queue to be consumed by - other application application. - - For the sake of simplicity, Hikari only provides implementations for - single process bots, since most of what you will need will be fairly - bespoke if you want to implement anything more complicated; regardless, - the tools are here for you to use as you see fit. - - !!! warning - This class provides the scaffold for making an event consumer, but doe - not physically implement the logic to deserialize and process specific - events. - - To provide this, use one of the provided implementations of this class, - or create your own as needed. - - Parameters - ---------- - app: hikari.components.application.Application - The client application that this event manager should be bound to. - Includes the event dispatcher that will store individual events and - manage dispatching them after this object creates them. - """ - - def __init__(self, app: application.Application) -> None: - self._app = app - self.logger = logging.getLogger(type(self).__qualname__) - self.raw_event_mappers = {} - - # Look for events and register them. - for _, member in inspect.getmembers(self, _has_event_marker): - event_names = _get_event_marker(member) - for event_name in event_names: - self.raw_event_mappers[event_name] = member - - async def process_raw_event( - self, shard_client_obj: gateway_client.GatewayClient, name: str, payload: more_typing.JSONObject, - ) -> None: - """Process a low level event. - - This will update the internal weaving, perform processing where necessary, - and then dispatch the event to any listeners. - - Parameters - ---------- - shard_client_obj : hikari.gateway.client.GatewayClient - The shard that triggered this event. - name : str - The raw event name. - payload : dict - The payload that was sent. - """ - try: - handler = self.raw_event_mappers[name] - except KeyError: - self.logger.debug("no handler for event %s is registered", name) - return - - try: - await handler(shard_client_obj, payload) - except Exception as ex: - self.logger.exception( - "Failed to unmarshal %r event payload. This is likely a bug in the library itself.", name, exc_info=ex, - ) diff --git a/hikari/gateway/gateway_state.py b/hikari/gateway/gateway_state.py deleted file mode 100644 index 2ea245a81d..0000000000 --- a/hikari/gateway/gateway_state.py +++ /dev/null @@ -1,54 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# Copyright © Nekoka.tt 2019-2020 -# -# This file is part of Hikari. -# -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . -"""State of a shard.""" - -from __future__ import annotations - -__all__ = ["GatewayState"] - -from hikari.internal import more_enums - - -@more_enums.must_be_unique -class GatewayState(int, more_enums.Enum): - """Describes the state of a shard.""" - - NOT_RUNNING = 0 - """The shard is not running.""" - - CONNECTING = more_enums.generated_value() - """The shard is undergoing the initial connection handshake.""" - - WAITING_FOR_READY = more_enums.generated_value() - """The initialization handshake has completed. - - We are waiting for the shard to receive the `READY` event. - """ - - READY = more_enums.generated_value() - """The shard is `READY`.""" - - RESUMING = more_enums.generated_value() - """The shard has sent a request to `RESUME` and is waiting for a response.""" - - STOPPING = more_enums.generated_value() - """The shard is currently shutting down permanently.""" - - STOPPED = more_enums.generated_value() - """The shard has shut down and is no longer connected.""" diff --git a/hikari/gateway/intent_aware_dispatchers.py b/hikari/gateway/intent_aware_dispatchers.py deleted file mode 100644 index 6430613a64..0000000000 --- a/hikari/gateway/intent_aware_dispatchers.py +++ /dev/null @@ -1,348 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# Copyright © Nekoka.tt 2019-2020 -# -# This file is part of Hikari. -# -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . -"""Event dispatcher implementations that are intent-aware.""" - -from __future__ import annotations - -__all__ = ["IntentAwareEventDispatcherImpl"] - -import asyncio -import logging -import typing -import warnings - -from hikari import errors -from hikari.events import base as event_base -from hikari.events import other as other_events -from hikari.internal import more_asyncio -from hikari.internal import more_collections -from hikari.models import intents -from . import dispatchers - -if typing.TYPE_CHECKING: - from hikari.internal import more_typing - - -class IntentAwareEventDispatcherImpl(dispatchers.EventDispatcher): - """Handles storing and dispatching to event listeners and one-time event waiters. - - Event listeners once registered will be stored until they are manually - removed. Each time an event is dispatched with a matching name, they will - be invoked on the event loop. - - One-time event waiters are futures that will be completed when a matching - event is fired. Once they are matched, they are removed from the listener - list. Each listener has a corresponding predicate that is invoked prior - to completing the waiter, with any event parameters being passed to the - predicate. If the predicate returns False, the waiter is not completed. This - allows filtering of certain events and conditions in a procedural way. - - Events that require a specific intent will trigger warnings on subscription - if the provided enabled intents are not a superset of this. - - Parameters - ---------- - enabled_intents : hikari.models.intents.Intent | None - The intents that are enabled for the application. If `None`, then no - intent checks are performed when subscribing a new event. - """ - - logger: logging.Logger - """The logger used to write log messages.""" - - def __init__(self, enabled_intents: typing.Optional[intents.Intent]) -> None: - self._enabled_intents = enabled_intents - self._listeners = {} - self._waiters = {} - self.logger = logging.getLogger(type(self).__qualname__) - - def close(self) -> None: - """Cancel anything that is waiting for an event to be dispatched.""" - self._listeners.clear() - for waiter in self._waiters.values(): - for future in waiter.keys(): - future.cancel() - self._waiters.clear() - - def add_listener( - self, event_type: typing.Type[event_base.HikariEvent], callback: dispatchers.EventCallbackT, **kwargs - ) -> dispatchers.EventCallbackT: - """Register a new event callback to a given event name. - - Parameters - ---------- - event_type : typing.Type[hikari.events.base.HikariEvent] - The event to register to. - callback : `async def callback(event: HikariEvent) -> ...` - The event callback to invoke when this event is fired; this can be - async or non-async. - - Returns - ------- - async def callback(event: hikari.events.base.HikariEvent) -> ... - The callback that was registered. - - Note - ---- - If you subscribe to an event that requires intents that you do not have - set, you will receive a warning. - """ - if not issubclass(event_type, event_base.HikariEvent): - raise TypeError("Events must subclass hikari.events.HikariEvent") - - required_intents = event_base.get_required_intents_for(event_type) - enabled_intents = self._enabled_intents if self._enabled_intents is not None else 0 - - any_intent_match = any(enabled_intents & i == i for i in required_intents) - - if self._enabled_intents is not None and required_intents and not any_intent_match: - intents_lists = [] - for required in required_intents: - set_of_intents = [] - for intent in intents.Intent: - if required & intent: - set_of_intents.append(f"{intent.name} " if intent.is_privileged else intent.name) - intents_lists.append(" + ".join(set_of_intents)) - - message = ( - f"Event {event_type.__module__}.{event_type.__qualname__} will never be triggered\n" - f"unless you enable one of the following intents:\n" - + "\n".join(f" - {intent_list}" for intent_list in intents_lists) - ) - - warnings.warn(message, category=errors.IntentWarning, stacklevel=kwargs.pop("_stack_level", 1)) - - if event_type not in self._listeners: - self._listeners[event_type] = [] - self._listeners[event_type].append(callback) - - return callback - - def remove_listener( - self, event_type: typing.Type[dispatchers.EventT], callback: dispatchers.EventCallbackT - ) -> None: - """Remove the given function from the handlers for the given event. - - The name is mandatory to enable supporting registering the same event callback for multiple event types. - - Parameters - ---------- - event_type : typing.Type[hikari.events.HikariEvent] - The type of event to remove the callback from. - callback : `async def callback(event: HikariEvent) -> ...` - The event callback to remove; this can be async or non-async. - """ - if event_type in self._listeners and callback in self._listeners[event_type]: - if len(self._listeners[event_type]) - 1 == 0: - del self._listeners[event_type] - else: - self._listeners[event_type].remove(callback) - - # Do not add an annotation here, it will mess with type hints in PyCharm which can lead to - # confusing telepathy comments to the user. - # Additionally, this MUST NOT BE A COROUTINE ITSELF. THIS IS NOT TYPESAFE! - def dispatch_event(self, event: event_base.HikariEvent) -> more_typing.Future[typing.Any]: - """Dispatch a given event to all listeners and waiters that are applicable. - - Parameters - ---------- - event : hikari.events.HikariEvent - The event to dispatch. - - Returns - ------- - asyncio.Future - This may be a gathering future of the callbacks to invoke, or it may - be a completed future object. Regardless, this result will be - scheduled on the event loop automatically, and does not need to be - awaited. Awaiting this future will await completion of all invoked - event handlers. - """ - this_event_type = type(event) - self.logger.debug("dispatching %s", this_event_type.__name__) - - callback_futures = [] - - for base_event_type in this_event_type.mro(): - for callback in self._listeners.get(base_event_type, more_collections.EMPTY_COLLECTION): - callback_futures.append(asyncio.create_task(self._failsafe_invoke(event, callback))) - - if base_event_type not in self._waiters: - continue - - # Quicker most of the time to iterate twice, than to copy the entire collection - # to iterate once after that. - futures_to_remove = [] - - subtype_waiters = self._waiters.get(base_event_type, more_collections.EMPTY_DICT) - - for future, predicate in subtype_waiters.items(): - # If there is no predicate, there is nothing to check, so just return what we got. - if not predicate: - future.set_result(event) - futures_to_remove.append(future) - continue - - # We execute async predicates differently to sync, because we hope most of the time - # these checks will be synchronous only, as these will perform significantly faster. - # I preferred execution speed over terseness here. - if asyncio.iscoroutinefunction(predicate): - # Reawaken it later once the predicate is complete. We can await this with the - # other dispatchers. - check_task = asyncio.create_task(self._async_check(future, predicate, event, base_event_type)) - callback_futures.append(check_task) - else: - try: - if predicate(event): - # We have to append this to a list, we can't mutate the dict while we iterate over it... - future.set_result(event) - futures_to_remove.append(future) - except Exception as ex: # pylint:disable=broad-except - future.set_exception(ex) - futures_to_remove.append(future) - - # We do this after to prevent changes to the dict while iterating causing exceptions. - for future in futures_to_remove: - # Off to the garbage collector you go. - subtype_waiters.pop(future) - - # If there are no waiters left, purge the entire dict. - if not subtype_waiters: - self._waiters.pop(base_event_type) - - result = asyncio.gather(*callback_futures) if callback_futures else more_asyncio.completed_future() - - # Stop intellij shenanigans with broken type hints that ruin my day. - return typing.cast(typing.Any, result) - - async def _async_check(self, future, predicate, event, event_type) -> None: - # If the predicate returns true, complete the future and pop it from the waiters. - # By this point we shouldn't be iterating over it anymore, so this is concurrent-modification - # safe on a single event loop. - try: - if await predicate(event): - future.set_result(event) - self._waiters[event_type].pop(future) - except Exception as ex: # pylint:disable=broad-except - future.set_exception(ex) - self._waiters[event_type].pop(future) - - async def _failsafe_invoke(self, event, callback) -> None: - try: - result = callback(event) - if asyncio.iscoroutine(result): - await result - except Exception as ex: # pylint:disable=broad-except - self.handle_exception(ex, event, callback) - - def handle_exception( - self, - exception: Exception, - event: event_base.HikariEvent, - callback: typing.Callable[..., typing.Union[typing.Awaitable[None]]], - ) -> None: - """Handle raised exception. - - This allows users to override this with a custom implementation if desired. - - This implementation will check to see if the event that triggered the - exception is an `hikari.events.ExceptionEvent`. If this - exception was caused by the `hikari.events.ExceptionEvent`, - then nothing is dispatched (thus preventing an exception handler recursively - re-triggering itself). Otherwise, an `hikari.events.ExceptionEvent` - is dispatched. - - Parameters - ---------- - exception: Exception - The exception that triggered this call. - event: hikari.events.HikariEvent - The event that was being dispatched. - callback - The callback that threw the exception. This may be an event - callback, or a `wait_for` predicate that threw an exception. - """ - # Do not recurse if a dodgy exception handler is added. - if not event_base.is_no_catch_event(event): - self.logger.exception( - 'Exception occurred in handler for event "%s"', type(event).__name__, exc_info=exception - ) - self.dispatch_event(other_events.ExceptionEvent(exception=exception, event=event, callback=callback)) - else: - self.logger.exception( - 'Exception occurred in handler for event "%s", and the exception has been dropped', - type(event).__name__, - exc_info=exception, - ) - - def wait_for( - self, - event_type: typing.Type[dispatchers.EventT], - *, - timeout: typing.Optional[float] = None, - predicate: typing.Optional[dispatchers.PredicateT] = None, - ) -> more_typing.Future: - """Wait for a event to occur once and then return the arguments the event was called with. - - Events can be filtered using a given predicate function. If unspecified, - the first event of the given name will be a match. - - Every event that matches the event name that the bot receives will be - checked. Thus, if you need to wait for events in a specific guild or - channel, or from a specific person, you want to give a predicate that - checks this. - - Parameters - ---------- - event_type : typing.Type[hikari.events.HikariEvent] - The name of the event to wait for. - timeout : float | None - The timeout to wait for before cancelling and raising an - `asyncio.TimeoutError` instead. If this is `None`, this will - wait forever. Care must be taken if you use `None` as this may - leak memory if you do this from an event listener that gets - repeatedly called. If you want to do this, you should consider - using an event listener instead of this function. - predicate : `def predicate(event) -> bool` or `async def predicate(event) -> bool` | None - A function that takes the arguments for the event and returns `True` - if it is a match, or `False` if it should be ignored. This must be - a regular function. - - Returns - ------- - asyncio.Future - A future that when awaited will provide a the arguments passed to - the first matching event. If no arguments are passed to the event, - then `None` is the result. If one argument is passed to the event, - then that argument is the result, otherwise a tuple of arguments is - the result instead. - - !!! note - Awaiting this result will raise an `asyncio.TimeoutError` if the - timeout is hit and no match is found. If the predicate throws any - exception, this is raised immediately. - """ - future = asyncio.get_event_loop().create_future() - if event_type not in self._waiters: - # This is used as a weakref dict to allow automatically tidying up - # any future that falls out of scope entirely. - self._waiters[event_type] = more_collections.WeakKeyDictionary() - self._waiters[event_type][future] = predicate - # noinspection PyTypeChecker - return asyncio.ensure_future(asyncio.wait_for(future, timeout)) diff --git a/hikari/gateway/runnable.py b/hikari/gateway/runnable.py deleted file mode 100644 index d3c65bd9f4..0000000000 --- a/hikari/gateway/runnable.py +++ /dev/null @@ -1,99 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# Copyright © Nekoka.tt 2019-2020 -# -# This file is part of Hikari. -# -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . -"""Provides a base for any type of websocket client.""" - -from __future__ import annotations - -__all__ = ["RunnableClient"] - -import abc -import asyncio -import contextlib -import signal -import typing - -if typing.TYPE_CHECKING: - import logging - - -class RunnableClient(abc.ABC): - """Base for any websocket client that must be kept alive.""" - - __slots__ = ("logger",) - - logger: logging.Logger - """The logger to use for this client.""" - - @abc.abstractmethod - def __init__(self, logger: typing.Union[logging.Logger, logging.LoggerAdapter]) -> None: - self.logger = logger - - @abc.abstractmethod - async def start(self) -> None: # noqa: D401 - """Start the component.""" - - @abc.abstractmethod - async def close(self) -> None: - """Shut down the component.""" - - @abc.abstractmethod - async def join(self) -> None: - """Wait for the component to terminate.""" - - def run(self) -> None: - """Execute this component on an event loop. - - Performs the same job as `RunnableClient.start`, but provides additional - preparation such as registering OS signal handlers for interrupts, - and preparing the initial event loop. - - This enables the client to be run immediately without having to - set up the `asyncio` event loop manually first. - """ - loop = asyncio.get_event_loop() - - def sigterm_handler(*_): - raise KeyboardInterrupt() - - ex = None - - try: - with contextlib.suppress(NotImplementedError): - # Not implemented on Windows - loop.add_signal_handler(signal.SIGTERM, sigterm_handler) - - loop.run_until_complete(self.start()) - loop.run_until_complete(self.join()) - - self.logger.info("client has shut down") - - except KeyboardInterrupt as _ex: - self.logger.info("received signal to shut down client") - loop.run_until_complete(self.close()) - # Apparently you have to alias except clauses or you get an - # UnboundLocalError. - ex = _ex - finally: - loop.run_until_complete(self.close()) - with contextlib.suppress(NotImplementedError): - # Not implemented on Windows - loop.remove_signal_handler(signal.SIGTERM) - - if ex: - raise ex from ex diff --git a/hikari/aiohttp_config.py b/hikari/http_settings.py similarity index 95% rename from hikari/aiohttp_config.py rename to hikari/http_settings.py index 906ae907ba..5fc84dbb73 100644 --- a/hikari/aiohttp_config.py +++ b/hikari/http_settings.py @@ -20,7 +20,7 @@ from __future__ import annotations -__all__ = ["AIOHTTPConfig"] +__all__ = ["HTTPSettings"] import typing @@ -32,7 +32,7 @@ @attr.s(kw_only=True, repr=False, auto_attribs=True) -class AIOHTTPConfig: +class HTTPSettings: """Config for application that use AIOHTTP.""" allow_redirects: bool = False @@ -51,7 +51,7 @@ class AIOHTTPConfig: """The optional URL of the proxy to send requests via.""" request_timeout: typing.Optional[float] = None - """Optional request timeout to use. If an HTTP request takes longer than + """Optional request _request_timeout to use. If an HTTP request takes longer than this, it will be aborted. If not `None`, the value represents a number of seconds as a floating diff --git a/hikari/impl/bot.py b/hikari/impl/bot.py index fdeb3a68fc..703720def2 100644 --- a/hikari/impl/bot.py +++ b/hikari/impl/bot.py @@ -39,7 +39,7 @@ from hikari.api import cache as cache_ from hikari.api import entity_factory as entity_factory_ from hikari.api import event_consumer as event_consumer_ - from hikari import aiohttp_config + from hikari import http_settings from hikari.api import event_dispatcher from hikari.api import gateway_zookeeper from hikari.models import gateway @@ -50,7 +50,7 @@ class BotImpl(gateway_zookeeper.AbstractGatewayZookeeper, bot.IBot): def __init__( self, *, - config: aiohttp_config.AIOHTTPConfig, + config: http_settings.HTTPSettings, debug: bool = False, gateway_url: str, gateway_version: int = 6, @@ -62,13 +62,27 @@ def __init__( large_threshold: int = 250, rest_version: int = 6, rest_url: str = urls.REST_API_URL, - shard_ids: typing.Set[int], - shard_count: int, + shard_ids: typing.Optional[typing.Set[int]], + shard_count: typing.Optional[int], token: str, use_compression: bool = True, ): self._logger = helpers.get_logger(self) + self._cache = cache_impl.CacheImpl() + self._event_manager = event_manager.EventManagerImpl() + self._entity_factory = entity_factory_impl.EntityFactoryImpl() + + self._rest = rest_client_.RESTClient( + app=self, + config=config, + debug=debug, + token=token, + token_type="Bot", + rest_url=rest_url, + version=rest_version, + ) + super().__init__( config=config, debug=debug, @@ -86,20 +100,6 @@ def __init__( version=gateway_version, ) - self._rest = rest_client_.RESTClient( - app=self, - config=config, - debug=debug, - token=token, - token_type="Bot", - rest_url=rest_url, - version=rest_version, - ) - - self._cache = cache_impl.CacheImpl() - self._event_manager = event_manager.EventManagerImpl() - self._entity_factory = entity_factory_impl.EntityFactoryImpl() - @property def event_dispatcher(self) -> event_dispatcher.IEventDispatcher: return self._event_manager diff --git a/hikari/impl/gateway_zookeeper.py b/hikari/impl/gateway_zookeeper.py index 18db398012..75405245e5 100644 --- a/hikari/impl/gateway_zookeeper.py +++ b/hikari/impl/gateway_zookeeper.py @@ -26,13 +26,13 @@ from hikari.api import event_dispatcher from hikari.api import gateway_zookeeper from hikari.events import other -from hikari.gateway import client +from hikari import gateway from hikari.internal import conversions if typing.TYPE_CHECKING: import datetime - from hikari import aiohttp_config + from hikari import http_settings from hikari.models import gateway from hikari.models import guilds from hikari.models import intents as intents_ @@ -42,7 +42,7 @@ class AbstractGatewayZookeeper(gateway_zookeeper.IGatewayZookeeper, abc.ABC): def __init__( self, *, - config: aiohttp_config.AIOHTTPConfig, + config: http_settings.HTTPSettings, debug: bool, initial_activity: typing.Optional[gateway.Activity], initial_idle_since: typing.Optional[datetime.datetime], @@ -61,9 +61,10 @@ def __init__( self._url = url self._shard_count = shard_count self._shards = { - shard_id: client.GatewayClient( + shard_id: gateway.Gateway( config=config, debug=debug, + dispatch=self.event_consumer.consume_raw_event, initial_activity=initial_activity, initial_idle_since=initial_idle_since, initial_is_afk=initial_is_afk, @@ -76,13 +77,12 @@ def __init__( url=url, use_compression=use_compression, version=version, - zookeeper=self, ) for shard_id in shard_ids } @property - def gateway_shards(self) -> typing.Mapping[int, client.GatewayClient]: + def gateway_shards(self) -> typing.Mapping[int, gateway.Gateway]: return self._shards @property @@ -100,7 +100,7 @@ async def start(self) -> None: await asyncio.sleep(5) shard_obj = self._shards[shard_id] - await shard_obj.start() + await shard_obj.run() finish_time = time.perf_counter() diff --git a/hikari/impl/rest_app.py b/hikari/impl/rest_app.py index 7be06a3e9b..f37435c645 100644 --- a/hikari/impl/rest_app.py +++ b/hikari/impl/rest_app.py @@ -23,7 +23,7 @@ import typing from concurrent import futures -from hikari import aiohttp_config +from hikari import http_settings from hikari.api import rest_app from hikari.impl import cache as cache_impl from hikari.impl import entity_factory as entity_factory_impl @@ -39,7 +39,7 @@ class RESTAppImpl(rest_app.IRESTApp): def __init__( self, - config: aiohttp_config.AIOHTTPConfig, + config: http_settings.HTTPSettings, debug: bool = False, token: typing.Optional[str] = None, token_type: typing.Optional[str] = None, diff --git a/hikari/internal/codes.py b/hikari/internal/codes.py deleted file mode 100644 index 42aad58a44..0000000000 --- a/hikari/internal/codes.py +++ /dev/null @@ -1,362 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# Copyright © Nekokatt 2019-2020 -# -# This file is part of Hikari. -# -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . -"""Enumerations for opcodes and status codes.""" - -from __future__ import annotations - -__all__ = ["GatewayCloseCode", "GatewayOpcode", "JSONErrorCode"] - -from . import more_enums - - -@more_enums.must_be_unique -class GatewayCloseCode(int, more_enums.Enum): - """Reasons for closing a gateway connection. - - !!! note - Any codes greater than or equal to `4000` are server-side codes. Any - codes between `1000` and `1999` inclusive are generally client-side codes. - """ - - NORMAL_CLOSURE = 1000 - """The application running closed.""" - - UNKNOWN_ERROR = 4000 - """Discord is not sure what went wrong. Try reconnecting?""" - - UNKNOWN_OPCODE = 4001 - """You sent an invalid Gateway opcode or an invalid payload for an opcode. - - Don't do that! - """ - - DECODE_ERROR = 4002 - """You sent an invalid payload to Discord. Don't do that!""" - - NOT_AUTHENTICATED = 4003 - """You sent Discord a payload prior to IDENTIFYing.""" - - AUTHENTICATION_FAILED = 4004 - """The account token sent with your identify payload is incorrect.""" - - ALREADY_AUTHENTICATED = 4005 - """You sent more than one identify payload. Don't do that!""" - - INVALID_SEQ = 4007 - """The sequence sent when resuming the session was invalid. - - Reconnect and start a new session. - """ - - RATE_LIMITED = 4008 - """Woah nelly! You're sending payloads to Discord too quickly. Slow it down!""" - - SESSION_TIMEOUT = 4009 - """Your session timed out. Reconnect and start a new one.""" - - INVALID_SHARD = 4010 - """You sent Discord an invalid shard when IDENTIFYing.""" - - SHARDING_REQUIRED = 4011 - """The session would have handled too many guilds. - - You are required to shard your connection in order to connect. - """ - - INVALID_VERSION = 4012 - """You sent an invalid version for the gateway.""" - - INVALID_INTENT = 4013 - """You sent an invalid intent for a Gateway Intent. - - You may have incorrectly calculated the bitwise value. - """ - - DISALLOWED_INTENT = 4014 - """You sent a disallowed intent for a Gateway Intent. - - You may have tried to specify an intent that you have not enabled or are not - whitelisted for. - """ - - def __str__(self) -> str: - name = self.name.replace("_", " ").title() - return f"{self.value} {name}" - - -@more_enums.must_be_unique -class GatewayOpcode(int, more_enums.Enum): - """Opcodes that the gateway uses internally.""" - - DISPATCH = 0 - """An event was dispatched.""" - - HEARTBEAT = 1 - """Used for ping checking.""" - - IDENTIFY = 2 - """Used for client handshake.""" - - PRESENCE_UPDATE = 3 - """Used to update the client status.""" - - VOICE_STATE_UPDATE = 4 - """Used to join/move/leave voice channels.""" - - RESUME = 6 - """Used to resume a closed connection.""" - - RECONNECT = 7 - """Used to tell clients to reconnect to the gateway.""" - - REQUEST_GUILD_MEMBERS = 8 - """Used to request guild members.""" - - INVALID_SESSION = 9 - """Used to notify client they have an invalid session id.""" - - HELLO = 10 - """Sent immediately after connecting. - - Contains heartbeat and server debug information. - """ - - HEARTBEAT_ACK = 11 - """Sent immediately following a client heartbeat that was received.""" - - GUILD_SYNC = 12 - """Not yet documented, so do not use.""" - - def __str__(self) -> str: - name = self.name.replace("_", " ").title() - return f"{self.value} {name}" - - -@more_enums.must_be_unique -class JSONErrorCode(int, more_enums.Enum): - """Error codes that can be returned by the REST API.""" - - GENERAL_ERROR = 0 - """This is sent if the payload is screwed up, etc.""" - - UNKNOWN_ACCOUNT = 10_001 - """Unknown account""" - - UNKNOWN_APPLICATION = 10_002 - """Unknown application""" - - UNKNOWN_CHANNEL = 10_003 - """Unknown channel""" - - UNKNOWN_GUILD = 10_004 - """Unknown guild""" - - UNKNOWN_INTEGRATION = 10_005 - """Unknown integration""" - - UNKNOWN_INVITE = 10_006 - """Unknown invite""" - - UNKNOWN_MEMBER = 10_007 - """Unknown member""" - - UNKNOWN_MESSAGE = 10_008 - """Unknown message""" - - UNKNOWN_OVERWRITE = 10_009 - """Unknown overwrite""" - - UNKNOWN_PROVIDER = 10_010 - """Unknown provider""" - - UNKNOWN_ROLE = 10_011 - """Unknown role""" - - UNKNOWN_TOKEN = 10_012 - """Unknown token""" - - UNKNOWN_USER = 10_013 - """Unknown user""" - - UNKNOWN_EMOJI = 10_014 - """Unknown emoji""" - - UNKNOWN_WEBHOOK = 10_015 - """Unknown Webhook""" - - UNKNOWN_BAN = 10_026 - """Unknown ban""" - - USERS_ONLY = 20_001 - """Bots cannot use this endpoint - - !!! note - You should never expect to receive this in normal API usage. - """ - - BOTS_ONLY = 20_002 - """Only bots can use this endpoint. - - !!! note - You should never expect to receive this in normal API usage. - """ - - MAX_GUILDS_REACHED = 30_001 - """Maximum number of guilds reached (100) - - !!! note - You should never expect to receive this in normal API usage as this only - applies to user accounts. - - This is unlimited for bot accounts. - """ - - MAX_FRIENDS_REACHED = 30_002 - """Maximum number of friends reached (1000) - - !!! note - You should never expect to receive this in normal API usage as this only - applies to user accounts. - - Bots cannot have friends :( . - """ - - MAX_PINS_REACHED = 30_003 - """Maximum number of pins reached (50)""" - - MAX_GUILD_ROLES_REACHED = 30_005 - """Maximum number of guild roles reached (250)""" - - MAX_WEBHOOKS_REACHED = 30_007 - """Maximum number of webhooks reached (10)""" - - MAX_REACTIONS_REACHED = 30_010 - """Maximum number of reactions reached (20)""" - - MAX_GUILD_CHANNELS_REACHED = 30_013 - """Maximum number of guild channels reached (500)""" - - MAX_MESSAGE_ATTACHMENTS_REACHED = 30_015 - """Maximum number of attachments in a message reached (10)""" - - MAX_INVITES_REACHED = 30_016 - """Maximum number of invites reached (10000)""" - - NEEDS_VERIFICATION = 40_002 - """You need to verify your account to perform this action.""" - - UNAUTHORIZED = 40_001 - """Unauthorized""" - - TOO_LARGE = 40_005 - """Request entity too large. Try sending something smaller in size""" - - DISABLED_TEMPORARILY = 40_006 - """This feature has been temporarily disabled server-side""" - - USER_BANNED = 40_007 - """The user is banned from this guild""" - - MISSING_ACCESS = 50_001 - """Missing access""" - - INVALID_ACCOUNT_TYPE = 50_002 - """Invalid account type""" - - CANNOT_EXECUTE_ACTION_ON_DM_CHANNEL = 50_003 - """Cannot execute action on a DM channel""" - - WIDGET_DISABLED = 50_004 - """Widget Disabled""" - - CANNOT_EDIT_A_MESSAGE_AUTHORED_BY_ANOTHER_USER = 50_005 - """Cannot edit a message authored by another user""" - - CANNOT_SEND_AN_EMPTY_MESSAGE = 50_006 - """Cannot send an empty message""" - - CANNOT_SEND_MESSAGES_TO_THIS_USER = 50_007 - """Cannot send messages to this user""" - - CANNOT_SEND_MESSAGES_IN_VOICE_CHANNEL = 50_008 - """Cannot send messages in a voice channel""" - - CHANNEL_VERIFICATION_TOO_HIGH = 50_009 - """Channel verification level is too high""" - - OAUTH2_APPLICATION_DOES_NOT_HAVE_A_BOT = 50_010 - """OAuth2 application does not have a bot""" - - OAUTH2_APPLICATION_LIMIT_REACHED = 50_011 - """OAuth2 application limit reached""" - - INVALID_OAUTH2_STATE = 50_012 - """Invalid OAuth state""" - - MISSING_PERMISSIONS = 50_013 - """Missing permissions""" - - INVALID_AUTHENTICATION_TOKEN = 50_014 - """Invalid authentication token""" - - NOTE_IS_TOO_LONG = 50_015 - """Note is too long""" - - INVALID_NUMBER_OF_MESSAGES_TO_DELETE = 50_016 - """Provided too few or too many messages to delete. - - Must provide at least 2 and fewer than 100 messages to delete. - """ - - CANNOT_PIN_A_MESSAGE_IN_A_DIFFERENT_CHANNEL = 50_019 - """A message can only be pinned to the channel it was sent in""" - - INVALID_INVITE = 50_020 - """Invite code is either invalid or taken.""" - - CANNOT_EXECUTE_ACTION_ON_SYSTEM_MESSAGE = 50_021 - """Cannot execute action on a system message""" - - INVALID_OAUTH2_TOKEN = 50_025 - """Invalid OAuth2 access token""" - - MESSAGE_PROVIDED_WAS_TOO_OLD_TO_BULK_DELETE = 50_034 - """A message provided was too old to bulk delete""" - - INVALID_FORM_BODY = 50_035 - """Invalid Form Body""" - - ACCEPTED_INVITE_TO_GUILD_BOT_IS_NOT_IN = 50_036 - """An invite was accepted to a guild the application's bot is not in""" - - INVALID_API_VERSION = 50_041 - """Invalid API version""" - - REACTION_BLOCKED = 90_001 - """Reaction blocked""" - - RESOURCE_OVERLOADED = 130_000 - """The resource is overloaded.""" - - def __str__(self) -> str: - name = self.name.replace("_", " ").title() - return f"{self.value} {name}" - - -# pylint: enable=no-member diff --git a/hikari/internal/conversions.py b/hikari/internal/conversions.py index 9bcfbb0a7b..a55ff65207 100644 --- a/hikari/internal/conversions.py +++ b/hikari/internal/conversions.py @@ -58,9 +58,7 @@ DISCORD_EPOCH: typing.Final[int] = 1_420_070_400 ISO_8601_DATE_PART: typing.Final[typing.Pattern] = re.compile(r"^(\d{4})-(\d{2})-(\d{2})") -ISO_8601_TIME_PART: typing.Final[typing.Pattern] = re.compile( - r"T(\d{2}):(\d{2}):(\d{2})(?:\.(\d{1,6}))?", re.I -) +ISO_8601_TIME_PART: typing.Final[typing.Pattern] = re.compile(r"T(\d{2}):(\d{2}):(\d{2})(?:\.(\d{1,6}))?", re.I) ISO_8601_TZ_PART: typing.Final[typing.Pattern] = re.compile(r"([+-])(\d{2}):(\d{2})$") diff --git a/hikari/internal/http_client.py b/hikari/internal/http_client.py index 4a80b2553d..34c41a6187 100644 --- a/hikari/internal/http_client.py +++ b/hikari/internal/http_client.py @@ -29,7 +29,7 @@ import aiohttp.typedefs -from . import tracing +from hikari.internal import tracing class HTTPClient(abc.ABC): # pylint:disable=too-many-instance-attributes @@ -48,17 +48,11 @@ class HTTPClient(abc.ABC): # pylint:disable=too-many-instance-attributes allow_redirects : bool Whether to allow redirects or not. Defaults to `False`. connector : aiohttp.BaseConnector | None - Optional aiohttp connector info for making an HTTP connection + Optional aiohttp _connector info for making an HTTP connection debug : bool Defaults to `False`. If `True`, then a lot of contextual information - regarding low-level HTTP communication will be logged to the debug + regarding low-level HTTP communication will be logged to the _debug logger on this class. - json_deserialize : deserialization function - A custom JSON deserializer function to use. Defaults to `json.loads`. - json_serialize : serialization function - A custom JSON serializer function to use. Defaults to `json.dumps`. - proxy_headers : typing.Mapping[str, str] | None - Optional proxy headers to pass to HTTP requests. proxy_auth : aiohttp.BasicAuth | None Optional authorization to be used if using a proxy. proxy_url : str | None @@ -70,7 +64,7 @@ class HTTPClient(abc.ABC): # pylint:disable=too-many-instance-attributes verification. If 1 it will ignore potentially malicious SSL certificates. timeout : float | None - The optional timeout for all HTTP requests. + The optional _request_timeout for all HTTP requests. trust_env : bool If `True`, and no proxy info is given, then `HTTP_PROXY` and `HTTPS_PROXY` will be used from the environment variables if present. @@ -81,81 +75,61 @@ class HTTPClient(abc.ABC): # pylint:disable=too-many-instance-attributes """ __slots__ = ( - "__client_session", - "allow_redirects", - "connector", - "debug", "logger", - "json_deserialize", - "json_serialize", - "proxy_auth", - "proxy_headers", - "proxy_url", - "ssl_context", - "timeout", - "tracers", - "trust_env", - "verify_ssl", + "__client_session", + "_allow_redirects", + "_connector", + "_debug", + "_json_deserialize", + "_json_serialize", + "_proxy_auth", + "_proxy_headers", + "_proxy_url", + "_ssl_context", + "_request_timeout", + "_tracers", + "_trust_env", + "_verify_ssl", ) - GET: typing.Final[str] = "get" - POST: typing.Final[str] = "post" - PATCH: typing.Final[str] = "patch" - PUT: typing.Final[str] = "put" - HEAD: typing.Final[str] = "head" - DELETE: typing.Final[str] = "delete" - OPTIONS: typing.Final[str] = "options" - - APPLICATION_JSON: typing.Final[str] = "application/json" - APPLICATION_X_WWW_FORM_URLENCODED: typing.Final[str] = "application/x-www-form-urlencoded" - APPLICATION_OCTET_STREAM: typing.Final[str] = "application/octet-stream" - - allow_redirects: bool - """`True` if HTTP redirects are enabled, or `False` otherwise.""" - - connector: typing.Optional[aiohttp.BaseConnector] - """The base connector for the `aiohttp.ClientSession`, if provided.""" - - debug: bool - """`True` if debug mode is enabled. `False` otherwise.""" + _APPLICATION_JSON: typing.Final[str] = "application/json" + _APPLICATION_X_WWW_FORM_URLENCODED: typing.Final[str] = "application/x-www-form-urlencoded" + _APPLICATION_OCTET_STREAM: typing.Final[str] = "application/octet-stream" logger: logging.Logger """The logger to use for this object.""" - json_deserialize: typing.Callable[[typing.AnyStr], typing.Any] - """The JSON deserialization function. - - This consumes a JSON string and produces some object. - """ + _allow_redirects: bool + """`True` if HTTP redirects are enabled, or `False` otherwise.""" - json_serialize: typing.Callable[[typing.Any], typing.AnyStr] - """The JSON deserialization function. + _connector: typing.Optional[aiohttp.BaseConnector] + """The base _connector for the `aiohttp.ClientSession`, if provided.""" - This consumes an object and produces some JSON string. - """ + _debug: bool + """`True` if _debug mode is enabled. `False` otherwise.""" - proxy_auth: typing.Optional[aiohttp.BasicAuth] + _proxy_auth: typing.Optional[aiohttp.BasicAuth] """Proxy authorization to use.""" - proxy_headers: typing.Optional[typing.Mapping[str, str]] + _proxy_headers: typing.Optional[typing.Mapping[str, str]] """A set of headers to provide to a proxy server.""" - proxy_url: typing.Optional[str] + _proxy_url: typing.Optional[str] """An optional proxy URL to send requests to.""" - ssl_context: typing.Optional[ssl.SSLContext] + _ssl_context: typing.Optional[ssl.SSLContext] """The custom SSL context to use.""" - timeout: typing.Optional[float] - """The HTTP request timeout to abort requests after.""" + _request_timeout: typing.Optional[float] + """The HTTP request _request_timeout to abort requests after.""" - tracers: typing.List[tracing.BaseTracer] - """Request tracers. + _tracers: typing.List[tracing.BaseTracer] + """Request _tracers. These can be used to intercept HTTP request events on a low level. """ - trust_env: bool + _trust_env: bool """Whether to take notice of proxy environment variables. If `True`, and no proxy info is given, then `HTTP_PROXY` and @@ -165,7 +139,7 @@ class HTTPClient(abc.ABC): # pylint:disable=too-many-instance-attributes If `False`, then this information is instead ignored. """ - verify_ssl: bool + _verify_ssl: bool """Whether SSL certificates should be verified for each request. When this is `True` then an exception will be raised whenever invalid SSL @@ -179,8 +153,6 @@ def __init__( allow_redirects: bool = False, connector: typing.Optional[aiohttp.BaseConnector] = None, debug: bool = False, - json_deserialize: typing.Callable[[typing.AnyStr], typing.Dict] = json.loads, - json_serialize: typing.Callable[[typing.Dict], typing.AnyStr] = json.dumps, logger_name: typing.Optional[str] = None, proxy_auth: typing.Optional[aiohttp.BasicAuth] = None, proxy_headers: typing.Optional[aiohttp.typedefs.LooseHeaders] = None, @@ -195,19 +167,17 @@ def __init__( ) self.__client_session = None - self.allow_redirects = allow_redirects - self.connector = connector - self.debug = debug - self.json_serialize = json_serialize - self.json_deserialize = json_deserialize - self.proxy_auth = proxy_auth - self.proxy_headers = proxy_headers - self.proxy_url = proxy_url - self.ssl_context: ssl.SSLContext = ssl_context - self.timeout = timeout - self.trust_env = trust_env - self.tracers = [(tracing.DebugTracer(self.logger) if debug else tracing.CFRayTracer(self.logger))] - self.verify_ssl = verify_ssl + self._allow_redirects = allow_redirects + self._connector = connector + self._debug = debug + self._proxy_auth = proxy_auth + self._proxy_headers = proxy_headers + self._proxy_url = proxy_url + self._ssl_context: ssl.SSLContext = ssl_context + self._request_timeout = timeout + self._trust_env = trust_env + self._tracers = [(tracing.DebugTracer(self.logger) if debug else tracing.CFRayTracer(self.logger))] + self._verify_ssl = verify_ssl async def __aenter__(self) -> HTTPClient: return self @@ -231,19 +201,14 @@ def _acquire_client_session(self) -> aiohttp.ClientSession: ------- aiohttp.ClientSession The client session to use for requests. - - !!! warn - This must only be accessed within an asyncio event loop, otherwise - there is a risk that the session will not have the correct event - loop; hence why this is private. """ if self.__client_session is None: self.__client_session = aiohttp.ClientSession( - connector=self.connector, - trust_env=self.trust_env, + connector=self._connector, + trust_env=self._trust_env, version=aiohttp.HttpVersion11, - json_serialize=self.json_serialize or json.dumps, - trace_configs=[t.trace_config for t in self.tracers], + json_serialize=json.dumps, + trace_configs=[t.trace_config for t in self._tracers], ) return self.__client_session @@ -280,8 +245,8 @@ async def _perform_request( The HTTP response. """ if isinstance(body, (dict, list)): - body = bytes(self.json_serialize(body), "utf-8") - headers["content-type"] = self.APPLICATION_JSON + body = bytes(json.dumps(body), "utf-8") + headers["content-type"] = self._APPLICATION_JSON trace_request_ctx = types.SimpleNamespace() trace_request_ctx.request_body = body @@ -292,18 +257,18 @@ async def _perform_request( params=query, headers=headers, data=body, - allow_redirects=self.allow_redirects, - proxy=self.proxy_url, - proxy_auth=self.proxy_auth, - proxy_headers=self.proxy_headers, - verify_ssl=self.verify_ssl, - ssl_context=self.ssl_context, - timeout=self.timeout, + allow_redirects=self._allow_redirects, + proxy=self._proxy_url, + proxy_auth=self._proxy_auth, + proxy_headers=self._proxy_headers, + verify_ssl=self._verify_ssl, + ssl_context=self._ssl_context, + timeout=self._request_timeout, trace_request_ctx=trace_request_ctx, ) async def _create_ws( - self, url: str, *, compress: int = 0, autoping: bool = True, max_msg_size: int = 0 + self, url: str, *, compress: int = 0, auto_ping: bool = True, max_msg_size: int = 0 ) -> aiohttp.ClientWebSocketResponse: """Create a websocket. @@ -314,7 +279,7 @@ async def _create_ws( compress : int The compression type to use, as an int value. Use `0` to disable compression. - autoping : bool + auto_ping : bool If `True`, the client will manage automatically pinging/ponging in the background. If `False`, this will not occur. max_msg_size : int @@ -330,11 +295,11 @@ async def _create_ws( return await self._acquire_client_session().ws_connect( url=url, compress=compress, - autoping=autoping, + autoping=auto_ping, max_msg_size=max_msg_size, - proxy=self.proxy_url, - proxy_auth=self.proxy_auth, - proxy_headers=self.proxy_headers, - verify_ssl=self.verify_ssl, - ssl_context=self.ssl_context, + proxy=self._proxy_url, + proxy_auth=self._proxy_auth, + proxy_headers=self._proxy_headers, + verify_ssl=self._verify_ssl, + ssl_context=self._ssl_context, ) diff --git a/hikari/internal/more_asyncio.py b/hikari/internal/more_asyncio.py index f609795261..e39c34d4c4 100644 --- a/hikari/internal/more_asyncio.py +++ b/hikari/internal/more_asyncio.py @@ -59,7 +59,7 @@ def completed_future(result=None, /): def wait( - aws: typing.Union[more_typing.Coroutine[more_typing.T_co], typing.Awaitable], + aws: typing.Collection[typing.Union[more_typing.Coroutine[more_typing.T_co], typing.Awaitable[more_typing.T_co]]], *, timeout=None, return_when=asyncio.ALL_COMPLETED, diff --git a/hikari/internal/ratelimits.py b/hikari/internal/ratelimits.py index 92253ca974..dd3e6adb1f 100644 --- a/hikari/internal/ratelimits.py +++ b/hikari/internal/ratelimits.py @@ -29,7 +29,7 @@ of a route with specific major parameter values included (e.g. `POST /channels/123/messages`), and a `hikari.rest.routes.Route` as a definition of a route without specific parameter values included (e.g. -`POST /channels/{channel_id}/messages`). We can compile a +`POST /channels/{channel}/messages`). We can compile a `hikari.rest.routes.CompiledRoute` from a `hikari.rest.routes.Route` by providing the corresponding parameters as kwargs, as you may already know. diff --git a/hikari/internal/tracing.py b/hikari/internal/tracing.py index 7453e85f9f..13693a8b72 100644 --- a/hikari/internal/tracing.py +++ b/hikari/internal/tracing.py @@ -48,7 +48,7 @@ def trace_config(self): class CFRayTracer(BaseTracer): - """Regular debug logging of requests to a Cloudflare resource. + """Regular _debug logging of requests to a Cloudflare resource. Logs information about endpoints being hit, response latency, and any Cloudflare rays in the response. @@ -86,7 +86,7 @@ async def on_request_end(self, _, ctx, params): class DebugTracer(BaseTracer): - """Provides verbose debug logging of requests. + """Provides verbose _debug logging of requests. This logs several pieces of information during an AIOHTTP request such as request headers and body chunks, response headers, response body chunks, @@ -96,7 +96,7 @@ class DebugTracer(BaseTracer): !!! warn This may log potentially sensitive information such as authorization - tokens, so ensure those are removed from debug logs before proceeding + tokens, so ensure those are removed from _debug logs before proceeding to send logs to anyone. """ diff --git a/hikari/models/audit_logs.py b/hikari/models/audit_logs.py index 881bb8ff14..70a8e92d99 100644 --- a/hikari/models/audit_logs.py +++ b/hikari/models/audit_logs.py @@ -100,7 +100,7 @@ class AuditLogChangeKey(str, more_enums.Enum): ALLOW = "allow" DENY = "deny" INVITE_CODE = "code" - CHANNEL_ID = "channel_id" + CHANNEL_ID = "channel" INVITER_ID = "inviter_id" MAX_USES = "max_uses" USES = "uses" diff --git a/hikari/rest/channel.py b/hikari/rest/channel.py index 4655a668a2..ce9126a630 100644 --- a/hikari/rest/channel.py +++ b/hikari/rest/channel.py @@ -61,7 +61,7 @@ def __init__(self, app, channel, direction, first, session) -> None: async def _next_chunk(self): kwargs = { self._direction: self._first_id, - "channel_id": self._channel_id, + "channel": self._channel_id, "limit": 100, } diff --git a/hikari/rest/client.py b/hikari/rest/client.py index 6a4fc55d77..77388683ce 100644 --- a/hikari/rest/client.py +++ b/hikari/rest/client.py @@ -28,7 +28,7 @@ import typing -from hikari import aiohttp_config +from hikari import http_settings from hikari.api import rest_app from hikari.rest import channel from hikari.rest import gateway @@ -78,7 +78,7 @@ def __init__( self, *, app: rest_app.IRESTApp, - config: aiohttp_config.AIOHTTPConfig, + config: http_settings.HTTPSettings, debug: bool, token: typing.Optional[str], token_type: typing.Optional[str], diff --git a/hikari/rest/guild.py b/hikari/rest/guild.py index b5e1930907..0ef4d72271 100644 --- a/hikari/rest/guild.py +++ b/hikari/rest/guild.py @@ -575,7 +575,7 @@ async def update_guild( afk_channel : hikari.models.channels.GuildVoiceChannel | hikari.models.bases.Snowflake | int | str If specified, the object or ID for the new AFK voice channel. afk_timeout : datetime.timedelta | int - If specified, the new AFK timeout seconds timedelta. + If specified, the new AFK _request_timeout seconds timedelta. icon : hikari.models.files.BaseStream If specified, the new guild icon image file. owner : hikari.models.users.User | hikari.models.bases.Snowflake | int | str @@ -941,7 +941,7 @@ async def update_member( # pylint: disable=too-many-arguments to the end channel. This will also be raised if you're not in the guild. hikari.errors.BadRequest - If you pass `mute`, `deaf` or `channel_id` while the member + If you pass `mute`, `deaf` or `channel` while the member is not connected to a voice channel. If any invalid snowflake IDs are passed; a snowflake may be invalid due to it being outside of the range of a 64 bit integer. diff --git a/hikari/rest/routes.py b/hikari/rest/routes.py index 1800cd99aa..ab0e9618fc 100644 --- a/hikari/rest/routes.py +++ b/hikari/rest/routes.py @@ -25,7 +25,7 @@ import re import typing -DEFAULT_MAJOR_PARAMS: typing.Final[typing.Set[str]] = {"channel_id", "guild_id", "webhook_id"} +DEFAULT_MAJOR_PARAMS: typing.Final[typing.Set[str]] = {"channel", "guild_id", "webhook_id"} HASH_SEPARATOR: typing.Final[str] = ";" @@ -218,41 +218,41 @@ def __eq__(self, other) -> bool: POST = "POST" # Channels -GET_CHANNEL = Route(GET, "/channels/{channel_id}") -PATCH_CHANNEL = Route(PATCH, "/channels/{channel_id}") -DELETE_CHANNEL = Route(DELETE, "/channels/{channel_id}") +GET_CHANNEL = Route(GET, "/channels/{channel}") +PATCH_CHANNEL = Route(PATCH, "/channels/{channel}") +DELETE_CHANNEL = Route(DELETE, "/channels/{channel}") -GET_CHANNEL_INVITES = Route(GET, "/channels/{channel_id}/invites") -POST_CHANNEL_INVITES = Route(POST, "/channels/{channel_id}/invites") +GET_CHANNEL_INVITES = Route(GET, "/channels/{channel}/invites") +POST_CHANNEL_INVITES = Route(POST, "/channels/{channel}/invites") -GET_CHANNEL_MESSAGE = Route(GET, "/channels/{channel_id}/messages/{message_id}") -PATCH_CHANNEL_MESSAGE = Route(PATCH, "/channels/{channel_id}/messages/{message_id}") -DELETE_CHANNEL_MESSAGE = Route(DELETE, "/channels/{channel_id}/messages/{message_id}") +GET_CHANNEL_MESSAGE = Route(GET, "/channels/{channel}/messages/{message_id}") +PATCH_CHANNEL_MESSAGE = Route(PATCH, "/channels/{channel}/messages/{message_id}") +DELETE_CHANNEL_MESSAGE = Route(DELETE, "/channels/{channel}/messages/{message_id}") -GET_CHANNEL_MESSAGES = Route(GET, "/channels/{channel_id}/messages") -POST_CHANNEL_MESSAGES = Route(POST, "/channels/{channel_id}/messages") +GET_CHANNEL_MESSAGES = Route(GET, "/channels/{channel}/messages") +POST_CHANNEL_MESSAGES = Route(POST, "/channels/{channel}/messages") -POST_DELETE_CHANNEL_MESSAGES_BULK = Route(POST, "/channels/{channel_id}/messages/bulk-delete") +POST_DELETE_CHANNEL_MESSAGES_BULK = Route(POST, "/channels/{channel}/messages/bulk-delete") -PATCH_CHANNEL_PERMISSIONS = Route(PATCH, "/channels/{channel_id}/permissions/{overwrite_id}") -DELETE_CHANNEL_PERMISSIONS = Route(DELETE, "/channels/{channel_id}/permissions/{overwrite_id}") +PATCH_CHANNEL_PERMISSIONS = Route(PATCH, "/channels/{channel}/permissions/{overwrite_id}") +DELETE_CHANNEL_PERMISSIONS = Route(DELETE, "/channels/{channel}/permissions/{overwrite_id}") -DELETE_CHANNEL_PIN = Route(DELETE, "/channels/{channel_id}/pins/{message_id}") +DELETE_CHANNEL_PIN = Route(DELETE, "/channels/{channel}/pins/{message_id}") -GET_CHANNEL_PINS = Route(GET, "/channels/{channel_id}/pins") -PUT_CHANNEL_PINS = Route(PUT, "/channels/{channel_id}/pins/{message_id}") +GET_CHANNEL_PINS = Route(GET, "/channels/{channel}/pins") +PUT_CHANNEL_PINS = Route(PUT, "/channels/{channel}/pins/{message_id}") -POST_CHANNEL_TYPING = Route(POST, "/channels/{channel_id}/typing") +POST_CHANNEL_TYPING = Route(POST, "/channels/{channel}/typing") -POST_CHANNEL_WEBHOOKS = Route(POST, "/channels/{channel_id}/webhooks") -GET_CHANNEL_WEBHOOKS = Route(GET, "/channels/{channel_id}/webhooks") +POST_CHANNEL_WEBHOOKS = Route(POST, "/channels/{channel}/webhooks") +GET_CHANNEL_WEBHOOKS = Route(GET, "/channels/{channel}/webhooks") # Reactions -DELETE_ALL_REACTIONS = Route(DELETE, "/channels/{channel_id}/messages/{message_id}/reactions") +DELETE_ALL_REACTIONS = Route(DELETE, "/channels/{channel}/messages/{message_id}/reactions") -DELETE_REACTION_EMOJI = Route(DELETE, "/channels/{channel_id}/messages/{message_id}/reactions/{emoji}") -DELETE_REACTION_USER = Route(DELETE, "/channels/{channel_id}/messages/{message_id}/reactions/{emoji}/{used_id}") -GET_REACTIONS = Route(GET, "/channels/{channel_id}/messages/{message_id}/reactions/{emoji}") +DELETE_REACTION_EMOJI = Route(DELETE, "/channels/{channel}/messages/{message_id}/reactions/{emoji}") +DELETE_REACTION_USER = Route(DELETE, "/channels/{channel}/messages/{message_id}/reactions/{emoji}/{used_id}") +GET_REACTIONS = Route(GET, "/channels/{channel}/messages/{message_id}/reactions/{emoji}") # Guilds GET_GUILD = Route(GET, "/guilds/{guild_id}") @@ -342,8 +342,8 @@ def __eq__(self, other) -> bool: GET_MY_USER = Route(GET, "/users/@me") PATCH_MY_USER = Route(PATCH, "/users/@me") -PUT_MY_REACTION = Route(PUT, "/channels/{channel_id}/messages/{message_id}/reactions/{emoji}/@me") -DELETE_MY_REACTION = Route(DELETE, "/channels/{channel_id}/messages/{message_id}/reactions/{emoji}/@me") +PUT_MY_REACTION = Route(PUT, "/channels/{channel}/messages/{message_id}/reactions/{emoji}/@me") +DELETE_MY_REACTION = Route(DELETE, "/channels/{channel}/messages/{message_id}/reactions/{emoji}/@me") # Voice GET_VOICE_REGIONS = Route(GET, "/voice/regions") diff --git a/hikari/rest/session.py b/hikari/rest/session.py index 9d1702eeda..45be31051c 100644 --- a/hikari/rest/session.py +++ b/hikari/rest/session.py @@ -69,10 +69,10 @@ class RESTSession(http_client.HTTPClient): # pylint: disable=too-many-public-me base_url : str The base URL and route for the discord API connector : aiohttp.BaseConnector | None - Optional aiohttp connector info for making an HTTP connection + Optional aiohttp _connector info for making an HTTP connection debug : bool Defaults to `False`. If `True`, then a lot of contextual information - regarding low-level HTTP communication will be logged to the debug + regarding low-level HTTP communication will be logged to the _debug logger on this class. json_deserialize : deserialization function A custom JSON deserializer function to use. Defaults to `json.loads`. @@ -91,7 +91,7 @@ class RESTSession(http_client.HTTPClient): # pylint: disable=too-many-public-me verification. If 1 it will ignore potentially malicious SSL certificates. timeout : float | None - The optional timeout for all HTTP requests. + The optional _request_timeout for all HTTP requests. token : string | None The bot token for the client to use. You may start this with a prefix of either `Bot` or `Bearer` to force the token type, or @@ -146,7 +146,7 @@ class RESTSession(http_client.HTTPClient): # pylint: disable=too-many-public-me Your mileage may vary (YMMV). """ - verify_ssl: bool + _verify_ssl: bool """Whether SSL certificates should be verified for each request. When this is `True` then an exception will be raised whenever invalid SSL @@ -180,8 +180,6 @@ def __init__( # pylint: disable=too-many-locals allow_redirects=allow_redirects, connector=connector, debug=debug, - json_deserialize=json_deserialize, - json_serialize=json_serialize, proxy_auth=proxy_auth, proxy_headers=proxy_headers, proxy_url=proxy_url, @@ -234,7 +232,7 @@ async def _request_json_response( headers = {} if headers is None else headers headers["x-ratelimit-precision"] = "millisecond" - headers["accept"] = self.APPLICATION_JSON + headers["accept"] = self._APPLICATION_JSON if self._token is not None and not suppress_authorization_header: headers["authorization"] = self._token @@ -275,7 +273,7 @@ async def __request_json_response(self, compiled_route, headers, body, query): # Handle the response. if 200 <= response.status < 300: - if response.content_type == self.APPLICATION_JSON: + if response.content_type == self._APPLICATION_JSON: # Only deserializing here stops Cloudflare shenanigans messing us around. return self.json_deserialize(raw_body) raise errors.HTTPError(real_url, f"Expected JSON response but received {response.content_type}") @@ -322,7 +320,7 @@ async def _handle_rate_limits_for_response(self, compiled_route, response): ) if response.status == http.HTTPStatus.TOO_MANY_REQUESTS: - body = await response.json() if response.content_type == self.APPLICATION_JSON else await response.read() + body = await response.json() if response.content_type == self._APPLICATION_JSON else await response.read() # We are being rate limited. if isinstance(body, dict): @@ -702,13 +700,13 @@ async def create_message( conversions.put_if_specified(json_payload, "embed", embed) conversions.put_if_specified(json_payload, "allowed_mentions", allowed_mentions) - form.add_field("payload_json", json.dumps(json_payload), content_type=self.APPLICATION_JSON) + form.add_field("payload_json", json.dumps(json_payload), content_type=self._APPLICATION_JSON) if files is ...: files = more_collections.EMPTY_SEQUENCE for i, file in enumerate(files): - form.add_field(f"file{i}", file, filename=file.filename, content_type=self.APPLICATION_OCTET_STREAM) + form.add_field(f"file{i}", file, filename=file.filename, content_type=self._APPLICATION_OCTET_STREAM) route = routes.POST_CHANNEL_MESSAGES.compile(channel_id=channel_id) @@ -1521,7 +1519,7 @@ async def modify_guild( # lgtm [py/similar-function] afk_channel_id : str If specified, the new ID for the AFK voice channel. afk_timeout : int - If specified, the new AFK timeout period in seconds + If specified, the new AFK _request_timeout period in seconds icon : bytes If specified, the new guild icon image in bytes form. owner_id : str @@ -1847,14 +1845,14 @@ async def modify_guild_member( # lgtm [py/similar-function] to the end channel. This will also be raised if you're not in the guild. hikari.errors.BadRequest - If you pass `mute`, `deaf` or `channel_id` while the member is not connected to a voice channel. + If you pass `mute`, `deaf` or `channel` while the member is not connected to a voice channel. """ payload = {} conversions.put_if_specified(payload, "nick", nick) conversions.put_if_specified(payload, "roles", roles) conversions.put_if_specified(payload, "mute", mute) conversions.put_if_specified(payload, "deaf", deaf) - conversions.put_if_specified(payload, "channel_id", channel_id) + conversions.put_if_specified(payload, "channel", channel_id) route = routes.PATCH_GUILD_MEMBER.compile(guild_id=guild_id, user_id=user_id) await self._request_json_response(route, body=payload, reason=reason) @@ -2530,7 +2528,7 @@ async def modify_guild_embed( If you either lack the `MANAGE_GUILD` permission or are not in the guild. """ payload = {} - conversions.put_if_specified(payload, "channel_id", channel_id) + conversions.put_if_specified(payload, "channel", channel_id) conversions.put_if_specified(payload, "enabled", enabled) route = routes.PATCH_GUILD_EMBED.compile(guild_id=guild_id) return await self._request_json_response(route, body=payload, reason=reason) @@ -2967,7 +2965,7 @@ async def modify_webhook( """ payload = {} conversions.put_if_specified(payload, "name", name) - conversions.put_if_specified(payload, "channel_id", channel_id) + conversions.put_if_specified(payload, "channel", channel_id) conversions.put_if_specified(payload, "avatar", avatar, conversions.image_bytes_to_image_data) if webhook_token is ...: route = routes.PATCH_WEBHOOK.compile(webhook_id=webhook_id) diff --git a/hikari/stateless/bot.py b/hikari/stateless/bot.py index f492b94d27..7ff78a57e0 100644 --- a/hikari/stateless/bot.py +++ b/hikari/stateless/bot.py @@ -26,13 +26,13 @@ from hikari import application from hikari import rest -from hikari.gateway import client +from .. import gateway from hikari.gateway import intent_aware_dispatchers from . import manager if typing.TYPE_CHECKING: from hikari import application - from hikari import aiohttp_config + from hikari import http_settings class StatelessBot(application.Application): @@ -42,8 +42,8 @@ class StatelessBot(application.Application): """ @staticmethod - def _create_shard(app: application.Application, shard_id: int, shard_count: int, url: str) -> client.GatewayClient: - return client.GatewayClient(app=app, shard_id=shard_id, shard_count=shard_count, url=url) + def _create_shard(app: application.Application, shard_id: int, shard_count: int, url: str) -> gateway.Gateway: + return gateway.Gateway(app=app, shard_id=shard_id, shard_count=shard_count, url=url) @staticmethod def _create_rest(app: application.Application) -> rest.RESTClient: @@ -55,6 +55,6 @@ def _create_event_manager(app: application.Application) -> manager.StatelessEven @staticmethod def _create_event_dispatcher( - config: aiohttp_config.BotConfig, + config: http_settings.BotConfig, ) -> intent_aware_dispatchers.IntentAwareEventDispatcherImpl: return intent_aware_dispatchers.IntentAwareEventDispatcherImpl(config.intents) diff --git a/tests/hikari/events/test_channel.py b/tests/hikari/events/test_channel.py index 7099a633a2..0768872627 100644 --- a/tests/hikari/events/test_channel.py +++ b/tests/hikari/events/test_channel.py @@ -123,7 +123,7 @@ class TestChannelPinsUpdateEvent: def test_chanel_pin_update_payload(self): return { "guild_id": "424242", - "channel_id": "29292929", + "channel": "29292929", "last_pin_timestamp": "2020-03-20T16:08:25.412000+00:00", } @@ -145,7 +145,7 @@ def test_deserialize(self, test_chanel_pin_update_payload): class TestWebhookUpdateEvent: @pytest.fixture() def test_webhook_update_payload(self): - return {"guild_id": "2929292", "channel_id": "94949494"} + return {"guild_id": "2929292", "channel": "94949494"} def test_deserialize(self, test_webhook_update_payload): webhook_update_obj = channel.WebhookUpdateEvent.deserialize(test_webhook_update_payload) @@ -169,7 +169,7 @@ def test_member_payload(self, test_user_payload): @pytest.fixture() def test_typing_start_event_payload(self, test_member_payload): return { - "channel_id": "123123123", + "channel": "123123123", "guild_id": "33333333", "user_id": "2020202", "timestamp": 1231231231, @@ -206,7 +206,7 @@ class TestInviteCreateEvent: @pytest.fixture() def test_invite_create_payload(self, test_user_payload): return { - "channel_id": "939393", + "channel": "939393", "code": "owouwuowouwu", "created_at": "2019-05-17T06:26:56.936000+00:00", "guild_id": "45949", @@ -269,7 +269,7 @@ def test_max_age_when_zero(self, test_invite_create_payload): class TestInviteDeleteEvent: @pytest.fixture() def test_invite_delete_payload(self): - return {"channel_id": "393939", "code": "blahblahblah", "guild_id": "3834833"} + return {"channel": "393939", "code": "blahblahblah", "guild_id": "3834833"} def test_deserialize(self, test_invite_delete_payload): invite_delete_obj = channel.InviteDeleteEvent.deserialize(test_invite_delete_payload) diff --git a/tests/hikari/events/test_message.py b/tests/hikari/events/test_message.py index b348a3b86f..69050d29a9 100644 --- a/tests/hikari/events/test_message.py +++ b/tests/hikari/events/test_message.py @@ -75,7 +75,7 @@ def test_attachment_payload(self): "filename": "nyaa.png", "size": 1024, "url": "heck.heck", - "proxy_url": "proxy.proxy?heck", + "_proxy_url": "proxy.proxy?heck", "height": 42, "width": 84, } @@ -98,7 +98,7 @@ def test_application_payload(self): @pytest.fixture() def test_reference_payload(self): - return {"channel_id": "432341231231"} + return {"channel": "432341231231"} @pytest.fixture() def test_message_update_payload( @@ -115,7 +115,7 @@ def test_message_update_payload( ): return { "id": "3939399393", - "channel_id": "93939393939", + "channel": "93939393939", "guild_id": "66557744883399", "author": test_user_payload, "member": test_member_payload, @@ -256,9 +256,9 @@ def test_deserialize( assert message_update_payload.nonce == "6454345345345345" def test_partial_message_update(self): - message_update_obj = message.MessageUpdateEvent.deserialize({"id": "393939", "channel_id": "434949"}) + message_update_obj = message.MessageUpdateEvent.deserialize({"id": "393939", "channel": "434949"}) for key in message_update_obj.__slots__: - if key in ("id", "channel_id"): + if key in ("id", "channel"): continue assert getattr(message_update_obj, key) is unset.UNSET assert message_update_obj.id == 393939 @@ -268,7 +268,7 @@ def test_partial_message_update(self): class TestMessageDeleteEvent: @pytest.fixture() def test_message_delete_payload(self): - return {"channel_id": "20202020", "id": "2929", "guild_id": "1010101"} + return {"channel": "20202020", "id": "2929", "guild_id": "1010101"} def test_deserialize(self, test_message_delete_payload): message_delete_obj = message.MessageDeleteEvent.deserialize(test_message_delete_payload) @@ -280,7 +280,7 @@ def test_deserialize(self, test_message_delete_payload): class TestMessageDeleteBulkEvent: @pytest.fixture() def test_message_delete_bulk_payload(self): - return {"channel_id": "20202020", "ids": ["2929", "4394"], "guild_id": "1010101"} + return {"channel": "20202020", "ids": ["2929", "4394"], "guild_id": "1010101"} def test_deserialize(self, test_message_delete_bulk_payload): message_delete_bulk_obj = message.MessageDeleteBulkEvent.deserialize(test_message_delete_bulk_payload) @@ -294,7 +294,7 @@ class TestMessageReactionAddEvent: def test_message_reaction_add_payload(self, test_member_payload, test_emoji_payload): return { "user_id": "9494949", - "channel_id": "4393939", + "channel": "4393939", "message_id": "2993993", "guild_id": "49494949", "member": test_member_payload, @@ -338,7 +338,7 @@ class TestMessageReactionRemoveEvent: def test_message_reaction_remove_payload(self, test_emoji_payload): return { "user_id": "9494949", - "channel_id": "4393939", + "channel": "4393939", "message_id": "2993993", "guild_id": "49494949", "emoji": test_emoji_payload, @@ -366,7 +366,7 @@ def test_deserialize(self, test_message_reaction_remove_payload, test_emoji_payl class TestMessageReactionRemoveAllEvent: @pytest.fixture() def test_reaction_remove_all_payload(self): - return {"channel_id": "3493939", "message_id": "944949", "guild_id": "49494949"} + return {"channel": "3493939", "message_id": "944949", "guild_id": "49494949"} def test_deserialize(self, test_reaction_remove_all_payload): message_reaction_remove_all_obj = message.MessageReactionRemoveAllEvent.deserialize( @@ -380,7 +380,7 @@ def test_deserialize(self, test_reaction_remove_all_payload): class TestMessageReactionRemoveEmojiEvent: @pytest.fixture() def test_message_reaction_remove_emoji_payload(self, test_emoji_payload): - return {"channel_id": "4393939", "message_id": "2993993", "guild_id": "49494949", "emoji": test_emoji_payload} + return {"channel": "4393939", "message_id": "2993993", "guild_id": "49494949", "emoji": test_emoji_payload} def test_deserialize(self, test_message_reaction_remove_emoji_payload, test_emoji_payload): mock_emoji = mock.MagicMock(emojis.CustomEmoji) diff --git a/tests/hikari/gateway/test_client.py b/tests/hikari/gateway/test_client.py index f1710bf54f..230b9a7c0d 100644 --- a/tests/hikari/gateway/test_client.py +++ b/tests/hikari/gateway/test_client.py @@ -24,11 +24,11 @@ import mock import pytest -from hikari import aiohttp_config +from hikari import http_settings from hikari import errors from hikari import application from hikari.gateway import consumers -from hikari.gateway import client as high_level_shards +from hikari import gateway as high_level_shards from hikari.gateway import connection as low_level_shards from hikari.gateway import gateway_state from hikari.internal import codes @@ -74,7 +74,7 @@ def shard_client_obj(mock_app): session_id=None, ) with mock.patch.object(low_level_shards, "Shard", return_value=mock_shard_connection): - return _helpers.unslot_class(high_level_shards.GatewayClient)(0, 1, mock_app, "some_url") + return _helpers.unslot_class(high_level_shards.Gateway)(0, 1, mock_app, "some_url") class TestShardClientImpl: @@ -83,11 +83,11 @@ class DummyConsumer(consumers.RawEventConsumer): def process_raw_event(self, _client, name, payload): return "ASSERT TRUE" - shard_client_obj = high_level_shards.GatewayClient( + shard_client_obj = high_level_shards.Gateway( 0, 1, mock.MagicMock( - application.Application, config=aiohttp_config.GatewayConfig(), event_manager=DummyConsumer() + application.Application, config=http_settings.GatewayConfig(), event_manager=DummyConsumer() ), "some_url", ) @@ -98,10 +98,10 @@ def test_connection_is_set(self, shard_client_obj): mock_shard_connection = mock.MagicMock(low_level_shards.Shard) with mock.patch.object(low_level_shards, "Shard", return_value=mock_shard_connection): - shard_client_obj = high_level_shards.GatewayClient( + shard_client_obj = high_level_shards.Gateway( 0, 1, - mock.MagicMock(application.Application, event_manager=None, config=aiohttp_config.GatewayConfig()), + mock.MagicMock(application.Application, event_manager=None, config=http_settings.GatewayConfig()), "some_url", ) diff --git a/tests/hikari/gateway/test_connection.py b/tests/hikari/gateway/test_connection.py index f036d261bc..5b8b89584c 100644 --- a/tests/hikari/gateway/test_connection.py +++ b/tests/hikari/gateway/test_connection.py @@ -104,7 +104,7 @@ async def test_dispatch_is_callable(self): async def test_compression(self, compression, expected_url_query): url = "ws://baka-im-not-a-http-url:49620/locate/the/bloody/websocket?ayyyyy=lmao" client = connection.Shard(token="xxx", url=url, compression=compression) - scheme, netloc, path, params, query, fragment = urllib.parse.urlparse(client._url) + scheme, netloc, path, params, query, fragment = urllib.parse.urlparse(client.url) assert scheme == "ws" assert netloc == "baka-im-not-a-http-url:49620" assert path == "/locate/the/bloody/websocket" @@ -121,7 +121,7 @@ async def test_init_hearbeat_defaults_before_startup(self): async def test_init_connected_at_is_nan(self): client = connection.Shard(token="xxx", url="yyy") - assert math.isnan(client._connected_at) + assert math.isnan(client.connected_at) @pytest.mark.asyncio @@ -133,7 +133,7 @@ class TestShardUptimeProperty: async def test_uptime(self, connected_at, now, expected_uptime): with mock.patch("time.perf_counter", return_value=now): client = connection.Shard(token="xxx", url="yyy") - client._connected_at = connected_at + client.connected_at = connected_at assert client.up_time == expected_uptime @@ -142,7 +142,7 @@ class TestShardIsConnectedProperty: @pytest.mark.parametrize(["connected_at", "is_connected"], [(float("nan"), False), (15, True), (2500.0, True),]) async def test_is_connected(self, connected_at, is_connected): client = connection.Shard(token="xxx", url="yyy") - client._connected_at = connected_at + client.connected_at = connected_at assert client.is_connected is is_connected @@ -164,7 +164,7 @@ class TestGatewayReconnectCountProperty: async def test_value(self, disconnect_count, is_connected, expected_reconnect_count): client = connection.Shard(token="xxx", url="yyy") client.disconnect_count = disconnect_count - client._connected_at = 420 if is_connected else float("nan") + client.connected_at = 420 if is_connected else float("nan") assert client.reconnect_count == expected_reconnect_count @@ -205,7 +205,7 @@ def suppress_closure(self): yield async def test_RuntimeError_if_already_connected(self, client): - client._connected_at = 22.4 # makes client expect to be connected + client.connected_at = 22.4 # makes client expect to be connected try: with self.suppress_closure(): @@ -253,7 +253,7 @@ async def test_closed_event_set_on_connect_terminate(self, client): async def test_session_opened_with_expected_kwargs(self, client): with self.suppress_closure(): await client.connect() - client._create_ws.assert_awaited_once_with(client._url, compress=0, autoping=True, max_msg_size=0) + client._create_ws.assert_awaited_once_with(client.url, compress=0, auto_ping=True, max_msg_size=0) @_helpers.timeout_after(10.0) async def test_ws_closed_afterwards(self, client): @@ -263,12 +263,12 @@ async def test_ws_closed_afterwards(self, client): @_helpers.timeout_after(10.0) async def test_disconnecting_unsets_connected_at(self, client): - assert math.isnan(client._connected_at) + assert math.isnan(client.connected_at) with mock.patch("time.perf_counter", return_value=420): with self.suppress_closure(): await client.connect() - assert math.isnan(client._connected_at) + assert math.isnan(client.connected_at) @_helpers.timeout_after(10.0) async def test_disconnecting_unsets_last_message_received(self, client): @@ -330,7 +330,7 @@ async def test_new_zlib_each_time(self, client): assert client._zlib is not None assert previous_zlib is not client._zlib previous_zlib = client._zlib - client._connected_at = float("nan") + client.connected_at = float("nan") @_helpers.timeout_after(10.0) async def test_hello(self, client): @@ -521,7 +521,7 @@ async def test_identify_payload_no_intents_no_presence(self, client): client._intents = None client.session_id = None client._token = "aaaa" - client._large_threshold = 420 + client.large_threshold = 420 client.shard_id = 69 client.shard_count = 96 @@ -546,7 +546,7 @@ async def test_identify_payload_with_presence(self, client): client._intents = None client.session_id = None client._token = "aaaa" - client._large_threshold = 420 + client.large_threshold = 420 client.shard_id = 69 client.shard_count = 96 @@ -572,7 +572,7 @@ async def test_identify_payload_with_intents(self, client): client._intents = intents client.session_id = None client._token = "aaaa" - client._large_threshold = 420 + client.large_threshold = 420 client.shard_id = 69 client.shard_count = 96 @@ -599,7 +599,7 @@ async def test_identify_payload_with_intents_and_presence(self, client): client._intents = intents client.session_id = None client._token = "aaaa" - client._large_threshold = 420 + client.large_threshold = 420 client.shard_id = 69 client.shard_count = 96 @@ -902,12 +902,12 @@ def client(self, event_loop): client = _helpers.mock_methods_on(client, except_=("update_voice_state",)) return client - @pytest.mark.parametrize("channel_id", ["1234", None]) + @pytest.mark.parametrize("channel", ["1234", None]) async def test_sends_payload(self, client, channel_id): await client.update_voice_state("9987", channel_id, True, False) client._send.assert_awaited_once_with( { "op": codes.GatewayOpcode.VOICE_STATE_UPDATE, - "d": {"guild_id": "9987", "channel_id": channel_id, "self_mute": True, "self_deaf": False,}, + "d": {"guild_id": "9987", "channel": channel_id, "self_mute": True, "self_deaf": False,}, } ) diff --git a/tests/hikari/internal/test_http_client.py b/tests/hikari/internal/test_http_client.py index 5b04e43952..698c98f14d 100644 --- a/tests/hikari/internal/test_http_client.py +++ b/tests/hikari/internal/test_http_client.py @@ -45,30 +45,30 @@ def client(client_session): class TestInit: async def test_CFRayTracer_used_for_non_debug(self): async with http_client.HTTPClient(debug=False) as client: - assert len(client.tracers) == 1 - assert isinstance(client.tracers[0], tracing.CFRayTracer) + assert len(client._tracers) == 1 + assert isinstance(client._tracers[0], tracing.CFRayTracer) async def test_DebugTracer_used_for_debug(self): async with http_client.HTTPClient(debug=True) as client: - assert len(client.tracers) == 1 - assert isinstance(client.tracers[0], tracing.DebugTracer) + assert len(client._tracers) == 1 + assert isinstance(client._tracers[0], tracing.DebugTracer) @pytest.mark.asyncio class TestAcquireClientSession: async def test_acquire_creates_new_session_if_one_does_not_exist(self, client): - client.connector = mock.MagicMock() - client.trust_env = mock.MagicMock() + client._connector = mock.MagicMock() + client._trust_env = mock.MagicMock() _helpers.set_private_attr(client, "client_session", None) cs = client._acquire_client_session() assert _helpers.get_private_attr(client, "client_session") is cs aiohttp.ClientSession.assert_called_once_with( - connector=client.connector, - trust_env=client.trust_env, + connector=client._connector, + trust_env=client._trust_env, version=aiohttp.HttpVersion11, json_serialize=json.dumps, - trace_configs=[t.trace_config for t in client.tracers], + trace_configs=[t.trace_config for t in client._tracers], ) async def test_acquire_repeated_calls_caches_client_session(self, client): @@ -97,13 +97,13 @@ async def test_close_when_running(self, client, client_session): @pytest.mark.asyncio class TestPerformRequest: async def test_perform_request_form_data(self, client, client_session): - client.allow_redirects = mock.MagicMock() - client.proxy_url = mock.MagicMock() - client.proxy_auth = mock.MagicMock() - client.proxy_headers = mock.MagicMock() - client.verify_ssl = mock.MagicMock() - client.ssl_context = mock.MagicMock() - client.timeout = mock.MagicMock() + client._allow_redirects = mock.MagicMock() + client._proxy_url = mock.MagicMock() + client._proxy_auth = mock.MagicMock() + client._proxy_headers = mock.MagicMock() + client._verify_ssl = mock.MagicMock() + client._ssl_context = mock.MagicMock() + client._request_timeout = mock.MagicMock() form_data = aiohttp.FormData() @@ -124,24 +124,24 @@ async def test_perform_request_form_data(self, client, client_session): params={"foo": "bar"}, headers={"X-Foo-Count": "122"}, data=form_data, - allow_redirects=client.allow_redirects, - proxy=client.proxy_url, - proxy_auth=client.proxy_auth, - proxy_headers=client.proxy_headers, - verify_ssl=client.verify_ssl, - ssl_context=client.ssl_context, - timeout=client.timeout, + allow_redirects=client._allow_redirects, + proxy=client._proxy_url, + proxy_auth=client._proxy_auth, + proxy_headers=client._proxy_headers, + verify_ssl=client._verify_ssl, + ssl_context=client._ssl_context, + timeout=client._request_timeout, trace_request_ctx=trace_request_ctx, ) async def test_perform_request_json(self, client, client_session): - client.allow_redirects = mock.MagicMock() - client.proxy_url = mock.MagicMock() - client.proxy_auth = mock.MagicMock() - client.proxy_headers = mock.MagicMock() - client.verify_ssl = mock.MagicMock() - client.ssl_context = mock.MagicMock() - client.timeout = mock.MagicMock() + client._allow_redirects = mock.MagicMock() + client._proxy_url = mock.MagicMock() + client._proxy_auth = mock.MagicMock() + client._proxy_headers = mock.MagicMock() + client._verify_ssl = mock.MagicMock() + client._ssl_context = mock.MagicMock() + client._request_timeout = mock.MagicMock() jsonified_body = b'{"hello": "world"}' @@ -166,13 +166,13 @@ async def test_perform_request_json(self, client, client_session): params={"foo": "bar"}, headers={"X-Foo-Count": "122", "content-type": "application/json"}, data=jsonified_body, - allow_redirects=client.allow_redirects, - proxy=client.proxy_url, - proxy_auth=client.proxy_auth, - proxy_headers=client.proxy_headers, - verify_ssl=client.verify_ssl, - ssl_context=client.ssl_context, - timeout=client.timeout, + allow_redirects=client._allow_redirects, + proxy=client._proxy_url, + proxy_auth=client._proxy_auth, + proxy_headers=client._proxy_headers, + verify_ssl=client._verify_ssl, + ssl_context=client._ssl_context, + timeout=client._request_timeout, trace_request_ctx=trace_request_ctx, ) @@ -180,18 +180,18 @@ async def test_perform_request_json(self, client, client_session): @pytest.mark.asyncio class TestCreateWs: async def test_create_ws(self, client, client_session): - client.allow_redirects = mock.MagicMock() - client.proxy_url = mock.MagicMock() - client.proxy_auth = mock.MagicMock() - client.proxy_headers = mock.MagicMock() - client.verify_ssl = mock.MagicMock() - client.ssl_context = mock.MagicMock() - client.timeout = mock.MagicMock() + client._allow_redirects = mock.MagicMock() + client._proxy_url = mock.MagicMock() + client._proxy_auth = mock.MagicMock() + client._proxy_headers = mock.MagicMock() + client._verify_ssl = mock.MagicMock() + client._ssl_context = mock.MagicMock() + client._request_timeout = mock.MagicMock() expected_ws = mock.MagicMock() client_session.ws_connect = mock.AsyncMock(return_value=expected_ws) - actual_ws = await client._create_ws("foo://bar", compress=5, autoping=True, max_msg_size=3) + actual_ws = await client._create_ws("foo://bar", compress=5, auto_ping=True, max_msg_size=3) assert expected_ws is actual_ws @@ -200,9 +200,9 @@ async def test_create_ws(self, client, client_session): compress=5, autoping=True, max_msg_size=3, - proxy=client.proxy_url, - proxy_auth=client.proxy_auth, - proxy_headers=client.proxy_headers, - verify_ssl=client.verify_ssl, - ssl_context=client.ssl_context, + proxy=client._proxy_url, + proxy_auth=client._proxy_auth, + proxy_headers=client._proxy_headers, + verify_ssl=client._verify_ssl, + ssl_context=client._ssl_context, ) diff --git a/tests/hikari/internal/test_marshaller.py b/tests/hikari/internal/test_marshaller.py index 646f56c1c2..b2870c1a3f 100644 --- a/tests/hikari/internal/test_marshaller.py +++ b/tests/hikari/internal/test_marshaller.py @@ -32,7 +32,7 @@ def test_dereference_handle_module_only(self): def test_dereference_handle_module_and_attribute(self): assert ( - marshaller.dereference_handle("hikari.internal.codes#GatewayCloseCode.AUTHENTICATION_FAILED") + marshaller.dereference_handle("hikari.internal.codes#_GatewayCloseCode.AUTHENTICATION_FAILED") is codes.GatewayCloseCode.AUTHENTICATION_FAILED ) diff --git a/tests/hikari/models/test_applications.py b/tests/hikari/models/test_applications.py index b379d15d0e..f8a9d48d85 100644 --- a/tests/hikari/models/test_applications.py +++ b/tests/hikari/models/test_applications.py @@ -210,9 +210,9 @@ class TestApplication: def test_deserialize(self, application_information_payload, team_payload, owner_payload, mock_app): application_obj = applications.Application.deserialize(application_information_payload, app=mock_app) assert application_obj.team == applications.Team.deserialize(team_payload) - assert application_obj.team._zookeeper is mock_app + assert application_obj.team._gateway_consumer is mock_app assert application_obj.owner == applications.ApplicationOwner.deserialize(owner_payload) - assert application_obj.owner._zookeeper is mock_app + assert application_obj.owner._gateway_consumer is mock_app assert application_obj.id == 209333111222 assert application_obj.name == "Dream Sweet in Sea Major" assert application_obj.icon_hash == "iwiwiwiwiw" diff --git a/tests/hikari/models/test_audit_logs.py b/tests/hikari/models/test_audit_logs.py index 95a69b459e..a88057d629 100644 --- a/tests/hikari/models/test_audit_logs.py +++ b/tests/hikari/models/test_audit_logs.py @@ -193,7 +193,7 @@ class TestMessagePinEntryInfo: @pytest.fixture() def test_message_pin_info_payload(self): return { - "channel_id": "123123123", + "channel": "123123123", "message_id": "69696969", } @@ -222,7 +222,7 @@ def test_deserialize(self, test_member_prune_info_payload, mock_app): class TestMessageDeleteEntryInfo: @pytest.fixture() def test_message_delete_info_payload(self): - return {"count": "42", "channel_id": "4206942069"} + return {"count": "42", "channel": "4206942069"} def test_deserialize(self, test_message_delete_info_payload, mock_app): message_delete_entry_info = audit_logs.MessageDeleteEntryInfo.deserialize( @@ -258,7 +258,7 @@ def test_deserialize(self, test_member_disconnect_info_payload, mock_app): class TestMemberMoveEntryInfo: @pytest.fixture() def test_member_move_info_payload(self): - return {"count": "42", "channel_id": "22222222"} + return {"count": "42", "channel": "22222222"} def test_deserialize(self, test_member_move_info_payload, mock_app): member_move_entry_info = audit_logs.MemberMoveEntryInfo.deserialize(test_member_move_info_payload, app=mock_app) @@ -391,7 +391,7 @@ def test_user_payload(self): @pytest.fixture() def test_webhook_payload(self): - return {"id": "424242", "type": 1, "channel_id": "2020202"} + return {"id": "424242", "type": 1, "channel": "2020202"} @pytest.fixture() def test_audit_log_payload( @@ -432,7 +432,7 @@ def test_deserialize( assert audit_log_obj.entries == { 694026906592477214: audit_logs.AuditLogEntry.deserialize(test_audit_log_entry_payload) } - assert audit_log_obj.entries[694026906592477214]._zookeeper is mock_app + assert audit_log_obj.entries[694026906592477214]._gateway_consumer is mock_app assert audit_log_obj.webhooks == {424242: mock_webhook_obj} assert audit_log_obj.users == {92929292: mock_user_obj} assert audit_log_obj.integrations == {33590653072239123: mock_integration_obj} @@ -441,7 +441,7 @@ def test_deserialize( class TestAuditLogIterator: @pytest.mark.asyncio async def test__fill_when_entities_returned(self, mock_app): - mock_webhook_payload = {"id": "292393993", "channel_id": "43242"} + mock_webhook_payload = {"id": "292393993", "channel": "43242"} mock_webhook_obj = mock.MagicMock(webhooks.Webhook, id=292393993) mock_user_payload = {"id": "929292", "public_flags": "22222"} mock_user_obj = mock.MagicMock(users.User, id=929292) diff --git a/tests/hikari/models/test_bases.py b/tests/hikari/models/test_bases.py index 6010a158ff..cd754ebbf0 100644 --- a/tests/hikari/models/test_bases.py +++ b/tests/hikari/models/test_bases.py @@ -41,7 +41,7 @@ class StubEntity(bases.Entity, marshaller.Deserializable, marshaller.Serializabl def test_deserialize(self, stub_entity): mock_app = mock.MagicMock(application.Application) entity = stub_entity.deserialize({}, app=mock_app) - assert entity._zookeeper is mock_app + assert entity._gateway_consumer is mock_app def test_serialize(self, stub_entity): mock_app = mock.MagicMock(application.Application) diff --git a/tests/hikari/models/test_channels.py b/tests/hikari/models/test_channels.py index 2e94d73bf9..e3c3d3a90b 100644 --- a/tests/hikari/models/test_channels.py +++ b/tests/hikari/models/test_channels.py @@ -272,7 +272,7 @@ def test_deserialize(self, test_guild_category_payload, test_permission_overwrit assert channel_obj.permission_overwrites == { 4242: channels.PermissionOverwrite.deserialize(test_permission_overwrite_payload) } - assert channel_obj.permission_overwrites[4242]._zookeeper is mock_app + assert channel_obj.permission_overwrites[4242]._gateway_consumer is mock_app assert channel_obj.guild_id == 9876 assert channel_obj.position == 3 assert channel_obj.name == "Test" @@ -289,7 +289,7 @@ def test_deserialize(self, test_guild_text_channel_payload, test_permission_over assert channel_obj.permission_overwrites == { 4242: channels.PermissionOverwrite.deserialize(test_permission_overwrite_payload) } - assert channel_obj.permission_overwrites[4242]._zookeeper is mock_app + assert channel_obj.permission_overwrites[4242]._gateway_consumer is mock_app assert channel_obj.guild_id == 567 assert channel_obj.position == 6 assert channel_obj.name == "general" @@ -309,7 +309,7 @@ def test_deserialize(self, test_guild_news_channel_payload, test_permission_over assert channel_obj.permission_overwrites == { 4242: channels.PermissionOverwrite.deserialize(test_permission_overwrite_payload) } - assert channel_obj.permission_overwrites[4242]._zookeeper is mock_app + assert channel_obj.permission_overwrites[4242]._gateway_consumer is mock_app assert channel_obj.guild_id == 123 assert channel_obj.position == 0 assert channel_obj.name == "Important Announcements" @@ -328,7 +328,7 @@ def test_deserialize(self, test_guild_store_channel_payload, test_permission_ove assert channel_obj.permission_overwrites == { 4242: channels.PermissionOverwrite.deserialize(test_permission_overwrite_payload) } - assert channel_obj.permission_overwrites[4242]._zookeeper is mock_app + assert channel_obj.permission_overwrites[4242]._gateway_consumer is mock_app assert channel_obj.guild_id == 1234 assert channel_obj.position == 2 assert channel_obj.name == "Half Life 3" @@ -345,7 +345,7 @@ def test_deserialize(self, test_guild_voice_channel_payload, test_permission_ove assert channel_obj.permission_overwrites == { 4242: channels.PermissionOverwrite.deserialize(test_permission_overwrite_payload) } - assert channel_obj.permission_overwrites[4242]._zookeeper is mock_app + assert channel_obj.permission_overwrites[4242]._gateway_consumer is mock_app assert channel_obj.guild_id == 789 assert channel_obj.position == 4 assert channel_obj.name == "Secret Developer Discussions" diff --git a/tests/hikari/models/test_embeds.py b/tests/hikari/models/test_embeds.py index 154dfaf569..045682d798 100644 --- a/tests/hikari/models/test_embeds.py +++ b/tests/hikari/models/test_embeds.py @@ -47,7 +47,7 @@ def test_footer_payload(): def test_image_payload(): return { "url": "https://somewhere.com/image.png", - "proxy_url": "https://media.somewhere.com/image.png", + "_proxy_url": "https://media.somewhere.com/image.png", "height": 122, "width": 133, } @@ -57,7 +57,7 @@ def test_image_payload(): def test_thumbnail_payload(): return { "url": "https://somewhere.com/thumbnail.png", - "proxy_url": "https://media.somewhere.com/thumbnail.png", + "_proxy_url": "https://media.somewhere.com/thumbnail.png", "height": 123, "width": 456, } @@ -143,7 +143,7 @@ def test_deserialize(self, test_image_payload, mock_app): image_obj = embeds.EmbedImage.deserialize(test_image_payload, app=mock_app) assert image_obj.url == "https://somewhere.com/image.png" - assert image_obj.proxy_url == "https://media.somewhere.com/image.png" + assert image_obj._proxy_url == "https://media.somewhere.com/image.png" assert image_obj.height == 122 assert image_obj.width == 133 @@ -161,7 +161,7 @@ def test_deserialize(self, test_thumbnail_payload, mock_app): thumbnail_obj = embeds.EmbedThumbnail.deserialize(test_thumbnail_payload, app=mock_app) assert thumbnail_obj.url == "https://somewhere.com/thumbnail.png" - assert thumbnail_obj.proxy_url == "https://media.somewhere.com/thumbnail.png" + assert thumbnail_obj._proxy_url == "https://media.somewhere.com/thumbnail.png" assert thumbnail_obj.height == 123 assert thumbnail_obj.width == 456 @@ -264,19 +264,19 @@ def test_deserialize( assert embed_obj.timestamp == mock_datetime assert embed_obj.color == colors.Color(14014915) assert embed_obj.footer == embeds.EmbedFooter.deserialize(test_footer_payload) - assert embed_obj.footer._zookeeper is mock_app + assert embed_obj.footer._gateway_consumer is mock_app assert embed_obj.image == embeds.EmbedImage.deserialize(test_image_payload) - assert embed_obj.image._zookeeper is mock_app + assert embed_obj.image._gateway_consumer is mock_app assert embed_obj.thumbnail == embeds.EmbedThumbnail.deserialize(test_thumbnail_payload) - assert embed_obj.thumbnail._zookeeper is mock_app + assert embed_obj.thumbnail._gateway_consumer is mock_app assert embed_obj.video == embeds.EmbedVideo.deserialize(test_video_payload) - assert embed_obj.video._zookeeper is mock_app + assert embed_obj.video._gateway_consumer is mock_app assert embed_obj.provider == embeds.EmbedProvider.deserialize(test_provider_payload) - assert embed_obj.provider._zookeeper is mock_app + assert embed_obj.provider._gateway_consumer is mock_app assert embed_obj.author == embeds.EmbedAuthor.deserialize(test_author_payload) - assert embed_obj.author._zookeeper is mock_app + assert embed_obj.author._gateway_consumer is mock_app assert embed_obj.fields == [embeds.EmbedField.deserialize(test_field_payload)] - assert embed_obj.fields[0]._zookeeper is mock_app + assert embed_obj.fields[0]._gateway_consumer is mock_app def test_serialize_full_embed(self): embed_obj = embeds.Embed( diff --git a/tests/hikari/models/test_guilds.py b/tests/hikari/models/test_guilds.py index b8b596e760..f26ab4fd40 100644 --- a/tests/hikari/models/test_guilds.py +++ b/tests/hikari/models/test_guilds.py @@ -118,7 +118,7 @@ def test_member_payload(test_user_payload): @pytest.fixture def test_voice_state_payload(): return { - "channel_id": "432123321", + "channel": "432123321", "user_id": "6543453", "session_id": "350a109226bd6f43c81f12c7c08de20a", "deaf": False, @@ -276,7 +276,7 @@ def mock_app(): class TestGuildEmbed: @pytest.fixture() def test_guild_embed_payload(self): - return {"channel_id": "123123123", "enabled": True} + return {"channel": "123123123", "enabled": True} def test_deserialize(self, test_guild_embed_payload, mock_app): guild_embed_obj = guilds.GuildEmbed.deserialize(test_guild_embed_payload, app=mock_app) @@ -506,17 +506,17 @@ def test_deserialize( assert presence_activity_obj.timestamps == guilds.ActivityTimestamps.deserialize( test_activity_timestamps_payload ) - assert presence_activity_obj.timestamps._zookeeper is mock_app + assert presence_activity_obj.timestamps._gateway_consumer is mock_app assert presence_activity_obj.application_id == 40404040404040 assert presence_activity_obj.details == "They are doing stuff" assert presence_activity_obj.state == "STATED" assert presence_activity_obj.emoji is mock_emoji assert presence_activity_obj.party == guilds.ActivityParty.deserialize(test_activity_party_payload) - assert presence_activity_obj.party._zookeeper is mock_app + assert presence_activity_obj.party._gateway_consumer is mock_app assert presence_activity_obj.assets == guilds.ActivityAssets.deserialize(test_activity_assets_payload) - assert presence_activity_obj.assets._zookeeper is mock_app + assert presence_activity_obj.assets._gateway_consumer is mock_app assert presence_activity_obj.secrets == guilds.ActivitySecret.deserialize(test_activity_secrets_payload) - assert presence_activity_obj.secrets._zookeeper is mock_app + assert presence_activity_obj.secrets._gateway_consumer is mock_app assert presence_activity_obj.is_instance is True assert presence_activity_obj.flags == guilds.ActivityFlag.INSTANCE | guilds.ActivityFlag.JOIN @@ -548,7 +548,7 @@ def test_deserialize_partial_presence_user(self, mock_app): presence_user_obj = guilds.PresenceUser.deserialize({"id": "115590097100865541"}, app=mock_app) assert presence_user_obj.id == 115590097100865541 for attr in presence_user_obj.__slots__: - if attr not in ("id", "_zookeeper"): + if attr not in ("id", "_gateway_consumer"): assert getattr(presence_user_obj, attr) is unset.UNSET @pytest.fixture() @@ -656,16 +656,16 @@ def test_deserialize( guild_member_presence_obj = guilds.GuildMemberPresence.deserialize(test_guild_member_presence, app=mock_app) patched_since_deserializer.assert_called_once_with("2015-04-26T06:26:56.936000+00:00") assert guild_member_presence_obj.user == guilds.PresenceUser.deserialize(test_user_payload) - assert guild_member_presence_obj.user._zookeeper is mock_app + assert guild_member_presence_obj.user._gateway_consumer is mock_app assert guild_member_presence_obj.role_ids == [49494949] assert guild_member_presence_obj.guild_id == 44004040 assert guild_member_presence_obj.visible_status is guilds.PresenceStatus.DND assert guild_member_presence_obj.activities == [ guilds.PresenceActivity.deserialize(test_presence_activity_payload) ] - assert guild_member_presence_obj.activities[0]._zookeeper is mock_app + assert guild_member_presence_obj.activities[0]._gateway_consumer is mock_app assert guild_member_presence_obj.client_status == guilds.ClientStatus.deserialize(test_client_status_payload) - assert guild_member_presence_obj.client_status._zookeeper is mock_app + assert guild_member_presence_obj.client_status._gateway_consumer is mock_app assert guild_member_presence_obj.premium_since is mock_since assert guild_member_presence_obj.nick == "Nick" @@ -718,7 +718,7 @@ def test_deserialise(self, test_partial_guild_integration_payload, test_integrat assert partial_guild_integration_obj.account == guilds.IntegrationAccount.deserialize( test_integration_account_payload ) - assert partial_guild_integration_obj.account._zookeeper is mock_app + assert partial_guild_integration_obj.account._gateway_consumer is mock_app class TestGuildIntegration: @@ -967,9 +967,9 @@ def test_deserialize( channels.deserialize_channel.assert_called_once_with(test_channel_payload, app=mock_app) emojis.KnownCustomEmoji.deserialize.assert_called_once_with(test_emoji_payload, app=mock_app) assert guild_obj.members == {123456: guilds.GuildMember.deserialize(test_member_payload)} - assert guild_obj.members[123456]._zookeeper is mock_app + assert guild_obj.members[123456]._gateway_consumer is mock_app assert guild_obj.presences == {123456: guilds.GuildMemberPresence.deserialize(test_guild_member_presence)} - assert guild_obj.presences[123456]._zookeeper is mock_app + assert guild_obj.presences[123456]._gateway_consumer is mock_app assert guild_obj.splash_hash == "0ff0ff0ff" assert guild_obj.discovery_splash_hash == "famfamFAMFAMfam" assert guild_obj.owner_id == 6969696 @@ -985,7 +985,7 @@ def test_deserialize( assert guild_obj.default_message_notifications is guilds.GuildMessageNotificationsLevel.ONLY_MENTIONS assert guild_obj.explicit_content_filter is guilds.GuildExplicitContentFilterLevel.ALL_MEMBERS assert guild_obj.roles == {41771983423143936: guilds.GuildRole.deserialize(test_roles_payload)} - assert guild_obj.roles[41771983423143936]._zookeeper is mock_app + assert guild_obj.roles[41771983423143936]._gateway_consumer is mock_app assert guild_obj.emojis == {41771983429993937: mock_emoji} assert guild_obj.mfa_level is guilds.GuildMFALevel.ELEVATED assert guild_obj.application_id == 39494949 diff --git a/tests/hikari/models/test_messages.py b/tests/hikari/models/test_messages.py index 0a3a1c3f71..1bd60ae782 100644 --- a/tests/hikari/models/test_messages.py +++ b/tests/hikari/models/test_messages.py @@ -43,7 +43,7 @@ def test_attachment_payload(): "filename": "IMG.jpg", "size": 660521, "url": "https://somewhere.com/attachments/123/456/IMG.jpg", - "proxy_url": "https://media.somewhere.com/attachments/123/456/IMG.jpg", + "_proxy_url": "https://media.somewhere.com/attachments/123/456/IMG.jpg", "width": 1844, "height": 2638, } @@ -66,7 +66,7 @@ def test_message_activity_payload(): @pytest.fixture def test_message_crosspost_payload(): - return {"channel_id": "278325129692446722", "guild_id": "278325129692446720", "message_id": "306588351130107906"} + return {"channel": "278325129692446722", "guild_id": "278325129692446720", "message_id": "306588351130107906"} @pytest.fixture() @@ -108,7 +108,7 @@ def test_message_payload( ): return { "id": "123", - "channel_id": "456", + "channel": "456", "guild_id": "678", "author": test_user_payload, "member": test_member_payload, @@ -149,7 +149,7 @@ def test_deserialize(self, test_attachment_payload, mock_app): assert attachment_obj.filename == "IMG.jpg" assert attachment_obj.size == 660521 assert attachment_obj.url == "https://somewhere.com/attachments/123/456/IMG.jpg" - assert attachment_obj.proxy_url == "https://media.somewhere.com/attachments/123/456/IMG.jpg" + assert attachment_obj._proxy_url == "https://media.somewhere.com/attachments/123/456/IMG.jpg" assert attachment_obj.height == 2638 assert attachment_obj.width == 1844 @@ -274,7 +274,7 @@ def test_deserialize( message_obj = messages.Message.deserialize(test_message_payload, app=mock_app) patched_emoji_deserializer.assert_called_once_with(test_reaction_payload["emoji"], app=mock_app) assert message_obj.reactions == [messages.Reaction.deserialize(test_reaction_payload)] - assert message_obj.reactions[0]._zookeeper is mock_app + assert message_obj.reactions[0]._gateway_consumer is mock_app patched_application_deserializer.assert_called_once_with(test_application_payload, app=mock_app) patched_edited_timestamp_deserializer.assert_called_once_with("2020-04-21T21:20:16.510000+00:00") patched_timestamp_deserializer.assert_called_once_with("2020-03-21T21:20:16.510000+00:00") @@ -295,17 +295,17 @@ def test_deserialize( assert message_obj.role_mentions == {987} assert message_obj.channel_mentions == {456} assert message_obj.attachments == [messages.Attachment.deserialize(test_attachment_payload)] - assert message_obj.attachments[0]._zookeeper is mock_app + assert message_obj.attachments[0]._gateway_consumer is mock_app assert message_obj.embeds == [embeds.Embed.deserialize({})] - assert message_obj.embeds[0]._zookeeper is mock_app + assert message_obj.embeds[0]._gateway_consumer is mock_app assert message_obj.is_pinned is True assert message_obj.webhook_id == 1234 assert message_obj.type == messages.MessageType.DEFAULT assert message_obj.activity == messages.MessageActivity.deserialize(test_message_activity_payload) - assert message_obj.activity._zookeeper is mock_app + assert message_obj.activity._gateway_consumer is mock_app assert message_obj.application == mock_app assert message_obj.message_reference == messages.MessageCrosspost.deserialize(test_message_crosspost_payload) - assert message_obj.message_reference._zookeeper is mock_app + assert message_obj.message_reference._gateway_consumer is mock_app assert message_obj.flags == messages.MessageFlag.IS_CROSSPOST assert message_obj.nonce == "171000788183678976" diff --git a/tests/hikari/models/test_voices.py b/tests/hikari/models/test_voices.py index 7752137613..b41ecc3d11 100644 --- a/tests/hikari/models/test_voices.py +++ b/tests/hikari/models/test_voices.py @@ -27,7 +27,7 @@ def voice_state_payload(): return { "guild_id": "929292929292992", - "channel_id": "157733188964188161", + "channel": "157733188964188161", "user_id": "80351110224678912", "session_id": "90326bd25d71d39b9ef95b299e3872ff", "deaf": True, diff --git a/tests/hikari/models/test_webhook.py b/tests/hikari/models/test_webhook.py index 1368741ade..6d7c2c9ecd 100644 --- a/tests/hikari/models/test_webhook.py +++ b/tests/hikari/models/test_webhook.py @@ -44,7 +44,7 @@ def test_deserialize(self, mock_app): "id": "1234", "type": 1, "guild_id": "123", - "channel_id": "456", + "channel": "456", "user": test_user_payload, "name": "hikari webhook", "avatar": "bb71f469c158984e265093a81b3397fb", diff --git a/tests/hikari/rest/test_channel.py b/tests/hikari/rest/test_channel.py index be2c082ad8..890bff9315 100644 --- a/tests/hikari/rest/test_channel.py +++ b/tests/hikari/rest/test_channel.py @@ -89,7 +89,7 @@ async def test_next_chunk_makes_api_call(self, mock_session, mock_app, message_c await pag._next_chunk() mock_session.get_channel_messages.assert_awaited_once_with( - **{direction: "12345", "channel_id": "55", "limit": 100} + **{direction: "12345", "channel": "55", "limit": 100} ) @pytest.mark.parametrize("direction", ["before", "after", "around"]) @@ -807,7 +807,7 @@ async def test_unpin_message(self, rest_channel_logic_impl, channel, message): @pytest.mark.asyncio @_helpers.parametrize_valid_id_formats_for_models("channel", 115590097100865541, channels.PartialChannel) async def test_create_webhook_with_optionals(self, rest_channel_logic_impl, channel): - mock_webhook_payload = {"id": "29292929", "channel_id": "2292992"} + mock_webhook_payload = {"id": "29292929", "channel": "2292992"} mock_webhook_obj = mock.MagicMock(webhooks.Webhook) rest_channel_logic_impl._session.create_webhook.return_value = mock_webhook_payload mock_image_data = mock.MagicMock(bytes) @@ -829,7 +829,7 @@ async def test_create_webhook_with_optionals(self, rest_channel_logic_impl, chan @pytest.mark.asyncio @_helpers.parametrize_valid_id_formats_for_models("channel", 115590097100865541, channels.PartialChannel) async def test_create_webhook_without_optionals(self, rest_channel_logic_impl, channel): - mock_webhook_payload = {"id": "29292929", "channel_id": "2292992"} + mock_webhook_payload = {"id": "29292929", "channel": "2292992"} mock_webhook_obj = mock.MagicMock(webhooks.Webhook) rest_channel_logic_impl._session.create_webhook.return_value = mock_webhook_payload with mock.patch.object(webhooks.Webhook, "deserialize", return_value=mock_webhook_obj): @@ -842,7 +842,7 @@ async def test_create_webhook_without_optionals(self, rest_channel_logic_impl, c @pytest.mark.asyncio @_helpers.parametrize_valid_id_formats_for_models("channel", 115590097100865541, channels.GuildChannel) async def test_fetch_channel_webhooks(self, rest_channel_logic_impl, channel): - mock_webhook_payload = {"id": "29292929", "channel_id": "2292992"} + mock_webhook_payload = {"id": "29292929", "channel": "2292992"} mock_webhook_obj = mock.MagicMock(webhooks.Webhook) rest_channel_logic_impl._session.get_channel_webhooks.return_value = [mock_webhook_payload] with mock.patch.object(webhooks.Webhook, "deserialize", return_value=mock_webhook_obj): diff --git a/tests/hikari/rest/test_client.py b/tests/hikari/rest/test_client.py index ef57a1d732..200e71d5e5 100644 --- a/tests/hikari/rest/test_client.py +++ b/tests/hikari/rest/test_client.py @@ -21,7 +21,7 @@ import mock import pytest -from hikari import aiohttp_config +from hikari import http_settings from hikari import rest as high_level_rest from hikari import application from hikari.rest import channel @@ -47,20 +47,20 @@ class TestRESTClient: ], ) def test_init(self, token, token_type, expected_token): - mock_config = aiohttp_config.RESTConfig(token=token, token_type=token_type, trust_env=True) + mock_config = http_settings.RESTConfig(token=token, token_type=token_type, trust_env=True) mock_app = mock.MagicMock(application.Application, config=mock_config) mock_low_level_rest_clients = mock.MagicMock(low_level_rest.RESTSession) with mock.patch.object(low_level_rest, "RESTSession", return_value=mock_low_level_rest_clients) as patched_init: client = high_level_rest.RESTClient(mock_app) patched_init.assert_called_once_with( - allow_redirects=mock_config.allow_redirects, + allow_redirects=mock_config._allow_redirects, base_url=mock_config.rest_url, connector=mock_config.tcp_connector, debug=False, - proxy_headers=mock_config.proxy_headers, - proxy_auth=mock_config.proxy_auth, - ssl_context=mock_config.ssl_context, - verify_ssl=mock_config.verify_ssl, + proxy_headers=mock_config._proxy_headers, + proxy_auth=mock_config._proxy_auth, + ssl_context=mock_config._ssl_context, + verify_ssl=mock_config._verify_ssl, timeout=mock_config.request_timeout, token=expected_token, trust_env=True, diff --git a/tests/hikari/rest/test_guild.py b/tests/hikari/rest/test_guild.py index 19585417bc..b64aebfb93 100644 --- a/tests/hikari/rest/test_guild.py +++ b/tests/hikari/rest/test_guild.py @@ -1208,7 +1208,7 @@ async def test_sync_guild_integration(self, rest_guild_logic_impl, guild, integr @pytest.mark.asyncio @_helpers.parametrize_valid_id_formats_for_models("guild", 574921006817476608, guilds.Guild) async def test_fetch_guild_embed(self, rest_guild_logic_impl, guild): - mock_embed_payload = {"enabled": True, "channel_id": "2020202"} + mock_embed_payload = {"enabled": True, "channel": "2020202"} mock_embed_obj = mock.MagicMock(guilds.GuildEmbed) rest_guild_logic_impl._session.get_guild_embed.return_value = mock_embed_payload with mock.patch.object(guilds.GuildEmbed, "deserialize", return_value=mock_embed_obj): @@ -1220,7 +1220,7 @@ async def test_fetch_guild_embed(self, rest_guild_logic_impl, guild): @_helpers.parametrize_valid_id_formats_for_models("guild", 574921006817476608, guilds.Guild) @_helpers.parametrize_valid_id_formats_for_models("channel", 123123, channels.GuildChannel) async def test_update_guild_embed_with_optionnal(self, rest_guild_logic_impl, guild, channel): - mock_embed_payload = {"enabled": True, "channel_id": "2020202"} + mock_embed_payload = {"enabled": True, "channel": "2020202"} mock_embed_obj = mock.MagicMock(guilds.GuildEmbed) rest_guild_logic_impl._session.modify_guild_embed.return_value = mock_embed_payload with mock.patch.object(guilds.GuildEmbed, "deserialize", return_value=mock_embed_obj): @@ -1236,7 +1236,7 @@ async def test_update_guild_embed_with_optionnal(self, rest_guild_logic_impl, gu @pytest.mark.asyncio @_helpers.parametrize_valid_id_formats_for_models("guild", 574921006817476608, guilds.Guild) async def test_update_guild_embed_without_optionnal(self, rest_guild_logic_impl, guild): - mock_embed_payload = {"enabled": True, "channel_id": "2020202"} + mock_embed_payload = {"enabled": True, "channel": "2020202"} mock_embed_obj = mock.MagicMock(guilds.GuildEmbed) rest_guild_logic_impl._session.modify_guild_embed.return_value = mock_embed_payload with mock.patch.object(guilds.GuildEmbed, "deserialize", return_value=mock_embed_obj): @@ -1278,7 +1278,7 @@ def test_fetch_guild_widget_image_without_style(self, rest_guild_logic_impl, gui @pytest.mark.asyncio @_helpers.parametrize_valid_id_formats_for_models("channel", 115590097100865541, channels.GuildChannel) async def test_fetch_guild_webhooks(self, rest_guild_logic_impl, channel): - mock_webhook_payload = {"id": "29292929", "channel_id": "2292992"} + mock_webhook_payload = {"id": "29292929", "channel": "2292992"} mock_webhook_obj = mock.MagicMock(webhooks.Webhook) rest_guild_logic_impl._session.get_guild_webhooks.return_value = [mock_webhook_payload] with mock.patch.object(webhooks.Webhook, "deserialize", return_value=mock_webhook_obj): diff --git a/tests/hikari/rest/test_routes.py b/tests/hikari/rest/test_routes.py index a2635e51a6..82e6db72ac 100644 --- a/tests/hikari/rest/test_routes.py +++ b/tests/hikari/rest/test_routes.py @@ -25,7 +25,7 @@ class TestCompiledRoute: @pytest.fixture def template_route(self): - return routes.Route("get", "/somewhere/{channel_id}") + return routes.Route("get", "/somewhere/{channel}") @pytest.fixture def compiled_route(self, template_route): @@ -73,10 +73,10 @@ def test___hash___negative(self): class TestRoute: @pytest.fixture def route(self): - return routes.Route("post", "/somewhere/{channel_id}") + return routes.Route("post", "/somewhere/{channel}") def test__init___without_major_params_uses_default_major_params(self, route): - assert route.major_param == "channel_id" + assert route.major_param == "channel" def test_compile(self, route): expected_compiled_route = routes.CompiledRoute(route, "/somewhere/123", "123") @@ -85,12 +85,12 @@ def test_compile(self, route): assert actual_compiled_route == expected_compiled_route def test__repr__(self, route): - expected_repr = "Route(path_template='/somewhere/{channel_id}', major_param='channel_id')" + expected_repr = "Route(path_template='/somewhere/{channel}', major_param='channel')" assert route.__repr__() == expected_repr def test__str__(self, route): - assert str(route) == "/somewhere/{channel_id}" + assert str(route) == "/somewhere/{channel}" def test___eq__(self): assert routes.Route("foo", "bar") == routes.Route("foo", "bar") diff --git a/tests/hikari/rest/test_session.py b/tests/hikari/rest/test_session.py index 4b5a053f5b..d570385923 100644 --- a/tests/hikari/rest/test_session.py +++ b/tests/hikari/rest/test_session.py @@ -1244,7 +1244,7 @@ async def test_modify_guild_member_with_optionals(self, rest_impl): template.compile.assert_called_once_with(guild_id="115590097100865541", user_id="379953393319542784") rest_impl._request_json_response.assert_awaited_once_with( compiled, - body={"nick": "QT", "roles": ["222222222"], "mute": True, "deaf": True, "channel_id": "777"}, + body={"nick": "QT", "roles": ["222222222"], "mute": True, "deaf": True, "channel": "777"}, reason="I will drink your blood.", ) @@ -1551,7 +1551,7 @@ async def test_sync_guild_integration(self, rest_impl): @pytest.mark.asyncio async def test_get_guild_embed(self, rest_impl): - mock_response = {"channel_id": "4304040", "enabled": True} + mock_response = {"channel": "4304040", "enabled": True} rest_impl._request_json_response.return_value = mock_response with mock_patch_route("GET_GUILD_EMBED") as (template, compiled): assert await rest_impl.get_guild_embed("4949") is mock_response @@ -1560,18 +1560,18 @@ async def test_get_guild_embed(self, rest_impl): @pytest.mark.asyncio async def test_modify_guild_embed_without_reason(self, rest_impl): - mock_response = {"channel_id": "4444", "enabled": False} + mock_response = {"channel": "4444", "enabled": False} rest_impl._request_json_response.return_value = mock_response with mock_patch_route("PATCH_GUILD_EMBED") as (template, compiled): assert await rest_impl.modify_guild_embed("393939", channel_id="222", enabled=True) is mock_response template.compile.assert_called_once_with(guild_id="393939") rest_impl._request_json_response.assert_awaited_once_with( - compiled, body={"channel_id": "222", "enabled": True}, reason=... + compiled, body={"channel": "222", "enabled": True}, reason=... ) @pytest.mark.asyncio async def test_modify_guild_embed_with_reason(self, rest_impl): - mock_response = {"channel_id": "4444", "enabled": False} + mock_response = {"channel": "4444", "enabled": False} rest_impl._request_json_response.return_value = mock_response with mock_patch_route("PATCH_GUILD_EMBED") as (template, compiled): assert ( @@ -1580,7 +1580,7 @@ async def test_modify_guild_embed_with_reason(self, rest_impl): ) template.compile.assert_called_once_with(guild_id="393939") rest_impl._request_json_response.assert_awaited_once_with( - compiled, body={"channel_id": "222", "enabled": True}, reason="OK" + compiled, body={"channel": "222", "enabled": True}, reason="OK" ) @pytest.mark.asyncio @@ -1735,7 +1735,7 @@ async def test_list_voice_regions(self, rest_impl): @pytest.mark.asyncio async def test_create_webhook_without_optionals(self, rest_impl): - mock_response = {"channel_id": "39393993", "id": "8383838"} + mock_response = {"channel": "39393993", "id": "8383838"} rest_impl._request_json_response.return_value = mock_response with mock_patch_route("POST_CHANNEL_WEBHOOKS") as (template, compiled): assert await rest_impl.create_webhook("39393939", "I am a webhook") is mock_response @@ -1744,7 +1744,7 @@ async def test_create_webhook_without_optionals(self, rest_impl): @pytest.mark.asyncio async def test_create_webhook_with_optionals(self, rest_impl): - mock_response = {"channel_id": "39393993", "id": "8383838"} + mock_response = {"channel": "39393993", "id": "8383838"} rest_impl._request_json_response.return_value = mock_response mock_image_data = "data:image/png;base64,iVBORw0KGgpibGFo" with mock_patch_route("POST_CHANNEL_WEBHOOKS") as (template, compiled): @@ -1761,7 +1761,7 @@ async def test_create_webhook_with_optionals(self, rest_impl): @pytest.mark.asyncio async def test_get_channel_webhooks(self, rest_impl): - mock_response = [{"channel_id": "39393993", "id": "8383838"}] + mock_response = [{"channel": "39393993", "id": "8383838"}] rest_impl._request_json_response.return_value = mock_response with mock_patch_route("GET_CHANNEL_WEBHOOKS") as (template, compiled): assert await rest_impl.get_channel_webhooks("9393939") is mock_response @@ -1770,7 +1770,7 @@ async def test_get_channel_webhooks(self, rest_impl): @pytest.mark.asyncio async def test_get_guild_webhooks(self, rest_impl): - mock_response = [{"channel_id": "39393993", "id": "8383838"}] + mock_response = [{"channel": "39393993", "id": "8383838"}] rest_impl._request_json_response.return_value = mock_response with mock_patch_route("GET_GUILD_WEBHOOKS") as (template, compiled): assert await rest_impl.get_guild_webhooks("9393939") is mock_response @@ -1779,7 +1779,7 @@ async def test_get_guild_webhooks(self, rest_impl): @pytest.mark.asyncio async def test_get_webhook_without_token(self, rest_impl): - mock_response = {"channel_id": "39393993", "id": "8383838"} + mock_response = {"channel": "39393993", "id": "8383838"} rest_impl._request_json_response.return_value = mock_response with mock_patch_route("GET_WEBHOOK") as (template, compiled): assert await rest_impl.get_webhook("9393939") is mock_response @@ -1788,7 +1788,7 @@ async def test_get_webhook_without_token(self, rest_impl): @pytest.mark.asyncio async def test_get_webhook_with_token(self, rest_impl): - mock_response = {"channel_id": "39393993", "id": "8383838"} + mock_response = {"channel": "39393993", "id": "8383838"} rest_impl._request_json_response.return_value = mock_response with mock_patch_route("GET_WEBHOOK_WITH_TOKEN") as (template, compiled): assert await rest_impl.get_webhook("9393939", webhook_token="a_webhook_token") is mock_response @@ -1797,7 +1797,7 @@ async def test_get_webhook_with_token(self, rest_impl): @pytest.mark.asyncio async def test_modify_webhook_without_optionals_without_token(self, rest_impl): - mock_response = {"channel_id": "39393993", "id": "8383838"} + mock_response = {"channel": "39393993", "id": "8383838"} rest_impl._request_json_response.return_value = mock_response with mock_patch_route("PATCH_WEBHOOK") as (template, compiled): assert await rest_impl.modify_webhook("929292") is mock_response @@ -1808,7 +1808,7 @@ async def test_modify_webhook_without_optionals_without_token(self, rest_impl): @pytest.mark.asyncio async def test_modify_webhook_with_optionals_without_token(self, rest_impl): - mock_response = {"channel_id": "39393993", "id": "8383838"} + mock_response = {"channel": "39393993", "id": "8383838"} rest_impl._request_json_response.return_value = mock_response with mock_patch_route("PATCH_WEBHOOK") as (template, compiled): assert ( @@ -1820,14 +1820,14 @@ async def test_modify_webhook_with_optionals_without_token(self, rest_impl): template.compile.assert_called_once_with(webhook_id="929292") rest_impl._request_json_response.assert_awaited_once_with( compiled, - body={"name": "nyaa", "avatar": "data:image/png;base64,iVBORw0KGgpibGFo", "channel_id": "2929292929",}, + body={"name": "nyaa", "avatar": "data:image/png;base64,iVBORw0KGgpibGFo", "channel": "2929292929",}, reason="nuzzle", suppress_authorization_header=False, ) @pytest.mark.asyncio async def test_modify_webhook_without_optionals_with_token(self, rest_impl): - mock_response = {"channel_id": "39393993", "id": "8383838"} + mock_response = {"channel": "39393993", "id": "8383838"} rest_impl._request_json_response.return_value = mock_response with mock_patch_route("PATCH_WEBHOOK_WITH_TOKEN") as (template, compiled): assert await rest_impl.modify_webhook("929292", webhook_token="a_webhook_token") is mock_response diff --git a/tests/hikari/rest/test_webhook.py b/tests/hikari/rest/test_webhook.py index 779b87ae2f..1886756e74 100644 --- a/tests/hikari/rest/test_webhook.py +++ b/tests/hikari/rest/test_webhook.py @@ -47,7 +47,7 @@ def __init__(self): @pytest.mark.asyncio @_helpers.parametrize_valid_id_formats_for_models("webhook", 379953393319542784, webhooks.Webhook) async def test_fetch_webhook_with_webhook_token(self, rest_webhook_logic_impl, webhook): - mock_webhook_payload = {"id": "29292929", "channel_id": "2292992"} + mock_webhook_payload = {"id": "29292929", "channel": "2292992"} mock_webhook_obj = mock.MagicMock(webhooks.Webhook) rest_webhook_logic_impl._session.get_webhook.return_value = mock_webhook_payload with mock.patch.object(webhooks.Webhook, "deserialize", return_value=mock_webhook_obj): @@ -63,7 +63,7 @@ async def test_fetch_webhook_with_webhook_token(self, rest_webhook_logic_impl, w @pytest.mark.asyncio @_helpers.parametrize_valid_id_formats_for_models("webhook", 379953393319542784, webhooks.Webhook) async def test_fetch_webhook_without_webhook_token(self, rest_webhook_logic_impl, webhook): - mock_webhook_payload = {"id": "29292929", "channel_id": "2292992"} + mock_webhook_payload = {"id": "29292929", "channel": "2292992"} mock_webhook_obj = mock.MagicMock(webhooks.Webhook) rest_webhook_logic_impl._session.get_webhook.return_value = mock_webhook_payload with mock.patch.object(webhooks.Webhook, "deserialize", return_value=mock_webhook_obj): diff --git a/tests/hikari/stateless/test_manager.py b/tests/hikari/stateless/test_manager.py index b29c0e8c8f..2633c6c678 100644 --- a/tests/hikari/stateless/test_manager.py +++ b/tests/hikari/stateless/test_manager.py @@ -25,7 +25,7 @@ from hikari.events import message from hikari.events import other from hikari.events import voice -from hikari.gateway import client +from hikari import gateway from hikari.stateless import manager @@ -45,7 +45,7 @@ class MockDispatcher: @pytest.fixture def mock_shard(self): - return mock.MagicMock(client.GatewayClient) + return mock.MagicMock(gateway.Gateway) @pytest.mark.asyncio async def test_on_connect(self, event_manager_impl, mock_shard): diff --git a/tests/hikari/test_configs.py b/tests/hikari/test_configs.py index ec48b1f51c..2095da68da 100644 --- a/tests/hikari/test_configs.py +++ b/tests/hikari/test_configs.py @@ -23,7 +23,7 @@ import mock import pytest -from hikari import aiohttp_config +from hikari import http_settings from hikari.internal import urls from hikari.models import gateway from hikari.models import guilds @@ -33,20 +33,20 @@ @pytest.fixture def test_debug_config(): - return {"debug": True} + return {"_debug": True} @pytest.fixture def test_aiohttp_config(): return { - "allow_redirects": True, + "_allow_redirects": True, "tcp_connector": "aiohttp#TCPConnector", - "proxy_headers": {"Some-Header": "headercontent"}, - "proxy_auth": "basic Tm90aGluZyB0byBzZWUgaGVyZSA6IGpvaW4gZGlzY29yZC5nZy9IS0dQRTlRIDopIH5kYXZmc2E=", - "proxy_url": "proxy_url", + "_proxy_headers": {"Some-Header": "headercontent"}, + "_proxy_auth": "basic Tm90aGluZyB0byBzZWUgaGVyZSA6IGpvaW4gZGlzY29yZC5nZy9IS0dQRTlRIDopIH5kYXZmc2E=", + "_proxy_url": "_proxy_url", "request_timeout": 100, - "ssl_context": "ssl#SSLContext", - "verify_ssl": False, + "_ssl_context": "ssl#SSLContext", + "_verify_ssl": False, } @@ -92,52 +92,52 @@ def test_bot_config(test_rest_config, test_websocket_config): class TestDebugConfig: def test_deserialize(self, test_debug_config): - debug_config_obj = aiohttp_config.DebugConfig.deserialize(test_debug_config) + debug_config_obj = http_settings.DebugConfig.deserialize(test_debug_config) - assert debug_config_obj.debug is True + assert debug_config_obj._debug is True def test_empty_deserialize(self): - debug_config_obj = aiohttp_config.DebugConfig.deserialize({}) + debug_config_obj = http_settings.DebugConfig.deserialize({}) - assert debug_config_obj.debug is False + assert debug_config_obj._debug is False class TestAIOHTTPConfig: def test_deserialize(self, test_aiohttp_config): - aiohttp_config_obj = aiohttp_config.AIOHTTPConfig.deserialize(test_aiohttp_config) + aiohttp_config_obj = http_settings.HTTPSettings.deserialize(test_aiohttp_config) - assert aiohttp_config_obj.allow_redirects is True + assert aiohttp_config_obj._allow_redirects is True assert aiohttp_config_obj.tcp_connector == aiohttp.TCPConnector - assert aiohttp_config_obj.proxy_headers == {"Some-Header": "headercontent"} - assert aiohttp_config_obj.proxy_auth == aiohttp.BasicAuth.decode( + assert aiohttp_config_obj._proxy_headers == {"Some-Header": "headercontent"} + assert aiohttp_config_obj._proxy_auth == aiohttp.BasicAuth.decode( "basic Tm90aGluZyB0byBzZWUgaGVyZSA6IGpvaW4gZGlzY29yZC5nZy9IS0dQRTlRIDopIH5kYXZmc2E=" ) - assert aiohttp_config_obj.proxy_url == "proxy_url" + assert aiohttp_config_obj._proxy_url == "_proxy_url" assert aiohttp_config_obj.request_timeout == 100 - assert aiohttp_config_obj.ssl_context == ssl.SSLContext - assert aiohttp_config_obj.verify_ssl is False + assert aiohttp_config_obj._ssl_context == ssl.SSLContext + assert aiohttp_config_obj._verify_ssl is False def test_empty_deserialize(self): - aiohttp_config_obj = aiohttp_config.AIOHTTPConfig.deserialize({}) + aiohttp_config_obj = http_settings.HTTPSettings.deserialize({}) - assert aiohttp_config_obj.allow_redirects is False + assert aiohttp_config_obj._allow_redirects is False assert aiohttp_config_obj.tcp_connector is None - assert aiohttp_config_obj.proxy_headers is None - assert aiohttp_config_obj.proxy_auth is None - assert aiohttp_config_obj.proxy_url is None + assert aiohttp_config_obj._proxy_headers is None + assert aiohttp_config_obj._proxy_auth is None + assert aiohttp_config_obj._proxy_url is None assert aiohttp_config_obj.request_timeout is None - assert aiohttp_config_obj.ssl_context is None - assert aiohttp_config_obj.verify_ssl is True + assert aiohttp_config_obj._ssl_context is None + assert aiohttp_config_obj._verify_ssl is True class TestTokenConfig: def test_deserialize(self, test_token_config): - token_config_obj = aiohttp_config.TokenConfig.deserialize(test_token_config) + token_config_obj = http_settings.TokenConfig.deserialize(test_token_config) assert token_config_obj.token == "token" def test_empty_deserialize(self): - token_config_obj = aiohttp_config.TokenConfig.deserialize({}) + token_config_obj = http_settings.TokenConfig.deserialize({}) assert token_config_obj.token is None @@ -148,12 +148,12 @@ def test_deserialize(self, test_websocket_config): test_websocket_config["initial_idle_since"] = datetime_obj.timestamp() mock_activity = mock.MagicMock(gateway.Activity) with _helpers.patch_marshal_attr( - aiohttp_config.GatewayConfig, + http_settings.GatewayConfig, "initial_activity", deserializer=gateway.Activity.deserialize, return_value=mock_activity, ) as patched_activity_deserializer: - websocket_config_obj = aiohttp_config.GatewayConfig.deserialize(test_websocket_config) + websocket_config_obj = http_settings.GatewayConfig.deserialize(test_websocket_config) patched_activity_deserializer.assert_called_once_with({"name": "test", "url": "some_url", "type": 0}) assert websocket_config_obj.gateway_use_compression is False assert websocket_config_obj.gateway_version == 6 @@ -162,23 +162,23 @@ def test_deserialize(self, test_websocket_config): assert websocket_config_obj.initial_idle_since == datetime_obj assert websocket_config_obj.intents == intents.Intent.GUILD_MESSAGES | intents.Intent.GUILDS assert websocket_config_obj.large_threshold == 1000 - assert websocket_config_obj.debug is True - assert websocket_config_obj.allow_redirects is True + assert websocket_config_obj._debug is True + assert websocket_config_obj._allow_redirects is True assert websocket_config_obj.tcp_connector == aiohttp.TCPConnector - assert websocket_config_obj.proxy_headers == {"Some-Header": "headercontent"} - assert websocket_config_obj.proxy_auth == aiohttp.BasicAuth.decode( + assert websocket_config_obj._proxy_headers == {"Some-Header": "headercontent"} + assert websocket_config_obj._proxy_auth == aiohttp.BasicAuth.decode( "basic Tm90aGluZyB0byBzZWUgaGVyZSA6IGpvaW4gZGlzY29yZC5nZy9IS0dQRTlRIDopIH5kYXZmc2E=" ) - assert websocket_config_obj.proxy_url == "proxy_url" + assert websocket_config_obj._proxy_url == "_proxy_url" assert websocket_config_obj.request_timeout == 100 - assert websocket_config_obj.ssl_context == ssl.SSLContext - assert websocket_config_obj.verify_ssl is False + assert websocket_config_obj._ssl_context == ssl.SSLContext + assert websocket_config_obj._verify_ssl is False assert websocket_config_obj.token == "token" assert websocket_config_obj.shard_ids == [5, 6, 7, 8, 9, 10] assert websocket_config_obj.shard_count == 17 def test_empty_deserialize(self): - websocket_config_obj = aiohttp_config.GatewayConfig.deserialize({}) + websocket_config_obj = http_settings.GatewayConfig.deserialize({}) assert websocket_config_obj.gateway_use_compression is True assert websocket_config_obj.gateway_version == 6 @@ -187,15 +187,15 @@ def test_empty_deserialize(self): assert websocket_config_obj.initial_idle_since is None assert websocket_config_obj.intents is None assert websocket_config_obj.large_threshold == 250 - assert websocket_config_obj.debug is False - assert websocket_config_obj.allow_redirects is False + assert websocket_config_obj._debug is False + assert websocket_config_obj._allow_redirects is False assert websocket_config_obj.tcp_connector is None - assert websocket_config_obj.proxy_headers is None - assert websocket_config_obj.proxy_auth is None - assert websocket_config_obj.proxy_url is None + assert websocket_config_obj._proxy_headers is None + assert websocket_config_obj._proxy_auth is None + assert websocket_config_obj._proxy_url is None assert websocket_config_obj.request_timeout is None - assert websocket_config_obj.ssl_context is None - assert websocket_config_obj.verify_ssl is True + assert websocket_config_obj._ssl_context is None + assert websocket_config_obj._verify_ssl is True assert websocket_config_obj.token is None assert websocket_config_obj.shard_ids is None assert websocket_config_obj.shard_count is None @@ -203,53 +203,53 @@ def test_empty_deserialize(self): class TestParseShardInfo: def test__parse_shard_info_when_exclusive_range(self): - assert aiohttp_config._parse_shard_info("0..2") == [0, 1] + assert http_settings._parse_shard_info("0..2") == [0, 1] def test__parse_shard_info_when_inclusive_range(self): - assert aiohttp_config._parse_shard_info("0...2") == [0, 1, 2] + assert http_settings._parse_shard_info("0...2") == [0, 1, 2] def test__parse_shard_info_when_specific_id(self): - assert aiohttp_config._parse_shard_info(2) == [2] + assert http_settings._parse_shard_info(2) == [2] def test__parse_shard_info_when_list(self): - assert aiohttp_config._parse_shard_info([2, 5, 6]) == [2, 5, 6] + assert http_settings._parse_shard_info([2, 5, 6]) == [2, 5, 6] @_helpers.assert_raises(type_=ValueError) def test__parse_shard_info_when_invalid(self): - aiohttp_config._parse_shard_info("something invalid") + http_settings._parse_shard_info("something invalid") class TestRESTConfig: def test_deserialize(self, test_rest_config): - rest_config_obj = aiohttp_config.RESTConfig.deserialize(test_rest_config) + rest_config_obj = http_settings.RESTConfig.deserialize(test_rest_config) assert rest_config_obj.rest_version == 6 - assert rest_config_obj.allow_redirects is True + assert rest_config_obj._allow_redirects is True assert rest_config_obj.tcp_connector == aiohttp.TCPConnector - assert rest_config_obj.proxy_headers == {"Some-Header": "headercontent"} - assert rest_config_obj.proxy_auth == aiohttp.BasicAuth.decode( + assert rest_config_obj._proxy_headers == {"Some-Header": "headercontent"} + assert rest_config_obj._proxy_auth == aiohttp.BasicAuth.decode( "basic Tm90aGluZyB0byBzZWUgaGVyZSA6IGpvaW4gZGlzY29yZC5nZy9IS0dQRTlRIDopIH5kYXZmc2E=" ) - assert rest_config_obj.proxy_url == "proxy_url" + assert rest_config_obj._proxy_url == "_proxy_url" assert rest_config_obj.request_timeout == 100 - assert rest_config_obj.ssl_context == ssl.SSLContext - assert rest_config_obj.verify_ssl is False + assert rest_config_obj._ssl_context == ssl.SSLContext + assert rest_config_obj._verify_ssl is False assert rest_config_obj.token == "token" assert rest_config_obj.rest_url == "foobar" assert rest_config_obj.oauth2_url == "quxquxx" def test_empty_deserialize(self): - rest_config_obj = aiohttp_config.RESTConfig.deserialize({}) + rest_config_obj = http_settings.RESTConfig.deserialize({}) assert rest_config_obj.rest_version == 6 - assert rest_config_obj.allow_redirects is False + assert rest_config_obj._allow_redirects is False assert rest_config_obj.tcp_connector is None - assert rest_config_obj.proxy_headers is None - assert rest_config_obj.proxy_auth is None - assert rest_config_obj.proxy_url is None + assert rest_config_obj._proxy_headers is None + assert rest_config_obj._proxy_auth is None + assert rest_config_obj._proxy_url is None assert rest_config_obj.request_timeout is None - assert rest_config_obj.ssl_context is None - assert rest_config_obj.verify_ssl is True + assert rest_config_obj._ssl_context is None + assert rest_config_obj._verify_ssl is True assert rest_config_obj.token is None assert rest_config_obj.rest_url == urls.REST_API_URL assert rest_config_obj.oauth2_url == urls.OAUTH2_API_URL @@ -261,25 +261,25 @@ def test_deserialize(self, test_bot_config): test_bot_config["initial_idle_since"] = datetime_obj.timestamp() mock_activity = mock.MagicMock(gateway.Activity) with _helpers.patch_marshal_attr( - aiohttp_config.BotConfig, + http_settings.BotConfig, "initial_activity", deserializer=gateway.Activity.deserialize, return_value=mock_activity, ) as patched_activity_deserializer: - bot_config_obj = aiohttp_config.BotConfig.deserialize(test_bot_config) + bot_config_obj = http_settings.BotConfig.deserialize(test_bot_config) patched_activity_deserializer.assert_called_once_with({"name": "test", "url": "some_url", "type": 0}) assert bot_config_obj.rest_version == 6 - assert bot_config_obj.allow_redirects is True + assert bot_config_obj._allow_redirects is True assert bot_config_obj.tcp_connector == aiohttp.TCPConnector - assert bot_config_obj.proxy_headers == {"Some-Header": "headercontent"} - assert bot_config_obj.proxy_auth == aiohttp.BasicAuth.decode( + assert bot_config_obj._proxy_headers == {"Some-Header": "headercontent"} + assert bot_config_obj._proxy_auth == aiohttp.BasicAuth.decode( "basic Tm90aGluZyB0byBzZWUgaGVyZSA6IGpvaW4gZGlzY29yZC5nZy9IS0dQRTlRIDopIH5kYXZmc2E=" ) - assert bot_config_obj.proxy_url == "proxy_url" + assert bot_config_obj._proxy_url == "_proxy_url" assert bot_config_obj.request_timeout == 100 - assert bot_config_obj.ssl_context == ssl.SSLContext - assert bot_config_obj.verify_ssl is False + assert bot_config_obj._ssl_context == ssl.SSLContext + assert bot_config_obj._verify_ssl is False assert bot_config_obj.token == "token" assert bot_config_obj.shard_ids == [5, 6, 7, 8, 9, 10] assert bot_config_obj.shard_count == 17 @@ -290,22 +290,22 @@ def test_deserialize(self, test_bot_config): assert bot_config_obj.initial_idle_since == datetime_obj assert bot_config_obj.intents == intents.Intent.GUILD_MESSAGES | intents.Intent.GUILDS assert bot_config_obj.large_threshold == 1000 - assert bot_config_obj.debug is True + assert bot_config_obj._debug is True assert bot_config_obj.rest_url == "foobar" assert bot_config_obj.oauth2_url == "quxquxx" def test_empty_deserialize(self): - bot_config_obj = aiohttp_config.BotConfig.deserialize({}) + bot_config_obj = http_settings.BotConfig.deserialize({}) assert bot_config_obj.rest_version == 6 - assert bot_config_obj.allow_redirects is False + assert bot_config_obj._allow_redirects is False assert bot_config_obj.tcp_connector is None - assert bot_config_obj.proxy_headers is None - assert bot_config_obj.proxy_auth is None - assert bot_config_obj.proxy_url is None + assert bot_config_obj._proxy_headers is None + assert bot_config_obj._proxy_auth is None + assert bot_config_obj._proxy_url is None assert bot_config_obj.request_timeout is None - assert bot_config_obj.ssl_context is None - assert bot_config_obj.verify_ssl is True + assert bot_config_obj._ssl_context is None + assert bot_config_obj._verify_ssl is True assert bot_config_obj.token is None assert bot_config_obj.shard_ids is None assert bot_config_obj.shard_count is None @@ -316,6 +316,6 @@ def test_empty_deserialize(self): assert bot_config_obj.initial_idle_since is None assert bot_config_obj.intents is None assert bot_config_obj.large_threshold == 250 - assert bot_config_obj.debug is False + assert bot_config_obj._debug is False assert bot_config_obj.rest_url == urls.REST_API_URL assert bot_config_obj.oauth2_url == urls.OAUTH2_API_URL From d8df5396458516124630c7abed1defc40778bc53 Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Wed, 20 May 2020 11:37:02 +0100 Subject: [PATCH 364/922] Added documentation to gateway.py and fixed a bunch of reconnecting bugs. --- hikari/api/gateway_zookeeper.py | 35 +-- hikari/gateway.py | 393 ++++++++++++++++++++----------- hikari/impl/gateway_zookeeper.py | 51 +++- hikari/internal/http_client.py | 2 +- hikari/internal/more_asyncio.py | 2 +- hikari/models/gateway.py | 28 +-- hikari/models/unset.py | 2 +- 7 files changed, 302 insertions(+), 211 deletions(-) diff --git a/hikari/api/gateway_zookeeper.py b/hikari/api/gateway_zookeeper.py index e2225f4774..588ec7302e 100644 --- a/hikari/api/gateway_zookeeper.py +++ b/hikari/api/gateway_zookeeper.py @@ -21,10 +21,7 @@ __all__ = ["IGatewayZookeeper"] import abc -import asyncio -import contextlib import datetime -import signal import typing from hikari.api import base_app @@ -108,6 +105,7 @@ async def update_presence( or `False` otherwise. """ + @abc.abstractmethod def run(self) -> None: """Execute this component on an event loop. @@ -118,34 +116,3 @@ def run(self) -> None: This enables the client to be run immediately without having to set up the `asyncio` event loop manually first. """ - loop = asyncio.get_event_loop() - - def sigterm_handler(*_): - raise KeyboardInterrupt() - - ex = None - - try: - with contextlib.suppress(NotImplementedError): - # Not implemented on Windows - loop.add_signal_handler(signal.SIGTERM, sigterm_handler) - - loop.run_until_complete(self.start()) - loop.run_until_complete(self.join()) - - self.logger.info("client has shut down") - - except KeyboardInterrupt as _ex: - self.logger.info("received signal to shut down client") - loop.run_until_complete(self.close()) - # Apparently you have to alias except clauses or you get an - # UnboundLocalError. - ex = _ex - finally: - loop.run_until_complete(self.close()) - with contextlib.suppress(NotImplementedError): - # Not implemented on Windows - loop.remove_signal_handler(signal.SIGTERM) - - if ex: - raise ex from ex diff --git a/hikari/gateway.py b/hikari/gateway.py index dad9f9ed71..57f9956b30 100644 --- a/hikari/gateway.py +++ b/hikari/gateway.py @@ -16,14 +16,7 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . -"""Provides a facade around `hikari.gateway.connection.Shard`. - -This handles parsing and initializing the object from a configuration, as -well as restarting it if it disconnects. - -Additional functions and coroutines are provided to update the presence on the -shard using models defined in `hikari`. -""" +"""Single-shard implementation for the V6 and V7 gateway.""" from __future__ import annotations @@ -38,6 +31,7 @@ import zlib import aiohttp +import attr from hikari import errors from hikari import http_settings @@ -48,6 +42,7 @@ from hikari.internal import user_agents from hikari.models import bases from hikari.models import channels +from hikari.models import guilds from hikari.models import unset from hikari.internal import more_typing @@ -55,11 +50,26 @@ if typing.TYPE_CHECKING: import datetime - from hikari.models import gateway - from hikari.models import guilds from hikari.models import intents as intents_ +@attr.s(eq=True, hash=False, kw_only=True, slots=True) +class Activity: + """An activity that the bot can set for one or more shards. + + This will show the activity as the bot's presence. + """ + + name: str = attr.ib() + """The activity name.""" + + url: typing.Optional[str] = attr.ib(default=None) + """The activity URL. Only valid for `STREAMING` activities.""" + + type: guilds.ActivityType = attr.ib(converter=guilds.ActivityType) + """The activity type.""" + + @more_enums.must_be_unique class _GatewayCloseCode(int, more_enums.Enum): """Reasons for closing a gateway connection.""" @@ -80,10 +90,6 @@ class _GatewayCloseCode(int, more_enums.Enum): INVALID_INTENT = 4013 DISALLOWED_INTENT = 4014 - def __str__(self) -> str: - name = self.name.replace("_", " ").title() - return f"{self.value} {name}" - @more_enums.must_be_unique class _GatewayOpcode(int, more_enums.Enum): @@ -101,13 +107,6 @@ class _GatewayOpcode(int, more_enums.Enum): HELLO = 10 HEARTBEAT_ACK = 11 - def __str__(self) -> str: - name = self.name.replace("_", " ").title() - return f"{self.value} {name}" - - -RawDispatchT = typing.Callable[["Gateway", str, more_typing.JSONObject], more_typing.Coroutine[None]] - class _Reconnect(RuntimeError): __slots__ = () @@ -117,35 +116,74 @@ class _Zombie(RuntimeError): __slots__ = () +@attr.s(auto_attribs=True, slots=True) class _InvalidSession(RuntimeError): - __slots__ = ("can_resume",) + can_resume: bool = False - def __init__(self, can_resume: bool) -> None: - self.can_resume = can_resume + +RawDispatchT = typing.Callable[["Gateway", str, more_typing.JSONObject], more_typing.Coroutine[None]] class Gateway(http_client.HTTPClient): - """Blah blah + """Implementation of a V6 and V7 compatible gateway. + + Parameters + ---------- + config : hikari.http_settings.HTTPSettings + The aiohttp settings to use for the client session. + debug : bool + If `True`, each sent and received payload is dumped to the logs. If + `False`, only the fact that data has been sent/received will be logged. + dispatch : coroutine function with signature `(Gateway, str, dict) -> None` + The dispatch coroutine to invoke each time an event is dispatched. + This is a tri-consumer that takes this gateway object as the first + parameter, the event name as the second parameter, and the JSON + event payload as a `dict` for the third parameter. + initial_activity : Activity | None + The initial activity to appear to have for this shard. + initial_idle_since : datetime.datetime | None + The datetime to appear to be idle since. + initial_is_afk : bool | None + Whether to appear to be AFK or not on login. + initial_status : hikari.models.guilds.PresenceStatus | None + The initial status to set on login for the shard. + intents : hikari.models.intents.Intent | None + Collection of intents to use, or `None` to not use intents at all. + large_threshold : int + The number of members to have in a guild for it to be considered large. + shard_id : int + The shard ID. + shard_count : int + The shard count. + token : str + The bot token to use. + url : str + The gateway URL to use. This should not contain a query-string or + fragments. + use_compression : bool + If `True`, then transport compression is enabled. + version : int + Gateway API version to use. """ def __init__( self, *, config: http_settings.HTTPSettings, + debug: bool = False, dispatch: RawDispatchT, - debug: bool, - initial_activity: typing.Optional[gateway.Activity] = None, + initial_activity: typing.Optional[Activity] = None, initial_idle_since: typing.Optional[datetime.datetime] = None, initial_is_afk: typing.Optional[bool] = None, initial_status: typing.Optional[guilds.PresenceStatus] = None, intents: typing.Optional[intents_.Intent] = None, large_threshold: int = 250, - shard_id: int, - shard_count: int, + shard_id: int = 0, + shard_count: int = 1, token: str, url: str, use_compression: bool = True, - version: int, + version: int = 6, ) -> None: super().__init__( allow_redirects=config.allow_redirects, @@ -161,17 +199,14 @@ def __init__( trust_env=config.trust_env, ) self._activity = initial_activity - self._dead_event = asyncio.Event() self._dispatch = dispatch + self._handshake_event = asyncio.Event() self._heartbeat_task = None self._idle_since = initial_idle_since self._intents = intents self._is_afk = initial_is_afk - self._ready_event = asyncio.Event() self._request_close_event = asyncio.Event() - self._resumed_event = asyncio.Event() self._run_task = None - self._running = False self._seq = None self._session_id = None self._shard_id = shard_id @@ -203,18 +238,135 @@ def __init__( self.url = urllib.parse.urlunparse((scheme, netloc, path, params, new_query, "")) + @property + def is_alive(self) -> bool: + """Return whether the shard is alive.""" + return self._run_task is not None + + async def start(self) -> None: + """Start the gateway and wait for the handshake to complete. + + This will continue to keep the connection alive in the background once + the handshake succeeds. + + If the handshake fails, this will raise the corresponding exception + immediately. + """ + self._run_task = asyncio.create_task(self.run(), name=f"gateway shard {self._shard_id} runner") + + wait_for_handshake = asyncio.create_task( + self._handshake_event.wait(), name=f"gateway shard {self._shard_id} wait for handshake" + ) + + await more_asyncio.wait( + [wait_for_handshake, self._run_task], return_when=asyncio.FIRST_COMPLETED, + ) + + if self._run_task.cancelled(): + # Raise the corresponding exception. + self._run_task.result() + + wait_for_handshake.cancel() + async def close(self) -> None: + """Request that the shard shuts down, then wait for it to close.""" + if self.is_alive is not None: + self.logger.warning("received signal to shut shard down") + await asyncio.shield(self._close()) + else: + self.logger.debug("received signal to shut shard down, but I am not running anyway") + + async def _close(self) -> None: self._request_close_event.set() - await self._dead_event.wait() + try: + await self.join() + finally: + await super().close() + + async def join(self) -> None: + """Wait for the shard to shut down.""" + if self._run_task is not None: + await self._run_task + + async def run(self) -> None: + """Start the shard and wait for it to shut down.""" + back_off = ratelimits.ExponentialBackOff(base=1.85, maximum=600, initial_increment=2) + last_start = self._now() + do_not_back_off = True + + while True: + try: + if not do_not_back_off and self._now() - last_start < 30: + next_back_off = next(back_off) + self.logger.info( + "restarted within 30 seconds, will back off for %.2fs", next_back_off, + ) + await asyncio.sleep(next_back_off) + else: + back_off.reset() + + last_start = self._now() + do_not_back_off = False + + await self._run_once() + + raise RuntimeError("This shouldn't be reached.") + + except aiohttp.ClientConnectorError as ex: + self.logger.exception( + "failed to connect to Discord to initialize a websocket connection", exc_info=ex, + ) + + except _Zombie: + self.logger.warning("entered a zombie state and will be restarted") + + except _InvalidSession as ex: + if ex.can_resume: + self.logger.warning("invalid session, so will attempt to resume") + else: + self.logger.warning("invalid session, so will attempt to reconnect") + self._seq = None + self._session_id = None + + do_not_back_off = True + await asyncio.sleep(5) + + except _Reconnect: + self.logger.warning("instructed by Discord to reconnect") + do_not_back_off = True + await asyncio.sleep(5) + + except errors.GatewayClientDisconnectedError: + self.logger.warning("unexpected connection close, will attempt to reconnect") + + except errors.GatewayClientClosedError: + self.logger.warning("gateway client closed by user, will not attempt to restart") + return async def update_presence( self, *, idle_since: unset.MayBeUnset[typing.Optional[datetime.datetime]] = unset.UNSET, is_afk: unset.MayBeUnset[bool] = unset.UNSET, - activity: unset.MayBeUnset[typing.Optional[gateway.Activity]] = unset.UNSET, + activity: unset.MayBeUnset[typing.Optional[Activity]] = unset.UNSET, status: unset.MayBeUnset[guilds.PresenceStatus] = unset.UNSET, ) -> None: + """Update the presence of the shard user. + + Parameters + ---------- + idle_since : datetime.datetime | None | UNSET + The datetime that the user started being idle. If unset, this + will not be changed. + is_afk : bool | UNSET + If `True`, the user is marked as AFK. If `False`, the user is marked + as being active. If unset, this will not be changed. + activity : Activity | None | UNSET + The activity to appear to be playing. If unset, this will not be + changed. + status : hikari.models.guilds.PresenceStatus | UNSET + The web status to show. If unset, this will not be changed. + """ payload = self._build_presence_payload(idle_since, is_afk, activity, status) await self._send_json({"op": _GatewayOpcode.PRESENCE_UPDATE, "d": payload}) self._idle_since = idle_since if not unset.is_unset(idle_since) else self._idle_since @@ -230,6 +382,23 @@ async def update_voice_state( self_mute: bool = False, self_deaf: bool = False, ) -> None: + """Update the voice state for this shard in a given guild. + + Parameters + ---------- + guild : hikari.models.guilds.PartialGuild | hikari.models.bases.Snowflake | int | str + The guild or guild ID to update the voice state for. + channel : hikari.models.channels.GuildVoiceChannel | hikari.models.bases.Snowflake | int | str | None + The channel or channel ID to update the voice state for. If `None` + then the bot will leave the voice channel that it is in for the + given guild. + self_mute : bool + If `True`, the bot will mute itself in that voice channel. If + `False`, then it will unmute itself. + self_deaf : bool + If `True`, the bot will deafen itself in that voice channel. If + `False`, then it will undeafen itself. + """ payload = { "op": _GatewayOpcode.VOICE_STATE_UPDATE, "d": { @@ -241,87 +410,24 @@ async def update_voice_state( } await self._send_json(payload) - async def run(self): - self._run_task = asyncio.Task.current_task() - self._dead_event.clear() - - back_off = ratelimits.ExponentialBackOff(base=1.85, maximum=600, initial_increment=2) - last_start = self._now() - do_not_back_off = True - - try: - while True: - try: - if not do_not_back_off and self._now() - last_start < 30: - next_back_off = next(back_off) - self.logger.info( - "restarted within 30 seconds, will back off for %.2fs", next_back_off, - ) - await asyncio.sleep(next_back_off) - else: - back_off.reset() - - last_start = self._now() - do_not_back_off = False - - await self._run_once() - - raise RuntimeError("This shouldn't be reached.") - - except aiohttp.ClientConnectorError as ex: - self.logger.exception( - "failed to connect to Discord to initialize a websocket connection", exc_info=ex, - ) - - except _Zombie: - self.logger.warning("entered a zombie state and will be restarted") - - except _InvalidSession as ex: - if ex.can_resume: - self.logger.warning("invalid session, so will attempt to resume") - else: - self.logger.warning("invalid session, so will attempt to reconnect") - self._seq = None - self._session_id = None - - do_not_back_off = True - await asyncio.sleep(5) - - except _Reconnect: - self.logger.warning("instructed by Discord to reconnect") - do_not_back_off = True - await asyncio.sleep(5) - - except errors.GatewayClientDisconnectedError: - self.logger.warning("unexpected connection close, will attempt to reconnect") - - except errors.GatewayClientClosedError: - self.logger.warning("gateway client closed by user, will not attempt to restart") - return - finally: - self._dead_event.set() - async def _run_once(self) -> None: try: self.logger.debug("creating websocket connection to %s", self.url) self._ws = await self._create_ws(self.url) self._zlib = zlib.decompressobj() - self._ready_event.clear() - self._resumed_event.clear() + self._handshake_event.clear() self._request_close_event.clear() - self._running = True await self._handshake() # We should ideally set this after HELLO, but it should be fine # here as well. If we don't heartbeat in time, something probably # went majorly wrong anyway. - heartbeat_task = asyncio.create_task( - self._maintain_heartbeat(), name=f"gateway shard {self._shard_id} heartbeat" - ) + heartbeat_task = asyncio.create_task(self._pulse(), name=f"gateway shard {self._shard_id} heartbeat") poll_events_task = asyncio.create_task(self._poll_events(), name=f"gateway shard {self._shard_id} poll") + completed, pending = await more_asyncio.wait( [heartbeat_task, poll_events_task], return_when=asyncio.FIRST_COMPLETED ) @@ -330,46 +436,33 @@ async def _run_once(self) -> None: pending_task.cancel() with contextlib.suppress(Exception): # Clear any pending exception to prevent a nasty console message. - pending_task.result() - - ex = None - while len(completed) > 0 and ex is None: - ex = completed.pop().exception() - - # If the heartbeat call closes normally, then we want to get the exception - # raised by the identify call if it raises anything. This prevents spammy - # exceptions being thrown if the client shuts down during the handshake, - # which becomes more and more likely when we consider bots may have many - # shards running, each taking min of 5s to start up after the first. - ex = None + await pending_task - while len(completed) > 0 and ex is None: - ex = completed.pop().exception() - - if isinstance(ex, asyncio.TimeoutError): + try: + await completed.pop() + except asyncio.TimeoutError: # If we get _request_timeout errors receiving stuff, propagate as a zombied connection. This # is already done by the ping keepalive and heartbeat keepalive partially, but this # is a second edge case. raise _Zombie() - if ex is not None: - raise ex finally: asyncio.create_task( - self._dispatch(self, "DISCONNECTED", {}), name=f"shard {self._shard_id} dispatch DISCONNECTED" + self._dispatch(self, "DISCONNECTED", {}), name=f"gateway shard {self._shard_id} dispatch DISCONNECTED" ) - self._running = False async def _handshake(self) -> None: # HELLO! - message = await self._recv_json() + message = await self._receive_json_payload() op = message["op"] if message["op"] != _GatewayOpcode.HELLO: raise errors.GatewayError(f"Expected HELLO opcode 10 but received {op}") self.heartbeat_interval = message["d"]["heartbeat_interval"] / 1_000.0 - asyncio.create_task(self._dispatch(self, "CONNECTED", {}), name=f"shard {self._shard_id} dispatch CONNECTED") + asyncio.create_task( + self._dispatch(self, "CONNECTED", {}), name=f"gateway shard {self._shard_id} dispatch CONNECTED" + ) self.logger.debug("received HELLO") if self._session_id is not None: @@ -404,7 +497,7 @@ async def _handshake(self) -> None: await self._send_json(payload) - async def _maintain_heartbeat(self) -> None: + async def _pulse(self) -> None: while not self._request_close_event.is_set(): time_since_message = self._now() - self.last_message_received if self.heartbeat_interval < time_since_message: @@ -422,7 +515,7 @@ async def _maintain_heartbeat(self) -> None: async def _poll_events(self) -> None: while not self._request_close_event.is_set(): - message = await self._recv_json() + message = await self._receive_json_payload() op = message["op"] data = message["d"] @@ -433,12 +526,14 @@ async def _poll_events(self) -> None: if event == "READY": self._session_id = data["session_id"] self.logger.info("connection is ready [session:%s]", self._session_id) - self._ready_event.set() + self._handshake_event.set() elif event == "RESUME": self.logger.info("connection has resumed [session:%s, seq:%s]", self._session_id, self._seq) - self._resumed_event.set() + self._handshake_event.set() - asyncio.create_task(self._dispatch(self, event, data), name=f"shard {self._shard_id} dispatch {event}") + asyncio.create_task( + self._dispatch(self, event, data), name=f"gateway shard {self._shard_id} dispatch {event}" + ) elif op == _GatewayOpcode.HEARTBEAT: self.logger.debug("received HEARTBEAT; sending HEARTBEAT ACK") @@ -463,20 +558,23 @@ async def _poll_events(self) -> None: else: self.logger.debug("ignoring unrecognised opcode %s", op) - async def _recv_json(self) -> more_typing.JSONObject: - message = await self._recv_raw() + async def _receive_json_payload(self) -> more_typing.JSONObject: + message = await self._receive_raw() if message.type == aiohttp.WSMsgType.BINARY: - n, string = await self._recv_zlib_str(message.data) - self._log_pl_debug(string, "received %s zlib encoded packets", n) + n, string = await self._receive_zlib_message(message.data) + self._log_debug_payload(string, "received %s zlib encoded packets", n) elif message.type == aiohttp.WSMsgType.TEXT: string = message.data - self._log_pl_debug(string, "received text payload") + self._log_debug_payload(string, "received text payload") elif message.type == aiohttp.WSMsgType.CLOSE: close_code = self._ws.close_code self.logger.debug("connection closed with code %s", close_code) - reason = _GatewayCloseCode(close_code).name if close_code in _GatewayCloseCode else "unknown close code" + if close_code in _GatewayCloseCode.__members__.values(): + reason = _GatewayCloseCode(close_code).name + else: + reason = f"unknown close code {close_code}" can_reconnect = close_code in ( _GatewayCloseCode.DECODE_ERROR, @@ -501,13 +599,13 @@ async def _recv_json(self) -> more_typing.JSONObject: return json.loads(string) - async def _recv_zlib_str(self, first_packet: bytes) -> typing.Tuple[int, str]: + async def _receive_zlib_message(self, first_packet: bytes) -> typing.Tuple[int, str]: buff = bytearray(first_packet) packets = 1 while not buff.endswith(b"\x00\x00\xff\xff"): - message = await self._recv_raw() + message = await self._receive_raw() if message.type != aiohttp.WSMsgType.BINARY: raise errors.GatewayError(f"Expected a binary message but got {message.type}") buff.append(message.data) @@ -515,7 +613,7 @@ async def _recv_zlib_str(self, first_packet: bytes) -> typing.Tuple[int, str]: return packets, self._zlib.decompress(buff).decode("utf-8") - async def _recv_raw(self) -> aiohttp.WSMessage: + async def _receive_raw(self) -> aiohttp.WSMessage: packet = await self._ws.receive() self.last_message_received = self._now() return packet @@ -523,14 +621,14 @@ async def _recv_raw(self) -> aiohttp.WSMessage: async def _send_json(self, payload: more_typing.JSONObject) -> None: await self.ratelimiter.acquire() message = json.dumps(payload) - self._log_pl_debug(message, "sending json payload") + self._log_debug_payload(message, "sending json payload") await self._ws.send_str(message) @staticmethod def _now() -> float: return time.perf_counter() - def _log_pl_debug(self, payload: str, message: str, *args: typing.Any) -> None: + def _log_debug_payload(self, payload: str, message: str, *args: typing.Any) -> None: message = f"{message} [seq:%s, session:%s, size:%s]" if self._debug: message = f"{message} with raw payload: %s" @@ -545,7 +643,7 @@ def _build_presence_payload( idle_since: unset.MayBeUnset[typing.Optional[datetime.datetime]] = unset.UNSET, is_afk: unset.MayBeUnset[bool] = unset.UNSET, status: unset.MayBeUnset[guilds.PresenceStatus] = unset.UNSET, - activity: unset.MayBeUnset[typing.Optional[gateway.Activity]] = unset.UNSET, + activity: unset.MayBeUnset[typing.Optional[Activity]] = unset.UNSET, ) -> more_typing.JSONObject: if unset.is_unset(idle_since): idle_since = self._idle_since @@ -556,9 +654,20 @@ def _build_presence_payload( if unset.is_unset(activity): activity = self._activity + activity = typing.cast(typing.Optional[Activity], activity) + + if activity is None: + game = None + else: + game = { + "name": activity.name, + "url": activity.url, + "type": activity.type, + } + return { "since": idle_since.timestamp() if idle_since is not None else None, "afk": is_afk if is_afk is not None else False, "status": status.value if status is not None else guilds.PresenceStatus.ONLINE.value, - "game": activity.serialize() if activity is not None else None, + "game": game, } diff --git a/hikari/impl/gateway_zookeeper.py b/hikari/impl/gateway_zookeeper.py index 75405245e5..a9be29fc63 100644 --- a/hikari/impl/gateway_zookeeper.py +++ b/hikari/impl/gateway_zookeeper.py @@ -20,6 +20,8 @@ import abc import asyncio +import contextlib +import signal import time import typing @@ -28,6 +30,8 @@ from hikari.events import other from hikari import gateway from hikari.internal import conversions +from hikari.internal import more_asyncio +from hikari.internal import more_typing if typing.TYPE_CHECKING: import datetime @@ -80,6 +84,7 @@ def __init__( ) for shard_id in shard_ids } + self._running = False @property def gateway_shards(self) -> typing.Mapping[int, gateway.Gateway]: @@ -90,17 +95,18 @@ def shard_count(self) -> int: return self._shard_count async def start(self) -> None: + self._running = True self.logger.info("starting %s", conversions.pluralize(len(self._shards), "shard")) start_time = time.perf_counter() for i, shard_id in enumerate(self._shards): if i > 0: - self.logger.info("idling for 5 seconds to avoid an invalid session") + self.logger.info("waiting for 5 seconds until next shard can start") await asyncio.sleep(5) shard_obj = self._shards[shard_id] - await shard_obj.run() + await shard_obj.start() finish_time = time.perf_counter() @@ -112,7 +118,42 @@ async def start(self) -> None: async def join(self) -> None: await asyncio.gather(*(shard_obj.join() for shard_obj in self._shards.values())) - async def close(self) -> None: + def close(self) -> more_typing.Future[None]: + if self._running: + # This way if we cancel the stopping task, we still shut down properly. + return asyncio.shield(self._close()) + return more_asyncio.completed_future() + + def run(self): + loop = asyncio.get_event_loop() + + def sigterm_handler(*_): + raise KeyboardInterrupt() + + try: + with contextlib.suppress(NotImplementedError): + # Not implemented on Windows + loop.add_signal_handler(signal.SIGTERM, sigterm_handler) + + loop.run_until_complete(self.start()) + loop.run_until_complete(self.join()) + + self.logger.info("client has shut down") + + except KeyboardInterrupt: + self.logger.info("received signal to shut down client") + loop.run_until_complete(self.close()) + # Apparently you have to alias except clauses or you get an + # UnboundLocalError. + raise + finally: + loop.run_until_complete(self.close()) + with contextlib.suppress(NotImplementedError): + # Not implemented on Windows + loop.remove_signal_handler(signal.SIGTERM) + + async def _close(self): + self._running = False self.logger.info("stopping %s shard(s)", len(self._shards)) start_time = time.perf_counter() @@ -125,7 +166,7 @@ async def close(self) -> None: # noinspection PyUnresolvedReferences await self.event_dispatcher.dispatch(other.StoppingEvent()) - await asyncio.gather(*(shard_obj.close() for shard_obj in self._shards.values())) + await more_asyncio.wait(shard_obj.close() for shard_obj in self._shards.values()) finally: finish_time = time.perf_counter() self.logger.info("stopped %s shard(s) in approx %.2fs", len(self._shards), finish_time - start_time) @@ -146,6 +187,6 @@ async def update_presence( *( s.update_presence(status=status, activity=activity, idle_since=idle_since, is_afk=is_afk) for s in self._shards.values() - if s.connection_state in (status.ShardState.WAITING_FOR_READY, status.ShardState.READY) + if s.is_alive ) ) diff --git a/hikari/internal/http_client.py b/hikari/internal/http_client.py index 34c41a6187..0feb3b16dc 100644 --- a/hikari/internal/http_client.py +++ b/hikari/internal/http_client.py @@ -192,7 +192,7 @@ async def close(self) -> None: with contextlib.suppress(Exception): await self.__client_session.close() self.__client_session = None - self.logger.debug("closed %s", type(self).__qualname__) + self.logger.debug("closed client session") def _acquire_client_session(self) -> aiohttp.ClientSession: """Acquire a client session to make requests with. diff --git a/hikari/internal/more_asyncio.py b/hikari/internal/more_asyncio.py index e39c34d4c4..e71ae3497f 100644 --- a/hikari/internal/more_asyncio.py +++ b/hikari/internal/more_asyncio.py @@ -59,7 +59,7 @@ def completed_future(result=None, /): def wait( - aws: typing.Collection[typing.Union[more_typing.Coroutine[more_typing.T_co], typing.Awaitable[more_typing.T_co]]], + aws: typing.Iterable[typing.Union[more_typing.Coroutine[more_typing.T_co], typing.Awaitable[more_typing.T_co]]], *, timeout=None, return_when=asyncio.ALL_COMPLETED, diff --git a/hikari/models/gateway.py b/hikari/models/gateway.py index f9a9ed6cd9..cea4bcc46c 100644 --- a/hikari/models/gateway.py +++ b/hikari/models/gateway.py @@ -20,7 +20,7 @@ from __future__ import annotations -__all__ = ["Activity", "GatewayBot", "SessionStartLimit"] +__all__ = ["GatewayBot", "SessionStartLimit"] import datetime import typing @@ -73,29 +73,3 @@ class GatewayBot(bases.Entity, marshaller.Deserializable): def _undefined_type_default() -> typing.Literal[guilds.ActivityType.PLAYING]: return guilds.ActivityType.PLAYING - - -@marshaller.marshallable() -@attr.s(eq=True, hash=False, kw_only=True, slots=True) -class Activity(marshaller.Deserializable, marshaller.Serializable): - """An activity that the bot can set for one or more shards. - - This will show the activity as the bot's presence. - """ - - name: str = marshaller.attrib(deserializer=str, serializer=str, repr=True) - """The activity name.""" - - url: typing.Optional[str] = marshaller.attrib( - deserializer=str, serializer=str, if_none=None, if_undefined=None, default=None, repr=True - ) - """The activity URL. Only valid for `STREAMING` activities.""" - - type: guilds.ActivityType = marshaller.attrib( - deserializer=guilds.ActivityType, - serializer=int, - if_undefined=_undefined_type_default, - default=guilds.ActivityType.PLAYING, - repr=True, - ) - """The activity type.""" diff --git a/hikari/models/unset.py b/hikari/models/unset.py index 6b38d07de4..3bbec3f133 100644 --- a/hikari/models/unset.py +++ b/hikari/models/unset.py @@ -47,7 +47,7 @@ def __init_subclass__(cls, **kwargs: typing.Any) -> typing.NoReturn: raise TypeError("Cannot subclass Unset type") -T = typing.TypeVar("T") +T = typing.TypeVar("T", contravariant=True) MayBeUnset = typing.Union[T, Unset] UNSET: typing.Final[Unset] = Unset() From de30bae4fb53115f5dd6574d8d6116c27a4ea709 Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Wed, 20 May 2020 14:20:39 +0100 Subject: [PATCH 365/922] Fixed RECONNECT error. --- hikari/gateway.py | 19 +++++-------------- 1 file changed, 5 insertions(+), 14 deletions(-) diff --git a/hikari/gateway.py b/hikari/gateway.py index 57f9956b30..ace38adfc7 100644 --- a/hikari/gateway.py +++ b/hikari/gateway.py @@ -428,24 +428,15 @@ async def _run_once(self) -> None: poll_events_task = asyncio.create_task(self._poll_events(), name=f"gateway shard {self._shard_id} poll") - completed, pending = await more_asyncio.wait( - [heartbeat_task, poll_events_task], return_when=asyncio.FIRST_COMPLETED - ) - - for pending_task in pending: - pending_task.cancel() - with contextlib.suppress(Exception): - # Clear any pending exception to prevent a nasty console message. - await pending_task + await more_asyncio.wait([heartbeat_task, poll_events_task], return_when=asyncio.FIRST_COMPLETED) try: - await completed.pop() + if poll_events_task.done(): + raise poll_events_task.exception() + if heartbeat_task.done(): + raise heartbeat_task.exception() except asyncio.TimeoutError: - # If we get _request_timeout errors receiving stuff, propagate as a zombied connection. This - # is already done by the ping keepalive and heartbeat keepalive partially, but this - # is a second edge case. raise _Zombie() - finally: asyncio.create_task( self._dispatch(self, "DISCONNECTED", {}), name=f"gateway shard {self._shard_id} dispatch DISCONNECTED" From d5fc0a97e3d1faced65a82e3ff28069d80d5c571 Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Wed, 20 May 2020 17:12:59 +0100 Subject: [PATCH 366/922] Tidyups, bugfixes, REST. - Tidied up layout and deprecated entire REST API, started rewriting it. - Fixed a bunch of random voodoo conditions in the gateway runner that caused a massive headache to track down. - Implemented proper closure of gateway connections with reasons so Discord can verify what the bot shut down for. - Documentation and moved the gateway/rest bits to the net package again. --- hikari/{rest => .rest}/__init__.py | 5 - hikari/{rest => .rest}/base.py | 7 +- hikari/{rest => .rest}/channel.py | 4 +- hikari/{rest => .rest}/gateway.py | 2 +- hikari/{rest => .rest}/guild.py | 4 +- hikari/{rest => .rest}/invite.py | 2 +- hikari/{rest => .rest}/me.py | 4 +- hikari/{rest => .rest}/oauth2.py | 2 +- hikari/{rest => .rest}/react.py | 4 +- hikari/{rest => .rest}/session.py | 12 +- hikari/{rest => .rest}/user.py | 2 +- hikari/{rest => .rest}/voice.py | 2 +- hikari/{rest => .rest}/webhook.py | 2 +- hikari/__init__.py | 6 +- hikari/{api => }/base_app.py | 4 +- hikari/{api => }/bot.py | 6 +- hikari/{api => }/cache.py | 0 hikari/{api => }/entity_factory.py | 0 hikari/{api => }/event_consumer.py | 2 +- hikari/{api => }/event_dispatcher.py | 0 hikari/events/other.py | 2 +- hikari/{api => }/gateway_dispatcher.py | 4 +- hikari/{api => }/gateway_zookeeper.py | 6 +- hikari/impl/bot.py | 36 +-- hikari/impl/cache.py | 2 +- hikari/impl/entity_factory.py | 2 +- hikari/impl/event_manager.py | 4 +- hikari/impl/gateway_zookeeper.py | 71 ++--- hikari/impl/pagination.py | 133 +++++++++ hikari/impl/rest_app.py | 28 +- hikari/internal/more_typing.py | 6 +- hikari/models/applications.py | 2 +- hikari/models/bases.py | 3 +- hikari/models/channels.py | 2 +- hikari/models/emojis.py | 2 +- hikari/models/guilds.py | 2 +- hikari/models/invites.py | 2 +- hikari/models/unset.py | 3 +- hikari/models/users.py | 2 +- hikari/models/webhooks.py | 2 +- hikari/{api => net}/__init__.py | 15 - hikari/{rest => net}/buckets.py | 2 +- hikari/{ => net}/gateway.py | 123 ++++---- hikari/{internal => net}/http_client.py | 2 +- hikari/net/rest.py | 275 ++++++++++++++++++ hikari/{rest => net}/routes.py | 0 hikari/{internal => net}/tracing.py | 40 +-- hikari/{internal => net}/urls.py | 0 hikari/{internal => net}/user_agents.py | 2 +- hikari/{models => }/pagination.py | 2 +- hikari/rest/client.py | 106 ------- hikari/{api => }/rest_app.py | 13 +- hikari/stateless/bot.py | 6 +- hikari/stateless/manager.py | 4 +- tests/hikari/gateway/test_client.py | 8 +- tests/hikari/gateway/test_connection.py | 4 +- tests/hikari/gateway/test_dispatchers.py | 2 +- .../gateway/test_intent_aware_dispatchers.py | 2 +- tests/hikari/internal/test_http_client.py | 3 +- tests/hikari/internal/test_urls.py | 2 +- tests/hikari/internal/test_user_agents.py | 2 +- tests/hikari/models/test_applications.py | 2 +- tests/hikari/models/test_channels.py | 2 +- tests/hikari/models/test_emojis.py | 2 +- tests/hikari/models/test_guilds.py | 2 +- tests/hikari/models/test_invites.py | 2 +- tests/hikari/models/test_users.py | 4 +- tests/hikari/models/test_webhook.py | 4 +- tests/hikari/rest/test_base.py | 6 +- tests/hikari/rest/test_buckets.py | 4 +- tests/hikari/rest/test_channel.py | 4 +- tests/hikari/rest/test_client.py | 24 +- tests/hikari/rest/test_gateway.py | 4 +- tests/hikari/rest/test_guild.py | 4 +- tests/hikari/rest/test_invite.py | 4 +- tests/hikari/rest/test_me.py | 4 +- tests/hikari/rest/test_oauth2.py | 4 +- tests/hikari/rest/test_react.py | 4 +- tests/hikari/rest/test_routes.py | 2 +- tests/hikari/rest/test_session.py | 8 +- tests/hikari/rest/test_user.py | 4 +- tests/hikari/rest/test_voice.py | 4 +- tests/hikari/rest/test_webhook.py | 4 +- tests/hikari/stateless/test_manager.py | 2 +- tests/hikari/test_configs.py | 2 +- 85 files changed, 707 insertions(+), 396 deletions(-) rename hikari/{rest => .rest}/__init__.py (90%) rename hikari/{rest => .rest}/base.py (93%) rename hikari/{rest => .rest}/channel.py (99%) rename hikari/{rest => .rest}/gateway.py (98%) rename hikari/{rest => .rest}/guild.py (99%) rename hikari/{rest => .rest}/invite.py (98%) rename hikari/{rest => .rest}/me.py (99%) rename hikari/{rest => .rest}/oauth2.py (98%) rename hikari/{rest => .rest}/react.py (99%) rename hikari/{rest => .rest}/session.py (99%) rename hikari/{rest => .rest}/user.py (98%) rename hikari/{rest => .rest}/voice.py (98%) rename hikari/{rest => .rest}/webhook.py (99%) rename hikari/{api => }/base_app.py (94%) rename hikari/{api => }/bot.py (91%) rename hikari/{api => }/cache.py (100%) rename hikari/{api => }/entity_factory.py (100%) rename hikari/{api => }/event_consumer.py (97%) rename hikari/{api => }/event_dispatcher.py (100%) rename hikari/{api => }/gateway_dispatcher.py (95%) rename hikari/{api => }/gateway_zookeeper.py (97%) create mode 100644 hikari/impl/pagination.py rename hikari/{api => net}/__init__.py (67%) rename hikari/{rest => net}/buckets.py (99%) rename hikari/{ => net}/gateway.py (85%) rename hikari/{internal => net}/http_client.py (99%) create mode 100644 hikari/net/rest.py rename hikari/{rest => net}/routes.py (100%) rename hikari/{internal => net}/tracing.py (81%) rename hikari/{internal => net}/urls.py (100%) rename hikari/{internal => net}/user_agents.py (98%) rename hikari/{models => }/pagination.py (99%) delete mode 100644 hikari/rest/client.py rename hikari/{api => }/rest_app.py (83%) diff --git a/hikari/rest/__init__.py b/hikari/.rest/__init__.py similarity index 90% rename from hikari/rest/__init__.py rename to hikari/.rest/__init__.py index c1f911ae6a..ea2673ae48 100644 --- a/hikari/rest/__init__.py +++ b/hikari/.rest/__init__.py @@ -19,8 +19,3 @@ """Components used for interacting with Discord's RESTful api.""" from __future__ import annotations - -from .client import * -from .session import * - -__all__ = client.__all__ + session.__all__ diff --git a/hikari/rest/base.py b/hikari/.rest/base.py similarity index 93% rename from hikari/rest/base.py rename to hikari/.rest/base.py index a65872aaf3..7a7b44ef7e 100644 --- a/hikari/rest/base.py +++ b/hikari/.rest/base.py @@ -30,8 +30,8 @@ if typing.TYPE_CHECKING: import types - from hikari import api - from hikari.rest import session as rest_session + from hikari import rest_app + from hikari.net.rest import session as rest_session class BaseRESTComponent(abc.ABC, metaclass=meta.UniqueFunctionMeta): @@ -43,9 +43,8 @@ class BaseRESTComponent(abc.ABC, metaclass=meta.UniqueFunctionMeta): """ @abc.abstractmethod - def __init__(self, app: api.IRESTApp, session: rest_session.RESTSession) -> None: + def __init__(self, app: rest_app.IRESTApp) -> None: self._app = app - self._session = session async def __aenter__(self) -> BaseRESTComponent: return self diff --git a/hikari/rest/channel.py b/hikari/.rest/channel.py similarity index 99% rename from hikari/rest/channel.py rename to hikari/.rest/channel.py index ce9126a630..950b45cff9 100644 --- a/hikari/rest/channel.py +++ b/hikari/.rest/channel.py @@ -26,14 +26,14 @@ import datetime import typing +from hikari import pagination from hikari.internal import helpers from hikari.models import bases from hikari.models import channels as channels_ from hikari.models import invites from hikari.models import messages as messages_ -from hikari.models import pagination from hikari.models import webhooks -from hikari.rest import base +from hikari.net.rest import base if typing.TYPE_CHECKING: from hikari.models import embeds as embeds_ diff --git a/hikari/rest/gateway.py b/hikari/.rest/gateway.py similarity index 98% rename from hikari/rest/gateway.py rename to hikari/.rest/gateway.py index bb48f21e73..5f9834e399 100644 --- a/hikari/rest/gateway.py +++ b/hikari/.rest/gateway.py @@ -25,7 +25,7 @@ import abc from hikari.models import gateway -from hikari.rest import base +from hikari.net.rest import base class RESTGatewayComponent(base.BaseRESTComponent, abc.ABC): # pylint: disable=abstract-method diff --git a/hikari/rest/guild.py b/hikari/.rest/guild.py similarity index 99% rename from hikari/rest/guild.py rename to hikari/.rest/guild.py index 0ef4d72271..ed444ffe29 100644 --- a/hikari/rest/guild.py +++ b/hikari/.rest/guild.py @@ -27,16 +27,16 @@ import functools import typing +from hikari import pagination from hikari.models import audit_logs from hikari.models import bases from hikari.models import channels as channels_ from hikari.models import emojis from hikari.models import guilds from hikari.models import invites -from hikari.models import pagination from hikari.models import voices from hikari.models import webhooks -from hikari.rest import base +from hikari.net.rest import base if typing.TYPE_CHECKING: from hikari.models import colors diff --git a/hikari/rest/invite.py b/hikari/.rest/invite.py similarity index 98% rename from hikari/rest/invite.py rename to hikari/.rest/invite.py index f27e8b8064..7fa7c337f6 100644 --- a/hikari/rest/invite.py +++ b/hikari/.rest/invite.py @@ -26,7 +26,7 @@ import typing from hikari.models import invites -from hikari.rest import base +from hikari.net.rest import base class RESTInviteComponent(base.BaseRESTComponent, abc.ABC): # pylint: disable=abstract-method diff --git a/hikari/rest/me.py b/hikari/.rest/me.py similarity index 99% rename from hikari/rest/me.py rename to hikari/.rest/me.py index 75d6060c8e..f4fb9cba19 100644 --- a/hikari/rest/me.py +++ b/hikari/.rest/me.py @@ -26,13 +26,13 @@ import datetime import typing +from hikari import pagination from hikari.models import applications from hikari.models import bases from hikari.models import channels as channels_ from hikari.models import guilds -from hikari.models import pagination from hikari.models import users -from hikari.rest import base +from hikari.net.rest import base if typing.TYPE_CHECKING: from hikari.models import files diff --git a/hikari/rest/oauth2.py b/hikari/.rest/oauth2.py similarity index 98% rename from hikari/rest/oauth2.py rename to hikari/.rest/oauth2.py index ff814e5b30..c5d1d21e83 100644 --- a/hikari/rest/oauth2.py +++ b/hikari/.rest/oauth2.py @@ -25,7 +25,7 @@ import abc from hikari.models import applications -from hikari.rest import base +from hikari.net.rest import base class RESTOAuth2Component(base.BaseRESTComponent, abc.ABC): # pylint: disable=abstract-method diff --git a/hikari/rest/react.py b/hikari/.rest/react.py similarity index 99% rename from hikari/rest/react.py rename to hikari/.rest/react.py index 11490e8c78..e6201345db 100644 --- a/hikari/rest/react.py +++ b/hikari/.rest/react.py @@ -26,11 +26,11 @@ import datetime import typing +from hikari import pagination from hikari.models import bases from hikari.models import messages as messages_ -from hikari.models import pagination from hikari.models import users -from hikari.rest import base +from hikari.net.rest import base if typing.TYPE_CHECKING: from hikari.models import channels as channels_ diff --git a/hikari/rest/session.py b/hikari/.rest/session.py similarity index 99% rename from hikari/rest/session.py rename to hikari/.rest/session.py index 45be31051c..0f8d0bbd40 100644 --- a/hikari/rest/session.py +++ b/hikari/.rest/session.py @@ -37,13 +37,13 @@ from hikari import errors from hikari.internal import conversions -from hikari.internal import http_client from hikari.internal import more_collections from hikari.internal import ratelimits -from hikari.internal import urls -from hikari.internal import user_agents -from hikari.rest import buckets -from hikari.rest import routes +from hikari.net import buckets +from hikari.net import http_client +from hikari.net import routes +from hikari.net import urls +from hikari.net import user_agents if typing.TYPE_CHECKING: import ssl @@ -164,8 +164,6 @@ def __init__( # pylint: disable=too-many-locals allow_redirects: bool = False, connector: typing.Optional[aiohttp.BaseConnector] = None, debug: bool = False, - json_deserialize: typing.Callable[[typing.AnyStr], typing.Dict] = json.loads, - json_serialize: typing.Callable[[typing.Dict], typing.AnyStr] = json.dumps, proxy_auth: typing.Optional[aiohttp.BasicAuth] = None, proxy_headers: typing.Optional[aiohttp.typedefs.LooseHeaders] = None, proxy_url: typing.Optional[str] = None, diff --git a/hikari/rest/user.py b/hikari/.rest/user.py similarity index 98% rename from hikari/rest/user.py rename to hikari/.rest/user.py index a75ea972f4..18293a1f8f 100644 --- a/hikari/rest/user.py +++ b/hikari/.rest/user.py @@ -27,7 +27,7 @@ from hikari.models import bases from hikari.models import users -from hikari.rest import base +from hikari.net.rest import base class RESTUserComponent(base.BaseRESTComponent, abc.ABC): # pylint: disable=abstract-method diff --git a/hikari/rest/voice.py b/hikari/.rest/voice.py similarity index 98% rename from hikari/rest/voice.py rename to hikari/.rest/voice.py index da6a46fb48..43a9ae76ab 100644 --- a/hikari/rest/voice.py +++ b/hikari/.rest/voice.py @@ -26,7 +26,7 @@ import typing from hikari.models import voices -from hikari.rest import base +from hikari.net.rest import base class RESTVoiceComponent(base.BaseRESTComponent, abc.ABC): # pylint: disable=abstract-method diff --git a/hikari/rest/webhook.py b/hikari/.rest/webhook.py similarity index 99% rename from hikari/rest/webhook.py rename to hikari/.rest/webhook.py index 2a806e263b..ae2778ce78 100644 --- a/hikari/rest/webhook.py +++ b/hikari/.rest/webhook.py @@ -29,7 +29,7 @@ from hikari.models import bases from hikari.models import messages as messages_ from hikari.models import webhooks -from hikari.rest import base +from hikari.net.rest import base if typing.TYPE_CHECKING: from hikari.models import channels as channels_ diff --git a/hikari/__init__.py b/hikari/__init__.py index bf7df8a88a..6a945d3452 100644 --- a/hikari/__init__.py +++ b/hikari/__init__.py @@ -20,6 +20,8 @@ from __future__ import annotations +from hikari.net.gateway import * +from hikari.net.rest import * from ._about import __author__ from ._about import __ci__ from ._about import __copyright__ @@ -30,11 +32,9 @@ from ._about import __license__ from ._about import __url__ from ._about import __version__ -from .http_settings import * from .errors import * from .events import * -from .gateway import * +from .http_settings import * from .models import * -from .rest import * __all__ = [] diff --git a/hikari/api/base_app.py b/hikari/base_app.py similarity index 94% rename from hikari/api/base_app.py rename to hikari/base_app.py index 2294450d02..4f43600ea0 100644 --- a/hikari/api/base_app.py +++ b/hikari/base_app.py @@ -27,8 +27,8 @@ from concurrent import futures if typing.TYPE_CHECKING: - from hikari.api import cache as cache_ - from hikari.api import entity_factory as entity_factory_ + from hikari import cache as cache_ + from hikari import entity_factory as entity_factory_ class IBaseApp(abc.ABC): diff --git a/hikari/api/bot.py b/hikari/bot.py similarity index 91% rename from hikari/api/bot.py rename to hikari/bot.py index 41b3bb405e..547380ef8b 100644 --- a/hikari/api/bot.py +++ b/hikari/bot.py @@ -22,9 +22,9 @@ import abc -from hikari.api import gateway_dispatcher -from hikari.api import gateway_zookeeper -from hikari.api import rest_app +from hikari import gateway_dispatcher +from hikari import gateway_zookeeper +from hikari import rest_app class IBot(rest_app.IRESTApp, gateway_zookeeper.IGatewayZookeeper, gateway_dispatcher.IGatewayDispatcher, abc.ABC): diff --git a/hikari/api/cache.py b/hikari/cache.py similarity index 100% rename from hikari/api/cache.py rename to hikari/cache.py diff --git a/hikari/api/entity_factory.py b/hikari/entity_factory.py similarity index 100% rename from hikari/api/entity_factory.py rename to hikari/entity_factory.py diff --git a/hikari/api/event_consumer.py b/hikari/event_consumer.py similarity index 97% rename from hikari/api/event_consumer.py rename to hikari/event_consumer.py index 9571eef0c6..2ea729b9bc 100644 --- a/hikari/api/event_consumer.py +++ b/hikari/event_consumer.py @@ -22,8 +22,8 @@ import abc -from hikari import gateway from hikari.internal import more_typing +from hikari.net import gateway class IEventConsumer(abc.ABC): diff --git a/hikari/api/event_dispatcher.py b/hikari/event_dispatcher.py similarity index 100% rename from hikari/api/event_dispatcher.py rename to hikari/event_dispatcher.py diff --git a/hikari/events/other.py b/hikari/events/other.py index 862cd12142..29bd23346d 100644 --- a/hikari/events/other.py +++ b/hikari/events/other.py @@ -44,7 +44,7 @@ from . import base as base_events if typing.TYPE_CHECKING: - from .. import gateway as gateway_client + from ..net import gateway as gateway_client from hikari.internal import more_typing diff --git a/hikari/api/gateway_dispatcher.py b/hikari/gateway_dispatcher.py similarity index 95% rename from hikari/api/gateway_dispatcher.py rename to hikari/gateway_dispatcher.py index 59f45cdc3c..59f2a83807 100644 --- a/hikari/api/gateway_dispatcher.py +++ b/hikari/gateway_dispatcher.py @@ -23,10 +23,10 @@ import abc import typing -from hikari.api import base_app +from hikari import base_app if typing.TYPE_CHECKING: - from hikari.api import event_dispatcher + from hikari import event_dispatcher class IGatewayDispatcher(base_app.IBaseApp, abc.ABC): diff --git a/hikari/api/gateway_zookeeper.py b/hikari/gateway_zookeeper.py similarity index 97% rename from hikari/api/gateway_zookeeper.py rename to hikari/gateway_zookeeper.py index 588ec7302e..f80215f0f1 100644 --- a/hikari/api/gateway_zookeeper.py +++ b/hikari/gateway_zookeeper.py @@ -24,12 +24,12 @@ import datetime import typing -from hikari.api import base_app +from hikari import base_app from hikari.models import guilds if typing.TYPE_CHECKING: - from hikari.api import event_consumer - from hikari import gateway + from hikari import event_consumer + from hikari.net import gateway class IGatewayZookeeper(base_app.IBaseApp, abc.ABC): diff --git a/hikari/impl/bot.py b/hikari/impl/bot.py index 703720def2..8faaa9fcd8 100644 --- a/hikari/impl/bot.py +++ b/hikari/impl/bot.py @@ -19,30 +19,32 @@ from __future__ import annotations +import asyncio +import contextlib import logging import typing from concurrent import futures -from hikari.api import bot +from hikari import bot from hikari.impl import cache as cache_impl from hikari.impl import entity_factory as entity_factory_impl from hikari.impl import event_manager from hikari.impl import gateway_zookeeper from hikari.internal import helpers -from hikari.internal import urls from hikari.models import guilds -from hikari.rest import client as rest_client_ +from hikari.net import gateway +from hikari.net import rest +from hikari.net import urls if typing.TYPE_CHECKING: import datetime - from hikari.api import cache as cache_ - from hikari.api import entity_factory as entity_factory_ - from hikari.api import event_consumer as event_consumer_ + from hikari import cache as cache_ + from hikari import entity_factory as entity_factory_ + from hikari import event_consumer as event_consumer_ from hikari import http_settings - from hikari.api import event_dispatcher - from hikari.api import gateway_zookeeper - from hikari.models import gateway + from hikari import event_dispatcher + from hikari import gateway_zookeeper from hikari.models import intents as intents_ @@ -73,14 +75,8 @@ def __init__( self._event_manager = event_manager.EventManagerImpl() self._entity_factory = entity_factory_impl.EntityFactoryImpl() - self._rest = rest_client_.RESTClient( - app=self, - config=config, - debug=debug, - token=token, - token_type="Bot", - rest_url=rest_url, - version=rest_version, + self._rest = rest.REST( + app=self, config=config, debug=debug, token=token, token_type="Bot", url=rest_url, version=rest_version, ) super().__init__( @@ -122,9 +118,13 @@ def thread_pool(self) -> typing.Optional[futures.ThreadPoolExecutor]: return None @property - def rest(self) -> rest_client_.RESTClient: + def rest(self) -> rest.REST: return self._rest @property def event_consumer(self) -> event_consumer_.IEventConsumer: return self._event_manager + + async def close(self) -> None: + await super().close() + await self._rest.close() diff --git a/hikari/impl/cache.py b/hikari/impl/cache.py index cf482e7d99..286a9f843a 100644 --- a/hikari/impl/cache.py +++ b/hikari/impl/cache.py @@ -18,7 +18,7 @@ # along with Hikari. If not, see . import typing -from hikari.api import cache +from hikari import cache from hikari.internal import more_typing from hikari.models import applications from hikari.models import audit_logs diff --git a/hikari/impl/entity_factory.py b/hikari/impl/entity_factory.py index 7d662dbe9f..8a4c92dec9 100644 --- a/hikari/impl/entity_factory.py +++ b/hikari/impl/entity_factory.py @@ -19,7 +19,7 @@ from __future__ import annotations -from hikari.api import entity_factory +from hikari import entity_factory class EntityFactoryImpl(entity_factory.IEntityFactory): diff --git a/hikari/impl/event_manager.py b/hikari/impl/event_manager.py index 5e6607be9f..ba37d9da5a 100644 --- a/hikari/impl/event_manager.py +++ b/hikari/impl/event_manager.py @@ -18,8 +18,8 @@ # along with Hikari. If not, see . import typing -from hikari.api import event_consumer -from hikari.api import event_dispatcher +from hikari import event_consumer +from hikari import event_dispatcher from hikari.events import base from hikari.internal import more_asyncio from hikari.internal import more_typing diff --git a/hikari/impl/gateway_zookeeper.py b/hikari/impl/gateway_zookeeper.py index a9be29fc63..25eac925a6 100644 --- a/hikari/impl/gateway_zookeeper.py +++ b/hikari/impl/gateway_zookeeper.py @@ -25,13 +25,11 @@ import time import typing -from hikari.api import event_dispatcher -from hikari.api import gateway_zookeeper +from hikari import event_dispatcher +from hikari import gateway_zookeeper from hikari.events import other -from hikari import gateway from hikari.internal import conversions -from hikari.internal import more_asyncio -from hikari.internal import more_typing +from hikari.net import gateway if typing.TYPE_CHECKING: import datetime @@ -62,6 +60,7 @@ def __init__( version: int, ) -> None: self._aiohttp_config = config + self._request_close_event = asyncio.Event() self._url = url self._shard_count = shard_count self._shards = { @@ -96,6 +95,7 @@ def shard_count(self) -> int: async def start(self) -> None: self._running = True + self._request_close_event.clear() self.logger.info("starting %s", conversions.pluralize(len(self._shards), "shard")) start_time = time.perf_counter() @@ -103,7 +103,14 @@ async def start(self) -> None: for i, shard_id in enumerate(self._shards): if i > 0: self.logger.info("waiting for 5 seconds until next shard can start") - await asyncio.sleep(5) + + try: + await asyncio.wait_for(self._request_close_event.wait(), timeout=5) + # If this passes, the bot got shut down while sharding. + return + except asyncio.TimeoutError: + # Continue, no close occurred. + pass shard_obj = self._shards[shard_id] await shard_obj.start() @@ -116,13 +123,34 @@ async def start(self) -> None: await self.event_dispatcher.dispatch(other.StartedEvent()) async def join(self) -> None: - await asyncio.gather(*(shard_obj.join() for shard_obj in self._shards.values())) + if self._running: + await asyncio.gather(*(shard_obj.join() for shard_obj in self._shards.values())) - def close(self) -> more_typing.Future[None]: + async def close(self) -> None: if self._running: # This way if we cancel the stopping task, we still shut down properly. - return asyncio.shield(self._close()) - return more_asyncio.completed_future() + self._request_close_event.set() + self._running = False + self.logger.info("stopping %s shard(s)", len(self._shards)) + start_time = time.perf_counter() + + has_event_dispatcher = hasattr(self, "event_dispatcher") and isinstance( + self.event_dispatcher, event_dispatcher.IEventDispatcher + ) + + try: + if has_event_dispatcher: + # noinspection PyUnresolvedReferences + await self.event_dispatcher.dispatch(other.StoppingEvent()) + + await asyncio.gather(*(shard_obj.close() for shard_obj in self._shards.values())) + finally: + finish_time = time.perf_counter() + self.logger.info("stopped %s shard(s) in approx %.2fs", len(self._shards), finish_time - start_time) + + if has_event_dispatcher: + # noinspection PyUnresolvedReferences + await self.event_dispatcher.dispatch(other.StoppedEvent()) def run(self): loop = asyncio.get_event_loop() @@ -152,29 +180,6 @@ def sigterm_handler(*_): # Not implemented on Windows loop.remove_signal_handler(signal.SIGTERM) - async def _close(self): - self._running = False - self.logger.info("stopping %s shard(s)", len(self._shards)) - start_time = time.perf_counter() - - has_event_dispatcher = hasattr(self, "event_dispatcher") and isinstance( - self.event_dispatcher, event_dispatcher.IEventDispatcher - ) - - try: - if has_event_dispatcher: - # noinspection PyUnresolvedReferences - await self.event_dispatcher.dispatch(other.StoppingEvent()) - - await more_asyncio.wait(shard_obj.close() for shard_obj in self._shards.values()) - finally: - finish_time = time.perf_counter() - self.logger.info("stopped %s shard(s) in approx %.2fs", len(self._shards), finish_time - start_time) - - if has_event_dispatcher: - # noinspection PyUnresolvedReferences - await self.event_dispatcher.dispatch(other.StoppedEvent()) - async def update_presence( self, *, diff --git a/hikari/impl/pagination.py b/hikari/impl/pagination.py new file mode 100644 index 0000000000..8a6cbf8f88 --- /dev/null +++ b/hikari/impl/pagination.py @@ -0,0 +1,133 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# Copyright © Nekoka.tt 2019-2020 +# +# This file is part of Hikari. +# +# Hikari is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Hikari is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Hikari. If not, see . +from __future__ import annotations + +__all__ = [] + +import datetime + +from hikari import pagination +from hikari.models import applications +from hikari.models import bases +from hikari.models import guilds +from hikari.models import messages +from hikari.models import users + + +class GuildPaginator(pagination.BufferedPaginatedResults[guilds.Guild]): + __slots__ = ("_app", "_session", "_newest_first", "_first_id") + + def __init__(self, app, newest_first, first_item, session): + super().__init__() + self._app = app + self._newest_first = newest_first + self._first_id = self._prepare_first_id( + first_item, bases.Snowflake.max() if newest_first else bases.Snowflake.min(), + ) + self._session = session + + async def _next_chunk(self): + kwargs = {"before" if self._newest_first else "after": self._first_id} + + chunk = await self._session.get_current_user_guilds(**kwargs) + + if not chunk: + return None + + self._first_id = chunk[-1]["id"] + + return (applications.OwnGuild.deserialize(g, app=self._app) for g in chunk) + + +class MemberPaginator(pagination.BufferedPaginatedResults[guilds.GuildMember]): + __slots__ = ("_app", "_guild_id", "_first_id", "_session") + + def __init__(self, app, guild, created_after, session): + super().__init__() + self._app = app + self._guild_id = str(int(guild)) + self._first_id = self._prepare_first_id(created_after) + self._session = session + + async def _next_chunk(self): + chunk = await self._session.list_guild_members(self._guild_id, after=self._first_id) + + if not chunk: + return None + + self._first_id = chunk[-1]["id"] + + return (guilds.GuildMember.deserialize(m, app=self._app) for m in chunk) + + +class MessagePaginator(pagination.BufferedPaginatedResults[messages.Message]): + __slots__ = ("_app", "_channel_id", "_direction", "_first_id", "_session") + + def __init__(self, app, channel, direction, first, session) -> None: + super().__init__() + self._app = app + self._channel_id = str(int(channel)) + self._direction = direction + self._first_id = ( + str(bases.Snowflake.from_datetime(first)) if isinstance(first, datetime.datetime) else str(int(first)) + ) + self._session = session + + async def _next_chunk(self): + kwargs = { + self._direction: self._first_id, + "channel": self._channel_id, + "limit": 100, + } + + chunk = await self._session.get_channel_messages(**kwargs) + + if not chunk: + return None + if self._direction == "after": + chunk.reverse() + + self._first_id = chunk[-1]["id"] + + return (messages.Message.deserialize(m, app=self._app) for m in chunk) + + +class ReactionPaginator(pagination.BufferedPaginatedResults[messages.Reaction]): + __slots__ = ("_app", "_channel_id", "_message_id", "_first_id", "_emoji", "_session") + + def __init__(self, app, channel, message, emoji, users_after, session) -> None: + super().__init__() + self._app = app + self._channel_id = str(int(channel)) + self._message_id = str(int(message)) + self._emoji = getattr(emoji, "url_name", emoji) + self._first_id = self._prepare_first_id(users_after) + self._session = session + + async def _next_chunk(self): + chunk = await self._session.get_reactions( + channel_id=self._channel_id, message_id=self._message_id, emoji=self._emoji, after=self._first_id + ) + + if not chunk: + return None + + self._first_id = chunk[-1]["id"] + + return (users.User.deserialize(u, app=self._app) for u in chunk) diff --git a/hikari/impl/rest_app.py b/hikari/impl/rest_app.py index f37435c645..d244c8c0ff 100644 --- a/hikari/impl/rest_app.py +++ b/hikari/impl/rest_app.py @@ -24,16 +24,16 @@ from concurrent import futures from hikari import http_settings -from hikari.api import rest_app +from hikari import rest_app from hikari.impl import cache as cache_impl from hikari.impl import entity_factory as entity_factory_impl from hikari.internal import helpers -from hikari.internal import urls -from hikari.rest import client as rest_client_ +from hikari.net import rest as rest_ +from hikari.net import urls if typing.TYPE_CHECKING: - from hikari.api import cache as cache_ - from hikari.api import entity_factory as entity_factory_ + from hikari import cache as cache_ + from hikari import entity_factory as entity_factory_ class RESTAppImpl(rest_app.IRESTApp): @@ -47,14 +47,8 @@ def __init__( version: int = 6, ) -> None: self._logger = helpers.get_logger(self) - self._rest = rest_client_.RESTClient( - app=self, - config=config, - debug=debug, - token=token, - token_type=token_type, - rest_url=rest_url, - version=version, + self._rest = rest_.REST( + app=self, config=config, debug=debug, token=token, token_type=token_type, url=rest_url, version=version, ) self._cache = cache_impl.CacheImpl() self._entity_factory = entity_factory_impl.EntityFactoryImpl() @@ -63,16 +57,13 @@ def __init__( def logger(self) -> logging.Logger: return self._logger - async def close(self) -> None: - await self._rest.close() - @property def thread_pool(self) -> typing.Optional[futures.ThreadPoolExecutor]: # XXX: fixme return None @property - def rest(self) -> rest_client_.RESTClient: + def rest(self) -> rest_.REST: return self._rest @property @@ -82,3 +73,6 @@ def cache(self) -> cache_.ICache: @property def entity_factory(self) -> entity_factory_.IEntityFactory: return self._entity_factory + + async def close(self) -> None: + await self._rest.close() diff --git a/hikari/internal/more_typing.py b/hikari/internal/more_typing.py index 74b899acfd..d1272039ae 100644 --- a/hikari/internal/more_typing.py +++ b/hikari/internal/more_typing.py @@ -32,11 +32,14 @@ "Coroutine", "Future", "Task", + "TimeSpanT", ] # Hide any imports; this encourages any uses of this to use the typing module # for regular stuff rather than relying on it being in here as well. # pylint: disable=ungrouped-imports +import datetime as _datetime + from typing import Any as _Any from typing import AnyStr as _AnyStr from typing import Coroutine as _Coroutine @@ -199,4 +202,5 @@ def __await__(self) -> Coroutine[T_contra]: ... -# pylint:enable=unused-variable +TimeSpanT = _Union[float, int, _datetime.timedelta] +"""A measurement of time.""" diff --git a/hikari/models/applications.py b/hikari/models/applications.py index bb248ede02..36c576b028 100644 --- a/hikari/models/applications.py +++ b/hikari/models/applications.py @@ -37,11 +37,11 @@ from hikari.internal import marshaller from hikari.internal import more_enums -from hikari.internal import urls from . import bases from . import guilds from . import permissions from . import users +from ..net import urls if typing.TYPE_CHECKING: from hikari.internal import more_typing diff --git a/hikari/models/bases.py b/hikari/models/bases.py index 41823514bd..a2c458b79a 100644 --- a/hikari/models/bases.py +++ b/hikari/models/bases.py @@ -127,4 +127,5 @@ def __int__(self) -> int: return int(self.id) -T = typing.TypeVar("T", bound=Unique) +UniqueObjectT = typing.Union[Unique, Snowflake, int, str] +"""A unique object.""" diff --git a/hikari/models/channels.py b/hikari/models/channels.py index 2ea6a6e8dc..e1c23d0f9f 100644 --- a/hikari/models/channels.py +++ b/hikari/models/channels.py @@ -45,10 +45,10 @@ from hikari.internal import marshaller from hikari.internal import more_collections from hikari.internal import more_enums -from hikari.internal import urls from . import bases from . import permissions from . import users +from ..net import urls if typing.TYPE_CHECKING: from hikari.internal import more_typing diff --git a/hikari/models/emojis.py b/hikari/models/emojis.py index 632f7fafe9..dad095d6bc 100644 --- a/hikari/models/emojis.py +++ b/hikari/models/emojis.py @@ -29,10 +29,10 @@ import attr from hikari.internal import marshaller -from hikari.internal import urls from . import bases from . import files from . import users +from ..net import urls if typing.TYPE_CHECKING: from hikari.internal import more_typing diff --git a/hikari/models/guilds.py b/hikari/models/guilds.py index 45467399e7..60a8da86b3 100644 --- a/hikari/models/guilds.py +++ b/hikari/models/guilds.py @@ -62,7 +62,6 @@ from hikari.internal import conversions from hikari.internal import marshaller from hikari.internal import more_enums -from hikari.internal import urls from . import bases from . import channels as channels_ from . import colors @@ -70,6 +69,7 @@ from . import permissions as permissions_ from . import unset from . import users +from ..net import urls if typing.TYPE_CHECKING: from hikari.internal import more_typing diff --git a/hikari/models/invites.py b/hikari/models/invites.py index 0ea067b24e..906751bd9b 100644 --- a/hikari/models/invites.py +++ b/hikari/models/invites.py @@ -30,11 +30,11 @@ from hikari.internal import conversions from hikari.internal import marshaller from hikari.internal import more_enums -from hikari.internal import urls from . import bases from . import channels from . import guilds from . import users +from ..net import urls @more_enums.must_be_unique diff --git a/hikari/models/unset.py b/hikari/models/unset.py index 3bbec3f133..8b8a4c56cb 100644 --- a/hikari/models/unset.py +++ b/hikari/models/unset.py @@ -20,7 +20,7 @@ from __future__ import annotations -__all__ = ["Unset", "UNSET", "MayBeUnset"] +__all__ = ["Unset", "UNSET", "is_unset"] import typing @@ -48,7 +48,6 @@ def __init_subclass__(cls, **kwargs: typing.Any) -> typing.NoReturn: T = typing.TypeVar("T", contravariant=True) -MayBeUnset = typing.Union[T, Unset] UNSET: typing.Final[Unset] = Unset() """A global instance of `Unset`.""" diff --git a/hikari/models/users.py b/hikari/models/users.py index 1b89e724af..90fe74ec38 100644 --- a/hikari/models/users.py +++ b/hikari/models/users.py @@ -28,8 +28,8 @@ from hikari.internal import marshaller from hikari.internal import more_enums -from hikari.internal import urls from . import bases +from ..net import urls @more_enums.must_be_unique diff --git a/hikari/models/webhooks.py b/hikari/models/webhooks.py index c80329d569..ec344d87f6 100644 --- a/hikari/models/webhooks.py +++ b/hikari/models/webhooks.py @@ -28,9 +28,9 @@ from hikari.internal import marshaller from hikari.internal import more_enums -from hikari.internal import urls from . import bases from . import users as users_ +from ..net import urls if typing.TYPE_CHECKING: from . import channels as channels_ diff --git a/hikari/api/__init__.py b/hikari/net/__init__.py similarity index 67% rename from hikari/api/__init__.py rename to hikari/net/__init__.py index dfe1dfe460..1c1502a5ca 100644 --- a/hikari/api/__init__.py +++ b/hikari/net/__init__.py @@ -16,18 +16,3 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . -"""ABCs that describe the core Hikari API for writing Discord applications.""" - -from __future__ import annotations - -__all__ = [] - -from . import base_app -from . import bot -from . import cache -from . import entity_factory -from . import event_consumer -from . import event_dispatcher -from . import gateway_dispatcher -from . import gateway_zookeeper -from . import rest_app diff --git a/hikari/rest/buckets.py b/hikari/net/buckets.py similarity index 99% rename from hikari/rest/buckets.py rename to hikari/net/buckets.py index 3d57e535af..f8dbe5222e 100644 --- a/hikari/rest/buckets.py +++ b/hikari/net/buckets.py @@ -30,7 +30,7 @@ from hikari.internal import more_asyncio from hikari.internal import more_typing from hikari.internal import ratelimits -from hikari.rest import routes +from hikari.net import routes UNKNOWN_HASH: typing.Final[str] = "UNKNOWN" """The hash used for an unknown bucket that has not yet been resolved.""" diff --git a/hikari/gateway.py b/hikari/net/gateway.py similarity index 85% rename from hikari/gateway.py rename to hikari/net/gateway.py index ace38adfc7..3934ab8675 100644 --- a/hikari/gateway.py +++ b/hikari/net/gateway.py @@ -23,7 +23,6 @@ __all__ = ["Gateway"] import asyncio -import contextlib import json import time import typing @@ -35,17 +34,15 @@ from hikari import errors from hikari import http_settings -from hikari.internal import http_client -from hikari.internal import more_asyncio from hikari.internal import more_enums +from hikari.internal import more_typing from hikari.internal import ratelimits -from hikari.internal import user_agents from hikari.models import bases from hikari.models import channels from hikari.models import guilds from hikari.models import unset -from hikari.internal import more_typing - +from hikari.net import http_client +from hikari.net import user_agents if typing.TYPE_CHECKING: import datetime @@ -74,7 +71,20 @@ class Activity: class _GatewayCloseCode(int, more_enums.Enum): """Reasons for closing a gateway connection.""" - NORMAL_CLOSURE = 1000 + RFC_6455_NORMAL_CLOSURE = 1000 + RFC_6455_GOING_AWAY = 1001 + RFC_6455_PROTOCOL_ERROR = 1002 + RFC_6455_TYPE_ERROR = 1003 + RFC_6455_ENCODING_ERROR = 1007 + RFC_6455_POLICY_VIOLATION = 1008 + RFC_6455_TOO_BIG = 1009 + RFC_6455_UNEXPECTED_CONDITION = 1011 + + # Discord seems to invalidate sessions if I send a 1xxx, which is useless + # for invalid session and reconnect messages where I want to be able to + # resume. + DO_NOT_INVALIDATE_SESSION = 3000 + UNKNOWN_ERROR = 4000 UNKNOWN_OPCODE = 4001 DECODE_ERROR = 4002 @@ -241,7 +251,7 @@ def __init__( @property def is_alive(self) -> bool: """Return whether the shard is alive.""" - return self._run_task is not None + return self._run_task is not None and not self._run_task.done() async def start(self) -> None: """Start the gateway and wait for the handshake to complete. @@ -252,37 +262,29 @@ async def start(self) -> None: If the handshake fails, this will raise the corresponding exception immediately. """ - self._run_task = asyncio.create_task(self.run(), name=f"gateway shard {self._shard_id} runner") + task = asyncio.create_task(self.run(), name=f"gateway shard {self._shard_id} starter") - wait_for_handshake = asyncio.create_task( + handshake_waiter = asyncio.create_task( self._handshake_event.wait(), name=f"gateway shard {self._shard_id} wait for handshake" ) - await more_asyncio.wait( - [wait_for_handshake, self._run_task], return_when=asyncio.FIRST_COMPLETED, - ) - - if self._run_task.cancelled(): - # Raise the corresponding exception. - self._run_task.result() + await asyncio.wait([handshake_waiter, task], return_when=asyncio.FIRST_COMPLETED) - wait_for_handshake.cancel() + if not handshake_waiter.done(): + handshake_waiter.cancel() async def close(self) -> None: """Request that the shard shuts down, then wait for it to close.""" - if self.is_alive is not None: - self.logger.warning("received signal to shut shard down") - await asyncio.shield(self._close()) + if self.is_alive: + self.logger.debug("received signal to shut shard down, will proceed to close runners") + self._request_close_event.set() + try: + await self.join() + finally: + await super().close() else: self.logger.debug("received signal to shut shard down, but I am not running anyway") - async def _close(self) -> None: - self._request_close_event.set() - try: - await self.join() - finally: - await super().close() - async def join(self) -> None: """Wait for the shard to shut down.""" if self._run_task is not None: @@ -290,6 +292,12 @@ async def join(self) -> None: async def run(self) -> None: """Start the shard and wait for it to shut down.""" + self._run_task = asyncio.Task.current_task() + self._run_task.set_name(f"gateway shard {self._shard_id} runner") + + # Clear the reference when we die. + self._run_task.add_done_callback(lambda _: setattr(self, "_run_task", None)) + back_off = ratelimits.ExponentialBackOff(base=1.85, maximum=600, initial_increment=2) last_start = self._now() do_not_back_off = True @@ -309,8 +317,7 @@ async def run(self) -> None: do_not_back_off = False await self._run_once() - - raise RuntimeError("This shouldn't be reached.") + raise RuntimeError("This shouldn't be reached unless an expected condition is never met") except aiohttp.ClientConnectorError as ex: self.logger.exception( @@ -319,12 +326,15 @@ async def run(self) -> None: except _Zombie: self.logger.warning("entered a zombie state and will be restarted") + # No need to shut down, the socket is dead anyway. except _InvalidSession as ex: if ex.can_resume: - self.logger.warning("invalid session, so will attempt to resume") + self.logger.warning("invalid session, so will attempt to resume session %s", self._session_id) + await self._close_ws(_GatewayCloseCode.DO_NOT_INVALIDATE_SESSION, "invalid session (resume)") else: - self.logger.warning("invalid session, so will attempt to reconnect") + self.logger.warning("invalid session, so will attempt to reconnect with new session") + await self._close_ws(_GatewayCloseCode.RFC_6455_NORMAL_CLOSURE, "invalid session (no resume)") self._seq = None self._session_id = None @@ -332,24 +342,32 @@ async def run(self) -> None: await asyncio.sleep(5) except _Reconnect: - self.logger.warning("instructed by Discord to reconnect") + self.logger.warning("instructed by Discord to reconnect and resume session %s", self._session_id) do_not_back_off = True + await self._close_ws(_GatewayCloseCode.DO_NOT_INVALIDATE_SESSION, "reconnecting") await asyncio.sleep(5) except errors.GatewayClientDisconnectedError: self.logger.warning("unexpected connection close, will attempt to reconnect") + # No need to shut down, the socket is dead anyway. except errors.GatewayClientClosedError: self.logger.warning("gateway client closed by user, will not attempt to restart") + await self._close_ws(_GatewayCloseCode.RFC_6455_NORMAL_CLOSURE, "user shut down application") return + except Exception as ex: + self.logger.critical("unexpected exception occurred, shard will now die", exc_info=ex) + await self._close_ws(_GatewayCloseCode.RFC_6455_UNEXPECTED_CONDITION, "unexpected error occurred") + raise + async def update_presence( self, *, - idle_since: unset.MayBeUnset[typing.Optional[datetime.datetime]] = unset.UNSET, - is_afk: unset.MayBeUnset[bool] = unset.UNSET, - activity: unset.MayBeUnset[typing.Optional[Activity]] = unset.UNSET, - status: unset.MayBeUnset[guilds.PresenceStatus] = unset.UNSET, + idle_since: typing.Union[unset.Unset, typing.Optional[datetime.datetime]] = unset.UNSET, + is_afk: typing.Union[unset.Unset, bool] = unset.UNSET, + activity: typing.Union[unset.Unset, typing.Optional[Activity]] = unset.UNSET, + status: typing.Union[unset.Unset, guilds.PresenceStatus] = unset.UNSET, ) -> None: """Update the presence of the shard user. @@ -410,6 +428,10 @@ async def update_voice_state( } await self._send_json(payload) + async def _close_ws(self, code: _GatewayCloseCode, message: str): + self.logger.debug("sending close frame with code %s and message %r", code.value, message) + await self._ws.close(code=code, message=bytes(message, "utf-8")) + async def _run_once(self) -> None: try: self.logger.debug("creating websocket connection to %s", self.url) @@ -428,15 +450,18 @@ async def _run_once(self) -> None: poll_events_task = asyncio.create_task(self._poll_events(), name=f"gateway shard {self._shard_id} poll") - await more_asyncio.wait([heartbeat_task, poll_events_task], return_when=asyncio.FIRST_COMPLETED) - try: - if poll_events_task.done(): - raise poll_events_task.exception() - if heartbeat_task.done(): - raise heartbeat_task.exception() + _, pending = await asyncio.wait([heartbeat_task, poll_events_task], return_when=asyncio.FIRST_COMPLETED) + for future in pending: + future.cancel() + + await asyncio.gather(*pending, return_exceptions=True) + except asyncio.TimeoutError: raise _Zombie() + else: + raise errors.GatewayClientClosedError() + finally: asyncio.create_task( self._dispatch(self, "DISCONNECTED", {}), name=f"gateway shard {self._shard_id} dispatch DISCONNECTED" @@ -536,14 +561,10 @@ async def _poll_events(self) -> None: elif op == _GatewayOpcode.RECONNECT: self.logger.debug("RECONNECT") - - # 4000 close code allows us to resume without the session being invalided - await self._ws.close(code=4000, message=b"processing RECONNECT") raise _Reconnect() elif op == _GatewayOpcode.INVALID_SESSION: self.logger.debug("INVALID SESSION [resume:%s]", data) - await self._ws.close(code=4000, message=b"processing INVALID SESSION") raise _InvalidSession(data) else: @@ -631,10 +652,10 @@ def _log_debug_payload(self, payload: str, message: str, *args: typing.Any) -> N def _build_presence_payload( self, - idle_since: unset.MayBeUnset[typing.Optional[datetime.datetime]] = unset.UNSET, - is_afk: unset.MayBeUnset[bool] = unset.UNSET, - status: unset.MayBeUnset[guilds.PresenceStatus] = unset.UNSET, - activity: unset.MayBeUnset[typing.Optional[Activity]] = unset.UNSET, + idle_since: typing.Union[unset.Unset, typing.Optional[datetime.datetime]] = unset.UNSET, + is_afk: typing.Union[unset.Unset, bool] = unset.UNSET, + status: typing.Union[unset.Unset, guilds.PresenceStatus] = unset.UNSET, + activity: typing.Union[unset.Unset, typing.Optional[Activity]] = unset.UNSET, ) -> more_typing.JSONObject: if unset.is_unset(idle_since): idle_since = self._idle_since diff --git a/hikari/internal/http_client.py b/hikari/net/http_client.py similarity index 99% rename from hikari/internal/http_client.py rename to hikari/net/http_client.py index 0feb3b16dc..8607d7a984 100644 --- a/hikari/internal/http_client.py +++ b/hikari/net/http_client.py @@ -29,7 +29,7 @@ import aiohttp.typedefs -from hikari.internal import tracing +from hikari.net import tracing class HTTPClient(abc.ABC): # pylint:disable=too-many-instance-attributes diff --git a/hikari/net/rest.py b/hikari/net/rest.py new file mode 100644 index 0000000000..0ba6c7323c --- /dev/null +++ b/hikari/net/rest.py @@ -0,0 +1,275 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# Copyright © Nekoka.tt 2019-2020 +# +# This file is part of Hikari. +# +# Hikari is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Hikari is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Hikari. If not, see . + +from __future__ import annotations + +__all__ = ["REST"] + +import asyncio +import datetime +import http +import json +import typing + +import aiohttp + +from hikari import base_app +from hikari import errors +from hikari import http_settings +from hikari.internal import conversions +from hikari.internal import more_typing +from hikari.internal import ratelimits +from hikari.models import bases +from hikari.models import channels +from hikari.models import unset +from hikari.net import buckets +from hikari.net import http_client +from hikari.net import routes + + +class _RateLimited(RuntimeError): + __slots__ = () + + +class REST(http_client.HTTPClient): + def __init__( + self, + *, + app: base_app.IBaseApp, + config: http_settings.HTTPSettings, + debug: bool = False, + token: typing.Optional[str], + token_type: str = "Bot", + url: str, + version: int, + ) -> None: + super().__init__( + allow_redirects=config.allow_redirects, + connector=config.tcp_connector, + debug=debug, + logger_name=f"{type(self).__module__}.{type(self).__qualname__}", + proxy_auth=config.proxy_auth, + proxy_headers=config.proxy_headers, + proxy_url=config.proxy_url, + ssl_context=config.ssl_context, + verify_ssl=config.verify_ssl, + timeout=config.request_timeout, + trust_env=config.trust_env, + ) + self.buckets = buckets.RESTBucketManager() + self.global_rate_limit = ratelimits.ManualRateLimiter() + self.version = version + + self._app = app + self._token = f"{token_type.title()} {token}" if token is not None else None + + self._url = url.format(self) + + async def close(self) -> None: + """Close the REST client.""" + await super().close() + self.buckets.close() + + async def _request( + self, + compiled_route: routes.CompiledRoute, + *, + headers: more_typing.Headers = None, + query: typing.Optional[more_typing.JSONObject] = None, + body: typing.Optional[typing.Union[aiohttp.FormData, more_typing.JSONType]] = None, + reason: typing.Union[unset.Unset, str] = None, + suppress_authorization_header: bool = False, + ) -> typing.Optional[more_typing.JSONObject, more_typing.JSONArray, bytes]: + # Make a ratelimit-protected HTTP request to a JSON endpoint and expect some form + # of JSON response. If an error occurs, the response body is returned in the + # raised exception as a bytes object. This is done since the differences between + # the V6 and V7 API error messages are not documented properly, and there are + # edge cases such as Cloudflare issues where we may receive arbitrary data in + # the response instead of a JSON object. + + if not self.buckets.is_started: + self.buckets.start() + + headers = {} if headers is None else headers + + headers["x-ratelimit-precision"] = "millisecond" + headers["accept"] = self._APPLICATION_JSON + + if self._token is not None and not suppress_authorization_header: + headers["authorization"] = self._token + + if not unset.is_unset(reason): + headers["x-audit-log-reason"] = reason + + while True: + try: + # Moved to a separate method to keep branch counts down. + return await self._request_once(compiled_route, headers, body, query) + except _RateLimited: + pass + + async def _request_once(self, compiled_route, headers, body, query): + url = compiled_route.create_url(self._url) + + # Wait for any ratelimits to finish. + await asyncio.gather(self.buckets.acquire(compiled_route), self.global_rate_limit.acquire()) + + # Make the request. + response = await self._perform_request( + method=compiled_route.method, url=url, headers=headers, body=body, query=query + ) + + real_url = str(response.real_url) + + # Ensure we aren't rate limited, and update rate limiting headers where appropriate. + await self._handle_rate_limits_for_response(compiled_route, response) + + # Don't bother processing any further if we got NO CONTENT. There's not anything + # to check. + if response.status == http.HTTPStatus.NO_CONTENT: + return None + + # Decode the body. + raw_body = await response.read() + + # Handle the response. + if 200 <= response.status < 300: + if response.content_type == self._APPLICATION_JSON: + # Only deserializing here stops Cloudflare shenanigans messing us around. + return json.loads(raw_body) + raise errors.HTTPError(real_url, f"Expected JSON response but received {response.content_type}") + + if response.status == http.HTTPStatus.BAD_REQUEST: + raise errors.BadRequest(real_url, response.headers, raw_body) + if response.status == http.HTTPStatus.UNAUTHORIZED: + raise errors.Unauthorized(real_url, response.headers, raw_body) + if response.status == http.HTTPStatus.FORBIDDEN: + raise errors.Forbidden(real_url, response.headers, raw_body) + if response.status == http.HTTPStatus.NOT_FOUND: + raise errors.NotFound(real_url, response.headers, raw_body) + + # noinspection PyArgumentList + status = http.HTTPStatus(response.status) + + if 400 <= status < 500: + cls = errors.ClientHTTPErrorResponse + elif 500 <= status < 600: + cls = errors.ServerHTTPErrorResponse + else: + cls = errors.HTTPErrorResponse + + raise cls(real_url, status, response.headers, raw_body) + + async def _handle_rate_limits_for_response(self, compiled_route, response): + # Worth noting there is some bug on V6 that ratelimits me immediately if I have an invalid token. + # https://github.com/discord/discord-api-docs/issues/1569 + + # Handle ratelimiting. + resp_headers = response.headers + limit = int(resp_headers.get("x-ratelimit-limit", "1")) + remaining = int(resp_headers.get("x-ratelimit-remaining", "1")) + bucket = resp_headers.get("x-ratelimit-bucket", "None") + reset = float(resp_headers.get("x-ratelimit-reset", "0")) + reset_date = datetime.datetime.fromtimestamp(reset, tz=datetime.timezone.utc) + now_date = conversions.parse_http_date(resp_headers["date"]) + self.buckets.update_rate_limits( + compiled_route=compiled_route, + bucket_header=bucket, + remaining_header=remaining, + limit_header=limit, + date_header=now_date, + reset_at_header=reset_date, + ) + + if response.status == http.HTTPStatus.TOO_MANY_REQUESTS: + body = await response.json() if response.content_type == self._APPLICATION_JSON else await response.read() + + # We are being rate limited. + if isinstance(body, dict): + if body.get("global", False): + retry_after = float(body["retry_after"]) / 1_000 + self.global_rate_limit.throttle(retry_after) + + self.logger.warning( + "you are being rate-limited globally - trying again after %ss", retry_after, + ) + else: + self.logger.warning( + "you are being rate-limited on bucket %s for route %s - trying again after %ss", + bucket, + compiled_route, + reset, + ) + + raise _RateLimited() + + # We might find out Cloudflare causes this scenario to occur. + # I hope we don't though. + raise errors.HTTPError( + str(response.real_url), + f"We were ratelimited but did not understand the response. Perhaps Cloudflare did this? {body!r}", + ) + + async def fetch_channel( + self, channel: typing.Union[channels.PartialChannel, bases.Snowflake, int], + ) -> channels.PartialChannel: + response = await self._request(routes.GET_CHANNEL.compile(channel_id=str(int(channel)))) + raise NotImplementedError + + async def update_channel( + self, + channel: typing.Union[channels.PartialChannel, bases.Snowflake, int], + *, + name: typing.Union[unset.Unset, str] = unset.UNSET, + position: typing.Union[unset.Unset, int] = unset.UNSET, + topic: typing.Union[unset.Unset, str] = unset.UNSET, + nsfw: typing.Union[unset.Unset, bool] = unset.UNSET, + bitrate: typing.Union[unset.Unset, int] = unset.UNSET, + user_limit: typing.Union[unset.Unset, int] = unset.UNSET, + rate_limit_per_user: typing.Union[unset.Unset, more_typing.TimeSpanT] = unset.UNSET, + permission_overwrites: typing.Union[unset.Unset, typing.Sequence[channels.PermissionOverwrite]] = unset.UNSET, + parent_category: typing.Union[unset.Unset, channels.GuildCategory] = unset.UNSET, + reason: typing.Union[unset.Unset, str] = unset.UNSET, + ) -> channels.PartialChannel: + payload = {} + if not unset.is_unset(name): + payload["name"] = name + if not unset.is_unset(position): + payload["position"] = position + if not unset.is_unset(topic): + payload["topic"] = topic + if not unset.is_unset(nsfw): + payload["nsfw"] = nsfw + if not unset.is_unset(bitrate): + payload["bitrate"] = bitrate + if not unset.is_unset(user_limit): + payload["user_limit"] = user_limit + if not unset.is_unset(rate_limit_per_user): + payload["rate_limit_per_user"] = rate_limit_per_user + if not unset.is_unset("permission_overwrites"): + # TODO: json serialize + payload["permission_overwrites"] = permission_overwrites + if not unset.is_unset(parent_category): + payload["parent_id"] = str(int(parent_category)) + + response = await self._request( + routes.PATCH_CHANNEL.compile(channel_id=str(int(channel))), body=payload, reason=reason, + ) + + raise NotImplementedError diff --git a/hikari/rest/routes.py b/hikari/net/routes.py similarity index 100% rename from hikari/rest/routes.py rename to hikari/net/routes.py diff --git a/hikari/internal/tracing.py b/hikari/net/tracing.py similarity index 81% rename from hikari/internal/tracing.py rename to hikari/net/tracing.py index 13693a8b72..c2faeb8afa 100644 --- a/hikari/internal/tracing.py +++ b/hikari/net/tracing.py @@ -56,16 +56,16 @@ class CFRayTracer(BaseTracer): async def on_request_start(self, _, ctx, params): """Log an outbound request.""" - ctx.identifier = f"uuid4:{uuid.uuid4()}" + ctx.identifier = f"request_id:{uuid.uuid4()}" ctx.start_time = time.perf_counter() self.logger.debug( - "[%s] %s %s [content-type:%s, accept:%s]", - ctx.identifier, + "%s %s [content-type:%s, accept:%s] [%s]", params.method, params.url, params.headers.get("content-type"), params.headers.get("accept"), + ctx.identifier, ) async def on_request_end(self, _, ctx, params): @@ -73,8 +73,7 @@ async def on_request_end(self, _, ctx, params): latency = round((time.perf_counter() - ctx.start_time) * 1_000, 1) response = params.response self.logger.debug( - "[%s] %s %s after %sms [content-type:%s, size:%s, cf-ray:%s, cf-request-id:%s]", - ctx.identifier, + "%s %s after %sms [content-type:%s, size:%s, cf-ray:%s, cf-request-id:%s] [%s]", response.status, response.reason, latency, @@ -82,6 +81,7 @@ async def on_request_end(self, _, ctx, params): response.headers.get("content-length", 0), response.headers.get("cf-ray"), response.headers.get("cf-request-id"), + ctx.identifier, ) @@ -102,15 +102,15 @@ class DebugTracer(BaseTracer): async def on_request_start(self, _, ctx, params): """Log an outbound request.""" - ctx.identifier = f"uuid4:{uuid.uuid4()}" + ctx.identifier = f"request_id:{uuid.uuid4()}" ctx.start_time = time.perf_counter() self.logger.debug( - "[%s] %s %s\n request headers: %s\n request body: %s", - ctx.identifier, + "%s %s [%s]\n request headers: %s\n request body: %s", params.method, params.url, - params.headers, + ctx.identifier, + dict(params.headers), getattr(ctx.trace_request_ctx, "request_body", ""), ) @@ -119,39 +119,39 @@ async def on_request_end(self, _, ctx, params): latency = round((time.perf_counter() - ctx.start_time) * 1_000, 2) response = params.response self.logger.debug( - "[%s] %s %s %s after %sms\n response headers: %s\n response body: %s", - ctx.identifier, + "%s %s %s after %sms [%s]\n response headers: %s\n response body: %s", response.real_url, response.status, response.reason, latency, - response.raw_headers, - await response.read(), + ctx.identifier, + dict(response.headers), + await response.read() if "content-type" in response.headers else "", ) async def on_request_exception(self, _, ctx, params): """Log an error while making a request.""" - self.logger.debug("[%s] encountered exception", ctx.identifier, exc_info=params.exception) + self.logger.debug("encountered exception [%s]", ctx.identifier, exc_info=params.exception) async def on_connection_queued_start(self, _, ctx, __): """Log when we have to wait for a new connection in the pool.""" - self.logger.debug("[%s] is waiting for a connection", ctx.identifier) + self.logger.debug("is waiting for a connection [%s]", ctx.identifier) async def on_connection_reuseconn(self, _, ctx, __): """Log when we re-use an existing connection in the pool.""" - self.logger.debug("[%s] has acquired an existing connection", ctx.identifier) + self.logger.debug("has acquired an existing connection [%s]", ctx.identifier) async def on_connection_create_end(self, _, ctx, __): """Log when we create a new connection in the pool.""" - self.logger.debug("[%s] has created a new connection", ctx.identifier) + self.logger.debug("has created a new connection [%s]", ctx.identifier) async def on_dns_cache_hit(self, _, ctx, params): """Log when we reuse the DNS cache and do not have to look up an IP.""" - self.logger.debug("[%s] has retrieved the IP of %s from the DNS cache", ctx.identifier, params.host) + self.logger.debug("has retrieved the IP of %s from the DNS cache [%s]", params.host, ctx.identifier) async def on_dns_cache_miss(self, _, ctx, params): """Log when we have to query a DNS server for an IP address.""" - self.logger.debug("[%s] will perform DNS lookup of new host %s", ctx.identifier, params.host) + self.logger.debug("will perform DNS lookup of new host %s [%s]", params.host, ctx.identifier) # noinspection PyMethodMayBeStatic async def on_dns_resolvehost_start(self, _, ctx, __): @@ -161,4 +161,4 @@ async def on_dns_resolvehost_start(self, _, ctx, __): async def on_dns_resolvehost_end(self, _, ctx, params): """Log how long a DNS lookup of an IP took to perform.""" latency = round((time.perf_counter() - ctx.dns_start_time) * 1_000, 2) - self.logger.debug("[%s] DNS lookup of host %s took %sms", ctx.identifier, params.host, latency) + self.logger.debug("DNS lookup of host %s took %sms [%s]", params.host, latency, ctx.identifier) diff --git a/hikari/internal/urls.py b/hikari/net/urls.py similarity index 100% rename from hikari/internal/urls.py rename to hikari/net/urls.py diff --git a/hikari/internal/user_agents.py b/hikari/net/user_agents.py similarity index 98% rename from hikari/internal/user_agents.py rename to hikari/net/user_agents.py index 8a7d6ebe84..57d0131833 100644 --- a/hikari/internal/user_agents.py +++ b/hikari/net/user_agents.py @@ -31,7 +31,7 @@ import typing -from . import meta +from hikari.internal import meta class UserAgent(metaclass=meta.SingletonMeta): diff --git a/hikari/models/pagination.py b/hikari/pagination.py similarity index 99% rename from hikari/models/pagination.py rename to hikari/pagination.py index 46b5ca6607..bc4ed7d393 100644 --- a/hikari/models/pagination.py +++ b/hikari/pagination.py @@ -24,7 +24,7 @@ import typing from hikari.internal import more_collections -from . import bases +from hikari.models import bases _T = typing.TypeVar("_T") diff --git a/hikari/rest/client.py b/hikari/rest/client.py deleted file mode 100644 index 77388683ce..0000000000 --- a/hikari/rest/client.py +++ /dev/null @@ -1,106 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# Copyright © Nekoka.tt 2019-2020 -# -# This file is part of Hikari. -# -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . -"""Marshall wrappings for the RESTSession implementation in `hikari.rest.session`. - -This provides an object-oriented interface for interacting with discord's RESTSession -API. -""" - -from __future__ import annotations - -__all__ = ["RESTClient"] - -import typing - -from hikari import http_settings -from hikari.api import rest_app -from hikari.rest import channel -from hikari.rest import gateway -from hikari.rest import guild -from hikari.rest import invite -from hikari.rest import me -from hikari.rest import oauth2 -from hikari.rest import react -from hikari.rest import session -from hikari.rest import user -from hikari.rest import voice -from hikari.rest import webhook - - -class RESTClient( - channel.RESTChannelComponent, - me.RESTCurrentUserComponent, - gateway.RESTGatewayComponent, - guild.RESTGuildComponent, - invite.RESTInviteComponent, - oauth2.RESTOAuth2Component, - react.RESTReactionComponent, - user.RESTUserComponent, - voice.RESTVoiceComponent, - webhook.RESTWebhookComponent, -): - """ - A marshalling object-oriented RESTSession API client. - - This client bridges the basic RESTSession API exposed by - `hikari.rest.session.RESTSession` and wraps it in a unit of processing that can handle - handle parsing API objects into Hikari entity objects. - - Parameters - ---------- - app : hikari.components.application.Application - The client application that this rest client should be bound by. - Includes the rest config. - - !!! note - For all endpoints where a `reason` argument is provided, this may be a - string inclusively between `0` and `512` characters length, with any - additional characters being cut off. - """ - - def __init__( - self, - *, - app: rest_app.IRESTApp, - config: http_settings.HTTPSettings, - debug: bool, - token: typing.Optional[str], - token_type: typing.Optional[str], - rest_url, - version, - ) -> None: - if token_type is not None: - token = f"{token_type} {token}" - super().__init__( - app, - session.RESTSession( - allow_redirects=config.allow_redirects, - base_url=rest_url, - connector=config.tcp_connector, - debug=debug, - proxy_headers=config.proxy_headers, - proxy_auth=config.proxy_auth, - ssl_context=config.ssl_context, - verify_ssl=config.verify_ssl, - timeout=config.request_timeout, - token=token, - trust_env=config.trust_env, - version=version, - ), - ) diff --git a/hikari/api/rest_app.py b/hikari/rest_app.py similarity index 83% rename from hikari/api/rest_app.py rename to hikari/rest_app.py index 2249ed4120..12ac473c17 100644 --- a/hikari/api/rest_app.py +++ b/hikari/rest_app.py @@ -21,9 +21,12 @@ __all__ = ["IRESTApp"] import abc +import typing -from hikari.api import base_app -from hikari.rest import client +from hikari import base_app + +if typing.TYPE_CHECKING: + from hikari.net import rest class IRESTApp(base_app.IBaseApp, abc.ABC): @@ -38,5 +41,9 @@ class IRESTApp(base_app.IBaseApp, abc.ABC): @property @abc.abstractmethod - def rest(self) -> client.RESTClient: + def rest(self) -> rest.REST: """REST API.""" + + @abc.abstractmethod + async def close(self) -> None: + """Close any open resources safely.""" diff --git a/hikari/stateless/bot.py b/hikari/stateless/bot.py index 7ff78a57e0..e66ed9dd07 100644 --- a/hikari/stateless/bot.py +++ b/hikari/stateless/bot.py @@ -25,10 +25,10 @@ import typing from hikari import application -from hikari import rest -from .. import gateway -from hikari.gateway import intent_aware_dispatchers +from hikari.net.gateway import intent_aware_dispatchers from . import manager +from ..net import gateway +from ..net import rest if typing.TYPE_CHECKING: from hikari import application diff --git a/hikari/stateless/manager.py b/hikari/stateless/manager.py index 005a43e9a8..527781901f 100644 --- a/hikari/stateless/manager.py +++ b/hikari/stateless/manager.py @@ -29,8 +29,8 @@ # pylint: disable=too-many-public-methods from hikari.events import voice -from hikari.gateway import dispatchers -from hikari.gateway import event_managers +from hikari.net.gateway import dispatchers +from hikari.net.gateway import event_managers class StatelessEventManagerImpl(event_managers.EventManager[dispatchers.EventDispatcher]): diff --git a/tests/hikari/gateway/test_client.py b/tests/hikari/gateway/test_client.py index 230b9a7c0d..9dc68a29dd 100644 --- a/tests/hikari/gateway/test_client.py +++ b/tests/hikari/gateway/test_client.py @@ -27,10 +27,10 @@ from hikari import http_settings from hikari import errors from hikari import application -from hikari.gateway import consumers -from hikari import gateway as high_level_shards -from hikari.gateway import connection as low_level_shards -from hikari.gateway import gateway_state +from hikari.net.gateway import consumers +from hikari.net import gateway as high_level_shards +from hikari.net.gateway import connection as low_level_shards +from hikari.net.gateway import gateway_state from hikari.internal import codes from hikari.internal import more_asyncio from hikari.models import guilds diff --git a/tests/hikari/gateway/test_connection.py b/tests/hikari/gateway/test_connection.py index 5b8b89584c..01ae22a07b 100644 --- a/tests/hikari/gateway/test_connection.py +++ b/tests/hikari/gateway/test_connection.py @@ -29,10 +29,10 @@ import pytest from hikari import errors -from hikari.gateway import connection +from hikari.net.gateway import connection from hikari.internal import codes from hikari.internal import more_collections -from hikari.internal import user_agents +from hikari.net import user_agents from tests.hikari import _helpers diff --git a/tests/hikari/gateway/test_dispatchers.py b/tests/hikari/gateway/test_dispatchers.py index 5869785b15..6dad45c5c4 100644 --- a/tests/hikari/gateway/test_dispatchers.py +++ b/tests/hikari/gateway/test_dispatchers.py @@ -20,7 +20,7 @@ import pytest from hikari import events -from hikari.gateway import dispatchers +from hikari.net.gateway import dispatchers from tests.hikari import _helpers diff --git a/tests/hikari/gateway/test_intent_aware_dispatchers.py b/tests/hikari/gateway/test_intent_aware_dispatchers.py index 39487cdccc..3e716bac83 100644 --- a/tests/hikari/gateway/test_intent_aware_dispatchers.py +++ b/tests/hikari/gateway/test_intent_aware_dispatchers.py @@ -24,7 +24,7 @@ import pytest from hikari import events -from hikari.gateway import intent_aware_dispatchers +from hikari.net.gateway import intent_aware_dispatchers from tests.hikari import _helpers diff --git a/tests/hikari/internal/test_http_client.py b/tests/hikari/internal/test_http_client.py index 698c98f14d..ddd7b6248c 100644 --- a/tests/hikari/internal/test_http_client.py +++ b/tests/hikari/internal/test_http_client.py @@ -23,7 +23,8 @@ import mock import pytest -from hikari.internal import http_client, tracing +from hikari.net import tracing +from hikari.net import http_client from tests.hikari import _helpers diff --git a/tests/hikari/internal/test_urls.py b/tests/hikari/internal/test_urls.py index 40e774cc00..8b6dbe4414 100644 --- a/tests/hikari/internal/test_urls.py +++ b/tests/hikari/internal/test_urls.py @@ -16,7 +16,7 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . -from hikari.internal import urls +from hikari.net import urls from tests.hikari import _helpers diff --git a/tests/hikari/internal/test_user_agents.py b/tests/hikari/internal/test_user_agents.py index 4de6ed8779..c10e2388b9 100644 --- a/tests/hikari/internal/test_user_agents.py +++ b/tests/hikari/internal/test_user_agents.py @@ -16,7 +16,7 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . -from hikari.internal import user_agents +from hikari.net import user_agents def test_library_version_is_callable_and_produces_string(): diff --git a/tests/hikari/models/test_applications.py b/tests/hikari/models/test_applications.py index f8a9d48d85..34403a8747 100644 --- a/tests/hikari/models/test_applications.py +++ b/tests/hikari/models/test_applications.py @@ -20,7 +20,7 @@ import pytest from hikari import application -from hikari.internal import urls +from hikari.net import urls from hikari.models import applications from hikari.models import guilds from hikari.models import users diff --git a/tests/hikari/models/test_channels.py b/tests/hikari/models/test_channels.py index e3c3d3a90b..d88d9b1a38 100644 --- a/tests/hikari/models/test_channels.py +++ b/tests/hikari/models/test_channels.py @@ -26,7 +26,7 @@ from hikari.models import permissions from hikari.models import users from hikari import application -from hikari.internal import urls +from hikari.net import urls @pytest.fixture() diff --git a/tests/hikari/models/test_emojis.py b/tests/hikari/models/test_emojis.py index 6704e133a3..0e31d0f55f 100644 --- a/tests/hikari/models/test_emojis.py +++ b/tests/hikari/models/test_emojis.py @@ -20,7 +20,7 @@ import pytest from hikari import application -from hikari.internal import urls +from hikari.net import urls from hikari.models import bases from hikari.models import emojis from hikari.models import files diff --git a/tests/hikari/models/test_guilds.py b/tests/hikari/models/test_guilds.py index f26ab4fd40..dc7e9fd464 100644 --- a/tests/hikari/models/test_guilds.py +++ b/tests/hikari/models/test_guilds.py @@ -24,7 +24,7 @@ from hikari import application from hikari.internal import conversions -from hikari.internal import urls +from hikari.net import urls from hikari.models import channels from hikari.models import colors from hikari.models import emojis diff --git a/tests/hikari/models/test_invites.py b/tests/hikari/models/test_invites.py index 51b8571a1d..5716309bd2 100644 --- a/tests/hikari/models/test_invites.py +++ b/tests/hikari/models/test_invites.py @@ -24,7 +24,7 @@ from hikari import application from hikari.internal import conversions -from hikari.internal import urls +from hikari.net import urls from hikari.models import channels from hikari.models import guilds from hikari.models import invites diff --git a/tests/hikari/models/test_users.py b/tests/hikari/models/test_users.py index f76d0bea79..3653d3c158 100644 --- a/tests/hikari/models/test_users.py +++ b/tests/hikari/models/test_users.py @@ -19,11 +19,11 @@ import mock import pytest -from hikari import rest +from hikari.net import rest from hikari import application from hikari.models import bases from hikari.models import users -from hikari.internal import urls +from hikari.net import urls @pytest.fixture() diff --git a/tests/hikari/models/test_webhook.py b/tests/hikari/models/test_webhook.py index 6d7c2c9ecd..d162947f9f 100644 --- a/tests/hikari/models/test_webhook.py +++ b/tests/hikari/models/test_webhook.py @@ -19,9 +19,9 @@ import mock import pytest -from hikari import rest +from hikari.net import rest from hikari import application -from hikari.internal import urls +from hikari.net import urls from hikari.models import bases from hikari.models import channels from hikari.models import embeds diff --git a/tests/hikari/rest/test_base.py b/tests/hikari/rest/test_base.py index a9a61422bb..6b2c222bc3 100644 --- a/tests/hikari/rest/test_base.py +++ b/tests/hikari/rest/test_base.py @@ -21,9 +21,9 @@ from hikari import application from hikari.internal import ratelimits -from hikari.rest import base -from hikari.rest import buckets -from hikari.rest import session +from hikari.net.rest import base +from hikari.net import buckets +from hikari.net.rest import session @pytest.fixture() diff --git a/tests/hikari/rest/test_buckets.py b/tests/hikari/rest/test_buckets.py index 36ff1059a2..2e51664336 100644 --- a/tests/hikari/rest/test_buckets.py +++ b/tests/hikari/rest/test_buckets.py @@ -24,8 +24,8 @@ import pytest from hikari.internal import ratelimits -from hikari.rest import buckets -from hikari.rest import routes +from hikari.net import buckets +from hikari.net import routes from tests.hikari import _helpers diff --git a/tests/hikari/rest/test_channel.py b/tests/hikari/rest/test_channel.py index 890bff9315..6f9223876c 100644 --- a/tests/hikari/rest/test_channel.py +++ b/tests/hikari/rest/test_channel.py @@ -35,8 +35,8 @@ from hikari.models import messages from hikari.models import users from hikari.models import webhooks -from hikari.rest import channel -from hikari.rest import session +from hikari.net.rest import channel +from hikari.net.rest import session from tests.hikari import _helpers diff --git a/tests/hikari/rest/test_client.py b/tests/hikari/rest/test_client.py index 200e71d5e5..14536e8da3 100644 --- a/tests/hikari/rest/test_client.py +++ b/tests/hikari/rest/test_client.py @@ -22,19 +22,19 @@ import pytest from hikari import http_settings -from hikari import rest as high_level_rest +from hikari.net import rest as high_level_rest from hikari import application -from hikari.rest import channel -from hikari.rest import gateway -from hikari.rest import guild -from hikari.rest import invite -from hikari.rest import me -from hikari.rest import oauth2 -from hikari.rest import react -from hikari.rest import session as low_level_rest -from hikari.rest import user -from hikari.rest import voice -from hikari.rest import webhook +from hikari.net.rest import channel +from hikari.net.rest import gateway +from hikari.net.rest import guild +from hikari.net.rest import invite +from hikari.net.rest import me +from hikari.net.rest import oauth2 +from hikari.net.rest import react +from hikari.net.rest import session as low_level_rest +from hikari.net.rest import user +from hikari.net.rest import voice +from hikari.net.rest import webhook class TestRESTClient: diff --git a/tests/hikari/rest/test_gateway.py b/tests/hikari/rest/test_gateway.py index 5d914a4f69..6c7bc37971 100644 --- a/tests/hikari/rest/test_gateway.py +++ b/tests/hikari/rest/test_gateway.py @@ -22,8 +22,8 @@ from hikari import application from hikari.models import gateway as gateway_models -from hikari.rest import gateway -from hikari.rest import session +from hikari.net.rest import gateway +from hikari.net.rest import session class TestRESTReactionLogic: diff --git a/tests/hikari/rest/test_guild.py b/tests/hikari/rest/test_guild.py index b64aebfb93..6eaca2c935 100644 --- a/tests/hikari/rest/test_guild.py +++ b/tests/hikari/rest/test_guild.py @@ -36,8 +36,8 @@ from hikari.models import users from hikari.models import voices from hikari.models import webhooks -from hikari.rest import guild as _guild -from hikari.rest import session +from hikari.net.rest import guild as _guild +from hikari.net.rest import session from tests.hikari import _helpers diff --git a/tests/hikari/rest/test_invite.py b/tests/hikari/rest/test_invite.py index 4a197ab965..4b9d8343a7 100644 --- a/tests/hikari/rest/test_invite.py +++ b/tests/hikari/rest/test_invite.py @@ -21,8 +21,8 @@ from hikari import application from hikari.models import invites -from hikari.rest import invite -from hikari.rest import session +from hikari.net.rest import invite +from hikari.net.rest import session class TestRESTInviteLogic: diff --git a/tests/hikari/rest/test_me.py b/tests/hikari/rest/test_me.py index 06ac10007c..3692931c98 100644 --- a/tests/hikari/rest/test_me.py +++ b/tests/hikari/rest/test_me.py @@ -29,8 +29,8 @@ from hikari.models import files from hikari.models import guilds from hikari.models import users -from hikari.rest import me -from hikari.rest import session +from hikari.net.rest import me +from hikari.net.rest import session from tests.hikari import _helpers diff --git a/tests/hikari/rest/test_oauth2.py b/tests/hikari/rest/test_oauth2.py index 83a149852c..68aad46a86 100644 --- a/tests/hikari/rest/test_oauth2.py +++ b/tests/hikari/rest/test_oauth2.py @@ -22,8 +22,8 @@ from hikari import application from hikari.models import applications -from hikari.rest import oauth2 -from hikari.rest import session +from hikari.net.rest import oauth2 +from hikari.net.rest import session class TestRESTReactionLogic: diff --git a/tests/hikari/rest/test_react.py b/tests/hikari/rest/test_react.py index ae0b34b492..12ce46dc33 100644 --- a/tests/hikari/rest/test_react.py +++ b/tests/hikari/rest/test_react.py @@ -28,8 +28,8 @@ from hikari.models import emojis from hikari.models import messages from hikari.models import users -from hikari.rest import react -from hikari.rest import session +from hikari.net.rest import react +from hikari.net.rest import session from tests.hikari import _helpers diff --git a/tests/hikari/rest/test_routes.py b/tests/hikari/rest/test_routes.py index 82e6db72ac..885ed6902b 100644 --- a/tests/hikari/rest/test_routes.py +++ b/tests/hikari/rest/test_routes.py @@ -19,7 +19,7 @@ import mock import pytest -from hikari.rest import routes +from hikari.net import routes class TestCompiledRoute: diff --git a/tests/hikari/rest/test_session.py b/tests/hikari/rest/test_session.py index d570385923..f8143d7884 100644 --- a/tests/hikari/rest/test_session.py +++ b/tests/hikari/rest/test_session.py @@ -30,12 +30,12 @@ from hikari import errors from hikari.internal import conversions -from hikari.internal import http_client +from hikari.net import http_client from hikari.internal import ratelimits from hikari.models import files -from hikari.rest import buckets -from hikari.rest import session -from hikari.rest import routes +from hikari.net import buckets +from hikari.net.rest import session +from hikari.net import routes from tests.hikari import _helpers diff --git a/tests/hikari/rest/test_user.py b/tests/hikari/rest/test_user.py index cd66c90232..a1fcd719e3 100644 --- a/tests/hikari/rest/test_user.py +++ b/tests/hikari/rest/test_user.py @@ -21,8 +21,8 @@ from hikari import application from hikari.models import users -from hikari.rest import session -from hikari.rest import user +from hikari.net.rest import session +from hikari.net.rest import user from tests.hikari import _helpers diff --git a/tests/hikari/rest/test_voice.py b/tests/hikari/rest/test_voice.py index 2b4ede3d3d..7e2b43d56e 100644 --- a/tests/hikari/rest/test_voice.py +++ b/tests/hikari/rest/test_voice.py @@ -21,8 +21,8 @@ from hikari import application from hikari.models import voices -from hikari.rest import voice -from hikari.rest import session +from hikari.net.rest import voice +from hikari.net.rest import session class TestRESTUserLogic: diff --git a/tests/hikari/rest/test_webhook.py b/tests/hikari/rest/test_webhook.py index 1886756e74..f3c39558a2 100644 --- a/tests/hikari/rest/test_webhook.py +++ b/tests/hikari/rest/test_webhook.py @@ -27,8 +27,8 @@ from hikari.models import files from hikari.models import messages from hikari.models import webhooks -from hikari.rest import session -from hikari.rest import webhook +from hikari.net.rest import session +from hikari.net.rest import webhook from tests.hikari import _helpers diff --git a/tests/hikari/stateless/test_manager.py b/tests/hikari/stateless/test_manager.py index 2633c6c678..6fd0934b45 100644 --- a/tests/hikari/stateless/test_manager.py +++ b/tests/hikari/stateless/test_manager.py @@ -25,7 +25,7 @@ from hikari.events import message from hikari.events import other from hikari.events import voice -from hikari import gateway +from hikari.net import gateway from hikari.stateless import manager diff --git a/tests/hikari/test_configs.py b/tests/hikari/test_configs.py index 2095da68da..47acc3d4af 100644 --- a/tests/hikari/test_configs.py +++ b/tests/hikari/test_configs.py @@ -24,7 +24,7 @@ import pytest from hikari import http_settings -from hikari.internal import urls +from hikari.net import urls from hikari.models import gateway from hikari.models import guilds from hikari.models import intents From 63b9c3fce133957fe0cdca19aaae600df7c2c8c5 Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Wed, 20 May 2020 18:18:49 +0100 Subject: [PATCH 367/922] Did something, but I forgot what it was, but it was probably important. --- hikari/internal/conversions.py | 45 ++++------------------------------ hikari/net/rest.py | 42 ++++++++++++++----------------- 2 files changed, 24 insertions(+), 63 deletions(-) diff --git a/hikari/internal/conversions.py b/hikari/internal/conversions.py index a55ff65207..d424da3643 100644 --- a/hikari/internal/conversions.py +++ b/hikari/internal/conversions.py @@ -45,6 +45,8 @@ import re import typing +from hikari.models import unset + if typing.TYPE_CHECKING: import enum @@ -115,7 +117,7 @@ def put_if_specified( type_after : typing.Callable[[`input type`], `output type`] | None Type to apply to the value when added. """ - if value is not ...: + if value is not unset.UNSET: if type_after: mapping[key] = type_after(value) else: @@ -312,42 +314,5 @@ def resolve_signature(func: typing.Callable) -> inspect.Signature: return signature -def dereference_int_flag( - int_flag_type: typing.Type[IntFlagT], - raw_value: typing.Union[RawIntFlagValueT, typing.Collection[RawIntFlagValueT]], -) -> IntFlagT: - """Cast to the provided `enum.IntFlag` type. - - This supports resolving bitfield integers as well as decoding a sequence - of case insensitive flag names into one combined value. - - Parameters - ---------- - int_flag_type : typing.Type[enum.IntFlag] - The type of the int flag to check. - raw_value : Castable Value - The raw value to convert. - - - !!! note - Types that are a `Castable Value` include: - - `str` - - `int` - - `typing.SupportsInt` - - `typing.Collection`[`Castable Value`] - - When a collection is passed, values will be combined using functional - reduction via the `operator.or_` operator. - - Returns - ------- - enum.IntFlag - The cast value as a flag. - """ - if isinstance(raw_value, str) and raw_value.isdigit(): - raw_value = int(raw_value) - - if not isinstance(raw_value, int): - raw_value = functools.reduce(operator.or_, (int_flag_type[name.upper()] for name in raw_value)) - - return int_flag_type(raw_value) +def cast_to_str_id(value: typing.Union[typing.SupportsInt, int]) -> str: + return str(int(value)) diff --git a/hikari/net/rest.py b/hikari/net/rest.py index 0ba6c7323c..a332d89d47 100644 --- a/hikari/net/rest.py +++ b/hikari/net/rest.py @@ -229,10 +229,12 @@ async def _handle_rate_limits_for_response(self, compiled_route, response): async def fetch_channel( self, channel: typing.Union[channels.PartialChannel, bases.Snowflake, int], ) -> channels.PartialChannel: - response = await self._request(routes.GET_CHANNEL.compile(channel_id=str(int(channel)))) - raise NotImplementedError + response = await self._request(routes.GET_CHANNEL.compile(channel_id=conversions.cast_to_str_id(channel))) - async def update_channel( + # TODO: implement serialization. + return NotImplemented + + async def edit_channel( self, channel: typing.Union[channels.PartialChannel, bases.Snowflake, int], *, @@ -248,28 +250,22 @@ async def update_channel( reason: typing.Union[unset.Unset, str] = unset.UNSET, ) -> channels.PartialChannel: payload = {} - if not unset.is_unset(name): - payload["name"] = name - if not unset.is_unset(position): - payload["position"] = position - if not unset.is_unset(topic): - payload["topic"] = topic - if not unset.is_unset(nsfw): - payload["nsfw"] = nsfw - if not unset.is_unset(bitrate): - payload["bitrate"] = bitrate - if not unset.is_unset(user_limit): - payload["user_limit"] = user_limit - if not unset.is_unset(rate_limit_per_user): - payload["rate_limit_per_user"] = rate_limit_per_user - if not unset.is_unset("permission_overwrites"): - # TODO: json serialize - payload["permission_overwrites"] = permission_overwrites - if not unset.is_unset(parent_category): - payload["parent_id"] = str(int(parent_category)) + conversions.put_if_specified(payload, "name", name) + conversions.put_if_specified(payload, "position", position) + conversions.put_if_specified(payload, "topic", topic) + conversions.put_if_specified(payload, "nsfw", nsfw) + conversions.put_if_specified(payload, "bitrate", bitrate) + conversions.put_if_specified(payload, "user_limit", user_limit) + conversions.put_if_specified(payload, "rate_limit_per_user", rate_limit_per_user) + conversions.put_if_specified(payload, "parent_id", parent_category, conversions.cast_to_str_id) + + if not unset.is_unset(permission_overwrites): + # TODO: implement serialization + raise NotImplementedError() response = await self._request( routes.PATCH_CHANNEL.compile(channel_id=str(int(channel))), body=payload, reason=reason, ) - raise NotImplementedError + # TODO: implement deserialization. + return NotImplemented From 0b6108b8cd3a576ae17a2f5ac0b079723f6f08a0 Mon Sep 17 00:00:00 2001 From: Faster Speeding Date: Fri, 22 May 2020 15:54:58 +0100 Subject: [PATCH 368/922] Add abstract methods for IEntityFactory abc. --- hikari/entity_factory.py | 232 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 232 insertions(+) diff --git a/hikari/entity_factory.py b/hikari/entity_factory.py index 093f7e3692..5aed3ba1b2 100644 --- a/hikari/entity_factory.py +++ b/hikari/entity_factory.py @@ -22,9 +22,241 @@ __all__ = ["IEntityFactory"] import abc +import typing + +if typing.TYPE_CHECKING: + from hikari.internal import more_typing + from hikari.models import applications + from hikari.models import audit_logs + from hikari.models import channels + from hikari.models import embeds + from hikari.models import emojis + from hikari.models import gateway + from hikari.models import guilds + from hikari.models import invites + from hikari.models import messages + from hikari.models import users + from hikari.models import voices + from hikari.models import webhooks class IEntityFactory(abc.ABC): """Interface for an entity factory implementation.""" __slots__ = () + + ################ + # APPLICATIONS # + ################ + + @abc.abstractmethod + def deserialize_own_connection(self, payload: more_typing.JSONObject) -> applications.OwnConnection: + ... + + @abc.abstractmethod + def deserialize_own_guild(self, payload: more_typing.JSONObject) -> applications.OwnGuild: + ... + + @abc.abstractmethod + def deserialize_application(self, payload: more_typing.JSONObject) -> applications: + ... + + ############## + # AUDIT_LOGS # + ############## + + @abc.abstractmethod + def deserialize_audit_log_entry(self, payload: more_typing.JSONObject) -> audit_logs.AuditLogEntry: + ... + + ############ + # CHANNELS # + ############ + + @abc.abstractmethod + def deserialize_permission_overwrite(self, payload: more_typing.JSONObject) -> channels.PermissionOverwrite: + ... + + @abc.abstractmethod + def serialize_permission_overwrite(self, overwrite: channels.PermissionOverwrite) -> more_typing.JSONObject: + ... + + @abc.abstractmethod + def deserialize_partial_channel(self, payload: more_typing.JSONObject) -> channels.PartialChannel: + ... + + @abc.abstractmethod + def deserialize_dm_channel(self, payload: more_typing.JSONObject) -> channels.DMChannel: + ... + + @abc.abstractmethod + def deserialize_group_dm_channel(self, payload: more_typing.JSONObject) -> channels.GroupDMChannel: + ... + + @abc.abstractmethod + def deserialize_guild_category(self, payload: more_typing.JSONObject) -> channels.GuildCategory: + ... + + @abc.abstractmethod + def deserialize_guild_text_channel(self, payload: more_typing.JSONObject) -> channels.GuildTextChannel: + ... + + @abc.abstractmethod + def deserialize_guild_news_channel(self, payload: more_typing.JSONObject) -> channels.GuildNewsChannel: + ... + + @abc.abstractmethod + def deserialize_guild_store_channel(self, payload: more_typing.JSONObject) -> channels.GuildStoreChannel: + ... + + @abc.abstractmethod + def deserialize_guild_voice_channel(self, payload: more_typing.JSONObject) -> channels.GuildVoiceChannel: + ... + + @abc.abstractmethod + def deserialize_channel(self, payload: more_typing.JSONObject) -> channels.PartialChannel: + ... + + ########## + # EMBEDS # + ########## + + @abc.abstractmethod + def deserialize_embed(self, payload: more_typing.JSONObject) -> embeds.Embed: + ... + + @abc.abstractmethod + def serialize_embed(self, embed: embeds.Embed) -> more_typing.JSONObject: + ... + + ########## + # EMOJIS # + ########## + + @abc.abstractmethod + def deserialize_unicode_emoji(self, payload: more_typing.JSONObject) -> emojis.UnicodeEmoji: + ... + + @abc.abstractmethod + def deserialize_custom_emoji(self, payload: more_typing.JSONObject) -> emojis.CustomEmoji: + ... + + @abc.abstractmethod + def deserialize_known_custom_emoji(self, payload: more_typing.JSONObject) -> emojis.KnownCustomEmoji: + ... + + @abc.abstractmethod + def deserialize_emoji( + self, payload: more_typing.JSONObject + ) -> typing.Union[emojis.UnicodeEmoji, emojis.CustomEmoji]: + ... + + ########### + # GATEWAY # + ########### + + @abc.abstractmethod + def deserialize_gateway_bot(self, payload: more_typing.JSONObject) -> gateway.GatewayBot: + ... + + ########## + # GUILDS # + ########## + + @abc.abstractmethod + def deserialize_guild_embed(self, payload: more_typing.JSONObject) -> guilds.GuildEmbed: + ... + + @abc.abstractmethod + def deserialize_guild_member( + self, payload: more_typing.JSONObject, *, user: typing.Optional[users.User] = None + ) -> guilds.GuildMember: + ... + + @abc.abstractmethod + def deserialize_guild_role(self, payload: more_typing.JSONObject) -> guilds.GuildRole: + ... + + @abc.abstractmethod + def deserialize_guild_member_presence(self, payload: more_typing.JSONObject) -> guilds.GuildMemberPresence: + ... + + @abc.abstractmethod + def deserialize_partial_guild_integration(self, payload: more_typing.JSONObject) -> guilds.PartialGuildIntegration: + ... + + @abc.abstractmethod + def deserialize_guild_integration(self, payload: more_typing.JSONObject) -> guilds.GuildIntegration: + ... + + @abc.abstractmethod + def deserialize_guild_member_ban(self, payload: more_typing.JSONObject) -> guilds.GuildMemberBan: + ... + + @abc.abstractmethod + def deserialize_unavailable_guild(self, payload: more_typing.JSONObject) -> guilds.UnavailableGuild: + ... + + @abc.abstractmethod + def deserialize_guild_preview(self, payload: more_typing.JSONObject) -> guilds.GuildPreview: + ... + + @abc.abstractmethod + def deserialize_guild(self, payload: more_typing.JSONObject) -> guilds.Guild: + ... + + ########### + # INVITES # + ########### + + @abc.abstractmethod + def deserialize_vanity_url(self, payload: more_typing.JSONObject) -> invites.VanityUrl: + ... + + @abc.abstractmethod + def deserialize_invite(self, payload: more_typing.JSONObject) -> invites.Invite: + ... + + @abc.abstractmethod + def deserialize_invite_with_metadata(self, payload: more_typing.JSONObject) -> invites.InviteWithMetadata: + ... + + ############ + # MESSAGES # + ############ + + @abc.abstractmethod + def deserialize_message(self, payload: more_typing.JSONObject) -> messages.Message: + ... + + ######### + # USERS # + ######### + + @abc.abstractmethod + def deserialize_user(self, payload: more_typing.JSONObject) -> users.User: + ... + + @abc.abstractmethod + def deserialize_my_user(self, payload: more_typing.JSONObject) -> users.MyUser: + ... + + ########## + # Voices # + ########## + + @abc.abstractmethod + def deserialize_voice_state(self, payload: more_typing.JSONObject) -> voices.VoiceState: + ... + + @abc.abstractmethod + def deserialize_voice_region(self, payload: more_typing.JSONObject) -> voices.VoiceRegion: + ... + + ############ + # WEBHOOKS # + ############ + + @abc.abstractmethod + def deserialize_webhook(self, payload: more_typing.JSONObject) -> webhooks.Webhook: + ... From f9397c8fbeeaacde1a1afbe744d7ae43761bfa82 Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Thu, 21 May 2020 10:15:05 +0100 Subject: [PATCH 369/922] Added message paginator endpoint. --- hikari/.rest/base.py | 2 +- hikari/.rest/channel.py | 2 +- hikari/.rest/session.py | 6 +- hikari/impl/pagination.py | 32 +++---- hikari/internal/ratelimits.py | 20 ++--- hikari/net/buckets.py | 14 ++-- hikari/net/rest.py | 134 ++++++++++++++++++++++++++---- hikari/net/routes.py | 22 ++--- hikari/net/urls.py | 2 +- tests/hikari/rest/test_buckets.py | 14 ++-- tests/hikari/rest/test_channel.py | 14 ++-- tests/hikari/rest/test_me.py | 2 +- 12 files changed, 183 insertions(+), 81 deletions(-) diff --git a/hikari/.rest/base.py b/hikari/.rest/base.py index 7a7b44ef7e..732fae91fc 100644 --- a/hikari/.rest/base.py +++ b/hikari/.rest/base.py @@ -75,7 +75,7 @@ def global_ratelimit_queue_size(self) -> int: @property def route_ratelimit_queue_size(self) -> int: - """Count of API waiting for a route-specific ratelimit to release. + """Count of API waiting for a _route-specific ratelimit to release. If this is non-zero, then you are being ratelimited somewhere. """ diff --git a/hikari/.rest/channel.py b/hikari/.rest/channel.py index 950b45cff9..ac86180901 100644 --- a/hikari/.rest/channel.py +++ b/hikari/.rest/channel.py @@ -366,7 +366,7 @@ def fetch_messages( !!! note The `around` parameter is not documented clearly by Discord. - The actual number of messages returned by this, and the direction + The actual number of messages returned by this, and the _direction (e.g. older/newer/both) is not overly intuitive. Thus, this specific functionality may be deprecated in the future in favour of a cleaner Python API until a time comes where this information is diff --git a/hikari/.rest/session.py b/hikari/.rest/session.py index 0f8d0bbd40..f526de7003 100644 --- a/hikari/.rest/session.py +++ b/hikari/.rest/session.py @@ -67,7 +67,7 @@ class RESTSession(http_client.HTTPClient): # pylint: disable=too-many-public-me allow_redirects : bool Whether to allow redirects or not. Defaults to `False`. base_url : str - The base URL and route for the discord API + The base URL and _route for the discord API connector : aiohttp.BaseConnector | None Optional aiohttp _connector info for making an HTTP connection debug : bool @@ -125,7 +125,7 @@ class RESTSession(http_client.HTTPClient): # pylint: disable=too-many-public-me """ bucket_ratelimiters: buckets.RESTBucketManager - """The per-route ratelimit manager. + """The per-_route ratelimit manager. This handles tracking any ratelimits for routes that have recently been used or are in active use, as well as keeping memory usage to a minimum where @@ -331,7 +331,7 @@ async def _handle_rate_limits_for_response(self, compiled_route, response): ) else: self.logger.warning( - "you are being rate-limited on bucket %s for route %s - trying again after %ss", + "you are being rate-limited on bucket %s for _route %s - trying again after %ss", bucket, compiled_route, reset, diff --git a/hikari/impl/pagination.py b/hikari/impl/pagination.py index 8a6cbf8f88..4cdf372697 100644 --- a/hikari/impl/pagination.py +++ b/hikari/impl/pagination.py @@ -31,21 +31,21 @@ class GuildPaginator(pagination.BufferedPaginatedResults[guilds.Guild]): - __slots__ = ("_app", "_session", "_newest_first", "_first_id") + __slots__ = ("_app", "_newest_first", "_first_id", "_request_partial") - def __init__(self, app, newest_first, first_item, session): + def __init__(self, app, newest_first, first_item, request_partial): super().__init__() self._app = app self._newest_first = newest_first self._first_id = self._prepare_first_id( first_item, bases.Snowflake.max() if newest_first else bases.Snowflake.min(), ) - self._session = session + self._request_partial = request_partial async def _next_chunk(self): kwargs = {"before" if self._newest_first else "after": self._first_id} - chunk = await self._session.get_current_user_guilds(**kwargs) + chunk = await self._request_partial(**kwargs) if not chunk: return None @@ -56,17 +56,17 @@ async def _next_chunk(self): class MemberPaginator(pagination.BufferedPaginatedResults[guilds.GuildMember]): - __slots__ = ("_app", "_guild_id", "_first_id", "_session") + __slots__ = ("_app", "_guild_id", "_first_id", "_request_partial") - def __init__(self, app, guild, created_after, session): + def __init__(self, app, guild, created_after, request_partial): super().__init__() self._app = app self._guild_id = str(int(guild)) self._first_id = self._prepare_first_id(created_after) - self._session = session + self._request_partial = request_partial async def _next_chunk(self): - chunk = await self._session.list_guild_members(self._guild_id, after=self._first_id) + chunk = await self._request_partial(guild_id=self._guild_id, after=self._first_id) if not chunk: return None @@ -77,9 +77,9 @@ async def _next_chunk(self): class MessagePaginator(pagination.BufferedPaginatedResults[messages.Message]): - __slots__ = ("_app", "_channel_id", "_direction", "_first_id", "_session") + __slots__ = ("_app", "_channel_id", "_direction", "_first_id", "_request_partial") - def __init__(self, app, channel, direction, first, session) -> None: + def __init__(self, app, channel, direction, first, request_partial) -> None: super().__init__() self._app = app self._channel_id = str(int(channel)) @@ -87,7 +87,7 @@ def __init__(self, app, channel, direction, first, session) -> None: self._first_id = ( str(bases.Snowflake.from_datetime(first)) if isinstance(first, datetime.datetime) else str(int(first)) ) - self._session = session + self._request_partial = request_partial async def _next_chunk(self): kwargs = { @@ -96,7 +96,7 @@ async def _next_chunk(self): "limit": 100, } - chunk = await self._session.get_channel_messages(**kwargs) + chunk = await self._request_partial(**kwargs) if not chunk: return None @@ -109,19 +109,19 @@ async def _next_chunk(self): class ReactionPaginator(pagination.BufferedPaginatedResults[messages.Reaction]): - __slots__ = ("_app", "_channel_id", "_message_id", "_first_id", "_emoji", "_session") + __slots__ = ("_app", "_channel_id", "_message_id", "_first_id", "_emoji", "_request_partial") - def __init__(self, app, channel, message, emoji, users_after, session) -> None: + def __init__(self, app, channel, message, emoji, users_after, request_partial) -> None: super().__init__() self._app = app self._channel_id = str(int(channel)) self._message_id = str(int(message)) self._emoji = getattr(emoji, "url_name", emoji) self._first_id = self._prepare_first_id(users_after) - self._session = session + self._request_partial = request_partial async def _next_chunk(self): - chunk = await self._session.get_reactions( + chunk = await self._request_partial( channel_id=self._channel_id, message_id=self._message_id, emoji=self._emoji, after=self._first_id ) diff --git a/hikari/internal/ratelimits.py b/hikari/internal/ratelimits.py index dd3e6adb1f..cdde72b6e8 100644 --- a/hikari/internal/ratelimits.py +++ b/hikari/internal/ratelimits.py @@ -26,9 +26,9 @@ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ In this module, we refer to a `hikari.rest.routes.CompiledRoute` as a definition -of a route with specific major parameter values included (e.g. +of a _route with specific major parameter values included (e.g. `POST /channels/123/messages`), and a `hikari.rest.routes.Route` as a -definition of a route without specific parameter values included (e.g. +definition of a _route without specific parameter values included (e.g. `POST /channels/{channel}/messages`). We can compile a `hikari.rest.routes.CompiledRoute` from a `hikari.rest.routes.Route` by providing the corresponding parameters as kwargs, as you may already know. @@ -39,19 +39,19 @@ It also supports providing in-order execution of queued tasks. Discord allocates types of buckets to routes. If you are making a request and -there is a valid rate limit on the route you hit, you should receive an +there is a valid rate limit on the _route you hit, you should receive an `X-RateLimit-Bucket` header from the server in your response. This is a hash -that identifies a route based on internal criteria that does not include major +that identifies a _route based on internal criteria that does not include major parameters. This `X-RateLimitBucket` is known in this module as an "bucket hash". -This means that generally, the route `POST /channels/123/messages` and +This means that generally, the _route `POST /channels/123/messages` and `POST /channels/456/messages` will usually sit in the same bucket, but `GET /channels/123/messages/789` and `PATCH /channels/123/messages/789` will usually not share the same bucket. Discord may or may not change this at any time, so hard coding this logic is not a useful thing to be doing. Rate limits, on the other hand, apply to a bucket and are specific to the major -parameters of the compiled route. This means that `POST /channels/123/messages` +parameters of the compiled _route. This means that `POST /channels/123/messages` and `POST /channels/456/messages` do not share the same real bucket, despite Discord providing the same bucket hash. A real bucket hash is the `str` hash of the bucket that Discord sends us in a response concatenated to the corresponding @@ -66,7 +66,7 @@ unless you are hitting it from elsewhere in the same time window outside your hikari.models.applications. To manage this situation, unknown endpoints are allocated to a special unlimited bucket until they have an initial bucket hash code allocated -from a response. Once this happens, the route is reallocated a dedicated bucket. +from a response. Once this happens, the _route is reallocated a dedicated bucket. Unknown buckets have a hardcoded initial hash code internally. Initially acquiring time on a bucket @@ -74,13 +74,13 @@ Each time you `BaseRateLimiter.acquire()` a request timeslice for a given `hikari.rest.routes.Route`, several things happen. The first is that we -attempt to find the existing bucket for that route, if there is one, or get an +attempt to find the existing bucket for that _route, if there is one, or get an unknown bucket otherwise. This is done by creating a real bucket hash from the -compiled route. The initial hash is calculated using a lookup table that maps +compiled _route. The initial hash is calculated using a lookup table that maps `hikari.rest.routes.CompiledRoute` objects to their corresponding initial hash codes, or to the unknown bucket hash code if not yet known. This initial hash is processed by the `hikari.rest.routes.CompiledRoute` to provide the real bucket -hash we need to get the route's bucket object internally. +hash we need to get the _route's bucket object internally. The `acquire` method will take the bucket and acquire a new timeslice on it. This takes the form of a `asyncio.Future` which should be awaited by diff --git a/hikari/net/buckets.py b/hikari/net/buckets.py index f8dbe5222e..52d703187e 100644 --- a/hikari/net/buckets.py +++ b/hikari/net/buckets.py @@ -39,7 +39,7 @@ class RESTBucket(ratelimits.WindowedBurstRateLimiter): """Represents a rate limit for an RESTSession endpoint. - Component to represent an active rate limit bucket on a specific RESTSession route + Component to represent an active rate limit bucket on a specific RESTSession _route with a specific major parameter combo. This is somewhat similar to the `WindowedBurstRateLimiter` in how it @@ -61,7 +61,7 @@ class RESTBucket(ratelimits.WindowedBurstRateLimiter): __slots__ = ("compiled_route",) compiled_route: typing.Final[routes.CompiledRoute] - """The compiled route that this rate limit is covering.""" + """The compiled _route that this rate limit is covering.""" def __init__(self, name: str, compiled_route: routes.CompiledRoute) -> None: super().__init__(name, 1, 1) @@ -145,7 +145,7 @@ class RESTBucketManager: real_hashes_to_buckets: typing.Final[typing.MutableMapping[str, RESTBucket]] """Maps full bucket hashes (`X-RateLimit-Bucket` appended with a hash of - major parameters used in that compiled route) to their corresponding rate + major parameters used in that compiled _route) to their corresponding rate limiters. """ @@ -213,7 +213,7 @@ async def gc(self, poll_period: float, expire_after: float) -> None: # noqa: D4 """The garbage collector loop. This is designed to run in the background and manage removing unused - route references from the rate-limiter collection to save memory. + _route references from the rate-limiter collection to save memory. This will run forever until `RESTBucketManager. closed_event` is set. This will invoke `RESTBucketManager.do_gc_pass` periodically. @@ -301,12 +301,12 @@ def do_gc_pass(self, expire_after: float) -> None: self.logger.debug("purged %s stale buckets, %s remain in survival, %s active", dead, survival, active) def acquire(self, compiled_route: routes.CompiledRoute) -> more_typing.Future[None]: - """Acquire a bucket for the given route. + """Acquire a bucket for the given _route. Parameters ---------- compiled_route : hikari.rest.routes.CompiledRoute - The route to get the bucket for. + The _route to get the bucket for. Returns ------- @@ -356,7 +356,7 @@ def update_rate_limits( Parameters ---------- compiled_route : hikari.rest.routes.CompiledRoute - The compiled route to get the bucket for. + The compiled _route to get the bucket for. bucket_header : str, optional The `X-RateLimit-Bucket` header that was provided in the response, or `None` if not present. diff --git a/hikari/net/rest.py b/hikari/net/rest.py index a332d89d47..428801b969 100644 --- a/hikari/net/rest.py +++ b/hikari/net/rest.py @@ -32,11 +32,13 @@ from hikari import base_app from hikari import errors from hikari import http_settings +from hikari import pagination from hikari.internal import conversions from hikari.internal import more_typing from hikari.internal import ratelimits from hikari.models import bases from hikari.models import channels +from hikari.models import messages from hikari.models import unset from hikari.net import buckets from hikari.net import http_client @@ -47,6 +49,36 @@ class _RateLimited(RuntimeError): __slots__ = () +class _MessagePaginator(pagination.BufferedPaginatedResults[messages.Message]): + __slots__ = ("_app", "_request_call", "_direction", "_first_id", "_route") + + def __init__( + self, + app: base_app.IBaseApp, + request_call: typing.Callable[..., more_typing.Coroutine[more_typing.JSONObject]], + channel_id: str, + direction: str, + first_id: str, + ) -> None: + self._app = app + self._request_call = request_call + self._direction = direction + self._first_id = first_id + self._route = routes.GET_CHANNEL_MESSAGES.compile(channel_id) + + async def _next_chunk(self) -> typing.Optional[typing.Generator[messages.Message, typing.Any, None]]: + chunk = await self._request_call(self._route, query={self._direction: self._first_id, "limit": 100}) + + if not chunk: + return None + if self._direction == "after": + chunk.reverse() + + self._first_id = chunk[-1]["id"] + + return (self._app.entity_factory.deserialize_message(m) for m in chunk) + + class REST(http_client.HTTPClient): def __init__( self, @@ -120,11 +152,17 @@ async def _request( while True: try: # Moved to a separate method to keep branch counts down. - return await self._request_once(compiled_route, headers, body, query) + return await self._request_once(compiled_route=compiled_route, headers=headers, body=body, query=query) except _RateLimited: pass - async def _request_once(self, compiled_route, headers, body, query): + async def _request_once( + self, + compiled_route: routes.CompiledRoute, + headers: more_typing.Headers, + body: typing.Optional[typing.Union[aiohttp.FormData, more_typing.JSONType]], + query: typing.Optional[typing.Dict[str, str]], + ): url = compiled_route.create_url(self._url) # Wait for any ratelimits to finish. @@ -176,11 +214,13 @@ async def _request_once(self, compiled_route, headers, body, query): raise cls(real_url, status, response.headers, raw_body) - async def _handle_rate_limits_for_response(self, compiled_route, response): - # Worth noting there is some bug on V6 that ratelimits me immediately if I have an invalid token. + async def _handle_rate_limits_for_response( + self, compiled_route: routes.CompiledRoute, response: aiohttp.ClientResponse + ) -> None: + # Worth noting there is some bug on V6 that rate limits me immediately if I have an invalid token. # https://github.com/discord/discord-api-docs/issues/1569 - # Handle ratelimiting. + # Handle rate limiting. resp_headers = response.headers limit = int(resp_headers.get("x-ratelimit-limit", "1")) remaining = int(resp_headers.get("x-ratelimit-remaining", "1")) @@ -211,7 +251,7 @@ async def _handle_rate_limits_for_response(self, compiled_route, response): ) else: self.logger.warning( - "you are being rate-limited on bucket %s for route %s - trying again after %ss", + "you are being rate-limited on bucket %s for _route %s - trying again after %ss", bucket, compiled_route, reset, @@ -223,20 +263,19 @@ async def _handle_rate_limits_for_response(self, compiled_route, response): # I hope we don't though. raise errors.HTTPError( str(response.real_url), - f"We were ratelimited but did not understand the response. Perhaps Cloudflare did this? {body!r}", + f"We were rate limited but did not understand the response. Perhaps Cloudflare did this? {body!r}", ) async def fetch_channel( - self, channel: typing.Union[channels.PartialChannel, bases.Snowflake, int], + self, channel: typing.Union[channels.PartialChannel, bases.Snowflake, int], /, ) -> channels.PartialChannel: response = await self._request(routes.GET_CHANNEL.compile(channel_id=conversions.cast_to_str_id(channel))) - - # TODO: implement serialization. - return NotImplemented + return self._app.entity_factory.deserialize_channel(response) async def edit_channel( self, channel: typing.Union[channels.PartialChannel, bases.Snowflake, int], + /, *, name: typing.Union[unset.Unset, str] = unset.UNSET, position: typing.Union[unset.Unset, int] = unset.UNSET, @@ -260,12 +299,75 @@ async def edit_channel( conversions.put_if_specified(payload, "parent_id", parent_category, conversions.cast_to_str_id) if not unset.is_unset(permission_overwrites): - # TODO: implement serialization - raise NotImplementedError() + payload["permission_overwrites"] = [ + self._app.entity_factory.serialize_permission_overwrite(p) for p in permission_overwrites + ] response = await self._request( - routes.PATCH_CHANNEL.compile(channel_id=str(int(channel))), body=payload, reason=reason, + routes.PATCH_CHANNEL.compile(channel_id=conversions.cast_to_str_id(channel)), body=payload, reason=reason, ) - # TODO: implement deserialization. - return NotImplemented + return self._app.entity_factory.deserialize_channel(response) + + async def delete_channel(self, channel: typing.Union[channels.PartialChannel, bases.Snowflake, int]) -> None: + await self._request(routes.DELETE_CHANNEL.compile(channel_id=conversions.cast_to_str_id(channel))) + + @typing.overload + async def fetch_messages( + self, channel: typing.Union[channels.PartialChannel, bases.UniqueObjectT], / + ) -> pagination.PaginatedResults[messages.Message]: + ... + + @typing.overload + async def fetch_messages( + self, + channel: typing.Union[channels.PartialChannel, bases.UniqueObjectT], + /, + *, + before: typing.Union[datetime.datetime, bases.UniqueObjectT], + ) -> pagination.PaginatedResults[messages.Message]: + ... + + @typing.overload + async def fetch_messages( + self, + channel: typing.Union[channels.PartialChannel, bases.UniqueObjectT], + /, + *, + around: typing.Union[datetime.datetime, bases.UniqueObjectT], + ) -> pagination.PaginatedResults[messages.Message]: + ... + + @typing.overload + async def fetch_messages( + self, + channel: typing.Union[channels.PartialChannel, bases.UniqueObjectT], + /, + *, + after: typing.Union[datetime.datetime, bases.UniqueObjectT], + ) -> pagination.PaginatedResults[messages.Message]: + ... + + def fetch_messages( + self, + channel: typing.Union[channels.PartialChannel, bases.UniqueObjectT], + /, + **kwargs: typing.Optional[typing.Union[datetime.datetime, bases.UniqueObjectT]], + ) -> pagination.PaginatedResults[messages.Message]: + if len(kwargs) == 1 and any(direction in kwargs for direction in ("before", "after", "around")): + direction, timestamp = kwargs.popitem() + elif not kwargs: + direction, timestamp = "before", bases.Snowflake.max() + else: + raise TypeError(f"Expected no kwargs, or one of 'before', 'after', 'around', received: {kwargs}") + + if isinstance(timestamp, datetime.datetime): + timestamp = bases.Snowflake.from_datetime(timestamp) + + return _MessagePaginator( + self._app, + self._request, + conversions.cast_to_str_id(channel), + direction, + conversions.cast_to_str_id(timestamp), + ) diff --git a/hikari/net/routes.py b/hikari/net/routes.py index ab0e9618fc..ad8f6bd51d 100644 --- a/hikari/net/routes.py +++ b/hikari/net/routes.py @@ -30,12 +30,12 @@ class CompiledRoute: - """A compiled representation of a route ready to be made into a full e and to be used for a request. + """A compiled representation of a _route ready to be made into a full e and to be used for a request. Parameters ---------- route : Route - The route used to make this compiled route. + The _route used to make this compiled _route. path : str The path with any major parameters interpolated in. major_params_hash : str @@ -45,13 +45,13 @@ class CompiledRoute: __slots__ = ("route", "major_param_hash", "compiled_path", "hash_code") route: typing.Final[Route] - """The route this compiled route was created from.""" + """The _route this compiled _route was created from.""" major_param_hash: typing.Final[str] """The major parameters in a bucket hash-compatible representation.""" compiled_path: typing.Final[str] - """The compiled route path to use.""" + """The compiled _route path to use.""" hash_code: typing.Final[int] """The hash code.""" @@ -64,7 +64,7 @@ def __init__(self, route: Route, path: str, major_params_hash: str) -> None: @property def method(self) -> str: - """Return the HTTP method of this compiled route.""" + """Return the HTTP method of this compiled _route.""" return self.route.method def create_url(self, base_url: str) -> str: @@ -78,7 +78,7 @@ def create_url(self, base_url: str) -> str: Returns ------- str - The full URL for the route. + The full URL for the _route. """ return base_url + self.compiled_path @@ -86,7 +86,7 @@ def create_real_bucket_hash(self, initial_bucket_hash: str) -> str: """Create a full bucket hash from a given initial hash. The result of this hash will be decided by the value of the major - parameters passed to the route during the compilation phase. + parameters passed to the _route during the compilation phase. Parameters ---------- @@ -98,7 +98,7 @@ def create_real_bucket_hash(self, initial_bucket_hash: str) -> str: ------- str The input hash amalgamated with a hash code produced by the - major parameters in this compiled route instance. + major parameters in this compiled _route instance. """ return initial_bucket_hash + HASH_SEPARATOR + self.major_param_hash @@ -172,19 +172,19 @@ def __init__(self, method: str, path_template: str) -> None: self.hash_code = hash((self.method, self.path_template)) def compile(self, **kwargs: typing.Any) -> CompiledRoute: - """Generate a formatted `CompiledRoute` for this route. + """Generate a formatted `CompiledRoute` for this _route. This takes into account any URL parameters that have been passed. Parameters ---------- **kwargs : typing.Any - Any parameters to interpolate into the route path. + Any parameters to interpolate into the _route path. Returns ------- CompiledRoute - The compiled route. + The compiled _route. """ return CompiledRoute( self, diff --git a/hikari/net/urls.py b/hikari/net/urls.py index 572a175429..c61de72b86 100644 --- a/hikari/net/urls.py +++ b/hikari/net/urls.py @@ -51,7 +51,7 @@ def generate_cdn_url(*route_parts: str, format_: str, size: typing.Optional[int] Parameters ---------- *route_parts : str - The string route parts that will be used to form the link. + The string _route parts that will be used to form the link. format_ : str The format to use for the wanted cdn entity, will usually be one of `webp`, `png`, `jpeg`, `jpg` or `gif` (which will be invalid diff --git a/tests/hikari/rest/test_buckets.py b/tests/hikari/rest/test_buckets.py index 2e51664336..372eb5beb7 100644 --- a/tests/hikari/rest/test_buckets.py +++ b/tests/hikari/rest/test_buckets.py @@ -222,7 +222,7 @@ async def test_acquire_route_when_not_in_routes_to_real_hashes_caches_route(self # noinspection PyAsyncCall mgr.acquire(route) - assert mgr.routes_to_hashes[route.route] == "UNKNOWN" + assert mgr.routes_to_hashes[route._route] == "UNKNOWN" @pytest.mark.asyncio async def test_acquire_route_when_route_cached_already_obtains_hash_from_route_and_bucket_from_hash(self): @@ -258,7 +258,7 @@ async def test_acquire_route_returns_acquired_future_for_new_bucket(self): route = mock.MagicMock() route.create_real_bucket_hash = mock.MagicMock(return_value="eat pant;bobs") bucket = mock.MagicMock() - mgr.routes_to_hashes[route.route] = "eat pant" + mgr.routes_to_hashes[route._route] = "eat pant" mgr.real_hashes_to_buckets["eat pant;bobs"] = bucket f = mgr.acquire(route) @@ -269,9 +269,9 @@ async def test_update_rate_limits_if_wrong_bucket_hash_reroutes_route(self): with buckets.RESTBucketManager() as mgr: route = mock.MagicMock() route.create_real_bucket_hash = mock.MagicMock(wraps=lambda intial_hash: intial_hash + ";bobs") - mgr.routes_to_hashes[route.route] = "123" + mgr.routes_to_hashes[route._route] = "123" mgr.update_rate_limits(route, "blep", 22, 23, datetime.datetime.now(), datetime.datetime.now()) - assert mgr.routes_to_hashes[route.route] == "blep" + assert mgr.routes_to_hashes[route._route] == "blep" assert isinstance(mgr.real_hashes_to_buckets["blep;bobs"], buckets.RESTBucket) @pytest.mark.asyncio @@ -279,11 +279,11 @@ async def test_update_rate_limits_if_right_bucket_hash_does_nothing_to_hash(self with buckets.RESTBucketManager() as mgr: route = mock.MagicMock() route.create_real_bucket_hash = mock.MagicMock(wraps=lambda intial_hash: intial_hash + ";bobs") - mgr.routes_to_hashes[route.route] = "123" + mgr.routes_to_hashes[route._route] = "123" bucket = mock.MagicMock() mgr.real_hashes_to_buckets["123;bobs"] = bucket mgr.update_rate_limits(route, "123", 22, 23, datetime.datetime.now(), datetime.datetime.now()) - assert mgr.routes_to_hashes[route.route] == "123" + assert mgr.routes_to_hashes[route._route] == "123" assert mgr.real_hashes_to_buckets["123;bobs"] is bucket @pytest.mark.asyncio @@ -291,7 +291,7 @@ async def test_update_rate_limits_updates_params(self): with buckets.RESTBucketManager() as mgr: route = mock.MagicMock() route.create_real_bucket_hash = mock.MagicMock(wraps=lambda intial_hash: intial_hash + ";bobs") - mgr.routes_to_hashes[route.route] = "123" + mgr.routes_to_hashes[route._route] = "123" bucket = mock.MagicMock() mgr.real_hashes_to_buckets["123;bobs"] = bucket date = datetime.datetime.now().replace(year=2004) diff --git a/tests/hikari/rest/test_channel.py b/tests/hikari/rest/test_channel.py index 6f9223876c..9bbfb2dec2 100644 --- a/tests/hikari/rest/test_channel.py +++ b/tests/hikari/rest/test_channel.py @@ -55,7 +55,7 @@ def message_cls(self): with mock.patch.object(messages, "Message") as message_cls: yield message_cls - @pytest.mark.parametrize("direction", ["before", "after", "around"]) + @pytest.mark.parametrize("_direction", ["before", "after", "around"]) def test_init_first_id_is_date(self, mock_session, mock_app, direction): date = datetime.datetime(2015, 11, 15, 23, 13, 46, 709000, tzinfo=datetime.timezone.utc) expected_id = 115590097100865536 @@ -67,7 +67,7 @@ def test_init_first_id_is_date(self, mock_session, mock_app, direction): assert pag._session is mock_session assert pag._app is mock_app - @pytest.mark.parametrize("direction", ["before", "after", "around"]) + @pytest.mark.parametrize("_direction", ["before", "after", "around"]) def test_init_first_id_is_id(self, mock_session, mock_app, direction): expected_id = 115590097100865536 channel_id = 1234567 @@ -78,7 +78,7 @@ def test_init_first_id_is_id(self, mock_session, mock_app, direction): assert pag._session is mock_session assert pag._app is mock_app - @pytest.mark.parametrize("direction", ["before", "after", "around"]) + @pytest.mark.parametrize("_direction", ["before", "after", "around"]) async def test_next_chunk_makes_api_call(self, mock_session, mock_app, message_cls, direction): channel_obj = mock.MagicMock(__int__=lambda _: 55) @@ -92,7 +92,7 @@ async def test_next_chunk_makes_api_call(self, mock_session, mock_app, message_c **{direction: "12345", "channel": "55", "limit": 100} ) - @pytest.mark.parametrize("direction", ["before", "after", "around"]) + @pytest.mark.parametrize("_direction", ["before", "after", "around"]) async def test_next_chunk_empty_response_returns_None(self, mock_session, mock_app, message_cls, direction): channel_obj = mock.MagicMock(__int__=lambda _: 55) @@ -103,7 +103,7 @@ async def test_next_chunk_empty_response_returns_None(self, mock_session, mock_a assert await pag._next_chunk() is None - @pytest.mark.parametrize(["direction", "expect_reverse"], [("before", False), ("after", True), ("around", False)]) + @pytest.mark.parametrize(["_direction", "expect_reverse"], [("before", False), ("after", True), ("around", False)]) async def test_next_chunk_updates_first_id(self, mock_session, mock_app, message_cls, expect_reverse, direction): return_payload = [ {"id": "1234", ...: ...}, @@ -123,7 +123,7 @@ async def test_next_chunk_updates_first_id(self, mock_session, mock_app, message assert pag._first_id == "1234" if expect_reverse else "512" - @pytest.mark.parametrize(["direction", "expect_reverse"], [("before", False), ("after", True), ("around", False)]) + @pytest.mark.parametrize(["_direction", "expect_reverse"], [("before", False), ("after", True), ("around", False)]) async def test_next_chunk_returns_generator(self, mock_session, mock_app, message_cls, expect_reverse, direction): return_payload = [ {"id": "1234", ...: ...}, @@ -276,7 +276,7 @@ async def test_delete_channel(self, rest_channel_logic_impl, channel): rest_channel_logic_impl._session.delete_close_channel.assert_called_once_with(channel_id="55555") @pytest.mark.parametrize( - ("direction", "expected_direction", "first", "expected_first"), + ("_direction", "expected_direction", "first", "expected_first"), [ [None, "before", None, bases.Snowflake.max()], ["before", "before", "1234", "1234"], diff --git a/tests/hikari/rest/test_me.py b/tests/hikari/rest/test_me.py index 3692931c98..222b90fb0b 100644 --- a/tests/hikari/rest/test_me.py +++ b/tests/hikari/rest/test_me.py @@ -65,7 +65,7 @@ def test_init_with_explicit_first_element(self, mock_app, mock_session): assert pag._app is mock_app assert pag._session is mock_session - @pytest.mark.parametrize(["newest_first", "direction"], [(True, "before"), (False, "after"),]) + @pytest.mark.parametrize(["newest_first", "_direction"], [(True, "before"), (False, "after"),]) @pytest.mark.asyncio async def test_next_chunk_performs_correct_api_call( self, mock_session, mock_app, newest_first, direction, ownguild_cls From 3d55890d6bfa0467fdfc25685322cf4174a4b63e Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Thu, 21 May 2020 15:11:04 +0100 Subject: [PATCH 370/922] Added more endpoints. --- hikari/.rest/channel.py | 20 +- hikari/.rest/guild.py | 64 ++-- hikari/.rest/webhook.py | 6 +- hikari/cache.py | 10 +- hikari/entity_factory.py | 2 +- hikari/events/guild.py | 6 +- hikari/impl/cache.py | 10 +- hikari/internal/conversions.py | 78 ++-- hikari/internal/helpers.py | 4 +- hikari/models/audit_logs.py | 4 +- hikari/models/channels.py | 12 +- hikari/models/files.py | 65 +++- hikari/models/guilds.py | 16 +- hikari/models/messages.py | 12 +- hikari/models/unset.py | 3 + hikari/models/webhooks.py | 6 +- hikari/net/rest.py | 478 +++++++++++++++++++++++-- hikari/net/routes.py | 140 ++++---- hikari/pagination.py | 36 -- tests/hikari/events/test_guild.py | 8 +- tests/hikari/internal/test_helpers.py | 4 +- tests/hikari/models/test_audit_logs.py | 32 +- tests/hikari/models/test_guilds.py | 10 +- tests/hikari/rest/test_channel.py | 4 +- tests/hikari/rest/test_guild.py | 62 ++-- 25 files changed, 768 insertions(+), 324 deletions(-) diff --git a/hikari/.rest/channel.py b/hikari/.rest/channel.py index ac86180901..84897ef38e 100644 --- a/hikari/.rest/channel.py +++ b/hikari/.rest/channel.py @@ -449,7 +449,7 @@ async def create_message( # pylint: disable=line-too-long typing.Collection[typing.Union[bases.Snowflake, int, str, users.User]], bool ] = True, role_mentions: typing.Union[ - typing.Collection[typing.Union[bases.Snowflake, int, str, guilds.GuildRole]], bool + typing.Collection[typing.Union[bases.Snowflake, int, str, guilds.Role]], bool ] = True, ) -> messages_.Message: """Create a message in the given channel. @@ -479,7 +479,7 @@ async def create_message( # pylint: disable=line-too-long Either an array of user objects/IDs to allow mentions for, `True` to allow all user mentions or `False` to block all user mentions from resolving, defaults to `True`. - role_mentions : typing.Collection[hikari.models.guilds.GuildRole | hikari.models.bases.Snowflake | int | str] | bool + role_mentions : typing.Collection[hikari.models.guilds.Role | hikari.models.bases.Snowflake | int | str] | bool Either an array of guild role objects/IDs to allow mentions for, `True` to allow all role mentions or `False` to block all role mentions from resolving, defaults to `True`. @@ -541,7 +541,7 @@ def safe_create_message( typing.Collection[typing.Union[bases.Snowflake, int, str, users.User]], bool ] = False, role_mentions: typing.Union[ - typing.Collection[typing.Union[bases.Snowflake, int, str, guilds.GuildRole]], bool + typing.Collection[typing.Union[bases.Snowflake, int, str, guilds.Role]], bool ] = False, ) -> more_typing.Coroutine[messages_.Message]: """Create a message in the given channel with mention safety. @@ -576,7 +576,7 @@ async def update_message( # pylint: disable=line-too-long typing.Collection[typing.Union[bases.Snowflake, int, str, users.User]], bool ] = True, role_mentions: typing.Union[ - typing.Collection[typing.Union[bases.Snowflake, int, str, guilds.GuildRole]], bool + typing.Collection[typing.Union[bases.Snowflake, int, str, guilds.Role]], bool ] = True, ) -> messages_.Message: """Update the given message. @@ -604,7 +604,7 @@ async def update_message( # pylint: disable=line-too-long Either an array of user objects/IDs to allow mentions for, `True` to allow all user mentions or `False` to block all user mentions from resolving, defaults to `True`. - role_mentions : typing.Collection[hikari.models.guilds.GuildRole | hikari.models.bases.Snowflake | int | str] | bool + role_mentions : typing.Collection[hikari.models.guilds.Role | hikari.models.bases.Snowflake | int | str] | bool Either an array of guild role objects/IDs to allow mentions for, `True` to allow all role mentions or `False` to block all role mentions from resolving, defaults to `True`. @@ -659,7 +659,7 @@ def safe_update_message( typing.Collection[typing.Union[bases.Snowflake, int, str, users.User]], bool ] = False, role_mentions: typing.Union[ - typing.Collection[typing.Union[bases.Snowflake, int, str, guilds.GuildRole]], bool + typing.Collection[typing.Union[bases.Snowflake, int, str, guilds.Role]], bool ] = False, ) -> more_typing.Coroutine[messages_.Message]: """Update a message in the given channel with mention safety. @@ -744,7 +744,7 @@ async def delete_messages( async def update_channel_overwrite( # pylint: disable=line-too-long self, channel: typing.Union[bases.Snowflake, int, str, channels_.PartialChannel], - overwrite: typing.Union[channels_.PermissionOverwrite, users.User, guilds.GuildRole, bases.Snowflake, int], + overwrite: typing.Union[channels_.PermissionOverwrite, users.User, guilds.Role, bases.Snowflake, int], target_type: typing.Union[channels_.PermissionOverwriteType, str], *, allow: typing.Union[permissions_.Permission, int] = ..., @@ -757,7 +757,7 @@ async def update_channel_overwrite( # pylint: disable=line-too-long ---------- channel : hikari.models.channels.PartialChannel | hikari.models.bases.Snowflake | int | str The object or ID of the channel to edit permissions for. - overwrite : hikari.models.channels.PermissionOverwrite | hikari.models.guilds.GuildRole | hikari.models.users.User | hikari.models.bases.Snowflake | int + overwrite : hikari.models.channels.PermissionOverwrite | hikari.models.guilds.Role | hikari.models.users.User | hikari.models.bases.Snowflake | int The object or ID of the target member or role to create/edit the overwrite for. target_type : hikari.models.channels.PermissionOverwriteType | int @@ -898,7 +898,7 @@ async def create_invite_for_channel( async def delete_channel_overwrite( # pylint: disable=line-too-long self, channel: typing.Union[bases.Snowflake, int, str, channels_.PartialChannel], - overwrite: typing.Union[channels_.PermissionOverwrite, guilds.GuildRole, users.User, bases.Snowflake, int], + overwrite: typing.Union[channels_.PermissionOverwrite, guilds.Role, users.User, bases.Snowflake, int], ) -> None: """Delete a channel permission overwrite for a user or a role. @@ -906,7 +906,7 @@ async def delete_channel_overwrite( # pylint: disable=line-too-long ---------- channel : hikari.models.channels.PartialChannel | hikari.models.bases.Snowflake | int | str The object or ID of the channel to delete the overwrite from. - overwrite : hikari.models.channels.PermissionOverwrite | hikari.models.guilds.GuildRole | hikari.models.users.User| hikari.models.bases.Snowflake | int + overwrite : hikari.models.channels.PermissionOverwrite | hikari.models.guilds.Role | hikari.models.users.User| hikari.models.bases.Snowflake | int The ID of the entity this overwrite targets. Raises diff --git a/hikari/.rest/guild.py b/hikari/.rest/guild.py index ed444ffe29..7b006cad3d 100644 --- a/hikari/.rest/guild.py +++ b/hikari/.rest/guild.py @@ -262,11 +262,11 @@ async def fetch_guild_emojis( async def create_guild_emoji( self, - guild: typing.Union[bases.Snowflake, int, str, guilds.GuildRole], + guild: typing.Union[bases.Snowflake, int, str, guilds.Role], name: str, image: files.BaseStream, *, - roles: typing.Sequence[typing.Union[bases.Snowflake, int, str, guilds.GuildRole]] = ..., + roles: typing.Sequence[typing.Union[bases.Snowflake, int, str, guilds.Role]] = ..., reason: str = ..., ) -> emojis.KnownCustomEmoji: """Create a new emoji for a given guild. @@ -279,7 +279,7 @@ async def create_guild_emoji( The new emoji's name. image : hikari.models.files.BaseStream The `128x128` image data. - roles : typing.Sequence[hikari.models.guilds.GuildRole | hikari.models.bases.Snowflake | int | str] + roles : typing.Sequence[hikari.models.guilds.Role | hikari.models.bases.Snowflake | int | str] If specified, a list of role objects or IDs for which the emoji will be whitelisted. If empty, all roles are whitelisted. reason : str @@ -323,7 +323,7 @@ async def update_guild_emoji( emoji: typing.Union[bases.Snowflake, int, str, emojis.KnownCustomEmoji], *, name: str = ..., - roles: typing.Sequence[typing.Union[bases.Snowflake, int, str, guilds.GuildRole]] = ..., + roles: typing.Sequence[typing.Union[bases.Snowflake, int, str, guilds.Role]] = ..., reason: str = ..., ) -> emojis.KnownCustomEmoji: """Edits an emoji of a given guild. @@ -337,7 +337,7 @@ async def update_guild_emoji( name : str If specified, a new emoji name string. Keep unspecified to leave the name unchanged. - roles : typing.Sequence[hikari.models.guilds.GuildRole | hikari.models.bases.Snowflake | int | str] + roles : typing.Sequence[hikari.models.guilds.Role | hikari.models.bases.Snowflake | int | str] If specified, a list of objects or IDs for the new whitelisted roles. Set to an empty list to whitelist all roles. Keep unspecified to leave the same roles already set. @@ -411,7 +411,7 @@ async def create_guild( verification_level: typing.Union[guilds.GuildVerificationLevel, int] = ..., default_message_notifications: typing.Union[guilds.GuildMessageNotificationsLevel, int] = ..., explicit_content_filter: typing.Union[guilds.GuildExplicitContentFilterLevel, int] = ..., - roles: typing.Sequence[guilds.GuildRole] = ..., + roles: typing.Sequence[guilds.Role] = ..., channels: typing.Sequence[channels_.GuildChannelBuilder] = ..., ) -> guilds.Guild: """Create a new guild. @@ -438,7 +438,7 @@ async def create_guild( explicit_content_filter : hikari.models.guilds.GuildExplicitContentFilterLevel | int If specified, the explicit content filter. Passing a raw int for this may lead to unexpected behaviour. - roles : typing.Sequence[hikari.models.guilds.GuildRole] + roles : typing.Sequence[hikari.models.guilds.Role] If specified, an array of role objects to be created alongside the guild. First element changes the `@everyone` role. channels : typing.Sequence[hikari.models.channels.GuildChannelBuilder] @@ -898,7 +898,7 @@ async def update_member( # pylint: disable=too-many-arguments guild: typing.Union[bases.Snowflake, int, str, guilds.Guild], user: typing.Union[bases.Snowflake, int, str, users.User], nickname: typing.Optional[str] = ..., - roles: typing.Sequence[typing.Union[bases.Snowflake, int, str, guilds.GuildRole]] = ..., + roles: typing.Sequence[typing.Union[bases.Snowflake, int, str, guilds.Role]] = ..., mute: bool = ..., deaf: bool = ..., voice_channel: typing.Optional[typing.Union[bases.Snowflake, int, str, channels_.GuildVoiceChannel]] = ..., @@ -915,7 +915,7 @@ async def update_member( # pylint: disable=too-many-arguments nickname : str | None If specified, the new nickname string. Setting it to `None` explicitly will clear the nickname. - roles : typing.Sequence[hikari.models.guilds.GuildRole | hikari.models.bases.Snowflake | int | str] + roles : typing.Sequence[hikari.models.guilds.Role | hikari.models.bases.Snowflake | int | str] If specified, a list of role IDs the member should have. mute : bool If specified, whether the user should be muted in the voice channel @@ -1005,7 +1005,7 @@ async def add_role_to_member( self, guild: typing.Union[bases.Snowflake, int, str, guilds.Guild], user: typing.Union[bases.Snowflake, int, str, users.User], - role: typing.Union[bases.Snowflake, int, str, guilds.GuildRole], + role: typing.Union[bases.Snowflake, int, str, guilds.Role], *, reason: str = ..., ) -> None: @@ -1017,7 +1017,7 @@ async def add_role_to_member( The object or ID of the guild the member belongs to. user : hikari.models.users.User | hikari.models.bases.Snowflake | int | str The object or ID of the member you want to add the role to. - role : hikari.models.guilds.GuildRole | hikari.models.bases.Snowflake | int | str + role : hikari.models.guilds.Role | hikari.models.bases.Snowflake | int | str The object or ID of the role you want to add. reason : str If specified, the audit log reason explaining why the operation @@ -1044,7 +1044,7 @@ async def remove_role_from_member( self, guild: typing.Union[bases.Snowflake, int, str, guilds.Guild], user: typing.Union[bases.Snowflake, int, str, users.User], - role: typing.Union[bases.Snowflake, int, str, guilds.GuildRole], + role: typing.Union[bases.Snowflake, int, str, guilds.Role], *, reason: str = ..., ) -> None: @@ -1056,7 +1056,7 @@ async def remove_role_from_member( The object or ID of the guild the member belongs to. user : hikari.models.users.User | hikari.models.bases.Snowflake | int | str The object or ID of the member you want to remove the role from. - role : hikari.models.guilds.GuildRole | hikari.models.bases.Snowflake | int | str + role : hikari.models.guilds.Role | hikari.models.bases.Snowflake | int | str The object or ID of the role you want to remove. reason : str If specified, the audit log reason explaining why the operation @@ -1259,7 +1259,7 @@ async def unban_member( async def fetch_roles( self, guild: typing.Union[bases.Snowflake, int, str, guilds.Guild], - ) -> typing.Mapping[bases.Snowflake, guilds.GuildRole]: + ) -> typing.Mapping[bases.Snowflake, guilds.Role]: """Get the roles for a given guild. Parameters @@ -1269,7 +1269,7 @@ async def fetch_roles( Returns ------- - typing.Mapping[hikari.models.bases.Snowflake, hikari.models.guilds.GuildRole] + typing.Mapping[hikari.models.bases.Snowflake, hikari.models.guilds.Role] A list of role objects. Raises @@ -1285,7 +1285,7 @@ async def fetch_roles( payload = await self._session.get_guild_roles( guild_id=str(guild.id if isinstance(guild, bases.Unique) else int(guild)) ) - return {bases.Snowflake(role["id"]): guilds.GuildRole.deserialize(role, app=self._app) for role in payload} + return {bases.Snowflake(role["id"]): guilds.Role.deserialize(role, app=self._app) for role in payload} async def create_role( self, @@ -1297,7 +1297,7 @@ async def create_role( hoist: bool = ..., mentionable: bool = ..., reason: str = ..., - ) -> guilds.GuildRole: + ) -> guilds.Role: """Create a new role for a given guild. Parameters @@ -1322,7 +1322,7 @@ async def create_role( Returns ------- - hikari.models.guilds.GuildRole + hikari.models.guilds.Role The newly created role object. Raises @@ -1346,21 +1346,21 @@ async def create_role( mentionable=mentionable, reason=reason, ) - return guilds.GuildRole.deserialize(payload, app=self._app) + return guilds.Role.deserialize(payload, app=self._app) async def reposition_roles( self, guild: typing.Union[bases.Snowflake, int, str, guilds.Guild], - role: typing.Tuple[int, typing.Union[bases.Snowflake, int, str, guilds.GuildRole]], - *additional_roles: typing.Tuple[int, typing.Union[bases.Snowflake, int, str, guilds.GuildRole]], - ) -> typing.Sequence[guilds.GuildRole]: + role: typing.Tuple[int, typing.Union[bases.Snowflake, int, str, guilds.Role]], + *additional_roles: typing.Tuple[int, typing.Union[bases.Snowflake, int, str, guilds.Role]], + ) -> typing.Sequence[guilds.Role]: """Edits the position of two or more roles in a given guild. Parameters ---------- guild : hikari.models.guilds.Guild | hikari.models.bases.Snowflake | int | str The ID of the guild the roles belong to. - role : typing.Tuple[int, hikari.models.guilds.GuildRole | hikari.models.bases.Snowflake | int | str] + role : typing.Tuple[int, hikari.models.guilds.Role | hikari.models.bases.Snowflake | int | str] The first role to move. This is a tuple of the integer position and the role object or ID. *additional_roles : typing.Tuple[int, thikari.guilds.GuildRole | hikari.models.bases.Snowflake | int | str] @@ -1369,7 +1369,7 @@ async def reposition_roles( Returns ------- - typing.Sequence[hikari.models.guilds.GuildRole] + typing.Sequence[hikari.models.guilds.Role] A list of all the guild roles. Raises @@ -1391,12 +1391,12 @@ async def reposition_roles( for position, channel in [role, *additional_roles] ], ) - return [guilds.GuildRole.deserialize(role, app=self._app) for role in payload] + return [guilds.Role.deserialize(role, app=self._app) for role in payload] async def update_role( self, guild: typing.Union[bases.Snowflake, int, str, guilds.Guild], - role: typing.Union[bases.Snowflake, int, str, guilds.GuildRole], + role: typing.Union[bases.Snowflake, int, str, guilds.Role], *, name: str = ..., permissions: typing.Union[permissions_.Permission, int] = ..., @@ -1404,14 +1404,14 @@ async def update_role( hoist: bool = ..., mentionable: bool = ..., reason: str = ..., - ) -> guilds.GuildRole: + ) -> guilds.Role: """Edits a role in a given guild. Parameters ---------- guild : hikari.models.guilds.Guild | hikari.models.bases.Snowflake | int | str The object or ID of the guild the role belong to. - role : hikari.models.guilds.GuildRole | hikari.models.bases.Snowflake | int | str + role : hikari.models.guilds.Role | hikari.models.bases.Snowflake | int | str The object or ID of the role you want to edit. name : str If specified, the new role's name string. @@ -1431,7 +1431,7 @@ async def update_role( Returns ------- - hikari.models.guilds.GuildRole + hikari.models.guilds.Role The edited role object. Raises @@ -1456,12 +1456,12 @@ async def update_role( mentionable=mentionable, reason=reason, ) - return guilds.GuildRole.deserialize(payload, app=self._app) + return guilds.Role.deserialize(payload, app=self._app) async def delete_role( self, guild: typing.Union[bases.Snowflake, int, str, guilds.Guild], - role: typing.Union[bases.Snowflake, int, str, guilds.GuildRole], + role: typing.Union[bases.Snowflake, int, str, guilds.Role], ) -> None: """Delete a role from a given guild. @@ -1469,7 +1469,7 @@ async def delete_role( ---------- guild : hikari.models.guilds.Guild | hikari.models.bases.Snowflake | int | str The object or ID of the guild you want to remove the role from. - role : hikari.models.guilds.GuildRole | hikari.models.bases.Snowflake | int | str + role : hikari.models.guilds.Role | hikari.models.bases.Snowflake | int | str The object or ID of the role you want to delete. Raises diff --git a/hikari/.rest/webhook.py b/hikari/.rest/webhook.py index ae2778ce78..5370ae3bf2 100644 --- a/hikari/.rest/webhook.py +++ b/hikari/.rest/webhook.py @@ -190,7 +190,7 @@ async def execute_webhook( # pylint:disable=too-many-locals,line-too-long typing.Collection[typing.Union[bases.Snowflake, int, str, users.User]], bool ] = True, role_mentions: typing.Union[ - typing.Collection[typing.Union[bases.Snowflake, int, str, guilds.GuildRole]], bool + typing.Collection[typing.Union[bases.Snowflake, int, str, guilds.Role]], bool ] = True, ) -> typing.Optional[messages_.Message]: """Execute a webhook to create a message. @@ -226,7 +226,7 @@ async def execute_webhook( # pylint:disable=too-many-locals,line-too-long Either an array of user objects/IDs to allow mentions for, `True` to allow all user mentions or `False` to block all user mentions from resolving, defaults to `True`. - role_mentions : typing.Collection[hikari.models.guilds.GuildRole | hikari.models.bases.Snowflake | int | str] | bool + role_mentions : typing.Collection[hikari.models.guilds.Role | hikari.models.bases.Snowflake | int | str] | bool Either an array of guild role objects/IDs to allow mentions for, `True` to allow all role mentions or `False` to block all role mentions from resolving, defaults to `True`. @@ -295,7 +295,7 @@ def safe_webhook_execute( typing.Collection[typing.Union[bases.Snowflake, int, str, users.User]], bool ] = False, role_mentions: typing.Union[ - typing.Collection[typing.Union[bases.Snowflake, int, str, guilds.GuildRole]], bool + typing.Collection[typing.Union[bases.Snowflake, int, str, guilds.Role]], bool ] = False, ) -> typing.Coroutine[typing.Any, typing.Any, typing.Optional[messages_.Message]]: """Execute a webhook to create a message with mention safety. diff --git a/hikari/cache.py b/hikari/cache.py index 3453a6ba12..749e743602 100644 --- a/hikari/cache.py +++ b/hikari/cache.py @@ -159,21 +159,19 @@ async def delete_member(self, guild_id: int, user_id: int) -> typing.Optional[gu ... @abc.abstractmethod - async def create_role(self, payload: more_typing.JSONObject, can_cache: bool = False) -> guilds.PartialGuildRole: + async def create_role(self, payload: more_typing.JSONObject, can_cache: bool = False) -> guilds.PartialRole: ... @abc.abstractmethod - async def update_role( - self, role: guilds.PartialGuildRole, payload: more_typing.JSONObject - ) -> guilds.PartialGuildRole: + async def update_role(self, role: guilds.PartialRole, payload: more_typing.JSONObject) -> guilds.PartialRole: ... @abc.abstractmethod - async def get_role(self, guild_id: int, role_id: int) -> typing.Optional[guilds.PartialGuildRole]: + async def get_role(self, guild_id: int, role_id: int) -> typing.Optional[guilds.PartialRole]: ... @abc.abstractmethod - async def delete_role(self, guild_id: int, role_id: int) -> typing.Optional[guilds.PartialGuildRole]: + async def delete_role(self, guild_id: int, role_id: int) -> typing.Optional[guilds.PartialRole]: ... @abc.abstractmethod diff --git a/hikari/entity_factory.py b/hikari/entity_factory.py index 5aed3ba1b2..b11bb7c74b 100644 --- a/hikari/entity_factory.py +++ b/hikari/entity_factory.py @@ -24,6 +24,7 @@ import abc import typing + if typing.TYPE_CHECKING: from hikari.internal import more_typing from hikari.models import applications @@ -225,7 +226,6 @@ def deserialize_invite_with_metadata(self, payload: more_typing.JSONObject) -> i # MESSAGES # ############ - @abc.abstractmethod def deserialize_message(self, payload: more_typing.JSONObject) -> messages.Message: ... diff --git a/hikari/events/guild.py b/hikari/events/guild.py index 6f2fb49264..d62bac166b 100644 --- a/hikari/events/guild.py +++ b/hikari/events/guild.py @@ -239,7 +239,7 @@ class GuildRoleCreateEvent(base_events.HikariEvent, marshaller.Deserializable): guild_id: base_models.Snowflake = marshaller.attrib(deserializer=base_models.Snowflake, repr=True) """The ID of the guild where this role was created.""" - role: guilds.GuildRole = marshaller.attrib(deserializer=guilds.GuildRole.deserialize, inherit_kwargs=True) + role: guilds.Role = marshaller.attrib(deserializer=guilds.Role.deserialize, inherit_kwargs=True) """The object of the role that was created.""" @@ -254,9 +254,7 @@ class GuildRoleUpdateEvent(base_events.HikariEvent, marshaller.Deserializable): guild_id: base_models.Snowflake = marshaller.attrib(deserializer=base_models.Snowflake, repr=True) """The ID of the guild where this role was updated.""" - role: guilds.GuildRole = marshaller.attrib( - deserializer=guilds.GuildRole.deserialize, inherit_kwargs=True, repr=True - ) + role: guilds.Role = marshaller.attrib(deserializer=guilds.Role.deserialize, inherit_kwargs=True, repr=True) """The updated role object.""" diff --git a/hikari/impl/cache.py b/hikari/impl/cache.py index 286a9f843a..37f1541268 100644 --- a/hikari/impl/cache.py +++ b/hikari/impl/cache.py @@ -99,18 +99,16 @@ async def get_member(self, guild_id: int, user_id: int) -> typing.Optional[guild async def delete_member(self, guild_id: int, user_id: int) -> typing.Optional[guilds.GuildMember]: pass - async def create_role(self, payload: more_typing.JSONObject, can_cache: bool = False) -> guilds.PartialGuildRole: + async def create_role(self, payload: more_typing.JSONObject, can_cache: bool = False) -> guilds.PartialRole: pass - async def update_role( - self, role: guilds.PartialGuildRole, payload: more_typing.JSONObject - ) -> guilds.PartialGuildRole: + async def update_role(self, role: guilds.PartialRole, payload: more_typing.JSONObject) -> guilds.PartialRole: pass - async def get_role(self, guild_id: int, role_id: int) -> typing.Optional[guilds.PartialGuildRole]: + async def get_role(self, guild_id: int, role_id: int) -> typing.Optional[guilds.PartialRole]: pass - async def delete_role(self, guild_id: int, role_id: int) -> typing.Optional[guilds.PartialGuildRole]: + async def delete_role(self, guild_id: int, role_id: int) -> typing.Optional[guilds.PartialRole]: pass async def create_presence( diff --git a/hikari/internal/conversions.py b/hikari/internal/conversions.py index d424da3643..e7963aa4c8 100644 --- a/hikari/internal/conversions.py +++ b/hikari/internal/conversions.py @@ -25,7 +25,6 @@ "try_cast", "try_cast_or_defer_unary_operator", "put_if_specified", - "image_bytes_to_image_data", "parse_http_date", "parse_iso_8601_ts", "discord_epoch_to_datetime", @@ -33,15 +32,14 @@ "pluralize", "resolve_signature", "EMPTY", + "cast_to_str_id", + "timespan_as_int", ] -import base64 import contextlib import datetime import email.utils -import functools import inspect -import operator import re import typing @@ -124,47 +122,6 @@ def put_if_specified( mapping[key] = value -def image_bytes_to_image_data(img_bytes: typing.Optional[bytes] = None, /) -> typing.Optional[str]: - """Encode image bytes into an image data string. - - Parameters - ---------- - img_bytes : bytes | None - The image bytes. - - Raises - ------ - ValueError - If the image type passed is not supported. - - Returns - ------- - str | None - The `image_bytes` given encoded into an image data string or - `None`. - - !!! note - Supported image types: `.png`, `.jpeg`, `.jfif`, `.gif`, `.webp` - """ - if img_bytes is None: - return None - - if img_bytes[:8] == b"\211PNG\r\n\032\n": - img_type = "image/png" - elif img_bytes[6:10] in (b"Exif", b"JFIF"): - img_type = "image/jpeg" - elif img_bytes[:6] in (b"GIF87a", b"GIF89a"): - img_type = "image/gif" - elif img_bytes.startswith(b"RIFF") and img_bytes[8:12] == b"WEBP": - img_type = "image/webp" - else: - raise ValueError("Unsupported image type passed") - - image_data = base64.b64encode(img_bytes).decode() - - return f"data:{img_type};base64,{image_data}" - - def parse_http_date(date_str: str, /) -> datetime.datetime: """Return the HTTP date as a datetime object. @@ -315,4 +272,35 @@ def resolve_signature(func: typing.Callable) -> inspect.Signature: def cast_to_str_id(value: typing.Union[typing.SupportsInt, int]) -> str: + """Cast the given object to an int and return the result as a string. + + Parameters + ---------- + value : typing.SupportsInt | int + A value that can be cast to an `int`. + + Returns + ------- + str + The string representation of the integer value. + """ return str(int(value)) + + +def timespan_as_int(value: typing.Union[int, datetime.timedelta, float]) -> int: + """Cast the given timespan in seconds to an integer value. + + Parameters + ---------- + value : int | float | datetime.timedelta + The number of seconds. + + Returns + ------- + int + The integer number of seconds. Fractions are discarded. Negative values + are removed. + """ + if isinstance(value, datetime.timedelta): + value = value.total_seconds() + return int(max(0, value)) diff --git a/hikari/internal/helpers.py b/hikari/internal/helpers.py index 9d4288ee96..b94f0bd9ce 100644 --- a/hikari/internal/helpers.py +++ b/hikari/internal/helpers.py @@ -41,7 +41,7 @@ def get_logger(cls: typing.Union[typing.Type, typing.Any], *additional_args: str def generate_allowed_mentions( # pylint:disable=line-too-long mentions_everyone: bool, user_mentions: typing.Union[typing.Collection[typing.Union[bases.Snowflake, int, str, users.User]], bool], - role_mentions: typing.Union[typing.Collection[typing.Union[bases.Snowflake, int, str, guilds.GuildRole]], bool], + role_mentions: typing.Union[typing.Collection[typing.Union[bases.Snowflake, int, str, guilds.Role]], bool], ) -> typing.Dict[str, typing.Sequence[str]]: """Generate an allowed mentions object based on input mention rules. @@ -54,7 +54,7 @@ def generate_allowed_mentions( # pylint:disable=line-too-long Either an array of user objects/IDs to allow mentions for, `True` to allow all user mentions or `False` to block all user mentions from resolving. - role_mentions : typing.Collection[hikari.models.guilds.GuildRole | hikari.models.bases.Snowflake | int | str] | bool + role_mentions : typing.Collection[hikari.models.guilds.Role | hikari.models.bases.Snowflake | int | str] | bool Either an array of guild role objects/IDs to allow mentions for, `True` to allow all role mentions or `False` to block all role mentions from resolving. diff --git a/hikari/models/audit_logs.py b/hikari/models/audit_logs.py index 70a8e92d99..abe6b0d195 100644 --- a/hikari/models/audit_logs.py +++ b/hikari/models/audit_logs.py @@ -133,8 +133,8 @@ def _deserialize_seconds_timedelta(seconds: typing.Union[str, int]) -> datetime. def _deserialize_partial_roles( payload: more_typing.JSONArray, **kwargs: typing.Any -) -> typing.Mapping[bases.Snowflake, guilds.GuildRole]: - return {bases.Snowflake(role["id"]): guilds.PartialGuildRole.deserialize(role, **kwargs) for role in payload} +) -> typing.Mapping[bases.Snowflake, guilds.Role]: + return {bases.Snowflake(role["id"]): guilds.PartialRole.deserialize(role, **kwargs) for role in payload} def _deserialize_day_timedelta(days: typing.Union[str, int]) -> datetime.timedelta: diff --git a/hikari/models/channels.py b/hikari/models/channels.py index e1c23d0f9f..e2d527bb38 100644 --- a/hikari/models/channels.py +++ b/hikari/models/channels.py @@ -25,6 +25,7 @@ "PermissionOverwrite", "PermissionOverwriteType", "PartialChannel", + "TextChannel", "DMChannel", "GroupDMChannel", "GuildCategory", @@ -147,6 +148,11 @@ def decorator(cls): return decorator +class TextChannel: + # This is a mixin, do not add slotted fields. + __slots__ = () + + @marshaller.marshallable() @attr.s(eq=True, hash=True, kw_only=True, slots=True) class PartialChannel(bases.Unique, marshaller.Deserializable): @@ -171,7 +177,7 @@ def _deserialize_recipients(payload: more_typing.JSONArray, **kwargs: typing.Any @register_channel_type(ChannelType.DM) @marshaller.marshallable() @attr.s(eq=True, hash=True, kw_only=True, slots=True) -class DMChannel(PartialChannel): +class DMChannel(PartialChannel, TextChannel): """Represents a DM channel.""" last_message_id: typing.Optional[bases.Snowflake] = marshaller.attrib( @@ -299,7 +305,7 @@ def _deserialize_rate_limit_per_user(payload: int) -> datetime.timedelta: @register_channel_type(ChannelType.GUILD_TEXT) @marshaller.marshallable() @attr.s(eq=True, hash=True, kw_only=True, slots=True) -class GuildTextChannel(GuildChannel): +class GuildTextChannel(GuildChannel, TextChannel): """Represents a guild text channel.""" topic: typing.Optional[str] = marshaller.attrib(deserializer=str, if_none=None, eq=False, hash=False) @@ -337,7 +343,7 @@ class GuildTextChannel(GuildChannel): @register_channel_type(ChannelType.GUILD_NEWS) @marshaller.marshallable() @attr.s(eq=True, hash=True, slots=True, kw_only=True) -class GuildNewsChannel(GuildChannel): +class GuildNewsChannel(GuildChannel, TextChannel): """Represents an news channel.""" topic: typing.Optional[str] = marshaller.attrib(deserializer=str, if_none=None, eq=False, hash=False) diff --git a/hikari/models/files.py b/hikari/models/files.py index e69d335b61..7bd9cd1e74 100644 --- a/hikari/models/files.py +++ b/hikari/models/files.py @@ -56,11 +56,13 @@ import abc import asyncio +import base64 import concurrent.futures import functools import http import inspect import io +import math import os import typing @@ -111,12 +113,65 @@ def filename(self) -> str: def __repr__(self) -> str: return f"{type(self).__name__}(filename={self.filename!r})" - async def read(self) -> bytes: - """Return the entire contents of the data stream.""" - data = io.BytesIO() + async def fetch_data_uri(self) -> str: + """Generate a data URI for the given resource. + + This will only work for select image types that Discord supports. + + Returns + ------- + str + A base-64 encoded data URI. + + Raises + ------ + TypeError + If the data format is not + """ + buff = await self.read(20) + + if buff[:8] == b"\211PNG\r\n\032\n": + img_type = "image/png" + elif buff[6:10] in (b"Exif", b"JFIF"): + img_type = "image/jpeg" + elif buff[:6] in (b"GIF87a", b"GIF89a"): + img_type = "image/gif" + elif buff.startswith(b"RIFF") and buff[8:12] == b"WEBP": + img_type = "image/webp" + else: + raise TypeError("Unsupported image type passed") + + image_data = base64.b64encode(buff).decode() + + return f"data:{img_type};base64,{image_data}" + + async def read(self, count: int = -1) -> bytes: + """Read from the data stream. + + Parameters + ---------- + count : int + The max number of bytes to read. If unspecified, the entire file + will be read. If the count is larger than the number of bytes in + the entire stream, then the entire stream will be returned. + + Returns + ------- + bytes + The bytes that were read. + """ + if count == -1: + count = float("inf") + + data = bytearray() async for chunk in self: - data.write(chunk) - return data.getvalue() + if len(data) >= count: + break + + data.extend(chunk) + + count = len(data) if math.isinf(count) else count + return data[:count] class ByteStream(BaseStream): diff --git a/hikari/models/guilds.py b/hikari/models/guilds.py index 60a8da86b3..a800388523 100644 --- a/hikari/models/guilds.py +++ b/hikari/models/guilds.py @@ -30,7 +30,7 @@ "ClientStatus", "Guild", "GuildEmbed", - "GuildRole", + "Role", "GuildFeature", "GuildSystemChannelFlag", "GuildMessageNotificationsLevel", @@ -47,7 +47,7 @@ "IntegrationExpireBehaviour", "PartialGuild", "PartialGuildIntegration", - "PartialGuildRole", + "PartialRole", "PresenceActivity", "PresenceStatus", "PresenceUser", @@ -285,7 +285,7 @@ class GuildMember(bases.Entity, marshaller.Deserializable): @marshaller.marshallable() @attr.s(eq=True, hash=True, kw_only=True, slots=True) -class PartialGuildRole(bases.Unique, marshaller.Deserializable): +class PartialRole(bases.Unique, marshaller.Deserializable): """Represents a partial guild bound Role object.""" name: str = marshaller.attrib(deserializer=str, serializer=str, eq=False, hash=False, repr=True) @@ -294,7 +294,7 @@ class PartialGuildRole(bases.Unique, marshaller.Deserializable): @marshaller.marshallable() @attr.s(eq=True, hash=True, kw_only=True, slots=True) -class GuildRole(PartialGuildRole, marshaller.Serializable): +class Role(PartialRole, marshaller.Serializable): """Represents a guild bound Role object.""" color: colors.Color = marshaller.attrib( @@ -1048,10 +1048,8 @@ def _deserialize_afk_timeout(payload: int) -> datetime.timedelta: return datetime.timedelta(seconds=payload) -def _deserialize_roles( - payload: more_typing.JSONArray, **kwargs: typing.Any -) -> typing.Mapping[bases.Snowflake, GuildRole]: - return {bases.Snowflake(role["id"]): GuildRole.deserialize(role, **kwargs) for role in payload} +def _deserialize_roles(payload: more_typing.JSONArray, **kwargs: typing.Any) -> typing.Mapping[bases.Snowflake, Role]: + return {bases.Snowflake(role["id"]): Role.deserialize(role, **kwargs) for role in payload} def _deserialize_members( @@ -1173,7 +1171,7 @@ class Guild(PartialGuild): # pylint:disable=too-many-instance-attributes ) """The setting for the explicit content filter in this guild.""" - roles: typing.Mapping[bases.Snowflake, GuildRole] = marshaller.attrib( + roles: typing.Mapping[bases.Snowflake, Role] = marshaller.attrib( deserializer=_deserialize_roles, inherit_kwargs=True, eq=False, hash=False, ) """The roles in this guild, represented as a mapping of ID to role object.""" diff --git a/hikari/models/messages.py b/hikari/models/messages.py index 0a23e34f95..82bea823f3 100644 --- a/hikari/models/messages.py +++ b/hikari/models/messages.py @@ -421,7 +421,7 @@ async def edit( # pylint:disable=line-too-long typing.Collection[typing.Union[bases.Snowflake, int, str, users.User]], bool ] = True, role_mentions: typing.Union[ - typing.Collection[typing.Union[bases.Snowflake, int, str, guilds.GuildRole]], bool + typing.Collection[typing.Union[bases.Snowflake, int, str, guilds.Role]], bool ] = True, ) -> Message: """Edit this message. @@ -442,7 +442,7 @@ async def edit( # pylint:disable=line-too-long Either an array of user objects/IDs to allow mentions for, `True` to allow all user mentions or `False` to block all user mentions from resolving, defaults to `True`. - role_mentions: typing.Collection[hikari.models.guilds.GuildRole | hikari.models.bases.Snowflake | int | str] | bool + role_mentions: typing.Collection[hikari.models.guilds.Role | hikari.models.bases.Snowflake | int | str] | bool Either an array of guild role objects/IDs to allow mentions for, `True` to allow all role mentions or `False` to block all role mentions from resolving, defaults to `True`. @@ -492,7 +492,7 @@ async def safe_edit( typing.Collection[typing.Union[bases.Snowflake, int, str, users.User]], bool ] = False, role_mentions: typing.Union[ - typing.Collection[typing.Union[bases.Snowflake, int, str, guilds.GuildRole]], bool + typing.Collection[typing.Union[bases.Snowflake, int, str, guilds.Role]], bool ] = False, ) -> Message: """Edit this message. @@ -521,7 +521,7 @@ async def reply( # pylint:disable=line-too-long typing.Collection[typing.Union[bases.Snowflake, int, str, users.User]], bool ] = True, role_mentions: typing.Union[ - typing.Collection[typing.Union[bases.Snowflake, int, str, guilds.GuildRole]], bool + typing.Collection[typing.Union[bases.Snowflake, int, str, guilds.Role]], bool ] = True, nonce: str = ..., tts: bool = ..., @@ -551,7 +551,7 @@ async def reply( # pylint:disable=line-too-long Either an array of user objects/IDs to allow mentions for, `True` to allow all user mentions or `False` to block all user mentions from resolving, defaults to `True`. - role_mentions: typing.Collection[hikari.models.guilds.GuildRole | hikari.models.bases.Snowflake | int | str] | bool + role_mentions: typing.Collection[hikari.models.guilds.Role | hikari.models.bases.Snowflake | int | str] | bool Either an array of guild role objects/IDs to allow mentions for, `True` to allow all role mentions or `False` to block all role mentions from resolving, defaults to `True`. @@ -604,7 +604,7 @@ async def safe_reply( typing.Collection[typing.Union[bases.Snowflake, int, str, users.User]], bool ] = False, role_mentions: typing.Union[ - typing.Collection[typing.Union[bases.Snowflake, int, str, guilds.GuildRole]], bool + typing.Collection[typing.Union[bases.Snowflake, int, str, guilds.Role]], bool ] = False, nonce: str = ..., tts: bool = ..., diff --git a/hikari/models/unset.py b/hikari/models/unset.py index 8b8a4c56cb..8876dfee6c 100644 --- a/hikari/models/unset.py +++ b/hikari/models/unset.py @@ -41,6 +41,9 @@ def __bool__(self) -> bool: def __repr__(self) -> str: return type(self).__name__.upper() + def __iter__(self) -> typing.Iterator[None]: + yield from () + __str__ = __repr__ def __init_subclass__(cls, **kwargs: typing.Any) -> typing.NoReturn: diff --git a/hikari/models/webhooks.py b/hikari/models/webhooks.py index ec344d87f6..1b389395ce 100644 --- a/hikari/models/webhooks.py +++ b/hikari/models/webhooks.py @@ -122,7 +122,7 @@ async def execute( typing.Collection[typing.Union[bases.Snowflake, int, str, users_.User]], bool ] = True, role_mentions: typing.Union[ - typing.Collection[typing.Union[bases.Snowflake, int, str, guilds_.GuildRole]], bool + typing.Collection[typing.Union[bases.Snowflake, int, str, guilds_.Role]], bool ] = True, ) -> typing.Optional[messages_.Message]: """Execute the webhook to create a message. @@ -154,7 +154,7 @@ async def execute( Either an array of user objects/IDs to allow mentions for, `True` to allow all user mentions or `False` to block all user mentions from resolving, defaults to `True`. - role_mentions: typing.Collection[hikari.models.guilds.GuildRole | hikari.models.bases.Snowflake | int | str] | bool + role_mentions: typing.Collection[hikari.models.guilds.Role | hikari.models.bases.Snowflake | int | str] | bool Either an array of guild role objects/IDs to allow mentions for, `True` to allow all role mentions or `False` to block all role mentions from resolving, defaults to `True`. @@ -214,7 +214,7 @@ async def safe_execute( typing.Collection[typing.Union[bases.Snowflake, int, str, users_.User]], bool ] = False, role_mentions: typing.Union[ - typing.Collection[typing.Union[bases.Snowflake, int, str, guilds_.GuildRole]], bool + typing.Collection[typing.Union[bases.Snowflake, int, str, guilds_.Role]], bool ] = False, ) -> typing.Optional[messages_.Message]: """Execute the webhook to create a message with mention safety. diff --git a/hikari/net/rest.py b/hikari/net/rest.py index 428801b969..7a78337f50 100644 --- a/hikari/net/rest.py +++ b/hikari/net/rest.py @@ -22,6 +22,7 @@ __all__ = ["REST"] import asyncio +import contextlib import datetime import http import json @@ -34,12 +35,21 @@ from hikari import http_settings from hikari import pagination from hikari.internal import conversions +from hikari.internal import more_collections from hikari.internal import more_typing from hikari.internal import ratelimits from hikari.models import bases from hikari.models import channels +from hikari.models import embeds +from hikari.models import emojis +from hikari.models import files +from hikari.models import guilds +from hikari.models import invites from hikari.models import messages +from hikari.models import permissions from hikari.models import unset +from hikari.models import users +from hikari.models import webhooks from hikari.net import buckets from hikari.net import http_client from hikari.net import routes @@ -60,11 +70,12 @@ def __init__( direction: str, first_id: str, ) -> None: + super().__init__() self._app = app self._request_call = request_call self._direction = direction self._first_id = first_id - self._route = routes.GET_CHANNEL_MESSAGES.compile(channel_id) + self._route = routes.GET_CHANNEL_MESSAGES.compile(channel=channel_id) async def _next_chunk(self) -> typing.Optional[typing.Generator[messages.Message, typing.Any, None]]: chunk = await self._request_call(self._route, query={self._direction: self._first_id, "limit": 100}) @@ -79,6 +90,34 @@ async def _next_chunk(self) -> typing.Optional[typing.Generator[messages.Message return (self._app.entity_factory.deserialize_message(m) for m in chunk) +class _ReactionPaginator(pagination.BufferedPaginatedResults[messages.Reaction]): + __slots__ = ("_app", "_first_id", "_route", "_request_call") + + def __init__( + self, + app: base_app.IBaseApp, + request_call: typing.Callable[..., more_typing.Coroutine[more_typing.JSONObject]], + channel_id: str, + message_id: str, + emoji: str, + ) -> None: + super().__init__() + self._app = app + self._request_call = request_call + self._first_id = bases.Snowflake.min() + self._route = routes.GET_REACTIONS.compile(channel_id=channel_id, message_id=message_id, emoji=emoji) + + async def _next_chunk(self): + chunk = await self._request_call(self._route, query={"after": self._first_id, "limit": 100}) + + if not chunk: + return None + + self._first_id = chunk[-1]["id"] + + return (users.User.deserialize(u, app=self._app) for u in chunk) + + class REST(http_client.HTTPClient): def __init__( self, @@ -110,7 +149,6 @@ def __init__( self._app = app self._token = f"{token_type.title()} {token}" if token is not None else None - self._url = url.format(self) async def close(self) -> None: @@ -122,12 +160,12 @@ async def _request( self, compiled_route: routes.CompiledRoute, *, - headers: more_typing.Headers = None, - query: typing.Optional[more_typing.JSONObject] = None, - body: typing.Optional[typing.Union[aiohttp.FormData, more_typing.JSONType]] = None, - reason: typing.Union[unset.Unset, str] = None, + headers: typing.Union[unset.Unset, more_typing.Headers] = unset.UNSET, + query: typing.Union[unset.Unset, typing.Mapping[str, str]] = unset.UNSET, + body: typing.Union[unset.Unset, aiohttp.FormData, more_typing.JSONType] = unset.UNSET, + reason: typing.Union[unset.Unset, str] = unset.UNSET, suppress_authorization_header: bool = False, - ) -> typing.Optional[more_typing.JSONObject, more_typing.JSONArray, bytes]: + ) -> typing.Optional[more_typing.JSONObject, more_typing.JSONArray, bytes, str]: # Make a ratelimit-protected HTTP request to a JSON endpoint and expect some form # of JSON response. If an error occurs, the response body is returned in the # raised exception as a bytes object. This is done since the differences between @@ -138,7 +176,7 @@ async def _request( if not self.buckets.is_started: self.buckets.start() - headers = {} if headers is None else headers + headers = {} if unset.is_unset(headers) else headers headers["x-ratelimit-precision"] = "millisecond" headers["accept"] = self._APPLICATION_JSON @@ -149,6 +187,9 @@ async def _request( if not unset.is_unset(reason): headers["x-audit-log-reason"] = reason + if unset.is_unset(query): + query = None + while True: try: # Moved to a separate method to keep branch counts down. @@ -162,7 +203,7 @@ async def _request_once( headers: more_typing.Headers, body: typing.Optional[typing.Union[aiohttp.FormData, more_typing.JSONType]], query: typing.Optional[typing.Dict[str, str]], - ): + ) -> typing.Optional[more_typing.JSONObject, more_typing.JSONArray, bytes, str]: url = compiled_route.create_url(self._url) # Wait for any ratelimits to finish. @@ -266,10 +307,57 @@ async def _handle_rate_limits_for_response( f"We were rate limited but did not understand the response. Perhaps Cloudflare did this? {body!r}", ) + @staticmethod + def _generate_allowed_mentions( + mentions_everyone: bool, + user_mentions: typing.Union[typing.Collection[typing.Union[bases.Snowflake, int, str, users.User]], bool], + role_mentions: typing.Union[typing.Collection[typing.Union[bases.Snowflake, int, str, guilds.Role]], bool], + ): + parsed_mentions = [] + allowed_mentions = {} + + if mentions_everyone is True: + parsed_mentions.append("everyone") + if user_mentions is True: + parsed_mentions.append("users") + + # This covers both `False` and an array of IDs/objs by using `user_mentions or EMPTY_SEQUENCE`, where a + # resultant empty list will mean that all user mentions are blacklisted. + else: + allowed_mentions["users"] = list( + # dict.fromkeys is used to remove duplicate entries that would cause discord to return an error. + dict.fromkeys( + str(user.id if isinstance(user, bases.Unique) else int(user)) + for user in user_mentions or more_collections.EMPTY_SEQUENCE + ) + ) + if len(allowed_mentions["users"]) > 100: + raise ValueError("Only up to 100 users can be provided.") + if role_mentions is True: + parsed_mentions.append("roles") + + # This covers both `False` and an array of IDs/objs by using `user_mentions or EMPTY_SEQUENCE`, where a + # resultant empty list will mean that all role mentions are blacklisted. + else: + allowed_mentions["roles"] = list( + # dict.fromkeys is used to remove duplicate entries that would cause discord to return an error. + dict.fromkeys( + str(role.id if isinstance(role, bases.Unique) else int(role)) + for role in role_mentions or more_collections.EMPTY_SEQUENCE + ) + ) + if len(allowed_mentions["roles"]) > 100: + raise ValueError("Only up to 100 roles can be provided.") + allowed_mentions["parse"] = parsed_mentions + + # As a note, discord will also treat an empty `allowed_mentions` object as if it wasn't passed at all, so we + # want to use empty lists for blacklisting elements rather than just not including blacklisted elements. + return allowed_mentions + async def fetch_channel( self, channel: typing.Union[channels.PartialChannel, bases.Snowflake, int], /, ) -> channels.PartialChannel: - response = await self._request(routes.GET_CHANNEL.compile(channel_id=conversions.cast_to_str_id(channel))) + response = await self._request(routes.GET_CHANNEL.compile(channel=conversions.cast_to_str_id(channel))) return self._app.entity_factory.deserialize_channel(response) async def edit_channel( @@ -304,24 +392,152 @@ async def edit_channel( ] response = await self._request( - routes.PATCH_CHANNEL.compile(channel_id=conversions.cast_to_str_id(channel)), body=payload, reason=reason, + routes.PATCH_CHANNEL.compile(channel=conversions.cast_to_str_id(channel)), body=payload, reason=reason, ) return self._app.entity_factory.deserialize_channel(response) async def delete_channel(self, channel: typing.Union[channels.PartialChannel, bases.Snowflake, int]) -> None: - await self._request(routes.DELETE_CHANNEL.compile(channel_id=conversions.cast_to_str_id(channel))) + await self._request(routes.DELETE_CHANNEL.compile(channel=conversions.cast_to_str_id(channel))) @typing.overload - async def fetch_messages( - self, channel: typing.Union[channels.PartialChannel, bases.UniqueObjectT], / + async def edit_channel_permissions( + self, + channel: typing.Union[channels.GuildChannel, bases.UniqueObjectT], + target: typing.Union[channels.PermissionOverwrite, users.User, guilds.Role], + *, + allow: typing.Union[unset.Unset, permissions.Permission] = unset.UNSET, + deny: typing.Union[unset.Unset, permissions.Permission] = unset.UNSET, + reason: typing.Union[unset.Unset, str] = unset.UNSET, + ) -> None: + ... + + @typing.overload + async def edit_channel_permissions( + self, + channel: typing.Union[channels.GuildChannel, bases.UniqueObjectT], + target: typing.Union[int, str, bases.Snowflake], + target_type: typing.Union[channels.PermissionOverwriteType, str], + *, + allow: typing.Union[unset.Unset, permissions.Permission] = unset.UNSET, + deny: typing.Union[unset.Unset, permissions.Permission] = unset.UNSET, + reason: typing.Union[unset.Unset, str] = unset.UNSET, + ) -> None: + ... + + async def edit_channel_permissions( + self, + channel: typing.Union[channels.GuildChannel, bases.UniqueObjectT], + target: typing.Union[bases.UniqueObjectT, users.User, guilds.Role, channels.PermissionOverwrite], + target_type: typing.Union[unset.Unset, channels.PermissionOverwriteType, str] = unset.UNSET, + *, + allow: typing.Union[unset.Unset, permissions.Permission] = unset.UNSET, + deny: typing.Union[unset.Unset, permissions.Permission] = unset.UNSET, + reason: typing.Union[unset.Unset, str] = unset.UNSET, + ) -> None: + if unset.is_unset(target_type): + if isinstance(target, users.User): + target_type = channels.PermissionOverwriteType.MEMBER + elif isinstance(target, guilds.Role): + target_type = channels.PermissionOverwriteType.ROLE + elif isinstance(target, channels.PermissionOverwrite): + target_type = target.type + else: + raise TypeError( + "Cannot determine the type of the target to update. Try specifying 'target_type' manually." + ) + + payload = {"type": target_type} + conversions.put_if_specified(payload, "allow", allow) + conversions.put_if_specified(payload, "deny", deny) + route = routes.PATCH_CHANNEL_PERMISSIONS.compile( + channel=conversions.cast_to_str_id(channel), overwrite=conversions.cast_to_str_id(target), + ) + + await self._request(route, body=payload, reason=reason) + + async def delete_channel_permission( + self, + channel: typing.Union[channels.GuildChannel, bases.UniqueObjectT], + target: typing.Union[channels.PermissionOverwrite, guilds.Role, users.User, bases.UniqueObjectT], + ) -> None: + route = routes.DELETE_CHANNEL_PERMISSIONS.compile( + channel=conversions.cast_to_str_id(channel), overwrite=conversions.cast_to_str_id(target), + ) + await self._request(route) + + async def fetch_channel_invites( + self, channel: typing.Union[channels.GuildChannel, bases.UniqueObjectT] + ) -> typing.Sequence[invites.InviteWithMetadata]: + route = routes.GET_CHANNEL_INVITES.compile(channel=conversions.cast_to_str_id(channel)) + response = await self._request(route) + return [self._app.entity_factory.deserialize_invite_with_metadata(i) for i in response] + + async def create_invite( + self, + channel: typing.Union[channels.GuildChannel, bases.UniqueObjectT], + /, + *, + max_age: typing.Union[unset.Unset, int, float, datetime.timedelta] = unset.UNSET, + max_uses: typing.Union[unset.Unset, int] = unset.UNSET, + temporary: typing.Union[unset.Unset, bool] = unset.UNSET, + unique: typing.Union[unset.Unset, bool] = unset.UNSET, + target_user: typing.Union[unset.Unset, users.User, bases.UniqueObjectT] = unset.UNSET, + target_user_type: typing.Union[unset.Unset, invites.TargetUserType] = unset.UNSET, + reason: typing.Union[unset.Unset, str] = unset.UNSET, + ) -> invites.InviteWithMetadata: + payload = {} + conversions.put_if_specified(payload, "max_age", max_age, conversions.timespan_as_int) + conversions.put_if_specified(payload, "max_uses", max_uses) + conversions.put_if_specified(payload, "temporary", temporary) + conversions.put_if_specified(payload, "unique", unique), + conversions.put_if_specified(payload, "target_user", target_user, conversions.cast_to_str_id) + conversions.put_if_specified(payload, "target_user_type", target_user_type) + route = routes.POST_CHANNEL_INVITES.compile(channel=conversions.cast_to_str_id(channel)) + response = await self._request(route, body=payload, reason=reason) + return self._app.entity_factory.deserialize_invite_with_metadata(response) + + async def trigger_typing(self, channel: typing.Union[channels.TextChannel, bases.UniqueObjectT], /) -> None: + route = routes.POST_CHANNEL_TYPING.compile(channel=conversions.cast_to_str_id(channel)) + await self._request(route) + + async def fetch_pins( + self, channel: typing.Union[channels.TextChannel, bases.UniqueObjectT], / + ) -> typing.Mapping[bases.Snowflake, messages.Message]: + route = routes.GET_CHANNEL_PINS.compile(channel=conversions.cast_to_str_id(channel)) + response = await self._request(route) + return {bases.Snowflake(m["id"]): self._app.entity_factory.deserialize_message(m) for m in response} + + async def create_pinned_message( + self, + channel: typing.Union[channels.TextChannel, bases.UniqueObjectT], + message: typing.Union[messages.Message, bases.UniqueObjectT], + ) -> None: + route = routes.PUT_CHANNEL_PINS.compile( + channel=conversions.cast_to_str_id(channel), message=conversions.cast_to_str_id(message), + ) + await self._request(route) + + async def delete_pinned_message( + self, + channel: typing.Union[channels.TextChannel, bases.UniqueObjectT], + message: typing.Union[messages.Message, bases.UniqueObjectT], + ) -> None: + route = routes.DELETE_CHANNEL_PIN.compile( + channel=conversions.cast_to_str_id(channel), message=conversions.cast_to_str_id(message), + ) + await self._request(route) + + @typing.overload + def fetch_messages( + self, channel: typing.Union[channels.TextChannel, bases.UniqueObjectT], / ) -> pagination.PaginatedResults[messages.Message]: ... @typing.overload - async def fetch_messages( + def fetch_messages( self, - channel: typing.Union[channels.PartialChannel, bases.UniqueObjectT], + channel: typing.Union[channels.TextChannel, bases.UniqueObjectT], /, *, before: typing.Union[datetime.datetime, bases.UniqueObjectT], @@ -329,9 +545,9 @@ async def fetch_messages( ... @typing.overload - async def fetch_messages( + def fetch_messages( self, - channel: typing.Union[channels.PartialChannel, bases.UniqueObjectT], + channel: typing.Union[channels.TextChannel, bases.UniqueObjectT], /, *, around: typing.Union[datetime.datetime, bases.UniqueObjectT], @@ -339,9 +555,9 @@ async def fetch_messages( ... @typing.overload - async def fetch_messages( + def fetch_messages( self, - channel: typing.Union[channels.PartialChannel, bases.UniqueObjectT], + channel: typing.Union[channels.TextChannel, bases.UniqueObjectT], /, *, after: typing.Union[datetime.datetime, bases.UniqueObjectT], @@ -350,7 +566,7 @@ async def fetch_messages( def fetch_messages( self, - channel: typing.Union[channels.PartialChannel, bases.UniqueObjectT], + channel: typing.Union[channels.TextChannel, bases.UniqueObjectT], /, **kwargs: typing.Optional[typing.Union[datetime.datetime, bases.UniqueObjectT]], ) -> pagination.PaginatedResults[messages.Message]: @@ -371,3 +587,223 @@ def fetch_messages( direction, conversions.cast_to_str_id(timestamp), ) + + async def fetch_message( + self, + channel: typing.Union[channels.TextChannel, bases.UniqueObjectT], + message: typing.Union[messages.Message, bases.UniqueObjectT], + /, + ) -> messages.Message: + route = routes.GET_CHANNEL_MESSAGE.compile( + channel=conversions.cast_to_str_id(channel), message=conversions.cast_to_str_id(message), + ) + response = await self._request(route) + return self._app.entity_factory.deserialize_message(response) + + async def create_message( + self, + channel: typing.Union[channels.TextChannel, bases.UniqueObjectT], + text: typing.Union[unset.Unset, typing.Any] = unset.UNSET, + *, + embed: typing.Union[unset.Unset, embeds.Embed] = unset.UNSET, + attachments: typing.Union[unset.Unset, typing.Sequence[files.BaseStream]] = unset.UNSET, + tts: typing.Union[unset.Unset, bool] = unset.UNSET, + nonce: typing.Union[unset.Unset, str] = unset.UNSET, + mentions_everyone: bool = False, + user_mentions: typing.Union[typing.Collection[typing.Union[users.User, bases.UniqueObjectT]], bool] = True, + role_mentions: typing.Union[typing.Collection[typing.Union[bases.UniqueObjectT, guilds.Role]], bool] = True, + ) -> messages.Message: + route = routes.POST_CHANNEL_MESSAGES.compile(channel=conversions.cast_to_str_id(channel)) + + payload = {} + conversions.put_if_specified(payload, "content", text, str) + conversions.put_if_specified(payload, "embed", embed, self._app.entity_factory.serialize_embed) + conversions.put_if_specified(payload, "nonce", nonce) + conversions.put_if_specified(payload, "tts", tts) + + payload["mentions"] = self._generate_allowed_mentions(mentions_everyone, user_mentions, role_mentions) + + if (unset.is_unset(embed) or not embed.assets_to_upload) and attachments is unset.UNSET: + response = await self._request(route, body=payload) + else: + form = aiohttp.FormData() + form.add_field("payload_json", json.dumps(payload), content_type=self._APPLICATION_JSON) + file_list = [*attachments] + if embed is not None and embed.assets_to_upload: + file_list.extend(embed.assets_to_upload) + for i, file in enumerate(file_list): + form.add_field(f"file{i}", file, content_type=self._APPLICATION_OCTET_STREAM) + + response = await self._request(route, body=form) + + return self._app.entity_factory.deserialize_message(response) + + async def edit_message( + self, + channel: typing.Union[channels.TextChannel, bases.UniqueObjectT], + message: typing.Union[messages.Message, bases.UniqueObjectT], + text: typing.Union[unset.Unset, typing.Any] = unset.UNSET, + *, + embed: typing.Union[unset.Unset, embeds.Embed] = unset.UNSET, + mentions_everyone: bool = False, + user_mentions: typing.Union[typing.Collection[typing.Union[users.User, bases.UniqueObjectT]], bool] = True, + role_mentions: typing.Union[typing.Collection[typing.Union[bases.UniqueObjectT, guilds.Role]], bool] = True, + flags: typing.Union[unset.Unset, messages.MessageFlag] = unset.UNSET, + ) -> messages.Message: + route = routes.PATCH_CHANNEL_MESSAGE.compile( + channel=conversions.cast_to_str_id(channel), message=conversions.cast_to_str_id(message), + ) + payload = {} + conversions.put_if_specified(payload, "content", text, str) + conversions.put_if_specified(payload, "embed", embed, self._app.entity_factory.serialize_embed) + conversions.put_if_specified(payload, "flags", flags) + payload["mentions"] = self._generate_allowed_mentions(mentions_everyone, user_mentions, role_mentions) + response = await self._request(route, body=payload) + return self._app.entity_factory.deserialize_message(response) + + async def delete_message( + self, + channel: typing.Union[channels.TextChannel, bases.UniqueObjectT], + message: typing.Union[messages.Message, bases.UniqueObjectT], + ) -> None: + route = routes.DELETE_CHANNEL_MESSAGE.compile( + channel=conversions.cast_to_str_id(channel), message=conversions.cast_to_str_id(message), + ) + await self._request(route) + + async def delete_messages( + self, + channel: typing.Union[channels.TextChannel, bases.UniqueObjectT], + *messages_to_delete: typing.Union[messages.Message, bases.UniqueObjectT], + ) -> None: + if 2 <= len(messages_to_delete) <= 100: + route = routes.POST_DELETE_CHANNEL_MESSAGES_BULK.compile(channel=conversions.cast_to_str_id(channel)) + payload = {"messages": [conversions.cast_to_str_id(m) for m in messages_to_delete]} + await self._request(route, body=payload) + else: + raise TypeError("Must delete a minimum of 2 messages and a maximum of 100") + + async def create_reaction( + self, + channel: typing.Union[channels.TextChannel, bases.UniqueObjectT], + message: typing.Union[messages.Message, bases.UniqueObjectT], + emoji: typing.Union[str, emojis.UnicodeEmoji, emojis.KnownCustomEmoji], + ) -> None: + emoji = emoji.url_name if isinstance(emoji, emojis.KnownCustomEmoji) else str(emoji) + channel = conversions.cast_to_str_id(channel) + message = conversions.cast_to_str_id(message) + route = routes.PUT_MY_REACTION.compile(channel=channel, message=message, emoji=emoji) + await self._request(route) + + async def delete_my_reaction( + self, + channel: typing.Union[channels.TextChannel, bases.UniqueObjectT], + message: typing.Union[messages.Message, bases.UniqueObjectT], + emoji: typing.Union[str, emojis.UnicodeEmoji, emojis.KnownCustomEmoji], + ) -> None: + emoji = emoji.url_name if isinstance(emoji, emojis.KnownCustomEmoji) else str(emoji) + channel = conversions.cast_to_str_id(channel) + message = conversions.cast_to_str_id(message) + route = routes.DELETE_MY_REACTION.compile(channel=channel, message=message, emoji=emoji) + await self._request(route) + + async def delete_all_reactions_for_emoji( + self, + channel: typing.Union[channels.TextChannel, bases.UniqueObjectT], + message: typing.Union[messages.Message, bases.UniqueObjectT], + emoji: typing.Union[str, emojis.UnicodeEmoji, emojis.KnownCustomEmoji], + ) -> None: + emoji = emoji.url_name if isinstance(emoji, emojis.KnownCustomEmoji) else str(emoji) + channel = conversions.cast_to_str_id(channel) + message = conversions.cast_to_str_id(message) + route = routes.DELETE_REACTION_EMOJI.compile(channel=channel, message=message, emoji=emoji) + await self._request(route) + + async def delete_reaction( + self, + channel: typing.Union[channels.TextChannel, bases.UniqueObjectT], + message: typing.Union[messages.Message, bases.UniqueObjectT], + emoji: typing.Union[str, emojis.UnicodeEmoji, emojis.KnownCustomEmoji], + user: typing.Union[users.User, bases.UniqueObjectT], + ) -> None: + emoji = emoji.url_name if isinstance(emoji, emojis.KnownCustomEmoji) else str(emoji) + channel = conversions.cast_to_str_id(channel) + message = conversions.cast_to_str_id(message) + user = conversions.cast_to_str_id(user) + route = routes.DELETE_REACTION_USER.compile(channel=channel, message=message, emoji=emoji, user=user) + await self._request(route) + + async def delete_all_reactions( + self, + channel: typing.Union[channels.TextChannel, bases.UniqueObjectT], + message: typing.Union[messages.Message, bases.UniqueObjectT], + ) -> None: + channel = conversions.cast_to_str_id(channel) + message = conversions.cast_to_str_id(message) + route = routes.DELETE_ALL_REACTIONS.compile(channel=channel, message=message) + await self._request(route) + + async def create_webhook( + self, + channel: typing.Union[channels.TextChannel, bases.UniqueObjectT], + name: str, + *, + avatar: typing.Union[unset.Unset, files.BaseStream] = unset.UNSET, + reason: typing.Union[unset.Unset, str] = unset.UNSET, + ) -> webhooks.Webhook: + payload = {"name": name} + conversions.put_if_specified(payload, "avatar", await avatar.fetch_data_uri()) + route = routes.POST_WEBHOOK.compile(channel=conversions.cast_to_str_id(channel)) + response = await self._request(route, body=payload, reason=reason) + return self._app.entity_factory.deserialize_webhook(response) + + async def fetch_webhook( + self, + webhook: typing.Union[webhooks.Webhook, bases.UniqueObjectT], + /, + *, + token: typing.Union[unset.Unset, str] = unset.UNSET, + ) -> webhooks.Webhook: + if unset.is_unset(token): + route = routes.GET_WEBHOOK.compile(webhook=conversions.cast_to_str_id(webhook)) + else: + route = routes.GET_WEBHOOK_WITH_TOKEN.compile(webhook=conversions.cast_to_str_id(webhook), token=token) + response = await self._request(route) + return self._app.entity_factory.deserialize_webhook(response) + + async def fetch_channel_webhooks( + self, channel: typing.Union[channels.TextChannel, bases.UniqueObjectT], / + ) -> typing.Mapping[bases.Snowflake, webhooks.Webhook]: + route = routes.GET_CHANNEL_WEBHOOKS.compile(channel=conversions.cast_to_str_id(channel)) + response = await self._request(route) + return {bases.Snowflake(w["id"]): self._app.entity_factory.deserialize_webhook(w) for w in response} + + async def fetch_guild_webhooks( + self, guild: typing.Union[guilds.Guild, bases.UniqueObjectT], / + ) -> typing.Mapping[bases.Snowflake, webhooks.Webhook]: + route = routes.GET_GUILD_WEBHOOKS.compile(channel=conversions.cast_to_str_id(guild)) + response = await self._request(route) + return {bases.Snowflake(w["id"]): self._app.entity_factory.deserialize_webhook(w) for w in response} + + # Keep this last, then it doesn't cause problems with the imports. + def typing( + self, channel: typing.Union[channels.TextChannel, bases.UniqueObjectT], / + ) -> contextlib.AbstractAsyncContextManager: + async def keep_typing(): + with contextlib.suppress(asyncio.CancelledError): + while True: + # Use gather so that if the API call takes more than 10s, we don't spam the API + # as something is not working properly somewhere, but at the same time do not + # take into account the API call time before waiting the 10s, as this stops + # the indicator showing up consistently. + await asyncio.gather(self.trigger_typing(channel), asyncio.sleep(9.9)) + + @contextlib.asynccontextmanager + async def typing_context(): + task = asyncio.create_task(keep_typing(), name=f"typing in {channel} continuously") + try: + yield + finally: + task.cancel() + + return typing_context() diff --git a/hikari/net/routes.py b/hikari/net/routes.py index ad8f6bd51d..bd1fa2f607 100644 --- a/hikari/net/routes.py +++ b/hikari/net/routes.py @@ -25,7 +25,7 @@ import re import typing -DEFAULT_MAJOR_PARAMS: typing.Final[typing.Set[str]] = {"channel", "guild_id", "webhook_id"} +DEFAULT_MAJOR_PARAMS: typing.Final[typing.Set[str]] = {"channel", "guild", "webhook"} HASH_SEPARATOR: typing.Final[str] = ";" @@ -225,22 +225,22 @@ def __eq__(self, other) -> bool: GET_CHANNEL_INVITES = Route(GET, "/channels/{channel}/invites") POST_CHANNEL_INVITES = Route(POST, "/channels/{channel}/invites") -GET_CHANNEL_MESSAGE = Route(GET, "/channels/{channel}/messages/{message_id}") -PATCH_CHANNEL_MESSAGE = Route(PATCH, "/channels/{channel}/messages/{message_id}") -DELETE_CHANNEL_MESSAGE = Route(DELETE, "/channels/{channel}/messages/{message_id}") +GET_CHANNEL_MESSAGE = Route(GET, "/channels/{channel}/messages/{message}") +PATCH_CHANNEL_MESSAGE = Route(PATCH, "/channels/{channel}/messages/{message}") +DELETE_CHANNEL_MESSAGE = Route(DELETE, "/channels/{channel}/messages/{message}") GET_CHANNEL_MESSAGES = Route(GET, "/channels/{channel}/messages") POST_CHANNEL_MESSAGES = Route(POST, "/channels/{channel}/messages") POST_DELETE_CHANNEL_MESSAGES_BULK = Route(POST, "/channels/{channel}/messages/bulk-delete") -PATCH_CHANNEL_PERMISSIONS = Route(PATCH, "/channels/{channel}/permissions/{overwrite_id}") -DELETE_CHANNEL_PERMISSIONS = Route(DELETE, "/channels/{channel}/permissions/{overwrite_id}") +PATCH_CHANNEL_PERMISSIONS = Route(PATCH, "/channels/{channel}/permissions/{overwrite}") +DELETE_CHANNEL_PERMISSIONS = Route(DELETE, "/channels/{channel}/permissions/{overwrite}") -DELETE_CHANNEL_PIN = Route(DELETE, "/channels/{channel}/pins/{message_id}") +DELETE_CHANNEL_PIN = Route(DELETE, "/channels/{channel}/pins/{message}") GET_CHANNEL_PINS = Route(GET, "/channels/{channel}/pins") -PUT_CHANNEL_PINS = Route(PUT, "/channels/{channel}/pins/{message_id}") +PUT_CHANNEL_PINS = Route(PUT, "/channels/{channel}/pins/{message}") POST_CHANNEL_TYPING = Route(POST, "/channels/{channel}/typing") @@ -248,88 +248,88 @@ def __eq__(self, other) -> bool: GET_CHANNEL_WEBHOOKS = Route(GET, "/channels/{channel}/webhooks") # Reactions -DELETE_ALL_REACTIONS = Route(DELETE, "/channels/{channel}/messages/{message_id}/reactions") +DELETE_ALL_REACTIONS = Route(DELETE, "/channels/{channel}/messages/{message}/reactions") -DELETE_REACTION_EMOJI = Route(DELETE, "/channels/{channel}/messages/{message_id}/reactions/{emoji}") -DELETE_REACTION_USER = Route(DELETE, "/channels/{channel}/messages/{message_id}/reactions/{emoji}/{used_id}") -GET_REACTIONS = Route(GET, "/channels/{channel}/messages/{message_id}/reactions/{emoji}") +DELETE_REACTION_EMOJI = Route(DELETE, "/channels/{channel}/messages/{message}/reactions/{emoji}") +DELETE_REACTION_USER = Route(DELETE, "/channels/{channel}/messages/{message}/reactions/{emoji}/{used}") +GET_REACTIONS = Route(GET, "/channels/{channel}/messages/{message}/reactions/{emoji}") # Guilds -GET_GUILD = Route(GET, "/guilds/{guild_id}") -PATCH_GUILD = Route(PATCH, "/guilds/{guild_id}") -DELETE_GUILD = Route(DELETE, "/guilds/{guild_id}") +GET_GUILD = Route(GET, "/guilds/{guild}") +PATCH_GUILD = Route(PATCH, "/guilds/{guild}") +DELETE_GUILD = Route(DELETE, "/guilds/{guild}") POST_GUILDS = Route(POST, "/guilds") -GET_GUILD_AUDIT_LOGS = Route(GET, "/guilds/{guild_id}/audit-logs") +GET_GUILD_AUDIT_LOGS = Route(GET, "/guilds/{guild}/audit-logs") -GET_GUILD_BAN = Route(GET, "/guilds/{guild_id}/bans/{user_id}") -PUT_GUILD_BAN = Route(PUT, "/guilds/{guild_id}/bans/{user_id}") -DELETE_GUILD_BAN = Route(DELETE, "/guilds/{guild_id}/bans/{user_id}") +GET_GUILD_BAN = Route(GET, "/guilds/{guild}/bans/{user}") +PUT_GUILD_BAN = Route(PUT, "/guilds/{guild}/bans/{user}") +DELETE_GUILD_BAN = Route(DELETE, "/guilds/{guild}/bans/{user}") -GET_GUILD_BANS = Route(GET, "/guilds/{guild_id}/bans") +GET_GUILD_BANS = Route(GET, "/guilds/{guild}/bans") -GET_GUILD_CHANNELS = Route(GET, "/guilds/{guild_id}/channels") -POST_GUILD_CHANNELS = Route(POST, "/guilds/{guild_id}/channels") -PATCH_GUILD_CHANNELS = Route(PATCH, "/guilds/{guild_id}/channels") +GET_GUILD_CHANNELS = Route(GET, "/guilds/{guild}/channels") +POST_GUILD_CHANNELS = Route(POST, "/guilds/{guild}/channels") +PATCH_GUILD_CHANNELS = Route(PATCH, "/guilds/{guild}/channels") -GET_GUILD_EMBED = Route(GET, "/guilds/{guild_id}/embed") -PATCH_GUILD_EMBED = Route(PATCH, "/guilds/{guild_id}/embed") +GET_GUILD_EMBED = Route(GET, "/guilds/{guild}/embed") +PATCH_GUILD_EMBED = Route(PATCH, "/guilds/{guild}/embed") -GET_GUILD_EMOJI = Route(GET, "/guilds/{guild_id}/emojis/{emoji_id}") -PATCH_GUILD_EMOJI = Route(PATCH, "/guilds/{guild_id}/emojis/{emoji_id}") -DELETE_GUILD_EMOJI = Route(DELETE, "/guilds/{guild_id}/emojis/{emoji_id}") +GET_GUILD_EMOJI = Route(GET, "/guilds/{guild}/emojis/{emoji}") +PATCH_GUILD_EMOJI = Route(PATCH, "/guilds/{guild}/emojis/{emoji}") +DELETE_GUILD_EMOJI = Route(DELETE, "/guilds/{guild}/emojis/{emoji}") -GET_GUILD_EMOJIS = Route(GET, "/guilds/{guild_id}/emojis") -POST_GUILD_EMOJIS = Route(POST, "/guilds/{guild_id}/emojis") +GET_GUILD_EMOJIS = Route(GET, "/guilds/{guild}/emojis") +POST_GUILD_EMOJIS = Route(POST, "/guilds/{guild}/emojis") -PATCH_GUILD_INTEGRATION = Route(PATCH, "/guilds/{guild_id}/integrations/{integration_id}") -DELETE_GUILD_INTEGRATION = Route(DELETE, "/guilds/{guild_id}/integrations/{integration_id}") +PATCH_GUILD_INTEGRATION = Route(PATCH, "/guilds/{guild}/integrations/{integration}") +DELETE_GUILD_INTEGRATION = Route(DELETE, "/guilds/{guild}/integrations/{integration}") -GET_GUILD_INTEGRATIONS = Route(GET, "/guilds/{guild_id}/integrations") +GET_GUILD_INTEGRATIONS = Route(GET, "/guilds/{guild}/integrations") -POST_GUILD_INTEGRATION_SYNC = Route(POST, "/guilds/{guild_id}/integrations/{integration_id}") +POST_GUILD_INTEGRATION_SYNC = Route(POST, "/guilds/{guild}/integrations/{integration}") -GET_GUILD_INVITES = Route(GET, "/guilds/{guild_id}/invites") +GET_GUILD_INVITES = Route(GET, "/guilds/{guild}/invites") -GET_GUILD_MEMBERS = Route(GET, "/guilds/{guild_id}/members") +GET_GUILD_MEMBERS = Route(GET, "/guilds/{guild}/members") -GET_GUILD_MEMBER = Route(GET, "/guilds/{guild_id}/members/{user_id}") -PATCH_GUILD_MEMBER = Route(PATCH, "/guilds/{guild_id}/members/{user_id}") -DELETE_GUILD_MEMBER = Route(DELETE, "/guilds/{guild_id}/members/{user_id}") +GET_GUILD_MEMBER = Route(GET, "/guilds/{guild}/members/{user}") +PATCH_GUILD_MEMBER = Route(PATCH, "/guilds/{guild}/members/{user}") +DELETE_GUILD_MEMBER = Route(DELETE, "/guilds/{guild}/members/{user}") -PUT_GUILD_MEMBER_ROLE = Route(PUT, "/guilds/{guild_id}/members/{user_id}/roles/{role_id}") -DELETE_GUILD_MEMBER_ROLE = Route(DELETE, "/guilds/{guild_id}/members/{user_id}/roles/{role_id}") +PUT_GUILD_MEMBER_ROLE = Route(PUT, "/guilds/{guild}/members/{user}/roles/{role}") +DELETE_GUILD_MEMBER_ROLE = Route(DELETE, "/guilds/{guild}/members/{user}/roles/{role}") -GET_GUILD_PREVIEW = Route(GET, "/guilds/{guild_id}/preview") +GET_GUILD_PREVIEW = Route(GET, "/guilds/{guild}/preview") -GET_GUILD_PRUNE = Route(GET, "/guilds/{guild_id}/prune") -POST_GUILD_PRUNE = Route(POST, "/guilds/{guild_id}/prune") +GET_GUILD_PRUNE = Route(GET, "/guilds/{guild}/prune") +POST_GUILD_PRUNE = Route(POST, "/guilds/{guild}/prune") -PATCH_GUILD_ROLE = Route(PATCH, "/guilds/{guild_id}/roles/{role_id}") -DELETE_GUILD_ROLE = Route(DELETE, "/guilds/{guild_id}/roles/{role_id}") +PATCH_GUILD_ROLE = Route(PATCH, "/guilds/{guild}/roles/{role}") +DELETE_GUILD_ROLE = Route(DELETE, "/guilds/{guild}/roles/{role}") -GET_GUILD_ROLES = Route(GET, "/guilds/{guild_id}/roles") -POST_GUILD_ROLES = Route(POST, "/guilds/{guild_id}/roles") -PATCH_GUILD_ROLES = Route(PATCH, "/guilds/{guild_id}/roles") +GET_GUILD_ROLES = Route(GET, "/guilds/{guild}/roles") +POST_GUILD_ROLES = Route(POST, "/guilds/{guild}/roles") +PATCH_GUILD_ROLES = Route(PATCH, "/guilds/{guild}/roles") -GET_GUILD_VANITY_URL = Route(GET, "/guilds/{guild_id}/vanity-url") +GET_GUILD_VANITY_URL = Route(GET, "/guilds/{guild}/vanity-url") -GET_GUILD_VOICE_REGIONS = Route(GET, "/guilds/{guild_id}/regions") +GET_GUILD_VOICE_REGIONS = Route(GET, "/guilds/{guild}/regions") -GET_GUILD_WEBHOOKS = Route(GET, "/guilds/{guild_id}/webhooks") +GET_GUILD_WEBHOOKS = Route(GET, "/guilds/{guild}/webhooks") -GET_GUILD_WIDGET_IMAGE = Route(GET, "/guilds/{guild_id}/widget.png") +GET_GUILD_WIDGET_IMAGE = Route(GET, "/guilds/{guild}/widget.png") # Invites GET_INVITE = Route(GET, "/invites/{invite_code}") DELETE_INVITE = Route(DELETE, "/invites/{invite_code}") # Users -GET_USER = Route(GET, "/users/{user_id}") +GET_USER = Route(GET, "/users/{user}") # @me -DELETE_MY_GUILD = Route(DELETE, "/users/@me/guilds/{guild_id}") +DELETE_MY_GUILD = Route(DELETE, "/users/@me/guilds/{guild}") GET_MY_CONNECTIONS = Route(GET, "/users/@me/connections") # OAuth2 only @@ -337,30 +337,30 @@ def __eq__(self, other) -> bool: GET_MY_GUILDS = Route(GET, "/users/@me/guilds") -PATCH_MY_GUILD_NICKNAME = Route(PATCH, "/guilds/{guild_id}/members/@me/nick") +PATCH_MY_GUILD_NICKNAME = Route(PATCH, "/guilds/{guild}/members/@me/nick") GET_MY_USER = Route(GET, "/users/@me") PATCH_MY_USER = Route(PATCH, "/users/@me") -PUT_MY_REACTION = Route(PUT, "/channels/{channel}/messages/{message_id}/reactions/{emoji}/@me") -DELETE_MY_REACTION = Route(DELETE, "/channels/{channel}/messages/{message_id}/reactions/{emoji}/@me") +PUT_MY_REACTION = Route(PUT, "/channels/{channel}/messages/{message}/reactions/{emoji}/@me") +DELETE_MY_REACTION = Route(DELETE, "/channels/{channel}/messages/{message}/reactions/{emoji}/@me") # Voice GET_VOICE_REGIONS = Route(GET, "/voice/regions") # Webhooks -GET_WEBHOOK = Route(GET, "/webhooks/{webhook_id}") -PATCH_WEBHOOK = Route(PATCH, "/webhooks/{webhook_id}") -POST_WEBHOOK = Route(POST, "/webhooks/{webhook_id}") -DELETE_WEBHOOK = Route(DELETE, "/webhooks/{webhook_id}") - -GET_WEBHOOK_WITH_TOKEN = Route(GET, "/webhooks/{webhook_id}/{webhook_token}") -PATCH_WEBHOOK_WITH_TOKEN = Route(PATCH, "/webhooks/{webhook_id}/{webhook_token}") -DELETE_WEBHOOK_WITH_TOKEN = Route(DELETE, "/webhooks/{webhook_id}/{webhook_token}") -POST_WEBHOOK_WITH_TOKEN = Route(POST, "/webhooks/{webhook_id}/{webhook_token}") - -POST_WEBHOOK_WITH_TOKEN_GITHUB = Route(POST, "/webhooks/{webhook_id}/{webhook_token}/github") -POST_WEBHOOK_WITH_TOKEN_SLACK = Route(POST, "/webhooks/{webhook_id}/{webhook_token}/slack") +GET_WEBHOOK = Route(GET, "/webhooks/{webhook}") +PATCH_WEBHOOK = Route(PATCH, "/webhooks/{webhook}") +POST_WEBHOOK = Route(POST, "/webhooks/{webhook}") +DELETE_WEBHOOK = Route(DELETE, "/webhooks/{webhook}") + +GET_WEBHOOK_WITH_TOKEN = Route(GET, "/webhooks/{webhook}/{token}") +PATCH_WEBHOOK_WITH_TOKEN = Route(PATCH, "/webhooks/{webhook}/{token}") +DELETE_WEBHOOK_WITH_TOKEN = Route(DELETE, "/webhooks/{webhook}/{token}") +POST_WEBHOOK_WITH_TOKEN = Route(POST, "/webhooks/{webhook}/{token}") + +POST_WEBHOOK_WITH_TOKEN_GITHUB = Route(POST, "/webhooks/{webhook}/{token}/github") +POST_WEBHOOK_WITH_TOKEN_SLACK = Route(POST, "/webhooks/{webhook}/{token}/slack") # OAuth2 API GET_MY_APPLICATION = Route(GET, "/oauth2/applications/@me") diff --git a/hikari/pagination.py b/hikari/pagination.py index bc4ed7d393..60d276f5ad 100644 --- a/hikari/pagination.py +++ b/hikari/pagination.py @@ -155,42 +155,6 @@ def __aiter__(self) -> PaginatedResults[_T]: async def _fetch_all(self) -> typing.Sequence[_T]: return [item async for item in self] - @staticmethod - def _prepare_first_id(value, if_none=bases.Snowflake.min()) -> str: - """Prepare the given first ID type passed by the user. - - Given an object with an ID, a datetime, an int, a snowflake, or a string - type, convert the element to the string ID snowflake it represents - that can be passed to the underlying RESTSession API safely. - - Parameters - ---------- - value - The element to prepare. - if_none - The value to use if the `value` is `None`. Defaults to a snowflake - of `0`. - - Returns - ------- - str - The string ID. - """ - if value is None: - value = if_none - - if isinstance(value, datetime.datetime): - value = str(int(bases.Snowflake.from_datetime(value))) - - if isinstance(value, (int, bases.Snowflake)): - return str(value) - if isinstance(value, bases.Unique): - return str(value.id) - if isinstance(value, str): - return value - - raise TypeError("expected object with ID, datetime, snowflake, or None") - def __await__(self): return self._fetch_all().__await__() diff --git a/tests/hikari/events/test_guild.py b/tests/hikari/events/test_guild.py index 10f882ea2e..614f275f90 100644 --- a/tests/hikari/events/test_guild.py +++ b/tests/hikari/events/test_guild.py @@ -203,9 +203,9 @@ def test_guild_role_create_update_payload(test_guild_payload): class TestGuildRoleCreateEvent: def test_deserialize(self, test_guild_role_create_update_payload, test_guild_payload): - mock_role = mock.MagicMock(guilds.GuildRole) + mock_role = mock.MagicMock(guilds.Role) with _helpers.patch_marshal_attr( - guild.GuildRoleCreateEvent, "role", deserializer=guilds.GuildRole.deserialize, return_value=mock_role + guild.GuildRoleCreateEvent, "role", deserializer=guilds.Role.deserialize, return_value=mock_role ) as patched_role_deserializer: guild_role_create_obj = guild.GuildRoleCreateEvent.deserialize(test_guild_role_create_update_payload) patched_role_deserializer.assert_called_once_with(test_guild_payload) @@ -219,9 +219,9 @@ def test_guild_role_create_fixture(self, test_guild_payload): return {"guild_id": "69240", "role": test_guild_payload} def test_deserialize(self, test_guild_role_create_update_payload, test_guild_payload): - mock_role = mock.MagicMock(guilds.GuildRole) + mock_role = mock.MagicMock(guilds.Role) with _helpers.patch_marshal_attr( - guild.GuildRoleUpdateEvent, "role", deserializer=guilds.GuildRole.deserialize, return_value=mock_role + guild.GuildRoleUpdateEvent, "role", deserializer=guilds.Role.deserialize, return_value=mock_role ) as patched_role_deserializer: guild_role_create_obj = guild.GuildRoleUpdateEvent.deserialize(test_guild_role_create_update_payload) patched_role_deserializer.assert_called_once_with(test_guild_payload) diff --git a/tests/hikari/internal/test_helpers.py b/tests/hikari/internal/test_helpers.py index 3636ad1685..86f72ff71c 100644 --- a/tests/hikari/internal/test_helpers.py +++ b/tests/hikari/internal/test_helpers.py @@ -72,7 +72,7 @@ def test_generate_allowed_mentions(kwargs, expected_result): assert helpers.generate_allowed_mentions(**kwargs) == expected_result -@_helpers.parametrize_valid_id_formats_for_models("role", 3, guilds.GuildRole) +@_helpers.parametrize_valid_id_formats_for_models("role", 3, guilds.Role) def test_generate_allowed_mentions_removes_duplicate_role_ids(role): result = helpers.generate_allowed_mentions( role_mentions=["1", "2", "1", "3", "5", "7", "2", role], user_mentions=True, mentions_everyone=True @@ -88,7 +88,7 @@ def test_generate_allowed_mentions_removes_duplicate_user_ids(user): assert result == {"users": ["1", "2", "3", "5", "7"], "parse": ["everyone", "roles"]} -@_helpers.parametrize_valid_id_formats_for_models("role", 190007233919057920, guilds.GuildRole) +@_helpers.parametrize_valid_id_formats_for_models("role", 190007233919057920, guilds.Role) def test_generate_allowed_mentions_handles_all_role_formats(role): result = helpers.generate_allowed_mentions(role_mentions=[role], user_mentions=True, mentions_everyone=True) assert result == {"roles": ["190007233919057920"], "parse": ["everyone", "users"]} diff --git a/tests/hikari/models/test_audit_logs.py b/tests/hikari/models/test_audit_logs.py index a88057d629..e2d8c02a95 100644 --- a/tests/hikari/models/test_audit_logs.py +++ b/tests/hikari/models/test_audit_logs.py @@ -52,11 +52,11 @@ def test__deserialize_partial_roles(mock_app): {"id": "24", "name": "roleA", "hoisted": True}, {"id": "48", "name": "roleA", "hoisted": True}, ] - mock_role_objs = [mock.MagicMock(guilds.PartialGuildRole, id=24), mock.MagicMock(guilds.PartialGuildRole, id=48)] - with mock.patch.object(guilds.PartialGuildRole, "deserialize", side_effect=mock_role_objs): + mock_role_objs = [mock.MagicMock(guilds.PartialRole, id=24), mock.MagicMock(guilds.PartialRole, id=48)] + with mock.patch.object(guilds.PartialRole, "deserialize", side_effect=mock_role_objs): result = audit_logs._deserialize_partial_roles(test_role_payloads, app=mock_app) assert result == {24: mock_role_objs[0], 48: mock_role_objs[1]} - guilds.PartialGuildRole.deserialize.assert_has_calls( + guilds.PartialRole.deserialize.assert_has_calls( [mock.call(test_role_payloads[0], app=mock_app), mock.call(test_role_payloads[1], app=mock_app),] ) @@ -68,8 +68,8 @@ def test__deserialize_day_timedelta(): def test__deserialize_overwrites(mock_app): test_overwrite_payloads = [{"id": "24", "allow": 21, "deny": 0}, {"id": "48", "deny": 42, "allow": 0}] mock_overwrite_objs = [ - mock.MagicMock(guilds.PartialGuildRole, id=24), - mock.MagicMock(guilds.PartialGuildRole, id=48), + mock.MagicMock(guilds.PartialRole, id=24), + mock.MagicMock(guilds.PartialRole, id=48), ] with mock.patch.object(channels.PermissionOverwrite, "deserialize", side_effect=mock_overwrite_objs): result = audit_logs._deserialize_overwrites(test_overwrite_payloads, app=mock_app) @@ -107,9 +107,9 @@ def test_audit_log_change_payload(): class TestAuditLogChange: def test_deserialize_with_known_component_less_converter_and_values(self, mock_app): test_audit_log_change_payload = {"key": "rate_limit_per_user", "old_value": "0", "new_value": "60"} - mock_role_zero = mock.MagicMock(guilds.PartialGuildRole, id=123123123312312) - mock_role_one = mock.MagicMock(guilds.PartialGuildRole, id=568651298858074123) - with mock.patch.object(guilds.PartialGuildRole, "deserialize", side_effect=[mock_role_zero, mock_role_one]): + mock_role_zero = mock.MagicMock(guilds.PartialRole, id=123123123312312) + mock_role_one = mock.MagicMock(guilds.PartialRole, id=568651298858074123) + with mock.patch.object(guilds.PartialRole, "deserialize", side_effect=[mock_role_zero, mock_role_one]): audit_log_change_obj = audit_logs.AuditLogChange.deserialize(test_audit_log_change_payload, app=mock_app) assert audit_log_change_obj._app is mock_app assert audit_log_change_obj.key is audit_logs.AuditLogChangeKey.RATE_LIMIT_PER_USER @@ -117,11 +117,11 @@ def test_deserialize_with_known_component_less_converter_and_values(self, mock_a assert audit_log_change_obj.new_value == datetime.timedelta(seconds=60) def test_deserialize_with_known_component_full_converter_and_values(self, test_audit_log_change_payload, mock_app): - mock_role_zero = mock.MagicMock(guilds.PartialGuildRole, id=123123123312312) - mock_role_one = mock.MagicMock(guilds.PartialGuildRole, id=568651298858074123) - with mock.patch.object(guilds.PartialGuildRole, "deserialize", side_effect=[mock_role_zero, mock_role_one]): + mock_role_zero = mock.MagicMock(guilds.PartialRole, id=123123123312312) + mock_role_one = mock.MagicMock(guilds.PartialRole, id=568651298858074123) + with mock.patch.object(guilds.PartialRole, "deserialize", side_effect=[mock_role_zero, mock_role_one]): audit_log_change_obj = audit_logs.AuditLogChange.deserialize(test_audit_log_change_payload, app=mock_app) - guilds.PartialGuildRole.deserialize.assert_has_calls( + guilds.PartialRole.deserialize.assert_has_calls( [ mock.call({"id": "123123123312312", "name": "aRole"}, app=mock_app), mock.call({"id": "568651298858074123", "name": "Casual"}, app=mock_app), @@ -136,9 +136,9 @@ def test_deserialize_with_known_component_full_converter_and_no_values( self, test_audit_log_change_payload, mock_app ): test_audit_log_change_payload = {"key": "$add"} - with mock.patch.object(guilds.PartialGuildRole, "deserialize"): + with mock.patch.object(guilds.PartialRole, "deserialize"): audit_log_change_obj = audit_logs.AuditLogChange.deserialize(test_audit_log_change_payload, app=mock_app) - guilds.PartialGuildRole.deserialize.assert_not_called() + guilds.PartialRole.deserialize.assert_not_called() assert audit_log_change_obj._app is mock_app assert audit_log_change_obj.key is audit_logs.AuditLogChangeKey.ADD_ROLE_TO_MEMBER assert audit_log_change_obj.old_value is None @@ -148,9 +148,9 @@ def test_deserialize_with_known_component_less_converter_and_no_values( self, test_audit_log_change_payload, mock_app ): test_audit_log_change_payload = {"key": "rate_limit_per_user"} - with mock.patch.object(guilds.PartialGuildRole, "deserialize"): + with mock.patch.object(guilds.PartialRole, "deserialize"): audit_log_change_obj = audit_logs.AuditLogChange.deserialize(test_audit_log_change_payload, app=mock_app) - guilds.PartialGuildRole.deserialize.assert_not_called() + guilds.PartialRole.deserialize.assert_not_called() assert audit_log_change_obj._app is mock_app assert audit_log_change_obj.key is audit_logs.AuditLogChangeKey.RATE_LIMIT_PER_USER assert audit_log_change_obj.old_value is None diff --git a/tests/hikari/models/test_guilds.py b/tests/hikari/models/test_guilds.py index dc7e9fd464..81975c1b33 100644 --- a/tests/hikari/models/test_guilds.py +++ b/tests/hikari/models/test_guilds.py @@ -334,7 +334,7 @@ def test_partial_guild_role_payload(self): } def test_deserialize(self, test_partial_guild_role_payload, mock_app): - partial_guild_role_obj = guilds.PartialGuildRole.deserialize(test_partial_guild_role_payload, app=mock_app) + partial_guild_role_obj = guilds.PartialRole.deserialize(test_partial_guild_role_payload, app=mock_app) assert partial_guild_role_obj.name == "WE DEM BOYZZ!!!!!!" @@ -353,7 +353,7 @@ def test_guild_role_payload(self): } def test_deserialize(self, test_guild_role_payload, mock_app): - guild_role_obj = guilds.GuildRole.deserialize(test_guild_role_payload, app=mock_app) + guild_role_obj = guilds.Role.deserialize(test_guild_role_payload, app=mock_app) assert guild_role_obj.color == 3_447_003 assert guild_role_obj.is_hoisted is True assert guild_role_obj.position == 0 @@ -362,7 +362,7 @@ def test_deserialize(self, test_guild_role_payload, mock_app): assert guild_role_obj.is_mentionable is False def test_serialize_full_role(self): - guild_role_obj = guilds.GuildRole( + guild_role_obj = guilds.Role( name="aRole", color=colors.Color(444), is_hoisted=True, @@ -382,7 +382,7 @@ def test_serialize_full_role(self): } def test_serialize_partial_role(self): - guild_role_obj = guilds.GuildRole(name="aRole", id=123) + guild_role_obj = guilds.Role(name="aRole", id=123) assert guild_role_obj.serialize() == { "name": "aRole", "color": 0, @@ -984,7 +984,7 @@ def test_deserialize( assert guild_obj.verification_level is guilds.GuildVerificationLevel.VERY_HIGH assert guild_obj.default_message_notifications is guilds.GuildMessageNotificationsLevel.ONLY_MENTIONS assert guild_obj.explicit_content_filter is guilds.GuildExplicitContentFilterLevel.ALL_MEMBERS - assert guild_obj.roles == {41771983423143936: guilds.GuildRole.deserialize(test_roles_payload)} + assert guild_obj.roles == {41771983423143936: guilds.Role.deserialize(test_roles_payload)} assert guild_obj.roles[41771983423143936]._gateway_consumer is mock_app assert guild_obj.emojis == {41771983429993937: mock_emoji} assert guild_obj.mfa_level is guilds.GuildMFALevel.ELEVATED diff --git a/tests/hikari/rest/test_channel.py b/tests/hikari/rest/test_channel.py index 9bbfb2dec2..51468cee1a 100644 --- a/tests/hikari/rest/test_channel.py +++ b/tests/hikari/rest/test_channel.py @@ -654,7 +654,7 @@ async def test_update_channel_overwrite_without_optionals(self, rest_channel_log @pytest.mark.parametrize( "target", [ - mock.MagicMock(guilds.GuildRole, id=bases.Snowflake(9999), __int__=guilds.GuildRole.__int__), + mock.MagicMock(guilds.Role, id=bases.Snowflake(9999), __int__=guilds.Role.__int__), mock.MagicMock(users.User, id=bases.Snowflake(9999), __int__=users.User.__int__), ], ) @@ -751,7 +751,7 @@ async def test_delete_channel_overwrite(self, rest_channel_logic_impl, channel, @pytest.mark.parametrize( "target", [ - mock.MagicMock(guilds.GuildRole, id=bases.Snowflake(123123123), __int__=guilds.GuildRole.__int__), + mock.MagicMock(guilds.Role, id=bases.Snowflake(123123123), __int__=guilds.Role.__int__), mock.MagicMock(users.User, id=bases.Snowflake(123123123), __int__=users.User.__int__), ], ) diff --git a/tests/hikari/rest/test_guild.py b/tests/hikari/rest/test_guild.py index 6eaca2c935..5743e626ec 100644 --- a/tests/hikari/rest/test_guild.py +++ b/tests/hikari/rest/test_guild.py @@ -337,7 +337,7 @@ async def test_fetch_guild_emojis(self, rest_guild_logic_impl, guild): @pytest.mark.asyncio @_helpers.parametrize_valid_id_formats_for_models("guild", 93443949, guilds.Guild) - @_helpers.parametrize_valid_id_formats_for_models("role", 537340989808050216, guilds.GuildRole) + @_helpers.parametrize_valid_id_formats_for_models("role", 537340989808050216, guilds.Role) async def test_create_guild_emoji_with_optionals(self, rest_guild_logic_impl, guild, role): mock_emoji_payload = {"id": "229292929", "animated": True} mock_emoji_obj = mock.MagicMock(emojis.KnownCustomEmoji) @@ -404,7 +404,7 @@ async def test_update_guild_emoji_without_optionals(self, rest_guild_logic_impl, @pytest.mark.asyncio @_helpers.parametrize_valid_id_formats_for_models("guild", 93443949, guilds.Guild) @_helpers.parametrize_valid_id_formats_for_models("emoji", 4123321, emojis.KnownCustomEmoji) - @_helpers.parametrize_valid_id_formats_for_models("role", 123123123, guilds.GuildRole) + @_helpers.parametrize_valid_id_formats_for_models("role", 123123123, guilds.Role) async def test_update_guild_emoji_with_optionals(self, rest_guild_logic_impl, guild, emoji, role): mock_emoji_payload = {"id": "202020", "name": "Nyaa", "animated": True} mock_emoji_obj = mock.MagicMock(emojis.KnownCustomEmoji) @@ -441,7 +441,7 @@ async def test_create_guild_with_optionals(self, rest_guild_logic_impl, region): mock_image_obj = mock.MagicMock(files.BaseStream) mock_image_obj.read = mock.AsyncMock(return_value=mock_image_data) mock_role_payload = {"permissions": 123123} - mock_role_obj = mock.MagicMock(guilds.GuildRole) + mock_role_obj = mock.MagicMock(guilds.Role) mock_role_obj.serialize = mock.MagicMock(return_value=mock_role_payload) mock_channel_payload = {"type": 2, "name": "aChannel"} mock_channel_obj = mock.MagicMock(channels.GuildChannelBuilder) @@ -730,7 +730,7 @@ def test_fetch_members(self, rest_guild_logic_impl): @pytest.mark.asyncio @_helpers.parametrize_valid_id_formats_for_models("guild", 229292992, guilds.Guild) @_helpers.parametrize_valid_id_formats_for_models("user", 1010101010, users.User) - @_helpers.parametrize_valid_id_formats_for_models("role", 11100010, guilds.GuildRole) + @_helpers.parametrize_valid_id_formats_for_models("role", 11100010, guilds.Role) @_helpers.parametrize_valid_id_formats_for_models("channel", 33333333, channels.GuildVoiceChannel) async def test_update_member_with_optionals(self, rest_guild_logic_impl, guild, user, role, channel): rest_guild_logic_impl._session.modify_guild_member.return_value = ... @@ -797,7 +797,7 @@ async def test_update_my_member_nickname_without_reason(self, rest_guild_logic_i @pytest.mark.asyncio @_helpers.parametrize_valid_id_formats_for_models("guild", 123123123, guilds.Guild) @_helpers.parametrize_valid_id_formats_for_models("user", 4444444, users.User) - @_helpers.parametrize_valid_id_formats_for_models("role", 101010101, guilds.GuildRole) + @_helpers.parametrize_valid_id_formats_for_models("role", 101010101, guilds.Role) async def test_add_role_to_member_with_reason(self, rest_guild_logic_impl, guild, user, role): rest_guild_logic_impl._session.add_guild_member_role.return_value = ... assert await rest_guild_logic_impl.add_role_to_member(guild, user, role, reason="Get role'd") is None @@ -808,7 +808,7 @@ async def test_add_role_to_member_with_reason(self, rest_guild_logic_impl, guild @pytest.mark.asyncio @_helpers.parametrize_valid_id_formats_for_models("guild", 123123123, guilds.Guild) @_helpers.parametrize_valid_id_formats_for_models("user", 4444444, users.User) - @_helpers.parametrize_valid_id_formats_for_models("role", 101010101, guilds.GuildRole) + @_helpers.parametrize_valid_id_formats_for_models("role", 101010101, guilds.Role) async def test_add_role_to_member_without_reason(self, rest_guild_logic_impl, guild, user, role): rest_guild_logic_impl._session.add_guild_member_role.return_value = ... assert await rest_guild_logic_impl.add_role_to_member(guild, user, role) is None @@ -819,7 +819,7 @@ async def test_add_role_to_member_without_reason(self, rest_guild_logic_impl, gu @pytest.mark.asyncio @_helpers.parametrize_valid_id_formats_for_models("guild", 123123123, guilds.Guild) @_helpers.parametrize_valid_id_formats_for_models("user", 4444444, users.User) - @_helpers.parametrize_valid_id_formats_for_models("role", 101010101, guilds.GuildRole) + @_helpers.parametrize_valid_id_formats_for_models("role", 101010101, guilds.Role) async def test_remove_role_from_member_with_reason(self, rest_guild_logic_impl, guild, user, role): rest_guild_logic_impl._session.remove_guild_member_role.return_value = ... assert await rest_guild_logic_impl.remove_role_from_member(guild, user, role, reason="Get role'd") is None @@ -830,7 +830,7 @@ async def test_remove_role_from_member_with_reason(self, rest_guild_logic_impl, @pytest.mark.asyncio @_helpers.parametrize_valid_id_formats_for_models("guild", 123123123, guilds.Guild) @_helpers.parametrize_valid_id_formats_for_models("user", 4444444, users.User) - @_helpers.parametrize_valid_id_formats_for_models("role", 101010101, guilds.GuildRole) + @_helpers.parametrize_valid_id_formats_for_models("role", 101010101, guilds.Role) async def test_remove_role_from_member_without_reason(self, rest_guild_logic_impl, guild, user, role): rest_guild_logic_impl._session.remove_guild_member_role.return_value = ... assert await rest_guild_logic_impl.remove_role_from_member(guild, user, role) is None @@ -932,20 +932,20 @@ async def test_unban_member_without_reason(self, rest_guild_logic_impl, guild, u @_helpers.parametrize_valid_id_formats_for_models("guild", 574921006817476608, guilds.Guild) async def test_fetch_roles(self, rest_guild_logic_impl, guild): mock_role_payload = {"id": "33030", "permissions": 333, "name": "ROlE"} - mock_role_obj = mock.MagicMock(guilds.GuildRole, id=33030) + mock_role_obj = mock.MagicMock(guilds.Role, id=33030) rest_guild_logic_impl._session.get_guild_roles.return_value = [mock_role_payload] - with mock.patch.object(guilds.GuildRole, "deserialize", return_value=mock_role_obj): + with mock.patch.object(guilds.Role, "deserialize", return_value=mock_role_obj): assert await rest_guild_logic_impl.fetch_roles(guild) == {33030: mock_role_obj} rest_guild_logic_impl._session.get_guild_roles.assert_called_once_with(guild_id="574921006817476608") - guilds.GuildRole.deserialize.assert_called_once_with(mock_role_payload, app=rest_guild_logic_impl._app) + guilds.Role.deserialize.assert_called_once_with(mock_role_payload, app=rest_guild_logic_impl._app) @pytest.mark.asyncio @_helpers.parametrize_valid_id_formats_for_models("guild", 574921006817476608, guilds.Guild) async def test_create_role_with_optionals(self, rest_guild_logic_impl, guild): mock_role_payload = {"id": "033030", "permissions": 333, "name": "ROlE"} - mock_role_obj = mock.MagicMock(guilds.GuildRole) + mock_role_obj = mock.MagicMock(guilds.Role) rest_guild_logic_impl._session.create_guild_role.return_value = mock_role_payload - with mock.patch.object(guilds.GuildRole, "deserialize", return_value=mock_role_obj): + with mock.patch.object(guilds.Role, "deserialize", return_value=mock_role_obj): result = await rest_guild_logic_impl.create_role( guild, name="Roleington", @@ -965,15 +965,15 @@ async def test_create_role_with_optionals(self, rest_guild_logic_impl, guild): mentionable=False, reason="And then there was a role.", ) - guilds.GuildRole.deserialize.assert_called_once_with(mock_role_payload, app=rest_guild_logic_impl._app) + guilds.Role.deserialize.assert_called_once_with(mock_role_payload, app=rest_guild_logic_impl._app) @pytest.mark.asyncio @_helpers.parametrize_valid_id_formats_for_models("guild", 574921006817476608, guilds.Guild) async def test_create_role_without_optionals(self, rest_guild_logic_impl, guild): mock_role_payload = {"id": "033030", "permissions": 333, "name": "ROlE"} - mock_role_obj = mock.MagicMock(guilds.GuildRole) + mock_role_obj = mock.MagicMock(guilds.Role) rest_guild_logic_impl._session.create_guild_role.return_value = mock_role_payload - with mock.patch.object(guilds.GuildRole, "deserialize", return_value=mock_role_obj): + with mock.patch.object(guilds.Role, "deserialize", return_value=mock_role_obj): result = await rest_guild_logic_impl.create_role(guild) assert result is mock_role_obj rest_guild_logic_impl._session.create_guild_role.assert_called_once_with( @@ -985,32 +985,32 @@ async def test_create_role_without_optionals(self, rest_guild_logic_impl, guild) mentionable=..., reason=..., ) - guilds.GuildRole.deserialize.assert_called_once_with(mock_role_payload, app=rest_guild_logic_impl._app) + guilds.Role.deserialize.assert_called_once_with(mock_role_payload, app=rest_guild_logic_impl._app) @pytest.mark.asyncio @_helpers.parametrize_valid_id_formats_for_models("guild", 574921006817476608, guilds.Guild) - @_helpers.parametrize_valid_id_formats_for_models("role", 123123, guilds.GuildRole) - @_helpers.parametrize_valid_id_formats_for_models("additional_role", 123456, guilds.GuildRole) + @_helpers.parametrize_valid_id_formats_for_models("role", 123123, guilds.Role) + @_helpers.parametrize_valid_id_formats_for_models("additional_role", 123456, guilds.Role) async def test_reposition_roles(self, rest_guild_logic_impl, guild, role, additional_role): mock_role_payload = {"id": "033030", "permissions": 333, "name": "ROlE"} - mock_role_obj = mock.MagicMock(guilds.GuildRole) + mock_role_obj = mock.MagicMock(guilds.Role) rest_guild_logic_impl._session.modify_guild_role_positions.return_value = [mock_role_payload] - with mock.patch.object(guilds.GuildRole, "deserialize", return_value=mock_role_obj): + with mock.patch.object(guilds.Role, "deserialize", return_value=mock_role_obj): result = await rest_guild_logic_impl.reposition_roles(guild, (1, role), (2, additional_role)) assert result == [mock_role_obj] rest_guild_logic_impl._session.modify_guild_role_positions.assert_called_once_with( "574921006817476608", ("123123", 1), ("123456", 2) ) - guilds.GuildRole.deserialize.assert_called_once_with(mock_role_payload, app=rest_guild_logic_impl._app) + guilds.Role.deserialize.assert_called_once_with(mock_role_payload, app=rest_guild_logic_impl._app) @pytest.mark.asyncio @_helpers.parametrize_valid_id_formats_for_models("guild", 574921006817476608, guilds.Guild) - @_helpers.parametrize_valid_id_formats_for_models("role", 123123, guilds.GuildRole) + @_helpers.parametrize_valid_id_formats_for_models("role", 123123, guilds.Role) async def test_update_role_with_optionals(self, rest_guild_logic_impl, guild, role): mock_role_payload = {"id": "033030", "permissions": 333, "name": "ROlE"} - mock_role_obj = mock.MagicMock(guilds.GuildRole) + mock_role_obj = mock.MagicMock(guilds.Role) rest_guild_logic_impl._session.modify_guild_role.return_value = mock_role_payload - with mock.patch.object(guilds.GuildRole, "deserialize", return_value=mock_role_obj): + with mock.patch.object(guilds.Role, "deserialize", return_value=mock_role_obj): result = await rest_guild_logic_impl.update_role( guild, role, @@ -1032,16 +1032,16 @@ async def test_update_role_with_optionals(self, rest_guild_logic_impl, guild, ro mentionable=False, reason="Why not?", ) - guilds.GuildRole.deserialize.assert_called_once_with(mock_role_payload, app=rest_guild_logic_impl._app) + guilds.Role.deserialize.assert_called_once_with(mock_role_payload, app=rest_guild_logic_impl._app) @pytest.mark.asyncio @_helpers.parametrize_valid_id_formats_for_models("guild", 574921006817476608, guilds.Guild) - @_helpers.parametrize_valid_id_formats_for_models("role", 123123, guilds.GuildRole) + @_helpers.parametrize_valid_id_formats_for_models("role", 123123, guilds.Role) async def test_update_role_without_optionals(self, rest_guild_logic_impl, guild, role): mock_role_payload = {"id": "033030", "permissions": 333, "name": "ROlE"} - mock_role_obj = mock.MagicMock(guilds.GuildRole) + mock_role_obj = mock.MagicMock(guilds.Role) rest_guild_logic_impl._session.modify_guild_role.return_value = mock_role_payload - with mock.patch.object(guilds.GuildRole, "deserialize", return_value=mock_role_obj): + with mock.patch.object(guilds.Role, "deserialize", return_value=mock_role_obj): assert await rest_guild_logic_impl.update_role(guild, role) is mock_role_obj rest_guild_logic_impl._session.modify_guild_role.assert_called_once_with( guild_id="574921006817476608", @@ -1053,11 +1053,11 @@ async def test_update_role_without_optionals(self, rest_guild_logic_impl, guild, mentionable=..., reason=..., ) - guilds.GuildRole.deserialize.assert_called_once_with(mock_role_payload, app=rest_guild_logic_impl._app) + guilds.Role.deserialize.assert_called_once_with(mock_role_payload, app=rest_guild_logic_impl._app) @pytest.mark.asyncio @_helpers.parametrize_valid_id_formats_for_models("guild", 574921006817476608, guilds.Guild) - @_helpers.parametrize_valid_id_formats_for_models("role", 123123, guilds.GuildRole) + @_helpers.parametrize_valid_id_formats_for_models("role", 123123, guilds.Role) async def test_delete_role(self, rest_guild_logic_impl, guild, role): rest_guild_logic_impl._session.delete_guild_role.return_value = ... assert await rest_guild_logic_impl.delete_role(guild, role) is None From 0e4acfac4797c4716443c17d846080bba7058d2c Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Fri, 22 May 2020 17:01:56 +0100 Subject: [PATCH 371/922] Fixed multipart form bug, added more endpoints, allowed logging formdata correctly for debug tracing. --- hikari/models/files.py | 55 +++++++++++----- hikari/net/http_client.py | 13 +++- hikari/net/rest.py | 134 ++++++++++++++++++++++++++++++++------ hikari/net/tracing.py | 39 ++++++++++- 4 files changed, 197 insertions(+), 44 deletions(-) diff --git a/hikari/models/files.py b/hikari/models/files.py index 7bd9cd1e74..542faaa028 100644 --- a/hikari/models/files.py +++ b/hikari/models/files.py @@ -174,6 +174,36 @@ async def read(self, count: int = -1) -> bytes: return data[:count] +class _AsyncByteIterable: + __slots__ = ("_byte_content",) + + def __init__(self, byte_content: bytes) -> None: + self._byte_content = byte_content + + async def __aiter__(self): + for i in range(0, len(self._byte_content), MAGIC_NUMBER): + yield self._byte_content[i : i + MAGIC_NUMBER] + + +class _MemorizedAsyncIteratorDecorator: + __slots__ = ("_async_iterator", "_exhausted", "_buff") + + def __init__(self, async_iterator: typing.AsyncIterator) -> None: + self._async_iterator = async_iterator + self._exhausted = False + self._buff = bytearray() + + async def __aiter__(self): + if self._exhausted: + async for chunk in _AsyncByteIterable(self._buff): + yield chunk + else: + async for chunk in self._async_iterator: + self._buff.extend(chunk) + yield chunk + self._exhausted = True + + class ByteStream(BaseStream): """A simple data stream that wraps something that gives bytes. @@ -323,17 +353,13 @@ def __init__(self, filename: str, obj: ___VALID_TYPES___) -> None: self._filename = filename if inspect.isasyncgenfunction(obj): - self._obj = obj() - return + obj = obj() - if more_asyncio.is_async_iterable(obj): - obj = obj.__aiter__() - - if more_asyncio.is_async_iterator(obj): - self._obj = self._aiter_async_iterator(obj) + if inspect.isasyncgen(obj) or more_asyncio.is_async_iterator(obj): + self._obj = _MemorizedAsyncIteratorDecorator(obj) return - if inspect.isasyncgen(obj): + if more_asyncio.is_async_iterable(obj): self._obj = obj return @@ -344,14 +370,13 @@ def __init__(self, filename: str, obj: ___VALID_TYPES___) -> None: obj = self._to_bytes(obj) if isinstance(obj, bytes): - self._obj = self._aiter_bytes(obj) + self._obj = _AsyncByteIterable(obj) return raise TypeError(f"Expected bytes-like object or async generator, got {type(obj).__qualname__}") - async def __aiter__(self) -> typing.AsyncGenerator[bytes]: - async for chunk in self._obj: - yield self._to_bytes(chunk) + def __aiter__(self) -> typing.AsyncGenerator[bytes]: + return self._obj.__aiter__() @property def filename(self) -> str: @@ -366,12 +391,6 @@ async def _aiter_async_iterator( except StopAsyncIteration: pass - @staticmethod - async def _aiter_bytes(bytes_: bytes) -> typing.AsyncGenerator[bytes]: - stream = io.BytesIO(bytes_) - while chunk := stream.read(MAGIC_NUMBER): - yield chunk - @staticmethod def _to_bytes(byte_like: ___VALID_BYTE_TYPES___) -> bytes: if isinstance(byte_like, str): diff --git a/hikari/net/http_client.py b/hikari/net/http_client.py index 8607d7a984..2e0f19105b 100644 --- a/hikari/net/http_client.py +++ b/hikari/net/http_client.py @@ -29,6 +29,7 @@ import aiohttp.typedefs +from hikari.models import unset from hikari.net import tracing @@ -95,6 +96,7 @@ class HTTPClient(abc.ABC): # pylint:disable=too-many-instance-attributes _APPLICATION_JSON: typing.Final[str] = "application/json" _APPLICATION_X_WWW_FORM_URLENCODED: typing.Final[str] = "application/x-www-form-urlencoded" _APPLICATION_OCTET_STREAM: typing.Final[str] = "application/octet-stream" + _MULTIPART_FORM_DATA: typing.Final[str] = "multipart/form-data" logger: logging.Logger """The logger to use for this object.""" @@ -245,8 +247,13 @@ async def _perform_request( The HTTP response. """ if isinstance(body, (dict, list)): - body = bytes(json.dumps(body), "utf-8") - headers["content-type"] = self._APPLICATION_JSON + kwargs = {"json": body} + + elif isinstance(body, aiohttp.FormData): + kwargs = {"data": body} + + else: + kwargs = {} trace_request_ctx = types.SimpleNamespace() trace_request_ctx.request_body = body @@ -256,7 +263,6 @@ async def _perform_request( url=url, params=query, headers=headers, - data=body, allow_redirects=self._allow_redirects, proxy=self._proxy_url, proxy_auth=self._proxy_auth, @@ -265,6 +271,7 @@ async def _perform_request( ssl_context=self._ssl_context, timeout=self._request_timeout, trace_request_ctx=trace_request_ctx, + **kwargs, ) async def _create_ws( diff --git a/hikari/net/rest.py b/hikari/net/rest.py index 7a78337f50..bb74c885bd 100644 --- a/hikari/net/rest.py +++ b/hikari/net/rest.py @@ -40,7 +40,7 @@ from hikari.internal import ratelimits from hikari.models import bases from hikari.models import channels -from hikari.models import embeds +from hikari.models import embeds as embeds_ from hikari.models import emojis from hikari.models import files from hikari.models import guilds @@ -164,7 +164,7 @@ async def _request( query: typing.Union[unset.Unset, typing.Mapping[str, str]] = unset.UNSET, body: typing.Union[unset.Unset, aiohttp.FormData, more_typing.JSONType] = unset.UNSET, reason: typing.Union[unset.Unset, str] = unset.UNSET, - suppress_authorization_header: bool = False, + no_auth: bool = False, ) -> typing.Optional[more_typing.JSONObject, more_typing.JSONArray, bytes, str]: # Make a ratelimit-protected HTTP request to a JSON endpoint and expect some form # of JSON response. If an error occurs, the response body is returned in the @@ -181,9 +181,12 @@ async def _request( headers["x-ratelimit-precision"] = "millisecond" headers["accept"] = self._APPLICATION_JSON - if self._token is not None and not suppress_authorization_header: + if self._token is not None and not no_auth: headers["authorization"] = self._token + if unset.is_unset(body): + body = None + if not unset.is_unset(reason): headers["x-audit-log-reason"] = reason @@ -354,6 +357,17 @@ def _generate_allowed_mentions( # want to use empty lists for blacklisting elements rather than just not including blacklisted elements. return allowed_mentions + def _build_message_creation_form( + self, payload: typing.Dict[str, typing.Any], attachments: typing.Sequence[files.BaseStream], + ) -> aiohttp.FormData: + form = aiohttp.FormData() + form.add_field("payload_json", json.dumps(payload), content_type=self._APPLICATION_JSON) + for i, attachment in enumerate(attachments): + form.add_field( + f"file{i}", attachment, filename=attachment.filename, content_type=self._APPLICATION_OCTET_STREAM + ) + return form + async def fetch_channel( self, channel: typing.Union[channels.PartialChannel, bases.Snowflake, int], /, ) -> channels.PartialChannel: @@ -605,7 +619,7 @@ async def create_message( channel: typing.Union[channels.TextChannel, bases.UniqueObjectT], text: typing.Union[unset.Unset, typing.Any] = unset.UNSET, *, - embed: typing.Union[unset.Unset, embeds.Embed] = unset.UNSET, + embed: typing.Union[unset.Unset, embeds_.Embed] = unset.UNSET, attachments: typing.Union[unset.Unset, typing.Sequence[files.BaseStream]] = unset.UNSET, tts: typing.Union[unset.Unset, bool] = unset.UNSET, nonce: typing.Union[unset.Unset, str] = unset.UNSET, @@ -615,26 +629,20 @@ async def create_message( ) -> messages.Message: route = routes.POST_CHANNEL_MESSAGES.compile(channel=conversions.cast_to_str_id(channel)) - payload = {} + payload = {"allowed_mentions": self._generate_allowed_mentions(mentions_everyone, user_mentions, role_mentions)} conversions.put_if_specified(payload, "content", text, str) conversions.put_if_specified(payload, "embed", embed, self._app.entity_factory.serialize_embed) conversions.put_if_specified(payload, "nonce", nonce) conversions.put_if_specified(payload, "tts", tts) - payload["mentions"] = self._generate_allowed_mentions(mentions_everyone, user_mentions, role_mentions) + attachments = [] if unset.is_unset(attachments) else [a for a in attachments] - if (unset.is_unset(embed) or not embed.assets_to_upload) and attachments is unset.UNSET: - response = await self._request(route, body=payload) - else: - form = aiohttp.FormData() - form.add_field("payload_json", json.dumps(payload), content_type=self._APPLICATION_JSON) - file_list = [*attachments] - if embed is not None and embed.assets_to_upload: - file_list.extend(embed.assets_to_upload) - for i, file in enumerate(file_list): - form.add_field(f"file{i}", file, content_type=self._APPLICATION_OCTET_STREAM) + if not unset.is_unset(embed): + attachments.extend(embed.assets_to_upload) - response = await self._request(route, body=form) + response = await self._request( + route, body=self._build_message_creation_form(payload, attachments) if attachments else payload + ) return self._app.entity_factory.deserialize_message(response) @@ -644,7 +652,7 @@ async def edit_message( message: typing.Union[messages.Message, bases.UniqueObjectT], text: typing.Union[unset.Unset, typing.Any] = unset.UNSET, *, - embed: typing.Union[unset.Unset, embeds.Embed] = unset.UNSET, + embed: typing.Union[unset.Unset, embeds_.Embed] = unset.UNSET, mentions_everyone: bool = False, user_mentions: typing.Union[typing.Collection[typing.Union[users.User, bases.UniqueObjectT]], bool] = True, role_mentions: typing.Union[typing.Collection[typing.Union[bases.UniqueObjectT, guilds.Role]], bool] = True, @@ -657,7 +665,7 @@ async def edit_message( conversions.put_if_specified(payload, "content", text, str) conversions.put_if_specified(payload, "embed", embed, self._app.entity_factory.serialize_embed) conversions.put_if_specified(payload, "flags", flags) - payload["mentions"] = self._generate_allowed_mentions(mentions_everyone, user_mentions, role_mentions) + payload["allowed_mentions"] = self._generate_allowed_mentions(mentions_everyone, user_mentions, role_mentions) response = await self._request(route, body=payload) return self._app.entity_factory.deserialize_message(response) @@ -752,7 +760,8 @@ async def create_webhook( reason: typing.Union[unset.Unset, str] = unset.UNSET, ) -> webhooks.Webhook: payload = {"name": name} - conversions.put_if_specified(payload, "avatar", await avatar.fetch_data_uri()) + if not unset.is_unset(avatar): + payload["avatar"] = await avatar.fetch_data_uri() route = routes.POST_WEBHOOK.compile(channel=conversions.cast_to_str_id(channel)) response = await self._request(route, body=payload, reason=reason) return self._app.entity_factory.deserialize_webhook(response) @@ -785,6 +794,91 @@ async def fetch_guild_webhooks( response = await self._request(route) return {bases.Snowflake(w["id"]): self._app.entity_factory.deserialize_webhook(w) for w in response} + async def edit_webhook( + self, + webhook: typing.Union[webhooks.Webhook, bases.UniqueObjectT], + /, + *, + token: typing.Union[unset.Unset, str] = unset.UNSET, + name: typing.Union[unset.Unset, str] = unset.UNSET, + avatar: typing.Union[unset.Unset, files.BaseStream] = unset.UNSET, + channel: typing.Union[unset.Unset, channels.TextChannel, bases.UniqueObjectT] = unset.UNSET, + reason: typing.Union[unset.Unset, str] = unset.UNSET, + ) -> webhooks.Webhook: + payload = {} + conversions.put_if_specified(payload, "name", name) + conversions.put_if_specified(payload, "channel", channel, conversions.cast_to_str_id) + if not unset.is_unset(avatar): + payload["avatar"] = await avatar.fetch_data_uri() + + if unset.is_unset(token): + route = routes.PATCH_WEBHOOK.compile(webhook=conversions.cast_to_str_id(webhook)) + else: + route = routes.PATCH_WEBHOOK_WITH_TOKEN.compile(webhook=conversions.cast_to_str_id(webhook), token=token) + + response = await self._request(route, body=payload, reason=reason) + return self._app.entity_factory.deserialize_webhook(response) + + async def delete_webhook( + self, + webhook: typing.Union[webhooks.Webhook, bases.UniqueObjectT], + /, + *, + token: typing.Union[unset.Unset, str] = unset.UNSET, + ) -> None: + if unset.is_unset(token): + route = routes.DELETE_WEBHOOK.compile(webhook=conversions.cast_to_str_id(webhook)) + else: + route = routes.DELETE_WEBHOOK_WITH_TOKEN.compile(webhook=conversions.cast_to_str_id(webhook), token=token) + await self._request(route) + + async def execute_embed( + self, + webhook: typing.Union[webhooks.Webhook, bases.UniqueObjectT], + text: typing.Union[unset.Unset, typing.Any] = unset.UNSET, + *, + token: typing.Union[unset.Unset, str] = unset.UNSET, + username: typing.Union[unset.Unset, str] = unset.UNSET, + avatar_url: typing.Union[unset.Unset, str] = unset.UNSET, + embeds: typing.Union[unset.Unset, typing.Sequence[embeds_.Embed]] = unset.UNSET, + attachments: typing.Union[unset.Unset, typing.Sequence[files.BaseStream]] = unset.UNSET, + tts: typing.Union[unset.Unset, bool] = unset.UNSET, + wait: typing.Union[unset.Unset, bool] = unset.UNSET, + mentions_everyone: bool = False, + user_mentions: typing.Union[typing.Collection[typing.Union[users.User, bases.UniqueObjectT]], bool] = True, + role_mentions: typing.Union[typing.Collection[typing.Union[bases.UniqueObjectT, guilds.Role]], bool] = True, + ) -> messages.Message: + if unset.is_unset(token): + route = routes.POST_WEBHOOK.compile(webhook=conversions.cast_to_str_id(webhook)) + no_auth = False + else: + route = routes.POST_WEBHOOK_WITH_TOKEN.compile(webhook=conversions.cast_to_str_id(webhook), token=token) + no_auth = True + + attachments = [] if unset.is_unset(attachments) else [a for a in attachments] + serialized_embeds = [] + + if not unset.is_unset(embeds): + for embed in embeds: + attachments.extend(embed.assets_to_upload) + serialized_embeds.append(self._app.entity_factory.serialize_embed(embed)) + + payload = {"mentions": self._generate_allowed_mentions(mentions_everyone, user_mentions, role_mentions)} + conversions.put_if_specified(payload, "content", text, str) + conversions.put_if_specified(payload, "embeds", serialized_embeds) + conversions.put_if_specified(payload, "username", username) + conversions.put_if_specified(payload, "avatar_url", avatar_url) + conversions.put_if_specified(payload, "tts", tts) + conversions.put_if_specified(payload, "wait", wait) + + response = await self._request( + route, + body=self._build_message_creation_form(payload, attachments) if attachments else payload, + no_auth=no_auth, + ) + + return self._app.entity_factory.deserialize_message(response) + # Keep this last, then it doesn't cause problems with the imports. def typing( self, channel: typing.Union[channels.TextChannel, bases.UniqueObjectT], / diff --git a/hikari/net/tracing.py b/hikari/net/tracing.py index c2faeb8afa..9b8a2ba520 100644 --- a/hikari/net/tracing.py +++ b/hikari/net/tracing.py @@ -22,11 +22,12 @@ __all__ = ["BaseTracer", "CFRayTracer", "DebugTracer"] import functools +import io import logging import time import uuid -import aiohttp +import aiohttp.abc class BaseTracer: @@ -85,6 +86,17 @@ async def on_request_end(self, _, ctx, params): ) +class _ByteStreamWriter(io.BytesIO, aiohttp.abc.AbstractStreamWriter): + async def write(self, data) -> None: + io.BytesIO.write(self, data) + + write_eof = NotImplemented + drain = NotImplemented + enable_compression = NotImplemented + enable_chunking = NotImplemented + write_headers = NotImplemented + + class DebugTracer(BaseTracer): """Provides verbose _debug logging of requests. @@ -100,18 +112,39 @@ class DebugTracer(BaseTracer): to send logs to anyone. """ + @staticmethod + async def _format_body(body): + if isinstance(body, aiohttp.FormData): + # We have to either copy the internal multipart writer, or we have + # to make a dummy second instance and read from that. I am putting + # my bets on the second option, simply because it reduces the + # risk of screwing up the original payload in some weird edge case. + # These objects have stateful stuff somewhere by the looks. + copy_of_data = aiohttp.FormData() + setattr(copy_of_data, "_fields", getattr(copy_of_data, "_fields")) + byte_writer = _ByteStreamWriter() + await copy_of_data().write(byte_writer) + return repr(byte_writer.read()) + return repr(body) + async def on_request_start(self, _, ctx, params): """Log an outbound request.""" ctx.identifier = f"request_id:{uuid.uuid4()}" ctx.start_time = time.perf_counter() + body = ( + await self._format_body(ctx.trace_request_ctx.request_body) + if hasattr(ctx.trace_request_ctx, "request_body") + else "" + ) + self.logger.debug( "%s %s [%s]\n request headers: %s\n request body: %s", params.method, params.url, ctx.identifier, dict(params.headers), - getattr(ctx.trace_request_ctx, "request_body", ""), + body, ) async def on_request_end(self, _, ctx, params): @@ -126,7 +159,7 @@ async def on_request_end(self, _, ctx, params): latency, ctx.identifier, dict(response.headers), - await response.read() if "content-type" in response.headers else "", + await self._format_body(await response.read()) if "content-type" in response.headers else "", ) async def on_request_exception(self, _, ctx, params): From ccf0a1b9ae24e40e432648cab64992a0a59a5ad9 Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Fri, 22 May 2020 23:05:39 +0100 Subject: [PATCH 372/922] Ironed out most race conditions and fixed gateway exception handling. Fixed ability for zookeeper to detect exceptions and respond to them correctly. --- hikari/errors.py | 16 -- hikari/http_settings.py | 6 +- hikari/impl/gateway_zookeeper.py | 93 ++++++--- hikari/internal/more_typing.py | 3 +- hikari/net/gateway.py | 307 ++++++++++++++-------------- hikari/net/http_client.py | 3 +- hikari/net/rest.py | 2 +- tests/hikari/gateway/test_client.py | 2 +- 8 files changed, 225 insertions(+), 207 deletions(-) diff --git a/hikari/errors.py b/hikari/errors.py index 6f7e4f56c4..2a5776fb24 100644 --- a/hikari/errors.py +++ b/hikari/errors.py @@ -33,7 +33,6 @@ "ServerHTTPErrorResponse", "GatewayServerClosedConnectionError", "GatewayClientClosedError", - "GatewayClientDisconnectedError", "GatewayError", ] @@ -107,21 +106,6 @@ def __init__(self, reason: str = "The gateway client has been closed") -> None: super().__init__(reason) -class GatewayClientDisconnectedError(GatewayError): - """An exception raised when the bot client-side disconnects unexpectedly. - - Parameters - ---------- - reason : str - A string explaining the issue. - """ - - __slots__ = () - - def __init__(self, reason: str = "The gateway client has disconnected unexpectedly") -> None: - super().__init__(reason) - - class GatewayServerClosedConnectionError(GatewayError): """An exception raised when the server closes the connection. diff --git a/hikari/http_settings.py b/hikari/http_settings.py index 5fc84dbb73..e3de20fe18 100644 --- a/hikari/http_settings.py +++ b/hikari/http_settings.py @@ -61,9 +61,9 @@ class HTTPSettings: ssl_context: typing.Optional[ssl.SSLContext] = None """The optional SSL context to use.""" - tcp_connector: typing.Optional[aiohttp.TCPConnector] = None - """This may otherwise be `None` to use the default settings provided by - `aiohttp`. + tcp_connector_factory: typing.Optional[typing.Callable[[], aiohttp.TCPConnector]] = None + """An optional TCP connector factory to use. A connector will be created + for each component (each shard, and each REST instance). """ trust_env: bool = False diff --git a/hikari/impl/gateway_zookeeper.py b/hikari/impl/gateway_zookeeper.py index 25eac925a6..e61105f9cb 100644 --- a/hikari/impl/gateway_zookeeper.py +++ b/hikari/impl/gateway_zookeeper.py @@ -60,8 +60,11 @@ def __init__( version: int, ) -> None: self._aiohttp_config = config + + # This is a little hacky workaround to boost performance. We force + + self._gather_task = None self._request_close_event = asyncio.Event() - self._url = url self._shard_count = shard_count self._shards = { shard_id: gateway.Gateway( @@ -83,7 +86,7 @@ def __init__( ) for shard_id in shard_ids } - self._running = False + self._tasks = {} @property def gateway_shards(self) -> typing.Mapping[int, gateway.Gateway]: @@ -94,45 +97,71 @@ def shard_count(self) -> int: return self._shard_count async def start(self) -> None: - self._running = True + self._tasks.clear() + self._gather_task = None + self._request_close_event.clear() self.logger.info("starting %s", conversions.pluralize(len(self._shards), "shard")) start_time = time.perf_counter() - for i, shard_id in enumerate(self._shards): - if i > 0: - self.logger.info("waiting for 5 seconds until next shard can start") + try: + for i, shard_id in enumerate(self._shards): + if self._request_close_event.is_set(): + break - try: - await asyncio.wait_for(self._request_close_event.wait(), timeout=5) - # If this passes, the bot got shut down while sharding. - return - except asyncio.TimeoutError: - # Continue, no close occurred. - pass + if i > 0: + self.logger.info("waiting for 5 seconds until next shard can start") - shard_obj = self._shards[shard_id] - await shard_obj.start() + completed, _ = await asyncio.wait( + self._tasks.values(), timeout=5, return_when=asyncio.FIRST_COMPLETED + ) - finish_time = time.perf_counter() + if completed: + raise completed.pop().exception() + shard_obj = self._shards[shard_id] + self._tasks[shard_id] = await shard_obj.start() + finally: + if len(self._tasks) != len(self._shards): + self.logger.warning( + "application aborted midway through initialization, will begin shutting down %s shard(s)", + len(self._tasks), + ) + await self._abort() + return + + finish_time = time.perf_counter() + self._gather_task = asyncio.create_task( + self._gather(), name=f"shard zookeeper for {len(self._shards)} shard(s)" + ) self.logger.info("started %s shard(s) in approx %.2fs", len(self._shards), finish_time - start_time) if hasattr(self, "event_dispatcher") and isinstance(self.event_dispatcher, event_dispatcher.IEventDispatcher): await self.event_dispatcher.dispatch(other.StartedEvent()) async def join(self) -> None: - if self._running: - await asyncio.gather(*(shard_obj.join() for shard_obj in self._shards.values())) + if self._gather_task is not None: + await self._gather_task + + async def _abort(self): + for shard_id in self._tasks: + await self._shards[shard_id].close() + await asyncio.gather(*self._tasks.values(), return_exceptions=True) + + async def _gather(self): + try: + await asyncio.gather(*self._tasks.values()) + finally: + self.logger.debug("gather failed, shutting down shard(s)") + await self.close() async def close(self) -> None: - if self._running: + if self._tasks: # This way if we cancel the stopping task, we still shut down properly. self._request_close_event.set() - self._running = False - self.logger.info("stopping %s shard(s)", len(self._shards)) - start_time = time.perf_counter() + + self.logger.info("stopping %s shard(s)", len(self._tasks)) has_event_dispatcher = hasattr(self, "event_dispatcher") and isinstance( self.event_dispatcher, event_dispatcher.IEventDispatcher @@ -143,42 +172,40 @@ async def close(self) -> None: # noinspection PyUnresolvedReferences await self.event_dispatcher.dispatch(other.StoppingEvent()) - await asyncio.gather(*(shard_obj.close() for shard_obj in self._shards.values())) + await self._abort() finally: - finish_time = time.perf_counter() - self.logger.info("stopped %s shard(s) in approx %.2fs", len(self._shards), finish_time - start_time) + self._tasks.clear() if has_event_dispatcher: # noinspection PyUnresolvedReferences await self.event_dispatcher.dispatch(other.StoppedEvent()) + async def _run(self): + await self.start() + await self.join() + def run(self): loop = asyncio.get_event_loop() def sigterm_handler(*_): - raise KeyboardInterrupt() + loop.create_task(self.close()) try: with contextlib.suppress(NotImplementedError): # Not implemented on Windows loop.add_signal_handler(signal.SIGTERM, sigterm_handler) - loop.run_until_complete(self.start()) - loop.run_until_complete(self.join()) - - self.logger.info("client has shut down") + loop.run_until_complete(self._run()) except KeyboardInterrupt: self.logger.info("received signal to shut down client") - loop.run_until_complete(self.close()) - # Apparently you have to alias except clauses or you get an - # UnboundLocalError. raise finally: loop.run_until_complete(self.close()) with contextlib.suppress(NotImplementedError): # Not implemented on Windows loop.remove_signal_handler(signal.SIGTERM) + self.logger.info("client has shut down") async def update_presence( self, diff --git a/hikari/internal/more_typing.py b/hikari/internal/more_typing.py index d1272039ae..5fbdd0b69d 100644 --- a/hikari/internal/more_typing.py +++ b/hikari/internal/more_typing.py @@ -43,6 +43,7 @@ from typing import Any as _Any from typing import AnyStr as _AnyStr from typing import Coroutine as _Coroutine +from typing import Generator as _Generator from typing import Mapping as _Mapping from typing import Optional as _Optional from typing import Protocol as _Protocol @@ -198,7 +199,7 @@ def get_name(self) -> str: def set_name(self, value: str, /) -> None: """See `asyncio.Task.set_name`.""" - def __await__(self) -> Coroutine[T_contra]: + def __await__(self) -> _Generator[T_contra, _Any, None]: ... diff --git a/hikari/net/gateway.py b/hikari/net/gateway.py index 3934ab8675..d42521f5d7 100644 --- a/hikari/net/gateway.py +++ b/hikari/net/gateway.py @@ -24,6 +24,7 @@ import asyncio import json +import math import time import typing import urllib.parse @@ -126,6 +127,14 @@ class _Zombie(RuntimeError): __slots__ = () +class _ClientClosed(RuntimeError): + __slots__ = () + + +class _SocketClosed(RuntimeError): + __slots__ = () + + @attr.s(auto_attribs=True, slots=True) class _InvalidSession(RuntimeError): can_resume: bool = False @@ -197,7 +206,7 @@ def __init__( ) -> None: super().__init__( allow_redirects=config.allow_redirects, - connector=config.tcp_connector, + connector=config.tcp_connector_factory() if config.tcp_connector_factory else None, debug=debug, logger_name=f"{type(self).__module__}.{type(self).__qualname__}.{shard_id}", proxy_auth=config.proxy_auth, @@ -209,14 +218,14 @@ def __init__( trust_env=config.trust_env, ) self._activity = initial_activity + self._backoff = ratelimits.ExponentialBackOff(base=1.85, maximum=600, initial_increment=2) self._dispatch = dispatch self._handshake_event = asyncio.Event() - self._heartbeat_task = None self._idle_since = initial_idle_since self._intents = intents self._is_afk = initial_is_afk + self._last_run_started_at = float("nan") self._request_close_event = asyncio.Event() - self._run_task = None self._seq = None self._session_id = None self._shard_id = shard_id @@ -227,9 +236,9 @@ def __init__( self._version = version self._ws = None self._zlib = None + self._zombied = False self.connected_at = float("nan") - self.disconnect_count = 0 self.heartbeat_interval = float("nan") self.heartbeat_latency = float("nan") self.last_heartbeat_sent = float("nan") @@ -251,115 +260,138 @@ def __init__( @property def is_alive(self) -> bool: """Return whether the shard is alive.""" - return self._run_task is not None and not self._run_task.done() + return not math.isnan(self.connected_at) - async def start(self) -> None: - """Start the gateway and wait for the handshake to complete. + async def start(self) -> more_typing.Task[None]: + """Start the shard, wait for it to become ready. - This will continue to keep the connection alive in the background once - the handshake succeeds. - - If the handshake fails, this will raise the corresponding exception - immediately. + Returns + ------- + asyncio.Task + The task containing the shard running logic. Awaiting this will + wait until the shard has shut down before returning. """ - task = asyncio.create_task(self.run(), name=f"gateway shard {self._shard_id} starter") + run_task = asyncio.create_task(self._run(), name=f"shard {self._shard_id} keep-alive") + await self._handshake_event.wait() + return run_task - handshake_waiter = asyncio.create_task( - self._handshake_event.wait(), name=f"gateway shard {self._shard_id} wait for handshake" - ) + async def close(self) -> None: + """Close the websocket.""" + if not self._request_close_event.is_set(): + if self.is_alive: + self.logger.info("received request to shut down shard") + else: + self.logger.debug("shard marked as closed before it was able to start") + self._request_close_event.set() - await asyncio.wait([handshake_waiter, task], return_when=asyncio.FIRST_COMPLETED) + if self._ws is not None: + self.logger.warning("gateway client closed by user, will not attempt to restart") + await self._close_ws(_GatewayCloseCode.RFC_6455_NORMAL_CLOSURE, "user shut down application") - if not handshake_waiter.done(): - handshake_waiter.cancel() + async def _run(self) -> None: + """Start the shard and wait for it to shut down.""" + try: + # This may be set if we are stuck in a reconnect loop. + while not self._request_close_event.is_set() and await self._run_once(): + pass - async def close(self) -> None: - """Request that the shard shuts down, then wait for it to close.""" - if self.is_alive: - self.logger.debug("received signal to shut shard down, will proceed to close runners") - self._request_close_event.set() + # Allow zookeepers to stop gathering tasks for each shard. + raise errors.GatewayClientClosedError() + finally: + # This is set to ensure that the `start' waiter does not deadlock if + # we cannot connect successfully. It is a hack, but it works. + self._handshake_event.set() + await super().close() + + async def _run_once(self) -> bool: + # returns `True` if we can reconnect, or `False` otherwise. + self._request_close_event.clear() + + if self._now() - self._last_run_started_at < 30: + # Interrupt sleep immediately if a request to close is fired. + wait_task = asyncio.create_task(self._request_close_event.wait()) try: - await self.join() - finally: - await super().close() - else: - self.logger.debug("received signal to shut shard down, but I am not running anyway") + backoff = next(self._backoff) + self.logger.debug("backing off for %ss", backoff) + await asyncio.wait_for(wait_task, timeout=backoff) + except asyncio.TimeoutError: + pass - async def join(self) -> None: - """Wait for the shard to shut down.""" - if self._run_task is not None: - await self._run_task + # Do this after; it prevents backing off on the first try. + self._last_run_started_at = self._now() - async def run(self) -> None: - """Start the shard and wait for it to shut down.""" - self._run_task = asyncio.Task.current_task() - self._run_task.set_name(f"gateway shard {self._shard_id} runner") + try: + self.logger.debug("creating websocket connection to %s", self.url) + self._ws = await self._create_ws(self.url) + self.connected_at = self._now() - # Clear the reference when we die. - self._run_task.add_done_callback(lambda _: setattr(self, "_run_task", None)) + self._zlib = zlib.decompressobj() - back_off = ratelimits.ExponentialBackOff(base=1.85, maximum=600, initial_increment=2) - last_start = self._now() - do_not_back_off = True + self._handshake_event.clear() + self._request_close_event.clear() + + await self._handshake() + + # Technically we are connected after the hello, but this ensures we can send and receive + # before firing that event. + asyncio.create_task(self._dispatch(self, "CONNECTED", {}), name=f"shard {self._shard_id} CONNECTED") + + # We should ideally set this after HELLO, but it should be fine + # here as well. If we don't heartbeat in time, something probably + # went majorly wrong anyway. + heartbeat = asyncio.create_task(self._pulse(), name=f"shard {self._shard_id} heartbeat") - while True: try: - if not do_not_back_off and self._now() - last_start < 30: - next_back_off = next(back_off) - self.logger.info( - "restarted within 30 seconds, will back off for %.2fs", next_back_off, - ) - await asyncio.sleep(next_back_off) - else: - back_off.reset() + await self._poll_events() + finally: + heartbeat.cancel() + + return False + + except aiohttp.ClientConnectorError as ex: + self.logger.error( + "failed to connect to Discord because %s.%s: %s", type(ex).__module__, type(ex).__qualname__, str(ex), + ) - last_start = self._now() - do_not_back_off = False + except _InvalidSession as ex: + if ex.can_resume: + self.logger.warning("invalid session, so will attempt to resume session %s", self._session_id) + await self._close_ws(_GatewayCloseCode.DO_NOT_INVALIDATE_SESSION, "invalid session (resume)") + else: + self.logger.warning("invalid session, so will attempt to reconnect with new session") + await self._close_ws(_GatewayCloseCode.RFC_6455_NORMAL_CLOSURE, "invalid session (no resume)") + self._seq = None + self._session_id = None + + except _Reconnect: + self.logger.warning("instructed by Discord to reconnect and resume session %s", self._session_id) + self._backoff.reset() + await self._close_ws(_GatewayCloseCode.DO_NOT_INVALIDATE_SESSION, "reconnecting") + + except _SocketClosed: + # The socket has already closed, so no need to close it again. + if not self._zombied and not self._request_close_event.is_set(): + # This will occur due to a network issue such as a network adapter going down. + self.logger.warning("unexpected socket closure, will attempt to reconnect") + else: + self._backoff.reset() + return not self._request_close_event.is_set() - await self._run_once() - raise RuntimeError("This shouldn't be reached unless an expected condition is never met") + except Exception as ex: + self.logger.error("unexpected exception occurred, shard will now die", exc_info=ex) + await self._close_ws(_GatewayCloseCode.RFC_6455_UNEXPECTED_CONDITION, "unexpected error occurred") + raise - except aiohttp.ClientConnectorError as ex: - self.logger.exception( - "failed to connect to Discord to initialize a websocket connection", exc_info=ex, + finally: + if not math.isnan(self.connected_at): + # Only dispatch this if we actually connected before we failed! + asyncio.create_task( + self._dispatch(self, "DISCONNECTED", {}), name=f"shard {self._shard_id} DISCONNECTED" ) - except _Zombie: - self.logger.warning("entered a zombie state and will be restarted") - # No need to shut down, the socket is dead anyway. - - except _InvalidSession as ex: - if ex.can_resume: - self.logger.warning("invalid session, so will attempt to resume session %s", self._session_id) - await self._close_ws(_GatewayCloseCode.DO_NOT_INVALIDATE_SESSION, "invalid session (resume)") - else: - self.logger.warning("invalid session, so will attempt to reconnect with new session") - await self._close_ws(_GatewayCloseCode.RFC_6455_NORMAL_CLOSURE, "invalid session (no resume)") - self._seq = None - self._session_id = None - - do_not_back_off = True - await asyncio.sleep(5) - - except _Reconnect: - self.logger.warning("instructed by Discord to reconnect and resume session %s", self._session_id) - do_not_back_off = True - await self._close_ws(_GatewayCloseCode.DO_NOT_INVALIDATE_SESSION, "reconnecting") - await asyncio.sleep(5) - - except errors.GatewayClientDisconnectedError: - self.logger.warning("unexpected connection close, will attempt to reconnect") - # No need to shut down, the socket is dead anyway. - - except errors.GatewayClientClosedError: - self.logger.warning("gateway client closed by user, will not attempt to restart") - await self._close_ws(_GatewayCloseCode.RFC_6455_NORMAL_CLOSURE, "user shut down application") - return + self.connected_at = float("nan") - except Exception as ex: - self.logger.critical("unexpected exception occurred, shard will now die", exc_info=ex) - await self._close_ws(_GatewayCloseCode.RFC_6455_UNEXPECTED_CONDITION, "unexpected error occurred") - raise + return True async def update_presence( self, @@ -432,41 +464,6 @@ async def _close_ws(self, code: _GatewayCloseCode, message: str): self.logger.debug("sending close frame with code %s and message %r", code.value, message) await self._ws.close(code=code, message=bytes(message, "utf-8")) - async def _run_once(self) -> None: - try: - self.logger.debug("creating websocket connection to %s", self.url) - self._ws = await self._create_ws(self.url) - self._zlib = zlib.decompressobj() - - self._handshake_event.clear() - self._request_close_event.clear() - - await self._handshake() - - # We should ideally set this after HELLO, but it should be fine - # here as well. If we don't heartbeat in time, something probably - # went majorly wrong anyway. - heartbeat_task = asyncio.create_task(self._pulse(), name=f"gateway shard {self._shard_id} heartbeat") - - poll_events_task = asyncio.create_task(self._poll_events(), name=f"gateway shard {self._shard_id} poll") - - try: - _, pending = await asyncio.wait([heartbeat_task, poll_events_task], return_when=asyncio.FIRST_COMPLETED) - for future in pending: - future.cancel() - - await asyncio.gather(*pending, return_exceptions=True) - - except asyncio.TimeoutError: - raise _Zombie() - else: - raise errors.GatewayClientClosedError() - - finally: - asyncio.create_task( - self._dispatch(self, "DISCONNECTED", {}), name=f"gateway shard {self._shard_id} dispatch DISCONNECTED" - ) - async def _handshake(self) -> None: # HELLO! message = await self._receive_json_payload() @@ -476,9 +473,6 @@ async def _handshake(self) -> None: self.heartbeat_interval = message["d"]["heartbeat_interval"] / 1_000.0 - asyncio.create_task( - self._dispatch(self, "CONNECTED", {}), name=f"gateway shard {self._shard_id} dispatch CONNECTED" - ) self.logger.debug("received HELLO") if self._session_id is not None: @@ -514,20 +508,36 @@ async def _handshake(self) -> None: await self._send_json(payload) async def _pulse(self) -> None: - while not self._request_close_event.is_set(): - time_since_message = self._now() - self.last_message_received - if self.heartbeat_interval < time_since_message: - self.logger.error("connection is a zombie, haven't received any message for %ss", time_since_message) - raise _Zombie() + try: + while not self._request_close_event.is_set(): + now = self._now() + time_since_message = now - self.last_message_received + time_since_heartbeat_sent = now - self.last_heartbeat_sent + + if self.heartbeat_interval < time_since_message: + self.logger.error( + "connection is a zombie, haven't received any message for %ss, last heartbeat sent %ss ago", + time_since_message, + time_since_heartbeat_sent, + ) + self._zombied = True + await self._close_ws(_GatewayCloseCode.RFC_6455_NORMAL_CLOSURE, "zombie connection") + return - self.logger.debug("preparing to send HEARTBEAT [s:%s, interval:%ss]", self._seq, self.heartbeat_interval) - await self._send_json({"op": _GatewayOpcode.HEARTBEAT, "d": self._seq}) - self.last_heartbeat_sent = self._now() + self.logger.debug( + "preparing to send HEARTBEAT [s:%s, interval:%ss]", self._seq, self.heartbeat_interval + ) + await self._send_json({"op": _GatewayOpcode.HEARTBEAT, "d": self._seq}) + self.last_heartbeat_sent = self._now() - try: - await asyncio.wait_for(self._request_close_event.wait(), timeout=self.heartbeat_interval) - except asyncio.TimeoutError: - pass + try: + await asyncio.wait_for(self._request_close_event.wait(), timeout=self.heartbeat_interval) + except asyncio.TimeoutError: + pass + + except asyncio.CancelledError: + # This happens if the poll task has stopped. It isn't a problem we need to report. + pass async def _poll_events(self) -> None: while not self._request_close_event.is_set(): @@ -547,9 +557,7 @@ async def _poll_events(self) -> None: self.logger.info("connection has resumed [session:%s, seq:%s]", self._session_id, self._seq) self._handshake_event.set() - asyncio.create_task( - self._dispatch(self, event, data), name=f"gateway shard {self._shard_id} dispatch {event}" - ) + asyncio.create_task(self._dispatch(self, event, data), name=f"shard {self._shard_id} {event}") elif op == _GatewayOpcode.HEARTBEAT: self.logger.debug("received HEARTBEAT; sending HEARTBEAT ACK") @@ -597,12 +605,9 @@ async def _receive_json_payload(self) -> more_typing.JSONObject: ) raise errors.GatewayServerClosedConnectionError(reason, close_code, can_reconnect, False, True) + elif message.type == aiohttp.WSMsgType.CLOSING or message.type == aiohttp.WSMsgType.CLOSED: - if self._request_close_event.is_set(): - self.logger.debug("user has requested the gateway to close") - raise errors.GatewayClientClosedError() - self.logger.debug("connection has been closed unexpectedly, probably a network issue") - raise errors.GatewayClientDisconnectedError() + raise _SocketClosed() else: # Assume exception for now. ex = self._ws.exception() diff --git a/hikari/net/http_client.py b/hikari/net/http_client.py index 2e0f19105b..1e04d5d9af 100644 --- a/hikari/net/http_client.py +++ b/hikari/net/http_client.py @@ -193,8 +193,8 @@ async def close(self) -> None: """Close the client safely.""" with contextlib.suppress(Exception): await self.__client_session.close() + self.logger.debug("closed client session object %r", self.__client_session) self.__client_session = None - self.logger.debug("closed client session") def _acquire_client_session(self) -> aiohttp.ClientSession: """Acquire a client session to make requests with. @@ -212,6 +212,7 @@ def _acquire_client_session(self) -> aiohttp.ClientSession: json_serialize=json.dumps, trace_configs=[t.trace_config for t in self._tracers], ) + self.logger.debug("acquired new client session object %r", self.__client_session) return self.__client_session async def _perform_request( diff --git a/hikari/net/rest.py b/hikari/net/rest.py index bb74c885bd..cfdb4cf0ad 100644 --- a/hikari/net/rest.py +++ b/hikari/net/rest.py @@ -132,7 +132,7 @@ def __init__( ) -> None: super().__init__( allow_redirects=config.allow_redirects, - connector=config.tcp_connector, + connector=config.tcp_connector_factory() if config.tcp_connector_factory else None, debug=debug, logger_name=f"{type(self).__module__}.{type(self).__qualname__}", proxy_auth=config.proxy_auth, diff --git a/tests/hikari/gateway/test_client.py b/tests/hikari/gateway/test_client.py index 9dc68a29dd..ea32b09eb5 100644 --- a/tests/hikari/gateway/test_client.py +++ b/tests/hikari/gateway/test_client.py @@ -342,7 +342,7 @@ async def test__keep_alive_repeats_silently_if_task_returns(self, shard_client_o errors.GatewayInvalidSessionError(False), errors.GatewayInvalidSessionError(True), errors.GatewayMustReconnectError(), - errors.GatewayClientDisconnectedError(), + errors.GatewayNetworkError(), ], ) @pytest.mark.asyncio From a229b5b5d39c0bc31b47dfab594a9ce289fc8ae8 Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Fri, 22 May 2020 23:15:29 +0100 Subject: [PATCH 373/922] Purged tests for net that no longer work --- tests/hikari/gateway/test_client.py | 5 - tests/hikari/rest/test_base.py | 84 -- tests/hikari/rest/test_channel.py | 853 ------------ tests/hikari/rest/test_client.py | 98 -- tests/hikari/rest/test_gateway.py | 58 - tests/hikari/rest/test_guild.py | 1287 ------------------ tests/hikari/rest/test_invite.py | 71 - tests/hikari/rest/test_me.py | 246 ---- tests/hikari/rest/test_oauth2.py | 51 - tests/hikari/rest/test_react.py | 266 ---- tests/hikari/rest/test_session.py | 1921 --------------------------- tests/hikari/rest/test_user.py | 50 - tests/hikari/rest/test_voice.py | 48 - tests/hikari/rest/test_webhook.py | 286 ---- 14 files changed, 5324 deletions(-) delete mode 100644 tests/hikari/rest/test_base.py delete mode 100644 tests/hikari/rest/test_channel.py delete mode 100644 tests/hikari/rest/test_client.py delete mode 100644 tests/hikari/rest/test_gateway.py delete mode 100644 tests/hikari/rest/test_guild.py delete mode 100644 tests/hikari/rest/test_invite.py delete mode 100644 tests/hikari/rest/test_me.py delete mode 100644 tests/hikari/rest/test_oauth2.py delete mode 100644 tests/hikari/rest/test_react.py delete mode 100644 tests/hikari/rest/test_session.py delete mode 100644 tests/hikari/rest/test_user.py delete mode 100644 tests/hikari/rest/test_voice.py delete mode 100644 tests/hikari/rest/test_webhook.py diff --git a/tests/hikari/gateway/test_client.py b/tests/hikari/gateway/test_client.py index ea32b09eb5..7fad021ffd 100644 --- a/tests/hikari/gateway/test_client.py +++ b/tests/hikari/gateway/test_client.py @@ -338,11 +338,6 @@ async def test__keep_alive_repeats_silently_if_task_returns(self, shard_client_o "error", [ aiohttp.ClientConnectorError(mock.MagicMock(), mock.MagicMock()), - errors.GatewayZombiedError(), - errors.GatewayInvalidSessionError(False), - errors.GatewayInvalidSessionError(True), - errors.GatewayMustReconnectError(), - errors.GatewayNetworkError(), ], ) @pytest.mark.asyncio diff --git a/tests/hikari/rest/test_base.py b/tests/hikari/rest/test_base.py deleted file mode 100644 index 6b2c222bc3..0000000000 --- a/tests/hikari/rest/test_base.py +++ /dev/null @@ -1,84 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# Copyright © Nekoka.tt 2019-2020 -# -# This file is part of Hikari. -# -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . -import mock -import pytest - -from hikari import application -from hikari.internal import ratelimits -from hikari.net.rest import base -from hikari.net import buckets -from hikari.net.rest import session - - -@pytest.fixture() -def low_level_rest_impl() -> session.RESTSession: - return mock.MagicMock( - spec=session.RESTSession, - global_ratelimiter=mock.create_autospec(ratelimits.ManualRateLimiter, spec_set=True), - bucket_ratelimiters=mock.create_autospec(buckets.RESTBucketManager, spec_set=True), - ) - - -@pytest.fixture() -def rest_clients_impl(low_level_rest_impl) -> base.BaseRESTComponent: - class RestClientImpl(base.BaseRESTComponent): - def __init__(self): - super().__init__(mock.MagicMock(application.Application), low_level_rest_impl) - - return RestClientImpl() - - -class TestBaseRESTComponentContextManager: - @pytest.mark.asyncio - async def test___aenter___and___aexit__(self, rest_clients_impl): - rest_clients_impl.close = mock.AsyncMock() - async with rest_clients_impl as client: - assert client is rest_clients_impl - rest_clients_impl.close.assert_called_once_with() - - -class TestBaseRESTComponentClose: - @pytest.mark.asyncio - async def test_close_delegates_to_low_level_rest_impl(self, rest_clients_impl): - await rest_clients_impl.close() - rest_clients_impl._session.close.assert_awaited_once_with() - - -class TestBaseRESTComponentQueueSizeProperties: - def test_global_ratelimit_queue_size(self, rest_clients_impl, low_level_rest_impl): - low_level_rest_impl.global_ratelimiter.queue = [object() for _ in range(107)] - assert rest_clients_impl.global_ratelimit_queue_size == 107 - - def test_route_ratelimit_queue_size(self, rest_clients_impl, low_level_rest_impl): - low_level_rest_impl.bucket_ratelimiters.real_hashes_to_buckets = { - "aaaaa;1234;5678;9101123": mock.create_autospec( - buckets.RESTBucket, spec_set=True, queue=[object() for _ in range(30)] - ), - "aaaaa;1234;5678;9101122": mock.create_autospec( - buckets.RESTBucket, spec_set=True, queue=[object() for _ in range(29)] - ), - "aaaab;1234;5678;9101123": mock.create_autospec( - buckets.RESTBucket, spec_set=True, queue=[object() for _ in range(28)] - ), - "zzzzz;1234;5678;9101123": mock.create_autospec( - buckets.RESTBucket, spec_set=True, queue=[object() for _ in range(20)] - ), - } - - assert rest_clients_impl.route_ratelimit_queue_size == 107 diff --git a/tests/hikari/rest/test_channel.py b/tests/hikari/rest/test_channel.py deleted file mode 100644 index 51468cee1a..0000000000 --- a/tests/hikari/rest/test_channel.py +++ /dev/null @@ -1,853 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# Copyright © Nekoka.tt 2019-2020 -# -# This file is part of Hikari. -# -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . -import contextlib -import datetime -import inspect - -import attr -import mock -import pytest - -from hikari import application -from hikari.internal import helpers -from hikari.models import bases -from hikari.models import channels -from hikari.models import embeds -from hikari.models import files -from hikari.models import guilds -from hikari.models import invites -from hikari.models import messages -from hikari.models import users -from hikari.models import webhooks -from hikari.net.rest import channel -from hikari.net.rest import session -from tests.hikari import _helpers - - -@pytest.mark.asyncio -class TestMessagePaginator: - @pytest.fixture - def mock_session(self): - return mock.MagicMock(spec_set=session.RESTSession) - - @pytest.fixture - def mock_app(self): - return mock.MagicMock(spec_set=application.Application) - - @pytest.fixture - def message_cls(self): - with mock.patch.object(messages, "Message") as message_cls: - yield message_cls - - @pytest.mark.parametrize("_direction", ["before", "after", "around"]) - def test_init_first_id_is_date(self, mock_session, mock_app, direction): - date = datetime.datetime(2015, 11, 15, 23, 13, 46, 709000, tzinfo=datetime.timezone.utc) - expected_id = 115590097100865536 - channel_id = 1234567 - pag = channel._MessagePaginator(mock_app, channel_id, direction, date, mock_session) - assert pag._first_id == str(expected_id) - assert pag._channel_id == str(channel_id) - assert pag._direction == direction - assert pag._session is mock_session - assert pag._app is mock_app - - @pytest.mark.parametrize("_direction", ["before", "after", "around"]) - def test_init_first_id_is_id(self, mock_session, mock_app, direction): - expected_id = 115590097100865536 - channel_id = 1234567 - pag = channel._MessagePaginator(mock_app, channel_id, direction, expected_id, mock_session) - assert pag._first_id == str(expected_id) - assert pag._channel_id == str(channel_id) - assert pag._direction == direction - assert pag._session is mock_session - assert pag._app is mock_app - - @pytest.mark.parametrize("_direction", ["before", "after", "around"]) - async def test_next_chunk_makes_api_call(self, mock_session, mock_app, message_cls, direction): - channel_obj = mock.MagicMock(__int__=lambda _: 55) - - mock_session.get_channel_messages = mock.AsyncMock(return_value=[]) - pag = channel._MessagePaginator(mock_app, channel_obj, direction, "12345", mock_session) - pag._first_id = "12345" - - await pag._next_chunk() - - mock_session.get_channel_messages.assert_awaited_once_with( - **{direction: "12345", "channel": "55", "limit": 100} - ) - - @pytest.mark.parametrize("_direction", ["before", "after", "around"]) - async def test_next_chunk_empty_response_returns_None(self, mock_session, mock_app, message_cls, direction): - channel_obj = mock.MagicMock(__int__=lambda _: 55) - - pag = channel._MessagePaginator(mock_app, channel_obj, direction, "12345", mock_session) - pag._first_id = "12345" - - mock_session.get_channel_messages = mock.AsyncMock(return_value=[]) - - assert await pag._next_chunk() is None - - @pytest.mark.parametrize(["_direction", "expect_reverse"], [("before", False), ("after", True), ("around", False)]) - async def test_next_chunk_updates_first_id(self, mock_session, mock_app, message_cls, expect_reverse, direction): - return_payload = [ - {"id": "1234", ...: ...}, - {"id": "3456", ...: ...}, - {"id": "3333", ...: ...}, - {"id": "512", ...: ...}, - ] - - mock_session.get_channel_messages = mock.AsyncMock(return_value=return_payload) - - channel_obj = mock.MagicMock(__int__=lambda _: 99) - - pag = channel._MessagePaginator(mock_app, channel_obj, direction, "12345", mock_session) - pag._first_id = "12345" - - await pag._next_chunk() - - assert pag._first_id == "1234" if expect_reverse else "512" - - @pytest.mark.parametrize(["_direction", "expect_reverse"], [("before", False), ("after", True), ("around", False)]) - async def test_next_chunk_returns_generator(self, mock_session, mock_app, message_cls, expect_reverse, direction): - return_payload = [ - {"id": "1234", ...: ...}, - {"id": "3456", ...: ...}, - {"id": "3333", ...: ...}, - {"id": "512", ...: ...}, - ] - - @attr.s(auto_attribs=True) - class DummyResponse: - id: int - - real_values = [ - DummyResponse(1234), - DummyResponse(3456), - DummyResponse(3333), - DummyResponse(512), - ] - - if expect_reverse: - real_values.reverse() - - assert len(real_values) == len(return_payload) - - message_cls.deserialize = mock.MagicMock(side_effect=real_values.copy()) - mock_session.get_channel_messages = mock.AsyncMock(return_value=return_payload) - - channel_obj = mock.MagicMock(__int__=lambda _: 99) - - pag = channel._MessagePaginator(mock_app, channel_obj, direction, "12345", mock_session) - pag._first_id = "12345" - - generator = await pag._next_chunk() - - assert inspect.isgenerator(generator) - - for i, item in enumerate(generator, start=1): - assert item == real_values.pop(0) - - assert locals()["i"] == 4, "Not iterated correctly somehow" - assert not real_values - - # Clear the generator result. - # This doesn't test anything, but there is an issue with coverage not detecting generator - # exit conditions properly. This fixes something that would otherwise be marked as - # uncovered behaviour erroneously. - # https://stackoverflow.com/questions/35317757/python-unittest-branch-coverage-seems-to-miss-executed-generator-in-zip - with pytest.raises(StopIteration): - next(generator) - - -class TestRESTChannel: - @pytest.fixture() - def rest_channel_logic_impl(self): - mock_app = mock.MagicMock(application.Application) - mock_low_level_restful_client = mock.MagicMock(session.RESTSession) - - class RESTChannelLogicImpl(channel.RESTChannelComponent): - def __init__(self): - super().__init__(mock_app, mock_low_level_restful_client) - - return RESTChannelLogicImpl() - - @pytest.mark.asyncio - @_helpers.parametrize_valid_id_formats_for_models("channel", 1234, channels.PartialChannel) - async def test_fetch_channel(self, rest_channel_logic_impl, channel): - mock_payload = {"id": "49494994", "type": 3} - mock_channel_obj = mock.MagicMock(channels.PartialChannel) - rest_channel_logic_impl._session.get_channel.return_value = mock_payload - with mock.patch.object(channels, "deserialize_channel", return_value=mock_channel_obj): - assert await rest_channel_logic_impl.fetch_channel(channel) is mock_channel_obj - rest_channel_logic_impl._session.get_channel.assert_called_once_with(channel_id="1234") - channels.deserialize_channel.assert_called_once_with(mock_payload, app=rest_channel_logic_impl._app) - - @pytest.mark.asyncio - @_helpers.parametrize_valid_id_formats_for_models("channel", 379953393319542784, channels.PartialChannel) - @_helpers.parametrize_valid_id_formats_for_models("parent_channel", 115590097100865541, channels.PartialChannel) - @pytest.mark.parametrize("rate_limit_per_user", [42, datetime.timedelta(seconds=42)]) - async def test_update_channel_with_optionals( - self, rest_channel_logic_impl, channel, parent_channel, rate_limit_per_user - ): - mock_payload = {"name": "Qts", "type": 2} - mock_channel_obj = mock.MagicMock(channels.PartialChannel) - mock_overwrite_payload = {"type": "user", "id": 543543543} - mock_overwrite_obj = mock.MagicMock(channels.PermissionOverwrite) - mock_overwrite_obj.serialize = mock.MagicMock(return_value=mock_overwrite_payload) - rest_channel_logic_impl._session.modify_channel.return_value = mock_payload - with mock.patch.object(channels, "deserialize_channel", return_value=mock_channel_obj): - result = await rest_channel_logic_impl.update_channel( - channel=channel, - name="ohNo", - position=7, - topic="camelsAreGreat", - nsfw=True, - bitrate=32000, - user_limit=42, - rate_limit_per_user=rate_limit_per_user, - permission_overwrites=[mock_overwrite_obj], - parent_category=parent_channel, - reason="Get Nyaa'd.", - ) - assert result is mock_channel_obj - rest_channel_logic_impl._session.modify_channel.assert_called_once_with( - channel_id="379953393319542784", - name="ohNo", - position=7, - topic="camelsAreGreat", - nsfw=True, - rate_limit_per_user=42, - bitrate=32000, - user_limit=42, - permission_overwrites=[mock_overwrite_payload], - parent_id="115590097100865541", - reason="Get Nyaa'd.", - ) - mock_overwrite_obj.serialize.assert_called_once() - channels.deserialize_channel.assert_called_once_with(mock_payload, app=rest_channel_logic_impl._app) - - @pytest.mark.asyncio - @_helpers.parametrize_valid_id_formats_for_models("channel", 379953393319542784, channels.PartialChannel) - async def test_update_channel_without_optionals( - self, rest_channel_logic_impl, channel, - ): - mock_payload = {"name": "Qts", "type": 2} - mock_channel_obj = mock.MagicMock(channels.PartialChannel) - rest_channel_logic_impl._session.modify_channel.return_value = mock_payload - with mock.patch.object(channels, "deserialize_channel", return_value=mock_channel_obj): - result = await rest_channel_logic_impl.update_channel(channel=channel,) - assert result is mock_channel_obj - rest_channel_logic_impl._session.modify_channel.assert_called_once_with( - channel_id="379953393319542784", - name=..., - position=..., - topic=..., - nsfw=..., - rate_limit_per_user=..., - bitrate=..., - user_limit=..., - permission_overwrites=..., - parent_id=..., - reason=..., - ) - channels.deserialize_channel.assert_called_once_with(mock_payload, app=rest_channel_logic_impl._app) - - @pytest.mark.asyncio - @_helpers.parametrize_valid_id_formats_for_models("channel", 55555, channels.PartialChannel) - async def test_delete_channel(self, rest_channel_logic_impl, channel): - rest_channel_logic_impl._session.delete_close_channel.return_value = ... - assert await rest_channel_logic_impl.delete_channel(channel) is None - rest_channel_logic_impl._session.delete_close_channel.assert_called_once_with(channel_id="55555") - - @pytest.mark.parametrize( - ("_direction", "expected_direction", "first", "expected_first"), - [ - [None, "before", None, bases.Snowflake.max()], - ["before", "before", "1234", "1234"], - ["before", "before", datetime.datetime(2007, 1, 6, 13), datetime.datetime(2007, 1, 6, 13)], - ["after", "after", 1235, 1235], - ["after", "after", datetime.datetime(2007, 11, 1, 15, 33, 33), datetime.datetime(2007, 11, 1, 15, 33, 33)], - ["around", "around", "1234", "1234"], - ["around", "around", datetime.datetime(2005, 12, 15), datetime.datetime(2005, 12, 15)], - ], - ) - def test_fetch_messages(self, rest_channel_logic_impl, direction, expected_direction, first, expected_first): - kwargs = {direction: first} if direction is not None else {} - mock_channel = mock.MagicMock(__int__=90213) - - with mock.patch.object(channel._MessagePaginator, "__init__", return_value=None) as init: - result = rest_channel_logic_impl.fetch_messages(mock_channel, **kwargs) - - assert isinstance(result, channel._MessagePaginator) - init.assert_called_once_with( - channel=mock_channel, - direction=expected_direction, - first=expected_first, - app=rest_channel_logic_impl._app, - session=rest_channel_logic_impl._session, - ) - - @_helpers.assert_raises(type_=TypeError) - @pytest.mark.parametrize( - "directions", - ( - {"after": 123, "before": 324}, - {"after": 312, "around": 444}, - {"before": 444, "around": 1010}, - {"around": 123, "before": 432, "after": 19929}, - ), - ) - def test_fetch_messages_raises_type_error_on_multiple_directions(self, rest_channel_logic_impl, directions): - rest_channel_logic_impl.fetch_messages(123123, **directions) - - @pytest.mark.asyncio - @_helpers.parametrize_valid_id_formats_for_models("channel", 55555, channels.PartialChannel) - @_helpers.parametrize_valid_id_formats_for_models("message", 565656, messages.Message) - async def test_fetch_message(self, rest_channel_logic_impl, channel, message): - mock_payload = {"id": "9409404", "content": "I AM A MESSAGE!"} - mock_message_obj = mock.MagicMock(messages.Message) - rest_channel_logic_impl._session.get_channel_message.return_value = mock_payload - with mock.patch.object(messages.Message, "deserialize", return_value=mock_message_obj): - assert await rest_channel_logic_impl.fetch_message(channel=channel, message=message) is mock_message_obj - rest_channel_logic_impl._session.get_channel_message.assert_called_once_with( - channel_id="55555", message_id="565656", - ) - messages.Message.deserialize.assert_called_once_with(mock_payload, app=rest_channel_logic_impl._app) - - @pytest.mark.asyncio - @_helpers.parametrize_valid_id_formats_for_models("channel", 694463529998352394, channels.PartialChannel) - async def test_create_message_with_optionals(self, rest_channel_logic_impl, channel): - mock_message_obj = mock.MagicMock(messages.Message) - mock_message_payload = {"id": "2929292992", "content": "222922"} - rest_channel_logic_impl._session.create_message.return_value = mock_message_payload - mock_allowed_mentions_payload = {"parse": ["everyone", "users", "roles"]} - mock_embed_payload = {"description": "424242"} - mock_file_obj = mock.MagicMock(files.BaseStream) - mock_file_obj2 = mock.MagicMock(files.BaseStream) - mock_embed_obj = mock.MagicMock(embeds.Embed) - mock_embed_obj.assets_to_upload = [mock_file_obj2] - mock_embed_obj.serialize = mock.MagicMock(return_value=mock_embed_payload) - stack = contextlib.ExitStack() - stack.enter_context( - mock.patch.object(helpers, "generate_allowed_mentions", return_value=mock_allowed_mentions_payload) - ) - stack.enter_context(mock.patch.object(messages.Message, "deserialize", return_value=mock_message_obj)) - with stack: - result = await rest_channel_logic_impl.create_message( - channel, - content="A CONTENT", - nonce="69696969696969", - tts=True, - files=[mock_file_obj], - embed=mock_embed_obj, - mentions_everyone=False, - user_mentions=False, - role_mentions=False, - ) - assert result is mock_message_obj - messages.Message.deserialize.assert_called_once_with(mock_message_payload, app=rest_channel_logic_impl._app) - helpers.generate_allowed_mentions.assert_called_once_with( - mentions_everyone=False, user_mentions=False, role_mentions=False - ) - rest_channel_logic_impl._session.create_message.assert_called_once_with( - channel_id="694463529998352394", - content="A CONTENT", - nonce="69696969696969", - tts=True, - files=[mock_file_obj, mock_file_obj2], - embed=mock_embed_payload, - allowed_mentions=mock_allowed_mentions_payload, - ) - mock_embed_obj.serialize.assert_called_once() - - @pytest.mark.asyncio - @_helpers.parametrize_valid_id_formats_for_models("channel", 694463529998352394, channels.PartialChannel) - async def test_create_message_without_optionals(self, rest_channel_logic_impl, channel): - mock_message_obj = mock.MagicMock(messages.Message) - mock_message_payload = {"id": "2929292992", "content": "222922"} - rest_channel_logic_impl._session.create_message.return_value = mock_message_payload - mock_allowed_mentions_payload = {"parse": ["everyone", "users", "roles"]} - stack = contextlib.ExitStack() - stack.enter_context( - mock.patch.object(helpers, "generate_allowed_mentions", return_value=mock_allowed_mentions_payload) - ) - stack.enter_context(mock.patch.object(messages.Message, "deserialize", return_value=mock_message_obj)) - with stack: - assert await rest_channel_logic_impl.create_message(channel) is mock_message_obj - messages.Message.deserialize.assert_called_once_with(mock_message_payload, app=rest_channel_logic_impl._app) - helpers.generate_allowed_mentions.assert_called_once_with( - mentions_everyone=True, user_mentions=True, role_mentions=True - ) - rest_channel_logic_impl._session.create_message.assert_called_once_with( - channel_id="694463529998352394", - content=..., - nonce=..., - tts=..., - files=..., - embed=..., - allowed_mentions=mock_allowed_mentions_payload, - ) - - @pytest.mark.asyncio - async def test_safe_create_message_without_optionals(self, rest_channel_logic_impl): - channel = mock.MagicMock(channels.PartialChannel) - mock_message_obj = mock.MagicMock(messages.Message) - rest_channel_logic_impl.create_message = mock.AsyncMock(return_value=mock_message_obj) - result = await rest_channel_logic_impl.safe_create_message(channel,) - assert result is mock_message_obj - rest_channel_logic_impl.create_message.assert_called_once_with( - channel=channel, - content=..., - nonce=..., - tts=..., - files=..., - embed=..., - mentions_everyone=False, - user_mentions=False, - role_mentions=False, - ) - - @pytest.mark.asyncio - async def test_safe_create_message_with_optionals(self, rest_channel_logic_impl): - channel = mock.MagicMock(channels.PartialChannel) - mock_embed_obj = mock.MagicMock(embeds.Embed) - mock_message_obj = mock.MagicMock(messages.Message) - mock_file_obj = mock.MagicMock(files.BaseStream) - mock_embed_obj = mock.MagicMock(embeds.Embed) - rest_channel_logic_impl.create_message = mock.AsyncMock(return_value=mock_message_obj) - result = await rest_channel_logic_impl.safe_create_message( - channel=channel, - content="A CONTENT", - nonce="69696969696969", - tts=True, - files=[mock_file_obj], - embed=mock_embed_obj, - mentions_everyone=True, - user_mentions=True, - role_mentions=True, - ) - assert result is mock_message_obj - rest_channel_logic_impl.create_message.assert_called_once_with( - channel=channel, - content="A CONTENT", - nonce="69696969696969", - tts=True, - files=[mock_file_obj], - embed=mock_embed_obj, - mentions_everyone=True, - user_mentions=True, - role_mentions=True, - ) - - @pytest.mark.asyncio - @_helpers.parametrize_valid_id_formats_for_models("message", 432, messages.Message) - @_helpers.parametrize_valid_id_formats_for_models("channel", 123, channels.PartialChannel) - async def test_update_message_with_optionals(self, rest_channel_logic_impl, message, channel): - mock_payload = {"id": "4242", "content": "I HAVE BEEN UPDATED!"} - mock_message_obj = mock.MagicMock(messages.Message) - mock_embed_payload = {"description": "blahblah"} - mock_embed = mock.MagicMock(embeds.Embed) - mock_embed.serialize = mock.MagicMock(return_value=mock_embed_payload) - mock_allowed_mentions_payload = {"parse": [], "users": ["123"]} - rest_channel_logic_impl._session.edit_message.return_value = mock_payload - stack = contextlib.ExitStack() - stack.enter_context( - mock.patch.object(helpers, "generate_allowed_mentions", return_value=mock_allowed_mentions_payload) - ) - stack.enter_context(mock.patch.object(messages.Message, "deserialize", return_value=mock_message_obj)) - with stack: - result = await rest_channel_logic_impl.update_message( - message=message, - channel=channel, - content="C O N T E N T", - embed=mock_embed, - flags=messages.MessageFlag.IS_CROSSPOST | messages.MessageFlag.SUPPRESS_EMBEDS, - mentions_everyone=False, - role_mentions=False, - user_mentions=[123123123], - ) - assert result is mock_message_obj - rest_channel_logic_impl._session.edit_message.assert_called_once_with( - channel_id="123", - message_id="432", - content="C O N T E N T", - embed=mock_embed_payload, - flags=6, - allowed_mentions=mock_allowed_mentions_payload, - ) - mock_embed.serialize.assert_called_once() - messages.Message.deserialize.assert_called_once_with(mock_payload, app=rest_channel_logic_impl._app) - helpers.generate_allowed_mentions.assert_called_once_with( - mentions_everyone=False, role_mentions=False, user_mentions=[123123123] - ) - - @pytest.mark.asyncio - @_helpers.parametrize_valid_id_formats_for_models("message", 432, messages.Message) - @_helpers.parametrize_valid_id_formats_for_models("channel", 123, channels.PartialChannel) - async def test_update_message_without_optionals(self, rest_channel_logic_impl, message, channel): - mock_payload = {"id": "4242", "content": "I HAVE BEEN UPDATED!"} - mock_message_obj = mock.MagicMock(messages.Message) - mock_allowed_mentions_payload = {"parse": ["everyone", "users", "roles"]} - rest_channel_logic_impl._session.edit_message.return_value = mock_payload - stack = contextlib.ExitStack() - stack.enter_context( - mock.patch.object(helpers, "generate_allowed_mentions", return_value=mock_allowed_mentions_payload) - ) - stack.enter_context(mock.patch.object(messages.Message, "deserialize", return_value=mock_message_obj)) - with stack: - assert await rest_channel_logic_impl.update_message(message=message, channel=channel) is mock_message_obj - rest_channel_logic_impl._session.edit_message.assert_called_once_with( - channel_id="123", - message_id="432", - content=..., - embed=..., - flags=..., - allowed_mentions=mock_allowed_mentions_payload, - ) - messages.Message.deserialize.assert_called_once_with(mock_payload, app=rest_channel_logic_impl._app) - helpers.generate_allowed_mentions.assert_called_once_with( - mentions_everyone=True, user_mentions=True, role_mentions=True - ) - - @pytest.mark.asyncio - async def test_safe_update_message_without_optionals(self, rest_channel_logic_impl): - message = mock.MagicMock(messages.Message) - channel = mock.MagicMock(channels.PartialChannel) - mock_message_obj = mock.MagicMock(messages.Message) - rest_channel_logic_impl.update_message = mock.AsyncMock(return_value=mock_message_obj) - result = await rest_channel_logic_impl.safe_update_message(message=message, channel=channel,) - assert result is mock_message_obj - rest_channel_logic_impl.update_message.assert_called_once_with( - message=message, - channel=channel, - content=..., - embed=..., - flags=..., - mentions_everyone=False, - role_mentions=False, - user_mentions=False, - ) - - @pytest.mark.asyncio - async def test_safe_update_message_with_optionals(self, rest_channel_logic_impl): - message = mock.MagicMock(messages.Message) - channel = mock.MagicMock(channels.PartialChannel) - mock_embed = mock.MagicMock(embeds.Embed) - mock_message_obj = mock.MagicMock(messages.Message) - rest_channel_logic_impl.update_message = mock.AsyncMock(return_value=mock_message_obj) - result = await rest_channel_logic_impl.safe_update_message( - message=message, - channel=channel, - content="C O N T E N T", - embed=mock_embed, - flags=messages.MessageFlag.IS_CROSSPOST | messages.MessageFlag.SUPPRESS_EMBEDS, - mentions_everyone=True, - role_mentions=True, - user_mentions=True, - ) - assert result is mock_message_obj - rest_channel_logic_impl.update_message.assert_called_once_with( - message=message, - channel=channel, - content="C O N T E N T", - embed=mock_embed, - flags=messages.MessageFlag.IS_CROSSPOST | messages.MessageFlag.SUPPRESS_EMBEDS, - mentions_everyone=True, - role_mentions=True, - user_mentions=True, - ) - - @pytest.mark.asyncio - @_helpers.parametrize_valid_id_formats_for_models("channel", 379953393319542784, channels.PartialChannel) - @_helpers.parametrize_valid_id_formats_for_models("message", 115590097100865541, messages.Message) - async def test_delete_messages_singular(self, rest_channel_logic_impl, channel, message): - rest_channel_logic_impl._session.delete_message.return_value = ... - assert await rest_channel_logic_impl.delete_messages(channel, message) is None - rest_channel_logic_impl._session.delete_message.assert_called_once_with( - channel_id="379953393319542784", message_id="115590097100865541", - ) - - @pytest.mark.asyncio - @_helpers.parametrize_valid_id_formats_for_models("channel", 379953393319542784, channels.PartialChannel) - @_helpers.parametrize_valid_id_formats_for_models("message", 115590097100865541, messages.Message) - @_helpers.parametrize_valid_id_formats_for_models("additional_message", 115590097100865541, messages.Message) - async def test_delete_messages_singular_after_duplicate_removal( - self, rest_channel_logic_impl, channel, message, additional_message - ): - rest_channel_logic_impl._session.delete_message.return_value = ... - assert await rest_channel_logic_impl.delete_messages(channel, message, additional_message) is None - rest_channel_logic_impl._session.delete_message.assert_called_once_with( - channel_id="379953393319542784", message_id="115590097100865541", - ) - - @pytest.mark.asyncio - @_helpers.parametrize_valid_id_formats_for_models("channel", 379953393319542784, channels.PartialChannel) - @_helpers.parametrize_valid_id_formats_for_models("message", 115590097100865541, messages.Message) - @_helpers.parametrize_valid_id_formats_for_models("additional_message", 572144340277919754, messages.Message) - async def test_delete_messages_bulk_removes_duplicates( - self, rest_channel_logic_impl, channel, message, additional_message - ): - rest_channel_logic_impl._session.bulk_delete_messages.return_value = ... - assert ( - await rest_channel_logic_impl.delete_messages(channel, message, additional_message, 115590097100865541) - is None - ) - rest_channel_logic_impl._session.bulk_delete_messages.assert_called_once_with( - channel_id="379953393319542784", messages=["115590097100865541", "572144340277919754"], - ) - rest_channel_logic_impl._session.delete_message.assert_not_called() - - @pytest.mark.asyncio - @_helpers.assert_raises(type_=ValueError) - async def test_delete_messages_raises_value_error_on_over_100_messages(self, rest_channel_logic_impl): - rest_channel_logic_impl._session.bulk_delete_messages.return_value = ... - assert await rest_channel_logic_impl.delete_messages(123123, *list(range(0, 111))) is None - - @pytest.mark.asyncio - @_helpers.parametrize_valid_id_formats_for_models("channel", 4123123, channels.PartialChannel) - @_helpers.parametrize_valid_id_formats_for_models("overwrite", 9999, channels.PermissionOverwrite) - async def test_update_channel_overwrite_with_optionals(self, rest_channel_logic_impl, channel, overwrite): - rest_channel_logic_impl._session.edit_channel_permissions.return_value = ... - result = await rest_channel_logic_impl.update_channel_overwrite( - channel=channel, - overwrite=overwrite, - target_type="member", - allow=messages.MessageFlag.IS_CROSSPOST | messages.MessageFlag.SUPPRESS_EMBEDS, - deny=21, - reason="get Nyaa'd", - ) - assert result is None - rest_channel_logic_impl._session.edit_channel_permissions.assert_called_once_with( - channel_id="4123123", overwrite_id="9999", type_="member", allow=6, deny=21, reason="get Nyaa'd", - ) - - @pytest.mark.asyncio - @_helpers.parametrize_valid_id_formats_for_models("channel", 4123123, channels.PartialChannel) - @_helpers.parametrize_valid_id_formats_for_models("overwrite", 9999, channels.PermissionOverwrite) - async def test_update_channel_overwrite_without_optionals(self, rest_channel_logic_impl, channel, overwrite): - rest_channel_logic_impl._session.edit_channel_permissions.return_value = ... - result = await rest_channel_logic_impl.update_channel_overwrite( - channel=channel, overwrite=overwrite, target_type="member" - ) - assert result is None - rest_channel_logic_impl._session.edit_channel_permissions.assert_called_once_with( - channel_id="4123123", overwrite_id="9999", type_="member", allow=..., deny=..., reason=..., - ) - - @pytest.mark.asyncio - @pytest.mark.parametrize( - "target", - [ - mock.MagicMock(guilds.Role, id=bases.Snowflake(9999), __int__=guilds.Role.__int__), - mock.MagicMock(users.User, id=bases.Snowflake(9999), __int__=users.User.__int__), - ], - ) - async def test_update_channel_overwrite_with_alternative_target_object(self, rest_channel_logic_impl, target): - rest_channel_logic_impl._session.edit_channel_permissions.return_value = ... - result = await rest_channel_logic_impl.update_channel_overwrite( - channel=4123123, overwrite=target, target_type="member" - ) - assert result is None - rest_channel_logic_impl._session.edit_channel_permissions.assert_called_once_with( - channel_id="4123123", overwrite_id="9999", type_="member", allow=..., deny=..., reason=..., - ) - - @pytest.mark.asyncio - @_helpers.parametrize_valid_id_formats_for_models("channel", 123123123, channels.PartialChannel) - async def test_fetch_invites_for_channel(self, rest_channel_logic_impl, channel): - mock_invite_payload = {"code": "ogogogogogogogo", "guild_id": "123123123"} - mock_invite_obj = mock.MagicMock(invites.InviteWithMetadata) - rest_channel_logic_impl._session.get_channel_invites.return_value = [mock_invite_payload] - with mock.patch.object(invites.InviteWithMetadata, "deserialize", return_value=mock_invite_obj): - assert await rest_channel_logic_impl.fetch_invites_for_channel(channel=channel) == [mock_invite_obj] - rest_channel_logic_impl._session.get_channel_invites.assert_called_once_with(channel_id="123123123") - invites.InviteWithMetadata.deserialize.assert_called_once_with( - mock_invite_payload, app=rest_channel_logic_impl._app - ) - - @pytest.mark.asyncio - @_helpers.parametrize_valid_id_formats_for_models("channel", 234123, channels.PartialChannel) - @_helpers.parametrize_valid_id_formats_for_models("user", 333333, users.User) - @pytest.mark.parametrize("max_age", [4444, datetime.timedelta(seconds=4444)]) - async def test_create_invite_for_channel_with_optionals(self, rest_channel_logic_impl, channel, user, max_age): - mock_invite_payload = {"code": "ogogogogogogogo", "guild_id": "123123123"} - mock_invite_obj = mock.MagicMock(invites.InviteWithMetadata) - rest_channel_logic_impl._session.create_channel_invite.return_value = mock_invite_payload - with mock.patch.object(invites.InviteWithMetadata, "deserialize", return_value=mock_invite_obj): - result = await rest_channel_logic_impl.create_invite_for_channel( - channel, - max_age=max_age, - max_uses=444, - temporary=True, - unique=False, - target_user=user, - target_user_type=invites.TargetUserType.STREAM, - reason="Hello there.", - ) - assert result is mock_invite_obj - rest_channel_logic_impl._session.create_channel_invite.assert_called_once_with( - channel_id="234123", - max_age=4444, - max_uses=444, - temporary=True, - unique=False, - target_user="333333", - target_user_type=1, - reason="Hello there.", - ) - invites.InviteWithMetadata.deserialize.assert_called_once_with( - mock_invite_payload, app=rest_channel_logic_impl._app - ) - - @pytest.mark.asyncio - @_helpers.parametrize_valid_id_formats_for_models("channel", 234123, channels.PartialChannel) - async def test_create_invite_for_channel_without_optionals(self, rest_channel_logic_impl, channel): - mock_invite_payload = {"code": "ogogogogogogogo", "guild_id": "123123123"} - mock_invite_obj = mock.MagicMock(invites.InviteWithMetadata) - rest_channel_logic_impl._session.create_channel_invite.return_value = mock_invite_payload - with mock.patch.object(invites.InviteWithMetadata, "deserialize", return_value=mock_invite_obj): - assert await rest_channel_logic_impl.create_invite_for_channel(channel) is mock_invite_obj - rest_channel_logic_impl._session.create_channel_invite.assert_called_once_with( - channel_id="234123", - max_age=..., - max_uses=..., - temporary=..., - unique=..., - target_user=..., - target_user_type=..., - reason=..., - ) - invites.InviteWithMetadata.deserialize.assert_called_once_with( - mock_invite_payload, app=rest_channel_logic_impl._app - ) - - @pytest.mark.asyncio - @_helpers.parametrize_valid_id_formats_for_models("channel", 379953393319542784, channels.PartialChannel) - @_helpers.parametrize_valid_id_formats_for_models("overwrite", 123123123, channels.PermissionOverwrite) - async def test_delete_channel_overwrite(self, rest_channel_logic_impl, channel, overwrite): - rest_channel_logic_impl._session.delete_channel_permission.return_value = ... - assert await rest_channel_logic_impl.delete_channel_overwrite(channel=channel, overwrite=overwrite) is None - rest_channel_logic_impl._session.delete_channel_permission.assert_called_once_with( - channel_id="379953393319542784", overwrite_id="123123123", - ) - - @pytest.mark.asyncio - @pytest.mark.parametrize( - "target", - [ - mock.MagicMock(guilds.Role, id=bases.Snowflake(123123123), __int__=guilds.Role.__int__), - mock.MagicMock(users.User, id=bases.Snowflake(123123123), __int__=users.User.__int__), - ], - ) - async def test_delete_channel_overwrite_with_alternative_target_objects(self, rest_channel_logic_impl, target): - rest_channel_logic_impl._session.delete_channel_permission.return_value = ... - assert ( - await rest_channel_logic_impl.delete_channel_overwrite(channel=379953393319542784, overwrite=target) is None - ) - rest_channel_logic_impl._session.delete_channel_permission.assert_called_once_with( - channel_id="379953393319542784", overwrite_id="123123123", - ) - - @pytest.mark.asyncio - @_helpers.parametrize_valid_id_formats_for_models("channel", 379953393319542784, channels.PermissionOverwrite) - async def test_trigger_typing(self, rest_channel_logic_impl, channel): - rest_channel_logic_impl._session.trigger_typing_indicator.return_value = ... - assert await rest_channel_logic_impl.trigger_typing(channel) is None - rest_channel_logic_impl._session.trigger_typing_indicator.assert_called_once_with( - channel_id="379953393319542784" - ) - - @pytest.mark.asyncio - @_helpers.parametrize_valid_id_formats_for_models("channel", 123123123, channels.PartialChannel) - async def test_fetch_pins(self, rest_channel_logic_impl, channel): - mock_message_payload = {"id": "21232", "content": "CONTENT"} - mock_message_obj = mock.MagicMock(messages.Message, id=21232) - rest_channel_logic_impl._session.get_pinned_messages.return_value = [mock_message_payload] - with mock.patch.object(messages.Message, "deserialize", return_value=mock_message_obj): - assert await rest_channel_logic_impl.fetch_pins(channel) == {21232: mock_message_obj} - rest_channel_logic_impl._session.get_pinned_messages.assert_called_once_with(channel_id="123123123") - messages.Message.deserialize.assert_called_once_with(mock_message_payload, app=rest_channel_logic_impl._app) - - @pytest.mark.asyncio - @_helpers.parametrize_valid_id_formats_for_models("channel", 292929, channels.PartialChannel) - @_helpers.parametrize_valid_id_formats_for_models("message", 123123, messages.Message) - async def test_pin_message(self, rest_channel_logic_impl, channel, message): - rest_channel_logic_impl._session.add_pinned_channel_message.return_value = ... - assert await rest_channel_logic_impl.pin_message(channel, message) is None - rest_channel_logic_impl._session.add_pinned_channel_message.assert_called_once_with( - channel_id="292929", message_id="123123" - ) - - @pytest.mark.asyncio - @_helpers.parametrize_valid_id_formats_for_models("channel", 292929, channels.PartialChannel) - @_helpers.parametrize_valid_id_formats_for_models("message", 123123, messages.Message) - async def test_unpin_message(self, rest_channel_logic_impl, channel, message): - rest_channel_logic_impl._session.delete_pinned_channel_message.return_value = ... - assert await rest_channel_logic_impl.unpin_message(channel, message) is None - rest_channel_logic_impl._session.delete_pinned_channel_message.assert_called_once_with( - channel_id="292929", message_id="123123" - ) - - @pytest.mark.asyncio - @_helpers.parametrize_valid_id_formats_for_models("channel", 115590097100865541, channels.PartialChannel) - async def test_create_webhook_with_optionals(self, rest_channel_logic_impl, channel): - mock_webhook_payload = {"id": "29292929", "channel": "2292992"} - mock_webhook_obj = mock.MagicMock(webhooks.Webhook) - rest_channel_logic_impl._session.create_webhook.return_value = mock_webhook_payload - mock_image_data = mock.MagicMock(bytes) - mock_image_obj = mock.MagicMock(files.BaseStream) - mock_image_obj.read = mock.AsyncMock(return_value=mock_image_data) - stack = contextlib.ExitStack() - stack.enter_context(mock.patch.object(webhooks.Webhook, "deserialize", return_value=mock_webhook_obj)) - with stack: - result = await rest_channel_logic_impl.create_webhook( - channel=channel, name="aWebhook", avatar=mock_image_obj, reason="And a webhook is born." - ) - assert result is mock_webhook_obj - mock_image_obj.read.assert_awaited_once() - webhooks.Webhook.deserialize.assert_called_once_with(mock_webhook_payload, app=rest_channel_logic_impl._app) - rest_channel_logic_impl._session.create_webhook.assert_called_once_with( - channel_id="115590097100865541", name="aWebhook", avatar=mock_image_data, reason="And a webhook is born." - ) - - @pytest.mark.asyncio - @_helpers.parametrize_valid_id_formats_for_models("channel", 115590097100865541, channels.PartialChannel) - async def test_create_webhook_without_optionals(self, rest_channel_logic_impl, channel): - mock_webhook_payload = {"id": "29292929", "channel": "2292992"} - mock_webhook_obj = mock.MagicMock(webhooks.Webhook) - rest_channel_logic_impl._session.create_webhook.return_value = mock_webhook_payload - with mock.patch.object(webhooks.Webhook, "deserialize", return_value=mock_webhook_obj): - assert await rest_channel_logic_impl.create_webhook(channel=channel, name="aWebhook") is mock_webhook_obj - webhooks.Webhook.deserialize.assert_called_once_with(mock_webhook_payload, app=rest_channel_logic_impl._app) - rest_channel_logic_impl._session.create_webhook.assert_called_once_with( - channel_id="115590097100865541", name="aWebhook", avatar=..., reason=... - ) - - @pytest.mark.asyncio - @_helpers.parametrize_valid_id_formats_for_models("channel", 115590097100865541, channels.GuildChannel) - async def test_fetch_channel_webhooks(self, rest_channel_logic_impl, channel): - mock_webhook_payload = {"id": "29292929", "channel": "2292992"} - mock_webhook_obj = mock.MagicMock(webhooks.Webhook) - rest_channel_logic_impl._session.get_channel_webhooks.return_value = [mock_webhook_payload] - with mock.patch.object(webhooks.Webhook, "deserialize", return_value=mock_webhook_obj): - assert await rest_channel_logic_impl.fetch_channel_webhooks(channel) == [mock_webhook_obj] - rest_channel_logic_impl._session.get_channel_webhooks.assert_called_once_with( - channel_id="115590097100865541" - ) - webhooks.Webhook.deserialize.assert_called_once_with(mock_webhook_payload, app=rest_channel_logic_impl._app) diff --git a/tests/hikari/rest/test_client.py b/tests/hikari/rest/test_client.py deleted file mode 100644 index 14536e8da3..0000000000 --- a/tests/hikari/rest/test_client.py +++ /dev/null @@ -1,98 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# Copyright © Nekoka.tt 2019-2020 -# -# This file is part of Hikari. -# -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . -import inspect - -import mock -import pytest - -from hikari import http_settings -from hikari.net import rest as high_level_rest -from hikari import application -from hikari.net.rest import channel -from hikari.net.rest import gateway -from hikari.net.rest import guild -from hikari.net.rest import invite -from hikari.net.rest import me -from hikari.net.rest import oauth2 -from hikari.net.rest import react -from hikari.net.rest import session as low_level_rest -from hikari.net.rest import user -from hikari.net.rest import voice -from hikari.net.rest import webhook - - -class TestRESTClient: - @pytest.mark.parametrize( - ["token", "token_type", "expected_token"], - [ - ("foobar.baz.bork", None, None), - ("foobar.baz.bork", "Bot", "Bot foobar.baz.bork"), - ("foobar.baz.bork", "Bearer", "Bearer foobar.baz.bork"), - ], - ) - def test_init(self, token, token_type, expected_token): - mock_config = http_settings.RESTConfig(token=token, token_type=token_type, trust_env=True) - mock_app = mock.MagicMock(application.Application, config=mock_config) - mock_low_level_rest_clients = mock.MagicMock(low_level_rest.RESTSession) - with mock.patch.object(low_level_rest, "RESTSession", return_value=mock_low_level_rest_clients) as patched_init: - client = high_level_rest.RESTClient(mock_app) - patched_init.assert_called_once_with( - allow_redirects=mock_config._allow_redirects, - base_url=mock_config.rest_url, - connector=mock_config.tcp_connector, - debug=False, - proxy_headers=mock_config._proxy_headers, - proxy_auth=mock_config._proxy_auth, - ssl_context=mock_config._ssl_context, - verify_ssl=mock_config._verify_ssl, - timeout=mock_config.request_timeout, - token=expected_token, - trust_env=True, - version=mock_config.rest_version, - ) - assert client._session is mock_low_level_rest_clients - assert client._app is mock_app - - def test_inheritance(self): - for attr, routine in ( - member - for component in [ - channel.RESTChannelComponent, - me.RESTCurrentUserComponent, - gateway.RESTGatewayComponent, - guild.RESTGuildComponent, - invite.RESTInviteComponent, - oauth2.RESTOAuth2Component, - react.RESTReactionComponent, - user.RESTUserComponent, - voice.RESTVoiceComponent, - webhook.RESTWebhookComponent, - ] - for member in inspect.getmembers(component, inspect.isroutine) - ): - if not attr.startswith("__"): - assert hasattr(high_level_rest.RESTClient, attr), ( - f"Missing {routine.__qualname__} on RestClient; the component might not be being " - "inherited properly or at all." - ) - assert getattr(high_level_rest.RESTClient, attr) == routine, ( - f"Mismatching method found on RestClient; expected {routine.__qualname__} but got " - f"{getattr(high_level_rest.RESTClient, attr).__qualname__}. `{attr}` is most likely being declared on" - "multiple application." - ) diff --git a/tests/hikari/rest/test_gateway.py b/tests/hikari/rest/test_gateway.py deleted file mode 100644 index 6c7bc37971..0000000000 --- a/tests/hikari/rest/test_gateway.py +++ /dev/null @@ -1,58 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# Copyright © Nekoka.tt 2019-2020 -# -# This file is part of Hikari. -# -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . - -import mock -import pytest - -from hikari import application -from hikari.models import gateway as gateway_models -from hikari.net.rest import gateway -from hikari.net.rest import session - - -class TestRESTReactionLogic: - @pytest.fixture() - def rest_gateway_logic_impl(self): - mock_app = mock.MagicMock(application.Application) - mock_low_level_restful_client = mock.MagicMock(session.RESTSession) - - class RESTGatewayLogicImpl(gateway.RESTGatewayComponent): - def __init__(self): - super().__init__(mock_app, mock_low_level_restful_client) - - return RESTGatewayLogicImpl() - - @pytest.mark.asyncio - async def test_fetch_gateway_url(self, rest_gateway_logic_impl): - mock_url = "wss://gateway.discord.gg/" - rest_gateway_logic_impl._session.get_gateway.return_value = mock_url - assert await rest_gateway_logic_impl.fetch_gateway_url() == mock_url - rest_gateway_logic_impl._session.get_gateway.assert_called_once() - - @pytest.mark.asyncio - async def test_fetch_gateway_bot(self, rest_gateway_logic_impl): - mock_payload = {"url": "wss://gateway.discord.gg/", "shards": 9, "session_start_limit": {}} - mock_gateway_bot_obj = mock.MagicMock(gateway_models.GatewayBot) - rest_gateway_logic_impl._session.get_gateway_bot.return_value = mock_payload - with mock.patch.object(gateway_models.GatewayBot, "deserialize", return_value=mock_gateway_bot_obj): - assert await rest_gateway_logic_impl.fetch_gateway_bot() is mock_gateway_bot_obj - rest_gateway_logic_impl._session.get_gateway_bot.assert_called_once() - gateway_models.GatewayBot.deserialize.assert_called_once_with( - mock_payload, app=rest_gateway_logic_impl._app - ) diff --git a/tests/hikari/rest/test_guild.py b/tests/hikari/rest/test_guild.py deleted file mode 100644 index 5743e626ec..0000000000 --- a/tests/hikari/rest/test_guild.py +++ /dev/null @@ -1,1287 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# Copyright © Nekoka.tt 2019-2020 -# -# This file is part of Hikari. -# -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . -import contextlib -import datetime -import inspect - -import mock -import pytest - -from hikari import application -from hikari.models import audit_logs -from hikari.models import bases -from hikari.models import channels -from hikari.models import colors -from hikari.models import emojis -from hikari.models import files -from hikari.models import guilds -from hikari.models import invites -from hikari.models import permissions -from hikari.models import users -from hikari.models import voices -from hikari.models import webhooks -from hikari.net.rest import guild as _guild -from hikari.net.rest import session -from tests.hikari import _helpers - - -class TestMemberPaginator: - @pytest.fixture() - def mock_session(self): - return mock.MagicMock(spec_set=session.RESTSession) - - @pytest.fixture() - def mock_app(self): - return mock.MagicMock(spec_set=application.Application) - - @pytest.fixture() - def member_cls(self): - with mock.patch.object(guilds, "GuildMember") as member_cls: - yield member_cls - - def test_init_no_start_bounds(self, mock_session, mock_app): - guild = mock.MagicMock(__int__=lambda _: 22) - pag = _guild._MemberPaginator(mock_app, guild, None, mock_session) - assert pag._first_id == "0" - assert pag._guild_id == "22" - assert pag._app is mock_app - assert pag._session is mock_session - - @pytest.mark.parametrize( - ["start_at", "expected"], - [ - (None, "0"), - (53, "53"), - (bases.Unique(id=bases.Snowflake(22)), "22"), - (bases.Snowflake(22), "22"), - (datetime.datetime(2019, 1, 22, 18, 41, 15, 283000, tzinfo=datetime.timezone.utc), "537340989807788032"), - ], - ) - def test_init_with_start_bounds(self, mock_session, mock_app, start_at, expected): - guild = mock.MagicMock(__int__=lambda _: 25) - pag = _guild._MemberPaginator(mock_app, guild, start_at, mock_session) - assert pag._first_id == expected - assert pag._guild_id == "25" - assert pag._app is mock_app - assert pag._session is mock_session - - @pytest.mark.asyncio - async def test_next_chunk_performs_correct_api_call(self, mock_session, mock_app, member_cls): - guild = mock.MagicMock(__int__=lambda _: 34) - pag = _guild._MemberPaginator(mock_app, guild, None, mock_session) - pag._first_id = "123456" - - await pag._next_chunk() - - mock_session.list_guild_members.assert_awaited_once_with("34", after="123456") - - @pytest.mark.asyncio - async def test_next_chunk_when_empty_returns_None(self, mock_session, mock_app, member_cls): - mock_session.list_guild_members = mock.AsyncMock(return_value=[]) - guild = mock.MagicMock(__int__=lambda _: 36) - pag = _guild._MemberPaginator(mock_app, guild, None, mock_session) - - assert await pag._next_chunk() is None - - @pytest.mark.asyncio - async def test_next_chunk_updates_first_id_to_last_item(self, mock_session, mock_app, member_cls): - return_payload = [ - {"id": "1234", ...: ...}, - {"id": "3456", ...: ...}, - {"id": "3333", ...: ...}, - {"id": "512", ...: ...}, - ] - - mock_session.list_guild_members = mock.AsyncMock(return_value=return_payload) - - guild = mock.MagicMock(__int__=lambda _: 42) - pag = _guild._MemberPaginator(mock_app, guild, None, mock_session) - - await pag._next_chunk() - - assert pag._first_id == "512" - - @pytest.mark.asyncio - async def test_next_chunk_deserializes_payload_in_generator_lazily(self, mock_session, mock_app, member_cls): - guild = mock.MagicMock(__int__=lambda _: 69) - pag = _guild._MemberPaginator(mock_app, guild, None, mock_session) - - return_payload = [ - {"id": "1234", ...: ...}, - {"id": "3456", ...: ...}, - {"id": "3333", ...: ...}, - {"id": "512", ...: ...}, - ] - - real_values = [ - mock.MagicMock(), - mock.MagicMock(), - mock.MagicMock(), - mock.MagicMock(), - ] - - assert len(real_values) == len(return_payload) - - member_cls.deserialize = mock.MagicMock(side_effect=real_values) - - mock_session.list_guild_members = mock.AsyncMock(return_value=return_payload) - generator = await pag._next_chunk() - - assert inspect.isgenerator(generator), "expected genexp result" - - # No calls, this should be lazy to be more performant for non-100-divisable limit counts. - member_cls.deserialize.assert_not_called() - - for i, input_payload in enumerate(return_payload): - expected_value = real_values[i] - assert next(generator) is expected_value - member_cls.deserialize.assert_called_with(input_payload, app=mock_app) - - # Clear the generator result. - # This doesn't test anything, but there is an issue with coverage not detecting generator - # exit conditions properly. This fixes something that would otherwise be marked as - # uncovered behaviour erroneously. - # https://stackoverflow.com/questions/35317757/python-unittest-branch-coverage-seems-to-miss-executed-generator-in-zip - with pytest.raises(StopIteration): - next(generator) - - assert locals()["i"] == len(return_payload) - 1, "Not iterated correctly somehow" - - -class TestRESTGuildLogic: - @pytest.fixture() - def rest_guild_logic_impl(self): - mock_app = mock.MagicMock(application.Application) - mock_low_level_restful_client = mock.MagicMock(session.RESTSession) - - class RESTGuildLogicImpl(_guild.RESTGuildComponent): - def __init__(self): - super().__init__(mock_app, mock_low_level_restful_client) - - return RESTGuildLogicImpl() - - @pytest.mark.asyncio - @_helpers.parametrize_valid_id_formats_for_models("guild", 379953393319542784, guilds.Guild) - @_helpers.parametrize_valid_id_formats_for_models("user", 22222222, users.User) - @_helpers.parametrize_valid_id_formats_for_models("before", 123123123123, audit_logs.AuditLogEntry) - async def test_fetch_audit_log_entries_before_with_optionals_integration( - self, rest_guild_logic_impl, guild, before, user - ): - mock_entry_payload = {"id": "123123"} - mock_entry = mock.MagicMock(audit_logs.AuditLogEntry) - rest_guild_logic_impl._session.get_guild_audit_log.return_value = { - "webhooks": [], - "audit_log_entries": [mock_entry_payload], - "users": [], - "integrations": [], - } - iterator = rest_guild_logic_impl.fetch_audit_log_entries_before( - guild, user=user, action_type=audit_logs.AuditLogEventType.MEMBER_MOVE, limit=42 - ) - with mock.patch.object(audit_logs.AuditLogEntry, "deserialize", return_value=mock_entry): - async for entry in iterator: - assert entry is mock_entry - break - audit_logs.AuditLogEntry.deserialize.assert_called_once_with( - mock_entry_payload, app=rest_guild_logic_impl._app - ) - rest_guild_logic_impl._session.get_guild_audit_log.assert_called_once_with( - guild_id="379953393319542784", user_id="22222222", action_type=26, before="9223372036854775807", limit=42 - ) - - @pytest.mark.asyncio - @_helpers.parametrize_valid_id_formats_for_models("guild", 379953393319542784, guilds.Guild) - async def test_fetch_audit_log_entries_before_without_optionals_integration(self, rest_guild_logic_impl, guild): - mock_entry_payload = {"id": "123123"} - mock_entry = mock.MagicMock(audit_logs.AuditLogEntry) - rest_guild_logic_impl._session.get_guild_audit_log.return_value = { - "webhooks": [], - "audit_log_entries": [mock_entry_payload], - "users": [], - "integrations": [], - } - iterator = rest_guild_logic_impl.fetch_audit_log_entries_before(guild) - with mock.patch.object(audit_logs.AuditLogEntry, "deserialize", return_value=mock_entry): - async for entry in iterator: - assert entry is mock_entry - break - audit_logs.AuditLogEntry.deserialize.assert_called_once_with( - mock_entry_payload, app=rest_guild_logic_impl._app - ) - rest_guild_logic_impl._session.get_guild_audit_log.assert_called_once_with( - guild_id="379953393319542784", user_id=..., action_type=..., before="9223372036854775807", limit=100 - ) - - @pytest.mark.asyncio - async def test_fetch_audit_log_entries_before_with_datetime_object_integration(self, rest_guild_logic_impl): - date = datetime.datetime(2019, 1, 22, 18, 41, 15, 283_000, tzinfo=datetime.timezone.utc) - mock_entry_payload = {"id": "123123"} - mock_entry = mock.MagicMock(audit_logs.AuditLogEntry) - rest_guild_logic_impl._session.get_guild_audit_log.return_value = { - "webhooks": [], - "audit_log_entries": [mock_entry_payload], - "users": [], - "integrations": [], - } - iterator = rest_guild_logic_impl.fetch_audit_log_entries_before(123123123, before=date) - with mock.patch.object(audit_logs.AuditLogEntry, "deserialize", return_value=mock_entry): - async for entry in iterator: - assert entry is mock_entry - break - audit_logs.AuditLogEntry.deserialize.assert_called_once_with( - mock_entry_payload, app=rest_guild_logic_impl._app - ) - rest_guild_logic_impl._session.get_guild_audit_log.assert_called_once_with( - guild_id="123123123", user_id=..., action_type=..., before="537340989807788032", limit=100 - ) - - @pytest.mark.asyncio - @_helpers.parametrize_valid_id_formats_for_models("guild", 379953393319542784, guilds.Guild) - @_helpers.parametrize_valid_id_formats_for_models("user", 115590097100865541, users.User) - @_helpers.parametrize_valid_id_formats_for_models("before", 1231231123, audit_logs.AuditLogEntry) - async def test_fetch_audit_log_with_optionals(self, rest_guild_logic_impl, guild, user, before): - mock_audit_log_payload = {"entries": [], "integrations": [], "webhooks": [], "users": []} - mock_audit_log_obj = mock.MagicMock(audit_logs.AuditLog) - rest_guild_logic_impl._session.get_guild_audit_log.return_value = mock_audit_log_payload - with mock.patch.object(audit_logs.AuditLog, "deserialize", return_value=mock_audit_log_obj): - result = await rest_guild_logic_impl.fetch_audit_log( - guild, user=user, action_type=audit_logs.AuditLogEventType.MEMBER_MOVE, limit=100, before=before, - ) - assert result is mock_audit_log_obj - rest_guild_logic_impl._session.get_guild_audit_log.assert_called_once_with( - guild_id="379953393319542784", - user_id="115590097100865541", - action_type=26, - limit=100, - before="1231231123", - ) - audit_logs.AuditLog.deserialize.assert_called_once_with( - mock_audit_log_payload, app=rest_guild_logic_impl._app - ) - - @pytest.mark.asyncio - @_helpers.parametrize_valid_id_formats_for_models("guild", 379953393319542784, guilds.Guild) - async def test_fetch_audit_log_without_optionals(self, rest_guild_logic_impl, guild): - mock_audit_log_payload = {"entries": [], "integrations": [], "webhooks": [], "users": []} - mock_audit_log_obj = mock.MagicMock(audit_logs.AuditLog) - rest_guild_logic_impl._session.get_guild_audit_log.return_value = mock_audit_log_payload - with mock.patch.object(audit_logs.AuditLog, "deserialize", return_value=mock_audit_log_obj): - assert await rest_guild_logic_impl.fetch_audit_log(guild) is mock_audit_log_obj - rest_guild_logic_impl._session.get_guild_audit_log.assert_called_once_with( - guild_id="379953393319542784", user_id=..., action_type=..., limit=..., before=... - ) - audit_logs.AuditLog.deserialize.assert_called_once_with( - mock_audit_log_payload, app=rest_guild_logic_impl._app - ) - - @pytest.mark.asyncio - @_helpers.parametrize_valid_id_formats_for_models("guild", 379953393319542784, guilds.Guild) - async def test_fetch_audit_log_handles_datetime_object(self, rest_guild_logic_impl, guild): - mock_audit_log_payload = {"entries": [], "integrations": [], "webhooks": [], "users": []} - mock_audit_log_obj = mock.MagicMock(audit_logs.AuditLog) - rest_guild_logic_impl._session.get_guild_audit_log.return_value = mock_audit_log_payload - date = datetime.datetime(2019, 1, 22, 18, 41, 15, 283_000, tzinfo=datetime.timezone.utc) - with mock.patch.object(audit_logs.AuditLog, "deserialize", return_value=mock_audit_log_obj): - assert await rest_guild_logic_impl.fetch_audit_log(guild, before=date) is mock_audit_log_obj - rest_guild_logic_impl._session.get_guild_audit_log.assert_called_once_with( - guild_id="379953393319542784", user_id=..., action_type=..., limit=..., before="537340989807788032" - ) - audit_logs.AuditLog.deserialize.assert_called_once_with( - mock_audit_log_payload, app=rest_guild_logic_impl._app - ) - - @pytest.mark.asyncio - @_helpers.parametrize_valid_id_formats_for_models("guild", 93443949, guilds.Guild) - @_helpers.parametrize_valid_id_formats_for_models("emoji", 40404040404, emojis.KnownCustomEmoji) - async def test_fetch_guild_emoji(self, rest_guild_logic_impl, guild, emoji): - mock_emoji_payload = {"id": "92929", "name": "nyaa", "animated": True} - mock_emoji_obj = mock.MagicMock(emojis.KnownCustomEmoji) - rest_guild_logic_impl._session.get_guild_emoji.return_value = mock_emoji_payload - with mock.patch.object(emojis.KnownCustomEmoji, "deserialize", return_value=mock_emoji_obj): - assert await rest_guild_logic_impl.fetch_guild_emoji(guild=guild, emoji=emoji) is mock_emoji_obj - rest_guild_logic_impl._session.get_guild_emoji.assert_called_once_with( - guild_id="93443949", emoji_id="40404040404", - ) - emojis.KnownCustomEmoji.deserialize.assert_called_once_with( - mock_emoji_payload, app=rest_guild_logic_impl._app - ) - - @pytest.mark.asyncio - @_helpers.parametrize_valid_id_formats_for_models("guild", 93443949, guilds.Guild) - async def test_fetch_guild_emojis(self, rest_guild_logic_impl, guild): - mock_emoji_payload = {"id": "92929", "name": "nyaa", "animated": True} - mock_emoji_obj = mock.MagicMock(emojis.KnownCustomEmoji) - rest_guild_logic_impl._session.list_guild_emojis.return_value = [mock_emoji_payload] - with mock.patch.object(emojis.KnownCustomEmoji, "deserialize", return_value=mock_emoji_obj): - assert await rest_guild_logic_impl.fetch_guild_emojis(guild=guild) == [mock_emoji_obj] - rest_guild_logic_impl._session.list_guild_emojis.assert_called_once_with(guild_id="93443949",) - emojis.KnownCustomEmoji.deserialize.assert_called_once_with( - mock_emoji_payload, app=rest_guild_logic_impl._app - ) - - @pytest.mark.asyncio - @_helpers.parametrize_valid_id_formats_for_models("guild", 93443949, guilds.Guild) - @_helpers.parametrize_valid_id_formats_for_models("role", 537340989808050216, guilds.Role) - async def test_create_guild_emoji_with_optionals(self, rest_guild_logic_impl, guild, role): - mock_emoji_payload = {"id": "229292929", "animated": True} - mock_emoji_obj = mock.MagicMock(emojis.KnownCustomEmoji) - rest_guild_logic_impl._session.create_guild_emoji.return_value = mock_emoji_payload - mock_image_data = mock.MagicMock(bytes) - mock_image_obj = mock.MagicMock(files.BaseStream) - mock_image_obj.read = mock.AsyncMock(return_value=mock_image_data) - stack = contextlib.ExitStack() - stack.enter_context(mock.patch.object(emojis.KnownCustomEmoji, "deserialize", return_value=mock_emoji_obj)) - with stack: - result = await rest_guild_logic_impl.create_guild_emoji( - guild=guild, name="fairEmoji", image=mock_image_obj, roles=[role], reason="hello", - ) - assert result is mock_emoji_obj - emojis.KnownCustomEmoji.deserialize.assert_called_once_with( - mock_emoji_payload, app=rest_guild_logic_impl._app - ) - mock_image_obj.read.assert_awaited_once() - rest_guild_logic_impl._session.create_guild_emoji.assert_called_once_with( - guild_id="93443949", name="fairEmoji", image=mock_image_data, roles=["537340989808050216"], reason="hello", - ) - - @pytest.mark.asyncio - @_helpers.parametrize_valid_id_formats_for_models("guild", 93443949, guilds.Guild) - async def test_create_guild_emoji_without_optionals(self, rest_guild_logic_impl, guild): - mock_emoji_payload = {"id": "229292929", "animated": True} - mock_emoji_obj = mock.MagicMock(emojis.KnownCustomEmoji) - rest_guild_logic_impl._session.create_guild_emoji.return_value = mock_emoji_payload - mock_image_obj = mock.MagicMock(files.BaseStream) - mock_image_data = mock.MagicMock(bytes) - mock_image_obj = mock.MagicMock(files.BaseStream) - mock_image_obj.read = mock.AsyncMock(return_value=mock_image_data) - stack = contextlib.ExitStack() - stack.enter_context(mock.patch.object(emojis.KnownCustomEmoji, "deserialize", return_value=mock_emoji_obj)) - with stack: - result = await rest_guild_logic_impl.create_guild_emoji( - guild=guild, name="fairEmoji", image=mock_image_obj, - ) - assert result is mock_emoji_obj - emojis.KnownCustomEmoji.deserialize.assert_called_once_with( - mock_emoji_payload, app=rest_guild_logic_impl._app - ) - mock_image_obj.read.assert_awaited_once() - rest_guild_logic_impl._session.create_guild_emoji.assert_called_once_with( - guild_id="93443949", name="fairEmoji", image=mock_image_data, roles=..., reason=..., - ) - - @pytest.mark.asyncio - @_helpers.parametrize_valid_id_formats_for_models("guild", 93443949, guilds.Guild) - @_helpers.parametrize_valid_id_formats_for_models("emoji", 4123321, emojis.KnownCustomEmoji) - async def test_update_guild_emoji_without_optionals(self, rest_guild_logic_impl, guild, emoji): - mock_emoji_payload = {"id": "202020", "name": "Nyaa", "animated": True} - mock_emoji_obj = mock.MagicMock(emojis.KnownCustomEmoji) - rest_guild_logic_impl._session.modify_guild_emoji.return_value = mock_emoji_payload - with mock.patch.object(emojis.KnownCustomEmoji, "deserialize", return_value=mock_emoji_obj): - assert await rest_guild_logic_impl.update_guild_emoji(guild, emoji) is mock_emoji_obj - rest_guild_logic_impl._session.modify_guild_emoji.assert_called_once_with( - guild_id="93443949", emoji_id="4123321", name=..., roles=..., reason=..., - ) - emojis.KnownCustomEmoji.deserialize.assert_called_once_with( - mock_emoji_payload, app=rest_guild_logic_impl._app - ) - - @pytest.mark.asyncio - @_helpers.parametrize_valid_id_formats_for_models("guild", 93443949, guilds.Guild) - @_helpers.parametrize_valid_id_formats_for_models("emoji", 4123321, emojis.KnownCustomEmoji) - @_helpers.parametrize_valid_id_formats_for_models("role", 123123123, guilds.Role) - async def test_update_guild_emoji_with_optionals(self, rest_guild_logic_impl, guild, emoji, role): - mock_emoji_payload = {"id": "202020", "name": "Nyaa", "animated": True} - mock_emoji_obj = mock.MagicMock(emojis.KnownCustomEmoji) - rest_guild_logic_impl._session.modify_guild_emoji.return_value = mock_emoji_payload - with mock.patch.object(emojis.KnownCustomEmoji, "deserialize", return_value=mock_emoji_obj): - result = await rest_guild_logic_impl.update_guild_emoji( - guild, emoji, name="Nyaa", roles=[role], reason="Agent 42" - ) - assert result is mock_emoji_obj - rest_guild_logic_impl._session.modify_guild_emoji.assert_called_once_with( - guild_id="93443949", emoji_id="4123321", name="Nyaa", roles=["123123123"], reason="Agent 42", - ) - emojis.KnownCustomEmoji.deserialize.assert_called_once_with( - mock_emoji_payload, app=rest_guild_logic_impl._app - ) - - @pytest.mark.asyncio - @_helpers.parametrize_valid_id_formats_for_models("guild", 93443949, guilds.Guild) - @_helpers.parametrize_valid_id_formats_for_models("emoji", 4123321, emojis.KnownCustomEmoji) - async def test_delete_guild_emoji(self, rest_guild_logic_impl, guild, emoji): - rest_guild_logic_impl._session.delete_guild_emoji.return_value = ... - assert await rest_guild_logic_impl.delete_guild_emoji(guild, emoji) is None - rest_guild_logic_impl._session.delete_guild_emoji.assert_called_once_with( - guild_id="93443949", emoji_id="4123321" - ) - - @pytest.mark.asyncio - @pytest.mark.parametrize("region", [mock.MagicMock(voices.VoiceRegion, id="LONDON"), "LONDON"]) - async def test_create_guild_with_optionals(self, rest_guild_logic_impl, region): - mock_guild_payload = {"id": "299292929292992", "region": "LONDON"} - mock_guild_obj = mock.MagicMock(guilds.Guild) - rest_guild_logic_impl._session.create_guild.return_value = mock_guild_payload - mock_image_data = mock.MagicMock(bytes) - mock_image_obj = mock.MagicMock(files.BaseStream) - mock_image_obj.read = mock.AsyncMock(return_value=mock_image_data) - mock_role_payload = {"permissions": 123123} - mock_role_obj = mock.MagicMock(guilds.Role) - mock_role_obj.serialize = mock.MagicMock(return_value=mock_role_payload) - mock_channel_payload = {"type": 2, "name": "aChannel"} - mock_channel_obj = mock.MagicMock(channels.GuildChannelBuilder) - mock_channel_obj.serialize = mock.MagicMock(return_value=mock_channel_payload) - stack = contextlib.ExitStack() - stack.enter_context(mock.patch.object(guilds.Guild, "deserialize", return_value=mock_guild_obj)) - with stack: - result = await rest_guild_logic_impl.create_guild( - name="OK", - region=region, - icon=mock_image_obj, - verification_level=guilds.GuildVerificationLevel.NONE, - default_message_notifications=guilds.GuildMessageNotificationsLevel.ONLY_MENTIONS, - explicit_content_filter=guilds.GuildExplicitContentFilterLevel.MEMBERS_WITHOUT_ROLES, - roles=[mock_role_obj], - channels=[mock_channel_obj], - ) - assert result is mock_guild_obj - mock_image_obj.read.assert_awaited_once() - guilds.Guild.deserialize.assert_called_once_with(mock_guild_payload, app=rest_guild_logic_impl._app) - mock_channel_obj.serialize.assert_called_once() - mock_role_obj.serialize.assert_called_once() - rest_guild_logic_impl._session.create_guild.assert_called_once_with( - name="OK", - region="LONDON", - icon=mock_image_data, - verification_level=0, - default_message_notifications=1, - explicit_content_filter=1, - roles=[mock_role_payload], - channels=[mock_channel_payload], - ) - - @pytest.mark.asyncio - async def test_create_guild_without_optionals(self, rest_guild_logic_impl): - mock_guild_payload = {"id": "299292929292992", "region": "LONDON"} - mock_guild_obj = mock.MagicMock(guilds.Guild) - rest_guild_logic_impl._session.create_guild.return_value = mock_guild_payload - with mock.patch.object(guilds.Guild, "deserialize", return_value=mock_guild_obj): - assert await rest_guild_logic_impl.create_guild(name="OK") is mock_guild_obj - guilds.Guild.deserialize.assert_called_once_with(mock_guild_payload, app=rest_guild_logic_impl._app) - rest_guild_logic_impl._session.create_guild.assert_called_once_with( - name="OK", - region=..., - icon=..., - verification_level=..., - default_message_notifications=..., - explicit_content_filter=..., - roles=..., - channels=..., - ) - - @pytest.mark.asyncio - @_helpers.parametrize_valid_id_formats_for_models("guild", 379953393319542784, guilds.Guild) - async def test_fetch_guild(self, rest_guild_logic_impl, guild): - mock_guild_payload = {"id": "94949494", "name": "A guild", "roles": []} - mock_guild_obj = mock.MagicMock(guilds.Guild) - rest_guild_logic_impl._session.get_guild.return_value = mock_guild_payload - with mock.patch.object(guilds.Guild, "deserialize", return_value=mock_guild_obj): - assert await rest_guild_logic_impl.fetch_guild(guild) is mock_guild_obj - rest_guild_logic_impl._session.get_guild.assert_called_once_with( - guild_id="379953393319542784", with_counts=True - ) - guilds.Guild.deserialize.assert_called_once_with(mock_guild_payload, app=rest_guild_logic_impl._app) - - @pytest.mark.asyncio - @_helpers.parametrize_valid_id_formats_for_models("guild", 379953393319542784, guilds.Guild) - async def test_fetch_guild_preview(self, rest_guild_logic_impl, guild): - mock_guild_preview_payload = {"id": "94949494", "name": "A guild", "emojis": []} - mock_guild_preview_obj = mock.MagicMock(guilds.GuildPreview) - rest_guild_logic_impl._session.get_guild_preview.return_value = mock_guild_preview_payload - with mock.patch.object(guilds.GuildPreview, "deserialize", return_value=mock_guild_preview_obj): - assert await rest_guild_logic_impl.fetch_guild_preview(guild) is mock_guild_preview_obj - rest_guild_logic_impl._session.get_guild_preview.assert_called_once_with(guild_id="379953393319542784") - guilds.GuildPreview.deserialize.assert_called_once_with( - mock_guild_preview_payload, app=rest_guild_logic_impl._app - ) - - @pytest.mark.asyncio - @_helpers.parametrize_valid_id_formats_for_models("guild", 379953393319542784, guilds.Guild) - @_helpers.parametrize_valid_id_formats_for_models("afk_channel", 669517187031105607, guilds.Guild) - @_helpers.parametrize_valid_id_formats_for_models("owner", 379953393319542784, users.User) - @_helpers.parametrize_valid_id_formats_for_models("system_channel", 537340989808050216, users.User) - @pytest.mark.parametrize("region", ["LONDON", mock.MagicMock(voices.VoiceRegion, id="LONDON")]) - @pytest.mark.parametrize("afk_timeout", [300, datetime.timedelta(seconds=300)]) - async def test_update_guild_with_optionals( - self, rest_guild_logic_impl, guild, region, afk_channel, afk_timeout, owner, system_channel - ): - mock_guild_payload = {"id": "424242", "splash": "2lmKmklsdlksalkd"} - mock_guild_obj = mock.MagicMock(guilds.Guild) - rest_guild_logic_impl._session.modify_guild.return_value = mock_guild_payload - mock_icon_data = mock.MagicMock(bytes) - mock_icon_obj = mock.MagicMock(files.BaseStream) - mock_icon_obj.read = mock.AsyncMock(return_value=mock_icon_data) - mock_splash_data = mock.MagicMock(bytes) - mock_splash_obj = mock.MagicMock(files.BaseStream) - mock_splash_obj.read = mock.AsyncMock(return_value=mock_splash_data) - stack = contextlib.ExitStack() - stack.enter_context(mock.patch.object(guilds.Guild, "deserialize", return_value=mock_guild_obj)) - with stack: - result = await rest_guild_logic_impl.update_guild( - guild, - name="aNewName", - region=region, - verification_level=guilds.GuildVerificationLevel.LOW, - default_message_notifications=guilds.GuildMessageNotificationsLevel.ONLY_MENTIONS, - explicit_content_filter=guilds.GuildExplicitContentFilterLevel.ALL_MEMBERS, - afk_channel=afk_channel, - afk_timeout=afk_timeout, - icon=mock_icon_obj, - owner=owner, - splash=mock_splash_obj, - system_channel=system_channel, - reason="A good reason", - ) - assert result is mock_guild_obj - guilds.Guild.deserialize.assert_called_once_with(mock_guild_payload, app=rest_guild_logic_impl._app) - mock_icon_obj.read.assert_awaited_once() - mock_splash_obj.read.assert_awaited_once() - rest_guild_logic_impl._session.modify_guild.assert_called_once_with( - guild_id="379953393319542784", - name="aNewName", - region="LONDON", - verification_level=1, - default_message_notifications=1, - explicit_content_filter=2, - afk_channel_id="669517187031105607", - afk_timeout=300, - icon=mock_icon_data, - owner_id="379953393319542784", - splash=mock_splash_data, - system_channel_id="537340989808050216", - reason="A good reason", - ) - - @pytest.mark.asyncio - @_helpers.parametrize_valid_id_formats_for_models("guild", 379953393319542784, guilds.Guild) - async def test_update_guild_without_optionals(self, rest_guild_logic_impl, guild): - mock_guild_payload = {"id": "424242", "splash": "2lmKmklsdlksalkd"} - mock_guild_obj = mock.MagicMock(guilds.Guild) - rest_guild_logic_impl._session.modify_guild.return_value = mock_guild_payload - with mock.patch.object(guilds.Guild, "deserialize", return_value=mock_guild_obj): - assert await rest_guild_logic_impl.update_guild(guild) is mock_guild_obj - rest_guild_logic_impl._session.modify_guild.assert_called_once_with( - guild_id="379953393319542784", - name=..., - region=..., - verification_level=..., - default_message_notifications=..., - explicit_content_filter=..., - afk_channel_id=..., - afk_timeout=..., - icon=..., - owner_id=..., - splash=..., - system_channel_id=..., - reason=..., - ) - guilds.Guild.deserialize.assert_called_once_with(mock_guild_payload, app=rest_guild_logic_impl._app) - - @pytest.mark.asyncio - @_helpers.parametrize_valid_id_formats_for_models("guild", 379953393319542784, guilds.Guild) - async def test_delete_guild(self, rest_guild_logic_impl, guild): - rest_guild_logic_impl._session.delete_guild.return_value = ... - assert await rest_guild_logic_impl.delete_guild(guild) is None - rest_guild_logic_impl._session.delete_guild.assert_called_once_with(guild_id="379953393319542784") - - @pytest.mark.asyncio - @_helpers.parametrize_valid_id_formats_for_models("guild", 379953393319542784, guilds.Guild) - async def test_fetch_guild_channels(self, rest_guild_logic_impl, guild): - mock_channel_payload = {"id": "292929", "type": 1, "description": "A CHANNEL"} - mock_channel_obj = mock.MagicMock(channels.GuildChannel) - rest_guild_logic_impl._session.list_guild_channels.return_value = [mock_channel_payload] - with mock.patch.object(channels, "deserialize_channel", return_value=mock_channel_obj): - assert await rest_guild_logic_impl.fetch_guild_channels(guild) == [mock_channel_obj] - rest_guild_logic_impl._session.list_guild_channels.assert_called_once_with(guild_id="379953393319542784") - channels.deserialize_channel.assert_called_once_with(mock_channel_payload, app=rest_guild_logic_impl._app) - - @pytest.mark.asyncio - @_helpers.parametrize_valid_id_formats_for_models("guild", 123123123, guilds.Guild) - @_helpers.parametrize_valid_id_formats_for_models("category", 5555, channels.GuildCategory) - @pytest.mark.parametrize("rate_limit_per_user", [500, datetime.timedelta(seconds=500)]) - async def test_create_guild_channel_with_optionals( - self, rest_guild_logic_impl, guild, category, rate_limit_per_user - ): - mock_channel_payload = {"id": "22929292", "type": "5", "description": "A C H A N N E L"} - mock_channel_obj = mock.MagicMock(channels.GuildChannel) - mock_overwrite_payload = {"type": "member", "id": "30303030"} - mock_overwrite_obj = mock.MagicMock( - channels.PermissionOverwrite, serialize=mock.MagicMock(return_value=mock_overwrite_payload) - ) - rest_guild_logic_impl._session.create_guild_channel.return_value = mock_channel_payload - with mock.patch.object(channels, "deserialize_channel", return_value=mock_channel_obj): - result = await rest_guild_logic_impl.create_guild_channel( - guild, - "Hi-i-am-a-name", - channel_type=channels.ChannelType.GUILD_VOICE, - position=42, - topic="A TOPIC", - nsfw=True, - rate_limit_per_user=rate_limit_per_user, - bitrate=36000, - user_limit=5, - permission_overwrites=[mock_overwrite_obj], - parent_category=category, - reason="A GOOD REASON!", - ) - assert result is mock_channel_obj - mock_overwrite_obj.serialize.assert_called_once() - rest_guild_logic_impl._session.create_guild_channel.assert_called_once_with( - guild_id="123123123", - name="Hi-i-am-a-name", - type_=2, - position=42, - topic="A TOPIC", - nsfw=True, - rate_limit_per_user=500, - bitrate=36000, - user_limit=5, - permission_overwrites=[mock_overwrite_payload], - parent_id="5555", - reason="A GOOD REASON!", - ) - channels.deserialize_channel.assert_called_once_with(mock_channel_payload, app=rest_guild_logic_impl._app) - - @pytest.mark.asyncio - @_helpers.parametrize_valid_id_formats_for_models("guild", 123123123, guilds.Guild) - async def test_create_guild_channel_without_optionals(self, rest_guild_logic_impl, guild): - mock_channel_payload = {"id": "22929292", "type": "5", "description": "A C H A N N E L"} - mock_channel_obj = mock.MagicMock(channels.GuildChannel) - rest_guild_logic_impl._session.create_guild_channel.return_value = mock_channel_payload - with mock.patch.object(channels, "deserialize_channel", return_value=mock_channel_obj): - assert await rest_guild_logic_impl.create_guild_channel(guild, "Hi-i-am-a-name") is mock_channel_obj - rest_guild_logic_impl._session.create_guild_channel.assert_called_once_with( - guild_id="123123123", - name="Hi-i-am-a-name", - type_=..., - position=..., - topic=..., - nsfw=..., - rate_limit_per_user=..., - bitrate=..., - user_limit=..., - permission_overwrites=..., - parent_id=..., - reason=..., - ) - channels.deserialize_channel.assert_called_once_with(mock_channel_payload, app=rest_guild_logic_impl._app) - - @pytest.mark.asyncio - @_helpers.parametrize_valid_id_formats_for_models("guild", 123123123, guilds.Guild) - @_helpers.parametrize_valid_id_formats_for_models("channel", 379953393319542784, channels.GuildChannel) - @_helpers.parametrize_valid_id_formats_for_models("second_channel", 115590097100865541, channels.GuildChannel) - async def test_reposition_guild_channels(self, rest_guild_logic_impl, guild, channel, second_channel): - rest_guild_logic_impl._session.modify_guild_channel_positions.return_value = ... - assert await rest_guild_logic_impl.reposition_guild_channels(guild, (1, channel), (2, second_channel)) is None - rest_guild_logic_impl._session.modify_guild_channel_positions.assert_called_once_with( - "123123123", ("379953393319542784", 1), ("115590097100865541", 2) - ) - - @pytest.mark.asyncio - @_helpers.parametrize_valid_id_formats_for_models("guild", 444444, guilds.Guild) - @_helpers.parametrize_valid_id_formats_for_models("user", 123123123123, users.User) - async def test_fetch_member(self, rest_guild_logic_impl, guild, user): - mock_member_payload = {"user": {}, "nick": "! Agent 47"} - mock_member_obj = mock.MagicMock(guilds.GuildMember) - rest_guild_logic_impl._session.get_guild_member.return_value = mock_member_payload - with mock.patch.object(guilds.GuildMember, "deserialize", return_value=mock_member_obj): - assert await rest_guild_logic_impl.fetch_member(guild, user) is mock_member_obj - rest_guild_logic_impl._session.get_guild_member.assert_called_once_with( - guild_id="444444", user_id="123123123123" - ) - guilds.GuildMember.deserialize.assert_called_once_with(mock_member_payload, app=rest_guild_logic_impl._app) - - def test_fetch_members(self, rest_guild_logic_impl): - guild = mock.MagicMock() - - with mock.patch.object(_guild._MemberPaginator, "__init__", return_value=None) as init: - result = rest_guild_logic_impl.fetch_members(guild) - - assert isinstance(result, _guild._MemberPaginator) - init.assert_called_once_with( - guild=guild, created_after=None, app=rest_guild_logic_impl._app, session=rest_guild_logic_impl._session, - ) - - @pytest.mark.asyncio - @_helpers.parametrize_valid_id_formats_for_models("guild", 229292992, guilds.Guild) - @_helpers.parametrize_valid_id_formats_for_models("user", 1010101010, users.User) - @_helpers.parametrize_valid_id_formats_for_models("role", 11100010, guilds.Role) - @_helpers.parametrize_valid_id_formats_for_models("channel", 33333333, channels.GuildVoiceChannel) - async def test_update_member_with_optionals(self, rest_guild_logic_impl, guild, user, role, channel): - rest_guild_logic_impl._session.modify_guild_member.return_value = ... - result = await rest_guild_logic_impl.update_member( - guild, - user, - nickname="Nick's Name", - roles=[role], - mute=True, - deaf=False, - voice_channel=channel, - reason="Get Tagged.", - ) - assert result is None - rest_guild_logic_impl._session.modify_guild_member.assert_called_once_with( - guild_id="229292992", - user_id="1010101010", - nick="Nick's Name", - roles=["11100010"], - mute=True, - deaf=False, - channel_id="33333333", - reason="Get Tagged.", - ) - - @pytest.mark.asyncio - @_helpers.parametrize_valid_id_formats_for_models("guild", 229292992, guilds.Guild) - @_helpers.parametrize_valid_id_formats_for_models("user", 1010101010, users.User) - async def test_update_member_without_optionals(self, rest_guild_logic_impl, guild, user): - rest_guild_logic_impl._session.modify_guild_member.return_value = ... - assert await rest_guild_logic_impl.update_member(guild, user) is None - rest_guild_logic_impl._session.modify_guild_member.assert_called_once_with( - guild_id="229292992", - user_id="1010101010", - nick=..., - roles=..., - mute=..., - deaf=..., - channel_id=..., - reason=..., - ) - - @pytest.mark.asyncio - @_helpers.parametrize_valid_id_formats_for_models("guild", 229292992, guilds.Guild) - async def test_update_my_member_nickname_with_reason(self, rest_guild_logic_impl, guild): - rest_guild_logic_impl._session.modify_current_user_nick.return_value = ... - result = await rest_guild_logic_impl.update_my_member_nickname( - guild, "Nick's nick", reason="I want to drink your blood." - ) - assert result is None - rest_guild_logic_impl._session.modify_current_user_nick.assert_called_once_with( - guild_id="229292992", nick="Nick's nick", reason="I want to drink your blood." - ) - - @pytest.mark.asyncio - @_helpers.parametrize_valid_id_formats_for_models("guild", 229292992, guilds.Guild) - async def test_update_my_member_nickname_without_reason(self, rest_guild_logic_impl, guild): - rest_guild_logic_impl._session.modify_current_user_nick.return_value = ... - assert await rest_guild_logic_impl.update_my_member_nickname(guild, "Nick's nick") is None - rest_guild_logic_impl._session.modify_current_user_nick.assert_called_once_with( - guild_id="229292992", nick="Nick's nick", reason=... - ) - - @pytest.mark.asyncio - @_helpers.parametrize_valid_id_formats_for_models("guild", 123123123, guilds.Guild) - @_helpers.parametrize_valid_id_formats_for_models("user", 4444444, users.User) - @_helpers.parametrize_valid_id_formats_for_models("role", 101010101, guilds.Role) - async def test_add_role_to_member_with_reason(self, rest_guild_logic_impl, guild, user, role): - rest_guild_logic_impl._session.add_guild_member_role.return_value = ... - assert await rest_guild_logic_impl.add_role_to_member(guild, user, role, reason="Get role'd") is None - rest_guild_logic_impl._session.add_guild_member_role.assert_called_once_with( - guild_id="123123123", user_id="4444444", role_id="101010101", reason="Get role'd" - ) - - @pytest.mark.asyncio - @_helpers.parametrize_valid_id_formats_for_models("guild", 123123123, guilds.Guild) - @_helpers.parametrize_valid_id_formats_for_models("user", 4444444, users.User) - @_helpers.parametrize_valid_id_formats_for_models("role", 101010101, guilds.Role) - async def test_add_role_to_member_without_reason(self, rest_guild_logic_impl, guild, user, role): - rest_guild_logic_impl._session.add_guild_member_role.return_value = ... - assert await rest_guild_logic_impl.add_role_to_member(guild, user, role) is None - rest_guild_logic_impl._session.add_guild_member_role.assert_called_once_with( - guild_id="123123123", user_id="4444444", role_id="101010101", reason=... - ) - - @pytest.mark.asyncio - @_helpers.parametrize_valid_id_formats_for_models("guild", 123123123, guilds.Guild) - @_helpers.parametrize_valid_id_formats_for_models("user", 4444444, users.User) - @_helpers.parametrize_valid_id_formats_for_models("role", 101010101, guilds.Role) - async def test_remove_role_from_member_with_reason(self, rest_guild_logic_impl, guild, user, role): - rest_guild_logic_impl._session.remove_guild_member_role.return_value = ... - assert await rest_guild_logic_impl.remove_role_from_member(guild, user, role, reason="Get role'd") is None - rest_guild_logic_impl._session.remove_guild_member_role.assert_called_once_with( - guild_id="123123123", user_id="4444444", role_id="101010101", reason="Get role'd" - ) - - @pytest.mark.asyncio - @_helpers.parametrize_valid_id_formats_for_models("guild", 123123123, guilds.Guild) - @_helpers.parametrize_valid_id_formats_for_models("user", 4444444, users.User) - @_helpers.parametrize_valid_id_formats_for_models("role", 101010101, guilds.Role) - async def test_remove_role_from_member_without_reason(self, rest_guild_logic_impl, guild, user, role): - rest_guild_logic_impl._session.remove_guild_member_role.return_value = ... - assert await rest_guild_logic_impl.remove_role_from_member(guild, user, role) is None - rest_guild_logic_impl._session.remove_guild_member_role.assert_called_once_with( - guild_id="123123123", user_id="4444444", role_id="101010101", reason=... - ) - - @pytest.mark.asyncio - @_helpers.parametrize_valid_id_formats_for_models("guild", 123123123, guilds.Guild) - @_helpers.parametrize_valid_id_formats_for_models("user", 4444444, users.User) - async def test_kick_member_with_reason(self, rest_guild_logic_impl, guild, user): - rest_guild_logic_impl._session.remove_guild_member.return_value = ... - assert await rest_guild_logic_impl.kick_member(guild, user, reason="TO DO") is None - rest_guild_logic_impl._session.remove_guild_member.assert_called_once_with( - guild_id="123123123", user_id="4444444", reason="TO DO" - ) - - @pytest.mark.asyncio - @_helpers.parametrize_valid_id_formats_for_models("guild", 123123123, guilds.Guild) - @_helpers.parametrize_valid_id_formats_for_models("user", 4444444, users.User) - async def test_kick_member_without_reason(self, rest_guild_logic_impl, guild, user): - rest_guild_logic_impl._session.remove_guild_member.return_value = ... - assert await rest_guild_logic_impl.kick_member(guild, user) is None - rest_guild_logic_impl._session.remove_guild_member.assert_called_once_with( - guild_id="123123123", user_id="4444444", reason=..., - ) - - @pytest.mark.asyncio - @_helpers.parametrize_valid_id_formats_for_models("guild", 123123123, guilds.Guild) - @_helpers.parametrize_valid_id_formats_for_models("user", 4444444, users.User) - async def test_fetch_ban(self, rest_guild_logic_impl, guild, user): - mock_ban_payload = {"reason": "42'd", "user": {}} - mock_ban_obj = mock.MagicMock(guilds.GuildMemberBan) - rest_guild_logic_impl._session.get_guild_ban.return_value = mock_ban_payload - with mock.patch.object(guilds.GuildMemberBan, "deserialize", return_value=mock_ban_obj): - assert await rest_guild_logic_impl.fetch_ban(guild, user) is mock_ban_obj - rest_guild_logic_impl._session.get_guild_ban.assert_called_once_with( - guild_id="123123123", user_id="4444444" - ) - guilds.GuildMemberBan.deserialize.assert_called_once_with(mock_ban_payload, app=rest_guild_logic_impl._app) - - @pytest.mark.asyncio - @_helpers.parametrize_valid_id_formats_for_models("guild", 123123123, guilds.Guild) - async def test_fetch_bans(self, rest_guild_logic_impl, guild): - mock_ban_payload = {"reason": "42'd", "user": {}} - mock_ban_obj = mock.MagicMock(guilds.GuildMemberBan) - rest_guild_logic_impl._session.get_guild_bans.return_value = [mock_ban_payload] - with mock.patch.object(guilds.GuildMemberBan, "deserialize", return_value=mock_ban_obj): - assert await rest_guild_logic_impl.fetch_bans(guild) == [mock_ban_obj] - rest_guild_logic_impl._session.get_guild_bans.assert_called_once_with(guild_id="123123123") - guilds.GuildMemberBan.deserialize.assert_called_once_with(mock_ban_payload, app=rest_guild_logic_impl._app) - - @pytest.mark.asyncio - @_helpers.parametrize_valid_id_formats_for_models("guild", 123123123, guilds.Guild) - @_helpers.parametrize_valid_id_formats_for_models("user", 4444444, users.User) - @pytest.mark.parametrize("delete_message_days", [datetime.timedelta(days=12), 12]) - async def test_ban_member_with_optionals(self, rest_guild_logic_impl, guild, user, delete_message_days): - rest_guild_logic_impl._session.create_guild_ban.return_value = ... - result = await rest_guild_logic_impl.ban_member( - guild, user, delete_message_days=delete_message_days, reason="bye" - ) - assert result is None - rest_guild_logic_impl._session.create_guild_ban.assert_called_once_with( - guild_id="123123123", user_id="4444444", delete_message_days=12, reason="bye" - ) - - @pytest.mark.asyncio - @_helpers.parametrize_valid_id_formats_for_models("guild", 123123123, guilds.Guild) - @_helpers.parametrize_valid_id_formats_for_models("user", 4444444, users.User) - async def test_ban_member_without_optionals(self, rest_guild_logic_impl, guild, user): - rest_guild_logic_impl._session.create_guild_ban.return_value = ... - assert await rest_guild_logic_impl.ban_member(guild, user) is None - rest_guild_logic_impl._session.create_guild_ban.assert_called_once_with( - guild_id="123123123", user_id="4444444", delete_message_days=..., reason=... - ) - - @pytest.mark.asyncio - @_helpers.parametrize_valid_id_formats_for_models("guild", 123123123, guilds.Guild) - @_helpers.parametrize_valid_id_formats_for_models("user", 4444444, users.User) - async def test_unban_member_with_reason(self, rest_guild_logic_impl, guild, user): - rest_guild_logic_impl._session.remove_guild_ban.return_value = ... - result = await rest_guild_logic_impl.unban_member(guild, user, reason="bye") - assert result is None - rest_guild_logic_impl._session.remove_guild_ban.assert_called_once_with( - guild_id="123123123", user_id="4444444", reason="bye" - ) - - @pytest.mark.asyncio - @_helpers.parametrize_valid_id_formats_for_models("guild", 123123123, guilds.Guild) - @_helpers.parametrize_valid_id_formats_for_models("user", 4444444, users.User) - async def test_unban_member_without_reason(self, rest_guild_logic_impl, guild, user): - rest_guild_logic_impl._session.remove_guild_ban.return_value = ... - assert await rest_guild_logic_impl.unban_member(guild, user) is None - rest_guild_logic_impl._session.remove_guild_ban.assert_called_once_with( - guild_id="123123123", user_id="4444444", reason=... - ) - - @pytest.mark.asyncio - @_helpers.parametrize_valid_id_formats_for_models("guild", 574921006817476608, guilds.Guild) - async def test_fetch_roles(self, rest_guild_logic_impl, guild): - mock_role_payload = {"id": "33030", "permissions": 333, "name": "ROlE"} - mock_role_obj = mock.MagicMock(guilds.Role, id=33030) - rest_guild_logic_impl._session.get_guild_roles.return_value = [mock_role_payload] - with mock.patch.object(guilds.Role, "deserialize", return_value=mock_role_obj): - assert await rest_guild_logic_impl.fetch_roles(guild) == {33030: mock_role_obj} - rest_guild_logic_impl._session.get_guild_roles.assert_called_once_with(guild_id="574921006817476608") - guilds.Role.deserialize.assert_called_once_with(mock_role_payload, app=rest_guild_logic_impl._app) - - @pytest.mark.asyncio - @_helpers.parametrize_valid_id_formats_for_models("guild", 574921006817476608, guilds.Guild) - async def test_create_role_with_optionals(self, rest_guild_logic_impl, guild): - mock_role_payload = {"id": "033030", "permissions": 333, "name": "ROlE"} - mock_role_obj = mock.MagicMock(guilds.Role) - rest_guild_logic_impl._session.create_guild_role.return_value = mock_role_payload - with mock.patch.object(guilds.Role, "deserialize", return_value=mock_role_obj): - result = await rest_guild_logic_impl.create_role( - guild, - name="Roleington", - permissions=permissions.Permission.STREAM | permissions.Permission.EMBED_LINKS, - color=colors.Color(21312), - hoist=True, - mentionable=False, - reason="And then there was a role.", - ) - assert result is mock_role_obj - rest_guild_logic_impl._session.create_guild_role.assert_called_once_with( - guild_id="574921006817476608", - name="Roleington", - permissions=16896, - color=21312, - hoist=True, - mentionable=False, - reason="And then there was a role.", - ) - guilds.Role.deserialize.assert_called_once_with(mock_role_payload, app=rest_guild_logic_impl._app) - - @pytest.mark.asyncio - @_helpers.parametrize_valid_id_formats_for_models("guild", 574921006817476608, guilds.Guild) - async def test_create_role_without_optionals(self, rest_guild_logic_impl, guild): - mock_role_payload = {"id": "033030", "permissions": 333, "name": "ROlE"} - mock_role_obj = mock.MagicMock(guilds.Role) - rest_guild_logic_impl._session.create_guild_role.return_value = mock_role_payload - with mock.patch.object(guilds.Role, "deserialize", return_value=mock_role_obj): - result = await rest_guild_logic_impl.create_role(guild) - assert result is mock_role_obj - rest_guild_logic_impl._session.create_guild_role.assert_called_once_with( - guild_id="574921006817476608", - name=..., - permissions=..., - color=..., - hoist=..., - mentionable=..., - reason=..., - ) - guilds.Role.deserialize.assert_called_once_with(mock_role_payload, app=rest_guild_logic_impl._app) - - @pytest.mark.asyncio - @_helpers.parametrize_valid_id_formats_for_models("guild", 574921006817476608, guilds.Guild) - @_helpers.parametrize_valid_id_formats_for_models("role", 123123, guilds.Role) - @_helpers.parametrize_valid_id_formats_for_models("additional_role", 123456, guilds.Role) - async def test_reposition_roles(self, rest_guild_logic_impl, guild, role, additional_role): - mock_role_payload = {"id": "033030", "permissions": 333, "name": "ROlE"} - mock_role_obj = mock.MagicMock(guilds.Role) - rest_guild_logic_impl._session.modify_guild_role_positions.return_value = [mock_role_payload] - with mock.patch.object(guilds.Role, "deserialize", return_value=mock_role_obj): - result = await rest_guild_logic_impl.reposition_roles(guild, (1, role), (2, additional_role)) - assert result == [mock_role_obj] - rest_guild_logic_impl._session.modify_guild_role_positions.assert_called_once_with( - "574921006817476608", ("123123", 1), ("123456", 2) - ) - guilds.Role.deserialize.assert_called_once_with(mock_role_payload, app=rest_guild_logic_impl._app) - - @pytest.mark.asyncio - @_helpers.parametrize_valid_id_formats_for_models("guild", 574921006817476608, guilds.Guild) - @_helpers.parametrize_valid_id_formats_for_models("role", 123123, guilds.Role) - async def test_update_role_with_optionals(self, rest_guild_logic_impl, guild, role): - mock_role_payload = {"id": "033030", "permissions": 333, "name": "ROlE"} - mock_role_obj = mock.MagicMock(guilds.Role) - rest_guild_logic_impl._session.modify_guild_role.return_value = mock_role_payload - with mock.patch.object(guilds.Role, "deserialize", return_value=mock_role_obj): - result = await rest_guild_logic_impl.update_role( - guild, - role, - name="ROLE", - permissions=permissions.Permission.STREAM | permissions.Permission.EMBED_LINKS, - color=colors.Color(12312), - hoist=True, - mentionable=False, - reason="Why not?", - ) - assert result is mock_role_obj - rest_guild_logic_impl._session.modify_guild_role.assert_called_once_with( - guild_id="574921006817476608", - role_id="123123", - name="ROLE", - permissions=16896, - color=12312, - hoist=True, - mentionable=False, - reason="Why not?", - ) - guilds.Role.deserialize.assert_called_once_with(mock_role_payload, app=rest_guild_logic_impl._app) - - @pytest.mark.asyncio - @_helpers.parametrize_valid_id_formats_for_models("guild", 574921006817476608, guilds.Guild) - @_helpers.parametrize_valid_id_formats_for_models("role", 123123, guilds.Role) - async def test_update_role_without_optionals(self, rest_guild_logic_impl, guild, role): - mock_role_payload = {"id": "033030", "permissions": 333, "name": "ROlE"} - mock_role_obj = mock.MagicMock(guilds.Role) - rest_guild_logic_impl._session.modify_guild_role.return_value = mock_role_payload - with mock.patch.object(guilds.Role, "deserialize", return_value=mock_role_obj): - assert await rest_guild_logic_impl.update_role(guild, role) is mock_role_obj - rest_guild_logic_impl._session.modify_guild_role.assert_called_once_with( - guild_id="574921006817476608", - role_id="123123", - name=..., - permissions=..., - color=..., - hoist=..., - mentionable=..., - reason=..., - ) - guilds.Role.deserialize.assert_called_once_with(mock_role_payload, app=rest_guild_logic_impl._app) - - @pytest.mark.asyncio - @_helpers.parametrize_valid_id_formats_for_models("guild", 574921006817476608, guilds.Guild) - @_helpers.parametrize_valid_id_formats_for_models("role", 123123, guilds.Role) - async def test_delete_role(self, rest_guild_logic_impl, guild, role): - rest_guild_logic_impl._session.delete_guild_role.return_value = ... - assert await rest_guild_logic_impl.delete_role(guild, role) is None - rest_guild_logic_impl._session.delete_guild_role.assert_called_once_with( - guild_id="574921006817476608", role_id="123123" - ) - - @pytest.mark.asyncio - @_helpers.parametrize_valid_id_formats_for_models("guild", 574921006817476608, guilds.Guild) - @pytest.mark.parametrize("days", [7, datetime.timedelta(days=7)]) - async def test_estimate_guild_prune_count(self, rest_guild_logic_impl, guild, days): - rest_guild_logic_impl._session.get_guild_prune_count.return_value = 42 - assert await rest_guild_logic_impl.estimate_guild_prune_count(guild, days) == 42 - rest_guild_logic_impl._session.get_guild_prune_count.assert_called_once_with( - guild_id="574921006817476608", days=7 - ) - - @pytest.mark.asyncio - @_helpers.parametrize_valid_id_formats_for_models("guild", 574921006817476608, guilds.Guild) - @pytest.mark.parametrize("days", [7, datetime.timedelta(days=7)]) - async def test_estimate_guild_with_optionals(self, rest_guild_logic_impl, guild, days): - rest_guild_logic_impl._session.begin_guild_prune.return_value = None - assert ( - await rest_guild_logic_impl.begin_guild_prune(guild, days, compute_prune_count=True, reason="nah m8") - is None - ) - rest_guild_logic_impl._session.begin_guild_prune.assert_called_once_with( - guild_id="574921006817476608", days=7, compute_prune_count=True, reason="nah m8" - ) - - @pytest.mark.asyncio - @_helpers.parametrize_valid_id_formats_for_models("guild", 574921006817476608, guilds.Guild) - @pytest.mark.parametrize("days", [7, datetime.timedelta(days=7)]) - async def test_estimate_guild_without_optionals(self, rest_guild_logic_impl, guild, days): - rest_guild_logic_impl._session.begin_guild_prune.return_value = 42 - assert await rest_guild_logic_impl.begin_guild_prune(guild, days) == 42 - rest_guild_logic_impl._session.begin_guild_prune.assert_called_once_with( - guild_id="574921006817476608", days=7, compute_prune_count=..., reason=... - ) - - @pytest.mark.asyncio - @_helpers.parametrize_valid_id_formats_for_models("guild", 574921006817476608, guilds.Guild) - async def test_fetch_guild_voice_regions(self, rest_guild_logic_impl, guild): - mock_voice_payload = {"name": "london", "id": "LONDON"} - mock_voice_obj = mock.MagicMock(voices.VoiceRegion) - rest_guild_logic_impl._session.get_guild_voice_regions.return_value = [mock_voice_payload] - with mock.patch.object(voices.VoiceRegion, "deserialize", return_value=mock_voice_obj): - assert await rest_guild_logic_impl.fetch_guild_voice_regions(guild) == [mock_voice_obj] - rest_guild_logic_impl._session.get_guild_voice_regions.assert_called_once_with( - guild_id="574921006817476608" - ) - voices.VoiceRegion.deserialize.assert_called_once_with(mock_voice_payload, app=rest_guild_logic_impl._app) - - @pytest.mark.asyncio - @_helpers.parametrize_valid_id_formats_for_models("guild", 574921006817476608, guilds.Guild) - async def test_fetch_guild_invites(self, rest_guild_logic_impl, guild): - mock_invite_payload = {"code": "dododo"} - mock_invite_obj = mock.MagicMock(invites.InviteWithMetadata) - rest_guild_logic_impl._session.get_guild_invites.return_value = [mock_invite_payload] - with mock.patch.object(invites.InviteWithMetadata, "deserialize", return_value=mock_invite_obj): - assert await rest_guild_logic_impl.fetch_guild_invites(guild) == [mock_invite_obj] - invites.InviteWithMetadata.deserialize.assert_called_once_with( - mock_invite_payload, app=rest_guild_logic_impl._app - ) - - @pytest.mark.asyncio - @_helpers.parametrize_valid_id_formats_for_models("guild", 574921006817476608, guilds.Guild) - async def test_fetch_integrations(self, rest_guild_logic_impl, guild): - mock_integration_payload = {"id": "123123", "name": "Integrated", "type": "twitch"} - mock_integration_obj = mock.MagicMock(guilds.GuildIntegration) - rest_guild_logic_impl._session.get_guild_integrations.return_value = [mock_integration_payload] - with mock.patch.object(guilds.GuildIntegration, "deserialize", return_value=mock_integration_obj): - assert await rest_guild_logic_impl.fetch_integrations(guild) == [mock_integration_obj] - rest_guild_logic_impl._session.get_guild_integrations.assert_called_once_with(guild_id="574921006817476608") - guilds.GuildIntegration.deserialize.assert_called_once_with( - mock_integration_payload, app=rest_guild_logic_impl._app - ) - - @pytest.mark.asyncio - @_helpers.parametrize_valid_id_formats_for_models("guild", 574921006817476608, guilds.Guild) - @_helpers.parametrize_valid_id_formats_for_models("integration", 379953393319542784, guilds.GuildIntegration) - @pytest.mark.parametrize("period", [datetime.timedelta(days=7), 7]) - async def test_update_integration_with_optionals(self, rest_guild_logic_impl, guild, integration, period): - rest_guild_logic_impl._session.modify_guild_integration.return_value = ... - result = await rest_guild_logic_impl.update_integration( - guild, - integration, - expire_behaviour=guilds.IntegrationExpireBehaviour.KICK, - expire_grace_period=period, - enable_emojis=True, - reason="GET YEET'D", - ) - assert result is None - rest_guild_logic_impl._session.modify_guild_integration.assert_called_once_with( - guild_id="574921006817476608", - integration_id="379953393319542784", - expire_behaviour=1, - expire_grace_period=7, - enable_emojis=True, - reason="GET YEET'D", - ) - - @pytest.mark.asyncio - @_helpers.parametrize_valid_id_formats_for_models("guild", 574921006817476608, guilds.Guild) - @_helpers.parametrize_valid_id_formats_for_models("integration", 379953393319542784, guilds.GuildIntegration) - async def test_update_integration_without_optionals(self, rest_guild_logic_impl, guild, integration): - rest_guild_logic_impl._session.modify_guild_integration.return_value = ... - assert await rest_guild_logic_impl.update_integration(guild, integration) is None - rest_guild_logic_impl._session.modify_guild_integration.assert_called_once_with( - guild_id="574921006817476608", - integration_id="379953393319542784", - expire_behaviour=..., - expire_grace_period=..., - enable_emojis=..., - reason=..., - ) - - @pytest.mark.asyncio - @_helpers.parametrize_valid_id_formats_for_models("guild", 574921006817476608, guilds.Guild) - @_helpers.parametrize_valid_id_formats_for_models("integration", 379953393319542784, guilds.GuildIntegration) - async def test_delete_integration_with_reason(self, rest_guild_logic_impl, guild, integration): - rest_guild_logic_impl._session.delete_guild_integration.return_value = ... - assert await rest_guild_logic_impl.delete_integration(guild, integration, reason="B Y E") is None - rest_guild_logic_impl._session.delete_guild_integration.assert_called_once_with( - guild_id="574921006817476608", integration_id="379953393319542784", reason="B Y E" - ) - - @pytest.mark.asyncio - @_helpers.parametrize_valid_id_formats_for_models("guild", 574921006817476608, guilds.Guild) - @_helpers.parametrize_valid_id_formats_for_models("integration", 379953393319542784, guilds.GuildIntegration) - async def test_delete_integration_without_reason(self, rest_guild_logic_impl, guild, integration): - rest_guild_logic_impl._session.delete_guild_integration.return_value = ... - assert await rest_guild_logic_impl.delete_integration(guild, integration) is None - rest_guild_logic_impl._session.delete_guild_integration.assert_called_once_with( - guild_id="574921006817476608", integration_id="379953393319542784", reason=... - ) - - @pytest.mark.asyncio - @_helpers.parametrize_valid_id_formats_for_models("guild", 574921006817476608, guilds.Guild) - @_helpers.parametrize_valid_id_formats_for_models("integration", 379953393319542784, guilds.GuildIntegration) - async def test_sync_guild_integration(self, rest_guild_logic_impl, guild, integration): - rest_guild_logic_impl._session.sync_guild_integration.return_value = ... - assert await rest_guild_logic_impl.sync_guild_integration(guild, integration) is None - rest_guild_logic_impl._session.sync_guild_integration.assert_called_once_with( - guild_id="574921006817476608", integration_id="379953393319542784", - ) - - @pytest.mark.asyncio - @_helpers.parametrize_valid_id_formats_for_models("guild", 574921006817476608, guilds.Guild) - async def test_fetch_guild_embed(self, rest_guild_logic_impl, guild): - mock_embed_payload = {"enabled": True, "channel": "2020202"} - mock_embed_obj = mock.MagicMock(guilds.GuildEmbed) - rest_guild_logic_impl._session.get_guild_embed.return_value = mock_embed_payload - with mock.patch.object(guilds.GuildEmbed, "deserialize", return_value=mock_embed_obj): - assert await rest_guild_logic_impl.fetch_guild_embed(guild) is mock_embed_obj - rest_guild_logic_impl._session.get_guild_embed.assert_called_once_with(guild_id="574921006817476608") - guilds.GuildEmbed.deserialize.assert_called_once_with(mock_embed_payload, app=rest_guild_logic_impl._app) - - @pytest.mark.asyncio - @_helpers.parametrize_valid_id_formats_for_models("guild", 574921006817476608, guilds.Guild) - @_helpers.parametrize_valid_id_formats_for_models("channel", 123123, channels.GuildChannel) - async def test_update_guild_embed_with_optionnal(self, rest_guild_logic_impl, guild, channel): - mock_embed_payload = {"enabled": True, "channel": "2020202"} - mock_embed_obj = mock.MagicMock(guilds.GuildEmbed) - rest_guild_logic_impl._session.modify_guild_embed.return_value = mock_embed_payload - with mock.patch.object(guilds.GuildEmbed, "deserialize", return_value=mock_embed_obj): - result = await rest_guild_logic_impl.update_guild_embed( - guild, channel=channel, enabled=True, reason="Nyaa!!!" - ) - assert result is mock_embed_obj - rest_guild_logic_impl._session.modify_guild_embed.assert_called_once_with( - guild_id="574921006817476608", channel_id="123123", enabled=True, reason="Nyaa!!!" - ) - guilds.GuildEmbed.deserialize.assert_called_once_with(mock_embed_payload, app=rest_guild_logic_impl._app) - - @pytest.mark.asyncio - @_helpers.parametrize_valid_id_formats_for_models("guild", 574921006817476608, guilds.Guild) - async def test_update_guild_embed_without_optionnal(self, rest_guild_logic_impl, guild): - mock_embed_payload = {"enabled": True, "channel": "2020202"} - mock_embed_obj = mock.MagicMock(guilds.GuildEmbed) - rest_guild_logic_impl._session.modify_guild_embed.return_value = mock_embed_payload - with mock.patch.object(guilds.GuildEmbed, "deserialize", return_value=mock_embed_obj): - assert await rest_guild_logic_impl.update_guild_embed(guild) is mock_embed_obj - rest_guild_logic_impl._session.modify_guild_embed.assert_called_once_with( - guild_id="574921006817476608", channel_id=..., enabled=..., reason=... - ) - guilds.GuildEmbed.deserialize.assert_called_once_with(mock_embed_payload, app=rest_guild_logic_impl._app) - - @pytest.mark.asyncio - @_helpers.parametrize_valid_id_formats_for_models("guild", 574921006817476608, guilds.Guild) - async def test_fetch_guild_vanity_url(self, rest_guild_logic_impl, guild): - mock_vanity_payload = {"code": "akfdk", "uses": 5} - mock_vanity_obj = mock.MagicMock(invites.VanityUrl) - rest_guild_logic_impl._session.get_guild_vanity_url.return_value = mock_vanity_payload - with mock.patch.object(invites.VanityUrl, "deserialize", return_value=mock_vanity_obj): - assert await rest_guild_logic_impl.fetch_guild_vanity_url(guild) is mock_vanity_obj - rest_guild_logic_impl._session.get_guild_vanity_url.assert_called_once_with(guild_id="574921006817476608") - invites.VanityUrl.deserialize.assert_called_once_with(mock_vanity_payload, app=rest_guild_logic_impl._app) - - @_helpers.parametrize_valid_id_formats_for_models("guild", 574921006817476608, guilds.Guild) - def test_fetch_guild_widget_image_with_style(self, rest_guild_logic_impl, guild): - mock_url = "not/a/url" - rest_guild_logic_impl._session.get_guild_widget_image_url.return_value = mock_url - assert rest_guild_logic_impl.format_guild_widget_image(guild, style="notAStyle") == mock_url - rest_guild_logic_impl._session.get_guild_widget_image_url.assert_called_once_with( - guild_id="574921006817476608", style="notAStyle", - ) - - @_helpers.parametrize_valid_id_formats_for_models("guild", 574921006817476608, guilds.Guild) - def test_fetch_guild_widget_image_without_style(self, rest_guild_logic_impl, guild): - mock_url = "not/a/url" - rest_guild_logic_impl._session.get_guild_widget_image_url.return_value = mock_url - assert rest_guild_logic_impl.format_guild_widget_image(guild) == mock_url - rest_guild_logic_impl._session.get_guild_widget_image_url.assert_called_once_with( - guild_id="574921006817476608", style=..., - ) - - @pytest.mark.asyncio - @_helpers.parametrize_valid_id_formats_for_models("channel", 115590097100865541, channels.GuildChannel) - async def test_fetch_guild_webhooks(self, rest_guild_logic_impl, channel): - mock_webhook_payload = {"id": "29292929", "channel": "2292992"} - mock_webhook_obj = mock.MagicMock(webhooks.Webhook) - rest_guild_logic_impl._session.get_guild_webhooks.return_value = [mock_webhook_payload] - with mock.patch.object(webhooks.Webhook, "deserialize", return_value=mock_webhook_obj): - assert await rest_guild_logic_impl.fetch_guild_webhooks(channel) == [mock_webhook_obj] - rest_guild_logic_impl._session.get_guild_webhooks.assert_called_once_with(guild_id="115590097100865541") - webhooks.Webhook.deserialize.assert_called_once_with(mock_webhook_payload, app=rest_guild_logic_impl._app) diff --git a/tests/hikari/rest/test_invite.py b/tests/hikari/rest/test_invite.py deleted file mode 100644 index 4b9d8343a7..0000000000 --- a/tests/hikari/rest/test_invite.py +++ /dev/null @@ -1,71 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# Copyright © Nekoka.tt 2019-2020 -# -# This file is part of Hikari. -# -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . -import mock -import pytest - -from hikari import application -from hikari.models import invites -from hikari.net.rest import invite -from hikari.net.rest import session - - -class TestRESTInviteLogic: - @pytest.fixture() - def rest_invite_logic_impl(self): - mock_app = mock.MagicMock(application.Application) - mock_low_level_restful_client = mock.MagicMock(session.RESTSession) - - class RESTInviteLogicImpl(invite.RESTInviteComponent): - def __init__(self): - super().__init__(mock_app, mock_low_level_restful_client) - - return RESTInviteLogicImpl() - - @pytest.mark.asyncio - @pytest.mark.parametrize("invite", [mock.MagicMock(invites.Invite, code="AAAAAAAAAAAAAAAA"), "AAAAAAAAAAAAAAAA"]) - async def test_fetch_invite_with_counts(self, rest_invite_logic_impl, invite): - mock_invite_payload = {"code": "AAAAAAAAAAAAAAAA", "guild": {}, "channel": {}} - mock_invite_obj = mock.MagicMock(invites.Invite) - rest_invite_logic_impl._session.get_invite.return_value = mock_invite_payload - with mock.patch.object(invites.Invite, "deserialize", return_value=mock_invite_obj): - assert await rest_invite_logic_impl.fetch_invite(invite, with_counts=True) is mock_invite_obj - rest_invite_logic_impl._session.get_invite.assert_called_once_with( - invite_code="AAAAAAAAAAAAAAAA", with_counts=True, - ) - invites.Invite.deserialize.assert_called_once_with(mock_invite_payload, app=rest_invite_logic_impl._app) - - @pytest.mark.asyncio - @pytest.mark.parametrize("invite", [mock.MagicMock(invites.Invite, code="AAAAAAAAAAAAAAAA"), "AAAAAAAAAAAAAAAA"]) - async def test_fetch_invite_without_counts(self, rest_invite_logic_impl, invite): - mock_invite_payload = {"code": "AAAAAAAAAAAAAAAA", "guild": {}, "channel": {}} - mock_invite_obj = mock.MagicMock(invites.Invite) - rest_invite_logic_impl._session.get_invite.return_value = mock_invite_payload - with mock.patch.object(invites.Invite, "deserialize", return_value=mock_invite_obj): - assert await rest_invite_logic_impl.fetch_invite(invite) is mock_invite_obj - rest_invite_logic_impl._session.get_invite.assert_called_once_with( - invite_code="AAAAAAAAAAAAAAAA", with_counts=..., - ) - invites.Invite.deserialize.assert_called_once_with(mock_invite_payload, app=rest_invite_logic_impl._app) - - @pytest.mark.asyncio - @pytest.mark.parametrize("invite", [mock.MagicMock(invites.Invite, code="AAAAAAAAAAAAAAAA"), "AAAAAAAAAAAAAAAA"]) - async def test_delete_invite(self, rest_invite_logic_impl, invite): - rest_invite_logic_impl._session.delete_invite.return_value = ... - assert await rest_invite_logic_impl.delete_invite(invite) is None - rest_invite_logic_impl._session.delete_invite.assert_called_once_with(invite_code="AAAAAAAAAAAAAAAA") diff --git a/tests/hikari/rest/test_me.py b/tests/hikari/rest/test_me.py deleted file mode 100644 index 222b90fb0b..0000000000 --- a/tests/hikari/rest/test_me.py +++ /dev/null @@ -1,246 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# Copyright © Nekoka.tt 2019-2020 -# -# This file is part of Hikari. -# -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . -import contextlib -import inspect - -import mock -import pytest - -from hikari import application -from hikari.models import applications -from hikari.models import bases -from hikari.models import channels -from hikari.models import files -from hikari.models import guilds -from hikari.models import users -from hikari.net.rest import me -from hikari.net.rest import session -from tests.hikari import _helpers - - -class TestGuildPaginator: - @pytest.fixture() - def mock_session(self): - return mock.MagicMock(spec_set=session.RESTSession) - - @pytest.fixture() - def mock_app(self): - return mock.MagicMock(spec_set=application.Application) - - @pytest.fixture() - def ownguild_cls(self): - with mock.patch.object(applications, "OwnGuild") as own_guild_cls: - yield own_guild_cls - - @pytest.mark.parametrize( - ["newest_first", "expected_first_id"], [(True, str(bases.Snowflake.max())), (False, str(bases.Snowflake.min()))] - ) - def test_init_with_no_explicit_first_element(self, newest_first, expected_first_id, mock_app, mock_session): - pag = me._GuildPaginator(mock_app, newest_first, None, mock_session) - assert pag._first_id == expected_first_id - assert pag._newest_first is newest_first - assert pag._app is mock_app - assert pag._session is mock_session - - def test_init_with_explicit_first_element(self, mock_app, mock_session): - pag = me._GuildPaginator(mock_app, False, 12345, mock_session) - assert pag._first_id == "12345" - assert pag._newest_first is False - assert pag._app is mock_app - assert pag._session is mock_session - - @pytest.mark.parametrize(["newest_first", "_direction"], [(True, "before"), (False, "after"),]) - @pytest.mark.asyncio - async def test_next_chunk_performs_correct_api_call( - self, mock_session, mock_app, newest_first, direction, ownguild_cls - ): - pag = me._GuildPaginator(mock_app, newest_first, None, mock_session) - pag._first_id = "123456" - - await pag._next_chunk() - - mock_session.get_current_user_guilds.assert_awaited_once_with(**{direction: "123456"}) - - @pytest.mark.asyncio - async def test_next_chunk_returns_None_if_no_items_returned(self, mock_session, mock_app, ownguild_cls): - pag = me._GuildPaginator(mock_app, False, None, mock_session) - mock_session.get_current_user_guilds = mock.AsyncMock(return_value=[]) - assert await pag._next_chunk() is None - - @pytest.mark.asyncio - async def test_next_chunk_updates_first_id_to_last_item(self, mock_session, mock_app, ownguild_cls): - pag = me._GuildPaginator(mock_app, False, None, mock_session) - - return_payload = [ - {"id": "1234", ...: ...}, - {"id": "3456", ...: ...}, - {"id": "3333", ...: ...}, - {"id": "512", ...: ...}, - ] - - mock_session.get_current_user_guilds = mock.AsyncMock(return_value=return_payload) - await pag._next_chunk() - assert pag._first_id == "512" - - @pytest.mark.asyncio - async def test_next_chunk_deserializes_payload_in_generator_lazily(self, mock_session, mock_app, ownguild_cls): - pag = me._GuildPaginator(mock_app, False, None, mock_session) - - return_payload = [ - {"id": "1234", ...: ...}, - {"id": "3456", ...: ...}, - {"id": "3333", ...: ...}, - {"id": "512", ...: ...}, - ] - - real_values = [ - mock.MagicMock(), - mock.MagicMock(), - mock.MagicMock(), - mock.MagicMock(), - ] - - assert len(real_values) == len(return_payload) - - ownguild_cls.deserialize = mock.MagicMock(side_effect=real_values) - - mock_session.get_current_user_guilds = mock.AsyncMock(return_value=return_payload) - generator = await pag._next_chunk() - - assert inspect.isgenerator(generator), "expected genexp result" - - # No calls, this should be lazy to be more performant for non-100-divisable limit counts. - ownguild_cls.deserialize.assert_not_called() - - for i, input_payload in enumerate(return_payload): - expected_value = real_values[i] - assert next(generator) is expected_value - ownguild_cls.deserialize.assert_called_with(input_payload, app=mock_app) - - # Clear the generator result. - # This doesn't test anything, but there is an issue with coverage not detecting generator - # exit conditions properly. This fixes something that would otherwise be marked as - # uncovered behaviour erroneously. - # https://stackoverflow.com/questions/35317757/python-unittest-branch-coverage-seems-to-miss-executed-generator-in-zip - with pytest.raises(StopIteration): - next(generator) - - assert locals()["i"] == len(return_payload) - 1, "Not iterated correctly somehow" - - -class TestRESTInviteLogic: - @pytest.fixture() - def rest_clients_impl(self): - mock_app = mock.MagicMock(application.Application) - mock_low_level_restful_client = mock.MagicMock(session.RESTSession) - - class RESTCurrentUserLogicImpl(me.RESTCurrentUserComponent): - def __init__(self): - super().__init__(mock_app, mock_low_level_restful_client) - - return RESTCurrentUserLogicImpl() - - @pytest.mark.asyncio - async def test_fetch_me(self, rest_clients_impl): - mock_user_payload = {"username": "A User", "id": "202020200202"} - mock_user_obj = mock.MagicMock(users.MyUser) - rest_clients_impl._session.get_current_user.return_value = mock_user_payload - with mock.patch.object(users.MyUser, "deserialize", return_value=mock_user_obj): - assert await rest_clients_impl.fetch_me() is mock_user_obj - rest_clients_impl._session.get_current_user.assert_called_once() - users.MyUser.deserialize.assert_called_once_with(mock_user_payload, app=rest_clients_impl._app) - - @pytest.mark.asyncio - async def test_update_me_with_optionals(self, rest_clients_impl): - mock_user_payload = {"id": "424242", "flags": "420", "discriminator": "6969"} - mock_user_obj = mock.MagicMock(users.MyUser) - rest_clients_impl._session.modify_current_user.return_value = mock_user_payload - mock_avatar_data = mock.MagicMock(bytes) - mock_avatar_obj = mock.MagicMock(files.BaseStream) - mock_avatar_obj.read = mock.AsyncMock(return_value=mock_avatar_data) - stack = contextlib.ExitStack() - stack.enter_context(mock.patch.object(users.MyUser, "deserialize", return_value=mock_user_obj)) - with stack: - assert await rest_clients_impl.update_me(username="aNewName", avatar=mock_avatar_obj) is mock_user_obj - rest_clients_impl._session.modify_current_user.assert_called_once_with( - username="aNewName", avatar=mock_avatar_data - ) - mock_avatar_obj.read.assert_awaited_once() - users.MyUser.deserialize.assert_called_once_with(mock_user_payload, app=rest_clients_impl._app) - - @pytest.mark.asyncio - async def test_update_me_without_optionals(self, rest_clients_impl): - mock_user_payload = {"id": "424242", "flags": "420", "discriminator": "6969"} - mock_user_obj = mock.MagicMock(users.MyUser) - rest_clients_impl._session.modify_current_user.return_value = mock_user_payload - with mock.patch.object(users.MyUser, "deserialize", return_value=mock_user_obj): - assert await rest_clients_impl.update_me() is mock_user_obj - rest_clients_impl._session.modify_current_user.assert_called_once_with(username=..., avatar=...) - users.MyUser.deserialize.assert_called_once_with(mock_user_payload, app=rest_clients_impl._app) - - @pytest.mark.asyncio - async def test_fetch_my_connections(self, rest_clients_impl): - mock_connection_payload = {"id": "odnkwu", "type": "twitch", "name": "eric"} - mock_connection_obj = mock.MagicMock(applications.OwnConnection) - rest_clients_impl._session.get_current_user_connections.return_value = [mock_connection_payload] - with mock.patch.object(applications.OwnConnection, "deserialize", return_value=mock_connection_obj): - assert await rest_clients_impl.fetch_my_connections() == [mock_connection_obj] - rest_clients_impl._session.get_current_user_connections.assert_called_once() - applications.OwnConnection.deserialize.assert_called_once_with( - mock_connection_payload, app=rest_clients_impl._app - ) - - @pytest.mark.parametrize("newest_first", [True, False]) - @pytest.mark.parametrize("start_at", [None, "abc", 123]) - def test_fetch_my_guilds(self, rest_clients_impl, newest_first, start_at): - with mock.patch.object(me._GuildPaginator, "__init__", return_value=None) as init: - result = rest_clients_impl.fetch_my_guilds(newest_first=newest_first, start_at=start_at) - assert isinstance(result, me._GuildPaginator) - init.assert_called_once_with( - newest_first=newest_first, - first_item=start_at, - app=rest_clients_impl._app, - session=rest_clients_impl._session, - ) - - def test_fetch_my_guilds_default_directionality(self, rest_clients_impl): - with mock.patch.object(me._GuildPaginator, "__init__", return_value=None) as init: - result = rest_clients_impl.fetch_my_guilds() - assert isinstance(result, me._GuildPaginator) - init.assert_called_once_with( - newest_first=False, first_item=None, app=rest_clients_impl._app, session=rest_clients_impl._session, - ) - - @pytest.mark.asyncio - @_helpers.parametrize_valid_id_formats_for_models("guild", 574921006817476608, guilds.Guild) - async def test_leave_guild(self, rest_clients_impl, guild): - rest_clients_impl._session.leave_guild.return_value = ... - assert await rest_clients_impl.leave_guild(guild) is None - rest_clients_impl._session.leave_guild.assert_called_once_with(guild_id="574921006817476608") - - @pytest.mark.asyncio - @_helpers.parametrize_valid_id_formats_for_models("recipient", 115590097100865541, users.User) - async def test_create_dm_channel(self, rest_clients_impl, recipient): - mock_dm_payload = {"id": "2202020", "type": 2, "recipients": []} - mock_dm_obj = mock.MagicMock(channels.DMChannel) - rest_clients_impl._session.create_dm.return_value = mock_dm_payload - with mock.patch.object(channels.DMChannel, "deserialize", return_value=mock_dm_obj): - assert await rest_clients_impl.create_dm_channel(recipient) is mock_dm_obj - rest_clients_impl._session.create_dm.assert_called_once_with(recipient_id="115590097100865541") - channels.DMChannel.deserialize.assert_called_once_with(mock_dm_payload, app=rest_clients_impl._app) diff --git a/tests/hikari/rest/test_oauth2.py b/tests/hikari/rest/test_oauth2.py deleted file mode 100644 index 68aad46a86..0000000000 --- a/tests/hikari/rest/test_oauth2.py +++ /dev/null @@ -1,51 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# Copyright © Nekoka.tt 2019-2020 -# -# This file is part of Hikari. -# -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . - -import mock -import pytest - -from hikari import application -from hikari.models import applications -from hikari.net.rest import oauth2 -from hikari.net.rest import session - - -class TestRESTReactionLogic: - @pytest.fixture() - def rest_oauth2_logic_impl(self): - mock_app = mock.MagicMock(application.Application) - mock_low_level_restful_client = mock.MagicMock(session.RESTSession) - - class RESTOauth2LogicImpl(oauth2.RESTOAuth2Component): - def __init__(self): - super().__init__(mock_app, mock_low_level_restful_client) - - return RESTOauth2LogicImpl() - - @pytest.mark.asyncio - async def test_fetch_my_application_info(self, rest_oauth2_logic_impl): - mock_application_payload = {"id": "2929292", "name": "blah blah", "description": "an application"} - mock_application_obj = mock.MagicMock(applications.Application) - rest_oauth2_logic_impl._session.get_current_application_info.return_value = mock_application_payload - with mock.patch.object(applications.Application, "deserialize", return_value=mock_application_obj): - assert await rest_oauth2_logic_impl.fetch_my_application_info() is mock_application_obj - rest_oauth2_logic_impl._session.get_current_application_info.assert_called_once_with() - applications.Application.deserialize.assert_called_once_with( - mock_application_payload, app=rest_oauth2_logic_impl._app - ) diff --git a/tests/hikari/rest/test_react.py b/tests/hikari/rest/test_react.py deleted file mode 100644 index 12ce46dc33..0000000000 --- a/tests/hikari/rest/test_react.py +++ /dev/null @@ -1,266 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# Copyright © Nekoka.tt 2019-2020 -# -# This file is part of Hikari. -# -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . -import datetime -import inspect - -import mock -import pytest - -from hikari import application -from hikari.models import bases -from hikari.models import channels -from hikari.models import emojis -from hikari.models import messages -from hikari.models import users -from hikari.net.rest import react -from hikari.net.rest import session -from tests.hikari import _helpers - - -@pytest.mark.parametrize( - "emoji", - [ - "\N{OK HAND SIGN}", - emojis.UnicodeEmoji(name="\N{OK HAND SIGN}"), - emojis.CustomEmoji(id=bases.Snowflake(9876), name="foof"), - ], - ids=lambda arg: str(arg), -) -class TestMemberPaginator: - @pytest.fixture() - def mock_session(self): - return mock.MagicMock(spec_set=session.RESTSession) - - @pytest.fixture() - def mock_app(self): - return mock.MagicMock(spec_set=application.Application) - - @pytest.fixture() - def user_cls(self): - with mock.patch.object(users, "User") as user_cls: - yield user_cls - - def test_init_no_start_bounds(self, mock_session, mock_app, emoji): - message = mock.MagicMock(__int__=lambda _: 22) - channel = mock.MagicMock(__int__=lambda _: 33) - - pag = react._ReactionPaginator(mock_app, channel, message, emoji, None, mock_session) - assert pag._app is mock_app - assert pag._first_id == "0" - assert pag._message_id == "22" - assert pag._session is mock_session - - @pytest.mark.parametrize( - ["start_at", "expected"], - [ - (None, "0"), - (53, "53"), - (bases.Unique(id=bases.Snowflake(22)), "22"), - (bases.Snowflake(22), "22"), - (datetime.datetime(2019, 1, 22, 18, 41, 15, 283000, tzinfo=datetime.timezone.utc), "537340989807788032"), - ], - ) - def test_init_with_start_bounds(self, mock_session, mock_app, start_at, expected, emoji): - message = mock.MagicMock(__int__=lambda _: 22) - channel = mock.MagicMock(__int__=lambda _: 33) - - pag = react._ReactionPaginator(mock_app, channel, message, emoji, start_at, mock_session) - assert pag._first_id == expected - assert pag._message_id == "22" - assert pag._channel_id == "33" - assert pag._app is mock_app - assert pag._session is mock_session - - @pytest.mark.asyncio - async def test_next_chunk_performs_correct_api_call(self, mock_session, mock_app, user_cls, emoji): - message = mock.MagicMock(__int__=lambda _: 44) - channel = mock.MagicMock(__int__=lambda _: 55) - - pag = react._ReactionPaginator(mock_app, channel, message, emoji, None, mock_session) - pag._first_id = "123456" - - await pag._next_chunk() - - mock_session.get_reactions.assert_awaited_once_with( - channel_id="55", message_id="44", emoji=getattr(emoji, "url_name", emoji), after="123456" - ) - - @pytest.mark.asyncio - async def test_next_chunk_when_empty_returns_None(self, mock_session, mock_app, user_cls, emoji): - mock_session.get_reactions = mock.AsyncMock(return_value=[]) - message = mock.MagicMock(__int__=lambda _: 66) - channel = mock.MagicMock(__int__=lambda _: 77) - - pag = react._ReactionPaginator(mock_app, channel, message, emoji, None, mock_session) - - assert await pag._next_chunk() is None - - @pytest.mark.asyncio - async def test_next_chunk_updates_first_id_to_last_item(self, mock_session, mock_app, user_cls, emoji): - return_payload = [ - {"id": "1234", ...: ...}, - {"id": "3456", ...: ...}, - {"id": "3333", ...: ...}, - {"id": "512", ...: ...}, - ] - - mock_session.get_reactions = mock.AsyncMock(return_value=return_payload) - - message = mock.MagicMock(__int__=lambda _: 88) - channel = mock.MagicMock(__int__=lambda _: 99) - - pag = react._ReactionPaginator(mock_app, channel, message, emoji, None, mock_session) - - await pag._next_chunk() - - assert pag._first_id == "512" - - @pytest.mark.asyncio - async def test_next_chunk_deserializes_payload_in_generator_lazily(self, mock_session, mock_app, user_cls, emoji): - message = mock.MagicMock(__int__=lambda _: 91210) - channel = mock.MagicMock(__int__=lambda _: 8008135) - - pag = react._ReactionPaginator(mock_app, channel, message, emoji, None, mock_session) - - return_payload = [ - {"id": "1234", ...: ...}, - {"id": "3456", ...: ...}, - {"id": "3333", ...: ...}, - {"id": "512", ...: ...}, - ] - - real_values = [ - mock.MagicMock(), - mock.MagicMock(), - mock.MagicMock(), - mock.MagicMock(), - ] - - assert len(real_values) == len(return_payload) - - user_cls.deserialize = mock.MagicMock(side_effect=real_values) - - mock_session.get_reactions = mock.AsyncMock(return_value=return_payload) - generator = await pag._next_chunk() - - assert inspect.isgenerator(generator), "expected genexp result" - - # No calls, this should be lazy to be more performant for non-100-divisable limit counts. - user_cls.deserialize.assert_not_called() - - for i, input_payload in enumerate(return_payload): - expected_value = real_values[i] - assert next(generator) is expected_value - user_cls.deserialize.assert_called_with(input_payload, app=mock_app) - - # Clear the generator result. - # This doesn't test anything, but there is an issue with coverage not detecting generator - # exit conditions properly. This fixes something that would otherwise be marked as - # uncovered behaviour erroneously. - # https://stackoverflow.com/questions/35317757/python-unittest-branch-coverage-seems-to-miss-executed-generator-in-zip - with pytest.raises(StopIteration): - next(generator) - - assert locals()["i"] == len(return_payload) - 1, "Not iterated correctly somehow" - - -class TestRESTReactionLogic: - @pytest.fixture() - def rest_reaction_logic_impl(self): - mock_app = mock.MagicMock(application.Application) - mock_low_level_restful_client = mock.MagicMock(session.RESTSession) - - class RESTReactionLogicImpl(react.RESTReactionComponent): - def __init__(self): - super().__init__(mock_app, mock_low_level_restful_client) - - return RESTReactionLogicImpl() - - @pytest.mark.asyncio - @_helpers.parametrize_valid_id_formats_for_models("channel", 213123, channels.PartialChannel) - @_helpers.parametrize_valid_id_formats_for_models("message", 987654321, messages.Message) - @pytest.mark.parametrize("emoji", ["blah:123", emojis.CustomEmoji(name="blah", id=123, is_animated=False)]) - async def test_create_reaction(self, rest_reaction_logic_impl, channel, message, emoji): - rest_reaction_logic_impl._session.create_reaction.return_value = ... - assert await rest_reaction_logic_impl.add_reaction(channel=channel, message=message, emoji=emoji) is None - rest_reaction_logic_impl._session.create_reaction.assert_called_once_with( - channel_id="213123", message_id="987654321", emoji="blah:123", - ) - - @pytest.mark.asyncio - @_helpers.parametrize_valid_id_formats_for_models("channel", 213123, channels.PartialChannel) - @_helpers.parametrize_valid_id_formats_for_models("message", 987654321, messages.Message) - @pytest.mark.parametrize("emoji", ["blah:123", emojis.CustomEmoji(name="blah", id=123, is_animated=False)]) - async def test_delete_reaction_for_bot_user(self, rest_reaction_logic_impl, channel, message, emoji): - rest_reaction_logic_impl._session.delete_own_reaction.return_value = ... - assert await rest_reaction_logic_impl.remove_reaction(channel=channel, message=message, emoji=emoji) is None - rest_reaction_logic_impl._session.delete_own_reaction.assert_called_once_with( - channel_id="213123", message_id="987654321", emoji="blah:123", - ) - - @pytest.mark.asyncio - @_helpers.parametrize_valid_id_formats_for_models("channel", 213123, channels.PartialChannel) - @_helpers.parametrize_valid_id_formats_for_models("message", 987654321, messages.Message) - @_helpers.parametrize_valid_id_formats_for_models("user", 96969696, users.User) - @pytest.mark.parametrize("emoji", ["blah:123", emojis.CustomEmoji(name="blah", id=123, is_animated=False)]) - async def test_delete_reaction_for_other_user(self, rest_reaction_logic_impl, channel, message, emoji, user): - rest_reaction_logic_impl._session.delete_user_reaction.return_value = ... - assert ( - await rest_reaction_logic_impl.remove_reaction(channel=channel, message=message, emoji=emoji, user=user) - is None - ) - rest_reaction_logic_impl._session.delete_user_reaction.assert_called_once_with( - channel_id="213123", message_id="987654321", emoji="blah:123", user_id="96969696", - ) - - @pytest.mark.asyncio - @_helpers.parametrize_valid_id_formats_for_models("channel", 213123, channels.PartialChannel) - @_helpers.parametrize_valid_id_formats_for_models("message", 987654321, messages.Message) - @pytest.mark.parametrize("emoji", [None, "blah:123", emojis.CustomEmoji(name="blah", id=123, is_animated=False)]) - async def test_delete_all_reactions(self, rest_reaction_logic_impl, channel, message, emoji): - rest_reaction_logic_impl._session = mock.MagicMock(spec_set=session.RESTSession) - assert ( - await rest_reaction_logic_impl.remove_all_reactions(channel=channel, message=message, emoji=emoji) is None - ) - - if emoji is None: - rest_reaction_logic_impl._session.delete_all_reactions.assert_called_once_with( - channel_id="213123", message_id="987654321", - ) - else: - rest_reaction_logic_impl._session.delete_all_reactions_for_emoji.assert_called_once_with( - channel_id="213123", message_id="987654321", emoji=getattr(emoji, "url_name", emoji) - ) - - def test_fetch_reactors(self, rest_reaction_logic_impl): - with mock.patch.object(react._ReactionPaginator, "__init__", return_value=None) as init: - paginator = rest_reaction_logic_impl.fetch_reactors( - channel=1234, message=bases.Snowflake("3456"), emoji="\N{OK HAND SIGN}", after=None - ) - - assert isinstance(paginator, react._ReactionPaginator) - - init.assert_called_once_with( - channel=1234, - message=bases.Snowflake("3456"), - users_after=None, - emoji="\N{OK HAND SIGN}", - app=rest_reaction_logic_impl._app, - session=rest_reaction_logic_impl._session, - ) diff --git a/tests/hikari/rest/test_session.py b/tests/hikari/rest/test_session.py deleted file mode 100644 index f8143d7884..0000000000 --- a/tests/hikari/rest/test_session.py +++ /dev/null @@ -1,1921 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# Copyright © Nekoka.tt 2019-2020 -# -# This file is part of Hikari. -# -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . -import asyncio -import contextlib -import datetime -import email.utils -import http -import json -import logging - -import aiohttp -import mock -import pytest - -from hikari import errors -from hikari.internal import conversions -from hikari.net import http_client -from hikari.internal import ratelimits -from hikari.models import files -from hikari.net import buckets -from hikari.net.rest import session -from hikari.net import routes -from tests.hikari import _helpers - - -class MockResponse: - def __init__(self, body=None, status=204, real_url="http://example.com", content_type=None, headers=None, **kwargs): - self.body = body - self.status = status - self.real_url = real_url - self.content_type = content_type - headers = {} if headers is None else headers - headers["content-type"] = content_type - headers.setdefault("date", email.utils.format_datetime(datetime.datetime.utcnow())) - self.headers = headers - self.__dict__.update(kwargs) - - async def read(self): - return self.body - - async def json(self): - return json.loads(await self.read()) - - -@contextlib.contextmanager -def mock_patch_route(real_route): - compiled_route = mock.MagicMock(routes.CompiledRoute) - compile = mock.Mock(spec=routes.Route.compile, spec_set=True, return_value=compiled_route) - route_template = mock.MagicMock(spec_set=routes.Route, compile=compile) - with mock.patch.object(routes, real_route, new=route_template): - yield route_template, compiled_route - - -# noinspection PyUnresolvedReferences -@pytest.mark.asyncio -class TestRESTInit: - async def test_base_url_is_formatted_correctly(self): - async with session.RESTSession( - base_url="http://example.com/api/v{0.version}/test", token=None, version=69 - ) as client: - assert client.base_url == "http://example.com/api/v69/test" - - async def test_no_token_sets_field_to_None(self): - async with session.RESTSession(token=None) as client: - assert client._token is None - - @_helpers.assert_raises(type_=RuntimeError) - async def test_bare_old_token_without_auth_scheme_raises_error(self): - async with session.RESTSession(token="1a2b.3c4d") as client: - pass - - @_helpers.assert_raises(type_=RuntimeError) - async def test_bare_old_token_without_recognised_auth_scheme_raises_error(self): - async with session.RESTSession(token="Token 1a2b.3c4d") as client: - pass - - @pytest.mark.parametrize("auth_type", ["Bot", "Bearer"]) - async def test_known_auth_type_is_allowed(self, auth_type): - token = f"{auth_type} 1a2b.3c4d" - async with session.RESTSession(token=token) as client: - assert client._token == token - - -@pytest.mark.asyncio -class TestRESTClose: - @pytest.fixture - def rest_impl(self, event_loop): - rest_impl = session.RESTSession(token="Bot token") - yield rest_impl - event_loop.run_until_complete(super(session.RESTSession, rest_impl).close()) - rest_impl.bucket_ratelimiters.close() - rest_impl.global_ratelimiter.close() - - @pytest.mark.parametrize("ratelimiter", ["bucket_ratelimiters", "global_ratelimiter"]) - async def test_close_calls_ratelimiter_close(self, rest_impl, ratelimiter): - with mock.patch.object(rest_impl, ratelimiter) as m: - await rest_impl.close() - m.close.assert_called_once_with() - - -@pytest.fixture -def compiled_route(): - template = routes.Route("POST", "/foo/{bar}/baz") - return routes.CompiledRoute(template, "/foo/bar/baz", "1a2a3b4b5c6d") - - -@pytest.mark.asyncio -class TestRESTRequestJsonResponse: - @pytest.fixture - def bucket_ratelimiters(self): - limiter = mock.MagicMock(spec_set=buckets.RESTBucketManager) - limiter.acquire = mock.MagicMock(return_value=_helpers.AwaitableMock()) - return limiter - - @pytest.fixture - def global_ratelimiter(self): - limiter = mock.MagicMock(spec_set=ratelimits.ManualRateLimiter) - limiter.acquire = mock.MagicMock(return_value=_helpers.AwaitableMock()) - return limiter - - @pytest.fixture - def rest_impl(self, bucket_ratelimiters, global_ratelimiter): - stack = contextlib.ExitStack() - stack.enter_context(mock.patch.object(http_client.HTTPClient, "__init__", new=lambda *_, **__: None)) - stack.enter_context(mock.patch.object(buckets, "RESTBucketManager", return_value=bucket_ratelimiters)) - stack.enter_context(mock.patch.object(ratelimits, "ManualRateLimiter", return_value=global_ratelimiter)) - with stack: - client = session.RESTSession(base_url="http://example.bloop.com", token="Bot blah.blah.blah") - client.logger = mock.MagicMock(spec_set=logging.Logger) - client.json_deserialize = json.loads - client.serialize = json.dumps - client._perform_request = mock.AsyncMock(spec_set=client._perform_request, return_value=MockResponse(None)) - client._handle_rate_limits_for_response = mock.AsyncMock() - return client - - async def test_bucket_ratelimiters_are_started_when_not_running(self, rest_impl, compiled_route): - # given - rest_impl.bucket_ratelimiters.is_started = False - # when - await rest_impl._request_json_response(compiled_route) - # then - rest_impl.bucket_ratelimiters.start.assert_called_once_with() - - async def test_bucket_ratelimiters_are_not_restarted_when_already_running(self, rest_impl, compiled_route): - # given - rest_impl.bucket_ratelimiters.is_started = True - # when - await rest_impl._request_json_response(compiled_route) - # then - rest_impl.bucket_ratelimiters.start.assert_not_called() - - async def test_perform_request_awaited(self, rest_impl, compiled_route): - # when - await rest_impl._request_json_response(compiled_route) - # then - rest_impl._perform_request.assert_awaited_once() - - async def test_passes_method_kwarg(self, rest_impl, compiled_route): - # when - await rest_impl._request_json_response(compiled_route) - # then - _, kwargs = rest_impl._perform_request.call_args - assert kwargs["method"] == "POST" - - async def test_passes_url_kwarg(self, rest_impl, compiled_route): - # when - await rest_impl._request_json_response(compiled_route) - # then - _, kwargs = rest_impl._perform_request.call_args - assert kwargs["url"] == "http://example.bloop.com/foo/bar/baz" - - async def test_passes_headers(self, rest_impl, compiled_route): - # given - headers = { - "X-Floofy-Floof": "ayaayayayayayaya", - "X-Cider-Preference": "Strongbow Rose", - "Correlation-ID": "128374ad-23vsvdbdbnd-123-12314145", - "Connection": "keepalive", - } - # when - await rest_impl._request_json_response(compiled_route, headers=headers) - # then - _, kwargs = rest_impl._perform_request.call_args - for k, v in headers.items(): - assert k in kwargs["headers"] - assert kwargs["headers"][k] == v - - async def test_accept_header_injected(self, rest_impl, compiled_route): - # when - await rest_impl._request_json_response(compiled_route) - # then - _, kwargs = rest_impl._perform_request.call_args - assert kwargs["headers"]["accept"] == "application/json" - - async def test_precision_header_injected(self, rest_impl, compiled_route): - # when - await rest_impl._request_json_response(compiled_route) - # then - _, kwargs = rest_impl._perform_request.call_args - assert kwargs["headers"]["x-ratelimit-precision"] == "millisecond" - - async def test_authorization_header_not_injected_if_none(self, rest_impl, compiled_route): - # given - rest_impl._token = None - # when - await rest_impl._request_json_response(compiled_route) - # then - assert "authorization" not in map(str.lower, rest_impl._perform_request.call_args[1]["headers"].keys()) - - async def test_authorization_header_injected_if_present(self, rest_impl, compiled_route): - # when - await rest_impl._request_json_response(compiled_route) - # then - _, kwargs = rest_impl._perform_request.call_args - assert kwargs["headers"]["authorization"] == rest_impl._token - - async def test_authorization_header_not_injected_if_present_but_suppress_arg_true(self, rest_impl, compiled_route): - # when - await rest_impl._request_json_response(compiled_route, suppress_authorization_header=True) - # then - assert "authorization" not in map(str.lower, rest_impl._perform_request.call_args[1]["headers"].keys()) - - async def test_auditlog_reason_header_not_injected_if_omitted(self, rest_impl, compiled_route): - # when - await rest_impl._request_json_response(compiled_route) - # then - assert "x-audit-log-reason" not in map(str.lower, rest_impl._perform_request.call_args[1]["headers"].keys()) - - async def test_auditlog_reason_header_not_injected_if_omitted(self, rest_impl, compiled_route): - # when - await rest_impl._request_json_response(compiled_route, reason="he was evil") - # then - headers = rest_impl._perform_request.call_args[1]["headers"] - assert headers["x-audit-log-reason"] == "he was evil" - - async def test_waits_for_rate_limits_before_requesting(self, rest_impl, compiled_route): - await_ratelimiter = object() - await_request = object() - - order = [] - - def on_gather(*_, **__): - order.append(await_ratelimiter) - - def on_request(*_, **__): - order.append(await_request) - return MockResponse() - - rest_impl._perform_request = mock.AsyncMock(wraps=on_request) - - with mock.patch.object(asyncio, "gather", new=mock.AsyncMock(wraps=on_gather)) as gather: - await rest_impl._request_json_response(compiled_route) - - rest_impl.bucket_ratelimiters.acquire.assert_called_once_with(compiled_route) - rest_impl.global_ratelimiter.acquire.assert_called_once_with() - - assert order == [await_ratelimiter, await_request] - - gather.assert_awaited_once_with( - rest_impl.bucket_ratelimiters.acquire(compiled_route), rest_impl.global_ratelimiter.acquire(), - ) - - async def test_response_ratelimits_considered(self, rest_impl, compiled_route): - response = MockResponse() - rest_impl._perform_request = mock.AsyncMock(return_value=response) - - await rest_impl._request_json_response(compiled_route) - - rest_impl._handle_rate_limits_for_response.assert_awaited_once_with(compiled_route, response) - - async def test_204_returns_None(self, rest_impl, compiled_route): - response = MockResponse(status=204, body="this is most certainly not None but it shouldn't be considered") - rest_impl._perform_request = mock.AsyncMock(return_value=response) - - assert await rest_impl._request_json_response(compiled_route) is None - - @pytest.mark.parametrize("status", [200, 201, 202, 203]) - async def test_2xx_returns_json_body_if_json_type(self, rest_impl, compiled_route, status): - response = MockResponse(status=status, body='{"foo": "bar"}', content_type="application/json") - rest_impl._perform_request = mock.AsyncMock(return_value=response) - - assert await rest_impl._request_json_response(compiled_route) == {"foo": "bar"} - - @pytest.mark.parametrize("status", [200, 201, 202, 203]) - @_helpers.assert_raises(type_=errors.HTTPError) - async def test_2xx_raises_http_error_if_unexpected_content_type(self, rest_impl, compiled_route, status): - response = MockResponse(status=status, body='{"foo": "bar"}', content_type="application/foobar") - rest_impl._perform_request = mock.AsyncMock(return_value=response) - - await rest_impl._request_json_response(compiled_route) - - @pytest.mark.parametrize( - ["status", "expected_exception_type"], - [ - (100, errors.HTTPErrorResponse), - (304, errors.HTTPErrorResponse), - (400, errors.BadRequest), - (401, errors.Unauthorized), - (403, errors.Forbidden), - (404, errors.NotFound), - (406, errors.ClientHTTPErrorResponse), - (408, errors.ClientHTTPErrorResponse), - (415, errors.ClientHTTPErrorResponse), - (500, errors.ServerHTTPErrorResponse), - (501, errors.ServerHTTPErrorResponse), - (502, errors.ServerHTTPErrorResponse), - (503, errors.ServerHTTPErrorResponse), - (504, errors.ServerHTTPErrorResponse), - ], - ) - async def test_error_responses_raises_error(self, rest_impl, compiled_route, status, expected_exception_type): - response = MockResponse(status=status, body="this is most certainly not None but it shouldn't be considered") - rest_impl._perform_request = mock.AsyncMock(return_value=response) - - try: - await rest_impl._request_json_response(compiled_route) - assert False - except expected_exception_type as ex: - assert ex.headers is response.headers - assert ex.status == response.status - assert isinstance(ex.status, http.HTTPStatus) - assert ex.raw_body is response.body - - async def test_ratelimited_429_retries_request_until_it_works(self, compiled_route, rest_impl): - # given - response = MockResponse() - rest_impl._handle_rate_limits_for_response = mock.AsyncMock( - # In reality, the ratelimiting logic will ensure we wait before retrying, but this - # is a test for a spammy edge-case scenario. - side_effect=[session._RateLimited, session._RateLimited, session._RateLimited, None] - ) - # when - await rest_impl._request_json_response(compiled_route) - # then - assert len(rest_impl._perform_request.call_args_list) == 4, rest_impl._perform_request.call_args_list - for args, kwargs in rest_impl._perform_request.call_args_list: - assert kwargs == { - "method": "POST", - "url": "http://example.bloop.com/foo/bar/baz", - "headers": mock.ANY, - "body": mock.ANY, - "query": mock.ANY, - } - - -@pytest.mark.asyncio -class TestHandleRateLimitsForResponse: - @pytest.fixture - def bucket_ratelimiters(self): - limiter = mock.MagicMock(spec_set=buckets.RESTBucketManager) - limiter.update_rate_limits = mock.MagicMock() - return limiter - - @pytest.fixture - def global_ratelimiter(self): - limiter = mock.MagicMock(spec_set=ratelimits.ManualRateLimiter) - limiter.throttle = mock.MagicMock() - return limiter - - @pytest.fixture - def rest_impl(self, bucket_ratelimiters, global_ratelimiter): - stack = contextlib.ExitStack() - stack.enter_context(mock.patch.object(http_client.HTTPClient, "__init__", new=lambda *_, **__: None)) - stack.enter_context(mock.patch.object(buckets, "RESTBucketManager", return_value=bucket_ratelimiters)) - stack.enter_context(mock.patch.object(ratelimits, "ManualRateLimiter", return_value=global_ratelimiter)) - with stack: - client = session.RESTSession(base_url="http://example.bloop.com", token="Bot blah.blah.blah") - client.logger = mock.MagicMock(spec_set=logging.Logger) - return client - - @pytest.mark.parametrize("status", [200, 201, 202, 203, 204, 400, 401, 403, 404, 429, 500]) - @pytest.mark.parametrize("content_type", ["application/json", "text/x-yaml", None]) - async def test_bucket_ratelimiter_updated( - self, bucket_ratelimiters, rest_impl, compiled_route, status, content_type - ): - response = MockResponse( - headers={ - "x-ratelimit-limit": "15", - "x-ratelimit-remaining": "3", - "x-ratelimit-bucket": "foobar", - "date": "Fri, 01 May 2020 10:23:54 GMT", - "x-ratelimit-reset": "1588334400", - }, - status=status, - content_type=content_type, - ) - - # We don't care about the result, as some cases throw exceptions purposely. We just want - # to invoke it and check a call is made before it returns. This ensures 429s still take - # into account the headers first. - with contextlib.suppress(Exception): - await rest_impl._handle_rate_limits_for_response(compiled_route, response) - - bucket_ratelimiters.update_rate_limits.assert_called_once_with( - compiled_route=compiled_route, - bucket_header="foobar", - remaining_header=3, - limit_header=15, - date_header=datetime.datetime(2020, 5, 1, 10, 23, 54, tzinfo=datetime.timezone.utc), - reset_at_header=datetime.datetime(2020, 5, 1, 12, tzinfo=datetime.timezone.utc), - ) - - @pytest.mark.parametrize("body", [b"{}", b'{"global": false}']) - @_helpers.assert_raises(type_=session._RateLimited) - async def test_non_global_429_raises_Ratelimited(self, rest_impl, compiled_route, body): - response = MockResponse( - headers={ - "x-ratelimit-limit": "15", - "x-ratelimit-remaining": "3", - "x-ratelimit-bucket": "foobar", - "date": "Fri, 01 May 2020 10:23:54 GMT", - "x-ratelimit-reset": "1588334400", - }, - status=429, - content_type="application/json", - body=body, - ) - - await rest_impl._handle_rate_limits_for_response(compiled_route, response) - - async def test_global_429_throttles_then_raises_Ratelimited(self, global_ratelimiter, rest_impl, compiled_route): - response = MockResponse( - headers={ - "x-ratelimit-limit": "15", - "x-ratelimit-remaining": "3", - "x-ratelimit-bucket": "foobar", - "date": "Fri, 01 May 2020 10:23:54 GMT", - "x-ratelimit-reset": "1588334400", - }, - status=429, - content_type="application/json", - body=b'{"global": true, "retry_after": 1024768}', - ) - - try: - await rest_impl._handle_rate_limits_for_response(compiled_route, response) - assert False - except session._RateLimited: - global_ratelimiter.throttle.assert_called_once_with(1024.768) - - async def test_non_json_429_causes_httperror(self, rest_impl, compiled_route): - response = MockResponse( - headers={ - "x-ratelimit-limit": "15", - "x-ratelimit-remaining": "3", - "x-ratelimit-bucket": "foobar", - "date": "Fri, 01 May 2020 10:23:54 GMT", - "x-ratelimit-reset": "1588334400", - }, - status=429, - content_type="text/x-markdown", - body=b'{"global": true, "retry_after": 1024768}', - real_url="http://foo-bar.com/api/v169/ree", - ) - - try: - await rest_impl._handle_rate_limits_for_response(compiled_route, response) - assert False - except errors.HTTPError as ex: - # We don't want a subclass, as this is an edge case. - assert type(ex) is errors.HTTPError - assert ex.url == "http://foo-bar.com/api/v169/ree" - - -class TestRESTEndpoints: - @pytest.fixture - def rest_impl(self): - stack = contextlib.ExitStack() - stack.enter_context(mock.patch.object(http_client.HTTPClient, "__init__", new=lambda *_, **__: None)) - stack.enter_context(mock.patch.object(buckets, "RESTBucketManager")) - stack.enter_context(mock.patch.object(ratelimits, "ManualRateLimiter")) - with stack: - client = session.RESTSession(base_url="https://discord.com/api/v6", token="Bot blah.blah.blah") - client.logger = mock.MagicMock(spec_set=logging.Logger) - client._request_json_response = mock.AsyncMock(return_value=...) - client.client_session = mock.MagicMock(aiohttp.ClientSession, spec_set=True) - - return client - - @pytest.mark.asyncio - async def test_get_gateway(self, rest_impl): - rest_impl._request_json_response.return_value = {"url": "discord.discord///"} - with mock_patch_route("GET_GATEWAY") as (template, compiled): - assert await rest_impl.get_gateway() == "discord.discord///" - template.compile.assert_called_once_with() - rest_impl._request_json_response.assert_awaited_once_with(compiled) - - @pytest.mark.asyncio - async def test_get_gateway_bot(self, rest_impl): - mock_response = {"url": "discord.discord///"} - rest_impl._request_json_response.return_value = mock_response - with mock_patch_route("GET_GATEWAY_BOT") as (template, compiled): - assert await rest_impl.get_gateway_bot() is mock_response - template.compile.assert_called_once_with() - rest_impl._request_json_response.assert_awaited_once_with(compiled) - - @pytest.mark.asyncio - async def test_get_guild_audit_log_without_optionals(self, rest_impl): - mock_response = {"webhooks": [], "users": []} - rest_impl._request_json_response.return_value = mock_response - with mock_patch_route("GET_GUILD_AUDIT_LOGS") as (template, compiled): - assert await rest_impl.get_guild_audit_log("2929292929") is mock_response - template.compile.assert_called_once_with(guild_id="2929292929") - rest_impl._request_json_response.assert_awaited_once_with(compiled, query={}) - - @pytest.mark.asyncio - async def test_get_guild_audit_log_with_optionals(self, rest_impl): - mock_response = {"webhooks": [], "users": []} - rest_impl._request_json_response.return_value = mock_response - with mock_patch_route("GET_GUILD_AUDIT_LOGS") as (template, compiled): - assert ( - await rest_impl.get_guild_audit_log( - "2929292929", user_id="115590097100865541", action_type=42, limit=5, before="123123123" - ) - is mock_response - ) - template.compile.assert_called_once_with(guild_id="2929292929") - rest_impl._request_json_response.assert_awaited_once_with( - compiled, query={"user_id": "115590097100865541", "action_type": 42, "limit": 5, "before": "123123123"} - ) - - @pytest.mark.asyncio - async def test_get_channel(self, rest_impl): - mock_response = {"id": "20202020200202"} - rest_impl._request_json_response.return_value = mock_response - with mock_patch_route("GET_CHANNEL") as (template, compiled): - assert await rest_impl.get_channel("20202020020202") is mock_response - template.compile.assert_called_once_with(channel_id="20202020020202") - rest_impl._request_json_response.assert_awaited_once_with(compiled) - - @pytest.mark.asyncio - async def test_modify_channel_without_optionals(self, rest_impl): - mock_response = {"id": "20393939"} - rest_impl._request_json_response.return_value = mock_response - with mock_patch_route("PATCH_CHANNEL") as (template, compiled): - assert await rest_impl.modify_channel("6942069420") is mock_response - template.compile.assert_called_once_with(channel_id="6942069420") - rest_impl._request_json_response.assert_awaited_once_with(compiled, body={}, reason=...) - - @pytest.mark.asyncio - async def test_modify_channel_with_optionals(self, rest_impl): - mock_response = {"id": "20393939"} - rest_impl._request_json_response.return_value = mock_response - with mock_patch_route("PATCH_CHANNEL") as (template, compiled): - result = await rest_impl.modify_channel( - "6942069420", - position=22, - topic="HAHAHAHHAHAHA", - nsfw=True, - rate_limit_per_user=222, - bitrate=320, - user_limit=5, - permission_overwrites=[{"type": "user", "allow": 33}], - parent_id="55555", - reason="Get channel'ed", - ) - assert result is mock_response - template.compile.assert_called_once_with(channel_id="6942069420") - rest_impl._request_json_response.assert_awaited_once_with( - compiled, - body={ - "position": 22, - "topic": "HAHAHAHHAHAHA", - "nsfw": True, - "rate_limit_per_user": 222, - "bitrate": 320, - "user_limit": 5, - "permission_overwrites": [{"type": "user", "allow": 33}], - "parent_id": "55555", - }, - reason="Get channel'ed", - ) - - @pytest.mark.asyncio - async def test_delete_channel_close(self, rest_impl): - mock_route = mock.MagicMock(routes.DELETE_CHANNEL) - with mock_patch_route("DELETE_CHANNEL") as (template, compiled): - assert await rest_impl.delete_close_channel("939392929") is None - template.compile.assert_called_once_with(channel_id="939392929") - rest_impl._request_json_response.assert_awaited_once_with(compiled) - - @pytest.mark.asyncio - async def test_get_channel_messages_without_optionals(self, rest_impl): - mock_response = [{"id": "29492", "content": "Kon'nichiwa"}] - rest_impl._request_json_response.return_value = mock_response - with mock_patch_route("GET_CHANNEL_MESSAGES") as (template, compiled): - assert await rest_impl.get_channel_messages("9292929292") is mock_response - template.compile.assert_called_once_with(channel_id="9292929292") - rest_impl._request_json_response.assert_awaited_once_with(compiled, query={}) - - @pytest.mark.asyncio - async def test_get_channel_messages_with_optionals(self, rest_impl): - mock_response = [{"id": "29492", "content": "Kon'nichiwa"}] - rest_impl._request_json_response.return_value = mock_response - with mock_patch_route("GET_CHANNEL_MESSAGES") as (template, compiled): - assert ( - await rest_impl.get_channel_messages( - "9292929292", limit=42, after="293939393", before="4945959595", around="44444444", - ) - is mock_response - ) - template.compile.assert_called_once_with(channel_id="9292929292") - rest_impl._request_json_response.assert_awaited_once_with( - compiled, query={"limit": 42, "after": "293939393", "before": "4945959595", "around": "44444444",} - ) - - @pytest.mark.asyncio - async def test_get_channel_message(self, rest_impl): - mock_response = {"content": "I'm really into networking with cute routers and modems."} - rest_impl._request_json_response.return_value = mock_response - with mock_patch_route("GET_CHANNEL_MESSAGE") as (template, compiled): - assert await rest_impl.get_channel_message("1111111111", "42424242") is mock_response - template.compile.assert_called_once_with(channel_id="1111111111", message_id="42424242") - rest_impl._request_json_response.assert_awaited_once_with(compiled) - - @pytest.mark.asyncio - async def test_create_message_without_optionals(self, rest_impl): - mock_response = {"content": "nyaa, nyaa, nyaa."} - rest_impl._request_json_response.return_value = mock_response - mock_form = mock.MagicMock(aiohttp.FormData, add_field=mock.MagicMock()) - with mock_patch_route("POST_CHANNEL_MESSAGES") as (template, compiled): - with mock.patch.object(aiohttp, "FormData", return_value=mock_form): - assert await rest_impl.create_message("22222222") is mock_response - template.compile.assert_called_once_with(channel_id="22222222") - mock_form.add_field.assert_called_once_with( - "payload_json", json.dumps({}), content_type="application/json" - ) - rest_impl._request_json_response.assert_awaited_once_with(compiled, body=mock_form) - - @pytest.mark.asyncio - @mock.patch.object(routes, "POST_CHANNEL_MESSAGES") - @mock.patch.object(aiohttp, "FormData") - @mock.patch.object(json, "dumps") - async def test_create_message_with_optionals(self, dumps, FormData, POST_CHANNEL_MESSAGES, rest_impl): - mock_response = {"content": "nyaa, nyaa, nyaa."} - rest_impl._request_json_response.return_value = mock_response - - mock_form = mock.MagicMock(aiohttp.FormData, add_field=mock.MagicMock()) - FormData.return_value = mock_form - mock_file = mock.MagicMock(files.BaseStream) - mock_file.filename = "file.txt" - mock_json = '{"description": "I am a message", "tts": "True"}' - dumps.return_value = mock_json - - with mock_patch_route("POST_CHANNEL_MESSAGES") as (template, compiled): - result = await rest_impl.create_message( - "22222222", - content="I am a message", - nonce="ag993okskm_cdolsio", - tts=True, - files=[mock_file], - embed={"description": "I am an embed"}, - allowed_mentions={"users": ["123"], "roles": ["456"]}, - ) - assert result is mock_response - template.compile.assert_called_once_with(channel_id="22222222") - dumps.assert_called_once_with( - { - "tts": True, - "content": "I am a message", - "nonce": "ag993okskm_cdolsio", - "embed": {"description": "I am an embed"}, - "allowed_mentions": {"users": ["123"], "roles": ["456"]}, - } - ) - - mock_form.add_field.assert_has_calls( - ( - mock.call("payload_json", mock_json, content_type="application/json"), - mock.call("file0", mock_file, filename="file.txt", content_type="application/octet-stream"), - ), - any_order=True, - ) - assert mock_form.add_field.call_count == 2 - rest_impl._request_json_response.assert_awaited_once_with(compiled, body=mock_form) - - @pytest.mark.asyncio - async def test_create_reaction(self, rest_impl): - with mock_patch_route("PUT_MY_REACTION") as (template, compiled): - assert await rest_impl.create_reaction("20202020", "8484848", "emoji:2929") is None - template.compile.assert_called_once_with(channel_id="20202020", message_id="8484848", emoji="emoji:2929") - rest_impl._request_json_response.assert_awaited_once_with(compiled) - - @pytest.mark.asyncio - async def test_delete_own_reaction(self, rest_impl): - with mock_patch_route("DELETE_MY_REACTION") as (template, compiled): - assert await rest_impl.delete_own_reaction("20202020", "8484848", "emoji:2929") is None - template.compile.assert_called_once_with(channel_id="20202020", message_id="8484848", emoji="emoji:2929") - rest_impl._request_json_response.assert_awaited_once_with(compiled) - - @pytest.mark.asyncio - async def test_delete_all_reactions_for_emoji(self, rest_impl): - with mock_patch_route("DELETE_REACTION_EMOJI") as (template, compiled): - assert await rest_impl.delete_all_reactions_for_emoji("222", "333", "222:owo") is None - template.compile.assert_called_once_with(channel_id="222", message_id="333", emoji="222:owo") - rest_impl._request_json_response.assert_awaited_once_with(compiled) - - @pytest.mark.asyncio - async def test_delete_user_reaction(self, rest_impl): - with mock_patch_route("DELETE_REACTION_USER") as (template, compiled): - assert await rest_impl.delete_user_reaction("11111", "4444", "emoji:42", "29292992") is None - template.compile.assert_called_once_with( - channel_id="11111", message_id="4444", emoji="emoji:42", user_id="29292992" - ) - rest_impl._request_json_response.assert_awaited_once_with(compiled) - - @pytest.mark.asyncio - async def test_get_reactions_without_optionals(self, rest_impl): - mock_response = [{"id": "42"}] - rest_impl._request_json_response.return_value = mock_response - with mock_patch_route("GET_REACTIONS") as (template, compiled): - assert await rest_impl.get_reactions("29292929", "48484848", "emoji:42") is mock_response - template.compile.assert_called_once_with(channel_id="29292929", message_id="48484848", emoji="emoji:42") - rest_impl._request_json_response.assert_awaited_once_with(compiled, query={}) - - @pytest.mark.asyncio - async def test_get_reactions_with_optionals(self, rest_impl): - mock_response = [{"id": "42"}] - rest_impl._request_json_response.return_value = mock_response - with mock_patch_route("GET_REACTIONS") as (template, compiled): - assert ( - await rest_impl.get_reactions("29292929", "48484848", "emoji:42", after="3333333", limit=40) - is mock_response - ) - template.compile.assert_called_once_with(channel_id="29292929", message_id="48484848", emoji="emoji:42") - rest_impl._request_json_response.assert_awaited_once_with(compiled, query={"after": "3333333", "limit": 40}) - - @pytest.mark.asyncio - async def test_delete_all_reactions(self, rest_impl): - with mock_patch_route("DELETE_ALL_REACTIONS") as (template, compiled): - assert await rest_impl.delete_all_reactions("44444", "999999") is None - template.compile.assert_called_once_with(channel_id="44444", message_id="999999") - rest_impl._request_json_response.assert_awaited_once_with(compiled) - - @pytest.mark.asyncio - async def test_edit_message_without_optionals(self, rest_impl): - mock_response = {"flags": 3, "content": "edited for the win."} - rest_impl._request_json_response.return_value = mock_response - with mock_patch_route("PATCH_CHANNEL_MESSAGE") as (template, compiled): - assert await rest_impl.edit_message("9292929", "484848") is mock_response - template.compile.assert_called_once_with(channel_id="9292929", message_id="484848") - rest_impl._request_json_response.assert_awaited_once_with(compiled, body={}) - - @pytest.mark.asyncio - async def test_edit_message_with_optionals(self, rest_impl): - mock_response = {"flags": 3, "content": "edited for the win."} - rest_impl._request_json_response.return_value = mock_response - with mock_patch_route("PATCH_CHANNEL_MESSAGE") as (template, compiled): - assert ( - await rest_impl.edit_message( - "9292929", - "484848", - content="42", - embed={"content": "I AM AN EMBED"}, - flags=2, - allowed_mentions={"parse": ["everyone", "users"]}, - ) - is mock_response - ) - template.compile.assert_called_once_with(channel_id="9292929", message_id="484848") - rest_impl._request_json_response.assert_awaited_once_with( - compiled, - body={ - "content": "42", - "embed": {"content": "I AM AN EMBED"}, - "flags": 2, - "allowed_mentions": {"parse": ["everyone", "users"]}, - }, - ) - - @pytest.mark.asyncio - async def test_delete_message(self, rest_impl): - with mock_patch_route("DELETE_CHANNEL_MESSAGE") as (template, compiled): - assert await rest_impl.delete_message("20202", "484848") is None - template.compile.assert_called_once_with(channel_id="20202", message_id="484848") - rest_impl._request_json_response.assert_awaited_once_with(compiled) - - @pytest.mark.asyncio - async def test_bulk_delete_messages(self, rest_impl): - with mock_patch_route("POST_DELETE_CHANNEL_MESSAGES_BULK") as (template, compiled): - assert await rest_impl.bulk_delete_messages("111", ["222", "333"]) is None - template.compile.assert_called_once_with(channel_id="111") - rest_impl._request_json_response.assert_awaited_once_with(compiled, body={"messages": ["222", "333"]}) - - @pytest.mark.asyncio - async def test_edit_channel_permissions_without_optionals(self, rest_impl): - with mock_patch_route("PATCH_CHANNEL_PERMISSIONS") as (template, compiled): - assert await rest_impl.edit_channel_permissions("101010101010", "100101010", type_="user") is None - template.compile.assert_called_once_with(channel_id="101010101010", overwrite_id="100101010") - rest_impl._request_json_response.assert_awaited_once_with(compiled, body={"type": "user"}, reason=...) - - @pytest.mark.asyncio - async def test_edit_channel_permissions_with_optionals(self, rest_impl): - with mock_patch_route("PATCH_CHANNEL_PERMISSIONS") as (template, compiled): - assert ( - await rest_impl.edit_channel_permissions( - "101010101010", "100101010", allow=243, deny=333, type_="user", reason="get vectored" - ) - is None - ) - template.compile.assert_called_once_with(channel_id="101010101010", overwrite_id="100101010") - rest_impl._request_json_response.assert_awaited_once_with( - compiled, body={"allow": 243, "deny": 333, "type": "user"}, reason="get vectored" - ) - - @pytest.mark.asyncio - async def test_get_channel_invites(self, rest_impl): - mock_response = {"code": "dasd32"} - rest_impl._request_json_response.return_value = mock_response - with mock_patch_route("GET_CHANNEL_INVITES") as (template, compiled): - assert await rest_impl.get_channel_invites("999999999") is mock_response - template.compile.assert_called_once_with(channel_id="999999999") - rest_impl._request_json_response.assert_awaited_once_with(compiled) - - @pytest.mark.asyncio - async def test_create_channel_invite_without_optionals(self, rest_impl): - mock_response = {"code": "ro934jsd"} - rest_impl._request_json_response.return_value = mock_response - with mock_patch_route("POST_CHANNEL_INVITES") as (template, compiled): - assert await rest_impl.create_channel_invite("99992929") is mock_response - template.compile.assert_called_once_with(channel_id="99992929") - rest_impl._request_json_response.assert_awaited_once_with(compiled, body={}, reason=...) - - @pytest.mark.asyncio - async def test_create_channel_invite_with_optionals(self, rest_impl): - mock_response = {"code": "ro934jsd"} - rest_impl._request_json_response.return_value = mock_response - with mock_patch_route("POST_CHANNEL_INVITES") as (template, compiled): - assert ( - await rest_impl.create_channel_invite( - "99992929", - max_age=5, - max_uses=7, - temporary=True, - unique=False, - target_user="29292929292", - target_user_type=2, - reason="XD", - ) - is mock_response - ) - template.compile.assert_called_once_with(channel_id="99992929") - rest_impl._request_json_response.assert_awaited_once_with( - compiled, - body={ - "max_age": 5, - "max_uses": 7, - "temporary": True, - "unique": False, - "target_user": "29292929292", - "target_user_type": 2, - }, - reason="XD", - ) - - @pytest.mark.asyncio - async def test_delete_channel_permission(self, rest_impl): - with mock_patch_route("DELETE_CHANNEL_PERMISSIONS") as (template, compiled): - assert await rest_impl.delete_channel_permission("9292929", "74574747") is None - template.compile.assert_called_once_with(channel_id="9292929", overwrite_id="74574747") - rest_impl._request_json_response.assert_awaited_once_with(compiled) - - @pytest.mark.asyncio - async def test_trigger_typing_indicator(self, rest_impl): - with mock_patch_route("POST_CHANNEL_TYPING") as (template, compiled): - assert await rest_impl.trigger_typing_indicator("11111111111") is None - template.compile.assert_called_once_with(channel_id="11111111111") - rest_impl._request_json_response.assert_awaited_once_with(compiled) - - @pytest.mark.asyncio - async def test_get_pinned_messages(self, rest_impl): - mock_response = [{"content": "no u", "id": "4212"}] - rest_impl._request_json_response.return_value = mock_response - with mock_patch_route("GET_CHANNEL_PINS") as (template, compiled): - assert await rest_impl.get_pinned_messages("393939") is mock_response - template.compile.assert_called_once_with(channel_id="393939") - rest_impl._request_json_response.assert_awaited_once_with(compiled) - - @pytest.mark.asyncio - async def test_add_pinned_channel_message(self, rest_impl): - with mock_patch_route("PUT_CHANNEL_PINS") as (template, compiled): - assert await rest_impl.add_pinned_channel_message("292929", "48458484") is None - template.compile.assert_called_once_with(channel_id="292929", message_id="48458484") - rest_impl._request_json_response.assert_awaited_once_with(compiled) - - @pytest.mark.asyncio - async def test_delete_pinned_channel_message(self, rest_impl): - with mock_patch_route("DELETE_CHANNEL_PIN") as (template, compiled): - assert await rest_impl.delete_pinned_channel_message("929292", "292929") is None - template.compile.assert_called_once_with(channel_id="929292", message_id="292929") - rest_impl._request_json_response.assert_awaited_once_with(compiled) - - @pytest.mark.asyncio - async def test_list_guild_emojis(self, rest_impl): - mock_response = [{"id": "444", "name": "nekonyan"}] - rest_impl._request_json_response.return_value = mock_response - with mock_patch_route("GET_GUILD_EMOJIS") as (template, compiled): - assert await rest_impl.list_guild_emojis("9929292") is mock_response - template.compile.assert_called_once_with(guild_id="9929292") - rest_impl._request_json_response.assert_awaited_once_with(compiled) - - @pytest.mark.asyncio - async def test_get_guild_emoji(self, rest_impl): - mock_response = {"id": "444", "name": "nekonyan"} - rest_impl._request_json_response.return_value = mock_response - with mock_patch_route("GET_GUILD_EMOJI") as (template, compiled): - assert await rest_impl.get_guild_emoji("292929", "44848") is mock_response - template.compile.assert_called_once_with(guild_id="292929", emoji_id="44848") - rest_impl._request_json_response.assert_awaited_once_with(compiled) - - @pytest.mark.asyncio - async def test_create_guild_emoji_without_optionals(self, rest_impl): - mock_response = {"id": "33", "name": "OwO"} - rest_impl._request_json_response.return_value = mock_response - mock_image_data = "data:image/png;base64,iVBORw0KGgpibGFo" - with mock_patch_route("POST_GUILD_EMOJIS") as (template, compiled): - with mock.patch.object(conversions, "image_bytes_to_image_data", return_value=mock_image_data): - result = await rest_impl.create_guild_emoji("2222", "iEmoji", b"\211PNG\r\n\032\nblah") - assert result is mock_response - conversions.image_bytes_to_image_data.assert_called_once_with(b"\211PNG\r\n\032\nblah") - template.compile.assert_called_once_with(guild_id="2222") - rest_impl._request_json_response.assert_awaited_once_with( - compiled, body={"name": "iEmoji", "roles": [], "image": mock_image_data}, reason=..., - ) - - @pytest.mark.asyncio - async def test_create_guild_emoji_with_optionals(self, rest_impl): - mock_response = {"id": "33", "name": "OwO"} - rest_impl._request_json_response.return_value = mock_response - mock_image_data = "data:image/png;base64,iVBORw0KGgpibGFo" - with mock_patch_route("POST_GUILD_EMOJIS") as (template, compiled): - with mock.patch.object(conversions, "image_bytes_to_image_data", return_value=mock_image_data): - result = await rest_impl.create_guild_emoji( - "2222", "iEmoji", b"\211PNG\r\n\032\nblah", roles=["292929", "484884"], reason="uwu owo" - ) - assert result is mock_response - conversions.image_bytes_to_image_data.assert_called_once_with(b"\211PNG\r\n\032\nblah") - template.compile.assert_called_once_with(guild_id="2222") - rest_impl._request_json_response.assert_awaited_once_with( - compiled, - body={"name": "iEmoji", "roles": ["292929", "484884"], "image": mock_image_data}, - reason="uwu owo", - ) - - @pytest.mark.asyncio - async def test_modify_guild_emoji_without_optionals(self, rest_impl): - mock_response = {"id": "20202", "name": "jeje"} - rest_impl._request_json_response.return_value = mock_response - with mock_patch_route("PATCH_GUILD_EMOJI") as (template, compiled): - assert await rest_impl.modify_guild_emoji("292929", "3484848") is mock_response - template.compile.assert_called_once_with(guild_id="292929", emoji_id="3484848") - rest_impl._request_json_response.assert_awaited_once_with(compiled, body={}, reason=...) - - @pytest.mark.asyncio - async def test_modify_guild_emoji_with_optionals(self, rest_impl): - mock_response = {"id": "20202", "name": "jeje"} - rest_impl._request_json_response.return_value = mock_response - with mock_patch_route("PATCH_GUILD_EMOJI") as (template, compiled): - assert ( - await rest_impl.modify_guild_emoji("292929", "3484848", name="ok", roles=["222", "111"]) - is mock_response - ) - template.compile.assert_called_once_with(guild_id="292929", emoji_id="3484848") - rest_impl._request_json_response.assert_awaited_once_with( - compiled, body={"name": "ok", "roles": ["222", "111"]}, reason=... - ) - - @pytest.mark.asyncio - async def test_delete_guild_emoji(self, rest_impl): - with mock_patch_route("DELETE_GUILD_EMOJI") as (template, compiled): - assert await rest_impl.delete_guild_emoji("202", "4454") is None - template.compile.assert_called_once_with(guild_id="202", emoji_id="4454") - rest_impl._request_json_response.assert_awaited_once_with(compiled) - - @pytest.mark.asyncio - async def test_create_guild_without_optionals(self, rest_impl): - mock_response = {"id": "99999", "name": "Guildith-Sama"} - rest_impl._request_json_response.return_value = mock_response - with mock_patch_route("POST_GUILDS") as (template, compiled): - assert await rest_impl.create_guild("GUILD TIME") is mock_response - template.compile.assert_called_once_with() - rest_impl._request_json_response.assert_awaited_once_with(compiled, body={"name": "GUILD TIME"}) - - @pytest.mark.asyncio - async def test_create_guild_with_optionals(self, rest_impl): - mock_response = {"id": "99999", "name": "Guildith-Sama"} - rest_impl._request_json_response.return_value = mock_response - mock_image_data = "data:image/png;base64,iVBORw0KGgpibGFo" - with mock_patch_route("POST_GUILDS") as (template, compiled): - with mock.patch.object(conversions, "image_bytes_to_image_data", return_value=mock_image_data): - result = await rest_impl.create_guild( - "GUILD TIME", - region="london", - icon=b"\211PNG\r\n\032\nblah", - verification_level=2, - explicit_content_filter=1, - roles=[{"name": "a role"}], - channels=[{"type": 0, "name": "444"}], - ) - assert result is mock_response - template.compile.assert_called_once_with() - conversions.image_bytes_to_image_data.assert_called_once_with(b"\211PNG\r\n\032\nblah") - rest_impl._request_json_response.assert_awaited_once_with( - compiled, - body={ - "name": "GUILD TIME", - "region": "london", - "icon": mock_image_data, - "verification_level": 2, - "explicit_content_filter": 1, - "roles": [{"name": "a role"}], - "channels": [{"type": 0, "name": "444"}], - }, - ) - - @pytest.mark.asyncio - @pytest.mark.parametrize( - ("kwargs", "with_counts"), - [({"with_counts": True}, "true"), ({"with_counts": False}, "false"), ({}, "true"),], # default value only - ) - async def test_get_guild(self, rest_impl, kwargs, with_counts): - mock_response = {"id": "42", "name": "Hikari"} - rest_impl._request_json_response.return_value = mock_response - with mock_patch_route("GET_GUILD") as (template, compiled): - assert await rest_impl.get_guild("3939393993939", **kwargs) is mock_response - template.compile.assert_called_once_with(guild_id="3939393993939") - rest_impl._request_json_response.assert_awaited_once_with(compiled, query={"with_counts": with_counts}) - - @pytest.mark.asyncio - async def test_get_guild_preview(self, rest_impl): - mock_response = {"id": "42", "name": "Hikari"} - rest_impl._request_json_response.return_value = mock_response - with mock_patch_route("GET_GUILD_PREVIEW") as (template, compiled): - assert await rest_impl.get_guild_preview("3939393993939") is mock_response - template.compile.assert_called_once_with(guild_id="3939393993939") - rest_impl._request_json_response.assert_awaited_once_with(compiled) - - @pytest.mark.asyncio - async def test_modify_guild_without_optionals(self, rest_impl): - mock_response = {"id": "42", "name": "Hikari"} - rest_impl._request_json_response.return_value = mock_response - with mock_patch_route("PATCH_GUILD") as (template, compiled): - assert await rest_impl.modify_guild("49949495") is mock_response - template.compile.assert_called_once_with(guild_id="49949495") - rest_impl._request_json_response.assert_awaited_once_with(compiled, body={}, reason=...) - - @pytest.mark.asyncio - async def test_modify_guild_with_optionals(self, rest_impl): - mock_response = {"id": "42", "name": "Hikari"} - rest_impl._request_json_response.return_value = mock_response - mock_icon_data = "data:image/png;base64,iVBORw0KGgpibGFo" - mock_splash_data = "data:image/png;base64,iVBORw0KGgpicnVo" - with mock_patch_route("PATCH_GUILD") as (template, compiled): - with mock.patch.object( - conversions, "image_bytes_to_image_data", side_effect=(mock_icon_data, mock_splash_data) - ): - result = await rest_impl.modify_guild( - "49949495", - name="Deutschland", - region="deutschland", - verification_level=2, - default_message_notifications=1, - explicit_content_filter=0, - afk_channel_id="49494949", - afk_timeout=5, - icon=b"\211PNG\r\n\032\nblah", - owner_id="379953393319542784", - splash=b"\211PNG\r\n\032\nbruh", - system_channel_id="123123123123", - reason="I USED TO RULE THE WORLD.", - ) - assert result is mock_response - - template.compile.assert_called_once_with(guild_id="49949495") - assert conversions.image_bytes_to_image_data.call_count == 2 - conversions.image_bytes_to_image_data.assert_has_calls( - ( - mock.call.__bool__(), - mock.call(b"\211PNG\r\n\032\nblah"), - mock.call.__bool__(), - mock.call(b"\211PNG\r\n\032\nbruh"), - ) - ) - rest_impl._request_json_response.assert_awaited_once_with( - compiled, - body={ - "name": "Deutschland", - "region": "deutschland", - "verification_level": 2, - "default_message_notifications": 1, - "explicit_content_filter": 0, - "afk_channel_id": "49494949", - "afk_timeout": 5, - "icon": mock_icon_data, - "owner_id": "379953393319542784", - "splash": mock_splash_data, - "system_channel_id": "123123123123", - }, - reason="I USED TO RULE THE WORLD.", - ) - - @pytest.mark.asyncio - async def test_delete_guild(self, rest_impl): - with mock_patch_route("DELETE_GUILD") as (template, compiled): - assert await rest_impl.delete_guild("92847478") is None - template.compile.assert_called_once_with(guild_id="92847478") - rest_impl._request_json_response.assert_awaited_once_with(compiled) - - @pytest.mark.asyncio - async def test_get_guild_channels(self, rest_impl): - mock_response = [{"type": 2, "id": "21", "name": "Watashi-wa-channel-desu"}] - rest_impl._request_json_response.return_value = mock_response - with mock_patch_route("GET_GUILD_CHANNELS") as (template, compiled): - assert await rest_impl.list_guild_channels("393939393") is mock_response - template.compile.assert_called_once_with(guild_id="393939393") - rest_impl._request_json_response.assert_awaited_once_with(compiled) - - @pytest.mark.asyncio - async def test_create_guild_channel_without_optionals(self, rest_impl): - mock_response = {"type": 2, "id": "3333"} - rest_impl._request_json_response.return_value = mock_response - with mock_patch_route("POST_GUILD_CHANNELS") as (template, compiled): - assert await rest_impl.create_guild_channel("292929", "I am a channel") is mock_response - template.compile.assert_called_once_with(guild_id="292929") - rest_impl._request_json_response.assert_awaited_once_with(compiled, body={"name": "I am a channel"}, reason=...) - - @pytest.mark.asyncio - async def test_create_guild_channel_with_optionals(self, rest_impl): - mock_response = {"type": 2, "id": "379953393319542784"} - rest_impl._request_json_response.return_value = mock_response - with mock_patch_route("POST_GUILD_CHANNELS") as (template, compiled): - result = await rest_impl.create_guild_channel( - "292929", - "I am a channel", - type_=2, - topic="chatter chatter", - bitrate=320, - user_limit=4, - rate_limit_per_user=2, - position=42, - permission_overwrites=[{"target": "379953393319542784", "type": "user"}], - parent_id="379953393319542784", - nsfw=True, - reason="Made a channel for you qt.", - ) - assert result is mock_response - - template.compile.assert_called_once_with(guild_id="292929") - rest_impl._request_json_response.assert_awaited_once_with( - compiled, - body={ - "name": "I am a channel", - "type": 2, - "topic": "chatter chatter", - "bitrate": 320, - "user_limit": 4, - "rate_limit_per_user": 2, - "position": 42, - "permission_overwrites": [{"target": "379953393319542784", "type": "user"}], - "parent_id": "379953393319542784", - "nsfw": True, - }, - reason="Made a channel for you qt.", - ) - - @pytest.mark.asyncio - async def test_modify_guild_channel_positions(self, rest_impl): - with mock_patch_route("PATCH_GUILD_CHANNELS") as (template, compiled): - assert ( - await rest_impl.modify_guild_channel_positions("379953393319542784", ("29292", 0), ("3838", 1)) is None - ) - template.compile.assert_called_once_with(guild_id="379953393319542784") - rest_impl._request_json_response.assert_awaited_once_with( - compiled, body=[{"id": "29292", "position": 0}, {"id": "3838", "position": 1}] - ) - - @pytest.mark.asyncio - async def test_get_guild_member(self, rest_impl): - mock_response = {"id": "379953393319542784", "nick": "Big Moh"} - rest_impl._request_json_response.return_value = mock_response - with mock_patch_route("GET_GUILD_MEMBER") as (template, compiled): - assert await rest_impl.get_guild_member("115590097100865541", "379953393319542784") is mock_response - template.compile.assert_called_once_with(guild_id="115590097100865541", user_id="379953393319542784") - rest_impl._request_json_response.assert_awaited_once_with(compiled) - - @pytest.mark.asyncio - async def test_list_guild_members_without_optionals(self, rest_impl): - mock_response = [{"id": "379953393319542784", "nick": "Big Moh"}] - rest_impl._request_json_response.return_value = mock_response - with mock_patch_route("GET_GUILD_MEMBERS") as (template, compiled): - assert await rest_impl.list_guild_members("115590097100865541") is mock_response - template.compile.assert_called_once_with(guild_id="115590097100865541") - rest_impl._request_json_response.assert_awaited_once_with(compiled, query={}) - - @pytest.mark.asyncio - async def test_list_guild_members_with_optionals(self, rest_impl): - mock_response = [{"id": "379953393319542784", "nick": "Big Moh"}] - rest_impl._request_json_response.return_value = mock_response - with mock_patch_route("GET_GUILD_MEMBERS") as (template, compiled): - assert ( - await rest_impl.list_guild_members("115590097100865541", limit=5, after="4444444444") is mock_response - ) - template.compile.assert_called_once_with(guild_id="115590097100865541") - rest_impl._request_json_response.assert_awaited_once_with(compiled, query={"limit": 5, "after": "4444444444"}) - - @pytest.mark.asyncio - async def test_modify_guild_member_without_optionals(self, rest_impl): - with mock_patch_route("PATCH_GUILD_MEMBER") as (template, compiled): - assert await rest_impl.modify_guild_member("115590097100865541", "379953393319542784") is None - template.compile.assert_called_once_with(guild_id="115590097100865541", user_id="379953393319542784") - rest_impl._request_json_response.assert_awaited_once_with(compiled, body={}, reason=...) - - @pytest.mark.asyncio - async def test_modify_guild_member_with_optionals(self, rest_impl): - with mock_patch_route("PATCH_GUILD_MEMBER") as (template, compiled): - result = await rest_impl.modify_guild_member( - "115590097100865541", - "379953393319542784", - nick="QT", - roles=["222222222"], - mute=True, - deaf=True, - channel_id="777", - reason="I will drink your blood.", - ) - assert result is None - - template.compile.assert_called_once_with(guild_id="115590097100865541", user_id="379953393319542784") - rest_impl._request_json_response.assert_awaited_once_with( - compiled, - body={"nick": "QT", "roles": ["222222222"], "mute": True, "deaf": True, "channel": "777"}, - reason="I will drink your blood.", - ) - - @pytest.mark.asyncio - async def test_modify_current_user_nick_without_reason(self, rest_impl): - with mock_patch_route("PATCH_MY_GUILD_NICKNAME") as (template, compiled): - assert await rest_impl.modify_current_user_nick("202020202", "Nickname me") is None - template.compile.assert_called_once_with(guild_id="202020202") - rest_impl._request_json_response.assert_awaited_once_with(compiled, body={"nick": "Nickname me"}, reason=...) - - @pytest.mark.asyncio - async def test_modify_current_user_nick_with_reason(self, rest_impl): - with mock_patch_route("PATCH_MY_GUILD_NICKNAME") as (template, compiled): - assert await rest_impl.modify_current_user_nick("202020202", "Nickname me", reason="Look at me") is None - template.compile.assert_called_once_with(guild_id="202020202") - rest_impl._request_json_response.assert_awaited_once_with( - compiled, body={"nick": "Nickname me"}, reason="Look at me" - ) - - @pytest.mark.asyncio - async def test_add_guild_member_role_without_reason(self, rest_impl): - with mock_patch_route("PUT_GUILD_MEMBER_ROLE") as (template, compiled): - assert await rest_impl.add_guild_member_role("3939393", "2838383", "84384848") is None - template.compile.assert_called_once_with(guild_id="3939393", user_id="2838383", role_id="84384848") - rest_impl._request_json_response.assert_awaited_once_with(compiled, reason=...) - - @pytest.mark.asyncio - async def test_add_guild_member_role_with_reason(self, rest_impl): - with mock_patch_route("PUT_GUILD_MEMBER_ROLE") as (template, compiled): - assert ( - await rest_impl.add_guild_member_role( - "3939393", "2838383", "84384848", reason="A special role for a special somebody" - ) - is None - ) - template.compile.assert_called_once_with(guild_id="3939393", user_id="2838383", role_id="84384848") - rest_impl._request_json_response.assert_awaited_once_with( - compiled, reason="A special role for a special somebody" - ) - - @pytest.mark.asyncio - async def test_remove_guild_member_role_without_reason(self, rest_impl): - with mock_patch_route("DELETE_GUILD_MEMBER_ROLE") as (template, compiled): - assert await rest_impl.remove_guild_member_role("22222", "3333", "44444") is None - template.compile.assert_called_once_with(guild_id="22222", user_id="3333", role_id="44444") - rest_impl._request_json_response.assert_awaited_once_with(compiled, reason=...) - - @pytest.mark.asyncio - async def test_remove_guild_member_role_with_reason(self, rest_impl): - with mock_patch_route("DELETE_GUILD_MEMBER_ROLE") as (template, compiled): - assert await rest_impl.remove_guild_member_role("22222", "3333", "44444", reason="bye") is None - template.compile.assert_called_once_with(guild_id="22222", user_id="3333", role_id="44444") - rest_impl._request_json_response.assert_awaited_once_with(compiled, reason="bye") - - @pytest.mark.asyncio - async def test_remove_guild_member_without_reason(self, rest_impl): - with mock_patch_route("DELETE_GUILD_MEMBER") as (template, compiled): - assert await rest_impl.remove_guild_member("393939", "82828") is None - template.compile.assert_called_once_with(guild_id="393939", user_id="82828") - rest_impl._request_json_response.assert_awaited_once_with(compiled, reason=...) - - @pytest.mark.asyncio - async def test_remove_guild_member_with_reason(self, rest_impl): - with mock_patch_route("DELETE_GUILD_MEMBER") as (template, compiled): - assert await rest_impl.remove_guild_member("393939", "82828", reason="super bye") is None - template.compile.assert_called_once_with(guild_id="393939", user_id="82828") - rest_impl._request_json_response.assert_awaited_once_with(compiled, reason="super bye") - - @pytest.mark.asyncio - async def test_get_guild_bans(self, rest_impl): - mock_response = [{"id": "3939393"}] - rest_impl._request_json_response.return_value = mock_response - with mock_patch_route("GET_GUILD_BANS") as (template, compiled): - assert await rest_impl.get_guild_bans("292929") is mock_response - template.compile.assert_called_once_with(guild_id="292929") - rest_impl._request_json_response.assert_awaited_once_with(compiled) - - @pytest.mark.asyncio - async def test_get_guild_ban(self, rest_impl): - mock_response = {"id": "3939393"} - rest_impl._request_json_response.return_value = mock_response - with mock_patch_route("GET_GUILD_BAN") as (template, compiled): - assert await rest_impl.get_guild_ban("92929", "44848") is mock_response - template.compile.assert_called_once_with(guild_id="92929", user_id="44848") - rest_impl._request_json_response.assert_awaited_once_with(compiled) - - @pytest.mark.asyncio - async def test_create_guild_ban_without_optionals(self, rest_impl): - with mock_patch_route("PUT_GUILD_BAN") as (template, compiled): - assert await rest_impl.create_guild_ban("222", "444") is None - template.compile.assert_called_once_with(guild_id="222", user_id="444") - rest_impl._request_json_response.assert_awaited_once_with(compiled, query={}) - - @pytest.mark.asyncio - async def test_create_guild_ban_with_optionals(self, rest_impl): - with mock_patch_route("PUT_GUILD_BAN") as (template, compiled): - assert await rest_impl.create_guild_ban("222", "444", delete_message_days=5, reason="TRUE") is None - template.compile.assert_called_once_with(guild_id="222", user_id="444") - rest_impl._request_json_response.assert_awaited_once_with( - compiled, query={"delete-message-days": 5, "reason": "TRUE"} - ) - - @pytest.mark.asyncio - async def test_remove_guild_ban_without_reason(self, rest_impl): - with mock_patch_route("DELETE_GUILD_BAN") as (template, compiled): - assert await rest_impl.remove_guild_ban("494949", "3737") is None - template.compile.assert_called_once_with(guild_id="494949", user_id="3737") - rest_impl._request_json_response.assert_awaited_once_with(compiled, reason=...) - - @pytest.mark.asyncio - async def test_remove_guild_ban_with_reason(self, rest_impl): - with mock_patch_route("DELETE_GUILD_BAN") as (template, compiled): - assert await rest_impl.remove_guild_ban("494949", "3737", reason="LMFAO") is None - template.compile.assert_called_once_with(guild_id="494949", user_id="3737") - rest_impl._request_json_response.assert_awaited_once_with(compiled, reason="LMFAO") - - @pytest.mark.asyncio - async def test_get_guild_roles(self, rest_impl): - mock_response = [{"name": "role", "id": "4949494994"}] - rest_impl._request_json_response.return_value = mock_response - with mock_patch_route("GET_GUILD_ROLES") as (template, compiled): - assert await rest_impl.get_guild_roles("909") is mock_response - template.compile.assert_called_once_with(guild_id="909") - rest_impl._request_json_response.assert_awaited_once_with(compiled) - - @pytest.mark.asyncio - async def test_create_guild_role_without_optionals(self, rest_impl): - mock_response = {"id": "42"} - rest_impl._request_json_response.return_value = mock_response - with mock_patch_route("POST_GUILD_ROLES") as (template, compiled): - assert await rest_impl.create_guild_role("9494") is mock_response - template.compile.assert_called_once_with(guild_id="9494") - rest_impl._request_json_response.assert_awaited_once_with(compiled, body={}, reason=...) - - @pytest.mark.asyncio - async def test_create_guild_role_with_optionals(self, rest_impl): - mock_response = {"id": "42"} - rest_impl._request_json_response.return_value = mock_response - with mock_patch_route("POST_GUILD_ROLES") as (template, compiled): - assert ( - await rest_impl.create_guild_role( - "9494", name="role sama", permissions=22, color=12, hoist=True, mentionable=True, reason="eat dirt" - ) - is mock_response - ) - template.compile.assert_called_once_with(guild_id="9494") - rest_impl._request_json_response.assert_awaited_once_with( - compiled, - body={"name": "role sama", "permissions": 22, "color": 12, "hoist": True, "mentionable": True,}, - reason="eat dirt", - ) - - @pytest.mark.asyncio - async def test_modify_guild_role_positions(self, rest_impl): - mock_response = [{"id": "444", "position": 0}, {"id": "999", "position": 1}] - rest_impl._request_json_response.return_value = mock_response - with mock_patch_route("PATCH_GUILD_ROLES") as (template, compiled): - assert await rest_impl.modify_guild_role_positions("292929", ("444", 0), ("999", 1)) is mock_response - template.compile.assert_called_once_with(guild_id="292929") - rest_impl._request_json_response.assert_awaited_once_with( - compiled, body=[{"id": "444", "position": 0}, {"id": "999", "position": 1}] - ) - - @pytest.mark.asyncio - async def test_modify_guild_role_with_optionals(self, rest_impl): - mock_response = {"id": "54234", "name": "roleio roleio"} - rest_impl._request_json_response.return_value = mock_response - with mock_patch_route("PATCH_GUILD_ROLE") as (template, compiled): - assert await rest_impl.modify_guild_role("999999", "54234") is mock_response - template.compile.assert_called_once_with(guild_id="999999", role_id="54234") - rest_impl._request_json_response.assert_awaited_once_with(compiled, body={}, reason=...) - - @pytest.mark.asyncio - async def test_modify_guild_role_without_optionals(self, rest_impl): - mock_response = {"id": "54234", "name": "roleio roleio"} - rest_impl._request_json_response.return_value = mock_response - with mock_patch_route("PATCH_GUILD_ROLE") as (template, compiled): - result = await rest_impl.modify_guild_role( - "999999", - "54234", - name="HAHA", - permissions=42, - color=69, - hoist=True, - mentionable=False, - reason="You are a pirate.", - ) - assert result is mock_response - template.compile.assert_called_once_with(guild_id="999999", role_id="54234") - rest_impl._request_json_response.assert_awaited_once_with( - compiled, - body={"name": "HAHA", "permissions": 42, "color": 69, "hoist": True, "mentionable": False,}, - reason="You are a pirate.", - ) - - @pytest.mark.asyncio - async def test_delete_guild_role(self, rest_impl): - with mock_patch_route("DELETE_GUILD_ROLE") as (template, compiled): - assert await rest_impl.delete_guild_role("29292", "4848") is None - template.compile.assert_called_once_with(guild_id="29292", role_id="4848") - rest_impl._request_json_response.assert_awaited_once_with(compiled) - - @pytest.mark.asyncio - async def test_get_guild_prune_count(self, rest_impl): - mock_response = {"pruned": 7} - rest_impl._request_json_response.return_value = mock_response - with mock_patch_route("GET_GUILD_PRUNE") as (template, compiled): - assert await rest_impl.get_guild_prune_count("29292", 14) == 7 - template.compile.assert_called_once_with(guild_id="29292") - rest_impl._request_json_response.assert_awaited_once_with(compiled, query={"days": 14}) - - @pytest.mark.asyncio - @pytest.mark.parametrize("mock_response", ({"pruned": None}, {})) - async def test_begin_guild_prune_without_optionals_returns_none(self, rest_impl, mock_response): - rest_impl._request_json_response.return_value = mock_response - with mock_patch_route("POST_GUILD_PRUNE") as (template, compiled): - assert await rest_impl.begin_guild_prune("39393", 14) is None - template.compile.assert_called_once_with(guild_id="39393") - rest_impl._request_json_response.assert_awaited_once_with(compiled, query={"days": 14}, reason=...) - - @pytest.mark.asyncio - async def test_begin_guild_prune_with_optionals(self, rest_impl): - rest_impl._request_json_response.return_value = {"pruned": 32} - with mock_patch_route("POST_GUILD_PRUNE") as (template, compiled): - assert await rest_impl.begin_guild_prune("39393", 14, compute_prune_count=True, reason="BYEBYE") == 32 - rest_impl._request_json_response.assert_awaited_once_with( - compiled, query={"days": 14, "compute_prune_count": "true"}, reason="BYEBYE" - ) - - @pytest.mark.asyncio - async def test_get_guild_voice_regions(self, rest_impl): - mock_response = [{"name": "london", "vip": True}] - rest_impl._request_json_response.return_value = mock_response - with mock_patch_route("GET_GUILD_VOICE_REGIONS") as (template, compiled): - assert await rest_impl.get_guild_voice_regions("2393939") is mock_response - template.compile.assert_called_once_with(guild_id="2393939") - rest_impl._request_json_response.assert_awaited_once_with(compiled) - - @pytest.mark.asyncio - async def test_get_guild_invites(self, rest_impl): - mock_response = [{"code": "ewkkww"}] - rest_impl._request_json_response.return_value = mock_response - with mock_patch_route("GET_GUILD_INVITES") as (template, compiled): - assert await rest_impl.get_guild_invites("9292929") is mock_response - template.compile.assert_called_once_with(guild_id="9292929") - rest_impl._request_json_response.assert_awaited_once_with(compiled) - - @pytest.mark.asyncio - async def test_get_guild_integrations(self, rest_impl): - mock_response = [{"id": "4242"}] - rest_impl._request_json_response.return_value = mock_response - with mock_patch_route("GET_GUILD_INTEGRATIONS") as (template, compiled): - assert await rest_impl.get_guild_integrations("537340989808050216") is mock_response - template.compile.assert_called_once_with(guild_id="537340989808050216") - rest_impl._request_json_response.assert_awaited_once_with(compiled) - - @pytest.mark.asyncio - async def test_modify_guild_integration_without_optionals(self, rest_impl): - with mock_patch_route("PATCH_GUILD_INTEGRATION") as (template, compiled): - assert await rest_impl.modify_guild_integration("292929", "747474") is None - template.compile.assert_called_once_with(guild_id="292929", integration_id="747474") - rest_impl._request_json_response.assert_awaited_once_with(compiled, body={}, reason=...) - - @pytest.mark.asyncio - async def test_modify_guild_integration_with_optionals(self, rest_impl): - with mock_patch_route("PATCH_GUILD_INTEGRATION") as (template, compiled): - result = await rest_impl.modify_guild_integration( - "292929", - "747474", - expire_behaviour=2, - expire_grace_period=1, - enable_emojis=True, - reason="This password is already taken by {redacted}", - ) - assert result is None - - template.compile.assert_called_once_with(guild_id="292929", integration_id="747474") - rest_impl._request_json_response.assert_awaited_once_with( - compiled, - body={"expire_behaviour": 2, "expire_grace_period": 1, "enable_emoticons": True}, - reason="This password is already taken by {redacted}", - ) - - @pytest.mark.asyncio - async def test_delete_guild_integration_without_reason(self, rest_impl): - with mock_patch_route("DELETE_GUILD_INTEGRATION") as (template, compiled): - assert await rest_impl.delete_guild_integration("23992", "7474") is None - template.compile.assert_called_once_with(guild_id="23992", integration_id="7474") - rest_impl._request_json_response.assert_awaited_once_with(compiled, reason=...) - - @pytest.mark.asyncio - async def test_delete_guild_integration_with_reason(self, rest_impl): - with mock_patch_route("DELETE_GUILD_INTEGRATION") as (template, compiled): - assert await rest_impl.delete_guild_integration("23992", "7474", reason="HOT") is None - template.compile.assert_called_once_with(guild_id="23992", integration_id="7474") - rest_impl._request_json_response.assert_awaited_once_with(compiled, reason="HOT") - - @pytest.mark.asyncio - async def test_sync_guild_integration(self, rest_impl): - with mock_patch_route("POST_GUILD_INTEGRATION_SYNC") as (template, compiled): - assert await rest_impl.sync_guild_integration("3939439", "84884") is None - template.compile.assert_called_once_with(guild_id="3939439", integration_id="84884") - rest_impl._request_json_response.assert_awaited_once_with(compiled) - - @pytest.mark.asyncio - async def test_get_guild_embed(self, rest_impl): - mock_response = {"channel": "4304040", "enabled": True} - rest_impl._request_json_response.return_value = mock_response - with mock_patch_route("GET_GUILD_EMBED") as (template, compiled): - assert await rest_impl.get_guild_embed("4949") is mock_response - template.compile.assert_called_once_with(guild_id="4949") - rest_impl._request_json_response.assert_awaited_once_with(compiled) - - @pytest.mark.asyncio - async def test_modify_guild_embed_without_reason(self, rest_impl): - mock_response = {"channel": "4444", "enabled": False} - rest_impl._request_json_response.return_value = mock_response - with mock_patch_route("PATCH_GUILD_EMBED") as (template, compiled): - assert await rest_impl.modify_guild_embed("393939", channel_id="222", enabled=True) is mock_response - template.compile.assert_called_once_with(guild_id="393939") - rest_impl._request_json_response.assert_awaited_once_with( - compiled, body={"channel": "222", "enabled": True}, reason=... - ) - - @pytest.mark.asyncio - async def test_modify_guild_embed_with_reason(self, rest_impl): - mock_response = {"channel": "4444", "enabled": False} - rest_impl._request_json_response.return_value = mock_response - with mock_patch_route("PATCH_GUILD_EMBED") as (template, compiled): - assert ( - await rest_impl.modify_guild_embed("393939", channel_id="222", enabled=True, reason="OK") - is mock_response - ) - template.compile.assert_called_once_with(guild_id="393939") - rest_impl._request_json_response.assert_awaited_once_with( - compiled, body={"channel": "222", "enabled": True}, reason="OK" - ) - - @pytest.mark.asyncio - async def test_get_guild_vanity_url(self, rest_impl): - mock_response = {"code": "dsidid"} - rest_impl._request_json_response.return_value = mock_response - with mock_patch_route("GET_GUILD_VANITY_URL") as (template, compiled): - assert await rest_impl.get_guild_vanity_url("399393") is mock_response - template.compile.assert_called_once_with(guild_id="399393") - rest_impl._request_json_response.assert_awaited_once_with(compiled) - - def test_get_guild_widget_image_url_without_style(self, rest_impl): - url = rest_impl.get_guild_widget_image_url("54949") - assert url == "https://discord.com/api/v6/guilds/54949/widget.png" - - def test_get_guild_widget_image_url_with_style(self, rest_impl): - url = rest_impl.get_guild_widget_image_url("54949", style="banner2") - assert url == "https://discord.com/api/v6/guilds/54949/widget.png?style=banner2" - - @pytest.mark.asyncio - async def test_get_invite_without_counts(self, rest_impl): - mock_response = {"code": "fesdfes"} - rest_impl._request_json_response.return_value = mock_response - with mock_patch_route("GET_INVITE") as (template, compiled): - assert await rest_impl.get_invite("fesdfes") is mock_response - template.compile.assert_called_once_with(invite_code="fesdfes") - rest_impl._request_json_response.assert_awaited_once_with(compiled, query={}) - - @pytest.mark.asyncio - async def test_get_invite_with_counts(self, rest_impl): - mock_response = {"code": "fesdfes"} - rest_impl._request_json_response.return_value = mock_response - with mock_patch_route("GET_INVITE") as (template, compiled): - assert await rest_impl.get_invite("fesdfes", with_counts=True) is mock_response - template.compile.assert_called_once_with(invite_code="fesdfes") - rest_impl._request_json_response.assert_awaited_once_with(compiled, query={"with_counts": "true"}) - - @pytest.mark.asyncio - async def test_delete_invite(self, rest_impl): - mock_response = {"code": "diidsk"} - rest_impl._request_json_response.return_value = mock_response - with mock_patch_route("DELETE_INVITE") as (template, compiled): - assert await rest_impl.delete_invite("diidsk") is mock_response - template.compile.assert_called_once_with(invite_code="diidsk") - rest_impl._request_json_response.assert_awaited_once_with(compiled) - - @pytest.mark.asyncio - async def test_get_current_application_info(self, rest_impl): - mock_response = {"bot_public": True} - rest_impl._request_json_response.return_value = mock_response - with mock_patch_route("GET_MY_APPLICATION") as (template, compiled): - assert await rest_impl.get_current_application_info() is mock_response - template.compile.assert_called_once_with() - rest_impl._request_json_response.assert_awaited_once_with(compiled) - - @pytest.mark.asyncio - async def test_get_current_user(self, rest_impl): - mock_response = {"id": "494949", "username": "A name"} - rest_impl._request_json_response.return_value = mock_response - with mock_patch_route("GET_MY_USER") as (template, compiled): - assert await rest_impl.get_current_user() is mock_response - template.compile.assert_called_once_with() - rest_impl._request_json_response.assert_awaited_once_with(compiled) - - @pytest.mark.asyncio - async def test_get_user(self, rest_impl): - mock_response = {"id": "54959"} - rest_impl._request_json_response.return_value = mock_response - with mock_patch_route("GET_USER") as (template, compiled): - assert await rest_impl.get_user("54959") is mock_response - template.compile.assert_called_once_with(user_id="54959") - rest_impl._request_json_response.assert_awaited_once_with(compiled) - - @pytest.mark.asyncio - async def test_modify_current_user_without_optionals(self, rest_impl): - mock_response = {"id": "44444", "username": "Watashi"} - rest_impl._request_json_response.return_value = mock_response - with mock_patch_route("PATCH_MY_USER") as (template, compiled): - assert await rest_impl.modify_current_user() is mock_response - template.compile.assert_called_once_with() - rest_impl._request_json_response.assert_awaited_once_with(compiled, body={}) - - @pytest.mark.asyncio - async def test_modify_current_user_with_optionals(self, rest_impl): - mock_response = {"id": "44444", "username": "Watashi"} - rest_impl._request_json_response.return_value = mock_response - mock_route = mock.MagicMock(routes.PATCH_MY_USER) - mock_image_data = "data:image/png;base64,iVBORw0KGgpibGFo" - with mock_patch_route("PATCH_MY_USER") as (template, compiled): - with mock.patch.object(conversions, "image_bytes_to_image_data", return_value=mock_image_data): - result = await rest_impl.modify_current_user(username="Watashi 2", avatar=b"\211PNG\r\n\032\nblah") - assert result is mock_response - template.compile.assert_called_once_with() - conversions.image_bytes_to_image_data.assert_called_once_with(b"\211PNG\r\n\032\nblah") - rest_impl._request_json_response.assert_awaited_once_with( - compiled, body={"username": "Watashi 2", "avatar": mock_image_data} - ) - - @pytest.mark.asyncio - async def test_get_current_user_connections(self, rest_impl): - mock_response = [{"id": "fspeed", "revoked": False}] - rest_impl._request_json_response.return_value = mock_response - with mock_patch_route("GET_MY_CONNECTIONS") as (template, compiled): - assert await rest_impl.get_current_user_connections() is mock_response - template.compile.assert_called_once_with() - rest_impl._request_json_response.assert_awaited_once_with(compiled) - - @pytest.mark.asyncio - async def test_get_current_user_guilds_without_optionals(self, rest_impl): - mock_response = [{"id": "452", "owner_id": "4949"}] - rest_impl._request_json_response.return_value = mock_response - with mock_patch_route("GET_MY_GUILDS") as (template, compiled): - assert await rest_impl.get_current_user_guilds() is mock_response - template.compile.assert_called_once_with() - rest_impl._request_json_response.assert_awaited_once_with(compiled, query={}) - - @pytest.mark.asyncio - async def test_get_current_user_guilds_with_optionals(self, rest_impl): - mock_response = [{"id": "452", "owner_id": "4949"}] - rest_impl._request_json_response.return_value = mock_response - with mock_patch_route("GET_MY_GUILDS") as (template, compiled): - assert await rest_impl.get_current_user_guilds(before="292929", after="22288", limit=5) is mock_response - template.compile.assert_called_once_with() - rest_impl._request_json_response.assert_awaited_once_with( - compiled, query={"before": "292929", "after": "22288", "limit": 5} - ) - - @pytest.mark.asyncio - async def test_leave_guild(self, rest_impl): - with mock_patch_route("DELETE_MY_GUILD") as (template, compiled): - assert await rest_impl.leave_guild("292929") is None - template.compile.assert_called_once_with(guild_id="292929") - rest_impl._request_json_response.assert_awaited_once_with(compiled) - - @pytest.mark.asyncio - async def test_create_dm(self, rest_impl): - mock_response = {"id": "404040", "recipients": []} - rest_impl._request_json_response.return_value = mock_response - with mock_patch_route("POST_MY_CHANNELS") as (template, compiled): - assert await rest_impl.create_dm("409491291156774923") is mock_response - template.compile.assert_called_once_with() - rest_impl._request_json_response.assert_awaited_once_with(compiled, body={"recipient_id": "409491291156774923"}) - - @pytest.mark.asyncio - async def test_list_voice_regions(self, rest_impl): - mock_response = [{"name": "neko-cafe"}] - rest_impl._request_json_response.return_value = mock_response - with mock_patch_route("GET_VOICE_REGIONS") as (template, compiled): - assert await rest_impl.list_voice_regions() is mock_response - template.compile.assert_called_once_with() - rest_impl._request_json_response.assert_awaited_once_with(compiled) - - @pytest.mark.asyncio - async def test_create_webhook_without_optionals(self, rest_impl): - mock_response = {"channel": "39393993", "id": "8383838"} - rest_impl._request_json_response.return_value = mock_response - with mock_patch_route("POST_CHANNEL_WEBHOOKS") as (template, compiled): - assert await rest_impl.create_webhook("39393939", "I am a webhook") is mock_response - template.compile.assert_called_once_with(channel_id="39393939") - rest_impl._request_json_response.assert_awaited_once_with(compiled, body={"name": "I am a webhook"}, reason=...) - - @pytest.mark.asyncio - async def test_create_webhook_with_optionals(self, rest_impl): - mock_response = {"channel": "39393993", "id": "8383838"} - rest_impl._request_json_response.return_value = mock_response - mock_image_data = "data:image/png;base64,iVBORw0KGgpibGFo" - with mock_patch_route("POST_CHANNEL_WEBHOOKS") as (template, compiled): - with mock.patch.object(conversions, "image_bytes_to_image_data", return_value=mock_image_data): - result = await rest_impl.create_webhook( - "39393939", "I am a webhook", avatar=b"\211PNG\r\n\032\nblah", reason="get reasoned" - ) - assert result is mock_response - template.compile.assert_called_once_with(channel_id="39393939") - conversions.image_bytes_to_image_data.assert_called_once_with(b"\211PNG\r\n\032\nblah") - rest_impl._request_json_response.assert_awaited_once_with( - compiled, body={"name": "I am a webhook", "avatar": mock_image_data}, reason="get reasoned", - ) - - @pytest.mark.asyncio - async def test_get_channel_webhooks(self, rest_impl): - mock_response = [{"channel": "39393993", "id": "8383838"}] - rest_impl._request_json_response.return_value = mock_response - with mock_patch_route("GET_CHANNEL_WEBHOOKS") as (template, compiled): - assert await rest_impl.get_channel_webhooks("9393939") is mock_response - template.compile.assert_called_once_with(channel_id="9393939") - rest_impl._request_json_response.assert_awaited_once_with(compiled) - - @pytest.mark.asyncio - async def test_get_guild_webhooks(self, rest_impl): - mock_response = [{"channel": "39393993", "id": "8383838"}] - rest_impl._request_json_response.return_value = mock_response - with mock_patch_route("GET_GUILD_WEBHOOKS") as (template, compiled): - assert await rest_impl.get_guild_webhooks("9393939") is mock_response - template.compile.assert_called_once_with(guild_id="9393939") - rest_impl._request_json_response.assert_awaited_once_with(compiled) - - @pytest.mark.asyncio - async def test_get_webhook_without_token(self, rest_impl): - mock_response = {"channel": "39393993", "id": "8383838"} - rest_impl._request_json_response.return_value = mock_response - with mock_patch_route("GET_WEBHOOK") as (template, compiled): - assert await rest_impl.get_webhook("9393939") is mock_response - template.compile.assert_called_once_with(webhook_id="9393939") - rest_impl._request_json_response.assert_awaited_once_with(compiled, suppress_authorization_header=False) - - @pytest.mark.asyncio - async def test_get_webhook_with_token(self, rest_impl): - mock_response = {"channel": "39393993", "id": "8383838"} - rest_impl._request_json_response.return_value = mock_response - with mock_patch_route("GET_WEBHOOK_WITH_TOKEN") as (template, compiled): - assert await rest_impl.get_webhook("9393939", webhook_token="a_webhook_token") is mock_response - template.compile.assert_called_once_with(webhook_id="9393939", webhook_token="a_webhook_token") - rest_impl._request_json_response.assert_awaited_once_with(compiled, suppress_authorization_header=True) - - @pytest.mark.asyncio - async def test_modify_webhook_without_optionals_without_token(self, rest_impl): - mock_response = {"channel": "39393993", "id": "8383838"} - rest_impl._request_json_response.return_value = mock_response - with mock_patch_route("PATCH_WEBHOOK") as (template, compiled): - assert await rest_impl.modify_webhook("929292") is mock_response - template.compile.assert_called_once_with(webhook_id="929292") - rest_impl._request_json_response.assert_awaited_once_with( - compiled, body={}, reason=..., suppress_authorization_header=False - ) - - @pytest.mark.asyncio - async def test_modify_webhook_with_optionals_without_token(self, rest_impl): - mock_response = {"channel": "39393993", "id": "8383838"} - rest_impl._request_json_response.return_value = mock_response - with mock_patch_route("PATCH_WEBHOOK") as (template, compiled): - assert ( - await rest_impl.modify_webhook( - "929292", name="nyaa", avatar=b"\211PNG\r\n\032\nblah", channel_id="2929292929", reason="nuzzle", - ) - is mock_response - ) - template.compile.assert_called_once_with(webhook_id="929292") - rest_impl._request_json_response.assert_awaited_once_with( - compiled, - body={"name": "nyaa", "avatar": "data:image/png;base64,iVBORw0KGgpibGFo", "channel": "2929292929",}, - reason="nuzzle", - suppress_authorization_header=False, - ) - - @pytest.mark.asyncio - async def test_modify_webhook_without_optionals_with_token(self, rest_impl): - mock_response = {"channel": "39393993", "id": "8383838"} - rest_impl._request_json_response.return_value = mock_response - with mock_patch_route("PATCH_WEBHOOK_WITH_TOKEN") as (template, compiled): - assert await rest_impl.modify_webhook("929292", webhook_token="a_webhook_token") is mock_response - template.compile.assert_called_once_with(webhook_id="929292", webhook_token="a_webhook_token") - rest_impl._request_json_response.assert_awaited_once_with( - compiled, body={}, reason=..., suppress_authorization_header=True - ) - - @pytest.mark.asyncio - async def test_delete_webhook_without_token(self, rest_impl): - with mock_patch_route("DELETE_WEBHOOK") as (template, compiled): - assert await rest_impl.delete_webhook("9393939") is None - template.compile.assert_called_once_with(webhook_id="9393939") - rest_impl._request_json_response.assert_awaited_once_with(compiled, suppress_authorization_header=False) - - @pytest.mark.asyncio - async def test_delete_webhook_with_token(self, rest_impl): - with mock_patch_route("DELETE_WEBHOOK_WITH_TOKEN") as (template, compiled): - assert await rest_impl.delete_webhook("9393939", webhook_token="a_webhook_token") is None - template.compile.assert_called_once_with(webhook_id="9393939", webhook_token="a_webhook_token") - rest_impl._request_json_response.assert_awaited_once_with(compiled, suppress_authorization_header=True) - - @pytest.mark.asyncio - async def test_execute_webhook_without_optionals(self, rest_impl): - mock_form = mock.MagicMock(aiohttp.FormData, add_field=mock.MagicMock()) - rest_impl._request_json_response.return_value = None - mock_json = "{}" - with mock.patch.object(aiohttp, "FormData", return_value=mock_form): - with mock_patch_route("POST_WEBHOOK_WITH_TOKEN") as (template, compiled): - with mock.patch.object(json, "dumps", return_value=mock_json): - assert await rest_impl.execute_webhook("9393939", "a_webhook_token") is None - template.compile.assert_called_once_with(webhook_id="9393939", webhook_token="a_webhook_token") - json.dumps.assert_called_once_with({}) - mock_form.add_field.assert_called_once_with("payload_json", mock_json, content_type="application/json") - rest_impl._request_json_response.assert_awaited_once_with( - compiled, body=mock_form, query={}, suppress_authorization_header=True, - ) - - @pytest.mark.asyncio - @mock.patch.object(aiohttp, "FormData") - @mock.patch.object(json, "dumps") - async def test_execute_webhook_with_optionals(self, dumps, FormData, rest_impl): - with mock_patch_route("POST_WEBHOOK_WITH_TOKEN") as (template, compiled): - mock_form = mock.MagicMock(aiohttp.FormData, add_field=mock.MagicMock()) - FormData.return_value = mock_form - mock_response = {"id": "53", "content": "la"} - rest_impl._request_json_response.return_value = mock_response - mock_file = mock.MagicMock(files.BaseStream) - mock_file.name = "file.txt" - mock_file2 = mock.MagicMock(files.BaseStream) - mock_file2.name = "file2.txt" - mock_json = '{"content": "A messages", "username": "agent 42"}' - dumps.return_value = mock_json - response = await rest_impl.execute_webhook( - "9393939", - "a_webhook_token", - content="A message", - username="agent 42", - avatar_url="https://localhost.bump", - tts=True, - wait=True, - files=[mock_file, mock_file2], - embeds=[{"type": "rich", "description": "A DESCRIPTION"}], - allowed_mentions={"users": ["123"], "roles": ["456"]}, - ) - assert response is mock_response - template.compile.assert_called_once_with(webhook_id="9393939", webhook_token="a_webhook_token") - dumps.assert_called_once_with( - { - "tts": True, - "content": "A message", - "username": "agent 42", - "avatar_url": "https://localhost.bump", - "embeds": [{"type": "rich", "description": "A DESCRIPTION"}], - "allowed_mentions": {"users": ["123"], "roles": ["456"]}, - } - ) - - assert mock_form.add_field.call_count == 3 - mock_form.add_field.assert_has_calls( - ( - mock.call("payload_json", mock_json, content_type="application/json"), - mock.call("file0", mock_file, filename="file.txt", content_type="application/octet-stream"), - mock.call("file1", mock_file2, filename="file2.txt", content_type="application/octet-stream"), - ), - any_order=True, - ) - - rest_impl._request_json_response.assert_awaited_once_with( - compiled, body=mock_form, query={"wait": "true"}, suppress_authorization_header=True, - ) diff --git a/tests/hikari/rest/test_user.py b/tests/hikari/rest/test_user.py deleted file mode 100644 index a1fcd719e3..0000000000 --- a/tests/hikari/rest/test_user.py +++ /dev/null @@ -1,50 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# Copyright © Nekoka.tt 2019-2020 -# -# This file is part of Hikari. -# -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . -import mock -import pytest - -from hikari import application -from hikari.models import users -from hikari.net.rest import session -from hikari.net.rest import user -from tests.hikari import _helpers - - -class TestRESTUserLogic: - @pytest.fixture() - def rest_user_logic_impl(self): - mock_app = mock.MagicMock(application.Application) - mock_low_level_restful_client = mock.MagicMock(session.RESTSession) - - class RESTUserLogicImpl(user.RESTUserComponent): - def __init__(self): - super().__init__(mock_app, mock_low_level_restful_client) - - return RESTUserLogicImpl() - - @pytest.mark.asyncio - @_helpers.parametrize_valid_id_formats_for_models("user", 123123123, users.User) - async def test_fetch_user(self, rest_user_logic_impl, user): - mock_user_payload = {"id": "123", "username": "userName"} - mock_user_obj = mock.MagicMock(users.User) - rest_user_logic_impl._session.get_user.return_value = mock_user_payload - with mock.patch.object(users.User, "deserialize", return_value=mock_user_obj): - assert await rest_user_logic_impl.fetch_user(user) is mock_user_obj - rest_user_logic_impl._session.get_user.assert_called_once_with(user_id="123123123") - users.User.deserialize.assert_called_once_with(mock_user_payload, app=rest_user_logic_impl._app) diff --git a/tests/hikari/rest/test_voice.py b/tests/hikari/rest/test_voice.py deleted file mode 100644 index 7e2b43d56e..0000000000 --- a/tests/hikari/rest/test_voice.py +++ /dev/null @@ -1,48 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# Copyright © Nekoka.tt 2019-2020 -# -# This file is part of Hikari. -# -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . -import mock -import pytest - -from hikari import application -from hikari.models import voices -from hikari.net.rest import voice -from hikari.net.rest import session - - -class TestRESTUserLogic: - @pytest.fixture() - def rest_voice_logic_impl(self): - mock_app = mock.MagicMock(application.Application) - mock_low_level_restful_client = mock.MagicMock(session.RESTSession) - - class RESTVoiceLogicImpl(voice.RESTVoiceComponent): - def __init__(self): - super().__init__(mock_app, mock_low_level_restful_client) - - return RESTVoiceLogicImpl() - - @pytest.mark.asyncio - async def test_fetch_voice_regions(self, rest_voice_logic_impl): - mock_voice_payload = {"id": "LONDON", "name": "london"} - mock_voice_obj = mock.MagicMock(voices.VoiceRegion) - rest_voice_logic_impl._session.list_voice_regions.return_value = [mock_voice_payload] - with mock.patch.object(voices.VoiceRegion, "deserialize", return_value=mock_voice_obj): - assert await rest_voice_logic_impl.fetch_voice_regions() == [mock_voice_obj] - rest_voice_logic_impl._session.list_voice_regions.assert_called_once() - voices.VoiceRegion.deserialize.assert_called_once_with(mock_voice_payload, app=rest_voice_logic_impl._app) diff --git a/tests/hikari/rest/test_webhook.py b/tests/hikari/rest/test_webhook.py deleted file mode 100644 index f3c39558a2..0000000000 --- a/tests/hikari/rest/test_webhook.py +++ /dev/null @@ -1,286 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# Copyright © Nekoka.tt 2019-2020 -# -# This file is part of Hikari. -# -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . -import contextlib - -import mock -import pytest - -from hikari import application -from hikari.internal import helpers -from hikari.models import embeds -from hikari.models import files -from hikari.models import messages -from hikari.models import webhooks -from hikari.net.rest import session -from hikari.net.rest import webhook -from tests.hikari import _helpers - - -class TestRESTUserLogic: - @pytest.fixture() - def rest_webhook_logic_impl(self): - mock_app = mock.MagicMock(application.Application) - mock_low_level_restful_client = mock.MagicMock(session.RESTSession) - - class RESTWebhookLogicImpl(webhook.RESTWebhookComponent): - def __init__(self): - super().__init__(mock_app, mock_low_level_restful_client) - - return RESTWebhookLogicImpl() - - @pytest.mark.asyncio - @_helpers.parametrize_valid_id_formats_for_models("webhook", 379953393319542784, webhooks.Webhook) - async def test_fetch_webhook_with_webhook_token(self, rest_webhook_logic_impl, webhook): - mock_webhook_payload = {"id": "29292929", "channel": "2292992"} - mock_webhook_obj = mock.MagicMock(webhooks.Webhook) - rest_webhook_logic_impl._session.get_webhook.return_value = mock_webhook_payload - with mock.patch.object(webhooks.Webhook, "deserialize", return_value=mock_webhook_obj): - assert ( - await rest_webhook_logic_impl.fetch_webhook(webhook, webhook_token="dsawqoepql.kmsdao") - is mock_webhook_obj - ) - rest_webhook_logic_impl._session.get_webhook.assert_called_once_with( - webhook_id="379953393319542784", webhook_token="dsawqoepql.kmsdao", - ) - webhooks.Webhook.deserialize.assert_called_once_with(mock_webhook_payload, app=rest_webhook_logic_impl._app) - - @pytest.mark.asyncio - @_helpers.parametrize_valid_id_formats_for_models("webhook", 379953393319542784, webhooks.Webhook) - async def test_fetch_webhook_without_webhook_token(self, rest_webhook_logic_impl, webhook): - mock_webhook_payload = {"id": "29292929", "channel": "2292992"} - mock_webhook_obj = mock.MagicMock(webhooks.Webhook) - rest_webhook_logic_impl._session.get_webhook.return_value = mock_webhook_payload - with mock.patch.object(webhooks.Webhook, "deserialize", return_value=mock_webhook_obj): - assert await rest_webhook_logic_impl.fetch_webhook(webhook) is mock_webhook_obj - rest_webhook_logic_impl._session.get_webhook.assert_called_once_with( - webhook_id="379953393319542784", webhook_token=..., - ) - webhooks.Webhook.deserialize.assert_called_once_with(mock_webhook_payload, app=rest_webhook_logic_impl._app) - - @pytest.mark.asyncio - @_helpers.parametrize_valid_id_formats_for_models("webhook", 379953393319542784, webhooks.Webhook) - @_helpers.parametrize_valid_id_formats_for_models("channel", 115590097100865541, webhooks.Webhook) - async def test_update_webhook_with_optionals(self, rest_webhook_logic_impl, webhook, channel): - mock_webhook_obj = mock.MagicMock(webhooks.Webhook) - mock_webhook_payload = {"id": "123123", "avatar": "1wedoklpasdoiksdoka"} - rest_webhook_logic_impl._session.modify_webhook.return_value = mock_webhook_payload - mock_image_data = mock.MagicMock(bytes) - mock_image_obj = mock.MagicMock(files.BaseStream) - mock_image_obj.read = mock.AsyncMock(return_value=mock_image_data) - stack = contextlib.ExitStack() - stack.enter_context(mock.patch.object(webhooks.Webhook, "deserialize", return_value=mock_webhook_obj)) - with stack: - result = await rest_webhook_logic_impl.update_webhook( - webhook, - webhook_token="a.wEbHoOk.ToKeN", - name="blah_blah_blah", - avatar=mock_image_obj, - channel=channel, - reason="A reason", - ) - assert result is mock_webhook_obj - rest_webhook_logic_impl._session.modify_webhook.assert_called_once_with( - webhook_id="379953393319542784", - webhook_token="a.wEbHoOk.ToKeN", - name="blah_blah_blah", - avatar=mock_image_data, - channel_id="115590097100865541", - reason="A reason", - ) - webhooks.Webhook.deserialize.assert_called_once_with(mock_webhook_payload, app=rest_webhook_logic_impl._app) - mock_image_obj.read.assert_awaited_once() - - @pytest.mark.asyncio - @_helpers.parametrize_valid_id_formats_for_models("webhook", 379953393319542784, webhooks.Webhook) - async def test_update_webhook_without_optionals(self, rest_webhook_logic_impl, webhook): - mock_webhook_obj = mock.MagicMock(webhooks.Webhook) - mock_webhook_payload = {"id": "123123", "avatar": "1wedoklpasdoiksdoka"} - rest_webhook_logic_impl._session.modify_webhook.return_value = mock_webhook_payload - with mock.patch.object(webhooks.Webhook, "deserialize", return_value=mock_webhook_obj): - assert await rest_webhook_logic_impl.update_webhook(webhook) is mock_webhook_obj - rest_webhook_logic_impl._session.modify_webhook.assert_called_once_with( - webhook_id="379953393319542784", webhook_token=..., name=..., avatar=..., channel_id=..., reason=..., - ) - webhooks.Webhook.deserialize.assert_called_once_with(mock_webhook_payload, app=rest_webhook_logic_impl._app) - - @pytest.mark.asyncio - @_helpers.parametrize_valid_id_formats_for_models("webhook", 379953393319542784, webhooks.Webhook) - async def test_delete_webhook_with_webhook_token(self, rest_webhook_logic_impl, webhook): - rest_webhook_logic_impl._session.delete_webhook.return_value = ... - assert await rest_webhook_logic_impl.delete_webhook(webhook, webhook_token="dsawqoepql.kmsdao") is None - rest_webhook_logic_impl._session.delete_webhook.assert_called_once_with( - webhook_id="379953393319542784", webhook_token="dsawqoepql.kmsdao" - ) - - @pytest.mark.asyncio - @_helpers.parametrize_valid_id_formats_for_models("webhook", 379953393319542784, webhooks.Webhook) - async def test_delete_webhook_without_webhook_token(self, rest_webhook_logic_impl, webhook): - rest_webhook_logic_impl._session.delete_webhook.return_value = ... - assert await rest_webhook_logic_impl.delete_webhook(webhook) is None - rest_webhook_logic_impl._session.delete_webhook.assert_called_once_with( - webhook_id="379953393319542784", webhook_token=... - ) - - @pytest.mark.asyncio - @_helpers.parametrize_valid_id_formats_for_models("webhook", 379953393319542784, webhooks.Webhook) - async def test_execute_webhook_without_optionals(self, rest_webhook_logic_impl, webhook): - rest_webhook_logic_impl._session.execute_webhook.return_value = ... - mock_allowed_mentions_payload = {"parse": ["everyone", "users", "roles"]} - with mock.patch.object(helpers, "generate_allowed_mentions", return_value=mock_allowed_mentions_payload): - assert await rest_webhook_logic_impl.execute_webhook(webhook, "a.webhook.token") is None - helpers.generate_allowed_mentions.assert_called_once_with( - mentions_everyone=True, user_mentions=True, role_mentions=True - ) - rest_webhook_logic_impl._session.execute_webhook.assert_called_once_with( - webhook_id="379953393319542784", - webhook_token="a.webhook.token", - content=..., - username=..., - avatar_url=..., - tts=..., - wait=False, - files=..., - embeds=..., - allowed_mentions=mock_allowed_mentions_payload, - ) - - @pytest.mark.asyncio - @_helpers.parametrize_valid_id_formats_for_models("webhook", 379953393319542784, webhooks.Webhook) - async def test_execute_webhook_with_optionals(self, rest_webhook_logic_impl, webhook): - rest_webhook_logic_impl._session.execute_webhook.return_value = ... - mock_allowed_mentions_payload = {"parse": ["everyone", "users", "roles"]} - mock_embed_payload = {"description": "424242"} - mock_file_obj = mock.MagicMock(files.BaseStream) - mock_file_obj2 = mock.MagicMock(files.BaseStream) - mock_embed_obj = mock.MagicMock(embeds.Embed) - mock_embed_obj.assets_to_upload = [mock_file_obj2] - mock_embed_obj.serialize = mock.MagicMock(return_value=mock_embed_payload) - stack = contextlib.ExitStack() - stack.enter_context(mock.patch.object(messages.Message, "deserialize")) - stack.enter_context( - mock.patch.object(helpers, "generate_allowed_mentions", return_value=mock_allowed_mentions_payload) - ) - with stack: - await rest_webhook_logic_impl.execute_webhook( - webhook, - "a.webhook.token", - content="THE TRUTH", - username="User 97", - avatar_url="httttttt/L//", - tts=True, - wait=True, - files=[mock_file_obj], - embeds=[mock_embed_obj], - mentions_everyone=False, - role_mentions=False, - user_mentions=False, - ) - helpers.generate_allowed_mentions.assert_called_once_with( - mentions_everyone=False, user_mentions=False, role_mentions=False - ) - rest_webhook_logic_impl._session.execute_webhook.assert_called_once_with( - webhook_id="379953393319542784", - webhook_token="a.webhook.token", - content="THE TRUTH", - username="User 97", - avatar_url="httttttt/L//", - tts=True, - wait=True, - files=[mock_file_obj, mock_file_obj2], - embeds=[mock_embed_payload], - allowed_mentions=mock_allowed_mentions_payload, - ) - mock_embed_obj.serialize.assert_called_once() - - @pytest.mark.asyncio - @_helpers.parametrize_valid_id_formats_for_models("webhook", 379953393319542784, webhooks.Webhook) - async def test_execute_webhook_returns_message_when_wait_is_true(self, rest_webhook_logic_impl, webhook): - mock_message_payload = {"id": "6796959949034", "content": "Nyaa Nyaa"} - mock_message_obj = mock.MagicMock(messages.Message) - rest_webhook_logic_impl._session.execute_webhook.return_value = mock_message_payload - mock_allowed_mentions_payload = {"parse": ["everyone", "users", "roles"]} - stack = contextlib.ExitStack() - stack.enter_context( - mock.patch.object(helpers, "generate_allowed_mentions", return_value=mock_allowed_mentions_payload) - ) - stack.enter_context(mock.patch.object(messages.Message, "deserialize", return_value=mock_message_obj)) - with stack: - assert ( - await rest_webhook_logic_impl.execute_webhook(webhook, "a.webhook.token", wait=True) is mock_message_obj - ) - messages.Message.deserialize.assert_called_once_with(mock_message_payload, app=rest_webhook_logic_impl._app) - - @pytest.mark.asyncio - async def test_safe_execute_webhook_without_optionals(self, rest_webhook_logic_impl): - webhook = mock.MagicMock(webhooks.Webhook) - mock_message_obj = mock.MagicMock(messages.Message) - rest_webhook_logic_impl.execute_webhook = mock.AsyncMock(return_value=mock_message_obj) - result = await rest_webhook_logic_impl.safe_webhook_execute(webhook, "a.webhook.token",) - assert result is mock_message_obj - rest_webhook_logic_impl.execute_webhook.assert_called_once_with( - webhook=webhook, - webhook_token="a.webhook.token", - content=..., - username=..., - avatar_url=..., - tts=..., - wait=False, - files=..., - embeds=..., - mentions_everyone=False, - user_mentions=False, - role_mentions=False, - ) - - @pytest.mark.asyncio - async def test_safe_execute_webhook_with_optionals(self, rest_webhook_logic_impl): - webhook = mock.MagicMock(webhooks.Webhook) - mock_file_obj = mock.MagicMock(files.BaseStream) - mock_embed_obj = mock.MagicMock(embeds.Embed) - mock_message_obj = mock.MagicMock(messages.Message) - rest_webhook_logic_impl.execute_webhook = mock.AsyncMock(return_value=mock_message_obj) - result = await rest_webhook_logic_impl.safe_webhook_execute( - webhook, - "a.webhook.token", - content="THE TRUTH", - username="User 97", - avatar_url="httttttt/L//", - tts=True, - wait=True, - files=[mock_file_obj], - embeds=[mock_embed_obj], - mentions_everyone=False, - role_mentions=False, - user_mentions=False, - ) - assert result is mock_message_obj - rest_webhook_logic_impl.execute_webhook.assert_called_once_with( - webhook=webhook, - webhook_token="a.webhook.token", - content="THE TRUTH", - username="User 97", - avatar_url="httttttt/L//", - tts=True, - wait=True, - files=[mock_file_obj], - embeds=[mock_embed_obj], - mentions_everyone=False, - role_mentions=False, - user_mentions=False, - ) From f6eae0243ee8ae5eba7780561382b625e1f94464 Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Fri, 22 May 2020 23:19:59 +0100 Subject: [PATCH 374/922] Removed dead exceptions for gateway --- hikari/net/gateway.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/hikari/net/gateway.py b/hikari/net/gateway.py index d42521f5d7..fe286ff315 100644 --- a/hikari/net/gateway.py +++ b/hikari/net/gateway.py @@ -123,14 +123,6 @@ class _Reconnect(RuntimeError): __slots__ = () -class _Zombie(RuntimeError): - __slots__ = () - - -class _ClientClosed(RuntimeError): - __slots__ = () - - class _SocketClosed(RuntimeError): __slots__ = () From c983a6cbe198643a6657be372a9ac03b6497a3e2 Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Fri, 22 May 2020 22:54:17 +0000 Subject: [PATCH 375/922] Update gateway.py to not invalidate session for zombification purposes. --- hikari/net/gateway.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hikari/net/gateway.py b/hikari/net/gateway.py index fe286ff315..f69706e66a 100644 --- a/hikari/net/gateway.py +++ b/hikari/net/gateway.py @@ -513,7 +513,7 @@ async def _pulse(self) -> None: time_since_heartbeat_sent, ) self._zombied = True - await self._close_ws(_GatewayCloseCode.RFC_6455_NORMAL_CLOSURE, "zombie connection") + await self._close_ws(_GatewayCloseCode.DO_NOT_INVALIDATE_SESSION, "zombie connection") return self.logger.debug( From ed5245ba4d8432b44a892853092cce7dcefaff53 Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Sat, 23 May 2020 11:01:31 +0100 Subject: [PATCH 376/922] Purged a load of test suites needing a rewrite. --- tests/hikari/events/__init__.py | 18 - tests/hikari/events/test_base.py | 51 - tests/hikari/events/test_channel.py | 278 ------ tests/hikari/events/test_guild.py | 245 ----- tests/hikari/events/test_message.py | 400 -------- tests/hikari/events/test_other.py | 128 --- tests/hikari/events/test_voice.py | 33 - tests/hikari/gateway/__init__.py | 18 - tests/hikari/gateway/test_client.py | 598 ------------ tests/hikari/gateway/test_connection.py | 913 ------------------ tests/hikari/gateway/test_dispatchers.py | 163 ---- .../gateway/test_intent_aware_dispatchers.py | 621 ------------ tests/hikari/internal/test_codes.py | 35 - tests/hikari/internal/test_http_client.py | 14 +- tests/hikari/internal/test_marshaller.py | 361 ------- tests/hikari/models/test_gateway.py | 100 -- tests/hikari/rest/__init__.py | 18 - tests/hikari/rest/test_buckets.py | 309 ------ tests/hikari/rest/test_routes.py | 111 --- tests/hikari/stateless/__init__.py | 18 - tests/hikari/stateless/test_manager.py | 507 ---------- tests/hikari/test_configs.py | 321 ------ tests/hikari/test_errors.py | 153 --- 23 files changed, 5 insertions(+), 5408 deletions(-) delete mode 100644 tests/hikari/events/__init__.py delete mode 100644 tests/hikari/events/test_base.py delete mode 100644 tests/hikari/events/test_channel.py delete mode 100644 tests/hikari/events/test_guild.py delete mode 100644 tests/hikari/events/test_message.py delete mode 100644 tests/hikari/events/test_other.py delete mode 100644 tests/hikari/events/test_voice.py delete mode 100644 tests/hikari/gateway/__init__.py delete mode 100644 tests/hikari/gateway/test_client.py delete mode 100644 tests/hikari/gateway/test_connection.py delete mode 100644 tests/hikari/gateway/test_dispatchers.py delete mode 100644 tests/hikari/gateway/test_intent_aware_dispatchers.py delete mode 100644 tests/hikari/internal/test_codes.py delete mode 100644 tests/hikari/internal/test_marshaller.py delete mode 100644 tests/hikari/models/test_gateway.py delete mode 100644 tests/hikari/rest/__init__.py delete mode 100644 tests/hikari/rest/test_buckets.py delete mode 100644 tests/hikari/rest/test_routes.py delete mode 100644 tests/hikari/stateless/__init__.py delete mode 100644 tests/hikari/stateless/test_manager.py delete mode 100644 tests/hikari/test_configs.py delete mode 100644 tests/hikari/test_errors.py diff --git a/tests/hikari/events/__init__.py b/tests/hikari/events/__init__.py deleted file mode 100644 index 1c1502a5ca..0000000000 --- a/tests/hikari/events/__init__.py +++ /dev/null @@ -1,18 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# Copyright © Nekoka.tt 2019-2020 -# -# This file is part of Hikari. -# -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . diff --git a/tests/hikari/events/test_base.py b/tests/hikari/events/test_base.py deleted file mode 100644 index 951295a57d..0000000000 --- a/tests/hikari/events/test_base.py +++ /dev/null @@ -1,51 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# Copyright © Nekoka.tt 2019-2020 -# -# This file is part of Hikari. -# -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . -from hikari.events import base -from hikari.internal import more_collections -from hikari.models import intents - - -# Base event, is not deserialized -class TestHikariEvent: - ... - - -def test_get_required_intents_for(): - class StubEvent: - ___required_intents___ = [intents.Intent.DIRECT_MESSAGES] - - base.get_required_intents_for(StubEvent()) == [intents.Intent.DIRECT_MESSAGES] - - -def test_get_required_intents_for_when_none_required(): - class StubEvent: - ... - - base.get_required_intents_for(StubEvent()) == more_collections.EMPTY_COLLECTION - - -def test_requires_intents(): - @base.requires_intents(intents.Intent.DIRECT_MESSAGES, intents.Intent.DIRECT_MESSAGE_REACTIONS) - class StubEvent: - ... - - assert StubEvent().___required_intents___ == [ - intents.Intent.DIRECT_MESSAGES, - intents.Intent.DIRECT_MESSAGE_REACTIONS, - ] diff --git a/tests/hikari/events/test_channel.py b/tests/hikari/events/test_channel.py deleted file mode 100644 index 0768872627..0000000000 --- a/tests/hikari/events/test_channel.py +++ /dev/null @@ -1,278 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# Copyright © Nekoka.tt 2019-2020 -# -# This file is part of Hikari. -# -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . -import contextlib -import datetime - -import mock -import pytest - -from hikari.events import channel -from hikari.internal import conversions -from hikari.models import channels -from hikari.models import guilds -from hikari.models import invites -from hikari.models import users -from tests.hikari import _helpers - - -@pytest.fixture() -def test_user_payload(): - return {"id": "2929292", "username": "agent 69", "discriminator": "4444", "avatar": "9292929292929292"} - - -class TestBaseChannelEvent: - @pytest.fixture() - def test_overwrite_payload(self): - return {"id": "292929", "type": "member", "allow": 49152, "deny": 0} - - @pytest.fixture() - def test_base_channel_payload(self, test_overwrite_payload, test_user_payload): - return { - "id": "424242", - "type": 2, - "guild_id": "69240", - "position": 7, - "permission_overwrites": [test_overwrite_payload], - "name": "Name", - "topic": "Topically drunk", - "nsfw": True, - "last_message_id": "22222222", - "bitrate": 96000, - "user_limit": 42, - "rate_limit_per_user": 2333, - "recipients": [test_user_payload], - "icon": "sdodsooioio2oi", - "owner_id": "32939393", - "application_id": "202020202", - "parent_id": "2030302939", - "last_pin_timestamp": "2019-05-17T06:26:56.936000+00:00", - } - - def test_deserialize(self, test_base_channel_payload, test_overwrite_payload, test_user_payload): - mock_timestamp = mock.MagicMock(datetime.datetime) - mock_user = mock.MagicMock(users.User, id=2929292) - mock_overwrite = mock.MagicMock(channels.PermissionOverwrite, id=292929) - stack = contextlib.ExitStack() - patched_timestamp_deserializer = stack.enter_context( - _helpers.patch_marshal_attr( - channel.BaseChannelEvent, - "last_pin_timestamp", - deserializer=conversions.parse_iso_8601_ts, - return_value=mock_timestamp, - ) - ) - stack.enter_context(mock.patch.object(users.User, "deserialize", return_value=mock_user)) - stack.enter_context(mock.patch.object(channels.PermissionOverwrite, "deserialize", return_value=mock_overwrite)) - with stack: - base_channel_payload = channel.BaseChannelEvent.deserialize(test_base_channel_payload) - channels.PermissionOverwrite.deserialize.assert_called_once_with(test_overwrite_payload) - users.User.deserialize.assert_called_once_with(test_user_payload) - patched_timestamp_deserializer.assert_called_once_with("2019-05-17T06:26:56.936000+00:00") - assert base_channel_payload.type is channels.ChannelType.GUILD_VOICE - assert base_channel_payload.guild_id == 69240 - assert base_channel_payload.position == 7 - assert base_channel_payload.permission_overwrites == {292929: mock_overwrite} - assert base_channel_payload.name == "Name" - assert base_channel_payload.topic == "Topically drunk" - assert base_channel_payload.is_nsfw is True - assert base_channel_payload.last_message_id == 22222222 - assert base_channel_payload.bitrate == 96000 - assert base_channel_payload.user_limit == 42 - assert base_channel_payload.rate_limit_per_user == datetime.timedelta(seconds=2333) - assert base_channel_payload.recipients == {2929292: mock_user} - assert base_channel_payload.icon_hash == "sdodsooioio2oi" - assert base_channel_payload.owner_id == 32939393 - assert base_channel_payload.application_id == 202020202 - assert base_channel_payload.parent_id == 2030302939 - assert base_channel_payload.last_pin_timestamp is mock_timestamp - - -# Doesn't declare any new fields. -class TestChannelCreateEvent: - ... - - -# Doesn't declare any new fields. -class TestChannelUpdateEvent: - ... - - -# Doesn't declare any new fields. -class TestChannelDeleteEvent: - ... - - -class TestChannelPinsUpdateEvent: - @pytest.fixture() - def test_chanel_pin_update_payload(self): - return { - "guild_id": "424242", - "channel": "29292929", - "last_pin_timestamp": "2020-03-20T16:08:25.412000+00:00", - } - - def test_deserialize(self, test_chanel_pin_update_payload): - mock_timestamp = mock.MagicMock(datetime.datetime) - with _helpers.patch_marshal_attr( - channel.ChannelPinsUpdateEvent, - "last_pin_timestamp", - deserializer=conversions.parse_iso_8601_ts, - return_value=mock_timestamp, - ) as patched_iso_parser: - channel_pin_add_obj = channel.ChannelPinsUpdateEvent.deserialize(test_chanel_pin_update_payload) - patched_iso_parser.assert_called_once_with("2020-03-20T16:08:25.412000+00:00") - assert channel_pin_add_obj.guild_id == 424242 - assert channel_pin_add_obj.channel_id == 29292929 - assert channel_pin_add_obj.last_pin_timestamp is mock_timestamp - - -class TestWebhookUpdateEvent: - @pytest.fixture() - def test_webhook_update_payload(self): - return {"guild_id": "2929292", "channel": "94949494"} - - def test_deserialize(self, test_webhook_update_payload): - webhook_update_obj = channel.WebhookUpdateEvent.deserialize(test_webhook_update_payload) - assert webhook_update_obj.guild_id == 2929292 - assert webhook_update_obj.channel_id == 94949494 - - -class TestTypingStartEvent: - @pytest.fixture() - def test_member_payload(self, test_user_payload): - return { - "user": test_user_payload, - "nick": "Agent 42", - "roles": [], - "joined_at": "2015-04-26T06:26:56.936000+00:00", - "premium_since": "2019-05-17T06:26:56.936000+00:00", - "deaf": True, - "mute": False, - } - - @pytest.fixture() - def test_typing_start_event_payload(self, test_member_payload): - return { - "channel": "123123123", - "guild_id": "33333333", - "user_id": "2020202", - "timestamp": 1231231231, - "member": test_member_payload, - } - - def test_deserialize(self, test_typing_start_event_payload, test_member_payload): - mock_member = mock.MagicMock(guilds.GuildMember) - mock_datetime = mock.MagicMock(datetime.datetime) - stack = contextlib.ExitStack() - mock_member_deserialize = stack.enter_context( - _helpers.patch_marshal_attr( - channel.TypingStartEvent, - "member", - deserializer=guilds.GuildMember.deserialize, - return_value=mock_member, - ) - ) - stack.enter_context( - mock.patch.object(datetime, "datetime", fromtimestamp=mock.MagicMock(return_value=mock_datetime)) - ) - with stack: - typing_start_event_obj = channel.TypingStartEvent.deserialize(test_typing_start_event_payload) - datetime.datetime.fromtimestamp.assert_called_once_with(1231231231, datetime.timezone.utc) - mock_member_deserialize.assert_called_once_with(test_member_payload) - assert typing_start_event_obj.channel_id == 123123123 - assert typing_start_event_obj.guild_id == 33333333 - assert typing_start_event_obj.user_id == 2020202 - assert typing_start_event_obj.timestamp is mock_datetime - assert typing_start_event_obj.member is mock_member - - -class TestInviteCreateEvent: - @pytest.fixture() - def test_invite_create_payload(self, test_user_payload): - return { - "channel": "939393", - "code": "owouwuowouwu", - "created_at": "2019-05-17T06:26:56.936000+00:00", - "guild_id": "45949", - "inviter": test_user_payload, - "max_age": 42, - "max_uses": 69, - "target_user": {"id": "420", "username": "blah", "discriminator": "4242", "avatar": "ha"}, - "target_user_type": 1, - "temporary": True, - "uses": 42, - } - - def test_deserialize(self, test_invite_create_payload, test_user_payload): - mock_inviter = mock.MagicMock(users.User) - mock_target = mock.MagicMock(users.User) - mock_created_at = mock.MagicMock(datetime.datetime) - stack = contextlib.ExitStack() - patched_inviter_deserializer = stack.enter_context( - _helpers.patch_marshal_attr( - channel.InviteCreateEvent, "inviter", deserializer=users.User.deserialize, return_value=mock_inviter - ) - ) - patched_target_deserializer = stack.enter_context( - _helpers.patch_marshal_attr( - channel.InviteCreateEvent, "target_user", deserializer=users.User.deserialize, return_value=mock_target - ) - ) - patched_created_at_deserializer = stack.enter_context( - _helpers.patch_marshal_attr( - channel.InviteCreateEvent, - "created_at", - deserializer=conversions.parse_iso_8601_ts, - return_value=mock_created_at, - ) - ) - with stack: - invite_create_obj = channel.InviteCreateEvent.deserialize(test_invite_create_payload) - patched_created_at_deserializer.assert_called_once_with("2019-05-17T06:26:56.936000+00:00") - patched_target_deserializer.assert_called_once_with( - {"id": "420", "username": "blah", "discriminator": "4242", "avatar": "ha"} - ) - patched_inviter_deserializer.assert_called_once_with(test_user_payload) - assert invite_create_obj.channel_id == 939393 - assert invite_create_obj.code == "owouwuowouwu" - assert invite_create_obj.created_at is mock_created_at - assert invite_create_obj.guild_id == 45949 - assert invite_create_obj.inviter is mock_inviter - assert invite_create_obj.max_age == datetime.timedelta(seconds=42) - assert invite_create_obj.max_uses == 69 - assert invite_create_obj.target_user is mock_target - assert invite_create_obj.target_user_type is invites.TargetUserType.STREAM - assert invite_create_obj.is_temporary is True - assert invite_create_obj.uses == 42 - - def test_max_age_when_zero(self, test_invite_create_payload): - test_invite_create_payload["max_age"] = 0 - assert channel.InviteCreateEvent.deserialize(test_invite_create_payload).max_age is None - - -class TestInviteDeleteEvent: - @pytest.fixture() - def test_invite_delete_payload(self): - return {"channel": "393939", "code": "blahblahblah", "guild_id": "3834833"} - - def test_deserialize(self, test_invite_delete_payload): - invite_delete_obj = channel.InviteDeleteEvent.deserialize(test_invite_delete_payload) - assert invite_delete_obj.channel_id == 393939 - assert invite_delete_obj.code == "blahblahblah" - assert invite_delete_obj.guild_id == 3834833 diff --git a/tests/hikari/events/test_guild.py b/tests/hikari/events/test_guild.py deleted file mode 100644 index 614f275f90..0000000000 --- a/tests/hikari/events/test_guild.py +++ /dev/null @@ -1,245 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# Copyright © Nekoka.tt 2019-2020 -# -# This file is part of Hikari. -# -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . -import contextlib -import datetime - -import mock -import pytest - -from hikari.events import guild -from hikari.internal import conversions -from hikari.models import emojis -from hikari.models import guilds -from hikari.models import unset -from hikari.models import users -from tests.hikari import _helpers - - -@pytest.fixture() -def test_user_payload(): - return {"id": "2929292", "username": "agent 69", "discriminator": "4444", "avatar": "9292929292929292"} - - -@pytest.fixture() -def test_guild_payload(): - return {"id": "40404040", "name": "electric guild boogaloo"} - - -@pytest.fixture() -def test_member_payload(test_user_payload): - return { - "user": test_user_payload, - "nick": "Agent 42", - "roles": [], - "joined_at": "2015-04-26T06:26:56.936000+00:00", - "premium_since": "2019-05-17T06:26:56.936000+00:00", - "deaf": True, - "mute": False, - } - - -# Doesn't declare any new fields. -class TestGuildCreateEvent: - ... - - -# Doesn't declare any new fields. -class TestGuildUpdateEvent: - ... - - -# Doesn't declare any new fields. -class GuildLeaveEvent: - ... - - -# Doesn't declare any new fields. -class GuildUnavailableEvent: - ... - - -class TestBaseGuildBanEvent: - @pytest.fixture() - def test_guild_ban_payload(self, test_user_payload): - return {"user": test_user_payload, "guild_id": "5959"} - - def test_deserialize(self, test_guild_ban_payload, test_user_payload): - mock_user = mock.MagicMock(users.User) - with _helpers.patch_marshal_attr( - guild.BaseGuildBanEvent, "user", deserializer=users.User.deserialize, return_value=mock_user - ) as patched_user_deserializer: - base_guild_ban_object = guild.BaseGuildBanEvent.deserialize(test_guild_ban_payload) - patched_user_deserializer.assert_called_once_with(test_user_payload) - assert base_guild_ban_object.user is mock_user - assert base_guild_ban_object.guild_id == 5959 - - -# Doesn't declare any new fields. -class TestGuildBanAddEvent: - ... - - -# Doesn't declare any new fields. -class TestGuildBanRemoveEvent: - ... - - -class TestGuildEmojisUpdateEvent: - @pytest.fixture() - def test_emoji_payload(self): - return {"id": "4242", "name": "blahblah", "animated": True} - - @pytest.fixture() - def test_guild_emojis_update_payload(self, test_emoji_payload): - return {"emojis": [test_emoji_payload], "guild_id": "696969"} - - def test_deserialize(self, test_guild_emojis_update_payload, test_emoji_payload): - mock_emoji = _helpers.mock_model(emojis.KnownCustomEmoji, id=4242) - with mock.patch.object(emojis.KnownCustomEmoji, "deserialize", return_value=mock_emoji): - guild_emojis_update_obj = guild.GuildEmojisUpdateEvent.deserialize(test_guild_emojis_update_payload) - emojis.KnownCustomEmoji.deserialize.assert_called_once_with(test_emoji_payload) - assert guild_emojis_update_obj.emojis == {mock_emoji.id: mock_emoji} - assert guild_emojis_update_obj.guild_id == 696969 - - -class TestGuildIntegrationsUpdateEvent: - def test_deserialize(self): - assert guild.GuildIntegrationsUpdateEvent.deserialize({"guild_id": "1234"}).guild_id == 1234 - - -class TestGuildMemberAddEvent: - @pytest.fixture() - def test_guild_member_add_payload(self, test_member_payload): - return {**test_member_payload, "guild_id": "292929"} - - def test_deserialize(self, test_guild_member_add_payload): - guild_member_add_obj = guild.GuildMemberAddEvent.deserialize(test_guild_member_add_payload) - assert guild_member_add_obj.guild_id == 292929 - - -class TestGuildMemberRemoveEvent: - @pytest.fixture() - def test_guild_member_remove_payload(self, test_user_payload): - return {"guild_id": "9494949", "user": test_user_payload} - - def test_deserialize(self, test_guild_member_remove_payload, test_user_payload): - mock_user = mock.MagicMock(users.User) - with _helpers.patch_marshal_attr( - guild.GuildMemberRemoveEvent, "user", deserializer=users.User.deserialize, return_value=mock_user - ) as patched_user_deseializer: - guild_member_remove_payload = guild.GuildMemberRemoveEvent.deserialize(test_guild_member_remove_payload) - patched_user_deseializer.assert_called_once_with(test_user_payload) - assert guild_member_remove_payload.guild_id == 9494949 - assert guild_member_remove_payload.user is mock_user - - -class TestGuildMemberUpdateEvent: - @pytest.fixture() - def guild_member_update_payload(self, test_user_payload): - return { - "guild_id": "292929", - "roles": ["213", "412"], - "user": test_user_payload, - "nick": "konnichiwa", - "premium_since": "2019-05-17T06:26:56.936000+00:00", - } - - def test_deserialize(self, guild_member_update_payload, test_user_payload): - mock_user = mock.MagicMock(users.User) - mock_premium_since = mock.MagicMock(datetime.datetime) - stack = contextlib.ExitStack() - patched_user_deserializer = stack.enter_context( - _helpers.patch_marshal_attr( - guild.GuildMemberUpdateEvent, "user", deserializer=users.User.deserialize, return_value=mock_user - ) - ) - patched_premium_since_deserializer = stack.enter_context( - _helpers.patch_marshal_attr( - guild.GuildMemberUpdateEvent, - "premium_since", - deserializer=conversions.parse_iso_8601_ts, - return_value=mock_premium_since, - ) - ) - with stack: - guild_member_update_obj = guild.GuildMemberUpdateEvent.deserialize(guild_member_update_payload) - patched_premium_since_deserializer.assert_called_once_with("2019-05-17T06:26:56.936000+00:00") - patched_user_deserializer.assert_called_once_with(test_user_payload) - assert guild_member_update_obj.guild_id == 292929 - assert guild_member_update_obj.role_ids == [213, 412] - assert guild_member_update_obj.user is mock_user - assert guild_member_update_obj.nickname == "konnichiwa" - assert guild_member_update_obj.premium_since is mock_premium_since - - def test_partial_deserializer(self, guild_member_update_payload): - del guild_member_update_payload["nick"] - del guild_member_update_payload["premium_since"] - with _helpers.patch_marshal_attr(guild.GuildMemberUpdateEvent, "user", deserializer=users.User.deserialize): - guild_member_update_obj = guild.GuildMemberUpdateEvent.deserialize(guild_member_update_payload) - assert guild_member_update_obj.nickname is unset.UNSET - assert guild_member_update_obj.premium_since is unset.UNSET - - -@pytest.fixture() -def test_guild_role_create_update_payload(test_guild_payload): - return {"guild_id": "69240", "role": test_guild_payload} - - -class TestGuildRoleCreateEvent: - def test_deserialize(self, test_guild_role_create_update_payload, test_guild_payload): - mock_role = mock.MagicMock(guilds.Role) - with _helpers.patch_marshal_attr( - guild.GuildRoleCreateEvent, "role", deserializer=guilds.Role.deserialize, return_value=mock_role - ) as patched_role_deserializer: - guild_role_create_obj = guild.GuildRoleCreateEvent.deserialize(test_guild_role_create_update_payload) - patched_role_deserializer.assert_called_once_with(test_guild_payload) - assert guild_role_create_obj.role is mock_role - assert guild_role_create_obj.guild_id == 69240 - - -class TestGuildRoleUpdateEvent: - @pytest.fixture() - def test_guild_role_create_fixture(self, test_guild_payload): - return {"guild_id": "69240", "role": test_guild_payload} - - def test_deserialize(self, test_guild_role_create_update_payload, test_guild_payload): - mock_role = mock.MagicMock(guilds.Role) - with _helpers.patch_marshal_attr( - guild.GuildRoleUpdateEvent, "role", deserializer=guilds.Role.deserialize, return_value=mock_role - ) as patched_role_deserializer: - guild_role_create_obj = guild.GuildRoleUpdateEvent.deserialize(test_guild_role_create_update_payload) - patched_role_deserializer.assert_called_once_with(test_guild_payload) - assert guild_role_create_obj.role is mock_role - assert guild_role_create_obj.guild_id == 69240 - - -class TestGuildRoleDeleteEvent: - @pytest.fixture() - def test_guild_role_delete_payload(self): - return {"guild_id": "424242", "role_id": "94595959"} - - def test_deserialize(self, test_guild_role_delete_payload): - guild_role_delete_payload = guild.GuildRoleDeleteEvent.deserialize(test_guild_role_delete_payload) - assert guild_role_delete_payload.guild_id == 424242 - assert guild_role_delete_payload.role_id == 94595959 - - -# Doesn't declare any new fields. -class TestPresenceUpdateEvent: - ... diff --git a/tests/hikari/events/test_message.py b/tests/hikari/events/test_message.py deleted file mode 100644 index 69050d29a9..0000000000 --- a/tests/hikari/events/test_message.py +++ /dev/null @@ -1,400 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# Copyright © Nekoka.tt 2019-2020 -# -# This file is part of Hikari. -# -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . -import contextlib -import datetime - -import mock -import pytest - -from hikari.events import message -from hikari.internal import conversions -from hikari.models import applications -from hikari.models import embeds -from hikari.models import emojis -from hikari.models import guilds -from hikari.models import messages -from hikari.models import unset -from hikari.models import users -from tests.hikari import _helpers - - -@pytest.fixture() -def test_emoji_payload(): - return {"id": "4242", "name": "blahblah", "animated": True} - - -@pytest.fixture() -def test_user_payload(): - return {"id": "2929292", "username": "agent 69", "discriminator": "4444", "avatar": "9292929292929292"} - - -@pytest.fixture() -def test_member_payload(test_user_payload): - return { - "user": test_user_payload, - "nick": "Agent 42", - "roles": [], - "joined_at": "2015-04-26T06:26:56.936000+00:00", - "premium_since": "2019-05-17T06:26:56.936000+00:00", - "deaf": True, - "mute": False, - } - - -@pytest.fixture() -def test_channel_payload(): - return {"id": "393939", "name": "a channel", "type": 2} - - -# Doesn't declare any new fields. -class TestMessageCreateEvent: - ... - - -class TestMessageUpdateEvent: - @pytest.fixture() - def test_attachment_payload(self): - return { - "id": "4242", - "filename": "nyaa.png", - "size": 1024, - "url": "heck.heck", - "_proxy_url": "proxy.proxy?heck", - "height": 42, - "width": 84, - } - - @pytest.fixture() - def test_embed_payload(self): - return {"title": "42", "description": "blah blah blah"} - - @pytest.fixture() - def test_reaction_payload(self): - return {"count": 69, "me": True, "emoji": "🤣"} - - @pytest.fixture() - def test_activity_payload(self): - return {"type": 1, "party_id": "spotify:23123123"} - - @pytest.fixture() - def test_application_payload(self): - return {"id": "292929", "icon": None, "description": "descript", "name": "A name"} - - @pytest.fixture() - def test_reference_payload(self): - return {"channel": "432341231231"} - - @pytest.fixture() - def test_message_update_payload( - self, - test_user_payload, - test_member_payload, - test_attachment_payload, - test_embed_payload, - test_reaction_payload, - test_activity_payload, - test_application_payload, - test_reference_payload, - test_channel_payload, - ): - return { - "id": "3939399393", - "channel": "93939393939", - "guild_id": "66557744883399", - "author": test_user_payload, - "member": test_member_payload, - "content": "THIS IS A CONTENT", - "timestamp": "2019-05-17T06:26:56.936000+00:00", - "edited_timestamp": "2019-05-17T06:58:56.936000+00:00", - "tts": True, - "mention_everyone": True, - "mentions": [test_user_payload], - "mention_roles": ["123"], - "mention_channels": [test_channel_payload], - "attachments": [test_attachment_payload], - "embeds": [test_embed_payload], - "reactions": [test_reaction_payload], - "nonce": "6454345345345345", - "pinned": True, - "webhook_id": "212231231232123", - "type": 2, - "activity": test_activity_payload, - "application": test_application_payload, - "message_reference": test_reference_payload, - "flags": 3, - } - - def test_deserialize( - self, - test_message_update_payload, - test_user_payload, - test_member_payload, - test_activity_payload, - test_application_payload, - test_reference_payload, - test_attachment_payload, - test_embed_payload, - test_reaction_payload, - ): - mock_author = mock.MagicMock(users.User) - mock_member = mock.MagicMock(guilds.GuildMember) - mock_timestamp = mock.MagicMock(datetime.datetime) - mock_edited_timestamp = mock.MagicMock(datetime.datetime) - mock_attachment = mock.MagicMock(messages.Attachment) - mock_embed = mock.MagicMock(embeds.Embed) - mock_reaction = mock.MagicMock(messages.Reaction) - mock_activity = mock.MagicMock(messages.MessageActivity) - mock_application = mock.MagicMock(applications.Application) - mock_reference = mock.MagicMock(messages.MessageCrosspost) - stack = contextlib.ExitStack() - patched_author_deserializer = stack.enter_context( - _helpers.patch_marshal_attr( - message.MessageUpdateEvent, "author", deserializer=users.User.deserialize, return_value=mock_author - ) - ) - patched_member_deserializer = stack.enter_context( - _helpers.patch_marshal_attr( - message.MessageUpdateEvent, - "member", - deserializer=guilds.GuildMember.deserialize, - return_value=mock_member, - ) - ) - patched_timestamp_deserializer = stack.enter_context( - _helpers.patch_marshal_attr( - message.MessageUpdateEvent, - "timestamp", - deserializer=conversions.parse_iso_8601_ts, - return_value=mock_timestamp, - ) - ) - patched_edit_deserializer = stack.enter_context( - _helpers.patch_marshal_attr( - message.MessageUpdateEvent, - "edited_timestamp", - deserializer=conversions.parse_iso_8601_ts, - return_value=mock_edited_timestamp, - ) - ) - patched_activity_deserializer = stack.enter_context( - _helpers.patch_marshal_attr( - message.MessageUpdateEvent, - "activity", - deserializer=messages.MessageActivity.deserialize, - return_value=mock_activity, - ) - ) - patched_application_deserializer = stack.enter_context( - _helpers.patch_marshal_attr( - message.MessageUpdateEvent, - "application", - deserializer=applications.Application.deserialize, - return_value=mock_application, - ) - ) - patched_reference_deserializer = stack.enter_context( - _helpers.patch_marshal_attr( - message.MessageUpdateEvent, - "message_reference", - deserializer=messages.MessageCrosspost.deserialize, - return_value=mock_reference, - ) - ) - stack.enter_context(mock.patch.object(messages.Attachment, "deserialize", return_value=mock_attachment)) - stack.enter_context(mock.patch.object(embeds.Embed, "deserialize", return_value=mock_embed)) - stack.enter_context(mock.patch.object(messages.Reaction, "deserialize", return_value=mock_reaction)) - with stack: - message_update_payload = message.MessageUpdateEvent.deserialize(test_message_update_payload) - messages.Reaction.deserialize.assert_called_once_with(test_reaction_payload) - embeds.Embed.deserialize.assert_called_once_with(test_embed_payload) - messages.Attachment.deserialize.assert_called_once_with(test_attachment_payload) - patched_reference_deserializer.assert_called_once_with(test_reference_payload) - patched_application_deserializer.assert_called_once_with(test_application_payload) - patched_activity_deserializer.assert_called_once_with(test_activity_payload) - patched_edit_deserializer.assert_called_once_with("2019-05-17T06:58:56.936000+00:00") - patched_timestamp_deserializer.assert_called_once_with("2019-05-17T06:26:56.936000+00:00") - patched_member_deserializer.assert_called_once_with(test_member_payload) - patched_author_deserializer.assert_called_once_with(test_user_payload) - assert message_update_payload.channel_id == 93939393939 - assert message_update_payload.guild_id == 66557744883399 - assert message_update_payload.author is mock_author - assert message_update_payload.member is mock_member - assert message_update_payload.content == "THIS IS A CONTENT" - assert message_update_payload.timestamp is mock_timestamp - assert message_update_payload.edited_timestamp is mock_edited_timestamp - assert message_update_payload.is_tts is True - assert message_update_payload.is_mentioning_everyone is True - assert message_update_payload.user_mentions == {2929292} - assert message_update_payload.role_mentions == {123} - assert message_update_payload.channel_mentions == {393939} - assert message_update_payload.attachments == [mock_attachment] - assert message_update_payload.embeds == [mock_embed] - assert message_update_payload.reactions == [mock_reaction] - assert message_update_payload.is_pinned is True - assert message_update_payload.webhook_id == 212231231232123 - assert message_update_payload.type is messages.MessageType.RECIPIENT_REMOVE - assert message_update_payload.activity is mock_activity - assert message_update_payload.application is mock_application - assert message_update_payload.message_reference is mock_reference - assert message_update_payload.flags == messages.MessageFlag.CROSSPOSTED | messages.MessageFlag.IS_CROSSPOST - assert message_update_payload.nonce == "6454345345345345" - - def test_partial_message_update(self): - message_update_obj = message.MessageUpdateEvent.deserialize({"id": "393939", "channel": "434949"}) - for key in message_update_obj.__slots__: - if key in ("id", "channel"): - continue - assert getattr(message_update_obj, key) is unset.UNSET - assert message_update_obj.id == 393939 - assert message_update_obj.channel_id == 434949 - - -class TestMessageDeleteEvent: - @pytest.fixture() - def test_message_delete_payload(self): - return {"channel": "20202020", "id": "2929", "guild_id": "1010101"} - - def test_deserialize(self, test_message_delete_payload): - message_delete_obj = message.MessageDeleteEvent.deserialize(test_message_delete_payload) - assert message_delete_obj.channel_id == 20202020 - assert message_delete_obj.message_id == 2929 - assert message_delete_obj.guild_id == 1010101 - - -class TestMessageDeleteBulkEvent: - @pytest.fixture() - def test_message_delete_bulk_payload(self): - return {"channel": "20202020", "ids": ["2929", "4394"], "guild_id": "1010101"} - - def test_deserialize(self, test_message_delete_bulk_payload): - message_delete_bulk_obj = message.MessageDeleteBulkEvent.deserialize(test_message_delete_bulk_payload) - assert message_delete_bulk_obj.channel_id == 20202020 - assert message_delete_bulk_obj.guild_id == 1010101 - assert message_delete_bulk_obj.message_ids == {2929, 4394} - - -class TestMessageReactionAddEvent: - @pytest.fixture() - def test_message_reaction_add_payload(self, test_member_payload, test_emoji_payload): - return { - "user_id": "9494949", - "channel": "4393939", - "message_id": "2993993", - "guild_id": "49494949", - "member": test_member_payload, - "emoji": test_emoji_payload, - } - - def test_deserialize(self, test_message_reaction_add_payload, test_member_payload, test_emoji_payload): - mock_member = mock.MagicMock(guilds.GuildMember) - mock_emoji = mock.MagicMock(emojis.CustomEmoji) - stack = contextlib.ExitStack() - patched_member_deserializer = stack.enter_context( - _helpers.patch_marshal_attr( - message.MessageReactionAddEvent, - "member", - deserializer=guilds.GuildMember.deserialize, - return_value=mock_member, - ) - ) - patched_emoji_deserializer = stack.enter_context( - _helpers.patch_marshal_attr( - message.MessageReactionAddEvent, - "emoji", - deserializer=emojis.deserialize_reaction_emoji, - return_value=mock_emoji, - ) - ) - with stack: - message_reaction_add_obj = message.MessageReactionAddEvent.deserialize(test_message_reaction_add_payload) - patched_emoji_deserializer.assert_called_once_with(test_emoji_payload) - patched_member_deserializer.assert_called_once_with(test_member_payload) - assert message_reaction_add_obj.user_id == 9494949 - assert message_reaction_add_obj.channel_id == 4393939 - assert message_reaction_add_obj.message_id == 2993993 - assert message_reaction_add_obj.guild_id == 49494949 - assert message_reaction_add_obj.member is mock_member - assert message_reaction_add_obj.emoji is mock_emoji - - -class TestMessageReactionRemoveEvent: - @pytest.fixture() - def test_message_reaction_remove_payload(self, test_emoji_payload): - return { - "user_id": "9494949", - "channel": "4393939", - "message_id": "2993993", - "guild_id": "49494949", - "emoji": test_emoji_payload, - } - - def test_deserialize(self, test_message_reaction_remove_payload, test_emoji_payload): - mock_emoji = mock.MagicMock(emojis.CustomEmoji) - with _helpers.patch_marshal_attr( - message.MessageReactionRemoveEvent, - "emoji", - deserializer=emojis.deserialize_reaction_emoji, - return_value=mock_emoji, - ) as patched_emoji_deserializer: - message_reaction_remove_obj = message.MessageReactionRemoveEvent.deserialize( - test_message_reaction_remove_payload - ) - patched_emoji_deserializer.assert_called_once_with(test_emoji_payload) - assert message_reaction_remove_obj.user_id == 9494949 - assert message_reaction_remove_obj.channel_id == 4393939 - assert message_reaction_remove_obj.message_id == 2993993 - assert message_reaction_remove_obj.guild_id == 49494949 - assert message_reaction_remove_obj.emoji is mock_emoji - - -class TestMessageReactionRemoveAllEvent: - @pytest.fixture() - def test_reaction_remove_all_payload(self): - return {"channel": "3493939", "message_id": "944949", "guild_id": "49494949"} - - def test_deserialize(self, test_reaction_remove_all_payload): - message_reaction_remove_all_obj = message.MessageReactionRemoveAllEvent.deserialize( - test_reaction_remove_all_payload - ) - assert message_reaction_remove_all_obj.channel_id == 3493939 - assert message_reaction_remove_all_obj.message_id == 944949 - assert message_reaction_remove_all_obj.guild_id == 49494949 - - -class TestMessageReactionRemoveEmojiEvent: - @pytest.fixture() - def test_message_reaction_remove_emoji_payload(self, test_emoji_payload): - return {"channel": "4393939", "message_id": "2993993", "guild_id": "49494949", "emoji": test_emoji_payload} - - def test_deserialize(self, test_message_reaction_remove_emoji_payload, test_emoji_payload): - mock_emoji = mock.MagicMock(emojis.CustomEmoji) - with _helpers.patch_marshal_attr( - message.MessageReactionRemoveEmojiEvent, - "emoji", - deserializer=emojis.deserialize_reaction_emoji, - return_value=mock_emoji, - ) as patched_emoji_deserializer: - message_reaction_remove_emoji_obj = message.MessageReactionRemoveEmojiEvent.deserialize( - test_message_reaction_remove_emoji_payload - ) - patched_emoji_deserializer.assert_called_once_with(test_emoji_payload) - assert message_reaction_remove_emoji_obj.channel_id == 4393939 - assert message_reaction_remove_emoji_obj.message_id == 2993993 - assert message_reaction_remove_emoji_obj.guild_id == 49494949 - assert message_reaction_remove_emoji_obj.emoji is mock_emoji diff --git a/tests/hikari/events/test_other.py b/tests/hikari/events/test_other.py deleted file mode 100644 index 9bb763b15b..0000000000 --- a/tests/hikari/events/test_other.py +++ /dev/null @@ -1,128 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# Copyright © Nekoka.tt 2019-2020 -# -# This file is part of Hikari. -# -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . -import contextlib - -import mock -import pytest - -from hikari.events import other -from hikari.models import guilds -from hikari.models import users -from tests.hikari import _helpers - - -# Synthetic event, is not deserialized -class TestConnectedEvent: - ... - - -# Synthetic event, is not deserialized -class TestDisconnectedEvent: - ... - - -# Synthetic event, is not deserialized -class TestReconnectedEvent: - ... - - -# Synthetic event, is not deserialized -class TestStartedEvent: - ... - - -# Synthetic event, is not deserialized -class TestStoppingEvent: - ... - - -# Synthetic event, is not deserialized -class TestStoppedEvent: - ... - - -class TestReadyEvent: - @pytest.fixture() - def test_guild_payload(self): - return {"id": "40404040", "name": "electric guild boogaloo"} - - @pytest.fixture() - def test_user_payload(self): - return {"id": "2929292", "username": "agent 69", "discriminator": "4444", "avatar": "9292929292929292"} - - @pytest.fixture() - def test_read_event_payload(self, test_guild_payload, test_user_payload): - return { - "v": 69420, - "user": test_user_payload, - "private_channels": [], - "guilds": [test_guild_payload], - "session_id": "osdkoiiodsaooeiwio9", - "shard": [42, 80], - } - - def test_deserialize(self, test_read_event_payload, test_guild_payload, test_user_payload): - mock_guild = mock.MagicMock(guilds.Guild, id=40404040) - mock_user = mock.MagicMock(users.MyUser) - stack = contextlib.ExitStack() - stack.enter_context(mock.patch.object(guilds.UnavailableGuild, "deserialize", return_value=mock_guild)) - patched_user_deserialize = stack.enter_context( - _helpers.patch_marshal_attr( - other.ReadyEvent, "my_user", deserializer=users.MyUser.deserialize, return_value=mock_user - ) - ) - with stack: - ready_obj = other.ReadyEvent.deserialize(test_read_event_payload) - patched_user_deserialize.assert_called_once_with(test_user_payload) - guilds.UnavailableGuild.deserialize.assert_called_once_with(test_guild_payload) - assert ready_obj.gateway_version == 69420 - assert ready_obj.my_user is mock_user - assert ready_obj.unavailable_guilds == {40404040: mock_guild} - assert ready_obj.session_id == "osdkoiiodsaooeiwio9" - assert ready_obj._shard_information == (42, 80) - - @pytest.fixture() - def mock_ready_event_obj(self): - return other.ReadyEvent( - gateway_version=None, my_user=None, unavailable_guilds=None, session_id=None, shard_information=(42, 80) - ) - - def test_shard_id_when_information_set(self, mock_ready_event_obj): - assert mock_ready_event_obj.shard_id == 42 - - def test_shard_count_when_information_set(self, mock_ready_event_obj): - assert mock_ready_event_obj.shard_count == 80 - - def test_shard_id_when_information_not_set(self, mock_ready_event_obj): - mock_ready_event_obj._shard_information = None - assert mock_ready_event_obj.shard_id is None - - def test_shard_count_when_information_not_set(self, mock_ready_event_obj): - mock_ready_event_obj._shard_information = None - assert mock_ready_event_obj.shard_count is None - - -# Synthetic event, is not deserialized -class TestResumedEvent: - ... - - -# Doesn't declare any new fields. -class TestMyUserUpdateEvent: - ... diff --git a/tests/hikari/events/test_voice.py b/tests/hikari/events/test_voice.py deleted file mode 100644 index 048981627d..0000000000 --- a/tests/hikari/events/test_voice.py +++ /dev/null @@ -1,33 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# Copyright © Nekoka.tt 2019-2020 -# -# This file is part of Hikari. -# -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . -import pytest - -from hikari.events import voice - - -class TestVoiceServerUpdateEvent: - @pytest.fixture() - def test_voice_server_update_payload(self): - return {"token": "a_token", "guild_id": "303030300303", "endpoint": "smart.loyal.discord.gg"} - - def test_deserialize(self, test_voice_server_update_payload): - voice_server_update_obj = voice.VoiceServerUpdateEvent.deserialize(test_voice_server_update_payload) - assert voice_server_update_obj.token == "a_token" - assert voice_server_update_obj.guild_id == 303030300303 - assert voice_server_update_obj.endpoint == "smart.loyal.discord.gg" diff --git a/tests/hikari/gateway/__init__.py b/tests/hikari/gateway/__init__.py deleted file mode 100644 index 1c1502a5ca..0000000000 --- a/tests/hikari/gateway/__init__.py +++ /dev/null @@ -1,18 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# Copyright © Nekoka.tt 2019-2020 -# -# This file is part of Hikari. -# -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . diff --git a/tests/hikari/gateway/test_client.py b/tests/hikari/gateway/test_client.py deleted file mode 100644 index 7fad021ffd..0000000000 --- a/tests/hikari/gateway/test_client.py +++ /dev/null @@ -1,598 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# Copyright © Nekoka.tt 2019-2020 -# -# This file is part of Hikari. -# -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . -import asyncio -import datetime - -import aiohttp -import async_timeout -import mock -import pytest - -from hikari import http_settings -from hikari import errors -from hikari import application -from hikari.net.gateway import consumers -from hikari.net import gateway as high_level_shards -from hikari.net.gateway import connection as low_level_shards -from hikari.net.gateway import gateway_state -from hikari.internal import codes -from hikari.internal import more_asyncio -from hikari.models import guilds -from tests.hikari import _helpers - - -def _generate_mock_task(exception=None): - class Task(mock.MagicMock): - def __init__(self): - super().__init__() - self._exception = exception - - def exception(self): - return self._exception - - def done(self): - return True - - return Task() - - -@pytest.fixture -def mock_app(): - app = mock.MagicMock() - app.config = mock.MagicMock() - app.event_dispatcher = mock.MagicMock(dispatch_event=mock.MagicMock(return_value=_helpers.AwaitableMock())) - app.event_manager = mock.MagicMock() - app.rest = mock.MagicMock() - app.shards = mock.MagicMock() - return app - - -@pytest.fixture -def shard_client_obj(mock_app): - mock_shard_connection = mock.MagicMock( - low_level_shards.Shard, - heartbeat_latency=float("nan"), - heartbeat_interval=float("nan"), - reconnect_count=0, - seq=None, - session_id=None, - ) - with mock.patch.object(low_level_shards, "Shard", return_value=mock_shard_connection): - return _helpers.unslot_class(high_level_shards.Gateway)(0, 1, mock_app, "some_url") - - -class TestShardClientImpl: - def test_raw_event_consumer_in_ShardClientImpl(self): - class DummyConsumer(consumers.RawEventConsumer): - def process_raw_event(self, _client, name, payload): - return "ASSERT TRUE" - - shard_client_obj = high_level_shards.Gateway( - 0, - 1, - mock.MagicMock( - application.Application, config=http_settings.GatewayConfig(), event_manager=DummyConsumer() - ), - "some_url", - ) - - assert shard_client_obj._connection.dispatcher(shard_client_obj, "TEST", {}) == "ASSERT TRUE" - - def test_connection_is_set(self, shard_client_obj): - mock_shard_connection = mock.MagicMock(low_level_shards.Shard) - - with mock.patch.object(low_level_shards, "Shard", return_value=mock_shard_connection): - shard_client_obj = high_level_shards.Gateway( - 0, - 1, - mock.MagicMock(application.Application, event_manager=None, config=http_settings.GatewayConfig()), - "some_url", - ) - - assert shard_client_obj._connection is mock_shard_connection - - -class TestShardClientImplDelegateProperties: - def test_shard_id(self, shard_client_obj): - marker = object() - shard_client_obj._connection.shard_id = marker - assert shard_client_obj.shard_id is marker - - def test_shard_count(self, shard_client_obj): - marker = object() - shard_client_obj._connection.shard_count = marker - assert shard_client_obj.shard_count is marker - - def test_status(self, shard_client_obj): - marker = object() - shard_client_obj._status = marker - assert shard_client_obj.status is marker - - def test_activity(self, shard_client_obj): - marker = object() - shard_client_obj._activity = marker - assert shard_client_obj.activity is marker - - def test_idle_since(self, shard_client_obj): - marker = object() - shard_client_obj._idle_since = marker - assert shard_client_obj.idle_since is marker - - def test_is_afk(self, shard_client_obj): - marker = object() - shard_client_obj._is_afk = marker - assert shard_client_obj.is_afk is marker - - def test_heartbeat_latency(self, shard_client_obj): - marker = object() - shard_client_obj._connection.heartbeat_latency = marker - assert shard_client_obj.heartbeat_latency is marker - - def test_heartbeat_interval(self, shard_client_obj): - marker = object() - shard_client_obj._connection.heartbeat_interval = marker - assert shard_client_obj.heartbeat_interval is marker - - def test_reconnect_count(self, shard_client_obj): - marker = object() - shard_client_obj._connection.reconnect_count = marker - assert shard_client_obj.reconnect_count is marker - - def test_disconnect_count(self, shard_client_obj): - marker = object() - shard_client_obj._connection.disconnect_count = marker - assert shard_client_obj.disconnect_count is marker - - def test_connection_state(self, shard_client_obj): - marker = object() - shard_client_obj._shard_state = marker - assert shard_client_obj.connection_state is marker - - def test_is_connected(self, shard_client_obj): - marker = object() - shard_client_obj._connection.is_connected = marker - assert shard_client_obj.is_connected is marker - - def test_seq(self, shard_client_obj): - marker = object() - shard_client_obj._connection.seq = marker - assert shard_client_obj.seq is marker - - def test_session_id(self, shard_client_obj): - marker = object() - shard_client_obj._connection.session_id = marker - assert shard_client_obj.session_id is marker - - def test_version(self, shard_client_obj): - marker = object() - shard_client_obj._connection.version = marker - assert shard_client_obj.version is marker - - def test_intents(self, shard_client_obj): - marker = object() - shard_client_obj._connection.intents = marker - assert shard_client_obj.intents is marker - - -class TestShardClientImplStart: - @pytest.mark.asyncio - async def test_start_when_ready_event_completes_first_without_error(self, shard_client_obj): - shard_client_obj._connection.seq = 123 - shard_client_obj._connection.session_id = 123 - stop_event = asyncio.Event() - try: - - async def forever(): - # make this so that it doesn't complete in time; - await stop_event.wait() - - shard_client_obj._keep_alive = mock.MagicMock(wraps=forever) - # Make this last a really long time so it doesn't complete immediately. - shard_client_obj._connection.ready_event = mock.MagicMock(wait=mock.AsyncMock()) - - # Do iiiit. - await shard_client_obj.start() - finally: - stop_event.set() - - @_helpers.assert_raises(type_=LookupError) - @pytest.mark.asyncio - async def test_start_when_ready_event_completes_first_with_error(self, shard_client_obj): - shard_client_obj._connection.seq = 123 - shard_client_obj._connection.session_id = 123 - stop_event = asyncio.Event() - try: - - async def forever(): - # make this so that it doesn't complete in time; - await stop_event.wait() - - shard_client_obj._keep_alive = mock.MagicMock(wraps=forever) - # Make this last a really long time so it doesn't complete immediately. - shard_client_obj._connection.ready_event = mock.MagicMock(wait=mock.AsyncMock(side_effect=LookupError)) - - # Do iiiit. - await shard_client_obj.start() - finally: - stop_event.set() - - @pytest.mark.asyncio - async def test_start_when_task_completes_with_no_exception(self, shard_client_obj): - shard_client_obj._connection.seq = 123 - shard_client_obj._connection.session_id = 123 - stop_event = asyncio.Event() - try: - - async def forever(): - # make this so that it doesn't complete in time; - await stop_event.wait() - - shard_client_obj._keep_alive = mock.AsyncMock() - # Make this last a really long time so it doesn't complete immediately. - shard_client_obj._connection.ready_event = mock.MagicMock(wait=forever) - - # Do iiiit. - await shard_client_obj.start() - finally: - stop_event.set() - - @_helpers.assert_raises(type_=RuntimeError) - @pytest.mark.asyncio - async def test_start_when_task_completes_with_exception(self, shard_client_obj): - shard_client_obj._connection.seq = 123 - shard_client_obj._connection.session_id = 123 - stop_event = asyncio.Event() - try: - - async def forever(): - # make this so that it doesn't complete in time; - await stop_event.wait() - - shard_client_obj._keep_alive = mock.AsyncMock(side_effect=RuntimeError) - # Make this last a really long time so it doesn't complete immediately. - shard_client_obj._connection.ready_event = mock.MagicMock(wait=forever) - - # Do iiiit. - await shard_client_obj.start() - finally: - stop_event.set() - - @_helpers.assert_raises(type_=RuntimeError) - @pytest.mark.asyncio - async def test_start_when_already_started(self, shard_client_obj): - shard_client_obj._shard_state = gateway_state.GatewayState.READY - - await shard_client_obj.start() - - @pytest.mark.asyncio - async def test_join_when__task(self, shard_client_obj): - shard_client_obj._task = _helpers.AwaitableMock() - - await shard_client_obj.join() - - shard_client_obj._task.assert_awaited_once() - - @pytest.mark.asyncio - async def test_join_when_not__task(self, shard_client_obj): - shard_client_obj._task = None - - await shard_client_obj.join() - - @pytest.mark.asyncio - async def test_close(self, shard_client_obj): - shard_client_obj._dispatch = _helpers.AwaitableMock() - shard_client_obj._task = _helpers.AwaitableMock() - - await shard_client_obj.close() - - shard_client_obj._connection.close.assert_called_once() - shard_client_obj._task.assert_awaited_once() - - @pytest.mark.asyncio - async def test_close_when_already_stopping(self, shard_client_obj): - shard_client_obj._shard_state = gateway_state.GatewayState.STOPPING - - await shard_client_obj.close() - - shard_client_obj._connection.close.assert_not_called() - - @pytest.mark.asyncio - async def test_close_when_not_running_is_not_an_error(self, shard_client_obj): - shard_client_obj._shard_state = gateway_state.GatewayState.NOT_RUNNING - shard_client_obj._task = None - - await shard_client_obj.close() - - shard_client_obj._connection.close.assert_called_once() - - @_helpers.timeout_after(5) - @pytest.mark.asyncio - async def test__keep_alive_repeats_silently_if_task_returns(self, shard_client_obj): - shard_client_obj._spin_up = mock.AsyncMock(return_value=more_asyncio.completed_future()) - - try: - async with async_timeout.timeout(1): - await shard_client_obj._keep_alive() - assert False - except asyncio.TimeoutError: - assert shard_client_obj._spin_up.await_count > 0 - - @_helpers.assert_raises(type_=RuntimeError) - @pytest.mark.parametrize( - "error", - [ - aiohttp.ClientConnectorError(mock.MagicMock(), mock.MagicMock()), - ], - ) - @pytest.mark.asyncio - @_helpers.timeout_after(5) - async def test__keep_alive_handles_errors(self, error, shard_client_obj): - should_return = False - - def side_effect(*args): - nonlocal should_return - if should_return: - return _helpers.AwaitableMock(return_value=RuntimeError) - - should_return = True - return _helpers.AwaitableMock(return_value=error) - - shard_client_obj._spin_up = mock.MagicMock(side_effect=side_effect) - - with mock.patch("asyncio.sleep", new=mock.AsyncMock()): - await shard_client_obj._keep_alive() - - @pytest.mark.asyncio - @_helpers.timeout_after(5) - async def test__keep_alive_shuts_down_when_GatewayClientClosedError(self, shard_client_obj): - shard_client_obj._spin_up = mock.AsyncMock( - return_value=_helpers.AwaitableMock(return_value=errors.GatewayClientClosedError) - ) - - with mock.patch("asyncio.sleep", new=mock.AsyncMock()): - await shard_client_obj._keep_alive() - - @_helpers.assert_raises(type_=errors.GatewayServerClosedConnectionError) - @pytest.mark.parametrize( - "code", - [ - codes.GatewayCloseCode.NOT_AUTHENTICATED, - codes.GatewayCloseCode.AUTHENTICATION_FAILED, - codes.GatewayCloseCode.ALREADY_AUTHENTICATED, - codes.GatewayCloseCode.SHARDING_REQUIRED, - codes.GatewayCloseCode.INVALID_VERSION, - codes.GatewayCloseCode.INVALID_INTENT, - codes.GatewayCloseCode.DISALLOWED_INTENT, - ], - ) - @pytest.mark.asyncio - @_helpers.timeout_after(5) - async def test__keep_alive_shuts_down_when_GatewayServerClosedConnectionError(self, code, shard_client_obj): - shard_client_obj._spin_up = mock.AsyncMock( - return_value=_helpers.AwaitableMock(return_value=errors.GatewayServerClosedConnectionError(code)) - ) - - with mock.patch("asyncio.sleep", new=mock.AsyncMock()): - await shard_client_obj._keep_alive() - - @_helpers.assert_raises(type_=RuntimeError) - @pytest.mark.asyncio - @_helpers.timeout_after(5) - async def test__keep_alive_ignores_when_GatewayServerClosedConnectionError_with_other_code(self, shard_client_obj): - should_return = False - - def side_effect(*args): - nonlocal should_return - if should_return: - return _helpers.AwaitableMock(return_value=RuntimeError) - - should_return = True - return _helpers.AwaitableMock( - return_value=errors.GatewayServerClosedConnectionError(codes.GatewayCloseCode.NORMAL_CLOSURE) - ) - - shard_client_obj._spin_up = mock.AsyncMock(side_effect=side_effect) - - with mock.patch("asyncio.sleep", new=mock.AsyncMock()): - await shard_client_obj._keep_alive() - - -class TestShardClientImplSpinUp: - @_helpers.assert_raises(type_=RuntimeError) - @pytest.mark.asyncio - async def test__spin_up_if_connect_task_is_completed_raises_exception_during_hello_event(self, shard_client_obj): - stop_event = asyncio.Event() - try: - - async def forever(): - # make this so that it doesn't complete in time; - await stop_event.wait() - - # Make this last a really long time so it doesn't complete immediately. - shard_client_obj._connection.connect = mock.MagicMock(wraps=forever) - - # Make these finish immediately. - shard_client_obj._connection.hello_event = mock.MagicMock(wait=mock.AsyncMock(side_effect=RuntimeError)) - - # Do iiiit. - await shard_client_obj._spin_up() - finally: - stop_event.set() - - @_helpers.assert_raises(type_=RuntimeError) - @pytest.mark.asyncio - async def test__spin_up_if_connect_task_is_completed_raises_exception_during_identify_event(self, shard_client_obj): - stop_event = asyncio.Event() - try: - - async def forever(): - # make this so that it doesn't complete in time; - await stop_event.wait() - - # Make this last a really long time so it doesn't complete immediately. - shard_client_obj._connection.connect = mock.MagicMock(wraps=forever) - - # Make these finish immediately. - shard_client_obj._connection.hello_event = mock.MagicMock(wait=mock.AsyncMock()) - shard_client_obj._connection.handshake_event = mock.MagicMock(wait=mock.AsyncMock(side_effect=RuntimeError)) - - # Do iiiit. - await shard_client_obj._spin_up() - finally: - stop_event.set() - - @pytest.mark.asyncio - async def test__spin_up_when_resuming(self, shard_client_obj): - shard_client_obj._connection.seq = 123 - shard_client_obj._connection.session_id = 123 - stop_event = asyncio.Event() - try: - - async def forever(): - # make this so that it doesn't complete in time; - await stop_event.wait() - - # Make this last a really long time so it doesn't complete immediately. - shard_client_obj._connection.connect = mock.MagicMock(wraps=forever) - - # Make these finish immediately. - shard_client_obj._connection.hello_event = mock.MagicMock(wait=mock.AsyncMock()) - shard_client_obj._connection.handshake_event = mock.MagicMock(wait=mock.AsyncMock()) - - # Make this one go boom. - shard_client_obj._connection.resumed_event = mock.MagicMock(wait=mock.AsyncMock()) - - # Do iiiit. - await shard_client_obj._spin_up() - finally: - stop_event.set() - - @_helpers.assert_raises(type_=RuntimeError) - @pytest.mark.asyncio - async def test__spin_up_if_connect_task_is_completed_raises_exception_during_resumed_event(self, shard_client_obj): - shard_client_obj._connection.seq = 123 - shard_client_obj._connection.session_id = 123 - stop_event = asyncio.Event() - try: - - async def forever(): - # make this so that it doesn't complete in time; - await stop_event.wait() - - # Make this last a really long time so it doesn't complete immediately. - shard_client_obj._connection.connect = mock.MagicMock(wraps=forever) - - # Make these finish immediately. - shard_client_obj._connection.hello_event = mock.MagicMock(wait=mock.AsyncMock()) - shard_client_obj._connection.handshake_event = mock.MagicMock(wait=mock.AsyncMock()) - - # Make this one go boom. - shard_client_obj._connection.resumed_event = mock.MagicMock(wait=mock.AsyncMock(side_effect=RuntimeError)) - - # Do iiiit. - await shard_client_obj._spin_up() - finally: - stop_event.set() - - @pytest.mark.asyncio - async def test__spin_up_when_not_resuming(self, shard_client_obj): - shard_client_obj._connection.seq = None - shard_client_obj._connection.session_id = None - stop_event = asyncio.Event() - try: - - async def forever(): - # make this so that it doesn't complete in time; - await stop_event.wait() - - # Make this last a really long time so it doesn't complete immediately. - shard_client_obj._connection.connect = mock.MagicMock(wraps=forever) - - # Make these finish immediately. - shard_client_obj._connection.hello_event = mock.MagicMock(wait=mock.AsyncMock()) - shard_client_obj._connection.handshake_event = mock.MagicMock(wait=mock.AsyncMock()) - - # Make this one go boom. - shard_client_obj._connection.ready_event = mock.MagicMock(wait=mock.AsyncMock()) - - # Do iiiit. - await shard_client_obj._spin_up() - finally: - stop_event.set() - - @_helpers.timeout_after(10) - @_helpers.assert_raises(type_=RuntimeError) - @pytest.mark.asyncio - async def test__spin_up_if_connect_task_is_completed_raises_exception_during_ready_event(self, shard_client_obj): - stop_event = asyncio.Event() - try: - - async def forever(): - # make this so that it doesn't complete in time; - await stop_event.wait() - - # Make this last a really long time so it doesn't complete immediately. - shard_client_obj._connection.connect = mock.MagicMock(wraps=forever) - - # Make these finish immediately. - shard_client_obj._connection.hello_event = mock.MagicMock(wait=mock.AsyncMock()) - shard_client_obj._connection.handshake_event = mock.MagicMock(wait=mock.AsyncMock()) - - # Make this one go boom. - shard_client_obj._connection.ready_event = mock.MagicMock(wait=mock.AsyncMock(side_effect=RuntimeError)) - - # Do iiiit. - await shard_client_obj._spin_up() - finally: - stop_event.set() - - -class TestShardClientImplUpdatePresence: - @pytest.mark.asyncio - @_helpers.assert_raises(type_=ValueError) - async def test_update_presence_with_no_arguments(self, shard_client_obj): - await shard_client_obj.update_presence() - - @pytest.mark.asyncio - async def test_update_presence_with_optionals(self, shard_client_obj): - datetime_obj = datetime.datetime.now() - - await shard_client_obj.update_presence( - status=guilds.PresenceStatus.DND, activity=None, idle_since=datetime_obj, is_afk=True - ) - - shard_client_obj._connection.update_presence.assert_called_once_with( - {"status": "dnd", "game": None, "idle_since": datetime_obj.timestamp() * 1000, "afk": True} - ) - - assert shard_client_obj._status == guilds.PresenceStatus.DND - assert shard_client_obj._activity is None - assert shard_client_obj._idle_since == datetime_obj - assert shard_client_obj._is_afk is True - - def test__create_presence_pl(self, shard_client_obj): - datetime_obj = datetime.datetime.now() - returned = shard_client_obj._create_presence_pl(guilds.PresenceStatus.DND, None, datetime_obj, True) - - assert returned == { - "status": "dnd", - "game": None, - "idle_since": datetime_obj.timestamp() * 1000, - "afk": True, - } diff --git a/tests/hikari/gateway/test_connection.py b/tests/hikari/gateway/test_connection.py deleted file mode 100644 index 01ae22a07b..0000000000 --- a/tests/hikari/gateway/test_connection.py +++ /dev/null @@ -1,913 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# Copyright © Nekoka.tt 2019-2020 -# -# This file is part of Hikari. -# -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . -import asyncio -import contextlib -import datetime -import math -import time -import urllib.parse - -import aiohttp -import async_timeout -import mock -import pytest - -from hikari import errors -from hikari.net.gateway import connection -from hikari.internal import codes -from hikari.internal import more_collections -from hikari.net import user_agents -from tests.hikari import _helpers - - -class MockWS: - def __init__(self): - self.args = None - self.kwargs = None - self.aenter = 0 - self.aexit = 0 - - def __call__(self, *args, **kwargs): - self.args = args - self.kwargs = kwargs - return self - - async def __aenter__(self): - self.aenter += 1 - return self - - async def __aexit__(self, exc_type, exc_val, exc_tb): - self.aexit += 1 - return self - - -class MockClientSession: - def __init__(self): - self.args = None - self.kwargs = None - self.aenter = 0 - self.aexit = 0 - self.ws = MockWS() - self.close = mock.AsyncMock() - - def __call__(self, *args, **kwargs): - self.args = args - self.kwargs = kwargs - return self - - async def __aenter__(self): - self.aenter += 1 - return self - - async def __aexit__(self, exc_type, exc_val, exc_tb): - self.aexit += 1 - - async def ws_connect(self, *args, **kwargs): - return self.ws(*args, **kwargs) - - -@pytest.mark.asyncio -class TestShardConstructor: - async def test_init_sets_shard_numbers_correctly(self): - input_shard_id, input_shard_count, expected_shard_id, expected_shard_count = 1, 2, 1, 2 - client = connection.Shard(shard_id=input_shard_id, shard_count=input_shard_count, token="xxx", url="yyy") - assert client.shard_id == expected_shard_id - assert client.shard_count == expected_shard_count - - async def test_dispatch_is_callable(self): - client = connection.Shard(token="xxx", url="yyy") - client.dispatcher(client, "ping", "pong") - - @pytest.mark.parametrize( - ["compression", "expected_url_query"], - [ - (True, dict(v=["6"], encoding=["json"], compress=["zlib-stream"])), - (False, dict(v=["6"], encoding=["json"])), - ], - ) - async def test_compression(self, compression, expected_url_query): - url = "ws://baka-im-not-a-http-url:49620/locate/the/bloody/websocket?ayyyyy=lmao" - client = connection.Shard(token="xxx", url=url, compression=compression) - scheme, netloc, path, params, query, fragment = urllib.parse.urlparse(client.url) - assert scheme == "ws" - assert netloc == "baka-im-not-a-http-url:49620" - assert path == "/locate/the/bloody/websocket" - assert params == "" - actual_query_dict = urllib.parse.parse_qs(query) - assert actual_query_dict == expected_url_query - assert fragment == "" - - async def test_init_hearbeat_defaults_before_startup(self): - client = connection.Shard(token="xxx", url="yyy") - assert math.isnan(client.last_heartbeat_sent) - assert math.isnan(client.heartbeat_latency) - assert math.isnan(client.last_message_received) - - async def test_init_connected_at_is_nan(self): - client = connection.Shard(token="xxx", url="yyy") - assert math.isnan(client.connected_at) - - -@pytest.mark.asyncio -class TestShardUptimeProperty: - @pytest.mark.parametrize( - ["connected_at", "now", "expected_uptime"], - [(float("nan"), 31.0, datetime.timedelta(seconds=0)), (10.0, 31.0, datetime.timedelta(seconds=21.0)),], - ) - async def test_uptime(self, connected_at, now, expected_uptime): - with mock.patch("time.perf_counter", return_value=now): - client = connection.Shard(token="xxx", url="yyy") - client.connected_at = connected_at - assert client.up_time == expected_uptime - - -@pytest.mark.asyncio -class TestShardIsConnectedProperty: - @pytest.mark.parametrize(["connected_at", "is_connected"], [(float("nan"), False), (15, True), (2500.0, True),]) - async def test_is_connected(self, connected_at, is_connected): - client = connection.Shard(token="xxx", url="yyy") - client.connected_at = connected_at - assert client.is_connected is is_connected - - -@pytest.mark.asyncio -class TestGatewayReconnectCountProperty: - @pytest.mark.parametrize( - ["disconnect_count", "is_connected", "expected_reconnect_count"], - [ - (0, False, 0), - (0, True, 0), - (1, False, 0), - (1, True, 1), - (2, False, 1), - (2, True, 2), - (3, False, 2), - (3, True, 3), - ], - ) - async def test_value(self, disconnect_count, is_connected, expected_reconnect_count): - client = connection.Shard(token="xxx", url="yyy") - client.disconnect_count = disconnect_count - client.connected_at = 420 if is_connected else float("nan") - assert client.reconnect_count == expected_reconnect_count - - -@pytest.mark.asyncio -class TestGatewayCurrentPresenceProperty: - async def test_returns_presence(self): - client = connection.Shard(token="xxx", url="yyy") - client._presence = {"foo": "bar"} - assert client.current_presence == {"foo": "bar"} - - async def test_returns_copy(self): - client = connection.Shard(token="xxx", url="yyy") - client._presence = {"foo": "bar"} - assert client.current_presence is not client._presence - - -@pytest.mark.asyncio -class TestConnect: - @property - def hello_payload(self): - return {"op": 10, "d": {"heartbeat_interval": 30_000}} - - @property - def non_hello_payload(self): - return {"op": 69, "d": "yeet"} - - @pytest.fixture - def client(self, event_loop): - asyncio.set_event_loop(event_loop) - client = _helpers.unslot_class(connection.Shard)(url="ws://localhost", token="xxx") - client = _helpers.mock_methods_on(client, except_=("connect",)) - client._receive = mock.AsyncMock(return_value=self.hello_payload) - return client - - @contextlib.contextmanager - def suppress_closure(self): - with contextlib.suppress(errors.GatewayClientClosedError): - yield - - async def test_RuntimeError_if_already_connected(self, client): - client.connected_at = 22.4 # makes client expect to be connected - - try: - with self.suppress_closure(): - await client.connect() - assert False - except RuntimeError: - pass - - assert client._ws is None - client._run.assert_not_called() - client._heartbeat_keep_alive.assert_not_called() - - @pytest.mark.parametrize( - "event_attr", ["closed_event", "handshake_event", "ready_event", "requesting_close_event", "resumed_event"] - ) - @_helpers.timeout_after(10.0) - async def test_events_unset_on_open(self, client, event_attr): - getattr(client, event_attr).set() - with self.suppress_closure(): - task = asyncio.create_task(client.connect()) - # Wait until the first main event object is set. By then we expect - # the event we are testing to have been unset again if it is - # working properly. - await client.hello_event.wait() - assert not getattr(client, event_attr).is_set() - await task - - async def test_hello_event_unset_on_open(self, client): - client.hello_event = mock.MagicMock() - - with self.suppress_closure(): - await client.connect() - - client.hello_event.clear.assert_called_once() - client.hello_event.set.assert_called_once() - - @_helpers.timeout_after(10.0) - async def test_closed_event_set_on_connect_terminate(self, client): - with self.suppress_closure(): - await asyncio.create_task(client.connect()) - - assert client.closed_event.is_set() - - @_helpers.timeout_after(10.0) - async def test_session_opened_with_expected_kwargs(self, client): - with self.suppress_closure(): - await client.connect() - client._create_ws.assert_awaited_once_with(client.url, compress=0, auto_ping=True, max_msg_size=0) - - @_helpers.timeout_after(10.0) - async def test_ws_closed_afterwards(self, client): - with self.suppress_closure(): - await client.connect() - client.close.assert_awaited_with(1000) - - @_helpers.timeout_after(10.0) - async def test_disconnecting_unsets_connected_at(self, client): - assert math.isnan(client.connected_at) - - with mock.patch("time.perf_counter", return_value=420): - with self.suppress_closure(): - await client.connect() - assert math.isnan(client.connected_at) - - @_helpers.timeout_after(10.0) - async def test_disconnecting_unsets_last_message_received(self, client): - assert math.isnan(client.last_message_received) - - with mock.patch("time.perf_counter", return_value=420): - with self.suppress_closure(): - await client.connect() - assert math.isnan(client.last_message_received) - - @_helpers.timeout_after(10.0) - async def test_disconnecting_unsets_last_heartbeat_sent( - self, client, - ): - with self.suppress_closure(): - await client.connect() - assert math.isnan(client.last_heartbeat_sent) - - @_helpers.timeout_after(10.0) - async def test_disconnecting_drops_reference_to_ws(self, client): - with self.suppress_closure(): - await client.connect() - assert client._ws is None - - @_helpers.timeout_after(10.0) - async def test_disconnecting_increments_disconnect_count(self, client): - client.disconnect_count = 69 - with self.suppress_closure(): - await client.connect() - assert client.disconnect_count == 70 - - @_helpers.timeout_after(10.0) - async def test_connecting_dispatches_CONNECTED(self, client): - with self.suppress_closure(): - task = asyncio.create_task(client.connect()) - await client.hello_event.wait() - # sanity check for the DISCONNECTED test - assert mock.call("CONNECTED", more_collections.EMPTY_DICT) in client.do_dispatch.call_args_list - client.do_dispatch.assert_called_with("CONNECTED", more_collections.EMPTY_DICT) - await task - - @_helpers.timeout_after(10.0) - async def test_disconnecting_dispatches_DISCONNECTED(self, client): - with self.suppress_closure(): - task = asyncio.create_task(client.connect()) - await client.hello_event.wait() - assert mock.call("DISCONNECTED", more_collections.EMPTY_DICT) not in client.do_dispatch.call_args_list - await task - client.do_dispatch.assert_called_with("DISCONNECTED", more_collections.EMPTY_DICT) - - @_helpers.timeout_after(10.0) - async def test_new_zlib_each_time(self, client): - assert client._zlib is None - previous_zlib = None - - for i in range(20): - with self.suppress_closure(): - await client.connect() - assert client._zlib is not None - assert previous_zlib is not client._zlib - previous_zlib = client._zlib - client.connected_at = float("nan") - - @_helpers.timeout_after(10.0) - async def test_hello(self, client): - with self.suppress_closure(): - await client.connect() - - client._receive.assert_awaited_once() - - @_helpers.timeout_after(10.0) - @_helpers.assert_raises(type_=errors.GatewayError) - async def test_no_hello_throws_GatewayError(self, client): - client._receive = mock.AsyncMock(return_value=self.non_hello_payload) - await client.connect() - - @_helpers.timeout_after(10.0) - async def test_heartbeat_keep_alive_correctly_started(self, client): - with self.suppress_closure(): - await client.connect() - - client._heartbeat_keep_alive.assert_called_with(self.hello_payload["d"]["heartbeat_interval"] / 1_000.0) - - @_helpers.timeout_after(10.0) - async def test_identify_or_resume_then_poll_events_started(self, client): - with self.suppress_closure(): - await client.connect() - - client._run.assert_called_once() - - @_helpers.timeout_after(10.0) - async def test_waits_indefinitely_if_everything_is_working(self, client): - async def deadlock(*_, **__): - await asyncio.get_running_loop().create_future() - - client._heartbeat_keep_alive = deadlock - client._run = deadlock - - try: - await asyncio.wait_for(client.connect(), timeout=2.5) - assert False - except asyncio.TimeoutError: - pass - - @_helpers.timeout_after(10.0) - async def test_waits_for_run_then_throws_that_exception(self, client): - async def deadlock(*_, **__): - await asyncio.get_running_loop().create_future() - - class ExceptionThing(Exception): - pass - - async def run(): - raise ExceptionThing() - - client._heartbeat_keep_alive = deadlock - client._run = run - - try: - await client.connect() - assert False - except ExceptionThing: - pass - - async def test_waits_for_heartbeat_keep_alive_to_return_then_throws_GatewayClientClosedError(self, client): - async def deadlock(*_, **__): - await asyncio.get_running_loop().create_future() - - async def heartbeat_keep_alive(_): - pass - - client._heartbeat_keep_alive = heartbeat_keep_alive - client._run = deadlock - - try: - await client.connect() - assert False - except errors.GatewayClientClosedError: - pass - - async def test_waits_for_identify_or_resume_then_poll_events_to_return_throws_GatewayClientClosedError( - self, client, - ): - async def deadlock(*_, **__): - await asyncio.get_running_loop().create_future() - - async def run(): - pass - - client._heartbeat_keep_alive = deadlock - client._run = run - - try: - await client.connect() - assert False - except errors.GatewayClientClosedError: - pass - - async def test_TimeoutError_on_heartbeat_keep_alive_raises_GatewayZombiedError(self, client): - async def deadlock(*_, **__): - await asyncio.get_running_loop().create_future() - - async def heartbeat_keep_alive(_): - raise asyncio.TimeoutError("reee") - - client._heartbeat_keep_alive = heartbeat_keep_alive - client._run = deadlock - - try: - await client.connect() - assert False - except errors.GatewayZombiedError: - pass - - async def test_TimeoutError_on_identify_or_resume_then_poll_events_raises_GatewayZombiedError(self, client): - async def deadlock(*_, **__): - await asyncio.get_running_loop().create_future() - - async def run(): - raise asyncio.TimeoutError("reee") - - client._heartbeat_keep_alive = deadlock - client._run = run - - try: - await client.connect() - assert False - except errors.GatewayZombiedError: - pass - - -@pytest.mark.asyncio -class TestShardRun: - @pytest.fixture - def client(self, event_loop): - asyncio.set_event_loop(event_loop) - client = _helpers.unslot_class(connection.Shard)(token="1234", url="xxx") - client = _helpers.mock_methods_on(client, except_=("_run",)) - - def receive(): - client.recv_time = time.perf_counter() - - def identify(): - client.identify_time = time.perf_counter() - - def resume(): - client.resume_time = time.perf_counter() - - client._identify = mock.AsyncMock(spec=connection.Shard._identify, wraps=identify) - client._resume = mock.AsyncMock(spec=connection.Shard._resume, wraps=resume) - client._receive = mock.AsyncMock(spec=connection.Shard._receive, wraps=receive) - return client - - async def test_no_session_id_sends_identify_then_polls_events(self, client): - client.session_id = None - task = asyncio.create_task(client._run()) - await asyncio.sleep(0.25) - try: - client._identify.assert_awaited_once() - client._receive.assert_awaited_once() - client._resume.assert_not_called() - assert client.identify_time <= client.recv_time - finally: - task.cancel() - - async def test_session_id_sends_resume_then_polls_events(self, client): - client.session_id = 69420 - task = asyncio.create_task(client._run()) - await asyncio.sleep(0.25) - try: - client._resume.assert_awaited_once() - client._receive.assert_awaited_once() - client._identify.assert_not_called() - assert client.resume_time <= client.recv_time - finally: - task.cancel() - - -@pytest.mark.asyncio -class TestIdentify: - @pytest.fixture - def client(self, event_loop): - asyncio.set_event_loop(event_loop) - client = _helpers.unslot_class(connection.Shard)(token="1234", url="xxx") - client = _helpers.mock_methods_on(client, except_=("_identify",)) - return client - - async def test_identify_payload_no_intents_no_presence(self, client): - client._presence = None - client._intents = None - client.session_id = None - client._token = "aaaa" - client.large_threshold = 420 - client.shard_id = 69 - client.shard_count = 96 - - await client._identify() - - client._send.assert_awaited_once_with( - { - "op": 2, - "d": { - "token": "aaaa", - "compress": False, - "large_threshold": 420, - "properties": user_agents.UserAgent().websocket_triplet, - "shard": [69, 96], - }, - } - ) - - async def test_identify_payload_with_presence(self, client): - presence = {"aaa": "bbb"} - client._presence = presence - client._intents = None - client.session_id = None - client._token = "aaaa" - client.large_threshold = 420 - client.shard_id = 69 - client.shard_count = 96 - - await client._identify() - - client._send.assert_awaited_once_with( - { - "op": 2, - "d": { - "token": "aaaa", - "compress": False, - "large_threshold": 420, - "properties": user_agents.UserAgent().websocket_triplet, - "shard": [69, 96], - "presence": presence, - }, - } - ) - - async def test_identify_payload_with_intents(self, client): - intents = 629 | 139 - client._presence = None - client._intents = intents - client.session_id = None - client._token = "aaaa" - client.large_threshold = 420 - client.shard_id = 69 - client.shard_count = 96 - - await client._identify() - - client._send.assert_awaited_once_with( - { - "op": 2, - "d": { - "token": "aaaa", - "compress": False, - "large_threshold": 420, - "properties": user_agents.UserAgent().websocket_triplet, - "shard": [69, 96], - "intents": intents, - }, - } - ) - - async def test_identify_payload_with_intents_and_presence(self, client): - intents = 629 | 139 - presence = {"aaa": "bbb"} - client._presence = presence - client._intents = intents - client.session_id = None - client._token = "aaaa" - client.large_threshold = 420 - client.shard_id = 69 - client.shard_count = 96 - - await client._identify() - - client._send.assert_awaited_once_with( - { - "op": 2, - "d": { - "token": "aaaa", - "compress": False, - "large_threshold": 420, - "properties": user_agents.UserAgent().websocket_triplet, - "shard": [69, 96], - "intents": intents, - "presence": presence, - }, - } - ) - - -@pytest.mark.asyncio -class TestResume: - @pytest.fixture - def client(self, event_loop): - asyncio.set_event_loop(event_loop) - client = _helpers.unslot_class(connection.Shard)(token="1234", url="xxx") - client = _helpers.mock_methods_on(client, except_=("_resume",)) - return client - - @_helpers.timeout_after(10.0) - @pytest.mark.parametrize("seq", [None, 999]) - async def test_resume_payload(self, client, seq): - client.session_id = 69420 - client.seq = seq - client._token = "reee" - - await client._resume() - - client._send.assert_awaited_once_with( - {"op": codes.GatewayOpcode.RESUME, "d": {"token": "reee", "session_id": 69420, "seq": seq}} - ) - - -@pytest.mark.asyncio -class TestHeartbeatKeepAlive: - @pytest.fixture - def client(self, event_loop): - asyncio.set_event_loop(event_loop) - client = _helpers.unslot_class(connection.Shard)(token="1234", url="xxx") - client = _helpers.mock_methods_on(client, except_=("_heartbeat_keep_alive", "_zombie_detector")) - client._send = mock.AsyncMock() - # This won't get set on the right event loop if we are not careful - client.closed_event = asyncio.Event() - client.last_heartbeat_ack_received = time.perf_counter() - return client - - @_helpers.timeout_after(10.0) - async def test_loops_indefinitely_until_requesting_close_event_set(self, client, event_loop): - async def recv(): - await asyncio.sleep(0.1) - client.last_heartbeat_ack_received = time.perf_counter() - client.last_message_received = client.last_heartbeat_ack_receied - - client._send = mock.AsyncMock(wraps=lambda *_: asyncio.create_task(recv())) - - task: asyncio.Future = event_loop.create_task(client._heartbeat_keep_alive(0.5)) - await asyncio.sleep(1.5) - - if task.done(): - raise task.exception() - - client.requesting_close_event.set() - await asyncio.sleep(1.5) - assert task.done() - - assert client._send.await_count > 2 # arbitrary number to imply a lot of calls. - - @_helpers.timeout_after(10.0) - async def test_heartbeat_interval_is_waited_on_heartbeat_sent_until_requesting_close_event_set( - self, client, event_loop - ): - task: asyncio.Future = event_loop.create_task(client._heartbeat_keep_alive(100_000)) - await asyncio.sleep(2) - client.requesting_close_event.set() - await asyncio.sleep(0.1) - assert task.done() - - @_helpers.timeout_after(1.0) - @_helpers.assert_raises(type_=asyncio.TimeoutError) - async def test_last_heartbeat_ack_received_less_than_last_heartbeat_sent_raises_TimeoutError(self, client): - client.last_message_received = time.perf_counter() - 1 - - await client._heartbeat_keep_alive(0.25) - - @pytest.mark.parametrize("seq", [None, 0, 259]) - async def test_heartbeat_payload(self, client, seq): - client.seq = seq - with contextlib.suppress(asyncio.TimeoutError): - with async_timeout.timeout(0.5): - await client._heartbeat_keep_alive(1) - - client._send.assert_awaited_once_with({"op": 1, "d": seq}) - - @_helpers.assert_does_not_raise(type_=asyncio.TimeoutError) - async def test_zombie_detector_not_a_zombie(self): - client = mock.MagicMock() - client.last_message_received = time.perf_counter() - 5 - heartbeat_interval = 41.25 - connection.Shard._zombie_detector(client, heartbeat_interval) - - @_helpers.assert_raises(type_=asyncio.TimeoutError) - async def test_zombie_detector_is_a_zombie(self): - client = mock.MagicMock() - client.last_message_received = time.perf_counter() - 500000 - heartbeat_interval = 41.25 - connection.Shard._zombie_detector(client, heartbeat_interval) - - -@pytest.mark.asyncio -class TestClose: - @pytest.fixture - def client(self, event_loop): - asyncio.set_event_loop(event_loop) - client = _helpers.unslot_class(connection.Shard)(token="1234", url="xxx") - client = _helpers.mock_methods_on(client, except_=("close",)) - client.ws = mock.MagicMock(aiohttp.ClientWebSocketResponse) - client.session = mock.MagicMock(aiohttp.ClientSession) - client.closed_event = asyncio.Event() - client._presence = {} - return client - - @_helpers.timeout_after(1.0) - async def test_closing_already_closing_websocket_does_nothing(self, client): - client.requesting_close_event.set() - client.requesting_close_event.set = mock.MagicMock() - await client.close() - client.requesting_close_event.set.assert_not_called() - - @_helpers.timeout_after(1.0) - async def test_closing_first_time_sets_closed_event(self, client): - client.closed_event.clear() - await client.close() - assert client.closed_event.is_set() - - @_helpers.timeout_after(3.0) - async def test_closing_ws_first_time_only_waits_2s(self, client): - client.closed_event.clear() - - async def close(_): - await asyncio.sleep(10.0) - - client.ws.close = close - client.session.close = mock.AsyncMock() - await client.close() - - @_helpers.timeout_after(3.0) - async def test_closing_session_first_time_only_waits_2s(self, client): - client.closed_event.clear() - - async def close(): - await asyncio.sleep(10.0) - - client.ws.close = mock.AsyncMock() - client.session.close = close - await client.close() - - @_helpers.timeout_after(3.0) - async def test_closing_ws_first_time_only_waits_2s(self, client): - client.closed_event.clear() - - async def close(code): - await asyncio.sleep(10.0) - - client.ws.close = close - client.session.close = mock.AsyncMock() - await client.close() - - @_helpers.timeout_after(5.0) - async def test_closing_ws_and_session_first_time_only_waits_4s(self, client): - client.closed_event.clear() - - async def close(code=...): - await asyncio.sleep(10.0) - - client.ws.close = close - client.session.close = close - await client.close() - - -@pytest.mark.asyncio -class TestPollEvents: - @pytest.fixture - def client(self, event_loop): - asyncio.set_event_loop(event_loop) - client = _helpers.unslot_class(connection.Shard)(token="1234", url="xxx") - client = _helpers.mock_methods_on(client, except_=("_run",)) - return client - - @_helpers.timeout_after(5.0) - async def test_opcode_0(self, client): - def receive(): - client.requesting_close_event.set() - return {"op": 0, "d": {"content": "whatever"}, "t": "MESSAGE_CREATE", "s": 123} - - client._receive = mock.AsyncMock(wraps=receive) - - await client._run() - - client.do_dispatch.assert_called_with("MESSAGE_CREATE", {"content": "whatever"}) - - @_helpers.timeout_after(5.0) - async def test_opcode_0_resume_sets_session_id(self, client): - client.seq = None - client.session_id = None - - def receive(): - client.requesting_close_event.set() - return {"op": 0, "d": {"v": 69, "session_id": "1a2b3c4d"}, "t": "READY", "s": 123} - - client._receive = mock.AsyncMock(wraps=receive) - - await client._run() - - client.do_dispatch.assert_called_with("READY", {"v": 69, "session_id": "1a2b3c4d"}) - - assert client.session_id == "1a2b3c4d" - assert client.seq == 123 - - -@pytest.mark.asyncio -class TestRequestGuildMembers: - @pytest.fixture - def client(self, event_loop): - asyncio.set_event_loop(event_loop) - client = _helpers.unslot_class(connection.Shard)(token="1234", url="xxx") - client = _helpers.mock_methods_on(client, except_=("request_guild_members",)) - return client - - async def test_no_kwargs(self, client): - await client.request_guild_members("1234", "5678") - client._send.assert_awaited_once_with({"op": 8, "d": {"guild_id": ["1234", "5678"], "query": "", "limit": 0}}) - - @pytest.mark.parametrize( - ["kwargs", "expected_limit", "expected_query"], - [({"limit": 22}, 22, ""), ({"query": "lol"}, 0, "lol"), ({"limit": 22, "query": "lol"}, 22, "lol"),], - ) - async def test_limit_and_query(self, client, kwargs, expected_limit, expected_query): - await client.request_guild_members("1234", "5678", **kwargs) - client._send.assert_awaited_once_with( - {"op": 8, "d": {"guild_id": ["1234", "5678"], "query": expected_query, "limit": expected_limit,}} - ) - - async def test_user_ids(self, client): - await client.request_guild_members("1234", "5678", user_ids=["9", "18", "27"]) - client._send.assert_awaited_once_with( - {"op": 8, "d": {"guild_id": ["1234", "5678"], "user_ids": ["9", "18", "27"]}} - ) - - @pytest.mark.parametrize("presences", [True, False]) - async def test_presences(self, client, presences): - await client.request_guild_members("1234", "5678", presences=presences) - client._send.assert_awaited_once_with( - {"op": 8, "d": {"guild_id": ["1234", "5678"], "query": "", "limit": 0, "presences": presences}} - ) - - -@pytest.mark.asyncio -class TestUpdatePresence: - @pytest.fixture - def client(self, event_loop): - asyncio.set_event_loop(event_loop) - client = _helpers.unslot_class(connection.Shard)(token="1234", url="xxx") - client = _helpers.mock_methods_on(client, except_=("update_presence",)) - return client - - async def test_sends_payload(self, client): - await client.update_presence({"afk": True, "game": {"name": "69", "type": 1}, "since": 69, "status": "dnd"}) - client._send.assert_awaited_once_with( - {"op": 3, "d": {"afk": True, "game": {"name": "69", "type": 1}, "since": 69, "status": "dnd"}} - ) - - async def test_caches_payload_for_later(self, client): - client._presence = {"baz": "bork"} - await client.update_presence({"afk": True, "game": {"name": "69", "type": 1}, "since": 69, "status": "dnd"}) - assert client._presence == {"afk": True, "game": {"name": "69", "type": 1}, "since": 69, "status": "dnd"} - - async def test_injects_default_fields(self, client): - await client.update_presence({"foo": "bar"}) - for k in ("foo", "afk", "game", "since", "status"): - assert k in client._presence - - -@pytest.mark.asyncio -class TestUpdateVoiceState: - @pytest.fixture - def client(self, event_loop): - asyncio.set_event_loop(event_loop) - client = _helpers.unslot_class(connection.Shard)(token="1234", url="xxx") - client = _helpers.mock_methods_on(client, except_=("update_voice_state",)) - return client - - @pytest.mark.parametrize("channel", ["1234", None]) - async def test_sends_payload(self, client, channel_id): - await client.update_voice_state("9987", channel_id, True, False) - client._send.assert_awaited_once_with( - { - "op": codes.GatewayOpcode.VOICE_STATE_UPDATE, - "d": {"guild_id": "9987", "channel": channel_id, "self_mute": True, "self_deaf": False,}, - } - ) diff --git a/tests/hikari/gateway/test_dispatchers.py b/tests/hikari/gateway/test_dispatchers.py deleted file mode 100644 index 6dad45c5c4..0000000000 --- a/tests/hikari/gateway/test_dispatchers.py +++ /dev/null @@ -1,163 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# Copyright © Nekoka.tt 2019-2020 -# -# This file is part of Hikari. -# -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . -import mock -import pytest - -from hikari import events -from hikari.net.gateway import dispatchers -from tests.hikari import _helpers - - -class SomeEvent(events.HikariEvent): - ... - - -@pytest.fixture -def dispatcher(): - class PartialDispatcherImpl(dispatchers.EventDispatcher): - close = NotImplemented - add_listener = mock.MagicMock(wraps=lambda _, f, **__: f) - remove_listener = NotImplemented - wait_for = NotImplemented - dispatch_event = NotImplemented - - return PartialDispatcherImpl() - - -class TestEventDispatcher: - def test_event_for_function_with_explicit_type_returns_decorated_function(self, dispatcher): - async def handler(event): - ... - - assert dispatcher.event(SomeEvent)(handler) is handler - - def test_event_for_function_with_type_hint_returns_decorated_function(self, dispatcher): - async def handler(event: SomeEvent): - ... - - assert dispatcher.event()(handler) is handler - - def test_event_for_function_with_explicit_type_registers_decorated_function(self, dispatcher): - async def handler(event): - ... - - dispatcher.event(SomeEvent)(handler) - - dispatcher.add_listener.assert_called_once_with(SomeEvent, handler, _stack_level=3) - - def test_event_for_function_with_type_hint_registers_decorated_function(self, dispatcher): - async def handler(event: SomeEvent): - ... - - dispatcher.event()(handler) - - dispatcher.add_listener.assert_called_once_with(SomeEvent, handler, _stack_level=3) - - @_helpers.assert_raises(type_=TypeError) - def test_event_for_function_without_type_hint_and_without_explicit_type_raises_AttributeError(self, dispatcher): - async def handler(event): - ... - - dispatcher.event()(handler) - - @_helpers.assert_raises(type_=TypeError) - def test_event_for_function_with_no_args_raises_TypeError(self, dispatcher): - async def handler(): - ... - - dispatcher.event()(handler) - - @_helpers.assert_raises(type_=TypeError) - def test_event_for_function_with_too_many_args_raises_TypeError(self, dispatcher): - async def handler(foo: SomeEvent, bar): - ... - - dispatcher.event()(handler) - - def test_event_for_method_with_explicit_type_returns_decorated_method(self, dispatcher): - class Class: - async def handler(self, event): - ... - - inst = Class() - - handler = inst.handler - assert dispatcher.event(SomeEvent)(handler) is handler - - def test_event_for_method_with_type_hint_returns_decorated_method(self, dispatcher): - class Class: - async def handler(self, event: SomeEvent): - ... - - inst = Class() - handler = inst.handler - - assert dispatcher.event()(handler) is handler - - def test_event_for_method_with_explicit_type_registers_decorated_method(self, dispatcher): - class Class: - async def handler(self, event): - ... - - inst = Class() - - dispatcher.event(SomeEvent)(inst.handler) - - dispatcher.add_listener.assert_called_once_with(SomeEvent, inst.handler, _stack_level=3) - - def test_event_for_method_with_type_hint_registers_decorated_method(self, dispatcher): - class Class: - async def handler(self, event: SomeEvent): - ... - - inst = Class() - - dispatcher.event()(inst.handler) - - dispatcher.add_listener.assert_called_once_with(SomeEvent, inst.handler, _stack_level=3) - - @_helpers.assert_raises(type_=TypeError) - def test_event_for_method_without_type_hint_and_without_explicit_type_raises_AttributeError(self, dispatcher): - class Class: - async def handler(self, event): - ... - - inst = Class() - - dispatcher.event()(inst.handler) - - @_helpers.assert_raises(type_=TypeError) - def test_event_for_method_with_no_args_raises_TypeError(self, dispatcher): - class Class: - async def handler(self): - ... - - inst = Class() - - dispatcher.event()(inst.handler) - - @_helpers.assert_raises(type_=TypeError) - def test_event_for_method_with_too_many_args_raises_TypeError(self, dispatcher): - class Class: - async def handler(self, event: SomeEvent, foo: int): - ... - - inst = Class() - - dispatcher.event()(inst.handler) diff --git a/tests/hikari/gateway/test_intent_aware_dispatchers.py b/tests/hikari/gateway/test_intent_aware_dispatchers.py deleted file mode 100644 index 3e716bac83..0000000000 --- a/tests/hikari/gateway/test_intent_aware_dispatchers.py +++ /dev/null @@ -1,621 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# Copyright © Nekoka.tt 2019-2020 -# -# This file is part of Hikari. -# -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . -import asyncio -import functools - -import async_timeout -import mock -import pytest - -from hikari import events -from hikari.net.gateway import intent_aware_dispatchers -from tests.hikari import _helpers - - -def coro(func): - @functools.wraps(func) - async def coro_fn(*args, **kwargs): - return func(*args, **kwargs) - - return coro_fn - - -class DummyEventType(events.HikariEvent): - ... - - -@pytest.fixture() -def intent_aware_dispatcher(): - return intent_aware_dispatchers.IntentAwareEventDispatcherImpl(None) - - -class TestClose: - def test_listeners_are_cleared(self, intent_aware_dispatcher): - intent_aware_dispatcher._listeners = mock.MagicMock() - - intent_aware_dispatcher.close() - - intent_aware_dispatcher._listeners.clear.assert_called_once_with() - - def test_pending_waiters_are_cancelled_and_cleared(self, intent_aware_dispatcher): - event1_mock_futures = [mock.MagicMock(cancel=mock.MagicMock()) for _ in range(30)] - event2_mock_futures = [mock.MagicMock(cancel=mock.MagicMock()) for _ in range(30)] - intent_aware_dispatcher._waiters = { - "event1": {f: lambda _: False for f in event1_mock_futures}, - "event2": {f: lambda _: False for f in event2_mock_futures}, - } - - intent_aware_dispatcher.close() - - for f in event1_mock_futures + event2_mock_futures: - f.cancel.assert_called_once_with() - assert all(f not in fs for fs in intent_aware_dispatcher._waiters.values()) - - assert intent_aware_dispatcher._waiters == {} - - -class TestAddListener: - @_helpers.assert_raises(type_=TypeError) - def test_non_HikariEvent_base_raises_TypeError(self, intent_aware_dispatcher): - async def callback(event): - pass - - intent_aware_dispatcher.add_listener(object, callback) - - def test_disabled_intents_never_raises_warning(self, intent_aware_dispatcher): - async def callback(event): - pass - - intent_aware_dispatcher._enabled_intents = None - - with mock.patch("warnings.warn") as warn: - intent_aware_dispatcher.add_listener(DummyEventType, callback) - - warn.assert_not_called() - - def test_add_when_no_existing_listeners_exist_for_event_adds_new_list_first(self, intent_aware_dispatcher): - class NewEventType(events.HikariEvent): - pass - - dummy_listeners = [...] - - intent_aware_dispatcher._listeners = {DummyEventType: [*dummy_listeners]} - - async def callback(event): - pass - - intent_aware_dispatcher.add_listener(NewEventType, callback) - - assert DummyEventType in intent_aware_dispatcher._listeners, "wrong event was removed somehow?" - assert dummy_listeners == intent_aware_dispatcher._listeners[DummyEventType], "wrong event was subscribed to!" - - assert NewEventType in intent_aware_dispatcher._listeners, "event was not subscribed to" - assert callback in intent_aware_dispatcher._listeners[NewEventType], "callback was not added as a subscriber" - assert len(intent_aware_dispatcher._listeners[NewEventType]) == 1, "event was not subscribed once" - - def test_add_when_other_listeners_exist_for_event_appends_callback(self, intent_aware_dispatcher): - class NewEventType(events.HikariEvent): - pass - - dummy_listeners = [...] - new_event_listeners = [mock.AsyncMock(), mock.AsyncMock()] - - intent_aware_dispatcher._listeners = {DummyEventType: [*dummy_listeners], NewEventType: [*new_event_listeners]} - - async def callback(event): - pass - - intent_aware_dispatcher.add_listener(NewEventType, callback) - - assert DummyEventType in intent_aware_dispatcher._listeners, "wrong event was removed somehow?" - assert dummy_listeners == intent_aware_dispatcher._listeners[DummyEventType], "wrong event was subscribed to!" - - assert NewEventType in intent_aware_dispatcher._listeners, "event was not subscribed to" - assert callback in intent_aware_dispatcher._listeners[NewEventType], "callback was not added as a subscriber" - assert intent_aware_dispatcher._listeners[NewEventType] == [ - *new_event_listeners, - callback, - ], "callbacks were mangled" - - -class TestRemoveListener: - def test_remove_listener_when_present(self, intent_aware_dispatcher): - async def callback(event): - pass - - a, b = mock.AsyncMock(), mock.AsyncMock() - intent_aware_dispatcher._listeners = {DummyEventType: [a, callback, b]} - - intent_aware_dispatcher.remove_listener(DummyEventType, callback) - - assert callback not in intent_aware_dispatcher._listeners[DummyEventType] - assert intent_aware_dispatcher._listeners[DummyEventType] == [a, b] - - def test_remove_listener_when_present_and_last_listener_of_that_type(self, intent_aware_dispatcher): - async def callback(event): - pass - - a, b = mock.AsyncMock(), mock.AsyncMock() - intent_aware_dispatcher._listeners = {DummyEventType: [callback]} - - intent_aware_dispatcher.remove_listener(DummyEventType, callback) - - assert DummyEventType not in intent_aware_dispatcher._listeners - - def test_remove_listener_when_callback_not_present(self, intent_aware_dispatcher): - async def callback(event): - pass - - a, b = mock.AsyncMock(), mock.AsyncMock() - intent_aware_dispatcher._listeners = {DummyEventType: [a, b]} - - intent_aware_dispatcher.remove_listener(DummyEventType, callback) - - def test_remove_listener_when_event_not_present(self, intent_aware_dispatcher): - async def callback(event): - pass - - class DummyEventType2(events.HikariEvent): - pass - - a, b = mock.AsyncMock(), mock.AsyncMock() - intent_aware_dispatcher._listeners = {DummyEventType2: [a, b]} - - intent_aware_dispatcher.remove_listener(DummyEventType, callback) - - -@pytest.mark.asyncio -class TestDispatchEvent: - async def test_listeners_invoked(self, intent_aware_dispatcher): - class Event1(events.HikariEvent): - pass - - class Event2(events.HikariEvent): - pass - - class Event3(events.HikariEvent): - pass - - e1_1, e1_2, e1_3 = mock.AsyncMock(), mock.AsyncMock(), mock.AsyncMock() - e2_1, e2_2, e2_3 = mock.AsyncMock(), mock.AsyncMock(), mock.AsyncMock() - e3_1, e3_2, e3_3 = mock.AsyncMock(), mock.AsyncMock(), mock.AsyncMock() - - intent_aware_dispatcher._listeners = { - Event1: [e1_1, e1_2, e1_3], - Event2: [e2_1, e2_2, e2_3], - Event3: [e3_1, e3_2, e3_3], - } - - await intent_aware_dispatcher.dispatch_event(Event2()) - - e1_1.assert_not_called() - e1_2.assert_not_called() - e1_3.assert_not_called() - - e2_1.assert_awaited_once() - e2_2.assert_awaited_once() - e2_3.assert_awaited_once() - - e3_1.assert_not_called() - e3_2.assert_not_called() - e3_3.assert_not_called() - - async def test_supertype_events_invoked(self, intent_aware_dispatcher): - class A(events.HikariEvent): - pass - - class B(A): - pass - - class C(B): - pass - - class D(C): - pass - - a_callback = mock.AsyncMock() - b_callback = mock.AsyncMock() - c_callback = mock.AsyncMock() - d_callback = mock.AsyncMock() - - intent_aware_dispatcher._listeners = {A: [a_callback], B: [b_callback], C: [c_callback], D: [d_callback]} - inst = C() - - await intent_aware_dispatcher.dispatch_event(inst) - - a_callback.assert_awaited_once_with(inst) - b_callback.assert_awaited_once_with(inst) - c_callback.assert_awaited_once_with(inst) - d_callback.assert_not_called() - - async def test_waiters_completed(self, intent_aware_dispatcher, event_loop): - class Event1(events.HikariEvent): - pass - - class Event2(events.HikariEvent): - pass - - class Event3(events.HikariEvent): - pass - - f1_1, f1_2, f1_3 = event_loop.create_future(), event_loop.create_future(), event_loop.create_future() - f2_1, f2_2, f2_3 = event_loop.create_future(), event_loop.create_future(), event_loop.create_future() - f3_1, f3_2, f3_3 = event_loop.create_future(), event_loop.create_future(), event_loop.create_future() - - def truthy(event): - return True - - intent_aware_dispatcher._waiters = { - Event1: {f1_1: None, f1_2: truthy, f1_3: None}, - Event2: {f2_1: truthy, f2_2: None, f2_3: truthy}, - Event3: {f3_1: None, f3_2: None, f3_3: None}, - } - - inst = Event2() - - await intent_aware_dispatcher.dispatch_event(inst) - - assert not f1_1.done() - assert not f1_2.done() - assert not f1_3.done() - - assert f2_1.result() is inst - assert f2_2.result() is inst - assert f2_3.result() is inst - - assert not f3_1.done() - assert not f3_2.done() - assert not f3_3.done() - - async def test_waiters_subtypes_completed(self, intent_aware_dispatcher, event_loop): - class A(events.HikariEvent): - pass - - class B(A): - pass - - class C(B): - pass - - class D(C): - pass - - inst = C() - - a_future = event_loop.create_future() - b_future = event_loop.create_future() - c_future = event_loop.create_future() - d_future = event_loop.create_future() - - def truthy(event): - return True - - intent_aware_dispatcher._waiters = { - A: {a_future: None}, - B: {b_future: truthy}, - C: {c_future: None}, - D: {d_future: truthy}, - } - - await intent_aware_dispatcher.dispatch_event(inst) - - assert a_future.result() is inst - assert b_future.result() is inst - assert c_future.result() is inst - assert not d_future.done() - - @pytest.mark.parametrize(["predicate", "expected_to_awaken"], [(lambda _: True, True), (lambda _: False, False),]) - async def test_waiters_adhere_to_sync_predicate( - self, intent_aware_dispatcher, predicate, expected_to_awaken, event_loop - ): - future = event_loop.create_future() - intent_aware_dispatcher._waiters = { - DummyEventType: {future: predicate}, - } - - inst = DummyEventType() - - await intent_aware_dispatcher.dispatch_event(inst) - - assert future.result() is inst if expected_to_awaken else not future.done() - - @pytest.mark.parametrize( - ["predicate", "expected_to_awaken"], [(coro(lambda _: True), True), (coro(lambda _: False), False),] - ) - async def test_waiters_adhere_to_async_predicate( - self, intent_aware_dispatcher, predicate, expected_to_awaken, event_loop - ): - future = event_loop.create_future() - intent_aware_dispatcher._waiters = { - DummyEventType: {future: predicate}, - } - - inst = DummyEventType() - - await intent_aware_dispatcher.dispatch_event(inst) - - # These get evaluated in the background... - await asyncio.sleep(0.25) - - assert future.result() is inst if expected_to_awaken else not future.done() - - @_helpers.timeout_after(5) - @_helpers.assert_raises(type_=LookupError) - async def test_waiter_sync_exception_is_propagated(self, intent_aware_dispatcher, event_loop): - def predicate(event): - raise LookupError("boom") - - future = event_loop.create_future() - intent_aware_dispatcher._waiters = { - DummyEventType: {future: predicate}, - } - - inst = DummyEventType() - - await intent_aware_dispatcher.dispatch_event(inst) - - await future - - @_helpers.timeout_after(5) - @_helpers.assert_raises(type_=LookupError) - async def test_waiter_async_exception_is_propagated(self, intent_aware_dispatcher, event_loop): - async def predicate(event): - raise LookupError("boom") - - future = event_loop.create_future() - intent_aware_dispatcher._waiters = { - DummyEventType: {future: predicate}, - } - - inst = DummyEventType() - - await intent_aware_dispatcher.dispatch_event(inst) - - await future - - async def test_no_dispatch_events_is_still_awaitable(self, intent_aware_dispatcher): - await intent_aware_dispatcher.dispatch_event(DummyEventType()) - - async def test_dispatch_event_awaits_async_function(self, intent_aware_dispatcher): - mock_async_listener = mock.AsyncMock() - dummy_event = DummyEventType() - intent_aware_dispatcher._listeners = {DummyEventType: [mock_async_listener]} - await intent_aware_dispatcher.dispatch_event(dummy_event) - mock_async_listener.assert_called_once_with(dummy_event) - - async def test_dispatch_event_calls_non_async_function(self, intent_aware_dispatcher): - mock_listener = mock.MagicMock() - dummy_event = DummyEventType() - intent_aware_dispatcher._listeners = {DummyEventType: [mock_listener]} - await intent_aware_dispatcher.dispatch_event(dummy_event) - mock_listener.assert_called_once_with(dummy_event) - - async def test_dispatch_event_returns_awaitable_future_if_noop(self, intent_aware_dispatcher): - try: - result = intent_aware_dispatcher.dispatch_event(DummyEventType()) - assert isinstance(result, asyncio.Future) - finally: - await result - - async def test_dispatch_event_returns_awaitable_future_if_futures_awakened(self, intent_aware_dispatcher): - try: - intent_aware_dispatcher._listeners = {DummyEventType: [mock.AsyncMock()]} - result = intent_aware_dispatcher.dispatch_event(DummyEventType()) - assert isinstance(result, asyncio.Future) - finally: - await result - - async def test_dispatch_event_handles_event_exception(self, intent_aware_dispatcher): - ex = LookupError("boom") - inst = DummyEventType() - - async def callback(event): - raise ex - - intent_aware_dispatcher.handle_exception = mock.MagicMock() - - intent_aware_dispatcher._listeners = {DummyEventType: [callback]} - await intent_aware_dispatcher.dispatch_event(inst) - - intent_aware_dispatcher.handle_exception.assert_called_once_with(ex, inst, callback) - - async def test_dispatch_event_does_not_redispatch_ExceptionEvent_recursively(self, intent_aware_dispatcher): - ex = LookupError("boom") - inst = DummyEventType() - - async def callback(event): - raise ex - - intent_aware_dispatcher.handle_exception = mock.MagicMock(wraps=intent_aware_dispatcher.handle_exception) - intent_aware_dispatcher._listeners = {DummyEventType: [callback], events.ExceptionEvent: [callback]} - await intent_aware_dispatcher.dispatch_event(inst) - - await asyncio.sleep(1) - - calls = intent_aware_dispatcher.handle_exception.call_args_list - - assert calls == [mock.call(ex, inst, callback), mock.call(ex, mock.ANY, callback)] - - -@pytest.mark.asyncio -class TestIntentAwareDispatcherImplIT: - async def test_dispatch_event_when_invoked_event_integration_test(self, intent_aware_dispatcher): - dummy_event_invoked = 0 - event_obj = DummyEventType() - - @intent_aware_dispatcher.event(DummyEventType) - async def on_dummy_event(actual_event_obj): - nonlocal dummy_event_invoked - dummy_event_invoked += 1 - assert actual_event_obj is event_obj - - await intent_aware_dispatcher.dispatch_event(event_obj) - - assert dummy_event_invoked == 1 - - async def test_dispatch_event_when_not_invoked_event_integration_test(self, intent_aware_dispatcher): - dummy_event_invoked = 0 - - @intent_aware_dispatcher.event(DummyEventType) - async def on_dummy_event(actual_event_obj): - nonlocal dummy_event_invoked - dummy_event_invoked += 1 - - await intent_aware_dispatcher.dispatch_event(mock.MagicMock()) - - assert dummy_event_invoked == 0 - - async def test_dispatch_event_when_event_invoked_errors_integration_test(self, intent_aware_dispatcher): - dummy_event_invoked = 0 - exception_event_invoked = 0 - event_obj = DummyEventType() - - @intent_aware_dispatcher.event(DummyEventType) - async def on_dummy_event(actual_event_obj): - nonlocal dummy_event_invoked - dummy_event_invoked += 1 - assert actual_event_obj is event_obj - raise RuntimeError("BANG") - - @intent_aware_dispatcher.event() - async def on_exception(actual_exception_event: events.ExceptionEvent): - nonlocal exception_event_invoked - assert isinstance(actual_exception_event, events.ExceptionEvent) - exception_event_invoked += 1 - - await intent_aware_dispatcher.dispatch_event(event_obj) - - # Just in case it isn't immediately scheduled to handle the exception. - await asyncio.sleep(0.25) - - assert dummy_event_invoked == 1 - assert exception_event_invoked == 1 - - -@pytest.mark.asyncio -class TestWaitForIntegrationTest: - @_helpers.timeout_after(5) - async def test_truthy_sync_predicate(self, intent_aware_dispatcher): - event = DummyEventType() - - def predicate(actual_event): - assert actual_event is event - return True - - task = intent_aware_dispatcher.wait_for(DummyEventType, predicate=predicate, timeout=None) - await intent_aware_dispatcher.dispatch_event(event) - - await task - - @_helpers.timeout_after(5) - async def test_truthy_async_predicate(self, intent_aware_dispatcher): - event = DummyEventType() - - async def predicate(actual_event): - assert actual_event is event - return True - - task = intent_aware_dispatcher.wait_for(DummyEventType, predicate=predicate, timeout=None) - await intent_aware_dispatcher.dispatch_event(event) - - await task - - @_helpers.timeout_after(5) - async def test_second_truthy_sync_predicate(self, intent_aware_dispatcher): - event = DummyEventType() - - def predicate(actual_event): - assert actual_event is event - return True - - dead_task = intent_aware_dispatcher.wait_for(DummyEventType, predicate=lambda _: False, timeout=None) - task = intent_aware_dispatcher.wait_for(DummyEventType, predicate=predicate, timeout=None) - await intent_aware_dispatcher.dispatch_event(event) - - await task - dead_task.cancel() - - @_helpers.timeout_after(5) - async def test_second_truthy_async_predicate(self, intent_aware_dispatcher): - event = DummyEventType() - - async def predicate(actual_event): - assert actual_event is event - return True - - dead_task = intent_aware_dispatcher.wait_for(DummyEventType, predicate=lambda _: False, timeout=None) - task = intent_aware_dispatcher.wait_for(DummyEventType, predicate=predicate, timeout=None) - await intent_aware_dispatcher.dispatch_event(event) - - await task - dead_task.cancel() - - async def test_falsy_sync_predicate(self, intent_aware_dispatcher): - event = DummyEventType() - - def predicate(actual_event): - assert actual_event is event - return False - - task = intent_aware_dispatcher.wait_for(DummyEventType, predicate=predicate, timeout=100) - await intent_aware_dispatcher.dispatch_event(event) - - try: - with async_timeout.timeout(1): - await task - assert False - except asyncio.TimeoutError: - pass - - async def test_falsy_async_predicate(self, intent_aware_dispatcher): - event = DummyEventType() - - async def predicate(actual_event): - assert actual_event is event - return False - - task = intent_aware_dispatcher.wait_for(DummyEventType, predicate=predicate, timeout=100) - await intent_aware_dispatcher.dispatch_event(event) - - try: - with async_timeout.timeout(1): - await task - assert False - except asyncio.TimeoutError: - pass - - @pytest.mark.parametrize("failed_attempts", [0, 1, 2]) - @_helpers.timeout_after(5) - async def test_wait_for_timeout(self, intent_aware_dispatcher, failed_attempts): - event = DummyEventType() - - async def predicate(actual_event): - assert actual_event is event - return False - - task = intent_aware_dispatcher.wait_for(DummyEventType, predicate=predicate, timeout=0.1) - - for i in range(failed_attempts): - await intent_aware_dispatcher.dispatch_event(event) - - try: - await task - assert False - except asyncio.TimeoutError: - pass diff --git a/tests/hikari/internal/test_codes.py b/tests/hikari/internal/test_codes.py deleted file mode 100644 index 38e3ddd34c..0000000000 --- a/tests/hikari/internal/test_codes.py +++ /dev/null @@ -1,35 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# Copyright © Nekokatt 2019-2020 -# -# This file is part of Hikari. -# -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . - -from hikari.internal import codes - - -def test_str_GatewayCloseCode(): - assert str(codes.GatewayCloseCode.UNKNOWN_ERROR) == "4000 Unknown Error" - - -def test_str_GatewayOpcode(): - assert str(codes.GatewayOpcode.HEARTBEAT_ACK) == "11 Heartbeat Ack" - - -def test_str_JSONErrorCode(): - assert ( - str(codes.JSONErrorCode.CANNOT_EDIT_A_MESSAGE_AUTHORED_BY_ANOTHER_USER) - == "50005 Cannot Edit A Message Authored By Another User" - ) diff --git a/tests/hikari/internal/test_http_client.py b/tests/hikari/internal/test_http_client.py index ddd7b6248c..ef4f308928 100644 --- a/tests/hikari/internal/test_http_client.py +++ b/tests/hikari/internal/test_http_client.py @@ -144,20 +144,16 @@ async def test_perform_request_json(self, client, client_session): client._ssl_context = mock.MagicMock() client._request_timeout = mock.MagicMock() - jsonified_body = b'{"hello": "world"}' + req = {"hello": "world"} trace_request_ctx = types.SimpleNamespace() - trace_request_ctx.request_body = jsonified_body + trace_request_ctx.request_body = req expected_response = mock.MagicMock() client_session.request = mock.AsyncMock(return_value=expected_response) actual_response = await client._perform_request( - method="POST", - url="http://foo.bar", - headers={"X-Foo-Count": "122"}, - body={"hello": "world"}, - query={"foo": "bar"}, + method="POST", url="http://foo.bar", headers={"X-Foo-Count": "122"}, body=req, query={"foo": "bar"}, ) assert expected_response is actual_response @@ -165,8 +161,8 @@ async def test_perform_request_json(self, client, client_session): method="POST", url="http://foo.bar", params={"foo": "bar"}, - headers={"X-Foo-Count": "122", "content-type": "application/json"}, - data=jsonified_body, + headers={"X-Foo-Count": "122"}, + json=req, allow_redirects=client._allow_redirects, proxy=client._proxy_url, proxy_auth=client._proxy_auth, diff --git a/tests/hikari/internal/test_marshaller.py b/tests/hikari/internal/test_marshaller.py deleted file mode 100644 index b2870c1a3f..0000000000 --- a/tests/hikari/internal/test_marshaller.py +++ /dev/null @@ -1,361 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# Copyright © Nekoka.tt 2019-2020 -# -# This file is part of Hikari. -# -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . -import attr -import mock -import pytest - -from hikari.internal import marshaller, codes -from tests.hikari import _helpers - - -class TestDereferenceHandle: - def test_dereference_handle_module_only(self): - from concurrent import futures - - assert marshaller.dereference_handle("concurrent.futures") is futures - - def test_dereference_handle_module_and_attribute(self): - assert ( - marshaller.dereference_handle("hikari.internal.codes#_GatewayCloseCode.AUTHENTICATION_FAILED") - is codes.GatewayCloseCode.AUTHENTICATION_FAILED - ) - - -class TestAttrib: - def test_invokes_attrs(self): - deserializer = lambda _: _ - serializer = lambda _: _ - - mock_default_factory_1 = mock.MagicMock - mock_default_factory_2 = mock.MagicMock - - with mock.patch("attr.ib") as attrib: - marshaller.attrib( - deserializer=deserializer, - raw_name="foo", - if_none=mock_default_factory_1, - if_undefined=mock_default_factory_2, - inherit_kwargs=True, - serializer=serializer, - foo=12, - bar="hello, world", - ) - - attrib.assert_called_once_with( - foo=12, - bar="hello, world", - metadata={ - marshaller._RAW_NAME_ATTR: "foo", - marshaller._SERIALIZER_ATTR: serializer, - marshaller._DESERIALIZER_ATTR: deserializer, - marshaller._INHERIT_KWARGS: True, - marshaller._IF_UNDEFINED: mock_default_factory_2, - marshaller._IF_NONE: mock_default_factory_1, - marshaller._MARSHALLER_ATTRIB: True, - }, - repr=False, - ) - - -@pytest.mark.parametrize("data", [2, "d", bytes("ok", "utf-8"), [], {}, set()]) -@_helpers.assert_raises(type_=RuntimeError) -def test_default_validator_raises_runtime_error(data): - marshaller._default_validator(data) - - -def method_stub(value): - ... - - -@pytest.mark.parametrize( - "data", [lambda x: "ok", *marshaller._PASSED_THROUGH_SINGLETONS, marshaller.RAISE, dict, method_stub] -) -def test_default_validator(data): - marshaller._default_validator(data) - - -class TestAttrs: - def test_invokes_attrs(self): - marshaller_mock = mock.MagicMock(marshaller.HikariEntityMarshaller) - - kwargs = {"marshaller": marshaller_mock} - - marshaller_mock.register = mock.MagicMock(wraps=lambda c: c) - - @marshaller.marshallable(**kwargs) - @attr.s() - class Foo: - bar = 69 - - assert Foo is not None - assert Foo.bar == 69 - - marshaller_mock.register.assert_called_once_with(Foo) - - -class TestMarshaller: - @pytest.fixture() - def marshaller_impl(self): - return marshaller.HikariEntityMarshaller() - - @_helpers.assert_raises(type_=TypeError) - def test_register_raises_type_error_on_none_attr_class(self, marshaller_impl): - defaulted_foo = mock.MagicMock() - - @marshaller.marshallable(marshaller=marshaller_impl) - class User: - id: int = marshaller.attrib(deserializer=int) - foo: list = attr.attrib(default=defaulted_foo) - - def test_register_ignores_none_marshaller_attrs(self, marshaller_impl): - defaulted_foo = mock.MagicMock() - - @marshaller.marshallable(marshaller=marshaller_impl) - @attr.s() - class User: - id: int = marshaller.attrib(deserializer=int) - foo: list = attr.attrib(default=defaulted_foo) - - result = marshaller_impl.deserialize({"id": "123", "foo": "blah"}, User) - assert result.id == 123 - assert result.foo is defaulted_foo - - def test_deserialize(self, marshaller_impl): - deserialized_id = mock.MagicMock() - id_deserializer = mock.MagicMock(return_value=deserialized_id) - - @marshaller.marshallable(marshaller=marshaller_impl) - @attr.s() - class User: - id: int = marshaller.attrib(deserializer=id_deserializer) - some_list: list = marshaller.attrib(deserializer=lambda items: [str(i) for i in items]) - - result = marshaller_impl.deserialize({"id": "12345", "some_list": [True, False, "foo", 12, 3.4]}, User) - - assert isinstance(result, User) - assert result.id == deserialized_id - assert result.some_list == ["True", "False", "foo", "12", "3.4"] - - def test_deserialize_not_required_success_if_specified(self, marshaller_impl): - @marshaller.marshallable(marshaller=marshaller_impl) - @attr.s() - class User: - id: int = marshaller.attrib(if_undefined=None, deserializer=str) - - result = marshaller_impl.deserialize({"id": 12345}, User) - - assert isinstance(result, User) - assert result.id == "12345" - - @pytest.mark.parametrize("singleton", marshaller._PASSED_THROUGH_SINGLETONS) - def test_deserialize_not_required_success_if_not_specified(self, marshaller_impl, singleton): - @marshaller.marshallable(marshaller=marshaller_impl) - @attr.s() - class User: - id: int = marshaller.attrib(if_undefined=singleton, deserializer=str) - - result = marshaller_impl.deserialize({}, User) - - assert isinstance(result, User) - assert result.id is singleton - - def test_deserialize_calls_if_undefined_if_not_none_and_field_not_present(self, marshaller_impl): - mock_result = mock.MagicMock() - mock_callable = mock.MagicMock(return_value=mock_result) - - @marshaller.marshallable(marshaller=marshaller_impl) - @attr.s() - class User: - id: int = marshaller.attrib(if_undefined=mock_callable, deserializer=str) - - result = marshaller_impl.deserialize({}, User) - - assert isinstance(result, User) - assert result.id is mock_result - mock_callable.assert_called_once() - - @_helpers.assert_raises(type_=AttributeError) - def test_deserialize_fail_on_unspecified_if_required(self, marshaller_impl): - @marshaller.marshallable(marshaller=marshaller_impl) - @attr.s() - class User: - id: int = marshaller.attrib(deserializer=str) - - marshaller_impl.deserialize({}, User) - - def test_deserialize_nullable_success_if_not_null(self, marshaller_impl): - @marshaller.marshallable(marshaller=marshaller_impl) - @attr.s() - class User: - id: int = marshaller.attrib(if_none=None, deserializer=str) - - result = marshaller_impl.deserialize({"id": 12345}, User) - - assert isinstance(result, User) - assert result.id == "12345" - - @pytest.mark.parametrize("singleton", marshaller._PASSED_THROUGH_SINGLETONS) - def test_deserialize_nullable_success_if_null(self, marshaller_impl, singleton): - @marshaller.marshallable(marshaller=marshaller_impl) - @attr.s() - class User: - id: int = marshaller.attrib(if_none=singleton, deserializer=str) - - result = marshaller_impl.deserialize({"id": None}, User) - - assert isinstance(result, User) - assert result.id is singleton - - def test_deserialize_calls_if_none_if_not_none_and_data_is_none(self, marshaller_impl): - mock_result = mock.MagicMock() - mock_callable = mock.MagicMock(return_value=mock_result) - - @marshaller.marshallable(marshaller=marshaller_impl) - @attr.s() - class User: - id: int = marshaller.attrib(if_none=mock_callable, deserializer=str) - - result = marshaller_impl.deserialize({"id": None}, User) - - assert isinstance(result, User) - assert result.id is mock_result - mock_callable.assert_called_once() - - @_helpers.assert_raises(type_=AttributeError) - def test_deserialize_fail_on_None_if_not_nullable(self, marshaller_impl): - @marshaller.marshallable(marshaller=marshaller_impl) - @attr.s() - class User: - id: int = marshaller.attrib(deserializer=str) - - marshaller_impl.deserialize({"id": None}, User) - - @_helpers.assert_raises(type_=TypeError) - def test_deserialize_fail_on_Error(self, marshaller_impl): - die = mock.MagicMock(side_effect=RuntimeError) - - @marshaller.marshallable(marshaller=marshaller_impl) - @attr.s() - class User: - id: int = marshaller.attrib(deserializer=die) - - marshaller_impl.deserialize({"id": 123,}, User) - - def test_serialize(self, marshaller_impl): - @marshaller.marshallable(marshaller=marshaller_impl) - @attr.s() - class User: - id: int = marshaller.attrib(deserializer=..., serializer=str) - some_list: list = marshaller.attrib(deserializer=..., serializer=lambda i: list(map(int, i))) - - u = User(12, ["9", "18", "27", "36"]) - - assert marshaller_impl.serialize(u) == {"id": "12", "some_list": [9, 18, 27, 36]} - - def test_serialize_skips_fields_with_null_serializer(self, marshaller_impl): - @marshaller.marshallable(marshaller=marshaller_impl) - @attr.s() - class User: - id: int = marshaller.attrib(deserializer=..., serializer=str) - some_list: list = marshaller.attrib( - deserializer=..., serializer=None, - ) - - u = User(12, ["9", "18", "27", "36"]) - - assert marshaller_impl.serialize(u) == { - "id": "12", - } - - def test_deserialize_skips_fields_with_null_deserializer(self, marshaller_impl): - @marshaller.marshallable(marshaller=marshaller_impl) - @attr.s - class User: - username: str = marshaller.attrib(deserializer=str) - _component: object = marshaller.attrib(deserializer=None, default=None) - - u = marshaller_impl.deserialize({"_component": "OK", "component": "Nay", "username": "Nay"}, User) - assert u._component is None - assert u.username == "Nay" - - def test_deserialize_kwarg_gets_set_for_skip_unmarshalling_attr(self, marshaller_impl): - @marshaller.marshallable(marshaller=marshaller_impl) - @attr.s - class User: - _component: object = marshaller.attrib(deserializer=None, default=None) - - mock_component = mock.MagicMock() - u = marshaller_impl.deserialize({"_component": "OK", "component": "Nay"}, User, component=mock_component) - assert u._component is mock_component - - def test_deserialize_injects_kwargs_to_inheriting_child_entity(self, marshaller_impl): - @marshaller.marshallable(marshaller=marshaller_impl) - @attr.s() - class Child: - _components: object = marshaller.attrib(deserializer=None, serializer=None) - - @marshaller.marshallable(marshaller=marshaller_impl) - @attr.s() - class User(Child): - child: Child = marshaller.attrib( - deserializer=lambda *args, **kwargs: marshaller_impl.deserialize(*args, Child, **kwargs), - inherit_kwargs=True, - ) - - components = mock.MagicMock() - - user = marshaller_impl.deserialize({"child": {}}, User, components=components) - assert user._components is components - assert user.child._components is components - - @_helpers.assert_raises(type_=LookupError) - def test_deserialize_on_unregistered_class_raises_LookupError(self, marshaller_impl): - class Foo: - pass - - marshaller_impl.deserialize({}, Foo) - - @_helpers.assert_raises(type_=LookupError) - def test_serialize_on_unregistered_class_raises_LookupError(self, marshaller_impl): - class Foo: - pass - - f = Foo() - - marshaller_impl.serialize(f) - - def test_handling_underscores_correctly_during_deserialization(self, marshaller_impl): - @marshaller.marshallable(marshaller=marshaller_impl) - @attr.s() - class ClassWithUnderscores: - _foo = marshaller.attrib(deserializer=str) - - impl = marshaller_impl.deserialize({"_foo": 1234}, ClassWithUnderscores) - - assert impl._foo == "1234" - - def test_handling_underscores_correctly_during_serialization(self, marshaller_impl): - @marshaller.marshallable(marshaller=marshaller_impl) - @attr.s() - class ClassWithUnderscores: - _foo = marshaller.attrib(serializer=int) - - impl = ClassWithUnderscores(foo="1234") - - assert marshaller_impl.serialize(impl) == {"_foo": 1234} diff --git a/tests/hikari/models/test_gateway.py b/tests/hikari/models/test_gateway.py deleted file mode 100644 index 181f275bd9..0000000000 --- a/tests/hikari/models/test_gateway.py +++ /dev/null @@ -1,100 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# Copyright © Nekoka.tt 2019-2020 -# -# This file is part of Hikari. -# -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . -import datetime - -import mock -import pytest - -from hikari import application -from hikari.models import gateway -from hikari.models import guilds -from tests.hikari import _helpers - - -@pytest.fixture() -def mock_app(): - return mock.MagicMock(application.Application) - - -@pytest.fixture() -def test_session_start_limit_payload(): - return {"total": 1000, "remaining": 991, "reset_after": 14170186} - - -class TestSessionStartLimit: - def test_deserialize(self, test_session_start_limit_payload, mock_app): - session_start_limit_obj = gateway.SessionStartLimit.deserialize(test_session_start_limit_payload, app=mock_app) - assert session_start_limit_obj.total == 1000 - assert session_start_limit_obj.remaining == 991 - assert session_start_limit_obj.reset_after == datetime.timedelta(milliseconds=14170186) - - -class TestGatewayBot: - @pytest.fixture() - def test_gateway_bot_payload(self, test_session_start_limit_payload): - return {"url": "wss://gateway.discord.gg", "shards": 1, "session_start_limit": test_session_start_limit_payload} - - def test_deserialize(self, test_gateway_bot_payload, test_session_start_limit_payload, mock_app): - mock_session_start_limit = mock.MagicMock(gateway.SessionStartLimit) - with _helpers.patch_marshal_attr( - gateway.GatewayBot, - "session_start_limit", - deserializer=gateway.SessionStartLimit.deserialize, - return_value=mock_session_start_limit, - ) as patched_start_limit_deserializer: - gateway_bot_obj = gateway.GatewayBot.deserialize(test_gateway_bot_payload, app=mock_app) - patched_start_limit_deserializer.assert_called_once_with(test_session_start_limit_payload, app=mock_app) - assert gateway_bot_obj.session_start_limit is mock_session_start_limit - assert gateway_bot_obj.url == "wss://gateway.discord.gg" - assert gateway_bot_obj.shard_count == 1 - - -class TestGatewayActivity: - @pytest.fixture() - def test_gateway_activity_config(self): - return {"name": "Presence me baby", "url": "http://a-url-name", "type": 1} - - def test_deserialize_full_config(self, test_gateway_activity_config): - gateway_activity_obj = gateway.Activity.deserialize(test_gateway_activity_config) - assert gateway_activity_obj.name == "Presence me baby" - assert gateway_activity_obj.url == "http://a-url-name" - assert gateway_activity_obj.type is guilds.ActivityType.STREAMING - - def test_deserialize_partial_config(self): - gateway_activity_obj = gateway.Activity.deserialize({"name": "Presence me baby"}) - assert gateway_activity_obj.name == "Presence me baby" - assert gateway_activity_obj.url == None - assert gateway_activity_obj.type is guilds.ActivityType.PLAYING - - def test_serialize_full_activity(self): - gateway_activity_obj = gateway.Activity( - name="Presence me baby", url="http://a-url-name", type=guilds.ActivityType.STREAMING - ) - assert gateway_activity_obj.serialize() == { - "name": "Presence me baby", - "url": "http://a-url-name", - "type": 1, - } - - def test_serialize_partial_activity(self): - gateway_activity_obj = gateway.Activity(name="Presence me baby",) - assert gateway_activity_obj.serialize() == { - "name": "Presence me baby", - "type": 0, - } diff --git a/tests/hikari/rest/__init__.py b/tests/hikari/rest/__init__.py deleted file mode 100644 index 1c1502a5ca..0000000000 --- a/tests/hikari/rest/__init__.py +++ /dev/null @@ -1,18 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# Copyright © Nekoka.tt 2019-2020 -# -# This file is part of Hikari. -# -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . diff --git a/tests/hikari/rest/test_buckets.py b/tests/hikari/rest/test_buckets.py deleted file mode 100644 index 372eb5beb7..0000000000 --- a/tests/hikari/rest/test_buckets.py +++ /dev/null @@ -1,309 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# Copyright © Nekoka.tt 2019-2020 -# -# This file is part of Hikari. -# -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . -import asyncio -import datetime -import time - -import mock -import pytest - -from hikari.internal import ratelimits -from hikari.net import buckets -from hikari.net import routes -from tests.hikari import _helpers - - -class TestRESTBucket: - @pytest.fixture - def template(self): - return routes.Route("GET", "/foo/bar") - - @pytest.fixture - def compiled_route(self, template): - return routes.CompiledRoute(template, "/foo/bar", "1a2b3c") - - @pytest.mark.parametrize("name", ["spaghetti", ratelimits.UNKNOWN_HASH]) - def test_is_unknown(self, name, compiled_route): - with buckets.RESTBucket(name, compiled_route) as rl: - assert rl.is_unknown is (name == ratelimits.UNKNOWN_HASH) - - def test_update_rate_limit(self, compiled_route): - with buckets.RESTBucket(__name__, compiled_route) as rl: - rl.remaining = 1 - rl.limit = 2 - rl.reset_at = 3 - rl.period = 2 - - with mock.patch("time.perf_counter", return_value=4.20): - rl.update_rate_limit(9, 18, 27) - - assert rl.remaining == 9 - assert rl.limit == 18 - assert rl.reset_at == 27 - assert rl.period == 27 - 4.20 - - @pytest.mark.parametrize("name", ["spaghetti", ratelimits.UNKNOWN_HASH]) - def test_drip(self, name, compiled_route): - with buckets.RESTBucket(name, compiled_route) as rl: - rl.remaining = 1 - rl.drip() - assert rl.remaining == 0 if name != ratelimits.UNKNOWN_HASH else 1 - - -class TestRESTBucketManager: - @pytest.mark.asyncio - async def test_close_closes_all_buckets(self): - class MockBucket: - def __init__(self): - self.close = mock.MagicMock() - - buckets_array = [MockBucket() for _ in range(30)] - - mgr = buckets.RESTBucketManager() - # noinspection PyFinal - mgr.real_hashes_to_buckets = {f"blah{i}": bucket for i, bucket in enumerate(buckets_array)} - - mgr.close() - - for i, bucket in enumerate(buckets_array): - bucket.close.assert_called_once(), i - - @pytest.mark.asyncio - async def test_close_sets_closed_event(self): - mgr = buckets.RESTBucketManager() - assert not mgr.closed_event.is_set() - mgr.close() - assert mgr.closed_event.is_set() - - @pytest.mark.asyncio - async def test_start(self): - with buckets.RESTBucketManager() as mgr: - assert mgr.gc_task is None - mgr.start() - mgr.start() - mgr.start() - assert mgr.gc_task is not None - - @pytest.mark.asyncio - async def test_exit_closes(self): - with mock.patch.object(buckets.RESTBucketManager, "close") as close: - with mock.patch.object(buckets.RESTBucketManager, "gc") as gc: - with buckets.RESTBucketManager() as mgr: - mgr.start(0.01, 32) - gc.assert_called_once_with(0.01, 32) - close.assert_called() - - @pytest.mark.asyncio - async def test_gc_polls_until_closed_event_set(self): - # This is shit, but it is good shit. - with buckets.RESTBucketManager() as mgr: - mgr.start(0.01) - assert mgr.gc_task is not None - assert not mgr.gc_task.done() - await asyncio.sleep(0.1) - assert mgr.gc_task is not None - assert not mgr.gc_task.done() - await asyncio.sleep(0.1) - mgr.closed_event.set() - assert mgr.gc_task is not None - assert not mgr.gc_task.done() - task = mgr.gc_task - await asyncio.sleep(0.1) - assert mgr.gc_task is None - assert task.done() - - @pytest.mark.asyncio - async def test_gc_calls_do_pass(self): - with _helpers.unslot_class(buckets.RESTBucketManager)() as mgr: - mgr.do_gc_pass = mock.MagicMock() - mgr.start(0.01, 33) - try: - await asyncio.sleep(0.1) - mgr.do_gc_pass.assert_called_with(33) - finally: - mgr.gc_task.cancel() - - @pytest.mark.asyncio - async def test_do_gc_pass_any_buckets_that_are_empty_but_still_rate_limited_are_kept_alive(self): - with _helpers.unslot_class(buckets.RESTBucketManager)() as mgr: - bucket = mock.MagicMock() - bucket.is_empty = True - bucket.is_unknown = False - bucket.reset_at = time.perf_counter() + 999999999999999999999999999 - - mgr.real_hashes_to_buckets["foobar"] = bucket - - mgr.do_gc_pass(0) - - assert "foobar" in mgr.real_hashes_to_buckets - bucket.close.assert_not_called() - - @pytest.mark.asyncio - async def test_do_gc_pass_any_buckets_that_are_empty_but_not_rate_limited_and_not_expired_are_kept_alive(self): - with _helpers.unslot_class(buckets.RESTBucketManager)() as mgr: - bucket = mock.MagicMock() - bucket.is_empty = True - bucket.is_unknown = False - bucket.reset_at = time.perf_counter() - - mgr.real_hashes_to_buckets["foobar"] = bucket - - mgr.do_gc_pass(10) - - assert "foobar" in mgr.real_hashes_to_buckets - bucket.close.assert_not_called() - - @pytest.mark.asyncio - async def test_do_gc_pass_any_buckets_that_are_empty_but_not_rate_limited_and_expired_are_closed(self): - with _helpers.unslot_class(buckets.RESTBucketManager)() as mgr: - bucket = mock.MagicMock() - bucket.is_empty = True - bucket.is_unknown = False - bucket.reset_at = time.perf_counter() - 999999999999999999999999999 - - mgr.real_hashes_to_buckets["foobar"] = bucket - - mgr.do_gc_pass(0) - - assert "foobar" not in mgr.real_hashes_to_buckets - bucket.close.assert_called_once() - - @pytest.mark.asyncio - async def test_do_gc_pass_any_buckets_that_are_not_empty_are_kept_alive(self): - with _helpers.unslot_class(buckets.RESTBucketManager)() as mgr: - bucket = mock.MagicMock() - bucket.is_empty = False - bucket.is_unknown = True - bucket.reset_at = time.perf_counter() - - mgr.real_hashes_to_buckets["foobar"] = bucket - - mgr.do_gc_pass(0) - - assert "foobar" in mgr.real_hashes_to_buckets - bucket.close.assert_not_called() - - @pytest.mark.asyncio - async def test_acquire_route_when_not_in_routes_to_real_hashes_makes_new_bucket_using_initial_hash(self): - with buckets.RESTBucketManager() as mgr: - route = mock.MagicMock() - route.create_real_bucket_hash = mock.MagicMock(wraps=lambda intial_hash: intial_hash + ";bobs") - - # This isn't a coroutine; why would I await it? - # noinspection PyAsyncCall - mgr.acquire(route) - - assert "UNKNOWN;bobs" in mgr.real_hashes_to_buckets - assert isinstance(mgr.real_hashes_to_buckets["UNKNOWN;bobs"], buckets.RESTBucket) - - @pytest.mark.asyncio - async def test_acquire_route_when_not_in_routes_to_real_hashes_caches_route(self): - with buckets.RESTBucketManager() as mgr: - route = mock.MagicMock() - route.create_real_bucket_hash = mock.MagicMock(wraps=lambda intial_hash: intial_hash + ";bobs") - - # This isn't a coroutine; why would I await it? - # noinspection PyAsyncCall - mgr.acquire(route) - - assert mgr.routes_to_hashes[route._route] == "UNKNOWN" - - @pytest.mark.asyncio - async def test_acquire_route_when_route_cached_already_obtains_hash_from_route_and_bucket_from_hash(self): - with buckets.RESTBucketManager() as mgr: - route = mock.MagicMock() - route.create_real_bucket_hash = mock.MagicMock(return_value="eat pant;1234") - bucket = mock.MagicMock() - mgr.routes_to_hashes[route] = "eat pant" - mgr.real_hashes_to_buckets["eat pant;1234"] = bucket - - # This isn't a coroutine; why would I await it? - # noinspection PyAsyncCall - mgr.acquire(route) - - # yes i test this twice, sort of. no, there isn't another way to verify this. sue me. - bucket.acquire.assert_called_once() - - @pytest.mark.asyncio - async def test_acquire_route_returns_acquired_future(self): - with buckets.RESTBucketManager() as mgr: - route = mock.MagicMock() - - bucket = mock.MagicMock() - with mock.patch.object(buckets, "RESTBucket", return_value=bucket): - route.create_real_bucket_hash = mock.MagicMock(wraps=lambda intial_hash: intial_hash + ";bobs") - - f = mgr.acquire(route) - assert f is bucket.acquire() - - @pytest.mark.asyncio - async def test_acquire_route_returns_acquired_future_for_new_bucket(self): - with buckets.RESTBucketManager() as mgr: - route = mock.MagicMock() - route.create_real_bucket_hash = mock.MagicMock(return_value="eat pant;bobs") - bucket = mock.MagicMock() - mgr.routes_to_hashes[route._route] = "eat pant" - mgr.real_hashes_to_buckets["eat pant;bobs"] = bucket - - f = mgr.acquire(route) - assert f is bucket.acquire() - - @pytest.mark.asyncio - async def test_update_rate_limits_if_wrong_bucket_hash_reroutes_route(self): - with buckets.RESTBucketManager() as mgr: - route = mock.MagicMock() - route.create_real_bucket_hash = mock.MagicMock(wraps=lambda intial_hash: intial_hash + ";bobs") - mgr.routes_to_hashes[route._route] = "123" - mgr.update_rate_limits(route, "blep", 22, 23, datetime.datetime.now(), datetime.datetime.now()) - assert mgr.routes_to_hashes[route._route] == "blep" - assert isinstance(mgr.real_hashes_to_buckets["blep;bobs"], buckets.RESTBucket) - - @pytest.mark.asyncio - async def test_update_rate_limits_if_right_bucket_hash_does_nothing_to_hash(self): - with buckets.RESTBucketManager() as mgr: - route = mock.MagicMock() - route.create_real_bucket_hash = mock.MagicMock(wraps=lambda intial_hash: intial_hash + ";bobs") - mgr.routes_to_hashes[route._route] = "123" - bucket = mock.MagicMock() - mgr.real_hashes_to_buckets["123;bobs"] = bucket - mgr.update_rate_limits(route, "123", 22, 23, datetime.datetime.now(), datetime.datetime.now()) - assert mgr.routes_to_hashes[route._route] == "123" - assert mgr.real_hashes_to_buckets["123;bobs"] is bucket - - @pytest.mark.asyncio - async def test_update_rate_limits_updates_params(self): - with buckets.RESTBucketManager() as mgr: - route = mock.MagicMock() - route.create_real_bucket_hash = mock.MagicMock(wraps=lambda intial_hash: intial_hash + ";bobs") - mgr.routes_to_hashes[route._route] = "123" - bucket = mock.MagicMock() - mgr.real_hashes_to_buckets["123;bobs"] = bucket - date = datetime.datetime.now().replace(year=2004) - reset_at = datetime.datetime.now() - - with mock.patch("time.perf_counter", return_value=27): - expect_reset_at_monotonic = 27 + (reset_at - date).total_seconds() - mgr.update_rate_limits(route, "123", 22, 23, date, reset_at) - bucket.update_rate_limit.assert_called_once_with(22, 23, expect_reset_at_monotonic) - - @pytest.mark.parametrize(("gc_task", "is_started"), [(None, False), (object(), True)]) - def test_is_started(self, gc_task, is_started): - with buckets.RESTBucketManager() as mgr: - mgr.gc_task = gc_task - assert mgr.is_started is is_started diff --git a/tests/hikari/rest/test_routes.py b/tests/hikari/rest/test_routes.py deleted file mode 100644 index 885ed6902b..0000000000 --- a/tests/hikari/rest/test_routes.py +++ /dev/null @@ -1,111 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# Copyright © Nekoka.tt 2019-2020 -# -# This file is part of Hikari. -# -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . -import mock -import pytest - -from hikari.net import routes - - -class TestCompiledRoute: - @pytest.fixture - def template_route(self): - return routes.Route("get", "/somewhere/{channel}") - - @pytest.fixture - def compiled_route(self, template_route): - return routes.CompiledRoute(template_route, "/somewhere/123", "123") - - def test_create_url(self, compiled_route): - assert compiled_route.create_url("https://something.com/api/v6") == "https://something.com/api/v6/somewhere/123" - - def test_create_real_bucket_hash(self, compiled_route): - assert compiled_route.create_real_bucket_hash("SOMETHING") == "SOMETHING;123" - - def test__repr__(self, compiled_route): - expected_repr = "CompiledRoute(method='get', compiled_path='/somewhere/123', major_params_hash='123')" - - assert compiled_route.__repr__() == expected_repr - - def test__str__(self, compiled_route): - assert str(compiled_route) == "get /somewhere/123" - - def test___eq___positive(self): - template = mock.MagicMock() - assert routes.CompiledRoute(template, "/foo/bar", "1a2b3c") == routes.CompiledRoute( - template, "/foo/bar", "1a2b3c" - ) - - def test___eq___negative_path(self): - template = mock.MagicMock() - assert routes.CompiledRoute(template, "/foo/baz", "1a2b3c") != routes.CompiledRoute( - template, "/foo/bar", "1a2b3c" - ) - - def test___eq___negative_hash(self): - t = mock.MagicMock() - assert routes.CompiledRoute(t, "/foo/bar", "1a2b3d") != routes.CompiledRoute(t, "/foo/bar", "1a2b3c") - - def test___hash___positive(self): - t = mock.MagicMock() - assert hash(routes.CompiledRoute(t, "/foo/bar", "1a2b3")) == hash(routes.CompiledRoute(t, "/foo/bar", "1a2b3")) - - def test___hash___negative(self): - t = mock.MagicMock() - assert hash(routes.CompiledRoute(t, "/foo/bar", "1a2b3c")) != hash(routes.CompiledRoute(t, "/foo/bar", "1a2b3")) - - -class TestRoute: - @pytest.fixture - def route(self): - return routes.Route("post", "/somewhere/{channel}") - - def test__init___without_major_params_uses_default_major_params(self, route): - assert route.major_param == "channel" - - def test_compile(self, route): - expected_compiled_route = routes.CompiledRoute(route, "/somewhere/123", "123") - - actual_compiled_route = route.compile(channel_id=123) - assert actual_compiled_route == expected_compiled_route - - def test__repr__(self, route): - expected_repr = "Route(path_template='/somewhere/{channel}', major_param='channel')" - - assert route.__repr__() == expected_repr - - def test__str__(self, route): - assert str(route) == "/somewhere/{channel}" - - def test___eq__(self): - assert routes.Route("foo", "bar") == routes.Route("foo", "bar") - - def test___ne___method(self): - assert routes.Route("foobar", "bar") != routes.Route("foo", "bar") - - def test___ne___path(self): - assert routes.Route("foo", "barbaz") != routes.Route("foo", "bar") - - def test___hash__when_equal(self): - assert hash(routes.Route("foo", "bar")) == hash(routes.Route("foo", "bar")) - - def test___hash___when_path_differs(self): - assert hash(routes.Route("foo", "barbaz")) != hash(routes.Route("foo", "bar")) - - def test___hash___when_method_differs(self): - assert hash(routes.Route("foobar", "baz")) != hash(routes.Route("foo", "baz")) diff --git a/tests/hikari/stateless/__init__.py b/tests/hikari/stateless/__init__.py deleted file mode 100644 index 1c1502a5ca..0000000000 --- a/tests/hikari/stateless/__init__.py +++ /dev/null @@ -1,18 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# Copyright © Nekoka.tt 2019-2020 -# -# This file is part of Hikari. -# -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . diff --git a/tests/hikari/stateless/test_manager.py b/tests/hikari/stateless/test_manager.py deleted file mode 100644 index 6fd0934b45..0000000000 --- a/tests/hikari/stateless/test_manager.py +++ /dev/null @@ -1,507 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# Copyright © Nekoka.tt 2019-2020 -# -# This file is part of Hikari. -# -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . -import mock -import pytest - -from hikari import application -from hikari.events import channel -from hikari.events import guild -from hikari.events import message -from hikari.events import other -from hikari.events import voice -from hikari.net import gateway -from hikari.stateless import manager - - -class TestStatelessEventManagerImpl: - @pytest.fixture - def mock_payload(self): - return {} - - @pytest.fixture - def event_manager_impl(self): - class MockDispatcher: - dispatch_event = mock.AsyncMock() - - return manager.StatelessEventManagerImpl( - app=mock.MagicMock(application.Application, event_dispatcher=MockDispatcher()) - ) - - @pytest.fixture - def mock_shard(self): - return mock.MagicMock(gateway.Gateway) - - @pytest.mark.asyncio - async def test_on_connect(self, event_manager_impl, mock_shard): - mock_event = mock.MagicMock(other.ConnectedEvent) - - with mock.patch("hikari.events.other.ConnectedEvent", return_value=mock_event) as event: - await event_manager_impl.on_connect(mock_shard, {}) - - assert event_manager_impl.on_connect.___event_name___ == {"CONNECTED"} - event.assert_called_once_with(shard=mock_shard) - event_manager_impl._app.event_dispatcher.dispatch_event.assert_called_once_with(mock_event) - - @pytest.mark.asyncio - async def test_on_disconnect(self, event_manager_impl, mock_shard): - mock_event = mock.MagicMock(other.DisconnectedEvent) - - with mock.patch("hikari.events.other.DisconnectedEvent", return_value=mock_event) as event: - await event_manager_impl.on_disconnect(mock_shard, {}) - - assert event_manager_impl.on_disconnect.___event_name___ == {"DISCONNECTED"} - event.assert_called_once_with(shard=mock_shard) - event_manager_impl._app.event_dispatcher.dispatch_event.assert_called_once_with(mock_event) - - @pytest.mark.asyncio - async def test_on_resume(self, event_manager_impl, mock_shard): - mock_event = mock.MagicMock(other.ResumedEvent) - - with mock.patch("hikari.events.other.ResumedEvent", return_value=mock_event) as event: - await event_manager_impl.on_resume(mock_shard, {}) - - assert event_manager_impl.on_resume.___event_name___ == {"RESUME"} - event.assert_called_once_with(shard=mock_shard) - event_manager_impl._app.event_dispatcher.dispatch_event.assert_called_once_with(mock_event) - - @pytest.mark.asyncio - async def test_on_ready(self, event_manager_impl, mock_payload): - mock_event = mock.MagicMock(other.ReadyEvent) - - with mock.patch("hikari.events.other.ReadyEvent.deserialize", return_value=mock_event) as event: - await event_manager_impl.on_ready(None, mock_payload) - - assert event_manager_impl.on_ready.___event_name___ == {"READY"} - event.assert_called_once_with(mock_payload, app=event_manager_impl._app) - event_manager_impl._app.event_dispatcher.dispatch_event.assert_called_once_with(mock_event) - - @pytest.mark.asyncio - async def test_on_channel_create(self, event_manager_impl, mock_payload): - mock_event = mock.MagicMock(channel.ChannelCreateEvent) - - with mock.patch("hikari.events.channel.ChannelCreateEvent.deserialize", return_value=mock_event) as event: - await event_manager_impl.on_channel_create(None, mock_payload) - - assert event_manager_impl.on_channel_create.___event_name___ == {"CHANNEL_CREATE"} - event.assert_called_once_with(mock_payload, app=event_manager_impl._app) - event_manager_impl._app.event_dispatcher.dispatch_event.assert_called_once_with(mock_event) - - @pytest.mark.asyncio - async def test_on_channel_update(self, event_manager_impl, mock_payload): - mock_event = mock.MagicMock(channel.ChannelUpdateEvent) - - with mock.patch("hikari.events.channel.ChannelUpdateEvent.deserialize", return_value=mock_event) as event: - await event_manager_impl.on_channel_update(None, mock_payload) - - assert event_manager_impl.on_channel_update.___event_name___ == {"CHANNEL_UPDATE"} - event.assert_called_once_with(mock_payload, app=event_manager_impl._app) - event_manager_impl._app.event_dispatcher.dispatch_event.assert_called_once_with(mock_event) - - @pytest.mark.asyncio - async def test_on_channel_delete(self, event_manager_impl, mock_payload): - mock_event = mock.MagicMock(channel.ChannelDeleteEvent) - - with mock.patch("hikari.events.channel.ChannelDeleteEvent.deserialize", return_value=mock_event) as event: - await event_manager_impl.on_channel_delete(None, mock_payload) - - assert event_manager_impl.on_channel_delete.___event_name___ == {"CHANNEL_DELETE"} - event.assert_called_once_with(mock_payload, app=event_manager_impl._app) - event_manager_impl._app.event_dispatcher.dispatch_event.assert_called_once_with(mock_event) - - @pytest.mark.asyncio - async def test_on_channel_pins_update(self, event_manager_impl, mock_payload): - mock_event = mock.MagicMock(channel.ChannelPinsUpdateEvent) - - with mock.patch("hikari.events.channel.ChannelPinsUpdateEvent.deserialize", return_value=mock_event) as event: - await event_manager_impl.on_channel_pins_update(None, mock_payload) - - assert event_manager_impl.on_channel_pins_update.___event_name___ == {"CHANNEL_PINS_UPDATE"} - event.assert_called_once_with(mock_payload, app=event_manager_impl._app) - event_manager_impl._app.event_dispatcher.dispatch_event.assert_called_once_with(mock_event) - - @pytest.mark.asyncio - async def test_on_guild_create(self, event_manager_impl, mock_payload): - mock_event = mock.MagicMock(guild.GuildCreateEvent) - - with mock.patch("hikari.events.guild.GuildCreateEvent.deserialize", return_value=mock_event) as event: - await event_manager_impl.on_guild_create(None, mock_payload) - - assert event_manager_impl.on_guild_create.___event_name___ == {"GUILD_CREATE"} - event.assert_called_once_with(mock_payload, app=event_manager_impl._app) - event_manager_impl._app.event_dispatcher.dispatch_event.assert_called_once_with(mock_event) - - @pytest.mark.asyncio - async def test_on_guild_update(self, event_manager_impl, mock_payload): - mock_event = mock.MagicMock(guild.GuildUpdateEvent) - - with mock.patch("hikari.events.guild.GuildUpdateEvent.deserialize", return_value=mock_event) as event: - await event_manager_impl.on_guild_update(None, mock_payload) - - assert event_manager_impl.on_guild_update.___event_name___ == {"GUILD_UPDATE"} - event.assert_called_once_with(mock_payload, app=event_manager_impl._app) - event_manager_impl._app.event_dispatcher.dispatch_event.assert_called_once_with(mock_event) - - @pytest.mark.asyncio - async def test_on_guild_delete_handles_guild_leave(self, event_manager_impl, mock_payload): - mock_payload.pop("unavailable", None) - mock_event = mock.MagicMock(guild.GuildLeaveEvent) - - with mock.patch("hikari.events.guild.GuildLeaveEvent.deserialize", return_value=mock_event) as event: - await event_manager_impl.on_guild_delete(None, mock_payload) - - assert event_manager_impl.on_guild_delete.___event_name___ == {"GUILD_DELETE"} - event.assert_called_once_with(mock_payload, app=event_manager_impl._app) - event_manager_impl._app.event_dispatcher.dispatch_event.assert_called_once_with(mock_event) - - @pytest.mark.asyncio - async def test_on_guild_delete_handles_guild_unavailable(self, event_manager_impl, mock_payload): - mock_payload["unavailable"] = True - mock_event = mock.MagicMock(guild.GuildUnavailableEvent) - - with mock.patch("hikari.events.guild.GuildUnavailableEvent.deserialize", return_value=mock_event) as event: - await event_manager_impl.on_guild_delete(None, mock_payload) - - assert event_manager_impl.on_guild_delete.___event_name___ == {"GUILD_DELETE"} - event.assert_called_once_with(mock_payload, app=event_manager_impl._app) - event_manager_impl._app.event_dispatcher.dispatch_event.assert_called_once_with(mock_event) - - @pytest.mark.asyncio - async def test_on_guild_ban_add(self, event_manager_impl, mock_payload): - mock_event = mock.MagicMock(guild.GuildBanAddEvent) - - with mock.patch("hikari.events.guild.GuildBanAddEvent.deserialize", return_value=mock_event) as event: - await event_manager_impl.on_guild_ban_add(None, mock_payload) - - assert event_manager_impl.on_guild_ban_add.___event_name___ == {"GUILD_BAN_ADD"} - event.assert_called_once_with(mock_payload, app=event_manager_impl._app) - event_manager_impl._app.event_dispatcher.dispatch_event.assert_called_once_with(mock_event) - - @pytest.mark.asyncio - async def test_on_guild_ban_remove(self, event_manager_impl, mock_payload): - mock_event = mock.MagicMock(guild.GuildBanRemoveEvent) - - with mock.patch("hikari.events.guild.GuildBanRemoveEvent.deserialize", return_value=mock_event) as event: - await event_manager_impl.on_guild_ban_remove(None, mock_payload) - - assert event_manager_impl.on_guild_ban_remove.___event_name___ == {"GUILD_BAN_REMOVE"} - event.assert_called_once_with(mock_payload, app=event_manager_impl._app) - event_manager_impl._app.event_dispatcher.dispatch_event.assert_called_once_with(mock_event) - - @pytest.mark.asyncio - async def test_on_guild_emojis_update(self, event_manager_impl, mock_payload): - mock_event = mock.MagicMock(guild.GuildEmojisUpdateEvent) - - with mock.patch("hikari.events.guild.GuildEmojisUpdateEvent.deserialize", return_value=mock_event) as event: - await event_manager_impl.on_guild_emojis_update(None, mock_payload) - - assert event_manager_impl.on_guild_emojis_update.___event_name___ == {"GUILD_EMOJIS_UPDATE"} - event.assert_called_once_with(mock_payload, app=event_manager_impl._app) - event_manager_impl._app.event_dispatcher.dispatch_event.assert_called_once_with(mock_event) - - @pytest.mark.asyncio - async def test_on_guild_integrations_update(self, event_manager_impl, mock_payload): - mock_event = mock.MagicMock(guild.GuildIntegrationsUpdateEvent) - - with mock.patch( - "hikari.events.guild.GuildIntegrationsUpdateEvent.deserialize", return_value=mock_event - ) as event: - await event_manager_impl.on_guild_integrations_update(None, mock_payload) - - assert event_manager_impl.on_guild_integrations_update.___event_name___ == {"GUILD_INTEGRATIONS_UPDATE"} - event.assert_called_once_with(mock_payload, app=event_manager_impl._app) - event_manager_impl._app.event_dispatcher.dispatch_event.assert_called_once_with(mock_event) - - @pytest.mark.asyncio - async def test_on_guild_member_add(self, event_manager_impl, mock_payload): - mock_event = mock.MagicMock(guild.GuildMemberAddEvent) - - with mock.patch("hikari.events.guild.GuildMemberAddEvent.deserialize", return_value=mock_event) as event: - await event_manager_impl.on_guild_member_add(None, mock_payload) - - assert event_manager_impl.on_guild_member_add.___event_name___ == {"GUILD_MEMBER_ADD"} - event.assert_called_once_with(mock_payload, app=event_manager_impl._app) - event_manager_impl._app.event_dispatcher.dispatch_event.assert_called_once_with(mock_event) - - @pytest.mark.asyncio - async def test_on_guild_member_update(self, event_manager_impl, mock_payload): - mock_event = mock.MagicMock(guild.GuildMemberUpdateEvent) - - with mock.patch("hikari.events.guild.GuildMemberUpdateEvent.deserialize", return_value=mock_event) as event: - await event_manager_impl.on_guild_member_update(None, mock_payload) - - assert event_manager_impl.on_guild_member_update.___event_name___ == {"GUILD_MEMBER_UPDATE"} - event.assert_called_once_with(mock_payload, app=event_manager_impl._app) - event_manager_impl._app.event_dispatcher.dispatch_event.assert_called_once_with(mock_event) - - @pytest.mark.asyncio - async def test_on_guild_member_remove(self, event_manager_impl, mock_payload): - mock_event = mock.MagicMock(guild.GuildMemberRemoveEvent) - - with mock.patch("hikari.events.guild.GuildMemberRemoveEvent.deserialize", return_value=mock_event) as event: - await event_manager_impl.on_guild_member_remove(None, mock_payload) - - assert event_manager_impl.on_guild_member_remove.___event_name___ == {"GUILD_MEMBER_REMOVE"} - event.assert_called_once_with(mock_payload, app=event_manager_impl._app) - event_manager_impl._app.event_dispatcher.dispatch_event.assert_called_once_with(mock_event) - - @pytest.mark.asyncio - async def test_on_guild_role_create(self, event_manager_impl, mock_payload): - mock_event = mock.MagicMock(guild.GuildRoleCreateEvent) - - with mock.patch("hikari.events.guild.GuildRoleCreateEvent.deserialize", return_value=mock_event) as event: - await event_manager_impl.on_guild_role_create(None, mock_payload) - - assert event_manager_impl.on_guild_role_create.___event_name___ == {"GUILD_ROLE_CREATE"} - event.assert_called_once_with(mock_payload, app=event_manager_impl._app) - event_manager_impl._app.event_dispatcher.dispatch_event.assert_called_once_with(mock_event) - - @pytest.mark.asyncio - async def test_on_guild_role_update(self, event_manager_impl, mock_payload): - mock_event = mock.MagicMock(guild.GuildRoleUpdateEvent) - - with mock.patch("hikari.events.guild.GuildRoleUpdateEvent.deserialize", return_value=mock_event) as event: - await event_manager_impl.on_guild_role_update(None, mock_payload) - - assert event_manager_impl.on_guild_role_update.___event_name___ == {"GUILD_ROLE_UPDATE"} - event.assert_called_once_with(mock_payload, app=event_manager_impl._app) - event_manager_impl._app.event_dispatcher.dispatch_event.assert_called_once_with(mock_event) - - @pytest.mark.asyncio - async def test_on_guild_role_delete(self, event_manager_impl, mock_payload): - mock_event = mock.MagicMock(guild.GuildRoleDeleteEvent) - - with mock.patch("hikari.events.guild.GuildRoleDeleteEvent.deserialize", return_value=mock_event) as event: - await event_manager_impl.on_guild_role_delete(None, mock_payload) - - assert event_manager_impl.on_guild_role_delete.___event_name___ == {"GUILD_ROLE_DELETE"} - event.assert_called_once_with(mock_payload, app=event_manager_impl._app) - event_manager_impl._app.event_dispatcher.dispatch_event.assert_called_once_with(mock_event) - - @pytest.mark.asyncio - async def test_on_invite_create(self, event_manager_impl, mock_payload): - mock_event = mock.MagicMock(channel.InviteCreateEvent) - - with mock.patch("hikari.events.channel.InviteCreateEvent.deserialize", return_value=mock_event) as event: - await event_manager_impl.on_invite_create(None, mock_payload) - - assert event_manager_impl.on_invite_create.___event_name___ == {"INVITE_CREATE"} - event.assert_called_once_with(mock_payload, app=event_manager_impl._app) - event_manager_impl._app.event_dispatcher.dispatch_event.assert_called_once_with(mock_event) - - @pytest.mark.asyncio - async def test_on_invite_delete(self, event_manager_impl, mock_payload): - mock_event = mock.MagicMock(channel.InviteDeleteEvent) - - with mock.patch("hikari.events.channel.InviteDeleteEvent.deserialize", return_value=mock_event) as event: - await event_manager_impl.on_invite_delete(None, mock_payload) - - assert event_manager_impl.on_invite_delete.___event_name___ == {"INVITE_DELETE"} - event.assert_called_once_with(mock_payload, app=event_manager_impl._app) - event_manager_impl._app.event_dispatcher.dispatch_event.assert_called_once_with(mock_event) - - @pytest.mark.asyncio - async def test_on_message_create_without_member_payload(self, event_manager_impl): - mock_payload = {"id": "424242", "user": {"id": "111", "username": "okokok", "discrim": "4242"}} - mock_event = mock.MagicMock(message.MessageCreateEvent) - - with mock.patch("hikari.events.message.MessageCreateEvent.deserialize", return_value=mock_event) as event: - await event_manager_impl.on_message_create(None, mock_payload) - - assert event_manager_impl.on_message_create.___event_name___ == {"MESSAGE_CREATE"} - event.assert_called_once_with(mock_payload, app=event_manager_impl._app) - event_manager_impl._app.event_dispatcher.dispatch_event.assert_called_once_with(mock_event) - - @pytest.mark.asyncio - async def test_on_message_create_injects_user_into_member_payload(self, event_manager_impl): - mock_payload = {"id": "424242", "author": {"id": "111", "username": "okokok", "discrim": "4242"}, "member": {}} - mock_event = mock.MagicMock(message.MessageCreateEvent) - - with mock.patch("hikari.events.message.MessageCreateEvent.deserialize", return_value=mock_event) as event: - await event_manager_impl.on_message_create(None, mock_payload) - - assert event_manager_impl.on_message_create.___event_name___ == {"MESSAGE_CREATE"} - event.assert_called_once_with( - { - "id": "424242", - "author": {"id": "111", "username": "okokok", "discrim": "4242"}, - "member": {"user": {"id": "111", "username": "okokok", "discrim": "4242"}}, - }, - app=event_manager_impl._app, - ) - event_manager_impl._app.event_dispatcher.dispatch_event.assert_called_once_with(mock_event) - - @pytest.mark.asyncio - async def test_on_message_update_without_member_payload(self, event_manager_impl, mock_payload): - mock_payload = {"id": "424242", "user": {"id": "111", "username": "okokok", "discrim": "4242"}} - mock_event = mock.MagicMock(message.MessageUpdateEvent) - - with mock.patch("hikari.events.message.MessageUpdateEvent.deserialize", return_value=mock_event) as event: - await event_manager_impl.on_message_update(None, mock_payload) - - assert event_manager_impl.on_message_update.___event_name___ == {"MESSAGE_UPDATE"} - event.assert_called_once_with(mock_payload, app=event_manager_impl._app) - event_manager_impl._app.event_dispatcher.dispatch_event.assert_called_once_with(mock_event) - - @pytest.mark.asyncio - async def test_on_message_update_injects_user_into_member_payload(self, event_manager_impl, mock_payload): - mock_payload = {"id": "424242", "author": {"id": "111", "username": "okokok", "discrim": "4242"}, "member": {}} - mock_event = mock.MagicMock(message.MessageUpdateEvent) - - with mock.patch("hikari.events.message.MessageUpdateEvent.deserialize", return_value=mock_event) as event: - await event_manager_impl.on_message_update(None, mock_payload) - - assert event_manager_impl.on_message_update.___event_name___ == {"MESSAGE_UPDATE"} - event.assert_called_once_with( - { - "id": "424242", - "author": {"id": "111", "username": "okokok", "discrim": "4242"}, - "member": {"user": {"id": "111", "username": "okokok", "discrim": "4242"}}, - }, - app=event_manager_impl._app, - ) - event_manager_impl._app.event_dispatcher.dispatch_event.assert_called_once_with(mock_event) - - @pytest.mark.asyncio - async def test_on_message_delete(self, event_manager_impl, mock_payload): - mock_event = mock.MagicMock(message.MessageDeleteEvent) - - with mock.patch("hikari.events.message.MessageDeleteEvent.deserialize", return_value=mock_event) as event: - await event_manager_impl.on_message_delete(None, mock_payload) - - assert event_manager_impl.on_message_delete.___event_name___ == {"MESSAGE_DELETE"} - event.assert_called_once_with(mock_payload, app=event_manager_impl._app) - event_manager_impl._app.event_dispatcher.dispatch_event.assert_called_once_with(mock_event) - - @pytest.mark.asyncio - async def test_on_message_delete_bulk(self, event_manager_impl, mock_payload): - mock_event = mock.MagicMock(message.MessageDeleteBulkEvent) - - with mock.patch("hikari.events.message.MessageDeleteBulkEvent.deserialize", return_value=mock_event) as event: - await event_manager_impl.on_message_delete_bulk(None, mock_payload) - - assert event_manager_impl.on_message_delete_bulk.___event_name___ == {"MESSAGE_DELETE_BULK"} - event.assert_called_once_with(mock_payload, app=event_manager_impl._app) - event_manager_impl._app.event_dispatcher.dispatch_event.assert_called_once_with(mock_event) - - @pytest.mark.asyncio - async def test_on_message_reaction_add(self, event_manager_impl, mock_payload): - mock_event = mock.MagicMock(message.MessageReactionAddEvent) - - with mock.patch("hikari.events.message.MessageReactionAddEvent.deserialize", return_value=mock_event) as event: - await event_manager_impl.on_message_reaction_add(None, mock_payload) - - assert event_manager_impl.on_message_reaction_add.___event_name___ == {"MESSAGE_REACTION_ADD"} - event.assert_called_once_with(mock_payload, app=event_manager_impl._app) - event_manager_impl._app.event_dispatcher.dispatch_event.assert_called_once_with(mock_event) - - @pytest.mark.asyncio - async def test_on_message_reaction_remove(self, event_manager_impl, mock_payload): - mock_payload["emoji"] = {} - mock_event = mock.MagicMock(message.MessageReactionRemoveEvent) - - with mock.patch( - "hikari.events.message.MessageReactionRemoveEvent.deserialize", return_value=mock_event - ) as event: - await event_manager_impl.on_message_reaction_remove(None, mock_payload) - - assert event_manager_impl.on_message_reaction_remove.___event_name___ == {"MESSAGE_REACTION_REMOVE"} - event.assert_called_once_with({"emoji": {"animated": None}}, app=event_manager_impl._app) - event_manager_impl._app.event_dispatcher.dispatch_event.assert_called_once_with(mock_event) - - @pytest.mark.asyncio - async def test_on_message_reaction_remove_emoji(self, event_manager_impl, mock_payload): - mock_payload["emoji"] = {} - mock_event = mock.MagicMock(message.MessageReactionRemoveEmojiEvent) - - with mock.patch( - "hikari.events.message.MessageReactionRemoveEmojiEvent.deserialize", return_value=mock_event - ) as event: - await event_manager_impl.on_message_reaction_remove_emoji(None, mock_payload) - - assert event_manager_impl.on_message_reaction_remove_emoji.___event_name___ == { - "MESSAGE_REACTION_REMOVE_EMOJI" - } - event.assert_called_once_with({"emoji": {"animated": None}}, app=event_manager_impl._app) - event_manager_impl._app.event_dispatcher.dispatch_event.assert_called_once_with(mock_event) - - @pytest.mark.asyncio - async def test_on_presence_update(self, event_manager_impl, mock_payload): - mock_event = mock.MagicMock(guild.PresenceUpdateEvent) - - with mock.patch("hikari.events.guild.PresenceUpdateEvent.deserialize", return_value=mock_event) as event: - await event_manager_impl.on_presence_update(None, mock_payload) - - assert event_manager_impl.on_presence_update.___event_name___ == {"PRESENCE_UPDATE"} - event.assert_called_once_with(mock_payload, app=event_manager_impl._app) - event_manager_impl._app.event_dispatcher.dispatch_event.assert_called_once_with(mock_event) - - @pytest.mark.asyncio - async def test_on_typing_start(self, event_manager_impl, mock_payload): - mock_event = mock.MagicMock(channel.TypingStartEvent) - - with mock.patch("hikari.events.channel.TypingStartEvent.deserialize", return_value=mock_event) as event: - await event_manager_impl.on_typing_start(None, mock_payload) - - assert event_manager_impl.on_typing_start.___event_name___ == {"TYPING_START"} - event.assert_called_once_with(mock_payload, app=event_manager_impl._app) - event_manager_impl._app.event_dispatcher.dispatch_event.assert_called_once_with(mock_event) - - @pytest.mark.asyncio - async def test_on_my_user_update(self, event_manager_impl, mock_payload): - mock_event = mock.MagicMock(other.MyUserUpdateEvent) - - with mock.patch("hikari.events.other.MyUserUpdateEvent.deserialize", return_value=mock_event) as event: - await event_manager_impl.on_my_user_update(None, mock_payload) - - assert event_manager_impl.on_my_user_update.___event_name___ == {"USER_UPDATE"} - event.assert_called_once_with(mock_payload, app=event_manager_impl._app) - event_manager_impl._app.event_dispatcher.dispatch_event.assert_called_once_with(mock_event) - - @pytest.mark.asyncio - async def test_on_voice_state_update(self, event_manager_impl, mock_payload): - mock_event = mock.MagicMock(voice.VoiceStateUpdateEvent) - - with mock.patch("hikari.events.voice.VoiceStateUpdateEvent.deserialize", return_value=mock_event) as event: - await event_manager_impl.on_voice_state_update(None, mock_payload) - - assert event_manager_impl.on_voice_state_update.___event_name___ == {"VOICE_STATE_UPDATE"} - event.assert_called_once_with(mock_payload, app=event_manager_impl._app) - event_manager_impl._app.event_dispatcher.dispatch_event.assert_called_once_with(mock_event) - - @pytest.mark.asyncio - async def test_on_voice_server_update(self, event_manager_impl, mock_payload): - mock_event = mock.MagicMock(voice.VoiceStateUpdateEvent) - - with mock.patch("hikari.events.voice.VoiceServerUpdateEvent.deserialize", return_value=mock_event) as event: - await event_manager_impl.on_voice_server_update(None, mock_payload) - - assert event_manager_impl.on_voice_server_update.___event_name___ == {"VOICE_SERVER_UPDATE"} - event.assert_called_once_with(mock_payload, app=event_manager_impl._app) - event_manager_impl._app.event_dispatcher.dispatch_event.assert_called_once_with(mock_event) - - @pytest.mark.asyncio - async def test_on_webhook_update(self, event_manager_impl, mock_payload): - mock_event = mock.MagicMock(channel.WebhookUpdateEvent) - - with mock.patch("hikari.events.channel.WebhookUpdateEvent.deserialize", return_value=mock_event) as event: - await event_manager_impl.on_webhook_update(None, mock_payload) - - assert event_manager_impl.on_webhook_update.___event_name___ == {"WEBHOOK_UPDATE"} - event.assert_called_once_with(mock_payload, app=event_manager_impl._app) - event_manager_impl._app.event_dispatcher.dispatch_event.assert_called_once_with(mock_event) diff --git a/tests/hikari/test_configs.py b/tests/hikari/test_configs.py deleted file mode 100644 index 47acc3d4af..0000000000 --- a/tests/hikari/test_configs.py +++ /dev/null @@ -1,321 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# Copyright © Nekoka.tt 2019-2020 -# -# This file is part of Hikari. -# -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . -import datetime -import ssl - -import aiohttp -import mock -import pytest - -from hikari import http_settings -from hikari.net import urls -from hikari.models import gateway -from hikari.models import guilds -from hikari.models import intents -from tests.hikari import _helpers - - -@pytest.fixture -def test_debug_config(): - return {"_debug": True} - - -@pytest.fixture -def test_aiohttp_config(): - return { - "_allow_redirects": True, - "tcp_connector": "aiohttp#TCPConnector", - "_proxy_headers": {"Some-Header": "headercontent"}, - "_proxy_auth": "basic Tm90aGluZyB0byBzZWUgaGVyZSA6IGpvaW4gZGlzY29yZC5nZy9IS0dQRTlRIDopIH5kYXZmc2E=", - "_proxy_url": "_proxy_url", - "request_timeout": 100, - "_ssl_context": "ssl#SSLContext", - "_verify_ssl": False, - } - - -@pytest.fixture -def test_token_config(): - return {"token": "token"} - - -@pytest.fixture -def test_websocket_config(test_debug_config, test_aiohttp_config, test_token_config): - return { - "gateway_use_compression": False, - "gateway_version": 6, - "initial_activity": {"name": "test", "url": "some_url", "type": 0}, - "initial_status": "dnd", - "initial_is_afk": True, - "initial_idle_since": None, # Set in test - "intents": 513, - "large_threshold": 1000, - "shard_ids": "5...10", - "shard_count": "17", - **test_debug_config, - **test_aiohttp_config, - **test_token_config, - } - - -@pytest.fixture -def test_rest_config(test_aiohttp_config, test_token_config): - return { - "rest_version": 6, - **test_aiohttp_config, - **test_token_config, - "rest_url": "foobar", - "oauth2_url": "quxquxx", - } - - -@pytest.fixture -def test_bot_config(test_rest_config, test_websocket_config): - return {**test_rest_config, **test_websocket_config} - - -class TestDebugConfig: - def test_deserialize(self, test_debug_config): - debug_config_obj = http_settings.DebugConfig.deserialize(test_debug_config) - - assert debug_config_obj._debug is True - - def test_empty_deserialize(self): - debug_config_obj = http_settings.DebugConfig.deserialize({}) - - assert debug_config_obj._debug is False - - -class TestAIOHTTPConfig: - def test_deserialize(self, test_aiohttp_config): - aiohttp_config_obj = http_settings.HTTPSettings.deserialize(test_aiohttp_config) - - assert aiohttp_config_obj._allow_redirects is True - assert aiohttp_config_obj.tcp_connector == aiohttp.TCPConnector - assert aiohttp_config_obj._proxy_headers == {"Some-Header": "headercontent"} - assert aiohttp_config_obj._proxy_auth == aiohttp.BasicAuth.decode( - "basic Tm90aGluZyB0byBzZWUgaGVyZSA6IGpvaW4gZGlzY29yZC5nZy9IS0dQRTlRIDopIH5kYXZmc2E=" - ) - assert aiohttp_config_obj._proxy_url == "_proxy_url" - assert aiohttp_config_obj.request_timeout == 100 - assert aiohttp_config_obj._ssl_context == ssl.SSLContext - assert aiohttp_config_obj._verify_ssl is False - - def test_empty_deserialize(self): - aiohttp_config_obj = http_settings.HTTPSettings.deserialize({}) - - assert aiohttp_config_obj._allow_redirects is False - assert aiohttp_config_obj.tcp_connector is None - assert aiohttp_config_obj._proxy_headers is None - assert aiohttp_config_obj._proxy_auth is None - assert aiohttp_config_obj._proxy_url is None - assert aiohttp_config_obj.request_timeout is None - assert aiohttp_config_obj._ssl_context is None - assert aiohttp_config_obj._verify_ssl is True - - -class TestTokenConfig: - def test_deserialize(self, test_token_config): - token_config_obj = http_settings.TokenConfig.deserialize(test_token_config) - - assert token_config_obj.token == "token" - - def test_empty_deserialize(self): - token_config_obj = http_settings.TokenConfig.deserialize({}) - - assert token_config_obj.token is None - - -class TestWebsocketConfig: - def test_deserialize(self, test_websocket_config): - datetime_obj = datetime.datetime.now() - test_websocket_config["initial_idle_since"] = datetime_obj.timestamp() - mock_activity = mock.MagicMock(gateway.Activity) - with _helpers.patch_marshal_attr( - http_settings.GatewayConfig, - "initial_activity", - deserializer=gateway.Activity.deserialize, - return_value=mock_activity, - ) as patched_activity_deserializer: - websocket_config_obj = http_settings.GatewayConfig.deserialize(test_websocket_config) - patched_activity_deserializer.assert_called_once_with({"name": "test", "url": "some_url", "type": 0}) - assert websocket_config_obj.gateway_use_compression is False - assert websocket_config_obj.gateway_version == 6 - assert websocket_config_obj.initial_activity == mock_activity - assert websocket_config_obj.initial_status == guilds.PresenceStatus.DND - assert websocket_config_obj.initial_idle_since == datetime_obj - assert websocket_config_obj.intents == intents.Intent.GUILD_MESSAGES | intents.Intent.GUILDS - assert websocket_config_obj.large_threshold == 1000 - assert websocket_config_obj._debug is True - assert websocket_config_obj._allow_redirects is True - assert websocket_config_obj.tcp_connector == aiohttp.TCPConnector - assert websocket_config_obj._proxy_headers == {"Some-Header": "headercontent"} - assert websocket_config_obj._proxy_auth == aiohttp.BasicAuth.decode( - "basic Tm90aGluZyB0byBzZWUgaGVyZSA6IGpvaW4gZGlzY29yZC5nZy9IS0dQRTlRIDopIH5kYXZmc2E=" - ) - assert websocket_config_obj._proxy_url == "_proxy_url" - assert websocket_config_obj.request_timeout == 100 - assert websocket_config_obj._ssl_context == ssl.SSLContext - assert websocket_config_obj._verify_ssl is False - assert websocket_config_obj.token == "token" - assert websocket_config_obj.shard_ids == [5, 6, 7, 8, 9, 10] - assert websocket_config_obj.shard_count == 17 - - def test_empty_deserialize(self): - websocket_config_obj = http_settings.GatewayConfig.deserialize({}) - - assert websocket_config_obj.gateway_use_compression is True - assert websocket_config_obj.gateway_version == 6 - assert websocket_config_obj.initial_activity is None - assert websocket_config_obj.initial_status == guilds.PresenceStatus.ONLINE - assert websocket_config_obj.initial_idle_since is None - assert websocket_config_obj.intents is None - assert websocket_config_obj.large_threshold == 250 - assert websocket_config_obj._debug is False - assert websocket_config_obj._allow_redirects is False - assert websocket_config_obj.tcp_connector is None - assert websocket_config_obj._proxy_headers is None - assert websocket_config_obj._proxy_auth is None - assert websocket_config_obj._proxy_url is None - assert websocket_config_obj.request_timeout is None - assert websocket_config_obj._ssl_context is None - assert websocket_config_obj._verify_ssl is True - assert websocket_config_obj.token is None - assert websocket_config_obj.shard_ids is None - assert websocket_config_obj.shard_count is None - - -class TestParseShardInfo: - def test__parse_shard_info_when_exclusive_range(self): - assert http_settings._parse_shard_info("0..2") == [0, 1] - - def test__parse_shard_info_when_inclusive_range(self): - assert http_settings._parse_shard_info("0...2") == [0, 1, 2] - - def test__parse_shard_info_when_specific_id(self): - assert http_settings._parse_shard_info(2) == [2] - - def test__parse_shard_info_when_list(self): - assert http_settings._parse_shard_info([2, 5, 6]) == [2, 5, 6] - - @_helpers.assert_raises(type_=ValueError) - def test__parse_shard_info_when_invalid(self): - http_settings._parse_shard_info("something invalid") - - -class TestRESTConfig: - def test_deserialize(self, test_rest_config): - rest_config_obj = http_settings.RESTConfig.deserialize(test_rest_config) - - assert rest_config_obj.rest_version == 6 - assert rest_config_obj._allow_redirects is True - assert rest_config_obj.tcp_connector == aiohttp.TCPConnector - assert rest_config_obj._proxy_headers == {"Some-Header": "headercontent"} - assert rest_config_obj._proxy_auth == aiohttp.BasicAuth.decode( - "basic Tm90aGluZyB0byBzZWUgaGVyZSA6IGpvaW4gZGlzY29yZC5nZy9IS0dQRTlRIDopIH5kYXZmc2E=" - ) - assert rest_config_obj._proxy_url == "_proxy_url" - assert rest_config_obj.request_timeout == 100 - assert rest_config_obj._ssl_context == ssl.SSLContext - assert rest_config_obj._verify_ssl is False - assert rest_config_obj.token == "token" - assert rest_config_obj.rest_url == "foobar" - assert rest_config_obj.oauth2_url == "quxquxx" - - def test_empty_deserialize(self): - rest_config_obj = http_settings.RESTConfig.deserialize({}) - - assert rest_config_obj.rest_version == 6 - assert rest_config_obj._allow_redirects is False - assert rest_config_obj.tcp_connector is None - assert rest_config_obj._proxy_headers is None - assert rest_config_obj._proxy_auth is None - assert rest_config_obj._proxy_url is None - assert rest_config_obj.request_timeout is None - assert rest_config_obj._ssl_context is None - assert rest_config_obj._verify_ssl is True - assert rest_config_obj.token is None - assert rest_config_obj.rest_url == urls.REST_API_URL - assert rest_config_obj.oauth2_url == urls.OAUTH2_API_URL - - -class TestBotConfig: - def test_deserialize(self, test_bot_config): - datetime_obj = datetime.datetime.now() - test_bot_config["initial_idle_since"] = datetime_obj.timestamp() - mock_activity = mock.MagicMock(gateway.Activity) - with _helpers.patch_marshal_attr( - http_settings.BotConfig, - "initial_activity", - deserializer=gateway.Activity.deserialize, - return_value=mock_activity, - ) as patched_activity_deserializer: - bot_config_obj = http_settings.BotConfig.deserialize(test_bot_config) - patched_activity_deserializer.assert_called_once_with({"name": "test", "url": "some_url", "type": 0}) - - assert bot_config_obj.rest_version == 6 - assert bot_config_obj._allow_redirects is True - assert bot_config_obj.tcp_connector == aiohttp.TCPConnector - assert bot_config_obj._proxy_headers == {"Some-Header": "headercontent"} - assert bot_config_obj._proxy_auth == aiohttp.BasicAuth.decode( - "basic Tm90aGluZyB0byBzZWUgaGVyZSA6IGpvaW4gZGlzY29yZC5nZy9IS0dQRTlRIDopIH5kYXZmc2E=" - ) - assert bot_config_obj._proxy_url == "_proxy_url" - assert bot_config_obj.request_timeout == 100 - assert bot_config_obj._ssl_context == ssl.SSLContext - assert bot_config_obj._verify_ssl is False - assert bot_config_obj.token == "token" - assert bot_config_obj.shard_ids == [5, 6, 7, 8, 9, 10] - assert bot_config_obj.shard_count == 17 - assert bot_config_obj.gateway_use_compression is False - assert bot_config_obj.gateway_version == 6 - assert bot_config_obj.initial_activity is mock_activity - assert bot_config_obj.initial_status == guilds.PresenceStatus.DND - assert bot_config_obj.initial_idle_since == datetime_obj - assert bot_config_obj.intents == intents.Intent.GUILD_MESSAGES | intents.Intent.GUILDS - assert bot_config_obj.large_threshold == 1000 - assert bot_config_obj._debug is True - assert bot_config_obj.rest_url == "foobar" - assert bot_config_obj.oauth2_url == "quxquxx" - - def test_empty_deserialize(self): - bot_config_obj = http_settings.BotConfig.deserialize({}) - - assert bot_config_obj.rest_version == 6 - assert bot_config_obj._allow_redirects is False - assert bot_config_obj.tcp_connector is None - assert bot_config_obj._proxy_headers is None - assert bot_config_obj._proxy_auth is None - assert bot_config_obj._proxy_url is None - assert bot_config_obj.request_timeout is None - assert bot_config_obj._ssl_context is None - assert bot_config_obj._verify_ssl is True - assert bot_config_obj.token is None - assert bot_config_obj.shard_ids is None - assert bot_config_obj.shard_count is None - assert bot_config_obj.gateway_use_compression is True - assert bot_config_obj.gateway_version == 6 - assert bot_config_obj.initial_activity is None - assert bot_config_obj.initial_status == guilds.PresenceStatus.ONLINE - assert bot_config_obj.initial_idle_since is None - assert bot_config_obj.intents is None - assert bot_config_obj.large_threshold == 250 - assert bot_config_obj._debug is False - assert bot_config_obj.rest_url == urls.REST_API_URL - assert bot_config_obj.oauth2_url == urls.OAUTH2_API_URL diff --git a/tests/hikari/test_errors.py b/tests/hikari/test_errors.py deleted file mode 100644 index 156905a935..0000000000 --- a/tests/hikari/test_errors.py +++ /dev/null @@ -1,153 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# Copyright © Nekokatt 2019-2020 -# -# This file is part of Hikari. -# -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . -import http - -import pytest - -from hikari import errors -from hikari.internal import codes - - -class TestHikariError: - def test_repr(self): - class ErrorImpl(errors.HikariError): - def __str__(self): - return "program go boom!" - - inst = ErrorImpl() - - assert repr(inst) == "ErrorImpl('program go boom!')" - - -class TestGatewayError: - def test_init(self): - err = errors.GatewayError("boom") - assert err.reason == "boom" - - def test_str(self): - err = errors.GatewayError("boom") - assert str(err) == "boom" - - -class TestGatewayClientClosedError: - def test_init(self): - err = errors.GatewayClientClosedError("blah") - assert err.reason == "blah" - - -class TestGatewayConnectionClosedError: - def test_init_valid_reason(self): - err = errors.GatewayServerClosedConnectionError(codes.GatewayCloseCode.UNKNOWN_OPCODE, "foo") - assert err.close_code == codes.GatewayCloseCode.UNKNOWN_OPCODE.value - assert err.reason == "foo" - - def test_init_valid_close_code(self): - err = errors.GatewayServerClosedConnectionError(codes.GatewayCloseCode.UNKNOWN_OPCODE) - assert err.close_code == codes.GatewayCloseCode.UNKNOWN_OPCODE.value - assert err.reason.endswith(f" ({codes.GatewayCloseCode.UNKNOWN_OPCODE.name})") - - def test_init_invalid_close_code(self): - err = errors.GatewayServerClosedConnectionError(69) - assert err.close_code == 69 - assert err.reason.endswith(" (69)") - - def test_init_no_close_code(self): - err = errors.GatewayServerClosedConnectionError() - assert err.close_code is None - assert err.reason.endswith(" (no reason)") - - -class TestGatewayInvalidTokenError: - def test_init(self): - err = errors.GatewayInvalidTokenError() - assert err.close_code == codes.GatewayCloseCode.AUTHENTICATION_FAILED - - -class TestGatewayInvalidSessionError: - @pytest.mark.parametrize("can_resume", (True, False)) - def test_init(self, can_resume): - err = errors.GatewayInvalidSessionError(can_resume) - assert err.close_code is None - - -class TestGatewayMustReconnectError: - def test_init(self): - err = errors.GatewayMustReconnectError() - assert err.close_code is None - - -class TestGatewayNeedsShardingError: - def test_init(self): - err = errors.GatewayNeedsShardingError() - assert err.close_code == codes.GatewayCloseCode.SHARDING_REQUIRED - - -class TestGatewayZombiedError: - def test_init(self): - err = errors.GatewayZombiedError() - assert err.reason.startswith("No heartbeat was received") - - -@pytest.mark.parametrize( - ("type", "expected_status", "expected_status_name"), - [ - (errors.BadRequest, http.HTTPStatus.BAD_REQUEST, "Bad Request"), - (errors.Unauthorized, http.HTTPStatus.UNAUTHORIZED, "Unauthorized"), - (errors.Forbidden, http.HTTPStatus.FORBIDDEN, "Forbidden"), - (errors.NotFound, http.HTTPStatus.NOT_FOUND, "Not Found"), - ( - lambda u, h, pl: errors.ClientHTTPErrorResponse(u, http.HTTPStatus.TOO_MANY_REQUESTS, h, pl), - http.HTTPStatus.TOO_MANY_REQUESTS, - "Too Many Requests", - ), - ( - lambda u, h, pl: errors.ServerHTTPErrorResponse(u, http.HTTPStatus.INTERNAL_SERVER_ERROR, h, pl), - http.HTTPStatus.INTERNAL_SERVER_ERROR, - "Internal Server Error", - ), - ( - lambda u, h, pl: errors.HTTPErrorResponse(u, http.HTTPStatus.CONTINUE, h, pl), - http.HTTPStatus.CONTINUE, - "Continue", - ), - ], -) -class TestHTTPClientErrors: - def test_init(self, type, expected_status, expected_status_name): - ex = type("http://foo.bar/api/v69/nice", {"foo": "bar"}, b"body") - - assert ex.status == expected_status - assert ex.url == "http://foo.bar/api/v69/nice" - assert ex.headers == {"foo": "bar"} - assert ex.raw_body == b"body" - - def test_str_if_unicode_bytestring(self, type, expected_status, expected_status_name): - ex = type("http://foo.bar/api/v69/nice", {"foo": "bar"}, b"body") - - assert str(ex) == f"{expected_status} {expected_status_name}: body" - - def test_str_if_not_unicode_bytestring(self, type, expected_status, expected_status_name): - ex = type("http://foo.bar/api/v69/nice", {"foo": "bar"}, b"\x1f\x0f\xff\xff\xff") - - assert str(ex) == f"{expected_status} {expected_status_name}: b'\\x1f\\x0f\\xff\\xff\\xff'" - - def test_str_if_payload(self, type, expected_status, expected_status_name): - ex = type("http://foo.bar/api/v69/nice", {"foo": "bar"}, {"code": 0, "message": "you broke it"}) - - assert str(ex) == f"{expected_status} {expected_status_name}: {{'code': 0, 'message': 'you broke it'}}" From d59fee494ade077516b854e693104d4bd7d7dfef Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Sat, 23 May 2020 11:25:41 +0100 Subject: [PATCH 377/922] Removed old channel.py, made trigger_typing a context manager optionally, added stubbed entity factory methods so I can actually run my code. --- hikari/.rest/channel.py | 1123 --------------------------------- hikari/impl/entity_factory.py | 138 +++- hikari/net/rest.py | 76 ++- 3 files changed, 185 insertions(+), 1152 deletions(-) delete mode 100644 hikari/.rest/channel.py diff --git a/hikari/.rest/channel.py b/hikari/.rest/channel.py deleted file mode 100644 index 84897ef38e..0000000000 --- a/hikari/.rest/channel.py +++ /dev/null @@ -1,1123 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# Copyright © Nekoka.tt 2019-2020 -# -# This file is part of Hikari. -# -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . -"""The logic for handling requests to channel endpoints.""" - -from __future__ import annotations - -__all__ = ["RESTChannelComponent"] - -import abc -import datetime -import typing - -from hikari import pagination -from hikari.internal import helpers -from hikari.models import bases -from hikari.models import channels as channels_ -from hikari.models import invites -from hikari.models import messages as messages_ -from hikari.models import webhooks -from hikari.net.rest import base - -if typing.TYPE_CHECKING: - from hikari.models import embeds as embeds_ - from hikari.models import files as files_ - from hikari.models import guilds - from hikari.models import permissions as permissions_ - from hikari.models import users - - from hikari.internal import more_typing - - -class _MessagePaginator(pagination.BufferedPaginatedResults[messages_.Message]): - __slots__ = ("_app", "_channel_id", "_direction", "_first_id", "_session") - - def __init__(self, app, channel, direction, first, session) -> None: - super().__init__() - self._app = app - self._channel_id = str(int(channel)) - self._direction = direction - self._first_id = ( - str(bases.Snowflake.from_datetime(first)) if isinstance(first, datetime.datetime) else str(int(first)) - ) - self._session = session - - async def _next_chunk(self): - kwargs = { - self._direction: self._first_id, - "channel": self._channel_id, - "limit": 100, - } - - chunk = await self._session.get_channel_messages(**kwargs) - - if not chunk: - return None - if self._direction == "after": - chunk.reverse() - - self._first_id = chunk[-1]["id"] - - return (messages_.Message.deserialize(m, app=self._app) for m in chunk) - - -class RESTChannelComponent(base.BaseRESTComponent, abc.ABC): # pylint: disable=abstract-method, too-many-public-methods - """The RESTSession client component for handling requests to channel endpoints.""" - - async def fetch_channel( - self, channel: typing.Union[bases.Snowflake, int, str, channels_.PartialChannel] - ) -> channels_.PartialChannel: - """Get an up to date channel object from a given channel object or ID. - - Parameters - ---------- - channel : hikari.models.channels.PartialChannel | hikari.models.bases.Snowflake | int | str - The object ID of the channel to look up. - - Returns - ------- - hikari.models.channels.PartialChannel - The channel object that has been found. - - Raises - ------ - hikari.errors.BadRequest - If any invalid snowflake IDs are passed; a snowflake may be invalid - due to it being outside of the range of a 64 bit integer. - hikari.errors.Forbidden - If you don't have access to the channel. - hikari.errors.NotFound - If the channel does not exist. - """ - payload = await self._session.get_channel( - channel_id=str(channel.id if isinstance(channel, bases.Unique) else int(channel)) - ) - return channels_.deserialize_channel(payload, app=self._app) - - async def update_channel( - self, - channel: typing.Union[bases.Snowflake, int, str, channels_.PartialChannel], - *, - name: str = ..., - position: int = ..., - topic: str = ..., - nsfw: bool = ..., - bitrate: int = ..., - user_limit: int = ..., - rate_limit_per_user: typing.Union[int, datetime.timedelta] = ..., - permission_overwrites: typing.Sequence[channels_.PermissionOverwrite] = ..., - parent_category: typing.Optional[typing.Union[bases.Snowflake, int, str, channels_.GuildCategory]] = ..., - reason: str = ..., - ) -> channels_.PartialChannel: - """Update one or more aspects of a given channel ID. - - Parameters - ---------- - channel : hikari.models.channels.PartialChannel | hikari.models.bases.Snowflake | int | str - The channel ID to update. - name : str - If specified, the new name for the channel. This must be - inclusively between `1` and `100` characters in length. - position : int - If specified, the position to change the channel to. - topic : str - If specified, the topic to set. This is only applicable to - text channels. This must be inclusively between `0` and `1024` - characters in length. - nsfw : bool - Mark the channel as being not safe for work (NSFW) if `True`. - If `False` or unspecified, then the channel is not marked as - NSFW. Will have no visible effect for non-text guild channels. - rate_limit_per_user : datetime.timedelta | int - If specified, the time delta of seconds the user has to wait - before sending another message. This will not apply to bots, or to - members with `MANAGE_MESSAGES` or `MANAGE_CHANNEL` permissions. - This must be inclusively between `0` and `21600` seconds. - bitrate : int - If specified, the bitrate in bits per second allowable for the - channel. This only applies to voice channels and must be inclusively - between `8000` and `96000` for normal servers or `8000` and - `128000` for VIP servers. - user_limit : int - If specified, the new max number of users to allow in a voice - channel. This must be between `0` and `99` inclusive, where - `0` implies no limit. - permission_overwrites : typing.Sequence[hikari.models.channels.PermissionOverwrite] - If specified, the new list of permission overwrites that are - category specific to replace the existing overwrites with. - parent_category : hikari.models.channels.GuildCategory | hikari.models.bases.Snowflake | int | None - If specified, the new parent category ID to set for the channel, - pass `None` to unset. - reason : str - If specified, the audit log reason explaining why the operation - was performed. - - Returns - ------- - hikari.models.channels.PartialChannel - The channel object that has been modified. - - Raises - ------ - hikari.errors.NotFound - If the channel does not exist. - hikari.errors.Forbidden - If you lack the permission to make the change. - hikari.errors.BadRequest - If you provide incorrect options for the corresponding channel type - (e.g. a `bitrate` for a text channel). - If any invalid snowflake IDs are passed; a snowflake may be invalid - due to it being outside of the range of a 64 bit integer. - """ - payload = await self._session.modify_channel( - channel_id=str(channel.id if isinstance(channel, bases.Unique) else int(channel)), - name=name, - position=position, - topic=topic, - nsfw=nsfw, - bitrate=bitrate, - user_limit=user_limit, - rate_limit_per_user=( - int(rate_limit_per_user.total_seconds()) - if isinstance(rate_limit_per_user, datetime.timedelta) - else rate_limit_per_user - ), - permission_overwrites=( - [po.serialize() for po in permission_overwrites] if permission_overwrites is not ... else ... - ), - parent_id=( - str(parent_category.id if isinstance(parent_category, bases.Unique) else int(parent_category)) - if parent_category is not ... and parent_category is not None - else parent_category - ), - reason=reason, - ) - return channels_.deserialize_channel(payload, app=self._app) - - async def delete_channel(self, channel: typing.Union[bases.Snowflake, int, str, channels_.PartialChannel]) -> None: - """Delete the given channel ID, or if it is a DM, close it. - - Parameters - ---------- - channel : hikari.models.channels.PartialChannel | hikari.models.bases.Snowflake | int | str - The object or ID of the channel to delete. - - Returns - ------- - None - Nothing, unlike what the API specifies. This is done to maintain - consistency with other calls of a similar nature in this API - wrapper. - - Raises - ------ - hikari.errors.BadRequest - If any invalid snowflake IDs are passed; a snowflake may be invalid - due to it being outside of the range of a 64 bit integer. - hikari.errors.NotFound - If the channel does not exist. - hikari.errors.Forbidden - If you do not have permission to delete the channel. - - !!! note - Closing a DM channel won't raise an exception but will have no - effect and "closed" DM channels will not have to be reopened to send - messages in theme. - - !!! warning - Deleted channels cannot be un-deleted. - """ - await self._session.delete_close_channel( - channel_id=str(channel.id if isinstance(channel, bases.Unique) else int(channel)) - ) - - @typing.overload - def fetch_messages( - self, channel: typing.Union[bases.Snowflake, int, str, channels_.PartialChannel] - ) -> pagination.PaginatedResults[messages_.Message]: - """Fetch the channel history, starting with the newest messages.""" - - @typing.overload - def fetch_messages( - self, - channel: typing.Union[bases.Snowflake, int, str, channels_.PartialChannel], - before: typing.Union[datetime.datetime, int, str, bases.Unique, bases.Snowflake], - ) -> pagination.PaginatedResults[messages_.Message]: - """Fetch the channel history before a given message/time.""" - - @typing.overload - def fetch_messages( - self, - channel: typing.Union[bases.Snowflake, int, str, channels_.PartialChannel], - after: typing.Union[datetime.datetime, int, str, bases.Unique, bases.Snowflake], - ) -> pagination.PaginatedResults[messages_.Message]: - """Fetch the channel history after a given message/time.""" - - @typing.overload - def fetch_messages( - self, - channel: typing.Union[bases.Snowflake, int, str, channels_.PartialChannel], - around: typing.Union[datetime.datetime, int, str, bases.Unique, bases.Snowflake], - ) -> pagination.PaginatedResults[messages_.Message]: - """Fetch the channel history around a given message/time.""" - - def fetch_messages( - self, - channel: typing.Union[bases.Snowflake, int, str, channels_.PartialChannel], - **kwargs: typing.Union[datetime.datetime, int, str, bases.Unique, bases.Snowflake], - ) -> pagination.PaginatedResults[messages_.Message]: - """Fetch messages from the channel's history. - - Parameters - ---------- - channel : hikari.models.channels.PartialChannel | hikari.models.bases.Snowflake | int | str - The channel to fetch messages from. - - Keyword Arguments - ----------------- - before : datetime.datetime | hikari.models.bases.Unique | hikari.models.bases.Snowflake | int | str - If a unique object (like a message), then message created before - this object will be returned. If a datetime, then messages before - that datetime will be returned. If unspecified or None, the filter - is not used. - after : datetime.datetime | hikari.models.bases.Unique | hikari.models.bases.Snowflake | int | str - If a unique object (like a message), then message created after this - object will be returned. If a datetime, then messages after that - datetime will be returned. If unspecified or None, the filter is not - used. - around : datetime.datetime | hikari.models.bases.Unique | hikari.models.bases.Snowflake | int | str - If a unique object (like a message), then message created around the - same time as this object will be returned. If a datetime, then - messages around that datetime will be returned. If unspecified or - `None`, the filter is not used. - - !!! info - Using `before` or no filter will return messages in the order - of newest-to-oldest. Using the `after` filter will return - messages in the order of oldest-to-newest. Using th `around` - filter may have arbitrary ordering. - - !!! warning - Only one of `before`, `after`, or `around` may be specified. - - !!! note - Passing no value for `before`, `after`, or `around` will have the - same effect as passing `before=hikari.models.bases.Snowflake.max()`. This - will return all messages that can be found, newest to oldest. - - Examples - -------- - Fetching the last 20 messages before May 2nd, 2020: - - timestamp = datetime.datetime(2020, 5, 2) - - async for message in rest.fetch_messages(channel, before=timestamp).limit(20): - print(message.author, message.content) - - Fetching messages sent around the same time as a given message. - - async for message in rest.fetch_messages(channel, around=event.message): - print(message.author, message.content) - - Fetching messages after May 3rd, 2020 at 15:33 UTC. - - timestamp = datetime.datetime(2020, 5, 3, 15, 33, tzinfo=datetime.timezone.utc) - - async for message in rest.fetch_messages(channel, after=timestamp): - print(message.author, message.content) - - Fetching all messages, newest to oldest: - - async for message in rest.fetch_messages(channel, before=datetime.datetime.utcnow()): - print(message) - - # More efficient alternative - async for message in rest.fetch_messages(channel): - print(message) - - Fetching all messages, oldest to newest: - - async for message in rest.fetch_messages(channel, after=): - print(message) - - !!! warning - `datetime.datetime` objects are expected to be `utc` if timezone - naieve (which they are by default). This means that - `datetime.datetime.now` will always be treated as if it were - UTC unless you specify a timezone. Thus, it is important to always - use `datetime.datetime.utcnow` over `datetime.datetime.now` if you - want your application to work outside the `GMT+0` timezone. - - !!! note - The `around` parameter is not documented clearly by Discord. - The actual number of messages returned by this, and the _direction - (e.g. older/newer/both) is not overly intuitive. Thus, this - specific functionality may be deprecated in the future in favour - of a cleaner Python API until a time comes where this information is - documented at a RESTSession API level by Discord. - - Returns - ------- - hikari.models.pagination.PaginatedResults[hikari.models.messages.Message] - An async iterator of message objects. - - Raises - ------ - hikari.errors.NotFound - If the channel is not found. - hikari.errors.Forbidden - If you are missing the `READ_MESSAGE_HISTORY` permission for the - channel or guild. - """ - if len(kwargs) > 1: - raise TypeError("only one of 'before', 'after', 'around' can be specified") - - try: - direction, first = kwargs.popitem() - except KeyError: - direction, first = "before", bases.Snowflake.max() - - return _MessagePaginator( - app=self._app, channel=channel, direction=direction, first=first, session=self._session, - ) - - async def fetch_message( - self, - channel: typing.Union[bases.Snowflake, int, str, channels_.PartialChannel], - message: typing.Union[bases.Snowflake, int, str, messages_.Message], - ) -> messages_.Message: - """Get a message from known channel that we can access. - - Parameters - ---------- - channel : hikari.models.channels.PartialChannel | hikari.models.bases.Snowflake | int | str - The object or ID of the channel to get the message from. - message : hikari.models.messages.Message | hikari.models.bases.Snowflake | int | str - The object or ID of the message to retrieve. - - Returns - ------- - hikari.models.messages.Message - The found message object. - - !!! note - This requires the `READ_MESSAGE_HISTORY` permission. - - Raises - ------ - hikari.errors.BadRequest - If any invalid snowflake IDs are passed; a snowflake may be invalid - due to it being outside of the range of a 64 bit integer. - hikari.errors.Forbidden - If you lack permission to see the message. - hikari.errors.NotFound - If the channel or message is not found. - """ - payload = await self._session.get_channel_message( - channel_id=str(channel.id if isinstance(channel, bases.Unique) else int(channel)), - message_id=str(message.id if isinstance(message, bases.Unique) else int(message)), - ) - return messages_.Message.deserialize(payload, app=self._app) - - async def create_message( # pylint: disable=line-too-long - self, - channel: typing.Union[bases.Snowflake, int, str, channels_.PartialChannel], - *, - content: str = ..., - nonce: str = ..., - tts: bool = ..., - files: typing.Sequence[files_.BaseStream] = ..., - embed: embeds_.Embed = ..., - mentions_everyone: bool = True, - user_mentions: typing.Union[ - typing.Collection[typing.Union[bases.Snowflake, int, str, users.User]], bool - ] = True, - role_mentions: typing.Union[ - typing.Collection[typing.Union[bases.Snowflake, int, str, guilds.Role]], bool - ] = True, - ) -> messages_.Message: - """Create a message in the given channel. - - Parameters - ---------- - channel : hikari.models.channels.PartialChannel | hikari.models.bases.Snowflake | int | str - The channel or ID of the channel to send to. - content : str - If specified, the message content to send with the message. - nonce : str - If specified, an optional ID to send for opportunistic message - creation. Any created message will have this nonce set on it. - Nonces are limited to 32 bytes in size. - tts : bool - If specified, whether the message will be sent as a TTS message. - files : typing.Sequence[hikari.models.files.BaseStream] - If specified, a sequence of files to upload, if desired. Should be - between 1 and 10 objects in size (inclusive), also including embed - attachments. - embed : hikari.models.embeds.Embed - If specified, the embed object to send with the message. - mentions_everyone : bool - Whether `@everyone` and `@here` mentions should be resolved by - discord and lead to actual pings, defaults to `True`. - user_mentions : typing.Collection[hikari.models.users.User | hikari.models.bases.Snowflake | int | str] | bool - Either an array of user objects/IDs to allow mentions for, - `True` to allow all user mentions or `False` to block all - user mentions from resolving, defaults to `True`. - role_mentions : typing.Collection[hikari.models.guilds.Role | hikari.models.bases.Snowflake | int | str] | bool - Either an array of guild role objects/IDs to allow mentions for, - `True` to allow all role mentions or `False` to block all - role mentions from resolving, defaults to `True`. - - Returns - ------- - hikari.models.messages.Message - The created message object. - - Raises - ------ - hikari.errors.NotFound - If the channel is not found. - hikari.errors.BadRequest - This can be raised if the file is too large; if the embed exceeds - the defined limits; if the message content is specified only and - empty or greater than `2000` characters; if neither content, files - or embed are specified. - If any invalid snowflake IDs are passed; a snowflake may be invalid - due to it being outside of the range of a 64 bit integer. - If you are trying to upload more than 10 files in total (including - embed attachments). - hikari.errors.Forbidden - If you lack permissions to send to this channel. - ValueError - If more than 100 unique objects/entities are passed for - `role_mentions` or `user_mentions`. - """ - file_resources = [] - if files is not ...: - file_resources += files - if embed is not ...: - file_resources += embed.assets_to_upload - - payload = await self._session.create_message( - channel_id=str(channel.id if isinstance(channel, bases.Unique) else int(channel)), - content=content, - nonce=nonce, - tts=tts, - files=file_resources if file_resources else ..., - embed=embed.serialize() if embed is not ... else ..., - allowed_mentions=helpers.generate_allowed_mentions( - mentions_everyone=mentions_everyone, user_mentions=user_mentions, role_mentions=role_mentions - ), - ) - return messages_.Message.deserialize(payload, app=self._app) - - def safe_create_message( - self, - channel: typing.Union[bases.Snowflake, int, str, channels_.PartialChannel], - *, - content: str = ..., - nonce: str = ..., - tts: bool = ..., - files: typing.Sequence[files_.BaseStream] = ..., - embed: embeds_.Embed = ..., - mentions_everyone: bool = False, - user_mentions: typing.Union[ - typing.Collection[typing.Union[bases.Snowflake, int, str, users.User]], bool - ] = False, - role_mentions: typing.Union[ - typing.Collection[typing.Union[bases.Snowflake, int, str, guilds.Role]], bool - ] = False, - ) -> more_typing.Coroutine[messages_.Message]: - """Create a message in the given channel with mention safety. - - This endpoint has the same signature as - `RESTChannelComponent.create_message` with the only difference being - that `mentions_everyone`, `user_mentions` and `role_mentions` default to - `False`. - """ - return self.create_message( - channel=channel, - content=content, - nonce=nonce, - tts=tts, - files=files, - embed=embed, - mentions_everyone=mentions_everyone, - user_mentions=user_mentions, - role_mentions=role_mentions, - ) - - async def update_message( # pylint: disable=line-too-long - self, - channel: typing.Union[bases.Snowflake, int, str, channels_.PartialChannel], - message: typing.Union[bases.Snowflake, int, str, messages_.Message], - *, - content: typing.Optional[str] = ..., - embed: typing.Optional[embeds_.Embed] = ..., - flags: int = ..., - mentions_everyone: bool = True, - user_mentions: typing.Union[ - typing.Collection[typing.Union[bases.Snowflake, int, str, users.User]], bool - ] = True, - role_mentions: typing.Union[ - typing.Collection[typing.Union[bases.Snowflake, int, str, guilds.Role]], bool - ] = True, - ) -> messages_.Message: - """Update the given message. - - Parameters - ---------- - channel : hikari.models.channels.PartialChannel | hikari.models.bases.Snowflake | int | str - The object or ID of the channel to get the message from. - message : hikari.models.messages.Message | hikari.models.bases.Snowflake | int | str - The object or ID of the message to edit. - content : str | None - If specified, the string content to replace with in the message. - If `None`, then the content will be removed from the message. - embed : hikari.models.embeds.Embed | None - If specified, then the embed to replace with in the message. - If `None`, then the embed will be removed from the message. - flags : hikari.models.messages.MessageFlag - If specified, the new flags for this message, while a raw int may - be passed for this, this can lead to unexpected behaviour if it's - outside the range of the MessageFlag int flag. - mentions_everyone : bool - Whether `@everyone` and `@here` mentions should be resolved by - discord and lead to actual pings, defaults to `True`. - user_mentions : typing.Collection[hikari.models.users.User | hikari.models.bases.Snowflake | int | str] | bool - Either an array of user objects/IDs to allow mentions for, - `True` to allow all user mentions or `False` to block all - user mentions from resolving, defaults to `True`. - role_mentions : typing.Collection[hikari.models.guilds.Role | hikari.models.bases.Snowflake | int | str] | bool - Either an array of guild role objects/IDs to allow mentions for, - `True` to allow all role mentions or `False` to block all - role mentions from resolving, defaults to `True`. - - Returns - ------- - hikari.models.messages.Message - The edited message object. - - Raises - ------ - hikari.errors.NotFound - If the channel or message is not found. - hikari.errors.BadRequest - This can be raised if the embed exceeds the defined limits; - if the message content is specified only and empty or greater - than `2000` characters; if neither content, file or embed - are specified. - If any invalid snowflake IDs are passed; a snowflake may be invalid - due to it being outside of the range of a 64 bit integer. - hikari.errors.Forbidden - If you try to edit `content` or `embed` or `allowed_mentions` - on a message you did not author. - If you try to edit the flags on a message you did not author without - the `MANAGE_MESSAGES` permission. - ValueError - If more than 100 unique objects/entities are passed for - `role_mentions` or `user_mentions`. - """ - payload = await self._session.edit_message( - channel_id=str(channel.id if isinstance(channel, bases.Unique) else int(channel)), - message_id=str(message.id if isinstance(message, bases.Unique) else int(message)), - content=content, - embed=embed.serialize() if embed is not ... and embed is not None else embed, - flags=flags, - allowed_mentions=helpers.generate_allowed_mentions( - mentions_everyone=mentions_everyone, user_mentions=user_mentions, role_mentions=role_mentions, - ), - ) - return messages_.Message.deserialize(payload, app=self._app) - - def safe_update_message( - self, - message: typing.Union[bases.Snowflake, int, str, messages_.Message], - channel: typing.Union[bases.Snowflake, int, str, channels_.PartialChannel], - *, - content: typing.Optional[str] = ..., - embed: typing.Optional[embeds_.Embed] = ..., - flags: int = ..., - mentions_everyone: bool = False, - user_mentions: typing.Union[ - typing.Collection[typing.Union[bases.Snowflake, int, str, users.User]], bool - ] = False, - role_mentions: typing.Union[ - typing.Collection[typing.Union[bases.Snowflake, int, str, guilds.Role]], bool - ] = False, - ) -> more_typing.Coroutine[messages_.Message]: - """Update a message in the given channel with mention safety. - - This endpoint has the same signature as - `RESTChannelComponent.update_message` with the only difference being - that `mentions_everyone`, `user_mentions` and `role_mentions` default to - `False`. - """ - return self.update_message( - message=message, - channel=channel, - content=content, - embed=embed, - flags=flags, - mentions_everyone=mentions_everyone, - user_mentions=user_mentions, - role_mentions=role_mentions, - ) - - async def delete_messages( - self, - channel: typing.Union[bases.Snowflake, int, str, channels_.PartialChannel], - message: typing.Union[bases.Snowflake, int, str, messages_.Message], - *additional_messages: typing.Union[bases.Snowflake, int, str, messages_.Message], - ) -> None: - """Delete a message in a given channel. - - Parameters - ---------- - channel : hikari.models.channels.PartialChannel | hikari.models.bases.Snowflake | int | str - The object or ID of the channel to get the message from. - message : message : hikari.models.messages.Message | hikari.models.bases.Snowflake | int | str - The object or ID of the message to delete. - *additional_messages : message : hikari.models.messages.Message | hikari.models.bases.Snowflake | int | str - Objects and/or IDs of additional messages to delete in the same - channel, in total you can delete up to 100 messages in a request. - - Raises - ------ - hikari.errors.BadRequest - If any invalid snowflake IDs are passed; a snowflake may be invalid - due to it being outside of the range of a 64 bit integer. - hikari.errors.Forbidden - If you did not author the message and are in a DM, or if you did - not author the message and lack the `MANAGE_MESSAGES` - permission in a guild channel. - hikari.errors.NotFound - If the channel or message is not found. - ValueError - If you try to delete over `100` messages in a single request. - - !!! note - This can only be used on guild text channels. - Any message IDs that do not exist or are invalid still add towards - the total `100` max messages to remove. This can only delete - messages that are newer than `2` weeks in age. If any of the - messages are older than `2` weeks then this call will fail. - """ - if additional_messages: - messages = list( - # dict.fromkeys is used to remove duplicate entries that would cause discord to return an error. - dict.fromkeys( - str(m.id if isinstance(m, bases.Unique) else int(m)) for m in (message, *additional_messages) - ) - ) - if len(messages) > 100: - raise ValueError("Only up to 100 messages can be bulk deleted in a single request.") - - if len(messages) > 1: - await self._session.bulk_delete_messages( - channel_id=str(channel.id if isinstance(channel, bases.Unique) else int(channel)), - messages=messages, - ) - return None - - await self._session.delete_message( - channel_id=str(channel.id if isinstance(channel, bases.Unique) else int(channel)), - message_id=str(message.id if isinstance(message, bases.Unique) else int(message)), - ) - - async def update_channel_overwrite( # pylint: disable=line-too-long - self, - channel: typing.Union[bases.Snowflake, int, str, channels_.PartialChannel], - overwrite: typing.Union[channels_.PermissionOverwrite, users.User, guilds.Role, bases.Snowflake, int], - target_type: typing.Union[channels_.PermissionOverwriteType, str], - *, - allow: typing.Union[permissions_.Permission, int] = ..., - deny: typing.Union[permissions_.Permission, int] = ..., - reason: str = ..., - ) -> None: - """Edit permissions for a given channel. - - Parameters - ---------- - channel : hikari.models.channels.PartialChannel | hikari.models.bases.Snowflake | int | str - The object or ID of the channel to edit permissions for. - overwrite : hikari.models.channels.PermissionOverwrite | hikari.models.guilds.Role | hikari.models.users.User | hikari.models.bases.Snowflake | int - The object or ID of the target member or role to create/edit the - overwrite for. - target_type : hikari.models.channels.PermissionOverwriteType | int - The type of overwrite, passing a raw string that's outside of the - enum's range for this may lead to unexpected behaviour. - allow : hikari.permissions.Permission | int - If specified, the value of all permissions to set to be allowed, - passing a raw integer for this may lead to unexpected behaviour. - deny : hikari.permissions.Permission | int - If specified, the value of all permissions to set to be denied, - passing a raw integer for this may lead to unexpected behaviour. - reason : str - If specified, the audit log reason explaining why the operation - was performed. - - Raises - ------ - hikari.errors.BadRequest - If any invalid snowflake IDs are passed; a snowflake may be invalid - due to it being outside of the range of a 64 bit integer. - hikari.errors.NotFound - If the target channel or overwrite doesn't exist. - hikari.errors.Forbidden - If you lack permission to do this. - """ - # pylint: enable=line-too-long - await self._session.edit_channel_permissions( - channel_id=str(channel.id if isinstance(channel, bases.Unique) else int(channel)), - overwrite_id=str(overwrite.id if isinstance(overwrite, bases.Unique) else int(overwrite)), - type_=target_type, - allow=allow, - deny=deny, - reason=reason, - ) - - async def fetch_invites_for_channel( - self, channel: typing.Union[bases.Snowflake, int, str, channels_.PartialChannel] - ) -> typing.Sequence[invites.InviteWithMetadata]: - """Get invites for a given channel. - - Parameters - ---------- - channel : hikari.models.channels.PartialChannel | hikari.models.bases.Snowflake | int | str - The object or ID of the channel to get invites for. - - Returns - ------- - typing.Sequence[hikari.models.invites.InviteWithMetadata] - A list of invite objects. - - Raises - ------ - hikari.errors.BadRequest - If any invalid snowflake IDs are passed; a snowflake may be invalid - due to it being outside of the range of a 64 bit integer. - hikari.errors.Forbidden - If you lack the `MANAGE_CHANNELS` permission. - hikari.errors.NotFound - If the channel does not exist. - """ - payload = await self._session.get_channel_invites( - channel_id=str(channel.id if isinstance(channel, bases.Unique) else int(channel)) - ) - return [invites.InviteWithMetadata.deserialize(invite, app=self._app) for invite in payload] - - async def create_invite_for_channel( - self, - channel: typing.Union[bases.Snowflake, int, str, channels_.PartialChannel], - *, - max_age: typing.Union[int, datetime.timedelta] = ..., - max_uses: int = ..., - temporary: bool = ..., - unique: bool = ..., - target_user: typing.Union[bases.Snowflake, int, str, users.User] = ..., - target_user_type: typing.Union[invites.TargetUserType, int] = ..., - reason: str = ..., - ) -> invites.InviteWithMetadata: - """Create a new invite for the given channel. - - Parameters - ---------- - channel : hikari.models.channels.PartialChannel | hikari.models.bases.Snowflake | int | str - The object or ID of the channel to create the invite for. - max_age : datetime.timedelta | int - If specified, the seconds time delta for the max age of the invite, - defaults to `86400` seconds (`24` hours). - Set to `0` seconds to never expire. - max_uses : int - If specified, the max number of uses this invite can have, or `0` - for unlimited (as per the default). - temporary : bool - If specified, whether to grant temporary membership, meaning the - user is kicked when their session ends unless they are given a role. - unique : bool - If specified, whether to try to reuse a similar invite. - target_user : hikari.models.users.User | hikari.models.bases.Snowflake | int | str - If specified, the object or ID of the user this invite should - target. - target_user_type : hikari.invites.TargetUserType | int - If specified, the type of target for this invite, passing a raw - integer for this may lead to unexpected results. - reason : str - If specified, the audit log reason explaining why the operation - was performed. - - Returns - ------- - hikari.models.invites.InviteWithMetadata - The created invite object. - - Raises - ------ - hikari.errors.Forbidden - If you lack the `CREATE_INSTANT_MESSAGES` permission. - hikari.errors.NotFound - If the channel does not exist. - hikari.errors.BadRequest - If the arguments provided are not valid (e.g. negative age, etc). - If any invalid snowflake IDs are passed; a snowflake may be invalid - due to it being outside of the range of a 64 bit integer. - """ - payload = await self._session.create_channel_invite( - channel_id=str(channel.id if isinstance(channel, bases.Unique) else int(channel)), - max_age=int(max_age.total_seconds()) if isinstance(max_age, datetime.timedelta) else max_age, - max_uses=max_uses, - temporary=temporary, - unique=unique, - target_user=( - str(target_user.id if isinstance(target_user, bases.Unique) else int(target_user)) - if target_user is not ... - else ... - ), - target_user_type=target_user_type, - reason=reason, - ) - return invites.InviteWithMetadata.deserialize(payload, app=self._app) - - async def delete_channel_overwrite( # pylint: disable=line-too-long - self, - channel: typing.Union[bases.Snowflake, int, str, channels_.PartialChannel], - overwrite: typing.Union[channels_.PermissionOverwrite, guilds.Role, users.User, bases.Snowflake, int], - ) -> None: - """Delete a channel permission overwrite for a user or a role. - - Parameters - ---------- - channel : hikari.models.channels.PartialChannel | hikari.models.bases.Snowflake | int | str - The object or ID of the channel to delete the overwrite from. - overwrite : hikari.models.channels.PermissionOverwrite | hikari.models.guilds.Role | hikari.models.users.User| hikari.models.bases.Snowflake | int - The ID of the entity this overwrite targets. - - Raises - ------ - hikari.errors.BadRequest - If any invalid snowflake IDs are passed; a snowflake may be invalid - due to it being outside of the range of a 64 bit integer. - hikari.errors.NotFound - If the overwrite or channel do not exist. - hikari.errors.Forbidden - If you lack the `MANAGE_ROLES` permission for that channel. - """ - # pylint: enable=line-too-long - await self._session.delete_channel_permission( - channel_id=str(channel.id if isinstance(channel, bases.Unique) else int(channel)), - overwrite_id=str(overwrite.id if isinstance(overwrite, bases.Unique) else int(overwrite)), - ) - - async def trigger_typing(self, channel: typing.Union[bases.Snowflake, int, str, channels_.PartialChannel]) -> None: - """Trigger the typing indicator for `10` seconds in a channel. - - Parameters - ---------- - channel : hikari.models.channels.PartialChannel | hikari.models.bases.Snowflake | int | str - The object or ID of the channel to appear to be typing in. - - Raises - ------ - hikari.errors.BadRequest - If any invalid snowflake IDs are passed; a snowflake may be invalid - due to it being outside of the range of a 64 bit integer. - hikari.errors.NotFound - If the channel is not found. - hikari.errors.Forbidden - If you are not able to type in the channel. - """ - await self._session.trigger_typing_indicator( - channel_id=str(channel.id if isinstance(channel, bases.Unique) else int(channel)) - ) - - async def fetch_pins( - self, channel: typing.Union[bases.Snowflake, int, str, channels_.PartialChannel] - ) -> typing.Mapping[bases.Snowflake, messages_.Message]: - """Get pinned messages for a given channel. - - Parameters - ---------- - channel : hikari.models.channels.PartialChannel | hikari.models.bases.Snowflake | int | str - The object or ID of the channel to get messages from. - - Returns - ------- - typing.Mapping[hikari.models.bases.Snowflake, hikari.models.messages.Message] - A list of message objects. - - Raises - ------ - hikari.errors.BadRequest - If any invalid snowflake IDs are passed; a snowflake may be invalid - due to it being outside of the range of a 64 bit integer. - hikari.errors.NotFound - If the channel is not found. - hikari.errors.Forbidden - If you are not able to see the channel. - - !!! note - If you are not able to see the pinned message (eg. you are missing - `READ_MESSAGE_HISTORY` and the pinned message is an old message), it - will not be returned. - """ - payload = await self._session.get_pinned_messages( - channel_id=str(channel.id if isinstance(channel, bases.Unique) else int(channel)) - ) - return { - bases.Snowflake(message["id"]): messages_.Message.deserialize(message, app=self._app) for message in payload - } - - async def pin_message( - self, - channel: typing.Union[bases.Snowflake, int, str, channels_.PartialChannel], - message: typing.Union[bases.Snowflake, int, str, messages_.Message], - ) -> None: - """Add a pinned message to the channel. - - Parameters - ---------- - channel : hikari.models.channels.PartialChannel | hikari.models.bases.Snowflake | int | str - The object or ID of the channel to pin a message to. - message : hikari.models.channels.PartialChannel | hikari.models.bases.Snowflake | int | str - The object or ID of the message to pin. - - Raises - ------ - hikari.errors.BadRequest - If any invalid snowflake IDs are passed; a snowflake may be invalid - due to it being outside of the range of a 64 bit integer. - hikari.errors.Forbidden - If you lack the `MANAGE_MESSAGES` permission. - hikari.errors.NotFound - If the message or channel do not exist. - """ - await self._session.add_pinned_channel_message( - channel_id=str(channel.id if isinstance(channel, bases.Unique) else int(channel)), - message_id=str(message.id if isinstance(message, bases.Unique) else int(message)), - ) - - async def unpin_message( - self, - channel: typing.Union[bases.Snowflake, int, str, channels_.PartialChannel], - message: typing.Union[bases.Snowflake, int, str, messages_.Message], - ) -> None: - """Remove a pinned message from the channel. - - This will only unpin the message, not delete it. - - Parameters - ---------- - channel : hikari.models.channels.PartialChannel | hikari.models.bases.Snowflake | int | str - The ID of the channel to remove a pin from. - message : hikari.models.messages.Message | hikari.models.bases.Snowflake | int | str - The object or ID of the message to unpin. - - Raises - ------ - hikari.errors.BadRequest - If any invalid snowflake IDs are passed; a snowflake may be invalid - due to it being outside of the range of a 64 bit integer. - hikari.errors.Forbidden - If you lack the `MANAGE_MESSAGES` permission. - hikari.errors.NotFound - If the message or channel do not exist. - """ - await self._session.delete_pinned_channel_message( - channel_id=str(channel.id if isinstance(channel, bases.Unique) else int(channel)), - message_id=str(message.id if isinstance(message, bases.Unique) else int(message)), - ) - - async def create_webhook( - self, - channel: typing.Union[bases.Snowflake, int, str, channels_.GuildChannel], - name: str, - *, - avatar: files_.BaseStream = ..., - reason: str = ..., - ) -> webhooks.Webhook: - """Create a webhook for a given channel. - - Parameters - ---------- - channel : hikari.models.channels.PartialChannel | hikari.models.bases.Snowflake | int | str - The object or ID of the channel for webhook to be created in. - name : str - The webhook's name string. - avatar : hikari.models.files.BaseStream - If specified, the avatar image to use. - reason : str - If specified, the audit log reason explaining why the operation - was performed. - - Returns - ------- - hikari.models.webhooks.Webhook - The newly created webhook object. - - Raises - ------ - hikari.errors.NotFound - If the channel is not found. - hikari.errors.Forbidden - If you either lack the `MANAGE_WEBHOOKS` permission or - can not see the given channel. - hikari.errors.BadRequest - If the avatar image is too big or the format is invalid. - If any invalid snowflake IDs are passed; a snowflake may be invalid - due to it being outside of the range of a 64 bit integer. - """ - payload = await self._session.create_webhook( - channel_id=str(channel.id if isinstance(channel, bases.Unique) else int(channel)), - name=name, - avatar=await avatar.read() if avatar is not ... else ..., - reason=reason, - ) - return webhooks.Webhook.deserialize(payload, app=self._app) - - async def fetch_channel_webhooks( - self, channel: typing.Union[bases.Snowflake, int, str, channels_.GuildChannel] - ) -> typing.Sequence[webhooks.Webhook]: - """Get all webhooks from a given channel. - - Parameters - ---------- - channel : hikari.models.channels.GuildChannel | hikari.models.bases.Snowflake | int | str - The object or ID of the guild channel to get the webhooks from. - - Returns - ------- - typing.Sequence[hikari.models.webhooks.Webhook] - A list of webhook objects for the give channel. - - Raises - ------ - hikari.errors.BadRequest - If any invalid snowflake IDs are passed; a snowflake may be invalid - due to it being outside of the range of a 64 bit integer. - hikari.errors.NotFound - If the channel is not found. - hikari.errors.Forbidden - If you either lack the `MANAGE_WEBHOOKS` permission or - can not see the given channel. - """ - payload = await self._session.get_channel_webhooks( - channel_id=str(channel.id if isinstance(channel, bases.Unique) else int(channel)) - ) - return [webhooks.Webhook.deserialize(webhook, app=self._app) for webhook in payload] diff --git a/hikari/impl/entity_factory.py b/hikari/impl/entity_factory.py index 8a4c92dec9..46c8b044c5 100644 --- a/hikari/impl/entity_factory.py +++ b/hikari/impl/entity_factory.py @@ -19,8 +19,144 @@ from __future__ import annotations +import typing + from hikari import entity_factory +from hikari.internal import more_typing +from hikari.models import applications +from hikari.models import audit_logs +from hikari.models import channels +from hikari.models import embeds +from hikari.models import emojis +from hikari.models import gateway +from hikari.models import guilds +from hikari.models import invites +from hikari.models import users +from hikari.models import voices +from hikari.models import webhooks class EntityFactoryImpl(entity_factory.IEntityFactory): - pass + def deserialize_own_connection(self, payload: more_typing.JSONObject) -> applications.OwnConnection: + pass + + def deserialize_own_guild(self, payload: more_typing.JSONObject) -> applications.OwnGuild: + pass + + def deserialize_application(self, payload: more_typing.JSONObject) -> applications: + pass + + def deserialize_audit_log_entry(self, payload: more_typing.JSONObject) -> audit_logs.AuditLogEntry: + pass + + def deserialize_permission_overwrite(self, payload: more_typing.JSONObject) -> channels.PermissionOverwrite: + pass + + def serialize_permission_overwrite(self, overwrite: channels.PermissionOverwrite) -> more_typing.JSONObject: + pass + + def deserialize_partial_channel(self, payload: more_typing.JSONObject) -> channels.PartialChannel: + pass + + def deserialize_dm_channel(self, payload: more_typing.JSONObject) -> channels.DMChannel: + pass + + def deserialize_group_dm_channel(self, payload: more_typing.JSONObject) -> channels.GroupDMChannel: + pass + + def deserialize_guild_category(self, payload: more_typing.JSONObject) -> channels.GuildCategory: + pass + + def deserialize_guild_text_channel(self, payload: more_typing.JSONObject) -> channels.GuildTextChannel: + pass + + def deserialize_guild_news_channel(self, payload: more_typing.JSONObject) -> channels.GuildNewsChannel: + pass + + def deserialize_guild_store_channel(self, payload: more_typing.JSONObject) -> channels.GuildStoreChannel: + pass + + def deserialize_guild_voice_channel(self, payload: more_typing.JSONObject) -> channels.GuildVoiceChannel: + pass + + def deserialize_channel(self, payload: more_typing.JSONObject) -> channels.PartialChannel: + pass + + def deserialize_embed(self, payload: more_typing.JSONObject) -> embeds.Embed: + pass + + def serialize_embed(self, embed: embeds.Embed) -> more_typing.JSONObject: + pass + + def deserialize_unicode_emoji(self, payload: more_typing.JSONObject) -> emojis.UnicodeEmoji: + pass + + def deserialize_custom_emoji(self, payload: more_typing.JSONObject) -> emojis.CustomEmoji: + pass + + def deserialize_known_custom_emoji(self, payload: more_typing.JSONObject) -> emojis.KnownCustomEmoji: + pass + + def deserialize_emoji( + self, payload: more_typing.JSONObject + ) -> typing.Union[emojis.UnicodeEmoji, emojis.CustomEmoji]: + pass + + def deserialize_gateway_bot(self, payload: more_typing.JSONObject) -> gateway.GatewayBot: + pass + + def deserialize_guild_embed(self, payload: more_typing.JSONObject) -> guilds.GuildEmbed: + pass + + def deserialize_guild_member( + self, payload: more_typing.JSONObject, *, user: typing.Optional[users.User] = None + ) -> guilds.GuildMember: + pass + + def deserialize_guild_role(self, payload: more_typing.JSONObject) -> guilds.GuildRole: + pass + + def deserialize_guild_member_presence(self, payload: more_typing.JSONObject) -> guilds.GuildMemberPresence: + pass + + def deserialize_partial_guild_integration(self, payload: more_typing.JSONObject) -> guilds.PartialGuildIntegration: + pass + + def deserialize_guild_integration(self, payload: more_typing.JSONObject) -> guilds.GuildIntegration: + pass + + def deserialize_guild_member_ban(self, payload: more_typing.JSONObject) -> guilds.GuildMemberBan: + pass + + def deserialize_unavailable_guild(self, payload: more_typing.JSONObject) -> guilds.UnavailableGuild: + pass + + def deserialize_guild_preview(self, payload: more_typing.JSONObject) -> guilds.GuildPreview: + pass + + def deserialize_guild(self, payload: more_typing.JSONObject) -> guilds.Guild: + pass + + def deserialize_vanity_url(self, payload: more_typing.JSONObject) -> invites.VanityUrl: + pass + + def deserialize_invite(self, payload: more_typing.JSONObject) -> invites.Invite: + pass + + def deserialize_invite_with_metadata(self, payload: more_typing.JSONObject) -> invites.InviteWithMetadata: + pass + + def deserialize_user(self, payload: more_typing.JSONObject) -> users.User: + pass + + def deserialize_my_user(self, payload: more_typing.JSONObject) -> users.MyUser: + pass + + def deserialize_voice_state(self, payload: more_typing.JSONObject) -> voices.VoiceState: + pass + + def deserialize_voice_region(self, payload: more_typing.JSONObject) -> voices.VoiceRegion: + pass + + def deserialize_webhook(self, payload: more_typing.JSONObject) -> webhooks.Webhook: + pass diff --git a/hikari/net/rest.py b/hikari/net/rest.py index cfdb4cf0ad..6e3c952d87 100644 --- a/hikari/net/rest.py +++ b/hikari/net/rest.py @@ -26,6 +26,7 @@ import datetime import http import json +import types import typing import aiohttp @@ -118,6 +119,37 @@ async def _next_chunk(self): return (users.User.deserialize(u, app=self._app) for u in chunk) +class _TypingIndicator: + __slots__ = ("_channel", "_request_call", "_task") + + def __init__( + self, + channel: typing.Union[channels.TextChannel, bases.UniqueObjectT], + request_call=typing.Callable[..., more_typing.Coroutine[more_typing.JSONObject]], + ) -> None: + self._channel = conversions.cast_to_str_id(channel) + self._request_call = request_call + self._task = None + + def __await__(self) -> typing.Generator[None, typing.Any, None]: + route = routes.POST_CHANNEL_TYPING.compile(channel=self._channel) + yield from self._request_call(route).__await__() + + async def __aenter__(self): + if self._task is not None: + raise TypeError("cannot enter a typing indicator context more than once.") + self._task = asyncio.create_task(self._keep_typing(), name=f"repeatedly trigger typing in {self._channel}") + + async def __aexit__(self, ex_t: typing.Type[Exception], ex_v: Exception, exc_tb: types.TracebackType) -> None: + self._task.cancel() + # Prevent reusing this object by not setting it back to None. + self._task = NotImplemented + + async def _keep_typing(self) -> None: + with contextlib.suppress(asyncio.CancelledError): + await asyncio.gather(self, asyncio.sleep(9.9), return_exceptions=True) + + class REST(http_client.HTTPClient): def __init__( self, @@ -411,7 +443,7 @@ async def edit_channel( return self._app.entity_factory.deserialize_channel(response) - async def delete_channel(self, channel: typing.Union[channels.PartialChannel, bases.Snowflake, int]) -> None: + async def delete_channel(self, channel: typing.Union[channels.PartialChannel, bases.Snowflake, int], /) -> None: await self._request(routes.DELETE_CHANNEL.compile(channel=conversions.cast_to_str_id(channel))) @typing.overload @@ -481,7 +513,7 @@ async def delete_channel_permission( await self._request(route) async def fetch_channel_invites( - self, channel: typing.Union[channels.GuildChannel, bases.UniqueObjectT] + self, channel: typing.Union[channels.GuildChannel, bases.UniqueObjectT], / ) -> typing.Sequence[invites.InviteWithMetadata]: route = routes.GET_CHANNEL_INVITES.compile(channel=conversions.cast_to_str_id(channel)) response = await self._request(route) @@ -511,9 +543,20 @@ async def create_invite( response = await self._request(route, body=payload, reason=reason) return self._app.entity_factory.deserialize_invite_with_metadata(response) - async def trigger_typing(self, channel: typing.Union[channels.TextChannel, bases.UniqueObjectT], /) -> None: - route = routes.POST_CHANNEL_TYPING.compile(channel=conversions.cast_to_str_id(channel)) - await self._request(route) + @typing.overload + def trigger_typing( + self, channel: typing.Union[channels.TextChannel, bases.UniqueObjectT], / + ) -> more_typing.Coroutine[None]: + ... + + @typing.overload + def trigger_typing( + self, channel: typing.Union[channels.TextChannel, bases.UniqueObjectT], / + ) -> typing.AsyncContextManager[None]: + ... + + def trigger_typing(self, channel: typing.Union[channels.TextChannel, bases.UniqueObjectT]) -> _TypingIndicator: + return _TypingIndicator(channel, self._request) async def fetch_pins( self, channel: typing.Union[channels.TextChannel, bases.UniqueObjectT], / @@ -878,26 +921,3 @@ async def execute_embed( ) return self._app.entity_factory.deserialize_message(response) - - # Keep this last, then it doesn't cause problems with the imports. - def typing( - self, channel: typing.Union[channels.TextChannel, bases.UniqueObjectT], / - ) -> contextlib.AbstractAsyncContextManager: - async def keep_typing(): - with contextlib.suppress(asyncio.CancelledError): - while True: - # Use gather so that if the API call takes more than 10s, we don't spam the API - # as something is not working properly somewhere, but at the same time do not - # take into account the API call time before waiting the 10s, as this stops - # the indicator showing up consistently. - await asyncio.gather(self.trigger_typing(channel), asyncio.sleep(9.9)) - - @contextlib.asynccontextmanager - async def typing_context(): - task = asyncio.create_task(keep_typing(), name=f"typing in {channel} continuously") - try: - yield - finally: - task.cancel() - - return typing_context() From c8532720e6af9c63d8adc4c5b95eb3b23e0fb3f2 Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Sat, 23 May 2020 11:38:51 +0100 Subject: [PATCH 378/922] Added missing fetch_reactions_for_emoji endpoint and moved paginators to another module. --- hikari/net/rest.py | 114 +++++++-------------------------- hikari/net/rest_utils.py | 133 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 154 insertions(+), 93 deletions(-) create mode 100644 hikari/net/rest_utils.py diff --git a/hikari/net/rest.py b/hikari/net/rest.py index 6e3c952d87..8e186ab8c7 100644 --- a/hikari/net/rest.py +++ b/hikari/net/rest.py @@ -53,6 +53,7 @@ from hikari.models import webhooks from hikari.net import buckets from hikari.net import http_client +from hikari.net import rest_utils from hikari.net import routes @@ -60,96 +61,6 @@ class _RateLimited(RuntimeError): __slots__ = () -class _MessagePaginator(pagination.BufferedPaginatedResults[messages.Message]): - __slots__ = ("_app", "_request_call", "_direction", "_first_id", "_route") - - def __init__( - self, - app: base_app.IBaseApp, - request_call: typing.Callable[..., more_typing.Coroutine[more_typing.JSONObject]], - channel_id: str, - direction: str, - first_id: str, - ) -> None: - super().__init__() - self._app = app - self._request_call = request_call - self._direction = direction - self._first_id = first_id - self._route = routes.GET_CHANNEL_MESSAGES.compile(channel=channel_id) - - async def _next_chunk(self) -> typing.Optional[typing.Generator[messages.Message, typing.Any, None]]: - chunk = await self._request_call(self._route, query={self._direction: self._first_id, "limit": 100}) - - if not chunk: - return None - if self._direction == "after": - chunk.reverse() - - self._first_id = chunk[-1]["id"] - - return (self._app.entity_factory.deserialize_message(m) for m in chunk) - - -class _ReactionPaginator(pagination.BufferedPaginatedResults[messages.Reaction]): - __slots__ = ("_app", "_first_id", "_route", "_request_call") - - def __init__( - self, - app: base_app.IBaseApp, - request_call: typing.Callable[..., more_typing.Coroutine[more_typing.JSONObject]], - channel_id: str, - message_id: str, - emoji: str, - ) -> None: - super().__init__() - self._app = app - self._request_call = request_call - self._first_id = bases.Snowflake.min() - self._route = routes.GET_REACTIONS.compile(channel_id=channel_id, message_id=message_id, emoji=emoji) - - async def _next_chunk(self): - chunk = await self._request_call(self._route, query={"after": self._first_id, "limit": 100}) - - if not chunk: - return None - - self._first_id = chunk[-1]["id"] - - return (users.User.deserialize(u, app=self._app) for u in chunk) - - -class _TypingIndicator: - __slots__ = ("_channel", "_request_call", "_task") - - def __init__( - self, - channel: typing.Union[channels.TextChannel, bases.UniqueObjectT], - request_call=typing.Callable[..., more_typing.Coroutine[more_typing.JSONObject]], - ) -> None: - self._channel = conversions.cast_to_str_id(channel) - self._request_call = request_call - self._task = None - - def __await__(self) -> typing.Generator[None, typing.Any, None]: - route = routes.POST_CHANNEL_TYPING.compile(channel=self._channel) - yield from self._request_call(route).__await__() - - async def __aenter__(self): - if self._task is not None: - raise TypeError("cannot enter a typing indicator context more than once.") - self._task = asyncio.create_task(self._keep_typing(), name=f"repeatedly trigger typing in {self._channel}") - - async def __aexit__(self, ex_t: typing.Type[Exception], ex_v: Exception, exc_tb: types.TracebackType) -> None: - self._task.cancel() - # Prevent reusing this object by not setting it back to None. - self._task = NotImplemented - - async def _keep_typing(self) -> None: - with contextlib.suppress(asyncio.CancelledError): - await asyncio.gather(self, asyncio.sleep(9.9), return_exceptions=True) - - class REST(http_client.HTTPClient): def __init__( self, @@ -555,8 +466,11 @@ def trigger_typing( ) -> typing.AsyncContextManager[None]: ... - def trigger_typing(self, channel: typing.Union[channels.TextChannel, bases.UniqueObjectT]) -> _TypingIndicator: - return _TypingIndicator(channel, self._request) + def trigger_typing( + self, + channel: typing.Union[channels.TextChannel, bases.UniqueObjectT] + ) -> rest_utils.TypingIndicator: + return rest_utils.TypingIndicator(channel, self._request) async def fetch_pins( self, channel: typing.Union[channels.TextChannel, bases.UniqueObjectT], / @@ -637,7 +551,7 @@ def fetch_messages( if isinstance(timestamp, datetime.datetime): timestamp = bases.Snowflake.from_datetime(timestamp) - return _MessagePaginator( + return rest_utils.MessagePaginator( self._app, self._request, conversions.cast_to_str_id(channel), @@ -794,6 +708,20 @@ async def delete_all_reactions( route = routes.DELETE_ALL_REACTIONS.compile(channel=channel, message=message) await self._request(route) + def fetch_reactions_for_emoji( + self, + channel: typing.Union[channels.TextChannel, bases.UniqueObjectT], + message: typing.Union[messages.Message, bases.UniqueObjectT], + emoji: typing.Union[str, emojis.UnicodeEmoji, emojis.KnownCustomEmoji] + ) -> pagination.PaginatedResults[users.User]: + return rest_utils.ReactionPaginator( + app=self._app, + request_call=self._request, + channel_id=conversions.cast_to_str_id(channel), + message_id=conversions.cast_to_str_id(message), + emoji=emoji.url_name if isinstance(emoji, emojis.KnownCustomEmoji) else str(emoji), + ) + async def create_webhook( self, channel: typing.Union[channels.TextChannel, bases.UniqueObjectT], diff --git a/hikari/net/rest_utils.py b/hikari/net/rest_utils.py new file mode 100644 index 0000000000..68a4487aa5 --- /dev/null +++ b/hikari/net/rest_utils.py @@ -0,0 +1,133 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# Copyright © Nekoka.tt 2019-2020 +# +# This file is part of Hikari. +# +# Hikari is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Hikari is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Hikari. If not, see . +"""Internal utilities used by the REST API. + +You should never need to refer to this documentation directly. +""" +from __future__ import annotations + +# Do not document anything in here. +__all__ = [] + +import asyncio +import contextlib +import types +import typing + +from hikari import pagination +from hikari.internal import conversions +from hikari.models import bases +from hikari.net import routes + + +if typing.TYPE_CHECKING: + from hikari import base_app + from hikari.internal import more_typing + from hikari.models import channels + from hikari.models import messages + + +class MessagePaginator(pagination.BufferedPaginatedResults[messages.Message]): + __slots__ = ("_app", "_request_call", "_direction", "_first_id", "_route") + + def __init__( + self, + app: base_app.IBaseApp, + request_call: typing.Callable[..., more_typing.Coroutine[more_typing.JSONObject]], + channel_id: str, + direction: str, + first_id: str, + ) -> None: + super().__init__() + self._app = app + self._request_call = request_call + self._direction = direction + self._first_id = first_id + self._route = routes.GET_CHANNEL_MESSAGES.compile(channel=channel_id) + + async def _next_chunk(self) -> typing.Optional[typing.Generator[messages.Message, typing.Any, None]]: + chunk = await self._request_call(self._route, query={self._direction: self._first_id, "limit": 100}) + + if not chunk: + return None + if self._direction == "after": + chunk.reverse() + + self._first_id = chunk[-1]["id"] + + return (self._app.entity_factory.deserialize_message(m) for m in chunk) + + +class ReactionPaginator(pagination.BufferedPaginatedResults[messages.Reaction]): + __slots__ = ("_app", "_first_id", "_route", "_request_call") + + def __init__( + self, + app: base_app.IBaseApp, + request_call: typing.Callable[..., more_typing.Coroutine[more_typing.JSONObject]], + channel_id: str, + message_id: str, + emoji: str, + ) -> None: + super().__init__() + self._app = app + self._request_call = request_call + self._first_id = bases.Snowflake.min() + self._route = routes.GET_REACTIONS.compile(channel_id=channel_id, message_id=message_id, emoji=emoji) + + async def _next_chunk(self): + chunk = await self._request_call(self._route, query={"after": self._first_id, "limit": 100}) + + if not chunk: + return None + + self._first_id = chunk[-1]["id"] + + return (self._app.entity_factory.deserialize_user(u) for u in chunk) + + +class TypingIndicator: + __slots__ = ("_channel", "_request_call", "_task") + + def __init__( + self, + channel: typing.Union[channels.TextChannel, bases.UniqueObjectT], + request_call=typing.Callable[..., more_typing.Coroutine[more_typing.JSONObject]], + ) -> None: + self._channel = conversions.cast_to_str_id(channel) + self._request_call = request_call + self._task = None + + def __await__(self) -> typing.Generator[None, typing.Any, None]: + route = routes.POST_CHANNEL_TYPING.compile(channel=self._channel) + yield from self._request_call(route).__await__() + + async def __aenter__(self): + if self._task is not None: + raise TypeError("cannot enter a typing indicator context more than once.") + self._task = asyncio.create_task(self._keep_typing(), name=f"repeatedly trigger typing in {self._channel}") + + async def __aexit__(self, ex_t: typing.Type[Exception], ex_v: Exception, exc_tb: types.TracebackType) -> None: + self._task.cancel() + # Prevent reusing this object by not setting it back to None. + self._task = NotImplemented + + async def _keep_typing(self) -> None: + with contextlib.suppress(asyncio.CancelledError): + await asyncio.gather(self, asyncio.sleep(9.9), return_exceptions=True) From ddb30539f64df7d72b28b80084bae2050d13d01b Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Sat, 23 May 2020 11:42:32 +0100 Subject: [PATCH 379/922] Implemented gateway API endpoints. --- hikari/net/rest.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/hikari/net/rest.py b/hikari/net/rest.py index 8e186ab8c7..3ce011d1d1 100644 --- a/hikari/net/rest.py +++ b/hikari/net/rest.py @@ -22,11 +22,9 @@ __all__ = ["REST"] import asyncio -import contextlib import datetime import http import json -import types import typing import aiohttp @@ -44,6 +42,7 @@ from hikari.models import embeds as embeds_ from hikari.models import emojis from hikari.models import files +from hikari.models import gateway from hikari.models import guilds from hikari.models import invites from hikari.models import messages @@ -849,3 +848,12 @@ async def execute_embed( ) return self._app.entity_factory.deserialize_message(response) + + async def fetch_gateway_url(self) -> str: + # This doesn't need authorization. + response = await self._request(routes.GET_GATEWAY.compile(), no_auth=True) + return response["url"] + + async def fetch_recommended_gateway_settings(self) -> gateway.GatewayBot: + response = await self._request(routes.GET_GATEWAY_BOT.compile()) + return self._app.entity_factory.deserialize_gateway_bot(response) From f4519d260a4beb6997cfab74b0a515ffe43c27b3 Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Sat, 23 May 2020 11:46:25 +0100 Subject: [PATCH 380/922] Implemented invite endpoints. --- hikari/.rest/gateway.py | 61 ----------------------------- hikari/.rest/invite.py | 86 ----------------------------------------- hikari/net/rest.py | 15 +++++-- 3 files changed, 12 insertions(+), 150 deletions(-) delete mode 100644 hikari/.rest/gateway.py delete mode 100644 hikari/.rest/invite.py diff --git a/hikari/.rest/gateway.py b/hikari/.rest/gateway.py deleted file mode 100644 index 5f9834e399..0000000000 --- a/hikari/.rest/gateway.py +++ /dev/null @@ -1,61 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# Copyright © Nekoka.tt 2019-2020 -# -# This file is part of Hikari. -# -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . -"""The logic for handling requests to gateway endpoints.""" - -from __future__ import annotations - -__all__ = ["RESTGatewayComponent"] - -import abc - -from hikari.models import gateway -from hikari.net.rest import base - - -class RESTGatewayComponent(base.BaseRESTComponent, abc.ABC): # pylint: disable=abstract-method - """The RESTSession client component for handling requests to gateway endpoints.""" - - async def fetch_gateway_url(self) -> str: - """Get a generic url used for establishing a Discord gateway connection. - - Returns - ------- - str - A static URL to use to connect to the gateway with. - - !!! note - Users are expected to attempt to cache this result. - """ - # noinspection PyTypeChecker - return await self._session.get_gateway() - - async def fetch_gateway_bot(self) -> gateway.GatewayBot: - """Get bot specific gateway information. - - !!! note - Unlike `RESTGatewayComponent.fetch_gateway_url`, this requires a - valid token to work. - - Returns - ------- - hikari.models.gateway.GatewayBot - The bot specific gateway information object. - """ - payload = await self._session.get_gateway_bot() - return gateway.GatewayBot.deserialize(payload, app=self._app) diff --git a/hikari/.rest/invite.py b/hikari/.rest/invite.py deleted file mode 100644 index 7fa7c337f6..0000000000 --- a/hikari/.rest/invite.py +++ /dev/null @@ -1,86 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# Copyright © Nekoka.tt 2019-2020 -# -# This file is part of Hikari. -# -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . -"""The logic for handling requests to invite endpoints.""" - -from __future__ import annotations - -__all__ = ["RESTInviteComponent"] - -import abc -import typing - -from hikari.models import invites -from hikari.net.rest import base - - -class RESTInviteComponent(base.BaseRESTComponent, abc.ABC): # pylint: disable=abstract-method - """The RESTSession client component for handling requests to invite endpoints.""" - - async def fetch_invite( - self, invite: typing.Union[invites.Invite, str], *, with_counts: bool = ... - ) -> invites.Invite: - """Get the given invite. - - Parameters - ---------- - invite : hikari.models.invites.Invite | str - The object or code of the wanted invite. - with_counts : bool - If specified, whether to attempt to count the number of - times the invite has been used. - - Returns - ------- - hikari.models.invites.Invite - The requested invite object. - - Raises - ------ - hikari.errors.BadRequest - If any invalid snowflake IDs are passed; a snowflake may be invalid - due to it being outside of the range of a 64 bit integer. - hikari.errors.NotFound - If the invite is not found. - """ - payload = await self._session.get_invite(invite_code=getattr(invite, "code", invite), with_counts=with_counts) - return invites.Invite.deserialize(payload, app=self._app) - - async def delete_invite(self, invite: typing.Union[invites.Invite, str]) -> None: - """Delete a given invite. - - Parameters - ---------- - invite : hikari.models.invites.Invite | str - The object or ID for the invite to be deleted. - - Returns - ------- - None - Nothing, unlike what the API specifies. This is done to maintain - consistency with other calls of a similar nature in this API wrapper. - - Raises - ------ - hikari.errors.NotFound - If the invite is not found. - hikari.errors.Forbidden - If you lack either `MANAGE_CHANNELS` on the channel the invite - belongs to or `MANAGE_GUILD` for guild-global delete. - """ - await self._session.delete_invite(invite_code=getattr(invite, "code", invite)) diff --git a/hikari/net/rest.py b/hikari/net/rest.py index 3ce011d1d1..f83534e13f 100644 --- a/hikari/net/rest.py +++ b/hikari/net/rest.py @@ -466,8 +466,7 @@ def trigger_typing( ... def trigger_typing( - self, - channel: typing.Union[channels.TextChannel, bases.UniqueObjectT] + self, channel: typing.Union[channels.TextChannel, bases.UniqueObjectT] ) -> rest_utils.TypingIndicator: return rest_utils.TypingIndicator(channel, self._request) @@ -711,7 +710,7 @@ def fetch_reactions_for_emoji( self, channel: typing.Union[channels.TextChannel, bases.UniqueObjectT], message: typing.Union[messages.Message, bases.UniqueObjectT], - emoji: typing.Union[str, emojis.UnicodeEmoji, emojis.KnownCustomEmoji] + emoji: typing.Union[str, emojis.UnicodeEmoji, emojis.KnownCustomEmoji], ) -> pagination.PaginatedResults[users.User]: return rest_utils.ReactionPaginator( app=self._app, @@ -857,3 +856,13 @@ async def fetch_gateway_url(self) -> str: async def fetch_recommended_gateway_settings(self) -> gateway.GatewayBot: response = await self._request(routes.GET_GATEWAY_BOT.compile()) return self._app.entity_factory.deserialize_gateway_bot(response) + + async def fetch_invite(self, invite: typing.Union[invites.Invite, str]) -> invites.Invite: + route = routes.GET_INVITE.compile(invite_code=invite if isinstance(invite, str) else invite.code) + payload = {"with_counts": True} + response = await self._request(route, body=payload) + return self._app.entity_factory.deserialize_invite(response) + + async def delete_invite(self, invite: typing.Union[invites.Invite, str]) -> None: + route = routes.DELETE_INVITE.compile(invite_code=invite if isinstance(invite, str) else invite.code) + response = await self._request(route) From 68582eb621e42ec2333a83ac4e59e5d56b9d5ab5 Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Sat, 23 May 2020 12:33:57 +0100 Subject: [PATCH 381/922] Implemented callee-specific API endpoints. --- hikari/.rest/guild.py | 6 +- hikari/.rest/me.py | 204 -------------------------------------- hikari/.rest/react.py | 6 +- hikari/impl/pagination.py | 8 +- hikari/net/rest.py | 63 ++++++++++-- hikari/net/rest_utils.py | 52 +++++++--- hikari/pagination.py | 28 +++--- 7 files changed, 118 insertions(+), 249 deletions(-) delete mode 100644 hikari/.rest/me.py diff --git a/hikari/.rest/guild.py b/hikari/.rest/guild.py index 7b006cad3d..5a6175fd40 100644 --- a/hikari/.rest/guild.py +++ b/hikari/.rest/guild.py @@ -45,7 +45,7 @@ from hikari.models import users -class _MemberPaginator(pagination.BufferedPaginatedResults[guilds.GuildMember]): +class _MemberPaginator(pagination.BufferedLazyIterator[guilds.GuildMember]): __slots__ = ("_app", "_guild_id", "_first_id", "_session") def __init__(self, app, guild, created_after, session): @@ -862,7 +862,7 @@ async def fetch_member( def fetch_members( self, guild: typing.Union[bases.Snowflake, int, str, guilds.Guild], - ) -> pagination.PaginatedResults[guilds.GuildMember]: + ) -> pagination.LazyIterator[guilds.GuildMember]: """Get an async iterator of all the members in a given guild. Parameters @@ -878,7 +878,7 @@ def fetch_members( Returns ------- - hikari.models.pagination.PaginatedResults[[hikari.models.guilds.GuildMember] + hikari.models.pagination.LazyIterator[[hikari.models.guilds.GuildMember] An async iterator of member objects. Raises diff --git a/hikari/.rest/me.py b/hikari/.rest/me.py deleted file mode 100644 index f4fb9cba19..0000000000 --- a/hikari/.rest/me.py +++ /dev/null @@ -1,204 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# Copyright © Nekoka.tt 2019-2020 -# -# This file is part of Hikari. -# -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . -"""The logic for handling requests to `@me` endpoints.""" - -from __future__ import annotations - -__all__ = ["RESTCurrentUserComponent"] - -import abc -import datetime -import typing - -from hikari import pagination -from hikari.models import applications -from hikari.models import bases -from hikari.models import channels as channels_ -from hikari.models import guilds -from hikari.models import users -from hikari.net.rest import base - -if typing.TYPE_CHECKING: - from hikari.models import files - - -class _GuildPaginator(pagination.BufferedPaginatedResults[guilds.Guild]): - __slots__ = ("_app", "_session", "_newest_first", "_first_id") - - def __init__(self, app, newest_first, first_item, session): - super().__init__() - self._app = app - self._newest_first = newest_first - self._first_id = self._prepare_first_id( - first_item, bases.Snowflake.max() if newest_first else bases.Snowflake.min(), - ) - self._session = session - - async def _next_chunk(self): - kwargs = {"before" if self._newest_first else "after": self._first_id} - - chunk = await self._session.get_current_user_guilds(**kwargs) - - if not chunk: - return None - - self._first_id = chunk[-1]["id"] - - return (applications.OwnGuild.deserialize(g, app=self._app) for g in chunk) - - -class RESTCurrentUserComponent(base.BaseRESTComponent, abc.ABC): # pylint: disable=abstract-method - """The RESTSession client component for handling requests to `@me` endpoints.""" - - async def fetch_me(self) -> users.MyUser: - """Get the current user that of the token given to the client. - - Returns - ------- - hikari.models.users.MyUser - The current user object. - """ - payload = await self._session.get_current_user() - return users.MyUser.deserialize(payload, app=self._app) - - async def update_me(self, *, username: str = ..., avatar: typing.Optional[files.BaseStream] = ...) -> users.MyUser: - """Edit the current user. - - Parameters - ---------- - username : str - If specified, the new username string. - avatar : hikari.models.files.BaseStream | None - If specified, the new avatar image data. - If it is None, the avatar is removed. - - !!! warning - Verified bots will not be able to change their username on this - endpoint, and should contact Discord support instead to change - this value. - - Returns - ------- - hikari.models.users.MyUser - The updated user object. - - Raises - ------ - hikari.errors.BadRequest - If you pass username longer than the limit (`2-32`) or an invalid image. - """ - payload = await self._session.modify_current_user( - username=username, avatar=await avatar.read() if avatar is not ... else ..., - ) - return users.MyUser.deserialize(payload, app=self._app) - - async def fetch_my_connections(self) -> typing.Sequence[applications.OwnConnection]: - """ - Get the current user's connections. - - !!! note - This endpoint can be used with both `Bearer` and `Bot` tokens but - will usually return an empty list for bots (with there being some - exceptions to this, like user accounts that have been converted to - bots). - - Returns - ------- - typing.Sequence[hikari.models.applications.OwnConnection] - A list of connection objects. - """ - payload = await self._session.get_current_user_connections() - return [applications.OwnConnection.deserialize(connection, app=self._app) for connection in payload] - - def fetch_my_guilds( - self, - *, - newest_first: bool = False, - start_at: typing.Optional[typing.Union[datetime.datetime, bases.Unique, bases.Snowflake, int]] = None, - ) -> pagination.PaginatedResults[applications.OwnGuild]: - """Get an async iterable of the guilds the current user is in. - - Parameters - ---------- - newest_first : bool - If specified and `True`, the guilds are returned in the order of - newest to oldest. The default is to return oldest guilds first. - start_at : datetime.datetime | hikari.models.bases.Unique | hikari.models.bases.Snowflake | int | None - The optional first item to start at, if you want to limit your - results. This will be interpreted as the date of creation for a - guild. If unspecified, the newest or older possible snowflake is - used, for `newest_first` being `True` and `False` respectively. - - Returns - ------- - hikari.models.pagination.PaginatedResults[hikari.models.applications.OwnGuild] - An async iterable of partial guild objects. - - Raises - ------ - hikari.errors.NotFound - If the guild is not found. - """ - return _GuildPaginator(app=self._app, newest_first=newest_first, first_item=start_at, session=self._session) - - async def leave_guild(self, guild: typing.Union[bases.Snowflake, int, str, guilds.Guild]) -> None: - """Make the current user leave a given guild. - - Parameters - ---------- - guild : hikari.models.guilds.Guild | hikari.models.bases.Snowflake | int | str - The object or ID of the guild to leave. - - Raises - ------ - hikari.errors.NotFound - If the guild is not found. - hikari.errors.BadRequest - If any invalid snowflake IDs are passed; a snowflake may be invalid - due to it being outside of the range of a 64 bit integer. - """ - await self._session.leave_guild(guild_id=str(guild.id if isinstance(guild, bases.Unique) else int(guild))) - - async def create_dm_channel( - self, recipient: typing.Union[bases.Snowflake, int, str, users.User] - ) -> channels_.DMChannel: - """Create a new DM channel with a given user. - - Parameters - ---------- - recipient : hikari.models.users.User | hikari.models.bases.Snowflake | int - The object or ID of the user to create the new DM channel with. - - Returns - ------- - hikari.models.channels.DMChannel - The newly created DM channel object. - - Raises - ------ - hikari.errors.NotFound - If the recipient is not found. - hikari.errors.BadRequest - If any invalid snowflake IDs are passed; a snowflake may be invalid - due to it being outside of the range of a 64 bit integer. - """ - payload = await self._session.create_dm( - recipient_id=str(recipient.id if isinstance(recipient, bases.Unique) else int(recipient)) - ) - return channels_.DMChannel.deserialize(payload, app=self._app) diff --git a/hikari/.rest/react.py b/hikari/.rest/react.py index e6201345db..a346157dcb 100644 --- a/hikari/.rest/react.py +++ b/hikari/.rest/react.py @@ -37,7 +37,7 @@ from hikari.models import emojis -class _ReactionPaginator(pagination.BufferedPaginatedResults[messages_.Reaction]): +class _ReactionPaginator(pagination.BufferedLazyIterator[messages_.Reaction]): __slots__ = ("_app", "_channel_id", "_message_id", "_first_id", "_emoji", "_session") def __init__(self, app, channel, message, emoji, users_after, session) -> None: @@ -212,7 +212,7 @@ def fetch_reactors( message: typing.Union[bases.Snowflake, int, str, messages_.Message], emoji: typing.Union[emojis.Emoji, str], after: typing.Optional[typing.Union[datetime.datetime, bases.Unique, bases.Snowflake, int, str]] = None, - ) -> pagination.PaginatedResults[users.User]: + ) -> pagination.LazyIterator[users.User]: """Get an async iterator of the users who reacted to a message. This returns the users created after a given user object/ID or from the @@ -245,7 +245,7 @@ def fetch_reactors( Returns ------- - hikari.models.pagination.PaginatedResults[hikari.models.users.User] + hikari.models.pagination.LazyIterator[hikari.models.users.User] An async iterator of user objects. Raises diff --git a/hikari/impl/pagination.py b/hikari/impl/pagination.py index 4cdf372697..15166f6b9e 100644 --- a/hikari/impl/pagination.py +++ b/hikari/impl/pagination.py @@ -30,7 +30,7 @@ from hikari.models import users -class GuildPaginator(pagination.BufferedPaginatedResults[guilds.Guild]): +class GuildPaginator(pagination.BufferedLazyIterator[guilds.Guild]): __slots__ = ("_app", "_newest_first", "_first_id", "_request_partial") def __init__(self, app, newest_first, first_item, request_partial): @@ -55,7 +55,7 @@ async def _next_chunk(self): return (applications.OwnGuild.deserialize(g, app=self._app) for g in chunk) -class MemberPaginator(pagination.BufferedPaginatedResults[guilds.GuildMember]): +class MemberPaginator(pagination.BufferedLazyIterator[guilds.GuildMember]): __slots__ = ("_app", "_guild_id", "_first_id", "_request_partial") def __init__(self, app, guild, created_after, request_partial): @@ -76,7 +76,7 @@ async def _next_chunk(self): return (guilds.GuildMember.deserialize(m, app=self._app) for m in chunk) -class MessagePaginator(pagination.BufferedPaginatedResults[messages.Message]): +class MessagePaginator(pagination.BufferedLazyIterator[messages.Message]): __slots__ = ("_app", "_channel_id", "_direction", "_first_id", "_request_partial") def __init__(self, app, channel, direction, first, request_partial) -> None: @@ -108,7 +108,7 @@ async def _next_chunk(self): return (messages.Message.deserialize(m, app=self._app) for m in chunk) -class ReactionPaginator(pagination.BufferedPaginatedResults[messages.Reaction]): +class ReactionPaginator(pagination.BufferedLazyIterator[messages.Reaction]): __slots__ = ("_app", "_channel_id", "_message_id", "_first_id", "_emoji", "_request_partial") def __init__(self, app, channel, message, emoji, users_after, request_partial) -> None: diff --git a/hikari/net/rest.py b/hikari/net/rest.py index f83534e13f..faceb03af2 100644 --- a/hikari/net/rest.py +++ b/hikari/net/rest.py @@ -37,6 +37,7 @@ from hikari.internal import more_collections from hikari.internal import more_typing from hikari.internal import ratelimits +from hikari.models import applications from hikari.models import bases from hikari.models import channels from hikari.models import embeds as embeds_ @@ -500,7 +501,7 @@ async def delete_pinned_message( @typing.overload def fetch_messages( self, channel: typing.Union[channels.TextChannel, bases.UniqueObjectT], / - ) -> pagination.PaginatedResults[messages.Message]: + ) -> pagination.LazyIterator[messages.Message]: ... @typing.overload @@ -510,7 +511,7 @@ def fetch_messages( /, *, before: typing.Union[datetime.datetime, bases.UniqueObjectT], - ) -> pagination.PaginatedResults[messages.Message]: + ) -> pagination.LazyIterator[messages.Message]: ... @typing.overload @@ -520,7 +521,7 @@ def fetch_messages( /, *, around: typing.Union[datetime.datetime, bases.UniqueObjectT], - ) -> pagination.PaginatedResults[messages.Message]: + ) -> pagination.LazyIterator[messages.Message]: ... @typing.overload @@ -530,7 +531,7 @@ def fetch_messages( /, *, after: typing.Union[datetime.datetime, bases.UniqueObjectT], - ) -> pagination.PaginatedResults[messages.Message]: + ) -> pagination.LazyIterator[messages.Message]: ... def fetch_messages( @@ -538,7 +539,7 @@ def fetch_messages( channel: typing.Union[channels.TextChannel, bases.UniqueObjectT], /, **kwargs: typing.Optional[typing.Union[datetime.datetime, bases.UniqueObjectT]], - ) -> pagination.PaginatedResults[messages.Message]: + ) -> pagination.LazyIterator[messages.Message]: if len(kwargs) == 1 and any(direction in kwargs for direction in ("before", "after", "around")): direction, timestamp = kwargs.popitem() elif not kwargs: @@ -711,8 +712,8 @@ def fetch_reactions_for_emoji( channel: typing.Union[channels.TextChannel, bases.UniqueObjectT], message: typing.Union[messages.Message, bases.UniqueObjectT], emoji: typing.Union[str, emojis.UnicodeEmoji, emojis.KnownCustomEmoji], - ) -> pagination.PaginatedResults[users.User]: - return rest_utils.ReactionPaginator( + ) -> pagination.LazyIterator[users.User]: + return rest_utils.ReactorPaginator( app=self._app, request_call=self._request, channel_id=conversions.cast_to_str_id(channel), @@ -865,4 +866,50 @@ async def fetch_invite(self, invite: typing.Union[invites.Invite, str]) -> invit async def delete_invite(self, invite: typing.Union[invites.Invite, str]) -> None: route = routes.DELETE_INVITE.compile(invite_code=invite if isinstance(invite, str) else invite.code) - response = await self._request(route) + await self._request(route) + + async def fetch_my_user(self) -> users.MyUser: + response = await self._request(routes.GET_MY_USER.compile()) + return self._app.entity_factory.deserialize_my_user(response) + + async def edit_my_user( + self, + *, + username: typing.Union[unset.Unset, str] = unset.UNSET, + avatar: typing.Union[unset.Unset, files.BaseStream] = unset.UNSET, + ) -> users.MyUser: + payload = {} + conversions.put_if_specified(payload, "username", username) + if not unset.is_unset(username): + payload["avatar"] = await avatar.read() + route = routes.PATCH_MY_USER.compile() + response = await self._request(route, body=payload) + return self._app.entity_factory.deserialize_my_user(response) + + async def fetch_my_connections(self) -> typing.Sequence[applications.OwnConnection]: + response = await self._request(routes.GET_MY_CONNECTIONS.compile()) + return [self._app.entity_factory.deserialize_own_connection(c) for c in response] + + def fetch_my_guilds( + self, + *, + newest_first: bool = False, + start_at: typing.Union[unset.Unset, guilds.PartialGuild, bases.UniqueObjectT, datetime.datetime] = unset.UNSET, + ) -> pagination.LazyIterator[applications.OwnGuild]: + if unset.is_unset(start_at): + start_at = bases.Snowflake.max() if newest_first else bases.Snowflake.min() + elif isinstance(start_at, datetime.datetime): + start_at = bases.Snowflake.from_datetime(start_at) + + return rest_utils.OwnGuildPaginator( + self._app, self._request, newest_first, conversions.cast_to_str_id(start_at) + ) + + async def leave_guild(self, guild: typing.Union[guilds.Guild, bases.UniqueObjectT]) -> None: + route = routes.DELETE_MY_GUILD.compile(guild=conversions.cast_to_str_id(guild)) + await self._request(route) + + async def create_dm_channel(self, user: typing.Union[users.User, bases.UniqueObjectT]) -> channels.DMChannel: + route = routes.POST_MY_CHANNELS.compile() + response = await self._request(route, body={"recipient_id": conversions.cast_to_str_id(user)}) + return self._app.entity_factory.deserialize_dm_channel(response) diff --git a/hikari/net/rest_utils.py b/hikari/net/rest_utils.py index 68a4487aa5..975eac4962 100644 --- a/hikari/net/rest_utils.py +++ b/hikari/net/rest_utils.py @@ -32,24 +32,25 @@ from hikari import pagination from hikari.internal import conversions +from hikari.models import applications from hikari.models import bases +from hikari.models import channels +from hikari.models import messages +from hikari.models import users from hikari.net import routes - if typing.TYPE_CHECKING: from hikari import base_app from hikari.internal import more_typing - from hikari.models import channels - from hikari.models import messages -class MessagePaginator(pagination.BufferedPaginatedResults[messages.Message]): +class MessagePaginator(pagination.BufferedLazyIterator[messages.Message]): __slots__ = ("_app", "_request_call", "_direction", "_first_id", "_route") def __init__( self, app: base_app.IBaseApp, - request_call: typing.Callable[..., more_typing.Coroutine[more_typing.JSONObject]], + request_call: typing.Callable[..., more_typing.Coroutine[more_typing.JSONArray]], channel_id: str, direction: str, first_id: str, @@ -70,17 +71,16 @@ async def _next_chunk(self) -> typing.Optional[typing.Generator[messages.Message chunk.reverse() self._first_id = chunk[-1]["id"] - return (self._app.entity_factory.deserialize_message(m) for m in chunk) -class ReactionPaginator(pagination.BufferedPaginatedResults[messages.Reaction]): +class ReactorPaginator(pagination.BufferedLazyIterator[users.User]): __slots__ = ("_app", "_first_id", "_route", "_request_call") def __init__( self, app: base_app.IBaseApp, - request_call: typing.Callable[..., more_typing.Coroutine[more_typing.JSONObject]], + request_call: typing.Callable[..., more_typing.Coroutine[more_typing.JSONArray]], channel_id: str, message_id: str, emoji: str, @@ -91,24 +91,52 @@ def __init__( self._first_id = bases.Snowflake.min() self._route = routes.GET_REACTIONS.compile(channel_id=channel_id, message_id=message_id, emoji=emoji) - async def _next_chunk(self): + async def _next_chunk(self) -> typing.Optional[typing.Generator[users.User, typing.Any, None]]: chunk = await self._request_call(self._route, query={"after": self._first_id, "limit": 100}) if not chunk: return None self._first_id = chunk[-1]["id"] - return (self._app.entity_factory.deserialize_user(u) for u in chunk) +class OwnGuildPaginator(pagination.BufferedLazyIterator[applications.OwnGuild]): + __slots__ = ("_app", "_request_call", "_route", "_newest_first", "_first_id") + + def __init__( + self, + app: base_app.IBaseApp, + request_call: typing.Callable[..., more_typing.Coroutine[more_typing.JSONArray]], + newest_first: bool, + first_id: str, + ) -> None: + super().__init__() + self._app = app + self._newest_first = newest_first + self._request_call = request_call + self._first_id = first_id + self._route = routes.GET_MY_GUILDS.compile() + + async def _next_chunk(self) -> typing.Optional[typing.Generator[applications.OwnGuild, typing.Any, None]]: + kwargs = {"before" if self._newest_first else "after": self._first_id, "limit": 100} + + chunk = await self._request_call(self._route, query=kwargs) + + if not chunk: + return None + + self._first_id = chunk[-1]["id"] + return (self._app.entity_factory.deserialize_own_guild(g) for g in chunk) + + class TypingIndicator: __slots__ = ("_channel", "_request_call", "_task") def __init__( self, channel: typing.Union[channels.TextChannel, bases.UniqueObjectT], - request_call=typing.Callable[..., more_typing.Coroutine[more_typing.JSONObject]], + request_call: typing.Callable[..., more_typing.Coroutine[more_typing.JSONObject]], ) -> None: self._channel = conversions.cast_to_str_id(channel) self._request_call = request_call @@ -118,7 +146,7 @@ def __await__(self) -> typing.Generator[None, typing.Any, None]: route = routes.POST_CHANNEL_TYPING.compile(channel=self._channel) yield from self._request_call(route).__await__() - async def __aenter__(self): + async def __aenter__(self) -> None: if self._task is not None: raise TypeError("cannot enter a typing indicator context more than once.") self._task = asyncio.create_task(self._keep_typing(), name=f"repeatedly trigger typing in {self._channel}") diff --git a/hikari/pagination.py b/hikari/pagination.py index 60d276f5ad..be7c2060dc 100644 --- a/hikari/pagination.py +++ b/hikari/pagination.py @@ -20,16 +20,14 @@ from __future__ import annotations import abc -import datetime import typing from hikari.internal import more_collections -from hikari.models import bases _T = typing.TypeVar("_T") -class PaginatedResults(typing.Generic[_T], abc.ABC): +class LazyIterator(typing.Generic[_T], abc.ABC): """A set of results that are fetched asynchronously from the API as needed. This is a `typing.AsyncIterable` and `typing.AsyncIterator` with several @@ -79,7 +77,7 @@ class PaginatedResults(typing.Generic[_T], abc.ABC): __slots__ = () - def enumerate(self, *, start: int = 0) -> PaginatedResults[typing.Tuple[int, _T]]: + def enumerate(self, *, start: int = 0) -> LazyIterator[typing.Tuple[int, _T]]: """Enumerate the paginated results lazily. This behaves as an asyncio-friendly version of `builtins.enumerate` @@ -117,13 +115,13 @@ def enumerate(self, *, start: int = 0) -> PaginatedResults[typing.Tuple[int, _T] Returns ------- - PaginatedResults[typing.Tuple[int, T]] + LazyIterator[typing.Tuple[int, T]] A paginated results view that asynchronously yields an increasing counter in a tuple with each result, lazily. """ - return _EnumeratedPaginatedResults(self, start=start) + return _EnumeratedLazyIterator(self, start=start) - def limit(self, limit: int) -> PaginatedResults[_T]: + def limit(self, limit: int) -> LazyIterator[_T]: """Limit the number of items you receive from this async iterator. Parameters @@ -139,16 +137,16 @@ def limit(self, limit: int) -> PaginatedResults[_T]: Returns ------- - PaginatedResults[T] + LazyIterator[T] A paginated results view that asynchronously yields a maximum of the given number of items before completing. """ - return _LimitedPaginatedResults(self, limit) + return _LimitedLazyIterator(self, limit) def _complete(self) -> typing.NoReturn: raise StopAsyncIteration("No more items exist in this paginator. It has been exhausted.") from None - def __aiter__(self) -> PaginatedResults[_T]: + def __aiter__(self) -> LazyIterator[_T]: # We are our own iterator. return self @@ -166,10 +164,10 @@ async def __anext__(self) -> _T: _EnumeratedT = typing.Tuple[int, _T] -class _EnumeratedPaginatedResults(typing.Generic[_T], PaginatedResults[_EnumeratedT]): +class _EnumeratedLazyIterator(typing.Generic[_T], LazyIterator[_EnumeratedT]): __slots__ = ("_i", "_paginator") - def __init__(self, paginator: PaginatedResults[_T], *, start: int) -> None: + def __init__(self, paginator: LazyIterator[_T], *, start: int) -> None: self._i = start self._paginator = paginator @@ -179,10 +177,10 @@ async def __anext__(self) -> typing.Tuple[int, _T]: return pair -class _LimitedPaginatedResults(typing.Generic[_T], PaginatedResults[_T]): +class _LimitedLazyIterator(typing.Generic[_T], LazyIterator[_T]): __slots__ = ("_paginator", "_count", "_limit") - def __init__(self, paginator: PaginatedResults[_T], limit: int) -> None: + def __init__(self, paginator: LazyIterator[_T], limit: int) -> None: if limit <= 0: raise ValueError("limit must be positive and non-zero") self._paginator = paginator @@ -198,7 +196,7 @@ async def __anext__(self) -> _T: return next_item -class BufferedPaginatedResults(typing.Generic[_T], PaginatedResults[_T]): +class BufferedLazyIterator(typing.Generic[_T], LazyIterator[_T]): """A buffered paginator implementation that handles chunked responses.""" __slots__ = ("_buffer",) From e0ccda4d62b37c176b4681897127845d285df70f Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Sat, 23 May 2020 14:05:51 +0100 Subject: [PATCH 382/922] Implemented OAuth2 endpoints, implemented add_user_to_guild finally (thanks @ DoesNotCompute) --- hikari/.rest/oauth2.py | 48 ------------------------------------------ hikari/net/rest.py | 37 ++++++++++++++++++++++++++++++++ hikari/net/routes.py | 1 + 3 files changed, 38 insertions(+), 48 deletions(-) delete mode 100644 hikari/.rest/oauth2.py diff --git a/hikari/.rest/oauth2.py b/hikari/.rest/oauth2.py deleted file mode 100644 index c5d1d21e83..0000000000 --- a/hikari/.rest/oauth2.py +++ /dev/null @@ -1,48 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# Copyright © Nekoka.tt 2019-2020 -# -# This file is part of Hikari. -# -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . -"""The logic for handling all requests to oauth2 endpoints.""" - -from __future__ import annotations - -__all__ = ["RESTOAuth2Component"] - -import abc - -from hikari.models import applications -from hikari.net.rest import base - - -class RESTOAuth2Component(base.BaseRESTComponent, abc.ABC): # pylint: disable=abstract-method - """The RESTSession client component for handling requests to oauth2 endpoints.""" - - async def fetch_my_application_info(self) -> applications.Application: - """Get the current application information. - - Returns - ------- - hikari.models.applications.Application - An application info object. - """ - payload = await self._session.get_current_application_info() - return applications.Application.deserialize(payload, app=self._app) - - async def add_guild_member(self, *_, **__): - # TODO: implement and document this. - # https://discord.com/developers/docs/resources/guild#add-guild-member - raise NotImplementedError() diff --git a/hikari/net/rest.py b/hikari/net/rest.py index faceb03af2..1652e52ee0 100644 --- a/hikari/net/rest.py +++ b/hikari/net/rest.py @@ -913,3 +913,40 @@ async def create_dm_channel(self, user: typing.Union[users.User, bases.UniqueObj route = routes.POST_MY_CHANNELS.compile() response = await self._request(route, body={"recipient_id": conversions.cast_to_str_id(user)}) return self._app.entity_factory.deserialize_dm_channel(response) + + async def fetch_application(self) -> applications.Application: + response = await self._request(routes.GET_MY_APPLICATION.compile()) + return self._app.entity_factory.deserialize_application(response) + + async def add_user_to_guild( + self, + access_token: str, + guild: typing.Union[guilds.Guild, bases.UniqueObjectT], + user: typing.Union[users.User, bases.UniqueObjectT], + *, + nickname: typing.Union[unset.Unset, str] = unset.UNSET, + roles: typing.Union[ + unset.Unset, + typing.Collection[typing.Union[guilds.Role, bases.UniqueObjectT]] + ] = unset.UNSET, + mute: typing.Union[unset.Unset, bool] = unset.UNSET, + deaf: typing.Union[unset.Unset, bool] = unset.UNSET, + ) -> typing.Optional[guilds.GuildMember]: + route = routes.PUT_GUILD_MEMBER.compile( + guild=conversions.cast_to_str_id(guild), + user=conversions.cast_to_str_id(user), + ) + + payload = {"access_token": access_token} + conversions.put_if_specified(payload, "nick", nickname) + conversions.put_if_specified(payload, "roles", roles, lambda rs: [conversions.cast_to_str_id(r) for r in rs]) + conversions.put_if_specified(payload, "mute", mute) + conversions.put_if_specified(payload, "deaf", deaf) + + response = await self._request(route, body=payload) + + if response is None: + # User already is in the guild. + return None + else: + return self._app.entity_factory.deserialize_guild_member(payload) diff --git a/hikari/net/routes.py b/hikari/net/routes.py index bd1fa2f607..855fb9dd47 100644 --- a/hikari/net/routes.py +++ b/hikari/net/routes.py @@ -296,6 +296,7 @@ def __eq__(self, other) -> bool: GET_GUILD_MEMBER = Route(GET, "/guilds/{guild}/members/{user}") PATCH_GUILD_MEMBER = Route(PATCH, "/guilds/{guild}/members/{user}") +PUT_GUILD_MEMBER = Route(PUT, "/guilds/{guild}/members/{user}") DELETE_GUILD_MEMBER = Route(DELETE, "/guilds/{guild}/members/{user}") PUT_GUILD_MEMBER_ROLE = Route(PUT, "/guilds/{guild}/members/{user}/roles/{role}") From e6bde425374bee4281b0b28dcef2678a5afc2389 Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Sat, 23 May 2020 14:10:15 +0100 Subject: [PATCH 383/922] Implemented fetch_voice_regions --- hikari/.rest/react.py | 263 -------------------------------- hikari/.rest/voice.py | 47 ------ hikari/.rest/webhook.py | 321 ---------------------------------------- hikari/net/rest.py | 12 +- 4 files changed, 8 insertions(+), 635 deletions(-) delete mode 100644 hikari/.rest/react.py delete mode 100644 hikari/.rest/voice.py delete mode 100644 hikari/.rest/webhook.py diff --git a/hikari/.rest/react.py b/hikari/.rest/react.py deleted file mode 100644 index a346157dcb..0000000000 --- a/hikari/.rest/react.py +++ /dev/null @@ -1,263 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# Copyright © Nekoka.tt 2019-2020 -# -# This file is part of Hikari. -# -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . -"""Logic for handling all requests to reaction endpoints.""" - -from __future__ import annotations - -__all__ = ["RESTReactionComponent"] - -import abc -import datetime -import typing - -from hikari import pagination -from hikari.models import bases -from hikari.models import messages as messages_ -from hikari.models import users -from hikari.net.rest import base - -if typing.TYPE_CHECKING: - from hikari.models import channels as channels_ - from hikari.models import emojis - - -class _ReactionPaginator(pagination.BufferedLazyIterator[messages_.Reaction]): - __slots__ = ("_app", "_channel_id", "_message_id", "_first_id", "_emoji", "_session") - - def __init__(self, app, channel, message, emoji, users_after, session) -> None: - super().__init__() - self._app = app - self._channel_id = str(int(channel)) - self._message_id = str(int(message)) - self._emoji = getattr(emoji, "url_name", emoji) - self._first_id = self._prepare_first_id(users_after) - self._session = session - - async def _next_chunk(self): - chunk = await self._session.get_reactions( - channel_id=self._channel_id, message_id=self._message_id, emoji=self._emoji, after=self._first_id - ) - - if not chunk: - return None - - self._first_id = chunk[-1]["id"] - - return (users.User.deserialize(u, app=self._app) for u in chunk) - - -class RESTReactionComponent(base.BaseRESTComponent, abc.ABC): # pylint: disable=abstract-method - """The RESTSession client component for handling requests to reaction endpoints.""" - - async def add_reaction( - self, - channel: typing.Union[bases.Snowflake, int, str, channels_.PartialChannel], - message: typing.Union[bases.Snowflake, int, str, messages_.Message], - emoji: typing.Union[emojis.Emoji, str], - ) -> None: - """Add a reaction to the given message in the given channel. - - Parameters - ---------- - channel : hikari.models.channels.PartialChannel | hikari.models.bases.Snowflake | int | str - The object or ID of the channel to add this reaction in. - message : hikari.models.messages.Message | hikari.models.bases.Snowflake | int | str - The object or ID of the message to add the reaction in. - emoji : hikari.models.emojis.Emoji | str - The emoji to add. This can either be an emoji object or a string - representation of an emoji. The string representation will be either - `"name:id"` for custom emojis, or it's unicode character(s) for - standard emojis. - - Raises - ------ - hikari.errors.Forbidden - If this is the first reaction using this specific emoji on this - message and you lack the `ADD_REACTIONS` permission. If you lack - `READ_MESSAGE_HISTORY`, this may also raise this error. - hikari.errors.NotFound - If the channel or message is not found, or if the emoji is not found. - hikari.errors.BadRequest - If the emoji is invalid, unknown, or formatted incorrectly. - If any invalid snowflake IDs are passed; a snowflake may be invalid - due to it being outside of the range of a 64 bit integer. - """ - await self._session.create_reaction( - channel_id=str(channel.id if isinstance(channel, bases.Unique) else int(channel)), - message_id=str(message.id if isinstance(message, bases.Unique) else int(message)), - emoji=str(getattr(emoji, "url_name", emoji)), - ) - - async def remove_reaction( - self, - channel: typing.Union[bases.Snowflake, int, str, channels_.PartialChannel], - message: typing.Union[bases.Snowflake, int, str, messages_.Message], - emoji: typing.Union[emojis.Emoji, str], - *, - user: typing.Optional[typing.Hashable[users.User]] = None, - ) -> None: - """Remove a given reaction from a given message in a given channel. - - If the user is `None`, then the bot's own reaction is removed, otherwise - the given user's reaction is removed instead. - - Parameters - ---------- - channel : hikari.models.channels.PartialChannel | hikari.models.bases.Snowflake | int | str - The object or ID of the channel to add this reaction in. - message : hikari.models.messages.Message | hikari.models.bases.Snowflake | int | str - The object or ID of the message to add the reaction in. - emoji : hikari.models.emojis.Emoji | str - The emoji to add. This can either be an emoji object or a - string representation of an emoji. The string representation will be - either `"name:id"` for custom emojis else it's unicode - character(s) (can be UTF-32). - user : hikari.models.users.User | hikari.models.bases.Snowflake | int | str | None - The user to remove the reaction of. If this is `None`, then the - bot's own reaction is removed instead. - - Raises - ------ - hikari.errors.Forbidden - If this is the first reaction using this specific emoji on this - message and you lack the `ADD_REACTIONS` permission. If you lack - `READ_MESSAGE_HISTORY`, this may also raise this error. - If the `user` is not `None` and the bot lacks permissions to - modify other user's reactions, this will also be raised. - hikari.errors.NotFound - If the channel or message is not found, or if the emoji is not - found. - hikari.errors.BadRequest - If the emoji is not valid, unknown, or formatted incorrectly. - If any invalid snowflake IDs are passed; a snowflake may be invalid - due to it being outside of the range of a 64 bit integer. - """ - if user is None: - await self._session.delete_own_reaction( - channel_id=str(channel.id if isinstance(channel, bases.Unique) else int(channel)), - message_id=str(message.id if isinstance(message, bases.Unique) else int(message)), - emoji=str(getattr(emoji, "url_name", emoji)), - ) - else: - await self._session.delete_user_reaction( - channel_id=str(channel.id if isinstance(channel, bases.Unique) else int(channel)), - message_id=str(message.id if isinstance(message, bases.Unique) else int(message)), - emoji=str(getattr(emoji, "url_name", emoji)), - user_id=str(user.id if isinstance(user, bases.Unique) else int(user)), - ) - - async def remove_all_reactions( - self, - channel: typing.Union[bases.Snowflake, int, str, channels_.PartialChannel], - message: typing.Union[bases.Snowflake, int, str, messages_.Message], - *, - emoji: typing.Optional[typing.Union[emojis.Emoji, str]] = None, - ) -> None: - """Remove all reactions for a single given emoji on a given message. - - Parameters - ---------- - channel : hikari.models.channels.PartialChannel | hikari.models.bases.Snowflake | int | str - The object or ID of the channel to get the message from. - message : hikari.models.messages.Message | hikari.models.bases.Snowflake | int | str - The object or ID of the message to delete the reactions from. - emoji : hikari.models.emojis.Emoji | str | None - The object or string representation of the emoji to delete. The - string representation will be either `"name:id"` for custom emojis - else it's unicode character(s) (can be UTF-32). - If `None` or unspecified, then all reactions are removed. - - Raises - ------ - hikari.errors.BadRequest - If any invalid snowflake IDs are passed; a snowflake may be invalid - due to it being outside of the range of a 64 bit integer. - hikari.errors.NotFound - If the channel or message or emoji or user is not found. - hikari.errors.Forbidden - If you lack the `MANAGE_MESSAGES` permission, or the channel is a - DM channel. - """ - if emoji is None: - await self._session.delete_all_reactions( - channel_id=str(channel.id if isinstance(channel, bases.Unique) else int(channel)), - message_id=str(message.id if isinstance(message, bases.Unique) else int(message)), - ) - else: - await self._session.delete_all_reactions_for_emoji( - channel_id=str(channel.id if isinstance(channel, bases.Unique) else int(channel)), - message_id=str(message.id if isinstance(message, bases.Unique) else int(message)), - emoji=str(getattr(emoji, "url_name", emoji)), - ) - - def fetch_reactors( - self, - channel: typing.Union[bases.Snowflake, int, str, channels_.PartialChannel], - message: typing.Union[bases.Snowflake, int, str, messages_.Message], - emoji: typing.Union[emojis.Emoji, str], - after: typing.Optional[typing.Union[datetime.datetime, bases.Unique, bases.Snowflake, int, str]] = None, - ) -> pagination.LazyIterator[users.User]: - """Get an async iterator of the users who reacted to a message. - - This returns the users created after a given user object/ID or from the - oldest user who reacted. - - Parameters - ---------- - channel : hikari.models.channels.PartialChannel | hikari.models.bases.Snowflake | int | str - The object or ID of the channel to get the message from. - message : hikari.models.messages.Message | hikari.models.bases.Snowflake | int | str - The object or ID of the message to get the reactions from. - emoji : hikari.models.emojis.Emoji | str - The emoji to get. This can either be it's object or the string - representation of the emoji. The string representation will be - either `"name:id"` for custom emojis else it's unicode - character(s) (can be UTF-32). - after : datetime.datetime | hikari.models.bases.Unique | hikari.models.bases.Snowflake | int | str | None - A limit to the users returned. This allows you to only receive - users that were created after the given object, if desired. - If a unique object (like a message), then message created after this - object will be returned. If a datetime, then messages after that - datetime will be returned. If unspecified or `None`, the filter is not - used. - - Examples - -------- - async for user in client.fetch_reactors_after(channel, message, emoji): - if user.is_bot: - await client.kick_member(channel.guild_id, user) - - Returns - ------- - hikari.models.pagination.LazyIterator[hikari.models.users.User] - An async iterator of user objects. - - Raises - ------ - hikari.errors.BadRequest - If any invalid snowflake IDs are passed; a snowflake may be invalid - due to it being outside of the range of a 64 bit integer. - hikari.errors.Forbidden - If you lack access to the message. - hikari.errors.NotFound - If the channel or message is not found. - """ - return _ReactionPaginator( - app=self._app, channel=channel, message=message, emoji=emoji, users_after=after, session=self._session, - ) diff --git a/hikari/.rest/voice.py b/hikari/.rest/voice.py deleted file mode 100644 index 43a9ae76ab..0000000000 --- a/hikari/.rest/voice.py +++ /dev/null @@ -1,47 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# Copyright © Nekoka.tt 2019-2020 -# -# This file is part of Hikari. -# -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . -"""The logic for handling all requests to voice endpoints.""" - -from __future__ import annotations - -__all__ = ["RESTVoiceComponent"] - -import abc -import typing - -from hikari.models import voices -from hikari.net.rest import base - - -class RESTVoiceComponent(base.BaseRESTComponent, abc.ABC): # pylint: disable=abstract-method - """The RESTSession client component for handling requests to voice endpoints.""" - - async def fetch_voice_regions(self) -> typing.Sequence[voices.VoiceRegion]: - """Get the voice regions that are available. - - Returns - ------- - typing.Sequence[hikari.models.voices.VoiceRegion] - A list of voice regions available - - !!! note - This does not include VIP servers. - """ - payload = await self._session.list_voice_regions() - return [voices.VoiceRegion.deserialize(region, app=self._app) for region in payload] diff --git a/hikari/.rest/webhook.py b/hikari/.rest/webhook.py deleted file mode 100644 index 5370ae3bf2..0000000000 --- a/hikari/.rest/webhook.py +++ /dev/null @@ -1,321 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# Copyright © Nekoka.tt 2019-2020 -# -# This file is part of Hikari. -# -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . -"""The logic for handling all requests to webhook endpoints.""" - -from __future__ import annotations - -__all__ = ["RESTWebhookComponent"] - -import abc -import typing - -from hikari.internal import helpers -from hikari.models import bases -from hikari.models import messages as messages_ -from hikari.models import webhooks -from hikari.net.rest import base - -if typing.TYPE_CHECKING: - from hikari.models import channels as channels_ - from hikari.models import embeds as embeds_ - from hikari.models import files as files_ - from hikari.models import guilds - from hikari.models import users - - -class RESTWebhookComponent(base.BaseRESTComponent, abc.ABC): # pylint: disable=abstract-method - """The RESTSession client component for handling requests to webhook endpoints.""" - - async def fetch_webhook( - self, webhook: typing.Union[bases.Snowflake, int, str, webhooks.Webhook], *, webhook_token: str = ... - ) -> webhooks.Webhook: - """Get a given webhook. - - Parameters - ---------- - webhook : hikari.models.webhooks.Webhook | hikari.models.bases.Snowflake | int | str - The object or ID of the webhook to get. - webhook_token : str - If specified, the webhook token to use to get it (bypassing this - session's provided authorization `token`). - - Returns - ------- - hikari.models.webhooks.Webhook - The requested webhook object. - - Raises - ------ - hikari.errors.BadRequest - If any invalid snowflake IDs are passed; a snowflake may be invalid - due to it being outside of the range of a 64 bit integer. - hikari.errors.NotFound - If the webhook is not found. - hikari.errors.Forbidden - If you're not in the guild that owns this webhook or - lack the `MANAGE_WEBHOOKS` permission. - hikari.errors.Unauthorized - If you pass a token that's invalid for the target webhook. - """ - payload = await self._session.get_webhook( - webhook_id=str(webhook.id if isinstance(webhook, bases.Unique) else int(webhook)), - webhook_token=webhook_token, - ) - return webhooks.Webhook.deserialize(payload, app=self._app) - - async def update_webhook( - self, - webhook: typing.Union[bases.Snowflake, int, str, webhooks.Webhook], - *, - webhook_token: str = ..., - name: str = ..., - avatar: typing.Optional[files_.BaseStream] = ..., - channel: typing.Union[bases.Snowflake, int, str, channels_.GuildChannel] = ..., - reason: str = ..., - ) -> webhooks.Webhook: - """Edit a given webhook. - - Parameters - ---------- - webhook : hikari.models.webhooks.Webhook | hikari.models.bases.Snowflake | int | str - The object or ID of the webhook to edit. - webhook_token : str - If specified, the webhook token to use to modify it (bypassing this - session's provided authorization `token`). - name : str - If specified, the new name string. - avatar : hikari.models.files.BaseStream | None - If specified, the new avatar image. If `None`, then - it is removed. - channel : hikari.models.channels.GuildChannel | hikari.models.bases.Snowflake | int | str - If specified, the object or ID of the new channel the given - webhook should be moved to. - reason : str - If specified, the audit log reason explaining why the operation - was performed. - - Returns - ------- - hikari.models.webhooks.Webhook - The updated webhook object. - - Raises - ------ - hikari.errors.BadRequest - If any invalid snowflake IDs are passed; a snowflake may be invalid - due to it being outside of the range of a 64 bit integer. - hikari.errors.NotFound - If either the webhook or the channel aren't found. - hikari.errors.Forbidden - If you either lack the `MANAGE_WEBHOOKS` permission or - aren't a member of the guild this webhook belongs to. - hikari.errors.Unauthorized - If you pass a token that's invalid for the target webhook. - """ - payload = await self._session.modify_webhook( - webhook_id=str(webhook.id if isinstance(webhook, bases.Unique) else int(webhook)), - webhook_token=webhook_token, - name=name, - avatar=await avatar.read() if avatar is not ... else ..., - channel_id=( - str(channel.id if isinstance(channel, bases.Unique) else int(channel)) - if channel and channel is not ... - else channel - ), - reason=reason, - ) - return webhooks.Webhook.deserialize(payload, app=self._app) - - async def delete_webhook( - self, webhook: typing.Union[bases.Snowflake, int, str, webhooks.Webhook], *, webhook_token: str = ... - ) -> None: - """Delete a given webhook. - - Parameters - ---------- - webhook : hikari.models.webhooks.Webhook | hikari.models.bases.Snowflake | int | str - The object or ID of the webhook to delete - webhook_token : str - If specified, the webhook token to use to delete it (bypassing this - session's provided authorization `token`). - - Raises - ------ - hikari.errors.BadRequest - If any invalid snowflake IDs are passed; a snowflake may be invalid - due to it being outside of the range of a 64 bit integer. - hikari.errors.NotFound - If the webhook is not found. - hikari.errors.Forbidden - If you either lack the `MANAGE_WEBHOOKS` permission or - aren't a member of the guild this webhook belongs to. - hikari.errors.Unauthorized - If you pass a token that's invalid for the target webhook. - """ - await self._session.delete_webhook( - webhook_id=str(webhook.id if isinstance(webhook, bases.Unique) else int(webhook)), - webhook_token=webhook_token, - ) - - async def execute_webhook( # pylint:disable=too-many-locals,line-too-long - self, - webhook: typing.Union[bases.Snowflake, int, str, webhooks.Webhook], - webhook_token: str, - *, - content: str = ..., - username: str = ..., - avatar_url: str = ..., - tts: bool = ..., - wait: bool = False, - files: typing.Sequence[files_.BaseStream] = ..., - embeds: typing.Sequence[embeds_.Embed] = ..., - mentions_everyone: bool = True, - user_mentions: typing.Union[ - typing.Collection[typing.Union[bases.Snowflake, int, str, users.User]], bool - ] = True, - role_mentions: typing.Union[ - typing.Collection[typing.Union[bases.Snowflake, int, str, guilds.Role]], bool - ] = True, - ) -> typing.Optional[messages_.Message]: - """Execute a webhook to create a message. - - Parameters - ---------- - webhook : hikari.models.webhooks.Webhook | hikari.models.bases.Snowflake | int | str - The object or ID of the webhook to execute. - webhook_token : str - The token of the webhook to execute. - content : str - If specified, the message content to send with the message. - username : str - If specified, the username to override the webhook's username - for this request. - avatar_url : str - If specified, the url of an image to override the webhook's - avatar with for this request. - tts : bool - If specified, whether the message will be sent as a TTS message. - wait : bool - If specified, whether this request should wait for the webhook - to be executed and return the resultant message object. - files : typing.Sequence[hikari.models.files.BaseStream] - If specified, a sequence of files to upload. - embeds : typing.Sequence[hikari.models.embeds.Embed] - If specified, a sequence of between `1` to `10` embed objects - (inclusive) to send with the embed. - mentions_everyone : bool - Whether `@everyone` and `@here` mentions should be resolved by - discord and lead to actual pings, defaults to `True`. - user_mentions : typing.Collection[hikari.models.users.User | hikari.models.bases.Snowflake | int | str] | bool - Either an array of user objects/IDs to allow mentions for, - `True` to allow all user mentions or `False` to block all - user mentions from resolving, defaults to `True`. - role_mentions : typing.Collection[hikari.models.guilds.Role | hikari.models.bases.Snowflake | int | str] | bool - Either an array of guild role objects/IDs to allow mentions for, - `True` to allow all role mentions or `False` to block all - role mentions from resolving, defaults to `True`. - - Returns - ------- - hikari.models.messages.Message | None - The created message object, if `wait` is `True`, else `None`. - - Raises - ------ - hikari.errors.NotFound - If the webhook ID is not found. - hikari.errors.BadRequest - This can be raised if the file is too large; if the embed exceeds - the defined limits; if the message content is specified only and - empty or greater than `2000` characters; if neither content, file - or embeds are specified. - If any invalid snowflake IDs are passed; a snowflake may be invalid - due to it being outside of the range of a 64 bit integer. - hikari.errors.Unauthorized - If you pass a token that's invalid for the target webhook. - ValueError - If more than 100 unique objects/entities are passed for - `role_mentions` or `user_mentions`. - """ - file_resources = [] - if files is not ...: - file_resources += files - if embeds is not ...: - for embed in embeds: - file_resources += embed.assets_to_upload - - payload = await self._session.execute_webhook( - webhook_id=str(webhook.id if isinstance(webhook, bases.Unique) else int(webhook)), - webhook_token=webhook_token, - content=content, - username=username, - avatar_url=avatar_url, - tts=tts, - wait=wait, - files=file_resources if file_resources else ..., - embeds=[embed.serialize() for embed in embeds] if embeds is not ... else ..., - allowed_mentions=helpers.generate_allowed_mentions( - mentions_everyone=mentions_everyone, user_mentions=user_mentions, role_mentions=role_mentions - ), - ) - if wait is True: - return messages_.Message.deserialize(payload, app=self._app) - return None - - def safe_webhook_execute( - self, - webhook: typing.Union[bases.Snowflake, int, str, webhooks.Webhook], - webhook_token: str, - *, - content: str = ..., - username: str = ..., - avatar_url: str = ..., - tts: bool = ..., - wait: bool = False, - files: typing.Sequence[files_.BaseStream] = ..., - embeds: typing.Sequence[embeds_.Embed] = ..., - mentions_everyone: bool = False, - user_mentions: typing.Union[ - typing.Collection[typing.Union[bases.Snowflake, int, str, users.User]], bool - ] = False, - role_mentions: typing.Union[ - typing.Collection[typing.Union[bases.Snowflake, int, str, guilds.Role]], bool - ] = False, - ) -> typing.Coroutine[typing.Any, typing.Any, typing.Optional[messages_.Message]]: - """Execute a webhook to create a message with mention safety. - - This endpoint has the same signature as - `RESTWebhookComponent.execute_webhook` with the only difference being - that `mentions_everyone`, `user_mentions` and `role_mentions` default to - `False`. - """ - return self.execute_webhook( - webhook=webhook, - webhook_token=webhook_token, - content=content, - username=username, - avatar_url=avatar_url, - tts=tts, - wait=wait, - files=files, - embeds=embeds, - mentions_everyone=mentions_everyone, - user_mentions=user_mentions, - role_mentions=role_mentions, - ) diff --git a/hikari/net/rest.py b/hikari/net/rest.py index 1652e52ee0..4c07db3635 100644 --- a/hikari/net/rest.py +++ b/hikari/net/rest.py @@ -50,6 +50,7 @@ from hikari.models import permissions from hikari.models import unset from hikari.models import users +from hikari.models import voices from hikari.models import webhooks from hikari.net import buckets from hikari.net import http_client @@ -926,15 +927,13 @@ async def add_user_to_guild( *, nickname: typing.Union[unset.Unset, str] = unset.UNSET, roles: typing.Union[ - unset.Unset, - typing.Collection[typing.Union[guilds.Role, bases.UniqueObjectT]] + unset.Unset, typing.Collection[typing.Union[guilds.Role, bases.UniqueObjectT]] ] = unset.UNSET, mute: typing.Union[unset.Unset, bool] = unset.UNSET, deaf: typing.Union[unset.Unset, bool] = unset.UNSET, ) -> typing.Optional[guilds.GuildMember]: route = routes.PUT_GUILD_MEMBER.compile( - guild=conversions.cast_to_str_id(guild), - user=conversions.cast_to_str_id(user), + guild=conversions.cast_to_str_id(guild), user=conversions.cast_to_str_id(user), ) payload = {"access_token": access_token} @@ -950,3 +949,8 @@ async def add_user_to_guild( return None else: return self._app.entity_factory.deserialize_guild_member(payload) + + async def fetch_voice_regions(self) -> typing.Sequence[voices.VoiceRegion]: + route = routes.GET_VOICE_REGIONS.compile() + response = await self._request(route) + return [self._app.entity_factory.deserialize_voice_region(r) for r in response] From 4a16b9dea7bbabb6e42b069cd1135c2f496fe1b5 Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Sat, 23 May 2020 14:15:47 +0100 Subject: [PATCH 384/922] Implemented fetch_user --- hikari/.rest/user.py | 58 -------------------------------------------- hikari/net/rest.py | 5 ++++ 2 files changed, 5 insertions(+), 58 deletions(-) delete mode 100644 hikari/.rest/user.py diff --git a/hikari/.rest/user.py b/hikari/.rest/user.py deleted file mode 100644 index 18293a1f8f..0000000000 --- a/hikari/.rest/user.py +++ /dev/null @@ -1,58 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# Copyright © Nekoka.tt 2019-2020 -# -# This file is part of Hikari. -# -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . -"""The logic for handling all requests to user endpoints.""" - -from __future__ import annotations - -__all__ = ["RESTUserComponent"] - -import abc -import typing - -from hikari.models import bases -from hikari.models import users -from hikari.net.rest import base - - -class RESTUserComponent(base.BaseRESTComponent, abc.ABC): # pylint: disable=abstract-method - """The RESTSession client component for handling requests to user endpoints.""" - - async def fetch_user(self, user: typing.Union[bases.Snowflake, int, str, users.User]) -> users.User: - """Get a given user. - - Parameters - ---------- - user : hikari.models.users.User | hikari.models.bases.Snowflake | int | str - The object or ID of the user to get. - - Returns - ------- - hikari.models.users.User - The requested user object. - - Raises - ------ - hikari.errors.BadRequest - If any invalid snowflake IDs are passed; a snowflake may be invalid - due to it being outside of the range of a 64 bit integer. - hikari.errors.NotFound - If the user is not found. - """ - payload = await self._session.get_user(user_id=str(user.id if isinstance(user, bases.Unique) else int(user))) - return users.User.deserialize(payload, app=self._app) diff --git a/hikari/net/rest.py b/hikari/net/rest.py index 4c07db3635..3e0865db82 100644 --- a/hikari/net/rest.py +++ b/hikari/net/rest.py @@ -954,3 +954,8 @@ async def fetch_voice_regions(self) -> typing.Sequence[voices.VoiceRegion]: route = routes.GET_VOICE_REGIONS.compile() response = await self._request(route) return [self._app.entity_factory.deserialize_voice_region(r) for r in response] + + async def fetch_user(self, user: typing.Union[users.User, bases.UniqueObjectT]) -> users.User: + route = routes.GET_USER.compile(user=conversions.cast_to_str_id(user)) + response = await self._request(route) + return self._app.entity_factory.deserialize_user(response) From 59602d14b82d9b216c38a7b4c2bc33b4f62dbe0e Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Sat, 23 May 2020 14:23:45 +0100 Subject: [PATCH 385/922] Moved pagination.py to net/iterators.py --- hikari/.rest/guild.py | 6 +- hikari/impl/pagination.py | 10 +- hikari/{pagination.py => net/iterators.py} | 105 ++++++++++++++++++++- hikari/net/rest.py | 24 +++-- hikari/net/rest_utils.py | 100 ++------------------ 5 files changed, 131 insertions(+), 114 deletions(-) rename hikari/{pagination.py => net/iterators.py} (66%) diff --git a/hikari/.rest/guild.py b/hikari/.rest/guild.py index 5a6175fd40..c1e63492cf 100644 --- a/hikari/.rest/guild.py +++ b/hikari/.rest/guild.py @@ -27,7 +27,7 @@ import functools import typing -from hikari import pagination +from hikari.net import iterators from hikari.models import audit_logs from hikari.models import bases from hikari.models import channels as channels_ @@ -45,7 +45,7 @@ from hikari.models import users -class _MemberPaginator(pagination.BufferedLazyIterator[guilds.GuildMember]): +class _MemberPaginator(iterators.BufferedLazyIterator[guilds.GuildMember]): __slots__ = ("_app", "_guild_id", "_first_id", "_session") def __init__(self, app, guild, created_after, session): @@ -862,7 +862,7 @@ async def fetch_member( def fetch_members( self, guild: typing.Union[bases.Snowflake, int, str, guilds.Guild], - ) -> pagination.LazyIterator[guilds.GuildMember]: + ) -> iterators.LazyIterator[guilds.GuildMember]: """Get an async iterator of all the members in a given guild. Parameters diff --git a/hikari/impl/pagination.py b/hikari/impl/pagination.py index 15166f6b9e..c3e3efe398 100644 --- a/hikari/impl/pagination.py +++ b/hikari/impl/pagination.py @@ -22,7 +22,7 @@ import datetime -from hikari import pagination +from hikari.net import iterators from hikari.models import applications from hikari.models import bases from hikari.models import guilds @@ -30,7 +30,7 @@ from hikari.models import users -class GuildPaginator(pagination.BufferedLazyIterator[guilds.Guild]): +class GuildPaginator(iterators.BufferedLazyIterator[guilds.Guild]): __slots__ = ("_app", "_newest_first", "_first_id", "_request_partial") def __init__(self, app, newest_first, first_item, request_partial): @@ -55,7 +55,7 @@ async def _next_chunk(self): return (applications.OwnGuild.deserialize(g, app=self._app) for g in chunk) -class MemberPaginator(pagination.BufferedLazyIterator[guilds.GuildMember]): +class MemberPaginator(iterators.BufferedLazyIterator[guilds.GuildMember]): __slots__ = ("_app", "_guild_id", "_first_id", "_request_partial") def __init__(self, app, guild, created_after, request_partial): @@ -76,7 +76,7 @@ async def _next_chunk(self): return (guilds.GuildMember.deserialize(m, app=self._app) for m in chunk) -class MessagePaginator(pagination.BufferedLazyIterator[messages.Message]): +class MessagePaginator(iterators.BufferedLazyIterator[messages.Message]): __slots__ = ("_app", "_channel_id", "_direction", "_first_id", "_request_partial") def __init__(self, app, channel, direction, first, request_partial) -> None: @@ -108,7 +108,7 @@ async def _next_chunk(self): return (messages.Message.deserialize(m, app=self._app) for m in chunk) -class ReactionPaginator(pagination.BufferedLazyIterator[messages.Reaction]): +class ReactionPaginator(iterators.BufferedLazyIterator[messages.Reaction]): __slots__ = ("_app", "_channel_id", "_message_id", "_first_id", "_emoji", "_request_partial") def __init__(self, app, channel, message, emoji, users_after, request_partial) -> None: diff --git a/hikari/pagination.py b/hikari/net/iterators.py similarity index 66% rename from hikari/pagination.py rename to hikari/net/iterators.py index be7c2060dc..f12a94476f 100644 --- a/hikari/pagination.py +++ b/hikari/net/iterators.py @@ -16,13 +16,22 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . -"""Async iterator extensions for paginated data.""" +"""Lazy iterators for data that requires repeated API calls to retrieve.""" from __future__ import annotations import abc import typing +from hikari import base_app from hikari.internal import more_collections +from hikari.internal import more_typing +from hikari.models import applications +from hikari.models import bases +from hikari.models import guilds +from hikari.models import messages +from hikari.models import users +from hikari.net import routes + _T = typing.TypeVar("_T") @@ -224,3 +233,97 @@ async def __anext__(self) -> _T: self._complete() else: return next(self._buffer) + + +class MessageIterator(BufferedLazyIterator[messages.Message]): + """Implementation of an iterator for message history.""" + + __slots__ = ("_app", "_request_call", "_direction", "_first_id", "_route") + + def __init__( + self, + app: base_app.IBaseApp, + request_call: typing.Callable[..., more_typing.Coroutine[more_typing.JSONArray]], + channel_id: str, + direction: str, + first_id: str, + ) -> None: + super().__init__() + self._app = app + self._request_call = request_call + self._direction = direction + self._first_id = first_id + self._route = routes.GET_CHANNEL_MESSAGES.compile(channel=channel_id) + + async def _next_chunk(self) -> typing.Optional[typing.Generator[messages.Message, typing.Any, None]]: + chunk = await self._request_call(self._route, query={self._direction: self._first_id, "limit": 100}) + + if not chunk: + return None + if self._direction == "after": + chunk.reverse() + + self._first_id = chunk[-1]["id"] + return (self._app.entity_factory.deserialize_message(m) for m in chunk) + + +class ReactorIterator(BufferedLazyIterator[users.User]): + """Implementation of an iterator for message reactions.""" + + __slots__ = ("_app", "_first_id", "_route", "_request_call") + + def __init__( + self, + app: base_app.IBaseApp, + request_call: typing.Callable[..., more_typing.Coroutine[more_typing.JSONArray]], + channel_id: str, + message_id: str, + emoji: str, + ) -> None: + super().__init__() + self._app = app + self._request_call = request_call + self._first_id = bases.Snowflake.min() + self._route = routes.GET_REACTIONS.compile(channel_id=channel_id, message_id=message_id, emoji=emoji) + + async def _next_chunk(self) -> typing.Optional[typing.Generator[users.User, typing.Any, None]]: + chunk = await self._request_call(self._route, query={"after": self._first_id, "limit": 100}) + + if not chunk: + return None + + self._first_id = chunk[-1]["id"] + return (self._app.entity_factory.deserialize_user(u) for u in chunk) + + +class OwnGuildIterator(BufferedLazyIterator[applications.OwnGuild]): + __slots__ = ("_app", "_request_call", "_route", "_newest_first", "_first_id") + + def __init__( + self, + app: base_app.IBaseApp, + request_call: typing.Callable[..., more_typing.Coroutine[more_typing.JSONArray]], + newest_first: bool, + first_id: str, + ) -> None: + super().__init__() + self._app = app + self._newest_first = newest_first + self._request_call = request_call + self._first_id = first_id + self._route = routes.GET_MY_GUILDS.compile() + + async def _next_chunk(self) -> typing.Optional[typing.Generator[applications.OwnGuild, typing.Any, None]]: + kwargs = {"before" if self._newest_first else "after": self._first_id, "limit": 100} + + chunk = await self._request_call(self._route, query=kwargs) + + if not chunk: + return None + + self._first_id = chunk[-1]["id"] + return (self._app.entity_factory.deserialize_own_guild(g) for g in chunk) + + +class MemberIterator(BufferedLazyIterator[guilds.GuildMember]): + __slots__ = () diff --git a/hikari/net/rest.py b/hikari/net/rest.py index 3e0865db82..829455f714 100644 --- a/hikari/net/rest.py +++ b/hikari/net/rest.py @@ -32,7 +32,7 @@ from hikari import base_app from hikari import errors from hikari import http_settings -from hikari import pagination +from hikari.net import iterators from hikari.internal import conversions from hikari.internal import more_collections from hikari.internal import more_typing @@ -502,7 +502,7 @@ async def delete_pinned_message( @typing.overload def fetch_messages( self, channel: typing.Union[channels.TextChannel, bases.UniqueObjectT], / - ) -> pagination.LazyIterator[messages.Message]: + ) -> iterators.LazyIterator[messages.Message]: ... @typing.overload @@ -512,7 +512,7 @@ def fetch_messages( /, *, before: typing.Union[datetime.datetime, bases.UniqueObjectT], - ) -> pagination.LazyIterator[messages.Message]: + ) -> iterators.LazyIterator[messages.Message]: ... @typing.overload @@ -522,7 +522,7 @@ def fetch_messages( /, *, around: typing.Union[datetime.datetime, bases.UniqueObjectT], - ) -> pagination.LazyIterator[messages.Message]: + ) -> iterators.LazyIterator[messages.Message]: ... @typing.overload @@ -532,7 +532,7 @@ def fetch_messages( /, *, after: typing.Union[datetime.datetime, bases.UniqueObjectT], - ) -> pagination.LazyIterator[messages.Message]: + ) -> iterators.LazyIterator[messages.Message]: ... def fetch_messages( @@ -540,7 +540,7 @@ def fetch_messages( channel: typing.Union[channels.TextChannel, bases.UniqueObjectT], /, **kwargs: typing.Optional[typing.Union[datetime.datetime, bases.UniqueObjectT]], - ) -> pagination.LazyIterator[messages.Message]: + ) -> iterators.LazyIterator[messages.Message]: if len(kwargs) == 1 and any(direction in kwargs for direction in ("before", "after", "around")): direction, timestamp = kwargs.popitem() elif not kwargs: @@ -551,7 +551,7 @@ def fetch_messages( if isinstance(timestamp, datetime.datetime): timestamp = bases.Snowflake.from_datetime(timestamp) - return rest_utils.MessagePaginator( + return iterators.MessageIterator( self._app, self._request, conversions.cast_to_str_id(channel), @@ -713,8 +713,8 @@ def fetch_reactions_for_emoji( channel: typing.Union[channels.TextChannel, bases.UniqueObjectT], message: typing.Union[messages.Message, bases.UniqueObjectT], emoji: typing.Union[str, emojis.UnicodeEmoji, emojis.KnownCustomEmoji], - ) -> pagination.LazyIterator[users.User]: - return rest_utils.ReactorPaginator( + ) -> iterators.LazyIterator[users.User]: + return iterators.ReactorIterator( app=self._app, request_call=self._request, channel_id=conversions.cast_to_str_id(channel), @@ -896,15 +896,13 @@ def fetch_my_guilds( *, newest_first: bool = False, start_at: typing.Union[unset.Unset, guilds.PartialGuild, bases.UniqueObjectT, datetime.datetime] = unset.UNSET, - ) -> pagination.LazyIterator[applications.OwnGuild]: + ) -> iterators.LazyIterator[applications.OwnGuild]: if unset.is_unset(start_at): start_at = bases.Snowflake.max() if newest_first else bases.Snowflake.min() elif isinstance(start_at, datetime.datetime): start_at = bases.Snowflake.from_datetime(start_at) - return rest_utils.OwnGuildPaginator( - self._app, self._request, newest_first, conversions.cast_to_str_id(start_at) - ) + return iterators.OwnGuildIterator(self._app, self._request, newest_first, conversions.cast_to_str_id(start_at)) async def leave_guild(self, guild: typing.Union[guilds.Guild, bases.UniqueObjectT]) -> None: route = routes.DELETE_MY_GUILD.compile(guild=conversions.cast_to_str_id(guild)) diff --git a/hikari/net/rest_utils.py b/hikari/net/rest_utils.py index 975eac4962..1980a98109 100644 --- a/hikari/net/rest_utils.py +++ b/hikari/net/rest_utils.py @@ -18,7 +18,7 @@ # along with Hikari. If not, see . """Internal utilities used by the REST API. -You should never need to refer to this documentation directly. +You should never need to make any of these objects manually. """ from __future__ import annotations @@ -30,107 +30,23 @@ import types import typing -from hikari import pagination from hikari.internal import conversions -from hikari.models import applications from hikari.models import bases -from hikari.models import channels -from hikari.models import messages -from hikari.models import users from hikari.net import routes if typing.TYPE_CHECKING: - from hikari import base_app from hikari.internal import more_typing + from hikari.models import channels -class MessagePaginator(pagination.BufferedLazyIterator[messages.Message]): - __slots__ = ("_app", "_request_call", "_direction", "_first_id", "_route") - - def __init__( - self, - app: base_app.IBaseApp, - request_call: typing.Callable[..., more_typing.Coroutine[more_typing.JSONArray]], - channel_id: str, - direction: str, - first_id: str, - ) -> None: - super().__init__() - self._app = app - self._request_call = request_call - self._direction = direction - self._first_id = first_id - self._route = routes.GET_CHANNEL_MESSAGES.compile(channel=channel_id) - - async def _next_chunk(self) -> typing.Optional[typing.Generator[messages.Message, typing.Any, None]]: - chunk = await self._request_call(self._route, query={self._direction: self._first_id, "limit": 100}) - - if not chunk: - return None - if self._direction == "after": - chunk.reverse() - - self._first_id = chunk[-1]["id"] - return (self._app.entity_factory.deserialize_message(m) for m in chunk) - - -class ReactorPaginator(pagination.BufferedLazyIterator[users.User]): - __slots__ = ("_app", "_first_id", "_route", "_request_call") - - def __init__( - self, - app: base_app.IBaseApp, - request_call: typing.Callable[..., more_typing.Coroutine[more_typing.JSONArray]], - channel_id: str, - message_id: str, - emoji: str, - ) -> None: - super().__init__() - self._app = app - self._request_call = request_call - self._first_id = bases.Snowflake.min() - self._route = routes.GET_REACTIONS.compile(channel_id=channel_id, message_id=message_id, emoji=emoji) - - async def _next_chunk(self) -> typing.Optional[typing.Generator[users.User, typing.Any, None]]: - chunk = await self._request_call(self._route, query={"after": self._first_id, "limit": 100}) - - if not chunk: - return None - - self._first_id = chunk[-1]["id"] - return (self._app.entity_factory.deserialize_user(u) for u in chunk) - - -class OwnGuildPaginator(pagination.BufferedLazyIterator[applications.OwnGuild]): - __slots__ = ("_app", "_request_call", "_route", "_newest_first", "_first_id") - - def __init__( - self, - app: base_app.IBaseApp, - request_call: typing.Callable[..., more_typing.Coroutine[more_typing.JSONArray]], - newest_first: bool, - first_id: str, - ) -> None: - super().__init__() - self._app = app - self._newest_first = newest_first - self._request_call = request_call - self._first_id = first_id - self._route = routes.GET_MY_GUILDS.compile() - - async def _next_chunk(self) -> typing.Optional[typing.Generator[applications.OwnGuild, typing.Any, None]]: - kwargs = {"before" if self._newest_first else "after": self._first_id, "limit": 100} - - chunk = await self._request_call(self._route, query=kwargs) - - if not chunk: - return None - - self._first_id = chunk[-1]["id"] - return (self._app.entity_factory.deserialize_own_guild(g) for g in chunk) +class TypingIndicator: + """Result type of `hiarki.net.rest.trigger_typing`. + This is an object that can either be awaited like a coroutine to trigger + the typing indicator once, or an async context manager to keep triggering + the typing indicator repeatedly until the context finishes. + """ -class TypingIndicator: __slots__ = ("_channel", "_request_call", "_task") def __init__( From 974939ad92a89fa02ed9a72adec38bde5fbcbd10 Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Sat, 23 May 2020 15:38:26 +0100 Subject: [PATCH 386/922] Audit logs --- hikari/.rest/guild.py | 2 +- hikari/.rest/session.py | 2 +- hikari/entity_factory.py | 4 +- hikari/impl/entity_factory.py | 4 +- hikari/impl/pagination.py | 8 +- hikari/models/audit_logs.py | 117 +------------------------ hikari/net/iterators.py | 89 +++++++++++++++---- hikari/net/rest.py | 53 ++++++----- tests/hikari/models/test_audit_logs.py | 6 +- 9 files changed, 121 insertions(+), 164 deletions(-) diff --git a/hikari/.rest/guild.py b/hikari/.rest/guild.py index c1e63492cf..d66a251e94 100644 --- a/hikari/.rest/guild.py +++ b/hikari/.rest/guild.py @@ -45,7 +45,7 @@ from hikari.models import users -class _MemberPaginator(iterators.BufferedLazyIterator[guilds.GuildMember]): +class _MemberPaginator(iterators._BufferedLazyIterator[guilds.GuildMember]): __slots__ = ("_app", "_guild_id", "_first_id", "_session") def __init__(self, app, guild, created_after, session): diff --git a/hikari/.rest/session.py b/hikari/.rest/session.py index f526de7003..83f6b97b97 100644 --- a/hikari/.rest/session.py +++ b/hikari/.rest/session.py @@ -409,7 +409,7 @@ async def get_guild_audit_log( """ query = {} conversions.put_if_specified(query, "user_id", user_id) - conversions.put_if_specified(query, "action_type", action_type) + conversions.put_if_specified(query, "event_type", action_type) conversions.put_if_specified(query, "limit", limit) conversions.put_if_specified(query, "before", before) route = routes.GET_GUILD_AUDIT_LOGS.compile(guild_id=guild_id) diff --git a/hikari/entity_factory.py b/hikari/entity_factory.py index b11bb7c74b..3de7cb7ab6 100644 --- a/hikari/entity_factory.py +++ b/hikari/entity_factory.py @@ -67,7 +67,7 @@ def deserialize_application(self, payload: more_typing.JSONObject) -> applicatio ############## @abc.abstractmethod - def deserialize_audit_log_entry(self, payload: more_typing.JSONObject) -> audit_logs.AuditLogEntry: + def deserialize_audit_log(self, payload: more_typing.JSONObject) -> audit_logs.AuditLog: ... ############ @@ -175,7 +175,7 @@ def deserialize_guild_member( ... @abc.abstractmethod - def deserialize_guild_role(self, payload: more_typing.JSONObject) -> guilds.GuildRole: + def deserialize_guild_role(self, payload: more_typing.JSONObject) -> guilds.Role: ... @abc.abstractmethod diff --git a/hikari/impl/entity_factory.py b/hikari/impl/entity_factory.py index 46c8b044c5..0a9aa66677 100644 --- a/hikari/impl/entity_factory.py +++ b/hikari/impl/entity_factory.py @@ -46,7 +46,7 @@ def deserialize_own_guild(self, payload: more_typing.JSONObject) -> applications def deserialize_application(self, payload: more_typing.JSONObject) -> applications: pass - def deserialize_audit_log_entry(self, payload: more_typing.JSONObject) -> audit_logs.AuditLogEntry: + def deserialize_audit_log(self, payload: more_typing.JSONObject) -> audit_logs.AuditLog: pass def deserialize_permission_overwrite(self, payload: more_typing.JSONObject) -> channels.PermissionOverwrite: @@ -113,7 +113,7 @@ def deserialize_guild_member( ) -> guilds.GuildMember: pass - def deserialize_guild_role(self, payload: more_typing.JSONObject) -> guilds.GuildRole: + def deserialize_guild_role(self, payload: more_typing.JSONObject) -> guilds.Role: pass def deserialize_guild_member_presence(self, payload: more_typing.JSONObject) -> guilds.GuildMemberPresence: diff --git a/hikari/impl/pagination.py b/hikari/impl/pagination.py index c3e3efe398..dd838b7fcf 100644 --- a/hikari/impl/pagination.py +++ b/hikari/impl/pagination.py @@ -30,7 +30,7 @@ from hikari.models import users -class GuildPaginator(iterators.BufferedLazyIterator[guilds.Guild]): +class GuildPaginator(iterators._BufferedLazyIterator[guilds.Guild]): __slots__ = ("_app", "_newest_first", "_first_id", "_request_partial") def __init__(self, app, newest_first, first_item, request_partial): @@ -55,7 +55,7 @@ async def _next_chunk(self): return (applications.OwnGuild.deserialize(g, app=self._app) for g in chunk) -class MemberPaginator(iterators.BufferedLazyIterator[guilds.GuildMember]): +class MemberPaginator(iterators._BufferedLazyIterator[guilds.GuildMember]): __slots__ = ("_app", "_guild_id", "_first_id", "_request_partial") def __init__(self, app, guild, created_after, request_partial): @@ -76,7 +76,7 @@ async def _next_chunk(self): return (guilds.GuildMember.deserialize(m, app=self._app) for m in chunk) -class MessagePaginator(iterators.BufferedLazyIterator[messages.Message]): +class MessagePaginator(iterators._BufferedLazyIterator[messages.Message]): __slots__ = ("_app", "_channel_id", "_direction", "_first_id", "_request_partial") def __init__(self, app, channel, direction, first, request_partial) -> None: @@ -108,7 +108,7 @@ async def _next_chunk(self): return (messages.Message.deserialize(m, app=self._app) for m in chunk) -class ReactionPaginator(iterators.BufferedLazyIterator[messages.Reaction]): +class ReactionPaginator(iterators._BufferedLazyIterator[messages.Reaction]): __slots__ = ("_app", "_channel_id", "_message_id", "_first_id", "_emoji", "_request_partial") def __init__(self, app, channel, message, emoji, users_after, request_partial) -> None: diff --git a/hikari/models/audit_logs.py b/hikari/models/audit_logs.py index abe6b0d195..445b3493ee 100644 --- a/hikari/models/audit_logs.py +++ b/hikari/models/audit_logs.py @@ -448,7 +448,7 @@ class AuditLogEntry(bases.Unique, marshaller.Deserializable): """The type of action this entry represents.""" options: typing.Optional[BaseAuditLogEntryInfo] = attr.attrib(eq=False, hash=False, repr=False) - """Extra information about this entry. Only be provided for certain `action_type`.""" + """Extra information about this entry. Only be provided for certain `event_type`.""" reason: typing.Optional[str] = attr.attrib(eq=False, hash=False, repr=False) """The reason for this change, if set (between 0-512 characters).""" @@ -456,7 +456,7 @@ class AuditLogEntry(bases.Unique, marshaller.Deserializable): @classmethod def deserialize(cls, payload: more_typing.JSONObject, **kwargs: typing.Any) -> AuditLogEntry: """Deserialize this model from a raw payload.""" - action_type = conversions.try_cast(payload["action_type"], AuditLogEventType, payload["action_type"]) + action_type = conversions.try_cast(payload["event_type"], AuditLogEventType, payload["event_type"]) if target_id := payload.get("target_id"): target_id = bases.Snowflake(target_id) @@ -507,7 +507,7 @@ def _deserialize_webhooks( return {bases.Snowflake(webhook["id"]): webhooks_.Webhook.deserialize(webhook, **kwargs) for webhook in payload} -# TODO: can we remove this? it is used by a seemingly duplicated endpoint that can just use the iterator. +# TODO: make this support looking like a list of entries... @marshaller.marshallable() @attr.s(eq=True, repr=False, kw_only=True, slots=True) class AuditLog(bases.Entity, marshaller.Deserializable): @@ -532,114 +532,3 @@ class AuditLog(bases.Entity, marshaller.Deserializable): deserializer=_deserialize_webhooks, inherit_kwargs=True, ) """A mapping of the objects of webhooks found in this audit log.""" - - -class AuditLogIterator(typing.AsyncIterator[AuditLogEntry]): - """An async iterator used for iterating through a guild's audit log entries. - - This returns the audit log entries created before a given entry object/ID or - from the newest audit log entry to the oldest. - - Parameters - ---------- - app : hikari.components.application.Application - The `hikari.components.application.Application` that this should pass through - to the generated entities. - request : typing.Callable[..., typing.Coroutine[typing.Any, typing.Any, typing.Any]] - The prepared session bound partial function that this iterator should - use for making Get Guild Audit Log requests. - limit : int - If specified, the limit to how many entries this iterator should return - else unlimited. - before : str - If specified, an entry ID to specify where this iterator's returned - audit log entries should start. - - Yields - ------ - AuditLogEntry - The entries found in this audit log. - - !!! note - This iterator's attributes `AuditLogIterator.integrations`, - `AuditLogIterator.users` and `AuditLogIterator.webhooks` will be filled - up as this iterator makes requests to the Get Guild Audit Log endpoint - with the relevant objects for entities referenced by returned entries. - """ - - __slots__ = ( - "_buffer", - "_app", - "_front", - "_limit", - "_request", - "integrations", - "users", - "webhooks", - ) - - integrations: typing.MutableMapping[bases.Snowflake, guilds.GuildIntegration] - """A mapping of the partial integrations objects found in this log so far.""" - - users: typing.MutableMapping[bases.Snowflake, users_.User] - """A mapping of the objects of users found in this audit log so far.""" - - webhooks: typing.MutableMapping[bases.Snowflake, webhooks_.Webhook] - """A mapping of the objects of webhooks found in this audit log so far.""" - - def __init__( - self, - app: application.Application, - request: typing.Callable[..., more_typing.Coroutine[typing.Any]], - before: typing.Optional[str] = str(bases.Snowflake.max()), - limit: typing.Optional[int] = None, - ) -> None: - self._app = app - self._limit = limit - self._buffer = [] - self._request = request - self._front = before - self.users = {} - self.webhooks = {} - self.integrations = {} - - def __aiter__(self) -> AuditLogIterator: - return self - - async def __anext__(self) -> AuditLogEntry: - if not self._buffer and self._limit != 0: - await self._fill() - try: - entry = AuditLogEntry.deserialize(self._buffer.pop(), app=self._app) - self._front = str(entry.id) - return entry - except IndexError: - raise StopAsyncIteration - - async def _fill(self) -> None: - """Retrieve entries before `_front` and add to `_buffer`.""" - payload = await self._request( - before=self._front, limit=100 if self._limit is None or self._limit > 100 else self._limit, - ) - if self._limit is not None: - self._limit -= len(payload["audit_log_entries"]) - - # Once the resources has been exhausted, discord will return empty lists. - payload["audit_log_entries"].reverse() - self._buffer.extend(payload["audit_log_entries"]) - if users := payload.get("users"): - self.users = copy.copy(self.users) - self.users.update({bases.Snowflake(u["id"]): users_.User.deserialize(u, app=self._app) for u in users}) - if webhooks := payload.get("webhooks"): - self.webhooks = copy.copy(self.webhooks) - self.webhooks.update( - {bases.Snowflake(w["id"]): webhooks_.Webhook.deserialize(w, app=self._app) for w in webhooks} - ) - if integrations := payload.get("integrations"): - self.integrations = copy.copy(self.integrations) - self.integrations.update( - { - bases.Snowflake(i["id"]): guilds.PartialGuildIntegration.deserialize(i, app=self._app) - for i in integrations - } - ) diff --git a/hikari/net/iterators.py b/hikari/net/iterators.py index f12a94476f..2a10ef4904 100644 --- a/hikari/net/iterators.py +++ b/hikari/net/iterators.py @@ -19,20 +19,24 @@ """Lazy iterators for data that requires repeated API calls to retrieve.""" from __future__ import annotations +__all__ = ["LazyIterator"] + import abc import typing -from hikari import base_app +from hikari import rest_app +from hikari.internal import conversions from hikari.internal import more_collections from hikari.internal import more_typing from hikari.models import applications +from hikari.models import audit_logs from hikari.models import bases from hikari.models import guilds from hikari.models import messages +from hikari.models import unset from hikari.models import users from hikari.net import routes - _T = typing.TypeVar("_T") @@ -205,18 +209,14 @@ async def __anext__(self) -> _T: return next_item -class BufferedLazyIterator(typing.Generic[_T], LazyIterator[_T]): - """A buffered paginator implementation that handles chunked responses.""" - +class _BufferedLazyIterator(typing.Generic[_T], LazyIterator[_T]): __slots__ = ("_buffer",) def __init__(self) -> None: - # Start with an empty generator to force the paginator to get the next item. self._buffer = (_ for _ in more_collections.EMPTY_COLLECTION) @abc.abstractmethod async def _next_chunk(self) -> typing.Optional[typing.Generator[typing.Any, None, _T]]: - # Return `None` when exhausted. ... async def __anext__(self) -> _T: @@ -235,14 +235,14 @@ async def __anext__(self) -> _T: return next(self._buffer) -class MessageIterator(BufferedLazyIterator[messages.Message]): +class MessageIterator(_BufferedLazyIterator[messages.Message]): """Implementation of an iterator for message history.""" __slots__ = ("_app", "_request_call", "_direction", "_first_id", "_route") def __init__( self, - app: base_app.IBaseApp, + app: rest_app.IRESTApp, request_call: typing.Callable[..., more_typing.Coroutine[more_typing.JSONArray]], channel_id: str, direction: str, @@ -267,14 +267,14 @@ async def _next_chunk(self) -> typing.Optional[typing.Generator[messages.Message return (self._app.entity_factory.deserialize_message(m) for m in chunk) -class ReactorIterator(BufferedLazyIterator[users.User]): +class ReactorIterator(_BufferedLazyIterator[users.User]): """Implementation of an iterator for message reactions.""" __slots__ = ("_app", "_first_id", "_route", "_request_call") def __init__( self, - app: base_app.IBaseApp, + app: rest_app.IRESTApp, request_call: typing.Callable[..., more_typing.Coroutine[more_typing.JSONArray]], channel_id: str, message_id: str, @@ -296,12 +296,14 @@ async def _next_chunk(self) -> typing.Optional[typing.Generator[users.User, typi return (self._app.entity_factory.deserialize_user(u) for u in chunk) -class OwnGuildIterator(BufferedLazyIterator[applications.OwnGuild]): +class OwnGuildIterator(_BufferedLazyIterator[applications.OwnGuild]): + """Implementation of an iterator for retrieving guilds you are in.""" + __slots__ = ("_app", "_request_call", "_route", "_newest_first", "_first_id") def __init__( self, - app: base_app.IBaseApp, + app: rest_app.IRESTApp, request_call: typing.Callable[..., more_typing.Coroutine[more_typing.JSONArray]], newest_first: bool, first_id: str, @@ -325,5 +327,62 @@ async def _next_chunk(self) -> typing.Optional[typing.Generator[applications.Own return (self._app.entity_factory.deserialize_own_guild(g) for g in chunk) -class MemberIterator(BufferedLazyIterator[guilds.GuildMember]): - __slots__ = () +class MemberIterator(_BufferedLazyIterator[guilds.GuildMember]): + """Implementation of an iterator for retrieving members in a guild.""" + + __slots__ = ("_app", "_request_call", "_route", "_first_id") + + def __init__( + self, + app: rest_app.IRESTApp, + request_call: typing.Callable[..., more_typing.Coroutine[more_typing.JSONArray]], + guild_id: str, + ) -> None: + super().__init__() + self._route = routes.GET_GUILD_MEMBERS.compile(guild=guild_id) + self._request_call = request_call + self._app = app + self._first_id = bases.Snowflake.min() + + async def _next_chunk(self) -> typing.Optional[typing.Generator[guilds.GuildMember, typing.Any, None]]: + chunk = await self._request_call(self._route, query={"after": self._first_id}) + + if not chunk: + return None + + self._first_id = chunk[-1]["id"] + + return (self._app.entity_factory.deserialize_guild_member(m) for m in chunk) + + +class AuditLogIterator(LazyIterator[audit_logs.AuditLog]): + """Iterator implementation for an audit log.""" + + def __init__( + self, + app: rest_app.IRESTApp, + request_call: typing.Callable[..., more_typing.Coroutine[more_typing.JSONObject]], + guild_id: str, + before: str, + user_id: typing.Optional[str, unset.Unset], + action_type: typing.Union[int, unset.Unset], + ) -> None: + self._action_type = action_type + self._app = app + self._first_id = before + self._request_call = request_call + self._route = routes.GET_GUILD_AUDIT_LOGS.compile(guild=guild_id) + self._user_id = user_id + + async def __anext__(self) -> audit_logs.AuditLog: + query = {"limit": 100} + conversions.put_if_specified(query, "user_id", self._user_id) + conversions.put_if_specified(query, "event_type", self._action_type) + response = await self._request_call(self._route, query=query) + + if not response["entries"]: + raise StopAsyncIteration() + + log = self._app.entity_factory.deserialize_audit_log(response) + self._first_id = str(min(log.entries.keys())) + return log diff --git a/hikari/net/rest.py b/hikari/net/rest.py index 829455f714..447f364cb0 100644 --- a/hikari/net/rest.py +++ b/hikari/net/rest.py @@ -29,9 +29,10 @@ import aiohttp -from hikari import base_app from hikari import errors from hikari import http_settings +from hikari import rest_app +from hikari.models import audit_logs from hikari.net import iterators from hikari.internal import conversions from hikari.internal import more_collections @@ -66,7 +67,7 @@ class REST(http_client.HTTPClient): def __init__( self, *, - app: base_app.IBaseApp, + app: rest_app.IRESTApp, config: http_settings.HTTPSettings, debug: bool = False, token: typing.Optional[str], @@ -95,11 +96,6 @@ def __init__( self._token = f"{token_type.title()} {token}" if token is not None else None self._url = url.format(self) - async def close(self) -> None: - """Close the REST client.""" - await super().close() - self.buckets.close() - async def _request( self, compiled_route: routes.CompiledRoute, @@ -312,6 +308,11 @@ def _build_message_creation_form( ) return form + async def close(self) -> None: + """Close the REST client.""" + await super().close() + self.buckets.close() + async def fetch_channel( self, channel: typing.Union[channels.PartialChannel, bases.Snowflake, int], /, ) -> channels.PartialChannel: @@ -455,20 +456,8 @@ async def create_invite( response = await self._request(route, body=payload, reason=reason) return self._app.entity_factory.deserialize_invite_with_metadata(response) - @typing.overload - def trigger_typing( - self, channel: typing.Union[channels.TextChannel, bases.UniqueObjectT], / - ) -> more_typing.Coroutine[None]: - ... - - @typing.overload def trigger_typing( self, channel: typing.Union[channels.TextChannel, bases.UniqueObjectT], / - ) -> typing.AsyncContextManager[None]: - ... - - def trigger_typing( - self, channel: typing.Union[channels.TextChannel, bases.UniqueObjectT] ) -> rest_utils.TypingIndicator: return rest_utils.TypingIndicator(channel, self._request) @@ -563,7 +552,6 @@ async def fetch_message( self, channel: typing.Union[channels.TextChannel, bases.UniqueObjectT], message: typing.Union[messages.Message, bases.UniqueObjectT], - /, ) -> messages.Message: route = routes.GET_CHANNEL_MESSAGE.compile( channel=conversions.cast_to_str_id(channel), message=conversions.cast_to_str_id(message), @@ -904,11 +892,11 @@ def fetch_my_guilds( return iterators.OwnGuildIterator(self._app, self._request, newest_first, conversions.cast_to_str_id(start_at)) - async def leave_guild(self, guild: typing.Union[guilds.Guild, bases.UniqueObjectT]) -> None: + async def leave_guild(self, guild: typing.Union[guilds.Guild, bases.UniqueObjectT], /) -> None: route = routes.DELETE_MY_GUILD.compile(guild=conversions.cast_to_str_id(guild)) await self._request(route) - async def create_dm_channel(self, user: typing.Union[users.User, bases.UniqueObjectT]) -> channels.DMChannel: + async def create_dm_channel(self, user: typing.Union[users.User, bases.UniqueObjectT], /) -> channels.DMChannel: route = routes.POST_MY_CHANNELS.compile() response = await self._request(route, body={"recipient_id": conversions.cast_to_str_id(user)}) return self._app.entity_factory.deserialize_dm_channel(response) @@ -957,3 +945,24 @@ async def fetch_user(self, user: typing.Union[users.User, bases.UniqueObjectT]) route = routes.GET_USER.compile(user=conversions.cast_to_str_id(user)) response = await self._request(route) return self._app.entity_factory.deserialize_user(response) + + def fetch_guild_audit_log( + self, + guild: typing.Union[guilds.Guild, bases.UniqueObjectT], + *, + before: typing.Union[unset.Unset, datetime.datetime, bases.UniqueObjectT] = unset.UNSET, + user: typing.Union[unset.Unset, users.User, bases.UniqueObjectT] = unset.UNSET, + event_type: typing.Union[unset.Unset, audit_logs.AuditLogEventType] = unset.UNSET, + ) -> iterators.LazyIterator[audit_logs.AuditLog]: + guild = conversions.cast_to_str_id(guild) + user = unset.UNSET if unset.is_unset(user) else conversions.cast_to_str_id(user) + event_type = unset.UNSET if unset.is_unset(event_type) else int(event_type) + + if unset.is_unset(before): + before = bases.Snowflake.max() + elif isinstance(before, datetime.datetime): + before = bases.Snowflake.from_datetime(before) + + before = conversions.cast_to_str_id(before) + + return iterators.AuditLogIterator(self._app, self._request, guild, before, user, event_type,) diff --git a/tests/hikari/models/test_audit_logs.py b/tests/hikari/models/test_audit_logs.py index e2d8c02a95..defdbdf6bb 100644 --- a/tests/hikari/models/test_audit_logs.py +++ b/tests/hikari/models/test_audit_logs.py @@ -308,7 +308,7 @@ def test_audit_log_option_payload(): @pytest.fixture() def test_audit_log_entry_payload(test_audit_log_change_payload, test_audit_log_option_payload): return { - "action_type": 14, + "event_type": 14, "changes": [test_audit_log_change_payload], "id": "694026906592477214", "options": test_audit_log_option_payload, @@ -352,7 +352,7 @@ def test_deserialize_with_known_type_without_options_or_target_( def test_deserialize_with_options_and_target_id_and_unknown_type( self, test_audit_log_entry_payload, test_audit_log_option_payload, test_audit_log_change_payload, mock_app, ): - test_audit_log_entry_payload["action_type"] = 123123123 + test_audit_log_entry_payload["event_type"] = 123123123 audit_log_entry_obj = audit_logs.AuditLogEntry.deserialize(test_audit_log_entry_payload, app=mock_app) assert audit_log_entry_obj.changes == [audit_logs.AuditLogChange.deserialize(test_audit_log_change_payload)] assert audit_log_entry_obj.options == audit_logs.UnrecognisedAuditLogEntryInfo.deserialize( @@ -369,7 +369,7 @@ def test_deserialize_without_options_or_target_id_and_unknown_type( ): del test_audit_log_entry_payload["options"] del test_audit_log_entry_payload["target_id"] - test_audit_log_entry_payload["action_type"] = 123123123 + test_audit_log_entry_payload["event_type"] = 123123123 audit_log_entry_obj = audit_logs.AuditLogEntry.deserialize(test_audit_log_entry_payload, app=mock_app) assert audit_log_entry_obj.changes == [audit_logs.AuditLogChange.deserialize(test_audit_log_change_payload)] assert audit_log_entry_obj.options is None From 4c4a7b0eb987cdaedea2f9e4d4f9f5d2df0680a9 Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Sun, 24 May 2020 10:50:35 +0100 Subject: [PATCH 387/922] Removed some dead conversions and typehints, added casts for emoji mappings. --- hikari/.rest/session.py | 2 +- hikari/events/channel.py | 6 +- hikari/events/guild.py | 5 +- hikari/events/message.py | 7 +- hikari/internal/conversions.py | 94 ++++++-------- hikari/internal/marshaller.py | 6 +- hikari/internal/more_asyncio.py | 43 +------ hikari/internal/more_typing.py | 35 ++--- hikari/models/channels.py | 14 +- hikari/models/embeds.py | 2 +- hikari/models/guilds.py | 30 ++++- hikari/models/invites.py | 4 +- hikari/models/messages.py | 4 +- hikari/net/rest.py | 141 ++++++++++++--------- hikari/net/rest_utils.py | 2 +- tests/hikari/internal/test_conversions.py | 12 +- tests/hikari/internal/test_more_asyncio.py | 15 --- 17 files changed, 211 insertions(+), 211 deletions(-) diff --git a/hikari/.rest/session.py b/hikari/.rest/session.py index 83f6b97b97..79fd692a60 100644 --- a/hikari/.rest/session.py +++ b/hikari/.rest/session.py @@ -307,7 +307,7 @@ async def _handle_rate_limits_for_response(self, compiled_route, response): bucket = resp_headers.get("x-ratelimit-bucket", "None") reset = float(resp_headers.get("x-ratelimit-reset", "0")) reset_date = datetime.datetime.fromtimestamp(reset, tz=datetime.timezone.utc) - now_date = conversions.parse_http_date(resp_headers["date"]) + now_date = conversions.rfc7231_datetime_string_to_datetime(resp_headers["date"]) self.bucket_ratelimiters.update_rate_limits( compiled_route=compiled_route, bucket_header=bucket, diff --git a/hikari/events/channel.py b/hikari/events/channel.py index 3b71bf8b79..f7dfd22879 100644 --- a/hikari/events/channel.py +++ b/hikari/events/channel.py @@ -155,7 +155,7 @@ class BaseChannelEvent(base_events.HikariEvent, base_models.Unique, marshaller.D """The ID of this channels's parent category within guild, if set.""" last_pin_timestamp: typing.Optional[datetime.datetime] = marshaller.attrib( - deserializer=conversions.parse_iso_8601_ts, if_undefined=None, default=None + deserializer=conversions.iso8601_datetime_string_to_datetime, if_undefined=None, default=None ) """The datetime of when the last message was pinned in this channel.""" @@ -207,7 +207,7 @@ class ChannelPinsUpdateEvent(base_events.HikariEvent, marshaller.Deserializable) """The ID of the channel where the message was pinned or unpinned.""" last_pin_timestamp: typing.Optional[datetime.datetime] = marshaller.attrib( - deserializer=conversions.parse_iso_8601_ts, if_undefined=None, default=None, repr=True + deserializer=conversions.iso8601_datetime_string_to_datetime, if_undefined=None, default=None, repr=True ) """The datetime of when the most recent message was pinned in this channel. @@ -290,7 +290,7 @@ class InviteCreateEvent(base_events.HikariEvent, marshaller.Deserializable): code: str = marshaller.attrib(deserializer=str, repr=True) """The code that identifies this invite.""" - created_at: datetime.datetime = marshaller.attrib(deserializer=conversions.parse_iso_8601_ts) + created_at: datetime.datetime = marshaller.attrib(deserializer=conversions.iso8601_datetime_string_to_datetime) """The datetime of when this invite was created.""" guild_id: typing.Optional[base_models.Snowflake] = marshaller.attrib( diff --git a/hikari/events/guild.py b/hikari/events/guild.py index d62bac166b..83224151e5 100644 --- a/hikari/events/guild.py +++ b/hikari/events/guild.py @@ -205,7 +205,10 @@ class GuildMemberUpdateEvent(base_events.HikariEvent, marshaller.Deserializable) """ premium_since: typing.Union[None, datetime.datetime, unset.Unset] = marshaller.attrib( - deserializer=conversions.parse_iso_8601_ts, if_none=None, if_undefined=unset.Unset, default=unset.UNSET + deserializer=conversions.iso8601_datetime_string_to_datetime, + if_none=None, + if_undefined=unset.Unset, + default=unset.UNSET, ) """The datetime of when this member started "boosting" this guild. diff --git a/hikari/events/message.py b/hikari/events/message.py index 38883a03d0..df6355d5f8 100644 --- a/hikari/events/message.py +++ b/hikari/events/message.py @@ -123,12 +123,15 @@ class MessageUpdateEvent(base_events.HikariEvent, base_models.Unique, marshaller """The content of the message.""" timestamp: typing.Union[datetime.datetime, unset.Unset] = marshaller.attrib( - deserializer=conversions.parse_iso_8601_ts, if_undefined=unset.Unset, default=unset.UNSET + deserializer=conversions.iso8601_datetime_string_to_datetime, if_undefined=unset.Unset, default=unset.UNSET ) """The timestamp that the message was sent at.""" edited_timestamp: typing.Union[datetime.datetime, unset.Unset, None] = marshaller.attrib( - deserializer=conversions.parse_iso_8601_ts, if_none=None, if_undefined=unset.Unset, default=unset.UNSET + deserializer=conversions.iso8601_datetime_string_to_datetime, + if_none=None, + if_undefined=unset.Unset, + default=unset.UNSET, ) """The timestamp that the message was last edited at. diff --git a/hikari/internal/conversions.py b/hikari/internal/conversions.py index e7963aa4c8..d25a18f7c7 100644 --- a/hikari/internal/conversions.py +++ b/hikari/internal/conversions.py @@ -21,22 +21,22 @@ from __future__ import annotations __all__ = [ - "nullable_cast", "try_cast", "try_cast_or_defer_unary_operator", "put_if_specified", - "parse_http_date", - "parse_iso_8601_ts", + "rfc7231_datetime_string_to_datetime", + "iso8601_datetime_string_to_datetime", "discord_epoch_to_datetime", "unix_epoch_to_datetime", "pluralize", "resolve_signature", "EMPTY", - "cast_to_str_id", - "timespan_as_int", + "value_to_snowflake", + "json_to_snowflake_map", + "json_to_sequence", + "timespan_to_int", ] -import contextlib import datetime import email.utils import inspect @@ -46,15 +46,14 @@ from hikari.models import unset if typing.TYPE_CHECKING: - import enum + from hikari.internal import more_typing + from hikari.models import bases + + _T = typing.TypeVar("_T") + _T_co = typing.TypeVar("_T_co", covariant=True) + _T_contra = typing.TypeVar("_T_contra", contravariant=True) + _Unique_contra = typing.TypeVar("_Unique_contra", bound=bases.Unique, contravariant=True) - IntFlagT = typing.TypeVar("IntFlagT", bound=enum.IntFlag) - RawIntFlagValueT = typing.Union[typing.AnyStr, typing.SupportsInt, int] - CastInputT = typing.TypeVar("CastInputT") - CastOutputT = typing.TypeVar("CastOutputT") - DefaultT = typing.TypeVar("DefaultT") - TypeCastT = typing.Callable[[CastInputT], CastOutputT] - ResultT = typing.Union[CastOutputT, DefaultT] DISCORD_EPOCH: typing.Final[int] = 1_420_070_400 ISO_8601_DATE_PART: typing.Final[typing.Pattern] = re.compile(r"^(\d{4})-(\d{2})-(\d{2})") @@ -62,44 +61,21 @@ ISO_8601_TZ_PART: typing.Final[typing.Pattern] = re.compile(r"([+-])(\d{2}):(\d{2})$") -def nullable_cast(value: CastInputT, cast: TypeCastT, /) -> ResultT: - """Attempt to cast the given `value` with the given `cast`. - - This will only succeed if `value` is not `None`. If it is `None`, then - `None` is returned instead. - """ - if value is None: - return None - return cast(value) - - -def try_cast(value: CastInputT, cast: TypeCastT, default: DefaultT = None, /) -> ResultT: - """Try to cast the given value to the given cast. +#: TODO: remove +def try_cast(value, cast, default, /): + return NotImplemented - If it throws a `Exception` or derivative, it will return `default` - instead of the cast value instead. - """ - with contextlib.suppress(Exception): - return cast(value) - return default - -def try_cast_or_defer_unary_operator(type_: typing.Type, /): - """Return a unary operator that will try to cast the given input to the type provided. - - Parameters - ---------- - type_ : typing.Callable[..., `output type`] - The type to cast to. - """ - return lambda data: try_cast(data, type_, data) +#: TODO: remove +def try_cast_or_defer_unary_operator(type_, /): + return NotImplemented def put_if_specified( mapping: typing.Dict[typing.Hashable, typing.Any], key: typing.Hashable, value: typing.Any, - type_after: typing.Optional[TypeCastT] = None, + cast: typing.Optional[typing.Callable[[_T], _T_co]] = None, /, ) -> None: """Add a value to the mapping under the given key as long as the value is not `...`. @@ -112,17 +88,18 @@ def put_if_specified( The key to add the value under. value : typing.Any The value to add. - type_after : typing.Callable[[`input type`], `output type`] | None - Type to apply to the value when added. + cast : typing.Callable[[`input type`], `output type`] | None + Optional cast to apply to the value when before inserting it into the + mapping. """ if value is not unset.UNSET: - if type_after: - mapping[key] = type_after(value) + if cast: + mapping[key] = cast(value) else: mapping[key] = value -def parse_http_date(date_str: str, /) -> datetime.datetime: +def rfc7231_datetime_string_to_datetime(date_str: str, /) -> datetime.datetime: """Return the HTTP date as a datetime object. Parameters @@ -144,7 +121,7 @@ def parse_http_date(date_str: str, /) -> datetime.datetime: return email.utils.parsedate_to_datetime(date_str).replace(tzinfo=datetime.timezone.utc) -def parse_iso_8601_ts(date_string: str, /) -> datetime.datetime: +def iso8601_datetime_string_to_datetime(date_string: str, /) -> datetime.datetime: """Parse an ISO 8601 date string into a `datetime.datetime` object. Parameters @@ -271,7 +248,7 @@ def resolve_signature(func: typing.Callable) -> inspect.Signature: return signature -def cast_to_str_id(value: typing.Union[typing.SupportsInt, int]) -> str: +def value_to_snowflake(value: typing.Union[typing.SupportsInt, int]) -> str: """Cast the given object to an int and return the result as a string. Parameters @@ -287,7 +264,20 @@ def cast_to_str_id(value: typing.Union[typing.SupportsInt, int]) -> str: return str(int(value)) -def timespan_as_int(value: typing.Union[int, datetime.timedelta, float]) -> int: +def json_to_snowflake_map( + payload: more_typing.JSONArray, cast: typing.Callable[[more_typing.JSONType], _Unique_contra] +) -> typing.Mapping[bases.Snowflake, _Unique_contra]: + items = (cast(obj) for obj in payload) + return {item.id: item for item in items} + + +def json_to_sequence( + payload: more_typing.JSONArray, cast: typing.Callable[[more_typing.JSONType], _T_contra] +) -> typing.Sequence[_T_contra]: + return [cast(obj) for obj in payload] + + +def timespan_to_int(value: typing.Union[more_typing.TimeSpanT]) -> int: """Cast the given timespan in seconds to an integer value. Parameters diff --git a/hikari/internal/marshaller.py b/hikari/internal/marshaller.py index 7b02a60d8d..fff35773fb 100644 --- a/hikari/internal/marshaller.py +++ b/hikari/internal/marshaller.py @@ -478,8 +478,8 @@ class Deserializable: @classmethod def deserialize( - cls: typing.Type[more_typing.T_contra], payload: more_typing.JSONType, **kwargs - ) -> more_typing.T_contra: + cls: typing.Type[more_typing._T_contra], payload: more_typing.JSONType, **kwargs + ) -> more_typing._T_contra: """Deserialize the given payload into the object. Parameters @@ -495,6 +495,6 @@ class Serializable: __slots__ = () - def serialize(self: more_typing.T_contra) -> more_typing.JSONType: + def serialize(self: more_typing._T_contra) -> more_typing.JSONType: """Serialize this instance into a naive value.""" return HIKARI_ENTITY_MARSHALLER.serialize(self) diff --git a/hikari/internal/more_asyncio.py b/hikari/internal/more_asyncio.py index e71ae3497f..13335f119f 100644 --- a/hikari/internal/more_asyncio.py +++ b/hikari/internal/more_asyncio.py @@ -27,20 +27,12 @@ import typing if typing.TYPE_CHECKING: - from . import more_typing + from hikari.internal import more_typing + _T_contra = typing.TypeVar("_T_contra", contravariant=True) -@typing.overload -def completed_future() -> more_typing.Future[None]: - """Return a completed future with no result.""" - -@typing.overload -def completed_future(result: more_typing.T_contra, /) -> more_typing.Future[more_typing.T_contra]: - """Return a completed future with the given value as the result.""" - - -def completed_future(result=None, /): +def completed_future(result: _T_contra = None, /) -> more_typing.Future[_T_contra]: """Create a future on the current running loop that is completed, then return it. Parameters @@ -58,28 +50,6 @@ def completed_future(result=None, /): return future -def wait( - aws: typing.Iterable[typing.Union[more_typing.Coroutine[more_typing.T_co], typing.Awaitable[more_typing.T_co]]], - *, - timeout=None, - return_when=asyncio.ALL_COMPLETED, -) -> more_typing.Coroutine[ - typing.Tuple[typing.Set[more_typing.Future[more_typing.T_co]], typing.Set[more_typing.Future[more_typing.T_co]]] -]: - """Run awaitable objects in the aws set concurrently. - - This blocks until the condition specified by `return_value`. - - Returns - ------- - typing.Tuple with two typing.Set of futures - The coroutine returned by `asyncio.wait` of two sets of - Tasks/Futures (done, pending). - """ - # noinspection PyTypeChecker - return asyncio.wait([asyncio.ensure_future(f) for f in aws], timeout=timeout, return_when=return_when) - - # On Python3.8.2, there appears to be a bug with the typing module: # >>> class Aiterable: @@ -99,12 +69,9 @@ def wait( def is_async_iterator(obj: typing.Any) -> bool: """Determine if the object is an async iterator or not.""" - return hasattr(obj, "__anext__") and asyncio.iscoroutinefunction(obj.__anext__) + return asyncio.iscoroutinefunction(getattr(obj, "__anext__", None)) def is_async_iterable(obj: typing.Any) -> bool: """Determine if the object is an async iterable or not.""" - if not hasattr(obj, "__aiter__"): - return False - # These could be async generators, or they could be something different. - return inspect.isfunction(obj.__aiter__) or inspect.ismethod(obj.__aiter__) + return inspect.isfunction(obj.__aiter__) or inspect.ismethod(getattr(obj, "__aiter__", None)) diff --git a/hikari/internal/more_typing.py b/hikari/internal/more_typing.py index 5fbdd0b69d..e727214934 100644 --- a/hikari/internal/more_typing.py +++ b/hikari/internal/more_typing.py @@ -47,12 +47,13 @@ from typing import Mapping as _Mapping from typing import Optional as _Optional from typing import Protocol as _Protocol -from typing import runtime_checkable as runtime_checkable +from typing import runtime_checkable from typing import Sequence as _Sequence from typing import TYPE_CHECKING as _TYPE_CHECKING from typing import TypeVar as _TypeVar from typing import Union as _Union + if _TYPE_CHECKING: import asyncio import contextvars @@ -63,9 +64,9 @@ # pylint: enable=ungrouped-imports -T_contra = _TypeVar("T_contra", contravariant=True) +_T_contra = _TypeVar("_T_contra", contravariant=True) # noinspection PyShadowingBuiltins -T_co = _TypeVar("T_co", covariant=True) +_T_co = _TypeVar("_T_co", covariant=True) ########################## # HTTP TYPE HINT HELPERS # @@ -92,13 +93,13 @@ """A sequence produced from a JSON array that may or may not be present.""" Headers = _Mapping[str, _Union[_Sequence[str], str]] -"""HTTP headers.""" +"""HTTP headers with case insensitive strings.""" ############################# # ASYNCIO TYPE HINT HELPERS # ############################# -Coroutine = _Coroutine[_Any, _Any, T_co] +Coroutine = _Coroutine[_Any, _Any, _T_co] """A coroutine object. This is awaitable but MUST be awaited somewhere to be completed correctly. @@ -106,16 +107,16 @@ @runtime_checkable -class Future(_Protocol[T_contra]): +class Future(_Protocol[_T_contra]): """Typed protocol representation of an `asyncio.Future`. You should consult the documentation for `asyncio.Future` for usage. """ - def result(self) -> T_contra: + def result(self) -> _T_contra: """See `asyncio.Future.result`.""" - def set_result(self, result: T_contra, /) -> None: + def set_result(self, result: _T_contra, /) -> None: """See `asyncio.Future.set_result`.""" def set_exception(self, exception: Exception, /) -> None: @@ -128,11 +129,11 @@ def cancelled(self) -> bool: """See `asyncio.Future.cancelled`.""" def add_done_callback( - self, callback: _Callable[[Future[T_contra]], None], /, *, context: _Optional[contextvars.Context], + self, callback: _Callable[[Future[_T_contra]], None], /, *, context: _Optional[contextvars.Context], ) -> None: """See `asyncio.Future.add_done_callback`.""" - def remove_done_callback(self, callback: _Callable[[Future[T_contra]], None], /) -> None: + def remove_done_callback(self, callback: _Callable[[Future[_T_contra]], None], /) -> None: """See `asyncio.Future.remove_done_callback`.""" def cancel(self) -> bool: @@ -144,21 +145,21 @@ def exception(self) -> _Optional[Exception]: def get_loop(self) -> asyncio.AbstractEventLoop: """See `asyncio.Future.get_loop`.""" - def __await__(self) -> Coroutine[T_contra]: + def __await__(self) -> _Generator[_T_contra, None, _Any]: ... @runtime_checkable -class Task(_Protocol[T_contra]): +class Task(_Protocol[_T_contra]): """Typed protocol representation of an `asyncio.Task`. You should consult the documentation for `asyncio.Task` for usage. """ - def result(self) -> T_contra: + def result(self) -> _T_contra: """See`asyncio.Future.result`.""" - def set_result(self, result: T_contra, /) -> None: + def set_result(self, result: _T_contra, /) -> None: """See `asyncio.Future.set_result`.""" def set_exception(self, exception: Exception, /) -> None: @@ -171,11 +172,11 @@ def cancelled(self) -> bool: """See `asyncio.Future.cancelled`.""" def add_done_callback( - self, callback: _Callable[[Future[T_contra]], None], /, *, context: _Optional[contextvars.Context], + self, callback: _Callable[[Future[_T_contra]], None], /, *, context: _Optional[contextvars.Context], ) -> None: """See `asyncio.Future.add_done_callback`.""" - def remove_done_callback(self, callback: _Callable[[Future[T_contra]], None], /) -> None: + def remove_done_callback(self, callback: _Callable[[Future[_T_contra]], None], /) -> None: """See `asyncio.Future.remove_done_callback`.""" def cancel(self) -> bool: @@ -199,7 +200,7 @@ def get_name(self) -> str: def set_name(self, value: str, /) -> None: """See `asyncio.Task.set_name`.""" - def __await__(self) -> _Generator[T_contra, _Any, None]: + def __await__(self) -> _Generator[_T_contra, None, _Any]: ... diff --git a/hikari/models/channels.py b/hikari/models/channels.py index e2d527bb38..b1f97692e5 100644 --- a/hikari/models/channels.py +++ b/hikari/models/channels.py @@ -331,7 +331,12 @@ class GuildTextChannel(GuildChannel, TextChannel): """ last_pin_timestamp: typing.Optional[datetime.datetime] = marshaller.attrib( - deserializer=conversions.parse_iso_8601_ts, if_none=None, if_undefined=None, default=None, eq=False, hash=False + deserializer=conversions.iso8601_datetime_string_to_datetime, + if_none=None, + if_undefined=None, + default=None, + eq=False, + hash=False, ) """The timestamp of the last-pinned message. @@ -359,7 +364,12 @@ class GuildNewsChannel(GuildChannel, TextChannel): """ last_pin_timestamp: typing.Optional[datetime.datetime] = marshaller.attrib( - deserializer=conversions.parse_iso_8601_ts, if_none=None, if_undefined=None, default=None, eq=False, hash=False + deserializer=conversions.iso8601_datetime_string_to_datetime, + if_none=None, + if_undefined=None, + default=None, + eq=False, + hash=False, ) """The timestamp of the last-pinned message. diff --git a/hikari/models/embeds.py b/hikari/models/embeds.py index ff7826a4e6..e1c9d0bf18 100644 --- a/hikari/models/embeds.py +++ b/hikari/models/embeds.py @@ -283,7 +283,7 @@ def _description_check(self, _, value): # pylint:disable=unused-argument """The URL of the embed.""" timestamp: typing.Optional[datetime.datetime] = marshaller.attrib( - deserializer=conversions.parse_iso_8601_ts, + deserializer=conversions.iso8601_datetime_string_to_datetime, serializer=_serialize_timestamp, if_undefined=None, default=None, diff --git a/hikari/models/guilds.py b/hikari/models/guilds.py index a800388523..fc31aa4256 100644 --- a/hikari/models/guilds.py +++ b/hikari/models/guilds.py @@ -265,11 +265,18 @@ class GuildMember(bases.Entity, marshaller.Deserializable): ) """A sequence of the IDs of the member's current roles.""" - joined_at: datetime.datetime = marshaller.attrib(deserializer=conversions.parse_iso_8601_ts, eq=False, hash=False) + joined_at: datetime.datetime = marshaller.attrib( + deserializer=conversions.iso8601_datetime_string_to_datetime, eq=False, hash=False + ) """The datetime of when this member joined the guild they belong to.""" premium_since: typing.Optional[datetime.datetime] = marshaller.attrib( - deserializer=conversions.parse_iso_8601_ts, if_none=None, if_undefined=None, default=None, eq=False, hash=False + deserializer=conversions.iso8601_datetime_string_to_datetime, + if_none=None, + if_undefined=None, + default=None, + eq=False, + hash=False, ) """The datetime of when this member started "boosting" this guild. @@ -748,7 +755,12 @@ class GuildMemberPresence(bases.Entity, marshaller.Deserializable): """An object of the target user's client statuses.""" premium_since: typing.Optional[datetime.datetime] = marshaller.attrib( - deserializer=conversions.parse_iso_8601_ts, if_undefined=None, if_none=None, default=None, eq=False, hash=False, + deserializer=conversions.iso8601_datetime_string_to_datetime, + if_undefined=None, + if_none=None, + default=None, + eq=False, + hash=False, ) """The datetime of when this member started "boosting" this guild. @@ -851,7 +863,11 @@ class GuildIntegration(bases.Unique, marshaller.Deserializable): """The user this integration belongs to.""" last_synced_at: datetime.datetime = marshaller.attrib( - raw_name="synced_at", deserializer=conversions.parse_iso_8601_ts, if_none=None, eq=False, hash=False + raw_name="synced_at", + deserializer=conversions.iso8601_datetime_string_to_datetime, + if_none=None, + eq=False, + hash=False, ) """The datetime of when this integration's subscribers were last synced.""" @@ -1245,7 +1261,11 @@ class Guild(PartialGuild): # pylint:disable=too-many-instance-attributes """ joined_at: typing.Optional[datetime.datetime] = marshaller.attrib( - deserializer=conversions.parse_iso_8601_ts, if_undefined=None, default=None, eq=False, hash=False + deserializer=conversions.iso8601_datetime_string_to_datetime, + if_undefined=None, + default=None, + eq=False, + hash=False, ) """The date and time that the bot user joined this guild. diff --git a/hikari/models/invites.py b/hikari/models/invites.py index 906751bd9b..287f9ae6f3 100644 --- a/hikari/models/invites.py +++ b/hikari/models/invites.py @@ -254,7 +254,9 @@ class InviteWithMetadata(Invite): is_temporary: bool = marshaller.attrib(raw_name="temporary", deserializer=bool, eq=False, hash=False, repr=True) """Whether this invite grants temporary membership.""" - created_at: datetime.datetime = marshaller.attrib(deserializer=conversions.parse_iso_8601_ts, eq=False, hash=False) + created_at: datetime.datetime = marshaller.attrib( + deserializer=conversions.iso8601_datetime_string_to_datetime, eq=False, hash=False + ) """When this invite was created.""" @property diff --git a/hikari/models/messages.py b/hikari/models/messages.py index 82bea823f3..809474cb57 100644 --- a/hikari/models/messages.py +++ b/hikari/models/messages.py @@ -285,12 +285,12 @@ class Message(bases.Unique, marshaller.Deserializable): """The content of the message.""" timestamp: datetime.datetime = marshaller.attrib( - deserializer=conversions.parse_iso_8601_ts, eq=False, hash=False, repr=True + deserializer=conversions.iso8601_datetime_string_to_datetime, eq=False, hash=False, repr=True ) """The timestamp that the message was sent at.""" edited_timestamp: typing.Optional[datetime.datetime] = marshaller.attrib( - deserializer=conversions.parse_iso_8601_ts, if_none=None, eq=False, hash=False + deserializer=conversions.iso8601_datetime_string_to_datetime, if_none=None, eq=False, hash=False ) """The timestamp that the message was last edited at. diff --git a/hikari/net/rest.py b/hikari/net/rest.py index 447f364cb0..a645decbc1 100644 --- a/hikari/net/rest.py +++ b/hikari/net/rest.py @@ -211,7 +211,7 @@ async def _handle_rate_limits_for_response( bucket = resp_headers.get("x-ratelimit-bucket", "None") reset = float(resp_headers.get("x-ratelimit-reset", "0")) reset_date = datetime.datetime.fromtimestamp(reset, tz=datetime.timezone.utc) - now_date = conversions.parse_http_date(resp_headers["date"]) + now_date = conversions.rfc7231_datetime_string_to_datetime(resp_headers["date"]) self.buckets.update_rate_limits( compiled_route=compiled_route, bucket_header=bucket, @@ -316,7 +316,7 @@ async def close(self) -> None: async def fetch_channel( self, channel: typing.Union[channels.PartialChannel, bases.Snowflake, int], /, ) -> channels.PartialChannel: - response = await self._request(routes.GET_CHANNEL.compile(channel=conversions.cast_to_str_id(channel))) + response = await self._request(routes.GET_CHANNEL.compile(channel=conversions.value_to_snowflake(channel))) return self._app.entity_factory.deserialize_channel(response) async def edit_channel( @@ -343,7 +343,7 @@ async def edit_channel( conversions.put_if_specified(payload, "bitrate", bitrate) conversions.put_if_specified(payload, "user_limit", user_limit) conversions.put_if_specified(payload, "rate_limit_per_user", rate_limit_per_user) - conversions.put_if_specified(payload, "parent_id", parent_category, conversions.cast_to_str_id) + conversions.put_if_specified(payload, "parent_id", parent_category, conversions.value_to_snowflake) if not unset.is_unset(permission_overwrites): payload["permission_overwrites"] = [ @@ -351,13 +351,13 @@ async def edit_channel( ] response = await self._request( - routes.PATCH_CHANNEL.compile(channel=conversions.cast_to_str_id(channel)), body=payload, reason=reason, + routes.PATCH_CHANNEL.compile(channel=conversions.value_to_snowflake(channel)), body=payload, reason=reason, ) return self._app.entity_factory.deserialize_channel(response) async def delete_channel(self, channel: typing.Union[channels.PartialChannel, bases.Snowflake, int], /) -> None: - await self._request(routes.DELETE_CHANNEL.compile(channel=conversions.cast_to_str_id(channel))) + await self._request(routes.DELETE_CHANNEL.compile(channel=conversions.value_to_snowflake(channel))) @typing.overload async def edit_channel_permissions( @@ -410,7 +410,7 @@ async def edit_channel_permissions( conversions.put_if_specified(payload, "allow", allow) conversions.put_if_specified(payload, "deny", deny) route = routes.PATCH_CHANNEL_PERMISSIONS.compile( - channel=conversions.cast_to_str_id(channel), overwrite=conversions.cast_to_str_id(target), + channel=conversions.value_to_snowflake(channel), overwrite=conversions.value_to_snowflake(target), ) await self._request(route, body=payload, reason=reason) @@ -421,16 +421,16 @@ async def delete_channel_permission( target: typing.Union[channels.PermissionOverwrite, guilds.Role, users.User, bases.UniqueObjectT], ) -> None: route = routes.DELETE_CHANNEL_PERMISSIONS.compile( - channel=conversions.cast_to_str_id(channel), overwrite=conversions.cast_to_str_id(target), + channel=conversions.value_to_snowflake(channel), overwrite=conversions.value_to_snowflake(target), ) await self._request(route) async def fetch_channel_invites( self, channel: typing.Union[channels.GuildChannel, bases.UniqueObjectT], / ) -> typing.Sequence[invites.InviteWithMetadata]: - route = routes.GET_CHANNEL_INVITES.compile(channel=conversions.cast_to_str_id(channel)) + route = routes.GET_CHANNEL_INVITES.compile(channel=conversions.value_to_snowflake(channel)) response = await self._request(route) - return [self._app.entity_factory.deserialize_invite_with_metadata(i) for i in response] + return conversions.json_to_sequence(response, self._app.entity_factory.deserialize_invite_with_metadata) async def create_invite( self, @@ -446,13 +446,13 @@ async def create_invite( reason: typing.Union[unset.Unset, str] = unset.UNSET, ) -> invites.InviteWithMetadata: payload = {} - conversions.put_if_specified(payload, "max_age", max_age, conversions.timespan_as_int) + conversions.put_if_specified(payload, "max_age", max_age, conversions.timespan_to_int) conversions.put_if_specified(payload, "max_uses", max_uses) conversions.put_if_specified(payload, "temporary", temporary) conversions.put_if_specified(payload, "unique", unique), - conversions.put_if_specified(payload, "target_user", target_user, conversions.cast_to_str_id) + conversions.put_if_specified(payload, "target_user", target_user, conversions.value_to_snowflake) conversions.put_if_specified(payload, "target_user_type", target_user_type) - route = routes.POST_CHANNEL_INVITES.compile(channel=conversions.cast_to_str_id(channel)) + route = routes.POST_CHANNEL_INVITES.compile(channel=conversions.value_to_snowflake(channel)) response = await self._request(route, body=payload, reason=reason) return self._app.entity_factory.deserialize_invite_with_metadata(response) @@ -464,9 +464,9 @@ def trigger_typing( async def fetch_pins( self, channel: typing.Union[channels.TextChannel, bases.UniqueObjectT], / ) -> typing.Mapping[bases.Snowflake, messages.Message]: - route = routes.GET_CHANNEL_PINS.compile(channel=conversions.cast_to_str_id(channel)) + route = routes.GET_CHANNEL_PINS.compile(channel=conversions.value_to_snowflake(channel)) response = await self._request(route) - return {bases.Snowflake(m["id"]): self._app.entity_factory.deserialize_message(m) for m in response} + return conversions.json_to_snowflake_map(response, self._app.entity_factory.deserialize_message) async def create_pinned_message( self, @@ -474,7 +474,7 @@ async def create_pinned_message( message: typing.Union[messages.Message, bases.UniqueObjectT], ) -> None: route = routes.PUT_CHANNEL_PINS.compile( - channel=conversions.cast_to_str_id(channel), message=conversions.cast_to_str_id(message), + channel=conversions.value_to_snowflake(channel), message=conversions.value_to_snowflake(message), ) await self._request(route) @@ -484,7 +484,7 @@ async def delete_pinned_message( message: typing.Union[messages.Message, bases.UniqueObjectT], ) -> None: route = routes.DELETE_CHANNEL_PIN.compile( - channel=conversions.cast_to_str_id(channel), message=conversions.cast_to_str_id(message), + channel=conversions.value_to_snowflake(channel), message=conversions.value_to_snowflake(message), ) await self._request(route) @@ -543,9 +543,9 @@ def fetch_messages( return iterators.MessageIterator( self._app, self._request, - conversions.cast_to_str_id(channel), + conversions.value_to_snowflake(channel), direction, - conversions.cast_to_str_id(timestamp), + conversions.value_to_snowflake(timestamp), ) async def fetch_message( @@ -554,7 +554,7 @@ async def fetch_message( message: typing.Union[messages.Message, bases.UniqueObjectT], ) -> messages.Message: route = routes.GET_CHANNEL_MESSAGE.compile( - channel=conversions.cast_to_str_id(channel), message=conversions.cast_to_str_id(message), + channel=conversions.value_to_snowflake(channel), message=conversions.value_to_snowflake(message), ) response = await self._request(route) return self._app.entity_factory.deserialize_message(response) @@ -572,7 +572,7 @@ async def create_message( user_mentions: typing.Union[typing.Collection[typing.Union[users.User, bases.UniqueObjectT]], bool] = True, role_mentions: typing.Union[typing.Collection[typing.Union[bases.UniqueObjectT, guilds.Role]], bool] = True, ) -> messages.Message: - route = routes.POST_CHANNEL_MESSAGES.compile(channel=conversions.cast_to_str_id(channel)) + route = routes.POST_CHANNEL_MESSAGES.compile(channel=conversions.value_to_snowflake(channel)) payload = {"allowed_mentions": self._generate_allowed_mentions(mentions_everyone, user_mentions, role_mentions)} conversions.put_if_specified(payload, "content", text, str) @@ -604,7 +604,7 @@ async def edit_message( flags: typing.Union[unset.Unset, messages.MessageFlag] = unset.UNSET, ) -> messages.Message: route = routes.PATCH_CHANNEL_MESSAGE.compile( - channel=conversions.cast_to_str_id(channel), message=conversions.cast_to_str_id(message), + channel=conversions.value_to_snowflake(channel), message=conversions.value_to_snowflake(message), ) payload = {} conversions.put_if_specified(payload, "content", text, str) @@ -620,7 +620,7 @@ async def delete_message( message: typing.Union[messages.Message, bases.UniqueObjectT], ) -> None: route = routes.DELETE_CHANNEL_MESSAGE.compile( - channel=conversions.cast_to_str_id(channel), message=conversions.cast_to_str_id(message), + channel=conversions.value_to_snowflake(channel), message=conversions.value_to_snowflake(message), ) await self._request(route) @@ -630,8 +630,8 @@ async def delete_messages( *messages_to_delete: typing.Union[messages.Message, bases.UniqueObjectT], ) -> None: if 2 <= len(messages_to_delete) <= 100: - route = routes.POST_DELETE_CHANNEL_MESSAGES_BULK.compile(channel=conversions.cast_to_str_id(channel)) - payload = {"messages": [conversions.cast_to_str_id(m) for m in messages_to_delete]} + route = routes.POST_DELETE_CHANNEL_MESSAGES_BULK.compile(channel=conversions.value_to_snowflake(channel)) + payload = {"messages": [conversions.value_to_snowflake(m) for m in messages_to_delete]} await self._request(route, body=payload) else: raise TypeError("Must delete a minimum of 2 messages and a maximum of 100") @@ -643,8 +643,8 @@ async def create_reaction( emoji: typing.Union[str, emojis.UnicodeEmoji, emojis.KnownCustomEmoji], ) -> None: emoji = emoji.url_name if isinstance(emoji, emojis.KnownCustomEmoji) else str(emoji) - channel = conversions.cast_to_str_id(channel) - message = conversions.cast_to_str_id(message) + channel = conversions.value_to_snowflake(channel) + message = conversions.value_to_snowflake(message) route = routes.PUT_MY_REACTION.compile(channel=channel, message=message, emoji=emoji) await self._request(route) @@ -655,8 +655,8 @@ async def delete_my_reaction( emoji: typing.Union[str, emojis.UnicodeEmoji, emojis.KnownCustomEmoji], ) -> None: emoji = emoji.url_name if isinstance(emoji, emojis.KnownCustomEmoji) else str(emoji) - channel = conversions.cast_to_str_id(channel) - message = conversions.cast_to_str_id(message) + channel = conversions.value_to_snowflake(channel) + message = conversions.value_to_snowflake(message) route = routes.DELETE_MY_REACTION.compile(channel=channel, message=message, emoji=emoji) await self._request(route) @@ -667,8 +667,8 @@ async def delete_all_reactions_for_emoji( emoji: typing.Union[str, emojis.UnicodeEmoji, emojis.KnownCustomEmoji], ) -> None: emoji = emoji.url_name if isinstance(emoji, emojis.KnownCustomEmoji) else str(emoji) - channel = conversions.cast_to_str_id(channel) - message = conversions.cast_to_str_id(message) + channel = conversions.value_to_snowflake(channel) + message = conversions.value_to_snowflake(message) route = routes.DELETE_REACTION_EMOJI.compile(channel=channel, message=message, emoji=emoji) await self._request(route) @@ -680,9 +680,9 @@ async def delete_reaction( user: typing.Union[users.User, bases.UniqueObjectT], ) -> None: emoji = emoji.url_name if isinstance(emoji, emojis.KnownCustomEmoji) else str(emoji) - channel = conversions.cast_to_str_id(channel) - message = conversions.cast_to_str_id(message) - user = conversions.cast_to_str_id(user) + channel = conversions.value_to_snowflake(channel) + message = conversions.value_to_snowflake(message) + user = conversions.value_to_snowflake(user) route = routes.DELETE_REACTION_USER.compile(channel=channel, message=message, emoji=emoji, user=user) await self._request(route) @@ -691,8 +691,8 @@ async def delete_all_reactions( channel: typing.Union[channels.TextChannel, bases.UniqueObjectT], message: typing.Union[messages.Message, bases.UniqueObjectT], ) -> None: - channel = conversions.cast_to_str_id(channel) - message = conversions.cast_to_str_id(message) + channel = conversions.value_to_snowflake(channel) + message = conversions.value_to_snowflake(message) route = routes.DELETE_ALL_REACTIONS.compile(channel=channel, message=message) await self._request(route) @@ -705,8 +705,8 @@ def fetch_reactions_for_emoji( return iterators.ReactorIterator( app=self._app, request_call=self._request, - channel_id=conversions.cast_to_str_id(channel), - message_id=conversions.cast_to_str_id(message), + channel_id=conversions.value_to_snowflake(channel), + message_id=conversions.value_to_snowflake(message), emoji=emoji.url_name if isinstance(emoji, emojis.KnownCustomEmoji) else str(emoji), ) @@ -721,7 +721,7 @@ async def create_webhook( payload = {"name": name} if not unset.is_unset(avatar): payload["avatar"] = await avatar.fetch_data_uri() - route = routes.POST_WEBHOOK.compile(channel=conversions.cast_to_str_id(channel)) + route = routes.POST_WEBHOOK.compile(channel=conversions.value_to_snowflake(channel)) response = await self._request(route, body=payload, reason=reason) return self._app.entity_factory.deserialize_webhook(response) @@ -733,25 +733,25 @@ async def fetch_webhook( token: typing.Union[unset.Unset, str] = unset.UNSET, ) -> webhooks.Webhook: if unset.is_unset(token): - route = routes.GET_WEBHOOK.compile(webhook=conversions.cast_to_str_id(webhook)) + route = routes.GET_WEBHOOK.compile(webhook=conversions.value_to_snowflake(webhook)) else: - route = routes.GET_WEBHOOK_WITH_TOKEN.compile(webhook=conversions.cast_to_str_id(webhook), token=token) + route = routes.GET_WEBHOOK_WITH_TOKEN.compile(webhook=conversions.value_to_snowflake(webhook), token=token) response = await self._request(route) return self._app.entity_factory.deserialize_webhook(response) async def fetch_channel_webhooks( self, channel: typing.Union[channels.TextChannel, bases.UniqueObjectT], / ) -> typing.Mapping[bases.Snowflake, webhooks.Webhook]: - route = routes.GET_CHANNEL_WEBHOOKS.compile(channel=conversions.cast_to_str_id(channel)) + route = routes.GET_CHANNEL_WEBHOOKS.compile(channel=conversions.value_to_snowflake(channel)) response = await self._request(route) - return {bases.Snowflake(w["id"]): self._app.entity_factory.deserialize_webhook(w) for w in response} + return conversions.json_to_snowflake_map(response, self._app.entity_factory.deserialize_webhook) async def fetch_guild_webhooks( self, guild: typing.Union[guilds.Guild, bases.UniqueObjectT], / ) -> typing.Mapping[bases.Snowflake, webhooks.Webhook]: - route = routes.GET_GUILD_WEBHOOKS.compile(channel=conversions.cast_to_str_id(guild)) + route = routes.GET_GUILD_WEBHOOKS.compile(channel=conversions.value_to_snowflake(guild)) response = await self._request(route) - return {bases.Snowflake(w["id"]): self._app.entity_factory.deserialize_webhook(w) for w in response} + return conversions.json_to_snowflake_map(response, self._app.entity_factory.deserialize_webhook) async def edit_webhook( self, @@ -766,14 +766,16 @@ async def edit_webhook( ) -> webhooks.Webhook: payload = {} conversions.put_if_specified(payload, "name", name) - conversions.put_if_specified(payload, "channel", channel, conversions.cast_to_str_id) + conversions.put_if_specified(payload, "channel", channel, conversions.value_to_snowflake) if not unset.is_unset(avatar): payload["avatar"] = await avatar.fetch_data_uri() if unset.is_unset(token): - route = routes.PATCH_WEBHOOK.compile(webhook=conversions.cast_to_str_id(webhook)) + route = routes.PATCH_WEBHOOK.compile(webhook=conversions.value_to_snowflake(webhook)) else: - route = routes.PATCH_WEBHOOK_WITH_TOKEN.compile(webhook=conversions.cast_to_str_id(webhook), token=token) + route = routes.PATCH_WEBHOOK_WITH_TOKEN.compile( + webhook=conversions.value_to_snowflake(webhook), token=token + ) response = await self._request(route, body=payload, reason=reason) return self._app.entity_factory.deserialize_webhook(response) @@ -786,9 +788,11 @@ async def delete_webhook( token: typing.Union[unset.Unset, str] = unset.UNSET, ) -> None: if unset.is_unset(token): - route = routes.DELETE_WEBHOOK.compile(webhook=conversions.cast_to_str_id(webhook)) + route = routes.DELETE_WEBHOOK.compile(webhook=conversions.value_to_snowflake(webhook)) else: - route = routes.DELETE_WEBHOOK_WITH_TOKEN.compile(webhook=conversions.cast_to_str_id(webhook), token=token) + route = routes.DELETE_WEBHOOK_WITH_TOKEN.compile( + webhook=conversions.value_to_snowflake(webhook), token=token + ) await self._request(route) async def execute_embed( @@ -808,10 +812,10 @@ async def execute_embed( role_mentions: typing.Union[typing.Collection[typing.Union[bases.UniqueObjectT, guilds.Role]], bool] = True, ) -> messages.Message: if unset.is_unset(token): - route = routes.POST_WEBHOOK.compile(webhook=conversions.cast_to_str_id(webhook)) + route = routes.POST_WEBHOOK.compile(webhook=conversions.value_to_snowflake(webhook)) no_auth = False else: - route = routes.POST_WEBHOOK_WITH_TOKEN.compile(webhook=conversions.cast_to_str_id(webhook), token=token) + route = routes.POST_WEBHOOK_WITH_TOKEN.compile(webhook=conversions.value_to_snowflake(webhook), token=token) no_auth = True attachments = [] if unset.is_unset(attachments) else [a for a in attachments] @@ -890,15 +894,17 @@ def fetch_my_guilds( elif isinstance(start_at, datetime.datetime): start_at = bases.Snowflake.from_datetime(start_at) - return iterators.OwnGuildIterator(self._app, self._request, newest_first, conversions.cast_to_str_id(start_at)) + return iterators.OwnGuildIterator( + self._app, self._request, newest_first, conversions.value_to_snowflake(start_at) + ) async def leave_guild(self, guild: typing.Union[guilds.Guild, bases.UniqueObjectT], /) -> None: - route = routes.DELETE_MY_GUILD.compile(guild=conversions.cast_to_str_id(guild)) + route = routes.DELETE_MY_GUILD.compile(guild=conversions.value_to_snowflake(guild)) await self._request(route) async def create_dm_channel(self, user: typing.Union[users.User, bases.UniqueObjectT], /) -> channels.DMChannel: route = routes.POST_MY_CHANNELS.compile() - response = await self._request(route, body={"recipient_id": conversions.cast_to_str_id(user)}) + response = await self._request(route, body={"recipient_id": conversions.value_to_snowflake(user)}) return self._app.entity_factory.deserialize_dm_channel(response) async def fetch_application(self) -> applications.Application: @@ -919,12 +925,14 @@ async def add_user_to_guild( deaf: typing.Union[unset.Unset, bool] = unset.UNSET, ) -> typing.Optional[guilds.GuildMember]: route = routes.PUT_GUILD_MEMBER.compile( - guild=conversions.cast_to_str_id(guild), user=conversions.cast_to_str_id(user), + guild=conversions.value_to_snowflake(guild), user=conversions.value_to_snowflake(user), ) payload = {"access_token": access_token} conversions.put_if_specified(payload, "nick", nickname) - conversions.put_if_specified(payload, "roles", roles, lambda rs: [conversions.cast_to_str_id(r) for r in rs]) + conversions.put_if_specified( + payload, "roles", roles, lambda rs: [conversions.value_to_snowflake(r) for r in rs] + ) conversions.put_if_specified(payload, "mute", mute) conversions.put_if_specified(payload, "deaf", deaf) @@ -939,10 +947,10 @@ async def add_user_to_guild( async def fetch_voice_regions(self) -> typing.Sequence[voices.VoiceRegion]: route = routes.GET_VOICE_REGIONS.compile() response = await self._request(route) - return [self._app.entity_factory.deserialize_voice_region(r) for r in response] + return conversions.json_to_snowflake_map(response, self._app.entity_factory.deserialize_voice_region) async def fetch_user(self, user: typing.Union[users.User, bases.UniqueObjectT]) -> users.User: - route = routes.GET_USER.compile(user=conversions.cast_to_str_id(user)) + route = routes.GET_USER.compile(user=conversions.value_to_snowflake(user)) response = await self._request(route) return self._app.entity_factory.deserialize_user(response) @@ -954,8 +962,8 @@ def fetch_guild_audit_log( user: typing.Union[unset.Unset, users.User, bases.UniqueObjectT] = unset.UNSET, event_type: typing.Union[unset.Unset, audit_logs.AuditLogEventType] = unset.UNSET, ) -> iterators.LazyIterator[audit_logs.AuditLog]: - guild = conversions.cast_to_str_id(guild) - user = unset.UNSET if unset.is_unset(user) else conversions.cast_to_str_id(user) + guild = conversions.value_to_snowflake(guild) + user = unset.UNSET if unset.is_unset(user) else conversions.value_to_snowflake(user) event_type = unset.UNSET if unset.is_unset(event_type) else int(event_type) if unset.is_unset(before): @@ -963,6 +971,17 @@ def fetch_guild_audit_log( elif isinstance(before, datetime.datetime): before = bases.Snowflake.from_datetime(before) - before = conversions.cast_to_str_id(before) + before = conversions.value_to_snowflake(before) return iterators.AuditLogIterator(self._app, self._request, guild, before, user, event_type,) + + async def fetch_guild_emoji( + self, + guild: typing.Union[guilds.Guild, bases.UniqueObjectT], + emoji: typing.Union[emojis.KnownCustomEmoji, bases.UniqueObjectT], + ) -> emojis.KnownCustomEmoji: + route = routes.GET_GUILD_EMOJI.compile( + guild=conversions.value_to_snowflake(guild), emoji=conversions.value_to_snowflake(emoji), + ) + response = await self._request(route) + return self._app.entity_factory.deserialize_known_custom_emoji(response) diff --git a/hikari/net/rest_utils.py b/hikari/net/rest_utils.py index 1980a98109..7e2ea01ebd 100644 --- a/hikari/net/rest_utils.py +++ b/hikari/net/rest_utils.py @@ -54,7 +54,7 @@ def __init__( channel: typing.Union[channels.TextChannel, bases.UniqueObjectT], request_call: typing.Callable[..., more_typing.Coroutine[more_typing.JSONObject]], ) -> None: - self._channel = conversions.cast_to_str_id(channel) + self._channel = conversions.value_to_snowflake(channel) self._request_call = request_call self._task = None diff --git a/tests/hikari/internal/test_conversions.py b/tests/hikari/internal/test_conversions.py index 76f0442a57..66eb23bb23 100644 --- a/tests/hikari/internal/test_conversions.py +++ b/tests/hikari/internal/test_conversions.py @@ -100,7 +100,7 @@ def test_image_bytes_to_image_data_when_unsupported_image_type_raises_value_erro def test_parse_iso_8601_date_with_negative_timezone(): string = "2019-10-10T05:22:33.023456-02:30" - date = conversions.parse_iso_8601_ts(string) + date = conversions.iso8601_datetime_string_to_datetime(string) assert date.year == 2019 assert date.month == 10 assert date.day == 10 @@ -114,7 +114,7 @@ def test_parse_iso_8601_date_with_negative_timezone(): def test_parse_iso_8601_date_with_positive_timezone(): string = "2019-10-10T05:22:33.023456+02:30" - date = conversions.parse_iso_8601_ts(string) + date = conversions.iso8601_datetime_string_to_datetime(string) assert date.year == 2019 assert date.month == 10 assert date.day == 10 @@ -128,7 +128,7 @@ def test_parse_iso_8601_date_with_positive_timezone(): def test_parse_iso_8601_date_with_zulu(): string = "2019-10-10T05:22:33.023456Z" - date = conversions.parse_iso_8601_ts(string) + date = conversions.iso8601_datetime_string_to_datetime(string) assert date.year == 2019 assert date.month == 10 assert date.day == 10 @@ -142,7 +142,7 @@ def test_parse_iso_8601_date_with_zulu(): def test_parse_iso_8601_date_with_milliseconds_instead_of_microseconds(): string = "2019-10-10T05:22:33.023Z" - date = conversions.parse_iso_8601_ts(string) + date = conversions.iso8601_datetime_string_to_datetime(string) assert date.year == 2019 assert date.month == 10 assert date.day == 10 @@ -154,7 +154,7 @@ def test_parse_iso_8601_date_with_milliseconds_instead_of_microseconds(): def test_parse_iso_8601_date_with_no_fraction(): string = "2019-10-10T05:22:33Z" - date = conversions.parse_iso_8601_ts(string) + date = conversions.iso8601_datetime_string_to_datetime(string) assert date.year == 2019 assert date.month == 10 assert date.day == 10 @@ -167,7 +167,7 @@ def test_parse_iso_8601_date_with_no_fraction(): def test_parse_http_date(): rfc_timestamp = "Mon, 03 Jun 2019 17:54:26 GMT" expected_timestamp = datetime.datetime(2019, 6, 3, 17, 54, 26, tzinfo=datetime.timezone.utc) - assert conversions.parse_http_date(rfc_timestamp) == expected_timestamp + assert conversions.rfc7231_datetime_string_to_datetime(rfc_timestamp) == expected_timestamp def test_parse_discord_epoch_to_datetime(): diff --git a/tests/hikari/internal/test_more_asyncio.py b/tests/hikari/internal/test_more_asyncio.py index 55b0aa3c04..01c653b250 100644 --- a/tests/hikari/internal/test_more_asyncio.py +++ b/tests/hikari/internal/test_more_asyncio.py @@ -80,21 +80,6 @@ async def test_non_default_result(self): assert more_asyncio.completed_future(...).result() is ... -@pytest.mark.asyncio -async def test_wait(): - mock_futures = ([mock.MagicMock(asyncio.Future)], [mock.MagicMock(asyncio.Future)]) - mock_awaitable = _helpers.AwaitableMock() - mock_future = mock.MagicMock(asyncio.Future) - stack = contextlib.ExitStack() - stack.enter_context(mock.patch.object(asyncio, "wait", return_value=mock_futures)) - stack.enter_context(mock.patch.object(asyncio, "ensure_future", return_value=mock_future)) - with stack: - result = await more_asyncio.wait([mock_awaitable], timeout=42, return_when=asyncio.FIRST_COMPLETED) - assert result == mock_futures - asyncio.wait.assert_called_once_with([mock_future], timeout=42, return_when=asyncio.FIRST_COMPLETED) - asyncio.ensure_future.assert_called_once_with(mock_awaitable) - - class TestIsAsyncIterator: def test_on_inst(self): class AsyncIterator: From 8128d25763e4f7876c8409d5c860b004dd078350 Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Sun, 24 May 2020 19:07:25 +0100 Subject: [PATCH 388/922] Implemented some guild endpoints and a guild builder object. --- hikari/.rest/session.py | 12 +-- hikari/internal/conversions.py | 13 +-- hikari/models/audit_logs.py | 4 +- hikari/net/rest.py | 75 ++++++++++++++++- hikari/net/rest_utils.py | 146 +++++++++++++++++++++++++++++++++ hikari/net/routes.py | 4 +- 6 files changed, 236 insertions(+), 18 deletions(-) diff --git a/hikari/.rest/session.py b/hikari/.rest/session.py index 79fd692a60..665f10298f 100644 --- a/hikari/.rest/session.py +++ b/hikari/.rest/session.py @@ -1417,8 +1417,8 @@ async def create_guild( """ payload = {"name": name} conversions.put_if_specified(payload, "region", region) - conversions.put_if_specified(payload, "verification_level", verification_level) - conversions.put_if_specified(payload, "default_message_notifications", default_message_notifications) + conversions.put_if_specified(payload, "verification", verification_level) + conversions.put_if_specified(payload, "notifications", default_message_notifications) conversions.put_if_specified(payload, "explicit_content_filter", explicit_content_filter) conversions.put_if_specified(payload, "roles", roles) conversions.put_if_specified(payload, "channels", channels) @@ -1545,8 +1545,8 @@ async def modify_guild( # lgtm [py/similar-function] payload = {} conversions.put_if_specified(payload, "name", name) conversions.put_if_specified(payload, "region", region) - conversions.put_if_specified(payload, "verification_level", verification_level) - conversions.put_if_specified(payload, "default_message_notifications", default_message_notifications) + conversions.put_if_specified(payload, "verification", verification_level) + conversions.put_if_specified(payload, "notifications", default_message_notifications) conversions.put_if_specified(payload, "explicit_content_filter", explicit_content_filter) conversions.put_if_specified(payload, "afk_channel_id", afk_channel_id) conversions.put_if_specified(payload, "afk_timeout", afk_timeout) @@ -2492,7 +2492,7 @@ async def get_guild_embed(self, guild_id: str) -> more_typing.JSONObject: hikari.errors.Forbidden If you either lack the `MANAGE_GUILD` permission or are not in the guild. """ - route = routes.GET_GUILD_EMBED.compile(guild_id=guild_id) + route = routes.GET_GUILD_WIDGET.compile(guild_id=guild_id) return await self._request_json_response(route) async def modify_guild_embed( @@ -2528,7 +2528,7 @@ async def modify_guild_embed( payload = {} conversions.put_if_specified(payload, "channel", channel_id) conversions.put_if_specified(payload, "enabled", enabled) - route = routes.PATCH_GUILD_EMBED.compile(guild_id=guild_id) + route = routes.PATCH_GUILD_WIDGET.compile(guild_id=guild_id) return await self._request_json_response(route, body=payload, reason=reason) async def get_guild_vanity_url(self, guild_id: str) -> more_typing.JSONObject: diff --git a/hikari/internal/conversions.py b/hikari/internal/conversions.py index d25a18f7c7..611d52da5d 100644 --- a/hikari/internal/conversions.py +++ b/hikari/internal/conversions.py @@ -33,7 +33,7 @@ "EMPTY", "value_to_snowflake", "json_to_snowflake_map", - "json_to_sequence", + "json_to_collection", "timespan_to_int", ] @@ -53,6 +53,7 @@ _T_co = typing.TypeVar("_T_co", covariant=True) _T_contra = typing.TypeVar("_T_contra", contravariant=True) _Unique_contra = typing.TypeVar("_Unique_contra", bound=bases.Unique, contravariant=True) + _CollectionImpl_contra = typing.TypeVar("_CollectionImpl_contra", bound=typing.Collection, contravariant=True) DISCORD_EPOCH: typing.Final[int] = 1_420_070_400 @@ -271,10 +272,12 @@ def json_to_snowflake_map( return {item.id: item for item in items} -def json_to_sequence( - payload: more_typing.JSONArray, cast: typing.Callable[[more_typing.JSONType], _T_contra] -) -> typing.Sequence[_T_contra]: - return [cast(obj) for obj in payload] +def json_to_collection( + payload: more_typing.JSONArray, + cast: typing.Callable[[more_typing.JSONType], _T_contra], + collection_type: typing.Type[_CollectionImpl_contra] = list, +) -> _CollectionImpl_contra[_T_contra]: + return collection_type(cast(obj) for obj in payload) def timespan_to_int(value: typing.Union[more_typing.TimeSpanT]) -> int: diff --git a/hikari/models/audit_logs.py b/hikari/models/audit_logs.py index 445b3493ee..6a4d81a5b9 100644 --- a/hikari/models/audit_logs.py +++ b/hikari/models/audit_logs.py @@ -78,9 +78,9 @@ class AuditLogChangeKey(str, more_enums.Enum): AFK_CHANNEL_ID = "afk_channel_id" AFK_TIMEOUT = "afk_timeout" MFA_LEVEL = "mfa_level" - VERIFICATION_LEVEL = "verification_level" + VERIFICATION_LEVEL = "verification" EXPLICIT_CONTENT_FILTER = "explicit_content_filter" - DEFAULT_MESSAGE_NOTIFICATIONS = "default_message_notifications" + DEFAULT_MESSAGE_NOTIFICATIONS = "notifications" VANITY_URL_CODE = "vanity_url_code" ADD_ROLE_TO_MEMBER = "$add" REMOVE_ROLE_FROM_MEMBER = "$remove" diff --git a/hikari/net/rest.py b/hikari/net/rest.py index a645decbc1..cb15e44dab 100644 --- a/hikari/net/rest.py +++ b/hikari/net/rest.py @@ -430,7 +430,7 @@ async def fetch_channel_invites( ) -> typing.Sequence[invites.InviteWithMetadata]: route = routes.GET_CHANNEL_INVITES.compile(channel=conversions.value_to_snowflake(channel)) response = await self._request(route) - return conversions.json_to_sequence(response, self._app.entity_factory.deserialize_invite_with_metadata) + return conversions.json_to_collection(response, self._app.entity_factory.deserialize_invite_with_metadata) async def create_invite( self, @@ -957,6 +957,7 @@ async def fetch_user(self, user: typing.Union[users.User, bases.UniqueObjectT]) def fetch_guild_audit_log( self, guild: typing.Union[guilds.Guild, bases.UniqueObjectT], + /, *, before: typing.Union[unset.Unset, datetime.datetime, bases.UniqueObjectT] = unset.UNSET, user: typing.Union[unset.Unset, users.User, bases.UniqueObjectT] = unset.UNSET, @@ -978,10 +979,78 @@ def fetch_guild_audit_log( async def fetch_guild_emoji( self, guild: typing.Union[guilds.Guild, bases.UniqueObjectT], - emoji: typing.Union[emojis.KnownCustomEmoji, bases.UniqueObjectT], + # This is an emoji ID, which is the URL-safe emoji name, not the snowflake alone. + emoji: typing.Union[emojis.KnownCustomEmoji, str], ) -> emojis.KnownCustomEmoji: route = routes.GET_GUILD_EMOJI.compile( - guild=conversions.value_to_snowflake(guild), emoji=conversions.value_to_snowflake(emoji), + guild=conversions.value_to_snowflake(guild), + emoji=emoji.url_name if isinstance(emoji, emojis.Emoji) else emoji, ) response = await self._request(route) return self._app.entity_factory.deserialize_known_custom_emoji(response) + + async def fetch_guild_emojis( + self, guild: typing.Union[guilds.Guild, bases.UniqueObjectT], / + ) -> typing.Set[emojis.KnownCustomEmoji]: + route = routes.GET_GUILD_EMOJIS.compile(guild=conversions.value_to_snowflake(guild)) + response = await self._request(route) + return conversions.json_to_collection(response, self._app.entity_factory.deserialize_known_custom_emoji, set) + + async def create_guild_emoji( + self, + guild: typing.Union[guilds.Guild, bases.UniqueObjectT], + name: str, + image: files.BaseStream, + *, + roles: typing.Union[ + unset.Unset, typing.Collection[typing.Union[guilds.Role, bases.UniqueObjectT]] + ] = unset.UNSET, + reason: typing.Union[unset.Unset, str] = unset.UNSET, + ) -> emojis.KnownCustomEmoji: + payload = {"name": name, "image": await image.read()} + conversions.put_if_specified( + payload, "roles", roles, lambda seq: [conversions.value_to_snowflake(r) for r in seq] + ) + route = routes.POST_GUILD_EMOJIS.compile(guild=conversions.value_to_snowflake(guild)) + response = await self._request(route, body=payload, reason=reason) + return self._app.entity_factory.deserialize_known_custom_emoji(response) + + async def edit_guild_emoji( + self, + guild: typing.Union[guilds.Guild, bases.UniqueObjectT], + # This is an emoji ID, which is the URL-safe emoji name, not the snowflake alone. + emoji: typing.Union[emojis.KnownCustomEmoji, str], + *, + name: typing.Union[unset.Unset, str] = unset.UNSET, + roles: typing.Union[ + unset.Unset, typing.Collection[typing.Union[guilds.Role, bases.UniqueObjectT]] + ] = unset.UNSET, + reason: typing.Union[unset.Unset, str] = unset.UNSET, + ) -> emojis.KnownCustomEmoji: + payload = {} + conversions.put_if_specified(payload, "name", name) + conversions.put_if_specified( + payload, "roles", roles, lambda seq: [conversions.value_to_snowflake(r) for r in seq] + ) + route = routes.PATCH_GUILD_EMOJI.compile( + guild=conversions.value_to_snowflake(guild), + emoji=emoji.url_name if isinstance(emoji, emojis.Emoji) else emoji, + ) + response = await self._request(route, body=payload, reason=reason) + return self._app.entity_factory.deserialize_known_custom_emoji(response) + + async def delete_guild_emoji( + self, + guild: typing.Union[guilds.Guild, bases.UniqueObjectT], + # This is an emoji ID, which is the URL-safe emoji name, not the snowflake alone. + emoji: typing.Union[emojis.KnownCustomEmoji, str], + # Reason is not currently supported for some reason. See + ) -> None: + route = routes.DELETE_GUILD_EMOJI.compile( + guild=conversions.value_to_snowflake(guild), + emoji=emoji.url_name if isinstance(emoji, emojis.Emoji) else emoji + ) + await self._request(route) + + def create_guild(self, name: str) -> rest_utils.GuildBuilder: + return rest_utils.GuildBuilder(app=self._app, name=name, request_call=self._request) diff --git a/hikari/net/rest_utils.py b/hikari/net/rest_utils.py index 7e2ea01ebd..6169038967 100644 --- a/hikari/net/rest_utils.py +++ b/hikari/net/rest_utils.py @@ -30,8 +30,16 @@ import types import typing +import attr + +from hikari import rest_app from hikari.internal import conversions from hikari.models import bases +from hikari.models import colors +from hikari.models import files +from hikari.models import guilds +from hikari.models import permissions as permissions_ +from hikari.models import unset from hikari.net import routes if typing.TYPE_CHECKING: @@ -75,3 +83,141 @@ async def __aexit__(self, ex_t: typing.Type[Exception], ex_v: Exception, exc_tb: async def _keep_typing(self) -> None: with contextlib.suppress(asyncio.CancelledError): await asyncio.gather(self, asyncio.sleep(9.9), return_exceptions=True) + + +@attr.s(auto_attribs=True, kw_only=True, slots=True) +class GuildBuilder: + _app: rest_app.IRESTApp + _categories: typing.MutableSet[int] = attr.ib(factory=set) + _channels: typing.MutableSequence[more_typing.JSONObject] = attr.ib(factory=list) + _counter: int = 0 + _name: typing.Union[unset.Unset, str] + _request_call: typing.Callable[..., more_typing.Coroutine[more_typing.JSONObject]] + _roles: typing.MutableSequence[more_typing.JSONObject] = attr.ib(factory=list) + default_message_notifications: typing.Union[unset.Unset, guilds.GuildMessageNotificationsLevel] = unset.UNSET + explicit_content_filter_level: typing.Union[unset.Unset, guilds.GuildExplicitContentFilterLevel] = unset.UNSET + icon: typing.Union[unset.Unset, files.BaseStream] = unset.UNSET + region: typing.Union[unset.Unset, str] = unset.UNSET + verification_level: typing.Union[unset.Unset, guilds.GuildVerificationLevel] = unset.UNSET + + @property + def name(self) -> str: + # Read-only! + return self._name + + def __await__(self) -> typing.Generator[guilds.Guild, None, typing.Any]: + yield from self.create().__await__() + + async def create(self) -> guilds.Guild: + route = routes.POST_GUILDS.compile() + payload = { + "name": self.name, + "icon": None if unset.is_unset(self.icon) else await self.icon.read(), + "roles": self._roles, + "channels": self._channels, + } + conversions.put_if_specified(payload, "region", self.region) + conversions.put_if_specified(payload, "verification_level", self.verification_level) + conversions.put_if_specified(payload, "default_message_notifications", self.default_message_notifications) + conversions.put_if_specified(payload, "explicit_content_filter", self.explicit_content_filter_level) + + response = await self._request_call(route, body=payload) + return self._app.entity_factory.deserialize_guild(response) + + def add_role( + self, + name: str, + /, + *, + color: typing.Union[unset.Unset, colors.Color] = unset.UNSET, + colour: typing.Union[unset.Unset, colors.Color] = unset.UNSET, + hoisted: typing.Union[unset.Unset, bool] = unset.UNSET, + mentionable: typing.Union[unset.Unset, bool] = unset.UNSET, + permissions: typing.Union[unset.Unset, permissions_.Permission] = unset.UNSET, + position: typing.Union[unset.Unset, int] = unset.UNSET, + ) -> None: + payload = {"name": name} + conversions.put_if_specified(payload, "color", color) + conversions.put_if_specified(payload, "color", colour) + conversions.put_if_specified(payload, "hoisted", hoisted) + conversions.put_if_specified(payload, "mentionable", mentionable) + conversions.put_if_specified(payload, "permissions", permissions) + conversions.put_if_specified(payload, "position", position) + self._roles.append(payload) + + def add_category( + self, + name: str, + /, + *, + position: typing.Union[unset.Unset, int] = unset.UNSET, + permission_overwrites: typing.Union[unset.Unset, typing.Collection[channels.PermissionOverwrite]] = unset.UNSET, + nsfw: typing.Union[unset.Unset, bool] = unset.UNSET, + ) -> int: + identifier = self._counter + self._categories.add(identifier) + self._counter += 1 + payload = { + "id": str(identifier), + "type": channels.ChannelType.GUILD_CATEGORY, + "name": name + } + conversions.put_if_specified(payload, "position", position) + conversions.put_if_specified(payload, "permission_overwrites", permission_overwrites) + conversions.put_if_specified(payload, "nsfw", nsfw) + self._channels.append(payload) + return identifier + + def add_text_channel( + self, + name: str, + /, + *, + parent_id: int = unset.UNSET, + topic: typing.Union[unset.Unset, str] = unset.UNSET, + rate_limit_per_user: typing.Union[unset.Unset, more_typing.TimeSpanT] = unset.UNSET, + position: typing.Union[unset.Unset, int] = unset.UNSET, + permission_overwrites: typing.Union[unset.Unset, typing.Collection[channels.PermissionOverwrite]] = unset.UNSET, + nsfw: typing.Union[unset.Unset, bool] = unset.UNSET, + ) -> None: + if not unset.is_unset(parent_id) and parent_id not in self._categories: + raise ValueError(f"ID {parent_id} is not a category in this guild builder.") + + payload = {"type": channels.ChannelType.GUILD_TEXT, "name": name} + conversions.put_if_specified(payload, "topic", topic) + conversions.put_if_specified(payload, "rate_limit_per_user", rate_limit_per_user, conversions.timespan_to_int) + conversions.put_if_specified(payload, "position", position) + conversions.put_if_specified(payload, "nsfw", nsfw) + + if not unset.is_unset(permission_overwrites): + overwrites = [self._app.entity_factory.serialize_permission_overwrite(o) for o in permission_overwrites] + payload["permission_overwrites"] = overwrites + + self._channels.append(payload) + + def add_voice_channel( + self, + name: str, + /, + *, + parent_id: int = unset.UNSET, + bitrate: typing.Union[unset.Unset, int] = unset.UNSET, + position: typing.Union[unset.Unset, int] = unset.UNSET, + permission_overwrites: typing.Union[unset.Unset, typing.Collection[channels.PermissionOverwrite]] = unset.UNSET, + nsfw: typing.Union[unset.Unset, bool] = unset.UNSET, + user_limit: typing.Union[unset.Unset, int] = unset.UNSET, + ) -> None: + if not unset.is_unset(parent_id) and parent_id not in self._categories: + raise ValueError(f"ID {parent_id} is not a category in this guild builder.") + + payload = {"type": channels.ChannelType.GUILD_VOICE, "name": name} + conversions.put_if_specified(payload, "bitrate", bitrate) + conversions.put_if_specified(payload, "position", position) + conversions.put_if_specified(payload, "nsfw", nsfw) + conversions.put_if_specified(payload, "user_limit", user_limit) + + if not unset.is_unset(permission_overwrites): + overwrites = [self._app.entity_factory.serialize_permission_overwrite(o) for o in permission_overwrites] + payload["permission_overwrites"] = overwrites + + self._channels.append(payload) diff --git a/hikari/net/routes.py b/hikari/net/routes.py index 855fb9dd47..3f1f546446 100644 --- a/hikari/net/routes.py +++ b/hikari/net/routes.py @@ -273,8 +273,8 @@ def __eq__(self, other) -> bool: POST_GUILD_CHANNELS = Route(POST, "/guilds/{guild}/channels") PATCH_GUILD_CHANNELS = Route(PATCH, "/guilds/{guild}/channels") -GET_GUILD_EMBED = Route(GET, "/guilds/{guild}/embed") -PATCH_GUILD_EMBED = Route(PATCH, "/guilds/{guild}/embed") +GET_GUILD_WIDGET = Route(GET, "/guilds/{guild}/widget") +PATCH_GUILD_WIDGET = Route(PATCH, "/guilds/{guild}/widget") GET_GUILD_EMOJI = Route(GET, "/guilds/{guild}/emojis/{emoji}") PATCH_GUILD_EMOJI = Route(PATCH, "/guilds/{guild}/emojis/{emoji}") From 612e425296560cef9878198595834e09b12b4661 Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Sun, 24 May 2020 20:13:37 +0100 Subject: [PATCH 389/922] Fixed GuildBuilder to generate fake snowflakes. --- hikari/internal/conversions.py | 19 ++++++++++- hikari/models/bases.py | 17 ++++++---- hikari/net/rest.py | 2 +- hikari/net/rest_utils.py | 61 +++++++++++++++++++--------------- 4 files changed, 65 insertions(+), 34 deletions(-) diff --git a/hikari/internal/conversions.py b/hikari/internal/conversions.py index 611d52da5d..0c21771a3a 100644 --- a/hikari/internal/conversions.py +++ b/hikari/internal/conversions.py @@ -27,6 +27,7 @@ "rfc7231_datetime_string_to_datetime", "iso8601_datetime_string_to_datetime", "discord_epoch_to_datetime", + "datetime_to_discord_epoch", "unix_epoch_to_datetime", "pluralize", "resolve_signature", @@ -173,7 +174,23 @@ def discord_epoch_to_datetime(epoch: int, /) -> datetime.datetime: datetime.datetime Number of seconds since 1/1/1970 within a datetime object (UTC). """ - return datetime.datetime.fromtimestamp(epoch / 1000 + DISCORD_EPOCH, datetime.timezone.utc) + return datetime.datetime.fromtimestamp(epoch / 1_000 + DISCORD_EPOCH, datetime.timezone.utc) + + +def datetime_to_discord_epoch(timestamp: datetime.datetime) -> int: + """Parse a `datetime.datetime` object into an integer discord epoch.. + + Parameters + ---------- + timestamp : datetime.datetime + Number of seconds since 1/1/1970 within a datetime object (UTC). + + Returns + ------- + int + Number of milliseconds since 1/1/2015 (UTC) + """ + return int((timestamp.timestamp() - DISCORD_EPOCH) * 1_000) def unix_epoch_to_datetime(epoch: int, /) -> datetime.datetime: diff --git a/hikari/models/bases.py b/hikari/models/bases.py index a2c458b79a..5e019741aa 100644 --- a/hikari/models/bases.py +++ b/hikari/models/bases.py @@ -84,12 +84,7 @@ def increment(self) -> int: @classmethod def from_datetime(cls, date: datetime.datetime) -> Snowflake: """Get a snowflake object from a datetime object.""" - return cls.from_timestamp(date.timestamp()) - - @classmethod - def from_timestamp(cls, timestamp: float) -> Snowflake: - """Get a snowflake object from a UNIX timestamp.""" - return cls(int((timestamp - conversions.DISCORD_EPOCH) * 1000) << 22) + return cls.from_data(date, 0, 0, 0) @classmethod def min(cls) -> Snowflake: @@ -105,6 +100,16 @@ def max(cls) -> Snowflake: cls.___MAX___ = Snowflake((1 << 63) - 1) return cls.___MAX___ + @classmethod + def from_data(cls, timestamp: datetime.datetime, worker_id: int, process_id: int, increment: int) -> Snowflake: + """Convert the pieces of info that comprise an ID into a Snowflake.""" + return cls( + (conversions.datetime_to_discord_epoch(timestamp) << 22) + | (worker_id << 17) + | (process_id << 12) + | increment + ) + @marshaller.marshallable() @attr.s(eq=True, hash=True, kw_only=True, slots=True) diff --git a/hikari/net/rest.py b/hikari/net/rest.py index cb15e44dab..7ec5786bef 100644 --- a/hikari/net/rest.py +++ b/hikari/net/rest.py @@ -1048,7 +1048,7 @@ async def delete_guild_emoji( ) -> None: route = routes.DELETE_GUILD_EMOJI.compile( guild=conversions.value_to_snowflake(guild), - emoji=emoji.url_name if isinstance(emoji, emojis.Emoji) else emoji + emoji=emoji.url_name if isinstance(emoji, emojis.Emoji) else emoji, ) await self._request(route) diff --git a/hikari/net/rest_utils.py b/hikari/net/rest_utils.py index 6169038967..f4d1cd63ff 100644 --- a/hikari/net/rest_utils.py +++ b/hikari/net/rest_utils.py @@ -27,6 +27,7 @@ import asyncio import contextlib +import datetime import types import typing @@ -85,10 +86,13 @@ async def _keep_typing(self) -> None: await asyncio.gather(self, asyncio.sleep(9.9), return_exceptions=True) +class DummyID(int): + __slots__ = () + + @attr.s(auto_attribs=True, kw_only=True, slots=True) class GuildBuilder: _app: rest_app.IRESTApp - _categories: typing.MutableSet[int] = attr.ib(factory=set) _channels: typing.MutableSequence[more_typing.JSONObject] = attr.ib(factory=list) _counter: int = 0 _name: typing.Union[unset.Unset, str] @@ -135,8 +139,9 @@ def add_role( mentionable: typing.Union[unset.Unset, bool] = unset.UNSET, permissions: typing.Union[unset.Unset, permissions_.Permission] = unset.UNSET, position: typing.Union[unset.Unset, int] = unset.UNSET, - ) -> None: - payload = {"name": name} + ) -> bases.Snowflake: + snowflake = self._new_snowflake() + payload = {"id": str(snowflake), "name": name} conversions.put_if_specified(payload, "color", color) conversions.put_if_specified(payload, "color", colour) conversions.put_if_specified(payload, "hoisted", hoisted) @@ -144,6 +149,7 @@ def add_role( conversions.put_if_specified(payload, "permissions", permissions) conversions.put_if_specified(payload, "position", position) self._roles.append(payload) + return snowflake def add_category( self, @@ -153,71 +159,74 @@ def add_category( position: typing.Union[unset.Unset, int] = unset.UNSET, permission_overwrites: typing.Union[unset.Unset, typing.Collection[channels.PermissionOverwrite]] = unset.UNSET, nsfw: typing.Union[unset.Unset, bool] = unset.UNSET, - ) -> int: - identifier = self._counter - self._categories.add(identifier) - self._counter += 1 - payload = { - "id": str(identifier), - "type": channels.ChannelType.GUILD_CATEGORY, - "name": name - } + ) -> bases.Snowflake: + snowflake = self._new_snowflake() + payload = {"id": str(snowflake), "type": channels.ChannelType.GUILD_CATEGORY, "name": name} conversions.put_if_specified(payload, "position", position) - conversions.put_if_specified(payload, "permission_overwrites", permission_overwrites) conversions.put_if_specified(payload, "nsfw", nsfw) + + if not unset.is_unset(permission_overwrites): + overwrites = [self._app.entity_factory.serialize_permission_overwrite(o) for o in permission_overwrites] + payload["permission_overwrites"] = overwrites + self._channels.append(payload) - return identifier + return snowflake def add_text_channel( self, name: str, /, *, - parent_id: int = unset.UNSET, + parent_id: bases.Snowflake = unset.UNSET, topic: typing.Union[unset.Unset, str] = unset.UNSET, rate_limit_per_user: typing.Union[unset.Unset, more_typing.TimeSpanT] = unset.UNSET, position: typing.Union[unset.Unset, int] = unset.UNSET, permission_overwrites: typing.Union[unset.Unset, typing.Collection[channels.PermissionOverwrite]] = unset.UNSET, nsfw: typing.Union[unset.Unset, bool] = unset.UNSET, - ) -> None: - if not unset.is_unset(parent_id) and parent_id not in self._categories: - raise ValueError(f"ID {parent_id} is not a category in this guild builder.") - - payload = {"type": channels.ChannelType.GUILD_TEXT, "name": name} + ) -> bases.Snowflake: + snowflake = self._new_snowflake() + payload = {"id": str(snowflake), "type": channels.ChannelType.GUILD_TEXT, "name": name} conversions.put_if_specified(payload, "topic", topic) conversions.put_if_specified(payload, "rate_limit_per_user", rate_limit_per_user, conversions.timespan_to_int) conversions.put_if_specified(payload, "position", position) conversions.put_if_specified(payload, "nsfw", nsfw) + conversions.put_if_specified(payload, "parent_id", parent_id, str) if not unset.is_unset(permission_overwrites): overwrites = [self._app.entity_factory.serialize_permission_overwrite(o) for o in permission_overwrites] payload["permission_overwrites"] = overwrites self._channels.append(payload) + return snowflake def add_voice_channel( self, name: str, /, *, - parent_id: int = unset.UNSET, + parent_id: bases.Snowflake = unset.UNSET, bitrate: typing.Union[unset.Unset, int] = unset.UNSET, position: typing.Union[unset.Unset, int] = unset.UNSET, permission_overwrites: typing.Union[unset.Unset, typing.Collection[channels.PermissionOverwrite]] = unset.UNSET, nsfw: typing.Union[unset.Unset, bool] = unset.UNSET, user_limit: typing.Union[unset.Unset, int] = unset.UNSET, - ) -> None: - if not unset.is_unset(parent_id) and parent_id not in self._categories: - raise ValueError(f"ID {parent_id} is not a category in this guild builder.") - - payload = {"type": channels.ChannelType.GUILD_VOICE, "name": name} + ) -> bases.Snowflake: + snowflake = self._new_snowflake() + payload = {"id": str(snowflake), "type": channels.ChannelType.GUILD_VOICE, "name": name} conversions.put_if_specified(payload, "bitrate", bitrate) conversions.put_if_specified(payload, "position", position) conversions.put_if_specified(payload, "nsfw", nsfw) conversions.put_if_specified(payload, "user_limit", user_limit) + conversions.put_if_specified(payload, "parent_id", parent_id, str) if not unset.is_unset(permission_overwrites): overwrites = [self._app.entity_factory.serialize_permission_overwrite(o) for o in permission_overwrites] payload["permission_overwrites"] = overwrites self._channels.append(payload) + return snowflake + + def _new_snowflake(self) -> bases.Snowflake: + value = self._counter + self._counter += 1 + return bases.Snowflake.from_data(datetime.datetime.now(tz=datetime.timezone.utc), 0, 0, value,) From cf96704be6e5661d125d5f382505ea03ed2ef7c5 Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Sun, 24 May 2020 22:02:32 +0100 Subject: [PATCH 390/922] Implemented a really complex work around for the first role a guild builder produces being @everyone. --- hikari/net/rest_utils.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/hikari/net/rest_utils.py b/hikari/net/rest_utils.py index f4d1cd63ff..465ed508a3 100644 --- a/hikari/net/rest_utils.py +++ b/hikari/net/rest_utils.py @@ -140,6 +140,9 @@ def add_role( permissions: typing.Union[unset.Unset, permissions_.Permission] = unset.UNSET, position: typing.Union[unset.Unset, int] = unset.UNSET, ) -> bases.Snowflake: + if len(self._roles) == 0 and name != "@everyone": + raise ValueError("First role must always be the @everyone role") + snowflake = self._new_snowflake() payload = {"id": str(snowflake), "name": name} conversions.put_if_specified(payload, "color", color) From 6391b8f4cb822c8829604a78bcf51586d31aedeb Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Sun, 24 May 2020 22:50:35 +0100 Subject: [PATCH 391/922] Added fetch_guild and fetch_guild_preview --- hikari/net/rest.py | 47 +++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 46 insertions(+), 1 deletion(-) diff --git a/hikari/net/rest.py b/hikari/net/rest.py index 7ec5786bef..875aa7b5da 100644 --- a/hikari/net/rest.py +++ b/hikari/net/rest.py @@ -947,7 +947,7 @@ async def add_user_to_guild( async def fetch_voice_regions(self) -> typing.Sequence[voices.VoiceRegion]: route = routes.GET_VOICE_REGIONS.compile() response = await self._request(route) - return conversions.json_to_snowflake_map(response, self._app.entity_factory.deserialize_voice_region) + return conversions.json_to_collection(response, self._app.entity_factory.deserialize_voice_region) async def fetch_user(self, user: typing.Union[users.User, bases.UniqueObjectT]) -> users.User: route = routes.GET_USER.compile(user=conversions.value_to_snowflake(user)) @@ -1054,3 +1054,48 @@ async def delete_guild_emoji( def create_guild(self, name: str) -> rest_utils.GuildBuilder: return rest_utils.GuildBuilder(app=self._app, name=name, request_call=self._request) + + async def fetch_guild(self, guild: typing.Union[guilds.Guild, bases.UniqueObjectT]) -> guilds.Guild: + route = routes.GET_GUILD.compile(guild=conversions.value_to_snowflake(guild)) + response = await self._request(route) + return self._app.entity_factory.deserialize_guild(response) + + async def fetch_guild_preview( + self, guild: typing.Union[guilds.PartialGuild, bases.UniqueObjectT] + ) -> guilds.GuildPreview: + route = routes.GET_GUILD_PREVIEW.compile(guild=conversions.value_to_snowflake(guild)) + response = await self._request(route) + return self._app.entity_factory.deserialize_guild_preview(response) + + edit_guild = NotImplemented + fetch_guild_channel = NotImplemented + fetch_guild_channels = NotImplemented + create_guild_channel = NotImplemented + reposition_guild_channels = NotImplemented + fetch_member = NotImplemented + fetch_members = NotImplemented + edit_member = NotImplemented + edit_my_nickname = NotImplemented + add_role_to_member = NotImplemented + remove_role_from_member = NotImplemented + kick_member = NotImplemented + create_ban = NotImplemented + delete_ban = NotImplemented + fetch_ban = NotImplemented + fetch_bans = NotImplemented + fetch_roles = NotImplemented + create_role = NotImplemented + reposition_roles = NotImplemented + edit_role = NotImplemented + delete_role = NotImplemented + estimate_guild_prune_count = NotImplemented + begin_guild_prune = NotImplemented + fetch_guild_voice_regions = NotImplemented + fetch_guild_invites = NotImplemented + fetch_guild_integrations = NotImplemented + edit_guild_integration = NotImplemented + delete_guild_integration = NotImplemented + sync_guild_integration = NotImplemented + fetch_guild_widget = NotImplemented + edit_guild_widget = NotImplemented + fetch_guild_vanity_url = NotImplemented From ac42eb8d11d6f3dddc6994c43cec8ac372354ce3 Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Mon, 25 May 2020 13:35:17 +0100 Subject: [PATCH 392/922] Added more guild REST endpoints. --- hikari/__init__.py | 33 +- hikari/events/guild.py | 2 +- hikari/events/message.py | 2 +- hikari/internal/conversions.py | 2 +- hikari/{models => internal}/unset.py | 0 hikari/models/__init__.py | 2 - hikari/models/guilds.py | 2 +- hikari/models/voices.py | 3 + hikari/net/gateway.py | 2 +- hikari/net/http_client.py | 1 - hikari/net/iterators.py | 2 +- hikari/net/rest.py | 421 ++++++++++++++++-- hikari/net/rest_utils.py | 2 +- hikari/stateless/__init__.py | 32 -- hikari/stateless/bot.py | 60 --- hikari/stateless/manager.py | 278 ------------ .../hikari/{models => internal}/test_unset.py | 2 +- tests/hikari/models/test_guilds.py | 2 +- 18 files changed, 407 insertions(+), 441 deletions(-) rename hikari/{models => internal}/unset.py (100%) delete mode 100644 hikari/stateless/__init__.py delete mode 100644 hikari/stateless/bot.py delete mode 100644 hikari/stateless/manager.py rename tests/hikari/{models => internal}/test_unset.py (97%) diff --git a/hikari/__init__.py b/hikari/__init__.py index 6a945d3452..175546c834 100644 --- a/hikari/__init__.py +++ b/hikari/__init__.py @@ -20,21 +20,22 @@ from __future__ import annotations -from hikari.net.gateway import * -from hikari.net.rest import * -from ._about import __author__ -from ._about import __ci__ -from ._about import __copyright__ -from ._about import __discord_invite__ -from ._about import __docs__ -from ._about import __email__ -from ._about import __issue_tracker__ -from ._about import __license__ -from ._about import __url__ -from ._about import __version__ -from .errors import * -from .events import * -from .http_settings import * -from .models import * +from hikari._about import __author__ +from hikari._about import __ci__ +from hikari._about import __copyright__ +from hikari._about import __discord_invite__ +from hikari._about import __docs__ +from hikari._about import __email__ +from hikari._about import __issue_tracker__ +from hikari._about import __license__ +from hikari._about import __url__ +from hikari._about import __version__ + +from hikari.errors import * +from hikari.events import * +from hikari.http_settings import * +from hikari.models import * +from hikari.net import * +from hikari.impl import * __all__ = [] diff --git a/hikari/events/guild.py b/hikari/events/guild.py index 83224151e5..209d2c5d06 100644 --- a/hikari/events/guild.py +++ b/hikari/events/guild.py @@ -50,7 +50,7 @@ from hikari.models import emojis as emojis_models from hikari.models import guilds from hikari.models import intents -from hikari.models import unset +from ..internal import unset from hikari.models import users from . import base as base_events diff --git a/hikari/events/message.py b/hikari/events/message.py index df6355d5f8..128d21b64a 100644 --- a/hikari/events/message.py +++ b/hikari/events/message.py @@ -44,7 +44,7 @@ from hikari.models import guilds from hikari.models import intents from hikari.models import messages -from hikari.models import unset +from ..internal import unset from hikari.models import users from . import base as base_events diff --git a/hikari/internal/conversions.py b/hikari/internal/conversions.py index 0c21771a3a..bf5077cadb 100644 --- a/hikari/internal/conversions.py +++ b/hikari/internal/conversions.py @@ -44,7 +44,7 @@ import re import typing -from hikari.models import unset +from hikari.internal import unset if typing.TYPE_CHECKING: from hikari.internal import more_typing diff --git a/hikari/models/unset.py b/hikari/internal/unset.py similarity index 100% rename from hikari/models/unset.py rename to hikari/internal/unset.py diff --git a/hikari/models/__init__.py b/hikari/models/__init__.py index 60fb41c6f7..96afc692af 100644 --- a/hikari/models/__init__.py +++ b/hikari/models/__init__.py @@ -35,7 +35,6 @@ from .invites import * from .messages import * from .permissions import * -from .unset import * from .users import * from .voices import * from .webhooks import * @@ -56,7 +55,6 @@ + invites.__all__ + messages.__all__ + permissions.__all__ - + unset.__all__ + users.__all__ + voices.__all__ + webhooks.__all__ diff --git a/hikari/models/guilds.py b/hikari/models/guilds.py index fc31aa4256..c26cd0f4bb 100644 --- a/hikari/models/guilds.py +++ b/hikari/models/guilds.py @@ -67,7 +67,7 @@ from . import colors from . import emojis as emojis_ from . import permissions as permissions_ -from . import unset +from ..internal import unset from . import users from ..net import urls diff --git a/hikari/models/voices.py b/hikari/models/voices.py index 0f866ec4bd..dfea94109e 100644 --- a/hikari/models/voices.py +++ b/hikari/models/voices.py @@ -108,3 +108,6 @@ class VoiceRegion(bases.Entity, marshaller.Deserializable): is_custom: bool = marshaller.attrib(raw_name="custom", deserializer=bool, eq=False, hash=False) """Whether this region is custom (e.g. used for events).""" + + def __str__(self) -> str: + return self.id diff --git a/hikari/net/gateway.py b/hikari/net/gateway.py index f69706e66a..43be5bca8c 100644 --- a/hikari/net/gateway.py +++ b/hikari/net/gateway.py @@ -41,7 +41,7 @@ from hikari.models import bases from hikari.models import channels from hikari.models import guilds -from hikari.models import unset +from hikari.internal import unset from hikari.net import http_client from hikari.net import user_agents diff --git a/hikari/net/http_client.py b/hikari/net/http_client.py index 1e04d5d9af..71c09b9172 100644 --- a/hikari/net/http_client.py +++ b/hikari/net/http_client.py @@ -29,7 +29,6 @@ import aiohttp.typedefs -from hikari.models import unset from hikari.net import tracing diff --git a/hikari/net/iterators.py b/hikari/net/iterators.py index 2a10ef4904..e950d979ba 100644 --- a/hikari/net/iterators.py +++ b/hikari/net/iterators.py @@ -33,7 +33,7 @@ from hikari.models import bases from hikari.models import guilds from hikari.models import messages -from hikari.models import unset +from hikari.internal import unset from hikari.models import users from hikari.net import routes diff --git a/hikari/net/rest.py b/hikari/net/rest.py index 875aa7b5da..223b822160 100644 --- a/hikari/net/rest.py +++ b/hikari/net/rest.py @@ -38,26 +38,28 @@ from hikari.internal import more_collections from hikari.internal import more_typing from hikari.internal import ratelimits -from hikari.models import applications -from hikari.models import bases -from hikari.models import channels -from hikari.models import embeds as embeds_ -from hikari.models import emojis -from hikari.models import files -from hikari.models import gateway -from hikari.models import guilds -from hikari.models import invites -from hikari.models import messages -from hikari.models import permissions -from hikari.models import unset -from hikari.models import users -from hikari.models import voices -from hikari.models import webhooks +from hikari.internal import unset from hikari.net import buckets from hikari.net import http_client from hikari.net import rest_utils from hikari.net import routes +if typing.TYPE_CHECKING: + from hikari.models import applications + from hikari.models import bases + from hikari.models import channels + from hikari.models import embeds as embeds_ + from hikari.models import emojis + from hikari.models import files + from hikari.models import gateway + from hikari.models import guilds + from hikari.models import invites + from hikari.models import messages + from hikari.models import permissions + from hikari.models import users + from hikari.models import voices + from hikari.models import webhooks + class _RateLimited(RuntimeError): __slots__ = () @@ -463,10 +465,10 @@ def trigger_typing( async def fetch_pins( self, channel: typing.Union[channels.TextChannel, bases.UniqueObjectT], / - ) -> typing.Mapping[bases.Snowflake, messages.Message]: + ) -> typing.Sequence[messages.Message]: route = routes.GET_CHANNEL_PINS.compile(channel=conversions.value_to_snowflake(channel)) response = await self._request(route) - return conversions.json_to_snowflake_map(response, self._app.entity_factory.deserialize_message) + return conversions.json_to_collection(response, self._app.entity_factory.deserialize_message) async def create_pinned_message( self, @@ -741,17 +743,17 @@ async def fetch_webhook( async def fetch_channel_webhooks( self, channel: typing.Union[channels.TextChannel, bases.UniqueObjectT], / - ) -> typing.Mapping[bases.Snowflake, webhooks.Webhook]: + ) -> typing.Sequence[webhooks.Webhook]: route = routes.GET_CHANNEL_WEBHOOKS.compile(channel=conversions.value_to_snowflake(channel)) response = await self._request(route) - return conversions.json_to_snowflake_map(response, self._app.entity_factory.deserialize_webhook) + return conversions.json_to_collection(response, self._app.entity_factory.deserialize_webhook) async def fetch_guild_webhooks( self, guild: typing.Union[guilds.Guild, bases.UniqueObjectT], / - ) -> typing.Mapping[bases.Snowflake, webhooks.Webhook]: + ) -> typing.Sequence[webhooks.Webhook]: route = routes.GET_GUILD_WEBHOOKS.compile(channel=conversions.value_to_snowflake(guild)) response = await self._request(route) - return conversions.json_to_snowflake_map(response, self._app.entity_factory.deserialize_webhook) + return conversions.json_to_collection(response, self._app.entity_factory.deserialize_webhook) async def edit_webhook( self, @@ -917,7 +919,7 @@ async def add_user_to_guild( guild: typing.Union[guilds.Guild, bases.UniqueObjectT], user: typing.Union[users.User, bases.UniqueObjectT], *, - nickname: typing.Union[unset.Unset, str] = unset.UNSET, + nick: typing.Union[unset.Unset, str] = unset.UNSET, roles: typing.Union[ unset.Unset, typing.Collection[typing.Union[guilds.Role, bases.UniqueObjectT]] ] = unset.UNSET, @@ -929,7 +931,7 @@ async def add_user_to_guild( ) payload = {"access_token": access_token} - conversions.put_if_specified(payload, "nick", nickname) + conversions.put_if_specified(payload, "nick", nick) conversions.put_if_specified( payload, "roles", roles, lambda rs: [conversions.value_to_snowflake(r) for r in rs] ) @@ -1052,38 +1054,371 @@ async def delete_guild_emoji( ) await self._request(route) - def create_guild(self, name: str) -> rest_utils.GuildBuilder: + def create_guild(self, name: str, /) -> rest_utils.GuildBuilder: return rest_utils.GuildBuilder(app=self._app, name=name, request_call=self._request) - async def fetch_guild(self, guild: typing.Union[guilds.Guild, bases.UniqueObjectT]) -> guilds.Guild: + async def fetch_guild(self, guild: typing.Union[guilds.Guild, bases.UniqueObjectT], /) -> guilds.Guild: route = routes.GET_GUILD.compile(guild=conversions.value_to_snowflake(guild)) response = await self._request(route) return self._app.entity_factory.deserialize_guild(response) async def fetch_guild_preview( - self, guild: typing.Union[guilds.PartialGuild, bases.UniqueObjectT] + self, guild: typing.Union[guilds.PartialGuild, bases.UniqueObjectT], / ) -> guilds.GuildPreview: route = routes.GET_GUILD_PREVIEW.compile(guild=conversions.value_to_snowflake(guild)) response = await self._request(route) return self._app.entity_factory.deserialize_guild_preview(response) - edit_guild = NotImplemented - fetch_guild_channel = NotImplemented - fetch_guild_channels = NotImplemented - create_guild_channel = NotImplemented - reposition_guild_channels = NotImplemented - fetch_member = NotImplemented - fetch_members = NotImplemented - edit_member = NotImplemented - edit_my_nickname = NotImplemented - add_role_to_member = NotImplemented - remove_role_from_member = NotImplemented - kick_member = NotImplemented - create_ban = NotImplemented - delete_ban = NotImplemented - fetch_ban = NotImplemented - fetch_bans = NotImplemented - fetch_roles = NotImplemented + async def edit_guild( + self, + guild: typing.Union[guilds.Guild, bases.UniqueObjectT], + /, + *, + name: typing.Union[unset.Unset, str] = unset.UNSET, + region: typing.Union[unset.Unset, voices.VoiceRegion, str] = unset.UNSET, + verification_level: typing.Union[unset.Unset, guilds.GuildVerificationLevel] = unset.UNSET, + default_message_notifications: typing.Union[unset.Unset, guilds.GuildMessageNotificationsLevel] = unset.UNSET, + explicit_content_filter_level: typing.Union[unset.Unset, guilds.GuildExplicitContentFilterLevel] = unset.UNSET, + afk_channel: typing.Union[unset.Unset, channels.GuildVoiceChannel, bases.UniqueObjectT] = unset.UNSET, + afk_timeout: typing.Union[unset.Unset, more_typing.TimeSpanT] = unset.UNSET, + icon: typing.Union[unset.Unset, files.BaseStream] = unset.UNSET, + owner: typing.Union[unset.Unset, users.User, bases.UniqueObjectT] = unset.UNSET, + splash: typing.Union[unset.Unset, files.BaseStream] = unset.UNSET, + system_channel: typing.Union[unset.Unset, channels.GuildTextChannel] = unset.UNSET, + reason: typing.Union[unset.Unset, str] = unset.UNSET, + ) -> guilds.Guild: + route = routes.PATCH_GUILD.compile(guild=conversions.value_to_snowflake(guild)) + payload = {} + conversions.put_if_specified(payload, "name", name) + conversions.put_if_specified(payload, "region", region, str) + conversions.put_if_specified(payload, "verification", verification_level) + conversions.put_if_specified(payload, "notifications", default_message_notifications) + conversions.put_if_specified(payload, "explicit_content_filter", explicit_content_filter_level) + conversions.put_if_specified(payload, "afk_channel_id", afk_channel, conversions.value_to_snowflake) + conversions.put_if_specified(payload, "afk_timeout", afk_timeout, conversions.timespan_to_int) + conversions.put_if_specified(payload, "owner_id", owner, conversions.value_to_snowflake) + conversions.put_if_specified(payload, "system_channel_id", system_channel, conversions.value_to_snowflake) + + if not unset.is_unset(icon): + payload["icon"] = await icon.read() + + if not unset.is_unset(splash): + payload["splash"] = await splash.read() + + response = await self._request(route, body=payload, reason=reason) + return self._app.entity_factory.deserialize_guild(response) + + async def delete_guild(self, guild: typing.Union[guilds.Guild, bases.UniqueObjectT]) -> None: + route = routes.DELETE_GUILD.compile(guild=conversions.value_to_snowflake(guild)) + await self._request(route) + + async def fetch_guild_channels( + self, guild: typing.Union[guilds.Guild, bases.UniqueObjectT] + ) -> typing.Sequence[channels.GuildChannel]: + route = routes.GET_GUILD_CHANNELS.compile(guild=conversions.value_to_snowflake(guild)) + response = await self._request(route) + channel_sequence = [self._app.entity_factory.deserialize_channel(c) for c in response] + # Will always be guild channels unless Discord messes up severely on something! + return typing.cast(typing.Sequence[channels.GuildChannel], channel_sequence) + + async def create_guild_text_channel( + self, + guild: typing.Union[guilds.Guild, bases.UniqueObjectT], + name: str, + *, + position: typing.Union[int, unset.Unset] = unset.UNSET, + topic: typing.Union[str, unset.Unset] = unset.UNSET, + nsfw: typing.Union[bool, unset.Unset] = unset.UNSET, + rate_limit_per_user: typing.Union[int, unset.Unset] = unset.UNSET, + permission_overwrites: typing.Union[typing.Sequence[channels.PermissionOverwrite], unset.Unset] = unset.UNSET, + category: typing.Union[channels.GuildCategory, bases.UniqueObjectT, unset.Unset] = unset.UNSET, + reason: typing.Union[str, unset.Unset] = unset.UNSET, + ) -> channels.GuildTextChannel: + channel = await self._create_guild_channel( + guild, + name, + channels.ChannelType.GUILD_TEXT, + position=position, + topic=topic, + nsfw=nsfw, + rate_limit_per_user=rate_limit_per_user, + permission_overwrites=permission_overwrites, + category=category, + reason=reason, + ) + return typing.cast(channels.GuildTextChannel, channel) + + async def create_guild_news_channel( + self, + guild: typing.Union[guilds.Guild, bases.UniqueObjectT], + name: str, + *, + position: typing.Union[int, unset.Unset] = unset.UNSET, + topic: typing.Union[str, unset.Unset] = unset.UNSET, + nsfw: typing.Union[bool, unset.Unset] = unset.UNSET, + rate_limit_per_user: typing.Union[int, unset.Unset] = unset.UNSET, + permission_overwrites: typing.Union[typing.Sequence[channels.PermissionOverwrite], unset.Unset] = unset.UNSET, + category: typing.Union[channels.GuildCategory, bases.UniqueObjectT, unset.Unset] = unset.UNSET, + reason: typing.Union[str, unset.Unset] = unset.UNSET, + ) -> channels.GuildNewsChannel: + channel = await self._create_guild_channel( + guild, + name, + channels.ChannelType.GUILD_NEWS, + position=position, + topic=topic, + nsfw=nsfw, + rate_limit_per_user=rate_limit_per_user, + permission_overwrites=permission_overwrites, + category=category, + reason=reason, + ) + return typing.cast(channels.GuildNewsChannel, channel) + + async def create_guild_voice_channel( + self, + guild: typing.Union[guilds.Guild, bases.UniqueObjectT], + name: str, + *, + position: typing.Union[int, unset.Unset] = unset.UNSET, + nsfw: typing.Union[bool, unset.Unset] = unset.UNSET, + user_limit: typing.Union[int, unset.Unset] = unset.UNSET, + bitrate: typing.Union[int, unset.Unset] = unset.UNSET, + permission_overwrites: typing.Union[typing.Sequence[channels.PermissionOverwrite], unset.Unset] = unset.UNSET, + category: typing.Union[channels.GuildCategory, bases.UniqueObjectT, unset.Unset] = unset.UNSET, + reason: typing.Union[str, unset.Unset] = unset.UNSET, + ) -> channels.GuildVoiceChannel: + channel = await self._create_guild_channel( + guild, + name, + channels.ChannelType.GUILD_VOICE, + position=position, + nsfw=nsfw, + user_limit=user_limit, + bitrate=bitrate, + permission_overwrites=permission_overwrites, + category=category, + reason=reason, + ) + return typing.cast(channels.GuildVoiceChannel, channel) + + async def create_guild_category( + self, + guild: typing.Union[guilds.Guild, bases.UniqueObjectT], + name: str, + *, + position: typing.Union[int, unset.Unset] = unset.UNSET, + nsfw: typing.Union[bool, unset.Unset] = unset.UNSET, + permission_overwrites: typing.Union[typing.Sequence[channels.PermissionOverwrite], unset.Unset] = unset.UNSET, + reason: typing.Union[str, unset.Unset] = unset.UNSET, + ) -> channels.GuildCategory: + channel = await self._create_guild_channel( + guild, + name, + channels.ChannelType.GUILD_CATEGORY, + position=position, + nsfw=nsfw, + permission_overwrites=permission_overwrites, + reason=reason, + ) + return typing.cast(channels.GuildCategory, channel) + + async def _create_guild_channel( + self, + guild: typing.Union[guilds.Guild, bases.UniqueObjectT], + name: str, + type_: channels.ChannelType, + *, + position: typing.Union[int, unset.Unset] = unset.UNSET, + topic: typing.Union[str, unset.Unset] = unset.UNSET, + nsfw: typing.Union[bool, unset.Unset] = unset.UNSET, + bitrate: typing.Union[int, unset.Unset] = unset.UNSET, + user_limit: typing.Union[int, unset.Unset] = unset.UNSET, + rate_limit_per_user: typing.Union[int, unset.Unset] = unset.UNSET, + permission_overwrites: typing.Union[typing.Sequence[channels.PermissionOverwrite], unset.Unset] = unset.UNSET, + category: typing.Union[channels.GuildCategory, bases.UniqueObjectT, unset.Unset] = unset.UNSET, + reason: typing.Union[str, unset.Unset] = unset.UNSET, + ) -> channels.GuildChannel: + route = routes.POST_GUILD_CHANNELS.compile(guild=conversions.value_to_snowflake(guild)) + payload = {"type": type_, "name": name} + conversions.put_if_specified(payload, "position", position) + conversions.put_if_specified(payload, "topic", topic) + conversions.put_if_specified(payload, "nsfw", nsfw) + conversions.put_if_specified(payload, "bitrate", bitrate) + conversions.put_if_specified(payload, "user_limit", user_limit) + conversions.put_if_specified(payload, "rate_limit_per_user", rate_limit_per_user) + conversions.put_if_specified(payload, "category", category, conversions.value_to_snowflake) + + if not unset.is_unset(permission_overwrites): + payload["permission_overwrites"] = [ + self._app.entity_factory.serialize_permission_overwrite(o) for o in permission_overwrites + ] + + response = await self._request(route, body=payload, reason=reason) + channel = self._app.entity_factory.deserialize_channel(response) + return typing.cast(channels.GuildChannel, channel) + + async def reposition_guild_channels( + self, + guild: typing.Union[guilds.Guild, bases.UniqueObjectT], + positions: typing.Mapping[int, typing.Union[channels.GuildChannel, bases.UniqueObjectT]], + ) -> None: + route = routes.POST_GUILD_CHANNELS.compile(guild=conversions.value_to_snowflake(guild)) + payload = [ + {"id": conversions.value_to_snowflake(channel), "position": pos} for pos, channel in positions.items() + ] + await self._request(route, body=payload) + + async def fetch_member( + self, + guild: typing.Union[guilds.Guild, bases.UniqueObjectT], + user: typing.Union[users.User, bases.UniqueObjectT], + ) -> guilds.GuildMember: + route = routes.GET_GUILD_MEMBER.compile( + guild=conversions.value_to_snowflake(guild), user=conversions.value_to_snowflake(user) + ) + response = await self._request(route) + return self._app.entity_factory.deserialize_guild_member(response) + + def fetch_members( + self, guild: typing.Union[guilds.Guild, bases.UniqueObjectT], + ) -> iterators.LazyIterator[guilds.GuildMember]: + return iterators.MemberIterator(self._app, self._request, conversions.value_to_snowflake(guild)) + + async def edit_member( + self, + guild: typing.Union[guilds.Guild, bases.UniqueObjectT], + user: typing.Union[users.User, bases.UniqueObjectT], + *, + nick: typing.Union[unset.Unset, str] = unset.UNSET, + roles: typing.Union[ + unset.Unset, typing.Collection[typing.Union[guilds.Role, bases.UniqueObjectT]] + ] = unset.UNSET, + mute: typing.Union[unset.Unset, bool] = unset.UNSET, + deaf: typing.Union[unset.Unset, bool] = unset.UNSET, + voice_channel: typing.Union[unset.Unset, channels.GuildVoiceChannel, bases.UniqueObjectT, None] = unset.UNSET, + reason: typing.Union[str, unset.Unset] = unset.UNSET, + ) -> None: + route = routes.PATCH_GUILD_MEMBER.compile( + guild=conversions.value_to_snowflake(guild), user=conversions.value_to_snowflake(user) + ) + payload = {} + conversions.put_if_specified(payload, "nick", nick) + conversions.put_if_specified(payload, "mute", mute) + conversions.put_if_specified(payload, "deaf", deaf) + + if voice_channel is None: + payload["channel_id"] = None + elif not unset.is_unset(voice_channel): + payload["channel_id"] = conversions.value_to_snowflake(voice_channel) + + if not unset.is_unset(roles): + payload["roles"] = [conversions.value_to_snowflake(r) for r in roles] + + await self._request(route, body=payload, reason=reason) + + async def edit_my_nick( + self, + guild: typing.Union[guilds.Guild, bases.UniqueObjectT], + nick: typing.Optional[str], + *, + reason: typing.Union[unset.Unset, str] = unset.UNSET, + ) -> None: + route = routes.PATCH_MY_GUILD_NICKNAME.compile(guild=conversions.value_to_snowflake(guild)) + payload = {"nick": nick} + await self._request(route, body=payload, reason=reason) + + async def add_role_to_member( + self, + guild: typing.Union[guilds.Guild, bases.UniqueObjectT], + user: typing.Union[users.User, bases.UniqueObjectT], + role: typing.Union[guilds.Role, bases.UniqueObjectT], + *, + reason: typing.Union[unset.Unset, str] = unset.UNSET, + ) -> None: + route = routes.PUT_GUILD_MEMBER_ROLE.compile( + guild=conversions.value_to_snowflake(guild), + user=conversions.value_to_snowflake(user), + role=conversions.value_to_snowflake(role), + ) + await self._request(route, reason=reason) + + async def remove_role_from_member( + self, + guild: typing.Union[guilds.Guild, bases.UniqueObjectT], + user: typing.Union[users.User, bases.UniqueObjectT], + role: typing.Union[guilds.Role, bases.UniqueObjectT], + *, + reason: typing.Union[unset.Unset, str] = unset.UNSET, + ) -> None: + route = routes.DELETE_GUILD_MEMBER_ROLE.compile( + guild=conversions.value_to_snowflake(guild), + user=conversions.value_to_snowflake(user), + role=conversions.value_to_snowflake(role), + ) + await self._request(route, reason=reason) + + async def kick_member( + self, + guild: typing.Union[guilds.Guild, bases.UniqueObjectT], + user: typing.Union[users.User, bases.UniqueObjectT], + *, + reason: typing.Union[unset.Unset, str] = unset.UNSET, + ) -> None: + route = routes.DELETE_GUILD_MEMBER.compile( + guild=conversions.value_to_snowflake(guild), user=conversions.value_to_snowflake(user), + ) + await self._request(route, reason=reason) + + async def ban_user( + self, + guild: typing.Union[guilds.Guild, bases.UniqueObjectT], + user: typing.Union[users.User, bases.UniqueObjectT], + *, + reason: typing.Union[unset.Unset, str] = unset.UNSET, + ) -> None: + route = routes.PUT_GUILD_BAN.compile( + guild=conversions.value_to_snowflake(guild), user=conversions.value_to_snowflake(user), + ) + await self._request(route, reason=reason) + + async def unban_user( + self, + guild: typing.Union[guilds.Guild, bases.UniqueObjectT], + user: typing.Union[users.User, bases.UniqueObjectT], + *, + reason: typing.Union[unset.Unset, str] = unset.UNSET, + ) -> None: + route = routes.DELETE_GUILD_BAN.compile( + guild=conversions.value_to_snowflake(guild), user=conversions.value_to_snowflake(user), + ) + await self._request(route, reason=reason) + + async def fetch_ban( + self, + guild: typing.Union[guilds.Guild, bases.UniqueObjectT], + user: typing.Union[users.User, bases.UniqueObjectT], + ) -> guilds.GuildMemberBan: + route = routes.GET_GUILD_BAN.compile( + guild=conversions.value_to_snowflake(guild), user=conversions.value_to_snowflake(user), + ) + response = await self._request(route) + return self._app.entity_factory.deserialize_guild_member_ban(response) + + async def fetch_bans( + self, guild: typing.Union[guilds.Guild, bases.UniqueObjectT], / + ) -> typing.Sequence[guilds.GuildMemberBan]: + route = routes.GET_GUILD_BANS.compile(guild=conversions.value_to_snowflake(guild)) + response = await self._request(route) + return [self._app.entity_factory.deserialize_guild_member_ban(b) for b in response] + + async def fetch_roles( + self, guild: typing.Union[guilds.Guild, bases.UniqueObjectT], / + ) -> typing.Sequence[guilds.Role]: + route = routes.GET_GUILD_ROLES.compile(guild=conversions.value_to_snowflake(guild)) + response = await self._request(route) + return [self._app.entity_factory.deserialize_guild_role(r) for r in response] + create_role = NotImplemented reposition_roles = NotImplemented edit_role = NotImplemented diff --git a/hikari/net/rest_utils.py b/hikari/net/rest_utils.py index 465ed508a3..9fbff619db 100644 --- a/hikari/net/rest_utils.py +++ b/hikari/net/rest_utils.py @@ -40,7 +40,7 @@ from hikari.models import files from hikari.models import guilds from hikari.models import permissions as permissions_ -from hikari.models import unset +from hikari.internal import unset from hikari.net import routes if typing.TYPE_CHECKING: diff --git a/hikari/stateless/__init__.py b/hikari/stateless/__init__.py deleted file mode 100644 index 5e0dd2c85c..0000000000 --- a/hikari/stateless/__init__.py +++ /dev/null @@ -1,32 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# Copyright © Nekoka.tt 2019-2020 -# -# This file is part of Hikari. -# -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . -"""Provides the internal framework for processing the lifetime of a bot. - -The API for this part of the framework has been split into groups of -abstract base classes, and corresponding implementations. This allows -several key application to be implemented separately, in case you have a -specific use case you want to provide (such as placing stuff on a message -queue if you distribute your bot). -""" - -from __future__ import annotations - -from .bot import * - -__all__ = bot.__all__ diff --git a/hikari/stateless/bot.py b/hikari/stateless/bot.py deleted file mode 100644 index e66ed9dd07..0000000000 --- a/hikari/stateless/bot.py +++ /dev/null @@ -1,60 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# Copyright © Nekoka.tt 2019-2020 -# -# This file is part of Hikari. -# -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . -"""Stateless bot implementation.""" - -from __future__ import annotations - -__all__ = ["StatelessBot"] - -import typing - -from hikari import application -from hikari.net.gateway import intent_aware_dispatchers -from . import manager -from ..net import gateway -from ..net import rest - -if typing.TYPE_CHECKING: - from hikari import application - from hikari import http_settings - - -class StatelessBot(application.Application): - """Bot client without any state internals. - - This is the most basic type of bot you can create. - """ - - @staticmethod - def _create_shard(app: application.Application, shard_id: int, shard_count: int, url: str) -> gateway.Gateway: - return gateway.Gateway(app=app, shard_id=shard_id, shard_count=shard_count, url=url) - - @staticmethod - def _create_rest(app: application.Application) -> rest.RESTClient: - return rest.RESTClient(app) - - @staticmethod - def _create_event_manager(app: application.Application) -> manager.StatelessEventManagerImpl: - return manager.StatelessEventManagerImpl(app) - - @staticmethod - def _create_event_dispatcher( - config: http_settings.BotConfig, - ) -> intent_aware_dispatchers.IntentAwareEventDispatcherImpl: - return intent_aware_dispatchers.IntentAwareEventDispatcherImpl(config.intents) diff --git a/hikari/stateless/manager.py b/hikari/stateless/manager.py deleted file mode 100644 index 527781901f..0000000000 --- a/hikari/stateless/manager.py +++ /dev/null @@ -1,278 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# Copyright © Nekoka.tt 2019-2020 -# -# This file is part of Hikari. -# -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . -"""Event management for stateless bots.""" - -from __future__ import annotations - -__all__ = ["StatelessEventManagerImpl"] - -from hikari.events import channel -from hikari.events import guild -from hikari.events import message -from hikari.events import other - -# pylint: disable=too-many-public-methods -from hikari.events import voice -from hikari.net.gateway import dispatchers -from hikari.net.gateway import event_managers - - -class StatelessEventManagerImpl(event_managers.EventManager[dispatchers.EventDispatcher]): - """Stateless event manager implementation for stateless bots. - - This is an implementation that does not rely on querying prior information to - operate. The implementation details of this are much simpler than a stateful - version, and are not immediately affected by the use of intents. - """ - - @event_managers.raw_event_mapper("CONNECTED") - async def on_connect(self, shard, _) -> None: - """Handle CONNECTED events.""" - event = other.ConnectedEvent(shard=shard) - await self._app.event_dispatcher.dispatch_event(event) - - @event_managers.raw_event_mapper("DISCONNECTED") - async def on_disconnect(self, shard, _) -> None: - """Handle DISCONNECTED events.""" - event = other.DisconnectedEvent(shard=shard) - await self._app.event_dispatcher.dispatch_event(event) - - @event_managers.raw_event_mapper("RESUME") - async def on_resume(self, shard, _) -> None: - """Handle RESUME events.""" - event = other.ResumedEvent(shard=shard) - await self._app.event_dispatcher.dispatch_event(event) - - @event_managers.raw_event_mapper("READY") - async def on_ready(self, _, payload) -> None: - """Handle READY events.""" - event = other.ReadyEvent.deserialize(payload, app=self._app) - await self._app.event_dispatcher.dispatch_event(event) - - @event_managers.raw_event_mapper("CHANNEL_CREATE") - async def on_channel_create(self, _, payload) -> None: - """Handle CHANNEL_CREATE events.""" - event = channel.ChannelCreateEvent.deserialize(payload, app=self._app) - await self._app.event_dispatcher.dispatch_event(event) - - @event_managers.raw_event_mapper("CHANNEL_UPDATE") - async def on_channel_update(self, _, payload) -> None: - """Handle CHANNEL_UPDATE events.""" - event = channel.ChannelUpdateEvent.deserialize(payload, app=self._app) - await self._app.event_dispatcher.dispatch_event(event) - - @event_managers.raw_event_mapper("CHANNEL_DELETE") - async def on_channel_delete(self, _, payload) -> None: - """Handle CHANNEL_DELETE events.""" - event = channel.ChannelDeleteEvent.deserialize(payload, app=self._app) - await self._app.event_dispatcher.dispatch_event(event) - - @event_managers.raw_event_mapper("CHANNEL_PINS_UPDATE") - async def on_channel_pins_update(self, _, payload) -> None: - """Handle CHANNEL_PINS_UPDATE events.""" - event = channel.ChannelPinsUpdateEvent.deserialize(payload, app=self._app) - await self._app.event_dispatcher.dispatch_event(event) - - @event_managers.raw_event_mapper("GUILD_CREATE") - async def on_guild_create(self, _, payload) -> None: - """Handle GUILD_CREATE events.""" - event = guild.GuildCreateEvent.deserialize(payload, app=self._app) - await self._app.event_dispatcher.dispatch_event(event) - - @event_managers.raw_event_mapper("GUILD_UPDATE") - async def on_guild_update(self, _, payload) -> None: - """Handle GUILD_UPDATE events.""" - event = guild.GuildUpdateEvent.deserialize(payload, app=self._app) - await self._app.event_dispatcher.dispatch_event(event) - - @event_managers.raw_event_mapper("GUILD_DELETE") - async def on_guild_delete(self, _, payload) -> None: - """Handle GUILD_DELETE events.""" - if payload.get("unavailable", False): - event = guild.GuildUnavailableEvent.deserialize(payload, app=self._app) - else: - event = guild.GuildLeaveEvent.deserialize(payload, app=self._app) - await self._app.event_dispatcher.dispatch_event(event) - - @event_managers.raw_event_mapper("GUILD_BAN_ADD") - async def on_guild_ban_add(self, _, payload) -> None: - """Handle GUILD_BAN_ADD events.""" - event = guild.GuildBanAddEvent.deserialize(payload, app=self._app) - await self._app.event_dispatcher.dispatch_event(event) - - @event_managers.raw_event_mapper("GUILD_BAN_REMOVE") - async def on_guild_ban_remove(self, _, payload) -> None: - """Handle GUILD_BAN_REMOVE events.""" - event = guild.GuildBanRemoveEvent.deserialize(payload, app=self._app) - await self._app.event_dispatcher.dispatch_event(event) - - @event_managers.raw_event_mapper("GUILD_EMOJIS_UPDATE") - async def on_guild_emojis_update(self, _, payload) -> None: - """Handle GUILD_EMOJIS_UPDATE events.""" - event = guild.GuildEmojisUpdateEvent.deserialize(payload, app=self._app) - await self._app.event_dispatcher.dispatch_event(event) - - @event_managers.raw_event_mapper("GUILD_INTEGRATIONS_UPDATE") - async def on_guild_integrations_update(self, _, payload) -> None: - """Handle GUILD_INTEGRATIONS_UPDATE events.""" - event = guild.GuildIntegrationsUpdateEvent.deserialize(payload, app=self._app) - await self._app.event_dispatcher.dispatch_event(event) - - @event_managers.raw_event_mapper("GUILD_MEMBER_ADD") - async def on_guild_member_add(self, _, payload) -> None: - """Handle GUILD_MEMBER_ADD events.""" - event = guild.GuildMemberAddEvent.deserialize(payload, app=self._app) - await self._app.event_dispatcher.dispatch_event(event) - - @event_managers.raw_event_mapper("GUILD_MEMBER_UPDATE") - async def on_guild_member_update(self, _, payload) -> None: - """Handle GUILD_MEMBER_UPDATE events.""" - event = guild.GuildMemberUpdateEvent.deserialize(payload, app=self._app) - await self._app.event_dispatcher.dispatch_event(event) - - @event_managers.raw_event_mapper("GUILD_MEMBER_REMOVE") - async def on_guild_member_remove(self, _, payload) -> None: - """Handle GUILD_MEMBER_REMOVE events.""" - event = guild.GuildMemberRemoveEvent.deserialize(payload, app=self._app) - await self._app.event_dispatcher.dispatch_event(event) - - @event_managers.raw_event_mapper("GUILD_ROLE_CREATE") - async def on_guild_role_create(self, _, payload) -> None: - """Handle GUILD_ROLE_CREATE events.""" - event = guild.GuildRoleCreateEvent.deserialize(payload, app=self._app) - await self._app.event_dispatcher.dispatch_event(event) - - @event_managers.raw_event_mapper("GUILD_ROLE_UPDATE") - async def on_guild_role_update(self, _, payload) -> None: - """Handle GUILD_ROLE_UPDATE events.""" - event = guild.GuildRoleUpdateEvent.deserialize(payload, app=self._app) - await self._app.event_dispatcher.dispatch_event(event) - - @event_managers.raw_event_mapper("GUILD_ROLE_DELETE") - async def on_guild_role_delete(self, _, payload) -> None: - """Handle GUILD_ROLE_DELETE events.""" - event = guild.GuildRoleDeleteEvent.deserialize(payload, app=self._app) - await self._app.event_dispatcher.dispatch_event(event) - - @event_managers.raw_event_mapper("INVITE_CREATE") - async def on_invite_create(self, _, payload) -> None: - """Handle INVITE_CREATE events.""" - event = channel.InviteCreateEvent.deserialize(payload, app=self._app) - await self._app.event_dispatcher.dispatch_event(event) - - @event_managers.raw_event_mapper("INVITE_DELETE") - async def on_invite_delete(self, _, payload) -> None: - """Handle INVITE_DELETE events.""" - event = channel.InviteDeleteEvent.deserialize(payload, app=self._app) - await self._app.event_dispatcher.dispatch_event(event) - - @event_managers.raw_event_mapper("MESSAGE_CREATE") - async def on_message_create(self, _, payload) -> None: - """Handle MESSAGE_CREATE events.""" - # For consistency's sake and to keep Member.user as a non-nullable field, here we inject the attached user - # payload into the member payload when the member payload is present as discord decided not to duplicate the - # user object between Message.author and Message.member.user - if "member" in payload: - payload["member"]["user"] = payload["author"] - event = message.MessageCreateEvent.deserialize(payload, app=self._app) - await self._app.event_dispatcher.dispatch_event(event) - - @event_managers.raw_event_mapper("MESSAGE_UPDATE") - async def on_message_update(self, _, payload) -> None: - """Handle MESSAGE_UPDATE events.""" - # For consistency's sake and to keep Member.user as a non-nullable field, here we inject the attached user - # payload into the member payload when the member payload is present as discord decided not to duplicate the - # user object between Message.author and Message.member.user - if "member" in payload and "author" in payload: - payload["member"]["user"] = payload["author"] - event = message.MessageUpdateEvent.deserialize(payload, app=self._app) - await self._app.event_dispatcher.dispatch_event(event) - - @event_managers.raw_event_mapper("MESSAGE_DELETE") - async def on_message_delete(self, _, payload) -> None: - """Handle MESSAGE_DELETE events.""" - event = message.MessageDeleteEvent.deserialize(payload, app=self._app) - await self._app.event_dispatcher.dispatch_event(event) - - @event_managers.raw_event_mapper("MESSAGE_DELETE_BULK") - async def on_message_delete_bulk(self, _, payload) -> None: - """Handle MESSAGE_DELETE_BULK events.""" - event = message.MessageDeleteBulkEvent.deserialize(payload, app=self._app) - await self._app.event_dispatcher.dispatch_event(event) - - @event_managers.raw_event_mapper("MESSAGE_REACTION_ADD") - async def on_message_reaction_add(self, _, payload) -> None: - """Handle MESSAGE_REACTION_ADD events.""" - event = message.MessageReactionAddEvent.deserialize(payload, app=self._app) - await self._app.event_dispatcher.dispatch_event(event) - - @event_managers.raw_event_mapper("MESSAGE_REACTION_REMOVE") - async def on_message_reaction_remove(self, _, payload) -> None: - """Handle MESSAGE_REACTION_REMOVE events.""" - payload["emoji"].setdefault("animated", None) - - event = message.MessageReactionRemoveEvent.deserialize(payload, app=self._app) - await self._app.event_dispatcher.dispatch_event(event) - - @event_managers.raw_event_mapper("MESSAGE_REACTION_REMOVE_EMOJI") - async def on_message_reaction_remove_emoji(self, _, payload) -> None: - """Handle MESSAGE_REACTION_REMOVE_EMOJI events.""" - payload["emoji"].setdefault("animated", None) - - event = message.MessageReactionRemoveEmojiEvent.deserialize(payload, app=self._app) - await self._app.event_dispatcher.dispatch_event(event) - - @event_managers.raw_event_mapper("PRESENCE_UPDATE") - async def on_presence_update(self, _, payload) -> None: - """Handle PRESENCE_UPDATE events.""" - event = guild.PresenceUpdateEvent.deserialize(payload, app=self._app) - await self._app.event_dispatcher.dispatch_event(event) - - @event_managers.raw_event_mapper("TYPING_START") - async def on_typing_start(self, _, payload) -> None: - """Handle TYPING_START events.""" - event = channel.TypingStartEvent.deserialize(payload, app=self._app) - await self._app.event_dispatcher.dispatch_event(event) - - @event_managers.raw_event_mapper("USER_UPDATE") - async def on_my_user_update(self, _, payload) -> None: - """Handle USER_UPDATE events.""" - event = other.MyUserUpdateEvent.deserialize(payload, app=self._app) - await self._app.event_dispatcher.dispatch_event(event) - - @event_managers.raw_event_mapper("VOICE_STATE_UPDATE") - async def on_voice_state_update(self, _, payload) -> None: - """Handle VOICE_STATE_UPDATE events.""" - event = voice.VoiceStateUpdateEvent.deserialize(payload, app=self._app) - await self._app.event_dispatcher.dispatch_event(event) - - @event_managers.raw_event_mapper("VOICE_SERVER_UPDATE") - async def on_voice_server_update(self, _, payload) -> None: - """Handle VOICE_SERVER_UPDATE events.""" - event = voice.VoiceServerUpdateEvent.deserialize(payload, app=self._app) - await self._app.event_dispatcher.dispatch_event(event) - - @event_managers.raw_event_mapper("WEBHOOK_UPDATE") - async def on_webhook_update(self, _, payload) -> None: - """Handle WEBHOOK_UPDATE events.""" - event = channel.WebhookUpdateEvent.deserialize(payload, app=self._app) - await self._app.event_dispatcher.dispatch_event(event) - - -# pylint: enable=too-many-public-methods diff --git a/tests/hikari/models/test_unset.py b/tests/hikari/internal/test_unset.py similarity index 97% rename from tests/hikari/models/test_unset.py rename to tests/hikari/internal/test_unset.py index 8dc6dcb135..429596e102 100644 --- a/tests/hikari/models/test_unset.py +++ b/tests/hikari/internal/test_unset.py @@ -18,7 +18,7 @@ # along with Hikari. If not, see . import pytest -from hikari.models import unset +from hikari.internal import unset from tests.hikari import _helpers diff --git a/tests/hikari/models/test_guilds.py b/tests/hikari/models/test_guilds.py index 81975c1b33..2f7dae2b65 100644 --- a/tests/hikari/models/test_guilds.py +++ b/tests/hikari/models/test_guilds.py @@ -30,7 +30,7 @@ from hikari.models import emojis from hikari.models import guilds from hikari.models import permissions -from hikari.models import unset +from hikari.internal import unset from hikari.models import users from tests.hikari import _helpers From 48fc9461ab5aa27bdf3989a3543311e6c7bf62b3 Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Mon, 25 May 2020 18:31:20 +0100 Subject: [PATCH 393/922] Fixed member pagination bugs, fixed bugs with image data, added more guild endpoints. --- hikari/.rest/guild.py | 4 +- hikari/entity_factory.py | 2 +- hikari/impl/entity_factory.py | 2 +- hikari/internal/unset.py | 4 + hikari/models/audit_logs.py | 3 - hikari/net/iterators.py | 4 +- hikari/net/rest.py | 218 ++++++++++++++++++----------- hikari/net/rest_utils.py | 3 + tests/hikari/models/test_guilds.py | 4 +- 9 files changed, 152 insertions(+), 92 deletions(-) diff --git a/hikari/.rest/guild.py b/hikari/.rest/guild.py index d66a251e94..62b8e351fa 100644 --- a/hikari/.rest/guild.py +++ b/hikari/.rest/guild.py @@ -306,7 +306,7 @@ async def create_guild_emoji( If any invalid snowflake IDs are passed; a snowflake may be invalid due to it being outside of the range of a 64 bit integer. """ - payload = await self._session.create_guild_emoji( + payload = await self._session.create_emoji( guild_id=str(guild.id if isinstance(guild, bases.Unique) else int(guild)), name=name, image=await image.read(), @@ -397,7 +397,7 @@ async def delete_guild_emoji( If you either lack the `MANAGE_EMOJIS` permission or aren't a member of said guild. """ - await self._session.delete_guild_emoji( + await self._session.delete_emoji( guild_id=str(guild.id if isinstance(guild, bases.Unique) else int(guild)), emoji_id=str(emoji.id if isinstance(emoji, bases.Unique) else int(emoji)), ) diff --git a/hikari/entity_factory.py b/hikari/entity_factory.py index 3de7cb7ab6..c5478d22b4 100644 --- a/hikari/entity_factory.py +++ b/hikari/entity_factory.py @@ -175,7 +175,7 @@ def deserialize_guild_member( ... @abc.abstractmethod - def deserialize_guild_role(self, payload: more_typing.JSONObject) -> guilds.Role: + def deserialize_role(self, payload: more_typing.JSONObject) -> guilds.Role: ... @abc.abstractmethod diff --git a/hikari/impl/entity_factory.py b/hikari/impl/entity_factory.py index 0a9aa66677..8d27af8103 100644 --- a/hikari/impl/entity_factory.py +++ b/hikari/impl/entity_factory.py @@ -113,7 +113,7 @@ def deserialize_guild_member( ) -> guilds.GuildMember: pass - def deserialize_guild_role(self, payload: more_typing.JSONObject) -> guilds.Role: + def deserialize_role(self, payload: more_typing.JSONObject) -> guilds.Role: pass def deserialize_guild_member_presence(self, payload: more_typing.JSONObject) -> guilds.GuildMemberPresence: diff --git a/hikari/internal/unset.py b/hikari/internal/unset.py index 8876dfee6c..f26289b55a 100644 --- a/hikari/internal/unset.py +++ b/hikari/internal/unset.py @@ -69,3 +69,7 @@ def is_unset(obj: typing.Any) -> typing.Literal[False]: def is_unset(obj): """Return `True` if the object is an `Unset` value.""" return isinstance(obj, Unset) + + +def count_unset_objects(obj1: typing.Any, obj2: typing.Any, *objs: typing.Any) -> int: + return sum(is_unset(o) for o in (obj1, obj2, *objs)) diff --git a/hikari/models/audit_logs.py b/hikari/models/audit_logs.py index 6a4d81a5b9..5eb44faf4c 100644 --- a/hikari/models/audit_logs.py +++ b/hikari/models/audit_logs.py @@ -26,7 +26,6 @@ "AuditLogChangeKey", "AuditLogEntry", "AuditLogEventType", - "AuditLogIterator", "BaseAuditLogEntryInfo", "ChannelOverwriteEntryInfo", "get_entry_info_entity", @@ -40,7 +39,6 @@ ] import abc -import copy import datetime import typing @@ -59,7 +57,6 @@ from . import webhooks as webhooks_ if typing.TYPE_CHECKING: - from hikari import application from hikari.internal import more_typing diff --git a/hikari/net/iterators.py b/hikari/net/iterators.py index e950d979ba..663707279a 100644 --- a/hikari/net/iterators.py +++ b/hikari/net/iterators.py @@ -345,12 +345,12 @@ def __init__( self._first_id = bases.Snowflake.min() async def _next_chunk(self) -> typing.Optional[typing.Generator[guilds.GuildMember, typing.Any, None]]: - chunk = await self._request_call(self._route, query={"after": self._first_id}) + chunk = await self._request_call(self._route, query={"after": self._first_id, "limit": 100}) if not chunk: return None - self._first_id = chunk[-1]["id"] + self._first_id = chunk[-1]["user"]["id"] return (self._app.entity_factory.deserialize_guild_member(m) for m in chunk) diff --git a/hikari/net/rest.py b/hikari/net/rest.py index 223b822160..434b2f1b5d 100644 --- a/hikari/net/rest.py +++ b/hikari/net/rest.py @@ -32,15 +32,16 @@ from hikari import errors from hikari import http_settings from hikari import rest_app -from hikari.models import audit_logs -from hikari.net import iterators from hikari.internal import conversions from hikari.internal import more_collections from hikari.internal import more_typing from hikari.internal import ratelimits from hikari.internal import unset +from hikari.models import audit_logs +from hikari.models import colors from hikari.net import buckets from hikari.net import http_client +from hikari.net import iterators from hikari.net import rest_utils from hikari.net import routes @@ -55,7 +56,7 @@ from hikari.models import guilds from hikari.models import invites from hikari.models import messages - from hikari.models import permissions + from hikari.models import permissions as permissions_ from hikari.models import users from hikari.models import voices from hikari.models import webhooks @@ -318,7 +319,8 @@ async def close(self) -> None: async def fetch_channel( self, channel: typing.Union[channels.PartialChannel, bases.Snowflake, int], /, ) -> channels.PartialChannel: - response = await self._request(routes.GET_CHANNEL.compile(channel=conversions.value_to_snowflake(channel))) + route = routes.GET_CHANNEL.compile(channel=conversions.value_to_snowflake(channel)) + response = await self._request(route) return self._app.entity_factory.deserialize_channel(response) async def edit_channel( @@ -337,6 +339,7 @@ async def edit_channel( parent_category: typing.Union[unset.Unset, channels.GuildCategory] = unset.UNSET, reason: typing.Union[unset.Unset, str] = unset.UNSET, ) -> channels.PartialChannel: + route = routes.PATCH_CHANNEL.compile(channel=conversions.value_to_snowflake(channel)) payload = {} conversions.put_if_specified(payload, "name", name) conversions.put_if_specified(payload, "position", position) @@ -352,48 +355,46 @@ async def edit_channel( self._app.entity_factory.serialize_permission_overwrite(p) for p in permission_overwrites ] - response = await self._request( - routes.PATCH_CHANNEL.compile(channel=conversions.value_to_snowflake(channel)), body=payload, reason=reason, - ) - + response = await self._request(route, body=payload, reason=reason,) return self._app.entity_factory.deserialize_channel(response) async def delete_channel(self, channel: typing.Union[channels.PartialChannel, bases.Snowflake, int], /) -> None: - await self._request(routes.DELETE_CHANNEL.compile(channel=conversions.value_to_snowflake(channel))) + route = routes.DELETE_CHANNEL.compile(channel=conversions.value_to_snowflake(channel)) + await self._request(route) @typing.overload - async def edit_channel_permissions( + async def edit_permission_overwrites( self, channel: typing.Union[channels.GuildChannel, bases.UniqueObjectT], target: typing.Union[channels.PermissionOverwrite, users.User, guilds.Role], *, - allow: typing.Union[unset.Unset, permissions.Permission] = unset.UNSET, - deny: typing.Union[unset.Unset, permissions.Permission] = unset.UNSET, + allow: typing.Union[unset.Unset, permissions_.Permission] = unset.UNSET, + deny: typing.Union[unset.Unset, permissions_.Permission] = unset.UNSET, reason: typing.Union[unset.Unset, str] = unset.UNSET, ) -> None: ... @typing.overload - async def edit_channel_permissions( + async def edit_permission_overwrites( self, channel: typing.Union[channels.GuildChannel, bases.UniqueObjectT], target: typing.Union[int, str, bases.Snowflake], target_type: typing.Union[channels.PermissionOverwriteType, str], *, - allow: typing.Union[unset.Unset, permissions.Permission] = unset.UNSET, - deny: typing.Union[unset.Unset, permissions.Permission] = unset.UNSET, + allow: typing.Union[unset.Unset, permissions_.Permission] = unset.UNSET, + deny: typing.Union[unset.Unset, permissions_.Permission] = unset.UNSET, reason: typing.Union[unset.Unset, str] = unset.UNSET, ) -> None: ... - async def edit_channel_permissions( + async def edit_permission_overwrites( self, channel: typing.Union[channels.GuildChannel, bases.UniqueObjectT], target: typing.Union[bases.UniqueObjectT, users.User, guilds.Role, channels.PermissionOverwrite], target_type: typing.Union[unset.Unset, channels.PermissionOverwriteType, str] = unset.UNSET, *, - allow: typing.Union[unset.Unset, permissions.Permission] = unset.UNSET, - deny: typing.Union[unset.Unset, permissions.Permission] = unset.UNSET, + allow: typing.Union[unset.Unset, permissions_.Permission] = unset.UNSET, + deny: typing.Union[unset.Unset, permissions_.Permission] = unset.UNSET, reason: typing.Union[unset.Unset, str] = unset.UNSET, ) -> None: if unset.is_unset(target_type): @@ -417,7 +418,7 @@ async def edit_channel_permissions( await self._request(route, body=payload, reason=reason) - async def delete_channel_permission( + async def delete_permission_overwrite( self, channel: typing.Union[channels.GuildChannel, bases.UniqueObjectT], target: typing.Union[channels.PermissionOverwrite, guilds.Role, users.User, bases.UniqueObjectT], @@ -470,7 +471,7 @@ async def fetch_pins( response = await self._request(route) return conversions.json_to_collection(response, self._app.entity_factory.deserialize_message) - async def create_pinned_message( + async def pin_message( self, channel: typing.Union[channels.TextChannel, bases.UniqueObjectT], message: typing.Union[messages.Message, bases.UniqueObjectT], @@ -480,7 +481,7 @@ async def create_pinned_message( ) await self._request(route) - async def delete_pinned_message( + async def unpin_message( self, channel: typing.Union[channels.TextChannel, bases.UniqueObjectT], message: typing.Union[messages.Message, bases.UniqueObjectT], @@ -644,10 +645,11 @@ async def create_reaction( message: typing.Union[messages.Message, bases.UniqueObjectT], emoji: typing.Union[str, emojis.UnicodeEmoji, emojis.KnownCustomEmoji], ) -> None: - emoji = emoji.url_name if isinstance(emoji, emojis.KnownCustomEmoji) else str(emoji) - channel = conversions.value_to_snowflake(channel) - message = conversions.value_to_snowflake(message) - route = routes.PUT_MY_REACTION.compile(channel=channel, message=message, emoji=emoji) + route = routes.PUT_MY_REACTION.compile( + emoji=emoji.url_name if isinstance(emoji, emojis.KnownCustomEmoji) else str(emoji), + channel=conversions.value_to_snowflake(channel), + message=conversions.value_to_snowflake(message), + ) await self._request(route) async def delete_my_reaction( @@ -656,10 +658,11 @@ async def delete_my_reaction( message: typing.Union[messages.Message, bases.UniqueObjectT], emoji: typing.Union[str, emojis.UnicodeEmoji, emojis.KnownCustomEmoji], ) -> None: - emoji = emoji.url_name if isinstance(emoji, emojis.KnownCustomEmoji) else str(emoji) - channel = conversions.value_to_snowflake(channel) - message = conversions.value_to_snowflake(message) - route = routes.DELETE_MY_REACTION.compile(channel=channel, message=message, emoji=emoji) + route = routes.DELETE_MY_REACTION.compile( + emoji=emoji.url_name if isinstance(emoji, emojis.KnownCustomEmoji) else str(emoji), + channel=conversions.value_to_snowflake(channel), + message=conversions.value_to_snowflake(message), + ) await self._request(route) async def delete_all_reactions_for_emoji( @@ -668,10 +671,11 @@ async def delete_all_reactions_for_emoji( message: typing.Union[messages.Message, bases.UniqueObjectT], emoji: typing.Union[str, emojis.UnicodeEmoji, emojis.KnownCustomEmoji], ) -> None: - emoji = emoji.url_name if isinstance(emoji, emojis.KnownCustomEmoji) else str(emoji) - channel = conversions.value_to_snowflake(channel) - message = conversions.value_to_snowflake(message) - route = routes.DELETE_REACTION_EMOJI.compile(channel=channel, message=message, emoji=emoji) + route = routes.DELETE_REACTION_EMOJI.compile( + emoji=emoji.url_name if isinstance(emoji, emojis.KnownCustomEmoji) else str(emoji), + channel=conversions.value_to_snowflake(channel), + message=conversions.value_to_snowflake(message), + ) await self._request(route) async def delete_reaction( @@ -681,11 +685,12 @@ async def delete_reaction( emoji: typing.Union[str, emojis.UnicodeEmoji, emojis.KnownCustomEmoji], user: typing.Union[users.User, bases.UniqueObjectT], ) -> None: - emoji = emoji.url_name if isinstance(emoji, emojis.KnownCustomEmoji) else str(emoji) - channel = conversions.value_to_snowflake(channel) - message = conversions.value_to_snowflake(message) - user = conversions.value_to_snowflake(user) - route = routes.DELETE_REACTION_USER.compile(channel=channel, message=message, emoji=emoji, user=user) + route = routes.DELETE_REACTION_USER.compile( + emoji=emoji.url_name if isinstance(emoji, emojis.KnownCustomEmoji) else str(emoji), + channel=conversions.value_to_snowflake(channel), + message=conversions.value_to_snowflake(message), + user=conversions.value_to_snowflake(user), + ) await self._request(route) async def delete_all_reactions( @@ -693,9 +698,10 @@ async def delete_all_reactions( channel: typing.Union[channels.TextChannel, bases.UniqueObjectT], message: typing.Union[messages.Message, bases.UniqueObjectT], ) -> None: - channel = conversions.value_to_snowflake(channel) - message = conversions.value_to_snowflake(message) - route = routes.DELETE_ALL_REACTIONS.compile(channel=channel, message=message) + route = routes.DELETE_ALL_REACTIONS.compile( + channel=conversions.value_to_snowflake(channel), message=conversions.value_to_snowflake(message), + ) + await self._request(route) def fetch_reactions_for_emoji( @@ -720,10 +726,10 @@ async def create_webhook( avatar: typing.Union[unset.Unset, files.BaseStream] = unset.UNSET, reason: typing.Union[unset.Unset, str] = unset.UNSET, ) -> webhooks.Webhook: + route = routes.POST_WEBHOOK.compile(channel=conversions.value_to_snowflake(channel)) payload = {"name": name} if not unset.is_unset(avatar): payload["avatar"] = await avatar.fetch_data_uri() - route = routes.POST_WEBHOOK.compile(channel=conversions.value_to_snowflake(channel)) response = await self._request(route, body=payload, reason=reason) return self._app.entity_factory.deserialize_webhook(response) @@ -766,12 +772,6 @@ async def edit_webhook( channel: typing.Union[unset.Unset, channels.TextChannel, bases.UniqueObjectT] = unset.UNSET, reason: typing.Union[unset.Unset, str] = unset.UNSET, ) -> webhooks.Webhook: - payload = {} - conversions.put_if_specified(payload, "name", name) - conversions.put_if_specified(payload, "channel", channel, conversions.value_to_snowflake) - if not unset.is_unset(avatar): - payload["avatar"] = await avatar.fetch_data_uri() - if unset.is_unset(token): route = routes.PATCH_WEBHOOK.compile(webhook=conversions.value_to_snowflake(webhook)) else: @@ -779,6 +779,12 @@ async def edit_webhook( webhook=conversions.value_to_snowflake(webhook), token=token ) + payload = {} + conversions.put_if_specified(payload, "name", name) + conversions.put_if_specified(payload, "channel", channel, conversions.value_to_snowflake) + if not unset.is_unset(avatar): + payload["avatar"] = await avatar.fetch_data_uri() + response = await self._request(route, body=payload, reason=reason) return self._app.entity_factory.deserialize_webhook(response) @@ -845,12 +851,14 @@ async def execute_embed( return self._app.entity_factory.deserialize_message(response) async def fetch_gateway_url(self) -> str: + route = routes.GET_GATEWAY.compile() # This doesn't need authorization. - response = await self._request(routes.GET_GATEWAY.compile(), no_auth=True) + response = await self._request(route, no_auth=True) return response["url"] async def fetch_recommended_gateway_settings(self) -> gateway.GatewayBot: - response = await self._request(routes.GET_GATEWAY_BOT.compile()) + route = routes.GET_GATEWAY_BOT.compile() + response = await self._request(route) return self._app.entity_factory.deserialize_gateway_bot(response) async def fetch_invite(self, invite: typing.Union[invites.Invite, str]) -> invites.Invite: @@ -864,7 +872,8 @@ async def delete_invite(self, invite: typing.Union[invites.Invite, str]) -> None await self._request(route) async def fetch_my_user(self) -> users.MyUser: - response = await self._request(routes.GET_MY_USER.compile()) + route = routes.GET_MY_USER.compile() + response = await self._request(route) return self._app.entity_factory.deserialize_my_user(response) async def edit_my_user( @@ -873,16 +882,17 @@ async def edit_my_user( username: typing.Union[unset.Unset, str] = unset.UNSET, avatar: typing.Union[unset.Unset, files.BaseStream] = unset.UNSET, ) -> users.MyUser: + route = routes.PATCH_MY_USER.compile() payload = {} conversions.put_if_specified(payload, "username", username) if not unset.is_unset(username): - payload["avatar"] = await avatar.read() - route = routes.PATCH_MY_USER.compile() + payload["avatar"] = await avatar.fetch_data_uri() response = await self._request(route, body=payload) return self._app.entity_factory.deserialize_my_user(response) async def fetch_my_connections(self) -> typing.Sequence[applications.OwnConnection]: - response = await self._request(routes.GET_MY_CONNECTIONS.compile()) + route = routes.GET_MY_CONNECTIONS.compile() + response = await self._request(route) return [self._app.entity_factory.deserialize_own_connection(c) for c in response] def fetch_my_guilds( @@ -910,7 +920,8 @@ async def create_dm_channel(self, user: typing.Union[users.User, bases.UniqueObj return self._app.entity_factory.deserialize_dm_channel(response) async def fetch_application(self) -> applications.Application: - response = await self._request(routes.GET_MY_APPLICATION.compile()) + route = routes.GET_MY_APPLICATION.compile() + response = await self._request(route) return self._app.entity_factory.deserialize_application(response) async def add_user_to_guild( @@ -929,22 +940,18 @@ async def add_user_to_guild( route = routes.PUT_GUILD_MEMBER.compile( guild=conversions.value_to_snowflake(guild), user=conversions.value_to_snowflake(user), ) - payload = {"access_token": access_token} conversions.put_if_specified(payload, "nick", nick) - conversions.put_if_specified( - payload, "roles", roles, lambda rs: [conversions.value_to_snowflake(r) for r in rs] - ) conversions.put_if_specified(payload, "mute", mute) conversions.put_if_specified(payload, "deaf", deaf) + if not unset.is_unset(roles): + payload["roles"] = [conversions.value_to_snowflake(r) for r in roles] - response = await self._request(route, body=payload) - - if response is None: + if (response := await self._request(route, body=payload)) is not None: + return self._app.entity_factory.deserialize_guild_member(response) + else: # User already is in the guild. return None - else: - return self._app.entity_factory.deserialize_guild_member(payload) async def fetch_voice_regions(self) -> typing.Sequence[voices.VoiceRegion]: route = routes.GET_VOICE_REGIONS.compile() @@ -956,7 +963,7 @@ async def fetch_user(self, user: typing.Union[users.User, bases.UniqueObjectT]) response = await self._request(route) return self._app.entity_factory.deserialize_user(response) - def fetch_guild_audit_log( + def fetch_audit_log( self, guild: typing.Union[guilds.Guild, bases.UniqueObjectT], /, @@ -978,7 +985,7 @@ def fetch_guild_audit_log( return iterators.AuditLogIterator(self._app, self._request, guild, before, user, event_type,) - async def fetch_guild_emoji( + async def fetch_emoji( self, guild: typing.Union[guilds.Guild, bases.UniqueObjectT], # This is an emoji ID, which is the URL-safe emoji name, not the snowflake alone. @@ -998,7 +1005,7 @@ async def fetch_guild_emojis( response = await self._request(route) return conversions.json_to_collection(response, self._app.entity_factory.deserialize_known_custom_emoji, set) - async def create_guild_emoji( + async def create_emoji( self, guild: typing.Union[guilds.Guild, bases.UniqueObjectT], name: str, @@ -1009,15 +1016,15 @@ async def create_guild_emoji( ] = unset.UNSET, reason: typing.Union[unset.Unset, str] = unset.UNSET, ) -> emojis.KnownCustomEmoji: - payload = {"name": name, "image": await image.read()} + route = routes.POST_GUILD_EMOJIS.compile(guild=conversions.value_to_snowflake(guild)) + payload = {"name": name, "image": await image.fetch_data_uri()} conversions.put_if_specified( payload, "roles", roles, lambda seq: [conversions.value_to_snowflake(r) for r in seq] ) - route = routes.POST_GUILD_EMOJIS.compile(guild=conversions.value_to_snowflake(guild)) response = await self._request(route, body=payload, reason=reason) return self._app.entity_factory.deserialize_known_custom_emoji(response) - async def edit_guild_emoji( + async def edit_emoji( self, guild: typing.Union[guilds.Guild, bases.UniqueObjectT], # This is an emoji ID, which is the URL-safe emoji name, not the snowflake alone. @@ -1029,19 +1036,19 @@ async def edit_guild_emoji( ] = unset.UNSET, reason: typing.Union[unset.Unset, str] = unset.UNSET, ) -> emojis.KnownCustomEmoji: + route = routes.PATCH_GUILD_EMOJI.compile( + guild=conversions.value_to_snowflake(guild), + emoji=emoji.url_name if isinstance(emoji, emojis.Emoji) else emoji, + ) payload = {} conversions.put_if_specified(payload, "name", name) conversions.put_if_specified( payload, "roles", roles, lambda seq: [conversions.value_to_snowflake(r) for r in seq] ) - route = routes.PATCH_GUILD_EMOJI.compile( - guild=conversions.value_to_snowflake(guild), - emoji=emoji.url_name if isinstance(emoji, emojis.Emoji) else emoji, - ) response = await self._request(route, body=payload, reason=reason) return self._app.entity_factory.deserialize_known_custom_emoji(response) - async def delete_guild_emoji( + async def delete_emoji( self, guild: typing.Union[guilds.Guild, bases.UniqueObjectT], # This is an emoji ID, which is the URL-safe emoji name, not the snowflake alone. @@ -1084,7 +1091,11 @@ async def edit_guild( icon: typing.Union[unset.Unset, files.BaseStream] = unset.UNSET, owner: typing.Union[unset.Unset, users.User, bases.UniqueObjectT] = unset.UNSET, splash: typing.Union[unset.Unset, files.BaseStream] = unset.UNSET, + banner: typing.Union[unset.Unset, files.BaseStream] = unset.UNSET, system_channel: typing.Union[unset.Unset, channels.GuildTextChannel] = unset.UNSET, + rules_channel: typing.Union[unset.Unset, channels.GuildTextChannel] = unset.UNSET, + public_updates_channel: typing.Union[unset.Unset, channels.GuildTextChannel] = unset.UNSET, + preferred_locale: typing.Union[unset.Unset, str] = unset.UNSET, reason: typing.Union[unset.Unset, str] = unset.UNSET, ) -> guilds.Guild: route = routes.PATCH_GUILD.compile(guild=conversions.value_to_snowflake(guild)) @@ -1098,12 +1109,20 @@ async def edit_guild( conversions.put_if_specified(payload, "afk_timeout", afk_timeout, conversions.timespan_to_int) conversions.put_if_specified(payload, "owner_id", owner, conversions.value_to_snowflake) conversions.put_if_specified(payload, "system_channel_id", system_channel, conversions.value_to_snowflake) + conversions.put_if_specified(payload, "rules_channel_id", rules_channel, conversions.value_to_snowflake) + conversions.put_if_specified( + payload, "public_updates_channel_id", public_updates_channel, conversions.value_to_snowflake + ) + conversions.put_if_specified(payload, "preferred_locale", preferred_locale, str) if not unset.is_unset(icon): - payload["icon"] = await icon.read() + payload["icon"] = await icon.fetch_data_uri() if not unset.is_unset(splash): - payload["splash"] = await splash.read() + payload["splash"] = await splash.fetch_data_uri() + + if not unset.is_unset(banner): + payload["banner"] = await banner.fetch_data_uri() response = await self._request(route, body=payload, reason=reason) return self._app.entity_factory.deserialize_guild(response) @@ -1258,7 +1277,7 @@ async def _create_guild_channel( channel = self._app.entity_factory.deserialize_channel(response) return typing.cast(channels.GuildChannel, channel) - async def reposition_guild_channels( + async def reposition_channels( self, guild: typing.Union[guilds.Guild, bases.UniqueObjectT], positions: typing.Mapping[int, typing.Union[channels.GuildChannel, bases.UniqueObjectT]], @@ -1417,10 +1436,47 @@ async def fetch_roles( ) -> typing.Sequence[guilds.Role]: route = routes.GET_GUILD_ROLES.compile(guild=conversions.value_to_snowflake(guild)) response = await self._request(route) - return [self._app.entity_factory.deserialize_guild_role(r) for r in response] + return [self._app.entity_factory.deserialize_role(r) for r in response] + + async def create_role( + self, + guild: typing.Union[guilds.Guild, bases.UniqueObjectT], + /, + *, + name: typing.Union[unset.Unset, str] = unset.UNSET, + permissions: typing.Union[unset.Unset, permissions_.Permission] = unset.UNSET, + color: typing.Union[unset.Unset, colors.Color] = unset.UNSET, + colour: typing.Union[unset.Unset, colors.Color] = unset.UNSET, + hoist: typing.Union[unset.Unset, bool] = unset.UNSET, + mentionable: typing.Union[unset.Unset, bool] = unset.UNSET, + reason: typing.Union[unset.Unset, str] = unset.UNSET, + ) -> guilds.Role: + if not unset.count_unset_objects(color, colour): + raise TypeError("Can not specify 'color' and 'colour' together.") + + route = routes.POST_GUILD_ROLES.compile(guild=conversions.value_to_snowflake(guild)) + payload = {} + conversions.put_if_specified(payload, "name", name) + conversions.put_if_specified(payload, "permissions", permissions) + conversions.put_if_specified(payload, "color", color) + conversions.put_if_specified(payload, "color", colour) + conversions.put_if_specified(payload, "hoist", hoist) + conversions.put_if_specified(payload, "mentionable", mentionable) + + response = await self._request(route, body=payload, reason=reason) + return self._app.entity_factory.deserialize_role(response) + + async def reposition_roles( + self, + guild: typing.Union[guilds.Guild, bases.UniqueObjectT], + positions: typing.Mapping[int, typing.Union[guilds.Role, bases.UniqueObjectT]], + ) -> None: + route = routes.POST_GUILD_ROLES.compile(guild=conversions.value_to_snowflake(guild)) + payload = [ + {"id": conversions.value_to_snowflake(role), "position": pos} for pos, role in positions.items() + ] + await self._request(route, body=payload) - create_role = NotImplemented - reposition_roles = NotImplemented edit_role = NotImplemented delete_role = NotImplemented estimate_guild_prune_count = NotImplemented diff --git a/hikari/net/rest_utils.py b/hikari/net/rest_utils.py index 9fbff619db..7416ee20ce 100644 --- a/hikari/net/rest_utils.py +++ b/hikari/net/rest_utils.py @@ -143,6 +143,9 @@ def add_role( if len(self._roles) == 0 and name != "@everyone": raise ValueError("First role must always be the @everyone role") + if not unset.count_unset_objects(color, colour): + raise TypeError("Cannot specify 'color' and 'colour' together.") + snowflake = self._new_snowflake() payload = {"id": str(snowflake), "name": name} conversions.put_if_specified(payload, "color", color) diff --git a/tests/hikari/models/test_guilds.py b/tests/hikari/models/test_guilds.py index 2f7dae2b65..0c3e4ad016 100644 --- a/tests/hikari/models/test_guilds.py +++ b/tests/hikari/models/test_guilds.py @@ -252,10 +252,10 @@ def test_guild_payload( "premium_subscription_count": 1, "premium_tier": 2, "presences": [test_guild_member_presence], - "public_updates_channel_id": "33333333", + "public_updates_channel": "33333333", "region": "eu-central", "roles": [test_roles_payload], - "rules_channel_id": "42042069", + "rules_channel": "42042069", "splash": "0ff0ff0ff", "system_channel_flags": 3, "system_channel_id": "19216801", From 6a0703fd9abbaac326736ec1a0b393325b4ad031 Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Mon, 25 May 2020 20:41:50 +0100 Subject: [PATCH 394/922] Added last API endpoints... --- hikari/.rest/guild.py | 20 ++-- hikari/.rest/session.py | 2 +- hikari/entity_factory.py | 4 +- hikari/impl/entity_factory.py | 4 +- hikari/models/guilds.py | 4 +- hikari/models/invites.py | 4 +- hikari/net/rest.py | 171 +++++++++++++++++++++++++--- hikari/net/routes.py | 2 +- tests/hikari/models/test_guilds.py | 2 +- tests/hikari/models/test_invites.py | 2 +- 10 files changed, 177 insertions(+), 38 deletions(-) diff --git a/hikari/.rest/guild.py b/hikari/.rest/guild.py index 62b8e351fa..ba962a3ca6 100644 --- a/hikari/.rest/guild.py +++ b/hikari/.rest/guild.py @@ -1736,7 +1736,7 @@ async def delete_integration( hikari.errors.Forbidden If you lack the `MANAGE_GUILD` permission or are not in the guild. """ - await self._session.delete_guild_integration( + await self._session.delete_integration( guild_id=str(guild.id if isinstance(guild, bases.Unique) else int(guild)), integration_id=str(integration.id if isinstance(integration, bases.Unique) else int(integration)), reason=reason, @@ -1773,7 +1773,7 @@ async def sync_guild_integration( async def fetch_guild_embed( self, guild: typing.Union[bases.Snowflake, int, str, guilds.Guild] - ) -> guilds.GuildEmbed: + ) -> guilds.GuildWidget: """Get the embed for a given guild. Parameters @@ -1783,7 +1783,7 @@ async def fetch_guild_embed( Returns ------- - hikari.models.guilds.GuildEmbed + hikari.models.guilds.GuildWidget A guild embed object. Raises @@ -1800,7 +1800,7 @@ async def fetch_guild_embed( payload = await self._session.get_guild_embed( guild_id=str(guild.id if isinstance(guild, bases.Unique) else int(guild)) ) - return guilds.GuildEmbed.deserialize(payload, app=self._app) + return guilds.GuildWidget.deserialize(payload, app=self._app) async def update_guild_embed( self, @@ -1809,7 +1809,7 @@ async def update_guild_embed( channel: typing.Union[bases.Snowflake, int, str, channels_.GuildChannel] = ..., enabled: bool = ..., reason: str = ..., - ) -> guilds.GuildEmbed: + ) -> guilds.GuildWidget: """Edits the embed for a given guild. Parameters @@ -1828,7 +1828,7 @@ async def update_guild_embed( Returns ------- - hikari.models.guilds.GuildEmbed + hikari.models.guilds.GuildWidget The updated embed object. Raises @@ -1850,11 +1850,11 @@ async def update_guild_embed( enabled=enabled, reason=reason, ) - return guilds.GuildEmbed.deserialize(payload, app=self._app) + return guilds.GuildWidget.deserialize(payload, app=self._app) async def fetch_guild_vanity_url( self, guild: typing.Union[bases.Snowflake, int, str, guilds.Guild] - ) -> invites.VanityUrl: + ) -> invites.VanityURL: """ Get the vanity URL for a given guild. @@ -1865,7 +1865,7 @@ async def fetch_guild_vanity_url( Returns ------- - hikari.models.invites.VanityUrl + hikari.models.invites.VanityURL A partial invite object containing the vanity URL in the `code` field. @@ -1883,7 +1883,7 @@ async def fetch_guild_vanity_url( payload = await self._session.get_guild_vanity_url( guild_id=str(guild.id if isinstance(guild, bases.Unique) else int(guild)) ) - return invites.VanityUrl.deserialize(payload, app=self._app) + return invites.VanityURL.deserialize(payload, app=self._app) def format_guild_widget_image( self, guild: typing.Union[bases.Snowflake, int, str, guilds.Guild], *, style: str = ... diff --git a/hikari/.rest/session.py b/hikari/.rest/session.py index 665f10298f..45b04fc061 100644 --- a/hikari/.rest/session.py +++ b/hikari/.rest/session.py @@ -2578,7 +2578,7 @@ def get_guild_widget_image_url(self, guild_id: str, *, style: str = ...) -> str: this to be valid. """ query = "" if style is ... else f"?style={style}" - route = routes.GET_GUILD_WIDGET_IMAGE.compile(guild_id=guild_id) + route = routes.GET_GUILD_BANNER_IMAGE.compile(guild_id=guild_id) return route.create_url(self.base_url) + query async def get_invite(self, invite_code: str, *, with_counts: bool = ...) -> more_typing.JSONObject: diff --git a/hikari/entity_factory.py b/hikari/entity_factory.py index c5478d22b4..a91eb5c723 100644 --- a/hikari/entity_factory.py +++ b/hikari/entity_factory.py @@ -165,7 +165,7 @@ def deserialize_gateway_bot(self, payload: more_typing.JSONObject) -> gateway.Ga ########## @abc.abstractmethod - def deserialize_guild_embed(self, payload: more_typing.JSONObject) -> guilds.GuildEmbed: + def deserialize_guild_widget(self, payload: more_typing.JSONObject) -> guilds.GuildWidget: ... @abc.abstractmethod @@ -211,7 +211,7 @@ def deserialize_guild(self, payload: more_typing.JSONObject) -> guilds.Guild: ########### @abc.abstractmethod - def deserialize_vanity_url(self, payload: more_typing.JSONObject) -> invites.VanityUrl: + def deserialize_vanity_url(self, payload: more_typing.JSONObject) -> invites.VanityURL: ... @abc.abstractmethod diff --git a/hikari/impl/entity_factory.py b/hikari/impl/entity_factory.py index 8d27af8103..513341886c 100644 --- a/hikari/impl/entity_factory.py +++ b/hikari/impl/entity_factory.py @@ -105,7 +105,7 @@ def deserialize_emoji( def deserialize_gateway_bot(self, payload: more_typing.JSONObject) -> gateway.GatewayBot: pass - def deserialize_guild_embed(self, payload: more_typing.JSONObject) -> guilds.GuildEmbed: + def deserialize_guild_widget(self, payload: more_typing.JSONObject) -> guilds.GuildWidget: pass def deserialize_guild_member( @@ -137,7 +137,7 @@ def deserialize_guild_preview(self, payload: more_typing.JSONObject) -> guilds.G def deserialize_guild(self, payload: more_typing.JSONObject) -> guilds.Guild: pass - def deserialize_vanity_url(self, payload: more_typing.JSONObject) -> invites.VanityUrl: + def deserialize_vanity_url(self, payload: more_typing.JSONObject) -> invites.VanityURL: pass def deserialize_invite(self, payload: more_typing.JSONObject) -> invites.Invite: diff --git a/hikari/models/guilds.py b/hikari/models/guilds.py index c26cd0f4bb..0df26e9400 100644 --- a/hikari/models/guilds.py +++ b/hikari/models/guilds.py @@ -29,7 +29,7 @@ "ActivityParty", "ClientStatus", "Guild", - "GuildEmbed", + "GuildWidget", "Role", "GuildFeature", "GuildSystemChannelFlag", @@ -217,7 +217,7 @@ class GuildVerificationLevel(int, more_enums.Enum): @marshaller.marshallable() @attr.s(eq=True, hash=False, kw_only=True, slots=True) -class GuildEmbed(bases.Entity, marshaller.Deserializable): +class GuildWidget(bases.Entity, marshaller.Deserializable): """Represents a guild embed.""" channel_id: typing.Optional[bases.Snowflake] = marshaller.attrib( diff --git a/hikari/models/invites.py b/hikari/models/invites.py index 287f9ae6f3..4d93ca6650 100644 --- a/hikari/models/invites.py +++ b/hikari/models/invites.py @@ -20,7 +20,7 @@ from __future__ import annotations -__all__ = ["TargetUserType", "VanityUrl", "InviteGuild", "Invite", "InviteWithMetadata"] +__all__ = ["TargetUserType", "VanityURL", "InviteGuild", "Invite", "InviteWithMetadata"] import datetime import typing @@ -47,7 +47,7 @@ class TargetUserType(int, more_enums.Enum): @marshaller.marshallable() @attr.s(eq=True, hash=True, kw_only=True, slots=True) -class VanityUrl(bases.Entity, marshaller.Deserializable): +class VanityURL(bases.Entity, marshaller.Deserializable): """A special case invite object, that represents a guild's vanity url.""" code: str = marshaller.attrib(deserializer=str, eq=True, hash=True, repr=True) diff --git a/hikari/net/rest.py b/hikari/net/rest.py index 434b2f1b5d..51fd6d3b7e 100644 --- a/hikari/net/rest.py +++ b/hikari/net/rest.py @@ -1472,21 +1472,160 @@ async def reposition_roles( positions: typing.Mapping[int, typing.Union[guilds.Role, bases.UniqueObjectT]], ) -> None: route = routes.POST_GUILD_ROLES.compile(guild=conversions.value_to_snowflake(guild)) - payload = [ - {"id": conversions.value_to_snowflake(role), "position": pos} for pos, role in positions.items() - ] + payload = [{"id": conversions.value_to_snowflake(role), "position": pos} for pos, role in positions.items()] await self._request(route, body=payload) - edit_role = NotImplemented - delete_role = NotImplemented - estimate_guild_prune_count = NotImplemented - begin_guild_prune = NotImplemented - fetch_guild_voice_regions = NotImplemented - fetch_guild_invites = NotImplemented - fetch_guild_integrations = NotImplemented - edit_guild_integration = NotImplemented - delete_guild_integration = NotImplemented - sync_guild_integration = NotImplemented - fetch_guild_widget = NotImplemented - edit_guild_widget = NotImplemented - fetch_guild_vanity_url = NotImplemented + async def edit_role( + self, + guild: typing.Union[guilds.Guild, bases.UniqueObjectT], + role: typing.Union[guilds.Role, bases.UniqueObjectT], + *, + name: typing.Union[unset.Unset, str] = unset.UNSET, + permissions: typing.Union[unset.Unset, permissions_.Permission] = unset.UNSET, + color: typing.Union[unset.Unset, colors.Color] = unset.UNSET, + colour: typing.Union[unset.Unset, colors.Color] = unset.UNSET, + hoist: typing.Union[unset.Unset, bool] = unset.UNSET, + mentionable: typing.Union[unset.Unset, bool] = unset.UNSET, + reason: typing.Union[unset.Unset, str] = unset.UNSET, + ) -> guilds.Role: + if not unset.count_unset_objects(color, colour): + raise TypeError("Can not specify 'color' and 'colour' together.") + + route = routes.PATCH_GUILD_ROLE.compile( + guild=conversions.value_to_snowflake(guild), role=conversions.value_to_snowflake(role), + ) + + payload = {} + conversions.put_if_specified(payload, "name", name) + conversions.put_if_specified(payload, "permissions", permissions) + conversions.put_if_specified(payload, "color", color) + conversions.put_if_specified(payload, "color", colour) + conversions.put_if_specified(payload, "hoist", hoist) + conversions.put_if_specified(payload, "mentionable", mentionable) + + response = await self._request(route, body=payload, reason=reason) + return self._app.entity_factory.deserialize_role(response) + + async def delete_role( + self, + guild: typing.Union[guilds.Guild, bases.UniqueObjectT], + role: typing.Union[guilds.Role, bases.UniqueObjectT], + ) -> None: + route = routes.DELETE_GUILD_ROLE.compile( + guild=conversions.value_to_snowflake(guild), role=conversions.value_to_snowflake(role), + ) + await self._request(route) + + async def estimate_guild_prune_count( + self, guild: typing.Union[guilds.Guild, bases.UniqueObjectT], days: int, + ) -> int: + route = routes.GET_GUILD_PRUNE.compile(guild=conversions.value_to_snowflake(guild)) + payload = {"days": str(days)} + response = await self._request(route, query=payload) + return int(response["pruned"]) + + async def begin_guild_prune( + self, + guild: typing.Union[guilds.Guild, bases.UniqueObjectT], + days: int, + *, + reason: typing.Union[unset.Unset, str], + ) -> int: + route = routes.POST_GUILD_PRUNE.compile(guild=conversions.value_to_snowflake(guild)) + payload = {"compute_prune_count": "true", "days": str(days)} + response = await self._request(route, query=payload, reason=reason) + return int(response["pruned"]) + + async def fetch_guild_voice_regions( + self, guild: typing.Union[guilds.Guild, bases.UniqueObjectT], / + ) -> typing.Sequence[voices.VoiceRegion]: + route = routes.GET_GUILD_VOICE_REGIONS.compile(guild=conversions.value_to_snowflake(guild)) + response = await self._request(route) + return [self._app.entity_factory.deserialize_voice_region(r) for r in response] + + async def fetch_guild_invites( + self, guild: typing.Union[guilds.Guild, bases.UniqueObjectT], / + ) -> typing.Sequence[invites.InviteWithMetadata]: + route = routes.GET_GUILD_INVITES.compile(guild=conversions.value_to_snowflake(guild)) + response = await self._request(route) + return [self._app.entity_factory.deserialize_invite_with_metadata(r) for r in response] + + async def fetch_integrations( + self, guild: typing.Union[guilds.Guild, bases.UniqueObjectT], / + ) -> typing.Sequence[guilds.GuildIntegration]: + route = routes.GET_GUILD_INTEGRATIONS.compile(guild=conversions.value_to_snowflake(guild)) + response = await self._request(route) + return [self._app.entity_factory.deserialize_guild_integration(i) for i in response] + + async def edit_integration( + self, + guild: typing.Union[guilds.Guild, bases.UniqueObjectT], + integration: typing.Union[guilds.GuildIntegration, bases.UniqueObjectT], + *, + expire_behaviour: typing.Union[unset.Unset, guilds.IntegrationExpireBehaviour] = unset.UNSET, + expire_grace_period: typing.Union[unset.Unset, more_typing.TimeSpanT] = unset.UNSET, + enable_emojis: typing.Union[unset.Unset, bool] = unset.UNSET, + reason: typing.Union[unset.Unset, str] = unset.UNSET, + ) -> None: + route = routes.PATCH_GUILD_INTEGRATION.compile( + guild=conversions.value_to_snowflake(guild), integration=conversions.value_to_snowflake(integration), + ) + payload = {} + conversions.put_if_specified(payload, "expire_behaviour", expire_behaviour) + conversions.put_if_specified(payload, "expire_grace_period", expire_grace_period, conversions.timespan_to_int) + # Inconsistent naming in the API itself, so I have changed the name. + conversions.put_if_specified(payload, "enable_emoticons", enable_emojis) + await self._request(route, body=payload, reason=reason) + + async def delete_integration( + self, + guild: typing.Union[guilds.Guild, bases.UniqueObjectT], + integration: typing.Union[guilds.GuildIntegration, bases.UniqueObjectT], + *, + reason: typing.Union[unset.Unset, str] = unset.UNSET, + ) -> None: + route = routes.DELETE_GUILD_INTEGRATION.compile( + guild=conversions.value_to_snowflake(guild), integration=conversions.value_to_snowflake(integration), + ) + await self._request(route, reason=reason) + + async def sync_integration( + self, + guild: typing.Union[guilds.Guild, bases.UniqueObjectT], + integration: typing.Union[guilds.GuildIntegration, bases.UniqueObjectT], + ) -> None: + route = routes.POST_GUILD_INTEGRATION_SYNC.compile( + guild=conversions.value_to_snowflake(guild), integration=conversions.value_to_snowflake(integration), + ) + await self._request(route) + + async def fetch_widget(self, guild: typing.Union[guilds.Guild, bases.UniqueObjectT], /) -> guilds.GuildWidget: + route = routes.GET_GUILD_WIDGET.compile(guild=conversions.value_to_snowflake(guild)) + response = await self._request(route) + return self._app.entity_factory.deserialize_guild_widget(response) + + async def edit_widget( + self, + guild: typing.Union[guilds.Guild, bases.UniqueObjectT], + /, + *, + channel: typing.Union[unset.Unset, channels.GuildChannel, bases.UniqueObjectT, None] = unset.UNSET, + enabled: typing.Union[unset.Unset, bool] = unset.UNSET, + reason: typing.Union[unset.Unset, str] = unset.UNSET, + ) -> guilds.GuildWidget: + route = routes.PATCH_GUILD_WIDGET.compile(guild=conversions.value_to_snowflake(guild)) + + payload = {} + conversions.put_if_specified(payload, "enabled", enabled) + if channel is None: + payload["channel"] = None + elif not unset.is_unset(channel): + payload["channel"] = conversions.value_to_snowflake(channel) + + response = await self._request(route, body=payload, reason=reason) + return self._app.entity_factory.deserialize_guild_widget(response) + + async def fetch_vanity_url(self, guild: typing.Union[guilds.Guild, bases.UniqueObjectT], /) -> invites.VanityURL: + route = routes.GET_GUILD_VANITY_URL.compile(guild=conversions.value_to_snowflake(guild)) + response = await self._request(route) + return self._app.entity_factory.deserialize_vanity_url(response) diff --git a/hikari/net/routes.py b/hikari/net/routes.py index 3f1f546446..c1bcb1e001 100644 --- a/hikari/net/routes.py +++ b/hikari/net/routes.py @@ -320,7 +320,7 @@ def __eq__(self, other) -> bool: GET_GUILD_WEBHOOKS = Route(GET, "/guilds/{guild}/webhooks") -GET_GUILD_WIDGET_IMAGE = Route(GET, "/guilds/{guild}/widget.png") +GET_GUILD_BANNER_IMAGE = Route(GET, "/guilds/{guild}/widget.png") # Invites GET_INVITE = Route(GET, "/invites/{invite_code}") diff --git a/tests/hikari/models/test_guilds.py b/tests/hikari/models/test_guilds.py index 0c3e4ad016..368fd4f2b2 100644 --- a/tests/hikari/models/test_guilds.py +++ b/tests/hikari/models/test_guilds.py @@ -279,7 +279,7 @@ def test_guild_embed_payload(self): return {"channel": "123123123", "enabled": True} def test_deserialize(self, test_guild_embed_payload, mock_app): - guild_embed_obj = guilds.GuildEmbed.deserialize(test_guild_embed_payload, app=mock_app) + guild_embed_obj = guilds.GuildWidget.deserialize(test_guild_embed_payload, app=mock_app) assert guild_embed_obj.channel_id == 123123123 assert guild_embed_obj.is_enabled is True diff --git a/tests/hikari/models/test_invites.py b/tests/hikari/models/test_invites.py index 5716309bd2..687312fc1d 100644 --- a/tests/hikari/models/test_invites.py +++ b/tests/hikari/models/test_invites.py @@ -169,7 +169,7 @@ def vanity_url_payload(self): return {"code": "iamacode", "uses": 42} def test_deserialize(self, vanity_url_payload, mock_app): - vanity_url_obj = invites.VanityUrl.deserialize(vanity_url_payload, app=mock_app) + vanity_url_obj = invites.VanityURL.deserialize(vanity_url_payload, app=mock_app) assert vanity_url_obj.code == "iamacode" assert vanity_url_obj.uses == 42 From e95ea464261bee7d54ab7828ed08d3b08d47e242 Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Mon, 25 May 2020 20:43:20 +0100 Subject: [PATCH 395/922] Purged the old rest package. --- hikari/.rest/__init__.py | 21 - hikari/.rest/base.py | 82 - hikari/.rest/guild.py | 1947 ------------------------ hikari/.rest/session.py | 3107 -------------------------------------- 4 files changed, 5157 deletions(-) delete mode 100644 hikari/.rest/__init__.py delete mode 100644 hikari/.rest/base.py delete mode 100644 hikari/.rest/guild.py delete mode 100644 hikari/.rest/session.py diff --git a/hikari/.rest/__init__.py b/hikari/.rest/__init__.py deleted file mode 100644 index ea2673ae48..0000000000 --- a/hikari/.rest/__init__.py +++ /dev/null @@ -1,21 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# Copyright © Nekoka.tt 2019-2020 -# -# This file is part of Hikari. -# -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . -"""Components used for interacting with Discord's RESTful api.""" - -from __future__ import annotations diff --git a/hikari/.rest/base.py b/hikari/.rest/base.py deleted file mode 100644 index 732fae91fc..0000000000 --- a/hikari/.rest/base.py +++ /dev/null @@ -1,82 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# Copyright © Nekoka.tt 2019-2020 -# -# This file is part of Hikari. -# -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . -"""The abstract class that all RESTSession client logic classes should inherit from.""" - -from __future__ import annotations - -__all__ = ["BaseRESTComponent"] - -import abc -import typing - -from hikari.internal import meta - -if typing.TYPE_CHECKING: - import types - - from hikari import rest_app - from hikari.net.rest import session as rest_session - - -class BaseRESTComponent(abc.ABC, metaclass=meta.UniqueFunctionMeta): - """An abstract class that all RESTSession client logic classes should inherit from. - - This defines the abstract method `__init__` which will assign an instance - of `hikari.rest.session.RESTSession` to the attribute that all application will expect - to make calls to. - """ - - @abc.abstractmethod - def __init__(self, app: rest_app.IRESTApp) -> None: - self._app = app - - async def __aenter__(self) -> BaseRESTComponent: - return self - - async def __aexit__( - self, exc_type: typing.Type[BaseException], exc_val: BaseException, exc_tb: types.TracebackType - ) -> None: - await self.close() - - async def close(self) -> None: - """Shut down the RESTSession client safely.""" - await self._session.close() - - # XXX: my plan is to abolish the low level objects soon and integrate their logic in here - # directly, thus I want this gone before 2.0.0. - @property - def session(self) -> rest_session.RESTSession: - """The low-level REST API session.""" - return self._session - - @property - def global_ratelimit_queue_size(self) -> int: - """Count of API calls waiting for the global ratelimiter to release. - - If this is non-zero, then you are being globally ratelimited. - """ - return len(self._session.global_ratelimiter.queue) - - @property - def route_ratelimit_queue_size(self) -> int: - """Count of API waiting for a _route-specific ratelimit to release. - - If this is non-zero, then you are being ratelimited somewhere. - """ - return sum(len(r.queue) for r in self._session.bucket_ratelimiters.real_hashes_to_buckets.values()) diff --git a/hikari/.rest/guild.py b/hikari/.rest/guild.py deleted file mode 100644 index ba962a3ca6..0000000000 --- a/hikari/.rest/guild.py +++ /dev/null @@ -1,1947 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# Copyright © Nekoka.tt 2019-2020 -# -# This file is part of Hikari. -# -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . -"""The logic for handling requests to guild endpoints.""" - -from __future__ import annotations - -__all__ = ["RESTGuildComponent"] - -import abc -import datetime -import functools -import typing - -from hikari.net import iterators -from hikari.models import audit_logs -from hikari.models import bases -from hikari.models import channels as channels_ -from hikari.models import emojis -from hikari.models import guilds -from hikari.models import invites -from hikari.models import voices -from hikari.models import webhooks -from hikari.net.rest import base - -if typing.TYPE_CHECKING: - from hikari.models import colors - from hikari.models import files - from hikari.models import permissions as permissions_ - from hikari.models import users - - -class _MemberPaginator(iterators._BufferedLazyIterator[guilds.GuildMember]): - __slots__ = ("_app", "_guild_id", "_first_id", "_session") - - def __init__(self, app, guild, created_after, session): - super().__init__() - self._app = app - self._guild_id = str(int(guild)) - self._first_id = self._prepare_first_id(created_after) - self._session = session - - async def _next_chunk(self): - chunk = await self._session.list_guild_members(self._guild_id, after=self._first_id) - - if not chunk: - return None - - self._first_id = chunk[-1]["id"] - - return (guilds.GuildMember.deserialize(m, app=self._app) for m in chunk) - - -class RESTGuildComponent(base.BaseRESTComponent, abc.ABC): # pylint: disable=abstract-method, too-many-public-methods - """The RESTSession client component for handling requests to guild endpoints.""" - - async def fetch_audit_log( - self, - guild: typing.Union[bases.Snowflake, int, str, guilds.Guild], - *, - user: typing.Union[bases.Snowflake, int, str, users.User] = ..., - action_type: typing.Union[audit_logs.AuditLogEventType, int] = ..., - limit: int = ..., - before: typing.Union[ - datetime.datetime, typing.Union[bases.Snowflake, int, str, audit_logs.AuditLogEntry] - ] = ..., - ) -> audit_logs.AuditLog: - """Get an audit log object for the given guild. - - Parameters - ---------- - guild : hikari.models.guilds.Guild | hikari.models.bases.Snowflake | int | str - The object or ID of the guild to get the audit logs for. - user : hikari.models.users.User | hikari.models.bases.Snowflake | int | str - If specified, the object or ID of the user to filter by. - action_type : hikari.models.audit_logs.AuditLogEventType | int - If specified, the action type to look up. Passing a raw integer - for this may lead to unexpected behaviour. - limit : int - If specified, the limit to apply to the number of records. - Defaults to `50`. Must be between `1` and `100` inclusive. - before : datetime.datetime | hikari.models.audit_logs.AuditLogEntry | hikari.models.bases.Snowflake | int | str - If specified, the object or ID of the entry that all retrieved - entries should have occurred before. - - Returns - ------- - hikari.models.audit_logs.AuditLog - An audit log object. - - Raises - ------ - hikari.errors.BadRequest - If any invalid snowflake IDs are passed; a snowflake may be invalid - due to it being outside of the range of a 64 bit integer. - hikari.errors.Forbidden - If you lack the given permissions to view an audit log. - hikari.errors.NotFound - If the guild does not exist. - """ - if isinstance(before, datetime.datetime): - before = str(bases.Snowflake.from_datetime(before)) - elif before is not ...: - # noinspection PyTypeChecker - before = str(before.id if isinstance(before, bases.Unique) else int(before)) - payload = await self._session.get_guild_audit_log( - guild_id=str(guild.id if isinstance(guild, bases.Unique) else int(guild)), - user_id=(str(user.id if isinstance(user, bases.Unique) else int(user)) if user is not ... else ...), - action_type=action_type, - limit=limit, - before=before, - ) - return audit_logs.AuditLog.deserialize(payload, app=self._app) - - def fetch_audit_log_entries_before( - self, - guild: typing.Union[bases.Snowflake, int, str, guilds.Guild], - *, - before: typing.Union[ - datetime.datetime, typing.Union[bases.Snowflake, int, str, audit_logs.AuditLogEntry] - ] = bases.Snowflake.max(), - user: typing.Union[bases.Snowflake, int, str, users.User] = ..., - action_type: typing.Union[audit_logs.AuditLogEventType, int] = ..., - limit: typing.Optional[int] = None, - ) -> audit_logs.AuditLogIterator: - """Return an async iterator that retrieves a guild's audit log entries. - - This will return the audit log entries before a given entry object/ID or - from the first guild audit log entry. - - Parameters - ---------- - guild : hikari.models.guilds.Guild | hikari.models.bases.Snowflake | int | str - The ID or object of the guild to get audit log entries for - before : datetime.datetime | hikari.models.audit_logs.AuditLogEntry | hikari.models.bases.Snowflake | int | str | None - If specified, the ID or object of the entry or datetime to get - entries that happened before otherwise this will start from the - newest entry. - user : hikari.models.users.User | hikari.models.bases.Snowflake | int | str - If specified, the object or ID of the user to filter by. - action_type : hikari.models.audit_logs.AuditLogEventType | int - If specified, the action type to look up. Passing a raw integer - for this may lead to unexpected behaviour. - limit : int | None - If specified, the limit for how many entries this iterator should - return, defaults to unlimited. - - Examples - -------- - audit_log_entries = client.fetch_audit_log_entries_before(guild, before=9876543, limit=6969) - async for entry in audit_log_entries: - if (user := audit_log_entries.users[entry.user_id]).is_bot: - await client.ban_member(guild, user) - - !!! note - The returned iterator has the attributes `users`, `members` and - `integrations` which are mappings of snowflake IDs to objects for - the relevant entities that are referenced by the retrieved audit log - entries. These will be filled over time as more audit log entries - are fetched by the iterator. - - Returns - ------- - hikari.errors.BadRequest - If any invalid snowflake IDs are passed; a snowflake may be invalid - due to it being outside of the range of a 64 bit integer. - hikari.models.audit_logs.AuditLogIterator - An async iterator of the audit log entries in a guild (from newest - to oldest). - """ - if isinstance(before, datetime.datetime): - before = str(bases.Snowflake.from_datetime(before)) - else: - before = str(before.id if isinstance(before, bases.Unique) else int(before)) - request = functools.partial( - self._session.get_guild_audit_log, - guild_id=str(guild.id if isinstance(guild, bases.Unique) else int(guild)), - user_id=(str(user.id if isinstance(user, bases.Unique) else int(user)) if user is not ... else ...), - action_type=action_type, - ) - return audit_logs.AuditLogIterator(app=self._app, request=request, before=before, limit=limit) - - async def fetch_guild_emoji( - self, - guild: typing.Union[bases.Snowflake, int, str, guilds.Guild], - emoji: typing.Union[bases.Snowflake, int, str, emojis.KnownCustomEmoji], - ) -> emojis.KnownCustomEmoji: - """Get an updated emoji object from a specific guild. - - Parameters - ---------- - guild : hikari.models.guilds.Guild | hikari.models.bases.Snowflake | int | str - The object or ID of the guild to get the emoji from. - emoji : hikari.models.emojis.KnownCustomEmoji | hikari.models.bases.Snowflake | int | str - The object or ID of the emoji to get. - - Returns - ------- - hikari.models.emojis.KnownCustomEmoji - A guild emoji object. - - Raises - ------ - hikari.errors.BadRequest - If any invalid snowflake IDs are passed; a snowflake may be invalid - due to it being outside of the range of a 64 bit integer. - hikari.errors.NotFound - If either the guild or the emoji aren't found. - hikari.errors.Forbidden - If you aren't a member of said guild. - """ - payload = await self._session.get_guild_emoji( - guild_id=str(guild.id if isinstance(guild, bases.Unique) else int(guild)), - emoji_id=str(emoji.id if isinstance(emoji, bases.Unique) else int(emoji)), - ) - return emojis.KnownCustomEmoji.deserialize(payload, app=self._app) - - async def fetch_guild_emojis( - self, guild: typing.Union[bases.Snowflake, int, str, guilds.Guild] - ) -> typing.Sequence[emojis.KnownCustomEmoji]: - """Get emojis for a given guild object or ID. - - Parameters - ---------- - guild : hikari.models.guilds.Guild | hikari.models.bases.Snowflake | int | str - The object or ID of the guild to get the emojis for. - - Returns - ------- - typing.Sequence[hikari.models.emojis.KnownCustomEmoji] - A list of guild emoji objects. - - Raises - ------ - hikari.errors.BadRequest - If any invalid snowflake IDs are passed; a snowflake may be invalid - due to it being outside of the range of a 64 bit integer. - hikari.errors.NotFound - If the guild is not found. - hikari.errors.Forbidden - If you aren't a member of the guild. - """ - payload = await self._session.list_guild_emojis( - guild_id=str(guild.id if isinstance(guild, bases.Unique) else int(guild)) - ) - return [emojis.KnownCustomEmoji.deserialize(emoji, app=self._app) for emoji in payload] - - async def create_guild_emoji( - self, - guild: typing.Union[bases.Snowflake, int, str, guilds.Role], - name: str, - image: files.BaseStream, - *, - roles: typing.Sequence[typing.Union[bases.Snowflake, int, str, guilds.Role]] = ..., - reason: str = ..., - ) -> emojis.KnownCustomEmoji: - """Create a new emoji for a given guild. - - Parameters - ---------- - guild : hikari.models.guilds.Guild | hikari.models.bases.Snowflake | int | str - The object or ID of the guild to create the emoji in. - name : str - The new emoji's name. - image : hikari.models.files.BaseStream - The `128x128` image data. - roles : typing.Sequence[hikari.models.guilds.Role | hikari.models.bases.Snowflake | int | str] - If specified, a list of role objects or IDs for which the emoji - will be whitelisted. If empty, all roles are whitelisted. - reason : str - If specified, the audit log reason explaining why the operation - was performed. - - Returns - ------- - hikari.models.emojis.KnownCustomEmoji - The newly created emoji object. - - Raises - ------ - ValueError - If `image` is `None`. - hikari.errors.NotFound - If the guild is not found. - hikari.errors.Forbidden - If you either lack the `MANAGE_EMOJIS` permission or aren't a - member of said guild. - hikari.errors.BadRequest - If you attempt to upload an image larger than `256kb`, an empty - image or an invalid image format. - If any invalid snowflake IDs are passed; a snowflake may be invalid - due to it being outside of the range of a 64 bit integer. - """ - payload = await self._session.create_emoji( - guild_id=str(guild.id if isinstance(guild, bases.Unique) else int(guild)), - name=name, - image=await image.read(), - roles=[str(role.id if isinstance(role, bases.Unique) else int(role)) for role in roles] - if roles is not ... - else ..., - reason=reason, - ) - return emojis.KnownCustomEmoji.deserialize(payload, app=self._app) - - async def update_guild_emoji( - self, - guild: typing.Union[bases.Snowflake, int, str, guilds.Guild], - emoji: typing.Union[bases.Snowflake, int, str, emojis.KnownCustomEmoji], - *, - name: str = ..., - roles: typing.Sequence[typing.Union[bases.Snowflake, int, str, guilds.Role]] = ..., - reason: str = ..., - ) -> emojis.KnownCustomEmoji: - """Edits an emoji of a given guild. - - Parameters - ---------- - guild : hikari.models.guilds.Guild | hikari.models.bases.Snowflake | int | str - The object or ID of the guild to which the emoji to edit belongs to. - emoji : hikari.models.emojis.KnownCustomEmoji | hikari.models.bases.Snowflake | int | str - The object or ID of the emoji to edit. - name : str - If specified, a new emoji name string. Keep unspecified to leave the - name unchanged. - roles : typing.Sequence[hikari.models.guilds.Role | hikari.models.bases.Snowflake | int | str] - If specified, a list of objects or IDs for the new whitelisted - roles. Set to an empty list to whitelist all roles. - Keep unspecified to leave the same roles already set. - reason : str - If specified, the audit log reason explaining why the operation - was performed. - - Returns - ------- - hikari.models.emojis.KnownCustomEmoji - The updated emoji object. - - Raises - ------ - hikari.errors.BadRequest - If any invalid snowflake IDs are passed; a snowflake may be invalid - due to it being outside of the range of a 64 bit integer. - hikari.errors.NotFound - If either the guild or the emoji aren't found. - hikari.errors.Forbidden - If you either lack the `MANAGE_EMOJIS` permission or are not a - member of the given guild. - """ - payload = await self._session.modify_guild_emoji( - guild_id=str(guild.id if isinstance(guild, bases.Unique) else int(guild)), - emoji_id=str(emoji.id if isinstance(emoji, bases.Unique) else int(emoji)), - name=name, - roles=[str(role.id if isinstance(role, bases.Unique) else int(role)) for role in roles] - if roles is not ... - else ..., - reason=reason, - ) - return emojis.KnownCustomEmoji.deserialize(payload, app=self._app) - - async def delete_guild_emoji( - self, - guild: typing.Union[bases.Snowflake, int, str, guilds.Guild], - emoji: typing.Union[bases.Snowflake, int, str, emojis.KnownCustomEmoji], - ) -> None: - """Delete an emoji from a given guild. - - Parameters - ---------- - guild : hikari.models.guilds.Guild | hikari.models.bases.Snowflake | int | str - The object or ID of the guild to delete the emoji from. - emoji : hikari.models.emojis.KnownCustomEmoji | hikari.models.bases.Snowflake | int | str - The object or ID of the guild emoji to be deleted. - - Raises - ------ - hikari.errors.BadRequest - If any invalid snowflake IDs are passed; a snowflake may be invalid - due to it being outside of the range of a 64 bit integer. - hikari.errors.NotFound - If either the guild or the emoji aren't found. - hikari.errors.Forbidden - If you either lack the `MANAGE_EMOJIS` permission or aren't a - member of said guild. - """ - await self._session.delete_emoji( - guild_id=str(guild.id if isinstance(guild, bases.Unique) else int(guild)), - emoji_id=str(emoji.id if isinstance(emoji, bases.Unique) else int(emoji)), - ) - - async def create_guild( - self, - name: str, - *, - region: typing.Union[voices.VoiceRegion, str] = ..., - icon: files.BaseStream = ..., - verification_level: typing.Union[guilds.GuildVerificationLevel, int] = ..., - default_message_notifications: typing.Union[guilds.GuildMessageNotificationsLevel, int] = ..., - explicit_content_filter: typing.Union[guilds.GuildExplicitContentFilterLevel, int] = ..., - roles: typing.Sequence[guilds.Role] = ..., - channels: typing.Sequence[channels_.GuildChannelBuilder] = ..., - ) -> guilds.Guild: - """Create a new guild. - - !!! warning - Can only be used by bots in less than `10` guilds. - - Parameters - ---------- - name : str - The name string for the new guild (`2-100` characters). - region : str - If specified, the voice region ID for new guild. You can use - `RESTGuildComponent.fetch_guild_voice_regions` to see which region - IDs are available. - icon : hikari.models.files.BaseStream - If specified, the guild icon image data. - verification_level : hikari.models.guilds.GuildVerificationLevel | int - If specified, the verification level. Passing a raw int for this - may lead to unexpected behaviour. - default_message_notifications : hikari.models.guilds.GuildMessageNotificationsLevel | int - If specified, the default notification level. Passing a raw int for - this may lead to unexpected behaviour. - explicit_content_filter : hikari.models.guilds.GuildExplicitContentFilterLevel | int - If specified, the explicit content filter. Passing a raw int for - this may lead to unexpected behaviour. - roles : typing.Sequence[hikari.models.guilds.Role] - If specified, an array of role objects to be created alongside the - guild. First element changes the `@everyone` role. - channels : typing.Sequence[hikari.models.channels.GuildChannelBuilder] - If specified, an array of guild channel builder objects to be - created within the guild. - - Returns - ------- - hikari.models.guilds.Guild - The newly created guild object. - - Raises - ------ - hikari.errors.Forbidden - If you are in `10` or more guilds. - hikari.errors.BadRequest - If you provide unsupported fields like `parent_id` in channel - objects. - If any invalid snowflake IDs are passed; a snowflake may be invalid - due to it being outside of the range of a 64 bit integer. - """ - payload = await self._session.create_guild( - name=name, - region=getattr(region, "id", region), - icon=await icon.read() if icon is not ... else ..., - verification_level=verification_level, - default_message_notifications=default_message_notifications, - explicit_content_filter=explicit_content_filter, - roles=[role.serialize() for role in roles] if roles is not ... else ..., - channels=[channel.serialize() for channel in channels] if channels is not ... else ..., - ) - return guilds.Guild.deserialize(payload, app=self._app) - - async def fetch_guild(self, guild: typing.Union[bases.Snowflake, int, str, guilds.Guild]) -> guilds.Guild: - """Get a given guild's object. - - Parameters - ---------- - guild : hikari.models.guilds.Guild | hikari.models.bases.Snowflake | int | str - The object or ID of the guild to get. - - Returns - ------- - hikari.models.guilds.Guild - The requested guild object. - - Raises - ------ - hikari.errors.BadRequest - If any invalid snowflake IDs are passed; a snowflake may be invalid - due to it being outside of the range of a 64 bit integer. - hikari.errors.NotFound - If the guild is not found. - hikari.errors.Forbidden - If you don't have access to the guild. - """ - payload = await self._session.get_guild( - guild_id=str(guild.id if isinstance(guild, bases.Unique) else int(guild)), - # Always get counts. There is no reason you would _not_ want this info, right? - with_counts=True, - ) - return guilds.Guild.deserialize(payload, app=self._app) - - async def fetch_guild_preview( - self, guild: typing.Union[bases.Snowflake, int, str, guilds.Guild] - ) -> guilds.GuildPreview: - """Get a given guild's object. - - Parameters - ---------- - guild : hikari.models.guilds.Guild | hikari.models.bases.Snowflake | int | str - The object or ID of the guild to get the preview object for. - - Returns - ------- - hikari.models.guilds.GuildPreview - The requested guild preview object. - - !!! note - Unlike other guild endpoints, the bot doesn't have to be in the - target guild to get it's preview. - - Raises - ------ - hikari.errors.BadRequest - If any invalid snowflake IDs are passed; a snowflake may be invalid - due to it being outside of the range of UINT64. - hikari.errors.NotFound - If the guild is not found or it isn't `PUBLIC`. - """ - payload = await self._session.get_guild_preview( - guild_id=str(guild.id if isinstance(guild, bases.Unique) else int(guild)) - ) - return guilds.GuildPreview.deserialize(payload, app=self._app) - - async def update_guild( - self, - guild: typing.Union[bases.Snowflake, int, str, guilds.Guild], - *, - name: str = ..., - region: typing.Union[voices.VoiceRegion, str] = ..., - verification_level: typing.Union[guilds.GuildVerificationLevel, int] = ..., - default_message_notifications: typing.Union[guilds.GuildMessageNotificationsLevel, int] = ..., - explicit_content_filter: typing.Union[guilds.GuildExplicitContentFilterLevel, int] = ..., - afk_channel: typing.Union[bases.Snowflake, int, str, channels_.GuildVoiceChannel] = ..., - afk_timeout: typing.Union[datetime.timedelta, int] = ..., - icon: files.BaseStream = ..., - owner: typing.Union[bases.Snowflake, int, str, users.User] = ..., - splash: files.BaseStream = ..., - system_channel: typing.Union[bases.Snowflake, int, str, channels_.PartialChannel] = ..., - reason: str = ..., - ) -> guilds.Guild: - """Edit a given guild. - - Parameters - ---------- - guild : hikari.models.guilds.Guild | hikari.models.bases.Snowflake | int | str - The object or ID of the guild to be edited. - name : str - If specified, the new name string for the guild (`2-100` characters). - region : str - If specified, the new voice region ID for guild. You can use - `RESTGuildComponent.fetch_guild_voice_regions` to see which region - IDs are available. - verification_level : hikari.models.guilds.GuildVerificationLevel | int - If specified, the new verification level. Passing a raw int for this - may lead to unexpected behaviour. - default_message_notifications : hikari.models.guilds.GuildMessageNotificationsLevel | int - If specified, the new default notification level. Passing a raw int - for this may lead to unexpected behaviour. - explicit_content_filter : hikari.models.guilds.GuildExplicitContentFilterLevel | int - If specified, the new explicit content filter. Passing a raw int for - this may lead to unexpected behaviour. - afk_channel : hikari.models.channels.GuildVoiceChannel | hikari.models.bases.Snowflake | int | str - If specified, the object or ID for the new AFK voice channel. - afk_timeout : datetime.timedelta | int - If specified, the new AFK _request_timeout seconds timedelta. - icon : hikari.models.files.BaseStream - If specified, the new guild icon image file. - owner : hikari.models.users.User | hikari.models.bases.Snowflake | int | str - If specified, the object or ID of the new guild owner. - splash : hikari.models.files.BaseStream - If specified, the new new splash image file. - system_channel : hikari.models.channels.PartialChannel | hikari.models.bases.Snowflake | int | str - If specified, the object or ID of the new system channel. - reason : str - If specified, the audit log reason explaining why the operation - was performed. - - Returns - ------- - hikari.models.guilds.Guild - The edited guild object. - - Raises - ------ - hikari.errors.BadRequest - If any invalid snowflake IDs are passed; a snowflake may be invalid - due to it being outside of the range of a 64 bit integer. - hikari.errors.NotFound - If the guild is not found. - hikari.errors.Forbidden - If you lack the `MANAGE_GUILD` permission or are not in the guild. - """ - payload = await self._session.modify_guild( - guild_id=str(guild.id if isinstance(guild, bases.Unique) else int(guild)), - name=name, - region=getattr(region, "id", region) if region is not ... else ..., - verification_level=verification_level, - default_message_notifications=default_message_notifications, - explicit_content_filter=explicit_content_filter, - afk_timeout=afk_timeout.total_seconds() if isinstance(afk_timeout, datetime.timedelta) else afk_timeout, - afk_channel_id=( - str(afk_channel.id if isinstance(afk_channel, bases.Unique) else int(afk_channel)) - if afk_channel is not ... - else ... - ), - icon=await icon.read() if icon is not ... else ..., - owner_id=(str(owner.id if isinstance(owner, bases.Unique) else int(owner)) if owner is not ... else ...), - splash=await splash.read() if splash is not ... else ..., - system_channel_id=( - str(system_channel.id if isinstance(system_channel, bases.Unique) else int(system_channel)) - if system_channel is not ... - else ... - ), - reason=reason, - ) - return guilds.Guild.deserialize(payload, app=self._app) - - async def delete_guild(self, guild: typing.Union[bases.Snowflake, int, str, guilds.Guild]) -> None: - """Permanently deletes the given guild. - - You must be owner of the guild to perform this action. - - Parameters - ---------- - guild : hikari.models.guilds.Guild | hikari.models.bases.Snowflake | int | str - The object or ID of the guild to be deleted. - - Raises - ------ - hikari.errors.BadRequest - If any invalid snowflake IDs are passed; a snowflake may be invalid - due to it being outside of the range of a 64 bit integer. - hikari.errors.NotFound - If the guild is not found. - hikari.errors.Forbidden - If you are not the guild owner. - """ - await self._session.delete_guild(guild_id=str(guild.id if isinstance(guild, bases.Unique) else int(guild))) - - async def fetch_guild_channels( - self, guild: typing.Union[bases.Snowflake, int, str, guilds.Guild] - ) -> typing.Sequence[channels_.GuildChannel]: - """Get all the channels for a given guild. - - Parameters - ---------- - guild :hikari.models.guilds.Guild | hikari.models.bases.Snowflake | int | str - The object or ID of the guild to get the channels from. - - Returns - ------- - typing.Sequence[hikari.models.channels.GuildChannel] - A list of guild channel objects. - - Raises - ------ - hikari.errors.BadRequest - If any invalid snowflake IDs are passed; a snowflake may be invalid - due to it being outside of the range of a 64 bit integer. - hikari.errors.NotFound - If the guild is not found. - hikari.errors.Forbidden - If you are not in the guild. - """ - payload = await self._session.list_guild_channels( - guild_id=str(guild.id if isinstance(guild, bases.Unique) else int(guild)) - ) - return [channels_.deserialize_channel(channel, app=self._app) for channel in payload] - - async def create_guild_channel( # pylint: disable=too-many-arguments - self, - guild: typing.Union[bases.Snowflake, int, str, guilds.Guild], - name: str, - channel_type: typing.Union[channels_.ChannelType, int] = ..., - position: int = ..., - topic: str = ..., - nsfw: bool = ..., - rate_limit_per_user: typing.Union[datetime.timedelta, int] = ..., - bitrate: int = ..., - user_limit: int = ..., - permission_overwrites: typing.Sequence[channels_.PermissionOverwrite] = ..., - parent_category: typing.Union[bases.Snowflake, int, str, channels_.GuildCategory] = ..., - reason: str = ..., - ) -> channels_.GuildChannel: - """Create a channel in a given guild. - - Parameters - ---------- - guild : hikari.models.guilds.Guild | hikari.models.bases.Snowflake | int | str - The object or ID of the guild to create the channel in. - name : str - If specified, the name for the channel. This must be - inclusively between `1` and `100` characters in length. - channel_type : hikari.models.channels.ChannelType | int - If specified, the channel type, passing through a raw integer here - may lead to unexpected behaviour. - position : int - If specified, the position to change the channel to. - topic : str - If specified, the topic to set. This is only applicable to - text channels. This must be inclusively between `0` and `1024` - characters in length. - nsfw : bool - If specified, whether the channel will be marked as NSFW. - Only applicable for text channels. - rate_limit_per_user : datetime.timedelta | int - If specified, the second time delta the user has to wait before - sending another message. This will not apply to bots, or to - members with `MANAGE_MESSAGES` or `MANAGE_CHANNEL` permissions. - This must be inclusively between `0` and `21600` seconds. - bitrate : int - If specified, the bitrate in bits per second allowable for the - channel. This only applies to voice channels and must be inclusively - between `8000` and `96000` for normal servers or `8000` and - `128000` for VIP servers. - user_limit : int - If specified, the max number of users to allow in a voice channel. - This must be between `0` and `99` inclusive, where - `0` implies no limit. - permission_overwrites : typing.Sequence[hikari.models.channels.PermissionOverwrite] - If specified, the list of permission overwrite objects that are - category specific to replace the existing overwrites with. - parent_category : hikari.models.guilds.GuildCategory | hikari.models.bases.Snowflake | int | str - If specified, the object or ID of the parent category to set for - the channel. - reason : str - If specified, the audit log reason explaining why the operation - was performed. - - Returns - ------- - hikari.models.channels.GuildChannel - The newly created channel object. - - Raises - ------ - hikari.errors.NotFound - If the guild is not found. - hikari.errors.Forbidden - If you lack the `MANAGE_CHANNEL` permission or are not in the - guild. - hikari.errors.BadRequest - If you provide incorrect options for the corresponding channel type - (e.g. a `bitrate` for a text channel). - If any invalid snowflake IDs are passed; a snowflake may be invalid - due to it being outside of the range of a 64 bit integer. - """ - payload = await self._session.create_guild_channel( - guild_id=str(guild.id if isinstance(guild, bases.Unique) else int(guild)), - name=name, - type_=channel_type, - position=position, - topic=topic, - nsfw=nsfw, - rate_limit_per_user=( - int(rate_limit_per_user.total_seconds()) - if isinstance(rate_limit_per_user, datetime.timedelta) - else rate_limit_per_user - ), - bitrate=bitrate, - user_limit=user_limit, - permission_overwrites=( - [po.serialize() for po in permission_overwrites] if permission_overwrites is not ... else ... - ), - parent_id=( - str(parent_category.id if isinstance(parent_category, bases.Unique) else int(parent_category)) - if parent_category is not ... - else ... - ), - reason=reason, - ) - return channels_.deserialize_channel(payload, app=self._app) - - async def reposition_guild_channels( - self, - guild: typing.Union[bases.Snowflake, int, str, guilds.Guild], - channel: typing.Tuple[int, typing.Union[bases.Snowflake, int, str, channels_.GuildChannel]], - *additional_channels: typing.Tuple[int, typing.Union[bases.Snowflake, int, str, channels_.GuildChannel]], - ) -> None: - """Edits the position of one or more given channels. - - Parameters - ---------- - guild : hikari.models.guilds.Guild | hikari.models.bases.Snowflake | int | str - The object or ID of the guild in which to edit the channels. - channel : typing.Tuple[int , hikari.models.channels.GuildChannel | hikari.models.bases.Snowflake | int | str] - The first channel to change the position of. This is a tuple of the - integer position the channel object or ID. - *additional_channels: typing.Tuple[int, hikari.models.channels.GuildChannel | hikari.models.bases.Snowflake | int | str] - Optional additional channels to change the position of. These must - be tuples of integer positions to change to and the channel object - or ID and the. - - Raises - ------ - hikari.errors.NotFound - If either the guild or any of the channels aren't found. - hikari.errors.Forbidden - If you either lack the `MANAGE_CHANNELS` permission or are not a - member of said guild or are not in the guild. - hikari.errors.BadRequest - If you provide anything other than the `id` and `position` - fields for the channels. - If any invalid snowflake IDs are passed; a snowflake may be invalid - due to it being outside of the range of a 64 bit integer. - """ - await self._session.modify_guild_channel_positions( - str(guild.id if isinstance(guild, bases.Unique) else int(guild)), - *[ - (str(channel.id if isinstance(channel, bases.Unique) else int(channel)), position) - for position, channel in [channel, *additional_channels] - ], - ) - - async def fetch_member( - self, - guild: typing.Union[bases.Snowflake, int, str, guilds.Guild], - user: typing.Union[bases.Snowflake, int, str, users.User], - ) -> guilds.GuildMember: - """Get a given guild member. - - Parameters - ---------- - guild : hikari.models.guilds.Guild | hikari.models.bases.Snowflake | int | str - The object or ID of the guild to get the member from. - user : hikari.models.users.User | hikari.models.bases.Snowflake | int | str - The object or ID of the member to get. - - Returns - ------- - hikari.models.guilds.GuildMember - The requested member object. - - Raises - ------ - hikari.errors.BadRequest - If any invalid snowflake IDs are passed; a snowflake may be invalid - due to it being outside of the range of a 64 bit integer. - hikari.errors.NotFound - If either the guild or the member aren't found. - hikari.errors.Forbidden - If you don't have access to the target guild. - """ - payload = await self._session.get_guild_member( - guild_id=str(guild.id if isinstance(guild, bases.Unique) else int(guild)), - user_id=str(user.id if isinstance(user, bases.Unique) else int(user)), - ) - return guilds.GuildMember.deserialize(payload, app=self._app) - - def fetch_members( - self, guild: typing.Union[bases.Snowflake, int, str, guilds.Guild], - ) -> iterators.LazyIterator[guilds.GuildMember]: - """Get an async iterator of all the members in a given guild. - - Parameters - ---------- - guild : hikari.models.guilds.Guild | hikari.models.bases.Snowflake | int | str - The object or ID of the guild to get the members from. - - Examples - -------- - async for user in client.fetch_members(guild): - if member.user.username[0] in HOIST_BLACKLIST: - await client.update_member(member, nickname="💩") - - Returns - ------- - hikari.models.pagination.LazyIterator[[hikari.models.guilds.GuildMember] - An async iterator of member objects. - - Raises - ------ - hikari.errors.BadRequest - If any invalid snowflake IDs are passed; a snowflake may be invalid - due to it being outside of the range of a 64 bit integer. - hikari.errors.NotFound - If the guild is not found. - hikari.errors.Forbidden - If you are not in the guild. - """ - return _MemberPaginator(app=self._app, guild=guild, created_after=None, session=self._session) - - async def update_member( # pylint: disable=too-many-arguments - self, - guild: typing.Union[bases.Snowflake, int, str, guilds.Guild], - user: typing.Union[bases.Snowflake, int, str, users.User], - nickname: typing.Optional[str] = ..., - roles: typing.Sequence[typing.Union[bases.Snowflake, int, str, guilds.Role]] = ..., - mute: bool = ..., - deaf: bool = ..., - voice_channel: typing.Optional[typing.Union[bases.Snowflake, int, str, channels_.GuildVoiceChannel]] = ..., - reason: str = ..., - ) -> None: - """Edits a guild's member, any unspecified fields will not be changed. - - Parameters - ---------- - guild : hikari.models.guilds.Guild | hikari.models.bases.Snowflake | int | str - The object or ID of the guild to edit the member from. - user : hikari.models.users.User | hikari.models.bases.Snowflake | int | str - The object or ID of the member to edit. - nickname : str | None - If specified, the new nickname string. Setting it to `None` - explicitly will clear the nickname. - roles : typing.Sequence[hikari.models.guilds.Role | hikari.models.bases.Snowflake | int | str] - If specified, a list of role IDs the member should have. - mute : bool - If specified, whether the user should be muted in the voice channel - or not. - deaf : bool - If specified, whether the user should be deafen in the voice - channel or not. - voice_channel : hikari.models.channels.GuildVoiceChannel | hikari.models.bases.Snowflake | int | str | None - If specified, the ID of the channel to move the member to. Setting - it to `None` explicitly will disconnect the user. - reason : str - If specified, the audit log reason explaining why the operation - was performed. - - Raises - ------ - hikari.errors.NotFound - If either the guild, user, channel or any of the roles aren't found. - hikari.errors.Forbidden - If you lack any of the applicable permissions (`MANAGE_NICKNAMES`, - `MANAGE_ROLES`, `MUTE_MEMBERS`, `DEAFEN_MEMBERS` or `MOVE_MEMBERS`). - Note that to move a member you must also have permission to connect - to the end channel. This will also be raised if you're not in the - guild. - hikari.errors.BadRequest - If you pass `mute`, `deaf` or `channel` while the member - is not connected to a voice channel. - If any invalid snowflake IDs are passed; a snowflake may be invalid - due to it being outside of the range of a 64 bit integer. - """ - await self._session.modify_guild_member( - guild_id=str(guild.id if isinstance(guild, bases.Unique) else int(guild)), - user_id=str(user.id if isinstance(user, bases.Unique) else int(user)), - nick=nickname, - roles=( - [str(role.id if isinstance(role, bases.Unique) else int(role)) for role in roles] - if roles is not ... - else ... - ), - mute=mute, - deaf=deaf, - channel_id=( - str(voice_channel.id if isinstance(voice_channel, bases.Unique) else int(voice_channel)) - if voice_channel is not ... - else ... - ), - reason=reason, - ) - - async def update_my_member_nickname( - self, - guild: typing.Union[bases.Snowflake, int, str, guilds.Guild], - nickname: typing.Optional[str], - *, - reason: str = ..., - ) -> None: - """Edits the current user's nickname for a given guild. - - Parameters - ---------- - guild : hikari.models.guilds.Guild | hikari.models.bases.Snowflake | int | str - The object or ID of the guild you want to change the nick on. - nickname : str | None - The new nick string. Setting this to `None` clears the nickname. - reason : str - If specified, the audit log reason explaining why the operation - was performed. - - Raises - ------ - hikari.errors.NotFound - If the guild is not found. - hikari.errors.Forbidden - If you lack the `CHANGE_NICKNAME` permission or are not in the - guild. - hikari.errors.BadRequest - If you provide a disallowed nickname, one that is too long, or one - that is empty. - If any invalid snowflake IDs are passed; a snowflake may be invalid - due to it being outside of the range of a 64 bit integer. - """ - await self._session.modify_current_user_nick( - guild_id=str(guild.id if isinstance(guild, bases.Unique) else int(guild)), nick=nickname, reason=reason, - ) - - async def add_role_to_member( - self, - guild: typing.Union[bases.Snowflake, int, str, guilds.Guild], - user: typing.Union[bases.Snowflake, int, str, users.User], - role: typing.Union[bases.Snowflake, int, str, guilds.Role], - *, - reason: str = ..., - ) -> None: - """Add a role to a given member. - - Parameters - ---------- - guild : hikari.models.guilds.Guild | hikari.models.bases.Snowflake | int | str - The object or ID of the guild the member belongs to. - user : hikari.models.users.User | hikari.models.bases.Snowflake | int | str - The object or ID of the member you want to add the role to. - role : hikari.models.guilds.Role | hikari.models.bases.Snowflake | int | str - The object or ID of the role you want to add. - reason : str - If specified, the audit log reason explaining why the operation - was performed. - - Raises - ------ - hikari.errors.BadRequest - If any invalid snowflake IDs are passed; a snowflake may be invalid - due to it being outside of the range of a 64 bit integer. - hikari.errors.NotFound - If either the guild, member or role aren't found. - hikari.errors.Forbidden - If you lack the `MANAGE_ROLES` permission or are not in the guild. - """ - await self._session.add_guild_member_role( - guild_id=str(guild.id if isinstance(guild, bases.Unique) else int(guild)), - user_id=str(user.id if isinstance(user, bases.Unique) else int(user)), - role_id=str(role.id if isinstance(role, bases.Unique) else int(role)), - reason=reason, - ) - - async def remove_role_from_member( - self, - guild: typing.Union[bases.Snowflake, int, str, guilds.Guild], - user: typing.Union[bases.Snowflake, int, str, users.User], - role: typing.Union[bases.Snowflake, int, str, guilds.Role], - *, - reason: str = ..., - ) -> None: - """Remove a role from a given member. - - Parameters - ---------- - guild : hikari.models.guilds.Guild | hikari.models.bases.Snowflake | int | str - The object or ID of the guild the member belongs to. - user : hikari.models.users.User | hikari.models.bases.Snowflake | int | str - The object or ID of the member you want to remove the role from. - role : hikari.models.guilds.Role | hikari.models.bases.Snowflake | int | str - The object or ID of the role you want to remove. - reason : str - If specified, the audit log reason explaining why the operation - was performed. - - Raises - ------ - hikari.errors.BadRequest - If any invalid snowflake IDs are passed; a snowflake may be invalid - due to it being outside of the range of a 64 bit integer. - hikari.errors.NotFound - If either the guild, member or role aren't found. - hikari.errors.Forbidden - If you lack the `MANAGE_ROLES` permission or are not in the guild. - """ - await self._session.remove_guild_member_role( - guild_id=str(guild.id if isinstance(guild, bases.Unique) else int(guild)), - user_id=str(user.id if isinstance(user, bases.Unique) else int(user)), - role_id=str(role.id if isinstance(role, bases.Unique) else int(role)), - reason=reason, - ) - - async def kick_member( - self, - guild: typing.Union[bases.Snowflake, int, str, guilds.Guild], - user: typing.Union[bases.Snowflake, int, str, users.User], - *, - reason: str = ..., - ) -> None: - """Kicks a user from a given guild. - - Parameters - ---------- - guild : hikari.models.guilds.Guild | hikari.models.bases.Snowflake | int | str - The object or ID of the guild the member belongs to. - user : hikari.models.users.User | hikari.models.bases.Snowflake | int | str - The object or ID of the member you want to kick. - reason : str - If specified, the audit log reason explaining why the operation - was performed. - - Raises - ------ - hikari.errors.BadRequest - If any invalid snowflake IDs are passed; a snowflake may be invalid - due to it being outside of the range of a 64 bit integer. - hikari.errors.NotFound - If either the guild or member aren't found. - hikari.errors.Forbidden - If you lack the `KICK_MEMBERS` permission or are not in the guild. - """ - await self._session.remove_guild_member( - guild_id=str(guild.id if isinstance(guild, bases.Unique) else int(guild)), - user_id=str(user.id if isinstance(user, bases.Unique) else int(user)), - reason=reason, - ) - - async def fetch_ban( - self, - guild: typing.Union[bases.Snowflake, int, str, guilds.Guild], - user: typing.Union[bases.Snowflake, int, str, users.User], - ) -> guilds.GuildMemberBan: - """Get a ban from a given guild. - - Parameters - ---------- - guild : hikari.models.guilds.Guild | hikari.models.bases.Snowflake | int | str - The object or ID of the guild you want to get the ban from. - user : hikari.models.users.User | hikari.models.bases.Snowflake | int | str - The object or ID of the user to get the ban information for. - - Returns - ------- - hikari.models.guilds.GuildMemberBan - A ban object for the requested user. - - Raises - ------ - hikari.errors.BadRequest - If any invalid snowflake IDs are passed; a snowflake may be invalid - due to it being outside of the range of a 64 bit integer. - hikari.errors.NotFound - If either the guild or the user aren't found, or if the user is not - banned. - hikari.errors.Forbidden - If you lack the `BAN_MEMBERS` permission or are not in the guild. - """ - payload = await self._session.get_guild_ban( - guild_id=str(guild.id if isinstance(guild, bases.Unique) else int(guild)), - user_id=str(user.id if isinstance(user, bases.Unique) else int(user)), - ) - return guilds.GuildMemberBan.deserialize(payload, app=self._app) - - async def fetch_bans( - self, guild: typing.Union[bases.Snowflake, int, str, guilds.Guild] - ) -> typing.Sequence[guilds.GuildMemberBan]: - """Get the bans for a given guild. - - Parameters - ---------- - guild : hikari.models.guilds.Guild | hikari.models.bases.Snowflake | int | str - The object or ID of the guild you want to get the bans from. - - Returns - ------- - typing.Sequence[hikari.models.guilds.GuildMemberBan] - A list of ban objects. - - Raises - ------ - hikari.errors.BadRequest - If any invalid snowflake IDs are passed; a snowflake may be invalid - due to it being outside of the range of a 64 bit integer. - hikari.errors.NotFound - If the guild is not found. - hikari.errors.Forbidden - If you lack the `BAN_MEMBERS` permission or are not in the guild. - """ - payload = await self._session.get_guild_bans( - guild_id=str(guild.id if isinstance(guild, bases.Unique) else int(guild)) - ) - return [guilds.GuildMemberBan.deserialize(ban, app=self._app) for ban in payload] - - async def ban_member( - self, - guild: typing.Union[bases.Snowflake, int, str, guilds.Guild], - user: typing.Union[bases.Snowflake, int, str, users.User], - *, - delete_message_days: typing.Union[datetime.timedelta, int] = ..., - reason: str = ..., - ) -> None: - """Bans a user from a given guild. - - Parameters - ---------- - guild : hikari.models.guilds.Guild | hikari.models.bases.Snowflake | int | str - The object or ID of the guild the member belongs to. - user : hikari.models.users.User | hikari.models.bases.Snowflake | int | str - The object or ID of the member you want to ban. - delete_message_days : datetime.timedelta | int - If specified, the tim delta of how many days of messages from the - user should be removed. - reason : str - If specified, the audit log reason explaining why the operation - was performed. - - Raises - ------ - hikari.errors.BadRequest - If any invalid snowflake IDs are passed; a snowflake may be invalid - due to it being outside of the range of a 64 bit integer. - hikari.errors.NotFound - If either the guild or member aren't found. - hikari.errors.Forbidden - If you lack the `BAN_MEMBERS` permission or are not in the guild. - """ - await self._session.create_guild_ban( - guild_id=str(guild.id if isinstance(guild, bases.Unique) else int(guild)), - user_id=str(user.id if isinstance(user, bases.Unique) else int(user)), - delete_message_days=getattr(delete_message_days, "days", delete_message_days), - reason=reason, - ) - - async def unban_member( - self, - guild: typing.Union[bases.Snowflake, int, str, guilds.Guild], - user: typing.Union[bases.Snowflake, int, str, users.User], - *, - reason: str = ..., - ) -> None: - """Un-bans a user from a given guild. - - Parameters - ---------- - guild : hikari.models.guilds.Guild | hikari.models.bases.Snowflake | int | str - The object or ID of the guild to un-ban the user from. - user : hikari.models.users.User | hikari.models.bases.Snowflake | int | str - The ID of the user you want to un-ban. - reason : str - If specified, the audit log reason explaining why the operation - was performed. - - Raises - ------ - hikari.errors.BadRequest - If any invalid snowflake IDs are passed; a snowflake may be invalid - due to it being outside of the range of a 64 bit integer. - hikari.errors.NotFound - If either the guild or member aren't found, or the member is not - banned. - hikari.errors.Forbidden - If you lack the `BAN_MEMBERS` permission or are not a in the - guild. - """ - await self._session.remove_guild_ban( - guild_id=str(guild.id if isinstance(guild, bases.Unique) else int(guild)), - user_id=str(user.id if isinstance(user, bases.Unique) else int(user)), - reason=reason, - ) - - async def fetch_roles( - self, guild: typing.Union[bases.Snowflake, int, str, guilds.Guild], - ) -> typing.Mapping[bases.Snowflake, guilds.Role]: - """Get the roles for a given guild. - - Parameters - ---------- - guild : hikari.models.guilds.Guild | hikari.models.bases.Snowflake | int | str - The object or ID of the guild you want to get the roles from. - - Returns - ------- - typing.Mapping[hikari.models.bases.Snowflake, hikari.models.guilds.Role] - A list of role objects. - - Raises - ------ - hikari.errors.BadRequest - If any invalid snowflake IDs are passed; a snowflake may be invalid - due to it being outside of the range of a 64 bit integer. - hikari.errors.NotFound - If the guild is not found. - hikari.errors.Forbidden - If you're not in the guild. - """ - payload = await self._session.get_guild_roles( - guild_id=str(guild.id if isinstance(guild, bases.Unique) else int(guild)) - ) - return {bases.Snowflake(role["id"]): guilds.Role.deserialize(role, app=self._app) for role in payload} - - async def create_role( - self, - guild: typing.Union[bases.Snowflake, int, str, guilds.Guild], - *, - name: str = ..., - permissions: typing.Union[permissions_.Permission, int] = ..., - color: typing.Union[colors.Color, int] = ..., - hoist: bool = ..., - mentionable: bool = ..., - reason: str = ..., - ) -> guilds.Role: - """Create a new role for a given guild. - - Parameters - ---------- - guild : hikari.models.guilds.Guild | hikari.models.bases.Snowflake | int | str - The object or ID of the guild you want to create the role on. - name : str - If specified, the new role name string. - permissions : hikari.permissions.Permission | int - If specified, the permissions integer for the role, passing a raw - integer rather than the int flag may lead to unexpected results. - color : hikari.colors.Color | int - If specified, the color for the role. - hoist : bool - If specified, whether the role will be hoisted. - mentionable : bool - If specified, whether the role will be able to be mentioned by any - user. - reason : str - If specified, the audit log reason explaining why the operation - was performed. - - Returns - ------- - hikari.models.guilds.Role - The newly created role object. - - Raises - ------ - hikari.errors.NotFound - If the guild is not found. - hikari.errors.Forbidden - If you lack the `MANAGE_ROLES` permission or you're not in the - guild. - hikari.errors.BadRequest - If you provide invalid values for the role attributes. - If any invalid snowflake IDs are passed; a snowflake may be invalid - due to it being outside of the range of a 64 bit integer. - """ - payload = await self._session.create_guild_role( - guild_id=str(guild.id if isinstance(guild, bases.Unique) else int(guild)), - name=name, - permissions=permissions, - color=color, - hoist=hoist, - mentionable=mentionable, - reason=reason, - ) - return guilds.Role.deserialize(payload, app=self._app) - - async def reposition_roles( - self, - guild: typing.Union[bases.Snowflake, int, str, guilds.Guild], - role: typing.Tuple[int, typing.Union[bases.Snowflake, int, str, guilds.Role]], - *additional_roles: typing.Tuple[int, typing.Union[bases.Snowflake, int, str, guilds.Role]], - ) -> typing.Sequence[guilds.Role]: - """Edits the position of two or more roles in a given guild. - - Parameters - ---------- - guild : hikari.models.guilds.Guild | hikari.models.bases.Snowflake | int | str - The ID of the guild the roles belong to. - role : typing.Tuple[int, hikari.models.guilds.Role | hikari.models.bases.Snowflake | int | str] - The first role to move. This is a tuple of the integer position and - the role object or ID. - *additional_roles : typing.Tuple[int, thikari.guilds.GuildRole | hikari.models.bases.Snowflake | int | str] - Optional extra roles to move. These must be tuples of the integer - position and the role object or ID. - - Returns - ------- - typing.Sequence[hikari.models.guilds.Role] - A list of all the guild roles. - - Raises - ------ - hikari.errors.NotFound - If either the guild or any of the roles aren't found. - hikari.errors.Forbidden - If you lack the `MANAGE_ROLES` permission or you're not in the - guild. - hikari.errors.BadRequest - If you provide invalid values for the `position` fields. - If any invalid snowflake IDs are passed; a snowflake may be invalid - due to it being outside of the range of a 64 bit integer. - """ - payload = await self._session.modify_guild_role_positions( - str(guild.id if isinstance(guild, bases.Unique) else int(guild)), - *[ - (str(channel.id if isinstance(channel, bases.Unique) else int(channel)), position) - for position, channel in [role, *additional_roles] - ], - ) - return [guilds.Role.deserialize(role, app=self._app) for role in payload] - - async def update_role( - self, - guild: typing.Union[bases.Snowflake, int, str, guilds.Guild], - role: typing.Union[bases.Snowflake, int, str, guilds.Role], - *, - name: str = ..., - permissions: typing.Union[permissions_.Permission, int] = ..., - color: typing.Union[colors.Color, int] = ..., - hoist: bool = ..., - mentionable: bool = ..., - reason: str = ..., - ) -> guilds.Role: - """Edits a role in a given guild. - - Parameters - ---------- - guild : hikari.models.guilds.Guild | hikari.models.bases.Snowflake | int | str - The object or ID of the guild the role belong to. - role : hikari.models.guilds.Role | hikari.models.bases.Snowflake | int | str - The object or ID of the role you want to edit. - name : str - If specified, the new role's name string. - permissions : hikari.permissions.Permission | int - If specified, the new permissions integer for the role, passing a - raw integer for this may lead to unexpected behaviour. - color : hikari.colors.Color | int - If specified, the new color for the new role passing a raw integer - for this may lead to unexpected behaviour. - hoist : bool - If specified, whether the role should hoist or not. - mentionable : bool - If specified, whether the role should be mentionable or not. - reason : str - If specified, the audit log reason explaining why the operation - was performed. - - Returns - ------- - hikari.models.guilds.Role - The edited role object. - - Raises - ------ - hikari.errors.NotFound - If either the guild or role aren't found. - hikari.errors.Forbidden - If you lack the `MANAGE_ROLES` permission or you're not in the - guild. - hikari.errors.BadRequest - If you provide invalid values for the role attributes. - If any invalid snowflake IDs are passed; a snowflake may be invalid - due to it being outside of the range of a 64 bit integer. - """ - payload = await self._session.modify_guild_role( - guild_id=str(guild.id if isinstance(guild, bases.Unique) else int(guild)), - role_id=str(role.id if isinstance(role, bases.Unique) else int(role)), - name=name, - permissions=permissions, - color=color, - hoist=hoist, - mentionable=mentionable, - reason=reason, - ) - return guilds.Role.deserialize(payload, app=self._app) - - async def delete_role( - self, - guild: typing.Union[bases.Snowflake, int, str, guilds.Guild], - role: typing.Union[bases.Snowflake, int, str, guilds.Role], - ) -> None: - """Delete a role from a given guild. - - Parameters - ---------- - guild : hikari.models.guilds.Guild | hikari.models.bases.Snowflake | int | str - The object or ID of the guild you want to remove the role from. - role : hikari.models.guilds.Role | hikari.models.bases.Snowflake | int | str - The object or ID of the role you want to delete. - - Raises - ------ - hikari.errors.BadRequest - If any invalid snowflake IDs are passed; a snowflake may be invalid - due to it being outside of the range of a 64 bit integer. - hikari.errors.NotFound - If either the guild or the role aren't found. - hikari.errors.Forbidden - If you lack the `MANAGE_ROLES` permission or are not in the guild. - """ - await self._session.delete_guild_role( - guild_id=str(guild.id if isinstance(guild, bases.Unique) else int(guild)), - role_id=str(role.id if isinstance(role, bases.Unique) else int(role)), - ) - - async def estimate_guild_prune_count( - self, guild: typing.Union[bases.Snowflake, int, str, guilds.Guild], days: typing.Union[datetime.timedelta, int], - ) -> int: - """Get the estimated prune count for a given guild. - - Parameters - ---------- - guild : hikari.models.guilds.Guild | hikari.models.bases.Snowflake | int | str - The object or ID of the guild you want to get the count for. - days : datetime.timedelta | int - The time delta of days to count prune for (at least `1`). - - Returns - ------- - int - The number of members estimated to be pruned. - - Raises - ------ - hikari.errors.NotFound - If the guild is not found. - hikari.errors.Forbidden - If you lack the `KICK_MEMBERS` or you are not in the guild. - hikari.errors.BadRequest - If you pass an invalid amount of days. - If any invalid snowflake IDs are passed; a snowflake may be invalid - due to it being outside of the range of a 64 bit integer. - """ - return await self._session.get_guild_prune_count( - guild_id=str(guild.id if isinstance(guild, bases.Unique) else int(guild)), days=getattr(days, "days", days), - ) - - async def begin_guild_prune( - self, - guild: typing.Union[bases.Snowflake, int, str, guilds.Guild], - days: typing.Union[datetime.timedelta, int], - *, - compute_prune_count: bool = ..., - reason: str = ..., - ) -> int: - """Prunes members of a given guild based on the number of inactive days. - - Parameters - ---------- - guild : hikari.models.guilds.Guild | hikari.models.bases.Snowflake | int | str - The object or ID of the guild you want to prune member of. - days : datetime.timedelta | int - The time delta of inactivity days you want to use as filter. - compute_prune_count : bool - Whether a count of pruned members is returned or not. - Discouraged for large guilds out of politeness. - reason : str - If specified, the audit log reason explaining why the operation - was performed. - - Returns - ------- - int | None - The number of members who were kicked if `compute_prune_count` - is `True`, else `None`. - - Raises - ------ - hikari.errors.NotFound - If the guild is not found: - hikari.errors.Forbidden - If you lack the `KICK_MEMBER` permission or are not in the guild. - hikari.errors.BadRequest - If you provide invalid values for the `days` or - `compute_prune_count` fields. - If any invalid snowflake IDs are passed; a snowflake may be invalid - due to it being outside of the range of a 64 bit integer. - """ - return await self._session.begin_guild_prune( - guild_id=str(guild.id if isinstance(guild, bases.Unique) else int(guild)), - days=getattr(days, "days", days), - compute_prune_count=compute_prune_count, - reason=reason, - ) - - async def fetch_guild_voice_regions( - self, guild: typing.Union[bases.Snowflake, int, str, guilds.Guild], - ) -> typing.Sequence[voices.VoiceRegion]: - """Get the voice regions for a given guild. - - Parameters - ---------- - guild : hikari.models.guilds.Guild | hikari.models.bases.Snowflake | int | str - The object or ID of the guild to get the voice regions for. - - Returns - ------- - typing.Sequence[hikari.models.voices.VoiceRegion] - A list of voice region objects. - - Raises - ------ - hikari.errors.BadRequest - If any invalid snowflake IDs are passed; a snowflake may be invalid - due to it being outside of the range of a 64 bit integer. - hikari.errors.NotFound - If the guild is not found. - hikari.errors.Forbidden - If you are not in the guild. - """ - payload = await self._session.get_guild_voice_regions( - guild_id=str(guild.id if isinstance(guild, bases.Unique) else int(guild)) - ) - return [voices.VoiceRegion.deserialize(region, app=self._app) for region in payload] - - async def fetch_guild_invites( - self, guild: typing.Union[bases.Snowflake, int, str, guilds.Guild], - ) -> typing.Sequence[invites.InviteWithMetadata]: - """Get the invites for a given guild. - - Parameters - ---------- - guild : hikari.models.guilds.Guild | hikari.models.bases.Snowflake | int | str - The object or ID of the guild to get the invites for. - - Returns - ------- - typing.Sequence[hikari.models.invites.InviteWithMetadata] - A list of invite objects (with metadata). - - Raises - ------ - hikari.errors.BadRequest - If any invalid snowflake IDs are passed; a snowflake may be invalid - due to it being outside of the range of a 64 bit integer. - hikari.errors.NotFound - If the guild is not found. - hikari.errors.Forbidden - If you lack the `MANAGE_GUILD` permission or are not in the guild. - """ - payload = await self._session.get_guild_invites( - guild_id=str(guild.id if isinstance(guild, bases.Unique) else int(guild)) - ) - return [invites.InviteWithMetadata.deserialize(invite, app=self._app) for invite in payload] - - async def fetch_integrations( - self, guild: typing.Union[bases.Snowflake, int, str, guilds.Guild] - ) -> typing.Sequence[guilds.GuildIntegration]: - """Get the integrations for a given guild. - - Parameters - ---------- - guild : hikari.models.guilds.Guild | hikari.models.bases.Snowflake | int | str - The object or ID of the guild to get the integrations for. - - Returns - ------- - typing.Sequence[hikari.models.guilds.GuildIntegration] - A list of integration objects. - - Raises - ------ - hikari.errors.BadRequest - If any invalid snowflake IDs are passed; a snowflake may be invalid - due to it being outside of the range of a 64 bit integer. - hikari.errors.NotFound - If the guild is not found. - hikari.errors.Forbidden - If you lack the `MANAGE_GUILD` permission or are not in the guild. - """ - payload = await self._session.get_guild_integrations( - guild_id=str(guild.id if isinstance(guild, bases.Unique) else int(guild)) - ) - return [guilds.GuildIntegration.deserialize(integration, app=self._app) for integration in payload] - - async def update_integration( - self, - guild: typing.Union[bases.Snowflake, int, str, guilds.Guild], - integration: typing.Union[bases.Snowflake, int, str, guilds.GuildIntegration], - *, - expire_behaviour: typing.Union[guilds.IntegrationExpireBehaviour, int] = ..., - expire_grace_period: typing.Union[datetime.timedelta, int] = ..., - enable_emojis: bool = ..., - reason: str = ..., - ) -> None: - """Edits an integrations for a given guild. - - Parameters - ---------- - guild : hikari.models.guilds.Guild | hikari.models.bases.Snowflake | int | str - The object or ID of the guild to which the integration belongs to. - integration : hikari.models.guilds.GuildIntegration | hikari.models.bases.Snowflake | int | str - The object or ID of the integration to update. - expire_behaviour : hikari.models.guilds.IntegrationExpireBehaviour | int - If specified, the behaviour for when an integration subscription - expires (passing a raw integer for this may lead to unexpected - behaviour). - expire_grace_period : datetime.timedelta | int - If specified, time time delta of how many days the integration will - ignore lapsed subscriptions for. - enable_emojis : bool - If specified, whether emojis should be synced for this integration. - reason : str - If specified, the audit log reason explaining why the operation - was performed. - - Raises - ------ - hikari.errors.BadRequest - If any invalid snowflake IDs are passed; a snowflake may be invalid - due to it being outside of the range of a 64 bit integer. - hikari.errors.NotFound - If either the guild or the integration aren't found. - hikari.errors.Forbidden - If you lack the `MANAGE_GUILD` permission or are not in the guild. - """ - await self._session.modify_guild_integration( - guild_id=str(guild.id if isinstance(guild, bases.Unique) else int(guild)), - integration_id=str(integration.id if isinstance(integration, bases.Unique) else int(integration)), - expire_behaviour=expire_behaviour, - expire_grace_period=getattr(expire_grace_period, "days", expire_grace_period), - enable_emojis=enable_emojis, - reason=reason, - ) - - async def delete_integration( - self, - guild: typing.Union[bases.Snowflake, int, str, guilds.Guild], - integration: typing.Union[bases.Snowflake, int, str, guilds.GuildIntegration], - *, - reason: str = ..., - ) -> None: - """Delete an integration for the given guild. - - Parameters - ---------- - guild : hikari.models.guilds.Guild | hikari.models.bases.Snowflake | int | str - The object or ID of the guild to which the integration belongs to. - integration : hikari.models.guilds.GuildIntegration | hikari.models.bases.Snowflake | int | str - The object or ID of the integration to delete. - reason : str - If specified, the audit log reason explaining why the operation - was performed. - - Raises - ------ - hikari.errors.BadRequest - If any invalid snowflake IDs are passed; a snowflake may be invalid - due to it being outside of the range of a 64 bit integer. - hikari.errors.NotFound - If either the guild or the integration aren't found. - hikari.errors.Forbidden - If you lack the `MANAGE_GUILD` permission or are not in the guild. - """ - await self._session.delete_integration( - guild_id=str(guild.id if isinstance(guild, bases.Unique) else int(guild)), - integration_id=str(integration.id if isinstance(integration, bases.Unique) else int(integration)), - reason=reason, - ) - - async def sync_guild_integration( - self, - guild: typing.Union[bases.Snowflake, int, str, guilds.Guild], - integration: typing.Union[bases.Snowflake, int, str, guilds.GuildIntegration], - ) -> None: - """Sync the given integration's subscribers/emojis. - - Parameters - ---------- - guild : hikari.models.guilds.Guild | hikari.models.bases.Snowflake | int | str - The object or ID of the guild to which the integration belongs to. - integration : hikari.models.guilds.GuildIntegration | hikari.models.bases.Snowflake | int | str - The ID of the integration to sync. - - Raises - ------ - hikari.errors.BadRequest - If any invalid snowflake IDs are passed; a snowflake may be invalid - due to it being outside of the range of a 64 bit integer. - hikari.errors.NotFound - If either the guild or the integration aren't found. - hikari.errors.Forbidden - If you lack the `MANAGE_GUILD` permission or are not in the guild. - """ - await self._session.sync_guild_integration( - guild_id=str(guild.id if isinstance(guild, bases.Unique) else int(guild)), - integration_id=str(integration.id if isinstance(integration, bases.Unique) else int(integration)), - ) - - async def fetch_guild_embed( - self, guild: typing.Union[bases.Snowflake, int, str, guilds.Guild] - ) -> guilds.GuildWidget: - """Get the embed for a given guild. - - Parameters - ---------- - guild : hikari.models.guilds.Guild | hikari.models.bases.Snowflake | int | str - The object or ID of the guild to get the embed for. - - Returns - ------- - hikari.models.guilds.GuildWidget - A guild embed object. - - Raises - ------ - hikari.errors.BadRequest - If any invalid snowflake IDs are passed; a snowflake may be invalid - due to it being outside of the range of a 64 bit integer. - hikari.errors.NotFound - If the guild is not found. - hikari.errors.Forbidden - If you either lack the `MANAGE_GUILD` permission or are not in - the guild. - """ - payload = await self._session.get_guild_embed( - guild_id=str(guild.id if isinstance(guild, bases.Unique) else int(guild)) - ) - return guilds.GuildWidget.deserialize(payload, app=self._app) - - async def update_guild_embed( - self, - guild: typing.Union[bases.Snowflake, int, str, guilds.Guild], - *, - channel: typing.Union[bases.Snowflake, int, str, channels_.GuildChannel] = ..., - enabled: bool = ..., - reason: str = ..., - ) -> guilds.GuildWidget: - """Edits the embed for a given guild. - - Parameters - ---------- - guild : hikari.models.guilds.Guild | hikari.models.bases.Snowflake | int | str - The object or ID of the guild to edit the embed for. - channel : hikari.models.guilds.GuildChannel | hikari.models.bases.Snowflake | int | str | None - If specified, the object or ID of the channel that this embed's - invite should target. Set to `None` to disable invites for this - embed. - enabled : bool - If specified, whether this embed should be enabled. - reason : str - If specified, the audit log reason explaining why the operation - was performed. - - Returns - ------- - hikari.models.guilds.GuildWidget - The updated embed object. - - Raises - ------ - hikari.errors.BadRequest - If any invalid snowflake IDs are passed; a snowflake may be invalid - due to it being outside of the range of a 64 bit integer. - hikari.errors.NotFound - If the guild is not found. - hikari.errors.Forbidden - If you either lack the `MANAGE_GUILD` permission or are not in - the guild. - """ - payload = await self._session.modify_guild_embed( - guild_id=str(guild.id if isinstance(guild, bases.Unique) else int(guild)), - channel_id=( - str(channel.id if isinstance(channel, bases.Unique) else int(channel)) if channel is not ... else ... - ), - enabled=enabled, - reason=reason, - ) - return guilds.GuildWidget.deserialize(payload, app=self._app) - - async def fetch_guild_vanity_url( - self, guild: typing.Union[bases.Snowflake, int, str, guilds.Guild] - ) -> invites.VanityURL: - """ - Get the vanity URL for a given guild. - - Parameters - ---------- - guild : hikari.models.guilds.Guild | hikari.models.bases.Snowflake | int | str - The object or ID of the guild to get the vanity URL for. - - Returns - ------- - hikari.models.invites.VanityURL - A partial invite object containing the vanity URL in the `code` - field. - - Raises - ------ - hikari.errors.BadRequest - If any invalid snowflake IDs are passed; a snowflake may be invalid - due to it being outside of the range of a 64 bit integer. - hikari.errors.NotFound - If the guild is not found. - hikari.errors.Forbidden - If you either lack the `MANAGE_GUILD` permission or are not in - the guild. - """ - payload = await self._session.get_guild_vanity_url( - guild_id=str(guild.id if isinstance(guild, bases.Unique) else int(guild)) - ) - return invites.VanityURL.deserialize(payload, app=self._app) - - def format_guild_widget_image( - self, guild: typing.Union[bases.Snowflake, int, str, guilds.Guild], *, style: str = ... - ) -> str: - """Get the URL for a guild widget. - - Parameters - ---------- - guild : hikari.models.guilds.Guild | hikari.models.bases.Snowflake | int | str - The object or ID of the guild to form the widget. - style : str - If specified, the style of the widget. - - Returns - ------- - str - A URL to retrieve a PNG widget for your guild. - - !!! note - This does not actually make any form of request, and shouldn't be - awaited. Thus, it doesn't have rate limits either. - - !!! warning - The guild must have the widget enabled in the guild settings for - this to be valid. - """ - # noinspection PyTypeChecker - return self._session.get_guild_widget_image_url( - guild_id=str(guild.id if isinstance(guild, bases.Unique) else int(guild)), style=style - ) - - async def fetch_guild_webhooks( - self, guild: typing.Union[bases.Snowflake, int, str, guilds.Guild] - ) -> typing.Sequence[webhooks.Webhook]: - """Get all webhooks for a given guild. - - Parameters - ---------- - guild : hikari.models.guilds.Guild | hikari.models.bases.Snowflake | int | str - The object or ID for the guild to get the webhooks from. - - Returns - ------- - typing.Sequence[hikari.models.webhooks.Webhook] - A list of webhook objects for the given guild. - - Raises - ------ - hikari.errors.BadRequest - If any invalid snowflake IDs are passed; a snowflake may be invalid - due to it being outside of the range of a 64 bit integer. - hikari.errors.NotFound - If the guild is not found. - hikari.errors.Forbidden - If you either lack the `MANAGE_WEBHOOKS` permission or - aren't a member of the given guild. - """ - payload = await self._session.get_guild_webhooks( - guild_id=str(guild.id if isinstance(guild, bases.Unique) else int(guild)) - ) - return [webhooks.Webhook.deserialize(webhook, app=self._app) for webhook in payload] diff --git a/hikari/.rest/session.py b/hikari/.rest/session.py deleted file mode 100644 index 45b04fc061..0000000000 --- a/hikari/.rest/session.py +++ /dev/null @@ -1,3107 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# Copyright © Nekokatt 2019-2020 -# -# This file is part of Hikari. -# -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . -"""Implementation of a basic HTTP client that uses aiohttp to interact with the Discord API.""" - -from __future__ import annotations - -#### -#### DELETE -#### - -__all__ = ["RESTSession"] - -import asyncio -import contextlib -import datetime -import http -import json -import typing - -import aiohttp.typedefs - -from hikari import errors -from hikari.internal import conversions -from hikari.internal import more_collections -from hikari.internal import ratelimits -from hikari.net import buckets -from hikari.net import http_client -from hikari.net import routes -from hikari.net import urls -from hikari.net import user_agents - -if typing.TYPE_CHECKING: - import ssl - - from hikari.models import files as files_ - from hikari.internal import more_typing - -VERSION_6: typing.Final[int] = 6 -VERSION_7: typing.Final[int] = 7 - - -class _RateLimited(RuntimeError): - __slots__ = () - - -class RESTSession(http_client.HTTPClient): # pylint: disable=too-many-public-methods, too-many-instance-attributes - """A low-level RESTful client to allow you to interact with the Discord API. - - Parameters - ---------- - allow_redirects : bool - Whether to allow redirects or not. Defaults to `False`. - base_url : str - The base URL and _route for the discord API - connector : aiohttp.BaseConnector | None - Optional aiohttp _connector info for making an HTTP connection - debug : bool - Defaults to `False`. If `True`, then a lot of contextual information - regarding low-level HTTP communication will be logged to the _debug - logger on this class. - json_deserialize : deserialization function - A custom JSON deserializer function to use. Defaults to `json.loads`. - json_serialize : serialization function - A custom JSON serializer function to use. Defaults to `json.dumps`. - proxy_headers : typing.Mapping[str, str] | None - Optional proxy headers to pass to HTTP requests. - proxy_auth : aiohttp.BasicAuth | None - Optional authorization to be used if using a proxy. - proxy_url : str | None - Optional proxy URL to use for HTTP requests. - ssl_context : ssl.SSLContext | None - The optional SSL context to be used. - verify_ssl : bool - Whether or not the client should enforce SSL signed certificate - verification. If 1 it will ignore potentially malicious - SSL certificates. - timeout : float | None - The optional _request_timeout for all HTTP requests. - token : string | None - The bot token for the client to use. You may start this with - a prefix of either `Bot` or `Bearer` to force the token type, or - not provide this information if you want to have it auto-detected. - If this is passed as `None`, then no token is used. - This will be passed as the `Authorization` header if not `None` - for each request. - trust_env : bool - If `True`, and no proxy info is given, then `HTTP_PROXY` and - `HTTPS_PROXY` will be used from the environment variables if present. - Any proxy credentials will be read from the user's `netrc` file - (https://www.gnu.org/software/inetutils/manual/html_node/The-_002enetrc-file.html) - If `False`, then this information is instead ignored. - Defaults to `False`. - version : int - The version of the API to use. Defaults to the most recent stable - version (v6). - """ - - _AUTHENTICATION_SCHEMES: typing.Final[typing.Tuple[str, ...]] = ("Bearer", "Bot") - - base_url: str - """The base URL to send requests to.""" - - global_ratelimiter: ratelimits.ManualRateLimiter - """The global ratelimiter. - - This is used if Discord declares a ratelimit across the entire API, - regardless of the endpoint. If this is set, then any HTTP operation using - this session will be paused. - """ - - bucket_ratelimiters: buckets.RESTBucketManager - """The per-_route ratelimit manager. - - This handles tracking any ratelimits for routes that have recently been used - or are in active use, as well as keeping memory usage to a minimum where - possible for large numbers of varying requests. This encapsulates a lot of - complex rate limiting rules to reduce the number of active `429` responses - this client gets, and thus reducing your chances of an API ban by Discord. - - You should not ever need to touch this implementation. - """ - - user_agent: str - """The `User-Agent` header to send to Discord. - - !!! warning - Changing this value may lead to undesirable results, as Discord document - that they can actively IP ban any client that does not have a valid - `User-Agent` header that conforms to specific requirements. - Your mileage may vary (YMMV). - """ - - _verify_ssl: bool - """Whether SSL certificates should be verified for each request. - - When this is `True` then an exception will be raised whenever invalid SSL - certificates are received. When this is `False` unrecognised certificates - that may be illegitimate are accepted and ignored. - """ - - version: int - """The API version number that is being used.""" - - def __init__( # pylint: disable=too-many-locals - self, - *, - base_url: str = urls.REST_API_URL, - allow_redirects: bool = False, - connector: typing.Optional[aiohttp.BaseConnector] = None, - debug: bool = False, - proxy_auth: typing.Optional[aiohttp.BasicAuth] = None, - proxy_headers: typing.Optional[aiohttp.typedefs.LooseHeaders] = None, - proxy_url: typing.Optional[str] = None, - ssl_context: typing.Optional[ssl.SSLContext] = None, - verify_ssl: bool = True, - timeout: typing.Optional[float] = None, - trust_env: bool = False, - token: typing.Optional[str], - version: int = VERSION_6, - ) -> None: - super().__init__( - allow_redirects=allow_redirects, - connector=connector, - debug=debug, - proxy_auth=proxy_auth, - proxy_headers=proxy_headers, - proxy_url=proxy_url, - ssl_context=ssl_context, - verify_ssl=verify_ssl, - timeout=timeout, - trust_env=trust_env, - ) - self.user_agent = user_agents.UserAgent().user_agent - self.version = version - self.global_ratelimiter = ratelimits.ManualRateLimiter() - self.bucket_ratelimiters = buckets.RESTBucketManager() - - if token is not None and not token.startswith(self._AUTHENTICATION_SCHEMES): - this_type = type(self).__name__ - auth_schemes = " or ".join(self._AUTHENTICATION_SCHEMES) - raise RuntimeError(f"Any token passed to {this_type} should begin with {auth_schemes}") - - self._token = token - self.base_url = base_url.format(self) - - async def close(self) -> None: - """Shut down the RESTSession client safely, and terminate any rate limiters executing in the background.""" - with contextlib.suppress(Exception): - self.bucket_ratelimiters.close() - with contextlib.suppress(Exception): - self.global_ratelimiter.close() - await super().close() - - async def _request_json_response( - self, - compiled_route: routes.CompiledRoute, - *, - headers: typing.Optional[typing.Dict[str, str]] = None, - query: typing.Optional[more_typing.JSONObject] = None, - body: typing.Optional[typing.Union[aiohttp.FormData, dict, list]] = None, - reason: str = ..., - suppress_authorization_header: bool = False, - ) -> typing.Optional[more_typing.JSONObject, more_typing.JSONArray, bytes]: - # Make a ratelimit-protected HTTP request to a JSON endpoint and expect some form - # of JSON response. If an error occurs, the response body is returned in the - # raised exception as a bytes object. This is done since the differences between - # the V6 and V7 API error messages are not documented properly, and there are - # edge cases such as Cloudflare issues where we may receive arbitrary data in - # the response instead of a JSON object. - - if not self.bucket_ratelimiters.is_started: - self.bucket_ratelimiters.start() - - headers = {} if headers is None else headers - - headers["x-ratelimit-precision"] = "millisecond" - headers["accept"] = self._APPLICATION_JSON - - if self._token is not None and not suppress_authorization_header: - headers["authorization"] = self._token - - if reason and reason is not ...: - headers["x-audit-log-reason"] = reason - - while True: - try: - # Moved to a separate method to keep branch counts down. - return await self.__request_json_response(compiled_route, headers, body, query) - except _RateLimited: - pass - - async def __request_json_response(self, compiled_route, headers, body, query): - url = compiled_route.create_url(self.base_url) - - # Wait for any ratelimits to finish. - await asyncio.gather(self.bucket_ratelimiters.acquire(compiled_route), self.global_ratelimiter.acquire()) - - # Make the request. - response = await self._perform_request( - method=compiled_route.method, url=url, headers=headers, body=body, query=query - ) - - real_url = str(response.real_url) - - # Ensure we aren't rate limited, and update rate limiting headers where appropriate. - await self._handle_rate_limits_for_response(compiled_route, response) - - # Don't bother processing any further if we got NO CONTENT. There's not anything - # to check. - if response.status == http.HTTPStatus.NO_CONTENT: - return None - - # Decode the body. - raw_body = await response.read() - - # Handle the response. - if 200 <= response.status < 300: - if response.content_type == self._APPLICATION_JSON: - # Only deserializing here stops Cloudflare shenanigans messing us around. - return self.json_deserialize(raw_body) - raise errors.HTTPError(real_url, f"Expected JSON response but received {response.content_type}") - - if response.status == http.HTTPStatus.BAD_REQUEST: - raise errors.BadRequest(real_url, response.headers, raw_body) - if response.status == http.HTTPStatus.UNAUTHORIZED: - raise errors.Unauthorized(real_url, response.headers, raw_body) - if response.status == http.HTTPStatus.FORBIDDEN: - raise errors.Forbidden(real_url, response.headers, raw_body) - if response.status == http.HTTPStatus.NOT_FOUND: - raise errors.NotFound(real_url, response.headers, raw_body) - - status = http.HTTPStatus(response.status) - - if 400 <= status < 500: - cls = errors.ClientHTTPErrorResponse - elif 500 <= status < 600: - cls = errors.ServerHTTPErrorResponse - else: - cls = errors.HTTPErrorResponse - - raise cls(real_url, status, response.headers, raw_body) - - async def _handle_rate_limits_for_response(self, compiled_route, response): - # Worth noting there is some bug on V6 that ratelimits me immediately if I have an invalid token. - # https://github.com/discord/discord-api-docs/issues/1569 - - # Handle ratelimiting. - resp_headers = response.headers - limit = int(resp_headers.get("x-ratelimit-limit", "1")) - remaining = int(resp_headers.get("x-ratelimit-remaining", "1")) - bucket = resp_headers.get("x-ratelimit-bucket", "None") - reset = float(resp_headers.get("x-ratelimit-reset", "0")) - reset_date = datetime.datetime.fromtimestamp(reset, tz=datetime.timezone.utc) - now_date = conversions.rfc7231_datetime_string_to_datetime(resp_headers["date"]) - self.bucket_ratelimiters.update_rate_limits( - compiled_route=compiled_route, - bucket_header=bucket, - remaining_header=remaining, - limit_header=limit, - date_header=now_date, - reset_at_header=reset_date, - ) - - if response.status == http.HTTPStatus.TOO_MANY_REQUESTS: - body = await response.json() if response.content_type == self._APPLICATION_JSON else await response.read() - - # We are being rate limited. - if isinstance(body, dict): - if body.get("global", False): - retry_after = float(body["retry_after"]) / 1_000 - self.global_ratelimiter.throttle(retry_after) - - self.logger.warning( - "you are being rate-limited globally - trying again after %ss", retry_after, - ) - else: - self.logger.warning( - "you are being rate-limited on bucket %s for _route %s - trying again after %ss", - bucket, - compiled_route, - reset, - ) - - raise _RateLimited() - - # We might find out Cloudflare causes this scenario to occur. - # I hope we don't though. - raise errors.HTTPError( - str(response.real_url), - f"We were ratelimited but did not understand the response. Perhaps Cloudflare did this? {body!r}", - ) - - async def get_gateway(self) -> str: - """Get the URL to use to connect to the gateway with. - - Returns - ------- - str - A static URL to use to connect to the gateway with. - - !!! note - Users are expected to attempt to cache this result. - """ - result = await self._request_json_response(routes.GET_GATEWAY.compile()) - return result["url"] - - async def get_gateway_bot(self) -> more_typing.JSONObject: - """Get the gateway info for the bot. - - Returns - ------- - more_typing.JSONObject - An object containing a `url` to connect to, an `int` number of - shards recommended to use for connecting, and a - `session_start_limit` object. - - !!! note - Unlike `RESTSession.get_gateway`, this requires a valid token to work. - """ - return await self._request_json_response(routes.GET_GATEWAY_BOT.compile()) - - async def get_guild_audit_log( - self, guild_id: str, *, user_id: str = ..., action_type: int = ..., limit: int = ..., before: str = ... - ) -> more_typing.JSONObject: - """Get an audit log object for the given guild. - - Parameters - ---------- - guild_id : str - The guild ID to look up. - user_id : str - If specified, the user ID to filter by. - action_type : int - If specified, the action type to look up. - limit : int - If specified, the limit to apply to the number of records. - Defaults to `50`. Must be between `1` and `100` inclusive. - before : str - If specified, the ID of the entry that all retrieved entries will - have occurred before. - - Returns - ------- - more_typing.JSONObject - An audit log object. - - Raises - ------ - hikari.errors.Forbidden - If you lack the given permissions to view an audit log. - hikari.errors.NotFound - If the guild does not exist. - """ - query = {} - conversions.put_if_specified(query, "user_id", user_id) - conversions.put_if_specified(query, "event_type", action_type) - conversions.put_if_specified(query, "limit", limit) - conversions.put_if_specified(query, "before", before) - route = routes.GET_GUILD_AUDIT_LOGS.compile(guild_id=guild_id) - return await self._request_json_response(route, query=query) - - async def get_channel(self, channel_id: str) -> more_typing.JSONObject: - """Get a channel object from a given channel ID. - - Parameters - ---------- - channel_id : str - The channel ID to look up. - - Returns - ------- - more_typing.JSONObject - The channel object that has been found. - - Raises - ------ - hikari.errors.Forbidden - If you don't have access to the channel. - hikari.errors.NotFound - If the channel does not exist. - """ - route = routes.GET_CHANNEL.compile(channel_id=channel_id) - return await self._request_json_response(route) - - async def modify_channel( # lgtm [py/similar-function] - self, - channel_id: str, - *, - name: str = ..., - position: int = ..., - topic: str = ..., - nsfw: bool = ..., - rate_limit_per_user: int = ..., - bitrate: int = ..., - user_limit: int = ..., - permission_overwrites: typing.Sequence[more_typing.JSONObject] = ..., - parent_id: str = ..., - reason: str = ..., - ) -> more_typing.JSONObject: - """Update one or more aspects of a given channel ID. - - Parameters - ---------- - channel_id : str - The channel ID to update. - name : str - If specified, the new name for the channel. This must be - between `2` and `100` characters in length. - position : int - If specified, the position to change the channel to. - topic : str - If specified, the topic to set. This is only applicable to - text channels. This must be between `0` and `1024` - characters in length. - nsfw : bool - If specified, whether the channel will be marked as NSFW. - Only applicable to text channels. - rate_limit_per_user : int - If specified, the number of seconds the user has to wait before sending - another message. This will not apply to bots, or to members with - `MANAGE_MESSAGES` or `MANAGE_CHANNEL` permissions. This must - be between `0` and `21600` seconds. - bitrate : int - If specified, the bitrate in bits per second allowable for the channel. - This only applies to voice channels and must be between `8000` - and `96000` for normal servers or `8000` and `128000` for - VIP servers. - user_limit : int - If specified, the new max number of users to allow in a voice channel. - This must be between `0` and `99` inclusive, where - `0` implies no limit. - permission_overwrites : typing.Sequence[more_typing.JSONObject] - If specified, the new list of permission overwrites that are category - specific to replace the existing overwrites with. - parent_id : str | None - If specified, the new parent category ID to set for the channel. - Pass `None` to unset. - reason : str - If specified, the audit log reason explaining why the operation - was performed. - - Returns - ------- - more_typing.JSONObject - The channel object that has been modified. - - Raises - ------ - hikari.errors.NotFound - If the channel does not exist. - hikari.errors.Forbidden - If you lack the permission to make the change. - hikari.errors.BadRequest - If you provide incorrect options for the corresponding channel type - (e.g. a `bitrate` for a text channel). - """ - payload = {} - conversions.put_if_specified(payload, "name", name) - conversions.put_if_specified(payload, "position", position) - conversions.put_if_specified(payload, "topic", topic) - conversions.put_if_specified(payload, "nsfw", nsfw) - conversions.put_if_specified(payload, "rate_limit_per_user", rate_limit_per_user) - conversions.put_if_specified(payload, "bitrate", bitrate) - conversions.put_if_specified(payload, "user_limit", user_limit) - conversions.put_if_specified(payload, "permission_overwrites", permission_overwrites) - conversions.put_if_specified(payload, "parent_id", parent_id) - route = routes.PATCH_CHANNEL.compile(channel_id=channel_id) - return await self._request_json_response(route, body=payload, reason=reason) - - async def delete_close_channel(self, channel_id: str) -> None: - """Delete the given channel ID, or if it is a DM, close it. - - Parameters - ---------- - channel_id : str - The channel ID to delete, or direct message channel to close. - - Returns - ------- - None - Nothing, unlike what the API specifies. This is done to maintain - consistency with other calls of a similar nature in this API wrapper. - - Raises - ------ - hikari.errors.NotFound - If the channel does not exist. - hikari.errors.Forbidden - If you do not have permission to delete the channel. - - !!! warning - Deleted channels cannot be un-deleted. Deletion of DMs is able to be - undone by reopening the DM. - """ - route = routes.DELETE_CHANNEL.compile(channel_id=channel_id) - await self._request_json_response(route) - - async def get_channel_messages( - self, channel_id: str, *, limit: int = ..., after: str = ..., before: str = ..., around: str = ..., - ) -> typing.Sequence[more_typing.JSONObject]: - """Retrieve message history for a given channel. - - Parameters - ---------- - channel_id : str - The ID of the channel to retrieve the messages from. - limit : int - If specified, the number of messages to return. Must be - between `1` and `100` inclusive.Defaults to `50` - if unspecified. - after : str - A message ID. If specified, only return messages sent AFTER this message. - before : str - A message ID. If specified, only return messages sent BEFORE this message. - around : str - A message ID. If specified, only return messages sent AROUND and - including (if it still exists) this message. - - Returns - ------- - typing.Sequence[more_typing.JSONObject] - A list of message objects. - - Raises - ------ - hikari.errors.Forbidden - If you lack permission to read the channel. - hikari.errors.BadRequest - If your query is malformed, has an invalid value for `limit`, - or contains more than one of `after`, `before` and `around`. - hikari.errors.NotFound - If the channel is not found, or the message - provided for one of the filter arguments is not found. - - !!! note - If you are missing the `VIEW_CHANNEL` permission, you will receive a - `hikari.errors.Forbidden`. If you are instead missing - the `READ_MESSAGE_HISTORY` permission, you will always receive - zero results, and thus an empty list will be returned instead. - - !!! warning - You can only specify a maximum of one from `before`, `after`, and - `around`; specifying more than one will cause a - `hikari.errors.BadRequest` to be raised. - """ - query = {} - conversions.put_if_specified(query, "limit", limit) - conversions.put_if_specified(query, "before", before) - conversions.put_if_specified(query, "after", after) - conversions.put_if_specified(query, "around", around) - route = routes.GET_CHANNEL_MESSAGES.compile(channel_id=channel_id) - return await self._request_json_response(route, query=query) - - async def get_channel_message(self, channel_id: str, message_id: str) -> more_typing.JSONObject: - """Get the message with the given message ID from the channel with the given channel ID. - - Parameters - ---------- - channel_id : str - The ID of the channel to get the message from. - message_id : str - The ID of the message to retrieve. - - Returns - ------- - more_typing.JSONObject - A message object. - - !!! note - This requires the `READ_MESSAGE_HISTORY` permission. - - Raises - ------ - hikari.errors.Forbidden - If you lack permission to see the message. - hikari.errors.NotFound - If the channel or message is not found. - """ - route = routes.GET_CHANNEL_MESSAGE.compile(channel_id=channel_id, message_id=message_id) - return await self._request_json_response(route) - - async def create_message( - self, - channel_id: str, - *, - content: str = ..., - nonce: str = ..., - tts: bool = ..., - files: typing.Sequence[files_.BaseStream] = ..., - embed: more_typing.JSONObject = ..., - allowed_mentions: more_typing.JSONObject = ..., - ) -> more_typing.JSONObject: - """Create a message in the given channel or DM. - - Parameters - ---------- - channel_id : str - The ID of the channel to send to. - content : str - If specified, the message content to send with the message. - nonce : str - If specified, an optional ID to send for opportunistic message - creation. Any created message will have this nonce set on it. - Nonces are limited to 32 bytes in size. - tts : bool - If specified, whether the message will be sent as a TTS message. - files : typing.Sequence[hikari.models.files.BaseStream] - If specified, this should be a list of between `1` and `5` file - objects to upload. Each should have a unique name. - embed : more_typing.JSONObject - If specified, the embed to send with the message. - allowed_mentions : more_typing.JSONObject - If specified, the mentions to parse from the `content`. - If not specified, will parse all mentions from the `content`. - - Returns - ------- - more_typing.JSONObject - The created message object. - - Raises - ------ - hikari.errors.NotFound - If the channel is not found. - hikari.errors.BadRequest - This can be raised if the file is too large; if the embed exceeds - the defined limits; if the message content is specified only and - empty or greater than `2000` characters; if neither content, file - or embed are specified; if there is a duplicate id in only of the - fields in `allowed_mentions`; if you specify to parse all - users/roles mentions but also specify which users/roles to - parse only. - hikari.errors.Forbidden - If you lack permissions to send to this channel. - """ - form = aiohttp.FormData() - - json_payload = {} - conversions.put_if_specified(json_payload, "content", content) - conversions.put_if_specified(json_payload, "nonce", nonce) - conversions.put_if_specified(json_payload, "tts", tts) - conversions.put_if_specified(json_payload, "embed", embed) - conversions.put_if_specified(json_payload, "allowed_mentions", allowed_mentions) - - form.add_field("payload_json", json.dumps(json_payload), content_type=self._APPLICATION_JSON) - - if files is ...: - files = more_collections.EMPTY_SEQUENCE - - for i, file in enumerate(files): - form.add_field(f"file{i}", file, filename=file.filename, content_type=self._APPLICATION_OCTET_STREAM) - - route = routes.POST_CHANNEL_MESSAGES.compile(channel_id=channel_id) - - return await self._request_json_response(route, body=form) - - async def create_reaction(self, channel_id: str, message_id: str, emoji: str) -> None: - """Add a reaction to the given message in the given channel. - - Parameters - ---------- - channel_id : str - The ID of the channel to add this reaction in. - message_id : str - The ID of the message to add the reaction in. - emoji : str - The emoji to add. This can either be a series of unicode - characters making up a valid Discord emoji, or it can be a the url - representation of a custom emoji `<{emoji.name}:{emoji.id}>`. - - Raises - ------ - hikari.errors.Forbidden - If this is the first reaction using this specific emoji on this - message and you lack the `ADD_REACTIONS` permission. If you lack - `READ_MESSAGE_HISTORY`, this may also raise this error. - hikari.errors.NotFound - If the channel or message is not found, or if the emoji is not found. - hikari.errors.BadRequest - If the emoji is not valid, unknown, or formatted incorrectly. - """ - route = routes.PUT_MY_REACTION.compile(channel_id=channel_id, message_id=message_id, emoji=emoji) - await self._request_json_response(route) - - async def delete_own_reaction(self, channel_id: str, message_id: str, emoji: str) -> None: - """Remove your own reaction from the given message in the given channel. - - Parameters - ---------- - channel_id : str - The ID of the channel to get the message from. - message_id : str - The ID of the message to delete the reaction from. - emoji : str - The emoji to delete. This can either be a series of unicode - characters making up a valid Discord emoji, or it can be a - snowflake ID for a custom emoji. - - Raises - ------ - hikari.errors.Forbidden - If you lack permission to do this. - hikari.errors.NotFound - If the channel or message or emoji is not found. - """ - route = routes.DELETE_MY_REACTION.compile(channel_id=channel_id, message_id=message_id, emoji=emoji) - await self._request_json_response(route) - - async def delete_all_reactions_for_emoji(self, channel_id: str, message_id: str, emoji: str) -> None: - """Remove all reactions for a single given emoji on a given message in a given channel. - - Parameters - ---------- - channel_id : str - The ID of the channel to get the message from. - message_id : str - The ID of the message to delete the reactions from. - emoji : str - The emoji to delete. This can either be a series of unicode - characters making up a valid Discord emoji, or it can be a - snowflake ID for a custom emoji. - - Raises - ------ - hikari.errors.NotFound - If the channel or message or emoji or user is not found. - hikari.errors.Forbidden - If you lack the `MANAGE_MESSAGES` permission, or are in DMs. - """ - route = routes.DELETE_REACTION_EMOJI.compile(channel_id=channel_id, message_id=message_id, emoji=emoji) - await self._request_json_response(route) - - async def delete_user_reaction(self, channel_id: str, message_id: str, emoji: str, user_id: str) -> None: - """Remove a reaction made by a given user using a given emoji on a given message in a given channel. - - Parameters - ---------- - channel_id : str - The ID of the channel to get the message from. - message_id : str - The ID of the message to remove the reaction from. - emoji : str - The emoji to delete. This can either be a series of unicode - characters making up a valid Discord emoji, or it can be a - snowflake ID for a custom emoji. - user_id : str - The ID of the user who made the reaction that you wish to remove. - - Raises - ------ - hikari.errors.NotFound - If the channel or message or emoji or user is not found. - hikari.errors.Forbidden - If you lack the `MANAGE_MESSAGES` permission, or are in DMs. - """ - route = routes.DELETE_REACTION_USER.compile( - channel_id=channel_id, message_id=message_id, emoji=emoji, user_id=user_id, - ) - await self._request_json_response(route) - - async def get_reactions( - self, channel_id: str, message_id: str, emoji: str, *, after: str = ..., limit: int = ..., - ) -> typing.Sequence[more_typing.JSONObject]: - """Get a list of users who reacted with the given emoji on the given message. - - Parameters - ---------- - channel_id : str - The ID of the channel to get the message from. - message_id : str - The ID of the message to get the reactions from. - emoji : str - The emoji to get. This can either be a series of unicode - characters making up a valid Discord emoji, or it can be a - snowflake ID for a custom emoji. - after : str - If specified, the user ID. If specified, only users with a snowflake - that is lexicographically greater than the value will be returned. - limit : str - If specified, the limit of the number of values to return. Must be - between `1` and `100` inclusive. If unspecified, - defaults to `25`. - - Returns - ------- - typing.Sequence[more_typing.JSONObject] - A list of user objects. - - Raises - ------ - hikari.errors.Forbidden - If you lack access to the message. - hikari.errors.NotFound - If the channel or message is not found. - """ - query = {} - conversions.put_if_specified(query, "after", after) - conversions.put_if_specified(query, "limit", limit) - route = routes.GET_REACTIONS.compile(channel_id=channel_id, message_id=message_id, emoji=emoji) - return await self._request_json_response(route, query=query) - - async def delete_all_reactions(self, channel_id: str, message_id: str) -> None: - """Delete all reactions from a given message in a given channel. - - Parameters - ---------- - channel_id : str - The ID of the channel to get the message from. - message_id : str - The ID of the message to remove all reactions from. - - Raises - ------ - hikari.errors.NotFound - If the channel or message is not found. - hikari.errors.Forbidden - If you lack the `MANAGE_MESSAGES` permission. - """ - route = routes.DELETE_ALL_REACTIONS.compile(channel_id=channel_id, message_id=message_id) - await self._request_json_response(route) - - async def edit_message( - self, - channel_id: str, - message_id: str, - *, - content: typing.Optional[str] = ..., - embed: typing.Optional[more_typing.JSONObject] = ..., - flags: int = ..., - allowed_mentions: more_typing.JSONObject = ..., - ) -> more_typing.JSONObject: - """Update the given message. - - Parameters - ---------- - channel_id : str - The ID of the channel to get the message from. - message_id : str - The ID of the message to edit. - content : str | None - If specified, the string content to replace with in the message. - If `None`, the content will be removed from the message. - embed : more_typing.JSONObject | None - If specified, the embed to replace with in the message. - If `None`, the embed will be removed from the message. - flags : int - If specified, the integer to replace the message's current flags. - allowed_mentions : more_typing.JSONObject - If specified, the mentions to parse from the `content`. - If not specified, will parse all mentions from the `content`. - - Returns - ------- - more_typing.JSONObject - The edited message object. - - Raises - ------ - hikari.errors.NotFound - If the channel or message is not found. - hikari.errors.BadRequest - This can be raised if the embed exceeds the defined limits; - if the message content is specified only and empty or greater - than `2000` characters; if neither content, file or embed - are specified. - parse only. - hikari.errors.Forbidden - If you try to edit `content` or `embed` or `allowed_mentions` - on a message you did not author or try to edit the flags on a - message you did not author without the `MANAGE_MESSAGES` - permission. - """ - payload = {} - conversions.put_if_specified(payload, "content", content) - conversions.put_if_specified(payload, "embed", embed) - conversions.put_if_specified(payload, "flags", flags) - conversions.put_if_specified(payload, "allowed_mentions", allowed_mentions) - route = routes.PATCH_CHANNEL_MESSAGE.compile(channel_id=channel_id, message_id=message_id) - return await self._request_json_response(route, body=payload) - - async def delete_message(self, channel_id: str, message_id: str) -> None: - """Delete a message in a given channel. - - Parameters - ---------- - channel_id : str - The ID of the channel to get the message from. - message_id : str - The ID of the message to delete. - - Raises - ------ - hikari.errors.Forbidden - If you did not author the message and are in a DM, or if you did not author the message and lack the - `MANAGE_MESSAGES` permission in a guild channel. - hikari.errors.NotFound - If the channel or message is not found. - """ - route = routes.DELETE_CHANNEL_MESSAGE.compile(channel_id=channel_id, message_id=message_id) - await self._request_json_response(route) - - async def bulk_delete_messages(self, channel_id: str, messages: typing.Sequence[str]) -> None: - """Delete multiple messages in a given channel. - - Parameters - ---------- - channel_id : str - The ID of the channel to get the message from. - messages : typing.Sequence[str] - A list of `2-100` message IDs to remove in the channel. - - Raises - ------ - hikari.errors.NotFound - If the channel is not found. - hikari.errors.Forbidden - If you lack the `MANAGE_MESSAGES` permission in the channel. - hikari.errors.BadRequest - If any of the messages passed are older than `2` weeks in age or - any duplicate message IDs are passed. - - !!! note - This can only be used on guild text channels. Any message IDs that - do not exist or are invalid still add towards the total `100` max - messages to remove. This can only delete messages that are newer - than `2` weeks in age. If any of the messages are older than - `2` weeks then this call will fail. - """ - payload = {"messages": messages} - route = routes.POST_DELETE_CHANNEL_MESSAGES_BULK.compile(channel_id=channel_id) - await self._request_json_response(route, body=payload) - - async def edit_channel_permissions( - self, channel_id: str, overwrite_id: str, type_: str, *, allow: int = ..., deny: int = ..., reason: str = ..., - ) -> None: - """Edit permissions for a given channel. - - Parameters - ---------- - channel_id : str - The ID of the channel to edit permissions for. - overwrite_id : str - The overwrite ID to edit. - type_ : str - The type of overwrite. `"member"` if it is for a member, - or `"role"` if it is for a role. - allow : int - If specified, the bitwise value of all permissions to set to be allowed. - deny : int - If specified, the bitwise value of all permissions to set to be denied. - reason : str - If specified, the audit log reason explaining why the operation - was performed. - - Raises - ------ - hikari.errors.NotFound - If the target channel or overwrite doesn't exist. - hikari.errors.Forbidden - If you lack permission to do this. - """ - payload = {"type": type_} - conversions.put_if_specified(payload, "allow", allow) - conversions.put_if_specified(payload, "deny", deny) - route = routes.PATCH_CHANNEL_PERMISSIONS.compile(channel_id=channel_id, overwrite_id=overwrite_id) - await self._request_json_response(route, body=payload, reason=reason) - - async def get_channel_invites(self, channel_id: str) -> typing.Sequence[more_typing.JSONObject]: - """Get invites for a given channel. - - Parameters - ---------- - channel_id : str - The ID of the channel to get invites for. - - Returns - ------- - typing.Sequence[more_typing.JSONObject] - A list of invite objects. - - Raises - ------ - hikari.errors.Forbidden - If you lack the `MANAGE_CHANNELS` permission. - hikari.errors.NotFound - If the channel does not exist. - """ - route = routes.GET_CHANNEL_INVITES.compile(channel_id=channel_id) - return await self._request_json_response(route) - - async def create_channel_invite( - self, - channel_id: str, - *, - max_age: int = ..., - max_uses: int = ..., - temporary: bool = ..., - unique: bool = ..., - target_user: str = ..., - target_user_type: int = ..., - reason: str = ..., - ) -> more_typing.JSONObject: - """Create a new invite for the given channel. - - Parameters - ---------- - channel_id : str - The ID of the channel to create the invite for. - max_age : int - If specified, the max age of the invite in seconds, defaults to - `86400` (`24` hours). - Set to `0` to never expire. - max_uses : int - If specified, the max number of uses this invite can have, or `0` - for unlimited (as per the default). - temporary : bool - If specified, whether to grant temporary membership, meaning the - user is kicked when their session ends unless they are given a role. - unique : bool - If specified, whether to try to reuse a similar invite. - target_user : str - If specified, the ID of the user this invite should target. - target_user_type : int - If specified, the type of target for this invite. - reason : str - If specified, the audit log reason explaining why the operation - was performed. - - Returns - ------- - more_typing.JSONObject - An invite object. - - Raises - ------ - hikari.errors.Forbidden - If you lack the `CREATE_INSTANT_MESSAGES` permission. - hikari.errors.NotFound - If the channel does not exist. - hikari.errors.BadRequest - If the arguments provided are not valid (e.g. negative age, etc). - """ - payload = {} - conversions.put_if_specified(payload, "max_age", max_age) - conversions.put_if_specified(payload, "max_uses", max_uses) - conversions.put_if_specified(payload, "temporary", temporary) - conversions.put_if_specified(payload, "unique", unique) - conversions.put_if_specified(payload, "target_user", target_user) - conversions.put_if_specified(payload, "target_user_type", target_user_type) - route = routes.POST_CHANNEL_INVITES.compile(channel_id=channel_id) - return await self._request_json_response(route, body=payload, reason=reason) - - async def delete_channel_permission(self, channel_id: str, overwrite_id: str) -> None: - """Delete a channel permission overwrite for a user or a role in a channel. - - Parameters - ---------- - channel_id : str - The ID of the channel to delete the overwrite from. - overwrite_id : str - The ID of the overwrite to remove. - - Raises - ------ - hikari.errors.NotFound - If the overwrite or channel do not exist. - hikari.errors.Forbidden - If you lack the `MANAGE_ROLES` permission for that channel. - """ - route = routes.DELETE_CHANNEL_PERMISSIONS.compile(channel_id=channel_id, overwrite_id=overwrite_id) - await self._request_json_response(route) - - async def trigger_typing_indicator(self, channel_id: str) -> None: - """Trigger the account to appear to be typing for the next `10` seconds in the given channel. - - Parameters - ---------- - channel_id : str - The ID of the channel to appear to be typing in. - - Raises - ------ - hikari.errors.NotFound - If the channel is not found. - hikari.errors.Forbidden - If you are not able to type in the channel. - """ - route = routes.POST_CHANNEL_TYPING.compile(channel_id=channel_id) - await self._request_json_response(route) - - async def get_pinned_messages(self, channel_id: str) -> typing.Sequence[more_typing.JSONObject]: - """Get pinned messages for a given channel. - - Parameters - ---------- - channel_id : str - The channel ID to get messages from. - - Returns - ------- - typing.Sequence[more_typing.JSONObject] - A list of messages. - - Raises - ------ - hikari.errors.NotFound - If the channel is not found. - hikari.errors.Forbidden - If you are not able to see the channel. - - !!! note - If you are not able to see the pinned message (eg. you are missing - `READ_MESSAGE_HISTORY` and the pinned message is an old message), it - will not be returned. - """ - route = routes.GET_CHANNEL_PINS.compile(channel_id=channel_id) - return await self._request_json_response(route) - - async def add_pinned_channel_message(self, channel_id: str, message_id: str) -> None: - """Add a pinned message to the channel. - - Parameters - ---------- - channel_id : str - The ID of the channel to pin a message to. - message_id : str - The ID of the message to pin. - - Raises - ------ - hikari.errors.Forbidden - If you lack the `MANAGE_MESSAGES` permission. - hikari.errors.NotFound - If the message or channel do not exist. - """ - route = routes.PUT_CHANNEL_PINS.compile(channel_id=channel_id, message_id=message_id) - await self._request_json_response(route) - - async def delete_pinned_channel_message(self, channel_id: str, message_id: str) -> None: - """Remove a pinned message from the channel. - - This will only unpin the message, not delete it. - - Parameters - ---------- - channel_id : str - The ID of the channel to remove a pin from. - message_id : str - The ID of the message to unpin. - - Raises - ------ - hikari.errors.Forbidden - If you lack the `MANAGE_MESSAGES` permission. - hikari.errors.NotFound - If the message or channel do not exist. - """ - route = routes.DELETE_CHANNEL_PIN.compile(channel_id=channel_id, message_id=message_id) - await self._request_json_response(route) - - async def list_guild_emojis(self, guild_id: str) -> typing.Sequence[more_typing.JSONObject]: - """Get a list of the emojis for a given guild ID. - - Parameters - ---------- - guild_id : str - The ID of the guild to get the emojis for. - - Returns - ------- - typing.Sequence[more_typing.JSONObject] - A list of emoji objects. - - Raises - ------ - hikari.errors.NotFound - If the guild is not found. - hikari.errors.Forbidden - If you aren't a member of the guild. - """ - route = routes.GET_GUILD_EMOJIS.compile(guild_id=guild_id) - return await self._request_json_response(route) - - async def get_guild_emoji(self, guild_id: str, emoji_id: str) -> more_typing.JSONObject: - """Get an emoji from a given guild and emoji IDs. - - Parameters - ---------- - guild_id : str - The ID of the guild to get the emoji from. - emoji_id : str - The ID of the emoji to get. - - Returns - ------- - more_typing.JSONObject - An emoji object. - - Raises - ------ - hikari.errors.NotFound - If either the guild or the emoji aren't found. - hikari.errors.Forbidden - If you aren't a member of said guild. - """ - route = routes.GET_GUILD_EMOJI.compile(guild_id=guild_id, emoji_id=emoji_id) - return await self._request_json_response(route) - - async def create_guild_emoji( - self, guild_id: str, name: str, image: bytes, *, roles: typing.Sequence[str] = ..., reason: str = ..., - ) -> more_typing.JSONObject: - """Create a new emoji for a given guild. - - Parameters - ---------- - guild_id : str - The ID of the guild to create the emoji in. - name : str - The new emoji's name. - image : bytes - The `128x128` image in bytes form. - roles : typing.Sequence[str] - If specified, a list of roles for which the emoji will be whitelisted. - If empty, all roles are whitelisted. - reason : str - If specified, the audit log reason explaining why the operation - was performed. - - Returns - ------- - more_typing.JSONObject - The newly created emoji object. - - Raises - ------ - ValueError - If `image` is `None`. - hikari.errors.NotFound - If the guild is not found. - hikari.errors.Forbidden - If you either lack the `MANAGE_EMOJIS` permission or aren't a member of said guild. - hikari.errors.BadRequest - If you attempt to upload an image larger than `256kb`, an empty image or an invalid image format. - """ - payload = { - "name": name, - "roles": [] if roles is ... else roles, - "image": conversions.image_bytes_to_image_data(image), - } - route = routes.POST_GUILD_EMOJIS.compile(guild_id=guild_id) - return await self._request_json_response(route, body=payload, reason=reason) - - async def modify_guild_emoji( - self, guild_id: str, emoji_id: str, *, name: str = ..., roles: typing.Sequence[str] = ..., reason: str = ..., - ) -> more_typing.JSONObject: - """Edit an emoji of a given guild. - - Parameters - ---------- - guild_id : str - The ID of the guild to which the emoji to update belongs to. - emoji_id : str - The ID of the emoji to update. - name : str - If specified, a new emoji name string. Keep unspecified to keep the name the same. - roles : typing.Sequence[str] - If specified, a list of IDs for the new whitelisted roles. - Set to an empty list to whitelist all roles. - Keep unspecified to leave the same roles already set. - reason : str - If specified, the audit log reason explaining why the operation - was performed. - - Returns - ------- - more_typing.JSONObject - The updated emoji object. - - Raises - ------ - hikari.errors.NotFound - If either the guild or the emoji aren't found. - hikari.errors.Forbidden - If you either lack the `MANAGE_EMOJIS` permission or are not a member of the given guild. - """ - payload = {} - conversions.put_if_specified(payload, "name", name) - conversions.put_if_specified(payload, "roles", roles) - route = routes.PATCH_GUILD_EMOJI.compile(guild_id=guild_id, emoji_id=emoji_id) - return await self._request_json_response(route, body=payload, reason=reason) - - async def delete_guild_emoji(self, guild_id: str, emoji_id: str) -> None: - """Delete an emoji from a given guild. - - Parameters - ---------- - guild_id : str - The ID of the guild to delete the emoji from. - emoji_id : str - The ID of the emoji to be deleted. - - Raises - ------ - hikari.errors.NotFound - If either the guild or the emoji aren't found. - hikari.errors.Forbidden - If you either lack the `MANAGE_EMOJIS` permission or aren't a member of said guild. - """ - route = routes.DELETE_GUILD_EMOJI.compile(guild_id=guild_id, emoji_id=emoji_id) - await self._request_json_response(route) - - async def create_guild( - self, - name: str, - *, - region: str = ..., - icon: bytes = ..., - verification_level: int = ..., - default_message_notifications: int = ..., - explicit_content_filter: int = ..., - roles: typing.Sequence[more_typing.JSONObject] = ..., - channels: typing.Sequence[more_typing.JSONObject] = ..., - ) -> more_typing.JSONObject: - """Create a new guild. - - !!! warning - Can only be used by bots in less than `10` guilds. - - Parameters - ---------- - name : str - The name string for the new guild (`2-100` characters). - region : str - If specified, the voice region ID for new guild. You can use - `RESTSession.list_voice_regions` to see which region IDs are available. - icon : bytes - If specified, the guild icon image in bytes form. - verification_level : int - If specified, the verification level integer (`0-5`). - default_message_notifications : int - If specified, the default notification level integer (`0-1`). - explicit_content_filter : int - If specified, the explicit content filter integer (`0-2`). - roles : typing.Sequence[more_typing.JSONObject] - If specified, an array of role objects to be created alongside the - guild. First element changes the `@everyone` role. - channels : typing.Sequence[more_typing.JSONObject] - If specified, an array of channel objects to be created alongside the guild. - - Returns - ------- - more_typing.JSONObject - The newly created guild object. - - Raises - ------ - hikari.errors.Forbidden - If you are on `10` or more guilds. - hikari.errors.BadRequest - If you provide unsupported fields like `parent_id` in channel objects. - """ - payload = {"name": name} - conversions.put_if_specified(payload, "region", region) - conversions.put_if_specified(payload, "verification", verification_level) - conversions.put_if_specified(payload, "notifications", default_message_notifications) - conversions.put_if_specified(payload, "explicit_content_filter", explicit_content_filter) - conversions.put_if_specified(payload, "roles", roles) - conversions.put_if_specified(payload, "channels", channels) - conversions.put_if_specified(payload, "icon", icon, conversions.image_bytes_to_image_data) - route = routes.POST_GUILDS.compile() - return await self._request_json_response(route, body=payload) - - async def get_guild(self, guild_id: str, *, with_counts: bool = True) -> more_typing.JSONObject: - """Get the information for the guild with the given ID. - - Parameters - ---------- - guild_id : str - The ID of the guild to get. - with_counts: bool - `True` if you wish to receive approximate member and presence counts - in the response, or `False` otherwise. Will default to `True`. - - Returns - ------- - more_typing.JSONObject - The requested guild object. - - Raises - ------ - hikari.errors.NotFound - If the guild is not found. - hikari.errors.Forbidden - If you do not have access to the guild. - """ - route = routes.GET_GUILD.compile(guild_id=guild_id) - return await self._request_json_response( - route, query={"with_counts": "true" if with_counts is True else "false"} - ) - - async def get_guild_preview(self, guild_id: str) -> more_typing.JSONObject: - """Get a public guild's preview object. - - Parameters - ---------- - guild_id : str - The ID of the guild to get the preview object of. - - Returns - ------- - more_typing.JSONObject - The requested guild preview object. - - !!! note - Unlike other guild endpoints, the bot doesn't have to be in the - target guild to get it's preview. - - Raises - ------ - hikari.errors.NotFound - If the guild is not found or it isn't `PUBLIC`. - """ - route = routes.GET_GUILD_PREVIEW.compile(guild_id=guild_id) - return await self._request_json_response(route) - - # pylint: disable=too-many-locals - async def modify_guild( # lgtm [py/similar-function] - self, - guild_id: str, - *, - name: str = ..., - region: str = ..., - verification_level: int = ..., - default_message_notifications: int = ..., - explicit_content_filter: int = ..., - afk_channel_id: str = ..., - afk_timeout: int = ..., - icon: bytes = ..., - owner_id: str = ..., - splash: bytes = ..., - system_channel_id: str = ..., - reason: str = ..., - ) -> more_typing.JSONObject: - """Edit a given guild. - - Parameters - ---------- - guild_id : str - The ID of the guild to be edited. - name : str - If specified, the new name string for the guild (`2-100` characters). - region : str - If specified, the new voice region ID for guild. You can use - `RESTSession.list_voice_regions` to see which region IDs are available. - verification_level : int - If specified, the new verification level integer (`0-5`). - default_message_notifications : int - If specified, the new default notification level integer (`0-1`). - explicit_content_filter : int - If specified, the new explicit content filter integer (`0-2`). - afk_channel_id : str - If specified, the new ID for the AFK voice channel. - afk_timeout : int - If specified, the new AFK _request_timeout period in seconds - icon : bytes - If specified, the new guild icon image in bytes form. - owner_id : str - If specified, the new ID of the new guild owner. - splash : bytes - If specified, the new new splash image in bytes form. - system_channel_id : str - If specified, the new ID of the new system channel. - reason : str - If specified, the audit log reason explaining why the operation - was performed. - - Returns - ------- - more_typing.JSONObject - The edited guild object. - - Raises - ------ - hikari.errors.NotFound - If the guild is not found. - hikari.errors.Forbidden - If you lack the `MANAGE_GUILD` permission or are not in the guild. - """ - payload = {} - conversions.put_if_specified(payload, "name", name) - conversions.put_if_specified(payload, "region", region) - conversions.put_if_specified(payload, "verification", verification_level) - conversions.put_if_specified(payload, "notifications", default_message_notifications) - conversions.put_if_specified(payload, "explicit_content_filter", explicit_content_filter) - conversions.put_if_specified(payload, "afk_channel_id", afk_channel_id) - conversions.put_if_specified(payload, "afk_timeout", afk_timeout) - conversions.put_if_specified(payload, "icon", icon, conversions.image_bytes_to_image_data) - conversions.put_if_specified(payload, "owner_id", owner_id) - conversions.put_if_specified(payload, "splash", splash, conversions.image_bytes_to_image_data) - conversions.put_if_specified(payload, "system_channel_id", system_channel_id) - route = routes.PATCH_GUILD.compile(guild_id=guild_id) - return await self._request_json_response(route, body=payload, reason=reason) - - # pylint: enable=too-many-locals - - async def delete_guild(self, guild_id: str) -> None: - """Permanently delete the given guild. - - You must be owner of the guild to perform this action. - - Parameters - ---------- - guild_id : str - The ID of the guild to be deleted. - - Raises - ------ - hikari.errors.NotFound - If the guild is not found. - hikari.errors.Forbidden - If you are not the guild owner. - """ - route = routes.DELETE_GUILD.compile(guild_id=guild_id) - await self._request_json_response(route) - - async def list_guild_channels(self, guild_id: str) -> typing.Sequence[more_typing.JSONObject]: - """Get all the channels for a given guild. - - Parameters - ---------- - guild_id : str - The ID of the guild to get the channels from. - - Returns - ------- - typing.Sequence[more_typing.JSONObject] - A list of channel objects. - - Raises - ------ - hikari.errors.NotFound - If the guild is not found. - hikari.errors.Forbidden - If you are not in the guild. - """ - route = routes.GET_GUILD_CHANNELS.compile(guild_id=guild_id) - return await self._request_json_response(route) - - async def create_guild_channel( - self, - guild_id: str, - name: str, - *, - type_: int = ..., - position: int = ..., - topic: str = ..., - nsfw: bool = ..., - rate_limit_per_user: int = ..., - bitrate: int = ..., - user_limit: int = ..., - permission_overwrites: typing.Sequence[more_typing.JSONObject] = ..., - parent_id: str = ..., - reason: str = ..., - ) -> more_typing.JSONObject: - """Create a channel in a given guild. - - Parameters - ---------- - guild_id : str - The ID of the guild to create the channel in. - name : str - If specified, the name for the channel.This must be - between `2` and `100` characters in length. - type_: int - If specified, the channel type integer (`0-6`). - position : int - If specified, the position to change the channel to. - topic : str - If specified, the topic to set. This is only applicable to - text channels. This must be between `0` and `1024` - characters in length. - nsfw : bool - If specified, whether the channel will be marked as NSFW. - Only applicable to text channels. - rate_limit_per_user : int - If specified, the number of seconds the user has to wait before sending - another message. This will not apply to bots, or to members with - `MANAGE_MESSAGES` or `MANAGE_CHANNEL` permissions. This must - be between `0` and `21600` seconds. - bitrate : int - If specified, the bitrate in bits per second allowable for the channel. - This only applies to voice channels and must be between `8000` - and `96000` for normal servers or `8000` and `128000` for - VIP servers. - user_limit : int - If specified, the max number of users to allow in a voice channel. - This must be between `0` and `99` inclusive, where - `0` implies no limit. - permission_overwrites : typing.Sequence[more_typing.JSONObject] - If specified, the list of permission overwrites that are category - specific to replace the existing overwrites with. - parent_id : str - If specified, the parent category ID to set for the channel. - reason : str - If specified, the audit log reason explaining why the operation - was performed. - - Returns - ------- - more_typing.JSONObject - The newly created channel object. - - Raises - ------ - hikari.errors.NotFound - If the guild is not found. - hikari.errors.Forbidden - If you lack the `MANAGE_CHANNEL` permission or are not in the guild. - hikari.errors.BadRequest - If you provide incorrect options for the corresponding channel type - (e.g. a `bitrate` for a text channel). - """ - payload = {} - conversions.put_if_specified(payload, "name", name) - conversions.put_if_specified(payload, "type", type_) - conversions.put_if_specified(payload, "position", position) - conversions.put_if_specified(payload, "topic", topic) - conversions.put_if_specified(payload, "nsfw", nsfw) - conversions.put_if_specified(payload, "rate_limit_per_user", rate_limit_per_user) - conversions.put_if_specified(payload, "bitrate", bitrate) - conversions.put_if_specified(payload, "user_limit", user_limit) - conversions.put_if_specified(payload, "permission_overwrites", permission_overwrites) - conversions.put_if_specified(payload, "parent_id", parent_id) - route = routes.POST_GUILD_CHANNELS.compile(guild_id=guild_id) - return await self._request_json_response(route, body=payload, reason=reason) - - async def modify_guild_channel_positions( - self, guild_id: str, channel: typing.Tuple[str, int], *channels: typing.Tuple[str, int] - ) -> None: - """Edit the position of one or more given channels. - - Parameters - ---------- - guild_id : str - The ID of the guild in which to edit the channels. - channel : typing.Tuple[str, int] - The first channel to change the position of. This is a tuple of the channel ID and the integer position. - *channels : typing.Tuple[str, int] - Optional additional channels to change the position of. These must be tuples of the channel ID and the - integer positions to change to. - - Raises - ------ - hikari.errors.NotFound - If either the guild or any of the channels aren't found. - hikari.errors.Forbidden - If you either lack the `MANAGE_CHANNELS` permission or are not a member of said guild or are not in - the guild. - hikari.errors.BadRequest - If you provide anything other than the `id` and `position` fields for the channels. - """ - payload = [{"id": ch[0], "position": ch[1]} for ch in (channel, *channels)] - route = routes.PATCH_GUILD_CHANNELS.compile(guild_id=guild_id) - await self._request_json_response(route, body=payload) - - async def get_guild_member(self, guild_id: str, user_id: str) -> more_typing.JSONObject: - """Get a given guild member. - - Parameters - ---------- - guild_id : str - The ID of the guild to get the member from. - user_id : str - The ID of the member to get. - - Returns - ------- - more_typing.JSONObject - The requested member object. - - Raises - ------ - hikari.errors.NotFound - If either the guild or the member aren't found. - hikari.errors.Forbidden - If you don't have access to the target guild. - """ - route = routes.GET_GUILD_MEMBER.compile(guild_id=guild_id, user_id=user_id) - return await self._request_json_response(route) - - async def list_guild_members( - self, guild_id: str, *, limit: int = ..., after: str = ..., - ) -> typing.Sequence[more_typing.JSONObject]: - """List all members of a given guild. - - Parameters - ---------- - guild_id : str - The ID of the guild to get the members from. - limit : int - If specified, the maximum number of members to return. This has to be between - `1` and `1000` inclusive. - after : str - If specified, the highest ID in the previous page. This is used for retrieving more - than `1000` members in a server using consecutive requests. - - Examples - -------- - members = [] - last_id = 0 - - while True: - next_members = await client.list_guild_members(1234567890, limit=1000, after=last_id) - members += next_members - - if len(next_members) == 1000: - last_id = next_members[-1]["user"]["id"] - else: - break - - Returns - ------- - more_typing.JSONObject - A list of member objects. - - Raises - ------ - hikari.errors.NotFound - If the guild is not found. - hikari.errors.Forbidden - If you are not in the guild. - hikari.errors.BadRequest - If you provide invalid values for the `limit` or `after` fields. - """ - query = {} - conversions.put_if_specified(query, "limit", limit) - conversions.put_if_specified(query, "after", after) - route = routes.GET_GUILD_MEMBERS.compile(guild_id=guild_id) - return await self._request_json_response(route, query=query) - - async def modify_guild_member( # lgtm [py/similar-function] - self, - guild_id: str, - user_id: str, - *, - nick: typing.Optional[str] = ..., - roles: typing.Sequence[str] = ..., - mute: bool = ..., - deaf: bool = ..., - channel_id: typing.Optional[str] = ..., - reason: str = ..., - ) -> None: - """Edit a member of a given guild. - - Parameters - ---------- - guild_id : str - The ID of the guild to edit the member from. - user_id : str - The ID of the member to edit. - nick : str | None - If specified, the new nickname string. Setting it to None - explicitly will clear the nickname. - roles : typing.Sequence[str] - If specified, a list of role IDs the member should have. - mute : bool - If specified, whether the user should be muted in the voice channel - or not. - deaf : bool - If specified, whether the user should be deafen in the voice channel - or not. - channel_id : str - If specified, the ID of the channel to move the member to. Setting - it to None explicitly will disconnect the user. - reason : str - If specified, the audit log reason explaining why the operation - was performed. - - Raises - ------ - hikari.errors.NotFound - If either the guild, user, channel or any of the roles aren't found. - hikari.errors.Forbidden - If you lack any of the applicable permissions (`MANAGE_NICKNAMES`, - `MANAGE_ROLES`, `MUTE_MEMBERS`,`DEAFEN_MEMBERS` or `MOVE_MEMBERS`). - Note that to move a member you must also have permission to connect - to the end channel. This will also be raised if you're not in the - guild. - hikari.errors.BadRequest - If you pass `mute`, `deaf` or `channel` while the member is not connected to a voice channel. - """ - payload = {} - conversions.put_if_specified(payload, "nick", nick) - conversions.put_if_specified(payload, "roles", roles) - conversions.put_if_specified(payload, "mute", mute) - conversions.put_if_specified(payload, "deaf", deaf) - conversions.put_if_specified(payload, "channel", channel_id) - route = routes.PATCH_GUILD_MEMBER.compile(guild_id=guild_id, user_id=user_id) - await self._request_json_response(route, body=payload, reason=reason) - - async def modify_current_user_nick(self, guild_id: str, nick: typing.Optional[str], *, reason: str = ...) -> None: - """Edit the current user's nickname for a given guild. - - Parameters - ---------- - guild_id : str - The ID of the guild you want to change the nick on. - nick : str | None - The new nick string. Setting this to `None` clears the nickname. - reason : str - If specified, the audit log reason explaining why the operation - was performed. - - Raises - ------ - hikari.errors.NotFound - If the guild is not found. - hikari.errors.Forbidden - If you lack the `CHANGE_NICKNAME` permission or are not in the guild. - hikari.errors.BadRequest - If you provide a disallowed nickname, one that is too long, or one that is empty. - """ - payload = {"nick": nick} - route = routes.PATCH_MY_GUILD_NICKNAME.compile(guild_id=guild_id) - await self._request_json_response(route, body=payload, reason=reason) - - async def add_guild_member_role(self, guild_id: str, user_id: str, role_id: str, *, reason: str = ...) -> None: - """Add a role to a given member. - - Parameters - ---------- - guild_id : str - The ID of the guild the member belongs to. - user_id : str - The ID of the member you want to add the role to. - role_id : str - The ID of the role you want to add. - reason : str - If specified, the audit log reason explaining why the operation - was performed. - - Raises - ------ - hikari.errors.NotFound - If either the guild, member or role aren't found. - hikari.errors.Forbidden - If you lack the `MANAGE_ROLES` permission or are not in the guild. - """ - route = routes.PUT_GUILD_MEMBER_ROLE.compile(guild_id=guild_id, user_id=user_id, role_id=role_id) - await self._request_json_response(route, reason=reason) - - async def remove_guild_member_role(self, guild_id: str, user_id: str, role_id: str, *, reason: str = ...) -> None: - """Remove a role from a given member. - - Parameters - ---------- - guild_id : str - The ID of the guild the member belongs to. - user_id : str - The ID of the member you want to remove the role from. - role_id : str - The ID of the role you want to remove. - reason : str - If specified, the audit log reason explaining why the operation - was performed. - - Raises - ------ - hikari.errors.NotFound - If either the guild, member or role aren't found. - hikari.errors.Forbidden - If you lack the `MANAGE_ROLES` permission or are not in the guild. - """ - route = routes.DELETE_GUILD_MEMBER_ROLE.compile(guild_id=guild_id, user_id=user_id, role_id=role_id) - await self._request_json_response(route, reason=reason) - - async def remove_guild_member(self, guild_id: str, user_id: str, *, reason: str = ...) -> None: - """Kick a user from a given guild. - - Parameters - ---------- - guild_id: str - The ID of the guild the member belongs to. - user_id: str - The ID of the member you want to kick. - reason : str - If specified, the audit log reason explaining why the operation - was performed. - - Raises - ------ - hikari.errors.NotFound - If either the guild or member aren't found. - hikari.errors.Forbidden - If you lack the `KICK_MEMBERS` permission or are not in the guild. - """ - route = routes.DELETE_GUILD_MEMBER.compile(guild_id=guild_id, user_id=user_id) - await self._request_json_response(route, reason=reason) - - async def get_guild_bans(self, guild_id: str) -> typing.Sequence[more_typing.JSONObject]: - """Get the bans for a given guild. - - Parameters - ---------- - guild_id : str - The ID of the guild you want to get the bans from. - - Returns - ------- - typing.Sequence[more_typing.JSONObject] - A list of ban objects. - - Raises - ------ - hikari.errors.NotFound - If the guild is not found. - hikari.errors.Forbidden - If you lack the `BAN_MEMBERS` permission or are not in the guild. - """ - route = routes.GET_GUILD_BANS.compile(guild_id=guild_id) - return await self._request_json_response(route) - - async def get_guild_ban(self, guild_id: str, user_id: str) -> more_typing.JSONObject: - """Get a ban from a given guild. - - Parameters - ---------- - guild_id : str - The ID of the guild you want to get the ban from. - user_id : str - The ID of the user to get the ban information for. - - Returns - ------- - more_typing.JSONObject - A ban object for the requested user. - - Raises - ------ - hikari.errors.NotFound - If either the guild or the user aren't found, or if the user is not banned. - hikari.errors.Forbidden - If you lack the `BAN_MEMBERS` permission or are not in the guild. - """ - route = routes.GET_GUILD_BAN.compile(guild_id=guild_id, user_id=user_id) - return await self._request_json_response(route) - - async def create_guild_ban( - self, guild_id: str, user_id: str, *, delete_message_days: int = ..., reason: str = ..., - ) -> None: - """Ban a user from a given guild. - - Parameters - ---------- - guild_id : str - The ID of the guild the member belongs to. - user_id : str - The ID of the member you want to ban. - delete_message_days : str - If specified, how many days of messages from the user should - be removed. - reason : str - If specified, the audit log reason explaining why the operation - was performed. - - Raises - ------ - hikari.errors.NotFound - If either the guild or member aren't found. - hikari.errors.Forbidden - If you lack the `BAN_MEMBERS` permission or are not in the guild. - """ - query = {} - conversions.put_if_specified(query, "delete-message-days", delete_message_days) - conversions.put_if_specified(query, "reason", reason) - route = routes.PUT_GUILD_BAN.compile(guild_id=guild_id, user_id=user_id) - await self._request_json_response(route, query=query) - - async def remove_guild_ban(self, guild_id: str, user_id: str, *, reason: str = ...) -> None: - """Un-bans a user from a given guild. - - Parameters - ---------- - guild_id : str - The ID of the guild to un-ban the user from. - user_id : str - The ID of the user you want to un-ban. - reason : str - If specified, the audit log reason explaining why the operation - was performed. - - Raises - ------ - hikari.errors.NotFound - If either the guild or member aren't found, or the member is not banned. - hikari.errors.Forbidden - If you lack the `BAN_MEMBERS` permission or are not a in the guild. - """ - route = routes.DELETE_GUILD_BAN.compile(guild_id=guild_id, user_id=user_id) - await self._request_json_response(route, reason=reason) - - async def get_guild_roles(self, guild_id: str) -> typing.Sequence[more_typing.JSONObject]: - """Get the roles for a given guild. - - Parameters - ---------- - guild_id : str - The ID of the guild you want to get the roles from. - - Returns - ------- - typing.Sequence[more_typing.JSONObject] - A list of role objects. - - Raises - ------ - hikari.errors.NotFound - If the guild is not found. - hikari.errors.Forbidden - If you're not in the guild. - """ - route = routes.GET_GUILD_ROLES.compile(guild_id=guild_id) - return await self._request_json_response(route) - - async def create_guild_role( - self, - guild_id: str, - *, - name: str = ..., - permissions: int = ..., - color: int = ..., - hoist: bool = ..., - mentionable: bool = ..., - reason: str = ..., - ) -> more_typing.JSONObject: - """Create a new role for a given guild. - - Parameters - ---------- - guild_id : str - The ID of the guild you want to create the role on. - name : str - If specified, the new role name string. - permissions : int - If specified, the permissions integer for the role. - color : int - If specified, the color for the role. - hoist : bool - If specified, whether the role will be hoisted. - mentionable : bool - If specified, whether the role will be able to be mentioned by any user. - reason : str - If specified, the audit log reason explaining why the operation - was performed. - - Returns - ------- - more_typing.JSONObject - The newly created role object. - - Raises - ------ - hikari.errors.NotFound - If the guild is not found. - hikari.errors.Forbidden - If you lack the `MANAGE_ROLES` permission or you're not in the guild. - hikari.errors.BadRequest - If you provide invalid values for the role attributes. - """ - payload = {} - conversions.put_if_specified(payload, "name", name) - conversions.put_if_specified(payload, "permissions", permissions) - conversions.put_if_specified(payload, "color", color) - conversions.put_if_specified(payload, "hoist", hoist) - conversions.put_if_specified(payload, "mentionable", mentionable) - route = routes.POST_GUILD_ROLES.compile(guild_id=guild_id) - return await self._request_json_response(route, body=payload, reason=reason) - - async def modify_guild_role_positions( - self, guild_id: str, role: typing.Tuple[str, int], *roles: typing.Tuple[str, int] - ) -> typing.Sequence[more_typing.JSONObject]: - """Edit the position of two or more roles in a given guild. - - Parameters - ---------- - guild_id : str - The ID of the guild the roles belong to. - role : typing.Tuple[str, int] - The first role to move. This is a tuple of the role ID and the - integer position. - *roles : typing.Tuple[str, int] - Optional extra roles to move. These must be tuples of the role ID - and the integer position. - - Returns - ------- - typing.Sequence[more_typing.JSONObject] - A list of all the guild roles. - - Raises - ------ - hikari.errors.NotFound - If either the guild or any of the roles aren't found. - hikari.errors.Forbidden - If you lack the `MANAGE_ROLES` permission or you're not in the guild. - hikari.errors.BadRequest - If you provide invalid values for the `position` fields. - """ - payload = [{"id": r[0], "position": r[1]} for r in (role, *roles)] - route = routes.PATCH_GUILD_ROLES.compile(guild_id=guild_id) - return await self._request_json_response(route, body=payload) - - async def modify_guild_role( # lgtm [py/similar-function] - self, - guild_id: str, - role_id: str, - *, - name: str = ..., - permissions: int = ..., - color: int = ..., - hoist: bool = ..., - mentionable: bool = ..., - reason: str = ..., - ) -> more_typing.JSONObject: - """Edits a role in a given guild. - - Parameters - ---------- - guild_id : str - The ID of the guild the role belong to. - role_id : str - The ID of the role you want to edit. - name : str - If specified, the new role's name string. - permissions : int - If specified, the new permissions integer for the role. - color : int - If specified, the new color for the new role. - hoist : bool - If specified, whether the role should hoist or not. - mentionable : bool - If specified, whether the role should be mentionable or not. - reason : str - If specified, the audit log reason explaining why the operation - was performed. - - Returns - ------- - more_typing.JSONObject - The edited role object. - - Raises - ------ - hikari.errors.NotFound - If either the guild or role aren't found. - hikari.errors.Forbidden - If you lack the `MANAGE_ROLES` permission or you're not in the guild. - hikari.errors.BadRequest - If you provide invalid values for the role attributes. - """ - payload = {} - conversions.put_if_specified(payload, "name", name) - conversions.put_if_specified(payload, "permissions", permissions) - conversions.put_if_specified(payload, "color", color) - conversions.put_if_specified(payload, "hoist", hoist) - conversions.put_if_specified(payload, "mentionable", mentionable) - route = routes.PATCH_GUILD_ROLE.compile(guild_id=guild_id, role_id=role_id) - return await self._request_json_response(route, body=payload, reason=reason) - - async def delete_guild_role(self, guild_id: str, role_id: str) -> None: - """Delete a role from a given guild. - - Parameters - ---------- - guild_id : str - The ID of the guild you want to remove the role from. - role_id : str - The ID of the role you want to delete. - - Raises - ------ - hikari.errors.NotFound - If either the guild or the role aren't found. - hikari.errors.Forbidden - If you lack the `MANAGE_ROLES` permission or are not in the guild. - """ - route = routes.DELETE_GUILD_ROLE.compile(guild_id=guild_id, role_id=role_id) - await self._request_json_response(route) - - async def get_guild_prune_count(self, guild_id: str, days: int) -> int: - """Get the estimated prune count for a given guild. - - Parameters - ---------- - guild_id : str - The ID of the guild you want to get the count for. - days : int - The number of days to count prune for (at least `1`). - - Returns - ------- - int - The number of members estimated to be pruned. - - Raises - ------ - hikari.errors.NotFound - If the guild is not found. - hikari.errors.Forbidden - If you lack the `KICK_MEMBERS` or you are not in the guild. - hikari.errors.BadRequest - If you pass an invalid amount of days. - """ - payload = {"days": days} - route = routes.GET_GUILD_PRUNE.compile(guild_id=guild_id) - result = await self._request_json_response(route, query=payload) - return int(result["pruned"]) - - async def begin_guild_prune( - self, guild_id: str, days: int, *, compute_prune_count: bool = ..., reason: str = ..., - ) -> typing.Optional[int]: - """Prune members of a given guild based on the number of inactive days. - - Parameters - ---------- - guild_id : str - The ID of the guild you want to prune member of. - days : int - The number of inactivity days you want to use as filter. - compute_prune_count : bool - Whether a count of pruned members is returned or not. - Discouraged for large guilds out of politeness. - reason : str - If specified, the audit log reason explaining why the operation - was performed. - - Returns - ------- - int | None - The number of members who were kicked if `compute_prune_count` - is True, else None. - - Raises - ------ - hikari.errors.NotFound - If the guild is not found: - hikari.errors.Forbidden - If you lack the `KICK_MEMBER` permission or are not in the guild. - hikari.errors.BadRequest - If you provide invalid values for the `days` or `compute_prune_count` fields. - """ - query = {"days": days} - conversions.put_if_specified(query, "compute_prune_count", compute_prune_count, lambda v: str(v).lower()) - route = routes.POST_GUILD_PRUNE.compile(guild_id=guild_id) - result = await self._request_json_response(route, query=query, reason=reason) - - try: - return int(result["pruned"]) - except (TypeError, KeyError): - return None - - async def get_guild_voice_regions(self, guild_id: str) -> typing.Sequence[more_typing.JSONObject]: - """Get the voice regions for a given guild. - - Parameters - ---------- - guild_id : str - The ID of the guild to get the voice regions for. - - Returns - ------- - typing.Sequence[more_typing.JSONObject] - A list of voice region objects. - - Raises - ------ - hikari.errors.NotFound - If the guild is not found. - hikari.errors.Forbidden - If you are not in the guild. - """ - route = routes.GET_GUILD_VOICE_REGIONS.compile(guild_id=guild_id) - return await self._request_json_response(route) - - async def get_guild_invites(self, guild_id: str) -> typing.Sequence[more_typing.JSONObject]: - """Get the invites for a given guild. - - Parameters - ---------- - guild_id : str - The ID of the guild to get the invites for. - - Returns - ------- - typing.Sequence[more_typing.JSONObject] - A list of invite objects (with metadata). - - Raises - ------ - hikari.errors.NotFound - If the guild is not found. - hikari.errors.Forbidden - If you lack the `MANAGE_GUILD` permission or are not in the guild. - """ - route = routes.GET_GUILD_INVITES.compile(guild_id=guild_id) - return await self._request_json_response(route) - - async def get_guild_integrations(self, guild_id: str) -> typing.Sequence[more_typing.JSONObject]: - """Get the integrations for a given guild. - - Parameters - ---------- - guild_id : int - The ID of the guild to get the integrations for. - - Returns - ------- - typing.Sequence[more_typing.JSONObject] - A list of integration objects. - - Raises - ------ - hikari.errors.NotFound - If the guild is not found. - hikari.errors.Forbidden - If you lack the `MANAGE_GUILD` permission or are not in the guild. - """ - route = routes.GET_GUILD_INTEGRATIONS.compile(guild_id=guild_id) - return await self._request_json_response(route) - - async def modify_guild_integration( - self, - guild_id: str, - integration_id: str, - *, - expire_behaviour: int = ..., - expire_grace_period: int = ..., - enable_emojis: bool = ..., - reason: str = ..., - ) -> None: - """Edit an integrations for a given guild. - - Parameters - ---------- - guild_id : str - The ID of the guild to which the integration belongs to. - integration_id : str - The ID of the integration. - expire_behaviour : int - If specified, the behaviour for when an integration subscription - lapses. - expire_grace_period : int - If specified, time interval in seconds in which the integration - will ignore lapsed subscriptions. - enable_emojis : bool - If specified, whether emojis should be synced for this integration. - reason : str - If specified, the audit log reason explaining why the operation - was performed. - - Raises - ------ - hikari.errors.NotFound - If either the guild or the integration aren't found. - hikari.errors.Forbidden - If you lack the `MANAGE_GUILD` permission or are not in the guild. - """ - payload = {} - conversions.put_if_specified(payload, "expire_behaviour", expire_behaviour) - conversions.put_if_specified(payload, "expire_grace_period", expire_grace_period) - # This is inconsistently named in their API. - conversions.put_if_specified(payload, "enable_emoticons", enable_emojis) - route = routes.PATCH_GUILD_INTEGRATION.compile(guild_id=guild_id, integration_id=integration_id) - await self._request_json_response(route, body=payload, reason=reason) - - async def delete_guild_integration(self, guild_id: str, integration_id: str, *, reason: str = ...) -> None: - """Delete an integration for the given guild. - - Parameters - ---------- - guild_id : str - The ID of the guild to which the integration belongs to. - integration_id : str - The ID of the integration to delete. - reason : str - If specified, the audit log reason explaining why the operation - was performed. - - Raises - ------ - hikari.errors.NotFound - If either the guild or the integration aren't found. - hikari.errors.Forbidden - If you lack the `MANAGE_GUILD` permission or are not in the guild. - """ - route = routes.DELETE_GUILD_INTEGRATION.compile(guild_id=guild_id, integration_id=integration_id) - await self._request_json_response(route, reason=reason) - - async def sync_guild_integration(self, guild_id: str, integration_id: str) -> None: - """Sync the given integration. - - Parameters - ---------- - guild_id : str - The ID of the guild to which the integration belongs to. - integration_id : str - The ID of the integration to sync. - - Raises - ------ - hikari.errors.NotFound - If either the guild or the integration aren't found. - hikari.errors.Forbidden - If you lack the `MANAGE_GUILD` permission or are not in the guild. - """ - route = routes.POST_GUILD_INTEGRATION_SYNC.compile(guild_id=guild_id, integration_id=integration_id) - await self._request_json_response(route) - - async def get_guild_embed(self, guild_id: str) -> more_typing.JSONObject: - """Get the embed for a given guild. - - Parameters - ---------- - guild_id : str - The ID of the guild to get the embed for. - - Returns - ------- - more_typing.JSONObject - A guild embed object. - - Raises - ------ - hikari.errors.NotFound - If the guild is not found. - hikari.errors.Forbidden - If you either lack the `MANAGE_GUILD` permission or are not in the guild. - """ - route = routes.GET_GUILD_WIDGET.compile(guild_id=guild_id) - return await self._request_json_response(route) - - async def modify_guild_embed( - self, guild_id: str, *, channel_id: typing.Optional[str] = ..., enabled: bool = ..., reason: str = ..., - ) -> more_typing.JSONObject: - """Edit the embed for a given guild. - - Parameters - ---------- - guild_id : str - The ID of the guild to edit the embed for. - channel_id : str | None - If specified, the channel that this embed's invite should target. - Set to None to disable invites for this embed. - enabled : bool - If specified, whether this embed should be enabled. - reason : str - If specified, the audit log reason explaining why the operation - was performed. - - Returns - ------- - more_typing.JSONObject - The updated embed object. - - Raises - ------ - hikari.errors.NotFound - If the guild is not found. - hikari.errors.Forbidden - If you either lack the `MANAGE_GUILD` permission or are not in the guild. - """ - payload = {} - conversions.put_if_specified(payload, "channel", channel_id) - conversions.put_if_specified(payload, "enabled", enabled) - route = routes.PATCH_GUILD_WIDGET.compile(guild_id=guild_id) - return await self._request_json_response(route, body=payload, reason=reason) - - async def get_guild_vanity_url(self, guild_id: str) -> more_typing.JSONObject: - """Get the vanity URL for a given guild. - - Parameters - ---------- - guild_id : str - The ID of the guild to get the vanity URL for. - - Returns - ------- - more_typing.JSONObject - A partial invite object containing the vanity URL in the `code` field. - - Raises - ------ - hikari.errors.NotFound - If the guild is not found. - hikari.errors.Forbidden - If you either lack the `MANAGE_GUILD` permission or are not in the guild. - """ - route = routes.GET_GUILD_VANITY_URL.compile(guild_id=guild_id) - return await self._request_json_response(route) - - def get_guild_widget_image_url(self, guild_id: str, *, style: str = ...) -> str: - """Get the URL for a guild widget. - - Parameters - ---------- - guild_id : str - The guild ID to use for the widget. - style : str - If specified, the style of the widget. - - Returns - ------- - str - A URL to retrieve a PNG widget for your guild. - - !!! note - This does not actually make any form of request, and shouldn't be - awaited. Thus, it doesn't have rate limits either. - - !!! warning - The guild must have the widget enabled in the guild settings for - this to be valid. - """ - query = "" if style is ... else f"?style={style}" - route = routes.GET_GUILD_BANNER_IMAGE.compile(guild_id=guild_id) - return route.create_url(self.base_url) + query - - async def get_invite(self, invite_code: str, *, with_counts: bool = ...) -> more_typing.JSONObject: - """Getsthe given invite. - - Parameters - ---------- - invite_code : str - The ID for wanted invite. - with_counts : bool - If specified, whether to attempt to count the number of - times the invite has been used. - - Returns - ------- - more_typing.JSONObject - The requested invite object. - - Raises - ------ - hikari.errors.NotFound - If the invite is not found. - """ - query = {} - conversions.put_if_specified(query, "with_counts", with_counts, lambda v: str(v).lower()) - route = routes.GET_INVITE.compile(invite_code=invite_code) - return await self._request_json_response(route, query=query) - - async def delete_invite(self, invite_code: str) -> None: - """Delete a given invite. - - Parameters - ---------- - invite_code : str - The ID for the invite to be deleted. - - Returns - ------- - None # Marker - Nothing, unlike what the API specifies. This is done to maintain - consistency with other calls of a similar nature in this API wrapper. - - Raises - ------ - hikari.errors.NotFound - If the invite is not found. - hikari.errors.Forbidden - If you lack either `MANAGE_CHANNELS` on the channel the invite - belongs to or `MANAGE_GUILD` for guild-global delete. - """ - route = routes.DELETE_INVITE.compile(invite_code=invite_code) - return await self._request_json_response(route) - - async def get_current_user(self) -> more_typing.JSONObject: - """Get the current user that is represented by token given to the client. - - Returns - ------- - more_typing.JSONObject - The current user object. - """ - route = routes.GET_MY_USER.compile() - return await self._request_json_response(route) - - async def get_user(self, user_id: str) -> more_typing.JSONObject: - """Get a given user. - - Parameters - ---------- - user_id : str - The ID of the user to get. - - Returns - ------- - more_typing.JSONObject - The requested user object. - - Raises - ------ - hikari.errors.NotFound - If the user is not found. - """ - route = routes.GET_USER.compile(user_id=user_id) - return await self._request_json_response(route) - - async def modify_current_user( - self, *, username: str = ..., avatar: typing.Optional[bytes] = ..., - ) -> more_typing.JSONObject: - """Edit the current user. - - Parameters - ---------- - username : str - If specified, the new username string. - avatar : bytes | None - If specified, the new avatar image in bytes form. - If it is None, the avatar is removed. - - !!! warning - Verified bots will not be able to change their username on this - endpoint, and should contact Discord support instead to change - this value. - - Returns - ------- - more_typing.JSONObject - The updated user object. - - Raises - ------ - hikari.errors.BadRequest - If you pass username longer than the limit (`2-32`) or an invalid image. - """ - payload = {} - conversions.put_if_specified(payload, "username", username) - conversions.put_if_specified(payload, "avatar", avatar, conversions.image_bytes_to_image_data) - route = routes.PATCH_MY_USER.compile() - return await self._request_json_response(route, body=payload) - - async def get_current_user_connections(self) -> typing.Sequence[more_typing.JSONObject]: - """Get the current user's connections. - - This endpoint can be used with both `Bearer` and `Bot` tokens but - will usually return an empty list for bots (with there being some exceptions - to this, like user accounts that have been converted to bots). - - Returns - ------- - typing.Sequence[more_typing.JSONObject] - A list of connection objects. - """ - route = routes.GET_MY_CONNECTIONS.compile() - return await self._request_json_response(route) - - async def get_current_user_guilds( - self, *, before: str = ..., after: str = ..., limit: int = ..., - ) -> typing.Sequence[more_typing.JSONObject]: - """Get the guilds the current user is in. - - Parameters - ---------- - before : str - If specified, the guild ID to get guilds before it. - - after : str - If specified, the guild ID to get guilds after it. - - limit : int - If specified, the limit of guilds to get. Has to be between - `1` and `100`. - - Returns - ------- - typing.Sequence[more_typing.JSONObject] - A list of partial guild objects. - - Raises - ------ - hikari.errors.BadRequest - If you pass both `before` and `after` or an - invalid value for `limit`. - """ - query = {} - conversions.put_if_specified(query, "before", before) - conversions.put_if_specified(query, "after", after) - conversions.put_if_specified(query, "limit", limit) - route = routes.GET_MY_GUILDS.compile() - return await self._request_json_response(route, query=query) - - async def leave_guild(self, guild_id: str) -> None: - """Make the current user leave a given guild. - - Parameters - ---------- - guild_id : str - The ID of the guild to leave. - - Raises - ------ - hikari.errors.NotFound - If the guild is not found. - """ - route = routes.DELETE_MY_GUILD.compile(guild_id=guild_id) - await self._request_json_response(route) - - async def create_dm(self, recipient_id: str) -> more_typing.JSONObject: - """Create a new DM channel with a given user. - - Parameters - ---------- - recipient_id : str - The ID of the user to create the new DM channel with. - - Returns - ------- - more_typing.JSONObject - The newly created DM channel object. - - Raises - ------ - hikari.errors.NotFound - If the recipient is not found. - """ - payload = {"recipient_id": recipient_id} - route = routes.POST_MY_CHANNELS.compile() - return await self._request_json_response(route, body=payload) - - async def list_voice_regions(self) -> typing.Sequence[more_typing.JSONObject]: - """Get the voice regions that are available. - - Returns - ------- - typing.Sequence[more_typing.JSONObject] - A list of voice regions available - - !!! note - This does not include VIP servers. - """ - route = routes.GET_VOICE_REGIONS.compile() - return await self._request_json_response(route) - - async def create_webhook( - self, channel_id: str, name: str, *, avatar: bytes = ..., reason: str = ..., - ) -> more_typing.JSONObject: - """Create a webhook for a given channel. - - Parameters - ---------- - channel_id : str - The ID of the channel for webhook to be created in. - name : str - The webhook's name string. - avatar : bytes - If specified, the avatar image in bytes form. - reason : str - If specified, the audit log reason explaining why the operation - was performed. - - Returns - ------- - more_typing.JSONObject - The newly created webhook object. - - Raises - ------ - hikari.errors.NotFound - If the channel is not found. - hikari.errors.Forbidden - If you either lack the `MANAGE_WEBHOOKS` permission or - can not see the given channel. - hikari.errors.BadRequest - If the avatar image is too big or the format is invalid. - """ - payload = {"name": name} - conversions.put_if_specified(payload, "avatar", avatar, conversions.image_bytes_to_image_data) - route = routes.POST_CHANNEL_WEBHOOKS.compile(channel_id=channel_id) - return await self._request_json_response(route, body=payload, reason=reason) - - async def get_channel_webhooks(self, channel_id: str) -> typing.Sequence[more_typing.JSONObject]: - """Get all webhooks from a given channel. - - Parameters - ---------- - channel_id : str - The ID of the channel to get the webhooks from. - - Returns - ------- - typing.Sequence[more_typing.JSONObject] - A list of webhook objects for the give channel. - - Raises - ------ - hikari.errors.NotFound - If the channel is not found. - hikari.errors.Forbidden - If you either lack the `MANAGE_WEBHOOKS` permission or - can not see the given channel. - """ - route = routes.GET_CHANNEL_WEBHOOKS.compile(channel_id=channel_id) - return await self._request_json_response(route) - - async def get_guild_webhooks(self, guild_id: str) -> typing.Sequence[more_typing.JSONObject]: - """Get all webhooks for a given guild. - - Parameters - ---------- - guild_id : str - The ID for the guild to get the webhooks from. - - Returns - ------- - typing.Sequence[more_typing.JSONObject] - A list of webhook objects for the given guild. - - Raises - ------ - hikari.errors.NotFound - If the guild is not found. - hikari.errors.Forbidden - If you either lack the `MANAGE_WEBHOOKS` permission or - aren't a member of the given guild. - """ - route = routes.GET_GUILD_WEBHOOKS.compile(guild_id=guild_id) - return await self._request_json_response(route) - - async def get_webhook(self, webhook_id: str, *, webhook_token: str = ...) -> more_typing.JSONObject: - """Get a given webhook. - - Parameters - ---------- - webhook_id : str - The ID of the webhook to get. - webhook_token : str - If specified, the webhook token to use to get it (bypassing bot authorization). - - Returns - ------- - more_typing.JSONObject - The requested webhook object. - - Raises - ------ - hikari.errors.NotFound - If the webhook is not found. - hikari.errors.Forbidden - If you're not in the guild that owns this webhook or - lack the `MANAGE_WEBHOOKS` permission. - hikari.errors.Unauthorized - If you pass a token that's invalid for the target webhook. - """ - if webhook_token is ...: - route = routes.GET_WEBHOOK.compile(webhook_id=webhook_id) - else: - route = routes.GET_WEBHOOK_WITH_TOKEN.compile(webhook_id=webhook_id, webhook_token=webhook_token) - return await self._request_json_response(route, suppress_authorization_header=webhook_token is not ...) - - async def modify_webhook( - self, - webhook_id: str, - *, - webhook_token: str = ..., - name: str = ..., - avatar: typing.Optional[bytes] = ..., - channel_id: str = ..., - reason: str = ..., - ) -> more_typing.JSONObject: - """Edit a given webhook. - - Parameters - ---------- - webhook_id : str - The ID of the webhook to edit. - webhook_token : str - If specified, the webhook token to use to modify it (bypassing bot authorization). - name : str - If specified, the new name string. - avatar : bytes - If specified, the new avatar image in bytes form. If None, then - it is removed. - channel_id : str - If specified, the ID of the new channel the given - webhook should be moved to. - reason : str - If specified, the audit log reason explaining why the operation - was performed. - - Returns - ------- - more_typing.JSONObject - The updated webhook object. - - Raises - ------ - hikari.errors.NotFound - If either the webhook or the channel aren't found. - hikari.errors.Forbidden - If you either lack the `MANAGE_WEBHOOKS` permission or - aren't a member of the guild this webhook belongs to. - hikari.errors.Unauthorized - If you pass a token that's invalid for the target webhook. - """ - payload = {} - conversions.put_if_specified(payload, "name", name) - conversions.put_if_specified(payload, "channel", channel_id) - conversions.put_if_specified(payload, "avatar", avatar, conversions.image_bytes_to_image_data) - if webhook_token is ...: - route = routes.PATCH_WEBHOOK.compile(webhook_id=webhook_id) - else: - route = routes.PATCH_WEBHOOK_WITH_TOKEN.compile(webhook_id=webhook_id, webhook_token=webhook_token) - return await self._request_json_response( - route, body=payload, reason=reason, suppress_authorization_header=webhook_token is not ..., - ) - - async def delete_webhook(self, webhook_id: str, *, webhook_token: str = ...) -> None: - """Delete a given webhook. - - Parameters - ---------- - webhook_id : str - The ID of the webhook to delete - webhook_token : str - If specified, the webhook token to use to - delete it (bypassing bot authorization). - - Raises - ------ - hikari.errors.NotFound - If the webhook is not found. - hikari.errors.Forbidden - If you either lack the `MANAGE_WEBHOOKS` permission or - aren't a member of the guild this webhook belongs to. - hikari.errors.Unauthorized - If you pass a token that's invalid for the target webhook. - """ - if webhook_token is ...: - route = routes.DELETE_WEBHOOK.compile(webhook_id=webhook_id) - else: - route = routes.DELETE_WEBHOOK_WITH_TOKEN.compile(webhook_id=webhook_id, webhook_token=webhook_token) - await self._request_json_response(route, suppress_authorization_header=webhook_token is not ...) - - async def execute_webhook( # pylint:disable=too-many-locals - self, - webhook_id: str, - webhook_token: str, - *, - content: str = ..., - username: str = ..., - avatar_url: str = ..., - tts: bool = ..., - wait: bool = ..., - files: typing.Sequence[files_.BaseStream] = ..., - embeds: typing.Sequence[more_typing.JSONObject] = ..., - allowed_mentions: more_typing.JSONObject = ..., - ) -> typing.Optional[more_typing.JSONObject]: - """Execute a webhook to create a message in its channel. - - Parameters - ---------- - webhook_id : str - The ID of the webhook to execute. - webhook_token : str - The token of the webhook to execute. - content : str - If specified, the webhook message content to send. - username : str - If specified, the username to override the webhook's username - for this request. - avatar_url : str - If specified, the url of an image to override the webhook's - avatar with for this request. - tts : bool - If specified, whether this webhook should create a TTS message. - wait : bool - If specified, whether this request should wait for the webhook - to be executed and return the resultant message object. - files : typing.Sequence[hikari.models.files.BaseStream] - If specified, the optional file objects to upload. - embeds : typing.Sequence[more_typing.JSONObject] - If specified, the sequence of embed objects that will be sent - with this message. - allowed_mentions : more_typing.JSONObject - If specified, the mentions to parse from the `content`. - If not specified, will parse all mentions from the `content`. - - Returns - ------- - more_typing.JSONObject | None - The created message object if `wait` is `True`, else - `None`. - - Raises - ------ - hikari.errors.NotFound - If the channel ID or webhook ID is not found. - hikari.errors.BadRequest - This can be raised if the file is too large; if the embed exceeds - the defined limits; if the message content is specified only and - empty or greater than `2000` characters; if neither content, file - or embed are specified; if there is a duplicate id in only of the - fields in `allowed_mentions`; if you specify to parse all - users/roles mentions but also specify which users/roles to parse - only. - hikari.errors.Forbidden - If you lack permissions to send to this channel. - hikari.errors.Unauthorized - If you pass a token that's invalid for the target webhook. - """ - form = aiohttp.FormData() - - json_payload = {} - conversions.put_if_specified(json_payload, "content", content) - conversions.put_if_specified(json_payload, "username", username) - conversions.put_if_specified(json_payload, "avatar_url", avatar_url) - conversions.put_if_specified(json_payload, "tts", tts) - conversions.put_if_specified(json_payload, "embeds", embeds) - conversions.put_if_specified(json_payload, "allowed_mentions", allowed_mentions) - - form.add_field("payload_json", json.dumps(json_payload), content_type="application/json") - - if files is ...: - files = more_collections.EMPTY_SEQUENCE - - for i, file in enumerate(files): - form.add_field(f"file{i}", file, filename=file.name, content_type="application/octet-stream") - - query = {} - conversions.put_if_specified(query, "wait", wait, lambda v: str(v).lower()) - - route = routes.POST_WEBHOOK_WITH_TOKEN.compile(webhook_id=webhook_id, webhook_token=webhook_token) - return await self._request_json_response(route, body=form, query=query, suppress_authorization_header=True) - - ########## - # OAUTH2 # - ########## - - async def get_current_application_info(self) -> more_typing.JSONObject: - """Get the current application information. - - Returns - ------- - more_typing.JSONObject - An application info object. - """ - route = routes.GET_MY_APPLICATION.compile() - return await self._request_json_response(route) From bbe68aa07fdfff758b086bd26a0a4668decb43e8 Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Mon, 25 May 2020 20:47:58 +0100 Subject: [PATCH 396/922] Optimised imports and moved class-specific stuff into the gateway implementation. --- hikari/__init__.py | 3 +- hikari/entity_factory.py | 1 - hikari/events/guild.py | 2 +- hikari/events/message.py | 2 +- hikari/impl/bot.py | 2 - hikari/impl/pagination.py | 2 +- hikari/internal/conversions.py | 1 - hikari/internal/more_typing.py | 1 - hikari/models/guilds.py | 2 +- hikari/net/gateway.py | 187 ++++++++++++++++----------------- hikari/net/iterators.py | 2 +- hikari/net/rest.py | 15 ++- hikari/net/rest_utils.py | 6 +- 13 files changed, 105 insertions(+), 121 deletions(-) diff --git a/hikari/__init__.py b/hikari/__init__.py index 175546c834..17b667b7a6 100644 --- a/hikari/__init__.py +++ b/hikari/__init__.py @@ -30,12 +30,11 @@ from hikari._about import __license__ from hikari._about import __url__ from hikari._about import __version__ - from hikari.errors import * from hikari.events import * from hikari.http_settings import * +from hikari.impl import * from hikari.models import * from hikari.net import * -from hikari.impl import * __all__ = [] diff --git a/hikari/entity_factory.py b/hikari/entity_factory.py index a91eb5c723..ff5027014e 100644 --- a/hikari/entity_factory.py +++ b/hikari/entity_factory.py @@ -24,7 +24,6 @@ import abc import typing - if typing.TYPE_CHECKING: from hikari.internal import more_typing from hikari.models import applications diff --git a/hikari/events/guild.py b/hikari/events/guild.py index 209d2c5d06..985601c24e 100644 --- a/hikari/events/guild.py +++ b/hikari/events/guild.py @@ -50,9 +50,9 @@ from hikari.models import emojis as emojis_models from hikari.models import guilds from hikari.models import intents -from ..internal import unset from hikari.models import users from . import base as base_events +from ..internal import unset if typing.TYPE_CHECKING: import datetime diff --git a/hikari/events/message.py b/hikari/events/message.py index 128d21b64a..8057e09aef 100644 --- a/hikari/events/message.py +++ b/hikari/events/message.py @@ -44,9 +44,9 @@ from hikari.models import guilds from hikari.models import intents from hikari.models import messages -from ..internal import unset from hikari.models import users from . import base as base_events +from ..internal import unset if typing.TYPE_CHECKING: import datetime diff --git a/hikari/impl/bot.py b/hikari/impl/bot.py index 8faaa9fcd8..5144f48345 100644 --- a/hikari/impl/bot.py +++ b/hikari/impl/bot.py @@ -19,8 +19,6 @@ from __future__ import annotations -import asyncio -import contextlib import logging import typing from concurrent import futures diff --git a/hikari/impl/pagination.py b/hikari/impl/pagination.py index dd838b7fcf..783825e2d4 100644 --- a/hikari/impl/pagination.py +++ b/hikari/impl/pagination.py @@ -22,12 +22,12 @@ import datetime -from hikari.net import iterators from hikari.models import applications from hikari.models import bases from hikari.models import guilds from hikari.models import messages from hikari.models import users +from hikari.net import iterators class GuildPaginator(iterators._BufferedLazyIterator[guilds.Guild]): diff --git a/hikari/internal/conversions.py b/hikari/internal/conversions.py index bf5077cadb..3d72b3b269 100644 --- a/hikari/internal/conversions.py +++ b/hikari/internal/conversions.py @@ -56,7 +56,6 @@ _Unique_contra = typing.TypeVar("_Unique_contra", bound=bases.Unique, contravariant=True) _CollectionImpl_contra = typing.TypeVar("_CollectionImpl_contra", bound=typing.Collection, contravariant=True) - DISCORD_EPOCH: typing.Final[int] = 1_420_070_400 ISO_8601_DATE_PART: typing.Final[typing.Pattern] = re.compile(r"^(\d{4})-(\d{2})-(\d{2})") ISO_8601_TIME_PART: typing.Final[typing.Pattern] = re.compile(r"T(\d{2}):(\d{2}):(\d{2})(?:\.(\d{1,6}))?", re.I) diff --git a/hikari/internal/more_typing.py b/hikari/internal/more_typing.py index e727214934..d94df6c9e4 100644 --- a/hikari/internal/more_typing.py +++ b/hikari/internal/more_typing.py @@ -53,7 +53,6 @@ from typing import TypeVar as _TypeVar from typing import Union as _Union - if _TYPE_CHECKING: import asyncio import contextvars diff --git a/hikari/models/guilds.py b/hikari/models/guilds.py index 0df26e9400..0c24025343 100644 --- a/hikari/models/guilds.py +++ b/hikari/models/guilds.py @@ -67,8 +67,8 @@ from . import colors from . import emojis as emojis_ from . import permissions as permissions_ -from ..internal import unset from . import users +from ..internal import unset from ..net import urls if typing.TYPE_CHECKING: diff --git a/hikari/net/gateway.py b/hikari/net/gateway.py index 43be5bca8c..77da8ebd34 100644 --- a/hikari/net/gateway.py +++ b/hikari/net/gateway.py @@ -38,10 +38,10 @@ from hikari.internal import more_enums from hikari.internal import more_typing from hikari.internal import ratelimits +from hikari.internal import unset from hikari.models import bases from hikari.models import channels from hikari.models import guilds -from hikari.internal import unset from hikari.net import http_client from hikari.net import user_agents @@ -68,70 +68,6 @@ class Activity: """The activity type.""" -@more_enums.must_be_unique -class _GatewayCloseCode(int, more_enums.Enum): - """Reasons for closing a gateway connection.""" - - RFC_6455_NORMAL_CLOSURE = 1000 - RFC_6455_GOING_AWAY = 1001 - RFC_6455_PROTOCOL_ERROR = 1002 - RFC_6455_TYPE_ERROR = 1003 - RFC_6455_ENCODING_ERROR = 1007 - RFC_6455_POLICY_VIOLATION = 1008 - RFC_6455_TOO_BIG = 1009 - RFC_6455_UNEXPECTED_CONDITION = 1011 - - # Discord seems to invalidate sessions if I send a 1xxx, which is useless - # for invalid session and reconnect messages where I want to be able to - # resume. - DO_NOT_INVALIDATE_SESSION = 3000 - - UNKNOWN_ERROR = 4000 - UNKNOWN_OPCODE = 4001 - DECODE_ERROR = 4002 - NOT_AUTHENTICATED = 4003 - AUTHENTICATION_FAILED = 4004 - ALREADY_AUTHENTICATED = 4005 - INVALID_SEQ = 4007 - RATE_LIMITED = 4008 - SESSION_TIMEOUT = 4009 - INVALID_SHARD = 4010 - SHARDING_REQUIRED = 4011 - INVALID_VERSION = 4012 - INVALID_INTENT = 4013 - DISALLOWED_INTENT = 4014 - - -@more_enums.must_be_unique -class _GatewayOpcode(int, more_enums.Enum): - """Opcodes that the gateway uses internally.""" - - DISPATCH = 0 - HEARTBEAT = 1 - IDENTIFY = 2 - PRESENCE_UPDATE = 3 - VOICE_STATE_UPDATE = 4 - RESUME = 6 - RECONNECT = 7 - REQUEST_GUILD_MEMBERS = 8 - INVALID_SESSION = 9 - HELLO = 10 - HEARTBEAT_ACK = 11 - - -class _Reconnect(RuntimeError): - __slots__ = () - - -class _SocketClosed(RuntimeError): - __slots__ = () - - -@attr.s(auto_attribs=True, slots=True) -class _InvalidSession(RuntimeError): - can_resume: bool = False - - RawDispatchT = typing.Callable[["Gateway", str, more_typing.JSONObject], more_typing.Coroutine[None]] @@ -177,6 +113,65 @@ class Gateway(http_client.HTTPClient): Gateway API version to use. """ + @more_enums.must_be_unique + class _GatewayCloseCode(int, more_enums.Enum): + """Reasons for closing a gateway connection.""" + + RFC_6455_NORMAL_CLOSURE = 1000 + RFC_6455_GOING_AWAY = 1001 + RFC_6455_PROTOCOL_ERROR = 1002 + RFC_6455_TYPE_ERROR = 1003 + RFC_6455_ENCODING_ERROR = 1007 + RFC_6455_POLICY_VIOLATION = 1008 + RFC_6455_TOO_BIG = 1009 + RFC_6455_UNEXPECTED_CONDITION = 1011 + + # Discord seems to invalidate sessions if I send a 1xxx, which is useless + # for invalid session and reconnect messages where I want to be able to + # resume. + DO_NOT_INVALIDATE_SESSION = 3000 + + UNKNOWN_ERROR = 4000 + UNKNOWN_OPCODE = 4001 + DECODE_ERROR = 4002 + NOT_AUTHENTICATED = 4003 + AUTHENTICATION_FAILED = 4004 + ALREADY_AUTHENTICATED = 4005 + INVALID_SEQ = 4007 + RATE_LIMITED = 4008 + SESSION_TIMEOUT = 4009 + INVALID_SHARD = 4010 + SHARDING_REQUIRED = 4011 + INVALID_VERSION = 4012 + INVALID_INTENT = 4013 + DISALLOWED_INTENT = 4014 + + @more_enums.must_be_unique + class _GatewayOpcode(int, more_enums.Enum): + """Opcodes that the gateway uses internally.""" + + DISPATCH = 0 + HEARTBEAT = 1 + IDENTIFY = 2 + PRESENCE_UPDATE = 3 + VOICE_STATE_UPDATE = 4 + RESUME = 6 + RECONNECT = 7 + REQUEST_GUILD_MEMBERS = 8 + INVALID_SESSION = 9 + HELLO = 10 + HEARTBEAT_ACK = 11 + + class _Reconnect(RuntimeError): + __slots__ = () + + class _SocketClosed(RuntimeError): + __slots__ = () + + @attr.s(auto_attribs=True, slots=True) + class _InvalidSession(RuntimeError): + can_resume: bool = False + def __init__( self, *, @@ -278,7 +273,7 @@ async def close(self) -> None: if self._ws is not None: self.logger.warning("gateway client closed by user, will not attempt to restart") - await self._close_ws(_GatewayCloseCode.RFC_6455_NORMAL_CLOSURE, "user shut down application") + await self._close_ws(self._GatewayCloseCode.RFC_6455_NORMAL_CLOSURE, "user shut down application") async def _run(self) -> None: """Start the shard and wait for it to shut down.""" @@ -345,22 +340,22 @@ async def _run_once(self) -> bool: "failed to connect to Discord because %s.%s: %s", type(ex).__module__, type(ex).__qualname__, str(ex), ) - except _InvalidSession as ex: + except self._InvalidSession as ex: if ex.can_resume: self.logger.warning("invalid session, so will attempt to resume session %s", self._session_id) - await self._close_ws(_GatewayCloseCode.DO_NOT_INVALIDATE_SESSION, "invalid session (resume)") + await self._close_ws(self._GatewayCloseCode.DO_NOT_INVALIDATE_SESSION, "invalid session (resume)") else: self.logger.warning("invalid session, so will attempt to reconnect with new session") - await self._close_ws(_GatewayCloseCode.RFC_6455_NORMAL_CLOSURE, "invalid session (no resume)") + await self._close_ws(self._GatewayCloseCode.RFC_6455_NORMAL_CLOSURE, "invalid session (no resume)") self._seq = None self._session_id = None - except _Reconnect: + except self._Reconnect: self.logger.warning("instructed by Discord to reconnect and resume session %s", self._session_id) self._backoff.reset() - await self._close_ws(_GatewayCloseCode.DO_NOT_INVALIDATE_SESSION, "reconnecting") + await self._close_ws(self._GatewayCloseCode.DO_NOT_INVALIDATE_SESSION, "reconnecting") - except _SocketClosed: + except self._SocketClosed: # The socket has already closed, so no need to close it again. if not self._zombied and not self._request_close_event.is_set(): # This will occur due to a network issue such as a network adapter going down. @@ -371,7 +366,7 @@ async def _run_once(self) -> bool: except Exception as ex: self.logger.error("unexpected exception occurred, shard will now die", exc_info=ex) - await self._close_ws(_GatewayCloseCode.RFC_6455_UNEXPECTED_CONDITION, "unexpected error occurred") + await self._close_ws(self._GatewayCloseCode.RFC_6455_UNEXPECTED_CONDITION, "unexpected error occurred") raise finally: @@ -410,7 +405,7 @@ async def update_presence( The web status to show. If unset, this will not be changed. """ payload = self._build_presence_payload(idle_since, is_afk, activity, status) - await self._send_json({"op": _GatewayOpcode.PRESENCE_UPDATE, "d": payload}) + await self._send_json({"op": self._GatewayOpcode.PRESENCE_UPDATE, "d": payload}) self._idle_since = idle_since if not unset.is_unset(idle_since) else self._idle_since self._is_afk = is_afk if not unset.is_unset(is_afk) else self._is_afk self._activity = activity if not unset.is_unset(activity) else self._activity @@ -442,7 +437,7 @@ async def update_voice_state( `False`, then it will undeafen itself. """ payload = { - "op": _GatewayOpcode.VOICE_STATE_UPDATE, + "op": self._GatewayOpcode.VOICE_STATE_UPDATE, "d": { "guild_id": str(int(guild)), "channel": str(int(channel)) if channel is not None else None, @@ -460,7 +455,7 @@ async def _handshake(self) -> None: # HELLO! message = await self._receive_json_payload() op = message["op"] - if message["op"] != _GatewayOpcode.HELLO: + if message["op"] != self._GatewayOpcode.HELLO: raise errors.GatewayError(f"Expected HELLO opcode 10 but received {op}") self.heartbeat_interval = message["d"]["heartbeat_interval"] / 1_000.0 @@ -471,7 +466,7 @@ async def _handshake(self) -> None: # RESUME! await self._send_json( { - "op": _GatewayOpcode.RESUME, + "op": self._GatewayOpcode.RESUME, "d": {"token": self._token, "seq": self._seq, "session_id": self._session_id}, } ) @@ -480,7 +475,7 @@ async def _handshake(self) -> None: # IDENTIFY! # noinspection PyArgumentList payload = { - "op": _GatewayOpcode.IDENTIFY, + "op": self._GatewayOpcode.IDENTIFY, "d": { "token": self._token, "compress": False, @@ -513,13 +508,13 @@ async def _pulse(self) -> None: time_since_heartbeat_sent, ) self._zombied = True - await self._close_ws(_GatewayCloseCode.DO_NOT_INVALIDATE_SESSION, "zombie connection") + await self._close_ws(self._GatewayCloseCode.DO_NOT_INVALIDATE_SESSION, "zombie connection") return self.logger.debug( "preparing to send HEARTBEAT [s:%s, interval:%ss]", self._seq, self.heartbeat_interval ) - await self._send_json({"op": _GatewayOpcode.HEARTBEAT, "d": self._seq}) + await self._send_json({"op": self._GatewayOpcode.HEARTBEAT, "d": self._seq}) self.last_heartbeat_sent = self._now() try: @@ -538,7 +533,7 @@ async def _poll_events(self) -> None: op = message["op"] data = message["d"] - if op == _GatewayOpcode.DISPATCH: + if op == self._GatewayOpcode.DISPATCH: event = message["t"] self._seq = message["s"] if event == "READY": @@ -551,21 +546,21 @@ async def _poll_events(self) -> None: asyncio.create_task(self._dispatch(self, event, data), name=f"shard {self._shard_id} {event}") - elif op == _GatewayOpcode.HEARTBEAT: + elif op == self._GatewayOpcode.HEARTBEAT: self.logger.debug("received HEARTBEAT; sending HEARTBEAT ACK") - await self._send_json({"op": _GatewayOpcode.HEARTBEAT_ACK}) + await self._send_json({"op": self._GatewayOpcode.HEARTBEAT_ACK}) - elif op == _GatewayOpcode.HEARTBEAT_ACK: + elif op == self._GatewayOpcode.HEARTBEAT_ACK: self.heartbeat_latency = self._now() - self.last_heartbeat_sent self.logger.debug("received HEARTBEAT ACK [latency:%ss]", self.heartbeat_latency) - elif op == _GatewayOpcode.RECONNECT: + elif op == self._GatewayOpcode.RECONNECT: self.logger.debug("RECONNECT") - raise _Reconnect() + raise self._Reconnect() - elif op == _GatewayOpcode.INVALID_SESSION: + elif op == self._GatewayOpcode.INVALID_SESSION: self.logger.debug("INVALID SESSION [resume:%s]", data) - raise _InvalidSession(data) + raise self._InvalidSession(data) else: self.logger.debug("ignoring unrecognised opcode %s", op) @@ -583,23 +578,23 @@ async def _receive_json_payload(self) -> more_typing.JSONObject: close_code = self._ws.close_code self.logger.debug("connection closed with code %s", close_code) - if close_code in _GatewayCloseCode.__members__.values(): - reason = _GatewayCloseCode(close_code).name + if close_code in self._GatewayCloseCode.__members__.values(): + reason = self._GatewayCloseCode(close_code).name else: reason = f"unknown close code {close_code}" can_reconnect = close_code in ( - _GatewayCloseCode.DECODE_ERROR, - _GatewayCloseCode.INVALID_SEQ, - _GatewayCloseCode.UNKNOWN_ERROR, - _GatewayCloseCode.SESSION_TIMEOUT, - _GatewayCloseCode.RATE_LIMITED, + self._GatewayCloseCode.DECODE_ERROR, + self._GatewayCloseCode.INVALID_SEQ, + self._GatewayCloseCode.UNKNOWN_ERROR, + self._GatewayCloseCode.SESSION_TIMEOUT, + self._GatewayCloseCode.RATE_LIMITED, ) raise errors.GatewayServerClosedConnectionError(reason, close_code, can_reconnect, False, True) elif message.type == aiohttp.WSMsgType.CLOSING or message.type == aiohttp.WSMsgType.CLOSED: - raise _SocketClosed() + raise self._SocketClosed() else: # Assume exception for now. ex = self._ws.exception() diff --git a/hikari/net/iterators.py b/hikari/net/iterators.py index 663707279a..265e2afcba 100644 --- a/hikari/net/iterators.py +++ b/hikari/net/iterators.py @@ -28,12 +28,12 @@ from hikari.internal import conversions from hikari.internal import more_collections from hikari.internal import more_typing +from hikari.internal import unset from hikari.models import applications from hikari.models import audit_logs from hikari.models import bases from hikari.models import guilds from hikari.models import messages -from hikari.internal import unset from hikari.models import users from hikari.net import routes diff --git a/hikari/net/rest.py b/hikari/net/rest.py index 51fd6d3b7e..df74b59e9b 100644 --- a/hikari/net/rest.py +++ b/hikari/net/rest.py @@ -37,8 +37,6 @@ from hikari.internal import more_typing from hikari.internal import ratelimits from hikari.internal import unset -from hikari.models import audit_logs -from hikari.models import colors from hikari.net import buckets from hikari.net import http_client from hikari.net import iterators @@ -47,8 +45,10 @@ if typing.TYPE_CHECKING: from hikari.models import applications + from hikari.models import audit_logs from hikari.models import bases from hikari.models import channels + from hikari.models import colors from hikari.models import embeds as embeds_ from hikari.models import emojis from hikari.models import files @@ -62,11 +62,10 @@ from hikari.models import webhooks -class _RateLimited(RuntimeError): - __slots__ = () - - class REST(http_client.HTTPClient): + class _RateLimited(RuntimeError): + __slots__ = () + def __init__( self, *, @@ -140,7 +139,7 @@ async def _request( try: # Moved to a separate method to keep branch counts down. return await self._request_once(compiled_route=compiled_route, headers=headers, body=body, query=query) - except _RateLimited: + except self._RateLimited: pass async def _request_once( @@ -244,7 +243,7 @@ async def _handle_rate_limits_for_response( reset, ) - raise _RateLimited() + raise self._RateLimited() # We might find out Cloudflare causes this scenario to occur. # I hope we don't though. diff --git a/hikari/net/rest_utils.py b/hikari/net/rest_utils.py index 7416ee20ce..dd5eb22806 100644 --- a/hikari/net/rest_utils.py +++ b/hikari/net/rest_utils.py @@ -35,12 +35,12 @@ from hikari import rest_app from hikari.internal import conversions +from hikari.internal import unset from hikari.models import bases from hikari.models import colors from hikari.models import files from hikari.models import guilds from hikari.models import permissions as permissions_ -from hikari.internal import unset from hikari.net import routes if typing.TYPE_CHECKING: @@ -86,10 +86,6 @@ async def _keep_typing(self) -> None: await asyncio.gather(self, asyncio.sleep(9.9), return_exceptions=True) -class DummyID(int): - __slots__ = () - - @attr.s(auto_attribs=True, kw_only=True, slots=True) class GuildBuilder: _app: rest_app.IRESTApp From 541e95c262d7be3237f4ece7136cbc2f68c4c677 Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Tue, 26 May 2020 21:30:37 +0100 Subject: [PATCH 397/922] Started documenting REST endpoints. --- hikari/internal/unset.py | 1 + hikari/net/rest.py | 622 ++++++++++++++++++++++++++++++++++++--- 2 files changed, 578 insertions(+), 45 deletions(-) diff --git a/hikari/internal/unset.py b/hikari/internal/unset.py index f26289b55a..bcc1572421 100644 --- a/hikari/internal/unset.py +++ b/hikari/internal/unset.py @@ -72,4 +72,5 @@ def is_unset(obj): def count_unset_objects(obj1: typing.Any, obj2: typing.Any, *objs: typing.Any) -> int: + """Count the number of objects that are unset in the provided parameters.""" return sum(is_unset(o) for o in (obj1, obj2, *objs)) diff --git a/hikari/net/rest.py b/hikari/net/rest.py index df74b59e9b..9ddf81dd20 100644 --- a/hikari/net/rest.py +++ b/hikari/net/rest.py @@ -55,7 +55,7 @@ from hikari.models import gateway from hikari.models import guilds from hikari.models import invites - from hikari.models import messages + from hikari.models import messages as messages_ from hikari.models import permissions as permissions_ from hikari.models import users from hikari.models import voices @@ -63,6 +63,39 @@ class REST(http_client.HTTPClient): + """Implementation of the V6 and V7-compatible Discord REST API. + + This manages making HTTP/1.1 requests to the API and using the entity + factory within the passed application instance to deserialize JSON responses + to Pythonic data classes that are used throughout this library. + + Parameters + ---------- + app : hikari.rest_app.IRESTApp + The REST application containing all other application components + that Hikari uses. + config : hikari.http_settings.HTTPSettings + The AIOHTTP-specific configuration settings. This is used to configure + proxies, and specify TCP connectors to control the size of HTTP + connection pools, etc. + debug : bool + If `True`, this will enable logging of each payload sent and received, + as well as information such as DNS cache hits and misses, and other + information useful for debugging this application. These logs will + be written as DEBUG log entries. For most purposes, this should be + left `False`. + token : str + The bot or bearer token. If no token is to be used, this can be `None`. + token_type : str + The type of token in use. If no token is used, this can be ignored and + left to the default value. This can be `"Bot"` or `"Bearer"`. + url : str + The REST API base URL. This can contain format-string specifiers to + interpolate information such as API version in use. + version : int + The API version to use. + """ + class _RateLimited(RuntimeError): __slots__ = () @@ -311,20 +344,70 @@ def _build_message_creation_form( return form async def close(self) -> None: - """Close the REST client.""" + """Close the REST client and any open HTTP connections.""" await super().close() self.buckets.close() async def fetch_channel( - self, channel: typing.Union[channels.PartialChannel, bases.Snowflake, int], /, + self, channel: typing.Union[channels.PartialChannel, bases.UniqueObjectT], /, ) -> channels.PartialChannel: + """Fetch a channel. + + Parameters + ---------- + channel : hikari.models.channels.PartialChannel | hikari.models.bases.Snowflake | int | str + The channel object to fetch. This can be an existing reference to a + channel object (if you want a more up-to-date representation, or it + can be a snowflake representation of the channel ID. + + Returns + ------- + hikari.models.channels.PartialChannel + The resultant channel. + + Raises + ------ + hikari.errors.Unauthorized + If you are unauthorized to make the request (invalid/missing token). + hikari.errors.Forbidden + If you lack permissions to access the channel. + hikari.errors.NotFound + If the channel is not found. + hikari.errors.ServerHTTPErrorResponse + If an internal error occurs on Discord while handling the request. + """ route = routes.GET_CHANNEL.compile(channel=conversions.value_to_snowflake(channel)) response = await self._request(route) return self._app.entity_factory.deserialize_channel(response) + _GuildChannelT = typing.TypeVar("_GuildChannelT", bound=channels.GuildChannel, contravariant=True) + + # This overload just tells any static type checker that if we input, say, + # a GuildTextChannel, we should always expect a GuildTextChannel as the + # result. This only applies to actual Channel types... we cannot infer the + # result of calling this endpoint with a snowflake. + @typing.overload async def edit_channel( self, - channel: typing.Union[channels.PartialChannel, bases.Snowflake, int], + channel: _GuildChannelT, + /, + *, + name: typing.Union[unset.Unset, str] = unset.UNSET, + position: typing.Union[unset.Unset, int] = unset.UNSET, + topic: typing.Union[unset.Unset, str] = unset.UNSET, + nsfw: typing.Union[unset.Unset, bool] = unset.UNSET, + bitrate: typing.Union[unset.Unset, int] = unset.UNSET, + user_limit: typing.Union[unset.Unset, int] = unset.UNSET, + rate_limit_per_user: typing.Union[unset.Unset, more_typing.TimeSpanT] = unset.UNSET, + permission_overwrites: typing.Union[unset.Unset, typing.Sequence[channels.PermissionOverwrite]] = unset.UNSET, + parent_category: typing.Union[unset.Unset, channels.GuildCategory] = unset.UNSET, + reason: typing.Union[unset.Unset, str] = unset.UNSET, + ) -> _GuildChannelT: + """Edit a guild channel, given an existing guild channel object.""" + + async def edit_channel( + self, + channel: typing.Union[channels.PartialChannel, bases.UniqueObjectT], /, *, name: typing.Union[unset.Unset, str] = unset.UNSET, @@ -338,6 +421,38 @@ async def edit_channel( parent_category: typing.Union[unset.Unset, channels.GuildCategory] = unset.UNSET, reason: typing.Union[unset.Unset, str] = unset.UNSET, ) -> channels.PartialChannel: + """Edit a channel. + + Parameters + ---------- + channel + name + position + topic + nsfw + bitrate + user_limit + rate_limit_per_user + permission_overwrites + parent_category + reason + + Returns + ------- + + Raises + ------ + hikari.errors.BadRequest + If any of the fields that are passed have an invalid value. + hikari.errors.Unauthorized + If you are unauthorized to make the request (invalid/missing token). + hikari.errors.Forbidden + If you lack permissions to edit the channel + hikari.errors.NotFound + If the channel is not found. + hikari.errors.ServerHTTPErrorResponse + If an internal error occurs on Discord while handling the request. + """ route = routes.PATCH_CHANNEL.compile(channel=conversions.value_to_snowflake(channel)) payload = {} conversions.put_if_specified(payload, "name", name) @@ -354,10 +469,28 @@ async def edit_channel( self._app.entity_factory.serialize_permission_overwrite(p) for p in permission_overwrites ] - response = await self._request(route, body=payload, reason=reason,) + response = await self._request(route, body=payload, reason=reason) return self._app.entity_factory.deserialize_channel(response) - async def delete_channel(self, channel: typing.Union[channels.PartialChannel, bases.Snowflake, int], /) -> None: + async def delete_channel(self, channel: typing.Union[channels.PartialChannel, bases.UniqueObjectT], /) -> None: + """Delete a channel in a guild, or close a DM. + + Parameters + ---------- + channel + + Raises + ------ + hikari.errors.Unauthorized + If you are unauthorized to make the request (invalid/missing token). + hikari.errors.Forbidden + If you lack permissions to delete the channel in a guild. + hikari.errors.NotFound + If the channel is not found. + hikari.errors.ServerHTTPErrorResponse + If an internal error occurs on Discord while handling the request. + + """ route = routes.DELETE_CHANNEL.compile(channel=conversions.value_to_snowflake(channel)) await self._request(route) @@ -371,7 +504,7 @@ async def edit_permission_overwrites( deny: typing.Union[unset.Unset, permissions_.Permission] = unset.UNSET, reason: typing.Union[unset.Unset, str] = unset.UNSET, ) -> None: - ... + """Edit permissions for a target entity.""" @typing.overload async def edit_permission_overwrites( @@ -384,7 +517,7 @@ async def edit_permission_overwrites( deny: typing.Union[unset.Unset, permissions_.Permission] = unset.UNSET, reason: typing.Union[unset.Unset, str] = unset.UNSET, ) -> None: - ... + """Edit permissions for a given entity ID and type.""" async def edit_permission_overwrites( self, @@ -396,6 +529,32 @@ async def edit_permission_overwrites( deny: typing.Union[unset.Unset, permissions_.Permission] = unset.UNSET, reason: typing.Union[unset.Unset, str] = unset.UNSET, ) -> None: + """Edit permissions for a specific entity in the given guild channel. + + Parameters + ---------- + channel + target + target_type + allow + deny + reason + + Raises + ------ + hikari.errors.BadRequest + If any of the fields that are passed have an invalid value. + hikari.errors.Unauthorized + If you are unauthorized to make the request (invalid/missing token). + hikari.errors.Forbidden + If you lack permissions to edit the permission overwrites. + hikari.errors.NotFound + If the channel is not found or the target is not found if it is + a role. + hikari.errors.ServerHTTPErrorResponse + If an internal error occurs on Discord while handling the request. + """ + if unset.is_unset(target_type): if isinstance(target, users.User): target_type = channels.PermissionOverwriteType.MEMBER @@ -422,6 +581,24 @@ async def delete_permission_overwrite( channel: typing.Union[channels.GuildChannel, bases.UniqueObjectT], target: typing.Union[channels.PermissionOverwrite, guilds.Role, users.User, bases.UniqueObjectT], ) -> None: + """Delete a custom permission for an entity in a given guild channel. + + Parameters + ---------- + channel + target + + Raises + ------ + hikari.errors.Unauthorized + If you are unauthorized to make the request (invalid/missing token). + hikari.errors.Forbidden + If you lack permissions to delete the permission overwrite. + hikari.errors.NotFound + If the channel is not found or the target is not found. + hikari.errors.ServerHTTPErrorResponse + If an internal error occurs on Discord while handling the request. + """ route = routes.DELETE_CHANNEL_PERMISSIONS.compile( channel=conversions.value_to_snowflake(channel), overwrite=conversions.value_to_snowflake(target), ) @@ -430,6 +607,26 @@ async def delete_permission_overwrite( async def fetch_channel_invites( self, channel: typing.Union[channels.GuildChannel, bases.UniqueObjectT], / ) -> typing.Sequence[invites.InviteWithMetadata]: + """Fetch all invites pointing to the given guild channel. + + Parameters + ---------- + channel + + Returns + ------- + + Raises + ------ + hikari.errors.Unauthorized + If you are unauthorized to make the request (invalid/missing token). + hikari.errors.Forbidden + If you lack permissions to view the invites for the given channel. + hikari.errors.NotFound + If the channel is not found in any guilds you are a member of. + hikari.errors.ServerHTTPErrorResponse + If an internal error occurs on Discord while handling the request. + """ route = routes.GET_CHANNEL_INVITES.compile(channel=conversions.value_to_snowflake(channel)) response = await self._request(route) return conversions.json_to_collection(response, self._app.entity_factory.deserialize_invite_with_metadata) @@ -447,6 +644,36 @@ async def create_invite( target_user_type: typing.Union[unset.Unset, invites.TargetUserType] = unset.UNSET, reason: typing.Union[unset.Unset, str] = unset.UNSET, ) -> invites.InviteWithMetadata: + """Create an invite to the given guild channel. + + Parameters + ---------- + channel + max_age + max_uses + temporary + unique + target_user + target_user_type + reason + + Returns + ------- + + Raises + ------ + hikari.errors.BadRequest + If any of the fields that are passed have an invalid value. + hikari.errors.Unauthorized + If you are unauthorized to make the request (invalid/missing token). + hikari.errors.Forbidden + If you lack permissions to create the given channel. + hikari.errors.NotFound + If the channel is not found, or if the target user does not exist, + if specified. + hikari.errors.ServerHTTPErrorResponse + If an internal error occurs on Discord while handling the request. + """ payload = {} conversions.put_if_specified(payload, "max_age", max_age, conversions.timespan_to_int) conversions.put_if_specified(payload, "max_uses", max_uses) @@ -461,11 +688,58 @@ async def create_invite( def trigger_typing( self, channel: typing.Union[channels.TextChannel, bases.UniqueObjectT], / ) -> rest_utils.TypingIndicator: + """Trigger typing in a text channel. + + Parameters + ---------- + channel + + Returns + ------- + + Raises + ------ + hikari.errors.Unauthorized + If you are unauthorized to make the request (invalid/missing token). + hikari.errors.Forbidden + If you lack permissions to read messages or send messages in the + text channel. + hikari.errors.NotFound + If the channel is not found. + hikari.errors.ServerHTTPErrorResponse + If an internal error occurs on Discord while handling the request. + + !!! note + The exceptions on this endpoint will only be raised once the result + is awaited or interacted with. Invoking this function itself will + not raise any of the above types. + """ return rest_utils.TypingIndicator(channel, self._request) async def fetch_pins( self, channel: typing.Union[channels.TextChannel, bases.UniqueObjectT], / - ) -> typing.Sequence[messages.Message]: + ) -> typing.Sequence[messages_.Message]: + """Fetch the pinned messages in this text channel. + + Parameters + ---------- + channel + + Returns + ------- + + Raises + ------ + hikari.errors.Unauthorized + If you are unauthorized to make the request (invalid/missing token). + hikari.errors.Forbidden + If you lack permissions to read messages or send messages in the + text channel. + hikari.errors.NotFound + If the channel is not found. + hikari.errors.ServerHTTPErrorResponse + If an internal error occurs on Discord while handling the request. + """ route = routes.GET_CHANNEL_PINS.compile(channel=conversions.value_to_snowflake(channel)) response = await self._request(route) return conversions.json_to_collection(response, self._app.entity_factory.deserialize_message) @@ -473,8 +747,27 @@ async def fetch_pins( async def pin_message( self, channel: typing.Union[channels.TextChannel, bases.UniqueObjectT], - message: typing.Union[messages.Message, bases.UniqueObjectT], + message: typing.Union[messages_.Message, bases.UniqueObjectT], ) -> None: + """Pin an existing message in the given text channel. + + Parameters + ---------- + channel + message + + Raises + ------ + hikari.errors.Unauthorized + If you are unauthorized to make the request (invalid/missing token). + hikari.errors.Forbidden + If you lack permissions to pin messages in the given channel. + hikari.errors.NotFound + If the channel is not found, or if the message does not exist in + the given channel. + hikari.errors.ServerHTTPErrorResponse + If an internal error occurs on Discord while handling the request. + """ route = routes.PUT_CHANNEL_PINS.compile( channel=conversions.value_to_snowflake(channel), message=conversions.value_to_snowflake(message), ) @@ -483,8 +776,32 @@ async def pin_message( async def unpin_message( self, channel: typing.Union[channels.TextChannel, bases.UniqueObjectT], - message: typing.Union[messages.Message, bases.UniqueObjectT], + message: typing.Union[messages_.Message, bases.UniqueObjectT], ) -> None: + """ + + Parameters + ---------- + channel + message + + Raises + ------ + hikari.errors.Unauthorized + If you are unauthorized to make the request (invalid/missing token). + hikari.errors.Forbidden + If you lack permissions pin messages in the given channel. + hikari.errors.NotFound + If the channel is not found or the message is not a pinned message + in the given channel. + hikari.errors.ServerHTTPErrorResponse + If an internal error occurs on Discord while handling the request. + + !!! note + The exceptions on this endpoint will only be raised once the result + is awaited or interacted with. Invoking this function itself will + not raise any of the above types. + """ route = routes.DELETE_CHANNEL_PIN.compile( channel=conversions.value_to_snowflake(channel), message=conversions.value_to_snowflake(message), ) @@ -493,8 +810,8 @@ async def unpin_message( @typing.overload def fetch_messages( self, channel: typing.Union[channels.TextChannel, bases.UniqueObjectT], / - ) -> iterators.LazyIterator[messages.Message]: - ... + ) -> iterators.LazyIterator[messages_.Message]: + """Fetch messages, newest first, sent in the given channel.""" @typing.overload def fetch_messages( @@ -503,8 +820,8 @@ def fetch_messages( /, *, before: typing.Union[datetime.datetime, bases.UniqueObjectT], - ) -> iterators.LazyIterator[messages.Message]: - ... + ) -> iterators.LazyIterator[messages_.Message]: + """Fetch messages, newest first, sent before a timestamp in the channel.""" @typing.overload def fetch_messages( @@ -513,8 +830,8 @@ def fetch_messages( /, *, around: typing.Union[datetime.datetime, bases.UniqueObjectT], - ) -> iterators.LazyIterator[messages.Message]: - ... + ) -> iterators.LazyIterator[messages_.Message]: + """Fetch messages sent around a given time in the channel.""" @typing.overload def fetch_messages( @@ -523,21 +840,60 @@ def fetch_messages( /, *, after: typing.Union[datetime.datetime, bases.UniqueObjectT], - ) -> iterators.LazyIterator[messages.Message]: - ... + ) -> iterators.LazyIterator[messages_.Message]: + """Fetch messages, oldest first, sent after a timestamp in the channel.""" def fetch_messages( self, channel: typing.Union[channels.TextChannel, bases.UniqueObjectT], /, - **kwargs: typing.Optional[typing.Union[datetime.datetime, bases.UniqueObjectT]], - ) -> iterators.LazyIterator[messages.Message]: - if len(kwargs) == 1 and any(direction in kwargs for direction in ("before", "after", "around")): - direction, timestamp = kwargs.popitem() - elif not kwargs: - direction, timestamp = "before", bases.Snowflake.max() + *, + before: typing.Union[unset.Unset, datetime.datetime, bases.UniqueObjectT] = unset.UNSET, + after: typing.Union[unset.Unset, datetime.datetime, bases.UniqueObjectT] = unset.UNSET, + around: typing.Union[unset.Unset, datetime.datetime, bases.UniqueObjectT] = unset.UNSET, + ) -> iterators.LazyIterator[messages_.Message]: + """Browse the message history for a given text channel. + + Parameters + ---------- + channel + before + after + around + + Returns + ------- + + Raises + ------ + hikari.errors.Unauthorized + If you are unauthorized to make the request (invalid/missing token). + hikari.errors.Forbidden + If you lack permissions to read message history in the given + channel. + hikari.errors.NotFound + If the channel is not found. + hikari.errors.ServerHTTPErrorResponse + If an internal error occurs on Discord while handling the request. + TypeError + If you specify more than one of `before`, `after`, `about`. + + !!! note + The exceptions on this endpoint (other than `TypeError`) will only + be raised once the result is awaited or interacted with. Invoking + this function itself will not raise anything (other than + `TypeError`). + """ + if unset.count_unset_objects(before, after, around) < 2: + raise TypeError(f"Expected no kwargs, or maximum of one of 'before', 'after', 'around'") + elif not unset.is_unset(before): + direction, timestamp = "before", before + elif not unset.is_unset(after): + direction, timestamp = "after", after + elif not unset.is_unset(around): + direction, timestamp = "around", around else: - raise TypeError(f"Expected no kwargs, or one of 'before', 'after', 'around', received: {kwargs}") + direction, timestamp = "before", bases.Snowflake.max() if isinstance(timestamp, datetime.datetime): timestamp = bases.Snowflake.from_datetime(timestamp) @@ -553,8 +909,31 @@ def fetch_messages( async def fetch_message( self, channel: typing.Union[channels.TextChannel, bases.UniqueObjectT], - message: typing.Union[messages.Message, bases.UniqueObjectT], - ) -> messages.Message: + message: typing.Union[messages_.Message, bases.UniqueObjectT], + ) -> messages_.Message: + """Fetch a specific message in the given text channel. + + Parameters + ---------- + channel + message + + Returns + ------- + + Raises + ------ + hikari.errors.Unauthorized + If you are unauthorized to make the request (invalid/missing token). + hikari.errors.Forbidden + If you lack permissions to read message history in the given + channel. + hikari.errors.NotFound + If the channel is not found or the message is not found in the + given text channel. + hikari.errors.ServerHTTPErrorResponse + If an internal error occurs on Discord while handling the request. + """ route = routes.GET_CHANNEL_MESSAGE.compile( channel=conversions.value_to_snowflake(channel), message=conversions.value_to_snowflake(message), ) @@ -573,7 +952,48 @@ async def create_message( mentions_everyone: bool = False, user_mentions: typing.Union[typing.Collection[typing.Union[users.User, bases.UniqueObjectT]], bool] = True, role_mentions: typing.Union[typing.Collection[typing.Union[bases.UniqueObjectT, guilds.Role]], bool] = True, - ) -> messages.Message: + ) -> messages_.Message: + """Create a message in the given channel. + + Parameters + ---------- + channel + text + embed + attachments + tts + nonce + mentions_everyone + user_mentions + role_mentions + + Returns + ------- + + Raises + ------ + hikari.errors.BadRequest + This may be raised in several discrete situations, such as messages + being empty with no attachments or embeds; messages with more than + 2000 characters in them, embeds that exceed one of the many embed + limits; too many attachments; attachments that are too large; + invalid image URLs in embeds; users in `user_mentions` not being + mentioned in the message content; roles in `role_mentions` not + being mentioned in the message content. + hikari.errors.Unauthorized + If you are unauthorized to make the request (invalid/missing token). + hikari.errors.Forbidden + If you lack permissions to send messages in the given channel. + hikari.errors.NotFound + If the channel is not found. + hikari.errors.ServerHTTPErrorResponse + If an internal error occurs on Discord while handling the request. + + !!! warn + You are expected to make a connection to the gateway and identify + once before being able to use this endpoint for a bot. + """ + route = routes.POST_CHANNEL_MESSAGES.compile(channel=conversions.value_to_snowflake(channel)) payload = {"allowed_mentions": self._generate_allowed_mentions(mentions_everyone, user_mentions, role_mentions)} @@ -596,15 +1016,45 @@ async def create_message( async def edit_message( self, channel: typing.Union[channels.TextChannel, bases.UniqueObjectT], - message: typing.Union[messages.Message, bases.UniqueObjectT], + message: typing.Union[messages_.Message, bases.UniqueObjectT], text: typing.Union[unset.Unset, typing.Any] = unset.UNSET, *, embed: typing.Union[unset.Unset, embeds_.Embed] = unset.UNSET, mentions_everyone: bool = False, user_mentions: typing.Union[typing.Collection[typing.Union[users.User, bases.UniqueObjectT]], bool] = True, role_mentions: typing.Union[typing.Collection[typing.Union[bases.UniqueObjectT, guilds.Role]], bool] = True, - flags: typing.Union[unset.Unset, messages.MessageFlag] = unset.UNSET, - ) -> messages.Message: + flags: typing.Union[unset.Unset, messages_.MessageFlag] = unset.UNSET, + ) -> messages_.Message: + """Edit an existing message in a given channel. + + Parameters + ---------- + + Returns + ------- + + Raises + ------ + hikari.errors.BadRequest + This may be raised in several discrete situations, such as messages + being empty with no embeds; messages with more than 2000 characters + in them, embeds that exceed one of the many embed + limits; invalid image URLs in embeds; users in `user_mentions` not + being mentioned in the message content; roles in `role_mentions` not + being mentioned in the message content. + hikari.errors.Unauthorized + If you are unauthorized to make the request (invalid/missing token). + hikari.errors.Forbidden + If you lack permissions to send messages in the given channel; if + you try to change the contents of another user's message; or if you + try to edit the flags on another user's message without the + permissions to manage messages_. + hikari.errors.NotFound + If the channel or message is not found. + hikari.errors.ServerHTTPErrorResponse + If an internal error occurs on Discord while handling the request. + """ + route = routes.PATCH_CHANNEL_MESSAGE.compile( channel=conversions.value_to_snowflake(channel), message=conversions.value_to_snowflake(message), ) @@ -619,8 +1069,27 @@ async def edit_message( async def delete_message( self, channel: typing.Union[channels.TextChannel, bases.UniqueObjectT], - message: typing.Union[messages.Message, bases.UniqueObjectT], + message: typing.Union[messages_.Message, bases.UniqueObjectT], ) -> None: + """Delete a given message in a given channel. + + Parameters + ---------- + channel + message + + Raises + ------ + hikari.errors.Unauthorized + If you are unauthorized to make the request (invalid/missing token). + hikari.errors.Forbidden + If you lack the permissions to manage messages, and the message is + not composed by your associated user. + hikari.errors.NotFound + If the channel or message is not found. + hikari.errors.ServerHTTPErrorResponse + If an internal error occurs on Discord while handling the request. + """ route = routes.DELETE_CHANNEL_MESSAGE.compile( channel=conversions.value_to_snowflake(channel), message=conversions.value_to_snowflake(message), ) @@ -628,22 +1097,65 @@ async def delete_message( async def delete_messages( self, - channel: typing.Union[channels.TextChannel, bases.UniqueObjectT], - *messages_to_delete: typing.Union[messages.Message, bases.UniqueObjectT], + channel: typing.Union[channels.GuildTextChannel, bases.UniqueObjectT], + *messages: typing.Union[messages_.Message, bases.UniqueObjectT], ) -> None: - if 2 <= len(messages_to_delete) <= 100: + """Bulk-delete between 2 and 100 messages from the given guild channel. + + Parameters + ---------- + channel + *messages + + Raises + ------ + hikari.errors.Unauthorized + If you are unauthorized to make the request (invalid/missing token). + hikari.errors.Forbidden + If you lack the permissions to manage messages, and the message is + not composed by your associated user. + hikari.errors.NotFound + If the channel or message is not found. + hikari.errors.ServerHTTPErrorResponse + If an internal error occurs on Discord while handling the request. + TypeError + If you do not provide between 2 and 100 messages (inclusive). + """ + if 2 <= len(messages) <= 100: route = routes.POST_DELETE_CHANNEL_MESSAGES_BULK.compile(channel=conversions.value_to_snowflake(channel)) - payload = {"messages": [conversions.value_to_snowflake(m) for m in messages_to_delete]} + payload = {"messages": [conversions.value_to_snowflake(m) for m in messages]} await self._request(route, body=payload) else: raise TypeError("Must delete a minimum of 2 messages and a maximum of 100") - async def create_reaction( + async def add_reaction( self, channel: typing.Union[channels.TextChannel, bases.UniqueObjectT], - message: typing.Union[messages.Message, bases.UniqueObjectT], + message: typing.Union[messages_.Message, bases.UniqueObjectT], emoji: typing.Union[str, emojis.UnicodeEmoji, emojis.KnownCustomEmoji], ) -> None: + """Add a reaction emoji to a message in a given channel. + + Parameters + ---------- + channel + message + emoji + + Raises + ------ + hikari.errors.BadRequest + If an invalid unicode emoji is given, or if the given custom emoji + does not exist. + hikari.errors.Unauthorized + If you are unauthorized to make the request (invalid/missing token). + hikari.errors.Forbidden + If you lack permissions to add reactions to messages. + hikari.errors.NotFound + If the channel or message is not found. + hikari.errors.ServerHTTPErrorResponse + If an internal error occurs on Discord while handling the request. + """ route = routes.PUT_MY_REACTION.compile( emoji=emoji.url_name if isinstance(emoji, emojis.KnownCustomEmoji) else str(emoji), channel=conversions.value_to_snowflake(channel), @@ -654,9 +1166,29 @@ async def create_reaction( async def delete_my_reaction( self, channel: typing.Union[channels.TextChannel, bases.UniqueObjectT], - message: typing.Union[messages.Message, bases.UniqueObjectT], + message: typing.Union[messages_.Message, bases.UniqueObjectT], emoji: typing.Union[str, emojis.UnicodeEmoji, emojis.KnownCustomEmoji], ) -> None: + """Delete a reaction that your application user created. + + Parameters + ---------- + channel + message + emoji + + Raises + ------ + hikari.errors.BadRequest + If an invalid unicode emoji is given, or if the given custom emoji + does not exist. + hikari.errors.Unauthorized + If you are unauthorized to make the request (invalid/missing token). + hikari.errors.NotFound + If the channel or message is not found. + hikari.errors.ServerHTTPErrorResponse + If an internal error occurs on Discord while handling the request. + """ route = routes.DELETE_MY_REACTION.compile( emoji=emoji.url_name if isinstance(emoji, emojis.KnownCustomEmoji) else str(emoji), channel=conversions.value_to_snowflake(channel), @@ -667,7 +1199,7 @@ async def delete_my_reaction( async def delete_all_reactions_for_emoji( self, channel: typing.Union[channels.TextChannel, bases.UniqueObjectT], - message: typing.Union[messages.Message, bases.UniqueObjectT], + message: typing.Union[messages_.Message, bases.UniqueObjectT], emoji: typing.Union[str, emojis.UnicodeEmoji, emojis.KnownCustomEmoji], ) -> None: route = routes.DELETE_REACTION_EMOJI.compile( @@ -680,7 +1212,7 @@ async def delete_all_reactions_for_emoji( async def delete_reaction( self, channel: typing.Union[channels.TextChannel, bases.UniqueObjectT], - message: typing.Union[messages.Message, bases.UniqueObjectT], + message: typing.Union[messages_.Message, bases.UniqueObjectT], emoji: typing.Union[str, emojis.UnicodeEmoji, emojis.KnownCustomEmoji], user: typing.Union[users.User, bases.UniqueObjectT], ) -> None: @@ -695,7 +1227,7 @@ async def delete_reaction( async def delete_all_reactions( self, channel: typing.Union[channels.TextChannel, bases.UniqueObjectT], - message: typing.Union[messages.Message, bases.UniqueObjectT], + message: typing.Union[messages_.Message, bases.UniqueObjectT], ) -> None: route = routes.DELETE_ALL_REACTIONS.compile( channel=conversions.value_to_snowflake(channel), message=conversions.value_to_snowflake(message), @@ -706,7 +1238,7 @@ async def delete_all_reactions( def fetch_reactions_for_emoji( self, channel: typing.Union[channels.TextChannel, bases.UniqueObjectT], - message: typing.Union[messages.Message, bases.UniqueObjectT], + message: typing.Union[messages_.Message, bases.UniqueObjectT], emoji: typing.Union[str, emojis.UnicodeEmoji, emojis.KnownCustomEmoji], ) -> iterators.LazyIterator[users.User]: return iterators.ReactorIterator( @@ -817,7 +1349,7 @@ async def execute_embed( mentions_everyone: bool = False, user_mentions: typing.Union[typing.Collection[typing.Union[users.User, bases.UniqueObjectT]], bool] = True, role_mentions: typing.Union[typing.Collection[typing.Union[bases.UniqueObjectT, guilds.Role]], bool] = True, - ) -> messages.Message: + ) -> messages_.Message: if unset.is_unset(token): route = routes.POST_WEBHOOK.compile(webhook=conversions.value_to_snowflake(webhook)) no_auth = False From c860ce01fb4aefe9db01723fe3fb6bcdc9e6e43a Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Tue, 26 May 2020 21:33:12 +0100 Subject: [PATCH 398/922] Bump nox, virtualenv, pytest-cov versions --- dev-requirements.txt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/dev-requirements.txt b/dev-requirements.txt index da282f832d..77369fae13 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -1,15 +1,15 @@ async-timeout~=3.0.1 coverage~=5.1 -nox==2019.11.9 +nox==2020.5.24 # Virtualenv 20.0.19 breaks nox randomnly... -virtualenv==20.0.18 +virtualenv==20.0.21 # Pylint 2.5.0 is broken pylint==2.5.2 pylint-json2html-v2~=0.2.2 pylint-junit~=0.2.0 pytest~=5.4.2 pytest-asyncio~=0.12.0 -pytest-cov~=2.8.1 +pytest-cov~=2.9.0 pytest-randomly~=3.3.1 pytest-testdox~=1.2.1 pytest-xdist~=1.32.0 From 77d593cc9be851a65db57d5b4256078681708b89 Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Wed, 27 May 2020 09:58:42 +0100 Subject: [PATCH 399/922] Added nox script to allow running nox inside a container of a specific python version. --- ci/config.py | 9 ++++++ ci/deploy.nox.py | 39 ++++++++++------------- ci/docker.nox.py | 79 ++++++++++++++++++++++++++++++++++++++++++++++ ci/nox.py | 7 ++++ docker-compose.yml | 9 ------ 5 files changed, 111 insertions(+), 32 deletions(-) create mode 100644 ci/docker.nox.py delete mode 100644 docker-compose.yml diff --git a/ci/config.py b/ci/config.py index 56d8558a2f..66c2ed18e1 100644 --- a/ci/config.py +++ b/ci/config.py @@ -16,6 +16,7 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . +import os import os as _os IS_CI = "CI" in _os.environ @@ -68,3 +69,11 @@ PYPI_REPO = "https://upload.pypi.org/legacy/" PYPI = "https://pypi.org/" PYPI_API = f"{PYPI}/pypi/{API_NAME}/json" + +# Docker stuff +DOCKER_ENVS = [ + "python:3.8.0", + "python:3.8.1", + "python:3.8.2", + "python:3.9-rc", +] diff --git a/ci/deploy.nox.py b/ci/deploy.nox.py index df11bdbfad..9e0375a172 100644 --- a/ci/deploy.nox.py +++ b/ci/deploy.nox.py @@ -21,7 +21,6 @@ import os import re import shlex -import subprocess from distutils.version import LooseVersion @@ -29,15 +28,9 @@ from ci import nox -def shell(arg, *args): - command = " ".join((arg, *args)) - print("\033[35mnox > shell >\033[0m", command) - return subprocess.check_call(command, shell=True) - - def update_version_string(version): print("Updating version in version file to", version) - shell("sed", shlex.quote(f's|^__version__.*|__version__ = "{version}"|g'), "-i", config.VERSION_FILE) + nox.shell("sed", shlex.quote(f's|^__version__.*|__version__ = "{version}"|g'), "-i", config.VERSION_FILE) def increment_prod_to_next_dev(version): @@ -101,38 +94,38 @@ def get_next_dev_version(version): def deploy_to_pypi() -> None: print("Performing PyPI deployment of current code") - shell("pip install -r requirements.txt twine") - shell("python", "setup.py", *config.DISTS) + nox.shell("pip install -r requirements.txt twine") + nox.shell("python", "setup.py", *config.DISTS) os.putenv("TWINE_USERNAME", os.environ["PYPI_USER"]) os.putenv("TWINE_PASSWORD", os.environ["PYPI_PASS"]) os.putenv("TWINE_REPOSITORY_URL", config.PYPI_REPO) dists = [os.path.join("dist", n) for n in os.listdir("dist")] - shell("twine", "upload", "--disable-progress-bar", "--skip-existing", *dists) + nox.shell("twine", "upload", "--disable-progress-bar", "--skip-existing", *dists) def deploy_to_git(next_version: str) -> None: print("Setting up the git repository ready to make automated changes") - shell("git config user.name", shlex.quote(config.CI_ROBOT_NAME)) - shell("git config user.email", shlex.quote(config.CI_ROBOT_EMAIL)) - shell( + nox.shell("git config user.name", shlex.quote(config.CI_ROBOT_NAME)) + nox.shell("git config user.email", shlex.quote(config.CI_ROBOT_EMAIL)) + nox.shell( "git remote set-url", config.REMOTE_NAME, "$(echo \"$CI_REPOSITORY_URL\" | perl -pe 's#.*@(.+?(\\:\\d+)?)/#git@\\1:#')", ) print("Making deployment commit") - shell( + nox.shell( "git commit -am", shlex.quote(f"(ci) Deployed {next_version} to PyPI {config.SKIP_CI_PHRASE}"), "--allow-empty", ) print("Tagging release") - shell("git tag", next_version) + nox.shell("git tag", next_version) print("Merging prod back into preprod") - shell("git checkout", config.PREPROD_BRANCH) - shell(f"git reset --hard {config.REMOTE_NAME}/{config.PREPROD_BRANCH}") + nox.shell("git checkout", config.PREPROD_BRANCH) + nox.shell(f"git reset --hard {config.REMOTE_NAME}/{config.PREPROD_BRANCH}") - shell( + nox.shell( f"git merge {config.PROD_BRANCH}", "--no-ff --strategy-option theirs --allow-unrelated-histories -m", shlex.quote(f"(ci) Merged {config.PROD_BRANCH} {next_version} into {config.PREPROD_BRANCH}"), @@ -140,15 +133,15 @@ def deploy_to_git(next_version: str) -> None: update_version_string(increment_prod_to_next_dev(next_version)) print("Making next dev commit on preprod") - shell( + nox.shell( "git commit -am", shlex.quote(f"(ci) Updated version for next development release {config.SKIP_DEPLOY_PHRASE}") ) - shell("git push --atomic", config.REMOTE_NAME, config.PREPROD_BRANCH, config.PROD_BRANCH, next_version) + nox.shell("git push --atomic", config.REMOTE_NAME, config.PREPROD_BRANCH, config.PROD_BRANCH, next_version) def send_notification(version: str, title: str, description: str, color: str) -> None: print("Sending webhook to Discord") - shell( + nox.shell( "curl", "-X POST", "-H", @@ -177,7 +170,7 @@ def send_notification(version: str, title: str, description: str, color: str) -> @nox.session() def deploy(session: nox.Session) -> None: """Perform a deployment. This will only work on the CI.""" - shell("pip install requests") + nox.shell("pip install requests") commit_ref = os.getenv("CI_COMMIT_REF_NAME", *session.posargs[0:1]) print("Commit ref is", commit_ref) current_version = get_current_version() diff --git a/ci/docker.nox.py b/ci/docker.nox.py new file mode 100644 index 0000000000..d8c84f35de --- /dev/null +++ b/ci/docker.nox.py @@ -0,0 +1,79 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# Copyright © Nekoka.tt 2019-2020 +# +# This file is part of Hikari. +# +# Hikari is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Hikari is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Hikari. If not, see . +"""Allows running CI scripts within a Docker container.""" +import os +import random +import shlex +import shutil + +from nox import options + +from ci import config +from ci import nox + + +if shutil.which("docker"): + @nox.session(reuse_venv=True) + def docker(session: nox.Session) -> None: + """Run a nox session in a container that targets a specific Python version. + + This will invoke nox with the given additional arguments. + """ + + try: + args = ["--help"] if not session.posargs else session.posargs + python, *args = args + args = shlex.join(args) + + if python not in config.DOCKER_ENVS: + print(f"\033[31m\033[1mNo environment called {python} found.\033[0m") + raise IndexError + except IndexError: + env_example = random.choice(config.DOCKER_ENVS) + command_example = random.choice(options.sessions) + print( + "USAGE: nox -s docker -- {env} [{arg}, ...]", + "", + docker.__doc__, + f"For example: 'nox -s docker -- {env_example} -s {command_example}'", + "", + "Parameters:", + " {env} The environment to build. Supported environments are:", + *(f" - {e}" for e in config.DOCKER_ENVS), + " {arg} Argument to pass to nox within the container.", + "", + "Supported sessions include:", + *(f" - {s}" for s in options.sessions if s != "docker"), + sep="\n", + ) + return + + print("\033[33m<<<<<<<<<<<<<<<<<<< ENTERING CONTAINER >>>>>>>>>>>>>>>>>>>\033[0m") + print(f"> will run 'nox {args}' in container using '{python}' image.") + nox.shell( + "docker", + "run", + "--mount", + f"type=bind,source={os.path.abspath(os.getcwd())!r},target=/hikari", + "--rm", + "-it", + python, + f"/bin/sh -c 'cd hikari && pip install nox && nox {args}'" + ) + print("\033[33m<<<<<<<<<<<<<<<<<<< EXITING CONTAINER >>>>>>>>>>>>>>>>>>>\033[0m") diff --git a/ci/nox.py b/ci/nox.py index 305e560c59..a4058c48f0 100644 --- a/ci/nox.py +++ b/ci/nox.py @@ -19,6 +19,7 @@ """Wrapper around nox to give default job kwargs.""" import functools import os +import subprocess from typing import Callable from nox.sessions import Session @@ -46,3 +47,9 @@ def logic(session): session.env[n] = v return func(session) return logic + + +def shell(arg, *args): + command = " ".join((arg, *args)) + print("\033[35mnox > shell >\033[0m", command) + return subprocess.check_call(command, shell=True) diff --git a/docker-compose.yml b/docker-compose.yml deleted file mode 100644 index 4182126c52..0000000000 --- a/docker-compose.yml +++ /dev/null @@ -1,9 +0,0 @@ -version: "3" - -services: - test-client: - entrypoint: bash -c 'pip install --pre hikari && python -m hikari.clients.test --shards=2' - env_file: - - credentials.env - image: python:3.8 - restart: always From 8c61623b8c86d4fd83effcd653accb16bfbc7786 Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Wed, 27 May 2020 13:32:03 +0100 Subject: [PATCH 400/922] Tidied up tests, moved 'get_logger'to meta script. --- hikari/impl/bot.py | 4 +- hikari/impl/rest_app.py | 3 +- hikari/internal/{meta.py => class_helpers.py} | 61 ++++------ hikari/internal/helpers.py | 108 ----------------- hikari/internal/more_asyncio.py | 3 +- hikari/internal/unset.py | 4 +- hikari/net/gateway.py | 7 +- hikari/net/http_client.py | 6 +- hikari/net/rest.py | 6 +- hikari/net/user_agents.py | 4 +- tests/hikari/internal/test_class_helpers.py | 52 +++++++++ tests/hikari/internal/test_conversions.py | 56 +-------- tests/hikari/internal/test_helpers.py | 110 ------------------ tests/hikari/internal/test_meta.py | 81 ------------- tests/hikari/models/test_applications.py | 4 +- tests/hikari/net/__init__.py | 18 +++ .../{internal => net}/test_http_client.py | 0 17 files changed, 118 insertions(+), 409 deletions(-) rename hikari/internal/{meta.py => class_helpers.py} (67%) delete mode 100644 hikari/internal/helpers.py create mode 100644 tests/hikari/internal/test_class_helpers.py delete mode 100644 tests/hikari/internal/test_helpers.py delete mode 100644 tests/hikari/internal/test_meta.py create mode 100644 tests/hikari/net/__init__.py rename tests/hikari/{internal => net}/test_http_client.py (100%) diff --git a/hikari/impl/bot.py b/hikari/impl/bot.py index 5144f48345..fde56ad1b6 100644 --- a/hikari/impl/bot.py +++ b/hikari/impl/bot.py @@ -28,7 +28,7 @@ from hikari.impl import entity_factory as entity_factory_impl from hikari.impl import event_manager from hikari.impl import gateway_zookeeper -from hikari.internal import helpers +from hikari.internal import class_helpers from hikari.models import guilds from hikari.net import gateway from hikari.net import rest @@ -67,7 +67,7 @@ def __init__( token: str, use_compression: bool = True, ): - self._logger = helpers.get_logger(self) + self._logger = class_helpers.get_logger(self) self._cache = cache_impl.CacheImpl() self._event_manager = event_manager.EventManagerImpl() diff --git a/hikari/impl/rest_app.py b/hikari/impl/rest_app.py index d244c8c0ff..59c15e5218 100644 --- a/hikari/impl/rest_app.py +++ b/hikari/impl/rest_app.py @@ -27,6 +27,7 @@ from hikari import rest_app from hikari.impl import cache as cache_impl from hikari.impl import entity_factory as entity_factory_impl +from hikari.internal import class_helpers from hikari.internal import helpers from hikari.net import rest as rest_ from hikari.net import urls @@ -46,7 +47,7 @@ def __init__( rest_url: str = urls.REST_API_URL, version: int = 6, ) -> None: - self._logger = helpers.get_logger(self) + self._logger = class_helpers.get_logger(self) self._rest = rest_.REST( app=self, config=config, debug=debug, token=token, token_type=token_type, url=rest_url, version=version, ) diff --git a/hikari/internal/meta.py b/hikari/internal/class_helpers.py similarity index 67% rename from hikari/internal/meta.py rename to hikari/internal/class_helpers.py index 905eeff5d2..2667a47e2b 100644 --- a/hikari/internal/meta.py +++ b/hikari/internal/class_helpers.py @@ -16,7 +16,7 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . -"""Various functional types and metatypes.""" +"""Various metatypes and utilities for configuring classes.""" from __future__ import annotations @@ -24,9 +24,31 @@ import abc import inspect +import logging import typing -from . import more_collections +from hikari.internal import more_collections + + +def get_logger(cls: typing.Union[typing.Type, typing.Any], *additional_args: str) -> logging.Logger: + """Get an appropriately named logger for the given class or object. + + Parameters + ---------- + cls : typing.Type OR object + A type or instance of a type to make a logger in the name of. + *additional_args : str + Additional tokens to append onto the logger name, separated by `.`. + This is useful in some places to append info such as shard ID to each + logger to enable shard-specific logging, for example. + + Returns + ------- + logging.Logger + The logger to use. + """ + cls = cls if isinstance(cls, type) else type(cls) + return logging.getLogger(".".join((cls.__module__, cls.__qualname__, *additional_args))) class SingletonMeta(type): @@ -52,9 +74,7 @@ class SingletonMeta(type): thread safe. """ - # pylint: disable=unsubscriptable-object ___instance_dict_t___ = more_collections.WeakKeyDictionary[typing.Type[typing.Any], typing.Any] - # pylint: enable=unsubscriptable-object ___instances___: ___instance_dict_t___ = more_collections.WeakKeyDictionary() __slots__ = () @@ -85,36 +105,3 @@ class Singleton(metaclass=SingletonMeta): Constructing instances of this class or derived classes may not be thread safe. """ - - -class UniqueFunctionMeta(abc.ABCMeta): - """Metaclass for mixins that are expected to provide unique function names. - - If subclassing from two mixins that are derived from this type and both - mixins provide the same function, a type error is raised when the class is - defined. - - !!! note - This metaclass derives from `abc.ABCMeta`, and thus is compatible with - abstract method conduit. - """ - - __slots__ = () - - @classmethod - def __prepare__(mcs, name, bases, **kwargs): - routines = {} - - for base in bases: - for identifier, method in inspect.getmembers(base, inspect.isroutine): - if identifier.startswith("__"): - continue - - if identifier in routines and method != routines[identifier]: - raise TypeError( - f"Conflicting methods {routines[identifier].__qualname__} and {method.__qualname__} found." - ) - - routines[identifier] = method - - return super().__prepare__(name, bases, **kwargs) diff --git a/hikari/internal/helpers.py b/hikari/internal/helpers.py deleted file mode 100644 index b94f0bd9ce..0000000000 --- a/hikari/internal/helpers.py +++ /dev/null @@ -1,108 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# Copyright © Nekoka.tt 2019-2020 -# -# This file is part of Hikari. -# -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . -"""General helper functions and classes that are not categorised elsewhere.""" - -from __future__ import annotations - -__all__ = [] - -import logging -import typing - -from hikari.models import bases -from . import more_collections - -if typing.TYPE_CHECKING: - from hikari.models import guilds - from hikari.models import users - - -def get_logger(cls: typing.Union[typing.Type, typing.Any], *additional_args: str) -> logging.Logger: - cls = cls if isinstance(cls, type) else type(cls) - return logging.getLogger(".".join((cls.__module__, cls.__qualname__, *additional_args))) - - -def generate_allowed_mentions( # pylint:disable=line-too-long - mentions_everyone: bool, - user_mentions: typing.Union[typing.Collection[typing.Union[bases.Snowflake, int, str, users.User]], bool], - role_mentions: typing.Union[typing.Collection[typing.Union[bases.Snowflake, int, str, guilds.Role]], bool], -) -> typing.Dict[str, typing.Sequence[str]]: - """Generate an allowed mentions object based on input mention rules. - - Parameters - ---------- - mentions_everyone : bool - Whether `@everyone` and `@here` mentions should be resolved by - discord and lead to actual pings. - user_mentions : typing.Collection[hikari.models.users.User | hikari.models.bases.Snowflake | int | str] | bool - Either an array of user objects/IDs to allow mentions for, - `True` to allow all user mentions or `False` to block all - user mentions from resolving. - role_mentions : typing.Collection[hikari.models.guilds.Role | hikari.models.bases.Snowflake | int | str] | bool - Either an array of guild role objects/IDs to allow mentions for, - `True` to allow all role mentions or `False` to block all - role mentions from resolving. - - Returns - ------- - typing.Dict[str, typing.Sequence[str]] - The resulting allowed mentions dict object. - - Raises - ------ - ValueError - If more than 100 unique objects/entities are passed for - `role_mentions` or `user_mentions. - """ - parsed_mentions = [] - allowed_mentions = {} - if mentions_everyone is True: - parsed_mentions.append("everyone") - if user_mentions is True: - parsed_mentions.append("users") - # This covers both `False` and an array of IDs/objs by using `user_mentions or EMPTY_SEQUENCE`, where a - # resultant empty list will mean that all user mentions are blacklisted. - else: - allowed_mentions["users"] = list( - # dict.fromkeys is used to remove duplicate entries that would cause discord to return an error. - dict.fromkeys( - str(user.id if isinstance(user, bases.Unique) else int(user)) - for user in user_mentions or more_collections.EMPTY_SEQUENCE - ) - ) - if len(allowed_mentions["users"]) > 100: - raise ValueError("Only up to 100 users can be provided.") - if role_mentions is True: - parsed_mentions.append("roles") - # This covers both `False` and an array of IDs/objs by using `user_mentions or EMPTY_SEQUENCE`, where a - # resultant empty list will mean that all role mentions are blacklisted. - else: - allowed_mentions["roles"] = list( - # dict.fromkeys is used to remove duplicate entries that would cause discord to return an error. - dict.fromkeys( - str(role.id if isinstance(role, bases.Unique) else int(role)) - for role in role_mentions or more_collections.EMPTY_SEQUENCE - ) - ) - if len(allowed_mentions["roles"]) > 100: - raise ValueError("Only up to 100 roles can be provided.") - allowed_mentions["parse"] = parsed_mentions - # As a note, discord will also treat an empty `allowed_mentions` object as if it wasn't passed at all, so we - # want to use empty lists for blacklisting elements rather than just not including blacklisted elements. - return allowed_mentions diff --git a/hikari/internal/more_asyncio.py b/hikari/internal/more_asyncio.py index 13335f119f..9bc1460b8c 100644 --- a/hikari/internal/more_asyncio.py +++ b/hikari/internal/more_asyncio.py @@ -74,4 +74,5 @@ def is_async_iterator(obj: typing.Any) -> bool: def is_async_iterable(obj: typing.Any) -> bool: """Determine if the object is an async iterable or not.""" - return inspect.isfunction(obj.__aiter__) or inspect.ismethod(getattr(obj, "__aiter__", None)) + attr = getattr(obj, "__aiter__", None) + return inspect.isfunction(attr) or inspect.ismethod(attr) diff --git a/hikari/internal/unset.py b/hikari/internal/unset.py index bcc1572421..68ddf69f2d 100644 --- a/hikari/internal/unset.py +++ b/hikari/internal/unset.py @@ -24,10 +24,10 @@ import typing -from hikari.internal import meta +from hikari.internal import class_helpers -class Unset(meta.Singleton): +class Unset(class_helpers.Singleton): """A singleton value that represents an unset field. This will always have a falsified value. diff --git a/hikari/net/gateway.py b/hikari/net/gateway.py index 77da8ebd34..74598599a6 100644 --- a/hikari/net/gateway.py +++ b/hikari/net/gateway.py @@ -35,6 +35,7 @@ from hikari import errors from hikari import http_settings +from hikari.internal import class_helpers from hikari.internal import more_enums from hikari.internal import more_typing from hikari.internal import ratelimits @@ -195,7 +196,7 @@ def __init__( allow_redirects=config.allow_redirects, connector=config.tcp_connector_factory() if config.tcp_connector_factory else None, debug=debug, - logger_name=f"{type(self).__module__}.{type(self).__qualname__}.{shard_id}", + logger=class_helpers.get_logger(self, str(shard_id)), proxy_auth=config.proxy_auth, proxy_headers=config.proxy_headers, proxy_url=config.proxy_url, @@ -447,8 +448,8 @@ async def update_voice_state( } await self._send_json(payload) - async def _close_ws(self, code: _GatewayCloseCode, message: str): - self.logger.debug("sending close frame with code %s and message %r", code.value, message) + async def _close_ws(self, code: int, message: str): + self.logger.debug("sending close frame with code %s and message %r", int(code), message) await self._ws.close(code=code, message=bytes(message, "utf-8")) async def _handshake(self) -> None: diff --git a/hikari/net/http_client.py b/hikari/net/http_client.py index 71c09b9172..9fd2118ba8 100644 --- a/hikari/net/http_client.py +++ b/hikari/net/http_client.py @@ -150,11 +150,11 @@ class HTTPClient(abc.ABC): # pylint:disable=too-many-instance-attributes def __init__( self, + logger: logging.Logger, *, allow_redirects: bool = False, connector: typing.Optional[aiohttp.BaseConnector] = None, debug: bool = False, - logger_name: typing.Optional[str] = None, proxy_auth: typing.Optional[aiohttp.BasicAuth] = None, proxy_headers: typing.Optional[aiohttp.typedefs.LooseHeaders] = None, proxy_url: typing.Optional[str] = None, @@ -163,9 +163,7 @@ def __init__( timeout: typing.Optional[float] = None, trust_env: bool = False, ) -> None: - self.logger = logging.getLogger( - f"{type(self).__module__}.{type(self).__qualname__}" if logger_name is None else logger_name - ) + self.logger = logger self.__client_session = None self._allow_redirects = allow_redirects diff --git a/hikari/net/rest.py b/hikari/net/rest.py index 9ddf81dd20..5560074af3 100644 --- a/hikari/net/rest.py +++ b/hikari/net/rest.py @@ -32,6 +32,7 @@ from hikari import errors from hikari import http_settings from hikari import rest_app +from hikari.internal import class_helpers from hikari.internal import conversions from hikari.internal import more_collections from hikari.internal import more_typing @@ -114,7 +115,7 @@ def __init__( allow_redirects=config.allow_redirects, connector=config.tcp_connector_factory() if config.tcp_connector_factory else None, debug=debug, - logger_name=f"{type(self).__module__}.{type(self).__qualname__}", + logger=class_helpers.get_logger(self), proxy_auth=config.proxy_auth, proxy_headers=config.proxy_headers, proxy_url=config.proxy_url, @@ -380,7 +381,8 @@ async def fetch_channel( response = await self._request(route) return self._app.entity_factory.deserialize_channel(response) - _GuildChannelT = typing.TypeVar("_GuildChannelT", bound=channels.GuildChannel, contravariant=True) + if typing.TYPE_CHECKING: + _GuildChannelT = typing.TypeVar("_GuildChannelT", bound=channels.GuildChannel, contravariant=True) # This overload just tells any static type checker that if we input, say, # a GuildTextChannel, we should always expect a GuildTextChannel as the diff --git a/hikari/net/user_agents.py b/hikari/net/user_agents.py index 57d0131833..41896f4374 100644 --- a/hikari/net/user_agents.py +++ b/hikari/net/user_agents.py @@ -31,10 +31,10 @@ import typing -from hikari.internal import meta +from hikari.internal import class_helpers -class UserAgent(metaclass=meta.SingletonMeta): +class UserAgent(metaclass=class_helpers.SingletonMeta): """Platform version info. !!! note diff --git a/tests/hikari/internal/test_class_helpers.py b/tests/hikari/internal/test_class_helpers.py new file mode 100644 index 0000000000..e9b7c7ba32 --- /dev/null +++ b/tests/hikari/internal/test_class_helpers.py @@ -0,0 +1,52 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# Copyright © Nekokatt 2019-2020 +# +# This file is part of Hikari. +# +# Hikari is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Hikari is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Hikari. If not, see . +import pytest + +from hikari.internal import class_helpers + + +def test_SingletonMeta(): + class StubSingleton(metaclass=class_helpers.SingletonMeta): + pass + + assert StubSingleton() is StubSingleton() + + +def test_Singleton(): + class StubSingleton(class_helpers.Singleton): + pass + + assert StubSingleton() is StubSingleton() + + +class Class: + pass + + +@pytest.mark.parametrize( + ["args", "expected_name"], + [ + ([Class], f"{__name__}.Class"), + ([Class()], f"{__name__}.Class"), + ([Class, "Foooo", "bar", "123"], f"{__name__}.Class.Foooo.bar.123"), + ([Class(), "qux", "QUx", "940"], f"{__name__}.Class.qux.QUx.940"), + ], +) +def test_get_logger(args, expected_name): + assert class_helpers.get_logger(*args).name == expected_name diff --git a/tests/hikari/internal/test_conversions.py b/tests/hikari/internal/test_conversions.py index 66eb23bb23..35e78eaf56 100644 --- a/tests/hikari/internal/test_conversions.py +++ b/tests/hikari/internal/test_conversions.py @@ -22,32 +22,7 @@ import pytest from hikari.internal import conversions - - -@pytest.mark.parametrize( - ["value", "cast", "expect"], - [ - ("22", int, 22), - (None, int, None), - ("22", lambda a: float(a) / 10 + 7, 9.2), - (None, lambda a: float(a) / 10 + 7, None), - ], -) -def test_nullable_cast(value, cast, expect): - assert conversions.nullable_cast(value, cast) == expect - - -@pytest.mark.parametrize( - ["value", "cast", "default", "expect"], - [ - ("hello", int, "dead", "dead"), - ("22", int, "dead", 22), - ("22", lambda n: n + 4, ..., ...), - (22, lambda n: n + 4, ..., 26), - ], -) -def test_try_cast(value, cast, default, expect): - assert conversions.try_cast(value, cast, default) == expect +from hikari.internal import unset def test_put_if_specified_when_specified(): @@ -60,7 +35,7 @@ def test_put_if_specified_when_specified(): def test_put_if_specified_when_unspecified(): d = {} - conversions.put_if_specified(d, "bar", ...) + conversions.put_if_specified(d, "bar", unset.UNSET) assert d == {} @@ -71,33 +46,6 @@ def test_put_if_specified_when_type_after_passed(): assert d == {"foo": "69", "bar": 69} -@pytest.mark.parametrize( - ["img_bytes", "expect"], - [ - (b"\211PNG\r\n\032\n", "data:image/png;base64,iVBORw0KGgo="), - (b" Exif", "data:image/jpeg;base64,ICAgICAgRXhpZg=="), - (b" JFIF", "data:image/jpeg;base64,ICAgICAgSkZJRg=="), - (b"GIF87a", "data:image/gif;base64,R0lGODdh"), - (b"GIF89a", "data:image/gif;base64,R0lGODlh"), - (b"RIFF WEBP", "data:image/webp;base64,UklGRiAgICBXRUJQ"), - ], -) -def test_image_bytes_to_image_data_img_types(img_bytes, expect): - assert conversions.image_bytes_to_image_data(img_bytes) == expect - - -def test_image_bytes_to_image_data_when_None_returns_None(): - assert conversions.image_bytes_to_image_data(None) is None - - -def test_image_bytes_to_image_data_when_unsupported_image_type_raises_value_error(): - try: - conversions.image_bytes_to_image_data(b"") - assert False - except ValueError: - assert True - - def test_parse_iso_8601_date_with_negative_timezone(): string = "2019-10-10T05:22:33.023456-02:30" date = conversions.iso8601_datetime_string_to_datetime(string) diff --git a/tests/hikari/internal/test_helpers.py b/tests/hikari/internal/test_helpers.py deleted file mode 100644 index 86f72ff71c..0000000000 --- a/tests/hikari/internal/test_helpers.py +++ /dev/null @@ -1,110 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# Copyright © Nekoka.tt 2019-2020 -# -# This file is part of Hikari. -# -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . -import pytest - -from hikari.models import guilds, users -from hikari.internal import helpers -from tests.hikari import _helpers - - -@pytest.mark.parametrize( - ("kwargs", "expected_result"), - [ - ( - {"mentions_everyone": True, "user_mentions": True, "role_mentions": True}, - {"parse": ["everyone", "users", "roles"]}, - ), - ( - {"mentions_everyone": False, "user_mentions": False, "role_mentions": False}, - {"parse": [], "users": [], "roles": []}, - ), - ( - {"mentions_everyone": True, "user_mentions": ["1123123"], "role_mentions": True}, - {"parse": ["everyone", "roles"], "users": ["1123123"]}, - ), - ( - {"mentions_everyone": True, "user_mentions": True, "role_mentions": ["1231123"]}, - {"parse": ["everyone", "users"], "roles": ["1231123"]}, - ), - ( - {"mentions_everyone": False, "user_mentions": ["1123123"], "role_mentions": True}, - {"parse": ["roles"], "users": ["1123123"]}, - ), - ( - {"mentions_everyone": False, "user_mentions": True, "role_mentions": ["1231123"]}, - {"parse": ["users"], "roles": ["1231123"]}, - ), - ( - {"mentions_everyone": False, "user_mentions": ["1123123"], "role_mentions": False}, - {"parse": [], "roles": [], "users": ["1123123"]}, - ), - ( - {"mentions_everyone": False, "user_mentions": False, "role_mentions": ["1231123"]}, - {"parse": [], "roles": ["1231123"], "users": []}, - ), - ( - {"mentions_everyone": False, "user_mentions": ["22222"], "role_mentions": ["1231123"]}, - {"parse": [], "users": ["22222"], "roles": ["1231123"]}, - ), - ( - {"mentions_everyone": True, "user_mentions": ["22222"], "role_mentions": ["1231123"]}, - {"parse": ["everyone"], "users": ["22222"], "roles": ["1231123"]}, - ), - ], -) -def test_generate_allowed_mentions(kwargs, expected_result): - assert helpers.generate_allowed_mentions(**kwargs) == expected_result - - -@_helpers.parametrize_valid_id_formats_for_models("role", 3, guilds.Role) -def test_generate_allowed_mentions_removes_duplicate_role_ids(role): - result = helpers.generate_allowed_mentions( - role_mentions=["1", "2", "1", "3", "5", "7", "2", role], user_mentions=True, mentions_everyone=True - ) - assert result == {"roles": ["1", "2", "3", "5", "7"], "parse": ["everyone", "users"]} - - -@_helpers.parametrize_valid_id_formats_for_models("user", 3, users.User) -def test_generate_allowed_mentions_removes_duplicate_user_ids(user): - result = helpers.generate_allowed_mentions( - role_mentions=True, user_mentions=["1", "2", "1", "3", "5", "7", "2", user], mentions_everyone=True - ) - assert result == {"users": ["1", "2", "3", "5", "7"], "parse": ["everyone", "roles"]} - - -@_helpers.parametrize_valid_id_formats_for_models("role", 190007233919057920, guilds.Role) -def test_generate_allowed_mentions_handles_all_role_formats(role): - result = helpers.generate_allowed_mentions(role_mentions=[role], user_mentions=True, mentions_everyone=True) - assert result == {"roles": ["190007233919057920"], "parse": ["everyone", "users"]} - - -@_helpers.parametrize_valid_id_formats_for_models("user", 190007233919057920, users.User) -def test_generate_allowed_mentions_handles_all_user_formats(user): - result = helpers.generate_allowed_mentions(role_mentions=True, user_mentions=[user], mentions_everyone=True) - assert result == {"users": ["190007233919057920"], "parse": ["everyone", "roles"]} - - -@_helpers.assert_raises(type_=ValueError) -def test_generate_allowed_mentions_raises_error_on_too_many_roles(): - helpers.generate_allowed_mentions(user_mentions=False, role_mentions=list(range(101)), mentions_everyone=False) - - -@_helpers.assert_raises(type_=ValueError) -def test_generate_allowed_mentions_raises_error_on_too_many_users(): - helpers.generate_allowed_mentions(user_mentions=list(range(101)), role_mentions=False, mentions_everyone=False) diff --git a/tests/hikari/internal/test_meta.py b/tests/hikari/internal/test_meta.py deleted file mode 100644 index 804c09ea24..0000000000 --- a/tests/hikari/internal/test_meta.py +++ /dev/null @@ -1,81 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# Copyright © Nekokatt 2019-2020 -# -# This file is part of Hikari. -# -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . -from hikari.internal import meta - - -def test_SingletonMeta(): - class StubSingleton(metaclass=meta.SingletonMeta): - pass - - assert StubSingleton() is StubSingleton() - - -class TestUniqueFunctionMeta: - def test_raises_type_error_on_duplicated_methods(self): - class StubMixin1(metaclass=meta.UniqueFunctionMeta): - def foo(self): - ... - - def bar(cls): - ... - - class StubMixin2(metaclass=meta.UniqueFunctionMeta): - def foo(cls): - ... - - def baz(cls): - ... - - try: - - class Impl(StubMixin1, StubMixin2): - ... - - assert False, "Should've raised a TypeError on overwritten function." - except TypeError: - pass - - def test_passes_when_no_duplication_present(self): - class StubMixin1(metaclass=meta.UniqueFunctionMeta): - def foo(self): - ... - - def bar(cls): - ... - - class StubMixin2(metaclass=meta.UniqueFunctionMeta): - def baz(cls): - ... - - class Impl(StubMixin1, StubMixin2): - ... - - def test_allows_duplicate_methods_when_inherited_from_same_base_further_up(self): - class StubMixin0(metaclass=meta.UniqueFunctionMeta): - def nyaa(self): - ... - - class StubMixin1(StubMixin0): - ... - - class StubMixin2(StubMixin0): - ... - - class Impl(StubMixin1, StubMixin2): - ... diff --git a/tests/hikari/models/test_applications.py b/tests/hikari/models/test_applications.py index 34403a8747..5bc8618433 100644 --- a/tests/hikari/models/test_applications.py +++ b/tests/hikari/models/test_applications.py @@ -19,7 +19,7 @@ import mock import pytest -from hikari import application +from hikari import base_app from hikari.net import urls from hikari.models import applications from hikari.models import guilds @@ -29,7 +29,7 @@ @pytest.fixture() def mock_app(): - return mock.MagicMock(application.Application) + return mock.MagicMock(base_app.IBaseApp) @pytest.fixture() diff --git a/tests/hikari/net/__init__.py b/tests/hikari/net/__init__.py new file mode 100644 index 0000000000..1c1502a5ca --- /dev/null +++ b/tests/hikari/net/__init__.py @@ -0,0 +1,18 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# Copyright © Nekoka.tt 2019-2020 +# +# This file is part of Hikari. +# +# Hikari is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Hikari is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Hikari. If not, see . diff --git a/tests/hikari/internal/test_http_client.py b/tests/hikari/net/test_http_client.py similarity index 100% rename from tests/hikari/internal/test_http_client.py rename to tests/hikari/net/test_http_client.py From f47799337cd9056476f4e7a3557fea50e999d979 Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Wed, 27 May 2020 18:27:18 +0100 Subject: [PATCH 401/922] Began voice implementation and fixed bugs in gateway. --- hikari/bot.py | 6 + hikari/impl/bot.py | 9 +- hikari/net/gateway.py | 48 ++--- hikari/net/voice_gateway.py | 385 ++++++++++++++++++++++++++++++++++++ 4 files changed, 423 insertions(+), 25 deletions(-) create mode 100644 hikari/net/voice_gateway.py diff --git a/hikari/bot.py b/hikari/bot.py index 547380ef8b..3c7b61a223 100644 --- a/hikari/bot.py +++ b/hikari/bot.py @@ -24,6 +24,7 @@ from hikari import gateway_dispatcher from hikari import gateway_zookeeper +from hikari import http_settings as http_settings_ from hikari import rest_app @@ -37,3 +38,8 @@ class IBot(rest_app.IRESTApp, gateway_zookeeper.IGatewayZookeeper, gateway_dispa """ __slots__ = () + + @property + @abc.abstractmethod + def http_settings(self) -> http_settings_.HTTPSettings: + """The HTTP settings to use.""" diff --git a/hikari/impl/bot.py b/hikari/impl/bot.py index fde56ad1b6..63ad08aa6f 100644 --- a/hikari/impl/bot.py +++ b/hikari/impl/bot.py @@ -40,7 +40,7 @@ from hikari import cache as cache_ from hikari import entity_factory as entity_factory_ from hikari import event_consumer as event_consumer_ - from hikari import http_settings + from hikari import http_settings as http_settings_ from hikari import event_dispatcher from hikari import gateway_zookeeper from hikari.models import intents as intents_ @@ -50,7 +50,7 @@ class BotImpl(gateway_zookeeper.AbstractGatewayZookeeper, bot.IBot): def __init__( self, *, - config: http_settings.HTTPSettings, + config: http_settings_.HTTPSettings, debug: bool = False, gateway_url: str, gateway_version: int = 6, @@ -70,6 +70,7 @@ def __init__( self._logger = class_helpers.get_logger(self) self._cache = cache_impl.CacheImpl() + self._config = config self._event_manager = event_manager.EventManagerImpl() self._entity_factory = entity_factory_impl.EntityFactoryImpl() @@ -123,6 +124,10 @@ def rest(self) -> rest.REST: def event_consumer(self) -> event_consumer_.IEventConsumer: return self._event_manager + @property + def http_settings(self) -> http_settings_.HTTPSettings: + return self._config + async def close(self) -> None: await super().close() await self._rest.close() diff --git a/hikari/net/gateway.py b/hikari/net/gateway.py index 74598599a6..b7f4404758 100644 --- a/hikari/net/gateway.py +++ b/hikari/net/gateway.py @@ -69,9 +69,6 @@ class Activity: """The activity type.""" -RawDispatchT = typing.Callable[["Gateway", str, more_typing.JSONObject], more_typing.Coroutine[None]] - - class Gateway(http_client.HTTPClient): """Implementation of a V6 and V7 compatible gateway. @@ -116,8 +113,6 @@ class Gateway(http_client.HTTPClient): @more_enums.must_be_unique class _GatewayCloseCode(int, more_enums.Enum): - """Reasons for closing a gateway connection.""" - RFC_6455_NORMAL_CLOSURE = 1000 RFC_6455_GOING_AWAY = 1001 RFC_6455_PROTOCOL_ERROR = 1002 @@ -149,8 +144,6 @@ class _GatewayCloseCode(int, more_enums.Enum): @more_enums.must_be_unique class _GatewayOpcode(int, more_enums.Enum): - """Opcodes that the gateway uses internally.""" - DISPATCH = 0 HEARTBEAT = 1 IDENTIFY = 2 @@ -173,6 +166,8 @@ class _SocketClosed(RuntimeError): class _InvalidSession(RuntimeError): can_resume: bool = False + RawDispatchT = typing.Callable[["Gateway", str, more_typing.JSONObject], more_typing.Coroutine[None]] + def __init__( self, *, @@ -215,7 +210,6 @@ def __init__( self._last_run_started_at = float("nan") self._request_close_event = asyncio.Event() self._seq = None - self._session_id = None self._shard_id = shard_id self._shard_count = shard_count self._status = initial_status @@ -233,6 +227,7 @@ def __init__( self.last_message_received = float("nan") self.large_threshold = large_threshold self.ratelimiter = ratelimits.WindowedBurstRateLimiter(str(shard_id), 60.0, 120) + self.session_id = None scheme, netloc, path, params, _, _ = urllib.parse.urlparse(url, allow_fragments=True) @@ -289,6 +284,7 @@ async def _run(self) -> None: # This is set to ensure that the `start' waiter does not deadlock if # we cannot connect successfully. It is a hack, but it works. self._handshake_event.set() + # Close the aiohttp client session. await super().close() async def _run_once(self) -> bool: @@ -297,11 +293,14 @@ async def _run_once(self) -> bool: if self._now() - self._last_run_started_at < 30: # Interrupt sleep immediately if a request to close is fired. - wait_task = asyncio.create_task(self._request_close_event.wait()) + wait_task = asyncio.create_task( + self._request_close_event.wait(), name=f"gateway client {self._shard_id} backing off" + ) try: backoff = next(self._backoff) self.logger.debug("backing off for %ss", backoff) await asyncio.wait_for(wait_task, timeout=backoff) + return False except asyncio.TimeoutError: pass @@ -343,16 +342,16 @@ async def _run_once(self) -> bool: except self._InvalidSession as ex: if ex.can_resume: - self.logger.warning("invalid session, so will attempt to resume session %s", self._session_id) + self.logger.warning("invalid session, so will attempt to resume session %s", self.session_id) await self._close_ws(self._GatewayCloseCode.DO_NOT_INVALIDATE_SESSION, "invalid session (resume)") else: self.logger.warning("invalid session, so will attempt to reconnect with new session") await self._close_ws(self._GatewayCloseCode.RFC_6455_NORMAL_CLOSURE, "invalid session (no resume)") self._seq = None - self._session_id = None + self.session_id = None except self._Reconnect: - self.logger.warning("instructed by Discord to reconnect and resume session %s", self._session_id) + self.logger.warning("instructed by Discord to reconnect and resume session %s", self.session_id) self._backoff.reset() await self._close_ws(self._GatewayCloseCode.DO_NOT_INVALIDATE_SESSION, "reconnecting") @@ -441,7 +440,7 @@ async def update_voice_state( "op": self._GatewayOpcode.VOICE_STATE_UPDATE, "d": { "guild_id": str(int(guild)), - "channel": str(int(channel)) if channel is not None else None, + "channel_id": str(int(channel)) if channel is not None else None, "self_mute": self_mute, "self_deaf": self_deaf, }, @@ -450,25 +449,28 @@ async def update_voice_state( async def _close_ws(self, code: int, message: str): self.logger.debug("sending close frame with code %s and message %r", int(code), message) - await self._ws.close(code=code, message=bytes(message, "utf-8")) + # None if the websocket errored on initialziation. + if self._ws is not None: + await self._ws.close(code=code, message=bytes(message, "utf-8")) async def _handshake(self) -> None: # HELLO! message = await self._receive_json_payload() op = message["op"] if message["op"] != self._GatewayOpcode.HELLO: - raise errors.GatewayError(f"Expected HELLO opcode 10 but received {op}") + await self._close_ws(self._GatewayCloseCode.RFC_6455_POLICY_VIOLATION.value, "did not receive HELLO") + raise errors.GatewayError(f"Expected HELLO opcode {self._GatewayOpcode.HELLO.value} but received {op}") self.heartbeat_interval = message["d"]["heartbeat_interval"] / 1_000.0 - self.logger.debug("received HELLO") + self.logger.debug("received HELLO, heartbeat interval is %s", self.heartbeat_interval) - if self._session_id is not None: + if self.session_id is not None: # RESUME! await self._send_json( { "op": self._GatewayOpcode.RESUME, - "d": {"token": self._token, "seq": self._seq, "session_id": self._session_id}, + "d": {"token": self._token, "seq": self._seq, "session_id": self.session_id}, } ) @@ -538,11 +540,11 @@ async def _poll_events(self) -> None: event = message["t"] self._seq = message["s"] if event == "READY": - self._session_id = data["session_id"] - self.logger.info("connection is ready [session:%s]", self._session_id) + self.session_id = data["session_id"] + self.logger.info("connection is ready [session:%s]", self.session_id) self._handshake_event.set() elif event == "RESUME": - self.logger.info("connection has resumed [session:%s, seq:%s]", self._session_id, self._seq) + self.logger.info("connection has resumed [session:%s, seq:%s]", self.session_id, self._seq) self._handshake_event.set() asyncio.create_task(self._dispatch(self, event, data), name=f"shard {self._shard_id} {event}") @@ -637,9 +639,9 @@ def _log_debug_payload(self, payload: str, message: str, *args: typing.Any) -> N message = f"{message} [seq:%s, session:%s, size:%s]" if self._debug: message = f"{message} with raw payload: %s" - args = (*args, self._seq, self._session_id, len(payload), payload) + args = (*args, self._seq, self.session_id, len(payload), payload) else: - args = (*args, self._seq, self._session_id, len(payload)) + args = (*args, self._seq, self.session_id, len(payload)) self.logger.debug(message, *args) diff --git a/hikari/net/voice_gateway.py b/hikari/net/voice_gateway.py new file mode 100644 index 0000000000..e01bb6be2e --- /dev/null +++ b/hikari/net/voice_gateway.py @@ -0,0 +1,385 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# Copyright © Nekoka.tt 2019-2020 +# +# This file is part of Hikari. +# +# Hikari is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Hikari is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Hikari. If not, see . +"""Implementation of the V4 voice gateway.""" +from __future__ import annotations + +__all__ = ["VoiceGateway"] + +import asyncio +import json +import math +import time +import typing +import urllib.parse + +import aiohttp +import attr + +from hikari import errors +from hikari.internal import class_helpers +from hikari.internal import conversions +from hikari.internal import more_enums +from hikari.internal import more_typing +from hikari.internal import ratelimits +from hikari.models import bases +from hikari.net import http_client + +if typing.TYPE_CHECKING: + from hikari import bot + from hikari import http_settings + + +# At the time of writing, Discord treats the `Upgrade` header used when making +# a websocket connection as being case sensitive, despite +# https://tools.ietf.org/html/rfc6455#page-19, +# https://tools.ietf.org/html/rfc6455#page-21 and +# https://tools.ietf.org/html/rfc6455#page-56 implying otherwise. +# AIOHTTP hard codes this value, annoyingly, and hard codes it to the +# format "WebSocket". Discord's voice will only accept "websocket", so I have +# to screw around by altering AIOHTTP just to make a voice connection until +# someone reviews https://github.com/discord/discord-api-docs/issues/1689 +# sensibly. +import aiohttp.hdrs as _hdrs + +setattr(_hdrs, "WEBSOCKET", "websocket") +del _hdrs + + +class VoiceGateway(http_client.HTTPClient): + @more_enums.must_be_unique + class _GatewayCloseCode(int, more_enums.Enum): + """Reasons for closing a gateway connection.""" + + RFC_6455_NORMAL_CLOSURE = 1000 + RFC_6455_GOING_AWAY = 1001 + RFC_6455_PROTOCOL_ERROR = 1002 + RFC_6455_TYPE_ERROR = 1003 + RFC_6455_ENCODING_ERROR = 1007 + RFC_6455_POLICY_VIOLATION = 1008 + RFC_6455_TOO_BIG = 1009 + RFC_6455_UNEXPECTED_CONDITION = 1011 + + UNKNOWN_OPCODE = 4001 + NOT_AUTHENTICATED = 4003 + AUTHENTICATION_FAILED = 4004 + ALREADY_AUTHENTICATED = 4005 + SESSION_NO_LONGER_VALID = 4006 + SESSION_TIMEOUT = 4009 + SERVER_NOT_FOUND = 4011 + UNKNOWN_PROTOCOL = 4012 + DISCONNECTED = 4014 + VOICE_SERVER_CRASHED = 4015 + UNKNOWN_ENCRYPTION_MODE = 4016 + + @more_enums.must_be_unique + class _GatewayOpcode(int, more_enums.Enum): + IDENTIFY = 0 + SELECT_PROTOCOL = 1 + READY = 2 + HEARTBEAT = 3 + SESSION_DESCRIPTION = 4 + SPEAKING = 5 + HEARTBEAT_ACK = 6 + RESUME = 7 + HELLO = 8 + RESUMED = 9 + CLIENT_DISCONNECT = 13 + + class _Reconnect(RuntimeError): + __slots__ = () + + class _SocketClosed(RuntimeError): + __slots__ = () + + @attr.s(auto_attribs=True, slots=True) + class _InvalidSession(RuntimeError): + can_resume: bool = False + + RawDispatchT = typing.Callable[["VoiceGateway", str, more_typing.JSONObject], more_typing.Coroutine[None]] + + def __init__( + self, + *, + app: bot.IBot, + config: http_settings.HTTPSettings, + debug: bool = False, + endpoint: str, + session_id: str, + user_id: bases.UniqueObjectT, + server_id: bases.UniqueObjectT, + token: str, + ) -> None: + super().__init__( + allow_redirects=config.allow_redirects, + connector=config.tcp_connector_factory() if config.tcp_connector_factory else None, + debug=debug, + # Use the server ID to identify each websocket based on a server. + logger=class_helpers.get_logger(self, conversions.value_to_snowflake(server_id)), + proxy_auth=config.proxy_auth, + proxy_headers=config.proxy_headers, + proxy_url=config.proxy_url, + ssl_context=config.ssl_context, + verify_ssl=config.verify_ssl, + timeout=config.request_timeout, + trust_env=config.trust_env, + ) + + # The port Discord gives me is plain wrong, which is helpful. + path = endpoint.rpartition(":")[0] + query = urllib.parse.urlencode({"v": "4"}) + self._url = f"wss://{path}?{query}" + + self._app = app + self._backoff = ratelimits.ExponentialBackOff(base=1.85, maximum=600, initial_increment=2) + self._last_run_started_at = float("nan") + self._nonce = None + self._request_close_event = asyncio.Event() + self._server_id = conversions.value_to_snowflake(server_id) + self._session_id = session_id + self._token = token + self._user_id = conversions.value_to_snowflake(user_id) + self._voice_ip = None + self._voice_modes = [] + self._voice_port = None + self._voice_ssrc = None + self._ws = None + self._zombied = False + + self.connected_at = float("nan") + self.heartbeat_interval = float("nan") + self.heartbeat_latency = float("nan") + self.last_heartbeat_sent = float("nan") + self.last_message_received = float("nan") + + @property + def is_alive(self) -> bool: + """Return whether the client is alive.""" + return not math.isnan(self.connected_at) + + async def run(self) -> None: + """Start the voice gateway client session.""" + try: + while not self._request_close_event.is_set() and await self._run_once(): + pass + finally: + # Close the aiohttp client session. + await super().close() + + async def close(self) -> None: + """Close the websocket.""" + if not self._request_close_event.is_set(): + if self.is_alive: + self.logger.info("received request to shut down voice gateway client") + else: + self.logger.debug("voice gateway client marked as closed before it was able to start") + self._request_close_event.set() + + if self._ws is not None: + self.logger.warning("voice gateway client closed by user, will not attempt to restart") + await self._close_ws(self._GatewayCloseCode.RFC_6455_NORMAL_CLOSURE, "user shut down application") + + async def _close_ws(self, code: int, message: str): + self.logger.debug("sending close frame with code %s and message %r", int(code), message) + # None if the websocket errored on initialziation. + if self._ws is not None: + await self._ws.close(code=code, message=bytes(message, "utf-8")) + + async def _run_once(self): + self._request_close_event.clear() + + if self._now() - self._last_run_started_at < 30: + # Interrupt sleep immediately if a request to close is fired. + wait_task = asyncio.create_task( + self._request_close_event.wait(), name=f"voice gateway client {self._server_id} backing off" + ) + try: + backoff = next(self._backoff) + self.logger.debug("backing off for %ss", backoff) + await asyncio.wait_for(wait_task, timeout=backoff) + return False + except asyncio.TimeoutError: + pass + + # Do this after; it prevents backing off on the first try. + self._last_run_started_at = self._now() + + try: + self.logger.debug("creating websocket connection to %s", self._url) + self._ws = await self._create_ws(self._url) + self.connected_at = self._now() + await self._handshake() + + # Technically we are connected after the hello, but this ensures we can send and receive + # before firing that event. + await self._on_connect() + + # We should ideally set this after HELLO, but it should be fine + # here as well. If we don't heartbeat in time, something probably + # went majorly wrong anyway. + heartbeat = asyncio.create_task(self._pulse(), name=f"voice gateway client {self._server_id} heartbeat") + + try: + await self._poll_events() + finally: + heartbeat.cancel() + except aiohttp.ClientConnectionError as ex: + self.logger.error( + "failed to connect to Discord because %s.%s: %s", type(ex).__module__, type(ex).__qualname__, str(ex), + ) + + except Exception as ex: + self.logger.error("unexpected exception occurred, shard will now die", exc_info=ex) + await self._close_ws(self._GatewayCloseCode.RFC_6455_UNEXPECTED_CONDITION, "unexpected error occurred") + raise + + finally: + if not math.isnan(self.connected_at): + # Only dispatch this if we actually connected before we failed! + await self._on_disconnect() + + self.connected_at = float("nan") + return True + + async def _poll_events(self): + pass + + async def _pulse(self) -> None: + try: + while not self._request_close_event.is_set(): + now = self._now() + time_since_message = now - self.last_message_received + time_since_heartbeat_sent = now - self.last_heartbeat_sent + + if self.heartbeat_interval < time_since_message: + self.logger.error( + "connection is a zombie, haven't received any message for %ss, last heartbeat sent %ss ago", + time_since_message, + time_since_heartbeat_sent, + ) + self._zombied = True + await self._close_ws(self._GatewayCloseCode.DO_NOT_INVALIDATE_SESSION, "zombie connection") + return + + self.logger.debug( + "preparing to send HEARTBEAT [nonce:%s, interval:%ss]", self._nonce, self.heartbeat_interval + ) + await self._send_json({"op": self._GatewayOpcode.HEARTBEAT, "d": self._nonce}) + self.last_heartbeat_sent = self._now() + + try: + await asyncio.wait_for(self._request_close_event.wait(), timeout=self.heartbeat_interval) + except asyncio.TimeoutError: + pass + + except asyncio.CancelledError: + # This happens if the poll task has stopped. It isn't a problem we need to report. + pass + + async def _on_connect(self): + pass + + async def _on_disconnect(self): + pass + + async def _handshake(self): + # HELLO! + message = await self._receive_json_payload() + op = message["op"] + if message["op"] != self._GatewayOpcode.HELLO: + await self._close_ws(self._GatewayCloseCode.RFC_6455_POLICY_VIOLATION.value, "did not receive HELLO") + raise errors.GatewayError(f"Expected HELLO opcode {self._GatewayOpcode.HELLO.value} but received {op}") + + self.heartbeat_interval = message["d"]["heartbeat_interval"] + + self.logger.debug("received HELLO, heartbeat interval is %s", self.heartbeat_interval) + + if self._session_id is not None: + # RESUME! + await self._send_json( + { + "op": self._GatewayOpcode.RESUME, + "d": {"token": self._token, "server_id": self._server_id, "session_id": self._session_id}, + } + ) + else: + await self._send_json( + { + "op": self._GatewayOpcode.IDENTIFY, + "d": { + "token": self._token, + "server_id": self._server_id, + "user_id": self._user_id, + "session_id": self._session_id, + }, + } + ) + + async def _receive_json_payload(self) -> more_typing.JSONObject: + message = await self._ws.receive() + self.last_message_received = self._now() + + if message.type == aiohttp.WSMsgType.TEXT: + self._log_debug_payload(message.data, "received text payload") + return json.loads(message.data) + + elif message.type == aiohttp.WSMsgType.CLOSE: + close_code = self._ws.close_code + self.logger.debug("connection closed with code %s", close_code) + + if close_code in self._GatewayCloseCode.__members__.values(): + reason = self._GatewayCloseCode(close_code).name + else: + reason = f"unknown close code {close_code}" + + can_reconnect = close_code in ( + self._GatewayCloseCode.DECODE_ERROR, + self._GatewayCloseCode.INVALID_SEQ, + self._GatewayCloseCode.UNKNOWN_ERROR, + self._GatewayCloseCode.SESSION_TIMEOUT, + self._GatewayCloseCode.RATE_LIMITED, + ) + + raise errors.GatewayServerClosedConnectionError(reason, close_code, can_reconnect, False, True) + + elif message.type == aiohttp.WSMsgType.CLOSING or message.type == aiohttp.WSMsgType.CLOSED: + raise self._SocketClosed() + else: + # Assume exception for now. + ex = self._ws.exception() + self.logger.debug("encountered unexpected error", exc_info=ex) + raise errors.GatewayError("Unexpected websocket exception from gateway") from ex + + async def _send_json(self, payload: more_typing.JSONObject) -> None: + message = json.dumps(payload) + self._log_debug_payload(message, "sending json payload") + await self._ws.send_str(message) + + def _log_debug_payload(self, payload: str, message: str, *args: typing.Any) -> None: + message = f"{message} [nonce:%s, url:%s, session_id: %s, server: %s, size:%s]" + if self._debug: + message = f"{message} with raw payload: %s" + args = (*args, self._nonce, self._url, self._session_id, self._server_id, len(payload), payload) + else: + args = (*args, self._nonce, self._url, self._session_id, self._server_id, len(payload)) + + self.logger.debug(message, *args) + + @staticmethod + def _now() -> float: + return time.perf_counter() From 9fe7b7fa9496e78f345fff0317b56e953d9bf856 Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Thu, 28 May 2020 13:02:59 +0100 Subject: [PATCH 402/922] Removed hack for RFC-6455 problems on Discord's side, now that is resolved. --- hikari/net/voice_gateway.py | 62 +++++++++++++++++++++++-------------- 1 file changed, 39 insertions(+), 23 deletions(-) diff --git a/hikari/net/voice_gateway.py b/hikari/net/voice_gateway.py index e01bb6be2e..e2e12c38cf 100644 --- a/hikari/net/voice_gateway.py +++ b/hikari/net/voice_gateway.py @@ -45,23 +45,9 @@ from hikari import http_settings -# At the time of writing, Discord treats the `Upgrade` header used when making -# a websocket connection as being case sensitive, despite -# https://tools.ietf.org/html/rfc6455#page-19, -# https://tools.ietf.org/html/rfc6455#page-21 and -# https://tools.ietf.org/html/rfc6455#page-56 implying otherwise. -# AIOHTTP hard codes this value, annoyingly, and hard codes it to the -# format "WebSocket". Discord's voice will only accept "websocket", so I have -# to screw around by altering AIOHTTP just to make a voice connection until -# someone reviews https://github.com/discord/discord-api-docs/issues/1689 -# sensibly. -import aiohttp.hdrs as _hdrs - -setattr(_hdrs, "WEBSOCKET", "websocket") -del _hdrs - - class VoiceGateway(http_client.HTTPClient): + """Implementation of the V4 Voice Gateway.""" + @more_enums.must_be_unique class _GatewayCloseCode(int, more_enums.Enum): """Reasons for closing a gateway connection.""" @@ -111,8 +97,6 @@ class _SocketClosed(RuntimeError): class _InvalidSession(RuntimeError): can_resume: bool = False - RawDispatchT = typing.Callable[["VoiceGateway", str, more_typing.JSONObject], more_typing.Coroutine[None]] - def __init__( self, *, @@ -150,6 +134,7 @@ def __init__( self._last_run_started_at = float("nan") self._nonce = None self._request_close_event = asyncio.Event() + self._resumable = False self._server_id = conversions.value_to_snowflake(server_id) self._session_id = session_id self._token = token @@ -257,7 +242,39 @@ async def _run_once(self): return True async def _poll_events(self): - pass + while not self._request_close_event.is_set(): + message = await self._receive_json_payload() + + op = message["op"] + data = message["d"] + + if op == self._GatewayOpcode.READY: + self.logger.debug( + "voice websocket is ready [session_id:%s, url:%s]", + self._session_id, + self._url, + ) + elif op == self._GatewayOpcode.RESUMED: + self.logger.debug( + "voice websocket has resumed [session_id:%s, nonce:%s, url:%s]", + self._session_id, + self._nonce, + self._url + ) + elif op == self._GatewayOpcode.HEARTBEAT: + self.logger.debug("received HEARTBEAT; sending HEARTBEAT ACK") + await self._send_json({"op": self._GatewayOpcode.HEARTBEAT_ACK, "d": self._nonce}) + elif op == self._GatewayOpcode.HEARTBEAT_ACK: + self.heartbeat_latency = self._now() - self.last_heartbeat_sent + self.logger.debug("received HEARTBEAT ACK [latency:%ss]", self.heartbeat_latency) + elif op == self._GatewayOpcode.SESSION_DESCRIPTION: + self.logger.debug("received session description data %s", data) + elif op == self._GatewayOpcode.SPEAKING: + self.logger.debug("someone is speaking with data %s", data) + elif op == self._GatewayOpcode.CLIENT_DISCONNECT: + self.logger.debug("a client has disconnected with data %s", data) + else: + self.logger.debug("ignoring unrecognised opcode %s", op) async def _pulse(self) -> None: try: @@ -348,11 +365,10 @@ async def _receive_json_payload(self) -> more_typing.JSONObject: reason = f"unknown close code {close_code}" can_reconnect = close_code in ( - self._GatewayCloseCode.DECODE_ERROR, - self._GatewayCloseCode.INVALID_SEQ, - self._GatewayCloseCode.UNKNOWN_ERROR, + self._GatewayCloseCode.SESSION_NO_LONGER_VALID, self._GatewayCloseCode.SESSION_TIMEOUT, - self._GatewayCloseCode.RATE_LIMITED, + self._GatewayCloseCode.DISCONNECTED, + self._GatewayCloseCode.VOICE_SERVER_CRASHED, ) raise errors.GatewayServerClosedConnectionError(reason, close_code, can_reconnect, False, True) From 7a6a055419ed058bd60bd081bad05e67c1aaea5c Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Thu, 28 May 2020 14:38:36 +0100 Subject: [PATCH 403/922] Moved interfaces for apps into one file, added docstrings and __future__ imports, added event dispatcher stuff, added base component type. --- hikari/{gateway_zookeeper.py => app.py} | 107 ++++++++++++++++-- hikari/base_app.py | 61 ----------- hikari/bot.py | 6 +- hikari/cache.py | 19 +++- hikari/{rest_app.py => component.py} | 25 +---- hikari/entity_factory.py | 6 +- hikari/event_consumer.py | 33 +++++- hikari/event_dispatcher.py | 122 ++++++++++++++++++++- hikari/gateway_dispatcher.py | 48 -------- hikari/impl/bot.py | 7 +- hikari/impl/cache.py | 39 +++++-- hikari/impl/entity_factory.py | 37 +++++-- hikari/impl/event_manager.py | 52 ++++++++- hikari/impl/gateway_zookeeper.py | 42 +++---- hikari/impl/pagination.py | 133 ----------------------- hikari/impl/rest_app.py | 7 +- hikari/net/gateway.py | 35 +++--- hikari/net/iterators.py | 12 +- hikari/net/rest.py | 12 +- hikari/net/rest_utils.py | 4 +- hikari/net/voice_gateway.py | 6 +- tests/hikari/models/test_applications.py | 4 +- 22 files changed, 441 insertions(+), 376 deletions(-) rename hikari/{gateway_zookeeper.py => app.py} (57%) delete mode 100644 hikari/base_app.py rename hikari/{rest_app.py => component.py} (62%) delete mode 100644 hikari/gateway_dispatcher.py delete mode 100644 hikari/impl/pagination.py diff --git a/hikari/gateway_zookeeper.py b/hikari/app.py similarity index 57% rename from hikari/gateway_zookeeper.py rename to hikari/app.py index f80215f0f1..a47674d4be 100644 --- a/hikari/gateway_zookeeper.py +++ b/hikari/app.py @@ -16,23 +16,111 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . +"""Core interfaces for a Hikari application.""" from __future__ import annotations -__all__ = ["IGatewayZookeeper"] +__all__ = ["IApp"] import abc -import datetime +import logging import typing -from hikari import base_app -from hikari.models import guilds +from concurrent import futures if typing.TYPE_CHECKING: - from hikari import event_consumer + import datetime + + from hikari import cache as cache_ + from hikari import entity_factory as entity_factory_ + from hikari import event_consumer as event_consumer_ + from hikari import event_dispatcher as event_dispatcher_ + from hikari.models import guilds from hikari.net import gateway + from hikari.net import rest as rest_ + + +class IApp(abc.ABC): + """Core components that any Hikari-based application will usually need.""" + + __slots__ = () + + @property + @abc.abstractmethod + def logger(self) -> logging.Logger: + """Logger for logging messages.""" + + @property + @abc.abstractmethod + def cache(self) -> cache_.ICache: + """Entity cache.""" + + @property + @abc.abstractmethod + def entity_factory(self) -> entity_factory_.IEntityFactory: + """Entity creator and updater facility.""" + + @property + @abc.abstractmethod + def thread_pool(self) -> typing.Optional[futures.ThreadPoolExecutor]: + """The optional library-wide thread-pool to utilise for file IO.""" + + @abc.abstractmethod + async def close(self) -> None: + """Safely shut down all resources.""" + + +class IRESTApp(IApp, abc.ABC): + """Component specialization that is used for REST-only applications. + + Examples may include web dashboards, or applications where no gateway + connection is required. As a result, no event conduit is provided by + these implementations. + """ + + __slots__ = () + + @property + @abc.abstractmethod + def rest(self) -> rest_.REST: + """REST API.""" + + +class IGatewayConsumer(IApp, abc.ABC): + """Component specialization that supports consumption of raw events. + + This may be combined with `IGatewayZookeeper` for most single-process + bots, or may be a specific component for large distributed applications + that consume events from a message queue, for example. + """ + + __slots__ = () + + @property + @abc.abstractmethod + def event_consumer(self) -> event_consumer_.IEventConsumer: + """Raw event consumer.""" + + +class IGatewayDispatcher(IApp, abc.ABC): + """Component specialization that supports dispatching of events. + These events are expected to be instances of + `hikari.events.base.HikariEvent`. -class IGatewayZookeeper(base_app.IBaseApp, abc.ABC): + This may be combined with `IGatewayZookeeper` for most single-process + bots, or may be a specific component for large distributed applications + that consume events from a message queue, for example. + """ + + __slots__ = () + + @property + @abc.abstractmethod + def event_dispatcher(self) -> event_dispatcher_.IEventDispatcher: + """Event dispatcher and waiter.""" + + +class IGatewayZookeeper(IGatewayConsumer, abc.ABC): """Component specialization that looks after a set of shards. These events will be produced by a low-level gateway implementation, and @@ -45,11 +133,6 @@ class IGatewayZookeeper(base_app.IBaseApp, abc.ABC): __slots__ = () - @property - @abc.abstractmethod - def event_consumer(self) -> event_consumer.IEventConsumer: - """Raw event consumer.""" - @property @abc.abstractmethod def gateway_shards(self) -> typing.Mapping[int, gateway.Gateway]: @@ -57,7 +140,7 @@ def gateway_shards(self) -> typing.Mapping[int, gateway.Gateway]: @property @abc.abstractmethod - def shard_count(self) -> int: + def gateway_shard_count(self) -> int: """The number of shards in the entire distributed application.""" @abc.abstractmethod diff --git a/hikari/base_app.py b/hikari/base_app.py deleted file mode 100644 index 4f43600ea0..0000000000 --- a/hikari/base_app.py +++ /dev/null @@ -1,61 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# Copyright © Nekoka.tt 2019-2020 -# -# This file is part of Hikari. -# -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . -from __future__ import annotations - -__all__ = ["IBaseApp"] - -import abc -import logging -import typing - -from concurrent import futures - -if typing.TYPE_CHECKING: - from hikari import cache as cache_ - from hikari import entity_factory as entity_factory_ - - -class IBaseApp(abc.ABC): - """Core components that any Hikari-based application will usually need.""" - - __slots__ = () - - @property - @abc.abstractmethod - def logger(self) -> logging.Logger: - """Logger for logging messages.""" - - @property - @abc.abstractmethod - def cache(self) -> cache_.ICache: - """Entity cache.""" - - @property - @abc.abstractmethod - def entity_factory(self) -> entity_factory_.IEntityFactory: - """Entity creator and updater facility.""" - - @property - @abc.abstractmethod - def thread_pool(self) -> typing.Optional[futures.ThreadPoolExecutor]: - """The optional library-wide thread-pool to utilise for file IO.""" - - @abc.abstractmethod - async def close(self) -> None: - """Safely shut down all resources.""" diff --git a/hikari/bot.py b/hikari/bot.py index 3c7b61a223..626435cb94 100644 --- a/hikari/bot.py +++ b/hikari/bot.py @@ -22,13 +22,11 @@ import abc -from hikari import gateway_dispatcher -from hikari import gateway_zookeeper +from hikari import app as app_ from hikari import http_settings as http_settings_ -from hikari import rest_app -class IBot(rest_app.IRESTApp, gateway_zookeeper.IGatewayZookeeper, gateway_dispatcher.IGatewayDispatcher, abc.ABC): +class IBot(app_.IRESTApp, app_.IGatewayZookeeper, app_.IGatewayDispatcher, abc.ABC): """Component for single-process bots. Bots are components that have access to a REST API, an event dispatcher, diff --git a/hikari/cache.py b/hikari/cache.py index 749e743602..b67f280bce 100644 --- a/hikari/cache.py +++ b/hikari/cache.py @@ -16,7 +16,7 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . -"""Utilities to handle cache management and object deserialization.""" +"""Utilities to handle cache management.""" from __future__ import annotations __all__ = ["ICache"] @@ -24,7 +24,10 @@ import abc import typing +from hikari import component + if typing.TYPE_CHECKING: + from hikari.internal import more_typing from hikari.models import applications from hikari.models import audit_logs from hikari.models import channels @@ -36,11 +39,19 @@ from hikari.models import messages from hikari.models import users from hikari.models import voices - from hikari.internal import more_typing -class ICache(abc.ABC): - """Interface for a cache implementation.""" +class ICache(component.IComponent, abc.ABC): + """Component that implements entity caching facilities. + + This will be used by the gateway and REST API to cache specific types of + objects that the application should attempt to remember for later, depending + on how this is implemented. + + The implementation may choose to use a simple in-memory collection of + objects, or may decide to use a distributed system such as a Redis cache + for cross-process bots. + """ __slots__ = () diff --git a/hikari/rest_app.py b/hikari/component.py similarity index 62% rename from hikari/rest_app.py rename to hikari/component.py index 12ac473c17..2314fa9cc4 100644 --- a/hikari/rest_app.py +++ b/hikari/component.py @@ -16,34 +16,21 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . -from __future__ import annotations +"""Defines a base interface for application components to derive from.""" -__all__ = ["IRESTApp"] +from __future__ import annotations import abc import typing -from hikari import base_app - if typing.TYPE_CHECKING: - from hikari.net import rest - + from hikari import app as app_ -class IRESTApp(base_app.IBaseApp, abc.ABC): - """Component specialization that is used for REST-only applications. - - Examples may include web dashboards, or applications where no gateway - connection is required. As a result, no event conduit is provided by - these implementations. - """ +class IComponent(abc.ABC): __slots__ = () @property @abc.abstractmethod - def rest(self) -> rest.REST: - """REST API.""" - - @abc.abstractmethod - async def close(self) -> None: - """Close any open resources safely.""" + def app(self) -> app_.IApp: + """The owning application object.""" diff --git a/hikari/entity_factory.py b/hikari/entity_factory.py index ff5027014e..e79c8b52f5 100644 --- a/hikari/entity_factory.py +++ b/hikari/entity_factory.py @@ -24,6 +24,8 @@ import abc import typing +from hikari import component + if typing.TYPE_CHECKING: from hikari.internal import more_typing from hikari.models import applications @@ -40,8 +42,8 @@ from hikari.models import webhooks -class IEntityFactory(abc.ABC): - """Interface for an entity factory implementation.""" +class IEntityFactory(component.IComponent, abc.ABC): + """Component that will serialize and deserialize JSON payloads.""" __slots__ = () diff --git a/hikari/event_consumer.py b/hikari/event_consumer.py index 2ea729b9bc..90672d8ea0 100644 --- a/hikari/event_consumer.py +++ b/hikari/event_consumer.py @@ -21,13 +21,36 @@ __all__ = ["IEventConsumer"] import abc +import typing -from hikari.internal import more_typing -from hikari.net import gateway +from hikari import component +if typing.TYPE_CHECKING: + from hikari.internal import more_typing + from hikari.net import gateway + + +class IEventConsumer(component.IComponent, abc.ABC): + """Interface describing a component that can consume raw gateway events. + + Implementations will usually want to combine this with a + `hikari.event_dispatcher.IEventDispatcher` for a basic in-memory single-app + event management system. You may in some cases implement this separately + if you are passing events onto a system such as a message queue. + """ -class IEventConsumer(abc.ABC): __slots__ = () - async def consume_raw_event(self, shard: gateway.Gateway, event_name: str, payload: more_typing.JSONType): - ... + @abc.abstractmethod + async def consume_raw_event(self, shard: gateway.Gateway, event_name: str, payload: more_typing.JSONType) -> None: + """Process a raw event from a gateway shard and process it. + + Parameters + ---------- + shard : hikari.net.gateway.Gateway + The gateway shard that emitted the event. + event_name : str + The event name. + payload : Any + The payload provided with the event. + """ diff --git a/hikari/event_dispatcher.py b/hikari/event_dispatcher.py index 798a6976f6..57f629bea6 100644 --- a/hikari/event_dispatcher.py +++ b/hikari/event_dispatcher.py @@ -16,6 +16,7 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . +"""Interface providing functionality to dispatch an event object.""" from __future__ import annotations __all__ = ["IEventDispatcher"] @@ -23,14 +24,131 @@ import abc import typing +from hikari import component +from hikari.internal import unset + if typing.TYPE_CHECKING: from hikari.events import base from hikari.internal import more_typing + _EventT = typing.TypeVar("_EventT", bound=base.HikariEvent, covariant=True) + _PredicateT = typing.Callable[[_EventT], typing.Union[bool, more_typing.Coroutine[bool]]] + + +class IEventDispatcher(component.IComponent, abc.ABC): + """Provides the interface for components wishing to implement dispatching. + + This is a consumer of a `hikari.events.bases.HikariEvent` object, and is + expected to invoke one or more corresponding event listeners where + appropriate. + """ -class IEventDispatcher(abc.ABC): __slots__ = () @abc.abstractmethod def dispatch(self, event: base.HikariEvent) -> more_typing.Future[typing.Any]: - ... + """Dispatch an event. + + Parameters + ---------- + event : hikari.events.base.HikariEvent + The event to dispatch. + + Returns + ------- + asyncio.Future + A future that can be optionally awaited. If awaited, the future + will complete once all corresponding event listeners have been + invoked. If not awaited, this will schedule the dispatch of the + events in the background for later. + """ + + @abc.abstractmethod + def subscribe( + self, + event_type: typing.Type[_EventT], + callback: typing.Callable[[_EventT], typing.Union[more_typing.Coroutine[None], None]], + ) -> None: + """Subscribe a given callback to a given event type. + + Parameters + ---------- + event_type : typing.Type[hikari.events.base.HikariEvent] + The event type to listen for. This will also listen for any + subclasses of the given type. + callback : + Either a function or a coroutine function to invoke. This should + consume an instance of the given event, or an instance of a valid + subclass if one exists. Any result is discarded. + """ + + @abc.abstractmethod + def unsubscribe( + self, + event_type: typing.Type[_EventT], + callback: typing.Callable[[_EventT], typing.Union[more_typing.Coroutine[None], None]], + ) -> None: + """Unsubscribe a given callback from a given event type, if present. + + Parameters + ---------- + event_type : typing.Type[hikari.events.base.HikariEvent] + The event type to unsubscribe from. This must be the same exact + type as was originally subscribed with to be removed correctly. + callback : + The callback to unsubscribe. + """ + + @abc.abstractmethod + def listen(self, event_type: typing.Union[unset.Unset, typing.Type[_EventT]]) -> None: + """Generate a decorator to subscribe a callback to an event type. + + This is a second-order decorator. + + Parameters + ---------- + event_type : hikari.internal.unset.Unset OR typing.Type[hikari.events.bases.HikariEvent] + The event type to subscribe to. The implementation may allow this + to be unset. If this is the case, the event type will be inferred + instead from the type hints on the function signature. + + Returns + ------- + typing.Callable + A decorator for a function or coroutine function that passes it + to `subscribe` before returning the function reference. + """ + + @abc.abstractmethod + async def wait_for( + self, event_type: typing.Type[_EventT], predicate: _PredicateT, timeout: typing.Union[float, int, None], + ) -> _EventT: + """Wait for a given event to occur once, then return the event. + + Parameters + ---------- + event_type : typing.Type[hikari.events.bases.HikariEvent] + The event type to listen for. This will listen for subclasses of + this type additionally. + predicate : + A function or coroutine taking the event as the single parameter. + This should return `True` if the event is one you want to return, + or `False` if the event should not be returned. + timeout : float OR int OR None + The amount of time to wait before raising an `asyncio.TimeoutError` + and giving up instead. This is measured in seconds. If `None`, then + no timeout will be waited for (no timeout can result in "leaking" of + coroutines that never complete if called in an uncontrolled way, + so is not recommended). + + Returns + ------- + hikari.events.bases.HikariEvent + The event that was provided. + + Raises + ------ + asyncio.TimeoutError + If the timeout is not `None` and is reached before an event is + received that the predicate returns `True` for. + """ diff --git a/hikari/gateway_dispatcher.py b/hikari/gateway_dispatcher.py deleted file mode 100644 index 59f2a83807..0000000000 --- a/hikari/gateway_dispatcher.py +++ /dev/null @@ -1,48 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# Copyright © Nekoka.tt 2019-2020 -# -# This file is part of Hikari. -# -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . -from __future__ import annotations - -__all__ = ["IGatewayDispatcher"] - -import abc -import typing - -from hikari import base_app - -if typing.TYPE_CHECKING: - from hikari import event_dispatcher - - -class IGatewayDispatcher(base_app.IBaseApp, abc.ABC): - """Component specialization that supports dispatching of events. - - These events are expected to be instances of - `hikari.events.base.HikariEvent`. - - This may be combined with `IGatewayZookeeper` for most single-process - bots, or may be a specific component for large distributed applications - that consume events from a message queue, for example. - """ - - __slots__ = () - - @property - @abc.abstractmethod - def event_dispatcher(self) -> event_dispatcher.IEventDispatcher: - """Event dispatcher and waiter.""" diff --git a/hikari/impl/bot.py b/hikari/impl/bot.py index 63ad08aa6f..9e7f79bc43 100644 --- a/hikari/impl/bot.py +++ b/hikari/impl/bot.py @@ -42,7 +42,6 @@ from hikari import event_consumer as event_consumer_ from hikari import http_settings as http_settings_ from hikari import event_dispatcher - from hikari import gateway_zookeeper from hikari.models import intents as intents_ @@ -69,10 +68,10 @@ def __init__( ): self._logger = class_helpers.get_logger(self) - self._cache = cache_impl.CacheImpl() + self._cache = cache_impl.CacheImpl(app=self) self._config = config - self._event_manager = event_manager.EventManagerImpl() - self._entity_factory = entity_factory_impl.EntityFactoryImpl() + self._event_manager = event_manager.EventManagerImpl(app=self) + self._entity_factory = entity_factory_impl.EntityFactoryImpl(app=self) self._rest = rest.REST( app=self, config=config, debug=debug, token=token, token_type="Bot", url=rest_url, version=rest_version, diff --git a/hikari/impl/cache.py b/hikari/impl/cache.py index 37f1541268..3473944941 100644 --- a/hikari/impl/cache.py +++ b/hikari/impl/cache.py @@ -16,24 +16,39 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . +from __future__ import annotations + +__all__ = ["CacheImpl"] + import typing from hikari import cache -from hikari.internal import more_typing -from hikari.models import applications -from hikari.models import audit_logs -from hikari.models import channels -from hikari.models import embeds -from hikari.models import emojis -from hikari.models import gateway -from hikari.models import guilds -from hikari.models import invites -from hikari.models import messages -from hikari.models import users -from hikari.models import voices + + +if typing.TYPE_CHECKING: + from hikari import app as app_ + from hikari.internal import more_typing + from hikari.models import applications + from hikari.models import audit_logs + from hikari.models import channels + from hikari.models import embeds + from hikari.models import emojis + from hikari.models import gateway + from hikari.models import guilds + from hikari.models import invites + from hikari.models import messages + from hikari.models import users + from hikari.models import voices class CacheImpl(cache.ICache): + def __init__(self, app: app_.IApp) -> None: + self._app = app + + @property + def app(self) -> app_.IApp: + return self._app + async def create_application(self, payload: more_typing.JSONObject) -> applications.Application: pass diff --git a/hikari/impl/entity_factory.py b/hikari/impl/entity_factory.py index 513341886c..782c2d3ae2 100644 --- a/hikari/impl/entity_factory.py +++ b/hikari/impl/entity_factory.py @@ -16,27 +16,40 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . +"""Basic implementation of an entity factory for general bots and REST apps.""" from __future__ import annotations +__all__ = ["EntityFactoryImpl"] + import typing from hikari import entity_factory -from hikari.internal import more_typing -from hikari.models import applications -from hikari.models import audit_logs -from hikari.models import channels -from hikari.models import embeds -from hikari.models import emojis -from hikari.models import gateway -from hikari.models import guilds -from hikari.models import invites -from hikari.models import users -from hikari.models import voices -from hikari.models import webhooks + +if typing.TYPE_CHECKING: + from hikari import app as app_ + from hikari.internal import more_typing + from hikari.models import applications + from hikari.models import audit_logs + from hikari.models import channels + from hikari.models import embeds + from hikari.models import emojis + from hikari.models import gateway + from hikari.models import guilds + from hikari.models import invites + from hikari.models import users + from hikari.models import voices + from hikari.models import webhooks class EntityFactoryImpl(entity_factory.IEntityFactory): + def __init__(self, app: app_.IApp) -> None: + self._app = app + + @property + def app(self) -> app_.IApp: + return self._app + def deserialize_own_connection(self, payload: more_typing.JSONObject) -> applications.OwnConnection: pass diff --git a/hikari/impl/event_manager.py b/hikari/impl/event_manager.py index ba37d9da5a..8d48c36417 100644 --- a/hikari/impl/event_manager.py +++ b/hikari/impl/event_manager.py @@ -16,16 +16,66 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . +from __future__ import annotations + +__all__ = ["EventManagerImpl"] + import typing from hikari import event_consumer from hikari import event_dispatcher from hikari.events import base +from hikari.internal import class_helpers from hikari.internal import more_asyncio -from hikari.internal import more_typing +from hikari.internal import unset +from hikari.net import gateway + +if typing.TYPE_CHECKING: + from hikari import app as app_ + from hikari.internal import more_typing + + _EventT = typing.TypeVar("_EventT", bound=base.HikariEvent, covariant=True) + _PredicateT = typing.Callable[[_EventT], typing.Union[bool, more_typing.Coroutine[bool]]] class EventManagerImpl(event_dispatcher.IEventDispatcher, event_consumer.IEventConsumer): + def __init__(self, app: app_.IApp) -> None: + self._app = app + self.logger = class_helpers.get_logger(self) + + @property + def app(self) -> app_.IApp: + return self._app + def dispatch(self, event: base.HikariEvent) -> more_typing.Future[typing.Any]: # TODO: this return more_asyncio.completed_future() + + async def consume_raw_event(self, shard: gateway.Gateway, event_name: str, payload: more_typing.JSONType) -> None: + try: + callback = getattr(self, "on_" + event_name.lower()) + await callback(shard, payload) + except AttributeError: + self.logger.debug("ignoring unknown event %s", event_name) + + def subscribe( + self, + event_type: typing.Type[_EventT], + callback: typing.Callable[[_EventT], typing.Union[more_typing.Coroutine[None], None]], + ) -> None: + pass + + def unsubscribe( + self, + event_type: typing.Type[_EventT], + callback: typing.Callable[[_EventT], typing.Union[more_typing.Coroutine[None], None]], + ) -> None: + pass + + def listen(self, event_type: typing.Union[unset.Unset, typing.Type[_EventT]]) -> None: + pass + + async def wait_for( + self, event_type: typing.Type[_EventT], predicate: _PredicateT, timeout: typing.Union[float, int, None] + ) -> _EventT: + pass diff --git a/hikari/impl/gateway_zookeeper.py b/hikari/impl/gateway_zookeeper.py index e61105f9cb..fb3b4dfb8c 100644 --- a/hikari/impl/gateway_zookeeper.py +++ b/hikari/impl/gateway_zookeeper.py @@ -18,6 +18,8 @@ # along with Hikari. If not, see . from __future__ import annotations +__all__ = ["AbstractGatewayZookeeper"] + import abc import asyncio import contextlib @@ -25,10 +27,11 @@ import time import typing +from hikari import app as app_ from hikari import event_dispatcher -from hikari import gateway_zookeeper from hikari.events import other from hikari.internal import conversions +from hikari.internal import unset from hikari.net import gateway if typing.TYPE_CHECKING: @@ -40,7 +43,7 @@ from hikari.models import intents as intents_ -class AbstractGatewayZookeeper(gateway_zookeeper.IGatewayZookeeper, abc.ABC): +class AbstractGatewayZookeeper(app_.IGatewayZookeeper, abc.ABC): def __init__( self, *, @@ -60,17 +63,14 @@ def __init__( version: int, ) -> None: self._aiohttp_config = config - - # This is a little hacky workaround to boost performance. We force - self._gather_task = None self._request_close_event = asyncio.Event() self._shard_count = shard_count self._shards = { shard_id: gateway.Gateway( + app=self, config=config, debug=debug, - dispatch=self.event_consumer.consume_raw_event, initial_activity=initial_activity, initial_idle_since=initial_idle_since, initial_is_afk=initial_is_afk, @@ -93,7 +93,7 @@ def gateway_shards(self) -> typing.Mapping[int, gateway.Gateway]: return self._shards @property - def shard_count(self) -> int: + def gateway_shard_count(self) -> int: return self._shard_count async def start(self) -> None: @@ -144,12 +144,12 @@ async def join(self) -> None: if self._gather_task is not None: await self._gather_task - async def _abort(self): + async def _abort(self) -> None: for shard_id in self._tasks: await self._shards[shard_id].close() await asyncio.gather(*self._tasks.values(), return_exceptions=True) - async def _gather(self): + async def _gather(self) -> None: try: await asyncio.gather(*self._tasks.values()) finally: @@ -180,11 +180,11 @@ async def close(self) -> None: # noinspection PyUnresolvedReferences await self.event_dispatcher.dispatch(other.StoppedEvent()) - async def _run(self): + async def _run(self) -> None: await self.start() await self.join() - def run(self): + def run(self) -> None: loop = asyncio.get_event_loop() def sigterm_handler(*_): @@ -210,15 +210,15 @@ def sigterm_handler(*_): async def update_presence( self, *, - status: guilds.PresenceStatus = ..., - activity: typing.Optional[gateway.Activity] = ..., - idle_since: typing.Optional[datetime.datetime] = ..., - is_afk: bool = ..., + status: typing.Union[unset.Unset, guilds.PresenceStatus] = unset.UNSET, + activity: typing.Union[unset.Unset, gateway.Activity, None] = unset.UNSET, + idle_since: typing.Union[unset.Unset, datetime.datetime] = unset.UNSET, + is_afk: typing.Union[unset.Unset, bool] = unset.UNSET, ) -> None: - await asyncio.gather( - *( - s.update_presence(status=status, activity=activity, idle_since=idle_since, is_afk=is_afk) - for s in self._shards.values() - if s.is_alive - ) + coros = ( + s.update_presence(status=status, activity=activity, idle_since=idle_since, is_afk=is_afk) + for s in self._shards.values() + if s.is_alive ) + + await asyncio.gather(*coros) diff --git a/hikari/impl/pagination.py b/hikari/impl/pagination.py deleted file mode 100644 index 783825e2d4..0000000000 --- a/hikari/impl/pagination.py +++ /dev/null @@ -1,133 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# Copyright © Nekoka.tt 2019-2020 -# -# This file is part of Hikari. -# -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . -from __future__ import annotations - -__all__ = [] - -import datetime - -from hikari.models import applications -from hikari.models import bases -from hikari.models import guilds -from hikari.models import messages -from hikari.models import users -from hikari.net import iterators - - -class GuildPaginator(iterators._BufferedLazyIterator[guilds.Guild]): - __slots__ = ("_app", "_newest_first", "_first_id", "_request_partial") - - def __init__(self, app, newest_first, first_item, request_partial): - super().__init__() - self._app = app - self._newest_first = newest_first - self._first_id = self._prepare_first_id( - first_item, bases.Snowflake.max() if newest_first else bases.Snowflake.min(), - ) - self._request_partial = request_partial - - async def _next_chunk(self): - kwargs = {"before" if self._newest_first else "after": self._first_id} - - chunk = await self._request_partial(**kwargs) - - if not chunk: - return None - - self._first_id = chunk[-1]["id"] - - return (applications.OwnGuild.deserialize(g, app=self._app) for g in chunk) - - -class MemberPaginator(iterators._BufferedLazyIterator[guilds.GuildMember]): - __slots__ = ("_app", "_guild_id", "_first_id", "_request_partial") - - def __init__(self, app, guild, created_after, request_partial): - super().__init__() - self._app = app - self._guild_id = str(int(guild)) - self._first_id = self._prepare_first_id(created_after) - self._request_partial = request_partial - - async def _next_chunk(self): - chunk = await self._request_partial(guild_id=self._guild_id, after=self._first_id) - - if not chunk: - return None - - self._first_id = chunk[-1]["id"] - - return (guilds.GuildMember.deserialize(m, app=self._app) for m in chunk) - - -class MessagePaginator(iterators._BufferedLazyIterator[messages.Message]): - __slots__ = ("_app", "_channel_id", "_direction", "_first_id", "_request_partial") - - def __init__(self, app, channel, direction, first, request_partial) -> None: - super().__init__() - self._app = app - self._channel_id = str(int(channel)) - self._direction = direction - self._first_id = ( - str(bases.Snowflake.from_datetime(first)) if isinstance(first, datetime.datetime) else str(int(first)) - ) - self._request_partial = request_partial - - async def _next_chunk(self): - kwargs = { - self._direction: self._first_id, - "channel": self._channel_id, - "limit": 100, - } - - chunk = await self._request_partial(**kwargs) - - if not chunk: - return None - if self._direction == "after": - chunk.reverse() - - self._first_id = chunk[-1]["id"] - - return (messages.Message.deserialize(m, app=self._app) for m in chunk) - - -class ReactionPaginator(iterators._BufferedLazyIterator[messages.Reaction]): - __slots__ = ("_app", "_channel_id", "_message_id", "_first_id", "_emoji", "_request_partial") - - def __init__(self, app, channel, message, emoji, users_after, request_partial) -> None: - super().__init__() - self._app = app - self._channel_id = str(int(channel)) - self._message_id = str(int(message)) - self._emoji = getattr(emoji, "url_name", emoji) - self._first_id = self._prepare_first_id(users_after) - self._request_partial = request_partial - - async def _next_chunk(self): - chunk = await self._request_partial( - channel_id=self._channel_id, message_id=self._message_id, emoji=self._emoji, after=self._first_id - ) - - if not chunk: - return None - - self._first_id = chunk[-1]["id"] - - return (users.User.deserialize(u, app=self._app) for u in chunk) diff --git a/hikari/impl/rest_app.py b/hikari/impl/rest_app.py index 59c15e5218..0bedfcde62 100644 --- a/hikari/impl/rest_app.py +++ b/hikari/impl/rest_app.py @@ -19,16 +19,17 @@ from __future__ import annotations +__all__ = ["RESTAppImpl"] + import logging import typing from concurrent import futures +from hikari import app as app_ from hikari import http_settings -from hikari import rest_app from hikari.impl import cache as cache_impl from hikari.impl import entity_factory as entity_factory_impl from hikari.internal import class_helpers -from hikari.internal import helpers from hikari.net import rest as rest_ from hikari.net import urls @@ -37,7 +38,7 @@ from hikari import entity_factory as entity_factory_ -class RESTAppImpl(rest_app.IRESTApp): +class RESTAppImpl(app_.IRESTApp): def __init__( self, config: http_settings.HTTPSettings, diff --git a/hikari/net/gateway.py b/hikari/net/gateway.py index b7f4404758..a086fda0a6 100644 --- a/hikari/net/gateway.py +++ b/hikari/net/gateway.py @@ -33,11 +33,12 @@ import aiohttp import attr + +from hikari import component from hikari import errors from hikari import http_settings from hikari.internal import class_helpers from hikari.internal import more_enums -from hikari.internal import more_typing from hikari.internal import ratelimits from hikari.internal import unset from hikari.models import bases @@ -49,7 +50,9 @@ if typing.TYPE_CHECKING: import datetime + from hikari import app as app_ from hikari.models import intents as intents_ + from hikari.internal import more_typing @attr.s(eq=True, hash=False, kw_only=True, slots=True) @@ -69,21 +72,18 @@ class Activity: """The activity type.""" -class Gateway(http_client.HTTPClient): +class Gateway(http_client.HTTPClient, component.IComponent): """Implementation of a V6 and V7 compatible gateway. Parameters ---------- + app : hikari.gateway_dispatcher.IGatewayConsumer + The base application. config : hikari.http_settings.HTTPSettings The aiohttp settings to use for the client session. debug : bool If `True`, each sent and received payload is dumped to the logs. If `False`, only the fact that data has been sent/received will be logged. - dispatch : coroutine function with signature `(Gateway, str, dict) -> None` - The dispatch coroutine to invoke each time an event is dispatched. - This is a tri-consumer that takes this gateway object as the first - parameter, the event name as the second parameter, and the JSON - event payload as a `dict` for the third parameter. initial_activity : Activity | None The initial activity to appear to have for this shard. initial_idle_since : datetime.datetime | None @@ -166,14 +166,12 @@ class _SocketClosed(RuntimeError): class _InvalidSession(RuntimeError): can_resume: bool = False - RawDispatchT = typing.Callable[["Gateway", str, more_typing.JSONObject], more_typing.Coroutine[None]] - def __init__( self, *, + app: app_.IGatewayConsumer, config: http_settings.HTTPSettings, debug: bool = False, - dispatch: RawDispatchT, initial_activity: typing.Optional[Activity] = None, initial_idle_since: typing.Optional[datetime.datetime] = None, initial_is_afk: typing.Optional[bool] = None, @@ -201,8 +199,8 @@ def __init__( trust_env=config.trust_env, ) self._activity = initial_activity + self._app = app self._backoff = ratelimits.ExponentialBackOff(base=1.85, maximum=600, initial_increment=2) - self._dispatch = dispatch self._handshake_event = asyncio.Event() self._idle_since = initial_idle_since self._intents = intents @@ -240,6 +238,10 @@ def __init__( self.url = urllib.parse.urlunparse((scheme, netloc, path, params, new_query, "")) + @property + def app(self) -> app_.IGatewayConsumer: + return self._app + @property def is_alive(self) -> bool: """Return whether the shard is alive.""" @@ -321,7 +323,7 @@ async def _run_once(self) -> bool: # Technically we are connected after the hello, but this ensures we can send and receive # before firing that event. - asyncio.create_task(self._dispatch(self, "CONNECTED", {}), name=f"shard {self._shard_id} CONNECTED") + asyncio.create_task(self._dispatch("CONNECTED", {}), name=f"shard {self._shard_id} CONNECTED") # We should ideally set this after HELLO, but it should be fine # here as well. If we don't heartbeat in time, something probably @@ -372,9 +374,7 @@ async def _run_once(self) -> bool: finally: if not math.isnan(self.connected_at): # Only dispatch this if we actually connected before we failed! - asyncio.create_task( - self._dispatch(self, "DISCONNECTED", {}), name=f"shard {self._shard_id} DISCONNECTED" - ) + asyncio.create_task(self._dispatch("DISCONNECTED", {}), name=f"shard {self._shard_id} DISCONNECTED") self.connected_at = float("nan") @@ -547,7 +547,7 @@ async def _poll_events(self) -> None: self.logger.info("connection has resumed [session:%s, seq:%s]", self.session_id, self._seq) self._handshake_event.set() - asyncio.create_task(self._dispatch(self, event, data), name=f"shard {self._shard_id} {event}") + asyncio.create_task(self._dispatch(event, data), name=f"shard {self._shard_id} {event}") elif op == self._GatewayOpcode.HEARTBEAT: self.logger.debug("received HEARTBEAT; sending HEARTBEAT ACK") @@ -631,6 +631,9 @@ async def _send_json(self, payload: more_typing.JSONObject) -> None: self._log_debug_payload(message, "sending json payload") await self._ws.send_str(message) + def _dispatch(self, event_name: str, payload: more_typing.JSONType) -> typing.Coroutine[None]: + return self._app.event_consumer.consume_raw_event(self, event_name, payload) + @staticmethod def _now() -> float: return time.perf_counter() diff --git a/hikari/net/iterators.py b/hikari/net/iterators.py index 265e2afcba..c65241eec2 100644 --- a/hikari/net/iterators.py +++ b/hikari/net/iterators.py @@ -24,7 +24,7 @@ import abc import typing -from hikari import rest_app +from hikari import app as app_ from hikari.internal import conversions from hikari.internal import more_collections from hikari.internal import more_typing @@ -242,7 +242,7 @@ class MessageIterator(_BufferedLazyIterator[messages.Message]): def __init__( self, - app: rest_app.IRESTApp, + app: app_.IRESTApp, request_call: typing.Callable[..., more_typing.Coroutine[more_typing.JSONArray]], channel_id: str, direction: str, @@ -274,7 +274,7 @@ class ReactorIterator(_BufferedLazyIterator[users.User]): def __init__( self, - app: rest_app.IRESTApp, + app: app_.IRESTApp, request_call: typing.Callable[..., more_typing.Coroutine[more_typing.JSONArray]], channel_id: str, message_id: str, @@ -303,7 +303,7 @@ class OwnGuildIterator(_BufferedLazyIterator[applications.OwnGuild]): def __init__( self, - app: rest_app.IRESTApp, + app: app_.IRESTApp, request_call: typing.Callable[..., more_typing.Coroutine[more_typing.JSONArray]], newest_first: bool, first_id: str, @@ -334,7 +334,7 @@ class MemberIterator(_BufferedLazyIterator[guilds.GuildMember]): def __init__( self, - app: rest_app.IRESTApp, + app: app_.IRESTApp, request_call: typing.Callable[..., more_typing.Coroutine[more_typing.JSONArray]], guild_id: str, ) -> None: @@ -360,7 +360,7 @@ class AuditLogIterator(LazyIterator[audit_logs.AuditLog]): def __init__( self, - app: rest_app.IRESTApp, + app: app_.IRESTApp, request_call: typing.Callable[..., more_typing.Coroutine[more_typing.JSONObject]], guild_id: str, before: str, diff --git a/hikari/net/rest.py b/hikari/net/rest.py index 5560074af3..25f339ec03 100644 --- a/hikari/net/rest.py +++ b/hikari/net/rest.py @@ -29,9 +29,9 @@ import aiohttp +from hikari import component from hikari import errors from hikari import http_settings -from hikari import rest_app from hikari.internal import class_helpers from hikari.internal import conversions from hikari.internal import more_collections @@ -45,6 +45,8 @@ from hikari.net import routes if typing.TYPE_CHECKING: + from hikari import app as app_ + from hikari.models import applications from hikari.models import audit_logs from hikari.models import bases @@ -63,7 +65,7 @@ from hikari.models import webhooks -class REST(http_client.HTTPClient): +class REST(http_client.HTTPClient, component.IComponent): """Implementation of the V6 and V7-compatible Discord REST API. This manages making HTTP/1.1 requests to the API and using the entity @@ -103,7 +105,7 @@ class _RateLimited(RuntimeError): def __init__( self, *, - app: rest_app.IRESTApp, + app: app_.IRESTApp, config: http_settings.HTTPSettings, debug: bool = False, token: typing.Optional[str], @@ -132,6 +134,10 @@ def __init__( self._token = f"{token_type.title()} {token}" if token is not None else None self._url = url.format(self) + @property + def app(self) -> rest_app.IRESTApp: + return self._app + async def _request( self, compiled_route: routes.CompiledRoute, diff --git a/hikari/net/rest_utils.py b/hikari/net/rest_utils.py index dd5eb22806..e009c39cbc 100644 --- a/hikari/net/rest_utils.py +++ b/hikari/net/rest_utils.py @@ -33,7 +33,7 @@ import attr -from hikari import rest_app +from hikari import app as app_ from hikari.internal import conversions from hikari.internal import unset from hikari.models import bases @@ -88,7 +88,7 @@ async def _keep_typing(self) -> None: @attr.s(auto_attribs=True, kw_only=True, slots=True) class GuildBuilder: - _app: rest_app.IRESTApp + _app: app_.IRESTApp _channels: typing.MutableSequence[more_typing.JSONObject] = attr.ib(factory=list) _counter: int = 0 _name: typing.Union[unset.Unset, str] diff --git a/hikari/net/voice_gateway.py b/hikari/net/voice_gateway.py index e2e12c38cf..ab5f2ad328 100644 --- a/hikari/net/voice_gateway.py +++ b/hikari/net/voice_gateway.py @@ -250,16 +250,14 @@ async def _poll_events(self): if op == self._GatewayOpcode.READY: self.logger.debug( - "voice websocket is ready [session_id:%s, url:%s]", - self._session_id, - self._url, + "voice websocket is ready [session_id:%s, url:%s]", self._session_id, self._url, ) elif op == self._GatewayOpcode.RESUMED: self.logger.debug( "voice websocket has resumed [session_id:%s, nonce:%s, url:%s]", self._session_id, self._nonce, - self._url + self._url, ) elif op == self._GatewayOpcode.HEARTBEAT: self.logger.debug("received HEARTBEAT; sending HEARTBEAT ACK") diff --git a/tests/hikari/models/test_applications.py b/tests/hikari/models/test_applications.py index 5bc8618433..a6bff6c714 100644 --- a/tests/hikari/models/test_applications.py +++ b/tests/hikari/models/test_applications.py @@ -19,7 +19,7 @@ import mock import pytest -from hikari import base_app +from hikari import app from hikari.net import urls from hikari.models import applications from hikari.models import guilds @@ -29,7 +29,7 @@ @pytest.fixture() def mock_app(): - return mock.MagicMock(base_app.IBaseApp) + return mock.MagicMock(app.IApp) @pytest.fixture() From d2b422c4fa27ff38c196324288cef47dd70dbbf2 Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Thu, 28 May 2020 15:50:14 +0100 Subject: [PATCH 404/922] Added autosharding support back in. --- hikari/app.py | 12 +++- hikari/impl/bot.py | 6 +- hikari/impl/entity_factory.py | 14 ++++- hikari/impl/event_manager.py | 2 +- hikari/impl/gateway_zookeeper.py | 96 ++++++++++++++++++++++++-------- hikari/models/gateway.py | 33 ++++------- hikari/net/rest.py | 4 +- 7 files changed, 112 insertions(+), 55 deletions(-) diff --git a/hikari/app.py b/hikari/app.py index a47674d4be..2664a81c06 100644 --- a/hikari/app.py +++ b/hikari/app.py @@ -136,12 +136,20 @@ class IGatewayZookeeper(IGatewayConsumer, abc.ABC): @property @abc.abstractmethod def gateway_shards(self) -> typing.Mapping[int, gateway.Gateway]: - """Mapping of each shard ID to the corresponding client for it.""" + """Mapping of each shard ID to the corresponding client for it. + + If the shards have not started, and auto=sharding is in-place, then it + is acceptable for this to return an empty mapping. + """ @property @abc.abstractmethod def gateway_shard_count(self) -> int: - """The number of shards in the entire distributed application.""" + """The number of shards in the entire distributed application. + + If the shards have not started, and auto=sharding is in-place, then it + is acceptable for this to return `0`. + """ @abc.abstractmethod async def start(self) -> None: diff --git a/hikari/impl/bot.py b/hikari/impl/bot.py index 9e7f79bc43..79e0df17ec 100644 --- a/hikari/impl/bot.py +++ b/hikari/impl/bot.py @@ -29,6 +29,7 @@ from hikari.impl import event_manager from hikari.impl import gateway_zookeeper from hikari.internal import class_helpers +from hikari.models import gateway as gateway_models from hikari.models import guilds from hikari.net import gateway from hikari.net import rest @@ -51,7 +52,6 @@ def __init__( *, config: http_settings_.HTTPSettings, debug: bool = False, - gateway_url: str, gateway_version: int = 6, initial_activity: typing.Optional[gateway.Activity] = None, initial_idle_since: typing.Optional[datetime.datetime] = None, @@ -89,7 +89,6 @@ def __init__( shard_ids=shard_ids, shard_count=shard_count, token=token, - url=gateway_url, use_compression=use_compression, version=gateway_version, ) @@ -130,3 +129,6 @@ def http_settings(self) -> http_settings_.HTTPSettings: async def close(self) -> None: await super().close() await self._rest.close() + + async def _fetch_gateway_recommendations(self) -> gateway_models.GatewayBot: + return await self.rest.fetch_recommended_gateway_settings() diff --git a/hikari/impl/entity_factory.py b/hikari/impl/entity_factory.py index 782c2d3ae2..0f00128637 100644 --- a/hikari/impl/entity_factory.py +++ b/hikari/impl/entity_factory.py @@ -22,9 +22,11 @@ __all__ = ["EntityFactoryImpl"] +import datetime import typing from hikari import entity_factory +from hikari.models import gateway if typing.TYPE_CHECKING: from hikari import app as app_ @@ -34,7 +36,6 @@ from hikari.models import channels from hikari.models import embeds from hikari.models import emojis - from hikari.models import gateway from hikari.models import guilds from hikari.models import invites from hikari.models import users @@ -116,7 +117,16 @@ def deserialize_emoji( pass def deserialize_gateway_bot(self, payload: more_typing.JSONObject) -> gateway.GatewayBot: - pass + gateway_bot = gateway.GatewayBot() + gateway_bot.url = payload["url"] + gateway_bot.shard_count = int(payload["shards"]) + session_start_limit_payload = payload["session_start_limit"] + session_start_limit = gateway.SessionStartLimit() + session_start_limit.total = int(session_start_limit_payload["total"]) + session_start_limit.remaining = int(session_start_limit_payload["remaining"]) + session_start_limit.reset_after = datetime.timedelta(milliseconds=session_start_limit_payload["reset_after"]) + gateway_bot.session_start_limit = session_start_limit + return gateway_bot def deserialize_guild_widget(self, payload: more_typing.JSONObject) -> guilds.GuildWidget: pass diff --git a/hikari/impl/event_manager.py b/hikari/impl/event_manager.py index 8d48c36417..76ad45ebc3 100644 --- a/hikari/impl/event_manager.py +++ b/hikari/impl/event_manager.py @@ -53,7 +53,7 @@ def dispatch(self, event: base.HikariEvent) -> more_typing.Future[typing.Any]: async def consume_raw_event(self, shard: gateway.Gateway, event_name: str, payload: more_typing.JSONType) -> None: try: - callback = getattr(self, "on_" + event_name.lower()) + callback = getattr(self, "_on_" + event_name.lower()) await callback(shard, payload) except AttributeError: self.logger.debug("ignoring unknown event %s", event_name) diff --git a/hikari/impl/gateway_zookeeper.py b/hikari/impl/gateway_zookeeper.py index fb3b4dfb8c..49e00f98e2 100644 --- a/hikari/impl/gateway_zookeeper.py +++ b/hikari/impl/gateway_zookeeper.py @@ -23,10 +23,15 @@ import abc import asyncio import contextlib +import datetime +import inspect +import os +import platform import signal import time import typing +from hikari import _about from hikari import app as app_ from hikari import event_dispatcher from hikari.events import other @@ -35,10 +40,8 @@ from hikari.net import gateway if typing.TYPE_CHECKING: - import datetime - from hikari import http_settings - from hikari.models import gateway + from hikari.models import gateway as gateway_models from hikari.models import guilds from hikari.models import intents as intents_ @@ -58,35 +61,26 @@ def __init__( shard_ids: typing.Set[int], shard_count: int, token: str, - url: str, use_compression: bool, version: int, ) -> None: self._aiohttp_config = config + self._debug = debug self._gather_task = None + self._initial_activity = initial_activity + self._initial_idle_since = initial_idle_since + self._initial_is_afk = initial_is_afk + self._initial_status = initial_status + self._intents = intents + self._large_threshold = large_threshold self._request_close_event = asyncio.Event() self._shard_count = shard_count - self._shards = { - shard_id: gateway.Gateway( - app=self, - config=config, - debug=debug, - initial_activity=initial_activity, - initial_idle_since=initial_idle_since, - initial_is_afk=initial_is_afk, - initial_status=initial_status, - intents=intents, - large_threshold=large_threshold, - shard_id=shard_id, - shard_count=shard_count, - token=token, - url=url, - use_compression=use_compression, - version=version, - ) - for shard_id in shard_ids - } + self._shard_ids = shard_ids + self._shards = {} self._tasks = {} + self._token = token + self._use_compression = use_compression + self._version = version @property def gateway_shards(self) -> typing.Mapping[int, gateway.Gateway]: @@ -100,6 +94,8 @@ async def start(self) -> None: self._tasks.clear() self._gather_task = None + await self._init() + self._request_close_event.clear() self.logger.info("starting %s", conversions.pluralize(len(self._shards), "shard")) @@ -222,3 +218,55 @@ async def update_presence( ) await asyncio.gather(*coros) + + async def _init(self): + version = _about.__version__ + path = os.path.abspath(os.path.dirname(inspect.getsourcefile(_about))) + py_impl = platform.python_implementation() + py_ver = platform.python_version() + py_compiler = platform.python_compiler() + self.logger.info( + "hikari v%s (installed in %s) (%s %s %s)", version, path, py_impl, py_ver, py_compiler, + ) + + gw_recs = await self._fetch_gateway_recommendations() + + self.logger.info( + "you have sent an IDENTIFY %s time(s) before now, and have %s remaining. This will reset at %s.", + gw_recs.session_start_limit.total - gw_recs.session_start_limit.remaining, + gw_recs.session_start_limit.remaining, + datetime.datetime.now() + gw_recs.session_start_limit.reset_after, + ) + + self._shard_count = self._shard_count if self._shard_count else gw_recs.shard_count + self._shard_ids = self._shard_ids if self._shard_ids else range(self._shard_count) + url = gw_recs.url + + self.logger.info("will connect shards to %s", url) + + shard_clients: typing.Dict[int, gateway.Gateway] = {} + for shard_id in self._shard_ids: + shard = gateway.Gateway( + app=self, + config=self._aiohttp_config, + debug=self._debug, + initial_activity=self._initial_activity, + initial_idle_since=self._initial_idle_since, + initial_is_afk=self._initial_is_afk, + initial_status=self._initial_status, + intents=self._intents, + large_threshold=self._large_threshold, + shard_id=shard_id, + shard_count=self._shard_count, + token=self._token, + url=url, + use_compression=self._use_compression, + version=self._version, + ) + shard_clients[shard_id] = shard + + self._shards = shard_clients # pylint: disable=attribute-defined-outside-init + + @abc.abstractmethod + async def _fetch_gateway_recommendations(self) -> gateway_models.GatewayBot: + ... diff --git a/hikari/models/gateway.py b/hikari/models/gateway.py index cea4bcc46c..1b0ad4611c 100644 --- a/hikari/models/gateway.py +++ b/hikari/models/gateway.py @@ -23,53 +23,40 @@ __all__ = ["GatewayBot", "SessionStartLimit"] import datetime -import typing import attr -from hikari.internal import marshaller -from . import bases -from . import guilds - def _rest_after_deserializer(after: int) -> datetime.timedelta: return datetime.timedelta(milliseconds=after) -@marshaller.marshallable() -@attr.s(eq=True, hash=False, kw_only=True, slots=True) -class SessionStartLimit(bases.Entity, marshaller.Deserializable): +@attr.s(eq=True, hash=False, init=False, kw_only=True, slots=True) +class SessionStartLimit: """Used to represent information about the current session start limits.""" - total: int = marshaller.attrib(deserializer=int, repr=True) + total: int = attr.ib(repr=True) """The total number of session starts the current bot is allowed.""" - remaining: int = marshaller.attrib(deserializer=int, repr=True) + remaining: int = attr.ib(repr=True) """The remaining number of session starts this bot has.""" - reset_after: datetime.timedelta = marshaller.attrib(deserializer=_rest_after_deserializer, repr=True) + reset_after: datetime.timedelta = attr.ib(repr=True) """When `SessionStartLimit.remaining` will reset for the current bot. After it resets it will be set to `SessionStartLimit.total`. """ -@marshaller.marshallable() -@attr.s(eq=True, hash=False, kw_only=True, slots=True) -class GatewayBot(bases.Entity, marshaller.Deserializable): +@attr.s(eq=True, hash=False, init=False, kw_only=True, slots=True) +class GatewayBot: """Used to represent gateway information for the connected bot.""" - url: str = marshaller.attrib(deserializer=str, repr=True) + url: str = attr.ib(repr=True) """The WSS URL that can be used for connecting to the gateway.""" - shard_count: int = marshaller.attrib(raw_name="shards", deserializer=int, repr=True) + shard_count: int = attr.ib(repr=True) """The recommended number of shards to use when connecting to the gateway.""" - session_start_limit: SessionStartLimit = marshaller.attrib( - deserializer=SessionStartLimit.deserialize, inherit_kwargs=True, repr=True - ) + session_start_limit: SessionStartLimit = attr.ib(repr=True) """Information about the bot's current session start limit.""" - - -def _undefined_type_default() -> typing.Literal[guilds.ActivityType.PLAYING]: - return guilds.ActivityType.PLAYING diff --git a/hikari/net/rest.py b/hikari/net/rest.py index 25f339ec03..23d35686e8 100644 --- a/hikari/net/rest.py +++ b/hikari/net/rest.py @@ -128,6 +128,8 @@ def __init__( ) self.buckets = buckets.RESTBucketManager() self.global_rate_limit = ratelimits.ManualRateLimiter() + self._invalid_requests = 0 + self._invalid_request_window = -float("inf") self.version = version self._app = app @@ -135,7 +137,7 @@ def __init__( self._url = url.format(self) @property - def app(self) -> rest_app.IRESTApp: + def app(self) -> app_.IRESTApp: return self._app async def _request( From 5df08fa4dabd93de00c1bec3d2cc2c2f2193d827 Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Thu, 28 May 2020 15:54:35 +0100 Subject: [PATCH 405/922] Made Singleton support slotting. --- hikari/internal/class_helpers.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/hikari/internal/class_helpers.py b/hikari/internal/class_helpers.py index 2667a47e2b..fb0d51e5d2 100644 --- a/hikari/internal/class_helpers.py +++ b/hikari/internal/class_helpers.py @@ -22,8 +22,6 @@ __all__ = ["SingletonMeta", "Singleton"] -import abc -import inspect import logging import typing @@ -74,10 +72,12 @@ class SingletonMeta(type): thread safe. """ - ___instance_dict_t___ = more_collections.WeakKeyDictionary[typing.Type[typing.Any], typing.Any] - ___instances___: ___instance_dict_t___ = more_collections.WeakKeyDictionary() __slots__ = () + ___instances___: typing.Final[ + more_collections.WeakKeyDictionary[typing.Type[typing.Any], typing.Any] + ] = more_collections.WeakKeyDictionary() + def __call__(cls): if cls not in SingletonMeta.___instances___: SingletonMeta.___instances___[cls] = super().__call__() @@ -105,3 +105,5 @@ class Singleton(metaclass=SingletonMeta): Constructing instances of this class or derived classes may not be thread safe. """ + + __slots__ = () From 2084a6e3900f05e4f5a9b551e4647779e7d91644 Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Thu, 28 May 2020 21:25:13 +0100 Subject: [PATCH 406/922] Event manager implementation. --- hikari/impl/bot.py | 1 + hikari/impl/event_manager.py | 65 ++---------- hikari/impl/event_manager_core.py | 167 ++++++++++++++++++++++++++++++ 3 files changed, 175 insertions(+), 58 deletions(-) create mode 100644 hikari/impl/event_manager_core.py diff --git a/hikari/impl/bot.py b/hikari/impl/bot.py index 79e0df17ec..a7b8472643 100644 --- a/hikari/impl/bot.py +++ b/hikari/impl/bot.py @@ -27,6 +27,7 @@ from hikari.impl import cache as cache_impl from hikari.impl import entity_factory as entity_factory_impl from hikari.impl import event_manager +from hikari.impl import event_manager_core from hikari.impl import gateway_zookeeper from hikari.internal import class_helpers from hikari.models import gateway as gateway_models diff --git a/hikari/impl/event_manager.py b/hikari/impl/event_manager.py index 76ad45ebc3..4eb4e22b99 100644 --- a/hikari/impl/event_manager.py +++ b/hikari/impl/event_manager.py @@ -16,66 +16,15 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . -from __future__ import annotations +"""Event handling logic.""" -__all__ = ["EventManagerImpl"] -import typing - -from hikari import event_consumer -from hikari import event_dispatcher -from hikari.events import base -from hikari.internal import class_helpers -from hikari.internal import more_asyncio -from hikari.internal import unset +from hikari.impl import event_manager_core +from hikari.internal import more_typing from hikari.net import gateway -if typing.TYPE_CHECKING: - from hikari import app as app_ - from hikari.internal import more_typing - - _EventT = typing.TypeVar("_EventT", bound=base.HikariEvent, covariant=True) - _PredicateT = typing.Callable[[_EventT], typing.Union[bool, more_typing.Coroutine[bool]]] - - -class EventManagerImpl(event_dispatcher.IEventDispatcher, event_consumer.IEventConsumer): - def __init__(self, app: app_.IApp) -> None: - self._app = app - self.logger = class_helpers.get_logger(self) - - @property - def app(self) -> app_.IApp: - return self._app - - def dispatch(self, event: base.HikariEvent) -> more_typing.Future[typing.Any]: - # TODO: this - return more_asyncio.completed_future() - - async def consume_raw_event(self, shard: gateway.Gateway, event_name: str, payload: more_typing.JSONType) -> None: - try: - callback = getattr(self, "_on_" + event_name.lower()) - await callback(shard, payload) - except AttributeError: - self.logger.debug("ignoring unknown event %s", event_name) - - def subscribe( - self, - event_type: typing.Type[_EventT], - callback: typing.Callable[[_EventT], typing.Union[more_typing.Coroutine[None], None]], - ) -> None: - pass - - def unsubscribe( - self, - event_type: typing.Type[_EventT], - callback: typing.Callable[[_EventT], typing.Union[more_typing.Coroutine[None], None]], - ) -> None: - pass - - def listen(self, event_type: typing.Union[unset.Unset, typing.Type[_EventT]]) -> None: - pass - async def wait_for( - self, event_type: typing.Type[_EventT], predicate: _PredicateT, timeout: typing.Union[float, int, None] - ) -> _EventT: - pass +class EventManagerImpl(event_manager_core.EventManagerCore): + """Provides event handling logic for Discord events.""" + async def _on_message_create(self, shard: gateway.Gateway, payload: more_typing.JSONType) -> None: + print(shard, payload) diff --git a/hikari/impl/event_manager_core.py b/hikari/impl/event_manager_core.py new file mode 100644 index 0000000000..b0eb4819a3 --- /dev/null +++ b/hikari/impl/event_manager_core.py @@ -0,0 +1,167 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# Copyright © Nekoka.tt 2019-2020 +# +# This file is part of Hikari. +# +# Hikari is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Hikari is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Hikari. If not, see . +"""A base implementation for an event manager.""" +from __future__ import annotations + +__all__ = ["EventManagerCore"] + +import asyncio +import typing + +from hikari import event_consumer +from hikari import event_dispatcher +from hikari.events import base +from hikari.events import other +from hikari.internal import class_helpers +from hikari.internal import more_asyncio +from hikari.net import gateway + +if typing.TYPE_CHECKING: + from hikari import app as app_ + from hikari.internal import more_typing + + _EventT = typing.TypeVar("_EventT", bound=base.HikariEvent, covariant=True) + _PredicateT = typing.Callable[[_EventT], typing.Union[bool, more_typing.Coroutine[bool]]] + _SyncCallbackT = typing.Callable[[_EventT], None] + _AsyncCallbackT = typing.Callable[[_EventT], more_typing.Coroutine[None]] + _CallbackT = typing.Union[_SyncCallbackT, _AsyncCallbackT] + _ListenerMapT = typing.MutableMapping[typing.Type[_EventT], typing.MutableSequence[_CallbackT]] + _WaiterT = typing.Tuple[_PredicateT, more_typing.Future[_EventT]] + _WaiterMapT = typing.MutableMapping[typing.Type[_EventT], typing.MutableSet[_WaiterT]] + + +class EventManagerCore(event_dispatcher.IEventDispatcher, event_consumer.IEventConsumer): + """Provides functionality to consume and dispatch events. + + Specific event handlers should be in functions named `_on_xxx` where `xxx` + is the raw event name being dispatched in lower-case. + """ + def __init__(self, app: app_.IApp) -> None: + self._app = app + self._listeners: _ListenerMapT = {} + self._waiters: _WaiterMapT = {} + self.logger = class_helpers.get_logger(self) + + @property + def app(self) -> app_.IApp: + return self._app + + def dispatch(self, event: base.HikariEvent) -> more_typing.Future[typing.Any]: + if not isinstance(event, base.HikariEvent): + raise TypeError(f"events must be subclasses of HikariEvent, not {type(event).__name__}") + + # We only need to iterate through the MRO until we hit HikariEvent, as + # anything after that is random garbage we don't care about, as they do + # not describe event types. This improves efficiency as well. + mro = type(event).mro() + + tasks = [] + + for cls in mro[:mro.index(base.HikariEvent) + 1]: + cls: typing.Type[_EventT] + + if cls in self._listeners: + for callback in self._listeners[cls]: + tasks.append(self._invoke_callback(callback, event)) + + if cls in self._waiters: + for predicate, future in self._waiters[cls]: + tasks.append(self._test_waiter(cls, event, predicate, future)) + + return asyncio.gather(*tasks) if tasks else more_asyncio.completed_future() + + async def consume_raw_event(self, shard: gateway.Gateway, event_name: str, payload: more_typing.JSONType) -> None: + try: + callback = getattr(self, "_on_" + event_name.lower()) + await callback(shard, payload) + except AttributeError: + self.logger.debug("ignoring unknown event %s", event_name) + + def subscribe( + self, + event_type: typing.Type[_EventT], + callback: typing.Callable[[_EventT], typing.Union[more_typing.Coroutine[None], None]], + ) -> None: + if event_type not in self._listeners: + self._listeners[event_type] = [] + + if not asyncio.iscoroutinefunction(callback): + async def wrapper(event): + return callback(event) + + self._listeners[event_type].append(wrapper) + + def unsubscribe( + self, + event_type: typing.Type[_EventT], + callback: typing.Callable[[_EventT], typing.Union[more_typing.Coroutine[None], None]], + ) -> None: + if event_type in self._listeners: + self._listeners[event_type].remove(callback) + if not self._listeners[event_type]: + del self._listeners[event_type] + + def listen(self, event_type: typing.Type[_EventT]) -> typing.Callable[[_CallbackT], _CallbackT]: + def decorator(callback: _CallbackT) -> _CallbackT: + self.subscribe(event_type, callback) + return callback + return decorator + + async def _test_waiter(self, cls, event, predicate, future): + try: + result = predicate(event) + if asyncio.iscoroutinefunction(result): + result = await result + + if not result: + return + + except Exception as ex: + future.set_exception(ex) + else: + future.set_result(event) + + self._waiters[cls].remove((predicate, future)) + if not self._waiters[cls]: + del self._waiters[cls] + + async def _invoke_callback(self, callback: _CallbackT, event: _EventT) -> None: + try: + result = callback(event) + if asyncio.iscoroutine(result): + await result + + except Exception as ex: + if base.is_no_catch_event(event): + self.logger.error("an exception occurred handling an event, but it has been ignored", exc_info=ex) + else: + await self.dispatch(other.ExceptionEvent(app=self._app, exception=ex, event=event, callback=callback)) + + async def wait_for( + self, event_type: typing.Type[_EventT], predicate: _PredicateT, timeout: typing.Union[float, int, None] + ) -> _EventT: + + future = asyncio.get_event_loop().create_future() + + if event_type not in self._waiters: + self._waiters[event_type] = set() + + self._waiters[event_type].add((predicate, future)) + + return await asyncio.wait_for(future, timeout=timeout) if timeout is not None else await future From 098a20a71a6475e0d04d018b66d4d3c01505336f Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Fri, 29 May 2020 19:26:11 +0100 Subject: [PATCH 407/922] Tidied some tests, removed a large amount of boilerplate and unneeded code. --- ci/gitlab/tests.yml | 2 +- coverage.ini | 2 - hikari/cache.py | 75 +- hikari/entity_factory.py | 87 +- hikari/event_consumer.py | 4 +- hikari/event_dispatcher.py | 14 +- hikari/events/base.py | 5 +- hikari/events/channel.py | 98 +- hikari/events/guild.py | 61 +- hikari/events/message.py | 115 +- hikari/events/other.py | 15 +- hikari/events/voice.py | 9 +- hikari/impl/bot.py | 5 +- hikari/impl/cache.py | 75 +- hikari/impl/entity_factory.py | 84 +- hikari/impl/event_manager.py | 8 +- hikari/impl/event_manager_core.py | 29 +- hikari/impl/gateway_zookeeper.py | 6 +- hikari/impl/rest_app.py | 4 +- hikari/internal/marshaller.py | 500 -------- hikari/internal/more_asyncio.py | 78 -- hikari/internal/more_collections.py | 76 -- hikari/internal/more_enums.py | 62 - hikari/internal/more_typing.py | 207 --- hikari/models/applications.py | 2 +- hikari/models/audit_logs.py | 2 +- hikari/models/bases.py | 1 - hikari/models/channels.py | 2 +- hikari/models/embeds.py | 2 +- hikari/models/emojis.py | 2 +- hikari/models/files.py | 2 +- hikari/models/guilds.py | 2 +- hikari/models/invites.py | 2 +- hikari/models/messages.py | 2 +- hikari/models/users.py | 2 +- hikari/models/voices.py | 2 +- hikari/models/webhooks.py | 2 +- hikari/net/buckets.py | 13 +- hikari/net/gateway.py | 49 +- hikari/net/iterators.py | 77 +- hikari/{internal => net}/ratelimits.py | 17 +- hikari/net/rest.py | 991 +++++++------- hikari/net/rest_utils.py | 135 +- hikari/net/routes.py | 8 +- hikari/net/user_agents.py | 4 +- hikari/net/voice_gateway.py | 36 +- hikari/{internal => utilities}/__init__.py | 0 hikari/utilities/aio.py | 177 +++ hikari/utilities/binding.py | 260 ++++ .../conversions.py => utilities/date.py} | 145 +-- .../class_helpers.py => utilities/klass.py} | 8 +- hikari/utilities/reflect.py | 65 + hikari/utilities/snowflake.py | 91 ++ hikari/{internal => utilities}/unset.py | 4 +- setup.py | 2 +- tests/hikari/_helpers.py | 1 - .../{test_more_asyncio.py => test_aio.py} | 63 +- tests/hikari/internal/test_conversions.py | 10 +- .../{test_class_helpers.py => test_klass.py} | 8 +- .../hikari/internal/test_more_collections.py | 74 -- tests/hikari/internal/test_unset.py | 2 +- tests/hikari/models/test_applications.py | 300 ----- tests/hikari/models/test_audit_logs.py | 612 --------- tests/hikari/models/test_bases.py | 140 -- tests/hikari/models/test_channels.py | 450 ------- tests/hikari/models/test_colors.py | 213 ---- tests/hikari/models/test_embeds.py | 538 -------- tests/hikari/models/test_emojis.py | 254 ---- tests/hikari/models/test_files.py | 416 ------ tests/hikari/models/test_guilds.py | 1134 ----------------- tests/hikari/models/test_invites.py | 296 ----- tests/hikari/models/test_messages.py | 552 -------- tests/hikari/models/test_users.py | 175 --- tests/hikari/models/test_voices.py | 73 -- tests/hikari/models/test_webhook.py | 369 ------ .../__init__.py => net/test_buckets.py} | 0 .../test_colours.py => net/test_gateway.py} | 9 - tests/hikari/net/test_http_client.py | 8 +- .../test_intents.py => net/test_iterators.py} | 9 - .../{internal => net}/test_ratelimits.py | 2 +- .../test_more_enums.py => net/test_rest.py} | 29 - .../test_rest_utils.py} | 27 - tests/hikari/net/test_routes.py | 18 + tests/hikari/net/test_tracing.py | 18 + tests/hikari/{internal => net}/test_urls.py | 0 .../{internal => net}/test_user_agents.py | 0 86 files changed, 1673 insertions(+), 7855 deletions(-) delete mode 100644 hikari/internal/marshaller.py delete mode 100644 hikari/internal/more_asyncio.py delete mode 100644 hikari/internal/more_collections.py delete mode 100644 hikari/internal/more_enums.py delete mode 100644 hikari/internal/more_typing.py rename hikari/{internal => net}/ratelimits.py (98%) rename hikari/{internal => utilities}/__init__.py (100%) create mode 100644 hikari/utilities/aio.py create mode 100644 hikari/utilities/binding.py rename hikari/{internal/conversions.py => utilities/date.py} (56%) rename hikari/{internal/class_helpers.py => utilities/klass.py} (92%) create mode 100644 hikari/utilities/reflect.py create mode 100644 hikari/utilities/snowflake.py rename hikari/{internal => utilities}/unset.py (96%) rename tests/hikari/internal/{test_more_asyncio.py => test_aio.py} (74%) rename tests/hikari/internal/{test_class_helpers.py => test_klass.py} (85%) delete mode 100644 tests/hikari/internal/test_more_collections.py delete mode 100644 tests/hikari/models/test_applications.py delete mode 100644 tests/hikari/models/test_audit_logs.py delete mode 100644 tests/hikari/models/test_bases.py delete mode 100644 tests/hikari/models/test_channels.py delete mode 100644 tests/hikari/models/test_colors.py delete mode 100644 tests/hikari/models/test_embeds.py delete mode 100644 tests/hikari/models/test_emojis.py delete mode 100644 tests/hikari/models/test_files.py delete mode 100644 tests/hikari/models/test_guilds.py delete mode 100644 tests/hikari/models/test_invites.py delete mode 100644 tests/hikari/models/test_messages.py delete mode 100644 tests/hikari/models/test_users.py delete mode 100644 tests/hikari/models/test_voices.py delete mode 100644 tests/hikari/models/test_webhook.py rename tests/hikari/{models/__init__.py => net/test_buckets.py} (100%) rename tests/hikari/{models/test_colours.py => net/test_gateway.py} (82%) rename tests/hikari/{models/test_intents.py => net/test_iterators.py} (74%) rename tests/hikari/{internal => net}/test_ratelimits.py (99%) rename tests/hikari/{internal/test_more_enums.py => net/test_rest.py} (58%) rename tests/hikari/{internal/test_more_typing.py => net/test_rest_utils.py} (55%) create mode 100644 tests/hikari/net/test_routes.py create mode 100644 tests/hikari/net/test_tracing.py rename tests/hikari/{internal => net}/test_urls.py (100%) rename tests/hikari/{internal => net}/test_user_agents.py (100%) diff --git a/ci/gitlab/tests.yml b/ci/gitlab/tests.yml index 627abfab24..cef1b58454 100644 --- a/ci/gitlab/tests.yml +++ b/ci/gitlab/tests.yml @@ -109,7 +109,7 @@ test:twemoji_mapping: - if: "$CI_PIPELINE_SOURCE == 'schedule' && $TEST_TWEMOJI_MAPPING != null" - changes: - hikari/emojis.py - - hikari/internal/urls.py + - hikari.utilities/urls.py script: - |+ set -e diff --git a/coverage.ini b/coverage.ini index 725bd8899a..f2a0177384 100644 --- a/coverage.ini +++ b/coverage.ini @@ -7,9 +7,7 @@ source = hikari omit = hikari/__main__.py hikari/_about.py - hikari/clients/test.py hikari/net/tracing.py - hikari/internal/more_typing.py .nox/* [report] diff --git a/hikari/cache.py b/hikari/cache.py index b67f280bce..d24b3e46f9 100644 --- a/hikari/cache.py +++ b/hikari/cache.py @@ -27,7 +27,6 @@ from hikari import component if typing.TYPE_CHECKING: - from hikari.internal import more_typing from hikari.models import applications from hikari.models import audit_logs from hikari.models import channels @@ -40,6 +39,8 @@ from hikari.models import users from hikari.models import voices + from hikari.utilities import binding + class ICache(component.IComponent, abc.ABC): """Component that implements entity caching facilities. @@ -59,15 +60,15 @@ class ICache(component.IComponent, abc.ABC): # APPLICATIONS # ################ @abc.abstractmethod - async def create_application(self, payload: more_typing.JSONObject) -> applications.Application: + async def create_application(self, payload: binding.JSONObject) -> applications.Application: ... @abc.abstractmethod - async def create_own_guild(self, payload: more_typing.JSONObject) -> applications.OwnGuild: + async def create_own_guild(self, payload: binding.JSONObject) -> applications.OwnGuild: ... @abc.abstractmethod - async def create_own_connection(self, payload: more_typing.JSONObject) -> applications.OwnConnection: + async def create_own_connection(self, payload: binding.JSONObject) -> applications.OwnConnection: ... ############## @@ -75,19 +76,19 @@ async def create_own_connection(self, payload: more_typing.JSONObject) -> applic ############## @abc.abstractmethod - async def create_audit_log_change(self, payload: more_typing.JSONObject) -> audit_logs.AuditLogChange: + async def create_audit_log_change(self, payload: binding.JSONObject) -> audit_logs.AuditLogChange: ... @abc.abstractmethod - async def create_audit_log_entry_info(self, payload: more_typing.JSONObject) -> audit_logs.BaseAuditLogEntryInfo: + async def create_audit_log_entry_info(self, payload: binding.JSONObject) -> audit_logs.BaseAuditLogEntryInfo: ... @abc.abstractmethod - async def create_audit_log_entry(self, payload: more_typing.JSONObject) -> audit_logs.AuditLogEntry: + async def create_audit_log_entry(self, payload: binding.JSONObject) -> audit_logs.AuditLogEntry: ... @abc.abstractmethod - async def create_audit_log(self, payload: more_typing.JSONObject) -> audit_logs.AuditLog: + async def create_audit_log(self, payload: binding.JSONObject) -> audit_logs.AuditLog: ... ############ @@ -95,12 +96,12 @@ async def create_audit_log(self, payload: more_typing.JSONObject) -> audit_logs. ############ @abc.abstractmethod - async def create_channel(self, payload: more_typing.JSONObject, can_cache: bool = False) -> channels.PartialChannel: + async def create_channel(self, payload: binding.JSONObject, can_cache: bool = False) -> channels.PartialChannel: ... @abc.abstractmethod async def update_channel( - self, channel: channels.PartialChannel, payload: more_typing.JSONObject, + self, channel: channels.PartialChannel, payload: binding.JSONObject, ) -> channels.PartialChannel: ... @@ -117,7 +118,7 @@ async def delete_channel(self, channel_id: int) -> typing.Optional[channels.Part ########## @abc.abstractmethod - async def create_embed(self, payload: more_typing.JSONObject) -> embeds.Embed: + async def create_embed(self, payload: binding.JSONObject) -> embeds.Embed: ... ########## @@ -125,11 +126,11 @@ async def create_embed(self, payload: more_typing.JSONObject) -> embeds.Embed: ########## @abc.abstractmethod - async def create_emoji(self, payload: more_typing.JSONObject, can_cache: bool = False) -> emojis.Emoji: + async def create_emoji(self, payload: binding.JSONObject, can_cache: bool = False) -> emojis.Emoji: ... @abc.abstractmethod - async def update_emoji(self, payload: more_typing.JSONObject) -> emojis.Emoji: + async def update_emoji(self, payload: binding.JSONObject) -> emojis.Emoji: ... @abc.abstractmethod @@ -145,7 +146,7 @@ async def delete_emoji(self, emoji_id: int) -> typing.Optional[emojis.KnownCusto ########### @abc.abstractmethod - async def create_gateway_bot(self, payload: more_typing.JSONObject) -> gateway.GatewayBot: + async def create_gateway_bot(self, payload: binding.JSONObject) -> gateway.GatewayBot: ... ########## @@ -153,12 +154,12 @@ async def create_gateway_bot(self, payload: more_typing.JSONObject) -> gateway.G ########## @abc.abstractmethod - async def create_member(self, payload: more_typing.JSONObject, can_cache: bool = False) -> guilds.GuildMember: + async def create_member(self, payload: binding.JSONObject, can_cache: bool = False) -> guilds.GuildMember: # TODO: revisit for the voodoo to make a member into a special user. ... @abc.abstractmethod - async def update_member(self, member: guilds.GuildMember, payload: more_typing.JSONObject) -> guilds.GuildMember: + async def update_member(self, member: guilds.GuildMember, payload: binding.JSONObject) -> guilds.GuildMember: ... @abc.abstractmethod @@ -170,11 +171,11 @@ async def delete_member(self, guild_id: int, user_id: int) -> typing.Optional[gu ... @abc.abstractmethod - async def create_role(self, payload: more_typing.JSONObject, can_cache: bool = False) -> guilds.PartialRole: + async def create_role(self, payload: binding.JSONObject, can_cache: bool = False) -> guilds.PartialRole: ... @abc.abstractmethod - async def update_role(self, role: guilds.PartialRole, payload: more_typing.JSONObject) -> guilds.PartialRole: + async def update_role(self, role: guilds.PartialRole, payload: binding.JSONObject) -> guilds.PartialRole: ... @abc.abstractmethod @@ -186,14 +187,12 @@ async def delete_role(self, guild_id: int, role_id: int) -> typing.Optional[guil ... @abc.abstractmethod - async def create_presence( - self, payload: more_typing.JSONObject, can_cache: bool = False - ) -> guilds.GuildMemberPresence: + async def create_presence(self, payload: binding.JSONObject, can_cache: bool = False) -> guilds.GuildMemberPresence: ... @abc.abstractmethod async def update_presence( - self, role: guilds.GuildMemberPresence, payload: more_typing.JSONObject + self, role: guilds.GuildMemberPresence, payload: binding.JSONObject ) -> guilds.GuildMemberPresence: ... @@ -206,19 +205,19 @@ async def delete_presence(self, guild_id: int, user_id: int) -> typing.Optional[ ... @abc.abstractmethod - async def create_guild_ban(self, payload: more_typing.JSONObject) -> guilds.GuildMemberBan: + async def create_guild_ban(self, payload: binding.JSONObject) -> guilds.GuildMemberBan: ... @abc.abstractmethod - async def create_guild_integration(self, payload: more_typing.JSONObject) -> guilds.PartialGuildIntegration: + async def create_guild_integration(self, payload: binding.JSONObject) -> guilds.PartialGuildIntegration: ... @abc.abstractmethod - async def create_guild(self, payload: more_typing.JSONObject, can_cache: bool = False) -> guilds.PartialGuild: + async def create_guild(self, payload: binding.JSONObject, can_cache: bool = False) -> guilds.PartialGuild: ... @abc.abstractmethod - async def update_guild(self, guild: guilds.PartialGuild, payload: more_typing.JSONObject) -> guilds.PartialGuild: + async def update_guild(self, guild: guilds.PartialGuild, payload: binding.JSONObject) -> guilds.PartialGuild: ... @abc.abstractmethod @@ -230,29 +229,29 @@ async def delete_guild(self, guild_id: int) -> typing.Optional[guilds.PartialGui ... @abc.abstractmethod - async def create_guild_preview(self, payload: more_typing.JSONObject) -> guilds.GuildPreview: + async def create_guild_preview(self, payload: binding.JSONObject) -> guilds.GuildPreview: ... ########### # INVITES # ########### @abc.abstractmethod - async def create_invite(self, payload: more_typing.JSONObject) -> invites.Invite: + async def create_invite(self, payload: binding.JSONObject) -> invites.Invite: ... ############ # MESSAGES # ############ @abc.abstractmethod - async def create_reaction(self, payload: more_typing.JSONObject) -> messages.Reaction: + async def create_reaction(self, payload: binding.JSONObject) -> messages.Reaction: ... @abc.abstractmethod - async def create_message(self, payload: more_typing.JSONObject, can_cache: bool = False) -> messages.Message: + async def create_message(self, payload: binding.JSONObject, can_cache: bool = False) -> messages.Message: ... @abc.abstractmethod - async def update_message(self, message: messages.Message, payload: more_typing.JSONObject) -> messages.Message: + async def update_message(self, message: messages.Message, payload: binding.JSONObject) -> messages.Message: ... @abc.abstractmethod @@ -267,11 +266,11 @@ async def delete_message(self, channel_id: int, message_id: int) -> typing.Optio # USERS # ######### @abc.abstractmethod - async def create_user(self, payload: more_typing.JSONObject, can_cache: bool = False) -> users.User: + async def create_user(self, payload: binding.JSONObject, can_cache: bool = False) -> users.User: ... @abc.abstractmethod - async def update_user(self, user: users.User, payload: more_typing.JSONObject) -> users.User: + async def update_user(self, user: users.User, payload: binding.JSONObject) -> users.User: ... @abc.abstractmethod @@ -283,11 +282,11 @@ async def delete_user(self, user_id: int) -> typing.Optional[users.User]: ... @abc.abstractmethod - async def create_my_user(self, payload: more_typing.JSONObject, can_cache: bool = False) -> users.MyUser: + async def create_my_user(self, payload: binding.JSONObject, can_cache: bool = False) -> users.MyUser: ... @abc.abstractmethod - async def update_my_user(self, my_user: users.MyUser, payload: more_typing.JSONObject) -> users.MyUser: + async def update_my_user(self, my_user: users.MyUser, payload: binding.JSONObject) -> users.MyUser: ... @abc.abstractmethod @@ -298,11 +297,11 @@ async def get_my_user(self) -> typing.Optional[users.User]: # VOICES # ########## @abc.abstractmethod - async def create_voice_state(self, payload: more_typing.JSONObject, can_cache: bool = False) -> voices.VoiceState: + async def create_voice_state(self, payload: binding.JSONObject, can_cache: bool = False) -> voices.VoiceState: ... @abc.abstractmethod - async def update_voice_state(self, payload: more_typing.JSONObject) -> voices.VoiceState: + async def update_voice_state(self, payload: binding.JSONObject) -> voices.VoiceState: ... @abc.abstractmethod @@ -314,5 +313,5 @@ async def delete_voice_state(self, guild_id: int, channel_id: int) -> typing.Opt ... @abc.abstractmethod - async def create_voice_region(self, payload: more_typing.JSONObject) -> voices.VoiceRegion: + async def create_voice_region(self, payload: binding.JSONObject) -> voices.VoiceRegion: ... diff --git a/hikari/entity_factory.py b/hikari/entity_factory.py index e79c8b52f5..229b326491 100644 --- a/hikari/entity_factory.py +++ b/hikari/entity_factory.py @@ -27,7 +27,6 @@ from hikari import component if typing.TYPE_CHECKING: - from hikari.internal import more_typing from hikari.models import applications from hikari.models import audit_logs from hikari.models import channels @@ -41,6 +40,8 @@ from hikari.models import voices from hikari.models import webhooks + from hikari.utilities import binding + class IEntityFactory(component.IComponent, abc.ABC): """Component that will serialize and deserialize JSON payloads.""" @@ -52,15 +53,15 @@ class IEntityFactory(component.IComponent, abc.ABC): ################ @abc.abstractmethod - def deserialize_own_connection(self, payload: more_typing.JSONObject) -> applications.OwnConnection: + def deserialize_own_connection(self, payload: binding.JSONObject) -> applications.OwnConnection: ... @abc.abstractmethod - def deserialize_own_guild(self, payload: more_typing.JSONObject) -> applications.OwnGuild: + def deserialize_own_guild(self, payload: binding.JSONObject) -> applications.OwnGuild: ... @abc.abstractmethod - def deserialize_application(self, payload: more_typing.JSONObject) -> applications: + def deserialize_application(self, payload: binding.JSONObject) -> applications: ... ############## @@ -68,7 +69,7 @@ def deserialize_application(self, payload: more_typing.JSONObject) -> applicatio ############## @abc.abstractmethod - def deserialize_audit_log(self, payload: more_typing.JSONObject) -> audit_logs.AuditLog: + def deserialize_audit_log(self, payload: binding.JSONObject) -> audit_logs.AuditLog: ... ############ @@ -76,47 +77,47 @@ def deserialize_audit_log(self, payload: more_typing.JSONObject) -> audit_logs.A ############ @abc.abstractmethod - def deserialize_permission_overwrite(self, payload: more_typing.JSONObject) -> channels.PermissionOverwrite: + def deserialize_permission_overwrite(self, payload: binding.JSONObject) -> channels.PermissionOverwrite: ... @abc.abstractmethod - def serialize_permission_overwrite(self, overwrite: channels.PermissionOverwrite) -> more_typing.JSONObject: + def serialize_permission_overwrite(self, overwrite: channels.PermissionOverwrite) -> binding.JSONObject: ... @abc.abstractmethod - def deserialize_partial_channel(self, payload: more_typing.JSONObject) -> channels.PartialChannel: + def deserialize_partial_channel(self, payload: binding.JSONObject) -> channels.PartialChannel: ... @abc.abstractmethod - def deserialize_dm_channel(self, payload: more_typing.JSONObject) -> channels.DMChannel: + def deserialize_dm_channel(self, payload: binding.JSONObject) -> channels.DMChannel: ... @abc.abstractmethod - def deserialize_group_dm_channel(self, payload: more_typing.JSONObject) -> channels.GroupDMChannel: + def deserialize_group_dm_channel(self, payload: binding.JSONObject) -> channels.GroupDMChannel: ... @abc.abstractmethod - def deserialize_guild_category(self, payload: more_typing.JSONObject) -> channels.GuildCategory: + def deserialize_guild_category(self, payload: binding.JSONObject) -> channels.GuildCategory: ... @abc.abstractmethod - def deserialize_guild_text_channel(self, payload: more_typing.JSONObject) -> channels.GuildTextChannel: + def deserialize_guild_text_channel(self, payload: binding.JSONObject) -> channels.GuildTextChannel: ... @abc.abstractmethod - def deserialize_guild_news_channel(self, payload: more_typing.JSONObject) -> channels.GuildNewsChannel: + def deserialize_guild_news_channel(self, payload: binding.JSONObject) -> channels.GuildNewsChannel: ... @abc.abstractmethod - def deserialize_guild_store_channel(self, payload: more_typing.JSONObject) -> channels.GuildStoreChannel: + def deserialize_guild_store_channel(self, payload: binding.JSONObject) -> channels.GuildStoreChannel: ... @abc.abstractmethod - def deserialize_guild_voice_channel(self, payload: more_typing.JSONObject) -> channels.GuildVoiceChannel: + def deserialize_guild_voice_channel(self, payload: binding.JSONObject) -> channels.GuildVoiceChannel: ... @abc.abstractmethod - def deserialize_channel(self, payload: more_typing.JSONObject) -> channels.PartialChannel: + def deserialize_channel(self, payload: binding.JSONObject) -> channels.PartialChannel: ... ########## @@ -124,11 +125,11 @@ def deserialize_channel(self, payload: more_typing.JSONObject) -> channels.Parti ########## @abc.abstractmethod - def deserialize_embed(self, payload: more_typing.JSONObject) -> embeds.Embed: + def deserialize_embed(self, payload: binding.JSONObject) -> embeds.Embed: ... @abc.abstractmethod - def serialize_embed(self, embed: embeds.Embed) -> more_typing.JSONObject: + def serialize_embed(self, embed: embeds.Embed) -> binding.JSONObject: ... ########## @@ -136,21 +137,19 @@ def serialize_embed(self, embed: embeds.Embed) -> more_typing.JSONObject: ########## @abc.abstractmethod - def deserialize_unicode_emoji(self, payload: more_typing.JSONObject) -> emojis.UnicodeEmoji: + def deserialize_unicode_emoji(self, payload: binding.JSONObject) -> emojis.UnicodeEmoji: ... @abc.abstractmethod - def deserialize_custom_emoji(self, payload: more_typing.JSONObject) -> emojis.CustomEmoji: + def deserialize_custom_emoji(self, payload: binding.JSONObject) -> emojis.CustomEmoji: ... @abc.abstractmethod - def deserialize_known_custom_emoji(self, payload: more_typing.JSONObject) -> emojis.KnownCustomEmoji: + def deserialize_known_custom_emoji(self, payload: binding.JSONObject) -> emojis.KnownCustomEmoji: ... @abc.abstractmethod - def deserialize_emoji( - self, payload: more_typing.JSONObject - ) -> typing.Union[emojis.UnicodeEmoji, emojis.CustomEmoji]: + def deserialize_emoji(self, payload: binding.JSONObject) -> typing.Union[emojis.UnicodeEmoji, emojis.CustomEmoji]: ... ########### @@ -158,7 +157,7 @@ def deserialize_emoji( ########### @abc.abstractmethod - def deserialize_gateway_bot(self, payload: more_typing.JSONObject) -> gateway.GatewayBot: + def deserialize_gateway_bot(self, payload: binding.JSONObject) -> gateway.GatewayBot: ... ########## @@ -166,45 +165,45 @@ def deserialize_gateway_bot(self, payload: more_typing.JSONObject) -> gateway.Ga ########## @abc.abstractmethod - def deserialize_guild_widget(self, payload: more_typing.JSONObject) -> guilds.GuildWidget: + def deserialize_guild_widget(self, payload: binding.JSONObject) -> guilds.GuildWidget: ... @abc.abstractmethod def deserialize_guild_member( - self, payload: more_typing.JSONObject, *, user: typing.Optional[users.User] = None + self, payload: binding.JSONObject, *, user: typing.Optional[users.User] = None ) -> guilds.GuildMember: ... @abc.abstractmethod - def deserialize_role(self, payload: more_typing.JSONObject) -> guilds.Role: + def deserialize_role(self, payload: binding.JSONObject) -> guilds.Role: ... @abc.abstractmethod - def deserialize_guild_member_presence(self, payload: more_typing.JSONObject) -> guilds.GuildMemberPresence: + def deserialize_guild_member_presence(self, payload: binding.JSONObject) -> guilds.GuildMemberPresence: ... @abc.abstractmethod - def deserialize_partial_guild_integration(self, payload: more_typing.JSONObject) -> guilds.PartialGuildIntegration: + def deserialize_partial_guild_integration(self, payload: binding.JSONObject) -> guilds.PartialGuildIntegration: ... @abc.abstractmethod - def deserialize_guild_integration(self, payload: more_typing.JSONObject) -> guilds.GuildIntegration: + def deserialize_guild_integration(self, payload: binding.JSONObject) -> guilds.GuildIntegration: ... @abc.abstractmethod - def deserialize_guild_member_ban(self, payload: more_typing.JSONObject) -> guilds.GuildMemberBan: + def deserialize_guild_member_ban(self, payload: binding.JSONObject) -> guilds.GuildMemberBan: ... @abc.abstractmethod - def deserialize_unavailable_guild(self, payload: more_typing.JSONObject) -> guilds.UnavailableGuild: + def deserialize_unavailable_guild(self, payload: binding.JSONObject) -> guilds.UnavailableGuild: ... @abc.abstractmethod - def deserialize_guild_preview(self, payload: more_typing.JSONObject) -> guilds.GuildPreview: + def deserialize_guild_preview(self, payload: binding.JSONObject) -> guilds.GuildPreview: ... @abc.abstractmethod - def deserialize_guild(self, payload: more_typing.JSONObject) -> guilds.Guild: + def deserialize_guild(self, payload: binding.JSONObject) -> guilds.Guild: ... ########### @@ -212,22 +211,22 @@ def deserialize_guild(self, payload: more_typing.JSONObject) -> guilds.Guild: ########### @abc.abstractmethod - def deserialize_vanity_url(self, payload: more_typing.JSONObject) -> invites.VanityURL: + def deserialize_vanity_url(self, payload: binding.JSONObject) -> invites.VanityURL: ... @abc.abstractmethod - def deserialize_invite(self, payload: more_typing.JSONObject) -> invites.Invite: + def deserialize_invite(self, payload: binding.JSONObject) -> invites.Invite: ... @abc.abstractmethod - def deserialize_invite_with_metadata(self, payload: more_typing.JSONObject) -> invites.InviteWithMetadata: + def deserialize_invite_with_metadata(self, payload: binding.JSONObject) -> invites.InviteWithMetadata: ... ############ # MESSAGES # ############ - def deserialize_message(self, payload: more_typing.JSONObject) -> messages.Message: + def deserialize_message(self, payload: binding.JSONObject) -> messages.Message: ... ######### @@ -235,11 +234,11 @@ def deserialize_message(self, payload: more_typing.JSONObject) -> messages.Messa ######### @abc.abstractmethod - def deserialize_user(self, payload: more_typing.JSONObject) -> users.User: + def deserialize_user(self, payload: binding.JSONObject) -> users.User: ... @abc.abstractmethod - def deserialize_my_user(self, payload: more_typing.JSONObject) -> users.MyUser: + def deserialize_my_user(self, payload: binding.JSONObject) -> users.MyUser: ... ########## @@ -247,11 +246,11 @@ def deserialize_my_user(self, payload: more_typing.JSONObject) -> users.MyUser: ########## @abc.abstractmethod - def deserialize_voice_state(self, payload: more_typing.JSONObject) -> voices.VoiceState: + def deserialize_voice_state(self, payload: binding.JSONObject) -> voices.VoiceState: ... @abc.abstractmethod - def deserialize_voice_region(self, payload: more_typing.JSONObject) -> voices.VoiceRegion: + def deserialize_voice_region(self, payload: binding.JSONObject) -> voices.VoiceRegion: ... ############ @@ -259,5 +258,5 @@ def deserialize_voice_region(self, payload: more_typing.JSONObject) -> voices.Vo ############ @abc.abstractmethod - def deserialize_webhook(self, payload: more_typing.JSONObject) -> webhooks.Webhook: + def deserialize_webhook(self, payload: binding.JSONObject) -> webhooks.Webhook: ... diff --git a/hikari/event_consumer.py b/hikari/event_consumer.py index 90672d8ea0..9f21391ec9 100644 --- a/hikari/event_consumer.py +++ b/hikari/event_consumer.py @@ -26,8 +26,8 @@ from hikari import component if typing.TYPE_CHECKING: - from hikari.internal import more_typing from hikari.net import gateway + from hikari.utilities import binding class IEventConsumer(component.IComponent, abc.ABC): @@ -42,7 +42,7 @@ class IEventConsumer(component.IComponent, abc.ABC): __slots__ = () @abc.abstractmethod - async def consume_raw_event(self, shard: gateway.Gateway, event_name: str, payload: more_typing.JSONType) -> None: + async def consume_raw_event(self, shard: gateway.Gateway, event_name: str, payload: binding.JSONObject) -> None: """Process a raw event from a gateway shard and process it. Parameters diff --git a/hikari/event_dispatcher.py b/hikari/event_dispatcher.py index 57f629bea6..d3ce57116f 100644 --- a/hikari/event_dispatcher.py +++ b/hikari/event_dispatcher.py @@ -25,14 +25,14 @@ import typing from hikari import component -from hikari.internal import unset +from hikari.utilities import unset if typing.TYPE_CHECKING: from hikari.events import base - from hikari.internal import more_typing + from hikari.utilities import aio _EventT = typing.TypeVar("_EventT", bound=base.HikariEvent, covariant=True) - _PredicateT = typing.Callable[[_EventT], typing.Union[bool, more_typing.Coroutine[bool]]] + _PredicateT = typing.Callable[[_EventT], typing.Union[bool, typing.Coroutine[None, typing.Any, bool]]] class IEventDispatcher(component.IComponent, abc.ABC): @@ -46,7 +46,7 @@ class IEventDispatcher(component.IComponent, abc.ABC): __slots__ = () @abc.abstractmethod - def dispatch(self, event: base.HikariEvent) -> more_typing.Future[typing.Any]: + def dispatch(self, event: base.HikariEvent) -> aio.Future[typing.Any]: """Dispatch an event. Parameters @@ -67,7 +67,7 @@ def dispatch(self, event: base.HikariEvent) -> more_typing.Future[typing.Any]: def subscribe( self, event_type: typing.Type[_EventT], - callback: typing.Callable[[_EventT], typing.Union[more_typing.Coroutine[None], None]], + callback: typing.Callable[[_EventT], typing.Union[typing.Coroutine[None, typing.Any, None], None]], ) -> None: """Subscribe a given callback to a given event type. @@ -86,7 +86,7 @@ def subscribe( def unsubscribe( self, event_type: typing.Type[_EventT], - callback: typing.Callable[[_EventT], typing.Union[more_typing.Coroutine[None], None]], + callback: typing.Callable[[_EventT], typing.Union[typing.Coroutine[None, typing.Any, None], None]], ) -> None: """Unsubscribe a given callback from a given event type, if present. @@ -107,7 +107,7 @@ def listen(self, event_type: typing.Union[unset.Unset, typing.Type[_EventT]]) -> Parameters ---------- - event_type : hikari.internal.unset.Unset OR typing.Type[hikari.events.bases.HikariEvent] + event_type : hikari.utilities.unset.Unset OR typing.Type[hikari.events.bases.HikariEvent] The event type to subscribe to. The implementation may allow this to be unset. If this is the case, the event type will be inferred instead from the type hints on the function signature. diff --git a/hikari/events/base.py b/hikari/events/base.py index 2d4a804d2a..6f6e9f5365 100644 --- a/hikari/events/base.py +++ b/hikari/events/base.py @@ -27,8 +27,6 @@ import attr -from hikari.internal import marshaller -from hikari.internal import more_collections from hikari.models import bases as base_models if typing.TYPE_CHECKING: @@ -36,7 +34,6 @@ # Base event, is not deserialized -@marshaller.marshallable() @attr.s(eq=False, hash=False, kw_only=True, slots=True) class HikariEvent(base_models.Entity, abc.ABC): """The base class that all events inherit from.""" @@ -61,7 +58,7 @@ def get_required_intents_for(event_type: typing.Type[HikariEvent]) -> typing.Col Collection of acceptable subset combinations of intent needed to be able to receive the given event type. """ - return getattr(event_type, _REQUIRED_INTENTS_ATTR, more_collections.EMPTY_COLLECTION) + return getattr(event_type, _REQUIRED_INTENTS_ATTR, ()) def requires_intents( diff --git a/hikari/events/channel.py b/hikari/events/channel.py index f7dfd22879..8a68920e30 100644 --- a/hikari/events/channel.py +++ b/hikari/events/channel.py @@ -38,18 +38,17 @@ import attr -from hikari.internal import conversions -from hikari.internal import marshaller from hikari.models import bases as base_models from hikari.models import channels from hikari.models import guilds from hikari.models import intents from hikari.models import invites from hikari.models import users +from hikari.utilities import conversions from . import base as base_events if typing.TYPE_CHECKING: - from hikari.internal import more_typing + from hikari.utilities import more_typing def _overwrite_deserializer( @@ -72,20 +71,19 @@ def _recipients_deserializer( @base_events.requires_intents(intents.Intent.GUILDS) -@marshaller.marshallable() @attr.s(eq=False, hash=False, kw_only=True, slots=True) class BaseChannelEvent(base_events.HikariEvent, base_models.Unique, marshaller.Deserializable, abc.ABC): """A base object that Channel events will inherit from.""" - type: channels.ChannelType = marshaller.attrib(deserializer=channels.ChannelType, repr=True) + type: channels.ChannelType = attr.ib(deserializer=channels.ChannelType, repr=True) """The channel's type.""" - guild_id: typing.Optional[base_models.Snowflake] = marshaller.attrib( + guild_id: typing.Optional[base_models.Snowflake] = attr.ib( deserializer=base_models.Snowflake, if_undefined=None, default=None, repr=True ) """The ID of the guild this channel is in, will be `None` for DMs.""" - position: typing.Optional[int] = marshaller.attrib(deserializer=int, if_undefined=None, default=None) + position: typing.Optional[int] = attr.ib(deserializer=int, if_undefined=None, default=None) """The sorting position of this channel. This will be relative to the `BaseChannelEvent.parent_id` if set. @@ -93,32 +91,30 @@ class BaseChannelEvent(base_events.HikariEvent, base_models.Unique, marshaller.D permission_overwrites: typing.Optional[ typing.Mapping[base_models.Snowflake, channels.PermissionOverwrite] - ] = marshaller.attrib(deserializer=_overwrite_deserializer, if_undefined=None, default=None, inherit_kwargs=True) + ] = attr.ib(deserializer=_overwrite_deserializer, if_undefined=None, default=None, inherit_kwargs=True) """An mapping of the set permission overwrites for this channel, if applicable.""" - name: typing.Optional[str] = marshaller.attrib(deserializer=str, if_undefined=None, default=None, repr=True) + name: typing.Optional[str] = attr.ib(deserializer=str, if_undefined=None, default=None, repr=True) """The name of this channel, if applicable.""" - topic: typing.Optional[str] = marshaller.attrib(deserializer=str, if_undefined=None, if_none=None, default=None) + topic: typing.Optional[str] = attr.ib(deserializer=str, if_undefined=None, if_none=None, default=None) """The topic of this channel, if applicable and set.""" - is_nsfw: typing.Optional[bool] = marshaller.attrib( - raw_name="nsfw", deserializer=bool, if_undefined=None, default=None - ) + is_nsfw: typing.Optional[bool] = attr.ib(raw_name="nsfw", deserializer=bool, if_undefined=None, default=None) """Whether this channel is nsfw, will be `None` if not applicable.""" - last_message_id: typing.Optional[base_models.Snowflake] = marshaller.attrib( + last_message_id: typing.Optional[base_models.Snowflake] = attr.ib( deserializer=base_models.Snowflake, if_none=None, if_undefined=None, default=None ) """The ID of the last message sent, if it's a text type channel.""" - bitrate: typing.Optional[int] = marshaller.attrib(deserializer=int, if_undefined=None, default=None) + bitrate: typing.Optional[int] = attr.ib(deserializer=int, if_undefined=None, default=None) """The bitrate (in bits) of this channel, if it's a guild voice channel.""" - user_limit: typing.Optional[int] = marshaller.attrib(deserializer=int, if_undefined=None, default=None) + user_limit: typing.Optional[int] = attr.ib(deserializer=int, if_undefined=None, default=None) """The user limit for this channel if it's a guild voice channel.""" - rate_limit_per_user: typing.Optional[datetime.timedelta] = marshaller.attrib( + rate_limit_per_user: typing.Optional[datetime.timedelta] = attr.ib( deserializer=_rate_limit_per_user_deserializer, if_undefined=None, default=None ) """How long a user has to wait before sending another message in this channel. @@ -126,22 +122,22 @@ class BaseChannelEvent(base_events.HikariEvent, base_models.Unique, marshaller.D This is only applicable to a guild text like channel. """ - recipients: typing.Optional[typing.Mapping[base_models.Snowflake, users.User]] = marshaller.attrib( + recipients: typing.Optional[typing.Mapping[base_models.Snowflake, users.User]] = attr.ib( deserializer=_recipients_deserializer, if_undefined=None, default=None, inherit_kwargs=True, ) """A mapping of this channel's recipient users, if it's a DM or group DM.""" - icon_hash: typing.Optional[str] = marshaller.attrib( + icon_hash: typing.Optional[str] = attr.ib( raw_name="icon", deserializer=str, if_undefined=None, if_none=None, default=None ) """The hash of this channel's icon, if it's a group DM channel and is set.""" - owner_id: typing.Optional[base_models.Snowflake] = marshaller.attrib( + owner_id: typing.Optional[base_models.Snowflake] = attr.ib( deserializer=base_models.Snowflake, if_undefined=None, default=None ) """The ID of this channel's creator, if it's a DM channel.""" - application_id: typing.Optional[base_models.Snowflake] = marshaller.attrib( + application_id: typing.Optional[base_models.Snowflake] = attr.ib( deserializer=base_models.Snowflake, if_undefined=None, default=None ) """The ID of the application that created the group DM. @@ -149,19 +145,18 @@ class BaseChannelEvent(base_events.HikariEvent, base_models.Unique, marshaller.D This is only applicable to bot based group DMs. """ - parent_id: typing.Optional[base_models.Snowflake] = marshaller.attrib( + parent_id: typing.Optional[base_models.Snowflake] = attr.ib( deserializer=base_models.Snowflake, if_undefined=None, if_none=None, default=None ) """The ID of this channels's parent category within guild, if set.""" - last_pin_timestamp: typing.Optional[datetime.datetime] = marshaller.attrib( + last_pin_timestamp: typing.Optional[datetime.datetime] = attr.ib( deserializer=conversions.iso8601_datetime_string_to_datetime, if_undefined=None, default=None ) """The datetime of when the last message was pinned in this channel.""" @base_events.requires_intents(intents.Intent.GUILDS) -@marshaller.marshallable() @attr.s(eq=False, hash=False, kw_only=True, slots=True) class ChannelCreateEvent(BaseChannelEvent): """Represents Channel Create gateway events. @@ -172,21 +167,18 @@ class ChannelCreateEvent(BaseChannelEvent): @base_events.requires_intents(intents.Intent.GUILDS) -@marshaller.marshallable() @attr.s(eq=False, hash=False, kw_only=True, slots=True) class ChannelUpdateEvent(BaseChannelEvent): """Represents Channel Update gateway events.""" @base_events.requires_intents(intents.Intent.GUILDS) -@marshaller.marshallable() @attr.s(eq=False, hash=False, kw_only=True, slots=True) class ChannelDeleteEvent(BaseChannelEvent): """Represents Channel Delete gateway events.""" @base_events.requires_intents(intents.Intent.GUILDS) -@marshaller.marshallable() @attr.s(eq=False, hash=False, kw_only=True, slots=True) class ChannelPinsUpdateEvent(base_events.HikariEvent, marshaller.Deserializable): """Used to represent the Channel Pins Update gateway event. @@ -195,7 +187,7 @@ class ChannelPinsUpdateEvent(base_events.HikariEvent, marshaller.Deserializable) when a pinned message is deleted. """ - guild_id: typing.Optional[base_models.Snowflake] = marshaller.attrib( + guild_id: typing.Optional[base_models.Snowflake] = attr.ib( deserializer=base_models.Snowflake, if_undefined=None, default=None, repr=True ) """The ID of the guild where this event happened. @@ -203,10 +195,10 @@ class ChannelPinsUpdateEvent(base_events.HikariEvent, marshaller.Deserializable) Will be `None` if this happened in a DM channel. """ - channel_id: base_models.Snowflake = marshaller.attrib(deserializer=base_models.Snowflake, repr=True) + channel_id: base_models.Snowflake = attr.ib(deserializer=base_models.Snowflake, repr=True) """The ID of the channel where the message was pinned or unpinned.""" - last_pin_timestamp: typing.Optional[datetime.datetime] = marshaller.attrib( + last_pin_timestamp: typing.Optional[datetime.datetime] = attr.ib( deserializer=conversions.iso8601_datetime_string_to_datetime, if_undefined=None, default=None, repr=True ) """The datetime of when the most recent message was pinned in this channel. @@ -216,7 +208,6 @@ class ChannelPinsUpdateEvent(base_events.HikariEvent, marshaller.Deserializable) @base_events.requires_intents(intents.Intent.GUILD_WEBHOOKS) -@marshaller.marshallable() @attr.s(eq=False, hash=False, kw_only=True, slots=True) class WebhookUpdateEvent(base_events.HikariEvent, marshaller.Deserializable): """Used to represent webhook update gateway events. @@ -224,10 +215,10 @@ class WebhookUpdateEvent(base_events.HikariEvent, marshaller.Deserializable): Sent when a webhook is updated, created or deleted in a guild. """ - guild_id: base_models.Snowflake = marshaller.attrib(deserializer=base_models.Snowflake, repr=True) + guild_id: base_models.Snowflake = attr.ib(deserializer=base_models.Snowflake, repr=True) """The ID of the guild this webhook is being updated in.""" - channel_id: base_models.Snowflake = marshaller.attrib(deserializer=base_models.Snowflake, repr=True) + channel_id: base_models.Snowflake = attr.ib(deserializer=base_models.Snowflake, repr=True) """The ID of the channel this webhook is being updated in.""" @@ -236,7 +227,6 @@ def _timestamp_deserializer(date: str) -> datetime.datetime: @base_events.requires_intents(intents.Intent.GUILD_MESSAGE_TYPING, intents.Intent.DIRECT_MESSAGE_TYPING) -@marshaller.marshallable() @attr.s(eq=False, hash=False, kw_only=True, slots=True) class TypingStartEvent(base_events.HikariEvent, marshaller.Deserializable): """Used to represent typing start gateway events. @@ -244,10 +234,10 @@ class TypingStartEvent(base_events.HikariEvent, marshaller.Deserializable): Received when a user or bot starts "typing" in a channel. """ - channel_id: base_models.Snowflake = marshaller.attrib(deserializer=base_models.Snowflake, repr=True) + channel_id: base_models.Snowflake = attr.ib(deserializer=base_models.Snowflake, repr=True) """The ID of the channel this typing event is occurring in.""" - guild_id: typing.Optional[base_models.Snowflake] = marshaller.attrib( + guild_id: typing.Optional[base_models.Snowflake] = attr.ib( deserializer=base_models.Snowflake, if_undefined=None, default=None, repr=True ) """The ID of the guild this typing event is occurring in. @@ -255,13 +245,13 @@ class TypingStartEvent(base_events.HikariEvent, marshaller.Deserializable): Will be `None` if this event is happening in a DM channel. """ - user_id: base_models.Snowflake = marshaller.attrib(deserializer=base_models.Snowflake, repr=True) + user_id: base_models.Snowflake = attr.ib(deserializer=base_models.Snowflake, repr=True) """The ID of the user who triggered this typing event.""" - timestamp: datetime.datetime = marshaller.attrib(deserializer=_timestamp_deserializer) + timestamp: datetime.datetime = attr.ib(deserializer=_timestamp_deserializer) """The datetime of when this typing event started.""" - member: typing.Optional[guilds.GuildMember] = marshaller.attrib( + member: typing.Optional[guilds.GuildMember] = attr.ib( deserializer=guilds.GuildMember.deserialize, if_undefined=None, default=None ) """The member object of the user who triggered this typing event. @@ -279,21 +269,20 @@ def _max_uses_deserializer(count: int) -> typing.Union[int, float]: @base_events.requires_intents(intents.Intent.GUILD_INVITES) -@marshaller.marshallable() @attr.s(eq=False, hash=False, kw_only=True, slots=True) class InviteCreateEvent(base_events.HikariEvent, marshaller.Deserializable): """Represents a gateway Invite Create event.""" - channel_id: base_models.Snowflake = marshaller.attrib(deserializer=base_models.Snowflake, repr=True) + channel_id: base_models.Snowflake = attr.ib(deserializer=base_models.Snowflake, repr=True) """The ID of the channel this invite targets.""" - code: str = marshaller.attrib(deserializer=str, repr=True) + code: str = attr.ib(deserializer=str, repr=True) """The code that identifies this invite.""" - created_at: datetime.datetime = marshaller.attrib(deserializer=conversions.iso8601_datetime_string_to_datetime) + created_at: datetime.datetime = attr.ib(deserializer=conversions.iso8601_datetime_string_to_datetime) """The datetime of when this invite was created.""" - guild_id: typing.Optional[base_models.Snowflake] = marshaller.attrib( + guild_id: typing.Optional[base_models.Snowflake] = attr.ib( deserializer=base_models.Snowflake, if_undefined=None, default=None, repr=True ) """The ID of the guild this invite was created in, if applicable. @@ -301,42 +290,41 @@ class InviteCreateEvent(base_events.HikariEvent, marshaller.Deserializable): Will be `None` for group DM invites. """ - inviter: typing.Optional[users.User] = marshaller.attrib( + inviter: typing.Optional[users.User] = attr.ib( deserializer=users.User.deserialize, if_undefined=None, default=None, inherit_kwargs=True ) """The object of the user who created this invite, if applicable.""" - max_age: typing.Optional[datetime.timedelta] = marshaller.attrib(deserializer=_max_age_deserializer,) + max_age: typing.Optional[datetime.timedelta] = attr.ib(deserializer=_max_age_deserializer,) """The timedelta of how long this invite will be valid for. If set to `None` then this is unlimited. """ - max_uses: typing.Union[int, float] = marshaller.attrib(deserializer=_max_uses_deserializer) + max_uses: typing.Union[int, float] = attr.ib(deserializer=_max_uses_deserializer) """The limit for how many times this invite can be used before it expires. If set to infinity (`float("inf")`) then this is unlimited. """ - target_user: typing.Optional[users.User] = marshaller.attrib( + target_user: typing.Optional[users.User] = attr.ib( deserializer=users.User.deserialize, if_undefined=None, default=None, inherit_kwargs=True ) """The object of the user who this invite targets, if set.""" - target_user_type: typing.Optional[invites.TargetUserType] = marshaller.attrib( + target_user_type: typing.Optional[invites.TargetUserType] = attr.ib( deserializer=invites.TargetUserType, if_undefined=None, default=None ) """The type of user target this invite is, if applicable.""" - is_temporary: bool = marshaller.attrib(raw_name="temporary", deserializer=bool) + is_temporary: bool = attr.ib(raw_name="temporary", deserializer=bool) """Whether this invite grants temporary membership.""" - uses: int = marshaller.attrib(deserializer=int) + uses: int = attr.ib(deserializer=int) """The amount of times this invite has been used.""" @base_events.requires_intents(intents.Intent.GUILD_INVITES) -@marshaller.marshallable() @attr.s(eq=False, hash=False, kw_only=True, slots=True) class InviteDeleteEvent(base_events.HikariEvent, marshaller.Deserializable): """Used to represent Invite Delete gateway events. @@ -344,14 +332,14 @@ class InviteDeleteEvent(base_events.HikariEvent, marshaller.Deserializable): Sent when an invite is deleted for a channel we can access. """ - channel_id: base_models.Snowflake = marshaller.attrib(deserializer=base_models.Snowflake, repr=True) + channel_id: base_models.Snowflake = attr.ib(deserializer=base_models.Snowflake, repr=True) """The ID of the channel this ID was attached to.""" # TODO: move common fields with InviteCreateEvent into base class. - code: str = marshaller.attrib(deserializer=str, repr=True) + code: str = attr.ib(deserializer=str, repr=True) """The code of this invite.""" - guild_id: typing.Optional[base_models.Snowflake] = marshaller.attrib( + guild_id: typing.Optional[base_models.Snowflake] = attr.ib( deserializer=base_models.Snowflake, if_undefined=None, default=None, repr=True ) """The ID of the guild this invite was deleted in. diff --git a/hikari/events/guild.py b/hikari/events/guild.py index 985601c24e..37fe74bb3d 100644 --- a/hikari/events/guild.py +++ b/hikari/events/guild.py @@ -44,24 +44,22 @@ import attr -from hikari.internal import conversions -from hikari.internal import marshaller from hikari.models import bases as base_models from hikari.models import emojis as emojis_models from hikari.models import guilds from hikari.models import intents from hikari.models import users +from hikari.utilities import conversions from . import base as base_events -from ..internal import unset +from ..utilities import unset if typing.TYPE_CHECKING: import datetime - from hikari.internal import more_typing + from hikari.utilities import more_typing @base_events.requires_intents(intents.Intent.GUILDS) -@marshaller.marshallable() @attr.s(eq=False, hash=False, kw_only=True, slots=True) class GuildCreateEvent(base_events.HikariEvent, guilds.Guild): """Used to represent Guild Create gateway events. @@ -72,14 +70,12 @@ class GuildCreateEvent(base_events.HikariEvent, guilds.Guild): @base_events.requires_intents(intents.Intent.GUILDS) -@marshaller.marshallable() @attr.s(eq=False, hash=False, kw_only=True, slots=True) class GuildUpdateEvent(base_events.HikariEvent, guilds.Guild): """Used to represent Guild Update gateway events.""" @base_events.requires_intents(intents.Intent.GUILDS) -@marshaller.marshallable() @attr.s(eq=False, hash=False, kw_only=True, slots=True) class GuildLeaveEvent(base_events.HikariEvent, base_models.Unique, marshaller.Deserializable): """Fired when the current user leaves the guild or is kicked/banned from it. @@ -90,7 +86,6 @@ class GuildLeaveEvent(base_events.HikariEvent, base_models.Unique, marshaller.De @base_events.requires_intents(intents.Intent.GUILDS) -@marshaller.marshallable() @attr.s(eq=False, hash=False, kw_only=True, slots=True) class GuildUnavailableEvent(base_events.HikariEvent, base_models.Unique, marshaller.Deserializable): """Fired when a guild becomes temporarily unavailable due to an outage. @@ -101,27 +96,24 @@ class GuildUnavailableEvent(base_events.HikariEvent, base_models.Unique, marshal @base_events.requires_intents(intents.Intent.GUILD_BANS) -@marshaller.marshallable() @attr.s(eq=False, hash=False, kw_only=True, slots=True) class BaseGuildBanEvent(base_events.HikariEvent, marshaller.Deserializable, abc.ABC): """A base object that guild ban events will inherit from.""" - guild_id: base_models.Snowflake = marshaller.attrib(deserializer=base_models.Snowflake, repr=True) + guild_id: base_models.Snowflake = attr.ib(deserializer=base_models.Snowflake, repr=True) """The ID of the guild this ban is in.""" - user: users.User = marshaller.attrib(deserializer=users.User.deserialize, inherit_kwargs=True, repr=True) + user: users.User = attr.ib(deserializer=users.User.deserialize, inherit_kwargs=True, repr=True) """The object of the user this ban targets.""" @base_events.requires_intents(intents.Intent.GUILD_BANS) -@marshaller.marshallable() @attr.s(eq=False, hash=False, kw_only=True, slots=True) class GuildBanAddEvent(BaseGuildBanEvent): """Used to represent a Guild Ban Add gateway event.""" @base_events.requires_intents(intents.Intent.GUILD_BANS) -@marshaller.marshallable() @attr.s(eq=False, hash=False, kw_only=True, slots=True) class GuildBanRemoveEvent(BaseGuildBanEvent): """Used to represent a Guild Ban Remove gateway event.""" @@ -137,37 +129,34 @@ def _deserialize_emojis( @base_events.requires_intents(intents.Intent.GUILD_EMOJIS) -@marshaller.marshallable() @attr.s(eq=False, hash=False, kw_only=True, slots=True) class GuildEmojisUpdateEvent(base_events.HikariEvent, marshaller.Deserializable): """Represents a Guild Emoji Update gateway event.""" - guild_id: base_models.Snowflake = marshaller.attrib(deserializer=base_models.Snowflake) + guild_id: base_models.Snowflake = attr.ib(deserializer=base_models.Snowflake) """The ID of the guild this emoji was updated in.""" - emojis: typing.Mapping[base_models.Snowflake, emojis_models.KnownCustomEmoji] = marshaller.attrib( + emojis: typing.Mapping[base_models.Snowflake, emojis_models.KnownCustomEmoji] = attr.ib( deserializer=_deserialize_emojis, inherit_kwargs=True, repr=True ) """The updated mapping of emojis by their ID.""" @base_events.requires_intents(intents.Intent.GUILD_INTEGRATIONS) -@marshaller.marshallable() @attr.s(eq=False, hash=False, kw_only=True, slots=True) class GuildIntegrationsUpdateEvent(base_events.HikariEvent, marshaller.Deserializable): """Used to represent Guild Integration Update gateway events.""" - guild_id: base_models.Snowflake = marshaller.attrib(deserializer=base_models.Snowflake, repr=True) + guild_id: base_models.Snowflake = attr.ib(deserializer=base_models.Snowflake, repr=True) """The ID of the guild the integration was updated in.""" @base_events.requires_intents(intents.Intent.GUILD_MEMBERS) -@marshaller.marshallable() @attr.s(eq=False, hash=False, kw_only=True, slots=True) class GuildMemberAddEvent(base_events.HikariEvent, guilds.GuildMember): """Used to represent a Guild Member Add gateway event.""" - guild_id: base_models.Snowflake = marshaller.attrib(deserializer=base_models.Snowflake, repr=True) + guild_id: base_models.Snowflake = attr.ib(deserializer=base_models.Snowflake, repr=True) """The ID of the guild where this member was added.""" @@ -176,7 +165,6 @@ def _deserialize_role_ids(payload: more_typing.JSONArray) -> typing.Sequence[bas @base_events.requires_intents(intents.Intent.GUILD_MEMBERS) -@marshaller.marshallable() @attr.s(eq=False, hash=False, kw_only=True, slots=True) class GuildMemberUpdateEvent(base_events.HikariEvent, marshaller.Deserializable): """Used to represent a Guild Member Update gateway event. @@ -184,18 +172,18 @@ class GuildMemberUpdateEvent(base_events.HikariEvent, marshaller.Deserializable) Sent when a guild member or their inner user object is updated. """ - guild_id: base_models.Snowflake = marshaller.attrib(deserializer=base_models.Snowflake, repr=True) + guild_id: base_models.Snowflake = attr.ib(deserializer=base_models.Snowflake, repr=True) """The ID of the guild this member was updated in.""" - role_ids: typing.Sequence[base_models.Snowflake] = marshaller.attrib( + role_ids: typing.Sequence[base_models.Snowflake] = attr.ib( raw_name="roles", deserializer=_deserialize_role_ids, ) """A sequence of the IDs of the member's current roles.""" - user: users.User = marshaller.attrib(deserializer=users.User.deserialize, inherit_kwargs=True, repr=True) + user: users.User = attr.ib(deserializer=users.User.deserialize, inherit_kwargs=True, repr=True) """The object of the user who was updated.""" - nickname: typing.Union[None, str, unset.Unset] = marshaller.attrib( + nickname: typing.Union[None, str, unset.Unset] = attr.ib( raw_name="nick", deserializer=str, if_none=None, if_undefined=unset.Unset, default=unset.UNSET ) """This member's nickname. @@ -204,7 +192,7 @@ class GuildMemberUpdateEvent(base_events.HikariEvent, marshaller.Deserializable) `hikari.models.unset.UNSET` this hasn't been acted on. """ - premium_since: typing.Union[None, datetime.datetime, unset.Unset] = marshaller.attrib( + premium_since: typing.Union[None, datetime.datetime, unset.Unset] = attr.ib( deserializer=conversions.iso8601_datetime_string_to_datetime, if_none=None, if_undefined=unset.Unset, @@ -217,7 +205,6 @@ class GuildMemberUpdateEvent(base_events.HikariEvent, marshaller.Deserializable) @base_events.requires_intents(intents.Intent.GUILD_MEMBERS) -@marshaller.marshallable() @attr.s(eq=False, hash=False, kw_only=True, slots=True) class GuildMemberRemoveEvent(base_events.HikariEvent, marshaller.Deserializable): """Used to represent Guild Member Remove gateway events. @@ -226,56 +213,52 @@ class GuildMemberRemoveEvent(base_events.HikariEvent, marshaller.Deserializable) """ # TODO: make GuildMember event into common base class. - guild_id: base_models.Snowflake = marshaller.attrib(deserializer=base_models.Snowflake, repr=True) + guild_id: base_models.Snowflake = attr.ib(deserializer=base_models.Snowflake, repr=True) """The ID of the guild this user was removed from.""" - user: users.User = marshaller.attrib(deserializer=users.User.deserialize, inherit_kwargs=True, repr=True) + user: users.User = attr.ib(deserializer=users.User.deserialize, inherit_kwargs=True, repr=True) """The object of the user who was removed from this guild.""" @base_events.requires_intents(intents.Intent.GUILDS) -@marshaller.marshallable() @attr.s(eq=False, hash=False, kw_only=True, slots=True) class GuildRoleCreateEvent(base_events.HikariEvent, marshaller.Deserializable): """Used to represent a Guild Role Create gateway event.""" - guild_id: base_models.Snowflake = marshaller.attrib(deserializer=base_models.Snowflake, repr=True) + guild_id: base_models.Snowflake = attr.ib(deserializer=base_models.Snowflake, repr=True) """The ID of the guild where this role was created.""" - role: guilds.Role = marshaller.attrib(deserializer=guilds.Role.deserialize, inherit_kwargs=True) + role: guilds.Role = attr.ib(deserializer=guilds.Role.deserialize, inherit_kwargs=True) """The object of the role that was created.""" @base_events.requires_intents(intents.Intent.GUILDS) -@marshaller.marshallable() @attr.s(eq=False, hash=False, kw_only=True, slots=True) class GuildRoleUpdateEvent(base_events.HikariEvent, marshaller.Deserializable): """Used to represent a Guild Role Create gateway event.""" # TODO: make any event with a guild ID into a custom base event. # https://pypi.org/project/stupid/ could this work around the multiple inheritance problem? - guild_id: base_models.Snowflake = marshaller.attrib(deserializer=base_models.Snowflake, repr=True) + guild_id: base_models.Snowflake = attr.ib(deserializer=base_models.Snowflake, repr=True) """The ID of the guild where this role was updated.""" - role: guilds.Role = marshaller.attrib(deserializer=guilds.Role.deserialize, inherit_kwargs=True, repr=True) + role: guilds.Role = attr.ib(deserializer=guilds.Role.deserialize, inherit_kwargs=True, repr=True) """The updated role object.""" @base_events.requires_intents(intents.Intent.GUILDS) -@marshaller.marshallable() @attr.s(eq=False, hash=False, kw_only=True, slots=True) class GuildRoleDeleteEvent(base_events.HikariEvent, marshaller.Deserializable): """Represents a gateway Guild Role Delete Event.""" - guild_id: base_models.Snowflake = marshaller.attrib(deserializer=base_models.Snowflake, repr=True) + guild_id: base_models.Snowflake = attr.ib(deserializer=base_models.Snowflake, repr=True) """The ID of the guild where this role is being deleted.""" - role_id: base_models.Snowflake = marshaller.attrib(deserializer=base_models.Snowflake, repr=True) + role_id: base_models.Snowflake = attr.ib(deserializer=base_models.Snowflake, repr=True) """The ID of the role being deleted.""" @base_events.requires_intents(intents.Intent.GUILD_PRESENCES) -@marshaller.marshallable() @attr.s(eq=False, hash=False, kw_only=True, slots=True) class PresenceUpdateEvent(base_events.HikariEvent, guilds.GuildMemberPresence): """Used to represent Presence Update gateway events. diff --git a/hikari/events/message.py b/hikari/events/message.py index 8057e09aef..f74dc6aa15 100644 --- a/hikari/events/message.py +++ b/hikari/events/message.py @@ -35,8 +35,6 @@ import attr -from hikari.internal import conversions -from hikari.internal import marshaller from hikari.models import applications from hikari.models import bases as base_models from hikari.models import embeds as embed_models @@ -45,17 +43,17 @@ from hikari.models import intents from hikari.models import messages from hikari.models import users +from hikari.utilities import conversions from . import base as base_events -from ..internal import unset +from ..utilities import unset if typing.TYPE_CHECKING: import datetime - from hikari.internal import more_typing + from hikari.utilities import more_typing @base_events.requires_intents(intents.Intent.GUILD_MESSAGES, intents.Intent.DIRECT_MESSAGES) -@marshaller.marshallable() @attr.s(eq=False, hash=False, kw_only=True, slots=True) class MessageCreateEvent(base_events.HikariEvent, messages.Message): """Used to represent Message Create gateway events.""" @@ -85,7 +83,6 @@ def _deserialize_reaction(payload: more_typing.JSONArray, **kwargs: typing.Any) # This is an arbitrarily partial version of `messages.Message` @base_events.requires_intents(intents.Intent.GUILD_MESSAGES, intents.Intent.DIRECT_MESSAGES) -@marshaller.marshallable() @attr.s(eq=False, hash=False, kw_only=True, slots=True) class MessageUpdateEvent(base_events.HikariEvent, base_models.Unique, marshaller.Deserializable): """Represents Message Update gateway events. @@ -97,37 +94,35 @@ class MessageUpdateEvent(base_events.HikariEvent, base_models.Unique, marshaller alongside field nullability. """ - channel_id: base_models.Snowflake = marshaller.attrib(deserializer=base_models.Snowflake, repr=True) + channel_id: base_models.Snowflake = attr.ib(deserializer=base_models.Snowflake, repr=True) """The ID of the channel that the message was sent in.""" - guild_id: typing.Union[base_models.Snowflake, unset.Unset] = marshaller.attrib( + guild_id: typing.Union[base_models.Snowflake, unset.Unset] = attr.ib( deserializer=base_models.Snowflake, if_undefined=unset.Unset, default=unset.UNSET, repr=True ) """The ID of the guild that the message was sent in.""" - author: typing.Union[users.User, unset.Unset] = marshaller.attrib( + author: typing.Union[users.User, unset.Unset] = attr.ib( deserializer=users.User.deserialize, if_undefined=unset.Unset, default=unset.UNSET, repr=True ) """The author of this message.""" # TODO: can we merge member and author together? # We could override deserialize to to this and then reorganise the payload, perhaps? - member: typing.Union[guilds.GuildMember, unset.Unset] = marshaller.attrib( + member: typing.Union[guilds.GuildMember, unset.Unset] = attr.ib( deserializer=guilds.GuildMember.deserialize, if_undefined=unset.Unset, default=unset.UNSET ) """The member properties for the message's author.""" - content: typing.Union[str, unset.Unset] = marshaller.attrib( - deserializer=str, if_undefined=unset.Unset, default=unset.UNSET - ) + content: typing.Union[str, unset.Unset] = attr.ib(deserializer=str, if_undefined=unset.Unset, default=unset.UNSET) """The content of the message.""" - timestamp: typing.Union[datetime.datetime, unset.Unset] = marshaller.attrib( + timestamp: typing.Union[datetime.datetime, unset.Unset] = attr.ib( deserializer=conversions.iso8601_datetime_string_to_datetime, if_undefined=unset.Unset, default=unset.UNSET ) """The timestamp that the message was sent at.""" - edited_timestamp: typing.Union[datetime.datetime, unset.Unset, None] = marshaller.attrib( + edited_timestamp: typing.Union[datetime.datetime, unset.Unset, None] = attr.ib( deserializer=conversions.iso8601_datetime_string_to_datetime, if_none=None, if_undefined=unset.Unset, @@ -138,27 +133,27 @@ class MessageUpdateEvent(base_events.HikariEvent, base_models.Unique, marshaller Will be `None` if the message wasn't ever edited. """ - is_tts: typing.Union[bool, unset.Unset] = marshaller.attrib( + is_tts: typing.Union[bool, unset.Unset] = attr.ib( raw_name="tts", deserializer=bool, if_undefined=unset.Unset, default=unset.UNSET ) """Whether the message is a TTS message.""" - is_mentioning_everyone: typing.Union[bool, unset.Unset] = marshaller.attrib( + is_mentioning_everyone: typing.Union[bool, unset.Unset] = attr.ib( raw_name="mention_everyone", deserializer=bool, if_undefined=unset.Unset, default=unset.UNSET ) """Whether the message mentions `@everyone` or `@here`.""" - user_mentions: typing.Union[typing.Set[base_models.Snowflake], unset.Unset] = marshaller.attrib( + user_mentions: typing.Union[typing.Set[base_models.Snowflake], unset.Unset] = attr.ib( raw_name="mentions", deserializer=_deserialize_object_mentions, if_undefined=unset.Unset, default=unset.UNSET, ) """The users the message mentions.""" - role_mentions: typing.Union[typing.Set[base_models.Snowflake], unset.Unset] = marshaller.attrib( + role_mentions: typing.Union[typing.Set[base_models.Snowflake], unset.Unset] = attr.ib( raw_name="mention_roles", deserializer=_deserialize_mentions, if_undefined=unset.Unset, default=unset.UNSET, ) """The roles the message mentions.""" - channel_mentions: typing.Union[typing.Set[base_models.Snowflake], unset.Unset] = marshaller.attrib( + channel_mentions: typing.Union[typing.Set[base_models.Snowflake], unset.Unset] = attr.ib( raw_name="mention_channels", deserializer=_deserialize_object_mentions, if_undefined=unset.Unset, @@ -166,37 +161,37 @@ class MessageUpdateEvent(base_events.HikariEvent, base_models.Unique, marshaller ) """The channels the message mentions.""" - attachments: typing.Union[typing.Sequence[messages.Attachment], unset.Unset] = marshaller.attrib( + attachments: typing.Union[typing.Sequence[messages.Attachment], unset.Unset] = attr.ib( deserializer=_deserialize_attachments, if_undefined=unset.Unset, default=unset.UNSET, inherit_kwargs=True, ) """The message attachments.""" - embeds: typing.Union[typing.Sequence[embed_models.Embed], unset.Unset] = marshaller.attrib( + embeds: typing.Union[typing.Sequence[embed_models.Embed], unset.Unset] = attr.ib( deserializer=_deserialize_embeds, if_undefined=unset.Unset, default=unset.UNSET, inherit_kwargs=True, ) """The message's embeds.""" - reactions: typing.Union[typing.Sequence[messages.Reaction], unset.Unset] = marshaller.attrib( + reactions: typing.Union[typing.Sequence[messages.Reaction], unset.Unset] = attr.ib( deserializer=_deserialize_reaction, if_undefined=unset.Unset, default=unset.UNSET, inherit_kwargs=True ) """The message's reactions.""" - is_pinned: typing.Union[bool, unset.Unset] = marshaller.attrib( + is_pinned: typing.Union[bool, unset.Unset] = attr.ib( raw_name="pinned", deserializer=bool, if_undefined=unset.Unset, default=unset.UNSET ) """Whether the message is pinned.""" - webhook_id: typing.Union[base_models.Snowflake, unset.Unset] = marshaller.attrib( + webhook_id: typing.Union[base_models.Snowflake, unset.Unset] = attr.ib( deserializer=base_models.Snowflake, if_undefined=unset.Unset, default=unset.UNSET ) """If the message was generated by a webhook, the webhook's ID.""" - type: typing.Union[messages.MessageType, unset.Unset] = marshaller.attrib( + type: typing.Union[messages.MessageType, unset.Unset] = attr.ib( deserializer=messages.MessageType, if_undefined=unset.Unset, default=unset.UNSET ) """The message's type.""" - activity: typing.Union[messages.MessageActivity, unset.Unset] = marshaller.attrib( + activity: typing.Union[messages.MessageActivity, unset.Unset] = attr.ib( deserializer=messages.MessageActivity.deserialize, if_undefined=unset.Unset, default=unset.UNSET, @@ -204,7 +199,7 @@ class MessageUpdateEvent(base_events.HikariEvent, base_models.Unique, marshaller ) """The message's activity.""" - application: typing.Optional[applications.Application] = marshaller.attrib( + application: typing.Optional[applications.Application] = attr.ib( deserializer=applications.Application.deserialize, if_undefined=unset.Unset, default=unset.UNSET, @@ -212,7 +207,7 @@ class MessageUpdateEvent(base_events.HikariEvent, base_models.Unique, marshaller ) """The message's application.""" - message_reference: typing.Union[messages.MessageCrosspost, unset.Unset] = marshaller.attrib( + message_reference: typing.Union[messages.MessageCrosspost, unset.Unset] = attr.ib( deserializer=messages.MessageCrosspost.deserialize, if_undefined=unset.Unset, default=unset.UNSET, @@ -220,14 +215,12 @@ class MessageUpdateEvent(base_events.HikariEvent, base_models.Unique, marshaller ) """The message's cross-posted reference data.""" - flags: typing.Union[messages.MessageFlag, unset.Unset] = marshaller.attrib( + flags: typing.Union[messages.MessageFlag, unset.Unset] = attr.ib( deserializer=messages.MessageFlag, if_undefined=unset.Unset, default=unset.UNSET ) """The message's flags.""" - nonce: typing.Union[str, unset.Unset] = marshaller.attrib( - deserializer=str, if_undefined=unset.Unset, default=unset.UNSET - ) + nonce: typing.Union[str, unset.Unset] = attr.ib(deserializer=str, if_undefined=unset.Unset, default=unset.UNSET) """The message nonce. This is a string used for validating a message was sent. @@ -235,7 +228,6 @@ class MessageUpdateEvent(base_events.HikariEvent, base_models.Unique, marshaller @base_events.requires_intents(intents.Intent.GUILD_MESSAGES, intents.Intent.DIRECT_MESSAGES) -@marshaller.marshallable() @attr.s(eq=False, hash=False, kw_only=True, slots=True) class MessageDeleteEvent(base_events.HikariEvent, marshaller.Deserializable): """Used to represent Message Delete gateway events. @@ -245,10 +237,10 @@ class MessageDeleteEvent(base_events.HikariEvent, marshaller.Deserializable): # TODO: common base class for Message events. - channel_id: base_models.Snowflake = marshaller.attrib(deserializer=base_models.Snowflake, repr=True) + channel_id: base_models.Snowflake = attr.ib(deserializer=base_models.Snowflake, repr=True) """The ID of the channel where this message was deleted.""" - guild_id: typing.Optional[base_models.Snowflake] = marshaller.attrib( + guild_id: typing.Optional[base_models.Snowflake] = attr.ib( deserializer=base_models.Snowflake, if_undefined=None, default=None, repr=True ) """The ID of the guild where this message was deleted. @@ -256,7 +248,7 @@ class MessageDeleteEvent(base_events.HikariEvent, marshaller.Deserializable): This will be `None` if this message was deleted in a DM channel. """ - message_id: base_models.Snowflake = marshaller.attrib(raw_name="id", deserializer=base_models.Snowflake, repr=True) + message_id: base_models.Snowflake = attr.ib(raw_name="id", deserializer=base_models.Snowflake, repr=True) """The ID of the message that was deleted.""" @@ -265,7 +257,6 @@ def _deserialize_message_ids(payload: more_typing.JSONArray) -> typing.Set[base_ @base_events.requires_intents(intents.Intent.GUILD_MESSAGES) -@marshaller.marshallable() @attr.s(eq=False, hash=False, kw_only=True, slots=True) class MessageDeleteBulkEvent(base_events.HikariEvent, marshaller.Deserializable): """Used to represent Message Bulk Delete gateway events. @@ -273,10 +264,10 @@ class MessageDeleteBulkEvent(base_events.HikariEvent, marshaller.Deserializable) Sent when multiple messages are deleted in a channel at once. """ - channel_id: base_models.Snowflake = marshaller.attrib(deserializer=base_models.Snowflake, repr=True) + channel_id: base_models.Snowflake = attr.ib(deserializer=base_models.Snowflake, repr=True) """The ID of the channel these messages have been deleted in.""" - guild_id: typing.Optional[base_models.Snowflake] = marshaller.attrib( + guild_id: typing.Optional[base_models.Snowflake] = attr.ib( deserializer=base_models.Snowflake, if_none=None, repr=True, ) """The ID of the channel these messages have been deleted in. @@ -284,30 +275,27 @@ class MessageDeleteBulkEvent(base_events.HikariEvent, marshaller.Deserializable) This will be `None` if these messages were bulk deleted in a DM channel. """ - message_ids: typing.Set[base_models.Snowflake] = marshaller.attrib( - raw_name="ids", deserializer=_deserialize_message_ids - ) + message_ids: typing.Set[base_models.Snowflake] = attr.ib(raw_name="ids", deserializer=_deserialize_message_ids) """A collection of the IDs of the messages that were deleted.""" @base_events.requires_intents(intents.Intent.GUILD_MESSAGE_REACTIONS, intents.Intent.DIRECT_MESSAGE_REACTIONS) -@marshaller.marshallable() @attr.s(eq=False, hash=False, kw_only=True, slots=True) class MessageReactionAddEvent(base_events.HikariEvent, marshaller.Deserializable): """Used to represent Message Reaction Add gateway events.""" # TODO: common base classes! - user_id: base_models.Snowflake = marshaller.attrib(deserializer=base_models.Snowflake, repr=True) + user_id: base_models.Snowflake = attr.ib(deserializer=base_models.Snowflake, repr=True) """The ID of the user adding the reaction.""" - channel_id: base_models.Snowflake = marshaller.attrib(deserializer=base_models.Snowflake, repr=True) + channel_id: base_models.Snowflake = attr.ib(deserializer=base_models.Snowflake, repr=True) """The ID of the channel where this reaction is being added.""" - message_id: base_models.Snowflake = marshaller.attrib(deserializer=base_models.Snowflake, repr=True) + message_id: base_models.Snowflake = attr.ib(deserializer=base_models.Snowflake, repr=True) """The ID of the message this reaction is being added to.""" - guild_id: typing.Optional[base_models.Snowflake] = marshaller.attrib( + guild_id: typing.Optional[base_models.Snowflake] = attr.ib( deserializer=base_models.Snowflake, if_undefined=None, default=None, repr=True ) """The ID of the guild where this reaction is being added. @@ -316,7 +304,7 @@ class MessageReactionAddEvent(base_events.HikariEvent, marshaller.Deserializable """ # TODO: does this contain a user? If not, should it be a PartialGuildMember? - member: typing.Optional[guilds.GuildMember] = marshaller.attrib( + member: typing.Optional[guilds.GuildMember] = attr.ib( deserializer=guilds.GuildMember.deserialize, if_undefined=None, default=None, inherit_kwargs=True ) """The member object of the user who's adding this reaction. @@ -324,28 +312,27 @@ class MessageReactionAddEvent(base_events.HikariEvent, marshaller.Deserializable This will be `None` if this is happening in a DM channel. """ - emoji: typing.Union[emojis.CustomEmoji, emojis.UnicodeEmoji] = marshaller.attrib( + emoji: typing.Union[emojis.CustomEmoji, emojis.UnicodeEmoji] = attr.ib( deserializer=emojis.deserialize_reaction_emoji, inherit_kwargs=True, repr=True ) """The object of the emoji being added.""" @base_events.requires_intents(intents.Intent.GUILD_MESSAGE_REACTIONS, intents.Intent.DIRECT_MESSAGE_REACTIONS) -@marshaller.marshallable() @attr.s(eq=False, hash=False, kw_only=True, slots=True) class MessageReactionRemoveEvent(base_events.HikariEvent, marshaller.Deserializable): """Used to represent Message Reaction Remove gateway events.""" - user_id: base_models.Snowflake = marshaller.attrib(deserializer=base_models.Snowflake, repr=True) + user_id: base_models.Snowflake = attr.ib(deserializer=base_models.Snowflake, repr=True) """The ID of the user who is removing their reaction.""" - channel_id: base_models.Snowflake = marshaller.attrib(deserializer=base_models.Snowflake, repr=True) + channel_id: base_models.Snowflake = attr.ib(deserializer=base_models.Snowflake, repr=True) """The ID of the channel where this reaction is being removed.""" - message_id: base_models.Snowflake = marshaller.attrib(deserializer=base_models.Snowflake, repr=True) + message_id: base_models.Snowflake = attr.ib(deserializer=base_models.Snowflake, repr=True) """The ID of the message this reaction is being removed from.""" - guild_id: typing.Optional[base_models.Snowflake] = marshaller.attrib( + guild_id: typing.Optional[base_models.Snowflake] = attr.ib( deserializer=base_models.Snowflake, if_undefined=None, default=None, repr=True ) """The ID of the guild where this reaction is being removed @@ -353,14 +340,13 @@ class MessageReactionRemoveEvent(base_events.HikariEvent, marshaller.Deserializa This will be `None` if this event is happening in a DM channel. """ - emoji: typing.Union[emojis.UnicodeEmoji, emojis.CustomEmoji] = marshaller.attrib( + emoji: typing.Union[emojis.UnicodeEmoji, emojis.CustomEmoji] = attr.ib( deserializer=emojis.deserialize_reaction_emoji, inherit_kwargs=True, repr=True ) """The object of the emoji being removed.""" @base_events.requires_intents(intents.Intent.GUILD_MESSAGE_REACTIONS, intents.Intent.DIRECT_MESSAGE_REACTIONS) -@marshaller.marshallable() @attr.s(eq=False, hash=False, kw_only=True, slots=True) class MessageReactionRemoveAllEvent(base_events.HikariEvent, marshaller.Deserializable): """Used to represent Message Reaction Remove All gateway events. @@ -368,20 +354,19 @@ class MessageReactionRemoveAllEvent(base_events.HikariEvent, marshaller.Deserial Sent when all the reactions are removed from a message, regardless of emoji. """ - channel_id: base_models.Snowflake = marshaller.attrib(deserializer=base_models.Snowflake, repr=True) + channel_id: base_models.Snowflake = attr.ib(deserializer=base_models.Snowflake, repr=True) """The ID of the channel where the targeted message is.""" - message_id: base_models.Snowflake = marshaller.attrib(deserializer=base_models.Snowflake, repr=True) + message_id: base_models.Snowflake = attr.ib(deserializer=base_models.Snowflake, repr=True) """The ID of the message all reactions are being removed from.""" - guild_id: typing.Optional[base_models.Snowflake] = marshaller.attrib( + guild_id: typing.Optional[base_models.Snowflake] = attr.ib( deserializer=base_models.Snowflake, if_undefined=None, default=None, repr=True, ) """The ID of the guild where the targeted message is, if applicable.""" @base_events.requires_intents(intents.Intent.GUILD_MESSAGE_REACTIONS, intents.Intent.DIRECT_MESSAGE_REACTIONS) -@marshaller.marshallable() @attr.s(eq=False, hash=False, kw_only=True, slots=True) class MessageReactionRemoveEmojiEvent(base_events.HikariEvent, marshaller.Deserializable): """Represents Message Reaction Remove Emoji events. @@ -389,18 +374,18 @@ class MessageReactionRemoveEmojiEvent(base_events.HikariEvent, marshaller.Deseri Sent when all the reactions for a single emoji are removed from a message. """ - channel_id: base_models.Snowflake = marshaller.attrib(deserializer=base_models.Snowflake, repr=True) + channel_id: base_models.Snowflake = attr.ib(deserializer=base_models.Snowflake, repr=True) """The ID of the channel where the targeted message is.""" - guild_id: typing.Optional[base_models.Snowflake] = marshaller.attrib( + guild_id: typing.Optional[base_models.Snowflake] = attr.ib( deserializer=base_models.Snowflake, if_undefined=None, default=None, repr=True ) """The ID of the guild where the targeted message is, if applicable.""" - message_id: base_models.Snowflake = marshaller.attrib(deserializer=base_models.Snowflake, repr=True) + message_id: base_models.Snowflake = attr.ib(deserializer=base_models.Snowflake, repr=True) """The ID of the message the reactions are being removed from.""" - emoji: typing.Union[emojis.UnicodeEmoji, emojis.CustomEmoji] = marshaller.attrib( + emoji: typing.Union[emojis.UnicodeEmoji, emojis.CustomEmoji] = attr.ib( deserializer=emojis.deserialize_reaction_emoji, inherit_kwargs=True, repr=True ) """The object of the emoji that's being removed.""" diff --git a/hikari/events/other.py b/hikari/events/other.py index 29bd23346d..5dcac1d9c8 100644 --- a/hikari/events/other.py +++ b/hikari/events/other.py @@ -37,7 +37,6 @@ import attr -from hikari.internal import marshaller from hikari.models import bases as base_models from hikari.models import guilds from hikari.models import users @@ -45,7 +44,7 @@ if typing.TYPE_CHECKING: from ..net import gateway as gateway_client - from hikari.internal import more_typing + from hikari.utilities import more_typing # Synthetic event, is not deserialized, and is produced by the dispatcher. @@ -120,7 +119,6 @@ def _deserialize_unavailable_guilds( } -@marshaller.marshallable() @attr.s(eq=False, hash=False, kw_only=True, slots=True) class ReadyEvent(base_events.HikariEvent, marshaller.Deserializable): """Represents the gateway Ready event. @@ -128,15 +126,15 @@ class ReadyEvent(base_events.HikariEvent, marshaller.Deserializable): This is received only when IDENTIFYing with the gateway. """ - gateway_version: int = marshaller.attrib(raw_name="v", deserializer=int, repr=True) + gateway_version: int = attr.ib(raw_name="v", deserializer=int, repr=True) """The gateway version this is currently connected to.""" - my_user: users.MyUser = marshaller.attrib( + my_user: users.MyUser = attr.ib( raw_name="user", deserializer=users.MyUser.deserialize, inherit_kwargs=True, repr=True ) """The object of the current bot account this connection is for.""" - unavailable_guilds: typing.Mapping[base_models.Snowflake, guilds.UnavailableGuild] = marshaller.attrib( + unavailable_guilds: typing.Mapping[base_models.Snowflake, guilds.UnavailableGuild] = attr.ib( raw_name="guilds", deserializer=_deserialize_unavailable_guilds, inherit_kwargs=True ) """A mapping of the guilds this bot is currently in. @@ -144,10 +142,10 @@ class ReadyEvent(base_events.HikariEvent, marshaller.Deserializable): All guilds will start off "unavailable". """ - session_id: str = marshaller.attrib(deserializer=str, repr=True) + session_id: str = attr.ib(deserializer=str, repr=True) """The id of the current gateway session, used for reconnecting.""" - _shard_information: typing.Optional[typing.Tuple[int, int]] = marshaller.attrib( + _shard_information: typing.Optional[typing.Tuple[int, int]] = attr.ib( raw_name="shard", deserializer=tuple, if_undefined=None, default=None ) """Information about the current shard, only provided when IDENTIFYing.""" @@ -169,7 +167,6 @@ def shard_count(self) -> typing.Optional[int]: return self._shard_information[1] if self._shard_information else None -@marshaller.marshallable() @attr.s(eq=False, hash=False, kw_only=True, slots=True) class MyUserUpdateEvent(base_events.HikariEvent, users.MyUser): """Used to represent User Update gateway events. diff --git a/hikari/events/voice.py b/hikari/events/voice.py index f7c2c3c588..024712e3a3 100644 --- a/hikari/events/voice.py +++ b/hikari/events/voice.py @@ -24,7 +24,6 @@ import attr -from hikari.internal import marshaller from hikari.models import bases as base_models from hikari.models import intents from hikari.models import voices @@ -32,7 +31,6 @@ @base_events.requires_intents(intents.Intent.GUILD_VOICE_STATES) -@marshaller.marshallable() @attr.s(eq=False, hash=False, kw_only=True, slots=True) class VoiceStateUpdateEvent(base_events.HikariEvent, voices.VoiceState): """Used to represent voice state update gateway events. @@ -41,7 +39,6 @@ class VoiceStateUpdateEvent(base_events.HikariEvent, voices.VoiceState): """ -@marshaller.marshallable() @attr.s(eq=False, hash=False, kw_only=True, slots=True) class VoiceServerUpdateEvent(base_events.HikariEvent, marshaller.Deserializable): """Used to represent voice server update gateway events. @@ -50,11 +47,11 @@ class VoiceServerUpdateEvent(base_events.HikariEvent, marshaller.Deserializable) falls over to a new server. """ - token: str = marshaller.attrib(deserializer=str) + token: str = attr.ib(deserializer=str) """The voice connection's string token.""" - guild_id: base_models.Snowflake = marshaller.attrib(deserializer=base_models.Snowflake, repr=True) + guild_id: base_models.Snowflake = attr.ib(deserializer=base_models.Snowflake, repr=True) """The ID of the guild this voice server update is for.""" - endpoint: str = marshaller.attrib(deserializer=str, repr=True) + endpoint: str = attr.ib(deserializer=str, repr=True) """The URI for this voice server host.""" diff --git a/hikari/impl/bot.py b/hikari/impl/bot.py index a7b8472643..1e329287b9 100644 --- a/hikari/impl/bot.py +++ b/hikari/impl/bot.py @@ -27,14 +27,13 @@ from hikari.impl import cache as cache_impl from hikari.impl import entity_factory as entity_factory_impl from hikari.impl import event_manager -from hikari.impl import event_manager_core from hikari.impl import gateway_zookeeper -from hikari.internal import class_helpers from hikari.models import gateway as gateway_models from hikari.models import guilds from hikari.net import gateway from hikari.net import rest from hikari.net import urls +from hikari.utilities import klass if typing.TYPE_CHECKING: import datetime @@ -67,7 +66,7 @@ def __init__( token: str, use_compression: bool = True, ): - self._logger = class_helpers.get_logger(self) + self._logger = klass.get_logger(self) self._cache = cache_impl.CacheImpl(app=self) self._config = config diff --git a/hikari/impl/cache.py b/hikari/impl/cache.py index 3473944941..28c4dd5b65 100644 --- a/hikari/impl/cache.py +++ b/hikari/impl/cache.py @@ -23,11 +23,10 @@ import typing from hikari import cache - +from hikari.utilities import binding if typing.TYPE_CHECKING: from hikari import app as app_ - from hikari.internal import more_typing from hikari.models import applications from hikari.models import audit_logs from hikari.models import channels @@ -49,32 +48,32 @@ def __init__(self, app: app_.IApp) -> None: def app(self) -> app_.IApp: return self._app - async def create_application(self, payload: more_typing.JSONObject) -> applications.Application: + async def create_application(self, payload: binding.JSONObject) -> applications.Application: pass - async def create_own_guild(self, payload: more_typing.JSONObject) -> applications.OwnGuild: + async def create_own_guild(self, payload: binding.JSONObject) -> applications.OwnGuild: pass - async def create_own_connection(self, payload: more_typing.JSONObject) -> applications.OwnConnection: + async def create_own_connection(self, payload: binding.JSONObject) -> applications.OwnConnection: pass - async def create_audit_log_change(self, payload: more_typing.JSONObject) -> audit_logs.AuditLogChange: + async def create_audit_log_change(self, payload: binding.JSONObject) -> audit_logs.AuditLogChange: pass - async def create_audit_log_entry_info(self, payload: more_typing.JSONObject) -> audit_logs.BaseAuditLogEntryInfo: + async def create_audit_log_entry_info(self, payload: binding.JSONObject) -> audit_logs.BaseAuditLogEntryInfo: pass - async def create_audit_log_entry(self, payload: more_typing.JSONObject) -> audit_logs.AuditLogEntry: + async def create_audit_log_entry(self, payload: binding.JSONObject) -> audit_logs.AuditLogEntry: pass - async def create_audit_log(self, payload: more_typing.JSONObject) -> audit_logs.AuditLog: + async def create_audit_log(self, payload: binding.JSONObject) -> audit_logs.AuditLog: pass - async def create_channel(self, payload: more_typing.JSONObject, can_cache: bool = False) -> channels.PartialChannel: + async def create_channel(self, payload: binding.JSONObject, can_cache: bool = False) -> channels.PartialChannel: pass async def update_channel( - self, channel: channels.PartialChannel, payload: more_typing.JSONObject + self, channel: channels.PartialChannel, payload: binding.JSONObject ) -> channels.PartialChannel: pass @@ -84,13 +83,13 @@ async def get_channel(self, channel_id: int) -> typing.Optional[channels.Partial async def delete_channel(self, channel_id: int) -> typing.Optional[channels.PartialChannel]: pass - async def create_embed(self, payload: more_typing.JSONObject) -> embeds.Embed: + async def create_embed(self, payload: binding.JSONObject) -> embeds.Embed: pass - async def create_emoji(self, payload: more_typing.JSONObject, can_cache: bool = False) -> emojis.Emoji: + async def create_emoji(self, payload: binding.JSONObject, can_cache: bool = False) -> emojis.Emoji: pass - async def update_emoji(self, payload: more_typing.JSONObject) -> emojis.Emoji: + async def update_emoji(self, payload: binding.JSONObject) -> emojis.Emoji: pass async def get_emoji(self, emoji_id: int) -> typing.Optional[emojis.KnownCustomEmoji]: @@ -99,13 +98,13 @@ async def get_emoji(self, emoji_id: int) -> typing.Optional[emojis.KnownCustomEm async def delete_emoji(self, emoji_id: int) -> typing.Optional[emojis.KnownCustomEmoji]: pass - async def create_gateway_bot(self, payload: more_typing.JSONObject) -> gateway.GatewayBot: + async def create_gateway_bot(self, payload: binding.JSONObject) -> gateway.GatewayBot: pass - async def create_member(self, payload: more_typing.JSONObject, can_cache: bool = False) -> guilds.GuildMember: + async def create_member(self, payload: binding.JSONObject, can_cache: bool = False) -> guilds.GuildMember: pass - async def update_member(self, member: guilds.GuildMember, payload: more_typing.JSONObject) -> guilds.GuildMember: + async def update_member(self, member: guilds.GuildMember, payload: binding.JSONObject) -> guilds.GuildMember: pass async def get_member(self, guild_id: int, user_id: int) -> typing.Optional[guilds.GuildMember]: @@ -114,10 +113,10 @@ async def get_member(self, guild_id: int, user_id: int) -> typing.Optional[guild async def delete_member(self, guild_id: int, user_id: int) -> typing.Optional[guilds.GuildMember]: pass - async def create_role(self, payload: more_typing.JSONObject, can_cache: bool = False) -> guilds.PartialRole: + async def create_role(self, payload: binding.JSONObject, can_cache: bool = False) -> guilds.PartialRole: pass - async def update_role(self, role: guilds.PartialRole, payload: more_typing.JSONObject) -> guilds.PartialRole: + async def update_role(self, role: guilds.PartialRole, payload: binding.JSONObject) -> guilds.PartialRole: pass async def get_role(self, guild_id: int, role_id: int) -> typing.Optional[guilds.PartialRole]: @@ -126,13 +125,11 @@ async def get_role(self, guild_id: int, role_id: int) -> typing.Optional[guilds. async def delete_role(self, guild_id: int, role_id: int) -> typing.Optional[guilds.PartialRole]: pass - async def create_presence( - self, payload: more_typing.JSONObject, can_cache: bool = False - ) -> guilds.GuildMemberPresence: + async def create_presence(self, payload: binding.JSONObject, can_cache: bool = False) -> guilds.GuildMemberPresence: pass async def update_presence( - self, role: guilds.GuildMemberPresence, payload: more_typing.JSONObject + self, role: guilds.GuildMemberPresence, payload: binding.JSONObject ) -> guilds.GuildMemberPresence: pass @@ -142,16 +139,16 @@ async def get_presence(self, guild_id: int, user_id: int) -> typing.Optional[gui async def delete_presence(self, guild_id: int, user_id: int) -> typing.Optional[guilds.GuildMemberPresence]: pass - async def create_guild_ban(self, payload: more_typing.JSONObject) -> guilds.GuildMemberBan: + async def create_guild_ban(self, payload: binding.JSONObject) -> guilds.GuildMemberBan: pass - async def create_guild_integration(self, payload: more_typing.JSONObject) -> guilds.PartialGuildIntegration: + async def create_guild_integration(self, payload: binding.JSONObject) -> guilds.PartialGuildIntegration: pass - async def create_guild(self, payload: more_typing.JSONObject, can_cache: bool = False) -> guilds.PartialGuild: + async def create_guild(self, payload: binding.JSONObject, can_cache: bool = False) -> guilds.PartialGuild: pass - async def update_guild(self, guild: guilds.PartialGuild, payload: more_typing.JSONObject) -> guilds.PartialGuild: + async def update_guild(self, guild: guilds.PartialGuild, payload: binding.JSONObject) -> guilds.PartialGuild: pass async def get_guild(self, guild_id: int) -> typing.Optional[guilds.PartialGuild]: @@ -160,19 +157,19 @@ async def get_guild(self, guild_id: int) -> typing.Optional[guilds.PartialGuild] async def delete_guild(self, guild_id: int) -> typing.Optional[guilds.PartialGuild]: pass - async def create_guild_preview(self, payload: more_typing.JSONObject) -> guilds.GuildPreview: + async def create_guild_preview(self, payload: binding.JSONObject) -> guilds.GuildPreview: pass - async def create_invite(self, payload: more_typing.JSONObject) -> invites.Invite: + async def create_invite(self, payload: binding.JSONObject) -> invites.Invite: pass - async def create_reaction(self, payload: more_typing.JSONObject) -> messages.Reaction: + async def create_reaction(self, payload: binding.JSONObject) -> messages.Reaction: pass - async def create_message(self, payload: more_typing.JSONObject, can_cache: bool = False) -> messages.Message: + async def create_message(self, payload: binding.JSONObject, can_cache: bool = False) -> messages.Message: pass - async def update_message(self, message: messages.Message, payload: more_typing.JSONObject) -> messages.Message: + async def update_message(self, message: messages.Message, payload: binding.JSONObject) -> messages.Message: pass async def get_message(self, channel_id: int, message_id: int) -> typing.Optional[messages.Message]: @@ -181,10 +178,10 @@ async def get_message(self, channel_id: int, message_id: int) -> typing.Optional async def delete_message(self, channel_id: int, message_id: int) -> typing.Optional[messages.Message]: pass - async def create_user(self, payload: more_typing.JSONObject, can_cache: bool = False) -> users.User: + async def create_user(self, payload: binding.JSONObject, can_cache: bool = False) -> users.User: pass - async def update_user(self, user: users.User, payload: more_typing.JSONObject) -> users.User: + async def update_user(self, user: users.User, payload: binding.JSONObject) -> users.User: pass async def get_user(self, user_id: int) -> typing.Optional[users.User]: @@ -193,19 +190,19 @@ async def get_user(self, user_id: int) -> typing.Optional[users.User]: async def delete_user(self, user_id: int) -> typing.Optional[users.User]: pass - async def create_my_user(self, payload: more_typing.JSONObject, can_cache: bool = False) -> users.MyUser: + async def create_my_user(self, payload: binding.JSONObject, can_cache: bool = False) -> users.MyUser: pass - async def update_my_user(self, my_user: users.MyUser, payload: more_typing.JSONObject) -> users.MyUser: + async def update_my_user(self, my_user: users.MyUser, payload: binding.JSONObject) -> users.MyUser: pass async def get_my_user(self) -> typing.Optional[users.User]: pass - async def create_voice_state(self, payload: more_typing.JSONObject, can_cache: bool = False) -> voices.VoiceState: + async def create_voice_state(self, payload: binding.JSONObject, can_cache: bool = False) -> voices.VoiceState: pass - async def update_voice_state(self, payload: more_typing.JSONObject) -> voices.VoiceState: + async def update_voice_state(self, payload: binding.JSONObject) -> voices.VoiceState: pass async def get_voice_state(self, guild_id: int, channel_id: int) -> typing.Optional[voices.VoiceState]: @@ -214,5 +211,5 @@ async def get_voice_state(self, guild_id: int, channel_id: int) -> typing.Option async def delete_voice_state(self, guild_id: int, channel_id: int) -> typing.Optional[voices.VoiceState]: pass - async def create_voice_region(self, payload: more_typing.JSONObject) -> voices.VoiceRegion: + async def create_voice_region(self, payload: binding.JSONObject) -> voices.VoiceRegion: pass diff --git a/hikari/impl/entity_factory.py b/hikari/impl/entity_factory.py index 0f00128637..047b2c450a 100644 --- a/hikari/impl/entity_factory.py +++ b/hikari/impl/entity_factory.py @@ -27,10 +27,10 @@ from hikari import entity_factory from hikari.models import gateway +from hikari.utilities import binding if typing.TYPE_CHECKING: from hikari import app as app_ - from hikari.internal import more_typing from hikari.models import applications from hikari.models import audit_logs from hikari.models import channels @@ -51,72 +51,70 @@ def __init__(self, app: app_.IApp) -> None: def app(self) -> app_.IApp: return self._app - def deserialize_own_connection(self, payload: more_typing.JSONObject) -> applications.OwnConnection: + def deserialize_own_connection(self, payload: binding.JSONObject) -> applications.OwnConnection: pass - def deserialize_own_guild(self, payload: more_typing.JSONObject) -> applications.OwnGuild: + def deserialize_own_guild(self, payload: binding.JSONObject) -> applications.OwnGuild: pass - def deserialize_application(self, payload: more_typing.JSONObject) -> applications: + def deserialize_application(self, payload: binding.JSONObject) -> applications: pass - def deserialize_audit_log(self, payload: more_typing.JSONObject) -> audit_logs.AuditLog: + def deserialize_audit_log(self, payload: binding.JSONObject) -> audit_logs.AuditLog: pass - def deserialize_permission_overwrite(self, payload: more_typing.JSONObject) -> channels.PermissionOverwrite: + def deserialize_permission_overwrite(self, payload: binding.JSONObject) -> channels.PermissionOverwrite: pass - def serialize_permission_overwrite(self, overwrite: channels.PermissionOverwrite) -> more_typing.JSONObject: + def serialize_permission_overwrite(self, overwrite: channels.PermissionOverwrite) -> binding.JSONObject: pass - def deserialize_partial_channel(self, payload: more_typing.JSONObject) -> channels.PartialChannel: + def deserialize_partial_channel(self, payload: binding.JSONObject) -> channels.PartialChannel: pass - def deserialize_dm_channel(self, payload: more_typing.JSONObject) -> channels.DMChannel: + def deserialize_dm_channel(self, payload: binding.JSONObject) -> channels.DMChannel: pass - def deserialize_group_dm_channel(self, payload: more_typing.JSONObject) -> channels.GroupDMChannel: + def deserialize_group_dm_channel(self, payload: binding.JSONObject) -> channels.GroupDMChannel: pass - def deserialize_guild_category(self, payload: more_typing.JSONObject) -> channels.GuildCategory: + def deserialize_guild_category(self, payload: binding.JSONObject) -> channels.GuildCategory: pass - def deserialize_guild_text_channel(self, payload: more_typing.JSONObject) -> channels.GuildTextChannel: + def deserialize_guild_text_channel(self, payload: binding.JSONObject) -> channels.GuildTextChannel: pass - def deserialize_guild_news_channel(self, payload: more_typing.JSONObject) -> channels.GuildNewsChannel: + def deserialize_guild_news_channel(self, payload: binding.JSONObject) -> channels.GuildNewsChannel: pass - def deserialize_guild_store_channel(self, payload: more_typing.JSONObject) -> channels.GuildStoreChannel: + def deserialize_guild_store_channel(self, payload: binding.JSONObject) -> channels.GuildStoreChannel: pass - def deserialize_guild_voice_channel(self, payload: more_typing.JSONObject) -> channels.GuildVoiceChannel: + def deserialize_guild_voice_channel(self, payload: binding.JSONObject) -> channels.GuildVoiceChannel: pass - def deserialize_channel(self, payload: more_typing.JSONObject) -> channels.PartialChannel: + def deserialize_channel(self, payload: binding.JSONObject) -> channels.PartialChannel: pass - def deserialize_embed(self, payload: more_typing.JSONObject) -> embeds.Embed: + def deserialize_embed(self, payload: binding.JSONObject) -> embeds.Embed: pass - def serialize_embed(self, embed: embeds.Embed) -> more_typing.JSONObject: + def serialize_embed(self, embed: embeds.Embed) -> binding.JSONObject: pass - def deserialize_unicode_emoji(self, payload: more_typing.JSONObject) -> emojis.UnicodeEmoji: + def deserialize_unicode_emoji(self, payload: binding.JSONObject) -> emojis.UnicodeEmoji: pass - def deserialize_custom_emoji(self, payload: more_typing.JSONObject) -> emojis.CustomEmoji: + def deserialize_custom_emoji(self, payload: binding.JSONObject) -> emojis.CustomEmoji: pass - def deserialize_known_custom_emoji(self, payload: more_typing.JSONObject) -> emojis.KnownCustomEmoji: + def deserialize_known_custom_emoji(self, payload: binding.JSONObject) -> emojis.KnownCustomEmoji: pass - def deserialize_emoji( - self, payload: more_typing.JSONObject - ) -> typing.Union[emojis.UnicodeEmoji, emojis.CustomEmoji]: + def deserialize_emoji(self, payload: binding.JSONObject) -> typing.Union[emojis.UnicodeEmoji, emojis.CustomEmoji]: pass - def deserialize_gateway_bot(self, payload: more_typing.JSONObject) -> gateway.GatewayBot: + def deserialize_gateway_bot(self, payload: binding.JSONObject) -> gateway.GatewayBot: gateway_bot = gateway.GatewayBot() gateway_bot.url = payload["url"] gateway_bot.shard_count = int(payload["shards"]) @@ -128,58 +126,58 @@ def deserialize_gateway_bot(self, payload: more_typing.JSONObject) -> gateway.Ga gateway_bot.session_start_limit = session_start_limit return gateway_bot - def deserialize_guild_widget(self, payload: more_typing.JSONObject) -> guilds.GuildWidget: + def deserialize_guild_widget(self, payload: binding.JSONObject) -> guilds.GuildWidget: pass def deserialize_guild_member( - self, payload: more_typing.JSONObject, *, user: typing.Optional[users.User] = None + self, payload: binding.JSONObject, *, user: typing.Optional[users.User] = None ) -> guilds.GuildMember: pass - def deserialize_role(self, payload: more_typing.JSONObject) -> guilds.Role: + def deserialize_role(self, payload: binding.JSONObject) -> guilds.Role: pass - def deserialize_guild_member_presence(self, payload: more_typing.JSONObject) -> guilds.GuildMemberPresence: + def deserialize_guild_member_presence(self, payload: binding.JSONObject) -> guilds.GuildMemberPresence: pass - def deserialize_partial_guild_integration(self, payload: more_typing.JSONObject) -> guilds.PartialGuildIntegration: + def deserialize_partial_guild_integration(self, payload: binding.JSONObject) -> guilds.PartialGuildIntegration: pass - def deserialize_guild_integration(self, payload: more_typing.JSONObject) -> guilds.GuildIntegration: + def deserialize_guild_integration(self, payload: binding.JSONObject) -> guilds.GuildIntegration: pass - def deserialize_guild_member_ban(self, payload: more_typing.JSONObject) -> guilds.GuildMemberBan: + def deserialize_guild_member_ban(self, payload: binding.JSONObject) -> guilds.GuildMemberBan: pass - def deserialize_unavailable_guild(self, payload: more_typing.JSONObject) -> guilds.UnavailableGuild: + def deserialize_unavailable_guild(self, payload: binding.JSONObject) -> guilds.UnavailableGuild: pass - def deserialize_guild_preview(self, payload: more_typing.JSONObject) -> guilds.GuildPreview: + def deserialize_guild_preview(self, payload: binding.JSONObject) -> guilds.GuildPreview: pass - def deserialize_guild(self, payload: more_typing.JSONObject) -> guilds.Guild: + def deserialize_guild(self, payload: binding.JSONObject) -> guilds.Guild: pass - def deserialize_vanity_url(self, payload: more_typing.JSONObject) -> invites.VanityURL: + def deserialize_vanity_url(self, payload: binding.JSONObject) -> invites.VanityURL: pass - def deserialize_invite(self, payload: more_typing.JSONObject) -> invites.Invite: + def deserialize_invite(self, payload: binding.JSONObject) -> invites.Invite: pass - def deserialize_invite_with_metadata(self, payload: more_typing.JSONObject) -> invites.InviteWithMetadata: + def deserialize_invite_with_metadata(self, payload: binding.JSONObject) -> invites.InviteWithMetadata: pass - def deserialize_user(self, payload: more_typing.JSONObject) -> users.User: + def deserialize_user(self, payload: binding.JSONObject) -> users.User: pass - def deserialize_my_user(self, payload: more_typing.JSONObject) -> users.MyUser: + def deserialize_my_user(self, payload: binding.JSONObject) -> users.MyUser: pass - def deserialize_voice_state(self, payload: more_typing.JSONObject) -> voices.VoiceState: + def deserialize_voice_state(self, payload: binding.JSONObject) -> voices.VoiceState: pass - def deserialize_voice_region(self, payload: more_typing.JSONObject) -> voices.VoiceRegion: + def deserialize_voice_region(self, payload: binding.JSONObject) -> voices.VoiceRegion: pass - def deserialize_webhook(self, payload: more_typing.JSONObject) -> webhooks.Webhook: + def deserialize_webhook(self, payload: binding.JSONObject) -> webhooks.Webhook: pass diff --git a/hikari/impl/event_manager.py b/hikari/impl/event_manager.py index 4eb4e22b99..cd66d34101 100644 --- a/hikari/impl/event_manager.py +++ b/hikari/impl/event_manager.py @@ -18,13 +18,17 @@ # along with Hikari. If not, see . """Event handling logic.""" +from __future__ import annotations + +__all__ = ["EventManagerImpl"] from hikari.impl import event_manager_core -from hikari.internal import more_typing from hikari.net import gateway +from hikari.utilities import binding class EventManagerImpl(event_manager_core.EventManagerCore): """Provides event handling logic for Discord events.""" - async def _on_message_create(self, shard: gateway.Gateway, payload: more_typing.JSONType) -> None: + + async def _on_message_create(self, shard: gateway.Gateway, payload: binding.JSONObject) -> None: print(shard, payload) diff --git a/hikari/impl/event_manager_core.py b/hikari/impl/event_manager_core.py index b0eb4819a3..87fb129a6f 100644 --- a/hikari/impl/event_manager_core.py +++ b/hikari/impl/event_manager_core.py @@ -28,21 +28,21 @@ from hikari import event_dispatcher from hikari.events import base from hikari.events import other -from hikari.internal import class_helpers -from hikari.internal import more_asyncio from hikari.net import gateway +from hikari.utilities import aio +from hikari.utilities import binding +from hikari.utilities import klass if typing.TYPE_CHECKING: from hikari import app as app_ - from hikari.internal import more_typing _EventT = typing.TypeVar("_EventT", bound=base.HikariEvent, covariant=True) - _PredicateT = typing.Callable[[_EventT], typing.Union[bool, more_typing.Coroutine[bool]]] + _PredicateT = typing.Callable[[_EventT], typing.Union[bool, typing.Coroutine[None, typing.Any, bool]]] _SyncCallbackT = typing.Callable[[_EventT], None] - _AsyncCallbackT = typing.Callable[[_EventT], more_typing.Coroutine[None]] + _AsyncCallbackT = typing.Callable[[_EventT], typing.Coroutine[None, typing.Any, None]] _CallbackT = typing.Union[_SyncCallbackT, _AsyncCallbackT] _ListenerMapT = typing.MutableMapping[typing.Type[_EventT], typing.MutableSequence[_CallbackT]] - _WaiterT = typing.Tuple[_PredicateT, more_typing.Future[_EventT]] + _WaiterT = typing.Tuple[_PredicateT, aio.Future[_EventT]] _WaiterMapT = typing.MutableMapping[typing.Type[_EventT], typing.MutableSet[_WaiterT]] @@ -52,17 +52,18 @@ class EventManagerCore(event_dispatcher.IEventDispatcher, event_consumer.IEventC Specific event handlers should be in functions named `_on_xxx` where `xxx` is the raw event name being dispatched in lower-case. """ + def __init__(self, app: app_.IApp) -> None: self._app = app self._listeners: _ListenerMapT = {} self._waiters: _WaiterMapT = {} - self.logger = class_helpers.get_logger(self) + self.logger = klass.get_logger(self) @property def app(self) -> app_.IApp: return self._app - def dispatch(self, event: base.HikariEvent) -> more_typing.Future[typing.Any]: + def dispatch(self, event: base.HikariEvent) -> aio.Future[typing.Any]: if not isinstance(event, base.HikariEvent): raise TypeError(f"events must be subclasses of HikariEvent, not {type(event).__name__}") @@ -73,7 +74,7 @@ def dispatch(self, event: base.HikariEvent) -> more_typing.Future[typing.Any]: tasks = [] - for cls in mro[:mro.index(base.HikariEvent) + 1]: + for cls in mro[: mro.index(base.HikariEvent) + 1]: cls: typing.Type[_EventT] if cls in self._listeners: @@ -84,9 +85,9 @@ def dispatch(self, event: base.HikariEvent) -> more_typing.Future[typing.Any]: for predicate, future in self._waiters[cls]: tasks.append(self._test_waiter(cls, event, predicate, future)) - return asyncio.gather(*tasks) if tasks else more_asyncio.completed_future() + return asyncio.gather(*tasks) if tasks else aio.completed_future() - async def consume_raw_event(self, shard: gateway.Gateway, event_name: str, payload: more_typing.JSONType) -> None: + async def consume_raw_event(self, shard: gateway.Gateway, event_name: str, payload: binding.JSONObject) -> None: try: callback = getattr(self, "_on_" + event_name.lower()) await callback(shard, payload) @@ -96,12 +97,13 @@ async def consume_raw_event(self, shard: gateway.Gateway, event_name: str, paylo def subscribe( self, event_type: typing.Type[_EventT], - callback: typing.Callable[[_EventT], typing.Union[more_typing.Coroutine[None], None]], + callback: typing.Callable[[_EventT], typing.Union[typing.Coroutine[None, typing.Any, None], None]], ) -> None: if event_type not in self._listeners: self._listeners[event_type] = [] if not asyncio.iscoroutinefunction(callback): + async def wrapper(event): return callback(event) @@ -110,7 +112,7 @@ async def wrapper(event): def unsubscribe( self, event_type: typing.Type[_EventT], - callback: typing.Callable[[_EventT], typing.Union[more_typing.Coroutine[None], None]], + callback: typing.Callable[[_EventT], typing.Union[typing.Coroutine[None, typing.Any, None], None]], ) -> None: if event_type in self._listeners: self._listeners[event_type].remove(callback) @@ -121,6 +123,7 @@ def listen(self, event_type: typing.Type[_EventT]) -> typing.Callable[[_Callback def decorator(callback: _CallbackT) -> _CallbackT: self.subscribe(event_type, callback) return callback + return decorator async def _test_waiter(self, cls, event, predicate, future): diff --git a/hikari/impl/gateway_zookeeper.py b/hikari/impl/gateway_zookeeper.py index 49e00f98e2..14061b86e8 100644 --- a/hikari/impl/gateway_zookeeper.py +++ b/hikari/impl/gateway_zookeeper.py @@ -35,9 +35,8 @@ from hikari import app as app_ from hikari import event_dispatcher from hikari.events import other -from hikari.internal import conversions -from hikari.internal import unset from hikari.net import gateway +from hikari.utilities import unset if typing.TYPE_CHECKING: from hikari import http_settings @@ -97,7 +96,7 @@ async def start(self) -> None: await self._init() self._request_close_event.clear() - self.logger.info("starting %s", conversions.pluralize(len(self._shards), "shard")) + self.logger.info("starting %s shard(s)", len(self._shards)) start_time = time.perf_counter() @@ -221,6 +220,7 @@ async def update_presence( async def _init(self): version = _about.__version__ + # noinspection PyTypeChecker path = os.path.abspath(os.path.dirname(inspect.getsourcefile(_about))) py_impl = platform.python_implementation() py_ver = platform.python_version() diff --git a/hikari/impl/rest_app.py b/hikari/impl/rest_app.py index 0bedfcde62..50c1262ac9 100644 --- a/hikari/impl/rest_app.py +++ b/hikari/impl/rest_app.py @@ -29,9 +29,9 @@ from hikari import http_settings from hikari.impl import cache as cache_impl from hikari.impl import entity_factory as entity_factory_impl -from hikari.internal import class_helpers from hikari.net import rest as rest_ from hikari.net import urls +from hikari.utilities import klass if typing.TYPE_CHECKING: from hikari import cache as cache_ @@ -48,7 +48,7 @@ def __init__( rest_url: str = urls.REST_API_URL, version: int = 6, ) -> None: - self._logger = class_helpers.get_logger(self) + self._logger = klass.get_logger(self) self._rest = rest_.REST( app=self, config=config, debug=debug, token=token, token_type=token_type, url=rest_url, version=version, ) diff --git a/hikari/internal/marshaller.py b/hikari/internal/marshaller.py deleted file mode 100644 index fff35773fb..0000000000 --- a/hikari/internal/marshaller.py +++ /dev/null @@ -1,500 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# Copyright © Nekoka.tt 2019-2020 -# -# This file is part of Hikari. -# -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . -"""An internal marshalling utility used by internal API application. - -!!! warning - You should not change anything in this file, if you do, you will likely get - unexpected behaviour elsewhere. -""" - -from __future__ import annotations - -__all__ = [ - "RAISE", - "dereference_handle", - "attrib", - "marshallable", - "HIKARI_ENTITY_MARSHALLER", - "HikariEntityMarshaller", - "Deserializable", - "Serializable", -] - -import importlib -import typing - -import attr - -from . import more_collections - -if typing.TYPE_CHECKING: - from . import more_typing - -_RAW_NAME_ATTR: typing.Final[str] = __name__ + "_RAW_NAME" -_SERIALIZER_ATTR: typing.Final[str] = __name__ + "_SERIALIZER" -_DESERIALIZER_ATTR: typing.Final[str] = __name__ + "_DESERIALIZER" -_INHERIT_KWARGS: typing.Final[str] = __name__ + "_INHERIT_KWARGS" -_IF_UNDEFINED: typing.Final[str] = __name__ + "IF_UNDEFINED" -_IF_NONE: typing.Final[str] = __name__ + "_IF_NONE" -_MARSHALLER_ATTRIB: typing.Final[str] = __name__ + "_MARSHALLER_ATTRIB" -_PASSED_THROUGH_SINGLETONS: typing.Final[typing.Sequence[bool]] = [False, True, None] -RAISE: typing.Final[typing.Any] = object() -EntityT = typing.TypeVar("EntityT", contravariant=True) -ClsT = typing.Type[EntityT] - - -def dereference_handle(handle_string: str) -> typing.Any: - """Parse a given handle string into an object reference. - - Parameters - ---------- - handle_string : str - The handle to the object to refer to. This is in the format - `fully.qualified.module.name#object.attribute`. If no `#` is - input, then the reference will be made to the module itself. - - Returns - ------- - typing.Any - The thing that is referred to from this reference. - - Examples - -------- - * `"collections#deque"`: - - Refers to `collections.deque` - - * `"asyncio.tasks#Task"`: - - Refers to `asyncio.tasks.Task` - - * `"hikari.rest"`: - - Refers to `hikari.rest` - - * `"foo.bar#baz.bork.qux"`: - - Would refer to a theoretical `qux` attribute on a `bork` - attribute on a `baz` object in the `foo.bar` module. - """ - if "#" not in handle_string: - module, attribute_names = handle_string, () - else: - module, _, attribute_string = handle_string.partition("#") - attribute_names = attribute_string.split(".") - - obj = importlib.import_module(module) - for attr_name in attribute_names: - obj = getattr(obj, attr_name) - - return obj - - -def attrib( - *, - # Mandatory! We do not want to rely on type annotations alone, as they will - # break if we use __future__.annotations anywhere. If we relied on the - # field type, that would work, but attrs doesn't let us supply field.type - # as an attr.ib() kwargs AND use type hints at the same time, and without - # type hints, the library loses the ability to be type checked properly - # anymore, so we have to pass this explicitly regardless. - deserializer: typing.Optional[typing.Callable[[...], typing.Any], type(RAISE)] = RAISE, - if_none: typing.Union[typing.Callable[[], typing.Any], None, type(RAISE)] = RAISE, - if_undefined: typing.Union[typing.Callable[[], typing.Any], None, type(RAISE)] = RAISE, - raw_name: typing.Optional[str] = None, - inherit_kwargs: bool = False, - serializer: typing.Optional[typing.Callable[[typing.Any], typing.Any], type(RAISE)] = RAISE, - **kwargs, -) -> attr.Attribute: - """Create an `attr.ib` with marshaller metadata attached. - - Parameters - ---------- - deserializer : typing.Callable[[...], typing.Any] | None - The deserializer to use to deserialize raw elements. - If `None` then this field will never be deserialized from a payload - and will have to be attached to the object after generation or passed - through to `deserialize` as a kwarg. - raw_name : str | None - The raw name of the element in its raw serialized form. If not provided, - then this will use the field's default name later. - inherit_kwargs : bool - If `True` then any fields passed to deserialize for the entity this - attribute is attached to as kwargs will also be passed through to this - entity's deserializer as kwargs. Defaults to `False`. - if_none : typing.Callable[[], typing.Any] | None - Either a default factory function called to get the default for when - this field is `None` or one of `None`, `False` or `True` to specify that - this should default to the given singleton. Will raise an exception when - `None` is received for this field later if this isn't specified. - if_undefined : typing.Callable[[], typing.Any] | None - Either a default factory function called to get the default for when - this field isn't defined or one of `None`, `False` or `True` to specify - that this should default to the given singleton. Will raise an exception - when this field is undefined later on if this isn't specified. - serializer : typing.Callable[[typing.Any], typing.Any] | None - The serializer to use. If not specified, then serializing the entire - class that this attribute is in will trigger a `TypeError` later. - If `None` then the field will not be serialized. - **kwargs : - Any kwargs to pass to `attr.ib`. - - Returns - ------- - typing.Any - The result of `attr.ib` internally being called with additional metadata. - """ - metadata = kwargs.pop("metadata", {}) - metadata[_RAW_NAME_ATTR] = raw_name - metadata[_SERIALIZER_ATTR] = serializer - metadata[_DESERIALIZER_ATTR] = deserializer - metadata[_IF_NONE] = if_none - metadata[_IF_UNDEFINED] = if_undefined - metadata[_INHERIT_KWARGS] = inherit_kwargs - metadata[_MARSHALLER_ATTRIB] = True - - # Default to not repr-ing a field. - kwargs.setdefault("repr", False) - - attribute = attr.ib(**kwargs, metadata=metadata) - # Fool pylint into thinking this is any type. - return typing.cast(typing.Any, attribute) - - -def _not_implemented(op, name): - def error(*_, **__) -> typing.NoReturn: - raise NotImplementedError(f"Field {name} does not support operation {op}") - - return error - - -def _default_validator(value: typing.Any): - if value is not RAISE and value not in _PASSED_THROUGH_SINGLETONS and not callable(value): - raise RuntimeError( - "Invalid default factory passed for `if_undefined` or `if_none`; " - f"expected a callable or one of the 'passed through singletons' but got {value}." - ) - - -class _AttributeDescriptor: - __slots__ = ( - "raw_name", - "field_name", - "constructor_name", - "if_none", - "if_undefined", - "is_inheriting_kwargs", - "deserializer", - "serializer", - ) - - def __init__( # pylint: disable=too-many-arguments - self, - raw_name: str, - field_name: str, - constructor_name: str, - if_none: typing.Union[typing.Callable[..., typing.Any], None, type(RAISE)], - if_undefined: typing.Union[typing.Callable[..., typing.Any], None, type(RAISE)], - is_inheriting_kwargs: bool, - deserializer: typing.Callable[..., typing.Any], - serializer: typing.Callable[[typing.Any], typing.Any], - ) -> None: - _default_validator(if_undefined) - _default_validator(if_none) - self.raw_name = raw_name - self.field_name = field_name - self.constructor_name = constructor_name - self.if_none = if_none - self.if_undefined = if_undefined - self.is_inheriting_kwargs = is_inheriting_kwargs - self.deserializer = deserializer - self.serializer = serializer - - -class _EntityDescriptor: - __slots__ = ("entity_type", "attribs") - - def __init__(self, entity_type: typing.Type, attribs: typing.Collection[_AttributeDescriptor]) -> None: - self.entity_type = entity_type - self.attribs = tuple(attribs) - - -def _construct_attribute_descriptor(field: attr.Attribute) -> _AttributeDescriptor: - raw_name = typing.cast(str, field.metadata.get(_RAW_NAME_ATTR) or field.name) - field_name = typing.cast(str, field.name) - - constructor_name = field_name - - # Attrs strips leading underscores for generated __init__ methods. - while constructor_name.startswith("_"): - constructor_name = constructor_name[1:] - - deserializer = field.metadata[_DESERIALIZER_ATTR] - serializer = field.metadata[_SERIALIZER_ATTR] - - return _AttributeDescriptor( - raw_name=raw_name, - field_name=field_name, - constructor_name=constructor_name, - if_none=field.metadata[_IF_NONE], - if_undefined=field.metadata[_IF_UNDEFINED], - is_inheriting_kwargs=field.metadata[_INHERIT_KWARGS], - deserializer=deserializer if deserializer is not RAISE else _not_implemented("deserialize", field_name), - serializer=serializer if serializer is not RAISE else _not_implemented("serialize", field_name), - ) - - -def _construct_entity_descriptor(entity: typing.Any) -> _EntityDescriptor: - if not hasattr(entity, "__attrs_attrs__"): - raise TypeError(f"{entity.__module__}.{entity.__qualname__} is not an attr class") - - return _EntityDescriptor( - entity, - [ - _construct_attribute_descriptor(field) - for field in attr.fields(entity) - if field.metadata.get(_MARSHALLER_ATTRIB) - ], - ) - - -class HikariEntityMarshaller: - """Hikari's utility to manage automated serialization and deserialization. - - It can deserialize and serialize any internal application that that are - decorated with the `marshallable` decorator, and that are - `attr.s` classes using fields with the`attrib` function call descriptor. - """ - - __slots__ = ("_registered_entities",) - - def __init__(self) -> None: - self._registered_entities: typing.MutableMapping[typing.Type, _EntityDescriptor] = {} - - def register(self, cls: typing.Type[EntityT]) -> typing.Type[EntityT]: - """Register an attrs type for fast future deserialization. - - Parameters - ---------- - cls : typing.Type[typing.Any] - The type to register. - - Returns - ------- - typing.Type[typing.Any] - The input argument. This enables this to be used as a decorator if - desired. - - Raises - ------ - TypeError - If the class is not an `attr.s` class. - """ - entity_descriptor = _construct_entity_descriptor(cls) - self._registered_entities[cls] = entity_descriptor - return cls - - def deserialize( - self, raw_data: more_typing.JSONObject, target_type: typing.Type[EntityT], **injected_kwargs: typing.Any - ) -> EntityT: - """Deserialize a given raw data item into the target type. - - Parameters - ---------- - raw_data : typing.Mapping[str, typing.Any] - The raw data to deserialize. - target_type : typing.Type[typing.Any] - The type to deserialize to. - **injected_kwargs : - Attributes to inject into the entity. These still need to be - included in the model's slots and should normally be fields where - both `deserializer` and `serializer` are set to `None`. - - Returns - ------- - typing.Any - The deserialized instance. - - Raises - ------ - LookupError - If the entity is not registered. - AttributeError - If the field is not optional, but the field was not present in the - raw payload, or it was present, but it was assigned `None`. - TypeError - If the deserialization call failed for some reason. - """ - try: - descriptor = self._registered_entities[target_type] - except KeyError: - raise LookupError(f"No registered entity {target_type.__module__}.{target_type.__qualname__}") - - kwargs = {} - - for a in descriptor.attribs: - if a.deserializer is None: - continue - kwarg_name = a.constructor_name - - if a.raw_name not in raw_data: - if a.if_undefined is RAISE: - raise AttributeError( - "Failed to deserialize data to instance of " - f"{target_type.__module__}.{target_type.__qualname__} due to required field {a.field_name} " - f"(from raw key {a.raw_name}) not being included in the input payload\n\n{repr(raw_data)}" - ) - if a.if_undefined in _PASSED_THROUGH_SINGLETONS: - kwargs[kwarg_name] = a.if_undefined - else: - kwargs[kwarg_name] = a.if_undefined() - continue - - if (data := raw_data[a.raw_name]) is None: - if a.if_none is RAISE: - raise AttributeError( - "Failed to deserialize data to instance of " - f"{target_type.__module__}.{target_type.__qualname__} due to non-nullable field " - f" (from raw key {a.raw_name!r}) being `None` in the input payload\n\n{raw_data!r}" - ) - if a.if_none in _PASSED_THROUGH_SINGLETONS: - kwargs[kwarg_name] = a.if_none - else: - kwargs[kwarg_name] = a.if_none() - continue - - try: - kwargs[kwarg_name] = a.deserializer( - data, **(injected_kwargs if a.is_inheriting_kwargs else more_collections.EMPTY_DICT) - ) - except Exception as exc: - raise TypeError( - "Failed to deserialize data to instance of " - f"{target_type.__module__}.{target_type.__qualname__} because marshalling failed on " - f"attribute {a.field_name!r} (passed to constructor as {kwarg_name!r})\n\n{data!r}" - ) from exc - - return target_type(**kwargs, **injected_kwargs) - - def serialize(self, obj: typing.Optional[typing.Any]) -> more_typing.NullableJSONObject: - """Serialize a given entity into a raw data item. - - Parameters - ---------- - obj : typing.Any | None - The entity to serialize. - - Returns - ------- - typing.Mapping[str, typing.Any] | None - The serialized raw data item. - - Raises - ------ - LookupError - If the entity is not registered. - """ - if obj is None: - return None - - input_type = type(obj) - - try: - descriptor = self._registered_entities[input_type] - except KeyError: - raise LookupError(f"No registered entity {input_type.__module__}.{input_type.__qualname__}") - - raw_data = {} - - for a in descriptor.attribs: - if a.serializer is None: - continue - if (value := getattr(obj, a.field_name)) is not None: - raw_data[a.raw_name] = a.serializer(value) - - return raw_data - - -HIKARI_ENTITY_MARSHALLER = HikariEntityMarshaller() - - -def marshallable(*, marshaller: HikariEntityMarshaller = HIKARI_ENTITY_MARSHALLER) -> typing.Callable[[ClsT], ClsT]: - """Create a decorator for a class to make it into an `attr.s` class. - - Parameters - ---------- - marshaller : HikariEntityMarshaller - If specified, this should be an instance of a marshaller to use. For - most internal purposes, you want to not specify this, since it will - then default to the hikari-global marshaller instead. This is - useful, however, for testing and for external usage. - - !!! note - The `auto_attribs` functionality provided by `attr.s` is not - supported by this marshaller utility. Do not attempt to use it! - - Returns - ------- - typing.Callable - A decorator to decorate a class with. - - Examples - -------- - @attrs() - class MyEntity: - id: int = attrib(deserializer=int, serializer=str) - password: str = attrib(deserializer=int, transient=True) - ... - - """ - - def decorator(cls: ClsT) -> ClsT: - marshaller.register(cls) - return cls - - return decorator - - -class Deserializable: - """Mixin that enables the class to be deserialized from a raw entity.""" - - __slots__ = () - - @classmethod - def deserialize( - cls: typing.Type[more_typing._T_contra], payload: more_typing.JSONType, **kwargs - ) -> more_typing._T_contra: - """Deserialize the given payload into the object. - - Parameters - ---------- - payload - The payload to deserialize into the object. - """ - return HIKARI_ENTITY_MARSHALLER.deserialize(payload, cls, **kwargs) - - -class Serializable: - """Mixin that enables an instance of the class to be serialized.""" - - __slots__ = () - - def serialize(self: more_typing._T_contra) -> more_typing.JSONType: - """Serialize this instance into a naive value.""" - return HIKARI_ENTITY_MARSHALLER.serialize(self) diff --git a/hikari/internal/more_asyncio.py b/hikari/internal/more_asyncio.py deleted file mode 100644 index 9bc1460b8c..0000000000 --- a/hikari/internal/more_asyncio.py +++ /dev/null @@ -1,78 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# Copyright © Nekokatt 2019-2020 -# -# This file is part of Hikari. -# -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . -"""Asyncio extensions and utilities.""" - -from __future__ import annotations - -__all__ = ["completed_future", "wait", "is_async_iterator", "is_async_iterable"] - -import asyncio -import inspect -import typing - -if typing.TYPE_CHECKING: - from hikari.internal import more_typing - - _T_contra = typing.TypeVar("_T_contra", contravariant=True) - - -def completed_future(result: _T_contra = None, /) -> more_typing.Future[_T_contra]: - """Create a future on the current running loop that is completed, then return it. - - Parameters - ---------- - result : typing.Any - The value to set for the result of the future. - - Returns - ------- - asyncio.Future - The completed future. - """ - future = asyncio.get_event_loop().create_future() - future.set_result(result) - return future - - -# On Python3.8.2, there appears to be a bug with the typing module: - -# >>> class Aiterable: -# ... async def __aiter__(self): -# ... yield ... -# >>> isinstance(Aiterable(), typing.AsyncIterable) -# True - -# >>> class Aiterator: -# ... async def __anext__(self): -# ... return ... -# >>> isinstance(Aiterator(), typing.AsyncIterator) -# False - -# ... so I guess I will have to determine this some other way. - - -def is_async_iterator(obj: typing.Any) -> bool: - """Determine if the object is an async iterator or not.""" - return asyncio.iscoroutinefunction(getattr(obj, "__anext__", None)) - - -def is_async_iterable(obj: typing.Any) -> bool: - """Determine if the object is an async iterable or not.""" - attr = getattr(obj, "__aiter__", None) - return inspect.isfunction(attr) or inspect.ismethod(attr) diff --git a/hikari/internal/more_collections.py b/hikari/internal/more_collections.py deleted file mode 100644 index b7b7830e7c..0000000000 --- a/hikari/internal/more_collections.py +++ /dev/null @@ -1,76 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# Copyright © Nekokatt 2019-2020 -# -# This file is part of Hikari. -# -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . -"""Special data structures and utilities.""" - -from __future__ import annotations - -__all__ = [ - "EMPTY_SEQUENCE", - "EMPTY_SET", - "EMPTY_COLLECTION", - "EMPTY_DICT", - "WeakKeyDictionary", -] - -import types -import typing - -import weakref - -_T = typing.TypeVar("_T") -_K = typing.TypeVar("_K", bound=typing.Hashable) -_V = typing.TypeVar("_V") - -EMPTY_SEQUENCE: typing.Final[typing.Sequence[_T]] = tuple() -EMPTY_SET: typing.Final[typing.AbstractSet[_T]] = frozenset() -EMPTY_COLLECTION: typing.Final[typing.Collection[_T]] = tuple() -EMPTY_DICT: typing.Final[typing.Mapping[_K, _V]] = types.MappingProxyType({}) - - -class WeakKeyDictionary(typing.Generic[_K, _V], weakref.WeakKeyDictionary, typing.MutableMapping[_K, _V]): - """A dictionary that has weak references to the keys. - - This is a type-safe version of `weakref.WeakKeyDictionary` which is - subscriptable. - - Examples - -------- - @attr.s(auto_attribs=True) - class Commands: - instances: Set[Command] - aliases: WeakKeyDictionary[Command, str] - """ - - __slots__ = () - - -class WeakValueDictionary(typing.Generic[_K, _V], weakref.WeakValueDictionary, typing.MutableMapping[_K, _V]): - """A dictionary that has weak references to the values. - - This is a type-safe version of `weakref.WeakValueDictionary` which is - subscriptable. - - Examples - -------- - @attr.s(auto_attribs=True) - class Commands: - aliases: WeakValueDictionary[str, Command] - """ - - __slots__ = () diff --git a/hikari/internal/more_enums.py b/hikari/internal/more_enums.py deleted file mode 100644 index 3b70ce295c..0000000000 --- a/hikari/internal/more_enums.py +++ /dev/null @@ -1,62 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# Copyright © Nekoka.tt 2019-2020 -# -# This file is part of Hikari. -# -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . -"""Mixin utilities for defining enums.""" - -from __future__ import annotations - -__all__ = ["Enum", "IntFlag", "must_be_unique", "generated_value"] - -import enum -import typing - - -class Enum(enum.Enum): - """A non-flag enum type. - - This gives a more meaningful `__str__` implementation than what is defined - in the `enum` module by default. - """ - - __slots__ = () - - name: str - """The name of the enum member.""" - - def __str__(self) -> str: - return self.name - - -class IntFlag(enum.IntFlag): - """Base for an integer flag enum type. - - This gives a more meaningful `__str__` implementation than what is defined - in the `enum` module by default. - """ - - __slots__ = () - - name: str - """The name of the enum member.""" - - def __str__(self) -> str: - return ", ".join(flag.name for flag in typing.cast(typing.Iterable, type(self)) if flag & self) - - -must_be_unique = enum.unique -generated_value = enum.auto diff --git a/hikari/internal/more_typing.py b/hikari/internal/more_typing.py deleted file mode 100644 index d94df6c9e4..0000000000 --- a/hikari/internal/more_typing.py +++ /dev/null @@ -1,207 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# Copyright © Nekoka.tt 2019-2020 -# -# This file is part of Hikari. -# -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . -"""Various reusable type-hints for this library.""" -# pylint:disable=unused-variable - -from __future__ import annotations - -__all__ = [ - "JSONType", - "NullableJSONArray", - "JSONObject", - "NullableJSONObject", - "JSONArray", - "NullableJSONType", - "Headers", - "Coroutine", - "Future", - "Task", - "TimeSpanT", -] - -# Hide any imports; this encourages any uses of this to use the typing module -# for regular stuff rather than relying on it being in here as well. -# pylint: disable=ungrouped-imports -import datetime as _datetime - -from typing import Any as _Any -from typing import AnyStr as _AnyStr -from typing import Coroutine as _Coroutine -from typing import Generator as _Generator -from typing import Mapping as _Mapping -from typing import Optional as _Optional -from typing import Protocol as _Protocol -from typing import runtime_checkable -from typing import Sequence as _Sequence -from typing import TYPE_CHECKING as _TYPE_CHECKING -from typing import TypeVar as _TypeVar -from typing import Union as _Union - -if _TYPE_CHECKING: - import asyncio - import contextvars - - from types import FrameType as _FrameType - from typing import Callable as _Callable - from typing import IO as _IO - -# pylint: enable=ungrouped-imports - -_T_contra = _TypeVar("_T_contra", contravariant=True) -# noinspection PyShadowingBuiltins -_T_co = _TypeVar("_T_co", covariant=True) - -########################## -# HTTP TYPE HINT HELPERS # -########################## - -JSONType = _Union[ - _Mapping[str, "NullableJSONType"], _Sequence["NullableJSONType"], _AnyStr, int, float, bool, -] -"""Any JSON type.""" - -NullableJSONType = _Optional[JSONType] -"""Any JSON type, including `null`.""" - -JSONObject = _Mapping[str, NullableJSONType] -"""A mapping produced from a JSON object.""" - -NullableJSONObject = _Optional[JSONObject] -"""A mapping produced from a JSON object that may or may not be present.""" - -JSONArray = _Sequence[NullableJSONType] -"""A sequence produced from a JSON array.""" - -NullableJSONArray = _Optional[JSONArray] -"""A sequence produced from a JSON array that may or may not be present.""" - -Headers = _Mapping[str, _Union[_Sequence[str], str]] -"""HTTP headers with case insensitive strings.""" - -############################# -# ASYNCIO TYPE HINT HELPERS # -############################# - -Coroutine = _Coroutine[_Any, _Any, _T_co] -"""A coroutine object. - -This is awaitable but MUST be awaited somewhere to be completed correctly. -""" - - -@runtime_checkable -class Future(_Protocol[_T_contra]): - """Typed protocol representation of an `asyncio.Future`. - - You should consult the documentation for `asyncio.Future` for usage. - """ - - def result(self) -> _T_contra: - """See `asyncio.Future.result`.""" - - def set_result(self, result: _T_contra, /) -> None: - """See `asyncio.Future.set_result`.""" - - def set_exception(self, exception: Exception, /) -> None: - """See `asyncio.Future.set_exception`.""" - - def done(self) -> bool: - """See `asyncio.Future.done`.""" - - def cancelled(self) -> bool: - """See `asyncio.Future.cancelled`.""" - - def add_done_callback( - self, callback: _Callable[[Future[_T_contra]], None], /, *, context: _Optional[contextvars.Context], - ) -> None: - """See `asyncio.Future.add_done_callback`.""" - - def remove_done_callback(self, callback: _Callable[[Future[_T_contra]], None], /) -> None: - """See `asyncio.Future.remove_done_callback`.""" - - def cancel(self) -> bool: - """See `asyncio.Future.cancel`.""" - - def exception(self) -> _Optional[Exception]: - """See `asyncio.Future.exception`.""" - - def get_loop(self) -> asyncio.AbstractEventLoop: - """See `asyncio.Future.get_loop`.""" - - def __await__(self) -> _Generator[_T_contra, None, _Any]: - ... - - -@runtime_checkable -class Task(_Protocol[_T_contra]): - """Typed protocol representation of an `asyncio.Task`. - - You should consult the documentation for `asyncio.Task` for usage. - """ - - def result(self) -> _T_contra: - """See`asyncio.Future.result`.""" - - def set_result(self, result: _T_contra, /) -> None: - """See `asyncio.Future.set_result`.""" - - def set_exception(self, exception: Exception, /) -> None: - """See `asyncio.Future.set_exception`.""" - - def done(self) -> bool: - """See `asyncio.Future.done`.""" - - def cancelled(self) -> bool: - """See `asyncio.Future.cancelled`.""" - - def add_done_callback( - self, callback: _Callable[[Future[_T_contra]], None], /, *, context: _Optional[contextvars.Context], - ) -> None: - """See `asyncio.Future.add_done_callback`.""" - - def remove_done_callback(self, callback: _Callable[[Future[_T_contra]], None], /) -> None: - """See `asyncio.Future.remove_done_callback`.""" - - def cancel(self) -> bool: - """See `asyncio.Future.cancel`.""" - - def exception(self) -> _Optional[Exception]: - """See `asyncio.Future.exception`.""" - - def get_loop(self) -> asyncio.AbstractEventLoop: - """See `asyncio.Future.get_loop`.""" - - def get_stack(self, *, limit: _Optional[int] = None) -> _Sequence[_FrameType]: - """See `asyncio.Task.get_stack`.""" - - def print_stack(self, *, limit: _Optional[int] = None, file: _Optional[_IO] = None) -> None: - """See `asyncio.Task.print_stack`.""" - - def get_name(self) -> str: - """See `asyncio.Task.get_name`.""" - - def set_name(self, value: str, /) -> None: - """See `asyncio.Task.set_name`.""" - - def __await__(self) -> _Generator[_T_contra, None, _Any]: - ... - - -TimeSpanT = _Union[float, int, _datetime.timedelta] -"""A measurement of time.""" diff --git a/hikari/models/applications.py b/hikari/models/applications.py index 36c576b028..8b07af38db 100644 --- a/hikari/models/applications.py +++ b/hikari/models/applications.py @@ -34,9 +34,9 @@ import typing import attr - from hikari.internal import marshaller from hikari.internal import more_enums + from . import bases from . import guilds from . import permissions diff --git a/hikari/models/audit_logs.py b/hikari/models/audit_logs.py index 5eb44faf4c..9f9939d234 100644 --- a/hikari/models/audit_logs.py +++ b/hikari/models/audit_logs.py @@ -43,11 +43,11 @@ import typing import attr - from hikari.internal import conversions from hikari.internal import marshaller from hikari.internal import more_collections from hikari.internal import more_enums + from . import bases from . import channels from . import colors diff --git a/hikari/models/bases.py b/hikari/models/bases.py index 5e019741aa..d69534d2ed 100644 --- a/hikari/models/bases.py +++ b/hikari/models/bases.py @@ -26,7 +26,6 @@ import typing import attr - from hikari.internal import conversions from hikari.internal import marshaller diff --git a/hikari/models/channels.py b/hikari/models/channels.py index b1f97692e5..214c921ff2 100644 --- a/hikari/models/channels.py +++ b/hikari/models/channels.py @@ -41,11 +41,11 @@ import typing import attr - from hikari.internal import conversions from hikari.internal import marshaller from hikari.internal import more_collections from hikari.internal import more_enums + from . import bases from . import permissions from . import users diff --git a/hikari/models/embeds.py b/hikari/models/embeds.py index e1c9d0bf18..ef46656f4a 100644 --- a/hikari/models/embeds.py +++ b/hikari/models/embeds.py @@ -35,9 +35,9 @@ import typing import attr - from hikari.internal import conversions from hikari.internal import marshaller + from . import bases from . import colors from . import files diff --git a/hikari/models/emojis.py b/hikari/models/emojis.py index dad095d6bc..fdc23d3c88 100644 --- a/hikari/models/emojis.py +++ b/hikari/models/emojis.py @@ -27,8 +27,8 @@ import unicodedata import attr - from hikari.internal import marshaller + from . import bases from . import files from . import users diff --git a/hikari/models/files.py b/hikari/models/files.py index 542faaa028..2b5701b67b 100644 --- a/hikari/models/files.py +++ b/hikari/models/files.py @@ -67,9 +67,9 @@ import typing import aiohttp +from hikari.internal import more_asyncio from hikari import errors -from hikari.internal import more_asyncio # XXX: find optimal size. MAGIC_NUMBER: typing.Final[int] = 128 * 1024 diff --git a/hikari/models/guilds.py b/hikari/models/guilds.py index 0c24025343..d24196f8a6 100644 --- a/hikari/models/guilds.py +++ b/hikari/models/guilds.py @@ -58,10 +58,10 @@ import typing import attr - from hikari.internal import conversions from hikari.internal import marshaller from hikari.internal import more_enums + from . import bases from . import channels as channels_ from . import colors diff --git a/hikari/models/invites.py b/hikari/models/invites.py index 4d93ca6650..73fac13068 100644 --- a/hikari/models/invites.py +++ b/hikari/models/invites.py @@ -26,10 +26,10 @@ import typing import attr - from hikari.internal import conversions from hikari.internal import marshaller from hikari.internal import more_enums + from . import bases from . import channels from . import guilds diff --git a/hikari/models/messages.py b/hikari/models/messages.py index 809474cb57..a872dc34d2 100644 --- a/hikari/models/messages.py +++ b/hikari/models/messages.py @@ -34,10 +34,10 @@ import typing import attr - from hikari.internal import conversions from hikari.internal import marshaller from hikari.internal import more_enums + from . import applications from . import bases from . import channels diff --git a/hikari/models/users.py b/hikari/models/users.py index 90fe74ec38..bc66f1dd74 100644 --- a/hikari/models/users.py +++ b/hikari/models/users.py @@ -25,9 +25,9 @@ import typing import attr - from hikari.internal import marshaller from hikari.internal import more_enums + from . import bases from ..net import urls diff --git a/hikari/models/voices.py b/hikari/models/voices.py index dfea94109e..e5566889db 100644 --- a/hikari/models/voices.py +++ b/hikari/models/voices.py @@ -25,8 +25,8 @@ import typing import attr - from hikari.internal import marshaller + from . import bases from . import guilds diff --git a/hikari/models/webhooks.py b/hikari/models/webhooks.py index 1b389395ce..d3fed6ef0a 100644 --- a/hikari/models/webhooks.py +++ b/hikari/models/webhooks.py @@ -25,9 +25,9 @@ import typing import attr - from hikari.internal import marshaller from hikari.internal import more_enums + from . import bases from . import users as users_ from ..net import urls diff --git a/hikari/net/buckets.py b/hikari/net/buckets.py index 52d703187e..af2843b159 100644 --- a/hikari/net/buckets.py +++ b/hikari/net/buckets.py @@ -27,10 +27,9 @@ import types import typing -from hikari.internal import more_asyncio -from hikari.internal import more_typing -from hikari.internal import ratelimits +from hikari.net import ratelimits from hikari.net import routes +from hikari.utilities import aio UNKNOWN_HASH: typing.Final[str] = "UNKNOWN" """The hash used for an unknown bucket that has not yet been resolved.""" @@ -72,7 +71,7 @@ def is_unknown(self) -> bool: """Return `True` if the bucket represents an `UNKNOWN` bucket.""" return self.name.startswith(UNKNOWN_HASH) - def acquire(self) -> more_typing.Future[None]: + def acquire(self) -> aio.Future[None]: """Acquire time on this rate limiter. !!! note @@ -85,7 +84,7 @@ def acquire(self) -> more_typing.Future[None]: A future that should be awaited immediately. Once the future completes, you are allowed to proceed with your operation. """ - return more_asyncio.completed_future(None) if self.is_unknown else super().acquire() + return aio.completed_future(None) if self.is_unknown else super().acquire() def update_rate_limit(self, remaining: int, limit: int, reset_at: float) -> None: """Amend the rate limit. @@ -152,7 +151,7 @@ class RESTBucketManager: closed_event: typing.Final[asyncio.Event] """An internal event that is set when the object is shut down.""" - gc_task: typing.Optional[more_typing.Task[None]] + gc_task: typing.Optional[aio.Task[None]] """The internal garbage collector task.""" logger: typing.Final[logging.Logger] @@ -300,7 +299,7 @@ def do_gc_pass(self, expire_after: float) -> None: self.logger.debug("purged %s stale buckets, %s remain in survival, %s active", dead, survival, active) - def acquire(self, compiled_route: routes.CompiledRoute) -> more_typing.Future[None]: + def acquire(self, compiled_route: routes.CompiledRoute) -> aio.Future[None]: """Acquire a bucket for the given _route. Parameters diff --git a/hikari/net/gateway.py b/hikari/net/gateway.py index a086fda0a6..b2a3021a59 100644 --- a/hikari/net/gateway.py +++ b/hikari/net/gateway.py @@ -23,7 +23,7 @@ __all__ = ["Gateway"] import asyncio -import json +import enum import math import time import typing @@ -33,26 +33,25 @@ import aiohttp import attr - from hikari import component from hikari import errors from hikari import http_settings -from hikari.internal import class_helpers -from hikari.internal import more_enums -from hikari.internal import ratelimits -from hikari.internal import unset -from hikari.models import bases -from hikari.models import channels -from hikari.models import guilds from hikari.net import http_client +from hikari.net import ratelimits from hikari.net import user_agents +from hikari.utilities import binding +from hikari.utilities import klass +from hikari.utilities import snowflake +from hikari.utilities import unset if typing.TYPE_CHECKING: import datetime from hikari import app as app_ + from hikari.utilities import aio + from hikari.models import channels + from hikari.models import guilds from hikari.models import intents as intents_ - from hikari.internal import more_typing @attr.s(eq=True, hash=False, kw_only=True, slots=True) @@ -111,8 +110,8 @@ class Gateway(http_client.HTTPClient, component.IComponent): Gateway API version to use. """ - @more_enums.must_be_unique - class _GatewayCloseCode(int, more_enums.Enum): + @enum.unique + class _GatewayCloseCode(enum.IntEnum): RFC_6455_NORMAL_CLOSURE = 1000 RFC_6455_GOING_AWAY = 1001 RFC_6455_PROTOCOL_ERROR = 1002 @@ -142,8 +141,8 @@ class _GatewayCloseCode(int, more_enums.Enum): INVALID_INTENT = 4013 DISALLOWED_INTENT = 4014 - @more_enums.must_be_unique - class _GatewayOpcode(int, more_enums.Enum): + @enum.unique + class _GatewayOpcode(enum.IntEnum): DISPATCH = 0 HEARTBEAT = 1 IDENTIFY = 2 @@ -189,7 +188,7 @@ def __init__( allow_redirects=config.allow_redirects, connector=config.tcp_connector_factory() if config.tcp_connector_factory else None, debug=debug, - logger=class_helpers.get_logger(self, str(shard_id)), + logger=klass.get_logger(self, str(shard_id)), proxy_auth=config.proxy_auth, proxy_headers=config.proxy_headers, proxy_url=config.proxy_url, @@ -247,7 +246,7 @@ def is_alive(self) -> bool: """Return whether the shard is alive.""" return not math.isnan(self.connected_at) - async def start(self) -> more_typing.Task[None]: + async def start(self) -> aio.Task[None]: """Start the shard, wait for it to become ready. Returns @@ -413,8 +412,8 @@ async def update_presence( async def update_voice_state( self, - guild: typing.Union[guilds.PartialGuild, bases.Snowflake, int, str], - channel: typing.Union[channels.GuildVoiceChannel, bases.Snowflake, int, str, None], + guild: typing.Union[guilds.PartialGuild, snowflake.Snowflake, int, str], + channel: typing.Union[channels.GuildVoiceChannel, snowflake.Snowflake, int, str, None], *, self_mute: bool = False, self_deaf: bool = False, @@ -449,7 +448,7 @@ async def update_voice_state( async def _close_ws(self, code: int, message: str): self.logger.debug("sending close frame with code %s and message %r", int(code), message) - # None if the websocket errored on initialziation. + # None if the websocket error'ed on initialization. if self._ws is not None: await self._ws.close(code=code, message=bytes(message, "utf-8")) @@ -568,7 +567,7 @@ async def _poll_events(self) -> None: else: self.logger.debug("ignoring unrecognised opcode %s", op) - async def _receive_json_payload(self) -> more_typing.JSONObject: + async def _receive_json_payload(self) -> binding.JSONObject: message = await self._receive_raw() if message.type == aiohttp.WSMsgType.BINARY: @@ -604,7 +603,7 @@ async def _receive_json_payload(self) -> more_typing.JSONObject: self.logger.debug("encountered unexpected error", exc_info=ex) raise errors.GatewayError("Unexpected websocket exception from gateway") from ex - return json.loads(string) + return binding.load_json(string) async def _receive_zlib_message(self, first_packet: bytes) -> typing.Tuple[int, str]: buff = bytearray(first_packet) @@ -625,13 +624,13 @@ async def _receive_raw(self) -> aiohttp.WSMessage: self.last_message_received = self._now() return packet - async def _send_json(self, payload: more_typing.JSONObject) -> None: + async def _send_json(self, payload: binding.JSONObject) -> None: await self.ratelimiter.acquire() - message = json.dumps(payload) + message = binding.dump_json(payload) self._log_debug_payload(message, "sending json payload") await self._ws.send_str(message) - def _dispatch(self, event_name: str, payload: more_typing.JSONType) -> typing.Coroutine[None]: + def _dispatch(self, event_name: str, payload: binding.JSONObject) -> typing.Coroutine[None, typing.Any, None]: return self._app.event_consumer.consume_raw_event(self, event_name, payload) @staticmethod @@ -654,7 +653,7 @@ def _build_presence_payload( is_afk: typing.Union[unset.Unset, bool] = unset.UNSET, status: typing.Union[unset.Unset, guilds.PresenceStatus] = unset.UNSET, activity: typing.Union[unset.Unset, typing.Optional[Activity]] = unset.UNSET, - ) -> more_typing.JSONObject: + ) -> binding.JSONObject: if unset.is_unset(idle_since): idle_since = self._idle_since if unset.is_unset(is_afk): diff --git a/hikari/net/iterators.py b/hikari/net/iterators.py index c65241eec2..5e562d4352 100644 --- a/hikari/net/iterators.py +++ b/hikari/net/iterators.py @@ -25,21 +25,23 @@ import typing from hikari import app as app_ -from hikari.internal import conversions -from hikari.internal import more_collections -from hikari.internal import more_typing -from hikari.internal import unset from hikari.models import applications from hikari.models import audit_logs -from hikari.models import bases from hikari.models import guilds from hikari.models import messages from hikari.models import users from hikari.net import routes +from hikari.utilities import binding +from hikari.utilities import snowflake +from hikari.utilities import unset _T = typing.TypeVar("_T") +def _empty_generator(): + yield from () + + class LazyIterator(typing.Generic[_T], abc.ABC): """A set of results that are fetched asynchronously from the API as needed. @@ -213,7 +215,7 @@ class _BufferedLazyIterator(typing.Generic[_T], LazyIterator[_T]): __slots__ = ("_buffer",) def __init__(self) -> None: - self._buffer = (_ for _ in more_collections.EMPTY_COLLECTION) + self._buffer = _empty_generator() @abc.abstractmethod async def _next_chunk(self) -> typing.Optional[typing.Generator[typing.Any, None, _T]]: @@ -243,10 +245,10 @@ class MessageIterator(_BufferedLazyIterator[messages.Message]): def __init__( self, app: app_.IRESTApp, - request_call: typing.Callable[..., more_typing.Coroutine[more_typing.JSONArray]], - channel_id: str, + request_call: typing.Callable[..., typing.Coroutine[None, typing.Any, binding.JSONArray]], + channel_id: typing.Union[typing.SupportsInt, int], direction: str, - first_id: str, + first_id: typing.Union[typing.SupportsInt, int], ) -> None: super().__init__() self._app = app @@ -256,7 +258,10 @@ def __init__( self._route = routes.GET_CHANNEL_MESSAGES.compile(channel=channel_id) async def _next_chunk(self) -> typing.Optional[typing.Generator[messages.Message, typing.Any, None]]: - chunk = await self._request_call(self._route, query={self._direction: self._first_id, "limit": 100}) + query = binding.StringMapBuilder() + query.put(self._direction, self._first_id) + query.put("limit", 100) + chunk = await self._request_call(self._route, query) if not chunk: return None @@ -275,19 +280,22 @@ class ReactorIterator(_BufferedLazyIterator[users.User]): def __init__( self, app: app_.IRESTApp, - request_call: typing.Callable[..., more_typing.Coroutine[more_typing.JSONArray]], - channel_id: str, - message_id: str, + request_call: typing.Callable[..., typing.Coroutine[None, typing.Any, binding.JSONArray]], + channel_id: typing.Union[typing.SupportsInt, int], + message_id: typing.Union[typing.SupportsInt, int], emoji: str, ) -> None: super().__init__() self._app = app self._request_call = request_call - self._first_id = bases.Snowflake.min() - self._route = routes.GET_REACTIONS.compile(channel_id=channel_id, message_id=message_id, emoji=emoji) + self._first_id = snowflake.Snowflake.min() + self._route = routes.GET_REACTIONS.compile(channel=channel_id, message=message_id, emoji=emoji) async def _next_chunk(self) -> typing.Optional[typing.Generator[users.User, typing.Any, None]]: - chunk = await self._request_call(self._route, query={"after": self._first_id, "limit": 100}) + query = binding.StringMapBuilder() + query.put("after", self._first_id) + query.put("limit", 100) + chunk = await self._request_call(self._route, query=query) if not chunk: return None @@ -304,9 +312,9 @@ class OwnGuildIterator(_BufferedLazyIterator[applications.OwnGuild]): def __init__( self, app: app_.IRESTApp, - request_call: typing.Callable[..., more_typing.Coroutine[more_typing.JSONArray]], + request_call: typing.Callable[..., typing.Coroutine[None, typing.Any, binding.JSONArray]], newest_first: bool, - first_id: str, + first_id: typing.Union[typing.SupportsInt, int], ) -> None: super().__init__() self._app = app @@ -316,9 +324,11 @@ def __init__( self._route = routes.GET_MY_GUILDS.compile() async def _next_chunk(self) -> typing.Optional[typing.Generator[applications.OwnGuild, typing.Any, None]]: - kwargs = {"before" if self._newest_first else "after": self._first_id, "limit": 100} + query = binding.StringMapBuilder() + query.put("before" if self._newest_first else "after", self._first_id) + query.put("limit", 100) - chunk = await self._request_call(self._route, query=kwargs) + chunk = await self._request_call(self._route, query=query) if not chunk: return None @@ -335,21 +345,25 @@ class MemberIterator(_BufferedLazyIterator[guilds.GuildMember]): def __init__( self, app: app_.IRESTApp, - request_call: typing.Callable[..., more_typing.Coroutine[more_typing.JSONArray]], - guild_id: str, + request_call: typing.Callable[..., typing.Coroutine[None, typing.Any, binding.JSONArray]], + guild_id: typing.Union[typing.SupportsInt, int], ) -> None: super().__init__() self._route = routes.GET_GUILD_MEMBERS.compile(guild=guild_id) self._request_call = request_call self._app = app - self._first_id = bases.Snowflake.min() + self._first_id = snowflake.Snowflake.min() async def _next_chunk(self) -> typing.Optional[typing.Generator[guilds.GuildMember, typing.Any, None]]: - chunk = await self._request_call(self._route, query={"after": self._first_id, "limit": 100}) + query = binding.StringMapBuilder() + query.put("after", self._first_id) + query.put("limit", 100) + chunk = await self._request_call(self._route, query=query) if not chunk: return None + # noinspection PyTypeChecker self._first_id = chunk[-1]["user"]["id"] return (self._app.entity_factory.deserialize_guild_member(m) for m in chunk) @@ -361,10 +375,10 @@ class AuditLogIterator(LazyIterator[audit_logs.AuditLog]): def __init__( self, app: app_.IRESTApp, - request_call: typing.Callable[..., more_typing.Coroutine[more_typing.JSONObject]], - guild_id: str, - before: str, - user_id: typing.Optional[str, unset.Unset], + request_call: typing.Callable[..., typing.Coroutine[None, typing.Any, binding.JSONObject]], + guild_id: typing.Union[typing.SupportsInt, int], + before: typing.Union[typing.SupportsInt, int], + user_id: typing.Union[typing.SupportsInt, int, unset.Unset], action_type: typing.Union[int, unset.Unset], ) -> None: self._action_type = action_type @@ -375,9 +389,10 @@ def __init__( self._user_id = user_id async def __anext__(self) -> audit_logs.AuditLog: - query = {"limit": 100} - conversions.put_if_specified(query, "user_id", self._user_id) - conversions.put_if_specified(query, "event_type", self._action_type) + query = binding.StringMapBuilder() + query.put("limit", 100) + query.put("user_id", self._user_id) + query.put("event_type", self._action_type) response = await self._request_call(self._route, query=query) if not response["entries"]: diff --git a/hikari/internal/ratelimits.py b/hikari/net/ratelimits.py similarity index 98% rename from hikari/internal/ratelimits.py rename to hikari/net/ratelimits.py index cdde72b6e8..b211c9074d 100644 --- a/hikari/internal/ratelimits.py +++ b/hikari/net/ratelimits.py @@ -173,8 +173,7 @@ import time import typing -if typing.TYPE_CHECKING: - from . import more_typing +from hikari.utilities import aio UNKNOWN_HASH: typing.Final[str] = "UNKNOWN" """The hash used for an unknown bucket that has not yet been resolved.""" @@ -192,7 +191,7 @@ class BaseRateLimiter(abc.ABC): __slots__ = () @abc.abstractmethod - def acquire(self) -> more_typing.Future[None]: + def acquire(self) -> aio.Future[None]: """Acquire permission to perform a task that needs to have rate limit management enforced. Returns @@ -225,10 +224,10 @@ class BurstRateLimiter(BaseRateLimiter, abc.ABC): name: typing.Final[str] """The name of the rate limiter.""" - throttle_task: typing.Optional[more_typing.Task[None]] + throttle_task: typing.Optional[aio.Task[None]] """The throttling task, or `None` if it isn't running.""" - queue: typing.Final[typing.List[more_typing.Future[None]]] + queue: typing.Final[typing.List[aio.Future[None]]] """The queue of any futures under a rate limit.""" logger: typing.Final[logging.Logger] @@ -238,11 +237,11 @@ def __init__(self, name: str) -> None: self.name = name self.throttle_task = None self.queue = [] - self.logger = logging.getLogger(f"hikari.internal.ratelimits.{type(self).__qualname__}.{name}") + self.logger = logging.getLogger(f"hikari.utilities.ratelimits.{type(self).__qualname__}.{name}") self._closed = False @abc.abstractmethod - def acquire(self) -> more_typing.Future[None]: + def acquire(self) -> aio.Future[None]: """Acquire time on this rate limiter. The implementation should define this. @@ -310,7 +309,7 @@ class ManualRateLimiter(BurstRateLimiter): def __init__(self) -> None: super().__init__("global") - def acquire(self) -> more_typing.Future[None]: + def acquire(self) -> aio.Future[None]: """Acquire time on this rate limiter. Returns @@ -439,7 +438,7 @@ def __init__(self, name: str, period: float, limit: int) -> None: self.limit = limit self.period = period - def acquire(self) -> more_typing.Future[None]: + def acquire(self) -> aio.Future[None]: """Acquire time on this rate limiter. Returns diff --git a/hikari/net/rest.py b/hikari/net/rest.py index 23d35686e8..80e763d431 100644 --- a/hikari/net/rest.py +++ b/hikari/net/rest.py @@ -24,7 +24,6 @@ import asyncio import datetime import http -import json import typing import aiohttp @@ -32,17 +31,17 @@ from hikari import component from hikari import errors from hikari import http_settings -from hikari.internal import class_helpers -from hikari.internal import conversions -from hikari.internal import more_collections -from hikari.internal import more_typing -from hikari.internal import ratelimits -from hikari.internal import unset from hikari.net import buckets from hikari.net import http_client from hikari.net import iterators +from hikari.net import ratelimits from hikari.net import rest_utils from hikari.net import routes +from hikari.utilities import binding +from hikari.utilities import date +from hikari.utilities import klass +from hikari.utilities import snowflake +from hikari.utilities import unset if typing.TYPE_CHECKING: from hikari import app as app_ @@ -117,7 +116,7 @@ def __init__( allow_redirects=config.allow_redirects, connector=config.tcp_connector_factory() if config.tcp_connector_factory else None, debug=debug, - logger=class_helpers.get_logger(self), + logger=klass.get_logger(self), proxy_auth=config.proxy_auth, proxy_headers=config.proxy_headers, proxy_url=config.proxy_url, @@ -144,12 +143,11 @@ async def _request( self, compiled_route: routes.CompiledRoute, *, - headers: typing.Union[unset.Unset, more_typing.Headers] = unset.UNSET, - query: typing.Union[unset.Unset, typing.Mapping[str, str]] = unset.UNSET, - body: typing.Union[unset.Unset, aiohttp.FormData, more_typing.JSONType] = unset.UNSET, + query: typing.Union[unset.Unset, binding.StringMapBuilder] = unset.UNSET, + body: typing.Union[unset.Unset, aiohttp.FormData, binding.JSONObjectBuilder, binding.JSONArray] = unset.UNSET, reason: typing.Union[unset.Unset, str] = unset.UNSET, no_auth: bool = False, - ) -> typing.Optional[more_typing.JSONObject, more_typing.JSONArray, bytes, str]: + ) -> typing.Optional[binding.JSONObject, binding.JSONArray, bytes, str]: # Make a ratelimit-protected HTTP request to a JSON endpoint and expect some form # of JSON response. If an error occurs, the response body is returned in the # raised exception as a bytes object. This is done since the differences between @@ -160,10 +158,10 @@ async def _request( if not self.buckets.is_started: self.buckets.start() - headers = {} if unset.is_unset(headers) else headers + headers = binding.StringMapBuilder() - headers["x-ratelimit-precision"] = "millisecond" - headers["accept"] = self._APPLICATION_JSON + headers.put("x-ratelimit-precision", "millisecond") + headers.put("accept", self._APPLICATION_JSON) if self._token is not None and not no_auth: headers["authorization"] = self._token @@ -171,8 +169,7 @@ async def _request( if unset.is_unset(body): body = None - if not unset.is_unset(reason): - headers["x-audit-log-reason"] = reason + headers.put("x-audit-log-reason", reason) if unset.is_unset(query): query = None @@ -187,10 +184,10 @@ async def _request( async def _request_once( self, compiled_route: routes.CompiledRoute, - headers: more_typing.Headers, - body: typing.Optional[typing.Union[aiohttp.FormData, more_typing.JSONType]], + headers: binding.Headers, + body: typing.Optional[typing.Union[aiohttp.FormData, binding.JSONArray, binding.JSONObject]], query: typing.Optional[typing.Dict[str, str]], - ) -> typing.Optional[more_typing.JSONObject, more_typing.JSONArray, bytes, str]: + ) -> typing.Optional[binding.JSONObject, binding.JSONArray, bytes, str]: url = compiled_route.create_url(self._url) # Wait for any ratelimits to finish. @@ -218,7 +215,7 @@ async def _request_once( if 200 <= response.status < 300: if response.content_type == self._APPLICATION_JSON: # Only deserializing here stops Cloudflare shenanigans messing us around. - return json.loads(raw_body) + return binding.load_json(raw_body) raise errors.HTTPError(real_url, f"Expected JSON response but received {response.content_type}") if response.status == http.HTTPStatus.BAD_REQUEST: @@ -255,7 +252,7 @@ async def _handle_rate_limits_for_response( bucket = resp_headers.get("x-ratelimit-bucket", "None") reset = float(resp_headers.get("x-ratelimit-reset", "0")) reset_date = datetime.datetime.fromtimestamp(reset, tz=datetime.timezone.utc) - now_date = conversions.rfc7231_datetime_string_to_datetime(resp_headers["date"]) + now_date = date.rfc7231_datetime_string_to_datetime(resp_headers["date"]) self.buckets.update_rate_limits( compiled_route=compiled_route, bucket_header=bucket, @@ -297,8 +294,8 @@ async def _handle_rate_limits_for_response( @staticmethod def _generate_allowed_mentions( mentions_everyone: bool, - user_mentions: typing.Union[typing.Collection[typing.Union[bases.Snowflake, int, str, users.User]], bool], - role_mentions: typing.Union[typing.Collection[typing.Union[bases.Snowflake, int, str, guilds.Role]], bool], + user_mentions: typing.Union[typing.Collection[typing.Union[bases.UniqueObject, users.User]], bool], + role_mentions: typing.Union[typing.Collection[typing.Union[bases.UniqueObject, guilds.Role]], bool], ): parsed_mentions = [] allowed_mentions = {} @@ -313,10 +310,7 @@ def _generate_allowed_mentions( else: allowed_mentions["users"] = list( # dict.fromkeys is used to remove duplicate entries that would cause discord to return an error. - dict.fromkeys( - str(user.id if isinstance(user, bases.Unique) else int(user)) - for user in user_mentions or more_collections.EMPTY_SEQUENCE - ) + dict.fromkeys(str(int(m)) for m in user_mentions) ) if len(allowed_mentions["users"]) > 100: raise ValueError("Only up to 100 users can be provided.") @@ -328,10 +322,7 @@ def _generate_allowed_mentions( else: allowed_mentions["roles"] = list( # dict.fromkeys is used to remove duplicate entries that would cause discord to return an error. - dict.fromkeys( - str(role.id if isinstance(role, bases.Unique) else int(role)) - for role in role_mentions or more_collections.EMPTY_SEQUENCE - ) + dict.fromkeys(str(int(m)) for m in role_mentions) ) if len(allowed_mentions["roles"]) > 100: raise ValueError("Only up to 100 roles can be provided.") @@ -342,10 +333,10 @@ def _generate_allowed_mentions( return allowed_mentions def _build_message_creation_form( - self, payload: typing.Dict[str, typing.Any], attachments: typing.Sequence[files.BaseStream], + self, payload: binding.JSONObject, attachments: typing.Sequence[files.BaseStream], ) -> aiohttp.FormData: - form = aiohttp.FormData() - form.add_field("payload_json", json.dumps(payload), content_type=self._APPLICATION_JSON) + form = binding.URLEncodedForm() + form.add_field("payload_json", binding.dump_json(payload), content_type=self._APPLICATION_JSON) for i, attachment in enumerate(attachments): form.add_field( f"file{i}", attachment, filename=attachment.filename, content_type=self._APPLICATION_OCTET_STREAM @@ -358,7 +349,7 @@ async def close(self) -> None: self.buckets.close() async def fetch_channel( - self, channel: typing.Union[channels.PartialChannel, bases.UniqueObjectT], /, + self, channel: typing.Union[channels.PartialChannel, bases.UniqueObject], /, ) -> channels.PartialChannel: """Fetch a channel. @@ -385,7 +376,7 @@ async def fetch_channel( hikari.errors.ServerHTTPErrorResponse If an internal error occurs on Discord while handling the request. """ - route = routes.GET_CHANNEL.compile(channel=conversions.value_to_snowflake(channel)) + route = routes.GET_CHANNEL.compile(channel=channel) response = await self._request(route) return self._app.entity_factory.deserialize_channel(response) @@ -408,7 +399,7 @@ async def edit_channel( nsfw: typing.Union[unset.Unset, bool] = unset.UNSET, bitrate: typing.Union[unset.Unset, int] = unset.UNSET, user_limit: typing.Union[unset.Unset, int] = unset.UNSET, - rate_limit_per_user: typing.Union[unset.Unset, more_typing.TimeSpanT] = unset.UNSET, + rate_limit_per_user: typing.Union[unset.Unset, date.TimeSpan] = unset.UNSET, permission_overwrites: typing.Union[unset.Unset, typing.Sequence[channels.PermissionOverwrite]] = unset.UNSET, parent_category: typing.Union[unset.Unset, channels.GuildCategory] = unset.UNSET, reason: typing.Union[unset.Unset, str] = unset.UNSET, @@ -417,7 +408,7 @@ async def edit_channel( async def edit_channel( self, - channel: typing.Union[channels.PartialChannel, bases.UniqueObjectT], + channel: typing.Union[channels.PartialChannel, bases.UniqueObject], /, *, name: typing.Union[unset.Unset, str] = unset.UNSET, @@ -426,7 +417,7 @@ async def edit_channel( nsfw: typing.Union[unset.Unset, bool] = unset.UNSET, bitrate: typing.Union[unset.Unset, int] = unset.UNSET, user_limit: typing.Union[unset.Unset, int] = unset.UNSET, - rate_limit_per_user: typing.Union[unset.Unset, more_typing.TimeSpanT] = unset.UNSET, + rate_limit_per_user: typing.Union[unset.Unset, date.TimeSpan] = unset.UNSET, permission_overwrites: typing.Union[unset.Unset, typing.Sequence[channels.PermissionOverwrite]] = unset.UNSET, parent_category: typing.Union[unset.Unset, channels.GuildCategory] = unset.UNSET, reason: typing.Union[unset.Unset, str] = unset.UNSET, @@ -463,26 +454,24 @@ async def edit_channel( hikari.errors.ServerHTTPErrorResponse If an internal error occurs on Discord while handling the request. """ - route = routes.PATCH_CHANNEL.compile(channel=conversions.value_to_snowflake(channel)) - payload = {} - conversions.put_if_specified(payload, "name", name) - conversions.put_if_specified(payload, "position", position) - conversions.put_if_specified(payload, "topic", topic) - conversions.put_if_specified(payload, "nsfw", nsfw) - conversions.put_if_specified(payload, "bitrate", bitrate) - conversions.put_if_specified(payload, "user_limit", user_limit) - conversions.put_if_specified(payload, "rate_limit_per_user", rate_limit_per_user) - conversions.put_if_specified(payload, "parent_id", parent_category, conversions.value_to_snowflake) - - if not unset.is_unset(permission_overwrites): - payload["permission_overwrites"] = [ - self._app.entity_factory.serialize_permission_overwrite(p) for p in permission_overwrites - ] - - response = await self._request(route, body=payload, reason=reason) + route = routes.PATCH_CHANNEL.compile(channel=channel) + body = binding.JSONObjectBuilder() + body.put("name", name) + body.put("position", position) + body.put("topic", topic) + body.put("nsfw", nsfw) + body.put("bitrate", bitrate) + body.put("user_limit", user_limit) + body.put("rate_limit_per_user", rate_limit_per_user) + body.put_snowflake("parent_id", parent_category) + body.put_array( + "permission_overwrites", permission_overwrites, self._app.entity_factory.serialize_permission_overwrite + ) + + response = await self._request(route, body=body, reason=reason) return self._app.entity_factory.deserialize_channel(response) - async def delete_channel(self, channel: typing.Union[channels.PartialChannel, bases.UniqueObjectT], /) -> None: + async def delete_channel(self, channel: typing.Union[channels.PartialChannel, bases.UniqueObject], /) -> None: """Delete a channel in a guild, or close a DM. Parameters @@ -501,13 +490,13 @@ async def delete_channel(self, channel: typing.Union[channels.PartialChannel, ba If an internal error occurs on Discord while handling the request. """ - route = routes.DELETE_CHANNEL.compile(channel=conversions.value_to_snowflake(channel)) + route = routes.DELETE_CHANNEL.compile(channel=channel) await self._request(route) @typing.overload async def edit_permission_overwrites( self, - channel: typing.Union[channels.GuildChannel, bases.UniqueObjectT], + channel: typing.Union[channels.GuildChannel, bases.UniqueObject], target: typing.Union[channels.PermissionOverwrite, users.User, guilds.Role], *, allow: typing.Union[unset.Unset, permissions_.Permission] = unset.UNSET, @@ -519,8 +508,8 @@ async def edit_permission_overwrites( @typing.overload async def edit_permission_overwrites( self, - channel: typing.Union[channels.GuildChannel, bases.UniqueObjectT], - target: typing.Union[int, str, bases.Snowflake], + channel: typing.Union[channels.GuildChannel, bases.UniqueObject], + target: typing.Union[int, str, snowflake.Snowflake], target_type: typing.Union[channels.PermissionOverwriteType, str], *, allow: typing.Union[unset.Unset, permissions_.Permission] = unset.UNSET, @@ -531,8 +520,8 @@ async def edit_permission_overwrites( async def edit_permission_overwrites( self, - channel: typing.Union[channels.GuildChannel, bases.UniqueObjectT], - target: typing.Union[bases.UniqueObjectT, users.User, guilds.Role, channels.PermissionOverwrite], + channel: typing.Union[channels.GuildChannel, bases.UniqueObject], + target: typing.Union[bases.UniqueObject, users.User, guilds.Role, channels.PermissionOverwrite], target_type: typing.Union[unset.Unset, channels.PermissionOverwriteType, str] = unset.UNSET, *, allow: typing.Union[unset.Unset, permissions_.Permission] = unset.UNSET, @@ -577,19 +566,18 @@ async def edit_permission_overwrites( "Cannot determine the type of the target to update. Try specifying 'target_type' manually." ) - payload = {"type": target_type} - conversions.put_if_specified(payload, "allow", allow) - conversions.put_if_specified(payload, "deny", deny) - route = routes.PATCH_CHANNEL_PERMISSIONS.compile( - channel=conversions.value_to_snowflake(channel), overwrite=conversions.value_to_snowflake(target), - ) + route = routes.PATCH_CHANNEL_PERMISSIONS.compile(channel=channel, overwrite=target) + body = binding.JSONObjectBuilder() + body.put("type", target_type) + body.put("allow", allow) + body.put("deny", deny) - await self._request(route, body=payload, reason=reason) + await self._request(route, body=body, reason=reason) async def delete_permission_overwrite( self, - channel: typing.Union[channels.GuildChannel, bases.UniqueObjectT], - target: typing.Union[channels.PermissionOverwrite, guilds.Role, users.User, bases.UniqueObjectT], + channel: typing.Union[channels.GuildChannel, bases.UniqueObject], + target: typing.Union[channels.PermissionOverwrite, guilds.Role, users.User, bases.UniqueObject], ) -> None: """Delete a custom permission for an entity in a given guild channel. @@ -609,13 +597,11 @@ async def delete_permission_overwrite( hikari.errors.ServerHTTPErrorResponse If an internal error occurs on Discord while handling the request. """ - route = routes.DELETE_CHANNEL_PERMISSIONS.compile( - channel=conversions.value_to_snowflake(channel), overwrite=conversions.value_to_snowflake(target), - ) + route = routes.DELETE_CHANNEL_PERMISSIONS.compile(channel=channel, overwrite=target) await self._request(route) async def fetch_channel_invites( - self, channel: typing.Union[channels.GuildChannel, bases.UniqueObjectT], / + self, channel: typing.Union[channels.GuildChannel, bases.UniqueObject], / ) -> typing.Sequence[invites.InviteWithMetadata]: """Fetch all invites pointing to the given guild channel. @@ -637,20 +623,20 @@ async def fetch_channel_invites( hikari.errors.ServerHTTPErrorResponse If an internal error occurs on Discord while handling the request. """ - route = routes.GET_CHANNEL_INVITES.compile(channel=conversions.value_to_snowflake(channel)) + route = routes.GET_CHANNEL_INVITES.compile(channel=channel) response = await self._request(route) - return conversions.json_to_collection(response, self._app.entity_factory.deserialize_invite_with_metadata) + return binding.cast_json_array(response, self._app.entity_factory.deserialize_invite_with_metadata) async def create_invite( self, - channel: typing.Union[channels.GuildChannel, bases.UniqueObjectT], + channel: typing.Union[channels.GuildChannel, bases.UniqueObject], /, *, max_age: typing.Union[unset.Unset, int, float, datetime.timedelta] = unset.UNSET, max_uses: typing.Union[unset.Unset, int] = unset.UNSET, temporary: typing.Union[unset.Unset, bool] = unset.UNSET, unique: typing.Union[unset.Unset, bool] = unset.UNSET, - target_user: typing.Union[unset.Unset, users.User, bases.UniqueObjectT] = unset.UNSET, + target_user: typing.Union[unset.Unset, users.User, bases.UniqueObject] = unset.UNSET, target_user_type: typing.Union[unset.Unset, invites.TargetUserType] = unset.UNSET, reason: typing.Union[unset.Unset, str] = unset.UNSET, ) -> invites.InviteWithMetadata: @@ -684,19 +670,19 @@ async def create_invite( hikari.errors.ServerHTTPErrorResponse If an internal error occurs on Discord while handling the request. """ - payload = {} - conversions.put_if_specified(payload, "max_age", max_age, conversions.timespan_to_int) - conversions.put_if_specified(payload, "max_uses", max_uses) - conversions.put_if_specified(payload, "temporary", temporary) - conversions.put_if_specified(payload, "unique", unique), - conversions.put_if_specified(payload, "target_user", target_user, conversions.value_to_snowflake) - conversions.put_if_specified(payload, "target_user_type", target_user_type) - route = routes.POST_CHANNEL_INVITES.compile(channel=conversions.value_to_snowflake(channel)) - response = await self._request(route, body=payload, reason=reason) + route = routes.POST_CHANNEL_INVITES.compile(channel=channel) + body = binding.JSONObjectBuilder() + body.put("max_age", max_age, date.timespan_to_int) + body.put("max_uses", max_uses) + body.put("temporary", temporary) + body.put("unique", unique), + body.put_snowflake("target_user", target_user) + body.put("target_user_type", target_user_type) + response = await self._request(route, body=body, reason=reason) return self._app.entity_factory.deserialize_invite_with_metadata(response) def trigger_typing( - self, channel: typing.Union[channels.TextChannel, bases.UniqueObjectT], / + self, channel: typing.Union[channels.TextChannel, bases.UniqueObject], / ) -> rest_utils.TypingIndicator: """Trigger typing in a text channel. @@ -727,7 +713,7 @@ def trigger_typing( return rest_utils.TypingIndicator(channel, self._request) async def fetch_pins( - self, channel: typing.Union[channels.TextChannel, bases.UniqueObjectT], / + self, channel: typing.Union[channels.TextChannel, bases.UniqueObject], / ) -> typing.Sequence[messages_.Message]: """Fetch the pinned messages in this text channel. @@ -750,14 +736,14 @@ async def fetch_pins( hikari.errors.ServerHTTPErrorResponse If an internal error occurs on Discord while handling the request. """ - route = routes.GET_CHANNEL_PINS.compile(channel=conversions.value_to_snowflake(channel)) + route = routes.GET_CHANNEL_PINS.compile(channel=channel) response = await self._request(route) - return conversions.json_to_collection(response, self._app.entity_factory.deserialize_message) + return binding.cast_json_array(response, self._app.entity_factory.deserialize_message) async def pin_message( self, - channel: typing.Union[channels.TextChannel, bases.UniqueObjectT], - message: typing.Union[messages_.Message, bases.UniqueObjectT], + channel: typing.Union[channels.TextChannel, bases.UniqueObject], + message: typing.Union[messages_.Message, bases.UniqueObject], ) -> None: """Pin an existing message in the given text channel. @@ -778,15 +764,13 @@ async def pin_message( hikari.errors.ServerHTTPErrorResponse If an internal error occurs on Discord while handling the request. """ - route = routes.PUT_CHANNEL_PINS.compile( - channel=conversions.value_to_snowflake(channel), message=conversions.value_to_snowflake(message), - ) + route = routes.PUT_CHANNEL_PINS.compile(channel=channel, message=message) await self._request(route) async def unpin_message( self, - channel: typing.Union[channels.TextChannel, bases.UniqueObjectT], - message: typing.Union[messages_.Message, bases.UniqueObjectT], + channel: typing.Union[channels.TextChannel, bases.UniqueObject], + message: typing.Union[messages_.Message, bases.UniqueObject], ) -> None: """ @@ -812,55 +796,53 @@ async def unpin_message( is awaited or interacted with. Invoking this function itself will not raise any of the above types. """ - route = routes.DELETE_CHANNEL_PIN.compile( - channel=conversions.value_to_snowflake(channel), message=conversions.value_to_snowflake(message), - ) + route = routes.DELETE_CHANNEL_PIN.compile(channel=channel, message=message) await self._request(route) @typing.overload def fetch_messages( - self, channel: typing.Union[channels.TextChannel, bases.UniqueObjectT], / + self, channel: typing.Union[channels.TextChannel, bases.UniqueObject], / ) -> iterators.LazyIterator[messages_.Message]: """Fetch messages, newest first, sent in the given channel.""" @typing.overload def fetch_messages( self, - channel: typing.Union[channels.TextChannel, bases.UniqueObjectT], + channel: typing.Union[channels.TextChannel, bases.UniqueObject], /, *, - before: typing.Union[datetime.datetime, bases.UniqueObjectT], + before: typing.Union[datetime.datetime, bases.UniqueObject], ) -> iterators.LazyIterator[messages_.Message]: """Fetch messages, newest first, sent before a timestamp in the channel.""" @typing.overload def fetch_messages( self, - channel: typing.Union[channels.TextChannel, bases.UniqueObjectT], + channel: typing.Union[channels.TextChannel, bases.UniqueObject], /, *, - around: typing.Union[datetime.datetime, bases.UniqueObjectT], + around: typing.Union[datetime.datetime, bases.UniqueObject], ) -> iterators.LazyIterator[messages_.Message]: """Fetch messages sent around a given time in the channel.""" @typing.overload def fetch_messages( self, - channel: typing.Union[channels.TextChannel, bases.UniqueObjectT], + channel: typing.Union[channels.TextChannel, bases.UniqueObject], /, *, - after: typing.Union[datetime.datetime, bases.UniqueObjectT], + after: typing.Union[datetime.datetime, bases.UniqueObject], ) -> iterators.LazyIterator[messages_.Message]: """Fetch messages, oldest first, sent after a timestamp in the channel.""" def fetch_messages( self, - channel: typing.Union[channels.TextChannel, bases.UniqueObjectT], + channel: typing.Union[channels.TextChannel, bases.UniqueObject], /, *, - before: typing.Union[unset.Unset, datetime.datetime, bases.UniqueObjectT] = unset.UNSET, - after: typing.Union[unset.Unset, datetime.datetime, bases.UniqueObjectT] = unset.UNSET, - around: typing.Union[unset.Unset, datetime.datetime, bases.UniqueObjectT] = unset.UNSET, + before: typing.Union[unset.Unset, datetime.datetime, bases.UniqueObject] = unset.UNSET, + after: typing.Union[unset.Unset, datetime.datetime, bases.UniqueObject] = unset.UNSET, + around: typing.Union[unset.Unset, datetime.datetime, bases.UniqueObject] = unset.UNSET, ) -> iterators.LazyIterator[messages_.Message]: """Browse the message history for a given text channel. @@ -903,23 +885,17 @@ def fetch_messages( elif not unset.is_unset(around): direction, timestamp = "around", around else: - direction, timestamp = "before", bases.Snowflake.max() + direction, timestamp = "before", snowflake.Snowflake.max() if isinstance(timestamp, datetime.datetime): - timestamp = bases.Snowflake.from_datetime(timestamp) - - return iterators.MessageIterator( - self._app, - self._request, - conversions.value_to_snowflake(channel), - direction, - conversions.value_to_snowflake(timestamp), - ) + timestamp = snowflake.Snowflake.from_datetime(timestamp) + + return iterators.MessageIterator(self._app, self._request, channel, direction, timestamp,) async def fetch_message( self, - channel: typing.Union[channels.TextChannel, bases.UniqueObjectT], - message: typing.Union[messages_.Message, bases.UniqueObjectT], + channel: typing.Union[channels.TextChannel, bases.UniqueObject], + message: typing.Union[messages_.Message, bases.UniqueObject], ) -> messages_.Message: """Fetch a specific message in the given text channel. @@ -944,15 +920,13 @@ async def fetch_message( hikari.errors.ServerHTTPErrorResponse If an internal error occurs on Discord while handling the request. """ - route = routes.GET_CHANNEL_MESSAGE.compile( - channel=conversions.value_to_snowflake(channel), message=conversions.value_to_snowflake(message), - ) + route = routes.GET_CHANNEL_MESSAGE.compile(channel=channel, message=message) response = await self._request(route) return self._app.entity_factory.deserialize_message(response) async def create_message( self, - channel: typing.Union[channels.TextChannel, bases.UniqueObjectT], + channel: typing.Union[channels.TextChannel, bases.UniqueObject], text: typing.Union[unset.Unset, typing.Any] = unset.UNSET, *, embed: typing.Union[unset.Unset, embeds_.Embed] = unset.UNSET, @@ -960,8 +934,8 @@ async def create_message( tts: typing.Union[unset.Unset, bool] = unset.UNSET, nonce: typing.Union[unset.Unset, str] = unset.UNSET, mentions_everyone: bool = False, - user_mentions: typing.Union[typing.Collection[typing.Union[users.User, bases.UniqueObjectT]], bool] = True, - role_mentions: typing.Union[typing.Collection[typing.Union[bases.UniqueObjectT, guilds.Role]], bool] = True, + user_mentions: typing.Union[typing.Collection[typing.Union[users.User, bases.UniqueObject]], bool] = True, + role_mentions: typing.Union[typing.Collection[typing.Union[bases.UniqueObject, guilds.Role]], bool] = True, ) -> messages_.Message: """Create a message in the given channel. @@ -1004,35 +978,36 @@ async def create_message( once before being able to use this endpoint for a bot. """ - route = routes.POST_CHANNEL_MESSAGES.compile(channel=conversions.value_to_snowflake(channel)) + route = routes.POST_CHANNEL_MESSAGES.compile(channel=channel) - payload = {"allowed_mentions": self._generate_allowed_mentions(mentions_everyone, user_mentions, role_mentions)} - conversions.put_if_specified(payload, "content", text, str) - conversions.put_if_specified(payload, "embed", embed, self._app.entity_factory.serialize_embed) - conversions.put_if_specified(payload, "nonce", nonce) - conversions.put_if_specified(payload, "tts", tts) + body = binding.JSONObjectBuilder() + body.put("allowed_mentions", self._generate_allowed_mentions(mentions_everyone, user_mentions, role_mentions)) + body.put("content", text, str) + body.put("embed", embed, self._app.entity_factory.serialize_embed) + body.put("nonce", nonce) + body.put("tts", tts) attachments = [] if unset.is_unset(attachments) else [a for a in attachments] if not unset.is_unset(embed): - attachments.extend(embed.assets_to_upload) + attachments += embed.assets_to_upload response = await self._request( - route, body=self._build_message_creation_form(payload, attachments) if attachments else payload + route, body=self._build_message_creation_form(body, attachments) if attachments else body ) return self._app.entity_factory.deserialize_message(response) async def edit_message( self, - channel: typing.Union[channels.TextChannel, bases.UniqueObjectT], - message: typing.Union[messages_.Message, bases.UniqueObjectT], + channel: typing.Union[channels.TextChannel, bases.UniqueObject], + message: typing.Union[messages_.Message, bases.UniqueObject], text: typing.Union[unset.Unset, typing.Any] = unset.UNSET, *, embed: typing.Union[unset.Unset, embeds_.Embed] = unset.UNSET, mentions_everyone: bool = False, - user_mentions: typing.Union[typing.Collection[typing.Union[users.User, bases.UniqueObjectT]], bool] = True, - role_mentions: typing.Union[typing.Collection[typing.Union[bases.UniqueObjectT, guilds.Role]], bool] = True, + user_mentions: typing.Union[typing.Collection[typing.Union[users.User, bases.UniqueObject]], bool] = True, + role_mentions: typing.Union[typing.Collection[typing.Union[bases.UniqueObject, guilds.Role]], bool] = True, flags: typing.Union[unset.Unset, messages_.MessageFlag] = unset.UNSET, ) -> messages_.Message: """Edit an existing message in a given channel. @@ -1065,21 +1040,19 @@ async def edit_message( If an internal error occurs on Discord while handling the request. """ - route = routes.PATCH_CHANNEL_MESSAGE.compile( - channel=conversions.value_to_snowflake(channel), message=conversions.value_to_snowflake(message), - ) - payload = {} - conversions.put_if_specified(payload, "content", text, str) - conversions.put_if_specified(payload, "embed", embed, self._app.entity_factory.serialize_embed) - conversions.put_if_specified(payload, "flags", flags) - payload["allowed_mentions"] = self._generate_allowed_mentions(mentions_everyone, user_mentions, role_mentions) - response = await self._request(route, body=payload) + route = routes.PATCH_CHANNEL_MESSAGE.compile(channel=channel, message=message) + body = binding.JSONObjectBuilder() + body.put("content", text, str) + body.put("embed", embed, self._app.entity_factory.serialize_embed) + body.put("flags", flags) + body.put("allowed_mentions", self._generate_allowed_mentions(mentions_everyone, user_mentions, role_mentions)) + response = await self._request(route, body=body) return self._app.entity_factory.deserialize_message(response) async def delete_message( self, - channel: typing.Union[channels.TextChannel, bases.UniqueObjectT], - message: typing.Union[messages_.Message, bases.UniqueObjectT], + channel: typing.Union[channels.TextChannel, bases.UniqueObject], + message: typing.Union[messages_.Message, bases.UniqueObject], ) -> None: """Delete a given message in a given channel. @@ -1100,15 +1073,13 @@ async def delete_message( hikari.errors.ServerHTTPErrorResponse If an internal error occurs on Discord while handling the request. """ - route = routes.DELETE_CHANNEL_MESSAGE.compile( - channel=conversions.value_to_snowflake(channel), message=conversions.value_to_snowflake(message), - ) + route = routes.DELETE_CHANNEL_MESSAGE.compile(channel=channel, message=message) await self._request(route) async def delete_messages( self, - channel: typing.Union[channels.GuildTextChannel, bases.UniqueObjectT], - *messages: typing.Union[messages_.Message, bases.UniqueObjectT], + channel: typing.Union[channels.GuildTextChannel, bases.UniqueObject], + *messages: typing.Union[messages_.Message, bases.UniqueObject], ) -> None: """Bulk-delete between 2 and 100 messages from the given guild channel. @@ -1132,16 +1103,17 @@ async def delete_messages( If you do not provide between 2 and 100 messages (inclusive). """ if 2 <= len(messages) <= 100: - route = routes.POST_DELETE_CHANNEL_MESSAGES_BULK.compile(channel=conversions.value_to_snowflake(channel)) - payload = {"messages": [conversions.value_to_snowflake(m) for m in messages]} - await self._request(route, body=payload) + route = routes.POST_DELETE_CHANNEL_MESSAGES_BULK.compile(channel=channel) + body = binding.JSONObjectBuilder() + body.put_snowflake_array("messages", messages) + await self._request(route, body=body) else: raise TypeError("Must delete a minimum of 2 messages and a maximum of 100") async def add_reaction( self, - channel: typing.Union[channels.TextChannel, bases.UniqueObjectT], - message: typing.Union[messages_.Message, bases.UniqueObjectT], + channel: typing.Union[channels.TextChannel, bases.UniqueObject], + message: typing.Union[messages_.Message, bases.UniqueObject], emoji: typing.Union[str, emojis.UnicodeEmoji, emojis.KnownCustomEmoji], ) -> None: """Add a reaction emoji to a message in a given channel. @@ -1168,15 +1140,15 @@ async def add_reaction( """ route = routes.PUT_MY_REACTION.compile( emoji=emoji.url_name if isinstance(emoji, emojis.KnownCustomEmoji) else str(emoji), - channel=conversions.value_to_snowflake(channel), - message=conversions.value_to_snowflake(message), + channel=channel, + message=message, ) await self._request(route) async def delete_my_reaction( self, - channel: typing.Union[channels.TextChannel, bases.UniqueObjectT], - message: typing.Union[messages_.Message, bases.UniqueObjectT], + channel: typing.Union[channels.TextChannel, bases.UniqueObject], + message: typing.Union[messages_.Message, bases.UniqueObject], emoji: typing.Union[str, emojis.UnicodeEmoji, emojis.KnownCustomEmoji], ) -> None: """Delete a reaction that your application user created. @@ -1201,152 +1173,147 @@ async def delete_my_reaction( """ route = routes.DELETE_MY_REACTION.compile( emoji=emoji.url_name if isinstance(emoji, emojis.KnownCustomEmoji) else str(emoji), - channel=conversions.value_to_snowflake(channel), - message=conversions.value_to_snowflake(message), + channel=channel, + message=message, ) await self._request(route) async def delete_all_reactions_for_emoji( self, - channel: typing.Union[channels.TextChannel, bases.UniqueObjectT], - message: typing.Union[messages_.Message, bases.UniqueObjectT], + channel: typing.Union[channels.TextChannel, bases.UniqueObject], + message: typing.Union[messages_.Message, bases.UniqueObject], emoji: typing.Union[str, emojis.UnicodeEmoji, emojis.KnownCustomEmoji], ) -> None: route = routes.DELETE_REACTION_EMOJI.compile( emoji=emoji.url_name if isinstance(emoji, emojis.KnownCustomEmoji) else str(emoji), - channel=conversions.value_to_snowflake(channel), - message=conversions.value_to_snowflake(message), + channel=channel, + message=message, ) await self._request(route) async def delete_reaction( self, - channel: typing.Union[channels.TextChannel, bases.UniqueObjectT], - message: typing.Union[messages_.Message, bases.UniqueObjectT], + channel: typing.Union[channels.TextChannel, bases.UniqueObject], + message: typing.Union[messages_.Message, bases.UniqueObject], emoji: typing.Union[str, emojis.UnicodeEmoji, emojis.KnownCustomEmoji], - user: typing.Union[users.User, bases.UniqueObjectT], + user: typing.Union[users.User, bases.UniqueObject], ) -> None: route = routes.DELETE_REACTION_USER.compile( emoji=emoji.url_name if isinstance(emoji, emojis.KnownCustomEmoji) else str(emoji), - channel=conversions.value_to_snowflake(channel), - message=conversions.value_to_snowflake(message), - user=conversions.value_to_snowflake(user), + channel=channel, + message=message, + user=user, ) await self._request(route) async def delete_all_reactions( self, - channel: typing.Union[channels.TextChannel, bases.UniqueObjectT], - message: typing.Union[messages_.Message, bases.UniqueObjectT], + channel: typing.Union[channels.TextChannel, bases.UniqueObject], + message: typing.Union[messages_.Message, bases.UniqueObject], ) -> None: - route = routes.DELETE_ALL_REACTIONS.compile( - channel=conversions.value_to_snowflake(channel), message=conversions.value_to_snowflake(message), - ) - + route = routes.DELETE_ALL_REACTIONS.compile(channel=channel, message=message) await self._request(route) def fetch_reactions_for_emoji( self, - channel: typing.Union[channels.TextChannel, bases.UniqueObjectT], - message: typing.Union[messages_.Message, bases.UniqueObjectT], + channel: typing.Union[channels.TextChannel, bases.UniqueObject], + message: typing.Union[messages_.Message, bases.UniqueObject], emoji: typing.Union[str, emojis.UnicodeEmoji, emojis.KnownCustomEmoji], ) -> iterators.LazyIterator[users.User]: return iterators.ReactorIterator( app=self._app, request_call=self._request, - channel_id=conversions.value_to_snowflake(channel), - message_id=conversions.value_to_snowflake(message), + channel_id=channel, + message_id=message, emoji=emoji.url_name if isinstance(emoji, emojis.KnownCustomEmoji) else str(emoji), ) async def create_webhook( self, - channel: typing.Union[channels.TextChannel, bases.UniqueObjectT], + channel: typing.Union[channels.TextChannel, bases.UniqueObject], name: str, *, avatar: typing.Union[unset.Unset, files.BaseStream] = unset.UNSET, reason: typing.Union[unset.Unset, str] = unset.UNSET, ) -> webhooks.Webhook: - route = routes.POST_WEBHOOK.compile(channel=conversions.value_to_snowflake(channel)) - payload = {"name": name} + route = routes.POST_WEBHOOK.compile(channel=channel) + body = binding.JSONObjectBuilder() + body.put("name", name) if not unset.is_unset(avatar): - payload["avatar"] = await avatar.fetch_data_uri() - response = await self._request(route, body=payload, reason=reason) + body.put("avatar", await avatar.fetch_data_uri()) + + response = await self._request(route, body=body, reason=reason) return self._app.entity_factory.deserialize_webhook(response) async def fetch_webhook( self, - webhook: typing.Union[webhooks.Webhook, bases.UniqueObjectT], + webhook: typing.Union[webhooks.Webhook, bases.UniqueObject], /, *, token: typing.Union[unset.Unset, str] = unset.UNSET, ) -> webhooks.Webhook: if unset.is_unset(token): - route = routes.GET_WEBHOOK.compile(webhook=conversions.value_to_snowflake(webhook)) + route = routes.GET_WEBHOOK.compile(webhook=webhook) else: - route = routes.GET_WEBHOOK_WITH_TOKEN.compile(webhook=conversions.value_to_snowflake(webhook), token=token) + route = routes.GET_WEBHOOK_WITH_TOKEN.compile(webhook=webhook, token=token) response = await self._request(route) return self._app.entity_factory.deserialize_webhook(response) async def fetch_channel_webhooks( - self, channel: typing.Union[channels.TextChannel, bases.UniqueObjectT], / + self, channel: typing.Union[channels.TextChannel, bases.UniqueObject], / ) -> typing.Sequence[webhooks.Webhook]: - route = routes.GET_CHANNEL_WEBHOOKS.compile(channel=conversions.value_to_snowflake(channel)) + route = routes.GET_CHANNEL_WEBHOOKS.compile(channel=channel) response = await self._request(route) - return conversions.json_to_collection(response, self._app.entity_factory.deserialize_webhook) + return binding.cast_json_array(response, self._app.entity_factory.deserialize_webhook) async def fetch_guild_webhooks( - self, guild: typing.Union[guilds.Guild, bases.UniqueObjectT], / + self, guild: typing.Union[guilds.Guild, bases.UniqueObject], / ) -> typing.Sequence[webhooks.Webhook]: - route = routes.GET_GUILD_WEBHOOKS.compile(channel=conversions.value_to_snowflake(guild)) + route = routes.GET_GUILD_WEBHOOKS.compile(channel=guild) response = await self._request(route) - return conversions.json_to_collection(response, self._app.entity_factory.deserialize_webhook) + return binding.cast_json_array(response, self._app.entity_factory.deserialize_webhook) async def edit_webhook( self, - webhook: typing.Union[webhooks.Webhook, bases.UniqueObjectT], + webhook: typing.Union[webhooks.Webhook, bases.UniqueObject], /, *, token: typing.Union[unset.Unset, str] = unset.UNSET, name: typing.Union[unset.Unset, str] = unset.UNSET, avatar: typing.Union[unset.Unset, files.BaseStream] = unset.UNSET, - channel: typing.Union[unset.Unset, channels.TextChannel, bases.UniqueObjectT] = unset.UNSET, + channel: typing.Union[unset.Unset, channels.TextChannel, bases.UniqueObject] = unset.UNSET, reason: typing.Union[unset.Unset, str] = unset.UNSET, ) -> webhooks.Webhook: if unset.is_unset(token): - route = routes.PATCH_WEBHOOK.compile(webhook=conversions.value_to_snowflake(webhook)) + route = routes.PATCH_WEBHOOK.compile(webhook=webhook) else: - route = routes.PATCH_WEBHOOK_WITH_TOKEN.compile( - webhook=conversions.value_to_snowflake(webhook), token=token - ) + route = routes.PATCH_WEBHOOK_WITH_TOKEN.compile(webhook=webhook, token=token) - payload = {} - conversions.put_if_specified(payload, "name", name) - conversions.put_if_specified(payload, "channel", channel, conversions.value_to_snowflake) + body = binding.JSONObjectBuilder() + body.put("name", name) + body.put_snowflake("channel", channel) if not unset.is_unset(avatar): - payload["avatar"] = await avatar.fetch_data_uri() + body.put("avatar", await avatar.fetch_data_uri()) - response = await self._request(route, body=payload, reason=reason) + response = await self._request(route, body=body, reason=reason) return self._app.entity_factory.deserialize_webhook(response) async def delete_webhook( self, - webhook: typing.Union[webhooks.Webhook, bases.UniqueObjectT], + webhook: typing.Union[webhooks.Webhook, bases.UniqueObject], /, *, token: typing.Union[unset.Unset, str] = unset.UNSET, ) -> None: if unset.is_unset(token): - route = routes.DELETE_WEBHOOK.compile(webhook=conversions.value_to_snowflake(webhook)) + route = routes.DELETE_WEBHOOK.compile(webhook=webhook) else: - route = routes.DELETE_WEBHOOK_WITH_TOKEN.compile( - webhook=conversions.value_to_snowflake(webhook), token=token - ) + route = routes.DELETE_WEBHOOK_WITH_TOKEN.compile(webhook=webhook, token=token) await self._request(route) - async def execute_embed( + async def execute_webhook( self, - webhook: typing.Union[webhooks.Webhook, bases.UniqueObjectT], + webhook: typing.Union[webhooks.Webhook, bases.UniqueObject], text: typing.Union[unset.Unset, typing.Any] = unset.UNSET, *, token: typing.Union[unset.Unset, str] = unset.UNSET, @@ -1357,14 +1324,14 @@ async def execute_embed( tts: typing.Union[unset.Unset, bool] = unset.UNSET, wait: typing.Union[unset.Unset, bool] = unset.UNSET, mentions_everyone: bool = False, - user_mentions: typing.Union[typing.Collection[typing.Union[users.User, bases.UniqueObjectT]], bool] = True, - role_mentions: typing.Union[typing.Collection[typing.Union[bases.UniqueObjectT, guilds.Role]], bool] = True, + user_mentions: typing.Union[typing.Collection[typing.Union[users.User, bases.UniqueObject]], bool] = True, + role_mentions: typing.Union[typing.Collection[typing.Union[bases.UniqueObject, guilds.Role]], bool] = True, ) -> messages_.Message: if unset.is_unset(token): - route = routes.POST_WEBHOOK.compile(webhook=conversions.value_to_snowflake(webhook)) + route = routes.POST_WEBHOOK.compile(webhook=webhook) no_auth = False else: - route = routes.POST_WEBHOOK_WITH_TOKEN.compile(webhook=conversions.value_to_snowflake(webhook), token=token) + route = routes.POST_WEBHOOK_WITH_TOKEN.compile(webhook=webhook, token=token) no_auth = True attachments = [] if unset.is_unset(attachments) else [a for a in attachments] @@ -1372,21 +1339,20 @@ async def execute_embed( if not unset.is_unset(embeds): for embed in embeds: - attachments.extend(embed.assets_to_upload) + attachments += embed.assets_to_upload serialized_embeds.append(self._app.entity_factory.serialize_embed(embed)) - payload = {"mentions": self._generate_allowed_mentions(mentions_everyone, user_mentions, role_mentions)} - conversions.put_if_specified(payload, "content", text, str) - conversions.put_if_specified(payload, "embeds", serialized_embeds) - conversions.put_if_specified(payload, "username", username) - conversions.put_if_specified(payload, "avatar_url", avatar_url) - conversions.put_if_specified(payload, "tts", tts) - conversions.put_if_specified(payload, "wait", wait) + body = binding.JSONObjectBuilder() + body.put("mentions", self._generate_allowed_mentions(mentions_everyone, user_mentions, role_mentions)) + body.put("content", text, str) + body.put("embeds", serialized_embeds) + body.put("username", username) + body.put("avatar_url", avatar_url) + body.put("tts", tts) + body.put("wait", wait) response = await self._request( - route, - body=self._build_message_creation_form(payload, attachments) if attachments else payload, - no_auth=no_auth, + route, body=self._build_message_creation_form(body, attachments) if attachments else body, no_auth=no_auth, ) return self._app.entity_factory.deserialize_message(response) @@ -1404,8 +1370,9 @@ async def fetch_recommended_gateway_settings(self) -> gateway.GatewayBot: async def fetch_invite(self, invite: typing.Union[invites.Invite, str]) -> invites.Invite: route = routes.GET_INVITE.compile(invite_code=invite if isinstance(invite, str) else invite.code) - payload = {"with_counts": True} - response = await self._request(route, body=payload) + query = binding.StringMapBuilder() + query.put("with_counts", True) + response = await self._request(route, query=query) return self._app.entity_factory.deserialize_invite(response) async def delete_invite(self, invite: typing.Union[invites.Invite, str]) -> None: @@ -1424,40 +1391,42 @@ async def edit_my_user( avatar: typing.Union[unset.Unset, files.BaseStream] = unset.UNSET, ) -> users.MyUser: route = routes.PATCH_MY_USER.compile() - payload = {} - conversions.put_if_specified(payload, "username", username) + body = binding.JSONObjectBuilder() + body.put("username", username) + if not unset.is_unset(username): - payload["avatar"] = await avatar.fetch_data_uri() - response = await self._request(route, body=payload) + body.put("avatar", await avatar.fetch_data_uri()) + + response = await self._request(route, body=body) return self._app.entity_factory.deserialize_my_user(response) async def fetch_my_connections(self) -> typing.Sequence[applications.OwnConnection]: route = routes.GET_MY_CONNECTIONS.compile() response = await self._request(route) - return [self._app.entity_factory.deserialize_own_connection(c) for c in response] + return binding.cast_json_array(response, self._app.entity_factory.deserialize_own_connection) def fetch_my_guilds( self, *, newest_first: bool = False, - start_at: typing.Union[unset.Unset, guilds.PartialGuild, bases.UniqueObjectT, datetime.datetime] = unset.UNSET, + start_at: typing.Union[unset.Unset, guilds.PartialGuild, bases.UniqueObject, datetime.datetime] = unset.UNSET, ) -> iterators.LazyIterator[applications.OwnGuild]: if unset.is_unset(start_at): - start_at = bases.Snowflake.max() if newest_first else bases.Snowflake.min() + start_at = snowflake.Snowflake.max() if newest_first else snowflake.Snowflake.min() elif isinstance(start_at, datetime.datetime): - start_at = bases.Snowflake.from_datetime(start_at) + start_at = snowflake.Snowflake.from_datetime(start_at) - return iterators.OwnGuildIterator( - self._app, self._request, newest_first, conversions.value_to_snowflake(start_at) - ) + return iterators.OwnGuildIterator(self._app, self._request, newest_first, start_at) - async def leave_guild(self, guild: typing.Union[guilds.Guild, bases.UniqueObjectT], /) -> None: - route = routes.DELETE_MY_GUILD.compile(guild=conversions.value_to_snowflake(guild)) + async def leave_guild(self, guild: typing.Union[guilds.Guild, bases.UniqueObject], /) -> None: + route = routes.DELETE_MY_GUILD.compile(guild=guild) await self._request(route) - async def create_dm_channel(self, user: typing.Union[users.User, bases.UniqueObjectT], /) -> channels.DMChannel: + async def create_dm_channel(self, user: typing.Union[users.User, bases.UniqueObject], /) -> channels.DMChannel: route = routes.POST_MY_CHANNELS.compile() - response = await self._request(route, body={"recipient_id": conversions.value_to_snowflake(user)}) + body = binding.JSONObjectBuilder() + body.put_snowflake("recipient_id", user) + response = await self._request(route, body=body) return self._app.entity_factory.deserialize_dm_channel(response) async def fetch_application(self) -> applications.Application: @@ -1468,27 +1437,25 @@ async def fetch_application(self) -> applications.Application: async def add_user_to_guild( self, access_token: str, - guild: typing.Union[guilds.Guild, bases.UniqueObjectT], - user: typing.Union[users.User, bases.UniqueObjectT], + guild: typing.Union[guilds.Guild, bases.UniqueObject], + user: typing.Union[users.User, bases.UniqueObject], *, nick: typing.Union[unset.Unset, str] = unset.UNSET, roles: typing.Union[ - unset.Unset, typing.Collection[typing.Union[guilds.Role, bases.UniqueObjectT]] + unset.Unset, typing.Collection[typing.Union[guilds.Role, bases.UniqueObject]] ] = unset.UNSET, mute: typing.Union[unset.Unset, bool] = unset.UNSET, deaf: typing.Union[unset.Unset, bool] = unset.UNSET, ) -> typing.Optional[guilds.GuildMember]: - route = routes.PUT_GUILD_MEMBER.compile( - guild=conversions.value_to_snowflake(guild), user=conversions.value_to_snowflake(user), - ) - payload = {"access_token": access_token} - conversions.put_if_specified(payload, "nick", nick) - conversions.put_if_specified(payload, "mute", mute) - conversions.put_if_specified(payload, "deaf", deaf) - if not unset.is_unset(roles): - payload["roles"] = [conversions.value_to_snowflake(r) for r in roles] - - if (response := await self._request(route, body=payload)) is not None: + route = routes.PUT_GUILD_MEMBER.compile(guild=guild, user=user) + body = binding.JSONObjectBuilder() + body.put("access_token", access_token) + body.put("nick", nick) + body.put("mute", mute) + body.put("deaf", deaf) + body.put_snowflake_array("roles", roles) + + if (response := await self._request(route, body=body)) is not None: return self._app.entity_factory.deserialize_guild_member(response) else: # User already is in the guild. @@ -1497,129 +1464,124 @@ async def add_user_to_guild( async def fetch_voice_regions(self) -> typing.Sequence[voices.VoiceRegion]: route = routes.GET_VOICE_REGIONS.compile() response = await self._request(route) - return conversions.json_to_collection(response, self._app.entity_factory.deserialize_voice_region) + return binding.cast_json_array(response, self._app.entity_factory.deserialize_voice_region) - async def fetch_user(self, user: typing.Union[users.User, bases.UniqueObjectT]) -> users.User: - route = routes.GET_USER.compile(user=conversions.value_to_snowflake(user)) + async def fetch_user(self, user: typing.Union[users.User, bases.UniqueObject]) -> users.User: + route = routes.GET_USER.compile(user=user) response = await self._request(route) return self._app.entity_factory.deserialize_user(response) def fetch_audit_log( self, - guild: typing.Union[guilds.Guild, bases.UniqueObjectT], + guild: typing.Union[guilds.Guild, bases.UniqueObject], /, *, - before: typing.Union[unset.Unset, datetime.datetime, bases.UniqueObjectT] = unset.UNSET, - user: typing.Union[unset.Unset, users.User, bases.UniqueObjectT] = unset.UNSET, + before: typing.Union[unset.Unset, datetime.datetime, bases.UniqueObject] = unset.UNSET, + user: typing.Union[unset.Unset, users.User, bases.UniqueObject] = unset.UNSET, event_type: typing.Union[unset.Unset, audit_logs.AuditLogEventType] = unset.UNSET, ) -> iterators.LazyIterator[audit_logs.AuditLog]: - guild = conversions.value_to_snowflake(guild) - user = unset.UNSET if unset.is_unset(user) else conversions.value_to_snowflake(user) - event_type = unset.UNSET if unset.is_unset(event_type) else int(event_type) - if unset.is_unset(before): - before = bases.Snowflake.max() + before = snowflake.Snowflake.max() elif isinstance(before, datetime.datetime): - before = bases.Snowflake.from_datetime(before) + before = snowflake.Snowflake.from_datetime(before) - before = conversions.value_to_snowflake(before) - - return iterators.AuditLogIterator(self._app, self._request, guild, before, user, event_type,) + return iterators.AuditLogIterator(self._app, self._request, guild, before, user, event_type) async def fetch_emoji( self, - guild: typing.Union[guilds.Guild, bases.UniqueObjectT], + guild: typing.Union[guilds.Guild, bases.UniqueObject], # This is an emoji ID, which is the URL-safe emoji name, not the snowflake alone. emoji: typing.Union[emojis.KnownCustomEmoji, str], ) -> emojis.KnownCustomEmoji: route = routes.GET_GUILD_EMOJI.compile( - guild=conversions.value_to_snowflake(guild), - emoji=emoji.url_name if isinstance(emoji, emojis.Emoji) else emoji, + guild=guild, emoji=emoji.url_name if isinstance(emoji, emojis.Emoji) else emoji, ) response = await self._request(route) return self._app.entity_factory.deserialize_known_custom_emoji(response) async def fetch_guild_emojis( - self, guild: typing.Union[guilds.Guild, bases.UniqueObjectT], / + self, guild: typing.Union[guilds.Guild, bases.UniqueObject], / ) -> typing.Set[emojis.KnownCustomEmoji]: - route = routes.GET_GUILD_EMOJIS.compile(guild=conversions.value_to_snowflake(guild)) + route = routes.GET_GUILD_EMOJIS.compile(guild=guild) response = await self._request(route) - return conversions.json_to_collection(response, self._app.entity_factory.deserialize_known_custom_emoji, set) + return binding.cast_json_array(response, self._app.entity_factory.deserialize_known_custom_emoji, set) async def create_emoji( self, - guild: typing.Union[guilds.Guild, bases.UniqueObjectT], + guild: typing.Union[guilds.Guild, bases.UniqueObject], name: str, image: files.BaseStream, *, roles: typing.Union[ - unset.Unset, typing.Collection[typing.Union[guilds.Role, bases.UniqueObjectT]] + unset.Unset, typing.Collection[typing.Union[guilds.Role, bases.UniqueObject]] ] = unset.UNSET, reason: typing.Union[unset.Unset, str] = unset.UNSET, ) -> emojis.KnownCustomEmoji: - route = routes.POST_GUILD_EMOJIS.compile(guild=conversions.value_to_snowflake(guild)) - payload = {"name": name, "image": await image.fetch_data_uri()} - conversions.put_if_specified( - payload, "roles", roles, lambda seq: [conversions.value_to_snowflake(r) for r in seq] - ) - response = await self._request(route, body=payload, reason=reason) + route = routes.POST_GUILD_EMOJIS.compile(guild=guild) + body = binding.JSONObjectBuilder() + body.put("name", name) + if not unset.is_unset(image): + body.put("image", await image.fetch_data_uri()) + + body.put_snowflake_array("roles", roles) + + response = await self._request(route, body=body, reason=reason) + return self._app.entity_factory.deserialize_known_custom_emoji(response) async def edit_emoji( self, - guild: typing.Union[guilds.Guild, bases.UniqueObjectT], + guild: typing.Union[guilds.Guild, bases.UniqueObject], # This is an emoji ID, which is the URL-safe emoji name, not the snowflake alone. emoji: typing.Union[emojis.KnownCustomEmoji, str], *, name: typing.Union[unset.Unset, str] = unset.UNSET, roles: typing.Union[ - unset.Unset, typing.Collection[typing.Union[guilds.Role, bases.UniqueObjectT]] + unset.Unset, typing.Collection[typing.Union[guilds.Role, bases.UniqueObject]] ] = unset.UNSET, reason: typing.Union[unset.Unset, str] = unset.UNSET, ) -> emojis.KnownCustomEmoji: route = routes.PATCH_GUILD_EMOJI.compile( - guild=conversions.value_to_snowflake(guild), - emoji=emoji.url_name if isinstance(emoji, emojis.Emoji) else emoji, + guild=guild, emoji=emoji.url_name if isinstance(emoji, emojis.Emoji) else emoji, ) - payload = {} - conversions.put_if_specified(payload, "name", name) - conversions.put_if_specified( - payload, "roles", roles, lambda seq: [conversions.value_to_snowflake(r) for r in seq] - ) - response = await self._request(route, body=payload, reason=reason) + body = binding.JSONObjectBuilder() + body.put("name", name) + body.put_snowflake_array("roles", roles) + + response = await self._request(route, body=body, reason=reason) + return self._app.entity_factory.deserialize_known_custom_emoji(response) async def delete_emoji( self, - guild: typing.Union[guilds.Guild, bases.UniqueObjectT], + guild: typing.Union[guilds.Guild, bases.UniqueObject], # This is an emoji ID, which is the URL-safe emoji name, not the snowflake alone. emoji: typing.Union[emojis.KnownCustomEmoji, str], # Reason is not currently supported for some reason. See ) -> None: route = routes.DELETE_GUILD_EMOJI.compile( - guild=conversions.value_to_snowflake(guild), - emoji=emoji.url_name if isinstance(emoji, emojis.Emoji) else emoji, + guild=guild, emoji=emoji.url_name if isinstance(emoji, emojis.Emoji) else emoji, ) await self._request(route) def create_guild(self, name: str, /) -> rest_utils.GuildBuilder: return rest_utils.GuildBuilder(app=self._app, name=name, request_call=self._request) - async def fetch_guild(self, guild: typing.Union[guilds.Guild, bases.UniqueObjectT], /) -> guilds.Guild: - route = routes.GET_GUILD.compile(guild=conversions.value_to_snowflake(guild)) + async def fetch_guild(self, guild: typing.Union[guilds.Guild, bases.UniqueObject], /) -> guilds.Guild: + route = routes.GET_GUILD.compile(guild=guild) response = await self._request(route) return self._app.entity_factory.deserialize_guild(response) async def fetch_guild_preview( - self, guild: typing.Union[guilds.PartialGuild, bases.UniqueObjectT], / + self, guild: typing.Union[guilds.PartialGuild, bases.UniqueObject], / ) -> guilds.GuildPreview: - route = routes.GET_GUILD_PREVIEW.compile(guild=conversions.value_to_snowflake(guild)) + route = routes.GET_GUILD_PREVIEW.compile(guild=guild) response = await self._request(route) return self._app.entity_factory.deserialize_guild_preview(response) async def edit_guild( self, - guild: typing.Union[guilds.Guild, bases.UniqueObjectT], + guild: typing.Union[guilds.Guild, bases.UniqueObject], /, *, name: typing.Union[unset.Unset, str] = unset.UNSET, @@ -1627,10 +1589,10 @@ async def edit_guild( verification_level: typing.Union[unset.Unset, guilds.GuildVerificationLevel] = unset.UNSET, default_message_notifications: typing.Union[unset.Unset, guilds.GuildMessageNotificationsLevel] = unset.UNSET, explicit_content_filter_level: typing.Union[unset.Unset, guilds.GuildExplicitContentFilterLevel] = unset.UNSET, - afk_channel: typing.Union[unset.Unset, channels.GuildVoiceChannel, bases.UniqueObjectT] = unset.UNSET, - afk_timeout: typing.Union[unset.Unset, more_typing.TimeSpanT] = unset.UNSET, + afk_channel: typing.Union[unset.Unset, channels.GuildVoiceChannel, bases.UniqueObject] = unset.UNSET, + afk_timeout: typing.Union[unset.Unset, date.TimeSpan] = unset.UNSET, icon: typing.Union[unset.Unset, files.BaseStream] = unset.UNSET, - owner: typing.Union[unset.Unset, users.User, bases.UniqueObjectT] = unset.UNSET, + owner: typing.Union[unset.Unset, users.User, bases.UniqueObject] = unset.UNSET, splash: typing.Union[unset.Unset, files.BaseStream] = unset.UNSET, banner: typing.Union[unset.Unset, files.BaseStream] = unset.UNSET, system_channel: typing.Union[unset.Unset, channels.GuildTextChannel] = unset.UNSET, @@ -1639,51 +1601,50 @@ async def edit_guild( preferred_locale: typing.Union[unset.Unset, str] = unset.UNSET, reason: typing.Union[unset.Unset, str] = unset.UNSET, ) -> guilds.Guild: - route = routes.PATCH_GUILD.compile(guild=conversions.value_to_snowflake(guild)) - payload = {} - conversions.put_if_specified(payload, "name", name) - conversions.put_if_specified(payload, "region", region, str) - conversions.put_if_specified(payload, "verification", verification_level) - conversions.put_if_specified(payload, "notifications", default_message_notifications) - conversions.put_if_specified(payload, "explicit_content_filter", explicit_content_filter_level) - conversions.put_if_specified(payload, "afk_channel_id", afk_channel, conversions.value_to_snowflake) - conversions.put_if_specified(payload, "afk_timeout", afk_timeout, conversions.timespan_to_int) - conversions.put_if_specified(payload, "owner_id", owner, conversions.value_to_snowflake) - conversions.put_if_specified(payload, "system_channel_id", system_channel, conversions.value_to_snowflake) - conversions.put_if_specified(payload, "rules_channel_id", rules_channel, conversions.value_to_snowflake) - conversions.put_if_specified( - payload, "public_updates_channel_id", public_updates_channel, conversions.value_to_snowflake - ) - conversions.put_if_specified(payload, "preferred_locale", preferred_locale, str) + route = routes.PATCH_GUILD.compile(guild=guild) + body = binding.JSONObjectBuilder() + body.put("name", name) + body.put("region", region, str) + body.put("verification", verification_level) + body.put("notifications", default_message_notifications) + body.put("explicit_content_filter", explicit_content_filter_level) + body.put("afk_timeout", afk_timeout) + body.put("preferred_locale", preferred_locale, str) + body.put_snowflake("afk_channel_id", afk_channel) + body.put_snowflake("owner_id", owner) + body.put_snowflake("system_channel_id", system_channel) + body.put_snowflake("rules_channel_id", rules_channel) + body.put_snowflake("public_updates_channel_id", public_updates_channel) if not unset.is_unset(icon): - payload["icon"] = await icon.fetch_data_uri() + body.put("icon", await icon.fetch_data_uri()) if not unset.is_unset(splash): - payload["splash"] = await splash.fetch_data_uri() + body.put("splash", await splash.fetch_data_uri()) if not unset.is_unset(banner): - payload["banner"] = await banner.fetch_data_uri() + body.put("banner", await banner.fetch_data_uri()) + + response = await self._request(route, body=body, reason=reason) - response = await self._request(route, body=payload, reason=reason) return self._app.entity_factory.deserialize_guild(response) - async def delete_guild(self, guild: typing.Union[guilds.Guild, bases.UniqueObjectT]) -> None: - route = routes.DELETE_GUILD.compile(guild=conversions.value_to_snowflake(guild)) + async def delete_guild(self, guild: typing.Union[guilds.Guild, bases.UniqueObject]) -> None: + route = routes.DELETE_GUILD.compile(guild=guild) await self._request(route) async def fetch_guild_channels( - self, guild: typing.Union[guilds.Guild, bases.UniqueObjectT] + self, guild: typing.Union[guilds.Guild, bases.UniqueObject] ) -> typing.Sequence[channels.GuildChannel]: - route = routes.GET_GUILD_CHANNELS.compile(guild=conversions.value_to_snowflake(guild)) + route = routes.GET_GUILD_CHANNELS.compile(guild=guild) response = await self._request(route) - channel_sequence = [self._app.entity_factory.deserialize_channel(c) for c in response] + channel_sequence = binding.cast_json_array(response, self._app.entity_factory.deserialize_channel) # Will always be guild channels unless Discord messes up severely on something! return typing.cast(typing.Sequence[channels.GuildChannel], channel_sequence) async def create_guild_text_channel( self, - guild: typing.Union[guilds.Guild, bases.UniqueObjectT], + guild: typing.Union[guilds.Guild, bases.UniqueObject], name: str, *, position: typing.Union[int, unset.Unset] = unset.UNSET, @@ -1691,7 +1652,7 @@ async def create_guild_text_channel( nsfw: typing.Union[bool, unset.Unset] = unset.UNSET, rate_limit_per_user: typing.Union[int, unset.Unset] = unset.UNSET, permission_overwrites: typing.Union[typing.Sequence[channels.PermissionOverwrite], unset.Unset] = unset.UNSET, - category: typing.Union[channels.GuildCategory, bases.UniqueObjectT, unset.Unset] = unset.UNSET, + category: typing.Union[channels.GuildCategory, bases.UniqueObject, unset.Unset] = unset.UNSET, reason: typing.Union[str, unset.Unset] = unset.UNSET, ) -> channels.GuildTextChannel: channel = await self._create_guild_channel( @@ -1710,7 +1671,7 @@ async def create_guild_text_channel( async def create_guild_news_channel( self, - guild: typing.Union[guilds.Guild, bases.UniqueObjectT], + guild: typing.Union[guilds.Guild, bases.UniqueObject], name: str, *, position: typing.Union[int, unset.Unset] = unset.UNSET, @@ -1718,7 +1679,7 @@ async def create_guild_news_channel( nsfw: typing.Union[bool, unset.Unset] = unset.UNSET, rate_limit_per_user: typing.Union[int, unset.Unset] = unset.UNSET, permission_overwrites: typing.Union[typing.Sequence[channels.PermissionOverwrite], unset.Unset] = unset.UNSET, - category: typing.Union[channels.GuildCategory, bases.UniqueObjectT, unset.Unset] = unset.UNSET, + category: typing.Union[channels.GuildCategory, bases.UniqueObject, unset.Unset] = unset.UNSET, reason: typing.Union[str, unset.Unset] = unset.UNSET, ) -> channels.GuildNewsChannel: channel = await self._create_guild_channel( @@ -1737,7 +1698,7 @@ async def create_guild_news_channel( async def create_guild_voice_channel( self, - guild: typing.Union[guilds.Guild, bases.UniqueObjectT], + guild: typing.Union[guilds.Guild, bases.UniqueObject], name: str, *, position: typing.Union[int, unset.Unset] = unset.UNSET, @@ -1745,7 +1706,7 @@ async def create_guild_voice_channel( user_limit: typing.Union[int, unset.Unset] = unset.UNSET, bitrate: typing.Union[int, unset.Unset] = unset.UNSET, permission_overwrites: typing.Union[typing.Sequence[channels.PermissionOverwrite], unset.Unset] = unset.UNSET, - category: typing.Union[channels.GuildCategory, bases.UniqueObjectT, unset.Unset] = unset.UNSET, + category: typing.Union[channels.GuildCategory, bases.UniqueObject, unset.Unset] = unset.UNSET, reason: typing.Union[str, unset.Unset] = unset.UNSET, ) -> channels.GuildVoiceChannel: channel = await self._create_guild_channel( @@ -1764,7 +1725,7 @@ async def create_guild_voice_channel( async def create_guild_category( self, - guild: typing.Union[guilds.Guild, bases.UniqueObjectT], + guild: typing.Union[guilds.Guild, bases.UniqueObject], name: str, *, position: typing.Union[int, unset.Unset] = unset.UNSET, @@ -1785,7 +1746,7 @@ async def create_guild_category( async def _create_guild_channel( self, - guild: typing.Union[guilds.Guild, bases.UniqueObjectT], + guild: typing.Union[guilds.Guild, bases.UniqueObject], name: str, type_: channels.ChannelType, *, @@ -1796,192 +1757,165 @@ async def _create_guild_channel( user_limit: typing.Union[int, unset.Unset] = unset.UNSET, rate_limit_per_user: typing.Union[int, unset.Unset] = unset.UNSET, permission_overwrites: typing.Union[typing.Sequence[channels.PermissionOverwrite], unset.Unset] = unset.UNSET, - category: typing.Union[channels.GuildCategory, bases.UniqueObjectT, unset.Unset] = unset.UNSET, + category: typing.Union[channels.GuildCategory, bases.UniqueObject, unset.Unset] = unset.UNSET, reason: typing.Union[str, unset.Unset] = unset.UNSET, ) -> channels.GuildChannel: - route = routes.POST_GUILD_CHANNELS.compile(guild=conversions.value_to_snowflake(guild)) - payload = {"type": type_, "name": name} - conversions.put_if_specified(payload, "position", position) - conversions.put_if_specified(payload, "topic", topic) - conversions.put_if_specified(payload, "nsfw", nsfw) - conversions.put_if_specified(payload, "bitrate", bitrate) - conversions.put_if_specified(payload, "user_limit", user_limit) - conversions.put_if_specified(payload, "rate_limit_per_user", rate_limit_per_user) - conversions.put_if_specified(payload, "category", category, conversions.value_to_snowflake) - - if not unset.is_unset(permission_overwrites): - payload["permission_overwrites"] = [ - self._app.entity_factory.serialize_permission_overwrite(o) for o in permission_overwrites - ] - - response = await self._request(route, body=payload, reason=reason) + route = routes.POST_GUILD_CHANNELS.compile(guild=guild) + body = binding.JSONObjectBuilder() + body.put("type", type_) + body.put("name", name) + body.put("position", position) + body.put("topic", topic) + body.put("nsfw", nsfw) + body.put("bitrate", bitrate) + body.put("user_limit", user_limit) + body.put("rate_limit_per_user", rate_limit_per_user) + body.put_snowflake("category_id", category) + body.put_array( + "permission_overwrites", permission_overwrites, self._app.entity_factory.serialize_permission_overwrite + ) + + response = await self._request(route, body=body, reason=reason) channel = self._app.entity_factory.deserialize_channel(response) return typing.cast(channels.GuildChannel, channel) async def reposition_channels( self, - guild: typing.Union[guilds.Guild, bases.UniqueObjectT], - positions: typing.Mapping[int, typing.Union[channels.GuildChannel, bases.UniqueObjectT]], + guild: typing.Union[guilds.Guild, bases.UniqueObject], + positions: typing.Mapping[int, typing.Union[channels.GuildChannel, bases.UniqueObject]], ) -> None: - route = routes.POST_GUILD_CHANNELS.compile(guild=conversions.value_to_snowflake(guild)) - payload = [ - {"id": conversions.value_to_snowflake(channel), "position": pos} for pos, channel in positions.items() - ] - await self._request(route, body=payload) + route = routes.POST_GUILD_CHANNELS.compile(guild=guild) + body = [{"id": str(int(channel)), "position": pos} for pos, channel in positions.items()] + await self._request(route, body=body) async def fetch_member( - self, - guild: typing.Union[guilds.Guild, bases.UniqueObjectT], - user: typing.Union[users.User, bases.UniqueObjectT], + self, guild: typing.Union[guilds.Guild, bases.UniqueObject], user: typing.Union[users.User, bases.UniqueObject], ) -> guilds.GuildMember: - route = routes.GET_GUILD_MEMBER.compile( - guild=conversions.value_to_snowflake(guild), user=conversions.value_to_snowflake(user) - ) + route = routes.GET_GUILD_MEMBER.compile(guild=guild, user=user) response = await self._request(route) return self._app.entity_factory.deserialize_guild_member(response) def fetch_members( - self, guild: typing.Union[guilds.Guild, bases.UniqueObjectT], + self, guild: typing.Union[guilds.Guild, bases.UniqueObject], ) -> iterators.LazyIterator[guilds.GuildMember]: - return iterators.MemberIterator(self._app, self._request, conversions.value_to_snowflake(guild)) + return iterators.MemberIterator(self._app, self._request, guild) async def edit_member( self, - guild: typing.Union[guilds.Guild, bases.UniqueObjectT], - user: typing.Union[users.User, bases.UniqueObjectT], + guild: typing.Union[guilds.Guild, bases.UniqueObject], + user: typing.Union[users.User, bases.UniqueObject], *, nick: typing.Union[unset.Unset, str] = unset.UNSET, roles: typing.Union[ - unset.Unset, typing.Collection[typing.Union[guilds.Role, bases.UniqueObjectT]] + unset.Unset, typing.Collection[typing.Union[guilds.Role, bases.UniqueObject]] ] = unset.UNSET, mute: typing.Union[unset.Unset, bool] = unset.UNSET, deaf: typing.Union[unset.Unset, bool] = unset.UNSET, - voice_channel: typing.Union[unset.Unset, channels.GuildVoiceChannel, bases.UniqueObjectT, None] = unset.UNSET, + voice_channel: typing.Union[unset.Unset, channels.GuildVoiceChannel, bases.UniqueObject, None] = unset.UNSET, reason: typing.Union[str, unset.Unset] = unset.UNSET, ) -> None: - route = routes.PATCH_GUILD_MEMBER.compile( - guild=conversions.value_to_snowflake(guild), user=conversions.value_to_snowflake(user) - ) - payload = {} - conversions.put_if_specified(payload, "nick", nick) - conversions.put_if_specified(payload, "mute", mute) - conversions.put_if_specified(payload, "deaf", deaf) + route = routes.PATCH_GUILD_MEMBER.compile(guild=guild, user=user) + body = binding.JSONObjectBuilder() + body.put("nick", nick) + body.put("mute", mute) + body.put("deaf", deaf) + body.put_snowflake_array("roles", roles) if voice_channel is None: - payload["channel_id"] = None + body.put("channel_id", None) elif not unset.is_unset(voice_channel): - payload["channel_id"] = conversions.value_to_snowflake(voice_channel) - - if not unset.is_unset(roles): - payload["roles"] = [conversions.value_to_snowflake(r) for r in roles] + body.put_snowflake("channel_id", voice_channel) - await self._request(route, body=payload, reason=reason) + await self._request(route, body=body, reason=reason) async def edit_my_nick( self, - guild: typing.Union[guilds.Guild, bases.UniqueObjectT], + guild: typing.Union[guilds.Guild, bases.UniqueObject], nick: typing.Optional[str], *, reason: typing.Union[unset.Unset, str] = unset.UNSET, ) -> None: - route = routes.PATCH_MY_GUILD_NICKNAME.compile(guild=conversions.value_to_snowflake(guild)) - payload = {"nick": nick} - await self._request(route, body=payload, reason=reason) + route = routes.PATCH_MY_GUILD_NICKNAME.compile(guild=guild) + body = binding.JSONObjectBuilder() + body.put("nick", nick) + await self._request(route, body=body, reason=reason) async def add_role_to_member( self, - guild: typing.Union[guilds.Guild, bases.UniqueObjectT], - user: typing.Union[users.User, bases.UniqueObjectT], - role: typing.Union[guilds.Role, bases.UniqueObjectT], + guild: typing.Union[guilds.Guild, bases.UniqueObject], + user: typing.Union[users.User, bases.UniqueObject], + role: typing.Union[guilds.Role, bases.UniqueObject], *, reason: typing.Union[unset.Unset, str] = unset.UNSET, ) -> None: - route = routes.PUT_GUILD_MEMBER_ROLE.compile( - guild=conversions.value_to_snowflake(guild), - user=conversions.value_to_snowflake(user), - role=conversions.value_to_snowflake(role), - ) + route = routes.PUT_GUILD_MEMBER_ROLE.compile(guild=guild, user=user, role=role) await self._request(route, reason=reason) async def remove_role_from_member( self, - guild: typing.Union[guilds.Guild, bases.UniqueObjectT], - user: typing.Union[users.User, bases.UniqueObjectT], - role: typing.Union[guilds.Role, bases.UniqueObjectT], + guild: typing.Union[guilds.Guild, bases.UniqueObject], + user: typing.Union[users.User, bases.UniqueObject], + role: typing.Union[guilds.Role, bases.UniqueObject], *, reason: typing.Union[unset.Unset, str] = unset.UNSET, ) -> None: - route = routes.DELETE_GUILD_MEMBER_ROLE.compile( - guild=conversions.value_to_snowflake(guild), - user=conversions.value_to_snowflake(user), - role=conversions.value_to_snowflake(role), - ) + route = routes.DELETE_GUILD_MEMBER_ROLE.compile(guild=guild, user=user, role=role) await self._request(route, reason=reason) async def kick_member( self, - guild: typing.Union[guilds.Guild, bases.UniqueObjectT], - user: typing.Union[users.User, bases.UniqueObjectT], + guild: typing.Union[guilds.Guild, bases.UniqueObject], + user: typing.Union[users.User, bases.UniqueObject], *, reason: typing.Union[unset.Unset, str] = unset.UNSET, ) -> None: - route = routes.DELETE_GUILD_MEMBER.compile( - guild=conversions.value_to_snowflake(guild), user=conversions.value_to_snowflake(user), - ) + route = routes.DELETE_GUILD_MEMBER.compile(guild=guild, user=user,) await self._request(route, reason=reason) async def ban_user( self, - guild: typing.Union[guilds.Guild, bases.UniqueObjectT], - user: typing.Union[users.User, bases.UniqueObjectT], + guild: typing.Union[guilds.Guild, bases.UniqueObject], + user: typing.Union[users.User, bases.UniqueObject], *, reason: typing.Union[unset.Unset, str] = unset.UNSET, ) -> None: - route = routes.PUT_GUILD_BAN.compile( - guild=conversions.value_to_snowflake(guild), user=conversions.value_to_snowflake(user), - ) + route = routes.PUT_GUILD_BAN.compile(guild=guild, user=user) await self._request(route, reason=reason) async def unban_user( self, - guild: typing.Union[guilds.Guild, bases.UniqueObjectT], - user: typing.Union[users.User, bases.UniqueObjectT], + guild: typing.Union[guilds.Guild, bases.UniqueObject], + user: typing.Union[users.User, bases.UniqueObject], *, reason: typing.Union[unset.Unset, str] = unset.UNSET, ) -> None: - route = routes.DELETE_GUILD_BAN.compile( - guild=conversions.value_to_snowflake(guild), user=conversions.value_to_snowflake(user), - ) + route = routes.DELETE_GUILD_BAN.compile(guild=guild, user=user) await self._request(route, reason=reason) async def fetch_ban( - self, - guild: typing.Union[guilds.Guild, bases.UniqueObjectT], - user: typing.Union[users.User, bases.UniqueObjectT], + self, guild: typing.Union[guilds.Guild, bases.UniqueObject], user: typing.Union[users.User, bases.UniqueObject], ) -> guilds.GuildMemberBan: - route = routes.GET_GUILD_BAN.compile( - guild=conversions.value_to_snowflake(guild), user=conversions.value_to_snowflake(user), - ) + route = routes.GET_GUILD_BAN.compile(guild=guild, user=user) response = await self._request(route) return self._app.entity_factory.deserialize_guild_member_ban(response) async def fetch_bans( - self, guild: typing.Union[guilds.Guild, bases.UniqueObjectT], / + self, guild: typing.Union[guilds.Guild, bases.UniqueObject], / ) -> typing.Sequence[guilds.GuildMemberBan]: - route = routes.GET_GUILD_BANS.compile(guild=conversions.value_to_snowflake(guild)) + route = routes.GET_GUILD_BANS.compile(guild=guild) response = await self._request(route) - return [self._app.entity_factory.deserialize_guild_member_ban(b) for b in response] + return binding.cast_json_array(response, self._app.entity_factory.deserialize_guild_member_ban) async def fetch_roles( - self, guild: typing.Union[guilds.Guild, bases.UniqueObjectT], / + self, guild: typing.Union[guilds.Guild, bases.UniqueObject], / ) -> typing.Sequence[guilds.Role]: - route = routes.GET_GUILD_ROLES.compile(guild=conversions.value_to_snowflake(guild)) + route = routes.GET_GUILD_ROLES.compile(guild=guild) response = await self._request(route) - return [self._app.entity_factory.deserialize_role(r) for r in response] + return binding.cast_json_array(response, self._app.entity_factory.deserialize_role) async def create_role( self, - guild: typing.Union[guilds.Guild, bases.UniqueObjectT], + guild: typing.Union[guilds.Guild, bases.UniqueObject], /, *, name: typing.Union[unset.Unset, str] = unset.UNSET, @@ -1995,31 +1929,31 @@ async def create_role( if not unset.count_unset_objects(color, colour): raise TypeError("Can not specify 'color' and 'colour' together.") - route = routes.POST_GUILD_ROLES.compile(guild=conversions.value_to_snowflake(guild)) - payload = {} - conversions.put_if_specified(payload, "name", name) - conversions.put_if_specified(payload, "permissions", permissions) - conversions.put_if_specified(payload, "color", color) - conversions.put_if_specified(payload, "color", colour) - conversions.put_if_specified(payload, "hoist", hoist) - conversions.put_if_specified(payload, "mentionable", mentionable) + route = routes.POST_GUILD_ROLES.compile(guild=guild) + body = binding.JSONObjectBuilder() + body.put("name", name) + body.put("permissions", permissions) + body.put("color", color) + body.put("color", colour) + body.put("hoist", hoist) + body.put("mentionable", mentionable) - response = await self._request(route, body=payload, reason=reason) + response = await self._request(route, body=body, reason=reason) return self._app.entity_factory.deserialize_role(response) async def reposition_roles( self, - guild: typing.Union[guilds.Guild, bases.UniqueObjectT], - positions: typing.Mapping[int, typing.Union[guilds.Role, bases.UniqueObjectT]], + guild: typing.Union[guilds.Guild, bases.UniqueObject], + positions: typing.Mapping[int, typing.Union[guilds.Role, bases.UniqueObject]], ) -> None: - route = routes.POST_GUILD_ROLES.compile(guild=conversions.value_to_snowflake(guild)) - payload = [{"id": conversions.value_to_snowflake(role), "position": pos} for pos, role in positions.items()] - await self._request(route, body=payload) + route = routes.POST_GUILD_ROLES.compile(guild=guild) + body = [{"id": str(int(role)), "position": pos} for pos, role in positions.items()] + await self._request(route, body=body) async def edit_role( self, - guild: typing.Union[guilds.Guild, bases.UniqueObjectT], - role: typing.Union[guilds.Role, bases.UniqueObjectT], + guild: typing.Union[guilds.Guild, bases.UniqueObject], + role: typing.Union[guilds.Role, bases.UniqueObject], *, name: typing.Union[unset.Unset, str] = unset.UNSET, permissions: typing.Union[unset.Unset, permissions_.Permission] = unset.UNSET, @@ -2032,141 +1966,134 @@ async def edit_role( if not unset.count_unset_objects(color, colour): raise TypeError("Can not specify 'color' and 'colour' together.") - route = routes.PATCH_GUILD_ROLE.compile( - guild=conversions.value_to_snowflake(guild), role=conversions.value_to_snowflake(role), - ) + route = routes.PATCH_GUILD_ROLE.compile(guild=guild, role=role) - payload = {} - conversions.put_if_specified(payload, "name", name) - conversions.put_if_specified(payload, "permissions", permissions) - conversions.put_if_specified(payload, "color", color) - conversions.put_if_specified(payload, "color", colour) - conversions.put_if_specified(payload, "hoist", hoist) - conversions.put_if_specified(payload, "mentionable", mentionable) + body = binding.JSONObjectBuilder() + body.put("name", name) + body.put("permissions", permissions) + body.put("color", color) + body.put("color", colour) + body.put("hoist", hoist) + body.put("mentionable", mentionable) - response = await self._request(route, body=payload, reason=reason) + response = await self._request(route, body=body, reason=reason) return self._app.entity_factory.deserialize_role(response) async def delete_role( self, - guild: typing.Union[guilds.Guild, bases.UniqueObjectT], - role: typing.Union[guilds.Role, bases.UniqueObjectT], + guild: typing.Union[guilds.Guild, bases.UniqueObject], + role: typing.Union[guilds.Role, bases.UniqueObject], ) -> None: - route = routes.DELETE_GUILD_ROLE.compile( - guild=conversions.value_to_snowflake(guild), role=conversions.value_to_snowflake(role), - ) + route = routes.DELETE_GUILD_ROLE.compile(guild=guild, role=role) await self._request(route) async def estimate_guild_prune_count( - self, guild: typing.Union[guilds.Guild, bases.UniqueObjectT], days: int, + self, guild: typing.Union[guilds.Guild, bases.UniqueObject], days: int, ) -> int: - route = routes.GET_GUILD_PRUNE.compile(guild=conversions.value_to_snowflake(guild)) - payload = {"days": str(days)} - response = await self._request(route, query=payload) + route = routes.GET_GUILD_PRUNE.compile(guild=guild) + query = binding.StringMapBuilder() + query.put("days", days) + response = await self._request(route, query=query) return int(response["pruned"]) async def begin_guild_prune( self, - guild: typing.Union[guilds.Guild, bases.UniqueObjectT], + guild: typing.Union[guilds.Guild, bases.UniqueObject], days: int, *, reason: typing.Union[unset.Unset, str], ) -> int: - route = routes.POST_GUILD_PRUNE.compile(guild=conversions.value_to_snowflake(guild)) - payload = {"compute_prune_count": "true", "days": str(days)} - response = await self._request(route, query=payload, reason=reason) + route = routes.POST_GUILD_PRUNE.compile(guild=guild) + query = binding.StringMapBuilder() + query.put("compute_prune_count", True) + query.put("days", days) + response = await self._request(route, query=query, reason=reason) return int(response["pruned"]) async def fetch_guild_voice_regions( - self, guild: typing.Union[guilds.Guild, bases.UniqueObjectT], / + self, guild: typing.Union[guilds.Guild, bases.UniqueObject], / ) -> typing.Sequence[voices.VoiceRegion]: - route = routes.GET_GUILD_VOICE_REGIONS.compile(guild=conversions.value_to_snowflake(guild)) + route = routes.GET_GUILD_VOICE_REGIONS.compile(guild=guild) response = await self._request(route) - return [self._app.entity_factory.deserialize_voice_region(r) for r in response] + return binding.cast_json_array(response, self._app.entity_factory.deserialize_voice_region) async def fetch_guild_invites( - self, guild: typing.Union[guilds.Guild, bases.UniqueObjectT], / + self, guild: typing.Union[guilds.Guild, bases.UniqueObject], / ) -> typing.Sequence[invites.InviteWithMetadata]: - route = routes.GET_GUILD_INVITES.compile(guild=conversions.value_to_snowflake(guild)) + route = routes.GET_GUILD_INVITES.compile(guild=guild) response = await self._request(route) - return [self._app.entity_factory.deserialize_invite_with_metadata(r) for r in response] + return binding.cast_json_array(response, self._app.entity_factory.deserialize_invite_with_metadata) async def fetch_integrations( - self, guild: typing.Union[guilds.Guild, bases.UniqueObjectT], / + self, guild: typing.Union[guilds.Guild, bases.UniqueObject], / ) -> typing.Sequence[guilds.GuildIntegration]: - route = routes.GET_GUILD_INTEGRATIONS.compile(guild=conversions.value_to_snowflake(guild)) + route = routes.GET_GUILD_INTEGRATIONS.compile(guild=guild) response = await self._request(route) - return [self._app.entity_factory.deserialize_guild_integration(i) for i in response] + return binding.cast_json_array(response, self._app.entity_factory.deserialize_guild_integration) async def edit_integration( self, - guild: typing.Union[guilds.Guild, bases.UniqueObjectT], - integration: typing.Union[guilds.GuildIntegration, bases.UniqueObjectT], + guild: typing.Union[guilds.Guild, bases.UniqueObject], + integration: typing.Union[guilds.GuildIntegration, bases.UniqueObject], *, expire_behaviour: typing.Union[unset.Unset, guilds.IntegrationExpireBehaviour] = unset.UNSET, - expire_grace_period: typing.Union[unset.Unset, more_typing.TimeSpanT] = unset.UNSET, + expire_grace_period: typing.Union[unset.Unset, date.TimeSpan] = unset.UNSET, enable_emojis: typing.Union[unset.Unset, bool] = unset.UNSET, reason: typing.Union[unset.Unset, str] = unset.UNSET, ) -> None: - route = routes.PATCH_GUILD_INTEGRATION.compile( - guild=conversions.value_to_snowflake(guild), integration=conversions.value_to_snowflake(integration), - ) - payload = {} - conversions.put_if_specified(payload, "expire_behaviour", expire_behaviour) - conversions.put_if_specified(payload, "expire_grace_period", expire_grace_period, conversions.timespan_to_int) + route = routes.PATCH_GUILD_INTEGRATION.compile(guild=guild, integration=integration) + body = binding.JSONObjectBuilder() + body.put("expire_behaviour", expire_behaviour) + body.put("expire_grace_period", expire_grace_period, date.timespan_to_int) # Inconsistent naming in the API itself, so I have changed the name. - conversions.put_if_specified(payload, "enable_emoticons", enable_emojis) - await self._request(route, body=payload, reason=reason) + body.put("enable_emoticons", enable_emojis) + await self._request(route, body=body, reason=reason) async def delete_integration( self, - guild: typing.Union[guilds.Guild, bases.UniqueObjectT], - integration: typing.Union[guilds.GuildIntegration, bases.UniqueObjectT], + guild: typing.Union[guilds.Guild, bases.UniqueObject], + integration: typing.Union[guilds.GuildIntegration, bases.UniqueObject], *, reason: typing.Union[unset.Unset, str] = unset.UNSET, ) -> None: - route = routes.DELETE_GUILD_INTEGRATION.compile( - guild=conversions.value_to_snowflake(guild), integration=conversions.value_to_snowflake(integration), - ) + route = routes.DELETE_GUILD_INTEGRATION.compile(guild=guild, integration=integration) await self._request(route, reason=reason) async def sync_integration( self, - guild: typing.Union[guilds.Guild, bases.UniqueObjectT], - integration: typing.Union[guilds.GuildIntegration, bases.UniqueObjectT], + guild: typing.Union[guilds.Guild, bases.UniqueObject], + integration: typing.Union[guilds.GuildIntegration, bases.UniqueObject], ) -> None: - route = routes.POST_GUILD_INTEGRATION_SYNC.compile( - guild=conversions.value_to_snowflake(guild), integration=conversions.value_to_snowflake(integration), - ) + route = routes.POST_GUILD_INTEGRATION_SYNC.compile(guild=guild, integration=integration) await self._request(route) - async def fetch_widget(self, guild: typing.Union[guilds.Guild, bases.UniqueObjectT], /) -> guilds.GuildWidget: - route = routes.GET_GUILD_WIDGET.compile(guild=conversions.value_to_snowflake(guild)) + async def fetch_widget(self, guild: typing.Union[guilds.Guild, bases.UniqueObject], /) -> guilds.GuildWidget: + route = routes.GET_GUILD_WIDGET.compile(guild=guild) response = await self._request(route) return self._app.entity_factory.deserialize_guild_widget(response) async def edit_widget( self, - guild: typing.Union[guilds.Guild, bases.UniqueObjectT], + guild: typing.Union[guilds.Guild, bases.UniqueObject], /, *, - channel: typing.Union[unset.Unset, channels.GuildChannel, bases.UniqueObjectT, None] = unset.UNSET, + channel: typing.Union[unset.Unset, channels.GuildChannel, bases.UniqueObject, None] = unset.UNSET, enabled: typing.Union[unset.Unset, bool] = unset.UNSET, reason: typing.Union[unset.Unset, str] = unset.UNSET, ) -> guilds.GuildWidget: - route = routes.PATCH_GUILD_WIDGET.compile(guild=conversions.value_to_snowflake(guild)) + route = routes.PATCH_GUILD_WIDGET.compile(guild=guild) - payload = {} - conversions.put_if_specified(payload, "enabled", enabled) + body = binding.JSONObjectBuilder() + body.put("enabled", enabled) if channel is None: - payload["channel"] = None + body.put("channel", None) elif not unset.is_unset(channel): - payload["channel"] = conversions.value_to_snowflake(channel) + body.put_snowflake("channel", channel) - response = await self._request(route, body=payload, reason=reason) + response = await self._request(route, body=body, reason=reason) return self._app.entity_factory.deserialize_guild_widget(response) - async def fetch_vanity_url(self, guild: typing.Union[guilds.Guild, bases.UniqueObjectT], /) -> invites.VanityURL: - route = routes.GET_GUILD_VANITY_URL.compile(guild=conversions.value_to_snowflake(guild)) + async def fetch_vanity_url(self, guild: typing.Union[guilds.Guild, bases.UniqueObject], /) -> invites.VanityURL: + route = routes.GET_GUILD_VANITY_URL.compile(guild=guild) response = await self._request(route) return self._app.entity_factory.deserialize_vanity_url(response) diff --git a/hikari/net/rest_utils.py b/hikari/net/rest_utils.py index e009c39cbc..276ad0a385 100644 --- a/hikari/net/rest_utils.py +++ b/hikari/net/rest_utils.py @@ -34,17 +34,18 @@ import attr from hikari import app as app_ -from hikari.internal import conversions -from hikari.internal import unset from hikari.models import bases from hikari.models import colors from hikari.models import files from hikari.models import guilds from hikari.models import permissions as permissions_ from hikari.net import routes +from hikari.utilities import binding +from hikari.utilities import date +from hikari.utilities import snowflake as snowflake_ +from hikari.utilities import unset if typing.TYPE_CHECKING: - from hikari.internal import more_typing from hikari.models import channels @@ -60,10 +61,10 @@ class TypingIndicator: def __init__( self, - channel: typing.Union[channels.TextChannel, bases.UniqueObjectT], - request_call: typing.Callable[..., more_typing.Coroutine[more_typing.JSONObject]], + channel: typing.Union[channels.TextChannel, bases.UniqueObject], + request_call: typing.Callable[..., typing.Coroutine[None, typing.Any, binding.JSONObject]], ) -> None: - self._channel = conversions.value_to_snowflake(channel) + self._channel = channel self._request_call = request_call self._task = None @@ -89,11 +90,11 @@ async def _keep_typing(self) -> None: @attr.s(auto_attribs=True, kw_only=True, slots=True) class GuildBuilder: _app: app_.IRESTApp - _channels: typing.MutableSequence[more_typing.JSONObject] = attr.ib(factory=list) + _channels: typing.MutableSequence[binding.JSONObject] = attr.ib(factory=list) _counter: int = 0 _name: typing.Union[unset.Unset, str] - _request_call: typing.Callable[..., more_typing.Coroutine[more_typing.JSONObject]] - _roles: typing.MutableSequence[more_typing.JSONObject] = attr.ib(factory=list) + _request_call: typing.Callable[..., typing.Coroutine[None, typing.Any, binding.JSONObject]] + _roles: typing.MutableSequence[binding.JSONObject] = attr.ib(factory=list) default_message_notifications: typing.Union[unset.Unset, guilds.GuildMessageNotificationsLevel] = unset.UNSET explicit_content_filter_level: typing.Union[unset.Unset, guilds.GuildExplicitContentFilterLevel] = unset.UNSET icon: typing.Union[unset.Unset, files.BaseStream] = unset.UNSET @@ -110,16 +111,17 @@ def __await__(self) -> typing.Generator[guilds.Guild, None, typing.Any]: async def create(self) -> guilds.Guild: route = routes.POST_GUILDS.compile() - payload = { - "name": self.name, - "icon": None if unset.is_unset(self.icon) else await self.icon.read(), - "roles": self._roles, - "channels": self._channels, - } - conversions.put_if_specified(payload, "region", self.region) - conversions.put_if_specified(payload, "verification_level", self.verification_level) - conversions.put_if_specified(payload, "default_message_notifications", self.default_message_notifications) - conversions.put_if_specified(payload, "explicit_content_filter", self.explicit_content_filter_level) + payload = binding.JSONObjectBuilder() + payload.put("name", self.name) + payload.put_array("roles", self._roles) + payload.put_array("channels", self._channels) + payload.put("region", self.region) + payload.put("verification_level", self.verification_level) + payload.put("default_message_notifications", self.default_message_notifications) + payload.put("explicit_content_filter", self.explicit_content_filter_level) + + if not unset.is_unset(self.icon): + payload.put("icon", await self.icon.fetch_data_uri()) response = await self._request_call(route, body=payload) return self._app.entity_factory.deserialize_guild(response) @@ -135,7 +137,7 @@ def add_role( mentionable: typing.Union[unset.Unset, bool] = unset.UNSET, permissions: typing.Union[unset.Unset, permissions_.Permission] = unset.UNSET, position: typing.Union[unset.Unset, int] = unset.UNSET, - ) -> bases.Snowflake: + ) -> snowflake_.Snowflake: if len(self._roles) == 0 and name != "@everyone": raise ValueError("First role must always be the @everyone role") @@ -143,13 +145,15 @@ def add_role( raise TypeError("Cannot specify 'color' and 'colour' together.") snowflake = self._new_snowflake() - payload = {"id": str(snowflake), "name": name} - conversions.put_if_specified(payload, "color", color) - conversions.put_if_specified(payload, "color", colour) - conversions.put_if_specified(payload, "hoisted", hoisted) - conversions.put_if_specified(payload, "mentionable", mentionable) - conversions.put_if_specified(payload, "permissions", permissions) - conversions.put_if_specified(payload, "position", position) + payload = binding.JSONObjectBuilder() + payload.put_snowflake("id", snowflake) + payload.put("name", name) + payload.put("color", color) + payload.put("color", colour) + payload.put("hoisted", hoisted) + payload.put("mentionable", mentionable) + payload.put("permissions", permissions) + payload.put("position", position) self._roles.append(payload) return snowflake @@ -161,15 +165,18 @@ def add_category( position: typing.Union[unset.Unset, int] = unset.UNSET, permission_overwrites: typing.Union[unset.Unset, typing.Collection[channels.PermissionOverwrite]] = unset.UNSET, nsfw: typing.Union[unset.Unset, bool] = unset.UNSET, - ) -> bases.Snowflake: + ) -> snowflake_.Snowflake: snowflake = self._new_snowflake() - payload = {"id": str(snowflake), "type": channels.ChannelType.GUILD_CATEGORY, "name": name} - conversions.put_if_specified(payload, "position", position) - conversions.put_if_specified(payload, "nsfw", nsfw) + payload = binding.JSONObjectBuilder() + payload.put_snowflake("id", snowflake) + payload.put("name", name) + payload.put("type", channels.ChannelType.GUILD_CATEGORY) + payload.put("position", position) + payload.put("nsfw", nsfw) - if not unset.is_unset(permission_overwrites): - overwrites = [self._app.entity_factory.serialize_permission_overwrite(o) for o in permission_overwrites] - payload["permission_overwrites"] = overwrites + payload.put_array( + "permission_overwrites", permission_overwrites, self._app.entity_factory.serialize_permission_overwrite + ) self._channels.append(payload) return snowflake @@ -179,24 +186,27 @@ def add_text_channel( name: str, /, *, - parent_id: bases.Snowflake = unset.UNSET, + parent_id: snowflake_.Snowflake = unset.UNSET, topic: typing.Union[unset.Unset, str] = unset.UNSET, - rate_limit_per_user: typing.Union[unset.Unset, more_typing.TimeSpanT] = unset.UNSET, + rate_limit_per_user: typing.Union[unset.Unset, date.TimeSpan] = unset.UNSET, position: typing.Union[unset.Unset, int] = unset.UNSET, permission_overwrites: typing.Union[unset.Unset, typing.Collection[channels.PermissionOverwrite]] = unset.UNSET, nsfw: typing.Union[unset.Unset, bool] = unset.UNSET, - ) -> bases.Snowflake: + ) -> snowflake_.Snowflake: snowflake = self._new_snowflake() - payload = {"id": str(snowflake), "type": channels.ChannelType.GUILD_TEXT, "name": name} - conversions.put_if_specified(payload, "topic", topic) - conversions.put_if_specified(payload, "rate_limit_per_user", rate_limit_per_user, conversions.timespan_to_int) - conversions.put_if_specified(payload, "position", position) - conversions.put_if_specified(payload, "nsfw", nsfw) - conversions.put_if_specified(payload, "parent_id", parent_id, str) - - if not unset.is_unset(permission_overwrites): - overwrites = [self._app.entity_factory.serialize_permission_overwrite(o) for o in permission_overwrites] - payload["permission_overwrites"] = overwrites + payload = binding.JSONObjectBuilder() + payload.put_snowflake("id", snowflake) + payload.put("name", name) + payload.put("type", channels.ChannelType.GUILD_TEXT) + payload.put("topic", topic) + payload.put("rate_limit_per_user", rate_limit_per_user, date.timespan_to_int) + payload.put("position", position) + payload.put("nsfw", nsfw) + payload.put_snowflake("parent_id", parent_id) + + payload.put_array( + "permission_overwrites", permission_overwrites, self._app.entity_factory.serialize_permission_overwrite + ) self._channels.append(payload) return snowflake @@ -206,29 +216,32 @@ def add_voice_channel( name: str, /, *, - parent_id: bases.Snowflake = unset.UNSET, + parent_id: snowflake_.Snowflake = unset.UNSET, bitrate: typing.Union[unset.Unset, int] = unset.UNSET, position: typing.Union[unset.Unset, int] = unset.UNSET, permission_overwrites: typing.Union[unset.Unset, typing.Collection[channels.PermissionOverwrite]] = unset.UNSET, nsfw: typing.Union[unset.Unset, bool] = unset.UNSET, user_limit: typing.Union[unset.Unset, int] = unset.UNSET, - ) -> bases.Snowflake: + ) -> snowflake_.Snowflake: snowflake = self._new_snowflake() - payload = {"id": str(snowflake), "type": channels.ChannelType.GUILD_VOICE, "name": name} - conversions.put_if_specified(payload, "bitrate", bitrate) - conversions.put_if_specified(payload, "position", position) - conversions.put_if_specified(payload, "nsfw", nsfw) - conversions.put_if_specified(payload, "user_limit", user_limit) - conversions.put_if_specified(payload, "parent_id", parent_id, str) - - if not unset.is_unset(permission_overwrites): - overwrites = [self._app.entity_factory.serialize_permission_overwrite(o) for o in permission_overwrites] - payload["permission_overwrites"] = overwrites + payload = binding.JSONObjectBuilder() + payload.put_snowflake("id", snowflake) + payload.put("name", name) + payload.put("type", channels.ChannelType.GUILD_VOICE) + payload.put("bitrate", bitrate) + payload.put("position", position) + payload.put("nsfw", nsfw) + payload.put("user_limit", user_limit) + payload.put_snowflake("parent_id", parent_id) + + payload.put_array( + "permission_overwrites", permission_overwrites, self._app.entity_factory.serialize_permission_overwrite + ) self._channels.append(payload) return snowflake - def _new_snowflake(self) -> bases.Snowflake: + def _new_snowflake(self) -> snowflake_.Snowflake: value = self._counter self._counter += 1 - return bases.Snowflake.from_data(datetime.datetime.now(tz=datetime.timezone.utc), 0, 0, value,) + return snowflake_.Snowflake.from_data(datetime.datetime.now(tz=datetime.timezone.utc), 0, 0, value,) diff --git a/hikari/net/routes.py b/hikari/net/routes.py index c1bcb1e001..be76588fa4 100644 --- a/hikari/net/routes.py +++ b/hikari/net/routes.py @@ -25,6 +25,8 @@ import re import typing +from hikari.utilities import binding + DEFAULT_MAJOR_PARAMS: typing.Final[typing.Set[str]] = {"channel", "guild", "webhook"} HASH_SEPARATOR: typing.Final[str] = ";" @@ -186,10 +188,10 @@ def compile(self, **kwargs: typing.Any) -> CompiledRoute: CompiledRoute The compiled _route. """ + data = binding.StringMapBuilder.from_dict(kwargs) + return CompiledRoute( - self, - self.path_template.format_map(kwargs), - str(kwargs[self.major_param]) if self.major_param is not None else "-", + self, self.path_template.format_map(data), data[self.major_param] if self.major_param is not None else "-", ) def __repr__(self) -> str: diff --git a/hikari/net/user_agents.py b/hikari/net/user_agents.py index 41896f4374..21d331212d 100644 --- a/hikari/net/user_agents.py +++ b/hikari/net/user_agents.py @@ -31,10 +31,10 @@ import typing -from hikari.internal import class_helpers +from hikari.utilities import klass -class UserAgent(metaclass=class_helpers.SingletonMeta): +class UserAgent(metaclass=klass.SingletonMeta): """Platform version info. !!! note diff --git a/hikari/net/voice_gateway.py b/hikari/net/voice_gateway.py index ab5f2ad328..5c93484c61 100644 --- a/hikari/net/voice_gateway.py +++ b/hikari/net/voice_gateway.py @@ -22,7 +22,7 @@ __all__ = ["VoiceGateway"] import asyncio -import json +import enum import math import time import typing @@ -32,13 +32,11 @@ import attr from hikari import errors -from hikari.internal import class_helpers -from hikari.internal import conversions -from hikari.internal import more_enums -from hikari.internal import more_typing -from hikari.internal import ratelimits from hikari.models import bases from hikari.net import http_client +from hikari.net import ratelimits +from hikari.utilities import binding +from hikari.utilities import klass if typing.TYPE_CHECKING: from hikari import bot @@ -48,8 +46,8 @@ class VoiceGateway(http_client.HTTPClient): """Implementation of the V4 Voice Gateway.""" - @more_enums.must_be_unique - class _GatewayCloseCode(int, more_enums.Enum): + @enum.unique + class _GatewayCloseCode(enum.IntEnum): """Reasons for closing a gateway connection.""" RFC_6455_NORMAL_CLOSURE = 1000 @@ -73,8 +71,8 @@ class _GatewayCloseCode(int, more_enums.Enum): VOICE_SERVER_CRASHED = 4015 UNKNOWN_ENCRYPTION_MODE = 4016 - @more_enums.must_be_unique - class _GatewayOpcode(int, more_enums.Enum): + @enum.unique + class _GatewayOpcode(enum.IntEnum): IDENTIFY = 0 SELECT_PROTOCOL = 1 READY = 2 @@ -105,8 +103,8 @@ def __init__( debug: bool = False, endpoint: str, session_id: str, - user_id: bases.UniqueObjectT, - server_id: bases.UniqueObjectT, + user_id: bases.UniqueObject, + server_id: bases.UniqueObject, token: str, ) -> None: super().__init__( @@ -114,7 +112,7 @@ def __init__( connector=config.tcp_connector_factory() if config.tcp_connector_factory else None, debug=debug, # Use the server ID to identify each websocket based on a server. - logger=class_helpers.get_logger(self, conversions.value_to_snowflake(server_id)), + logger=klass.get_logger(self, str(int(server_id))), proxy_auth=config.proxy_auth, proxy_headers=config.proxy_headers, proxy_url=config.proxy_url, @@ -135,10 +133,10 @@ def __init__( self._nonce = None self._request_close_event = asyncio.Event() self._resumable = False - self._server_id = conversions.value_to_snowflake(server_id) + self._server_id = str(int(server_id)) self._session_id = session_id self._token = token - self._user_id = conversions.value_to_snowflake(user_id) + self._user_id = str(int(user_id)) self._voice_ip = None self._voice_modes = [] self._voice_port = None @@ -345,13 +343,13 @@ async def _handshake(self): } ) - async def _receive_json_payload(self) -> more_typing.JSONObject: + async def _receive_json_payload(self) -> binding.JSONObject: message = await self._ws.receive() self.last_message_received = self._now() if message.type == aiohttp.WSMsgType.TEXT: self._log_debug_payload(message.data, "received text payload") - return json.loads(message.data) + return binding.load_json(message.data) elif message.type == aiohttp.WSMsgType.CLOSE: close_code = self._ws.close_code @@ -379,8 +377,8 @@ async def _receive_json_payload(self) -> more_typing.JSONObject: self.logger.debug("encountered unexpected error", exc_info=ex) raise errors.GatewayError("Unexpected websocket exception from gateway") from ex - async def _send_json(self, payload: more_typing.JSONObject) -> None: - message = json.dumps(payload) + async def _send_json(self, payload: binding.JSONObject) -> None: + message = binding.dump_json(payload) self._log_debug_payload(message, "sending json payload") await self._ws.send_str(message) diff --git a/hikari/internal/__init__.py b/hikari/utilities/__init__.py similarity index 100% rename from hikari/internal/__init__.py rename to hikari/utilities/__init__.py diff --git a/hikari/utilities/aio.py b/hikari/utilities/aio.py new file mode 100644 index 0000000000..ac08afd8b2 --- /dev/null +++ b/hikari/utilities/aio.py @@ -0,0 +1,177 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# Copyright © Nekokatt 2019-2020 +# +# This file is part of Hikari. +# +# Hikari is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Hikari is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Hikari. If not, see . +"""Asyncio extensions and utilities.""" + +from __future__ import annotations + +__all__ = ["completed_future", "is_async_iterator", "is_async_iterable"] + +import asyncio +import inspect +import typing + +if typing.TYPE_CHECKING: + import contextvars + import types + + _T_contra = typing.TypeVar("_T_contra", contravariant=True) + + +def completed_future(result: _T_contra = None, /) -> Future[_T_contra]: + """Create a future on the current running loop that is completed, then return it. + + Parameters + ---------- + result : typing.Any + The value to set for the result of the future. + + Returns + ------- + asyncio.Future + The completed future. + """ + future = asyncio.get_event_loop().create_future() + future.set_result(result) + return future + + +# On Python3.8.2, there appears to be a bug with the typing module: + +# >>> class Aiterable: +# ... async def __aiter__(self): +# ... yield ... +# >>> isinstance(Aiterable(), typing.AsyncIterable) +# True + +# >>> class Aiterator: +# ... async def __anext__(self): +# ... return ... +# >>> isinstance(Aiterator(), typing.AsyncIterator) +# False + +# ... so I guess I will have to determine this some other way. + + +def is_async_iterator(obj: typing.Any) -> bool: + """Determine if the object is an async iterator or not.""" + return asyncio.iscoroutinefunction(getattr(obj, "__anext__", None)) + + +def is_async_iterable(obj: typing.Any) -> bool: + """Determine if the object is an async iterable or not.""" + attr = getattr(obj, "__aiter__", None) + return inspect.isfunction(attr) or inspect.ismethod(attr) + + +@typing.runtime_checkable +class Future(typing.Protocol[_T_contra]): + """Typed protocol representation of an `asyncio.Future`. + + You should consult the documentation for `asyncio.Future` for usage. + """ + + def result(self) -> _T_contra: + """See `asyncio.Future.result`.""" + + def set_result(self, result: _T_contra, /) -> None: + """See `asyncio.Future.set_result`.""" + + def set_exception(self, exception: Exception, /) -> None: + """See `asyncio.Future.set_exception`.""" + + def done(self) -> bool: + """See `asyncio.Future.done`.""" + + def cancelled(self) -> bool: + """See `asyncio.Future.cancelled`.""" + + def add_done_callback( + self, callback: typing.Callable[[Future[_T_contra]], None], /, *, context: typing.Optional[contextvars.Context], + ) -> None: + """See `asyncio.Future.add_done_callback`.""" + + def remove_done_callback(self, callback: typing.Callable[[Future[_T_contra]], None], /) -> None: + """See `asyncio.Future.remove_done_callback`.""" + + def cancel(self) -> bool: + """See `asyncio.Future.cancel`.""" + + def exception(self) -> typing.Optional[Exception]: + """See `asyncio.Future.exception`.""" + + def get_loop(self) -> asyncio.AbstractEventLoop: + """See `asyncio.Future.get_loop`.""" + + def __await__(self) -> typing.Generator[_T_contra, None, typing.Any]: + ... + + +@typing.runtime_checkable +class Task(typing.Protocol[_T_contra]): + """Typed protocol representation of an `asyncio.Task`. + + You should consult the documentation for `asyncio.Task` for usage. + """ + + def result(self) -> _T_contra: + """See`asyncio.Future.result`.""" + + def set_result(self, result: _T_contra, /) -> None: + """See `asyncio.Future.set_result`.""" + + def set_exception(self, exception: Exception, /) -> None: + """See `asyncio.Future.set_exception`.""" + + def done(self) -> bool: + """See `asyncio.Future.done`.""" + + def cancelled(self) -> bool: + """See `asyncio.Future.cancelled`.""" + + def add_done_callback( + self, callback: typing.Callable[[Future[_T_contra]], None], /, *, context: typing.Optional[contextvars.Context], + ) -> None: + """See `asyncio.Future.add_done_callback`.""" + + def remove_done_callback(self, callback: typing.Callable[[Future[_T_contra]], None], /) -> None: + """See `asyncio.Future.remove_done_callback`.""" + + def cancel(self) -> bool: + """See `asyncio.Future.cancel`.""" + + def exception(self) -> typing.Optional[Exception]: + """See `asyncio.Future.exception`.""" + + def get_loop(self) -> asyncio.AbstractEventLoop: + """See `asyncio.Future.get_loop`.""" + + def get_stack(self, *, limit: typing.Optional[int] = None) -> typing.Sequence[types.FrameType]: + """See `asyncio.Task.get_stack`.""" + + def print_stack(self, *, limit: typing.Optional[int] = None, file: typing.Optional[typing.IO] = None) -> None: + """See `asyncio.Task.print_stack`.""" + + def get_name(self) -> str: + """See `asyncio.Task.get_name`.""" + + def set_name(self, value: str, /) -> None: + """See `asyncio.Task.set_name`.""" + + def __await__(self) -> typing.Generator[_T_contra, None, typing.Any]: + ... diff --git a/hikari/utilities/binding.py b/hikari/utilities/binding.py new file mode 100644 index 0000000000..5a0e37c3bd --- /dev/null +++ b/hikari/utilities/binding.py @@ -0,0 +1,260 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# Copyright © Nekoka.tt 2019-2020 +# +# This file is part of Hikari. +# +# Hikari is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Hikari is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Hikari. If not, see . +"""Data binding utilities.""" +from __future__ import annotations + +__all__ = [ + "Headers", + "Query", + "JSONObject", + "JSONArray", + "JSONNull", + "JSONBoolean", + "JSONString", + "JSONNumber", + "JSONAny", + "URLEncodedForm", + "MultipartForm", + "dump_json", + "load_json", + "JSONObjectBuilder", + "cast_json_array", +] + +import json +import typing + +import aiohttp.typedefs + +from hikari.models import bases +from hikari.utilities import unset + +Headers = typing.Mapping[str, str] +"""HTTP headers.""" + +Query = typing.Dict[str, str] +"""HTTP query string.""" + +URLEncodedForm = aiohttp.FormData +"""Content of type application/x-www-form-encoded""" + +MultipartForm = aiohttp.FormData +"""Content of type multipart/form-data""" + +JSONString = str +"""A JSON string.""" + +JSONNumber = typing.Union[int, float] +"""A JSON numeric value.""" + +JSONBoolean = bool +"""A JSON boolean value.""" + +JSONNull = None +"""A null JSON value.""" + +# We cant include JSONArray and JSONObject in the definition as MyPY does not support +# recursive type definitions, sadly. +JSONObject = typing.Dict[JSONString, typing.Union[JSONString, JSONNumber, JSONBoolean, JSONNull, list, dict]] +"""A JSON object representation as a dict.""" + +JSONArray = typing.List[typing.Union[JSONString, JSONNumber, JSONBoolean, JSONNull, dict, list]] +"""A JSON array representation as a list.""" + +JSONAny = typing.Union[JSONString, JSONNumber, JSONBoolean, JSONNull, JSONArray, JSONObject] +"""Any JSON type.""" + +if typing.TYPE_CHECKING: + + def dump_json(_: typing.Union[JSONArray, JSONObject]) -> str: + ... + + def load_json(_: str) -> typing.Union[JSONArray, JSONObject]: + ... + + +else: + dump_json = json.dumps + """Convert a Python type to a JSON string.""" + + load_json = json.loads + """Convert a JSON string to a Python type.""" + + +class StringMapBuilder(typing.Dict[str, str]): + """Helper class used to quickly build query strings or header maps.""" + + __slots__ = () + + def __init__(self): + super().__init__() + + def put( + self, + key: str, + value: typing.Union[unset.Unset, typing.Any], + conversion: typing.Optional[typing.Callable[[typing.Any], typing.Any]] = None, + ) -> None: + """Add a key and value to the string map. + + Parameters + ---------- + key : str + The string key. + value : hikari.utilities.unset.Unset | typing.Any + The value to set. + conversion : typing.Callable[[typing.Any], typing.Any] | None + An optional conversion to perform. + + !!! note + The value will always be cast to a `str` before inserting it. + """ + if not unset.is_unset(value): + if conversion is not None: + value = conversion(value) + + if value is True: + value = "true" + elif value is False: + value = "false" + elif value is None: + value = "null" + elif isinstance(value, bases.Unique): + value = str(value.id) + else: + value = str(value) + + self[key] = value + + @classmethod + def from_dict(cls, d: typing.Union[unset.Unset, typing.Dict[str, typing.Any]]) -> StringMapBuilder: + """Build a query from an existing dict.""" + sb = cls() + + if unset.is_unset(d): + return sb + + for k, v in d.items(): + sb.put(k, v) + + return sb + + +class JSONObjectBuilder(typing.Dict[JSONString, JSONAny]): + """Helper class used to quickly build JSON objects from various values.""" + + __slots__ = () + + def __init__(self): + super().__init__() + + def put( + self, + key: JSONString, + value: typing.Any, + conversion: typing.Optional[typing.Callable[[typing.Any], JSONAny]] = None, + ) -> None: + """Put a JSON value. + + If the value is unset, then it will not be stored. + + Parameters + ---------- + key : JSONString + The key to give the element. + value : JSONType | typing.Any | hikari.utilities.unset.Unset + The JSON type to put. This may be a non-JSON type if a conversion + is also specified. This may alternatively be unset. In the latter + case, nothing is performed. + conversion : typing.Callable[[typing.Any], JSONType] | None + Optional conversion to apply. + """ + if not unset.is_unset(value): + if conversion is not None: + self[key] = conversion(value) + else: + self[key] = value + + def put_array( + self, + key: JSONString, + values: typing.Union[unset.Unset, typing.Iterable[_T]], + conversion: typing.Optional[typing.Callable[[_T], JSONAny]] = None, + ) -> None: + """Put a JSON array. + + If the value is unset, then it will not be stored. + + Parameters + ---------- + key : JSONString + The key to give the element. + values : JSONType | typing.Any | hikari.utilities.unset.Unset + The JSON types to put. This may be an iterable of non-JSON types if + a conversion is also specified. This may alternatively be unset. + In the latter case, nothing is performed. + conversion : typing.Callable[[typing.Any], JSONType] | None + Optional conversion to apply. + """ + if not unset.is_unset(values): + if conversion is not None: + self[key] = [conversion(value) for value in values] + else: + self[key] = list(values) + + def put_snowflake(self, key: JSONString, value: typing.Union[unset.Unset, typing.SupportsInt, int]) -> None: + """Put a snowflake. + + Parameters + ---------- + key : JSONString + The key to give the element. + value : JSONType | hikari.utilities.unset.Unset + The JSON type to put. This may alternatively be unset. In the latter + case, nothing is performed. + """ + if not unset.is_unset(value): + self[key] = str(int(value)) + + def put_snowflake_array( + self, key: JSONString, values: typing.Union[unset.Unset, typing.Iterable[typing.SupportsInt, int]] + ) -> None: + """Put an array of snowflakes. + + Parameters + ---------- + key : JSONString + The key to give the element. + values : typing.Iterable[typing.SupportsInt, int] | hikari.utilities.unset.Unset + The JSON snowflakes to put. This may alternatively be unset. In the latter + case, nothing is performed. + """ + if not unset.is_unset(values): + self[key] = [str(int(value)) for value in values] + + +_T = typing.TypeVar("_T", covariant=True) +_CT = typing.TypeVar("_CT", bound=typing.Collection, contravariant=True) + + +def cast_json_array( + array: JSONArray, cast: typing.Callable[[JSONAny], _T], collection_type: typing.Type[_CT] = list +) -> _CT: + """Cast a JSON array to a given collection type, casting each item.""" + return collection_type(cast(item) for item in array) diff --git a/hikari/internal/conversions.py b/hikari/utilities/date.py similarity index 56% rename from hikari/internal/conversions.py rename to hikari/utilities/date.py index 3d72b3b269..e36cf485ea 100644 --- a/hikari/internal/conversions.py +++ b/hikari/utilities/date.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -# Copyright © Nekokatt 2019-2020 +# Copyright © Nekoka.tt 2019-2020 # # This file is part of Hikari. # @@ -16,90 +16,29 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . -"""Basic transformation utilities.""" from __future__ import annotations __all__ = [ - "try_cast", - "try_cast_or_defer_unary_operator", - "put_if_specified", "rfc7231_datetime_string_to_datetime", - "iso8601_datetime_string_to_datetime", - "discord_epoch_to_datetime", "datetime_to_discord_epoch", + "discord_epoch_to_datetime", "unix_epoch_to_datetime", - "pluralize", - "resolve_signature", - "EMPTY", - "value_to_snowflake", - "json_to_snowflake_map", - "json_to_collection", + "TimeSpan", "timespan_to_int", ] import datetime import email.utils -import inspect import re import typing -from hikari.internal import unset - -if typing.TYPE_CHECKING: - from hikari.internal import more_typing - from hikari.models import bases - - _T = typing.TypeVar("_T") - _T_co = typing.TypeVar("_T_co", covariant=True) - _T_contra = typing.TypeVar("_T_contra", contravariant=True) - _Unique_contra = typing.TypeVar("_Unique_contra", bound=bases.Unique, contravariant=True) - _CollectionImpl_contra = typing.TypeVar("_CollectionImpl_contra", bound=typing.Collection, contravariant=True) - DISCORD_EPOCH: typing.Final[int] = 1_420_070_400 ISO_8601_DATE_PART: typing.Final[typing.Pattern] = re.compile(r"^(\d{4})-(\d{2})-(\d{2})") ISO_8601_TIME_PART: typing.Final[typing.Pattern] = re.compile(r"T(\d{2}):(\d{2}):(\d{2})(?:\.(\d{1,6}))?", re.I) ISO_8601_TZ_PART: typing.Final[typing.Pattern] = re.compile(r"([+-])(\d{2}):(\d{2})$") -#: TODO: remove -def try_cast(value, cast, default, /): - return NotImplemented - - -#: TODO: remove -def try_cast_or_defer_unary_operator(type_, /): - return NotImplemented - - -def put_if_specified( - mapping: typing.Dict[typing.Hashable, typing.Any], - key: typing.Hashable, - value: typing.Any, - cast: typing.Optional[typing.Callable[[_T], _T_co]] = None, - /, -) -> None: - """Add a value to the mapping under the given key as long as the value is not `...`. - - Parameters - ---------- - mapping : typing.Dict[typing.Hashable, typing.Any] - The mapping to add to. - key : typing.Hashable - The key to add the value under. - value : typing.Any - The value to add. - cast : typing.Callable[[`input type`], `output type`] | None - Optional cast to apply to the value when before inserting it into the - mapping. - """ - if value is not unset.UNSET: - if cast: - mapping[key] = cast(value) - else: - mapping[key] = value - - def rfc7231_datetime_string_to_datetime(date_str: str, /) -> datetime.datetime: """Return the HTTP date as a datetime object. @@ -220,83 +159,11 @@ def unix_epoch_to_datetime(epoch: int, /) -> datetime.datetime: return datetime.datetime.min -def pluralize(count: int, name: str, suffix: str = "s") -> str: - """Pluralizes a word.""" - return f"{count} {name + suffix}" if count - 1 else f"{count} {name}" - - -EMPTY: typing.Final[inspect.Parameter.empty] = inspect.Parameter.empty -"""A singleton that empty annotations will be set to in `resolve_signature`.""" - - -def resolve_signature(func: typing.Callable) -> inspect.Signature: - """Get the `inspect.Signature` of `func` with resolved forward annotations. - - Parameters - ---------- - func : typing.Callable[[...], ...] - The function to get the resolved annotations from. - - Returns - ------- - typing.Signature - A `typing.Signature` object with all forward reference annotations - resolved. - """ - signature = inspect.signature(func) - resolved_type_hints = None - parameters = [] - for key, value in signature.parameters.items(): - if isinstance(value.annotation, str): - if resolved_type_hints is None: - resolved_type_hints = typing.get_type_hints(func) - parameters.append(value.replace(annotation=resolved_type_hints[key])) - else: - parameters.append(value) - signature = signature.replace(parameters=parameters) - - if isinstance(signature.return_annotation, str): - if resolved_type_hints is None: - return_annotation = typing.get_type_hints(func)["return"] - else: - return_annotation = resolved_type_hints["return"] - signature = signature.replace(return_annotation=return_annotation) - - return signature - - -def value_to_snowflake(value: typing.Union[typing.SupportsInt, int]) -> str: - """Cast the given object to an int and return the result as a string. - - Parameters - ---------- - value : typing.SupportsInt | int - A value that can be cast to an `int`. - - Returns - ------- - str - The string representation of the integer value. - """ - return str(int(value)) - - -def json_to_snowflake_map( - payload: more_typing.JSONArray, cast: typing.Callable[[more_typing.JSONType], _Unique_contra] -) -> typing.Mapping[bases.Snowflake, _Unique_contra]: - items = (cast(obj) for obj in payload) - return {item.id: item for item in items} - - -def json_to_collection( - payload: more_typing.JSONArray, - cast: typing.Callable[[more_typing.JSONType], _T_contra], - collection_type: typing.Type[_CollectionImpl_contra] = list, -) -> _CollectionImpl_contra[_T_contra]: - return collection_type(cast(obj) for obj in payload) +TimeSpan = typing.Union[int, float, datetime.timedelta] +"""A representation of time.""" -def timespan_to_int(value: typing.Union[more_typing.TimeSpanT]) -> int: +def timespan_to_int(value: typing.Union[TimeSpan], /) -> int: """Cast the given timespan in seconds to an integer value. Parameters diff --git a/hikari/internal/class_helpers.py b/hikari/utilities/klass.py similarity index 92% rename from hikari/internal/class_helpers.py rename to hikari/utilities/klass.py index fb0d51e5d2..bc9ea642ff 100644 --- a/hikari/internal/class_helpers.py +++ b/hikari/utilities/klass.py @@ -20,13 +20,11 @@ from __future__ import annotations -__all__ = ["SingletonMeta", "Singleton"] +__all__ = ["get_logger", "SingletonMeta", "Singleton"] import logging import typing -from hikari.internal import more_collections - def get_logger(cls: typing.Union[typing.Type, typing.Any], *additional_args: str) -> logging.Logger: """Get an appropriately named logger for the given class or object. @@ -74,9 +72,7 @@ class SingletonMeta(type): __slots__ = () - ___instances___: typing.Final[ - more_collections.WeakKeyDictionary[typing.Type[typing.Any], typing.Any] - ] = more_collections.WeakKeyDictionary() + ___instances___ = {} def __call__(cls): if cls not in SingletonMeta.___instances___: diff --git a/hikari/utilities/reflect.py b/hikari/utilities/reflect.py new file mode 100644 index 0000000000..f119145b4e --- /dev/null +++ b/hikari/utilities/reflect.py @@ -0,0 +1,65 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# Copyright © Nekoka.tt 2019-2020 +# +# This file is part of Hikari. +# +# Hikari is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Hikari is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Hikari. If not, see . +"""Reflection utilities.""" + +from __future__ import annotations + +__all__ = ["resolve_signature", "EMPTY"] + +import inspect +import typing + +EMPTY: typing.Final[inspect.Parameter.empty] = inspect.Parameter.empty +"""A singleton that empty annotations will be set to in `resolve_signature`.""" + + +def resolve_signature(func: typing.Callable) -> inspect.Signature: + """Get the `inspect.Signature` of `func` with resolved forward annotations. + + Parameters + ---------- + func : typing.Callable[[...], ...] + The function to get the resolved annotations from. + + Returns + ------- + typing.Signature + A `typing.Signature` object with all forward reference annotations + resolved. + """ + signature = inspect.signature(func) + resolved_type_hints = None + parameters = [] + for key, value in signature.parameters.items(): + if isinstance(value.annotation, str): + if resolved_type_hints is None: + resolved_type_hints = typing.get_type_hints(func) + parameters.append(value.replace(annotation=resolved_type_hints[key])) + else: + parameters.append(value) + signature = signature.replace(parameters=parameters) + + if isinstance(signature.return_annotation, str): + if resolved_type_hints is None: + return_annotation = typing.get_type_hints(func)["return"] + else: + return_annotation = resolved_type_hints["return"] + signature = signature.replace(return_annotation=return_annotation) + + return signature diff --git a/hikari/utilities/snowflake.py b/hikari/utilities/snowflake.py new file mode 100644 index 0000000000..78eb0de1c5 --- /dev/null +++ b/hikari/utilities/snowflake.py @@ -0,0 +1,91 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# Copyright © Nekoka.tt 2019-2020 +# +# This file is part of Hikari. +# +# Hikari is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Hikari is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Hikari. If not, see . +"""Implementation of a Snowflake type.""" + +from __future__ import annotations + +__all__ = ["Snowflake"] + +import datetime +import typing + +from hikari.utilities import date + + +class Snowflake(int): + """A concrete representation of a unique identifier for an object on Discord. + + This object can be treated as a regular `int` for most purposes. + """ + + __slots__ = () + + ___MIN___: Snowflake + ___MAX___: Snowflake + + @staticmethod + def __new__(cls, value: typing.Union[int, str, typing.SupportsInt]) -> Snowflake: + return super(Snowflake, cls).__new__(cls, value) + + @property + def created_at(self) -> datetime.datetime: + """When the object was created.""" + epoch = self >> 22 + return date.discord_epoch_to_datetime(epoch) + + @property + def internal_worker_id(self) -> int: + """ID of the worker that created this snowflake on Discord's systems.""" + return (self & 0x3E0_000) >> 17 + + @property + def internal_process_id(self) -> int: + """ID of the process that created this snowflake on Discord's systems.""" + return (self & 0x1F_000) >> 12 + + @property + def increment(self) -> int: + """Increment of Discord's system when this object was made.""" + return self & 0xFFF + + @classmethod + def from_datetime(cls, timestamp: datetime.datetime) -> Snowflake: + """Get a snowflake object from a datetime object.""" + return cls.from_data(timestamp, 0, 0, 0) + + @classmethod + def min(cls) -> Snowflake: + """Minimum value for a snowflake.""" + if not hasattr(cls, "___MIN___"): + cls.___MIN___ = Snowflake(0) + return cls.___MIN___ + + @classmethod + def max(cls) -> Snowflake: + """Maximum value for a snowflake.""" + if not hasattr(cls, "___MAX___"): + cls.___MAX___ = Snowflake((1 << 63) - 1) + return cls.___MAX___ + + @classmethod + def from_data(cls, timestamp: datetime.datetime, worker_id: int, process_id: int, increment: int) -> Snowflake: + """Convert the pieces of info that comprise an ID into a Snowflake.""" + return cls( + (date.datetime_to_discord_epoch(timestamp) << 22) | (worker_id << 17) | (process_id << 12) | increment + ) diff --git a/hikari/internal/unset.py b/hikari/utilities/unset.py similarity index 96% rename from hikari/internal/unset.py rename to hikari/utilities/unset.py index 68ddf69f2d..fdaf216bdd 100644 --- a/hikari/internal/unset.py +++ b/hikari/utilities/unset.py @@ -24,10 +24,10 @@ import typing -from hikari.internal import class_helpers +from hikari.utilities import klass -class Unset(class_helpers.Singleton): +class Unset(klass.Singleton): """A singleton value that represents an unset field. This will always have a falsified value. diff --git a/setup.py b/setup.py index 0269e8d7a6..738e6a24ac 100644 --- a/setup.py +++ b/setup.py @@ -62,7 +62,7 @@ # log.warn("!!!!!!!!!!!!!!!!!!!!EXPERIMENTAL!!!!!!!!!!!!!!!!!!!!") # log.warn("HIKARI ACCELERATION SUPPORT IS ENABLED: YOUR MILEAGE MAY VARY :^)") # -# extensions = [Accelerator("hikari.internal.marshaller", ["hikari/internal/marshaller.cpp"], **cxx_compile_kwargs)] +# extensions = [Accelerator("hikari.utilities.marshaller", ["hikari.utilities/marshaller.cpp"], **cxx_compile_kwargs)] # # cxx_spec = "c++17" # compiler_type = ccompiler.get_default_compiler() diff --git a/tests/hikari/_helpers.py b/tests/hikari/_helpers.py index f852cc2c89..cf46dbf1c0 100644 --- a/tests/hikari/_helpers.py +++ b/tests/hikari/_helpers.py @@ -35,7 +35,6 @@ import mock import pytest -from hikari.internal import marshaller from hikari.models import bases _LOGGER = logging.getLogger(__name__) diff --git a/tests/hikari/internal/test_more_asyncio.py b/tests/hikari/internal/test_aio.py similarity index 74% rename from tests/hikari/internal/test_more_asyncio.py rename to tests/hikari/internal/test_aio.py index 01c653b250..0cd520b103 100644 --- a/tests/hikari/internal/test_more_asyncio.py +++ b/tests/hikari/internal/test_aio.py @@ -17,13 +17,10 @@ # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . import asyncio -import contextlib -import mock import pytest -from hikari.internal import more_asyncio -from tests.hikari import _helpers +from hikari.utilities import aio class CoroutineStub: @@ -63,21 +60,21 @@ class TestCompletedFuture: @pytest.mark.asyncio @pytest.mark.parametrize("args", [(), (12,)]) async def test_is_awaitable(self, args): - await more_asyncio.completed_future(*args) + await aio.completed_future(*args) @pytest.mark.asyncio @pytest.mark.parametrize("args", [(), (12,)]) async def test_is_completed(self, args): - future = more_asyncio.completed_future(*args) + future = aio.completed_future(*args) assert future.done() @pytest.mark.asyncio async def test_default_result_is_none(self): - assert more_asyncio.completed_future().result() is None + assert aio.completed_future().result() is None @pytest.mark.asyncio async def test_non_default_result(self): - assert more_asyncio.completed_future(...).result() is ... + assert aio.completed_future(...).result() is ... class TestIsAsyncIterator: @@ -86,14 +83,14 @@ class AsyncIterator: async def __anext__(self): return None - assert more_asyncio.is_async_iterator(AsyncIterator()) + assert aio.is_async_iterator(AsyncIterator()) def test_on_class(self): class AsyncIterator: async def __anext__(self): return ... - assert more_asyncio.is_async_iterator(AsyncIterator) + assert aio.is_async_iterator(AsyncIterator) @pytest.mark.asyncio async def test_on_genexp(self): @@ -103,7 +100,7 @@ async def genexp(): exp = genexp() try: - assert not more_asyncio.is_async_iterator(exp) + assert not aio.is_async_iterator(exp) finally: await exp.aclose() @@ -112,28 +109,28 @@ class Iter: def __next__(self): return ... - assert not more_asyncio.is_async_iterator(Iter()) + assert not aio.is_async_iterator(Iter()) def test_on_iterator_class(self): class Iter: def __next__(self): return ... - assert not more_asyncio.is_async_iterator(Iter) + assert not aio.is_async_iterator(Iter) def test_on_async_iterable(self): class AsyncIter: def __aiter__(self): yield ... - assert not more_asyncio.is_async_iterator(AsyncIter()) + assert not aio.is_async_iterator(AsyncIter()) def test_on_async_iterable_class(self): class AsyncIter: def __aiter__(self): yield ... - assert not more_asyncio.is_async_iterator(AsyncIter) + assert not aio.is_async_iterator(AsyncIter) class TestIsAsyncIterable: @@ -142,14 +139,14 @@ class AsyncIter: async def __aiter__(self): yield ... - assert more_asyncio.is_async_iterable(AsyncIter()) + assert aio.is_async_iterable(AsyncIter()) def test_on_class(self): class AsyncIter: async def __aiter__(self): yield ... - assert more_asyncio.is_async_iterable(AsyncIter) + assert aio.is_async_iterable(AsyncIter) def test_on_delegate(self): class AsyncIterator: @@ -160,7 +157,7 @@ class AsyncIterable: def __aiter__(self): return AsyncIterator() - assert more_asyncio.is_async_iterable(AsyncIterable()) + assert aio.is_async_iterable(AsyncIterable()) def test_on_delegate_class(self): class AsyncIterator: @@ -171,25 +168,47 @@ class AsyncIterable: def __aiter__(self): return AsyncIterator() - assert more_asyncio.is_async_iterable(AsyncIterable) + assert aio.is_async_iterable(AsyncIterable) def test_on_inst(self): class AsyncIterator: async def __anext__(self): return None - assert more_asyncio.is_async_iterator(AsyncIterator()) + assert aio.is_async_iterator(AsyncIterator()) def test_on_AsyncIterator(self): class AsyncIterator: async def __anext__(self): return ... - assert not more_asyncio.is_async_iterable(AsyncIterator()) + assert not aio.is_async_iterable(AsyncIterator()) def test_on_AsyncIterator_class(self): class AsyncIterator: async def __anext__(self): return ... - assert not more_asyncio.is_async_iterable(AsyncIterator) + assert not aio.is_async_iterable(AsyncIterator) + + +# noinspection PyProtocol +@pytest.mark.asyncio +class TestFuture: + async def test_is_instance(self, event_loop): + assert isinstance(event_loop.create_future(), aio.Future) + + async def nil(): + pass + + assert isinstance(asyncio.create_task(nil()), aio.Future) + + +# noinspection PyProtocol +@pytest.mark.asyncio +class TestTask: + async def test_is_instance(self, event_loop): + async def nil(): + pass + + assert isinstance(asyncio.create_task(nil()), aio.Task) diff --git a/tests/hikari/internal/test_conversions.py b/tests/hikari/internal/test_conversions.py index 35e78eaf56..4ab65a318d 100644 --- a/tests/hikari/internal/test_conversions.py +++ b/tests/hikari/internal/test_conversions.py @@ -21,8 +21,8 @@ import pytest -from hikari.internal import conversions -from hikari.internal import unset +from hikari.utilities import conversions +from hikari.utilities import unset def test_put_if_specified_when_specified(): @@ -124,6 +124,12 @@ def test_parse_discord_epoch_to_datetime(): assert conversions.discord_epoch_to_datetime(discord_timestamp) == expected_timestamp +def test_parse_datetime_to_discord_epoch(): + timestamp = datetime.datetime(2016, 3, 14, 21, 41, 18, 956000, tzinfo=datetime.timezone.utc) + expected_discord_timestamp = 37921278956 + assert conversions.datetime_to_discord_epoch(timestamp) == expected_discord_timestamp + + def test_parse_unix_epoch_to_datetime(): unix_timestamp = 1457991678956 expected_timestamp = datetime.datetime(2016, 3, 14, 21, 41, 18, 956000, tzinfo=datetime.timezone.utc) diff --git a/tests/hikari/internal/test_class_helpers.py b/tests/hikari/internal/test_klass.py similarity index 85% rename from tests/hikari/internal/test_class_helpers.py rename to tests/hikari/internal/test_klass.py index e9b7c7ba32..05fe19d8f7 100644 --- a/tests/hikari/internal/test_class_helpers.py +++ b/tests/hikari/internal/test_klass.py @@ -18,18 +18,18 @@ # along with Hikari. If not, see . import pytest -from hikari.internal import class_helpers +from hikari.utilities import klass def test_SingletonMeta(): - class StubSingleton(metaclass=class_helpers.SingletonMeta): + class StubSingleton(metaclass=klass.SingletonMeta): pass assert StubSingleton() is StubSingleton() def test_Singleton(): - class StubSingleton(class_helpers.Singleton): + class StubSingleton(klass.Singleton): pass assert StubSingleton() is StubSingleton() @@ -49,4 +49,4 @@ class Class: ], ) def test_get_logger(args, expected_name): - assert class_helpers.get_logger(*args).name == expected_name + assert klass.get_logger(*args).name == expected_name diff --git a/tests/hikari/internal/test_more_collections.py b/tests/hikari/internal/test_more_collections.py deleted file mode 100644 index b931f8a2b6..0000000000 --- a/tests/hikari/internal/test_more_collections.py +++ /dev/null @@ -1,74 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# Copyright © Nekoka.tt 2019-2020 -# -# This file is part of Hikari. -# -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . - -from hikari.internal import more_collections - - -class TestWeakKeyDictionary: - def test_is_weak(self): - class Key: - pass - - class Value: - pass - - d: more_collections.WeakKeyDictionary[Key, Value] = more_collections.WeakKeyDictionary() - - key1 = Key() - key2 = Key() - value1 = Value() - value2 = Value() - - d[key1] = value1 - d[key2] = value2 - - assert key1 in d - assert key2 in d - assert value1 in d.values() - assert value2 in d.values() - del key2 - assert len([*d.keys()]) == 1 - assert value1 in d.values() - assert value2 not in d.values() - - -class TestWeakValueDictionary: - def test_is_weak(self): - class Key: - pass - - class Value: - pass - - d: more_collections.WeakValueDictionary[Key, Value] = more_collections.WeakValueDictionary() - - key1 = Key() - key2 = Key() - value1 = Value() - value2 = Value() - - d[key1] = value1 - d[key2] = value2 - - assert key1 in d - assert key2 in d - del value2 - assert len([*d.keys()]) == 1 - assert key1 in d - assert key2 not in d diff --git a/tests/hikari/internal/test_unset.py b/tests/hikari/internal/test_unset.py index 429596e102..4e6e05855b 100644 --- a/tests/hikari/internal/test_unset.py +++ b/tests/hikari/internal/test_unset.py @@ -18,7 +18,7 @@ # along with Hikari. If not, see . import pytest -from hikari.internal import unset +from hikari.utilities import unset from tests.hikari import _helpers diff --git a/tests/hikari/models/test_applications.py b/tests/hikari/models/test_applications.py deleted file mode 100644 index a6bff6c714..0000000000 --- a/tests/hikari/models/test_applications.py +++ /dev/null @@ -1,300 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# Copyright © Nekoka.tt 2019-2020 -# -# This file is part of Hikari. -# -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . -import mock -import pytest - -from hikari import app -from hikari.net import urls -from hikari.models import applications -from hikari.models import guilds -from hikari.models import users -from tests.hikari import _helpers - - -@pytest.fixture() -def mock_app(): - return mock.MagicMock(app.IApp) - - -@pytest.fixture() -def test_partial_integration(): - return { - "id": "123123123123123", - "name": "A Name", - "type": "twitch", - "account": {"name": "twitchUsername", "id": "123123"}, - } - - -@pytest.fixture() -def own_connection_payload(test_partial_integration): - return { - "friend_sync": False, - "id": "2513849648", - "integrations": [test_partial_integration], - "name": "FS", - "revoked": False, - "show_activity": True, - "type": "twitter", - "verified": True, - "visibility": 0, - } - - -@pytest.fixture() -def own_guild_payload(): - return { - "id": "152559372126519269", - "name": "Isopropyl", - "icon": "d4a983885dsaa7691ce8bcaaf945a", - "owner": False, - "permissions": 2147483647, - "features": ["DISCOVERABLE"], - } - - -@pytest.fixture() -def owner_payload(): - return {"username": "agent 47", "avatar": "hashed", "discriminator": "4747", "id": "474747474", "flags": 1 << 10} - - -@pytest.fixture() -def team_user_payload(): - return {"username": "aka", "avatar": "I am an avatar", "discriminator": "2222", "id": "202292292"} - - -@pytest.fixture() -def member_payload(team_user_payload): - return {"membership_state": 1, "permissions": ["*"], "team_id": "209333111222", "user": team_user_payload} - - -@pytest.fixture() -def team_payload(member_payload): - return {"icon": "hashtag", "id": "202020202", "members": [member_payload], "owner_user_id": "393030292"} - - -@pytest.fixture() -def application_information_payload(owner_payload, team_payload): - return { - "id": "209333111222", - "name": "Dream Sweet in Sea Major", - "icon": "iwiwiwiwiw", - "description": "I am an application", - "rpc_origins": ["127.0.0.0"], - "bot_public": True, - "bot_require_code_grant": False, - "owner": owner_payload, - "summary": "", - "verify_key": "698c5d0859abb686be1f8a19e0e7634d8471e33817650f9fb29076de227bca90", - "team": team_payload, - "guild_id": "2020293939", - "primary_sku_id": "2020202002", - "slug": "192.168.1.254", - "cover_image": "hashmebaby", - } - - -class TestOwnConnection: - def test_deserialize(self, own_connection_payload, test_partial_integration, mock_app): - mock_integration_obj = mock.MagicMock(guilds.PartialGuildIntegration) - with mock.patch.object(guilds.PartialGuildIntegration, "deserialize", return_value=mock_integration_obj): - connection_obj = applications.OwnConnection.deserialize(own_connection_payload, app=mock_app) - guilds.PartialGuildIntegration.deserialize.assert_called_once_with(test_partial_integration, app=mock_app) - assert connection_obj.id == "2513849648" - assert connection_obj.name == "FS" - assert connection_obj.type == "twitter" - assert connection_obj.is_revoked is False - assert connection_obj.integrations == [mock_integration_obj] - assert connection_obj.is_verified is True - assert connection_obj.is_friend_syncing is False - assert connection_obj.is_showing_activity is True - assert connection_obj.visibility is applications.ConnectionVisibility.NONE - - -class TestOwnGuild: - def test_deserialize(self, own_guild_payload, mock_app): - own_guild_obj = applications.OwnGuild.deserialize(own_guild_payload, app=mock_app) - assert own_guild_obj.is_owner is False - assert own_guild_obj.my_permissions == 2147483647 - - -class TestApplicationOwner: - def test_deserialize(self, owner_payload, mock_app): - owner_obj = applications.ApplicationOwner.deserialize(owner_payload, app=mock_app) - assert owner_obj.username == "agent 47" - assert owner_obj.discriminator == "4747" - assert owner_obj.id == 474747474 - assert owner_obj.flags == users.UserFlag.TEAM_USER - assert owner_obj.avatar_hash == "hashed" - - @pytest.fixture() - def owner_obj(self, owner_payload, mock_app): - return applications.ApplicationOwner(username=None, discriminator=None, id=None, flags=None, avatar_hash=None) - - def test_is_team_user(self, owner_obj): - owner_obj.flags = users.UserFlag.TEAM_USER | users.UserFlag.SYSTEM - assert owner_obj.is_team_user is True - owner_obj.flags = users.UserFlag.BUG_HUNTER_LEVEL_1 | users.UserFlag.HYPESQUAD_EVENTS - assert owner_obj.is_team_user is False - - -class TestTeamMember: - def test_deserialize(self, member_payload, team_user_payload, mock_app): - mock_team_user = mock.MagicMock(users.User) - with _helpers.patch_marshal_attr( - applications.TeamMember, "user", deserializer=users.User.deserialize, return_value=mock_team_user - ) as patched_deserializer: - member_obj = applications.TeamMember.deserialize(member_payload, app=mock_app) - patched_deserializer.assert_called_once_with(team_user_payload, app=mock_app) - assert member_obj.user is mock_team_user - assert member_obj.membership_state is applications.TeamMembershipState.INVITED - assert member_obj.permissions == {"*"} - assert member_obj.team_id == 209333111222 - - -class TestTeam: - def test_deserialize(self, team_payload, member_payload, mock_app): - mock_member = mock.MagicMock(applications.Team, user=mock.MagicMock(id=202292292)) - with mock.patch.object(applications.TeamMember, "deserialize", return_value=mock_member): - team_obj = applications.Team.deserialize(team_payload, app=mock_app) - applications.TeamMember.deserialize.assert_called_once_with(member_payload, app=mock_app) - assert team_obj.members == {202292292: mock_member} - assert team_obj.icon_hash == "hashtag" - assert team_obj.id == 202020202 - assert team_obj.owner_user_id == 393030292 - - @pytest.fixture() - def team_obj(self, team_payload): - return applications.Team(id=None, icon_hash="3o2o32o", members=None, owner_user_id=None,) - - def test_format_icon_url(self): - mock_team = mock.MagicMock(applications.Team, icon_hash="3o2o32o", id=22323) - mock_url = "https://cdn.discordapp.com/team-icons/22323/3o2o32o.jpg?size=64" - with mock.patch.object(urls, "generate_cdn_url", return_value=mock_url): - url = applications.Team.format_icon_url(mock_team, format_="jpg", size=64) - urls.generate_cdn_url.assert_called_once_with("team-icons", "22323", "3o2o32o", format_="jpg", size=64) - assert url == mock_url - - def test_format_icon_url_returns_none(self): - mock_team = mock.MagicMock(applications.Team, icon_hash=None, id=22323) - with mock.patch.object(urls, "generate_cdn_url", return_value=...): - url = applications.Team.format_icon_url(mock_team, format_="jpg", size=64) - urls.generate_cdn_url.assert_not_called() - assert url is None - - def test_icon_url(self, team_obj): - mock_url = "https://cdn.discordapp.com/team-icons/202020202/hashtag.png?size=4096" - with mock.patch.object(urls, "generate_cdn_url", return_value=mock_url): - url = team_obj.icon_url - urls.generate_cdn_url.assert_called_once() - assert url == mock_url - - -class TestApplication: - def test_deserialize(self, application_information_payload, team_payload, owner_payload, mock_app): - application_obj = applications.Application.deserialize(application_information_payload, app=mock_app) - assert application_obj.team == applications.Team.deserialize(team_payload) - assert application_obj.team._gateway_consumer is mock_app - assert application_obj.owner == applications.ApplicationOwner.deserialize(owner_payload) - assert application_obj.owner._gateway_consumer is mock_app - assert application_obj.id == 209333111222 - assert application_obj.name == "Dream Sweet in Sea Major" - assert application_obj.icon_hash == "iwiwiwiwiw" - assert application_obj.description == "I am an application" - assert application_obj.rpc_origins == {"127.0.0.0"} - assert application_obj.is_bot_public is True - assert application_obj.is_bot_code_grant_required is False - assert application_obj.summary == "" - assert application_obj.verify_key == b"698c5d0859abb686be1f8a19e0e7634d8471e33817650f9fb29076de227bca90" - assert application_obj.guild_id == 2020293939 - assert application_obj.primary_sku_id == 2020202002 - assert application_obj.slug == "192.168.1.254" - assert application_obj.cover_image_hash == "hashmebaby" - - @pytest.fixture() - def application_obj(self, application_information_payload): - return applications.Application( - team=None, - owner=None, - id=209333111222, - name=None, - icon_hash="iwiwiwiwiw", - description=None, - rpc_origins=None, - is_bot_public=None, - is_bot_code_grant_required=None, - summary=None, - verify_key=None, - guild_id=None, - primary_sku_id=None, - slug=None, - cover_image_hash="hashmebaby", - ) - - @pytest.fixture() - def mock_application(self): - return mock.MagicMock(applications.Application, id=22222) - - def test_icon_url(self, application_obj): - mock_url = "https://cdn.discordapp.com/app-icons/209333111222/iwiwiwiwiw.png?size=4096" - with mock.patch.object(urls, "generate_cdn_url", return_value=mock_url): - url = application_obj.icon_url - urls.generate_cdn_url.assert_called_once() - assert url == "https://cdn.discordapp.com/app-icons/209333111222/iwiwiwiwiw.png?size=4096" - - def test_format_icon_url(self, mock_application): - mock_application.icon_hash = "wosososoos" - mock_url = "https://cdn.discordapp.com/app-icons/22222/wosososoos.jpg?size=4" - with mock.patch.object(urls, "generate_cdn_url", return_value=mock_url): - url = applications.Application.format_icon_url(mock_application, format_="jpg", size=4) - urls.generate_cdn_url.assert_called_once_with( - "application-icons", "22222", "wosososoos", format_="jpg", size=4 - ) - assert url == mock_url - - def test_format_icon_url_returns_none(self, mock_application): - mock_application.icon_hash = None - with mock.patch.object(urls, "generate_cdn_url", return_value=...): - url = applications.Application.format_icon_url(mock_application, format_="jpg", size=4) - urls.generate_cdn_url.assert_not_called() - assert url is None - - def test_cover_image_url(self, application_obj): - mock_url = "https://cdn.discordapp.com/app-assets/209333111222/hashmebaby.png?size=4096" - with mock.patch.object(urls, "generate_cdn_url", return_value=mock_url): - url = application_obj.cover_image_url - urls.generate_cdn_url.assert_called_once() - assert url == mock_url - - def test_format_cover_image_url(self, mock_application): - mock_application.cover_image_hash = "wowowowowo" - mock_url = "https://cdn.discordapp.com/app-assets/22222/wowowowowo.jpg?size=42" - with mock.patch.object(urls, "generate_cdn_url", return_value=mock_url): - url = applications.Application.format_cover_image_url(mock_application, format_="jpg", size=42) - urls.generate_cdn_url.assert_called_once_with( - "application-assets", "22222", "wowowowowo", format_="jpg", size=42 - ) - assert url == mock_url - - def test_format_cover_image_url_returns_none(self, mock_application): - mock_application.cover_image_hash = None - with mock.patch.object(urls, "generate_cdn_url", return_value=...): - url = applications.Application.format_cover_image_url(mock_application, format_="jpg", size=42) - urls.generate_cdn_url.assert_not_called() - assert url is None diff --git a/tests/hikari/models/test_audit_logs.py b/tests/hikari/models/test_audit_logs.py deleted file mode 100644 index defdbdf6bb..0000000000 --- a/tests/hikari/models/test_audit_logs.py +++ /dev/null @@ -1,612 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# Copyright © Nekoka.tt 2019-2020 -# -# This file is part of Hikari. -# -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . -import contextlib -import datetime - -import mock -import pytest - -from hikari import application -from hikari.models import audit_logs -from hikari.models import channels -from hikari.models import guilds -from hikari.models import users -from hikari.models import webhooks - - -@pytest.fixture() -def mock_app(): - return mock.MagicMock(application.Application) - - -class TestAuditLogChangeKey: - def test___str__(self): - assert str(audit_logs.AuditLogChangeKey.ID) == "ID" - - def test___repr__(self): - assert repr(audit_logs.AuditLogChangeKey.ID) == "ID" - - -def test__deserialize_seconds_timedelta(): - assert audit_logs._deserialize_seconds_timedelta(30) == datetime.timedelta(seconds=30) - - -def test__deserialize_partial_roles(mock_app): - test_role_payloads = [ - {"id": "24", "name": "roleA", "hoisted": True}, - {"id": "48", "name": "roleA", "hoisted": True}, - ] - mock_role_objs = [mock.MagicMock(guilds.PartialRole, id=24), mock.MagicMock(guilds.PartialRole, id=48)] - with mock.patch.object(guilds.PartialRole, "deserialize", side_effect=mock_role_objs): - result = audit_logs._deserialize_partial_roles(test_role_payloads, app=mock_app) - assert result == {24: mock_role_objs[0], 48: mock_role_objs[1]} - guilds.PartialRole.deserialize.assert_has_calls( - [mock.call(test_role_payloads[0], app=mock_app), mock.call(test_role_payloads[1], app=mock_app),] - ) - - -def test__deserialize_day_timedelta(): - assert audit_logs._deserialize_day_timedelta("4") == datetime.timedelta(days=4) - - -def test__deserialize_overwrites(mock_app): - test_overwrite_payloads = [{"id": "24", "allow": 21, "deny": 0}, {"id": "48", "deny": 42, "allow": 0}] - mock_overwrite_objs = [ - mock.MagicMock(guilds.PartialRole, id=24), - mock.MagicMock(guilds.PartialRole, id=48), - ] - with mock.patch.object(channels.PermissionOverwrite, "deserialize", side_effect=mock_overwrite_objs): - result = audit_logs._deserialize_overwrites(test_overwrite_payloads, app=mock_app) - assert result == {24: mock_overwrite_objs[0], 48: mock_overwrite_objs[1]} - channels.PermissionOverwrite.deserialize.assert_has_calls( - [mock.call(test_overwrite_payloads[0], app=mock_app), mock.call(test_overwrite_payloads[1], app=mock_app),] - ) - - -def test__deserialize_max_uses_returns_int(): - assert audit_logs._deserialize_max_uses(120) == 120 - - -def test__deserialize_max_uses_returns_infinity(): - assert audit_logs._deserialize_max_uses(0) == float("inf") - - -def test__deserialize_max_age_returns_timedelta(): - assert audit_logs._deserialize_max_age(120) == datetime.timedelta(seconds=120) - - -def test__deserialize_max_age_returns_null(): - assert audit_logs._deserialize_max_age(0) is None - - -@pytest.fixture() -def test_audit_log_change_payload(): - return { - "key": "$add", - "old_value": [{"id": "568651298858074123", "name": "Casual"}], - "new_value": [{"id": "123123123312312", "name": "aRole"}], - } - - -class TestAuditLogChange: - def test_deserialize_with_known_component_less_converter_and_values(self, mock_app): - test_audit_log_change_payload = {"key": "rate_limit_per_user", "old_value": "0", "new_value": "60"} - mock_role_zero = mock.MagicMock(guilds.PartialRole, id=123123123312312) - mock_role_one = mock.MagicMock(guilds.PartialRole, id=568651298858074123) - with mock.patch.object(guilds.PartialRole, "deserialize", side_effect=[mock_role_zero, mock_role_one]): - audit_log_change_obj = audit_logs.AuditLogChange.deserialize(test_audit_log_change_payload, app=mock_app) - assert audit_log_change_obj._app is mock_app - assert audit_log_change_obj.key is audit_logs.AuditLogChangeKey.RATE_LIMIT_PER_USER - assert audit_log_change_obj.old_value == datetime.timedelta(seconds=0) - assert audit_log_change_obj.new_value == datetime.timedelta(seconds=60) - - def test_deserialize_with_known_component_full_converter_and_values(self, test_audit_log_change_payload, mock_app): - mock_role_zero = mock.MagicMock(guilds.PartialRole, id=123123123312312) - mock_role_one = mock.MagicMock(guilds.PartialRole, id=568651298858074123) - with mock.patch.object(guilds.PartialRole, "deserialize", side_effect=[mock_role_zero, mock_role_one]): - audit_log_change_obj = audit_logs.AuditLogChange.deserialize(test_audit_log_change_payload, app=mock_app) - guilds.PartialRole.deserialize.assert_has_calls( - [ - mock.call({"id": "123123123312312", "name": "aRole"}, app=mock_app), - mock.call({"id": "568651298858074123", "name": "Casual"}, app=mock_app), - ] - ) - assert audit_log_change_obj._app is mock_app - assert audit_log_change_obj.key is audit_logs.AuditLogChangeKey.ADD_ROLE_TO_MEMBER - assert audit_log_change_obj.old_value == {568651298858074123: mock_role_one} - assert audit_log_change_obj.new_value == {123123123312312: mock_role_zero} - - def test_deserialize_with_known_component_full_converter_and_no_values( - self, test_audit_log_change_payload, mock_app - ): - test_audit_log_change_payload = {"key": "$add"} - with mock.patch.object(guilds.PartialRole, "deserialize"): - audit_log_change_obj = audit_logs.AuditLogChange.deserialize(test_audit_log_change_payload, app=mock_app) - guilds.PartialRole.deserialize.assert_not_called() - assert audit_log_change_obj._app is mock_app - assert audit_log_change_obj.key is audit_logs.AuditLogChangeKey.ADD_ROLE_TO_MEMBER - assert audit_log_change_obj.old_value is None - assert audit_log_change_obj.new_value is None - - def test_deserialize_with_known_component_less_converter_and_no_values( - self, test_audit_log_change_payload, mock_app - ): - test_audit_log_change_payload = {"key": "rate_limit_per_user"} - with mock.patch.object(guilds.PartialRole, "deserialize"): - audit_log_change_obj = audit_logs.AuditLogChange.deserialize(test_audit_log_change_payload, app=mock_app) - guilds.PartialRole.deserialize.assert_not_called() - assert audit_log_change_obj._app is mock_app - assert audit_log_change_obj.key is audit_logs.AuditLogChangeKey.RATE_LIMIT_PER_USER - assert audit_log_change_obj.old_value is None - assert audit_log_change_obj.new_value is None - - def test_deserialize_with_unknown_converter_and_values(self, test_audit_log_change_payload, mock_app): - test_audit_log_change_payload["key"] = "aUnknownKey" - audit_log_change_obj = audit_logs.AuditLogChange.deserialize(test_audit_log_change_payload, app=mock_app) - assert audit_log_change_obj._app is mock_app - assert audit_log_change_obj.key == "aUnknownKey" - assert audit_log_change_obj.old_value == test_audit_log_change_payload["old_value"] - assert audit_log_change_obj.new_value == test_audit_log_change_payload["new_value"] - - def test_deserialize_with_unknown_converter_and_no_values(self, test_audit_log_change_payload, mock_app): - test_audit_log_change_payload["key"] = "aUnknownKey" - del test_audit_log_change_payload["old_value"] - del test_audit_log_change_payload["new_value"] - audit_log_change_obj = audit_logs.AuditLogChange.deserialize(test_audit_log_change_payload, app=mock_app) - assert audit_log_change_obj._app is mock_app - assert audit_log_change_obj.key == "aUnknownKey" - assert audit_log_change_obj.old_value is None - assert audit_log_change_obj.new_value is None - - -class TestChannelOverwriteEntryInfo: - @pytest.fixture() - def test_overwrite_info_payload(self): - return {"id": "123123123", "type": "role", "role_name": "aRole"} - - def test_deserialize(self, test_overwrite_info_payload, mock_app): - overwrite_entry_info = audit_logs.ChannelOverwriteEntryInfo.deserialize( - test_overwrite_info_payload, app=mock_app - ) - assert overwrite_entry_info.id == 123123123 - assert overwrite_entry_info.type is channels.PermissionOverwriteType.ROLE - assert overwrite_entry_info.role_name == "aRole" - - -class TestMessagePinEntryInfo: - @pytest.fixture() - def test_message_pin_info_payload(self): - return { - "channel": "123123123", - "message_id": "69696969", - } - - def test_deserialize(self, test_message_pin_info_payload, mock_app): - message_pin_info_obj = audit_logs.MessagePinEntryInfo.deserialize(test_message_pin_info_payload, app=mock_app) - assert message_pin_info_obj.channel_id == 123123123 - assert message_pin_info_obj.message_id == 69696969 - - -class TestMemberPruneEntryInfo: - @pytest.fixture() - def test_member_prune_info_payload(self): - return { - "delete_member_days": "7", - "members_removed": "1", - } - - def test_deserialize(self, test_member_prune_info_payload, mock_app): - member_prune_info_obj = audit_logs.MemberPruneEntryInfo.deserialize( - test_member_prune_info_payload, app=mock_app - ) - assert member_prune_info_obj.delete_member_days == datetime.timedelta(days=7) - assert member_prune_info_obj.members_removed == 1 - - -class TestMessageDeleteEntryInfo: - @pytest.fixture() - def test_message_delete_info_payload(self): - return {"count": "42", "channel": "4206942069"} - - def test_deserialize(self, test_message_delete_info_payload, mock_app): - message_delete_entry_info = audit_logs.MessageDeleteEntryInfo.deserialize( - test_message_delete_info_payload, app=mock_app - ) - assert message_delete_entry_info.channel_id == 4206942069 - - -class TestMessageBulkDeleteEntryInfo: - @pytest.fixture() - def test_message_bulk_delete_info_payload(self): - return {"count": "42"} - - def test_deserialize(self, test_message_bulk_delete_info_payload, mock_app): - message_bulk_delete_entry_info = audit_logs.MessageBulkDeleteEntryInfo.deserialize( - test_message_bulk_delete_info_payload, app=mock_app - ) - assert message_bulk_delete_entry_info.count == 42 - - -class TestMemberDisconnectEntryInfo: - @pytest.fixture() - def test_member_disconnect_info_payload(self): - return {"count": "42"} - - def test_deserialize(self, test_member_disconnect_info_payload, mock_app): - member_disconnect_entry_info = audit_logs.MemberDisconnectEntryInfo.deserialize( - test_member_disconnect_info_payload, app=mock_app - ) - assert member_disconnect_entry_info.count == 42 - - -class TestMemberMoveEntryInfo: - @pytest.fixture() - def test_member_move_info_payload(self): - return {"count": "42", "channel": "22222222"} - - def test_deserialize(self, test_member_move_info_payload, mock_app): - member_move_entry_info = audit_logs.MemberMoveEntryInfo.deserialize(test_member_move_info_payload, app=mock_app) - assert member_move_entry_info.channel_id == 22222222 - - -class TestUnrecognisedAuditLogEntryInfo: - @pytest.fixture() - def test_unrecognised_audit_log_entry(self): - return {"count": "5412", "action": "nyaa'd"} - - def test_deserialize(self, test_unrecognised_audit_log_entry, mock_app): - unrecognised_info_obj = audit_logs.UnrecognisedAuditLogEntryInfo.deserialize( - test_unrecognised_audit_log_entry, app=mock_app - ) - assert unrecognised_info_obj.count == "5412" - assert unrecognised_info_obj.action == "nyaa'd" - - -@pytest.mark.parametrize( - ("type_", "expected_entity"), - [ - (13, audit_logs.ChannelOverwriteEntryInfo), - (14, audit_logs.ChannelOverwriteEntryInfo), - (15, audit_logs.ChannelOverwriteEntryInfo), - (74, audit_logs.MessagePinEntryInfo), - (75, audit_logs.MessagePinEntryInfo), - (21, audit_logs.MemberPruneEntryInfo), - (72, audit_logs.MessageDeleteEntryInfo), - (73, audit_logs.MessageBulkDeleteEntryInfo), - (27, audit_logs.MemberDisconnectEntryInfo), - (26, audit_logs.MemberMoveEntryInfo), - ], -) -def test_get_audit_log_entry_info_entity(type_, expected_entity): - assert audit_logs.get_entry_info_entity(type_) is expected_entity - - -@pytest.fixture() -def test_audit_log_option_payload(): - return { - "id": "115590097100865541", - "type": "member", - } - - -@pytest.fixture() -def test_audit_log_entry_payload(test_audit_log_change_payload, test_audit_log_option_payload): - return { - "event_type": 14, - "changes": [test_audit_log_change_payload], - "id": "694026906592477214", - "options": test_audit_log_option_payload, - "target_id": "115590097100865541", - "user_id": "560984860634644482", - "reason": "An artificial insanity.", - } - - -class TestAuditLogEntry: - def test_deserialize_with_options_and_target_id_and_known_type( - self, test_audit_log_entry_payload, test_audit_log_option_payload, test_audit_log_change_payload, mock_app, - ): - audit_log_entry_obj = audit_logs.AuditLogEntry.deserialize(test_audit_log_entry_payload, app=mock_app) - assert audit_log_entry_obj._app is mock_app - assert audit_log_entry_obj.changes == [audit_logs.AuditLogChange.deserialize(test_audit_log_change_payload)] - assert audit_log_entry_obj.options == audit_logs.ChannelOverwriteEntryInfo.deserialize( - test_audit_log_option_payload - ) - assert audit_log_entry_obj.options._app is mock_app - assert audit_log_entry_obj.target_id == 115590097100865541 - assert audit_log_entry_obj.user_id == 560984860634644482 - assert audit_log_entry_obj.id == 694026906592477214 - assert audit_log_entry_obj.action_type is audit_logs.AuditLogEventType.CHANNEL_OVERWRITE_UPDATE - assert audit_log_entry_obj.reason == "An artificial insanity." - - def test_deserialize_with_known_type_without_options_or_target_( - self, test_audit_log_entry_payload, test_audit_log_change_payload, mock_app - ): - del test_audit_log_entry_payload["options"] - del test_audit_log_entry_payload["target_id"] - audit_log_entry_obj = audit_logs.AuditLogEntry.deserialize(test_audit_log_entry_payload, app=mock_app) - assert audit_log_entry_obj.changes == [audit_logs.AuditLogChange.deserialize(test_audit_log_change_payload)] - assert audit_log_entry_obj.options is None - assert audit_log_entry_obj.target_id is None - assert audit_log_entry_obj.user_id == 560984860634644482 - assert audit_log_entry_obj.id == 694026906592477214 - assert audit_log_entry_obj.action_type is audit_logs.AuditLogEventType.CHANNEL_OVERWRITE_UPDATE - assert audit_log_entry_obj.reason == "An artificial insanity." - - def test_deserialize_with_options_and_target_id_and_unknown_type( - self, test_audit_log_entry_payload, test_audit_log_option_payload, test_audit_log_change_payload, mock_app, - ): - test_audit_log_entry_payload["event_type"] = 123123123 - audit_log_entry_obj = audit_logs.AuditLogEntry.deserialize(test_audit_log_entry_payload, app=mock_app) - assert audit_log_entry_obj.changes == [audit_logs.AuditLogChange.deserialize(test_audit_log_change_payload)] - assert audit_log_entry_obj.options == audit_logs.UnrecognisedAuditLogEntryInfo.deserialize( - test_audit_log_option_payload - ) - assert audit_log_entry_obj.target_id == 115590097100865541 - assert audit_log_entry_obj.user_id == 560984860634644482 - assert audit_log_entry_obj.id == 694026906592477214 - assert audit_log_entry_obj.action_type == 123123123 - assert audit_log_entry_obj.reason == "An artificial insanity." - - def test_deserialize_without_options_or_target_id_and_unknown_type( - self, test_audit_log_entry_payload, test_audit_log_option_payload, test_audit_log_change_payload, mock_app, - ): - del test_audit_log_entry_payload["options"] - del test_audit_log_entry_payload["target_id"] - test_audit_log_entry_payload["event_type"] = 123123123 - audit_log_entry_obj = audit_logs.AuditLogEntry.deserialize(test_audit_log_entry_payload, app=mock_app) - assert audit_log_entry_obj.changes == [audit_logs.AuditLogChange.deserialize(test_audit_log_change_payload)] - assert audit_log_entry_obj.options is None - assert audit_log_entry_obj.target_id is None - assert audit_log_entry_obj.user_id == 560984860634644482 - assert audit_log_entry_obj.id == 694026906592477214 - assert audit_log_entry_obj.action_type == 123123123 - assert audit_log_entry_obj.reason == "An artificial insanity." - - -class TestAuditLog: - @pytest.fixture() - def test_integration_payload(self): - return {"id": 33590653072239123, "name": "A Name", "type": "twitch", "account": {}} - - @pytest.fixture() - def test_user_payload(self): - return {"id": "92929292", "username": "A USER", "discriminator": "6969", "avatar": None} - - @pytest.fixture() - def test_webhook_payload(self): - return {"id": "424242", "type": 1, "channel": "2020202"} - - @pytest.fixture() - def test_audit_log_payload( - self, test_audit_log_entry_payload, test_integration_payload, test_user_payload, test_webhook_payload - ): - return { - "audit_log_entries": [test_audit_log_entry_payload], - "integrations": [test_integration_payload], - "users": [test_user_payload], - "webhooks": [test_webhook_payload], - } - - def test_deserialize( - self, - test_audit_log_payload, - test_audit_log_entry_payload, - test_integration_payload, - test_user_payload, - test_webhook_payload, - mock_app, - ): - mock_webhook_obj = mock.MagicMock(webhooks.Webhook, id=424242) - mock_user_obj = mock.MagicMock(users.User, id=92929292) - mock_integration_obj = mock.MagicMock(guilds.PartialGuildIntegration, id=33590653072239123) - - stack = contextlib.ExitStack() - stack.enter_context(mock.patch.object(webhooks.Webhook, "deserialize", return_value=mock_webhook_obj)) - stack.enter_context(mock.patch.object(users.User, "deserialize", return_value=mock_user_obj)) - stack.enter_context( - mock.patch.object(guilds.PartialGuildIntegration, "deserialize", return_value=mock_integration_obj) - ) - - with stack: - audit_log_obj = audit_logs.AuditLog.deserialize(test_audit_log_payload, app=mock_app) - webhooks.Webhook.deserialize.assert_called_once_with(test_webhook_payload, app=mock_app) - users.User.deserialize.assert_called_once_with(test_user_payload, app=mock_app) - guilds.PartialGuildIntegration.deserialize.assert_called_once_with(test_integration_payload, app=mock_app) - assert audit_log_obj.entries == { - 694026906592477214: audit_logs.AuditLogEntry.deserialize(test_audit_log_entry_payload) - } - assert audit_log_obj.entries[694026906592477214]._gateway_consumer is mock_app - assert audit_log_obj.webhooks == {424242: mock_webhook_obj} - assert audit_log_obj.users == {92929292: mock_user_obj} - assert audit_log_obj.integrations == {33590653072239123: mock_integration_obj} - - -class TestAuditLogIterator: - @pytest.mark.asyncio - async def test__fill_when_entities_returned(self, mock_app): - mock_webhook_payload = {"id": "292393993", "channel": "43242"} - mock_webhook_obj = mock.MagicMock(webhooks.Webhook, id=292393993) - mock_user_payload = {"id": "929292", "public_flags": "22222"} - mock_user_obj = mock.MagicMock(users.User, id=929292) - mock_audit_log_entry_payload = {"target_id": "202020", "id": "222"} - mock_integration_payload = {"id": "123123123", "account": {}} - mock_integration_obj = mock.MagicMock(guilds.PartialGuildIntegration, id=123123123) - mock_request = mock.AsyncMock( - return_value={ - "webhooks": [mock_webhook_payload], - "users": [mock_user_payload], - "audit_log_entries": [mock_audit_log_entry_payload], - "integrations": [mock_integration_payload], - } - ) - audit_log_iterator = audit_logs.AuditLogIterator(app=mock_app, request=mock_request, before="123", limit=None,) - stack = contextlib.ExitStack() - stack.enter_context(mock.patch.object(users.User, "deserialize", return_value=mock_user_obj)) - stack.enter_context(mock.patch.object(webhooks.Webhook, "deserialize", return_value=mock_webhook_obj)) - stack.enter_context( - mock.patch.object(guilds.PartialGuildIntegration, "deserialize", return_value=mock_integration_obj) - ) - - with stack: - assert await audit_log_iterator._fill() is None - users.User.deserialize.assert_called_once_with(mock_user_payload, app=mock_app) - webhooks.Webhook.deserialize.assert_called_once_with(mock_webhook_payload, app=mock_app) - guilds.PartialGuildIntegration.deserialize.assert_called_once_with(mock_integration_payload, app=mock_app) - assert audit_log_iterator.webhooks == {292393993: mock_webhook_obj} - assert audit_log_iterator.users == {929292: mock_user_obj} - assert audit_log_iterator.integrations == {123123123: mock_integration_obj} - assert audit_log_iterator._buffer == [mock_audit_log_entry_payload] - assert audit_log_iterator._app is mock_app - mock_request.assert_called_once_with( - before="123", limit=100, - ) - - @pytest.mark.asyncio - async def test__fill_when_resource_exhausted(self, mock_app): - mock_request = mock.AsyncMock( - return_value={"webhooks": [], "users": [], "audit_log_entries": [], "integrations": []} - ) - audit_log_iterator = audit_logs.AuditLogIterator( - app=mock_app, request=mock_request, before="222222222", limit=None, - ) - stack = contextlib.ExitStack() - stack.enter_context(mock.patch.object(users.User, "deserialize", return_value=...)) - stack.enter_context(mock.patch.object(webhooks.Webhook, "deserialize", return_value=...)) - stack.enter_context(mock.patch.object(guilds.PartialGuildIntegration, "deserialize", return_value=...)) - - with stack: - assert await audit_log_iterator._fill() is None - users.User.deserialize.assert_not_called() - webhooks.Webhook.deserialize.assert_not_called() - guilds.PartialGuildIntegration.deserialize.assert_not_called() - assert audit_log_iterator.webhooks == {} - assert audit_log_iterator.users == {} - assert audit_log_iterator.integrations == {} - assert audit_log_iterator._buffer == [] - mock_request.assert_called_once_with( - before="222222222", limit=100, - ) - - @pytest.mark.asyncio - async def test__fill_when_before_and_limit_not_set(self, mock_app): - mock_request = mock.AsyncMock( - return_value={ - "webhooks": [], - "users": [], - "audit_log_entries": [{"id": "123123123"}, {"id": "123123123"}], - "integrations": [], - } - ) - audit_log_iterator = audit_logs.AuditLogIterator(app=mock_app, request=mock_request, before="123", limit=None,) - stack = contextlib.ExitStack() - stack.enter_context(mock.patch.object(users.User, "deserialize", return_value=...)) - stack.enter_context(mock.patch.object(webhooks.Webhook, "deserialize", return_value=...)) - stack.enter_context(mock.patch.object(guilds.PartialGuildIntegration, "deserialize", return_value=...)) - - with stack: - assert await audit_log_iterator._fill() is None - mock_request.assert_called_once_with( - before="123", limit=100, - ) - assert audit_log_iterator._limit is None - - @pytest.mark.asyncio - async def test__fill_when_before_and_limit_set(self, mock_app): - mock_request = mock.AsyncMock( - return_value={ - "webhooks": [], - "users": [], - "audit_log_entries": [{"id": "123123123"}, {"id": "123123123"}], - "integrations": [], - } - ) - audit_log_iterator = audit_logs.AuditLogIterator( - app=mock_app, request=mock_request, before="222222222", limit=44, - ) - stack = contextlib.ExitStack() - stack.enter_context(mock.patch.object(users.User, "deserialize", return_value=...)) - stack.enter_context(mock.patch.object(webhooks.Webhook, "deserialize", return_value=...)) - stack.enter_context(mock.patch.object(guilds.PartialGuildIntegration, "deserialize", return_value=...)) - - with stack: - assert await audit_log_iterator._fill() is None - mock_request.assert_called_once_with( - before="222222222", limit=44, - ) - assert audit_log_iterator._limit == 42 - - @pytest.mark.asyncio - async def test___anext___when_not_filled_and_resource_is_exhausted(self, mock_app): - mock_request = mock.AsyncMock( - return_value={"webhooks": [], "users": [], "audit_log_entries": [], "integrations": []} - ) - iterator = audit_logs.AuditLogIterator(app=mock_app, request=mock_request, before="123", limit=None) - with mock.patch.object(audit_logs.AuditLogEntry, "deserialize", return_value=...): - async for _ in iterator: - assert False, "Iterator shouldn't have yielded anything." - audit_logs.AuditLogEntry.deserialize.assert_not_called() - assert iterator._front == "123" - - @pytest.mark.asyncio - async def test___anext___when_not_filled(self, mock_app): - mock_request = mock.AsyncMock( - side_effect=[{"webhooks": [], "users": [], "audit_log_entries": [{"id": "666666"}], "integrations": []}] - ) - mock_audit_log_entry = mock.MagicMock(audit_logs.AuditLogEntry, id=666666) - iterator = audit_logs.AuditLogIterator(app=mock_app, request=mock_request, before="123", limit=None) - with mock.patch.object(audit_logs.AuditLogEntry, "deserialize", side_effect=[mock_audit_log_entry]): - async for result in iterator: - assert result is mock_audit_log_entry - break - audit_logs.AuditLogEntry.deserialize.assert_called_once_with({"id": "666666"}, app=mock_app) - mock_request.assert_called_once_with( - before="123", limit=100, - ) - assert iterator._front == "666666" - - @pytest.mark.asyncio - async def test___anext___when_not_filled_and_limit_exhausted(self, mock_app): - mock_request = mock.AsyncMock( - side_effect=[{"webhooks": [], "users": [], "audit_log_entries": [], "integrations": []}] - ) - mock_audit_log_entry = mock.MagicMock(audit_logs.AuditLogEntry, id=666666) - iterator = audit_logs.AuditLogIterator(app=mock_app, request=mock_request, before="123", limit=None) - with mock.patch.object(audit_logs.AuditLogEntry, "deserialize", side_effect=[mock_audit_log_entry]): - async for _ in iterator: - assert False, "Iterator shouldn't have yielded anything." - audit_logs.AuditLogEntry.deserialize.assert_not_called() - mock_request.assert_called_once_with( - before="123", limit=100, - ) - assert iterator._front == "123" - - @pytest.mark.asyncio - async def test___anext___when_filled(self, mock_app): - mock_request = mock.AsyncMock(side_effect=[]) - mock_audit_log_entry = mock.MagicMock(audit_logs.AuditLogEntry, id=4242) - iterator = audit_logs.AuditLogIterator(app=mock_app, request=mock_request, before="123",) - iterator._buffer = [{"id": "123123"}] - with mock.patch.object(audit_logs.AuditLogEntry, "deserialize", side_effect=[mock_audit_log_entry]): - async for result in iterator: - assert result is mock_audit_log_entry - break - audit_logs.AuditLogEntry.deserialize.assert_called_once_with({"id": "123123"}, app=mock_app) - mock_request.assert_not_called() - assert iterator._front == "4242" diff --git a/tests/hikari/models/test_bases.py b/tests/hikari/models/test_bases.py deleted file mode 100644 index cd754ebbf0..0000000000 --- a/tests/hikari/models/test_bases.py +++ /dev/null @@ -1,140 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# Copyright © Nekoka.tt 2019-2020 -# -# This file is part of Hikari. -# -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . -import datetime -import typing - -import attr -import mock -import pytest - -from hikari import application -from hikari.internal import marshaller -from hikari.models import bases - - -class TestHikariEntity: - @pytest.fixture() - def stub_entity(self) -> typing.Type["StubEntity"]: - @marshaller.marshallable() - @attr.s() - class StubEntity(bases.Entity, marshaller.Deserializable, marshaller.Serializable): - ... - - return StubEntity - - def test_deserialize(self, stub_entity): - mock_app = mock.MagicMock(application.Application) - entity = stub_entity.deserialize({}, app=mock_app) - assert entity._gateway_consumer is mock_app - - def test_serialize(self, stub_entity): - mock_app = mock.MagicMock(application.Application) - assert stub_entity(app=mock_app).serialize() == {} - - -class TestSnowflake: - @pytest.fixture() - def raw_id(self): - return 537_340_989_808_050_216 - - @pytest.fixture() - def neko_snowflake(self, raw_id): - return bases.Snowflake(raw_id) - - def test_created_at(self, neko_snowflake): - assert neko_snowflake.created_at == datetime.datetime( - 2019, 1, 22, 18, 41, 15, 283_000, tzinfo=datetime.timezone.utc - ) - - def test_increment(self, neko_snowflake): - assert neko_snowflake.increment == 40 - - def test_internal_process_id(self, neko_snowflake): - assert neko_snowflake.internal_process_id == 0 - - def test_internal_worker_id(self, neko_snowflake): - assert neko_snowflake.internal_worker_id == 2 - - def test_hash(self, neko_snowflake, raw_id): - assert hash(neko_snowflake) == raw_id - - def test_int_cast(self, neko_snowflake, raw_id): - assert int(neko_snowflake) == raw_id - - def test_str_cast(self, neko_snowflake, raw_id): - assert str(neko_snowflake) == str(raw_id) - - def test_repr_cast(self, neko_snowflake, raw_id): - assert repr(neko_snowflake) == repr(raw_id) - - def test_eq(self, neko_snowflake, raw_id): - assert neko_snowflake == raw_id - assert neko_snowflake == bases.Snowflake(raw_id) - assert str(raw_id) != neko_snowflake - - def test_lt(self, neko_snowflake, raw_id): - assert neko_snowflake < raw_id + 1 - - def test_deserialize(self, neko_snowflake, raw_id): - assert neko_snowflake == bases.Snowflake(raw_id) - - def test_from_datetime(self): - result = bases.Snowflake.from_datetime( - datetime.datetime(2019, 1, 22, 18, 41, 15, 283_000, tzinfo=datetime.timezone.utc) - ) - assert result == 537340989807788032 - assert isinstance(result, bases.Snowflake) - - def test_from_timestamp(self): - result = bases.Snowflake.from_timestamp(1548182475.283) - assert result == 537340989807788032 - assert isinstance(result, bases.Snowflake) - - def test_min(self): - sf = bases.Snowflake.min() - assert sf == 0 - assert bases.Snowflake.min() is sf - - def test_max(self): - sf = bases.Snowflake.max() - assert sf == (1 << 63) - 1 - assert bases.Snowflake.max() is sf - - -@marshaller.marshallable() -@attr.s(slots=True) -class StubEntity(bases.Unique, marshaller.Deserializable, marshaller.Serializable): - ... - - -class TestUnique: - def test_created_at(self): - entity = bases.Unique(id=bases.Snowflake("9217346714023428234")) - assert entity.created_at == entity.id.created_at - - def test_int(self): - assert int(bases.Unique(id=bases.Snowflake("2333333"))) == 2333333 - - def test_deserialize(self): - unique_entity = StubEntity.deserialize({"id": "5445"}) - assert unique_entity.id == bases.Snowflake("5445") - assert isinstance(unique_entity.id, bases.Snowflake) - - def test_serialize(self): - assert StubEntity(id=bases.Snowflake(5445)).serialize() == {"id": "5445"} diff --git a/tests/hikari/models/test_channels.py b/tests/hikari/models/test_channels.py deleted file mode 100644 index d88d9b1a38..0000000000 --- a/tests/hikari/models/test_channels.py +++ /dev/null @@ -1,450 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# Copyright © Nekoka.tt 2019-2020 -# -# This file is part of Hikari. -# -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . -import datetime - -import mock -import pytest - -from hikari.models import bases -from hikari.models import channels -from hikari.models import permissions -from hikari.models import users -from hikari import application -from hikari.net import urls - - -@pytest.fixture() -def test_recipient_payload(): - return {"username": "someone", "discriminator": "9999", "id": "987", "avatar": "qrqwefasfefd"} - - -@pytest.fixture() -def test_permission_overwrite_payload(): - return {"id": "4242", "type": "member", "allow": 65, "deny": 49152} - - -@pytest.fixture() -def test_dm_channel_payload(test_recipient_payload): - return { - "id": "123", - "last_message_id": "456", - "type": 1, - "recipients": [test_recipient_payload], - } - - -@pytest.fixture() -def test_group_dm_channel_payload(test_recipient_payload): - return { - "id": "123", - "name": "Secret Developer Group", - "icon": "123asdf123adsf", - "owner_id": "456", - "application_id": "123789", - "last_message_id": "456", - "type": 3, - "recipients": [test_recipient_payload], - } - - -@pytest.fixture() -def test_guild_category_payload(test_permission_overwrite_payload): - return { - "id": "123", - "permission_overwrites": [test_permission_overwrite_payload], - "name": "Test", - "parent_id": None, - "nsfw": True, - "position": 3, - "guild_id": "9876", - "type": 4, - } - - -@pytest.fixture() -def test_guild_text_channel_payload(test_permission_overwrite_payload): - return { - "id": "123", - "guild_id": "567", - "name": "general", - "type": 0, - "position": 6, - "permission_overwrites": [test_permission_overwrite_payload], - "rate_limit_per_user": 2, - "nsfw": True, - "topic": "¯\\_(ツ)_/¯", - "last_message_id": "123456", - "parent_id": "987", - } - - -@pytest.fixture() -def test_guild_news_channel_payload(test_permission_overwrite_payload): - return { - "id": "567", - "guild_id": "123", - "name": "Important Announcements", - "type": 5, - "position": 0, - "permission_overwrites": [test_permission_overwrite_payload], - "nsfw": True, - "topic": "Super Important Announcements", - "last_message_id": "456", - "parent_id": "654", - } - - -@pytest.fixture() -def test_guild_store_channel_payload(test_permission_overwrite_payload): - return { - "id": "123", - "permission_overwrites": [test_permission_overwrite_payload], - "name": "Half Life 3", - "parent_id": "9876", - "nsfw": True, - "position": 2, - "guild_id": "1234", - "type": 6, - } - - -@pytest.fixture() -def test_guild_voice_channel_payload(test_permission_overwrite_payload): - return { - "id": "123", - "guild_id": "789", - "name": "Secret Developer Discussions", - "type": 2, - "nsfw": True, - "position": 4, - "permission_overwrites": [test_permission_overwrite_payload], - "bitrate": 64000, - "user_limit": 3, - "parent_id": "456", - } - - -@pytest.fixture() -def mock_app(): - return mock.MagicMock(application.Application) - - -class TestPartialChannel: - @pytest.fixture() - def test_partial_channel_payload(self): - return {"id": "561884984214814750", "name": "general", "type": 0} - - def test_deserialize(self, test_partial_channel_payload, mock_app): - partial_channel_obj = channels.PartialChannel.deserialize(test_partial_channel_payload, app=mock_app) - assert partial_channel_obj.id == 561884984214814750 - assert partial_channel_obj.name == "general" - assert partial_channel_obj.type is channels.ChannelType.GUILD_TEXT - - -class TestPermissionOverwriteType: - def test___int__(self): - assert str(channels.PermissionOverwriteType.ROLE) == "role" - - -class TestPermissionOverwrite: - def test_deserialize(self, test_permission_overwrite_payload, mock_app): - permission_overwrite_obj = channels.PermissionOverwrite.deserialize( - test_permission_overwrite_payload, app=mock_app - ) - assert ( - permission_overwrite_obj.allow - == permissions.Permission.CREATE_INSTANT_INVITE | permissions.Permission.ADD_REACTIONS - ) - assert permission_overwrite_obj.deny == permissions.Permission.EMBED_LINKS | permissions.Permission.ATTACH_FILES - - def test_serialize_full_overwrite(self): - permission_overwrite_obj = channels.PermissionOverwrite( - id=bases.Snowflake(11111111), - type=channels.PermissionOverwriteType.ROLE, - allow=permissions.Permission(1321), - deny=permissions.Permission(39939), - ) - assert permission_overwrite_obj.serialize() == {"id": "11111111", "type": "role", "allow": 1321, "deny": 39939} - - def test_serialize_partial_overwrite(self): - permission_overwrite_obj = channels.PermissionOverwrite( - id=bases.Snowflake(11111111), type=channels.PermissionOverwriteType.ROLE, - ) - assert permission_overwrite_obj.serialize() == {"id": "11111111", "type": "role", "allow": 0, "deny": 0} - - def test_unset(self): - permission_overwrite_obj = channels.PermissionOverwrite( - id=None, type=None, allow=permissions.Permission(65), deny=permissions.Permission(49152) - ) - assert permission_overwrite_obj.unset == permissions.Permission(49217) - assert isinstance(permission_overwrite_obj.unset, permissions.Permission) - - -class TestDMChannel: - def test_deserialize(self, test_dm_channel_payload, test_recipient_payload, mock_app): - mock_user = mock.MagicMock(users.User, id=987) - - with mock.patch.object(users.User, "deserialize", return_value=mock_user) as patched_user_deserialize: - channel_obj = channels.DMChannel.deserialize(test_dm_channel_payload, app=mock_app) - patched_user_deserialize.assert_called_once_with(test_recipient_payload, app=mock_app) - - assert channel_obj.id == 123 - assert channel_obj.last_message_id == 456 - assert channel_obj.type == channels.ChannelType.DM - assert channel_obj.recipients == {987: mock_user} - - -class TestGroupDMChannel: - def test_deserialize(self, test_group_dm_channel_payload, test_recipient_payload, mock_app): - mock_user = mock.MagicMock(users.User, id=987) - - with mock.patch.object(users.User, "deserialize", return_value=mock_user) as patched_user_deserialize: - channel_obj = channels.GroupDMChannel.deserialize(test_group_dm_channel_payload, app=mock_app) - patched_user_deserialize.assert_called_once_with(test_recipient_payload, app=mock_app) - - assert channel_obj.id == 123 - assert channel_obj.last_message_id == 456 - assert channel_obj.type == channels.ChannelType.GROUP_DM - assert channel_obj.recipients == {987: mock_user} - assert channel_obj.name == "Secret Developer Group" - assert channel_obj.icon_hash == "123asdf123adsf" - assert channel_obj.owner_id == 456 - assert channel_obj.application_id == 123789 - - @pytest.fixture() - def group_dm_obj(self): - return channels.GroupDMChannel( - id=bases.Snowflake(123123123), - last_message_id=None, - type=None, - recipients=None, - name=None, - icon_hash="123asdf123adsf", - owner_id=None, - application_id=None, - ) - - def test_icon_url(self, group_dm_obj): - mock_url = "https://cdn.discordapp.com/channel-icons/209333111222/hashmebaby.png?size=4096" - with mock.patch.object(urls, "generate_cdn_url", return_value=mock_url): - url = group_dm_obj.icon_url - urls.generate_cdn_url.assert_called_once() - assert url == mock_url - - def test_format_icon_url(self, group_dm_obj): - mock_url = "https://cdn.discordapp.com/channel-icons/22222/wowowowowo.jpg?size=42" - with mock.patch.object(urls, "generate_cdn_url", return_value=mock_url): - url = channels.GroupDMChannel.format_icon_url(group_dm_obj, format_="jpg", size=42) - urls.generate_cdn_url.assert_called_once_with( - "channel-icons", "123123123", "123asdf123adsf", format_="jpg", size=42 - ) - assert url == mock_url - - def test_format_icon_url_returns_none(self, group_dm_obj): - group_dm_obj.icon_hash = None - with mock.patch.object(urls, "generate_cdn_url", return_value=...): - url = channels.GroupDMChannel.format_icon_url(group_dm_obj, format_="jpg", size=42) - urls.generate_cdn_url.assert_not_called() - assert url is None - - -class TestGuildCategory: - def test_deserialize(self, test_guild_category_payload, test_permission_overwrite_payload, mock_app): - channel_obj = channels.GuildCategory.deserialize(test_guild_category_payload, app=mock_app) - - assert channel_obj.id == 123 - assert channel_obj.permission_overwrites == { - 4242: channels.PermissionOverwrite.deserialize(test_permission_overwrite_payload) - } - assert channel_obj.permission_overwrites[4242]._gateway_consumer is mock_app - assert channel_obj.guild_id == 9876 - assert channel_obj.position == 3 - assert channel_obj.name == "Test" - assert channel_obj.is_nsfw is True - assert channel_obj.parent_id is None - assert channel_obj.type == channels.ChannelType.GUILD_CATEGORY - - -class TestGuildTextChannel: - def test_deserialize(self, test_guild_text_channel_payload, test_permission_overwrite_payload, mock_app): - channel_obj = channels.GuildTextChannel.deserialize(test_guild_text_channel_payload, app=mock_app) - - assert channel_obj.id == 123 - assert channel_obj.permission_overwrites == { - 4242: channels.PermissionOverwrite.deserialize(test_permission_overwrite_payload) - } - assert channel_obj.permission_overwrites[4242]._gateway_consumer is mock_app - assert channel_obj.guild_id == 567 - assert channel_obj.position == 6 - assert channel_obj.name == "general" - assert channel_obj.topic == "¯\\_(ツ)_/¯" - assert channel_obj.is_nsfw is True - assert channel_obj.parent_id == 987 - assert channel_obj.type == channels.ChannelType.GUILD_TEXT - assert channel_obj.last_message_id == 123456 - assert channel_obj.rate_limit_per_user == datetime.timedelta(seconds=2) - - -class TestGuildNewsChannel: - def test_deserialize(self, test_guild_news_channel_payload, test_permission_overwrite_payload, mock_app): - channel_obj = channels.GuildNewsChannel.deserialize(test_guild_news_channel_payload, app=mock_app) - - assert channel_obj.id == 567 - assert channel_obj.permission_overwrites == { - 4242: channels.PermissionOverwrite.deserialize(test_permission_overwrite_payload) - } - assert channel_obj.permission_overwrites[4242]._gateway_consumer is mock_app - assert channel_obj.guild_id == 123 - assert channel_obj.position == 0 - assert channel_obj.name == "Important Announcements" - assert channel_obj.topic == "Super Important Announcements" - assert channel_obj.is_nsfw is True - assert channel_obj.parent_id == 654 - assert channel_obj.type == channels.ChannelType.GUILD_NEWS - assert channel_obj.last_message_id == 456 - - -class TestGuildStoreChannel: - def test_deserialize(self, test_guild_store_channel_payload, test_permission_overwrite_payload, mock_app): - channel_obj = channels.GuildStoreChannel.deserialize(test_guild_store_channel_payload, app=mock_app) - - assert channel_obj.id == 123 - assert channel_obj.permission_overwrites == { - 4242: channels.PermissionOverwrite.deserialize(test_permission_overwrite_payload) - } - assert channel_obj.permission_overwrites[4242]._gateway_consumer is mock_app - assert channel_obj.guild_id == 1234 - assert channel_obj.position == 2 - assert channel_obj.name == "Half Life 3" - assert channel_obj.is_nsfw is True - assert channel_obj.parent_id == 9876 - assert channel_obj.type == channels.ChannelType.GUILD_STORE - - -class TestGuildVoiceChannell: - def test_deserialize(self, test_guild_voice_channel_payload, test_permission_overwrite_payload, mock_app): - channel_obj = channels.GuildVoiceChannel.deserialize(test_guild_voice_channel_payload, app=mock_app) - - assert channel_obj.id == 123 - assert channel_obj.permission_overwrites == { - 4242: channels.PermissionOverwrite.deserialize(test_permission_overwrite_payload) - } - assert channel_obj.permission_overwrites[4242]._gateway_consumer is mock_app - assert channel_obj.guild_id == 789 - assert channel_obj.position == 4 - assert channel_obj.name == "Secret Developer Discussions" - assert channel_obj.is_nsfw is True - assert channel_obj.parent_id == 456 - assert channel_obj.type == channels.ChannelType.GUILD_VOICE - assert channel_obj.bitrate == 64000 - assert channel_obj.user_limit == 3 - - -class TestGuildChannelBuilder: - def test___init__(self): - channel_builder_obj = channels.GuildChannelBuilder( - channel_name="A channel", channel_type=channels.ChannelType.GUILD_TEXT - ) - assert channel_builder_obj._payload == {"type": 0, "name": "A channel"} - - def test_is_sfw(self): - channel_builder_obj = channels.GuildChannelBuilder("A channel", channels.ChannelType.GUILD_TEXT).is_nsfw() - assert channel_builder_obj._payload == {"type": 0, "name": "A channel", "nsfw": True} - - def test_with_permission_overwrites(self): - channel_builder_obj = channels.GuildChannelBuilder( - "A channel", channels.ChannelType.GUILD_TEXT - ).with_permission_overwrites( - [channels.PermissionOverwrite(id=1231, type=channels.PermissionOverwriteType.MEMBER)] - ) - assert channel_builder_obj._payload == { - "type": 0, - "name": "A channel", - "permission_overwrites": [{"type": "member", "id": "1231", "allow": 0, "deny": 0}], - } - - def test_with_topic(self): - channel_builder_obj = channels.GuildChannelBuilder("A channel", channels.ChannelType.GUILD_TEXT).with_topic( - "A TOPIC" - ) - assert channel_builder_obj._payload == {"type": 0, "name": "A channel", "topic": "A TOPIC"} - - def test_with_bitrate(self): - channel_builder_obj = channels.GuildChannelBuilder("A channel", channels.ChannelType.GUILD_TEXT).with_bitrate( - 123123 - ) - assert channel_builder_obj._payload == {"type": 0, "name": "A channel", "bitrate": 123123} - - def test_with_user_limit(self): - channel_builder_obj = channels.GuildChannelBuilder( - "A channel", channels.ChannelType.GUILD_TEXT - ).with_user_limit(123123) - assert channel_builder_obj._payload == {"type": 0, "name": "A channel", "user_limit": 123123} - - @pytest.mark.parametrize("rate_limit", [3232, datetime.timedelta(seconds=3232)]) - def test_with_rate_limit_per_user(self, rate_limit): - channel_builder_obj = channels.GuildChannelBuilder( - "A channel", channels.ChannelType.GUILD_TEXT - ).with_rate_limit_per_user(rate_limit) - assert channel_builder_obj._payload == {"type": 0, "name": "A channel", "rate_limit_per_user": 3232} - - @pytest.mark.parametrize( - "category", [54321, bases.Snowflake(54321)], - ) - def test_with_parent_category(self, category): - channel_builder_obj = channels.GuildChannelBuilder( - "A channel", channels.ChannelType.GUILD_TEXT - ).with_parent_category(category) - assert channel_builder_obj._payload == {"type": 0, "name": "A channel", "parent_id": "54321"} - - @pytest.mark.parametrize("placeholder_id", [444444, bases.Snowflake(444444)]) - def test_with_id(self, placeholder_id): - channel_builder_obj = channels.GuildChannelBuilder("A channel", channels.ChannelType.GUILD_TEXT).with_id( - placeholder_id - ) - assert channel_builder_obj._payload == {"type": 0, "name": "A channel", "id": "444444"} - - def test_serialize(self): - mock_payload = {"id": "424242", "name": "aChannel", "type": 4, "nsfw": True} - channel_builder_obj = channels.GuildChannelBuilder("A channel", channels.ChannelType.GUILD_TEXT) - channel_builder_obj._payload = mock_payload - assert channel_builder_obj.serialize() == mock_payload - - -def test_deserialize_channel_returns_correct_type( - test_dm_channel_payload, - test_group_dm_channel_payload, - test_guild_category_payload, - test_guild_text_channel_payload, - test_guild_news_channel_payload, - test_guild_store_channel_payload, - test_guild_voice_channel_payload, -): - assert isinstance(channels.deserialize_channel(test_dm_channel_payload), channels.DMChannel) - assert isinstance(channels.deserialize_channel(test_group_dm_channel_payload), channels.GroupDMChannel) - assert isinstance(channels.deserialize_channel(test_guild_category_payload), channels.GuildCategory) - assert isinstance(channels.deserialize_channel(test_guild_text_channel_payload), channels.GuildTextChannel) - assert isinstance(channels.deserialize_channel(test_guild_news_channel_payload), channels.GuildNewsChannel) - assert isinstance(channels.deserialize_channel(test_guild_store_channel_payload), channels.GuildStoreChannel) - assert isinstance(channels.deserialize_channel(test_guild_voice_channel_payload), channels.GuildVoiceChannel) - - -def test_deserialize_channel_type_passes_kwargs(test_dm_channel_payload, mock_app): - channel_obj = channels.deserialize_channel(test_dm_channel_payload, app=mock_app) - assert channel_obj._app is mock_app diff --git a/tests/hikari/models/test_colors.py b/tests/hikari/models/test_colors.py deleted file mode 100644 index 591c1c9666..0000000000 --- a/tests/hikari/models/test_colors.py +++ /dev/null @@ -1,213 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# Copyright © Nekoka.tt 2019-2020 -# -# This file is part of Hikari. -# -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . -import math - -import pytest - -from hikari.models import colors -from tests.hikari import _helpers - - -@pytest.mark.model -class TestColor: - @pytest.mark.parametrize("i", [0, 0x1, 0x11, 0x111, 0x1111, 0x11111, 0xFFFFFF]) - def test_Color_validates_constructor_and_passes_for_valid_values(self, i): - assert colors.Color(i) is not None - - @pytest.mark.parametrize("i", [-1, 0x1000000]) - def test_Color_validates_constructor_and_fails_for_out_of_range_values(self, i): - try: - colors.Color(i) - assert False, "Failed to fail validation for bad values" - except ValueError: - pass - - @pytest.mark.parametrize("i", [0, 0x1, 0x11, 0x111, 0x1111, 0x11111, 0xFFFFFF]) - def test_Color_from_int_passes_for_valid_values(self, i): - assert colors.Color.from_int(i) is not None - - @pytest.mark.parametrize("i", [-1, 0x1000000]) - def test_Color_from_int_fails_for_out_of_range_values(self, i): - try: - colors.Color.from_int(i) - assert False, "Failed to fail validation for bad values" - except ValueError: - pass - - def test_equality_with_int(self): - assert colors.Color(0xFA) == 0xFA - - def test_cast_to_int(self): - assert int(colors.Color(0xFA)) == 0xFA - - @pytest.mark.parametrize( - ["i", "string"], [(0x1A2B3C, "Color(r=0x1a, g=0x2b, b=0x3c)"), (0x1A2, "Color(r=0x0, g=0x1, b=0xa2)")] - ) - def test_Color_repr_operator(self, i, string): - assert repr(colors.Color(i)) == string - - @pytest.mark.parametrize(["i", "string"], [(0x1A2B3C, "#1A2B3C"), (0x1A2, "#0001A2")]) - def test_Color_str_operator(self, i, string): - assert str(colors.Color(i)) == string - - @pytest.mark.parametrize(["i", "string"], [(0x1A2B3C, "#1A2B3C"), (0x1A2, "#0001A2")]) - def test_Color_hex_code(self, i, string): - assert colors.Color(i).hex_code == string - - @pytest.mark.parametrize(["i", "string"], [(0x1A2B3C, "1A2B3C"), (0x1A2, "0001A2")]) - def test_Color_raw_hex_code(self, i, string): - assert colors.Color(i).raw_hex_code == string - - @pytest.mark.parametrize( - ["i", "expected_outcome"], [(0x1A2B3C, False), (0x1AAA2B, False), (0x0, True), (0x11AA33, True)] - ) - def test_Color_is_web_safe(self, i, expected_outcome): - assert colors.Color(i).is_web_safe is expected_outcome - - @pytest.mark.parametrize(["r", "g", "b", "expected"], [(0x9, 0x18, 0x27, 0x91827), (0x55, 0x1A, 0xFF, 0x551AFF)]) - def test_Color_from_rgb(self, r, g, b, expected): - assert colors.Color.from_rgb(r, g, b) == expected - - @_helpers.assert_raises(type_=ValueError) - def test_color_from_rgb_raises_value_error_on_invalid_red(self): - colors.Color.from_rgb(0x999, 32, 32) - - @_helpers.assert_raises(type_=ValueError) - def test_color_from_rgb_raises_value_error_on_invalid_green(self): - colors.Color.from_rgb(32, 0x999, 32) - - @_helpers.assert_raises(type_=ValueError) - def test_color_from_rgb_raises_value_error_on_invalid_blue(self): - colors.Color.from_rgb(32, 32, 0x999) - - @pytest.mark.parametrize( - ["r", "g", "b", "expected"], - [(0x09 / 0xFF, 0x18 / 0xFF, 0x27 / 0xFF, 0x91827), (0x55 / 0xFF, 0x1A / 0xFF, 0xFF / 0xFF, 0x551AFF)], - ) - def test_Color_from_rgb_float(self, r, g, b, expected): - assert math.isclose(colors.Color.from_rgb_float(r, g, b), expected, abs_tol=1) - - @_helpers.assert_raises(type_=ValueError) - def test_color_from_rgb_float_raises_value_error_on_invalid_red(self): - colors.Color.from_rgb_float(1.5, 0.5, 0.5) - - @_helpers.assert_raises(type_=ValueError) - def test_color_from_rgb_float_raises_value_error_on_invalid_green(self): - colors.Color.from_rgb_float(0.5, 1.5, 0.5) - - @_helpers.assert_raises(type_=ValueError) - def test_color_from_rgb_float_raises_value_error_on_invalid_blue(self): - colors.Color.from_rgb_float(0.5, 0.5, 1.5) - - @pytest.mark.parametrize(["input", "r", "g", "b"], [(0x91827, 0x9, 0x18, 0x27), (0x551AFF, 0x55, 0x1A, 0xFF)]) - def test_Color_rgb(self, input, r, g, b): - assert colors.Color(input).rgb == (r, g, b) - - @pytest.mark.parametrize( - ["input", "r", "g", "b"], - [(0x91827, 0x09 / 0xFF, 0x18 / 0xFF, 0x27 / 0xFF), (0x551AFF, 0x55 / 0xFF, 0x1A / 0xFF, 0xFF / 0xFF)], - ) - def test_Color_rgb_float(self, input, r, g, b): - assert colors.Color(input).rgb_float == (r, g, b) - - @pytest.mark.parametrize("prefix", ["0x", "0X", "#", ""]) - @pytest.mark.parametrize( - ["expected", "string"], [(0x1A2B3C, "1A2B3C"), (0x1A2, "0001A2"), (0xAABBCC, "ABC"), (0x00AA00, "0A0")] - ) - def test_Color_from_hex_code(self, prefix, string, expected): - actual_string = prefix + string - assert colors.Color.from_hex_code(actual_string) == expected - - def test_Color_from_hex_code_ValueError_when_not_hex(self): - try: - colors.Color.from_hex_code("0xlmfao") - assert False, "No failure" - except ValueError: - pass - - def test_Color_from_hex_code_ValueError_when_not_6_or_3_in_size(self): - try: - colors.Color.from_hex_code("0x1111") - assert False, "No failure" - except ValueError: - pass - - def test_Color_from_bytes(self): - assert colors.Color(0xFFAAFF) == colors.Color.from_bytes(b"\xff\xaa\xff\x00\x00\x00\x00\x00\x00\x00", "little") - - def test_Color_to_bytes(self): - c = colors.Color(0xFFAAFF) - b = c.to_bytes(10, "little") - assert b == b"\xff\xaa\xff\x00\x00\x00\x00\x00\x00\x00" - - @pytest.mark.parametrize( - ["input", "expected_result"], - [ - (0xFF051A, colors.Color(0xFF051A)), - (16712986, colors.Color(0xFF051A)), - ((255, 5, 26), colors.Color(0xFF051A)), - ((1, 0.5, 0), colors.Color(0xFF7F00)), - ([0xFF, 0x5, 0x1A], colors.Color(0xFF051A)), - ("#1a2b3c", colors.Color(0x1A2B3C)), - ("#123", colors.Color(0x112233)), - ("0x1a2b3c", colors.Color(0x1A2B3C)), - ("0x123", colors.Color(0x112233)), - ("0X1a2b3c", colors.Color(0x1A2B3C)), - ("0X123", colors.Color(0x112233)), - ("1a2b3c", colors.Color(0x1A2B3C)), - ("123", colors.Color(0x112233)), - ((1.0, 0.0196078431372549, 0.10196078431372549), colors.Color(0xFF051A)), - ([1.0, 0.0196078431372549, 0.10196078431372549], colors.Color(0xFF051A)), - ], - ) - def test_Color_of_happy_path(self, input, expected_result): - result = colors.Color.of(input) - assert result == expected_result, f"{input}" - result.__repr__() - - @pytest.mark.parametrize( - "input", - [ - "blah", - "0xfff1", - lambda: 22, - NotImplementedError, - NotImplemented, - (1, 1, 1, 1), - (1, "a", 1), - (1, 1.1, 1), - (), - {}, - [], - {1, 1, 1}, - set(), - b"1ff1ff", - ], - ) - @_helpers.assert_raises(type_=ValueError) - def test_Color_of_sad_path(self, input): - colors.Color.of(input) - - def test_Color_of_with_multiple_args(self): - result = colors.Color.of(0xFF, 0x5, 0x1A) - assert result == colors.Color(0xFF051A) - - @pytest.mark.model - def test_Color___repr__(self): - assert repr(colors.Color.of("#1a2b3c")) == "Color(r=0x1a, g=0x2b, b=0x3c)" diff --git a/tests/hikari/models/test_embeds.py b/tests/hikari/models/test_embeds.py deleted file mode 100644 index 045682d798..0000000000 --- a/tests/hikari/models/test_embeds.py +++ /dev/null @@ -1,538 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# Copyright © Nekoka.tt 2019-2020 -# -# This file is part of Hikari. -# -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . -import datetime - -import mock -import pytest - -from hikari import application -from hikari.internal import conversions -from hikari.models import colors -from hikari.models import embeds -from hikari.models import files -from tests.hikari import _helpers - - -@pytest.fixture() -def mock_app(): - return mock.MagicMock(application.Application) - - -@pytest.fixture -def test_footer_payload(): - return { - "text": "footer text", - "icon_url": "https://somewhere.com/footer.png", - "proxy_icon_url": "https://media.somewhere.com/footer.png", - } - - -@pytest.fixture -def test_image_payload(): - return { - "url": "https://somewhere.com/image.png", - "_proxy_url": "https://media.somewhere.com/image.png", - "height": 122, - "width": 133, - } - - -@pytest.fixture -def test_thumbnail_payload(): - return { - "url": "https://somewhere.com/thumbnail.png", - "_proxy_url": "https://media.somewhere.com/thumbnail.png", - "height": 123, - "width": 456, - } - - -@pytest.fixture -def test_video_payload(): - return { - "url": "https://somewhere.com/video.mp4", - "height": 1234, - "width": 4567, - } - - -@pytest.fixture -def test_provider_payload(): - return {"name": "some name", "url": "https://somewhere.com/provider"} - - -@pytest.fixture -def test_author_payload(): - return { - "name": "some name", - "url": "https://somewhere.com/author", - "icon_url": "https://somewhere.com/author.png", - "proxy_icon_url": "https://media.somewhere.com/author.png", - } - - -@pytest.fixture -def test_field_payload(): - return {"name": "title", "value": "some value", "inline": True} - - -@pytest.fixture -def test_embed_payload( - test_footer_payload, - test_image_payload, - test_thumbnail_payload, - test_video_payload, - test_provider_payload, - test_author_payload, - test_field_payload, -): - return { - "title": "embed title", - "description": "embed description", - "url": "https://somewhere.com", - "timestamp": "2020-03-22T16:40:39.218000+00:00", - "color": 14014915, - "footer": test_footer_payload, - "image": test_image_payload, - "thumbnail": test_thumbnail_payload, - "video": test_video_payload, - "provider": test_provider_payload, - "image": test_image_payload, - "author": test_author_payload, - "fields": [test_field_payload], - } - - -class TestEmbedFooter: - def test_deserialize(self, test_footer_payload, mock_app): - footer_obj = embeds.EmbedFooter.deserialize(test_footer_payload, app=mock_app) - - assert footer_obj.text == "footer text" - assert footer_obj.icon_url == "https://somewhere.com/footer.png" - assert footer_obj.proxy_icon_url == "https://media.somewhere.com/footer.png" - - def test_serialize_full_footer(self): - footer_obj = embeds.EmbedFooter(text="OK", icon_url="https:////////////",) - - assert footer_obj.serialize() == {"text": "OK", "icon_url": "https:////////////"} - - def test_serialize_partial_footer(self): - footer_obj = embeds.EmbedFooter(text="OK",) - - assert footer_obj.serialize() == {"text": "OK"} - - -class TestEmbedImage: - def test_deserialize(self, test_image_payload, mock_app): - image_obj = embeds.EmbedImage.deserialize(test_image_payload, app=mock_app) - - assert image_obj.url == "https://somewhere.com/image.png" - assert image_obj._proxy_url == "https://media.somewhere.com/image.png" - assert image_obj.height == 122 - assert image_obj.width == 133 - - def test_serialize_full_image(self): - image_obj = embeds.EmbedImage(url="https://///////",) - - assert image_obj.serialize() == {"url": "https://///////"} - - def test_serialize_empty_image(self): - assert embeds.EmbedImage().serialize() == {} - - -class TestEmbedThumbnail: - def test_deserialize(self, test_thumbnail_payload, mock_app): - thumbnail_obj = embeds.EmbedThumbnail.deserialize(test_thumbnail_payload, app=mock_app) - - assert thumbnail_obj.url == "https://somewhere.com/thumbnail.png" - assert thumbnail_obj._proxy_url == "https://media.somewhere.com/thumbnail.png" - assert thumbnail_obj.height == 123 - assert thumbnail_obj.width == 456 - - def test_serialize_full_thumbnail(self): - thumbnail_obj = embeds.EmbedThumbnail(url="https://somewhere.com/thumbnail.png") - - assert thumbnail_obj.serialize() == {"url": "https://somewhere.com/thumbnail.png"} - - def test_serialize_empty_thumbnail(self): - assert embeds.EmbedThumbnail().serialize() == {} - - -class TestEmbedVideo: - def test_deserialize(self, test_video_payload, mock_app): - video_obj = embeds.EmbedVideo.deserialize(test_video_payload, app=mock_app) - - assert video_obj.url == "https://somewhere.com/video.mp4" - assert video_obj.height == 1234 - assert video_obj.width == 4567 - - -class TestEmbedProvider: - def test_deserialize(self, test_provider_payload, mock_app): - provider_obj = embeds.EmbedProvider.deserialize(test_provider_payload, app=mock_app) - - assert provider_obj.name == "some name" - assert provider_obj.url == "https://somewhere.com/provider" - - -class TestEmbedAuthor: - def test_deserialize(self, test_author_payload, mock_app): - author_obj = embeds.EmbedAuthor.deserialize(test_author_payload, app=mock_app) - - assert author_obj.name == "some name" - assert author_obj.url == "https://somewhere.com/author" - assert author_obj.icon_url == "https://somewhere.com/author.png" - assert author_obj.proxy_icon_url == "https://media.somewhere.com/author.png" - - def test_serialize_full_author(self): - author_obj = embeds.EmbedAuthor( - name="Author 187", url="https://nyaanyaanyaa", icon_url="https://a-proper-domain" - ) - - assert author_obj.serialize() == { - "name": "Author 187", - "url": "https://nyaanyaanyaa", - "icon_url": "https://a-proper-domain", - } - - def test_serialize_empty_author(self): - assert embeds.EmbedAuthor().serialize() == {} - - -class TestEmbedField: - def test_deserialize(self, mock_app): - field_obj = embeds.EmbedField.deserialize({"name": "title", "value": "some value"}, app=mock_app) - - assert field_obj.name == "title" - assert field_obj.value == "some value" - assert field_obj.is_inline is False - - def test_serialize(self, test_field_payload): - field_obj = embeds.EmbedField(name="NAME", value="nyaa nyaa nyaa", is_inline=True) - - assert field_obj.serialize() == {"name": "NAME", "value": "nyaa nyaa nyaa", "inline": True} - - -class TestEmbed: - @_helpers.assert_raises(type_=ValueError) - def test_embed___init___raises_value_error_on_invalid_title(self): - embeds.Embed(title="x" * 257) - - @_helpers.assert_raises(type_=ValueError) - def test_embed___init___raises_value_error_on_invalid_description(self): - embeds.Embed(description="x" * 2049) - - def test_deserialize( - self, - test_embed_payload, - test_footer_payload, - test_image_payload, - test_thumbnail_payload, - test_video_payload, - test_provider_payload, - test_author_payload, - test_field_payload, - mock_app, - ): - mock_datetime = mock.MagicMock(datetime.datetime) - - with _helpers.patch_marshal_attr( - embeds.Embed, "timestamp", deserializer=conversions.parse_iso_8601_ts, return_value=mock_datetime, - ) as patched_timestamp_deserializer: - embed_obj = embeds.Embed.deserialize(test_embed_payload, app=mock_app) - patched_timestamp_deserializer.assert_called_once_with("2020-03-22T16:40:39.218000+00:00") - - assert embed_obj.title == "embed title" - assert embed_obj.description == "embed description" - assert embed_obj.url == "https://somewhere.com" - assert embed_obj.timestamp == mock_datetime - assert embed_obj.color == colors.Color(14014915) - assert embed_obj.footer == embeds.EmbedFooter.deserialize(test_footer_payload) - assert embed_obj.footer._gateway_consumer is mock_app - assert embed_obj.image == embeds.EmbedImage.deserialize(test_image_payload) - assert embed_obj.image._gateway_consumer is mock_app - assert embed_obj.thumbnail == embeds.EmbedThumbnail.deserialize(test_thumbnail_payload) - assert embed_obj.thumbnail._gateway_consumer is mock_app - assert embed_obj.video == embeds.EmbedVideo.deserialize(test_video_payload) - assert embed_obj.video._gateway_consumer is mock_app - assert embed_obj.provider == embeds.EmbedProvider.deserialize(test_provider_payload) - assert embed_obj.provider._gateway_consumer is mock_app - assert embed_obj.author == embeds.EmbedAuthor.deserialize(test_author_payload) - assert embed_obj.author._gateway_consumer is mock_app - assert embed_obj.fields == [embeds.EmbedField.deserialize(test_field_payload)] - assert embed_obj.fields[0]._gateway_consumer is mock_app - - def test_serialize_full_embed(self): - embed_obj = embeds.Embed( - title="Nyaa me pls >////<", - description="Nyan >////<", - url="https://a-url-now", - timestamp=datetime.datetime.fromisoformat("2020-03-22T16:40:39.218000+00:00"), - color=colors.Color(123123), - footer=embeds.EmbedFooter(text="HI"), - image=embeds.EmbedImage(url="https://not-a-url"), - thumbnail=embeds.EmbedThumbnail(url="https://url-a-not"), - author=embeds.EmbedAuthor(name="a name", url="https://a-man"), - fields=[embeds.EmbedField(name="aField", value="agent69", is_inline=True)], - ) - - with mock.patch.object(embeds.Embed, "_check_total_length") as mock_check: - assert embed_obj.serialize() == { - "title": "Nyaa me pls >////<", - "description": "Nyan >////<", - "url": "https://a-url-now", - "timestamp": "2020-03-22T16:40:39.218000+00:00", - "color": 123123, - "footer": {"text": "HI"}, - "image": {"url": "https://not-a-url"}, - "thumbnail": {"url": "https://url-a-not"}, - "author": {"name": "a name", "url": "https://a-man"}, - "fields": [{"name": "aField", "value": "agent69", "inline": True}], - } - mock_check.assert_called_once() - - def test_serialize_empty_embed(self): - assert embeds.Embed().serialize() == {"fields": []} - - def test_assets_to_upload(self): - em = embeds.Embed() - em._assets_to_upload = ["asset_1", "asset_2"] - assert em.assets_to_upload == ["asset_1", "asset_2"] - - @pytest.mark.parametrize( - ["input", "expected_output"], - [ - ("https://some.url/to/somewhere.png", ("https://some.url/to/somewhere.png", None)), - (files.FileStream("test.png"), ["attachment://test.png", "the inputed file"]), - (None, (None, None)), - ], - ) - def test__extract_url(self, input, expected_output): - if isinstance(input, files.BaseStream): - expected_output[1] = input - expected_output = tuple(expected_output) - em = embeds.Embed() - assert em._extract_url(input) == expected_output - - def test__maybe_ref_file_obj(self): - mock_file_obj = mock.MagicMock(files.BaseStream) - em = embeds.Embed() - em._maybe_ref_file_obj(mock_file_obj) - assert em.assets_to_upload == [mock_file_obj] - - def test__maybe_ref_file_obj_when_None(self): - em = embeds.Embed() - em._maybe_ref_file_obj(None) - assert em.assets_to_upload == [] - - def test_set_footer_without_optionals(self): - em = embeds.Embed() - assert em.set_footer(text="test") == em - assert em.footer.text == "test" - assert em.footer.icon_url is None - assert em._assets_to_upload == [] - - def test_set_footer_with_optionals_with_image_as_file(self): - mock_file_obj = mock.MagicMock(files.BaseStream) - mock_file_obj.filename = "test.png" - em = embeds.Embed() - assert em.set_footer(text="test", icon=mock_file_obj) == em - assert em.footer.text == "test" - assert em.footer.icon_url == "attachment://test.png" - assert em._assets_to_upload == [mock_file_obj] - - def test_set_image_with_optionals_with_image_as_string(self): - em = embeds.Embed() - assert em.set_footer(text="test", icon="https://somewhere.url/image.png") == em - assert em.footer.text == "test" - assert em.footer.icon_url == "https://somewhere.url/image.png" - assert em._assets_to_upload == [] - - @_helpers.assert_raises(type_=ValueError) - @pytest.mark.parametrize("text", [" ", "", "x" * 2100]) - def test_set_footer_raises_value_error_on_invalid_text(self, text): - embeds.Embed().set_footer(text=text) - - def test_set_image_without_optionals(self): - em = embeds.Embed() - assert em.set_image() == em - assert em.image.url is None - assert em._assets_to_upload == [] - - def test_set_image_with_optionals_with_image_as_file(self): - mock_file_obj = mock.MagicMock(files.BaseStream) - mock_file_obj.filename = "test.png" - em = embeds.Embed() - assert em.set_image(mock_file_obj) == em - assert em.image.url == "attachment://test.png" - assert em._assets_to_upload == [mock_file_obj] - - def test_set_image_with_optionals_with_image_as_string(self): - em = embeds.Embed() - assert em.set_image("https://somewhere.url/image.png") == em - assert em.image.url == "https://somewhere.url/image.png" - assert em._assets_to_upload == [] - - def test_set_thumbnail_without_optionals(self): - em = embeds.Embed() - assert em.set_thumbnail() == em - assert em.thumbnail.url is None - assert em._assets_to_upload == [] - - def test_set_thumbnail_with_optionals_with_image_as_file(self): - mock_file_obj = mock.MagicMock(files.BaseStream) - mock_file_obj.filename = "test.png" - em = embeds.Embed() - assert em.set_thumbnail(mock_file_obj) == em - assert em.thumbnail.url == "attachment://test.png" - assert em._assets_to_upload == [mock_file_obj] - - def test_set_thumbnail_with_optionals_with_image_as_string(self): - em = embeds.Embed() - assert em.set_thumbnail("https://somewhere.url/image.png") == em - assert em.thumbnail.url == "https://somewhere.url/image.png" - assert em._assets_to_upload == [] - - def test_set_author_without_optionals(self): - em = embeds.Embed() - assert em.set_author() == em - assert em.author.name is None - assert em.author.url is None - assert em.author.icon_url is None - assert em._assets_to_upload == [] - - def test_set_author_with_optionals_with_icon_as_file(self): - mock_file_obj = mock.MagicMock(files.BaseStream) - mock_file_obj.filename = "test.png" - em = embeds.Embed() - assert em.set_author(name="hikari", url="nekokatt.gitlab.io/hikari", icon=mock_file_obj) == em - assert em.author.name == "hikari" - assert em.author.url == "nekokatt.gitlab.io/hikari" - assert em.author.icon_url == "attachment://test.png" - assert em._assets_to_upload == [mock_file_obj] - - def test_set_author_with_optionals_with_icon_as_string(self): - em = embeds.Embed() - assert ( - em.set_author(name="hikari", url="nekokatt.gitlab.io/hikari", icon="https://somewhere.url/image.png") == em - ) - assert em.author.name == "hikari" - assert em.author.url == "nekokatt.gitlab.io/hikari" - assert em.author.icon_url == "https://somewhere.url/image.png" - assert em._assets_to_upload == [] - - @_helpers.assert_raises(type_=ValueError) - @pytest.mark.parametrize("name", ["", " ", "x" * 257]) - def test_set_author_raises_value_error_on_invalid_name(self, name): - embeds.Embed().set_author(name=name) - - def test_add_field_without_optionals(self): - em = embeds.Embed() - assert em.add_field(name="test_name", value="test_value") == em - assert len(em.fields) == 1 - assert em.fields[0].name == "test_name" - assert em.fields[0].value == "test_value" - assert em.fields[0].is_inline is False - - def test_add_field_with_optionals(self): - field_obj1 = embeds.EmbedField(name="nothing to see here", value="still nothing") - field_obj2 = embeds.EmbedField(name="test_name", value="test_value", is_inline=True) - em = embeds.Embed() - em.fields = [field_obj1] - with mock.patch.object(embeds, "EmbedField", return_value=field_obj2) as mock_embed_field: - assert em.add_field(name="test_name", value="test_value", inline=True, index=0) == em - mock_embed_field.assert_called_once_with(name="test_name", value="test_value", is_inline=True) - assert em.fields == [field_obj2, field_obj1] - assert em.fields[0].name == "test_name" - assert em.fields[0].value == "test_value" - assert em.fields[0].is_inline is True - - @_helpers.assert_raises(type_=ValueError) - def test_add_field_raises_value_error_on_too_many_fields(self): - fields = [mock.MagicMock(embeds.EmbedField) for _ in range(25)] - embeds.Embed(fields=fields).add_field(name="test", value="blam") - - @_helpers.assert_raises(type_=ValueError) - @pytest.mark.parametrize("name", ["", " ", "x" * 257]) - def test_add_field_raises_value_error_on_invalid_name(self, name): - fields = [mock.MagicMock(embeds.EmbedField)] - embeds.Embed(fields=fields).add_field(name=name, value="blam") - - @_helpers.assert_raises(type_=ValueError) - @pytest.mark.parametrize("value", ["", " ", "x" * 2049]) - def test_add_field_raises_value_error_on_invalid_value(self, value): - fields = [mock.MagicMock(embeds.EmbedField)] - embeds.Embed(fields=fields).add_field(name="test", value=value) - - def test_edit_field_without_optionals(self): - field_obj = embeds.EmbedField(name="nothing to see here", value="still nothing") - em = embeds.Embed() - em.fields = [field_obj] - assert em.edit_field(0) == em - assert em.fields == [field_obj] - assert em.fields[0].name == "nothing to see here" - assert em.fields[0].value == "still nothing" - assert em.fields[0].is_inline is False - - def test_edit_field_with_optionals(self): - field_obj = embeds.EmbedField(name="nothing to see here", value="still nothing") - em = embeds.Embed() - em.fields = [field_obj] - assert em.edit_field(0, name="test_name", value="test_value", inline=True) == em - assert em.fields == [field_obj] - assert em.fields[0].name == "test_name" - assert em.fields[0].value == "test_value" - assert em.fields[0].is_inline is True - - @_helpers.assert_raises(type_=ValueError) - @pytest.mark.parametrize("name", ["", " ", "x" * 257]) - def test_edit_field_raises_value_error_on_invalid_name(self, name): - fields = [mock.MagicMock(embeds.EmbedField)] - embeds.Embed(fields=fields).edit_field(0, name=name, value="blam") - - @_helpers.assert_raises(type_=ValueError) - @pytest.mark.parametrize("value", ["", " ", "x" * 2049]) - def test_edit_field_raises_value_error_on_invalid_value(self, value): - fields = [mock.MagicMock(embeds.EmbedField)] - embeds.Embed(fields=fields).edit_field(0, name="test", value=value) - - def test_remove_field(self): - mock_field1 = mock.MagicMock(embeds.EmbedField) - mock_field2 = mock.MagicMock(embeds.EmbedField) - em = embeds.Embed() - em.fields = [mock_field1, mock_field2] - - assert em.remove_field(0) == em - assert em.fields == [mock_field2] - - @pytest.mark.parametrize(["input", "expected_output"], [(None, 0), ("this is 21 characters", 21)]) - def test__safe_len(self, input, expected_output): - em = embeds.Embed() - assert em._safe_len(input) == expected_output - - @_helpers.assert_raises(type_=ValueError) - def test__check_total(self): - em = embeds.Embed() - em.title = "a" * 1000 - em.description = "b" * 1000 - em.author = embeds.EmbedAuthor(name="c" * 1000) - em.footer = embeds.EmbedFooter(text="d" * 1000) - em.fields.append(embeds.EmbedField(name="e" * 1000, value="f" * 1001)) - - em._check_total_length() diff --git a/tests/hikari/models/test_emojis.py b/tests/hikari/models/test_emojis.py deleted file mode 100644 index 0e31d0f55f..0000000000 --- a/tests/hikari/models/test_emojis.py +++ /dev/null @@ -1,254 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# Copyright © Nekoka.tt 2019-2020 -# -# This file is part of Hikari. -# -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . -import mock -import pytest - -from hikari import application -from hikari.net import urls -from hikari.models import bases -from hikari.models import emojis -from hikari.models import files -from hikari.models import users -from tests.hikari import _helpers - - -@pytest.fixture() -def mock_app(): - return mock.MagicMock(application.Application) - - -class TestEmoji: - @pytest.fixture - def test_emoji(self): - class Impl(emojis.Emoji): - @property - def url(self): - return "http://example.com/test" - - @property - def url_name(self): - return "test:1234" - - @property - def mention(self): - return "<:test:1234>" - - @property - def filename(self) -> str: - return "test.png" - - return Impl() - - def test_is_mentionable(self, test_emoji): - assert test_emoji.is_mentionable - - def test_aiter(self, test_emoji): - aiter = mock.MagicMock() - stream = mock.MagicMock(__aiter__=mock.MagicMock(return_value=aiter)) - with mock.patch.object(files, "WebResourceStream", return_value=stream) as new: - assert test_emoji.__aiter__() is aiter - - new.assert_called_with("test.png", "http://example.com/test") - - -class TestUnicodeEmoji: - england = [0x1F3F4, 0xE0067, 0xE0062, 0xE0065, 0xE006E, 0xE0067, 0xE007F] - - def test_deserialize(self, mock_app): - emoji_obj = emojis.UnicodeEmoji.deserialize({"name": "🤷"}, app=mock_app) - - assert emoji_obj.name == "🤷" - - def test_url_name(self): - assert emojis.UnicodeEmoji(name="🤷").url_name == "🤷" - - def test_mention(self): - assert emojis.UnicodeEmoji(name="🤷").mention == "🤷" - - def test_codepoints(self): - # :england: - codepoints = self.england.copy() - e = emojis.UnicodeEmoji(name="".join(map(chr, codepoints))) - assert e.codepoints == codepoints - - def test_from_codepoints(self): - # :england: - codepoints = self.england.copy() - e = emojis.UnicodeEmoji.from_codepoints(*codepoints) - assert e.codepoints == codepoints - - def test_from_emoji(self): - string = "\N{WHITE SMILING FACE}\N{VARIATION SELECTOR-16}" - assert emojis.UnicodeEmoji.from_emoji(string).codepoints == [0x263A, 0xFE0F] - - @pytest.mark.parametrize( - ["codepoints", "filename"], - [ - (england.copy(), "1f3f4-e0067-e0062-e0065-e006e-e0067-e007f.png"), # england - ([0x1F38C], "1f38c.png"), # crossed_flag - ([0x263A, 0xFE0F], "263a.png"), # relaxed - ([0x1F3F3, 0xFE0F, 0x200D, 0x1F308], "1f3f3-fe0f-200d-1f308.png"), # gay pride (outlier case) - ([0x1F3F4, 0x200D, 0x2620, 0xFE0F], "1f3f4-200d-2620-fe0f.png"), # pirate flag - ([0x1F3F3, 0xFE0F], "1f3f3.png"), # white flag - ([0x1F939, 0x1F3FE, 0x200D, 0x2642, 0xFE0F], "1f939-1f3fe-200d-2642-fe0f.png"), # man-juggling-tone-4 - ], - ) - def test_filename(self, codepoints, filename): - char = "".join(map(chr, codepoints)) - assert emojis.UnicodeEmoji(name=char).filename == filename - - @pytest.mark.parametrize( - ["codepoints", "filename"], - [ - (england.copy(), "1f3f4-e0067-e0062-e0065-e006e-e0067-e007f.png"), # england - ([0x1F38C], "1f38c.png"), # crossed_flag - ([0x263A, 0xFE0F], "263a.png"), # relaxed - ([0x1F3F3, 0xFE0F, 0x200D, 0x1F308], "1f3f3-fe0f-200d-1f308.png"), # gay pride (outlier case) - ([0x1F3F4, 0x200D, 0x2620, 0xFE0F], "1f3f4-200d-2620-fe0f.png"), # pirate flag - ([0x1F3F3, 0xFE0F], "1f3f3.png"), # white flag - ([0x1F939, 0x1F3FE, 0x200D, 0x2642, 0xFE0F], "1f939-1f3fe-200d-2642-fe0f.png"), # man-juggling-tone-4 - ], - ) - def test_url(self, codepoints, filename): - char = "".join(map(chr, codepoints)) - url = "https://github.com/twitter/twemoji/raw/master/assets/72x72/" + filename - assert emojis.UnicodeEmoji(name=char).url == url - - def test_unicode_names(self): - codepoints = [0x1F939, 0x1F3FE, 0x200D, 0x2642, 0xFE0F] - # https://unicode-table.com/en/ - names = [ - "JUGGLING", - "EMOJI MODIFIER FITZPATRICK TYPE-5", - "ZERO WIDTH JOINER", - "MALE SIGN", - "VARIATION SELECTOR-16", - ] - - char = "".join(map(chr, codepoints)) - assert emojis.UnicodeEmoji(name=char).unicode_names == names - - def test_from_unicode_escape(self): - input_string = r"\U0001f939\U0001f3fe\u200d\u2642\ufe0f" - codepoints = [0x1F939, 0x1F3FE, 0x200D, 0x2642, 0xFE0F] - assert emojis.UnicodeEmoji.from_unicode_escape(input_string).codepoints == codepoints - - def test_unicode_escape(self): - codepoints = [0x1F939, 0x1F3FE, 0x200D, 0x2642, 0xFE0F] - expected_string = r"\U0001f939\U0001f3fe\u200d\u2642\ufe0f" - assert emojis.UnicodeEmoji(name="".join(map(chr, codepoints))).unicode_escape == expected_string - - -class TestCustomEmoji: - def test_deserialize(self, mock_app): - emoji_obj = emojis.CustomEmoji.deserialize({"id": "1234", "name": "test", "animated": True}, app=mock_app) - - assert emoji_obj.id == 1234 - assert emoji_obj.name == "test" - assert emoji_obj.is_animated is True - - def test_url_name(self): - name = emojis.CustomEmoji(is_animated=True, id=bases.Snowflake("650573534627758100"), name="nyaa").url_name - assert name == "nyaa:650573534627758100" - - @pytest.mark.parametrize(["animated", "extension"], [(True, ".gif"), (False, ".png")]) - def test_filename(self, animated, extension): - emoji = emojis.CustomEmoji(is_animated=animated, id=bases.Snowflake(9876543210), name="Foo") - assert emoji.filename == f"9876543210{extension}" - - @pytest.mark.parametrize(["animated", "is_mentionable"], [(True, True), (False, True), (None, False)]) - def test_is_mentionable(self, animated, is_mentionable): - emoji = emojis.CustomEmoji(is_animated=animated, id=bases.Snowflake(123), name="Foo") - assert emoji.is_mentionable is is_mentionable - - @pytest.mark.parametrize(["animated", "format_"], [(True, "gif"), (False, "png")]) - def test_url(self, animated, format_): - emoji = emojis.CustomEmoji(is_animated=animated, id=bases.Snowflake(98765), name="Foo") - mock_result = mock.MagicMock(spec_set=str) - with mock.patch.object(urls, "generate_cdn_url", return_value=mock_result) as generate_cdn_url: - assert emoji.url is mock_result - - generate_cdn_url.assert_called_once_with("emojis", "98765", format_=format_, size=None) - - -class TestKnownCustomEmoji: - def test_deserialize(self, mock_app): - mock_user = mock.MagicMock(users.User) - - test_user_payload = {"id": "123456", "username": "hikari", "discriminator": "0000", "avatar": None} - with _helpers.patch_marshal_attr( - emojis.KnownCustomEmoji, "user", deserializer=users.User.deserialize, return_value=mock_user - ) as patched_user_deserializer: - emoji_obj = emojis.KnownCustomEmoji.deserialize( - { - "id": "12345", - "name": "testing", - "animated": False, - "available": True, - "roles": ["123", "456"], - "user": test_user_payload, - "require_colons": True, - "managed": False, - }, - app=mock_app, - ) - patched_user_deserializer.assert_called_once_with(test_user_payload, app=mock_app) - - assert emoji_obj.id == 12345 - assert emoji_obj.name == "testing" - assert emoji_obj.is_animated is False - assert emoji_obj.role_ids == {123, 456} - assert emoji_obj.user == mock_user - assert emoji_obj.is_colons_required is True - assert emoji_obj.is_managed is False - assert emoji_obj.is_available is True - - @pytest.fixture() - def mock_guild_emoji_obj(self): - return emojis.KnownCustomEmoji( - is_animated=False, - is_available=True, - id=650573534627758100, - name="nyaa", - role_ids=[], - is_colons_required=True, - is_managed=False, - user=mock.MagicMock(users.User), - ) - - def test_mention_when_animated(self, mock_guild_emoji_obj): - mock_guild_emoji_obj.is_animated = True - assert mock_guild_emoji_obj.mention == "" - - def test_mention_when_not_animated(self, mock_guild_emoji_obj): - mock_guild_emoji_obj.is_animated = False - assert mock_guild_emoji_obj.mention == "<:nyaa:650573534627758100>" - - -@pytest.mark.parametrize( - ["payload", "expected_type"], - [({"name": "🤷"}, emojis.UnicodeEmoji), ({"id": "1234", "name": "test"}, emojis.CustomEmoji)], -) -def test_deserialize_reaction_emoji_returns_expected_type(payload, expected_type): - assert isinstance(emojis.deserialize_reaction_emoji(payload), expected_type) - - -def test_deserialize_reaction_emoji_passes_kwargs(mock_app): - emoji_obj = emojis.deserialize_reaction_emoji({"id": "1234", "name": "test"}, app=mock_app) - assert emoji_obj._app is mock_app diff --git a/tests/hikari/models/test_files.py b/tests/hikari/models/test_files.py deleted file mode 100644 index 02d080af39..0000000000 --- a/tests/hikari/models/test_files.py +++ /dev/null @@ -1,416 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# Copyright © Nekoka.tt 2019-2020 -# -# This file is part of Hikari. -# -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . -import concurrent.futures -import io -import os -import random -import tempfile -import uuid - -import aiohttp -import mock -import pytest - -from hikari import errors -from hikari.models import files -from tests.hikari import _helpers - - -@pytest.mark.asyncio -class TestBaseStream: - async def test_read(self): - class Impl(files.BaseStream): - async def __aiter__(self): - yield b"foo" - yield b" " - yield b"bar" - yield b" " - yield b"baz" - yield b" " - yield b"bork" - - @property - def filename(self) -> str: - return "poof" - - i = Impl() - - assert await i.read() == b"foo bar baz bork" - - def test_repr(self): - class Impl(files.BaseStream): - @property - def filename(self) -> str: - return "poofs.gpg" - - async def __aiter__(self): - yield b"" - - i = Impl() - - assert repr(i) == "Impl(filename='poofs.gpg')" - - -@pytest.mark.asyncio -class TestByteStream: - async def test_filename(self): - stream = files.ByteStream("foo.txt", b"(.) (.)") - assert stream.filename == "foo.txt" - - @pytest.mark.parametrize( - "chunks", - [ - (b"foo", b"bar"), - (bytearray(b"foo"), bytearray(b"bar")), - (memoryview(b"foo"), memoryview(b"bar")), - ("foo", "bar"), - ("foo", b"bar"), - ], - ) - async def test_async_gen_function(self, chunks): - async def generator(): - for chunk in chunks: - yield chunk - - stream = files.ByteStream("foo.txt", generator) - - assert await stream.read() == b"foobar" - - @pytest.mark.parametrize( - "chunks", - [ - (b"foo", b"bar"), - (bytearray(b"foo"), bytearray(b"bar")), - (memoryview(b"foo"), memoryview(b"bar")), - ("foo", "bar"), - ("foo", b"bar"), - ], - ) - async def test_async_gen(self, chunks): - async def generator(): - for chunk in chunks: - yield chunk - - stream = files.ByteStream("foo.txt", generator) - - assert await stream.read() == b"foobar" - - @pytest.mark.parametrize( - "chunks", - [ - (b"foo", b"bar"), - (bytearray(b"foo"), bytearray(b"bar")), - (memoryview(b"foo"), memoryview(b"bar")), - ("foo", "bar"), - ("foo", b"bar"), - ], - ) - async def test_generator_async_iterable(self, chunks): - class AsyncIterable: - async def __aiter__(self): - for chunk in chunks: - yield chunk - - stream = files.ByteStream("foo.txt", AsyncIterable()) - - assert await stream.read() == b"foobar" - - @pytest.mark.parametrize( - "chunks", - [ - (b"foo", b"bar"), - (bytearray(b"foo"), bytearray(b"bar")), - (memoryview(b"foo"), memoryview(b"bar")), - ("foo", "bar"), - ("foo", b"bar"), - ], - ) - async def test_delegating_async_iterable(self, chunks): - async def delegated_to(): - for chunk in chunks: - yield chunk - - class AsyncIterable: - def __aiter__(self): - return delegated_to() - - stream = files.ByteStream("foo.txt", AsyncIterable()) - - assert await stream.read() == b"foobar" - - @pytest.mark.parametrize( - "chunks", - [ - (b"foo", b"bar"), - (bytearray(b"foo"), bytearray(b"bar")), - (memoryview(b"foo"), memoryview(b"bar")), - ("foo", "bar"), - ("foo", b"bar"), - ], - ) - async def test_generator_async_iterator(self, chunks): - class AsyncIterator: - def __init__(self): - self.i = 0 - - async def __anext__(self): - if self.i < len(chunks): - chunk = chunks[self.i] - self.i += 1 - return chunk - raise StopAsyncIteration() - - stream = files.ByteStream("foo.txt", AsyncIterator()) - - assert await stream.read() == b"foobar" - - async def test_BytesIO(self): - stream = files.ByteStream("foo.txt", io.BytesIO(b"foobar")) - assert await stream.read() == b"foobar" - - async def test_StringIO(self): - stream = files.ByteStream("foo.txt", io.StringIO("foobar")) - assert await stream.read() == b"foobar" - - async def test_bytes(self): - stream = files.ByteStream("foo.txt", b"foobar") - assert await stream.read() == b"foobar" - - async def test_bytearray(self): - stream = files.ByteStream("foo.txt", bytearray(b"foobar")) - assert await stream.read() == b"foobar" - - async def test_memoryview(self): - stream = files.ByteStream("foo.txt", memoryview(b"foobar")) - assert await stream.read() == b"foobar" - - async def test_str(self): - stream = files.ByteStream("foo.txt", "foobar") - assert await stream.read() == b"foobar" - - async def test_large_BytesIO_chunks_in_sections(self): - data = bytearray(random.getrandbits(8) for _ in range(1 * 1024 * 1024)) - data_file = io.BytesIO(data) - data_stream = files.ByteStream("large_data_file.bin", data_file) - - i = 0 - async for chunk in data_stream: - expected_slice = data[i * files.MAGIC_NUMBER : (i + 1) * files.MAGIC_NUMBER] - assert chunk == expected_slice, f"failed on slice #{i}" - i += 1 - - @_helpers.assert_raises(type_=TypeError) - def test_bad_type(self): - files.ByteStream("foo", 3.14) - - @_helpers.assert_raises(type_=TypeError) - async def test_bad_chunk(self): - async def chunker(): - yield 3.14 - - await files.ByteStream("foo", chunker()).read() - - -@pytest.mark.asyncio -class TestWebResource: - # This is slow, generate once per class only. - @pytest.fixture(scope="class") - def random_bytes(self): - return bytes(bytearray(random.getrandbits(8) for _ in range(1 * 1024 * 1024))) - - @pytest.fixture - def stub_content(self, random_bytes): - class ContentIterator: - def __init__(self): - self.raw_content = random_bytes - - async def __aiter__(self): - # Pretend to send 1KiB chunks in response. - for i in range(0, len(self.raw_content), 1024): - yield self.raw_content[i : i + 1024] - - return ContentIterator() - - @pytest.fixture - def stub_response(self, stub_content): - class StubResponse: - def __init__(self): - self.status = 200 - self.content = stub_content - self.headers = {"x-whatever": "bleh"} - self.real_url = "https://some-websi.te" - - async def read(self): - return self.content.raw_content - - async def __aenter__(self): - return self - - async def __aexit__(self, exc_type, exc_val, exc_tb): - pass - - return StubResponse() - - @pytest.fixture - def mock_request(self, stub_response): - with mock.patch.object(aiohttp, "request", new=mock.MagicMock(return_value=stub_response)) as request: - yield request - - async def test_filename(self): - stream = files.WebResourceStream("cat.png", "http://http.cat") - assert stream.filename == "cat.png" - - async def test_happy_path_reads_data_in_chunks(self, stub_content, stub_response, mock_request): - stream = files.WebResourceStream("cat.png", "https://some-websi.te") - - i = 0 - async for chunk in stream: - assert chunk == stub_content.raw_content[i * 1024 : (i + 1) * 1024] - i += 1 - - assert i > 0, "no data yielded :(" - - mock_request.assert_called_once_with("GET", "https://some-websi.te") - - @_helpers.assert_raises(type_=errors.BadRequest) - async def test_400(self, stub_content, stub_response, mock_request): - stub_response.status = 400 - stream = files.WebResourceStream("cat.png", "https://some-websi.te") - - async for _ in stream: - assert False, "expected error by now" - - @_helpers.assert_raises(type_=errors.Unauthorized) - async def test_401(self, stub_content, stub_response, mock_request): - stub_response.status = 401 - stream = files.WebResourceStream("cat.png", "https://some-websi.te") - - async for _ in stream: - assert False, "expected error by now" - - @_helpers.assert_raises(type_=errors.Forbidden) - async def test_403(self, stub_content, stub_response, mock_request): - stub_response.status = 403 - stream = files.WebResourceStream("cat.png", "https://some-websi.te") - - async for _ in stream: - assert False, "expected error by now" - - @_helpers.assert_raises(type_=errors.NotFound) - async def test_404(self, stub_content, stub_response, mock_request): - stub_response.status = 404 - stream = files.WebResourceStream("cat.png", "https://some-websi.te") - - async for _ in stream: - assert False, "expected error by now" - - @pytest.mark.parametrize("status", [402, 405, 406, 408, 415, 429]) - @_helpers.assert_raises(type_=errors.ClientHTTPErrorResponse) - async def test_4xx(self, stub_content, stub_response, mock_request, status): - stub_response.status = status - stream = files.WebResourceStream("cat.png", "https://some-websi.te") - - async for _ in stream: - assert False, "expected error by now" - - @pytest.mark.parametrize("status", [500, 501, 502, 503, 504]) - @_helpers.assert_raises(type_=errors.ServerHTTPErrorResponse) - async def test_5xx(self, stub_content, stub_response, mock_request, status): - stub_response.status = status - stream = files.WebResourceStream("cat.png", "https://some-websi.te") - - async for _ in stream: - assert False, "expected error by now" - - @pytest.mark.parametrize("status", [100, 101, 102, 300, 301, 302, 303]) - @_helpers.assert_raises(type_=errors.HTTPErrorResponse) - async def test_random_status_codes(self, stub_content, stub_response, mock_request, status): - stub_response.status = status - stream = files.WebResourceStream("cat.png", "https://some-websi.te") - - async for _ in stream: - assert False, "expected error by now" - - -@pytest.mark.asyncio -class TestFileStream: - # slow, again. - @pytest.fixture(scope="class") - def random_bytes(self): - return bytes(bytearray(random.getrandbits(8) for _ in range(5 * 1024 * 1024))) - - @pytest.fixture - def threadpool_executor(self): - executor = concurrent.futures.ThreadPoolExecutor(max_workers=5) - yield executor - executor.shutdown(wait=True) - - @pytest.fixture - def processpool_executor(self): - executor = concurrent.futures.ProcessPoolExecutor(max_workers=5) - yield executor - executor.shutdown(wait=True) - - @pytest.fixture(scope="class") - def dummy_file(self, random_bytes): - with tempfile.TemporaryDirectory() as directory: - file = os.path.join(directory, str(uuid.uuid4())) - - with open(file, "wb") as fp: - fp.write(random_bytes) - - yield file - - async def test_filename(self): - stream = files.FileStream("cat.png", "/root/cat.png") - assert stream.filename == "cat.png" - - async def test_read_no_executor(self, random_bytes, dummy_file): - stream = files.FileStream("xxx", dummy_file) - - start = 0 - async for chunk in stream: - end = start + len(chunk) - assert chunk == random_bytes[start:end] - start = end - - assert start == len(random_bytes) - - async def test_read_in_threadpool(self, random_bytes, dummy_file, threadpool_executor): - stream = files.FileStream("xxx", dummy_file, executor=threadpool_executor) - - start = 0 - async for chunk in stream: - end = start + len(chunk) - assert chunk == random_bytes[start:end] - start = end - - assert start == len(random_bytes) - - @_helpers.skip_if_no_sem_open - async def test_read_in_processpool(self, random_bytes, dummy_file, processpool_executor): - stream = files.FileStream("xxx", dummy_file, executor=processpool_executor) - - start = 0 - async for chunk in stream: - end = start + len(chunk) - assert chunk == random_bytes[start:end] - start = end - - assert start == len(random_bytes) diff --git a/tests/hikari/models/test_guilds.py b/tests/hikari/models/test_guilds.py deleted file mode 100644 index 368fd4f2b2..0000000000 --- a/tests/hikari/models/test_guilds.py +++ /dev/null @@ -1,1134 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# Copyright © Nekoka.tt 2019-2020 -# -# This file is part of Hikari. -# -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . -import contextlib -import datetime - -import mock -import pytest - -from hikari import application -from hikari.internal import conversions -from hikari.net import urls -from hikari.models import channels -from hikari.models import colors -from hikari.models import emojis -from hikari.models import guilds -from hikari.models import permissions -from hikari.internal import unset -from hikari.models import users -from tests.hikari import _helpers - - -@pytest.fixture -def test_emoji_payload(): - return { - "id": "41771983429993937", - "name": "LUL", - "roles": ["41771983429993000", "41771983429993111"], - "user": { - "username": "Luigi", - "discriminator": "0002", - "id": "96008815106887111", - "avatar": "5500909a3274e1812beb4e8de6631111", - }, - "require_colons": True, - "managed": False, - "animated": False, - } - - -@pytest.fixture -def test_roles_payload(): - return { - "id": "41771983423143936", - "name": "WE DEM BOYZZ!!!!!!", - "color": 3_447_003, - "hoist": True, - "position": 0, - "permissions": 66_321_471, - "managed": False, - "mentionable": False, - } - - -@pytest.fixture -def test_channel_payload(): - return { - "type": 0, - "id": "1234567", - "guild_id": "696969", - "position": 100, - "permission_overwrites": [], - "nsfw": True, - "parent_id": None, - "rate_limit_per_user": 420, - "topic": "nsfw stuff", - "name": "shh!", - "last_message_id": "1234", - } - - -@pytest.fixture -def test_user_payload(): - return { - "id": "123456", - "username": "Boris Johnson", - "discriminator": "6969", - "avatar": "1a2b3c4d", - "mfa_enabled": True, - "locale": "gb", - "system": True, - "bot": True, - "flags": 0b00101101, - "premium_type": 1, - "public_flags": 0b0001101, - } - - -@pytest.fixture -def test_member_payload(test_user_payload): - return { - "nick": "foobarbaz", - "roles": ["11111", "22222", "33333", "44444"], - "joined_at": "2015-04-26T06:26:56.936000+00:00", - "premium_since": "2019-05-17T06:26:56.936000+00:00", - # These should be completely ignored. - "deaf": False, - "mute": True, - "user": test_user_payload, - } - - -@pytest.fixture -def test_voice_state_payload(): - return { - "channel": "432123321", - "user_id": "6543453", - "session_id": "350a109226bd6f43c81f12c7c08de20a", - "deaf": False, - "mute": True, - "self_deaf": True, - "self_mute": False, - "self_stream": True, - "suppress": False, - } - - -@pytest.fixture() -def test_activity_party_payload(): - return {"id": "spotify:3234234234", "size": [2, 5]} - - -@pytest.fixture() -def test_activity_timestamps_payload(): - return { - "start": 1584996792798, - "end": 1999999792798, - } - - -@pytest.fixture() -def test_activity_assets_payload(): - return { - "large_image": "34234234234243", - "large_text": "LARGE TEXT", - "small_image": "3939393", - "small_text": "small text", - } - - -@pytest.fixture() -def test_activity_secrets_payload(): - return {"join": "who's a good secret?", "spectate": "I'm a good secret", "match": "No."} - - -@pytest.fixture() -def test_presence_activity_payload( - test_activity_timestamps_payload, - test_emoji_payload, - test_activity_party_payload, - test_activity_assets_payload, - test_activity_secrets_payload, -): - return { - "name": "an activity", - "type": 1, - "url": "https://69.420.owouwunyaa", - "created_at": 1584996792798, - "timestamps": test_activity_timestamps_payload, - "application_id": "40404040404040", - "details": "They are doing stuff", - "state": "STATED", - "emoji": test_emoji_payload, - "party": test_activity_party_payload, - "assets": test_activity_assets_payload, - "secrets": test_activity_secrets_payload, - "instance": True, - "flags": 3, - } - - -@pytest.fixture() -def test_partial_guild_payload(): - return { - "id": "152559372126519269", - "name": "Isopropyl", - "icon": "d4a983885dsaa7691ce8bcaaf945a", - "features": ["DISCOVERABLE"], - } - - -@pytest.fixture() -def test_guild_preview_payload(test_emoji_payload): - return { - "id": "152559372126519269", - "name": "Isopropyl", - "icon": "d4a983885dsaa7691ce8bcaaf945a", - "splash": "dsa345tfcdg54b", - "discovery_splash": "lkodwaidi09239uid", - "emojis": [test_emoji_payload], - "features": ["DISCOVERABLE"], - "approximate_member_count": 69, - "approximate_presence_count": 42, - "description": "A DESCRIPTION.", - } - - -@pytest.fixture -def test_guild_payload( - test_emoji_payload, - test_roles_payload, - test_channel_payload, - test_member_payload, - test_voice_state_payload, - test_guild_member_presence, -): - return { - "afk_channel_id": "99998888777766", - "afk_timeout": 1200, - "application_id": "39494949", - "approximate_member_count": 15, - "approximate_presence_count": 7, - "banner": "1a2b3c", - "channels": [test_channel_payload], - "default_message_notifications": 1, - "description": "This is a server I guess, its a bit crap though", - "discovery_splash": "famfamFAMFAMfam", - "embed_channel_id": "9439394949", - "embed_enabled": True, - "emojis": [test_emoji_payload], - "explicit_content_filter": 2, - "features": ["ANIMATED_ICON", "MORE_EMOJI", "NEWS", "SOME_UNDOCUMENTED_FEATURE"], - "icon": "1a2b3c4d", - "id": "265828729970753537", - "joined_at": "2019-05-17T06:26:56.936000+00:00", - "large": False, - "max_members": 25000, - "max_presences": 250, - "max_video_channel_users": 25, - "member_count": 14, - "members": [test_member_payload], - "mfa_level": 1, - "name": "L33t guild", - "owner_id": "6969696", - "permissions": 66_321_471, - "preferred_locale": "en-GB", - "premium_subscription_count": 1, - "premium_tier": 2, - "presences": [test_guild_member_presence], - "public_updates_channel": "33333333", - "region": "eu-central", - "roles": [test_roles_payload], - "rules_channel": "42042069", - "splash": "0ff0ff0ff", - "system_channel_flags": 3, - "system_channel_id": "19216801", - "unavailable": False, - "vanity_url_code": "loool", - "verification_level": 4, - "voice_states": [test_voice_state_payload], - "widget_channel_id": "9439394949", - "widget_enabled": True, - } - - -@pytest.fixture() -def mock_app(): - return mock.MagicMock(application.Application) - - -class TestGuildEmbed: - @pytest.fixture() - def test_guild_embed_payload(self): - return {"channel": "123123123", "enabled": True} - - def test_deserialize(self, test_guild_embed_payload, mock_app): - guild_embed_obj = guilds.GuildWidget.deserialize(test_guild_embed_payload, app=mock_app) - assert guild_embed_obj.channel_id == 123123123 - assert guild_embed_obj.is_enabled is True - - -class TestGuildMember: - def test_deserialize(self, test_member_payload, test_user_payload, mock_app): - mock_user = mock.MagicMock(users.User) - mock_datetime_1 = mock.MagicMock(datetime.datetime) - mock_datetime_2 = mock.MagicMock(datetime.datetime) - stack = contextlib.ExitStack() - patched_user_deserializer = stack.enter_context( - _helpers.patch_marshal_attr( - guilds.GuildMember, "user", deserializer=users.User.deserialize, return_value=mock_user - ) - ) - patched_joined_at_deserializer = stack.enter_context( - _helpers.patch_marshal_attr( - guilds.GuildMember, - "joined_at", - deserializer=conversions.parse_iso_8601_ts, - return_value=mock_datetime_1, - ) - ) - patched_premium_since_deserializer = stack.enter_context( - _helpers.patch_marshal_attr( - guilds.GuildMember, - "premium_since", - deserializer=conversions.parse_iso_8601_ts, - return_value=mock_datetime_2, - ) - ) - with stack: - guild_member_obj = guilds.GuildMember.deserialize(test_member_payload, app=mock_app) - patched_premium_since_deserializer.assert_called_once_with("2019-05-17T06:26:56.936000+00:00") - patched_joined_at_deserializer.assert_called_once_with("2015-04-26T06:26:56.936000+00:00") - patched_user_deserializer.assert_called_once_with(test_user_payload, app=mock_app) - assert guild_member_obj.user is mock_user - assert guild_member_obj.nickname == "foobarbaz" - assert guild_member_obj.role_ids == [11111, 22222, 33333, 44444] - assert guild_member_obj.joined_at is mock_datetime_1 - assert guild_member_obj.premium_since is mock_datetime_2 - assert guild_member_obj.is_deaf is False - assert guild_member_obj.is_mute is True - - -class TestPartialGuildRole: - @pytest.fixture() - def test_partial_guild_role_payload(self): - return { - "id": "41771983423143936", - "name": "WE DEM BOYZZ!!!!!!", - } - - def test_deserialize(self, test_partial_guild_role_payload, mock_app): - partial_guild_role_obj = guilds.PartialRole.deserialize(test_partial_guild_role_payload, app=mock_app) - assert partial_guild_role_obj.name == "WE DEM BOYZZ!!!!!!" - - -class TestGuildRole: - @pytest.fixture() - def test_guild_role_payload(self): - return { - "id": "41771983423143936", - "name": "WE DEM BOYZZ!!!!!!", - "color": 3_447_003, - "hoist": True, - "position": 0, - "permissions": 66_321_471, - "managed": False, - "mentionable": False, - } - - def test_deserialize(self, test_guild_role_payload, mock_app): - guild_role_obj = guilds.Role.deserialize(test_guild_role_payload, app=mock_app) - assert guild_role_obj.color == 3_447_003 - assert guild_role_obj.is_hoisted is True - assert guild_role_obj.position == 0 - assert guild_role_obj.permissions == 66_321_471 - assert guild_role_obj.is_managed is False - assert guild_role_obj.is_mentionable is False - - def test_serialize_full_role(self): - guild_role_obj = guilds.Role( - name="aRole", - color=colors.Color(444), - is_hoisted=True, - position=42, - permissions=permissions.Permission(69), - is_mentionable=True, - id=123, - ) - assert guild_role_obj.serialize() == { - "name": "aRole", - "color": 444, - "hoist": True, - "position": 42, - "permissions": 69, - "mentionable": True, - "id": "123", - } - - def test_serialize_partial_role(self): - guild_role_obj = guilds.Role(name="aRole", id=123) - assert guild_role_obj.serialize() == { - "name": "aRole", - "color": 0, - "hoist": False, - "permissions": 0, - "mentionable": False, - "id": "123", - } - - -class TestActivityTimestamps: - def test_deserialize(self, test_activity_timestamps_payload, mock_app): - mock_start_date = mock.MagicMock(datetime.datetime) - mock_end_date = mock.MagicMock(datetime.datetime) - stack = contextlib.ExitStack() - patched_start_deserializer = stack.enter_context( - _helpers.patch_marshal_attr( - guilds.ActivityTimestamps, - "start", - deserializer=conversions.unix_epoch_to_datetime, - return_value=mock_start_date, - ) - ) - patched_end_deserializer = stack.enter_context( - _helpers.patch_marshal_attr( - guilds.ActivityTimestamps, - "end", - deserializer=conversions.unix_epoch_to_datetime, - return_value=mock_end_date, - ) - ) - with stack: - activity_timestamps_obj = guilds.ActivityTimestamps.deserialize( - test_activity_timestamps_payload, app=mock_app - ) - patched_end_deserializer.assert_called_once_with(1999999792798) - patched_start_deserializer.assert_called_once_with(1584996792798) - assert activity_timestamps_obj.start is mock_start_date - assert activity_timestamps_obj.end is mock_end_date - - -class TestActivityParty: - @pytest.fixture() - def test_activity_party_obj(self, test_activity_party_payload): - return guilds.ActivityParty.deserialize(test_activity_party_payload) - - def test_deserialize(self, test_activity_party_obj): - assert test_activity_party_obj.id == "spotify:3234234234" - assert test_activity_party_obj._size_information == (2, 5) - - def test_current_size(self, test_activity_party_obj): - assert test_activity_party_obj.current_size == 2 - - def test_current_size_when_null(self, test_activity_party_obj): - test_activity_party_obj._size_information = None - assert test_activity_party_obj.current_size is None - - def test_max_size(self, test_activity_party_obj): - assert test_activity_party_obj.max_size == 5 - - def test_max_size_when_null(self, test_activity_party_obj): - test_activity_party_obj._size_information = None - assert test_activity_party_obj.max_size is None - - -class TestActivityAssets: - def test_deserialize(self, test_activity_assets_payload, mock_app): - activity_assets_obj = guilds.ActivityAssets.deserialize(test_activity_assets_payload, app=mock_app) - assert activity_assets_obj.large_image == "34234234234243" - assert activity_assets_obj.large_text == "LARGE TEXT" - assert activity_assets_obj.small_image == "3939393" - assert activity_assets_obj.small_text == "small text" - - -class TestActivitySecret: - def test_deserialize(self, test_activity_secrets_payload, mock_app): - activity_secret_obj = guilds.ActivitySecret.deserialize(test_activity_secrets_payload, app=mock_app) - assert activity_secret_obj.join == "who's a good secret?" - assert activity_secret_obj.spectate == "I'm a good secret" - assert activity_secret_obj.match == "No." - - -class TestPresenceActivity: - def test_deserialize( - self, - test_presence_activity_payload, - test_activity_secrets_payload, - test_activity_assets_payload, - test_activity_party_payload, - test_emoji_payload, - test_activity_timestamps_payload, - mock_app, - ): - mock_created_at = mock.MagicMock(datetime.datetime) - mock_emoji = mock.MagicMock(emojis.CustomEmoji) - stack = contextlib.ExitStack() - patched_created_at_deserializer = stack.enter_context( - _helpers.patch_marshal_attr( - guilds.PresenceActivity, - "created_at", - deserializer=conversions.unix_epoch_to_datetime, - return_value=mock_created_at, - ) - ) - patched_emoji_deserializer = stack.enter_context( - _helpers.patch_marshal_attr( - guilds.PresenceActivity, - "emoji", - deserializer=emojis.deserialize_reaction_emoji, - return_value=mock_emoji, - ) - ) - with stack: - presence_activity_obj = guilds.PresenceActivity.deserialize(test_presence_activity_payload, app=mock_app) - patched_emoji_deserializer.assert_called_once_with(test_emoji_payload, app=mock_app) - patched_created_at_deserializer.assert_called_once_with(1584996792798) - assert presence_activity_obj.name == "an activity" - assert presence_activity_obj.type is guilds.ActivityType.STREAMING - assert presence_activity_obj.url == "https://69.420.owouwunyaa" - assert presence_activity_obj.created_at is mock_created_at - assert presence_activity_obj.timestamps == guilds.ActivityTimestamps.deserialize( - test_activity_timestamps_payload - ) - assert presence_activity_obj.timestamps._gateway_consumer is mock_app - assert presence_activity_obj.application_id == 40404040404040 - assert presence_activity_obj.details == "They are doing stuff" - assert presence_activity_obj.state == "STATED" - assert presence_activity_obj.emoji is mock_emoji - assert presence_activity_obj.party == guilds.ActivityParty.deserialize(test_activity_party_payload) - assert presence_activity_obj.party._gateway_consumer is mock_app - assert presence_activity_obj.assets == guilds.ActivityAssets.deserialize(test_activity_assets_payload) - assert presence_activity_obj.assets._gateway_consumer is mock_app - assert presence_activity_obj.secrets == guilds.ActivitySecret.deserialize(test_activity_secrets_payload) - assert presence_activity_obj.secrets._gateway_consumer is mock_app - assert presence_activity_obj.is_instance is True - assert presence_activity_obj.flags == guilds.ActivityFlag.INSTANCE | guilds.ActivityFlag.JOIN - - -@pytest.fixture() -def test_client_status_payload(): - return {"desktop": "online", "mobile": "idle"} - - -class TestClientStatus: - def test_deserialize(self, test_client_status_payload, mock_app): - client_status_obj = guilds.ClientStatus.deserialize(test_client_status_payload, app=mock_app) - assert client_status_obj.desktop is guilds.PresenceStatus.ONLINE - assert client_status_obj.mobile is guilds.PresenceStatus.IDLE - assert client_status_obj.web is guilds.PresenceStatus.OFFLINE - - -class TestPresenceUser: - def test_deserialize_filled_presence_user(self, test_user_payload, mock_app): - presence_user_obj = guilds.PresenceUser.deserialize(test_user_payload, app=mock_app) - assert presence_user_obj.username == "Boris Johnson" - assert presence_user_obj.discriminator == "6969" - assert presence_user_obj.avatar_hash == "1a2b3c4d" - assert presence_user_obj.is_system is True - assert presence_user_obj.is_bot is True - assert presence_user_obj.flags == users.UserFlag(0b0001101) - - def test_deserialize_partial_presence_user(self, mock_app): - presence_user_obj = guilds.PresenceUser.deserialize({"id": "115590097100865541"}, app=mock_app) - assert presence_user_obj.id == 115590097100865541 - for attr in presence_user_obj.__slots__: - if attr not in ("id", "_gateway_consumer"): - assert getattr(presence_user_obj, attr) is unset.UNSET - - @pytest.fixture() - def test_presence_user_obj(self): - return guilds.PresenceUser( - id=4242424242, - discriminator=unset.UNSET, - username=unset.UNSET, - avatar_hash=unset.UNSET, - is_bot=unset.UNSET, - is_system=unset.UNSET, - flags=unset.UNSET, - ) - - def test_avatar_url(self, test_presence_user_obj): - mock_url = mock.MagicMock(str) - test_presence_user_obj.discriminator = 2222 - with mock.patch.object(users.User, "format_avatar_url", return_value=mock_url): - assert test_presence_user_obj.avatar_url is mock_url - users.User.format_avatar_url.assert_called_once() - - @pytest.mark.parametrize(["avatar_hash", "discriminator"], [("dwaea22", unset.UNSET), (unset.UNSET, "2929")]) - def test_format_avatar_url_when_discriminator_or_avatar_hash_set_without_optionals( - self, test_presence_user_obj, avatar_hash, discriminator - ): - test_presence_user_obj.avatar_hash = avatar_hash - test_presence_user_obj.discriminator = discriminator - mock_url = mock.MagicMock(str) - with mock.patch.object(users.User, "format_avatar_url", return_value=mock_url): - assert test_presence_user_obj.format_avatar_url() is mock_url - users.User.format_avatar_url.assert_called_once_with(format_=None, size=4096) - - @pytest.mark.parametrize(["avatar_hash", "discriminator"], [("dwaea22", unset.UNSET), (unset.UNSET, "2929")]) - def test_format_avatar_url_when_discriminator_or_avatar_hash_set_with_optionals( - self, test_presence_user_obj, avatar_hash, discriminator - ): - test_presence_user_obj.avatar_hash = avatar_hash - test_presence_user_obj.discriminator = discriminator - mock_url = mock.MagicMock(str) - with mock.patch.object(users.User, "format_avatar_url", return_value=mock_url): - assert test_presence_user_obj.format_avatar_url(format_="nyaapeg", size=2048) is mock_url - users.User.format_avatar_url.assert_called_once_with(format_="nyaapeg", size=2048) - - def test_format_avatar_url_when_discriminator_and_avatar_hash_unset(self, test_presence_user_obj): - test_presence_user_obj.avatar_hash = unset.UNSET - test_presence_user_obj.discriminator = unset.UNSET - with mock.patch.object(users.User, "format_avatar_url", return_value=...): - assert test_presence_user_obj.format_avatar_url() is unset.UNSET - users.User.format_avatar_url.assert_not_called() - - def test_default_avatar_index_when_discriminator_set(self, test_presence_user_obj): - test_presence_user_obj.discriminator = 4242 - assert test_presence_user_obj.default_avatar_index == 2 - - def test_default_avatar_when_discriminator_unset(self, test_presence_user_obj): - test_presence_user_obj.discriminator = unset.UNSET - assert test_presence_user_obj.default_avatar_index is unset.UNSET - - def test_default_avatar_url_when_discriminator_is_set(self, test_presence_user_obj): - mock_url = "https://cdn.discordapp.com/embed/avatars/2.png" - test_presence_user_obj.discriminator = 4232 - with mock.patch.object(urls, "generate_cdn_url", return_value=mock_url): - url = test_presence_user_obj.default_avatar_url is mock_url - urls.generate_cdn_url.assert_called_once_with("embed", "avatars", "2", format_="png", size=None) - - def test_default_avatar_url_when_discriminator_is_unset(self, test_presence_user_obj): - mock_url = ... - test_presence_user_obj.discriminator = unset.UNSET - with mock.patch.object(urls, "generate_cdn_url", return_value=mock_url): - assert test_presence_user_obj.default_avatar_url is unset.UNSET - urls.generate_cdn_url.assert_not_called() - - -@pytest.fixture() -def test_guild_member_presence(test_user_payload, test_presence_activity_payload, test_client_status_payload): - return { - "user": test_user_payload, - "roles": ["49494949"], - "game": test_presence_activity_payload, - "guild_id": "44004040", - "status": "dnd", - "activities": [test_presence_activity_payload], - "client_status": test_client_status_payload, - "premium_since": "2015-04-26T06:26:56.936000+00:00", - "nick": "Nick", - } - - -class TestGuildMemberPresence: - def test_deserialize( - self, - test_guild_member_presence, - test_user_payload, - test_presence_activity_payload, - test_client_status_payload, - mock_app, - ): - mock_since = mock.MagicMock(datetime.datetime) - with _helpers.patch_marshal_attr( - guilds.GuildMemberPresence, - "premium_since", - deserializer=conversions.parse_iso_8601_ts, - return_value=mock_since, - ) as patched_since_deserializer: - guild_member_presence_obj = guilds.GuildMemberPresence.deserialize(test_guild_member_presence, app=mock_app) - patched_since_deserializer.assert_called_once_with("2015-04-26T06:26:56.936000+00:00") - assert guild_member_presence_obj.user == guilds.PresenceUser.deserialize(test_user_payload) - assert guild_member_presence_obj.user._gateway_consumer is mock_app - assert guild_member_presence_obj.role_ids == [49494949] - assert guild_member_presence_obj.guild_id == 44004040 - assert guild_member_presence_obj.visible_status is guilds.PresenceStatus.DND - assert guild_member_presence_obj.activities == [ - guilds.PresenceActivity.deserialize(test_presence_activity_payload) - ] - assert guild_member_presence_obj.activities[0]._gateway_consumer is mock_app - assert guild_member_presence_obj.client_status == guilds.ClientStatus.deserialize(test_client_status_payload) - assert guild_member_presence_obj.client_status._gateway_consumer is mock_app - assert guild_member_presence_obj.premium_since is mock_since - assert guild_member_presence_obj.nick == "Nick" - - -class TestGuildMemberBan: - @pytest.fixture() - def test_guild_member_ban_payload(self, test_user_payload): - return {"reason": "Get Nyaa'ed", "user": test_user_payload} - - def test_deserializer(self, test_guild_member_ban_payload, test_user_payload, mock_app): - mock_user = mock.MagicMock(users.User) - with _helpers.patch_marshal_attr( - guilds.GuildMemberBan, "user", deserializer=users.User.deserialize, return_value=mock_user - ) as patched_user_deserializer: - guild_member_ban_obj = guilds.GuildMemberBan.deserialize(test_guild_member_ban_payload, app=mock_app) - patched_user_deserializer.assert_called_once_with(test_user_payload, app=mock_app) - assert guild_member_ban_obj.reason == "Get Nyaa'ed" - assert guild_member_ban_obj.user is mock_user - - -@pytest.fixture() -def test_integration_account_payload(): - return {"id": "543453", "name": "Blah Blah"} - - -class TestIntegrationAccount: - def test_deserializer(self, test_integration_account_payload, mock_app): - integration_account_obj = guilds.IntegrationAccount.deserialize(test_integration_account_payload, app=mock_app) - assert integration_account_obj.id == "543453" - assert integration_account_obj.name == "Blah Blah" - - -@pytest.fixture() -def test_partial_guild_integration_payload(test_integration_account_payload): - return { - "id": "4949494949", - "name": "Blah blah", - "type": "twitch", - "account": test_integration_account_payload, - } - - -class TestPartialGuildIntegration: - def test_deserialise(self, test_partial_guild_integration_payload, test_integration_account_payload, mock_app): - partial_guild_integration_obj = guilds.PartialGuildIntegration.deserialize( - test_partial_guild_integration_payload, app=mock_app - ) - assert partial_guild_integration_obj.name == "Blah blah" - assert partial_guild_integration_obj.type == "twitch" - assert partial_guild_integration_obj.account == guilds.IntegrationAccount.deserialize( - test_integration_account_payload - ) - assert partial_guild_integration_obj.account._gateway_consumer is mock_app - - -class TestGuildIntegration: - @pytest.fixture() - def test_guild_integration_payload(self, test_user_payload, test_partial_guild_integration_payload): - return { - **test_partial_guild_integration_payload, - "enabled": True, - "syncing": False, - "role_id": "98494949", - "enable_emoticons": False, - "expire_behavior": 1, - "expire_grace_period": 7, - "user": test_user_payload, - "synced_at": "2015-04-26T06:26:56.936000+00:00", - } - - def test_deserialize( - self, test_guild_integration_payload, test_user_payload, test_integration_account_payload, mock_app - ): - mock_user = mock.MagicMock(users.User) - mock_sync_date = mock.MagicMock(datetime.datetime) - stack = contextlib.ExitStack() - patched_sync_at_deserializer = stack.enter_context( - _helpers.patch_marshal_attr( - guilds.GuildIntegration, - "last_synced_at", - deserializer=conversions.parse_iso_8601_ts, - return_value=mock_sync_date, - ) - ) - patched_user_deserializer = stack.enter_context( - _helpers.patch_marshal_attr( - guilds.GuildIntegration, "user", deserializer=users.User.deserialize, return_value=mock_user - ) - ) - with stack: - guild_integration_obj = guilds.GuildIntegration.deserialize(test_guild_integration_payload, app=mock_app) - patched_user_deserializer.assert_called_once_with(test_user_payload, app=mock_app) - patched_sync_at_deserializer.assert_called_once_with("2015-04-26T06:26:56.936000+00:00") - - assert guild_integration_obj.is_enabled is True - assert guild_integration_obj.is_syncing is False - assert guild_integration_obj.role_id == 98494949 - assert guild_integration_obj.is_emojis_enabled is False - assert guild_integration_obj.expire_behavior is guilds.IntegrationExpireBehaviour.KICK - assert guild_integration_obj.expire_grace_period == datetime.timedelta(days=7) - assert guild_integration_obj.user is mock_user - assert guild_integration_obj.last_synced_at is mock_sync_date - - -class TestUnavailableGuild: - def test_deserialize_when_unavailable_is_defined(self, mock_app): - guild_delete_event_obj = guilds.UnavailableGuild.deserialize( - {"id": "293293939", "unavailable": True}, app=mock_app - ) - assert guild_delete_event_obj.is_unavailable is True - - def test_deserialize_when_unavailable_is_undefined(self, mock_app): - guild_delete_event_obj = guilds.UnavailableGuild.deserialize({"id": "293293939"}, app=mock_app) - assert guild_delete_event_obj.is_unavailable is True - - -class TestPartialGuild: - def test_deserialize(self, test_partial_guild_payload, mock_app): - partial_guild_obj = guilds.PartialGuild.deserialize(test_partial_guild_payload, app=mock_app) - assert partial_guild_obj.id == 152559372126519269 - assert partial_guild_obj.name == "Isopropyl" - assert partial_guild_obj.icon_hash == "d4a983885dsaa7691ce8bcaaf945a" - assert partial_guild_obj.features == {guilds.GuildFeature.DISCOVERABLE} - - @pytest.fixture() - def partial_guild_obj(self, test_partial_guild_payload): - return _helpers.unslot_class(guilds.PartialGuild)( - id=152559372126519269, icon_hash="d4a983885dsaa7691ce8bcaaf945a", name=None, features=None, - ) - - def test_format_icon_url(self, partial_guild_obj): - mock_url = "https://cdn.discordapp.com/icons/152559372126519269/d4a983885dsaa7691ce8bcaaf945a.png?size=20" - with mock.patch.object(urls, "generate_cdn_url", return_value=mock_url): - url = partial_guild_obj.format_icon_url(format_="nyaapeg", size=42) - urls.generate_cdn_url.assert_called_once_with( - "icons", "152559372126519269", "d4a983885dsaa7691ce8bcaaf945a", format_="nyaapeg", size=42 - ) - assert url == mock_url - - def test_format_icon_url_animated_default(self, partial_guild_obj): - partial_guild_obj.icon_hash = "a_d4a983885dsaa7691ce8bcaaf945a" - mock_url = "https://cdn.discordapp.com/icons/152559372126519269/a_d4a983885dsaa7691ce8bcaaf945a.gif?size=20" - with mock.patch.object(urls, "generate_cdn_url", return_value=mock_url): - url = partial_guild_obj.format_icon_url() - urls.generate_cdn_url.assert_called_once_with( - "icons", "152559372126519269", "a_d4a983885dsaa7691ce8bcaaf945a", format_="gif", size=4096 - ) - assert url == mock_url - - def test_format_icon_url_none_animated_default(self, partial_guild_obj): - partial_guild_obj.icon_hash = "d4a983885dsaa7691ce8bcaaf945a" - mock_url = "https://cdn.discordapp.com/icons/152559372126519269/d4a983885dsaa7691ce8bcaaf945a.png?size=20" - with mock.patch.object(urls, "generate_cdn_url", return_value=mock_url): - url = partial_guild_obj.format_icon_url() - urls.generate_cdn_url.assert_called_once_with( - "icons", "152559372126519269", "d4a983885dsaa7691ce8bcaaf945a", format_="png", size=4096 - ) - assert url == mock_url - - def test_format_icon_url_returns_none(self, partial_guild_obj): - partial_guild_obj.icon_hash = None - with mock.patch.object(urls, "generate_cdn_url", return_value=...): - url = partial_guild_obj.format_icon_url(format_="nyaapeg", size=42) - urls.generate_cdn_url.assert_not_called() - assert url is None - - @pytest.mark.parametrize( - ["format_", "expected_format", "icon_hash", "size"], - [ - ("png", "png", "a_1a2b3c", 1 << 4), - ("png", "png", "1a2b3c", 1 << 5), - ("jpeg", "jpeg", "a_1a2b3c", 1 << 6), - ("jpeg", "jpeg", "1a2b3c", 1 << 7), - ("jpg", "jpg", "a_1a2b3c", 1 << 8), - ("jpg", "jpg", "1a2b3c", 1 << 9), - ("webp", "webp", "a_1a2b3c", 1 << 10), - ("webp", "webp", "1a2b3c", 1 << 11), - ("gif", "gif", "a_1a2b3c", 1 << 12), - ("gif", "gif", "1a2b3c", 1 << 7), - (None, "gif", "a_1a2b3c", 1 << 5), - (None, "png", "1a2b3c", 1 << 10), - ], - ) - def test_format_icon_url(self, partial_guild_obj, format_, expected_format, icon_hash, size): - mock_url = "https://cdn.discordapp.com/icons/152559372126519269/d4a983885dsaa7691ce8bcaaf945a.png?size=20" - partial_guild_obj.icon_hash = icon_hash - with mock.patch.object(urls, "generate_cdn_url", return_value=mock_url): - url = partial_guild_obj.format_icon_url(format_=format_, size=size) - urls.generate_cdn_url.assert_called_once_with( - "icons", str(partial_guild_obj.id), partial_guild_obj.icon_hash, format_=expected_format, size=size - ) - assert url == mock_url - - def test_icon_url_default(self, partial_guild_obj): - result = mock.MagicMock() - partial_guild_obj.format_icon_url = mock.MagicMock(return_value=result) - assert partial_guild_obj.icon_url is result - partial_guild_obj.format_icon_url.assert_called_once_with() - - -class TestGuildPreview: - def test_deserialize(self, test_guild_preview_payload, test_emoji_payload, mock_app): - mock_emoji = mock.MagicMock(emojis.KnownCustomEmoji, id=41771983429993937) - with mock.patch.object(emojis.KnownCustomEmoji, "deserialize", return_value=mock_emoji): - guild_preview_obj = guilds.GuildPreview.deserialize(test_guild_preview_payload, app=mock_app) - emojis.KnownCustomEmoji.deserialize.assert_called_once_with(test_emoji_payload, app=mock_app) - assert guild_preview_obj.splash_hash == "dsa345tfcdg54b" - assert guild_preview_obj.discovery_splash_hash == "lkodwaidi09239uid" - assert guild_preview_obj.emojis == {41771983429993937: mock_emoji} - assert guild_preview_obj.approximate_presence_count == 42 - assert guild_preview_obj.approximate_member_count == 69 - assert guild_preview_obj.description == "A DESCRIPTION." - - @pytest.fixture() - def test_guild_preview_obj(self): - return guilds.GuildPreview( - id="23123123123", - name=None, - icon_hash=None, - features=None, - splash_hash="dsa345tfcdg54b", - discovery_splash_hash="lkodwaidi09239uid", - emojis=None, - approximate_presence_count=None, - approximate_member_count=None, - description=None, - ) - - def test_format_discovery_splash_url(self, test_guild_preview_obj): - mock_url = "https://not-al" - with mock.patch.object(urls, "generate_cdn_url", return_value=mock_url): - url = test_guild_preview_obj.format_discovery_splash_url(format_="nyaapeg", size=4000) - urls.generate_cdn_url.assert_called_once_with( - "discovery-splashes", "23123123123", "lkodwaidi09239uid", format_="nyaapeg", size=4000 - ) - assert url == mock_url - - def test_format_discovery_splash_returns_none(self, test_guild_preview_obj): - test_guild_preview_obj.discovery_splash_hash = None - with mock.patch.object(urls, "generate_cdn_url", return_value=...): - url = test_guild_preview_obj.format_discovery_splash_url() - urls.generate_cdn_url.assert_not_called() - assert url is None - - def test_discover_splash_url(self, test_guild_preview_obj): - mock_url = "https://not-al" - with mock.patch.object(urls, "generate_cdn_url", return_value=mock_url): - url = test_guild_preview_obj.discovery_splash_url - urls.generate_cdn_url.assert_called_once() - assert url == mock_url - - def test_format_splash_url(self, test_guild_preview_obj): - mock_url = "https://not-al" - with mock.patch.object(urls, "generate_cdn_url", return_value=mock_url): - url = test_guild_preview_obj.format_splash_url(format_="nyaapeg", size=4000) - urls.generate_cdn_url.assert_called_once_with( - "splashes", "23123123123", "dsa345tfcdg54b", format_="nyaapeg", size=4000 - ) - assert url == mock_url - - def test_format_splash_returns_none(self, test_guild_preview_obj): - test_guild_preview_obj.splash_hash = None - with mock.patch.object(urls, "generate_cdn_url", return_value=...): - url = test_guild_preview_obj.format_splash_url() - urls.generate_cdn_url.assert_not_called() - assert url is None - - def test_splash_url(self, test_guild_preview_obj): - mock_url = "https://not-al" - with mock.patch.object(urls, "generate_cdn_url", return_value=mock_url): - url = test_guild_preview_obj.splash_url - urls.generate_cdn_url.assert_called_once() - assert url == mock_url - - -class TestGuild: - def test_deserialize( - self, - mock_app, - test_guild_payload, - test_roles_payload, - test_emoji_payload, - test_member_payload, - test_channel_payload, - test_guild_member_presence, - ): - mock_emoji = mock.MagicMock(emojis.KnownCustomEmoji, id=41771983429993937) - mock_guild_channel = mock.MagicMock(channels.GuildChannel, id=1234567) - stack = contextlib.ExitStack() - stack.enter_context(mock.patch.object(emojis.KnownCustomEmoji, "deserialize", return_value=mock_emoji)) - stack.enter_context(mock.patch.object(channels, "deserialize_channel", return_value=mock_guild_channel)) - stack.enter_context( - _helpers.patch_marshal_attr( - guilds.GuildMember, "user", deserializer=users.User.deserialize, return_value=mock.MagicMock(users.User) - ) - ) - with stack: - guild_obj = guilds.Guild.deserialize(test_guild_payload, app=mock_app) - channels.deserialize_channel.assert_called_once_with(test_channel_payload, app=mock_app) - emojis.KnownCustomEmoji.deserialize.assert_called_once_with(test_emoji_payload, app=mock_app) - assert guild_obj.members == {123456: guilds.GuildMember.deserialize(test_member_payload)} - assert guild_obj.members[123456]._gateway_consumer is mock_app - assert guild_obj.presences == {123456: guilds.GuildMemberPresence.deserialize(test_guild_member_presence)} - assert guild_obj.presences[123456]._gateway_consumer is mock_app - assert guild_obj.splash_hash == "0ff0ff0ff" - assert guild_obj.discovery_splash_hash == "famfamFAMFAMfam" - assert guild_obj.owner_id == 6969696 - assert guild_obj.my_permissions == 66_321_471 - assert guild_obj.region == "eu-central" - assert guild_obj.afk_channel_id == 99998888777766 - assert guild_obj.afk_timeout == datetime.timedelta(minutes=20) - assert guild_obj.is_embed_enabled is True - assert guild_obj.embed_channel_id == 9439394949 - assert guild_obj.is_widget_enabled is True - assert guild_obj.widget_channel_id == 9439394949 - assert guild_obj.verification_level is guilds.GuildVerificationLevel.VERY_HIGH - assert guild_obj.default_message_notifications is guilds.GuildMessageNotificationsLevel.ONLY_MENTIONS - assert guild_obj.explicit_content_filter is guilds.GuildExplicitContentFilterLevel.ALL_MEMBERS - assert guild_obj.roles == {41771983423143936: guilds.Role.deserialize(test_roles_payload)} - assert guild_obj.roles[41771983423143936]._gateway_consumer is mock_app - assert guild_obj.emojis == {41771983429993937: mock_emoji} - assert guild_obj.mfa_level is guilds.GuildMFALevel.ELEVATED - assert guild_obj.application_id == 39494949 - assert guild_obj.is_unavailable is False - assert guild_obj.system_channel_id == 19216801 - assert ( - guild_obj.system_channel_flags - == guilds.GuildSystemChannelFlag.SUPPRESS_PREMIUM_SUBSCRIPTION - | guilds.GuildSystemChannelFlag.SUPPRESS_USER_JOIN - ) - assert guild_obj.rules_channel_id == 42042069 - assert guild_obj.joined_at == conversions.parse_iso_8601_ts("2019-05-17T06:26:56.936000+00:00") - assert guild_obj.is_large is False - assert guild_obj.member_count == 14 - assert guild_obj.channels == {1234567: mock_guild_channel} - assert guild_obj.max_presences == 250 - assert guild_obj.max_members == 25000 - assert guild_obj.vanity_url_code == "loool" - assert guild_obj.description == "This is a server I guess, its a bit crap though" - assert guild_obj.banner_hash == "1a2b3c" - assert guild_obj.premium_tier is guilds.GuildPremiumTier.TIER_2 - assert guild_obj.premium_subscription_count == 1 - assert guild_obj.preferred_locale == "en-GB" - assert guild_obj.public_updates_channel_id == 33333333 - assert guild_obj.max_video_channel_users == 25 - assert guild_obj.approximate_member_count == 15 - assert guild_obj.approximate_active_member_count == 7 - - @pytest.fixture() - def test_guild_obj(self): - return guilds.Guild( - # TODO: fix null spam here, it is terrible test data, as it is not possible!!!! - id=265828729970753537, - icon_hash=None, - name=None, - features=None, - splash_hash="0ff0ff0ff", - banner_hash="1a2b3c", - discovery_splash_hash="famfamFAMFAMfam", - owner_id=None, - my_permissions=None, - region=None, - afk_channel_id=None, - afk_timeout=None, - is_embed_enabled=None, - embed_channel_id=None, - verification_level=None, - default_message_notifications=None, - explicit_content_filter=None, - roles=None, - emojis=None, - mfa_level=None, - application_id=None, - is_unavailable=None, - is_widget_enabled=None, - widget_channel_id=None, - system_channel_id=None, - system_channel_flags=None, - rules_channel_id=None, - joined_at=None, - is_large=None, - member_count=None, - members=None, - channels=None, - presences=None, - max_presences=None, - max_members=None, - vanity_url_code=None, - description=None, - premium_tier=None, - premium_subscription_count=None, - preferred_locale=None, - public_updates_channel_id=None, - approximate_active_member_count=None, - approximate_member_count=None, - max_video_channel_users=None, - ) - - def test_format_banner_url(self, test_guild_obj): - mock_url = "https://not-al" - with mock.patch.object(urls, "generate_cdn_url", return_value=mock_url): - url = test_guild_obj.format_banner_url(format_="nyaapeg", size=4000) - urls.generate_cdn_url.assert_called_once_with( - "banners", "265828729970753537", "1a2b3c", format_="nyaapeg", size=4000 - ) - assert url == mock_url - - def test_format_banner_url_returns_none(self, test_guild_obj): - test_guild_obj.banner_hash = None - with mock.patch.object(urls, "generate_cdn_url", return_value=...): - url = test_guild_obj.format_banner_url() - urls.generate_cdn_url.assert_not_called() - assert url is None - - def test_banner_url(self, test_guild_obj): - mock_url = "https://not-al" - with mock.patch.object(urls, "generate_cdn_url", return_value=mock_url): - url = test_guild_obj.banner_url - urls.generate_cdn_url.assert_called_once() - assert url == mock_url - - def test_format_discovery_splash_url(self, test_guild_obj): - mock_url = "https://not-al" - with mock.patch.object(urls, "generate_cdn_url", return_value=mock_url): - url = test_guild_obj.format_discovery_splash_url(format_="nyaapeg", size=4000) - urls.generate_cdn_url.assert_called_once_with( - "discovery-splashes", "265828729970753537", "famfamFAMFAMfam", format_="nyaapeg", size=4000 - ) - assert url == mock_url - - def test_format_discovery_splash_returns_none(self, test_guild_obj): - test_guild_obj.discovery_splash_hash = None - with mock.patch.object(urls, "generate_cdn_url", return_value=...): - url = test_guild_obj.format_discovery_splash_url() - urls.generate_cdn_url.assert_not_called() - assert url is None - - def test_discover_splash_url(self, test_guild_obj): - mock_url = "https://not-al" - with mock.patch.object(urls, "generate_cdn_url", return_value=mock_url): - url = test_guild_obj.discovery_splash_url - urls.generate_cdn_url.assert_called_once() - assert url == mock_url - - def test_format_splash_url(self, test_guild_obj): - mock_url = "https://not-al" - with mock.patch.object(urls, "generate_cdn_url", return_value=mock_url): - url = test_guild_obj.format_splash_url(format_="nyaapeg", size=4000) - urls.generate_cdn_url.assert_called_once_with( - "splashes", "265828729970753537", "0ff0ff0ff", format_="nyaapeg", size=4000 - ) - assert url == mock_url - - def test_format_splash_returns_none(self, test_guild_obj): - test_guild_obj.splash_hash = None - with mock.patch.object(urls, "generate_cdn_url", return_value=...): - url = test_guild_obj.format_splash_url() - urls.generate_cdn_url.assert_not_called() - assert url is None - - def test_splash_url(self, test_guild_obj): - mock_url = "https://not-al" - with mock.patch.object(urls, "generate_cdn_url", return_value=mock_url): - url = test_guild_obj.splash_url - urls.generate_cdn_url.assert_called_once() - assert url == mock_url diff --git a/tests/hikari/models/test_invites.py b/tests/hikari/models/test_invites.py deleted file mode 100644 index 687312fc1d..0000000000 --- a/tests/hikari/models/test_invites.py +++ /dev/null @@ -1,296 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# Copyright © Nekoka.tt 2019-2020 -# -# This file is part of Hikari. -# -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . -import contextlib -import datetime - -import mock -import pytest - -from hikari import application -from hikari.internal import conversions -from hikari.net import urls -from hikari.models import channels -from hikari.models import guilds -from hikari.models import invites -from hikari.models import users -from tests.hikari import _helpers - - -@pytest.fixture() -def test_user_payload(): - return {"id": "2020202", "username": "bang", "discriminator": "2222", "avatar": None} - - -@pytest.fixture() -def test_2nd_user_payload(): - return {"id": "1231231", "username": "soad", "discriminator": "3333", "avatar": None} - - -@pytest.fixture() -def test_invite_guild_payload(): - return { - "id": "56188492224814744", - "name": "Testin' Your Scene", - "splash": "aSplashForSure", - "banner": "aBannerForSure", - "description": "Describe me cute kitty.", - "icon": "bb71f469c158984e265093a81b3397fb", - "features": [], - "verification_level": 2, - "vanity_url_code": "I-am-very-vain", - } - - -@pytest.fixture() -def test_partial_channel(): - return {"id": "303030", "name": "channel-time", "type": 3} - - -@pytest.fixture() -def test_invite_payload(test_user_payload, test_2nd_user_payload, test_invite_guild_payload, test_partial_channel): - return { - "code": "aCode", - "guild": test_invite_guild_payload, - "channel": test_partial_channel, - "inviter": test_user_payload, - "target_user": test_2nd_user_payload, - "target_user_type": 1, - "approximate_presence_count": 42, - "approximate_member_count": 84, - } - - -@pytest.fixture() -def test_invite_with_metadata_payload(test_invite_payload): - return { - **test_invite_payload, - "uses": 3, - "max_uses": 8, - "max_age": 239349393, - "temporary": True, - "created_at": "2015-04-26T06:26:56.936000+00:00", - } - - -@pytest.fixture() -def mock_app(): - return mock.MagicMock(application.Application) - - -class TestInviteGuild: - def test_deserialize(self, test_invite_guild_payload, mock_app): - invite_guild_obj = invites.InviteGuild.deserialize(test_invite_guild_payload, app=mock_app) - assert invite_guild_obj.splash_hash == "aSplashForSure" - assert invite_guild_obj.banner_hash == "aBannerForSure" - assert invite_guild_obj.description == "Describe me cute kitty." - assert invite_guild_obj.verification_level is guilds.GuildVerificationLevel.MEDIUM - assert invite_guild_obj.vanity_url_code == "I-am-very-vain" - - @pytest.fixture() - def invite_guild_obj(self): - return invites.InviteGuild( - id="56188492224814744", - name=None, - icon_hash=None, - features=None, - splash_hash="aSplashForSure", - banner_hash="aBannerForSure", - description=None, - verification_level=None, - vanity_url_code=None, - ) - - def test_format_splash_url(self, invite_guild_obj): - mock_url = "https://not-al" - with mock.patch.object(urls, "generate_cdn_url", return_value=mock_url): - url = invite_guild_obj.format_splash_url(format_="nyaapeg", size=4000) - urls.generate_cdn_url.assert_called_once_with( - "splashes", "56188492224814744", "aSplashForSure", format_="nyaapeg", size=4000 - ) - assert url == mock_url - - def test_format_splash_url_returns_none(self, invite_guild_obj): - invite_guild_obj.splash_hash = None - with mock.patch.object(urls, "generate_cdn_url", return_value=...): - url = invite_guild_obj.format_splash_url() - urls.generate_cdn_url.assert_not_called() - assert url is None - - def test_splash_url(self, invite_guild_obj): - mock_url = "https://not-al" - with mock.patch.object(urls, "generate_cdn_url", return_value=mock_url): - url = invite_guild_obj.splash_url - urls.generate_cdn_url.assert_called_once() - assert url == mock_url - - def test_format_banner_url(self, invite_guild_obj): - mock_url = "https://not-al" - with mock.patch.object(urls, "generate_cdn_url", return_value=mock_url): - url = invite_guild_obj.format_banner_url(format_="nyaapeg", size=4000) - urls.generate_cdn_url.assert_called_once_with( - "banners", "56188492224814744", "aBannerForSure", format_="nyaapeg", size=4000 - ) - assert url == mock_url - - def test_format_banner_url_returns_none(self, invite_guild_obj): - invite_guild_obj.banner_hash = None - with mock.patch.object(urls, "generate_cdn_url", return_value=...): - url = invite_guild_obj.format_banner_url() - urls.generate_cdn_url.assert_not_called() - assert url is None - - def test_banner_url(self, invite_guild_obj): - mock_url = "https://not-al" - with mock.patch.object(urls, "generate_cdn_url", return_value=mock_url): - url = invite_guild_obj.banner_url - urls.generate_cdn_url.assert_called_once() - assert url == mock_url - - -class TestVanityUrl: - @pytest.fixture() - def vanity_url_payload(self): - return {"code": "iamacode", "uses": 42} - - def test_deserialize(self, vanity_url_payload, mock_app): - vanity_url_obj = invites.VanityURL.deserialize(vanity_url_payload, app=mock_app) - assert vanity_url_obj.code == "iamacode" - assert vanity_url_obj.uses == 42 - - -class TestInvite: - def test_deserialize( - self, - test_invite_payload, - test_user_payload, - test_2nd_user_payload, - test_partial_channel, - test_invite_guild_payload, - mock_app, - ): - mock_guild = mock.MagicMock(invites.InviteGuild) - mock_channel = mock.MagicMock(channels.PartialChannel) - mock_user_1 = mock.MagicMock(users.User) - mock_user_2 = mock.MagicMock(users.User) - stack = contextlib.ExitStack() - mock_guld_deseralize = stack.enter_context( - _helpers.patch_marshal_attr( - invites.Invite, "guild", deserializer=invites.InviteGuild.deserialize, return_value=mock_guild - ) - ) - mock_channel_deseralize = stack.enter_context( - _helpers.patch_marshal_attr( - invites.Invite, "channel", deserializer=channels.PartialChannel.deserialize, return_value=mock_channel - ) - ) - mock_inviter_deseralize = stack.enter_context( - _helpers.patch_marshal_attr( - invites.Invite, "inviter", deserializer=users.User.deserialize, return_value=mock_user_1 - ) - ) - mock_target_user_deseralize = stack.enter_context( - _helpers.patch_marshal_attr( - invites.Invite, "target_user", deserializer=users.User.deserialize, return_value=mock_user_2 - ) - ) - with stack: - invite_obj = invites.Invite.deserialize(test_invite_payload, app=mock_app) - mock_target_user_deseralize.assert_called_once_with(test_2nd_user_payload, app=mock_app) - mock_inviter_deseralize.assert_called_once_with(test_user_payload, app=mock_app) - mock_channel_deseralize.assert_called_once_with(test_partial_channel, app=mock_app) - mock_guld_deseralize.assert_called_once_with(test_invite_guild_payload, app=mock_app) - assert invite_obj.code == "aCode" - assert invite_obj.guild is mock_guild - assert invite_obj.channel is mock_channel - assert invite_obj.inviter is mock_user_1 - assert invite_obj.target_user is mock_user_2 - assert invite_obj.target_user_type is invites.TargetUserType.STREAM - assert invite_obj.approximate_member_count == 84 - assert invite_obj.approximate_presence_count == 42 - - -class TestInviteWithMetadata: - def test_deserialize(self, test_invite_with_metadata_payload, mock_app): - mock_datetime = mock.MagicMock(datetime.datetime) - stack = contextlib.ExitStack() - stack.enter_context( - _helpers.patch_marshal_attr( - invites.InviteWithMetadata, "guild", deserializer=invites.InviteGuild.deserialize - ) - ) - stack.enter_context( - _helpers.patch_marshal_attr( - invites.InviteWithMetadata, "channel", deserializer=channels.PartialChannel.deserialize - ) - ) - stack.enter_context( - _helpers.patch_marshal_attr(invites.InviteWithMetadata, "inviter", deserializer=users.User.deserialize) - ) - stack.enter_context( - _helpers.patch_marshal_attr(invites.InviteWithMetadata, "target_user", deserializer=users.User.deserialize) - ) - mock_created_at_deserializer = stack.enter_context( - _helpers.patch_marshal_attr( - invites.InviteWithMetadata, - "created_at", - deserializer=conversions.parse_iso_8601_ts, - return_value=mock_datetime, - ) - ) - with stack: - invite_with_metadata_obj = invites.InviteWithMetadata.deserialize( - test_invite_with_metadata_payload, app=mock_app - ) - mock_created_at_deserializer.assert_called_once_with("2015-04-26T06:26:56.936000+00:00") - assert invite_with_metadata_obj.uses == 3 - assert invite_with_metadata_obj.max_uses == 8 - assert invite_with_metadata_obj.max_age == datetime.timedelta(seconds=239349393) - assert invite_with_metadata_obj.is_temporary is True - assert invite_with_metadata_obj.created_at is mock_datetime - - @pytest.fixture() - def mock_invite_with_metadata(self, test_invite_with_metadata_payload): - return invites.InviteWithMetadata( - code=None, - guild=None, - channel=None, - inviter=None, - target_user=None, - target_user_type=None, - approximate_presence_count=None, - approximate_member_count=None, - uses=None, - max_uses=None, - max_age=datetime.timedelta(seconds=239349393), - is_temporary=None, - created_at=conversions.parse_iso_8601_ts("2015-04-26T06:26:56.936000+00:00"), - ) - - def test_max_age_when_zero(self, test_invite_with_metadata_payload): - test_invite_with_metadata_payload["max_age"] = 0 - assert invites.InviteWithMetadata.deserialize(test_invite_with_metadata_payload).max_age is None - - def test_expires_at(self, mock_invite_with_metadata): - assert mock_invite_with_metadata.expires_at == datetime.datetime.fromisoformat( - "2022-11-25 12:23:29.936000+00:00" - ) - - def test_expires_at_returns_none(self, mock_invite_with_metadata): - mock_invite_with_metadata.max_age = None - assert mock_invite_with_metadata.expires_at is None diff --git a/tests/hikari/models/test_messages.py b/tests/hikari/models/test_messages.py deleted file mode 100644 index 1bd60ae782..0000000000 --- a/tests/hikari/models/test_messages.py +++ /dev/null @@ -1,552 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# Copyright © Nekoka.tt 2019-2020 -# -# This file is part of Hikari. -# -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . -import contextlib -import datetime - -import mock -import pytest - -from hikari import application -from hikari.internal import conversions -from hikari.models import applications -from hikari.models import bases -from hikari.models import channels -from hikari.models import embeds -from hikari.models import emojis -from hikari.models import files -from hikari.models import guilds -from hikari.models import messages -from hikari.models import users -from tests.hikari import _helpers - - -@pytest.fixture -def test_attachment_payload(): - return { - "id": "690922406474154014", - "filename": "IMG.jpg", - "size": 660521, - "url": "https://somewhere.com/attachments/123/456/IMG.jpg", - "_proxy_url": "https://media.somewhere.com/attachments/123/456/IMG.jpg", - "width": 1844, - "height": 2638, - } - - -@pytest.fixture() -def test_emoji_payload(): - return {"id": "691225175349395456", "name": "test"} - - -@pytest.fixture -def test_reaction_payload(test_emoji_payload): - return {"emoji": test_emoji_payload, "count": 100, "me": True} - - -@pytest.fixture -def test_message_activity_payload(): - return {"type": 5, "party_id": "ae488379-351d-4a4f-ad32-2b9b01c91657"} - - -@pytest.fixture -def test_message_crosspost_payload(): - return {"channel": "278325129692446722", "guild_id": "278325129692446720", "message_id": "306588351130107906"} - - -@pytest.fixture() -def test_application_payload(): - return { - "id": "456", - "name": "hikari", - "description": "The best application", - "icon": "2658b3029e775a931ffb49380073fa63", - "cover_image": "58982a23790c4f22787b05d3be38a026", - } - - -@pytest.fixture() -def test_user_payload(): - return { - "bot": True, - "id": "1234", - "username": "cool username", - "avatar": "6608709a3274e1812beb4e8de6631111", - "discriminator": "0000", - } - - -@pytest.fixture() -def test_member_payload(test_user_payload): - return {"user": test_user_payload} - - -@pytest.fixture -def test_message_payload( - test_application_payload, - test_attachment_payload, - test_reaction_payload, - test_user_payload, - test_member_payload, - test_message_activity_payload, - test_message_crosspost_payload, -): - return { - "id": "123", - "channel": "456", - "guild_id": "678", - "author": test_user_payload, - "member": test_member_payload, - "content": "some info", - "timestamp": "2020-03-21T21:20:16.510000+00:00", - "edited_timestamp": "2020-04-21T21:20:16.510000+00:00", - "tts": True, - "mention_everyone": True, - "mentions": [ - {"id": "5678", "username": "uncool username", "avatar": "129387dskjafhasf", "discriminator": "4532"} - ], - "mention_roles": ["987"], - "mention_channels": [{"id": "456", "guild_id": "678", "type": 1, "name": "hikari-testing"}], - "attachments": [test_attachment_payload], - "embeds": [{}], - "reactions": [test_reaction_payload], - "pinned": True, - "webhook_id": "1234", - "type": 0, - "activity": test_message_activity_payload, - "application": test_application_payload, - "message_reference": test_message_crosspost_payload, - "flags": 2, - "nonce": "171000788183678976", - } - - -@pytest.fixture() -def mock_app(): - return mock.MagicMock(application.Application) - - -class TestAttachment: - def test_deserialize(self, test_attachment_payload, mock_app): - attachment_obj = messages.Attachment.deserialize(test_attachment_payload, app=mock_app) - - assert attachment_obj.id == 690922406474154014 - assert attachment_obj.filename == "IMG.jpg" - assert attachment_obj.size == 660521 - assert attachment_obj.url == "https://somewhere.com/attachments/123/456/IMG.jpg" - assert attachment_obj._proxy_url == "https://media.somewhere.com/attachments/123/456/IMG.jpg" - assert attachment_obj.height == 2638 - assert attachment_obj.width == 1844 - - @pytest.mark.asyncio - async def test_aiter_yields_from_WebResourceStream(self): - attachment = messages.Attachment( - id=bases.Snowflake("1234"), - filename="foobar.png", - size=1_024_024, - url="https://example.com/foobar.png?x=4096", - proxy_url="https://example.com/foobar.png?x=4096", - ) - - async def __aiter__(_): - yield b"foo" - yield b"bar" - yield b"baz" - - web_resource = mock.MagicMock(spec_set=files.WebResourceStream) - web_resource.__aiter__ = __aiter__ - - with mock.patch.object(files, "WebResourceStream", return_value=web_resource): - async_iterator = attachment.__aiter__() - assert await async_iterator.__anext__() == b"foo" - assert await async_iterator.__anext__() == b"bar" - assert await async_iterator.__anext__() == b"baz" - - with pytest.raises(StopAsyncIteration): - await async_iterator.__anext__() - - -class TestReaction: - def test_deserialize(self, test_reaction_payload, mock_app, test_emoji_payload): - mock_emoji = mock.MagicMock(emojis.CustomEmoji) - - with _helpers.patch_marshal_attr( - messages.Reaction, "emoji", return_value=mock_emoji, deserializer=emojis.deserialize_reaction_emoji - ) as patched_emoji_deserializer: - reaction_obj = messages.Reaction.deserialize(test_reaction_payload, app=mock_app) - patched_emoji_deserializer.assert_called_once_with(test_emoji_payload, app=mock_app) - - assert reaction_obj.count == 100 - assert reaction_obj.emoji == mock_emoji - assert reaction_obj.is_reacted_by_me is True - - -class TestMessageActivity: - def test_deserialize(self, test_message_activity_payload, mock_app): - message_activity_obj = messages.MessageActivity.deserialize(test_message_activity_payload, app=mock_app) - - assert message_activity_obj.type == messages.MessageActivityType.JOIN_REQUEST - assert message_activity_obj.party_id == "ae488379-351d-4a4f-ad32-2b9b01c91657" - - -class TestMessageCrosspost: - def test_deserialize(self, test_message_crosspost_payload, mock_app): - message_crosspost_obj = messages.MessageCrosspost.deserialize(test_message_crosspost_payload, app=mock_app) - - assert message_crosspost_obj.id == 306588351130107906 - assert message_crosspost_obj.channel_id == 278325129692446722 - assert message_crosspost_obj.guild_id == 278325129692446720 - - -class TestMessage: - def test_deserialize( - self, - test_message_payload, - test_application_payload, - test_attachment_payload, - test_reaction_payload, - test_user_payload, - test_member_payload, - test_message_activity_payload, - test_message_crosspost_payload, - mock_app, - ): - mock_user = mock.MagicMock(users.User) - mock_member = mock.MagicMock(guilds.GuildMember) - mock_datetime = mock.MagicMock(datetime.datetime) - mock_datetime2 = mock.MagicMock(datetime.datetime) - mock_emoji = mock.MagicMock(emojis.Emoji) - mock_app = mock.MagicMock(applications.Application) - - stack = contextlib.ExitStack() - patched_author_deserializer = stack.enter_context( - _helpers.patch_marshal_attr( - messages.Message, "author", deserializer=users.User.deserialize, return_value=mock_user - ) - ) - patched_member_deserializer = stack.enter_context( - _helpers.patch_marshal_attr( - messages.Message, "member", deserializer=guilds.GuildMember.deserialize, return_value=mock_member - ) - ) - patched_timestamp_deserializer = stack.enter_context( - _helpers.patch_marshal_attr( - messages.Message, "timestamp", deserializer=conversions.parse_iso_8601_ts, return_value=mock_datetime, - ) - ) - patched_edited_timestamp_deserializer = stack.enter_context( - _helpers.patch_marshal_attr( - messages.Message, - "edited_timestamp", - deserializer=conversions.parse_iso_8601_ts, - return_value=mock_datetime2, - ) - ) - patched_application_deserializer = stack.enter_context( - _helpers.patch_marshal_attr( - messages.Message, - "application", - deserializer=applications.Application.deserialize, - return_value=mock_app, - ) - ) - patched_emoji_deserializer = stack.enter_context( - _helpers.patch_marshal_attr( - messages.Reaction, "emoji", deserializer=emojis.deserialize_reaction_emoji, return_value=mock_emoji, - ) - ) - with stack: - message_obj = messages.Message.deserialize(test_message_payload, app=mock_app) - patched_emoji_deserializer.assert_called_once_with(test_reaction_payload["emoji"], app=mock_app) - assert message_obj.reactions == [messages.Reaction.deserialize(test_reaction_payload)] - assert message_obj.reactions[0]._gateway_consumer is mock_app - patched_application_deserializer.assert_called_once_with(test_application_payload, app=mock_app) - patched_edited_timestamp_deserializer.assert_called_once_with("2020-04-21T21:20:16.510000+00:00") - patched_timestamp_deserializer.assert_called_once_with("2020-03-21T21:20:16.510000+00:00") - patched_member_deserializer.assert_called_once_with(test_member_payload, app=mock_app) - patched_author_deserializer.assert_called_once_with(test_user_payload, app=mock_app) - - assert message_obj.id == 123 - assert message_obj.channel_id == 456 - assert message_obj.guild_id == 678 - assert message_obj.author == mock_user - assert message_obj.member == mock_member - assert message_obj.content == "some info" - assert message_obj.timestamp == mock_datetime - assert message_obj.edited_timestamp == mock_datetime2 - assert message_obj.is_tts is True - assert message_obj.is_mentioning_everyone is True - assert message_obj.user_mentions == {5678} - assert message_obj.role_mentions == {987} - assert message_obj.channel_mentions == {456} - assert message_obj.attachments == [messages.Attachment.deserialize(test_attachment_payload)] - assert message_obj.attachments[0]._gateway_consumer is mock_app - assert message_obj.embeds == [embeds.Embed.deserialize({})] - assert message_obj.embeds[0]._gateway_consumer is mock_app - assert message_obj.is_pinned is True - assert message_obj.webhook_id == 1234 - assert message_obj.type == messages.MessageType.DEFAULT - assert message_obj.activity == messages.MessageActivity.deserialize(test_message_activity_payload) - assert message_obj.activity._gateway_consumer is mock_app - assert message_obj.application == mock_app - assert message_obj.message_reference == messages.MessageCrosspost.deserialize(test_message_crosspost_payload) - assert message_obj.message_reference._gateway_consumer is mock_app - assert message_obj.flags == messages.MessageFlag.IS_CROSSPOST - assert message_obj.nonce == "171000788183678976" - - @pytest.fixture() - def components_impl(self) -> application.Application: - return mock.MagicMock(application.Application, rest=mock.AsyncMock()) - - @pytest.fixture() - def message_obj(self, components_impl): - return messages.Message( - app=components_impl, - id=123, - channel_id=44444, - guild_id=44334, - author=None, - member=None, - content=None, - timestamp=None, - edited_timestamp=None, - is_tts=None, - is_mentioning_everyone=None, - user_mentions=[], - role_mentions=[], - attachments=[], - embeds=[], - is_pinned=None, - webhook_id=None, - type=None, - activity=None, - application=None, - message_reference=None, - flags=None, - nonce=None, - ) - - @pytest.mark.asyncio - async def test_fetch_channel(self, message_obj, components_impl): - mock_channel = mock.MagicMock(channels.GuildChannel) - components_impl.rest.fetch_channel.return_value = mock_channel - assert await message_obj.fetch_channel() is mock_channel - components_impl.rest.fetch_channel.assert_called_once_with(channel=44444) - - @pytest.mark.asyncio - async def test_edit_without_optionals(self, message_obj, components_impl): - mock_message = mock.MagicMock(messages.Message) - components_impl.rest.update_message.return_value = mock_message - assert await message_obj.edit() is mock_message - components_impl.rest.update_message.assert_called_once_with( - message=123, - channel=44444, - content=..., - embed=..., - mentions_everyone=True, - user_mentions=True, - role_mentions=True, - ) - - @pytest.mark.asyncio - async def test_edit_with_optionals(self, message_obj, components_impl): - mock_embed = mock.MagicMock(embeds.Embed) - mock_message = mock.MagicMock(messages.Message) - components_impl.rest.update_message.return_value = mock_message - result = await message_obj.edit( - content="OKOKOKOKOKOKOK", - embed=mock_embed, - mentions_everyone=False, - user_mentions=[123], - role_mentions=[456], - ) - assert result is mock_message - components_impl.rest.update_message.assert_called_once_with( - message=123, - channel=44444, - content="OKOKOKOKOKOKOK", - embed=mock_embed, - mentions_everyone=False, - user_mentions=[123], - role_mentions=[456], - ) - - @pytest.mark.asyncio - async def test_safe_edit_without_optionals(self, message_obj, components_impl): - mock_message = mock.MagicMock(messages.Message) - components_impl.rest.safe_update_message.return_value = mock_message - assert await message_obj.safe_edit() is mock_message - components_impl.rest.safe_update_message.assert_called_once_with( - message=123, - channel=44444, - content=..., - embed=..., - mentions_everyone=False, - user_mentions=False, - role_mentions=False, - ) - - @pytest.mark.asyncio - async def test_safe_edit_with_optionals(self, message_obj, components_impl): - mock_embed = mock.MagicMock(embeds.Embed) - mock_message = mock.MagicMock(messages.Message) - components_impl.rest.safe_update_message.return_value = mock_message - result = await message_obj.safe_edit( - content="OKOKOKOKOKOKOK", embed=mock_embed, mentions_everyone=True, user_mentions=[123], role_mentions=[456] - ) - assert result is mock_message - components_impl.rest.safe_update_message.assert_called_once_with( - message=123, - channel=44444, - content="OKOKOKOKOKOKOK", - embed=mock_embed, - mentions_everyone=True, - user_mentions=[123], - role_mentions=[456], - ) - - @pytest.mark.asyncio - async def test_reply_without_optionals(self, message_obj, components_impl): - mock_message = mock.MagicMock(messages.Message) - components_impl.rest.create_message.return_value = mock_message - assert await message_obj.reply() is mock_message - components_impl.rest.create_message.assert_called_once_with( - channel=44444, - content=..., - nonce=..., - tts=..., - files=..., - embed=..., - mentions_everyone=True, - user_mentions=True, - role_mentions=True, - ) - - @pytest.mark.asyncio - async def test_reply_with_optionals(self, message_obj, components_impl): - mock_file = mock.MagicMock(files.BaseStream) - mock_embed = mock.MagicMock(embeds.Embed) - mock_message = mock.MagicMock(messages.Message) - components_impl.rest.create_message.return_value = mock_message - result = await message_obj.reply( - content="blah", - nonce="blah2", - tts=True, - files=[mock_file], - embed=mock_embed, - mentions_everyone=True, - user_mentions=[123], - role_mentions=[444], - ) - assert result is mock_message - components_impl.rest.create_message.assert_called_once_with( - channel=44444, - content="blah", - nonce="blah2", - tts=True, - files=[mock_file], - embed=mock_embed, - mentions_everyone=True, - user_mentions=[123], - role_mentions=[444], - ) - - @pytest.mark.asyncio - async def test_safe_reply_without_optionals(self, message_obj, components_impl): - mock_message = mock.MagicMock(messages.Message) - components_impl.rest.safe_create_message.return_value = mock_message - assert await message_obj.safe_reply() is mock_message - components_impl.rest.safe_create_message.assert_called_once_with( - channel=44444, - content=..., - nonce=..., - tts=..., - files=..., - embed=..., - mentions_everyone=False, - user_mentions=False, - role_mentions=False, - ) - - @pytest.mark.asyncio - async def test_safe_reply_with_optionals(self, message_obj, components_impl): - mock_file = mock.MagicMock(files.BaseStream) - mock_embed = mock.MagicMock(embeds.Embed) - mock_message = mock.MagicMock(messages.Message) - components_impl.rest.safe_create_message.return_value = mock_message - result = await message_obj.safe_reply( - content="blah", - nonce="blah2", - tts=True, - files=[mock_file], - embed=mock_embed, - mentions_everyone=True, - user_mentions=[123], - role_mentions=[444], - ) - assert result is mock_message - components_impl.rest.safe_create_message.assert_called_once_with( - channel=44444, - content="blah", - nonce="blah2", - tts=True, - files=[mock_file], - embed=mock_embed, - mentions_everyone=True, - user_mentions=[123], - role_mentions=[444], - ) - - @pytest.mark.asyncio - async def test_delete(self, message_obj, components_impl): - assert await message_obj.delete() is None - components_impl.rest.delete_messages.assert_called_once_with(channel=44444, message=123) - - @pytest.mark.asyncio - async def test_add_reaction(self, message_obj, components_impl): - mock_emoji = mock.MagicMock(emojis.Emoji) - assert await message_obj.add_reaction(mock_emoji) is None - components_impl.rest.add_reaction.assert_called_once_with(channel=44444, message=123, emoji=mock_emoji) - - @pytest.mark.asyncio - async def test_add_reaction_without_user(self, message_obj, components_impl): - mock_emoji = mock.MagicMock(emojis.Emoji) - assert await message_obj.remove_reaction(mock_emoji) is None - components_impl.rest.remove_reaction.assert_called_once_with( - channel=44444, message=123, emoji=mock_emoji, user=None - ) - - @pytest.mark.asyncio - async def test_add_reaction_with_user(self, message_obj, components_impl): - mock_emoji = mock.MagicMock(emojis.Emoji) - user = mock.MagicMock(users.User) - assert await message_obj.remove_reaction(mock_emoji, user=user) is None - components_impl.rest.remove_reaction.assert_called_once_with( - channel=44444, message=123, emoji=mock_emoji, user=user - ) - - @pytest.mark.asyncio - async def test_remove_all_reactions_without_emoji(self, message_obj, components_impl): - assert await message_obj.remove_all_reactions() is None - components_impl.rest.remove_all_reactions.assert_called_once_with(channel=44444, message=123, emoji=None) - - @pytest.mark.asyncio - async def test_remove_all_reactions_with_emoji(self, message_obj, components_impl): - mock_emoji = mock.MagicMock(emojis.Emoji) - assert await message_obj.remove_all_reactions(mock_emoji) is None - components_impl.rest.remove_all_reactions.assert_called_once_with(channel=44444, message=123, emoji=mock_emoji) diff --git a/tests/hikari/models/test_users.py b/tests/hikari/models/test_users.py deleted file mode 100644 index 3653d3c158..0000000000 --- a/tests/hikari/models/test_users.py +++ /dev/null @@ -1,175 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# Copyright © Nekoka.tt 2019-2020 -# -# This file is part of Hikari. -# -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . -import mock -import pytest - -from hikari.net import rest -from hikari import application -from hikari.models import bases -from hikari.models import users -from hikari.net import urls - - -@pytest.fixture() -def test_user_payload(): - return { - "id": "115590097100865541", - "username": "nyaa", - "avatar": "b3b24c6d7cbcdec129d5d537067061a8", - "discriminator": "6127", - "bot": True, - "system": True, - "public_flags": int(users.UserFlag.VERIFIED_BOT_DEVELOPER), - } - - -@pytest.fixture() -def test_oauth_user_payload(): - return { - "id": "379953393319542784", - "username": "qt pi", - "avatar": "820d0e50543216e812ad94e6ab7", - "discriminator": "2880", - "email": "blahblah@blah.blah", - "verified": True, - "locale": "en-US", - "mfa_enabled": True, - "public_flags": int(users.UserFlag.VERIFIED_BOT_DEVELOPER), - "flags": int(users.UserFlag.DISCORD_PARTNER | users.UserFlag.DISCORD_EMPLOYEE), - "premium_type": 1, - } - - -@pytest.fixture() -def mock_app() -> application.Application: - return mock.MagicMock(application.Application, rest=mock.AsyncMock(rest.RESTClient)) - - -class TestUser: - def test_deserialize(self, test_user_payload, mock_app): - user_obj = users.User.deserialize(test_user_payload, app=mock_app) - assert user_obj.id == 115590097100865541 - assert user_obj.username == "nyaa" - assert user_obj.avatar_hash == "b3b24c6d7cbcdec129d5d537067061a8" - assert user_obj.discriminator == "6127" - assert user_obj.is_bot is True - assert user_obj.is_system is True - assert user_obj.flags == users.UserFlag.VERIFIED_BOT_DEVELOPER - - @pytest.fixture() - def user_obj(self, test_user_payload, mock_app): - return users.User( - app=mock_app, - id=bases.Snowflake(115590097100865541), - username=None, - avatar_hash="b3b24c6d7cbcdec129d5d537067061a8", - discriminator="6127", - is_bot=None, - is_system=None, - flags=None, - ) - - @pytest.mark.asyncio - async def test_fetch_self(self, user_obj, mock_app): - mock_user = mock.MagicMock(users.User) - mock_app.rest.fetch_user.return_value = mock_user - assert await user_obj.fetch_self() is mock_user - mock_app.rest.fetch_user.assert_called_once_with(user=115590097100865541) - - def test_avatar_url(self, user_obj): - mock_url = "https://cdn.discordapp.com/avatars/115590097100865541" - with mock.patch.object(urls, "generate_cdn_url", return_value=mock_url): - url = user_obj.avatar_url - urls.generate_cdn_url.assert_called_once() - assert url == mock_url - - def test_default_avatar_index(self, user_obj): - assert user_obj.default_avatar_index == 2 - - def test_default_avatar_url(self, user_obj): - mock_url = "https://cdn.discordapp.com/embed/avatars/2.png" - with mock.patch.object(urls, "generate_cdn_url", return_value=mock_url): - url = user_obj.default_avatar_url - urls.generate_cdn_url.assert_called_once_with("embed", "avatars", "2", format_="png", size=None) - assert url == mock_url - - def test_format_avatar_url_when_animated(self, user_obj): - mock_url = "https://cdn.discordapp.com/avatars/115590097100865541/a_820d0e50543216e812ad94e6ab7.gif?size=3232" - user_obj.avatar_hash = "a_820d0e50543216e812ad94e6ab7" - with mock.patch.object(urls, "generate_cdn_url", return_value=mock_url): - url = user_obj.format_avatar_url(size=3232) - urls.generate_cdn_url.assert_called_once_with( - "avatars", "115590097100865541", "a_820d0e50543216e812ad94e6ab7", format_="gif", size=3232 - ) - assert url == mock_url - - def test_format_avatar_url_default(self, user_obj): - user_obj.avatar_hash = None - mock_url = "https://cdn.discordapp.com/embed/avatars/2.png" - with mock.patch.object(urls, "generate_cdn_url", return_value=mock_url): - url = user_obj.format_avatar_url(size=3232) - urls.generate_cdn_url.assert_called_once_with("embed", "avatars", "2", format_="png", size=None) - assert url == mock_url - - def test_format_avatar_url_when_format_specified(self, user_obj): - mock_url = "https://cdn.discordapp.com/avatars/115590097100865541/b3b24c6d7c37067061a8.nyaapeg?size=1024" - with mock.patch.object(urls, "generate_cdn_url", return_value=mock_url): - url = user_obj.format_avatar_url(format_="nyaapeg", size=1024) - urls.generate_cdn_url.assert_called_once_with( - "avatars", "115590097100865541", "b3b24c6d7cbcdec129d5d537067061a8", format_="nyaapeg", size=1024 - ) - assert url == mock_url - - -class TestMyUser: - def test_deserialize(self, test_oauth_user_payload, mock_app): - my_user_obj = users.MyUser.deserialize(test_oauth_user_payload, app=mock_app) - assert my_user_obj.id == 379953393319542784 - assert my_user_obj.username == "qt pi" - assert my_user_obj.avatar_hash == "820d0e50543216e812ad94e6ab7" - assert my_user_obj.discriminator == "2880" - assert my_user_obj.is_mfa_enabled is True - assert my_user_obj.locale == "en-US" - assert my_user_obj.is_verified is True - assert my_user_obj.email == "blahblah@blah.blah" - assert my_user_obj.flags == users.UserFlag.DISCORD_PARTNER | users.UserFlag.DISCORD_EMPLOYEE - assert my_user_obj.premium_type is users.PremiumType.NITRO_CLASSIC - - @pytest.fixture() - def my_user_obj(self, mock_app): - return users.MyUser( - app=mock_app, - id=None, - username=None, - avatar_hash=None, - discriminator=None, - is_mfa_enabled=None, - locale=None, - is_verified=None, - email=None, - flags=None, - premium_type=None, - ) - - @pytest.mark.asyncio - async def test_fetch_me(self, my_user_obj, mock_app): - mock_user = mock.MagicMock(users.MyUser) - mock_app.rest.fetch_me.return_value = mock_user - assert await my_user_obj.fetch_self() is mock_user - mock_app.rest.fetch_me.assert_called_once() diff --git a/tests/hikari/models/test_voices.py b/tests/hikari/models/test_voices.py deleted file mode 100644 index b41ecc3d11..0000000000 --- a/tests/hikari/models/test_voices.py +++ /dev/null @@ -1,73 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# Copyright © Nekoka.tt 2019-2020 -# -# This file is part of Hikari. -# -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . -import mock -import pytest - -from hikari import application -from hikari.models import voices - - -@pytest.fixture() -def voice_state_payload(): - return { - "guild_id": "929292929292992", - "channel": "157733188964188161", - "user_id": "80351110224678912", - "session_id": "90326bd25d71d39b9ef95b299e3872ff", - "deaf": True, - "mute": True, - "self_deaf": False, - "self_mute": True, - "suppress": False, - } - - -@pytest.fixture() -def voice_region_payload(): - return {"id": "london", "name": "LONDON", "vip": True, "optimal": False, "deprecated": True, "custom": False} - - -@pytest.fixture() -def mock_app(): - return mock.MagicMock(application.Application) - - -class TestVoiceState: - def test_deserialize(self, voice_state_payload, mock_app): - voice_state_obj = voices.VoiceState.deserialize(voice_state_payload, app=mock_app) - assert voice_state_obj.guild_id == 929292929292992 - assert voice_state_obj.channel_id == 157733188964188161 - assert voice_state_obj.user_id == 80351110224678912 - assert voice_state_obj.session_id == "90326bd25d71d39b9ef95b299e3872ff" - assert voice_state_obj.is_guild_deafened is True - assert voice_state_obj.is_guild_muted is True - assert voice_state_obj.is_self_deafened is False - assert voice_state_obj.is_self_muted is True - assert voice_state_obj.is_suppressed is False - - -class TestVoiceRegion: - def test_deserialize(self, voice_region_payload, mock_app): - voice_region_obj = voices.VoiceRegion.deserialize(voice_region_payload, app=mock_app) - assert voice_region_obj.id == "london" - assert voice_region_obj.name == "LONDON" - assert voice_region_obj.is_vip is True - assert voice_region_obj.is_optimal_location is False - assert voice_region_obj.is_deprecated is True - assert voice_region_obj.is_custom is False diff --git a/tests/hikari/models/test_webhook.py b/tests/hikari/models/test_webhook.py deleted file mode 100644 index d162947f9f..0000000000 --- a/tests/hikari/models/test_webhook.py +++ /dev/null @@ -1,369 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# Copyright © Nekoka.tt 2019-2020 -# -# This file is part of Hikari. -# -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . -import mock -import pytest - -from hikari.net import rest -from hikari import application -from hikari.net import urls -from hikari.models import bases -from hikari.models import channels -from hikari.models import embeds -from hikari.models import files -from hikari.models import messages -from hikari.models import users -from hikari.models import webhooks -from tests.hikari import _helpers - - -@pytest.fixture() -def mock_app() -> application.Application: - return mock.MagicMock(application.Application, rest=mock.AsyncMock(rest.RESTClient)) - - -class TestWebhook: - def test_deserialize(self, mock_app): - test_user_payload = {"id": "123456", "username": "hikari", "discriminator": "0000", "avatar": None} - payload = { - "id": "1234", - "type": 1, - "guild_id": "123", - "channel": "456", - "user": test_user_payload, - "name": "hikari webhook", - "avatar": "bb71f469c158984e265093a81b3397fb", - "token": "ueoqrialsdfaKJLKfajslkdf", - } - mock_user = mock.MagicMock(users.User) - - with _helpers.patch_marshal_attr( - webhooks.Webhook, "author", deserializer=users.User.deserialize, return_value=mock_user - ) as mock_user_deserializer: - webhook_obj = webhooks.Webhook.deserialize(payload, app=mock_app) - mock_user_deserializer.assert_called_once_with(test_user_payload, app=mock_app) - - assert webhook_obj.id == 1234 - assert webhook_obj.type == webhooks.WebhookType.INCOMING - assert webhook_obj.guild_id == 123 - assert webhook_obj.channel_id == 456 - assert webhook_obj.author is mock_user - assert webhook_obj.name == "hikari webhook" - assert webhook_obj.avatar_hash == "bb71f469c158984e265093a81b3397fb" - assert webhook_obj.token == "ueoqrialsdfaKJLKfajslkdf" - - @pytest.fixture() - def webhook_obj(self, mock_app): - return webhooks.Webhook( - app=mock_app, - id=bases.Snowflake(123123), - type=None, - guild_id=None, - channel_id=None, - author=None, - name=None, - avatar_hash="b3b24c6d7cbcdec129d5d537067061a8", - token="blah.blah.blah", - ) - - @pytest.mark.asyncio - async def test_execute_without_optionals(self, webhook_obj, mock_app): - mock_webhook = mock.MagicMock(messages.Message) - mock_app.rest.execute_webhook.return_value = mock_webhook - assert await webhook_obj.execute() is mock_webhook - mock_app.rest.execute_webhook.assert_called_once_with( - webhook=123123, - webhook_token="blah.blah.blah", - content=..., - username=..., - avatar_url=..., - tts=..., - wait=False, - files=..., - embeds=..., - mentions_everyone=True, - user_mentions=True, - role_mentions=True, - ) - - @pytest.mark.asyncio - async def test_execute_with_optionals(self, webhook_obj, mock_app): - mock_webhook = mock.MagicMock(messages.Message) - mock_files = mock.MagicMock(files.BaseStream) - mock_embed = mock.MagicMock(embeds.Embed) - mock_app.rest.execute_webhook.return_value = mock_webhook - result = await webhook_obj.execute( - content="A CONTENT", - username="Name user", - avatar_url=">///<", - tts=True, - wait=True, - files=[mock_files], - embeds=[mock_embed], - mentions_everyone=False, - user_mentions=[123, 456], - role_mentions=[444], - ) - assert result is mock_webhook - mock_app.rest.execute_webhook.assert_called_once_with( - webhook=123123, - webhook_token="blah.blah.blah", - content="A CONTENT", - username="Name user", - avatar_url=">///<", - tts=True, - wait=True, - files=[mock_files], - embeds=[mock_embed], - mentions_everyone=False, - user_mentions=[123, 456], - role_mentions=[444], - ) - - @_helpers.assert_raises(type_=ValueError) - @pytest.mark.asyncio - async def test_execute_raises_value_error_without_token(self, webhook_obj): - webhook_obj.token = None - await webhook_obj.execute() - - @pytest.mark.asyncio - async def test_safe_execute_without_optionals(self, webhook_obj, mock_app): - mock_webhook = mock.MagicMock(messages.Message) - mock_app.rest.safe_webhook_execute = mock.AsyncMock(return_value=mock_webhook) - assert await webhook_obj.safe_execute() is mock_webhook - mock_app.rest.safe_webhook_execute.assert_called_once_with( - webhook=123123, - webhook_token="blah.blah.blah", - content=..., - username=..., - avatar_url=..., - tts=..., - wait=False, - files=..., - embeds=..., - mentions_everyone=False, - user_mentions=False, - role_mentions=False, - ) - - @pytest.mark.asyncio - async def test_safe_execute_with_optionals(self, webhook_obj, mock_app): - mock_webhook = mock.MagicMock(messages.Message) - mock_files = mock.MagicMock(files.BaseStream) - mock_embed = mock.MagicMock(embeds.Embed) - mock_app.rest.safe_webhook_execute = mock.AsyncMock(return_value=mock_webhook) - result = await webhook_obj.safe_execute( - content="A CONTENT", - username="Name user", - avatar_url=">///<", - tts=True, - wait=True, - files=[mock_files], - embeds=[mock_embed], - mentions_everyone=False, - user_mentions=[123, 456], - role_mentions=[444], - ) - assert result is mock_webhook - mock_app.rest.safe_webhook_execute.assert_called_once_with( - webhook=123123, - webhook_token="blah.blah.blah", - content="A CONTENT", - username="Name user", - avatar_url=">///<", - tts=True, - wait=True, - files=[mock_files], - embeds=[mock_embed], - mentions_everyone=False, - user_mentions=[123, 456], - role_mentions=[444], - ) - - @_helpers.assert_raises(type_=ValueError) - @pytest.mark.asyncio - async def test_safe_execute_raises_value_error_without_token(self, webhook_obj): - webhook_obj.token = None - await webhook_obj.safe_execute() - - @pytest.mark.asyncio - async def test_delete_with_token(self, webhook_obj, mock_app): - mock_app.rest.delete_webhook.return_value = ... - assert await webhook_obj.delete() is None - mock_app.rest.delete_webhook.assert_called_once_with(webhook=123123, webhook_token="blah.blah.blah") - - @pytest.mark.asyncio - async def test_delete_without_token(self, webhook_obj, mock_app): - webhook_obj.token = None - mock_app.rest.delete_webhook.return_value = ... - assert await webhook_obj.delete() is None - mock_app.rest.delete_webhook.assert_called_once_with(webhook=123123, webhook_token=...) - - @pytest.mark.asyncio - async def test_delete_with_use_token_set_to_true(self, webhook_obj, mock_app): - mock_app.rest.delete_webhook.return_value = ... - assert await webhook_obj.delete(use_token=True) is None - mock_app.rest.delete_webhook.assert_called_once_with(webhook=123123, webhook_token="blah.blah.blah") - - @pytest.mark.asyncio - async def test_delete_with_use_token_set_to_false(self, webhook_obj, mock_app): - mock_app.rest.delete_webhook.return_value = ... - assert await webhook_obj.delete(use_token=False) is None - mock_app.rest.delete_webhook.assert_called_once_with(webhook=123123, webhook_token=...) - - @_helpers.assert_raises(type_=ValueError) - @pytest.mark.asyncio - async def test_delete_raises_value_error_when_use_token_set_to_true_without_token(self, webhook_obj, mock_app): - webhook_obj.token = None - await webhook_obj.delete(use_token=True) - - @pytest.mark.asyncio - async def test_edit_without_optionals_nor_token(self, webhook_obj, mock_app): - webhook_obj.token = None - mock_webhook = mock.MagicMock(webhooks.Webhook) - mock_app.rest.update_webhook.return_value = mock_webhook - assert await webhook_obj.edit() is mock_webhook - mock_app.rest.update_webhook.assert_called_once_with( - webhook=123123, webhook_token=..., name=..., avatar=..., channel=..., reason=... - ) - - @pytest.mark.asyncio - async def test_edit_with_optionals_and_token(self, webhook_obj, mock_app): - mock_webhook = mock.MagicMock(webhooks.Webhook) - mock_avatar = mock.MagicMock(files.BaseStream) - mock_channel = mock.MagicMock(channels.GuildChannel) - mock_app.rest.update_webhook.return_value = mock_webhook - result = await webhook_obj.edit(name="A name man", avatar=mock_avatar, channel=mock_channel, reason="xd420") - assert result is mock_webhook - mock_app.rest.update_webhook.assert_called_once_with( - webhook=123123, - webhook_token="blah.blah.blah", - name="A name man", - avatar=mock_avatar, - channel=mock_channel, - reason="xd420", - ) - - @pytest.mark.asyncio - async def test_edit_with_use_token_set_to_true(self, webhook_obj, mock_app): - mock_webhook = mock.MagicMock(webhooks.Webhook) - mock_app.rest.update_webhook.return_value = mock_webhook - assert await webhook_obj.edit(use_token=True) is mock_webhook - mock_app.rest.update_webhook.assert_called_once_with( - webhook=123123, webhook_token="blah.blah.blah", name=..., avatar=..., channel=..., reason=... - ) - - @pytest.mark.asyncio - async def test_edit_with_use_token_set_to_false(self, webhook_obj, mock_app): - mock_webhook = mock.MagicMock(webhooks.Webhook) - mock_app.rest.update_webhook.return_value = mock_webhook - assert await webhook_obj.edit(use_token=False) is mock_webhook - mock_app.rest.update_webhook.assert_called_once_with( - webhook=123123, webhook_token=..., name=..., avatar=..., channel=..., reason=... - ) - - @_helpers.assert_raises(type_=ValueError) - @pytest.mark.asyncio - async def test_edit_raises_value_error_when_use_token_set_to_true_without_token(self, webhook_obj, mock_app): - webhook_obj.token = None - await webhook_obj.edit(use_token=True) - - @pytest.mark.asyncio - async def test_fetch_channel(self, webhook_obj, mock_app): - webhook_obj.channel_id = bases.Snowflake(202020) - mock_channel = mock.MagicMock(channels.GuildChannel) - mock_app.rest.fetch_channel.return_value = mock_channel - assert await webhook_obj.fetch_channel() is mock_channel - mock_app.rest.fetch_channel.assert_called_once_with(channel=202020) - - @pytest.mark.asyncio - async def test_fetch_guild(self, webhook_obj, mock_app): - webhook_obj.guild_id = bases.Snowflake(202020) - mock_channel = mock.MagicMock(channels.GuildChannel) - mock_app.rest.fetch_guild.return_value = mock_channel - assert await webhook_obj.fetch_guild() is mock_channel - mock_app.rest.fetch_guild.assert_called_once_with(guild=202020) - - @pytest.mark.asyncio - async def test_fetch_self_with_token(self, webhook_obj, mock_app): - mock_webhook = mock.MagicMock(webhooks.Webhook) - mock_app.rest.fetch_webhook.return_value = mock_webhook - assert await webhook_obj.fetch_self() is mock_webhook - mock_app.rest.fetch_webhook.assert_called_once_with(webhook=123123, webhook_token="blah.blah.blah") - - @pytest.mark.asyncio - async def test_fetch_self_without_token(self, webhook_obj, mock_app): - webhook_obj.token = None - mock_webhook = mock.MagicMock(webhooks.Webhook) - mock_app.rest.fetch_webhook.return_value = mock_webhook - assert await webhook_obj.fetch_self() is mock_webhook - mock_app.rest.fetch_webhook.assert_called_once_with(webhook=123123, webhook_token=...) - - @pytest.mark.asyncio - async def test_fetch_self_with_use_token_set_to_true(self, webhook_obj, mock_app): - mock_webhook = mock.MagicMock(webhooks.Webhook) - mock_app.rest.fetch_webhook.return_value = mock_webhook - assert await webhook_obj.fetch_self(use_token=True) is mock_webhook - mock_app.rest.fetch_webhook.assert_called_once_with(webhook=123123, webhook_token="blah.blah.blah") - - @pytest.mark.asyncio - async def test_fetch_self_with_use_token_set_to_false(self, webhook_obj, mock_app): - mock_webhook = mock.MagicMock(webhooks.Webhook) - mock_app.rest.fetch_webhook.return_value = mock_webhook - assert await webhook_obj.fetch_self(use_token=False) is mock_webhook - mock_app.rest.fetch_webhook.assert_called_once_with(webhook=123123, webhook_token=...) - - @_helpers.assert_raises(type_=ValueError) - @pytest.mark.asyncio - async def test_fetch_self_raises_value_error_when_use_token_set_to_true_without_token(self, webhook_obj, mock_app): - webhook_obj.token = None - assert await webhook_obj.fetch_self(use_token=True) - - def test_avatar_url(self, webhook_obj): - mock_url = "https://cdn.discordapp.com/avatars/115590097100865541" - with mock.patch.object(urls, "generate_cdn_url", return_value=mock_url): - url = webhook_obj.avatar_url - urls.generate_cdn_url.assert_called_once() - assert url == mock_url - - def test_test_default_avatar_index(self, webhook_obj): - assert webhook_obj.default_avatar_index == 0 - - def test_default_avatar_url(self, webhook_obj): - mock_url = "https://cdn.discordapp.com/embed/avatars/2.png" - with mock.patch.object(urls, "generate_cdn_url", return_value=mock_url): - url = webhook_obj.default_avatar_url - urls.generate_cdn_url.assert_called_once_with("embed", "avatars", "0", format_="png", size=None) - assert url == mock_url - - def test_format_avatar_url_default(self, webhook_obj): - webhook_obj.avatar_hash = None - mock_url = "https://cdn.discordapp.com/embed/avatars/2.png" - with mock.patch.object(urls, "generate_cdn_url", return_value=mock_url): - url = webhook_obj.format_avatar_url(size=3232) - urls.generate_cdn_url.assert_called_once_with("embed", "avatars", "0", format_="png", size=None) - assert url == mock_url - - def test_format_avatar_url_when_format_specified(self, webhook_obj): - mock_url = "https://cdn.discordapp.com/avatars/115590097100865541/b3b24c6d7c37067061a8.nyaapeg?size=1024" - with mock.patch.object(urls, "generate_cdn_url", return_value=mock_url): - url = webhook_obj.format_avatar_url(format_="nyaapeg", size=1024) - urls.generate_cdn_url.assert_called_once_with( - "avatars", "123123", "b3b24c6d7cbcdec129d5d537067061a8", format_="nyaapeg", size=1024 - ) - assert url == mock_url diff --git a/tests/hikari/models/__init__.py b/tests/hikari/net/test_buckets.py similarity index 100% rename from tests/hikari/models/__init__.py rename to tests/hikari/net/test_buckets.py diff --git a/tests/hikari/models/test_colours.py b/tests/hikari/net/test_gateway.py similarity index 82% rename from tests/hikari/models/test_colours.py rename to tests/hikari/net/test_gateway.py index 66d984c080..1c1502a5ca 100644 --- a/tests/hikari/models/test_colours.py +++ b/tests/hikari/net/test_gateway.py @@ -16,12 +16,3 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . -import pytest - -from hikari.models import colors -from hikari.models import colours - - -@pytest.mark.model -def test_colours(): - assert colors.Color is colours.Colour diff --git a/tests/hikari/net/test_http_client.py b/tests/hikari/net/test_http_client.py index ef4f308928..898693a453 100644 --- a/tests/hikari/net/test_http_client.py +++ b/tests/hikari/net/test_http_client.py @@ -23,8 +23,8 @@ import mock import pytest -from hikari.net import tracing from hikari.net import http_client +from hikari.net import tracing from tests.hikari import _helpers @@ -38,19 +38,19 @@ def client_session(): @pytest.fixture def client(client_session): assert client_session, "this param is needed, it ensures aiohttp is patched for the test" - client = http_client.HTTPClient() + client = http_client.HTTPClient(mock.MagicMock()) yield client @pytest.mark.asyncio class TestInit: async def test_CFRayTracer_used_for_non_debug(self): - async with http_client.HTTPClient(debug=False) as client: + async with http_client.HTTPClient(debug=False, logger=mock.MagicMock()) as client: assert len(client._tracers) == 1 assert isinstance(client._tracers[0], tracing.CFRayTracer) async def test_DebugTracer_used_for_debug(self): - async with http_client.HTTPClient(debug=True) as client: + async with http_client.HTTPClient(debug=True, logger=mock.MagicMock()) as client: assert len(client._tracers) == 1 assert isinstance(client._tracers[0], tracing.DebugTracer) diff --git a/tests/hikari/models/test_intents.py b/tests/hikari/net/test_iterators.py similarity index 74% rename from tests/hikari/models/test_intents.py rename to tests/hikari/net/test_iterators.py index a47141aaf0..1c1502a5ca 100644 --- a/tests/hikari/models/test_intents.py +++ b/tests/hikari/net/test_iterators.py @@ -16,12 +16,3 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . -from hikari.models import intents - - -class TestIntent: - def test_is_privileged(self): - assert intents.Intent.GUILD_MEMBERS.is_privileged - - def test_not_is_privileged(self): - assert not intents.Intent.DIRECT_MESSAGE_TYPING.is_privileged diff --git a/tests/hikari/internal/test_ratelimits.py b/tests/hikari/net/test_ratelimits.py similarity index 99% rename from tests/hikari/internal/test_ratelimits.py rename to tests/hikari/net/test_ratelimits.py index aa5002b0d8..6fa065060b 100644 --- a/tests/hikari/internal/test_ratelimits.py +++ b/tests/hikari/net/test_ratelimits.py @@ -26,7 +26,7 @@ import mock import pytest -from hikari.internal import ratelimits +from hikari.net import ratelimits from tests.hikari import _helpers diff --git a/tests/hikari/internal/test_more_enums.py b/tests/hikari/net/test_rest.py similarity index 58% rename from tests/hikari/internal/test_more_enums.py rename to tests/hikari/net/test_rest.py index a662277162..1c1502a5ca 100644 --- a/tests/hikari/internal/test_more_enums.py +++ b/tests/hikari/net/test_rest.py @@ -16,32 +16,3 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . - -from hikari.internal import more_enums - - -class TestEnumMixin: - def test_str(self): - class TestType(more_enums.Enum): - a = 1 - b = 2 - c = 4 - d = 8 - e = 16 - - inst = TestType(2) - assert str(inst) == "b" - - -class TestFlagMixin: - def test_str(self): - class TestType(more_enums.IntFlag): - a = 1 - b = 2 - c = 4 - d = 8 - e = 16 - - inst = TestType(7) - - assert str(inst) == "a, b, c" diff --git a/tests/hikari/internal/test_more_typing.py b/tests/hikari/net/test_rest_utils.py similarity index 55% rename from tests/hikari/internal/test_more_typing.py rename to tests/hikari/net/test_rest_utils.py index e07d6967fc..1c1502a5ca 100644 --- a/tests/hikari/internal/test_more_typing.py +++ b/tests/hikari/net/test_rest_utils.py @@ -16,30 +16,3 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . -import asyncio - -import pytest - -from hikari.internal import more_typing - - -# noinspection PyProtocol -@pytest.mark.asyncio -class TestFuture: - async def test_is_instance(self, event_loop): - assert isinstance(event_loop.create_future(), more_typing.Future) - - async def nil(): - pass - - assert isinstance(asyncio.create_task(nil()), more_typing.Future) - - -# noinspection PyProtocol -@pytest.mark.asyncio -class TestTask: - async def test_is_instance(self, event_loop): - async def nil(): - pass - - assert isinstance(asyncio.create_task(nil()), more_typing.Task) diff --git a/tests/hikari/net/test_routes.py b/tests/hikari/net/test_routes.py new file mode 100644 index 0000000000..1c1502a5ca --- /dev/null +++ b/tests/hikari/net/test_routes.py @@ -0,0 +1,18 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# Copyright © Nekoka.tt 2019-2020 +# +# This file is part of Hikari. +# +# Hikari is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Hikari is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Hikari. If not, see . diff --git a/tests/hikari/net/test_tracing.py b/tests/hikari/net/test_tracing.py new file mode 100644 index 0000000000..1c1502a5ca --- /dev/null +++ b/tests/hikari/net/test_tracing.py @@ -0,0 +1,18 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# Copyright © Nekoka.tt 2019-2020 +# +# This file is part of Hikari. +# +# Hikari is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Hikari is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Hikari. If not, see . diff --git a/tests/hikari/internal/test_urls.py b/tests/hikari/net/test_urls.py similarity index 100% rename from tests/hikari/internal/test_urls.py rename to tests/hikari/net/test_urls.py diff --git a/tests/hikari/internal/test_user_agents.py b/tests/hikari/net/test_user_agents.py similarity index 100% rename from tests/hikari/internal/test_user_agents.py rename to tests/hikari/net/test_user_agents.py From 4902f6767ae46e24fce45e184758de51d5a4b565 Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Fri, 29 May 2020 20:45:50 +0100 Subject: [PATCH 408/922] Alterred rest.py ratelimit logic to handle Discord sending random ratelimits they didn't tell us about in responses randomly. --- hikari/errors.py | 3 +- hikari/net/rest.py | 97 ++++++++++++++++++++++++++++++---------------- 2 files changed, 65 insertions(+), 35 deletions(-) diff --git a/hikari/errors.py b/hikari/errors.py index 2a5776fb24..df4028e20d 100644 --- a/hikari/errors.py +++ b/hikari/errors.py @@ -195,8 +195,9 @@ def __init__( status: typing.Union[int, http.HTTPStatus], headers: aiohttp.typedefs.LooseHeaders, raw_body: typing.Any, + reason: typing.Optional[str] = None, ) -> None: - super().__init__(url, f"{status}: {raw_body}") + super().__init__(url, f"{status}: {raw_body}" if reason is None else reason) self.status = status self.headers = headers self.raw_body = raw_body diff --git a/hikari/net/rest.py b/hikari/net/rest.py index 23d35686e8..9d157b0e51 100644 --- a/hikari/net/rest.py +++ b/hikari/net/rest.py @@ -211,16 +211,15 @@ async def _request_once( if response.status == http.HTTPStatus.NO_CONTENT: return None - # Decode the body. - raw_body = await response.read() - # Handle the response. if 200 <= response.status < 300: if response.content_type == self._APPLICATION_JSON: # Only deserializing here stops Cloudflare shenanigans messing us around. - return json.loads(raw_body) + return await response.json() raise errors.HTTPError(real_url, f"Expected JSON response but received {response.content_type}") + raw_body = await response.read() + if response.status == http.HTTPStatus.BAD_REQUEST: raise errors.BadRequest(real_url, response.headers, raw_body) if response.status == http.HTTPStatus.UNAUTHORIZED: @@ -253,9 +252,65 @@ async def _handle_rate_limits_for_response( limit = int(resp_headers.get("x-ratelimit-limit", "1")) remaining = int(resp_headers.get("x-ratelimit-remaining", "1")) bucket = resp_headers.get("x-ratelimit-bucket", "None") - reset = float(resp_headers.get("x-ratelimit-reset", "0")) - reset_date = datetime.datetime.fromtimestamp(reset, tz=datetime.timezone.utc) + reset_at = float(resp_headers.get("x-ratelimit-reset", "0")) now_date = conversions.rfc7231_datetime_string_to_datetime(resp_headers["date"]) + + is_rate_limited = response.status == http.HTTPStatus.TOO_MANY_REQUESTS + + if is_rate_limited: + if response.content_type != self._APPLICATION_JSON: + # We don't know exactly what this could imply. It is likely Cloudflare interfering + # but I'd rather we just give up than do something resulting in multiple failed + # requests repeatedly. + raise errors.HTTPErrorResponse( + str(response.real_url), + http.HTTPStatus.TOO_MANY_REQUESTS, + response.headers, + await response.read(), + f"received rate limited response with unexpected response type {response.content_type}", + ) + + body = await response.json() + + body_retry_after = float(body["retry_after"]) / 1_000 + + if body.get("global", False): + self.global_rate_limit.throttle(body_retry_after) + + self.logger.warning("you are being rate-limited globally - trying again after %ss", body_retry_after) + else: + # Discord can do a messed up thing where the headers suggest we aren't rate limited, + # but we still get 429s with a different rate limit. + # If this occurs, we need to take the rate limit that is furthest in the future + # to avoid excessive 429ing everywhere repeatedly, causing an API ban, + # since our logic assumes the rate limit info they give us is actually + # remotely correct. + # + # At the time of writing, editing a channel more than twice per 10 minutes seems + # to trigger this, which makes me nervous that the info we are receiving isn't + # correct, but whatever... this is the best we can do. + + header_reset_at = reset_at + body_retry_at = now_date.timestamp() + body_retry_after + + # Pick the value that is the furthest in the future. + reset_at = max(body_retry_at, header_reset_at) + + self.logger.warning( + "you are being rate-limited on bucket %s for route %s - trying again after %ss " + "(headers suggest %ss back-off finishing at %s; rate-limited response specifies %ss " + "back-off finishing at %s)", + bucket, + compiled_route, + reset_at, + header_reset_at - now_date.timestamp(), + header_reset_at, + body_retry_after, + body_retry_at, + ) + + reset_date = datetime.datetime.fromtimestamp(reset_at, tz=datetime.timezone.utc) + self.buckets.update_rate_limits( compiled_route=compiled_route, bucket_header=bucket, @@ -265,34 +320,8 @@ async def _handle_rate_limits_for_response( reset_at_header=reset_date, ) - if response.status == http.HTTPStatus.TOO_MANY_REQUESTS: - body = await response.json() if response.content_type == self._APPLICATION_JSON else await response.read() - - # We are being rate limited. - if isinstance(body, dict): - if body.get("global", False): - retry_after = float(body["retry_after"]) / 1_000 - self.global_rate_limit.throttle(retry_after) - - self.logger.warning( - "you are being rate-limited globally - trying again after %ss", retry_after, - ) - else: - self.logger.warning( - "you are being rate-limited on bucket %s for _route %s - trying again after %ss", - bucket, - compiled_route, - reset, - ) - - raise self._RateLimited() - - # We might find out Cloudflare causes this scenario to occur. - # I hope we don't though. - raise errors.HTTPError( - str(response.real_url), - f"We were rate limited but did not understand the response. Perhaps Cloudflare did this? {body!r}", - ) + if is_rate_limited: + raise self._RateLimited() @staticmethod def _generate_allowed_mentions( From bf508d9d0760cbd6d3c11f7cc4beffc816dbef7b Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Fri, 29 May 2020 20:53:07 +0100 Subject: [PATCH 409/922] Alterred logic flow in ratelimit handler for REST a bit... --- hikari/net/rest.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/hikari/net/rest.py b/hikari/net/rest.py index 9d157b0e51..20ecd756f5 100644 --- a/hikari/net/rest.py +++ b/hikari/net/rest.py @@ -253,6 +253,7 @@ async def _handle_rate_limits_for_response( remaining = int(resp_headers.get("x-ratelimit-remaining", "1")) bucket = resp_headers.get("x-ratelimit-bucket", "None") reset_at = float(resp_headers.get("x-ratelimit-reset", "0")) + reset_date = datetime.datetime.fromtimestamp(reset_at, tz=datetime.timezone.utc) now_date = conversions.rfc7231_datetime_string_to_datetime(resp_headers["date"]) is_rate_limited = response.status == http.HTTPStatus.TOO_MANY_REQUESTS @@ -293,8 +294,9 @@ async def _handle_rate_limits_for_response( header_reset_at = reset_at body_retry_at = now_date.timestamp() + body_retry_after - # Pick the value that is the furthest in the future. - reset_at = max(body_retry_at, header_reset_at) + if body_retry_at > header_reset_at: + reset_date = datetime.datetime.fromtimestamp(body_retry_at, tz=datetime.timezone.utc) + self.logger.warning( "you are being rate-limited on bucket %s for route %s - trying again after %ss " @@ -309,8 +311,6 @@ async def _handle_rate_limits_for_response( body_retry_at, ) - reset_date = datetime.datetime.fromtimestamp(reset_at, tz=datetime.timezone.utc) - self.buckets.update_rate_limits( compiled_route=compiled_route, bucket_header=bucket, From a96b76a53b49779302aea3daedcbd5da1bb77908 Mon Sep 17 00:00:00 2001 From: Faster Speeding Date: Sat, 30 May 2020 04:56:11 +0100 Subject: [PATCH 410/922] Add entity factory logic for handling models. --- hikari/cache.py | 2 +- hikari/entity_factory.py | 543 ++++- hikari/events/channel.py | 165 +- hikari/events/guild.py | 85 +- hikari/events/message.py | 210 +- hikari/events/other.py | 31 +- hikari/events/voice.py | 8 +- hikari/impl/cache.py | 2 +- hikari/impl/entity_factory.py | 1563 +++++++++++++- hikari/models/applications.py | 176 +- hikari/models/audit_logs.py | 271 +-- hikari/models/bases.py | 34 +- hikari/models/channels.py | 355 +--- hikari/models/embeds.py | 200 +- hikari/models/emojis.py | 83 +- hikari/models/files.py | 7 +- hikari/models/gateway.py | 7 +- hikari/models/guilds.py | 692 ++----- hikari/models/intents.py | 6 +- hikari/models/invites.py | 109 +- hikari/models/messages.py | 217 +- hikari/models/permissions.py | 6 +- hikari/models/users.py | 67 +- hikari/models/voices.py | 57 +- hikari/models/webhooks.py | 43 +- hikari/net/rest.py | 18 +- hikari/utilities/aio.py | 2 +- tests/hikari/impl/__init__.py | 18 + tests/hikari/impl/test_entity_factory.py | 2422 ++++++++++++++++++++++ 29 files changed, 5240 insertions(+), 2159 deletions(-) create mode 100644 tests/hikari/impl/__init__.py create mode 100644 tests/hikari/impl/test_entity_factory.py diff --git a/hikari/cache.py b/hikari/cache.py index d24b3e46f9..1e83bf445c 100644 --- a/hikari/cache.py +++ b/hikari/cache.py @@ -209,7 +209,7 @@ async def create_guild_ban(self, payload: binding.JSONObject) -> guilds.GuildMem ... @abc.abstractmethod - async def create_guild_integration(self, payload: binding.JSONObject) -> guilds.PartialGuildIntegration: + async def create_guild_integration(self, payload: binding.JSONObject) -> guilds.PartialIntegration: ... @abc.abstractmethod diff --git a/hikari/entity_factory.py b/hikari/entity_factory.py index 229b326491..8f8a8ec09e 100644 --- a/hikari/entity_factory.py +++ b/hikari/entity_factory.py @@ -54,15 +54,48 @@ class IEntityFactory(component.IComponent, abc.ABC): @abc.abstractmethod def deserialize_own_connection(self, payload: binding.JSONObject) -> applications.OwnConnection: - ... + """Parse a raw payload from Discord into an own connection object. + + Parameters + ---------- + payload : Mapping[Hashable, Any] + The dict payload to parse. + + Returns + ------- + hikari.models.applications.OwnConnection + The parsed own connection object. + """ @abc.abstractmethod def deserialize_own_guild(self, payload: binding.JSONObject) -> applications.OwnGuild: - ... + """Parse a raw payload from Discord into an own guild object. + + Parameters + ---------- + payload : Mapping[Hashable, Any] + The dict payload to parse. + + Returns + ------- + hikari.models.applications.OwnGuild + The parsed own guild object. + """ @abc.abstractmethod - def deserialize_application(self, payload: binding.JSONObject) -> applications: - ... + def deserialize_application(self, payload: binding.JSONObject) -> applications.Application: + """Parse a raw payload from Discord into an application object. + + Parameters + ---------- + payload : Mapping[Hashable, Any] + The dict payload to parse. + + Returns + ------- + hikari.models.applications.Application + The parsed application object. + """ ############## # AUDIT_LOGS # @@ -70,7 +103,18 @@ def deserialize_application(self, payload: binding.JSONObject) -> applications: @abc.abstractmethod def deserialize_audit_log(self, payload: binding.JSONObject) -> audit_logs.AuditLog: - ... + """Parse a raw payload from Discord into an audit log object. + + Parameters + ---------- + payload : Mapping[Hashable, Any] + The dict payload to parse. + + Returns + ------- + hikari.models.audit_logs.AuditLog + The parsed audit log object. + """ ############ # CHANNELS # @@ -78,47 +122,168 @@ def deserialize_audit_log(self, payload: binding.JSONObject) -> audit_logs.Audit @abc.abstractmethod def deserialize_permission_overwrite(self, payload: binding.JSONObject) -> channels.PermissionOverwrite: - ... + """Parse a raw payload from Discord into a permission overwrite object. + + Parameters + ---------- + payload : Mapping[Hashable, Any] + The dict payload to parse. + + Returns + ------- + hikari.models.channels.PermissionOverwrote + The parsed permission overwrite object. + """ @abc.abstractmethod def serialize_permission_overwrite(self, overwrite: channels.PermissionOverwrite) -> binding.JSONObject: - ... + """Serialize a permission overwrite object to a json serializable dict. + + Parameters + ---------- + overwrite : hikari.models.channels.PermissionOverwrite + The permission overwrite object to serialize. + + Returns + ------- + Dict[Hashable, Any] + The dict representation of the permission overwrite object provided. + """ @abc.abstractmethod def deserialize_partial_channel(self, payload: binding.JSONObject) -> channels.PartialChannel: - ... + """Parse a raw payload from Discord into a partial channel object. + + Parameters + ---------- + payload : Mapping[Hashable, Any] + The dict payload to parse. + + Returns + ------- + hikari.models.channels.PartialChannel + The parsed partial channel object. + """ @abc.abstractmethod def deserialize_dm_channel(self, payload: binding.JSONObject) -> channels.DMChannel: - ... + """Parse a raw payload from Discord into a DM channel object. + + Parameters + ---------- + payload : Mapping[Hashable, Any] + The dict payload to parse. + + Returns + ------- + hikari.models.channels.DMChannel + The parsed DM channel object. + """ @abc.abstractmethod def deserialize_group_dm_channel(self, payload: binding.JSONObject) -> channels.GroupDMChannel: - ... + """Parse a raw payload from Discord into a group DM channel object. + + Parameters + ---------- + payload : Mapping[Hashable, Any] + The dict payload to parse. + + Returns + ------- + hikari.models.channels.GroupDMChannel + The parsed group DM channel object. + """ @abc.abstractmethod def deserialize_guild_category(self, payload: binding.JSONObject) -> channels.GuildCategory: - ... + """Parse a raw payload from Discord into a guild category object. + + Parameters + ---------- + payload : Mapping[Hashable, Any] + The dict payload to parse. + + Returns + ------- + hikari.models.channels.GuildCategory + The parsed partial channel object. + """ @abc.abstractmethod def deserialize_guild_text_channel(self, payload: binding.JSONObject) -> channels.GuildTextChannel: - ... + """Parse a raw payload from Discord into a guild text channel object. + + Parameters + ---------- + payload : Mapping[Hashable, Any] + The dict payload to parse. + + Returns + ------- + hikari.models.channels.GuildTextChannel + The parsed guild text channel object. + """ @abc.abstractmethod def deserialize_guild_news_channel(self, payload: binding.JSONObject) -> channels.GuildNewsChannel: - ... + """Parse a raw payload from Discord into a guild news channel object. + + Parameters + ---------- + payload : Mapping[Hashable, Any] + The dict payload to parse. + + Returns + ------- + hikari.models.channels.GuildNewsChannel + The parsed guild news channel object. + """ @abc.abstractmethod def deserialize_guild_store_channel(self, payload: binding.JSONObject) -> channels.GuildStoreChannel: - ... + """Parse a raw payload from Discord into a guild store channel object. + + Parameters + ---------- + payload : Mapping[Hashable, Any] + The dict payload to parse. + + Returns + ------- + hikari.models.channels.GuildStoreChannel + The parsed guild store channel object. + """ @abc.abstractmethod def deserialize_guild_voice_channel(self, payload: binding.JSONObject) -> channels.GuildVoiceChannel: - ... + """Parse a raw payload from Discord into a guild voice channel object. + + Parameters + ---------- + payload : Mapping[Hashable, Any] + The dict payload to parse. + + Returns + ------- + hikari.models.channels.GuildVoiceChannel + The parsed guild voice channel object. + """ @abc.abstractmethod def deserialize_channel(self, payload: binding.JSONObject) -> channels.PartialChannel: - ... + """Parse a raw payload from Discord into a channel object. + + Parameters + ---------- + payload : Mapping[Hashable, Any] + The dict payload to parse. + + Returns + ------- + hikari.models.channels.PartialChannel + The parsed partial channel based object. + """ ########## # EMBEDS # @@ -126,11 +291,33 @@ def deserialize_channel(self, payload: binding.JSONObject) -> channels.PartialCh @abc.abstractmethod def deserialize_embed(self, payload: binding.JSONObject) -> embeds.Embed: - ... + """Parse a raw payload from Discord into an embed object. + + Parameters + ---------- + payload : Mapping[Hashable, Any] + The dict payload to parse. + + Returns + ------- + hikari.models.embeds.Embed + The parsed embed object. + """ @abc.abstractmethod def serialize_embed(self, embed: embeds.Embed) -> binding.JSONObject: - ... + """Serialize an embed object to a json serializable dict. + + Parameters + ---------- + embed : hikari.models.embeds.Embed + The embed object to serialize. + + Returns + ------- + Dict[Hashable, Any] + The dict representation of the provided embed object. + """ ########## # EMOJIS # @@ -138,19 +325,63 @@ def serialize_embed(self, embed: embeds.Embed) -> binding.JSONObject: @abc.abstractmethod def deserialize_unicode_emoji(self, payload: binding.JSONObject) -> emojis.UnicodeEmoji: - ... + """Parse a raw payload from Discord into a unicode emoji object. + + Parameters + ---------- + payload : Mapping[Hashable, Any] + The dict payload to parse. + + Returns + ------- + hikari.models.emojis.UnicodeEmoji + The parsed unicode emoji object. + """ @abc.abstractmethod def deserialize_custom_emoji(self, payload: binding.JSONObject) -> emojis.CustomEmoji: - ... + """Parse a raw payload from Discord into a custom emoji object. + + Parameters + ---------- + payload : Mapping[Hashable, Any] + The dict payload to parse. + + Returns + ------- + hikari.models.emojis.CustomEmoji + The parsed custom emoji object. + """ @abc.abstractmethod def deserialize_known_custom_emoji(self, payload: binding.JSONObject) -> emojis.KnownCustomEmoji: - ... + """Parse a raw payload from Discord into a known custom emoji object. + + Parameters + ---------- + payload : Mapping[Hashable, Any] + The dict payload to parse. + + Returns + ------- + hikari.models.emojis.KnownCustomEmoji + The parsed known custom emoji object. + """ @abc.abstractmethod def deserialize_emoji(self, payload: binding.JSONObject) -> typing.Union[emojis.UnicodeEmoji, emojis.CustomEmoji]: - ... + """Parse a raw payload from Discord into an emoji object. + + Parameters + ---------- + payload : Mapping[Hashable, Any] + The dict payload to parse. + + Returns + ------- + hikari.models.emojis.UnicodeEmoji | hikari.models.emoji.CustomEmoji + The parsed custom or unicode emoji object. + """ ########### # GATEWAY # @@ -158,7 +389,18 @@ def deserialize_emoji(self, payload: binding.JSONObject) -> typing.Union[emojis. @abc.abstractmethod def deserialize_gateway_bot(self, payload: binding.JSONObject) -> gateway.GatewayBot: - ... + """Parse a raw payload from Discord into a gateway bot object. + + Parameters + ---------- + payload : Mapping[Hashable, Any] + The dict payload to parse. + + Returns + ------- + hikari.models.gateway.GatewayBot + The parsed gateway bot object. + """ ########## # GUILDS # @@ -166,45 +408,159 @@ def deserialize_gateway_bot(self, payload: binding.JSONObject) -> gateway.Gatewa @abc.abstractmethod def deserialize_guild_widget(self, payload: binding.JSONObject) -> guilds.GuildWidget: - ... + """Parse a raw payload from Discord into a guild embed object. + + Parameters + ---------- + payload : Mapping[Hashable, Any] + The dict payload to parse. + + Returns + ------- + hikari.models.guilds.GuildEmbed + The parsed guild embed object. + """ @abc.abstractmethod def deserialize_guild_member( self, payload: binding.JSONObject, *, user: typing.Optional[users.User] = None ) -> guilds.GuildMember: - ... + """Parse a raw payload from Discord into a guild member object. + + Parameters + ---------- + payload : Mapping[Hashable, Any] + The dict payload to parse. + *, + user : hikari.models.users.User? + The user to attach to this member, should be passed in situations + where "user" is not included in the payload. + + Returns + ------- + hikari.models.guilds.GuildMember + The parsed guild member object. + """ @abc.abstractmethod def deserialize_role(self, payload: binding.JSONObject) -> guilds.Role: - ... + """Parse a raw payload from Discord into a guild role object. + + Parameters + ---------- + payload : Mapping[Hashable, Any] + The dict payload to parse. + + Returns + ------- + hikari.models.guilds.GuildRole + The parsed guild role object. + """ @abc.abstractmethod def deserialize_guild_member_presence(self, payload: binding.JSONObject) -> guilds.GuildMemberPresence: - ... + """Parse a raw payload from Discord into a guild member presence object. + + Parameters + ---------- + payload : Mapping[Hashable, Any] + The dict payload to parse. + + Returns + ------- + hikari.models.guilds.GuildMemberPresence + The parsed guild member presence object. + """ @abc.abstractmethod - def deserialize_partial_guild_integration(self, payload: binding.JSONObject) -> guilds.PartialGuildIntegration: - ... + def deserialize_partial_integration(self, payload: binding.JSONObject) -> guilds.PartialIntegration: + """Parse a raw payload from Discord into a partial integration object. + + Parameters + ---------- + payload : Mapping[Hashable, Any] + The dict payload to parse. + + Returns + ------- + hikari.models.guilds.PartialIntegration + The parsed partial integration object. + """ @abc.abstractmethod - def deserialize_guild_integration(self, payload: binding.JSONObject) -> guilds.GuildIntegration: - ... + def deserialize_integration(self, payload: binding.JSONObject) -> guilds.Integration: + """Parse a raw payload from Discord into an integration object. + + Parameters + ---------- + payload : Mapping[Hashable, Any] + The dict payload to parse. + + Returns + ------- + hikari.models.guilds.Integration + The parsed integration object. + """ @abc.abstractmethod def deserialize_guild_member_ban(self, payload: binding.JSONObject) -> guilds.GuildMemberBan: - ... + """Parse a raw payload from Discord into a guild member ban object. + + Parameters + ---------- + payload : Mapping[Hashable, Any] + The dict payload to parse. + + Returns + ------- + hikari.models.GuildMemberBan + The parsed guild member ban object. + """ @abc.abstractmethod def deserialize_unavailable_guild(self, payload: binding.JSONObject) -> guilds.UnavailableGuild: - ... + """Parse a raw payload from Discord into a unavailable guild object. + + Parameters + ---------- + payload : Mapping[Hashable, Any] + The dict payload to parse. + + Returns + ------- + hikari.models.guilds.UnavailableGuild + The parsed unavailable guild object. + """ @abc.abstractmethod def deserialize_guild_preview(self, payload: binding.JSONObject) -> guilds.GuildPreview: - ... + """Parse a raw payload from Discord into a guild preview object. + + Parameters + ---------- + payload : Mapping[Hashable, Any] + The dict payload to parse. + + Returns + ------- + hikari.models.guilds.GuildPreview + The parsed guild preview object. + """ @abc.abstractmethod def deserialize_guild(self, payload: binding.JSONObject) -> guilds.Guild: - ... + """Parse a raw payload from Discord into a guild object. + + Parameters + ---------- + payload : Mapping[Hashable, Any] + The dict payload to parse. + + Returns + ------- + hikari.models.guilds.Guild + The parsed guild object. + """ ########### # INVITES # @@ -212,22 +568,66 @@ def deserialize_guild(self, payload: binding.JSONObject) -> guilds.Guild: @abc.abstractmethod def deserialize_vanity_url(self, payload: binding.JSONObject) -> invites.VanityURL: - ... + """Parse a raw payload from Discord into a vanity url object. + + Parameters + ---------- + payload : Mapping[Hashable, Any] + The dict payload to parse. + + Returns + ------- + hikari.models.invites.VanityUrl + The parsed vanity url object. + """ @abc.abstractmethod def deserialize_invite(self, payload: binding.JSONObject) -> invites.Invite: - ... + """Parse a raw payload from Discord into an invite object. + + Parameters + ---------- + payload : Mapping[Hashable, Any] + The dict payload to parse. + + Returns + ------- + hikari.models.invites.Invite + The parsed invite object. + """ @abc.abstractmethod def deserialize_invite_with_metadata(self, payload: binding.JSONObject) -> invites.InviteWithMetadata: - ... + """Parse a raw payload from Discord into a invite with metadata object. + + Parameters + ---------- + payload : Mapping[Hashable, Any] + The dict payload to parse. + + Returns + ------- + hikari.models.invites.InviteWithMetadata + The parsed invite with metadata object. + """ ############ # MESSAGES # ############ def deserialize_message(self, payload: binding.JSONObject) -> messages.Message: - ... + """Parse a raw payload from Discord into a message object. + + Parameters + ---------- + payload : Mapping[Hashable, Any] + The dict payload to parse. + + Returns + ------- + hikari.models.messages.Message + The parsed message object. + """ ######### # USERS # @@ -235,11 +635,33 @@ def deserialize_message(self, payload: binding.JSONObject) -> messages.Message: @abc.abstractmethod def deserialize_user(self, payload: binding.JSONObject) -> users.User: - ... + """Parse a raw payload from Discord into a user object. + + Parameters + ---------- + payload : Mapping[Hashable, Any] + The dict payload to parse. + + Returns + ------- + hikari.models.users.User + The parsed user object. + """ @abc.abstractmethod def deserialize_my_user(self, payload: binding.JSONObject) -> users.MyUser: - ... + """Parse a raw payload from Discord into a my user object. + + Parameters + ---------- + payload : Mapping[Hashable, Any] + The dict payload to parse. + + Returns + ------- + hikari.models.users.MyUser + The parsed my user object. + """ ########## # Voices # @@ -247,11 +669,33 @@ def deserialize_my_user(self, payload: binding.JSONObject) -> users.MyUser: @abc.abstractmethod def deserialize_voice_state(self, payload: binding.JSONObject) -> voices.VoiceState: - ... + """Parse a raw payload from Discord into a voice state object. + + Parameters + ---------- + payload : Mapping[Hashable, Any] + The dict payload to parse. + + Returns + ------- + hikari.models.voices.VoiceState + The parsed voice state object. + """ @abc.abstractmethod def deserialize_voice_region(self, payload: binding.JSONObject) -> voices.VoiceRegion: - ... + """Parse a raw payload from Discord into a voice region object. + + Parameters + ---------- + payload : Mapping[Hashable, Any] + The dict payload to parse. + + Returns + ------- + hikari.models.voices.VoiceRegion + The parsed voice region object. + """ ############ # WEBHOOKS # @@ -259,4 +703,15 @@ def deserialize_voice_region(self, payload: binding.JSONObject) -> voices.VoiceR @abc.abstractmethod def deserialize_webhook(self, payload: binding.JSONObject) -> webhooks.Webhook: - ... + """Parse a raw payload from Discord into a webhook object. + + Parameters + ---------- + payload : Mapping[Hashable, Any] + The dict payload to parse. + + Returns + ------- + hikari.models.webhooks.Webhook + The parsed webhook object. + """ diff --git a/hikari/events/channel.py b/hikari/events/channel.py index 8a68920e30..74d0c99163 100644 --- a/hikari/events/channel.py +++ b/hikari/events/channel.py @@ -44,46 +44,21 @@ from hikari.models import intents from hikari.models import invites from hikari.models import users -from hikari.utilities import conversions from . import base as base_events -if typing.TYPE_CHECKING: - from hikari.utilities import more_typing - - -def _overwrite_deserializer( - payload: more_typing.JSONArray, **kwargs: typing.Any -) -> typing.Mapping[base_models.Snowflake, channels.PermissionOverwrite]: - return { - base_models.Snowflake(overwrite["id"]): channels.PermissionOverwrite.deserialize(overwrite, **kwargs) - for overwrite in payload - } - - -def _rate_limit_per_user_deserializer(seconds: int) -> datetime.timedelta: - return datetime.timedelta(seconds=seconds) - - -def _recipients_deserializer( - payload: more_typing.JSONArray, **kwargs: typing.Any -) -> typing.Mapping[base_models.Snowflake, users.User]: - return {base_models.Snowflake(user["id"]): users.User.deserialize(user, **kwargs) for user in payload} - @base_events.requires_intents(intents.Intent.GUILDS) @attr.s(eq=False, hash=False, kw_only=True, slots=True) -class BaseChannelEvent(base_events.HikariEvent, base_models.Unique, marshaller.Deserializable, abc.ABC): +class BaseChannelEvent(base_events.HikariEvent, base_models.Unique, abc.ABC): """A base object that Channel events will inherit from.""" - type: channels.ChannelType = attr.ib(deserializer=channels.ChannelType, repr=True) + type: channels.ChannelType = attr.ib(repr=True) """The channel's type.""" - guild_id: typing.Optional[base_models.Snowflake] = attr.ib( - deserializer=base_models.Snowflake, if_undefined=None, default=None, repr=True - ) + guild_id: typing.Optional[base_models.Snowflake] = attr.ib(repr=True) """The ID of the guild this channel is in, will be `None` for DMs.""" - position: typing.Optional[int] = attr.ib(deserializer=int, if_undefined=None, default=None) + position: typing.Optional[int] = attr.ib() """The sorting position of this channel. This will be relative to the `BaseChannelEvent.parent_id` if set. @@ -91,68 +66,52 @@ class BaseChannelEvent(base_events.HikariEvent, base_models.Unique, marshaller.D permission_overwrites: typing.Optional[ typing.Mapping[base_models.Snowflake, channels.PermissionOverwrite] - ] = attr.ib(deserializer=_overwrite_deserializer, if_undefined=None, default=None, inherit_kwargs=True) + ] = attr.ib() """An mapping of the set permission overwrites for this channel, if applicable.""" - name: typing.Optional[str] = attr.ib(deserializer=str, if_undefined=None, default=None, repr=True) + name: typing.Optional[str] = attr.ib(repr=True) """The name of this channel, if applicable.""" - topic: typing.Optional[str] = attr.ib(deserializer=str, if_undefined=None, if_none=None, default=None) + topic: typing.Optional[str] = attr.ib() """The topic of this channel, if applicable and set.""" - is_nsfw: typing.Optional[bool] = attr.ib(raw_name="nsfw", deserializer=bool, if_undefined=None, default=None) + is_nsfw: typing.Optional[bool] = attr.ib() """Whether this channel is nsfw, will be `None` if not applicable.""" - last_message_id: typing.Optional[base_models.Snowflake] = attr.ib( - deserializer=base_models.Snowflake, if_none=None, if_undefined=None, default=None - ) + last_message_id: typing.Optional[base_models.Snowflake] = attr.ib() """The ID of the last message sent, if it's a text type channel.""" - bitrate: typing.Optional[int] = attr.ib(deserializer=int, if_undefined=None, default=None) + bitrate: typing.Optional[int] = attr.ib() """The bitrate (in bits) of this channel, if it's a guild voice channel.""" - user_limit: typing.Optional[int] = attr.ib(deserializer=int, if_undefined=None, default=None) + user_limit: typing.Optional[int] = attr.ib() """The user limit for this channel if it's a guild voice channel.""" - rate_limit_per_user: typing.Optional[datetime.timedelta] = attr.ib( - deserializer=_rate_limit_per_user_deserializer, if_undefined=None, default=None - ) + rate_limit_per_user: typing.Optional[datetime.timedelta] = attr.ib() """How long a user has to wait before sending another message in this channel. This is only applicable to a guild text like channel. """ - recipients: typing.Optional[typing.Mapping[base_models.Snowflake, users.User]] = attr.ib( - deserializer=_recipients_deserializer, if_undefined=None, default=None, inherit_kwargs=True, - ) + recipients: typing.Optional[typing.Mapping[base_models.Snowflake, users.User]] = attr.ib() """A mapping of this channel's recipient users, if it's a DM or group DM.""" - icon_hash: typing.Optional[str] = attr.ib( - raw_name="icon", deserializer=str, if_undefined=None, if_none=None, default=None - ) + icon_hash: typing.Optional[str] = attr.ib() """The hash of this channel's icon, if it's a group DM channel and is set.""" - owner_id: typing.Optional[base_models.Snowflake] = attr.ib( - deserializer=base_models.Snowflake, if_undefined=None, default=None - ) + owner_id: typing.Optional[base_models.Snowflake] = attr.ib() """The ID of this channel's creator, if it's a DM channel.""" - application_id: typing.Optional[base_models.Snowflake] = attr.ib( - deserializer=base_models.Snowflake, if_undefined=None, default=None - ) + application_id: typing.Optional[base_models.Snowflake] = attr.ib() """The ID of the application that created the group DM. This is only applicable to bot based group DMs. """ - parent_id: typing.Optional[base_models.Snowflake] = attr.ib( - deserializer=base_models.Snowflake, if_undefined=None, if_none=None, default=None - ) + parent_id: typing.Optional[base_models.Snowflake] = attr.ib() """The ID of this channels's parent category within guild, if set.""" - last_pin_timestamp: typing.Optional[datetime.datetime] = attr.ib( - deserializer=conversions.iso8601_datetime_string_to_datetime, if_undefined=None, default=None - ) + last_pin_timestamp: typing.Optional[datetime.datetime] = attr.ib() """The datetime of when the last message was pinned in this channel.""" @@ -180,27 +139,23 @@ class ChannelDeleteEvent(BaseChannelEvent): @base_events.requires_intents(intents.Intent.GUILDS) @attr.s(eq=False, hash=False, kw_only=True, slots=True) -class ChannelPinsUpdateEvent(base_events.HikariEvent, marshaller.Deserializable): +class ChannelPinsUpdateEvent(base_events.HikariEvent): """Used to represent the Channel Pins Update gateway event. Sent when a message is pinned or unpinned in a channel but not when a pinned message is deleted. """ - guild_id: typing.Optional[base_models.Snowflake] = attr.ib( - deserializer=base_models.Snowflake, if_undefined=None, default=None, repr=True - ) + guild_id: typing.Optional[base_models.Snowflake] = attr.ib() """The ID of the guild where this event happened. Will be `None` if this happened in a DM channel. """ - channel_id: base_models.Snowflake = attr.ib(deserializer=base_models.Snowflake, repr=True) + channel_id: base_models.Snowflake = attr.ib(repr=True) """The ID of the channel where the message was pinned or unpinned.""" - last_pin_timestamp: typing.Optional[datetime.datetime] = attr.ib( - deserializer=conversions.iso8601_datetime_string_to_datetime, if_undefined=None, default=None, repr=True - ) + last_pin_timestamp: typing.Optional[datetime.datetime] = attr.ib(repr=True) """The datetime of when the most recent message was pinned in this channel. Will be `None` if there are no messages pinned after this change. @@ -209,139 +164,113 @@ class ChannelPinsUpdateEvent(base_events.HikariEvent, marshaller.Deserializable) @base_events.requires_intents(intents.Intent.GUILD_WEBHOOKS) @attr.s(eq=False, hash=False, kw_only=True, slots=True) -class WebhookUpdateEvent(base_events.HikariEvent, marshaller.Deserializable): +class WebhookUpdateEvent(base_events.HikariEvent): """Used to represent webhook update gateway events. Sent when a webhook is updated, created or deleted in a guild. """ - guild_id: base_models.Snowflake = attr.ib(deserializer=base_models.Snowflake, repr=True) + guild_id: base_models.Snowflake = attr.ib(repr=True) """The ID of the guild this webhook is being updated in.""" - channel_id: base_models.Snowflake = attr.ib(deserializer=base_models.Snowflake, repr=True) + channel_id: base_models.Snowflake = attr.ib(repr=True) """The ID of the channel this webhook is being updated in.""" -def _timestamp_deserializer(date: str) -> datetime.datetime: - return datetime.datetime.fromtimestamp(float(date), datetime.timezone.utc) - - @base_events.requires_intents(intents.Intent.GUILD_MESSAGE_TYPING, intents.Intent.DIRECT_MESSAGE_TYPING) @attr.s(eq=False, hash=False, kw_only=True, slots=True) -class TypingStartEvent(base_events.HikariEvent, marshaller.Deserializable): +class TypingStartEvent(base_events.HikariEvent): """Used to represent typing start gateway events. Received when a user or bot starts "typing" in a channel. """ - channel_id: base_models.Snowflake = attr.ib(deserializer=base_models.Snowflake, repr=True) + channel_id: base_models.Snowflake = attr.ib(repr=True) """The ID of the channel this typing event is occurring in.""" - guild_id: typing.Optional[base_models.Snowflake] = attr.ib( - deserializer=base_models.Snowflake, if_undefined=None, default=None, repr=True - ) + guild_id: typing.Optional[base_models.Snowflake] = attr.ib(repr=True) """The ID of the guild this typing event is occurring in. Will be `None` if this event is happening in a DM channel. """ - user_id: base_models.Snowflake = attr.ib(deserializer=base_models.Snowflake, repr=True) + user_id: base_models.Snowflake = attr.ib(repr=True) """The ID of the user who triggered this typing event.""" - timestamp: datetime.datetime = attr.ib(deserializer=_timestamp_deserializer) + timestamp: datetime.datetime = attr.ib() """The datetime of when this typing event started.""" - member: typing.Optional[guilds.GuildMember] = attr.ib( - deserializer=guilds.GuildMember.deserialize, if_undefined=None, default=None - ) + member: typing.Optional[guilds.GuildMember] = attr.ib() """The member object of the user who triggered this typing event. Will be `None` if this was triggered in a DM. """ -def _max_age_deserializer(age: int) -> typing.Optional[datetime.datetime]: - return datetime.timedelta(seconds=age) if age > 0 else None - - -def _max_uses_deserializer(count: int) -> typing.Union[int, float]: - return count or float("inf") - - @base_events.requires_intents(intents.Intent.GUILD_INVITES) @attr.s(eq=False, hash=False, kw_only=True, slots=True) -class InviteCreateEvent(base_events.HikariEvent, marshaller.Deserializable): +class InviteCreateEvent(base_events.HikariEvent): """Represents a gateway Invite Create event.""" - channel_id: base_models.Snowflake = attr.ib(deserializer=base_models.Snowflake, repr=True) + channel_id: base_models.Snowflake = attr.ib(repr=True) """The ID of the channel this invite targets.""" - code: str = attr.ib(deserializer=str, repr=True) + code: str = attr.ib(repr=True) """The code that identifies this invite.""" - created_at: datetime.datetime = attr.ib(deserializer=conversions.iso8601_datetime_string_to_datetime) + created_at: datetime.datetime = attr.ib() """The datetime of when this invite was created.""" - guild_id: typing.Optional[base_models.Snowflake] = attr.ib( - deserializer=base_models.Snowflake, if_undefined=None, default=None, repr=True - ) + guild_id: typing.Optional[base_models.Snowflake] = attr.ib(repr=True) """The ID of the guild this invite was created in, if applicable. Will be `None` for group DM invites. """ - inviter: typing.Optional[users.User] = attr.ib( - deserializer=users.User.deserialize, if_undefined=None, default=None, inherit_kwargs=True - ) + inviter: typing.Optional[users.User] = attr.ib() """The object of the user who created this invite, if applicable.""" - max_age: typing.Optional[datetime.timedelta] = attr.ib(deserializer=_max_age_deserializer,) + max_age: typing.Optional[datetime.timedelta] = attr.ib() """The timedelta of how long this invite will be valid for. If set to `None` then this is unlimited. """ - max_uses: typing.Union[int, float] = attr.ib(deserializer=_max_uses_deserializer) + max_uses: typing.Union[int, float] = attr.ib() """The limit for how many times this invite can be used before it expires. If set to infinity (`float("inf")`) then this is unlimited. """ - target_user: typing.Optional[users.User] = attr.ib( - deserializer=users.User.deserialize, if_undefined=None, default=None, inherit_kwargs=True - ) + target_user: typing.Optional[users.User] = attr.ib() """The object of the user who this invite targets, if set.""" - target_user_type: typing.Optional[invites.TargetUserType] = attr.ib( - deserializer=invites.TargetUserType, if_undefined=None, default=None - ) + target_user_type: typing.Optional[invites.TargetUserType] = attr.ib() """The type of user target this invite is, if applicable.""" - is_temporary: bool = attr.ib(raw_name="temporary", deserializer=bool) + is_temporary: bool = attr.ib() """Whether this invite grants temporary membership.""" - uses: int = attr.ib(deserializer=int) + uses: int = attr.ib() """The amount of times this invite has been used.""" @base_events.requires_intents(intents.Intent.GUILD_INVITES) @attr.s(eq=False, hash=False, kw_only=True, slots=True) -class InviteDeleteEvent(base_events.HikariEvent, marshaller.Deserializable): +class InviteDeleteEvent(base_events.HikariEvent): """Used to represent Invite Delete gateway events. Sent when an invite is deleted for a channel we can access. """ - channel_id: base_models.Snowflake = attr.ib(deserializer=base_models.Snowflake, repr=True) + channel_id: base_models.Snowflake = attr.ib(repr=True) """The ID of the channel this ID was attached to.""" # TODO: move common fields with InviteCreateEvent into base class. - code: str = attr.ib(deserializer=str, repr=True) + code: str = attr.ib(repr=True) """The code of this invite.""" - guild_id: typing.Optional[base_models.Snowflake] = attr.ib( - deserializer=base_models.Snowflake, if_undefined=None, default=None, repr=True - ) + guild_id: typing.Optional[base_models.Snowflake] = attr.ib(repr=True) """The ID of the guild this invite was deleted in. This will be `None` if this invite belonged to a DM channel. diff --git a/hikari/events/guild.py b/hikari/events/guild.py index 37fe74bb3d..b55dc58bda 100644 --- a/hikari/events/guild.py +++ b/hikari/events/guild.py @@ -49,15 +49,12 @@ from hikari.models import guilds from hikari.models import intents from hikari.models import users -from hikari.utilities import conversions from . import base as base_events from ..utilities import unset if typing.TYPE_CHECKING: import datetime - from hikari.utilities import more_typing - @base_events.requires_intents(intents.Intent.GUILDS) @attr.s(eq=False, hash=False, kw_only=True, slots=True) @@ -77,7 +74,7 @@ class GuildUpdateEvent(base_events.HikariEvent, guilds.Guild): @base_events.requires_intents(intents.Intent.GUILDS) @attr.s(eq=False, hash=False, kw_only=True, slots=True) -class GuildLeaveEvent(base_events.HikariEvent, base_models.Unique, marshaller.Deserializable): +class GuildLeaveEvent(base_events.HikariEvent, base_models.Unique): """Fired when the current user leaves the guild or is kicked/banned from it. !!! note @@ -87,7 +84,7 @@ class GuildLeaveEvent(base_events.HikariEvent, base_models.Unique, marshaller.De @base_events.requires_intents(intents.Intent.GUILDS) @attr.s(eq=False, hash=False, kw_only=True, slots=True) -class GuildUnavailableEvent(base_events.HikariEvent, base_models.Unique, marshaller.Deserializable): +class GuildUnavailableEvent(base_events.HikariEvent, base_models.Unique): """Fired when a guild becomes temporarily unavailable due to an outage. !!! note @@ -97,13 +94,13 @@ class GuildUnavailableEvent(base_events.HikariEvent, base_models.Unique, marshal @base_events.requires_intents(intents.Intent.GUILD_BANS) @attr.s(eq=False, hash=False, kw_only=True, slots=True) -class BaseGuildBanEvent(base_events.HikariEvent, marshaller.Deserializable, abc.ABC): +class BaseGuildBanEvent(base_events.HikariEvent, abc.ABC): """A base object that guild ban events will inherit from.""" - guild_id: base_models.Snowflake = attr.ib(deserializer=base_models.Snowflake, repr=True) + guild_id: base_models.Snowflake = attr.ib(repr=True) """The ID of the guild this ban is in.""" - user: users.User = attr.ib(deserializer=users.User.deserialize, inherit_kwargs=True, repr=True) + user: users.User = attr.ib(repr=True) """The object of the user this ban targets.""" @@ -119,35 +116,24 @@ class GuildBanRemoveEvent(BaseGuildBanEvent): """Used to represent a Guild Ban Remove gateway event.""" -def _deserialize_emojis( - payload: more_typing.JSONArray, **kwargs: typing.Any -) -> typing.Mapping[base_models.Snowflake, emojis_models.KnownCustomEmoji]: - return { - base_models.Snowflake(emoji["id"]): emojis_models.KnownCustomEmoji.deserialize(emoji, **kwargs) - for emoji in payload - } - - @base_events.requires_intents(intents.Intent.GUILD_EMOJIS) @attr.s(eq=False, hash=False, kw_only=True, slots=True) -class GuildEmojisUpdateEvent(base_events.HikariEvent, marshaller.Deserializable): +class GuildEmojisUpdateEvent(base_events.HikariEvent): """Represents a Guild Emoji Update gateway event.""" - guild_id: base_models.Snowflake = attr.ib(deserializer=base_models.Snowflake) + guild_id: base_models.Snowflake = attr.ib() """The ID of the guild this emoji was updated in.""" - emojis: typing.Mapping[base_models.Snowflake, emojis_models.KnownCustomEmoji] = attr.ib( - deserializer=_deserialize_emojis, inherit_kwargs=True, repr=True - ) + emojis: typing.Mapping[base_models.Snowflake, emojis_models.KnownCustomEmoji] = attr.ib(repr=True) """The updated mapping of emojis by their ID.""" @base_events.requires_intents(intents.Intent.GUILD_INTEGRATIONS) @attr.s(eq=False, hash=False, kw_only=True, slots=True) -class GuildIntegrationsUpdateEvent(base_events.HikariEvent, marshaller.Deserializable): +class GuildIntegrationsUpdateEvent(base_events.HikariEvent): """Used to represent Guild Integration Update gateway events.""" - guild_id: base_models.Snowflake = attr.ib(deserializer=base_models.Snowflake, repr=True) + guild_id: base_models.Snowflake = attr.ib(repr=True) """The ID of the guild the integration was updated in.""" @@ -156,48 +142,35 @@ class GuildIntegrationsUpdateEvent(base_events.HikariEvent, marshaller.Deseriali class GuildMemberAddEvent(base_events.HikariEvent, guilds.GuildMember): """Used to represent a Guild Member Add gateway event.""" - guild_id: base_models.Snowflake = attr.ib(deserializer=base_models.Snowflake, repr=True) + guild_id: base_models.Snowflake = attr.ib(repr=True) """The ID of the guild where this member was added.""" -def _deserialize_role_ids(payload: more_typing.JSONArray) -> typing.Sequence[base_models.Snowflake]: - return [base_models.Snowflake(role_id) for role_id in payload] - - @base_events.requires_intents(intents.Intent.GUILD_MEMBERS) @attr.s(eq=False, hash=False, kw_only=True, slots=True) -class GuildMemberUpdateEvent(base_events.HikariEvent, marshaller.Deserializable): +class GuildMemberUpdateEvent(base_events.HikariEvent): """Used to represent a Guild Member Update gateway event. Sent when a guild member or their inner user object is updated. """ - guild_id: base_models.Snowflake = attr.ib(deserializer=base_models.Snowflake, repr=True) + guild_id: base_models.Snowflake = attr.ib(repr=True) """The ID of the guild this member was updated in.""" - role_ids: typing.Sequence[base_models.Snowflake] = attr.ib( - raw_name="roles", deserializer=_deserialize_role_ids, - ) + role_ids: typing.Sequence[base_models.Snowflake] = attr.ib() """A sequence of the IDs of the member's current roles.""" - user: users.User = attr.ib(deserializer=users.User.deserialize, inherit_kwargs=True, repr=True) + user: users.User = attr.ib(repr=True) """The object of the user who was updated.""" - nickname: typing.Union[None, str, unset.Unset] = attr.ib( - raw_name="nick", deserializer=str, if_none=None, if_undefined=unset.Unset, default=unset.UNSET - ) + nickname: typing.Union[None, str, unset.Unset] = attr.ib() """This member's nickname. When set to `None`, this has been removed and when set to `hikari.models.unset.UNSET` this hasn't been acted on. """ - premium_since: typing.Union[None, datetime.datetime, unset.Unset] = attr.ib( - deserializer=conversions.iso8601_datetime_string_to_datetime, - if_none=None, - if_undefined=unset.Unset, - default=unset.UNSET, - ) + premium_since: typing.Union[None, datetime.datetime, unset.Unset] = attr.ib() """The datetime of when this member started "boosting" this guild. Will be `None` if they aren't boosting. @@ -206,55 +179,55 @@ class GuildMemberUpdateEvent(base_events.HikariEvent, marshaller.Deserializable) @base_events.requires_intents(intents.Intent.GUILD_MEMBERS) @attr.s(eq=False, hash=False, kw_only=True, slots=True) -class GuildMemberRemoveEvent(base_events.HikariEvent, marshaller.Deserializable): +class GuildMemberRemoveEvent(base_events.HikariEvent): """Used to represent Guild Member Remove gateway events. Sent when a member is kicked, banned or leaves a guild. """ # TODO: make GuildMember event into common base class. - guild_id: base_models.Snowflake = attr.ib(deserializer=base_models.Snowflake, repr=True) + guild_id: base_models.Snowflake = attr.ib(repr=True) """The ID of the guild this user was removed from.""" - user: users.User = attr.ib(deserializer=users.User.deserialize, inherit_kwargs=True, repr=True) + user: users.User = attr.ib(repr=True) """The object of the user who was removed from this guild.""" @base_events.requires_intents(intents.Intent.GUILDS) @attr.s(eq=False, hash=False, kw_only=True, slots=True) -class GuildRoleCreateEvent(base_events.HikariEvent, marshaller.Deserializable): +class GuildRoleCreateEvent(base_events.HikariEvent): """Used to represent a Guild Role Create gateway event.""" - guild_id: base_models.Snowflake = attr.ib(deserializer=base_models.Snowflake, repr=True) + guild_id: base_models.Snowflake = attr.ib(repr=True) """The ID of the guild where this role was created.""" - role: guilds.Role = attr.ib(deserializer=guilds.Role.deserialize, inherit_kwargs=True) + role: guilds.Role = attr.ib() """The object of the role that was created.""" @base_events.requires_intents(intents.Intent.GUILDS) @attr.s(eq=False, hash=False, kw_only=True, slots=True) -class GuildRoleUpdateEvent(base_events.HikariEvent, marshaller.Deserializable): +class GuildRoleUpdateEvent(base_events.HikariEvent): """Used to represent a Guild Role Create gateway event.""" # TODO: make any event with a guild ID into a custom base event. # https://pypi.org/project/stupid/ could this work around the multiple inheritance problem? - guild_id: base_models.Snowflake = attr.ib(deserializer=base_models.Snowflake, repr=True) + guild_id: base_models.Snowflake = attr.ib(repr=True) """The ID of the guild where this role was updated.""" - role: guilds.Role = attr.ib(deserializer=guilds.Role.deserialize, inherit_kwargs=True, repr=True) + role: guilds.Role = attr.ib(repr=True) """The updated role object.""" @base_events.requires_intents(intents.Intent.GUILDS) @attr.s(eq=False, hash=False, kw_only=True, slots=True) -class GuildRoleDeleteEvent(base_events.HikariEvent, marshaller.Deserializable): +class GuildRoleDeleteEvent(base_events.HikariEvent): """Represents a gateway Guild Role Delete Event.""" - guild_id: base_models.Snowflake = attr.ib(deserializer=base_models.Snowflake, repr=True) + guild_id: base_models.Snowflake = attr.ib(repr=True) """The ID of the guild where this role is being deleted.""" - role_id: base_models.Snowflake = attr.ib(deserializer=base_models.Snowflake, repr=True) + role_id: base_models.Snowflake = attr.ib(repr=True) """The ID of the role being deleted.""" diff --git a/hikari/events/message.py b/hikari/events/message.py index f74dc6aa15..d7542a3fa0 100644 --- a/hikari/events/message.py +++ b/hikari/events/message.py @@ -43,7 +43,6 @@ from hikari.models import intents from hikari.models import messages from hikari.models import users -from hikari.utilities import conversions from . import base as base_events from ..utilities import unset @@ -59,32 +58,10 @@ class MessageCreateEvent(base_events.HikariEvent, messages.Message): """Used to represent Message Create gateway events.""" -def _deserialize_object_mentions(payload: more_typing.JSONArray) -> typing.Set[base_models.Snowflake]: - return {base_models.Snowflake(mention["id"]) for mention in payload} - - -def _deserialize_mentions(payload: more_typing.JSONArray) -> typing.Set[base_models.Snowflake]: - return {base_models.Snowflake(mention) for mention in payload} - - -def _deserialize_attachments( - payload: more_typing.JSONArray, **kwargs: typing.Any -) -> typing.Sequence[messages.Attachment]: - return [messages.Attachment.deserialize(attachment, **kwargs) for attachment in payload] - - -def _deserialize_embeds(payload: more_typing.JSONArray, **kwargs: typing.Any) -> typing.Sequence[embed_models.Embed]: - return [embed_models.Embed.deserialize(embed, **kwargs) for embed in payload] - - -def _deserialize_reaction(payload: more_typing.JSONArray, **kwargs: typing.Any) -> typing.Sequence[messages.Reaction]: - return [messages.Reaction.deserialize(reaction, **kwargs) for reaction in payload] - - # This is an arbitrarily partial version of `messages.Message` @base_events.requires_intents(intents.Intent.GUILD_MESSAGES, intents.Intent.DIRECT_MESSAGES) @attr.s(eq=False, hash=False, kw_only=True, slots=True) -class MessageUpdateEvent(base_events.HikariEvent, base_models.Unique, marshaller.Deserializable): +class MessageUpdateEvent(base_events.HikariEvent, base_models.Unique): """Represents Message Update gateway events. !!! note @@ -94,133 +71,78 @@ class MessageUpdateEvent(base_events.HikariEvent, base_models.Unique, marshaller alongside field nullability. """ - channel_id: base_models.Snowflake = attr.ib(deserializer=base_models.Snowflake, repr=True) + channel_id: base_models.Snowflake = attr.ib(repr=True) """The ID of the channel that the message was sent in.""" - guild_id: typing.Union[base_models.Snowflake, unset.Unset] = attr.ib( - deserializer=base_models.Snowflake, if_undefined=unset.Unset, default=unset.UNSET, repr=True - ) + guild_id: typing.Union[base_models.Snowflake, unset.Unset] = attr.ib(repr=True) """The ID of the guild that the message was sent in.""" - author: typing.Union[users.User, unset.Unset] = attr.ib( - deserializer=users.User.deserialize, if_undefined=unset.Unset, default=unset.UNSET, repr=True - ) + author: typing.Union[users.User, unset.Unset] = attr.ib(repr=True) """The author of this message.""" # TODO: can we merge member and author together? # We could override deserialize to to this and then reorganise the payload, perhaps? - member: typing.Union[guilds.GuildMember, unset.Unset] = attr.ib( - deserializer=guilds.GuildMember.deserialize, if_undefined=unset.Unset, default=unset.UNSET - ) + member: typing.Union[guilds.GuildMember, unset.Unset] = attr.ib() """The member properties for the message's author.""" - content: typing.Union[str, unset.Unset] = attr.ib(deserializer=str, if_undefined=unset.Unset, default=unset.UNSET) + content: typing.Union[str, unset.Unset] = attr.ib() """The content of the message.""" - timestamp: typing.Union[datetime.datetime, unset.Unset] = attr.ib( - deserializer=conversions.iso8601_datetime_string_to_datetime, if_undefined=unset.Unset, default=unset.UNSET - ) + timestamp: typing.Union[datetime.datetime, unset.Unset] = attr.ib() """The timestamp that the message was sent at.""" - edited_timestamp: typing.Union[datetime.datetime, unset.Unset, None] = attr.ib( - deserializer=conversions.iso8601_datetime_string_to_datetime, - if_none=None, - if_undefined=unset.Unset, - default=unset.UNSET, - ) + edited_timestamp: typing.Union[datetime.datetime, unset.Unset, None] = attr.ib() """The timestamp that the message was last edited at. Will be `None` if the message wasn't ever edited. """ - is_tts: typing.Union[bool, unset.Unset] = attr.ib( - raw_name="tts", deserializer=bool, if_undefined=unset.Unset, default=unset.UNSET - ) + is_tts: typing.Union[bool, unset.Unset] = attr.ib() """Whether the message is a TTS message.""" - is_mentioning_everyone: typing.Union[bool, unset.Unset] = attr.ib( - raw_name="mention_everyone", deserializer=bool, if_undefined=unset.Unset, default=unset.UNSET - ) + is_mentioning_everyone: typing.Union[bool, unset.Unset] = attr.ib() """Whether the message mentions `@everyone` or `@here`.""" - user_mentions: typing.Union[typing.Set[base_models.Snowflake], unset.Unset] = attr.ib( - raw_name="mentions", deserializer=_deserialize_object_mentions, if_undefined=unset.Unset, default=unset.UNSET, - ) + user_mentions: typing.Union[typing.Set[base_models.Snowflake], unset.Unset] = attr.ib() """The users the message mentions.""" - role_mentions: typing.Union[typing.Set[base_models.Snowflake], unset.Unset] = attr.ib( - raw_name="mention_roles", deserializer=_deserialize_mentions, if_undefined=unset.Unset, default=unset.UNSET, - ) + role_mentions: typing.Union[typing.Set[base_models.Snowflake], unset.Unset] = attr.ib() """The roles the message mentions.""" - channel_mentions: typing.Union[typing.Set[base_models.Snowflake], unset.Unset] = attr.ib( - raw_name="mention_channels", - deserializer=_deserialize_object_mentions, - if_undefined=unset.Unset, - default=unset.UNSET, - ) + channel_mentions: typing.Union[typing.Set[base_models.Snowflake], unset.Unset] = attr.ib() """The channels the message mentions.""" - attachments: typing.Union[typing.Sequence[messages.Attachment], unset.Unset] = attr.ib( - deserializer=_deserialize_attachments, if_undefined=unset.Unset, default=unset.UNSET, inherit_kwargs=True, - ) + attachments: typing.Union[typing.Sequence[messages.Attachment], unset.Unset] = attr.ib() """The message attachments.""" - embeds: typing.Union[typing.Sequence[embed_models.Embed], unset.Unset] = attr.ib( - deserializer=_deserialize_embeds, if_undefined=unset.Unset, default=unset.UNSET, inherit_kwargs=True, - ) + embeds: typing.Union[typing.Sequence[embed_models.Embed], unset.Unset] = attr.ib() """The message's embeds.""" - reactions: typing.Union[typing.Sequence[messages.Reaction], unset.Unset] = attr.ib( - deserializer=_deserialize_reaction, if_undefined=unset.Unset, default=unset.UNSET, inherit_kwargs=True - ) + reactions: typing.Union[typing.Sequence[messages.Reaction], unset.Unset] = attr.ib() """The message's reactions.""" - is_pinned: typing.Union[bool, unset.Unset] = attr.ib( - raw_name="pinned", deserializer=bool, if_undefined=unset.Unset, default=unset.UNSET - ) + is_pinned: typing.Union[bool, unset.Unset] = attr.ib() """Whether the message is pinned.""" - webhook_id: typing.Union[base_models.Snowflake, unset.Unset] = attr.ib( - deserializer=base_models.Snowflake, if_undefined=unset.Unset, default=unset.UNSET - ) + webhook_id: typing.Union[base_models.Snowflake, unset.Unset] = attr.ib() """If the message was generated by a webhook, the webhook's ID.""" - type: typing.Union[messages.MessageType, unset.Unset] = attr.ib( - deserializer=messages.MessageType, if_undefined=unset.Unset, default=unset.UNSET - ) + type: typing.Union[messages.MessageType, unset.Unset] = attr.ib() """The message's type.""" - activity: typing.Union[messages.MessageActivity, unset.Unset] = attr.ib( - deserializer=messages.MessageActivity.deserialize, - if_undefined=unset.Unset, - default=unset.UNSET, - inherit_kwargs=True, - ) + activity: typing.Union[messages.MessageActivity, unset.Unset] = attr.ib() """The message's activity.""" - application: typing.Optional[applications.Application] = attr.ib( - deserializer=applications.Application.deserialize, - if_undefined=unset.Unset, - default=unset.UNSET, - inherit_kwargs=True, - ) + application: typing.Optional[applications.Application] = attr.ib() """The message's application.""" - message_reference: typing.Union[messages.MessageCrosspost, unset.Unset] = attr.ib( - deserializer=messages.MessageCrosspost.deserialize, - if_undefined=unset.Unset, - default=unset.UNSET, - inherit_kwargs=True, - ) + message_reference: typing.Union[messages.MessageCrosspost, unset.Unset] = attr.ib() """The message's cross-posted reference data.""" - flags: typing.Union[messages.MessageFlag, unset.Unset] = attr.ib( - deserializer=messages.MessageFlag, if_undefined=unset.Unset, default=unset.UNSET - ) + flags: typing.Union[messages.MessageFlag, unset.Unset] = attr.ib() """The message's flags.""" - nonce: typing.Union[str, unset.Unset] = attr.ib(deserializer=str, if_undefined=unset.Unset, default=unset.UNSET) + nonce: typing.Union[str, unset.Unset] = attr.ib() """The message nonce. This is a string used for validating a message was sent. @@ -229,7 +151,7 @@ class MessageUpdateEvent(base_events.HikariEvent, base_models.Unique, marshaller @base_events.requires_intents(intents.Intent.GUILD_MESSAGES, intents.Intent.DIRECT_MESSAGES) @attr.s(eq=False, hash=False, kw_only=True, slots=True) -class MessageDeleteEvent(base_events.HikariEvent, marshaller.Deserializable): +class MessageDeleteEvent(base_events.HikariEvent): """Used to represent Message Delete gateway events. Sent when a message is deleted in a channel we have access to. @@ -237,155 +159,131 @@ class MessageDeleteEvent(base_events.HikariEvent, marshaller.Deserializable): # TODO: common base class for Message events. - channel_id: base_models.Snowflake = attr.ib(deserializer=base_models.Snowflake, repr=True) + channel_id: base_models.Snowflake = attr.ib(repr=True) """The ID of the channel where this message was deleted.""" - guild_id: typing.Optional[base_models.Snowflake] = attr.ib( - deserializer=base_models.Snowflake, if_undefined=None, default=None, repr=True - ) + guild_id: typing.Optional[base_models.Snowflake] = attr.ib(repr=True) """The ID of the guild where this message was deleted. This will be `None` if this message was deleted in a DM channel. """ - message_id: base_models.Snowflake = attr.ib(raw_name="id", deserializer=base_models.Snowflake, repr=True) + message_id: base_models.Snowflake = attr.ib(repr=True) """The ID of the message that was deleted.""" -def _deserialize_message_ids(payload: more_typing.JSONArray) -> typing.Set[base_models.Snowflake]: - return {base_models.Snowflake(message_id) for message_id in payload} - - @base_events.requires_intents(intents.Intent.GUILD_MESSAGES) @attr.s(eq=False, hash=False, kw_only=True, slots=True) -class MessageDeleteBulkEvent(base_events.HikariEvent, marshaller.Deserializable): +class MessageDeleteBulkEvent(base_events.HikariEvent): """Used to represent Message Bulk Delete gateway events. Sent when multiple messages are deleted in a channel at once. """ - channel_id: base_models.Snowflake = attr.ib(deserializer=base_models.Snowflake, repr=True) + channel_id: base_models.Snowflake = attr.ib(repr=True) """The ID of the channel these messages have been deleted in.""" - guild_id: typing.Optional[base_models.Snowflake] = attr.ib( - deserializer=base_models.Snowflake, if_none=None, repr=True, - ) + guild_id: typing.Optional[base_models.Snowflake] = attr.ib(repr=True,) """The ID of the channel these messages have been deleted in. This will be `None` if these messages were bulk deleted in a DM channel. """ - message_ids: typing.Set[base_models.Snowflake] = attr.ib(raw_name="ids", deserializer=_deserialize_message_ids) + message_ids: typing.Set[base_models.Snowflake] = attr.ib() """A collection of the IDs of the messages that were deleted.""" @base_events.requires_intents(intents.Intent.GUILD_MESSAGE_REACTIONS, intents.Intent.DIRECT_MESSAGE_REACTIONS) @attr.s(eq=False, hash=False, kw_only=True, slots=True) -class MessageReactionAddEvent(base_events.HikariEvent, marshaller.Deserializable): +class MessageReactionAddEvent(base_events.HikariEvent): """Used to represent Message Reaction Add gateway events.""" # TODO: common base classes! - user_id: base_models.Snowflake = attr.ib(deserializer=base_models.Snowflake, repr=True) + user_id: base_models.Snowflake = attr.ib(repr=True) """The ID of the user adding the reaction.""" - channel_id: base_models.Snowflake = attr.ib(deserializer=base_models.Snowflake, repr=True) + channel_id: base_models.Snowflake = attr.ib(repr=True) """The ID of the channel where this reaction is being added.""" - message_id: base_models.Snowflake = attr.ib(deserializer=base_models.Snowflake, repr=True) + message_id: base_models.Snowflake = attr.ib(repr=True) """The ID of the message this reaction is being added to.""" - guild_id: typing.Optional[base_models.Snowflake] = attr.ib( - deserializer=base_models.Snowflake, if_undefined=None, default=None, repr=True - ) + guild_id: typing.Optional[base_models.Snowflake] = attr.ib(repr=True) """The ID of the guild where this reaction is being added. This will be `None` if this is happening in a DM channel. """ # TODO: does this contain a user? If not, should it be a PartialGuildMember? - member: typing.Optional[guilds.GuildMember] = attr.ib( - deserializer=guilds.GuildMember.deserialize, if_undefined=None, default=None, inherit_kwargs=True - ) + member: typing.Optional[guilds.GuildMember] = attr.ib() """The member object of the user who's adding this reaction. This will be `None` if this is happening in a DM channel. """ - emoji: typing.Union[emojis.CustomEmoji, emojis.UnicodeEmoji] = attr.ib( - deserializer=emojis.deserialize_reaction_emoji, inherit_kwargs=True, repr=True - ) + emoji: typing.Union[emojis.CustomEmoji, emojis.UnicodeEmoji] = attr.ib(repr=True) """The object of the emoji being added.""" @base_events.requires_intents(intents.Intent.GUILD_MESSAGE_REACTIONS, intents.Intent.DIRECT_MESSAGE_REACTIONS) @attr.s(eq=False, hash=False, kw_only=True, slots=True) -class MessageReactionRemoveEvent(base_events.HikariEvent, marshaller.Deserializable): +class MessageReactionRemoveEvent(base_events.HikariEvent): """Used to represent Message Reaction Remove gateway events.""" - user_id: base_models.Snowflake = attr.ib(deserializer=base_models.Snowflake, repr=True) + user_id: base_models.Snowflake = attr.ib(repr=True) """The ID of the user who is removing their reaction.""" - channel_id: base_models.Snowflake = attr.ib(deserializer=base_models.Snowflake, repr=True) + channel_id: base_models.Snowflake = attr.ib(repr=True) """The ID of the channel where this reaction is being removed.""" - message_id: base_models.Snowflake = attr.ib(deserializer=base_models.Snowflake, repr=True) + message_id: base_models.Snowflake = attr.ib(repr=True) """The ID of the message this reaction is being removed from.""" - guild_id: typing.Optional[base_models.Snowflake] = attr.ib( - deserializer=base_models.Snowflake, if_undefined=None, default=None, repr=True - ) + guild_id: typing.Optional[base_models.Snowflake] = attr.ib(repr=True) """The ID of the guild where this reaction is being removed This will be `None` if this event is happening in a DM channel. """ - emoji: typing.Union[emojis.UnicodeEmoji, emojis.CustomEmoji] = attr.ib( - deserializer=emojis.deserialize_reaction_emoji, inherit_kwargs=True, repr=True - ) + emoji: typing.Union[emojis.UnicodeEmoji, emojis.CustomEmoji] = attr.ib(repr=True) """The object of the emoji being removed.""" @base_events.requires_intents(intents.Intent.GUILD_MESSAGE_REACTIONS, intents.Intent.DIRECT_MESSAGE_REACTIONS) @attr.s(eq=False, hash=False, kw_only=True, slots=True) -class MessageReactionRemoveAllEvent(base_events.HikariEvent, marshaller.Deserializable): +class MessageReactionRemoveAllEvent(base_events.HikariEvent): """Used to represent Message Reaction Remove All gateway events. Sent when all the reactions are removed from a message, regardless of emoji. """ - channel_id: base_models.Snowflake = attr.ib(deserializer=base_models.Snowflake, repr=True) + channel_id: base_models.Snowflake = attr.ib(repr=True) """The ID of the channel where the targeted message is.""" - message_id: base_models.Snowflake = attr.ib(deserializer=base_models.Snowflake, repr=True) + message_id: base_models.Snowflake = attr.ib(repr=True) """The ID of the message all reactions are being removed from.""" - guild_id: typing.Optional[base_models.Snowflake] = attr.ib( - deserializer=base_models.Snowflake, if_undefined=None, default=None, repr=True, - ) + guild_id: typing.Optional[base_models.Snowflake] = attr.ib(repr=True,) """The ID of the guild where the targeted message is, if applicable.""" @base_events.requires_intents(intents.Intent.GUILD_MESSAGE_REACTIONS, intents.Intent.DIRECT_MESSAGE_REACTIONS) @attr.s(eq=False, hash=False, kw_only=True, slots=True) -class MessageReactionRemoveEmojiEvent(base_events.HikariEvent, marshaller.Deserializable): +class MessageReactionRemoveEmojiEvent(base_events.HikariEvent): """Represents Message Reaction Remove Emoji events. Sent when all the reactions for a single emoji are removed from a message. """ - channel_id: base_models.Snowflake = attr.ib(deserializer=base_models.Snowflake, repr=True) + channel_id: base_models.Snowflake = attr.ib(repr=True) """The ID of the channel where the targeted message is.""" - guild_id: typing.Optional[base_models.Snowflake] = attr.ib( - deserializer=base_models.Snowflake, if_undefined=None, default=None, repr=True - ) + guild_id: typing.Optional[base_models.Snowflake] = attr.ib(repr=True) """The ID of the guild where the targeted message is, if applicable.""" - message_id: base_models.Snowflake = attr.ib(deserializer=base_models.Snowflake, repr=True) + message_id: base_models.Snowflake = attr.ib(repr=True) """The ID of the message the reactions are being removed from.""" - emoji: typing.Union[emojis.UnicodeEmoji, emojis.CustomEmoji] = attr.ib( - deserializer=emojis.deserialize_reaction_emoji, inherit_kwargs=True, repr=True - ) + emoji: typing.Union[emojis.UnicodeEmoji, emojis.CustomEmoji] = attr.ib(repr=True) """The object of the emoji that's being removed.""" diff --git a/hikari/events/other.py b/hikari/events/other.py index 5dcac1d9c8..49fc7b381e 100644 --- a/hikari/events/other.py +++ b/hikari/events/other.py @@ -44,7 +44,6 @@ if typing.TYPE_CHECKING: from ..net import gateway as gateway_client - from hikari.utilities import more_typing # Synthetic event, is not deserialized, and is produced by the dispatcher. @@ -88,7 +87,7 @@ class StoppedEvent(base_events.HikariEvent): @attr.s(auto_attribs=True, eq=False, hash=False, kw_only=True, slots=True) -class ConnectedEvent(base_events.HikariEvent, marshaller.Deserializable): +class ConnectedEvent(base_events.HikariEvent): """Event invoked each time a shard connects.""" shard: gateway_client.Gateway @@ -96,7 +95,7 @@ class ConnectedEvent(base_events.HikariEvent, marshaller.Deserializable): @attr.s(auto_attribs=True, eq=False, hash=False, kw_only=True, slots=True) -class DisconnectedEvent(base_events.HikariEvent, marshaller.Deserializable): +class DisconnectedEvent(base_events.HikariEvent): """Event invoked each time a shard disconnects.""" shard: gateway_client.Gateway @@ -111,43 +110,29 @@ class ResumedEvent(base_events.HikariEvent): """The shard that reconnected.""" -def _deserialize_unavailable_guilds( - payload: more_typing.JSONArray, **kwargs: typing.Any -) -> typing.Mapping[base_models.Snowflake, guilds.UnavailableGuild]: - return { - base_models.Snowflake(guild["id"]): guilds.UnavailableGuild.deserialize(guild, **kwargs) for guild in payload - } - - @attr.s(eq=False, hash=False, kw_only=True, slots=True) -class ReadyEvent(base_events.HikariEvent, marshaller.Deserializable): +class ReadyEvent(base_events.HikariEvent): """Represents the gateway Ready event. This is received only when IDENTIFYing with the gateway. """ - gateway_version: int = attr.ib(raw_name="v", deserializer=int, repr=True) + gateway_version: int = attr.ib(repr=True) """The gateway version this is currently connected to.""" - my_user: users.MyUser = attr.ib( - raw_name="user", deserializer=users.MyUser.deserialize, inherit_kwargs=True, repr=True - ) + my_user: users.MyUser = attr.ib(repr=True) """The object of the current bot account this connection is for.""" - unavailable_guilds: typing.Mapping[base_models.Snowflake, guilds.UnavailableGuild] = attr.ib( - raw_name="guilds", deserializer=_deserialize_unavailable_guilds, inherit_kwargs=True - ) + unavailable_guilds: typing.Mapping[base_models.Snowflake, guilds.UnavailableGuild] = attr.ib() """A mapping of the guilds this bot is currently in. All guilds will start off "unavailable". """ - session_id: str = attr.ib(deserializer=str, repr=True) + session_id: str = attr.ib(repr=True) """The id of the current gateway session, used for reconnecting.""" - _shard_information: typing.Optional[typing.Tuple[int, int]] = attr.ib( - raw_name="shard", deserializer=tuple, if_undefined=None, default=None - ) + _shard_information: typing.Optional[typing.Tuple[int, int]] = attr.ib() """Information about the current shard, only provided when IDENTIFYing.""" @property diff --git a/hikari/events/voice.py b/hikari/events/voice.py index 024712e3a3..25dfd8d0d4 100644 --- a/hikari/events/voice.py +++ b/hikari/events/voice.py @@ -40,18 +40,18 @@ class VoiceStateUpdateEvent(base_events.HikariEvent, voices.VoiceState): @attr.s(eq=False, hash=False, kw_only=True, slots=True) -class VoiceServerUpdateEvent(base_events.HikariEvent, marshaller.Deserializable): +class VoiceServerUpdateEvent(base_events.HikariEvent): """Used to represent voice server update gateway events. Sent when initially connecting to voice and when the current voice instance falls over to a new server. """ - token: str = attr.ib(deserializer=str) + token: str = attr.ib() """The voice connection's string token.""" - guild_id: base_models.Snowflake = attr.ib(deserializer=base_models.Snowflake, repr=True) + guild_id: base_models.Snowflake = attr.ib(repr=True) """The ID of the guild this voice server update is for.""" - endpoint: str = attr.ib(deserializer=str, repr=True) + endpoint: str = attr.ib(repr=True) """The URI for this voice server host.""" diff --git a/hikari/impl/cache.py b/hikari/impl/cache.py index 28c4dd5b65..05a692e87b 100644 --- a/hikari/impl/cache.py +++ b/hikari/impl/cache.py @@ -142,7 +142,7 @@ async def delete_presence(self, guild_id: int, user_id: int) -> typing.Optional[ async def create_guild_ban(self, payload: binding.JSONObject) -> guilds.GuildMemberBan: pass - async def create_guild_integration(self, payload: binding.JSONObject) -> guilds.PartialGuildIntegration: + async def create_guild_integration(self, payload: binding.JSONObject) -> guilds.PartialIntegration: pass async def create_guild(self, payload: binding.JSONObject, can_cache: bool = False) -> guilds.PartialGuild: diff --git a/hikari/impl/entity_factory.py b/hikari/impl/entity_factory.py index 047b2c450a..97052ef1de 100644 --- a/hikari/impl/entity_factory.py +++ b/hikari/impl/entity_factory.py @@ -25,96 +25,815 @@ import datetime import typing +from hikari import app as app_ from hikari import entity_factory +from hikari.models import applications +from hikari.models import audit_logs +from hikari.models import bases as base_models +from hikari.models import channels as channels_ +from hikari.models import colors +from hikari.models import embeds +from hikari.models import emojis from hikari.models import gateway -from hikari.utilities import binding +from hikari.models import guilds +from hikari.models import invites +from hikari.models import messages +from hikari.models import permissions +from hikari.models import users +from hikari.models import voices +from hikari.models import webhooks +from hikari.utilities import date +from hikari.utilities import unset if typing.TYPE_CHECKING: - from hikari import app as app_ - from hikari.models import applications - from hikari.models import audit_logs - from hikari.models import channels - from hikari.models import embeds - from hikari.models import emojis - from hikari.models import guilds - from hikari.models import invites - from hikari.models import users - from hikari.models import voices - from hikari.models import webhooks + from hikari.utilities import binding + + +DMChannelT = typing.TypeVar("DMChannelT", bound=channels_.DMChannel) +GuildChannelT = typing.TypeVar("GuildChannelT", bound=channels_.GuildChannel) +InviteT = typing.TypeVar("InviteT", bound=invites.Invite) +PartialChannelT = typing.TypeVar("PartialChannelT", bound=channels_.PartialChannel) +PartialGuildT = typing.TypeVar("PartialGuildT", bound=guilds.PartialGuild) +PartialGuildIntegrationT = typing.TypeVar("PartialGuildIntegrationT", bound=guilds.PartialIntegration) +UserT = typing.TypeVar("UserT", bound=users.User) class EntityFactoryImpl(entity_factory.IEntityFactory): + """Interface for an entity factory implementation.""" + def __init__(self, app: app_.IApp) -> None: self._app = app + self._audit_log_entry_converters = { + audit_logs.AuditLogChangeKey.ADD_ROLE_TO_MEMBER: self._deserialize_audit_log_change_roles, + audit_logs.AuditLogChangeKey.REMOVE_ROLE_FROM_MEMBER: self._deserialize_audit_log_change_roles, + audit_logs.AuditLogChangeKey.PERMISSION_OVERWRITES: self._deserialize_audit_log_overwrites, + } + self._audit_log_event_mapping = { + audit_logs.AuditLogEventType.CHANNEL_OVERWRITE_CREATE: self._deserialize_channel_overwrite_entry_info, + audit_logs.AuditLogEventType.CHANNEL_OVERWRITE_UPDATE: self._deserialize_channel_overwrite_entry_info, + audit_logs.AuditLogEventType.CHANNEL_OVERWRITE_DELETE: self._deserialize_channel_overwrite_entry_info, + audit_logs.AuditLogEventType.MESSAGE_PIN: self._deserialize_message_pin_entry_info, + audit_logs.AuditLogEventType.MESSAGE_UNPIN: self._deserialize_message_pin_entry_info, + audit_logs.AuditLogEventType.MEMBER_PRUNE: self._deserialize_member_prune_entry_info, + audit_logs.AuditLogEventType.MESSAGE_BULK_DELETE: self._deserialize_message_bulk_delete_entry_info, + audit_logs.AuditLogEventType.MESSAGE_DELETE: self._deserialize_message_delete_entry_info, + audit_logs.AuditLogEventType.MEMBER_DISCONNECT: self._deserialize_member_disconnect_entry_info, + audit_logs.AuditLogEventType.MEMBER_MOVE: self._deserialize_member_move_entry_info, + } + self._channel_type_mapping = { + channels_.ChannelType.DM: self.deserialize_dm_channel, + channels_.ChannelType.GROUP_DM: self.deserialize_group_dm_channel, + channels_.ChannelType.GUILD_CATEGORY: self.deserialize_guild_category, + channels_.ChannelType.GUILD_TEXT: self.deserialize_guild_text_channel, + channels_.ChannelType.GUILD_NEWS: self.deserialize_guild_news_channel, + channels_.ChannelType.GUILD_STORE: self.deserialize_guild_store_channel, + channels_.ChannelType.GUILD_VOICE: self.deserialize_guild_voice_channel, + } @property def app(self) -> app_.IApp: return self._app + ################ + # APPLICATIONS # + ################ + def deserialize_own_connection(self, payload: binding.JSONObject) -> applications.OwnConnection: - pass + """Parse a raw payload from Discord into an own connection object. + + Parameters + ---------- + payload : Mapping[Hashable, Any] + The dict payload to parse. + + Returns + ------- + hikari.models.applications.OwnConnection + The parsed own connection object. + """ + own_connection = applications.OwnConnection() + own_connection.id = base_models.Snowflake(payload["id"]) + own_connection.name = payload["name"] + own_connection.type = payload["type"] + own_connection.is_revoked = payload.get("revoked") + own_connection.integrations = [ + self.deserialize_partial_integration(integration) for integration in payload.get("integrations", ()) + ] + own_connection.is_verified = payload["verified"] + own_connection.is_friend_syncing = payload["friend_sync"] + own_connection.is_showing_activity = payload["show_activity"] + # noinspection PyArgumentList + own_connection.visibility = applications.ConnectionVisibility(payload["visibility"]) + return own_connection def deserialize_own_guild(self, payload: binding.JSONObject) -> applications.OwnGuild: - pass + """Parse a raw payload from Discord into an own guild object. + + Parameters + ---------- + payload : Mapping[Hashable, Any] + The dict payload to parse. + + Returns + ------- + hikari.models.applications.OwnGuild + The parsed own guild object. + """ + own_guild = self._set_partial_guild_attributes(payload, applications.OwnGuild()) + own_guild.is_owner = bool(payload["owner"]) + # noinspection PyArgumentList + own_guild.my_permissions = permissions.Permission(payload["permissions"]) + return own_guild + + def deserialize_application(self, payload: binding.JSONObject) -> applications.Application: + """Parse a raw payload from Discord into an application object. + + Parameters + ---------- + payload : Mapping[Hashable, Any] + The dict payload to parse. + + Returns + ------- + hikari.models.applications.Application + The parsed application object. + """ + application = applications.Application() + application.set_app(self._app) + application.id = base_models.Snowflake(payload["id"]) + application.name = payload["name"] + application.description = payload["description"] + application.is_bot_public = payload.get("bot_public") + application.is_bot_code_grant_required = payload.get("bot_require_code_grant") + application.owner = self.deserialize_user(payload["owner"]) if "owner" in payload else None + application.rpc_origins = set(payload["rpc_origins"]) if "rpc_origins" in payload else None + application.summary = payload["summary"] + application.verify_key = bytes(payload["verify_key"], "utf-8") if "verify_key" in payload else None + application.icon_hash = payload.get("icon") + if (team_payload := payload.get("team")) is not None: + team = applications.Team() + team.set_app(self._app) + team.id = base_models.Snowflake(team_payload["id"]) + team.icon_hash = team_payload["icon"] + members = {} + for member_payload in team_payload["members"]: + team_member = applications.TeamMember() + team_member.set_app(self._app) + # noinspection PyArgumentList + team_member.membership_state = applications.TeamMembershipState(member_payload["membership_state"]) + team_member.permissions = set(member_payload["permissions"]) + team_member.team_id = base_models.Snowflake(member_payload["team_id"]) + team_member.user = self.deserialize_user(member_payload["user"]) + members[team_member.user.id] = team_member + team.members = members + team.owner_user_id = base_models.Snowflake(team_payload["owner_user_id"]) + application.team = team + else: + application.team = None + application.guild_id = base_models.Snowflake(payload["guild_id"]) if "guild_id" in payload else None + application.primary_sku_id = ( + base_models.Snowflake(payload["primary_sku_id"]) if "primary_sku_id" in payload else None + ) + application.slug = payload.get("slug") + application.cover_image_hash = payload.get("cover_image") + return application + + ############## + # AUDIT_LOGS # + ############## + + def _deserialize_audit_log_change_roles( + self, payload: binding.JSONArray + ) -> typing.Mapping[base_models.Snowflake, guilds.PartialRole]: + roles = {} + for role_payload in payload: + role = guilds.PartialRole() + role.set_app(self._app) + role.id = base_models.Snowflake(role_payload["id"]) + role.name = role_payload["name"] + roles[role.id] = role + return roles + + def _deserialize_audit_log_overwrites( + self, payload: binding.JSONArray + ) -> typing.Mapping[base_models.Snowflake, channels_.PermissionOverwrite]: + return { + base_models.Snowflake(overwrite["id"]): self.deserialize_permission_overwrite(overwrite) + for overwrite in payload + } + + @staticmethod + def _deserialize_channel_overwrite_entry_info(payload: binding.JSONObject,) -> audit_logs.ChannelOverwriteEntryInfo: + channel_overwrite_entry_info = audit_logs.ChannelOverwriteEntryInfo() + channel_overwrite_entry_info.id = base_models.Snowflake(payload["id"]) + # noinspection PyArgumentList + channel_overwrite_entry_info.type = channels_.PermissionOverwriteType(payload["type"]) + channel_overwrite_entry_info.role_name = payload.get("role_name") + return channel_overwrite_entry_info - def deserialize_application(self, payload: binding.JSONObject) -> applications: - pass + @staticmethod + def _deserialize_message_pin_entry_info(payload: binding.JSONObject) -> audit_logs.MessagePinEntryInfo: + message_pin_entry_info = audit_logs.MessagePinEntryInfo() + message_pin_entry_info.channel_id = base_models.Snowflake(payload["channel_id"]) + message_pin_entry_info.message_id = base_models.Snowflake(payload["message_id"]) + return message_pin_entry_info + + @staticmethod + def _deserialize_member_prune_entry_info(payload: binding.JSONObject) -> audit_logs.MemberPruneEntryInfo: + member_prune_entry_info = audit_logs.MemberPruneEntryInfo() + member_prune_entry_info.delete_member_days = datetime.timedelta(days=int(payload["delete_member_days"])) + member_prune_entry_info.members_removed = int(payload["members_removed"]) + return member_prune_entry_info + + @staticmethod + def _deserialize_message_bulk_delete_entry_info( + payload: binding.JSONObject, + ) -> audit_logs.MessageBulkDeleteEntryInfo: + message_bulk_delete_entry_info = audit_logs.MessageBulkDeleteEntryInfo() + message_bulk_delete_entry_info.count = int(payload["count"]) + return message_bulk_delete_entry_info + + @staticmethod + def _deserialize_message_delete_entry_info(payload: binding.JSONObject) -> audit_logs.MessageDeleteEntryInfo: + message_delete_entry_info = audit_logs.MessageDeleteEntryInfo() + message_delete_entry_info.channel_id = base_models.Snowflake(payload["channel_id"]) + message_delete_entry_info.count = int(payload["count"]) + return message_delete_entry_info + + @staticmethod + def _deserialize_member_disconnect_entry_info(payload: binding.JSONObject,) -> audit_logs.MemberDisconnectEntryInfo: + member_disconnect_entry_info = audit_logs.MemberDisconnectEntryInfo() + member_disconnect_entry_info.count = int(payload["count"]) + return member_disconnect_entry_info + + @staticmethod + def _deserialize_member_move_entry_info(payload: binding.JSONObject) -> audit_logs.MemberMoveEntryInfo: + member_move_entry_info = audit_logs.MemberMoveEntryInfo() + member_move_entry_info.channel_id = base_models.Snowflake(payload["channel_id"]) + member_move_entry_info.count = int(payload["count"]) + return member_move_entry_info + + @staticmethod + def _deserialize_unrecognised_audit_log_entry_info( + payload: binding.JSONObject, + ) -> audit_logs.UnrecognisedAuditLogEntryInfo: + return audit_logs.UnrecognisedAuditLogEntryInfo(payload) def deserialize_audit_log(self, payload: binding.JSONObject) -> audit_logs.AuditLog: - pass + """Parse a raw payload from Discord into an audit log object. + + Parameters + ---------- + payload : Mapping[Hashable, Any] + The dict payload to parse. + + Returns + ------- + hikari.models.audit_logs.AuditLogEntry + The parsed audit log object. + """ + audit_log = audit_logs.AuditLog() + entries = {} + for entry_payload in payload["audit_log_entries"]: + entry = audit_logs.AuditLogEntry() + entry.set_app(self._app) + entry.id = base_models.Snowflake(entry_payload["id"]) + if (target_id := entry_payload["target_id"]) is not None: + target_id = base_models.Snowflake(target_id) + entry.target_id = target_id + changes = [] + for change_payload in entry_payload.get("changes", ()): + change = audit_logs.AuditLogChange() + try: + change.key = audit_logs.AuditLogChangeKey(change_payload["key"]) + except ValueError: + change.key = change_payload["key"] + new_value = change_payload.get("new_value") + old_value = change_payload.get("old_value") + value_converter = audit_logs.AUDIT_LOG_ENTRY_CONVERTERS.get( + change.key + ) or self._audit_log_entry_converters.get(change.key) + if value_converter: + new_value = value_converter(new_value) if new_value is not None else None + old_value = value_converter(old_value) if old_value is not None else None + change.new_value = new_value + change.old_value = old_value + changes.append(change) + entry.changes = changes + if (user_id := entry_payload["user_id"]) is not None: + user_id = base_models.Snowflake(user_id) + entry.user_id = user_id + try: + entry.action_type = audit_logs.AuditLogEventType(entry_payload["action_type"]) + except ValueError: + entry.action_type = entry_payload["action_type"] + if (options := entry_payload.get("options")) is not None: + option_converter = ( + self._audit_log_event_mapping.get(entry.action_type) + or self._deserialize_unrecognised_audit_log_entry_info + ) + options = option_converter(options) + entry.options = options + entry.reason = entry_payload.get("reason") + entries[entry.id] = entry + audit_log.entries = entries + audit_log.integrations = { + base_models.Snowflake(integration["id"]): self.deserialize_partial_integration(integration) + for integration in payload["integrations"] + } + audit_log.users = {base_models.Snowflake(user["id"]): self.deserialize_user(user) for user in payload["users"]} + audit_log.webhooks = { + base_models.Snowflake(webhook["id"]): self.deserialize_webhook(webhook) for webhook in payload["webhooks"] + } + return audit_log + + ############ + # CHANNELS # + ############ + + def deserialize_permission_overwrite(self, payload: binding.JSONObject) -> channels_.PermissionOverwrite: + """Parse a raw payload from Discord into a permission overwrite object. + + Parameters + ---------- + payload : Mapping[Hashable, Any] + The dict payload to parse. + + Returns + ------- + hikari.models.channels.PermissionOverwrote + The parsed permission overwrite object. + """ + # noinspection PyArgumentList + permission_overwrite = channels_.PermissionOverwrite( + id=base_models.Snowflake(payload["id"]), type=channels_.PermissionOverwriteType(payload["type"]), + ) + permission_overwrite.allow = permissions.Permission(payload["allow"]) + # noinspection PyArgumentList + permission_overwrite.deny = permissions.Permission(payload["deny"]) + return permission_overwrite + + def serialize_permission_overwrite(self, overwrite: channels_.PermissionOverwrite) -> binding.JSONObject: + """Serialize a permission overwrite object to a json serializable dict. + + Parameters + ---------- + overwrite : hikari.models.channels.PermissionOverwrite + The permission overwrite object to serialize. - def deserialize_permission_overwrite(self, payload: binding.JSONObject) -> channels.PermissionOverwrite: - pass + Returns + ------- + Dict[Hashable, Any] + The dict representation of the permission overwrite object provided. + """ + return {"id": str(overwrite.id), "type": overwrite.type, "allow": overwrite.allow, "deny": overwrite.deny} - def serialize_permission_overwrite(self, overwrite: channels.PermissionOverwrite) -> binding.JSONObject: - pass + def _set_partial_channel_attributes(self, payload: binding.JSONObject, channel: PartialChannelT) -> PartialChannelT: + channel.set_app(self._app) + channel.id = base_models.Snowflake(payload["id"]) + channel.name = payload.get("name") + # noinspection PyArgumentList + channel.type = channels_.ChannelType(payload["type"]) + return channel - def deserialize_partial_channel(self, payload: binding.JSONObject) -> channels.PartialChannel: - pass + def deserialize_partial_channel(self, payload: binding.JSONObject) -> channels_.PartialChannel: + """Parse a raw payload from Discord into a partial channel object. - def deserialize_dm_channel(self, payload: binding.JSONObject) -> channels.DMChannel: - pass + Parameters + ---------- + payload : Mapping[Hashable, Any] + The dict payload to parse. - def deserialize_group_dm_channel(self, payload: binding.JSONObject) -> channels.GroupDMChannel: - pass + Returns + ------- + hikari.models.channels.PartialChannel + The parsed partial channel object. + """ + return self._set_partial_channel_attributes(payload, channels_.PartialChannel()) - def deserialize_guild_category(self, payload: binding.JSONObject) -> channels.GuildCategory: - pass + def _set_dm_channel_attributes(self, payload: binding.JSONObject, channel: DMChannelT) -> DMChannelT: + channel = self._set_partial_channel_attributes(payload, channel) + if (last_message_id := payload["last_message_id"]) is not None: + last_message_id = base_models.Snowflake(last_message_id) + channel.last_message_id = last_message_id + channel.recipients = { + base_models.Snowflake(user["id"]): self.deserialize_user(user) for user in payload["recipients"] + } + return channel - def deserialize_guild_text_channel(self, payload: binding.JSONObject) -> channels.GuildTextChannel: - pass + def deserialize_dm_channel(self, payload: binding.JSONObject) -> channels_.DMChannel: + """Parse a raw payload from Discord into a DM channel object. - def deserialize_guild_news_channel(self, payload: binding.JSONObject) -> channels.GuildNewsChannel: - pass + Parameters + ---------- + payload : Mapping[Hashable, Any] + The dict payload to parse. - def deserialize_guild_store_channel(self, payload: binding.JSONObject) -> channels.GuildStoreChannel: - pass + Returns + ------- + hikari.models.channels.DMChannel + The parsed DM channel object. + """ + return self._set_dm_channel_attributes(payload, channels_.DMChannel()) - def deserialize_guild_voice_channel(self, payload: binding.JSONObject) -> channels.GuildVoiceChannel: - pass + def deserialize_group_dm_channel(self, payload: binding.JSONObject) -> channels_.GroupDMChannel: + """Parse a raw payload from Discord into a group DM channel object. - def deserialize_channel(self, payload: binding.JSONObject) -> channels.PartialChannel: - pass + Parameters + ---------- + payload : Mapping[Hashable, Any] + The dict payload to parse. + + Returns + ------- + hikari.models.channels.GroupDMChannel + The parsed group DM channel object. + """ + group_dm_channel = self._set_dm_channel_attributes(payload, channels_.GroupDMChannel()) + group_dm_channel.owner_id = base_models.Snowflake(payload["owner_id"]) + group_dm_channel.icon_hash = payload["icon"] + group_dm_channel.application_id = ( + base_models.Snowflake(payload["application_id"]) if "application_id" in payload else None + ) + return group_dm_channel + + def _set_guild_channel_attributes(self, payload: binding.JSONObject, channel: GuildChannelT) -> GuildChannelT: + channel = self._set_partial_channel_attributes(payload, channel) + channel.guild_id = base_models.Snowflake(payload["guild_id"]) if "guild_id" in payload else None + channel.position = int(payload["position"]) + channel.permission_overwrites = { + base_models.Snowflake(overwrite["id"]): self.deserialize_permission_overwrite(overwrite) + for overwrite in payload["permission_overwrites"] + } # TODO: while snowflakes are guaranteed to be unique within their own resource, there is no guarantee for + # across between resources (user and role in this case); while in practice we won't get overlap there is a + # chance that this may happen in the future, would it be more sensible to use a Sequence here? + channel.is_nsfw = payload.get("nsfw") + if (parent_id := payload.get("parent_id")) is not None: + parent_id = base_models.Snowflake(parent_id) + channel.parent_id = parent_id + return channel + + def deserialize_guild_category(self, payload: binding.JSONObject) -> channels_.GuildCategory: + """Parse a raw payload from Discord into a guild category object. + + Parameters + ---------- + payload : Mapping[Hashable, Any] + The dict payload to parse. + + Returns + ------- + hikari.models.channels.GuildCategory + The parsed partial channel object. + """ + return self._set_guild_channel_attributes(payload, channels_.GuildCategory()) + + def deserialize_guild_text_channel(self, payload: binding.JSONObject) -> channels_.GuildTextChannel: + """Parse a raw payload from Discord into a guild text channel object. + + Parameters + ---------- + payload : Mapping[Hashable, Any] + The dict payload to parse. + + Returns + ------- + hikari.models.channels.GuildTextChannel + The parsed guild text channel object. + """ + guild_text_category = self._set_guild_channel_attributes(payload, channels_.GuildTextChannel()) + guild_text_category.topic = payload["topic"] + if (last_message_id := payload["last_message_id"]) is not None: + last_message_id = base_models.Snowflake(last_message_id) + guild_text_category.last_message_id = last_message_id + guild_text_category.rate_limit_per_user = datetime.timedelta(seconds=payload["rate_limit_per_user"]) + if (last_pin_timestamp := payload.get("last_pin_timestamp")) is not None: + last_pin_timestamp = date.iso8601_datetime_string_to_datetime(last_pin_timestamp) + guild_text_category.last_pin_timestamp = last_pin_timestamp + return guild_text_category + + def deserialize_guild_news_channel(self, payload: binding.JSONObject) -> channels_.GuildNewsChannel: + """Parse a raw payload from Discord into a guild news channel object. + + Parameters + ---------- + payload : Mapping[Hashable, Any] + The dict payload to parse. + + Returns + ------- + hikari.models.channels.GuildNewsChannel + The parsed guild news channel object. + """ + guild_news_channel = self._set_guild_channel_attributes(payload, channels_.GuildNewsChannel()) + guild_news_channel.topic = payload["topic"] + if (last_message_id := payload["last_message_id"]) is not None: + last_message_id = base_models.Snowflake(last_message_id) + guild_news_channel.last_message_id = last_message_id + if (last_pin_timestamp := payload.get("last_pin_timestamp")) is not None: + last_pin_timestamp = date.iso8601_datetime_string_to_datetime(last_pin_timestamp) + guild_news_channel.last_pin_timestamp = last_pin_timestamp + return guild_news_channel + + def deserialize_guild_store_channel(self, payload: binding.JSONObject) -> channels_.GuildStoreChannel: + """Parse a raw payload from Discord into a guild store channel object. + + Parameters + ---------- + payload : Mapping[Hashable, Any] + The dict payload to parse. + + Returns + ------- + hikari.models.channels.GuildStoreChannel + The parsed guild store channel object. + """ + return self._set_guild_channel_attributes(payload, channels_.GuildStoreChannel()) + + def deserialize_guild_voice_channel(self, payload: binding.JSONObject) -> channels_.GuildVoiceChannel: + """Parse a raw payload from Discord into a guild voice channel object. + + Parameters + ---------- + payload : Mapping[Hashable, Any] + The dict payload to parse. + + Returns + ------- + hikari.models.channels.GuildVoiceChannel + The parsed guild voice channel object. + """ + guild_voice_channel = self._set_guild_channel_attributes(payload, channels_.GuildVoiceChannel()) + guild_voice_channel.bitrate = int(payload["bitrate"]) + guild_voice_channel.user_limit = int(payload["user_limit"]) + return guild_voice_channel + + def deserialize_channel(self, payload: binding.JSONObject) -> channels_.PartialChannel: + """Parse a raw payload from Discord into a channel object. + + Parameters + ---------- + payload : Mapping[Hashable, Any] + The dict payload to parse. + + Returns + ------- + hikari.models.channels.PartialChannel + The parsed partial channel based object. + """ + return self._channel_type_mapping[payload["type"]](payload) + + ########## + # EMBEDS # + ########## def deserialize_embed(self, payload: binding.JSONObject) -> embeds.Embed: - pass + """Parse a raw payload from Discord into an embed object. + + Parameters + ---------- + payload : Mapping[Hashable, Any] + The dict payload to parse. + + Returns + ------- + hikari.models.embeds.Embed + The parsed embed object. + """ + embed = embeds.Embed() + embed.title = payload.get("title") + embed.description = payload.get("description") + embed.url = payload.get("url") + embed.timestamp = ( + date.iso8601_datetime_string_to_datetime(payload["timestamp"]) if "timestamp" in payload else None + ) + embed.color = colors.Color(payload["color"]) if "color" in payload else None + if (footer_payload := payload.get("footer", ...)) is not ...: + footer = embeds.EmbedFooter() + footer.text = footer_payload["text"] + footer.icon_url = footer_payload.get("icon_url") + footer.proxy_icon_url = footer_payload.get("proxy_icon_url") + embed.footer = footer + else: + embed.footer = None + if (image_payload := payload.get("image", ...)) is not ...: + image = embeds.EmbedImage() + image.url = image_payload.get("url") + image.proxy_url = image_payload.get("proxy_url") + image.height = int(image_payload["height"]) if "height" in image_payload else None + image.width = int(image_payload["width"]) if "width" in image_payload else None + embed.image = image + else: + embed.image = None + if (thumbnail_payload := payload.get("thumbnail", ...)) is not ...: + thumbnail = embeds.EmbedThumbnail() + thumbnail.url = thumbnail_payload.get("url") + thumbnail.proxy_url = thumbnail_payload.get("proxy_url") + thumbnail.height = int(thumbnail_payload["height"]) if "height" in thumbnail_payload else None + thumbnail.width = int(thumbnail_payload["width"]) if "width" in thumbnail_payload else None + embed.thumbnail = thumbnail + else: + embed.thumbnail = None + if (video_payload := payload.get("video", ...)) is not ...: + video = embeds.EmbedVideo() + video.url = video_payload.get("url") + video.height = int(video_payload["height"]) if "height" in video_payload else None + video.width = int(video_payload["width"]) if "width" in video_payload else None + embed.video = video + else: + embed.video = None + if (provider_payload := payload.get("provider", ...)) is not ...: + provider = embeds.EmbedProvider() + provider.name = provider_payload.get("name") + provider.url = provider_payload.get("url") + embed.provider = provider + else: + embed.provider = None + if (author_payload := payload.get("author", ...)) is not ...: + author = embeds.EmbedAuthor() + author.name = author_payload.get("name") + author.url = author_payload.get("url") + author.icon_url = author_payload.get("icon_url") + author.proxy_icon_url = author_payload.get("proxy_icon_url") + embed.author = author + else: + embed.author = None + fields = [] + for field_payload in payload.get("fields", ()): + field = embeds.EmbedField() + field.name = field_payload["name"] + field.value = field_payload["value"] + field.is_inline = field_payload.get("inline", False) + fields.append(field) + embed.fields = fields + return embed def serialize_embed(self, embed: embeds.Embed) -> binding.JSONObject: - pass + """Serialize an embed object to a json serializable dict. + + Parameters + ---------- + embed : hikari.models.embeds.Embed + The embed object to serialize. + + Returns + ------- + Dict[Hashable, Any] + The dict representation of the provided embed object. + """ + payload = {} + if embed.title is not None: + payload["title"] = embed.title + if embed.description is not None: + payload["description"] = embed.description + if embed.url is not None: + payload["url"] = embed.url + if embed.timestamp is not None: + payload["timestamp"] = embed.timestamp.isoformat() + if embed.color is not None: + payload["color"] = embed.color + if embed.footer is not None: + footer_payload = {} + if embed.footer.text is not None: + footer_payload["text"] = embed.footer.text + if embed.footer.icon_url is not None: + footer_payload["icon_url"] = embed.footer.icon_url + payload["footer"] = footer_payload + if embed.image is not None: + image_payload = {} + if embed.image.url is not None: + image_payload["url"] = embed.image.url + payload["image"] = image_payload + if embed.thumbnail is not None: + thumbnail_payload = {} + if embed.thumbnail.url is not None: + thumbnail_payload["url"] = embed.thumbnail.url + payload["thumbnail"] = thumbnail_payload + if embed.author is not None: + author_payload = {} + if embed.author.name is not None: + author_payload["name"] = embed.author.name + if embed.author.url is not None: + author_payload["url"] = embed.author.url + if embed.author.icon_url is not None: + author_payload["icon_url"] = embed.author.icon_url + payload["author"] = author_payload + if embed.fields: + field_payloads = [] + for field in embed.fields: + field_payload = {} + if field.name: + field_payload["name"] = field.name + if field.value: + field_payload["value"] = field.value + field_payload["inline"] = field.is_inline + field_payloads.append(field_payload) + payload["fields"] = field_payloads + + return payload + + ########## + # EMOJIS # + ########## def deserialize_unicode_emoji(self, payload: binding.JSONObject) -> emojis.UnicodeEmoji: - pass + """Parse a raw payload from Discord into a unicode emoji object. + + Parameters + ---------- + payload : Mapping[Hashable, Any] + The dict payload to parse. + + Returns + ------- + hikari.models.emojis.UnicodeEmoji + The parsed unicode emoji object. + """ + unicode_emoji = emojis.UnicodeEmoji() + unicode_emoji.name = payload["name"] + return unicode_emoji def deserialize_custom_emoji(self, payload: binding.JSONObject) -> emojis.CustomEmoji: - pass + """Parse a raw payload from Discord into a custom emoji object. + + Parameters + ---------- + payload : Mapping[Hashable, Any] + The dict payload to parse. + + Returns + ------- + hikari.models.emojis.CustomEmoji + The parsed custom emoji object. + """ + custom_emoji = emojis.CustomEmoji() + custom_emoji.set_app(self._app) + custom_emoji.id = base_models.Snowflake(payload["id"]) + custom_emoji.name = payload["name"] + custom_emoji.is_animated = payload.get("animated", False) + return custom_emoji def deserialize_known_custom_emoji(self, payload: binding.JSONObject) -> emojis.KnownCustomEmoji: - pass + """Parse a raw payload from Discord into a known custom emoji object. + + Parameters + ---------- + payload : Mapping[Hashable, Any] + The dict payload to parse. + + Returns + ------- + hikari.models.emojis.KnownCustomEmoji + The parsed known custom emoji object. + """ + known_custom_emoji = emojis.KnownCustomEmoji() + known_custom_emoji.set_app(self._app) + known_custom_emoji.id = base_models.Snowflake(payload["id"]) + known_custom_emoji.name = payload["name"] + known_custom_emoji.is_animated = payload.get("animated", False) + known_custom_emoji.role_ids = {base_models.Snowflake(role_id) for role_id in payload.get("roles", ())} + if (user := payload.get("user")) is not None: + user = self.deserialize_user(user) + known_custom_emoji.user = user + known_custom_emoji.is_colons_required = payload["require_colons"] + known_custom_emoji.is_managed = payload["managed"] + known_custom_emoji.is_available = payload["available"] + return known_custom_emoji def deserialize_emoji(self, payload: binding.JSONObject) -> typing.Union[emojis.UnicodeEmoji, emojis.CustomEmoji]: - pass + """Parse a raw payload from Discord into an emoji object. + + Parameters + ---------- + payload : Mapping[Hashable, Any] + The dict payload to parse. + + Returns + ------- + hikari.models.emojis.UnicodeEmoji | hikari.models.emoji.CustomEmoji + The parsed custom or unicode emoji object. + """ + if payload.get("id") is not None: + return self.deserialize_custom_emoji(payload) + return self.deserialize_unicode_emoji(payload) + + ########### + # GATEWAY # + ########### def deserialize_gateway_bot(self, payload: binding.JSONObject) -> gateway.GatewayBot: + """Parse a raw payload from Discord into a gateway bot object. + + Parameters + ---------- + payload : Mapping[Hashable, Any] + The dict payload to parse. + + Returns + ------- + hikari.models.gateway.GatewayBot + The parsed gateway bot object. + """ gateway_bot = gateway.GatewayBot() gateway_bot.url = payload["url"] gateway_bot.shard_count = int(payload["shards"]) @@ -126,58 +845,772 @@ def deserialize_gateway_bot(self, payload: binding.JSONObject) -> gateway.Gatewa gateway_bot.session_start_limit = session_start_limit return gateway_bot + ########## + # GUILDS # + ########## + def deserialize_guild_widget(self, payload: binding.JSONObject) -> guilds.GuildWidget: - pass + """Parse a raw payload from Discord into a guild widget object. + + Parameters + ---------- + payload : Mapping[Hashable, Any] + The dict payload to parse. + + Returns + ------- + hikari.models.guilds.GuildWidget + The parsed guild embed object. + """ + guild_embed = guilds.GuildWidget() + guild_embed.set_app(self._app) + if (channel_id := payload["channel_id"]) is not None: + channel_id = base_models.Snowflake(channel_id) + guild_embed.channel_id = channel_id + guild_embed.is_enabled = payload["enabled"] + return guild_embed def deserialize_guild_member( self, payload: binding.JSONObject, *, user: typing.Optional[users.User] = None ) -> guilds.GuildMember: - pass + """Parse a raw payload from Discord into a guild member object. + + Parameters + ---------- + payload : Mapping[Hashable, Any] + The dict payload to parse. + *, + user : hikari.models.users.User? + The user to attach to this member, should be passed in situations + where "user" is not included in the payload. + + Returns + ------- + hikari.models.guilds.GuildMember + The parsed guild member object. + """ + guild_member = guilds.GuildMember() + guild_member.set_app(self._app) + guild_member.user = user or self.deserialize_user(payload["user"]) + guild_member.nickname = payload.get("nick") + guild_member.role_ids = {base_models.Snowflake(role_id) for role_id in payload["roles"]} + guild_member.joined_at = date.iso8601_datetime_string_to_datetime(payload["joined_at"]) + if (premium_since := payload.get("premium_since")) is not None: + premium_since = date.iso8601_datetime_string_to_datetime(premium_since) + guild_member.premium_since = premium_since + guild_member.is_deaf = payload["deaf"] + guild_member.is_mute = payload["mute"] + return guild_member def deserialize_role(self, payload: binding.JSONObject) -> guilds.Role: - pass + """Parse a raw payload from Discord into a guild role object. + + Parameters + ---------- + payload : Mapping[Hashable, Any] + The dict payload to parse. + + Returns + ------- + hikari.models.guilds.GuildRole + The parsed guild role object. + """ + guild_role = guilds.Role() + guild_role.set_app(self._app) + guild_role.id = base_models.Snowflake(payload["id"]) + guild_role.name = payload["name"] + guild_role.color = colors.Color(payload["color"]) + guild_role.is_hoisted = payload["hoist"] + guild_role.position = int(payload["position"]) + # noinspection PyArgumentList + guild_role.permissions = permissions.Permission(payload["permissions"]) + guild_role.is_managed = payload["managed"] + guild_role.is_mentionable = payload["mentionable"] + return guild_role def deserialize_guild_member_presence(self, payload: binding.JSONObject) -> guilds.GuildMemberPresence: - pass + """Parse a raw payload from Discord into a guild member presence object. + + Parameters + ---------- + payload : Mapping[Hashable, Any] + The dict payload to parse. + + Returns + ------- + hikari.models.guilds.GuildMemberPresence + The parsed guild member presence object. + """ + guild_member_presence = guilds.GuildMemberPresence() + guild_member_presence.set_app(self._app) + user_payload = payload["user"] + user = guilds.PresenceUser() + user.set_app(self._app) + user.id = base_models.Snowflake(user_payload["id"]) + user.discriminator = user_payload["discriminator"] if "discriminator" in user_payload else unset.UNSET + user.username = user_payload["username"] if "username" in user_payload else unset.UNSET + user.avatar_hash = user_payload["avatar"] if "avatar" in user_payload else unset.UNSET + user.is_bot = user_payload["bot"] if "bot" in user_payload else unset.UNSET + user.is_system = user_payload["system"] if "system" in user_payload else unset.UNSET + # noinspection PyArgumentList + user.flags = users.UserFlag(user_payload["public_flags"]) if "public_flags" in user_payload else unset.UNSET + guild_member_presence.user = user + if (role_ids := payload.get("roles", ...)) is not ...: + guild_member_presence.role_ids = {base_models.Snowflake(role_id) for role_id in role_ids} + else: + guild_member_presence.role_ids = None + guild_member_presence.guild_id = base_models.Snowflake(payload["guild_id"]) if "guild_id" in payload else None + # noinspection PyArgumentList + guild_member_presence.visible_status = guilds.PresenceStatus(payload["status"]) + activities = [] + for activity_payload in payload["activities"]: + activity = guilds.PresenceActivity() + activity.name = activity_payload["name"] + # noinspection PyArgumentList + activity.type = guilds.ActivityType(activity_payload["type"]) + activity.url = activity_payload.get("url") + activity.created_at = date.unix_epoch_to_datetime(activity_payload["created_at"]) + if (timestamps_payload := activity_payload.get("timestamps", ...)) is not ...: + timestamps = guilds.ActivityTimestamps() + timestamps.start = ( + date.unix_epoch_to_datetime(timestamps_payload["start"]) if "start" in timestamps_payload else None + ) + timestamps.end = ( + date.unix_epoch_to_datetime(timestamps_payload["end"]) if "end" in timestamps_payload else None + ) + activity.timestamps = timestamps + else: + activity.timestamps = None + activity.application_id = ( + base_models.Snowflake(activity_payload["application_id"]) + if "application_id" in activity_payload + else None + ) + activity.details = activity_payload.get("details") + activity.state = activity_payload.get("state") + if (emoji := activity_payload.get("emoji")) is not None: + emoji = self.deserialize_emoji(emoji) + activity.emoji = emoji + if (party_payload := activity_payload.get("party", ...)) is not ...: + party = guilds.ActivityParty() + party.id = party_payload.get("id") + if (size := party_payload.get("size", ...)) is not ...: + party.current_size = int(size[0]) + party.max_size = int(size[1]) + else: + party.current_size = party.max_size = None + activity.party = party + else: + activity.party = None + if (assets_payload := activity_payload.get("assets", ...)) is not ...: + assets = guilds.ActivityAssets() + assets.large_image = assets_payload.get("large_image") + assets.large_text = assets_payload.get("large_text") + assets.small_image = assets_payload.get("small_image") + assets.small_text = assets_payload.get("small_text") + activity.assets = assets + else: + activity.assets = None + if (secrets_payload := activity_payload.get("secrets", ...)) is not ...: + secret = guilds.ActivitySecret() + secret.join = secrets_payload.get("join") + secret.spectate = secrets_payload.get("spectate") + secret.match = secrets_payload.get("match") + activity.secrets = secret + else: + activity.secrets = None + activity.is_instance = activity_payload.get("instance") # TODO: can we safely default this to False? + # noinspection PyArgumentList + activity.flags = guilds.ActivityFlag(activity_payload["flags"]) if "flags" in activity_payload else None + activities.append(activity) + guild_member_presence.activities = activities + client_status_payload = payload["client_status"] + client_status = guilds.ClientStatus() + # noinspection PyArgumentList + client_status.desktop = ( + guilds.PresenceStatus(client_status_payload["desktop"]) + if "desktop" in client_status_payload + else guilds.PresenceStatus.OFFLINE + ) + # noinspection PyArgumentList + client_status.mobile = ( + guilds.PresenceStatus(client_status_payload["mobile"]) + if "mobile" in client_status_payload + else guilds.PresenceStatus.OFFLINE + ) + # noinspection PyArgumentList + client_status.web = ( + guilds.PresenceStatus(client_status_payload["web"]) + if "web" in client_status_payload + else guilds.PresenceStatus.OFFLINE + ) + guild_member_presence.client_status = client_status + if (premium_since := payload.get("premium_since")) is not None: + premium_since = date.iso8601_datetime_string_to_datetime(premium_since) + # TODO: do we want to differentiate between unset and null here? + guild_member_presence.premium_since = premium_since + guild_member_presence.nickname = payload.get("nick") + return guild_member_presence + + @staticmethod + def _set_partial_integration_attributes( + payload: binding.JSONObject, integration: PartialGuildIntegrationT + ) -> PartialGuildIntegrationT: + integration.id = base_models.Snowflake(payload["id"]) + integration.name = payload["name"] + integration.type = payload["type"] + account_payload = payload["account"] + account = guilds.IntegrationAccount() + account.id = account_payload["id"] + account.name = account_payload["name"] + integration.account = account + return integration + + def deserialize_partial_integration(self, payload: binding.JSONObject) -> guilds.PartialIntegration: + """Parse a raw payload from Discord into a partial integration object. + + Parameters + ---------- + payload : Mapping[Hashable, Any] + The dict payload to parse. + + Returns + ------- + hikari.models.guilds.PartialIntegration + The parsed partial integration object. + """ + return self._set_partial_integration_attributes(payload, guilds.PartialIntegration()) - def deserialize_partial_guild_integration(self, payload: binding.JSONObject) -> guilds.PartialGuildIntegration: - pass + def deserialize_integration(self, payload: binding.JSONObject) -> guilds.Integration: + """Parse a raw payload from Discord into an integration object. - def deserialize_guild_integration(self, payload: binding.JSONObject) -> guilds.GuildIntegration: - pass + Parameters + ---------- + payload : Mapping[Hashable, Any] + The dict payload to parse. + + Returns + ------- + hikari.models.guilds.Integration + The parsed integration object. + """ + guild_integration = self._set_partial_integration_attributes(payload, guilds.Integration()) + guild_integration.is_enabled = payload["enabled"] + guild_integration.is_syncing = payload["syncing"] + if (role_id := payload.get("role_id")) is not None: + role_id = base_models.Snowflake(role_id) + guild_integration.role_id = role_id + guild_integration.is_emojis_enabled = payload.get("enable_emoticons") + # noinspection PyArgumentList + guild_integration.expire_behavior = guilds.IntegrationExpireBehaviour(payload["expire_behavior"]) + guild_integration.expire_grace_period = datetime.timedelta(days=payload["expire_grace_period"]) + guild_integration.user = self.deserialize_user(payload["user"]) + if (last_synced_at := payload["synced_at"]) is not None: + last_synced_at = date.iso8601_datetime_string_to_datetime(last_synced_at) + guild_integration.last_synced_at = last_synced_at + return guild_integration def deserialize_guild_member_ban(self, payload: binding.JSONObject) -> guilds.GuildMemberBan: - pass + """Parse a raw payload from Discord into a guild member ban object. + + Parameters + ---------- + payload : Mapping[Hashable, Any] + The dict payload to parse. + + Returns + ------- + hikari.models.GuildMemberBan + The parsed guild member ban object. + """ + guild_member_ban = guilds.GuildMemberBan() + guild_member_ban.reason = payload["reason"] + guild_member_ban.user = self.deserialize_user(payload["user"]) + return guild_member_ban def deserialize_unavailable_guild(self, payload: binding.JSONObject) -> guilds.UnavailableGuild: - pass + """Parse a raw payload from Discord into a unavailable guild object. + + Parameters + ---------- + payload : Mapping[Hashable, Any] + The dict payload to parse. + + Returns + ------- + hikari.models.guilds.UnavailableGuild + The parsed unavailable guild object. + """ + unavailable_guild = guilds.UnavailableGuild() + unavailable_guild.set_app(self._app) + unavailable_guild.id = base_models.Snowflake(payload["id"]) + return unavailable_guild + + def _set_partial_guild_attributes(self, payload: binding.JSONObject, guild: PartialGuildT) -> PartialGuildT: + guild.set_app(self._app) + guild.id = base_models.Snowflake(payload["id"]) + guild.name = payload["name"] + guild.icon_hash = payload["icon"] + features = [] + for feature in payload["features"]: + try: + features.append(guilds.GuildFeature(feature)) + except ValueError: + features.append(feature) + guild.features = set(features) + return guild def deserialize_guild_preview(self, payload: binding.JSONObject) -> guilds.GuildPreview: - pass + """Parse a raw payload from Discord into a guild preview object. + + Parameters + ---------- + payload : Mapping[Hashable, Any] + The dict payload to parse. + + Returns + ------- + hikari.models.guilds.GuildPreview + The parsed guild preview object. + """ + guild_preview = self._set_partial_guild_attributes(payload, guilds.GuildPreview()) + guild_preview.splash_hash = payload["splash"] + guild_preview.discovery_splash_hash = payload["discovery_splash"] + guild_preview.emojis = { + base_models.Snowflake(emoji["id"]): self.deserialize_known_custom_emoji(emoji) + for emoji in payload["emojis"] + } + guild_preview.approximate_presence_count = int(payload["approximate_presence_count"]) + guild_preview.approximate_member_count = int(payload["approximate_member_count"]) + guild_preview.description = payload["description"] + return guild_preview def deserialize_guild(self, payload: binding.JSONObject) -> guilds.Guild: - pass + """Parse a raw payload from Discord into a guild object. + + Parameters + ---------- + payload : Mapping[Hashable, Any] + The dict payload to parse. + + Returns + ------- + hikari.models.guilds.Guild + The parsed guild object. + """ + guild = self._set_partial_guild_attributes(payload, guilds.Guild()) + guild.splash_hash = payload["splash"] + guild.discovery_splash_hash = payload["discovery_splash"] + guild.owner_id = base_models.Snowflake(payload["owner_id"]) + # noinspection PyArgumentList + guild.my_permissions = permissions.Permission(payload["permissions"]) if "permissions" in payload else None + guild.region = payload["region"] + if (afk_channel_id := payload["afk_channel_id"]) is not None: + afk_channel_id = base_models.Snowflake(afk_channel_id) + guild.afk_channel_id = afk_channel_id + guild.afk_timeout = datetime.timedelta(seconds=payload["afk_timeout"]) + guild.is_embed_enabled = payload.get("embed_enabled", False) + if (embed_channel_id := payload.get("embed_channel_id")) is not None: + embed_channel_id = base_models.Snowflake(embed_channel_id) + guild.embed_channel_id = embed_channel_id + # noinspection PyArgumentList + guild.verification_level = guilds.GuildVerificationLevel(payload["verification_level"]) + # noinspection PyArgumentList + guild.default_message_notifications = guilds.GuildMessageNotificationsLevel( + payload["default_message_notifications"] + ) + # noinspection PyArgumentList + guild.explicit_content_filter = guilds.GuildExplicitContentFilterLevel(payload["explicit_content_filter"]) + guild.roles = {base_models.Snowflake(role["id"]): self.deserialize_role(role) for role in payload["roles"]} + guild.emojis = { + base_models.Snowflake(emoji["id"]): self.deserialize_known_custom_emoji(emoji) + for emoji in payload["emojis"] + } + # noinspection PyArgumentList + guild.mfa_level = guilds.GuildMFALevel(payload["mfa_level"]) + if (application_id := payload["application_id"]) is not None: + application_id = base_models.Snowflake(application_id) + guild.application_id = application_id + guild.is_unavailable = payload["unavailable"] if "unavailable" in payload else None + guild.is_widget_enabled = payload["widget_enabled"] if "widget_enabled" in payload else None + if (widget_channel_id := payload.get("widget_channel_id")) is not None: + widget_channel_id = base_models.Snowflake(widget_channel_id) + guild.widget_channel_id = widget_channel_id + if (system_channel_id := payload["system_channel_id"]) is not None: + system_channel_id = base_models.Snowflake(system_channel_id) + guild.system_channel_id = system_channel_id + # noinspection PyArgumentList + guild.system_channel_flags = guilds.GuildSystemChannelFlag(payload["system_channel_flags"]) + if (rules_channel_id := payload["rules_channel_id"]) is not None: + rules_channel_id = base_models.Snowflake(rules_channel_id) + guild.rules_channel_id = rules_channel_id + guild.joined_at = ( + date.iso8601_datetime_string_to_datetime(payload["joined_at"]) if "joined_at" in payload else None + ) + guild.is_large = payload["large"] if "large" in payload else None + guild.member_count = int(payload["member_count"]) if "member_count" in payload else None + if (members := payload.get("members", ...)) is not ...: + guild.members = { + base_models.Snowflake(member["user"]["id"]): self.deserialize_guild_member(member) for member in members + } + else: + guild.members = None + if (channels := payload.get("channels", ...)) is not ...: + guild.channels = { + base_models.Snowflake(channel["id"]): self.deserialize_channel(channel) for channel in channels + } + else: + guild.channels = None + if (presences := payload.get("presences", ...)) is not ...: + guild.presences = { + base_models.Snowflake(presence["user"]["id"]): self.deserialize_guild_member_presence(presence) + for presence in presences + } + else: + guild.presences = None + if (max_presences := payload.get("max_presences")) is not None: + max_presences = int(max_presences) + guild.max_presences = max_presences + guild.max_members = int(payload["max_members"]) if "max_members" in payload else None + guild.max_video_channel_users = ( + int(payload["max_video_channel_users"]) if "max_video_channel_users" in payload else None + ) + guild.vanity_url_code = payload["vanity_url_code"] + guild.description = payload["description"] + guild.banner_hash = payload["banner"] + # noinspection PyArgumentList + guild.premium_tier = guilds.GuildPremiumTier(payload["premium_tier"]) + if (premium_subscription_count := payload.get("premium_subscription_count")) is not None: + premium_subscription_count = int(premium_subscription_count) + guild.premium_subscription_count = premium_subscription_count + guild.preferred_locale = payload["preferred_locale"] + if (public_updates_channel_id := payload["public_updates_channel_id"]) is not None: + public_updates_channel_id = base_models.Snowflake(public_updates_channel_id) + guild.public_updates_channel_id = public_updates_channel_id + guild.approximate_member_count = ( + int(payload["approximate_member_count"]) if "approximate_member_count" in payload else None + ) + guild.approximate_active_member_count = ( + int(payload["approximate_presence_count"]) if "approximate_presence_count" in payload else None + ) + return guild + + ########### + # INVITES # + ########### def deserialize_vanity_url(self, payload: binding.JSONObject) -> invites.VanityURL: - pass + """Parse a raw payload from Discord into a vanity url object. + + Parameters + ---------- + payload : Mapping[Hashable, Any] + The dict payload to parse. + + Returns + ------- + hikari.models.invites.VanityURL + The parsed vanity url object. + """ + vanity_url = invites.VanityURL() + vanity_url.set_app(self._app) + vanity_url.code = payload["code"] + vanity_url.uses = int(payload["uses"]) + return vanity_url + + def _set_invite_attributes(self, payload: binding.JSONObject, invite: InviteT) -> InviteT: + invite.set_app(self._app) + invite.code = payload["code"] + if (guild_payload := payload.get("guild", ...)) is not ...: + guild = self._set_partial_guild_attributes(guild_payload, invites.InviteGuild()) + guild.splash_hash = guild_payload["splash"] + guild.banner_hash = guild_payload["banner"] + guild.description = guild_payload["description"] + # noinspection PyArgumentList + guild.verification_level = guilds.GuildVerificationLevel(guild_payload["verification_level"]) + guild.vanity_url_code = guild_payload["vanity_url_code"] + invite.guild = guild + else: + invite.guild = None + invite.channel = self.deserialize_partial_channel(payload["channel"]) + invite.inviter = self.deserialize_user(payload["inviter"]) if "inviter" in payload else None + invite.target_user = self.deserialize_user(payload["target_user"]) if "target_user" in payload else None + # noinspection PyArgumentList + invite.target_user_type = ( + invites.TargetUserType(payload["target_user_type"]) if "target_user_type" in payload else None + ) + invite.approximate_presence_count = ( + int(payload["approximate_presence_count"]) if "approximate_presence_count" in payload else None + ) + invite.approximate_member_count = ( + int(payload["approximate_member_count"]) if "approximate_member_count" in payload else None + ) + return invite def deserialize_invite(self, payload: binding.JSONObject) -> invites.Invite: - pass + """Parse a raw payload from Discord into an invite object. + + Parameters + ---------- + payload : Mapping[Hashable, Any] + The dict payload to parse. + + Returns + ------- + hikari.models.invites.Invite + The parsed invite object. + """ + return self._set_invite_attributes(payload, invites.Invite()) def deserialize_invite_with_metadata(self, payload: binding.JSONObject) -> invites.InviteWithMetadata: - pass + """Parse a raw payload from Discord into a invite with metadata object. + + Parameters + ---------- + payload : Mapping[Hashable, Any] + The dict payload to parse. + + Returns + ------- + hikari.models.invites.InviteWithMetadata + The parsed invite with metadata object. + """ + invite_with_metadata = self._set_invite_attributes(payload, invites.InviteWithMetadata()) + invite_with_metadata.uses = int(payload["uses"]) + invite_with_metadata.max_uses = int(payload["max_uses"]) + max_age = payload["max_age"] + invite_with_metadata.max_age = datetime.timedelta(seconds=max_age) if max_age > 0 else None + invite_with_metadata.is_temporary = payload["temporary"] + invite_with_metadata.created_at = date.iso8601_datetime_string_to_datetime(payload["created_at"]) + return invite_with_metadata + + ############ + # MESSAGES # + ############ + + # TODO: arbitrarily partial ver? + def deserialize_message(self, payload: binding.JSONObject) -> messages.Message: + """Parse a raw payload from Discord into a message object. + + Parameters + ---------- + payload : Mapping[Hashable, Any] + The dict payload to parse. + + Returns + ------- + hikari.models.messages.Message + The parsed message object. + """ + message = messages.Message() + message.set_app(self._app) + message.id = base_models.Snowflake(payload["id"]) + message.channel_id = base_models.Snowflake(payload["channel_id"]) + message.guild_id = base_models.Snowflake(payload["guild_id"]) if "guild_id" in payload else None + message.author = self.deserialize_user(payload["author"]) + message.member = ( + self.deserialize_guild_member(payload["member"], user=message.author) if "member" in payload else None + ) + message.content = payload["content"] + message.timestamp = date.iso8601_datetime_string_to_datetime(payload["timestamp"]) + if (edited_timestamp := payload["edited_timestamp"]) is not None: + edited_timestamp = date.iso8601_datetime_string_to_datetime(edited_timestamp) + message.edited_timestamp = edited_timestamp + message.is_tts = payload["tts"] + message.is_mentioning_everyone = payload["mention_everyone"] + message.user_mentions = {base_models.Snowflake(mention["id"]) for mention in payload["mentions"]} + message.role_mentions = {base_models.Snowflake(mention) for mention in payload["mention_roles"]} + message.channel_mentions = { + base_models.Snowflake(mention["id"]) for mention in payload.get("mention_channels", ()) + } + attachments = [] + for attachment_payload in payload["attachments"]: + attachment = messages.Attachment() + attachment.id = base_models.Snowflake(attachment_payload["id"]) + attachment.filename = attachment_payload["filename"] + attachment.size = int(attachment_payload["size"]) + attachment.url = attachment_payload["url"] + attachment.proxy_url = attachment_payload["proxy_url"] + attachment.height = attachment_payload.get("height") + attachment.width = attachment_payload.get("width") + attachments.append(attachment) + message.attachments = attachments + message.embeds = [self.deserialize_embed(embed) for embed in payload["embeds"]] + reactions = [] + for reaction_payload in payload.get("reactions", ()): + reaction = messages.Reaction() + reaction.count = int(reaction_payload["count"]) + reaction.emoji = self.deserialize_emoji(reaction_payload["emoji"]) + reaction.is_reacted_by_me = reaction_payload["me"] + reactions.append(reaction) + message.reactions = reactions + message.is_pinned = payload["pinned"] + message.webhook_id = base_models.Snowflake(payload["webhook_id"]) if "webhook_id" in payload else None + # noinspection PyArgumentList + message.type = messages.MessageType(payload["type"]) + if (activity_payload := payload.get("activity", ...)) is not ...: + activity = messages.MessageActivity() + # noinspection PyArgumentList + activity.type = messages.MessageActivityType(activity_payload["type"]) + activity.party_id = activity_payload.get("party_id") + message.activity = activity + else: + message.activity = None + message.application = self.deserialize_application(payload["application"]) if "application" in payload else None + if (crosspost_payload := payload.get("message_reference", ...)) is not ...: + crosspost = messages.MessageCrosspost() + crosspost.set_app(self._app) + crosspost.id = ( + base_models.Snowflake(crosspost_payload["message_id"]) if "message_id" in crosspost_payload else None + ) + crosspost.channel_id = base_models.Snowflake(crosspost_payload["channel_id"]) + crosspost.guild_id = ( + base_models.Snowflake(crosspost_payload["guild_id"]) if "guild_id" in crosspost_payload else None + ) + message.message_reference = crosspost + else: + message.message_reference = None + # noinspection PyArgumentList + message.flags = messages.MessageFlag(payload["flags"]) if "flags" in payload else None + message.nonce = payload.get("nonce") + return message + + ######### + # USERS # + ######### + + def _set_user_attributes(self, payload: binding.JSONObject, user: UserT) -> UserT: + user.set_app(self._app) + user.id = base_models.Snowflake(payload["id"]) + user.discriminator = payload["discriminator"] + user.username = payload["username"] + user.avatar_hash = payload["avatar"] + user.is_bot = payload.get("bot", False) + user.is_system = payload.get("system", False) + return user def deserialize_user(self, payload: binding.JSONObject) -> users.User: - pass + """Parse a raw payload from Discord into a user object. + + Parameters + ---------- + payload : Mapping[Hashable, Any] + The dict payload to parse. + + Returns + ------- + hikari.models.users.User + The parsed user object. + """ + user = self._set_user_attributes(payload, users.User()) + user.flags = users.UserFlag(payload["public_flags"]) if "public_flags" in payload else users.UserFlag.NONE + return user def deserialize_my_user(self, payload: binding.JSONObject) -> users.MyUser: - pass + """Parse a raw payload from Discord into a my user object. + + Parameters + ---------- + payload : Mapping[Hashable, Any] + The dict payload to parse. + + Returns + ------- + hikari.models.users.MyUser + The parsed my user object. + """ + my_user = self._set_user_attributes(payload, users.MyUser()) + my_user.is_mfa_enabled = payload["mfa_enabled"] + my_user.locale = payload.get("locale") + my_user.is_verified = payload.get("verified") + my_user.email = payload.get("email") + # noinspection PyArgumentList + my_user.flags = users.UserFlag(payload["flags"]) + # noinspection PyArgumentList + my_user.premium_type = users.PremiumType(payload["premium_type"]) if "premium_type" in payload else None + return my_user + + ########## + # Voices # + ########## def deserialize_voice_state(self, payload: binding.JSONObject) -> voices.VoiceState: - pass + """Parse a raw payload from Discord into a voice state object. + + Parameters + ---------- + payload : Mapping[Hashable, Any] + The dict payload to parse. + + Returns + ------- + hikari.models.voices.VoiceState + The parsed voice state object. + """ + voice_state = voices.VoiceState() + voice_state.set_app(self._app) + voice_state.guild_id = base_models.Snowflake(payload["guild_id"]) if "guild_id" in payload else None + if (channel_id := payload["channel_id"]) is not None: + channel_id = base_models.Snowflake(channel_id) + voice_state.channel_id = channel_id + voice_state.user_id = base_models.Snowflake(payload["user_id"]) + voice_state.member = self.deserialize_guild_member(payload["member"]) if "member" in payload else None + voice_state.session_id = payload["session_id"] + voice_state.is_guild_deafened = payload["deaf"] + voice_state.is_guild_muted = payload["mute"] + voice_state.is_self_deafened = payload["self_deaf"] + voice_state.is_self_muted = payload["self_mute"] + voice_state.is_streaming = payload.get("self_stream", False) + voice_state.is_suppressed = payload["suppress"] + return voice_state def deserialize_voice_region(self, payload: binding.JSONObject) -> voices.VoiceRegion: - pass + """Parse a raw payload from Discord into a voice region object. + + Parameters + ---------- + payload : Mapping[Hashable, Any] + The dict payload to parse. + + Returns + ------- + hikari.models.voices.VoiceRegion + The parsed voice region object. + """ + voice_region = voices.VoiceRegion() + voice_region.id = payload["id"] + voice_region.name = payload["name"] + voice_region.is_vip = payload["vip"] + voice_region.is_optimal_location = payload["optimal"] + voice_region.is_deprecated = payload["deprecated"] + voice_region.is_custom = payload["custom"] + return voice_region + + ############ + # WEBHOOKS # + ############ def deserialize_webhook(self, payload: binding.JSONObject) -> webhooks.Webhook: - pass + """Parse a raw payload from Discord into a webhook object. + + Parameters + ---------- + payload : Mapping[Hashable, Any] + The dict payload to parse. + + Returns + ------- + hikari.models.webhooks.Webhook + The parsed webhook object. + """ + webhook = webhooks.Webhook() + webhook.id = base_models.Snowflake(payload["id"]) + # noinspection PyArgumentList + webhook.type = webhooks.WebhookType(payload["type"]) + webhook.guild_id = base_models.Snowflake(payload["guild_id"]) if "guild_id" in payload else None + webhook.channel_id = base_models.Snowflake(payload["channel_id"]) + webhook.author = self.deserialize_user(payload["user"]) if "user" in payload else None + webhook.name = payload["name"] + webhook.avatar_hash = payload["avatar"] + webhook.token = payload.get("token") + return webhook diff --git a/hikari/models/applications.py b/hikari/models/applications.py index 8b07af38db..1bdda2689c 100644 --- a/hikari/models/applications.py +++ b/hikari/models/applications.py @@ -31,24 +31,22 @@ "TeamMembershipState", ] +import enum import typing import attr -from hikari.internal import marshaller -from hikari.internal import more_enums from . import bases from . import guilds -from . import permissions -from . import users from ..net import urls if typing.TYPE_CHECKING: - from hikari.internal import more_typing + from . import permissions as permissions_ + from . import users -@more_enums.must_be_unique -class OAuth2Scope(str, more_enums.Enum): +@enum.unique +class OAuth2Scope(str, enum.Enum): """OAuth2 Scopes that Discord allows. These are categories of permissions for applications using the OAuth2 API @@ -171,8 +169,8 @@ class OAuth2Scope(str, more_enums.Enum): """ -@more_enums.must_be_unique -class ConnectionVisibility(int, more_enums.Enum): +@enum.unique +class ConnectionVisibility(int, enum.Enum): """Describes who can see a connection with a third party account.""" NONE = 0 @@ -182,79 +180,62 @@ class ConnectionVisibility(int, more_enums.Enum): """Everyone can see the connection.""" -def _deserialize_integrations( - payload: more_typing.JSONArray, **kwargs: typing.Any -) -> typing.Sequence[guilds.GuildIntegration]: - return [guilds.PartialGuildIntegration.deserialize(integration, **kwargs) for integration in payload] - - -@marshaller.marshallable() -@attr.s(eq=True, hash=True, kw_only=True, slots=True) -class OwnConnection(bases.Entity, marshaller.Deserializable): +@attr.s(eq=True, hash=True, init=False, kw_only=True, slots=True) +class OwnConnection: """Represents a user's connection with a third party account. Returned by the `GET Current User Connections` endpoint. """ - id: str = marshaller.attrib(deserializer=str, eq=True, hash=True, repr=True) + id: str = attr.ib(eq=True, hash=True, repr=True) """The string ID of the third party connected account. !!! warning Seeing as this is a third party ID, it will not be a snowflake. """ - name: str = marshaller.attrib(deserializer=str, eq=False, hash=False, repr=True) + name: str = attr.ib(eq=False, hash=False, repr=True) """The username of the connected account.""" - type: str = marshaller.attrib(deserializer=str, eq=False, hash=False, repr=True) + type: str = attr.ib(eq=False, hash=False, repr=True) """The type of service this connection is for.""" - is_revoked: bool = marshaller.attrib( - raw_name="revoked", deserializer=bool, if_undefined=False, default=False, eq=False, hash=False, + is_revoked: bool = attr.ib( + eq=False, hash=False, ) """Whether the connection has been revoked.""" - integrations: typing.Sequence[guilds.PartialGuildIntegration] = marshaller.attrib( - deserializer=_deserialize_integrations, - if_undefined=list, - factory=list, - inherit_kwargs=True, - eq=False, - hash=False, + integrations: typing.Sequence[guilds.PartialIntegration] = attr.ib( + eq=False, hash=False, ) """A sequence of the partial guild integration objects this connection has.""" - is_verified: bool = marshaller.attrib(raw_name="verified", deserializer=bool, eq=False, hash=False) + is_verified: bool = attr.ib(eq=False, hash=False) """Whether the connection has been verified.""" - is_friend_syncing: bool = marshaller.attrib(raw_name="friend_sync", deserializer=bool, eq=False, hash=False) + is_friend_syncing: bool = attr.ib(eq=False, hash=False) """Whether friends should be added based on this connection.""" - is_showing_activity: bool = marshaller.attrib(raw_name="show_activity", deserializer=bool, eq=False, hash=False) + is_showing_activity: bool = attr.ib(eq=False, hash=False) """Whether this connection's activities are shown in the user's presence.""" - visibility: ConnectionVisibility = marshaller.attrib( - deserializer=ConnectionVisibility, eq=False, hash=False, repr=True - ) + visibility: ConnectionVisibility = attr.ib(eq=False, hash=False, repr=True) """The visibility of the connection.""" -@marshaller.marshallable() -@attr.s(eq=True, hash=True, kw_only=True, slots=True) +@attr.s(eq=True, hash=True, init=False, kw_only=True, slots=True) class OwnGuild(guilds.PartialGuild): """Represents a user bound partial guild object.""" - is_owner: bool = marshaller.attrib(raw_name="owner", deserializer=bool, eq=False, hash=False, repr=True) + is_owner: bool = attr.ib(eq=False, hash=False, repr=True) """Whether the current user owns this guild.""" - my_permissions: permissions.Permission = marshaller.attrib( - raw_name="permissions", deserializer=permissions.Permission, eq=False, hash=False - ) + my_permissions: permissions_.Permission = attr.ib(eq=False, hash=False) """The guild level permissions that apply to the current user or bot.""" -@more_enums.must_be_unique -class TeamMembershipState(int, more_enums.Enum): +@enum.unique +class TeamMembershipState(int, enum.Enum): """Represents the state of a user's team membership.""" INVITED = 1 @@ -264,51 +245,37 @@ class TeamMembershipState(int, more_enums.Enum): """Denotes the user has accepted the invite and is now a member.""" -@marshaller.marshallable() -@attr.s(eq=True, hash=True, kw_only=True, slots=True) -class TeamMember(bases.Entity, marshaller.Deserializable): +@attr.s(eq=True, hash=True, init=False, kw_only=True, slots=True) +class TeamMember(bases.Entity): """Represents a member of a Team.""" - membership_state: TeamMembershipState = marshaller.attrib(deserializer=TeamMembershipState, eq=False, hash=False) + membership_state: TeamMembershipState = attr.ib(eq=False, hash=False) """The state of this user's membership.""" - permissions: typing.Set[str] = marshaller.attrib(deserializer=set, eq=False, hash=False) + permissions: typing.Set[str] = attr.ib(eq=False, hash=False) """This member's permissions within a team. Will always be `["*"]` until Discord starts using this. """ - team_id: bases.Snowflake = marshaller.attrib(deserializer=bases.Snowflake, eq=True, hash=True, repr=True) + team_id: bases.Snowflake = attr.ib(eq=True, hash=True, repr=True) """The ID of the team this member belongs to.""" - user: users.User = marshaller.attrib( - deserializer=users.User.deserialize, inherit_kwargs=True, eq=True, hash=True, repr=True - ) + user: users.User = attr.ib(eq=True, hash=True, repr=True) """The user object of this team member.""" -def _deserialize_members( - payload: more_typing.JSONArray, **kwargs: typing.Any -) -> typing.Mapping[bases.Snowflake, TeamMember]: - return {bases.Snowflake(member["user"]["id"]): TeamMember.deserialize(member, **kwargs) for member in payload} - - -@marshaller.marshallable() -@attr.s(eq=True, hash=True, kw_only=True, slots=True) -class Team(bases.Unique, marshaller.Deserializable): +@attr.s(eq=True, hash=True, init=False, kw_only=True, slots=True) +class Team(bases.Entity, bases.Unique): """Represents a development team, along with all its members.""" - icon_hash: typing.Optional[str] = marshaller.attrib( - raw_name="icon", deserializer=str, if_none=None, eq=False, hash=False - ) + icon_hash: typing.Optional[str] = attr.ib(eq=False, hash=False) """The hash of this team's icon, if set.""" - members: typing.Mapping[bases.Snowflake, TeamMember] = marshaller.attrib( - deserializer=_deserialize_members, inherit_kwargs=True, eq=False, hash=False - ) + members: typing.Mapping[bases.Snowflake, TeamMember] = attr.ib(eq=False, hash=False) """The member's that belong to this team.""" - owner_user_id: bases.Snowflake = marshaller.attrib(deserializer=bases.Snowflake, eq=False, hash=False, repr=True) + owner_user_id: bases.Snowflake = attr.ib(eq=False, hash=False, repr=True) """The ID of this team's owner.""" @property @@ -343,45 +310,30 @@ def format_icon_url(self, *, format_: str = "png", size: int = 4096) -> typing.O return None -def _deserialize_verify_key(payload: str) -> bytes: - return bytes(payload, "utf-8") - - -@marshaller.marshallable() -@attr.s(eq=True, hash=True, kw_only=True, slots=True) -class Application(bases.Unique, marshaller.Deserializable): +@attr.s(eq=True, hash=True, init=False, kw_only=True, slots=True) +class Application(bases.Entity, bases.Unique): """Represents the information of an Oauth2 Application.""" - name: str = marshaller.attrib(deserializer=str, eq=False, hash=False, repr=True) + name: str = attr.ib(eq=False, hash=False, repr=True) """The name of this application.""" - description: str = marshaller.attrib(deserializer=str, eq=False, hash=False) + description: str = attr.ib(eq=False, hash=False) """The description of this application, will be an empty string if unset.""" - is_bot_public: typing.Optional[bool] = marshaller.attrib( - raw_name="bot_public", deserializer=bool, if_undefined=None, default=None, eq=False, hash=False, repr=True - ) + is_bot_public: typing.Optional[bool] = attr.ib(eq=False, hash=False, repr=True) """Whether the bot associated with this application is public. Will be `None` if this application doesn't have an associated bot. """ - is_bot_code_grant_required: typing.Optional[bool] = marshaller.attrib( - raw_name="bot_require_code_grant", deserializer=bool, if_undefined=None, default=None, eq=False, hash=False - ) + is_bot_code_grant_required: typing.Optional[bool] = attr.ib(eq=False, hash=False) """Whether this application's bot is requiring code grant for invites. Will be `None` if this application doesn't have a bot. """ - owner: typing.Optional[users.User] = marshaller.attrib( - deserializer=users.User.deserialize, - if_undefined=None, - default=None, - inherit_kwargs=True, - eq=False, - hash=False, - repr=True, + owner: typing.Optional[users.User] = attr.ib( + eq=False, hash=False, repr=True, ) """The object of this application's owner. @@ -389,59 +341,39 @@ class Application(bases.Unique, marshaller.Deserializable): Discord's oauth2 flow. """ - rpc_origins: typing.Optional[typing.Set[str]] = marshaller.attrib( - deserializer=set, if_undefined=None, default=None, eq=False, hash=False - ) + rpc_origins: typing.Optional[typing.Set[str]] = attr.ib(eq=False, hash=False) """A collection of this application's rpc origin URLs, if rpc is enabled.""" - summary: str = marshaller.attrib(deserializer=str, eq=False, hash=False) + summary: str = attr.ib(eq=False, hash=False) """This summary for this application's primary SKU if it's sold on Discord. Will be an empty string if unset. """ - verify_key: typing.Optional[bytes] = marshaller.attrib( - deserializer=_deserialize_verify_key, if_undefined=None, default=None, eq=False, hash=False - ) + verify_key: typing.Optional[bytes] = attr.ib(eq=False, hash=False) """The base64 encoded key used for the GameSDK's `GetTicket`.""" - icon_hash: typing.Optional[str] = marshaller.attrib( - raw_name="icon", deserializer=str, if_undefined=None, if_none=None, default=None, eq=False, hash=False - ) + icon_hash: typing.Optional[str] = attr.ib(eq=False, hash=False) """The hash of this application's icon, if set.""" - team: typing.Optional[Team] = marshaller.attrib( - deserializer=Team.deserialize, - if_undefined=None, - if_none=None, - default=None, - eq=False, - hash=False, - inherit_kwargs=True, + team: typing.Optional[Team] = attr.ib( + eq=False, hash=False, ) """This application's team if it belongs to one.""" - guild_id: typing.Optional[bases.Snowflake] = marshaller.attrib( - deserializer=bases.Snowflake, if_undefined=None, default=None, eq=False, hash=False - ) + guild_id: typing.Optional[bases.Snowflake] = attr.ib(eq=False, hash=False) """The ID of the guild this application is linked to if sold on Discord.""" - primary_sku_id: typing.Optional[bases.Snowflake] = marshaller.attrib( - deserializer=bases.Snowflake, if_undefined=None, default=None, eq=False, hash=False - ) + primary_sku_id: typing.Optional[bases.Snowflake] = attr.ib(eq=False, hash=False) """The ID of the primary "Game SKU" of a game that's sold on Discord.""" - slug: typing.Optional[str] = marshaller.attrib( - deserializer=str, if_undefined=None, default=None, eq=False, hash=False - ) + slug: typing.Optional[str] = attr.ib(eq=False, hash=False) """The URL slug that links to this application's store page. Only applicable to applications sold on Discord. """ - cover_image_hash: typing.Optional[str] = marshaller.attrib( - raw_name="cover_image", deserializer=str, if_undefined=None, default=None, eq=False, hash=False - ) + cover_image_hash: typing.Optional[str] = attr.ib(eq=False, hash=False) """The hash of this application's cover image on it's store, if set.""" @property diff --git a/hikari/models/audit_logs.py b/hikari/models/audit_logs.py index 9f9939d234..42d4389fd6 100644 --- a/hikari/models/audit_logs.py +++ b/hikari/models/audit_logs.py @@ -21,14 +21,12 @@ from __future__ import annotations __all__ = [ - "AuditLog", "AuditLogChange", "AuditLogChangeKey", "AuditLogEntry", "AuditLogEventType", "BaseAuditLogEntryInfo", "ChannelOverwriteEntryInfo", - "get_entry_info_entity", "MemberDisconnectEntryInfo", "MemberMoveEntryInfo", "MemberPruneEntryInfo", @@ -40,16 +38,12 @@ import abc import datetime +import enum import typing import attr -from hikari.internal import conversions -from hikari.internal import marshaller -from hikari.internal import more_collections -from hikari.internal import more_enums from . import bases -from . import channels from . import colors from . import guilds from . import permissions @@ -57,10 +51,10 @@ from . import webhooks as webhooks_ if typing.TYPE_CHECKING: - from hikari.internal import more_typing + from . import channels -class AuditLogChangeKey(str, more_enums.Enum): +class AuditLogChangeKey(str, enum.Enum): """Commonly known and documented keys for audit log change objects. Others may exist. These should be expected to default to the raw string @@ -128,25 +122,10 @@ def _deserialize_seconds_timedelta(seconds: typing.Union[str, int]) -> datetime. return datetime.timedelta(seconds=int(seconds)) -def _deserialize_partial_roles( - payload: more_typing.JSONArray, **kwargs: typing.Any -) -> typing.Mapping[bases.Snowflake, guilds.Role]: - return {bases.Snowflake(role["id"]): guilds.PartialRole.deserialize(role, **kwargs) for role in payload} - - def _deserialize_day_timedelta(days: typing.Union[str, int]) -> datetime.timedelta: return datetime.timedelta(days=int(days)) -def _deserialize_overwrites( - payload: more_typing.JSONArray, **kwargs: typing.Any -) -> typing.Mapping[bases.Snowflake, channels.PermissionOverwrite]: - return { - bases.Snowflake(overwrite["id"]): channels.PermissionOverwrite.deserialize(overwrite, **kwargs) - for overwrite in payload - } - - def _deserialize_max_uses(age: int) -> typing.Union[int, float]: return age if age > 0 else float("inf") @@ -186,46 +165,23 @@ def _deserialize_max_age(seconds: int) -> typing.Optional[datetime.timedelta]: AuditLogChangeKey.SYSTEM_CHANNEL_ID: bases.Snowflake, } -COMPONENT_BOUND_AUDIT_LOG_ENTRY_CONVERTERS = { - AuditLogChangeKey.ADD_ROLE_TO_MEMBER: _deserialize_partial_roles, - AuditLogChangeKey.REMOVE_ROLE_FROM_MEMBER: _deserialize_partial_roles, - AuditLogChangeKey.PERMISSION_OVERWRITES: _deserialize_overwrites, -} - -@marshaller.marshallable() -@attr.s(eq=True, hash=False, kw_only=True, slots=True) -class AuditLogChange(bases.Entity, marshaller.Deserializable): +@attr.s(eq=True, hash=False, init=False, kw_only=True, slots=True) +class AuditLogChange: """Represents a change made to an audit log entry's target entity.""" - new_value: typing.Optional[typing.Any] = attr.attrib(repr=True) + new_value: typing.Optional[typing.Any] = attr.ib(repr=True) """The new value of the key, if something was added or changed.""" - old_value: typing.Optional[typing.Any] = attr.attrib(repr=True) + old_value: typing.Optional[typing.Any] = attr.ib(repr=True) """The old value of the key, if something was removed or changed.""" - key: typing.Union[AuditLogChangeKey, str] = attr.attrib(repr=True) + key: typing.Union[AuditLogChangeKey, str] = attr.ib(repr=True) """The name of the audit log change's key.""" - @classmethod - def deserialize(cls, payload: typing.Mapping[str, str], **kwargs: typing.Any) -> AuditLogChange: - """Deserialize this model from a raw payload.""" - key = conversions.try_cast(payload["key"], AuditLogChangeKey, payload["key"]) - new_value = payload.get("new_value") - old_value = payload.get("old_value") - if value_converter := AUDIT_LOG_ENTRY_CONVERTERS.get(key): - new_value = value_converter(new_value) if new_value is not None else None - old_value = value_converter(old_value) if old_value is not None else None - elif value_converter := COMPONENT_BOUND_AUDIT_LOG_ENTRY_CONVERTERS.get(key): - new_value = value_converter(new_value, **kwargs) if new_value is not None else None - old_value = value_converter(old_value, **kwargs) if old_value is not None else None - - # noinspection PyArgumentList - return cls(key=key, new_value=new_value, old_value=old_value, **kwargs) - - -@more_enums.must_be_unique -class AuditLogEventType(int, more_enums.Enum): + +@enum.unique +class AuditLogEventType(int, enum.Enum): """The type of event that occurred.""" GUILD_UPDATE = 1 @@ -265,50 +221,12 @@ class AuditLogEventType(int, more_enums.Enum): INTEGRATION_DELETE = 82 -# Ignore docstring not starting in an imperative mood -def register_audit_log_entry_info( - type_: AuditLogEventType, *additional_types: AuditLogEventType -) -> typing.Callable[[typing.Type[BaseAuditLogEntryInfo]], typing.Type[BaseAuditLogEntryInfo]]: # noqa: D401 - """Generates a decorator for defined audit log entry info entities. - - Allows them to be associated with given entry type(s). - - Parameters - ---------- - type_ : AuditLogEventType - An entry types to associate the entity with. - *additional_types : AuditLogEventType - Extra entry types to associate the entity with. - - Returns - ------- - decorator(T) -> T - The decorator to decorate the class with. - """ - - def decorator(cls): - mapping = getattr(register_audit_log_entry_info, "types", {}) - for t in [type_, *additional_types]: - mapping[t] = cls - setattr(register_audit_log_entry_info, "types", mapping) - return cls - - return decorator - - -@marshaller.marshallable() -@attr.s(eq=True, hash=False, kw_only=True, slots=True) -class BaseAuditLogEntryInfo(bases.Entity, marshaller.Deserializable, abc.ABC): +@attr.s(eq=True, hash=False, init=False, kw_only=True, slots=True) +class BaseAuditLogEntryInfo(abc.ABC): """A base object that all audit log entry info objects will inherit from.""" -@register_audit_log_entry_info( - AuditLogEventType.CHANNEL_OVERWRITE_CREATE, - AuditLogEventType.CHANNEL_OVERWRITE_UPDATE, - AuditLogEventType.CHANNEL_OVERWRITE_DELETE, -) -@marshaller.marshallable() -@attr.s(eq=True, hash=False, kw_only=True, slots=True) +@attr.s(eq=True, hash=False, init=False, kw_only=True, slots=True) class ChannelOverwriteEntryInfo(BaseAuditLogEntryInfo, bases.Unique): """Represents the extra information for overwrite related audit log entries. @@ -316,79 +234,67 @@ class ChannelOverwriteEntryInfo(BaseAuditLogEntryInfo, bases.Unique): entries. """ - type: channels.PermissionOverwriteType = marshaller.attrib(deserializer=channels.PermissionOverwriteType) + type: channels.PermissionOverwriteType = attr.ib(repr=True) """The type of entity this overwrite targets.""" - role_name: typing.Optional[str] = marshaller.attrib(deserializer=str, if_undefined=None, default=None, repr=True) + role_name: typing.Optional[str] = attr.ib(repr=True) """The name of the role this overwrite targets, if it targets a role.""" -@register_audit_log_entry_info(AuditLogEventType.MESSAGE_PIN, AuditLogEventType.MESSAGE_UNPIN) -@marshaller.marshallable() -@attr.s(eq=True, hash=False, kw_only=True, slots=True) +@attr.s(eq=True, hash=False, init=False, kw_only=True, slots=True) class MessagePinEntryInfo(BaseAuditLogEntryInfo): """The extra information for message pin related audit log entries. Will be attached to the message pin and message unpin audit log entries. """ - channel_id: bases.Snowflake = marshaller.attrib(deserializer=bases.Snowflake, repr=True) + channel_id: bases.Snowflake = attr.ib(repr=True) """The ID of the text based channel where a pinned message is being targeted.""" - message_id: bases.Snowflake = marshaller.attrib(deserializer=bases.Snowflake, repr=True) + message_id: bases.Snowflake = attr.ib(repr=True) """The ID of the message that's being pinned or unpinned.""" -@register_audit_log_entry_info(AuditLogEventType.MEMBER_PRUNE) -@marshaller.marshallable() -@attr.s(eq=True, hash=False, kw_only=True, slots=True) +@attr.s(eq=True, hash=False, init=False, kw_only=True, slots=True) class MemberPruneEntryInfo(BaseAuditLogEntryInfo): """Represents the extra information attached to guild prune log entries.""" - delete_member_days: datetime.timedelta = marshaller.attrib(deserializer=_deserialize_day_timedelta, repr=True) + delete_member_days: datetime.timedelta = attr.ib(repr=True) """The timedelta of how many days members were pruned for inactivity based on.""" - members_removed: int = marshaller.attrib(deserializer=int, repr=True) + members_removed: int = attr.ib(repr=True) """The number of members who were removed by this prune.""" -@register_audit_log_entry_info(AuditLogEventType.MESSAGE_BULK_DELETE) -@marshaller.marshallable() -@attr.s(eq=True, hash=False, kw_only=True, slots=True) +@attr.s(eq=True, hash=False, init=False, kw_only=True, slots=True) class MessageBulkDeleteEntryInfo(BaseAuditLogEntryInfo): """Represents extra information for the message bulk delete audit entry.""" - count: int = marshaller.attrib(deserializer=int, repr=True) + count: int = attr.ib(repr=True) """The amount of messages that were deleted.""" -@register_audit_log_entry_info(AuditLogEventType.MESSAGE_DELETE) -@marshaller.marshallable() -@attr.s(eq=True, hash=False, kw_only=True, slots=True) +@attr.s(eq=True, hash=False, init=False, kw_only=True, slots=True) class MessageDeleteEntryInfo(MessageBulkDeleteEntryInfo): """Represents extra information attached to the message delete audit entry.""" - channel_id: bases.Snowflake = marshaller.attrib(deserializer=bases.Snowflake, repr=True) + channel_id: bases.Snowflake = attr.ib(repr=True) """The guild text based channel where these message(s) were deleted.""" -@register_audit_log_entry_info(AuditLogEventType.MEMBER_DISCONNECT) -@marshaller.marshallable() -@attr.s(eq=True, hash=False, kw_only=True, slots=True) +@attr.s(eq=True, hash=False, init=False, kw_only=True, slots=True) class MemberDisconnectEntryInfo(BaseAuditLogEntryInfo): """Represents extra information for the voice chat member disconnect entry.""" - count: int = marshaller.attrib(deserializer=int, repr=True) + count: int = attr.ib(repr=True) """The amount of members who were disconnected from voice in this entry.""" -@register_audit_log_entry_info(AuditLogEventType.MEMBER_MOVE) -@marshaller.marshallable() -@attr.s(eq=True, hash=False, kw_only=True, slots=True) +@attr.s(eq=True, hash=False, init=False, kw_only=True, slots=True) class MemberMoveEntryInfo(MemberDisconnectEntryInfo): """Represents extra information for the voice chat based member move entry.""" - channel_id: bases.Snowflake = marshaller.attrib(deserializer=bases.Snowflake, repr=True) + channel_id: bases.Snowflake = attr.ib(repr=True) """The amount of members who were disconnected from voice in this entry.""" @@ -403,129 +309,46 @@ class UnrecognisedAuditLogEntryInfo(BaseAuditLogEntryInfo): def __init__(self, payload: typing.Mapping[str, str]) -> None: self.__dict__.update(payload) - @classmethod - def deserialize(cls, payload: typing.Mapping[str, str], **_) -> UnrecognisedAuditLogEntryInfo: - return cls(payload) - - -def get_entry_info_entity(type_: int) -> typing.Type[BaseAuditLogEntryInfo]: - """Get the entity that's registered for an entry's options. - Parameters - ---------- - type_ : int - The identifier for this entry type. - - Returns - ------- - typing.Type[BaseAuditLogEntryInfo] - The associated options entity. If not implemented then this will be - `UnrecognisedAuditLogEntryInfo`. - """ - types = getattr(register_audit_log_entry_info, "types", more_collections.EMPTY_DICT) - entry_type = types.get(type_) - return entry_type if entry_type is not None else UnrecognisedAuditLogEntryInfo - - -@marshaller.marshallable() -@attr.s(eq=True, hash=True, kw_only=True, slots=True) -class AuditLogEntry(bases.Unique, marshaller.Deserializable): +@attr.s(eq=True, hash=True, init=False, kw_only=True, slots=True) +class AuditLogEntry(bases.Entity, bases.Unique): """Represents an entry in a guild's audit log.""" - target_id: typing.Optional[bases.Snowflake] = attr.attrib(eq=False, hash=False) + target_id: typing.Optional[bases.Snowflake] = attr.ib(eq=False, hash=False) """The ID of the entity affected by this change, if applicable.""" - changes: typing.Sequence[AuditLogChange] = attr.attrib(eq=False, hash=False, repr=False) + changes: typing.Sequence[AuditLogChange] = attr.ib(eq=False, hash=False, repr=False) """A sequence of the changes made to `AuditLogEntry.target_id`.""" - user_id: bases.Snowflake = attr.attrib(eq=False, hash=False) + user_id: typing.Optional[bases.Snowflake] = attr.ib(eq=False, hash=False) """The ID of the user who made this change.""" - action_type: typing.Union[AuditLogEventType, str] = attr.attrib(eq=False, hash=False) + action_type: typing.Union[AuditLogEventType, str] = attr.ib(eq=False, hash=False) """The type of action this entry represents.""" - options: typing.Optional[BaseAuditLogEntryInfo] = attr.attrib(eq=False, hash=False, repr=False) + options: typing.Optional[BaseAuditLogEntryInfo] = attr.ib(eq=False, hash=False, repr=False) """Extra information about this entry. Only be provided for certain `event_type`.""" - reason: typing.Optional[str] = attr.attrib(eq=False, hash=False, repr=False) + reason: typing.Optional[str] = attr.ib(eq=False, hash=False, repr=False) """The reason for this change, if set (between 0-512 characters).""" - @classmethod - def deserialize(cls, payload: more_typing.JSONObject, **kwargs: typing.Any) -> AuditLogEntry: - """Deserialize this model from a raw payload.""" - action_type = conversions.try_cast(payload["event_type"], AuditLogEventType, payload["event_type"]) - if target_id := payload.get("target_id"): - target_id = bases.Snowflake(target_id) - - if (options := payload.get("options")) is not None: - option_converter = get_entry_info_entity(action_type) - options = option_converter.deserialize(options, **kwargs) - - # noinspection PyArgumentList - return cls( - target_id=target_id, - changes=[ - AuditLogChange.deserialize(payload, **kwargs) - for payload in payload.get("changes", more_collections.EMPTY_SEQUENCE) - ], - user_id=bases.Snowflake(payload["user_id"]), - id=bases.Snowflake(payload["id"]), - action_type=action_type, - options=options, - reason=payload.get("reason"), - **kwargs, - ) - - -def _deserialize_entries( - payload: more_typing.JSONArray, **kwargs: typing.Any -) -> typing.Mapping[bases.Snowflake, AuditLogEntry]: - return {bases.Snowflake(entry["id"]): AuditLogEntry.deserialize(entry, **kwargs) for entry in payload} - - -def _deserialize_integrations( - payload: more_typing.JSONArray, **kwargs: typing.Any -) -> typing.Mapping[bases.Snowflake, guilds.GuildIntegration]: - return { - bases.Snowflake(integration["id"]): guilds.PartialGuildIntegration.deserialize(integration, **kwargs) - for integration in payload - } - - -def _deserialize_users( - payload: more_typing.JSONArray, **kwargs: typing.Any -) -> typing.Mapping[bases.Snowflake, users_.User]: - return {bases.Snowflake(user["id"]): users_.User.deserialize(user, **kwargs) for user in payload} - - -def _deserialize_webhooks( - payload: more_typing.JSONArray, **kwargs: typing.Any -) -> typing.Mapping[bases.Snowflake, webhooks_.Webhook]: - return {bases.Snowflake(webhook["id"]): webhooks_.Webhook.deserialize(webhook, **kwargs) for webhook in payload} - # TODO: make this support looking like a list of entries... -@marshaller.marshallable() -@attr.s(eq=True, repr=False, kw_only=True, slots=True) -class AuditLog(bases.Entity, marshaller.Deserializable): +@attr.s(eq=True, hash=False, init=False, kw_only=True, repr=False, slots=True) +class AuditLog: """Represents a guilds audit log.""" - entries: typing.Mapping[bases.Snowflake, AuditLogEntry] = marshaller.attrib( - raw_name="audit_log_entries", deserializer=_deserialize_entries, inherit_kwargs=True, - ) + entries: typing.Mapping[bases.Snowflake, AuditLogEntry] = attr.ib() """A sequence of the audit log's entries.""" - integrations: typing.Mapping[bases.Snowflake, guilds.GuildIntegration] = marshaller.attrib( - deserializer=_deserialize_integrations, inherit_kwargs=True, - ) + integrations: typing.Mapping[bases.Snowflake, guilds.Integration] = attr.ib() """A mapping of the partial objects of integrations found in this audit log.""" - users: typing.Mapping[bases.Snowflake, users_.User] = marshaller.attrib( - deserializer=_deserialize_users, inherit_kwargs=True - ) + users: typing.Mapping[bases.Snowflake, users_.User] = attr.ib() """A mapping of the objects of users found in this audit log.""" - webhooks: typing.Mapping[bases.Snowflake, webhooks_.Webhook] = marshaller.attrib( - deserializer=_deserialize_webhooks, inherit_kwargs=True, - ) + webhooks: typing.Mapping[bases.Snowflake, webhooks_.Webhook] = attr.ib() """A mapping of the objects of webhooks found in this audit log.""" + + def __iter__(self) -> typing.Iterable[AuditLogEntry]: + return self.entries.values() diff --git a/hikari/models/bases.py b/hikari/models/bases.py index d69534d2ed..7dcdc2e701 100644 --- a/hikari/models/bases.py +++ b/hikari/models/bases.py @@ -26,23 +26,27 @@ import typing import attr -from hikari.internal import conversions -from hikari.internal import marshaller + +from hikari.utilities import date as date_ if typing.TYPE_CHECKING: import datetime - from hikari import application + from hikari import app as app_ -@marshaller.marshallable() -@attr.s(eq=True, hash=False, init=False, kw_only=True, slots=True) +@attr.s(eq=True, hash=False, init=False, kw_only=True, slots=False) class Entity(abc.ABC): """The base for any entity used in this API.""" - _app: typing.Optional[application.Application] = attr.attrib(default=None, repr=False, eq=False, hash=False) + _app: typing.Optional[app_.IApp] = attr.ib(default=None, repr=False, eq=False, hash=False) """The client application that models may use for procedures.""" + def set_app(self, app: app_.IApp) -> None: + if hasattr(self, "_app"): + raise AttributeError("Protected attribute '_app' cannot be overwritten.") + self._app = app + class Snowflake(int): """A concrete representation of a unique identifier for an object on Discord. @@ -63,7 +67,7 @@ def __new__(cls, value: typing.Union[int, str]) -> Snowflake: def created_at(self) -> datetime.datetime: """When the object was created.""" epoch = self >> 22 - return conversions.discord_epoch_to_datetime(epoch) + return date_.discord_epoch_to_datetime(epoch) @property def internal_worker_id(self) -> int: @@ -103,23 +107,19 @@ def max(cls) -> Snowflake: def from_data(cls, timestamp: datetime.datetime, worker_id: int, process_id: int, increment: int) -> Snowflake: """Convert the pieces of info that comprise an ID into a Snowflake.""" return cls( - (conversions.datetime_to_discord_epoch(timestamp) << 22) - | (worker_id << 17) - | (process_id << 12) - | increment + (date_.datetime_to_discord_epoch(timestamp) << 22) | (worker_id << 17) | (process_id << 12) | increment ) -@marshaller.marshallable() -@attr.s(eq=True, hash=True, kw_only=True, slots=True) -class Unique(Entity, typing.SupportsInt, abc.ABC): +@attr.s(eq=True, hash=True, init=False, kw_only=True, slots=False) +class Unique(typing.SupportsInt): """A base for an entity that has an integer ID of some sort. Casting an object of this type to an `int` will produce the integer ID of the object. """ - id: Snowflake = marshaller.attrib(hash=True, eq=True, repr=True, deserializer=Snowflake, serializer=str) + id: Snowflake = attr.ib(converter=Snowflake, hash=True, eq=True, repr=True) """The ID of this entity.""" @property @@ -131,5 +131,7 @@ def __int__(self) -> int: return int(self.id) -UniqueObjectT = typing.Union[Unique, Snowflake, int, str] +UniqueT = typing.TypeVar("UniqueT", bound=Unique) """A unique object.""" +UniqueLikeT = typing.Union[UniqueT, Snowflake, int, str] +"""A representation of a unique entity.""" diff --git a/hikari/models/channels.py b/hikari/models/channels.py index 214c921ff2..6055f9dd74 100644 --- a/hikari/models/channels.py +++ b/hikari/models/channels.py @@ -25,7 +25,6 @@ "PermissionOverwrite", "PermissionOverwriteType", "PartialChannel", - "TextChannel", "DMChannel", "GroupDMChannel", "GuildCategory", @@ -34,29 +33,25 @@ "GuildNewsChannel", "GuildStoreChannel", "GuildVoiceChannel", - "GuildChannelBuilder", ] -import datetime +import enum import typing import attr -from hikari.internal import conversions -from hikari.internal import marshaller -from hikari.internal import more_collections -from hikari.internal import more_enums from . import bases from . import permissions -from . import users from ..net import urls if typing.TYPE_CHECKING: - from hikari.internal import more_typing + import datetime + from . import users -@more_enums.must_be_unique -class ChannelType(int, more_enums.Enum): + +@enum.unique +class ChannelType(int, enum.Enum): """The known channel types that are exposed to us by the API.""" GUILD_TEXT = 0 @@ -81,8 +76,8 @@ class ChannelType(int, more_enums.Enum): """A channel that show's a game's store page.""" -@more_enums.must_be_unique -class PermissionOverwriteType(str, more_enums.Enum): +@enum.unique +class PermissionOverwriteType(str, enum.Enum): """The type of entity a Permission Overwrite targets.""" ROLE = "role" @@ -95,24 +90,17 @@ def __str__(self) -> str: return self.value -@marshaller.marshallable() -@attr.s(eq=True, hash=True, kw_only=True, slots=True) -class PermissionOverwrite(bases.Unique, marshaller.Deserializable, marshaller.Serializable): +@attr.s(eq=True, hash=True, init=True, kw_only=True, slots=True) +class PermissionOverwrite(bases.Unique): """Represents permission overwrites for a channel or role in a channel.""" - type: PermissionOverwriteType = marshaller.attrib( - deserializer=PermissionOverwriteType, serializer=str, eq=True, hash=True - ) + type: PermissionOverwriteType = attr.ib(converter=PermissionOverwriteType, eq=True, hash=True) """The type of entity this overwrite targets.""" - allow: permissions.Permission = marshaller.attrib( - deserializer=permissions.Permission, serializer=int, default=permissions.Permission(0), eq=False, hash=False - ) + allow: permissions.Permission = attr.ib(converter=permissions.Permission, default=0, eq=False, hash=False) """The permissions this overwrite allows.""" - deny: permissions.Permission = marshaller.attrib( - deserializer=permissions.Permission, serializer=int, default=permissions.Permission(0), eq=False, hash=False - ) + deny: permissions.Permission = attr.ib(converter=permissions.Permission, default=0, eq=False, hash=False) """The permissions this overwrite denies.""" @property @@ -121,97 +109,48 @@ def unset(self) -> permissions.Permission: return typing.cast(permissions.Permission, (self.allow | self.deny)) -def register_channel_type( - type_: ChannelType, -) -> typing.Callable[[typing.Type[PartialChannel]], typing.Type[PartialChannel]]: - """Generate a decorator for channel classes defined in this library. - - This allows them to associate themselves with a given channel type. - - Parameters - ---------- - type_ : ChannelType - The channel type to associate with. - - Returns - ------- - decorator(T) -> T - The decorator to decorate the class with. - """ - - def decorator(cls): - mapping = getattr(register_channel_type, "types", {}) - mapping[type_] = cls - setattr(register_channel_type, "types", mapping) - return cls - - return decorator - - -class TextChannel: - # This is a mixin, do not add slotted fields. - __slots__ = () - - -@marshaller.marshallable() -@attr.s(eq=True, hash=True, kw_only=True, slots=True) -class PartialChannel(bases.Unique, marshaller.Deserializable): +@attr.s(eq=True, hash=True, init=False, kw_only=True, slots=True) +class PartialChannel(bases.Entity, bases.Unique): """Represents a channel where we've only received it's basic information. This is commonly received in RESTSession responses. """ - name: typing.Optional[str] = marshaller.attrib( - deserializer=str, if_undefined=None, if_none=None, default=None, eq=False, hash=False, repr=True - ) + name: typing.Optional[str] = attr.ib(eq=False, hash=False, repr=True) """The channel's name. This will be missing for DM channels.""" - type: ChannelType = marshaller.attrib(deserializer=ChannelType, eq=False, hash=False, repr=True) + type: ChannelType = attr.ib(eq=False, hash=False, repr=True) """The channel's type.""" -def _deserialize_recipients(payload: more_typing.JSONArray, **kwargs: typing.Any) -> typing.Sequence[users.User]: - return {bases.Snowflake(user["id"]): users.User.deserialize(user, **kwargs) for user in payload} - - -@register_channel_type(ChannelType.DM) -@marshaller.marshallable() -@attr.s(eq=True, hash=True, kw_only=True, slots=True) -class DMChannel(PartialChannel, TextChannel): +@attr.s(eq=True, hash=True, init=False, kw_only=True, slots=True) +class DMChannel(PartialChannel): """Represents a DM channel.""" - last_message_id: typing.Optional[bases.Snowflake] = marshaller.attrib( - deserializer=bases.Snowflake, if_none=None, eq=False, hash=False - ) + last_message_id: typing.Optional[bases.Snowflake] = attr.ib(eq=False, hash=False) """The ID of the last message sent in this channel. !!! note This might point to an invalid or deleted message. """ - recipients: typing.Mapping[bases.Snowflake, users.User] = marshaller.attrib( - deserializer=_deserialize_recipients, inherit_kwargs=True, eq=False, hash=False, + recipients: typing.Mapping[bases.Snowflake, users.User] = attr.ib( + eq=False, hash=False, ) """The recipients of the DM.""" -@register_channel_type(ChannelType.GROUP_DM) -@marshaller.marshallable() -@attr.s(eq=True, hash=True, kw_only=True, slots=True) +@attr.s(eq=True, hash=True, init=False, kw_only=True, slots=True) class GroupDMChannel(DMChannel): """Represents a DM group channel.""" - owner_id: bases.Snowflake = marshaller.attrib(deserializer=bases.Snowflake, eq=False, hash=False, repr=True) + owner_id: bases.Snowflake = attr.ib(eq=False, hash=False, repr=True) """The ID of the owner of the group.""" - icon_hash: typing.Optional[str] = marshaller.attrib( - raw_name="icon", deserializer=str, if_none=None, eq=False, hash=False - ) + icon_hash: typing.Optional[str] = attr.ib(eq=False, hash=False) """The hash of the icon of the group.""" - application_id: typing.Optional[bases.Snowflake] = marshaller.attrib( - deserializer=bases.Snowflake, if_undefined=None, default=None, eq=False, hash=False - ) + application_id: typing.Optional[bases.Snowflake] = attr.ib(eq=False, hash=False) """The ID of the application that created the group DM, if it's a bot based group DM.""" @property @@ -246,83 +185,54 @@ def format_icon_url(self, *, format_: str = "png", size: int = 4096) -> typing.O return None -def _deserialize_overwrites( - payload: more_typing.JSONArray, **kwargs: typing.Any -) -> typing.Mapping[bases.Snowflake, PermissionOverwrite]: - return { - bases.Snowflake(overwrite["id"]): PermissionOverwrite.deserialize(overwrite, **kwargs) for overwrite in payload - } - - -@marshaller.marshallable() -@attr.s(eq=True, hash=True, kw_only=True, slots=True) +@attr.s(eq=True, hash=True, init=False, kw_only=True, slots=True) class GuildChannel(PartialChannel): """The base for anything that is a guild channel.""" - guild_id: typing.Optional[bases.Snowflake] = marshaller.attrib( - deserializer=bases.Snowflake, if_undefined=None, default=None, eq=False, hash=False, repr=True - ) + guild_id: typing.Optional[bases.Snowflake] = attr.ib(eq=False, hash=False, repr=True) """The ID of the guild the channel belongs to. This will be `None` when received over the gateway in certain events (e.g. Guild Create). """ - position: int = marshaller.attrib(deserializer=int, eq=False, hash=False) + position: int = attr.ib(eq=False, hash=False) """The sorting position of the channel.""" - permission_overwrites: PermissionOverwrite = marshaller.attrib( - deserializer=_deserialize_overwrites, inherit_kwargs=True, eq=False, hash=False - ) + permission_overwrites: typing.Mapping[bases.Snowflake, PermissionOverwrite] = attr.ib(eq=False, hash=False) """The permission overwrites for the channel.""" - is_nsfw: typing.Optional[bool] = marshaller.attrib( - raw_name="nsfw", deserializer=bool, if_undefined=None, default=None, eq=False, hash=False - ) + is_nsfw: typing.Optional[bool] = attr.ib(eq=False, hash=False) """Whether the channel is marked as NSFW. This will be `None` when received over the gateway in certain events (e.g Guild Create). """ - parent_id: typing.Optional[bases.Snowflake] = marshaller.attrib( - deserializer=bases.Snowflake, if_none=None, if_undefined=None, default=None, eq=False, hash=False, repr=True - ) + parent_id: typing.Optional[bases.Snowflake] = attr.ib(eq=False, hash=False, repr=True) """The ID of the parent category the channel belongs to.""" -@register_channel_type(ChannelType.GUILD_CATEGORY) -@marshaller.marshallable() -@attr.s(eq=True, hash=True, kw_only=True, slots=True) +@attr.s(eq=True, hash=True, init=False, kw_only=True, slots=True) class GuildCategory(GuildChannel): """Represents a guild category.""" -def _deserialize_rate_limit_per_user(payload: int) -> datetime.timedelta: - return datetime.timedelta(seconds=payload) - - -@register_channel_type(ChannelType.GUILD_TEXT) -@marshaller.marshallable() -@attr.s(eq=True, hash=True, kw_only=True, slots=True) -class GuildTextChannel(GuildChannel, TextChannel): +@attr.s(eq=True, hash=True, init=False, kw_only=True, slots=True) +class GuildTextChannel(GuildChannel): """Represents a guild text channel.""" - topic: typing.Optional[str] = marshaller.attrib(deserializer=str, if_none=None, eq=False, hash=False) + topic: typing.Optional[str] = attr.ib(eq=False, hash=False) """The topic of the channel.""" - last_message_id: typing.Optional[bases.Snowflake] = marshaller.attrib( - deserializer=bases.Snowflake, if_none=None, eq=False, hash=False - ) + last_message_id: typing.Optional[bases.Snowflake] = attr.ib(eq=False, hash=False) """The ID of the last message sent in this channel. !!! note This might point to an invalid or deleted message. """ - rate_limit_per_user: datetime.timedelta = marshaller.attrib( - deserializer=_deserialize_rate_limit_per_user, eq=False, hash=False - ) + rate_limit_per_user: datetime.timedelta = attr.ib(eq=False, hash=False) """The delay (in seconds) between a user can send a message to this channel. !!! note @@ -330,14 +240,7 @@ class GuildTextChannel(GuildChannel, TextChannel): are not affected by this. """ - last_pin_timestamp: typing.Optional[datetime.datetime] = marshaller.attrib( - deserializer=conversions.iso8601_datetime_string_to_datetime, - if_none=None, - if_undefined=None, - default=None, - eq=False, - hash=False, - ) + last_pin_timestamp: typing.Optional[datetime.datetime] = attr.ib(eq=False, hash=False) """The timestamp of the last-pinned message. This may be `None` in several cases (currently undocumented clearly by @@ -345,32 +248,21 @@ class GuildTextChannel(GuildChannel, TextChannel): """ -@register_channel_type(ChannelType.GUILD_NEWS) -@marshaller.marshallable() -@attr.s(eq=True, hash=True, slots=True, kw_only=True) -class GuildNewsChannel(GuildChannel, TextChannel): +@attr.s(eq=True, hash=True, init=False, slots=True, kw_only=True) +class GuildNewsChannel(GuildChannel): """Represents an news channel.""" - topic: typing.Optional[str] = marshaller.attrib(deserializer=str, if_none=None, eq=False, hash=False) + topic: typing.Optional[str] = attr.ib(eq=False, hash=False) """The topic of the channel.""" - last_message_id: typing.Optional[bases.Snowflake] = marshaller.attrib( - deserializer=bases.Snowflake, if_none=None, eq=False, hash=False - ) + last_message_id: typing.Optional[bases.Snowflake] = attr.ib(eq=False, hash=False) """The ID of the last message sent in this channel. !!! note This might point to an invalid or deleted message. """ - last_pin_timestamp: typing.Optional[datetime.datetime] = marshaller.attrib( - deserializer=conversions.iso8601_datetime_string_to_datetime, - if_none=None, - if_undefined=None, - default=None, - eq=False, - hash=False, - ) + last_pin_timestamp: typing.Optional[datetime.datetime] = attr.ib(eq=False, hash=False) """The timestamp of the last-pinned message. This may be `None` in several cases (currently undocumented clearly by @@ -378,168 +270,17 @@ class GuildNewsChannel(GuildChannel, TextChannel): """ -@register_channel_type(ChannelType.GUILD_STORE) -@marshaller.marshallable() -@attr.s(eq=True, hash=True, kw_only=True, slots=True) +@attr.s(eq=True, hash=True, init=False, kw_only=True, slots=True) class GuildStoreChannel(GuildChannel): """Represents a store channel.""" -@register_channel_type(ChannelType.GUILD_VOICE) -@marshaller.marshallable() -@attr.s(eq=True, hash=True, kw_only=True, slots=True) +@attr.s(eq=True, hash=True, init=False, kw_only=True, slots=True) class GuildVoiceChannel(GuildChannel): """Represents an voice channel.""" - bitrate: int = marshaller.attrib(deserializer=int, eq=False, hash=False, repr=True) + bitrate: int = attr.ib(eq=False, hash=False, repr=True) """The bitrate for the voice channel (in bits).""" - user_limit: int = marshaller.attrib(deserializer=int, eq=False, hash=False, repr=True) + user_limit: int = attr.ib(eq=False, hash=False, repr=True) """The user limit for the voice channel.""" - - -class GuildChannelBuilder(marshaller.Serializable): - """Used to create channel objects to send in guild create requests. - - Parameters - ---------- - channel_name : str - The name to set for the channel. - channel_type : ChannelType - The type of channel this should build. - - Examples - -------- - channel_obj = ( - channels.GuildChannelBuilder("Catgirl-appreciation", channels.ChannelType.GUILD_TEXT) - .is_nsfw(True) - .with_topic("Here we men of culture appreciate the way of the neko.") - .with_rate_limit_per_user(datetime.timedelta(seconds=5)) - .with_permission_overwrites([overwrite_obj]) - .with_id(1) - ) - """ - - __slots__ = ("_payload",) - - def __init__(self, channel_name: str, channel_type: ChannelType) -> None: - self._payload: typing.Dict[str, typing.Any] = { - "type": channel_type, - "name": channel_name, - } - - def serialize(self: GuildChannelBuilder) -> typing.Mapping[str, typing.Any]: - """Serialize this instance into a payload to send to Discord.""" - return self._payload - - def is_nsfw(self) -> GuildChannelBuilder: - """Mark this channel as NSFW.""" - self._payload["nsfw"] = True - return self - - def with_permission_overwrites(self, overwrites: typing.Sequence[PermissionOverwrite]) -> GuildChannelBuilder: - """Set the permission overwrites for this channel. - - Parameters - ---------- - overwrites : typing.Sequence[PermissionOverwrite] - A sequence of overwrite objects to add, where the first overwrite - object - - !!! note - Calling this multiple times will overwrite any previously added - overwrites. - """ - self._payload["permission_overwrites"] = [o.serialize() for o in overwrites] - return self - - def with_topic(self, topic: str) -> GuildChannelBuilder: - """Set the topic for this channel. - - Parameters - ---------- - topic : str - The string topic to set. - """ - self._payload["topic"] = topic - return self - - def with_bitrate(self, bitrate: int) -> GuildChannelBuilder: - """Set the bitrate for this channel. - - Parameters - ---------- - bitrate : int - The bitrate to set in bits. - """ - self._payload["bitrate"] = int(bitrate) - return self - - def with_user_limit(self, user_limit: int) -> GuildChannelBuilder: - """Set the limit for how many users can be in this channel at once. - - Parameters - ---------- - user_limit : int - The user limit to set. - """ - self._payload["user_limit"] = int(user_limit) - return self - - def with_rate_limit_per_user( - self, rate_limit_per_user: typing.Union[datetime.timedelta, int] - ) -> GuildChannelBuilder: - """Set the rate limit for users sending messages in this channel. - - Parameters - ---------- - rate_limit_per_user : datetime.timedelta | int - The amount of seconds users will have to wait before sending another - message in the channel to set. - """ - self._payload["rate_limit_per_user"] = int( - rate_limit_per_user.total_seconds() - if isinstance(rate_limit_per_user, datetime.timedelta) - else rate_limit_per_user - ) - return self - - def with_parent_category(self, category: typing.Union[bases.Snowflake, int]) -> GuildChannelBuilder: - """Set the parent category for this channel. - - Parameters - ---------- - category : hikari.models.bases.Snowflake | int - The placeholder ID of the category channel that should be this - channel's parent. - """ - self._payload["parent_id"] = str(int(category)) - return self - - def with_id(self, channel_id: typing.Union[bases.Snowflake, int]) -> GuildChannelBuilder: - """Set the placeholder ID for this channel. - - Parameters - ---------- - channel_id : hikari.models.bases.Snowflake | int - The placeholder ID to use. - - !!! note - This ID is purely a place holder used for setting parent category - channels and will have no effect on the created channel's ID. - """ - self._payload["id"] = str(int(channel_id)) - return self - - -def deserialize_channel(payload: more_typing.JSONObject, **kwargs: typing.Any) -> typing.Union[GuildChannel, DMChannel]: - """Deserialize a channel object into the corresponding class. - - !!! warning - This can only be used to deserialize full channel objects. To - deserialize a partial object, use `PartialChannel.deserialize`. - """ - type_id = payload["type"] - types = getattr(register_channel_type, "types", more_collections.EMPTY_DICT) - channel_type = types[type_id] - return channel_type.deserialize(payload, **kwargs) diff --git a/hikari/models/embeds.py b/hikari/models/embeds.py index ef46656f4a..ed6e2dba6e 100644 --- a/hikari/models/embeds.py +++ b/hikari/models/embeds.py @@ -35,10 +35,6 @@ import typing import attr -from hikari.internal import conversions -from hikari.internal import marshaller - -from . import bases from . import colors from . import files @@ -55,22 +51,17 @@ _MAX_EMBED_SIZE: typing.Final[int] = 6000 -@marshaller.marshallable() -@attr.s(eq=True, hash=False, kw_only=True, slots=True) -class EmbedFooter(bases.Entity, marshaller.Deserializable, marshaller.Serializable): +@attr.s(eq=True, hash=False, init=True, kw_only=True, slots=True) +class EmbedFooter: """Represents an embed footer.""" - text: str = marshaller.attrib(deserializer=str, serializer=str, repr=True) + text: typing.Optional[str] = attr.ib(default=None, repr=True) """The footer text.""" - icon_url: typing.Optional[str] = marshaller.attrib( - deserializer=str, serializer=str, if_undefined=None, default=None - ) + icon_url: typing.Optional[str] = attr.ib(default=None) """The URL of the footer icon.""" - proxy_icon_url: typing.Optional[str] = marshaller.attrib( - deserializer=str, serializer=None, if_undefined=None, default=None - ) + proxy_icon_url: typing.Optional[str] = attr.ib(default=None) """The proxied URL of the footer icon. !!! note @@ -79,19 +70,16 @@ class EmbedFooter(bases.Entity, marshaller.Deserializable, marshaller.Serializab """ -@marshaller.marshallable() -@attr.s(eq=True, hash=False, kw_only=True, slots=True) -class EmbedImage(bases.Entity, marshaller.Deserializable, marshaller.Serializable): +@attr.s(eq=True, hash=False, init=True, kw_only=True, slots=True) +class EmbedImage: """Represents an embed image.""" - url: typing.Optional[str] = marshaller.attrib( - deserializer=str, serializer=str, if_undefined=None, default=None, repr=True, + url: typing.Optional[str] = attr.ib( + default=None, repr=True, ) """The URL of the image.""" - proxy_url: typing.Optional[str] = marshaller.attrib( - deserializer=str, serializer=None, if_undefined=None, default=None, - ) + proxy_url: typing.Optional[str] = attr.ib(default=None,) """The proxied URL of the image. !!! note @@ -99,7 +87,7 @@ class EmbedImage(bases.Entity, marshaller.Deserializable, marshaller.Serializabl will be ignored during serialization. """ - height: typing.Optional[int] = marshaller.attrib(deserializer=int, serializer=None, if_undefined=None, default=None) + height: typing.Optional[int] = attr.ib(default=None) """The height of the image. !!! note @@ -107,7 +95,7 @@ class EmbedImage(bases.Entity, marshaller.Deserializable, marshaller.Serializabl will be ignored during serialization. """ - width: typing.Optional[int] = marshaller.attrib(deserializer=int, serializer=None, if_undefined=None, default=None) + width: typing.Optional[int] = attr.ib(default=None) """The width of the image. !!! note @@ -116,19 +104,16 @@ class EmbedImage(bases.Entity, marshaller.Deserializable, marshaller.Serializabl """ -@marshaller.marshallable() -@attr.s(eq=True, hash=False, kw_only=True, slots=True) -class EmbedThumbnail(bases.Entity, marshaller.Deserializable, marshaller.Serializable): +@attr.s(eq=True, hash=False, init=True, kw_only=True, slots=True) +class EmbedThumbnail: """Represents an embed thumbnail.""" - url: typing.Optional[str] = marshaller.attrib( - deserializer=str, serializer=str, if_undefined=None, default=None, repr=True, + url: typing.Optional[str] = attr.ib( + default=None, repr=True, ) """The URL of the thumbnail.""" - proxy_url: typing.Optional[str] = marshaller.attrib( - deserializer=str, serializer=None, if_undefined=None, default=None, - ) + proxy_url: typing.Optional[str] = attr.ib(default=None,) """The proxied URL of the thumbnail. !!! note @@ -136,7 +121,7 @@ class EmbedThumbnail(bases.Entity, marshaller.Deserializable, marshaller.Seriali will be ignored during serialization. """ - height: typing.Optional[int] = marshaller.attrib(deserializer=int, serializer=None, if_undefined=None, default=None) + height: typing.Optional[int] = attr.ib(default=None) """The height of the thumbnail. !!! note @@ -144,7 +129,7 @@ class EmbedThumbnail(bases.Entity, marshaller.Deserializable, marshaller.Seriali will be ignored during serialization. """ - width: typing.Optional[int] = marshaller.attrib(deserializer=int, serializer=None, if_undefined=None, default=None) + width: typing.Optional[int] = attr.ib(default=None) """The width of the thumbnail. !!! note @@ -153,9 +138,8 @@ class EmbedThumbnail(bases.Entity, marshaller.Deserializable, marshaller.Seriali """ -@marshaller.marshallable() -@attr.s(eq=True, hash=False, kw_only=True, slots=True) -class EmbedVideo(bases.Entity, marshaller.Deserializable): +@attr.s(eq=True, hash=False, init=False, kw_only=True, slots=True) +class EmbedVideo: """Represents an embed video. !!! note @@ -164,19 +148,18 @@ class EmbedVideo(bases.Entity, marshaller.Deserializable): embed objects. """ - url: typing.Optional[str] = marshaller.attrib(deserializer=str, if_undefined=None, default=None, repr=True) + url: typing.Optional[str] = attr.ib(default=None, repr=True) """The URL of the video.""" - height: typing.Optional[int] = marshaller.attrib(deserializer=int, if_undefined=None, default=None) + height: typing.Optional[int] = attr.ib(default=None) """The height of the video.""" - width: typing.Optional[int] = marshaller.attrib(deserializer=int, if_undefined=None, default=None) + width: typing.Optional[int] = attr.ib(default=None) """The width of the video.""" -@marshaller.marshallable() -@attr.s(eq=True, hash=False, kw_only=True, slots=True) -class EmbedProvider(bases.Entity, marshaller.Deserializable): +@attr.s(eq=True, hash=False, init=False, kw_only=True, slots=True) +class EmbedProvider: """Represents an embed provider. !!! note @@ -185,38 +168,27 @@ class EmbedProvider(bases.Entity, marshaller.Deserializable): You should still expect to receive these objects where appropriate. """ - name: typing.Optional[str] = marshaller.attrib(deserializer=str, if_undefined=None, default=None, repr=True) + name: typing.Optional[str] = attr.ib(default=None, repr=True) """The name of the provider.""" - url: typing.Optional[str] = marshaller.attrib( - deserializer=str, if_undefined=None, if_none=None, default=None, repr=True - ) + url: typing.Optional[str] = attr.ib(default=None, repr=True) """The URL of the provider.""" -@marshaller.marshallable() -@attr.s(eq=True, hash=False, kw_only=True, slots=True) -class EmbedAuthor(bases.Entity, marshaller.Deserializable, marshaller.Serializable): +@attr.s(eq=True, hash=False, init=True, kw_only=True, slots=True) +class EmbedAuthor: """Represents an embed author.""" - name: typing.Optional[str] = marshaller.attrib( - deserializer=str, serializer=str, if_undefined=None, default=None, repr=True - ) + name: typing.Optional[str] = attr.ib(default=None, repr=True) """The name of the author.""" - url: typing.Optional[str] = marshaller.attrib( - deserializer=str, serializer=str, if_undefined=None, default=None, repr=True - ) + url: typing.Optional[str] = attr.ib(default=None, repr=True) """The URL of the author.""" - icon_url: typing.Optional[str] = marshaller.attrib( - deserializer=str, serializer=str, if_undefined=None, default=None - ) + icon_url: typing.Optional[str] = attr.ib(default=None) """The URL of the author icon.""" - proxy_icon_url: typing.Optional[str] = marshaller.attrib( - deserializer=str, serializer=None, if_undefined=None, default=None - ) + proxy_icon_url: typing.Optional[str] = attr.ib(default=None) """The proxied URL of the author icon. !!! note @@ -225,43 +197,25 @@ class EmbedAuthor(bases.Entity, marshaller.Deserializable, marshaller.Serializab """ -@marshaller.marshallable() -@attr.s(eq=True, hash=False, kw_only=True, slots=True) -class EmbedField(bases.Entity, marshaller.Deserializable, marshaller.Serializable): +@attr.s(eq=True, hash=False, init=True, kw_only=True, slots=True) +class EmbedField: """Represents a field in a embed.""" - name: str = marshaller.attrib(deserializer=str, serializer=str, repr=True) + name: typing.Optional[str] = attr.ib(default=None, repr=True) """The name of the field.""" - value: str = marshaller.attrib(deserializer=str, serializer=str, repr=True) + value: typing.Optional[str] = attr.ib(default=None, repr=True) """The value of the field.""" - is_inline: bool = marshaller.attrib( - raw_name="inline", deserializer=bool, serializer=bool, if_undefined=False, default=False, repr=True - ) + is_inline: bool = attr.ib(default=False, repr=True) """Whether the field should display inline. Defaults to `False`.""" -def _serialize_timestamp(timestamp: datetime.datetime) -> str: - return timestamp.replace(tzinfo=datetime.timezone.utc).isoformat() - - -def _deserialize_fields(payload: more_typing.JSONArray, **kwargs: typing.Any) -> typing.Sequence[EmbedField]: - return [EmbedField.deserialize(field, **kwargs) for field in payload] - - -def _serialize_fields(fields: typing.Sequence[EmbedField]) -> more_typing.JSONArray: - return [field.serialize() for field in fields] - - -@marshaller.marshallable() -@attr.s(eq=True, hash=False, kw_only=True, slots=True) -class Embed(bases.Entity, marshaller.Deserializable, marshaller.Serializable): +@attr.s(eq=True, hash=False, init=True, kw_only=True, slots=True) +class Embed: """Represents an embed.""" - title: typing.Optional[str] = marshaller.attrib( - deserializer=str, serializer=str, if_undefined=None, default=None, repr=True - ) + title: typing.Optional[str] = attr.ib(default=None, repr=True) """The title of the embed.""" @title.validator @@ -269,9 +223,7 @@ def _title_check(self, _, value): # pylint:disable=unused-argument if value is not None and len(value) > _MAX_EMBED_TITLE: raise ValueError(f"title must not exceed {_MAX_EMBED_TITLE} characters") - description: typing.Optional[str] = marshaller.attrib( - deserializer=str, serializer=str, if_undefined=None, default=None - ) + description: typing.Optional[str] = attr.ib(default=None) """The description of the embed.""" @description.validator @@ -279,57 +231,29 @@ def _description_check(self, _, value): # pylint:disable=unused-argument if value is not None and len(value) > _MAX_EMBED_DESCRIPTION: raise ValueError(f"description must not exceed {_MAX_EMBED_DESCRIPTION} characters") - url: typing.Optional[str] = marshaller.attrib(deserializer=str, serializer=str, if_undefined=None, default=None) + url: typing.Optional[str] = attr.ib(default=None) """The URL of the embed.""" - timestamp: typing.Optional[datetime.datetime] = marshaller.attrib( - deserializer=conversions.iso8601_datetime_string_to_datetime, - serializer=_serialize_timestamp, - if_undefined=None, - default=None, - repr=True, + timestamp: typing.Optional[datetime.datetime] = attr.ib( + default=None, repr=True, ) """The timestamp of the embed.""" - color: typing.Optional[colors.Color] = marshaller.attrib( - deserializer=colors.Color, - serializer=int, - converter=attr.converters.optional(colors.Color.of), - if_undefined=None, - default=None, + color: typing.Optional[colors.Color] = attr.ib( + converter=attr.converters.optional(colors.Color.of), default=None, ) """The colour of this embed's sidebar.""" - footer: typing.Optional[EmbedFooter] = marshaller.attrib( - deserializer=EmbedFooter.deserialize, - serializer=EmbedFooter.serialize, - if_undefined=None, - default=None, - inherit_kwargs=True, - ) + footer: typing.Optional[EmbedFooter] = attr.ib(default=None,) """The footer of the embed.""" - image: typing.Optional[EmbedImage] = marshaller.attrib( - deserializer=EmbedImage.deserialize, - serializer=EmbedImage.serialize, - if_undefined=None, - default=None, - inherit_kwargs=True, - ) + image: typing.Optional[EmbedImage] = attr.ib(default=None,) """The image of the embed.""" - thumbnail: typing.Optional[EmbedThumbnail] = marshaller.attrib( - deserializer=EmbedThumbnail.deserialize, - serializer=EmbedThumbnail.serialize, - if_undefined=None, - default=None, - inherit_kwargs=True, - ) + thumbnail: typing.Optional[EmbedThumbnail] = attr.ib(default=None,) """The thumbnail of the embed.""" - video: typing.Optional[EmbedVideo] = marshaller.attrib( - deserializer=EmbedVideo.deserialize, serializer=None, if_undefined=None, default=None, inherit_kwargs=True - ) + video: typing.Optional[EmbedVideo] = attr.ib(default=None) """The video of the embed. !!! note @@ -337,9 +261,7 @@ def _description_check(self, _, value): # pylint:disable=unused-argument will be ignored during serialization. """ - provider: typing.Optional[EmbedProvider] = marshaller.attrib( - deserializer=EmbedProvider.deserialize, serializer=None, if_undefined=None, default=None, inherit_kwargs=True, - ) + provider: typing.Optional[EmbedProvider] = attr.ib(default=None) """The provider of the embed. !!! note @@ -347,22 +269,10 @@ def _description_check(self, _, value): # pylint:disable=unused-argument will be ignored during serialization. """ - author: typing.Optional[EmbedAuthor] = marshaller.attrib( - deserializer=EmbedAuthor.deserialize, - serializer=EmbedAuthor.serialize, - if_undefined=None, - default=None, - inherit_kwargs=True, - ) + author: typing.Optional[EmbedAuthor] = attr.ib(default=None,) """The author of the embed.""" - fields: typing.Sequence[EmbedField] = marshaller.attrib( - deserializer=_deserialize_fields, - serializer=_serialize_fields, - if_undefined=list, - factory=list, - inherit_kwargs=True, - ) + fields: typing.Sequence[EmbedField] = attr.ib(factory=list) """The fields of the embed.""" _assets_to_upload = attr.attrib(factory=list) diff --git a/hikari/models/emojis.py b/hikari/models/emojis.py index fdc23d3c88..5a5be42fe3 100644 --- a/hikari/models/emojis.py +++ b/hikari/models/emojis.py @@ -27,20 +27,17 @@ import unicodedata import attr -from hikari.internal import marshaller from . import bases from . import files -from . import users from ..net import urls if typing.TYPE_CHECKING: - from hikari.internal import more_typing + from . import users -@marshaller.marshallable() -@attr.s(eq=True, hash=True, kw_only=True, slots=True) -class Emoji(bases.Entity, marshaller.Deserializable, files.BaseStream, abc.ABC): +@attr.s(eq=True, hash=True, init=False, kw_only=True, slots=True) +class Emoji(files.BaseStream, abc.ABC): """Base class for all emojis. Any emoji implementation supports being used as a `hikari.models.files.BaseStream` @@ -72,8 +69,7 @@ def __aiter__(self) -> typing.AsyncIterator[bytes]: return files.WebResourceStream(self.filename, self.url).__aiter__() -@marshaller.marshallable() -@attr.s(eq=True, hash=True, kw_only=True, slots=True) +@attr.s(eq=True, hash=True, init=False, kw_only=True, slots=True) class UnicodeEmoji(Emoji): """Represents a unicode emoji. @@ -93,7 +89,7 @@ class UnicodeEmoji(Emoji): removed in a future release after a deprecation period. """ - name: str = marshaller.attrib(deserializer=str, eq=True, hash=True, repr=True) + name: str = attr.ib(eq=True, hash=True, repr=True) """The code points that form the emoji.""" @property @@ -162,22 +158,27 @@ def unicode_escape(self) -> str: @classmethod def from_codepoints(cls, codepoint: int, *codepoints: int) -> UnicodeEmoji: """Create a unicode emoji from one or more UTF-32 codepoints.""" - return UnicodeEmoji(name="".join(map(chr, (codepoint, *codepoints)))) + unicode_emoji = cls() + unicode_emoji.name = "".join(map(chr, (codepoint, *codepoints))) + return unicode_emoji @classmethod def from_emoji(cls, emoji: str) -> UnicodeEmoji: """Create a unicode emoji from a raw emoji.""" - return cls(name=emoji) + unicode_emoji = cls() + cls.name = emoji + return unicode_emoji @classmethod def from_unicode_escape(cls, escape: str) -> UnicodeEmoji: """Create a unicode emoji from a unicode escape string.""" - return cls(name=str(escape.encode("utf-8"), "unicode_escape")) + unicode_emoji = cls() + unicode_emoji.name = str(escape.encode("utf-8"), "unicode_escape") + return unicode_emoji -@marshaller.marshallable() -@attr.s(eq=True, hash=True, kw_only=True, slots=True) -class CustomEmoji(Emoji, bases.Unique): +@attr.s(eq=True, hash=True, init=False, kw_only=True, slots=True) +class CustomEmoji(Emoji, bases.Entity, bases.Unique): """Represents a custom emoji. This is a custom emoji that is from a guild you might not be part of. @@ -201,18 +202,11 @@ class CustomEmoji(Emoji, bases.Unique): https://github.com/discord/discord-api-docs/issues/1614 """ - name: typing.Optional[str] = marshaller.attrib(deserializer=str, if_none=None, eq=False, hash=False, repr=True) + name: typing.Optional[str] = attr.ib(eq=False, hash=False, repr=True) """The name of the emoji.""" - is_animated: typing.Optional[bool] = marshaller.attrib( - raw_name="animated", - deserializer=bool, - if_undefined=False, - if_none=None, - default=False, - eq=False, - hash=False, - repr=True, + is_animated: typing.Optional[bool] = attr.ib( + eq=False, hash=False, repr=True, ) """Whether the emoji is animated. @@ -241,12 +235,7 @@ def url(self) -> str: return urls.generate_cdn_url("emojis", str(self.id), format_="gif" if self.is_animated else "png", size=None) -def _deserialize_role_ids(payload: more_typing.JSONArray) -> typing.Set[bases.Snowflake]: - return {bases.Snowflake(role_id) for role_id in payload} - - -@marshaller.marshallable() -@attr.s(eq=True, hash=True, kw_only=True, slots=True) +@attr.s(eq=True, hash=True, init=False, kw_only=True, slots=True) class KnownCustomEmoji(CustomEmoji): """Represents an emoji that is known from a guild the bot is in. @@ -254,22 +243,16 @@ class KnownCustomEmoji(CustomEmoji): _are_ part of. Ass a result, it contains a lot more information with it. """ - role_ids: typing.Set[bases.Snowflake] = marshaller.attrib( - raw_name="roles", deserializer=_deserialize_role_ids, if_undefined=set, eq=False, hash=False, factory=set, + role_ids: typing.Set[bases.Snowflake] = attr.ib( + eq=False, hash=False, ) """The IDs of the roles that are whitelisted to use this emoji. If this is empty then any user can use this emoji regardless of their roles. """ - user: typing.Optional[users.User] = marshaller.attrib( - deserializer=users.User.deserialize, - if_none=None, - if_undefined=None, - inherit_kwargs=True, - default=None, - eq=False, - hash=False, + user: typing.Optional[users.User] = attr.ib( + eq=False, hash=False, ) """The user that created the emoji. @@ -278,30 +261,20 @@ class KnownCustomEmoji(CustomEmoji): permission in the server the emoji is from. """ - is_animated: bool = marshaller.attrib( - raw_name="animated", deserializer=bool, if_undefined=False, default=False, eq=False, hash=False, repr=True - ) + is_animated: bool = attr.ib(eq=False, hash=False, repr=True) """Whether the emoji is animated. Unlike in `CustomEmoji`, this information is always known, and will thus never be `None`. """ - is_colons_required: bool = marshaller.attrib(raw_name="require_colons", deserializer=bool, eq=False, hash=False) + is_colons_required: bool = attr.ib(eq=False, hash=False) """Whether this emoji must be wrapped in colons.""" - is_managed: bool = marshaller.attrib(raw_name="managed", deserializer=bool, eq=False, hash=False) + is_managed: bool = attr.ib(eq=False, hash=False) """Whether the emoji is managed by an integration.""" - is_available: bool = marshaller.attrib(raw_name="available", deserializer=bool, eq=False, hash=False) + is_available: bool = attr.ib(eq=False, hash=False) """Whether this emoji can currently be used. May be `False` due to a loss of Sever Boosts on the emoji's guild. """ - - -def deserialize_reaction_emoji(payload: typing.Dict, **kwargs: typing.Any) -> typing.Union[UnicodeEmoji, CustomEmoji]: - """Deserialize a reaction emoji into an emoji.""" - if payload.get("id"): - return CustomEmoji.deserialize(payload, **kwargs) - - return UnicodeEmoji.deserialize(payload, **kwargs) diff --git a/hikari/models/files.py b/hikari/models/files.py index 2b5701b67b..ae349e2ab8 100644 --- a/hikari/models/files.py +++ b/hikari/models/files.py @@ -67,9 +67,10 @@ import typing import aiohttp -from hikari.internal import more_asyncio + from hikari import errors +from hikari.utilities import aio # XXX: find optimal size. MAGIC_NUMBER: typing.Final[int] = 128 * 1024 @@ -355,11 +356,11 @@ def __init__(self, filename: str, obj: ___VALID_TYPES___) -> None: if inspect.isasyncgenfunction(obj): obj = obj() - if inspect.isasyncgen(obj) or more_asyncio.is_async_iterator(obj): + if inspect.isasyncgen(obj) or aio.is_async_iterator(obj): self._obj = _MemorizedAsyncIteratorDecorator(obj) return - if more_asyncio.is_async_iterable(obj): + if aio.is_async_iterable(obj): self._obj = obj return diff --git a/hikari/models/gateway.py b/hikari/models/gateway.py index 1b0ad4611c..461d645e5d 100644 --- a/hikari/models/gateway.py +++ b/hikari/models/gateway.py @@ -22,13 +22,12 @@ __all__ = ["GatewayBot", "SessionStartLimit"] -import datetime +import typing import attr - -def _rest_after_deserializer(after: int) -> datetime.timedelta: - return datetime.timedelta(milliseconds=after) +if typing.TYPE_CHECKING: + import datetime @attr.s(eq=True, hash=False, init=False, kw_only=True, slots=True) diff --git a/hikari/models/guilds.py b/hikari/models/guilds.py index d24196f8a6..da5656cb0a 100644 --- a/hikari/models/guilds.py +++ b/hikari/models/guilds.py @@ -41,12 +41,12 @@ "GuildPreview", "GuildMember", "GuildMemberPresence", - "GuildIntegration", + "Integration", "GuildMemberBan", "IntegrationAccount", "IntegrationExpireBehaviour", "PartialGuild", - "PartialGuildIntegration", + "PartialIntegration", "PartialRole", "PresenceActivity", "PresenceStatus", @@ -54,29 +54,28 @@ "UnavailableGuild", ] -import datetime +import enum import typing import attr -from hikari.internal import conversions -from hikari.internal import marshaller -from hikari.internal import more_enums from . import bases -from . import channels as channels_ -from . import colors -from . import emojis as emojis_ -from . import permissions as permissions_ from . import users -from ..internal import unset -from ..net import urls + +from hikari.net import urls +from hikari.utilities import unset if typing.TYPE_CHECKING: - from hikari.internal import more_typing + import datetime + + from . import channels as channels_ + from . import colors + from . import emojis as emojis_ + from . import permissions as permissions_ -@more_enums.must_be_unique -class GuildExplicitContentFilterLevel(int, more_enums.Enum): +@enum.unique +class GuildExplicitContentFilterLevel(int, enum.Enum): """Represents the explicit content filter setting for a guild.""" DISABLED = 0 @@ -89,8 +88,8 @@ class GuildExplicitContentFilterLevel(int, more_enums.Enum): """Filter all posts.""" -@more_enums.must_be_unique -class GuildFeature(str, more_enums.Enum): +@enum.unique +class GuildFeature(str, enum.Enum): """Features that a guild can provide.""" ANIMATED_ICON = "ANIMATED_ICON" @@ -145,8 +144,8 @@ class GuildFeature(str, more_enums.Enum): """Guild has enabled the welcome screen.""" -@more_enums.must_be_unique -class GuildMessageNotificationsLevel(int, more_enums.Enum): +@enum.unique +class GuildMessageNotificationsLevel(int, enum.Enum): """Represents the default notification level for new messages in a guild.""" ALL_MESSAGES = 0 @@ -156,8 +155,8 @@ class GuildMessageNotificationsLevel(int, more_enums.Enum): """Only notify users when they are @mentioned.""" -@more_enums.must_be_unique -class GuildMFALevel(int, more_enums.Enum): +@enum.unique +class GuildMFALevel(int, enum.Enum): """Represents the multi-factor authorization requirement for a guild.""" NONE = 0 @@ -167,8 +166,8 @@ class GuildMFALevel(int, more_enums.Enum): """MFA requirement.""" -@more_enums.must_be_unique -class GuildPremiumTier(int, more_enums.Enum): +@enum.unique +class GuildPremiumTier(int, enum.Enum): """Tier for Discord Nitro boosting in a guild.""" NONE = 0 @@ -184,8 +183,8 @@ class GuildPremiumTier(int, more_enums.Enum): """Level 3 Nitro boost.""" -@more_enums.must_be_unique -class GuildSystemChannelFlag(more_enums.IntFlag): +@enum.unique +class GuildSystemChannelFlag(enum.IntFlag): """Defines which features are suppressed in the system channel.""" SUPPRESS_USER_JOIN = 1 << 0 @@ -195,8 +194,8 @@ class GuildSystemChannelFlag(more_enums.IntFlag): """Display a message when the guild is Nitro boosted.""" -@more_enums.must_be_unique -class GuildVerificationLevel(int, more_enums.Enum): +@enum.unique +class GuildVerificationLevel(int, enum.Enum): """Represents the level of verification of a guild.""" NONE = 0 @@ -215,133 +214,99 @@ class GuildVerificationLevel(int, more_enums.Enum): """┻━┻ミヽ(ಠ益ಠ)ノ彡┻━┻ - must have a verified phone number.""" -@marshaller.marshallable() -@attr.s(eq=True, hash=False, kw_only=True, slots=True) -class GuildWidget(bases.Entity, marshaller.Deserializable): +@attr.s(eq=True, hash=False, init=False, kw_only=True, slots=True) +class GuildWidget(bases.Entity): """Represents a guild embed.""" - channel_id: typing.Optional[bases.Snowflake] = marshaller.attrib( - deserializer=bases.Snowflake, serializer=str, if_none=None, repr=True - ) + channel_id: typing.Optional[bases.Snowflake] = attr.ib(repr=True) """The ID of the channel the invite for this embed targets, if enabled.""" - is_enabled: bool = marshaller.attrib(raw_name="enabled", deserializer=bool, serializer=bool, repr=True) + is_enabled: bool = attr.ib(repr=True) """Whether this embed is enabled.""" -def _deserialize_role_ids(payload: more_typing.JSONArray) -> typing.Sequence[bases.Snowflake]: - return [bases.Snowflake(role_id) for role_id in payload] - - -@marshaller.marshallable() -@attr.s(eq=True, hash=True, kw_only=True, slots=True) -class GuildMember(bases.Entity, marshaller.Deserializable): +@attr.s(eq=True, hash=True, init=False, kw_only=True, slots=True) +class GuildMember(bases.Entity): """Used to represent a guild bound member.""" # TODO: make GuildMember delegate to user and implement a common base class # this allows members and users to be used interchangeably. - user: users.User = marshaller.attrib( - deserializer=users.User.deserialize, inherit_kwargs=True, eq=True, hash=True, repr=True - ) + user: users.User = attr.ib(eq=True, hash=True, repr=True) """This member's user object. This will be `None` when attached to Message Create and Update gateway events. """ - nickname: typing.Optional[str] = marshaller.attrib( - raw_name="nick", - deserializer=str, - if_none=None, - if_undefined=None, - default=None, - eq=False, - hash=False, - repr=True, + nickname: typing.Optional[str] = attr.ib( + eq=False, hash=False, repr=True, ) """This member's nickname, if set.""" - role_ids: typing.Sequence[bases.Snowflake] = marshaller.attrib( - raw_name="roles", deserializer=_deserialize_role_ids, eq=False, hash=False, + role_ids: typing.Set[bases.Snowflake] = attr.ib( + eq=False, hash=False, ) """A sequence of the IDs of the member's current roles.""" - joined_at: datetime.datetime = marshaller.attrib( - deserializer=conversions.iso8601_datetime_string_to_datetime, eq=False, hash=False - ) + joined_at: datetime.datetime = attr.ib(eq=False, hash=False) """The datetime of when this member joined the guild they belong to.""" - premium_since: typing.Optional[datetime.datetime] = marshaller.attrib( - deserializer=conversions.iso8601_datetime_string_to_datetime, - if_none=None, - if_undefined=None, - default=None, - eq=False, - hash=False, - ) + premium_since: typing.Optional[datetime.datetime] = attr.ib(eq=False, hash=False) """The datetime of when this member started "boosting" this guild. This will be `None` if they aren't boosting. """ - is_deaf: bool = marshaller.attrib(raw_name="deaf", deserializer=bool, eq=False, hash=False) + is_deaf: bool = attr.ib(eq=False, hash=False) """Whether this member is deafened by this guild in it's voice channels.""" - is_mute: bool = marshaller.attrib(raw_name="mute", deserializer=bool, eq=False, hash=False) + is_mute: bool = attr.ib(eq=False, hash=False) """Whether this member is muted by this guild in it's voice channels.""" -@marshaller.marshallable() -@attr.s(eq=True, hash=True, kw_only=True, slots=True) -class PartialRole(bases.Unique, marshaller.Deserializable): +@attr.s(eq=True, hash=True, init=False, kw_only=True, slots=True) +class PartialRole(bases.Entity, bases.Unique): """Represents a partial guild bound Role object.""" - name: str = marshaller.attrib(deserializer=str, serializer=str, eq=False, hash=False, repr=True) + name: str = attr.ib(eq=False, hash=False, repr=True) """The role's name.""" -@marshaller.marshallable() -@attr.s(eq=True, hash=True, kw_only=True, slots=True) -class Role(PartialRole, marshaller.Serializable): +@attr.s(eq=True, hash=True, init=False, kw_only=True, slots=True) +class Role(PartialRole): """Represents a guild bound Role object.""" - color: colors.Color = marshaller.attrib( - deserializer=colors.Color, serializer=int, default=colors.Color(0), eq=False, hash=False, repr=True, + color: colors.Color = attr.ib( + eq=False, hash=False, repr=True, ) """The colour of this role. - This will be applied to a member's name in chat if it's their top coloured role.""" + This will be applied to a member's name in chat if it's their top coloured role. + """ - is_hoisted: bool = marshaller.attrib( - raw_name="hoist", deserializer=bool, serializer=bool, default=False, eq=False, hash=False, repr=True - ) + is_hoisted: bool = attr.ib(eq=False, hash=False, repr=True) """Whether this role is hoisting the members it's attached to in the member list. - members will be hoisted under their highest role where this is set to `True`.""" + members will be hoisted under their highest role where this is set to `True`. + """ - position: int = marshaller.attrib(deserializer=int, serializer=int, default=None, eq=False, hash=False, repr=True) + position: int = attr.ib(eq=False, hash=False, repr=True) """The position of this role in the role hierarchy.""" - permissions: permissions_.Permission = marshaller.attrib( - deserializer=permissions_.Permission, serializer=int, default=permissions_.Permission(0), eq=False, hash=False - ) + permissions: permissions_.Permission = attr.ib(eq=False, hash=False) """The guild wide permissions this role gives to the members it's attached to, This may be overridden by channel overwrites. """ - is_managed: bool = marshaller.attrib( - raw_name="managed", deserializer=bool, serializer=None, default=None, eq=False, hash=False - ) + is_managed: bool = attr.ib(eq=False, hash=False) """Whether this role is managed by an integration.""" - is_mentionable: bool = marshaller.attrib( - raw_name="mentionable", deserializer=bool, serializer=bool, default=False, eq=False, hash=False - ) + is_mentionable: bool = attr.ib(eq=False, hash=False) """Whether this role can be mentioned by all regardless of permissions.""" -@more_enums.must_be_unique -class ActivityType(int, more_enums.Enum): +@enum.unique +class ActivityType(int, enum.Enum): """The activity type.""" PLAYING = 0 @@ -368,84 +333,64 @@ class ActivityType(int, more_enums.Enum): """ -@marshaller.marshallable() -@attr.s(eq=True, hash=False, kw_only=True, slots=True) -class ActivityTimestamps(bases.Entity, marshaller.Deserializable): +@attr.s(eq=True, hash=False, init=False, kw_only=True, slots=True) +class ActivityTimestamps: """The datetimes for the start and/or end of an activity session.""" - start: typing.Optional[datetime.datetime] = marshaller.attrib( - deserializer=conversions.unix_epoch_to_datetime, if_undefined=None, default=None, repr=True - ) + start: typing.Optional[datetime.datetime] = attr.ib(repr=True) """When this activity's session was started, if applicable.""" - end: typing.Optional[datetime.datetime] = marshaller.attrib( - deserializer=conversions.unix_epoch_to_datetime, if_undefined=None, default=None, repr=True - ) + end: typing.Optional[datetime.datetime] = attr.ib(repr=True) """When this activity's session will end, if applicable.""" -@marshaller.marshallable() -@attr.s(eq=True, hash=True, kw_only=True, slots=True) -class ActivityParty(bases.Entity, marshaller.Deserializable): +@attr.s(eq=True, hash=True, init=False, kw_only=True, slots=True) +class ActivityParty: """Used to represent activity groups of users.""" - id: typing.Optional[str] = marshaller.attrib( - deserializer=str, if_undefined=None, default=None, eq=True, hash=True, repr=True - ) + id: typing.Optional[str] = attr.ib(eq=True, hash=True, repr=True) """The string id of this party instance, if set.""" - _size_information: typing.Optional[typing.Tuple[int, int]] = marshaller.attrib( - raw_name="size", deserializer=tuple, if_undefined=None, default=None, eq=False, hash=False - ) - """The size metadata of this party, if applicable.""" - - # Ignore docstring not starting in an imperative mood - @property - def current_size(self) -> typing.Optional[int]: # noqa: D401 - """Current size of this party, if applicable.""" - return self._size_information[0] if self._size_information else None + current_size: typing.Optional[int] = attr.ib(eq=False, hash=False) + """Current size of this party, if applicable.""" - @property - def max_size(self) -> typing.Optional[int]: - """Maximum size of this party, if applicable.""" - return self._size_information[1] if self._size_information else None + max_size: typing.Optional[int] = attr.ib(eq=False, hash=False) + """Maximum size of this party, if applicable.""" -@marshaller.marshallable() -@attr.s(eq=True, hash=False, kw_only=True, slots=True) -class ActivityAssets(bases.Entity, marshaller.Deserializable): +@attr.s(eq=True, hash=False, init=False, kw_only=True, slots=True) +class ActivityAssets: """Used to represent possible assets for an activity.""" - large_image: typing.Optional[str] = marshaller.attrib(deserializer=str, if_undefined=None, default=None) + large_image: typing.Optional[str] = attr.ib() """The ID of the asset's large image, if set.""" - large_text: typing.Optional[str] = marshaller.attrib(deserializer=str, if_undefined=None, default=None) + large_text: typing.Optional[str] = attr.ib() """The text that'll appear when hovering over the large image, if set.""" - small_image: typing.Optional[str] = marshaller.attrib(deserializer=str, if_undefined=None, default=None) + small_image: typing.Optional[str] = attr.ib() """The ID of the asset's small image, if set.""" - small_text: typing.Optional[str] = marshaller.attrib(deserializer=str, if_undefined=None, default=None) + small_text: typing.Optional[str] = attr.ib() """The text that'll appear when hovering over the small image, if set.""" -@marshaller.marshallable() -@attr.s(eq=True, hash=False, kw_only=True, slots=True) -class ActivitySecret(bases.Entity, marshaller.Deserializable): +@attr.s(eq=True, hash=False, init=False, kw_only=True, slots=True) +class ActivitySecret: """The secrets used for interacting with an activity party.""" - join: typing.Optional[str] = marshaller.attrib(deserializer=str, if_undefined=None, default=None) + join: typing.Optional[str] = attr.ib() """The secret used for joining a party, if applicable.""" - spectate: typing.Optional[str] = marshaller.attrib(deserializer=str, if_undefined=None, default=None) + spectate: typing.Optional[str] = attr.ib() """The secret used for spectating a party, if applicable.""" - match: typing.Optional[str] = marshaller.attrib(deserializer=str, if_undefined=None, default=None) + match: typing.Optional[str] = attr.ib() """The secret used for joining a party, if applicable.""" -@more_enums.must_be_unique -class ActivityFlag(more_enums.IntFlag): +@enum.unique +class ActivityFlag(enum.IntFlag): """Flags that describe what an activity includes. This can be more than one using bitwise-combinations. @@ -470,71 +415,56 @@ class ActivityFlag(more_enums.IntFlag): """Play""" -@marshaller.marshallable() -@attr.s(eq=True, hash=False, kw_only=True, slots=True) -class PresenceActivity(bases.Entity, marshaller.Deserializable): +@attr.s(eq=True, hash=False, init=False, kw_only=True, slots=True) +class PresenceActivity: """Represents an activity that will be attached to a member's presence.""" - name: str = marshaller.attrib(deserializer=str, repr=True) + name: str = attr.ib(repr=True) """The activity's name.""" - type: ActivityType = marshaller.attrib(deserializer=ActivityType, repr=True) + type: ActivityType = attr.ib(repr=True) """The activity's type.""" - url: typing.Optional[str] = marshaller.attrib(deserializer=str, if_undefined=None, if_none=None, default=None) + url: typing.Optional[str] = attr.ib() """The URL for a `STREAM` type activity, if applicable.""" - created_at: datetime.datetime = marshaller.attrib(deserializer=conversions.unix_epoch_to_datetime) + created_at: datetime.datetime = attr.ib() """When this activity was added to the user's session.""" - timestamps: typing.Optional[ActivityTimestamps] = marshaller.attrib( - deserializer=ActivityTimestamps.deserialize, if_undefined=None, default=None, inherit_kwargs=True - ) + timestamps: typing.Optional[ActivityTimestamps] = attr.ib() """The timestamps for when this activity's current state will start and end, if applicable. """ - application_id: typing.Optional[bases.Snowflake] = marshaller.attrib( - deserializer=bases.Snowflake, if_undefined=None, default=None - ) + application_id: typing.Optional[bases.Snowflake] = attr.ib() """The ID of the application this activity is for, if applicable.""" - details: typing.Optional[str] = marshaller.attrib(deserializer=str, if_undefined=None, if_none=None, default=None) + details: typing.Optional[str] = attr.ib() """The text that describes what the activity's target is doing, if set.""" - state: typing.Optional[str] = marshaller.attrib(deserializer=str, if_undefined=None, if_none=None, default=None) + state: typing.Optional[str] = attr.ib() """The current status of this activity's target, if set.""" - emoji: typing.Union[None, emojis_.UnicodeEmoji, emojis_.CustomEmoji] = marshaller.attrib( - deserializer=emojis_.deserialize_reaction_emoji, if_undefined=None, default=None, inherit_kwargs=True - ) + emoji: typing.Union[None, emojis_.UnicodeEmoji, emojis_.CustomEmoji] = attr.ib() """The emoji of this activity, if it is a custom status and set.""" - party: typing.Optional[ActivityParty] = marshaller.attrib( - deserializer=ActivityParty.deserialize, if_undefined=None, default=None, inherit_kwargs=True - ) + party: typing.Optional[ActivityParty] = attr.ib() """Information about the party associated with this activity, if set.""" - assets: typing.Optional[ActivityAssets] = marshaller.attrib( - deserializer=ActivityAssets.deserialize, if_undefined=None, default=None, inherit_kwargs=True - ) + assets: typing.Optional[ActivityAssets] = attr.ib() """Images and their hover over text for the activity.""" - secrets: typing.Optional[ActivitySecret] = marshaller.attrib( - deserializer=ActivitySecret.deserialize, if_undefined=None, default=None, inherit_kwargs=True - ) + secrets: typing.Optional[ActivitySecret] = attr.ib() """Secrets for Rich Presence joining and spectating.""" - is_instance: typing.Optional[bool] = marshaller.attrib( - raw_name="instance", deserializer=bool, if_undefined=None, default=None - ) + is_instance: typing.Optional[bool] = attr.ib() """Whether this activity is an instanced game session.""" - flags: ActivityFlag = marshaller.attrib(deserializer=ActivityFlag, if_undefined=None, default=None) + flags: ActivityFlag = attr.ib() """Flags that describe what the activity includes.""" -class PresenceStatus(str, more_enums.Enum): +class PresenceStatus(str, enum.Enum): """The status of a member.""" ONLINE = "online" @@ -553,34 +483,22 @@ class PresenceStatus(str, more_enums.Enum): """Offline or invisible/grey.""" -def _default_status() -> typing.Literal[PresenceStatus.OFFLINE]: - return PresenceStatus.OFFLINE - - -@marshaller.marshallable() -@attr.s(eq=True, hash=False, kw_only=True, slots=True) -class ClientStatus(bases.Entity, marshaller.Deserializable): +@attr.s(eq=True, hash=False, init=False, kw_only=True, slots=True) +class ClientStatus: """The client statuses for this member.""" - desktop: PresenceStatus = marshaller.attrib( - deserializer=PresenceStatus, if_undefined=_default_status, default=PresenceStatus.OFFLINE, repr=True - ) + desktop: PresenceStatus = attr.ib(repr=True) """The status of the target user's desktop session.""" - mobile: PresenceStatus = marshaller.attrib( - deserializer=PresenceStatus, if_undefined=_default_status, default=PresenceStatus.OFFLINE, repr=True - ) + mobile: PresenceStatus = attr.ib(repr=True) """The status of the target user's mobile session.""" - web: PresenceStatus = marshaller.attrib( - deserializer=PresenceStatus, if_undefined=_default_status, default=PresenceStatus.OFFLINE, repr=True - ) + web: PresenceStatus = attr.ib(repr=True) """The status of the target user's web session.""" # TODO: should this be an event instead? -@marshaller.marshallable() -@attr.s(eq=True, hash=True, kw_only=True, slots=True) +@attr.s(eq=True, hash=True, init=False, kw_only=True, slots=True) class PresenceUser(users.User): """A user representation specifically used for presence updates. @@ -589,47 +507,28 @@ class PresenceUser(users.User): unless it is specifically being modified for this update. """ - discriminator: typing.Union[str, unset.Unset] = marshaller.attrib( - deserializer=str, if_undefined=unset.Unset, default=unset.UNSET, eq=False, hash=False, repr=True - ) + discriminator: typing.Union[str, unset.Unset] = attr.ib(eq=False, hash=False, repr=True) """This user's discriminator.""" - username: typing.Union[str, unset.Unset] = marshaller.attrib( - deserializer=str, if_undefined=unset.Unset, default=unset.UNSET, eq=False, hash=False, repr=True - ) + username: typing.Union[str, unset.Unset] = attr.ib(eq=False, hash=False, repr=True) """This user's username.""" - avatar_hash: typing.Union[None, str, unset.Unset] = marshaller.attrib( - raw_name="avatar", - deserializer=str, - if_none=None, - if_undefined=unset.Unset, - default=unset.UNSET, - eq=False, - hash=False, - repr=True, + avatar_hash: typing.Union[None, str, unset.Unset] = attr.ib( + eq=False, hash=False, repr=True, ) """This user's avatar hash, if set.""" - is_bot: typing.Union[bool, unset.Unset] = marshaller.attrib( - raw_name="bot", - deserializer=bool, - if_undefined=unset.Unset, - default=unset.UNSET, - eq=False, - hash=False, - repr=True, + is_bot: typing.Union[bool, unset.Unset] = attr.ib( + eq=False, hash=False, repr=True, ) """Whether this user is a bot account.""" - is_system: typing.Union[bool, unset.Unset] = marshaller.attrib( - raw_name="system", deserializer=bool, if_undefined=unset.Unset, default=unset.UNSET, eq=False, hash=False, + is_system: typing.Union[bool, unset.Unset] = attr.ib( + eq=False, hash=False, ) """Whether this user is a system account.""" - flags: typing.Union[users.UserFlag, unset.Unset] = marshaller.attrib( - raw_name="public_flags", deserializer=users.UserFlag, if_undefined=unset.Unset, eq=False, hash=False - ) + flags: typing.Union[users.UserFlag, unset.Unset] = attr.ib(eq=False, hash=False) """The public flags for this user.""" @property @@ -699,18 +598,11 @@ def default_avatar_url(self) -> typing.Union[str, unset.Unset]: return unset.UNSET -def _deserialize_activities(payload: more_typing.JSONArray, **kwargs: typing.Any) -> typing.Sequence[PresenceActivity]: - return [PresenceActivity.deserialize(activity, **kwargs) for activity in payload] - - -@marshaller.marshallable() -@attr.s(eq=True, hash=True, kw_only=True, slots=True) -class GuildMemberPresence(bases.Entity, marshaller.Deserializable): +@attr.s(eq=True, hash=True, init=False, kw_only=True, slots=True) +class GuildMemberPresence(bases.Entity): """Used to represent a guild member's presence.""" - user: PresenceUser = marshaller.attrib( - deserializer=PresenceUser.deserialize, inherit_kwargs=True, eq=True, hash=True, repr=True - ) + user: PresenceUser = attr.ib(eq=True, hash=True, repr=True) """The object of the user who this presence is for. !!! info @@ -719,8 +611,8 @@ class GuildMemberPresence(bases.Entity, marshaller.Deserializable): changed in an event. """ - role_ids: typing.Optional[typing.Sequence[bases.Snowflake]] = marshaller.attrib( - raw_name="roles", deserializer=_deserialize_role_ids, if_undefined=None, default=None, eq=False, hash=False, + role_ids: typing.Optional[typing.Sequence[bases.Snowflake]] = attr.ib( + eq=False, hash=False, ) """The ids of the user's current roles in the guild this presence belongs to. @@ -728,60 +620,42 @@ class GuildMemberPresence(bases.Entity, marshaller.Deserializable): If this is `None` then this information wasn't provided and is unknown. """ - guild_id: typing.Optional[bases.Snowflake] = marshaller.attrib( - deserializer=bases.Snowflake, if_undefined=None, default=None, eq=True, hash=True, repr=True - ) + guild_id: typing.Optional[bases.Snowflake] = attr.ib(eq=True, hash=True, repr=True) """The ID of the guild this presence belongs to. This will be `None` when received in an array of members attached to a guild object (e.g on Guild Create). """ - visible_status: PresenceStatus = marshaller.attrib( - raw_name="status", deserializer=PresenceStatus, eq=False, hash=False, repr=True - ) + visible_status: PresenceStatus = attr.ib(eq=False, hash=False, repr=True) """This user's current status being displayed by the client.""" - activities: typing.Sequence[PresenceActivity] = marshaller.attrib( - deserializer=_deserialize_activities, inherit_kwargs=True, eq=False, hash=False, - ) + activities: typing.Sequence[PresenceActivity] = attr.ib(eq=False, hash=False) """An array of the user's activities, with the top one will being prioritised by the client. """ - client_status: ClientStatus = marshaller.attrib( - deserializer=ClientStatus.deserialize, inherit_kwargs=True, eq=False, hash=False, + client_status: ClientStatus = attr.ib( + eq=False, hash=False, ) """An object of the target user's client statuses.""" - premium_since: typing.Optional[datetime.datetime] = marshaller.attrib( - deserializer=conversions.iso8601_datetime_string_to_datetime, - if_undefined=None, - if_none=None, - default=None, - eq=False, - hash=False, + premium_since: typing.Optional[datetime.datetime] = attr.ib( + eq=False, hash=False, ) """The datetime of when this member started "boosting" this guild. This will be `None` if they aren't boosting. """ - nick: typing.Optional[str] = marshaller.attrib( - raw_name="nick", - deserializer=str, - if_undefined=None, - if_none=None, - default=None, - eq=False, - hash=False, - repr=True, + nickname: typing.Optional[str] = attr.ib( + eq=False, hash=False, repr=True, ) """This member's nickname, if set.""" -@more_enums.must_be_unique -class IntegrationExpireBehaviour(int, more_enums.Enum): +@enum.unique +class IntegrationExpireBehaviour(int, enum.Enum): """Behavior for expiring integration subscribers.""" REMOVE_ROLE = 0 @@ -791,102 +665,79 @@ class IntegrationExpireBehaviour(int, more_enums.Enum): """Kick the subscriber.""" -@marshaller.marshallable() -@attr.s(eq=True, hash=True, kw_only=True, slots=True) -class IntegrationAccount(bases.Entity, marshaller.Deserializable): +@attr.s(eq=True, hash=True, init=False, kw_only=True, slots=True) +class IntegrationAccount: """An account that's linked to an integration.""" - id: str = marshaller.attrib(deserializer=str, eq=True, hash=True) + id: str = attr.ib(eq=True, hash=True) """The string ID of this (likely) third party account.""" - name: str = marshaller.attrib(deserializer=str, eq=False, hash=False) + name: str = attr.ib(eq=False, hash=False) """The name of this account.""" -@marshaller.marshallable() -@attr.s(eq=True, hash=True, kw_only=True, slots=True) -class PartialGuildIntegration(bases.Unique, marshaller.Deserializable): +@attr.s(eq=True, hash=True, init=False, kw_only=True, slots=True) +class PartialIntegration(bases.Unique): """A partial representation of an integration, found in audit logs.""" - name: str = marshaller.attrib(deserializer=str, eq=False, hash=False, repr=True) + name: str = attr.ib(eq=False, hash=False, repr=True) """The name of this integration.""" - type: str = marshaller.attrib(deserializer=str, eq=False, hash=False, repr=True) + type: str = attr.ib(eq=False, hash=False, repr=True) """The type of this integration.""" - account: IntegrationAccount = marshaller.attrib( - deserializer=IntegrationAccount.deserialize, inherit_kwargs=True, eq=False, hash=False - ) + account: IntegrationAccount = attr.ib(eq=False, hash=False) """The account connected to this integration.""" -def _deserialize_expire_grace_period(payload: int) -> datetime.timedelta: - return datetime.timedelta(days=payload) - - -@marshaller.marshallable() -@attr.s(eq=True, hash=True, kw_only=True, slots=True) -class GuildIntegration(bases.Unique, marshaller.Deserializable): +@attr.s(eq=True, hash=True, init=False, kw_only=True, slots=True) +class Integration(PartialIntegration): """Represents a guild integration object.""" - is_enabled: bool = marshaller.attrib(raw_name="enabled", deserializer=bool, eq=False, hash=False, repr=True) + is_enabled: bool = attr.ib(eq=False, hash=False, repr=True) """Whether this integration is enabled.""" - is_syncing: bool = marshaller.attrib(raw_name="syncing", deserializer=bool, eq=False, hash=False) + is_syncing: bool = attr.ib(eq=False, hash=False) """Whether this integration is syncing subscribers/emojis.""" - role_id: typing.Optional[bases.Snowflake] = marshaller.attrib(deserializer=bases.Snowflake, eq=False, hash=False) + role_id: typing.Optional[bases.Snowflake] = attr.ib(eq=False, hash=False) """The ID of the managed role used for this integration's subscribers.""" - is_emojis_enabled: typing.Optional[bool] = marshaller.attrib( - raw_name="enable_emoticons", deserializer=bool, if_undefined=None, default=None, eq=False, hash=False - ) + is_emojis_enabled: typing.Optional[bool] = attr.ib(eq=False, hash=False) """Whether users under this integration are allowed to use it's custom emojis.""" - expire_behavior: IntegrationExpireBehaviour = marshaller.attrib( - deserializer=IntegrationExpireBehaviour, eq=False, hash=False - ) + expire_behavior: IntegrationExpireBehaviour = attr.ib(eq=False, hash=False) """How members should be treated after their connected subscription expires. This won't be enacted until after `GuildIntegration.expire_grace_period` passes. """ - expire_grace_period: datetime.timedelta = marshaller.attrib( - deserializer=_deserialize_expire_grace_period, eq=False, hash=False - ) + expire_grace_period: datetime.timedelta = attr.ib(eq=False, hash=False) """How many days users with expired subscriptions are given until `GuildIntegration.expire_behavior` is enacted out on them """ - user: users.User = marshaller.attrib(deserializer=users.User.deserialize, inherit_kwargs=True, eq=False, hash=False) + user: users.User = attr.ib(eq=False, hash=False) """The user this integration belongs to.""" - last_synced_at: datetime.datetime = marshaller.attrib( - raw_name="synced_at", - deserializer=conversions.iso8601_datetime_string_to_datetime, - if_none=None, - eq=False, - hash=False, - ) + last_synced_at: datetime.datetime = attr.ib(eq=False, hash=False) """The datetime of when this integration's subscribers were last synced.""" -@marshaller.marshallable() -@attr.s(eq=True, hash=False, kw_only=True, slots=True) -class GuildMemberBan(bases.Entity, marshaller.Deserializable): +@attr.s(eq=True, hash=False, init=False, kw_only=True, slots=True) +class GuildMemberBan: """Used to represent guild bans.""" - reason: typing.Optional[str] = marshaller.attrib(deserializer=str, if_none=None, repr=True) + reason: typing.Optional[str] = attr.ib(repr=True) """The reason for this ban, will be `None` if no reason was given.""" - user: users.User = marshaller.attrib(deserializer=users.User.deserialize, inherit_kwargs=True, repr=True) + user: users.User = attr.ib(repr=True) """The object of the user this ban targets.""" -@marshaller.marshallable() -@attr.s(eq=True, hash=True, kw_only=True, slots=True) -class UnavailableGuild(bases.Unique, marshaller.Deserializable): +@attr.s(eq=True, hash=True, init=False, kw_only=True, slots=True) +class UnavailableGuild(bases.Entity, bases.Unique): """An unavailable guild object, received during gateway events such as READY. An unavailable guild cannot be interacted with, and most information may @@ -903,26 +754,17 @@ def is_unavailable(self) -> bool: # noqa: D401 return True -def _deserialize_features(payload: more_typing.JSONArray) -> typing.Set[typing.Union[GuildFeature, str]]: - return {conversions.try_cast(feature, GuildFeature, feature) for feature in payload} - - -@marshaller.marshallable() -@attr.s(eq=True, hash=True, kw_only=True, slots=True) -class PartialGuild(bases.Unique, marshaller.Deserializable): +@attr.s(eq=True, hash=True, init=False, kw_only=True, slots=True) +class PartialGuild(bases.Entity, bases.Unique): """Base object for any partial guild objects.""" - name: str = marshaller.attrib(deserializer=str, eq=False, hash=False, repr=True) + name: str = attr.ib(eq=False, hash=False, repr=True) """The name of the guild.""" - icon_hash: typing.Optional[str] = marshaller.attrib( - raw_name="icon", deserializer=str, if_none=None, eq=False, hash=False - ) + icon_hash: typing.Optional[str] = attr.ib(eq=False, hash=False) """The hash for the guild icon, if there is one.""" - features: typing.Set[typing.Union[GuildFeature, str]] = marshaller.attrib( - deserializer=_deserialize_features, eq=False, hash=False - ) + features: typing.Set[typing.Union[GuildFeature, str]] = attr.ib(eq=False, hash=False) """A set of the features in this guild.""" def format_icon_url(self, *, format_: typing.Optional[str] = None, size: int = 4096) -> typing.Optional[str]: @@ -960,39 +802,30 @@ def icon_url(self) -> typing.Optional[str]: return self.format_icon_url() -def _deserialize_emojis( - payload: more_typing.JSONArray, **kwargs: typing.Any -) -> typing.Mapping[bases.Snowflake, emojis_.KnownCustomEmoji]: - return {bases.Snowflake(emoji["id"]): emojis_.KnownCustomEmoji.deserialize(emoji, **kwargs) for emoji in payload} - - -@marshaller.marshallable() -@attr.s(eq=True, hash=True, kw_only=True, slots=True) +@attr.s(eq=True, hash=True, init=False, kw_only=True, slots=True) class GuildPreview(PartialGuild): """A preview of a guild with the `GuildFeature.PUBLIC` feature.""" - splash_hash: typing.Optional[str] = marshaller.attrib( - raw_name="splash", deserializer=str, if_none=None, eq=False, hash=False - ) + splash_hash: typing.Optional[str] = attr.ib(eq=False, hash=False) """The hash of the splash for the guild, if there is one.""" - discovery_splash_hash: typing.Optional[str] = marshaller.attrib( - raw_name="discovery_splash", deserializer=str, if_none=None, eq=False, hash=False, + discovery_splash_hash: typing.Optional[str] = attr.ib( + eq=False, hash=False, ) """The hash of the discovery splash for the guild, if there is one.""" - emojis: typing.Mapping[bases.Snowflake, emojis_.KnownCustomEmoji] = marshaller.attrib( - deserializer=_deserialize_emojis, inherit_kwargs=True, eq=False, hash=False, + emojis: typing.Mapping[bases.Snowflake, emojis_.KnownCustomEmoji] = attr.ib( + eq=False, hash=False, ) """The mapping of IDs to the emojis this guild provides.""" - approximate_presence_count: int = marshaller.attrib(deserializer=int, eq=False, hash=False, repr=True) + approximate_presence_count: int = attr.ib(eq=False, hash=False, repr=True) """The approximate amount of presences in guild.""" - approximate_member_count: int = marshaller.attrib(deserializer=int, eq=False, hash=False, repr=True) + approximate_member_count: int = attr.ib(eq=False, hash=False, repr=True) """The approximate amount of members in this guild.""" - description: typing.Optional[str] = marshaller.attrib(deserializer=str, if_none=None, eq=False, hash=False) + description: typing.Optional[str] = attr.ib(eq=False, hash=False) """The guild's description, if set.""" def format_splash_url(self, *, format_: str = "png", size: int = 4096) -> typing.Optional[str]: @@ -1060,37 +893,7 @@ def discovery_splash_url(self) -> typing.Optional[str]: return self.format_discovery_splash_url() -def _deserialize_afk_timeout(payload: int) -> datetime.timedelta: - return datetime.timedelta(seconds=payload) - - -def _deserialize_roles(payload: more_typing.JSONArray, **kwargs: typing.Any) -> typing.Mapping[bases.Snowflake, Role]: - return {bases.Snowflake(role["id"]): Role.deserialize(role, **kwargs) for role in payload} - - -def _deserialize_members( - payload: more_typing.JSONArray, **kwargs: typing.Any -) -> typing.Mapping[bases.Snowflake, GuildMember]: - return {bases.Snowflake(member["user"]["id"]): GuildMember.deserialize(member, **kwargs) for member in payload} - - -def _deserialize_channels( - payload: more_typing.JSONArray, **kwargs: typing.Any -) -> typing.Mapping[bases.Snowflake, channels_.GuildChannel]: - return {bases.Snowflake(channel["id"]): channels_.deserialize_channel(channel, **kwargs) for channel in payload} - - -def _deserialize_presences( - payload: more_typing.JSONArray, **kwargs: typing.Any -) -> typing.Mapping[bases.Snowflake, GuildMemberPresence]: - return { - bases.Snowflake(presence["user"]["id"]): GuildMemberPresence.deserialize(presence, **kwargs) - for presence in payload - } - - -@marshaller.marshallable() -@attr.s(eq=True, hash=True, kw_only=True, slots=True) +@attr.s(eq=True, hash=True, init=False, kw_only=True, slots=True) class Guild(PartialGuild): # pylint:disable=too-many-instance-attributes """A representation of a guild on Discord. @@ -1101,26 +904,17 @@ class Guild(PartialGuild): # pylint:disable=too-many-instance-attributes any other fields should be ignored. """ - splash_hash: typing.Optional[str] = marshaller.attrib( - raw_name="splash", deserializer=str, if_none=None, eq=False, hash=False - ) + splash_hash: typing.Optional[str] = attr.ib(eq=False, hash=False) """The hash of the splash for the guild, if there is one.""" - discovery_splash_hash: typing.Optional[str] = marshaller.attrib( - raw_name="discovery_splash", deserializer=str, if_none=None, eq=False, hash=False - ) + discovery_splash_hash: typing.Optional[str] = attr.ib(eq=False, hash=False) """The hash of the discovery splash for the guild, if there is one.""" - owner_id: bases.Snowflake = marshaller.attrib(deserializer=bases.Snowflake, eq=False, hash=False, repr=True) + owner_id: bases.Snowflake = attr.ib(eq=False, hash=False, repr=True) """The ID of the owner of this guild.""" - my_permissions: permissions_.Permission = marshaller.attrib( - raw_name="permissions", - deserializer=permissions_.Permission, - if_undefined=None, - default=None, - eq=False, - hash=False, + my_permissions: permissions_.Permission = attr.ib( + eq=False, hash=False, ) """The guild-level permissions that apply to the bot user. @@ -1131,27 +925,23 @@ class Guild(PartialGuild): # pylint:disable=too-many-instance-attributes rather than from the gateway. """ - region: str = marshaller.attrib(deserializer=str, eq=False, hash=False) + region: str = attr.ib(eq=False, hash=False) """The voice region for the guild.""" - afk_channel_id: typing.Optional[bases.Snowflake] = marshaller.attrib( - deserializer=bases.Snowflake, if_none=None, eq=False, hash=False - ) + afk_channel_id: typing.Optional[bases.Snowflake] = attr.ib(eq=False, hash=False) """The ID for the channel that AFK voice users get sent to. If `None`, then no AFK channel is set up for this guild. """ - afk_timeout: datetime.timedelta = marshaller.attrib(deserializer=_deserialize_afk_timeout, eq=False, hash=False) + afk_timeout: datetime.timedelta = attr.ib(eq=False, hash=False) """Timeout for activity before a member is classed as AFK. How long a voice user has to be AFK for before they are classed as being AFK and are moved to the AFK channel (`Guild.afk_channel_id`). """ - is_embed_enabled: typing.Optional[bool] = marshaller.attrib( - raw_name="embed_enabled", deserializer=bool, if_undefined=False, default=False, eq=False, hash=False - ) + is_embed_enabled: typing.Optional[bool] = attr.ib(eq=False, hash=False) """Defines if the guild embed is enabled or not. This information may not be present, in which case, it will be `None` @@ -1161,9 +951,7 @@ class Guild(PartialGuild): # pylint:disable=too-many-instance-attributes Use `is_widget_enabled` instead. """ - embed_channel_id: typing.Optional[bases.Snowflake] = marshaller.attrib( - deserializer=bases.Snowflake, if_undefined=None, if_none=None, default=None, eq=False, hash=False - ) + embed_channel_id: typing.Optional[bases.Snowflake] = attr.ib(eq=False, hash=False) """The channel ID that the guild embed will generate an invite to. Will be `None` if invites are disabled for this guild's embed. @@ -1172,45 +960,33 @@ class Guild(PartialGuild): # pylint:disable=too-many-instance-attributes Use `widget_channel_id` instead. """ - verification_level: GuildVerificationLevel = marshaller.attrib( - deserializer=GuildVerificationLevel, eq=False, hash=False - ) + verification_level: GuildVerificationLevel = attr.ib(eq=False, hash=False) """The verification level required for a user to participate in this guild.""" - default_message_notifications: GuildMessageNotificationsLevel = marshaller.attrib( - deserializer=GuildMessageNotificationsLevel, eq=False, hash=False - ) + default_message_notifications: GuildMessageNotificationsLevel = attr.ib(eq=False, hash=False) """The default setting for message notifications in this guild.""" - explicit_content_filter: GuildExplicitContentFilterLevel = marshaller.attrib( - deserializer=GuildExplicitContentFilterLevel, eq=False, hash=False - ) + explicit_content_filter: GuildExplicitContentFilterLevel = attr.ib(eq=False, hash=False) """The setting for the explicit content filter in this guild.""" - roles: typing.Mapping[bases.Snowflake, Role] = marshaller.attrib( - deserializer=_deserialize_roles, inherit_kwargs=True, eq=False, hash=False, + roles: typing.Mapping[bases.Snowflake, Role] = attr.ib( + eq=False, hash=False, ) """The roles in this guild, represented as a mapping of ID to role object.""" - emojis: typing.Mapping[bases.Snowflake, emojis_.KnownCustomEmoji] = marshaller.attrib( - deserializer=_deserialize_emojis, inherit_kwargs=True, eq=False, hash=False, - ) + emojis: typing.Mapping[bases.Snowflake, emojis_.KnownCustomEmoji] = attr.ib(eq=False, hash=False) """A mapping of IDs to the objects of the emojis this guild provides.""" - mfa_level: GuildMFALevel = marshaller.attrib(deserializer=GuildMFALevel, eq=False, hash=False) + mfa_level: GuildMFALevel = attr.ib(eq=False, hash=False) """The required MFA level for users wishing to participate in this guild.""" - application_id: typing.Optional[bases.Snowflake] = marshaller.attrib( - deserializer=bases.Snowflake, if_none=None, eq=False, hash=False - ) + application_id: typing.Optional[bases.Snowflake] = attr.ib(eq=False, hash=False) """The ID of the application that created this guild. This will always be `None` for guilds that weren't created by a bot. """ - is_unavailable: typing.Optional[bool] = marshaller.attrib( - raw_name="unavailable", deserializer=bool, if_undefined=None, default=None, eq=False, hash=False - ) + is_unavailable: typing.Optional[bool] = attr.ib(eq=False, hash=False) """Whether the guild is unavailable or not. This information is only available if the guild was sent via a @@ -1221,52 +997,36 @@ class Guild(PartialGuild): # pylint:disable=too-many-instance-attributes be outdated if that is the case. """ - is_widget_enabled: typing.Optional[bool] = marshaller.attrib( - raw_name="widget_enabled", deserializer=bool, if_undefined=None, default=None, eq=False, hash=False - ) + is_widget_enabled: typing.Optional[bool] = attr.ib(eq=False, hash=False) """Describes whether the guild widget is enabled or not. If this information is not present, this will be `None`. """ - widget_channel_id: typing.Optional[bases.Snowflake] = marshaller.attrib( - deserializer=bases.Snowflake, if_undefined=None, if_none=None, default=None, eq=False, hash=False - ) + widget_channel_id: typing.Optional[bases.Snowflake] = attr.ib(eq=False, hash=False) """The channel ID that the widget's generated invite will send the user to. If this information is unavailable or this isn't enabled for the guild then this will be `None`. """ - system_channel_id: typing.Optional[bases.Snowflake] = marshaller.attrib( - if_none=None, deserializer=bases.Snowflake, eq=False, hash=False - ) + system_channel_id: typing.Optional[bases.Snowflake] = attr.ib(eq=False, hash=False) """The ID of the system channel or `None` if it is not enabled. Welcome messages and Nitro boost messages may be sent to this channel. """ - system_channel_flags: GuildSystemChannelFlag = marshaller.attrib( - deserializer=GuildSystemChannelFlag, eq=False, hash=False - ) + system_channel_flags: GuildSystemChannelFlag = attr.ib(eq=False, hash=False) """Flags for the guild system channel to describe which notifications are suppressed.""" - rules_channel_id: typing.Optional[bases.Snowflake] = marshaller.attrib( - if_none=None, deserializer=bases.Snowflake, eq=False, hash=False - ) + rules_channel_id: typing.Optional[bases.Snowflake] = attr.ib(eq=False, hash=False) """The ID of the channel where guilds with the `GuildFeature.PUBLIC` `features` display rules and guidelines. If the `GuildFeature.PUBLIC` feature is not defined, then this is `None`. """ - joined_at: typing.Optional[datetime.datetime] = marshaller.attrib( - deserializer=conversions.iso8601_datetime_string_to_datetime, - if_undefined=None, - default=None, - eq=False, - hash=False, - ) + joined_at: typing.Optional[datetime.datetime] = attr.ib(eq=False, hash=False) """The date and time that the bot user joined this guild. This information is only available if the guild was sent via a `GUILD_CREATE` @@ -1274,9 +1034,7 @@ class Guild(PartialGuild): # pylint:disable=too-many-instance-attributes `None`. """ - is_large: typing.Optional[bool] = marshaller.attrib( - raw_name="large", deserializer=bool, if_undefined=None, default=None, eq=False, hash=False - ) + is_large: typing.Optional[bool] = attr.ib(eq=False, hash=False) """Whether the guild is considered to be large or not. This information is only available if the guild was sent via a `GUILD_CREATE` @@ -1287,9 +1045,7 @@ class Guild(PartialGuild): # pylint:disable=too-many-instance-attributes sent about members who are offline or invisible. """ - member_count: typing.Optional[int] = marshaller.attrib( - deserializer=int, if_undefined=None, default=None, eq=False, hash=False - ) + member_count: typing.Optional[int] = attr.ib(eq=False, hash=False) """The number of members in this guild. This information is only available if the guild was sent via a `GUILD_CREATE` @@ -1297,9 +1053,7 @@ class Guild(PartialGuild): # pylint:disable=too-many-instance-attributes `None`. """ - members: typing.Optional[typing.Mapping[bases.Snowflake, GuildMember]] = marshaller.attrib( - deserializer=_deserialize_members, if_undefined=None, inherit_kwargs=True, default=None, eq=False, hash=False - ) + members: typing.Optional[typing.Mapping[bases.Snowflake, GuildMember]] = attr.ib(eq=False, hash=False) """A mapping of ID to the corresponding guild members in this guild. This information is only available if the guild was sent via a `GUILD_CREATE` @@ -1318,8 +1072,8 @@ class Guild(PartialGuild): # pylint:disable=too-many-instance-attributes query the members using the appropriate API call instead. """ - channels: typing.Optional[typing.Mapping[bases.Snowflake, channels_.GuildChannel]] = marshaller.attrib( - deserializer=_deserialize_channels, if_undefined=None, inherit_kwargs=True, default=None, eq=False, hash=False, + channels: typing.Optional[typing.Mapping[bases.Snowflake, channels_.GuildChannel]] = attr.ib( + eq=False, hash=False, ) """A mapping of ID to the corresponding guild channels in this guild. @@ -1337,8 +1091,8 @@ class Guild(PartialGuild): # pylint:disable=too-many-instance-attributes appropriate API call to retrieve this information. """ - presences: typing.Optional[typing.Mapping[bases.Snowflake, GuildMemberPresence]] = marshaller.attrib( - deserializer=_deserialize_presences, if_undefined=None, inherit_kwargs=True, default=None, eq=False, hash=False, + presences: typing.Optional[typing.Mapping[bases.Snowflake, GuildMemberPresence]] = attr.ib( + eq=False, hash=False, ) """A mapping of member ID to the corresponding presence information for the given member, if available. @@ -1357,74 +1111,62 @@ class Guild(PartialGuild): # pylint:disable=too-many-instance-attributes appropriate API call to retrieve this information. """ - max_presences: typing.Optional[int] = marshaller.attrib( - deserializer=int, if_undefined=None, if_none=None, default=None, eq=False, hash=False - ) + max_presences: typing.Optional[int] = attr.ib(eq=False, hash=False) """The maximum number of presences for the guild. If this is `None`, then the default value is used (currently 25000). """ - max_members: typing.Optional[int] = marshaller.attrib( - deserializer=int, if_undefined=None, default=None, eq=False, hash=False - ) + max_members: typing.Optional[int] = attr.ib(eq=False, hash=False) """The maximum number of members allowed in this guild. This information may not be present, in which case, it will be `None`. """ - max_video_channel_users: typing.Optional[int] = marshaller.attrib( - deserializer=int, if_undefined=None, default=None, eq=False, hash=False - ) + max_video_channel_users: typing.Optional[int] = attr.ib(eq=False, hash=False) """The maximum number of users allowed in a video channel together. This information may not be present, in which case, it will be `None`. """ - vanity_url_code: typing.Optional[str] = marshaller.attrib(deserializer=str, if_none=None, eq=False, hash=False) + vanity_url_code: typing.Optional[str] = attr.ib(eq=False, hash=False) """The vanity URL code for the guild's vanity URL. This is only present if `GuildFeature.VANITY_URL` is in `Guild.features` for this guild. If not, this will always be `None`. """ - description: typing.Optional[str] = marshaller.attrib(if_none=None, deserializer=str, eq=False, hash=False) + description: typing.Optional[str] = attr.ib(eq=False, hash=False) """The guild's description. This is only present if certain `GuildFeature`'s are set in `Guild.features` for this guild. Otherwise, this will always be `None`. """ - banner_hash: typing.Optional[str] = marshaller.attrib( - raw_name="banner", if_none=None, deserializer=str, eq=False, hash=False - ) + banner_hash: typing.Optional[str] = attr.ib(eq=False, hash=False) """The hash for the guild's banner. This is only present if the guild has `GuildFeature.BANNER` in `Guild.features` for this guild. For all other purposes, it is `None`. """ - premium_tier: GuildPremiumTier = marshaller.attrib(deserializer=GuildPremiumTier, eq=False, hash=False) + premium_tier: GuildPremiumTier = attr.ib(eq=False, hash=False) """The premium tier for this guild.""" - premium_subscription_count: typing.Optional[int] = marshaller.attrib( - deserializer=int, if_undefined=None, if_none=None, default=None, eq=False, hash=False - ) + premium_subscription_count: typing.Optional[int] = attr.ib(eq=False, hash=False) """The number of nitro boosts that the server currently has. This information may not be present, in which case, it will be `None`. """ - preferred_locale: str = marshaller.attrib(deserializer=str, eq=False, hash=False) + preferred_locale: str = attr.ib(eq=False, hash=False) """The preferred locale to use for this guild. This can only be change if `GuildFeature.PUBLIC` is in `Guild.features` for this guild and will otherwise default to `en-US`. """ - public_updates_channel_id: typing.Optional[bases.Snowflake] = marshaller.attrib( - if_none=None, deserializer=bases.Snowflake, eq=False, hash=False - ) + public_updates_channel_id: typing.Optional[bases.Snowflake] = attr.ib(eq=False, hash=False) """The channel ID of the channel where admins and moderators receive notices from Discord. @@ -1433,9 +1175,7 @@ class Guild(PartialGuild): # pylint:disable=too-many-instance-attributes """ # TODO: if this is `None`, then should we attempt to look at the known member count if present? - approximate_member_count: typing.Optional[int] = marshaller.attrib( - if_undefined=None, deserializer=int, default=None, eq=False, hash=False - ) + approximate_member_count: typing.Optional[int] = attr.ib(eq=False, hash=False) """The approximate number of members in the guild. This information will be provided by RESTSession API calls fetching the guilds that @@ -1443,9 +1183,7 @@ class Guild(PartialGuild): # pylint:disable=too-many-instance-attributes remain `None`. """ - approximate_active_member_count: typing.Optional[int] = marshaller.attrib( - raw_name="approximate_presence_count", if_undefined=None, deserializer=int, default=None, eq=False, hash=False - ) + approximate_active_member_count: typing.Optional[int] = attr.ib(eq=False, hash=False) """The approximate number of members in the guild that are not offline. This information will be provided by RESTSession API calls fetching the guilds that diff --git a/hikari/models/intents.py b/hikari/models/intents.py index 492788f910..e1067c3fd5 100644 --- a/hikari/models/intents.py +++ b/hikari/models/intents.py @@ -22,11 +22,11 @@ __all__ = ["Intent"] -from hikari.internal import more_enums +import enum -@more_enums.must_be_unique -class Intent(more_enums.IntFlag): +@enum.unique +class Intent(enum.IntFlag): """Represents an intent on the gateway. This is a bitfield representation of all the categories of event diff --git a/hikari/models/invites.py b/hikari/models/invites.py index 73fac13068..cd3db67a4a 100644 --- a/hikari/models/invites.py +++ b/hikari/models/invites.py @@ -22,75 +22,66 @@ __all__ = ["TargetUserType", "VanityURL", "InviteGuild", "Invite", "InviteWithMetadata"] -import datetime +import enum import typing import attr -from hikari.internal import conversions -from hikari.internal import marshaller -from hikari.internal import more_enums from . import bases -from . import channels from . import guilds -from . import users from ..net import urls +if typing.TYPE_CHECKING: + import datetime -@more_enums.must_be_unique -class TargetUserType(int, more_enums.Enum): + from . import channels + from . import users + + +@enum.unique +class TargetUserType(int, enum.Enum): """The reason a invite targets a user.""" STREAM = 1 """This invite is targeting a "Go Live" stream.""" -@marshaller.marshallable() -@attr.s(eq=True, hash=True, kw_only=True, slots=True) -class VanityURL(bases.Entity, marshaller.Deserializable): +@attr.s(eq=True, hash=True, init=False, kw_only=True, slots=True) +class VanityURL(bases.Entity): """A special case invite object, that represents a guild's vanity url.""" - code: str = marshaller.attrib(deserializer=str, eq=True, hash=True, repr=True) + code: str = attr.ib(eq=True, hash=True, repr=True) """The code for this invite.""" - uses: int = marshaller.attrib(deserializer=int, eq=False, hash=False, repr=True) + uses: int = attr.ib(eq=False, hash=False, repr=True) """The amount of times this invite has been used.""" -@marshaller.marshallable() -@attr.s(eq=True, hash=True, kw_only=True, slots=True) +@attr.s(eq=True, hash=True, init=False, kw_only=True, slots=True) class InviteGuild(guilds.PartialGuild): """Represents the partial data of a guild that'll be attached to invites.""" - splash_hash: typing.Optional[str] = marshaller.attrib( - raw_name="splash", deserializer=str, if_none=None, eq=False, hash=False - ) + splash_hash: typing.Optional[str] = attr.ib(eq=False, hash=False) """The hash of the splash for the guild, if there is one.""" - banner_hash: typing.Optional[str] = marshaller.attrib( - raw_name="banner", deserializer=str, if_none=None, eq=False, hash=False - ) + banner_hash: typing.Optional[str] = attr.ib(eq=False, hash=False) """The hash for the guild's banner. This is only present if `hikari.models.guilds.GuildFeature.BANNER` is in the `features` for this guild. For all other purposes, it is `None`. """ - description: typing.Optional[str] = marshaller.attrib(deserializer=str, if_none=None, eq=False, hash=False) + description: typing.Optional[str] = attr.ib(eq=False, hash=False) """The guild's description. This is only present if certain `features` are set in this guild. Otherwise, this will always be `None`. For all other purposes, it is `None`. """ - verification_level: guilds.GuildVerificationLevel = marshaller.attrib( - deserializer=guilds.GuildVerificationLevel, eq=False, hash=False - ) + verification_level: guilds.GuildVerificationLevel = attr.ib(eq=False, hash=False) """The verification level required for a user to participate in this guild.""" - vanity_url_code: typing.Optional[str] = marshaller.attrib( - if_none=None, deserializer=str, eq=False, hash=False, repr=True - ) + vanity_url_code: typing.Optional[str] = attr.ib(eq=False, hash=False, repr=True) """The vanity URL code for the guild's vanity URL. This is only present if `hikari.models.guilds.GuildFeature.VANITY_URL` is in the @@ -160,50 +151,43 @@ def banner_url(self) -> typing.Optional[str]: return self.format_banner_url() -@marshaller.marshallable() -@attr.s(eq=True, hash=True, kw_only=True, slots=True) -class Invite(bases.Entity, marshaller.Deserializable): +@attr.s(eq=True, hash=True, init=False, kw_only=True, slots=True) +class Invite(bases.Entity): """Represents an invite that's used to add users to a guild or group dm.""" - code: str = marshaller.attrib(deserializer=str, eq=True, hash=True, repr=True) + code: str = attr.ib(eq=True, hash=True, repr=True) """The code for this invite.""" - guild: typing.Optional[InviteGuild] = marshaller.attrib( - deserializer=InviteGuild.deserialize, - if_undefined=None, - inherit_kwargs=True, - default=None, - eq=False, - hash=False, - repr=True, + guild: typing.Optional[InviteGuild] = attr.ib( + eq=False, hash=False, repr=True, ) """The partial object of the guild this dm belongs to. Will be `None` for group dm invites. """ - channel: channels.PartialChannel = marshaller.attrib( - deserializer=channels.PartialChannel.deserialize, inherit_kwargs=True, eq=False, hash=False, repr=True, + channel: channels.PartialChannel = attr.ib( + eq=False, hash=False, repr=True, ) """The partial object of the channel this invite targets.""" - inviter: typing.Optional[users.User] = marshaller.attrib( - deserializer=users.User.deserialize, if_undefined=None, inherit_kwargs=True, default=None, eq=False, hash=False, + inviter: typing.Optional[users.User] = attr.ib( + eq=False, hash=False, ) """The object of the user who created this invite.""" - target_user: typing.Optional[users.User] = marshaller.attrib( - deserializer=users.User.deserialize, if_undefined=None, inherit_kwargs=True, default=None, eq=False, hash=False, + target_user: typing.Optional[users.User] = attr.ib( + eq=False, hash=False, ) """The object of the user who this invite targets, if set.""" - target_user_type: typing.Optional[TargetUserType] = marshaller.attrib( - deserializer=TargetUserType, if_undefined=None, default=None, eq=False, hash=False, + target_user_type: typing.Optional[TargetUserType] = attr.ib( + eq=False, hash=False, ) """The type of user target this invite is, if applicable.""" - approximate_presence_count: typing.Optional[int] = marshaller.attrib( - deserializer=int, if_undefined=None, default=None, eq=False, hash=False, + approximate_presence_count: typing.Optional[int] = attr.ib( + eq=False, hash=False, ) """The approximate amount of presences in this invite's guild. @@ -211,8 +195,8 @@ class Invite(bases.Entity, marshaller.Deserializable): Invites endpoint. """ - approximate_member_count: typing.Optional[int] = marshaller.attrib( - deserializer=int, if_undefined=None, default=None, eq=False, hash=False, + approximate_member_count: typing.Optional[int] = attr.ib( + eq=False, hash=False, ) """The approximate amount of members in this invite's guild. @@ -221,12 +205,7 @@ class Invite(bases.Entity, marshaller.Deserializable): """ -def _max_age_deserializer(age: int) -> datetime.timedelta: - return datetime.timedelta(seconds=age) if age > 0 else None - - -@marshaller.marshallable() -@attr.s(eq=True, hash=True, kw_only=True, slots=True) +@attr.s(eq=True, hash=True, init=False, kw_only=True, slots=True) class InviteWithMetadata(Invite): """Extends the base `Invite` object with metadata. @@ -234,29 +213,25 @@ class InviteWithMetadata(Invite): guild permissions, rather than it's code. """ - uses: int = marshaller.attrib(deserializer=int, eq=False, hash=False, repr=True) + uses: int = attr.ib(eq=False, hash=False, repr=True) """The amount of times this invite has been used.""" - max_uses: int = marshaller.attrib(deserializer=int, eq=False, hash=False, repr=True) + max_uses: int = attr.attrib(eq=False, hash=False, repr=True) """The limit for how many times this invite can be used before it expires. If set to `0` then this is unlimited. """ - max_age: typing.Optional[datetime.timedelta] = marshaller.attrib( - deserializer=_max_age_deserializer, eq=False, hash=False - ) + max_age: typing.Optional[datetime.timedelta] = attr.attrib(eq=False, hash=False) """The timedelta of how long this invite will be valid for. If set to `None` then this is unlimited. """ - is_temporary: bool = marshaller.attrib(raw_name="temporary", deserializer=bool, eq=False, hash=False, repr=True) + is_temporary: bool = attr.attrib(eq=False, hash=False, repr=True) """Whether this invite grants temporary membership.""" - created_at: datetime.datetime = marshaller.attrib( - deserializer=conversions.iso8601_datetime_string_to_datetime, eq=False, hash=False - ) + created_at: datetime.datetime = attr.attrib(eq=False, hash=False) """When this invite was created.""" @property diff --git a/hikari/models/messages.py b/hikari/models/messages.py index a872dc34d2..2dbbe77800 100644 --- a/hikari/models/messages.py +++ b/hikari/models/messages.py @@ -31,30 +31,26 @@ "Message", ] +import enum import typing import attr -from hikari.internal import conversions -from hikari.internal import marshaller -from hikari.internal import more_enums - -from . import applications from . import bases -from . import channels -from . import embeds as embeds_ -from . import emojis as emojis_ from . import files as files_ -from . import guilds -from . import users if typing.TYPE_CHECKING: import datetime - from hikari.internal import more_typing + from . import applications + from . import channels + from . import embeds as embeds_ + from . import emojis as emojis_ + from . import guilds + from . import users -@more_enums.must_be_unique -class MessageType(int, more_enums.Enum): +@enum.unique +class MessageType(int, enum.Enum): """The type of a message.""" DEFAULT = 0 @@ -97,8 +93,8 @@ class MessageType(int, more_enums.Enum): """Channel follow add.""" -@more_enums.must_be_unique -class MessageFlag(more_enums.IntFlag): +@enum.unique +class MessageFlag(enum.IntFlag): """Additional flags for message options.""" NONE = 0 @@ -120,8 +116,8 @@ class MessageFlag(more_enums.IntFlag): """This message came from the urgent message system.""" -@more_enums.must_be_unique -class MessageActivityType(int, more_enums.Enum): +@enum.unique +class MessageActivityType(int, enum.Enum): """The type of a rich presence message activity.""" NONE = 0 @@ -140,9 +136,8 @@ class MessageActivityType(int, more_enums.Enum): """Request to join an activity.""" -@marshaller.marshallable() -@attr.s(eq=True, hash=False, kw_only=True, slots=True) -class Attachment(bases.Unique, files_.BaseStream, marshaller.Deserializable): +@attr.s(eq=True, hash=False, init=False, kw_only=True, slots=True) +class Attachment(bases.Unique, files_.BaseStream): """Represents a file attached to a message. You can use this object in the same way as a @@ -150,65 +145,58 @@ class Attachment(bases.Unique, files_.BaseStream, marshaller.Deserializable): message, etc. """ - filename: str = marshaller.attrib(deserializer=str, repr=True) + filename: str = attr.ib(repr=True) """The name of the file.""" - size: int = marshaller.attrib(deserializer=int, repr=True) + size: int = attr.ib(repr=True) """The size of the file in bytes.""" - url: str = marshaller.attrib(deserializer=str, repr=True) + url: str = attr.ib(repr=True) """The source URL of file.""" - proxy_url: str = marshaller.attrib(deserializer=str) + proxy_url: str = attr.ib() """The proxied URL of file.""" - height: typing.Optional[int] = marshaller.attrib(deserializer=int, if_undefined=None, default=None) + height: typing.Optional[int] = attr.ib() """The height of the image (if the file is an image).""" - width: typing.Optional[int] = marshaller.attrib(deserializer=int, if_undefined=None, default=None) + width: typing.Optional[int] = attr.ib() """The width of the image (if the file is an image).""" def __aiter__(self) -> typing.AsyncGenerator[bytes]: return files_.WebResourceStream(self.filename, self.url).__aiter__() -@marshaller.marshallable() -@attr.s(eq=True, hash=True, kw_only=True, slots=True) -class Reaction(bases.Entity, marshaller.Deserializable): +@attr.s(eq=True, hash=True, init=False, kw_only=True, slots=True) +class Reaction: """Represents a reaction in a message.""" - count: int = marshaller.attrib(deserializer=int, eq=False, hash=False, repr=True) + count: int = attr.ib(eq=False, hash=False, repr=True) """The amount of times the emoji has been used to react.""" - emoji: typing.Union[emojis_.UnicodeEmoji, emojis_.CustomEmoji] = marshaller.attrib( - deserializer=emojis_.deserialize_reaction_emoji, inherit_kwargs=True, eq=True, hash=True, repr=True - ) + emoji: typing.Union[emojis_.UnicodeEmoji, emojis_.CustomEmoji] = attr.ib(eq=True, hash=True, repr=True) """The emoji used to react.""" - is_reacted_by_me: bool = marshaller.attrib(raw_name="me", deserializer=bool, eq=False, hash=False) + is_reacted_by_me: bool = attr.ib(eq=False, hash=False) """Whether the current user reacted using this emoji.""" -@marshaller.marshallable() -@attr.s(eq=True, hash=False, kw_only=True, slots=True) -class MessageActivity(bases.Entity, marshaller.Deserializable): +@attr.s(eq=True, hash=False, init=False, kw_only=True, slots=True) +class MessageActivity: """Represents the activity of a rich presence-enabled message.""" - type: MessageActivityType = marshaller.attrib(deserializer=MessageActivityType, repr=True) + type: MessageActivityType = attr.ib(repr=True) """The type of message activity.""" - party_id: typing.Optional[str] = marshaller.attrib(deserializer=str, if_undefined=None, default=None, repr=True) + party_id: typing.Optional[str] = attr.ib(repr=True) """The party ID of the message activity.""" -@marshaller.marshallable() -@attr.s(eq=True, hash=False, kw_only=True, slots=True) -class MessageCrosspost(bases.Unique, marshaller.Deserializable): +@attr.s(eq=True, hash=False, init=False, kw_only=True, slots=True) +class MessageCrosspost(bases.Entity, bases.Unique): """Represents information about a cross-posted message and the origin of the original message.""" - id: typing.Optional[bases.Snowflake] = marshaller.attrib( - raw_name="message_id", deserializer=bases.Snowflake, if_undefined=None, default=None, repr=True - ) + id: typing.Optional[bases.Snowflake] = attr.ib(repr=True) """The ID of the message. !!! warning @@ -217,12 +205,10 @@ class MessageCrosspost(bases.Unique, marshaller.Deserializable): currently documented. """ - channel_id: bases.Snowflake = marshaller.attrib(deserializer=bases.Snowflake, repr=True) + channel_id: bases.Snowflake = attr.ib(repr=True) """The ID of the channel that the message originated from.""" - guild_id: typing.Optional[bases.Snowflake] = marshaller.attrib( - deserializer=bases.Snowflake, if_undefined=None, default=None, repr=True - ) + guild_id: typing.Optional[bases.Snowflake] = attr.ib(repr=True) """The ID of the guild that the message originated from. !!! warning @@ -232,163 +218,98 @@ class MessageCrosspost(bases.Unique, marshaller.Deserializable): """ -def _deserialize_object_mentions(payload: more_typing.JSONArray) -> typing.Set[bases.Snowflake]: - return {bases.Snowflake(mention["id"]) for mention in payload} - - -def _deserialize_mentions(payload: more_typing.JSONArray) -> typing.Set[bases.Snowflake]: - return {bases.Snowflake(mention) for mention in payload} - - -def _deserialize_attachments(payload: more_typing.JSONArray, **kwargs: typing.Any) -> typing.Sequence[Attachment]: - return [Attachment.deserialize(attachment, **kwargs) for attachment in payload] - - -def _deserialize_embeds(payload: more_typing.JSONArray, **kwargs: typing.Any) -> typing.Sequence[embeds_.Embed]: - return [embeds_.Embed.deserialize(embed, **kwargs) for embed in payload] - - -def _deserialize_reactions(payload: more_typing.JSONArray, **kwargs: typing.Any) -> typing.Sequence[Reaction]: - return [Reaction.deserialize(reaction, **kwargs) for reaction in payload] - - -@marshaller.marshallable() -@attr.s(eq=True, hash=True, kw_only=True, slots=True) -class Message(bases.Unique, marshaller.Deserializable): +@attr.s(eq=True, hash=True, init=False, kw_only=True, slots=True) +class Message(bases.Entity, bases.Unique): """Represents a message.""" - channel_id: bases.Snowflake = marshaller.attrib(deserializer=bases.Snowflake, eq=False, hash=False, repr=True) + channel_id: bases.Snowflake = attr.ib(eq=False, hash=False, repr=True) """The ID of the channel that the message was sent in.""" - guild_id: typing.Optional[bases.Snowflake] = marshaller.attrib( - deserializer=bases.Snowflake, if_undefined=None, default=None, eq=False, hash=False, repr=True - ) + guild_id: typing.Optional[bases.Snowflake] = attr.ib(eq=False, hash=False, repr=True) """The ID of the guild that the message was sent in.""" - author: users.User = marshaller.attrib( - deserializer=users.User.deserialize, inherit_kwargs=True, eq=False, hash=False, repr=True - ) + author: users.User = attr.ib(eq=False, hash=False, repr=True) """The author of this message.""" - member: typing.Optional[guilds.GuildMember] = marshaller.attrib( - deserializer=guilds.GuildMember.deserialize, - if_undefined=None, - default=None, - inherit_kwargs=True, - eq=False, - hash=False, - repr=True, + member: typing.Optional[guilds.GuildMember] = attr.ib( + eq=False, hash=False, repr=True, ) """The member properties for the message's author.""" - content: str = marshaller.attrib(deserializer=str, eq=False, hash=False) + content: str = attr.ib(eq=False, hash=False) """The content of the message.""" - timestamp: datetime.datetime = marshaller.attrib( - deserializer=conversions.iso8601_datetime_string_to_datetime, eq=False, hash=False, repr=True - ) + timestamp: datetime.datetime = attr.ib(eq=False, hash=False, repr=True) """The timestamp that the message was sent at.""" - edited_timestamp: typing.Optional[datetime.datetime] = marshaller.attrib( - deserializer=conversions.iso8601_datetime_string_to_datetime, if_none=None, eq=False, hash=False - ) + edited_timestamp: typing.Optional[datetime.datetime] = attr.ib(eq=False, hash=False) """The timestamp that the message was last edited at. Will be `None` if it wasn't ever edited. """ - is_tts: bool = marshaller.attrib(raw_name="tts", deserializer=bool, eq=False, hash=False) + is_tts: bool = attr.ib(eq=False, hash=False) """Whether the message is a TTS message.""" - is_mentioning_everyone: bool = marshaller.attrib( - raw_name="mention_everyone", deserializer=bool, eq=False, hash=False - ) + is_mentioning_everyone: bool = attr.ib(eq=False, hash=False) """Whether the message mentions `@everyone` or `@here`.""" - user_mentions: typing.Set[bases.Snowflake] = marshaller.attrib( - raw_name="mentions", deserializer=_deserialize_object_mentions, eq=False, hash=False, + user_mentions: typing.Set[bases.Snowflake] = attr.ib( + eq=False, hash=False, ) """The users the message mentions.""" - role_mentions: typing.Set[bases.Snowflake] = marshaller.attrib( - raw_name="mention_roles", deserializer=_deserialize_mentions, eq=False, hash=False, + role_mentions: typing.Set[bases.Snowflake] = attr.ib( + eq=False, hash=False, ) """The roles the message mentions.""" - channel_mentions: typing.Set[bases.Snowflake] = marshaller.attrib( - raw_name="mention_channels", - deserializer=_deserialize_object_mentions, - if_undefined=set, - eq=False, - hash=False, - factory=set, - ) + channel_mentions: typing.Set[bases.Snowflake] = attr.ib(eq=False, hash=False) """The channels the message mentions.""" - attachments: typing.Sequence[Attachment] = marshaller.attrib( - deserializer=_deserialize_attachments, inherit_kwargs=True, eq=False, hash=False, + attachments: typing.Sequence[Attachment] = attr.ib( + eq=False, hash=False, ) """The message attachments.""" - embeds: typing.Sequence[embeds_.Embed] = marshaller.attrib( - deserializer=_deserialize_embeds, inherit_kwargs=True, eq=False, hash=False - ) + embeds: typing.Sequence[embeds_.Embed] = attr.ib(eq=False, hash=False) """The message embeds.""" - reactions: typing.Sequence[Reaction] = marshaller.attrib( - deserializer=_deserialize_reactions, if_undefined=list, inherit_kwargs=True, eq=False, hash=False, factory=list, + reactions: typing.Sequence[Reaction] = attr.ib( + eq=False, hash=False, ) """The message reactions.""" - is_pinned: bool = marshaller.attrib(raw_name="pinned", deserializer=bool, eq=False, hash=False) + is_pinned: bool = attr.ib(eq=False, hash=False) """Whether the message is pinned.""" - webhook_id: typing.Optional[bases.Snowflake] = marshaller.attrib( - deserializer=bases.Snowflake, if_undefined=None, default=None, eq=False, hash=False, + webhook_id: typing.Optional[bases.Snowflake] = attr.ib( + eq=False, hash=False, ) """If the message was generated by a webhook, the webhook's id.""" - type: MessageType = marshaller.attrib(deserializer=MessageType, eq=False, hash=False) + type: MessageType = attr.ib(eq=False, hash=False) """The message type.""" - activity: typing.Optional[MessageActivity] = marshaller.attrib( - deserializer=MessageActivity.deserialize, - if_undefined=None, - inherit_kwargs=True, - default=None, - eq=False, - hash=False, + activity: typing.Optional[MessageActivity] = attr.ib( + eq=False, hash=False, ) """The message activity.""" - application: typing.Optional[applications.Application] = marshaller.attrib( - deserializer=applications.Application.deserialize, - if_undefined=None, - inherit_kwargs=True, - default=None, - eq=False, - hash=False, + application: typing.Optional[applications.Application] = attr.ib( + eq=False, hash=False, ) """The message application.""" - message_reference: typing.Optional[MessageCrosspost] = marshaller.attrib( - deserializer=MessageCrosspost.deserialize, - if_undefined=None, - inherit_kwargs=True, - default=None, - eq=False, - hash=False, + message_reference: typing.Optional[MessageCrosspost] = attr.ib( + eq=False, hash=False, ) """The message crossposted reference data.""" - flags: typing.Optional[MessageFlag] = marshaller.attrib( - deserializer=MessageFlag, if_undefined=None, default=None, eq=False, hash=False - ) + flags: typing.Optional[MessageFlag] = attr.ib(eq=False, hash=False) """The message flags.""" - nonce: typing.Optional[str] = marshaller.attrib( - deserializer=str, if_undefined=None, default=None, eq=False, hash=False - ) + nonce: typing.Optional[str] = attr.ib(eq=False, hash=False) """The message nonce. This is a string used for validating a message was sent.""" async def fetch_channel(self) -> channels.PartialChannel: diff --git a/hikari/models/permissions.py b/hikari/models/permissions.py index b5bf3c6a65..05d23ad3dd 100644 --- a/hikari/models/permissions.py +++ b/hikari/models/permissions.py @@ -22,11 +22,11 @@ __all__ = ["Permission"] -from hikari.internal import more_enums +import enum -@more_enums.must_be_unique -class Permission(more_enums.IntFlag): +@enum.unique +class Permission(enum.IntFlag): """Represents the permissions available in a given channel or guild. This is an int-flag enum. This means that you can **combine multiple diff --git a/hikari/models/users.py b/hikari/models/users.py index bc66f1dd74..3cf6400ae4 100644 --- a/hikari/models/users.py +++ b/hikari/models/users.py @@ -22,18 +22,17 @@ __all__ = ["User", "MyUser", "UserFlag", "PremiumType"] +import enum import typing import attr -from hikari.internal import marshaller -from hikari.internal import more_enums from . import bases from ..net import urls -@more_enums.must_be_unique -class UserFlag(more_enums.IntFlag): +@enum.unique +class UserFlag(enum.IntFlag): """The known user flags that represent account badges.""" NONE = 0 @@ -79,8 +78,8 @@ class UserFlag(more_enums.IntFlag): """Verified Bot Developer""" -@more_enums.must_be_unique -class PremiumType(int, more_enums.Enum): +@enum.unique +class PremiumType(int, enum.Enum): """The types of Nitro.""" NONE = 0 @@ -93,42 +92,27 @@ class PremiumType(int, more_enums.Enum): """Premium including all perks (e.g. 2 server boosts).""" -@marshaller.marshallable() -@attr.s( - eq=True, hash=True, kw_only=True, slots=True, -) -class User(bases.Unique, marshaller.Deserializable): +@attr.s(eq=True, hash=True, init=False, kw_only=True, slots=True) +class User(bases.Entity, bases.Unique): """Represents a user.""" - discriminator: str = marshaller.attrib(deserializer=str, eq=False, hash=False, repr=True) + discriminator: str = attr.ib(eq=False, hash=False, repr=True) """This user's discriminator.""" - username: str = marshaller.attrib(deserializer=str, eq=False, hash=False, repr=True) + username: str = attr.ib(eq=False, hash=False, repr=True) """This user's username.""" - avatar_hash: typing.Optional[str] = marshaller.attrib( - raw_name="avatar", deserializer=str, if_none=None, eq=False, hash=False - ) + avatar_hash: typing.Optional[str] = attr.ib(eq=False, hash=False) """This user's avatar hash, if set.""" - is_bot: bool = marshaller.attrib( - raw_name="bot", deserializer=bool, if_undefined=False, default=False, eq=False, hash=False - ) + is_bot: bool = attr.ib(eq=False, hash=False) """Whether this user is a bot account.""" - is_system: bool = marshaller.attrib( - raw_name="system", deserializer=bool, if_undefined=False, default=False, eq=False, hash=False - ) + is_system: bool = attr.ib(eq=False, hash=False) """Whether this user is a system account.""" - flags: typing.Optional[UserFlag] = marshaller.attrib( - raw_name="public_flags", deserializer=UserFlag, if_undefined=None, default=None, eq=False, hash=False - ) - """The public flags for this user. - - !!! info - This will be `None` if it's a webhook user. - """ + flags: UserFlag = attr.ib(eq=False, hash=False) + """The public flags for this user.""" async def fetch_self(self) -> User: """Get this user's up-to-date object. @@ -195,43 +179,34 @@ def default_avatar_url(self) -> str: return urls.generate_cdn_url("embed", "avatars", str(self.default_avatar_index), format_="png", size=None) -@marshaller.marshallable() -@attr.s(eq=True, hash=True, kw_only=True, slots=True) +@attr.s(eq=True, hash=True, init=False, kw_only=True, slots=True) class MyUser(User): """Represents a user with extended oauth2 information.""" - is_mfa_enabled: bool = marshaller.attrib(raw_name="mfa_enabled", deserializer=bool, eq=False, hash=False) + is_mfa_enabled: bool = attr.ib(eq=False, hash=False) """Whether the user's account has 2fa enabled.""" - locale: typing.Optional[str] = marshaller.attrib( - deserializer=str, if_none=None, if_undefined=None, default=None, eq=False, hash=False - ) + locale: typing.Optional[str] = attr.ib(eq=False, hash=False) """The user's set language. This is not provided by the `READY` event.""" - is_verified: typing.Optional[bool] = marshaller.attrib( - raw_name="verified", deserializer=bool, if_undefined=None, default=None, eq=False, hash=False - ) + is_verified: typing.Optional[bool] = attr.ib(eq=False, hash=False) """Whether the email for this user's account has been verified. Will be `None` if retrieved through the oauth2 flow without the `email` scope. """ - email: typing.Optional[str] = marshaller.attrib( - deserializer=str, if_undefined=None, if_none=None, default=None, eq=False, hash=False - ) + email: typing.Optional[str] = attr.ib(eq=False, hash=False) """The user's set email. Will be `None` if retrieved through the oauth2 flow without the `email` scope and for bot users. """ - flags: UserFlag = marshaller.attrib(deserializer=UserFlag, eq=False, hash=False) + flags: UserFlag = attr.ib(eq=False, hash=False) """This user account's flags.""" - premium_type: typing.Optional[PremiumType] = marshaller.attrib( - deserializer=PremiumType, if_undefined=None, default=None, eq=False, hash=False - ) + premium_type: typing.Optional[PremiumType] = attr.ib(eq=False, hash=False) """The type of Nitro Subscription this user account had. This will always be `None` for bots. diff --git a/hikari/models/voices.py b/hikari/models/voices.py index e5566889db..3e8f741fe4 100644 --- a/hikari/models/voices.py +++ b/hikari/models/voices.py @@ -25,68 +25,61 @@ import typing import attr -from hikari.internal import marshaller from . import bases -from . import guilds +if typing.TYPE_CHECKING: + from . import guilds -@marshaller.marshallable() -@attr.s(eq=True, hash=True, kw_only=True, slots=True) -class VoiceState(bases.Entity, marshaller.Deserializable): + +@attr.s(eq=True, hash=True, init=False, kw_only=True, slots=True) +class VoiceState(bases.Entity): """Represents a user's voice connection status.""" - guild_id: typing.Optional[bases.Snowflake] = marshaller.attrib( - deserializer=bases.Snowflake, if_undefined=None, default=None, eq=False, hash=False, repr=True - ) + guild_id: typing.Optional[bases.Snowflake] = attr.ib(eq=False, hash=False, repr=True) """The ID of the guild this voice state is in, if applicable.""" - channel_id: typing.Optional[bases.Snowflake] = marshaller.attrib( - deserializer=bases.Snowflake, if_none=None, eq=False, hash=False, repr=True - ) + channel_id: typing.Optional[bases.Snowflake] = attr.ib(eq=False, hash=False, repr=True) """The ID of the channel this user is connected to. This will be `None` if they are leaving voice. """ - user_id: bases.Snowflake = marshaller.attrib(deserializer=bases.Snowflake, eq=False, hash=False, repr=True) + user_id: bases.Snowflake = attr.ib(eq=False, hash=False, repr=True) """The ID of the user this voice state is for.""" - member: typing.Optional[guilds.GuildMember] = marshaller.attrib( - deserializer=guilds.GuildMember.deserialize, if_undefined=None, default=None, eq=False, hash=False, + member: typing.Optional[guilds.GuildMember] = attr.ib( + eq=False, hash=False, ) """The guild member this voice state is for if the voice state is in a guild.""" - session_id: str = marshaller.attrib(deserializer=str, eq=True, hash=True, repr=True) + session_id: str = attr.ib(eq=True, hash=True, repr=True) """The string ID of this voice state's session.""" - is_guild_deafened: bool = marshaller.attrib(raw_name="deaf", deserializer=bool, eq=False, hash=False) + is_guild_deafened: bool = attr.ib(eq=False, hash=False) """Whether this user is deafened by the guild.""" - is_guild_muted: bool = marshaller.attrib(raw_name="mute", deserializer=bool, eq=False, hash=False) + is_guild_muted: bool = attr.ib(eq=False, hash=False) """Whether this user is muted by the guild.""" - is_self_deafened: bool = marshaller.attrib(raw_name="self_deaf", deserializer=bool, eq=False, hash=False) + is_self_deafened: bool = attr.ib(eq=False, hash=False) """Whether this user is deafened by their client.""" - is_self_muted: bool = marshaller.attrib(raw_name="self_mute", deserializer=bool, eq=False, hash=False) + is_self_muted: bool = attr.ib(eq=False, hash=False) """Whether this user is muted by their client.""" - is_streaming: bool = marshaller.attrib( - raw_name="self_stream", deserializer=bool, if_undefined=False, default=False, eq=False, hash=False - ) + is_streaming: bool = attr.ib(eq=False, hash=False) """Whether this user is streaming using "Go Live".""" - is_suppressed: bool = marshaller.attrib(raw_name="suppress", deserializer=bool, eq=False, hash=False) + is_suppressed: bool = attr.ib(eq=False, hash=False) """Whether this user is muted by the current user.""" -@marshaller.marshallable() -@attr.s(eq=True, hash=True, kw_only=True, slots=True) -class VoiceRegion(bases.Entity, marshaller.Deserializable): +@attr.s(eq=True, hash=True, init=False, kw_only=True, slots=True) +class VoiceRegion: """Represents a voice region server.""" - id: str = marshaller.attrib(deserializer=str, eq=True, hash=True, repr=True) + id: str = attr.ib(eq=True, hash=True, repr=True) """The string ID of this region. !!! note @@ -94,19 +87,19 @@ class VoiceRegion(bases.Entity, marshaller.Deserializable): This is intentional. """ - name: str = marshaller.attrib(deserializer=str, eq=False, hash=False, repr=True) + name: str = attr.ib(eq=False, hash=False, repr=True) """The name of this region.""" - is_vip: bool = marshaller.attrib(raw_name="vip", deserializer=bool, eq=False, hash=False) + is_vip: bool = attr.ib(eq=False, hash=False) """Whether this region is vip-only.""" - is_optimal_location: bool = marshaller.attrib(raw_name="optimal", deserializer=bool, eq=False, hash=False) + is_optimal_location: bool = attr.ib(eq=False, hash=False) """Whether this region's server is closest to the current user's client.""" - is_deprecated: bool = marshaller.attrib(raw_name="deprecated", deserializer=bool, eq=False, hash=False) + is_deprecated: bool = attr.ib(eq=False, hash=False) """Whether this region is deprecated.""" - is_custom: bool = marshaller.attrib(raw_name="custom", deserializer=bool, eq=False, hash=False) + is_custom: bool = attr.ib(eq=False, hash=False) """Whether this region is custom (e.g. used for events).""" def __str__(self) -> str: diff --git a/hikari/models/webhooks.py b/hikari/models/webhooks.py index d3fed6ef0a..1f61cef12d 100644 --- a/hikari/models/webhooks.py +++ b/hikari/models/webhooks.py @@ -22,14 +22,12 @@ __all__ = ["WebhookType", "Webhook"] +import enum import typing import attr -from hikari.internal import marshaller -from hikari.internal import more_enums from . import bases -from . import users as users_ from ..net import urls if typing.TYPE_CHECKING: @@ -38,10 +36,11 @@ from . import files as files_ from . import guilds as guilds_ from . import messages as messages_ + from . import users as users_ -@more_enums.must_be_unique -class WebhookType(int, more_enums.Enum): +@enum.unique +class WebhookType(int, enum.Enum): """Types of webhook.""" INCOMING = 1 @@ -51,9 +50,8 @@ class WebhookType(int, more_enums.Enum): """Channel Follower webhook.""" -@marshaller.marshallable() -@attr.s(eq=True, hash=True, kw_only=True, slots=True) -class Webhook(bases.Unique, marshaller.Deserializable): +@attr.s(eq=True, hash=True, init=False, kw_only=True, slots=True) +class Webhook(bases.Entity, bases.Unique): """Represents a webhook object on Discord. This is an endpoint that can have messages sent to it using standard @@ -61,26 +59,17 @@ class Webhook(bases.Unique, marshaller.Deserializable): send informational messages to specific channels. """ - type: WebhookType = marshaller.attrib(deserializer=WebhookType, eq=False, hash=False, repr=True) + type: WebhookType = attr.ib(eq=False, hash=False, repr=True) """The type of the webhook.""" - guild_id: typing.Optional[bases.Snowflake] = marshaller.attrib( - deserializer=bases.Snowflake, if_undefined=None, default=None, eq=False, hash=False, repr=True - ) + guild_id: typing.Optional[bases.Snowflake] = attr.ib(eq=False, hash=False, repr=True) """The guild ID of the webhook.""" - channel_id: bases.Snowflake = marshaller.attrib(deserializer=bases.Snowflake, eq=False, hash=False, repr=True) + channel_id: bases.Snowflake = attr.ib(eq=False, hash=False, repr=True) """The channel ID this webhook is for.""" - author: typing.Optional[users_.User] = marshaller.attrib( - raw_name="user", - deserializer=users_.User.deserialize, - if_undefined=None, - inherit_kwargs=True, - default=None, - eq=False, - hash=False, - repr=True, + author: typing.Optional[users_.User] = attr.ib( + eq=False, hash=False, repr=True, ) """The user that created the webhook @@ -89,17 +78,13 @@ class Webhook(bases.Unique, marshaller.Deserializable): than the webhook's token. """ - name: typing.Optional[str] = marshaller.attrib(deserializer=str, if_none=None, eq=False, hash=False, repr=True) + name: typing.Optional[str] = attr.ib(eq=False, hash=False, repr=True) """The name of the webhook.""" - avatar_hash: typing.Optional[str] = marshaller.attrib( - raw_name="avatar", deserializer=str, if_none=None, eq=False, hash=False - ) + avatar_hash: typing.Optional[str] = attr.ib(eq=False, hash=False) """The avatar hash of the webhook.""" - token: typing.Optional[str] = marshaller.attrib( - deserializer=str, if_undefined=None, default=None, eq=False, hash=False - ) + token: typing.Optional[str] = attr.ib(eq=False, hash=False) """The token for the webhook. !!! info diff --git a/hikari/net/rest.py b/hikari/net/rest.py index 80e763d431..ef9cb9642f 100644 --- a/hikari/net/rest.py +++ b/hikari/net/rest.py @@ -305,24 +305,24 @@ def _generate_allowed_mentions( if user_mentions is True: parsed_mentions.append("users") - # This covers both `False` and an array of IDs/objs by using `user_mentions or EMPTY_SEQUENCE`, where a + # This covers both `False` and an array of IDs/objs by using `user_mentions or a empty sequence`, where a # resultant empty list will mean that all user mentions are blacklisted. else: allowed_mentions["users"] = list( # dict.fromkeys is used to remove duplicate entries that would cause discord to return an error. - dict.fromkeys(str(int(m)) for m in user_mentions) + dict.fromkeys(str(int(m)) for m in user_mentions or ()) ) if len(allowed_mentions["users"]) > 100: raise ValueError("Only up to 100 users can be provided.") if role_mentions is True: parsed_mentions.append("roles") - # This covers both `False` and an array of IDs/objs by using `user_mentions or EMPTY_SEQUENCE`, where a + # This covers both `False` and an array of IDs/objs by using `user_mentions or a empty sequence`, where a # resultant empty list will mean that all role mentions are blacklisted. else: allowed_mentions["roles"] = list( # dict.fromkeys is used to remove duplicate entries that would cause discord to return an error. - dict.fromkeys(str(int(m)) for m in role_mentions) + dict.fromkeys(str(int(m)) for m in role_mentions or ()) ) if len(allowed_mentions["roles"]) > 100: raise ValueError("Only up to 100 roles can be provided.") @@ -2026,15 +2026,15 @@ async def fetch_guild_invites( async def fetch_integrations( self, guild: typing.Union[guilds.Guild, bases.UniqueObject], / - ) -> typing.Sequence[guilds.GuildIntegration]: + ) -> typing.Sequence[guilds.Integration]: route = routes.GET_GUILD_INTEGRATIONS.compile(guild=guild) response = await self._request(route) - return binding.cast_json_array(response, self._app.entity_factory.deserialize_guild_integration) + return binding.cast_json_array(response, self._app.entity_factory.deserialize_integration) async def edit_integration( self, guild: typing.Union[guilds.Guild, bases.UniqueObject], - integration: typing.Union[guilds.GuildIntegration, bases.UniqueObject], + integration: typing.Union[guilds.Integration, bases.UniqueObject], *, expire_behaviour: typing.Union[unset.Unset, guilds.IntegrationExpireBehaviour] = unset.UNSET, expire_grace_period: typing.Union[unset.Unset, date.TimeSpan] = unset.UNSET, @@ -2052,7 +2052,7 @@ async def edit_integration( async def delete_integration( self, guild: typing.Union[guilds.Guild, bases.UniqueObject], - integration: typing.Union[guilds.GuildIntegration, bases.UniqueObject], + integration: typing.Union[guilds.Integration, bases.UniqueObject], *, reason: typing.Union[unset.Unset, str] = unset.UNSET, ) -> None: @@ -2062,7 +2062,7 @@ async def delete_integration( async def sync_integration( self, guild: typing.Union[guilds.Guild, bases.UniqueObject], - integration: typing.Union[guilds.GuildIntegration, bases.UniqueObject], + integration: typing.Union[guilds.Integration, bases.UniqueObject], ) -> None: route = routes.POST_GUILD_INTEGRATION_SYNC.compile(guild=guild, integration=integration) await self._request(route) diff --git a/hikari/utilities/aio.py b/hikari/utilities/aio.py index ac08afd8b2..45cfaf9452 100644 --- a/hikari/utilities/aio.py +++ b/hikari/utilities/aio.py @@ -30,7 +30,7 @@ import contextvars import types - _T_contra = typing.TypeVar("_T_contra", contravariant=True) +_T_contra = typing.TypeVar("_T_contra", contravariant=True) def completed_future(result: _T_contra = None, /) -> Future[_T_contra]: diff --git a/tests/hikari/impl/__init__.py b/tests/hikari/impl/__init__.py new file mode 100644 index 0000000000..1c1502a5ca --- /dev/null +++ b/tests/hikari/impl/__init__.py @@ -0,0 +1,18 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# Copyright © Nekoka.tt 2019-2020 +# +# This file is part of Hikari. +# +# Hikari is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Hikari is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Hikari. If not, see . diff --git a/tests/hikari/impl/test_entity_factory.py b/tests/hikari/impl/test_entity_factory.py new file mode 100644 index 0000000000..f4d3481ad2 --- /dev/null +++ b/tests/hikari/impl/test_entity_factory.py @@ -0,0 +1,2422 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# Copyright © Nekoka.tt 2019-2020 +# +# This file is part of Hikari. +# +# Hikari is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Hikari is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Hikari. If not, see . +import datetime + +import mock +import pytest + +from hikari import app as app_ +from hikari.impl import entity_factory +from hikari.models import applications +from hikari.models import audit_logs +from hikari.models import channels +from hikari.models import colors +from hikari.models import embeds +from hikari.models import emojis +from hikari.models import gateway +from hikari.models import guilds +from hikari.models import invites +from hikari.models import messages +from hikari.models import permissions +from hikari.models import webhooks +from hikari.models import users +from hikari.models import voices +from hikari.utilities import unset + + +class TestEntityFactoryImpl: + @pytest.fixture() + def mock_app(self) -> app_.IApp: + return mock.MagicMock(app_.IApp) + + @pytest.fixture() + def entity_factory_impl(self, mock_app) -> entity_factory.EntityFactoryImpl: + return entity_factory.EntityFactoryImpl(app=mock_app) + + def test_app(self, entity_factory_impl, mock_app): + assert entity_factory_impl.app is mock_app + + ################ + # APPLICATIONS # + ################ + + @pytest.fixture() + def partial_integration(self): + return { + "id": "123123123123123", + "name": "A Name", + "type": "twitch", + "account": {"name": "twitchUsername", "id": "123123"}, + } + + @pytest.fixture() + def own_connection_payload(self, partial_integration): + return { + "friend_sync": False, + "id": "2513849648", + "integrations": [partial_integration], + "name": "FS", + "revoked": False, + "show_activity": True, + "type": "twitter", + "verified": True, + "visibility": 0, + } + + def test_deserialize_own_connection( + self, entity_factory_impl, mock_app, own_connection_payload, partial_integration + ): + own_connection = entity_factory_impl.deserialize_own_connection(own_connection_payload) + assert own_connection.id == 2513849648 + assert own_connection.name == "FS" + assert own_connection.type == "twitter" + assert own_connection.is_revoked is False + assert own_connection.integrations == [entity_factory_impl.deserialize_partial_integration(partial_integration)] + assert own_connection.is_verified is True + assert own_connection.is_friend_syncing is False + assert own_connection.is_showing_activity is True + assert own_connection.visibility == applications.ConnectionVisibility.NONE + assert isinstance(own_connection, applications.OwnConnection) + + @pytest.fixture() + def own_guild_payload(self): + return { + "id": "152559372126519269", + "name": "Isopropyl", + "icon": "d4a983885dsaa7691ce8bcaaf945a", + "owner": False, + "permissions": 2147483647, + "features": ["DISCOVERABLE", "FORCE_RELAY"], + } + + def test_deserialize_own_guild(self, entity_factory_impl, mock_app, own_guild_payload): + own_guild = entity_factory_impl.deserialize_own_guild(own_guild_payload) + assert own_guild.id == 152559372126519269 + assert own_guild.name == "Isopropyl" + assert own_guild.icon_hash == "d4a983885dsaa7691ce8bcaaf945a" + assert own_guild.features == {guilds.GuildFeature.DISCOVERABLE, "FORCE_RELAY"} + assert own_guild.is_owner is False + assert own_guild.my_permissions == permissions.Permission(2147483647) + + def test_deserialize_own_guild_with_null_and_unset_fields(self, entity_factory_impl): + own_guild = entity_factory_impl.deserialize_own_guild( + { + "id": "152559372126519269", + "name": "Isopropyl", + "icon": None, + "owner": False, + "permissions": 2147483647, + "features": ["DISCOVERABLE", "FORCE_RELAY"], + } + ) + assert own_guild.icon_hash is None + + @pytest.fixture() + def user_payload(self): + return {"username": "agent 47", "avatar": "hashed", "discriminator": "4747", "id": "474747474"} + + @pytest.fixture() + def owner_payload(self, user_payload): + return {**user_payload, "flags": 1 << 10} + + @pytest.fixture() + def application_information_payload(self, owner_payload, user_payload): + return { + "id": "209333111222", + "name": "Dream Sweet in Sea Major", + "icon": "iwiwiwiwiw", + "description": "I am an application", + "rpc_origins": ["127.0.0.0"], + "bot_public": True, + "bot_require_code_grant": False, + "owner": owner_payload, + "summary": "not a blank string", + "verify_key": "698c5d0859abb686be1f8a19e0e7634d8471e33817650f9fb29076de227bca90", + "team": { + "icon": "hashtag", + "id": "202020202", + "members": [ + {"membership_state": 1, "permissions": ["*"], "team_id": "209333111222", "user": user_payload} + ], + "owner_user_id": "393030292", + }, + "guild_id": "2020293939", + "primary_sku_id": "2020202002", + "slug": "192.168.1.254", + "cover_image": "hashmebaby", + } + + def test_deserialize_application( + self, entity_factory_impl, mock_app, application_information_payload, owner_payload, user_payload + ): + application = entity_factory_impl.deserialize_application(application_information_payload) + assert application._app is mock_app + assert application.id == 209333111222 + assert application.name == "Dream Sweet in Sea Major" + assert application.description == "I am an application" + assert application.is_bot_public is True + assert application.is_bot_code_grant_required is False + assert application.owner == entity_factory_impl.deserialize_user(owner_payload) + assert application.rpc_origins == {"127.0.0.0"} + assert application.summary == "not a blank string" + assert application.verify_key == b"698c5d0859abb686be1f8a19e0e7634d8471e33817650f9fb29076de227bca90" + assert application.icon_hash == "iwiwiwiwiw" + # Team + assert application.team.id == 202020202 + assert application.team.icon_hash == "hashtag" + assert application.team.owner_user_id == 393030292 + assert isinstance(application.team, applications.Team) + # TeamMember + assert len(application.team.members) == 1 + member = application.team.members[115590097100865541] + assert member.membership_state == applications.TeamMembershipState.INVITED + assert member.permissions == {"*"} + assert member.team_id == 209333111222 + assert member.user == entity_factory_impl.deserialize_user(user_payload) + assert isinstance(member, applications.TeamMember) + + assert application.guild_id == 2020293939 + assert application.primary_sku_id == 2020202002 + assert application.slug == "192.168.1.254" + assert application.cover_image_hash == "hashmebaby" + assert isinstance(application, applications.Application) + + def test_deserialize_application_with_unset_fields(self, entity_factory_impl, mock_app, owner_payload): + application = entity_factory_impl.deserialize_application( + { + "id": "209333111222", + "name": "Dream Sweet in Sea Major", + "icon": "3123123", + "description": "I am an application", + "summary": "not a blank string", + } + ) + assert application.is_bot_public is None + assert application.is_bot_code_grant_required is None + assert application.owner is None + assert application.rpc_origins is None + assert application.verify_key is None + assert application.team is None + assert application.guild_id is None + assert application.primary_sku_id is None + assert application.slug is None + assert application.cover_image_hash is None + + def test_deserialize_application_with_null_fields(self, entity_factory_impl, mock_app, owner_payload): + application = entity_factory_impl.deserialize_application( + { + "id": "209333111222", + "name": "Dream Sweet in Sea Major", + "icon": None, + "description": "I am an application", + "summary": "not a blank string", + "team": None, + } + ) + assert application.icon_hash is None + assert application.team is None + + ############## + # AUDIT_LOGS # + ############## + + def test__deserialize_audit_log_change_roles(self, entity_factory_impl): + test_role_payloads = [ + {"id": "24", "name": "roleA"}, + ] + roles = entity_factory_impl._deserialize_audit_log_change_roles(test_role_payloads) + assert len(roles) == 1 + role = roles[24] + assert role.id == 24 + assert role.name == "roleA" + assert isinstance(role, guilds.PartialRole) + + def test__deserialize_audit_log_overwrites(self, entity_factory_impl): + test_overwrite_payloads = [ + {"id": "24", "type": "role", "allow": 21, "deny": 0}, + {"id": "48", "type": "role", "deny": 42, "allow": 0}, + ] + overwrites = entity_factory_impl._deserialize_audit_log_overwrites(test_overwrite_payloads) + assert overwrites == { + 24: entity_factory_impl.deserialize_permission_overwrite( + {"id": "24", "type": "role", "allow": 21, "deny": 0} + ), + 48: entity_factory_impl.deserialize_permission_overwrite( + {"id": "48", "type": "role", "deny": 42, "allow": 0} + ), + } + + @pytest.fixture() + def overwrite_info_payload(self): + return {"id": "123123123", "type": "role", "role_name": "aRole"} + + def test__deserialize_channel_overwrite_entry_info(self, entity_factory_impl, overwrite_info_payload): + overwrite_entry_info = entity_factory_impl._deserialize_channel_overwrite_entry_info(overwrite_info_payload) + assert overwrite_entry_info.id == 123123123 + assert overwrite_entry_info.type is channels.PermissionOverwriteType.ROLE + assert overwrite_entry_info.role_name == "aRole" + assert isinstance(overwrite_entry_info, audit_logs.ChannelOverwriteEntryInfo) + + @pytest.fixture() + def message_pin_info_payload(self): + return { + "channel_id": "123123123", + "message_id": "69696969", + } + + def test__deserialize_message_pin_entry_info(self, entity_factory_impl, message_pin_info_payload): + message_pin_info = entity_factory_impl._deserialize_message_pin_entry_info(message_pin_info_payload) + assert message_pin_info.channel_id == 123123123 + assert message_pin_info.message_id == 69696969 + assert isinstance(message_pin_info, audit_logs.MessagePinEntryInfo) + + @pytest.fixture() + def member_prune_info_payload(self): + return { + "delete_member_days": "7", + "members_removed": "1", + } + + def test__deserialize_member_prune_entry_info(self, entity_factory_impl, member_prune_info_payload): + member_prune_info = entity_factory_impl._deserialize_member_prune_entry_info(member_prune_info_payload) + assert member_prune_info.delete_member_days == datetime.timedelta(days=7) + assert member_prune_info.members_removed == 1 + assert isinstance(member_prune_info, audit_logs.MemberPruneEntryInfo) + + @pytest.fixture() + def message_bulk_delete_info_payload(self): + return {"count": "42"} + + def test__deserialize_message_bulk_delete_entry_info(self, entity_factory_impl, message_bulk_delete_info_payload): + message_bulk_delete_entry_info = entity_factory_impl._deserialize_message_bulk_delete_entry_info( + message_bulk_delete_info_payload + ) + assert message_bulk_delete_entry_info.count == 42 + assert isinstance(message_bulk_delete_entry_info, audit_logs.MessageBulkDeleteEntryInfo) + + @pytest.fixture() + def message_delete_info_payload(self): + return {"count": "42", "channel_id": "4206942069"} + + def test__deserialize_message_delete_entry_info(self, entity_factory_impl, message_delete_info_payload): + message_delete_entry_info = entity_factory_impl._deserialize_message_delete_entry_info( + message_delete_info_payload + ) + assert message_delete_entry_info.count == 42 + assert message_delete_entry_info.channel_id == 4206942069 + assert isinstance(message_delete_entry_info, audit_logs.MessageDeleteEntryInfo) + + @pytest.fixture() + def member_disconnect_info_payload(self): + return {"count": "42"} + + def test__deserialize_member_disconnect_entry_info(self, entity_factory_impl, member_disconnect_info_payload): + member_disconnect_entry_info = entity_factory_impl._deserialize_member_disconnect_entry_info( + member_disconnect_info_payload + ) + assert member_disconnect_entry_info.count == 42 + assert isinstance(member_disconnect_entry_info, audit_logs.MemberDisconnectEntryInfo) + + @pytest.fixture() + def member_move_info_payload(self): + return {"count": "42", "channel_id": "22222222"} + + def test__deserialize_member_move_entry_info(self, entity_factory_impl, member_move_info_payload): + member_move_entry_info = entity_factory_impl._deserialize_member_move_entry_info(member_move_info_payload) + assert member_move_entry_info.channel_id == 22222222 + assert isinstance(member_move_entry_info, audit_logs.MemberMoveEntryInfo) + + @pytest.fixture() + def unrecognised_audit_log_entry(self): + return {"count": "5412", "action": "nyaa'd"} + + def test__deserialize_unrecognised_audit_log_entry_info(self, entity_factory_impl, unrecognised_audit_log_entry): + unrecognised_info = entity_factory_impl._deserialize_unrecognised_audit_log_entry_info( + unrecognised_audit_log_entry + ) + assert unrecognised_info.count == "5412" + assert unrecognised_info.action == "nyaa'd" + assert isinstance(unrecognised_info, audit_logs.UnrecognisedAuditLogEntryInfo) + + @pytest.fixture() + def audit_log_entry_payload(self): + return { + "action_type": 14, + "changes": [ + { + "key": "$add", + "old_value": [{"id": "568651298858074123", "name": "Casual"}], + "new_value": [{"id": "123123123312312", "name": "aRole"}], + } + ], + "id": "694026906592477214", + "options": {"id": "115590097100865541", "type": "member"}, + "target_id": "115590097100865541", + "user_id": "560984860634644482", + "reason": "An artificial insanity.", + } + + @pytest.mark.skip("TODO") + def test_deserialize_audit_log(self, entity_factory_impl, mock_app, audit_log_entry_payload): + raise NotImplementedError # TODO: test coverage for audit log cases + + ############ + # CHANNELS # + ############ + + @pytest.fixture() + def permission_overwrite_payload(self): + return {"id": "4242", "type": "member", "allow": 65, "deny": 49152} + + def test_deserialize_permission_overwrite(self, entity_factory_impl, permission_overwrite_payload): + overwrite = entity_factory_impl.deserialize_permission_overwrite(permission_overwrite_payload) + assert overwrite.type == channels.PermissionOverwriteType.MEMBER + assert overwrite.allow == permissions.Permission(65) + assert overwrite.deny == permissions.Permission(49152) + assert isinstance(overwrite, channels.PermissionOverwrite) + + def test_serialize_permission_overwrite(self, entity_factory_impl): + overwrite = channels.PermissionOverwrite(id=123123, type="member", allow=42, deny=62) + payload = entity_factory_impl.serialize_permission_overwrite(overwrite) + assert payload == {"id": "123123", "type": "member", "allow": 42, "deny": 62} + + @pytest.fixture() + def partial_channel_payload(self): + return {"id": "561884984214814750", "name": "general", "type": 0} + + def test_deserialize_partial_channel(self, entity_factory_impl, mock_app, partial_channel_payload): + partial_channel = entity_factory_impl.deserialize_partial_channel(partial_channel_payload) + assert partial_channel._app is mock_app + assert partial_channel.id == 561884984214814750 + assert partial_channel.name == "general" + assert partial_channel.type == channels.ChannelType.GUILD_TEXT + assert isinstance(partial_channel, channels.PartialChannel) + + def test_deserialize_partial_channel_with_unset_fields(self, entity_factory_impl): + assert entity_factory_impl.deserialize_partial_channel({"id": "22", "type": 0}).name is None + + @pytest.fixture() + def dm_channel_payload(self, user_payload): + return { + "id": "123", + "last_message_id": "456", + "type": 1, + "recipients": [user_payload], + } + + def test_deserialize_dm_channel(self, entity_factory_impl, mock_app, dm_channel_payload, user_payload): + dm_channel = entity_factory_impl.deserialize_dm_channel(dm_channel_payload) + assert dm_channel._app is mock_app + assert dm_channel.id == 123 + assert dm_channel.name is None + assert dm_channel.last_message_id == 456 + assert dm_channel.type is channels.ChannelType.DM + assert dm_channel.recipients == {115590097100865541: entity_factory_impl.deserialize_user(user_payload)} + assert isinstance(dm_channel, channels.DMChannel) + + def test_deserialize_dm_channel_with_null_fields(self, entity_factory_impl, user_payload): + dm_channel = entity_factory_impl.deserialize_dm_channel( + {"id": "123", "last_message_id": None, "type": 1, "recipients": [user_payload]} + ) + assert dm_channel.last_message_id is None + + @pytest.fixture() + def group_dm_channel_payload(self, user_payload): + return { + "id": "123", + "name": "Secret Developer Group", + "icon": "123asdf123adsf", + "owner_id": "456", + "application_id": "123789", + "last_message_id": "456", + "type": 3, + "recipients": [user_payload], + } + + def test_deserialize_group_dm_channel(self, entity_factory_impl, mock_app, group_dm_channel_payload, user_payload): + group_dm = entity_factory_impl.deserialize_group_dm_channel(group_dm_channel_payload) + assert group_dm._app is mock_app + assert group_dm.id == 123 + assert group_dm.name == "Secret Developer Group" + assert group_dm.icon_hash == "123asdf123adsf" + assert group_dm.application_id == 123789 + assert group_dm.last_message_id == 456 + assert group_dm.type == channels.ChannelType.GROUP_DM + assert group_dm.recipients == {115590097100865541: entity_factory_impl.deserialize_user(user_payload)} + assert isinstance(group_dm, channels.GroupDMChannel) + + def test_test_deserialize_group_dm_channel_with_unset_fields(self, entity_factory_impl, user_payload): + dm_channel = entity_factory_impl.deserialize_group_dm_channel( + { + "id": "123", + "name": "Secret Developer Group", + "icon": "123asdf123adsf", + "owner_id": "456", + "last_message_id": "456", + "type": 3, + "recipients": [user_payload], + } + ) + assert dm_channel.application_id is None + + @pytest.fixture() + def guild_category_payload(self, permission_overwrite_payload): + return { + "id": "123", + "permission_overwrites": [permission_overwrite_payload], + "name": "Test", + "parent_id": "664565", + "nsfw": True, + "position": 3, + "guild_id": "9876", + "type": 4, + } + + def test_deserialize_guild_category( + self, entity_factory_impl, mock_app, guild_category_payload, permission_overwrite_payload + ): + guild_category = entity_factory_impl.deserialize_guild_category(guild_category_payload) + assert guild_category._app is mock_app + assert guild_category.id == 123 + assert guild_category.name == "Test" + assert guild_category.type == channels.ChannelType.GUILD_CATEGORY + assert guild_category.guild_id == 9876 + assert guild_category.position == 3 + assert guild_category.permission_overwrites == { + 4242: entity_factory_impl.deserialize_permission_overwrite(permission_overwrite_payload) + } + assert guild_category.is_nsfw is True + assert guild_category.parent_id == 664565 + assert isinstance(guild_category, channels.GuildCategory) + + def test_deserialize_guild_category_with_unset_fields(self, entity_factory_impl, permission_overwrite_payload): + guild_category = entity_factory_impl.deserialize_guild_category( + { + "id": "123", + "permission_overwrites": [permission_overwrite_payload], + "name": "Test", + "position": 3, + "type": 4, + } + ) + assert guild_category.parent_id is None + assert guild_category.is_nsfw is None + assert guild_category.guild_id is None + + def test_deserialize_guild_category_with_null_fields(self, entity_factory_impl, permission_overwrite_payload): + guild_category = entity_factory_impl.deserialize_guild_category( + { + "id": "123", + "permission_overwrites": [permission_overwrite_payload], + "name": "Test", + "parent_id": None, + "nsfw": True, + "position": 3, + "guild_id": "9876", + "type": 4, + } + ) + assert guild_category.parent_id is None + + @pytest.fixture() + def guild_text_channel_payload(self, permission_overwrite_payload): + return { + "id": "123", + "guild_id": "567", + "name": "general", + "type": 0, + "position": 6, + "permission_overwrites": [permission_overwrite_payload], + "rate_limit_per_user": 2, + "nsfw": True, + "topic": "¯\\_(ツ)_/¯", + "last_message_id": "123456", + "last_pin_timestamp": "2020-05-27T15:58:51.545252+00:00", + "parent_id": "987", + } + + def test_deserialize_guild_text_channel( + self, entity_factory_impl, mock_app, guild_text_channel_payload, permission_overwrite_payload + ): + guild_text_channel = entity_factory_impl.deserialize_guild_text_channel(guild_text_channel_payload) + assert guild_text_channel._app is mock_app + assert guild_text_channel.id == 123 + assert guild_text_channel.name == "general" + assert guild_text_channel.type == channels.ChannelType.GUILD_TEXT + assert guild_text_channel.guild_id == 567 + assert guild_text_channel.position == 6 + assert guild_text_channel.permission_overwrites == { + 4242: entity_factory_impl.deserialize_permission_overwrite(permission_overwrite_payload) + } + assert guild_text_channel.is_nsfw is True + assert guild_text_channel.parent_id == 987 + assert guild_text_channel.topic == "¯\\_(ツ)_/¯" + assert guild_text_channel.last_message_id == 123456 + assert guild_text_channel.rate_limit_per_user == datetime.timedelta(seconds=2) + assert guild_text_channel.last_pin_timestamp == datetime.datetime( + 2020, 5, 27, 15, 58, 51, 545252, tzinfo=datetime.timezone.utc + ) + assert isinstance(guild_text_channel, channels.GuildTextChannel) + + def test_deserialize_guild_text_channel_with_unset_fields(self, entity_factory_impl): + guild_text_channel = entity_factory_impl.deserialize_guild_text_channel( + { + "id": "123", + "name": "general", + "type": 0, + "position": 6, + "permission_overwrites": [], + "rate_limit_per_user": 2, + "topic": "¯\\_(ツ)_/¯", + "last_message_id": "123456", + } + ) + assert guild_text_channel.guild_id is None + assert guild_text_channel.is_nsfw is None + assert guild_text_channel.last_pin_timestamp is None + assert guild_text_channel.parent_id is None + + def test_deserialize_guild_text_channel_with_null_fields(self, entity_factory_impl): + guild_text_channel = entity_factory_impl.deserialize_guild_text_channel( + { + "id": "123", + "guild_id": "567", + "name": "general", + "type": 0, + "position": 6, + "permission_overwrites": [], + "rate_limit_per_user": 2, + "nsfw": True, + "topic": None, + "last_message_id": None, + "last_pin_timestamp": None, + "parent_id": None, + } + ) + assert guild_text_channel.topic is None + assert guild_text_channel.last_message_id is None + assert guild_text_channel.last_pin_timestamp is None + assert guild_text_channel.parent_id is None + + @pytest.fixture() + def guild_news_channel_payload(self, permission_overwrite_payload): + return { + "id": "567", + "guild_id": "123", + "name": "Important Announcements", + "type": 5, + "position": 0, + "permission_overwrites": [permission_overwrite_payload], + "nsfw": True, + "topic": "Super Important Announcements", + "last_message_id": "456", + "parent_id": "654", + "last_pin_timestamp": "2020-05-27T15:58:51.545252+00:00", + } + + def test_deserialize_guild_news_channel( + self, entity_factory_impl, mock_app, guild_news_channel_payload, permission_overwrite_payload + ): + news_channel = entity_factory_impl.deserialize_guild_news_channel(guild_news_channel_payload) + assert news_channel._app is mock_app + assert news_channel.id == 567 + assert news_channel.name == "Important Announcements" + assert news_channel.type == channels.ChannelType.GUILD_NEWS + assert news_channel.guild_id == 123 + assert news_channel.position == 0 + assert news_channel.permission_overwrites == { + 4242: entity_factory_impl.deserialize_permission_overwrite(permission_overwrite_payload) + } + assert news_channel.is_nsfw is True + assert news_channel.parent_id == 654 + assert news_channel.topic == "Super Important Announcements" + assert news_channel.last_message_id == 456 + assert news_channel.last_pin_timestamp == datetime.datetime( + 2020, 5, 27, 15, 58, 51, 545252, tzinfo=datetime.timezone.utc + ) + assert isinstance(news_channel, channels.GuildNewsChannel) + + def test_deserialize_guild_news_channel_with_unset_fields(self, entity_factory_impl): + news_channel = entity_factory_impl.deserialize_guild_news_channel( + { + "id": "567", + "name": "Important Announcements", + "type": 5, + "position": 0, + "permission_overwrites": [], + "topic": "Super Important Announcements", + "last_message_id": "456", + } + ) + assert news_channel.guild_id is None + assert news_channel.is_nsfw is None + assert news_channel.parent_id is None + assert news_channel.last_pin_timestamp is None + + def test_deserialize_guild_news_channel_with_null_fields(self, entity_factory_impl): + news_channel = entity_factory_impl.deserialize_guild_news_channel( + { + "id": "567", + "guild_id": "123", + "name": "Important Announcements", + "type": 5, + "position": 0, + "permission_overwrites": [], + "nsfw": True, + "topic": None, + "last_message_id": None, + "parent_id": None, + "last_pin_timestamp": None, + } + ) + assert news_channel.topic is None + assert news_channel.last_message_id is None + assert news_channel.parent_id is None + assert news_channel.last_pin_timestamp is None + + @pytest.fixture() + def guild_store_channel_payload(self, permission_overwrite_payload): + return { + "id": "123", + "permission_overwrites": [permission_overwrite_payload], + "name": "Half Life 3", + "parent_id": "9876", + "nsfw": True, + "position": 2, + "guild_id": "1234", + "type": 6, + } + + def test_deserialize_guild_store_channel( + self, entity_factory_impl, mock_app, guild_store_channel_payload, permission_overwrite_payload + ): + store_chanel = entity_factory_impl.deserialize_guild_store_channel(guild_store_channel_payload) + assert store_chanel.id == 123 + assert store_chanel.name == "Half Life 3" + assert store_chanel.type == channels.ChannelType.GUILD_STORE + assert store_chanel.guild_id == 1234 + assert store_chanel.position == 2 + assert store_chanel.permission_overwrites == { + 4242: entity_factory_impl.deserialize_permission_overwrite(permission_overwrite_payload) + } + assert store_chanel.is_nsfw is True + assert store_chanel.parent_id == 9876 + assert isinstance(store_chanel, channels.GuildStoreChannel) + + def test_deserialize_guild_store_channel_with_unset_fields(self, entity_factory_impl): + store_chanel = entity_factory_impl.deserialize_guild_store_channel( + {"id": "123", "permission_overwrites": [], "name": "Half Life 3", "position": 2, "type": 6,} + ) + assert store_chanel.parent_id is None + assert store_chanel.is_nsfw is None + assert store_chanel.guild_id is None + + def test_deserialize_guild_store_channel_with_null_fields(self, entity_factory_impl): + store_chanel = entity_factory_impl.deserialize_guild_store_channel( + { + "id": "123", + "permission_overwrites": [], + "name": "Half Life 3", + "parent_id": None, + "nsfw": True, + "position": 2, + "guild_id": "1234", + "type": 6, + } + ) + assert store_chanel.parent_id is None + + @pytest.fixture() + def guild_voice_channel_payload(self, permission_overwrite_payload): + return { + "id": "555", + "guild_id": "789", + "name": "Secret Developer Discussions", + "type": 2, + "nsfw": True, + "position": 4, + "permission_overwrites": [permission_overwrite_payload], + "bitrate": 64000, + "user_limit": 3, + "parent_id": "456", + } + + def test_deserialize_guild_voice_channel( + self, entity_factory_impl, mock_app, guild_voice_channel_payload, permission_overwrite_payload + ): + voice_channel = entity_factory_impl.deserialize_guild_voice_channel(guild_voice_channel_payload) + assert voice_channel.id == 555 + assert voice_channel.name == "Secret Developer Discussions" + assert voice_channel.type == channels.ChannelType.GUILD_VOICE + assert voice_channel.guild_id == 789 + assert voice_channel.position == 4 + assert voice_channel.permission_overwrites == { + 4242: entity_factory_impl.deserialize_permission_overwrite(permission_overwrite_payload) + } + assert voice_channel.is_nsfw is True + assert voice_channel.parent_id == 456 + assert voice_channel.bitrate == 64000 + assert voice_channel.user_limit == 3 + assert isinstance(voice_channel, channels.GuildVoiceChannel) + + def test_deserialize_guild_voice_channel_with_null_fields(self, entity_factory_impl): + voice_channel = entity_factory_impl.deserialize_guild_voice_channel( + { + "id": "123", + "permission_overwrites": [], + "name": "Half Life 3", + "parent_id": None, + "nsfw": True, + "position": 2, + "guild_id": "1234", + "bitrate": 64000, + "user_limit": 3, + "type": 6, + } + ) + assert voice_channel.parent_id is None + + def test_deserialize_guild_voice_channel_with_unset_fields(self, entity_factory_impl): + voice_channel = entity_factory_impl.deserialize_guild_voice_channel( + { + "id": "123", + "permission_overwrites": [], + "name": "Half Life 3", + "position": 2, + "bitrate": 64000, + "user_limit": 3, + "type": 6, + } + ) + assert voice_channel.parent_id is None + assert voice_channel.is_nsfw is None + assert voice_channel.guild_id is None + + def test_deserialize_channel_returns_right_type( + self, + entity_factory_impl, + dm_channel_payload, + group_dm_channel_payload, + guild_category_payload, + guild_text_channel_payload, + guild_news_channel_payload, + guild_store_channel_payload, + guild_voice_channel_payload, + ): + for payload, expected_type in [ + (dm_channel_payload, channels.DMChannel), + (group_dm_channel_payload, channels.GroupDMChannel), + (guild_category_payload, channels.GuildCategory), + (guild_text_channel_payload, channels.GuildTextChannel), + (guild_news_channel_payload, channels.GuildNewsChannel), + (guild_store_channel_payload, channels.GuildStoreChannel), + (guild_voice_channel_payload, channels.GuildVoiceChannel), + ]: + assert isinstance(entity_factory_impl.deserialize_channel(payload), expected_type) + + ########## + # EMBEDS # + ########## + + @pytest.fixture + def embed_payload(self): + return { + "title": "embed title", + "description": "embed description", + "url": "https://somewhere.com", + "timestamp": "2020-03-22T16:40:39.218000+00:00", + "color": 14014915, + "footer": { + "text": "footer text", + "icon_url": "https://somewhere.com/footer.png", + "proxy_icon_url": "https://media.somewhere.com/footer.png", + }, + "image": { + "url": "https://somewhere.com/image.png", + "proxy_url": "https://media.somewhere.com/image.png", + "height": 122, + "width": 133, + }, + "thumbnail": { + "url": "https://somewhere.com/thumbnail.png", + "proxy_url": "https://media.somewhere.com/thumbnail.png", + "height": 123, + "width": 456, + }, + "video": {"url": "https://somewhere.com/video.mp4", "height": 1234, "width": 4567}, + "provider": {"name": "some name", "url": "https://somewhere.com/provider"}, + "author": { + "name": "some name", + "url": "https://somewhere.com/author", + "icon_url": "https://somewhere.com/author.png", + "proxy_icon_url": "https://media.somewhere.com/author.png", + }, + "fields": [{"name": "title", "value": "some value", "inline": True}], + } + + def test_deserialize_embed_with_full_embed(self, entity_factory_impl, embed_payload): + embed = entity_factory_impl.deserialize_embed(embed_payload) + assert embed.title == "embed title" + assert embed.description == "embed description" + assert embed.url == "https://somewhere.com" + assert embed.timestamp == datetime.datetime(2020, 3, 22, 16, 40, 39, 218000, tzinfo=datetime.timezone.utc) + assert embed.color == colors.Color(14014915) + assert isinstance(embed.color, colors.Color) + # EmbedFooter + assert embed.footer.text == "footer text" + assert embed.footer.icon_url == "https://somewhere.com/footer.png" + assert embed.footer.proxy_icon_url == "https://media.somewhere.com/footer.png" + assert isinstance(embed.footer, embeds.EmbedFooter) + # EmbedImage + assert embed.image.url == "https://somewhere.com/image.png" + assert embed.image.proxy_url == "https://media.somewhere.com/image.png" + assert embed.image.height == 122 + assert embed.image.width == 133 + assert isinstance(embed.image, embeds.EmbedImage) + # EmbedThumbnail + assert embed.thumbnail.url == "https://somewhere.com/thumbnail.png" + assert embed.thumbnail.proxy_url == "https://media.somewhere.com/thumbnail.png" + assert embed.thumbnail.height == 123 + assert embed.thumbnail.width == 456 + assert isinstance(embed.thumbnail, embeds.EmbedThumbnail) + # EmbedVideo + assert embed.video.url == "https://somewhere.com/video.mp4" + assert embed.video.height == 1234 + assert embed.video.width == 4567 + assert isinstance(embed.video, embeds.EmbedVideo) + # EmbedProvider + assert embed.provider.name == "some name" + assert embed.provider.url == "https://somewhere.com/provider" + assert isinstance(embed.provider, embeds.EmbedProvider) + # EmbedAuthor + assert embed.author.name == "some name" + assert embed.author.url == "https://somewhere.com/author" + assert embed.author.icon_url == "https://somewhere.com/author.png" + assert embed.author.proxy_icon_url == "https://media.somewhere.com/author.png" + assert isinstance(embed.author, embeds.EmbedAuthor) + # EmbedField + assert len(embed.fields) == 1 + field = embed.fields[0] + assert field.name == "title" + assert field.value == "some value" + assert field.is_inline is True + assert isinstance(field, embeds.EmbedField) + + def test_deserialize_embed_with_partial_fields(self, entity_factory_impl, embed_payload): + embed = entity_factory_impl.deserialize_embed( + { + "footer": {"text": "footer text"}, + "image": {}, + "thumbnail": {}, + "video": {}, + "provider": {}, + "author": {}, + "fields": [{"name": "title", "value": "some value"}], + } + ) + # EmbedFooter + assert embed.footer.text == "footer text" + assert embed.footer.icon_url is None + assert embed.footer.proxy_icon_url is None + assert isinstance(embed.footer, embeds.EmbedFooter) + # EmbedImage + assert embed.image.url is None + assert embed.image.proxy_url is None + assert embed.image.height is None + assert embed.image.width is None + assert isinstance(embed.image, embeds.EmbedImage) + # EmbedThumbnail + assert embed.thumbnail.url is None + assert embed.thumbnail.proxy_url is None + assert embed.thumbnail.height is None + assert embed.thumbnail.width is None + assert isinstance(embed.thumbnail, embeds.EmbedThumbnail) + # EmbedVideo + assert embed.video.url is None + assert embed.video.height is None + assert embed.video.width is None + assert isinstance(embed.video, embeds.EmbedVideo) + # EmbedProvider + assert embed.provider.name is None + assert embed.provider.url is None + assert isinstance(embed.provider, embeds.EmbedProvider) + # EmbedAuthor + assert embed.author.name is None + assert embed.author.url is None + assert embed.author.icon_url is None + assert embed.author.proxy_icon_url is None + assert isinstance(embed.author, embeds.EmbedAuthor) + # EmbedField + assert len(embed.fields) == 1 + field = embed.fields[0] + assert field.name == "title" + assert field.value == "some value" + assert field.is_inline is False + assert isinstance(field, embeds.EmbedField) + + def test_deserialize_embed_with_empty_embed(self, entity_factory_impl): + embed = entity_factory_impl.deserialize_embed({}) + assert embed.title is None + assert embed.description is None + assert embed.url is None + assert embed.timestamp is None + assert embed.color is None + assert embed.footer is None + assert embed.image is None + assert embed.thumbnail is None + assert embed.video is None + assert embed.provider is None + assert embed.author is None + assert embed.fields == [] + + def test_serialize_embed(self, entity_factory_impl): + payload = entity_factory_impl.serialize_embed( + embeds.Embed( + title="Title", + description="Nyaa", + url="https://some-url", + timestamp=datetime.datetime(2020, 5, 29, 20, 37, 22, 865139), + color=colors.Color(321321), + footer=embeds.EmbedFooter(text="TEXT", icon_url="httppppp"), + image=embeds.EmbedImage(url="https://///"), + thumbnail=embeds.EmbedThumbnail(url="wss://not-a-valid-url"), + author=embeds.EmbedAuthor(name="AUTH ME", url="wss://\\_/-_-\\_/", icon_url="icon://"), + fields=[embeds.EmbedField(value="VALUE", name="NAME", is_inline=True)], + ) + ) + assert payload == { + "title": "Title", + "description": "Nyaa", + "url": "https://some-url", + "timestamp": "2020-05-29T20:37:22.865139", + "color": 321321, + "footer": {"text": "TEXT", "icon_url": "httppppp"}, + "image": {"url": "https://///"}, + "thumbnail": {"url": "wss://not-a-valid-url"}, + "author": {"name": "AUTH ME", "url": "wss://\\_/-_-\\_/", "icon_url": "icon://"}, + "fields": [{"value": "VALUE", "name": "NAME", "inline": True}], + } + + def test_serialize_embed_with_null_sub_fields(self, entity_factory_impl): + payload = entity_factory_impl.serialize_embed( + embeds.Embed( + title="Title", + description="Nyaa", + url="https://some-url", + timestamp=datetime.datetime(2020, 5, 29, 20, 37, 22, 865139), + color=colors.Color(321321), + footer=embeds.EmbedFooter(), + image=embeds.EmbedImage(), + thumbnail=embeds.EmbedThumbnail(), + author=embeds.EmbedAuthor(), + fields=[embeds.EmbedField()], + ) + ) + assert payload == { + "title": "Title", + "description": "Nyaa", + "url": "https://some-url", + "timestamp": "2020-05-29T20:37:22.865139", + "color": 321321, + "footer": {}, + "image": {}, + "thumbnail": {}, + "author": {}, + "fields": [{"inline": False}], + } + + def test_serialize_embed_with_null_attributes(self, entity_factory_impl): + assert entity_factory_impl.serialize_embed(embeds.Embed()) == {} + + ########## + # EMOJIS # + ########## + + def test_deserialize_unicode_emoji(self, entity_factory_impl): + emoji = entity_factory_impl.deserialize_unicode_emoji({"name": "🤷"}) + assert emoji.name == "🤷" + assert isinstance(emoji, emojis.UnicodeEmoji) + + @pytest.fixture() + def custom_emoji_payload(self): + return {"id": "691225175349395456", "name": "test", "animated": True} + + def test_deserialize_custom_emoji(self, entity_factory_impl, mock_app, custom_emoji_payload): + emoji = entity_factory_impl.deserialize_custom_emoji(custom_emoji_payload) + assert emoji._app is mock_app + assert emoji.id == 691225175349395456 + assert emoji.name == "test" + assert emoji.is_animated is True + assert isinstance(emoji, emojis.CustomEmoji) + + def test_deserialize_custom_emoji_with_unset_and_null_fields( + self, entity_factory_impl, mock_app, custom_emoji_payload + ): + emoji = entity_factory_impl.deserialize_custom_emoji({"id": "691225175349395456", "name": None}) + assert emoji.is_animated is False + assert emoji.name is None + + @pytest.fixture() + def known_custom_emoji_payload(self, user_payload): + return { + "id": "12345", + "name": "testing", + "animated": False, + "available": True, + "roles": ["123", "456"], + "user": user_payload, + "require_colons": True, + "managed": False, + } + + def test_deserialize_known_custom_emoji( + self, entity_factory_impl, mock_app, user_payload, known_custom_emoji_payload + ): + emoji = entity_factory_impl.deserialize_known_custom_emoji(known_custom_emoji_payload) + assert emoji._app is mock_app + assert emoji.id == 12345 + assert emoji.name == "testing" + assert emoji.is_animated is False + assert emoji.role_ids == {123, 456} + assert emoji.user == entity_factory_impl.deserialize_user(user_payload) + assert emoji.is_colons_required is True + assert emoji.is_managed is False + assert emoji.is_available is True + assert isinstance(emoji, emojis.KnownCustomEmoji) + + def test_deserialize_known_custom_emoji_with_unset_fields(self, entity_factory_impl): + emoji = entity_factory_impl.deserialize_known_custom_emoji( + { + "id": "12345", + "name": "testing", + "available": True, + "roles": ["123", "456"], + "require_colons": True, + "managed": False, + } + ) + assert emoji.user is None + assert emoji.is_animated is False + + @pytest.mark.parametrize( + ["payload", "expected_type"], + [({"name": "🤷"}, emojis.UnicodeEmoji), ({"id": "1234", "name": "test"}, emojis.CustomEmoji)], + ) + def test_deserialize_emoji_returns_expected_type(self, entity_factory_impl, payload, expected_type): + isinstance(entity_factory_impl.deserialize_emoji(payload), expected_type) + + ########### + # GATEWAY # + ########### + + @pytest.fixture() + def gateway_bot_payload(self): + return { + "url": "wss://gateway.discord.gg", + "shards": 1, + "session_start_limit": {"total": 1000, "remaining": 991, "reset_after": 14170186}, + } + + def test_deserialize_gateway_bot(self, entity_factory_impl, gateway_bot_payload): + gateway_bot = entity_factory_impl.deserialize_gateway_bot(gateway_bot_payload) + assert gateway_bot.url == "wss://gateway.discord.gg" + assert gateway_bot.shard_count == 1 + assert isinstance(gateway_bot, gateway.GatewayBot) + # SessionStartLimit + assert gateway_bot.session_start_limit.total == 1000 + assert gateway_bot.session_start_limit.remaining == 991 + assert gateway_bot.session_start_limit.reset_after == datetime.timedelta(milliseconds=14170186) + assert isinstance(gateway_bot.session_start_limit, gateway.SessionStartLimit) + + ########## + # GUILDS # + ########## + + @pytest.fixture() + def guild_embed_payload(self): + return {"channel_id": "123123123", "enabled": True} + + def test_deserialize_widget_embed(self, entity_factory_impl, mock_app, guild_embed_payload): + guild_embed = entity_factory_impl.deserialize_guild_widget(guild_embed_payload) + assert guild_embed._app is mock_app + assert guild_embed.channel_id == 123123123 + assert guild_embed.is_enabled is True + assert isinstance(guild_embed, guilds.GuildWidget) + + def test_deserialize_guild_embed_with_null_fields(self, entity_factory_impl, mock_app): + assert entity_factory_impl.deserialize_guild_widget({"channel_id": None, "enabled": True}).channel_id is None + + @pytest.fixture() + def guild_member_payload(self, user_payload): + return { + "nick": "foobarbaz", + "roles": ["11111", "22222", "33333", "44444"], + "joined_at": "2015-04-26T06:26:56.936000+00:00", + "premium_since": "2019-05-17T06:26:56.936000+00:00", + # These should be completely ignored. + "deaf": False, + "mute": True, + "user": user_payload, + } + + def test_deserialize_guild_member(self, entity_factory_impl, mock_app, guild_member_payload, user_payload): + member = entity_factory_impl.deserialize_guild_member(guild_member_payload) + assert member._app is mock_app + assert member.user == entity_factory_impl.deserialize_user(user_payload) + assert member.nickname == "foobarbaz" + assert member.role_ids == {11111, 22222, 33333, 44444} + assert member.joined_at == datetime.datetime(2015, 4, 26, 6, 26, 56, 936000, tzinfo=datetime.timezone.utc) + assert member.premium_since == datetime.datetime(2019, 5, 17, 6, 26, 56, 936000, tzinfo=datetime.timezone.utc) + assert member.is_deaf is False + assert member.is_mute is True + assert isinstance(member, guilds.GuildMember) + + @pytest.fixture() + def guild_role_payload(self): + return { + "id": "41771983423143936", + "name": "WE DEM BOYZZ!!!!!!", + "color": 3_447_003, + "hoist": True, + "position": 0, + "permissions": 66_321_471, + "managed": False, + "mentionable": False, + } + + def test_deserialize_role(self, entity_factory_impl, mock_app, guild_role_payload): + guild_role = entity_factory_impl.deserialize_role(guild_role_payload) + assert guild_role._app is mock_app + assert guild_role.id == 41771983423143936 + assert guild_role.name == "WE DEM BOYZZ!!!!!!" + assert guild_role.color == colors.Color(3_447_003) + assert guild_role.is_hoisted is True + assert guild_role.position == 0 + assert guild_role.permissions == permissions.Permission(66_321_471) + assert guild_role.is_managed is False + assert guild_role.is_mentionable is False + assert isinstance(guild_role, guilds.Role) + + @pytest.fixture() + def presence_activity_payload(self, custom_emoji_payload): + return { + "name": "an activity", + "type": 1, + "url": "https://69.420.owouwunyaa", + "created_at": 1584996792798, + "timestamps": {"start": 1584996792798, "end": 1999999792798}, + "application_id": "40404040404040", + "details": "They are doing stuff", + "state": "STATED", + "emoji": custom_emoji_payload, + "party": {"id": "spotify:3234234234", "size": [2, 5]}, + "assets": { + "large_image": "34234234234243", + "large_text": "LARGE TEXT", + "small_image": "3939393", + "small_text": "small text", + }, + "secrets": {"join": "who's a good secret?", "spectate": "I'm a good secret", "match": "No."}, + "instance": True, + "flags": 3, + } + + @pytest.fixture() + def guild_member_presence_payload(self, user_payload, presence_activity_payload): + return { + "user": user_payload, + "roles": ["49494949"], + "game": presence_activity_payload, + "guild_id": "44004040", + "status": "dnd", + "activities": [presence_activity_payload], + "client_status": {"desktop": "online", "mobile": "idle", "web": "dnd"}, + "premium_since": "2015-04-26T06:26:56.936000+00:00", + "nick": "Nick", + } + + def test_deserialize_guild_member_presence( + self, entity_factory_impl, mock_app, guild_member_presence_payload, custom_emoji_payload, user_payload + ): + presence = entity_factory_impl.deserialize_guild_member_presence(guild_member_presence_payload) + assert presence._app is mock_app + # PresenceUser + assert presence.user._app is mock_app + assert presence.user.id == 115590097100865541 + assert presence.user.discriminator == "6127" + assert presence.user.username == "nyaa" + assert presence.user.avatar_hash == "b3b24c6d7cbcdec129d5d537067061a8" + assert presence.user.is_bot is True + assert presence.user.is_system is True + assert presence.user.flags == users.UserFlag(131072) + + assert isinstance(presence.user, guilds.PresenceUser) + assert presence.role_ids == {49494949} + assert presence.guild_id == 44004040 + assert presence.visible_status == guilds.PresenceStatus.DND + # PresenceActivity + assert len(presence.activities) == 1 + activity = presence.activities[0] + assert activity.name == "an activity" + assert activity.type == guilds.ActivityType.STREAMING + assert activity.url == "https://69.420.owouwunyaa" + assert activity.created_at == datetime.datetime(2020, 3, 23, 20, 53, 12, 798000, tzinfo=datetime.timezone.utc) + # ActivityTimestamps + assert activity.timestamps.start == datetime.datetime( + 2020, 3, 23, 20, 53, 12, 798000, tzinfo=datetime.timezone.utc + ) + assert activity.timestamps.end == datetime.datetime( + 2033, 5, 18, 3, 29, 52, 798000, tzinfo=datetime.timezone.utc + ) + + assert activity.application_id == 40404040404040 + assert activity.details == "They are doing stuff" + assert activity.state == "STATED" + assert activity.emoji == entity_factory_impl.deserialize_emoji(custom_emoji_payload) + # ActivityParty + assert activity.party.id == "spotify:3234234234" + assert activity.party.current_size == 2 + assert activity.party.max_size == 5 + assert isinstance(activity.party, guilds.ActivityParty) + # ActivityAssets + assert activity.assets.large_image == "34234234234243" + assert activity.assets.large_text == "LARGE TEXT" + assert activity.assets.small_image == "3939393" + assert activity.assets.small_text == "small text" + assert isinstance(activity.assets, guilds.ActivityAssets) + # ActivitySecrets + assert activity.secrets.join == "who's a good secret?" + assert activity.secrets.spectate == "I'm a good secret" + assert activity.secrets.match == "No." + assert isinstance(activity.secrets, guilds.ActivitySecret) + assert activity.is_instance is True + assert activity.flags == guilds.ActivityFlag(3) + assert isinstance(activity, guilds.PresenceActivity) + + # ClientStatus + assert presence.client_status.desktop == guilds.PresenceStatus.ONLINE + assert presence.client_status.mobile == guilds.PresenceStatus.IDLE + assert presence.client_status.web == guilds.PresenceStatus.DND + assert isinstance(presence.client_status, guilds.ClientStatus) + + assert presence.premium_since == datetime.datetime(2015, 4, 26, 6, 26, 56, 936000, tzinfo=datetime.timezone.utc) + assert presence.nickname == "Nick" + assert isinstance(presence, guilds.GuildMemberPresence) + + def test_deserialize_guild_member_presence_with_null_fields(self, entity_factory_impl, user_payload): + presence = entity_factory_impl.deserialize_guild_member_presence( + { + "user": {"username": "agent 47", "avatar": None, "discriminator": "4747", "id": "474747474"}, + "roles": [], + "game": None, + "guild_id": "42", + "status": "dnd", + "activities": [], + "client_status": {}, + "premium_since": None, + "nick": None, + } + ) + assert presence.premium_since is None + assert presence.nickname is None + # PresenceUser + assert presence.user.avatar_hash is None + + def test_deserialize_guild_member_presence_with_unset_fields( + self, entity_factory_impl, user_payload, presence_activity_payload + ): + presence = entity_factory_impl.deserialize_guild_member_presence( + { + "user": {"id": "42"}, + "game": presence_activity_payload, + "guild_id": "44004040", + "status": "dnd", + "activities": [], + "client_status": {}, + } + ) + assert presence.premium_since is None + assert presence.nickname is None + assert presence.role_ids is None + # ClientStatus + assert presence.client_status.desktop is guilds.PresenceStatus.OFFLINE + assert presence.client_status.mobile is guilds.PresenceStatus.OFFLINE + assert presence.client_status.web is guilds.PresenceStatus.OFFLINE + # PresenceUser + assert presence.user.id == 42 + assert presence.user.discriminator is unset.UNSET + assert presence.user.username is unset.UNSET + assert presence.user.avatar_hash is unset.UNSET + assert presence.user.is_bot is unset.UNSET + assert presence.user.is_system is unset.UNSET + assert presence.user.flags is unset.UNSET + + def test_deserialize_guild_member_presence_with_unset_activity_fields(self, entity_factory_impl, user_payload): + presence = entity_factory_impl.deserialize_guild_member_presence( + { + "user": user_payload, + "roles": ["49494949"], + "game": None, + "guild_id": "44004040", + "status": "dnd", + "activities": [{"name": "an activity", "type": 1, "created_at": 1584996792798,}], + "client_status": {}, + } + ) + assert len(presence.activities) == 1 + activity = presence.activities[0] + assert activity.url is None + assert activity.timestamps is None + assert activity.application_id is None + assert activity.details is None + assert activity.state is None + assert activity.emoji is None + assert activity.party is None + assert activity.assets is None + assert activity.secrets is None + assert activity.is_instance is None + assert activity.flags is None + + def test_deserialize_guild_member_presence_with_null_activity_fields(self, entity_factory_impl, user_payload): + presence = entity_factory_impl.deserialize_guild_member_presence( + { + "user": user_payload, + "roles": ["49494949"], + "game": None, + "guild_id": "44004040", + "status": "dnd", + "activities": [ + { + "name": "an activity", + "type": 1, + "url": None, + "created_at": 1584996792798, + "timestamps": {"start": 1584996792798, "end": 1999999792798,}, + "application_id": "40404040404040", + "details": None, + "state": None, + "emoji": None, + "party": {"id": "spotify:3234234234", "size": [2, 5]}, + "assets": { + "large_image": "34234234234243", + "large_text": "LARGE TEXT", + "small_image": "3939393", + "small_text": "small text", + }, + "secrets": {"join": "who's a good secret?", "spectate": "I'm a good secret", "match": "No."}, + "instance": True, + "flags": 3, + } + ], + "client_status": {}, + } + ) + assert len(presence.activities) == 1 + activity = presence.activities[0] + assert activity.url is None + assert activity.details is None + assert activity.state is None + assert activity.emoji is None + + def test_deserialize_guild_member_presence_with_unset_activity_sub_fields(self, entity_factory_impl, user_payload): + presence = entity_factory_impl.deserialize_guild_member_presence( + { + "user": user_payload, + "roles": ["49494949"], + "game": None, + "guild_id": "44004040", + "status": "dnd", + "activities": [ + { + "name": "an activity", + "type": 1, + "url": "https://69.420.owouwunyaa", + "created_at": 1584996792798, + "timestamps": {}, + "application_id": "40404040404040", + "details": "They are doing stuff", + "state": "STATED", + "emoji": None, + "party": {}, + "assets": {}, + "secrets": {}, + "instance": True, + "flags": 3, + } + ], + "client_status": {}, + } + ) + activity = presence.activities[0] + # ActivityTimestamps + assert activity.timestamps.start is None + assert activity.timestamps.end is None + # ActivityParty + assert activity.party.id is None + assert activity.party.max_size is None + assert activity.party.current_size is None + # ActivityAssets + assert activity.assets.small_text is None + assert activity.assets.small_image is None + assert activity.assets.large_text is None + assert activity.assets.large_image is None + # ActivitySecrets + assert activity.secrets.join is None + assert activity.secrets.spectate is None + assert activity.secrets.match is None + + @pytest.fixture() + def partial_integration_payload(self): + return { + "id": "4949494949", + "name": "Blah blah", + "type": "twitch", + "account": {"id": "543453", "name": "Blam"}, + } + + def test_deserialize_partial_integration(self, entity_factory_impl, partial_integration_payload): + partial_integration = entity_factory_impl.deserialize_partial_integration(partial_integration_payload) + assert partial_integration.id == 4949494949 + assert partial_integration.name == "Blah blah" + assert partial_integration.type == "twitch" + assert isinstance(partial_integration, guilds.PartialIntegration) + # IntegrationAccount + assert partial_integration.account.id == "543453" + assert partial_integration.account.name == "Blam" + assert isinstance(partial_integration.account, guilds.IntegrationAccount) + + @pytest.fixture() + def integration_payload(self, user_payload): + return { + "id": "420", + "name": "blaze it", + "type": "youtube", + "account": {"id": "6969", "name": "Blaze it"}, + "enabled": True, + "syncing": False, + "role_id": "98494949", + "enable_emoticons": False, + "expire_behavior": 1, + "expire_grace_period": 7, + "user": user_payload, + "synced_at": "2015-04-26T06:26:56.936000+00:00", + } + + def test_deserialize_integration(self, entity_factory_impl, integration_payload, user_payload): + integration = entity_factory_impl.deserialize_integration(integration_payload) + assert integration.id == 420 + assert integration.name == "blaze it" + assert integration.type == "youtube" + # IntegrationAccount + assert integration.account.id == "6969" + assert integration.account.name == "Blaze it" + assert isinstance(integration.account, guilds.IntegrationAccount) + + assert integration.is_enabled is True + assert integration.is_syncing is False + assert integration.role_id == 98494949 + assert integration.is_emojis_enabled is False + assert integration.expire_behavior == guilds.IntegrationExpireBehaviour.KICK + assert integration.expire_grace_period == datetime.timedelta(days=7) + assert integration.user == entity_factory_impl.deserialize_user(user_payload) + assert integration.last_synced_at == datetime.datetime( + 2015, 4, 26, 6, 26, 56, 936000, tzinfo=datetime.timezone.utc + ) + assert isinstance(integration, guilds.Integration) + + def test_deserialize_guild_integration_with_null_and_unset_fields(self, entity_factory_impl, user_payload): + integration = entity_factory_impl.deserialize_integration( + { + "id": "420", + "name": "blaze it", + "type": "youtube", + "account": {"id": "6969", "name": "Blaze it"}, + "enabled": True, + "syncing": False, + "role_id": "98494949", + "expire_behavior": 1, + "expire_grace_period": 7, + "user": user_payload, + "synced_at": None, + } + ) + assert integration.is_emojis_enabled is None + assert integration.last_synced_at is None + + @pytest.fixture() + def guild_member_ban_payload(self, user_payload): + return {"reason": "Get nyaa'ed", "user": user_payload} + + def test_deserialize_guild_member_ban(self, entity_factory_impl, guild_member_ban_payload, user_payload): + member_ban = entity_factory_impl.deserialize_guild_member_ban(guild_member_ban_payload) + assert member_ban.reason == "Get nyaa'ed" + assert member_ban.user == entity_factory_impl.deserialize_user(user_payload) + assert isinstance(member_ban, guilds.GuildMemberBan) + + def test_deserialize_guild_member_ban_with_null_fields(self, entity_factory_impl, user_payload): + assert entity_factory_impl.deserialize_guild_member_ban({"reason": None, "user": user_payload}).reason is None + + def test_deserialize_unavailable_guild(self, entity_factory_impl, mock_app): + unavailable_guild = entity_factory_impl.deserialize_unavailable_guild({"id": "42069", "unavailable": True}) + assert unavailable_guild._app is mock_app + assert unavailable_guild.id == 42069 + assert unavailable_guild.is_unavailable is True + assert isinstance(unavailable_guild, guilds.UnavailableGuild) + + @pytest.fixture() + def guild_preview_payload(self, known_custom_emoji_payload): + return { + "id": "152559372126519269", + "name": "Isopropyl", + "icon": "d4a983885dsaa7691ce8bcaaf945a", + "splash": "dsa345tfcdg54b", + "discovery_splash": "lkodwaidi09239uid", + "emojis": [known_custom_emoji_payload], + "features": ["DISCOVERABLE", "FORCE_RELAY"], + "approximate_member_count": 69, + "approximate_presence_count": 42, + "description": "A DESCRIPTION.", + } + + def test_deserialize_guild_preview( + self, entity_factory_impl, mock_app, guild_preview_payload, known_custom_emoji_payload + ): + guild_preview = entity_factory_impl.deserialize_guild_preview(guild_preview_payload) + assert guild_preview._app is mock_app + assert guild_preview.id == 152559372126519269 + assert guild_preview.name == "Isopropyl" + assert guild_preview.icon_hash == "d4a983885dsaa7691ce8bcaaf945a" + assert guild_preview.features == {guilds.GuildFeature.DISCOVERABLE, "FORCE_RELAY"} + assert guild_preview.splash_hash == "dsa345tfcdg54b" + assert guild_preview.discovery_splash_hash == "lkodwaidi09239uid" + assert guild_preview.emojis == { + 12345: entity_factory_impl.deserialize_known_custom_emoji(known_custom_emoji_payload) + } + assert guild_preview.approximate_member_count == 69 + assert guild_preview.approximate_presence_count == 42 + assert guild_preview.description == "A DESCRIPTION." + assert isinstance(guild_preview, guilds.GuildPreview) + + def test_deserialize_guild_preview_with_null_fields(self, entity_factory_impl, mock_app, guild_preview_payload): + guild_preview = entity_factory_impl.deserialize_guild_preview( + { + "id": "152559372126519269", + "name": "Isopropyl", + "icon": None, + "splash": None, + "discovery_splash": None, + "emojis": [], + "features": ["DISCOVERABLE", "FORCE_RELAY"], + "approximate_member_count": 69, + "approximate_presence_count": 42, + "description": None, + } + ) + assert guild_preview.icon_hash is None + assert guild_preview.splash_hash is None + assert guild_preview.discovery_splash_hash is None + assert guild_preview.description is None + + @pytest.fixture() + def guild_payload( + self, + guild_text_channel_payload, + guild_voice_channel_payload, + known_custom_emoji_payload, + guild_member_payload, + guild_member_presence_payload, + guild_role_payload, + voice_state_payload, + ): + return { + "afk_channel_id": "99998888777766", + "afk_timeout": 1200, + "application_id": "39494949", + "approximate_member_count": 15, + "approximate_presence_count": 7, + "banner": "1a2b3c", + "channels": [guild_text_channel_payload, guild_voice_channel_payload], + "default_message_notifications": 1, + "description": "This is a server I guess, its a bit crap though", + "discovery_splash": "famfamFAMFAMfam", + "embed_channel_id": "9439394949", + "embed_enabled": True, + "emojis": [known_custom_emoji_payload], + "explicit_content_filter": 2, + "features": ["ANIMATED_ICON", "MORE_EMOJI", "NEWS", "SOME_UNDOCUMENTED_FEATURE"], + "icon": "1a2b3c4d", + "id": "265828729970753537", + "joined_at": "2019-05-17T06:26:56.936000+00:00", + "large": False, + "max_members": 25000, + "max_presences": 250, + "max_video_channel_users": 25, + "member_count": 14, + "members": [guild_member_payload], + "mfa_level": 1, + "name": "L33t guild", + "owner_id": "6969696", + "permissions": 66_321_471, + "preferred_locale": "en-GB", + "premium_subscription_count": 1, + "premium_tier": 2, + "presences": [guild_member_presence_payload], + "public_updates_channel_id": "33333333", + "region": "eu-central", + "roles": [guild_role_payload], + "rules_channel_id": "42042069", + "splash": "0ff0ff0ff", + "system_channel_flags": 3, + "system_channel_id": "19216801", + "unavailable": False, + "vanity_url_code": "loool", + "verification_level": 4, + "voice_states": [voice_state_payload], + "widget_channel_id": "9439394949", + "widget_enabled": True, + } + + def test_deserialize_guild( + self, + entity_factory_impl, + mock_app, + guild_payload, + guild_text_channel_payload, + guild_voice_channel_payload, + known_custom_emoji_payload, + guild_member_payload, + guild_member_presence_payload, + guild_role_payload, + voice_state_payload, + ): + guild = entity_factory_impl.deserialize_guild(guild_payload) + assert guild._app is mock_app + assert guild.id == 265828729970753537 + assert guild.name == "L33t guild" + assert guild.icon_hash == "1a2b3c4d" + assert guild.features == { + guilds.GuildFeature.ANIMATED_ICON, + guilds.GuildFeature.MORE_EMOJI, + guilds.GuildFeature.NEWS, + "SOME_UNDOCUMENTED_FEATURE", + } + assert guild.splash_hash == "0ff0ff0ff" + assert guild.discovery_splash_hash == "famfamFAMFAMfam" + assert guild.owner_id == 6969696 + assert guild.my_permissions == permissions.Permission(66_321_471) + assert guild.region == "eu-central" + assert guild.afk_channel_id == 99998888777766 + assert guild.afk_timeout == datetime.timedelta(seconds=1200) + assert guild.is_embed_enabled is True + assert guild.embed_channel_id == 9439394949 + assert guild.verification_level == guilds.GuildVerificationLevel.VERY_HIGH + assert guild.default_message_notifications == guilds.GuildMessageNotificationsLevel.ONLY_MENTIONS + assert guild.explicit_content_filter == guilds.GuildExplicitContentFilterLevel.ALL_MEMBERS + assert guild.roles == {41771983423143936: entity_factory_impl.deserialize_role(guild_role_payload)} + assert guild.emojis == {12345: entity_factory_impl.deserialize_known_custom_emoji(known_custom_emoji_payload)} + assert guild.mfa_level == guilds.GuildMFALevel.ELEVATED + assert guild.application_id == 39494949 + assert guild.is_unavailable is False + assert guild.widget_channel_id == 9439394949 + assert guild.is_widget_enabled is True + assert guild.system_channel_id == 19216801 + assert guild.system_channel_flags == guilds.GuildSystemChannelFlag(3) + assert guild.rules_channel_id == 42042069 + assert guild.joined_at == datetime.datetime(2019, 5, 17, 6, 26, 56, 936000, tzinfo=datetime.timezone.utc) + assert guild.is_large is False + assert guild.member_count == 14 + assert guild.members == {115590097100865541: entity_factory_impl.deserialize_guild_member(guild_member_payload)} + assert guild.channels == { + 123: entity_factory_impl.deserialize_guild_text_channel(guild_text_channel_payload), + 555: entity_factory_impl.deserialize_guild_voice_channel(guild_voice_channel_payload), + } + assert guild.presences == { + 115590097100865541: entity_factory_impl.deserialize_guild_member_presence(guild_member_presence_payload) + } + assert guild.max_presences == 250 + assert guild.max_members == 25000 + assert guild.max_video_channel_users == 25 + assert guild.vanity_url_code == "loool" + assert guild.description == "This is a server I guess, its a bit crap though" + assert guild.banner_hash == "1a2b3c" + assert guild.premium_tier == guilds.GuildPremiumTier.TIER_2 + assert guild.premium_subscription_count == 1 + assert guild.preferred_locale == "en-GB" + assert guild.public_updates_channel_id == 33333333 + assert guild.approximate_member_count == 15 + assert guild.approximate_active_member_count == 7 + + def test_deserialize_guild_with_unset_fields(self, entity_factory_impl): + guild = entity_factory_impl.deserialize_guild( + { + "afk_channel_id": "99998888777766", + "afk_timeout": 1200, + "application_id": "39494949", + "banner": "1a2b3c", + "default_message_notifications": 1, + "description": "This is a server I guess, its a bit crap though", + "discovery_splash": "famfamFAMFAMfam", + "emojis": [], + "explicit_content_filter": 2, + "features": ["ANIMATED_ICON", "MORE_EMOJI", "NEWS", "SOME_UNDOCUMENTED_FEATURE"], + "icon": "1a2b3c4d", + "id": "265828729970753537", + "mfa_level": 1, + "name": "L33t guild", + "owner_id": "6969696", + "preferred_locale": "en-GB", + "premium_tier": 2, + "public_updates_channel_id": "33333333", + "region": "eu-central", + "roles": [], + "rules_channel_id": "42042069", + "splash": "0ff0ff0ff", + "system_channel_flags": 3, + "system_channel_id": "19216801", + "vanity_url_code": "loool", + "verification_level": 4, + } + ) + assert guild.channels is None + assert guild.embed_channel_id is None + assert guild.is_embed_enabled is False + assert guild.joined_at is None + assert guild.is_large is None + assert guild.max_members is None + assert guild.max_presences is None + assert guild.max_video_channel_users is None + assert guild.member_count is None + assert guild.members is None + assert guild.my_permissions is None + assert guild.premium_subscription_count is None + assert guild.presences is None + assert guild.is_unavailable is None + assert guild.widget_channel_id is None + assert guild.is_widget_enabled is None + assert guild.approximate_active_member_count is None + assert guild.approximate_active_member_count is None + + def test_deserialize_guild_with_null_fields(self, entity_factory_impl): + guild = entity_factory_impl.deserialize_guild( + { + "afk_channel_id": None, + "afk_timeout": 1200, + "application_id": None, + "approximate_member_count": 15, + "approximate_presence_count": 7, + "banner": None, + "channels": [], + "default_message_notifications": 1, + "description": None, + "discovery_splash": None, + "embed_channel_id": None, + "embed_enabled": True, + "emojis": [], + "explicit_content_filter": 2, + "features": ["ANIMATED_ICON", "MORE_EMOJI", "NEWS", "SOME_UNDOCUMENTED_FEATURE"], + "icon": None, + "id": "265828729970753537", + "joined_at": "2019-05-17T06:26:56.936000+00:00", + "large": False, + "max_members": 25000, + "max_presences": None, + "max_video_channel_users": 25, + "member_count": 14, + "members": [], + "mfa_level": 1, + "name": "L33t guild", + "owner_id": "6969696", + "permissions": 66_321_471, + "preferred_locale": "en-GB", + "premium_subscription_count": None, + "premium_tier": 2, + "presences": [], + "public_updates_channel_id": None, + "region": "eu-central", + "roles": [], + "rules_channel_id": None, + "splash": None, + "system_channel_flags": 3, + "system_channel_id": None, + "unavailable": False, + "vanity_url_code": None, + "verification_level": 4, + "voice_states": [], + "widget_channel_id": None, + "widget_enabled": True, + } + ) + assert guild.icon_hash is None + assert guild.splash_hash is None + assert guild.discovery_splash_url is None + assert guild.afk_channel_id is None + assert guild.embed_channel_id is None + assert guild.application_id is None + assert guild.widget_channel_id is None + assert guild.system_channel_id is None + assert guild.rules_channel_id is None + assert guild.max_presences is None + assert guild.vanity_url_code is None + assert guild.description is None + assert guild.banner_hash is None + assert guild.premium_subscription_count is None + assert guild.public_updates_channel_id is None + + ########### + # INVITES # + ########### + + @pytest.fixture() + def vanity_url_payload(self): + return {"code": "iamacode", "uses": 42} + + def test_deserialize_vanity_url(self, entity_factory_impl, mock_app, vanity_url_payload): + vanity_url = entity_factory_impl.deserialize_vanity_url(vanity_url_payload) + assert vanity_url._app is mock_app + assert vanity_url.code == "iamacode" + assert vanity_url.uses == 42 + assert isinstance(vanity_url, invites.VanityURL) + + @pytest.fixture() + def alternative_user_payload(self): + return {"id": "1231231", "username": "soad", "discriminator": "3333", "avatar": None} + + @pytest.fixture() + def invite_payload(self, partial_channel_payload, user_payload, alternative_user_payload): + return { + "code": "aCode", + "guild": { + "id": "56188492224814744", + "name": "Testin' Your Scene", + "splash": "aSplashForSure", + "banner": "aBannerForSure", + "description": "Describe me cute kitty.", + "icon": "bb71f469c158984e265093a81b3397fb", + "features": ["FORCE_RELAY"], + "verification_level": 2, + "vanity_url_code": "I-am-very-vain", + }, + "channel": partial_channel_payload, + "inviter": user_payload, + "target_user": alternative_user_payload, + "target_user_type": 1, + "approximate_presence_count": 42, + "approximate_member_count": 84, + } + + def test_deserialize_invite( + self, + entity_factory_impl, + mock_app, + invite_payload, + partial_channel_payload, + user_payload, + alternative_user_payload, + ): + invite = entity_factory_impl.deserialize_invite(invite_payload) + assert invite._app is mock_app + assert invite.code == "aCode" + # InviteGuild + assert invite.guild.id == 56188492224814744 + assert invite.guild.name == "Testin' Your Scene" + assert invite.guild.icon_hash == "bb71f469c158984e265093a81b3397fb" + assert invite.guild.features == {"FORCE_RELAY"} + assert invite.guild.splash_hash == "aSplashForSure" + assert invite.guild.banner_hash == "aBannerForSure" + assert invite.guild.description == "Describe me cute kitty." + assert invite.guild.verification_level == guilds.GuildVerificationLevel.MEDIUM + assert invite.guild.vanity_url_code == "I-am-very-vain" + + assert invite.channel == entity_factory_impl.deserialize_partial_channel(partial_channel_payload) + assert invite.inviter == entity_factory_impl.deserialize_user(user_payload) + assert invite.target_user == entity_factory_impl.deserialize_user(alternative_user_payload) + assert invite.target_user_type == invites.TargetUserType.STREAM + assert invite.approximate_member_count == 84 + assert invite.approximate_presence_count == 42 + assert isinstance(invite, invites.Invite) + + def test_deserialize_invite_with_null_and_unset_fields(self, entity_factory_impl, partial_channel_payload): + invite = entity_factory_impl.deserialize_invite({"code": "aCode", "channel": partial_channel_payload}) + assert invite.guild is None + assert invite.inviter is None + assert invite.target_user is None + assert invite.target_user_type is None + assert invite.approximate_presence_count is None + assert invite.approximate_member_count is None + + @pytest.fixture() + def invite_with_metadata_payload(self, partial_channel_payload, user_payload, alternative_user_payload): + return { + "code": "aCode", + "guild": { + "id": "56188492224814744", + "name": "Testin' Your Scene", + "splash": "aSplashForSure", + "banner": "aBannerForSure", + "description": "Describe me cute kitty.", + "icon": "bb71f469c158984e265093a81b3397fb", + "features": ["FORCE_RELAY"], + "verification_level": 2, + "vanity_url_code": "I-am-very-vain", + }, + "channel": partial_channel_payload, + "inviter": user_payload, + "target_user": alternative_user_payload, + "target_user_type": 1, + "approximate_presence_count": 42, + "approximate_member_count": 84, + "uses": 3, + "max_uses": 8, + "max_age": 239349393, + "temporary": True, + "created_at": "2015-04-26T06:26:56.936000+00:00", + } + + def test_deserialize_invite_with_metadata( + self, + entity_factory_impl, + mock_app, + invite_with_metadata_payload, + partial_channel_payload, + user_payload, + alternative_user_payload, + ): + invite_with_metadata = entity_factory_impl.deserialize_invite_with_metadata(invite_with_metadata_payload) + assert invite_with_metadata._app is mock_app + assert invite_with_metadata.code == "aCode" + # InviteGuild + assert invite_with_metadata.guild.id == 56188492224814744 + assert invite_with_metadata.guild.name == "Testin' Your Scene" + assert invite_with_metadata.guild.icon_hash == "bb71f469c158984e265093a81b3397fb" + assert invite_with_metadata.guild.features == {"FORCE_RELAY"} + assert invite_with_metadata.guild.splash_hash == "aSplashForSure" + assert invite_with_metadata.guild.banner_hash == "aBannerForSure" + assert invite_with_metadata.guild.description == "Describe me cute kitty." + assert invite_with_metadata.guild.verification_level == guilds.GuildVerificationLevel.MEDIUM + assert invite_with_metadata.guild.vanity_url_code == "I-am-very-vain" + + assert invite_with_metadata.channel == entity_factory_impl.deserialize_partial_channel(partial_channel_payload) + assert invite_with_metadata.inviter == entity_factory_impl.deserialize_user(user_payload) + assert invite_with_metadata.target_user == entity_factory_impl.deserialize_user(alternative_user_payload) + assert invite_with_metadata.target_user_type == invites.TargetUserType.STREAM + assert invite_with_metadata.approximate_member_count == 84 + assert invite_with_metadata.approximate_presence_count == 42 + assert invite_with_metadata.uses == 3 + assert invite_with_metadata.max_uses == 8 + assert invite_with_metadata.max_age == datetime.timedelta(seconds=239349393) + assert invite_with_metadata.is_temporary is True + assert invite_with_metadata.created_at == datetime.datetime( + 2015, 4, 26, 6, 26, 56, 936000, tzinfo=datetime.timezone.utc + ) + assert isinstance(invite_with_metadata, invites.InviteWithMetadata) + + def test_deserialize_invite_with_metadata_with_null_and_unset_fields( + self, entity_factory_impl, partial_channel_payload + ): + invite_with_metadata = entity_factory_impl.deserialize_invite( + { + "code": "aCode", + "channel": partial_channel_payload, + "uses": 42, + "max_uses": 0, + "max_age": 0, + "temporary": True, + "created_at": "2015-04-26T06:26:56.936000+00:00", + } + ) + assert invite_with_metadata.guild is None + assert invite_with_metadata.inviter is None + assert invite_with_metadata.target_user is None + assert invite_with_metadata.target_user_type is None + assert invite_with_metadata.approximate_presence_count is None + assert invite_with_metadata.approximate_member_count is None + + def test_max_age_when_zero(self, entity_factory_impl, invite_with_metadata_payload): + invite_with_metadata_payload["max_age"] = 0 + assert entity_factory_impl.deserialize_invite_with_metadata(invite_with_metadata_payload).max_age is None + + ############ + # MESSAGES # + ############ + + @pytest.fixture() + def partial_application_payload(self): + return { + "id": "456", + "name": "hikari", + "description": "The best application", + "icon": "2658b3029e775a931ffb49380073fa63", + "cover_image": "58982a23790c4f22787b05d3be38a026", + "summary": "asas", + } + + @pytest.fixture() + def message_payload( + self, user_payload, guild_member_payload, custom_emoji_payload, partial_application_payload, embed_payload + ): + del guild_member_payload["user"] + return { + "id": "123", + "channel_id": "456", + "guild_id": "678", + "author": user_payload, + "member": guild_member_payload, + "content": "some info", + "timestamp": "2020-03-21T21:20:16.510000+00:00", + "edited_timestamp": "2020-04-21T21:20:16.510000+00:00", + "tts": True, + "mention_everyone": True, + "mentions": [ + {"id": "5678", "username": "uncool username", "avatar": "129387dskjafhasf", "discriminator": "4532"} + ], + "mention_roles": ["987"], + "mention_channels": [{"id": "456", "guild_id": "678", "type": 1, "name": "hikari-testing"}], + "attachments": [ + { + "id": "690922406474154014", + "filename": "IMG.jpg", + "size": 660521, + "url": "https://somewhere.com/attachments/123/456/IMG.jpg", + "proxy_url": "https://media.somewhere.com/attachments/123/456/IMG.jpg", + "width": 1844, + "height": 2638, + } + ], + "embeds": [embed_payload], + "reactions": [{"emoji": custom_emoji_payload, "count": 100, "me": True}], + "pinned": True, + "webhook_id": "1234", + "type": 0, + "activity": {"type": 5, "party_id": "ae488379-351d-4a4f-ad32-2b9b01c91657"}, + "application": partial_application_payload, + "message_reference": { + "channel_id": "278325129692446722", + "guild_id": "278325129692446720", + "message_id": "306588351130107906", + }, + "flags": 2, + "nonce": "171000788183678976", + } + + def test_deserialize_full_message( + self, + entity_factory_impl, + mock_app, + message_payload, + user_payload, + guild_member_payload, + partial_application_payload, + custom_emoji_payload, + embed_payload, + ): + message = entity_factory_impl.deserialize_message(message_payload) + assert message._app is mock_app + assert message.id == 123 + assert message.channel_id == 456 + assert message.guild_id == 678 + assert message.author == entity_factory_impl.deserialize_user(user_payload) + assert message.member == entity_factory_impl.deserialize_guild_member(guild_member_payload, user=message.author) + assert message.content == "some info" + assert message.timestamp == datetime.datetime(2020, 3, 21, 21, 20, 16, 510000, tzinfo=datetime.timezone.utc) + assert message.edited_timestamp == datetime.datetime( + 2020, 4, 21, 21, 20, 16, 510000, tzinfo=datetime.timezone.utc + ) + assert message.is_tts is True + assert message.is_mentioning_everyone is True + assert message.user_mentions == {5678} + assert message.role_mentions == {987} + assert message.channel_mentions == {456} + # Attachment + assert len(message.attachments) == 1 + attachment = message.attachments[0] + assert attachment.id == 690922406474154014 + assert attachment.filename == "IMG.jpg" + assert attachment.size == 660521 + assert attachment.url == "https://somewhere.com/attachments/123/456/IMG.jpg" + assert attachment.proxy_url == "https://media.somewhere.com/attachments/123/456/IMG.jpg" + assert attachment.width == 1844 + assert attachment.height == 2638 + assert isinstance(attachment, messages.Attachment) + + assert message.embeds == [entity_factory_impl.deserialize_embed(embed_payload)] + # Reaction + reaction = message.reactions[0] + assert reaction.count == 100 + assert reaction.is_reacted_by_me is True + assert reaction.emoji == entity_factory_impl.deserialize_emoji(custom_emoji_payload) + assert isinstance(reaction, messages.Reaction) + + assert message.is_pinned is True + assert message.webhook_id == 1234 + assert message.type == messages.MessageType.DEFAULT + # Activity + assert message.activity.type == messages.MessageActivityType.JOIN_REQUEST + assert message.activity.party_id == "ae488379-351d-4a4f-ad32-2b9b01c91657" + assert isinstance(message.activity, messages.MessageActivity) + + assert message.application == entity_factory_impl.deserialize_application(partial_application_payload) + # MessageCrosspost + assert message.message_reference._app is mock_app + assert message.message_reference.id == 306588351130107906 + assert message.message_reference.channel_id == 278325129692446722 + assert message.message_reference.guild_id == 278325129692446720 + + assert message.flags == messages.MessageFlag.IS_CROSSPOST + assert message.nonce == "171000788183678976" + + def test_deserialize_message_with_null_and_unset_fields( + self, entity_factory_impl, mock_app, user_payload, + ): + message_payload = { + "id": "123", + "channel_id": "456", + "author": user_payload, + "content": "some info", + "timestamp": "2020-03-21T21:20:16.510000+00:00", + "edited_timestamp": None, + "tts": True, + "mention_everyone": True, + "mentions": [], + "mention_roles": [], + "attachments": [], + "embeds": [], + "pinned": True, + "type": 0, + } + + message = entity_factory_impl.deserialize_message(message_payload) + assert message._app is mock_app + assert message.guild_id is None + assert message.member is None + assert message.edited_timestamp is None + assert message.channel_mentions == set() + assert message.role_mentions == set() + assert message.channel_mentions == set() + assert message.attachments == [] + assert message.embeds == [] + assert message.reactions == [] + assert message.webhook_id is None + assert message.activity is None + assert message.application is None + assert message.message_reference is None + assert message.nonce is None + + ######### + # USERS # + ######### + + @pytest.fixture() + def user_payload(self): + return { + "id": "115590097100865541", + "username": "nyaa", + "avatar": "b3b24c6d7cbcdec129d5d537067061a8", + "discriminator": "6127", + "bot": True, + "system": True, + "public_flags": int(users.UserFlag.VERIFIED_BOT_DEVELOPER), + } + + def test_deserialize_user(self, entity_factory_impl, mock_app, user_payload): + user = entity_factory_impl.deserialize_user(user_payload) + assert user._app is mock_app + assert user.id == 115590097100865541 + assert user.username == "nyaa" + assert user.avatar_hash == "b3b24c6d7cbcdec129d5d537067061a8" + assert user.discriminator == "6127" + assert user.is_bot is True + assert user.is_system is True + assert user.flags == users.UserFlag.VERIFIED_BOT_DEVELOPER + assert isinstance(user, users.User) + + def test_deserialize_user_with_unset_fields(self, entity_factory_impl, mock_app, user_payload): + user = entity_factory_impl.deserialize_user( + { + "id": "115590097100865541", + "username": "nyaa", + "avatar": "b3b24c6d7cbcdec129d5d537067061a8", + "discriminator": "6127", + } + ) + assert user.is_bot is False + assert user.is_system is False + assert user.flags == users.UserFlag.NONE + + @pytest.fixture() + def my_user_payload(self): + return { + "id": "379953393319542784", + "username": "qt pi", + "avatar": "820d0e50543216e812ad94e6ab7", + "discriminator": "2880", + "bot": True, + "system": True, + "email": "blahblah@blah.blah", + "verified": True, + "locale": "en-US", + "mfa_enabled": True, + "public_flags": int(users.UserFlag.VERIFIED_BOT_DEVELOPER), + "flags": int(users.UserFlag.DISCORD_PARTNER | users.UserFlag.DISCORD_EMPLOYEE), + "premium_type": 1, + } + + def test_deserialize_my_user(self, entity_factory_impl, mock_app, my_user_payload): + my_user = entity_factory_impl.deserialize_my_user(my_user_payload) + assert my_user._app is mock_app + assert my_user.id == 379953393319542784 + assert my_user.username == "qt pi" + assert my_user.avatar_hash == "820d0e50543216e812ad94e6ab7" + assert my_user.discriminator == "2880" + assert my_user.is_bot is True + assert my_user.is_system is True + assert my_user.is_mfa_enabled is True + assert my_user.locale == "en-US" + assert my_user.is_verified is True + assert my_user.email == "blahblah@blah.blah" + assert my_user.flags == users.UserFlag.DISCORD_PARTNER | users.UserFlag.DISCORD_EMPLOYEE + assert my_user.premium_type is users.PremiumType.NITRO_CLASSIC + assert isinstance(my_user, users.MyUser) + + def test_deserialize_my_user_with_unset_fields(self, entity_factory_impl, mock_app, my_user_payload): + my_user = entity_factory_impl.deserialize_my_user( + { + "id": "379953393319542784", + "username": "qt pi", + "avatar": "820d0e50543216e812ad94e6ab7", + "discriminator": "2880", + "locale": "en-US", + "mfa_enabled": True, + "public_flags": int(users.UserFlag.VERIFIED_BOT_DEVELOPER), + "flags": int(users.UserFlag.DISCORD_PARTNER | users.UserFlag.DISCORD_EMPLOYEE), + "premium_type": 1, + } + ) + assert my_user._app is mock_app + assert my_user.is_bot is False + assert my_user.is_system is False + assert my_user.is_verified is None + assert my_user.email is None + assert isinstance(my_user, users.MyUser) + + ########## + # Voices # + ########## + + @pytest.fixture() + def voice_state_payload(self, guild_member_payload): + return { + "guild_id": "929292929292992", + "channel_id": "157733188964188161", + "user_id": "80351110224678912", + "member": guild_member_payload, + "session_id": "90326bd25d71d39b9ef95b299e3872ff", + "deaf": True, + "mute": True, + "self_deaf": False, + "self_mute": True, + "self_stream": True, + "suppress": False, + } + + def test_deserialize_voice_state(self, entity_factory_impl, mock_app, voice_state_payload, guild_member_payload): + voice_state = entity_factory_impl.deserialize_voice_state(voice_state_payload) + assert voice_state._app is mock_app + assert voice_state.guild_id == 929292929292992 + assert voice_state.channel_id == 157733188964188161 + assert voice_state.user_id == 80351110224678912 + assert voice_state.member == entity_factory_impl.deserialize_guild_member(guild_member_payload) + assert voice_state.session_id == "90326bd25d71d39b9ef95b299e3872ff" + assert voice_state.is_guild_deafened is True + assert voice_state.is_guild_muted is True + assert voice_state.is_self_deafened is False + assert voice_state.is_self_muted is True + assert voice_state.is_streaming is True + assert voice_state.is_suppressed is False + assert isinstance(voice_state, voices.VoiceState) + + def test_deserialize_voice_state_with_null_and_unset_fields(self, entity_factory_impl): + voice_state = entity_factory_impl.deserialize_voice_state( + { + "channel_id": None, + "user_id": "80351110224678912", + "session_id": "90326bd25d71d39b9ef95b299e3872ff", + "deaf": True, + "mute": True, + "self_deaf": False, + "self_mute": True, + "suppress": False, + } + ) + assert voice_state.guild_id is None + assert voice_state.channel_id is None + assert voice_state.member is None + assert voice_state.is_streaming is False + + @pytest.fixture() + def voice_region_payload(self): + return {"id": "london", "name": "LONDON", "vip": True, "optimal": False, "deprecated": True, "custom": False} + + def test_deserialize_voice_region(self, entity_factory_impl, voice_region_payload): + voice_region = entity_factory_impl.deserialize_voice_region(voice_region_payload) + assert voice_region.id == "london" + assert voice_region.name == "LONDON" + assert voice_region.is_vip is True + assert voice_region.is_optimal_location is False + assert voice_region.is_deprecated is True + assert voice_region.is_custom is False + assert isinstance(voice_region, voices.VoiceRegion) + + ############ + # WEBHOOKS # + ############ + + @pytest.fixture() + def webhook_payload(self, user_payload): + return { + "id": "1234", + "type": 1, + "guild_id": "123", + "channel_id": "456", + "user": user_payload, + "name": "hikari webhook", + "avatar": "bb71f469c158984e265093a81b3397fb", + "token": "ueoqrialsdfaKJLKfajslkdf", + } + + def test_deserialize_webhook(self, entity_factory_impl, mock_app, webhook_payload, user_payload): + webhook = entity_factory_impl.deserialize_webhook(webhook_payload) + assert webhook.id == 1234 + assert webhook.type == webhooks.WebhookType.INCOMING + assert webhook.guild_id == 123 + assert webhook.channel_id == 456 + assert webhook.author == entity_factory_impl.deserialize_user(user_payload) + assert webhook.name == "hikari webhook" + assert webhook.avatar_hash == "bb71f469c158984e265093a81b3397fb" + assert webhook.token == "ueoqrialsdfaKJLKfajslkdf" + assert isinstance(webhook, webhooks.Webhook) + + def test_deserialize_webhook_with_null_and_unset_fields(self, entity_factory_impl): + webhook = entity_factory_impl.deserialize_webhook( + {"id": "1234", "type": 1, "channel_id": "456", "name": None, "avatar": None} + ) + assert webhook.guild_id is None + assert webhook.author is None + assert webhook.name is None + assert webhook.avatar_hash is None + assert webhook.token is None From d59506edef6fef82702b2a051c2437cf5ca2b306 Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Sat, 30 May 2020 11:50:03 +0100 Subject: [PATCH 411/922] Renamed 'unset' to 'Undefined' and changed how it is used a little. --- hikari/__main__.py | 1 + hikari/app.py | 3 +- hikari/bot.py | 5 +- hikari/component.py | 2 + hikari/event_dispatcher.py | 6 +- hikari/events/guild.py | 6 +- hikari/events/message.py | 46 ++- hikari/impl/entity_factory.py | 16 +- hikari/impl/gateway_zookeeper.py | 10 +- hikari/models/embeds.py | 1 + hikari/models/files.py | 1 - hikari/models/guilds.py | 37 +- hikari/models/messages.py | 1 + hikari/net/gateway.py | 37 +- hikari/net/iterators.py | 6 +- hikari/net/rest.py | 438 ++++++++++++---------- hikari/net/rest_utils.py | 70 ++-- hikari/utilities/binding.py | 34 +- hikari/utilities/klass.py | 5 + hikari/utilities/undefined.py | 113 ++++++ hikari/utilities/unset.py | 76 ---- tests/hikari/impl/test_entity_factory.py | 14 +- tests/hikari/internal/test_conversions.py | 4 +- tests/hikari/internal/test_unset.py | 18 +- 24 files changed, 522 insertions(+), 428 deletions(-) create mode 100644 hikari/utilities/undefined.py delete mode 100644 hikari/utilities/unset.py diff --git a/hikari/__main__.py b/hikari/__main__.py index 3da0fae356..0a7064402c 100644 --- a/hikari/__main__.py +++ b/hikari/__main__.py @@ -27,6 +27,7 @@ from hikari import _about version = _about.__version__ +# noinspection PyTypeChecker path = os.path.abspath(os.path.dirname(inspect.getsourcefile(_about))) py_impl = platform.python_implementation() py_ver = platform.python_version() diff --git a/hikari/app.py b/hikari/app.py index 2664a81c06..2186502f41 100644 --- a/hikari/app.py +++ b/hikari/app.py @@ -25,9 +25,8 @@ import logging import typing -from concurrent import futures - if typing.TYPE_CHECKING: + from concurrent import futures import datetime from hikari import cache as cache_ diff --git a/hikari/bot.py b/hikari/bot.py index 626435cb94..d69f6eaa8b 100644 --- a/hikari/bot.py +++ b/hikari/bot.py @@ -21,9 +21,12 @@ __all__ = ["IBot"] import abc +import typing from hikari import app as app_ -from hikari import http_settings as http_settings_ + +if typing.TYPE_CHECKING: + from hikari import http_settings as http_settings_ class IBot(app_.IRESTApp, app_.IGatewayZookeeper, app_.IGatewayDispatcher, abc.ABC): diff --git a/hikari/component.py b/hikari/component.py index 2314fa9cc4..2f68c36cd9 100644 --- a/hikari/component.py +++ b/hikari/component.py @@ -28,6 +28,8 @@ class IComponent(abc.ABC): + """A component that makes up part of the application.""" + __slots__ = () @property diff --git a/hikari/event_dispatcher.py b/hikari/event_dispatcher.py index d3ce57116f..da801bad6e 100644 --- a/hikari/event_dispatcher.py +++ b/hikari/event_dispatcher.py @@ -25,7 +25,7 @@ import typing from hikari import component -from hikari.utilities import unset +from hikari.utilities import undefined if typing.TYPE_CHECKING: from hikari.events import base @@ -100,14 +100,14 @@ def unsubscribe( """ @abc.abstractmethod - def listen(self, event_type: typing.Union[unset.Unset, typing.Type[_EventT]]) -> None: + def listen(self, event_type: typing.Union[undefined.Undefined, typing.Type[_EventT]]) -> None: """Generate a decorator to subscribe a callback to an event type. This is a second-order decorator. Parameters ---------- - event_type : hikari.utilities.unset.Unset OR typing.Type[hikari.events.bases.HikariEvent] + event_type : hikari.utilities.unset.Undefined OR typing.Type[hikari.events.bases.HikariEvent] The event type to subscribe to. The implementation may allow this to be unset. If this is the case, the event type will be inferred instead from the type hints on the function signature. diff --git a/hikari/events/guild.py b/hikari/events/guild.py index b55dc58bda..d7096086e0 100644 --- a/hikari/events/guild.py +++ b/hikari/events/guild.py @@ -50,7 +50,7 @@ from hikari.models import intents from hikari.models import users from . import base as base_events -from ..utilities import unset +from ..utilities import undefined if typing.TYPE_CHECKING: import datetime @@ -163,14 +163,14 @@ class GuildMemberUpdateEvent(base_events.HikariEvent): user: users.User = attr.ib(repr=True) """The object of the user who was updated.""" - nickname: typing.Union[None, str, unset.Unset] = attr.ib() + nickname: typing.Union[None, str, undefined.Undefined] = attr.ib() """This member's nickname. When set to `None`, this has been removed and when set to `hikari.models.unset.UNSET` this hasn't been acted on. """ - premium_since: typing.Union[None, datetime.datetime, unset.Unset] = attr.ib() + premium_since: typing.Union[None, datetime.datetime, undefined.Undefined] = attr.ib() """The datetime of when this member started "boosting" this guild. Will be `None` if they aren't boosting. diff --git a/hikari/events/message.py b/hikari/events/message.py index d7542a3fa0..63029cc6a1 100644 --- a/hikari/events/message.py +++ b/hikari/events/message.py @@ -44,13 +44,11 @@ from hikari.models import messages from hikari.models import users from . import base as base_events -from ..utilities import unset +from ..utilities import undefined if typing.TYPE_CHECKING: import datetime - from hikari.utilities import more_typing - @base_events.requires_intents(intents.Intent.GUILD_MESSAGES, intents.Intent.DIRECT_MESSAGES) @attr.s(eq=False, hash=False, kw_only=True, slots=True) @@ -74,75 +72,75 @@ class MessageUpdateEvent(base_events.HikariEvent, base_models.Unique): channel_id: base_models.Snowflake = attr.ib(repr=True) """The ID of the channel that the message was sent in.""" - guild_id: typing.Union[base_models.Snowflake, unset.Unset] = attr.ib(repr=True) + guild_id: typing.Union[base_models.Snowflake, undefined.Undefined] = attr.ib(repr=True) """The ID of the guild that the message was sent in.""" - author: typing.Union[users.User, unset.Unset] = attr.ib(repr=True) + author: typing.Union[users.User, undefined.Undefined] = attr.ib(repr=True) """The author of this message.""" # TODO: can we merge member and author together? # We could override deserialize to to this and then reorganise the payload, perhaps? - member: typing.Union[guilds.GuildMember, unset.Unset] = attr.ib() + member: typing.Union[guilds.GuildMember, undefined.Undefined] = attr.ib() """The member properties for the message's author.""" - content: typing.Union[str, unset.Unset] = attr.ib() + content: typing.Union[str, undefined.Undefined] = attr.ib() """The content of the message.""" - timestamp: typing.Union[datetime.datetime, unset.Unset] = attr.ib() + timestamp: typing.Union[datetime.datetime, undefined.Undefined] = attr.ib() """The timestamp that the message was sent at.""" - edited_timestamp: typing.Union[datetime.datetime, unset.Unset, None] = attr.ib() + edited_timestamp: typing.Union[datetime.datetime, undefined.Undefined, None] = attr.ib() """The timestamp that the message was last edited at. Will be `None` if the message wasn't ever edited. """ - is_tts: typing.Union[bool, unset.Unset] = attr.ib() + is_tts: typing.Union[bool, undefined.Undefined] = attr.ib() """Whether the message is a TTS message.""" - is_mentioning_everyone: typing.Union[bool, unset.Unset] = attr.ib() + is_mentioning_everyone: typing.Union[bool, undefined.Undefined] = attr.ib() """Whether the message mentions `@everyone` or `@here`.""" - user_mentions: typing.Union[typing.Set[base_models.Snowflake], unset.Unset] = attr.ib() + user_mentions: typing.Union[typing.Set[base_models.Snowflake], undefined.Undefined] = attr.ib() """The users the message mentions.""" - role_mentions: typing.Union[typing.Set[base_models.Snowflake], unset.Unset] = attr.ib() + role_mentions: typing.Union[typing.Set[base_models.Snowflake], undefined.Undefined] = attr.ib() """The roles the message mentions.""" - channel_mentions: typing.Union[typing.Set[base_models.Snowflake], unset.Unset] = attr.ib() + channel_mentions: typing.Union[typing.Set[base_models.Snowflake], undefined.Undefined] = attr.ib() """The channels the message mentions.""" - attachments: typing.Union[typing.Sequence[messages.Attachment], unset.Unset] = attr.ib() + attachments: typing.Union[typing.Sequence[messages.Attachment], undefined.Undefined] = attr.ib() """The message attachments.""" - embeds: typing.Union[typing.Sequence[embed_models.Embed], unset.Unset] = attr.ib() + embeds: typing.Union[typing.Sequence[embed_models.Embed], undefined.Undefined] = attr.ib() """The message's embeds.""" - reactions: typing.Union[typing.Sequence[messages.Reaction], unset.Unset] = attr.ib() + reactions: typing.Union[typing.Sequence[messages.Reaction], undefined.Undefined] = attr.ib() """The message's reactions.""" - is_pinned: typing.Union[bool, unset.Unset] = attr.ib() + is_pinned: typing.Union[bool, undefined.Undefined] = attr.ib() """Whether the message is pinned.""" - webhook_id: typing.Union[base_models.Snowflake, unset.Unset] = attr.ib() + webhook_id: typing.Union[base_models.Snowflake, undefined.Undefined] = attr.ib() """If the message was generated by a webhook, the webhook's ID.""" - type: typing.Union[messages.MessageType, unset.Unset] = attr.ib() + type: typing.Union[messages.MessageType, undefined.Undefined] = attr.ib() """The message's type.""" - activity: typing.Union[messages.MessageActivity, unset.Unset] = attr.ib() + activity: typing.Union[messages.MessageActivity, undefined.Undefined] = attr.ib() """The message's activity.""" application: typing.Optional[applications.Application] = attr.ib() """The message's application.""" - message_reference: typing.Union[messages.MessageCrosspost, unset.Unset] = attr.ib() + message_reference: typing.Union[messages.MessageCrosspost, undefined.Undefined] = attr.ib() """The message's cross-posted reference data.""" - flags: typing.Union[messages.MessageFlag, unset.Unset] = attr.ib() + flags: typing.Union[messages.MessageFlag, undefined.Undefined] = attr.ib() """The message's flags.""" - nonce: typing.Union[str, unset.Unset] = attr.ib() + nonce: typing.Union[str, undefined.Undefined] = attr.ib() """The message nonce. This is a string used for validating a message was sent. diff --git a/hikari/impl/entity_factory.py b/hikari/impl/entity_factory.py index 97052ef1de..94612c87d6 100644 --- a/hikari/impl/entity_factory.py +++ b/hikari/impl/entity_factory.py @@ -43,7 +43,7 @@ from hikari.models import voices from hikari.models import webhooks from hikari.utilities import date -from hikari.utilities import unset +from hikari.utilities import undefined if typing.TYPE_CHECKING: from hikari.utilities import binding @@ -947,13 +947,15 @@ def deserialize_guild_member_presence(self, payload: binding.JSONObject) -> guil user = guilds.PresenceUser() user.set_app(self._app) user.id = base_models.Snowflake(user_payload["id"]) - user.discriminator = user_payload["discriminator"] if "discriminator" in user_payload else unset.UNSET - user.username = user_payload["username"] if "username" in user_payload else unset.UNSET - user.avatar_hash = user_payload["avatar"] if "avatar" in user_payload else unset.UNSET - user.is_bot = user_payload["bot"] if "bot" in user_payload else unset.UNSET - user.is_system = user_payload["system"] if "system" in user_payload else unset.UNSET + user.discriminator = user_payload["discriminator"] if "discriminator" in user_payload else undefined.Undefined() + user.username = user_payload["username"] if "username" in user_payload else undefined.Undefined() + user.avatar_hash = user_payload["avatar"] if "avatar" in user_payload else undefined.Undefined() + user.is_bot = user_payload["bot"] if "bot" in user_payload else undefined.Undefined() + user.is_system = user_payload["system"] if "system" in user_payload else undefined.Undefined() # noinspection PyArgumentList - user.flags = users.UserFlag(user_payload["public_flags"]) if "public_flags" in user_payload else unset.UNSET + user.flags = ( + users.UserFlag(user_payload["public_flags"]) if "public_flags" in user_payload else undefined.Undefined() + ) guild_member_presence.user = user if (role_ids := payload.get("roles", ...)) is not ...: guild_member_presence.role_ids = {base_models.Snowflake(role_id) for role_id in role_ids} diff --git a/hikari/impl/gateway_zookeeper.py b/hikari/impl/gateway_zookeeper.py index 14061b86e8..eb51433df9 100644 --- a/hikari/impl/gateway_zookeeper.py +++ b/hikari/impl/gateway_zookeeper.py @@ -36,7 +36,7 @@ from hikari import event_dispatcher from hikari.events import other from hikari.net import gateway -from hikari.utilities import unset +from hikari.utilities import undefined if typing.TYPE_CHECKING: from hikari import http_settings @@ -205,10 +205,10 @@ def sigterm_handler(*_): async def update_presence( self, *, - status: typing.Union[unset.Unset, guilds.PresenceStatus] = unset.UNSET, - activity: typing.Union[unset.Unset, gateway.Activity, None] = unset.UNSET, - idle_since: typing.Union[unset.Unset, datetime.datetime] = unset.UNSET, - is_afk: typing.Union[unset.Unset, bool] = unset.UNSET, + status: typing.Union[undefined.Undefined, guilds.PresenceStatus] = undefined.Undefined(), + activity: typing.Union[undefined.Undefined, gateway.Activity, None] = undefined.Undefined(), + idle_since: typing.Union[undefined.Undefined, datetime.datetime] = undefined.Undefined(), + is_afk: typing.Union[undefined.Undefined, bool] = undefined.Undefined(), ) -> None: coros = ( s.update_presence(status=status, activity=activity, idle_since=idle_since, is_afk=is_afk) diff --git a/hikari/models/embeds.py b/hikari/models/embeds.py index ed6e2dba6e..1cf951e6f8 100644 --- a/hikari/models/embeds.py +++ b/hikari/models/embeds.py @@ -35,6 +35,7 @@ import typing import attr + from . import colors from . import files diff --git a/hikari/models/files.py b/hikari/models/files.py index ae349e2ab8..d14388616f 100644 --- a/hikari/models/files.py +++ b/hikari/models/files.py @@ -68,7 +68,6 @@ import aiohttp - from hikari import errors from hikari.utilities import aio diff --git a/hikari/models/guilds.py b/hikari/models/guilds.py index da5656cb0a..0edfcde4a8 100644 --- a/hikari/models/guilds.py +++ b/hikari/models/guilds.py @@ -59,12 +59,11 @@ import attr +from hikari.net import urls +from hikari.utilities import undefined from . import bases from . import users -from hikari.net import urls -from hikari.utilities import unset - if typing.TYPE_CHECKING: import datetime @@ -507,32 +506,32 @@ class PresenceUser(users.User): unless it is specifically being modified for this update. """ - discriminator: typing.Union[str, unset.Unset] = attr.ib(eq=False, hash=False, repr=True) + discriminator: typing.Union[str, undefined.Undefined] = attr.ib(eq=False, hash=False, repr=True) """This user's discriminator.""" - username: typing.Union[str, unset.Unset] = attr.ib(eq=False, hash=False, repr=True) + username: typing.Union[str, undefined.Undefined] = attr.ib(eq=False, hash=False, repr=True) """This user's username.""" - avatar_hash: typing.Union[None, str, unset.Unset] = attr.ib( + avatar_hash: typing.Union[None, str, undefined.Undefined] = attr.ib( eq=False, hash=False, repr=True, ) """This user's avatar hash, if set.""" - is_bot: typing.Union[bool, unset.Unset] = attr.ib( + is_bot: typing.Union[bool, undefined.Undefined] = attr.ib( eq=False, hash=False, repr=True, ) """Whether this user is a bot account.""" - is_system: typing.Union[bool, unset.Unset] = attr.ib( + is_system: typing.Union[bool, undefined.Undefined] = attr.ib( eq=False, hash=False, ) """Whether this user is a system account.""" - flags: typing.Union[users.UserFlag, unset.Unset] = attr.ib(eq=False, hash=False) + flags: typing.Union[users.UserFlag, undefined.Undefined] = attr.ib(eq=False, hash=False) """The public flags for this user.""" @property - def avatar_url(self) -> typing.Union[str, unset.Unset]: + def avatar_url(self) -> typing.Union[str, undefined.Undefined]: """URL for this user's avatar if the relevant info is available. !!! note @@ -543,7 +542,7 @@ def avatar_url(self) -> typing.Union[str, unset.Unset]: def format_avatar_url( self, *, format_: typing.Optional[str] = None, size: int = 4096 - ) -> typing.Union[str, unset.Unset]: + ) -> typing.Union[str, undefined.Undefined]: """Generate the avatar URL for this user's avatar if available. Parameters @@ -569,33 +568,33 @@ def format_avatar_url( ValueError If `size` is not a power of two or not between 16 and 4096. """ - if self.discriminator is not unset.UNSET or self.avatar_hash is not unset.UNSET: + if self.discriminator is not undefined.Undefined() or self.avatar_hash is not undefined.Undefined(): return super().format_avatar_url(format_=format_, size=size) - return unset.UNSET + return undefined.Undefined() @property - def default_avatar_index(self) -> typing.Union[int, unset.Unset]: + def default_avatar_index(self) -> typing.Union[int, undefined.Undefined]: """Integer representation of this user's default avatar. !!! note This will be `hikari.models.unset.UNSET` if `PresenceUser.discriminator` is `hikari.models.unset.UNSET`. """ - if self.discriminator is not unset.UNSET: + if self.discriminator is not undefined.Undefined(): return super().default_avatar_index - return unset.UNSET + return undefined.Undefined() @property - def default_avatar_url(self) -> typing.Union[str, unset.Unset]: + def default_avatar_url(self) -> typing.Union[str, undefined.Undefined]: """URL for this user's default avatar. !!! note This will be `hikari.models.unset.UNSET` if `PresenceUser.discriminator` is `hikari.models.unset.UNSET`. """ - if self.discriminator is not unset.UNSET: + if self.discriminator is not undefined.Undefined(): return super().default_avatar_url - return unset.UNSET + return undefined.Undefined() @attr.s(eq=True, hash=True, init=False, kw_only=True, slots=True) diff --git a/hikari/models/messages.py b/hikari/models/messages.py index 2dbbe77800..d5c2d202cc 100644 --- a/hikari/models/messages.py +++ b/hikari/models/messages.py @@ -35,6 +35,7 @@ import typing import attr + from . import bases from . import files as files_ diff --git a/hikari/net/gateway.py b/hikari/net/gateway.py index b2a3021a59..f4720dc6c6 100644 --- a/hikari/net/gateway.py +++ b/hikari/net/gateway.py @@ -36,13 +36,15 @@ from hikari import component from hikari import errors from hikari import http_settings +from hikari.models import guilds from hikari.net import http_client from hikari.net import ratelimits from hikari.net import user_agents from hikari.utilities import binding from hikari.utilities import klass from hikari.utilities import snowflake -from hikari.utilities import unset +from hikari.utilities import undefined + if typing.TYPE_CHECKING: import datetime @@ -50,7 +52,6 @@ from hikari import app as app_ from hikari.utilities import aio from hikari.models import channels - from hikari.models import guilds from hikari.models import intents as intents_ @@ -382,10 +383,10 @@ async def _run_once(self) -> bool: async def update_presence( self, *, - idle_since: typing.Union[unset.Unset, typing.Optional[datetime.datetime]] = unset.UNSET, - is_afk: typing.Union[unset.Unset, bool] = unset.UNSET, - activity: typing.Union[unset.Unset, typing.Optional[Activity]] = unset.UNSET, - status: typing.Union[unset.Unset, guilds.PresenceStatus] = unset.UNSET, + idle_since: typing.Union[undefined.Undefined, typing.Optional[datetime.datetime]] = undefined.Undefined(), + is_afk: typing.Union[undefined.Undefined, bool] = undefined.Undefined(), + activity: typing.Union[undefined.Undefined, typing.Optional[Activity]] = undefined.Undefined(), + status: typing.Union[undefined.Undefined, guilds.PresenceStatus] = undefined.Undefined(), ) -> None: """Update the presence of the shard user. @@ -405,10 +406,10 @@ async def update_presence( """ payload = self._build_presence_payload(idle_since, is_afk, activity, status) await self._send_json({"op": self._GatewayOpcode.PRESENCE_UPDATE, "d": payload}) - self._idle_since = idle_since if not unset.is_unset(idle_since) else self._idle_since - self._is_afk = is_afk if not unset.is_unset(is_afk) else self._is_afk - self._activity = activity if not unset.is_unset(activity) else self._activity - self._status = status if not unset.is_unset(status) else self._status + self._idle_since = idle_since if not isinstance(idle_since, undefined.Undefined) else self._idle_since + self._is_afk = is_afk if not isinstance(is_afk, undefined.Undefined) else self._is_afk + self._activity = activity if not isinstance(activity, undefined.Undefined) else self._activity + self._status = status if not isinstance(status, undefined.Undefined) else self._status async def update_voice_state( self, @@ -649,18 +650,18 @@ def _log_debug_payload(self, payload: str, message: str, *args: typing.Any) -> N def _build_presence_payload( self, - idle_since: typing.Union[unset.Unset, typing.Optional[datetime.datetime]] = unset.UNSET, - is_afk: typing.Union[unset.Unset, bool] = unset.UNSET, - status: typing.Union[unset.Unset, guilds.PresenceStatus] = unset.UNSET, - activity: typing.Union[unset.Unset, typing.Optional[Activity]] = unset.UNSET, + idle_since: typing.Union[undefined.Undefined, typing.Optional[datetime.datetime]] = undefined.Undefined(), + is_afk: typing.Union[undefined.Undefined, bool] = undefined.Undefined(), + status: typing.Union[undefined.Undefined, guilds.PresenceStatus] = undefined.Undefined(), + activity: typing.Union[undefined.Undefined, typing.Optional[Activity]] = undefined.Undefined(), ) -> binding.JSONObject: - if unset.is_unset(idle_since): + if isinstance(idle_since, undefined.Undefined): idle_since = self._idle_since - if unset.is_unset(is_afk): + if isinstance(is_afk, undefined.Undefined): is_afk = self._is_afk - if unset.is_unset(status): + if isinstance(status, undefined.Undefined): status = self._status - if unset.is_unset(activity): + if isinstance(activity, undefined.Undefined): activity = self._activity activity = typing.cast(typing.Optional[Activity], activity) diff --git a/hikari/net/iterators.py b/hikari/net/iterators.py index 5e562d4352..0301c34aa2 100644 --- a/hikari/net/iterators.py +++ b/hikari/net/iterators.py @@ -33,7 +33,7 @@ from hikari.net import routes from hikari.utilities import binding from hikari.utilities import snowflake -from hikari.utilities import unset +from hikari.utilities import undefined _T = typing.TypeVar("_T") @@ -378,8 +378,8 @@ def __init__( request_call: typing.Callable[..., typing.Coroutine[None, typing.Any, binding.JSONObject]], guild_id: typing.Union[typing.SupportsInt, int], before: typing.Union[typing.SupportsInt, int], - user_id: typing.Union[typing.SupportsInt, int, unset.Unset], - action_type: typing.Union[int, unset.Unset], + user_id: typing.Union[typing.SupportsInt, int, undefined.Undefined], + action_type: typing.Union[int, undefined.Undefined], ) -> None: self._action_type = action_type self._app = app diff --git a/hikari/net/rest.py b/hikari/net/rest.py index dffa4a1297..adf6ef9f7d 100644 --- a/hikari/net/rest.py +++ b/hikari/net/rest.py @@ -41,7 +41,7 @@ from hikari.utilities import date from hikari.utilities import klass from hikari.utilities import snowflake -from hikari.utilities import unset +from hikari.utilities import undefined if typing.TYPE_CHECKING: from hikari import app as app_ @@ -143,9 +143,11 @@ async def _request( self, compiled_route: routes.CompiledRoute, *, - query: typing.Union[unset.Unset, binding.StringMapBuilder] = unset.UNSET, - body: typing.Union[unset.Unset, aiohttp.FormData, binding.JSONObjectBuilder, binding.JSONArray] = unset.UNSET, - reason: typing.Union[unset.Unset, str] = unset.UNSET, + query: typing.Union[undefined.Undefined, binding.StringMapBuilder] = undefined.Undefined(), + body: typing.Union[ + undefined.Undefined, aiohttp.FormData, binding.JSONObjectBuilder, binding.JSONArray + ] = undefined.Undefined(), + reason: typing.Union[undefined.Undefined, str] = undefined.Undefined(), no_auth: bool = False, ) -> typing.Optional[binding.JSONObject, binding.JSONArray, bytes, str]: # Make a ratelimit-protected HTTP request to a JSON endpoint and expect some form @@ -166,12 +168,12 @@ async def _request( if self._token is not None and not no_auth: headers["authorization"] = self._token - if unset.is_unset(body): + if isinstance(body, undefined.Undefined): body = None headers.put("x-audit-log-reason", reason) - if unset.is_unset(query): + if isinstance(query, undefined.Undefined): query = None while True: @@ -421,16 +423,18 @@ async def edit_channel( channel: _GuildChannelT, /, *, - name: typing.Union[unset.Unset, str] = unset.UNSET, - position: typing.Union[unset.Unset, int] = unset.UNSET, - topic: typing.Union[unset.Unset, str] = unset.UNSET, - nsfw: typing.Union[unset.Unset, bool] = unset.UNSET, - bitrate: typing.Union[unset.Unset, int] = unset.UNSET, - user_limit: typing.Union[unset.Unset, int] = unset.UNSET, - rate_limit_per_user: typing.Union[unset.Unset, date.TimeSpan] = unset.UNSET, - permission_overwrites: typing.Union[unset.Unset, typing.Sequence[channels.PermissionOverwrite]] = unset.UNSET, - parent_category: typing.Union[unset.Unset, channels.GuildCategory] = unset.UNSET, - reason: typing.Union[unset.Unset, str] = unset.UNSET, + name: typing.Union[undefined.Undefined, str] = undefined.Undefined(), + position: typing.Union[undefined.Undefined, int] = undefined.Undefined(), + topic: typing.Union[undefined.Undefined, str] = undefined.Undefined(), + nsfw: typing.Union[undefined.Undefined, bool] = undefined.Undefined(), + bitrate: typing.Union[undefined.Undefined, int] = undefined.Undefined(), + user_limit: typing.Union[undefined.Undefined, int] = undefined.Undefined(), + rate_limit_per_user: typing.Union[undefined.Undefined, date.TimeSpan] = undefined.Undefined(), + permission_overwrites: typing.Union[ + undefined.Undefined, typing.Sequence[channels.PermissionOverwrite] + ] = undefined.Undefined(), + parent_category: typing.Union[undefined.Undefined, channels.GuildCategory] = undefined.Undefined(), + reason: typing.Union[undefined.Undefined, str] = undefined.Undefined(), ) -> _GuildChannelT: """Edit a guild channel, given an existing guild channel object.""" @@ -439,16 +443,18 @@ async def edit_channel( channel: typing.Union[channels.PartialChannel, bases.UniqueObjectT], /, *, - name: typing.Union[unset.Unset, str] = unset.UNSET, - position: typing.Union[unset.Unset, int] = unset.UNSET, - topic: typing.Union[unset.Unset, str] = unset.UNSET, - nsfw: typing.Union[unset.Unset, bool] = unset.UNSET, - bitrate: typing.Union[unset.Unset, int] = unset.UNSET, - user_limit: typing.Union[unset.Unset, int] = unset.UNSET, - rate_limit_per_user: typing.Union[unset.Unset, date.TimeSpan] = unset.UNSET, - permission_overwrites: typing.Union[unset.Unset, typing.Sequence[channels.PermissionOverwrite]] = unset.UNSET, - parent_category: typing.Union[unset.Unset, channels.GuildCategory] = unset.UNSET, - reason: typing.Union[unset.Unset, str] = unset.UNSET, + name: typing.Union[undefined.Undefined, str] = undefined.Undefined(), + position: typing.Union[undefined.Undefined, int] = undefined.Undefined(), + topic: typing.Union[undefined.Undefined, str] = undefined.Undefined(), + nsfw: typing.Union[undefined.Undefined, bool] = undefined.Undefined(), + bitrate: typing.Union[undefined.Undefined, int] = undefined.Undefined(), + user_limit: typing.Union[undefined.Undefined, int] = undefined.Undefined(), + rate_limit_per_user: typing.Union[undefined.Undefined, date.TimeSpan] = undefined.Undefined(), + permission_overwrites: typing.Union[ + undefined.Undefined, typing.Sequence[channels.PermissionOverwrite] + ] = undefined.Undefined(), + parent_category: typing.Union[undefined.Undefined, channels.GuildCategory] = undefined.Undefined(), + reason: typing.Union[undefined.Undefined, str] = undefined.Undefined(), ) -> channels.PartialChannel: """Edit a channel. @@ -527,9 +533,9 @@ async def edit_permission_overwrites( channel: typing.Union[channels.GuildChannel, bases.UniqueObjectT], target: typing.Union[channels.PermissionOverwrite, users.User, guilds.Role], *, - allow: typing.Union[unset.Unset, permissions_.Permission] = unset.UNSET, - deny: typing.Union[unset.Unset, permissions_.Permission] = unset.UNSET, - reason: typing.Union[unset.Unset, str] = unset.UNSET, + allow: typing.Union[undefined.Undefined, permissions_.Permission] = undefined.Undefined(), + deny: typing.Union[undefined.Undefined, permissions_.Permission] = undefined.Undefined(), + reason: typing.Union[undefined.Undefined, str] = undefined.Undefined(), ) -> None: """Edit permissions for a target entity.""" @@ -540,9 +546,9 @@ async def edit_permission_overwrites( target: typing.Union[int, str, snowflake.Snowflake], target_type: typing.Union[channels.PermissionOverwriteType, str], *, - allow: typing.Union[unset.Unset, permissions_.Permission] = unset.UNSET, - deny: typing.Union[unset.Unset, permissions_.Permission] = unset.UNSET, - reason: typing.Union[unset.Unset, str] = unset.UNSET, + allow: typing.Union[undefined.Undefined, permissions_.Permission] = undefined.Undefined(), + deny: typing.Union[undefined.Undefined, permissions_.Permission] = undefined.Undefined(), + reason: typing.Union[undefined.Undefined, str] = undefined.Undefined(), ) -> None: """Edit permissions for a given entity ID and type.""" @@ -550,11 +556,11 @@ async def edit_permission_overwrites( self, channel: typing.Union[channels.GuildChannel, bases.UniqueObjectT], target: typing.Union[bases.UniqueObjectT, users.User, guilds.Role, channels.PermissionOverwrite], - target_type: typing.Union[unset.Unset, channels.PermissionOverwriteType, str] = unset.UNSET, + target_type: typing.Union[undefined.Undefined, channels.PermissionOverwriteType, str] = undefined.Undefined(), *, - allow: typing.Union[unset.Unset, permissions_.Permission] = unset.UNSET, - deny: typing.Union[unset.Unset, permissions_.Permission] = unset.UNSET, - reason: typing.Union[unset.Unset, str] = unset.UNSET, + allow: typing.Union[undefined.Undefined, permissions_.Permission] = undefined.Undefined(), + deny: typing.Union[undefined.Undefined, permissions_.Permission] = undefined.Undefined(), + reason: typing.Union[undefined.Undefined, str] = undefined.Undefined(), ) -> None: """Edit permissions for a specific entity in the given guild channel. @@ -582,7 +588,7 @@ async def edit_permission_overwrites( If an internal error occurs on Discord while handling the request. """ - if unset.is_unset(target_type): + if isinstance(target_type, undefined.Undefined): if isinstance(target, users.User): target_type = channels.PermissionOverwriteType.MEMBER elif isinstance(target, guilds.Role): @@ -660,13 +666,13 @@ async def create_invite( channel: typing.Union[channels.GuildChannel, bases.UniqueObjectT], /, *, - max_age: typing.Union[unset.Unset, int, float, datetime.timedelta] = unset.UNSET, - max_uses: typing.Union[unset.Unset, int] = unset.UNSET, - temporary: typing.Union[unset.Unset, bool] = unset.UNSET, - unique: typing.Union[unset.Unset, bool] = unset.UNSET, - target_user: typing.Union[unset.Unset, users.User, bases.UniqueObjectT] = unset.UNSET, - target_user_type: typing.Union[unset.Unset, invites.TargetUserType] = unset.UNSET, - reason: typing.Union[unset.Unset, str] = unset.UNSET, + max_age: typing.Union[undefined.Undefined, int, float, datetime.timedelta] = undefined.Undefined(), + max_uses: typing.Union[undefined.Undefined, int] = undefined.Undefined(), + temporary: typing.Union[undefined.Undefined, bool] = undefined.Undefined(), + unique: typing.Union[undefined.Undefined, bool] = undefined.Undefined(), + target_user: typing.Union[undefined.Undefined, users.User, bases.UniqueObjectT] = undefined.Undefined(), + target_user_type: typing.Union[undefined.Undefined, invites.TargetUserType] = undefined.Undefined(), + reason: typing.Union[undefined.Undefined, str] = undefined.Undefined(), ) -> invites.InviteWithMetadata: """Create an invite to the given guild channel. @@ -868,9 +874,9 @@ def fetch_messages( channel: typing.Union[channels.TextChannel, bases.UniqueObjectT], /, *, - before: typing.Union[unset.Unset, datetime.datetime, bases.UniqueObjectT] = unset.UNSET, - after: typing.Union[unset.Unset, datetime.datetime, bases.UniqueObjectT] = unset.UNSET, - around: typing.Union[unset.Unset, datetime.datetime, bases.UniqueObjectT] = unset.UNSET, + before: typing.Union[undefined.Undefined, datetime.datetime, bases.UniqueObjectT] = undefined.Undefined(), + after: typing.Union[undefined.Undefined, datetime.datetime, bases.UniqueObjectT] = undefined.Undefined(), + around: typing.Union[undefined.Undefined, datetime.datetime, bases.UniqueObjectT] = undefined.Undefined(), ) -> iterators.LazyIterator[messages_.Message]: """Browse the message history for a given text channel. @@ -904,13 +910,13 @@ def fetch_messages( this function itself will not raise anything (other than `TypeError`). """ - if unset.count_unset_objects(before, after, around) < 2: + if undefined.Undefined.count(before, after, around) < 2: raise TypeError(f"Expected no kwargs, or maximum of one of 'before', 'after', 'around'") - elif not unset.is_unset(before): + elif not isinstance(before, undefined.Undefined): direction, timestamp = "before", before - elif not unset.is_unset(after): + elif not isinstance(after, undefined.Undefined): direction, timestamp = "after", after - elif not unset.is_unset(around): + elif not isinstance(around, undefined.Undefined): direction, timestamp = "around", around else: direction, timestamp = "before", snowflake.Snowflake.max() @@ -955,12 +961,12 @@ async def fetch_message( async def create_message( self, channel: typing.Union[channels.TextChannel, bases.UniqueObjectT], - text: typing.Union[unset.Unset, typing.Any] = unset.UNSET, + text: typing.Union[undefined.Undefined, typing.Any] = undefined.Undefined(), *, - embed: typing.Union[unset.Unset, embeds_.Embed] = unset.UNSET, - attachments: typing.Union[unset.Unset, typing.Sequence[files.BaseStream]] = unset.UNSET, - tts: typing.Union[unset.Unset, bool] = unset.UNSET, - nonce: typing.Union[unset.Unset, str] = unset.UNSET, + embed: typing.Union[undefined.Undefined, embeds_.Embed] = undefined.Undefined(), + attachments: typing.Union[undefined.Undefined, typing.Sequence[files.BaseStream]] = undefined.Undefined(), + tts: typing.Union[undefined.Undefined, bool] = undefined.Undefined(), + nonce: typing.Union[undefined.Undefined, str] = undefined.Undefined(), mentions_everyone: bool = False, user_mentions: typing.Union[typing.Collection[typing.Union[users.User, bases.UniqueObjectT]], bool] = True, role_mentions: typing.Union[typing.Collection[typing.Union[bases.UniqueObjectT, guilds.Role]], bool] = True, @@ -1015,9 +1021,9 @@ async def create_message( body.put("nonce", nonce) body.put("tts", tts) - attachments = [] if unset.is_unset(attachments) else [a for a in attachments] + attachments = [] if isinstance(attachments, undefined.Undefined) else [a for a in attachments] - if not unset.is_unset(embed): + if not isinstance(embed, undefined.Undefined): attachments += embed.assets_to_upload response = await self._request( @@ -1030,13 +1036,13 @@ async def edit_message( self, channel: typing.Union[channels.TextChannel, bases.UniqueObjectT], message: typing.Union[messages_.Message, bases.UniqueObjectT], - text: typing.Union[unset.Unset, typing.Any] = unset.UNSET, + text: typing.Union[undefined.Undefined, typing.Any] = undefined.Undefined(), *, - embed: typing.Union[unset.Unset, embeds_.Embed] = unset.UNSET, + embed: typing.Union[undefined.Undefined, embeds_.Embed] = undefined.Undefined(), mentions_everyone: bool = False, user_mentions: typing.Union[typing.Collection[typing.Union[users.User, bases.UniqueObjectT]], bool] = True, role_mentions: typing.Union[typing.Collection[typing.Union[bases.UniqueObjectT, guilds.Role]], bool] = True, - flags: typing.Union[unset.Unset, messages_.MessageFlag] = unset.UNSET, + flags: typing.Union[undefined.Undefined, messages_.MessageFlag] = undefined.Undefined(), ) -> messages_.Message: """Edit an existing message in a given channel. @@ -1261,13 +1267,13 @@ async def create_webhook( channel: typing.Union[channels.TextChannel, bases.UniqueObjectT], name: str, *, - avatar: typing.Union[unset.Unset, files.BaseStream] = unset.UNSET, - reason: typing.Union[unset.Unset, str] = unset.UNSET, + avatar: typing.Union[undefined.Undefined, files.BaseStream] = undefined.Undefined(), + reason: typing.Union[undefined.Undefined, str] = undefined.Undefined(), ) -> webhooks.Webhook: route = routes.POST_WEBHOOK.compile(channel=channel) body = binding.JSONObjectBuilder() body.put("name", name) - if not unset.is_unset(avatar): + if not isinstance(avatar, undefined.Undefined): body.put("avatar", await avatar.fetch_data_uri()) response = await self._request(route, body=body, reason=reason) @@ -1278,9 +1284,9 @@ async def fetch_webhook( webhook: typing.Union[webhooks.Webhook, bases.UniqueObjectT], /, *, - token: typing.Union[unset.Unset, str] = unset.UNSET, + token: typing.Union[undefined.Undefined, str] = undefined.Undefined(), ) -> webhooks.Webhook: - if unset.is_unset(token): + if isinstance(token, undefined.Undefined): route = routes.GET_WEBHOOK.compile(webhook=webhook) else: route = routes.GET_WEBHOOK_WITH_TOKEN.compile(webhook=webhook, token=token) @@ -1306,13 +1312,13 @@ async def edit_webhook( webhook: typing.Union[webhooks.Webhook, bases.UniqueObjectT], /, *, - token: typing.Union[unset.Unset, str] = unset.UNSET, - name: typing.Union[unset.Unset, str] = unset.UNSET, - avatar: typing.Union[unset.Unset, files.BaseStream] = unset.UNSET, - channel: typing.Union[unset.Unset, channels.TextChannel, bases.UniqueObjectT] = unset.UNSET, - reason: typing.Union[unset.Unset, str] = unset.UNSET, + token: typing.Union[undefined.Undefined, str] = undefined.Undefined(), + name: typing.Union[undefined.Undefined, str] = undefined.Undefined(), + avatar: typing.Union[undefined.Undefined, files.BaseStream] = undefined.Undefined(), + channel: typing.Union[undefined.Undefined, channels.TextChannel, bases.UniqueObjectT] = undefined.Undefined(), + reason: typing.Union[undefined.Undefined, str] = undefined.Undefined(), ) -> webhooks.Webhook: - if unset.is_unset(token): + if isinstance(token, undefined.Undefined): route = routes.PATCH_WEBHOOK.compile(webhook=webhook) else: route = routes.PATCH_WEBHOOK_WITH_TOKEN.compile(webhook=webhook, token=token) @@ -1320,7 +1326,7 @@ async def edit_webhook( body = binding.JSONObjectBuilder() body.put("name", name) body.put_snowflake("channel", channel) - if not unset.is_unset(avatar): + if not isinstance(avatar, undefined.Undefined): body.put("avatar", await avatar.fetch_data_uri()) response = await self._request(route, body=body, reason=reason) @@ -1331,9 +1337,9 @@ async def delete_webhook( webhook: typing.Union[webhooks.Webhook, bases.UniqueObjectT], /, *, - token: typing.Union[unset.Unset, str] = unset.UNSET, + token: typing.Union[undefined.Undefined, str] = undefined.Undefined(), ) -> None: - if unset.is_unset(token): + if isinstance(token, undefined.Undefined): route = routes.DELETE_WEBHOOK.compile(webhook=webhook) else: route = routes.DELETE_WEBHOOK_WITH_TOKEN.compile(webhook=webhook, token=token) @@ -1342,30 +1348,30 @@ async def delete_webhook( async def execute_webhook( self, webhook: typing.Union[webhooks.Webhook, bases.UniqueObjectT], - text: typing.Union[unset.Unset, typing.Any] = unset.UNSET, + text: typing.Union[undefined.Undefined, typing.Any] = undefined.Undefined(), *, - token: typing.Union[unset.Unset, str] = unset.UNSET, - username: typing.Union[unset.Unset, str] = unset.UNSET, - avatar_url: typing.Union[unset.Unset, str] = unset.UNSET, - embeds: typing.Union[unset.Unset, typing.Sequence[embeds_.Embed]] = unset.UNSET, - attachments: typing.Union[unset.Unset, typing.Sequence[files.BaseStream]] = unset.UNSET, - tts: typing.Union[unset.Unset, bool] = unset.UNSET, - wait: typing.Union[unset.Unset, bool] = unset.UNSET, + token: typing.Union[undefined.Undefined, str] = undefined.Undefined(), + username: typing.Union[undefined.Undefined, str] = undefined.Undefined(), + avatar_url: typing.Union[undefined.Undefined, str] = undefined.Undefined(), + embeds: typing.Union[undefined.Undefined, typing.Sequence[embeds_.Embed]] = undefined.Undefined(), + attachments: typing.Union[undefined.Undefined, typing.Sequence[files.BaseStream]] = undefined.Undefined(), + tts: typing.Union[undefined.Undefined, bool] = undefined.Undefined(), + wait: typing.Union[undefined.Undefined, bool] = undefined.Undefined(), mentions_everyone: bool = False, user_mentions: typing.Union[typing.Collection[typing.Union[users.User, bases.UniqueObjectT]], bool] = True, role_mentions: typing.Union[typing.Collection[typing.Union[bases.UniqueObjectT, guilds.Role]], bool] = True, ) -> messages_.Message: - if unset.is_unset(token): + if isinstance(token, undefined.Undefined): route = routes.POST_WEBHOOK.compile(webhook=webhook) no_auth = False else: route = routes.POST_WEBHOOK_WITH_TOKEN.compile(webhook=webhook, token=token) no_auth = True - attachments = [] if unset.is_unset(attachments) else [a for a in attachments] + attachments = [] if isinstance(attachments, undefined.Undefined) else [a for a in attachments] serialized_embeds = [] - if not unset.is_unset(embeds): + if not isinstance(embeds, undefined.Undefined): for embed in embeds: attachments += embed.assets_to_upload serialized_embeds.append(self._app.entity_factory.serialize_embed(embed)) @@ -1415,14 +1421,14 @@ async def fetch_my_user(self) -> users.MyUser: async def edit_my_user( self, *, - username: typing.Union[unset.Unset, str] = unset.UNSET, - avatar: typing.Union[unset.Unset, files.BaseStream] = unset.UNSET, + username: typing.Union[undefined.Undefined, str] = undefined.Undefined(), + avatar: typing.Union[undefined.Undefined, files.BaseStream] = undefined.Undefined(), ) -> users.MyUser: route = routes.PATCH_MY_USER.compile() body = binding.JSONObjectBuilder() body.put("username", username) - if not unset.is_unset(username): + if not isinstance(username, undefined.Undefined): body.put("avatar", await avatar.fetch_data_uri()) response = await self._request(route, body=body) @@ -1437,9 +1443,11 @@ def fetch_my_guilds( self, *, newest_first: bool = False, - start_at: typing.Union[unset.Unset, guilds.PartialGuild, bases.UniqueObjectT, datetime.datetime] = unset.UNSET, + start_at: typing.Union[ + undefined.Undefined, guilds.PartialGuild, bases.UniqueObjectT, datetime.datetime + ] = undefined.Undefined(), ) -> iterators.LazyIterator[applications.OwnGuild]: - if unset.is_unset(start_at): + if isinstance(start_at, undefined.Undefined): start_at = snowflake.Snowflake.max() if newest_first else snowflake.Snowflake.min() elif isinstance(start_at, datetime.datetime): start_at = snowflake.Snowflake.from_datetime(start_at) @@ -1468,12 +1476,12 @@ async def add_user_to_guild( guild: typing.Union[guilds.Guild, bases.UniqueObjectT], user: typing.Union[users.User, bases.UniqueObjectT], *, - nick: typing.Union[unset.Unset, str] = unset.UNSET, + nick: typing.Union[undefined.Undefined, str] = undefined.Undefined(), roles: typing.Union[ - unset.Unset, typing.Collection[typing.Union[guilds.Role, bases.UniqueObjectT]] - ] = unset.UNSET, - mute: typing.Union[unset.Unset, bool] = unset.UNSET, - deaf: typing.Union[unset.Unset, bool] = unset.UNSET, + undefined.Undefined, typing.Collection[typing.Union[guilds.Role, bases.UniqueObjectT]] + ] = undefined.Undefined(), + mute: typing.Union[undefined.Undefined, bool] = undefined.Undefined(), + deaf: typing.Union[undefined.Undefined, bool] = undefined.Undefined(), ) -> typing.Optional[guilds.GuildMember]: route = routes.PUT_GUILD_MEMBER.compile(guild=guild, user=user) body = binding.JSONObjectBuilder() @@ -1504,11 +1512,11 @@ def fetch_audit_log( guild: typing.Union[guilds.Guild, bases.UniqueObjectT], /, *, - before: typing.Union[unset.Unset, datetime.datetime, bases.UniqueObjectT] = unset.UNSET, - user: typing.Union[unset.Unset, users.User, bases.UniqueObjectT] = unset.UNSET, - event_type: typing.Union[unset.Unset, audit_logs.AuditLogEventType] = unset.UNSET, + before: typing.Union[undefined.Undefined, datetime.datetime, bases.UniqueObjectT] = undefined.Undefined(), + user: typing.Union[undefined.Undefined, users.User, bases.UniqueObjectT] = undefined.Undefined(), + event_type: typing.Union[undefined.Undefined, audit_logs.AuditLogEventType] = undefined.Undefined(), ) -> iterators.LazyIterator[audit_logs.AuditLog]: - if unset.is_unset(before): + if isinstance(before, undefined.Undefined): before = snowflake.Snowflake.max() elif isinstance(before, datetime.datetime): before = snowflake.Snowflake.from_datetime(before) @@ -1541,14 +1549,14 @@ async def create_emoji( image: files.BaseStream, *, roles: typing.Union[ - unset.Unset, typing.Collection[typing.Union[guilds.Role, bases.UniqueObjectT]] - ] = unset.UNSET, - reason: typing.Union[unset.Unset, str] = unset.UNSET, + undefined.Undefined, typing.Collection[typing.Union[guilds.Role, bases.UniqueObjectT]] + ] = undefined.Undefined(), + reason: typing.Union[undefined.Undefined, str] = undefined.Undefined(), ) -> emojis.KnownCustomEmoji: route = routes.POST_GUILD_EMOJIS.compile(guild=guild) body = binding.JSONObjectBuilder() body.put("name", name) - if not unset.is_unset(image): + if not isinstance(image, undefined.Undefined): body.put("image", await image.fetch_data_uri()) body.put_snowflake_array("roles", roles) @@ -1563,11 +1571,11 @@ async def edit_emoji( # This is an emoji ID, which is the URL-safe emoji name, not the snowflake alone. emoji: typing.Union[emojis.KnownCustomEmoji, str], *, - name: typing.Union[unset.Unset, str] = unset.UNSET, + name: typing.Union[undefined.Undefined, str] = undefined.Undefined(), roles: typing.Union[ - unset.Unset, typing.Collection[typing.Union[guilds.Role, bases.UniqueObjectT]] - ] = unset.UNSET, - reason: typing.Union[unset.Unset, str] = unset.UNSET, + undefined.Undefined, typing.Collection[typing.Union[guilds.Role, bases.UniqueObjectT]] + ] = undefined.Undefined(), + reason: typing.Union[undefined.Undefined, str] = undefined.Undefined(), ) -> emojis.KnownCustomEmoji: route = routes.PATCH_GUILD_EMOJI.compile( guild=guild, emoji=emoji.url_name if isinstance(emoji, emojis.Emoji) else emoji, @@ -1612,22 +1620,28 @@ async def edit_guild( guild: typing.Union[guilds.Guild, bases.UniqueObjectT], /, *, - name: typing.Union[unset.Unset, str] = unset.UNSET, - region: typing.Union[unset.Unset, voices.VoiceRegion, str] = unset.UNSET, - verification_level: typing.Union[unset.Unset, guilds.GuildVerificationLevel] = unset.UNSET, - default_message_notifications: typing.Union[unset.Unset, guilds.GuildMessageNotificationsLevel] = unset.UNSET, - explicit_content_filter_level: typing.Union[unset.Unset, guilds.GuildExplicitContentFilterLevel] = unset.UNSET, - afk_channel: typing.Union[unset.Unset, channels.GuildVoiceChannel, bases.UniqueObjectT] = unset.UNSET, - afk_timeout: typing.Union[unset.Unset, date.TimeSpan] = unset.UNSET, - icon: typing.Union[unset.Unset, files.BaseStream] = unset.UNSET, - owner: typing.Union[unset.Unset, users.User, bases.UniqueObjectT] = unset.UNSET, - splash: typing.Union[unset.Unset, files.BaseStream] = unset.UNSET, - banner: typing.Union[unset.Unset, files.BaseStream] = unset.UNSET, - system_channel: typing.Union[unset.Unset, channels.GuildTextChannel] = unset.UNSET, - rules_channel: typing.Union[unset.Unset, channels.GuildTextChannel] = unset.UNSET, - public_updates_channel: typing.Union[unset.Unset, channels.GuildTextChannel] = unset.UNSET, - preferred_locale: typing.Union[unset.Unset, str] = unset.UNSET, - reason: typing.Union[unset.Unset, str] = unset.UNSET, + name: typing.Union[undefined.Undefined, str] = undefined.Undefined(), + region: typing.Union[undefined.Undefined, voices.VoiceRegion, str] = undefined.Undefined(), + verification_level: typing.Union[undefined.Undefined, guilds.GuildVerificationLevel] = undefined.Undefined(), + default_message_notifications: typing.Union[ + undefined.Undefined, guilds.GuildMessageNotificationsLevel + ] = undefined.Undefined(), + explicit_content_filter_level: typing.Union[ + undefined.Undefined, guilds.GuildExplicitContentFilterLevel + ] = undefined.Undefined(), + afk_channel: typing.Union[ + undefined.Undefined, channels.GuildVoiceChannel, bases.UniqueObjectT + ] = undefined.Undefined(), + afk_timeout: typing.Union[undefined.Undefined, date.TimeSpan] = undefined.Undefined(), + icon: typing.Union[undefined.Undefined, files.BaseStream] = undefined.Undefined(), + owner: typing.Union[undefined.Undefined, users.User, bases.UniqueObjectT] = undefined.Undefined(), + splash: typing.Union[undefined.Undefined, files.BaseStream] = undefined.Undefined(), + banner: typing.Union[undefined.Undefined, files.BaseStream] = undefined.Undefined(), + system_channel: typing.Union[undefined.Undefined, channels.GuildTextChannel] = undefined.Undefined(), + rules_channel: typing.Union[undefined.Undefined, channels.GuildTextChannel] = undefined.Undefined(), + public_updates_channel: typing.Union[undefined.Undefined, channels.GuildTextChannel] = undefined.Undefined(), + preferred_locale: typing.Union[undefined.Undefined, str] = undefined.Undefined(), + reason: typing.Union[undefined.Undefined, str] = undefined.Undefined(), ) -> guilds.Guild: route = routes.PATCH_GUILD.compile(guild=guild) body = binding.JSONObjectBuilder() @@ -1644,13 +1658,13 @@ async def edit_guild( body.put_snowflake("rules_channel_id", rules_channel) body.put_snowflake("public_updates_channel_id", public_updates_channel) - if not unset.is_unset(icon): + if not isinstance(icon, undefined.Undefined): body.put("icon", await icon.fetch_data_uri()) - if not unset.is_unset(splash): + if not isinstance(splash, undefined.Undefined): body.put("splash", await splash.fetch_data_uri()) - if not unset.is_unset(banner): + if not isinstance(banner, undefined.Undefined): body.put("banner", await banner.fetch_data_uri()) response = await self._request(route, body=body, reason=reason) @@ -1675,13 +1689,17 @@ async def create_guild_text_channel( guild: typing.Union[guilds.Guild, bases.UniqueObjectT], name: str, *, - position: typing.Union[int, unset.Unset] = unset.UNSET, - topic: typing.Union[str, unset.Unset] = unset.UNSET, - nsfw: typing.Union[bool, unset.Unset] = unset.UNSET, - rate_limit_per_user: typing.Union[int, unset.Unset] = unset.UNSET, - permission_overwrites: typing.Union[typing.Sequence[channels.PermissionOverwrite], unset.Unset] = unset.UNSET, - category: typing.Union[channels.GuildCategory, bases.UniqueObjectT, unset.Unset] = unset.UNSET, - reason: typing.Union[str, unset.Unset] = unset.UNSET, + position: typing.Union[int, undefined.Undefined] = undefined.Undefined(), + topic: typing.Union[str, undefined.Undefined] = undefined.Undefined(), + nsfw: typing.Union[bool, undefined.Undefined] = undefined.Undefined(), + rate_limit_per_user: typing.Union[int, undefined.Undefined] = undefined.Undefined(), + permission_overwrites: typing.Union[ + typing.Sequence[channels.PermissionOverwrite], undefined.Undefined + ] = undefined.Undefined(), + category: typing.Union[ + channels.GuildCategory, bases.UniqueObjectT, undefined.Undefined + ] = undefined.Undefined(), + reason: typing.Union[str, undefined.Undefined] = undefined.Undefined(), ) -> channels.GuildTextChannel: channel = await self._create_guild_channel( guild, @@ -1702,13 +1720,17 @@ async def create_guild_news_channel( guild: typing.Union[guilds.Guild, bases.UniqueObjectT], name: str, *, - position: typing.Union[int, unset.Unset] = unset.UNSET, - topic: typing.Union[str, unset.Unset] = unset.UNSET, - nsfw: typing.Union[bool, unset.Unset] = unset.UNSET, - rate_limit_per_user: typing.Union[int, unset.Unset] = unset.UNSET, - permission_overwrites: typing.Union[typing.Sequence[channels.PermissionOverwrite], unset.Unset] = unset.UNSET, - category: typing.Union[channels.GuildCategory, bases.UniqueObjectT, unset.Unset] = unset.UNSET, - reason: typing.Union[str, unset.Unset] = unset.UNSET, + position: typing.Union[int, undefined.Undefined] = undefined.Undefined(), + topic: typing.Union[str, undefined.Undefined] = undefined.Undefined(), + nsfw: typing.Union[bool, undefined.Undefined] = undefined.Undefined(), + rate_limit_per_user: typing.Union[int, undefined.Undefined] = undefined.Undefined(), + permission_overwrites: typing.Union[ + typing.Sequence[channels.PermissionOverwrite], undefined.Undefined + ] = undefined.Undefined(), + category: typing.Union[ + channels.GuildCategory, bases.UniqueObjectT, undefined.Undefined + ] = undefined.Undefined(), + reason: typing.Union[str, undefined.Undefined] = undefined.Undefined(), ) -> channels.GuildNewsChannel: channel = await self._create_guild_channel( guild, @@ -1729,13 +1751,17 @@ async def create_guild_voice_channel( guild: typing.Union[guilds.Guild, bases.UniqueObjectT], name: str, *, - position: typing.Union[int, unset.Unset] = unset.UNSET, - nsfw: typing.Union[bool, unset.Unset] = unset.UNSET, - user_limit: typing.Union[int, unset.Unset] = unset.UNSET, - bitrate: typing.Union[int, unset.Unset] = unset.UNSET, - permission_overwrites: typing.Union[typing.Sequence[channels.PermissionOverwrite], unset.Unset] = unset.UNSET, - category: typing.Union[channels.GuildCategory, bases.UniqueObjectT, unset.Unset] = unset.UNSET, - reason: typing.Union[str, unset.Unset] = unset.UNSET, + position: typing.Union[int, undefined.Undefined] = undefined.Undefined(), + nsfw: typing.Union[bool, undefined.Undefined] = undefined.Undefined(), + user_limit: typing.Union[int, undefined.Undefined] = undefined.Undefined(), + bitrate: typing.Union[int, undefined.Undefined] = undefined.Undefined(), + permission_overwrites: typing.Union[ + typing.Sequence[channels.PermissionOverwrite], undefined.Undefined + ] = undefined.Undefined(), + category: typing.Union[ + channels.GuildCategory, bases.UniqueObjectT, undefined.Undefined + ] = undefined.Undefined(), + reason: typing.Union[str, undefined.Undefined] = undefined.Undefined(), ) -> channels.GuildVoiceChannel: channel = await self._create_guild_channel( guild, @@ -1756,10 +1782,12 @@ async def create_guild_category( guild: typing.Union[guilds.Guild, bases.UniqueObjectT], name: str, *, - position: typing.Union[int, unset.Unset] = unset.UNSET, - nsfw: typing.Union[bool, unset.Unset] = unset.UNSET, - permission_overwrites: typing.Union[typing.Sequence[channels.PermissionOverwrite], unset.Unset] = unset.UNSET, - reason: typing.Union[str, unset.Unset] = unset.UNSET, + position: typing.Union[int, undefined.Undefined] = undefined.Undefined(), + nsfw: typing.Union[bool, undefined.Undefined] = undefined.Undefined(), + permission_overwrites: typing.Union[ + typing.Sequence[channels.PermissionOverwrite], undefined.Undefined + ] = undefined.Undefined(), + reason: typing.Union[str, undefined.Undefined] = undefined.Undefined(), ) -> channels.GuildCategory: channel = await self._create_guild_channel( guild, @@ -1778,15 +1806,19 @@ async def _create_guild_channel( name: str, type_: channels.ChannelType, *, - position: typing.Union[int, unset.Unset] = unset.UNSET, - topic: typing.Union[str, unset.Unset] = unset.UNSET, - nsfw: typing.Union[bool, unset.Unset] = unset.UNSET, - bitrate: typing.Union[int, unset.Unset] = unset.UNSET, - user_limit: typing.Union[int, unset.Unset] = unset.UNSET, - rate_limit_per_user: typing.Union[int, unset.Unset] = unset.UNSET, - permission_overwrites: typing.Union[typing.Sequence[channels.PermissionOverwrite], unset.Unset] = unset.UNSET, - category: typing.Union[channels.GuildCategory, bases.UniqueObjectT, unset.Unset] = unset.UNSET, - reason: typing.Union[str, unset.Unset] = unset.UNSET, + position: typing.Union[int, undefined.Undefined] = undefined.Undefined(), + topic: typing.Union[str, undefined.Undefined] = undefined.Undefined(), + nsfw: typing.Union[bool, undefined.Undefined] = undefined.Undefined(), + bitrate: typing.Union[int, undefined.Undefined] = undefined.Undefined(), + user_limit: typing.Union[int, undefined.Undefined] = undefined.Undefined(), + rate_limit_per_user: typing.Union[int, undefined.Undefined] = undefined.Undefined(), + permission_overwrites: typing.Union[ + typing.Sequence[channels.PermissionOverwrite], undefined.Undefined + ] = undefined.Undefined(), + category: typing.Union[ + channels.GuildCategory, bases.UniqueObjectT, undefined.Undefined + ] = undefined.Undefined(), + reason: typing.Union[str, undefined.Undefined] = undefined.Undefined(), ) -> channels.GuildChannel: route = routes.POST_GUILD_CHANNELS.compile(guild=guild) body = binding.JSONObjectBuilder() @@ -1835,14 +1867,16 @@ async def edit_member( guild: typing.Union[guilds.Guild, bases.UniqueObjectT], user: typing.Union[users.User, bases.UniqueObjectT], *, - nick: typing.Union[unset.Unset, str] = unset.UNSET, + nick: typing.Union[undefined.Undefined, str] = undefined.Undefined(), roles: typing.Union[ - unset.Unset, typing.Collection[typing.Union[guilds.Role, bases.UniqueObjectT]] - ] = unset.UNSET, - mute: typing.Union[unset.Unset, bool] = unset.UNSET, - deaf: typing.Union[unset.Unset, bool] = unset.UNSET, - voice_channel: typing.Union[unset.Unset, channels.GuildVoiceChannel, bases.UniqueObjectT, None] = unset.UNSET, - reason: typing.Union[str, unset.Unset] = unset.UNSET, + undefined.Undefined, typing.Collection[typing.Union[guilds.Role, bases.UniqueObjectT]] + ] = undefined.Undefined(), + mute: typing.Union[undefined.Undefined, bool] = undefined.Undefined(), + deaf: typing.Union[undefined.Undefined, bool] = undefined.Undefined(), + voice_channel: typing.Union[ + undefined.Undefined, channels.GuildVoiceChannel, bases.UniqueObjectT, None + ] = undefined.Undefined(), + reason: typing.Union[str, undefined.Undefined] = undefined.Undefined(), ) -> None: route = routes.PATCH_GUILD_MEMBER.compile(guild=guild, user=user) body = binding.JSONObjectBuilder() @@ -1853,7 +1887,7 @@ async def edit_member( if voice_channel is None: body.put("channel_id", None) - elif not unset.is_unset(voice_channel): + elif not isinstance(voice_channel, undefined.Undefined): body.put_snowflake("channel_id", voice_channel) await self._request(route, body=body, reason=reason) @@ -1863,7 +1897,7 @@ async def edit_my_nick( guild: typing.Union[guilds.Guild, bases.UniqueObjectT], nick: typing.Optional[str], *, - reason: typing.Union[unset.Unset, str] = unset.UNSET, + reason: typing.Union[undefined.Undefined, str] = undefined.Undefined(), ) -> None: route = routes.PATCH_MY_GUILD_NICKNAME.compile(guild=guild) body = binding.JSONObjectBuilder() @@ -1876,7 +1910,7 @@ async def add_role_to_member( user: typing.Union[users.User, bases.UniqueObjectT], role: typing.Union[guilds.Role, bases.UniqueObjectT], *, - reason: typing.Union[unset.Unset, str] = unset.UNSET, + reason: typing.Union[undefined.Undefined, str] = undefined.Undefined(), ) -> None: route = routes.PUT_GUILD_MEMBER_ROLE.compile(guild=guild, user=user, role=role) await self._request(route, reason=reason) @@ -1887,7 +1921,7 @@ async def remove_role_from_member( user: typing.Union[users.User, bases.UniqueObjectT], role: typing.Union[guilds.Role, bases.UniqueObjectT], *, - reason: typing.Union[unset.Unset, str] = unset.UNSET, + reason: typing.Union[undefined.Undefined, str] = undefined.Undefined(), ) -> None: route = routes.DELETE_GUILD_MEMBER_ROLE.compile(guild=guild, user=user, role=role) await self._request(route, reason=reason) @@ -1897,7 +1931,7 @@ async def kick_member( guild: typing.Union[guilds.Guild, bases.UniqueObjectT], user: typing.Union[users.User, bases.UniqueObjectT], *, - reason: typing.Union[unset.Unset, str] = unset.UNSET, + reason: typing.Union[undefined.Undefined, str] = undefined.Undefined(), ) -> None: route = routes.DELETE_GUILD_MEMBER.compile(guild=guild, user=user,) await self._request(route, reason=reason) @@ -1907,7 +1941,7 @@ async def ban_user( guild: typing.Union[guilds.Guild, bases.UniqueObjectT], user: typing.Union[users.User, bases.UniqueObjectT], *, - reason: typing.Union[unset.Unset, str] = unset.UNSET, + reason: typing.Union[undefined.Undefined, str] = undefined.Undefined(), ) -> None: route = routes.PUT_GUILD_BAN.compile(guild=guild, user=user) await self._request(route, reason=reason) @@ -1917,7 +1951,7 @@ async def unban_user( guild: typing.Union[guilds.Guild, bases.UniqueObjectT], user: typing.Union[users.User, bases.UniqueObjectT], *, - reason: typing.Union[unset.Unset, str] = unset.UNSET, + reason: typing.Union[undefined.Undefined, str] = undefined.Undefined(), ) -> None: route = routes.DELETE_GUILD_BAN.compile(guild=guild, user=user) await self._request(route, reason=reason) @@ -1950,15 +1984,15 @@ async def create_role( guild: typing.Union[guilds.Guild, bases.UniqueObjectT], /, *, - name: typing.Union[unset.Unset, str] = unset.UNSET, - permissions: typing.Union[unset.Unset, permissions_.Permission] = unset.UNSET, - color: typing.Union[unset.Unset, colors.Color] = unset.UNSET, - colour: typing.Union[unset.Unset, colors.Color] = unset.UNSET, - hoist: typing.Union[unset.Unset, bool] = unset.UNSET, - mentionable: typing.Union[unset.Unset, bool] = unset.UNSET, - reason: typing.Union[unset.Unset, str] = unset.UNSET, + name: typing.Union[undefined.Undefined, str] = undefined.Undefined(), + permissions: typing.Union[undefined.Undefined, permissions_.Permission] = undefined.Undefined(), + color: typing.Union[undefined.Undefined, colors.Color] = undefined.Undefined(), + colour: typing.Union[undefined.Undefined, colors.Color] = undefined.Undefined(), + hoist: typing.Union[undefined.Undefined, bool] = undefined.Undefined(), + mentionable: typing.Union[undefined.Undefined, bool] = undefined.Undefined(), + reason: typing.Union[undefined.Undefined, str] = undefined.Undefined(), ) -> guilds.Role: - if not unset.count_unset_objects(color, colour): + if not undefined.Undefined.count(color, colour): raise TypeError("Can not specify 'color' and 'colour' together.") route = routes.POST_GUILD_ROLES.compile(guild=guild) @@ -1987,15 +2021,15 @@ async def edit_role( guild: typing.Union[guilds.Guild, bases.UniqueObjectT], role: typing.Union[guilds.Role, bases.UniqueObjectT], *, - name: typing.Union[unset.Unset, str] = unset.UNSET, - permissions: typing.Union[unset.Unset, permissions_.Permission] = unset.UNSET, - color: typing.Union[unset.Unset, colors.Color] = unset.UNSET, - colour: typing.Union[unset.Unset, colors.Color] = unset.UNSET, - hoist: typing.Union[unset.Unset, bool] = unset.UNSET, - mentionable: typing.Union[unset.Unset, bool] = unset.UNSET, - reason: typing.Union[unset.Unset, str] = unset.UNSET, + name: typing.Union[undefined.Undefined, str] = undefined.Undefined(), + permissions: typing.Union[undefined.Undefined, permissions_.Permission] = undefined.Undefined(), + color: typing.Union[undefined.Undefined, colors.Color] = undefined.Undefined(), + colour: typing.Union[undefined.Undefined, colors.Color] = undefined.Undefined(), + hoist: typing.Union[undefined.Undefined, bool] = undefined.Undefined(), + mentionable: typing.Union[undefined.Undefined, bool] = undefined.Undefined(), + reason: typing.Union[undefined.Undefined, str] = undefined.Undefined(), ) -> guilds.Role: - if not unset.count_unset_objects(color, colour): + if not undefined.Undefined.count(color, colour): raise TypeError("Can not specify 'color' and 'colour' together.") route = routes.PATCH_GUILD_ROLE.compile(guild=guild, role=role) @@ -2033,7 +2067,7 @@ async def begin_guild_prune( guild: typing.Union[guilds.Guild, bases.UniqueObjectT], days: int, *, - reason: typing.Union[unset.Unset, str], + reason: typing.Union[undefined.Undefined, str], ) -> int: route = routes.POST_GUILD_PRUNE.compile(guild=guild) query = binding.StringMapBuilder() @@ -2068,10 +2102,10 @@ async def edit_integration( guild: typing.Union[guilds.Guild, bases.UniqueObjectT], integration: typing.Union[guilds.Integration, bases.UniqueObjectT], *, - expire_behaviour: typing.Union[unset.Unset, guilds.IntegrationExpireBehaviour] = unset.UNSET, - expire_grace_period: typing.Union[unset.Unset, date.TimeSpan] = unset.UNSET, - enable_emojis: typing.Union[unset.Unset, bool] = unset.UNSET, - reason: typing.Union[unset.Unset, str] = unset.UNSET, + expire_behaviour: typing.Union[undefined.Undefined, guilds.IntegrationExpireBehaviour] = undefined.Undefined(), + expire_grace_period: typing.Union[undefined.Undefined, date.TimeSpan] = undefined.Undefined(), + enable_emojis: typing.Union[undefined.Undefined, bool] = undefined.Undefined(), + reason: typing.Union[undefined.Undefined, str] = undefined.Undefined(), ) -> None: route = routes.PATCH_GUILD_INTEGRATION.compile(guild=guild, integration=integration) body = binding.JSONObjectBuilder() @@ -2086,7 +2120,7 @@ async def delete_integration( guild: typing.Union[guilds.Guild, bases.UniqueObjectT], integration: typing.Union[guilds.Integration, bases.UniqueObjectT], *, - reason: typing.Union[unset.Unset, str] = unset.UNSET, + reason: typing.Union[undefined.Undefined, str] = undefined.Undefined(), ) -> None: route = routes.DELETE_GUILD_INTEGRATION.compile(guild=guild, integration=integration) await self._request(route, reason=reason) @@ -2109,9 +2143,11 @@ async def edit_widget( guild: typing.Union[guilds.Guild, bases.UniqueObjectT], /, *, - channel: typing.Union[unset.Unset, channels.GuildChannel, bases.UniqueObjectT, None] = unset.UNSET, - enabled: typing.Union[unset.Unset, bool] = unset.UNSET, - reason: typing.Union[unset.Unset, str] = unset.UNSET, + channel: typing.Union[ + undefined.Undefined, channels.GuildChannel, bases.UniqueObjectT, None + ] = undefined.Undefined(), + enabled: typing.Union[undefined.Undefined, bool] = undefined.Undefined(), + reason: typing.Union[undefined.Undefined, str] = undefined.Undefined(), ) -> guilds.GuildWidget: route = routes.PATCH_GUILD_WIDGET.compile(guild=guild) @@ -2119,7 +2155,7 @@ async def edit_widget( body.put("enabled", enabled) if channel is None: body.put("channel", None) - elif not unset.is_unset(channel): + elif not isinstance(channel, undefined.Undefined): body.put_snowflake("channel", channel) response = await self._request(route, body=body, reason=reason) diff --git a/hikari/net/rest_utils.py b/hikari/net/rest_utils.py index 276ad0a385..3de7e68850 100644 --- a/hikari/net/rest_utils.py +++ b/hikari/net/rest_utils.py @@ -43,7 +43,7 @@ from hikari.utilities import binding from hikari.utilities import date from hikari.utilities import snowflake as snowflake_ -from hikari.utilities import unset +from hikari.utilities import undefined if typing.TYPE_CHECKING: from hikari.models import channels @@ -92,14 +92,18 @@ class GuildBuilder: _app: app_.IRESTApp _channels: typing.MutableSequence[binding.JSONObject] = attr.ib(factory=list) _counter: int = 0 - _name: typing.Union[unset.Unset, str] + _name: typing.Union[undefined.Undefined, str] _request_call: typing.Callable[..., typing.Coroutine[None, typing.Any, binding.JSONObject]] _roles: typing.MutableSequence[binding.JSONObject] = attr.ib(factory=list) - default_message_notifications: typing.Union[unset.Unset, guilds.GuildMessageNotificationsLevel] = unset.UNSET - explicit_content_filter_level: typing.Union[unset.Unset, guilds.GuildExplicitContentFilterLevel] = unset.UNSET - icon: typing.Union[unset.Unset, files.BaseStream] = unset.UNSET - region: typing.Union[unset.Unset, str] = unset.UNSET - verification_level: typing.Union[unset.Unset, guilds.GuildVerificationLevel] = unset.UNSET + default_message_notifications: typing.Union[ + undefined.Undefined, guilds.GuildMessageNotificationsLevel + ] = undefined.Undefined() + explicit_content_filter_level: typing.Union[ + undefined.Undefined, guilds.GuildExplicitContentFilterLevel + ] = undefined.Undefined() + icon: typing.Union[undefined.Undefined, files.BaseStream] = undefined.Undefined() + region: typing.Union[undefined.Undefined, str] = undefined.Undefined() + verification_level: typing.Union[undefined.Undefined, guilds.GuildVerificationLevel] = undefined.Undefined() @property def name(self) -> str: @@ -120,7 +124,7 @@ async def create(self) -> guilds.Guild: payload.put("default_message_notifications", self.default_message_notifications) payload.put("explicit_content_filter", self.explicit_content_filter_level) - if not unset.is_unset(self.icon): + if not isinstance(self.icon, undefined.Undefined): payload.put("icon", await self.icon.fetch_data_uri()) response = await self._request_call(route, body=payload) @@ -131,17 +135,17 @@ def add_role( name: str, /, *, - color: typing.Union[unset.Unset, colors.Color] = unset.UNSET, - colour: typing.Union[unset.Unset, colors.Color] = unset.UNSET, - hoisted: typing.Union[unset.Unset, bool] = unset.UNSET, - mentionable: typing.Union[unset.Unset, bool] = unset.UNSET, - permissions: typing.Union[unset.Unset, permissions_.Permission] = unset.UNSET, - position: typing.Union[unset.Unset, int] = unset.UNSET, + color: typing.Union[undefined.Undefined, colors.Color] = undefined.Undefined(), + colour: typing.Union[undefined.Undefined, colors.Color] = undefined.Undefined(), + hoisted: typing.Union[undefined.Undefined, bool] = undefined.Undefined(), + mentionable: typing.Union[undefined.Undefined, bool] = undefined.Undefined(), + permissions: typing.Union[undefined.Undefined, permissions_.Permission] = undefined.Undefined(), + position: typing.Union[undefined.Undefined, int] = undefined.Undefined(), ) -> snowflake_.Snowflake: if len(self._roles) == 0 and name != "@everyone": raise ValueError("First role must always be the @everyone role") - if not unset.count_unset_objects(color, colour): + if not undefined.Undefined.count(color, colour): raise TypeError("Cannot specify 'color' and 'colour' together.") snowflake = self._new_snowflake() @@ -162,9 +166,11 @@ def add_category( name: str, /, *, - position: typing.Union[unset.Unset, int] = unset.UNSET, - permission_overwrites: typing.Union[unset.Unset, typing.Collection[channels.PermissionOverwrite]] = unset.UNSET, - nsfw: typing.Union[unset.Unset, bool] = unset.UNSET, + position: typing.Union[undefined.Undefined, int] = undefined.Undefined(), + permission_overwrites: typing.Union[ + undefined.Undefined, typing.Collection[channels.PermissionOverwrite] + ] = undefined.Undefined(), + nsfw: typing.Union[undefined.Undefined, bool] = undefined.Undefined(), ) -> snowflake_.Snowflake: snowflake = self._new_snowflake() payload = binding.JSONObjectBuilder() @@ -186,12 +192,14 @@ def add_text_channel( name: str, /, *, - parent_id: snowflake_.Snowflake = unset.UNSET, - topic: typing.Union[unset.Unset, str] = unset.UNSET, - rate_limit_per_user: typing.Union[unset.Unset, date.TimeSpan] = unset.UNSET, - position: typing.Union[unset.Unset, int] = unset.UNSET, - permission_overwrites: typing.Union[unset.Unset, typing.Collection[channels.PermissionOverwrite]] = unset.UNSET, - nsfw: typing.Union[unset.Unset, bool] = unset.UNSET, + parent_id: snowflake_.Snowflake = undefined.Undefined(), + topic: typing.Union[undefined.Undefined, str] = undefined.Undefined(), + rate_limit_per_user: typing.Union[undefined.Undefined, date.TimeSpan] = undefined.Undefined(), + position: typing.Union[undefined.Undefined, int] = undefined.Undefined(), + permission_overwrites: typing.Union[ + undefined.Undefined, typing.Collection[channels.PermissionOverwrite] + ] = undefined.Undefined(), + nsfw: typing.Union[undefined.Undefined, bool] = undefined.Undefined(), ) -> snowflake_.Snowflake: snowflake = self._new_snowflake() payload = binding.JSONObjectBuilder() @@ -216,12 +224,14 @@ def add_voice_channel( name: str, /, *, - parent_id: snowflake_.Snowflake = unset.UNSET, - bitrate: typing.Union[unset.Unset, int] = unset.UNSET, - position: typing.Union[unset.Unset, int] = unset.UNSET, - permission_overwrites: typing.Union[unset.Unset, typing.Collection[channels.PermissionOverwrite]] = unset.UNSET, - nsfw: typing.Union[unset.Unset, bool] = unset.UNSET, - user_limit: typing.Union[unset.Unset, int] = unset.UNSET, + parent_id: snowflake_.Snowflake = undefined.Undefined(), + bitrate: typing.Union[undefined.Undefined, int] = undefined.Undefined(), + position: typing.Union[undefined.Undefined, int] = undefined.Undefined(), + permission_overwrites: typing.Union[ + undefined.Undefined, typing.Collection[channels.PermissionOverwrite] + ] = undefined.Undefined(), + nsfw: typing.Union[undefined.Undefined, bool] = undefined.Undefined(), + user_limit: typing.Union[undefined.Undefined, int] = undefined.Undefined(), ) -> snowflake_.Snowflake: snowflake = self._new_snowflake() payload = binding.JSONObjectBuilder() diff --git a/hikari/utilities/binding.py b/hikari/utilities/binding.py index 5a0e37c3bd..b4c574e9fc 100644 --- a/hikari/utilities/binding.py +++ b/hikari/utilities/binding.py @@ -43,7 +43,7 @@ import aiohttp.typedefs from hikari.models import bases -from hikari.utilities import unset +from hikari.utilities import undefined Headers = typing.Mapping[str, str] """HTTP headers.""" @@ -108,7 +108,7 @@ def __init__(self): def put( self, key: str, - value: typing.Union[unset.Unset, typing.Any], + value: typing.Union[undefined.Undefined, typing.Any], conversion: typing.Optional[typing.Callable[[typing.Any], typing.Any]] = None, ) -> None: """Add a key and value to the string map. @@ -117,7 +117,7 @@ def put( ---------- key : str The string key. - value : hikari.utilities.unset.Unset | typing.Any + value : hikari.utilities.undefined.Undefined | typing.Any The value to set. conversion : typing.Callable[[typing.Any], typing.Any] | None An optional conversion to perform. @@ -125,7 +125,7 @@ def put( !!! note The value will always be cast to a `str` before inserting it. """ - if not unset.is_unset(value): + if not isinstance(value, undefined.Undefined): if conversion is not None: value = conversion(value) @@ -143,11 +143,11 @@ def put( self[key] = value @classmethod - def from_dict(cls, d: typing.Union[unset.Unset, typing.Dict[str, typing.Any]]) -> StringMapBuilder: + def from_dict(cls, d: typing.Union[undefined.Undefined, typing.Dict[str, typing.Any]]) -> StringMapBuilder: """Build a query from an existing dict.""" sb = cls() - if unset.is_unset(d): + if isinstance(d, undefined.Undefined): return sb for k, v in d.items(): @@ -178,14 +178,14 @@ def put( ---------- key : JSONString The key to give the element. - value : JSONType | typing.Any | hikari.utilities.unset.Unset + value : JSONType | typing.Any | hikari.utilities.undefined.Undefined The JSON type to put. This may be a non-JSON type if a conversion is also specified. This may alternatively be unset. In the latter case, nothing is performed. conversion : typing.Callable[[typing.Any], JSONType] | None Optional conversion to apply. """ - if not unset.is_unset(value): + if not isinstance(value, undefined.Undefined): if conversion is not None: self[key] = conversion(value) else: @@ -194,7 +194,7 @@ def put( def put_array( self, key: JSONString, - values: typing.Union[unset.Unset, typing.Iterable[_T]], + values: typing.Union[undefined.Undefined, typing.Iterable[_T]], conversion: typing.Optional[typing.Callable[[_T], JSONAny]] = None, ) -> None: """Put a JSON array. @@ -205,35 +205,35 @@ def put_array( ---------- key : JSONString The key to give the element. - values : JSONType | typing.Any | hikari.utilities.unset.Unset + values : JSONType | typing.Any | hikari.utilities.undefined.Undefined The JSON types to put. This may be an iterable of non-JSON types if a conversion is also specified. This may alternatively be unset. In the latter case, nothing is performed. conversion : typing.Callable[[typing.Any], JSONType] | None Optional conversion to apply. """ - if not unset.is_unset(values): + if not isinstance(values, undefined.Undefined): if conversion is not None: self[key] = [conversion(value) for value in values] else: self[key] = list(values) - def put_snowflake(self, key: JSONString, value: typing.Union[unset.Unset, typing.SupportsInt, int]) -> None: + def put_snowflake(self, key: JSONString, value: typing.Union[undefined.Undefined, typing.SupportsInt, int]) -> None: """Put a snowflake. Parameters ---------- key : JSONString The key to give the element. - value : JSONType | hikari.utilities.unset.Unset + value : JSONType | hikari.utilities.undefined.Undefined The JSON type to put. This may alternatively be unset. In the latter case, nothing is performed. """ - if not unset.is_unset(value): + if not isinstance(value, undefined.Undefined): self[key] = str(int(value)) def put_snowflake_array( - self, key: JSONString, values: typing.Union[unset.Unset, typing.Iterable[typing.SupportsInt, int]] + self, key: JSONString, values: typing.Union[undefined.Undefined, typing.Iterable[typing.SupportsInt, int]] ) -> None: """Put an array of snowflakes. @@ -241,11 +241,11 @@ def put_snowflake_array( ---------- key : JSONString The key to give the element. - values : typing.Iterable[typing.SupportsInt, int] | hikari.utilities.unset.Unset + values : typing.Iterable[typing.SupportsInt, int] | hikari.utilities.undefined.Undefined The JSON snowflakes to put. This may alternatively be unset. In the latter case, nothing is performed. """ - if not unset.is_unset(values): + if not isinstance(values, undefined.Undefined): self[key] = [str(int(value)) for value in values] diff --git a/hikari/utilities/klass.py b/hikari/utilities/klass.py index bc9ea642ff..9ae38dcaaf 100644 --- a/hikari/utilities/klass.py +++ b/hikari/utilities/klass.py @@ -74,6 +74,8 @@ class SingletonMeta(type): ___instances___ = {} + # Disable type-checking to hide a bug in IntelliJ for the time being. + @typing.no_type_check def __call__(cls): if cls not in SingletonMeta.___instances___: SingletonMeta.___instances___[cls] = super().__call__() @@ -103,3 +105,6 @@ class Singleton(metaclass=SingletonMeta): """ __slots__ = () + + def __init__(self): + pass diff --git a/hikari/utilities/undefined.py b/hikari/utilities/undefined.py new file mode 100644 index 0000000000..1ff4f0003f --- /dev/null +++ b/hikari/utilities/undefined.py @@ -0,0 +1,113 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# Copyright © Nekoka.tt 2019-2020 +# +# This file is part of Hikari. +# +# Hikari is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Hikari is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Hikari. If not, see . +"""Singleton used throughout the library to denote values that are not present.""" + +from __future__ import annotations + +__all__ = ["Undefined"] + +import typing + +from hikari.utilities import klass + + +class Undefined(klass.Singleton): + """A singleton value that represents an undefined field or argument. + + Undefined will always have a falsy value. + + This type exists to allow differentiation between values that are + "optional" in the sense that they can be `None` (which is the + definition of "optional" in Python's `typing` module), and + values that are truly "optional" in that they may not be present. + + Some cases in Discord's API exist where passing a value as `None` or + `null` have totally different semantics to not passing the value at all. + The most noticeable case of this is for "patch" REST endpoints where + specifying a value will cause it to be updated, but not specifying it + will result in it being left alone. + + This type differs from `None` in a few different ways. Firstly, + it will only ever be considered equal to itself, thus the following will + always be false. + + >>> Undefined() == None + False + + The type will always be equatable to itself. + + >>> Undefined() == Undefined() + True + + The second differentiation is that you always instantiate this class to + obtain an instance of it. + + >>> undefined_value = Undefined() + + ...since this is a singleton, this value will always return the same + physical object. This improves efficiency. + + The third differentiation is that you can iterate across an undefined value. + This is used to simplify logic elsewhere. The behaviour of iterating across + an undefined value is to simply return an iterator that immediately + completes. + + >>> [*Undefined()] + [] + + This type cannot be mutated, subclassed, or have attributes removed from it + using conventional methods. + """ + + __slots__ = () + + def __bool__(self) -> bool: + return False + + def __repr__(self) -> str: + return type(self).__name__.upper() + + def __iter__(self) -> typing.Iterator[None]: + yield from () + + __str__ = __repr__ + + def __init_subclass__(cls, **kwargs: typing.Any) -> typing.NoReturn: + raise TypeError("Cannot subclass Undefined type") + + def __setattr__(self, _, __) -> typing.NoReturn: + raise TypeError("Cannot modify Undefined type") + + def __delattr__(self, _) -> typing.NoReturn: + raise TypeError("Cannot modify Undefined type") + + def __eq__(self, other: typing.Any) -> bool: + return other is self + + @staticmethod + def count(*objs: typing.Any) -> int: + """Count how many of the given objects are undefined values. + + Returns + ------- + int + The number of undefined values given. + """ + undefined = Undefined() + return sum(o is undefined for o in objs) diff --git a/hikari/utilities/unset.py b/hikari/utilities/unset.py deleted file mode 100644 index fdaf216bdd..0000000000 --- a/hikari/utilities/unset.py +++ /dev/null @@ -1,76 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# Copyright © Nekoka.tt 2019-2020 -# -# This file is part of Hikari. -# -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . -"""Sentinel for an unset value or attribute.""" - -from __future__ import annotations - -__all__ = ["Unset", "UNSET", "is_unset"] - -import typing - -from hikari.utilities import klass - - -class Unset(klass.Singleton): - """A singleton value that represents an unset field. - - This will always have a falsified value. - """ - - __slots__ = () - - def __bool__(self) -> bool: - return False - - def __repr__(self) -> str: - return type(self).__name__.upper() - - def __iter__(self) -> typing.Iterator[None]: - yield from () - - __str__ = __repr__ - - def __init_subclass__(cls, **kwargs: typing.Any) -> typing.NoReturn: - raise TypeError("Cannot subclass Unset type") - - -T = typing.TypeVar("T", contravariant=True) - -UNSET: typing.Final[Unset] = Unset() -"""A global instance of `Unset`.""" - - -@typing.overload -def is_unset(obj: UNSET) -> typing.Literal[True]: - """Return `True` always.""" - - -@typing.overload -def is_unset(obj: typing.Any) -> typing.Literal[False]: - """Return `False` always.""" - - -def is_unset(obj): - """Return `True` if the object is an `Unset` value.""" - return isinstance(obj, Unset) - - -def count_unset_objects(obj1: typing.Any, obj2: typing.Any, *objs: typing.Any) -> int: - """Count the number of objects that are unset in the provided parameters.""" - return sum(is_unset(o) for o in (obj1, obj2, *objs)) diff --git a/tests/hikari/impl/test_entity_factory.py b/tests/hikari/impl/test_entity_factory.py index f4d3481ad2..1701bebc3a 100644 --- a/tests/hikari/impl/test_entity_factory.py +++ b/tests/hikari/impl/test_entity_factory.py @@ -37,7 +37,7 @@ from hikari.models import webhooks from hikari.models import users from hikari.models import voices -from hikari.utilities import unset +from hikari.utilities import undefined class TestEntityFactoryImpl: @@ -1360,12 +1360,12 @@ def test_deserialize_guild_member_presence_with_unset_fields( assert presence.client_status.web is guilds.PresenceStatus.OFFLINE # PresenceUser assert presence.user.id == 42 - assert presence.user.discriminator is unset.UNSET - assert presence.user.username is unset.UNSET - assert presence.user.avatar_hash is unset.UNSET - assert presence.user.is_bot is unset.UNSET - assert presence.user.is_system is unset.UNSET - assert presence.user.flags is unset.UNSET + assert presence.user.discriminator is undefined.Undefined() + assert presence.user.username is undefined.Undefined() + assert presence.user.avatar_hash is undefined.Undefined() + assert presence.user.is_bot is undefined.Undefined() + assert presence.user.is_system is undefined.Undefined() + assert presence.user.flags is undefined.Undefined() def test_deserialize_guild_member_presence_with_unset_activity_fields(self, entity_factory_impl, user_payload): presence = entity_factory_impl.deserialize_guild_member_presence( diff --git a/tests/hikari/internal/test_conversions.py b/tests/hikari/internal/test_conversions.py index 4ab65a318d..531dc74ec1 100644 --- a/tests/hikari/internal/test_conversions.py +++ b/tests/hikari/internal/test_conversions.py @@ -22,7 +22,7 @@ import pytest from hikari.utilities import conversions -from hikari.utilities import unset +from hikari.utilities import undefined def test_put_if_specified_when_specified(): @@ -35,7 +35,7 @@ def test_put_if_specified_when_specified(): def test_put_if_specified_when_unspecified(): d = {} - conversions.put_if_specified(d, "bar", unset.UNSET) + conversions.put_if_specified(d, "bar", undefined.Undefined()) assert d == {} diff --git a/tests/hikari/internal/test_unset.py b/tests/hikari/internal/test_unset.py index 4e6e05855b..6821caa842 100644 --- a/tests/hikari/internal/test_unset.py +++ b/tests/hikari/internal/test_unset.py @@ -18,31 +18,31 @@ # along with Hikari. If not, see . import pytest -from hikari.utilities import unset +from hikari.utilities import undefined from tests.hikari import _helpers class TestUnset: def test_repr(self): - assert repr(unset.UNSET) == "UNSET" + assert repr(undefined.Undefined()) == "UNSET" def test_str(self): - assert str(unset.UNSET) == "UNSET" + assert str(undefined.Undefined()) == "UNSET" def test_bool(self): - assert bool(unset.UNSET) is False + assert bool(undefined.Undefined()) is False def test_singleton_behaviour(self): - assert unset.Unset() is unset.Unset() - assert unset.UNSET is unset.Unset() + assert undefined.Unset() is undefined.Unset() + assert undefined.Undefined() is undefined.Unset() @_helpers.assert_raises(type_=TypeError) def test_cannot_subclass(self): - class _(unset.Unset): + class _(undefined.Undefined): pass class TestIsUnset: - @pytest.mark.parametrize(["obj", "is_unset"], [(unset.UNSET, True), (object(), False),]) + @pytest.mark.parametrize(["obj", "is_unset"], [(undefined.Undefined(), True), (object(), False),]) def test_is_unset(self, obj, is_unset): - assert unset.is_unset(obj) is is_unset + assert isinstance(obj, undefined.Undefined) is is_unset From f898357e6cc2b4df8568bbe7017019b3249cf296 Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Sat, 30 May 2020 12:07:25 +0100 Subject: [PATCH 412/922] Fixed a few type-hinting issues. --- hikari/cache.py | 76 ++-- hikari/entity_factory.py | 86 ++-- hikari/event_consumer.py | 6 +- hikari/impl/cache.py | 76 ++-- hikari/impl/entity_factory.py | 126 +++--- hikari/impl/event_manager.py | 4 +- hikari/impl/event_manager_core.py | 6 +- hikari/models/audit_logs.py | 5 +- hikari/models/bases.py | 2 +- hikari/net/gateway.py | 15 +- hikari/net/iterators.py | 22 +- hikari/net/rest.py | 404 +++++++++--------- hikari/net/rest_utils.py | 20 +- hikari/net/routes.py | 4 +- hikari/net/voice_gateway.py | 10 +- .../utilities/{binding.py => data_binding.py} | 9 +- hikari/utilities/klass.py | 3 - 17 files changed, 442 insertions(+), 432 deletions(-) rename hikari/utilities/{binding.py => data_binding.py} (95%) diff --git a/hikari/cache.py b/hikari/cache.py index 1e83bf445c..af839db829 100644 --- a/hikari/cache.py +++ b/hikari/cache.py @@ -39,7 +39,7 @@ from hikari.models import users from hikari.models import voices - from hikari.utilities import binding + from hikari.utilities import data_binding class ICache(component.IComponent, abc.ABC): @@ -60,15 +60,15 @@ class ICache(component.IComponent, abc.ABC): # APPLICATIONS # ################ @abc.abstractmethod - async def create_application(self, payload: binding.JSONObject) -> applications.Application: + async def create_application(self, payload: data_binding.JSONObject) -> applications.Application: ... @abc.abstractmethod - async def create_own_guild(self, payload: binding.JSONObject) -> applications.OwnGuild: + async def create_own_guild(self, payload: data_binding.JSONObject) -> applications.OwnGuild: ... @abc.abstractmethod - async def create_own_connection(self, payload: binding.JSONObject) -> applications.OwnConnection: + async def create_own_connection(self, payload: data_binding.JSONObject) -> applications.OwnConnection: ... ############## @@ -76,19 +76,19 @@ async def create_own_connection(self, payload: binding.JSONObject) -> applicatio ############## @abc.abstractmethod - async def create_audit_log_change(self, payload: binding.JSONObject) -> audit_logs.AuditLogChange: + async def create_audit_log_change(self, payload: data_binding.JSONObject) -> audit_logs.AuditLogChange: ... @abc.abstractmethod - async def create_audit_log_entry_info(self, payload: binding.JSONObject) -> audit_logs.BaseAuditLogEntryInfo: + async def create_audit_log_entry_info(self, payload: data_binding.JSONObject) -> audit_logs.BaseAuditLogEntryInfo: ... @abc.abstractmethod - async def create_audit_log_entry(self, payload: binding.JSONObject) -> audit_logs.AuditLogEntry: + async def create_audit_log_entry(self, payload: data_binding.JSONObject) -> audit_logs.AuditLogEntry: ... @abc.abstractmethod - async def create_audit_log(self, payload: binding.JSONObject) -> audit_logs.AuditLog: + async def create_audit_log(self, payload: data_binding.JSONObject) -> audit_logs.AuditLog: ... ############ @@ -96,12 +96,14 @@ async def create_audit_log(self, payload: binding.JSONObject) -> audit_logs.Audi ############ @abc.abstractmethod - async def create_channel(self, payload: binding.JSONObject, can_cache: bool = False) -> channels.PartialChannel: + async def create_channel( + self, payload: data_binding.JSONObject, can_cache: bool = False + ) -> channels.PartialChannel: ... @abc.abstractmethod async def update_channel( - self, channel: channels.PartialChannel, payload: binding.JSONObject, + self, channel: channels.PartialChannel, payload: data_binding.JSONObject, ) -> channels.PartialChannel: ... @@ -118,7 +120,7 @@ async def delete_channel(self, channel_id: int) -> typing.Optional[channels.Part ########## @abc.abstractmethod - async def create_embed(self, payload: binding.JSONObject) -> embeds.Embed: + async def create_embed(self, payload: data_binding.JSONObject) -> embeds.Embed: ... ########## @@ -126,11 +128,11 @@ async def create_embed(self, payload: binding.JSONObject) -> embeds.Embed: ########## @abc.abstractmethod - async def create_emoji(self, payload: binding.JSONObject, can_cache: bool = False) -> emojis.Emoji: + async def create_emoji(self, payload: data_binding.JSONObject, can_cache: bool = False) -> emojis.Emoji: ... @abc.abstractmethod - async def update_emoji(self, payload: binding.JSONObject) -> emojis.Emoji: + async def update_emoji(self, payload: data_binding.JSONObject) -> emojis.Emoji: ... @abc.abstractmethod @@ -146,7 +148,7 @@ async def delete_emoji(self, emoji_id: int) -> typing.Optional[emojis.KnownCusto ########### @abc.abstractmethod - async def create_gateway_bot(self, payload: binding.JSONObject) -> gateway.GatewayBot: + async def create_gateway_bot(self, payload: data_binding.JSONObject) -> gateway.GatewayBot: ... ########## @@ -154,12 +156,12 @@ async def create_gateway_bot(self, payload: binding.JSONObject) -> gateway.Gatew ########## @abc.abstractmethod - async def create_member(self, payload: binding.JSONObject, can_cache: bool = False) -> guilds.GuildMember: + async def create_member(self, payload: data_binding.JSONObject, can_cache: bool = False) -> guilds.GuildMember: # TODO: revisit for the voodoo to make a member into a special user. ... @abc.abstractmethod - async def update_member(self, member: guilds.GuildMember, payload: binding.JSONObject) -> guilds.GuildMember: + async def update_member(self, member: guilds.GuildMember, payload: data_binding.JSONObject) -> guilds.GuildMember: ... @abc.abstractmethod @@ -171,11 +173,11 @@ async def delete_member(self, guild_id: int, user_id: int) -> typing.Optional[gu ... @abc.abstractmethod - async def create_role(self, payload: binding.JSONObject, can_cache: bool = False) -> guilds.PartialRole: + async def create_role(self, payload: data_binding.JSONObject, can_cache: bool = False) -> guilds.PartialRole: ... @abc.abstractmethod - async def update_role(self, role: guilds.PartialRole, payload: binding.JSONObject) -> guilds.PartialRole: + async def update_role(self, role: guilds.PartialRole, payload: data_binding.JSONObject) -> guilds.PartialRole: ... @abc.abstractmethod @@ -187,12 +189,14 @@ async def delete_role(self, guild_id: int, role_id: int) -> typing.Optional[guil ... @abc.abstractmethod - async def create_presence(self, payload: binding.JSONObject, can_cache: bool = False) -> guilds.GuildMemberPresence: + async def create_presence( + self, payload: data_binding.JSONObject, can_cache: bool = False + ) -> guilds.GuildMemberPresence: ... @abc.abstractmethod async def update_presence( - self, role: guilds.GuildMemberPresence, payload: binding.JSONObject + self, role: guilds.GuildMemberPresence, payload: data_binding.JSONObject ) -> guilds.GuildMemberPresence: ... @@ -205,19 +209,19 @@ async def delete_presence(self, guild_id: int, user_id: int) -> typing.Optional[ ... @abc.abstractmethod - async def create_guild_ban(self, payload: binding.JSONObject) -> guilds.GuildMemberBan: + async def create_guild_ban(self, payload: data_binding.JSONObject) -> guilds.GuildMemberBan: ... @abc.abstractmethod - async def create_guild_integration(self, payload: binding.JSONObject) -> guilds.PartialIntegration: + async def create_guild_integration(self, payload: data_binding.JSONObject) -> guilds.PartialIntegration: ... @abc.abstractmethod - async def create_guild(self, payload: binding.JSONObject, can_cache: bool = False) -> guilds.PartialGuild: + async def create_guild(self, payload: data_binding.JSONObject, can_cache: bool = False) -> guilds.PartialGuild: ... @abc.abstractmethod - async def update_guild(self, guild: guilds.PartialGuild, payload: binding.JSONObject) -> guilds.PartialGuild: + async def update_guild(self, guild: guilds.PartialGuild, payload: data_binding.JSONObject) -> guilds.PartialGuild: ... @abc.abstractmethod @@ -229,29 +233,29 @@ async def delete_guild(self, guild_id: int) -> typing.Optional[guilds.PartialGui ... @abc.abstractmethod - async def create_guild_preview(self, payload: binding.JSONObject) -> guilds.GuildPreview: + async def create_guild_preview(self, payload: data_binding.JSONObject) -> guilds.GuildPreview: ... ########### # INVITES # ########### @abc.abstractmethod - async def create_invite(self, payload: binding.JSONObject) -> invites.Invite: + async def create_invite(self, payload: data_binding.JSONObject) -> invites.Invite: ... ############ # MESSAGES # ############ @abc.abstractmethod - async def create_reaction(self, payload: binding.JSONObject) -> messages.Reaction: + async def create_reaction(self, payload: data_binding.JSONObject) -> messages.Reaction: ... @abc.abstractmethod - async def create_message(self, payload: binding.JSONObject, can_cache: bool = False) -> messages.Message: + async def create_message(self, payload: data_binding.JSONObject, can_cache: bool = False) -> messages.Message: ... @abc.abstractmethod - async def update_message(self, message: messages.Message, payload: binding.JSONObject) -> messages.Message: + async def update_message(self, message: messages.Message, payload: data_binding.JSONObject) -> messages.Message: ... @abc.abstractmethod @@ -266,11 +270,11 @@ async def delete_message(self, channel_id: int, message_id: int) -> typing.Optio # USERS # ######### @abc.abstractmethod - async def create_user(self, payload: binding.JSONObject, can_cache: bool = False) -> users.User: + async def create_user(self, payload: data_binding.JSONObject, can_cache: bool = False) -> users.User: ... @abc.abstractmethod - async def update_user(self, user: users.User, payload: binding.JSONObject) -> users.User: + async def update_user(self, user: users.User, payload: data_binding.JSONObject) -> users.User: ... @abc.abstractmethod @@ -282,11 +286,11 @@ async def delete_user(self, user_id: int) -> typing.Optional[users.User]: ... @abc.abstractmethod - async def create_my_user(self, payload: binding.JSONObject, can_cache: bool = False) -> users.MyUser: + async def create_my_user(self, payload: data_binding.JSONObject, can_cache: bool = False) -> users.MyUser: ... @abc.abstractmethod - async def update_my_user(self, my_user: users.MyUser, payload: binding.JSONObject) -> users.MyUser: + async def update_my_user(self, my_user: users.MyUser, payload: data_binding.JSONObject) -> users.MyUser: ... @abc.abstractmethod @@ -297,11 +301,11 @@ async def get_my_user(self) -> typing.Optional[users.User]: # VOICES # ########## @abc.abstractmethod - async def create_voice_state(self, payload: binding.JSONObject, can_cache: bool = False) -> voices.VoiceState: + async def create_voice_state(self, payload: data_binding.JSONObject, can_cache: bool = False) -> voices.VoiceState: ... @abc.abstractmethod - async def update_voice_state(self, payload: binding.JSONObject) -> voices.VoiceState: + async def update_voice_state(self, payload: data_binding.JSONObject) -> voices.VoiceState: ... @abc.abstractmethod @@ -313,5 +317,5 @@ async def delete_voice_state(self, guild_id: int, channel_id: int) -> typing.Opt ... @abc.abstractmethod - async def create_voice_region(self, payload: binding.JSONObject) -> voices.VoiceRegion: + async def create_voice_region(self, payload: data_binding.JSONObject) -> voices.VoiceRegion: ... diff --git a/hikari/entity_factory.py b/hikari/entity_factory.py index 8f8a8ec09e..9c77f50e93 100644 --- a/hikari/entity_factory.py +++ b/hikari/entity_factory.py @@ -40,7 +40,7 @@ from hikari.models import voices from hikari.models import webhooks - from hikari.utilities import binding + from hikari.utilities import data_binding class IEntityFactory(component.IComponent, abc.ABC): @@ -53,7 +53,7 @@ class IEntityFactory(component.IComponent, abc.ABC): ################ @abc.abstractmethod - def deserialize_own_connection(self, payload: binding.JSONObject) -> applications.OwnConnection: + def deserialize_own_connection(self, payload: data_binding.JSONObject) -> applications.OwnConnection: """Parse a raw payload from Discord into an own connection object. Parameters @@ -68,7 +68,7 @@ def deserialize_own_connection(self, payload: binding.JSONObject) -> application """ @abc.abstractmethod - def deserialize_own_guild(self, payload: binding.JSONObject) -> applications.OwnGuild: + def deserialize_own_guild(self, payload: data_binding.JSONObject) -> applications.OwnGuild: """Parse a raw payload from Discord into an own guild object. Parameters @@ -83,7 +83,7 @@ def deserialize_own_guild(self, payload: binding.JSONObject) -> applications.Own """ @abc.abstractmethod - def deserialize_application(self, payload: binding.JSONObject) -> applications.Application: + def deserialize_application(self, payload: data_binding.JSONObject) -> applications.Application: """Parse a raw payload from Discord into an application object. Parameters @@ -102,7 +102,7 @@ def deserialize_application(self, payload: binding.JSONObject) -> applications.A ############## @abc.abstractmethod - def deserialize_audit_log(self, payload: binding.JSONObject) -> audit_logs.AuditLog: + def deserialize_audit_log(self, payload: data_binding.JSONObject) -> audit_logs.AuditLog: """Parse a raw payload from Discord into an audit log object. Parameters @@ -121,7 +121,7 @@ def deserialize_audit_log(self, payload: binding.JSONObject) -> audit_logs.Audit ############ @abc.abstractmethod - def deserialize_permission_overwrite(self, payload: binding.JSONObject) -> channels.PermissionOverwrite: + def deserialize_permission_overwrite(self, payload: data_binding.JSONObject) -> channels.PermissionOverwrite: """Parse a raw payload from Discord into a permission overwrite object. Parameters @@ -136,7 +136,7 @@ def deserialize_permission_overwrite(self, payload: binding.JSONObject) -> chann """ @abc.abstractmethod - def serialize_permission_overwrite(self, overwrite: channels.PermissionOverwrite) -> binding.JSONObject: + def serialize_permission_overwrite(self, overwrite: channels.PermissionOverwrite) -> data_binding.JSONObject: """Serialize a permission overwrite object to a json serializable dict. Parameters @@ -151,7 +151,7 @@ def serialize_permission_overwrite(self, overwrite: channels.PermissionOverwrite """ @abc.abstractmethod - def deserialize_partial_channel(self, payload: binding.JSONObject) -> channels.PartialChannel: + def deserialize_partial_channel(self, payload: data_binding.JSONObject) -> channels.PartialChannel: """Parse a raw payload from Discord into a partial channel object. Parameters @@ -166,7 +166,7 @@ def deserialize_partial_channel(self, payload: binding.JSONObject) -> channels.P """ @abc.abstractmethod - def deserialize_dm_channel(self, payload: binding.JSONObject) -> channels.DMChannel: + def deserialize_dm_channel(self, payload: data_binding.JSONObject) -> channels.DMChannel: """Parse a raw payload from Discord into a DM channel object. Parameters @@ -181,7 +181,7 @@ def deserialize_dm_channel(self, payload: binding.JSONObject) -> channels.DMChan """ @abc.abstractmethod - def deserialize_group_dm_channel(self, payload: binding.JSONObject) -> channels.GroupDMChannel: + def deserialize_group_dm_channel(self, payload: data_binding.JSONObject) -> channels.GroupDMChannel: """Parse a raw payload from Discord into a group DM channel object. Parameters @@ -196,7 +196,7 @@ def deserialize_group_dm_channel(self, payload: binding.JSONObject) -> channels. """ @abc.abstractmethod - def deserialize_guild_category(self, payload: binding.JSONObject) -> channels.GuildCategory: + def deserialize_guild_category(self, payload: data_binding.JSONObject) -> channels.GuildCategory: """Parse a raw payload from Discord into a guild category object. Parameters @@ -211,7 +211,7 @@ def deserialize_guild_category(self, payload: binding.JSONObject) -> channels.Gu """ @abc.abstractmethod - def deserialize_guild_text_channel(self, payload: binding.JSONObject) -> channels.GuildTextChannel: + def deserialize_guild_text_channel(self, payload: data_binding.JSONObject) -> channels.GuildTextChannel: """Parse a raw payload from Discord into a guild text channel object. Parameters @@ -226,7 +226,7 @@ def deserialize_guild_text_channel(self, payload: binding.JSONObject) -> channel """ @abc.abstractmethod - def deserialize_guild_news_channel(self, payload: binding.JSONObject) -> channels.GuildNewsChannel: + def deserialize_guild_news_channel(self, payload: data_binding.JSONObject) -> channels.GuildNewsChannel: """Parse a raw payload from Discord into a guild news channel object. Parameters @@ -241,7 +241,7 @@ def deserialize_guild_news_channel(self, payload: binding.JSONObject) -> channel """ @abc.abstractmethod - def deserialize_guild_store_channel(self, payload: binding.JSONObject) -> channels.GuildStoreChannel: + def deserialize_guild_store_channel(self, payload: data_binding.JSONObject) -> channels.GuildStoreChannel: """Parse a raw payload from Discord into a guild store channel object. Parameters @@ -256,7 +256,7 @@ def deserialize_guild_store_channel(self, payload: binding.JSONObject) -> channe """ @abc.abstractmethod - def deserialize_guild_voice_channel(self, payload: binding.JSONObject) -> channels.GuildVoiceChannel: + def deserialize_guild_voice_channel(self, payload: data_binding.JSONObject) -> channels.GuildVoiceChannel: """Parse a raw payload from Discord into a guild voice channel object. Parameters @@ -271,7 +271,7 @@ def deserialize_guild_voice_channel(self, payload: binding.JSONObject) -> channe """ @abc.abstractmethod - def deserialize_channel(self, payload: binding.JSONObject) -> channels.PartialChannel: + def deserialize_channel(self, payload: data_binding.JSONObject) -> channels.PartialChannel: """Parse a raw payload from Discord into a channel object. Parameters @@ -290,7 +290,7 @@ def deserialize_channel(self, payload: binding.JSONObject) -> channels.PartialCh ########## @abc.abstractmethod - def deserialize_embed(self, payload: binding.JSONObject) -> embeds.Embed: + def deserialize_embed(self, payload: data_binding.JSONObject) -> embeds.Embed: """Parse a raw payload from Discord into an embed object. Parameters @@ -305,7 +305,7 @@ def deserialize_embed(self, payload: binding.JSONObject) -> embeds.Embed: """ @abc.abstractmethod - def serialize_embed(self, embed: embeds.Embed) -> binding.JSONObject: + def serialize_embed(self, embed: embeds.Embed) -> data_binding.JSONObject: """Serialize an embed object to a json serializable dict. Parameters @@ -324,7 +324,7 @@ def serialize_embed(self, embed: embeds.Embed) -> binding.JSONObject: ########## @abc.abstractmethod - def deserialize_unicode_emoji(self, payload: binding.JSONObject) -> emojis.UnicodeEmoji: + def deserialize_unicode_emoji(self, payload: data_binding.JSONObject) -> emojis.UnicodeEmoji: """Parse a raw payload from Discord into a unicode emoji object. Parameters @@ -339,7 +339,7 @@ def deserialize_unicode_emoji(self, payload: binding.JSONObject) -> emojis.Unico """ @abc.abstractmethod - def deserialize_custom_emoji(self, payload: binding.JSONObject) -> emojis.CustomEmoji: + def deserialize_custom_emoji(self, payload: data_binding.JSONObject) -> emojis.CustomEmoji: """Parse a raw payload from Discord into a custom emoji object. Parameters @@ -354,7 +354,7 @@ def deserialize_custom_emoji(self, payload: binding.JSONObject) -> emojis.Custom """ @abc.abstractmethod - def deserialize_known_custom_emoji(self, payload: binding.JSONObject) -> emojis.KnownCustomEmoji: + def deserialize_known_custom_emoji(self, payload: data_binding.JSONObject) -> emojis.KnownCustomEmoji: """Parse a raw payload from Discord into a known custom emoji object. Parameters @@ -369,7 +369,9 @@ def deserialize_known_custom_emoji(self, payload: binding.JSONObject) -> emojis. """ @abc.abstractmethod - def deserialize_emoji(self, payload: binding.JSONObject) -> typing.Union[emojis.UnicodeEmoji, emojis.CustomEmoji]: + def deserialize_emoji( + self, payload: data_binding.JSONObject + ) -> typing.Union[emojis.UnicodeEmoji, emojis.CustomEmoji]: """Parse a raw payload from Discord into an emoji object. Parameters @@ -388,7 +390,7 @@ def deserialize_emoji(self, payload: binding.JSONObject) -> typing.Union[emojis. ########### @abc.abstractmethod - def deserialize_gateway_bot(self, payload: binding.JSONObject) -> gateway.GatewayBot: + def deserialize_gateway_bot(self, payload: data_binding.JSONObject) -> gateway.GatewayBot: """Parse a raw payload from Discord into a gateway bot object. Parameters @@ -407,7 +409,7 @@ def deserialize_gateway_bot(self, payload: binding.JSONObject) -> gateway.Gatewa ########## @abc.abstractmethod - def deserialize_guild_widget(self, payload: binding.JSONObject) -> guilds.GuildWidget: + def deserialize_guild_widget(self, payload: data_binding.JSONObject) -> guilds.GuildWidget: """Parse a raw payload from Discord into a guild embed object. Parameters @@ -423,7 +425,7 @@ def deserialize_guild_widget(self, payload: binding.JSONObject) -> guilds.GuildW @abc.abstractmethod def deserialize_guild_member( - self, payload: binding.JSONObject, *, user: typing.Optional[users.User] = None + self, payload: data_binding.JSONObject, *, user: typing.Optional[users.User] = None ) -> guilds.GuildMember: """Parse a raw payload from Discord into a guild member object. @@ -443,7 +445,7 @@ def deserialize_guild_member( """ @abc.abstractmethod - def deserialize_role(self, payload: binding.JSONObject) -> guilds.Role: + def deserialize_role(self, payload: data_binding.JSONObject) -> guilds.Role: """Parse a raw payload from Discord into a guild role object. Parameters @@ -458,7 +460,7 @@ def deserialize_role(self, payload: binding.JSONObject) -> guilds.Role: """ @abc.abstractmethod - def deserialize_guild_member_presence(self, payload: binding.JSONObject) -> guilds.GuildMemberPresence: + def deserialize_guild_member_presence(self, payload: data_binding.JSONObject) -> guilds.GuildMemberPresence: """Parse a raw payload from Discord into a guild member presence object. Parameters @@ -473,7 +475,7 @@ def deserialize_guild_member_presence(self, payload: binding.JSONObject) -> guil """ @abc.abstractmethod - def deserialize_partial_integration(self, payload: binding.JSONObject) -> guilds.PartialIntegration: + def deserialize_partial_integration(self, payload: data_binding.JSONObject) -> guilds.PartialIntegration: """Parse a raw payload from Discord into a partial integration object. Parameters @@ -488,7 +490,7 @@ def deserialize_partial_integration(self, payload: binding.JSONObject) -> guilds """ @abc.abstractmethod - def deserialize_integration(self, payload: binding.JSONObject) -> guilds.Integration: + def deserialize_integration(self, payload: data_binding.JSONObject) -> guilds.Integration: """Parse a raw payload from Discord into an integration object. Parameters @@ -503,7 +505,7 @@ def deserialize_integration(self, payload: binding.JSONObject) -> guilds.Integra """ @abc.abstractmethod - def deserialize_guild_member_ban(self, payload: binding.JSONObject) -> guilds.GuildMemberBan: + def deserialize_guild_member_ban(self, payload: data_binding.JSONObject) -> guilds.GuildMemberBan: """Parse a raw payload from Discord into a guild member ban object. Parameters @@ -518,7 +520,7 @@ def deserialize_guild_member_ban(self, payload: binding.JSONObject) -> guilds.Gu """ @abc.abstractmethod - def deserialize_unavailable_guild(self, payload: binding.JSONObject) -> guilds.UnavailableGuild: + def deserialize_unavailable_guild(self, payload: data_binding.JSONObject) -> guilds.UnavailableGuild: """Parse a raw payload from Discord into a unavailable guild object. Parameters @@ -533,7 +535,7 @@ def deserialize_unavailable_guild(self, payload: binding.JSONObject) -> guilds.U """ @abc.abstractmethod - def deserialize_guild_preview(self, payload: binding.JSONObject) -> guilds.GuildPreview: + def deserialize_guild_preview(self, payload: data_binding.JSONObject) -> guilds.GuildPreview: """Parse a raw payload from Discord into a guild preview object. Parameters @@ -548,7 +550,7 @@ def deserialize_guild_preview(self, payload: binding.JSONObject) -> guilds.Guild """ @abc.abstractmethod - def deserialize_guild(self, payload: binding.JSONObject) -> guilds.Guild: + def deserialize_guild(self, payload: data_binding.JSONObject) -> guilds.Guild: """Parse a raw payload from Discord into a guild object. Parameters @@ -567,7 +569,7 @@ def deserialize_guild(self, payload: binding.JSONObject) -> guilds.Guild: ########### @abc.abstractmethod - def deserialize_vanity_url(self, payload: binding.JSONObject) -> invites.VanityURL: + def deserialize_vanity_url(self, payload: data_binding.JSONObject) -> invites.VanityURL: """Parse a raw payload from Discord into a vanity url object. Parameters @@ -582,7 +584,7 @@ def deserialize_vanity_url(self, payload: binding.JSONObject) -> invites.VanityU """ @abc.abstractmethod - def deserialize_invite(self, payload: binding.JSONObject) -> invites.Invite: + def deserialize_invite(self, payload: data_binding.JSONObject) -> invites.Invite: """Parse a raw payload from Discord into an invite object. Parameters @@ -597,7 +599,7 @@ def deserialize_invite(self, payload: binding.JSONObject) -> invites.Invite: """ @abc.abstractmethod - def deserialize_invite_with_metadata(self, payload: binding.JSONObject) -> invites.InviteWithMetadata: + def deserialize_invite_with_metadata(self, payload: data_binding.JSONObject) -> invites.InviteWithMetadata: """Parse a raw payload from Discord into a invite with metadata object. Parameters @@ -615,7 +617,7 @@ def deserialize_invite_with_metadata(self, payload: binding.JSONObject) -> invit # MESSAGES # ############ - def deserialize_message(self, payload: binding.JSONObject) -> messages.Message: + def deserialize_message(self, payload: data_binding.JSONObject) -> messages.Message: """Parse a raw payload from Discord into a message object. Parameters @@ -634,7 +636,7 @@ def deserialize_message(self, payload: binding.JSONObject) -> messages.Message: ######### @abc.abstractmethod - def deserialize_user(self, payload: binding.JSONObject) -> users.User: + def deserialize_user(self, payload: data_binding.JSONObject) -> users.User: """Parse a raw payload from Discord into a user object. Parameters @@ -649,7 +651,7 @@ def deserialize_user(self, payload: binding.JSONObject) -> users.User: """ @abc.abstractmethod - def deserialize_my_user(self, payload: binding.JSONObject) -> users.MyUser: + def deserialize_my_user(self, payload: data_binding.JSONObject) -> users.MyUser: """Parse a raw payload from Discord into a my user object. Parameters @@ -668,7 +670,7 @@ def deserialize_my_user(self, payload: binding.JSONObject) -> users.MyUser: ########## @abc.abstractmethod - def deserialize_voice_state(self, payload: binding.JSONObject) -> voices.VoiceState: + def deserialize_voice_state(self, payload: data_binding.JSONObject) -> voices.VoiceState: """Parse a raw payload from Discord into a voice state object. Parameters @@ -683,7 +685,7 @@ def deserialize_voice_state(self, payload: binding.JSONObject) -> voices.VoiceSt """ @abc.abstractmethod - def deserialize_voice_region(self, payload: binding.JSONObject) -> voices.VoiceRegion: + def deserialize_voice_region(self, payload: data_binding.JSONObject) -> voices.VoiceRegion: """Parse a raw payload from Discord into a voice region object. Parameters @@ -702,7 +704,7 @@ def deserialize_voice_region(self, payload: binding.JSONObject) -> voices.VoiceR ############ @abc.abstractmethod - def deserialize_webhook(self, payload: binding.JSONObject) -> webhooks.Webhook: + def deserialize_webhook(self, payload: data_binding.JSONObject) -> webhooks.Webhook: """Parse a raw payload from Discord into a webhook object. Parameters diff --git a/hikari/event_consumer.py b/hikari/event_consumer.py index 9f21391ec9..bdfb5a7f60 100644 --- a/hikari/event_consumer.py +++ b/hikari/event_consumer.py @@ -27,7 +27,7 @@ if typing.TYPE_CHECKING: from hikari.net import gateway - from hikari.utilities import binding + from hikari.utilities import data_binding class IEventConsumer(component.IComponent, abc.ABC): @@ -42,7 +42,9 @@ class IEventConsumer(component.IComponent, abc.ABC): __slots__ = () @abc.abstractmethod - async def consume_raw_event(self, shard: gateway.Gateway, event_name: str, payload: binding.JSONObject) -> None: + async def consume_raw_event( + self, shard: gateway.Gateway, event_name: str, payload: data_binding.JSONObject + ) -> None: """Process a raw event from a gateway shard and process it. Parameters diff --git a/hikari/impl/cache.py b/hikari/impl/cache.py index 05a692e87b..f3f7bf4bc9 100644 --- a/hikari/impl/cache.py +++ b/hikari/impl/cache.py @@ -23,7 +23,7 @@ import typing from hikari import cache -from hikari.utilities import binding +from hikari.utilities import data_binding if typing.TYPE_CHECKING: from hikari import app as app_ @@ -48,32 +48,34 @@ def __init__(self, app: app_.IApp) -> None: def app(self) -> app_.IApp: return self._app - async def create_application(self, payload: binding.JSONObject) -> applications.Application: + async def create_application(self, payload: data_binding.JSONObject) -> applications.Application: pass - async def create_own_guild(self, payload: binding.JSONObject) -> applications.OwnGuild: + async def create_own_guild(self, payload: data_binding.JSONObject) -> applications.OwnGuild: pass - async def create_own_connection(self, payload: binding.JSONObject) -> applications.OwnConnection: + async def create_own_connection(self, payload: data_binding.JSONObject) -> applications.OwnConnection: pass - async def create_audit_log_change(self, payload: binding.JSONObject) -> audit_logs.AuditLogChange: + async def create_audit_log_change(self, payload: data_binding.JSONObject) -> audit_logs.AuditLogChange: pass - async def create_audit_log_entry_info(self, payload: binding.JSONObject) -> audit_logs.BaseAuditLogEntryInfo: + async def create_audit_log_entry_info(self, payload: data_binding.JSONObject) -> audit_logs.BaseAuditLogEntryInfo: pass - async def create_audit_log_entry(self, payload: binding.JSONObject) -> audit_logs.AuditLogEntry: + async def create_audit_log_entry(self, payload: data_binding.JSONObject) -> audit_logs.AuditLogEntry: pass - async def create_audit_log(self, payload: binding.JSONObject) -> audit_logs.AuditLog: + async def create_audit_log(self, payload: data_binding.JSONObject) -> audit_logs.AuditLog: pass - async def create_channel(self, payload: binding.JSONObject, can_cache: bool = False) -> channels.PartialChannel: + async def create_channel( + self, payload: data_binding.JSONObject, can_cache: bool = False + ) -> channels.PartialChannel: pass async def update_channel( - self, channel: channels.PartialChannel, payload: binding.JSONObject + self, channel: channels.PartialChannel, payload: data_binding.JSONObject ) -> channels.PartialChannel: pass @@ -83,13 +85,13 @@ async def get_channel(self, channel_id: int) -> typing.Optional[channels.Partial async def delete_channel(self, channel_id: int) -> typing.Optional[channels.PartialChannel]: pass - async def create_embed(self, payload: binding.JSONObject) -> embeds.Embed: + async def create_embed(self, payload: data_binding.JSONObject) -> embeds.Embed: pass - async def create_emoji(self, payload: binding.JSONObject, can_cache: bool = False) -> emojis.Emoji: + async def create_emoji(self, payload: data_binding.JSONObject, can_cache: bool = False) -> emojis.Emoji: pass - async def update_emoji(self, payload: binding.JSONObject) -> emojis.Emoji: + async def update_emoji(self, payload: data_binding.JSONObject) -> emojis.Emoji: pass async def get_emoji(self, emoji_id: int) -> typing.Optional[emojis.KnownCustomEmoji]: @@ -98,13 +100,13 @@ async def get_emoji(self, emoji_id: int) -> typing.Optional[emojis.KnownCustomEm async def delete_emoji(self, emoji_id: int) -> typing.Optional[emojis.KnownCustomEmoji]: pass - async def create_gateway_bot(self, payload: binding.JSONObject) -> gateway.GatewayBot: + async def create_gateway_bot(self, payload: data_binding.JSONObject) -> gateway.GatewayBot: pass - async def create_member(self, payload: binding.JSONObject, can_cache: bool = False) -> guilds.GuildMember: + async def create_member(self, payload: data_binding.JSONObject, can_cache: bool = False) -> guilds.GuildMember: pass - async def update_member(self, member: guilds.GuildMember, payload: binding.JSONObject) -> guilds.GuildMember: + async def update_member(self, member: guilds.GuildMember, payload: data_binding.JSONObject) -> guilds.GuildMember: pass async def get_member(self, guild_id: int, user_id: int) -> typing.Optional[guilds.GuildMember]: @@ -113,10 +115,10 @@ async def get_member(self, guild_id: int, user_id: int) -> typing.Optional[guild async def delete_member(self, guild_id: int, user_id: int) -> typing.Optional[guilds.GuildMember]: pass - async def create_role(self, payload: binding.JSONObject, can_cache: bool = False) -> guilds.PartialRole: + async def create_role(self, payload: data_binding.JSONObject, can_cache: bool = False) -> guilds.PartialRole: pass - async def update_role(self, role: guilds.PartialRole, payload: binding.JSONObject) -> guilds.PartialRole: + async def update_role(self, role: guilds.PartialRole, payload: data_binding.JSONObject) -> guilds.PartialRole: pass async def get_role(self, guild_id: int, role_id: int) -> typing.Optional[guilds.PartialRole]: @@ -125,11 +127,13 @@ async def get_role(self, guild_id: int, role_id: int) -> typing.Optional[guilds. async def delete_role(self, guild_id: int, role_id: int) -> typing.Optional[guilds.PartialRole]: pass - async def create_presence(self, payload: binding.JSONObject, can_cache: bool = False) -> guilds.GuildMemberPresence: + async def create_presence( + self, payload: data_binding.JSONObject, can_cache: bool = False + ) -> guilds.GuildMemberPresence: pass async def update_presence( - self, role: guilds.GuildMemberPresence, payload: binding.JSONObject + self, role: guilds.GuildMemberPresence, payload: data_binding.JSONObject ) -> guilds.GuildMemberPresence: pass @@ -139,16 +143,16 @@ async def get_presence(self, guild_id: int, user_id: int) -> typing.Optional[gui async def delete_presence(self, guild_id: int, user_id: int) -> typing.Optional[guilds.GuildMemberPresence]: pass - async def create_guild_ban(self, payload: binding.JSONObject) -> guilds.GuildMemberBan: + async def create_guild_ban(self, payload: data_binding.JSONObject) -> guilds.GuildMemberBan: pass - async def create_guild_integration(self, payload: binding.JSONObject) -> guilds.PartialIntegration: + async def create_guild_integration(self, payload: data_binding.JSONObject) -> guilds.PartialIntegration: pass - async def create_guild(self, payload: binding.JSONObject, can_cache: bool = False) -> guilds.PartialGuild: + async def create_guild(self, payload: data_binding.JSONObject, can_cache: bool = False) -> guilds.PartialGuild: pass - async def update_guild(self, guild: guilds.PartialGuild, payload: binding.JSONObject) -> guilds.PartialGuild: + async def update_guild(self, guild: guilds.PartialGuild, payload: data_binding.JSONObject) -> guilds.PartialGuild: pass async def get_guild(self, guild_id: int) -> typing.Optional[guilds.PartialGuild]: @@ -157,19 +161,19 @@ async def get_guild(self, guild_id: int) -> typing.Optional[guilds.PartialGuild] async def delete_guild(self, guild_id: int) -> typing.Optional[guilds.PartialGuild]: pass - async def create_guild_preview(self, payload: binding.JSONObject) -> guilds.GuildPreview: + async def create_guild_preview(self, payload: data_binding.JSONObject) -> guilds.GuildPreview: pass - async def create_invite(self, payload: binding.JSONObject) -> invites.Invite: + async def create_invite(self, payload: data_binding.JSONObject) -> invites.Invite: pass - async def create_reaction(self, payload: binding.JSONObject) -> messages.Reaction: + async def create_reaction(self, payload: data_binding.JSONObject) -> messages.Reaction: pass - async def create_message(self, payload: binding.JSONObject, can_cache: bool = False) -> messages.Message: + async def create_message(self, payload: data_binding.JSONObject, can_cache: bool = False) -> messages.Message: pass - async def update_message(self, message: messages.Message, payload: binding.JSONObject) -> messages.Message: + async def update_message(self, message: messages.Message, payload: data_binding.JSONObject) -> messages.Message: pass async def get_message(self, channel_id: int, message_id: int) -> typing.Optional[messages.Message]: @@ -178,10 +182,10 @@ async def get_message(self, channel_id: int, message_id: int) -> typing.Optional async def delete_message(self, channel_id: int, message_id: int) -> typing.Optional[messages.Message]: pass - async def create_user(self, payload: binding.JSONObject, can_cache: bool = False) -> users.User: + async def create_user(self, payload: data_binding.JSONObject, can_cache: bool = False) -> users.User: pass - async def update_user(self, user: users.User, payload: binding.JSONObject) -> users.User: + async def update_user(self, user: users.User, payload: data_binding.JSONObject) -> users.User: pass async def get_user(self, user_id: int) -> typing.Optional[users.User]: @@ -190,19 +194,19 @@ async def get_user(self, user_id: int) -> typing.Optional[users.User]: async def delete_user(self, user_id: int) -> typing.Optional[users.User]: pass - async def create_my_user(self, payload: binding.JSONObject, can_cache: bool = False) -> users.MyUser: + async def create_my_user(self, payload: data_binding.JSONObject, can_cache: bool = False) -> users.MyUser: pass - async def update_my_user(self, my_user: users.MyUser, payload: binding.JSONObject) -> users.MyUser: + async def update_my_user(self, my_user: users.MyUser, payload: data_binding.JSONObject) -> users.MyUser: pass async def get_my_user(self) -> typing.Optional[users.User]: pass - async def create_voice_state(self, payload: binding.JSONObject, can_cache: bool = False) -> voices.VoiceState: + async def create_voice_state(self, payload: data_binding.JSONObject, can_cache: bool = False) -> voices.VoiceState: pass - async def update_voice_state(self, payload: binding.JSONObject) -> voices.VoiceState: + async def update_voice_state(self, payload: data_binding.JSONObject) -> voices.VoiceState: pass async def get_voice_state(self, guild_id: int, channel_id: int) -> typing.Optional[voices.VoiceState]: @@ -211,5 +215,5 @@ async def get_voice_state(self, guild_id: int, channel_id: int) -> typing.Option async def delete_voice_state(self, guild_id: int, channel_id: int) -> typing.Optional[voices.VoiceState]: pass - async def create_voice_region(self, payload: binding.JSONObject) -> voices.VoiceRegion: + async def create_voice_region(self, payload: data_binding.JSONObject) -> voices.VoiceRegion: pass diff --git a/hikari/impl/entity_factory.py b/hikari/impl/entity_factory.py index 94612c87d6..692b5e0ff8 100644 --- a/hikari/impl/entity_factory.py +++ b/hikari/impl/entity_factory.py @@ -46,7 +46,7 @@ from hikari.utilities import undefined if typing.TYPE_CHECKING: - from hikari.utilities import binding + from hikari.utilities import data_binding DMChannelT = typing.TypeVar("DMChannelT", bound=channels_.DMChannel) @@ -98,7 +98,7 @@ def app(self) -> app_.IApp: # APPLICATIONS # ################ - def deserialize_own_connection(self, payload: binding.JSONObject) -> applications.OwnConnection: + def deserialize_own_connection(self, payload: data_binding.JSONObject) -> applications.OwnConnection: """Parse a raw payload from Discord into an own connection object. Parameters @@ -126,7 +126,7 @@ def deserialize_own_connection(self, payload: binding.JSONObject) -> application own_connection.visibility = applications.ConnectionVisibility(payload["visibility"]) return own_connection - def deserialize_own_guild(self, payload: binding.JSONObject) -> applications.OwnGuild: + def deserialize_own_guild(self, payload: data_binding.JSONObject) -> applications.OwnGuild: """Parse a raw payload from Discord into an own guild object. Parameters @@ -145,7 +145,7 @@ def deserialize_own_guild(self, payload: binding.JSONObject) -> applications.Own own_guild.my_permissions = permissions.Permission(payload["permissions"]) return own_guild - def deserialize_application(self, payload: binding.JSONObject) -> applications.Application: + def deserialize_application(self, payload: data_binding.JSONObject) -> applications.Application: """Parse a raw payload from Discord into an application object. Parameters @@ -203,7 +203,7 @@ def deserialize_application(self, payload: binding.JSONObject) -> applications.A ############## def _deserialize_audit_log_change_roles( - self, payload: binding.JSONArray + self, payload: data_binding.JSONArray ) -> typing.Mapping[base_models.Snowflake, guilds.PartialRole]: roles = {} for role_payload in payload: @@ -215,7 +215,7 @@ def _deserialize_audit_log_change_roles( return roles def _deserialize_audit_log_overwrites( - self, payload: binding.JSONArray + self, payload: data_binding.JSONArray ) -> typing.Mapping[base_models.Snowflake, channels_.PermissionOverwrite]: return { base_models.Snowflake(overwrite["id"]): self.deserialize_permission_overwrite(overwrite) @@ -223,7 +223,9 @@ def _deserialize_audit_log_overwrites( } @staticmethod - def _deserialize_channel_overwrite_entry_info(payload: binding.JSONObject,) -> audit_logs.ChannelOverwriteEntryInfo: + def _deserialize_channel_overwrite_entry_info( + payload: data_binding.JSONObject, + ) -> audit_logs.ChannelOverwriteEntryInfo: channel_overwrite_entry_info = audit_logs.ChannelOverwriteEntryInfo() channel_overwrite_entry_info.id = base_models.Snowflake(payload["id"]) # noinspection PyArgumentList @@ -232,14 +234,14 @@ def _deserialize_channel_overwrite_entry_info(payload: binding.JSONObject,) -> a return channel_overwrite_entry_info @staticmethod - def _deserialize_message_pin_entry_info(payload: binding.JSONObject) -> audit_logs.MessagePinEntryInfo: + def _deserialize_message_pin_entry_info(payload: data_binding.JSONObject) -> audit_logs.MessagePinEntryInfo: message_pin_entry_info = audit_logs.MessagePinEntryInfo() message_pin_entry_info.channel_id = base_models.Snowflake(payload["channel_id"]) message_pin_entry_info.message_id = base_models.Snowflake(payload["message_id"]) return message_pin_entry_info @staticmethod - def _deserialize_member_prune_entry_info(payload: binding.JSONObject) -> audit_logs.MemberPruneEntryInfo: + def _deserialize_member_prune_entry_info(payload: data_binding.JSONObject) -> audit_logs.MemberPruneEntryInfo: member_prune_entry_info = audit_logs.MemberPruneEntryInfo() member_prune_entry_info.delete_member_days = datetime.timedelta(days=int(payload["delete_member_days"])) member_prune_entry_info.members_removed = int(payload["members_removed"]) @@ -247,27 +249,29 @@ def _deserialize_member_prune_entry_info(payload: binding.JSONObject) -> audit_l @staticmethod def _deserialize_message_bulk_delete_entry_info( - payload: binding.JSONObject, + payload: data_binding.JSONObject, ) -> audit_logs.MessageBulkDeleteEntryInfo: message_bulk_delete_entry_info = audit_logs.MessageBulkDeleteEntryInfo() message_bulk_delete_entry_info.count = int(payload["count"]) return message_bulk_delete_entry_info @staticmethod - def _deserialize_message_delete_entry_info(payload: binding.JSONObject) -> audit_logs.MessageDeleteEntryInfo: + def _deserialize_message_delete_entry_info(payload: data_binding.JSONObject) -> audit_logs.MessageDeleteEntryInfo: message_delete_entry_info = audit_logs.MessageDeleteEntryInfo() message_delete_entry_info.channel_id = base_models.Snowflake(payload["channel_id"]) message_delete_entry_info.count = int(payload["count"]) return message_delete_entry_info @staticmethod - def _deserialize_member_disconnect_entry_info(payload: binding.JSONObject,) -> audit_logs.MemberDisconnectEntryInfo: + def _deserialize_member_disconnect_entry_info( + payload: data_binding.JSONObject, + ) -> audit_logs.MemberDisconnectEntryInfo: member_disconnect_entry_info = audit_logs.MemberDisconnectEntryInfo() member_disconnect_entry_info.count = int(payload["count"]) return member_disconnect_entry_info @staticmethod - def _deserialize_member_move_entry_info(payload: binding.JSONObject) -> audit_logs.MemberMoveEntryInfo: + def _deserialize_member_move_entry_info(payload: data_binding.JSONObject) -> audit_logs.MemberMoveEntryInfo: member_move_entry_info = audit_logs.MemberMoveEntryInfo() member_move_entry_info.channel_id = base_models.Snowflake(payload["channel_id"]) member_move_entry_info.count = int(payload["count"]) @@ -275,11 +279,11 @@ def _deserialize_member_move_entry_info(payload: binding.JSONObject) -> audit_lo @staticmethod def _deserialize_unrecognised_audit_log_entry_info( - payload: binding.JSONObject, + payload: data_binding.JSONObject, ) -> audit_logs.UnrecognisedAuditLogEntryInfo: return audit_logs.UnrecognisedAuditLogEntryInfo(payload) - def deserialize_audit_log(self, payload: binding.JSONObject) -> audit_logs.AuditLog: + def deserialize_audit_log(self, payload: data_binding.JSONObject) -> audit_logs.AuditLog: """Parse a raw payload from Discord into an audit log object. Parameters @@ -351,7 +355,7 @@ def deserialize_audit_log(self, payload: binding.JSONObject) -> audit_logs.Audit # CHANNELS # ############ - def deserialize_permission_overwrite(self, payload: binding.JSONObject) -> channels_.PermissionOverwrite: + def deserialize_permission_overwrite(self, payload: data_binding.JSONObject) -> channels_.PermissionOverwrite: """Parse a raw payload from Discord into a permission overwrite object. Parameters @@ -373,7 +377,7 @@ def deserialize_permission_overwrite(self, payload: binding.JSONObject) -> chann permission_overwrite.deny = permissions.Permission(payload["deny"]) return permission_overwrite - def serialize_permission_overwrite(self, overwrite: channels_.PermissionOverwrite) -> binding.JSONObject: + def serialize_permission_overwrite(self, overwrite: channels_.PermissionOverwrite) -> data_binding.JSONObject: """Serialize a permission overwrite object to a json serializable dict. Parameters @@ -388,7 +392,9 @@ def serialize_permission_overwrite(self, overwrite: channels_.PermissionOverwrit """ return {"id": str(overwrite.id), "type": overwrite.type, "allow": overwrite.allow, "deny": overwrite.deny} - def _set_partial_channel_attributes(self, payload: binding.JSONObject, channel: PartialChannelT) -> PartialChannelT: + def _set_partial_channel_attributes( + self, payload: data_binding.JSONObject, channel: PartialChannelT + ) -> PartialChannelT: channel.set_app(self._app) channel.id = base_models.Snowflake(payload["id"]) channel.name = payload.get("name") @@ -396,7 +402,7 @@ def _set_partial_channel_attributes(self, payload: binding.JSONObject, channel: channel.type = channels_.ChannelType(payload["type"]) return channel - def deserialize_partial_channel(self, payload: binding.JSONObject) -> channels_.PartialChannel: + def deserialize_partial_channel(self, payload: data_binding.JSONObject) -> channels_.PartialChannel: """Parse a raw payload from Discord into a partial channel object. Parameters @@ -411,7 +417,7 @@ def deserialize_partial_channel(self, payload: binding.JSONObject) -> channels_. """ return self._set_partial_channel_attributes(payload, channels_.PartialChannel()) - def _set_dm_channel_attributes(self, payload: binding.JSONObject, channel: DMChannelT) -> DMChannelT: + def _set_dm_channel_attributes(self, payload: data_binding.JSONObject, channel: DMChannelT) -> DMChannelT: channel = self._set_partial_channel_attributes(payload, channel) if (last_message_id := payload["last_message_id"]) is not None: last_message_id = base_models.Snowflake(last_message_id) @@ -421,7 +427,7 @@ def _set_dm_channel_attributes(self, payload: binding.JSONObject, channel: DMCha } return channel - def deserialize_dm_channel(self, payload: binding.JSONObject) -> channels_.DMChannel: + def deserialize_dm_channel(self, payload: data_binding.JSONObject) -> channels_.DMChannel: """Parse a raw payload from Discord into a DM channel object. Parameters @@ -436,7 +442,7 @@ def deserialize_dm_channel(self, payload: binding.JSONObject) -> channels_.DMCha """ return self._set_dm_channel_attributes(payload, channels_.DMChannel()) - def deserialize_group_dm_channel(self, payload: binding.JSONObject) -> channels_.GroupDMChannel: + def deserialize_group_dm_channel(self, payload: data_binding.JSONObject) -> channels_.GroupDMChannel: """Parse a raw payload from Discord into a group DM channel object. Parameters @@ -457,7 +463,7 @@ def deserialize_group_dm_channel(self, payload: binding.JSONObject) -> channels_ ) return group_dm_channel - def _set_guild_channel_attributes(self, payload: binding.JSONObject, channel: GuildChannelT) -> GuildChannelT: + def _set_guild_channel_attributes(self, payload: data_binding.JSONObject, channel: GuildChannelT) -> GuildChannelT: channel = self._set_partial_channel_attributes(payload, channel) channel.guild_id = base_models.Snowflake(payload["guild_id"]) if "guild_id" in payload else None channel.position = int(payload["position"]) @@ -473,7 +479,7 @@ def _set_guild_channel_attributes(self, payload: binding.JSONObject, channel: Gu channel.parent_id = parent_id return channel - def deserialize_guild_category(self, payload: binding.JSONObject) -> channels_.GuildCategory: + def deserialize_guild_category(self, payload: data_binding.JSONObject) -> channels_.GuildCategory: """Parse a raw payload from Discord into a guild category object. Parameters @@ -488,7 +494,7 @@ def deserialize_guild_category(self, payload: binding.JSONObject) -> channels_.G """ return self._set_guild_channel_attributes(payload, channels_.GuildCategory()) - def deserialize_guild_text_channel(self, payload: binding.JSONObject) -> channels_.GuildTextChannel: + def deserialize_guild_text_channel(self, payload: data_binding.JSONObject) -> channels_.GuildTextChannel: """Parse a raw payload from Discord into a guild text channel object. Parameters @@ -512,7 +518,7 @@ def deserialize_guild_text_channel(self, payload: binding.JSONObject) -> channel guild_text_category.last_pin_timestamp = last_pin_timestamp return guild_text_category - def deserialize_guild_news_channel(self, payload: binding.JSONObject) -> channels_.GuildNewsChannel: + def deserialize_guild_news_channel(self, payload: data_binding.JSONObject) -> channels_.GuildNewsChannel: """Parse a raw payload from Discord into a guild news channel object. Parameters @@ -535,7 +541,7 @@ def deserialize_guild_news_channel(self, payload: binding.JSONObject) -> channel guild_news_channel.last_pin_timestamp = last_pin_timestamp return guild_news_channel - def deserialize_guild_store_channel(self, payload: binding.JSONObject) -> channels_.GuildStoreChannel: + def deserialize_guild_store_channel(self, payload: data_binding.JSONObject) -> channels_.GuildStoreChannel: """Parse a raw payload from Discord into a guild store channel object. Parameters @@ -550,7 +556,7 @@ def deserialize_guild_store_channel(self, payload: binding.JSONObject) -> channe """ return self._set_guild_channel_attributes(payload, channels_.GuildStoreChannel()) - def deserialize_guild_voice_channel(self, payload: binding.JSONObject) -> channels_.GuildVoiceChannel: + def deserialize_guild_voice_channel(self, payload: data_binding.JSONObject) -> channels_.GuildVoiceChannel: """Parse a raw payload from Discord into a guild voice channel object. Parameters @@ -568,7 +574,7 @@ def deserialize_guild_voice_channel(self, payload: binding.JSONObject) -> channe guild_voice_channel.user_limit = int(payload["user_limit"]) return guild_voice_channel - def deserialize_channel(self, payload: binding.JSONObject) -> channels_.PartialChannel: + def deserialize_channel(self, payload: data_binding.JSONObject) -> channels_.PartialChannel: """Parse a raw payload from Discord into a channel object. Parameters @@ -587,7 +593,7 @@ def deserialize_channel(self, payload: binding.JSONObject) -> channels_.PartialC # EMBEDS # ########## - def deserialize_embed(self, payload: binding.JSONObject) -> embeds.Embed: + def deserialize_embed(self, payload: data_binding.JSONObject) -> embeds.Embed: """Parse a raw payload from Discord into an embed object. Parameters @@ -668,7 +674,7 @@ def deserialize_embed(self, payload: binding.JSONObject) -> embeds.Embed: embed.fields = fields return embed - def serialize_embed(self, embed: embeds.Embed) -> binding.JSONObject: + def serialize_embed(self, embed: embeds.Embed) -> data_binding.JSONObject: """Serialize an embed object to a json serializable dict. Parameters @@ -736,7 +742,7 @@ def serialize_embed(self, embed: embeds.Embed) -> binding.JSONObject: # EMOJIS # ########## - def deserialize_unicode_emoji(self, payload: binding.JSONObject) -> emojis.UnicodeEmoji: + def deserialize_unicode_emoji(self, payload: data_binding.JSONObject) -> emojis.UnicodeEmoji: """Parse a raw payload from Discord into a unicode emoji object. Parameters @@ -753,7 +759,7 @@ def deserialize_unicode_emoji(self, payload: binding.JSONObject) -> emojis.Unico unicode_emoji.name = payload["name"] return unicode_emoji - def deserialize_custom_emoji(self, payload: binding.JSONObject) -> emojis.CustomEmoji: + def deserialize_custom_emoji(self, payload: data_binding.JSONObject) -> emojis.CustomEmoji: """Parse a raw payload from Discord into a custom emoji object. Parameters @@ -773,7 +779,7 @@ def deserialize_custom_emoji(self, payload: binding.JSONObject) -> emojis.Custom custom_emoji.is_animated = payload.get("animated", False) return custom_emoji - def deserialize_known_custom_emoji(self, payload: binding.JSONObject) -> emojis.KnownCustomEmoji: + def deserialize_known_custom_emoji(self, payload: data_binding.JSONObject) -> emojis.KnownCustomEmoji: """Parse a raw payload from Discord into a known custom emoji object. Parameters @@ -800,7 +806,9 @@ def deserialize_known_custom_emoji(self, payload: binding.JSONObject) -> emojis. known_custom_emoji.is_available = payload["available"] return known_custom_emoji - def deserialize_emoji(self, payload: binding.JSONObject) -> typing.Union[emojis.UnicodeEmoji, emojis.CustomEmoji]: + def deserialize_emoji( + self, payload: data_binding.JSONObject + ) -> typing.Union[emojis.UnicodeEmoji, emojis.CustomEmoji]: """Parse a raw payload from Discord into an emoji object. Parameters @@ -821,7 +829,7 @@ def deserialize_emoji(self, payload: binding.JSONObject) -> typing.Union[emojis. # GATEWAY # ########### - def deserialize_gateway_bot(self, payload: binding.JSONObject) -> gateway.GatewayBot: + def deserialize_gateway_bot(self, payload: data_binding.JSONObject) -> gateway.GatewayBot: """Parse a raw payload from Discord into a gateway bot object. Parameters @@ -849,7 +857,7 @@ def deserialize_gateway_bot(self, payload: binding.JSONObject) -> gateway.Gatewa # GUILDS # ########## - def deserialize_guild_widget(self, payload: binding.JSONObject) -> guilds.GuildWidget: + def deserialize_guild_widget(self, payload: data_binding.JSONObject) -> guilds.GuildWidget: """Parse a raw payload from Discord into a guild widget object. Parameters @@ -871,7 +879,7 @@ def deserialize_guild_widget(self, payload: binding.JSONObject) -> guilds.GuildW return guild_embed def deserialize_guild_member( - self, payload: binding.JSONObject, *, user: typing.Optional[users.User] = None + self, payload: data_binding.JSONObject, *, user: typing.Optional[users.User] = None ) -> guilds.GuildMember: """Parse a raw payload from Discord into a guild member object. @@ -902,7 +910,7 @@ def deserialize_guild_member( guild_member.is_mute = payload["mute"] return guild_member - def deserialize_role(self, payload: binding.JSONObject) -> guilds.Role: + def deserialize_role(self, payload: data_binding.JSONObject) -> guilds.Role: """Parse a raw payload from Discord into a guild role object. Parameters @@ -928,7 +936,7 @@ def deserialize_role(self, payload: binding.JSONObject) -> guilds.Role: guild_role.is_mentionable = payload["mentionable"] return guild_role - def deserialize_guild_member_presence(self, payload: binding.JSONObject) -> guilds.GuildMemberPresence: + def deserialize_guild_member_presence(self, payload: data_binding.JSONObject) -> guilds.GuildMemberPresence: """Parse a raw payload from Discord into a guild member presence object. Parameters @@ -1056,7 +1064,7 @@ def deserialize_guild_member_presence(self, payload: binding.JSONObject) -> guil @staticmethod def _set_partial_integration_attributes( - payload: binding.JSONObject, integration: PartialGuildIntegrationT + payload: data_binding.JSONObject, integration: PartialGuildIntegrationT ) -> PartialGuildIntegrationT: integration.id = base_models.Snowflake(payload["id"]) integration.name = payload["name"] @@ -1068,7 +1076,7 @@ def _set_partial_integration_attributes( integration.account = account return integration - def deserialize_partial_integration(self, payload: binding.JSONObject) -> guilds.PartialIntegration: + def deserialize_partial_integration(self, payload: data_binding.JSONObject) -> guilds.PartialIntegration: """Parse a raw payload from Discord into a partial integration object. Parameters @@ -1083,7 +1091,7 @@ def deserialize_partial_integration(self, payload: binding.JSONObject) -> guilds """ return self._set_partial_integration_attributes(payload, guilds.PartialIntegration()) - def deserialize_integration(self, payload: binding.JSONObject) -> guilds.Integration: + def deserialize_integration(self, payload: data_binding.JSONObject) -> guilds.Integration: """Parse a raw payload from Discord into an integration object. Parameters @@ -1112,7 +1120,7 @@ def deserialize_integration(self, payload: binding.JSONObject) -> guilds.Integra guild_integration.last_synced_at = last_synced_at return guild_integration - def deserialize_guild_member_ban(self, payload: binding.JSONObject) -> guilds.GuildMemberBan: + def deserialize_guild_member_ban(self, payload: data_binding.JSONObject) -> guilds.GuildMemberBan: """Parse a raw payload from Discord into a guild member ban object. Parameters @@ -1130,7 +1138,7 @@ def deserialize_guild_member_ban(self, payload: binding.JSONObject) -> guilds.Gu guild_member_ban.user = self.deserialize_user(payload["user"]) return guild_member_ban - def deserialize_unavailable_guild(self, payload: binding.JSONObject) -> guilds.UnavailableGuild: + def deserialize_unavailable_guild(self, payload: data_binding.JSONObject) -> guilds.UnavailableGuild: """Parse a raw payload from Discord into a unavailable guild object. Parameters @@ -1148,7 +1156,7 @@ def deserialize_unavailable_guild(self, payload: binding.JSONObject) -> guilds.U unavailable_guild.id = base_models.Snowflake(payload["id"]) return unavailable_guild - def _set_partial_guild_attributes(self, payload: binding.JSONObject, guild: PartialGuildT) -> PartialGuildT: + def _set_partial_guild_attributes(self, payload: data_binding.JSONObject, guild: PartialGuildT) -> PartialGuildT: guild.set_app(self._app) guild.id = base_models.Snowflake(payload["id"]) guild.name = payload["name"] @@ -1162,7 +1170,7 @@ def _set_partial_guild_attributes(self, payload: binding.JSONObject, guild: Part guild.features = set(features) return guild - def deserialize_guild_preview(self, payload: binding.JSONObject) -> guilds.GuildPreview: + def deserialize_guild_preview(self, payload: data_binding.JSONObject) -> guilds.GuildPreview: """Parse a raw payload from Discord into a guild preview object. Parameters @@ -1187,7 +1195,7 @@ def deserialize_guild_preview(self, payload: binding.JSONObject) -> guilds.Guild guild_preview.description = payload["description"] return guild_preview - def deserialize_guild(self, payload: binding.JSONObject) -> guilds.Guild: + def deserialize_guild(self, payload: data_binding.JSONObject) -> guilds.Guild: """Parse a raw payload from Discord into a guild object. Parameters @@ -1301,7 +1309,7 @@ def deserialize_guild(self, payload: binding.JSONObject) -> guilds.Guild: # INVITES # ########### - def deserialize_vanity_url(self, payload: binding.JSONObject) -> invites.VanityURL: + def deserialize_vanity_url(self, payload: data_binding.JSONObject) -> invites.VanityURL: """Parse a raw payload from Discord into a vanity url object. Parameters @@ -1320,7 +1328,7 @@ def deserialize_vanity_url(self, payload: binding.JSONObject) -> invites.VanityU vanity_url.uses = int(payload["uses"]) return vanity_url - def _set_invite_attributes(self, payload: binding.JSONObject, invite: InviteT) -> InviteT: + def _set_invite_attributes(self, payload: data_binding.JSONObject, invite: InviteT) -> InviteT: invite.set_app(self._app) invite.code = payload["code"] if (guild_payload := payload.get("guild", ...)) is not ...: @@ -1349,7 +1357,7 @@ def _set_invite_attributes(self, payload: binding.JSONObject, invite: InviteT) - ) return invite - def deserialize_invite(self, payload: binding.JSONObject) -> invites.Invite: + def deserialize_invite(self, payload: data_binding.JSONObject) -> invites.Invite: """Parse a raw payload from Discord into an invite object. Parameters @@ -1364,7 +1372,7 @@ def deserialize_invite(self, payload: binding.JSONObject) -> invites.Invite: """ return self._set_invite_attributes(payload, invites.Invite()) - def deserialize_invite_with_metadata(self, payload: binding.JSONObject) -> invites.InviteWithMetadata: + def deserialize_invite_with_metadata(self, payload: data_binding.JSONObject) -> invites.InviteWithMetadata: """Parse a raw payload from Discord into a invite with metadata object. Parameters @@ -1391,7 +1399,7 @@ def deserialize_invite_with_metadata(self, payload: binding.JSONObject) -> invit ############ # TODO: arbitrarily partial ver? - def deserialize_message(self, payload: binding.JSONObject) -> messages.Message: + def deserialize_message(self, payload: data_binding.JSONObject) -> messages.Message: """Parse a raw payload from Discord into a message object. Parameters @@ -1481,7 +1489,7 @@ def deserialize_message(self, payload: binding.JSONObject) -> messages.Message: # USERS # ######### - def _set_user_attributes(self, payload: binding.JSONObject, user: UserT) -> UserT: + def _set_user_attributes(self, payload: data_binding.JSONObject, user: UserT) -> UserT: user.set_app(self._app) user.id = base_models.Snowflake(payload["id"]) user.discriminator = payload["discriminator"] @@ -1491,7 +1499,7 @@ def _set_user_attributes(self, payload: binding.JSONObject, user: UserT) -> User user.is_system = payload.get("system", False) return user - def deserialize_user(self, payload: binding.JSONObject) -> users.User: + def deserialize_user(self, payload: data_binding.JSONObject) -> users.User: """Parse a raw payload from Discord into a user object. Parameters @@ -1508,7 +1516,7 @@ def deserialize_user(self, payload: binding.JSONObject) -> users.User: user.flags = users.UserFlag(payload["public_flags"]) if "public_flags" in payload else users.UserFlag.NONE return user - def deserialize_my_user(self, payload: binding.JSONObject) -> users.MyUser: + def deserialize_my_user(self, payload: data_binding.JSONObject) -> users.MyUser: """Parse a raw payload from Discord into a my user object. Parameters @@ -1536,7 +1544,7 @@ def deserialize_my_user(self, payload: binding.JSONObject) -> users.MyUser: # Voices # ########## - def deserialize_voice_state(self, payload: binding.JSONObject) -> voices.VoiceState: + def deserialize_voice_state(self, payload: data_binding.JSONObject) -> voices.VoiceState: """Parse a raw payload from Discord into a voice state object. Parameters @@ -1566,7 +1574,7 @@ def deserialize_voice_state(self, payload: binding.JSONObject) -> voices.VoiceSt voice_state.is_suppressed = payload["suppress"] return voice_state - def deserialize_voice_region(self, payload: binding.JSONObject) -> voices.VoiceRegion: + def deserialize_voice_region(self, payload: data_binding.JSONObject) -> voices.VoiceRegion: """Parse a raw payload from Discord into a voice region object. Parameters @@ -1592,7 +1600,7 @@ def deserialize_voice_region(self, payload: binding.JSONObject) -> voices.VoiceR # WEBHOOKS # ############ - def deserialize_webhook(self, payload: binding.JSONObject) -> webhooks.Webhook: + def deserialize_webhook(self, payload: data_binding.JSONObject) -> webhooks.Webhook: """Parse a raw payload from Discord into a webhook object. Parameters diff --git a/hikari/impl/event_manager.py b/hikari/impl/event_manager.py index cd66d34101..767e634385 100644 --- a/hikari/impl/event_manager.py +++ b/hikari/impl/event_manager.py @@ -24,11 +24,11 @@ from hikari.impl import event_manager_core from hikari.net import gateway -from hikari.utilities import binding +from hikari.utilities import data_binding class EventManagerImpl(event_manager_core.EventManagerCore): """Provides event handling logic for Discord events.""" - async def _on_message_create(self, shard: gateway.Gateway, payload: binding.JSONObject) -> None: + async def _on_message_create(self, shard: gateway.Gateway, payload: data_binding.JSONObject) -> None: print(shard, payload) diff --git a/hikari/impl/event_manager_core.py b/hikari/impl/event_manager_core.py index 87fb129a6f..df85967c15 100644 --- a/hikari/impl/event_manager_core.py +++ b/hikari/impl/event_manager_core.py @@ -30,7 +30,7 @@ from hikari.events import other from hikari.net import gateway from hikari.utilities import aio -from hikari.utilities import binding +from hikari.utilities import data_binding from hikari.utilities import klass if typing.TYPE_CHECKING: @@ -87,7 +87,9 @@ def dispatch(self, event: base.HikariEvent) -> aio.Future[typing.Any]: return asyncio.gather(*tasks) if tasks else aio.completed_future() - async def consume_raw_event(self, shard: gateway.Gateway, event_name: str, payload: binding.JSONObject) -> None: + async def consume_raw_event( + self, shard: gateway.Gateway, event_name: str, payload: data_binding.JSONObject + ) -> None: try: callback = getattr(self, "_on_" + event_name.lower()) await callback(shard, payload) diff --git a/hikari/models/audit_logs.py b/hikari/models/audit_logs.py index 42d4389fd6..c525b862ce 100644 --- a/hikari/models/audit_logs.py +++ b/hikari/models/audit_logs.py @@ -181,7 +181,7 @@ class AuditLogChange: @enum.unique -class AuditLogEventType(int, enum.Enum): +class AuditLogEventType(enum.IntEnum): """The type of event that occurred.""" GUILD_UPDATE = 1 @@ -220,6 +220,9 @@ class AuditLogEventType(int, enum.Enum): INTEGRATION_UPDATE = 81 INTEGRATION_DELETE = 82 + def __str__(self) -> str: + return self.name + @attr.s(eq=True, hash=False, init=False, kw_only=True, slots=True) class BaseAuditLogEntryInfo(abc.ABC): diff --git a/hikari/models/bases.py b/hikari/models/bases.py index 3ed3fed3ca..1324358182 100644 --- a/hikari/models/bases.py +++ b/hikari/models/bases.py @@ -131,5 +131,5 @@ def __int__(self) -> int: return int(self.id) -UniqueObjectT = typing.Union[Unique, Snowflake, int, str] +UniqueObject = typing.Union[Unique, Snowflake, int, str] """A unique object.""" diff --git a/hikari/net/gateway.py b/hikari/net/gateway.py index f4720dc6c6..083e2d7ee4 100644 --- a/hikari/net/gateway.py +++ b/hikari/net/gateway.py @@ -40,12 +40,11 @@ from hikari.net import http_client from hikari.net import ratelimits from hikari.net import user_agents -from hikari.utilities import binding +from hikari.utilities import data_binding from hikari.utilities import klass from hikari.utilities import snowflake from hikari.utilities import undefined - if typing.TYPE_CHECKING: import datetime @@ -568,7 +567,7 @@ async def _poll_events(self) -> None: else: self.logger.debug("ignoring unrecognised opcode %s", op) - async def _receive_json_payload(self) -> binding.JSONObject: + async def _receive_json_payload(self) -> data_binding.JSONObject: message = await self._receive_raw() if message.type == aiohttp.WSMsgType.BINARY: @@ -604,7 +603,7 @@ async def _receive_json_payload(self) -> binding.JSONObject: self.logger.debug("encountered unexpected error", exc_info=ex) raise errors.GatewayError("Unexpected websocket exception from gateway") from ex - return binding.load_json(string) + return data_binding.load_json(string) async def _receive_zlib_message(self, first_packet: bytes) -> typing.Tuple[int, str]: buff = bytearray(first_packet) @@ -625,13 +624,13 @@ async def _receive_raw(self) -> aiohttp.WSMessage: self.last_message_received = self._now() return packet - async def _send_json(self, payload: binding.JSONObject) -> None: + async def _send_json(self, payload: data_binding.JSONObject) -> None: await self.ratelimiter.acquire() - message = binding.dump_json(payload) + message = data_binding.dump_json(payload) self._log_debug_payload(message, "sending json payload") await self._ws.send_str(message) - def _dispatch(self, event_name: str, payload: binding.JSONObject) -> typing.Coroutine[None, typing.Any, None]: + def _dispatch(self, event_name: str, payload: data_binding.JSONObject) -> typing.Coroutine[None, typing.Any, None]: return self._app.event_consumer.consume_raw_event(self, event_name, payload) @staticmethod @@ -654,7 +653,7 @@ def _build_presence_payload( is_afk: typing.Union[undefined.Undefined, bool] = undefined.Undefined(), status: typing.Union[undefined.Undefined, guilds.PresenceStatus] = undefined.Undefined(), activity: typing.Union[undefined.Undefined, typing.Optional[Activity]] = undefined.Undefined(), - ) -> binding.JSONObject: + ) -> data_binding.JSONObject: if isinstance(idle_since, undefined.Undefined): idle_since = self._idle_since if isinstance(is_afk, undefined.Undefined): diff --git a/hikari/net/iterators.py b/hikari/net/iterators.py index 0301c34aa2..c86387717f 100644 --- a/hikari/net/iterators.py +++ b/hikari/net/iterators.py @@ -31,7 +31,7 @@ from hikari.models import messages from hikari.models import users from hikari.net import routes -from hikari.utilities import binding +from hikari.utilities import data_binding from hikari.utilities import snowflake from hikari.utilities import undefined @@ -245,7 +245,7 @@ class MessageIterator(_BufferedLazyIterator[messages.Message]): def __init__( self, app: app_.IRESTApp, - request_call: typing.Callable[..., typing.Coroutine[None, typing.Any, binding.JSONArray]], + request_call: typing.Callable[..., typing.Coroutine[None, typing.Any, data_binding.JSONArray]], channel_id: typing.Union[typing.SupportsInt, int], direction: str, first_id: typing.Union[typing.SupportsInt, int], @@ -258,7 +258,7 @@ def __init__( self._route = routes.GET_CHANNEL_MESSAGES.compile(channel=channel_id) async def _next_chunk(self) -> typing.Optional[typing.Generator[messages.Message, typing.Any, None]]: - query = binding.StringMapBuilder() + query = data_binding.StringMapBuilder() query.put(self._direction, self._first_id) query.put("limit", 100) chunk = await self._request_call(self._route, query) @@ -280,7 +280,7 @@ class ReactorIterator(_BufferedLazyIterator[users.User]): def __init__( self, app: app_.IRESTApp, - request_call: typing.Callable[..., typing.Coroutine[None, typing.Any, binding.JSONArray]], + request_call: typing.Callable[..., typing.Coroutine[None, typing.Any, data_binding.JSONArray]], channel_id: typing.Union[typing.SupportsInt, int], message_id: typing.Union[typing.SupportsInt, int], emoji: str, @@ -292,7 +292,7 @@ def __init__( self._route = routes.GET_REACTIONS.compile(channel=channel_id, message=message_id, emoji=emoji) async def _next_chunk(self) -> typing.Optional[typing.Generator[users.User, typing.Any, None]]: - query = binding.StringMapBuilder() + query = data_binding.StringMapBuilder() query.put("after", self._first_id) query.put("limit", 100) chunk = await self._request_call(self._route, query=query) @@ -312,7 +312,7 @@ class OwnGuildIterator(_BufferedLazyIterator[applications.OwnGuild]): def __init__( self, app: app_.IRESTApp, - request_call: typing.Callable[..., typing.Coroutine[None, typing.Any, binding.JSONArray]], + request_call: typing.Callable[..., typing.Coroutine[None, typing.Any, data_binding.JSONArray]], newest_first: bool, first_id: typing.Union[typing.SupportsInt, int], ) -> None: @@ -324,7 +324,7 @@ def __init__( self._route = routes.GET_MY_GUILDS.compile() async def _next_chunk(self) -> typing.Optional[typing.Generator[applications.OwnGuild, typing.Any, None]]: - query = binding.StringMapBuilder() + query = data_binding.StringMapBuilder() query.put("before" if self._newest_first else "after", self._first_id) query.put("limit", 100) @@ -345,7 +345,7 @@ class MemberIterator(_BufferedLazyIterator[guilds.GuildMember]): def __init__( self, app: app_.IRESTApp, - request_call: typing.Callable[..., typing.Coroutine[None, typing.Any, binding.JSONArray]], + request_call: typing.Callable[..., typing.Coroutine[None, typing.Any, data_binding.JSONArray]], guild_id: typing.Union[typing.SupportsInt, int], ) -> None: super().__init__() @@ -355,7 +355,7 @@ def __init__( self._first_id = snowflake.Snowflake.min() async def _next_chunk(self) -> typing.Optional[typing.Generator[guilds.GuildMember, typing.Any, None]]: - query = binding.StringMapBuilder() + query = data_binding.StringMapBuilder() query.put("after", self._first_id) query.put("limit", 100) chunk = await self._request_call(self._route, query=query) @@ -375,7 +375,7 @@ class AuditLogIterator(LazyIterator[audit_logs.AuditLog]): def __init__( self, app: app_.IRESTApp, - request_call: typing.Callable[..., typing.Coroutine[None, typing.Any, binding.JSONObject]], + request_call: typing.Callable[..., typing.Coroutine[None, typing.Any, data_binding.JSONObject]], guild_id: typing.Union[typing.SupportsInt, int], before: typing.Union[typing.SupportsInt, int], user_id: typing.Union[typing.SupportsInt, int, undefined.Undefined], @@ -389,7 +389,7 @@ def __init__( self._user_id = user_id async def __anext__(self) -> audit_logs.AuditLog: - query = binding.StringMapBuilder() + query = data_binding.StringMapBuilder() query.put("limit", 100) query.put("user_id", self._user_id) query.put("event_type", self._action_type) diff --git a/hikari/net/rest.py b/hikari/net/rest.py index adf6ef9f7d..9d2e81d254 100644 --- a/hikari/net/rest.py +++ b/hikari/net/rest.py @@ -37,7 +37,7 @@ from hikari.net import ratelimits from hikari.net import rest_utils from hikari.net import routes -from hikari.utilities import binding +from hikari.utilities import data_binding from hikari.utilities import date from hikari.utilities import klass from hikari.utilities import snowflake @@ -143,13 +143,13 @@ async def _request( self, compiled_route: routes.CompiledRoute, *, - query: typing.Union[undefined.Undefined, binding.StringMapBuilder] = undefined.Undefined(), + query: typing.Union[undefined.Undefined, data_binding.StringMapBuilder] = undefined.Undefined(), body: typing.Union[ - undefined.Undefined, aiohttp.FormData, binding.JSONObjectBuilder, binding.JSONArray + undefined.Undefined, aiohttp.FormData, data_binding.JSONObjectBuilder, data_binding.JSONArray ] = undefined.Undefined(), reason: typing.Union[undefined.Undefined, str] = undefined.Undefined(), no_auth: bool = False, - ) -> typing.Optional[binding.JSONObject, binding.JSONArray, bytes, str]: + ) -> typing.Optional[data_binding.JSONObject, data_binding.JSONArray, bytes, str]: # Make a ratelimit-protected HTTP request to a JSON endpoint and expect some form # of JSON response. If an error occurs, the response body is returned in the # raised exception as a bytes object. This is done since the differences between @@ -160,7 +160,7 @@ async def _request( if not self.buckets.is_started: self.buckets.start() - headers = binding.StringMapBuilder() + headers = data_binding.StringMapBuilder() headers.put("x-ratelimit-precision", "millisecond") headers.put("accept", self._APPLICATION_JSON) @@ -186,10 +186,10 @@ async def _request( async def _request_once( self, compiled_route: routes.CompiledRoute, - headers: binding.Headers, - body: typing.Optional[typing.Union[aiohttp.FormData, binding.JSONArray, binding.JSONObject]], + headers: data_binding.Headers, + body: typing.Optional[typing.Union[aiohttp.FormData, data_binding.JSONArray, data_binding.JSONObject]], query: typing.Optional[typing.Dict[str, str]], - ) -> typing.Optional[binding.JSONObject, binding.JSONArray, bytes, str]: + ) -> typing.Optional[data_binding.JSONObject, data_binding.JSONArray, bytes, str]: url = compiled_route.create_url(self._url) # Wait for any ratelimits to finish. @@ -216,7 +216,7 @@ async def _request_once( if 200 <= response.status < 300: if response.content_type == self._APPLICATION_JSON: # Only deserializing here stops Cloudflare shenanigans messing us around. - return binding.load_json(raw_body) + return data_binding.load_json(raw_body) raise errors.HTTPError(real_url, f"Expected JSON response but received {response.content_type}") if response.status == http.HTTPStatus.BAD_REQUEST: @@ -324,8 +324,8 @@ async def _handle_rate_limits_for_response( @staticmethod def _generate_allowed_mentions( mentions_everyone: bool, - user_mentions: typing.Union[typing.Collection[typing.Union[bases.UniqueObjectT, users.User]], bool], - role_mentions: typing.Union[typing.Collection[typing.Union[bases.UniqueObjectT, guilds.Role]], bool], + user_mentions: typing.Union[typing.Collection[typing.Union[bases.UniqueObject, users.User]], bool], + role_mentions: typing.Union[typing.Collection[typing.Union[bases.UniqueObject, guilds.Role]], bool], ): parsed_mentions = [] allowed_mentions = {} @@ -363,10 +363,10 @@ def _generate_allowed_mentions( return allowed_mentions def _build_message_creation_form( - self, payload: binding.JSONObject, attachments: typing.Sequence[files.BaseStream], + self, payload: data_binding.JSONObject, attachments: typing.Sequence[files.BaseStream], ) -> aiohttp.FormData: - form = binding.URLEncodedForm() - form.add_field("payload_json", binding.dump_json(payload), content_type=self._APPLICATION_JSON) + form = data_binding.URLEncodedForm() + form.add_field("payload_json", data_binding.dump_json(payload), content_type=self._APPLICATION_JSON) for i, attachment in enumerate(attachments): form.add_field( f"file{i}", attachment, filename=attachment.filename, content_type=self._APPLICATION_OCTET_STREAM @@ -379,7 +379,7 @@ async def close(self) -> None: self.buckets.close() async def fetch_channel( - self, channel: typing.Union[channels.PartialChannel, bases.UniqueObjectT], /, + self, channel: typing.Union[channels.PartialChannel, bases.UniqueObject], /, ) -> channels.PartialChannel: """Fetch a channel. @@ -440,7 +440,7 @@ async def edit_channel( async def edit_channel( self, - channel: typing.Union[channels.PartialChannel, bases.UniqueObjectT], + channel: typing.Union[channels.PartialChannel, bases.UniqueObject], /, *, name: typing.Union[undefined.Undefined, str] = undefined.Undefined(), @@ -489,7 +489,7 @@ async def edit_channel( If an internal error occurs on Discord while handling the request. """ route = routes.PATCH_CHANNEL.compile(channel=channel) - body = binding.JSONObjectBuilder() + body = data_binding.JSONObjectBuilder() body.put("name", name) body.put("position", position) body.put("topic", topic) @@ -505,7 +505,7 @@ async def edit_channel( response = await self._request(route, body=body, reason=reason) return self._app.entity_factory.deserialize_channel(response) - async def delete_channel(self, channel: typing.Union[channels.PartialChannel, bases.UniqueObjectT], /) -> None: + async def delete_channel(self, channel: typing.Union[channels.PartialChannel, bases.UniqueObject], /) -> None: """Delete a channel in a guild, or close a DM. Parameters @@ -530,7 +530,7 @@ async def delete_channel(self, channel: typing.Union[channels.PartialChannel, ba @typing.overload async def edit_permission_overwrites( self, - channel: typing.Union[channels.GuildChannel, bases.UniqueObjectT], + channel: typing.Union[channels.GuildChannel, bases.UniqueObject], target: typing.Union[channels.PermissionOverwrite, users.User, guilds.Role], *, allow: typing.Union[undefined.Undefined, permissions_.Permission] = undefined.Undefined(), @@ -542,7 +542,7 @@ async def edit_permission_overwrites( @typing.overload async def edit_permission_overwrites( self, - channel: typing.Union[channels.GuildChannel, bases.UniqueObjectT], + channel: typing.Union[channels.GuildChannel, bases.UniqueObject], target: typing.Union[int, str, snowflake.Snowflake], target_type: typing.Union[channels.PermissionOverwriteType, str], *, @@ -554,8 +554,8 @@ async def edit_permission_overwrites( async def edit_permission_overwrites( self, - channel: typing.Union[channels.GuildChannel, bases.UniqueObjectT], - target: typing.Union[bases.UniqueObjectT, users.User, guilds.Role, channels.PermissionOverwrite], + channel: typing.Union[channels.GuildChannel, bases.UniqueObject], + target: typing.Union[bases.UniqueObject, users.User, guilds.Role, channels.PermissionOverwrite], target_type: typing.Union[undefined.Undefined, channels.PermissionOverwriteType, str] = undefined.Undefined(), *, allow: typing.Union[undefined.Undefined, permissions_.Permission] = undefined.Undefined(), @@ -601,7 +601,7 @@ async def edit_permission_overwrites( ) route = routes.PATCH_CHANNEL_PERMISSIONS.compile(channel=channel, overwrite=target) - body = binding.JSONObjectBuilder() + body = data_binding.JSONObjectBuilder() body.put("type", target_type) body.put("allow", allow) body.put("deny", deny) @@ -610,8 +610,8 @@ async def edit_permission_overwrites( async def delete_permission_overwrite( self, - channel: typing.Union[channels.GuildChannel, bases.UniqueObjectT], - target: typing.Union[channels.PermissionOverwrite, guilds.Role, users.User, bases.UniqueObjectT], + channel: typing.Union[channels.GuildChannel, bases.UniqueObject], + target: typing.Union[channels.PermissionOverwrite, guilds.Role, users.User, bases.UniqueObject], ) -> None: """Delete a custom permission for an entity in a given guild channel. @@ -635,7 +635,7 @@ async def delete_permission_overwrite( await self._request(route) async def fetch_channel_invites( - self, channel: typing.Union[channels.GuildChannel, bases.UniqueObjectT], / + self, channel: typing.Union[channels.GuildChannel, bases.UniqueObject], / ) -> typing.Sequence[invites.InviteWithMetadata]: """Fetch all invites pointing to the given guild channel. @@ -659,18 +659,18 @@ async def fetch_channel_invites( """ route = routes.GET_CHANNEL_INVITES.compile(channel=channel) response = await self._request(route) - return binding.cast_json_array(response, self._app.entity_factory.deserialize_invite_with_metadata) + return data_binding.cast_json_array(response, self._app.entity_factory.deserialize_invite_with_metadata) async def create_invite( self, - channel: typing.Union[channels.GuildChannel, bases.UniqueObjectT], + channel: typing.Union[channels.GuildChannel, bases.UniqueObject], /, *, max_age: typing.Union[undefined.Undefined, int, float, datetime.timedelta] = undefined.Undefined(), max_uses: typing.Union[undefined.Undefined, int] = undefined.Undefined(), temporary: typing.Union[undefined.Undefined, bool] = undefined.Undefined(), unique: typing.Union[undefined.Undefined, bool] = undefined.Undefined(), - target_user: typing.Union[undefined.Undefined, users.User, bases.UniqueObjectT] = undefined.Undefined(), + target_user: typing.Union[undefined.Undefined, users.User, bases.UniqueObject] = undefined.Undefined(), target_user_type: typing.Union[undefined.Undefined, invites.TargetUserType] = undefined.Undefined(), reason: typing.Union[undefined.Undefined, str] = undefined.Undefined(), ) -> invites.InviteWithMetadata: @@ -705,7 +705,7 @@ async def create_invite( If an internal error occurs on Discord while handling the request. """ route = routes.POST_CHANNEL_INVITES.compile(channel=channel) - body = binding.JSONObjectBuilder() + body = data_binding.JSONObjectBuilder() body.put("max_age", max_age, date.timespan_to_int) body.put("max_uses", max_uses) body.put("temporary", temporary) @@ -716,7 +716,7 @@ async def create_invite( return self._app.entity_factory.deserialize_invite_with_metadata(response) def trigger_typing( - self, channel: typing.Union[channels.TextChannel, bases.UniqueObjectT], / + self, channel: typing.Union[channels.TextChannel, bases.UniqueObject], / ) -> rest_utils.TypingIndicator: """Trigger typing in a text channel. @@ -747,7 +747,7 @@ def trigger_typing( return rest_utils.TypingIndicator(channel, self._request) async def fetch_pins( - self, channel: typing.Union[channels.TextChannel, bases.UniqueObjectT], / + self, channel: typing.Union[channels.TextChannel, bases.UniqueObject], / ) -> typing.Sequence[messages_.Message]: """Fetch the pinned messages in this text channel. @@ -772,12 +772,12 @@ async def fetch_pins( """ route = routes.GET_CHANNEL_PINS.compile(channel=channel) response = await self._request(route) - return binding.cast_json_array(response, self._app.entity_factory.deserialize_message) + return data_binding.cast_json_array(response, self._app.entity_factory.deserialize_message) async def pin_message( self, - channel: typing.Union[channels.TextChannel, bases.UniqueObjectT], - message: typing.Union[messages_.Message, bases.UniqueObjectT], + channel: typing.Union[channels.TextChannel, bases.UniqueObject], + message: typing.Union[messages_.Message, bases.UniqueObject], ) -> None: """Pin an existing message in the given text channel. @@ -803,8 +803,8 @@ async def pin_message( async def unpin_message( self, - channel: typing.Union[channels.TextChannel, bases.UniqueObjectT], - message: typing.Union[messages_.Message, bases.UniqueObjectT], + channel: typing.Union[channels.TextChannel, bases.UniqueObject], + message: typing.Union[messages_.Message, bases.UniqueObject], ) -> None: """ @@ -835,48 +835,48 @@ async def unpin_message( @typing.overload def fetch_messages( - self, channel: typing.Union[channels.TextChannel, bases.UniqueObjectT], / + self, channel: typing.Union[channels.TextChannel, bases.UniqueObject], / ) -> iterators.LazyIterator[messages_.Message]: """Fetch messages, newest first, sent in the given channel.""" @typing.overload def fetch_messages( self, - channel: typing.Union[channels.TextChannel, bases.UniqueObjectT], + channel: typing.Union[channels.TextChannel, bases.UniqueObject], /, *, - before: typing.Union[datetime.datetime, bases.UniqueObjectT], + before: typing.Union[datetime.datetime, bases.UniqueObject], ) -> iterators.LazyIterator[messages_.Message]: """Fetch messages, newest first, sent before a timestamp in the channel.""" @typing.overload def fetch_messages( self, - channel: typing.Union[channels.TextChannel, bases.UniqueObjectT], + channel: typing.Union[channels.TextChannel, bases.UniqueObject], /, *, - around: typing.Union[datetime.datetime, bases.UniqueObjectT], + around: typing.Union[datetime.datetime, bases.UniqueObject], ) -> iterators.LazyIterator[messages_.Message]: """Fetch messages sent around a given time in the channel.""" @typing.overload def fetch_messages( self, - channel: typing.Union[channels.TextChannel, bases.UniqueObjectT], + channel: typing.Union[channels.TextChannel, bases.UniqueObject], /, *, - after: typing.Union[datetime.datetime, bases.UniqueObjectT], + after: typing.Union[datetime.datetime, bases.UniqueObject], ) -> iterators.LazyIterator[messages_.Message]: """Fetch messages, oldest first, sent after a timestamp in the channel.""" def fetch_messages( self, - channel: typing.Union[channels.TextChannel, bases.UniqueObjectT], + channel: typing.Union[channels.TextChannel, bases.UniqueObject], /, *, - before: typing.Union[undefined.Undefined, datetime.datetime, bases.UniqueObjectT] = undefined.Undefined(), - after: typing.Union[undefined.Undefined, datetime.datetime, bases.UniqueObjectT] = undefined.Undefined(), - around: typing.Union[undefined.Undefined, datetime.datetime, bases.UniqueObjectT] = undefined.Undefined(), + before: typing.Union[undefined.Undefined, datetime.datetime, bases.UniqueObject] = undefined.Undefined(), + after: typing.Union[undefined.Undefined, datetime.datetime, bases.UniqueObject] = undefined.Undefined(), + around: typing.Union[undefined.Undefined, datetime.datetime, bases.UniqueObject] = undefined.Undefined(), ) -> iterators.LazyIterator[messages_.Message]: """Browse the message history for a given text channel. @@ -928,8 +928,8 @@ def fetch_messages( async def fetch_message( self, - channel: typing.Union[channels.TextChannel, bases.UniqueObjectT], - message: typing.Union[messages_.Message, bases.UniqueObjectT], + channel: typing.Union[channels.TextChannel, bases.UniqueObject], + message: typing.Union[messages_.Message, bases.UniqueObject], ) -> messages_.Message: """Fetch a specific message in the given text channel. @@ -960,7 +960,7 @@ async def fetch_message( async def create_message( self, - channel: typing.Union[channels.TextChannel, bases.UniqueObjectT], + channel: typing.Union[channels.TextChannel, bases.UniqueObject], text: typing.Union[undefined.Undefined, typing.Any] = undefined.Undefined(), *, embed: typing.Union[undefined.Undefined, embeds_.Embed] = undefined.Undefined(), @@ -968,8 +968,8 @@ async def create_message( tts: typing.Union[undefined.Undefined, bool] = undefined.Undefined(), nonce: typing.Union[undefined.Undefined, str] = undefined.Undefined(), mentions_everyone: bool = False, - user_mentions: typing.Union[typing.Collection[typing.Union[users.User, bases.UniqueObjectT]], bool] = True, - role_mentions: typing.Union[typing.Collection[typing.Union[bases.UniqueObjectT, guilds.Role]], bool] = True, + user_mentions: typing.Union[typing.Collection[typing.Union[users.User, bases.UniqueObject]], bool] = True, + role_mentions: typing.Union[typing.Collection[typing.Union[bases.UniqueObject, guilds.Role]], bool] = True, ) -> messages_.Message: """Create a message in the given channel. @@ -1014,7 +1014,7 @@ async def create_message( route = routes.POST_CHANNEL_MESSAGES.compile(channel=channel) - body = binding.JSONObjectBuilder() + body = data_binding.JSONObjectBuilder() body.put("allowed_mentions", self._generate_allowed_mentions(mentions_everyone, user_mentions, role_mentions)) body.put("content", text, str) body.put("embed", embed, self._app.entity_factory.serialize_embed) @@ -1034,14 +1034,14 @@ async def create_message( async def edit_message( self, - channel: typing.Union[channels.TextChannel, bases.UniqueObjectT], - message: typing.Union[messages_.Message, bases.UniqueObjectT], + channel: typing.Union[channels.TextChannel, bases.UniqueObject], + message: typing.Union[messages_.Message, bases.UniqueObject], text: typing.Union[undefined.Undefined, typing.Any] = undefined.Undefined(), *, embed: typing.Union[undefined.Undefined, embeds_.Embed] = undefined.Undefined(), mentions_everyone: bool = False, - user_mentions: typing.Union[typing.Collection[typing.Union[users.User, bases.UniqueObjectT]], bool] = True, - role_mentions: typing.Union[typing.Collection[typing.Union[bases.UniqueObjectT, guilds.Role]], bool] = True, + user_mentions: typing.Union[typing.Collection[typing.Union[users.User, bases.UniqueObject]], bool] = True, + role_mentions: typing.Union[typing.Collection[typing.Union[bases.UniqueObject, guilds.Role]], bool] = True, flags: typing.Union[undefined.Undefined, messages_.MessageFlag] = undefined.Undefined(), ) -> messages_.Message: """Edit an existing message in a given channel. @@ -1075,7 +1075,7 @@ async def edit_message( """ route = routes.PATCH_CHANNEL_MESSAGE.compile(channel=channel, message=message) - body = binding.JSONObjectBuilder() + body = data_binding.JSONObjectBuilder() body.put("content", text, str) body.put("embed", embed, self._app.entity_factory.serialize_embed) body.put("flags", flags) @@ -1085,8 +1085,8 @@ async def edit_message( async def delete_message( self, - channel: typing.Union[channels.TextChannel, bases.UniqueObjectT], - message: typing.Union[messages_.Message, bases.UniqueObjectT], + channel: typing.Union[channels.TextChannel, bases.UniqueObject], + message: typing.Union[messages_.Message, bases.UniqueObject], ) -> None: """Delete a given message in a given channel. @@ -1112,8 +1112,8 @@ async def delete_message( async def delete_messages( self, - channel: typing.Union[channels.GuildTextChannel, bases.UniqueObjectT], - *messages: typing.Union[messages_.Message, bases.UniqueObjectT], + channel: typing.Union[channels.GuildTextChannel, bases.UniqueObject], + *messages: typing.Union[messages_.Message, bases.UniqueObject], ) -> None: """Bulk-delete between 2 and 100 messages from the given guild channel. @@ -1138,7 +1138,7 @@ async def delete_messages( """ if 2 <= len(messages) <= 100: route = routes.POST_DELETE_CHANNEL_MESSAGES_BULK.compile(channel=channel) - body = binding.JSONObjectBuilder() + body = data_binding.JSONObjectBuilder() body.put_snowflake_array("messages", messages) await self._request(route, body=body) else: @@ -1146,8 +1146,8 @@ async def delete_messages( async def add_reaction( self, - channel: typing.Union[channels.TextChannel, bases.UniqueObjectT], - message: typing.Union[messages_.Message, bases.UniqueObjectT], + channel: typing.Union[channels.TextChannel, bases.UniqueObject], + message: typing.Union[messages_.Message, bases.UniqueObject], emoji: typing.Union[str, emojis.UnicodeEmoji, emojis.KnownCustomEmoji], ) -> None: """Add a reaction emoji to a message in a given channel. @@ -1181,8 +1181,8 @@ async def add_reaction( async def delete_my_reaction( self, - channel: typing.Union[channels.TextChannel, bases.UniqueObjectT], - message: typing.Union[messages_.Message, bases.UniqueObjectT], + channel: typing.Union[channels.TextChannel, bases.UniqueObject], + message: typing.Union[messages_.Message, bases.UniqueObject], emoji: typing.Union[str, emojis.UnicodeEmoji, emojis.KnownCustomEmoji], ) -> None: """Delete a reaction that your application user created. @@ -1214,8 +1214,8 @@ async def delete_my_reaction( async def delete_all_reactions_for_emoji( self, - channel: typing.Union[channels.TextChannel, bases.UniqueObjectT], - message: typing.Union[messages_.Message, bases.UniqueObjectT], + channel: typing.Union[channels.TextChannel, bases.UniqueObject], + message: typing.Union[messages_.Message, bases.UniqueObject], emoji: typing.Union[str, emojis.UnicodeEmoji, emojis.KnownCustomEmoji], ) -> None: route = routes.DELETE_REACTION_EMOJI.compile( @@ -1227,10 +1227,10 @@ async def delete_all_reactions_for_emoji( async def delete_reaction( self, - channel: typing.Union[channels.TextChannel, bases.UniqueObjectT], - message: typing.Union[messages_.Message, bases.UniqueObjectT], + channel: typing.Union[channels.TextChannel, bases.UniqueObject], + message: typing.Union[messages_.Message, bases.UniqueObject], emoji: typing.Union[str, emojis.UnicodeEmoji, emojis.KnownCustomEmoji], - user: typing.Union[users.User, bases.UniqueObjectT], + user: typing.Union[users.User, bases.UniqueObject], ) -> None: route = routes.DELETE_REACTION_USER.compile( emoji=emoji.url_name if isinstance(emoji, emojis.KnownCustomEmoji) else str(emoji), @@ -1242,16 +1242,16 @@ async def delete_reaction( async def delete_all_reactions( self, - channel: typing.Union[channels.TextChannel, bases.UniqueObjectT], - message: typing.Union[messages_.Message, bases.UniqueObjectT], + channel: typing.Union[channels.TextChannel, bases.UniqueObject], + message: typing.Union[messages_.Message, bases.UniqueObject], ) -> None: route = routes.DELETE_ALL_REACTIONS.compile(channel=channel, message=message) await self._request(route) def fetch_reactions_for_emoji( self, - channel: typing.Union[channels.TextChannel, bases.UniqueObjectT], - message: typing.Union[messages_.Message, bases.UniqueObjectT], + channel: typing.Union[channels.TextChannel, bases.UniqueObject], + message: typing.Union[messages_.Message, bases.UniqueObject], emoji: typing.Union[str, emojis.UnicodeEmoji, emojis.KnownCustomEmoji], ) -> iterators.LazyIterator[users.User]: return iterators.ReactorIterator( @@ -1264,14 +1264,14 @@ def fetch_reactions_for_emoji( async def create_webhook( self, - channel: typing.Union[channels.TextChannel, bases.UniqueObjectT], + channel: typing.Union[channels.TextChannel, bases.UniqueObject], name: str, *, avatar: typing.Union[undefined.Undefined, files.BaseStream] = undefined.Undefined(), reason: typing.Union[undefined.Undefined, str] = undefined.Undefined(), ) -> webhooks.Webhook: route = routes.POST_WEBHOOK.compile(channel=channel) - body = binding.JSONObjectBuilder() + body = data_binding.JSONObjectBuilder() body.put("name", name) if not isinstance(avatar, undefined.Undefined): body.put("avatar", await avatar.fetch_data_uri()) @@ -1281,7 +1281,7 @@ async def create_webhook( async def fetch_webhook( self, - webhook: typing.Union[webhooks.Webhook, bases.UniqueObjectT], + webhook: typing.Union[webhooks.Webhook, bases.UniqueObject], /, *, token: typing.Union[undefined.Undefined, str] = undefined.Undefined(), @@ -1294,28 +1294,28 @@ async def fetch_webhook( return self._app.entity_factory.deserialize_webhook(response) async def fetch_channel_webhooks( - self, channel: typing.Union[channels.TextChannel, bases.UniqueObjectT], / + self, channel: typing.Union[channels.TextChannel, bases.UniqueObject], / ) -> typing.Sequence[webhooks.Webhook]: route = routes.GET_CHANNEL_WEBHOOKS.compile(channel=channel) response = await self._request(route) - return binding.cast_json_array(response, self._app.entity_factory.deserialize_webhook) + return data_binding.cast_json_array(response, self._app.entity_factory.deserialize_webhook) async def fetch_guild_webhooks( - self, guild: typing.Union[guilds.Guild, bases.UniqueObjectT], / + self, guild: typing.Union[guilds.Guild, bases.UniqueObject], / ) -> typing.Sequence[webhooks.Webhook]: route = routes.GET_GUILD_WEBHOOKS.compile(channel=guild) response = await self._request(route) - return binding.cast_json_array(response, self._app.entity_factory.deserialize_webhook) + return data_binding.cast_json_array(response, self._app.entity_factory.deserialize_webhook) async def edit_webhook( self, - webhook: typing.Union[webhooks.Webhook, bases.UniqueObjectT], + webhook: typing.Union[webhooks.Webhook, bases.UniqueObject], /, *, token: typing.Union[undefined.Undefined, str] = undefined.Undefined(), name: typing.Union[undefined.Undefined, str] = undefined.Undefined(), avatar: typing.Union[undefined.Undefined, files.BaseStream] = undefined.Undefined(), - channel: typing.Union[undefined.Undefined, channels.TextChannel, bases.UniqueObjectT] = undefined.Undefined(), + channel: typing.Union[undefined.Undefined, channels.TextChannel, bases.UniqueObject] = undefined.Undefined(), reason: typing.Union[undefined.Undefined, str] = undefined.Undefined(), ) -> webhooks.Webhook: if isinstance(token, undefined.Undefined): @@ -1323,7 +1323,7 @@ async def edit_webhook( else: route = routes.PATCH_WEBHOOK_WITH_TOKEN.compile(webhook=webhook, token=token) - body = binding.JSONObjectBuilder() + body = data_binding.JSONObjectBuilder() body.put("name", name) body.put_snowflake("channel", channel) if not isinstance(avatar, undefined.Undefined): @@ -1334,7 +1334,7 @@ async def edit_webhook( async def delete_webhook( self, - webhook: typing.Union[webhooks.Webhook, bases.UniqueObjectT], + webhook: typing.Union[webhooks.Webhook, bases.UniqueObject], /, *, token: typing.Union[undefined.Undefined, str] = undefined.Undefined(), @@ -1347,7 +1347,7 @@ async def delete_webhook( async def execute_webhook( self, - webhook: typing.Union[webhooks.Webhook, bases.UniqueObjectT], + webhook: typing.Union[webhooks.Webhook, bases.UniqueObject], text: typing.Union[undefined.Undefined, typing.Any] = undefined.Undefined(), *, token: typing.Union[undefined.Undefined, str] = undefined.Undefined(), @@ -1358,8 +1358,8 @@ async def execute_webhook( tts: typing.Union[undefined.Undefined, bool] = undefined.Undefined(), wait: typing.Union[undefined.Undefined, bool] = undefined.Undefined(), mentions_everyone: bool = False, - user_mentions: typing.Union[typing.Collection[typing.Union[users.User, bases.UniqueObjectT]], bool] = True, - role_mentions: typing.Union[typing.Collection[typing.Union[bases.UniqueObjectT, guilds.Role]], bool] = True, + user_mentions: typing.Union[typing.Collection[typing.Union[users.User, bases.UniqueObject]], bool] = True, + role_mentions: typing.Union[typing.Collection[typing.Union[bases.UniqueObject, guilds.Role]], bool] = True, ) -> messages_.Message: if isinstance(token, undefined.Undefined): route = routes.POST_WEBHOOK.compile(webhook=webhook) @@ -1376,7 +1376,7 @@ async def execute_webhook( attachments += embed.assets_to_upload serialized_embeds.append(self._app.entity_factory.serialize_embed(embed)) - body = binding.JSONObjectBuilder() + body = data_binding.JSONObjectBuilder() body.put("mentions", self._generate_allowed_mentions(mentions_everyone, user_mentions, role_mentions)) body.put("content", text, str) body.put("embeds", serialized_embeds) @@ -1404,7 +1404,7 @@ async def fetch_recommended_gateway_settings(self) -> gateway.GatewayBot: async def fetch_invite(self, invite: typing.Union[invites.Invite, str]) -> invites.Invite: route = routes.GET_INVITE.compile(invite_code=invite if isinstance(invite, str) else invite.code) - query = binding.StringMapBuilder() + query = data_binding.StringMapBuilder() query.put("with_counts", True) response = await self._request(route, query=query) return self._app.entity_factory.deserialize_invite(response) @@ -1425,7 +1425,7 @@ async def edit_my_user( avatar: typing.Union[undefined.Undefined, files.BaseStream] = undefined.Undefined(), ) -> users.MyUser: route = routes.PATCH_MY_USER.compile() - body = binding.JSONObjectBuilder() + body = data_binding.JSONObjectBuilder() body.put("username", username) if not isinstance(username, undefined.Undefined): @@ -1437,14 +1437,14 @@ async def edit_my_user( async def fetch_my_connections(self) -> typing.Sequence[applications.OwnConnection]: route = routes.GET_MY_CONNECTIONS.compile() response = await self._request(route) - return binding.cast_json_array(response, self._app.entity_factory.deserialize_own_connection) + return data_binding.cast_json_array(response, self._app.entity_factory.deserialize_own_connection) def fetch_my_guilds( self, *, newest_first: bool = False, start_at: typing.Union[ - undefined.Undefined, guilds.PartialGuild, bases.UniqueObjectT, datetime.datetime + undefined.Undefined, guilds.PartialGuild, bases.UniqueObject, datetime.datetime ] = undefined.Undefined(), ) -> iterators.LazyIterator[applications.OwnGuild]: if isinstance(start_at, undefined.Undefined): @@ -1454,13 +1454,13 @@ def fetch_my_guilds( return iterators.OwnGuildIterator(self._app, self._request, newest_first, start_at) - async def leave_guild(self, guild: typing.Union[guilds.Guild, bases.UniqueObjectT], /) -> None: + async def leave_guild(self, guild: typing.Union[guilds.Guild, bases.UniqueObject], /) -> None: route = routes.DELETE_MY_GUILD.compile(guild=guild) await self._request(route) - async def create_dm_channel(self, user: typing.Union[users.User, bases.UniqueObjectT], /) -> channels.DMChannel: + async def create_dm_channel(self, user: typing.Union[users.User, bases.UniqueObject], /) -> channels.DMChannel: route = routes.POST_MY_CHANNELS.compile() - body = binding.JSONObjectBuilder() + body = data_binding.JSONObjectBuilder() body.put_snowflake("recipient_id", user) response = await self._request(route, body=body) return self._app.entity_factory.deserialize_dm_channel(response) @@ -1473,18 +1473,18 @@ async def fetch_application(self) -> applications.Application: async def add_user_to_guild( self, access_token: str, - guild: typing.Union[guilds.Guild, bases.UniqueObjectT], - user: typing.Union[users.User, bases.UniqueObjectT], + guild: typing.Union[guilds.Guild, bases.UniqueObject], + user: typing.Union[users.User, bases.UniqueObject], *, nick: typing.Union[undefined.Undefined, str] = undefined.Undefined(), roles: typing.Union[ - undefined.Undefined, typing.Collection[typing.Union[guilds.Role, bases.UniqueObjectT]] + undefined.Undefined, typing.Collection[typing.Union[guilds.Role, bases.UniqueObject]] ] = undefined.Undefined(), mute: typing.Union[undefined.Undefined, bool] = undefined.Undefined(), deaf: typing.Union[undefined.Undefined, bool] = undefined.Undefined(), ) -> typing.Optional[guilds.GuildMember]: route = routes.PUT_GUILD_MEMBER.compile(guild=guild, user=user) - body = binding.JSONObjectBuilder() + body = data_binding.JSONObjectBuilder() body.put("access_token", access_token) body.put("nick", nick) body.put("mute", mute) @@ -1500,20 +1500,20 @@ async def add_user_to_guild( async def fetch_voice_regions(self) -> typing.Sequence[voices.VoiceRegion]: route = routes.GET_VOICE_REGIONS.compile() response = await self._request(route) - return binding.cast_json_array(response, self._app.entity_factory.deserialize_voice_region) + return data_binding.cast_json_array(response, self._app.entity_factory.deserialize_voice_region) - async def fetch_user(self, user: typing.Union[users.User, bases.UniqueObjectT]) -> users.User: + async def fetch_user(self, user: typing.Union[users.User, bases.UniqueObject]) -> users.User: route = routes.GET_USER.compile(user=user) response = await self._request(route) return self._app.entity_factory.deserialize_user(response) def fetch_audit_log( self, - guild: typing.Union[guilds.Guild, bases.UniqueObjectT], + guild: typing.Union[guilds.Guild, bases.UniqueObject], /, *, - before: typing.Union[undefined.Undefined, datetime.datetime, bases.UniqueObjectT] = undefined.Undefined(), - user: typing.Union[undefined.Undefined, users.User, bases.UniqueObjectT] = undefined.Undefined(), + before: typing.Union[undefined.Undefined, datetime.datetime, bases.UniqueObject] = undefined.Undefined(), + user: typing.Union[undefined.Undefined, users.User, bases.UniqueObject] = undefined.Undefined(), event_type: typing.Union[undefined.Undefined, audit_logs.AuditLogEventType] = undefined.Undefined(), ) -> iterators.LazyIterator[audit_logs.AuditLog]: if isinstance(before, undefined.Undefined): @@ -1525,7 +1525,7 @@ def fetch_audit_log( async def fetch_emoji( self, - guild: typing.Union[guilds.Guild, bases.UniqueObjectT], + guild: typing.Union[guilds.Guild, bases.UniqueObject], # This is an emoji ID, which is the URL-safe emoji name, not the snowflake alone. emoji: typing.Union[emojis.KnownCustomEmoji, str], ) -> emojis.KnownCustomEmoji: @@ -1536,25 +1536,25 @@ async def fetch_emoji( return self._app.entity_factory.deserialize_known_custom_emoji(response) async def fetch_guild_emojis( - self, guild: typing.Union[guilds.Guild, bases.UniqueObjectT], / + self, guild: typing.Union[guilds.Guild, bases.UniqueObject], / ) -> typing.Set[emojis.KnownCustomEmoji]: route = routes.GET_GUILD_EMOJIS.compile(guild=guild) response = await self._request(route) - return binding.cast_json_array(response, self._app.entity_factory.deserialize_known_custom_emoji, set) + return data_binding.cast_json_array(response, self._app.entity_factory.deserialize_known_custom_emoji, set) async def create_emoji( self, - guild: typing.Union[guilds.Guild, bases.UniqueObjectT], + guild: typing.Union[guilds.Guild, bases.UniqueObject], name: str, image: files.BaseStream, *, roles: typing.Union[ - undefined.Undefined, typing.Collection[typing.Union[guilds.Role, bases.UniqueObjectT]] + undefined.Undefined, typing.Collection[typing.Union[guilds.Role, bases.UniqueObject]] ] = undefined.Undefined(), reason: typing.Union[undefined.Undefined, str] = undefined.Undefined(), ) -> emojis.KnownCustomEmoji: route = routes.POST_GUILD_EMOJIS.compile(guild=guild) - body = binding.JSONObjectBuilder() + body = data_binding.JSONObjectBuilder() body.put("name", name) if not isinstance(image, undefined.Undefined): body.put("image", await image.fetch_data_uri()) @@ -1567,20 +1567,20 @@ async def create_emoji( async def edit_emoji( self, - guild: typing.Union[guilds.Guild, bases.UniqueObjectT], + guild: typing.Union[guilds.Guild, bases.UniqueObject], # This is an emoji ID, which is the URL-safe emoji name, not the snowflake alone. emoji: typing.Union[emojis.KnownCustomEmoji, str], *, name: typing.Union[undefined.Undefined, str] = undefined.Undefined(), roles: typing.Union[ - undefined.Undefined, typing.Collection[typing.Union[guilds.Role, bases.UniqueObjectT]] + undefined.Undefined, typing.Collection[typing.Union[guilds.Role, bases.UniqueObject]] ] = undefined.Undefined(), reason: typing.Union[undefined.Undefined, str] = undefined.Undefined(), ) -> emojis.KnownCustomEmoji: route = routes.PATCH_GUILD_EMOJI.compile( guild=guild, emoji=emoji.url_name if isinstance(emoji, emojis.Emoji) else emoji, ) - body = binding.JSONObjectBuilder() + body = data_binding.JSONObjectBuilder() body.put("name", name) body.put_snowflake_array("roles", roles) @@ -1590,7 +1590,7 @@ async def edit_emoji( async def delete_emoji( self, - guild: typing.Union[guilds.Guild, bases.UniqueObjectT], + guild: typing.Union[guilds.Guild, bases.UniqueObject], # This is an emoji ID, which is the URL-safe emoji name, not the snowflake alone. emoji: typing.Union[emojis.KnownCustomEmoji, str], # Reason is not currently supported for some reason. See @@ -1603,13 +1603,13 @@ async def delete_emoji( def create_guild(self, name: str, /) -> rest_utils.GuildBuilder: return rest_utils.GuildBuilder(app=self._app, name=name, request_call=self._request) - async def fetch_guild(self, guild: typing.Union[guilds.Guild, bases.UniqueObjectT], /) -> guilds.Guild: + async def fetch_guild(self, guild: typing.Union[guilds.Guild, bases.UniqueObject], /) -> guilds.Guild: route = routes.GET_GUILD.compile(guild=guild) response = await self._request(route) return self._app.entity_factory.deserialize_guild(response) async def fetch_guild_preview( - self, guild: typing.Union[guilds.PartialGuild, bases.UniqueObjectT], / + self, guild: typing.Union[guilds.PartialGuild, bases.UniqueObject], / ) -> guilds.GuildPreview: route = routes.GET_GUILD_PREVIEW.compile(guild=guild) response = await self._request(route) @@ -1617,7 +1617,7 @@ async def fetch_guild_preview( async def edit_guild( self, - guild: typing.Union[guilds.Guild, bases.UniqueObjectT], + guild: typing.Union[guilds.Guild, bases.UniqueObject], /, *, name: typing.Union[undefined.Undefined, str] = undefined.Undefined(), @@ -1630,11 +1630,11 @@ async def edit_guild( undefined.Undefined, guilds.GuildExplicitContentFilterLevel ] = undefined.Undefined(), afk_channel: typing.Union[ - undefined.Undefined, channels.GuildVoiceChannel, bases.UniqueObjectT + undefined.Undefined, channels.GuildVoiceChannel, bases.UniqueObject ] = undefined.Undefined(), afk_timeout: typing.Union[undefined.Undefined, date.TimeSpan] = undefined.Undefined(), icon: typing.Union[undefined.Undefined, files.BaseStream] = undefined.Undefined(), - owner: typing.Union[undefined.Undefined, users.User, bases.UniqueObjectT] = undefined.Undefined(), + owner: typing.Union[undefined.Undefined, users.User, bases.UniqueObject] = undefined.Undefined(), splash: typing.Union[undefined.Undefined, files.BaseStream] = undefined.Undefined(), banner: typing.Union[undefined.Undefined, files.BaseStream] = undefined.Undefined(), system_channel: typing.Union[undefined.Undefined, channels.GuildTextChannel] = undefined.Undefined(), @@ -1644,7 +1644,7 @@ async def edit_guild( reason: typing.Union[undefined.Undefined, str] = undefined.Undefined(), ) -> guilds.Guild: route = routes.PATCH_GUILD.compile(guild=guild) - body = binding.JSONObjectBuilder() + body = data_binding.JSONObjectBuilder() body.put("name", name) body.put("region", region, str) body.put("verification", verification_level) @@ -1671,22 +1671,22 @@ async def edit_guild( return self._app.entity_factory.deserialize_guild(response) - async def delete_guild(self, guild: typing.Union[guilds.Guild, bases.UniqueObjectT]) -> None: + async def delete_guild(self, guild: typing.Union[guilds.Guild, bases.UniqueObject]) -> None: route = routes.DELETE_GUILD.compile(guild=guild) await self._request(route) async def fetch_guild_channels( - self, guild: typing.Union[guilds.Guild, bases.UniqueObjectT] + self, guild: typing.Union[guilds.Guild, bases.UniqueObject] ) -> typing.Sequence[channels.GuildChannel]: route = routes.GET_GUILD_CHANNELS.compile(guild=guild) response = await self._request(route) - channel_sequence = binding.cast_json_array(response, self._app.entity_factory.deserialize_channel) + channel_sequence = data_binding.cast_json_array(response, self._app.entity_factory.deserialize_channel) # Will always be guild channels unless Discord messes up severely on something! return typing.cast(typing.Sequence[channels.GuildChannel], channel_sequence) async def create_guild_text_channel( self, - guild: typing.Union[guilds.Guild, bases.UniqueObjectT], + guild: typing.Union[guilds.Guild, bases.UniqueObject], name: str, *, position: typing.Union[int, undefined.Undefined] = undefined.Undefined(), @@ -1696,9 +1696,7 @@ async def create_guild_text_channel( permission_overwrites: typing.Union[ typing.Sequence[channels.PermissionOverwrite], undefined.Undefined ] = undefined.Undefined(), - category: typing.Union[ - channels.GuildCategory, bases.UniqueObjectT, undefined.Undefined - ] = undefined.Undefined(), + category: typing.Union[channels.GuildCategory, bases.UniqueObject, undefined.Undefined] = undefined.Undefined(), reason: typing.Union[str, undefined.Undefined] = undefined.Undefined(), ) -> channels.GuildTextChannel: channel = await self._create_guild_channel( @@ -1717,7 +1715,7 @@ async def create_guild_text_channel( async def create_guild_news_channel( self, - guild: typing.Union[guilds.Guild, bases.UniqueObjectT], + guild: typing.Union[guilds.Guild, bases.UniqueObject], name: str, *, position: typing.Union[int, undefined.Undefined] = undefined.Undefined(), @@ -1727,9 +1725,7 @@ async def create_guild_news_channel( permission_overwrites: typing.Union[ typing.Sequence[channels.PermissionOverwrite], undefined.Undefined ] = undefined.Undefined(), - category: typing.Union[ - channels.GuildCategory, bases.UniqueObjectT, undefined.Undefined - ] = undefined.Undefined(), + category: typing.Union[channels.GuildCategory, bases.UniqueObject, undefined.Undefined] = undefined.Undefined(), reason: typing.Union[str, undefined.Undefined] = undefined.Undefined(), ) -> channels.GuildNewsChannel: channel = await self._create_guild_channel( @@ -1748,7 +1744,7 @@ async def create_guild_news_channel( async def create_guild_voice_channel( self, - guild: typing.Union[guilds.Guild, bases.UniqueObjectT], + guild: typing.Union[guilds.Guild, bases.UniqueObject], name: str, *, position: typing.Union[int, undefined.Undefined] = undefined.Undefined(), @@ -1758,9 +1754,7 @@ async def create_guild_voice_channel( permission_overwrites: typing.Union[ typing.Sequence[channels.PermissionOverwrite], undefined.Undefined ] = undefined.Undefined(), - category: typing.Union[ - channels.GuildCategory, bases.UniqueObjectT, undefined.Undefined - ] = undefined.Undefined(), + category: typing.Union[channels.GuildCategory, bases.UniqueObject, undefined.Undefined] = undefined.Undefined(), reason: typing.Union[str, undefined.Undefined] = undefined.Undefined(), ) -> channels.GuildVoiceChannel: channel = await self._create_guild_channel( @@ -1779,7 +1773,7 @@ async def create_guild_voice_channel( async def create_guild_category( self, - guild: typing.Union[guilds.Guild, bases.UniqueObjectT], + guild: typing.Union[guilds.Guild, bases.UniqueObject], name: str, *, position: typing.Union[int, undefined.Undefined] = undefined.Undefined(), @@ -1802,7 +1796,7 @@ async def create_guild_category( async def _create_guild_channel( self, - guild: typing.Union[guilds.Guild, bases.UniqueObjectT], + guild: typing.Union[guilds.Guild, bases.UniqueObject], name: str, type_: channels.ChannelType, *, @@ -1815,13 +1809,11 @@ async def _create_guild_channel( permission_overwrites: typing.Union[ typing.Sequence[channels.PermissionOverwrite], undefined.Undefined ] = undefined.Undefined(), - category: typing.Union[ - channels.GuildCategory, bases.UniqueObjectT, undefined.Undefined - ] = undefined.Undefined(), + category: typing.Union[channels.GuildCategory, bases.UniqueObject, undefined.Undefined] = undefined.Undefined(), reason: typing.Union[str, undefined.Undefined] = undefined.Undefined(), ) -> channels.GuildChannel: route = routes.POST_GUILD_CHANNELS.compile(guild=guild) - body = binding.JSONObjectBuilder() + body = data_binding.JSONObjectBuilder() body.put("type", type_) body.put("name", name) body.put("position", position) @@ -1841,45 +1833,43 @@ async def _create_guild_channel( async def reposition_channels( self, - guild: typing.Union[guilds.Guild, bases.UniqueObjectT], - positions: typing.Mapping[int, typing.Union[channels.GuildChannel, bases.UniqueObjectT]], + guild: typing.Union[guilds.Guild, bases.UniqueObject], + positions: typing.Mapping[int, typing.Union[channels.GuildChannel, bases.UniqueObject]], ) -> None: route = routes.POST_GUILD_CHANNELS.compile(guild=guild) body = [{"id": str(int(channel)), "position": pos} for pos, channel in positions.items()] await self._request(route, body=body) async def fetch_member( - self, - guild: typing.Union[guilds.Guild, bases.UniqueObjectT], - user: typing.Union[users.User, bases.UniqueObjectT], + self, guild: typing.Union[guilds.Guild, bases.UniqueObject], user: typing.Union[users.User, bases.UniqueObject], ) -> guilds.GuildMember: route = routes.GET_GUILD_MEMBER.compile(guild=guild, user=user) response = await self._request(route) return self._app.entity_factory.deserialize_guild_member(response) def fetch_members( - self, guild: typing.Union[guilds.Guild, bases.UniqueObjectT], + self, guild: typing.Union[guilds.Guild, bases.UniqueObject], ) -> iterators.LazyIterator[guilds.GuildMember]: return iterators.MemberIterator(self._app, self._request, guild) async def edit_member( self, - guild: typing.Union[guilds.Guild, bases.UniqueObjectT], - user: typing.Union[users.User, bases.UniqueObjectT], + guild: typing.Union[guilds.Guild, bases.UniqueObject], + user: typing.Union[users.User, bases.UniqueObject], *, nick: typing.Union[undefined.Undefined, str] = undefined.Undefined(), roles: typing.Union[ - undefined.Undefined, typing.Collection[typing.Union[guilds.Role, bases.UniqueObjectT]] + undefined.Undefined, typing.Collection[typing.Union[guilds.Role, bases.UniqueObject]] ] = undefined.Undefined(), mute: typing.Union[undefined.Undefined, bool] = undefined.Undefined(), deaf: typing.Union[undefined.Undefined, bool] = undefined.Undefined(), voice_channel: typing.Union[ - undefined.Undefined, channels.GuildVoiceChannel, bases.UniqueObjectT, None + undefined.Undefined, channels.GuildVoiceChannel, bases.UniqueObject, None ] = undefined.Undefined(), reason: typing.Union[str, undefined.Undefined] = undefined.Undefined(), ) -> None: route = routes.PATCH_GUILD_MEMBER.compile(guild=guild, user=user) - body = binding.JSONObjectBuilder() + body = data_binding.JSONObjectBuilder() body.put("nick", nick) body.put("mute", mute) body.put("deaf", deaf) @@ -1894,21 +1884,21 @@ async def edit_member( async def edit_my_nick( self, - guild: typing.Union[guilds.Guild, bases.UniqueObjectT], + guild: typing.Union[guilds.Guild, bases.UniqueObject], nick: typing.Optional[str], *, reason: typing.Union[undefined.Undefined, str] = undefined.Undefined(), ) -> None: route = routes.PATCH_MY_GUILD_NICKNAME.compile(guild=guild) - body = binding.JSONObjectBuilder() + body = data_binding.JSONObjectBuilder() body.put("nick", nick) await self._request(route, body=body, reason=reason) async def add_role_to_member( self, - guild: typing.Union[guilds.Guild, bases.UniqueObjectT], - user: typing.Union[users.User, bases.UniqueObjectT], - role: typing.Union[guilds.Role, bases.UniqueObjectT], + guild: typing.Union[guilds.Guild, bases.UniqueObject], + user: typing.Union[users.User, bases.UniqueObject], + role: typing.Union[guilds.Role, bases.UniqueObject], *, reason: typing.Union[undefined.Undefined, str] = undefined.Undefined(), ) -> None: @@ -1917,9 +1907,9 @@ async def add_role_to_member( async def remove_role_from_member( self, - guild: typing.Union[guilds.Guild, bases.UniqueObjectT], - user: typing.Union[users.User, bases.UniqueObjectT], - role: typing.Union[guilds.Role, bases.UniqueObjectT], + guild: typing.Union[guilds.Guild, bases.UniqueObject], + user: typing.Union[users.User, bases.UniqueObject], + role: typing.Union[guilds.Role, bases.UniqueObject], *, reason: typing.Union[undefined.Undefined, str] = undefined.Undefined(), ) -> None: @@ -1928,8 +1918,8 @@ async def remove_role_from_member( async def kick_member( self, - guild: typing.Union[guilds.Guild, bases.UniqueObjectT], - user: typing.Union[users.User, bases.UniqueObjectT], + guild: typing.Union[guilds.Guild, bases.UniqueObject], + user: typing.Union[users.User, bases.UniqueObject], *, reason: typing.Union[undefined.Undefined, str] = undefined.Undefined(), ) -> None: @@ -1938,8 +1928,8 @@ async def kick_member( async def ban_user( self, - guild: typing.Union[guilds.Guild, bases.UniqueObjectT], - user: typing.Union[users.User, bases.UniqueObjectT], + guild: typing.Union[guilds.Guild, bases.UniqueObject], + user: typing.Union[users.User, bases.UniqueObject], *, reason: typing.Union[undefined.Undefined, str] = undefined.Undefined(), ) -> None: @@ -1948,8 +1938,8 @@ async def ban_user( async def unban_user( self, - guild: typing.Union[guilds.Guild, bases.UniqueObjectT], - user: typing.Union[users.User, bases.UniqueObjectT], + guild: typing.Union[guilds.Guild, bases.UniqueObject], + user: typing.Union[users.User, bases.UniqueObject], *, reason: typing.Union[undefined.Undefined, str] = undefined.Undefined(), ) -> None: @@ -1957,31 +1947,29 @@ async def unban_user( await self._request(route, reason=reason) async def fetch_ban( - self, - guild: typing.Union[guilds.Guild, bases.UniqueObjectT], - user: typing.Union[users.User, bases.UniqueObjectT], + self, guild: typing.Union[guilds.Guild, bases.UniqueObject], user: typing.Union[users.User, bases.UniqueObject], ) -> guilds.GuildMemberBan: route = routes.GET_GUILD_BAN.compile(guild=guild, user=user) response = await self._request(route) return self._app.entity_factory.deserialize_guild_member_ban(response) async def fetch_bans( - self, guild: typing.Union[guilds.Guild, bases.UniqueObjectT], / + self, guild: typing.Union[guilds.Guild, bases.UniqueObject], / ) -> typing.Sequence[guilds.GuildMemberBan]: route = routes.GET_GUILD_BANS.compile(guild=guild) response = await self._request(route) - return binding.cast_json_array(response, self._app.entity_factory.deserialize_guild_member_ban) + return data_binding.cast_json_array(response, self._app.entity_factory.deserialize_guild_member_ban) async def fetch_roles( - self, guild: typing.Union[guilds.Guild, bases.UniqueObjectT], / + self, guild: typing.Union[guilds.Guild, bases.UniqueObject], / ) -> typing.Sequence[guilds.Role]: route = routes.GET_GUILD_ROLES.compile(guild=guild) response = await self._request(route) - return binding.cast_json_array(response, self._app.entity_factory.deserialize_role) + return data_binding.cast_json_array(response, self._app.entity_factory.deserialize_role) async def create_role( self, - guild: typing.Union[guilds.Guild, bases.UniqueObjectT], + guild: typing.Union[guilds.Guild, bases.UniqueObject], /, *, name: typing.Union[undefined.Undefined, str] = undefined.Undefined(), @@ -1996,7 +1984,7 @@ async def create_role( raise TypeError("Can not specify 'color' and 'colour' together.") route = routes.POST_GUILD_ROLES.compile(guild=guild) - body = binding.JSONObjectBuilder() + body = data_binding.JSONObjectBuilder() body.put("name", name) body.put("permissions", permissions) body.put("color", color) @@ -2009,8 +1997,8 @@ async def create_role( async def reposition_roles( self, - guild: typing.Union[guilds.Guild, bases.UniqueObjectT], - positions: typing.Mapping[int, typing.Union[guilds.Role, bases.UniqueObjectT]], + guild: typing.Union[guilds.Guild, bases.UniqueObject], + positions: typing.Mapping[int, typing.Union[guilds.Role, bases.UniqueObject]], ) -> None: route = routes.POST_GUILD_ROLES.compile(guild=guild) body = [{"id": str(int(role)), "position": pos} for pos, role in positions.items()] @@ -2018,8 +2006,8 @@ async def reposition_roles( async def edit_role( self, - guild: typing.Union[guilds.Guild, bases.UniqueObjectT], - role: typing.Union[guilds.Role, bases.UniqueObjectT], + guild: typing.Union[guilds.Guild, bases.UniqueObject], + role: typing.Union[guilds.Role, bases.UniqueObject], *, name: typing.Union[undefined.Undefined, str] = undefined.Undefined(), permissions: typing.Union[undefined.Undefined, permissions_.Permission] = undefined.Undefined(), @@ -2034,7 +2022,7 @@ async def edit_role( route = routes.PATCH_GUILD_ROLE.compile(guild=guild, role=role) - body = binding.JSONObjectBuilder() + body = data_binding.JSONObjectBuilder() body.put("name", name) body.put("permissions", permissions) body.put("color", color) @@ -2047,60 +2035,60 @@ async def edit_role( async def delete_role( self, - guild: typing.Union[guilds.Guild, bases.UniqueObjectT], - role: typing.Union[guilds.Role, bases.UniqueObjectT], + guild: typing.Union[guilds.Guild, bases.UniqueObject], + role: typing.Union[guilds.Role, bases.UniqueObject], ) -> None: route = routes.DELETE_GUILD_ROLE.compile(guild=guild, role=role) await self._request(route) async def estimate_guild_prune_count( - self, guild: typing.Union[guilds.Guild, bases.UniqueObjectT], days: int, + self, guild: typing.Union[guilds.Guild, bases.UniqueObject], days: int, ) -> int: route = routes.GET_GUILD_PRUNE.compile(guild=guild) - query = binding.StringMapBuilder() + query = data_binding.StringMapBuilder() query.put("days", days) response = await self._request(route, query=query) return int(response["pruned"]) async def begin_guild_prune( self, - guild: typing.Union[guilds.Guild, bases.UniqueObjectT], + guild: typing.Union[guilds.Guild, bases.UniqueObject], days: int, *, reason: typing.Union[undefined.Undefined, str], ) -> int: route = routes.POST_GUILD_PRUNE.compile(guild=guild) - query = binding.StringMapBuilder() + query = data_binding.StringMapBuilder() query.put("compute_prune_count", True) query.put("days", days) response = await self._request(route, query=query, reason=reason) return int(response["pruned"]) async def fetch_guild_voice_regions( - self, guild: typing.Union[guilds.Guild, bases.UniqueObjectT], / + self, guild: typing.Union[guilds.Guild, bases.UniqueObject], / ) -> typing.Sequence[voices.VoiceRegion]: route = routes.GET_GUILD_VOICE_REGIONS.compile(guild=guild) response = await self._request(route) - return binding.cast_json_array(response, self._app.entity_factory.deserialize_voice_region) + return data_binding.cast_json_array(response, self._app.entity_factory.deserialize_voice_region) async def fetch_guild_invites( - self, guild: typing.Union[guilds.Guild, bases.UniqueObjectT], / + self, guild: typing.Union[guilds.Guild, bases.UniqueObject], / ) -> typing.Sequence[invites.InviteWithMetadata]: route = routes.GET_GUILD_INVITES.compile(guild=guild) response = await self._request(route) - return binding.cast_json_array(response, self._app.entity_factory.deserialize_invite_with_metadata) + return data_binding.cast_json_array(response, self._app.entity_factory.deserialize_invite_with_metadata) async def fetch_integrations( - self, guild: typing.Union[guilds.Guild, bases.UniqueObjectT], / + self, guild: typing.Union[guilds.Guild, bases.UniqueObject], / ) -> typing.Sequence[guilds.Integration]: route = routes.GET_GUILD_INTEGRATIONS.compile(guild=guild) response = await self._request(route) - return binding.cast_json_array(response, self._app.entity_factory.deserialize_integration) + return data_binding.cast_json_array(response, self._app.entity_factory.deserialize_integration) async def edit_integration( self, - guild: typing.Union[guilds.Guild, bases.UniqueObjectT], - integration: typing.Union[guilds.Integration, bases.UniqueObjectT], + guild: typing.Union[guilds.Guild, bases.UniqueObject], + integration: typing.Union[guilds.Integration, bases.UniqueObject], *, expire_behaviour: typing.Union[undefined.Undefined, guilds.IntegrationExpireBehaviour] = undefined.Undefined(), expire_grace_period: typing.Union[undefined.Undefined, date.TimeSpan] = undefined.Undefined(), @@ -2108,7 +2096,7 @@ async def edit_integration( reason: typing.Union[undefined.Undefined, str] = undefined.Undefined(), ) -> None: route = routes.PATCH_GUILD_INTEGRATION.compile(guild=guild, integration=integration) - body = binding.JSONObjectBuilder() + body = data_binding.JSONObjectBuilder() body.put("expire_behaviour", expire_behaviour) body.put("expire_grace_period", expire_grace_period, date.timespan_to_int) # Inconsistent naming in the API itself, so I have changed the name. @@ -2117,8 +2105,8 @@ async def edit_integration( async def delete_integration( self, - guild: typing.Union[guilds.Guild, bases.UniqueObjectT], - integration: typing.Union[guilds.Integration, bases.UniqueObjectT], + guild: typing.Union[guilds.Guild, bases.UniqueObject], + integration: typing.Union[guilds.Integration, bases.UniqueObject], *, reason: typing.Union[undefined.Undefined, str] = undefined.Undefined(), ) -> None: @@ -2127,31 +2115,31 @@ async def delete_integration( async def sync_integration( self, - guild: typing.Union[guilds.Guild, bases.UniqueObjectT], - integration: typing.Union[guilds.Integration, bases.UniqueObjectT], + guild: typing.Union[guilds.Guild, bases.UniqueObject], + integration: typing.Union[guilds.Integration, bases.UniqueObject], ) -> None: route = routes.POST_GUILD_INTEGRATION_SYNC.compile(guild=guild, integration=integration) await self._request(route) - async def fetch_widget(self, guild: typing.Union[guilds.Guild, bases.UniqueObjectT], /) -> guilds.GuildWidget: + async def fetch_widget(self, guild: typing.Union[guilds.Guild, bases.UniqueObject], /) -> guilds.GuildWidget: route = routes.GET_GUILD_WIDGET.compile(guild=guild) response = await self._request(route) return self._app.entity_factory.deserialize_guild_widget(response) async def edit_widget( self, - guild: typing.Union[guilds.Guild, bases.UniqueObjectT], + guild: typing.Union[guilds.Guild, bases.UniqueObject], /, *, channel: typing.Union[ - undefined.Undefined, channels.GuildChannel, bases.UniqueObjectT, None + undefined.Undefined, channels.GuildChannel, bases.UniqueObject, None ] = undefined.Undefined(), enabled: typing.Union[undefined.Undefined, bool] = undefined.Undefined(), reason: typing.Union[undefined.Undefined, str] = undefined.Undefined(), ) -> guilds.GuildWidget: route = routes.PATCH_GUILD_WIDGET.compile(guild=guild) - body = binding.JSONObjectBuilder() + body = data_binding.JSONObjectBuilder() body.put("enabled", enabled) if channel is None: body.put("channel", None) @@ -2161,7 +2149,7 @@ async def edit_widget( response = await self._request(route, body=body, reason=reason) return self._app.entity_factory.deserialize_guild_widget(response) - async def fetch_vanity_url(self, guild: typing.Union[guilds.Guild, bases.UniqueObjectT], /) -> invites.VanityURL: + async def fetch_vanity_url(self, guild: typing.Union[guilds.Guild, bases.UniqueObject], /) -> invites.VanityURL: route = routes.GET_GUILD_VANITY_URL.compile(guild=guild) response = await self._request(route) return self._app.entity_factory.deserialize_vanity_url(response) diff --git a/hikari/net/rest_utils.py b/hikari/net/rest_utils.py index 3de7e68850..94a7c62314 100644 --- a/hikari/net/rest_utils.py +++ b/hikari/net/rest_utils.py @@ -40,7 +40,7 @@ from hikari.models import guilds from hikari.models import permissions as permissions_ from hikari.net import routes -from hikari.utilities import binding +from hikari.utilities import data_binding from hikari.utilities import date from hikari.utilities import snowflake as snowflake_ from hikari.utilities import undefined @@ -62,7 +62,7 @@ class TypingIndicator: def __init__( self, channel: typing.Union[channels.TextChannel, bases.UniqueObject], - request_call: typing.Callable[..., typing.Coroutine[None, typing.Any, binding.JSONObject]], + request_call: typing.Callable[..., typing.Coroutine[None, typing.Any, data_binding.JSONObject]], ) -> None: self._channel = channel self._request_call = request_call @@ -90,11 +90,11 @@ async def _keep_typing(self) -> None: @attr.s(auto_attribs=True, kw_only=True, slots=True) class GuildBuilder: _app: app_.IRESTApp - _channels: typing.MutableSequence[binding.JSONObject] = attr.ib(factory=list) + _channels: typing.MutableSequence[data_binding.JSONObject] = attr.ib(factory=list) _counter: int = 0 _name: typing.Union[undefined.Undefined, str] - _request_call: typing.Callable[..., typing.Coroutine[None, typing.Any, binding.JSONObject]] - _roles: typing.MutableSequence[binding.JSONObject] = attr.ib(factory=list) + _request_call: typing.Callable[..., typing.Coroutine[None, typing.Any, data_binding.JSONObject]] + _roles: typing.MutableSequence[data_binding.JSONObject] = attr.ib(factory=list) default_message_notifications: typing.Union[ undefined.Undefined, guilds.GuildMessageNotificationsLevel ] = undefined.Undefined() @@ -115,7 +115,7 @@ def __await__(self) -> typing.Generator[guilds.Guild, None, typing.Any]: async def create(self) -> guilds.Guild: route = routes.POST_GUILDS.compile() - payload = binding.JSONObjectBuilder() + payload = data_binding.JSONObjectBuilder() payload.put("name", self.name) payload.put_array("roles", self._roles) payload.put_array("channels", self._channels) @@ -149,7 +149,7 @@ def add_role( raise TypeError("Cannot specify 'color' and 'colour' together.") snowflake = self._new_snowflake() - payload = binding.JSONObjectBuilder() + payload = data_binding.JSONObjectBuilder() payload.put_snowflake("id", snowflake) payload.put("name", name) payload.put("color", color) @@ -173,7 +173,7 @@ def add_category( nsfw: typing.Union[undefined.Undefined, bool] = undefined.Undefined(), ) -> snowflake_.Snowflake: snowflake = self._new_snowflake() - payload = binding.JSONObjectBuilder() + payload = data_binding.JSONObjectBuilder() payload.put_snowflake("id", snowflake) payload.put("name", name) payload.put("type", channels.ChannelType.GUILD_CATEGORY) @@ -202,7 +202,7 @@ def add_text_channel( nsfw: typing.Union[undefined.Undefined, bool] = undefined.Undefined(), ) -> snowflake_.Snowflake: snowflake = self._new_snowflake() - payload = binding.JSONObjectBuilder() + payload = data_binding.JSONObjectBuilder() payload.put_snowflake("id", snowflake) payload.put("name", name) payload.put("type", channels.ChannelType.GUILD_TEXT) @@ -234,7 +234,7 @@ def add_voice_channel( user_limit: typing.Union[undefined.Undefined, int] = undefined.Undefined(), ) -> snowflake_.Snowflake: snowflake = self._new_snowflake() - payload = binding.JSONObjectBuilder() + payload = data_binding.JSONObjectBuilder() payload.put_snowflake("id", snowflake) payload.put("name", name) payload.put("type", channels.ChannelType.GUILD_VOICE) diff --git a/hikari/net/routes.py b/hikari/net/routes.py index be76588fa4..de1afdd530 100644 --- a/hikari/net/routes.py +++ b/hikari/net/routes.py @@ -25,7 +25,7 @@ import re import typing -from hikari.utilities import binding +from hikari.utilities import data_binding DEFAULT_MAJOR_PARAMS: typing.Final[typing.Set[str]] = {"channel", "guild", "webhook"} HASH_SEPARATOR: typing.Final[str] = ";" @@ -188,7 +188,7 @@ def compile(self, **kwargs: typing.Any) -> CompiledRoute: CompiledRoute The compiled _route. """ - data = binding.StringMapBuilder.from_dict(kwargs) + data = data_binding.StringMapBuilder.from_dict(kwargs) return CompiledRoute( self, self.path_template.format_map(data), data[self.major_param] if self.major_param is not None else "-", diff --git a/hikari/net/voice_gateway.py b/hikari/net/voice_gateway.py index 5c93484c61..97fcc5e25b 100644 --- a/hikari/net/voice_gateway.py +++ b/hikari/net/voice_gateway.py @@ -35,7 +35,7 @@ from hikari.models import bases from hikari.net import http_client from hikari.net import ratelimits -from hikari.utilities import binding +from hikari.utilities import data_binding from hikari.utilities import klass if typing.TYPE_CHECKING: @@ -343,13 +343,13 @@ async def _handshake(self): } ) - async def _receive_json_payload(self) -> binding.JSONObject: + async def _receive_json_payload(self) -> data_binding.JSONObject: message = await self._ws.receive() self.last_message_received = self._now() if message.type == aiohttp.WSMsgType.TEXT: self._log_debug_payload(message.data, "received text payload") - return binding.load_json(message.data) + return data_binding.load_json(message.data) elif message.type == aiohttp.WSMsgType.CLOSE: close_code = self._ws.close_code @@ -377,8 +377,8 @@ async def _receive_json_payload(self) -> binding.JSONObject: self.logger.debug("encountered unexpected error", exc_info=ex) raise errors.GatewayError("Unexpected websocket exception from gateway") from ex - async def _send_json(self, payload: binding.JSONObject) -> None: - message = binding.dump_json(payload) + async def _send_json(self, payload: data_binding.JSONObject) -> None: + message = data_binding.dump_json(payload) self._log_debug_payload(message, "sending json payload") await self._ws.send_str(message) diff --git a/hikari/utilities/binding.py b/hikari/utilities/data_binding.py similarity index 95% rename from hikari/utilities/binding.py rename to hikari/utilities/data_binding.py index b4c574e9fc..244eaa27e3 100644 --- a/hikari/utilities/binding.py +++ b/hikari/utilities/data_binding.py @@ -69,12 +69,13 @@ JSONNull = None """A null JSON value.""" -# We cant include JSONArray and JSONObject in the definition as MyPY does not support -# recursive type definitions, sadly. -JSONObject = typing.Dict[JSONString, typing.Union[JSONString, JSONNumber, JSONBoolean, JSONNull, list, dict]] +# MyPy does not support recursive types yet. This has been ongoing for a long time, unfortunately. +# See https://github.com/python/typing/issues/182 + +JSONObject = typing.Dict[JSONString, typing.Any] """A JSON object representation as a dict.""" -JSONArray = typing.List[typing.Union[JSONString, JSONNumber, JSONBoolean, JSONNull, dict, list]] +JSONArray = typing.List[typing.Any] """A JSON array representation as a list.""" JSONAny = typing.Union[JSONString, JSONNumber, JSONBoolean, JSONNull, JSONArray, JSONObject] diff --git a/hikari/utilities/klass.py b/hikari/utilities/klass.py index 9ae38dcaaf..884f6029f7 100644 --- a/hikari/utilities/klass.py +++ b/hikari/utilities/klass.py @@ -105,6 +105,3 @@ class Singleton(metaclass=SingletonMeta): """ __slots__ = () - - def __init__(self): - pass From 5c5ce2685aefe0bf040780fd859b21f7875a307a Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Sat, 30 May 2020 12:32:16 +0100 Subject: [PATCH 413/922] Fixed a bunch of inconsistent imports. --- hikari/events/channel.py | 2 +- hikari/events/guild.py | 4 ++-- hikari/events/message.py | 4 ++-- hikari/events/other.py | 4 ++-- hikari/events/voice.py | 2 +- hikari/models/applications.py | 10 +++++----- hikari/models/audit_logs.py | 14 +++++++------- hikari/models/bases.py | 9 +++++++-- hikari/models/channels.py | 8 ++++---- hikari/models/colours.py | 2 +- hikari/models/embeds.py | 8 ++++---- hikari/models/emojis.py | 8 ++++---- hikari/models/files.py | 10 +++++++--- hikari/models/guilds.py | 12 ++++++------ hikari/models/invites.py | 10 +++++----- hikari/models/messages.py | 16 ++++++++-------- hikari/models/users.py | 4 ++-- hikari/models/voices.py | 4 ++-- hikari/models/webhooks.py | 16 ++++++++-------- 19 files changed, 78 insertions(+), 69 deletions(-) diff --git a/hikari/events/channel.py b/hikari/events/channel.py index 74d0c99163..3dce8f00b5 100644 --- a/hikari/events/channel.py +++ b/hikari/events/channel.py @@ -38,13 +38,13 @@ import attr +from hikari.events import base as base_events from hikari.models import bases as base_models from hikari.models import channels from hikari.models import guilds from hikari.models import intents from hikari.models import invites from hikari.models import users -from . import base as base_events @base_events.requires_intents(intents.Intent.GUILDS) diff --git a/hikari/events/guild.py b/hikari/events/guild.py index d7096086e0..229408d40c 100644 --- a/hikari/events/guild.py +++ b/hikari/events/guild.py @@ -44,13 +44,13 @@ import attr +from hikari.events import base as base_events from hikari.models import bases as base_models from hikari.models import emojis as emojis_models from hikari.models import guilds from hikari.models import intents from hikari.models import users -from . import base as base_events -from ..utilities import undefined +from hikari.utilities import undefined if typing.TYPE_CHECKING: import datetime diff --git a/hikari/events/message.py b/hikari/events/message.py index 63029cc6a1..1546e9604e 100644 --- a/hikari/events/message.py +++ b/hikari/events/message.py @@ -35,6 +35,7 @@ import attr +from hikari.events import base as base_events from hikari.models import applications from hikari.models import bases as base_models from hikari.models import embeds as embed_models @@ -43,8 +44,7 @@ from hikari.models import intents from hikari.models import messages from hikari.models import users -from . import base as base_events -from ..utilities import undefined +from hikari.utilities import undefined if typing.TYPE_CHECKING: import datetime diff --git a/hikari/events/other.py b/hikari/events/other.py index 49fc7b381e..6ce2beb509 100644 --- a/hikari/events/other.py +++ b/hikari/events/other.py @@ -37,13 +37,13 @@ import attr +from hikari.events import base as base_events from hikari.models import bases as base_models from hikari.models import guilds from hikari.models import users -from . import base as base_events if typing.TYPE_CHECKING: - from ..net import gateway as gateway_client + from hikari.net import gateway as gateway_client # Synthetic event, is not deserialized, and is produced by the dispatcher. diff --git a/hikari/events/voice.py b/hikari/events/voice.py index 25dfd8d0d4..581775a360 100644 --- a/hikari/events/voice.py +++ b/hikari/events/voice.py @@ -24,10 +24,10 @@ import attr +from hikari.events import base as base_events from hikari.models import bases as base_models from hikari.models import intents from hikari.models import voices -from . import base as base_events @base_events.requires_intents(intents.Intent.GUILD_VOICE_STATES) diff --git a/hikari/models/applications.py b/hikari/models/applications.py index 1bdda2689c..5b78f15511 100644 --- a/hikari/models/applications.py +++ b/hikari/models/applications.py @@ -36,13 +36,13 @@ import attr -from . import bases -from . import guilds -from ..net import urls +from hikari.models import bases +from hikari.models import guilds +from hikari.net import urls if typing.TYPE_CHECKING: - from . import permissions as permissions_ - from . import users + from hikari.models import permissions as permissions_ + from hikari.models import users @enum.unique diff --git a/hikari/models/audit_logs.py b/hikari/models/audit_logs.py index c525b862ce..26ca16cb1a 100644 --- a/hikari/models/audit_logs.py +++ b/hikari/models/audit_logs.py @@ -43,15 +43,15 @@ import attr -from . import bases -from . import colors -from . import guilds -from . import permissions -from . import users as users_ -from . import webhooks as webhooks_ +from hikari.models import bases +from hikari.models import colors +from hikari.models import guilds +from hikari.models import permissions +from hikari.models import users as users_ +from hikari.models import webhooks as webhooks_ if typing.TYPE_CHECKING: - from . import channels + from hikari.models import channels class AuditLogChangeKey(str, enum.Enum): diff --git a/hikari/models/bases.py b/hikari/models/bases.py index 1324358182..546d13e7c3 100644 --- a/hikari/models/bases.py +++ b/hikari/models/bases.py @@ -37,7 +37,12 @@ @attr.s(eq=True, hash=False, init=False, kw_only=True, slots=False) class Entity(abc.ABC): - """The base for any entity used in this API.""" + """The base for an entity used in this API. + + An entity is a managed object that contains a binding to the owning + application instance. This enables it to perform API calls from + methods directly. + """ _app: typing.Optional[app_.IApp] = attr.ib(default=None, repr=False, eq=False, hash=False) """The client application that models may use for procedures.""" @@ -132,4 +137,4 @@ def __int__(self) -> int: UniqueObject = typing.Union[Unique, Snowflake, int, str] -"""A unique object.""" +"""A unique object type-hint.""" diff --git a/hikari/models/channels.py b/hikari/models/channels.py index a3f1d2b171..5e4b468183 100644 --- a/hikari/models/channels.py +++ b/hikari/models/channels.py @@ -41,14 +41,14 @@ import attr -from . import bases -from . import permissions -from ..net import urls +from hikari.models import bases +from hikari.models import permissions +from hikari.net import urls if typing.TYPE_CHECKING: import datetime - from . import users + from hikari.models import users @enum.unique diff --git a/hikari/models/colours.py b/hikari/models/colours.py index a39be2dce5..1a0bb15e36 100644 --- a/hikari/models/colours.py +++ b/hikari/models/colours.py @@ -22,4 +22,4 @@ __all__ = ["Colour"] -from .colors import Color as Colour +from hikari.models.colors import Color as Colour diff --git a/hikari/models/embeds.py b/hikari/models/embeds.py index 1cf951e6f8..d5a9ed6955 100644 --- a/hikari/models/embeds.py +++ b/hikari/models/embeds.py @@ -36,11 +36,11 @@ import attr -from . import colors -from . import files +from hikari.models import colors +from hikari.models import files if typing.TYPE_CHECKING: - from hikari.internal import more_typing + from hikari.utilities import data_binding _MAX_FOOTER_TEXT: typing.Final[int] = 2048 _MAX_AUTHOR_NAME: typing.Final[int] = 256 @@ -525,6 +525,6 @@ def _check_total_length(self) -> None: if total_size > _MAX_EMBED_SIZE: raise ValueError("Total characters in an embed can not exceed {_MAX_EMBED_SIZE}") - def serialize(self) -> more_typing.JSONObject: + def serialize(self) -> data_binding.JSONObject: self._check_total_length() return super().serialize() diff --git a/hikari/models/emojis.py b/hikari/models/emojis.py index 5a5be42fe3..f19e2e1276 100644 --- a/hikari/models/emojis.py +++ b/hikari/models/emojis.py @@ -28,12 +28,12 @@ import attr -from . import bases -from . import files -from ..net import urls +from hikari.models import bases +from hikari.models import files +from hikari.net import urls if typing.TYPE_CHECKING: - from . import users + from hikari.models import users @attr.s(eq=True, hash=True, init=False, kw_only=True, slots=True) diff --git a/hikari/models/files.py b/hikari/models/files.py index d14388616f..bf504dd1d8 100644 --- a/hikari/models/files.py +++ b/hikari/models/files.py @@ -108,7 +108,7 @@ async def __aiter__(self): @property @abc.abstractmethod def filename(self) -> str: - """Ffilename for the file object.""" + """Filename for the file object.""" def __repr__(self) -> str: return f"{type(self).__name__}(filename={self.filename!r})" @@ -116,7 +116,11 @@ def __repr__(self) -> str: async def fetch_data_uri(self) -> str: """Generate a data URI for the given resource. - This will only work for select image types that Discord supports. + This will only work for select image types that Discord supports, + currently. + + The type is resolved by reading the first 20 bytes of the resource + asynchronously. Returns ------- @@ -126,7 +130,7 @@ async def fetch_data_uri(self) -> str: Raises ------ TypeError - If the data format is not + If the data format is not supported. """ buff = await self.read(20) diff --git a/hikari/models/guilds.py b/hikari/models/guilds.py index 0edfcde4a8..53bb541bf7 100644 --- a/hikari/models/guilds.py +++ b/hikari/models/guilds.py @@ -59,18 +59,18 @@ import attr +from hikari.models import bases +from hikari.models import users from hikari.net import urls from hikari.utilities import undefined -from . import bases -from . import users if typing.TYPE_CHECKING: import datetime - from . import channels as channels_ - from . import colors - from . import emojis as emojis_ - from . import permissions as permissions_ + from hikari.models import channels as channels_ + from hikari.models import colors + from hikari.models import emojis as emojis_ + from hikari.models import permissions as permissions_ @enum.unique diff --git a/hikari/models/invites.py b/hikari/models/invites.py index cd3db67a4a..b9b3611ad1 100644 --- a/hikari/models/invites.py +++ b/hikari/models/invites.py @@ -27,15 +27,15 @@ import attr -from . import bases -from . import guilds -from ..net import urls +from hikari.models import bases +from hikari.models import guilds +from hikari.net import urls if typing.TYPE_CHECKING: import datetime - from . import channels - from . import users + from hikari.models import channels + from hikari.models import users @enum.unique diff --git a/hikari/models/messages.py b/hikari/models/messages.py index d5c2d202cc..75e03a32d8 100644 --- a/hikari/models/messages.py +++ b/hikari/models/messages.py @@ -36,18 +36,18 @@ import attr -from . import bases -from . import files as files_ +from hikari.models import bases +from hikari.models import files as files_ if typing.TYPE_CHECKING: import datetime - from . import applications - from . import channels - from . import embeds as embeds_ - from . import emojis as emojis_ - from . import guilds - from . import users + from hikari.models import applications + from hikari.models import channels + from hikari.models import embeds as embeds_ + from hikari.models import emojis as emojis_ + from hikari.models import guilds + from hikari.models import users @enum.unique diff --git a/hikari/models/users.py b/hikari/models/users.py index 3cf6400ae4..bfe4a36b86 100644 --- a/hikari/models/users.py +++ b/hikari/models/users.py @@ -27,8 +27,8 @@ import attr -from . import bases -from ..net import urls +from hikari.models import bases +from hikari.net import urls @enum.unique diff --git a/hikari/models/voices.py b/hikari/models/voices.py index 3e8f741fe4..7bd2798238 100644 --- a/hikari/models/voices.py +++ b/hikari/models/voices.py @@ -26,10 +26,10 @@ import attr -from . import bases +from hikari.models import bases if typing.TYPE_CHECKING: - from . import guilds + from hikari.models import guilds @attr.s(eq=True, hash=True, init=False, kw_only=True, slots=True) diff --git a/hikari/models/webhooks.py b/hikari/models/webhooks.py index 1f61cef12d..1fff05de89 100644 --- a/hikari/models/webhooks.py +++ b/hikari/models/webhooks.py @@ -27,16 +27,16 @@ import attr -from . import bases -from ..net import urls +from hikari.models import bases +from hikari.net import urls if typing.TYPE_CHECKING: - from . import channels as channels_ - from . import embeds as embeds_ - from . import files as files_ - from . import guilds as guilds_ - from . import messages as messages_ - from . import users as users_ + from hikari.models import channels as channels_ + from hikari.models import embeds as embeds_ + from hikari.models import files as files_ + from hikari.models import guilds as guilds_ + from hikari.models import messages as messages_ + from hikari.models import users as users_ @enum.unique From 94005ee092ca5de96174c782279a3bc1d66d82a0 Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Sat, 30 May 2020 12:33:14 +0100 Subject: [PATCH 414/922] Fixed type-hinting error in embeds.py --- hikari/models/embeds.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hikari/models/embeds.py b/hikari/models/embeds.py index d5a9ed6955..153b998f31 100644 --- a/hikari/models/embeds.py +++ b/hikari/models/embeds.py @@ -273,7 +273,7 @@ def _description_check(self, _, value): # pylint:disable=unused-argument author: typing.Optional[EmbedAuthor] = attr.ib(default=None,) """The author of the embed.""" - fields: typing.Sequence[EmbedField] = attr.ib(factory=list) + fields: typing.MutableSequence[EmbedField] = attr.ib(factory=list) """The fields of the embed.""" _assets_to_upload = attr.attrib(factory=list) From fafc0a801f30ed31c10085dc00fb446791dcf45d Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Sat, 30 May 2020 12:34:24 +0100 Subject: [PATCH 415/922] Fixed typo creating tuple in rest.py --- hikari/net/rest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hikari/net/rest.py b/hikari/net/rest.py index 9d2e81d254..3727943be1 100644 --- a/hikari/net/rest.py +++ b/hikari/net/rest.py @@ -709,7 +709,7 @@ async def create_invite( body.put("max_age", max_age, date.timespan_to_int) body.put("max_uses", max_uses) body.put("temporary", temporary) - body.put("unique", unique), + body.put("unique", unique) body.put_snowflake("target_user", target_user) body.put("target_user_type", target_user_type) response = await self._request(route, body=body, reason=reason) From f8ace6f44284c4fa7bba451b223431e5220218e7 Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Sat, 30 May 2020 18:24:56 +0100 Subject: [PATCH 416/922] Commented out bandit task in CI --- ci/gitlab/linting.yml | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/ci/gitlab/linting.yml b/ci/gitlab/linting.yml index 3c5a95aad1..0f7079c08a 100644 --- a/ci/gitlab/linting.yml +++ b/ci/gitlab/linting.yml @@ -85,8 +85,9 @@ safety: ### ### This runs static application security tests in addition to those that ### GitLab provides. Any issues are reported and the pipeline is aborted. +### Currently commented out as it is run by the GitLab SAST. ### -bandit: - extends: .lint - script: - - nox -s bandit --no-error-on-external-run +# bandit: +# extends: .lint +# script: +# - nox -s bandit --no-error-on-external-run From 69157b917eaca5119f02653ace153929a869d4ef Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Sat, 30 May 2020 18:57:57 +0100 Subject: [PATCH 417/922] Fixed failing tests, renamed package, implemented type hints in @listen(), and fixed typehint sniffer to give None instead of NoneType. --- hikari/__init__.py | 5 +- hikari/event_dispatcher.py | 4 +- hikari/impl/event_manager_core.py | 42 +++- hikari/utilities/reflect.py | 13 +- tests/hikari/internal/test_conversions.py | 231 ------------------ .../{internal => utilities}/__init__.py | 0 .../{internal => utilities}/test_aio.py | 0 .../{internal => utilities}/test_klass.py | 0 .../test_undefined.py} | 21 +- 9 files changed, 67 insertions(+), 249 deletions(-) delete mode 100644 tests/hikari/internal/test_conversions.py rename tests/hikari/{internal => utilities}/__init__.py (100%) rename tests/hikari/{internal => utilities}/test_aio.py (100%) rename tests/hikari/{internal => utilities}/test_klass.py (100%) rename tests/hikari/{internal/test_unset.py => utilities/test_undefined.py} (68%) diff --git a/hikari/__init__.py b/hikari/__init__.py index 17b667b7a6..f34fc6113a 100644 --- a/hikari/__init__.py +++ b/hikari/__init__.py @@ -37,4 +37,7 @@ from hikari.models import * from hikari.net import * -__all__ = [] +from hikari.impl.bot import BotImpl as HikariBot +from hikari.impl.rest_app import RESTAppImpl as HikariREST + +__all__ = ["HikariBot", "HikariREST"] diff --git a/hikari/event_dispatcher.py b/hikari/event_dispatcher.py index da801bad6e..08cc2d4ba6 100644 --- a/hikari/event_dispatcher.py +++ b/hikari/event_dispatcher.py @@ -100,7 +100,9 @@ def unsubscribe( """ @abc.abstractmethod - def listen(self, event_type: typing.Union[undefined.Undefined, typing.Type[_EventT]]) -> None: + def listen( + self, event_type: typing.Union[undefined.Undefined, typing.Type[_EventT]] = undefined.Undefined(), + ) -> None: """Generate a decorator to subscribe a callback to an event type. This is a second-order decorator. diff --git a/hikari/impl/event_manager_core.py b/hikari/impl/event_manager_core.py index df85967c15..1e68247309 100644 --- a/hikari/impl/event_manager_core.py +++ b/hikari/impl/event_manager_core.py @@ -22,6 +22,7 @@ __all__ = ["EventManagerCore"] import asyncio +import functools import typing from hikari import event_consumer @@ -32,6 +33,8 @@ from hikari.utilities import aio from hikari.utilities import data_binding from hikari.utilities import klass +from hikari.utilities import reflect +from hikari.utilities import undefined if typing.TYPE_CHECKING: from hikari import app as app_ @@ -106,10 +109,20 @@ def subscribe( if not asyncio.iscoroutinefunction(callback): + @functools.wraps(callback) async def wrapper(event): return callback(event) - self._listeners[event_type].append(wrapper) + self.subscribe(event_type, wrapper) + else: + self.logger.debug( + "subscribing callback 'async def %s%s' to event-type %s.%s", + getattr(callback, "__name__", ""), + reflect.resolve_signature(callback), + event_type.__module__, + event_type.__qualname__, + ) + self._listeners[event_type].append(callback) def unsubscribe( self, @@ -117,12 +130,37 @@ def unsubscribe( callback: typing.Callable[[_EventT], typing.Union[typing.Coroutine[None, typing.Any, None], None]], ) -> None: if event_type in self._listeners: + self.logger.debug( + "unsubscribing callback %s%s from event-type %s.%s", + getattr(callback, "__name__", ""), + reflect.resolve_signature(callback), + event_type.__module__, + event_type.__qualname__, + ) self._listeners[event_type].remove(callback) if not self._listeners[event_type]: del self._listeners[event_type] - def listen(self, event_type: typing.Type[_EventT]) -> typing.Callable[[_CallbackT], _CallbackT]: + def listen( + self, event_type: typing.Union[undefined.Undefined, typing.Type[_EventT]] = undefined.Undefined(), + ) -> typing.Callable[[_CallbackT], _CallbackT]: def decorator(callback: _CallbackT) -> _CallbackT: + nonlocal event_type + + signature = reflect.resolve_signature(callback) + params = signature.parameters.values() + + if len(params) != 1: + raise TypeError("Event listener must have one parameter, the event object.") + + event_param = next(iter(params)) + + if event_type is undefined.Undefined(): + if event_param.annotation is event_param.empty: + raise TypeError("Must provide the event type in the @listen decorator or as a type hint!") + + event_type = event_param.annotation + self.subscribe(event_type, callback) return callback diff --git a/hikari/utilities/reflect.py b/hikari/utilities/reflect.py index f119145b4e..5c48e63c7d 100644 --- a/hikari/utilities/reflect.py +++ b/hikari/utilities/reflect.py @@ -50,9 +50,12 @@ def resolve_signature(func: typing.Callable) -> inspect.Signature: if isinstance(value.annotation, str): if resolved_type_hints is None: resolved_type_hints = typing.get_type_hints(func) - parameters.append(value.replace(annotation=resolved_type_hints[key])) - else: - parameters.append(value) + value = value.replace(annotation=resolved_type_hints[key]) + + if value is type(None): + value = None + + parameters.append(value) signature = signature.replace(parameters=parameters) if isinstance(signature.return_annotation, str): @@ -60,6 +63,10 @@ def resolve_signature(func: typing.Callable) -> inspect.Signature: return_annotation = typing.get_type_hints(func)["return"] else: return_annotation = resolved_type_hints["return"] + + if return_annotation is type(None): + return_annotation = None + signature = signature.replace(return_annotation=return_annotation) return signature diff --git a/tests/hikari/internal/test_conversions.py b/tests/hikari/internal/test_conversions.py deleted file mode 100644 index 531dc74ec1..0000000000 --- a/tests/hikari/internal/test_conversions.py +++ /dev/null @@ -1,231 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# Copyright © Nekoka.tt 2019-2020 -# -# This file is part of Hikari. -# -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . -import datetime -import typing - -import pytest - -from hikari.utilities import conversions -from hikari.utilities import undefined - - -def test_put_if_specified_when_specified(): - d = {} - conversions.put_if_specified(d, "foo", 69) - conversions.put_if_specified(d, "bar", "hi") - conversions.put_if_specified(d, "bar", None) - assert d == {"foo": 69, "bar": None} - - -def test_put_if_specified_when_unspecified(): - d = {} - conversions.put_if_specified(d, "bar", undefined.Undefined()) - assert d == {} - - -def test_put_if_specified_when_type_after_passed(): - d = {} - conversions.put_if_specified(d, "foo", 69, str) - conversions.put_if_specified(d, "bar", "69", int) - assert d == {"foo": "69", "bar": 69} - - -def test_parse_iso_8601_date_with_negative_timezone(): - string = "2019-10-10T05:22:33.023456-02:30" - date = conversions.iso8601_datetime_string_to_datetime(string) - assert date.year == 2019 - assert date.month == 10 - assert date.day == 10 - assert date.hour == 5 - assert date.minute == 22 - assert date.second == 33 - assert date.microsecond == 23456 - offset = date.tzinfo.utcoffset(None) - assert offset == datetime.timedelta(hours=-2, minutes=-30) - - -def test_parse_iso_8601_date_with_positive_timezone(): - string = "2019-10-10T05:22:33.023456+02:30" - date = conversions.iso8601_datetime_string_to_datetime(string) - assert date.year == 2019 - assert date.month == 10 - assert date.day == 10 - assert date.hour == 5 - assert date.minute == 22 - assert date.second == 33 - assert date.microsecond == 23456 - offset = date.tzinfo.utcoffset(None) - assert offset == datetime.timedelta(hours=2, minutes=30) - - -def test_parse_iso_8601_date_with_zulu(): - string = "2019-10-10T05:22:33.023456Z" - date = conversions.iso8601_datetime_string_to_datetime(string) - assert date.year == 2019 - assert date.month == 10 - assert date.day == 10 - assert date.hour == 5 - assert date.minute == 22 - assert date.second == 33 - assert date.microsecond == 23456 - offset = date.tzinfo.utcoffset(None) - assert offset == datetime.timedelta(seconds=0) - - -def test_parse_iso_8601_date_with_milliseconds_instead_of_microseconds(): - string = "2019-10-10T05:22:33.023Z" - date = conversions.iso8601_datetime_string_to_datetime(string) - assert date.year == 2019 - assert date.month == 10 - assert date.day == 10 - assert date.hour == 5 - assert date.minute == 22 - assert date.second == 33 - assert date.microsecond == 23000 - - -def test_parse_iso_8601_date_with_no_fraction(): - string = "2019-10-10T05:22:33Z" - date = conversions.iso8601_datetime_string_to_datetime(string) - assert date.year == 2019 - assert date.month == 10 - assert date.day == 10 - assert date.hour == 5 - assert date.minute == 22 - assert date.second == 33 - assert date.microsecond == 0 - - -def test_parse_http_date(): - rfc_timestamp = "Mon, 03 Jun 2019 17:54:26 GMT" - expected_timestamp = datetime.datetime(2019, 6, 3, 17, 54, 26, tzinfo=datetime.timezone.utc) - assert conversions.rfc7231_datetime_string_to_datetime(rfc_timestamp) == expected_timestamp - - -def test_parse_discord_epoch_to_datetime(): - discord_timestamp = 37921278956 - expected_timestamp = datetime.datetime(2016, 3, 14, 21, 41, 18, 956000, tzinfo=datetime.timezone.utc) - assert conversions.discord_epoch_to_datetime(discord_timestamp) == expected_timestamp - - -def test_parse_datetime_to_discord_epoch(): - timestamp = datetime.datetime(2016, 3, 14, 21, 41, 18, 956000, tzinfo=datetime.timezone.utc) - expected_discord_timestamp = 37921278956 - assert conversions.datetime_to_discord_epoch(timestamp) == expected_discord_timestamp - - -def test_parse_unix_epoch_to_datetime(): - unix_timestamp = 1457991678956 - expected_timestamp = datetime.datetime(2016, 3, 14, 21, 41, 18, 956000, tzinfo=datetime.timezone.utc) - assert conversions.unix_epoch_to_datetime(unix_timestamp) == expected_timestamp - - -def test_unix_epoch_to_datetime_with_out_of_range_positive_timestamp(): - assert conversions.unix_epoch_to_datetime(996877846784536) == datetime.datetime.max - - -def test_unix_epoch_to_datetime_with_out_of_range_negative_timestamp(): - assert conversions.unix_epoch_to_datetime(-996877846784536) == datetime.datetime.min - - -@pytest.mark.parametrize( - ["count", "name", "kwargs", "expect"], - [ - (0, "foo", {}, "0 foos"), - (1, "foo", {}, "1 foo"), - (2, "foo", {}, "2 foos"), - (0, "foo", dict(suffix="es"), "0 fooes"), - (1, "foo", dict(suffix="es"), "1 foo"), - (2, "foo", dict(suffix="es"), "2 fooes"), - ], -) -def test_pluralize(count, name, kwargs, expect): - assert conversions.pluralize(count, name, **kwargs) == expect - - -class TestResolveSignature: - def test_handles_normal_references(self): - def foo(bar: str, bat: int) -> str: - ... - - signature = conversions.resolve_signature(foo) - assert signature.parameters["bar"].annotation is str - assert signature.parameters["bat"].annotation is int - assert signature.return_annotation is str - - def test_handles_normal_no_annotations(self): - def foo(bar, bat): - ... - - signature = conversions.resolve_signature(foo) - assert signature.parameters["bar"].annotation is conversions.EMPTY - assert signature.parameters["bat"].annotation is conversions.EMPTY - assert signature.return_annotation is conversions.EMPTY - - def test_handles_forward_annotated_parameters(self): - def foo(bar: "str", bat: "int") -> str: - ... - - signature = conversions.resolve_signature(foo) - assert signature.parameters["bar"].annotation is str - assert signature.parameters["bat"].annotation is int - assert signature.return_annotation is str - - def test_handles_forward_annotated_return(self): - def foo(bar: str, bat: int) -> "str": - ... - - signature = conversions.resolve_signature(foo) - assert signature.parameters["bar"].annotation is str - assert signature.parameters["bat"].annotation is int - assert signature.return_annotation is str - - def test_handles_forward_annotations(self): - def foo(bar: "str", bat: "int") -> "str": - ... - - signature = conversions.resolve_signature(foo) - assert signature.parameters["bar"].annotation is str - assert signature.parameters["bat"].annotation is int - assert signature.return_annotation is str - - def test_handles_mixed_annotations(self): - def foo(bar: str, bat: "int"): - ... - - signature = conversions.resolve_signature(foo) - assert signature.parameters["bar"].annotation is str - assert signature.parameters["bat"].annotation is int - assert signature.return_annotation is conversions.EMPTY - - def test_handles_only_return_annotated(self): - def foo(bar, bat) -> str: - ... - - signature = conversions.resolve_signature(foo) - assert signature.parameters["bar"].annotation is conversions.EMPTY - assert signature.parameters["bat"].annotation is conversions.EMPTY - assert signature.return_annotation is str - - def test_handles_nested_annotations(self): - def foo(bar: typing.Optional[typing.Iterator[int]]): - ... - - signature = conversions.resolve_signature(foo) - assert signature.parameters["bar"].annotation == typing.Optional[typing.Iterator[int]] diff --git a/tests/hikari/internal/__init__.py b/tests/hikari/utilities/__init__.py similarity index 100% rename from tests/hikari/internal/__init__.py rename to tests/hikari/utilities/__init__.py diff --git a/tests/hikari/internal/test_aio.py b/tests/hikari/utilities/test_aio.py similarity index 100% rename from tests/hikari/internal/test_aio.py rename to tests/hikari/utilities/test_aio.py diff --git a/tests/hikari/internal/test_klass.py b/tests/hikari/utilities/test_klass.py similarity index 100% rename from tests/hikari/internal/test_klass.py rename to tests/hikari/utilities/test_klass.py diff --git a/tests/hikari/internal/test_unset.py b/tests/hikari/utilities/test_undefined.py similarity index 68% rename from tests/hikari/internal/test_unset.py rename to tests/hikari/utilities/test_undefined.py index 6821caa842..74a7590b32 100644 --- a/tests/hikari/internal/test_unset.py +++ b/tests/hikari/utilities/test_undefined.py @@ -16,33 +16,32 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . -import pytest from hikari.utilities import undefined from tests.hikari import _helpers -class TestUnset: +class TestUndefined: def test_repr(self): - assert repr(undefined.Undefined()) == "UNSET" + assert repr(undefined.Undefined()) == "UNDEFINED" def test_str(self): - assert str(undefined.Undefined()) == "UNSET" + assert str(undefined.Undefined()) == "UNDEFINED" def test_bool(self): assert bool(undefined.Undefined()) is False + # noinspection PyComparisonWithNone def test_singleton_behaviour(self): - assert undefined.Unset() is undefined.Unset() - assert undefined.Undefined() is undefined.Unset() + assert undefined.Undefined() is undefined.Undefined() + assert undefined.Undefined() == undefined.Undefined() + assert undefined.Undefined() != None + assert undefined.Undefined() != False @_helpers.assert_raises(type_=TypeError) def test_cannot_subclass(self): class _(undefined.Undefined): pass - -class TestIsUnset: - @pytest.mark.parametrize(["obj", "is_unset"], [(undefined.Undefined(), True), (object(), False),]) - def test_is_unset(self, obj, is_unset): - assert isinstance(obj, undefined.Undefined) is is_unset + def test_count(self): + assert undefined.Undefined.count(9, 18, undefined.Undefined(), 36, undefined.Undefined(), 54) == 2 From b2c71d2bb27debd1e6dd164233b34fcf153b2969 Mon Sep 17 00:00:00 2001 From: Faster Speeding Date: Sat, 30 May 2020 17:28:56 +0100 Subject: [PATCH 418/922] Fix tests + consistency changes + remove Snowflake duplication in model.base --- black_test.py | 0 hikari/app.py | 18 +- hikari/bot.py | 3 +- hikari/cache.py | 22 +- hikari/component.py | 2 +- hikari/entity_factory.py | 125 +++-- hikari/event_consumer.py | 1 + hikari/event_dispatcher.py | 4 +- hikari/events/__init__.py | 12 +- hikari/events/channel.py | 39 +- hikari/events/guild.py | 32 +- hikari/events/message.py | 59 +- hikari/events/other.py | 4 +- hikari/events/voice.py | 4 +- hikari/impl/__init__.py | 20 + hikari/impl/bot.py | 14 +- hikari/impl/cache.py | 24 +- hikari/impl/entity_factory.py | 678 ++++++++++++++--------- hikari/impl/event_manager_core.py | 1 + hikari/impl/gateway_zookeeper.py | 11 +- hikari/models/__init__.py | 38 +- hikari/models/applications.py | 15 +- hikari/models/audit_logs.py | 77 +-- hikari/models/bases.py | 71 +-- hikari/models/channels.py | 19 +- hikari/models/emojis.py | 3 +- hikari/models/guilds.py | 403 +------------- hikari/models/messages.py | 73 +-- hikari/models/presences.py | 418 ++++++++++++++ hikari/models/voices.py | 11 +- hikari/models/webhooks.py | 21 +- hikari/net/__init__.py | 32 ++ hikari/net/buckets.py | 2 + hikari/net/gateway.py | 66 +-- hikari/net/http_client.py | 2 + hikari/net/iterators.py | 6 +- hikari/net/rest.py | 12 +- hikari/net/rest_utils.py | 15 +- hikari/net/voice_gateway.py | 2 +- hikari/utilities/__init__.py | 18 +- hikari/utilities/data_binding.py | 16 +- hikari/utilities/date.py | 1 + hikari/utilities/undefined.py | 15 +- tests/hikari/_helpers.py | 4 +- tests/hikari/impl/test_entity_factory.py | 127 +++-- tests/hikari/models/__init__.py | 18 + tests/hikari/utilities/test_date.py | 119 ++++ tests/hikari/utilities/test_reflect.py | 93 ++++ tests/hikari/utilities/test_undefined.py | 2 +- 49 files changed, 1602 insertions(+), 1170 deletions(-) create mode 100644 black_test.py create mode 100644 hikari/models/presences.py create mode 100644 tests/hikari/models/__init__.py create mode 100644 tests/hikari/utilities/test_date.py create mode 100644 tests/hikari/utilities/test_reflect.py diff --git a/black_test.py b/black_test.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/hikari/app.py b/hikari/app.py index 2186502f41..c66893d4ad 100644 --- a/hikari/app.py +++ b/hikari/app.py @@ -16,7 +16,7 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . -"""Core interfaces for a Hikari application.""" +"""Contains the core interfaces for a Hikari application.""" from __future__ import annotations __all__ = ["IApp"] @@ -33,7 +33,7 @@ from hikari import entity_factory as entity_factory_ from hikari import event_consumer as event_consumer_ from hikari import event_dispatcher as event_dispatcher_ - from hikari.models import guilds + from hikari.models import presences from hikari.net import gateway from hikari.net import rest as rest_ @@ -61,7 +61,7 @@ def entity_factory(self) -> entity_factory_.IEntityFactory: @property @abc.abstractmethod def thread_pool(self) -> typing.Optional[futures.ThreadPoolExecutor]: - """The optional library-wide thread-pool to utilise for file IO.""" + """Thread-pool to utilise for file IO within the library, if set.""" @abc.abstractmethod async def close(self) -> None: @@ -135,7 +135,7 @@ class IGatewayZookeeper(IGatewayConsumer, abc.ABC): @property @abc.abstractmethod def gateway_shards(self) -> typing.Mapping[int, gateway.Gateway]: - """Mapping of each shard ID to the corresponding client for it. + """Map of each shard ID to the corresponding client for it. If the shards have not started, and auto=sharding is in-place, then it is acceptable for this to return an empty mapping. @@ -144,7 +144,7 @@ def gateway_shards(self) -> typing.Mapping[int, gateway.Gateway]: @property @abc.abstractmethod def gateway_shard_count(self) -> int: - """The number of shards in the entire distributed application. + """Amount of shards in the entire distributed application. If the shards have not started, and auto=sharding is in-place, then it is acceptable for this to return `0`. @@ -162,8 +162,8 @@ async def join(self) -> None: async def update_presence( self, *, - status: guilds.PresenceStatus = ..., - activity: typing.Optional[gateway.Activity] = ..., + status: presences.PresenceStatus = ..., + activity: typing.Optional[presences.OwnActivity] = ..., idle_since: typing.Optional[datetime.datetime] = ..., is_afk: bool = ..., ) -> None: @@ -183,9 +183,9 @@ async def update_presence( Parameters ---------- - status : hikari.models.guilds.PresenceStatus + status : hikari.models.presences.PresenceStatus If specified, the new status to set. - activity : hikari.models.gateway.Activity | None + activity : hikari.models.presences.OwnActivity | None If specified, the new activity to set. idle_since : datetime.datetime | None If specified, the time to show up as being idle since, diff --git a/hikari/bot.py b/hikari/bot.py index d69f6eaa8b..f884a6f1b0 100644 --- a/hikari/bot.py +++ b/hikari/bot.py @@ -16,6 +16,7 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . +"""Contains the interface which describes the components for single-process bots.""" from __future__ import annotations __all__ = ["IBot"] @@ -43,4 +44,4 @@ class IBot(app_.IRESTApp, app_.IGatewayZookeeper, app_.IGatewayDispatcher, abc.A @property @abc.abstractmethod def http_settings(self) -> http_settings_.HTTPSettings: - """The HTTP settings to use.""" + """HTTP settings to use.""" diff --git a/hikari/cache.py b/hikari/cache.py index af839db829..52b21b7338 100644 --- a/hikari/cache.py +++ b/hikari/cache.py @@ -156,20 +156,20 @@ async def create_gateway_bot(self, payload: data_binding.JSONObject) -> gateway. ########## @abc.abstractmethod - async def create_member(self, payload: data_binding.JSONObject, can_cache: bool = False) -> guilds.GuildMember: + async def create_member(self, payload: data_binding.JSONObject, can_cache: bool = False) -> guilds.Member: # TODO: revisit for the voodoo to make a member into a special user. ... @abc.abstractmethod - async def update_member(self, member: guilds.GuildMember, payload: data_binding.JSONObject) -> guilds.GuildMember: + async def update_member(self, member: guilds.Member, payload: data_binding.JSONObject) -> guilds.Member: ... @abc.abstractmethod - async def get_member(self, guild_id: int, user_id: int) -> typing.Optional[guilds.GuildMember]: + async def get_member(self, guild_id: int, user_id: int) -> typing.Optional[guilds.Member]: ... @abc.abstractmethod - async def delete_member(self, guild_id: int, user_id: int) -> typing.Optional[guilds.GuildMember]: + async def delete_member(self, guild_id: int, user_id: int) -> typing.Optional[guilds.Member]: ... @abc.abstractmethod @@ -189,23 +189,21 @@ async def delete_role(self, guild_id: int, role_id: int) -> typing.Optional[guil ... @abc.abstractmethod - async def create_presence( - self, payload: data_binding.JSONObject, can_cache: bool = False - ) -> guilds.GuildMemberPresence: + async def create_presence(self, payload: data_binding.JSONObject, can_cache: bool = False) -> guilds.MemberPresence: ... @abc.abstractmethod async def update_presence( - self, role: guilds.GuildMemberPresence, payload: data_binding.JSONObject - ) -> guilds.GuildMemberPresence: + self, role: guilds.MemberPresence, payload: data_binding.JSONObject + ) -> guilds.MemberPresence: ... @abc.abstractmethod - async def get_presence(self, guild_id: int, user_id: int) -> typing.Optional[guilds.GuildMemberPresence]: + async def get_presence(self, guild_id: int, user_id: int) -> typing.Optional[guilds.MemberPresence]: ... @abc.abstractmethod - async def delete_presence(self, guild_id: int, user_id: int) -> typing.Optional[guilds.GuildMemberPresence]: + async def delete_presence(self, guild_id: int, user_id: int) -> typing.Optional[guilds.MemberPresence]: ... @abc.abstractmethod @@ -213,7 +211,7 @@ async def create_guild_ban(self, payload: data_binding.JSONObject) -> guilds.Gui ... @abc.abstractmethod - async def create_guild_integration(self, payload: data_binding.JSONObject) -> guilds.PartialIntegration: + async def create_integration(self, payload: data_binding.JSONObject) -> guilds.PartialIntegration: ... @abc.abstractmethod diff --git a/hikari/component.py b/hikari/component.py index 2f68c36cd9..c2ea1df15c 100644 --- a/hikari/component.py +++ b/hikari/component.py @@ -35,4 +35,4 @@ class IComponent(abc.ABC): @property @abc.abstractmethod def app(self) -> app_.IApp: - """The owning application object.""" + """Application that owns this component.""" diff --git a/hikari/entity_factory.py b/hikari/entity_factory.py index 9c77f50e93..568dc9f62a 100644 --- a/hikari/entity_factory.py +++ b/hikari/entity_factory.py @@ -36,6 +36,7 @@ from hikari.models import guilds from hikari.models import invites from hikari.models import messages + from hikari.models import presences from hikari.models import users from hikari.models import voices from hikari.models import webhooks @@ -58,7 +59,7 @@ def deserialize_own_connection(self, payload: data_binding.JSONObject) -> applic Parameters ---------- - payload : Mapping[Hashable, Any] + payload : Mapping[str, Any] The dict payload to parse. Returns @@ -73,7 +74,7 @@ def deserialize_own_guild(self, payload: data_binding.JSONObject) -> application Parameters ---------- - payload : Mapping[Hashable, Any] + payload : Mapping[str, Any] The dict payload to parse. Returns @@ -88,7 +89,7 @@ def deserialize_application(self, payload: data_binding.JSONObject) -> applicati Parameters ---------- - payload : Mapping[Hashable, Any] + payload : Mapping[str, Any] The dict payload to parse. Returns @@ -107,7 +108,7 @@ def deserialize_audit_log(self, payload: data_binding.JSONObject) -> audit_logs. Parameters ---------- - payload : Mapping[Hashable, Any] + payload : Mapping[str, Any] The dict payload to parse. Returns @@ -126,7 +127,7 @@ def deserialize_permission_overwrite(self, payload: data_binding.JSONObject) -> Parameters ---------- - payload : Mapping[Hashable, Any] + payload : Mapping[str, Any] The dict payload to parse. Returns @@ -156,7 +157,7 @@ def deserialize_partial_channel(self, payload: data_binding.JSONObject) -> chann Parameters ---------- - payload : Mapping[Hashable, Any] + payload : Mapping[str, Any] The dict payload to parse. Returns @@ -171,7 +172,7 @@ def deserialize_dm_channel(self, payload: data_binding.JSONObject) -> channels.D Parameters ---------- - payload : Mapping[Hashable, Any] + payload : Mapping[str, Any] The dict payload to parse. Returns @@ -186,7 +187,7 @@ def deserialize_group_dm_channel(self, payload: data_binding.JSONObject) -> chan Parameters ---------- - payload : Mapping[Hashable, Any] + payload : Mapping[str, Any] The dict payload to parse. Returns @@ -201,7 +202,7 @@ def deserialize_guild_category(self, payload: data_binding.JSONObject) -> channe Parameters ---------- - payload : Mapping[Hashable, Any] + payload : Mapping[str, Any] The dict payload to parse. Returns @@ -216,7 +217,7 @@ def deserialize_guild_text_channel(self, payload: data_binding.JSONObject) -> ch Parameters ---------- - payload : Mapping[Hashable, Any] + payload : Mapping[str, Any] The dict payload to parse. Returns @@ -231,7 +232,7 @@ def deserialize_guild_news_channel(self, payload: data_binding.JSONObject) -> ch Parameters ---------- - payload : Mapping[Hashable, Any] + payload : Mapping[str, Any] The dict payload to parse. Returns @@ -246,7 +247,7 @@ def deserialize_guild_store_channel(self, payload: data_binding.JSONObject) -> c Parameters ---------- - payload : Mapping[Hashable, Any] + payload : Mapping[str, Any] The dict payload to parse. Returns @@ -261,7 +262,7 @@ def deserialize_guild_voice_channel(self, payload: data_binding.JSONObject) -> c Parameters ---------- - payload : Mapping[Hashable, Any] + payload : Mapping[str, Any] The dict payload to parse. Returns @@ -276,7 +277,7 @@ def deserialize_channel(self, payload: data_binding.JSONObject) -> channels.Part Parameters ---------- - payload : Mapping[Hashable, Any] + payload : Mapping[str, Any] The dict payload to parse. Returns @@ -295,7 +296,7 @@ def deserialize_embed(self, payload: data_binding.JSONObject) -> embeds.Embed: Parameters ---------- - payload : Mapping[Hashable, Any] + payload : Mapping[str, Any] The dict payload to parse. Returns @@ -329,7 +330,7 @@ def deserialize_unicode_emoji(self, payload: data_binding.JSONObject) -> emojis. Parameters ---------- - payload : Mapping[Hashable, Any] + payload : Mapping[str, Any] The dict payload to parse. Returns @@ -344,7 +345,7 @@ def deserialize_custom_emoji(self, payload: data_binding.JSONObject) -> emojis.C Parameters ---------- - payload : Mapping[Hashable, Any] + payload : Mapping[str, Any] The dict payload to parse. Returns @@ -359,7 +360,7 @@ def deserialize_known_custom_emoji(self, payload: data_binding.JSONObject) -> em Parameters ---------- - payload : Mapping[Hashable, Any] + payload : Mapping[str, Any] The dict payload to parse. Returns @@ -376,7 +377,7 @@ def deserialize_emoji( Parameters ---------- - payload : Mapping[Hashable, Any] + payload : Mapping[str, Any] The dict payload to parse. Returns @@ -395,7 +396,7 @@ def deserialize_gateway_bot(self, payload: data_binding.JSONObject) -> gateway.G Parameters ---------- - payload : Mapping[Hashable, Any] + payload : Mapping[str, Any] The dict payload to parse. Returns @@ -414,7 +415,7 @@ def deserialize_guild_widget(self, payload: data_binding.JSONObject) -> guilds.G Parameters ---------- - payload : Mapping[Hashable, Any] + payload : Mapping[str, Any] The dict payload to parse. Returns @@ -424,14 +425,14 @@ def deserialize_guild_widget(self, payload: data_binding.JSONObject) -> guilds.G """ @abc.abstractmethod - def deserialize_guild_member( + def deserialize_member( self, payload: data_binding.JSONObject, *, user: typing.Optional[users.User] = None - ) -> guilds.GuildMember: - """Parse a raw payload from Discord into a guild member object. + ) -> guilds.Member: + """Parse a raw payload from Discord into a member object. Parameters ---------- - payload : Mapping[Hashable, Any] + payload : Mapping[str, Any] The dict payload to parse. *, user : hikari.models.users.User? @@ -440,38 +441,23 @@ def deserialize_guild_member( Returns ------- - hikari.models.guilds.GuildMember - The parsed guild member object. + hikari.models.guilds.Member + The parsed member object. """ @abc.abstractmethod def deserialize_role(self, payload: data_binding.JSONObject) -> guilds.Role: - """Parse a raw payload from Discord into a guild role object. + """Parse a raw payload from Discord into a role object. Parameters ---------- - payload : Mapping[Hashable, Any] + payload : Mapping[str, Any] The dict payload to parse. Returns ------- hikari.models.guilds.GuildRole - The parsed guild role object. - """ - - @abc.abstractmethod - def deserialize_guild_member_presence(self, payload: data_binding.JSONObject) -> guilds.GuildMemberPresence: - """Parse a raw payload from Discord into a guild member presence object. - - Parameters - ---------- - payload : Mapping[Hashable, Any] - The dict payload to parse. - - Returns - ------- - hikari.models.guilds.GuildMemberPresence - The parsed guild member presence object. + The parsed role object. """ @abc.abstractmethod @@ -480,7 +466,7 @@ def deserialize_partial_integration(self, payload: data_binding.JSONObject) -> g Parameters ---------- - payload : Mapping[Hashable, Any] + payload : Mapping[str, Any] The dict payload to parse. Returns @@ -495,7 +481,7 @@ def deserialize_integration(self, payload: data_binding.JSONObject) -> guilds.In Parameters ---------- - payload : Mapping[Hashable, Any] + payload : Mapping[str, Any] The dict payload to parse. Returns @@ -510,7 +496,7 @@ def deserialize_guild_member_ban(self, payload: data_binding.JSONObject) -> guil Parameters ---------- - payload : Mapping[Hashable, Any] + payload : Mapping[str, Any] The dict payload to parse. Returns @@ -525,7 +511,7 @@ def deserialize_unavailable_guild(self, payload: data_binding.JSONObject) -> gui Parameters ---------- - payload : Mapping[Hashable, Any] + payload : Mapping[str, Any] The dict payload to parse. Returns @@ -540,7 +526,7 @@ def deserialize_guild_preview(self, payload: data_binding.JSONObject) -> guilds. Parameters ---------- - payload : Mapping[Hashable, Any] + payload : Mapping[str, Any] The dict payload to parse. Returns @@ -555,7 +541,7 @@ def deserialize_guild(self, payload: data_binding.JSONObject) -> guilds.Guild: Parameters ---------- - payload : Mapping[Hashable, Any] + payload : Mapping[str, Any] The dict payload to parse. Returns @@ -574,7 +560,7 @@ def deserialize_vanity_url(self, payload: data_binding.JSONObject) -> invites.Va Parameters ---------- - payload : Mapping[Hashable, Any] + payload : Mapping[str, Any] The dict payload to parse. Returns @@ -589,7 +575,7 @@ def deserialize_invite(self, payload: data_binding.JSONObject) -> invites.Invite Parameters ---------- - payload : Mapping[Hashable, Any] + payload : Mapping[str, Any] The dict payload to parse. Returns @@ -604,7 +590,7 @@ def deserialize_invite_with_metadata(self, payload: data_binding.JSONObject) -> Parameters ---------- - payload : Mapping[Hashable, Any] + payload : Mapping[str, Any] The dict payload to parse. Returns @@ -622,7 +608,7 @@ def deserialize_message(self, payload: data_binding.JSONObject) -> messages.Mess Parameters ---------- - payload : Mapping[Hashable, Any] + payload : Mapping[str, Any] The dict payload to parse. Returns @@ -631,6 +617,25 @@ def deserialize_message(self, payload: data_binding.JSONObject) -> messages.Mess The parsed message object. """ + ############# + # PRESENCES # + ############# + + @abc.abstractmethod + def deserialize_member_presence(self, payload: data_binding.JSONObject) -> presences.MemberPresence: + """Parse a raw payload from Discord into a member presence object. + + Parameters + ---------- + payload : Mapping[str, Any] + The dict payload to parse. + + Returns + ------- + hikari.models.guilds.MemberPresence + The parsed member presence object. + """ + ######### # USERS # ######### @@ -641,7 +646,7 @@ def deserialize_user(self, payload: data_binding.JSONObject) -> users.User: Parameters ---------- - payload : Mapping[Hashable, Any] + payload : Mapping[str, Any] The dict payload to parse. Returns @@ -656,7 +661,7 @@ def deserialize_my_user(self, payload: data_binding.JSONObject) -> users.MyUser: Parameters ---------- - payload : Mapping[Hashable, Any] + payload : Mapping[str, Any] The dict payload to parse. Returns @@ -675,7 +680,7 @@ def deserialize_voice_state(self, payload: data_binding.JSONObject) -> voices.Vo Parameters ---------- - payload : Mapping[Hashable, Any] + payload : Mapping[str, Any] The dict payload to parse. Returns @@ -690,7 +695,7 @@ def deserialize_voice_region(self, payload: data_binding.JSONObject) -> voices.V Parameters ---------- - payload : Mapping[Hashable, Any] + payload : Mapping[str, Any] The dict payload to parse. Returns @@ -709,7 +714,7 @@ def deserialize_webhook(self, payload: data_binding.JSONObject) -> webhooks.Webh Parameters ---------- - payload : Mapping[Hashable, Any] + payload : Mapping[str, Any] The dict payload to parse. Returns diff --git a/hikari/event_consumer.py b/hikari/event_consumer.py index bdfb5a7f60..ea08fca46e 100644 --- a/hikari/event_consumer.py +++ b/hikari/event_consumer.py @@ -16,6 +16,7 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . +"""Contains an interface which describes component that can consume raw gateway events.""" from __future__ import annotations __all__ = ["IEventConsumer"] diff --git a/hikari/event_dispatcher.py b/hikari/event_dispatcher.py index 08cc2d4ba6..e13e05f808 100644 --- a/hikari/event_dispatcher.py +++ b/hikari/event_dispatcher.py @@ -109,9 +109,9 @@ def listen( Parameters ---------- - event_type : hikari.utilities.unset.Undefined OR typing.Type[hikari.events.bases.HikariEvent] + event_type : hikari.utilities.undefined.Undefined OR typing.Type[hikari.events.bases.HikariEvent] The event type to subscribe to. The implementation may allow this - to be unset. If this is the case, the event type will be inferred + to be undefined. If this is the case, the event type will be inferred instead from the type hints on the function signature. Returns diff --git a/hikari/events/__init__.py b/hikari/events/__init__.py index 0a74928018..d39dabbc7f 100644 --- a/hikari/events/__init__.py +++ b/hikari/events/__init__.py @@ -20,11 +20,11 @@ from __future__ import annotations -from .base import * -from .channel import * -from .guild import * -from .message import * -from .other import * -from .voice import * +from hikari.events.base import * +from hikari.events.channel import * +from hikari.events.guild import * +from hikari.events.message import * +from hikari.events.other import * +from hikari.events.voice import * __all__ = base.__all__ + channel.__all__ + guild.__all__ + message.__all__ + other.__all__ + voice.__all__ diff --git a/hikari/events/channel.py b/hikari/events/channel.py index 3dce8f00b5..4fc650367a 100644 --- a/hikari/events/channel.py +++ b/hikari/events/channel.py @@ -45,6 +45,7 @@ from hikari.models import intents from hikari.models import invites from hikari.models import users +from hikari.utilities import snowflake @base_events.requires_intents(intents.Intent.GUILDS) @@ -55,7 +56,7 @@ class BaseChannelEvent(base_events.HikariEvent, base_models.Unique, abc.ABC): type: channels.ChannelType = attr.ib(repr=True) """The channel's type.""" - guild_id: typing.Optional[base_models.Snowflake] = attr.ib(repr=True) + guild_id: typing.Optional[snowflake.Snowflake] = attr.ib(repr=True) """The ID of the guild this channel is in, will be `None` for DMs.""" position: typing.Optional[int] = attr.ib() @@ -65,7 +66,7 @@ class BaseChannelEvent(base_events.HikariEvent, base_models.Unique, abc.ABC): """ permission_overwrites: typing.Optional[ - typing.Mapping[base_models.Snowflake, channels.PermissionOverwrite] + typing.Mapping[snowflake.Snowflake, channels.PermissionOverwrite] ] = attr.ib() """An mapping of the set permission overwrites for this channel, if applicable.""" @@ -78,7 +79,7 @@ class BaseChannelEvent(base_events.HikariEvent, base_models.Unique, abc.ABC): is_nsfw: typing.Optional[bool] = attr.ib() """Whether this channel is nsfw, will be `None` if not applicable.""" - last_message_id: typing.Optional[base_models.Snowflake] = attr.ib() + last_message_id: typing.Optional[snowflake.Snowflake] = attr.ib() """The ID of the last message sent, if it's a text type channel.""" bitrate: typing.Optional[int] = attr.ib() @@ -93,22 +94,22 @@ class BaseChannelEvent(base_events.HikariEvent, base_models.Unique, abc.ABC): This is only applicable to a guild text like channel. """ - recipients: typing.Optional[typing.Mapping[base_models.Snowflake, users.User]] = attr.ib() + recipients: typing.Optional[typing.Mapping[snowflake.Snowflake, users.User]] = attr.ib() """A mapping of this channel's recipient users, if it's a DM or group DM.""" icon_hash: typing.Optional[str] = attr.ib() """The hash of this channel's icon, if it's a group DM channel and is set.""" - owner_id: typing.Optional[base_models.Snowflake] = attr.ib() + owner_id: typing.Optional[snowflake.Snowflake] = attr.ib() """The ID of this channel's creator, if it's a DM channel.""" - application_id: typing.Optional[base_models.Snowflake] = attr.ib() + application_id: typing.Optional[snowflake.Snowflake] = attr.ib() """The ID of the application that created the group DM. This is only applicable to bot based group DMs. """ - parent_id: typing.Optional[base_models.Snowflake] = attr.ib() + parent_id: typing.Optional[snowflake.Snowflake] = attr.ib() """The ID of this channels's parent category within guild, if set.""" last_pin_timestamp: typing.Optional[datetime.datetime] = attr.ib() @@ -146,13 +147,13 @@ class ChannelPinsUpdateEvent(base_events.HikariEvent): when a pinned message is deleted. """ - guild_id: typing.Optional[base_models.Snowflake] = attr.ib() + guild_id: typing.Optional[snowflake.Snowflake] = attr.ib() """The ID of the guild where this event happened. Will be `None` if this happened in a DM channel. """ - channel_id: base_models.Snowflake = attr.ib(repr=True) + channel_id: snowflake.Snowflake = attr.ib(repr=True) """The ID of the channel where the message was pinned or unpinned.""" last_pin_timestamp: typing.Optional[datetime.datetime] = attr.ib(repr=True) @@ -170,10 +171,10 @@ class WebhookUpdateEvent(base_events.HikariEvent): Sent when a webhook is updated, created or deleted in a guild. """ - guild_id: base_models.Snowflake = attr.ib(repr=True) + guild_id: snowflake.Snowflake = attr.ib(repr=True) """The ID of the guild this webhook is being updated in.""" - channel_id: base_models.Snowflake = attr.ib(repr=True) + channel_id: snowflake.Snowflake = attr.ib(repr=True) """The ID of the channel this webhook is being updated in.""" @@ -185,22 +186,22 @@ class TypingStartEvent(base_events.HikariEvent): Received when a user or bot starts "typing" in a channel. """ - channel_id: base_models.Snowflake = attr.ib(repr=True) + channel_id: snowflake.Snowflake = attr.ib(repr=True) """The ID of the channel this typing event is occurring in.""" - guild_id: typing.Optional[base_models.Snowflake] = attr.ib(repr=True) + guild_id: typing.Optional[snowflake.Snowflake] = attr.ib(repr=True) """The ID of the guild this typing event is occurring in. Will be `None` if this event is happening in a DM channel. """ - user_id: base_models.Snowflake = attr.ib(repr=True) + user_id: snowflake.Snowflake = attr.ib(repr=True) """The ID of the user who triggered this typing event.""" timestamp: datetime.datetime = attr.ib() """The datetime of when this typing event started.""" - member: typing.Optional[guilds.GuildMember] = attr.ib() + member: typing.Optional[guilds.Member] = attr.ib() """The member object of the user who triggered this typing event. Will be `None` if this was triggered in a DM. @@ -212,7 +213,7 @@ class TypingStartEvent(base_events.HikariEvent): class InviteCreateEvent(base_events.HikariEvent): """Represents a gateway Invite Create event.""" - channel_id: base_models.Snowflake = attr.ib(repr=True) + channel_id: snowflake.Snowflake = attr.ib(repr=True) """The ID of the channel this invite targets.""" code: str = attr.ib(repr=True) @@ -221,7 +222,7 @@ class InviteCreateEvent(base_events.HikariEvent): created_at: datetime.datetime = attr.ib() """The datetime of when this invite was created.""" - guild_id: typing.Optional[base_models.Snowflake] = attr.ib(repr=True) + guild_id: typing.Optional[snowflake.Snowflake] = attr.ib(repr=True) """The ID of the guild this invite was created in, if applicable. Will be `None` for group DM invites. @@ -263,14 +264,14 @@ class InviteDeleteEvent(base_events.HikariEvent): Sent when an invite is deleted for a channel we can access. """ - channel_id: base_models.Snowflake = attr.ib(repr=True) + channel_id: snowflake.Snowflake = attr.ib(repr=True) """The ID of the channel this ID was attached to.""" # TODO: move common fields with InviteCreateEvent into base class. code: str = attr.ib(repr=True) """The code of this invite.""" - guild_id: typing.Optional[base_models.Snowflake] = attr.ib(repr=True) + guild_id: typing.Optional[snowflake.Snowflake] = attr.ib(repr=True) """The ID of the guild this invite was deleted in. This will be `None` if this invite belonged to a DM channel. diff --git a/hikari/events/guild.py b/hikari/events/guild.py index 229408d40c..dae0ba395b 100644 --- a/hikari/events/guild.py +++ b/hikari/events/guild.py @@ -49,8 +49,10 @@ from hikari.models import emojis as emojis_models from hikari.models import guilds from hikari.models import intents +from hikari.models import presences from hikari.models import users from hikari.utilities import undefined +from hikari.utilities import snowflake if typing.TYPE_CHECKING: import datetime @@ -97,7 +99,7 @@ class GuildUnavailableEvent(base_events.HikariEvent, base_models.Unique): class BaseGuildBanEvent(base_events.HikariEvent, abc.ABC): """A base object that guild ban events will inherit from.""" - guild_id: base_models.Snowflake = attr.ib(repr=True) + guild_id: snowflake.Snowflake = attr.ib(repr=True) """The ID of the guild this ban is in.""" user: users.User = attr.ib(repr=True) @@ -121,10 +123,10 @@ class GuildBanRemoveEvent(BaseGuildBanEvent): class GuildEmojisUpdateEvent(base_events.HikariEvent): """Represents a Guild Emoji Update gateway event.""" - guild_id: base_models.Snowflake = attr.ib() + guild_id: snowflake.Snowflake = attr.ib() """The ID of the guild this emoji was updated in.""" - emojis: typing.Mapping[base_models.Snowflake, emojis_models.KnownCustomEmoji] = attr.ib(repr=True) + emojis: typing.Mapping[snowflake.Snowflake, emojis_models.KnownCustomEmoji] = attr.ib(repr=True) """The updated mapping of emojis by their ID.""" @@ -133,16 +135,16 @@ class GuildEmojisUpdateEvent(base_events.HikariEvent): class GuildIntegrationsUpdateEvent(base_events.HikariEvent): """Used to represent Guild Integration Update gateway events.""" - guild_id: base_models.Snowflake = attr.ib(repr=True) + guild_id: snowflake.Snowflake = attr.ib(repr=True) """The ID of the guild the integration was updated in.""" @base_events.requires_intents(intents.Intent.GUILD_MEMBERS) @attr.s(eq=False, hash=False, kw_only=True, slots=True) -class GuildMemberAddEvent(base_events.HikariEvent, guilds.GuildMember): +class GuildMemberAddEvent(base_events.HikariEvent, guilds.Member): """Used to represent a Guild Member Add gateway event.""" - guild_id: base_models.Snowflake = attr.ib(repr=True) + guild_id: snowflake.Snowflake = attr.ib(repr=True) """The ID of the guild where this member was added.""" @@ -154,10 +156,10 @@ class GuildMemberUpdateEvent(base_events.HikariEvent): Sent when a guild member or their inner user object is updated. """ - guild_id: base_models.Snowflake = attr.ib(repr=True) + guild_id: snowflake.Snowflake = attr.ib(repr=True) """The ID of the guild this member was updated in.""" - role_ids: typing.Sequence[base_models.Snowflake] = attr.ib() + role_ids: typing.Sequence[snowflake.Snowflake] = attr.ib() """A sequence of the IDs of the member's current roles.""" user: users.User = attr.ib(repr=True) @@ -167,7 +169,7 @@ class GuildMemberUpdateEvent(base_events.HikariEvent): """This member's nickname. When set to `None`, this has been removed and when set to - `hikari.models.unset.UNSET` this hasn't been acted on. + `hikari.models.undefined.Undefined` this hasn't been acted on. """ premium_since: typing.Union[None, datetime.datetime, undefined.Undefined] = attr.ib() @@ -186,7 +188,7 @@ class GuildMemberRemoveEvent(base_events.HikariEvent): """ # TODO: make GuildMember event into common base class. - guild_id: base_models.Snowflake = attr.ib(repr=True) + guild_id: snowflake.Snowflake = attr.ib(repr=True) """The ID of the guild this user was removed from.""" user: users.User = attr.ib(repr=True) @@ -198,7 +200,7 @@ class GuildMemberRemoveEvent(base_events.HikariEvent): class GuildRoleCreateEvent(base_events.HikariEvent): """Used to represent a Guild Role Create gateway event.""" - guild_id: base_models.Snowflake = attr.ib(repr=True) + guild_id: snowflake.Snowflake = attr.ib(repr=True) """The ID of the guild where this role was created.""" role: guilds.Role = attr.ib() @@ -212,7 +214,7 @@ class GuildRoleUpdateEvent(base_events.HikariEvent): # TODO: make any event with a guild ID into a custom base event. # https://pypi.org/project/stupid/ could this work around the multiple inheritance problem? - guild_id: base_models.Snowflake = attr.ib(repr=True) + guild_id: snowflake.Snowflake = attr.ib(repr=True) """The ID of the guild where this role was updated.""" role: guilds.Role = attr.ib(repr=True) @@ -224,16 +226,16 @@ class GuildRoleUpdateEvent(base_events.HikariEvent): class GuildRoleDeleteEvent(base_events.HikariEvent): """Represents a gateway Guild Role Delete Event.""" - guild_id: base_models.Snowflake = attr.ib(repr=True) + guild_id: snowflake.Snowflake = attr.ib(repr=True) """The ID of the guild where this role is being deleted.""" - role_id: base_models.Snowflake = attr.ib(repr=True) + role_id: snowflake.Snowflake = attr.ib(repr=True) """The ID of the role being deleted.""" @base_events.requires_intents(intents.Intent.GUILD_PRESENCES) @attr.s(eq=False, hash=False, kw_only=True, slots=True) -class PresenceUpdateEvent(base_events.HikariEvent, guilds.GuildMemberPresence): +class PresenceUpdateEvent(base_events.HikariEvent, presences.MemberPresence): """Used to represent Presence Update gateway events. Sent when a guild member changes their presence. diff --git a/hikari/events/message.py b/hikari/events/message.py index 1546e9604e..e6d0357ff5 100644 --- a/hikari/events/message.py +++ b/hikari/events/message.py @@ -45,6 +45,7 @@ from hikari.models import messages from hikari.models import users from hikari.utilities import undefined +from hikari.utilities import snowflake if typing.TYPE_CHECKING: import datetime @@ -64,15 +65,15 @@ class MessageUpdateEvent(base_events.HikariEvent, base_models.Unique): !!! note All fields on this model except `MessageUpdateEvent.channel` and - `MessageUpdateEvent.id` may be set to `hikari.models.unset.UNSET` (a singleton) + `MessageUpdateEvent.id` may be set to `hikari.models.undefined.Undefined` (a singleton) we have not received information about their state from Discord alongside field nullability. """ - channel_id: base_models.Snowflake = attr.ib(repr=True) + channel_id: snowflake.Snowflake = attr.ib(repr=True) """The ID of the channel that the message was sent in.""" - guild_id: typing.Union[base_models.Snowflake, undefined.Undefined] = attr.ib(repr=True) + guild_id: typing.Union[snowflake.Snowflake, undefined.Undefined] = attr.ib(repr=True) """The ID of the guild that the message was sent in.""" author: typing.Union[users.User, undefined.Undefined] = attr.ib(repr=True) @@ -80,7 +81,7 @@ class MessageUpdateEvent(base_events.HikariEvent, base_models.Unique): # TODO: can we merge member and author together? # We could override deserialize to to this and then reorganise the payload, perhaps? - member: typing.Union[guilds.GuildMember, undefined.Undefined] = attr.ib() + member: typing.Union[guilds.Member, undefined.Undefined] = attr.ib() """The member properties for the message's author.""" content: typing.Union[str, undefined.Undefined] = attr.ib() @@ -101,13 +102,13 @@ class MessageUpdateEvent(base_events.HikariEvent, base_models.Unique): is_mentioning_everyone: typing.Union[bool, undefined.Undefined] = attr.ib() """Whether the message mentions `@everyone` or `@here`.""" - user_mentions: typing.Union[typing.Set[base_models.Snowflake], undefined.Undefined] = attr.ib() + user_mentions: typing.Union[typing.Set[snowflake.Snowflake], undefined.Undefined] = attr.ib() """The users the message mentions.""" - role_mentions: typing.Union[typing.Set[base_models.Snowflake], undefined.Undefined] = attr.ib() + role_mentions: typing.Union[typing.Set[snowflake.Snowflake], undefined.Undefined] = attr.ib() """The roles the message mentions.""" - channel_mentions: typing.Union[typing.Set[base_models.Snowflake], undefined.Undefined] = attr.ib() + channel_mentions: typing.Union[typing.Set[snowflake.Snowflake], undefined.Undefined] = attr.ib() """The channels the message mentions.""" attachments: typing.Union[typing.Sequence[messages.Attachment], undefined.Undefined] = attr.ib() @@ -122,7 +123,7 @@ class MessageUpdateEvent(base_events.HikariEvent, base_models.Unique): is_pinned: typing.Union[bool, undefined.Undefined] = attr.ib() """Whether the message is pinned.""" - webhook_id: typing.Union[base_models.Snowflake, undefined.Undefined] = attr.ib() + webhook_id: typing.Union[snowflake.Snowflake, undefined.Undefined] = attr.ib() """If the message was generated by a webhook, the webhook's ID.""" type: typing.Union[messages.MessageType, undefined.Undefined] = attr.ib() @@ -157,16 +158,16 @@ class MessageDeleteEvent(base_events.HikariEvent): # TODO: common base class for Message events. - channel_id: base_models.Snowflake = attr.ib(repr=True) + channel_id: snowflake.Snowflake = attr.ib(repr=True) """The ID of the channel where this message was deleted.""" - guild_id: typing.Optional[base_models.Snowflake] = attr.ib(repr=True) + guild_id: typing.Optional[snowflake.Snowflake] = attr.ib(repr=True) """The ID of the guild where this message was deleted. This will be `None` if this message was deleted in a DM channel. """ - message_id: base_models.Snowflake = attr.ib(repr=True) + message_id: snowflake.Snowflake = attr.ib(repr=True) """The ID of the message that was deleted.""" @@ -178,16 +179,16 @@ class MessageDeleteBulkEvent(base_events.HikariEvent): Sent when multiple messages are deleted in a channel at once. """ - channel_id: base_models.Snowflake = attr.ib(repr=True) + channel_id: snowflake.Snowflake = attr.ib(repr=True) """The ID of the channel these messages have been deleted in.""" - guild_id: typing.Optional[base_models.Snowflake] = attr.ib(repr=True,) + guild_id: typing.Optional[snowflake.Snowflake] = attr.ib(repr=True) """The ID of the channel these messages have been deleted in. This will be `None` if these messages were bulk deleted in a DM channel. """ - message_ids: typing.Set[base_models.Snowflake] = attr.ib() + message_ids: typing.Set[snowflake.Snowflake] = attr.ib() """A collection of the IDs of the messages that were deleted.""" @@ -198,23 +199,23 @@ class MessageReactionAddEvent(base_events.HikariEvent): # TODO: common base classes! - user_id: base_models.Snowflake = attr.ib(repr=True) + user_id: snowflake.Snowflake = attr.ib(repr=True) """The ID of the user adding the reaction.""" - channel_id: base_models.Snowflake = attr.ib(repr=True) + channel_id: snowflake.Snowflake = attr.ib(repr=True) """The ID of the channel where this reaction is being added.""" - message_id: base_models.Snowflake = attr.ib(repr=True) + message_id: snowflake.Snowflake = attr.ib(repr=True) """The ID of the message this reaction is being added to.""" - guild_id: typing.Optional[base_models.Snowflake] = attr.ib(repr=True) + guild_id: typing.Optional[snowflake.Snowflake] = attr.ib(repr=True) """The ID of the guild where this reaction is being added. This will be `None` if this is happening in a DM channel. """ # TODO: does this contain a user? If not, should it be a PartialGuildMember? - member: typing.Optional[guilds.GuildMember] = attr.ib() + member: typing.Optional[guilds.Member] = attr.ib() """The member object of the user who's adding this reaction. This will be `None` if this is happening in a DM channel. @@ -229,16 +230,16 @@ class MessageReactionAddEvent(base_events.HikariEvent): class MessageReactionRemoveEvent(base_events.HikariEvent): """Used to represent Message Reaction Remove gateway events.""" - user_id: base_models.Snowflake = attr.ib(repr=True) + user_id: snowflake.Snowflake = attr.ib(repr=True) """The ID of the user who is removing their reaction.""" - channel_id: base_models.Snowflake = attr.ib(repr=True) + channel_id: snowflake.Snowflake = attr.ib(repr=True) """The ID of the channel where this reaction is being removed.""" - message_id: base_models.Snowflake = attr.ib(repr=True) + message_id: snowflake.Snowflake = attr.ib(repr=True) """The ID of the message this reaction is being removed from.""" - guild_id: typing.Optional[base_models.Snowflake] = attr.ib(repr=True) + guild_id: typing.Optional[snowflake.Snowflake] = attr.ib(repr=True) """The ID of the guild where this reaction is being removed This will be `None` if this event is happening in a DM channel. @@ -256,13 +257,13 @@ class MessageReactionRemoveAllEvent(base_events.HikariEvent): Sent when all the reactions are removed from a message, regardless of emoji. """ - channel_id: base_models.Snowflake = attr.ib(repr=True) + channel_id: snowflake.Snowflake = attr.ib(repr=True) """The ID of the channel where the targeted message is.""" - message_id: base_models.Snowflake = attr.ib(repr=True) + message_id: snowflake.Snowflake = attr.ib(repr=True) """The ID of the message all reactions are being removed from.""" - guild_id: typing.Optional[base_models.Snowflake] = attr.ib(repr=True,) + guild_id: typing.Optional[snowflake.Snowflake] = attr.ib(repr=True,) """The ID of the guild where the targeted message is, if applicable.""" @@ -274,13 +275,13 @@ class MessageReactionRemoveEmojiEvent(base_events.HikariEvent): Sent when all the reactions for a single emoji are removed from a message. """ - channel_id: base_models.Snowflake = attr.ib(repr=True) + channel_id: snowflake.Snowflake = attr.ib(repr=True) """The ID of the channel where the targeted message is.""" - guild_id: typing.Optional[base_models.Snowflake] = attr.ib(repr=True) + guild_id: typing.Optional[snowflake.Snowflake] = attr.ib(repr=True) """The ID of the guild where the targeted message is, if applicable.""" - message_id: base_models.Snowflake = attr.ib(repr=True) + message_id: snowflake.Snowflake = attr.ib(repr=True) """The ID of the message the reactions are being removed from.""" emoji: typing.Union[emojis.UnicodeEmoji, emojis.CustomEmoji] = attr.ib(repr=True) diff --git a/hikari/events/other.py b/hikari/events/other.py index 6ce2beb509..a2fd0071d8 100644 --- a/hikari/events/other.py +++ b/hikari/events/other.py @@ -38,9 +38,9 @@ import attr from hikari.events import base as base_events -from hikari.models import bases as base_models from hikari.models import guilds from hikari.models import users +from hikari.utilities import snowflake if typing.TYPE_CHECKING: from hikari.net import gateway as gateway_client @@ -123,7 +123,7 @@ class ReadyEvent(base_events.HikariEvent): my_user: users.MyUser = attr.ib(repr=True) """The object of the current bot account this connection is for.""" - unavailable_guilds: typing.Mapping[base_models.Snowflake, guilds.UnavailableGuild] = attr.ib() + unavailable_guilds: typing.Mapping[snowflake.Snowflake, guilds.UnavailableGuild] = attr.ib() """A mapping of the guilds this bot is currently in. All guilds will start off "unavailable". diff --git a/hikari/events/voice.py b/hikari/events/voice.py index 581775a360..8e772875ba 100644 --- a/hikari/events/voice.py +++ b/hikari/events/voice.py @@ -25,9 +25,9 @@ import attr from hikari.events import base as base_events -from hikari.models import bases as base_models from hikari.models import intents from hikari.models import voices +from hikari.utilities import snowflake @base_events.requires_intents(intents.Intent.GUILD_VOICE_STATES) @@ -50,7 +50,7 @@ class VoiceServerUpdateEvent(base_events.HikariEvent): token: str = attr.ib() """The voice connection's string token.""" - guild_id: base_models.Snowflake = attr.ib(repr=True) + guild_id: snowflake.Snowflake = attr.ib(repr=True) """The ID of the guild this voice server update is for.""" endpoint: str = attr.ib(repr=True) diff --git a/hikari/impl/__init__.py b/hikari/impl/__init__.py index 1c1502a5ca..32ae2fa926 100644 --- a/hikari/impl/__init__.py +++ b/hikari/impl/__init__.py @@ -16,3 +16,23 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . + +from __future__ import annotations + +from hikari.impl.bot import * +from hikari.impl.cache import * +from hikari.impl.entity_factory import * +from hikari.impl.event_manager import * +from hikari.impl.event_manager_core import * +from hikari.impl.gateway_zookeeper import * +from hikari.impl.rest_app import * + +__all__ = ( + bot.__all__ + + cache.__all__ + + entity_factory.__all__ + + event_manager.__all__ + + event_manager_core.__all__ + + gateway_zookeeper.__all__ + + rest_app.__all__ +) diff --git a/hikari/impl/bot.py b/hikari/impl/bot.py index 1e329287b9..a89eb33905 100644 --- a/hikari/impl/bot.py +++ b/hikari/impl/bot.py @@ -16,9 +16,12 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . +"""Basic implementation the components for a single-process bot.""" from __future__ import annotations +__all__ = ["BotImpl"] + import logging import typing from concurrent import futures @@ -28,9 +31,7 @@ from hikari.impl import entity_factory as entity_factory_impl from hikari.impl import event_manager from hikari.impl import gateway_zookeeper -from hikari.models import gateway as gateway_models -from hikari.models import guilds -from hikari.net import gateway +from hikari.models import presences from hikari.net import rest from hikari.net import urls from hikari.utilities import klass @@ -43,6 +44,7 @@ from hikari import event_consumer as event_consumer_ from hikari import http_settings as http_settings_ from hikari import event_dispatcher + from hikari.models import gateway as gateway_models from hikari.models import intents as intents_ @@ -53,10 +55,10 @@ def __init__( config: http_settings_.HTTPSettings, debug: bool = False, gateway_version: int = 6, - initial_activity: typing.Optional[gateway.Activity] = None, + initial_activity: typing.Optional[presences.OwnActivity] = None, initial_idle_since: typing.Optional[datetime.datetime] = None, initial_is_afk: bool = False, - initial_status: guilds.PresenceStatus = guilds.PresenceStatus.ONLINE, + initial_status: presences.PresenceStatus = presences.PresenceStatus.ONLINE, intents: typing.Optional[intents_.Intent] = None, large_threshold: int = 250, rest_version: int = 6, @@ -73,7 +75,7 @@ def __init__( self._event_manager = event_manager.EventManagerImpl(app=self) self._entity_factory = entity_factory_impl.EntityFactoryImpl(app=self) - self._rest = rest.REST( + self._rest = rest.REST( # nosec app=self, config=config, debug=debug, token=token, token_type="Bot", url=rest_url, version=rest_version, ) diff --git a/hikari/impl/cache.py b/hikari/impl/cache.py index f3f7bf4bc9..91de162807 100644 --- a/hikari/impl/cache.py +++ b/hikari/impl/cache.py @@ -16,6 +16,8 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . +"""Basic implementation of a cache for general bots and gateway apps.""" + from __future__ import annotations __all__ = ["CacheImpl"] @@ -103,16 +105,16 @@ async def delete_emoji(self, emoji_id: int) -> typing.Optional[emojis.KnownCusto async def create_gateway_bot(self, payload: data_binding.JSONObject) -> gateway.GatewayBot: pass - async def create_member(self, payload: data_binding.JSONObject, can_cache: bool = False) -> guilds.GuildMember: + async def create_member(self, payload: data_binding.JSONObject, can_cache: bool = False) -> guilds.Member: pass - async def update_member(self, member: guilds.GuildMember, payload: data_binding.JSONObject) -> guilds.GuildMember: + async def update_member(self, member: guilds.Member, payload: data_binding.JSONObject) -> guilds.Member: pass - async def get_member(self, guild_id: int, user_id: int) -> typing.Optional[guilds.GuildMember]: + async def get_member(self, guild_id: int, user_id: int) -> typing.Optional[guilds.Member]: pass - async def delete_member(self, guild_id: int, user_id: int) -> typing.Optional[guilds.GuildMember]: + async def delete_member(self, guild_id: int, user_id: int) -> typing.Optional[guilds.Member]: pass async def create_role(self, payload: data_binding.JSONObject, can_cache: bool = False) -> guilds.PartialRole: @@ -127,26 +129,24 @@ async def get_role(self, guild_id: int, role_id: int) -> typing.Optional[guilds. async def delete_role(self, guild_id: int, role_id: int) -> typing.Optional[guilds.PartialRole]: pass - async def create_presence( - self, payload: data_binding.JSONObject, can_cache: bool = False - ) -> guilds.GuildMemberPresence: + async def create_presence(self, payload: data_binding.JSONObject, can_cache: bool = False) -> guilds.MemberPresence: pass async def update_presence( - self, role: guilds.GuildMemberPresence, payload: data_binding.JSONObject - ) -> guilds.GuildMemberPresence: + self, role: guilds.MemberPresence, payload: data_binding.JSONObject + ) -> guilds.MemberPresence: pass - async def get_presence(self, guild_id: int, user_id: int) -> typing.Optional[guilds.GuildMemberPresence]: + async def get_presence(self, guild_id: int, user_id: int) -> typing.Optional[guilds.MemberPresence]: pass - async def delete_presence(self, guild_id: int, user_id: int) -> typing.Optional[guilds.GuildMemberPresence]: + async def delete_presence(self, guild_id: int, user_id: int) -> typing.Optional[guilds.MemberPresence]: pass async def create_guild_ban(self, payload: data_binding.JSONObject) -> guilds.GuildMemberBan: pass - async def create_guild_integration(self, payload: data_binding.JSONObject) -> guilds.PartialIntegration: + async def create_integration(self, payload: data_binding.JSONObject) -> guilds.PartialIntegration: pass async def create_guild(self, payload: data_binding.JSONObject, can_cache: bool = False) -> guilds.PartialGuild: diff --git a/hikari/impl/entity_factory.py b/hikari/impl/entity_factory.py index 692b5e0ff8..520b4d0517 100644 --- a/hikari/impl/entity_factory.py +++ b/hikari/impl/entity_factory.py @@ -29,7 +29,6 @@ from hikari import entity_factory from hikari.models import applications from hikari.models import audit_logs -from hikari.models import bases as base_models from hikari.models import channels as channels_ from hikari.models import colors from hikari.models import embeds @@ -39,10 +38,12 @@ from hikari.models import invites from hikari.models import messages from hikari.models import permissions +from hikari.models import presences as presences_ from hikari.models import users from hikari.models import voices from hikari.models import webhooks from hikari.utilities import date +from hikari.utilities import snowflake from hikari.utilities import undefined if typing.TYPE_CHECKING: @@ -58,12 +59,56 @@ UserT = typing.TypeVar("UserT", bound=users.User) +def _deserialize_seconds_timedelta(seconds: typing.Union[str, int]) -> datetime.timedelta: + return datetime.timedelta(seconds=int(seconds)) + + +def _deserialize_day_timedelta(days: typing.Union[str, int]) -> datetime.timedelta: + return datetime.timedelta(days=int(days)) + + +def _deserialize_max_uses(age: int) -> typing.Union[int, float]: + return age if age > 0 else float("inf") + + +def _deserialize_max_age(seconds: int) -> typing.Optional[datetime.timedelta]: + return datetime.timedelta(seconds=seconds) if seconds > 0 else None + + class EntityFactoryImpl(entity_factory.IEntityFactory): """Interface for an entity factory implementation.""" def __init__(self, app: app_.IApp) -> None: self._app = app self._audit_log_entry_converters = { + audit_logs.AuditLogChangeKey.OWNER_ID: snowflake.Snowflake, + audit_logs.AuditLogChangeKey.AFK_CHANNEL_ID: snowflake.Snowflake, + audit_logs.AuditLogChangeKey.AFK_TIMEOUT: _deserialize_seconds_timedelta, + audit_logs.AuditLogChangeKey.MFA_LEVEL: guilds.GuildMFALevel, + audit_logs.AuditLogChangeKey.VERIFICATION_LEVEL: guilds.GuildVerificationLevel, + audit_logs.AuditLogChangeKey.EXPLICIT_CONTENT_FILTER: guilds.GuildExplicitContentFilterLevel, + audit_logs.AuditLogChangeKey.DEFAULT_MESSAGE_NOTIFICATIONS: guilds.GuildMessageNotificationsLevel, + audit_logs.AuditLogChangeKey.PRUNE_DELETE_DAYS: _deserialize_day_timedelta, + audit_logs.AuditLogChangeKey.WIDGET_CHANNEL_ID: snowflake.Snowflake, + audit_logs.AuditLogChangeKey.POSITION: int, + audit_logs.AuditLogChangeKey.BITRATE: int, + audit_logs.AuditLogChangeKey.APPLICATION_ID: snowflake.Snowflake, + audit_logs.AuditLogChangeKey.PERMISSIONS: permissions.Permission, + audit_logs.AuditLogChangeKey.COLOR: colors.Color, + audit_logs.AuditLogChangeKey.ALLOW: permissions.Permission, + audit_logs.AuditLogChangeKey.DENY: permissions.Permission, + audit_logs.AuditLogChangeKey.CHANNEL_ID: snowflake.Snowflake, + audit_logs.AuditLogChangeKey.INVITER_ID: snowflake.Snowflake, + audit_logs.AuditLogChangeKey.MAX_USES: _deserialize_max_uses, + audit_logs.AuditLogChangeKey.USES: int, + audit_logs.AuditLogChangeKey.MAX_AGE: _deserialize_max_age, + audit_logs.AuditLogChangeKey.ID: snowflake.Snowflake, + audit_logs.AuditLogChangeKey.TYPE: str, + audit_logs.AuditLogChangeKey.ENABLE_EMOTICONS: bool, + audit_logs.AuditLogChangeKey.EXPIRE_BEHAVIOR: guilds.IntegrationExpireBehaviour, + audit_logs.AuditLogChangeKey.EXPIRE_GRACE_PERIOD: _deserialize_day_timedelta, + audit_logs.AuditLogChangeKey.RATE_LIMIT_PER_USER: _deserialize_seconds_timedelta, + audit_logs.AuditLogChangeKey.SYSTEM_CHANNEL_ID: snowflake.Snowflake, audit_logs.AuditLogChangeKey.ADD_ROLE_TO_MEMBER: self._deserialize_audit_log_change_roles, audit_logs.AuditLogChangeKey.REMOVE_ROLE_FROM_MEMBER: self._deserialize_audit_log_change_roles, audit_logs.AuditLogChangeKey.PERMISSION_OVERWRITES: self._deserialize_audit_log_overwrites, @@ -103,7 +148,7 @@ def deserialize_own_connection(self, payload: data_binding.JSONObject) -> applic Parameters ---------- - payload : Mapping[Hashable, Any] + payload : Mapping[str, Any] The dict payload to parse. Returns @@ -112,7 +157,7 @@ def deserialize_own_connection(self, payload: data_binding.JSONObject) -> applic The parsed own connection object. """ own_connection = applications.OwnConnection() - own_connection.id = base_models.Snowflake(payload["id"]) + own_connection.id = snowflake.Snowflake(payload["id"]) own_connection.name = payload["name"] own_connection.type = payload["type"] own_connection.is_revoked = payload.get("revoked") @@ -131,7 +176,7 @@ def deserialize_own_guild(self, payload: data_binding.JSONObject) -> application Parameters ---------- - payload : Mapping[Hashable, Any] + payload : Mapping[str, Any] The dict payload to parse. Returns @@ -150,7 +195,7 @@ def deserialize_application(self, payload: data_binding.JSONObject) -> applicati Parameters ---------- - payload : Mapping[Hashable, Any] + payload : Mapping[str, Any] The dict payload to parse. Returns @@ -160,7 +205,7 @@ def deserialize_application(self, payload: data_binding.JSONObject) -> applicati """ application = applications.Application() application.set_app(self._app) - application.id = base_models.Snowflake(payload["id"]) + application.id = snowflake.Snowflake(payload["id"]) application.name = payload["name"] application.description = payload["description"] application.is_bot_public = payload.get("bot_public") @@ -170,11 +215,13 @@ def deserialize_application(self, payload: data_binding.JSONObject) -> applicati application.summary = payload["summary"] application.verify_key = bytes(payload["verify_key"], "utf-8") if "verify_key" in payload else None application.icon_hash = payload.get("icon") + if (team_payload := payload.get("team")) is not None: team = applications.Team() team.set_app(self._app) - team.id = base_models.Snowflake(team_payload["id"]) + team.id = snowflake.Snowflake(team_payload["id"]) team.icon_hash = team_payload["icon"] + members = {} for member_payload in team_payload["members"]: team_member = applications.TeamMember() @@ -182,17 +229,19 @@ def deserialize_application(self, payload: data_binding.JSONObject) -> applicati # noinspection PyArgumentList team_member.membership_state = applications.TeamMembershipState(member_payload["membership_state"]) team_member.permissions = set(member_payload["permissions"]) - team_member.team_id = base_models.Snowflake(member_payload["team_id"]) + team_member.team_id = snowflake.Snowflake(member_payload["team_id"]) team_member.user = self.deserialize_user(member_payload["user"]) members[team_member.user.id] = team_member team.members = members - team.owner_user_id = base_models.Snowflake(team_payload["owner_user_id"]) + + team.owner_user_id = snowflake.Snowflake(team_payload["owner_user_id"]) application.team = team else: application.team = None - application.guild_id = base_models.Snowflake(payload["guild_id"]) if "guild_id" in payload else None + + application.guild_id = snowflake.Snowflake(payload["guild_id"]) if "guild_id" in payload else None application.primary_sku_id = ( - base_models.Snowflake(payload["primary_sku_id"]) if "primary_sku_id" in payload else None + snowflake.Snowflake(payload["primary_sku_id"]) if "primary_sku_id" in payload else None ) application.slug = payload.get("slug") application.cover_image_hash = payload.get("cover_image") @@ -204,21 +253,21 @@ def deserialize_application(self, payload: data_binding.JSONObject) -> applicati def _deserialize_audit_log_change_roles( self, payload: data_binding.JSONArray - ) -> typing.Mapping[base_models.Snowflake, guilds.PartialRole]: + ) -> typing.Mapping[snowflake.Snowflake, guilds.PartialRole]: roles = {} for role_payload in payload: role = guilds.PartialRole() role.set_app(self._app) - role.id = base_models.Snowflake(role_payload["id"]) + role.id = snowflake.Snowflake(role_payload["id"]) role.name = role_payload["name"] roles[role.id] = role return roles def _deserialize_audit_log_overwrites( self, payload: data_binding.JSONArray - ) -> typing.Mapping[base_models.Snowflake, channels_.PermissionOverwrite]: + ) -> typing.Mapping[snowflake.Snowflake, channels_.PermissionOverwrite]: return { - base_models.Snowflake(overwrite["id"]): self.deserialize_permission_overwrite(overwrite) + snowflake.Snowflake(overwrite["id"]): self.deserialize_permission_overwrite(overwrite) for overwrite in payload } @@ -227,7 +276,7 @@ def _deserialize_channel_overwrite_entry_info( payload: data_binding.JSONObject, ) -> audit_logs.ChannelOverwriteEntryInfo: channel_overwrite_entry_info = audit_logs.ChannelOverwriteEntryInfo() - channel_overwrite_entry_info.id = base_models.Snowflake(payload["id"]) + channel_overwrite_entry_info.id = snowflake.Snowflake(payload["id"]) # noinspection PyArgumentList channel_overwrite_entry_info.type = channels_.PermissionOverwriteType(payload["type"]) channel_overwrite_entry_info.role_name = payload.get("role_name") @@ -236,8 +285,8 @@ def _deserialize_channel_overwrite_entry_info( @staticmethod def _deserialize_message_pin_entry_info(payload: data_binding.JSONObject) -> audit_logs.MessagePinEntryInfo: message_pin_entry_info = audit_logs.MessagePinEntryInfo() - message_pin_entry_info.channel_id = base_models.Snowflake(payload["channel_id"]) - message_pin_entry_info.message_id = base_models.Snowflake(payload["message_id"]) + message_pin_entry_info.channel_id = snowflake.Snowflake(payload["channel_id"]) + message_pin_entry_info.message_id = snowflake.Snowflake(payload["message_id"]) return message_pin_entry_info @staticmethod @@ -258,7 +307,7 @@ def _deserialize_message_bulk_delete_entry_info( @staticmethod def _deserialize_message_delete_entry_info(payload: data_binding.JSONObject) -> audit_logs.MessageDeleteEntryInfo: message_delete_entry_info = audit_logs.MessageDeleteEntryInfo() - message_delete_entry_info.channel_id = base_models.Snowflake(payload["channel_id"]) + message_delete_entry_info.channel_id = snowflake.Snowflake(payload["channel_id"]) message_delete_entry_info.count = int(payload["count"]) return message_delete_entry_info @@ -273,7 +322,7 @@ def _deserialize_member_disconnect_entry_info( @staticmethod def _deserialize_member_move_entry_info(payload: data_binding.JSONObject) -> audit_logs.MemberMoveEntryInfo: member_move_entry_info = audit_logs.MemberMoveEntryInfo() - member_move_entry_info.channel_id = base_models.Snowflake(payload["channel_id"]) + member_move_entry_info.channel_id = snowflake.Snowflake(payload["channel_id"]) member_move_entry_info.count = int(payload["count"]) return member_move_entry_info @@ -288,7 +337,7 @@ def deserialize_audit_log(self, payload: data_binding.JSONObject) -> audit_logs. Parameters ---------- - payload : Mapping[Hashable, Any] + payload : Mapping[str, Any] The dict payload to parse. Returns @@ -297,40 +346,48 @@ def deserialize_audit_log(self, payload: data_binding.JSONObject) -> audit_logs. The parsed audit log object. """ audit_log = audit_logs.AuditLog() + entries = {} for entry_payload in payload["audit_log_entries"]: entry = audit_logs.AuditLogEntry() entry.set_app(self._app) - entry.id = base_models.Snowflake(entry_payload["id"]) + entry.id = snowflake.Snowflake(entry_payload["id"]) + if (target_id := entry_payload["target_id"]) is not None: - target_id = base_models.Snowflake(target_id) + target_id = snowflake.Snowflake(target_id) entry.target_id = target_id + changes = [] for change_payload in entry_payload.get("changes", ()): change = audit_logs.AuditLogChange() + try: + # noinspection PyArgumentList change.key = audit_logs.AuditLogChangeKey(change_payload["key"]) except ValueError: change.key = change_payload["key"] + new_value = change_payload.get("new_value") old_value = change_payload.get("old_value") - value_converter = audit_logs.AUDIT_LOG_ENTRY_CONVERTERS.get( - change.key - ) or self._audit_log_entry_converters.get(change.key) - if value_converter: + if value_converter := self._audit_log_entry_converters.get(change.key): new_value = value_converter(new_value) if new_value is not None else None old_value = value_converter(old_value) if old_value is not None else None change.new_value = new_value change.old_value = old_value + changes.append(change) entry.changes = changes + if (user_id := entry_payload["user_id"]) is not None: - user_id = base_models.Snowflake(user_id) + user_id = snowflake.Snowflake(user_id) entry.user_id = user_id + try: + # noinspection PyArgumentList entry.action_type = audit_logs.AuditLogEventType(entry_payload["action_type"]) except ValueError: entry.action_type = entry_payload["action_type"] + if (options := entry_payload.get("options")) is not None: option_converter = ( self._audit_log_event_mapping.get(entry.action_type) @@ -338,16 +395,18 @@ def deserialize_audit_log(self, payload: data_binding.JSONObject) -> audit_logs. ) options = option_converter(options) entry.options = options + entry.reason = entry_payload.get("reason") entries[entry.id] = entry audit_log.entries = entries + audit_log.integrations = { - base_models.Snowflake(integration["id"]): self.deserialize_partial_integration(integration) + snowflake.Snowflake(integration["id"]): self.deserialize_partial_integration(integration) for integration in payload["integrations"] } - audit_log.users = {base_models.Snowflake(user["id"]): self.deserialize_user(user) for user in payload["users"]} + audit_log.users = {snowflake.Snowflake(user["id"]): self.deserialize_user(user) for user in payload["users"]} audit_log.webhooks = { - base_models.Snowflake(webhook["id"]): self.deserialize_webhook(webhook) for webhook in payload["webhooks"] + snowflake.Snowflake(webhook["id"]): self.deserialize_webhook(webhook) for webhook in payload["webhooks"] } return audit_log @@ -360,7 +419,7 @@ def deserialize_permission_overwrite(self, payload: data_binding.JSONObject) -> Parameters ---------- - payload : Mapping[Hashable, Any] + payload : Mapping[str, Any] The dict payload to parse. Returns @@ -370,8 +429,9 @@ def deserialize_permission_overwrite(self, payload: data_binding.JSONObject) -> """ # noinspection PyArgumentList permission_overwrite = channels_.PermissionOverwrite( - id=base_models.Snowflake(payload["id"]), type=channels_.PermissionOverwriteType(payload["type"]), + id=snowflake.Snowflake(payload["id"]), type=channels_.PermissionOverwriteType(payload["type"]), ) + # noinspection PyArgumentList permission_overwrite.allow = permissions.Permission(payload["allow"]) # noinspection PyArgumentList permission_overwrite.deny = permissions.Permission(payload["deny"]) @@ -396,7 +456,7 @@ def _set_partial_channel_attributes( self, payload: data_binding.JSONObject, channel: PartialChannelT ) -> PartialChannelT: channel.set_app(self._app) - channel.id = base_models.Snowflake(payload["id"]) + channel.id = snowflake.Snowflake(payload["id"]) channel.name = payload.get("name") # noinspection PyArgumentList channel.type = channels_.ChannelType(payload["type"]) @@ -407,7 +467,7 @@ def deserialize_partial_channel(self, payload: data_binding.JSONObject) -> chann Parameters ---------- - payload : Mapping[Hashable, Any] + payload : Mapping[str, Any] The dict payload to parse. Returns @@ -419,11 +479,13 @@ def deserialize_partial_channel(self, payload: data_binding.JSONObject) -> chann def _set_dm_channel_attributes(self, payload: data_binding.JSONObject, channel: DMChannelT) -> DMChannelT: channel = self._set_partial_channel_attributes(payload, channel) + if (last_message_id := payload["last_message_id"]) is not None: - last_message_id = base_models.Snowflake(last_message_id) + last_message_id = snowflake.Snowflake(last_message_id) + channel.last_message_id = last_message_id channel.recipients = { - base_models.Snowflake(user["id"]): self.deserialize_user(user) for user in payload["recipients"] + snowflake.Snowflake(user["id"]): self.deserialize_user(user) for user in payload["recipients"] } return channel @@ -432,7 +494,7 @@ def deserialize_dm_channel(self, payload: data_binding.JSONObject) -> channels_. Parameters ---------- - payload : Mapping[Hashable, Any] + payload : Mapping[str, Any] The dict payload to parse. Returns @@ -447,7 +509,7 @@ def deserialize_group_dm_channel(self, payload: data_binding.JSONObject) -> chan Parameters ---------- - payload : Mapping[Hashable, Any] + payload : Mapping[str, Any] The dict payload to parse. Returns @@ -456,27 +518,29 @@ def deserialize_group_dm_channel(self, payload: data_binding.JSONObject) -> chan The parsed group DM channel object. """ group_dm_channel = self._set_dm_channel_attributes(payload, channels_.GroupDMChannel()) - group_dm_channel.owner_id = base_models.Snowflake(payload["owner_id"]) + group_dm_channel.owner_id = snowflake.Snowflake(payload["owner_id"]) group_dm_channel.icon_hash = payload["icon"] group_dm_channel.application_id = ( - base_models.Snowflake(payload["application_id"]) if "application_id" in payload else None + snowflake.Snowflake(payload["application_id"]) if "application_id" in payload else None ) return group_dm_channel def _set_guild_channel_attributes(self, payload: data_binding.JSONObject, channel: GuildChannelT) -> GuildChannelT: channel = self._set_partial_channel_attributes(payload, channel) - channel.guild_id = base_models.Snowflake(payload["guild_id"]) if "guild_id" in payload else None + channel.guild_id = snowflake.Snowflake(payload["guild_id"]) if "guild_id" in payload else None channel.position = int(payload["position"]) channel.permission_overwrites = { - base_models.Snowflake(overwrite["id"]): self.deserialize_permission_overwrite(overwrite) + snowflake.Snowflake(overwrite["id"]): self.deserialize_permission_overwrite(overwrite) for overwrite in payload["permission_overwrites"] } # TODO: while snowflakes are guaranteed to be unique within their own resource, there is no guarantee for # across between resources (user and role in this case); while in practice we won't get overlap there is a # chance that this may happen in the future, would it be more sensible to use a Sequence here? channel.is_nsfw = payload.get("nsfw") + if (parent_id := payload.get("parent_id")) is not None: - parent_id = base_models.Snowflake(parent_id) + parent_id = snowflake.Snowflake(parent_id) channel.parent_id = parent_id + return channel def deserialize_guild_category(self, payload: data_binding.JSONObject) -> channels_.GuildCategory: @@ -484,7 +548,7 @@ def deserialize_guild_category(self, payload: data_binding.JSONObject) -> channe Parameters ---------- - payload : Mapping[Hashable, Any] + payload : Mapping[str, Any] The dict payload to parse. Returns @@ -499,7 +563,7 @@ def deserialize_guild_text_channel(self, payload: data_binding.JSONObject) -> ch Parameters ---------- - payload : Mapping[Hashable, Any] + payload : Mapping[str, Any] The dict payload to parse. Returns @@ -509,13 +573,17 @@ def deserialize_guild_text_channel(self, payload: data_binding.JSONObject) -> ch """ guild_text_category = self._set_guild_channel_attributes(payload, channels_.GuildTextChannel()) guild_text_category.topic = payload["topic"] + if (last_message_id := payload["last_message_id"]) is not None: - last_message_id = base_models.Snowflake(last_message_id) + last_message_id = snowflake.Snowflake(last_message_id) guild_text_category.last_message_id = last_message_id + guild_text_category.rate_limit_per_user = datetime.timedelta(seconds=payload["rate_limit_per_user"]) + if (last_pin_timestamp := payload.get("last_pin_timestamp")) is not None: last_pin_timestamp = date.iso8601_datetime_string_to_datetime(last_pin_timestamp) guild_text_category.last_pin_timestamp = last_pin_timestamp + return guild_text_category def deserialize_guild_news_channel(self, payload: data_binding.JSONObject) -> channels_.GuildNewsChannel: @@ -523,7 +591,7 @@ def deserialize_guild_news_channel(self, payload: data_binding.JSONObject) -> ch Parameters ---------- - payload : Mapping[Hashable, Any] + payload : Mapping[str, Any] The dict payload to parse. Returns @@ -533,12 +601,15 @@ def deserialize_guild_news_channel(self, payload: data_binding.JSONObject) -> ch """ guild_news_channel = self._set_guild_channel_attributes(payload, channels_.GuildNewsChannel()) guild_news_channel.topic = payload["topic"] + if (last_message_id := payload["last_message_id"]) is not None: - last_message_id = base_models.Snowflake(last_message_id) + last_message_id = snowflake.Snowflake(last_message_id) guild_news_channel.last_message_id = last_message_id + if (last_pin_timestamp := payload.get("last_pin_timestamp")) is not None: last_pin_timestamp = date.iso8601_datetime_string_to_datetime(last_pin_timestamp) guild_news_channel.last_pin_timestamp = last_pin_timestamp + return guild_news_channel def deserialize_guild_store_channel(self, payload: data_binding.JSONObject) -> channels_.GuildStoreChannel: @@ -546,7 +617,7 @@ def deserialize_guild_store_channel(self, payload: data_binding.JSONObject) -> c Parameters ---------- - payload : Mapping[Hashable, Any] + payload : Mapping[str, Any] The dict payload to parse. Returns @@ -561,7 +632,7 @@ def deserialize_guild_voice_channel(self, payload: data_binding.JSONObject) -> c Parameters ---------- - payload : Mapping[Hashable, Any] + payload : Mapping[str, Any] The dict payload to parse. Returns @@ -579,7 +650,7 @@ def deserialize_channel(self, payload: data_binding.JSONObject) -> channels_.Par Parameters ---------- - payload : Mapping[Hashable, Any] + payload : Mapping[str, Any] The dict payload to parse. Returns @@ -598,7 +669,7 @@ def deserialize_embed(self, payload: data_binding.JSONObject) -> embeds.Embed: Parameters ---------- - payload : Mapping[Hashable, Any] + payload : Mapping[str, Any] The dict payload to parse. Returns @@ -614,6 +685,7 @@ def deserialize_embed(self, payload: data_binding.JSONObject) -> embeds.Embed: date.iso8601_datetime_string_to_datetime(payload["timestamp"]) if "timestamp" in payload else None ) embed.color = colors.Color(payload["color"]) if "color" in payload else None + if (footer_payload := payload.get("footer", ...)) is not ...: footer = embeds.EmbedFooter() footer.text = footer_payload["text"] @@ -622,6 +694,7 @@ def deserialize_embed(self, payload: data_binding.JSONObject) -> embeds.Embed: embed.footer = footer else: embed.footer = None + if (image_payload := payload.get("image", ...)) is not ...: image = embeds.EmbedImage() image.url = image_payload.get("url") @@ -631,6 +704,7 @@ def deserialize_embed(self, payload: data_binding.JSONObject) -> embeds.Embed: embed.image = image else: embed.image = None + if (thumbnail_payload := payload.get("thumbnail", ...)) is not ...: thumbnail = embeds.EmbedThumbnail() thumbnail.url = thumbnail_payload.get("url") @@ -640,6 +714,7 @@ def deserialize_embed(self, payload: data_binding.JSONObject) -> embeds.Embed: embed.thumbnail = thumbnail else: embed.thumbnail = None + if (video_payload := payload.get("video", ...)) is not ...: video = embeds.EmbedVideo() video.url = video_payload.get("url") @@ -648,6 +723,7 @@ def deserialize_embed(self, payload: data_binding.JSONObject) -> embeds.Embed: embed.video = video else: embed.video = None + if (provider_payload := payload.get("provider", ...)) is not ...: provider = embeds.EmbedProvider() provider.name = provider_payload.get("name") @@ -655,6 +731,7 @@ def deserialize_embed(self, payload: data_binding.JSONObject) -> embeds.Embed: embed.provider = provider else: embed.provider = None + if (author_payload := payload.get("author", ...)) is not ...: author = embeds.EmbedAuthor() author.name = author_payload.get("name") @@ -664,6 +741,7 @@ def deserialize_embed(self, payload: data_binding.JSONObject) -> embeds.Embed: embed.author = author else: embed.author = None + fields = [] for field_payload in payload.get("fields", ()): field = embeds.EmbedField() @@ -672,6 +750,7 @@ def deserialize_embed(self, payload: data_binding.JSONObject) -> embeds.Embed: field.is_inline = field_payload.get("inline", False) fields.append(field) embed.fields = fields + return embed def serialize_embed(self, embed: embeds.Embed) -> data_binding.JSONObject: @@ -688,50 +767,74 @@ def serialize_embed(self, embed: embeds.Embed) -> data_binding.JSONObject: The dict representation of the provided embed object. """ payload = {} + if embed.title is not None: payload["title"] = embed.title + if embed.description is not None: payload["description"] = embed.description + if embed.url is not None: payload["url"] = embed.url + if embed.timestamp is not None: payload["timestamp"] = embed.timestamp.isoformat() + if embed.color is not None: payload["color"] = embed.color + if embed.footer is not None: footer_payload = {} + if embed.footer.text is not None: footer_payload["text"] = embed.footer.text + if embed.footer.icon_url is not None: footer_payload["icon_url"] = embed.footer.icon_url + payload["footer"] = footer_payload + if embed.image is not None: image_payload = {} + if embed.image.url is not None: image_payload["url"] = embed.image.url + payload["image"] = image_payload + if embed.thumbnail is not None: thumbnail_payload = {} + if embed.thumbnail.url is not None: thumbnail_payload["url"] = embed.thumbnail.url + payload["thumbnail"] = thumbnail_payload + if embed.author is not None: author_payload = {} + if embed.author.name is not None: author_payload["name"] = embed.author.name + if embed.author.url is not None: author_payload["url"] = embed.author.url + if embed.author.icon_url is not None: author_payload["icon_url"] = embed.author.icon_url + payload["author"] = author_payload + if embed.fields: field_payloads = [] for field in embed.fields: field_payload = {} + if field.name: field_payload["name"] = field.name + if field.value: field_payload["value"] = field.value + field_payload["inline"] = field.is_inline field_payloads.append(field_payload) payload["fields"] = field_payloads @@ -747,7 +850,7 @@ def deserialize_unicode_emoji(self, payload: data_binding.JSONObject) -> emojis. Parameters ---------- - payload : Mapping[Hashable, Any] + payload : Mapping[str, Any] The dict payload to parse. Returns @@ -764,7 +867,7 @@ def deserialize_custom_emoji(self, payload: data_binding.JSONObject) -> emojis.C Parameters ---------- - payload : Mapping[Hashable, Any] + payload : Mapping[str, Any] The dict payload to parse. Returns @@ -774,7 +877,7 @@ def deserialize_custom_emoji(self, payload: data_binding.JSONObject) -> emojis.C """ custom_emoji = emojis.CustomEmoji() custom_emoji.set_app(self._app) - custom_emoji.id = base_models.Snowflake(payload["id"]) + custom_emoji.id = snowflake.Snowflake(payload["id"]) custom_emoji.name = payload["name"] custom_emoji.is_animated = payload.get("animated", False) return custom_emoji @@ -784,7 +887,7 @@ def deserialize_known_custom_emoji(self, payload: data_binding.JSONObject) -> em Parameters ---------- - payload : Mapping[Hashable, Any] + payload : Mapping[str, Any] The dict payload to parse. Returns @@ -794,13 +897,15 @@ def deserialize_known_custom_emoji(self, payload: data_binding.JSONObject) -> em """ known_custom_emoji = emojis.KnownCustomEmoji() known_custom_emoji.set_app(self._app) - known_custom_emoji.id = base_models.Snowflake(payload["id"]) + known_custom_emoji.id = snowflake.Snowflake(payload["id"]) known_custom_emoji.name = payload["name"] known_custom_emoji.is_animated = payload.get("animated", False) - known_custom_emoji.role_ids = {base_models.Snowflake(role_id) for role_id in payload.get("roles", ())} + known_custom_emoji.role_ids = {snowflake.Snowflake(role_id) for role_id in payload.get("roles", ())} + if (user := payload.get("user")) is not None: user = self.deserialize_user(user) known_custom_emoji.user = user + known_custom_emoji.is_colons_required = payload["require_colons"] known_custom_emoji.is_managed = payload["managed"] known_custom_emoji.is_available = payload["available"] @@ -813,7 +918,7 @@ def deserialize_emoji( Parameters ---------- - payload : Mapping[Hashable, Any] + payload : Mapping[str, Any] The dict payload to parse. Returns @@ -823,6 +928,7 @@ def deserialize_emoji( """ if payload.get("id") is not None: return self.deserialize_custom_emoji(payload) + return self.deserialize_unicode_emoji(payload) ########### @@ -834,7 +940,7 @@ def deserialize_gateway_bot(self, payload: data_binding.JSONObject) -> gateway.G Parameters ---------- - payload : Mapping[Hashable, Any] + payload : Mapping[str, Any] The dict payload to parse. Returns @@ -862,7 +968,7 @@ def deserialize_guild_widget(self, payload: data_binding.JSONObject) -> guilds.G Parameters ---------- - payload : Mapping[Hashable, Any] + payload : Mapping[str, Any] The dict payload to parse. Returns @@ -872,20 +978,22 @@ def deserialize_guild_widget(self, payload: data_binding.JSONObject) -> guilds.G """ guild_embed = guilds.GuildWidget() guild_embed.set_app(self._app) + if (channel_id := payload["channel_id"]) is not None: - channel_id = base_models.Snowflake(channel_id) + channel_id = snowflake.Snowflake(channel_id) guild_embed.channel_id = channel_id + guild_embed.is_enabled = payload["enabled"] return guild_embed - def deserialize_guild_member( + def deserialize_member( self, payload: data_binding.JSONObject, *, user: typing.Optional[users.User] = None - ) -> guilds.GuildMember: - """Parse a raw payload from Discord into a guild member object. + ) -> guilds.Member: + """Parse a raw payload from Discord into a member object. Parameters ---------- - payload : Mapping[Hashable, Any] + payload : Mapping[str, Any] The dict payload to parse. *, user : hikari.models.users.User? @@ -894,18 +1002,20 @@ def deserialize_guild_member( Returns ------- - hikari.models.guilds.GuildMember - The parsed guild member object. + hikari.models.guilds.Member + The parsed member object. """ - guild_member = guilds.GuildMember() + guild_member = guilds.Member() guild_member.set_app(self._app) guild_member.user = user or self.deserialize_user(payload["user"]) guild_member.nickname = payload.get("nick") - guild_member.role_ids = {base_models.Snowflake(role_id) for role_id in payload["roles"]} + guild_member.role_ids = {snowflake.Snowflake(role_id) for role_id in payload["roles"]} guild_member.joined_at = date.iso8601_datetime_string_to_datetime(payload["joined_at"]) + if (premium_since := payload.get("premium_since")) is not None: premium_since = date.iso8601_datetime_string_to_datetime(premium_since) guild_member.premium_since = premium_since + guild_member.is_deaf = payload["deaf"] guild_member.is_mute = payload["mute"] return guild_member @@ -915,7 +1025,7 @@ def deserialize_role(self, payload: data_binding.JSONObject) -> guilds.Role: Parameters ---------- - payload : Mapping[Hashable, Any] + payload : Mapping[str, Any] The dict payload to parse. Returns @@ -925,7 +1035,7 @@ def deserialize_role(self, payload: data_binding.JSONObject) -> guilds.Role: """ guild_role = guilds.Role() guild_role.set_app(self._app) - guild_role.id = base_models.Snowflake(payload["id"]) + guild_role.id = snowflake.Snowflake(payload["id"]) guild_role.name = payload["name"] guild_role.color = colors.Color(payload["color"]) guild_role.is_hoisted = payload["hoist"] @@ -936,137 +1046,11 @@ def deserialize_role(self, payload: data_binding.JSONObject) -> guilds.Role: guild_role.is_mentionable = payload["mentionable"] return guild_role - def deserialize_guild_member_presence(self, payload: data_binding.JSONObject) -> guilds.GuildMemberPresence: - """Parse a raw payload from Discord into a guild member presence object. - - Parameters - ---------- - payload : Mapping[Hashable, Any] - The dict payload to parse. - - Returns - ------- - hikari.models.guilds.GuildMemberPresence - The parsed guild member presence object. - """ - guild_member_presence = guilds.GuildMemberPresence() - guild_member_presence.set_app(self._app) - user_payload = payload["user"] - user = guilds.PresenceUser() - user.set_app(self._app) - user.id = base_models.Snowflake(user_payload["id"]) - user.discriminator = user_payload["discriminator"] if "discriminator" in user_payload else undefined.Undefined() - user.username = user_payload["username"] if "username" in user_payload else undefined.Undefined() - user.avatar_hash = user_payload["avatar"] if "avatar" in user_payload else undefined.Undefined() - user.is_bot = user_payload["bot"] if "bot" in user_payload else undefined.Undefined() - user.is_system = user_payload["system"] if "system" in user_payload else undefined.Undefined() - # noinspection PyArgumentList - user.flags = ( - users.UserFlag(user_payload["public_flags"]) if "public_flags" in user_payload else undefined.Undefined() - ) - guild_member_presence.user = user - if (role_ids := payload.get("roles", ...)) is not ...: - guild_member_presence.role_ids = {base_models.Snowflake(role_id) for role_id in role_ids} - else: - guild_member_presence.role_ids = None - guild_member_presence.guild_id = base_models.Snowflake(payload["guild_id"]) if "guild_id" in payload else None - # noinspection PyArgumentList - guild_member_presence.visible_status = guilds.PresenceStatus(payload["status"]) - activities = [] - for activity_payload in payload["activities"]: - activity = guilds.PresenceActivity() - activity.name = activity_payload["name"] - # noinspection PyArgumentList - activity.type = guilds.ActivityType(activity_payload["type"]) - activity.url = activity_payload.get("url") - activity.created_at = date.unix_epoch_to_datetime(activity_payload["created_at"]) - if (timestamps_payload := activity_payload.get("timestamps", ...)) is not ...: - timestamps = guilds.ActivityTimestamps() - timestamps.start = ( - date.unix_epoch_to_datetime(timestamps_payload["start"]) if "start" in timestamps_payload else None - ) - timestamps.end = ( - date.unix_epoch_to_datetime(timestamps_payload["end"]) if "end" in timestamps_payload else None - ) - activity.timestamps = timestamps - else: - activity.timestamps = None - activity.application_id = ( - base_models.Snowflake(activity_payload["application_id"]) - if "application_id" in activity_payload - else None - ) - activity.details = activity_payload.get("details") - activity.state = activity_payload.get("state") - if (emoji := activity_payload.get("emoji")) is not None: - emoji = self.deserialize_emoji(emoji) - activity.emoji = emoji - if (party_payload := activity_payload.get("party", ...)) is not ...: - party = guilds.ActivityParty() - party.id = party_payload.get("id") - if (size := party_payload.get("size", ...)) is not ...: - party.current_size = int(size[0]) - party.max_size = int(size[1]) - else: - party.current_size = party.max_size = None - activity.party = party - else: - activity.party = None - if (assets_payload := activity_payload.get("assets", ...)) is not ...: - assets = guilds.ActivityAssets() - assets.large_image = assets_payload.get("large_image") - assets.large_text = assets_payload.get("large_text") - assets.small_image = assets_payload.get("small_image") - assets.small_text = assets_payload.get("small_text") - activity.assets = assets - else: - activity.assets = None - if (secrets_payload := activity_payload.get("secrets", ...)) is not ...: - secret = guilds.ActivitySecret() - secret.join = secrets_payload.get("join") - secret.spectate = secrets_payload.get("spectate") - secret.match = secrets_payload.get("match") - activity.secrets = secret - else: - activity.secrets = None - activity.is_instance = activity_payload.get("instance") # TODO: can we safely default this to False? - # noinspection PyArgumentList - activity.flags = guilds.ActivityFlag(activity_payload["flags"]) if "flags" in activity_payload else None - activities.append(activity) - guild_member_presence.activities = activities - client_status_payload = payload["client_status"] - client_status = guilds.ClientStatus() - # noinspection PyArgumentList - client_status.desktop = ( - guilds.PresenceStatus(client_status_payload["desktop"]) - if "desktop" in client_status_payload - else guilds.PresenceStatus.OFFLINE - ) - # noinspection PyArgumentList - client_status.mobile = ( - guilds.PresenceStatus(client_status_payload["mobile"]) - if "mobile" in client_status_payload - else guilds.PresenceStatus.OFFLINE - ) - # noinspection PyArgumentList - client_status.web = ( - guilds.PresenceStatus(client_status_payload["web"]) - if "web" in client_status_payload - else guilds.PresenceStatus.OFFLINE - ) - guild_member_presence.client_status = client_status - if (premium_since := payload.get("premium_since")) is not None: - premium_since = date.iso8601_datetime_string_to_datetime(premium_since) - # TODO: do we want to differentiate between unset and null here? - guild_member_presence.premium_since = premium_since - guild_member_presence.nickname = payload.get("nick") - return guild_member_presence - @staticmethod def _set_partial_integration_attributes( payload: data_binding.JSONObject, integration: PartialGuildIntegrationT ) -> PartialGuildIntegrationT: - integration.id = base_models.Snowflake(payload["id"]) + integration.id = snowflake.Snowflake(payload["id"]) integration.name = payload["name"] integration.type = payload["type"] account_payload = payload["account"] @@ -1081,7 +1065,7 @@ def deserialize_partial_integration(self, payload: data_binding.JSONObject) -> g Parameters ---------- - payload : Mapping[Hashable, Any] + payload : Mapping[str, Any] The dict payload to parse. Returns @@ -1096,7 +1080,7 @@ def deserialize_integration(self, payload: data_binding.JSONObject) -> guilds.In Parameters ---------- - payload : Mapping[Hashable, Any] + payload : Mapping[str, Any] The dict payload to parse. Returns @@ -1107,17 +1091,21 @@ def deserialize_integration(self, payload: data_binding.JSONObject) -> guilds.In guild_integration = self._set_partial_integration_attributes(payload, guilds.Integration()) guild_integration.is_enabled = payload["enabled"] guild_integration.is_syncing = payload["syncing"] + if (role_id := payload.get("role_id")) is not None: - role_id = base_models.Snowflake(role_id) + role_id = snowflake.Snowflake(role_id) guild_integration.role_id = role_id + guild_integration.is_emojis_enabled = payload.get("enable_emoticons") # noinspection PyArgumentList guild_integration.expire_behavior = guilds.IntegrationExpireBehaviour(payload["expire_behavior"]) guild_integration.expire_grace_period = datetime.timedelta(days=payload["expire_grace_period"]) guild_integration.user = self.deserialize_user(payload["user"]) + if (last_synced_at := payload["synced_at"]) is not None: last_synced_at = date.iso8601_datetime_string_to_datetime(last_synced_at) guild_integration.last_synced_at = last_synced_at + return guild_integration def deserialize_guild_member_ban(self, payload: data_binding.JSONObject) -> guilds.GuildMemberBan: @@ -1125,7 +1113,7 @@ def deserialize_guild_member_ban(self, payload: data_binding.JSONObject) -> guil Parameters ---------- - payload : Mapping[Hashable, Any] + payload : Mapping[str, Any] The dict payload to parse. Returns @@ -1143,7 +1131,7 @@ def deserialize_unavailable_guild(self, payload: data_binding.JSONObject) -> gui Parameters ---------- - payload : Mapping[Hashable, Any] + payload : Mapping[str, Any] The dict payload to parse. Returns @@ -1153,21 +1141,24 @@ def deserialize_unavailable_guild(self, payload: data_binding.JSONObject) -> gui """ unavailable_guild = guilds.UnavailableGuild() unavailable_guild.set_app(self._app) - unavailable_guild.id = base_models.Snowflake(payload["id"]) + unavailable_guild.id = snowflake.Snowflake(payload["id"]) return unavailable_guild def _set_partial_guild_attributes(self, payload: data_binding.JSONObject, guild: PartialGuildT) -> PartialGuildT: guild.set_app(self._app) - guild.id = base_models.Snowflake(payload["id"]) + guild.id = snowflake.Snowflake(payload["id"]) guild.name = payload["name"] guild.icon_hash = payload["icon"] + features = [] for feature in payload["features"]: try: + # noinspection PyArgumentList features.append(guilds.GuildFeature(feature)) except ValueError: features.append(feature) guild.features = set(features) + return guild def deserialize_guild_preview(self, payload: data_binding.JSONObject) -> guilds.GuildPreview: @@ -1175,7 +1166,7 @@ def deserialize_guild_preview(self, payload: data_binding.JSONObject) -> guilds. Parameters ---------- - payload : Mapping[Hashable, Any] + payload : Mapping[str, Any] The dict payload to parse. Returns @@ -1187,8 +1178,7 @@ def deserialize_guild_preview(self, payload: data_binding.JSONObject) -> guilds. guild_preview.splash_hash = payload["splash"] guild_preview.discovery_splash_hash = payload["discovery_splash"] guild_preview.emojis = { - base_models.Snowflake(emoji["id"]): self.deserialize_known_custom_emoji(emoji) - for emoji in payload["emojis"] + snowflake.Snowflake(emoji["id"]): self.deserialize_known_custom_emoji(emoji) for emoji in payload["emojis"] } guild_preview.approximate_presence_count = int(payload["approximate_presence_count"]) guild_preview.approximate_member_count = int(payload["approximate_member_count"]) @@ -1200,7 +1190,7 @@ def deserialize_guild(self, payload: data_binding.JSONObject) -> guilds.Guild: Parameters ---------- - payload : Mapping[Hashable, Any] + payload : Mapping[str, Any] The dict payload to parse. Returns @@ -1211,18 +1201,22 @@ def deserialize_guild(self, payload: data_binding.JSONObject) -> guilds.Guild: guild = self._set_partial_guild_attributes(payload, guilds.Guild()) guild.splash_hash = payload["splash"] guild.discovery_splash_hash = payload["discovery_splash"] - guild.owner_id = base_models.Snowflake(payload["owner_id"]) + guild.owner_id = snowflake.Snowflake(payload["owner_id"]) # noinspection PyArgumentList guild.my_permissions = permissions.Permission(payload["permissions"]) if "permissions" in payload else None guild.region = payload["region"] + if (afk_channel_id := payload["afk_channel_id"]) is not None: - afk_channel_id = base_models.Snowflake(afk_channel_id) + afk_channel_id = snowflake.Snowflake(afk_channel_id) guild.afk_channel_id = afk_channel_id + guild.afk_timeout = datetime.timedelta(seconds=payload["afk_timeout"]) guild.is_embed_enabled = payload.get("embed_enabled", False) + if (embed_channel_id := payload.get("embed_channel_id")) is not None: - embed_channel_id = base_models.Snowflake(embed_channel_id) + embed_channel_id = snowflake.Snowflake(embed_channel_id) guild.embed_channel_id = embed_channel_id + # noinspection PyArgumentList guild.verification_level = guilds.GuildVerificationLevel(payload["verification_level"]) # noinspection PyArgumentList @@ -1231,56 +1225,67 @@ def deserialize_guild(self, payload: data_binding.JSONObject) -> guilds.Guild: ) # noinspection PyArgumentList guild.explicit_content_filter = guilds.GuildExplicitContentFilterLevel(payload["explicit_content_filter"]) - guild.roles = {base_models.Snowflake(role["id"]): self.deserialize_role(role) for role in payload["roles"]} + guild.roles = {snowflake.Snowflake(role["id"]): self.deserialize_role(role) for role in payload["roles"]} guild.emojis = { - base_models.Snowflake(emoji["id"]): self.deserialize_known_custom_emoji(emoji) - for emoji in payload["emojis"] + snowflake.Snowflake(emoji["id"]): self.deserialize_known_custom_emoji(emoji) for emoji in payload["emojis"] } # noinspection PyArgumentList guild.mfa_level = guilds.GuildMFALevel(payload["mfa_level"]) + if (application_id := payload["application_id"]) is not None: - application_id = base_models.Snowflake(application_id) + application_id = snowflake.Snowflake(application_id) guild.application_id = application_id + guild.is_unavailable = payload["unavailable"] if "unavailable" in payload else None guild.is_widget_enabled = payload["widget_enabled"] if "widget_enabled" in payload else None + if (widget_channel_id := payload.get("widget_channel_id")) is not None: - widget_channel_id = base_models.Snowflake(widget_channel_id) + widget_channel_id = snowflake.Snowflake(widget_channel_id) guild.widget_channel_id = widget_channel_id + if (system_channel_id := payload["system_channel_id"]) is not None: - system_channel_id = base_models.Snowflake(system_channel_id) + system_channel_id = snowflake.Snowflake(system_channel_id) guild.system_channel_id = system_channel_id + # noinspection PyArgumentList guild.system_channel_flags = guilds.GuildSystemChannelFlag(payload["system_channel_flags"]) + if (rules_channel_id := payload["rules_channel_id"]) is not None: - rules_channel_id = base_models.Snowflake(rules_channel_id) + rules_channel_id = snowflake.Snowflake(rules_channel_id) guild.rules_channel_id = rules_channel_id + guild.joined_at = ( date.iso8601_datetime_string_to_datetime(payload["joined_at"]) if "joined_at" in payload else None ) guild.is_large = payload["large"] if "large" in payload else None guild.member_count = int(payload["member_count"]) if "member_count" in payload else None + if (members := payload.get("members", ...)) is not ...: guild.members = { - base_models.Snowflake(member["user"]["id"]): self.deserialize_guild_member(member) for member in members + snowflake.Snowflake(member["user"]["id"]): self.deserialize_member(member) for member in members } else: guild.members = None + if (channels := payload.get("channels", ...)) is not ...: guild.channels = { - base_models.Snowflake(channel["id"]): self.deserialize_channel(channel) for channel in channels + snowflake.Snowflake(channel["id"]): self.deserialize_channel(channel) for channel in channels } else: guild.channels = None + if (presences := payload.get("presences", ...)) is not ...: guild.presences = { - base_models.Snowflake(presence["user"]["id"]): self.deserialize_guild_member_presence(presence) + snowflake.Snowflake(presence["user"]["id"]): self.deserialize_member_presence(presence) for presence in presences } else: guild.presences = None + if (max_presences := payload.get("max_presences")) is not None: max_presences = int(max_presences) guild.max_presences = max_presences + guild.max_members = int(payload["max_members"]) if "max_members" in payload else None guild.max_video_channel_users = ( int(payload["max_video_channel_users"]) if "max_video_channel_users" in payload else None @@ -1290,13 +1295,17 @@ def deserialize_guild(self, payload: data_binding.JSONObject) -> guilds.Guild: guild.banner_hash = payload["banner"] # noinspection PyArgumentList guild.premium_tier = guilds.GuildPremiumTier(payload["premium_tier"]) + if (premium_subscription_count := payload.get("premium_subscription_count")) is not None: premium_subscription_count = int(premium_subscription_count) guild.premium_subscription_count = premium_subscription_count + guild.preferred_locale = payload["preferred_locale"] + if (public_updates_channel_id := payload["public_updates_channel_id"]) is not None: - public_updates_channel_id = base_models.Snowflake(public_updates_channel_id) + public_updates_channel_id = snowflake.Snowflake(public_updates_channel_id) guild.public_updates_channel_id = public_updates_channel_id + guild.approximate_member_count = ( int(payload["approximate_member_count"]) if "approximate_member_count" in payload else None ) @@ -1314,7 +1323,7 @@ def deserialize_vanity_url(self, payload: data_binding.JSONObject) -> invites.Va Parameters ---------- - payload : Mapping[Hashable, Any] + payload : Mapping[str, Any] The dict payload to parse. Returns @@ -1331,6 +1340,7 @@ def deserialize_vanity_url(self, payload: data_binding.JSONObject) -> invites.Va def _set_invite_attributes(self, payload: data_binding.JSONObject, invite: InviteT) -> InviteT: invite.set_app(self._app) invite.code = payload["code"] + if (guild_payload := payload.get("guild", ...)) is not ...: guild = self._set_partial_guild_attributes(guild_payload, invites.InviteGuild()) guild.splash_hash = guild_payload["splash"] @@ -1342,6 +1352,7 @@ def _set_invite_attributes(self, payload: data_binding.JSONObject, invite: Invit invite.guild = guild else: invite.guild = None + invite.channel = self.deserialize_partial_channel(payload["channel"]) invite.inviter = self.deserialize_user(payload["inviter"]) if "inviter" in payload else None invite.target_user = self.deserialize_user(payload["target_user"]) if "target_user" in payload else None @@ -1362,7 +1373,7 @@ def deserialize_invite(self, payload: data_binding.JSONObject) -> invites.Invite Parameters ---------- - payload : Mapping[Hashable, Any] + payload : Mapping[str, Any] The dict payload to parse. Returns @@ -1377,7 +1388,7 @@ def deserialize_invite_with_metadata(self, payload: data_binding.JSONObject) -> Parameters ---------- - payload : Mapping[Hashable, Any] + payload : Mapping[str, Any] The dict payload to parse. Returns @@ -1404,7 +1415,7 @@ def deserialize_message(self, payload: data_binding.JSONObject) -> messages.Mess Parameters ---------- - payload : Mapping[Hashable, Any] + payload : Mapping[str, Any] The dict payload to parse. Returns @@ -1414,29 +1425,32 @@ def deserialize_message(self, payload: data_binding.JSONObject) -> messages.Mess """ message = messages.Message() message.set_app(self._app) - message.id = base_models.Snowflake(payload["id"]) - message.channel_id = base_models.Snowflake(payload["channel_id"]) - message.guild_id = base_models.Snowflake(payload["guild_id"]) if "guild_id" in payload else None + message.id = snowflake.Snowflake(payload["id"]) + message.channel_id = snowflake.Snowflake(payload["channel_id"]) + message.guild_id = snowflake.Snowflake(payload["guild_id"]) if "guild_id" in payload else None message.author = self.deserialize_user(payload["author"]) message.member = ( - self.deserialize_guild_member(payload["member"], user=message.author) if "member" in payload else None + self.deserialize_member(payload["member"], user=message.author) if "member" in payload else None ) message.content = payload["content"] message.timestamp = date.iso8601_datetime_string_to_datetime(payload["timestamp"]) + if (edited_timestamp := payload["edited_timestamp"]) is not None: edited_timestamp = date.iso8601_datetime_string_to_datetime(edited_timestamp) message.edited_timestamp = edited_timestamp + message.is_tts = payload["tts"] message.is_mentioning_everyone = payload["mention_everyone"] - message.user_mentions = {base_models.Snowflake(mention["id"]) for mention in payload["mentions"]} - message.role_mentions = {base_models.Snowflake(mention) for mention in payload["mention_roles"]} + message.user_mentions = {snowflake.Snowflake(mention["id"]) for mention in payload["mentions"]} + message.role_mentions = {snowflake.Snowflake(mention) for mention in payload["mention_roles"]} message.channel_mentions = { - base_models.Snowflake(mention["id"]) for mention in payload.get("mention_channels", ()) + snowflake.Snowflake(mention["id"]) for mention in payload.get("mention_channels", ()) } + attachments = [] for attachment_payload in payload["attachments"]: attachment = messages.Attachment() - attachment.id = base_models.Snowflake(attachment_payload["id"]) + attachment.id = snowflake.Snowflake(attachment_payload["id"]) attachment.filename = attachment_payload["filename"] attachment.size = int(attachment_payload["size"]) attachment.url = attachment_payload["url"] @@ -1445,7 +1459,9 @@ def deserialize_message(self, payload: data_binding.JSONObject) -> messages.Mess attachment.width = attachment_payload.get("width") attachments.append(attachment) message.attachments = attachments + message.embeds = [self.deserialize_embed(embed) for embed in payload["embeds"]] + reactions = [] for reaction_payload in payload.get("reactions", ()): reaction = messages.Reaction() @@ -1454,10 +1470,12 @@ def deserialize_message(self, payload: data_binding.JSONObject) -> messages.Mess reaction.is_reacted_by_me = reaction_payload["me"] reactions.append(reaction) message.reactions = reactions + message.is_pinned = payload["pinned"] - message.webhook_id = base_models.Snowflake(payload["webhook_id"]) if "webhook_id" in payload else None + message.webhook_id = snowflake.Snowflake(payload["webhook_id"]) if "webhook_id" in payload else None # noinspection PyArgumentList message.type = messages.MessageType(payload["type"]) + if (activity_payload := payload.get("activity", ...)) is not ...: activity = messages.MessageActivity() # noinspection PyArgumentList @@ -1466,32 +1484,181 @@ def deserialize_message(self, payload: data_binding.JSONObject) -> messages.Mess message.activity = activity else: message.activity = None + message.application = self.deserialize_application(payload["application"]) if "application" in payload else None + if (crosspost_payload := payload.get("message_reference", ...)) is not ...: crosspost = messages.MessageCrosspost() crosspost.set_app(self._app) crosspost.id = ( - base_models.Snowflake(crosspost_payload["message_id"]) if "message_id" in crosspost_payload else None + snowflake.Snowflake(crosspost_payload["message_id"]) if "message_id" in crosspost_payload else None ) - crosspost.channel_id = base_models.Snowflake(crosspost_payload["channel_id"]) + crosspost.channel_id = snowflake.Snowflake(crosspost_payload["channel_id"]) crosspost.guild_id = ( - base_models.Snowflake(crosspost_payload["guild_id"]) if "guild_id" in crosspost_payload else None + snowflake.Snowflake(crosspost_payload["guild_id"]) if "guild_id" in crosspost_payload else None ) message.message_reference = crosspost else: message.message_reference = None + # noinspection PyArgumentList message.flags = messages.MessageFlag(payload["flags"]) if "flags" in payload else None message.nonce = payload.get("nonce") return message + ############# + # PRESENCES # + ############# + + def deserialize_member_presence(self, payload: data_binding.JSONObject) -> presences_.MemberPresence: + """Parse a raw payload from Discord into a member presence object. + + Parameters + ---------- + payload : Mapping[str, Any] + The dict payload to parse. + + Returns + ------- + hikari.models.guilds.MemberPresence + The parsed member presence object. + """ + guild_member_presence = presences_.MemberPresence() + guild_member_presence.set_app(self._app) + user_payload = payload["user"] + user = presences_.PresenceUser() + user.set_app(self._app) + user.id = snowflake.Snowflake(user_payload["id"]) + user.discriminator = user_payload["discriminator"] if "discriminator" in user_payload else undefined.Undefined() + user.username = user_payload["username"] if "username" in user_payload else undefined.Undefined() + user.avatar_hash = user_payload["avatar"] if "avatar" in user_payload else undefined.Undefined() + user.is_bot = user_payload["bot"] if "bot" in user_payload else undefined.Undefined() + user.is_system = user_payload["system"] if "system" in user_payload else undefined.Undefined() + # noinspection PyArgumentList + user.flags = ( + users.UserFlag(user_payload["public_flags"]) if "public_flags" in user_payload else undefined.Undefined() + ) + guild_member_presence.user = user + + if (role_ids := payload.get("roles", ...)) is not ...: + guild_member_presence.role_ids = {snowflake.Snowflake(role_id) for role_id in role_ids} + else: + guild_member_presence.role_ids = None + + guild_member_presence.guild_id = snowflake.Snowflake(payload["guild_id"]) if "guild_id" in payload else None + # noinspection PyArgumentList + guild_member_presence.visible_status = presences_.PresenceStatus(payload["status"]) + + activities = [] + for activity_payload in payload["activities"]: + activity = presences_.RichActivity() + activity.name = activity_payload["name"] + # noinspection PyArgumentList + activity.type = presences_.ActivityType(activity_payload["type"]) + activity.url = activity_payload.get("url") + activity.created_at = date.unix_epoch_to_datetime(activity_payload["created_at"]) + + if (timestamps_payload := activity_payload.get("timestamps", ...)) is not ...: + timestamps = presences_.ActivityTimestamps() + timestamps.start = ( + date.unix_epoch_to_datetime(timestamps_payload["start"]) if "start" in timestamps_payload else None + ) + timestamps.end = ( + date.unix_epoch_to_datetime(timestamps_payload["end"]) if "end" in timestamps_payload else None + ) + activity.timestamps = timestamps + else: + activity.timestamps = None + + activity.application_id = ( + snowflake.Snowflake(activity_payload["application_id"]) + if "application_id" in activity_payload + else None + ) + activity.details = activity_payload.get("details") + activity.state = activity_payload.get("state") + + if (emoji := activity_payload.get("emoji")) is not None: + emoji = self.deserialize_emoji(emoji) + activity.emoji = emoji + + if (party_payload := activity_payload.get("party", ...)) is not ...: + party = presences_.ActivityParty() + party.id = party_payload.get("id") + + if (size := party_payload.get("size", ...)) is not ...: + party.current_size = int(size[0]) + party.max_size = int(size[1]) + else: + party.current_size = party.max_size = None + + activity.party = party + else: + activity.party = None + + if (assets_payload := activity_payload.get("assets", ...)) is not ...: + assets = presences_.ActivityAssets() + assets.large_image = assets_payload.get("large_image") + assets.large_text = assets_payload.get("large_text") + assets.small_image = assets_payload.get("small_image") + assets.small_text = assets_payload.get("small_text") + activity.assets = assets + else: + activity.assets = None + + if (secrets_payload := activity_payload.get("secrets", ...)) is not ...: + secret = presences_.ActivitySecret() + secret.join = secrets_payload.get("join") + secret.spectate = secrets_payload.get("spectate") + secret.match = secrets_payload.get("match") + activity.secrets = secret + else: + activity.secrets = None + + activity.is_instance = activity_payload.get("instance") # TODO: can we safely default this to False? + # noinspection PyArgumentList + activity.flags = presences_.ActivityFlag(activity_payload["flags"]) if "flags" in activity_payload else None + activities.append(activity) + guild_member_presence.activities = activities + + client_status_payload = payload["client_status"] + client_status = presences_.ClientStatus() + # noinspection PyArgumentList + client_status.desktop = ( + presences_.PresenceStatus(client_status_payload["desktop"]) + if "desktop" in client_status_payload + else presences_.PresenceStatus.OFFLINE + ) + # noinspection PyArgumentList + client_status.mobile = ( + presences_.PresenceStatus(client_status_payload["mobile"]) + if "mobile" in client_status_payload + else presences_.PresenceStatus.OFFLINE + ) + # noinspection PyArgumentList + client_status.web = ( + presences_.PresenceStatus(client_status_payload["web"]) + if "web" in client_status_payload + else presences_.PresenceStatus.OFFLINE + ) + guild_member_presence.client_status = client_status + + # TODO: do we want to differentiate between undefined and null here? + if (premium_since := payload.get("premium_since")) is not None: + premium_since = date.iso8601_datetime_string_to_datetime(premium_since) + guild_member_presence.premium_since = premium_since + + # TODO: do we want to differentiate between undefined and null here? + guild_member_presence.nickname = payload.get("nick") + return guild_member_presence + ######### # USERS # ######### def _set_user_attributes(self, payload: data_binding.JSONObject, user: UserT) -> UserT: user.set_app(self._app) - user.id = base_models.Snowflake(payload["id"]) + user.id = snowflake.Snowflake(payload["id"]) user.discriminator = payload["discriminator"] user.username = payload["username"] user.avatar_hash = payload["avatar"] @@ -1504,7 +1671,7 @@ def deserialize_user(self, payload: data_binding.JSONObject) -> users.User: Parameters ---------- - payload : Mapping[Hashable, Any] + payload : Mapping[str, Any] The dict payload to parse. Returns @@ -1513,6 +1680,7 @@ def deserialize_user(self, payload: data_binding.JSONObject) -> users.User: The parsed user object. """ user = self._set_user_attributes(payload, users.User()) + # noinspection PyArgumentList user.flags = users.UserFlag(payload["public_flags"]) if "public_flags" in payload else users.UserFlag.NONE return user @@ -1521,7 +1689,7 @@ def deserialize_my_user(self, payload: data_binding.JSONObject) -> users.MyUser: Parameters ---------- - payload : Mapping[Hashable, Any] + payload : Mapping[str, Any] The dict payload to parse. Returns @@ -1549,7 +1717,7 @@ def deserialize_voice_state(self, payload: data_binding.JSONObject) -> voices.Vo Parameters ---------- - payload : Mapping[Hashable, Any] + payload : Mapping[str, Any] The dict payload to parse. Returns @@ -1559,12 +1727,14 @@ def deserialize_voice_state(self, payload: data_binding.JSONObject) -> voices.Vo """ voice_state = voices.VoiceState() voice_state.set_app(self._app) - voice_state.guild_id = base_models.Snowflake(payload["guild_id"]) if "guild_id" in payload else None + voice_state.guild_id = snowflake.Snowflake(payload["guild_id"]) if "guild_id" in payload else None + if (channel_id := payload["channel_id"]) is not None: - channel_id = base_models.Snowflake(channel_id) + channel_id = snowflake.Snowflake(channel_id) voice_state.channel_id = channel_id - voice_state.user_id = base_models.Snowflake(payload["user_id"]) - voice_state.member = self.deserialize_guild_member(payload["member"]) if "member" in payload else None + + voice_state.user_id = snowflake.Snowflake(payload["user_id"]) + voice_state.member = self.deserialize_member(payload["member"]) if "member" in payload else None voice_state.session_id = payload["session_id"] voice_state.is_guild_deafened = payload["deaf"] voice_state.is_guild_muted = payload["mute"] @@ -1579,7 +1749,7 @@ def deserialize_voice_region(self, payload: data_binding.JSONObject) -> voices.V Parameters ---------- - payload : Mapping[Hashable, Any] + payload : Mapping[str, Any] The dict payload to parse. Returns @@ -1605,7 +1775,7 @@ def deserialize_webhook(self, payload: data_binding.JSONObject) -> webhooks.Webh Parameters ---------- - payload : Mapping[Hashable, Any] + payload : Mapping[str, Any] The dict payload to parse. Returns @@ -1614,11 +1784,11 @@ def deserialize_webhook(self, payload: data_binding.JSONObject) -> webhooks.Webh The parsed webhook object. """ webhook = webhooks.Webhook() - webhook.id = base_models.Snowflake(payload["id"]) + webhook.id = snowflake.Snowflake(payload["id"]) # noinspection PyArgumentList webhook.type = webhooks.WebhookType(payload["type"]) - webhook.guild_id = base_models.Snowflake(payload["guild_id"]) if "guild_id" in payload else None - webhook.channel_id = base_models.Snowflake(payload["channel_id"]) + webhook.guild_id = snowflake.Snowflake(payload["guild_id"]) if "guild_id" in payload else None + webhook.channel_id = snowflake.Snowflake(payload["channel_id"]) webhook.author = self.deserialize_user(payload["user"]) if "user" in payload else None webhook.name = payload["name"] webhook.avatar_hash = payload["avatar"] diff --git a/hikari/impl/event_manager_core.py b/hikari/impl/event_manager_core.py index 1e68247309..bb69e58f5b 100644 --- a/hikari/impl/event_manager_core.py +++ b/hikari/impl/event_manager_core.py @@ -17,6 +17,7 @@ # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . """A base implementation for an event manager.""" + from __future__ import annotations __all__ = ["EventManagerCore"] diff --git a/hikari/impl/gateway_zookeeper.py b/hikari/impl/gateway_zookeeper.py index eb51433df9..a3b2a67189 100644 --- a/hikari/impl/gateway_zookeeper.py +++ b/hikari/impl/gateway_zookeeper.py @@ -16,6 +16,7 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . + from __future__ import annotations __all__ = ["AbstractGatewayZookeeper"] @@ -41,8 +42,8 @@ if typing.TYPE_CHECKING: from hikari import http_settings from hikari.models import gateway as gateway_models - from hikari.models import guilds from hikari.models import intents as intents_ + from hikari.models import presences class AbstractGatewayZookeeper(app_.IGatewayZookeeper, abc.ABC): @@ -51,10 +52,10 @@ def __init__( *, config: http_settings.HTTPSettings, debug: bool, - initial_activity: typing.Optional[gateway.Activity], + initial_activity: typing.Optional[presences.OwnActivity], initial_idle_since: typing.Optional[datetime.datetime], initial_is_afk: bool, - initial_status: guilds.PresenceStatus, + initial_status: presences.PresenceStatus, intents: typing.Optional[intents_.Intent], large_threshold: int, shard_ids: typing.Set[int], @@ -205,8 +206,8 @@ def sigterm_handler(*_): async def update_presence( self, *, - status: typing.Union[undefined.Undefined, guilds.PresenceStatus] = undefined.Undefined(), - activity: typing.Union[undefined.Undefined, gateway.Activity, None] = undefined.Undefined(), + status: typing.Union[undefined.Undefined, presences.PresenceStatus] = undefined.Undefined(), + activity: typing.Union[undefined.Undefined, presences.OwnActivity, None] = undefined.Undefined(), idle_since: typing.Union[undefined.Undefined, datetime.datetime] = undefined.Undefined(), is_afk: typing.Union[undefined.Undefined, bool] = undefined.Undefined(), ) -> None: diff --git a/hikari/models/__init__.py b/hikari/models/__init__.py index 96afc692af..679a22b40e 100644 --- a/hikari/models/__init__.py +++ b/hikari/models/__init__.py @@ -20,24 +20,25 @@ from __future__ import annotations -from .applications import * -from .audit_logs import * -from .bases import * -from .channels import * -from .colors import * -from .colours import * -from .embeds import * -from .emojis import * -from .files import * -from .gateway import * -from .guilds import * -from .intents import * -from .invites import * -from .messages import * -from .permissions import * -from .users import * -from .voices import * -from .webhooks import * +from hikari.models.applications import * +from hikari.models.audit_logs import * +from hikari.models.bases import * +from hikari.models.channels import * +from hikari.models.colors import * +from hikari.models.colours import * +from hikari.models.embeds import * +from hikari.models.emojis import * +from hikari.models.files import * +from hikari.models.gateway import * +from hikari.models.guilds import * +from hikari.models.intents import * +from hikari.models.invites import * +from hikari.models.messages import * +from hikari.models.permissions import * +from hikari.models.presences import * +from hikari.models.users import * +from hikari.models.voices import * +from hikari.models.webhooks import * __all__ = ( applications.__all__ @@ -55,6 +56,7 @@ + invites.__all__ + messages.__all__ + permissions.__all__ + + presences.__all__ + users.__all__ + voices.__all__ + webhooks.__all__ diff --git a/hikari/models/applications.py b/hikari/models/applications.py index 5b78f15511..ada2786440 100644 --- a/hikari/models/applications.py +++ b/hikari/models/applications.py @@ -43,6 +43,7 @@ if typing.TYPE_CHECKING: from hikari.models import permissions as permissions_ from hikari.models import users + from hikari.utilities import snowflake @enum.unique @@ -258,7 +259,7 @@ class TeamMember(bases.Entity): Will always be `["*"]` until Discord starts using this. """ - team_id: bases.Snowflake = attr.ib(eq=True, hash=True, repr=True) + team_id: snowflake.Snowflake = attr.ib(eq=True, hash=True, repr=True) """The ID of the team this member belongs to.""" user: users.User = attr.ib(eq=True, hash=True, repr=True) @@ -272,10 +273,10 @@ class Team(bases.Entity, bases.Unique): icon_hash: typing.Optional[str] = attr.ib(eq=False, hash=False) """The hash of this team's icon, if set.""" - members: typing.Mapping[bases.Snowflake, TeamMember] = attr.ib(eq=False, hash=False) + members: typing.Mapping[snowflake.Snowflake, TeamMember] = attr.ib(eq=False, hash=False) """The member's that belong to this team.""" - owner_user_id: bases.Snowflake = attr.ib(eq=False, hash=False, repr=True) + owner_user_id: snowflake.Snowflake = attr.ib(eq=False, hash=False, repr=True) """The ID of this team's owner.""" @property @@ -318,7 +319,7 @@ class Application(bases.Entity, bases.Unique): """The name of this application.""" description: str = attr.ib(eq=False, hash=False) - """The description of this application, will be an empty string if unset.""" + """The description of this application, will be an empty string if undefined.""" is_bot_public: typing.Optional[bool] = attr.ib(eq=False, hash=False, repr=True) """Whether the bot associated with this application is public. @@ -347,7 +348,7 @@ class Application(bases.Entity, bases.Unique): summary: str = attr.ib(eq=False, hash=False) """This summary for this application's primary SKU if it's sold on Discord. - Will be an empty string if unset. + Will be an empty string if undefined. """ verify_key: typing.Optional[bytes] = attr.ib(eq=False, hash=False) @@ -361,10 +362,10 @@ class Application(bases.Entity, bases.Unique): ) """This application's team if it belongs to one.""" - guild_id: typing.Optional[bases.Snowflake] = attr.ib(eq=False, hash=False) + guild_id: typing.Optional[snowflake.Snowflake] = attr.ib(eq=False, hash=False) """The ID of the guild this application is linked to if sold on Discord.""" - primary_sku_id: typing.Optional[bases.Snowflake] = attr.ib(eq=False, hash=False) + primary_sku_id: typing.Optional[snowflake.Snowflake] = attr.ib(eq=False, hash=False) """The ID of the primary "Game SKU" of a game that's sold on Discord.""" slug: typing.Optional[str] = attr.ib(eq=False, hash=False) diff --git a/hikari/models/audit_logs.py b/hikari/models/audit_logs.py index 26ca16cb1a..727ef17562 100644 --- a/hikari/models/audit_logs.py +++ b/hikari/models/audit_logs.py @@ -44,14 +44,13 @@ import attr from hikari.models import bases -from hikari.models import colors -from hikari.models import guilds -from hikari.models import permissions -from hikari.models import users as users_ -from hikari.models import webhooks as webhooks_ if typing.TYPE_CHECKING: from hikari.models import channels + from hikari.models import guilds + from hikari.models import users as users_ + from hikari.models import webhooks as webhooks_ + from hikari.utilities import snowflake class AuditLogChangeKey(str, enum.Enum): @@ -118,54 +117,6 @@ def __str__(self) -> str: __repr__ = __str__ -def _deserialize_seconds_timedelta(seconds: typing.Union[str, int]) -> datetime.timedelta: - return datetime.timedelta(seconds=int(seconds)) - - -def _deserialize_day_timedelta(days: typing.Union[str, int]) -> datetime.timedelta: - return datetime.timedelta(days=int(days)) - - -def _deserialize_max_uses(age: int) -> typing.Union[int, float]: - return age if age > 0 else float("inf") - - -def _deserialize_max_age(seconds: int) -> typing.Optional[datetime.timedelta]: - return datetime.timedelta(seconds=seconds) if seconds > 0 else None - - -AUDIT_LOG_ENTRY_CONVERTERS = { - AuditLogChangeKey.OWNER_ID: bases.Snowflake, - AuditLogChangeKey.AFK_CHANNEL_ID: bases.Snowflake, - AuditLogChangeKey.AFK_TIMEOUT: _deserialize_seconds_timedelta, - AuditLogChangeKey.MFA_LEVEL: guilds.GuildMFALevel, - AuditLogChangeKey.VERIFICATION_LEVEL: guilds.GuildVerificationLevel, - AuditLogChangeKey.EXPLICIT_CONTENT_FILTER: guilds.GuildExplicitContentFilterLevel, - AuditLogChangeKey.DEFAULT_MESSAGE_NOTIFICATIONS: guilds.GuildMessageNotificationsLevel, - AuditLogChangeKey.PRUNE_DELETE_DAYS: _deserialize_day_timedelta, - AuditLogChangeKey.WIDGET_CHANNEL_ID: bases.Snowflake, - AuditLogChangeKey.POSITION: int, - AuditLogChangeKey.BITRATE: int, - AuditLogChangeKey.APPLICATION_ID: bases.Snowflake, - AuditLogChangeKey.PERMISSIONS: permissions.Permission, - AuditLogChangeKey.COLOR: colors.Color, - AuditLogChangeKey.ALLOW: permissions.Permission, - AuditLogChangeKey.DENY: permissions.Permission, - AuditLogChangeKey.CHANNEL_ID: bases.Snowflake, - AuditLogChangeKey.INVITER_ID: bases.Snowflake, - AuditLogChangeKey.MAX_USES: _deserialize_max_uses, - AuditLogChangeKey.USES: int, - AuditLogChangeKey.MAX_AGE: _deserialize_max_age, - AuditLogChangeKey.ID: bases.Snowflake, - AuditLogChangeKey.TYPE: str, - AuditLogChangeKey.ENABLE_EMOTICONS: bool, - AuditLogChangeKey.EXPIRE_BEHAVIOR: guilds.IntegrationExpireBehaviour, - AuditLogChangeKey.EXPIRE_GRACE_PERIOD: _deserialize_day_timedelta, - AuditLogChangeKey.RATE_LIMIT_PER_USER: _deserialize_seconds_timedelta, - AuditLogChangeKey.SYSTEM_CHANNEL_ID: bases.Snowflake, -} - - @attr.s(eq=True, hash=False, init=False, kw_only=True, slots=True) class AuditLogChange: """Represents a change made to an audit log entry's target entity.""" @@ -251,10 +202,10 @@ class MessagePinEntryInfo(BaseAuditLogEntryInfo): Will be attached to the message pin and message unpin audit log entries. """ - channel_id: bases.Snowflake = attr.ib(repr=True) + channel_id: snowflake.Snowflake = attr.ib(repr=True) """The ID of the text based channel where a pinned message is being targeted.""" - message_id: bases.Snowflake = attr.ib(repr=True) + message_id: snowflake.Snowflake = attr.ib(repr=True) """The ID of the message that's being pinned or unpinned.""" @@ -281,7 +232,7 @@ class MessageBulkDeleteEntryInfo(BaseAuditLogEntryInfo): class MessageDeleteEntryInfo(MessageBulkDeleteEntryInfo): """Represents extra information attached to the message delete audit entry.""" - channel_id: bases.Snowflake = attr.ib(repr=True) + channel_id: snowflake.Snowflake = attr.ib(repr=True) """The guild text based channel where these message(s) were deleted.""" @@ -297,7 +248,7 @@ class MemberDisconnectEntryInfo(BaseAuditLogEntryInfo): class MemberMoveEntryInfo(MemberDisconnectEntryInfo): """Represents extra information for the voice chat based member move entry.""" - channel_id: bases.Snowflake = attr.ib(repr=True) + channel_id: snowflake.Snowflake = attr.ib(repr=True) """The amount of members who were disconnected from voice in this entry.""" @@ -317,13 +268,13 @@ def __init__(self, payload: typing.Mapping[str, str]) -> None: class AuditLogEntry(bases.Entity, bases.Unique): """Represents an entry in a guild's audit log.""" - target_id: typing.Optional[bases.Snowflake] = attr.ib(eq=False, hash=False) + target_id: typing.Optional[snowflake.Snowflake] = attr.ib(eq=False, hash=False) """The ID of the entity affected by this change, if applicable.""" changes: typing.Sequence[AuditLogChange] = attr.ib(eq=False, hash=False, repr=False) """A sequence of the changes made to `AuditLogEntry.target_id`.""" - user_id: typing.Optional[bases.Snowflake] = attr.ib(eq=False, hash=False) + user_id: typing.Optional[snowflake.Snowflake] = attr.ib(eq=False, hash=False) """The ID of the user who made this change.""" action_type: typing.Union[AuditLogEventType, str] = attr.ib(eq=False, hash=False) @@ -341,16 +292,16 @@ class AuditLogEntry(bases.Entity, bases.Unique): class AuditLog: """Represents a guilds audit log.""" - entries: typing.Mapping[bases.Snowflake, AuditLogEntry] = attr.ib() + entries: typing.Mapping[snowflake.Snowflake, AuditLogEntry] = attr.ib() """A sequence of the audit log's entries.""" - integrations: typing.Mapping[bases.Snowflake, guilds.Integration] = attr.ib() + integrations: typing.Mapping[snowflake.Snowflake, guilds.Integration] = attr.ib() """A mapping of the partial objects of integrations found in this audit log.""" - users: typing.Mapping[bases.Snowflake, users_.User] = attr.ib() + users: typing.Mapping[snowflake.Snowflake, users_.User] = attr.ib() """A mapping of the objects of users found in this audit log.""" - webhooks: typing.Mapping[bases.Snowflake, webhooks_.Webhook] = attr.ib() + webhooks: typing.Mapping[snowflake.Snowflake, webhooks_.Webhook] = attr.ib() """A mapping of the objects of webhooks found in this audit log.""" def __iter__(self) -> typing.Iterable[AuditLogEntry]: diff --git a/hikari/models/bases.py b/hikari/models/bases.py index 546d13e7c3..54f602d36c 100644 --- a/hikari/models/bases.py +++ b/hikari/models/bases.py @@ -20,14 +20,14 @@ from __future__ import annotations -__all__ = ["Entity", "Snowflake", "Unique"] +__all__ = ["Entity", "Unique"] import abc import typing import attr -from hikari.utilities import date as date_ +from hikari.utilities import snowflake if typing.TYPE_CHECKING: import datetime @@ -53,69 +53,6 @@ def set_app(self, app: app_.IApp) -> None: self._app = app -class Snowflake(int): - """A concrete representation of a unique identifier for an object on Discord. - - This object can be treated as a regular `int` for most purposes. - """ - - __slots__ = () - - ___MIN___: Snowflake - ___MAX___: Snowflake - - @staticmethod - def __new__(cls, value: typing.Union[int, str]) -> Snowflake: - return super(Snowflake, cls).__new__(cls, value) - - @property - def created_at(self) -> datetime.datetime: - """When the object was created.""" - epoch = self >> 22 - return date_.discord_epoch_to_datetime(epoch) - - @property - def internal_worker_id(self) -> int: - """ID of the worker that created this snowflake on Discord's systems.""" - return (self & 0x3E0_000) >> 17 - - @property - def internal_process_id(self) -> int: - """ID of the process that created this snowflake on Discord's systems.""" - return (self & 0x1F_000) >> 12 - - @property - def increment(self) -> int: - """Increment of Discord's system when this object was made.""" - return self & 0xFFF - - @classmethod - def from_datetime(cls, date: datetime.datetime) -> Snowflake: - """Get a snowflake object from a datetime object.""" - return cls.from_data(date, 0, 0, 0) - - @classmethod - def min(cls) -> Snowflake: - """Minimum value for a snowflake.""" - if not hasattr(cls, "___MIN___"): - cls.___MIN___ = Snowflake(0) - return cls.___MIN___ - - @classmethod - def max(cls) -> Snowflake: - """Maximum value for a snowflake.""" - if not hasattr(cls, "___MAX___"): - cls.___MAX___ = Snowflake((1 << 63) - 1) - return cls.___MAX___ - - @classmethod - def from_data(cls, timestamp: datetime.datetime, worker_id: int, process_id: int, increment: int) -> Snowflake: - """Convert the pieces of info that comprise an ID into a Snowflake.""" - return cls( - (date_.datetime_to_discord_epoch(timestamp) << 22) | (worker_id << 17) | (process_id << 12) | increment - ) - - @attr.s(eq=True, hash=True, init=False, kw_only=True, slots=False) class Unique(typing.SupportsInt): """A base for an entity that has an integer ID of some sort. @@ -124,7 +61,7 @@ class Unique(typing.SupportsInt): integer ID of the object. """ - id: Snowflake = attr.ib(converter=Snowflake, hash=True, eq=True, repr=True) + id: snowflake.Snowflake = attr.ib(converter=snowflake.Snowflake, hash=True, eq=True, repr=True) """The ID of this entity.""" @property @@ -136,5 +73,5 @@ def __int__(self) -> int: return int(self.id) -UniqueObject = typing.Union[Unique, Snowflake, int, str] +UniqueObject = typing.Union[Unique, snowflake.Snowflake, int, str] """A unique object type-hint.""" diff --git a/hikari/models/channels.py b/hikari/models/channels.py index 5e4b468183..8c1b481438 100644 --- a/hikari/models/channels.py +++ b/hikari/models/channels.py @@ -49,6 +49,7 @@ import datetime from hikari.models import users + from hikari.utilities import snowflake @enum.unique @@ -133,14 +134,14 @@ class PartialChannel(bases.Entity, bases.Unique): class DMChannel(PartialChannel, TextChannel): """Represents a DM channel.""" - last_message_id: typing.Optional[bases.Snowflake] = attr.ib(eq=False, hash=False) + last_message_id: typing.Optional[snowflake.Snowflake] = attr.ib(eq=False, hash=False) """The ID of the last message sent in this channel. !!! note This might point to an invalid or deleted message. """ - recipients: typing.Mapping[bases.Snowflake, users.User] = attr.ib( + recipients: typing.Mapping[snowflake.Snowflake, users.User] = attr.ib( eq=False, hash=False, ) """The recipients of the DM.""" @@ -150,13 +151,13 @@ class DMChannel(PartialChannel, TextChannel): class GroupDMChannel(DMChannel): """Represents a DM group channel.""" - owner_id: bases.Snowflake = attr.ib(eq=False, hash=False, repr=True) + owner_id: snowflake.Snowflake = attr.ib(eq=False, hash=False, repr=True) """The ID of the owner of the group.""" icon_hash: typing.Optional[str] = attr.ib(eq=False, hash=False) """The hash of the icon of the group.""" - application_id: typing.Optional[bases.Snowflake] = attr.ib(eq=False, hash=False) + application_id: typing.Optional[snowflake.Snowflake] = attr.ib(eq=False, hash=False) """The ID of the application that created the group DM, if it's a bot based group DM.""" @property @@ -195,7 +196,7 @@ def format_icon_url(self, *, format_: str = "png", size: int = 4096) -> typing.O class GuildChannel(PartialChannel): """The base for anything that is a guild channel.""" - guild_id: typing.Optional[bases.Snowflake] = attr.ib(eq=False, hash=False, repr=True) + guild_id: typing.Optional[snowflake.Snowflake] = attr.ib(eq=False, hash=False, repr=True) """The ID of the guild the channel belongs to. This will be `None` when received over the gateway in certain events (e.g. @@ -205,7 +206,7 @@ class GuildChannel(PartialChannel): position: int = attr.ib(eq=False, hash=False) """The sorting position of the channel.""" - permission_overwrites: typing.Mapping[bases.Snowflake, PermissionOverwrite] = attr.ib(eq=False, hash=False) + permission_overwrites: typing.Mapping[snowflake.Snowflake, PermissionOverwrite] = attr.ib(eq=False, hash=False) """The permission overwrites for the channel.""" is_nsfw: typing.Optional[bool] = attr.ib(eq=False, hash=False) @@ -215,7 +216,7 @@ class GuildChannel(PartialChannel): Guild Create). """ - parent_id: typing.Optional[bases.Snowflake] = attr.ib(eq=False, hash=False, repr=True) + parent_id: typing.Optional[snowflake.Snowflake] = attr.ib(eq=False, hash=False, repr=True) """The ID of the parent category the channel belongs to.""" @@ -231,7 +232,7 @@ class GuildTextChannel(GuildChannel, TextChannel): topic: typing.Optional[str] = attr.ib(eq=False, hash=False) """The topic of the channel.""" - last_message_id: typing.Optional[bases.Snowflake] = attr.ib(eq=False, hash=False) + last_message_id: typing.Optional[snowflake.Snowflake] = attr.ib(eq=False, hash=False) """The ID of the last message sent in this channel. !!! note @@ -261,7 +262,7 @@ class GuildNewsChannel(GuildChannel, TextChannel): topic: typing.Optional[str] = attr.ib(eq=False, hash=False) """The topic of the channel.""" - last_message_id: typing.Optional[bases.Snowflake] = attr.ib(eq=False, hash=False) + last_message_id: typing.Optional[snowflake.Snowflake] = attr.ib(eq=False, hash=False) """The ID of the last message sent in this channel. !!! note diff --git a/hikari/models/emojis.py b/hikari/models/emojis.py index f19e2e1276..ef0228cc89 100644 --- a/hikari/models/emojis.py +++ b/hikari/models/emojis.py @@ -34,6 +34,7 @@ if typing.TYPE_CHECKING: from hikari.models import users + from hikari.utilities import snowflake @attr.s(eq=True, hash=True, init=False, kw_only=True, slots=True) @@ -243,7 +244,7 @@ class KnownCustomEmoji(CustomEmoji): _are_ part of. Ass a result, it contains a lot more information with it. """ - role_ids: typing.Set[bases.Snowflake] = attr.ib( + role_ids: typing.Set[snowflake.Snowflake] = attr.ib( eq=False, hash=False, ) """The IDs of the roles that are whitelisted to use this emoji. diff --git a/hikari/models/guilds.py b/hikari/models/guilds.py index 53bb541bf7..e2da1deb55 100644 --- a/hikari/models/guilds.py +++ b/hikari/models/guilds.py @@ -21,13 +21,6 @@ from __future__ import annotations __all__ = [ - "ActivityAssets", - "ActivityFlag", - "ActivitySecret", - "ActivityTimestamps", - "ActivityType", - "ActivityParty", - "ClientStatus", "Guild", "GuildWidget", "Role", @@ -39,8 +32,7 @@ "GuildVerificationLevel", "GuildPremiumTier", "GuildPreview", - "GuildMember", - "GuildMemberPresence", + "Member", "Integration", "GuildMemberBan", "IntegrationAccount", @@ -48,9 +40,6 @@ "PartialGuild", "PartialIntegration", "PartialRole", - "PresenceActivity", - "PresenceStatus", - "PresenceUser", "UnavailableGuild", ] @@ -62,7 +51,6 @@ from hikari.models import bases from hikari.models import users from hikari.net import urls -from hikari.utilities import undefined if typing.TYPE_CHECKING: import datetime @@ -71,6 +59,8 @@ from hikari.models import colors from hikari.models import emojis as emojis_ from hikari.models import permissions as permissions_ + from hikari.models import presences + from hikari.utilities import snowflake @enum.unique @@ -217,7 +207,7 @@ class GuildVerificationLevel(int, enum.Enum): class GuildWidget(bases.Entity): """Represents a guild embed.""" - channel_id: typing.Optional[bases.Snowflake] = attr.ib(repr=True) + channel_id: typing.Optional[snowflake.Snowflake] = attr.ib(repr=True) """The ID of the channel the invite for this embed targets, if enabled.""" is_enabled: bool = attr.ib(repr=True) @@ -225,10 +215,10 @@ class GuildWidget(bases.Entity): @attr.s(eq=True, hash=True, init=False, kw_only=True, slots=True) -class GuildMember(bases.Entity): +class Member(bases.Entity): """Used to represent a guild bound member.""" - # TODO: make GuildMember delegate to user and implement a common base class + # TODO: make Member delegate to user and implement a common base class # this allows members and users to be used interchangeably. user: users.User = attr.ib(eq=True, hash=True, repr=True) """This member's user object. @@ -241,7 +231,7 @@ class GuildMember(bases.Entity): ) """This member's nickname, if set.""" - role_ids: typing.Set[bases.Snowflake] = attr.ib( + role_ids: typing.Set[snowflake.Snowflake] = attr.ib( eq=False, hash=False, ) """A sequence of the IDs of the member's current roles.""" @@ -304,355 +294,6 @@ class Role(PartialRole): """Whether this role can be mentioned by all regardless of permissions.""" -@enum.unique -class ActivityType(int, enum.Enum): - """The activity type.""" - - PLAYING = 0 - """Shows up as `Playing `""" - - STREAMING = 1 - - LISTENING = 2 - """Shows up as `Listening to `.""" - - WATCHING = 3 - """Shows up as `Watching `. - - !!! note - this is not officially documented, so will be likely removed in the near - future. - """ - - CUSTOM = 4 - """A custom status. - - To set an emoji with the status, place a unicode emoji or Discord emoji - (`:smiley:`) as the first part of the status activity name. - """ - - -@attr.s(eq=True, hash=False, init=False, kw_only=True, slots=True) -class ActivityTimestamps: - """The datetimes for the start and/or end of an activity session.""" - - start: typing.Optional[datetime.datetime] = attr.ib(repr=True) - """When this activity's session was started, if applicable.""" - - end: typing.Optional[datetime.datetime] = attr.ib(repr=True) - """When this activity's session will end, if applicable.""" - - -@attr.s(eq=True, hash=True, init=False, kw_only=True, slots=True) -class ActivityParty: - """Used to represent activity groups of users.""" - - id: typing.Optional[str] = attr.ib(eq=True, hash=True, repr=True) - """The string id of this party instance, if set.""" - - current_size: typing.Optional[int] = attr.ib(eq=False, hash=False) - """Current size of this party, if applicable.""" - - max_size: typing.Optional[int] = attr.ib(eq=False, hash=False) - """Maximum size of this party, if applicable.""" - - -@attr.s(eq=True, hash=False, init=False, kw_only=True, slots=True) -class ActivityAssets: - """Used to represent possible assets for an activity.""" - - large_image: typing.Optional[str] = attr.ib() - """The ID of the asset's large image, if set.""" - - large_text: typing.Optional[str] = attr.ib() - """The text that'll appear when hovering over the large image, if set.""" - - small_image: typing.Optional[str] = attr.ib() - """The ID of the asset's small image, if set.""" - - small_text: typing.Optional[str] = attr.ib() - """The text that'll appear when hovering over the small image, if set.""" - - -@attr.s(eq=True, hash=False, init=False, kw_only=True, slots=True) -class ActivitySecret: - """The secrets used for interacting with an activity party.""" - - join: typing.Optional[str] = attr.ib() - """The secret used for joining a party, if applicable.""" - - spectate: typing.Optional[str] = attr.ib() - """The secret used for spectating a party, if applicable.""" - - match: typing.Optional[str] = attr.ib() - """The secret used for joining a party, if applicable.""" - - -@enum.unique -class ActivityFlag(enum.IntFlag): - """Flags that describe what an activity includes. - - This can be more than one using bitwise-combinations. - """ - - INSTANCE = 1 << 0 - """Instance""" - - JOIN = 1 << 1 - """Join""" - - SPECTATE = 1 << 2 - """Spectate""" - - JOIN_REQUEST = 1 << 3 - """Join Request""" - - SYNC = 1 << 4 - """Sync""" - - PLAY = 1 << 5 - """Play""" - - -@attr.s(eq=True, hash=False, init=False, kw_only=True, slots=True) -class PresenceActivity: - """Represents an activity that will be attached to a member's presence.""" - - name: str = attr.ib(repr=True) - """The activity's name.""" - - type: ActivityType = attr.ib(repr=True) - """The activity's type.""" - - url: typing.Optional[str] = attr.ib() - """The URL for a `STREAM` type activity, if applicable.""" - - created_at: datetime.datetime = attr.ib() - """When this activity was added to the user's session.""" - - timestamps: typing.Optional[ActivityTimestamps] = attr.ib() - """The timestamps for when this activity's current state will start and - end, if applicable. - """ - - application_id: typing.Optional[bases.Snowflake] = attr.ib() - """The ID of the application this activity is for, if applicable.""" - - details: typing.Optional[str] = attr.ib() - """The text that describes what the activity's target is doing, if set.""" - - state: typing.Optional[str] = attr.ib() - """The current status of this activity's target, if set.""" - - emoji: typing.Union[None, emojis_.UnicodeEmoji, emojis_.CustomEmoji] = attr.ib() - """The emoji of this activity, if it is a custom status and set.""" - - party: typing.Optional[ActivityParty] = attr.ib() - """Information about the party associated with this activity, if set.""" - - assets: typing.Optional[ActivityAssets] = attr.ib() - """Images and their hover over text for the activity.""" - - secrets: typing.Optional[ActivitySecret] = attr.ib() - """Secrets for Rich Presence joining and spectating.""" - - is_instance: typing.Optional[bool] = attr.ib() - """Whether this activity is an instanced game session.""" - - flags: ActivityFlag = attr.ib() - """Flags that describe what the activity includes.""" - - -class PresenceStatus(str, enum.Enum): - """The status of a member.""" - - ONLINE = "online" - """Online/green.""" - - IDLE = "idle" - """Idle/yellow.""" - - DND = "dnd" - """Do not disturb/red.""" - - DO_NOT_DISTURB = DND - """An alias for `PresenceStatus.DND`""" - - OFFLINE = "offline" - """Offline or invisible/grey.""" - - -@attr.s(eq=True, hash=False, init=False, kw_only=True, slots=True) -class ClientStatus: - """The client statuses for this member.""" - - desktop: PresenceStatus = attr.ib(repr=True) - """The status of the target user's desktop session.""" - - mobile: PresenceStatus = attr.ib(repr=True) - """The status of the target user's mobile session.""" - - web: PresenceStatus = attr.ib(repr=True) - """The status of the target user's web session.""" - - -# TODO: should this be an event instead? -@attr.s(eq=True, hash=True, init=False, kw_only=True, slots=True) -class PresenceUser(users.User): - """A user representation specifically used for presence updates. - - !!! warning - Every attribute except `PresenceUser.id` may be as `hikari.models.unset.UNSET` - unless it is specifically being modified for this update. - """ - - discriminator: typing.Union[str, undefined.Undefined] = attr.ib(eq=False, hash=False, repr=True) - """This user's discriminator.""" - - username: typing.Union[str, undefined.Undefined] = attr.ib(eq=False, hash=False, repr=True) - """This user's username.""" - - avatar_hash: typing.Union[None, str, undefined.Undefined] = attr.ib( - eq=False, hash=False, repr=True, - ) - """This user's avatar hash, if set.""" - - is_bot: typing.Union[bool, undefined.Undefined] = attr.ib( - eq=False, hash=False, repr=True, - ) - """Whether this user is a bot account.""" - - is_system: typing.Union[bool, undefined.Undefined] = attr.ib( - eq=False, hash=False, - ) - """Whether this user is a system account.""" - - flags: typing.Union[users.UserFlag, undefined.Undefined] = attr.ib(eq=False, hash=False) - """The public flags for this user.""" - - @property - def avatar_url(self) -> typing.Union[str, undefined.Undefined]: - """URL for this user's avatar if the relevant info is available. - - !!! note - This will be `hikari.models.unset.UNSET` if both `PresenceUser.avatar_hash` - and `PresenceUser.discriminator` are `hikari.models.unset.UNSET`. - """ - return self.format_avatar_url() - - def format_avatar_url( - self, *, format_: typing.Optional[str] = None, size: int = 4096 - ) -> typing.Union[str, undefined.Undefined]: - """Generate the avatar URL for this user's avatar if available. - - Parameters - ---------- - format_ : str - The format to use for this URL, defaults to `png` or `gif`. - Supports `png`, `jpeg`, `jpg`, `webp` and `gif` (when animated). - Will be ignored for default avatars which can only be `png`. - size : int - The size to set for the URL, defaults to `4096`. - Can be any power of two between 16 and 4096. - Will be ignored for default avatars. - - Returns - ------- - hikari.models.unset.UNSET | str - The string URL of the user's custom avatar if - either `PresenceUser.avatar_hash` is set or their default avatar if - `PresenceUser.discriminator` is set, else `hikari.models.unset.UNSET`. - - Raises - ------ - ValueError - If `size` is not a power of two or not between 16 and 4096. - """ - if self.discriminator is not undefined.Undefined() or self.avatar_hash is not undefined.Undefined(): - return super().format_avatar_url(format_=format_, size=size) - return undefined.Undefined() - - @property - def default_avatar_index(self) -> typing.Union[int, undefined.Undefined]: - """Integer representation of this user's default avatar. - - !!! note - This will be `hikari.models.unset.UNSET` if `PresenceUser.discriminator` is - `hikari.models.unset.UNSET`. - """ - if self.discriminator is not undefined.Undefined(): - return super().default_avatar_index - return undefined.Undefined() - - @property - def default_avatar_url(self) -> typing.Union[str, undefined.Undefined]: - """URL for this user's default avatar. - - !!! note - This will be `hikari.models.unset.UNSET` if `PresenceUser.discriminator` is - `hikari.models.unset.UNSET`. - """ - if self.discriminator is not undefined.Undefined(): - return super().default_avatar_url - return undefined.Undefined() - - -@attr.s(eq=True, hash=True, init=False, kw_only=True, slots=True) -class GuildMemberPresence(bases.Entity): - """Used to represent a guild member's presence.""" - - user: PresenceUser = attr.ib(eq=True, hash=True, repr=True) - """The object of the user who this presence is for. - - !!! info - Only `PresenceUser.id` is guaranteed for this partial object, - with other attributes only being included when when they are being - changed in an event. - """ - - role_ids: typing.Optional[typing.Sequence[bases.Snowflake]] = attr.ib( - eq=False, hash=False, - ) - """The ids of the user's current roles in the guild this presence belongs to. - - !!! info - If this is `None` then this information wasn't provided and is unknown. - """ - - guild_id: typing.Optional[bases.Snowflake] = attr.ib(eq=True, hash=True, repr=True) - """The ID of the guild this presence belongs to. - - This will be `None` when received in an array of members attached to a guild - object (e.g on Guild Create). - """ - - visible_status: PresenceStatus = attr.ib(eq=False, hash=False, repr=True) - """This user's current status being displayed by the client.""" - - activities: typing.Sequence[PresenceActivity] = attr.ib(eq=False, hash=False) - """An array of the user's activities, with the top one will being - prioritised by the client. - """ - - client_status: ClientStatus = attr.ib( - eq=False, hash=False, - ) - """An object of the target user's client statuses.""" - - premium_since: typing.Optional[datetime.datetime] = attr.ib( - eq=False, hash=False, - ) - """The datetime of when this member started "boosting" this guild. - - This will be `None` if they aren't boosting. - """ - - nickname: typing.Optional[str] = attr.ib( - eq=False, hash=False, repr=True, - ) - """This member's nickname, if set.""" - - @enum.unique class IntegrationExpireBehaviour(int, enum.Enum): """Behavior for expiring integration subscribers.""" @@ -699,7 +340,7 @@ class Integration(PartialIntegration): is_syncing: bool = attr.ib(eq=False, hash=False) """Whether this integration is syncing subscribers/emojis.""" - role_id: typing.Optional[bases.Snowflake] = attr.ib(eq=False, hash=False) + role_id: typing.Optional[snowflake.Snowflake] = attr.ib(eq=False, hash=False) """The ID of the managed role used for this integration's subscribers.""" is_emojis_enabled: typing.Optional[bool] = attr.ib(eq=False, hash=False) @@ -813,7 +454,7 @@ class GuildPreview(PartialGuild): ) """The hash of the discovery splash for the guild, if there is one.""" - emojis: typing.Mapping[bases.Snowflake, emojis_.KnownCustomEmoji] = attr.ib( + emojis: typing.Mapping[snowflake.Snowflake, emojis_.KnownCustomEmoji] = attr.ib( eq=False, hash=False, ) """The mapping of IDs to the emojis this guild provides.""" @@ -909,7 +550,7 @@ class Guild(PartialGuild): # pylint:disable=too-many-instance-attributes discovery_splash_hash: typing.Optional[str] = attr.ib(eq=False, hash=False) """The hash of the discovery splash for the guild, if there is one.""" - owner_id: bases.Snowflake = attr.ib(eq=False, hash=False, repr=True) + owner_id: snowflake.Snowflake = attr.ib(eq=False, hash=False, repr=True) """The ID of the owner of this guild.""" my_permissions: permissions_.Permission = attr.ib( @@ -927,7 +568,7 @@ class Guild(PartialGuild): # pylint:disable=too-many-instance-attributes region: str = attr.ib(eq=False, hash=False) """The voice region for the guild.""" - afk_channel_id: typing.Optional[bases.Snowflake] = attr.ib(eq=False, hash=False) + afk_channel_id: typing.Optional[snowflake.Snowflake] = attr.ib(eq=False, hash=False) """The ID for the channel that AFK voice users get sent to. If `None`, then no AFK channel is set up for this guild. @@ -950,7 +591,7 @@ class Guild(PartialGuild): # pylint:disable=too-many-instance-attributes Use `is_widget_enabled` instead. """ - embed_channel_id: typing.Optional[bases.Snowflake] = attr.ib(eq=False, hash=False) + embed_channel_id: typing.Optional[snowflake.Snowflake] = attr.ib(eq=False, hash=False) """The channel ID that the guild embed will generate an invite to. Will be `None` if invites are disabled for this guild's embed. @@ -968,18 +609,18 @@ class Guild(PartialGuild): # pylint:disable=too-many-instance-attributes explicit_content_filter: GuildExplicitContentFilterLevel = attr.ib(eq=False, hash=False) """The setting for the explicit content filter in this guild.""" - roles: typing.Mapping[bases.Snowflake, Role] = attr.ib( + roles: typing.Mapping[snowflake.Snowflake, Role] = attr.ib( eq=False, hash=False, ) """The roles in this guild, represented as a mapping of ID to role object.""" - emojis: typing.Mapping[bases.Snowflake, emojis_.KnownCustomEmoji] = attr.ib(eq=False, hash=False) + emojis: typing.Mapping[snowflake.Snowflake, emojis_.KnownCustomEmoji] = attr.ib(eq=False, hash=False) """A mapping of IDs to the objects of the emojis this guild provides.""" mfa_level: GuildMFALevel = attr.ib(eq=False, hash=False) """The required MFA level for users wishing to participate in this guild.""" - application_id: typing.Optional[bases.Snowflake] = attr.ib(eq=False, hash=False) + application_id: typing.Optional[snowflake.Snowflake] = attr.ib(eq=False, hash=False) """The ID of the application that created this guild. This will always be `None` for guilds that weren't created by a bot. @@ -1002,14 +643,14 @@ class Guild(PartialGuild): # pylint:disable=too-many-instance-attributes If this information is not present, this will be `None`. """ - widget_channel_id: typing.Optional[bases.Snowflake] = attr.ib(eq=False, hash=False) + widget_channel_id: typing.Optional[snowflake.Snowflake] = attr.ib(eq=False, hash=False) """The channel ID that the widget's generated invite will send the user to. If this information is unavailable or this isn't enabled for the guild then this will be `None`. """ - system_channel_id: typing.Optional[bases.Snowflake] = attr.ib(eq=False, hash=False) + system_channel_id: typing.Optional[snowflake.Snowflake] = attr.ib(eq=False, hash=False) """The ID of the system channel or `None` if it is not enabled. Welcome messages and Nitro boost messages may be sent to this channel. @@ -1018,7 +659,7 @@ class Guild(PartialGuild): # pylint:disable=too-many-instance-attributes system_channel_flags: GuildSystemChannelFlag = attr.ib(eq=False, hash=False) """Flags for the guild system channel to describe which notifications are suppressed.""" - rules_channel_id: typing.Optional[bases.Snowflake] = attr.ib(eq=False, hash=False) + rules_channel_id: typing.Optional[snowflake.Snowflake] = attr.ib(eq=False, hash=False) """The ID of the channel where guilds with the `GuildFeature.PUBLIC` `features` display rules and guidelines. @@ -1052,7 +693,7 @@ class Guild(PartialGuild): # pylint:disable=too-many-instance-attributes `None`. """ - members: typing.Optional[typing.Mapping[bases.Snowflake, GuildMember]] = attr.ib(eq=False, hash=False) + members: typing.Optional[typing.Mapping[snowflake.Snowflake, Member]] = attr.ib(eq=False, hash=False) """A mapping of ID to the corresponding guild members in this guild. This information is only available if the guild was sent via a `GUILD_CREATE` @@ -1071,7 +712,7 @@ class Guild(PartialGuild): # pylint:disable=too-many-instance-attributes query the members using the appropriate API call instead. """ - channels: typing.Optional[typing.Mapping[bases.Snowflake, channels_.GuildChannel]] = attr.ib( + channels: typing.Optional[typing.Mapping[snowflake.Snowflake, channels_.GuildChannel]] = attr.ib( eq=False, hash=False, ) """A mapping of ID to the corresponding guild channels in this guild. @@ -1090,7 +731,7 @@ class Guild(PartialGuild): # pylint:disable=too-many-instance-attributes appropriate API call to retrieve this information. """ - presences: typing.Optional[typing.Mapping[bases.Snowflake, GuildMemberPresence]] = attr.ib( + presences: typing.Optional[typing.Mapping[snowflake.Snowflake, presences.MemberPresence]] = attr.ib( eq=False, hash=False, ) """A mapping of member ID to the corresponding presence information for @@ -1165,7 +806,7 @@ class Guild(PartialGuild): # pylint:disable=too-many-instance-attributes for this guild and will otherwise default to `en-US`. """ - public_updates_channel_id: typing.Optional[bases.Snowflake] = attr.ib(eq=False, hash=False) + public_updates_channel_id: typing.Optional[snowflake.Snowflake] = attr.ib(eq=False, hash=False) """The channel ID of the channel where admins and moderators receive notices from Discord. diff --git a/hikari/models/messages.py b/hikari/models/messages.py index 75e03a32d8..2171a6f151 100644 --- a/hikari/models/messages.py +++ b/hikari/models/messages.py @@ -48,6 +48,7 @@ from hikari.models import emojis as emojis_ from hikari.models import guilds from hikari.models import users + from hikari.utilities import snowflake @enum.unique @@ -197,7 +198,7 @@ class MessageActivity: class MessageCrosspost(bases.Entity, bases.Unique): """Represents information about a cross-posted message and the origin of the original message.""" - id: typing.Optional[bases.Snowflake] = attr.ib(repr=True) + id: typing.Optional[snowflake.Snowflake] = attr.ib(repr=True) """The ID of the message. !!! warning @@ -206,10 +207,10 @@ class MessageCrosspost(bases.Entity, bases.Unique): currently documented. """ - channel_id: bases.Snowflake = attr.ib(repr=True) + channel_id: snowflake.Snowflake = attr.ib(repr=True) """The ID of the channel that the message originated from.""" - guild_id: typing.Optional[bases.Snowflake] = attr.ib(repr=True) + guild_id: typing.Optional[snowflake.Snowflake] = attr.ib(repr=True) """The ID of the guild that the message originated from. !!! warning @@ -223,18 +224,16 @@ class MessageCrosspost(bases.Entity, bases.Unique): class Message(bases.Entity, bases.Unique): """Represents a message.""" - channel_id: bases.Snowflake = attr.ib(eq=False, hash=False, repr=True) + channel_id: snowflake.Snowflake = attr.ib(eq=False, hash=False, repr=True) """The ID of the channel that the message was sent in.""" - guild_id: typing.Optional[bases.Snowflake] = attr.ib(eq=False, hash=False, repr=True) + guild_id: typing.Optional[snowflake.Snowflake] = attr.ib(eq=False, hash=False, repr=True) """The ID of the guild that the message was sent in.""" author: users.User = attr.ib(eq=False, hash=False, repr=True) """The author of this message.""" - member: typing.Optional[guilds.GuildMember] = attr.ib( - eq=False, hash=False, repr=True, - ) + member: typing.Optional[guilds.Member] = attr.ib(eq=False, hash=False, repr=True) """The member properties for the message's author.""" content: str = attr.ib(eq=False, hash=False) @@ -255,56 +254,40 @@ class Message(bases.Entity, bases.Unique): is_mentioning_everyone: bool = attr.ib(eq=False, hash=False) """Whether the message mentions `@everyone` or `@here`.""" - user_mentions: typing.Set[bases.Snowflake] = attr.ib( - eq=False, hash=False, - ) + user_mentions: typing.Set[snowflake.Snowflake] = attr.ib(eq=False, hash=False) """The users the message mentions.""" - role_mentions: typing.Set[bases.Snowflake] = attr.ib( - eq=False, hash=False, - ) + role_mentions: typing.Set[snowflake.Snowflake] = attr.ib(eq=False, hash=False) """The roles the message mentions.""" - channel_mentions: typing.Set[bases.Snowflake] = attr.ib(eq=False, hash=False) + channel_mentions: typing.Set[snowflake.Snowflake] = attr.ib(eq=False, hash=False) """The channels the message mentions.""" - attachments: typing.Sequence[Attachment] = attr.ib( - eq=False, hash=False, - ) + attachments: typing.Sequence[Attachment] = attr.ib(eq=False, hash=False) """The message attachments.""" embeds: typing.Sequence[embeds_.Embed] = attr.ib(eq=False, hash=False) """The message embeds.""" - reactions: typing.Sequence[Reaction] = attr.ib( - eq=False, hash=False, - ) + reactions: typing.Sequence[Reaction] = attr.ib(eq=False, hash=False) """The message reactions.""" is_pinned: bool = attr.ib(eq=False, hash=False) """Whether the message is pinned.""" - webhook_id: typing.Optional[bases.Snowflake] = attr.ib( - eq=False, hash=False, - ) + webhook_id: typing.Optional[snowflake.Snowflake] = attr.ib(eq=False, hash=False) """If the message was generated by a webhook, the webhook's id.""" type: MessageType = attr.ib(eq=False, hash=False) """The message type.""" - activity: typing.Optional[MessageActivity] = attr.ib( - eq=False, hash=False, - ) + activity: typing.Optional[MessageActivity] = attr.ib(eq=False, hash=False) """The message activity.""" - application: typing.Optional[applications.Application] = attr.ib( - eq=False, hash=False, - ) + application: typing.Optional[applications.Application] = attr.ib(eq=False, hash=False) """The message application.""" - message_reference: typing.Optional[MessageCrosspost] = attr.ib( - eq=False, hash=False, - ) + message_reference: typing.Optional[MessageCrosspost] = attr.ib(eq=False, hash=False) """The message crossposted reference data.""" flags: typing.Optional[MessageFlag] = attr.ib(eq=False, hash=False) @@ -340,10 +323,10 @@ async def edit( # pylint:disable=line-too-long embed: embeds_.Embed = ..., mentions_everyone: bool = True, user_mentions: typing.Union[ - typing.Collection[typing.Union[bases.Snowflake, int, str, users.User]], bool + typing.Collection[typing.Union[snowflake.Snowflake, int, str, users.User]], bool ] = True, role_mentions: typing.Union[ - typing.Collection[typing.Union[bases.Snowflake, int, str, guilds.Role]], bool + typing.Collection[typing.Union[snowflake.Snowflake, int, str, guilds.Role]], bool ] = True, ) -> Message: """Edit this message. @@ -360,11 +343,11 @@ async def edit( # pylint:disable=line-too-long mentions_everyone : bool Whether `@everyone` and `@here` mentions should be resolved by discord and lead to actual pings, defaults to `True`. - user_mentions : typing.Collection[hikari.models.users.User | hikari.models.bases.Snowflake | int | str] | bool + user_mentions : typing.Collection[hikari.models.users.User | hikari.models.snowflake.Snowflake | int | str] | bool Either an array of user objects/IDs to allow mentions for, `True` to allow all user mentions or `False` to block all user mentions from resolving, defaults to `True`. - role_mentions: typing.Collection[hikari.models.guilds.Role | hikari.models.bases.Snowflake | int | str] | bool + role_mentions: typing.Collection[hikari.models.guilds.Role | hikari.models.snowflake.Snowflake | int | str] | bool Either an array of guild role objects/IDs to allow mentions for, `True` to allow all role mentions or `False` to block all role mentions from resolving, defaults to `True`. @@ -411,10 +394,10 @@ async def safe_edit( embed: embeds_.Embed = ..., mentions_everyone: bool = False, user_mentions: typing.Union[ - typing.Collection[typing.Union[bases.Snowflake, int, str, users.User]], bool + typing.Collection[typing.Union[snowflake.Snowflake, int, str, users.User]], bool ] = False, role_mentions: typing.Union[ - typing.Collection[typing.Union[bases.Snowflake, int, str, guilds.Role]], bool + typing.Collection[typing.Union[snowflake.Snowflake, int, str, guilds.Role]], bool ] = False, ) -> Message: """Edit this message. @@ -440,10 +423,10 @@ async def reply( # pylint:disable=line-too-long files: typing.Sequence[files_.BaseStream] = ..., mentions_everyone: bool = True, user_mentions: typing.Union[ - typing.Collection[typing.Union[bases.Snowflake, int, str, users.User]], bool + typing.Collection[typing.Union[snowflake.Snowflake, int, str, users.User]], bool ] = True, role_mentions: typing.Union[ - typing.Collection[typing.Union[bases.Snowflake, int, str, guilds.Role]], bool + typing.Collection[typing.Union[snowflake.Snowflake, int, str, guilds.Role]], bool ] = True, nonce: str = ..., tts: bool = ..., @@ -469,11 +452,11 @@ async def reply( # pylint:disable=line-too-long mentions_everyone : bool Whether `@everyone` and `@here` mentions should be resolved by discord and lead to actual pings, defaults to `True`. - user_mentions : typing.Collection[hikari.models.users.User | hikari.models.bases.Snowflake | int | str] | bool + user_mentions : typing.Collection[hikari.models.users.User | hikari.models.snowflake.Snowflake | int | str] | bool Either an array of user objects/IDs to allow mentions for, `True` to allow all user mentions or `False` to block all user mentions from resolving, defaults to `True`. - role_mentions: typing.Collection[hikari.models.guilds.Role | hikari.models.bases.Snowflake | int | str] | bool + role_mentions: typing.Collection[hikari.models.guilds.Role | hikari.models.snowflake.Snowflake | int | str] | bool Either an array of guild role objects/IDs to allow mentions for, `True` to allow all role mentions or `False` to block all role mentions from resolving, defaults to `True`. @@ -523,10 +506,10 @@ async def safe_reply( files: typing.Sequence[files_.BaseStream] = ..., mentions_everyone: bool = False, user_mentions: typing.Union[ - typing.Collection[typing.Union[bases.Snowflake, int, str, users.User]], bool + typing.Collection[typing.Union[snowflake.Snowflake, int, str, users.User]], bool ] = False, role_mentions: typing.Union[ - typing.Collection[typing.Union[bases.Snowflake, int, str, guilds.Role]], bool + typing.Collection[typing.Union[snowflake.Snowflake, int, str, guilds.Role]], bool ] = False, nonce: str = ..., tts: bool = ..., diff --git a/hikari/models/presences.py b/hikari/models/presences.py new file mode 100644 index 0000000000..3c3e4a3fcf --- /dev/null +++ b/hikari/models/presences.py @@ -0,0 +1,418 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# Copyright © Nekoka.tt 2019-2020 +# +# This file is part of Hikari. +# +# Hikari is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Hikari is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Hikari. If not, see . +"""Application and entities that are used to describe guilds on Discord.""" + +from __future__ import annotations + +__all__ = [ + "OwnActivity", + "ActivityAssets", + "ActivityFlag", + "ActivitySecret", + "ActivityTimestamps", + "ActivityType", + "ActivityParty", + "ClientStatus", + "MemberPresence", + "RichActivity", + "PresenceStatus", + "PresenceUser", +] + +import enum +import typing + +import attr + +from hikari.models import bases +from hikari.models import users +from hikari.utilities import undefined + +if typing.TYPE_CHECKING: + import datetime + + from hikari.models import emojis as emojis_ + from hikari.utilities import snowflake + + +@enum.unique +class ActivityType(int, enum.Enum): + """The activity type.""" + + PLAYING = 0 + """Shows up as `Playing `""" + + STREAMING = 1 + + LISTENING = 2 + """Shows up as `Listening to `.""" + + WATCHING = 3 + """Shows up as `Watching `. + + !!! note + this is not officially documented, so will be likely removed in the near + future. + """ + + CUSTOM = 4 + """A custom status. + + To set an emoji with the status, place a unicode emoji or Discord emoji + (`:smiley:`) as the first part of the status activity name. + """ + + +@attr.s(eq=True, hash=False, init=False, kw_only=True, slots=True) +class ActivityTimestamps: + """The datetimes for the start and/or end of an activity session.""" + + start: typing.Optional[datetime.datetime] = attr.ib(repr=True) + """When this activity's session was started, if applicable.""" + + end: typing.Optional[datetime.datetime] = attr.ib(repr=True) + """When this activity's session will end, if applicable.""" + + +@attr.s(eq=True, hash=True, init=False, kw_only=True, slots=True) +class ActivityParty: + """Used to represent activity groups of users.""" + + id: typing.Optional[str] = attr.ib(eq=True, hash=True, repr=True) + """The string id of this party instance, if set.""" + + current_size: typing.Optional[int] = attr.ib(eq=False, hash=False) + """Current size of this party, if applicable.""" + + max_size: typing.Optional[int] = attr.ib(eq=False, hash=False) + """Maximum size of this party, if applicable.""" + + +@attr.s(eq=True, hash=False, init=False, kw_only=True, slots=True) +class ActivityAssets: + """Used to represent possible assets for an activity.""" + + large_image: typing.Optional[str] = attr.ib() + """The ID of the asset's large image, if set.""" + + large_text: typing.Optional[str] = attr.ib() + """The text that'll appear when hovering over the large image, if set.""" + + small_image: typing.Optional[str] = attr.ib() + """The ID of the asset's small image, if set.""" + + small_text: typing.Optional[str] = attr.ib() + """The text that'll appear when hovering over the small image, if set.""" + + +@attr.s(eq=True, hash=False, init=False, kw_only=True, slots=True) +class ActivitySecret: + """The secrets used for interacting with an activity party.""" + + join: typing.Optional[str] = attr.ib() + """The secret used for joining a party, if applicable.""" + + spectate: typing.Optional[str] = attr.ib() + """The secret used for spectating a party, if applicable.""" + + match: typing.Optional[str] = attr.ib() + """The secret used for joining a party, if applicable.""" + + +@enum.unique +class ActivityFlag(enum.IntFlag): + """Flags that describe what an activity includes. + + This can be more than one using bitwise-combinations. + """ + + INSTANCE = 1 << 0 + """Instance""" + + JOIN = 1 << 1 + """Join""" + + SPECTATE = 1 << 2 + """Spectate""" + + JOIN_REQUEST = 1 << 3 + """Join Request""" + + SYNC = 1 << 4 + """Sync""" + + PLAY = 1 << 5 + """Play""" + + +@attr.s(eq=True, hash=False, init=False, kw_only=True, slots=True) +class RichActivity: + """Represents an activity that will be attached to a member's presence.""" + + name: str = attr.ib(repr=True) + """The activity's name.""" + + type: ActivityType = attr.ib(repr=True) + """The activity's type.""" + + url: typing.Optional[str] = attr.ib() + """The URL for a `STREAM` type activity, if applicable.""" + + created_at: datetime.datetime = attr.ib() + """When this activity was added to the user's session.""" + + timestamps: typing.Optional[ActivityTimestamps] = attr.ib() + """The timestamps for when this activity's current state will start and + end, if applicable. + """ + + application_id: typing.Optional[snowflake.Snowflake] = attr.ib() + """The ID of the application this activity is for, if applicable.""" + + details: typing.Optional[str] = attr.ib() + """The text that describes what the activity's target is doing, if set.""" + + state: typing.Optional[str] = attr.ib() + """The current status of this activity's target, if set.""" + + emoji: typing.Union[None, emojis_.UnicodeEmoji, emojis_.CustomEmoji] = attr.ib() + """The emoji of this activity, if it is a custom status and set.""" + + party: typing.Optional[ActivityParty] = attr.ib() + """Information about the party associated with this activity, if set.""" + + assets: typing.Optional[ActivityAssets] = attr.ib() + """Images and their hover over text for the activity.""" + + secrets: typing.Optional[ActivitySecret] = attr.ib() + """Secrets for Rich Presence joining and spectating.""" + + is_instance: typing.Optional[bool] = attr.ib() + """Whether this activity is an instanced game session.""" + + flags: ActivityFlag = attr.ib() + """Flags that describe what the activity includes.""" + + +class PresenceStatus(str, enum.Enum): + """The status of a member.""" + + ONLINE = "online" + """Online/green.""" + + IDLE = "idle" + """Idle/yellow.""" + + DND = "dnd" + """Do not disturb/red.""" + + DO_NOT_DISTURB = DND + """An alias for `PresenceStatus.DND`""" + + OFFLINE = "offline" + """Offline or invisible/grey.""" + + +@attr.s(eq=True, hash=False, init=False, kw_only=True, slots=True) +class ClientStatus: + """The client statuses for this member.""" + + desktop: PresenceStatus = attr.ib(repr=True) + """The status of the target user's desktop session.""" + + mobile: PresenceStatus = attr.ib(repr=True) + """The status of the target user's mobile session.""" + + web: PresenceStatus = attr.ib(repr=True) + """The status of the target user's web session.""" + + +# TODO: should this be an event instead? +@attr.s(eq=True, hash=True, init=False, kw_only=True, slots=True) +class PresenceUser(users.User): + """A user representation specifically used for presence updates. + + !!! warning + Every attribute except `PresenceUser.id` may be as + `hikari.utilities.undefined.Undefined` unless it is specifically being modified + for this update. + """ + + discriminator: typing.Union[str, undefined.Undefined] = attr.ib(eq=False, hash=False, repr=True) + """This user's discriminator.""" + + username: typing.Union[str, undefined.Undefined] = attr.ib(eq=False, hash=False, repr=True) + """This user's username.""" + + avatar_hash: typing.Union[None, str, undefined.Undefined] = attr.ib( + eq=False, hash=False, repr=True, + ) + """This user's avatar hash, if set.""" + + is_bot: typing.Union[bool, undefined.Undefined] = attr.ib( + eq=False, hash=False, repr=True, + ) + """Whether this user is a bot account.""" + + is_system: typing.Union[bool, undefined.Undefined] = attr.ib( + eq=False, hash=False, + ) + """Whether this user is a system account.""" + + flags: typing.Union[users.UserFlag, undefined.Undefined] = attr.ib(eq=False, hash=False) + """The public flags for this user.""" + + @property + def avatar_url(self) -> typing.Union[str, undefined.Undefined]: + """URL for this user's avatar if the relevant info is available. + + !!! note + This will be `hikari.models.undefined.Undefined` if both `PresenceUser.avatar_hash` + and `PresenceUser.discriminator` are `hikari.models.undefined.Undefined`. + """ + return self.format_avatar_url() + + def format_avatar_url( + self, *, format_: typing.Optional[str] = None, size: int = 4096 + ) -> typing.Union[str, undefined.Undefined]: + """Generate the avatar URL for this user's avatar if available. + + Parameters + ---------- + format_ : str + The format to use for this URL, defaults to `png` or `gif`. + Supports `png`, `jpeg`, `jpg`, `webp` and `gif` (when animated). + Will be ignored for default avatars which can only be `png`. + size : int + The size to set for the URL, defaults to `4096`. + Can be any power of two between 16 and 4096. + Will be ignored for default avatars. + + Returns + ------- + hikari.models.undefined.Undefined | str + The string URL of the user's custom avatar if + either `PresenceUser.avatar_hash` is set or their default avatar if + `PresenceUser.discriminator` is set, else `hikari.models.undefined.Undefined`. + + Raises + ------ + ValueError + If `size` is not a power of two or not between 16 and 4096. + """ + if self.discriminator is not undefined.Undefined() or self.avatar_hash is not undefined.Undefined(): + return super().format_avatar_url(format_=format_, size=size) + return undefined.Undefined() + + @property + def default_avatar_index(self) -> typing.Union[int, undefined.Undefined]: + """Integer representation of this user's default avatar. + + !!! note + This will be `hikari.models.undefined.Undefined` if `PresenceUser.discriminator` is + `hikari.models.undefined.Undefined`. + """ + if self.discriminator is not undefined.Undefined(): + return super().default_avatar_index + return undefined.Undefined() + + @property + def default_avatar_url(self) -> typing.Union[str, undefined.Undefined]: + """URL for this user's default avatar. + + !!! note + This will be `hikari.models.undefined.Undefined` if `PresenceUser.discriminator` is + `hikari.models.undefined.Undefined`. + """ + if self.discriminator is not undefined.Undefined(): + return super().default_avatar_url + return undefined.Undefined() + + +@attr.s(eq=True, hash=True, init=False, kw_only=True, slots=True) +class MemberPresence(bases.Entity): + """Used to represent a guild member's presence.""" + + user: PresenceUser = attr.ib(eq=True, hash=True, repr=True) + """The object of the user who this presence is for. + + !!! info + Only `PresenceUser.id` is guaranteed for this partial object, + with other attributes only being included when when they are being + changed in an event. + """ + + role_ids: typing.Optional[typing.Sequence[snowflake.Snowflake]] = attr.ib( + eq=False, hash=False, + ) + """The ids of the user's current roles in the guild this presence belongs to. + + !!! info + If this is `None` then this information wasn't provided and is unknown. + """ + + guild_id: typing.Optional[snowflake.Snowflake] = attr.ib(eq=True, hash=True, repr=True) + """The ID of the guild this presence belongs to. + + This will be `None` when received in an array of members attached to a guild + object (e.g on Guild Create). + """ + + visible_status: PresenceStatus = attr.ib(eq=False, hash=False, repr=True) + """This user's current status being displayed by the client.""" + + activities: typing.Sequence[RichActivity] = attr.ib(eq=False, hash=False) + """An array of the user's activities, with the top one will being + prioritised by the client. + """ + + client_status: ClientStatus = attr.ib( + eq=False, hash=False, + ) + """An object of the target user's client statuses.""" + + premium_since: typing.Optional[datetime.datetime] = attr.ib( + eq=False, hash=False, + ) + """The datetime of when this member started "boosting" this guild. + + This will be `None` if they aren't boosting. + """ + + nickname: typing.Optional[str] = attr.ib( + eq=False, hash=False, repr=True, + ) + """This member's nickname, if set.""" + + +@attr.s(eq=True, hash=False, kw_only=True, slots=True) +class OwnActivity: + """An activity that the bot can set for one or more shards. + + This will show the activity as the bot's presence. + """ + + name: str = attr.ib() + """The activity name.""" + + url: typing.Optional[str] = attr.ib(default=None) + """The activity URL. Only valid for `STREAMING` activities.""" + + type: ActivityType = attr.ib(converter=ActivityType) + """The activity type.""" diff --git a/hikari/models/voices.py b/hikari/models/voices.py index 7bd2798238..31a13f3b59 100644 --- a/hikari/models/voices.py +++ b/hikari/models/voices.py @@ -30,27 +30,26 @@ if typing.TYPE_CHECKING: from hikari.models import guilds + from hikari.utilities import snowflake @attr.s(eq=True, hash=True, init=False, kw_only=True, slots=True) class VoiceState(bases.Entity): """Represents a user's voice connection status.""" - guild_id: typing.Optional[bases.Snowflake] = attr.ib(eq=False, hash=False, repr=True) + guild_id: typing.Optional[snowflake.Snowflake] = attr.ib(eq=False, hash=False, repr=True) """The ID of the guild this voice state is in, if applicable.""" - channel_id: typing.Optional[bases.Snowflake] = attr.ib(eq=False, hash=False, repr=True) + channel_id: typing.Optional[snowflake.Snowflake] = attr.ib(eq=False, hash=False, repr=True) """The ID of the channel this user is connected to. This will be `None` if they are leaving voice. """ - user_id: bases.Snowflake = attr.ib(eq=False, hash=False, repr=True) + user_id: snowflake.Snowflake = attr.ib(eq=False, hash=False, repr=True) """The ID of the user this voice state is for.""" - member: typing.Optional[guilds.GuildMember] = attr.ib( - eq=False, hash=False, - ) + member: typing.Optional[guilds.Member] = attr.ib(eq=False, hash=False) """The guild member this voice state is for if the voice state is in a guild.""" session_id: str = attr.ib(eq=True, hash=True, repr=True) diff --git a/hikari/models/webhooks.py b/hikari/models/webhooks.py index 1fff05de89..369269272f 100644 --- a/hikari/models/webhooks.py +++ b/hikari/models/webhooks.py @@ -29,6 +29,7 @@ from hikari.models import bases from hikari.net import urls +from hikari.utilities import snowflake if typing.TYPE_CHECKING: from hikari.models import channels as channels_ @@ -62,10 +63,10 @@ class Webhook(bases.Entity, bases.Unique): type: WebhookType = attr.ib(eq=False, hash=False, repr=True) """The type of the webhook.""" - guild_id: typing.Optional[bases.Snowflake] = attr.ib(eq=False, hash=False, repr=True) + guild_id: typing.Optional[snowflake.Snowflake] = attr.ib(eq=False, hash=False, repr=True) """The guild ID of the webhook.""" - channel_id: bases.Snowflake = attr.ib(eq=False, hash=False, repr=True) + channel_id: snowflake.Snowflake = attr.ib(eq=False, hash=False, repr=True) """The channel ID this webhook is for.""" author: typing.Optional[users_.User] = attr.ib( @@ -104,10 +105,10 @@ async def execute( embeds: typing.Sequence[embeds_.Embed] = ..., mentions_everyone: bool = True, user_mentions: typing.Union[ - typing.Collection[typing.Union[bases.Snowflake, int, str, users_.User]], bool + typing.Collection[typing.Union[snowflake.Snowflake, int, str, users_.User]], bool ] = True, role_mentions: typing.Union[ - typing.Collection[typing.Union[bases.Snowflake, int, str, guilds_.Role]], bool + typing.Collection[typing.Union[snowflake.Snowflake, int, str, guilds_.Role]], bool ] = True, ) -> typing.Optional[messages_.Message]: """Execute the webhook to create a message. @@ -135,11 +136,11 @@ async def execute( mentions_everyone : bool Whether `@everyone` and `@here` mentions should be resolved by discord and lead to actual pings, defaults to `True`. - user_mentions : typing.Collection[hikari.models.users.User | hikari.models.bases.Snowflake | int | str] | bool + user_mentions : typing.Collection[hikari.models.users.User | hikari.models.snowflake.Snowflake | int | str] | bool Either an array of user objects/IDs to allow mentions for, `True` to allow all user mentions or `False` to block all user mentions from resolving, defaults to `True`. - role_mentions: typing.Collection[hikari.models.guilds.Role | hikari.models.bases.Snowflake | int | str] | bool + role_mentions: typing.Collection[hikari.models.guilds.Role | hikari.models.snowflake.Snowflake | int | str] | bool Either an array of guild role objects/IDs to allow mentions for, `True` to allow all role mentions or `False` to block all role mentions from resolving, defaults to `True`. @@ -196,10 +197,10 @@ async def safe_execute( embeds: typing.Sequence[embeds_.Embed] = ..., mentions_everyone: bool = False, user_mentions: typing.Union[ - typing.Collection[typing.Union[bases.Snowflake, int, str, users_.User]], bool + typing.Collection[typing.Union[snowflake.Snowflake, int, str, users_.User]], bool ] = False, role_mentions: typing.Union[ - typing.Collection[typing.Union[bases.Snowflake, int, str, guilds_.Role]], bool + typing.Collection[typing.Union[snowflake.Snowflake, int, str, guilds_.Role]], bool ] = False, ) -> typing.Optional[messages_.Message]: """Execute the webhook to create a message with mention safety. @@ -261,7 +262,7 @@ async def edit( *, name: str = ..., avatar: typing.Optional[files_.BaseStream] = ..., - channel: typing.Union[bases.Snowflake, int, str, channels_.GuildChannel] = ..., + channel: typing.Union[snowflake.Snowflake, int, str, channels_.GuildChannel] = ..., reason: str = ..., use_token: typing.Optional[bool] = None, ) -> Webhook: @@ -274,7 +275,7 @@ async def edit( avatar : hikari.models.files.BaseStream | None If specified, the new avatar image. If `None`, then it is removed. - channel : hikari.models.channels.GuildChannel | hikari.models.bases.Snowflake | int + channel : hikari.models.channels.GuildChannel | hikari.models.snowflake.Snowflake | int If specified, the object or ID of the new channel the given webhook should be moved to. reason : str diff --git a/hikari/net/__init__.py b/hikari/net/__init__.py index 1c1502a5ca..1f66191c0c 100644 --- a/hikari/net/__init__.py +++ b/hikari/net/__init__.py @@ -16,3 +16,35 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . +"""The logic used for handling Discord's REST and gateway APIs.""" + +from __future__ import annotations + +# TODO: this exacerbates an undetermined amount of circular import errors +# from hikari.net.buckets import * +# from hikari.net.gateway import * +# from hikari.net.http_client import * +# from hikari.net.iterators import * +# from hikari.net.ratelimits import * +# from hikari.net.rest import * +# from hikari.net.rest_utils import * +# from hikari.net.routes import * +# from hikari.net.tracing import * +# from hikari.net.urls import * +# from hikari.net.user_agents import * +# from hikari.net.voice_gateway import * +# +# __all__ = ( +# buckets.__all__ +# + gateway.__all__ +# + http_client.__all__ +# + iterators.__all__ +# + ratelimits.__all__ +# + rest.__all__ +# + rest_utils.__all__ +# + routes.__all__ +# + tracing.__all__ +# + urls.__all__ +# + user_agents.__all__ +# + voice_gateway.__all__ +# ) diff --git a/hikari/net/buckets.py b/hikari/net/buckets.py index af2843b159..12c9cb46c5 100644 --- a/hikari/net/buckets.py +++ b/hikari/net/buckets.py @@ -20,6 +20,8 @@ from __future__ import annotations +__all__ = ["UNKNOWN_HASH", "RESTBucket", "RESTBucketManager"] + import asyncio import datetime import logging diff --git a/hikari/net/gateway.py b/hikari/net/gateway.py index 083e2d7ee4..17dd824f22 100644 --- a/hikari/net/gateway.py +++ b/hikari/net/gateway.py @@ -35,40 +35,24 @@ from hikari import component from hikari import errors -from hikari import http_settings -from hikari.models import guilds from hikari.net import http_client from hikari.net import ratelimits from hikari.net import user_agents from hikari.utilities import data_binding from hikari.utilities import klass -from hikari.utilities import snowflake from hikari.utilities import undefined if typing.TYPE_CHECKING: import datetime from hikari import app as app_ - from hikari.utilities import aio + from hikari import http_settings from hikari.models import channels + from hikari.models import guilds from hikari.models import intents as intents_ - - -@attr.s(eq=True, hash=False, kw_only=True, slots=True) -class Activity: - """An activity that the bot can set for one or more shards. - - This will show the activity as the bot's presence. - """ - - name: str = attr.ib() - """The activity name.""" - - url: typing.Optional[str] = attr.ib(default=None) - """The activity URL. Only valid for `STREAMING` activities.""" - - type: guilds.ActivityType = attr.ib(converter=guilds.ActivityType) - """The activity type.""" + from hikari.models import presences + from hikari.utilities import snowflake + from hikari.utilities import aio class Gateway(http_client.HTTPClient, component.IComponent): @@ -83,13 +67,13 @@ class Gateway(http_client.HTTPClient, component.IComponent): debug : bool If `True`, each sent and received payload is dumped to the logs. If `False`, only the fact that data has been sent/received will be logged. - initial_activity : Activity | None + initial_activity : hikari.presences.OwnActivity | None The initial activity to appear to have for this shard. initial_idle_since : datetime.datetime | None The datetime to appear to be idle since. initial_is_afk : bool | None Whether to appear to be AFK or not on login. - initial_status : hikari.models.guilds.PresenceStatus | None + initial_status : hikari.models.presences.PresenceStatus | None The initial status to set on login for the shard. intents : hikari.models.intents.Intent | None Collection of intents to use, or `None` to not use intents at all. @@ -171,10 +155,10 @@ def __init__( app: app_.IGatewayConsumer, config: http_settings.HTTPSettings, debug: bool = False, - initial_activity: typing.Optional[Activity] = None, + initial_activity: typing.Optional[presences.OwnActivity] = None, initial_idle_since: typing.Optional[datetime.datetime] = None, initial_is_afk: typing.Optional[bool] = None, - initial_status: typing.Optional[guilds.PresenceStatus] = None, + initial_status: typing.Optional[presences.PresenceStatus] = None, intents: typing.Optional[intents_.Intent] = None, large_threshold: int = 250, shard_id: int = 0, @@ -384,24 +368,24 @@ async def update_presence( *, idle_since: typing.Union[undefined.Undefined, typing.Optional[datetime.datetime]] = undefined.Undefined(), is_afk: typing.Union[undefined.Undefined, bool] = undefined.Undefined(), - activity: typing.Union[undefined.Undefined, typing.Optional[Activity]] = undefined.Undefined(), - status: typing.Union[undefined.Undefined, guilds.PresenceStatus] = undefined.Undefined(), + activity: typing.Union[undefined.Undefined, typing.Optional[presences.OwnActivity]] = undefined.Undefined(), + status: typing.Union[undefined.Undefined, presences.PresenceStatus] = undefined.Undefined(), ) -> None: """Update the presence of the shard user. Parameters ---------- - idle_since : datetime.datetime | None | UNSET - The datetime that the user started being idle. If unset, this + idle_since : datetime.datetime | None | hikari.utilities.undefined.Undefined + The datetime that the user started being idle. If undefined, this will not be changed. - is_afk : bool | UNSET + is_afk : bool | hikari.utilities.undefined.Undefined If `True`, the user is marked as AFK. If `False`, the user is marked - as being active. If unset, this will not be changed. - activity : Activity | None | UNSET - The activity to appear to be playing. If unset, this will not be + as being active. If undefined, this will not be changed. + activity : hikari.models.presences.OwnActivity | None | hikari.utilities.undefined.Undefined + The activity to appear to be playing. If undefined, this will not be changed. - status : hikari.models.guilds.PresenceStatus | UNSET - The web status to show. If unset, this will not be changed. + status : hikari.models.presences.PresenceStatus | hikari.utilities.undefined.Undefined + The web status to show. If undefined, this will not be changed. """ payload = self._build_presence_payload(idle_since, is_afk, activity, status) await self._send_json({"op": self._GatewayOpcode.PRESENCE_UPDATE, "d": payload}) @@ -422,9 +406,9 @@ async def update_voice_state( Parameters ---------- - guild : hikari.models.guilds.PartialGuild | hikari.models.bases.Snowflake | int | str + guild : hikari.models.guilds.PartialGuild | hikari.utilities.snowflake.Snowflake | int | str The guild or guild ID to update the voice state for. - channel : hikari.models.channels.GuildVoiceChannel | hikari.models.bases.Snowflake | int | str | None + channel : hikari.models.channels.GuildVoiceChannel | hikari.utilities.Snowflake | int | str | None The channel or channel ID to update the voice state for. If `None` then the bot will leave the voice channel that it is in for the given guild. @@ -651,8 +635,8 @@ def _build_presence_payload( self, idle_since: typing.Union[undefined.Undefined, typing.Optional[datetime.datetime]] = undefined.Undefined(), is_afk: typing.Union[undefined.Undefined, bool] = undefined.Undefined(), - status: typing.Union[undefined.Undefined, guilds.PresenceStatus] = undefined.Undefined(), - activity: typing.Union[undefined.Undefined, typing.Optional[Activity]] = undefined.Undefined(), + status: typing.Union[undefined.Undefined, presences.PresenceStatus] = undefined.Undefined(), + activity: typing.Union[undefined.Undefined, typing.Optional[presences.OwnActivity]] = undefined.Undefined(), ) -> data_binding.JSONObject: if isinstance(idle_since, undefined.Undefined): idle_since = self._idle_since @@ -663,7 +647,7 @@ def _build_presence_payload( if isinstance(activity, undefined.Undefined): activity = self._activity - activity = typing.cast(typing.Optional[Activity], activity) + activity = typing.cast(typing.Optional[presences.OwnActivity], activity) if activity is None: game = None @@ -677,6 +661,6 @@ def _build_presence_payload( return { "since": idle_since.timestamp() if idle_since is not None else None, "afk": is_afk if is_afk is not None else False, - "status": status.value if status is not None else guilds.PresenceStatus.ONLINE.value, + "status": status.value if status is not None else presences.PresenceStatus.ONLINE.value, "game": game, } diff --git a/hikari/net/http_client.py b/hikari/net/http_client.py index 9fd2118ba8..bcc1367c8c 100644 --- a/hikari/net/http_client.py +++ b/hikari/net/http_client.py @@ -19,6 +19,8 @@ """Base functionality for any HTTP-based network component.""" from __future__ import annotations +__all__ = ["HTTPClient"] + import abc import contextlib import json diff --git a/hikari/net/iterators.py b/hikari/net/iterators.py index c86387717f..18c4f7f8d4 100644 --- a/hikari/net/iterators.py +++ b/hikari/net/iterators.py @@ -337,7 +337,7 @@ async def _next_chunk(self) -> typing.Optional[typing.Generator[applications.Own return (self._app.entity_factory.deserialize_own_guild(g) for g in chunk) -class MemberIterator(_BufferedLazyIterator[guilds.GuildMember]): +class MemberIterator(_BufferedLazyIterator[guilds.Member]): """Implementation of an iterator for retrieving members in a guild.""" __slots__ = ("_app", "_request_call", "_route", "_first_id") @@ -354,7 +354,7 @@ def __init__( self._app = app self._first_id = snowflake.Snowflake.min() - async def _next_chunk(self) -> typing.Optional[typing.Generator[guilds.GuildMember, typing.Any, None]]: + async def _next_chunk(self) -> typing.Optional[typing.Generator[guilds.Member, typing.Any, None]]: query = data_binding.StringMapBuilder() query.put("after", self._first_id) query.put("limit", 100) @@ -366,7 +366,7 @@ async def _next_chunk(self) -> typing.Optional[typing.Generator[guilds.GuildMemb # noinspection PyTypeChecker self._first_id = chunk[-1]["user"]["id"] - return (self._app.entity_factory.deserialize_guild_member(m) for m in chunk) + return (self._app.entity_factory.deserialize_member(m) for m in chunk) class AuditLogIterator(LazyIterator[audit_logs.AuditLog]): diff --git a/hikari/net/rest.py b/hikari/net/rest.py index 3727943be1..6b698d16a1 100644 --- a/hikari/net/rest.py +++ b/hikari/net/rest.py @@ -385,7 +385,7 @@ async def fetch_channel( Parameters ---------- - channel : hikari.models.channels.PartialChannel | hikari.models.bases.Snowflake | int | str + channel : hikari.models.channels.PartialChannel | hikari.utilities.snowflake.Snowflake | int | str The channel object to fetch. This can be an existing reference to a channel object (if you want a more up-to-date representation, or it can be a snowflake representation of the channel ID. @@ -1482,7 +1482,7 @@ async def add_user_to_guild( ] = undefined.Undefined(), mute: typing.Union[undefined.Undefined, bool] = undefined.Undefined(), deaf: typing.Union[undefined.Undefined, bool] = undefined.Undefined(), - ) -> typing.Optional[guilds.GuildMember]: + ) -> typing.Optional[guilds.Member]: route = routes.PUT_GUILD_MEMBER.compile(guild=guild, user=user) body = data_binding.JSONObjectBuilder() body.put("access_token", access_token) @@ -1492,7 +1492,7 @@ async def add_user_to_guild( body.put_snowflake_array("roles", roles) if (response := await self._request(route, body=body)) is not None: - return self._app.entity_factory.deserialize_guild_member(response) + return self._app.entity_factory.deserialize_member(response) else: # User already is in the guild. return None @@ -1842,14 +1842,14 @@ async def reposition_channels( async def fetch_member( self, guild: typing.Union[guilds.Guild, bases.UniqueObject], user: typing.Union[users.User, bases.UniqueObject], - ) -> guilds.GuildMember: + ) -> guilds.Member: route = routes.GET_GUILD_MEMBER.compile(guild=guild, user=user) response = await self._request(route) - return self._app.entity_factory.deserialize_guild_member(response) + return self._app.entity_factory.deserialize_member(response) def fetch_members( self, guild: typing.Union[guilds.Guild, bases.UniqueObject], - ) -> iterators.LazyIterator[guilds.GuildMember]: + ) -> iterators.LazyIterator[guilds.Member]: return iterators.MemberIterator(self._app, self._request, guild) async def edit_member( diff --git a/hikari/net/rest_utils.py b/hikari/net/rest_utils.py index 94a7c62314..a8f79fa4e1 100644 --- a/hikari/net/rest_utils.py +++ b/hikari/net/rest_utils.py @@ -28,17 +28,10 @@ import asyncio import contextlib import datetime -import types import typing import attr -from hikari import app as app_ -from hikari.models import bases -from hikari.models import colors -from hikari.models import files -from hikari.models import guilds -from hikari.models import permissions as permissions_ from hikari.net import routes from hikari.utilities import data_binding from hikari.utilities import date @@ -46,7 +39,15 @@ from hikari.utilities import undefined if typing.TYPE_CHECKING: + import types + + from hikari import app as app_ + from hikari.models import bases from hikari.models import channels + from hikari.models import colors + from hikari.models import files + from hikari.models import guilds + from hikari.models import permissions as permissions_ class TypingIndicator: diff --git a/hikari/net/voice_gateway.py b/hikari/net/voice_gateway.py index 97fcc5e25b..65542214c2 100644 --- a/hikari/net/voice_gateway.py +++ b/hikari/net/voice_gateway.py @@ -32,7 +32,6 @@ import attr from hikari import errors -from hikari.models import bases from hikari.net import http_client from hikari.net import ratelimits from hikari.utilities import data_binding @@ -41,6 +40,7 @@ if typing.TYPE_CHECKING: from hikari import bot from hikari import http_settings + from hikari.models import bases class VoiceGateway(http_client.HTTPClient): diff --git a/hikari/utilities/__init__.py b/hikari/utilities/__init__.py index 1798e98493..970a1c6e91 100644 --- a/hikari/utilities/__init__.py +++ b/hikari/utilities/__init__.py @@ -20,4 +20,20 @@ from __future__ import annotations -__all__ = [] +from hikari.utilities.aio import * +from hikari.utilities.data_binding import * +from hikari.utilities.date import * +from hikari.utilities.klass import * +from hikari.utilities.reflect import * +from hikari.utilities.snowflake import * +from hikari.utilities.undefined import * + +__all__ = [ + *aio.__all__, + *data_binding.__all__, + *date.__all__, + *klass.__all__, + *reflect.__all__, + *snowflake.__all__, + *undefined.__all__, +] diff --git a/hikari/utilities/data_binding.py b/hikari/utilities/data_binding.py index 244eaa27e3..720dd9c666 100644 --- a/hikari/utilities/data_binding.py +++ b/hikari/utilities/data_binding.py @@ -84,10 +84,10 @@ if typing.TYPE_CHECKING: def dump_json(_: typing.Union[JSONArray, JSONObject]) -> str: - ... + """Convert a Python type to a JSON string.""" def load_json(_: str) -> typing.Union[JSONArray, JSONObject]: - ... + """Convert a JSON string to a Python type.""" else: @@ -173,7 +173,7 @@ def put( ) -> None: """Put a JSON value. - If the value is unset, then it will not be stored. + If the value is undefined, then it will not be stored. Parameters ---------- @@ -181,7 +181,7 @@ def put( The key to give the element. value : JSONType | typing.Any | hikari.utilities.undefined.Undefined The JSON type to put. This may be a non-JSON type if a conversion - is also specified. This may alternatively be unset. In the latter + is also specified. This may alternatively be undefined. In the latter case, nothing is performed. conversion : typing.Callable[[typing.Any], JSONType] | None Optional conversion to apply. @@ -200,7 +200,7 @@ def put_array( ) -> None: """Put a JSON array. - If the value is unset, then it will not be stored. + If the value is undefined, then it will not be stored. Parameters ---------- @@ -208,7 +208,7 @@ def put_array( The key to give the element. values : JSONType | typing.Any | hikari.utilities.undefined.Undefined The JSON types to put. This may be an iterable of non-JSON types if - a conversion is also specified. This may alternatively be unset. + a conversion is also specified. This may alternatively be undefined. In the latter case, nothing is performed. conversion : typing.Callable[[typing.Any], JSONType] | None Optional conversion to apply. @@ -227,7 +227,7 @@ def put_snowflake(self, key: JSONString, value: typing.Union[undefined.Undefined key : JSONString The key to give the element. value : JSONType | hikari.utilities.undefined.Undefined - The JSON type to put. This may alternatively be unset. In the latter + The JSON type to put. This may alternatively be undefined. In the latter case, nothing is performed. """ if not isinstance(value, undefined.Undefined): @@ -243,7 +243,7 @@ def put_snowflake_array( key : JSONString The key to give the element. values : typing.Iterable[typing.SupportsInt, int] | hikari.utilities.undefined.Undefined - The JSON snowflakes to put. This may alternatively be unset. In the latter + The JSON snowflakes to put. This may alternatively be undefined. In the latter case, nothing is performed. """ if not isinstance(values, undefined.Undefined): diff --git a/hikari/utilities/date.py b/hikari/utilities/date.py index e36cf485ea..85b4239201 100644 --- a/hikari/utilities/date.py +++ b/hikari/utilities/date.py @@ -16,6 +16,7 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . +"""Utility methods used for parsing timestamps and datetimes.""" from __future__ import annotations diff --git a/hikari/utilities/undefined.py b/hikari/utilities/undefined.py index 1ff4f0003f..4e46473898 100644 --- a/hikari/utilities/undefined.py +++ b/hikari/utilities/undefined.py @@ -80,26 +80,27 @@ class Undefined(klass.Singleton): def __bool__(self) -> bool: return False - def __repr__(self) -> str: - return type(self).__name__.upper() + def __eq__(self, other: typing.Any) -> bool: + return other is self def __iter__(self) -> typing.Iterator[None]: yield from () - __str__ = __repr__ - def __init_subclass__(cls, **kwargs: typing.Any) -> typing.NoReturn: raise TypeError("Cannot subclass Undefined type") + def __repr__(self) -> str: + return f"{type(self).__name__}()" + + def __str__(self) -> str: + return type(self).__name__.upper() + def __setattr__(self, _, __) -> typing.NoReturn: raise TypeError("Cannot modify Undefined type") def __delattr__(self, _) -> typing.NoReturn: raise TypeError("Cannot modify Undefined type") - def __eq__(self, other: typing.Any) -> bool: - return other is self - @staticmethod def count(*objs: typing.Any) -> int: """Count how many of the given objects are undefined values. diff --git a/tests/hikari/_helpers.py b/tests/hikari/_helpers.py index cf46dbf1c0..b96819d266 100644 --- a/tests/hikari/_helpers.py +++ b/tests/hikari/_helpers.py @@ -253,8 +253,8 @@ def parametrize_valid_id_formats_for_models(param_name, id, model_type1, *model_ ... "guild", ... [ ... 1234, - ... bases.Snowflake(1234), - ... mock_model(guilds.Guild, id=bases.Snowflake(1234), unavailable=False) + ... snowflakes.Snowflake(1234), + ... mock_model(guilds.Guild, id=snowflakes.Snowflake(1234), unavailable=False) ... ], ... id=lambda ...: ... ... ) diff --git a/tests/hikari/impl/test_entity_factory.py b/tests/hikari/impl/test_entity_factory.py index 1701bebc3a..13ca9ec24c 100644 --- a/tests/hikari/impl/test_entity_factory.py +++ b/tests/hikari/impl/test_entity_factory.py @@ -34,12 +34,37 @@ from hikari.models import invites from hikari.models import messages from hikari.models import permissions +from hikari.models import presences from hikari.models import webhooks from hikari.models import users from hikari.models import voices from hikari.utilities import undefined +def test__deserialize_seconds_timedelta(): + assert entity_factory._deserialize_seconds_timedelta(30) == datetime.timedelta(seconds=30) + + +def test__deserialize_day_timedelta(): + assert entity_factory._deserialize_day_timedelta("4") == datetime.timedelta(days=4) + + +def test__deserialize_max_uses_returns_int(): + assert entity_factory._deserialize_max_uses(120) == 120 + + +def test__deserialize_max_uses_returns_infinity(): + assert entity_factory._deserialize_max_uses(0) == float("inf") + + +def test__deserialize_max_age_returns_timedelta(): + assert entity_factory._deserialize_max_age(120) == datetime.timedelta(seconds=120) + + +def test__deserialize_max_age_returns_null(): + assert entity_factory._deserialize_max_age(0) is None + + class TestEntityFactoryImpl: @pytest.fixture() def mock_app(self) -> app_.IApp: @@ -1163,7 +1188,7 @@ def test_deserialize_guild_embed_with_null_fields(self, entity_factory_impl, moc assert entity_factory_impl.deserialize_guild_widget({"channel_id": None, "enabled": True}).channel_id is None @pytest.fixture() - def guild_member_payload(self, user_payload): + def member_payload(self, user_payload): return { "nick": "foobarbaz", "roles": ["11111", "22222", "33333", "44444"], @@ -1175,8 +1200,8 @@ def guild_member_payload(self, user_payload): "user": user_payload, } - def test_deserialize_guild_member(self, entity_factory_impl, mock_app, guild_member_payload, user_payload): - member = entity_factory_impl.deserialize_guild_member(guild_member_payload) + def test_deserialize_member(self, entity_factory_impl, mock_app, member_payload, user_payload): + member = entity_factory_impl.deserialize_member(member_payload) assert member._app is mock_app assert member.user == entity_factory_impl.deserialize_user(user_payload) assert member.nickname == "foobarbaz" @@ -1185,7 +1210,7 @@ def test_deserialize_guild_member(self, entity_factory_impl, mock_app, guild_mem assert member.premium_since == datetime.datetime(2019, 5, 17, 6, 26, 56, 936000, tzinfo=datetime.timezone.utc) assert member.is_deaf is False assert member.is_mute is True - assert isinstance(member, guilds.GuildMember) + assert isinstance(member, guilds.Member) @pytest.fixture() def guild_role_payload(self): @@ -1238,7 +1263,7 @@ def presence_activity_payload(self, custom_emoji_payload): } @pytest.fixture() - def guild_member_presence_payload(self, user_payload, presence_activity_payload): + def member_presence_payload(self, user_payload, presence_activity_payload): return { "user": user_payload, "roles": ["49494949"], @@ -1251,10 +1276,10 @@ def guild_member_presence_payload(self, user_payload, presence_activity_payload) "nick": "Nick", } - def test_deserialize_guild_member_presence( - self, entity_factory_impl, mock_app, guild_member_presence_payload, custom_emoji_payload, user_payload + def test_deserialize_member_presence( + self, entity_factory_impl, mock_app, member_presence_payload, custom_emoji_payload, user_payload ): - presence = entity_factory_impl.deserialize_guild_member_presence(guild_member_presence_payload) + presence = entity_factory_impl.deserialize_member_presence(member_presence_payload) assert presence._app is mock_app # PresenceUser assert presence.user._app is mock_app @@ -1266,15 +1291,15 @@ def test_deserialize_guild_member_presence( assert presence.user.is_system is True assert presence.user.flags == users.UserFlag(131072) - assert isinstance(presence.user, guilds.PresenceUser) + assert isinstance(presence.user, presences.PresenceUser) assert presence.role_ids == {49494949} assert presence.guild_id == 44004040 - assert presence.visible_status == guilds.PresenceStatus.DND + assert presence.visible_status == presences.PresenceStatus.DND # PresenceActivity assert len(presence.activities) == 1 activity = presence.activities[0] assert activity.name == "an activity" - assert activity.type == guilds.ActivityType.STREAMING + assert activity.type == presences.ActivityType.STREAMING assert activity.url == "https://69.420.owouwunyaa" assert activity.created_at == datetime.datetime(2020, 3, 23, 20, 53, 12, 798000, tzinfo=datetime.timezone.utc) # ActivityTimestamps @@ -1293,34 +1318,34 @@ def test_deserialize_guild_member_presence( assert activity.party.id == "spotify:3234234234" assert activity.party.current_size == 2 assert activity.party.max_size == 5 - assert isinstance(activity.party, guilds.ActivityParty) + assert isinstance(activity.party, presences.ActivityParty) # ActivityAssets assert activity.assets.large_image == "34234234234243" assert activity.assets.large_text == "LARGE TEXT" assert activity.assets.small_image == "3939393" assert activity.assets.small_text == "small text" - assert isinstance(activity.assets, guilds.ActivityAssets) + assert isinstance(activity.assets, presences.ActivityAssets) # ActivitySecrets assert activity.secrets.join == "who's a good secret?" assert activity.secrets.spectate == "I'm a good secret" assert activity.secrets.match == "No." - assert isinstance(activity.secrets, guilds.ActivitySecret) + assert isinstance(activity.secrets, presences.ActivitySecret) assert activity.is_instance is True - assert activity.flags == guilds.ActivityFlag(3) - assert isinstance(activity, guilds.PresenceActivity) + assert activity.flags == presences.ActivityFlag(3) + assert isinstance(activity, presences.RichActivity) # ClientStatus - assert presence.client_status.desktop == guilds.PresenceStatus.ONLINE - assert presence.client_status.mobile == guilds.PresenceStatus.IDLE - assert presence.client_status.web == guilds.PresenceStatus.DND - assert isinstance(presence.client_status, guilds.ClientStatus) + assert presence.client_status.desktop == presences.PresenceStatus.ONLINE + assert presence.client_status.mobile == presences.PresenceStatus.IDLE + assert presence.client_status.web == presences.PresenceStatus.DND + assert isinstance(presence.client_status, presences.ClientStatus) assert presence.premium_since == datetime.datetime(2015, 4, 26, 6, 26, 56, 936000, tzinfo=datetime.timezone.utc) assert presence.nickname == "Nick" - assert isinstance(presence, guilds.GuildMemberPresence) + assert isinstance(presence, presences.MemberPresence) - def test_deserialize_guild_member_presence_with_null_fields(self, entity_factory_impl, user_payload): - presence = entity_factory_impl.deserialize_guild_member_presence( + def test_deserialize_member_presence_with_null_fields(self, entity_factory_impl, user_payload): + presence = entity_factory_impl.deserialize_member_presence( { "user": {"username": "agent 47", "avatar": None, "discriminator": "4747", "id": "474747474"}, "roles": [], @@ -1338,10 +1363,10 @@ def test_deserialize_guild_member_presence_with_null_fields(self, entity_factory # PresenceUser assert presence.user.avatar_hash is None - def test_deserialize_guild_member_presence_with_unset_fields( + def test_deserialize_member_presence_with_unset_fields( self, entity_factory_impl, user_payload, presence_activity_payload ): - presence = entity_factory_impl.deserialize_guild_member_presence( + presence = entity_factory_impl.deserialize_member_presence( { "user": {"id": "42"}, "game": presence_activity_payload, @@ -1355,9 +1380,9 @@ def test_deserialize_guild_member_presence_with_unset_fields( assert presence.nickname is None assert presence.role_ids is None # ClientStatus - assert presence.client_status.desktop is guilds.PresenceStatus.OFFLINE - assert presence.client_status.mobile is guilds.PresenceStatus.OFFLINE - assert presence.client_status.web is guilds.PresenceStatus.OFFLINE + assert presence.client_status.desktop is presences.PresenceStatus.OFFLINE + assert presence.client_status.mobile is presences.PresenceStatus.OFFLINE + assert presence.client_status.web is presences.PresenceStatus.OFFLINE # PresenceUser assert presence.user.id == 42 assert presence.user.discriminator is undefined.Undefined() @@ -1367,8 +1392,8 @@ def test_deserialize_guild_member_presence_with_unset_fields( assert presence.user.is_system is undefined.Undefined() assert presence.user.flags is undefined.Undefined() - def test_deserialize_guild_member_presence_with_unset_activity_fields(self, entity_factory_impl, user_payload): - presence = entity_factory_impl.deserialize_guild_member_presence( + def test_deserialize_member_presence_with_unset_activity_fields(self, entity_factory_impl, user_payload): + presence = entity_factory_impl.deserialize_member_presence( { "user": user_payload, "roles": ["49494949"], @@ -1393,8 +1418,8 @@ def test_deserialize_guild_member_presence_with_unset_activity_fields(self, enti assert activity.is_instance is None assert activity.flags is None - def test_deserialize_guild_member_presence_with_null_activity_fields(self, entity_factory_impl, user_payload): - presence = entity_factory_impl.deserialize_guild_member_presence( + def test_deserialize_member_presence_with_null_activity_fields(self, entity_factory_impl, user_payload): + presence = entity_factory_impl.deserialize_member_presence( { "user": user_payload, "roles": ["49494949"], @@ -1434,8 +1459,8 @@ def test_deserialize_guild_member_presence_with_null_activity_fields(self, entit assert activity.state is None assert activity.emoji is None - def test_deserialize_guild_member_presence_with_unset_activity_sub_fields(self, entity_factory_impl, user_payload): - presence = entity_factory_impl.deserialize_guild_member_presence( + def test_deserialize_member_presence_with_unset_activity_sub_fields(self, entity_factory_impl, user_payload): + presence = entity_factory_impl.deserialize_member_presence( { "user": user_payload, "roles": ["49494949"], @@ -1639,8 +1664,8 @@ def guild_payload( guild_text_channel_payload, guild_voice_channel_payload, known_custom_emoji_payload, - guild_member_payload, - guild_member_presence_payload, + member_payload, + member_presence_payload, guild_role_payload, voice_state_payload, ): @@ -1668,7 +1693,7 @@ def guild_payload( "max_presences": 250, "max_video_channel_users": 25, "member_count": 14, - "members": [guild_member_payload], + "members": [member_payload], "mfa_level": 1, "name": "L33t guild", "owner_id": "6969696", @@ -1676,7 +1701,7 @@ def guild_payload( "preferred_locale": "en-GB", "premium_subscription_count": 1, "premium_tier": 2, - "presences": [guild_member_presence_payload], + "presences": [member_presence_payload], "public_updates_channel_id": "33333333", "region": "eu-central", "roles": [guild_role_payload], @@ -1700,8 +1725,8 @@ def test_deserialize_guild( guild_text_channel_payload, guild_voice_channel_payload, known_custom_emoji_payload, - guild_member_payload, - guild_member_presence_payload, + member_payload, + member_presence_payload, guild_role_payload, voice_state_payload, ): @@ -1741,13 +1766,13 @@ def test_deserialize_guild( assert guild.joined_at == datetime.datetime(2019, 5, 17, 6, 26, 56, 936000, tzinfo=datetime.timezone.utc) assert guild.is_large is False assert guild.member_count == 14 - assert guild.members == {115590097100865541: entity_factory_impl.deserialize_guild_member(guild_member_payload)} + assert guild.members == {115590097100865541: entity_factory_impl.deserialize_member(member_payload)} assert guild.channels == { 123: entity_factory_impl.deserialize_guild_text_channel(guild_text_channel_payload), 555: entity_factory_impl.deserialize_guild_voice_channel(guild_voice_channel_payload), } assert guild.presences == { - 115590097100865541: entity_factory_impl.deserialize_guild_member_presence(guild_member_presence_payload) + 115590097100865541: entity_factory_impl.deserialize_member_presence(member_presence_payload) } assert guild.max_presences == 250 assert guild.max_members == 25000 @@ -2068,15 +2093,15 @@ def partial_application_payload(self): @pytest.fixture() def message_payload( - self, user_payload, guild_member_payload, custom_emoji_payload, partial_application_payload, embed_payload + self, user_payload, member_payload, custom_emoji_payload, partial_application_payload, embed_payload ): - del guild_member_payload["user"] + del member_payload["user"] return { "id": "123", "channel_id": "456", "guild_id": "678", "author": user_payload, - "member": guild_member_payload, + "member": member_payload, "content": "some info", "timestamp": "2020-03-21T21:20:16.510000+00:00", "edited_timestamp": "2020-04-21T21:20:16.510000+00:00", @@ -2120,7 +2145,7 @@ def test_deserialize_full_message( mock_app, message_payload, user_payload, - guild_member_payload, + member_payload, partial_application_payload, custom_emoji_payload, embed_payload, @@ -2131,7 +2156,7 @@ def test_deserialize_full_message( assert message.channel_id == 456 assert message.guild_id == 678 assert message.author == entity_factory_impl.deserialize_user(user_payload) - assert message.member == entity_factory_impl.deserialize_guild_member(guild_member_payload, user=message.author) + assert message.member == entity_factory_impl.deserialize_member(member_payload, user=message.author) assert message.content == "some info" assert message.timestamp == datetime.datetime(2020, 3, 21, 21, 20, 16, 510000, tzinfo=datetime.timezone.utc) assert message.edited_timestamp == datetime.datetime( @@ -2319,12 +2344,12 @@ def test_deserialize_my_user_with_unset_fields(self, entity_factory_impl, mock_a ########## @pytest.fixture() - def voice_state_payload(self, guild_member_payload): + def voice_state_payload(self, member_payload): return { "guild_id": "929292929292992", "channel_id": "157733188964188161", "user_id": "80351110224678912", - "member": guild_member_payload, + "member": member_payload, "session_id": "90326bd25d71d39b9ef95b299e3872ff", "deaf": True, "mute": True, @@ -2334,13 +2359,13 @@ def voice_state_payload(self, guild_member_payload): "suppress": False, } - def test_deserialize_voice_state(self, entity_factory_impl, mock_app, voice_state_payload, guild_member_payload): + def test_deserialize_voice_state(self, entity_factory_impl, mock_app, voice_state_payload, member_payload): voice_state = entity_factory_impl.deserialize_voice_state(voice_state_payload) assert voice_state._app is mock_app assert voice_state.guild_id == 929292929292992 assert voice_state.channel_id == 157733188964188161 assert voice_state.user_id == 80351110224678912 - assert voice_state.member == entity_factory_impl.deserialize_guild_member(guild_member_payload) + assert voice_state.member == entity_factory_impl.deserialize_member(member_payload) assert voice_state.session_id == "90326bd25d71d39b9ef95b299e3872ff" assert voice_state.is_guild_deafened is True assert voice_state.is_guild_muted is True diff --git a/tests/hikari/models/__init__.py b/tests/hikari/models/__init__.py new file mode 100644 index 0000000000..1c1502a5ca --- /dev/null +++ b/tests/hikari/models/__init__.py @@ -0,0 +1,18 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# Copyright © Nekoka.tt 2019-2020 +# +# This file is part of Hikari. +# +# Hikari is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Hikari is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Hikari. If not, see . diff --git a/tests/hikari/utilities/test_date.py b/tests/hikari/utilities/test_date.py new file mode 100644 index 0000000000..ea6c60de87 --- /dev/null +++ b/tests/hikari/utilities/test_date.py @@ -0,0 +1,119 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# Copyright © Nekoka.tt 2019-2020 +# +# This file is part of Hikari. +# +# Hikari is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Hikari is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Hikari. If not, see . +import datetime + +from hikari.utilities import date as date_ + + +def test_parse_iso_8601_date_with_negative_timezone(): + string = "2019-10-10T05:22:33.023456-02:30" + date = date_.iso8601_datetime_string_to_datetime(string) + assert date.year == 2019 + assert date.month == 10 + assert date.day == 10 + assert date.hour == 5 + assert date.minute == 22 + assert date.second == 33 + assert date.microsecond == 23456 + offset = date.tzinfo.utcoffset(None) + assert offset == datetime.timedelta(hours=-2, minutes=-30) + + +def test_parse_iso_8601_date_with_positive_timezone(): + string = "2019-10-10T05:22:33.023456+02:30" + date = date_.iso8601_datetime_string_to_datetime(string) + assert date.year == 2019 + assert date.month == 10 + assert date.day == 10 + assert date.hour == 5 + assert date.minute == 22 + assert date.second == 33 + assert date.microsecond == 23456 + offset = date.tzinfo.utcoffset(None) + assert offset == datetime.timedelta(hours=2, minutes=30) + + +def test_parse_iso_8601_date_with_zulu(): + string = "2019-10-10T05:22:33.023456Z" + date = date_.iso8601_datetime_string_to_datetime(string) + assert date.year == 2019 + assert date.month == 10 + assert date.day == 10 + assert date.hour == 5 + assert date.minute == 22 + assert date.second == 33 + assert date.microsecond == 23456 + offset = date.tzinfo.utcoffset(None) + assert offset == datetime.timedelta(seconds=0) + + +def test_parse_iso_8601_date_with_milliseconds_instead_of_microseconds(): + string = "2019-10-10T05:22:33.023Z" + date = date_.iso8601_datetime_string_to_datetime(string) + assert date.year == 2019 + assert date.month == 10 + assert date.day == 10 + assert date.hour == 5 + assert date.minute == 22 + assert date.second == 33 + assert date.microsecond == 23000 + + +def test_parse_iso_8601_date_with_no_fraction(): + string = "2019-10-10T05:22:33Z" + date = date_.iso8601_datetime_string_to_datetime(string) + assert date.year == 2019 + assert date.month == 10 + assert date.day == 10 + assert date.hour == 5 + assert date.minute == 22 + assert date.second == 33 + assert date.microsecond == 0 + + +def test_parse_http_date(): + rfc_timestamp = "Mon, 03 Jun 2019 17:54:26 GMT" + expected_timestamp = datetime.datetime(2019, 6, 3, 17, 54, 26, tzinfo=datetime.timezone.utc) + assert date_.rfc7231_datetime_string_to_datetime(rfc_timestamp) == expected_timestamp + + +def test_parse_discord_epoch_to_datetime(): + discord_timestamp = 37921278956 + expected_timestamp = datetime.datetime(2016, 3, 14, 21, 41, 18, 956000, tzinfo=datetime.timezone.utc) + assert date_.discord_epoch_to_datetime(discord_timestamp) == expected_timestamp + + +def test_parse_datetime_to_discord_epoch(): + timestamp = datetime.datetime(2016, 3, 14, 21, 41, 18, 956000, tzinfo=datetime.timezone.utc) + expected_discord_timestamp = 37921278956 + assert date_.datetime_to_discord_epoch(timestamp) == expected_discord_timestamp + + +def test_parse_unix_epoch_to_datetime(): + unix_timestamp = 1457991678956 + expected_timestamp = datetime.datetime(2016, 3, 14, 21, 41, 18, 956000, tzinfo=datetime.timezone.utc) + assert date_.unix_epoch_to_datetime(unix_timestamp) == expected_timestamp + + +def test_unix_epoch_to_datetime_with_out_of_range_positive_timestamp(): + assert date_.unix_epoch_to_datetime(996877846784536) == datetime.datetime.max + + +def test_unix_epoch_to_datetime_with_out_of_range_negative_timestamp(): + assert date_.unix_epoch_to_datetime(-996877846784536) == datetime.datetime.min diff --git a/tests/hikari/utilities/test_reflect.py b/tests/hikari/utilities/test_reflect.py new file mode 100644 index 0000000000..d4937310ad --- /dev/null +++ b/tests/hikari/utilities/test_reflect.py @@ -0,0 +1,93 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# Copyright © Nekoka.tt 2019-2020 +# +# This file is part of Hikari. +# +# Hikari is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Hikari is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Hikari. If not, see . +import typing + +from hikari.utilities import reflect + + +class TestResolveSignature: + def test_handles_normal_references(self): + def foo(bar: str, bat: int) -> str: + ... + + signature = reflect.resolve_signature(foo) + assert signature.parameters["bar"].annotation is str + assert signature.parameters["bat"].annotation is int + assert signature.return_annotation is str + + def test_handles_normal_no_annotations(self): + def foo(bar, bat): + ... + + signature = reflect.resolve_signature(foo) + assert signature.parameters["bar"].annotation is reflect.EMPTY + assert signature.parameters["bat"].annotation is reflect.EMPTY + assert signature.return_annotation is reflect.EMPTY + + def test_handles_forward_annotated_parameters(self): + def foo(bar: "str", bat: "int") -> str: + ... + + signature = reflect.resolve_signature(foo) + assert signature.parameters["bar"].annotation is str + assert signature.parameters["bat"].annotation is int + assert signature.return_annotation is str + + def test_handles_forward_annotated_return(self): + def foo(bar: str, bat: int) -> "str": + ... + + signature = reflect.resolve_signature(foo) + assert signature.parameters["bar"].annotation is str + assert signature.parameters["bat"].annotation is int + assert signature.return_annotation is str + + def test_handles_forward_annotations(self): + def foo(bar: "str", bat: "int") -> "str": + ... + + signature = reflect.resolve_signature(foo) + assert signature.parameters["bar"].annotation is str + assert signature.parameters["bat"].annotation is int + assert signature.return_annotation is str + + def test_handles_mixed_annotations(self): + def foo(bar: str, bat: "int"): + ... + + signature = reflect.resolve_signature(foo) + assert signature.parameters["bar"].annotation is str + assert signature.parameters["bat"].annotation is int + assert signature.return_annotation is reflect.EMPTY + + def test_handles_only_return_annotated(self): + def foo(bar, bat) -> str: + ... + + signature = reflect.resolve_signature(foo) + assert signature.parameters["bar"].annotation is reflect.EMPTY + assert signature.parameters["bat"].annotation is reflect.EMPTY + assert signature.return_annotation is str + + def test_handles_nested_annotations(self): + def foo(bar: typing.Optional[typing.Iterator[int]]): + ... + + signature = reflect.resolve_signature(foo) + assert signature.parameters["bar"].annotation == typing.Optional[typing.Iterator[int]] diff --git a/tests/hikari/utilities/test_undefined.py b/tests/hikari/utilities/test_undefined.py index 74a7590b32..165c1a3eee 100644 --- a/tests/hikari/utilities/test_undefined.py +++ b/tests/hikari/utilities/test_undefined.py @@ -23,7 +23,7 @@ class TestUndefined: def test_repr(self): - assert repr(undefined.Undefined()) == "UNDEFINED" + assert repr(undefined.Undefined()) == "Undefined()" def test_str(self): assert str(undefined.Undefined()) == "UNDEFINED" From 39e531630020054e7928db4744f5d00809af4064 Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Sat, 30 May 2020 21:37:32 +0100 Subject: [PATCH 419/922] Fixed some of the bugs regarding layout in #313 to make function representations a bit clearer. --- docs/css.mako | 1 + docs/html.mako | 490 +++++++++++++++++++++++++++++++++++++ hikari/app.py | 65 ++++- hikari/bot.py | 47 ---- hikari/event_dispatcher.py | 7 +- hikari/impl/bot.py | 20 +- hikari/net/gateway.py | 2 +- 7 files changed, 578 insertions(+), 54 deletions(-) create mode 100644 docs/html.mako delete mode 100644 hikari/bot.py diff --git a/docs/css.mako b/docs/css.mako index 378f754039..0bd190f63f 100644 --- a/docs/css.mako +++ b/docs/css.mako @@ -101,6 +101,7 @@ padding: 1px 4px; overflow-wrap: break-word; overflow: auto !important; + word-break: keep-all !important; } h1 code { background: transparent } pre { diff --git a/docs/html.mako b/docs/html.mako new file mode 100644 index 0000000000..80c4baaadb --- /dev/null +++ b/docs/html.mako @@ -0,0 +1,490 @@ +<% + import os + + import pdoc + from pdoc.html_helpers import extract_toc, glimpse, to_html as _to_html, format_git_link + + + def link(dobj: pdoc.Doc, name=None): + name = name or dobj.qualname + ('()' if isinstance(dobj, pdoc.Function) else '') + if isinstance(dobj, pdoc.External) and not external_links: + return name + url = dobj.url(relative_to=module, link_prefix=link_prefix, + top_ancestor=not show_inherited_members) + return '{}'.format(dobj.refname, url, name) + + + def to_html(text): + return _to_html(text, module=module, link=link, latex_math=latex_math) + + + def get_annotation(bound_method, sep=':'): + annot = show_type_annotations and bound_method(link=link) or '' + annot = annot.replace("NoneType", "None") + # Remove quotes. + if annot.startswith("'") and annot.endswith("'"): + annot = annot[1:-1] + if annot: + annot = ' ' + sep + '\N{NBSP}' + annot + return annot +%> + +<%def name="ident(name)">${name} + +<%def name="show_source(d)"> + % if (show_source_code or git_link_template) and d.source and d.obj is not getattr(d.inherits, 'obj', None): + <% git_link = format_git_link(git_link_template, d) %> + % if show_source_code: +
+ + Expand source code + % if git_link: + Browse git + %endif + +
${d.source | h}
+
+ % elif git_link: + + %endif + %endif + + +<%def name="show_desc(d, short=False)"> + <% + inherits = ' inherited' if d.inherits else '' + # docstring = glimpse(d.docstring) if short or inherits else d.docstring + docstring = d.docstring + %> + % if d.inherits: +

+ Inherited from: + % if hasattr(d.inherits, 'cls'): + ${link(d.inherits.cls)}.${link(d.inherits, d.name)} + % else: + ${link(d.inherits)} + % endif +

+ % endif + + <% + # Don't quote this bit. + #
${docstring | to_html}
+ %> + ${docstring | to_html} + + % if not isinstance(d, pdoc.Module): + ${show_source(d)} + % endif + + +<%def name="show_module_list(modules)"> +

Python module list

+ +% if not modules: +

No modules found.

+% else: +
+ % for name, desc in modules: +
+
${name}
+
${desc | glimpse, to_html}
+
+ % endfor +
+% endif + + +<%def name="show_column_list(items)"> + <% + two_column = len(items) >= 6 and all(len(i.name) < 20 for i in items) + %> +
    + % for item in items: +
  • ${link(item, item.name)}
  • + % endfor +
+ + +<%def name="show_module(module)"> + <% + variables = module.variables(sort=sort_identifiers) + classes = module.classes(sort=sort_identifiers) + functions = module.functions(sort=sort_identifiers) + submodules = module.submodules() + %> + + <%def name="show_func(f)"> +
+ + <% + params = f.params(annotate=show_type_annotations, link=link) + return_type = get_annotation(f.return_annotation, '->') + + example_str = f.funcdef() + f.name + "(" + ", ".join(params) + ")" + return_type + %> + + % if len(params) > 4 or len(example_str) > 70: + <% + params = "".join(p + ',
' for p in params) + %> +
${f.funcdef()} ${ident(f.name)} (
+
${params}
+
) ${return_type}
+ % else: + <% + params = ", ".join(params) + %> +
${f.funcdef()} ${ident(f.name)} (${params}) ${return_type}
+ % endif +
+
+
${show_desc(f)}
+ + +
+ % if http_server: + + % endif + + <% + module_name = "​.".join(module.name.split('.')) + %> +

${'Namespace' if module.is_namespace else \ + 'Package' if module.is_package and not module.supermodule else \ + 'Module'}
${module_name}

+
+ +
+ ${module.docstring | to_html} + ${show_source(module)} +
+ +
+ % if submodules: +

Sub-modules

+
+ % for m in submodules: +
${link(m)}
+
${show_desc(m, short=True)}
+ % endfor +
+ % endif +
+ +
+ % if variables: +

Global variables

+
+ % for v in variables: + <% return_type = get_annotation(v.type_annotation) %> +
var ${ident(v.name)}${return_type}
+
${show_desc(v)}
+ % endfor +
+ % endif +
+ +
+ % if functions: +

Functions

+
+ % for f in functions: + ${show_func(f)} + % endfor +
+ % endif +
+ +
+ % if classes: +

Classes

+
+ % for c in classes: + <% + class_vars = c.class_variables(show_inherited_members, sort=sort_identifiers) + smethods = c.functions(show_inherited_members, sort=sort_identifiers) + inst_vars = c.instance_variables(show_inherited_members, sort=sort_identifiers) + methods = c.methods(show_inherited_members, sort=sort_identifiers) + mro = c.mro() + subclasses = c.subclasses() + #params = ', '.join(c.params(annotate=show_type_annotations, link=link)) + + params = c.params(annotate=show_type_annotations, link=link) + %> + +
+ % if len(params) > 4 or len(", ".join(params)) > 50: + <% + params = "".join(p + ',
' for p in params) + %> +
class ${ident(c.name)} (
+
${params}
+
)
+ % elif params: + class ${ident(c.name)} + <% + params = ", ".join(params) + %> + (${params}) + % else: + class ${ident(c.name)} + % endif +
+ +
${show_desc(c)} + + % if mro: +

Ancestors

+
    + % for cls in mro: +
  • ${link(cls)}
  • + % endfor +
+ %endif + + % if subclasses: +

Subclasses

+
    + % for sub in subclasses: +
  • ${link(sub)}
  • + % endfor +
+ % endif + % if class_vars: +

Class variables

+
+ % for v in class_vars: + <% return_type = get_annotation(v.type_annotation) %> +
var ${ident(v.name)}${return_type}
+
${show_desc(v)}
+ % endfor +
+ % endif + % if smethods: +

Static methods

+
+ % for f in smethods: + ${show_func(f)} + % endfor +
+ % endif + % if inst_vars: +

Instance variables

+
+ % for v in inst_vars: + <% return_type = get_annotation(v.type_annotation) %> +
var ${ident(v.name)}${return_type}
+
${show_desc(v)}
+ % endfor +
+ % endif + % if methods: +

Methods

+
+ % for f in methods: + ${show_func(f)} + % endfor +
+ % endif + + % if not show_inherited_members: + <% + members = c.inherited_members() + %> + % if members: +

Inherited members

+
    + % for cls, mems in members: +
  • ${link(cls)}: +
      + % for m in mems: +
    • ${link(m, name=m.name)}
    • + % endfor +
    + +
  • + % endfor +
+ % endif + % endif + +
+ % endfor +
+ % endif +
+ + +<%def name="module_index(module)"> + <% + variables = module.variables(sort=sort_identifiers) + classes = module.classes(sort=sort_identifiers) + functions = module.functions(sort=sort_identifiers) + submodules = module.submodules() + supermodule = module.supermodule + %> + + + + + + + + + + +<% + module_list = 'modules' in context.keys() # Whether we're showing module list in server mode +%> + + % if module_list: + Python module list + + % else: + ${module.name} API documentation + + % endif + + + + % if syntax_highlighting: + + %endif + + <%namespace name="css" file="css.mako" /> + + + + + % if google_analytics: + + % endif + + % if search_query: + + + % endif + + % if latex_math: + + % endif + + <%include file="head.mako"/> + + +
+ % if module_list: +
+ ${show_module_list(modules)} +
+ % else: +
+ ${show_module(module)} +
+ ${module_index(module)} + % endif +
+ + + +% if syntax_highlighting: + + +% endif + +% if http_server and module: ## Auto-reload on file change in dev mode + +% endif + + diff --git a/hikari/app.py b/hikari/app.py index c66893d4ad..43284c16ab 100644 --- a/hikari/app.py +++ b/hikari/app.py @@ -19,12 +19,16 @@ """Contains the core interfaces for a Hikari application.""" from __future__ import annotations -__all__ = ["IApp"] +__all__ = ["IApp", "IRESTApp", "IGatewayConsumer", "IGatewayDispatcher", "IGatewayZookeeper"] import abc +import functools import logging import typing +from hikari import event_dispatcher as event_dispatcher_ +from hikari.utilities import undefined + if typing.TYPE_CHECKING: from concurrent import futures import datetime @@ -32,8 +36,8 @@ from hikari import cache as cache_ from hikari import entity_factory as entity_factory_ from hikari import event_consumer as event_consumer_ - from hikari import event_dispatcher as event_dispatcher_ from hikari.models import presences + from hikari import http_settings as http_settings_ from hikari.net import gateway from hikari.net import rest as rest_ @@ -109,6 +113,21 @@ class IGatewayDispatcher(IApp, abc.ABC): This may be combined with `IGatewayZookeeper` for most single-process bots, or may be a specific component for large distributed applications that consume events from a message queue, for example. + + This purposely also implements some calls found in + `hikari.event_dispatcher.IEventDispatcher` with defaulting delegated calls + to the event dispatcher. This provides a more intuitive syntax for + applications. + + # We can now do this... + + >>> @bot.listen() + >>> async def on_message(event: hikari.MessageCreateEvent) -> None: ... + + # ...instead of having to do this... + + >>> @bot.listen() + >>> async def on_message(event: hikari.MessageCreateEvent) -> None: ... """ __slots__ = () @@ -118,6 +137,31 @@ class IGatewayDispatcher(IApp, abc.ABC): def event_dispatcher(self) -> event_dispatcher_.IEventDispatcher: """Event dispatcher and waiter.""" + # Do not add type hints to me! I delegate to a documented method elsewhere! + @functools.wraps(event_dispatcher_.IEventDispatcher.listen) + def listen(self, event_type=undefined.Undefined()): + ... + + # Do not add type hints to me! I delegate to a documented method elsewhere! + @functools.wraps(event_dispatcher_.IEventDispatcher.subscribe) + def subscribe(self, event_type, callback): + ... + + # Do not add type hints to me! I delegate to a documented method elsewhere! + @functools.wraps(event_dispatcher_.IEventDispatcher.unsubscribe) + def unsubscribe(self, event_type, callback): + ... + + # Do not add type hints to me! I delegate to a documented method elsewhere! + @functools.wraps(event_dispatcher_.IEventDispatcher.wait_for) + async def wait_for(self, event_type, predicate, timeout): + ... + + # Do not add type hints to me! I delegate to a documented method elsewhere! + @functools.wraps(event_dispatcher_.IEventDispatcher.dispatch) + def dispatch(self, event): + ... + class IGatewayZookeeper(IGatewayConsumer, abc.ABC): """Component specialization that looks after a set of shards. @@ -206,3 +250,20 @@ def run(self) -> None: This enables the client to be run immediately without having to set up the `asyncio` event loop manually first. """ + + +class IBot(IRESTApp, IGatewayZookeeper, IGatewayDispatcher, abc.ABC): + """Component for single-process bots. + + Bots are components that have access to a REST API, an event dispatcher, + and an event consumer. + + Additionally, bots will contain a collection of Gateway client objects. + """ + + __slots__ = () + + @property + @abc.abstractmethod + def http_settings(self) -> http_settings_.HTTPSettings: + """The HTTP settings to use.""" diff --git a/hikari/bot.py b/hikari/bot.py deleted file mode 100644 index f884a6f1b0..0000000000 --- a/hikari/bot.py +++ /dev/null @@ -1,47 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# Copyright © Nekoka.tt 2019-2020 -# -# This file is part of Hikari. -# -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . -"""Contains the interface which describes the components for single-process bots.""" -from __future__ import annotations - -__all__ = ["IBot"] - -import abc -import typing - -from hikari import app as app_ - -if typing.TYPE_CHECKING: - from hikari import http_settings as http_settings_ - - -class IBot(app_.IRESTApp, app_.IGatewayZookeeper, app_.IGatewayDispatcher, abc.ABC): - """Component for single-process bots. - - Bots are components that have access to a REST API, an event dispatcher, - and an event consumer. - - Additionally, bots will contain a collection of Gateway client objects. - """ - - __slots__ = () - - @property - @abc.abstractmethod - def http_settings(self) -> http_settings_.HTTPSettings: - """HTTP settings to use.""" diff --git a/hikari/event_dispatcher.py b/hikari/event_dispatcher.py index e13e05f808..616b0644dd 100644 --- a/hikari/event_dispatcher.py +++ b/hikari/event_dispatcher.py @@ -33,6 +33,9 @@ _EventT = typing.TypeVar("_EventT", bound=base.HikariEvent, covariant=True) _PredicateT = typing.Callable[[_EventT], typing.Union[bool, typing.Coroutine[None, typing.Any, bool]]] + _SyncCallbackT = typing.Callable[[_EventT], None] + _AsyncCallbackT = typing.Callable[[_EventT], typing.Coroutine[None, typing.Any, None]] + _CallbackT = typing.Union[_SyncCallbackT, _AsyncCallbackT] class IEventDispatcher(component.IComponent, abc.ABC): @@ -102,7 +105,7 @@ def unsubscribe( @abc.abstractmethod def listen( self, event_type: typing.Union[undefined.Undefined, typing.Type[_EventT]] = undefined.Undefined(), - ) -> None: + ) -> typing.Callable[[_CallbackT], _CallbackT]: """Generate a decorator to subscribe a callback to an event type. This is a second-order decorator. @@ -132,7 +135,7 @@ async def wait_for( event_type : typing.Type[hikari.events.bases.HikariEvent] The event type to listen for. This will listen for subclasses of this type additionally. - predicate : + predicate A function or coroutine taking the event as the single parameter. This should return `True` if the event is one you want to return, or `False` if the event should not be returned. diff --git a/hikari/impl/bot.py b/hikari/impl/bot.py index a89eb33905..ef4d6567a7 100644 --- a/hikari/impl/bot.py +++ b/hikari/impl/bot.py @@ -26,7 +26,7 @@ import typing from concurrent import futures -from hikari import bot +from hikari import app from hikari.impl import cache as cache_impl from hikari.impl import entity_factory as entity_factory_impl from hikari.impl import event_manager @@ -35,6 +35,7 @@ from hikari.net import rest from hikari.net import urls from hikari.utilities import klass +from hikari.utilities import undefined if typing.TYPE_CHECKING: import datetime @@ -48,7 +49,7 @@ from hikari.models import intents as intents_ -class BotImpl(gateway_zookeeper.AbstractGatewayZookeeper, bot.IBot): +class BotImpl(gateway_zookeeper.AbstractGatewayZookeeper, app.IBot): def __init__( self, *, @@ -128,6 +129,21 @@ def event_consumer(self) -> event_consumer_.IEventConsumer: def http_settings(self) -> http_settings_.HTTPSettings: return self._config + def listen(self, event_type=undefined.Undefined()): + return self.event_dispatcher.listen(event_type) + + def subscribe(self, event_type, callback): + return self.event_dispatcher.subscribe(event_type, callback) + + def unsubscribe(self, event_type, callback): + return self.event_dispatcher.unsubscribe(event_type, callback) + + async def wait_for(self, event_type, predicate, timeout): + return await self.event_dispatcher.wait_for(event_type, predicate, timeout) + + def dispatch(self, event): + return self.event_dispatcher.dispatch(event) + async def close(self) -> None: await super().close() await self._rest.close() diff --git a/hikari/net/gateway.py b/hikari/net/gateway.py index 17dd824f22..1ec9cf1298 100644 --- a/hikari/net/gateway.py +++ b/hikari/net/gateway.py @@ -406,7 +406,7 @@ async def update_voice_state( Parameters ---------- - guild : hikari.models.guilds.PartialGuild | hikari.utilities.snowflake.Snowflake | int | str + guild : hikari.models.guilds.PartialGuild or hikari.utilities.snowflake.Snowflake or int or str The guild or guild ID to update the voice state for. channel : hikari.models.channels.GuildVoiceChannel | hikari.utilities.Snowflake | int | str | None The channel or channel ID to update the voice state for. If `None` From 570e770bab52897da1251571e8d0bf860bf9bc66 Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Sat, 30 May 2020 21:44:33 +0100 Subject: [PATCH 420/922] Fixed typo in app.py docstring, disabled columns in pdoc summary navbar. --- docs/css.mako | 6 +++--- hikari/app.py | 5 +++-- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/docs/css.mako b/docs/css.mako index 0bd190f63f..56f0182656 100644 --- a/docs/css.mako +++ b/docs/css.mako @@ -142,8 +142,8 @@ } /* Make TOC lists have 2+ columns when viewport is wide enough. Assuming ~20-character identifiers and ~30% wide sidebar. */ - @media (min-width: 200ex) { #index .two-column { column-count: 2 } } - @media (min-width: 300ex) { #index .two-column { column-count: 3 } } + @media (min-width: 300ex) { #index .two-column { column-count: 2 } } + @media (min-width: 500ex) { #index .two-column { column-count: 3 } } dl { margin-bottom: 2em; } @@ -275,7 +275,7 @@ <%def name="desktop()" filter="minify_css"> - @media screen and (min-width: 700px) { + @media screen and (min-width: 1100px) { #sidebar { width: 30%; height: 100vh; diff --git a/hikari/app.py b/hikari/app.py index 43284c16ab..972a2d5f58 100644 --- a/hikari/app.py +++ b/hikari/app.py @@ -19,7 +19,7 @@ """Contains the core interfaces for a Hikari application.""" from __future__ import annotations -__all__ = ["IApp", "IRESTApp", "IGatewayConsumer", "IGatewayDispatcher", "IGatewayZookeeper"] +__all__ = ["IApp", "IRESTApp", "IGatewayConsumer", "IGatewayDispatcher", "IGatewayZookeeper", "IBot"] import abc import functools @@ -126,8 +126,9 @@ class IGatewayDispatcher(IApp, abc.ABC): # ...instead of having to do this... - >>> @bot.listen() + >>> @bot.listen(hikari.MessageCreateEvent) >>> async def on_message(event: hikari.MessageCreateEvent) -> None: ... + """ __slots__ = () From bc0671e445b0a85db51548944727286693b21a4e Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Sun, 31 May 2020 08:27:58 +0000 Subject: [PATCH 421/922] Update rest.py to fix change in https://github.com/discord/discord-api-docs/pull/1708 --- hikari/net/rest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hikari/net/rest.py b/hikari/net/rest.py index 6b698d16a1..1f0727fe24 100644 --- a/hikari/net/rest.py +++ b/hikari/net/rest.py @@ -710,7 +710,7 @@ async def create_invite( body.put("max_uses", max_uses) body.put("temporary", temporary) body.put("unique", unique) - body.put_snowflake("target_user", target_user) + body.put_snowflake("target_user_id", target_user) body.put("target_user_type", target_user_type) response = await self._request(route, body=body, reason=reason) return self._app.entity_factory.deserialize_invite_with_metadata(response) From c0e994ba3c4d9b16c6489d2c5015e5bc39996efb Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Sun, 31 May 2020 11:24:57 +0100 Subject: [PATCH 422/922] Fixed circular import issue caused by url.py module. --- hikari/__init__.py | 7 ++-- hikari/impl/bot.py | 12 +++++-- hikari/impl/rest_app.py | 14 +++++--- hikari/models/applications.py | 8 ++--- hikari/models/channels.py | 4 +-- hikari/models/emojis.py | 13 ++++++-- hikari/models/guilds.py | 14 ++++---- hikari/models/invites.py | 6 ++-- hikari/models/users.py | 6 ++-- hikari/models/webhooks.py | 6 ++-- hikari/net/__init__.py | 33 +++---------------- hikari/net/gateway.py | 2 +- hikari/net/rest.py | 42 +++++++++++++++++++----- hikari/{net/urls.py => utilities/cdn.py} | 14 -------- tests/hikari/net/test_urls.py | 10 +++--- 15 files changed, 99 insertions(+), 92 deletions(-) rename hikari/{net/urls.py => utilities/cdn.py} (79%) diff --git a/hikari/__init__.py b/hikari/__init__.py index f34fc6113a..0759a45d10 100644 --- a/hikari/__init__.py +++ b/hikari/__init__.py @@ -33,11 +33,10 @@ from hikari.errors import * from hikari.events import * from hikari.http_settings import * -from hikari.impl import * from hikari.models import * from hikari.net import * -from hikari.impl.bot import BotImpl as HikariBot -from hikari.impl.rest_app import RESTAppImpl as HikariREST +from hikari.impl.bot import BotImpl as BotApp +from hikari.impl.rest_app import RESTAppImpl as RESTApp -__all__ = ["HikariBot", "HikariREST"] +__all__ = errors.__all__ + http_settings.__all__ + models.__all__ + net.__all__ + ["BotApp", "RESTApp"] diff --git a/hikari/impl/bot.py b/hikari/impl/bot.py index ef4d6567a7..e29d8f01f3 100644 --- a/hikari/impl/bot.py +++ b/hikari/impl/bot.py @@ -33,7 +33,7 @@ from hikari.impl import gateway_zookeeper from hikari.models import presences from hikari.net import rest -from hikari.net import urls +from hikari.utilities import cdn from hikari.utilities import klass from hikari.utilities import undefined @@ -63,7 +63,7 @@ def __init__( intents: typing.Optional[intents_.Intent] = None, large_threshold: int = 250, rest_version: int = 6, - rest_url: str = urls.REST_API_URL, + rest_url: typing.Union[undefined.Undefined, str] = undefined.Undefined(), shard_ids: typing.Optional[typing.Set[int]], shard_count: typing.Optional[int], token: str, @@ -77,7 +77,13 @@ def __init__( self._entity_factory = entity_factory_impl.EntityFactoryImpl(app=self) self._rest = rest.REST( # nosec - app=self, config=config, debug=debug, token=token, token_type="Bot", url=rest_url, version=rest_version, + app=self, + config=config, + debug=debug, + token=token, + token_type="Bot", + rest_url=rest_url, + version=rest_version, ) super().__init__( diff --git a/hikari/impl/rest_app.py b/hikari/impl/rest_app.py index 50c1262ac9..2925ef16c9 100644 --- a/hikari/impl/rest_app.py +++ b/hikari/impl/rest_app.py @@ -30,8 +30,8 @@ from hikari.impl import cache as cache_impl from hikari.impl import entity_factory as entity_factory_impl from hikari.net import rest as rest_ -from hikari.net import urls from hikari.utilities import klass +from hikari.utilities import undefined if typing.TYPE_CHECKING: from hikari import cache as cache_ @@ -44,13 +44,19 @@ def __init__( config: http_settings.HTTPSettings, debug: bool = False, token: typing.Optional[str] = None, - token_type: typing.Optional[str] = None, - rest_url: str = urls.REST_API_URL, + token_type: typing.Union[undefined.Undefined, str] = undefined.Undefined(), + rest_url: typing.Union[undefined.Undefined, str] = undefined.Undefined(), version: int = 6, ) -> None: self._logger = klass.get_logger(self) self._rest = rest_.REST( - app=self, config=config, debug=debug, token=token, token_type=token_type, url=rest_url, version=version, + app=self, + config=config, + debug=debug, + token=token, + token_type=token_type, + rest_url=rest_url, + version=version, ) self._cache = cache_impl.CacheImpl() self._entity_factory = entity_factory_impl.EntityFactoryImpl() diff --git a/hikari/models/applications.py b/hikari/models/applications.py index ada2786440..5369b3bc08 100644 --- a/hikari/models/applications.py +++ b/hikari/models/applications.py @@ -38,7 +38,7 @@ from hikari.models import bases from hikari.models import guilds -from hikari.net import urls +from hikari.utilities import cdn if typing.TYPE_CHECKING: from hikari.models import permissions as permissions_ @@ -307,7 +307,7 @@ def format_icon_url(self, *, format_: str = "png", size: int = 4096) -> typing.O If `size` is not a power of two or not between 16 and 4096. """ if self.icon_hash: - return urls.generate_cdn_url("team-icons", str(self.id), self.icon_hash, format_=format_, size=size) + return cdn.generate_cdn_url("team-icons", str(self.id), self.icon_hash, format_=format_, size=size) return None @@ -405,7 +405,7 @@ def format_icon_url(self, *, format_: str = "png", size: int = 4096) -> typing.O If `size` is not a power of two or not between 16 and 4096. """ if self.icon_hash: - return urls.generate_cdn_url("application-icons", str(self.id), self.icon_hash, format_=format_, size=size) + return cdn.generate_cdn_url("application-icons", str(self.id), self.icon_hash, format_=format_, size=size) return None @property @@ -436,7 +436,7 @@ def format_cover_image_url(self, *, format_: str = "png", size: int = 4096) -> t If `size` is not a power of two or not between 16 and 4096. """ if self.cover_image_hash: - return urls.generate_cdn_url( + return cdn.generate_cdn_url( "application-assets", str(self.id), self.cover_image_hash, format_=format_, size=size ) return None diff --git a/hikari/models/channels.py b/hikari/models/channels.py index 8c1b481438..fd125b82dc 100644 --- a/hikari/models/channels.py +++ b/hikari/models/channels.py @@ -43,7 +43,7 @@ from hikari.models import bases from hikari.models import permissions -from hikari.net import urls +from hikari.utilities import cdn if typing.TYPE_CHECKING: import datetime @@ -188,7 +188,7 @@ def format_icon_url(self, *, format_: str = "png", size: int = 4096) -> typing.O If `size` is not a power of two or not between 16 and 4096. """ if self.icon_hash: - return urls.generate_cdn_url("channel-icons", str(self.id), self.icon_hash, format_=format_, size=size) + return cdn.generate_cdn_url("channel-icons", str(self.id), self.icon_hash, format_=format_, size=size) return None diff --git a/hikari/models/emojis.py b/hikari/models/emojis.py index ef0228cc89..2a3a2d3822 100644 --- a/hikari/models/emojis.py +++ b/hikari/models/emojis.py @@ -30,13 +30,20 @@ from hikari.models import bases from hikari.models import files -from hikari.net import urls +from hikari.utilities import cdn if typing.TYPE_CHECKING: from hikari.models import users from hikari.utilities import snowflake +_TWEMOJI_PNG_BASE_URL: typing.Final[str] = "https://github.com/twitter/twemoji/raw/master/assets/72x72/" +"""The URL for Twemoji PNG artwork for built-in emojis.""" + +_TWEMOJI_SVG_BASE_URL: typing.Final[str] = "https://github.com/twitter/twemoji/raw/master/assets/svg/" +"""The URL for Twemoji SVG artwork for built-in emojis.""" + + @attr.s(eq=True, hash=True, init=False, kw_only=True, slots=True) class Emoji(files.BaseStream, abc.ABC): """Base class for all emojis. @@ -140,7 +147,7 @@ def url(self) -> str: ------- https://github.com/twitter/twemoji/raw/master/assets/72x72/1f004.png """ - return urls.TWEMOJI_PNG_BASE_URL + self.filename + return _TWEMOJI_PNG_BASE_URL + self.filename @property def unicode_names(self) -> typing.Sequence[str]: @@ -233,7 +240,7 @@ def is_mentionable(self) -> bool: @property def url(self) -> str: - return urls.generate_cdn_url("emojis", str(self.id), format_="gif" if self.is_animated else "png", size=None) + return cdn.generate_cdn_url("emojis", str(self.id), format_="gif" if self.is_animated else "png", size=None) @attr.s(eq=True, hash=True, init=False, kw_only=True, slots=True) diff --git a/hikari/models/guilds.py b/hikari/models/guilds.py index e2da1deb55..adb188a04b 100644 --- a/hikari/models/guilds.py +++ b/hikari/models/guilds.py @@ -50,7 +50,7 @@ from hikari.models import bases from hikari.models import users -from hikari.net import urls +from hikari.utilities import cdn if typing.TYPE_CHECKING: import datetime @@ -433,7 +433,7 @@ def format_icon_url(self, *, format_: typing.Optional[str] = None, size: int = 4 if self.icon_hash: if format_ is None: format_ = "gif" if self.icon_hash.startswith("a_") else "png" - return urls.generate_cdn_url("icons", str(self.id), self.icon_hash, format_=format_, size=size) + return cdn.generate_cdn_url("icons", str(self.id), self.icon_hash, format_=format_, size=size) return None @property @@ -491,7 +491,7 @@ def format_splash_url(self, *, format_: str = "png", size: int = 4096) -> typing If `size` is not a power of two or not between 16 and 4096. """ if self.splash_hash: - return urls.generate_cdn_url("splashes", str(self.id), self.splash_hash, format_=format_, size=size) + return cdn.generate_cdn_url("splashes", str(self.id), self.splash_hash, format_=format_, size=size) return None @property @@ -522,7 +522,7 @@ def format_discovery_splash_url(self, *, format_: str = "png", size: int = 4096) If `size` is not a power of two or not between 16 and 4096. """ if self.discovery_splash_hash: - return urls.generate_cdn_url( + return cdn.generate_cdn_url( "discovery-splashes", str(self.id), self.discovery_splash_hash, format_=format_, size=size ) return None @@ -854,7 +854,7 @@ def format_splash_url(self, *, format_: str = "png", size: int = 4096) -> typing If `size` is not a power of two or not between 16 and 4096. """ if self.splash_hash: - return urls.generate_cdn_url("splashes", str(self.id), self.splash_hash, format_=format_, size=size) + return cdn.generate_cdn_url("splashes", str(self.id), self.splash_hash, format_=format_, size=size) return None @property @@ -885,7 +885,7 @@ def format_discovery_splash_url(self, *, format_: str = "png", size: int = 4096) If `size` is not a power of two or not between 16 and 4096. """ if self.discovery_splash_hash: - return urls.generate_cdn_url( + return cdn.generate_cdn_url( "discovery-splashes", str(self.id), self.discovery_splash_hash, format_=format_, size=size ) return None @@ -918,7 +918,7 @@ def format_banner_url(self, *, format_: str = "png", size: int = 4096) -> typing If `size` is not a power of two or not between 16 and 4096. """ if self.banner_hash: - return urls.generate_cdn_url("banners", str(self.id), self.banner_hash, format_=format_, size=size) + return cdn.generate_cdn_url("banners", str(self.id), self.banner_hash, format_=format_, size=size) return None @property diff --git a/hikari/models/invites.py b/hikari/models/invites.py index b9b3611ad1..0e7b46202f 100644 --- a/hikari/models/invites.py +++ b/hikari/models/invites.py @@ -29,7 +29,7 @@ from hikari.models import bases from hikari.models import guilds -from hikari.net import urls +from hikari.utilities import cdn if typing.TYPE_CHECKING: import datetime @@ -111,7 +111,7 @@ def format_splash_url(self, *, format_: str = "png", size: int = 4096) -> typing If `size` is not a power of two or not between 16 and 4096. """ if self.splash_hash: - return urls.generate_cdn_url("splashes", str(self.id), self.splash_hash, format_=format_, size=size) + return cdn.generate_cdn_url("splashes", str(self.id), self.splash_hash, format_=format_, size=size) return None @property @@ -142,7 +142,7 @@ def format_banner_url(self, *, format_: str = "png", size: int = 4096) -> typing If `size` is not a power of two or not between 16 and 4096. """ if self.banner_hash: - return urls.generate_cdn_url("banners", str(self.id), self.banner_hash, format_=format_, size=size) + return cdn.generate_cdn_url("banners", str(self.id), self.banner_hash, format_=format_, size=size) return None @property diff --git a/hikari/models/users.py b/hikari/models/users.py index bfe4a36b86..d405513903 100644 --- a/hikari/models/users.py +++ b/hikari/models/users.py @@ -28,7 +28,7 @@ import attr from hikari.models import bases -from hikari.net import urls +from hikari.utilities import cdn @enum.unique @@ -166,7 +166,7 @@ def format_avatar_url(self, *, format_: typing.Optional[str] = None, size: int = format_ = "gif" elif format_ is None: format_ = "png" - return urls.generate_cdn_url("avatars", str(self.id), self.avatar_hash, format_=format_, size=size) + return cdn.generate_cdn_url("avatars", str(self.id), self.avatar_hash, format_=format_, size=size) @property def default_avatar_index(self) -> int: @@ -176,7 +176,7 @@ def default_avatar_index(self) -> int: @property def default_avatar_url(self) -> str: """URL for this user's default avatar.""" - return urls.generate_cdn_url("embed", "avatars", str(self.default_avatar_index), format_="png", size=None) + return cdn.generate_cdn_url("embed", "avatars", str(self.default_avatar_index), format_="png", size=None) @attr.s(eq=True, hash=True, init=False, kw_only=True, slots=True) diff --git a/hikari/models/webhooks.py b/hikari/models/webhooks.py index 369269272f..8d8efdb752 100644 --- a/hikari/models/webhooks.py +++ b/hikari/models/webhooks.py @@ -28,7 +28,7 @@ import attr from hikari.models import bases -from hikari.net import urls +from hikari.utilities import cdn from hikari.utilities import snowflake if typing.TYPE_CHECKING: @@ -408,7 +408,7 @@ def default_avatar_index(self) -> int: @property def default_avatar_url(self) -> str: """URL for this webhook's default avatar.""" - return urls.generate_cdn_url("embed", "avatars", str(self.default_avatar_index), format_="png", size=None) + return cdn.generate_cdn_url("embed", "avatars", str(self.default_avatar_index), format_="png", size=None) def format_avatar_url(self, format_: str = "png", size: int = 4096) -> str: """Generate the avatar URL for this webhook's custom avatar if set, else it's default avatar. @@ -436,4 +436,4 @@ def format_avatar_url(self, format_: str = "png", size: int = 4096) -> str: """ if not self.avatar_hash: return self.default_avatar_url - return urls.generate_cdn_url("avatars", str(self.id), self.avatar_hash, format_=format_, size=size) + return cdn.generate_cdn_url("avatars", str(self.id), self.avatar_hash, format_=format_, size=size) diff --git a/hikari/net/__init__.py b/hikari/net/__init__.py index 1f66191c0c..18710ca9fe 100644 --- a/hikari/net/__init__.py +++ b/hikari/net/__init__.py @@ -20,31 +20,8 @@ from __future__ import annotations -# TODO: this exacerbates an undetermined amount of circular import errors -# from hikari.net.buckets import * -# from hikari.net.gateway import * -# from hikari.net.http_client import * -# from hikari.net.iterators import * -# from hikari.net.ratelimits import * -# from hikari.net.rest import * -# from hikari.net.rest_utils import * -# from hikari.net.routes import * -# from hikari.net.tracing import * -# from hikari.net.urls import * -# from hikari.net.user_agents import * -# from hikari.net.voice_gateway import * -# -# __all__ = ( -# buckets.__all__ -# + gateway.__all__ -# + http_client.__all__ -# + iterators.__all__ -# + ratelimits.__all__ -# + rest.__all__ -# + rest_utils.__all__ -# + routes.__all__ -# + tracing.__all__ -# + urls.__all__ -# + user_agents.__all__ -# + voice_gateway.__all__ -# ) +from hikari.net.gateway import * +from hikari.net.rest import * +from hikari.net.voice_gateway import * + +__all__ = gateway.__all__ + rest.__all__ + voice_gateway.__all__ diff --git a/hikari/net/gateway.py b/hikari/net/gateway.py index 1ec9cf1298..c7979f0da6 100644 --- a/hikari/net/gateway.py +++ b/hikari/net/gateway.py @@ -38,6 +38,7 @@ from hikari.net import http_client from hikari.net import ratelimits from hikari.net import user_agents +from hikari.models import presences from hikari.utilities import data_binding from hikari.utilities import klass from hikari.utilities import undefined @@ -50,7 +51,6 @@ from hikari.models import channels from hikari.models import guilds from hikari.models import intents as intents_ - from hikari.models import presences from hikari.utilities import snowflake from hikari.utilities import aio diff --git a/hikari/net/rest.py b/hikari/net/rest.py index 1f0727fe24..7592a6c5da 100644 --- a/hikari/net/rest.py +++ b/hikari/net/rest.py @@ -64,6 +64,15 @@ from hikari.models import webhooks +_REST_API_URL: typing.Final[str] = "https://discord.com/api/v{0.version}" +"""The URL for the RESTSession API. This contains a version number parameter that +should be interpolated. +""" + +_OAUTH2_API_URL: typing.Final[str] = "https://discord.com/api/oauth2" +"""The URL to the Discord OAuth2 API.""" + + class REST(http_client.HTTPClient, component.IComponent): """Implementation of the V6 and V7-compatible Discord REST API. @@ -88,10 +97,11 @@ class REST(http_client.HTTPClient, component.IComponent): left `False`. token : str The bot or bearer token. If no token is to be used, this can be `None`. - token_type : str + token_type : str or hikari.utilities.undefined.Undefined The type of token in use. If no token is used, this can be ignored and - left to the default value. This can be `"Bot"` or `"Bearer"`. - url : str + left to the default value. This can be `"Bot"` or `"Bearer"`. The + default if not provided will be `"Bot"`. + rest_url : str The REST API base URL. This can contain format-string specifiers to interpolate information such as API version in use. version : int @@ -108,8 +118,9 @@ def __init__( config: http_settings.HTTPSettings, debug: bool = False, token: typing.Optional[str], - token_type: str = "Bot", - url: str, + token_type: typing.Union[undefined.Undefined, str] = undefined.Undefined(), + rest_url: typing.Union[undefined.Undefined, str] = undefined.Undefined(), + oauth2_url: typing.Union[undefined.Undefined, str] = undefined.Undefined(), version: int, ) -> None: super().__init__( @@ -132,8 +143,23 @@ def __init__( self.version = version self._app = app - self._token = f"{token_type.title()} {token}" if token is not None else None - self._url = url.format(self) + + if token is None: + self._token = None + else: + if token_type is undefined.Undefined(): + token_type = "Bot" + + self._token = f"{token_type.title()} {token}" if token is not None else None + + if rest_url is undefined.Undefined(): + rest_url = _REST_API_URL + + if oauth2_url is undefined.Undefined(): + oauth2_url = _OAUTH2_API_URL + + self._rest_url = rest_url.format(self) + self._oauth2_url = oauth2_url.format(self) @property def app(self) -> app_.IRESTApp: @@ -190,7 +216,7 @@ async def _request_once( body: typing.Optional[typing.Union[aiohttp.FormData, data_binding.JSONArray, data_binding.JSONObject]], query: typing.Optional[typing.Dict[str, str]], ) -> typing.Optional[data_binding.JSONObject, data_binding.JSONArray, bytes, str]: - url = compiled_route.create_url(self._url) + url = compiled_route.create_url(self._rest_url) # Wait for any ratelimits to finish. await asyncio.gather(self.buckets.acquire(compiled_route), self.global_rate_limit.acquire()) diff --git a/hikari/net/urls.py b/hikari/utilities/cdn.py similarity index 79% rename from hikari/net/urls.py rename to hikari/utilities/cdn.py index c61de72b86..c78e54ea7b 100644 --- a/hikari/net/urls.py +++ b/hikari/utilities/cdn.py @@ -30,20 +30,6 @@ BASE_CDN_URL: typing.Final[str] = "https://cdn.discordapp.com" """The URL for the CDN.""" -REST_API_URL: typing.Final[str] = "https://discord.com/api/v{0.version}" -"""The URL for the RESTSession API. This contains a version number parameter that -should be interpolated. -""" - -OAUTH2_API_URL: typing.Final[str] = "https://discord.com/api/oauth2" -"""The URL to the Discord OAuth2 API.""" - -TWEMOJI_PNG_BASE_URL: typing.Final[str] = "https://github.com/twitter/twemoji/raw/master/assets/72x72/" -"""The URL for Twemoji PNG artwork for built-in emojis.""" - -TWEMOJI_SVG_BASE_URL: typing.Final[str] = "https://github.com/twitter/twemoji/raw/master/assets/svg/" -"""The URL for Twemoji SVG artwork for built-in emojis.""" - def generate_cdn_url(*route_parts: str, format_: str, size: typing.Optional[int]) -> str: """Generate a link for a Discord CDN media resource. diff --git a/tests/hikari/net/test_urls.py b/tests/hikari/net/test_urls.py index 8b6dbe4414..95edea8cba 100644 --- a/tests/hikari/net/test_urls.py +++ b/tests/hikari/net/test_urls.py @@ -16,25 +16,25 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . -from hikari.net import urls +from hikari.utilities import cdn from tests.hikari import _helpers def test_generate_cdn_url(): - url = urls.generate_cdn_url("not", "a", "path", format_="neko", size=16) + url = cdn.generate_cdn_url("not", "a", "path", format_="neko", size=16) assert url == "https://cdn.discordapp.com/not/a/path.neko?size=16" def test_generate_cdn_url_with_size_set_to_none(): - url = urls.generate_cdn_url("not", "a", "path", format_="neko", size=None) + url = cdn.generate_cdn_url("not", "a", "path", format_="neko", size=None) assert url == "https://cdn.discordapp.com/not/a/path.neko" @_helpers.assert_raises(type_=ValueError) def test_generate_cdn_url_with_invalid_size_out_of_limits(): - urls.generate_cdn_url("not", "a", "path", format_="neko", size=11) + cdn.generate_cdn_url("not", "a", "path", format_="neko", size=11) @_helpers.assert_raises(type_=ValueError) def test_generate_cdn_url_with_invalid_size_now_power_of_two(): - urls.generate_cdn_url("not", "a", "path", format_="neko", size=111) + cdn.generate_cdn_url("not", "a", "path", format_="neko", size=111) From bfa00f3aec405121fb9d769c7260fbd932383a89 Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Sun, 31 May 2020 11:30:53 +0100 Subject: [PATCH 423/922] Made iterators generics use forward-references explicitly to break up any further issue of import problems. --- hikari/net/iterators.py | 25 ++++++++++++++++++++----- hikari/net/rest.py | 4 ++-- 2 files changed, 22 insertions(+), 7 deletions(-) diff --git a/hikari/net/iterators.py b/hikari/net/iterators.py index 18c4f7f8d4..f353cb055f 100644 --- a/hikari/net/iterators.py +++ b/hikari/net/iterators.py @@ -237,7 +237,10 @@ async def __anext__(self) -> _T: return next(self._buffer) -class MessageIterator(_BufferedLazyIterator[messages.Message]): +# We use an explicit forward reference for this, since this breaks potential +# circular import issues (once the file has executed, using those resources is +# not an issue for us). +class MessageIterator(_BufferedLazyIterator["messages.Message"]): """Implementation of an iterator for message history.""" __slots__ = ("_app", "_request_call", "_direction", "_first_id", "_route") @@ -272,7 +275,10 @@ async def _next_chunk(self) -> typing.Optional[typing.Generator[messages.Message return (self._app.entity_factory.deserialize_message(m) for m in chunk) -class ReactorIterator(_BufferedLazyIterator[users.User]): +# We use an explicit forward reference for this, since this breaks potential +# circular import issues (once the file has executed, using those resources is +# not an issue for us). +class ReactorIterator(_BufferedLazyIterator["users.User"]): """Implementation of an iterator for message reactions.""" __slots__ = ("_app", "_first_id", "_route", "_request_call") @@ -304,7 +310,10 @@ async def _next_chunk(self) -> typing.Optional[typing.Generator[users.User, typi return (self._app.entity_factory.deserialize_user(u) for u in chunk) -class OwnGuildIterator(_BufferedLazyIterator[applications.OwnGuild]): +# We use an explicit forward reference for this, since this breaks potential +# circular import issues (once the file has executed, using those resources is +# not an issue for us). +class OwnGuildIterator(_BufferedLazyIterator["applications.OwnGuild"]): """Implementation of an iterator for retrieving guilds you are in.""" __slots__ = ("_app", "_request_call", "_route", "_newest_first", "_first_id") @@ -337,7 +346,10 @@ async def _next_chunk(self) -> typing.Optional[typing.Generator[applications.Own return (self._app.entity_factory.deserialize_own_guild(g) for g in chunk) -class MemberIterator(_BufferedLazyIterator[guilds.Member]): +# We use an explicit forward reference for this, since this breaks potential +# circular import issues (once the file has executed, using those resources is +# not an issue for us). +class MemberIterator(_BufferedLazyIterator["guilds.Member"]): """Implementation of an iterator for retrieving members in a guild.""" __slots__ = ("_app", "_request_call", "_route", "_first_id") @@ -369,7 +381,10 @@ async def _next_chunk(self) -> typing.Optional[typing.Generator[guilds.Member, t return (self._app.entity_factory.deserialize_member(m) for m in chunk) -class AuditLogIterator(LazyIterator[audit_logs.AuditLog]): +# We use an explicit forward reference for this, since this breaks potential +# circular import issues (once the file has executed, using those resources is +# not an issue for us). +class AuditLogIterator(LazyIterator["audit_logs.AuditLog"]): """Iterator implementation for an audit log.""" def __init__( diff --git a/hikari/net/rest.py b/hikari/net/rest.py index 7592a6c5da..40b66faf96 100644 --- a/hikari/net/rest.py +++ b/hikari/net/rest.py @@ -152,10 +152,10 @@ def __init__( self._token = f"{token_type.title()} {token}" if token is not None else None - if rest_url is undefined.Undefined(): + if isinstance(rest_url, undefined.Undefined): rest_url = _REST_API_URL - if oauth2_url is undefined.Undefined(): + if isinstance(oauth2_url, undefined.Undefined): oauth2_url = _OAUTH2_API_URL self._rest_url = rest_url.format(self) From 78affeb9de560b352e2aa5833d3334d93290d545 Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Sun, 31 May 2020 11:32:18 +0100 Subject: [PATCH 424/922] Added 'if typing.TYPE_CHECKING' guard to iterators.py --- hikari/net/iterators.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/hikari/net/iterators.py b/hikari/net/iterators.py index f353cb055f..cf70903896 100644 --- a/hikari/net/iterators.py +++ b/hikari/net/iterators.py @@ -24,17 +24,20 @@ import abc import typing -from hikari import app as app_ -from hikari.models import applications -from hikari.models import audit_logs -from hikari.models import guilds -from hikari.models import messages -from hikari.models import users from hikari.net import routes from hikari.utilities import data_binding from hikari.utilities import snowflake from hikari.utilities import undefined +if typing.TYPE_CHECKING: + from hikari import app as app_ + from hikari.models import applications + from hikari.models import audit_logs + from hikari.models import guilds + from hikari.models import messages + from hikari.models import users + + _T = typing.TypeVar("_T") From 686bca1ba0dc58fbf20df00187f13cc9cd3897e2 Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Sun, 31 May 2020 17:27:16 +0100 Subject: [PATCH 425/922] Implemented pdoc3 bootstrap template from scratch. Function layout and CSS tweaks. Removed old mako source for docs. --- ci/pdoc.nox.py | 2 +- docs/body.mako | 4 + docs/config.mako | 56 ++- docs/credits.mako | 21 -- docs/css.mako | 533 +++++++++-------------------- docs/documentation.mako | 435 +++++++++++++++++++++++ docs/footer.mako | 0 docs/head.mako | 113 ++---- docs/html.mako | 570 +++++-------------------------- docs/logo.mako | 35 -- hikari/__init__.py | 10 +- hikari/app.py | 3 +- hikari/cache.py | 2 +- hikari/component.py | 2 +- hikari/entity_factory.py | 2 +- hikari/errors.py | 2 +- hikari/event_consumer.py | 2 +- hikari/event_dispatcher.py | 2 +- hikari/http_settings.py | 2 +- hikari/impl/bot.py | 2 +- hikari/net/ratelimits.py | 44 +-- hikari/net/rest.py | 2 +- hikari/net/routes.py | 22 +- hikari/utilities/__init__.py | 20 +- hikari/utilities/aio.py | 36 +- hikari/utilities/data_binding.py | 162 +++++---- hikari/utilities/date.py | 61 ++-- pages/index.html | 148 ++++---- 28 files changed, 1015 insertions(+), 1278 deletions(-) create mode 100644 docs/body.mako delete mode 100644 docs/credits.mako create mode 100644 docs/documentation.mako create mode 100644 docs/footer.mako delete mode 100644 docs/logo.mako diff --git a/ci/pdoc.nox.py b/ci/pdoc.nox.py index 0b5e5f7885..7a042125f1 100644 --- a/ci/pdoc.nox.py +++ b/ci/pdoc.nox.py @@ -25,7 +25,7 @@ @nox.inherit_environment_vars def pdoc(session: nox.Session) -> None: """Generate documentation with pdoc.""" - session.install("-r", config.REQUIREMENTS, "pdoc3==0.8.1") + session.install("-r", config.REQUIREMENTS, "git+https://github.com/pdoc3/pdoc") session.run( "python", diff --git a/docs/body.mako b/docs/body.mako new file mode 100644 index 0000000000..127fcd24f0 --- /dev/null +++ b/docs/body.mako @@ -0,0 +1,4 @@ + +<%include file="head.mako"/> +<%include file="documentation.mako"/> +<%include file="footer.mako"/> diff --git a/docs/config.mako b/docs/config.mako index 407bd68d5a..dcc102311e 100644 --- a/docs/config.mako +++ b/docs/config.mako @@ -20,51 +20,35 @@ import hikari as _hikari - _staging_version = "dev" in _version.LooseVersion(_hikari.__version__).version - - # Template configuration. Copy over in your template directory - # (used with `--template-dir`) and adapt as necessary. - # Note, defaults are loaded from this distribution file, so your - # config.mako only needs to contain values you want overridden. - # You can also run pdoc with `--config KEY=VALUE` to override - # individual values. - html_lang = "en" show_inherited_members = True extract_module_toc_into_sidebar = True list_class_variables_in_index = True sort_identifiers = True show_type_annotations = True - # Show collapsed source code block next to each item. - # Disabling this can improve rendering speed of large modules. + show_source_code = True - # If set, format links to objects in online source code repository - # according to this template. Supported keywords for interpolation - # are: commit, path, start_line, end_line. + git_link_template = "https://gitlab.com/nekokatt/hikari/blob/{commit}/{path}#L{start_line}" - # A prefix to use for every HTML hyperlink in the generated documentation. - # No prefix results in all links being relative. + link_prefix = "" - # Enable syntax highlighting for code/source blocks by including Highlight.js - syntax_highlighting = True - # Set the style keyword such as 'atom-one-light' or 'github-gist' - # Options: https://github.com/highlightjs/highlight.js/tree/master/src/styles - # Demo: https://highlightjs.org/static/demo/ - hljs_style = "rainbow" - # If set, insert Google Analytics tracking code. Value is GA - # tracking id (UA-XXXXXX-Y). - google_analytics = "" - # If set, insert Google Custom Search search bar widget above the sidebar index. - # The whitespace-separated tokens represent arbitrary extra queries (at least one - # must match) passed to regular Google search. Example: - #search_query = 'inurl:github.com/USER/PROJECT site:PROJECT.github.io site:PROJECT.website' - if _staging_version: + + hljs_style = "atom-one-light" + + if "dev" in _version.LooseVersion(_hikari.__version__).version: search_query = "inurl:github.com/nekokatt/hikari site:nekokatt.gitlab.io/hikari/hikari" else: # TODO: "hikari/staging/hikari" temporarily changed to "hikari/hikari" for staging site search link. search_query = "inurl:github.com/nekokatt/hikari site:nekokatt.gitlab.io/hikari/hikari" - # If set, render LaTeX math syntax within \(...\) (inline equations), - # or within \[...\] or $$...$$ or `.. math::` (block equations) - # as nicely-formatted math formulas using MathJax. - # Note: in Python docstrings, either all backslashes need to be escaped (\\) - # or you need to use raw r-strings. - latex_math = True + + site_accent = "#ff029a" + site_logo = "https://assets.gitlab-static.net/uploads/-/system/project/avatar/12050696/Hikari-Logo_1.png" + site_description = "A Discord Bot framework for modern Python and asyncio built on good intentions" + + # Versions of stuff + mathjax_version = "2.7.5" + bootstrap_version = "4.5.0" + highlightjs_version = "9.12.0" + jquery_version = "3.5.1" + popperjs_version = "1.16.0" + + root_url = "https://gitlab.com/nekokatt/hikari" %> diff --git a/docs/credits.mako b/docs/credits.mako deleted file mode 100644 index e66e0b4cb3..0000000000 --- a/docs/credits.mako +++ /dev/null @@ -1,21 +0,0 @@ -## Copyright © Nekokatt 2019-2020 -## -## This file is part of Hikari. -## -## Hikari is free software: you can redistribute it and/or modify -## it under the terms of the GNU Lesser General Public License as published by -## the Free Software Foundation, either version 3 of the License, or -## (at your option) any later version. -## -## Hikari is distributed in the hope that it will be useful, -## but WITHOUT ANY WARRANTY; without even the implied warranty of -## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -## GNU Lesser General Public License for more details. -## -## You should have received a copy of the GNU Lesser General Public License -## along with Hikari. If not, see . -<%! - import hikari -%> -

Licensed under GNU LGPLv3.

-

${hikari.__copyright__}.

diff --git a/docs/css.mako b/docs/css.mako index 56f0182656..8ed4cda672 100644 --- a/docs/css.mako +++ b/docs/css.mako @@ -1,382 +1,155 @@ -## Copyright © Nekokatt 2019-2020 -## -## This file is part of Hikari. -## -## Hikari is free software: you can redistribute it and/or modify -## it under the terms of the GNU Lesser General Public License as published by -## the Free Software Foundation, either version 3 of the License, or -## (at your option) any later version. -## -## Hikari is distributed in the hope that it will be useful, -## but WITHOUT ANY WARRANTY; without even the implied warranty of -## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -## GNU Lesser General Public License for more details. -## -## You should have received a copy of the GNU Lesser General Public License -## along with Hikari. If not, see . - -<%! - from pdoc.html_helpers import minify_css -%> - -<%def name="mobile()" filter="minify_css"> - .flex { - display: flex !important; - } - body { - background-color: #373b3e; - color: #FFFFFF; - line-height: 1.5em; - min-width: 512px; - } - #content { - padding: 20px; - } - #sidebar { - padding: 30px; - overflow: hidden; - } - #sidebar > *:last-child { - margin-bottom: 2cm; - } - .http-server-breadcrumbs { - font-size: 130%; - margin: 0 0 15px 0; - } - #footer { - font-size: .75em; - padding: 5px 30px; - border-top: 1px solid #373b3e; - text-align: right; - } - #footer p { - margin: 0 0 0 1em; - display: inline-block; - } - #footer p:last-child { - margin-right: 30px; - } - h1, h2, h3, h4, h5 { - font-weight: 300; - } - h1 { - font-size: 2.5em; - line-height: 1.1em; - } - h2 { - font-size: 1.75em; - margin: 1em 0 .50em 0; - } - h3 { - font-size: 1.4em; - margin: 25px 0 10px 0; - } - h4 { - margin: 0; - font-size: 105%; - } - a { - color: #86cecb; - text-decoration: none; - transition: color .3s ease-in-out; - } - a:hover { - color: #137a7f; - } - .title code { - font-weight: bold; - } - h2[id^="header-"] { +img#logo { + border-radius: 15px; + width: 30px; + height: 30px; + margin-right: 0.5em; +} + +small.smaller { + font-size: 0.75em; +} + +h1 { + margin-top: 3rem; +} + +h2 { + margin-top: 1.75rem; + margin-bottom: 1em; +} + +h3 { + margin-top: 1.25rem; +} + +h4 { + margin-top: 1rem; +} + +.nav-section { margin-top: 2em; - } - .ident { - color: #e12885; - } - pre code { - font-size: .8em; - line-height: 1.4em; - } - code { - background: #42464a; - padding: 1px 4px; - overflow-wrap: break-word; - overflow: auto !important; - word-break: keep-all !important; - } - h1 code { background: transparent } - pre { - margin: 2em 0; - padding: 2ex; - } - #http-server-module-list { - display: flex; - flex-flow: column; - } - #http-server-module-list div { - display: flex; - } - #http-server-module-list dt { - min-width: 10%; - } - #http-server-module-list p { - margin-top: 0; - } - .toc ul, - #index { - list-style-type: none; - margin: 0; - padding: 0; - } - #index code { - background: #42464a; - } - #index h3 { - border-bottom: 1px solid #373b3e; - } - #index ul { - padding: 0; - } - #index h4 { - margin-top: .6em; - font-weight: bold; - } - /* Make TOC lists have 2+ columns when viewport is wide enough. - Assuming ~20-character identifiers and ~30% wide sidebar. */ - @media (min-width: 300ex) { #index .two-column { column-count: 2 } } - @media (min-width: 500ex) { #index .two-column { column-count: 3 } } - dl { +} + +a.sidebar-nav-pill, +a.sidebar-nav-pill:active, +a.sidebar-nav-pill:hover { + color: #444; +} + +.module-source > details > pre { + display: block; + overflow-x: auto; + overflow-y: auto; + max-height: 600px; + font-size: 0.8em; +} + +a { + color: #e83e8c; +} + +.breadcrumb-item.active { + color: #e83e8c; +} + +.breadcrumb-item+.breadcrumb-item::before { + content: "."; +} + +.module-breadcrumb { + padding-left: 0 !important; +} + +ul.nested { + margin-left: 1em; +} + +h2#parameters::after { + margin-left: 2em; +} + +dt { + margin-left: 2em; +} + +dd { + margin-left: 4em; +} + +dl.no-nest > dt { + margin-left: 0em; +} + +dl.no-nest > dd { + margin-left: 2em; +} + +dl.root { margin-bottom: 2em; - } - dl dl:last-child { - margin-bottom: 4em; - } - dd { - margin: 0 0 1em 3em; - } - #header-classes + dl > dd { - margin-bottom: 3em; - } - dd dd { - margin-left: 2em; - } - dd p { - margin: 10px 0; - } - .name { - background: #42464a; - font-weight: bold; - font-size: .85em; - padding: 5px 10px; - display: inline-block; - min-width: 40%; - } - .name > span:first-child { - white-space: nowrap; - } - .name.class > span:nth-child(2) { - margin-left: .4em; - } - .inherited { - border-left: 5px solid #bec8d1; - padding-left: 1em; - } - .inheritance em { - font-style: normal; - font-weight: bold; - } - /* Docstrings titles, e.g. in numpydoc format */ - .desc h2 { - font-weight: 400; - font-size: 1.25em; - } - .desc h3 { - font-size: 1em; - } - .desc dt code { - background: inherit; /* Don't grey-back parameters */ - } - .source summary, - .git-link-div { - color: #bec8d1; - text-align: right; - font-weight: 400; - font-size: .8em; - text-transform: uppercase; - } - .source summary > * { - white-space: nowrap; - cursor: pointer; - } - .git-link { - color: inherit; - margin-left: 1em; - } - .source pre { - max-height: 500px; - overflow: auto; - margin: 0; - } - .source pre code { - font-size: 12px; - overflow: visible; - } - .hlist { - list-style: none; - } - .hlist li { - display: inline; - } - .hlist li:after { - content: ',\2002'; - } - .hlist li:last-child:after { - content: none; - } - .hlist .hlist { - display: inline; - padding-left: 1em; - } - img { - max-width: 100%; - } - .admonition { - padding: .1em .5em; - margin-bottom: 1em; - } - .admonition-title { - font-weight: bold; - } - .admonition.note, - .admonition.info, - .admonition.important, - .admonition.todo, - .admonition.versionadded, - .admonition.tip, - .admonition.hint { - background: #505050; - } - .admonition.versionchanged, - .admonition.deprecated { - background: #cca300; - } - .admonition.warning, - .admonition.error, - .admonition.danger, - .admonition.caution { - background: #ff4d4d; - } - .gsc-control-cse { - background-color: #373b3e !important; - border-color: #373b3e !important; - } - .gsc-modal-background-image { - background-color: #373b3e !important; - } - - -<%def name="desktop()" filter="minify_css"> - @media screen and (min-width: 1100px) { - #sidebar { - width: 30%; - height: 100vh; - overflow: auto; - position: sticky; - top: 0; - } - #content { - width: 70%; - max-width: 100ch; - padding: 3em 4em; - border-left: 1px solid #bec8d1; - } - pre code { - font-size: 1em; - } - .item .name { - font-size: 1em; - } - main { - display: flex; - flex-direction: row-reverse; - justify-content: flex-end; - } - .toc ul ul, - #index ul { - padding-left: 1.5em; - } - .toc > ul > li { - margin-top: .5em; - } - } - - -<%def name="print()" filter="minify_css"> -@media print { - #sidebar h1 { - page-break-before: always; - } - .source { - display: none; - } -} -@media print { - * { - background: transparent !important; - color: #000 !important; /* Black prints faster: h5bp.com/s */ - box-shadow: none !important; - text-shadow: none !important; - } - a[href]:after { - content: " (" attr(href) ")"; - font-size: 90%; - } - /* Internal, documentation links, recognized by having a title, - don't need the URL explicity stated. */ - a[href][title]:after { - content: none; - } - abbr[title]:after { - content: " (" attr(title) ")"; - } - /* - * Don't show links for images, or javascript/internal links - */ - .ir a:after, - a[href^="javascript:"]:after, - a[href^="#"]:after { - content: ""; - } - pre, - blockquote { - border: 1px solid #373b3e; - page-break-inside: avoid; - } - thead { - display: table-header-group; /* h5bp.com/t */ - } - tr, - img { - page-break-inside: avoid; - } - img { - max-width: 100% !important; - } - @page { - margin: 0.5cm; - } - p, - h2, - h3 { - orphans: 3; - widows: 3; - } - h1, - h2, - h3, - h4, - h5, - h6 { - page-break-after: avoid; - } -} - +} + +.definition { + display: block; + margin-bottom: 8em !important; +} + +.definition .row { + display: block; + margin-bottom: 4em !important; +} + +.definition h2 { + font-size: 1em; + font-weight: bolder; +} + +.sep { + height: 2em; +} + +code { + color: #a626a4; +} + +code .active { + color: #e83e8c; +} + +code a { + color: #e83e8c; +} + +a.dotted { + text-decoration: underline #6c757d dotted !important; +} + +## Custom search formatting to look somewhat bootstrap-py +.gsc-search-box, .gsc-search-box-tools, .gsc-control-cse { + background: none !important; + border: none !important; +} + +.gsc-search-button-v2, .gsc-search-button-v2:hover, .gsc-search-button-v2:focus { + color: var(--success) !important; + border-color: var(--success) !important; + background: none !important; + padding: 6px 32px !important; + font-size: inherit !important; +} + +.gsc-search-button-v2 > svg { + fill: var(--success) !important; +} + +.gsc-input-box { + border-radius: 3px; +} + +.gsc-control-cse { + width: 300px !important; + margin-top: 0 !important; +} + +.gsc-control-cse .gsc-control-cse-en { + margin-top: 0 !important; +} \ No newline at end of file diff --git a/docs/documentation.mako b/docs/documentation.mako new file mode 100644 index 0000000000..ecf752872f --- /dev/null +++ b/docs/documentation.mako @@ -0,0 +1,435 @@ +<% + import abc + import enum + import inspect + import textwrap + import typing + + import pdoc + + from pdoc.html_helpers import extract_toc, glimpse, to_html as _to_html, format_git_link + + def link(dobj: pdoc.Doc, *, with_prefixes=False, simple_names=False, css_classes="", name=None, default_type="", dotted=True, anchor=False): + prefix = "" + name = name or dobj.name + + if with_prefixes: + if isinstance(dobj, pdoc.Function): + prefix = "" + dobj.funcdef() + " " + + elif isinstance(dobj, pdoc.Variable): + if dobj.module.name == "typing" or dobj.docstring and dobj.docstring.casefold().startswith("type hint"): + prefix = "type hint " + elif all(not c.isalpha() or c.isupper() for c in dobj.name): + prefix = "const " + else: + prefix = "var " + + elif isinstance(dobj, pdoc.Class): + if issubclass(dobj.obj, type): + qual = "metaclass" + else: + if enum.Flag in dobj.obj.mro(): + qual = "enum flag" + elif enum.Enum in dobj.obj.mro(): + qual = "enum" + else: + qual = "class" + + if inspect.isabstract(dobj.obj): + qual = "abstract " + qual + + prefix = f"{qual} " + + if "__call__" in dobj.obj.__dict__: + name += "()" + + elif isinstance(dobj, pdoc.Module): + qual = "package" if dobj.is_package else "namespace" if dobj.is_namespace else "module" + prefix = f"{qual} " + + else: + prefix = f"{default_type} " + else: + name = name or dobj.name or "" + + + if isinstance(dobj, pdoc.External) and not external_links: + return name + + url = dobj.url(relative_to=module, link_prefix=link_prefix, top_ancestor=not show_inherited_members) + + if simple_names: + name = simple_name(name) + + classes = [] + if dotted: + classes.append("dotted") + if css_classes: + classes.append(css_classes) + class_str = " ".join(classes) + + if class_str.strip(): + class_str = f"class={class_str!r}" + + anchor = "" if not anchor else f'id="{dobj.refname}"' + + return '{} {}'.format(prefix, glimpse(dobj.docstring), url, anchor, class_str, name) + + def simple_name(s): + _, _, name = s.rpartition(".") + return name + + def get_annotation(bound_method, sep=':'): + annot = show_type_annotations and bound_method(link=link) or '' + annot = annot.replace("NoneType", "None") + # Remove quotes. + if annot.startswith("'") and annot.endswith("'"): + annot = annot[1:-1] + if annot: + annot = ' ' + sep + '\N{NBSP}' + annot + return annot + + def to_html(text): + return _to_html(text, module=module, link=link, latex_math=latex_math) +%> + +<%def name="ident(name)">${name} + +<%def name="breadcrumb()"> + <% + module_breadcrumb = [] + + sm = module + while sm is not None: + module_breadcrumb.append(sm) + sm = sm.supermodule + + module_breadcrumb.reverse() + %> + + + + +<%def name="show_var(v, is_nested=False)"> + <% return_type = get_annotation(v.type_annotation) %> +
+
${link(v, anchor=True)}${return_type}
+
+
${v.docstring | to_html}
+ + +<%def name="show_func(f, is_nested=False)"> + <% + params = f.params(annotate=show_type_annotations, link=link) + return_type = get_annotation(f.return_annotation, '->') + example_str = f.funcdef() + f.name + "(" + ", ".join(params) + ")" + return_type + + if len(params) > 4 or len(example_str) > 70: + representation = "\n".join(( + f.funcdef() + " " + f.name + "(", + *(f" {p}," for p in params), + ")" + return_type + ": ..." + )) + else: + representation = f"{f.funcdef()} {f.name}(){return_type}: ..." + %> +
+
${representation}
+
+
+ ${show_desc(f)} + + ${show_source(f)} +
+
+ + + +<%def name="show_class(c, is_nested=False)"> + <% + class_vars = c.class_variables(show_inherited_members, sort=sort_identifiers) + smethods = c.functions(show_inherited_members, sort=sort_identifiers) + inst_vars = c.instance_variables(show_inherited_members, sort=sort_identifiers) + methods = c.methods(show_inherited_members, sort=sort_identifiers) + mro = c.mro() + subclasses = c.subclasses() + + params = c.params(annotate=show_type_annotations, link=link) + example_str = "class " + c.name + "(" + ", ".join(params) + ")" + + if len(params) > 4 or len(example_str) > 70: + representation = "\n".join(( + f"class {c.name} (", + *(f" {p}," for p in params), + "): ..." + )) + elif params: + representation = f"class {c.name} (" + ", ".join(params) + "): ..." + else: + representation = f"class {c.name}: ..." + %> +
+

${link(c, with_prefixes=True, simple_names=True)}

+
+
+
${representation}
+ + ${show_desc(c)} +
+ + % if subclasses: +
Subclasses
+
+ % for sc in subclasses: +
${link(sc, with_prefixes=True, default_type="class")}
+
${sc.docstring | glimpse, to_html}
+ % endfor +
+
+ % endif + + % if mro: +
Method resolution order
+
+
${link(c, with_prefixes=True)}
+
That's this class!
+ % for mro_c in mro: +
${link(mro_c, with_prefixes=True, default_type="class")}
+
${mro_c.docstring | glimpse, to_html}
+ % endfor +
+
+ % endif + + % if class_vars: +
Class variables
+
+ % for cv in class_vars: + ${show_var(cv)} + % endfor +
+
+ % endif + + % if smethods: +
Class methods
+
+ % for m in smethods: + ${show_func(m)} + % endfor +
+
+ % endif + + % if inst_vars: +
Instance variables
+
+ % for i in inst_vars: + ${show_var(i)} + % endfor +
+
+ % endif + + % if methods: +
Instance methods
+
+ % for m in methods: + ${show_func(m)} + % endfor +
+
+ % endif +
+ + +<%def name="show_desc(d, short=False)"> + + <% + inherits = ' inherited' if d.inherits else '' + # docstring = glimpse(d.docstring) if short or inherits else d.docstring + docstring = d.docstring + %> + % if d.inherits: +

+ Inherited from: + % if hasattr(d.inherits, 'cls'): + ${link(d.inherits.cls, with_prefixes=False)}.${link(d.inherits, name=d.name, with_prefixes=False)} + % else: + ${link(d.inherits, with_prefixes=False)} + % endif +

+ % endif + + ${docstring | to_html} + + +<%def name="show_source(d)"> + % if (show_source_code or git_link_template) and d.source and d.obj is not getattr(d.inherits, 'obj', None): + <% git_link = format_git_link(git_link_template, d) %> + % if show_source_code: +
+ + Expand source code + % if git_link: +
+ Browse git + %endif +
+
${d.source | h}
+
+ % elif git_link: + + %endif + %endif + + +
+
+

${breadcrumb()}

+

${module.docstring | to_html}

+
+
+ +
+
+ <% + variables = module.variables(sort=sort_identifiers) + classes = module.classes(sort=sort_identifiers) + functions = module.functions(sort=sort_identifiers) + submodules = module.submodules() + supermodule = module.supermodule + %> + +
+ +
+ +
+
+
+ ${show_source(module)} +
+
+ + + % if submodules: +

Child Modules

+
+
+ % for m in submodules: +
${link(m, simple_names=True, with_prefixes=True, anchor=True)}
+
${m.docstring | glimpse, to_html}
+ % endfor +
+
+ % endif + + % if variables: +

Variables and Type Hints

+
+
+ % for v in variables: + ${show_var(v)} + % endfor +
+
+ % endif + + % if functions: +

Functions

+
+
+ % for f in functions: + ${show_func(f)} + % endfor +
+
+ % endif + + % if classes: +

Classes

+
+
+ % for c in classes: + ${show_class(c)} + % endfor +
+
+ % endif +
+
+
\ No newline at end of file diff --git a/docs/footer.mako b/docs/footer.mako new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs/head.mako b/docs/head.mako index 7b2e559953..aeb75c4bc7 100644 --- a/docs/head.mako +++ b/docs/head.mako @@ -1,93 +1,24 @@ - -<%! - import os - from pdoc.html_helpers import minify_css -%> -% if "CI_MERGE_REQUEST_IID" in os.environ: - -% endif + \ No newline at end of file diff --git a/docs/html.mako b/docs/html.mako index 80c4baaadb..8308d0c84a 100644 --- a/docs/html.mako +++ b/docs/html.mako @@ -1,490 +1,98 @@ -<% - import os - - import pdoc - from pdoc.html_helpers import extract_toc, glimpse, to_html as _to_html, format_git_link - - - def link(dobj: pdoc.Doc, name=None): - name = name or dobj.qualname + ('()' if isinstance(dobj, pdoc.Function) else '') - if isinstance(dobj, pdoc.External) and not external_links: - return name - url = dobj.url(relative_to=module, link_prefix=link_prefix, - top_ancestor=not show_inherited_members) - return '{}'.format(dobj.refname, url, name) - - - def to_html(text): - return _to_html(text, module=module, link=link, latex_math=latex_math) - - - def get_annotation(bound_method, sep=':'): - annot = show_type_annotations and bound_method(link=link) or '' - annot = annot.replace("NoneType", "None") - # Remove quotes. - if annot.startswith("'") and annot.endswith("'"): - annot = annot[1:-1] - if annot: - annot = ' ' + sep + '\N{NBSP}' + annot - return annot +## Copyright © Nekokatt 2019-2020 +## +## This file is part of Hikari. +## +## Hikari is free software: you can redistribute it and/or modify +## it under the terms of the GNU Lesser General Public License as published by +## the Free Software Foundation, either version 3 of the License, or +## (at your option) any later version. +## +## Hikari is distributed in the hope that it will be useful, +## but WITHOUT ANY WARRANTY; without even the implied warranty of +## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +## GNU Lesser General Public License for more details. +## +## You should have received a copy of the GNU Lesser General Public License +## along with Hikari. If not, see . + +############################# IMPORTS ############################## +<%! + import os + from pdoc import html_helpers %> -<%def name="ident(name)">${name} - -<%def name="show_source(d)"> - % if (show_source_code or git_link_template) and d.source and d.obj is not getattr(d.inherits, 'obj', None): - <% git_link = format_git_link(git_link_template, d) %> - % if show_source_code: -
- - Expand source code - % if git_link: - Browse git - %endif - -
${d.source | h}
-
- % elif git_link: - - %endif - %endif - - -<%def name="show_desc(d, short=False)"> - <% - inherits = ' inherited' if d.inherits else '' - # docstring = glimpse(d.docstring) if short or inherits else d.docstring - docstring = d.docstring - %> - % if d.inherits: -

- Inherited from: - % if hasattr(d.inherits, 'cls'): - ${link(d.inherits.cls)}.${link(d.inherits, d.name)} - % else: - ${link(d.inherits)} - % endif -

- % endif - - <% - # Don't quote this bit. - #
${docstring | to_html}
- %> - ${docstring | to_html} - - % if not isinstance(d, pdoc.Module): - ${show_source(d)} - % endif - - -<%def name="show_module_list(modules)"> -

Python module list

- -% if not modules: -

No modules found.

-% else: -
- % for name, desc in modules: -
-
${name}
-
${desc | glimpse, to_html}
-
- % endfor -
-% endif - - -<%def name="show_column_list(items)"> - <% - two_column = len(items) >= 6 and all(len(i.name) < 20 for i in items) - %> -
    - % for item in items: -
  • ${link(item, item.name)}
  • - % endfor -
- - -<%def name="show_module(module)"> - <% - variables = module.variables(sort=sort_identifiers) - classes = module.classes(sort=sort_identifiers) - functions = module.functions(sort=sort_identifiers) - submodules = module.submodules() - %> - - <%def name="show_func(f)"> -
- - <% - params = f.params(annotate=show_type_annotations, link=link) - return_type = get_annotation(f.return_annotation, '->') - - example_str = f.funcdef() + f.name + "(" + ", ".join(params) + ")" + return_type - %> - - % if len(params) > 4 or len(example_str) > 70: - <% - params = "".join(p + ',
' for p in params) - %> -
${f.funcdef()} ${ident(f.name)} (
-
${params}
-
) ${return_type}
- % else: - <% - params = ", ".join(params) - %> -
${f.funcdef()} ${ident(f.name)} (${params}) ${return_type}
- % endif -
-
-
${show_desc(f)}
- - -
- % if http_server: - - % endif - - <% - module_name = "​.".join(module.name.split('.')) - %> -

${'Namespace' if module.is_namespace else \ - 'Package' if module.is_package and not module.supermodule else \ - 'Module'}
${module_name}

-
- -
- ${module.docstring | to_html} - ${show_source(module)} -
- -
- % if submodules: -

Sub-modules

-
- % for m in submodules: -
${link(m)}
-
${show_desc(m, short=True)}
- % endfor -
- % endif -
- -
- % if variables: -

Global variables

-
- % for v in variables: - <% return_type = get_annotation(v.type_annotation) %> -
var ${ident(v.name)}${return_type}
-
${show_desc(v)}
- % endfor -
- % endif -
- -
- % if functions: -

Functions

-
- % for f in functions: - ${show_func(f)} - % endfor -
- % endif -
- -
- % if classes: -

Classes

-
- % for c in classes: - <% - class_vars = c.class_variables(show_inherited_members, sort=sort_identifiers) - smethods = c.functions(show_inherited_members, sort=sort_identifiers) - inst_vars = c.instance_variables(show_inherited_members, sort=sort_identifiers) - methods = c.methods(show_inherited_members, sort=sort_identifiers) - mro = c.mro() - subclasses = c.subclasses() - #params = ', '.join(c.params(annotate=show_type_annotations, link=link)) - - params = c.params(annotate=show_type_annotations, link=link) - %> - -
- % if len(params) > 4 or len(", ".join(params)) > 50: - <% - params = "".join(p + ',
' for p in params) - %> -
class ${ident(c.name)} (
-
${params}
-
)
- % elif params: - class ${ident(c.name)} - <% - params = ", ".join(params) - %> - (${params}) - % else: - class ${ident(c.name)} - % endif -
- -
${show_desc(c)} - - % if mro: -

Ancestors

-
    - % for cls in mro: -
  • ${link(cls)}
  • - % endfor -
- %endif - - % if subclasses: -

Subclasses

-
    - % for sub in subclasses: -
  • ${link(sub)}
  • - % endfor -
- % endif - % if class_vars: -

Class variables

-
- % for v in class_vars: - <% return_type = get_annotation(v.type_annotation) %> -
var ${ident(v.name)}${return_type}
-
${show_desc(v)}
- % endfor -
- % endif - % if smethods: -

Static methods

-
- % for f in smethods: - ${show_func(f)} - % endfor -
- % endif - % if inst_vars: -

Instance variables

-
- % for v in inst_vars: - <% return_type = get_annotation(v.type_annotation) %> -
var ${ident(v.name)}${return_type}
-
${show_desc(v)}
- % endfor -
- % endif - % if methods: -

Methods

-
- % for f in methods: - ${show_func(f)} - % endfor -
- % endif - - % if not show_inherited_members: - <% - members = c.inherited_members() - %> - % if members: -

Inherited members

-
    - % for cls, mems in members: -
  • ${link(cls)}: -
      - % for m in mems: -
    • ${link(m, name=m.name)}
    • - % endfor -
    - -
  • - % endfor -
- % endif - % endif - -
- % endfor -
- % endif -
- - -<%def name="module_index(module)"> - <% - variables = module.variables(sort=sort_identifiers) - classes = module.classes(sort=sort_identifiers) - functions = module.functions(sort=sort_identifiers) - submodules = module.submodules() - supermodule = module.supermodule - %> - - +########################### CONFIGURATION ########################## +<%include file="config.mako"/> +############################ COMPONENTS ############################ - - - - - - -<% - module_list = 'modules' in context.keys() # Whether we're showing module list in server mode -%> - - % if module_list: - Python module list - - % else: - ${module.name} API documentation - - % endif - - - - % if syntax_highlighting: - - %endif - - <%namespace name="css" file="css.mako" /> - - - - - % if google_analytics: - - % endif - - % if search_query: - - - % endif + + + + + + % if module_list: + Python module list + + % else: + ${module.name} API documentation + + % endif - % if latex_math: - - % endif + ## Determine how to name the page. + % if "." in module.name: + + % else: + + % endif - <%include file="head.mako"/> - - -
- % if module_list: -
- ${show_module_list(modules)} -
- % else: -
- ${show_module(module)} -
- ${module_index(module)} - % endif -
+ + + + + + + ## Google Search Engine integration + + + + ## Bootstrap 4 stylesheet + + ## Highlight.js stylesheet + + ## Custom stylesheets + + + + ## Provide LaTeX math support + + + ## If this is a merge request on GitLab, inject the visual feedback scripts. + % if "CI_MERGE_REQUEST_IID" in os.environ: + <% print("Injecting Visual Feedback GitLab scripts") %> + + % endif + - + -% if syntax_highlighting: - - -% endif + <%include file="body.mako" /> -% if http_server and module: ## Auto-reload on file change in dev mode - -% endif - - + ## Script dependencies for Bootstrap. + + + + ## Highlightjs stuff + + + + \ No newline at end of file diff --git a/docs/logo.mako b/docs/logo.mako deleted file mode 100644 index e35ab00dbd..0000000000 --- a/docs/logo.mako +++ /dev/null @@ -1,35 +0,0 @@ -## Copyright © Nekokatt 2019-2020 -## -## This file is part of Hikari. -## -## Hikari is free software: you can redistribute it and/or modify -## it under the terms of the GNU Lesser General Public License as published by -## the Free Software Foundation, either version 3 of the License, or -## (at your option) any later version. -## -## Hikari is distributed in the hope that it will be useful, -## but WITHOUT ANY WARRANTY; without even the implied warranty of -## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -## GNU Lesser General Public License for more details. -## -## You should have received a copy of the GNU Lesser General Public License -## along with Hikari. If not, see . -<%! - from distutils import version - - import hikari - - version = "staging" if "dev" in version.LooseVersion(hikari.__version__).version else "production" -%> -
- - - Hikari - - - -
diff --git a/hikari/__init__.py b/hikari/__init__.py index 0759a45d10..204cef0e7c 100644 --- a/hikari/__init__.py +++ b/hikari/__init__.py @@ -16,10 +16,12 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . -"""Hikari's models framework for writing Discord bots in Python.""" +"""A sane Python framework for writing modern Discord bots.""" from __future__ import annotations +import functools as _functools + from hikari._about import __author__ from hikari._about import __ci__ from hikari._about import __copyright__ @@ -30,8 +32,14 @@ from hikari._about import __license__ from hikari._about import __url__ from hikari._about import __version__ +from hikari.app import * +from hikari.cache import * +from hikari.component import * +from hikari.entity_factory import * from hikari.errors import * from hikari.events import * +from hikari.event_consumer import * +from hikari.event_dispatcher import * from hikari.http_settings import * from hikari.models import * from hikari.net import * diff --git a/hikari/app.py b/hikari/app.py index 972a2d5f58..8362275914 100644 --- a/hikari/app.py +++ b/hikari/app.py @@ -16,7 +16,8 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . -"""Contains the core interfaces for a Hikari application.""" +"""Core interfaces for types of Hikari application.""" + from __future__ import annotations __all__ = ["IApp", "IRESTApp", "IGatewayConsumer", "IGatewayDispatcher", "IGatewayZookeeper", "IBot"] diff --git a/hikari/cache.py b/hikari/cache.py index 52b21b7338..78d0028b92 100644 --- a/hikari/cache.py +++ b/hikari/cache.py @@ -16,7 +16,7 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . -"""Utilities to handle cache management.""" +"""Core interface for a cache implementation.""" from __future__ import annotations __all__ = ["ICache"] diff --git a/hikari/component.py b/hikari/component.py index c2ea1df15c..71d55e83cb 100644 --- a/hikari/component.py +++ b/hikari/component.py @@ -16,7 +16,7 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . -"""Defines a base interface for application components to derive from.""" +"""Core interface that any component should derive from""" from __future__ import annotations diff --git a/hikari/entity_factory.py b/hikari/entity_factory.py index 568dc9f62a..d15c864e3a 100644 --- a/hikari/entity_factory.py +++ b/hikari/entity_factory.py @@ -16,7 +16,7 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . -"""Contains an interface for components wishing to build entities.""" +"""Core interface for an object that serializes/deserializes API objects.""" from __future__ import annotations __all__ = ["IEntityFactory"] diff --git a/hikari/errors.py b/hikari/errors.py index df4028e20d..7be8b10606 100644 --- a/hikari/errors.py +++ b/hikari/errors.py @@ -16,7 +16,7 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . -"""Core errors that may be raised by this API implementation.""" +"""Exceptions and warnings that can be thrown by this library.""" from __future__ import annotations diff --git a/hikari/event_consumer.py b/hikari/event_consumer.py index ea08fca46e..b2faaab4eb 100644 --- a/hikari/event_consumer.py +++ b/hikari/event_consumer.py @@ -16,7 +16,7 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . -"""Contains an interface which describes component that can consume raw gateway events.""" +"""Core interface for components that consume raw API event payloads.""" from __future__ import annotations __all__ = ["IEventConsumer"] diff --git a/hikari/event_dispatcher.py b/hikari/event_dispatcher.py index 616b0644dd..8cccd632f7 100644 --- a/hikari/event_dispatcher.py +++ b/hikari/event_dispatcher.py @@ -16,7 +16,7 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . -"""Interface providing functionality to dispatch an event object.""" +"""Core interface for components that dispatch events to the library.""" from __future__ import annotations __all__ = ["IEventDispatcher"] diff --git a/hikari/http_settings.py b/hikari/http_settings.py index e3de20fe18..f4f62f8629 100644 --- a/hikari/http_settings.py +++ b/hikari/http_settings.py @@ -16,7 +16,7 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . -"""Configuration data classes.""" +"""Data class containing AIOHTTP-specific configuration settings.""" from __future__ import annotations diff --git a/hikari/impl/bot.py b/hikari/impl/bot.py index e29d8f01f3..e1d1456314 100644 --- a/hikari/impl/bot.py +++ b/hikari/impl/bot.py @@ -155,4 +155,4 @@ async def close(self) -> None: await self._rest.close() async def _fetch_gateway_recommendations(self) -> gateway_models.GatewayBot: - return await self.rest.fetch_recommended_gateway_settings() + return await self.rest.fetch_gateway_bot() diff --git a/hikari/net/ratelimits.py b/hikari/net/ratelimits.py index b211c9074d..1e664d0290 100644 --- a/hikari/net/ratelimits.py +++ b/hikari/net/ratelimits.py @@ -23,35 +23,35 @@ correctly. What is the theory behind this implementation? -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +---------------------------------------------- -In this module, we refer to a `hikari.rest.routes.CompiledRoute` as a definition -of a _route with specific major parameter values included (e.g. -`POST /channels/123/messages`), and a `hikari.rest.routes.Route` as a -definition of a _route without specific parameter values included (e.g. +In this module, we refer to a `hikari.net.routes.CompiledRoute` as a definition +of a route with specific major parameter values included (e.g. +`POST /channels/123/messages`), and a `hikari.net.routes.Route` as a +definition of a route without specific parameter values included (e.g. `POST /channels/{channel}/messages`). We can compile a -`hikari.rest.routes.CompiledRoute` from a `hikari.rest.routes.Route` +`hikari.net.routes.CompiledRoute` from a `hikari.net.routes.Route` by providing the corresponding parameters as kwargs, as you may already know. In this module, a "bucket" is an internal data structure that tracks and -enforces the rate limit state for a specific `hikari.rest.routes.CompiledRoute`, +enforces the rate limit state for a specific `hikari.net.routes.CompiledRoute`, and can manage delaying tasks in the event that we begin to get rate limited. It also supports providing in-order execution of queued tasks. Discord allocates types of buckets to routes. If you are making a request and -there is a valid rate limit on the _route you hit, you should receive an +there is a valid rate limit on the route you hit, you should receive an `X-RateLimit-Bucket` header from the server in your response. This is a hash -that identifies a _route based on internal criteria that does not include major +that identifies a route based on internal criteria that does not include major parameters. This `X-RateLimitBucket` is known in this module as an "bucket hash". -This means that generally, the _route `POST /channels/123/messages` and +This means that generally, the route `POST /channels/123/messages` and `POST /channels/456/messages` will usually sit in the same bucket, but `GET /channels/123/messages/789` and `PATCH /channels/123/messages/789` will usually not share the same bucket. Discord may or may not change this at any time, so hard coding this logic is not a useful thing to be doing. Rate limits, on the other hand, apply to a bucket and are specific to the major -parameters of the compiled _route. This means that `POST /channels/123/messages` +parameters of the compiled route. This means that `POST /channels/123/messages` and `POST /channels/456/messages` do not share the same real bucket, despite Discord providing the same bucket hash. A real bucket hash is the `str` hash of the bucket that Discord sends us in a response concatenated to the corresponding @@ -59,28 +59,28 @@ module. One issue that occurs from this is that we cannot effectively hash a -`hikari.rest.routes.CompiledRoute` that has not yet been hit, meaning that +`hikari.net.routes.CompiledRoute` that has not yet been hit, meaning that until we receive a response from this endpoint, we have no idea what our rate limits could be, nor the bucket that they sit in. This is usually not problematic, as the first request to an endpoint should never be rate limited unless you are hitting it from elsewhere in the same time window outside your hikari.models.applications. To manage this situation, unknown endpoints are allocated to a special unlimited bucket until they have an initial bucket hash code allocated -from a response. Once this happens, the _route is reallocated a dedicated bucket. +from a response. Once this happens, the route is reallocated a dedicated bucket. Unknown buckets have a hardcoded initial hash code internally. Initially acquiring time on a bucket -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +------------------------------------ Each time you `BaseRateLimiter.acquire()` a request timeslice for a given -`hikari.rest.routes.Route`, several things happen. The first is that we -attempt to find the existing bucket for that _route, if there is one, or get an +`hikari.net.routes.Route`, several things happen. The first is that we +attempt to find the existing bucket for that route, if there is one, or get an unknown bucket otherwise. This is done by creating a real bucket hash from the -compiled _route. The initial hash is calculated using a lookup table that maps -`hikari.rest.routes.CompiledRoute` objects to their corresponding initial hash +compiled route. The initial hash is calculated using a lookup table that maps +`hikari.net.routes.CompiledRoute` objects to their corresponding initial hash codes, or to the unknown bucket hash code if not yet known. This initial hash is -processed by the `hikari.rest.routes.CompiledRoute` to provide the real bucket -hash we need to get the _route's bucket object internally. +processed by the `hikari.net.routes.CompiledRoute` to provide the real bucket +hash we need to get the route's bucket object internally. The `acquire` method will take the bucket and acquire a new timeslice on it. This takes the form of a `asyncio.Future` which should be awaited by @@ -108,7 +108,7 @@ `1` second to `1` millisecond). Handling the rate limit headers of a response -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +--------------------------------------------- Once you have received your response, you are expected to extract the values of the vital rate limit headers manually and parse them to the correct data types. @@ -141,7 +141,7 @@ information in each bucket you use. Tidying up -~~~~~~~~~~ +---------- To prevent unused buckets cluttering up memory, each `BaseRateLimiter` instance spins up a `asyncio.Task` that periodically locks the bucket list diff --git a/hikari/net/rest.py b/hikari/net/rest.py index 40b66faf96..bbc84fd1a2 100644 --- a/hikari/net/rest.py +++ b/hikari/net/rest.py @@ -1423,7 +1423,7 @@ async def fetch_gateway_url(self) -> str: response = await self._request(route, no_auth=True) return response["url"] - async def fetch_recommended_gateway_settings(self) -> gateway.GatewayBot: + async def fetch_gateway_bot(self) -> gateway.GatewayBot: route = routes.GET_GATEWAY_BOT.compile() response = await self._request(route) return self._app.entity_factory.deserialize_gateway_bot(response) diff --git a/hikari/net/routes.py b/hikari/net/routes.py index de1afdd530..06bc9928b5 100644 --- a/hikari/net/routes.py +++ b/hikari/net/routes.py @@ -32,12 +32,12 @@ class CompiledRoute: - """A compiled representation of a _route ready to be made into a full e and to be used for a request. + """A compiled representation of a route ready to be made into a full e and to be used for a request. Parameters ---------- route : Route - The _route used to make this compiled _route. + The route used to make this compiled route. path : str The path with any major parameters interpolated in. major_params_hash : str @@ -47,13 +47,13 @@ class CompiledRoute: __slots__ = ("route", "major_param_hash", "compiled_path", "hash_code") route: typing.Final[Route] - """The _route this compiled _route was created from.""" + """The route this compiled route was created from.""" major_param_hash: typing.Final[str] """The major parameters in a bucket hash-compatible representation.""" compiled_path: typing.Final[str] - """The compiled _route path to use.""" + """The compiled route path to use.""" hash_code: typing.Final[int] """The hash code.""" @@ -66,7 +66,7 @@ def __init__(self, route: Route, path: str, major_params_hash: str) -> None: @property def method(self) -> str: - """Return the HTTP method of this compiled _route.""" + """Return the HTTP method of this compiled route.""" return self.route.method def create_url(self, base_url: str) -> str: @@ -80,7 +80,7 @@ def create_url(self, base_url: str) -> str: Returns ------- str - The full URL for the _route. + The full URL for the route. """ return base_url + self.compiled_path @@ -88,7 +88,7 @@ def create_real_bucket_hash(self, initial_bucket_hash: str) -> str: """Create a full bucket hash from a given initial hash. The result of this hash will be decided by the value of the major - parameters passed to the _route during the compilation phase. + parameters passed to the route during the compilation phase. Parameters ---------- @@ -100,7 +100,7 @@ def create_real_bucket_hash(self, initial_bucket_hash: str) -> str: ------- str The input hash amalgamated with a hash code produced by the - major parameters in this compiled _route instance. + major parameters in this compiled route instance. """ return initial_bucket_hash + HASH_SEPARATOR + self.major_param_hash @@ -174,19 +174,19 @@ def __init__(self, method: str, path_template: str) -> None: self.hash_code = hash((self.method, self.path_template)) def compile(self, **kwargs: typing.Any) -> CompiledRoute: - """Generate a formatted `CompiledRoute` for this _route. + """Generate a formatted `CompiledRoute` for this route. This takes into account any URL parameters that have been passed. Parameters ---------- **kwargs : typing.Any - Any parameters to interpolate into the _route path. + Any parameters to interpolate into the route path. Returns ------- CompiledRoute - The compiled _route. + The compiled route. """ data = data_binding.StringMapBuilder.from_dict(kwargs) diff --git a/hikari/utilities/__init__.py b/hikari/utilities/__init__.py index 970a1c6e91..2cf29c2cb2 100644 --- a/hikari/utilities/__init__.py +++ b/hikari/utilities/__init__.py @@ -16,24 +16,8 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . -"""Various utilities used internally within this API.""" +"""Package containing internal utilities used within this API.""" from __future__ import annotations -from hikari.utilities.aio import * -from hikari.utilities.data_binding import * -from hikari.utilities.date import * -from hikari.utilities.klass import * -from hikari.utilities.reflect import * -from hikari.utilities.snowflake import * -from hikari.utilities.undefined import * - -__all__ = [ - *aio.__all__, - *data_binding.__all__, - *date.__all__, - *klass.__all__, - *reflect.__all__, - *snowflake.__all__, - *undefined.__all__, -] +__all__ = [] diff --git a/hikari/utilities/aio.py b/hikari/utilities/aio.py index 45cfaf9452..89319f1d76 100644 --- a/hikari/utilities/aio.py +++ b/hikari/utilities/aio.py @@ -20,7 +20,7 @@ from __future__ import annotations -__all__ = ["completed_future", "is_async_iterator", "is_async_iterable"] +__all__ = ["completed_future", "is_async_iterator", "is_async_iterable", "Future", "Task"] import asyncio import inspect @@ -30,20 +30,22 @@ import contextvars import types -_T_contra = typing.TypeVar("_T_contra", contravariant=True) +T_contra = typing.TypeVar("T_contra", contravariant=True) -def completed_future(result: _T_contra = None, /) -> Future[_T_contra]: +def completed_future(result: typing.Optional[T_contra] = None, /) -> Future[typing.Optional[T_contra]]: """Create a future on the current running loop that is completed, then return it. Parameters ---------- - result : typing.Any + result : T_contra or None The value to set for the result of the future. + `T_contra` is a generic type placeholder for the type that + the future will have set as the result. Returns ------- - asyncio.Future + Future[T_contra or None] The completed future. """ future = asyncio.get_event_loop().create_future() @@ -80,16 +82,16 @@ def is_async_iterable(obj: typing.Any) -> bool: @typing.runtime_checkable -class Future(typing.Protocol[_T_contra]): +class Future(typing.Protocol[T_contra]): """Typed protocol representation of an `asyncio.Future`. You should consult the documentation for `asyncio.Future` for usage. """ - def result(self) -> _T_contra: + def result(self) -> T_contra: """See `asyncio.Future.result`.""" - def set_result(self, result: _T_contra, /) -> None: + def set_result(self, result: T_contra, /) -> None: """See `asyncio.Future.set_result`.""" def set_exception(self, exception: Exception, /) -> None: @@ -102,11 +104,11 @@ def cancelled(self) -> bool: """See `asyncio.Future.cancelled`.""" def add_done_callback( - self, callback: typing.Callable[[Future[_T_contra]], None], /, *, context: typing.Optional[contextvars.Context], + self, callback: typing.Callable[[Future[T_contra]], None], /, *, context: typing.Optional[contextvars.Context], ) -> None: """See `asyncio.Future.add_done_callback`.""" - def remove_done_callback(self, callback: typing.Callable[[Future[_T_contra]], None], /) -> None: + def remove_done_callback(self, callback: typing.Callable[[Future[T_contra]], None], /) -> None: """See `asyncio.Future.remove_done_callback`.""" def cancel(self) -> bool: @@ -118,21 +120,21 @@ def exception(self) -> typing.Optional[Exception]: def get_loop(self) -> asyncio.AbstractEventLoop: """See `asyncio.Future.get_loop`.""" - def __await__(self) -> typing.Generator[_T_contra, None, typing.Any]: + def __await__(self) -> typing.Generator[T_contra, None, typing.Any]: ... @typing.runtime_checkable -class Task(typing.Protocol[_T_contra]): +class Task(typing.Protocol[T_contra]): """Typed protocol representation of an `asyncio.Task`. You should consult the documentation for `asyncio.Task` for usage. """ - def result(self) -> _T_contra: + def result(self) -> T_contra: """See`asyncio.Future.result`.""" - def set_result(self, result: _T_contra, /) -> None: + def set_result(self, result: T_contra, /) -> None: """See `asyncio.Future.set_result`.""" def set_exception(self, exception: Exception, /) -> None: @@ -145,11 +147,11 @@ def cancelled(self) -> bool: """See `asyncio.Future.cancelled`.""" def add_done_callback( - self, callback: typing.Callable[[Future[_T_contra]], None], /, *, context: typing.Optional[contextvars.Context], + self, callback: typing.Callable[[Future[T_contra]], None], /, *, context: typing.Optional[contextvars.Context], ) -> None: """See `asyncio.Future.add_done_callback`.""" - def remove_done_callback(self, callback: typing.Callable[[Future[_T_contra]], None], /) -> None: + def remove_done_callback(self, callback: typing.Callable[[Future[T_contra]], None], /) -> None: """See `asyncio.Future.remove_done_callback`.""" def cancel(self) -> bool: @@ -173,5 +175,5 @@ def get_name(self) -> str: def set_name(self, value: str, /) -> None: """See `asyncio.Task.set_name`.""" - def __await__(self) -> typing.Generator[_T_contra, None, typing.Any]: + def __await__(self) -> typing.Generator[T_contra, None, typing.Any]: ... diff --git a/hikari/utilities/data_binding.py b/hikari/utilities/data_binding.py index 720dd9c666..c5e996c2cc 100644 --- a/hikari/utilities/data_binding.py +++ b/hikari/utilities/data_binding.py @@ -24,10 +24,6 @@ "Query", "JSONObject", "JSONArray", - "JSONNull", - "JSONBoolean", - "JSONString", - "JSONNumber", "JSONAny", "URLEncodedForm", "MultipartForm", @@ -45,41 +41,33 @@ from hikari.models import bases from hikari.utilities import undefined + +T = typing.TypeVar("T", covariant=True) +CollectionT = typing.TypeVar("CollectionT", bound=typing.Collection, contravariant=True) + Headers = typing.Mapping[str, str] -"""HTTP headers.""" +"""Type hint for HTTP headers.""" Query = typing.Dict[str, str] -"""HTTP query string.""" +"""Type hint for HTTP query string.""" URLEncodedForm = aiohttp.FormData -"""Content of type application/x-www-form-encoded""" +"""Type hint for content of type application/x-www-form-encoded""" MultipartForm = aiohttp.FormData -"""Content of type multipart/form-data""" - -JSONString = str -"""A JSON string.""" - -JSONNumber = typing.Union[int, float] -"""A JSON numeric value.""" - -JSONBoolean = bool -"""A JSON boolean value.""" - -JSONNull = None -"""A null JSON value.""" +"""Type hint for content of type multipart/form-data""" # MyPy does not support recursive types yet. This has been ongoing for a long time, unfortunately. # See https://github.com/python/typing/issues/182 -JSONObject = typing.Dict[JSONString, typing.Any] -"""A JSON object representation as a dict.""" +JSONObject = typing.Dict[str, typing.Any] +"""Type hint for a JSON-decoded object representation as a dict.""" JSONArray = typing.List[typing.Any] -"""A JSON array representation as a list.""" +"""Type hint for a JSON-decoded array representation as a list.""" -JSONAny = typing.Union[JSONString, JSONNumber, JSONBoolean, JSONNull, JSONArray, JSONObject] -"""Any JSON type.""" +JSONAny = typing.Union[str, int, float, bool, None, JSONArray, JSONObject] +"""Type hint for any valid JSON-decoded type.""" if typing.TYPE_CHECKING: @@ -99,11 +87,24 @@ def load_json(_: str) -> typing.Union[JSONArray, JSONObject]: class StringMapBuilder(typing.Dict[str, str]): - """Helper class used to quickly build query strings or header maps.""" + """Helper class used to quickly build query strings or header maps. + + This will consume any items that are not + `hikari.utilities.unspecified.Unspecified`. If a value _is_ unspecified, + it will be ignored when inserting it. This reduces the amount of + boilerplate needed for generating the headers and query strings for + low-level HTTP API interaction, amongst other things. + + + !!! warn + Because this subclasses `dict`, you should not use the + index operator to set items on this object. Doing so will skip any + form of validation on the type. Use the `put*` methods instead. + """ __slots__ = () - def __init__(self): + def __init__(self) -> None: super().__init__() def put( @@ -118,13 +119,16 @@ def put( ---------- key : str The string key. - value : hikari.utilities.undefined.Undefined | typing.Any + value : hikari.utilities.undefined.Undefined or typing.Any The value to set. - conversion : typing.Callable[[typing.Any], typing.Any] | None + conversion : typing.Callable[[typing.Any], typing.Any] or None An optional conversion to perform. !!! note The value will always be cast to a `str` before inserting it. + + `True` will be translated to `"true"`, `False` will be translated + to `"false"`, and `None` will be translated to `"null"`. """ if not isinstance(value, undefined.Undefined): if conversion is not None: @@ -157,8 +161,20 @@ def from_dict(cls, d: typing.Union[undefined.Undefined, typing.Dict[str, typing. return sb -class JSONObjectBuilder(typing.Dict[JSONString, JSONAny]): - """Helper class used to quickly build JSON objects from various values.""" +class JSONObjectBuilder(typing.Dict[str, JSONAny]): + """Helper class used to quickly build JSON objects from various values. + + If provided with any values that are + `hikari.utilities.unspecified.Unspecified`, then these values will be ignored. + + This speeds up generation of JSON payloads for low level HTTP and websocket + API interaction. + + !!! warn + Because this subclasses `dict`, you should not use the + index operator to set items on this object. Doing so will skip any + form of validation on the type. Use the `put*` methods instead. + """ __slots__ = () @@ -166,10 +182,7 @@ def __init__(self): super().__init__() def put( - self, - key: JSONString, - value: typing.Any, - conversion: typing.Optional[typing.Callable[[typing.Any], JSONAny]] = None, + self, key: str, value: typing.Any, conversion: typing.Optional[typing.Callable[[typing.Any], JSONAny]] = None, ) -> None: """Put a JSON value. @@ -177,7 +190,7 @@ def put( Parameters ---------- - key : JSONString + key : str The key to give the element. value : JSONType | typing.Any | hikari.utilities.undefined.Undefined The JSON type to put. This may be a non-JSON type if a conversion @@ -194,23 +207,25 @@ def put( def put_array( self, - key: JSONString, - values: typing.Union[undefined.Undefined, typing.Iterable[_T]], - conversion: typing.Optional[typing.Callable[[_T], JSONAny]] = None, + key: str, + values: typing.Union[undefined.Undefined, typing.Iterable[T]], + conversion: typing.Optional[typing.Callable[[T], JSONAny]] = None, ) -> None: """Put a JSON array. If the value is undefined, then it will not be stored. + If provided, a conversion will be applied to each item. + Parameters ---------- - key : JSONString + key : str The key to give the element. - values : JSONType | typing.Any | hikari.utilities.undefined.Undefined + values : JSONAny or Any or hikari.utilities.undefined.Undefined The JSON types to put. This may be an iterable of non-JSON types if a conversion is also specified. This may alternatively be undefined. In the latter case, nothing is performed. - conversion : typing.Callable[[typing.Any], JSONType] | None + conversion : typing.Callable[[typing.Any], JSONType] or None Optional conversion to apply. """ if not isinstance(values, undefined.Undefined): @@ -219,12 +234,12 @@ def put_array( else: self[key] = list(values) - def put_snowflake(self, key: JSONString, value: typing.Union[undefined.Undefined, typing.SupportsInt, int]) -> None: - """Put a snowflake. + def put_snowflake(self, key: str, value: typing.Union[undefined.Undefined, typing.SupportsInt, int]) -> None: + """Put a key with a snowflake value into the builder. Parameters ---------- - key : JSONString + key : str The key to give the element. value : JSONType | hikari.utilities.undefined.Undefined The JSON type to put. This may alternatively be undefined. In the latter @@ -234,28 +249,59 @@ def put_snowflake(self, key: JSONString, value: typing.Union[undefined.Undefined self[key] = str(int(value)) def put_snowflake_array( - self, key: JSONString, values: typing.Union[undefined.Undefined, typing.Iterable[typing.SupportsInt, int]] + self, key: str, values: typing.Union[undefined.Undefined, typing.Iterable[typing.SupportsInt, int]] ) -> None: - """Put an array of snowflakes. + """Put an array of snowflakes with the given key into this builder. + + If an undefined value is given, it is ignored. + + Each snowflake should be castable to an `int`. Parameters ---------- - key : JSONString + key : str The key to give the element. - values : typing.Iterable[typing.SupportsInt, int] | hikari.utilities.undefined.Undefined - The JSON snowflakes to put. This may alternatively be undefined. In the latter - case, nothing is performed. + values : typing.Iterable[typing.SupportsInt or int] or hikari.utilities.undefined.Undefined + The JSON snowflakes to put. This may alternatively be undefined. + In the latter case, nothing is performed. """ if not isinstance(values, undefined.Undefined): self[key] = [str(int(value)) for value in values] -_T = typing.TypeVar("_T", covariant=True) -_CT = typing.TypeVar("_CT", bound=typing.Collection, contravariant=True) - - def cast_json_array( - array: JSONArray, cast: typing.Callable[[JSONAny], _T], collection_type: typing.Type[_CT] = list -) -> _CT: - """Cast a JSON array to a given collection type, casting each item.""" + array: JSONArray, cast: typing.Callable[[JSONAny], T], collection_type: typing.Type[CollectionT] = typing.List[T], +) -> CollectionT: + """Cast a JSON array to a given generic collection type. + + This will perform casts on each internal item individually. + + Note that + + >>> cast_json_array(raw_list, foo, bar) + + ...is equivalent to doing.... + + >>> bar(foo(item) for item in raw_list) + + Parameters + ---------- + array : JSONArray + The raw JSON-decoded array. + cast : typing.Callable[[JSONAny], T] + The cast to apply to each item in the array. This should + consume any valid JSON-decoded type and return the type + corresponding to the generic type of the provided collection. + collection_type : typing.Type[CollectionT] + The container type to store the cast items within. + `CollectionT` should be a concrete implementation that is + a subtype of `typing.Collection`, such as `typing.List`, + `typing.Set`, `typing.FrozenSet`, `typing.Tuple`, etc. + If unspecified, this defaults to `typing.List`. + + Returns + ------- + CollectionT + The generated collection. + """ return collection_type(cast(item) for item in array) diff --git a/hikari/utilities/date.py b/hikari/utilities/date.py index 85b4239201..f2c9e02add 100644 --- a/hikari/utilities/date.py +++ b/hikari/utilities/date.py @@ -16,11 +16,12 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . -"""Utility methods used for parsing timestamps and datetimes.""" +"""Utility methods used for parsing timestamps and datetimes from Discord.""" from __future__ import annotations __all__ = [ + "DISCORD_EPOCH", "rfc7231_datetime_string_to_datetime", "datetime_to_discord_epoch", "discord_epoch_to_datetime", @@ -34,7 +35,26 @@ import re import typing + +TimeSpan = typing.Union[int, float, datetime.timedelta] +"""Type hint representing a naive time period or time span. + +This is an alias for `typing.Union[int, float, datetime.datetime]`, +where `int` and `float` types are interpreted as a number of seconds. +""" + + DISCORD_EPOCH: typing.Final[int] = 1_420_070_400 +"""The Discord epoch used within snowflake identifiers. + +This is defined as the number of seconds between +`1/1/1970 00:00:00 UTC` and `1/1/2015 00:00:00 UTC`. + +References +---------- +* [Discord API documentation - Snowflakes](https://discord.com/developers/docs/reference#snowflakes) +""" + ISO_8601_DATE_PART: typing.Final[typing.Pattern] = re.compile(r"^(\d{4})-(\d{2})-(\d{2})") ISO_8601_TIME_PART: typing.Final[typing.Pattern] = re.compile(r"T(\d{2}):(\d{2}):(\d{2})(?:\.(\d{1,6}))?", re.I) ISO_8601_TZ_PART: typing.Final[typing.Pattern] = re.compile(r"([+-])(\d{2}):(\d{2})$") @@ -55,8 +75,8 @@ def rfc7231_datetime_string_to_datetime(date_str: str, /) -> datetime.datetime: References ---------- - [RFC 2822](https://www.ietf.org/rfc/rfc2822.txt) - [Mozilla documentation for Date header](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Date) + * [RFC-2822](https://www.ietf.org/rfc/rfc2822.txt) + * [Mozilla documentation for `Date` HTTP header](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Date) """ # According to Mozilla, these are always going to be GMT (which is UTC). return email.utils.parsedate_to_datetime(date_str).replace(tzinfo=datetime.timezone.utc) @@ -68,16 +88,16 @@ def iso8601_datetime_string_to_datetime(date_string: str, /) -> datetime.datetim Parameters ---------- date_string : str - The ISO 8601 compliant date string to parse. + The ISO-8601 compliant date string to parse. Returns ------- datetime.datetime - The ISO 8601 date string as a datetime object. + The ISO-8601 date string as a datetime object. References ---------- - [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601) + * [ISO-8601](https://en.wikipedia.org/wiki/ISO_8601) """ year, month, day = map(int, ISO_8601_DATE_PART.findall(date_string)[0]) @@ -106,28 +126,28 @@ def discord_epoch_to_datetime(epoch: int, /) -> datetime.datetime: Parameters ---------- epoch : int - Number of milliseconds since 1/1/2015 (UTC) + Number of milliseconds since `1/1/2015 00:00:00 UTC`. Returns ------- datetime.datetime - Number of seconds since 1/1/1970 within a datetime object (UTC). + Number of seconds since `1/1/1970 00:00:00 UTC`. """ return datetime.datetime.fromtimestamp(epoch / 1_000 + DISCORD_EPOCH, datetime.timezone.utc) def datetime_to_discord_epoch(timestamp: datetime.datetime) -> int: - """Parse a `datetime.datetime` object into an integer discord epoch.. + """Parse a `datetime.datetime` object into an `int` `DISCORD_EPOCH` offset. Parameters ---------- timestamp : datetime.datetime - Number of seconds since 1/1/1970 within a datetime object (UTC). + Number of seconds since `1/1/1970 00:00:00 UTC`. Returns ------- int - Number of milliseconds since 1/1/2015 (UTC) + Number of milliseconds since `1/1/2015 00:00:00 UTC`. """ return int((timestamp.timestamp() - DISCORD_EPOCH) * 1_000) @@ -138,21 +158,22 @@ def unix_epoch_to_datetime(epoch: int, /) -> datetime.datetime: Parameters ---------- epoch : int - Number of milliseconds since 1/1/1970 (UTC) + Number of milliseconds since `1/1/1970 00:00:00 UTC`. Returns ------- datetime.datetime - Number of seconds since 1/1/1970 within a datetime object (UTC). + Number of seconds since `1/1/1970 00:00:00 UTC`. !!! note If an epoch that's outside the range of what this system can handle, - this will return `datetime.datetime.max` or `datetime.datetime.min`. + this will return `datetime.datetime.max` if the timestamp is positive, + or `datetime.datetime.min` otherwise. """ - try: - return datetime.datetime.fromtimestamp(epoch / 1000, datetime.timezone.utc) # Datetime seems to raise an OSError when you try to convert an out of range timestamp on Windows and a ValueError # if you try on a UNIX system so we want to catch both. + try: + return datetime.datetime.fromtimestamp(epoch / 1000, datetime.timezone.utc) except (OSError, ValueError): if epoch > 0: return datetime.datetime.max @@ -160,16 +181,12 @@ def unix_epoch_to_datetime(epoch: int, /) -> datetime.datetime: return datetime.datetime.min -TimeSpan = typing.Union[int, float, datetime.timedelta] -"""A representation of time.""" - - -def timespan_to_int(value: typing.Union[TimeSpan], /) -> int: +def timespan_to_int(value: TimeSpan, /) -> int: """Cast the given timespan in seconds to an integer value. Parameters ---------- - value : int | float | datetime.timedelta + value : TimeSpan The number of seconds. Returns diff --git a/pages/index.html b/pages/index.html index e1af28522f..495191e1c3 100644 --- a/pages/index.html +++ b/pages/index.html @@ -21,27 +21,26 @@ | along with Hikari. If not, see . !--> - - + href="https://assets.gitlab-static.net/uploads/-/system/project/avatar/12050696/Hikari-Logo_1.png" /> + + + content="https://assets.gitlab-static.net/uploads/-/system/project/avatar/12050696/Hikari-Logo_1.png" /> - - - + content="A Discord Bot framework for modern Python and asyncio built on good intentions" /> + + + Hikari.py - - + integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm" crossorigin="anonymous" /> + + -
+    
     
         #!/usr/bin/env python
         # -*- coding: utf-8 -*-
@@ -98,70 +97,71 @@
     
 
-
-
- -
- -
-

- pip install --pre
- hikari -

-

A new, powerful, sane Python API for writing Discord bots.

-

- This API is still in a pre-alpha state, and is a work in progress! Features may change - or undergo improvements before the design is finalized. Until then, why not join our Discord? Feel free - to drop in to ask questions, get updates on progress, and be able to provide valuable contributions and - feedback. -

-

- Tutorials, tips, and additional resources will come soon! -

-
-

- Slide into our server -

-
- -
-
- © 2019-2020, Nekoka.tt -
-
-
- - - - - - - + + - + + \ No newline at end of file From f9cddea56f180591efa39c3326d848d917dbe61c Mon Sep 17 00:00:00 2001 From: davfsa Date: Tue, 2 Jun 2020 10:42:41 +0200 Subject: [PATCH 426/922] Started writing documentation --- hikari/entity_factory.py | 1 + hikari/models/emojis.py | 2 +- hikari/models/messages.py | 4 +- hikari/models/webhooks.py | 2 +- hikari/net/rest.py | 292 ++++++++++++++++++++++++++------------ hikari/net/tracing.py | 2 +- 6 files changed, 211 insertions(+), 92 deletions(-) diff --git a/hikari/entity_factory.py b/hikari/entity_factory.py index d15c864e3a..03f0ed2a25 100644 --- a/hikari/entity_factory.py +++ b/hikari/entity_factory.py @@ -603,6 +603,7 @@ def deserialize_invite_with_metadata(self, payload: data_binding.JSONObject) -> # MESSAGES # ############ + @abc.abstractmethod def deserialize_message(self, payload: data_binding.JSONObject) -> messages.Message: """Parse a raw payload from Discord into a message object. diff --git a/hikari/models/emojis.py b/hikari/models/emojis.py index 2a3a2d3822..a31cb81c32 100644 --- a/hikari/models/emojis.py +++ b/hikari/models/emojis.py @@ -81,7 +81,7 @@ def __aiter__(self) -> typing.AsyncIterator[bytes]: class UnicodeEmoji(Emoji): """Represents a unicode emoji. - !!! warn + !!! warning A word of warning if you try to upload this emoji as a file attachment. While this emoji type can be used to upload the Twemoji representations diff --git a/hikari/models/messages.py b/hikari/models/messages.py index 2171a6f151..a3b777622c 100644 --- a/hikari/models/messages.py +++ b/hikari/models/messages.py @@ -392,7 +392,7 @@ async def safe_edit( *, content: str = ..., embed: embeds_.Embed = ..., - mentions_everyone: bool = False, + mentions_everyone: bool = True, user_mentions: typing.Union[ typing.Collection[typing.Union[snowflake.Snowflake, int, str, users.User]], bool ] = False, @@ -504,7 +504,7 @@ async def safe_reply( content: str = ..., embed: embeds_.Embed = ..., files: typing.Sequence[files_.BaseStream] = ..., - mentions_everyone: bool = False, + mentions_everyone: bool = True, user_mentions: typing.Union[ typing.Collection[typing.Union[snowflake.Snowflake, int, str, users.User]], bool ] = False, diff --git a/hikari/models/webhooks.py b/hikari/models/webhooks.py index 8d8efdb752..90006321d4 100644 --- a/hikari/models/webhooks.py +++ b/hikari/models/webhooks.py @@ -195,7 +195,7 @@ async def safe_execute( wait: bool = False, files: typing.Sequence[files_.BaseStream] = ..., embeds: typing.Sequence[embeds_.Embed] = ..., - mentions_everyone: bool = False, + mentions_everyone: bool = True, user_mentions: typing.Union[ typing.Collection[typing.Union[snowflake.Snowflake, int, str, users_.User]], bool ] = False, diff --git a/hikari/net/rest.py b/hikari/net/rest.py index bbc84fd1a2..12fd22713e 100644 --- a/hikari/net/rest.py +++ b/hikari/net/rest.py @@ -73,7 +73,7 @@ """The URL to the Discord OAuth2 API.""" -class REST(http_client.HTTPClient, component.IComponent): +class REST(http_client.HTTPClient, component.IComponent): # pylint:disable=too-many-public-methods """Implementation of the V6 and V7-compatible Discord REST API. This manages making HTTP/1.1 requests to the API and using the entity @@ -349,43 +349,48 @@ async def _handle_rate_limits_for_response( @staticmethod def _generate_allowed_mentions( - mentions_everyone: bool, - user_mentions: typing.Union[typing.Collection[typing.Union[bases.UniqueObject, users.User]], bool], - role_mentions: typing.Union[typing.Collection[typing.Union[bases.UniqueObject, guilds.Role]], bool], + mentions_everyone: typing.Union[undefined.Undefined, bool], + user_mentions: typing.Union[ + undefined.Undefined, typing.Collection[typing.Union[users.User, bases.UniqueObject]], bool + ], + role_mentions: typing.Union[ + undefined.Undefined, typing.Collection[typing.Union[bases.UniqueObject, guilds.Role]], bool + ], ): parsed_mentions = [] allowed_mentions = {} if mentions_everyone is True: parsed_mentions.append("everyone") + if user_mentions is True: parsed_mentions.append("users") - - # This covers both `False` and an array of IDs/objs by using `user_mentions or a empty sequence`, where a + # This covers both `False` and an array of IDs/objs by using `user_mentions` or `EMPTY_SEQUENCE`, where a # resultant empty list will mean that all user mentions are blacklisted. - else: + elif not isinstance(user_mentions, undefined.Undefined): allowed_mentions["users"] = list( # dict.fromkeys is used to remove duplicate entries that would cause discord to return an error. dict.fromkeys(str(int(m)) for m in user_mentions or ()) ) if len(allowed_mentions["users"]) > 100: raise ValueError("Only up to 100 users can be provided.") + if role_mentions is True: parsed_mentions.append("roles") - - # This covers both `False` and an array of IDs/objs by using `user_mentions or a empty sequence`, where a + # This covers both `False` and an array of IDs/objs by using `user_mentions` or `EMPTY_SEQUENCE`, where a # resultant empty list will mean that all role mentions are blacklisted. - else: + elif not isinstance(role_mentions, undefined.Undefined): allowed_mentions["roles"] = list( # dict.fromkeys is used to remove duplicate entries that would cause discord to return an error. dict.fromkeys(str(int(m)) for m in role_mentions or ()) ) if len(allowed_mentions["roles"]) > 100: raise ValueError("Only up to 100 roles can be provided.") - allowed_mentions["parse"] = parsed_mentions - # As a note, discord will also treat an empty `allowed_mentions` object as if it wasn't passed at all, so we - # want to use empty lists for blacklisting elements rather than just not including blacklisted elements. + if not parsed_mentions and not allowed_mentions: + return undefined.Undefined() + + allowed_mentions["parse"] = parsed_mentions return allowed_mentions def _build_message_creation_form( @@ -412,14 +417,13 @@ async def fetch_channel( Parameters ---------- channel : hikari.models.channels.PartialChannel | hikari.utilities.snowflake.Snowflake | int | str - The channel object to fetch. This can be an existing reference to a - channel object (if you want a more up-to-date representation, or it - can be a snowflake representation of the channel ID. + The channel to fetch. This may be a channel object, or the ID of an + existing channel. Returns ------- hikari.models.channels.PartialChannel - The resultant channel. + The fetched channel. Raises ------ @@ -479,27 +483,44 @@ async def edit_channel( permission_overwrites: typing.Union[ undefined.Undefined, typing.Sequence[channels.PermissionOverwrite] ] = undefined.Undefined(), - parent_category: typing.Union[undefined.Undefined, channels.GuildCategory] = undefined.Undefined(), + parent_category: typing.Union[ + undefined.Undefined, channels.GuildCategory, bases.UniqueObject + ] = undefined.Undefined(), reason: typing.Union[undefined.Undefined, str] = undefined.Undefined(), ) -> channels.PartialChannel: """Edit a channel. Parameters ---------- - channel - name - position - topic - nsfw - bitrate - user_limit - rate_limit_per_user - permission_overwrites - parent_category - reason + channel : hikari.models.channels.PartialChannel | hikari.utilities.snowflake.Snowflake | int | str + The channel to edit. This may be a channel object, or the ID of an + existing channel. + name : hikari.utilities.undefined.Undefined | str + If provided, the new name for the channel. + position : hikari.utilities.undefined.Undefined | int + If provided, the new position for the channel. + topic : hikari.utilities.undefined.Undefined | str + If provided, the new topic for the channel. + nsfw : hikari.utilities.undefined.Undefined | bool + If provided, whether the channel should be marked as NSFW or not. + bitrate : hikari.utilities.undefined.Undefined | int + If provided, the new bitrate for the channel. + user_limit : hikari.utilities.undefined.Undefined | int + If provided, the new user limit in the channel. + rate_limit_per_user : hikari.utilities.undefined.Undefined | datetime.timedelta | float | int + If provided, the new rate limit per user in the channel. + permission_overwrites : hikari.utilities.undefined.Undefined | typing.Sequence[hikari.models.channels.PermissionOverwrite] + If provided, the new permission overwrites for the channel. + parent_category : hikari.utilities.undefined.Undefined | hikari.models.channels.GuildCategory | hikari.utilities.snowflake.Snowflake | int | str + If provided, the new guild category for the channel. This may be + a category object, or the ID of an existing category. + reason : hikari.utilities.undefined.Undefined | str + If provided, the reason that will be recorded in the audit logs. Returns ------- + hikari.models.channels.PartialChannel + The edited channel. Raises ------ @@ -536,7 +557,9 @@ async def delete_channel(self, channel: typing.Union[channels.PartialChannel, ba Parameters ---------- - channel + channel : hikari.models.channels.PartialChannel | hikari.utilities.snowflake.Snowflake | int | str + The channel to delete. This may be a channel object, or the ID of an + existing channel. Raises ------ @@ -549,6 +572,9 @@ async def delete_channel(self, channel: typing.Union[channels.PartialChannel, ba hikari.errors.ServerHTTPErrorResponse If an internal error occurs on Discord while handling the request. + !!! note + For Public servers, the set 'Rules' or 'Guidelines' channels and the + 'Public Server Updates' channel cannot be deleted. """ route = routes.DELETE_CHANNEL.compile(channel=channel) await self._request(route) @@ -582,8 +608,8 @@ async def edit_permission_overwrites( self, channel: typing.Union[channels.GuildChannel, bases.UniqueObject], target: typing.Union[bases.UniqueObject, users.User, guilds.Role, channels.PermissionOverwrite], - target_type: typing.Union[undefined.Undefined, channels.PermissionOverwriteType, str] = undefined.Undefined(), *, + target_type: typing.Union[undefined.Undefined, channels.PermissionOverwriteType, str] = undefined.Undefined(), allow: typing.Union[undefined.Undefined, permissions_.Permission] = undefined.Undefined(), deny: typing.Union[undefined.Undefined, permissions_.Permission] = undefined.Undefined(), reason: typing.Union[undefined.Undefined, str] = undefined.Undefined(), @@ -592,15 +618,27 @@ async def edit_permission_overwrites( Parameters ---------- - channel - target - target_type - allow - deny - reason + channel : hikari.models.channels.PartialChannel | hikari.utilities.snowflake.Snowflake | int | str + The channel to edit a permission overwrite in. This may be a channel object, or + the ID of an existing channel. + target : hikari.models.users.User | hikari.models.guidls.Role | hikari.models.channels.PermissionOverwrite | hikari.utilities.snowflake.Snowflake | int | str + The channel overwrite to edit. This may be a overwrite object, or the ID of an + existing channel. + target_type : hikari.utilities.undefined.Undefined | hikari.models.channels.PermissionOverwriteType | str + If provided, the type of the target to update. If unset, will attempt to get + the type from `target`. + allow : hikari.utilities.undefined.Undefined | hikari.models.permissions.Permission + If provided, the new vale of all allowed permissions. + deny : hikari.utilities.undefined.Undefined | hikari.models.permissions.Permission + If provided, the new vale of all disallowed permissions. + reason : hikari.utilities.undefined.Undefined | str + If provided, the reason that will be recorded in the audit logs. Raises ------ + TypeError + If `target_type` is unset and we were unable to determine the type + from `target`. hikari.errors.BadRequest If any of the fields that are passed have an invalid value. hikari.errors.Unauthorized @@ -643,8 +681,11 @@ async def delete_permission_overwrite( Parameters ---------- - channel - target + channel : hikari.models.channels.PartialChannel | hikari.utilities.snowflake.Snowflake | int | str + The channel to delete a permission overwrite in. This may be a channel + object, or the ID of an existing channel. + target : hikari.models.users.User | hikari.models.guidls.Role | hikari.models.channels.PermissionOverwrite | hikari.utilities.snowflake.Snowflake | int | str + The channel overwrite to delete. Raises ------ @@ -667,10 +708,14 @@ async def fetch_channel_invites( Parameters ---------- - channel + channel : hikari.models.channels.PartialChannel | hikari.utilities.snowflake.Snowflake | int | str + The channel to fetch the invites from. This may be a channel + object, or the ID of an existing channel. Returns ------- + typing.Sequence[hikari.models.invites.InviteWithMetadata] + The invites pointing to the given guild channel. Raises ------ @@ -704,17 +749,29 @@ async def create_invite( Parameters ---------- - channel - max_age - max_uses - temporary - unique - target_user - target_user_type - reason + channel : hikari.models.channels.PartialChannel | hikari.utilities.snowflake.Snowflake | int | str + The channel to create a invite for. This may be a channel object, + or the ID of an existing channel. + max_age : hikari.utilities.undefined.Undefined | datetime.timedelta | float | int + If provided, the duration of the invite before expiry. + max_uses : hikari.utilities.undefined.Undefined | int + If provided, the max uses the invite can have. + temporary : hikari.utilities.undefined.Undefined | bool + If provided, whether the invite only grants temporary membership. + unique : hikari.utilities.undefined.Undefined | bool + If provided, wheter the invite should be unique. + target_user : hikari.utilities.undefined.Undefined | hikari.models.users.User | hikari.utilities.snowflake.Snowflake | int | str + If provided, the target user id for this invite. This may be a + user object, or the ID of an existing user. + target_user_type : hikari.utilities.undefined.Undefined | hikari.models.invites.TargetUserType | int + If provided, the type of target user for this invite. + reason : hikari.utilities.undefined.Undefined | str + If provided, the reason that will be recorded in the audit logs. Returns ------- + hikari.models.invites.InviteWithMetadata + The invite to the given guild channel. Raises ------ @@ -748,10 +805,14 @@ def trigger_typing( Parameters ---------- - channel + channel : hikari.models.channels.PartialChannel | hikari.utilities.snowflake.Snowflake | int | str + The channel to trigger typing in. This may be a channel object, or + the ID of an existing channel. Returns ------- + hikari.net.rest_utils.TypingIndicator + A typing indicator to use. Raises ------ @@ -779,10 +840,14 @@ async def fetch_pins( Parameters ---------- - channel + channel : hikari.models.channels.PartialChannel | hikari.utilities.snowflake.Snowflake | int | str + The channel to fetch pins from. This may be a channel object, or + the ID of an existing channel. Returns ------- + typing.Sequence[hikari.models.messages.Message] + The pinned messages in this text channel. Raises ------ @@ -809,8 +874,12 @@ async def pin_message( Parameters ---------- - channel - message + channel : hikari.models.channels.PartialChannel | hikari.utilities.snowflake.Snowflake | int | str + The channel to pin a message in. This may be a channel object, or + the ID of an existing channel. + message : hikari.models.messges.Message | hikari.utilities.snowflake.Snowflake | int | str + The message to pin. This may be a message object, + or the ID of an existing message. Raises ------ @@ -836,8 +905,12 @@ async def unpin_message( Parameters ---------- - channel - message + channel : hikari.models.channels.PartialChannel | hikari.utilities.snowflake.Snowflake | int | str + The channel to unpin a message in. This may be a channel object, or + the ID of an existing channel. + message : hikari.models.messges.Message | hikari.utilities.snowflake.Snowflake | int | str + The message to unpin. This may be a message object, or the ID of an + existing message. Raises ------ @@ -850,11 +923,6 @@ async def unpin_message( in the given channel. hikari.errors.ServerHTTPErrorResponse If an internal error occurs on Discord while handling the request. - - !!! note - The exceptions on this endpoint will only be raised once the result - is awaited or interacted with. Invoking this function itself will - not raise any of the above types. """ route = routes.DELETE_CHANNEL_PIN.compile(channel=channel, message=message) await self._request(route) @@ -908,16 +976,28 @@ def fetch_messages( Parameters ---------- - channel - before - after - around + channel : hikari.models.channels.PartialChannel | hikari.utilities.snowflake.Snowflake | int | str + The channel to fetch messages in. This may be a channel object, or + the ID of an existing channel. + before : hikari.utilities.undefined.Undefined | datetime.datetime | hikari.utilities.snowflake.Snowflake | int | str + If provided, fetch messages before this snowflake. If you provide + a datetime object, it will be transformed into a snowflake. + after : hikari.utilities.undefined.Undefined | datetime.datetime | hikari.utilities.snowflake.Snowflake | int | str + If provided, fetch messages after this snowflake. If you provide + a datetime object, it will be transformed into a snowflake. + around : hikari.utilities.undefined.Undefined | datetime.datetime | hikari.utilities.snowflake.Snowflake | int | str + If provided, fetch messages around this snowflake. If you provide + a datetime object, it will be transformed into a snowflake. Returns ------- + hikari.net.iterators.LazyIterator[hikari.models.messages.Message] + A iterator to fetch the messages. Raises ------ + TypeError + If you specify more than one of `before`, `after`, `about`. hikari.errors.Unauthorized If you are unauthorized to make the request (invalid/missing token). hikari.errors.Forbidden @@ -927,8 +1007,6 @@ def fetch_messages( If the channel is not found. hikari.errors.ServerHTTPErrorResponse If an internal error occurs on Discord while handling the request. - TypeError - If you specify more than one of `before`, `after`, `about`. !!! note The exceptions on this endpoint (other than `TypeError`) will only @@ -937,8 +1015,9 @@ def fetch_messages( `TypeError`). """ if undefined.Undefined.count(before, after, around) < 2: - raise TypeError(f"Expected no kwargs, or maximum of one of 'before', 'after', 'around'") - elif not isinstance(before, undefined.Undefined): + raise TypeError("Expected no kwargs, or maximum of one of 'before', 'after', 'around'") + + if not isinstance(before, undefined.Undefined): direction, timestamp = "before", before elif not isinstance(after, undefined.Undefined): direction, timestamp = "after", after @@ -961,11 +1040,17 @@ async def fetch_message( Parameters ---------- - channel - message + channel : hikari.models.channels.PartialChannel | hikari.utilities.snowflake.Snowflake | int | str + The channel to fetch messages in. This may be a channel object, or + the ID of an existing channel. + message : hikari.models.messages.Message | hikari.utilities.snowflake.Snowflake | int | str + The message to fetch. This may be a channel object, or the ID of an + existing channel. Returns ------- + hikari.models.messages.Message + The requested message. Raises ------ @@ -993,26 +1078,42 @@ async def create_message( attachments: typing.Union[undefined.Undefined, typing.Sequence[files.BaseStream]] = undefined.Undefined(), tts: typing.Union[undefined.Undefined, bool] = undefined.Undefined(), nonce: typing.Union[undefined.Undefined, str] = undefined.Undefined(), - mentions_everyone: bool = False, + mentions_everyone: bool = True, user_mentions: typing.Union[typing.Collection[typing.Union[users.User, bases.UniqueObject]], bool] = True, - role_mentions: typing.Union[typing.Collection[typing.Union[bases.UniqueObject, guilds.Role]], bool] = True, + role_mentions: typing.Union[typing.Collection[typing.Union[guilds.Role, bases.UniqueObject]], bool] = True, ) -> messages_.Message: """Create a message in the given channel. Parameters ---------- - channel - text - embed - attachments - tts - nonce - mentions_everyone - user_mentions - role_mentions + channel : hikari.models.channels.PartialChannel | hikari.utilities.snowflake.Snowflake | int | str + The channel to create the message in. This may be a channel object, or + the ID of an existing channel. + text : hikari.utilities.undefined.Undefined | str + If specified, the message contents. + embed : hikari.utilities.undefined.Undefined | hikari.models.embeds.Embed + If specified, the message embed. + attachments : hikari.utilities.undefined.Undefined | typing.Sequence[hikari.models.files.BaseStream] + If specified, the message attachments. + tts : hikari.utilities.undefined.Undefined | bool + If specified, whether the message will be TTS (Text To Speech). + nonce : hikari.utilities.undefined.Undefined | str + If specified, a nonce that can be used for optimistic message sending. + mentions_everyone : bool + If specified, whether the message should parse @everyone/@here mentions. + user_mentions : typing.Collection[hikari.models.users.User | hikari.utilities.snowflake.Snowflake | int | str] | bool + If specified, and `bool`, whether to parse user mentions. If specified and + `list`, the users to parse the mention for. This may be a user object, or + the ID of an existing user. + role_mentions : typing.Collection[hikari.models.guilds.Role | hikari.utilities.snowflake.Snowflake | int | str] | bool + If specified and `bool`, whether to parse role mentions. If specified and + `list`, the roles to parse the mention for. This may be a role object, or + the ID of an existing role. Returns ------- + hikari.models.messages.Message + The created message. Raises ------ @@ -1033,7 +1134,7 @@ async def create_message( hikari.errors.ServerHTTPErrorResponse If an internal error occurs on Discord while handling the request. - !!! warn + !!! warning You are expected to make a connection to the gateway and identify once before being able to use this endpoint for a bot. """ @@ -1050,7 +1151,7 @@ async def create_message( attachments = [] if isinstance(attachments, undefined.Undefined) else [a for a in attachments] if not isinstance(embed, undefined.Undefined): - attachments += embed.assets_to_upload + attachments.extend(embed.assets_to_upload) response = await self._request( route, body=self._build_message_creation_form(body, attachments) if attachments else body @@ -1065,18 +1166,35 @@ async def edit_message( text: typing.Union[undefined.Undefined, typing.Any] = undefined.Undefined(), *, embed: typing.Union[undefined.Undefined, embeds_.Embed] = undefined.Undefined(), - mentions_everyone: bool = False, - user_mentions: typing.Union[typing.Collection[typing.Union[users.User, bases.UniqueObject]], bool] = True, - role_mentions: typing.Union[typing.Collection[typing.Union[bases.UniqueObject, guilds.Role]], bool] = True, + mentions_everyone: typing.Union[undefined.Undefined, bool] = undefined.Undefined(), + user_mentions: typing.Union[ + undefined.Undefined, typing.Collection[typing.Union[users.User, bases.UniqueObject]], bool + ] = undefined.Undefined(), + role_mentions: typing.Union[ + undefined.Undefined, typing.Collection[typing.Union[bases.UniqueObject, guilds.Role]], bool + ] = undefined.Undefined(), flags: typing.Union[undefined.Undefined, messages_.MessageFlag] = undefined.Undefined(), ) -> messages_.Message: """Edit an existing message in a given channel. Parameters ---------- + channel : hikari.models.channels.PartialChannel | hikari.utilities.snowflake.Snowflake | int | str + The channel to edit the message in. This may be a channel object, or + the ID of an existing channel. + message : hikari.models.messages.Messages | hikari.utilities.snowflake.Snowflake | int | str + The message to fetch. + text + embed + mentions_everyone + user_mentions + role_mentions + flags Returns ------- + hikari.models.messages.Message + The edited message. Raises ------ @@ -1383,7 +1501,7 @@ async def execute_webhook( attachments: typing.Union[undefined.Undefined, typing.Sequence[files.BaseStream]] = undefined.Undefined(), tts: typing.Union[undefined.Undefined, bool] = undefined.Undefined(), wait: typing.Union[undefined.Undefined, bool] = undefined.Undefined(), - mentions_everyone: bool = False, + mentions_everyone: bool = True, user_mentions: typing.Union[typing.Collection[typing.Union[users.User, bases.UniqueObject]], bool] = True, role_mentions: typing.Union[typing.Collection[typing.Union[bases.UniqueObject, guilds.Role]], bool] = True, ) -> messages_.Message: @@ -1399,7 +1517,7 @@ async def execute_webhook( if not isinstance(embeds, undefined.Undefined): for embed in embeds: - attachments += embed.assets_to_upload + attachments.extend(embed.assets_to_upload) serialized_embeds.append(self._app.entity_factory.serialize_embed(embed)) body = data_binding.JSONObjectBuilder() diff --git a/hikari/net/tracing.py b/hikari/net/tracing.py index 9b8a2ba520..cf7c04cdce 100644 --- a/hikari/net/tracing.py +++ b/hikari/net/tracing.py @@ -106,7 +106,7 @@ class DebugTracer(BaseTracer): and other pieces of information that can be incredibly useful for debugging performance issues and API issues. - !!! warn + !!! warning This may log potentially sensitive information such as authorization tokens, so ensure those are removed from _debug logs before proceeding to send logs to anyone. From 8321adab3f0e781f1d6533566bac0292639cb35b Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Tue, 2 Jun 2020 12:07:31 +0100 Subject: [PATCH 427/922] Fixed some problems with the documentation. --- docs/css.mako | 4 + docs/documentation.mako | 324 +++++++++++++++++++++++++++----------- hikari/utilities/klass.py | 5 +- 3 files changed, 243 insertions(+), 90 deletions(-) diff --git a/docs/css.mako b/docs/css.mako index 8ed4cda672..bf41511673 100644 --- a/docs/css.mako +++ b/docs/css.mako @@ -48,6 +48,10 @@ a { color: #e83e8c; } +.breadcrumb-item.inactive { + color: #a626a4 !important; +} + .breadcrumb-item.active { color: #e83e8c; } diff --git a/docs/documentation.mako b/docs/documentation.mako index ecf752872f..14c8c03b47 100644 --- a/docs/documentation.mako +++ b/docs/documentation.mako @@ -9,43 +9,77 @@ from pdoc.html_helpers import extract_toc, glimpse, to_html as _to_html, format_git_link - def link(dobj: pdoc.Doc, *, with_prefixes=False, simple_names=False, css_classes="", name=None, default_type="", dotted=True, anchor=False): + QUAL_ABC = "abc" + QUAL_ASYNC_DEF = "async def" + QUAL_CLASS = "class" + QUAL_DATACLASS = "dataclass" + QUAL_CONST = "const" + QUAL_DEF = "def" + QUAL_ENUM = "enum" + QUAL_ENUM_FLAG = "flag" + QUAL_EXTERNAL = "external" + QUAL_METACLASS = "metaclass" + QUAL_MODULE = "module" + QUAL_NAMESPACE = "namespace" + QUAL_PACKAGE = "package" + QUAL_REF = "ref" + QUAL_TYPEHINT = "type hint" + QUAL_VAR = "var" + + def link(dobj: pdoc.Doc, *, with_prefixes=False, simple_names=False, css_classes="", name=None, default_type="", dotted=True, anchor=False, fully_qualified=False): prefix = "" name = name or dobj.name if with_prefixes: if isinstance(dobj, pdoc.Function): - prefix = "" + dobj.funcdef() + " " + if dobj.module.name != dobj.obj.__module__: + qual = QUAL_REF + " " + dobj.funcdef() + else: + qual = dobj.funcdef() + + prefix = "" + qual + " " elif isinstance(dobj, pdoc.Variable): - if dobj.module.name == "typing" or dobj.docstring and dobj.docstring.casefold().startswith("type hint"): - prefix = "type hint " + if dobj.module.name == "typing" or dobj.docstring and dobj.docstring.casefold().startswith(("type hint", "typehint", "type alias")): + prefix = F"{QUAL_TYPEHINT} " elif all(not c.isalpha() or c.isupper() for c in dobj.name): - prefix = "const " + prefix = f"{QUAL_CONST}" else: - prefix = "var " + prefix = f"{QUAL_VAR} " elif isinstance(dobj, pdoc.Class): + if dobj.module.name != dobj.obj.__module__: + qual = f"{QUAL_REF} " + else: + qual = "" + if issubclass(dobj.obj, type): - qual = "metaclass" + qual += QUAL_METACLASS else: + if "__call__" in dobj.obj.__dict__: + name += "()" + if enum.Flag in dobj.obj.mro(): - qual = "enum flag" + qual += QUAL_ENUM_FLAG elif enum.Enum in dobj.obj.mro(): - qual = "enum" + qual += QUAL_ENUM + elif hasattr(dobj.obj, "__attrs_attrs__"): + qual += QUAL_DATACLASS else: - qual = "class" + qual += QUAL_CLASS if inspect.isabstract(dobj.obj): - qual = "abstract " + qual + qual = f"{QUAL_ABC} {qual}" prefix = f"{qual} " - if "__call__" in dobj.obj.__dict__: - name += "()" - elif isinstance(dobj, pdoc.Module): - qual = "package" if dobj.is_package else "namespace" if dobj.is_namespace else "module" + if dobj.module.name != dobj.obj.__name__: + qual = f"{QUAL_REF} " + else: + qual = "" + + qual += QUAL_PACKAGE if dobj.is_package else QUAL_NAMESPACE if dobj.is_namespace else QUAL_MODULE prefix = f"{qual} " else: @@ -53,9 +87,11 @@ else: name = name or dobj.name or "" + if fully_qualified and not simple_names: + name = dobj.module.name + "." + dobj.obj.__qualname__ if isinstance(dobj, pdoc.External) and not external_links: - return name + return name if not with_prefixes else f"{QUAL_EXTERNAL} {name}" url = dobj.url(relative_to=module, link_prefix=link_prefix, top_ancestor=not show_inherited_members) @@ -115,7 +151,7 @@ % else: <% url = link(m) %> - + % endif % endfor @@ -144,14 +180,28 @@ )) else: representation = f"{f.funcdef()} {f.name}(){return_type}: ..." + + if f.module.name != f.obj.__module__: + try: + ref = pdoc._global_context[f.obj.__module__ + "." + f.obj.__qualname__] + redirect = True + except KeyError: + redirect = False + else: + redirect = False %>
${representation}
- ${show_desc(f)} + % if redirect: + ${show_desc(f) | glimpse, to_html} + This class is defined explicitly at ${link(ref, with_prefixes=True, fully_qualified=True)}. Visit that link to view the full documentation! + % else: + ${show_desc(f)} - ${show_source(f)} + ${show_source(f)} + % endif
@@ -167,18 +217,18 @@ subclasses = c.subclasses() params = c.params(annotate=show_type_annotations, link=link) - example_str = "class " + c.name + "(" + ", ".join(params) + ")" + example_str = f"{QUAL_CLASS} " + c.name + "(" + ", ".join(params) + ")" if len(params) > 4 or len(example_str) > 70: representation = "\n".join(( - f"class {c.name} (", + f"{QUAL_CLASS} {c.name} (", *(f" {p}," for p in params), "): ..." )) elif params: - representation = f"class {c.name} (" + ", ".join(params) + "): ..." + representation = f"{QUAL_CLASS} {c.name} (" + ", ".join(params) + "): ..." else: - representation = f"class {c.name}: ..." + representation = f"{QUAL_CLASS} {c.name}: ..." %>

${link(c, with_prefixes=True, simple_names=True)}

@@ -186,71 +236,89 @@
${representation}
- ${show_desc(c)} -
- - % if subclasses: -
Subclasses
-
- % for sc in subclasses: -
${link(sc, with_prefixes=True, default_type="class")}
-
${sc.docstring | glimpse, to_html}
- % endfor -
-
- % endif + <% + if c.module.name != c.obj.__module__: + try: + ref = pdoc._global_context[c.obj.__module__ + "." + c.obj.__qualname__] + redirect = True + except KeyError: + redirect = False + else: + redirect = False + %> - % if mro: -
Method resolution order
-
-
${link(c, with_prefixes=True)}
-
That's this class!
- % for mro_c in mro: -
${link(mro_c, with_prefixes=True, default_type="class")}
-
${mro_c.docstring | glimpse, to_html}
- % endfor -
-
- % endif - % if class_vars: -
Class variables
-
- % for cv in class_vars: - ${show_var(cv)} - % endfor -
-
- % endif + % if redirect: + ${show_desc(c) | glimpse, to_html} + This class is defined explicitly at ${link(ref, with_prefixes=True, fully_qualified=True)}. Visit that link to view the full documentation! + % else: - % if smethods: -
Class methods
-
- % for m in smethods: - ${show_func(m)} - % endfor -
+ ${show_desc(c)}
- % endif - % if inst_vars: -
Instance variables
-
- % for i in inst_vars: - ${show_var(i)} - % endfor -
-
- % endif + % if subclasses: +
Subclasses
+
+ % for sc in subclasses: +
${link(sc, with_prefixes=True, default_type="class")}
+
${sc.docstring | glimpse, to_html}
+ % endfor +
+
+ % endif - % if methods: -
Instance methods
-
- % for m in methods: - ${show_func(m)} - % endfor -
-
+ % if mro: +
Method resolution order
+
+
${link(c, with_prefixes=True)}
+
That's this class!
+ % for mro_c in mro: +
${link(mro_c, with_prefixes=True, default_type="class")}
+
${mro_c.docstring | glimpse, to_html}
+ % endfor +
+
+ % endif + + % if class_vars: +
Class variables
+
+ % for cv in class_vars: + ${show_var(cv)} + % endfor +
+
+ % endif + + % if smethods: +
Class methods
+
+ % for m in smethods: + ${show_func(m)} + % endfor +
+
+ % endif + + % if inst_vars: +
Instance variables
+
+ % for i in inst_vars: + ${show_var(i)} + % endfor +
+
+ % endif + + % if methods: +
Instance methods
+
+ % for m in methods: + ${show_func(m)} + % endfor +
+
+ % endif % endif
@@ -283,11 +351,11 @@
Expand source code - % if git_link: -
- Browse git - %endif -
+ % if git_link: +
+ Browse git + %endif +
${d.source | h}
% elif git_link: @@ -314,7 +382,7 @@ %>
-
+ +
+
+

Notation used in this documentation

+
+
${QUAL_DEF}
+
Regular function.
+ +
${QUAL_ASYNC_DEF}
+
Coroutine function that should be awaited.
+ +
${QUAL_CLASS}
+
Regular class that provides a certain functionality.
+ +
${QUAL_ABC}
+
+ Abstract base class. These are partially implemented classes that require + additional implementation details to be fully usable. Generally these are + used to represet a subset of behaviour common between different + implementations. +
+ +
${QUAL_DATACLASS}
+
+ Data class. This is a class designed to model and store information + rather than provide a certain behaviour or functionality. +
+ +
${QUAL_ENUM}
+
Enumerated type.
+ +
${QUAL_ENUM_FLAG}
+
Enumerated flag type. Supports being combined.
+ +
${QUAL_METACLASS}
+
+ Metaclass. This is a base type of a class, used to control how implementing + classes are created, exist, operate, and get destroyed. +
+ +
${QUAL_MODULE}
+
Python module that you can import directly
+ +
${QUAL_PACKAGE}
+
Python package that can be imported and can contain sub-modules.
+ +
${QUAL_NAMESPACE}
+
Python namespace package that can contain sub-modules, but is not directly importable.
+ +
${QUAL_TYPEHINT}
+
+ An object or attribute used to denote a certain type or combination of types. + These usually provide no functionality and only exist for documentation purposes + and for static type-checkers. +
+ +
${QUAL_REF}
+
+ Used to flag that an object is defined in a different file, and is just + referred to at the current location. +
+ +
${QUAL_VAR}
+
+ Variable or attribute. +
+ +
${QUAL_CONST}
+
+ Value that should not be changed manually. +
+ +
${QUAL_EXTERNAL}
+
+ Attribute or object that is not covered by this documentation. This usually + denotes types from other dependencies, or from the standard library. +
+
+
+
\ No newline at end of file diff --git a/hikari/utilities/klass.py b/hikari/utilities/klass.py index 884f6029f7..6a05c639d5 100644 --- a/hikari/utilities/klass.py +++ b/hikari/utilities/klass.py @@ -22,6 +22,7 @@ __all__ = ["get_logger", "SingletonMeta", "Singleton"] +import abc import logging import typing @@ -47,7 +48,7 @@ def get_logger(cls: typing.Union[typing.Type, typing.Any], *additional_args: str return logging.getLogger(".".join((cls.__module__, cls.__qualname__, *additional_args))) -class SingletonMeta(type): +class SingletonMeta(abc.ABCMeta): """Metaclass that makes the class a singleton. Once an instance has been defined at runtime, it will exist until the @@ -82,7 +83,7 @@ def __call__(cls): return SingletonMeta.___instances___[cls] -class Singleton(metaclass=SingletonMeta): +class Singleton(abc.ABC, metaclass=SingletonMeta): """Base type for anything implementing the `SingletonMeta` metaclass. Once an instance has been defined at runtime, it will exist until the From 44573c98395a7dab0989307918d80ce815a4377f Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Tue, 2 Jun 2020 12:08:50 +0100 Subject: [PATCH 428/922] Fixed documentation styling issue. --- docs/css.mako | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/css.mako b/docs/css.mako index bf41511673..2c01556af4 100644 --- a/docs/css.mako +++ b/docs/css.mako @@ -48,12 +48,12 @@ a { color: #e83e8c; } -.breadcrumb-item.inactive { +.breadcrumb-item.inactive > a { color: #a626a4 !important; } -.breadcrumb-item.active { - color: #e83e8c; +.breadcrumb-item.active > a { + color: #e83e8c !important; } .breadcrumb-item+.breadcrumb-item::before { From f0c32e29786c6a7a9d80f546e4acd1cc5e1bb793 Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Tue, 2 Jun 2020 12:14:20 +0100 Subject: [PATCH 429/922] Fixed documentation missing class source. --- docs/documentation.mako | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/docs/documentation.mako b/docs/documentation.mako index 14c8c03b47..2612dd91ac 100644 --- a/docs/documentation.mako +++ b/docs/documentation.mako @@ -26,13 +26,25 @@ QUAL_TYPEHINT = "type hint" QUAL_VAR = "var" - def link(dobj: pdoc.Doc, *, with_prefixes=False, simple_names=False, css_classes="", name=None, default_type="", dotted=True, anchor=False, fully_qualified=False): + def link( + dobj: pdoc.Doc, + *, + with_prefixes=False, + simple_names=False, + css_classes="", + name=None, + default_type="", + dotted=True, + anchor=False, + fully_qualified=False, + hide_ref=False, + ): prefix = "" name = name or dobj.name if with_prefixes: if isinstance(dobj, pdoc.Function): - if dobj.module.name != dobj.obj.__module__: + if not hide_ref and dobj.module.name != dobj.obj.__module__: qual = QUAL_REF + " " + dobj.funcdef() else: qual = dobj.funcdef() @@ -48,7 +60,7 @@ prefix = f"{QUAL_VAR} " elif isinstance(dobj, pdoc.Class): - if dobj.module.name != dobj.obj.__module__: + if not hide_ref and dobj.module.name != dobj.obj.__module__: qual = f"{QUAL_REF} " else: qual = "" @@ -255,6 +267,8 @@ ${show_desc(c)}
+ ${show_source(c)} +
% if subclasses:
Subclasses
From e8ce0ea9be4783d9b1dad0fe6b0bf1aceaec7026 Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Tue, 2 Jun 2020 15:03:33 +0100 Subject: [PATCH 430/922] Moved interfaces into api package. --- hikari/__init__.py | 11 ++----- hikari/api/__init__.py | 41 ++++++++++++++++++++++++ hikari/{ => api}/app.py | 8 ++--- hikari/{ => api}/cache.py | 15 +++++---- hikari/{ => api}/component.py | 10 +++--- hikari/{ => api}/entity_factory.py | 2 +- hikari/{ => api}/event_consumer.py | 2 +- hikari/{ => api}/event_dispatcher.py | 2 +- hikari/impl/__init__.py | 7 ++++ hikari/impl/bot.py | 11 +++---- hikari/impl/cache.py | 4 +-- hikari/impl/entity_factory.py | 4 +-- hikari/impl/event_manager_core.py | 6 ++-- hikari/impl/gateway_zookeeper.py | 4 +-- hikari/impl/rest_app.py | 6 ++-- hikari/models/__init__.py | 2 +- hikari/models/bases.py | 2 +- hikari/net/__init__.py | 2 +- hikari/net/gateway.py | 4 +-- hikari/net/iterators.py | 2 +- hikari/net/rest.py | 6 ++-- hikari/net/rest_utils.py | 2 +- hikari/net/voice_gateway.py | 8 ++++- hikari/utilities/data_binding.py | 16 ++++----- tests/hikari/impl/test_entity_factory.py | 2 +- 25 files changed, 114 insertions(+), 65 deletions(-) create mode 100644 hikari/api/__init__.py rename hikari/{ => api}/app.py (97%) rename hikari/{ => api}/cache.py (95%) rename hikari/{ => api}/component.py (85%) rename hikari/{ => api}/entity_factory.py (99%) rename hikari/{ => api}/event_consumer.py (98%) rename hikari/{ => api}/event_dispatcher.py (99%) diff --git a/hikari/__init__.py b/hikari/__init__.py index 204cef0e7c..b8fc4b699a 100644 --- a/hikari/__init__.py +++ b/hikari/__init__.py @@ -20,8 +20,6 @@ from __future__ import annotations -import functools as _functools - from hikari._about import __author__ from hikari._about import __ci__ from hikari._about import __copyright__ @@ -32,14 +30,9 @@ from hikari._about import __license__ from hikari._about import __url__ from hikari._about import __version__ -from hikari.app import * -from hikari.cache import * -from hikari.component import * -from hikari.entity_factory import * from hikari.errors import * from hikari.events import * -from hikari.event_consumer import * -from hikari.event_dispatcher import * +from hikari.api import * from hikari.http_settings import * from hikari.models import * from hikari.net import * @@ -47,4 +40,4 @@ from hikari.impl.bot import BotImpl as BotApp from hikari.impl.rest_app import RESTAppImpl as RESTApp -__all__ = errors.__all__ + http_settings.__all__ + models.__all__ + net.__all__ + ["BotApp", "RESTApp"] +__all__ = api.__all__ + errors.__all__ + http_settings.__all__ + models.__all__ + net.__all__ + ["BotApp", "RESTApp"] diff --git a/hikari/api/__init__.py b/hikari/api/__init__.py new file mode 100644 index 0000000000..da4a013b8a --- /dev/null +++ b/hikari/api/__init__.py @@ -0,0 +1,41 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# Copyright © Nekoka.tt 2019-2020 +# +# This file is part of Hikari. +# +# Hikari is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Hikari is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Hikari. If not, see . +"""Interfaces for components that make up Hikari applications. + +These are provided to uncouple specific implementation details from each +implementation, thus allowing custom solutions to be engineered such as bots +relying on a distributed event bus or cache. +""" +from __future__ import annotations + +from hikari.api.app import * +from hikari.api.cache import * +from hikari.api.component import * +from hikari.api.entity_factory import * +from hikari.api.event_consumer import * +from hikari.api.event_dispatcher import * + +__all__ = ( + app.__all__ + + cache.__all__ + + component.__all__ + + entity_factory.__all__ + + event_consumer.__all__ + + event_dispatcher.__all__ +) diff --git a/hikari/app.py b/hikari/api/app.py similarity index 97% rename from hikari/app.py rename to hikari/api/app.py index 8362275914..5a8016bfd9 100644 --- a/hikari/app.py +++ b/hikari/api/app.py @@ -27,16 +27,16 @@ import logging import typing -from hikari import event_dispatcher as event_dispatcher_ +from hikari.api import event_dispatcher as event_dispatcher_ from hikari.utilities import undefined if typing.TYPE_CHECKING: from concurrent import futures import datetime - from hikari import cache as cache_ - from hikari import entity_factory as entity_factory_ - from hikari import event_consumer as event_consumer_ + from hikari.api import cache as cache_ + from hikari.api import entity_factory as entity_factory_ + from hikari.api import event_consumer as event_consumer_ from hikari.models import presences from hikari import http_settings as http_settings_ from hikari.net import gateway diff --git a/hikari/cache.py b/hikari/api/cache.py similarity index 95% rename from hikari/cache.py rename to hikari/api/cache.py index 78d0028b92..1adbc97912 100644 --- a/hikari/cache.py +++ b/hikari/api/cache.py @@ -24,7 +24,7 @@ import abc import typing -from hikari import component +from hikari.api import component if typing.TYPE_CHECKING: from hikari.models import applications @@ -36,6 +36,7 @@ from hikari.models import guilds from hikari.models import invites from hikari.models import messages + from hikari.models import presences from hikari.models import users from hikari.models import voices @@ -189,21 +190,23 @@ async def delete_role(self, guild_id: int, role_id: int) -> typing.Optional[guil ... @abc.abstractmethod - async def create_presence(self, payload: data_binding.JSONObject, can_cache: bool = False) -> guilds.MemberPresence: + async def create_presence( + self, payload: data_binding.JSONObject, can_cache: bool = False + ) -> presences.MemberPresence: ... @abc.abstractmethod async def update_presence( - self, role: guilds.MemberPresence, payload: data_binding.JSONObject - ) -> guilds.MemberPresence: + self, role: presences.MemberPresence, payload: data_binding.JSONObject + ) -> presences.MemberPresence: ... @abc.abstractmethod - async def get_presence(self, guild_id: int, user_id: int) -> typing.Optional[guilds.MemberPresence]: + async def get_presence(self, guild_id: int, user_id: int) -> typing.Optional[presences.MemberPresence]: ... @abc.abstractmethod - async def delete_presence(self, guild_id: int, user_id: int) -> typing.Optional[guilds.MemberPresence]: + async def delete_presence(self, guild_id: int, user_id: int) -> typing.Optional[presences.MemberPresence]: ... @abc.abstractmethod diff --git a/hikari/component.py b/hikari/api/component.py similarity index 85% rename from hikari/component.py rename to hikari/api/component.py index 71d55e83cb..d1513d7171 100644 --- a/hikari/component.py +++ b/hikari/api/component.py @@ -16,15 +16,15 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . -"""Core interface that any component should derive from""" +"""Base interface for any internal components of an application.""" from __future__ import annotations +__all__ = ["IComponent"] + import abc -import typing -if typing.TYPE_CHECKING: - from hikari import app as app_ +from hikari.api import app class IComponent(abc.ABC): @@ -34,5 +34,5 @@ class IComponent(abc.ABC): @property @abc.abstractmethod - def app(self) -> app_.IApp: + def app(self) -> app.IApp: """Application that owns this component.""" diff --git a/hikari/entity_factory.py b/hikari/api/entity_factory.py similarity index 99% rename from hikari/entity_factory.py rename to hikari/api/entity_factory.py index 03f0ed2a25..0b90af3d6c 100644 --- a/hikari/entity_factory.py +++ b/hikari/api/entity_factory.py @@ -24,7 +24,7 @@ import abc import typing -from hikari import component +from hikari.api import component if typing.TYPE_CHECKING: from hikari.models import applications diff --git a/hikari/event_consumer.py b/hikari/api/event_consumer.py similarity index 98% rename from hikari/event_consumer.py rename to hikari/api/event_consumer.py index b2faaab4eb..0c990d316e 100644 --- a/hikari/event_consumer.py +++ b/hikari/api/event_consumer.py @@ -24,7 +24,7 @@ import abc import typing -from hikari import component +from hikari.api import component if typing.TYPE_CHECKING: from hikari.net import gateway diff --git a/hikari/event_dispatcher.py b/hikari/api/event_dispatcher.py similarity index 99% rename from hikari/event_dispatcher.py rename to hikari/api/event_dispatcher.py index 8cccd632f7..6f35b31419 100644 --- a/hikari/event_dispatcher.py +++ b/hikari/api/event_dispatcher.py @@ -24,7 +24,7 @@ import abc import typing -from hikari import component +from hikari.api import component from hikari.utilities import undefined if typing.TYPE_CHECKING: diff --git a/hikari/impl/__init__.py b/hikari/impl/__init__.py index 32ae2fa926..585d9e8d84 100644 --- a/hikari/impl/__init__.py +++ b/hikari/impl/__init__.py @@ -16,6 +16,13 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . +"""Basic implementations of application components. + +These components implement the interfaces in `hikari.api` to provide the +baseline functionality. For most applications that do not have bespoke +performance or structural requirements, you will want to use these +implementations. +""" from __future__ import annotations diff --git a/hikari/impl/bot.py b/hikari/impl/bot.py index e1d1456314..79d53036bf 100644 --- a/hikari/impl/bot.py +++ b/hikari/impl/bot.py @@ -26,25 +26,24 @@ import typing from concurrent import futures -from hikari import app +from hikari.api import app from hikari.impl import cache as cache_impl from hikari.impl import entity_factory as entity_factory_impl from hikari.impl import event_manager from hikari.impl import gateway_zookeeper from hikari.models import presences from hikari.net import rest -from hikari.utilities import cdn from hikari.utilities import klass from hikari.utilities import undefined if typing.TYPE_CHECKING: import datetime - from hikari import cache as cache_ - from hikari import entity_factory as entity_factory_ - from hikari import event_consumer as event_consumer_ + from hikari.api import cache as cache_ + from hikari.api import entity_factory as entity_factory_ + from hikari.api import event_consumer as event_consumer_ from hikari import http_settings as http_settings_ - from hikari import event_dispatcher + from hikari.api import event_dispatcher from hikari.models import gateway as gateway_models from hikari.models import intents as intents_ diff --git a/hikari/impl/cache.py b/hikari/impl/cache.py index 91de162807..7ddd2f840d 100644 --- a/hikari/impl/cache.py +++ b/hikari/impl/cache.py @@ -24,11 +24,11 @@ import typing -from hikari import cache +from hikari.api import cache from hikari.utilities import data_binding if typing.TYPE_CHECKING: - from hikari import app as app_ + from hikari.api import app as app_ from hikari.models import applications from hikari.models import audit_logs from hikari.models import channels diff --git a/hikari/impl/entity_factory.py b/hikari/impl/entity_factory.py index 520b4d0517..07c4bdf392 100644 --- a/hikari/impl/entity_factory.py +++ b/hikari/impl/entity_factory.py @@ -25,8 +25,8 @@ import datetime import typing -from hikari import app as app_ -from hikari import entity_factory +from hikari.api import app as app_ +from hikari.api import entity_factory from hikari.models import applications from hikari.models import audit_logs from hikari.models import channels as channels_ diff --git a/hikari/impl/event_manager_core.py b/hikari/impl/event_manager_core.py index bb69e58f5b..1095de5edc 100644 --- a/hikari/impl/event_manager_core.py +++ b/hikari/impl/event_manager_core.py @@ -26,8 +26,8 @@ import functools import typing -from hikari import event_consumer -from hikari import event_dispatcher +from hikari.api import event_consumer +from hikari.api import event_dispatcher from hikari.events import base from hikari.events import other from hikari.net import gateway @@ -38,7 +38,7 @@ from hikari.utilities import undefined if typing.TYPE_CHECKING: - from hikari import app as app_ + from hikari.api import app as app_ _EventT = typing.TypeVar("_EventT", bound=base.HikariEvent, covariant=True) _PredicateT = typing.Callable[[_EventT], typing.Union[bool, typing.Coroutine[None, typing.Any, bool]]] diff --git a/hikari/impl/gateway_zookeeper.py b/hikari/impl/gateway_zookeeper.py index a3b2a67189..0736fbd601 100644 --- a/hikari/impl/gateway_zookeeper.py +++ b/hikari/impl/gateway_zookeeper.py @@ -33,8 +33,8 @@ import typing from hikari import _about -from hikari import app as app_ -from hikari import event_dispatcher +from hikari.api import app as app_ +from hikari.api import event_dispatcher from hikari.events import other from hikari.net import gateway from hikari.utilities import undefined diff --git a/hikari/impl/rest_app.py b/hikari/impl/rest_app.py index 2925ef16c9..5d28e8fb43 100644 --- a/hikari/impl/rest_app.py +++ b/hikari/impl/rest_app.py @@ -25,7 +25,7 @@ import typing from concurrent import futures -from hikari import app as app_ +from hikari.api import app as app_ from hikari import http_settings from hikari.impl import cache as cache_impl from hikari.impl import entity_factory as entity_factory_impl @@ -34,8 +34,8 @@ from hikari.utilities import undefined if typing.TYPE_CHECKING: - from hikari import cache as cache_ - from hikari import entity_factory as entity_factory_ + from hikari.api import cache as cache_ + from hikari.api import entity_factory as entity_factory_ class RESTAppImpl(app_.IRESTApp): diff --git a/hikari/models/__init__.py b/hikari/models/__init__.py index 679a22b40e..4297364d3f 100644 --- a/hikari/models/__init__.py +++ b/hikari/models/__init__.py @@ -16,7 +16,7 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . -"""Marshall classes used for describing discord entities.""" +"""Data classes representing Discord entities.""" from __future__ import annotations diff --git a/hikari/models/bases.py b/hikari/models/bases.py index 54f602d36c..e63980ae09 100644 --- a/hikari/models/bases.py +++ b/hikari/models/bases.py @@ -32,7 +32,7 @@ if typing.TYPE_CHECKING: import datetime - from hikari import app as app_ + from hikari.api import app as app_ @attr.s(eq=True, hash=False, init=False, kw_only=True, slots=False) diff --git a/hikari/net/__init__.py b/hikari/net/__init__.py index 18710ca9fe..afbd205d76 100644 --- a/hikari/net/__init__.py +++ b/hikari/net/__init__.py @@ -16,7 +16,7 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . -"""The logic used for handling Discord's REST and gateway APIs.""" +"""Communication tools for Discord's network-level API endpoints.""" from __future__ import annotations diff --git a/hikari/net/gateway.py b/hikari/net/gateway.py index c7979f0da6..e558189df5 100644 --- a/hikari/net/gateway.py +++ b/hikari/net/gateway.py @@ -33,7 +33,7 @@ import aiohttp import attr -from hikari import component +from hikari.api import component from hikari import errors from hikari.net import http_client from hikari.net import ratelimits @@ -46,7 +46,7 @@ if typing.TYPE_CHECKING: import datetime - from hikari import app as app_ + from hikari.api import app as app_ from hikari import http_settings from hikari.models import channels from hikari.models import guilds diff --git a/hikari/net/iterators.py b/hikari/net/iterators.py index cf70903896..db9a35db04 100644 --- a/hikari/net/iterators.py +++ b/hikari/net/iterators.py @@ -30,7 +30,7 @@ from hikari.utilities import undefined if typing.TYPE_CHECKING: - from hikari import app as app_ + from hikari.api import app as app_ from hikari.models import applications from hikari.models import audit_logs from hikari.models import guilds diff --git a/hikari/net/rest.py b/hikari/net/rest.py index 12fd22713e..4ebae0013b 100644 --- a/hikari/net/rest.py +++ b/hikari/net/rest.py @@ -28,7 +28,7 @@ import aiohttp -from hikari import component +from hikari.api import component from hikari import errors from hikari import http_settings from hikari.net import buckets @@ -44,7 +44,7 @@ from hikari.utilities import undefined if typing.TYPE_CHECKING: - from hikari import app as app_ + from hikari.api import app as app_ from hikari.models import applications from hikari.models import audit_logs @@ -1183,7 +1183,7 @@ async def edit_message( The channel to edit the message in. This may be a channel object, or the ID of an existing channel. message : hikari.models.messages.Messages | hikari.utilities.snowflake.Snowflake | int | str - The message to fetch. + The message to fetch. text embed mentions_everyone diff --git a/hikari/net/rest_utils.py b/hikari/net/rest_utils.py index a8f79fa4e1..0566bd44d4 100644 --- a/hikari/net/rest_utils.py +++ b/hikari/net/rest_utils.py @@ -41,7 +41,7 @@ if typing.TYPE_CHECKING: import types - from hikari import app as app_ + from hikari.api import app as app_ from hikari.models import bases from hikari.models import channels from hikari.models import colors diff --git a/hikari/net/voice_gateway.py b/hikari/net/voice_gateway.py index 65542214c2..4bf1e9b158 100644 --- a/hikari/net/voice_gateway.py +++ b/hikari/net/voice_gateway.py @@ -31,6 +31,7 @@ import aiohttp import attr +from hikari.api import component from hikari import errors from hikari.net import http_client from hikari.net import ratelimits @@ -38,12 +39,13 @@ from hikari.utilities import klass if typing.TYPE_CHECKING: + from hikari.api import app as app_ from hikari import bot from hikari import http_settings from hikari.models import bases -class VoiceGateway(http_client.HTTPClient): +class VoiceGateway(http_client.HTTPClient, component.IComponent): """Implementation of the V4 Voice Gateway.""" @enum.unique @@ -155,6 +157,10 @@ def is_alive(self) -> bool: """Return whether the client is alive.""" return not math.isnan(self.connected_at) + @property + def app(self) -> app_.IApp: + return self._app + async def run(self) -> None: """Start the voice gateway client session.""" try: diff --git a/hikari/utilities/data_binding.py b/hikari/utilities/data_binding.py index c5e996c2cc..1b2f228fec 100644 --- a/hikari/utilities/data_binding.py +++ b/hikari/utilities/data_binding.py @@ -89,9 +89,9 @@ def load_json(_: str) -> typing.Union[JSONArray, JSONObject]: class StringMapBuilder(typing.Dict[str, str]): """Helper class used to quickly build query strings or header maps. - This will consume any items that are not + This will consume any items that are not `hikari.utilities.unspecified.Unspecified`. If a value _is_ unspecified, - it will be ignored when inserting it. This reduces the amount of + it will be ignored when inserting it. This reduces the amount of boilerplate needed for generating the headers and query strings for low-level HTTP API interaction, amongst other things. @@ -163,8 +163,8 @@ def from_dict(cls, d: typing.Union[undefined.Undefined, typing.Dict[str, typing. class JSONObjectBuilder(typing.Dict[str, JSONAny]): """Helper class used to quickly build JSON objects from various values. - - If provided with any values that are + + If provided with any values that are `hikari.utilities.unspecified.Unspecified`, then these values will be ignored. This speeds up generation of JSON payloads for low level HTTP and websocket @@ -252,7 +252,7 @@ def put_snowflake_array( self, key: str, values: typing.Union[undefined.Undefined, typing.Iterable[typing.SupportsInt, int]] ) -> None: """Put an array of snowflakes with the given key into this builder. - + If an undefined value is given, it is ignored. Each snowflake should be castable to an `int`. @@ -273,17 +273,17 @@ def cast_json_array( array: JSONArray, cast: typing.Callable[[JSONAny], T], collection_type: typing.Type[CollectionT] = typing.List[T], ) -> CollectionT: """Cast a JSON array to a given generic collection type. - + This will perform casts on each internal item individually. Note that >>> cast_json_array(raw_list, foo, bar) - + ...is equivalent to doing.... >>> bar(foo(item) for item in raw_list) - + Parameters ---------- array : JSONArray diff --git a/tests/hikari/impl/test_entity_factory.py b/tests/hikari/impl/test_entity_factory.py index 13ca9ec24c..cc468c93a6 100644 --- a/tests/hikari/impl/test_entity_factory.py +++ b/tests/hikari/impl/test_entity_factory.py @@ -21,7 +21,7 @@ import mock import pytest -from hikari import app as app_ +from hikari.api import app as app_ from hikari.impl import entity_factory from hikari.models import applications from hikari.models import audit_logs From 965c8c512a4cf6d2247dd866a8676fb1503e3c7d Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Tue, 2 Jun 2020 16:48:33 +0100 Subject: [PATCH 431/922] Fixed ratelimits and buckets docstrings. --- hikari/net/buckets.py | 186 ++++++++++++++++++++++++++++++++++++++- hikari/net/ratelimits.py | 141 +---------------------------- 2 files changed, 188 insertions(+), 139 deletions(-) diff --git a/hikari/net/buckets.py b/hikari/net/buckets.py index 12c9cb46c5..cc31b9a886 100644 --- a/hikari/net/buckets.py +++ b/hikari/net/buckets.py @@ -16,7 +16,191 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . -"""Rate-limit implementations for the RESTSession clients.""" +"""Rate-limit extensions for RESTful bucketed endpoints. + +Provides implementations for the complex rate limiting mechanisms that Discord +requires for rate limit handling that conforms to the passed bucket headers +correctly. + +This was initially a bit of a headache for me to understand, personally, since +there is a lot of "implicit detail" that is easy to miss from the documentation. + +In an attempt to make this somewhat understandable by anyone else, I have tried +to document the theory of how this is handled here. + +What is the theory behind this implementation? +---------------------------------------------- + +In this module, we refer to a `hikari.net.routes.CompiledRoute` as a definition +of a route with specific major parameter values included (e.g. +`POST /channels/123/messages`), and a `hikari.net.routes.Route` as a +definition of a route without specific parameter values included (e.g. +`POST /channels/{channel}/messages`). We can compile a +`hikari.net.routes.CompiledRoute` from a `hikari.net.routes.Route` +by providing the corresponding parameters as kwargs, as you may already know. + +In this module, a "bucket" is an internal data structure that tracks and +enforces the rate limit state for a specific `hikari.net.routes.CompiledRoute`, +and can manage delaying tasks in the event that we begin to get rate limited. +It also supports providing in-order execution of queued tasks. + +Discord allocates types of buckets to routes. If you are making a request and +there is a valid rate limit on the route you hit, you should receive an +`X-RateLimit-Bucket` header from the server in your response. This is a hash +that identifies a route based on internal criteria that does not include major +parameters. This `X-RateLimitBucket` is known in this module as an "bucket hash". + +This means that generally, the route `POST /channels/123/messages` and +`POST /channels/456/messages` will usually sit in the same bucket, but +`GET /channels/123/messages/789` and `PATCH /channels/123/messages/789` will +usually not share the same bucket. Discord may or may not change this at any +time, so hard coding this logic is not a useful thing to be doing. + +Rate limits, on the other hand, apply to a bucket and are specific to the major +parameters of the compiled route. This means that `POST /channels/123/messages` +and `POST /channels/456/messages` do not share the same real bucket, despite +Discord providing the same bucket hash. A real bucket hash is the `str` hash of +the bucket that Discord sends us in a response concatenated to the corresponding +major parameters. This is used for quick bucket indexing internally in this +module. + +One issue that occurs from this is that we cannot effectively hash a +`hikari.net.routes.CompiledRoute` that has not yet been hit, meaning that +until we receive a response from this endpoint, we have no idea what our rate +limits could be, nor the bucket that they sit in. This is usually not +problematic, as the first request to an endpoint should never be rate limited +unless you are hitting it from elsewhere in the same time window outside your +hikari.models.applications. To manage this situation, unknown endpoints are allocated to +a special unlimited bucket until they have an initial bucket hash code allocated +from a response. Once this happens, the route is reallocated a dedicated bucket. +Unknown buckets have a hardcoded initial hash code internally. + +Initially acquiring time on a bucket +------------------------------------ + +Each time you `BaseRateLimiter.acquire()` a request timeslice for a given +`hikari.net.routes.Route`, several things happen. The first is that we +attempt to find the existing bucket for that route, if there is one, or get an +unknown bucket otherwise. This is done by creating a real bucket hash from the +compiled route. The initial hash is calculated using a lookup table that maps +`hikari.net.routes.CompiledRoute` objects to their corresponding initial hash +codes, or to the unknown bucket hash code if not yet known. This initial hash is +processed by the `hikari.net.routes.CompiledRoute` to provide the real bucket +hash we need to get the route's bucket object internally. + +The `BaseRateLimiter.acquire()` method will take the bucket and acquire a new +timeslice on it. This takes the form of a `asyncio.Future` which should be +awaited by the caller and will complete once the caller is allowed to make a +request. Most of the time, this is done instantly, but if the bucket has an +active rate limit preventing requests being sent, then the future will be paused +until the rate limit is over. This may be longer than the rate limit period if +you have queued a large number of requests during this limit, as it is +first-come-first-served. + +Acquiring a rate limited bucket will start a bucket-wide task (if not already +running) that will wait until the rate limit has completed before allowing more +futures to complete. This is done while observing the rate limits again, so can +easily begin to re-ratelimit itself if needed. Once the task is complete, it +tidies itself up and disposes of itself. This task will complete once the queue +becomes empty. + +The result of `RESTBucketManager.acquire()` is a tuple of a `asyncio.Future` to +await on which completes when you are allowed to proceed with making a request, +and a real bucket hash which should be stored temporarily. This will be +explained in the next section. + +When you make your response, you should be sure to set the +`X-RateLimit-Precision` header to `millisecond` to ensure a much greater +accuracy against rounding errors for rate limits (reduces the error margin from +`1` second to `1` millisecond). + +Handling the rate limit headers of a response +--------------------------------------------- + +Once you have received your response, you are expected to extract the values of +the vital rate limit headers manually and parse them to the correct data types. +These headers are: + +* `Date`: + the response date on the server. This should be parsed to a + `datetime.datetime` using `email.utils.parsedate_to_datetime`. +* `X-RateLimit-Limit`: + an `int` describing the max requests in the bucket from empty to being rate + limited. +* `X-RateLimit-Remaining`: + an `int` describing the remaining number of requests before rate limiting + occurs in the current window. +* `X-RateLimit-Bucket`: + a `str` containing the initial bucket hash. +* `X-RateLimit-Reset`: + a `float` containing the number of seconds since + 1st January 1970 at 0:00:00 UTC at which the current ratelimit window + resets. This should be parsed to a `datetime.datetime` using + `datetime.datetime.fromtimestamp`, passing `datetime.timezone.utc` + as `tz`. + +Each of the above values should be passed to the `update_rate_limits` method to +ensure that the bucket you acquired time from is correctly updated should +Discord decide to alter their ratelimits on the fly without warning (including +timings and the bucket). + +This method will manage creating new buckets as needed and resetting vital +information in each bucket you use. + +Tidying up +---------- + +To prevent unused buckets cluttering up memory, each `RESTBucketManager` +instance spins up a `asyncio.Task` that periodically locks the bucket list +(not threadsafe, only using the concept of asyncio not yielding in regular +functions) and disposes of any clearly stale buckets that are no longer needed. +These will be recreated again in the future if they are needed. + +When shutting down an application, one must remember to `close()` the +`RESTBucketManager` that has been used. This will ensure the garbage collection +task is stopped, and will also ensure any remaining futures in any bucket queues +have an `asyncio.CancelledError` set on them to prevent deadlocking ratelimited +calls that may be waiting to be unlocked. + +Body-field-specific rate limiting +--------------------------------- + +As of the start of June, 2020, Discord appears to be enforcing another layer +of rate limiting logic to their REST APIs which is field-specific. This means +that special rate limits will also exist on some endpoints that limit based +on what attributes you send in a JSON or form data payload. + +No information is sent in headers about these specific limits. You will only +be made aware that they exist once you get ratelimited. In the 429 ratelimited +response, you will have the `"global"` attribute set to `false`, and a +`"reset_after"` attribute that differs entirely to the `X-RateLimit-Reset` +header. Thus, it is important to not assume the value in the 429 response +for the reset time is the same as the one in the bucket headers. Hikari's +`hikari.net.rest.REST` implementation specifically uses the value furthest +in the future when working out which bucket to adhere to. + +It is worth remembering that there is an API limit to the number of 401s, +403s, and 429s you receive, which is around 10,000 per 15 minutes. Passing this +limit results in a soft ban of your account. + +At the time of writing, the only example of this appears to be on the +`PATCH /channels/{channel_id}` endpoint. This has a limit of two changes per +10 minutes. More details about how this is implemented have yet to be +released or documented... + + +Caveats +------- + +These implementations rely on Discord sending consistent buckets back to us. + +This also begins to crumble if more than one REST client is in use, since +there is no performant way to communicate shared rate limits between +distributed applications. The general concept to follow is that if you are +making repeated API calls, or calls that are not event-based (e.g. +fetching messages on a timer), then this can be considered a form of API abuse +and should be used sparingly. +""" from __future__ import annotations diff --git a/hikari/net/ratelimits.py b/hikari/net/ratelimits.py index 1e664d0290..be1c8b082b 100644 --- a/hikari/net/ratelimits.py +++ b/hikari/net/ratelimits.py @@ -16,144 +16,9 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . -"""Complex rate limiting mechanisms. - -Provides an implementation for the complex rate limiting mechanisms that Discord -requires for rate limit handling that conforms to the passed bucket headers -correctly. - -What is the theory behind this implementation? ----------------------------------------------- - -In this module, we refer to a `hikari.net.routes.CompiledRoute` as a definition -of a route with specific major parameter values included (e.g. -`POST /channels/123/messages`), and a `hikari.net.routes.Route` as a -definition of a route without specific parameter values included (e.g. -`POST /channels/{channel}/messages`). We can compile a -`hikari.net.routes.CompiledRoute` from a `hikari.net.routes.Route` -by providing the corresponding parameters as kwargs, as you may already know. - -In this module, a "bucket" is an internal data structure that tracks and -enforces the rate limit state for a specific `hikari.net.routes.CompiledRoute`, -and can manage delaying tasks in the event that we begin to get rate limited. -It also supports providing in-order execution of queued tasks. - -Discord allocates types of buckets to routes. If you are making a request and -there is a valid rate limit on the route you hit, you should receive an -`X-RateLimit-Bucket` header from the server in your response. This is a hash -that identifies a route based on internal criteria that does not include major -parameters. This `X-RateLimitBucket` is known in this module as an "bucket hash". - -This means that generally, the route `POST /channels/123/messages` and -`POST /channels/456/messages` will usually sit in the same bucket, but -`GET /channels/123/messages/789` and `PATCH /channels/123/messages/789` will -usually not share the same bucket. Discord may or may not change this at any -time, so hard coding this logic is not a useful thing to be doing. - -Rate limits, on the other hand, apply to a bucket and are specific to the major -parameters of the compiled route. This means that `POST /channels/123/messages` -and `POST /channels/456/messages` do not share the same real bucket, despite -Discord providing the same bucket hash. A real bucket hash is the `str` hash of -the bucket that Discord sends us in a response concatenated to the corresponding -major parameters. This is used for quick bucket indexing internally in this -module. - -One issue that occurs from this is that we cannot effectively hash a -`hikari.net.routes.CompiledRoute` that has not yet been hit, meaning that -until we receive a response from this endpoint, we have no idea what our rate -limits could be, nor the bucket that they sit in. This is usually not -problematic, as the first request to an endpoint should never be rate limited -unless you are hitting it from elsewhere in the same time window outside your -hikari.models.applications. To manage this situation, unknown endpoints are allocated to -a special unlimited bucket until they have an initial bucket hash code allocated -from a response. Once this happens, the route is reallocated a dedicated bucket. -Unknown buckets have a hardcoded initial hash code internally. - -Initially acquiring time on a bucket ------------------------------------- - -Each time you `BaseRateLimiter.acquire()` a request timeslice for a given -`hikari.net.routes.Route`, several things happen. The first is that we -attempt to find the existing bucket for that route, if there is one, or get an -unknown bucket otherwise. This is done by creating a real bucket hash from the -compiled route. The initial hash is calculated using a lookup table that maps -`hikari.net.routes.CompiledRoute` objects to their corresponding initial hash -codes, or to the unknown bucket hash code if not yet known. This initial hash is -processed by the `hikari.net.routes.CompiledRoute` to provide the real bucket -hash we need to get the route's bucket object internally. - -The `acquire` method will take the bucket and acquire a new timeslice on -it. This takes the form of a `asyncio.Future` which should be awaited by -the caller and will complete once the caller is allowed to make a request. Most -of the time, this is done instantly, but if the bucket has an active rate limit -preventing requests being sent, then the future will be paused until the rate -limit is over. This may be longer than the rate limit period if you have queued -a large number of requests during this limit, as it is first-come-first-served. - -Acquiring a rate limited bucket will start a bucket-wide task (if not already -running) that will wait until the rate limit has completed before allowing more -futures to complete. This is done while observing the rate limits again, so can -easily begin to re-ratelimit itself if needed. Once the task is complete, it -tidies itself up and disposes of itself. This task will complete once the queue -becomes empty. - -The result of `RateLimiter.acquire()` is a tuple of a `asyncio.Future` to await -on which completes when you are allowed to proceed with making a request, and a -real bucket hash which should be stored temporarily. This will be explained in -the next section. - -When you make your response, you should be sure to set the -`X-RateLimit-Precision` header to `millisecond` to ensure a much greater -accuracy against rounding errors for rate limits (reduces the error margin from -`1` second to `1` millisecond). - -Handling the rate limit headers of a response ---------------------------------------------- - -Once you have received your response, you are expected to extract the values of -the vital rate limit headers manually and parse them to the correct data types. -These headers are: - -* `Date`: - the response date on the server. This should be parsed to a - `datetime.datetime` using `email.utils.parsedate_to_datetime`. -* `X-RateLimit-Limit`: - an `int` describing the max requests in the bucket from empty to being rate - limited. -* `X-RateLimit-Remaining`: - an `int` describing the remaining number of requests before rate limiting - occurs in the current window. -* `X-RateLimit-Bucket`: - a `str` containing the initial bucket hash. -* `X-RateLimit-Reset`: - a `float` containing the number of seconds since - 1st January 1970 at 0:00:00 UTC at which the current ratelimit window - resets. This should be parsed to a `datetime.datetime` using - `datetime.datetime.fromtimestamp`, passing `datetime.timezone.utc` - as `tz`. - -Each of the above values should be passed to the `update_rate_limits` method to -ensure that the bucket you acquired time from is correctly updated should -Discord decide to alter their ratelimits on the fly without warning (including -timings and the bucket). - -This method will manage creating new buckets as needed and resetting vital -information in each bucket you use. - -Tidying up ----------- - -To prevent unused buckets cluttering up memory, each `BaseRateLimiter` -instance spins up a `asyncio.Task` that periodically locks the bucket list -(not threadsafe, only using the concept of asyncio not yielding in regular -functions) and disposes of any clearly stale buckets that are no longer needed. -These will be recreated again in the future if they are needed. - -When shutting down an application, one must remember to `close()` the -`BaseRateLimiter` that has been used. This will ensure the garbage collection -task is stopped, and will also ensure any remaining futures in any bucket queues -have an `asyncio.CancelledError` set on them to prevent deadlocking ratelimited -calls that may be waiting to be unlocked. +"""Basic lazy ratelimit systems for asyncio. + +See `hikari.net.buckets` for REST-specific rate-limiting logic. """ from __future__ import annotations From f86b259255e86994599db95cfb0079418437dda8 Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Tue, 2 Jun 2020 18:52:21 +0100 Subject: [PATCH 432/922] Removed '__init__' imports and moved http_settings to hikari.net --- docs/head.mako | 10 +++++--- hikari/__init__.py | 12 +++------ hikari/api/__init__.py | 16 +----------- hikari/api/app.py | 2 +- hikari/events/__init__.py | 9 +------ hikari/events/guild.py | 2 +- hikari/events/message.py | 2 +- hikari/impl/__init__.py | 18 +------------ hikari/impl/bot.py | 6 +++-- hikari/impl/entity_factory.py | 1 - hikari/impl/gateway_zookeeper.py | 2 +- hikari/impl/rest_app.py | 7 ++++-- hikari/models/__init__.py | 42 +------------------------------ hikari/models/emojis.py | 1 - hikari/net/__init__.py | 6 +---- hikari/net/gateway.py | 6 ++--- hikari/{ => net}/http_settings.py | 0 hikari/net/iterators.py | 1 - hikari/net/rest.py | 5 ++-- hikari/net/voice_gateway.py | 4 +-- hikari/utilities/data_binding.py | 1 - hikari/utilities/date.py | 2 -- 22 files changed, 35 insertions(+), 120 deletions(-) rename hikari/{ => net}/http_settings.py (100%) diff --git a/docs/head.mako b/docs/head.mako index aeb75c4bc7..c72a8d8727 100644 --- a/docs/head.mako +++ b/docs/head.mako @@ -1,5 +1,9 @@ +<% + import hikari +%> + \ No newline at end of file + diff --git a/hikari/__init__.py b/hikari/__init__.py index b8fc4b699a..14f6eacfdc 100644 --- a/hikari/__init__.py +++ b/hikari/__init__.py @@ -30,14 +30,8 @@ from hikari._about import __license__ from hikari._about import __url__ from hikari._about import __version__ -from hikari.errors import * -from hikari.events import * -from hikari.api import * -from hikari.http_settings import * -from hikari.models import * -from hikari.net import * -from hikari.impl.bot import BotImpl as BotApp -from hikari.impl.rest_app import RESTAppImpl as RESTApp +from hikari.impl.bot import BotImpl as Bot +from hikari.impl.bot import BotImpl as RESTClient -__all__ = api.__all__ + errors.__all__ + http_settings.__all__ + models.__all__ + net.__all__ + ["BotApp", "RESTApp"] +__all__ = ["RESTClient", "Bot"] diff --git a/hikari/api/__init__.py b/hikari/api/__init__.py index da4a013b8a..cda5f6e30f 100644 --- a/hikari/api/__init__.py +++ b/hikari/api/__init__.py @@ -24,18 +24,4 @@ """ from __future__ import annotations -from hikari.api.app import * -from hikari.api.cache import * -from hikari.api.component import * -from hikari.api.entity_factory import * -from hikari.api.event_consumer import * -from hikari.api.event_dispatcher import * - -__all__ = ( - app.__all__ - + cache.__all__ - + component.__all__ - + entity_factory.__all__ - + event_consumer.__all__ - + event_dispatcher.__all__ -) +__all__ = [] diff --git a/hikari/api/app.py b/hikari/api/app.py index 5a8016bfd9..325ede90b1 100644 --- a/hikari/api/app.py +++ b/hikari/api/app.py @@ -38,7 +38,7 @@ from hikari.api import entity_factory as entity_factory_ from hikari.api import event_consumer as event_consumer_ from hikari.models import presences - from hikari import http_settings as http_settings_ + from hikari.net import http_settings as http_settings_ from hikari.net import gateway from hikari.net import rest as rest_ diff --git a/hikari/events/__init__.py b/hikari/events/__init__.py index d39dabbc7f..e6d867bd7f 100644 --- a/hikari/events/__init__.py +++ b/hikari/events/__init__.py @@ -20,11 +20,4 @@ from __future__ import annotations -from hikari.events.base import * -from hikari.events.channel import * -from hikari.events.guild import * -from hikari.events.message import * -from hikari.events.other import * -from hikari.events.voice import * - -__all__ = base.__all__ + channel.__all__ + guild.__all__ + message.__all__ + other.__all__ + voice.__all__ +__all__ = [] diff --git a/hikari/events/guild.py b/hikari/events/guild.py index dae0ba395b..4367c80839 100644 --- a/hikari/events/guild.py +++ b/hikari/events/guild.py @@ -51,8 +51,8 @@ from hikari.models import intents from hikari.models import presences from hikari.models import users -from hikari.utilities import undefined from hikari.utilities import snowflake +from hikari.utilities import undefined if typing.TYPE_CHECKING: import datetime diff --git a/hikari/events/message.py b/hikari/events/message.py index e6d0357ff5..0f2f299ebf 100644 --- a/hikari/events/message.py +++ b/hikari/events/message.py @@ -44,8 +44,8 @@ from hikari.models import intents from hikari.models import messages from hikari.models import users -from hikari.utilities import undefined from hikari.utilities import snowflake +from hikari.utilities import undefined if typing.TYPE_CHECKING: import datetime diff --git a/hikari/impl/__init__.py b/hikari/impl/__init__.py index 585d9e8d84..8f9a70f321 100644 --- a/hikari/impl/__init__.py +++ b/hikari/impl/__init__.py @@ -26,20 +26,4 @@ from __future__ import annotations -from hikari.impl.bot import * -from hikari.impl.cache import * -from hikari.impl.entity_factory import * -from hikari.impl.event_manager import * -from hikari.impl.event_manager_core import * -from hikari.impl.gateway_zookeeper import * -from hikari.impl.rest_app import * - -__all__ = ( - bot.__all__ - + cache.__all__ - + entity_factory.__all__ - + event_manager.__all__ - + event_manager_core.__all__ - + gateway_zookeeper.__all__ - + rest_app.__all__ -) +__all__ = [] diff --git a/hikari/impl/bot.py b/hikari/impl/bot.py index 79d53036bf..703ed85825 100644 --- a/hikari/impl/bot.py +++ b/hikari/impl/bot.py @@ -32,6 +32,7 @@ from hikari.impl import event_manager from hikari.impl import gateway_zookeeper from hikari.models import presences +from hikari.net import http_settings as http_settings_ from hikari.net import rest from hikari.utilities import klass from hikari.utilities import undefined @@ -42,7 +43,6 @@ from hikari.api import cache as cache_ from hikari.api import entity_factory as entity_factory_ from hikari.api import event_consumer as event_consumer_ - from hikari import http_settings as http_settings_ from hikari.api import event_dispatcher from hikari.models import gateway as gateway_models from hikari.models import intents as intents_ @@ -52,7 +52,7 @@ class BotImpl(gateway_zookeeper.AbstractGatewayZookeeper, app.IBot): def __init__( self, *, - config: http_settings_.HTTPSettings, + config: typing.Union[undefined.Undefined, http_settings_.HTTPSettings] = undefined.Undefined(), debug: bool = False, gateway_version: int = 6, initial_activity: typing.Optional[presences.OwnActivity] = None, @@ -70,6 +70,8 @@ def __init__( ): self._logger = klass.get_logger(self) + config = http_settings_.HTTPSettings() if isinstance(config, undefined.Undefined) else config + self._cache = cache_impl.CacheImpl(app=self) self._config = config self._event_manager = event_manager.EventManagerImpl(app=self) diff --git a/hikari/impl/entity_factory.py b/hikari/impl/entity_factory.py index 07c4bdf392..384176b198 100644 --- a/hikari/impl/entity_factory.py +++ b/hikari/impl/entity_factory.py @@ -49,7 +49,6 @@ if typing.TYPE_CHECKING: from hikari.utilities import data_binding - DMChannelT = typing.TypeVar("DMChannelT", bound=channels_.DMChannel) GuildChannelT = typing.TypeVar("GuildChannelT", bound=channels_.GuildChannel) InviteT = typing.TypeVar("InviteT", bound=invites.Invite) diff --git a/hikari/impl/gateway_zookeeper.py b/hikari/impl/gateway_zookeeper.py index 0736fbd601..e8ea86520b 100644 --- a/hikari/impl/gateway_zookeeper.py +++ b/hikari/impl/gateway_zookeeper.py @@ -40,7 +40,7 @@ from hikari.utilities import undefined if typing.TYPE_CHECKING: - from hikari import http_settings + from hikari.net import http_settings from hikari.models import gateway as gateway_models from hikari.models import intents as intents_ from hikari.models import presences diff --git a/hikari/impl/rest_app.py b/hikari/impl/rest_app.py index 5d28e8fb43..0457dc03ff 100644 --- a/hikari/impl/rest_app.py +++ b/hikari/impl/rest_app.py @@ -26,9 +26,9 @@ from concurrent import futures from hikari.api import app as app_ -from hikari import http_settings from hikari.impl import cache as cache_impl from hikari.impl import entity_factory as entity_factory_impl +from hikari.net import http_settings as http_settings_ from hikari.net import rest as rest_ from hikari.utilities import klass from hikari.utilities import undefined @@ -41,7 +41,7 @@ class RESTAppImpl(app_.IRESTApp): def __init__( self, - config: http_settings.HTTPSettings, + config: typing.Union[undefined.Undefined, http_settings_.HTTPSettings] = undefined.Undefined(), debug: bool = False, token: typing.Optional[str] = None, token_type: typing.Union[undefined.Undefined, str] = undefined.Undefined(), @@ -49,6 +49,9 @@ def __init__( version: int = 6, ) -> None: self._logger = klass.get_logger(self) + + config = http_settings_.HTTPSettings() if isinstance(config, undefined.Undefined) else config + self._rest = rest_.REST( app=self, config=config, diff --git a/hikari/models/__init__.py b/hikari/models/__init__.py index 4297364d3f..e98ab52cf1 100644 --- a/hikari/models/__init__.py +++ b/hikari/models/__init__.py @@ -20,44 +20,4 @@ from __future__ import annotations -from hikari.models.applications import * -from hikari.models.audit_logs import * -from hikari.models.bases import * -from hikari.models.channels import * -from hikari.models.colors import * -from hikari.models.colours import * -from hikari.models.embeds import * -from hikari.models.emojis import * -from hikari.models.files import * -from hikari.models.gateway import * -from hikari.models.guilds import * -from hikari.models.intents import * -from hikari.models.invites import * -from hikari.models.messages import * -from hikari.models.permissions import * -from hikari.models.presences import * -from hikari.models.users import * -from hikari.models.voices import * -from hikari.models.webhooks import * - -__all__ = ( - applications.__all__ - + audit_logs.__all__ - + bases.__all__ - + channels.__all__ - + colors.__all__ - + colours.__all__ - + embeds.__all__ - + emojis.__all__ - + files.__all__ - + gateway.__all__ - + guilds.__all__ - + intents.__all__ - + invites.__all__ - + messages.__all__ - + permissions.__all__ - + presences.__all__ - + users.__all__ - + voices.__all__ - + webhooks.__all__ -) +__all__ = [] diff --git a/hikari/models/emojis.py b/hikari/models/emojis.py index a31cb81c32..3f5c6a044f 100644 --- a/hikari/models/emojis.py +++ b/hikari/models/emojis.py @@ -36,7 +36,6 @@ from hikari.models import users from hikari.utilities import snowflake - _TWEMOJI_PNG_BASE_URL: typing.Final[str] = "https://github.com/twitter/twemoji/raw/master/assets/72x72/" """The URL for Twemoji PNG artwork for built-in emojis.""" diff --git a/hikari/net/__init__.py b/hikari/net/__init__.py index afbd205d76..a0304ad728 100644 --- a/hikari/net/__init__.py +++ b/hikari/net/__init__.py @@ -20,8 +20,4 @@ from __future__ import annotations -from hikari.net.gateway import * -from hikari.net.rest import * -from hikari.net.voice_gateway import * - -__all__ = gateway.__all__ + rest.__all__ + voice_gateway.__all__ +__all__ = [] diff --git a/hikari/net/gateway.py b/hikari/net/gateway.py index e558189df5..601b3df661 100644 --- a/hikari/net/gateway.py +++ b/hikari/net/gateway.py @@ -33,12 +33,12 @@ import aiohttp import attr -from hikari.api import component from hikari import errors +from hikari.api import component +from hikari.models import presences from hikari.net import http_client from hikari.net import ratelimits from hikari.net import user_agents -from hikari.models import presences from hikari.utilities import data_binding from hikari.utilities import klass from hikari.utilities import undefined @@ -47,7 +47,7 @@ import datetime from hikari.api import app as app_ - from hikari import http_settings + from hikari.net import http_settings from hikari.models import channels from hikari.models import guilds from hikari.models import intents as intents_ diff --git a/hikari/http_settings.py b/hikari/net/http_settings.py similarity index 100% rename from hikari/http_settings.py rename to hikari/net/http_settings.py diff --git a/hikari/net/iterators.py b/hikari/net/iterators.py index db9a35db04..8f84bb8702 100644 --- a/hikari/net/iterators.py +++ b/hikari/net/iterators.py @@ -37,7 +37,6 @@ from hikari.models import messages from hikari.models import users - _T = typing.TypeVar("_T") diff --git a/hikari/net/rest.py b/hikari/net/rest.py index 4ebae0013b..8387b7a3eb 100644 --- a/hikari/net/rest.py +++ b/hikari/net/rest.py @@ -28,11 +28,11 @@ import aiohttp -from hikari.api import component from hikari import errors -from hikari import http_settings +from hikari.api import component from hikari.net import buckets from hikari.net import http_client +from hikari.net import http_settings from hikari.net import iterators from hikari.net import ratelimits from hikari.net import rest_utils @@ -63,7 +63,6 @@ from hikari.models import voices from hikari.models import webhooks - _REST_API_URL: typing.Final[str] = "https://discord.com/api/v{0.version}" """The URL for the RESTSession API. This contains a version number parameter that should be interpolated. diff --git a/hikari/net/voice_gateway.py b/hikari/net/voice_gateway.py index 4bf1e9b158..af0d42b2ee 100644 --- a/hikari/net/voice_gateway.py +++ b/hikari/net/voice_gateway.py @@ -31,8 +31,8 @@ import aiohttp import attr -from hikari.api import component from hikari import errors +from hikari.api import component from hikari.net import http_client from hikari.net import ratelimits from hikari.utilities import data_binding @@ -41,7 +41,7 @@ if typing.TYPE_CHECKING: from hikari.api import app as app_ from hikari import bot - from hikari import http_settings + from hikari.net import http_settings from hikari.models import bases diff --git a/hikari/utilities/data_binding.py b/hikari/utilities/data_binding.py index 1b2f228fec..8e62142dcd 100644 --- a/hikari/utilities/data_binding.py +++ b/hikari/utilities/data_binding.py @@ -41,7 +41,6 @@ from hikari.models import bases from hikari.utilities import undefined - T = typing.TypeVar("T", covariant=True) CollectionT = typing.TypeVar("CollectionT", bound=typing.Collection, contravariant=True) diff --git a/hikari/utilities/date.py b/hikari/utilities/date.py index f2c9e02add..276bb00f85 100644 --- a/hikari/utilities/date.py +++ b/hikari/utilities/date.py @@ -35,7 +35,6 @@ import re import typing - TimeSpan = typing.Union[int, float, datetime.timedelta] """Type hint representing a naive time period or time span. @@ -43,7 +42,6 @@ where `int` and `float` types are interpreted as a number of seconds. """ - DISCORD_EPOCH: typing.Final[int] = 1_420_070_400 """The Discord epoch used within snowflake identifiers. From 9d92d8d007dc66bd5dba6f55df866e6978f40fdf Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Tue, 2 Jun 2020 18:52:52 +0100 Subject: [PATCH 433/922] Fixed RESTApp import --- hikari/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hikari/__init__.py b/hikari/__init__.py index 14f6eacfdc..e4656326c3 100644 --- a/hikari/__init__.py +++ b/hikari/__init__.py @@ -32,6 +32,6 @@ from hikari._about import __version__ from hikari.impl.bot import BotImpl as Bot -from hikari.impl.bot import BotImpl as RESTClient +from hikari.impl.rest_app import RESTAppImpl as RESTClient __all__ = ["RESTClient", "Bot"] From bbc50aeb6dcb7f9e00f5739134ea44f08d51d2a4 Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Tue, 2 Jun 2020 20:11:15 +0100 Subject: [PATCH 434/922] Added intersphinx to pdoc --- ci/pdoc.nox.py | 4 ++- docs/documentation.mako | 68 +++++++++++++++++++++++++++++++++++++++-- 2 files changed, 68 insertions(+), 4 deletions(-) diff --git a/ci/pdoc.nox.py b/ci/pdoc.nox.py index 7a042125f1..5f0f4260fa 100644 --- a/ci/pdoc.nox.py +++ b/ci/pdoc.nox.py @@ -25,7 +25,9 @@ @nox.inherit_environment_vars def pdoc(session: nox.Session) -> None: """Generate documentation with pdoc.""" - session.install("-r", config.REQUIREMENTS, "git+https://github.com/pdoc3/pdoc") + session.install("-r", config.REQUIREMENTS) + session.install("pdoc3") + session.install("sphobjinv") session.run( "python", diff --git a/docs/documentation.mako b/docs/documentation.mako index 2612dd91ac..23c324b93a 100644 --- a/docs/documentation.mako +++ b/docs/documentation.mako @@ -1,3 +1,55 @@ +<%! + import builtins + import sphobjinv + + inventory_urls = [ + "https://docs.python.org/3/objects.inv", + "https://docs.aiohttp.org/en/stable/objects.inv", + "https://www.attrs.org/en/stable/objects.inv", + "https://multidict.readthedocs.io/en/latest/objects.inv", + "https://yarl.readthedocs.io/en/latest/objects.inv", + ] + + inventories = {} + + for i in inventory_urls: + print("Prefetching", i) + inv = sphobjinv.Inventory(url=i) + url, _, _ = i.partition("objects.inv") + inventories[url] = inv.json_dict() + + located_external_refs = {} + unlocatable_external_refs = set() + + + def discover_source(fqn): + #print("attempting to find", fqn, "in intersphinx inventories") + if fqn in unlocatable_external_refs: + return + + if fqn in dir(builtins): + fqn = "builtins." + fqn + + if fqn not in located_external_refs: + print("attempting to find intersphinx reference for", fqn) + for base_url, inv in inventories.items(): + for obj in inv.values(): + if isinstance(obj, dict) and obj["name"] == fqn: + uri_frag = obj["uri"] + if uri_frag.endswith("$"): + uri_frag = uri_frag[:-1] + fqn + + url = base_url + uri_frag + print("discovered", fqn, "at", url) + located_external_refs[fqn] = url + break + try: + return located_external_refs[fqn] + except KeyError: + print("blacklisting", fqn, "as it cannot be dereferenced from external documentation") + unlocatable_external_refs.add(fqn) +%> + <% import abc import enum @@ -102,11 +154,21 @@ if fully_qualified and not simple_names: name = dobj.module.name + "." + dobj.obj.__qualname__ - if isinstance(dobj, pdoc.External) and not external_links: - return name if not with_prefixes else f"{QUAL_EXTERNAL} {name}" - url = dobj.url(relative_to=module, link_prefix=link_prefix, top_ancestor=not show_inherited_members) + if url.startswith("/"): + + if isinstance(dobj, pdoc.External): + if dobj.module: + fqn = dobj.module.name + "." + dobj.obj.__qualname__ + else: + fqn = dobj.name + + url = discover_source(fqn) + + if url is None: + return name if not with_prefixes else f"{QUAL_EXTERNAL} {name}" + if simple_names: name = simple_name(name) From 1f4d02c3fb4780675be59d3b0cd63a55fbd622f6 Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Tue, 2 Jun 2020 21:48:54 +0100 Subject: [PATCH 435/922] Fixed more broken link generation in docs. --- docs/documentation.mako | 48 +++++++++++++++++++++++++++-------------- 1 file changed, 32 insertions(+), 16 deletions(-) diff --git a/docs/documentation.mako b/docs/documentation.mako index 23c324b93a..a017488920 100644 --- a/docs/documentation.mako +++ b/docs/documentation.mako @@ -27,9 +27,6 @@ if fqn in unlocatable_external_refs: return - if fqn in dir(builtins): - fqn = "builtins." + fqn - if fqn not in located_external_refs: print("attempting to find intersphinx reference for", fqn) for base_url, inv in inventories.items(): @@ -42,7 +39,7 @@ url = base_url + uri_frag print("discovered", fqn, "at", url) located_external_refs[fqn] = url - break + break try: return located_external_refs[fqn] except KeyError: @@ -61,7 +58,7 @@ from pdoc.html_helpers import extract_toc, glimpse, to_html as _to_html, format_git_link - QUAL_ABC = "abc" + QUAL_ABC = "abstract" QUAL_ASYNC_DEF = "async def" QUAL_CLASS = "class" QUAL_DATACLASS = "dataclass" @@ -94,6 +91,9 @@ prefix = "" name = name or dobj.name + if name.startswith("builtins."): + _, _, name = name.partition("builtins.") + if with_prefixes: if isinstance(dobj, pdoc.Function): if not hide_ref and dobj.module.name != dobj.obj.__module__: @@ -101,15 +101,15 @@ else: qual = dobj.funcdef() - prefix = "" + qual + " " + prefix = "" + qual + " " elif isinstance(dobj, pdoc.Variable): if dobj.module.name == "typing" or dobj.docstring and dobj.docstring.casefold().startswith(("type hint", "typehint", "type alias")): - prefix = F"{QUAL_TYPEHINT} " + prefix = F"{QUAL_TYPEHINT} " elif all(not c.isalpha() or c.isupper() for c in dobj.name): - prefix = f"{QUAL_CONST}" + prefix = f"{QUAL_CONST} " else: - prefix = f"{QUAL_VAR} " + prefix = f"{QUAL_VAR} " elif isinstance(dobj, pdoc.Class): if not hide_ref and dobj.module.name != dobj.obj.__module__: @@ -135,7 +135,7 @@ if inspect.isabstract(dobj.obj): qual = f"{QUAL_ABC} {qual}" - prefix = f"{qual} " + prefix = f"{qual} " elif isinstance(dobj, pdoc.Module): if dobj.module.name != dobj.obj.__name__: @@ -144,10 +144,10 @@ qual = "" qual += QUAL_PACKAGE if dobj.is_package else QUAL_NAMESPACE if dobj.is_namespace else QUAL_MODULE - prefix = f"{qual} " + prefix = f"{qual} " else: - prefix = f"{default_type} " + prefix = f"{default_type} " else: name = name or dobj.name or "" @@ -164,10 +164,13 @@ else: fqn = dobj.name + url = discover_source(fqn) + if url is None: + url = discover_source(name) if url is None: - return name if not with_prefixes else f"{QUAL_EXTERNAL} {name}" + return name if not with_prefixes else f"{QUAL_EXTERNAL} {name}" if simple_names: name = simple_name(name) @@ -191,7 +194,7 @@ return name def get_annotation(bound_method, sep=':'): - annot = show_type_annotations and bound_method(link=link) or '' + annot = bound_method(link=link) or '' annot = annot.replace("NoneType", "None") # Remove quotes. if annot.startswith("'") and annot.endswith("'"): @@ -201,7 +204,18 @@ return annot def to_html(text): - return _to_html(text, module=module, link=link, latex_math=latex_math) + text = _to_html(text, module=module, link=link, latex_math=latex_math) + replacements = [ + ('class="admonition info"', 'class="alert alert-primary"'), + ('class="admonition warning"', 'class="alert alert-warning"'), + ('class="admonition danger"', 'class="alert alert-danger"'), + ('class="admonition note"', 'class="alert alert-success"') + ] + + for before, after in replacements: + text = text.replace(before, after) + + return text %> <%def name="ident(name)">${name} @@ -233,7 +247,9 @@ <%def name="show_var(v, is_nested=False)"> - <% return_type = get_annotation(v.type_annotation) %> + <% + return_type = get_annotation(v.type_annotation) + %>
${link(v, anchor=True)}${return_type}
From cc0d82640dd7ffae179f5f727125e7abfca3280d Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Tue, 2 Jun 2020 23:07:31 +0100 Subject: [PATCH 436/922] Fixed docstrings in hikari.api.app --- docs/documentation.mako | 4 +- hikari/api/app.py | 185 +++++++++++++++++++++++++++++++++------- 2 files changed, 158 insertions(+), 31 deletions(-) diff --git a/docs/documentation.mako b/docs/documentation.mako index a017488920..51244b401b 100644 --- a/docs/documentation.mako +++ b/docs/documentation.mako @@ -286,7 +286,7 @@
% if redirect: ${show_desc(f) | glimpse, to_html} - This class is defined explicitly at ${link(ref, with_prefixes=True, fully_qualified=True)}. Visit that link to view the full documentation! + This class is defined explicitly at ${link(ref, with_prefixes=False, fully_qualified=True)}. Visit that link to view the full documentation! % else: ${show_desc(f)} @@ -340,7 +340,7 @@ % if redirect: ${show_desc(c) | glimpse, to_html} - This class is defined explicitly at ${link(ref, with_prefixes=True, fully_qualified=True)}. Visit that link to view the full documentation! + This class is defined explicitly at ${link(ref, with_prefixes=False, fully_qualified=True)}. Visit that link to view the full documentation! % else: ${show_desc(c)} diff --git a/hikari/api/app.py b/hikari/api/app.py index 325ede90b1..745ebf7534 100644 --- a/hikari/api/app.py +++ b/hikari/api/app.py @@ -44,29 +44,87 @@ class IApp(abc.ABC): - """Core components that any Hikari-based application will usually need.""" + """The base for any type of Hikari application object. + + All types of Hikari-based application should derive from this type in order + to provide a consistent interface that is compatible with models and events + that make reference to it. + + Following this pattern allows you to extend this library in pretty much + any direction you can think of without having to rewrite major piece of + this code base. + + Example + ------- + A quick and dirty theoretical concrete implementation may look like the + following. + + class MyApp(IApp): + def __init__(self): + self._logger = logging.getLogger(__name__) + self._cache = MyCacheImplementation(self) + self._entity_factory = MyEntityFactoryImplementation(self) + self._thread_pool = concurrent.futures.ThreadPoolExecutor() + + logger = property(lambda self: self._logger) + cache = property(lambda self: self._cache) + entity_factory = property(lambda self: self._entity_factory) + thread_pool = property(lambda self: self._thread_pool) + + async def close(self): + self._thread_pool.shutdown() + + If you are in any doubt, check out the `hikari.RESTApp` and `hikari.Bot` + implementations to see how they are pieced together! + """ __slots__ = () @property @abc.abstractmethod def logger(self) -> logging.Logger: - """Logger for logging messages.""" + """Logger for logging messages. + + Returns + ------- + logging.Logger + The application-level logger. + """ @property @abc.abstractmethod def cache(self) -> cache_.ICache: - """Entity cache.""" + """Entity cache. + + Returns + ------- + hikari.api.cache.ICache + The cache implementation used in this application. + """ @property @abc.abstractmethod def entity_factory(self) -> entity_factory_.IEntityFactory: - """Entity creator and updater facility.""" + """Entity creator and updater facility. + + Returns + ------- + hikari.api.entity_factory.IEntityFactory + The factory object used to produce and update Python entities. + """ @property @abc.abstractmethod def thread_pool(self) -> typing.Optional[futures.ThreadPoolExecutor]: - """Thread-pool to utilise for file IO within the library, if set.""" + """Thread-pool to utilise for file IO within the library, if set. + + Returns + ------- + concurrent.futures.ThreadPoolExecutor or None + The custom thread-pool being used for blocking IO. If the + default event loop thread-pool is being used, then this will + return `None` instead. + """ @abc.abstractmethod async def close(self) -> None: @@ -78,7 +136,8 @@ class IRESTApp(IApp, abc.ABC): Examples may include web dashboards, or applications where no gateway connection is required. As a result, no event conduit is provided by - these implementations. + these implementations. They do however provide a REST client, and the + general components defined in `IApp` """ __slots__ = () @@ -86,7 +145,15 @@ class IRESTApp(IApp, abc.ABC): @property @abc.abstractmethod def rest(self) -> rest_.REST: - """REST API.""" + """REST API Client. + + Use this to make calls to Discord's REST API over HTTPS. + + Returns + ------- + hikari.net.rest.REST + The REST API client. + """ class IGatewayConsumer(IApp, abc.ABC): @@ -102,7 +169,16 @@ class IGatewayConsumer(IApp, abc.ABC): @property @abc.abstractmethod def event_consumer(self) -> event_consumer_.IEventConsumer: - """Raw event consumer.""" + """Raw event consumer. + + This should be passed raw event payloads from your gateway + websocket implementation. + + Returns + ------- + hikari.api.event_consumer.IEventConsumer + The event consumer implementation in-use. + """ class IGatewayDispatcher(IApp, abc.ABC): @@ -137,7 +213,27 @@ class IGatewayDispatcher(IApp, abc.ABC): @property @abc.abstractmethod def event_dispatcher(self) -> event_dispatcher_.IEventDispatcher: - """Event dispatcher and waiter.""" + """Event dispatcher and subscription manager. + + This stores every event you subscribe to in your application, and + manages invoking those subscribed callbacks when the corresponding + event occurs. + + Event dispatchers also provide a `wait_for` functionality that can be + used to wait for a one-off event that matches a certain criteria. This + is useful if waiting for user feedback for a specific procedure being + performed. + + Users may create their own events and trigger them using this as well, + thus providing a simple in-process event bus that can easily be extended + with a little work to span multiple applications in a distributed + cluster. + + Returns + ------- + hikari.api.event_dispatcher.IEventDispatcher + The event dispatcher in use. + """ # Do not add type hints to me! I delegate to a documented method elsewhere! @functools.wraps(event_dispatcher_.IEventDispatcher.listen) @@ -185,15 +281,35 @@ def gateway_shards(self) -> typing.Mapping[int, gateway.Gateway]: If the shards have not started, and auto=sharding is in-place, then it is acceptable for this to return an empty mapping. + + !!! note + "Non-sharded" bots should expect one value to be in this mapping + under the shard ID `0`. + + >>> bot.gateway_shards[0].heartbeat_latency + 0.145612141 + + Returns + ------- + typing.Mapping[int, hikari.net.gateway.Gateway] + The mapping of shard IDs to gateway connections for the + corresponding shard. These shard IDs are 0-indexed. """ @property @abc.abstractmethod def gateway_shard_count(self) -> int: - """Amount of shards in the entire distributed application. + """Number of shards in the entire distributed application. - If the shards have not started, and auto=sharding is in-place, then it - is acceptable for this to return `0`. + If the shards have not started, and auto-sharding is in-place, then it + is acceptable for this to return `0`. When the application is running, + this should always be a non-zero natural number that is greater than the + maximum ID in `gateway_shards`. + + Returns + ------- + int + The number of shards in the entire application. """ @abc.abstractmethod @@ -208,16 +324,16 @@ async def join(self) -> None: async def update_presence( self, *, - status: presences.PresenceStatus = ..., - activity: typing.Optional[presences.OwnActivity] = ..., - idle_since: typing.Optional[datetime.datetime] = ..., - is_afk: bool = ..., + status: typing.Union[undefined.Undefined, presences.PresenceStatus] = undefined.Undefined(), + activity: typing.Union[undefined.Undefined, presences.OwnActivity, None] = undefined.Undefined(), + idle_since: typing.Union[undefined.Undefined, datetime.datetime, None] = undefined.Undefined(), + is_afk: typing.Union[undefined.Undefined, bool] = undefined.Undefined(), ) -> None: """Update the presence of the user for all shards. This will only update arguments that you explicitly specify a value for. Any arguments that you do not explicitly provide some value for will - not be changed. + not be changed (these values will default to be `undefined`). !!! warning This will only apply to connected shards. @@ -229,16 +345,16 @@ async def update_presence( Parameters ---------- - status : hikari.models.presences.PresenceStatus - If specified, the new status to set. - activity : hikari.models.presences.OwnActivity | None - If specified, the new activity to set. - idle_since : datetime.datetime | None - If specified, the time to show up as being idle since, - or `None` if not applicable. - is_afk : bool - If specified, `True` if the user should be marked as AFK, - or `False` otherwise. + status : hikari.models.presences.PresenceStatus or hikari.utilities.undefined.Undefined + If defined, the new status to set. + activity : hikari.models.presences.OwnActivity or None or hikari.utilities.undefined.Undefined + If defined, the new activity to set. + idle_since : datetime.datetime or None or hikari.utilities.undefined.Undefined + If defined, the time to show up as being idle since, or `None` if + not applicable. If undefined, then it is not changed. + is_afk : bool or hikari.utilities.undefined.Undefined + If defined, `True` if the user should be marked as AFK, + or `False` if not AFK. """ @abc.abstractmethod @@ -255,7 +371,7 @@ def run(self) -> None: class IBot(IRESTApp, IGatewayZookeeper, IGatewayDispatcher, abc.ABC): - """Component for single-process bots. + """Base for bot applications. Bots are components that have access to a REST API, an event dispatcher, and an event consumer. @@ -268,4 +384,15 @@ class IBot(IRESTApp, IGatewayZookeeper, IGatewayDispatcher, abc.ABC): @property @abc.abstractmethod def http_settings(self) -> http_settings_.HTTPSettings: - """The HTTP settings to use.""" + """HTTP settings to use for the shards when they get created. + + !!! info + This is stored only for bots, since shards are generated lazily on + start-up once sharding information has been retrieved from the REST + API. To do this, an event loop has to be running first. + + Returns + ------- + hikari.net.http_settings.HTTPSettings + The HTTP settings to use. + """ From 0db9677f6e8c08a944a97fa4fbaae64e87f3f47a Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Wed, 3 Jun 2020 10:04:29 +0100 Subject: [PATCH 437/922] Fixes #375, implementing max_concurrency for large sharded applications. --- hikari/impl/entity_factory.py | 4 ++ hikari/impl/event_manager.py | 5 +- hikari/impl/gateway_zookeeper.py | 61 +++++++++++++++++++----- hikari/models/gateway.py | 11 +++++ tests/hikari/impl/test_entity_factory.py | 7 +-- 5 files changed, 69 insertions(+), 19 deletions(-) diff --git a/hikari/impl/entity_factory.py b/hikari/impl/entity_factory.py index 384176b198..7e88a38194 100644 --- a/hikari/impl/entity_factory.py +++ b/hikari/impl/entity_factory.py @@ -657,6 +657,7 @@ def deserialize_channel(self, payload: data_binding.JSONObject) -> channels_.Par hikari.models.channels.PartialChannel The parsed partial channel based object. """ + # noinspection PyArgumentList return self._channel_type_mapping[payload["type"]](payload) ########## @@ -955,6 +956,9 @@ def deserialize_gateway_bot(self, payload: data_binding.JSONObject) -> gateway.G session_start_limit.total = int(session_start_limit_payload["total"]) session_start_limit.remaining = int(session_start_limit_payload["remaining"]) session_start_limit.reset_after = datetime.timedelta(milliseconds=session_start_limit_payload["reset_after"]) + # I do not trust that this may never be zero for some unknown reason. If it was 0, it + # would hang the application on start up, so I enforce it is at least 1. + session_start_limit.max_concurrency = max(session_start_limit_payload.get("max_concurrency", 0), 1) gateway_bot.session_start_limit = session_start_limit return gateway_bot diff --git a/hikari/impl/event_manager.py b/hikari/impl/event_manager.py index 767e634385..59afd3bc30 100644 --- a/hikari/impl/event_manager.py +++ b/hikari/impl/event_manager.py @@ -23,12 +23,9 @@ __all__ = ["EventManagerImpl"] from hikari.impl import event_manager_core -from hikari.net import gateway -from hikari.utilities import data_binding class EventManagerImpl(event_manager_core.EventManagerCore): """Provides event handling logic for Discord events.""" - async def _on_message_create(self, shard: gateway.Gateway, payload: data_binding.JSONObject) -> None: - print(shard, payload) + pass diff --git a/hikari/impl/gateway_zookeeper.py b/hikari/impl/gateway_zookeeper.py index e8ea86520b..2c860cdd10 100644 --- a/hikari/impl/gateway_zookeeper.py +++ b/hikari/impl/gateway_zookeeper.py @@ -73,6 +73,7 @@ def __init__( self._initial_status = initial_status self._intents = intents self._large_threshold = large_threshold + self._max_concurrency = 1 self._request_close_event = asyncio.Event() self._shard_count = shard_count self._shard_ids = shard_ids @@ -102,7 +103,7 @@ async def start(self) -> None: start_time = time.perf_counter() try: - for i, shard_id in enumerate(self._shards): + for i, shard_ids in enumerate(self._max_concurrency_chunker()): if self._request_close_event.is_set(): break @@ -116,8 +117,18 @@ async def start(self) -> None: if completed: raise completed.pop().exception() - shard_obj = self._shards[shard_id] - self._tasks[shard_id] = await shard_obj.start() + window = {} + for shard_id in shard_ids: + shard_obj = self._shards[shard_id] + window[shard_id] = asyncio.create_task(shard_obj.start(), name=f"start gateway shard {shard_id}") + + # Wait for the group to start. + await asyncio.gather(*window.values()) + + # Store the keep-alive tasks and continue. + for shard_id, start_task in window.items(): + self._tasks[shard_id] = start_task.result() + finally: if len(self._tasks) != len(self._shards): self.logger.warning( @@ -125,16 +136,20 @@ async def start(self) -> None: len(self._tasks), ) await self._abort() - return - finish_time = time.perf_counter() - self._gather_task = asyncio.create_task( - self._gather(), name=f"shard zookeeper for {len(self._shards)} shard(s)" - ) - self.logger.info("started %s shard(s) in approx %.2fs", len(self._shards), finish_time - start_time) + # We know an error occurred if this condition is met, so re-raise it. + raise - if hasattr(self, "event_dispatcher") and isinstance(self.event_dispatcher, event_dispatcher.IEventDispatcher): - await self.event_dispatcher.dispatch(other.StartedEvent()) + finish_time = time.perf_counter() + self._gather_task = asyncio.create_task( + self._gather(), name=f"shard zookeeper for {len(self._shards)} shard(s)" + ) + self.logger.info("started %s shard(s) in approx %.2fs", len(self._shards), finish_time - start_time) + + if hasattr(self, "event_dispatcher") and isinstance( + self.event_dispatcher, event_dispatcher.IEventDispatcher + ): + await self.event_dispatcher.dispatch(other.StartedEvent()) async def join(self) -> None: if self._gather_task is not None: @@ -196,6 +211,7 @@ def sigterm_handler(*_): except KeyboardInterrupt: self.logger.info("received signal to shut down client") raise + finally: loop.run_until_complete(self.close()) with contextlib.suppress(NotImplementedError): @@ -241,9 +257,14 @@ async def _init(self): self._shard_count = self._shard_count if self._shard_count else gw_recs.shard_count self._shard_ids = self._shard_ids if self._shard_ids else range(self._shard_count) + self._max_concurrency = gw_recs.session_start_limit.max_concurrency url = gw_recs.url - self.logger.info("will connect shards to %s", url) + self.logger.info( + "will connect shards to %s. max_concurrency while connecting is %s, contact Discord to get this increased", + url, + self._max_concurrency, + ) shard_clients: typing.Dict[int, gateway.Gateway] = {} for shard_id in self._shard_ids: @@ -268,6 +289,22 @@ async def _init(self): self._shards = shard_clients # pylint: disable=attribute-defined-outside-init + def _max_concurrency_chunker(self) -> typing.Iterator[typing.Iterator[int]]: + """Yield generators of shard IDs. + + Each yielded generator will yield every shard ID that can be started + at the same time. + + You should then wait 5 seconds between each window. + """ + n = 0 + while n < self._shard_count: + next_window = [i for i in range(n, n + self._max_concurrency) if i in self._shard_ids] + # Don't yield anything if no IDs are in the given window. + if next_window: + yield iter(next_window) + n += self._max_concurrency + @abc.abstractmethod async def _fetch_gateway_recommendations(self) -> gateway_models.GatewayBot: ... diff --git a/hikari/models/gateway.py b/hikari/models/gateway.py index 461d645e5d..1c6e0fc652 100644 --- a/hikari/models/gateway.py +++ b/hikari/models/gateway.py @@ -46,6 +46,17 @@ class SessionStartLimit: After it resets it will be set to `SessionStartLimit.total`. """ + # This is not documented at the time of writing, but is a confirmed API + # feature, so I have included it for completeness. + max_concurrency: int = attr.ib(repr=True) + """Maximum connection concurrency. + + This defines how many shards can be started at once within a 5 second + window. For most bots, this will always be `1`, but for very large bots, + this may be increased to reduce startup times. Contact Discord for + more information. + """ + @attr.s(eq=True, hash=False, init=False, kw_only=True, slots=True) class GatewayBot: diff --git a/tests/hikari/impl/test_entity_factory.py b/tests/hikari/impl/test_entity_factory.py index cc468c93a6..435710eb57 100644 --- a/tests/hikari/impl/test_entity_factory.py +++ b/tests/hikari/impl/test_entity_factory.py @@ -1155,19 +1155,20 @@ def gateway_bot_payload(self): return { "url": "wss://gateway.discord.gg", "shards": 1, - "session_start_limit": {"total": 1000, "remaining": 991, "reset_after": 14170186}, + "session_start_limit": {"total": 1000, "remaining": 991, "reset_after": 14170186, "max_concurrency": 5}, } def test_deserialize_gateway_bot(self, entity_factory_impl, gateway_bot_payload): gateway_bot = entity_factory_impl.deserialize_gateway_bot(gateway_bot_payload) + assert isinstance(gateway_bot, gateway.GatewayBot) assert gateway_bot.url == "wss://gateway.discord.gg" assert gateway_bot.shard_count == 1 - assert isinstance(gateway_bot, gateway.GatewayBot) # SessionStartLimit + assert isinstance(gateway_bot.session_start_limit, gateway.SessionStartLimit) + assert gateway_bot.session_start_limit.max_concurrency == 5 assert gateway_bot.session_start_limit.total == 1000 assert gateway_bot.session_start_limit.remaining == 991 assert gateway_bot.session_start_limit.reset_after == datetime.timedelta(milliseconds=14170186) - assert isinstance(gateway_bot.session_start_limit, gateway.SessionStartLimit) ########## # GUILDS # From 04f65db1be026d8a3532415fde48a1e908dbd932 Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Wed, 3 Jun 2020 10:07:44 +0100 Subject: [PATCH 438/922] Addressing bug for documentation not having any gutter on ultra-thin devices. --- docs/documentation.mako | 156 ++++++++++++++++++++-------------------- 1 file changed, 77 insertions(+), 79 deletions(-) diff --git a/docs/documentation.mako b/docs/documentation.mako index 51244b401b..3f2b8bf1a2 100644 --- a/docs/documentation.mako +++ b/docs/documentation.mako @@ -592,84 +592,82 @@ % endif -
-
-

Notation used in this documentation

-
-
${QUAL_DEF}
-
Regular function.
- -
${QUAL_ASYNC_DEF}
-
Coroutine function that should be awaited.
- -
${QUAL_CLASS}
-
Regular class that provides a certain functionality.
- -
${QUAL_ABC}
-
- Abstract base class. These are partially implemented classes that require - additional implementation details to be fully usable. Generally these are - used to represet a subset of behaviour common between different - implementations. -
- -
${QUAL_DATACLASS}
-
- Data class. This is a class designed to model and store information - rather than provide a certain behaviour or functionality. -
- -
${QUAL_ENUM}
-
Enumerated type.
- -
${QUAL_ENUM_FLAG}
-
Enumerated flag type. Supports being combined.
- -
${QUAL_METACLASS}
-
- Metaclass. This is a base type of a class, used to control how implementing - classes are created, exist, operate, and get destroyed. -
- -
${QUAL_MODULE}
-
Python module that you can import directly
- -
${QUAL_PACKAGE}
-
Python package that can be imported and can contain sub-modules.
- -
${QUAL_NAMESPACE}
-
Python namespace package that can contain sub-modules, but is not directly importable.
- -
${QUAL_TYPEHINT}
-
- An object or attribute used to denote a certain type or combination of types. - These usually provide no functionality and only exist for documentation purposes - and for static type-checkers. -
- -
${QUAL_REF}
-
- Used to flag that an object is defined in a different file, and is just - referred to at the current location. -
- -
${QUAL_VAR}
-
- Variable or attribute. -
- -
${QUAL_CONST}
-
- Value that should not be changed manually. -
- -
${QUAL_EXTERNAL}
-
- Attribute or object that is not covered by this documentation. This usually - denotes types from other dependencies, or from the standard library. -
-
-
+
+

Notation used in this documentation

+
+
${QUAL_DEF}
+
Regular function.
+ +
${QUAL_ASYNC_DEF}
+
Coroutine function that should be awaited.
+ +
${QUAL_CLASS}
+
Regular class that provides a certain functionality.
+ +
${QUAL_ABC}
+
+ Abstract base class. These are partially implemented classes that require + additional implementation details to be fully usable. Generally these are + used to represet a subset of behaviour common between different + implementations. +
+ +
${QUAL_DATACLASS}
+
+ Data class. This is a class designed to model and store information + rather than provide a certain behaviour or functionality. +
+ +
${QUAL_ENUM}
+
Enumerated type.
+ +
${QUAL_ENUM_FLAG}
+
Enumerated flag type. Supports being combined.
+ +
${QUAL_METACLASS}
+
+ Metaclass. This is a base type of a class, used to control how implementing + classes are created, exist, operate, and get destroyed. +
+ +
${QUAL_MODULE}
+
Python module that you can import directly
+ +
${QUAL_PACKAGE}
+
Python package that can be imported and can contain sub-modules.
+ +
${QUAL_NAMESPACE}
+
Python namespace package that can contain sub-modules, but is not directly importable.
+ +
${QUAL_TYPEHINT}
+
+ An object or attribute used to denote a certain type or combination of types. + These usually provide no functionality and only exist for documentation purposes + and for static type-checkers. +
+ +
${QUAL_REF}
+
+ Used to flag that an object is defined in a different file, and is just + referred to at the current location. +
+ +
${QUAL_VAR}
+
+ Variable or attribute. +
+ +
${QUAL_CONST}
+
+ Value that should not be changed manually. +
+ +
${QUAL_EXTERNAL}
+
+ Attribute or object that is not covered by this documentation. This usually + denotes types from other dependencies, or from the standard library. +
+
- \ No newline at end of file + From 223646458c4ba703ae0bdf5827f9d0a24d8517cf Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Wed, 3 Jun 2020 17:59:56 +0100 Subject: [PATCH 439/922] More fixing of various bits of documentation. --- hikari/api/app.py | 51 ++- hikari/api/cache.py | 286 +------------ hikari/api/component.py | 13 +- hikari/api/entity_factory.py | 267 ++++++------ hikari/api/event_consumer.py | 8 +- hikari/api/event_dispatcher.py | 126 +++++- hikari/events/other.py | 6 +- hikari/impl/cache.py | 4 +- hikari/impl/entity_factory.py | 510 +---------------------- hikari/impl/event_manager.py | 2 - hikari/models/users.py | 8 +- hikari/net/rest.py | 4 +- tests/hikari/impl/test_entity_factory.py | 4 +- 13 files changed, 322 insertions(+), 967 deletions(-) diff --git a/hikari/api/app.py b/hikari/api/app.py index 745ebf7534..2177507f17 100644 --- a/hikari/api/app.py +++ b/hikari/api/app.py @@ -59,20 +59,22 @@ class IApp(abc.ABC): A quick and dirty theoretical concrete implementation may look like the following. - class MyApp(IApp): - def __init__(self): - self._logger = logging.getLogger(__name__) - self._cache = MyCacheImplementation(self) - self._entity_factory = MyEntityFactoryImplementation(self) - self._thread_pool = concurrent.futures.ThreadPoolExecutor() - - logger = property(lambda self: self._logger) - cache = property(lambda self: self._cache) - entity_factory = property(lambda self: self._entity_factory) - thread_pool = property(lambda self: self._thread_pool) - - async def close(self): - self._thread_pool.shutdown() + ```py + class MyApp(IApp): + def __init__(self): + self._logger = logging.getLogger(__name__) + self._cache = MyCacheImplementation(self) + self._entity_factory = MyEntityFactoryImplementation(self) + self._thread_pool = concurrent.futures.ThreadPoolExecutor() + + logger = property(lambda self: self._logger) + cache = property(lambda self: self._cache) + entity_factory = property(lambda self: self._entity_factory) + thread_pool = property(lambda self: self._thread_pool) + + async def close(self): + self._thread_pool.shutdown() + ``` If you are in any doubt, check out the `hikari.RESTApp` and `hikari.Bot` implementations to see how they are pieced together! @@ -196,15 +198,18 @@ class IGatewayDispatcher(IApp, abc.ABC): to the event dispatcher. This provides a more intuitive syntax for applications. - # We can now do this... + ```py + + # We can now do this... - >>> @bot.listen() - >>> async def on_message(event: hikari.MessageCreateEvent) -> None: ... + >>> @bot.listen() + >>> async def on_message(event: hikari.MessageCreateEvent) -> None: ... - # ...instead of having to do this... + # ...instead of having to do this... - >>> @bot.listen(hikari.MessageCreateEvent) - >>> async def on_message(event: hikari.MessageCreateEvent) -> None: ... + >>> @bot.listen(hikari.MessageCreateEvent) + >>> async def on_message(event: hikari.MessageCreateEvent) -> None: ... + ``` """ @@ -286,8 +291,10 @@ def gateway_shards(self) -> typing.Mapping[int, gateway.Gateway]: "Non-sharded" bots should expect one value to be in this mapping under the shard ID `0`. - >>> bot.gateway_shards[0].heartbeat_latency - 0.145612141 + ```py + >>> bot.gateway_shards[0].heartbeat_latency + 0.145612141 + ``` Returns ------- diff --git a/hikari/api/cache.py b/hikari/api/cache.py index 1adbc97912..4bc261c430 100644 --- a/hikari/api/cache.py +++ b/hikari/api/cache.py @@ -26,29 +26,15 @@ from hikari.api import component -if typing.TYPE_CHECKING: - from hikari.models import applications - from hikari.models import audit_logs - from hikari.models import channels - from hikari.models import embeds - from hikari.models import emojis - from hikari.models import gateway - from hikari.models import guilds - from hikari.models import invites - from hikari.models import messages - from hikari.models import presences - from hikari.models import users - from hikari.models import voices - - from hikari.utilities import data_binding - class ICache(component.IComponent, abc.ABC): - """Component that implements entity caching facilities. + """Interface describing the operations a cache component should provide This will be used by the gateway and REST API to cache specific types of objects that the application should attempt to remember for later, depending - on how this is implemented. + on how this is implemented. The requirement for this stems from the + assumption by Discord that bot applications will maintain some form of + "memory" of the events that occur. The implementation may choose to use a simple in-memory collection of objects, or may decide to use a distributed system such as a Redis cache @@ -56,267 +42,3 @@ class ICache(component.IComponent, abc.ABC): """ __slots__ = () - - ################ - # APPLICATIONS # - ################ - @abc.abstractmethod - async def create_application(self, payload: data_binding.JSONObject) -> applications.Application: - ... - - @abc.abstractmethod - async def create_own_guild(self, payload: data_binding.JSONObject) -> applications.OwnGuild: - ... - - @abc.abstractmethod - async def create_own_connection(self, payload: data_binding.JSONObject) -> applications.OwnConnection: - ... - - ############## - # AUDIT LOGS # - ############## - - @abc.abstractmethod - async def create_audit_log_change(self, payload: data_binding.JSONObject) -> audit_logs.AuditLogChange: - ... - - @abc.abstractmethod - async def create_audit_log_entry_info(self, payload: data_binding.JSONObject) -> audit_logs.BaseAuditLogEntryInfo: - ... - - @abc.abstractmethod - async def create_audit_log_entry(self, payload: data_binding.JSONObject) -> audit_logs.AuditLogEntry: - ... - - @abc.abstractmethod - async def create_audit_log(self, payload: data_binding.JSONObject) -> audit_logs.AuditLog: - ... - - ############ - # CHANNELS # - ############ - - @abc.abstractmethod - async def create_channel( - self, payload: data_binding.JSONObject, can_cache: bool = False - ) -> channels.PartialChannel: - ... - - @abc.abstractmethod - async def update_channel( - self, channel: channels.PartialChannel, payload: data_binding.JSONObject, - ) -> channels.PartialChannel: - ... - - @abc.abstractmethod - async def get_channel(self, channel_id: int) -> typing.Optional[channels.PartialChannel]: - ... - - @abc.abstractmethod - async def delete_channel(self, channel_id: int) -> typing.Optional[channels.PartialChannel]: - ... - - ########## - # EMBEDS # - ########## - - @abc.abstractmethod - async def create_embed(self, payload: data_binding.JSONObject) -> embeds.Embed: - ... - - ########## - # EMOJIS # - ########## - - @abc.abstractmethod - async def create_emoji(self, payload: data_binding.JSONObject, can_cache: bool = False) -> emojis.Emoji: - ... - - @abc.abstractmethod - async def update_emoji(self, payload: data_binding.JSONObject) -> emojis.Emoji: - ... - - @abc.abstractmethod - async def get_emoji(self, emoji_id: int) -> typing.Optional[emojis.KnownCustomEmoji]: - ... - - @abc.abstractmethod - async def delete_emoji(self, emoji_id: int) -> typing.Optional[emojis.KnownCustomEmoji]: - ... - - ########### - # GATEWAY # - ########### - - @abc.abstractmethod - async def create_gateway_bot(self, payload: data_binding.JSONObject) -> gateway.GatewayBot: - ... - - ########## - # GUILDS # - ########## - - @abc.abstractmethod - async def create_member(self, payload: data_binding.JSONObject, can_cache: bool = False) -> guilds.Member: - # TODO: revisit for the voodoo to make a member into a special user. - ... - - @abc.abstractmethod - async def update_member(self, member: guilds.Member, payload: data_binding.JSONObject) -> guilds.Member: - ... - - @abc.abstractmethod - async def get_member(self, guild_id: int, user_id: int) -> typing.Optional[guilds.Member]: - ... - - @abc.abstractmethod - async def delete_member(self, guild_id: int, user_id: int) -> typing.Optional[guilds.Member]: - ... - - @abc.abstractmethod - async def create_role(self, payload: data_binding.JSONObject, can_cache: bool = False) -> guilds.PartialRole: - ... - - @abc.abstractmethod - async def update_role(self, role: guilds.PartialRole, payload: data_binding.JSONObject) -> guilds.PartialRole: - ... - - @abc.abstractmethod - async def get_role(self, guild_id: int, role_id: int) -> typing.Optional[guilds.PartialRole]: - ... - - @abc.abstractmethod - async def delete_role(self, guild_id: int, role_id: int) -> typing.Optional[guilds.PartialRole]: - ... - - @abc.abstractmethod - async def create_presence( - self, payload: data_binding.JSONObject, can_cache: bool = False - ) -> presences.MemberPresence: - ... - - @abc.abstractmethod - async def update_presence( - self, role: presences.MemberPresence, payload: data_binding.JSONObject - ) -> presences.MemberPresence: - ... - - @abc.abstractmethod - async def get_presence(self, guild_id: int, user_id: int) -> typing.Optional[presences.MemberPresence]: - ... - - @abc.abstractmethod - async def delete_presence(self, guild_id: int, user_id: int) -> typing.Optional[presences.MemberPresence]: - ... - - @abc.abstractmethod - async def create_guild_ban(self, payload: data_binding.JSONObject) -> guilds.GuildMemberBan: - ... - - @abc.abstractmethod - async def create_integration(self, payload: data_binding.JSONObject) -> guilds.PartialIntegration: - ... - - @abc.abstractmethod - async def create_guild(self, payload: data_binding.JSONObject, can_cache: bool = False) -> guilds.PartialGuild: - ... - - @abc.abstractmethod - async def update_guild(self, guild: guilds.PartialGuild, payload: data_binding.JSONObject) -> guilds.PartialGuild: - ... - - @abc.abstractmethod - async def get_guild(self, guild_id: int) -> typing.Optional[guilds.PartialGuild]: - ... - - @abc.abstractmethod - async def delete_guild(self, guild_id: int) -> typing.Optional[guilds.PartialGuild]: - ... - - @abc.abstractmethod - async def create_guild_preview(self, payload: data_binding.JSONObject) -> guilds.GuildPreview: - ... - - ########### - # INVITES # - ########### - @abc.abstractmethod - async def create_invite(self, payload: data_binding.JSONObject) -> invites.Invite: - ... - - ############ - # MESSAGES # - ############ - @abc.abstractmethod - async def create_reaction(self, payload: data_binding.JSONObject) -> messages.Reaction: - ... - - @abc.abstractmethod - async def create_message(self, payload: data_binding.JSONObject, can_cache: bool = False) -> messages.Message: - ... - - @abc.abstractmethod - async def update_message(self, message: messages.Message, payload: data_binding.JSONObject) -> messages.Message: - ... - - @abc.abstractmethod - async def get_message(self, channel_id: int, message_id: int) -> typing.Optional[messages.Message]: - ... - - @abc.abstractmethod - async def delete_message(self, channel_id: int, message_id: int) -> typing.Optional[messages.Message]: - ... - - ######### - # USERS # - ######### - @abc.abstractmethod - async def create_user(self, payload: data_binding.JSONObject, can_cache: bool = False) -> users.User: - ... - - @abc.abstractmethod - async def update_user(self, user: users.User, payload: data_binding.JSONObject) -> users.User: - ... - - @abc.abstractmethod - async def get_user(self, user_id: int) -> typing.Optional[users.User]: - ... - - @abc.abstractmethod - async def delete_user(self, user_id: int) -> typing.Optional[users.User]: - ... - - @abc.abstractmethod - async def create_my_user(self, payload: data_binding.JSONObject, can_cache: bool = False) -> users.MyUser: - ... - - @abc.abstractmethod - async def update_my_user(self, my_user: users.MyUser, payload: data_binding.JSONObject) -> users.MyUser: - ... - - @abc.abstractmethod - async def get_my_user(self) -> typing.Optional[users.User]: - ... - - ########## - # VOICES # - ########## - @abc.abstractmethod - async def create_voice_state(self, payload: data_binding.JSONObject, can_cache: bool = False) -> voices.VoiceState: - ... - - @abc.abstractmethod - async def update_voice_state(self, payload: data_binding.JSONObject) -> voices.VoiceState: - ... - - @abc.abstractmethod - async def get_voice_state(self, guild_id: int, channel_id: int) -> typing.Optional[voices.VoiceState]: - ... - - @abc.abstractmethod - async def delete_voice_state(self, guild_id: int, channel_id: int) -> typing.Optional[voices.VoiceState]: - ... - - @abc.abstractmethod - async def create_voice_region(self, payload: data_binding.JSONObject) -> voices.VoiceRegion: - ... diff --git a/hikari/api/component.py b/hikari/api/component.py index d1513d7171..6fac64c7e0 100644 --- a/hikari/api/component.py +++ b/hikari/api/component.py @@ -28,11 +28,20 @@ class IComponent(abc.ABC): - """A component that makes up part of the application.""" + """A component that makes up part of the application. + Objects that derive from this should usually be attributes on the + `hikari.api.app.IApp` object. + """ __slots__ = () @property @abc.abstractmethod def app(self) -> app.IApp: - """Application that owns this component.""" + """Return the Application that owns this component. + + Returns + ------- + hikari.api.app.IApp + The application implementation that owns this component. + """ diff --git a/hikari/api/entity_factory.py b/hikari/api/entity_factory.py index 0b90af3d6c..b09e6c4d4b 100644 --- a/hikari/api/entity_factory.py +++ b/hikari/api/entity_factory.py @@ -25,6 +25,7 @@ import typing from hikari.api import component +from hikari.utilities import undefined if typing.TYPE_CHECKING: from hikari.models import applications @@ -45,7 +46,7 @@ class IEntityFactory(component.IComponent, abc.ABC): - """Component that will serialize and deserialize JSON payloads.""" + """Interface for components that serialize and deserialize JSON payloads.""" __slots__ = () @@ -59,13 +60,13 @@ def deserialize_own_connection(self, payload: data_binding.JSONObject) -> applic Parameters ---------- - payload : Mapping[str, Any] - The dict payload to parse. + payload : hikari.utilities.data_binding.JSONObject + The JSON payload to deserialize. Returns ------- hikari.models.applications.OwnConnection - The parsed own connection object. + The deserialized own connection object. """ @abc.abstractmethod @@ -74,13 +75,13 @@ def deserialize_own_guild(self, payload: data_binding.JSONObject) -> application Parameters ---------- - payload : Mapping[str, Any] - The dict payload to parse. + payload : hikari.utilities.data_binding.JSONObject + The JSON payload to deserialize. Returns ------- hikari.models.applications.OwnGuild - The parsed own guild object. + The deserialized own guild object. """ @abc.abstractmethod @@ -89,13 +90,13 @@ def deserialize_application(self, payload: data_binding.JSONObject) -> applicati Parameters ---------- - payload : Mapping[str, Any] - The dict payload to parse. + payload : hikari.utilities.data_binding.JSONObject + The JSON payload to deserialize. Returns ------- hikari.models.applications.Application - The parsed application object. + The deserialized application object. """ ############## @@ -108,13 +109,13 @@ def deserialize_audit_log(self, payload: data_binding.JSONObject) -> audit_logs. Parameters ---------- - payload : Mapping[str, Any] - The dict payload to parse. + payload : hikari.utilities.data_binding.JSONObject + The JSON payload to deserialize. Returns ------- hikari.models.audit_logs.AuditLog - The parsed audit log object. + The deserialized audit log object. """ ############ @@ -127,13 +128,13 @@ def deserialize_permission_overwrite(self, payload: data_binding.JSONObject) -> Parameters ---------- - payload : Mapping[str, Any] - The dict payload to parse. + payload : hikari.utilities.data_binding.JSONObject + The JSON payload to deserialize. Returns ------- hikari.models.channels.PermissionOverwrote - The parsed permission overwrite object. + The deserialized permission overwrite object. """ @abc.abstractmethod @@ -147,8 +148,8 @@ def serialize_permission_overwrite(self, overwrite: channels.PermissionOverwrite Returns ------- - Dict[Hashable, Any] - The dict representation of the permission overwrite object provided. + hikari.utilities.data_binding.JSONObject + The serialized representation. """ @abc.abstractmethod @@ -157,13 +158,13 @@ def deserialize_partial_channel(self, payload: data_binding.JSONObject) -> chann Parameters ---------- - payload : Mapping[str, Any] - The dict payload to parse. + payload : hikari.utilities.data_binding.JSONObject + The JSON payload to deserialize. Returns ------- hikari.models.channels.PartialChannel - The parsed partial channel object. + The deserialized partial channel object. """ @abc.abstractmethod @@ -172,13 +173,13 @@ def deserialize_dm_channel(self, payload: data_binding.JSONObject) -> channels.D Parameters ---------- - payload : Mapping[str, Any] - The dict payload to parse. + payload : hikari.utilities.data_binding.JSONObject + The JSON payload to deserialize. Returns ------- hikari.models.channels.DMChannel - The parsed DM channel object. + The deserialized DM channel object. """ @abc.abstractmethod @@ -187,13 +188,13 @@ def deserialize_group_dm_channel(self, payload: data_binding.JSONObject) -> chan Parameters ---------- - payload : Mapping[str, Any] - The dict payload to parse. + payload : hikari.utilities.data_binding.JSONObject + The JSON payload to deserialize. Returns ------- hikari.models.channels.GroupDMChannel - The parsed group DM channel object. + The deserialized group DM channel object. """ @abc.abstractmethod @@ -202,13 +203,13 @@ def deserialize_guild_category(self, payload: data_binding.JSONObject) -> channe Parameters ---------- - payload : Mapping[str, Any] - The dict payload to parse. + payload : hikari.utilities.data_binding.JSONObject + The JSON payload to deserialize. Returns ------- hikari.models.channels.GuildCategory - The parsed partial channel object. + The deserialized partial channel object. """ @abc.abstractmethod @@ -217,13 +218,13 @@ def deserialize_guild_text_channel(self, payload: data_binding.JSONObject) -> ch Parameters ---------- - payload : Mapping[str, Any] - The dict payload to parse. + payload : hikari.utilities.data_binding.JSONObject + The JSON payload to deserialize. Returns ------- hikari.models.channels.GuildTextChannel - The parsed guild text channel object. + The deserialized guild text channel object. """ @abc.abstractmethod @@ -232,13 +233,13 @@ def deserialize_guild_news_channel(self, payload: data_binding.JSONObject) -> ch Parameters ---------- - payload : Mapping[str, Any] - The dict payload to parse. + payload : hikari.utilities.data_binding.JSONObject + The JSON payload to deserialize. Returns ------- hikari.models.channels.GuildNewsChannel - The parsed guild news channel object. + The deserialized guild news channel object. """ @abc.abstractmethod @@ -247,13 +248,13 @@ def deserialize_guild_store_channel(self, payload: data_binding.JSONObject) -> c Parameters ---------- - payload : Mapping[str, Any] - The dict payload to parse. + payload : hikari.utilities.data_binding.JSONObject + The JSON payload to deserialize. Returns ------- hikari.models.channels.GuildStoreChannel - The parsed guild store channel object. + The deserialized guild store channel object. """ @abc.abstractmethod @@ -262,13 +263,13 @@ def deserialize_guild_voice_channel(self, payload: data_binding.JSONObject) -> c Parameters ---------- - payload : Mapping[str, Any] - The dict payload to parse. + payload : hikari.utilities.data_binding.JSONObject + The JSON payload to deserialize. Returns ------- hikari.models.channels.GuildVoiceChannel - The parsed guild voice channel object. + The deserialized guild voice channel object. """ @abc.abstractmethod @@ -277,13 +278,13 @@ def deserialize_channel(self, payload: data_binding.JSONObject) -> channels.Part Parameters ---------- - payload : Mapping[str, Any] - The dict payload to parse. + payload : hikari.utilities.data_binding.JSONObject + The JSON payload to deserialize. Returns ------- hikari.models.channels.PartialChannel - The parsed partial channel based object. + The deserialized partial channel-derived object. """ ########## @@ -296,13 +297,13 @@ def deserialize_embed(self, payload: data_binding.JSONObject) -> embeds.Embed: Parameters ---------- - payload : Mapping[str, Any] - The dict payload to parse. + payload : hikari.utilities.data_binding.JSONObject + The JSON payload to deserialize. Returns ------- hikari.models.embeds.Embed - The parsed embed object. + The deserialized embed object. """ @abc.abstractmethod @@ -316,8 +317,8 @@ def serialize_embed(self, embed: embeds.Embed) -> data_binding.JSONObject: Returns ------- - Dict[Hashable, Any] - The dict representation of the provided embed object. + hikari.utilities.data_binding.JSONObject + The serialized object representation. """ ########## @@ -330,13 +331,13 @@ def deserialize_unicode_emoji(self, payload: data_binding.JSONObject) -> emojis. Parameters ---------- - payload : Mapping[str, Any] - The dict payload to parse. + payload : hikari.utilities.data_binding.JSONObject + The JSON payload to deserialize. Returns ------- hikari.models.emojis.UnicodeEmoji - The parsed unicode emoji object. + The deserialized unicode emoji object. """ @abc.abstractmethod @@ -345,13 +346,13 @@ def deserialize_custom_emoji(self, payload: data_binding.JSONObject) -> emojis.C Parameters ---------- - payload : Mapping[str, Any] - The dict payload to parse. + payload : hikari.utilities.data_binding.JSONObject + The JSON payload to deserialize. Returns ------- hikari.models.emojis.CustomEmoji - The parsed custom emoji object. + The deserialized custom emoji object. """ @abc.abstractmethod @@ -360,13 +361,13 @@ def deserialize_known_custom_emoji(self, payload: data_binding.JSONObject) -> em Parameters ---------- - payload : Mapping[str, Any] - The dict payload to parse. + payload : hikari.utilities.data_binding.JSONObject + The JSON payload to deserialize. Returns ------- hikari.models.emojis.KnownCustomEmoji - The parsed known custom emoji object. + The deserialized known custom emoji object. """ @abc.abstractmethod @@ -377,13 +378,13 @@ def deserialize_emoji( Parameters ---------- - payload : Mapping[str, Any] - The dict payload to parse. + payload : hikari.utilities.data_binding.JSONObject + The JSON payload to deserialize. Returns ------- hikari.models.emojis.UnicodeEmoji | hikari.models.emoji.CustomEmoji - The parsed custom or unicode emoji object. + The deserialized custom or unicode emoji object. """ ########### @@ -396,13 +397,13 @@ def deserialize_gateway_bot(self, payload: data_binding.JSONObject) -> gateway.G Parameters ---------- - payload : Mapping[str, Any] - The dict payload to parse. + payload : hikari.utilities.data_binding.JSONObject + The JSON payload to deserialize. Returns ------- hikari.models.gateway.GatewayBot - The parsed gateway bot object. + The deserialized gateway bot object. """ ########## @@ -411,38 +412,40 @@ def deserialize_gateway_bot(self, payload: data_binding.JSONObject) -> gateway.G @abc.abstractmethod def deserialize_guild_widget(self, payload: data_binding.JSONObject) -> guilds.GuildWidget: - """Parse a raw payload from Discord into a guild embed object. + """Parse a raw payload from Discord into a guild widget object. Parameters ---------- - payload : Mapping[str, Any] - The dict payload to parse. + payload : hikari.utilities.data_binding.JSONObject + The JSON payload to deserialize. Returns ------- - hikari.models.guilds.GuildEmbed - The parsed guild embed object. + hikari.models.guilds.GuildWidget + The deserialized guild widget object. """ @abc.abstractmethod def deserialize_member( - self, payload: data_binding.JSONObject, *, user: typing.Optional[users.User] = None + self, + payload: data_binding.JSONObject, + *, + user: typing.Union[undefined.Undefined, users.User] = undefined.Undefined() ) -> guilds.Member: """Parse a raw payload from Discord into a member object. Parameters ---------- - payload : Mapping[str, Any] - The dict payload to parse. - *, - user : hikari.models.users.User? - The user to attach to this member, should be passed in situations - where "user" is not included in the payload. + payload : hikari.utilities.data_binding.JSONObject + The JSON payload to deserialize. + user : hikari.models.users.User or hikari.utilities.undefined.Undefined + The user to attach to this member, should only be passed in + situations where "user" is not included in the payload. Returns ------- hikari.models.guilds.Member - The parsed member object. + The deserialized member object. """ @abc.abstractmethod @@ -451,13 +454,13 @@ def deserialize_role(self, payload: data_binding.JSONObject) -> guilds.Role: Parameters ---------- - payload : Mapping[str, Any] - The dict payload to parse. + payload : hikari.utilities.data_binding.JSONObject + The JSON payload to deserialize. Returns ------- hikari.models.guilds.GuildRole - The parsed role object. + The deserialized role object. """ @abc.abstractmethod @@ -466,13 +469,13 @@ def deserialize_partial_integration(self, payload: data_binding.JSONObject) -> g Parameters ---------- - payload : Mapping[str, Any] - The dict payload to parse. + payload : hikari.utilities.data_binding.JSONObject + The JSON payload to deserialize. Returns ------- hikari.models.guilds.PartialIntegration - The parsed partial integration object. + The deserialized partial integration object. """ @abc.abstractmethod @@ -481,13 +484,13 @@ def deserialize_integration(self, payload: data_binding.JSONObject) -> guilds.In Parameters ---------- - payload : Mapping[str, Any] - The dict payload to parse. + payload : hikari.utilities.data_binding.JSONObject + The JSON payload to deserialize. Returns ------- hikari.models.guilds.Integration - The parsed integration object. + The deserialized integration object. """ @abc.abstractmethod @@ -496,13 +499,13 @@ def deserialize_guild_member_ban(self, payload: data_binding.JSONObject) -> guil Parameters ---------- - payload : Mapping[str, Any] - The dict payload to parse. + payload : hikari.utilities.data_binding.JSONObject + The JSON payload to deserialize. Returns ------- hikari.models.GuildMemberBan - The parsed guild member ban object. + The deserialized guild member ban object. """ @abc.abstractmethod @@ -511,13 +514,13 @@ def deserialize_unavailable_guild(self, payload: data_binding.JSONObject) -> gui Parameters ---------- - payload : Mapping[str, Any] - The dict payload to parse. + payload : hikari.utilities.data_binding.JSONObject + The JSON payload to deserialize. Returns ------- hikari.models.guilds.UnavailableGuild - The parsed unavailable guild object. + The deserialized unavailable guild object. """ @abc.abstractmethod @@ -526,13 +529,13 @@ def deserialize_guild_preview(self, payload: data_binding.JSONObject) -> guilds. Parameters ---------- - payload : Mapping[str, Any] - The dict payload to parse. + payload : hikari.utilities.data_binding.JSONObject + The JSON payload to deserialize. Returns ------- hikari.models.guilds.GuildPreview - The parsed guild preview object. + The deserialized guild preview object. """ @abc.abstractmethod @@ -541,13 +544,13 @@ def deserialize_guild(self, payload: data_binding.JSONObject) -> guilds.Guild: Parameters ---------- - payload : Mapping[str, Any] - The dict payload to parse. + payload : hikari.utilities.data_binding.JSONObject + The JSON payload to deserialize. Returns ------- hikari.models.guilds.Guild - The parsed guild object. + The deserialized guild object. """ ########### @@ -560,13 +563,13 @@ def deserialize_vanity_url(self, payload: data_binding.JSONObject) -> invites.Va Parameters ---------- - payload : Mapping[str, Any] - The dict payload to parse. + payload : hikari.utilities.data_binding.JSONObject + The JSON payload to deserialize. Returns ------- hikari.models.invites.VanityUrl - The parsed vanity url object. + The deserialized vanity url object. """ @abc.abstractmethod @@ -575,13 +578,13 @@ def deserialize_invite(self, payload: data_binding.JSONObject) -> invites.Invite Parameters ---------- - payload : Mapping[str, Any] - The dict payload to parse. + payload : hikari.utilities.data_binding.JSONObject + The JSON payload to deserialize. Returns ------- hikari.models.invites.Invite - The parsed invite object. + The deserialized invite object. """ @abc.abstractmethod @@ -590,13 +593,13 @@ def deserialize_invite_with_metadata(self, payload: data_binding.JSONObject) -> Parameters ---------- - payload : Mapping[str, Any] - The dict payload to parse. + payload : hikari.utilities.data_binding.JSONObject + The JSON payload to deserialize. Returns ------- hikari.models.invites.InviteWithMetadata - The parsed invite with metadata object. + The deserialized invite with metadata object. """ ############ @@ -609,13 +612,13 @@ def deserialize_message(self, payload: data_binding.JSONObject) -> messages.Mess Parameters ---------- - payload : Mapping[str, Any] - The dict payload to parse. + payload : hikari.utilities.data_binding.JSONObject + The JSON payload to deserialize. Returns ------- hikari.models.messages.Message - The parsed message object. + The deserialized message object. """ ############# @@ -628,13 +631,13 @@ def deserialize_member_presence(self, payload: data_binding.JSONObject) -> prese Parameters ---------- - payload : Mapping[str, Any] - The dict payload to parse. + payload : hikari.utilities.data_binding.JSONObject + The JSON payload to deserialize. Returns ------- hikari.models.guilds.MemberPresence - The parsed member presence object. + The deserialized member presence object. """ ######### @@ -647,28 +650,28 @@ def deserialize_user(self, payload: data_binding.JSONObject) -> users.User: Parameters ---------- - payload : Mapping[str, Any] - The dict payload to parse. + payload : hikari.utilities.data_binding.JSONObject + The JSON payload to deserialize. Returns ------- hikari.models.users.User - The parsed user object. + The deserialized user object. """ @abc.abstractmethod - def deserialize_my_user(self, payload: data_binding.JSONObject) -> users.MyUser: - """Parse a raw payload from Discord into a my user object. + def deserialize_my_user(self, payload: data_binding.JSONObject) -> users.OwnUser: + """Parse a raw payload from Discord into a user object. Parameters ---------- - payload : Mapping[str, Any] - The dict payload to parse. + payload : hikari.utilities.data_binding.JSONObject + The JSON payload to deserialize. Returns ------- - hikari.models.users.MyUser - The parsed my user object. + hikari.models.users.OwnUser + The deserialized user object. """ ########## @@ -681,13 +684,13 @@ def deserialize_voice_state(self, payload: data_binding.JSONObject) -> voices.Vo Parameters ---------- - payload : Mapping[str, Any] - The dict payload to parse. + payload : hikari.utilities.data_binding.JSONObject + The JSON payload to deserialize. Returns ------- hikari.models.voices.VoiceState - The parsed voice state object. + The deserialized voice state object. """ @abc.abstractmethod @@ -696,13 +699,13 @@ def deserialize_voice_region(self, payload: data_binding.JSONObject) -> voices.V Parameters ---------- - payload : Mapping[str, Any] - The dict payload to parse. + payload : hikari.utilities.data_binding.JSONObject + The JSON payload to deserialize. Returns ------- hikari.models.voices.VoiceRegion - The parsed voice region object. + The deserialized voice region object. """ ############ @@ -715,11 +718,11 @@ def deserialize_webhook(self, payload: data_binding.JSONObject) -> webhooks.Webh Parameters ---------- - payload : Mapping[str, Any] - The dict payload to parse. + payload : hikari.utilities.data_binding.JSONObject + The JSON payload to deserialize. Returns ------- hikari.models.webhooks.Webhook - The parsed webhook object. + The deserialized webhook object. """ diff --git a/hikari/api/event_consumer.py b/hikari/api/event_consumer.py index 0c990d316e..2081ff11b5 100644 --- a/hikari/api/event_consumer.py +++ b/hikari/api/event_consumer.py @@ -35,9 +35,9 @@ class IEventConsumer(component.IComponent, abc.ABC): """Interface describing a component that can consume raw gateway events. Implementations will usually want to combine this with a - `hikari.event_dispatcher.IEventDispatcher` for a basic in-memory single-app - event management system. You may in some cases implement this separately - if you are passing events onto a system such as a message queue. + `hikari.api.event_dispatcher.IEventDispatcher` for a basic in-memory + single-app event management system. You may in some cases implement this + separately if you are passing events onto a system such as a message queue. """ __slots__ = () @@ -54,6 +54,6 @@ async def consume_raw_event( The gateway shard that emitted the event. event_name : str The event name. - payload : Any + payload : hikari.utility.data_binding.JSONObject The payload provided with the event. """ diff --git a/hikari/api/event_dispatcher.py b/hikari/api/event_dispatcher.py index 6f35b31419..80cfbb5f8f 100644 --- a/hikari/api/event_dispatcher.py +++ b/hikari/api/event_dispatcher.py @@ -39,9 +39,9 @@ class IEventDispatcher(component.IComponent, abc.ABC): - """Provides the interface for components wishing to implement dispatching. + """Interface for event dispatchers. - This is a consumer of a `hikari.events.bases.HikariEvent` object, and is + This is a consumer of a `hikari.events.base.HikariEvent` object, and is expected to invoke one or more corresponding event listeners where appropriate. """ @@ -57,6 +57,60 @@ def dispatch(self, event: base.HikariEvent) -> aio.Future[typing.Any]: event : hikari.events.base.HikariEvent The event to dispatch. + Example + ------- + We can dispatch custom events by first defining a class that + derives from `hikari.events.base.HikariEvent`. + + ```py + import attr + + from hikari.events.base import HikariEvent + from hikari.models.users import User + from hikari.utilities.snowflake import Snowflake + + @attr.s(auto_attribs=True) + class EveryoneMentionedEvent(HikariEvent): + author: User + '''The user who mentioned everyone.''' + + content: str + '''The message that was sent.''' + + message_id: Snowflake + '''The message ID.''' + + channel_id: Snowflake + '''The channel ID.''' + ``` + + We can then dispatch our event as we see fit. + + ```py + from hikari.events.messages import MessageCreateEvent + + @bot.listen(MessageCreateEvent) + async def on_message(event): + if "@everyone" in event.content or "@here" in event.content: + event = EveryoneMentionedEvent( + author=event.author, + content=event.content, + message_id=event.id, + channel_id=event.channel_id, + ) + + bot.dispatch(event) + ``` + + This event can be listened to elsewhere by subscribing to it with + `IEventDispatcher.subscribe`. + + ```py + @bot.listen(EveryoneMentionedEvent) + async def on_everyone_mentioned(event): + print(event.user, "just pinged everyone in", event.channel_id) + ``` + Returns ------- asyncio.Future @@ -64,6 +118,11 @@ def dispatch(self, event: base.HikariEvent) -> aio.Future[typing.Any]: will complete once all corresponding event listeners have been invoked. If not awaited, this will schedule the dispatch of the events in the background for later. + + See Also + -------- + IEventDispatcher.subscribe + IEventDispatcher.wait_for """ @abc.abstractmethod @@ -79,10 +138,29 @@ def subscribe( event_type : typing.Type[hikari.events.base.HikariEvent] The event type to listen for. This will also listen for any subclasses of the given type. - callback : + callback Either a function or a coroutine function to invoke. This should consume an instance of the given event, or an instance of a valid subclass if one exists. Any result is discarded. + + Example + ------- + The following demonstrates subscribing a callback to message creation + events. + + ```py + from hikari.events.messages import MessageCreateEvent + + async def on_message(event): + ... + + bot.subscribe(MessageCreateEvent, on_message) + ``` + + See Also + -------- + IEventDispatcher.listen + IEventDispatcher.wait_for """ @abc.abstractmethod @@ -98,8 +176,22 @@ def unsubscribe( event_type : typing.Type[hikari.events.base.HikariEvent] The event type to unsubscribe from. This must be the same exact type as was originally subscribed with to be removed correctly. - callback : + callback The callback to unsubscribe. + + Example + ------- + The following demonstrates unsubscribing a callback from a message + creation event. + + ```py + from hikari.events.messages import MessageCreateEvent + + async def on_message(event): + ... + + bot.unsubscribe(MessageCreateEvent, on_message) + ``` """ @abc.abstractmethod @@ -112,16 +204,24 @@ def listen( Parameters ---------- - event_type : hikari.utilities.undefined.Undefined OR typing.Type[hikari.events.bases.HikariEvent] + event_type : hikari.utilities.undefined.Undefined or typing.Type[hikari.events.base.HikariEvent] The event type to subscribe to. The implementation may allow this to be undefined. If this is the case, the event type will be inferred instead from the type hints on the function signature. Returns ------- - typing.Callable + typing.Callable[[Callback], Callback] A decorator for a function or coroutine function that passes it - to `subscribe` before returning the function reference. + to `IEventDispatcher.subscribe` before returning the function + reference. + + See Also + -------- + IEventDispatcher.dispatch + IEventDispatcher.subscribe + IEventDispatcher.unsubscribe + IEventDispatcher.wait_for """ @abc.abstractmethod @@ -132,14 +232,14 @@ async def wait_for( Parameters ---------- - event_type : typing.Type[hikari.events.bases.HikariEvent] + event_type : typing.Type[hikari.events.base.HikariEvent] The event type to listen for. This will listen for subclasses of this type additionally. predicate A function or coroutine taking the event as the single parameter. This should return `True` if the event is one you want to return, or `False` if the event should not be returned. - timeout : float OR int OR None + timeout : float or int or None The amount of time to wait before raising an `asyncio.TimeoutError` and giving up instead. This is measured in seconds. If `None`, then no timeout will be waited for (no timeout can result in "leaking" of @@ -148,7 +248,7 @@ async def wait_for( Returns ------- - hikari.events.bases.HikariEvent + hikari.events.base.HikariEvent The event that was provided. Raises @@ -156,4 +256,10 @@ async def wait_for( asyncio.TimeoutError If the timeout is not `None` and is reached before an event is received that the predicate returns `True` for. + + See Also + -------- + IEventDispatcher.listen + IEventDispatcher.subscribe + IEventDispatcher.dispatch """ diff --git a/hikari/events/other.py b/hikari/events/other.py index a2fd0071d8..5c69cf1a07 100644 --- a/hikari/events/other.py +++ b/hikari/events/other.py @@ -30,7 +30,7 @@ "StoppedEvent", "ReadyEvent", "ResumedEvent", - "MyUserUpdateEvent", + "OwnUserUpdateEvent", ] import typing @@ -120,7 +120,7 @@ class ReadyEvent(base_events.HikariEvent): gateway_version: int = attr.ib(repr=True) """The gateway version this is currently connected to.""" - my_user: users.MyUser = attr.ib(repr=True) + my_user: users.OwnUser = attr.ib(repr=True) """The object of the current bot account this connection is for.""" unavailable_guilds: typing.Mapping[snowflake.Snowflake, guilds.UnavailableGuild] = attr.ib() @@ -153,7 +153,7 @@ def shard_count(self) -> typing.Optional[int]: @attr.s(eq=False, hash=False, kw_only=True, slots=True) -class MyUserUpdateEvent(base_events.HikariEvent, users.MyUser): +class OwnUserUpdateEvent(base_events.HikariEvent, users.OwnUser): """Used to represent User Update gateway events. Sent when the current user is updated. diff --git a/hikari/impl/cache.py b/hikari/impl/cache.py index 7ddd2f840d..a78582370e 100644 --- a/hikari/impl/cache.py +++ b/hikari/impl/cache.py @@ -194,10 +194,10 @@ async def get_user(self, user_id: int) -> typing.Optional[users.User]: async def delete_user(self, user_id: int) -> typing.Optional[users.User]: pass - async def create_my_user(self, payload: data_binding.JSONObject, can_cache: bool = False) -> users.MyUser: + async def create_my_user(self, payload: data_binding.JSONObject, can_cache: bool = False) -> users.OwnUser: pass - async def update_my_user(self, my_user: users.MyUser, payload: data_binding.JSONObject) -> users.MyUser: + async def update_my_user(self, my_user: users.OwnUser, payload: data_binding.JSONObject) -> users.OwnUser: pass async def get_my_user(self) -> typing.Optional[users.User]: diff --git a/hikari/impl/entity_factory.py b/hikari/impl/entity_factory.py index 7e88a38194..27d8959200 100644 --- a/hikari/impl/entity_factory.py +++ b/hikari/impl/entity_factory.py @@ -75,7 +75,10 @@ def _deserialize_max_age(seconds: int) -> typing.Optional[datetime.timedelta]: class EntityFactoryImpl(entity_factory.IEntityFactory): - """Interface for an entity factory implementation.""" + """Standard implementation for a serializer/deserializer. + + This will convert objects to/from JSON compatible representations. + """ def __init__(self, app: app_.IApp) -> None: self._app = app @@ -143,18 +146,6 @@ def app(self) -> app_.IApp: ################ def deserialize_own_connection(self, payload: data_binding.JSONObject) -> applications.OwnConnection: - """Parse a raw payload from Discord into an own connection object. - - Parameters - ---------- - payload : Mapping[str, Any] - The dict payload to parse. - - Returns - ------- - hikari.models.applications.OwnConnection - The parsed own connection object. - """ own_connection = applications.OwnConnection() own_connection.id = snowflake.Snowflake(payload["id"]) own_connection.name = payload["name"] @@ -171,18 +162,6 @@ def deserialize_own_connection(self, payload: data_binding.JSONObject) -> applic return own_connection def deserialize_own_guild(self, payload: data_binding.JSONObject) -> applications.OwnGuild: - """Parse a raw payload from Discord into an own guild object. - - Parameters - ---------- - payload : Mapping[str, Any] - The dict payload to parse. - - Returns - ------- - hikari.models.applications.OwnGuild - The parsed own guild object. - """ own_guild = self._set_partial_guild_attributes(payload, applications.OwnGuild()) own_guild.is_owner = bool(payload["owner"]) # noinspection PyArgumentList @@ -190,18 +169,6 @@ def deserialize_own_guild(self, payload: data_binding.JSONObject) -> application return own_guild def deserialize_application(self, payload: data_binding.JSONObject) -> applications.Application: - """Parse a raw payload from Discord into an application object. - - Parameters - ---------- - payload : Mapping[str, Any] - The dict payload to parse. - - Returns - ------- - hikari.models.applications.Application - The parsed application object. - """ application = applications.Application() application.set_app(self._app) application.id = snowflake.Snowflake(payload["id"]) @@ -332,18 +299,6 @@ def _deserialize_unrecognised_audit_log_entry_info( return audit_logs.UnrecognisedAuditLogEntryInfo(payload) def deserialize_audit_log(self, payload: data_binding.JSONObject) -> audit_logs.AuditLog: - """Parse a raw payload from Discord into an audit log object. - - Parameters - ---------- - payload : Mapping[str, Any] - The dict payload to parse. - - Returns - ------- - hikari.models.audit_logs.AuditLogEntry - The parsed audit log object. - """ audit_log = audit_logs.AuditLog() entries = {} @@ -414,18 +369,6 @@ def deserialize_audit_log(self, payload: data_binding.JSONObject) -> audit_logs. ############ def deserialize_permission_overwrite(self, payload: data_binding.JSONObject) -> channels_.PermissionOverwrite: - """Parse a raw payload from Discord into a permission overwrite object. - - Parameters - ---------- - payload : Mapping[str, Any] - The dict payload to parse. - - Returns - ------- - hikari.models.channels.PermissionOverwrote - The parsed permission overwrite object. - """ # noinspection PyArgumentList permission_overwrite = channels_.PermissionOverwrite( id=snowflake.Snowflake(payload["id"]), type=channels_.PermissionOverwriteType(payload["type"]), @@ -437,18 +380,6 @@ def deserialize_permission_overwrite(self, payload: data_binding.JSONObject) -> return permission_overwrite def serialize_permission_overwrite(self, overwrite: channels_.PermissionOverwrite) -> data_binding.JSONObject: - """Serialize a permission overwrite object to a json serializable dict. - - Parameters - ---------- - overwrite : hikari.models.channels.PermissionOverwrite - The permission overwrite object to serialize. - - Returns - ------- - Dict[Hashable, Any] - The dict representation of the permission overwrite object provided. - """ return {"id": str(overwrite.id), "type": overwrite.type, "allow": overwrite.allow, "deny": overwrite.deny} def _set_partial_channel_attributes( @@ -462,18 +393,6 @@ def _set_partial_channel_attributes( return channel def deserialize_partial_channel(self, payload: data_binding.JSONObject) -> channels_.PartialChannel: - """Parse a raw payload from Discord into a partial channel object. - - Parameters - ---------- - payload : Mapping[str, Any] - The dict payload to parse. - - Returns - ------- - hikari.models.channels.PartialChannel - The parsed partial channel object. - """ return self._set_partial_channel_attributes(payload, channels_.PartialChannel()) def _set_dm_channel_attributes(self, payload: data_binding.JSONObject, channel: DMChannelT) -> DMChannelT: @@ -489,33 +408,9 @@ def _set_dm_channel_attributes(self, payload: data_binding.JSONObject, channel: return channel def deserialize_dm_channel(self, payload: data_binding.JSONObject) -> channels_.DMChannel: - """Parse a raw payload from Discord into a DM channel object. - - Parameters - ---------- - payload : Mapping[str, Any] - The dict payload to parse. - - Returns - ------- - hikari.models.channels.DMChannel - The parsed DM channel object. - """ return self._set_dm_channel_attributes(payload, channels_.DMChannel()) def deserialize_group_dm_channel(self, payload: data_binding.JSONObject) -> channels_.GroupDMChannel: - """Parse a raw payload from Discord into a group DM channel object. - - Parameters - ---------- - payload : Mapping[str, Any] - The dict payload to parse. - - Returns - ------- - hikari.models.channels.GroupDMChannel - The parsed group DM channel object. - """ group_dm_channel = self._set_dm_channel_attributes(payload, channels_.GroupDMChannel()) group_dm_channel.owner_id = snowflake.Snowflake(payload["owner_id"]) group_dm_channel.icon_hash = payload["icon"] @@ -543,33 +438,9 @@ def _set_guild_channel_attributes(self, payload: data_binding.JSONObject, channe return channel def deserialize_guild_category(self, payload: data_binding.JSONObject) -> channels_.GuildCategory: - """Parse a raw payload from Discord into a guild category object. - - Parameters - ---------- - payload : Mapping[str, Any] - The dict payload to parse. - - Returns - ------- - hikari.models.channels.GuildCategory - The parsed partial channel object. - """ return self._set_guild_channel_attributes(payload, channels_.GuildCategory()) def deserialize_guild_text_channel(self, payload: data_binding.JSONObject) -> channels_.GuildTextChannel: - """Parse a raw payload from Discord into a guild text channel object. - - Parameters - ---------- - payload : Mapping[str, Any] - The dict payload to parse. - - Returns - ------- - hikari.models.channels.GuildTextChannel - The parsed guild text channel object. - """ guild_text_category = self._set_guild_channel_attributes(payload, channels_.GuildTextChannel()) guild_text_category.topic = payload["topic"] @@ -586,18 +457,6 @@ def deserialize_guild_text_channel(self, payload: data_binding.JSONObject) -> ch return guild_text_category def deserialize_guild_news_channel(self, payload: data_binding.JSONObject) -> channels_.GuildNewsChannel: - """Parse a raw payload from Discord into a guild news channel object. - - Parameters - ---------- - payload : Mapping[str, Any] - The dict payload to parse. - - Returns - ------- - hikari.models.channels.GuildNewsChannel - The parsed guild news channel object. - """ guild_news_channel = self._set_guild_channel_attributes(payload, channels_.GuildNewsChannel()) guild_news_channel.topic = payload["topic"] @@ -612,51 +471,15 @@ def deserialize_guild_news_channel(self, payload: data_binding.JSONObject) -> ch return guild_news_channel def deserialize_guild_store_channel(self, payload: data_binding.JSONObject) -> channels_.GuildStoreChannel: - """Parse a raw payload from Discord into a guild store channel object. - - Parameters - ---------- - payload : Mapping[str, Any] - The dict payload to parse. - - Returns - ------- - hikari.models.channels.GuildStoreChannel - The parsed guild store channel object. - """ return self._set_guild_channel_attributes(payload, channels_.GuildStoreChannel()) def deserialize_guild_voice_channel(self, payload: data_binding.JSONObject) -> channels_.GuildVoiceChannel: - """Parse a raw payload from Discord into a guild voice channel object. - - Parameters - ---------- - payload : Mapping[str, Any] - The dict payload to parse. - - Returns - ------- - hikari.models.channels.GuildVoiceChannel - The parsed guild voice channel object. - """ guild_voice_channel = self._set_guild_channel_attributes(payload, channels_.GuildVoiceChannel()) guild_voice_channel.bitrate = int(payload["bitrate"]) guild_voice_channel.user_limit = int(payload["user_limit"]) return guild_voice_channel def deserialize_channel(self, payload: data_binding.JSONObject) -> channels_.PartialChannel: - """Parse a raw payload from Discord into a channel object. - - Parameters - ---------- - payload : Mapping[str, Any] - The dict payload to parse. - - Returns - ------- - hikari.models.channels.PartialChannel - The parsed partial channel based object. - """ # noinspection PyArgumentList return self._channel_type_mapping[payload["type"]](payload) @@ -665,18 +488,6 @@ def deserialize_channel(self, payload: data_binding.JSONObject) -> channels_.Par ########## def deserialize_embed(self, payload: data_binding.JSONObject) -> embeds.Embed: - """Parse a raw payload from Discord into an embed object. - - Parameters - ---------- - payload : Mapping[str, Any] - The dict payload to parse. - - Returns - ------- - hikari.models.embeds.Embed - The parsed embed object. - """ embed = embeds.Embed() embed.title = payload.get("title") embed.description = payload.get("description") @@ -754,18 +565,6 @@ def deserialize_embed(self, payload: data_binding.JSONObject) -> embeds.Embed: return embed def serialize_embed(self, embed: embeds.Embed) -> data_binding.JSONObject: - """Serialize an embed object to a json serializable dict. - - Parameters - ---------- - embed : hikari.models.embeds.Embed - The embed object to serialize. - - Returns - ------- - Dict[Hashable, Any] - The dict representation of the provided embed object. - """ payload = {} if embed.title is not None: @@ -846,35 +645,11 @@ def serialize_embed(self, embed: embeds.Embed) -> data_binding.JSONObject: ########## def deserialize_unicode_emoji(self, payload: data_binding.JSONObject) -> emojis.UnicodeEmoji: - """Parse a raw payload from Discord into a unicode emoji object. - - Parameters - ---------- - payload : Mapping[str, Any] - The dict payload to parse. - - Returns - ------- - hikari.models.emojis.UnicodeEmoji - The parsed unicode emoji object. - """ unicode_emoji = emojis.UnicodeEmoji() unicode_emoji.name = payload["name"] return unicode_emoji def deserialize_custom_emoji(self, payload: data_binding.JSONObject) -> emojis.CustomEmoji: - """Parse a raw payload from Discord into a custom emoji object. - - Parameters - ---------- - payload : Mapping[str, Any] - The dict payload to parse. - - Returns - ------- - hikari.models.emojis.CustomEmoji - The parsed custom emoji object. - """ custom_emoji = emojis.CustomEmoji() custom_emoji.set_app(self._app) custom_emoji.id = snowflake.Snowflake(payload["id"]) @@ -883,18 +658,6 @@ def deserialize_custom_emoji(self, payload: data_binding.JSONObject) -> emojis.C return custom_emoji def deserialize_known_custom_emoji(self, payload: data_binding.JSONObject) -> emojis.KnownCustomEmoji: - """Parse a raw payload from Discord into a known custom emoji object. - - Parameters - ---------- - payload : Mapping[str, Any] - The dict payload to parse. - - Returns - ------- - hikari.models.emojis.KnownCustomEmoji - The parsed known custom emoji object. - """ known_custom_emoji = emojis.KnownCustomEmoji() known_custom_emoji.set_app(self._app) known_custom_emoji.id = snowflake.Snowflake(payload["id"]) @@ -914,18 +677,6 @@ def deserialize_known_custom_emoji(self, payload: data_binding.JSONObject) -> em def deserialize_emoji( self, payload: data_binding.JSONObject ) -> typing.Union[emojis.UnicodeEmoji, emojis.CustomEmoji]: - """Parse a raw payload from Discord into an emoji object. - - Parameters - ---------- - payload : Mapping[str, Any] - The dict payload to parse. - - Returns - ------- - hikari.models.emojis.UnicodeEmoji | hikari.models.emoji.CustomEmoji - The parsed custom or unicode emoji object. - """ if payload.get("id") is not None: return self.deserialize_custom_emoji(payload) @@ -936,18 +687,6 @@ def deserialize_emoji( ########### def deserialize_gateway_bot(self, payload: data_binding.JSONObject) -> gateway.GatewayBot: - """Parse a raw payload from Discord into a gateway bot object. - - Parameters - ---------- - payload : Mapping[str, Any] - The dict payload to parse. - - Returns - ------- - hikari.models.gateway.GatewayBot - The parsed gateway bot object. - """ gateway_bot = gateway.GatewayBot() gateway_bot.url = payload["url"] gateway_bot.shard_count = int(payload["shards"]) @@ -967,18 +706,6 @@ def deserialize_gateway_bot(self, payload: data_binding.JSONObject) -> gateway.G ########## def deserialize_guild_widget(self, payload: data_binding.JSONObject) -> guilds.GuildWidget: - """Parse a raw payload from Discord into a guild widget object. - - Parameters - ---------- - payload : Mapping[str, Any] - The dict payload to parse. - - Returns - ------- - hikari.models.guilds.GuildWidget - The parsed guild embed object. - """ guild_embed = guilds.GuildWidget() guild_embed.set_app(self._app) @@ -990,24 +717,11 @@ def deserialize_guild_widget(self, payload: data_binding.JSONObject) -> guilds.G return guild_embed def deserialize_member( - self, payload: data_binding.JSONObject, *, user: typing.Optional[users.User] = None - ) -> guilds.Member: - """Parse a raw payload from Discord into a member object. - - Parameters - ---------- - payload : Mapping[str, Any] - The dict payload to parse. + self, + payload: data_binding.JSONObject, *, - user : hikari.models.users.User? - The user to attach to this member, should be passed in situations - where "user" is not included in the payload. - - Returns - ------- - hikari.models.guilds.Member - The parsed member object. - """ + user: typing.Union[undefined.Undefined, users.User] = undefined.Undefined() + ) -> guilds.Member: guild_member = guilds.Member() guild_member.set_app(self._app) guild_member.user = user or self.deserialize_user(payload["user"]) @@ -1024,18 +738,6 @@ def deserialize_member( return guild_member def deserialize_role(self, payload: data_binding.JSONObject) -> guilds.Role: - """Parse a raw payload from Discord into a guild role object. - - Parameters - ---------- - payload : Mapping[str, Any] - The dict payload to parse. - - Returns - ------- - hikari.models.guilds.GuildRole - The parsed guild role object. - """ guild_role = guilds.Role() guild_role.set_app(self._app) guild_role.id = snowflake.Snowflake(payload["id"]) @@ -1064,33 +766,9 @@ def _set_partial_integration_attributes( return integration def deserialize_partial_integration(self, payload: data_binding.JSONObject) -> guilds.PartialIntegration: - """Parse a raw payload from Discord into a partial integration object. - - Parameters - ---------- - payload : Mapping[str, Any] - The dict payload to parse. - - Returns - ------- - hikari.models.guilds.PartialIntegration - The parsed partial integration object. - """ return self._set_partial_integration_attributes(payload, guilds.PartialIntegration()) def deserialize_integration(self, payload: data_binding.JSONObject) -> guilds.Integration: - """Parse a raw payload from Discord into an integration object. - - Parameters - ---------- - payload : Mapping[str, Any] - The dict payload to parse. - - Returns - ------- - hikari.models.guilds.Integration - The parsed integration object. - """ guild_integration = self._set_partial_integration_attributes(payload, guilds.Integration()) guild_integration.is_enabled = payload["enabled"] guild_integration.is_syncing = payload["syncing"] @@ -1112,36 +790,12 @@ def deserialize_integration(self, payload: data_binding.JSONObject) -> guilds.In return guild_integration def deserialize_guild_member_ban(self, payload: data_binding.JSONObject) -> guilds.GuildMemberBan: - """Parse a raw payload from Discord into a guild member ban object. - - Parameters - ---------- - payload : Mapping[str, Any] - The dict payload to parse. - - Returns - ------- - hikari.models.GuildMemberBan - The parsed guild member ban object. - """ guild_member_ban = guilds.GuildMemberBan() guild_member_ban.reason = payload["reason"] guild_member_ban.user = self.deserialize_user(payload["user"]) return guild_member_ban def deserialize_unavailable_guild(self, payload: data_binding.JSONObject) -> guilds.UnavailableGuild: - """Parse a raw payload from Discord into a unavailable guild object. - - Parameters - ---------- - payload : Mapping[str, Any] - The dict payload to parse. - - Returns - ------- - hikari.models.guilds.UnavailableGuild - The parsed unavailable guild object. - """ unavailable_guild = guilds.UnavailableGuild() unavailable_guild.set_app(self._app) unavailable_guild.id = snowflake.Snowflake(payload["id"]) @@ -1165,18 +819,6 @@ def _set_partial_guild_attributes(self, payload: data_binding.JSONObject, guild: return guild def deserialize_guild_preview(self, payload: data_binding.JSONObject) -> guilds.GuildPreview: - """Parse a raw payload from Discord into a guild preview object. - - Parameters - ---------- - payload : Mapping[str, Any] - The dict payload to parse. - - Returns - ------- - hikari.models.guilds.GuildPreview - The parsed guild preview object. - """ guild_preview = self._set_partial_guild_attributes(payload, guilds.GuildPreview()) guild_preview.splash_hash = payload["splash"] guild_preview.discovery_splash_hash = payload["discovery_splash"] @@ -1189,18 +831,6 @@ def deserialize_guild_preview(self, payload: data_binding.JSONObject) -> guilds. return guild_preview def deserialize_guild(self, payload: data_binding.JSONObject) -> guilds.Guild: - """Parse a raw payload from Discord into a guild object. - - Parameters - ---------- - payload : Mapping[str, Any] - The dict payload to parse. - - Returns - ------- - hikari.models.guilds.Guild - The parsed guild object. - """ guild = self._set_partial_guild_attributes(payload, guilds.Guild()) guild.splash_hash = payload["splash"] guild.discovery_splash_hash = payload["discovery_splash"] @@ -1322,18 +952,6 @@ def deserialize_guild(self, payload: data_binding.JSONObject) -> guilds.Guild: ########### def deserialize_vanity_url(self, payload: data_binding.JSONObject) -> invites.VanityURL: - """Parse a raw payload from Discord into a vanity url object. - - Parameters - ---------- - payload : Mapping[str, Any] - The dict payload to parse. - - Returns - ------- - hikari.models.invites.VanityURL - The parsed vanity url object. - """ vanity_url = invites.VanityURL() vanity_url.set_app(self._app) vanity_url.code = payload["code"] @@ -1372,33 +990,9 @@ def _set_invite_attributes(self, payload: data_binding.JSONObject, invite: Invit return invite def deserialize_invite(self, payload: data_binding.JSONObject) -> invites.Invite: - """Parse a raw payload from Discord into an invite object. - - Parameters - ---------- - payload : Mapping[str, Any] - The dict payload to parse. - - Returns - ------- - hikari.models.invites.Invite - The parsed invite object. - """ return self._set_invite_attributes(payload, invites.Invite()) def deserialize_invite_with_metadata(self, payload: data_binding.JSONObject) -> invites.InviteWithMetadata: - """Parse a raw payload from Discord into a invite with metadata object. - - Parameters - ---------- - payload : Mapping[str, Any] - The dict payload to parse. - - Returns - ------- - hikari.models.invites.InviteWithMetadata - The parsed invite with metadata object. - """ invite_with_metadata = self._set_invite_attributes(payload, invites.InviteWithMetadata()) invite_with_metadata.uses = int(payload["uses"]) invite_with_metadata.max_uses = int(payload["max_uses"]) @@ -1414,18 +1008,6 @@ def deserialize_invite_with_metadata(self, payload: data_binding.JSONObject) -> # TODO: arbitrarily partial ver? def deserialize_message(self, payload: data_binding.JSONObject) -> messages.Message: - """Parse a raw payload from Discord into a message object. - - Parameters - ---------- - payload : Mapping[str, Any] - The dict payload to parse. - - Returns - ------- - hikari.models.messages.Message - The parsed message object. - """ message = messages.Message() message.set_app(self._app) message.id = snowflake.Snowflake(payload["id"]) @@ -1514,18 +1096,6 @@ def deserialize_message(self, payload: data_binding.JSONObject) -> messages.Mess ############# def deserialize_member_presence(self, payload: data_binding.JSONObject) -> presences_.MemberPresence: - """Parse a raw payload from Discord into a member presence object. - - Parameters - ---------- - payload : Mapping[str, Any] - The dict payload to parse. - - Returns - ------- - hikari.models.guilds.MemberPresence - The parsed member presence object. - """ guild_member_presence = presences_.MemberPresence() guild_member_presence.set_app(self._app) user_payload = payload["user"] @@ -1670,37 +1240,13 @@ def _set_user_attributes(self, payload: data_binding.JSONObject, user: UserT) -> return user def deserialize_user(self, payload: data_binding.JSONObject) -> users.User: - """Parse a raw payload from Discord into a user object. - - Parameters - ---------- - payload : Mapping[str, Any] - The dict payload to parse. - - Returns - ------- - hikari.models.users.User - The parsed user object. - """ user = self._set_user_attributes(payload, users.User()) # noinspection PyArgumentList user.flags = users.UserFlag(payload["public_flags"]) if "public_flags" in payload else users.UserFlag.NONE return user - def deserialize_my_user(self, payload: data_binding.JSONObject) -> users.MyUser: - """Parse a raw payload from Discord into a my user object. - - Parameters - ---------- - payload : Mapping[str, Any] - The dict payload to parse. - - Returns - ------- - hikari.models.users.MyUser - The parsed my user object. - """ - my_user = self._set_user_attributes(payload, users.MyUser()) + def deserialize_my_user(self, payload: data_binding.JSONObject) -> users.OwnUser: + my_user = self._set_user_attributes(payload, users.OwnUser()) my_user.is_mfa_enabled = payload["mfa_enabled"] my_user.locale = payload.get("locale") my_user.is_verified = payload.get("verified") @@ -1716,18 +1262,6 @@ def deserialize_my_user(self, payload: data_binding.JSONObject) -> users.MyUser: ########## def deserialize_voice_state(self, payload: data_binding.JSONObject) -> voices.VoiceState: - """Parse a raw payload from Discord into a voice state object. - - Parameters - ---------- - payload : Mapping[str, Any] - The dict payload to parse. - - Returns - ------- - hikari.models.voices.VoiceState - The parsed voice state object. - """ voice_state = voices.VoiceState() voice_state.set_app(self._app) voice_state.guild_id = snowflake.Snowflake(payload["guild_id"]) if "guild_id" in payload else None @@ -1748,18 +1282,6 @@ def deserialize_voice_state(self, payload: data_binding.JSONObject) -> voices.Vo return voice_state def deserialize_voice_region(self, payload: data_binding.JSONObject) -> voices.VoiceRegion: - """Parse a raw payload from Discord into a voice region object. - - Parameters - ---------- - payload : Mapping[str, Any] - The dict payload to parse. - - Returns - ------- - hikari.models.voices.VoiceRegion - The parsed voice region object. - """ voice_region = voices.VoiceRegion() voice_region.id = payload["id"] voice_region.name = payload["name"] @@ -1774,18 +1296,6 @@ def deserialize_voice_region(self, payload: data_binding.JSONObject) -> voices.V ############ def deserialize_webhook(self, payload: data_binding.JSONObject) -> webhooks.Webhook: - """Parse a raw payload from Discord into a webhook object. - - Parameters - ---------- - payload : Mapping[str, Any] - The dict payload to parse. - - Returns - ------- - hikari.models.webhooks.Webhook - The parsed webhook object. - """ webhook = webhooks.Webhook() webhook.id = snowflake.Snowflake(payload["id"]) # noinspection PyArgumentList diff --git a/hikari/impl/event_manager.py b/hikari/impl/event_manager.py index 59afd3bc30..11ead2b8d2 100644 --- a/hikari/impl/event_manager.py +++ b/hikari/impl/event_manager.py @@ -27,5 +27,3 @@ class EventManagerImpl(event_manager_core.EventManagerCore): """Provides event handling logic for Discord events.""" - - pass diff --git a/hikari/models/users.py b/hikari/models/users.py index d405513903..457a41c02d 100644 --- a/hikari/models/users.py +++ b/hikari/models/users.py @@ -20,7 +20,7 @@ from __future__ import annotations -__all__ = ["User", "MyUser", "UserFlag", "PremiumType"] +__all__ = ["User", "OwnUser", "UserFlag", "PremiumType"] import enum import typing @@ -180,8 +180,8 @@ def default_avatar_url(self) -> str: @attr.s(eq=True, hash=True, init=False, kw_only=True, slots=True) -class MyUser(User): - """Represents a user with extended oauth2 information.""" +class OwnUser(User): + """Represents a user with extended OAuth2 information.""" is_mfa_enabled: bool = attr.ib(eq=False, hash=False) """Whether the user's account has 2fa enabled.""" @@ -212,7 +212,7 @@ class MyUser(User): This will always be `None` for bots. """ - async def fetch_self(self) -> MyUser: + async def fetch_self(self) -> OwnUser: """Get this user's up-to-date object. Returns diff --git a/hikari/net/rest.py b/hikari/net/rest.py index 8387b7a3eb..618a11836d 100644 --- a/hikari/net/rest.py +++ b/hikari/net/rest.py @@ -1556,7 +1556,7 @@ async def delete_invite(self, invite: typing.Union[invites.Invite, str]) -> None route = routes.DELETE_INVITE.compile(invite_code=invite if isinstance(invite, str) else invite.code) await self._request(route) - async def fetch_my_user(self) -> users.MyUser: + async def fetch_my_user(self) -> users.OwnUser: route = routes.GET_MY_USER.compile() response = await self._request(route) return self._app.entity_factory.deserialize_my_user(response) @@ -1566,7 +1566,7 @@ async def edit_my_user( *, username: typing.Union[undefined.Undefined, str] = undefined.Undefined(), avatar: typing.Union[undefined.Undefined, files.BaseStream] = undefined.Undefined(), - ) -> users.MyUser: + ) -> users.OwnUser: route = routes.PATCH_MY_USER.compile() body = data_binding.JSONObjectBuilder() body.put("username", username) diff --git a/tests/hikari/impl/test_entity_factory.py b/tests/hikari/impl/test_entity_factory.py index 435710eb57..1799299b36 100644 --- a/tests/hikari/impl/test_entity_factory.py +++ b/tests/hikari/impl/test_entity_factory.py @@ -2317,7 +2317,7 @@ def test_deserialize_my_user(self, entity_factory_impl, mock_app, my_user_payloa assert my_user.email == "blahblah@blah.blah" assert my_user.flags == users.UserFlag.DISCORD_PARTNER | users.UserFlag.DISCORD_EMPLOYEE assert my_user.premium_type is users.PremiumType.NITRO_CLASSIC - assert isinstance(my_user, users.MyUser) + assert isinstance(my_user, users.OwnUser) def test_deserialize_my_user_with_unset_fields(self, entity_factory_impl, mock_app, my_user_payload): my_user = entity_factory_impl.deserialize_my_user( @@ -2338,7 +2338,7 @@ def test_deserialize_my_user_with_unset_fields(self, entity_factory_impl, mock_a assert my_user.is_system is False assert my_user.is_verified is None assert my_user.email is None - assert isinstance(my_user, users.MyUser) + assert isinstance(my_user, users.OwnUser) ########## # Voices # From 986d6e99c73f7026cdb107286a9a8857fb67a21f Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Wed, 3 Jun 2020 18:39:39 +0100 Subject: [PATCH 440/922] Fixed some docstrings in models.applications.py --- hikari/api/cache.py | 1 - hikari/api/component.py | 7 + hikari/api/entity_factory.py | 10 +- hikari/impl/cache.py | 180 ----------------------- hikari/impl/entity_factory.py | 6 +- hikari/impl/event_manager_core.py | 26 ++-- hikari/impl/gateway_zookeeper.py | 32 ++-- hikari/impl/rest_app.py | 7 +- hikari/models/applications.py | 149 +++++++++++-------- hikari/net/rest.py | 6 +- tests/hikari/impl/test_entity_factory.py | 4 +- 11 files changed, 143 insertions(+), 285 deletions(-) diff --git a/hikari/api/cache.py b/hikari/api/cache.py index 4bc261c430..00b1b411bf 100644 --- a/hikari/api/cache.py +++ b/hikari/api/cache.py @@ -22,7 +22,6 @@ __all__ = ["ICache"] import abc -import typing from hikari.api import component diff --git a/hikari/api/component.py b/hikari/api/component.py index 6fac64c7e0..267cb303eb 100644 --- a/hikari/api/component.py +++ b/hikari/api/component.py @@ -32,7 +32,14 @@ class IComponent(abc.ABC): Objects that derive from this should usually be attributes on the `hikari.api.app.IApp` object. + + Examples + -------- + See the source code for `hikari.api.entity_factory.IEntityFactory`, + `hikari.api.cache.ICache`, and + `hikari.api.event_dispatcher.IEventDispatcher` for examples of usage. """ + __slots__ = () @property diff --git a/hikari/api/entity_factory.py b/hikari/api/entity_factory.py index b09e6c4d4b..4f8eb5be53 100644 --- a/hikari/api/entity_factory.py +++ b/hikari/api/entity_factory.py @@ -427,10 +427,10 @@ def deserialize_guild_widget(self, payload: data_binding.JSONObject) -> guilds.G @abc.abstractmethod def deserialize_member( - self, - payload: data_binding.JSONObject, - *, - user: typing.Union[undefined.Undefined, users.User] = undefined.Undefined() + self, + payload: data_binding.JSONObject, + *, + user: typing.Union[undefined.Undefined, users.User] = undefined.Undefined(), ) -> guilds.Member: """Parse a raw payload from Discord into a member object. @@ -439,7 +439,7 @@ def deserialize_member( payload : hikari.utilities.data_binding.JSONObject The JSON payload to deserialize. user : hikari.models.users.User or hikari.utilities.undefined.Undefined - The user to attach to this member, should only be passed in + The user to attach to this member, should only be passed in situations where "user" is not included in the payload. Returns diff --git a/hikari/impl/cache.py b/hikari/impl/cache.py index a78582370e..a3982b07f4 100644 --- a/hikari/impl/cache.py +++ b/hikari/impl/cache.py @@ -25,21 +25,9 @@ import typing from hikari.api import cache -from hikari.utilities import data_binding if typing.TYPE_CHECKING: from hikari.api import app as app_ - from hikari.models import applications - from hikari.models import audit_logs - from hikari.models import channels - from hikari.models import embeds - from hikari.models import emojis - from hikari.models import gateway - from hikari.models import guilds - from hikari.models import invites - from hikari.models import messages - from hikari.models import users - from hikari.models import voices class CacheImpl(cache.ICache): @@ -49,171 +37,3 @@ def __init__(self, app: app_.IApp) -> None: @property def app(self) -> app_.IApp: return self._app - - async def create_application(self, payload: data_binding.JSONObject) -> applications.Application: - pass - - async def create_own_guild(self, payload: data_binding.JSONObject) -> applications.OwnGuild: - pass - - async def create_own_connection(self, payload: data_binding.JSONObject) -> applications.OwnConnection: - pass - - async def create_audit_log_change(self, payload: data_binding.JSONObject) -> audit_logs.AuditLogChange: - pass - - async def create_audit_log_entry_info(self, payload: data_binding.JSONObject) -> audit_logs.BaseAuditLogEntryInfo: - pass - - async def create_audit_log_entry(self, payload: data_binding.JSONObject) -> audit_logs.AuditLogEntry: - pass - - async def create_audit_log(self, payload: data_binding.JSONObject) -> audit_logs.AuditLog: - pass - - async def create_channel( - self, payload: data_binding.JSONObject, can_cache: bool = False - ) -> channels.PartialChannel: - pass - - async def update_channel( - self, channel: channels.PartialChannel, payload: data_binding.JSONObject - ) -> channels.PartialChannel: - pass - - async def get_channel(self, channel_id: int) -> typing.Optional[channels.PartialChannel]: - pass - - async def delete_channel(self, channel_id: int) -> typing.Optional[channels.PartialChannel]: - pass - - async def create_embed(self, payload: data_binding.JSONObject) -> embeds.Embed: - pass - - async def create_emoji(self, payload: data_binding.JSONObject, can_cache: bool = False) -> emojis.Emoji: - pass - - async def update_emoji(self, payload: data_binding.JSONObject) -> emojis.Emoji: - pass - - async def get_emoji(self, emoji_id: int) -> typing.Optional[emojis.KnownCustomEmoji]: - pass - - async def delete_emoji(self, emoji_id: int) -> typing.Optional[emojis.KnownCustomEmoji]: - pass - - async def create_gateway_bot(self, payload: data_binding.JSONObject) -> gateway.GatewayBot: - pass - - async def create_member(self, payload: data_binding.JSONObject, can_cache: bool = False) -> guilds.Member: - pass - - async def update_member(self, member: guilds.Member, payload: data_binding.JSONObject) -> guilds.Member: - pass - - async def get_member(self, guild_id: int, user_id: int) -> typing.Optional[guilds.Member]: - pass - - async def delete_member(self, guild_id: int, user_id: int) -> typing.Optional[guilds.Member]: - pass - - async def create_role(self, payload: data_binding.JSONObject, can_cache: bool = False) -> guilds.PartialRole: - pass - - async def update_role(self, role: guilds.PartialRole, payload: data_binding.JSONObject) -> guilds.PartialRole: - pass - - async def get_role(self, guild_id: int, role_id: int) -> typing.Optional[guilds.PartialRole]: - pass - - async def delete_role(self, guild_id: int, role_id: int) -> typing.Optional[guilds.PartialRole]: - pass - - async def create_presence(self, payload: data_binding.JSONObject, can_cache: bool = False) -> guilds.MemberPresence: - pass - - async def update_presence( - self, role: guilds.MemberPresence, payload: data_binding.JSONObject - ) -> guilds.MemberPresence: - pass - - async def get_presence(self, guild_id: int, user_id: int) -> typing.Optional[guilds.MemberPresence]: - pass - - async def delete_presence(self, guild_id: int, user_id: int) -> typing.Optional[guilds.MemberPresence]: - pass - - async def create_guild_ban(self, payload: data_binding.JSONObject) -> guilds.GuildMemberBan: - pass - - async def create_integration(self, payload: data_binding.JSONObject) -> guilds.PartialIntegration: - pass - - async def create_guild(self, payload: data_binding.JSONObject, can_cache: bool = False) -> guilds.PartialGuild: - pass - - async def update_guild(self, guild: guilds.PartialGuild, payload: data_binding.JSONObject) -> guilds.PartialGuild: - pass - - async def get_guild(self, guild_id: int) -> typing.Optional[guilds.PartialGuild]: - pass - - async def delete_guild(self, guild_id: int) -> typing.Optional[guilds.PartialGuild]: - pass - - async def create_guild_preview(self, payload: data_binding.JSONObject) -> guilds.GuildPreview: - pass - - async def create_invite(self, payload: data_binding.JSONObject) -> invites.Invite: - pass - - async def create_reaction(self, payload: data_binding.JSONObject) -> messages.Reaction: - pass - - async def create_message(self, payload: data_binding.JSONObject, can_cache: bool = False) -> messages.Message: - pass - - async def update_message(self, message: messages.Message, payload: data_binding.JSONObject) -> messages.Message: - pass - - async def get_message(self, channel_id: int, message_id: int) -> typing.Optional[messages.Message]: - pass - - async def delete_message(self, channel_id: int, message_id: int) -> typing.Optional[messages.Message]: - pass - - async def create_user(self, payload: data_binding.JSONObject, can_cache: bool = False) -> users.User: - pass - - async def update_user(self, user: users.User, payload: data_binding.JSONObject) -> users.User: - pass - - async def get_user(self, user_id: int) -> typing.Optional[users.User]: - pass - - async def delete_user(self, user_id: int) -> typing.Optional[users.User]: - pass - - async def create_my_user(self, payload: data_binding.JSONObject, can_cache: bool = False) -> users.OwnUser: - pass - - async def update_my_user(self, my_user: users.OwnUser, payload: data_binding.JSONObject) -> users.OwnUser: - pass - - async def get_my_user(self) -> typing.Optional[users.User]: - pass - - async def create_voice_state(self, payload: data_binding.JSONObject, can_cache: bool = False) -> voices.VoiceState: - pass - - async def update_voice_state(self, payload: data_binding.JSONObject) -> voices.VoiceState: - pass - - async def get_voice_state(self, guild_id: int, channel_id: int) -> typing.Optional[voices.VoiceState]: - pass - - async def delete_voice_state(self, guild_id: int, channel_id: int) -> typing.Optional[voices.VoiceState]: - pass - - async def create_voice_region(self, payload: data_binding.JSONObject) -> voices.VoiceRegion: - pass diff --git a/hikari/impl/entity_factory.py b/hikari/impl/entity_factory.py index 27d8959200..3300da9290 100644 --- a/hikari/impl/entity_factory.py +++ b/hikari/impl/entity_factory.py @@ -155,8 +155,8 @@ def deserialize_own_connection(self, payload: data_binding.JSONObject) -> applic self.deserialize_partial_integration(integration) for integration in payload.get("integrations", ()) ] own_connection.is_verified = payload["verified"] - own_connection.is_friend_syncing = payload["friend_sync"] - own_connection.is_showing_activity = payload["show_activity"] + own_connection.is_friend_sync_enabled = payload["friend_sync"] + own_connection.is_activity_visible = payload["show_activity"] # noinspection PyArgumentList own_connection.visibility = applications.ConnectionVisibility(payload["visibility"]) return own_connection @@ -720,7 +720,7 @@ def deserialize_member( self, payload: data_binding.JSONObject, *, - user: typing.Union[undefined.Undefined, users.User] = undefined.Undefined() + user: typing.Union[undefined.Undefined, users.User] = undefined.Undefined(), ) -> guilds.Member: guild_member = guilds.Member() guild_member.set_app(self._app) diff --git a/hikari/impl/event_manager_core.py b/hikari/impl/event_manager_core.py index 1095de5edc..a6939c16fc 100644 --- a/hikari/impl/event_manager_core.py +++ b/hikari/impl/event_manager_core.py @@ -167,6 +167,19 @@ def decorator(callback: _CallbackT) -> _CallbackT: return decorator + async def wait_for( + self, event_type: typing.Type[_EventT], predicate: _PredicateT, timeout: typing.Union[float, int, None] + ) -> _EventT: + + future = asyncio.get_event_loop().create_future() + + if event_type not in self._waiters: + self._waiters[event_type] = set() + + self._waiters[event_type].add((predicate, future)) + + return await asyncio.wait_for(future, timeout=timeout) if timeout is not None else await future + async def _test_waiter(self, cls, event, predicate, future): try: result = predicate(event) @@ -196,16 +209,3 @@ async def _invoke_callback(self, callback: _CallbackT, event: _EventT) -> None: self.logger.error("an exception occurred handling an event, but it has been ignored", exc_info=ex) else: await self.dispatch(other.ExceptionEvent(app=self._app, exception=ex, event=event, callback=callback)) - - async def wait_for( - self, event_type: typing.Type[_EventT], predicate: _PredicateT, timeout: typing.Union[float, int, None] - ) -> _EventT: - - future = asyncio.get_event_loop().create_future() - - if event_type not in self._waiters: - self._waiters[event_type] = set() - - self._waiters[event_type].add((predicate, future)) - - return await asyncio.wait_for(future, timeout=timeout) if timeout is not None else await future diff --git a/hikari/impl/gateway_zookeeper.py b/hikari/impl/gateway_zookeeper.py index 2c860cdd10..89c79206f8 100644 --- a/hikari/impl/gateway_zookeeper.py +++ b/hikari/impl/gateway_zookeeper.py @@ -155,18 +155,6 @@ async def join(self) -> None: if self._gather_task is not None: await self._gather_task - async def _abort(self) -> None: - for shard_id in self._tasks: - await self._shards[shard_id].close() - await asyncio.gather(*self._tasks.values(), return_exceptions=True) - - async def _gather(self) -> None: - try: - await asyncio.gather(*self._tasks.values()) - finally: - self.logger.debug("gather failed, shutting down shard(s)") - await self.close() - async def close(self) -> None: if self._tasks: # This way if we cancel the stopping task, we still shut down properly. @@ -191,10 +179,6 @@ async def close(self) -> None: # noinspection PyUnresolvedReferences await self.event_dispatcher.dispatch(other.StoppedEvent()) - async def _run(self) -> None: - await self.start() - await self.join() - def run(self) -> None: loop = asyncio.get_event_loop() @@ -308,3 +292,19 @@ def _max_concurrency_chunker(self) -> typing.Iterator[typing.Iterator[int]]: @abc.abstractmethod async def _fetch_gateway_recommendations(self) -> gateway_models.GatewayBot: ... + + async def _abort(self) -> None: + for shard_id in self._tasks: + await self._shards[shard_id].close() + await asyncio.gather(*self._tasks.values(), return_exceptions=True) + + async def _gather(self) -> None: + try: + await asyncio.gather(*self._tasks.values()) + finally: + self.logger.debug("gather failed, shutting down shard(s)") + await self.close() + + async def _run(self) -> None: + await self.start() + await self.join() diff --git a/hikari/impl/rest_app.py b/hikari/impl/rest_app.py index 0457dc03ff..a08135c8b3 100644 --- a/hikari/impl/rest_app.py +++ b/hikari/impl/rest_app.py @@ -43,7 +43,7 @@ def __init__( self, config: typing.Union[undefined.Undefined, http_settings_.HTTPSettings] = undefined.Undefined(), debug: bool = False, - token: typing.Optional[str] = None, + token: typing.Union[undefined.Undefined, str] = undefined.Undefined(), token_type: typing.Union[undefined.Undefined, str] = undefined.Undefined(), rest_url: typing.Union[undefined.Undefined, str] = undefined.Undefined(), version: int = 6, @@ -61,8 +61,8 @@ def __init__( rest_url=rest_url, version=version, ) - self._cache = cache_impl.CacheImpl() - self._entity_factory = entity_factory_impl.EntityFactoryImpl() + self._cache = cache_impl.CacheImpl(self) + self._entity_factory = entity_factory_impl.EntityFactoryImpl(self) @property def logger(self) -> logging.Logger: @@ -70,7 +70,6 @@ def logger(self) -> logging.Logger: @property def thread_pool(self) -> typing.Optional[futures.ThreadPoolExecutor]: - # XXX: fixme return None @property diff --git a/hikari/models/applications.py b/hikari/models/applications.py index 5369b3bc08..8570b9d406 100644 --- a/hikari/models/applications.py +++ b/hikari/models/applications.py @@ -56,14 +56,14 @@ class OAuth2Scope(str, enum.Enum): """ ACTIVITIES_READ = "activities.read" - """Enable the application to fetch a user's "Now Playing/Recently Played" list. + """Enables fetching the "Now Playing/Recently Played" list. !!! note You must be whitelisted to use this scope. """ ACTIVITIES_WRITE = "activities.write" - """Enable the application to update a user's activity. + """Enables updating a user's activity. !!! note You must be whitelisted to use this scope. @@ -73,24 +73,24 @@ class OAuth2Scope(str, enum.Enum): """ APPLICATIONS_BUILDS_READ = "applications.builds.read" - """Enable the application to read build data for a user's applications. + """Enables reading build data for a user's applications. !!! note You must be whitelisted to use this scope. """ APPLICATIONS_BUILDS_UPLOAD = "applications.builds.upload" - """Enable the application to upload/update builds for a user's applications. + """Enables uploading/updating builds for a user's applications. !!! note You must be whitelisted to use this scope. """ APPLICATIONS_ENTITLEMENTS = "applications.entitlements" - """Enable the application to read entitlements for a user's applications.""" + """Enables reading entitlements for a user's applications.""" APPLICATIONS_STORE_UPDATE = "applications.store.update" - """Enable the application to read and update store data for the user's applications. + """Enables reading/updating store data for the user's applications. This includes store listings, achievements, SKU's, etc. @@ -99,33 +99,37 @@ class OAuth2Scope(str, enum.Enum): """ BOT = "bot" - """Used to add OAuth2 bots to a guild. + """Enables adding a bot application to a guild. !!! note This requires you to have set up a bot account for your application. """ CONNECTIONS = "connections" - """Enable the application to view third-party linked accounts such as Twitch.""" + """Enables viewing third-party linked accounts such as Twitch.""" EMAIL = "email" """Enable the application to view the user's email and application info.""" GROUP_DM_JOIN = "gdm.join" - """Enable the application to join users into a group DM.""" + """Enables joining users into a group DM. + + !!! warn + This cannot add the bot to a group DM. + """ GUILDS = "guilds" - """Enable the application to view the guilds the user is in.""" + """Enables viewing the guilds the user is in.""" GUILDS_JOIN = "guilds.join" - """Enable the application to add the user to a specific guild. + """Enables adding the user to a specific guild. !!! note This requires you to have set up a bot account for your application. """ IDENTIFY = "identify" - """Enable the application to view info about itself. + """Enables viewing info about itself. !!! note This does not include email address info. Use the `EMAIL` scope instead @@ -133,31 +137,31 @@ class OAuth2Scope(str, enum.Enum): """ RELATIONSHIPS_READ = "relationships.read" - """Enable the application to view a user's friend list. + """Enables viewing a user's friend list. !!! note You must be whitelisted to use this scope. """ RPC = "rpc" - """Enable the RPC application to control the local user's Discord client. + """Enables the RPC application to control the local user's Discord client. !!! note You must be whitelisted to use this scope. """ RPC_API = "rpc.api" - """Enable the RPC application to access the RPC API as the local user. + """Enables the RPC application to access the RPC API as the local user. !!! note You must be whitelisted to use this scope. """ RPC_MESSAGES_READ = "messages.read" - """Enable the RPC application to read messages from all channels the user is in.""" + """Enables the RPC application to read messages from all channels the user is in.""" RPC_NOTIFICATIONS_READ = "rpc.notifications.read" - """Enable the RPC application to read from all channels the user is in. + """Enables the RPC application to read from all channels the user is in. !!! note You must be whitelisted to use this scope. @@ -175,7 +179,7 @@ class ConnectionVisibility(int, enum.Enum): """Describes who can see a connection with a third party account.""" NONE = 0 - """Only you can see the connection.""" + """Implies that only you can see the corresponding connection.""" EVERYONE = 1 """Everyone can see the connection.""" @@ -204,7 +208,7 @@ class OwnConnection: is_revoked: bool = attr.ib( eq=False, hash=False, ) - """Whether the connection has been revoked.""" + """`True` if the connection has been revoked.""" integrations: typing.Sequence[guilds.PartialIntegration] = attr.ib( eq=False, hash=False, @@ -212,13 +216,13 @@ class OwnConnection: """A sequence of the partial guild integration objects this connection has.""" is_verified: bool = attr.ib(eq=False, hash=False) - """Whether the connection has been verified.""" + """`True` if the connection has been verified.""" - is_friend_syncing: bool = attr.ib(eq=False, hash=False) - """Whether friends should be added based on this connection.""" + is_friend_sync_enabled: bool = attr.ib(eq=False, hash=False) + """`True` if friends should be added based on this connection.""" - is_showing_activity: bool = attr.ib(eq=False, hash=False) - """Whether this connection's activities are shown in the user's presence.""" + is_activity_visible: bool = attr.ib(eq=False, hash=False) + """`True` if this connection's activities are shown in the user's presence.""" visibility: ConnectionVisibility = attr.ib(eq=False, hash=False, repr=True) """The visibility of the connection.""" @@ -229,10 +233,10 @@ class OwnGuild(guilds.PartialGuild): """Represents a user bound partial guild object.""" is_owner: bool = attr.ib(eq=False, hash=False, repr=True) - """Whether the current user owns this guild.""" + """`True` when the current user owns this guild.""" my_permissions: permissions_.Permission = attr.ib(eq=False, hash=False) - """The guild level permissions that apply to the current user or bot.""" + """The guild-level permissions that apply to the current user or bot.""" @enum.unique @@ -256,14 +260,15 @@ class TeamMember(bases.Entity): permissions: typing.Set[str] = attr.ib(eq=False, hash=False) """This member's permissions within a team. - Will always be `["*"]` until Discord starts using this. + At the time of writing, this will always be a set of one `str`, which + will always be `"*"`. This may change in the future, however. """ team_id: snowflake.Snowflake = attr.ib(eq=True, hash=True, repr=True) """The ID of the team this member belongs to.""" user: users.User = attr.ib(eq=True, hash=True, repr=True) - """The user object of this team member.""" + """The user representation of this team member.""" @attr.s(eq=True, hash=True, init=False, kw_only=True, slots=True) @@ -271,17 +276,30 @@ class Team(bases.Entity, bases.Unique): """Represents a development team, along with all its members.""" icon_hash: typing.Optional[str] = attr.ib(eq=False, hash=False) - """The hash of this team's icon, if set.""" + """The CDN hash of this team's icon. + + If no icon is provided, this will be `None`. + """ members: typing.Mapping[snowflake.Snowflake, TeamMember] = attr.ib(eq=False, hash=False) - """The member's that belong to this team.""" + """A mapping containing each member in this team. + + The mapping maps keys containing the member's ID to values containing the + member object. + """ owner_user_id: snowflake.Snowflake = attr.ib(eq=False, hash=False, repr=True) """The ID of this team's owner.""" @property def icon_url(self) -> typing.Optional[str]: - """URL of this team's icon, if set.""" + """URL for this team's icon. + + Returns + ------- + str or None + The URL, or `None` if no icon exists. + """ return self.format_icon_url() def format_icon_url(self, *, format_: str = "png", size: int = 4096) -> typing.Optional[str]: @@ -298,13 +316,14 @@ def format_icon_url(self, *, format_: str = "png", size: int = 4096) -> typing.O Returns ------- - str | None - The string URL. + str or None + The string URL, or `None` if no icon exists. Raises ------ ValueError - If `size` is not a power of two or not between 16 and 4096. + If the size is not an integer power of 2 between 16 and 4096 + (inclusive). """ if self.icon_hash: return cdn.generate_cdn_url("team-icons", str(self.id), self.icon_hash, format_=format_, size=size) @@ -318,32 +337,31 @@ class Application(bases.Entity, bases.Unique): name: str = attr.ib(eq=False, hash=False, repr=True) """The name of this application.""" + # TODO: default to None for consistency? description: str = attr.ib(eq=False, hash=False) - """The description of this application, will be an empty string if undefined.""" + """The description of this application, or an empty string if undefined.""" is_bot_public: typing.Optional[bool] = attr.ib(eq=False, hash=False, repr=True) - """Whether the bot associated with this application is public. + """`True` if the bot associated with this application is public. Will be `None` if this application doesn't have an associated bot. """ is_bot_code_grant_required: typing.Optional[bool] = attr.ib(eq=False, hash=False) - """Whether this application's bot is requiring code grant for invites. + """`True` if this application's bot is requiring code grant for invites. Will be `None` if this application doesn't have a bot. """ - owner: typing.Optional[users.User] = attr.ib( - eq=False, hash=False, repr=True, - ) - """The object of this application's owner. + owner: typing.Optional[users.User] = attr.ib(eq=False, hash=False, repr=True) + """The application's owner. This should always be `None` in application objects retrieved outside Discord's oauth2 flow. """ rpc_origins: typing.Optional[typing.Set[str]] = attr.ib(eq=False, hash=False) - """A collection of this application's rpc origin URLs, if rpc is enabled.""" + """A collection of this application's RPC origin URLs, if RPC is enabled.""" summary: str = attr.ib(eq=False, hash=False) """This summary for this application's primary SKU if it's sold on Discord. @@ -355,12 +373,13 @@ class Application(bases.Entity, bases.Unique): """The base64 encoded key used for the GameSDK's `GetTicket`.""" icon_hash: typing.Optional[str] = attr.ib(eq=False, hash=False) - """The hash of this application's icon, if set.""" + """The CDN hash of this application's icon, if set.""" - team: typing.Optional[Team] = attr.ib( - eq=False, hash=False, - ) - """This application's team if it belongs to one.""" + team: typing.Optional[Team] = attr.ib(eq=False, hash=False) + """The team this application belongs to. + + If the application is not part of a team, this will be `None`. + """ guild_id: typing.Optional[snowflake.Snowflake] = attr.ib(eq=False, hash=False) """The ID of the guild this application is linked to if sold on Discord.""" @@ -369,21 +388,27 @@ class Application(bases.Entity, bases.Unique): """The ID of the primary "Game SKU" of a game that's sold on Discord.""" slug: typing.Optional[str] = attr.ib(eq=False, hash=False) - """The URL slug that links to this application's store page. + """The URL "slug" that is used to point to this application's store page. Only applicable to applications sold on Discord. """ cover_image_hash: typing.Optional[str] = attr.ib(eq=False, hash=False) - """The hash of this application's cover image on it's store, if set.""" + """The CDN's hash of this application's cover image, used on the store.""" @property def icon_url(self) -> typing.Optional[str]: - """URL for this team's icon, if set.""" + """URL for the team's icon, if there is one. + + Returns + ------- + str or None + The URL, or `None` if no icon is set. + """ return self.format_icon_url() def format_icon_url(self, *, format_: str = "png", size: int = 4096) -> typing.Optional[str]: - """Generate the icon URL for this application if set. + """Generate the icon URL for this application. Parameters ---------- @@ -396,13 +421,14 @@ def format_icon_url(self, *, format_: str = "png", size: int = 4096) -> typing.O Returns ------- - str | None - The string URL. + str or None + The string URL, or `None` if no icon is set. Raises ------ ValueError - If `size` is not a power of two or not between 16 and 4096. + If the size is not an integer power of 2 between 16 and 4096 + (inclusive). """ if self.icon_hash: return cdn.generate_cdn_url("application-icons", str(self.id), self.icon_hash, format_=format_, size=size) @@ -410,7 +436,13 @@ def format_icon_url(self, *, format_: str = "png", size: int = 4096) -> typing.O @property def cover_image_url(self) -> typing.Optional[str]: - """URL for this icon's store cover image, if set.""" + """URL for the cover image used on the store. + + Returns + ------- + str or None + The URL, or `None` if no cover image exists. + """ return self.format_cover_image_url() def format_cover_image_url(self, *, format_: str = "png", size: int = 4096) -> typing.Optional[str]: @@ -427,13 +459,14 @@ def format_cover_image_url(self, *, format_: str = "png", size: int = 4096) -> t Returns ------- - str | None - The string URL. + str or None + The URL, or `None` if no cover image exists. Raises ------ ValueError - If `size` is not a power of two or not between 16 and 4096. + If the size is not an integer power of 2 between 16 and 4096 + (inclusive). """ if self.cover_image_hash: return cdn.generate_cdn_url( diff --git a/hikari/net/rest.py b/hikari/net/rest.py index 618a11836d..c3e1fde7c4 100644 --- a/hikari/net/rest.py +++ b/hikari/net/rest.py @@ -116,7 +116,7 @@ def __init__( app: app_.IRESTApp, config: http_settings.HTTPSettings, debug: bool = False, - token: typing.Optional[str], + token: typing.Union[undefined.Undefined, str] = undefined.Undefined(), token_type: typing.Union[undefined.Undefined, str] = undefined.Undefined(), rest_url: typing.Union[undefined.Undefined, str] = undefined.Undefined(), oauth2_url: typing.Union[undefined.Undefined, str] = undefined.Undefined(), @@ -143,10 +143,10 @@ def __init__( self._app = app - if token is None: + if isinstance(token, undefined.Undefined): self._token = None else: - if token_type is undefined.Undefined(): + if isinstance(token_type, undefined.Undefined): token_type = "Bot" self._token = f"{token_type.title()} {token}" if token is not None else None diff --git a/tests/hikari/impl/test_entity_factory.py b/tests/hikari/impl/test_entity_factory.py index 1799299b36..a76754f1af 100644 --- a/tests/hikari/impl/test_entity_factory.py +++ b/tests/hikari/impl/test_entity_factory.py @@ -114,8 +114,8 @@ def test_deserialize_own_connection( assert own_connection.is_revoked is False assert own_connection.integrations == [entity_factory_impl.deserialize_partial_integration(partial_integration)] assert own_connection.is_verified is True - assert own_connection.is_friend_syncing is False - assert own_connection.is_showing_activity is True + assert own_connection.is_friend_sync_enabled is False + assert own_connection.is_activity_visible is True assert own_connection.visibility == applications.ConnectionVisibility.NONE assert isinstance(own_connection, applications.OwnConnection) From f09d68f408d269a2c6fc1a59503da5499d3e532d Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Wed, 3 Jun 2020 19:07:28 +0100 Subject: [PATCH 441/922] Added identifiers in docs for properties. --- docs/documentation.mako | 23 ++++++++++++++++------- hikari/api/app.py | 6 ++---- 2 files changed, 18 insertions(+), 11 deletions(-) diff --git a/docs/documentation.mako b/docs/documentation.mako index 3f2b8bf1a2..a180d676df 100644 --- a/docs/documentation.mako +++ b/docs/documentation.mako @@ -71,6 +71,7 @@ QUAL_MODULE = "module" QUAL_NAMESPACE = "namespace" QUAL_PACKAGE = "package" + QUAL_PROPERTY = "property" QUAL_REF = "ref" QUAL_TYPEHINT = "type hint" QUAL_VAR = "var" @@ -101,19 +102,22 @@ else: qual = dobj.funcdef() - prefix = "" + qual + " " + prefix = "" + qual + "" elif isinstance(dobj, pdoc.Variable): - if dobj.module.name == "typing" or dobj.docstring and dobj.docstring.casefold().startswith(("type hint", "typehint", "type alias")): - prefix = F"{QUAL_TYPEHINT} " + if hasattr(dobj.cls, "obj") and (descriptor := dobj.cls.obj.__dict__.get(dobj.name)) and isinstance(descriptor, property): + prefix = f"{QUAL_PROPERTY}" + + elif dobj.module.name == "typing" or dobj.docstring and dobj.docstring.casefold().startswith(("type hint", "typehint", "type alias")): + prefix = F"{QUAL_TYPEHINT} " elif all(not c.isalpha() or c.isupper() for c in dobj.name): - prefix = f"{QUAL_CONST} " + prefix = f"{QUAL_CONST}" else: - prefix = f"{QUAL_VAR} " + prefix = f"{QUAL_VAR}" elif isinstance(dobj, pdoc.Class): if not hide_ref and dobj.module.name != dobj.obj.__module__: - qual = f"{QUAL_REF} " + qual = f"{QUAL_REF}" else: qual = "" @@ -251,7 +255,7 @@ return_type = get_annotation(v.type_annotation) %>
-
${link(v, anchor=True)}${return_type}
+
${link(v, with_prefixes=True, anchor=True)}${return_type}
${v.docstring | to_html}
@@ -636,6 +640,11 @@
${QUAL_PACKAGE}
Python package that can be imported and can contain sub-modules.
+
${QUAL_PROPERTY}
+
+ Property type. Will always support read operations. +
+
${QUAL_NAMESPACE}
Python namespace package that can contain sub-modules, but is not directly importable.
diff --git a/hikari/api/app.py b/hikari/api/app.py index 2177507f17..11a37b6a1e 100644 --- a/hikari/api/app.py +++ b/hikari/api/app.py @@ -291,10 +291,8 @@ def gateway_shards(self) -> typing.Mapping[int, gateway.Gateway]: "Non-sharded" bots should expect one value to be in this mapping under the shard ID `0`. - ```py - >>> bot.gateway_shards[0].heartbeat_latency - 0.145612141 - ``` + >>> bot.gateway_shards[0].heartbeat_latency + 0.145612141 Returns ------- From ff9508243d096f1e538eb817e9432eeb4b9502a2 Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Wed, 3 Jun 2020 20:05:53 +0100 Subject: [PATCH 442/922] Allowed querystrings to be multidicts. --- hikari/net/http_client.py | 3 ++- hikari/utilities/data_binding.py | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/hikari/net/http_client.py b/hikari/net/http_client.py index bcc1367c8c..33cc932b88 100644 --- a/hikari/net/http_client.py +++ b/hikari/net/http_client.py @@ -30,6 +30,7 @@ import typing import aiohttp.typedefs +import multidict from hikari.net import tracing @@ -221,7 +222,7 @@ async def _perform_request( url: str, headers: aiohttp.typedefs.LooseHeaders, body: typing.Union[aiohttp.FormData, dict, list, None], - query: typing.Dict[str, str], + query: typing.Union[typing.Dict[str, str], multidict.MultiDict[str, str]], ) -> aiohttp.ClientResponse: """Make an HTTP request and return the response. diff --git a/hikari/utilities/data_binding.py b/hikari/utilities/data_binding.py index 8e62142dcd..86c62976c9 100644 --- a/hikari/utilities/data_binding.py +++ b/hikari/utilities/data_binding.py @@ -37,6 +37,7 @@ import typing import aiohttp.typedefs +import multidict from hikari.models import bases from hikari.utilities import undefined @@ -85,7 +86,7 @@ def load_json(_: str) -> typing.Union[JSONArray, JSONObject]: """Convert a JSON string to a Python type.""" -class StringMapBuilder(typing.Dict[str, str]): +class StringMapBuilder(multidict.MultiDict[str, str]): """Helper class used to quickly build query strings or header maps. This will consume any items that are not @@ -94,7 +95,6 @@ class StringMapBuilder(typing.Dict[str, str]): boilerplate needed for generating the headers and query strings for low-level HTTP API interaction, amongst other things. - !!! warn Because this subclasses `dict`, you should not use the index operator to set items on this object. Doing so will skip any From 13932aa4fada24a1d2efde190664f993b94c3c5f Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Thu, 4 Jun 2020 09:31:19 +0100 Subject: [PATCH 443/922] Added base raw event parsing method stubs. --- hikari/impl/event_manager.py | 130 +++++++++++++++++++++++++++++- hikari/impl/event_manager_core.py | 52 ++++++------ hikari/impl/gateway_zookeeper.py | 25 +++--- 3 files changed, 170 insertions(+), 37 deletions(-) diff --git a/hikari/impl/event_manager.py b/hikari/impl/event_manager.py index 11ead2b8d2..f843dd6164 100644 --- a/hikari/impl/event_manager.py +++ b/hikari/impl/event_manager.py @@ -16,14 +16,142 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . -"""Event handling logic.""" +"""Event handling logic for more info.""" from __future__ import annotations __all__ = ["EventManagerImpl"] +import typing + from hikari.impl import event_manager_core +if typing.TYPE_CHECKING: + from hikari.net import gateway + from hikari.utilities import data_binding + class EventManagerImpl(event_manager_core.EventManagerCore): """Provides event handling logic for Discord events.""" + + async def on_connect(self, shard: gateway.Gateway, payload: data_binding.JSONObject) -> None: + """Handle connection events. + + This is a synthetic event produced by the gateway implementation in + Hikari. + """ + + async def on_disconnect(self, shard: gateway.Gateway, payload: data_binding.JSONObject) -> None: + """Handle disconnection events. + + This is a synthetic event produced by the gateway implementation in + Hikari. + """ + + async def on_ready(self, shard: gateway.Gateway, payload: data_binding.JSONObject) -> None: + """See https://discord.com/developers/docs/topics/gateway#ready for more info.""" + + async def on_resumed(self, shard: gateway.Gateway, payload: data_binding.JSONObject) -> None: + """See https://discord.com/developers/docs/topics/gateway#resumed for more info.""" + + async def on_channel_create(self, shard: gateway.Gateway, payload: data_binding.JSONObject) -> None: + """See https://discord.com/developers/docs/topics/gateway#channel-create for more info.""" + + async def on_channel_update(self, shard: gateway.Gateway, payload: data_binding.JSONObject) -> None: + """See https://discord.com/developers/docs/topics/gateway#channel-update for more info.""" + + async def on_channel_delete(self, shard: gateway.Gateway, payload: data_binding.JSONObject) -> None: + """See https://discord.com/developers/docs/topics/gateway#channel-delete for more info.""" + + async def on_channel_pins_update(self, shard: gateway.Gateway, payload: data_binding.JSONObject) -> None: + """See https://discord.com/developers/docs/topics/gateway#channel-pins-update for more info.""" + + async def on_guild_create(self, shard: gateway.Gateway, payload: data_binding.JSONObject) -> None: + """See https://discord.com/developers/docs/topics/gateway#guild-create for more info.""" + + async def on_guild_update(self, shard: gateway.Gateway, payload: data_binding.JSONObject) -> None: + """See https://discord.com/developers/docs/topics/gateway#guild-update for more info.""" + + async def on_guild_delete(self, shard: gateway.Gateway, payload: data_binding.JSONObject) -> None: + """See https://discord.com/developers/docs/topics/gateway#guild-delete for more info.""" + + async def on_guild_ban_add(self, shard: gateway.Gateway, payload: data_binding.JSONObject) -> None: + """See https://discord.com/developers/docs/topics/gateway#guild-ban-add for more info.""" + + async def on_guild_ban_remove(self, shard: gateway.Gateway, payload: data_binding.JSONObject) -> None: + """See https://discord.com/developers/docs/topics/gateway#guild-ban-remove for more info.""" + + async def on_guild_emojis_update(self, shard: gateway.Gateway, payload: data_binding.JSONObject) -> None: + """See https://discord.com/developers/docs/topics/gateway#guild-emojis-update for more info.""" + + async def on_guild_integrations_update(self, shard: gateway.Gateway, payload: data_binding.JSONObject) -> None: + """See https://discord.com/developers/docs/topics/gateway#guild-integrations-update for more info.""" + + async def on_guild_member_add(self, shard: gateway.Gateway, payload: data_binding.JSONObject) -> None: + """See https://discord.com/developers/docs/topics/gateway#guild-member-add for more info.""" + + async def on_guild_member_remove(self, shard: gateway.Gateway, payload: data_binding.JSONObject) -> None: + """See https://discord.com/developers/docs/topics/gateway#guild-member-remove for more info.""" + + async def on_guild_member_update(self, shard: gateway.Gateway, payload: data_binding.JSONObject) -> None: + """See https://discord.com/developers/docs/topics/gateway#guild-member-update for more info.""" + + async def on_guild_members_chunk(self, shard: gateway.Gateway, payload: data_binding.JSONObject) -> None: + """See https://discord.com/developers/docs/topics/gateway#guild-members-chunk for more info.""" + + async def on_guild_role_create(self, shard: gateway.Gateway, payload: data_binding.JSONObject) -> None: + """See https://discord.com/developers/docs/topics/gateway#guild-role-create for more info.""" + + async def on_guild_role_update(self, shard: gateway.Gateway, payload: data_binding.JSONObject) -> None: + """See https://discord.com/developers/docs/topics/gateway#guild-role-update for more info.""" + + async def on_guild_role_delete(self, shard: gateway.Gateway, payload: data_binding.JSONObject) -> None: + """See https://discord.com/developers/docs/topics/gateway#guild-role-delete for more info.""" + + async def on_invite_create(self, shard: gateway.Gateway, payload: data_binding.JSONObject) -> None: + """See https://discord.com/developers/docs/topics/gateway#invite-create for more info.""" + + async def on_invite_delete(self, shard: gateway.Gateway, payload: data_binding.JSONObject) -> None: + """See https://discord.com/developers/docs/topics/gateway#invite-delete for more info.""" + + async def on_message_create(self, shard: gateway.Gateway, payload: data_binding.JSONObject) -> None: + """See https://discord.com/developers/docs/topics/gateway#message-create for more info.""" + + async def on_message_update(self, shard: gateway.Gateway, payload: data_binding.JSONObject) -> None: + """See https://discord.com/developers/docs/topics/gateway#message-update for more info.""" + + async def on_message_delete(self, shard: gateway.Gateway, payload: data_binding.JSONObject) -> None: + """See https://discord.com/developers/docs/topics/gateway#message-delete for more info.""" + + async def on_message_delete_bulk(self, shard: gateway.Gateway, payload: data_binding.JSONObject) -> None: + """See https://discord.com/developers/docs/topics/gateway#message-delete-bulk for more info.""" + + async def on_message_reaction_add(self, shard: gateway.Gateway, payload: data_binding.JSONObject) -> None: + """See https://discord.com/developers/docs/topics/gateway#message-reaction-add for more info.""" + + async def on_message_reaction_remove(self, shard: gateway.Gateway, payload: data_binding.JSONObject) -> None: + """See https://discord.com/developers/docs/topics/gateway#message-reaction-remove for more info.""" + + async def on_message_reaction_remove_all(self, shard: gateway.Gateway, payload: data_binding.JSONObject) -> None: + """See https://discord.com/developers/docs/topics/gateway#message-reaction-remove-all for more info.""" + + async def on_message_reaction_remove_emoji(self, shard: gateway.Gateway, payload: data_binding.JSONObject) -> None: + """See https://discord.com/developers/docs/topics/gateway#message-reaction-remove-emoji for more info.""" + + async def on_presence_update(self, shard: gateway.Gateway, payload: data_binding.JSONObject) -> None: + """See https://discord.com/developers/docs/topics/gateway#presence-update for more info.""" + + async def on_typing_start(self, shard: gateway.Gateway, payload: data_binding.JSONObject) -> None: + """See https://discord.com/developers/docs/topics/gateway#typing-start for more info.""" + + async def on_user_update(self, shard: gateway.Gateway, payload: data_binding.JSONObject) -> None: + """See https://discord.com/developers/docs/topics/gateway#user-update for more info.""" + + async def on_voice_state_update(self, shard: gateway.Gateway, payload: data_binding.JSONObject) -> None: + """See https://discord.com/developers/docs/topics/gateway#voice-state-update for more info.""" + + async def on_voice_server_update(self, shard: gateway.Gateway, payload: data_binding.JSONObject) -> None: + """See https://discord.com/developers/docs/topics/gateway#voice-server-update for more info.""" + + async def on_webhooks_update(self, shard: gateway.Gateway, payload: data_binding.JSONObject) -> None: + """See https://discord.com/developers/docs/topics/gateway#webhooks-update for more info.""" diff --git a/hikari/impl/event_manager_core.py b/hikari/impl/event_manager_core.py index a6939c16fc..4bcb9608b6 100644 --- a/hikari/impl/event_manager_core.py +++ b/hikari/impl/event_manager_core.py @@ -53,7 +53,7 @@ class EventManagerCore(event_dispatcher.IEventDispatcher, event_consumer.IEventConsumer): """Provides functionality to consume and dispatch events. - Specific event handlers should be in functions named `_on_xxx` where `xxx` + Specific event handlers should be in functions named `on_xxx` where `xxx` is the raw event name being dispatched in lower-case. """ @@ -67,35 +67,11 @@ def __init__(self, app: app_.IApp) -> None: def app(self) -> app_.IApp: return self._app - def dispatch(self, event: base.HikariEvent) -> aio.Future[typing.Any]: - if not isinstance(event, base.HikariEvent): - raise TypeError(f"events must be subclasses of HikariEvent, not {type(event).__name__}") - - # We only need to iterate through the MRO until we hit HikariEvent, as - # anything after that is random garbage we don't care about, as they do - # not describe event types. This improves efficiency as well. - mro = type(event).mro() - - tasks = [] - - for cls in mro[: mro.index(base.HikariEvent) + 1]: - cls: typing.Type[_EventT] - - if cls in self._listeners: - for callback in self._listeners[cls]: - tasks.append(self._invoke_callback(callback, event)) - - if cls in self._waiters: - for predicate, future in self._waiters[cls]: - tasks.append(self._test_waiter(cls, event, predicate, future)) - - return asyncio.gather(*tasks) if tasks else aio.completed_future() - async def consume_raw_event( self, shard: gateway.Gateway, event_name: str, payload: data_binding.JSONObject ) -> None: try: - callback = getattr(self, "_on_" + event_name.lower()) + callback = getattr(self, "on_" + event_name.lower()) await callback(shard, payload) except AttributeError: self.logger.debug("ignoring unknown event %s", event_name) @@ -209,3 +185,27 @@ async def _invoke_callback(self, callback: _CallbackT, event: _EventT) -> None: self.logger.error("an exception occurred handling an event, but it has been ignored", exc_info=ex) else: await self.dispatch(other.ExceptionEvent(app=self._app, exception=ex, event=event, callback=callback)) + + def dispatch(self, event: base.HikariEvent) -> aio.Future[typing.Any]: + if not isinstance(event, base.HikariEvent): + raise TypeError(f"Events must be subclasses of {base.HikariEvent.__name__}, not {type(event).__name__}") + + # We only need to iterate through the MRO until we hit HikariEvent, as + # anything after that is random garbage we don't care about, as they do + # not describe event types. This improves efficiency as well. + mro = type(event).mro() + + tasks = [] + + for cls in mro[: mro.index(base.HikariEvent) + 1]: + cls: typing.Type[_EventT] + + if cls in self._listeners: + for callback in self._listeners[cls]: + tasks.append(self._invoke_callback(callback, event)) + + if cls in self._waiters: + for predicate, future in self._waiters[cls]: + tasks.append(self._test_waiter(cls, event, predicate, future)) + + return asyncio.gather(*tasks) if tasks else aio.completed_future() diff --git a/hikari/impl/gateway_zookeeper.py b/hikari/impl/gateway_zookeeper.py index 89c79206f8..cae39a6b02 100644 --- a/hikari/impl/gateway_zookeeper.py +++ b/hikari/impl/gateway_zookeeper.py @@ -98,6 +98,9 @@ async def start(self) -> None: await self._init() self._request_close_event.clear() + + await self._maybe_dispatch(other.StartingEvent()) + self.logger.info("starting %s shard(s)", len(self._shards)) start_time = time.perf_counter() @@ -146,10 +149,7 @@ async def start(self) -> None: ) self.logger.info("started %s shard(s) in approx %.2fs", len(self._shards), finish_time - start_time) - if hasattr(self, "event_dispatcher") and isinstance( - self.event_dispatcher, event_dispatcher.IEventDispatcher - ): - await self.event_dispatcher.dispatch(other.StartedEvent()) + await self._maybe_dispatch(other.StartedEvent()) async def join(self) -> None: if self._gather_task is not None: @@ -211,14 +211,14 @@ async def update_presence( idle_since: typing.Union[undefined.Undefined, datetime.datetime] = undefined.Undefined(), is_afk: typing.Union[undefined.Undefined, bool] = undefined.Undefined(), ) -> None: - coros = ( - s.update_presence(status=status, activity=activity, idle_since=idle_since, is_afk=is_afk) - for s in self._shards.values() - if s.is_alive + await asyncio.gather( + *( + s.update_presence(status=status, activity=activity, idle_since=idle_since, is_afk=is_afk) + for s in self._shards.values() + if s.is_alive + ) ) - await asyncio.gather(*coros) - async def _init(self): version = _about.__version__ # noinspection PyTypeChecker @@ -308,3 +308,8 @@ async def _gather(self) -> None: async def _run(self) -> None: await self.start() await self.join() + + async def _maybe_dispatch(self, event) -> None: + if hasattr(self, "event_dispatcher"): + # noinspection PyUnresolvedReferences + await self.event_dispatcher.dispatch(event) From 9ee10bd16dd42cae6e1139a2c4e7de6c8f82d457 Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Thu, 4 Jun 2020 10:53:32 +0100 Subject: [PATCH 444/922] Added auto-enabling of logging for bots and fixed BotImpl docs. --- hikari/api/app.py | 2 +- hikari/api/cache.py | 2 +- hikari/impl/bot.py | 174 +++++++++++++++++++++++++++++-- hikari/impl/gateway_zookeeper.py | 112 ++++++++++++++++---- hikari/net/gateway.py | 26 +++-- 5 files changed, 278 insertions(+), 38 deletions(-) diff --git a/hikari/api/app.py b/hikari/api/app.py index 11a37b6a1e..d4879fde24 100644 --- a/hikari/api/app.py +++ b/hikari/api/app.py @@ -304,7 +304,7 @@ def gateway_shards(self) -> typing.Mapping[int, gateway.Gateway]: @property @abc.abstractmethod def gateway_shard_count(self) -> int: - """Number of shards in the entire distributed application. + """Count the number of shards in the entire distributed application. If the shards have not started, and auto-sharding is in-place, then it is acceptable for this to return `0`. When the application is running, diff --git a/hikari/api/cache.py b/hikari/api/cache.py index 00b1b411bf..c5a58c6a26 100644 --- a/hikari/api/cache.py +++ b/hikari/api/cache.py @@ -27,7 +27,7 @@ class ICache(component.IComponent, abc.ABC): - """Interface describing the operations a cache component should provide + """Interface describing the operations a cache component should provide. This will be used by the gateway and REST API to cache specific types of objects that the application should attempt to remember for later, depending diff --git a/hikari/impl/bot.py b/hikari/impl/bot.py index 703ed85825..c98afd312b 100644 --- a/hikari/impl/bot.py +++ b/hikari/impl/bot.py @@ -22,7 +22,12 @@ __all__ = ["BotImpl"] +import inspect import logging +import os +import platform +import sys + import typing from concurrent import futures @@ -49,27 +54,121 @@ class BotImpl(gateway_zookeeper.AbstractGatewayZookeeper, app.IBot): + """Implementation of an auto-sharded bot application. + + Parameters + ---------- + config : hikari.utilities.undefined.Undefined or hikari.net.http_settings.HTTPSettings + Optional aiohttp settings to apply to the REST components, gateway + shards, and voice websockets. If undefined, then sane defaults are used. + debug : bool + Defaulting to `False`, if `True`, then each payload sent and received + on the gateway will be dumped to debug logs, and every REST API request + and response will also be dumped to logs. This will provide useful + debugging context at the cost of performance. Generally you do not + need to enable this. + gateway_compression : bool + Defaulting to `True`, if `True`, then zlib transport compression is used + for each shard connection. If `False`, no compression is used. + gateway_version : int + The version of the gateway to connect to. At the time of writing, + only version `6` and version `7` (undocumented development release) + are supported. This defaults to using v6. + initial_activity : hikari.models.presences.OwnActivity or None or hikari.utilities.undefined.Undefined + The initial activity to have on each shard. + initial_activity : hikari.models.presences.PresenceStatus or hikari.utilities.undefined.Undefined + The initial status to have on each shard. + initial_idle_since : datetime.datetime or None or hikari.utilities.undefined.Undefined + The initial time to show as being idle since, or `None` if not idle, + for each shard. + initial_idle_since : bool or hikari.utilities.undefined.Undefined + If `True`, each shard will appear as being AFK on startup. If `False`, + each shard will appear as _not_ being AFK. + intents : hikari.models.intents.Intent or None + The intents to use for each shard. If `None`, then no intents are + passed. Note that on the version `7` gateway, this will cause an + immediate connection close with an error code. + large_threshold : int + The number of members that need to be in a guild for the guild to be + considered large. Defaults to the maximum, which is `250`. + logging_level : str or None + If not `None`, then this will be the logging level set if you have not + enabled logging already. In this case, it should be a valid + `logging` level that can be passed to `logging.basicConfig`. If you have + already initialized logging, then this is irrelevant and this + parameter can be safely ignored. If you set this to `None`, then no + logging will initialize if you have a reason to not use any logging + or simply wish to initialize it in your own time instead. + rest_version : int + The version of the REST API to connect to. At the time of writing, + only version `6` and version `7` (undocumented development release) + are supported. This defaults to v6. + shard_ids : typing.Set[int] or undefined.Undefined + A set of every shard ID that should be created and started on startup. + If left undefined along with `shard_count`, then auto-sharding is used + instead, which is the default. + shard_count : int or undefined.Undefined + The number of shards in the entire application. If left undefined along + with `shard_ids`, then auto-sharding is used instead, which is the + default. + token : str + The bot token to use. This should not start with a prefix such as + `Bot `, but instead only contain the token itself. + + !!! note + The default parameters for `shard_ids` and `shard_count` are marked as + undefined. When both of these are left to the default value, the + application will use the Discord-provided recommendation for the number + of shards to start. + + If only one of these two parameters are specified, expect a `TypeError` + to be raised. + + Likewise, all shard_ids must be greater-than or equal-to `0`, and + less than `shard_count` to be valid. Failing to provide valid + values will result in a `ValueError` being raised. + + !!! note + If all four of `initial_activity`, `initial_idle_since`, + `initial_is_afk`, and `initial_status` are not defined and left to their + default values, then the presence will not be _updated_ on startup + at all. + + Raises + ------ + TypeError + If sharding information is not specified correctly. + ValueError + If sharding information is provided, but is unfeasible or invalid. + """ + def __init__( self, *, config: typing.Union[undefined.Undefined, http_settings_.HTTPSettings] = undefined.Undefined(), debug: bool = False, + gateway_compression: bool = True, gateway_version: int = 6, - initial_activity: typing.Optional[presences.OwnActivity] = None, - initial_idle_since: typing.Optional[datetime.datetime] = None, - initial_is_afk: bool = False, - initial_status: presences.PresenceStatus = presences.PresenceStatus.ONLINE, + initial_activity: typing.Union[undefined.Undefined, presences.OwnActivity, None] = undefined.Undefined(), + initial_idle_since: typing.Union[undefined.Undefined, datetime.datetime, None] = undefined.Undefined(), + initial_is_afk: typing.Union[undefined.Undefined, bool] = undefined.Undefined(), + initial_status: typing.Union[undefined.Undefined, presences.PresenceStatus] = undefined.Undefined(), intents: typing.Optional[intents_.Intent] = None, large_threshold: int = 250, + logging_level: typing.Optional[str] = "INFO", rest_version: int = 6, rest_url: typing.Union[undefined.Undefined, str] = undefined.Undefined(), shard_ids: typing.Optional[typing.Set[int]], shard_count: typing.Optional[int], token: str, - use_compression: bool = True, ): self._logger = klass.get_logger(self) + # If logging is already configured, then this does nothing. + if logging_level is not None: + logging.basicConfig(level=logging_level, format=self.__get_logging_format()) + self.__print_banner() + config = http_settings_.HTTPSettings() if isinstance(config, undefined.Undefined) else config self._cache = cache_impl.CacheImpl(app=self) @@ -99,7 +198,7 @@ def __init__( shard_ids=shard_ids, shard_count=shard_count, token=token, - use_compression=use_compression, + compression=gateway_compression, version=gateway_version, ) @@ -157,3 +256,66 @@ async def close(self) -> None: async def _fetch_gateway_recommendations(self) -> gateway_models.GatewayBot: return await self.rest.fetch_gateway_bot() + + def __print_banner(self): + from hikari import _about + + version = _about.__version__ + # noinspection PyTypeChecker + path = os.path.abspath(os.path.dirname(inspect.getsourcefile(_about))) + python_implementation = platform.python_implementation() + python_version = platform.python_version() + operating_system = " ".join((platform.system(), *platform.architecture())) + python_compiler = platform.python_compiler() + + copyright_str = f"{_about.__copyright__}, licensed under {_about.__license__}" + version_str = f"hikari v{version} (installed in {path})" + impl_str = f"Running on {python_implementation} v{python_version}, {python_compiler}, ({operating_system})" + doc_line = f"Documentation: {_about.__docs__}" + guild_line = f"Support: {_about.__discord_invite__}" + line_len = max(len(version_str), len(copyright_str), len(impl_str), len(guild_line), len(doc_line)) + + copyright_str = f"|* {copyright_str:^{line_len}} *|" + impl_str = f"|* {impl_str:^{line_len}} *|" + version_str = f"|* {version_str:^{line_len}} *|" + doc_line = f"|* {doc_line:^{line_len}} *|" + guild_line = f"|* {guild_line:^{line_len}} *|" + line_len = max(len(version_str), len(copyright_str), len(impl_str), len(guild_line), len(doc_line)) - 4 + + top_line = "//" + ("=" * line_len) + r"\\" + bottom_line = r"\\" + ("=" * line_len) + "//" + + self.logger.info( + "\n%s\n%s\n%s\n%s\n%s\n%s\n%s\n", + top_line, + version_str, + copyright_str, + impl_str, + doc_line, + guild_line, + bottom_line, + ) + + @staticmethod + def __get_logging_format(): + # https://github.com/django/django/blob/master/django/core/management/color.py + plat = sys.platform + supported_platform = plat != "Pocket PC" and (plat != "win32" or "ANSICON" in os.environ) + # isatty is not always implemented, #6223. + is_a_tty = hasattr(sys.stdout, "isatty") and sys.stdout.isatty() + # https://youtrack.jetbrains.com/issue/PY-4853 + + if supported_platform and is_a_tty or os.getenv("PYCHARM_HOSTED"): + blue = "\033[1;35m" + gray = "\033[1;37m" + green = "\033[1;32m" + red = "\033[1;31m" + yellow = "\033[1;33m" + default = "\033[0m" + else: + blue = gray = green = red = yellow = default = "" + + return ( + f"{red}%(levelname)4.4s {yellow}%(name)-20.20s {green}#%(lineno)-4d {blue}%(asctime)23.23s " + f"{default}:: {gray}%(message)s{default}" + ) diff --git a/hikari/impl/gateway_zookeeper.py b/hikari/impl/gateway_zookeeper.py index cae39a6b02..296091bf2a 100644 --- a/hikari/impl/gateway_zookeeper.py +++ b/hikari/impl/gateway_zookeeper.py @@ -25,14 +25,10 @@ import asyncio import contextlib import datetime -import inspect -import os -import platform import signal import time import typing -from hikari import _about from hikari.api import app as app_ from hikari.api import event_dispatcher from hikari.events import other @@ -47,23 +43,109 @@ class AbstractGatewayZookeeper(app_.IGatewayZookeeper, abc.ABC): + """Provides keep-alive logic for orchestrating multiple shards. + + Parameters + ---------- + compression : bool + Defaulting to `True`, if `True`, then zlib transport compression is used + for each shard connection. If `False`, no compression is used. + config : hikari.utilities.undefined.Undefined or hikari.net.http_settings.HTTPSettings + Optional aiohttp settings to apply to the created shards. + debug : bool + Defaulting to `False`, if `True`, then each payload sent and received + on the gateway will be dumped to debug logs, and every REST API request + and response will also be dumped to logs. This will provide useful + debugging context at the cost of performance. Generally you do not + need to enable this. + initial_activity : hikari.models.presences.OwnActivity or None or hikari.utilities.undefined.Undefined + The initial activity to have on each shard. + initial_activity : hikari.models.presences.PresenceStatus or hikari.utilities.undefined.Undefined + The initial status to have on each shard. + initial_idle_since : datetime.datetime or None or hikari.utilities.undefined.Undefined + The initial time to show as being idle since, or `None` if not idle, + for each shard. + initial_idle_since : bool or hikari.utilities.undefined.Undefined + If `True`, each shard will appear as being AFK on startup. If `False`, + each shard will appear as _not_ being AFK. + intents : hikari.models.intents.Intent or None + The intents to use for each shard. If `None`, then no intents are + passed. Note that on the version `7` gateway, this will cause an + immediate connection close with an error code. + large_threshold : int + The number of members that need to be in a guild for the guild to be + considered large. Defaults to the maximum, which is `250`. + shard_ids : typing.Set[int] or undefined.Undefined + A set of every shard ID that should be created and started on startup. + If left undefined along with `shard_count`, then auto-sharding is used + instead, which is the default. + shard_count : int or undefined.Undefined + The number of shards in the entire application. If left undefined along + with `shard_ids`, then auto-sharding is used instead, which is the + default. + token : str + The bot token to use. This should not start with a prefix such as + `Bot `, but instead only contain the token itself. + version : int + The version of the gateway to connect to. At the time of writing, + only version `6` and version `7` (undocumented development release) + are supported. This defaults to using v6. + + !!! note + The default parameters for `shard_ids` and `shard_count` are marked as + undefined. When both of these are left to the default value, the + application will use the Discord-provided recommendation for the number + of shards to start. + + If only one of these two parameters are specified, expect a `TypeError` + to be raised. + + Likewise, all shard_ids must be greater-than or equal-to `0`, and + less than `shard_count` to be valid. Failing to provide valid + values will result in a `ValueError` being raised. + + !!! note + If all four of `initial_activity`, `initial_idle_since`, + `initial_is_afk`, and `initial_status` are not defined and left to their + default values, then the presence will not be _updated_ on startup + at all. + + Raises + ------ + TypeError + If sharding information is not specified correctly. + ValueError + If sharding information is provided, but is unfeasible or invalid. + + """ + def __init__( self, *, + compression: bool, config: http_settings.HTTPSettings, debug: bool, - initial_activity: typing.Optional[presences.OwnActivity], - initial_idle_since: typing.Optional[datetime.datetime], - initial_is_afk: bool, - initial_status: presences.PresenceStatus, + initial_activity: typing.Union[undefined.Undefined, presences.OwnActivity, None] = undefined.Undefined(), + initial_idle_since: typing.Union[undefined.Undefined, datetime.datetime, None] = undefined.Undefined(), + initial_is_afk: typing.Union[undefined.Undefined, bool] = undefined.Undefined(), + initial_status: typing.Union[undefined.Undefined, presences.PresenceStatus] = undefined.Undefined(), intents: typing.Optional[intents_.Intent], large_threshold: int, shard_ids: typing.Set[int], shard_count: int, token: str, - use_compression: bool, version: int, ) -> None: + if undefined.Undefined.count(shard_ids, shard_count): + raise TypeError("You must provide values for both shard_ids and shard_count, or neither.") + if not isinstance(shard_ids, undefined.Undefined): + if not shard_ids: + raise ValueError("At least one shard ID must be specified if provided.") + if not all(shard_id >= 0 for shard_id in shard_ids): + raise ValueError("shard_ids must be greater than or equal to 0.") + if not all(shard_id < shard_count for shard_id in shard_ids): + raise ValueError("shard_ids must be less than the total shard_count.") + self._aiohttp_config = config self._debug = debug self._gather_task = None @@ -80,7 +162,7 @@ def __init__( self._shards = {} self._tasks = {} self._token = token - self._use_compression = use_compression + self._use_compression = compression self._version = version @property @@ -220,16 +302,6 @@ async def update_presence( ) async def _init(self): - version = _about.__version__ - # noinspection PyTypeChecker - path = os.path.abspath(os.path.dirname(inspect.getsourcefile(_about))) - py_impl = platform.python_implementation() - py_ver = platform.python_version() - py_compiler = platform.python_compiler() - self.logger.info( - "hikari v%s (installed in %s) (%s %s %s)", version, path, py_impl, py_ver, py_compiler, - ) - gw_recs = await self._fetch_gateway_recommendations() self.logger.info( diff --git a/hikari/net/gateway.py b/hikari/net/gateway.py index 601b3df661..f49af0d04e 100644 --- a/hikari/net/gateway.py +++ b/hikari/net/gateway.py @@ -63,17 +63,17 @@ class Gateway(http_client.HTTPClient, component.IComponent): app : hikari.gateway_dispatcher.IGatewayConsumer The base application. config : hikari.http_settings.HTTPSettings - The aiohttp settings to use for the client session. + The AIOHTTP settings to use for the client session. debug : bool If `True`, each sent and received payload is dumped to the logs. If `False`, only the fact that data has been sent/received will be logged. - initial_activity : hikari.presences.OwnActivity | None + initial_activity : hikari.presences.OwnActivity or None or hikari.utilities.undefined.Undefined The initial activity to appear to have for this shard. - initial_idle_since : datetime.datetime | None + initial_idle_since : datetime.datetime or None or hikari.utilities.undefined.Undefined The datetime to appear to be idle since. - initial_is_afk : bool | None + initial_is_afk : bool or hikari.utilities.undefined.Undefined Whether to appear to be AFK or not on login. - initial_status : hikari.models.presences.PresenceStatus | None + initial_status : hikari.models.presences.PresenceStatus or hikari.utilities.undefined.Undefined The initial status to set on login for the shard. intents : hikari.models.intents.Intent | None Collection of intents to use, or `None` to not use intents at all. @@ -92,6 +92,12 @@ class Gateway(http_client.HTTPClient, component.IComponent): If `True`, then transport compression is enabled. version : int Gateway API version to use. + + !!! note + If all four of `initial_activity`, `initial_idle_since`, + `initial_is_afk`, and `initial_status` are not defined and left to their + default values, then the presence will not be _updated_ on startup + at all. """ @enum.unique @@ -155,10 +161,10 @@ def __init__( app: app_.IGatewayConsumer, config: http_settings.HTTPSettings, debug: bool = False, - initial_activity: typing.Optional[presences.OwnActivity] = None, - initial_idle_since: typing.Optional[datetime.datetime] = None, - initial_is_afk: typing.Optional[bool] = None, - initial_status: typing.Optional[presences.PresenceStatus] = None, + initial_activity: typing.Union[undefined.Undefined, None, presences.OwnActivity] = undefined.Undefined(), + initial_idle_since: typing.Union[undefined.Undefined, None, datetime.datetime] = undefined.Undefined(), + initial_is_afk: typing.Union[undefined.Undefined, bool] = undefined.Undefined(), + initial_status: typing.Union[undefined.Undefined, presences.PresenceStatus] = undefined.Undefined(), intents: typing.Optional[intents_.Intent] = None, large_threshold: int = 250, shard_id: int = 0, @@ -474,7 +480,7 @@ async def _handshake(self) -> None: if self._intents is not None: payload["d"]["intents"] = self._intents - if any(item is not None for item in (self._activity, self._idle_since, self._is_afk, self._status)): + if undefined.Undefined.count(self._activity, self._status, self._idle_since, self._is_afk) != 4: # noinspection PyTypeChecker payload["d"]["presence"] = self._build_presence_payload() From 5170e495d756c4205b9e0b6b2e40c5e3ce6d62d3 Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Thu, 4 Jun 2020 11:17:08 +0100 Subject: [PATCH 445/922] Added logging optimisation for gateway. --- hikari/net/gateway.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/hikari/net/gateway.py b/hikari/net/gateway.py index f49af0d04e..5060da6e91 100644 --- a/hikari/net/gateway.py +++ b/hikari/net/gateway.py @@ -24,6 +24,7 @@ import asyncio import enum +import logging import math import time import typing @@ -628,6 +629,10 @@ def _now() -> float: return time.perf_counter() def _log_debug_payload(self, payload: str, message: str, *args: typing.Any) -> None: + # Prevent logging these payloads if logging isn't enabled. This aids performance a little. + if not self.logger.isEnabledFor(logging.DEBUG): + return + message = f"{message} [seq:%s, session:%s, size:%s]" if self._debug: message = f"{message} with raw payload: %s" From 8d203e841526d85b92eecc0f68fa6ce79917e11d Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Thu, 4 Jun 2020 15:03:04 +0100 Subject: [PATCH 446/922] Updated auditlog docs. --- docs/documentation.mako | 5 +++++ hikari/models/audit_logs.py | 30 +++++++++++++++++++++--------- 2 files changed, 26 insertions(+), 9 deletions(-) diff --git a/docs/documentation.mako b/docs/documentation.mako index a180d676df..99d53a11a8 100644 --- a/docs/documentation.mako +++ b/docs/documentation.mako @@ -266,12 +266,17 @@ return_type = get_annotation(f.return_annotation, '->') example_str = f.funcdef() + f.name + "(" + ", ".join(params) + ")" + return_type + if params and params[0] in ("self", "mcs", "mcls", "metacls"): + params = params[1:] + if len(params) > 4 or len(example_str) > 70: representation = "\n".join(( f.funcdef() + " " + f.name + "(", *(f" {p}," for p in params), ")" + return_type + ": ..." )) + elif params: + representation = f"{f.funcdef()} {f.name}({', '.join(params)}){return_type}: ..." else: representation = f"{f.funcdef()} {f.name}(){return_type}: ..." diff --git a/hikari/models/audit_logs.py b/hikari/models/audit_logs.py index 727ef17562..7be53fa98a 100644 --- a/hikari/models/audit_logs.py +++ b/hikari/models/audit_logs.py @@ -57,7 +57,8 @@ class AuditLogChangeKey(str, enum.Enum): """Commonly known and documented keys for audit log change objects. Others may exist. These should be expected to default to the raw string - Discord provided us. + Discord provided us. These are defined for documentation purposes and + can be treated as regular strings for all other purposes. """ NAME = "name" @@ -72,8 +73,6 @@ class AuditLogChangeKey(str, enum.Enum): EXPLICIT_CONTENT_FILTER = "explicit_content_filter" DEFAULT_MESSAGE_NOTIFICATIONS = "notifications" VANITY_URL_CODE = "vanity_url_code" - ADD_ROLE_TO_MEMBER = "$add" - REMOVE_ROLE_FROM_MEMBER = "$remove" PRUNE_DELETE_DAYS = "prune_delete_days" WIDGET_ENABLED = "widget_enabled" WIDGET_CHANNEL_ID = "widget_channel_id" @@ -108,6 +107,10 @@ class AuditLogChangeKey(str, enum.Enum): RATE_LIMIT_PER_USER = "rate_limit_per_user" SYSTEM_CHANNEL_ID = "system_channel_id" + # Who needs consistency? + ADD_ROLE_TO_MEMBER = "$add" + REMOVE_ROLE_FROM_MEMBER = "$remove" + COLOUR = COLOR """Alias for "COLOR""" @@ -211,7 +214,7 @@ class MessagePinEntryInfo(BaseAuditLogEntryInfo): @attr.s(eq=True, hash=False, init=False, kw_only=True, slots=True) class MemberPruneEntryInfo(BaseAuditLogEntryInfo): - """Represents the extra information attached to guild prune log entries.""" + """Extra information attached to guild prune log entries.""" delete_member_days: datetime.timedelta = attr.ib(repr=True) """The timedelta of how many days members were pruned for inactivity based on.""" @@ -222,7 +225,7 @@ class MemberPruneEntryInfo(BaseAuditLogEntryInfo): @attr.s(eq=True, hash=False, init=False, kw_only=True, slots=True) class MessageBulkDeleteEntryInfo(BaseAuditLogEntryInfo): - """Represents extra information for the message bulk delete audit entry.""" + """Extra information for the message bulk delete audit entry.""" count: int = attr.ib(repr=True) """The amount of messages that were deleted.""" @@ -230,7 +233,7 @@ class MessageBulkDeleteEntryInfo(BaseAuditLogEntryInfo): @attr.s(eq=True, hash=False, init=False, kw_only=True, slots=True) class MessageDeleteEntryInfo(MessageBulkDeleteEntryInfo): - """Represents extra information attached to the message delete audit entry.""" + """Extra information attached to the message delete audit entry.""" channel_id: snowflake.Snowflake = attr.ib(repr=True) """The guild text based channel where these message(s) were deleted.""" @@ -238,7 +241,7 @@ class MessageDeleteEntryInfo(MessageBulkDeleteEntryInfo): @attr.s(eq=True, hash=False, init=False, kw_only=True, slots=True) class MemberDisconnectEntryInfo(BaseAuditLogEntryInfo): - """Represents extra information for the voice chat member disconnect entry.""" + """Extra information for the voice chat member disconnect entry.""" count: int = attr.ib(repr=True) """The amount of members who were disconnected from voice in this entry.""" @@ -246,14 +249,20 @@ class MemberDisconnectEntryInfo(BaseAuditLogEntryInfo): @attr.s(eq=True, hash=False, init=False, kw_only=True, slots=True) class MemberMoveEntryInfo(MemberDisconnectEntryInfo): - """Represents extra information for the voice chat based member move entry.""" + """Extra information for the voice chat based member move entry.""" channel_id: snowflake.Snowflake = attr.ib(repr=True) """The amount of members who were disconnected from voice in this entry.""" class UnrecognisedAuditLogEntryInfo(BaseAuditLogEntryInfo): - """Represents any audit log entry options that haven't been implemented. + """Audit log entry options that haven't been implemented in the library. + + The attributes on this object are undocumented and dynamic. + + Example + ------- + >>> entry_info.foobar.baz !!! note This model has no slots and will have arbitrary undocumented attributes @@ -306,3 +315,6 @@ class AuditLog: def __iter__(self) -> typing.Iterable[AuditLogEntry]: return self.entries.values() + + def __len__(self): + return len(self.entries) From bfbed2681eec43e92e512ab5df18b14066ecb6d1 Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Thu, 4 Jun 2020 15:11:32 +0100 Subject: [PATCH 447/922] Documented bases.py --- hikari/models/bases.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hikari/models/bases.py b/hikari/models/bases.py index e63980ae09..02c97d9a01 100644 --- a/hikari/models/bases.py +++ b/hikari/models/bases.py @@ -74,4 +74,4 @@ def __int__(self) -> int: UniqueObject = typing.Union[Unique, snowflake.Snowflake, int, str] -"""A unique object type-hint.""" +"""Type hint representing a unique object entity.""" From 1a76637528271091d0b052cc1d11ffdf8e7fca58 Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Fri, 5 Jun 2020 11:07:01 +0100 Subject: [PATCH 448/922] Updated pytest and pytest-randomly --- dev-requirements.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dev-requirements.txt b/dev-requirements.txt index 77369fae13..5ad1e9e1e4 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -7,10 +7,10 @@ virtualenv==20.0.21 pylint==2.5.2 pylint-json2html-v2~=0.2.2 pylint-junit~=0.2.0 -pytest~=5.4.2 +pytest~=5.4.3 pytest-asyncio~=0.12.0 pytest-cov~=2.9.0 -pytest-randomly~=3.3.1 +pytest-randomly~=3.4.0 pytest-testdox~=1.2.1 pytest-xdist~=1.32.0 mock~=4.0.2 From e93d6105002184916e9c24fac241e357e91ce881 Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Fri, 5 Jun 2020 11:13:58 +0100 Subject: [PATCH 449/922] Fixed non-awaited task in ratelimit test. --- tests/hikari/__init__.py | 10 +++++----- tests/hikari/net/test_ratelimits.py | 12 ++++++++---- 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/tests/hikari/__init__.py b/tests/hikari/__init__.py index 2d3e6ef370..47513b5a29 100644 --- a/tests/hikari/__init__.py +++ b/tests/hikari/__init__.py @@ -26,12 +26,12 @@ def _new_event_loop(): loop = _real_new_event_loop() loop.set_debug(True) - return loop + with contextlib.suppress(AttributeError): + # provisional since py37 + sys.set_coroutine_origin_tracking_depth(20) -asyncio.new_event_loop = _new_event_loop + return loop -with contextlib.suppress(AttributeError): - # provisional since py37 - sys.set_coroutine_origin_tracking_depth(20) +asyncio.new_event_loop = _new_event_loop diff --git a/tests/hikari/net/test_ratelimits.py b/tests/hikari/net/test_ratelimits.py index 6fa065060b..f07cba5b12 100644 --- a/tests/hikari/net/test_ratelimits.py +++ b/tests/hikari/net/test_ratelimits.py @@ -214,12 +214,16 @@ async def test_task_scheduled_if_rate_limited_and_throttle_task_is_None(self, ra ratelimiter.throttle = mock.AsyncMock() ratelimiter.is_rate_limited = mock.MagicMock(return_value=True) - ratelimiter.acquire() - assert ratelimiter.throttle_task is not None + task = ratelimiter.acquire() + try: + assert ratelimiter.throttle_task is not None + + await asyncio.sleep(0.01) - await asyncio.sleep(0.01) + ratelimiter.throttle.assert_called() + finally: + task.cancel() - 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): From 5b6f8534fe77aa89fa7a52aaad96652ed8bfa4ea Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Fri, 5 Jun 2020 11:23:57 +0100 Subject: [PATCH 450/922] Updated README --- README.md | 13 ++++++------- tests/hikari/net/test_ratelimits.py | 1 - 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index a1dea046a4..22de6c7d5a 100644 --- a/README.md +++ b/README.md @@ -13,16 +13,17 @@ and the hope that it will be extendable and reusable, rather than an obstacle. ```py import hikari +from hikari.events.message import MessageCreateEvent -bot = hikari.StatelessBot(token="...") +bot = hikari.Bot(token="...") -@bot.event(hikari.MessageCreateEvent) +@bot.event(MessageCreateEvent) async def ping(event): # If a non-bot user sends a message "hk.ping", respond with "Pong!" if not event.author.is_bot and event.content.startswith("hk.ping"): - await event.reply(content="Pong!") + await event.reply("Pong!") bot.run() @@ -94,15 +95,13 @@ to utilize these components as a black box where necessary. to the original format of information provided by Discord as possible ensures that minimal changes are required when a breaking API design is introduced. This reduces the amount of stuff you need to fix in your applications as a result. -- RESTSession only API functionality. Want to write a web dashboard? Feel free to just reuse the - RESTSession client components to achieve that! +- Standalone REST client. Not writing a bot, but need to use the API anyway? Simply + initialize a `hikari.RESTClient` and away you go. ### Stuff coming soon - Optional, optimised C implementations of internals to give large applications a well-deserved performance boost. -- Voice support. -- Stateful bot support (intents will be supported as first-class citizens). ### Planned extension modules for the future diff --git a/tests/hikari/net/test_ratelimits.py b/tests/hikari/net/test_ratelimits.py index f07cba5b12..d28288496e 100644 --- a/tests/hikari/net/test_ratelimits.py +++ b/tests/hikari/net/test_ratelimits.py @@ -224,7 +224,6 @@ async def test_task_scheduled_if_rate_limited_and_throttle_task_is_None(self, ra finally: task.cancel() - @pytest.mark.asyncio async def test_task_not_scheduled_if_rate_limited_and_throttle_task_not_None(self, ratelimiter, event_loop): ratelimiter.drip = mock.MagicMock() From fa0fd3351b705b1f8b45334585b4ad7da3b407d1 Mon Sep 17 00:00:00 2001 From: Faster Speeding Date: Wed, 3 Jun 2020 04:10:17 +0100 Subject: [PATCH 451/922] Add event model deserialization logic to entity_factory.py --- hikari/api/entity_factory.py | 771 +++++++-- hikari/events/base.py | 6 +- hikari/events/channel.py | 156 +- hikari/events/guild.py | 104 +- hikari/events/message.py | 136 +- hikari/events/other.py | 55 +- hikari/events/voice.py | 17 +- hikari/impl/entity_factory.py | 1444 +++++++++++++---- hikari/models/channels.py | 3 + hikari/models/guilds.py | 28 +- hikari/models/invites.py | 49 +- tests/hikari/impl/test_entity_factory.py | 1873 +++++++++++++++------- 12 files changed, 3344 insertions(+), 1298 deletions(-) diff --git a/hikari/api/entity_factory.py b/hikari/api/entity_factory.py index 4f8eb5be53..53034a0e5e 100644 --- a/hikari/api/entity_factory.py +++ b/hikari/api/entity_factory.py @@ -28,19 +28,24 @@ from hikari.utilities import undefined if typing.TYPE_CHECKING: - from hikari.models import applications - from hikari.models import audit_logs - from hikari.models import channels - from hikari.models import embeds - from hikari.models import emojis - from hikari.models import gateway - from hikari.models import guilds - from hikari.models import invites - from hikari.models import messages - from hikari.models import presences - from hikari.models import users - from hikari.models import voices - from hikari.models import webhooks + from hikari.events import channel as channel_events + from hikari.events import guild as guild_events + from hikari.events import message as message_events + from hikari.events import other as other_events + from hikari.events import voice as voice_events + from hikari.models import applications as application_models + from hikari.models import audit_logs as audit_log_models + from hikari.models import channels as channel_models + from hikari.models import embeds as embed_models + from hikari.models import emojis as emoji_models + from hikari.models import gateway as gateway_models + from hikari.models import guilds as guild_models + from hikari.models import invites as invite_models + from hikari.models import messages as message_models + from hikari.models import presences as presence_models + from hikari.models import users as user_models + from hikari.models import voices as voice_models + from hikari.models import webhooks as webhook_models from hikari.utilities import data_binding @@ -50,12 +55,12 @@ class IEntityFactory(component.IComponent, abc.ABC): __slots__ = () - ################ - # APPLICATIONS # - ################ + ###################### + # APPLICATION MODELS # + ###################### @abc.abstractmethod - def deserialize_own_connection(self, payload: data_binding.JSONObject) -> applications.OwnConnection: + def deserialize_own_connection(self, payload: data_binding.JSONObject) -> application_models.OwnConnection: """Parse a raw payload from Discord into an own connection object. Parameters @@ -70,7 +75,7 @@ def deserialize_own_connection(self, payload: data_binding.JSONObject) -> applic """ @abc.abstractmethod - def deserialize_own_guild(self, payload: data_binding.JSONObject) -> applications.OwnGuild: + def deserialize_own_guild(self, payload: data_binding.JSONObject) -> application_models.OwnGuild: """Parse a raw payload from Discord into an own guild object. Parameters @@ -85,7 +90,7 @@ def deserialize_own_guild(self, payload: data_binding.JSONObject) -> application """ @abc.abstractmethod - def deserialize_application(self, payload: data_binding.JSONObject) -> applications.Application: + def deserialize_application(self, payload: data_binding.JSONObject) -> application_models.Application: """Parse a raw payload from Discord into an application object. Parameters @@ -99,12 +104,12 @@ def deserialize_application(self, payload: data_binding.JSONObject) -> applicati The deserialized application object. """ - ############## - # AUDIT_LOGS # - ############## + ##################### + # AUDIT LOGS MODELS # + ##################### @abc.abstractmethod - def deserialize_audit_log(self, payload: data_binding.JSONObject) -> audit_logs.AuditLog: + def deserialize_audit_log(self, payload: data_binding.JSONObject) -> audit_log_models.AuditLog: """Parse a raw payload from Discord into an audit log object. Parameters @@ -118,12 +123,12 @@ def deserialize_audit_log(self, payload: data_binding.JSONObject) -> audit_logs. The deserialized audit log object. """ - ############ - # CHANNELS # - ############ + ################## + # CHANNEL MODELS # + ################## @abc.abstractmethod - def deserialize_permission_overwrite(self, payload: data_binding.JSONObject) -> channels.PermissionOverwrite: + def deserialize_permission_overwrite(self, payload: data_binding.JSONObject) -> channel_models.PermissionOverwrite: """Parse a raw payload from Discord into a permission overwrite object. Parameters @@ -138,7 +143,7 @@ def deserialize_permission_overwrite(self, payload: data_binding.JSONObject) -> """ @abc.abstractmethod - def serialize_permission_overwrite(self, overwrite: channels.PermissionOverwrite) -> data_binding.JSONObject: + def serialize_permission_overwrite(self, overwrite: channel_models.PermissionOverwrite) -> data_binding.JSONObject: """Serialize a permission overwrite object to a json serializable dict. Parameters @@ -153,7 +158,7 @@ def serialize_permission_overwrite(self, overwrite: channels.PermissionOverwrite """ @abc.abstractmethod - def deserialize_partial_channel(self, payload: data_binding.JSONObject) -> channels.PartialChannel: + def deserialize_partial_channel(self, payload: data_binding.JSONObject) -> channel_models.PartialChannel: """Parse a raw payload from Discord into a partial channel object. Parameters @@ -168,7 +173,7 @@ def deserialize_partial_channel(self, payload: data_binding.JSONObject) -> chann """ @abc.abstractmethod - def deserialize_dm_channel(self, payload: data_binding.JSONObject) -> channels.DMChannel: + def deserialize_dm_channel(self, payload: data_binding.JSONObject) -> channel_models.DMChannel: """Parse a raw payload from Discord into a DM channel object. Parameters @@ -183,7 +188,7 @@ def deserialize_dm_channel(self, payload: data_binding.JSONObject) -> channels.D """ @abc.abstractmethod - def deserialize_group_dm_channel(self, payload: data_binding.JSONObject) -> channels.GroupDMChannel: + def deserialize_group_dm_channel(self, payload: data_binding.JSONObject) -> channel_models.GroupDMChannel: """Parse a raw payload from Discord into a group DM channel object. Parameters @@ -198,7 +203,7 @@ def deserialize_group_dm_channel(self, payload: data_binding.JSONObject) -> chan """ @abc.abstractmethod - def deserialize_guild_category(self, payload: data_binding.JSONObject) -> channels.GuildCategory: + def deserialize_guild_category(self, payload: data_binding.JSONObject) -> channel_models.GuildCategory: """Parse a raw payload from Discord into a guild category object. Parameters @@ -213,7 +218,7 @@ def deserialize_guild_category(self, payload: data_binding.JSONObject) -> channe """ @abc.abstractmethod - def deserialize_guild_text_channel(self, payload: data_binding.JSONObject) -> channels.GuildTextChannel: + def deserialize_guild_text_channel(self, payload: data_binding.JSONObject) -> channel_models.GuildTextChannel: """Parse a raw payload from Discord into a guild text channel object. Parameters @@ -228,7 +233,7 @@ def deserialize_guild_text_channel(self, payload: data_binding.JSONObject) -> ch """ @abc.abstractmethod - def deserialize_guild_news_channel(self, payload: data_binding.JSONObject) -> channels.GuildNewsChannel: + def deserialize_guild_news_channel(self, payload: data_binding.JSONObject) -> channel_models.GuildNewsChannel: """Parse a raw payload from Discord into a guild news channel object. Parameters @@ -243,7 +248,7 @@ def deserialize_guild_news_channel(self, payload: data_binding.JSONObject) -> ch """ @abc.abstractmethod - def deserialize_guild_store_channel(self, payload: data_binding.JSONObject) -> channels.GuildStoreChannel: + def deserialize_guild_store_channel(self, payload: data_binding.JSONObject) -> channel_models.GuildStoreChannel: """Parse a raw payload from Discord into a guild store channel object. Parameters @@ -258,7 +263,7 @@ def deserialize_guild_store_channel(self, payload: data_binding.JSONObject) -> c """ @abc.abstractmethod - def deserialize_guild_voice_channel(self, payload: data_binding.JSONObject) -> channels.GuildVoiceChannel: + def deserialize_guild_voice_channel(self, payload: data_binding.JSONObject) -> channel_models.GuildVoiceChannel: """Parse a raw payload from Discord into a guild voice channel object. Parameters @@ -273,7 +278,7 @@ def deserialize_guild_voice_channel(self, payload: data_binding.JSONObject) -> c """ @abc.abstractmethod - def deserialize_channel(self, payload: data_binding.JSONObject) -> channels.PartialChannel: + def deserialize_channel(self, payload: data_binding.JSONObject) -> channel_models.PartialChannel: """Parse a raw payload from Discord into a channel object. Parameters @@ -287,12 +292,12 @@ def deserialize_channel(self, payload: data_binding.JSONObject) -> channels.Part The deserialized partial channel-derived object. """ - ########## - # EMBEDS # - ########## + ################ + # EMBED MODELS # + ################ @abc.abstractmethod - def deserialize_embed(self, payload: data_binding.JSONObject) -> embeds.Embed: + def deserialize_embed(self, payload: data_binding.JSONObject) -> embed_models.Embed: """Parse a raw payload from Discord into an embed object. Parameters @@ -307,7 +312,7 @@ def deserialize_embed(self, payload: data_binding.JSONObject) -> embeds.Embed: """ @abc.abstractmethod - def serialize_embed(self, embed: embeds.Embed) -> data_binding.JSONObject: + def serialize_embed(self, embed: embed_models.Embed) -> data_binding.JSONObject: """Serialize an embed object to a json serializable dict. Parameters @@ -321,12 +326,12 @@ def serialize_embed(self, embed: embeds.Embed) -> data_binding.JSONObject: The serialized object representation. """ - ########## - # EMOJIS # - ########## + ################ + # EMOJI MODELS # + ################ @abc.abstractmethod - def deserialize_unicode_emoji(self, payload: data_binding.JSONObject) -> emojis.UnicodeEmoji: + def deserialize_unicode_emoji(self, payload: data_binding.JSONObject) -> emoji_models.UnicodeEmoji: """Parse a raw payload from Discord into a unicode emoji object. Parameters @@ -341,7 +346,7 @@ def deserialize_unicode_emoji(self, payload: data_binding.JSONObject) -> emojis. """ @abc.abstractmethod - def deserialize_custom_emoji(self, payload: data_binding.JSONObject) -> emojis.CustomEmoji: + def deserialize_custom_emoji(self, payload: data_binding.JSONObject) -> emoji_models.CustomEmoji: """Parse a raw payload from Discord into a custom emoji object. Parameters @@ -356,7 +361,7 @@ def deserialize_custom_emoji(self, payload: data_binding.JSONObject) -> emojis.C """ @abc.abstractmethod - def deserialize_known_custom_emoji(self, payload: data_binding.JSONObject) -> emojis.KnownCustomEmoji: + def deserialize_known_custom_emoji(self, payload: data_binding.JSONObject) -> emoji_models.KnownCustomEmoji: """Parse a raw payload from Discord into a known custom emoji object. Parameters @@ -373,7 +378,7 @@ def deserialize_known_custom_emoji(self, payload: data_binding.JSONObject) -> em @abc.abstractmethod def deserialize_emoji( self, payload: data_binding.JSONObject - ) -> typing.Union[emojis.UnicodeEmoji, emojis.CustomEmoji]: + ) -> typing.Union[emoji_models.UnicodeEmoji, emoji_models.CustomEmoji]: """Parse a raw payload from Discord into an emoji object. Parameters @@ -387,12 +392,12 @@ def deserialize_emoji( The deserialized custom or unicode emoji object. """ - ########### - # GATEWAY # - ########### + ################## + # GATEWAY MODELS # + ################## @abc.abstractmethod - def deserialize_gateway_bot(self, payload: data_binding.JSONObject) -> gateway.GatewayBot: + def deserialize_gateway_bot(self, payload: data_binding.JSONObject) -> gateway_models.GatewayBot: """Parse a raw payload from Discord into a gateway bot object. Parameters @@ -406,12 +411,12 @@ def deserialize_gateway_bot(self, payload: data_binding.JSONObject) -> gateway.G The deserialized gateway bot object. """ - ########## - # GUILDS # - ########## + ################ + # GUILD MODELS # + ################ @abc.abstractmethod - def deserialize_guild_widget(self, payload: data_binding.JSONObject) -> guilds.GuildWidget: + def deserialize_guild_widget(self, payload: data_binding.JSONObject) -> guild_models.GuildWidget: """Parse a raw payload from Discord into a guild widget object. Parameters @@ -430,8 +435,8 @@ def deserialize_member( self, payload: data_binding.JSONObject, *, - user: typing.Union[undefined.Undefined, users.User] = undefined.Undefined(), - ) -> guilds.Member: + user: typing.Union[undefined.Undefined, user_models.User] = undefined.Undefined(), + ) -> guild_models.Member: """Parse a raw payload from Discord into a member object. Parameters @@ -449,7 +454,7 @@ def deserialize_member( """ @abc.abstractmethod - def deserialize_role(self, payload: data_binding.JSONObject) -> guilds.Role: + def deserialize_role(self, payload: data_binding.JSONObject) -> guild_models.Role: """Parse a raw payload from Discord into a role object. Parameters @@ -464,7 +469,7 @@ def deserialize_role(self, payload: data_binding.JSONObject) -> guilds.Role: """ @abc.abstractmethod - def deserialize_partial_integration(self, payload: data_binding.JSONObject) -> guilds.PartialIntegration: + def deserialize_partial_integration(self, payload: data_binding.JSONObject) -> guild_models.PartialIntegration: """Parse a raw payload from Discord into a partial integration object. Parameters @@ -479,7 +484,7 @@ def deserialize_partial_integration(self, payload: data_binding.JSONObject) -> g """ @abc.abstractmethod - def deserialize_integration(self, payload: data_binding.JSONObject) -> guilds.Integration: + def deserialize_integration(self, payload: data_binding.JSONObject) -> guild_models.Integration: """Parse a raw payload from Discord into an integration object. Parameters @@ -494,7 +499,7 @@ def deserialize_integration(self, payload: data_binding.JSONObject) -> guilds.In """ @abc.abstractmethod - def deserialize_guild_member_ban(self, payload: data_binding.JSONObject) -> guilds.GuildMemberBan: + def deserialize_guild_member_ban(self, payload: data_binding.JSONObject) -> guild_models.GuildMemberBan: """Parse a raw payload from Discord into a guild member ban object. Parameters @@ -509,7 +514,7 @@ def deserialize_guild_member_ban(self, payload: data_binding.JSONObject) -> guil """ @abc.abstractmethod - def deserialize_unavailable_guild(self, payload: data_binding.JSONObject) -> guilds.UnavailableGuild: + def deserialize_unavailable_guild(self, payload: data_binding.JSONObject) -> guild_models.UnavailableGuild: """Parse a raw payload from Discord into a unavailable guild object. Parameters @@ -524,7 +529,7 @@ def deserialize_unavailable_guild(self, payload: data_binding.JSONObject) -> gui """ @abc.abstractmethod - def deserialize_guild_preview(self, payload: data_binding.JSONObject) -> guilds.GuildPreview: + def deserialize_guild_preview(self, payload: data_binding.JSONObject) -> guild_models.GuildPreview: """Parse a raw payload from Discord into a guild preview object. Parameters @@ -539,7 +544,7 @@ def deserialize_guild_preview(self, payload: data_binding.JSONObject) -> guilds. """ @abc.abstractmethod - def deserialize_guild(self, payload: data_binding.JSONObject) -> guilds.Guild: + def deserialize_guild(self, payload: data_binding.JSONObject) -> guild_models.Guild: """Parse a raw payload from Discord into a guild object. Parameters @@ -553,12 +558,12 @@ def deserialize_guild(self, payload: data_binding.JSONObject) -> guilds.Guild: The deserialized guild object. """ - ########### - # INVITES # - ########### + ################# + # INVITE MODELS # + ################# @abc.abstractmethod - def deserialize_vanity_url(self, payload: data_binding.JSONObject) -> invites.VanityURL: + def deserialize_vanity_url(self, payload: data_binding.JSONObject) -> invite_models.VanityURL: """Parse a raw payload from Discord into a vanity url object. Parameters @@ -573,7 +578,7 @@ def deserialize_vanity_url(self, payload: data_binding.JSONObject) -> invites.Va """ @abc.abstractmethod - def deserialize_invite(self, payload: data_binding.JSONObject) -> invites.Invite: + def deserialize_invite(self, payload: data_binding.JSONObject) -> invite_models.Invite: """Parse a raw payload from Discord into an invite object. Parameters @@ -588,7 +593,7 @@ def deserialize_invite(self, payload: data_binding.JSONObject) -> invites.Invite """ @abc.abstractmethod - def deserialize_invite_with_metadata(self, payload: data_binding.JSONObject) -> invites.InviteWithMetadata: + def deserialize_invite_with_metadata(self, payload: data_binding.JSONObject) -> invite_models.InviteWithMetadata: """Parse a raw payload from Discord into a invite with metadata object. Parameters @@ -602,12 +607,13 @@ def deserialize_invite_with_metadata(self, payload: data_binding.JSONObject) -> The deserialized invite with metadata object. """ - ############ - # MESSAGES # - ############ + ################## + # MESSAGE MODELS # + ################## @abc.abstractmethod - def deserialize_message(self, payload: data_binding.JSONObject) -> messages.Message: + @abc.abstractmethod + def deserialize_message(self, payload: data_binding.JSONObject) -> message_models.Message: """Parse a raw payload from Discord into a message object. Parameters @@ -621,12 +627,12 @@ def deserialize_message(self, payload: data_binding.JSONObject) -> messages.Mess The deserialized message object. """ - ############# - # PRESENCES # - ############# + ################### + # PRESENCE MODELS # + ################### @abc.abstractmethod - def deserialize_member_presence(self, payload: data_binding.JSONObject) -> presences.MemberPresence: + def deserialize_member_presence(self, payload: data_binding.JSONObject) -> presence_models.MemberPresence: """Parse a raw payload from Discord into a member presence object. Parameters @@ -640,12 +646,12 @@ def deserialize_member_presence(self, payload: data_binding.JSONObject) -> prese The deserialized member presence object. """ - ######### - # USERS # - ######### + ############### + # USER MODELS # + ############### @abc.abstractmethod - def deserialize_user(self, payload: data_binding.JSONObject) -> users.User: + def deserialize_user(self, payload: data_binding.JSONObject) -> user_models.User: """Parse a raw payload from Discord into a user object. Parameters @@ -660,7 +666,7 @@ def deserialize_user(self, payload: data_binding.JSONObject) -> users.User: """ @abc.abstractmethod - def deserialize_my_user(self, payload: data_binding.JSONObject) -> users.OwnUser: + def deserialize_my_user(self, payload: data_binding.JSONObject) -> user_models.OwnUser: """Parse a raw payload from Discord into a user object. Parameters @@ -674,12 +680,12 @@ def deserialize_my_user(self, payload: data_binding.JSONObject) -> users.OwnUser The deserialized user object. """ - ########## - # Voices # - ########## + ################ + # VOICE MODELS # + ################ @abc.abstractmethod - def deserialize_voice_state(self, payload: data_binding.JSONObject) -> voices.VoiceState: + def deserialize_voice_state(self, payload: data_binding.JSONObject) -> voice_models.VoiceState: """Parse a raw payload from Discord into a voice state object. Parameters @@ -694,7 +700,7 @@ def deserialize_voice_state(self, payload: data_binding.JSONObject) -> voices.Vo """ @abc.abstractmethod - def deserialize_voice_region(self, payload: data_binding.JSONObject) -> voices.VoiceRegion: + def deserialize_voice_region(self, payload: data_binding.JSONObject) -> voice_models.VoiceRegion: """Parse a raw payload from Discord into a voice region object. Parameters @@ -708,12 +714,12 @@ def deserialize_voice_region(self, payload: data_binding.JSONObject) -> voices.V The deserialized voice region object. """ - ############ - # WEBHOOKS # - ############ + ################## + # WEBHOOK MODELS # + ################## @abc.abstractmethod - def deserialize_webhook(self, payload: data_binding.JSONObject) -> webhooks.Webhook: + def deserialize_webhook(self, payload: data_binding.JSONObject) -> webhook_models.Webhook: """Parse a raw payload from Discord into a webhook object. Parameters @@ -726,3 +732,580 @@ def deserialize_webhook(self, payload: data_binding.JSONObject) -> webhooks.Webh hikari.models.webhooks.Webhook The deserialized webhook object. """ + + ################## + # CHANNEL EVENTS # + ################## + + @abc.abstractmethod + def deserialize_channel_create_event(self, payload: data_binding.JSONObject) -> channel_events.ChannelCreateEvent: + """Parse a raw payload from Discord into a channel create event object. + + Parameters + ---------- + payload : Mapping[str, Any] + The dict payload to parse. + + Returns + ------- + hikari.events.channel.ChannelCreateEvent + The parsed channel create event object. + """ + + @abc.abstractmethod + def deserialize_channel_update_event(self, payload: data_binding.JSONObject) -> channel_events.ChannelUpdateEvent: + """Parse a raw payload from Discord into a channel update event object. + + Parameters + ---------- + payload : Mapping[str, Any] + The dict payload to parse. + + Returns + ------- + hikari.events.channel.ChannelUpdateEvent + The parsed event object. + """ + + @abc.abstractmethod + def deserialize_channel_delete_event(self, payload: data_binding.JSONObject) -> channel_events.ChannelDeleteEvent: + """Parse a raw payload from Discord into a channel delete event object. + + Parameters + ---------- + payload : Mapping[str, Any] + The dict payload to parse. + + Returns + ------- + hikari.events.channel.ChannelDeleteEvent + The parsed channel delete event object. + """ + + @abc.abstractmethod + def deserialize_channel_pins_update_event( + self, payload: data_binding.JSONObject + ) -> channel_events.ChannelPinsUpdateEvent: + """Parse a raw payload from Discord into a channel pins update event object. + + Parameters + ---------- + payload : Mapping[str, Any] + The dict payload to parse. + + Returns + ------- + hikari.events.channel.ChannelPinsUpdateEvent + The parsed channel pins update event object. + """ + + @abc.abstractmethod + def deserialize_webhook_update_event(self, payload: data_binding.JSONObject) -> channel_events.WebhookUpdateEvent: + """Parse a raw payload from Discord into a webhook update event object. + + Parameters + ---------- + payload : Mapping[str, Any] + The dict payload to parse. + + Returns + ------- + hikari.events.channel.WebhookUpdateEvent + The parsed webhook update event object. + """ + + @abc.abstractmethod + def deserialize_typing_start_event(self, payload: data_binding.JSONObject) -> channel_events.TypingStartEvent: + """Parse a raw payload from Discord into a typing start event object. + + Parameters + ---------- + payload : Mapping[str, Any] + The dict payload to parse. + + Returns + ------- + hikari.events.channel.TypingStartEvent + The parsed typing start event object. + """ + + @abc.abstractmethod + def deserialize_invite_create_event(self, payload: data_binding.JSONObject) -> channel_events.InviteCreateEvent: + """Parse a raw payload from Discord into an invite create event object. + + Parameters + ---------- + payload : Mapping[str, Any] + The dict payload to parse. + + Returns + ------- + hikari.events.channel.InviteCreateEvent + The parsed invite create event object. + """ + + @abc.abstractmethod + def deserialize_invite_delete_event(self, payload: data_binding.JSONObject) -> channel_events.InviteDeleteEvent: + """Parse a raw payload from Discord into an invite delete event object. + + Parameters + ---------- + payload : Mapping[str, Any] + The dict payload to parse. + + Returns + ------- + hikari.events.channel.InviteDeleteEvent + The parsed invite delete event object. + """ + + ################ + # GUILD EVENTS # + ################ + + @abc.abstractmethod + def deserialize_guild_create_event(self, payload: data_binding.JSONObject) -> guild_events.GuildCreateEvent: + """Parse a raw payload from Discord into a guild create event object. + + Parameters + ---------- + payload : Mapping[str, Any] + The dict payload to parse. + + Returns + ------- + hikari.events.guild.GuildCreateEvent + The parsed guild create event object. + """ + + @abc.abstractmethod + def deserialize_guild_update_event(self, payload: data_binding.JSONObject) -> guild_events.GuildUpdateEvent: + """Parse a raw payload from Discord into a guild update event object. + + Parameters + ---------- + payload : Mapping[str, Any] + The dict payload to parse. + + Returns + ------- + hikari.events.guild.GuildUpdateEvent + The parsed guild update event object. + """ + + @abc.abstractmethod + def deserialize_guild_leave_event(self, payload: data_binding.JSONObject) -> guild_events.GuildLeaveEvent: + """Parse a raw payload from Discord into a guild leave event object. + + Parameters + ---------- + payload : Mapping[str, Any] + The dict payload to parse. + + Returns + ------- + hikari.events.guild.GuildLeaveEvent + The parsed guild leave event object. + """ + + @abc.abstractmethod + def deserialize_guild_unavailable_event( + self, payload: data_binding.JSONObject + ) -> guild_events.GuildUnavailableEvent: + """Parse a raw payload from Discord into a guild unavailable event object. + + Parameters + ---------- + payload : Mapping[str, Any] + The dict payload to parse. + + Returns + ------- + hikari.events. + The parsed guild unavailable event object. + """ + + @abc.abstractmethod + def deserialize_guild_ban_add_event(self, payload: data_binding.JSONObject) -> guild_events.GuildBanAddEvent: + """Parse a raw payload from Discord into a guild ban add event object. + + Parameters + ---------- + payload : Mapping[str, Any] + The dict payload to parse. + + Returns + ------- + hikari.events.guild.GuildBanAddEvent + The parsed guild ban add event object. + """ + + @abc.abstractmethod + def deserialize_guild_ban_remove_event(self, payload: data_binding.JSONObject) -> guild_events.GuildBanRemoveEvent: + """Parse a raw payload from Discord into a guild ban remove event object. + + Parameters + ---------- + payload : Mapping[str, Any] + The dict payload to parse. + + Returns + ------- + hikari.events.guild.GuildBanRemoveEvent + The parsed guild ban remove event object. + """ + + @abc.abstractmethod + def deserialize_guild_emojis_update_event( + self, payload: data_binding.JSONObject + ) -> guild_events.GuildEmojisUpdateEvent: + """Parse a raw payload from Discord into a guild emojis update event object. + + Parameters + ---------- + payload : Mapping[str, Any] + The dict payload to parse. + + Returns + ------- + hikari.events.guild.GuildEmojisUpdateEvent + The parsed guild emojis update event object. + """ + + @abc.abstractmethod + def deserialize_guild_integrations_update_event( + self, payload: data_binding.JSONObject + ) -> guild_events.GuildIntegrationsUpdateEvent: + """Parse a raw payload from Discord into a guilds integrations update event object. + + Parameters + ---------- + payload : Mapping[str, Any] + The dict payload to parse. + + Returns + ------- + hikari.events.guild.GuildIntegrationsUpdateEvent + The parsed guilds integrations update event object. + """ + + @abc.abstractmethod + def deserialize_guild_member_add_event(self, payload: data_binding.JSONObject) -> guild_events.GuildMemberAddEvent: + """Parse a raw payload from Discord into a guild member add event object. + + Parameters + ---------- + payload : Mapping[str, Any] + The dict payload to parse. + + Returns + ------- + hikari.events.guild.GuildMemberAddEvent + The parsed guild member add event object. + """ + + @abc.abstractmethod + def deserialize_guild_member_update_event( + self, payload: data_binding.JSONObject + ) -> guild_events.GuildMemberUpdateEvent: + """Parse a raw payload from Discord into a guild member update event object. + + Parameters + ---------- + payload : Mapping[str, Any] + The dict payload to parse. + + Returns + ------- + hikari.events.guild.GuildMemberUpdateEvent + The parsed guild member update event object. + """ + + @abc.abstractmethod + def deserialize_guild_member_remove_event( + self, payload: data_binding.JSONObject + ) -> guild_events.GuildMemberRemoveEvent: + """Parse a raw payload from Discord into a guild member remove event object. + + Parameters + ---------- + payload : Mapping[str, Any] + The dict payload to parse. + + Returns + ------- + hikari.events.guild.GuildMemberRemoveEvent + The parsed guild member remove event object. + """ + + @abc.abstractmethod + def deserialize_guild_role_create_event( + self, payload: data_binding.JSONObject + ) -> guild_events.GuildRoleCreateEvent: + """Parse a raw payload from Discord into a guild role create event object. + + Parameters + ---------- + payload : Mapping[str, Any] + The dict payload to parse. + + Returns + ------- + hikari.events.guild.GuildRoleCreateEvent + The parsed guild role create event object. + """ + + @abc.abstractmethod + def deserialize_guild_role_update_event( + self, payload: data_binding.JSONObject + ) -> guild_events.GuildRoleUpdateEvent: + """Parse a raw payload from Discord into a guild role update event object. + + Parameters + ---------- + payload : Mapping[str, Any] + The dict payload to parse. + + Returns + ------- + hikari.events.guild.GuildRoleUpdateEvent + The parsed guild role update event object. + """ + + @abc.abstractmethod + def deserialize_guild_role_delete_event( + self, payload: data_binding.JSONObject + ) -> guild_events.GuildRoleDeleteEvent: + """Parse a raw payload from Discord into a guild role delete event object. + + Parameters + ---------- + payload : Mapping[str, Any] + The dict payload to parse. + + Returns + ------- + hikari.events.guild.GuildRoleDeleteEvent + The parsed guild role delete event object. + """ + + @abc.abstractmethod + def deserialize_presence_update_event(self, payload: data_binding.JSONObject) -> guild_events.PresenceUpdateEvent: + """Parse a raw payload from Discord into a presence update event object. + + Parameters + ---------- + payload : Mapping[str, Any] + The dict payload to parse. + + Returns + ------- + hikari.events.guild.PresenceUpdateEvent + The parsed presence update event object. + """ + + ################## + # MESSAGE EVENTS # + ################## + + @abc.abstractmethod + def deserialize_message_create_event(self, payload: data_binding.JSONObject) -> message_events.MessageCreateEvent: + """Parse a raw payload from Discord into a message create event object. + + Parameters + ---------- + payload : Mapping[str, Any] + The dict payload to parse. + + Returns + ------- + hikari.events.message.MessageCreateEvent + The parsed message create event object. + """ + + @abc.abstractmethod + def deserialize_message_update_event(self, payload: data_binding.JSONObject) -> message_events.MessageUpdateEvent: + """Parse a raw payload from Discord into a message update event object. + + Parameters + ---------- + payload : Mapping[str, Any] + The dict payload to parse. + + Returns + ------- + hikari.events.message.MessageUpdateEvent + The parsed message update event object. + """ + + @abc.abstractmethod + def deserialize_message_delete_event(self, payload: data_binding.JSONObject) -> message_events.MessageDeleteEvent: + """Parse a raw payload from Discord into a message delete event object. + + Parameters + ---------- + payload : Mapping[str, Any] + The dict payload to parse. + + Returns + ------- + hikari.events.message.MessageDeleteEvent + The parsed message delete event object. + """ + + @abc.abstractmethod + def deserialize_message_delete_bulk_event( + self, payload: data_binding.JSONObject + ) -> message_events.MessageDeleteBulkEvent: + """Parse a raw payload from Discord into a message delete bulk event object. + + Parameters + ---------- + payload : Mapping[str, Any] + The dict payload to parse. + + Returns + ------- + hikari.events.message.MessageDeleteBulkEvent + The parsed message delete bulk event object. + """ + + @abc.abstractmethod + def deserialize_message_reaction_add_event( + self, payload: data_binding.JSONObject + ) -> message_events.MessageReactionAddEvent: + """Parse a raw payload from Discord into a message reaction add event object. + + Parameters + ---------- + payload : Mapping[str, Any] + The dict payload to parse. + + Returns + ------- + hikari.events.message.MessageReactionAddEvent + The parsed message reaction add event object. + """ + + @abc.abstractmethod + def deserialize_message_reaction_remove_event( + self, payload: data_binding.JSONObject + ) -> message_events.MessageReactionRemoveEvent: + """Parse a raw payload from Discord into a message reaction remove event object. + + Parameters + ---------- + payload : Mapping[str, Any] + The dict payload to parse. + + Returns + ------- + hikari.events.message.MessageReactionRemoveEvent + The parsed message reaction remove event object. + """ + + @abc.abstractmethod + def deserialize_message_reaction_remove_all_event( + self, payload: data_binding.JSONObject + ) -> message_events.MessageReactionRemoveAllEvent: + """Parse a raw payload from Discord into a message reaction remove all event object. + + Parameters + ---------- + payload : Mapping[str, Any] + The dict payload to parse. + + Returns + ------- + hikari.events.message.MessageReactionRemoveAllEvent + The parsed message reaction remove all event object. + """ + + @abc.abstractmethod + def deserialize_message_reaction_remove_emoji_event( + self, payload: data_binding.JSONObject + ) -> message_events.MessageReactionRemoveEmojiEvent: + """Parse a raw payload from Discord into a message reaction remove emoji event object. + + Parameters + ---------- + payload : Mapping[str, Any] + The dict payload to parse. + + Returns + ------- + hikari.events.message.MessageReactionRemoveEmojiEvent + The parsed message reaction remove emoji event object. + """ + + ################ + # OTHER EVENTS # + ################ + + @abc.abstractmethod + def deserialize_ready_event(self, payload: data_binding.JSONObject) -> other_events.ReadyEvent: + """Parse a raw payload from Discord into a ready event object. + + Parameters + ---------- + payload : Mapping[str, Any] + The dict payload to parse. + + Returns + ------- + hikari.events.other.ReadyEvent + The parsed ready event object. + """ + + @abc.abstractmethod + def deserialize_my_user_update_event(self, payload: data_binding.JSONObject) -> other_events.MyUserUpdateEvent: + """Parse a raw payload from Discord into a my user update event object. + + Parameters + ---------- + payload : Mapping[str, Any] + The dict payload to parse. + + Returns + ------- + hikari.events.other.MyUserUpdateEvent + The parsed my user update event object. + """ + + ################ + # VOICE EVENTS # + ################ + + @abc.abstractmethod + def deserialize_voice_state_update_event( + self, payload: data_binding.JSONObject + ) -> voice_events.VoiceStateUpdateEvent: + """Parse a raw payload from Discord into a voice state update event object. + + Parameters + ---------- + payload : Mapping[str, Any] + The dict payload to parse. + + Returns + ------- + hikari.events.voice.VoiceStateUpdateEvent + The parsed voice state update event object. + """ + + @abc.abstractmethod + def deserialize_voice_server_update_event( + self, payload: data_binding.JSONObject + ) -> voice_events.VoiceServerUpdateEvent: + """Parse a raw payload from Discord into a voice server update event object. + + Parameters + ---------- + payload : Mapping[str, Any] + The dict payload to parse. + + Returns + ------- + hikari.events.voice.VoiceServerUpdateEvent + The parsed voice server update event object. + """ diff --git a/hikari/events/base.py b/hikari/events/base.py index 6f6e9f5365..1e20d12015 100644 --- a/hikari/events/base.py +++ b/hikari/events/base.py @@ -27,15 +27,13 @@ import attr -from hikari.models import bases as base_models - if typing.TYPE_CHECKING: from hikari.models import intents # Base event, is not deserialized -@attr.s(eq=False, hash=False, kw_only=True, slots=True) -class HikariEvent(base_models.Entity, abc.ABC): +@attr.s(eq=False, hash=False, init=False, kw_only=True, slots=True) +class HikariEvent(abc.ABC): """The base class that all events inherit from.""" diff --git a/hikari/events/channel.py b/hikari/events/channel.py index 4fc650367a..dd4916258e 100644 --- a/hikari/events/channel.py +++ b/hikari/events/channel.py @@ -40,84 +40,26 @@ from hikari.events import base as base_events from hikari.models import bases as base_models -from hikari.models import channels -from hikari.models import guilds from hikari.models import intents -from hikari.models import invites -from hikari.models import users -from hikari.utilities import snowflake +if typing.TYPE_CHECKING: + from hikari.models import channels + from hikari.models import guilds + from hikari.models import invites + from hikari.utilities import snowflake -@base_events.requires_intents(intents.Intent.GUILDS) -@attr.s(eq=False, hash=False, kw_only=True, slots=True) -class BaseChannelEvent(base_events.HikariEvent, base_models.Unique, abc.ABC): - """A base object that Channel events will inherit from.""" - - type: channels.ChannelType = attr.ib(repr=True) - """The channel's type.""" - - guild_id: typing.Optional[snowflake.Snowflake] = attr.ib(repr=True) - """The ID of the guild this channel is in, will be `None` for DMs.""" - - position: typing.Optional[int] = attr.ib() - """The sorting position of this channel. - - This will be relative to the `BaseChannelEvent.parent_id` if set. - """ - - permission_overwrites: typing.Optional[ - typing.Mapping[snowflake.Snowflake, channels.PermissionOverwrite] - ] = attr.ib() - """An mapping of the set permission overwrites for this channel, if applicable.""" - - name: typing.Optional[str] = attr.ib(repr=True) - """The name of this channel, if applicable.""" - - topic: typing.Optional[str] = attr.ib() - """The topic of this channel, if applicable and set.""" - - is_nsfw: typing.Optional[bool] = attr.ib() - """Whether this channel is nsfw, will be `None` if not applicable.""" - last_message_id: typing.Optional[snowflake.Snowflake] = attr.ib() - """The ID of the last message sent, if it's a text type channel.""" - - bitrate: typing.Optional[int] = attr.ib() - """The bitrate (in bits) of this channel, if it's a guild voice channel.""" - - user_limit: typing.Optional[int] = attr.ib() - """The user limit for this channel if it's a guild voice channel.""" - - rate_limit_per_user: typing.Optional[datetime.timedelta] = attr.ib() - """How long a user has to wait before sending another message in this channel. - - This is only applicable to a guild text like channel. - """ - - recipients: typing.Optional[typing.Mapping[snowflake.Snowflake, users.User]] = attr.ib() - """A mapping of this channel's recipient users, if it's a DM or group DM.""" - - icon_hash: typing.Optional[str] = attr.ib() - """The hash of this channel's icon, if it's a group DM channel and is set.""" - - owner_id: typing.Optional[snowflake.Snowflake] = attr.ib() - """The ID of this channel's creator, if it's a DM channel.""" - - application_id: typing.Optional[snowflake.Snowflake] = attr.ib() - """The ID of the application that created the group DM. - - This is only applicable to bot based group DMs. - """ - - parent_id: typing.Optional[snowflake.Snowflake] = attr.ib() - """The ID of this channels's parent category within guild, if set.""" +@base_events.requires_intents(intents.Intent.GUILDS) # TODO: this intent doesn't account for DM channels. +@attr.s(auto_attribs=True, eq=False, hash=False, init=False, kw_only=True, slots=True) +class BaseChannelEvent(base_events.HikariEvent, abc.ABC): + """A base object that Channel events will inherit from.""" - last_pin_timestamp: typing.Optional[datetime.datetime] = attr.ib() - """The datetime of when the last message was pinned in this channel.""" + channel: channels.PartialChannel + """The object of the channel this event involved.""" -@base_events.requires_intents(intents.Intent.GUILDS) -@attr.s(eq=False, hash=False, kw_only=True, slots=True) +@base_events.requires_intents(intents.Intent.GUILDS) # TODO: this intent doesn't account for DM channels. +@attr.s(auto_attribs=True, eq=False, hash=False, init=False, kw_only=True, slots=True) class ChannelCreateEvent(BaseChannelEvent): """Represents Channel Create gateway events. @@ -126,21 +68,21 @@ class ChannelCreateEvent(BaseChannelEvent): """ -@base_events.requires_intents(intents.Intent.GUILDS) -@attr.s(eq=False, hash=False, kw_only=True, slots=True) +@base_events.requires_intents(intents.Intent.GUILDS) # TODO: this intent doesn't account for DM channels. +@attr.s(auto_attribs=True, eq=False, hash=False, init=False, kw_only=True, slots=True) class ChannelUpdateEvent(BaseChannelEvent): """Represents Channel Update gateway events.""" -@base_events.requires_intents(intents.Intent.GUILDS) -@attr.s(eq=False, hash=False, kw_only=True, slots=True) +@base_events.requires_intents(intents.Intent.GUILDS) # TODO: this intent doesn't account for DM channels. +@attr.s(auto_attribs=True, eq=False, hash=False, init=False, kw_only=True, slots=True) class ChannelDeleteEvent(BaseChannelEvent): """Represents Channel Delete gateway events.""" -@base_events.requires_intents(intents.Intent.GUILDS) -@attr.s(eq=False, hash=False, kw_only=True, slots=True) -class ChannelPinsUpdateEvent(base_events.HikariEvent): +@base_events.requires_intents(intents.Intent.GUILDS) # TODO: this intent doesn't account for DM channels. +@attr.s(eq=False, hash=False, init=False, kw_only=True, slots=True) +class ChannelPinsUpdateEvent(base_events.HikariEvent, base_models.Entity): """Used to represent the Channel Pins Update gateway event. Sent when a message is pinned or unpinned in a channel but not @@ -164,8 +106,8 @@ class ChannelPinsUpdateEvent(base_events.HikariEvent): @base_events.requires_intents(intents.Intent.GUILD_WEBHOOKS) -@attr.s(eq=False, hash=False, kw_only=True, slots=True) -class WebhookUpdateEvent(base_events.HikariEvent): +@attr.s(eq=False, hash=False, init=False, kw_only=True, slots=True) +class WebhookUpdateEvent(base_events.HikariEvent, base_models.Entity): """Used to represent webhook update gateway events. Sent when a webhook is updated, created or deleted in a guild. @@ -179,8 +121,8 @@ class WebhookUpdateEvent(base_events.HikariEvent): @base_events.requires_intents(intents.Intent.GUILD_MESSAGE_TYPING, intents.Intent.DIRECT_MESSAGE_TYPING) -@attr.s(eq=False, hash=False, kw_only=True, slots=True) -class TypingStartEvent(base_events.HikariEvent): +@attr.s(eq=False, hash=False, init=False, kw_only=True, slots=True) +class TypingStartEvent(base_events.HikariEvent, base_models.Entity): """Used to represent typing start gateway events. Received when a user or bot starts "typing" in a channel. @@ -209,56 +151,17 @@ class TypingStartEvent(base_events.HikariEvent): @base_events.requires_intents(intents.Intent.GUILD_INVITES) -@attr.s(eq=False, hash=False, kw_only=True, slots=True) +@attr.s(auto_attribs=True, eq=False, hash=False, init=False, kw_only=True, slots=True) class InviteCreateEvent(base_events.HikariEvent): """Represents a gateway Invite Create event.""" - channel_id: snowflake.Snowflake = attr.ib(repr=True) - """The ID of the channel this invite targets.""" - - code: str = attr.ib(repr=True) - """The code that identifies this invite.""" - - created_at: datetime.datetime = attr.ib() - """The datetime of when this invite was created.""" - - guild_id: typing.Optional[snowflake.Snowflake] = attr.ib(repr=True) - """The ID of the guild this invite was created in, if applicable. - - Will be `None` for group DM invites. - """ - - inviter: typing.Optional[users.User] = attr.ib() - """The object of the user who created this invite, if applicable.""" - - max_age: typing.Optional[datetime.timedelta] = attr.ib() - """The timedelta of how long this invite will be valid for. - - If set to `None` then this is unlimited. - """ - - max_uses: typing.Union[int, float] = attr.ib() - """The limit for how many times this invite can be used before it expires. - - If set to infinity (`float("inf")`) then this is unlimited. - """ - - target_user: typing.Optional[users.User] = attr.ib() - """The object of the user who this invite targets, if set.""" - - target_user_type: typing.Optional[invites.TargetUserType] = attr.ib() - """The type of user target this invite is, if applicable.""" - - is_temporary: bool = attr.ib() - """Whether this invite grants temporary membership.""" - - uses: int = attr.ib() - """The amount of times this invite has been used.""" + invite: invites.InviteWithMetadata + """The object of the invite being created.""" @base_events.requires_intents(intents.Intent.GUILD_INVITES) -@attr.s(eq=False, hash=False, kw_only=True, slots=True) -class InviteDeleteEvent(base_events.HikariEvent): +@attr.s(eq=False, hash=False, init=False, kw_only=True, slots=True) +class InviteDeleteEvent(base_events.HikariEvent, base_models.Entity): """Used to represent Invite Delete gateway events. Sent when an invite is deleted for a channel we can access. @@ -267,7 +170,6 @@ class InviteDeleteEvent(base_events.HikariEvent): channel_id: snowflake.Snowflake = attr.ib(repr=True) """The ID of the channel this ID was attached to.""" - # TODO: move common fields with InviteCreateEvent into base class. code: str = attr.ib(repr=True) """The code of this invite.""" diff --git a/hikari/events/guild.py b/hikari/events/guild.py index 4367c80839..d675dfbb61 100644 --- a/hikari/events/guild.py +++ b/hikari/events/guild.py @@ -46,36 +46,40 @@ from hikari.events import base as base_events from hikari.models import bases as base_models -from hikari.models import emojis as emojis_models -from hikari.models import guilds from hikari.models import intents -from hikari.models import presences -from hikari.models import users -from hikari.utilities import snowflake -from hikari.utilities import undefined if typing.TYPE_CHECKING: - import datetime + from hikari.models import emojis as emojis_models + from hikari.models import guilds + from hikari.models import presences + from hikari.models import users + from hikari.utilities import snowflake @base_events.requires_intents(intents.Intent.GUILDS) -@attr.s(eq=False, hash=False, kw_only=True, slots=True) -class GuildCreateEvent(base_events.HikariEvent, guilds.Guild): +@attr.s(auto_attribs=True, eq=False, hash=False, init=False, kw_only=True, slots=True) +class GuildCreateEvent(base_events.HikariEvent): """Used to represent Guild Create gateway events. Will be received when the bot joins a guild, and when a guild becomes available to a guild (either due to outage or at startup). """ + guild: guilds.Guild + """The object of the guild that's being created.""" + @base_events.requires_intents(intents.Intent.GUILDS) -@attr.s(eq=False, hash=False, kw_only=True, slots=True) -class GuildUpdateEvent(base_events.HikariEvent, guilds.Guild): +@attr.s(auto_attribs=True, eq=False, hash=False, init=False, kw_only=True, slots=True) +class GuildUpdateEvent(base_events.HikariEvent): """Used to represent Guild Update gateway events.""" + guild: guilds.Guild + """The object of the guild that's being updated.""" + @base_events.requires_intents(intents.Intent.GUILDS) -@attr.s(eq=False, hash=False, kw_only=True, slots=True) +@attr.s(eq=False, hash=False, init=False, kw_only=True, slots=True) class GuildLeaveEvent(base_events.HikariEvent, base_models.Unique): """Fired when the current user leaves the guild or is kicked/banned from it. @@ -85,8 +89,8 @@ class GuildLeaveEvent(base_events.HikariEvent, base_models.Unique): @base_events.requires_intents(intents.Intent.GUILDS) -@attr.s(eq=False, hash=False, kw_only=True, slots=True) -class GuildUnavailableEvent(base_events.HikariEvent, base_models.Unique): +@attr.s(eq=False, hash=False, init=False, kw_only=True, slots=True) +class GuildUnavailableEvent(base_events.HikariEvent, base_models.Entity, base_models.Unique): """Fired when a guild becomes temporarily unavailable due to an outage. !!! note @@ -95,8 +99,8 @@ class GuildUnavailableEvent(base_events.HikariEvent, base_models.Unique): @base_events.requires_intents(intents.Intent.GUILD_BANS) -@attr.s(eq=False, hash=False, kw_only=True, slots=True) -class BaseGuildBanEvent(base_events.HikariEvent, abc.ABC): +@attr.s(eq=False, hash=False, init=False, kw_only=True, slots=True) +class BaseGuildBanEvent(base_events.HikariEvent, base_models.Entity, abc.ABC): """A base object that guild ban events will inherit from.""" guild_id: snowflake.Snowflake = attr.ib(repr=True) @@ -107,20 +111,20 @@ class BaseGuildBanEvent(base_events.HikariEvent, abc.ABC): @base_events.requires_intents(intents.Intent.GUILD_BANS) -@attr.s(eq=False, hash=False, kw_only=True, slots=True) +@attr.s(eq=False, hash=False, init=False, kw_only=True, slots=True) class GuildBanAddEvent(BaseGuildBanEvent): """Used to represent a Guild Ban Add gateway event.""" @base_events.requires_intents(intents.Intent.GUILD_BANS) -@attr.s(eq=False, hash=False, kw_only=True, slots=True) +@attr.s(eq=False, hash=False, init=False, kw_only=True, slots=True) class GuildBanRemoveEvent(BaseGuildBanEvent): """Used to represent a Guild Ban Remove gateway event.""" @base_events.requires_intents(intents.Intent.GUILD_EMOJIS) -@attr.s(eq=False, hash=False, kw_only=True, slots=True) -class GuildEmojisUpdateEvent(base_events.HikariEvent): +@attr.s(eq=False, hash=False, init=False, kw_only=True, slots=True) +class GuildEmojisUpdateEvent(base_events.HikariEvent, base_models.Entity): """Represents a Guild Emoji Update gateway event.""" guild_id: snowflake.Snowflake = attr.ib() @@ -131,8 +135,8 @@ class GuildEmojisUpdateEvent(base_events.HikariEvent): @base_events.requires_intents(intents.Intent.GUILD_INTEGRATIONS) -@attr.s(eq=False, hash=False, kw_only=True, slots=True) -class GuildIntegrationsUpdateEvent(base_events.HikariEvent): +@attr.s(eq=False, hash=False, init=False, kw_only=True, slots=True) +class GuildIntegrationsUpdateEvent(base_events.HikariEvent, base_models.Entity): """Used to represent Guild Integration Update gateway events.""" guild_id: snowflake.Snowflake = attr.ib(repr=True) @@ -140,48 +144,31 @@ class GuildIntegrationsUpdateEvent(base_events.HikariEvent): @base_events.requires_intents(intents.Intent.GUILD_MEMBERS) -@attr.s(eq=False, hash=False, kw_only=True, slots=True) -class GuildMemberAddEvent(base_events.HikariEvent, guilds.Member): +@attr.s(eq=False, hash=False, init=False, kw_only=True, slots=True) +class GuildMemberAddEvent(base_events.HikariEvent, base_models.Entity): """Used to represent a Guild Member Add gateway event.""" - guild_id: snowflake.Snowflake = attr.ib(repr=True) + guild_id: snowflake.Snowflake = attr.ib(repr=True) # TODO: do we want to have guild_id on all members? """The ID of the guild where this member was added.""" + member: guilds.Member = attr.ib(repr=True) + """The object of the member who's being added.""" + @base_events.requires_intents(intents.Intent.GUILD_MEMBERS) -@attr.s(eq=False, hash=False, kw_only=True, slots=True) +@attr.s(auto_attribs=True, eq=False, hash=False, init=False, kw_only=True, slots=True) class GuildMemberUpdateEvent(base_events.HikariEvent): """Used to represent a Guild Member Update gateway event. Sent when a guild member or their inner user object is updated. """ - guild_id: snowflake.Snowflake = attr.ib(repr=True) - """The ID of the guild this member was updated in.""" - - role_ids: typing.Sequence[snowflake.Snowflake] = attr.ib() - """A sequence of the IDs of the member's current roles.""" - - user: users.User = attr.ib(repr=True) - """The object of the user who was updated.""" - - nickname: typing.Union[None, str, undefined.Undefined] = attr.ib() - """This member's nickname. - - When set to `None`, this has been removed and when set to - `hikari.models.undefined.Undefined` this hasn't been acted on. - """ - - premium_since: typing.Union[None, datetime.datetime, undefined.Undefined] = attr.ib() - """The datetime of when this member started "boosting" this guild. - - Will be `None` if they aren't boosting. - """ + member: guilds.Member @base_events.requires_intents(intents.Intent.GUILD_MEMBERS) -@attr.s(eq=False, hash=False, kw_only=True, slots=True) -class GuildMemberRemoveEvent(base_events.HikariEvent): +@attr.s(eq=False, hash=False, init=False, kw_only=True, slots=True) +class GuildMemberRemoveEvent(base_events.HikariEvent, base_models.Entity): """Used to represent Guild Member Remove gateway events. Sent when a member is kicked, banned or leaves a guild. @@ -196,8 +183,8 @@ class GuildMemberRemoveEvent(base_events.HikariEvent): @base_events.requires_intents(intents.Intent.GUILDS) -@attr.s(eq=False, hash=False, kw_only=True, slots=True) -class GuildRoleCreateEvent(base_events.HikariEvent): +@attr.s(eq=False, hash=False, init=False, kw_only=True, slots=True) +class GuildRoleCreateEvent(base_events.HikariEvent, base_models.Entity): """Used to represent a Guild Role Create gateway event.""" guild_id: snowflake.Snowflake = attr.ib(repr=True) @@ -208,8 +195,8 @@ class GuildRoleCreateEvent(base_events.HikariEvent): @base_events.requires_intents(intents.Intent.GUILDS) -@attr.s(eq=False, hash=False, kw_only=True, slots=True) -class GuildRoleUpdateEvent(base_events.HikariEvent): +@attr.s(eq=False, hash=False, init=False, kw_only=True, slots=True) +class GuildRoleUpdateEvent(base_events.HikariEvent, base_models.Entity): """Used to represent a Guild Role Create gateway event.""" # TODO: make any event with a guild ID into a custom base event. @@ -222,8 +209,8 @@ class GuildRoleUpdateEvent(base_events.HikariEvent): @base_events.requires_intents(intents.Intent.GUILDS) -@attr.s(eq=False, hash=False, kw_only=True, slots=True) -class GuildRoleDeleteEvent(base_events.HikariEvent): +@attr.s(eq=False, hash=False, init=False, kw_only=True, slots=True) +class GuildRoleDeleteEvent(base_events.HikariEvent, base_models.Entity): """Represents a gateway Guild Role Delete Event.""" guild_id: snowflake.Snowflake = attr.ib(repr=True) @@ -234,9 +221,12 @@ class GuildRoleDeleteEvent(base_events.HikariEvent): @base_events.requires_intents(intents.Intent.GUILD_PRESENCES) -@attr.s(eq=False, hash=False, kw_only=True, slots=True) -class PresenceUpdateEvent(base_events.HikariEvent, presences.MemberPresence): +@attr.s(auto_attribs=True, eq=False, hash=False, init=False, kw_only=True, slots=True) +class PresenceUpdateEvent(base_events.HikariEvent): """Used to represent Presence Update gateway events. Sent when a guild member changes their presence. """ + + presence: presences.MemberPresence + """The object of the presence being updated.""" diff --git a/hikari/events/message.py b/hikari/events/message.py index 0f2f299ebf..dc0f182add 100644 --- a/hikari/events/message.py +++ b/hikari/events/message.py @@ -22,6 +22,7 @@ __all__ = [ "MessageCreateEvent", + "UpdateMessage", "MessageUpdateEvent", "MessageDeleteEvent", "MessageDeleteBulkEvent", @@ -36,38 +37,39 @@ import attr from hikari.events import base as base_events -from hikari.models import applications from hikari.models import bases as base_models -from hikari.models import embeds as embed_models -from hikari.models import emojis -from hikari.models import guilds -from hikari.models import intents from hikari.models import messages -from hikari.models import users -from hikari.utilities import snowflake -from hikari.utilities import undefined +from hikari.models import intents if typing.TYPE_CHECKING: import datetime + from hikari.models import applications + from hikari.models import embeds as embed_models + from hikari.models import emojis + from hikari.models import guilds + from hikari.models import users + from hikari.utilities import snowflake + from hikari.utilities import undefined + @base_events.requires_intents(intents.Intent.GUILD_MESSAGES, intents.Intent.DIRECT_MESSAGES) -@attr.s(eq=False, hash=False, kw_only=True, slots=True) -class MessageCreateEvent(base_events.HikariEvent, messages.Message): +@attr.s(auto_attribs=True, eq=False, hash=False, init=False, kw_only=True, slots=True) +class MessageCreateEvent(base_events.HikariEvent): """Used to represent Message Create gateway events.""" + message: messages.Message -# This is an arbitrarily partial version of `messages.Message` -@base_events.requires_intents(intents.Intent.GUILD_MESSAGES, intents.Intent.DIRECT_MESSAGES) -@attr.s(eq=False, hash=False, kw_only=True, slots=True) -class MessageUpdateEvent(base_events.HikariEvent, base_models.Unique): - """Represents Message Update gateway events. - !!! note - All fields on this model except `MessageUpdateEvent.channel` and - `MessageUpdateEvent.id` may be set to `hikari.models.undefined.Undefined` (a singleton) - we have not received information about their state from Discord - alongside field nullability. +class UpdateMessage(messages.Message): + """An arbitrarily partial version of `hikari.models.messages.Message`. + + !!! warn + All fields on this model except `UpdateMessage.channel` and + `UpdateMessage.id` may be set to + `hikari.models.undefined.Undefined` (a singleton) if we have not + received information about their state from Discord alongside field + nullability. """ channel_id: snowflake.Snowflake = attr.ib(repr=True) @@ -149,8 +151,25 @@ class MessageUpdateEvent(base_events.HikariEvent, base_models.Unique): @base_events.requires_intents(intents.Intent.GUILD_MESSAGES, intents.Intent.DIRECT_MESSAGES) -@attr.s(eq=False, hash=False, kw_only=True, slots=True) -class MessageDeleteEvent(base_events.HikariEvent): +@attr.s(eq=False, hash=False, init=False, kw_only=True, slots=True) +class MessageUpdateEvent(base_events.HikariEvent, base_models.Unique): + """Represents Message Update gateway events. + + !!! warn + Unlike `MessageCreateEvent`, `MessageUpdateEvent.message` is an + arbitrarily partial version of `hikari.models.messages.Message` where + any field except `UpdateMessage.id` may be set to + `hikari.models.undefined.Undefined` (a singleton) to indicate that + it has not been changed. + """ + + message: UpdateMessage + """The partial message object with all updated fields.""" + + +@base_events.requires_intents(intents.Intent.GUILD_MESSAGES, intents.Intent.DIRECT_MESSAGES) +@attr.s(eq=False, hash=False, init=False, kw_only=True, slots=True) +class MessageDeleteEvent(base_events.HikariEvent, base_models.Entity): """Used to represent Message Delete gateway events. Sent when a message is deleted in a channel we have access to. @@ -171,9 +190,10 @@ class MessageDeleteEvent(base_events.HikariEvent): """The ID of the message that was deleted.""" +# TODO: if this doesn't apply to DMs then does guild_id need to be nullable here? @base_events.requires_intents(intents.Intent.GUILD_MESSAGES) -@attr.s(eq=False, hash=False, kw_only=True, slots=True) -class MessageDeleteBulkEvent(base_events.HikariEvent): +@attr.s(eq=False, hash=False, init=False, kw_only=True, slots=True) +class MessageDeleteBulkEvent(base_events.HikariEvent, base_models.Entity): """Used to represent Message Bulk Delete gateway events. Sent when multiple messages are deleted in a channel at once. @@ -192,28 +212,30 @@ class MessageDeleteBulkEvent(base_events.HikariEvent): """A collection of the IDs of the messages that were deleted.""" -@base_events.requires_intents(intents.Intent.GUILD_MESSAGE_REACTIONS, intents.Intent.DIRECT_MESSAGE_REACTIONS) -@attr.s(eq=False, hash=False, kw_only=True, slots=True) -class MessageReactionAddEvent(base_events.HikariEvent): - """Used to represent Message Reaction Add gateway events.""" - - # TODO: common base classes! - - user_id: snowflake.Snowflake = attr.ib(repr=True) - """The ID of the user adding the reaction.""" +class BaseMessageReactionEvent(base_events.HikariEvent, base_models.Entity): + """A base class that all message reaction events will inherit from.""" channel_id: snowflake.Snowflake = attr.ib(repr=True) - """The ID of the channel where this reaction is being added.""" + """The ID of the channel where this reaction is happening.""" message_id: snowflake.Snowflake = attr.ib(repr=True) - """The ID of the message this reaction is being added to.""" + """The ID of the message this reaction event is happening on.""" guild_id: typing.Optional[snowflake.Snowflake] = attr.ib(repr=True) - """The ID of the guild where this reaction is being added. + """The ID of the guild where this reaction event is happening. This will be `None` if this is happening in a DM channel. """ + +@base_events.requires_intents(intents.Intent.GUILD_MESSAGE_REACTIONS, intents.Intent.DIRECT_MESSAGE_REACTIONS) +@attr.s(eq=False, hash=False, init=False, kw_only=True, slots=True) +class MessageReactionAddEvent(BaseMessageReactionEvent): + """Used to represent Message Reaction Add gateway events.""" + + user_id: snowflake.Snowflake = attr.ib(repr=True) + """The ID of the user adding the reaction.""" + # TODO: does this contain a user? If not, should it be a PartialGuildMember? member: typing.Optional[guilds.Member] = attr.ib() """The member object of the user who's adding this reaction. @@ -226,63 +248,33 @@ class MessageReactionAddEvent(base_events.HikariEvent): @base_events.requires_intents(intents.Intent.GUILD_MESSAGE_REACTIONS, intents.Intent.DIRECT_MESSAGE_REACTIONS) -@attr.s(eq=False, hash=False, kw_only=True, slots=True) -class MessageReactionRemoveEvent(base_events.HikariEvent): +@attr.s(eq=False, hash=False, init=False, kw_only=True, slots=True) +class MessageReactionRemoveEvent(BaseMessageReactionEvent): """Used to represent Message Reaction Remove gateway events.""" user_id: snowflake.Snowflake = attr.ib(repr=True) """The ID of the user who is removing their reaction.""" - channel_id: snowflake.Snowflake = attr.ib(repr=True) - """The ID of the channel where this reaction is being removed.""" - - message_id: snowflake.Snowflake = attr.ib(repr=True) - """The ID of the message this reaction is being removed from.""" - - guild_id: typing.Optional[snowflake.Snowflake] = attr.ib(repr=True) - """The ID of the guild where this reaction is being removed - - This will be `None` if this event is happening in a DM channel. - """ - emoji: typing.Union[emojis.UnicodeEmoji, emojis.CustomEmoji] = attr.ib(repr=True) """The object of the emoji being removed.""" @base_events.requires_intents(intents.Intent.GUILD_MESSAGE_REACTIONS, intents.Intent.DIRECT_MESSAGE_REACTIONS) -@attr.s(eq=False, hash=False, kw_only=True, slots=True) -class MessageReactionRemoveAllEvent(base_events.HikariEvent): +@attr.s(eq=False, hash=False, init=False, kw_only=True, slots=True) +class MessageReactionRemoveAllEvent(BaseMessageReactionEvent): """Used to represent Message Reaction Remove All gateway events. Sent when all the reactions are removed from a message, regardless of emoji. """ - channel_id: snowflake.Snowflake = attr.ib(repr=True) - """The ID of the channel where the targeted message is.""" - - message_id: snowflake.Snowflake = attr.ib(repr=True) - """The ID of the message all reactions are being removed from.""" - - guild_id: typing.Optional[snowflake.Snowflake] = attr.ib(repr=True,) - """The ID of the guild where the targeted message is, if applicable.""" - @base_events.requires_intents(intents.Intent.GUILD_MESSAGE_REACTIONS, intents.Intent.DIRECT_MESSAGE_REACTIONS) -@attr.s(eq=False, hash=False, kw_only=True, slots=True) -class MessageReactionRemoveEmojiEvent(base_events.HikariEvent): +@attr.s(eq=False, hash=False, init=False, kw_only=True, slots=True) +class MessageReactionRemoveEmojiEvent(BaseMessageReactionEvent): """Represents Message Reaction Remove Emoji events. Sent when all the reactions for a single emoji are removed from a message. """ - channel_id: snowflake.Snowflake = attr.ib(repr=True) - """The ID of the channel where the targeted message is.""" - - guild_id: typing.Optional[snowflake.Snowflake] = attr.ib(repr=True) - """The ID of the guild where the targeted message is, if applicable.""" - - message_id: snowflake.Snowflake = attr.ib(repr=True) - """The ID of the message the reactions are being removed from.""" - emoji: typing.Union[emojis.UnicodeEmoji, emojis.CustomEmoji] = attr.ib(repr=True) """The object of the emoji that's being removed.""" diff --git a/hikari/events/other.py b/hikari/events/other.py index 5c69cf1a07..dfb537f061 100644 --- a/hikari/events/other.py +++ b/hikari/events/other.py @@ -38,17 +38,17 @@ import attr from hikari.events import base as base_events -from hikari.models import guilds -from hikari.models import users -from hikari.utilities import snowflake if typing.TYPE_CHECKING: + from hikari.models import guilds + from hikari.models import users from hikari.net import gateway as gateway_client + from hikari.utilities import snowflake # Synthetic event, is not deserialized, and is produced by the dispatcher. @base_events.no_catch() -@attr.s(auto_attribs=True, eq=False, hash=False, kw_only=True, slots=True) +@attr.s(auto_attribs=True, eq=False, hash=False, init=True, kw_only=True, slots=True) class ExceptionEvent(base_events.HikariEvent): """Descriptor for an exception thrown while processing an event.""" @@ -63,30 +63,31 @@ class ExceptionEvent(base_events.HikariEvent): # Synthetic event, is not deserialized -@attr.s(auto_attribs=True, eq=False, hash=False, kw_only=True, slots=True) +@attr.s(auto_attribs=True, eq=False, hash=False, init=False, kw_only=True, slots=True) class StartingEvent(base_events.HikariEvent): """Event that is fired before the gateway client starts all shards.""" # Synthetic event, is not deserialized -@attr.s(auto_attribs=True, eq=False, hash=False, kw_only=True, slots=True) +@attr.s(auto_attribs=True, eq=False, hash=False, init=False, kw_only=True, slots=True) class StartedEvent(base_events.HikariEvent): """Event that is fired when the gateway client starts all shards.""" # Synthetic event, is not deserialized -@attr.s(auto_attribs=True, eq=False, hash=False, kw_only=True, slots=True) +@attr.s(auto_attribs=True, eq=False, hash=False, init=False, kw_only=True, slots=True) class StoppingEvent(base_events.HikariEvent): """Event that is fired when the gateway client is instructed to disconnect all shards.""" # Synthetic event, is not deserialized -@attr.s(auto_attribs=True, eq=False, hash=False, kw_only=True, slots=True) +@attr.s(auto_attribs=True, eq=False, hash=False, init=False, kw_only=True, slots=True) class StoppedEvent(base_events.HikariEvent): """Event that is fired when the gateway client has finished disconnecting all shards.""" -@attr.s(auto_attribs=True, eq=False, hash=False, kw_only=True, slots=True) +# Synthetic event, is not deserialized +@attr.s(auto_attribs=True, eq=False, hash=False, init=True, kw_only=True, slots=True) class ConnectedEvent(base_events.HikariEvent): """Event invoked each time a shard connects.""" @@ -94,7 +95,8 @@ class ConnectedEvent(base_events.HikariEvent): """The shard that connected.""" -@attr.s(auto_attribs=True, eq=False, hash=False, kw_only=True, slots=True) +# Synthetic event, is not deserialized +@attr.s(auto_attribs=True, eq=False, hash=False, init=True, kw_only=True, slots=True) class DisconnectedEvent(base_events.HikariEvent): """Event invoked each time a shard disconnects.""" @@ -102,7 +104,8 @@ class DisconnectedEvent(base_events.HikariEvent): """The shard that disconnected.""" -@attr.s(auto_attribs=True, eq=False, hash=False, kw_only=True, slots=True) +# Synthetic event, is not deserialized +@attr.s(auto_attribs=True, eq=False, hash=False, init=True, kw_only=True, slots=True) class ResumedEvent(base_events.HikariEvent): """Represents a gateway Resume event.""" @@ -110,7 +113,7 @@ class ResumedEvent(base_events.HikariEvent): """The shard that reconnected.""" -@attr.s(eq=False, hash=False, kw_only=True, slots=True) +@attr.s(eq=False, hash=False, init=False, kw_only=True, slots=True) class ReadyEvent(base_events.HikariEvent): """Represents the gateway Ready event. @@ -132,29 +135,25 @@ class ReadyEvent(base_events.HikariEvent): session_id: str = attr.ib(repr=True) """The id of the current gateway session, used for reconnecting.""" - _shard_information: typing.Optional[typing.Tuple[int, int]] = attr.ib() - """Information about the current shard, only provided when IDENTIFYing.""" - - @property - def shard_id(self) -> typing.Optional[int]: - """Zero-indexed ID of the current shard. + shard_id: typing.Optional[int] = attr.ib(repr=True) + """Zero-indexed ID of the current shard. - This is only available if this ready event was received while IDENTIFYing. - """ - return self._shard_information[0] if self._shard_information else None + This is only available if this ready event was received while IDENTIFYing. + """ - @property - def shard_count(self) -> typing.Optional[int]: - """Total shard count for this bot. + shard_count: typing.Optional[int] = attr.ib(repr=True) + """Total shard count for this bot. - This is only available if this ready event was received while IDENTIFYing. - """ - return self._shard_information[1] if self._shard_information else None + This is only available if this ready event was received while IDENTIFYing. + """ -@attr.s(eq=False, hash=False, kw_only=True, slots=True) +@attr.s(eq=False, hash=False, init=False, kw_only=True, slots=True) class OwnUserUpdateEvent(base_events.HikariEvent, users.OwnUser): """Used to represent User Update gateway events. Sent when the current user is updated. """ + + my_user: users.MyUser = attr.ib(repr=True) + """The updated object of the current application's user.""" diff --git a/hikari/events/voice.py b/hikari/events/voice.py index 8e772875ba..ec63f3b445 100644 --- a/hikari/events/voice.py +++ b/hikari/events/voice.py @@ -22,24 +22,31 @@ __all__ = ["VoiceStateUpdateEvent", "VoiceServerUpdateEvent"] +import typing + import attr from hikari.events import base as base_events from hikari.models import intents -from hikari.models import voices -from hikari.utilities import snowflake + +if typing.TYPE_CHECKING: + from hikari.models import voices + from hikari.utilities import snowflake @base_events.requires_intents(intents.Intent.GUILD_VOICE_STATES) -@attr.s(eq=False, hash=False, kw_only=True, slots=True) -class VoiceStateUpdateEvent(base_events.HikariEvent, voices.VoiceState): +@attr.s(eq=False, hash=False, init=False, kw_only=True, slots=True) +class VoiceStateUpdateEvent(base_events.HikariEvent): """Used to represent voice state update gateway events. Sent when a user joins, leaves or moves voice channel(s). """ + state: voices.VoiceState = attr.ib() + """The object of the voice state that's being updated.""" + -@attr.s(eq=False, hash=False, kw_only=True, slots=True) +@attr.s(eq=False, hash=False, init=False, kw_only=True, slots=True) class VoiceServerUpdateEvent(base_events.HikariEvent): """Used to represent voice server update gateway events. diff --git a/hikari/impl/entity_factory.py b/hikari/impl/entity_factory.py index 3300da9290..505b811b2e 100644 --- a/hikari/impl/entity_factory.py +++ b/hikari/impl/entity_factory.py @@ -27,21 +27,26 @@ from hikari.api import app as app_ from hikari.api import entity_factory -from hikari.models import applications -from hikari.models import audit_logs -from hikari.models import channels as channels_ -from hikari.models import colors -from hikari.models import embeds -from hikari.models import emojis -from hikari.models import gateway -from hikari.models import guilds -from hikari.models import invites -from hikari.models import messages -from hikari.models import permissions -from hikari.models import presences as presences_ -from hikari.models import users -from hikari.models import voices -from hikari.models import webhooks +from hikari.events import channel as channel_events +from hikari.events import guild as guild_events +from hikari.events import message as message_events +from hikari.events import other as other_events +from hikari.events import voice as voice_events +from hikari.models import applications as application_models +from hikari.models import audit_logs as audit_log_models +from hikari.models import channels as channel_models +from hikari.models import colors as color_models +from hikari.models import embeds as embed_models +from hikari.models import emojis as emoji_models +from hikari.models import gateway as gateway_models +from hikari.models import guilds as guild_models +from hikari.models import invites as invite_model +from hikari.models import messages as message_models +from hikari.models import permissions as permission_models +from hikari.models import presences as presence_models +from hikari.models import users as user_models +from hikari.models import voices as voice_models +from hikari.models import webhooks as webhook_models from hikari.utilities import date from hikari.utilities import snowflake from hikari.utilities import undefined @@ -49,13 +54,15 @@ if typing.TYPE_CHECKING: from hikari.utilities import data_binding -DMChannelT = typing.TypeVar("DMChannelT", bound=channels_.DMChannel) -GuildChannelT = typing.TypeVar("GuildChannelT", bound=channels_.GuildChannel) -InviteT = typing.TypeVar("InviteT", bound=invites.Invite) -PartialChannelT = typing.TypeVar("PartialChannelT", bound=channels_.PartialChannel) -PartialGuildT = typing.TypeVar("PartialGuildT", bound=guilds.PartialGuild) -PartialGuildIntegrationT = typing.TypeVar("PartialGuildIntegrationT", bound=guilds.PartialIntegration) -UserT = typing.TypeVar("UserT", bound=users.User) +DMChannelT = typing.TypeVar("DMChannelT", bound=channel_models.DMChannel) +GuildChannelT = typing.TypeVar("GuildChannelT", bound=channel_models.GuildChannel) +InviteT = typing.TypeVar("InviteT", bound=invite_model.Invite) +PartialChannelT = typing.TypeVar("PartialChannelT", bound=channel_models.PartialChannel) +PartialGuildT = typing.TypeVar("PartialGuildT", bound=guild_models.PartialGuild) +PartialGuildIntegrationT = typing.TypeVar("PartialGuildIntegrationT", bound=guild_models.PartialIntegration) +UserT = typing.TypeVar("UserT", bound=user_models.User) +ReactionEventT = typing.TypeVar("ReactionEventT", bound=message_events.BaseMessageReactionEvent) +GuildBanEventT = typing.TypeVar("GuildBanEventT", bound=guild_events.BaseGuildBanEvent) def _deserialize_seconds_timedelta(seconds: typing.Union[str, int]) -> datetime.timedelta: @@ -83,70 +90,70 @@ class EntityFactoryImpl(entity_factory.IEntityFactory): def __init__(self, app: app_.IApp) -> None: self._app = app self._audit_log_entry_converters = { - audit_logs.AuditLogChangeKey.OWNER_ID: snowflake.Snowflake, - audit_logs.AuditLogChangeKey.AFK_CHANNEL_ID: snowflake.Snowflake, - audit_logs.AuditLogChangeKey.AFK_TIMEOUT: _deserialize_seconds_timedelta, - audit_logs.AuditLogChangeKey.MFA_LEVEL: guilds.GuildMFALevel, - audit_logs.AuditLogChangeKey.VERIFICATION_LEVEL: guilds.GuildVerificationLevel, - audit_logs.AuditLogChangeKey.EXPLICIT_CONTENT_FILTER: guilds.GuildExplicitContentFilterLevel, - audit_logs.AuditLogChangeKey.DEFAULT_MESSAGE_NOTIFICATIONS: guilds.GuildMessageNotificationsLevel, - audit_logs.AuditLogChangeKey.PRUNE_DELETE_DAYS: _deserialize_day_timedelta, - audit_logs.AuditLogChangeKey.WIDGET_CHANNEL_ID: snowflake.Snowflake, - audit_logs.AuditLogChangeKey.POSITION: int, - audit_logs.AuditLogChangeKey.BITRATE: int, - audit_logs.AuditLogChangeKey.APPLICATION_ID: snowflake.Snowflake, - audit_logs.AuditLogChangeKey.PERMISSIONS: permissions.Permission, - audit_logs.AuditLogChangeKey.COLOR: colors.Color, - audit_logs.AuditLogChangeKey.ALLOW: permissions.Permission, - audit_logs.AuditLogChangeKey.DENY: permissions.Permission, - audit_logs.AuditLogChangeKey.CHANNEL_ID: snowflake.Snowflake, - audit_logs.AuditLogChangeKey.INVITER_ID: snowflake.Snowflake, - audit_logs.AuditLogChangeKey.MAX_USES: _deserialize_max_uses, - audit_logs.AuditLogChangeKey.USES: int, - audit_logs.AuditLogChangeKey.MAX_AGE: _deserialize_max_age, - audit_logs.AuditLogChangeKey.ID: snowflake.Snowflake, - audit_logs.AuditLogChangeKey.TYPE: str, - audit_logs.AuditLogChangeKey.ENABLE_EMOTICONS: bool, - audit_logs.AuditLogChangeKey.EXPIRE_BEHAVIOR: guilds.IntegrationExpireBehaviour, - audit_logs.AuditLogChangeKey.EXPIRE_GRACE_PERIOD: _deserialize_day_timedelta, - audit_logs.AuditLogChangeKey.RATE_LIMIT_PER_USER: _deserialize_seconds_timedelta, - audit_logs.AuditLogChangeKey.SYSTEM_CHANNEL_ID: snowflake.Snowflake, - audit_logs.AuditLogChangeKey.ADD_ROLE_TO_MEMBER: self._deserialize_audit_log_change_roles, - audit_logs.AuditLogChangeKey.REMOVE_ROLE_FROM_MEMBER: self._deserialize_audit_log_change_roles, - audit_logs.AuditLogChangeKey.PERMISSION_OVERWRITES: self._deserialize_audit_log_overwrites, + audit_log_models.AuditLogChangeKey.OWNER_ID: snowflake.Snowflake, + audit_log_models.AuditLogChangeKey.AFK_CHANNEL_ID: snowflake.Snowflake, + audit_log_models.AuditLogChangeKey.AFK_TIMEOUT: _deserialize_seconds_timedelta, + audit_log_models.AuditLogChangeKey.MFA_LEVEL: guild_models.GuildMFALevel, + audit_log_models.AuditLogChangeKey.VERIFICATION_LEVEL: guild_models.GuildVerificationLevel, + audit_log_models.AuditLogChangeKey.EXPLICIT_CONTENT_FILTER: guild_models.GuildExplicitContentFilterLevel, + audit_log_models.AuditLogChangeKey.DEFAULT_MESSAGE_NOTIFICATIONS: guild_models.GuildMessageNotificationsLevel, + audit_log_models.AuditLogChangeKey.PRUNE_DELETE_DAYS: _deserialize_day_timedelta, + audit_log_models.AuditLogChangeKey.WIDGET_CHANNEL_ID: snowflake.Snowflake, + audit_log_models.AuditLogChangeKey.POSITION: int, + audit_log_models.AuditLogChangeKey.BITRATE: int, + audit_log_models.AuditLogChangeKey.APPLICATION_ID: snowflake.Snowflake, + audit_log_models.AuditLogChangeKey.PERMISSIONS: permission_models.Permission, + audit_log_models.AuditLogChangeKey.COLOR: color_models.Color, + audit_log_models.AuditLogChangeKey.ALLOW: permission_models.Permission, + audit_log_models.AuditLogChangeKey.DENY: permission_models.Permission, + audit_log_models.AuditLogChangeKey.CHANNEL_ID: snowflake.Snowflake, + audit_log_models.AuditLogChangeKey.INVITER_ID: snowflake.Snowflake, + audit_log_models.AuditLogChangeKey.MAX_USES: _deserialize_max_uses, + audit_log_models.AuditLogChangeKey.USES: int, + audit_log_models.AuditLogChangeKey.MAX_AGE: _deserialize_max_age, + audit_log_models.AuditLogChangeKey.ID: snowflake.Snowflake, + audit_log_models.AuditLogChangeKey.TYPE: str, + audit_log_models.AuditLogChangeKey.ENABLE_EMOTICONS: bool, + audit_log_models.AuditLogChangeKey.EXPIRE_BEHAVIOR: guild_models.IntegrationExpireBehaviour, + audit_log_models.AuditLogChangeKey.EXPIRE_GRACE_PERIOD: _deserialize_day_timedelta, + audit_log_models.AuditLogChangeKey.RATE_LIMIT_PER_USER: _deserialize_seconds_timedelta, + audit_log_models.AuditLogChangeKey.SYSTEM_CHANNEL_ID: snowflake.Snowflake, + audit_log_models.AuditLogChangeKey.ADD_ROLE_TO_MEMBER: self._deserialize_audit_log_change_roles, + audit_log_models.AuditLogChangeKey.REMOVE_ROLE_FROM_MEMBER: self._deserialize_audit_log_change_roles, + audit_log_models.AuditLogChangeKey.PERMISSION_OVERWRITES: self._deserialize_audit_log_overwrites, } self._audit_log_event_mapping = { - audit_logs.AuditLogEventType.CHANNEL_OVERWRITE_CREATE: self._deserialize_channel_overwrite_entry_info, - audit_logs.AuditLogEventType.CHANNEL_OVERWRITE_UPDATE: self._deserialize_channel_overwrite_entry_info, - audit_logs.AuditLogEventType.CHANNEL_OVERWRITE_DELETE: self._deserialize_channel_overwrite_entry_info, - audit_logs.AuditLogEventType.MESSAGE_PIN: self._deserialize_message_pin_entry_info, - audit_logs.AuditLogEventType.MESSAGE_UNPIN: self._deserialize_message_pin_entry_info, - audit_logs.AuditLogEventType.MEMBER_PRUNE: self._deserialize_member_prune_entry_info, - audit_logs.AuditLogEventType.MESSAGE_BULK_DELETE: self._deserialize_message_bulk_delete_entry_info, - audit_logs.AuditLogEventType.MESSAGE_DELETE: self._deserialize_message_delete_entry_info, - audit_logs.AuditLogEventType.MEMBER_DISCONNECT: self._deserialize_member_disconnect_entry_info, - audit_logs.AuditLogEventType.MEMBER_MOVE: self._deserialize_member_move_entry_info, + audit_log_models.AuditLogEventType.CHANNEL_OVERWRITE_CREATE: self._deserialize_channel_overwrite_entry_info, + audit_log_models.AuditLogEventType.CHANNEL_OVERWRITE_UPDATE: self._deserialize_channel_overwrite_entry_info, + audit_log_models.AuditLogEventType.CHANNEL_OVERWRITE_DELETE: self._deserialize_channel_overwrite_entry_info, + audit_log_models.AuditLogEventType.MESSAGE_PIN: self._deserialize_message_pin_entry_info, + audit_log_models.AuditLogEventType.MESSAGE_UNPIN: self._deserialize_message_pin_entry_info, + audit_log_models.AuditLogEventType.MEMBER_PRUNE: self._deserialize_member_prune_entry_info, + audit_log_models.AuditLogEventType.MESSAGE_BULK_DELETE: self._deserialize_message_bulk_delete_entry_info, + audit_log_models.AuditLogEventType.MESSAGE_DELETE: self._deserialize_message_delete_entry_info, + audit_log_models.AuditLogEventType.MEMBER_DISCONNECT: self._deserialize_member_disconnect_entry_info, + audit_log_models.AuditLogEventType.MEMBER_MOVE: self._deserialize_member_move_entry_info, } self._channel_type_mapping = { - channels_.ChannelType.DM: self.deserialize_dm_channel, - channels_.ChannelType.GROUP_DM: self.deserialize_group_dm_channel, - channels_.ChannelType.GUILD_CATEGORY: self.deserialize_guild_category, - channels_.ChannelType.GUILD_TEXT: self.deserialize_guild_text_channel, - channels_.ChannelType.GUILD_NEWS: self.deserialize_guild_news_channel, - channels_.ChannelType.GUILD_STORE: self.deserialize_guild_store_channel, - channels_.ChannelType.GUILD_VOICE: self.deserialize_guild_voice_channel, + channel_models.ChannelType.DM: self.deserialize_dm_channel, + channel_models.ChannelType.GROUP_DM: self.deserialize_group_dm_channel, + channel_models.ChannelType.GUILD_CATEGORY: self.deserialize_guild_category, + channel_models.ChannelType.GUILD_TEXT: self.deserialize_guild_text_channel, + channel_models.ChannelType.GUILD_NEWS: self.deserialize_guild_news_channel, + channel_models.ChannelType.GUILD_STORE: self.deserialize_guild_store_channel, + channel_models.ChannelType.GUILD_VOICE: self.deserialize_guild_voice_channel, } @property def app(self) -> app_.IApp: return self._app - ################ - # APPLICATIONS # - ################ + ###################### + # APPLICATION MODELS # + ###################### - def deserialize_own_connection(self, payload: data_binding.JSONObject) -> applications.OwnConnection: - own_connection = applications.OwnConnection() + def deserialize_own_connection(self, payload: data_binding.JSONObject) -> application_models.OwnConnection: + own_connection = application_models.OwnConnection() own_connection.id = snowflake.Snowflake(payload["id"]) own_connection.name = payload["name"] own_connection.type = payload["type"] @@ -158,18 +165,18 @@ def deserialize_own_connection(self, payload: data_binding.JSONObject) -> applic own_connection.is_friend_sync_enabled = payload["friend_sync"] own_connection.is_activity_visible = payload["show_activity"] # noinspection PyArgumentList - own_connection.visibility = applications.ConnectionVisibility(payload["visibility"]) + own_connection.visibility = application_models.ConnectionVisibility(payload["visibility"]) return own_connection - def deserialize_own_guild(self, payload: data_binding.JSONObject) -> applications.OwnGuild: - own_guild = self._set_partial_guild_attributes(payload, applications.OwnGuild()) + def deserialize_own_guild(self, payload: data_binding.JSONObject) -> application_models.OwnGuild: + own_guild = self._set_partial_guild_attributes(payload, application_models.OwnGuild()) own_guild.is_owner = bool(payload["owner"]) # noinspection PyArgumentList - own_guild.my_permissions = permissions.Permission(payload["permissions"]) + own_guild.my_permissions = permission_models.Permission(payload["permissions"]) return own_guild - def deserialize_application(self, payload: data_binding.JSONObject) -> applications.Application: - application = applications.Application() + def deserialize_application(self, payload: data_binding.JSONObject) -> application_models.Application: + application = application_models.Application() application.set_app(self._app) application.id = snowflake.Snowflake(payload["id"]) application.name = payload["name"] @@ -183,17 +190,19 @@ def deserialize_application(self, payload: data_binding.JSONObject) -> applicati application.icon_hash = payload.get("icon") if (team_payload := payload.get("team")) is not None: - team = applications.Team() + team = application_models.Team() team.set_app(self._app) team.id = snowflake.Snowflake(team_payload["id"]) team.icon_hash = team_payload["icon"] members = {} for member_payload in team_payload["members"]: - team_member = applications.TeamMember() + team_member = application_models.TeamMember() team_member.set_app(self._app) # noinspection PyArgumentList - team_member.membership_state = applications.TeamMembershipState(member_payload["membership_state"]) + team_member.membership_state = application_models.TeamMembershipState( + member_payload["membership_state"] + ) team_member.permissions = set(member_payload["permissions"]) team_member.team_id = snowflake.Snowflake(member_payload["team_id"]) team_member.user = self.deserialize_user(member_payload["user"]) @@ -213,16 +222,16 @@ def deserialize_application(self, payload: data_binding.JSONObject) -> applicati application.cover_image_hash = payload.get("cover_image") return application - ############## - # AUDIT_LOGS # - ############## + ##################### + # AUDIT LOGS MODELS # + ##################### def _deserialize_audit_log_change_roles( self, payload: data_binding.JSONArray - ) -> typing.Mapping[snowflake.Snowflake, guilds.PartialRole]: + ) -> typing.Mapping[snowflake.Snowflake, guild_models.PartialRole]: roles = {} for role_payload in payload: - role = guilds.PartialRole() + role = guild_models.PartialRole() role.set_app(self._app) role.id = snowflake.Snowflake(role_payload["id"]) role.name = role_payload["name"] @@ -231,7 +240,7 @@ def _deserialize_audit_log_change_roles( def _deserialize_audit_log_overwrites( self, payload: data_binding.JSONArray - ) -> typing.Mapping[snowflake.Snowflake, channels_.PermissionOverwrite]: + ) -> typing.Mapping[snowflake.Snowflake, channel_models.PermissionOverwrite]: return { snowflake.Snowflake(overwrite["id"]): self.deserialize_permission_overwrite(overwrite) for overwrite in payload @@ -240,24 +249,24 @@ def _deserialize_audit_log_overwrites( @staticmethod def _deserialize_channel_overwrite_entry_info( payload: data_binding.JSONObject, - ) -> audit_logs.ChannelOverwriteEntryInfo: - channel_overwrite_entry_info = audit_logs.ChannelOverwriteEntryInfo() + ) -> audit_log_models.ChannelOverwriteEntryInfo: + channel_overwrite_entry_info = audit_log_models.ChannelOverwriteEntryInfo() channel_overwrite_entry_info.id = snowflake.Snowflake(payload["id"]) # noinspection PyArgumentList - channel_overwrite_entry_info.type = channels_.PermissionOverwriteType(payload["type"]) + channel_overwrite_entry_info.type = channel_models.PermissionOverwriteType(payload["type"]) channel_overwrite_entry_info.role_name = payload.get("role_name") return channel_overwrite_entry_info @staticmethod - def _deserialize_message_pin_entry_info(payload: data_binding.JSONObject) -> audit_logs.MessagePinEntryInfo: - message_pin_entry_info = audit_logs.MessagePinEntryInfo() + def _deserialize_message_pin_entry_info(payload: data_binding.JSONObject) -> audit_log_models.MessagePinEntryInfo: + message_pin_entry_info = audit_log_models.MessagePinEntryInfo() message_pin_entry_info.channel_id = snowflake.Snowflake(payload["channel_id"]) message_pin_entry_info.message_id = snowflake.Snowflake(payload["message_id"]) return message_pin_entry_info @staticmethod - def _deserialize_member_prune_entry_info(payload: data_binding.JSONObject) -> audit_logs.MemberPruneEntryInfo: - member_prune_entry_info = audit_logs.MemberPruneEntryInfo() + def _deserialize_member_prune_entry_info(payload: data_binding.JSONObject) -> audit_log_models.MemberPruneEntryInfo: + member_prune_entry_info = audit_log_models.MemberPruneEntryInfo() member_prune_entry_info.delete_member_days = datetime.timedelta(days=int(payload["delete_member_days"])) member_prune_entry_info.members_removed = int(payload["members_removed"]) return member_prune_entry_info @@ -265,14 +274,16 @@ def _deserialize_member_prune_entry_info(payload: data_binding.JSONObject) -> au @staticmethod def _deserialize_message_bulk_delete_entry_info( payload: data_binding.JSONObject, - ) -> audit_logs.MessageBulkDeleteEntryInfo: - message_bulk_delete_entry_info = audit_logs.MessageBulkDeleteEntryInfo() + ) -> audit_log_models.MessageBulkDeleteEntryInfo: + message_bulk_delete_entry_info = audit_log_models.MessageBulkDeleteEntryInfo() message_bulk_delete_entry_info.count = int(payload["count"]) return message_bulk_delete_entry_info @staticmethod - def _deserialize_message_delete_entry_info(payload: data_binding.JSONObject) -> audit_logs.MessageDeleteEntryInfo: - message_delete_entry_info = audit_logs.MessageDeleteEntryInfo() + def _deserialize_message_delete_entry_info( + payload: data_binding.JSONObject, + ) -> audit_log_models.MessageDeleteEntryInfo: + message_delete_entry_info = audit_log_models.MessageDeleteEntryInfo() message_delete_entry_info.channel_id = snowflake.Snowflake(payload["channel_id"]) message_delete_entry_info.count = int(payload["count"]) return message_delete_entry_info @@ -280,14 +291,14 @@ def _deserialize_message_delete_entry_info(payload: data_binding.JSONObject) -> @staticmethod def _deserialize_member_disconnect_entry_info( payload: data_binding.JSONObject, - ) -> audit_logs.MemberDisconnectEntryInfo: - member_disconnect_entry_info = audit_logs.MemberDisconnectEntryInfo() + ) -> audit_log_models.MemberDisconnectEntryInfo: + member_disconnect_entry_info = audit_log_models.MemberDisconnectEntryInfo() member_disconnect_entry_info.count = int(payload["count"]) return member_disconnect_entry_info @staticmethod - def _deserialize_member_move_entry_info(payload: data_binding.JSONObject) -> audit_logs.MemberMoveEntryInfo: - member_move_entry_info = audit_logs.MemberMoveEntryInfo() + def _deserialize_member_move_entry_info(payload: data_binding.JSONObject) -> audit_log_models.MemberMoveEntryInfo: + member_move_entry_info = audit_log_models.MemberMoveEntryInfo() member_move_entry_info.channel_id = snowflake.Snowflake(payload["channel_id"]) member_move_entry_info.count = int(payload["count"]) return member_move_entry_info @@ -295,15 +306,15 @@ def _deserialize_member_move_entry_info(payload: data_binding.JSONObject) -> aud @staticmethod def _deserialize_unrecognised_audit_log_entry_info( payload: data_binding.JSONObject, - ) -> audit_logs.UnrecognisedAuditLogEntryInfo: - return audit_logs.UnrecognisedAuditLogEntryInfo(payload) + ) -> audit_log_models.UnrecognisedAuditLogEntryInfo: + return audit_log_models.UnrecognisedAuditLogEntryInfo(payload) - def deserialize_audit_log(self, payload: data_binding.JSONObject) -> audit_logs.AuditLog: - audit_log = audit_logs.AuditLog() + def deserialize_audit_log(self, payload: data_binding.JSONObject) -> audit_log_models.AuditLog: + audit_log = audit_log_models.AuditLog() entries = {} for entry_payload in payload["audit_log_entries"]: - entry = audit_logs.AuditLogEntry() + entry = audit_log_models.AuditLogEntry() entry.set_app(self._app) entry.id = snowflake.Snowflake(entry_payload["id"]) @@ -313,11 +324,11 @@ def deserialize_audit_log(self, payload: data_binding.JSONObject) -> audit_logs. changes = [] for change_payload in entry_payload.get("changes", ()): - change = audit_logs.AuditLogChange() + change = audit_log_models.AuditLogChange() try: # noinspection PyArgumentList - change.key = audit_logs.AuditLogChangeKey(change_payload["key"]) + change.key = audit_log_models.AuditLogChangeKey(change_payload["key"]) except ValueError: change.key = change_payload["key"] @@ -338,7 +349,7 @@ def deserialize_audit_log(self, payload: data_binding.JSONObject) -> audit_logs. try: # noinspection PyArgumentList - entry.action_type = audit_logs.AuditLogEventType(entry_payload["action_type"]) + entry.action_type = audit_log_models.AuditLogEventType(entry_payload["action_type"]) except ValueError: entry.action_type = entry_payload["action_type"] @@ -364,22 +375,22 @@ def deserialize_audit_log(self, payload: data_binding.JSONObject) -> audit_logs. } return audit_log - ############ - # CHANNELS # - ############ + ################## + # CHANNEL MODELS # + ################## - def deserialize_permission_overwrite(self, payload: data_binding.JSONObject) -> channels_.PermissionOverwrite: + def deserialize_permission_overwrite(self, payload: data_binding.JSONObject) -> channel_models.PermissionOverwrite: # noinspection PyArgumentList - permission_overwrite = channels_.PermissionOverwrite( - id=snowflake.Snowflake(payload["id"]), type=channels_.PermissionOverwriteType(payload["type"]), + permission_overwrite = channel_models.PermissionOverwrite( + id=snowflake.Snowflake(payload["id"]), type=channel_models.PermissionOverwriteType(payload["type"]), ) # noinspection PyArgumentList - permission_overwrite.allow = permissions.Permission(payload["allow"]) + permission_overwrite.allow = permission_models.Permission(payload["allow"]) # noinspection PyArgumentList - permission_overwrite.deny = permissions.Permission(payload["deny"]) + permission_overwrite.deny = permission_models.Permission(payload["deny"]) return permission_overwrite - def serialize_permission_overwrite(self, overwrite: channels_.PermissionOverwrite) -> data_binding.JSONObject: + def serialize_permission_overwrite(self, overwrite: channel_models.PermissionOverwrite) -> data_binding.JSONObject: return {"id": str(overwrite.id), "type": overwrite.type, "allow": overwrite.allow, "deny": overwrite.deny} def _set_partial_channel_attributes( @@ -389,11 +400,11 @@ def _set_partial_channel_attributes( channel.id = snowflake.Snowflake(payload["id"]) channel.name = payload.get("name") # noinspection PyArgumentList - channel.type = channels_.ChannelType(payload["type"]) + channel.type = channel_models.ChannelType(payload["type"]) return channel - def deserialize_partial_channel(self, payload: data_binding.JSONObject) -> channels_.PartialChannel: - return self._set_partial_channel_attributes(payload, channels_.PartialChannel()) + def deserialize_partial_channel(self, payload: data_binding.JSONObject) -> channel_models.PartialChannel: + return self._set_partial_channel_attributes(payload, channel_models.PartialChannel()) def _set_dm_channel_attributes(self, payload: data_binding.JSONObject, channel: DMChannelT) -> DMChannelT: channel = self._set_partial_channel_attributes(payload, channel) @@ -407,13 +418,16 @@ def _set_dm_channel_attributes(self, payload: data_binding.JSONObject, channel: } return channel - def deserialize_dm_channel(self, payload: data_binding.JSONObject) -> channels_.DMChannel: - return self._set_dm_channel_attributes(payload, channels_.DMChannel()) + def deserialize_dm_channel(self, payload: data_binding.JSONObject) -> channel_models.DMChannel: + return self._set_dm_channel_attributes(payload, channel_models.DMChannel()) - def deserialize_group_dm_channel(self, payload: data_binding.JSONObject) -> channels_.GroupDMChannel: - group_dm_channel = self._set_dm_channel_attributes(payload, channels_.GroupDMChannel()) + def deserialize_group_dm_channel(self, payload: data_binding.JSONObject) -> channel_models.GroupDMChannel: + group_dm_channel = self._set_dm_channel_attributes(payload, channel_models.GroupDMChannel()) group_dm_channel.owner_id = snowflake.Snowflake(payload["owner_id"]) group_dm_channel.icon_hash = payload["icon"] + group_dm_channel.nicknames = { + snowflake.Snowflake(entry["id"]): entry["nick"] for entry in payload.get("nicks", ()) + } group_dm_channel.application_id = ( snowflake.Snowflake(payload["application_id"]) if "application_id" in payload else None ) @@ -437,11 +451,11 @@ def _set_guild_channel_attributes(self, payload: data_binding.JSONObject, channe return channel - def deserialize_guild_category(self, payload: data_binding.JSONObject) -> channels_.GuildCategory: - return self._set_guild_channel_attributes(payload, channels_.GuildCategory()) + def deserialize_guild_category(self, payload: data_binding.JSONObject) -> channel_models.GuildCategory: + return self._set_guild_channel_attributes(payload, channel_models.GuildCategory()) - def deserialize_guild_text_channel(self, payload: data_binding.JSONObject) -> channels_.GuildTextChannel: - guild_text_category = self._set_guild_channel_attributes(payload, channels_.GuildTextChannel()) + def deserialize_guild_text_channel(self, payload: data_binding.JSONObject) -> channel_models.GuildTextChannel: + guild_text_category = self._set_guild_channel_attributes(payload, channel_models.GuildTextChannel()) guild_text_category.topic = payload["topic"] if (last_message_id := payload["last_message_id"]) is not None: @@ -456,8 +470,8 @@ def deserialize_guild_text_channel(self, payload: data_binding.JSONObject) -> ch return guild_text_category - def deserialize_guild_news_channel(self, payload: data_binding.JSONObject) -> channels_.GuildNewsChannel: - guild_news_channel = self._set_guild_channel_attributes(payload, channels_.GuildNewsChannel()) + def deserialize_guild_news_channel(self, payload: data_binding.JSONObject) -> channel_models.GuildNewsChannel: + guild_news_channel = self._set_guild_channel_attributes(payload, channel_models.GuildNewsChannel()) guild_news_channel.topic = payload["topic"] if (last_message_id := payload["last_message_id"]) is not None: @@ -470,35 +484,35 @@ def deserialize_guild_news_channel(self, payload: data_binding.JSONObject) -> ch return guild_news_channel - def deserialize_guild_store_channel(self, payload: data_binding.JSONObject) -> channels_.GuildStoreChannel: - return self._set_guild_channel_attributes(payload, channels_.GuildStoreChannel()) + def deserialize_guild_store_channel(self, payload: data_binding.JSONObject) -> channel_models.GuildStoreChannel: + return self._set_guild_channel_attributes(payload, channel_models.GuildStoreChannel()) - def deserialize_guild_voice_channel(self, payload: data_binding.JSONObject) -> channels_.GuildVoiceChannel: - guild_voice_channel = self._set_guild_channel_attributes(payload, channels_.GuildVoiceChannel()) + def deserialize_guild_voice_channel(self, payload: data_binding.JSONObject) -> channel_models.GuildVoiceChannel: + guild_voice_channel = self._set_guild_channel_attributes(payload, channel_models.GuildVoiceChannel()) guild_voice_channel.bitrate = int(payload["bitrate"]) guild_voice_channel.user_limit = int(payload["user_limit"]) return guild_voice_channel - def deserialize_channel(self, payload: data_binding.JSONObject) -> channels_.PartialChannel: + def deserialize_channel(self, payload: data_binding.JSONObject) -> channel_models.PartialChannel: # noinspection PyArgumentList return self._channel_type_mapping[payload["type"]](payload) - ########## - # EMBEDS # - ########## + ################ + # EMBED MODELS # + ################ - def deserialize_embed(self, payload: data_binding.JSONObject) -> embeds.Embed: - embed = embeds.Embed() + def deserialize_embed(self, payload: data_binding.JSONObject) -> embed_models.Embed: + embed = embed_models.Embed() embed.title = payload.get("title") embed.description = payload.get("description") embed.url = payload.get("url") embed.timestamp = ( date.iso8601_datetime_string_to_datetime(payload["timestamp"]) if "timestamp" in payload else None ) - embed.color = colors.Color(payload["color"]) if "color" in payload else None + embed.color = color_models.Color(payload["color"]) if "color" in payload else None if (footer_payload := payload.get("footer", ...)) is not ...: - footer = embeds.EmbedFooter() + footer = embed_models.EmbedFooter() footer.text = footer_payload["text"] footer.icon_url = footer_payload.get("icon_url") footer.proxy_icon_url = footer_payload.get("proxy_icon_url") @@ -507,7 +521,7 @@ def deserialize_embed(self, payload: data_binding.JSONObject) -> embeds.Embed: embed.footer = None if (image_payload := payload.get("image", ...)) is not ...: - image = embeds.EmbedImage() + image = embed_models.EmbedImage() image.url = image_payload.get("url") image.proxy_url = image_payload.get("proxy_url") image.height = int(image_payload["height"]) if "height" in image_payload else None @@ -517,7 +531,7 @@ def deserialize_embed(self, payload: data_binding.JSONObject) -> embeds.Embed: embed.image = None if (thumbnail_payload := payload.get("thumbnail", ...)) is not ...: - thumbnail = embeds.EmbedThumbnail() + thumbnail = embed_models.EmbedThumbnail() thumbnail.url = thumbnail_payload.get("url") thumbnail.proxy_url = thumbnail_payload.get("proxy_url") thumbnail.height = int(thumbnail_payload["height"]) if "height" in thumbnail_payload else None @@ -527,7 +541,7 @@ def deserialize_embed(self, payload: data_binding.JSONObject) -> embeds.Embed: embed.thumbnail = None if (video_payload := payload.get("video", ...)) is not ...: - video = embeds.EmbedVideo() + video = embed_models.EmbedVideo() video.url = video_payload.get("url") video.height = int(video_payload["height"]) if "height" in video_payload else None video.width = int(video_payload["width"]) if "width" in video_payload else None @@ -536,7 +550,7 @@ def deserialize_embed(self, payload: data_binding.JSONObject) -> embeds.Embed: embed.video = None if (provider_payload := payload.get("provider", ...)) is not ...: - provider = embeds.EmbedProvider() + provider = embed_models.EmbedProvider() provider.name = provider_payload.get("name") provider.url = provider_payload.get("url") embed.provider = provider @@ -544,7 +558,7 @@ def deserialize_embed(self, payload: data_binding.JSONObject) -> embeds.Embed: embed.provider = None if (author_payload := payload.get("author", ...)) is not ...: - author = embeds.EmbedAuthor() + author = embed_models.EmbedAuthor() author.name = author_payload.get("name") author.url = author_payload.get("url") author.icon_url = author_payload.get("icon_url") @@ -555,7 +569,7 @@ def deserialize_embed(self, payload: data_binding.JSONObject) -> embeds.Embed: fields = [] for field_payload in payload.get("fields", ()): - field = embeds.EmbedField() + field = embed_models.EmbedField() field.name = field_payload["name"] field.value = field_payload["value"] field.is_inline = field_payload.get("inline", False) @@ -564,7 +578,7 @@ def deserialize_embed(self, payload: data_binding.JSONObject) -> embeds.Embed: return embed - def serialize_embed(self, embed: embeds.Embed) -> data_binding.JSONObject: + def serialize_embed(self, embed: embed_models.Embed) -> data_binding.JSONObject: payload = {} if embed.title is not None: @@ -640,25 +654,25 @@ def serialize_embed(self, embed: embeds.Embed) -> data_binding.JSONObject: return payload - ########## - # EMOJIS # - ########## + ################ + # EMOJI MODELS # + ################ - def deserialize_unicode_emoji(self, payload: data_binding.JSONObject) -> emojis.UnicodeEmoji: - unicode_emoji = emojis.UnicodeEmoji() + def deserialize_unicode_emoji(self, payload: data_binding.JSONObject) -> emoji_models.UnicodeEmoji: + unicode_emoji = emoji_models.UnicodeEmoji() unicode_emoji.name = payload["name"] return unicode_emoji - def deserialize_custom_emoji(self, payload: data_binding.JSONObject) -> emojis.CustomEmoji: - custom_emoji = emojis.CustomEmoji() + def deserialize_custom_emoji(self, payload: data_binding.JSONObject) -> emoji_models.CustomEmoji: + custom_emoji = emoji_models.CustomEmoji() custom_emoji.set_app(self._app) custom_emoji.id = snowflake.Snowflake(payload["id"]) custom_emoji.name = payload["name"] custom_emoji.is_animated = payload.get("animated", False) return custom_emoji - def deserialize_known_custom_emoji(self, payload: data_binding.JSONObject) -> emojis.KnownCustomEmoji: - known_custom_emoji = emojis.KnownCustomEmoji() + def deserialize_known_custom_emoji(self, payload: data_binding.JSONObject) -> emoji_models.KnownCustomEmoji: + known_custom_emoji = emoji_models.KnownCustomEmoji() known_custom_emoji.set_app(self._app) known_custom_emoji.id = snowflake.Snowflake(payload["id"]) known_custom_emoji.name = payload["name"] @@ -676,22 +690,22 @@ def deserialize_known_custom_emoji(self, payload: data_binding.JSONObject) -> em def deserialize_emoji( self, payload: data_binding.JSONObject - ) -> typing.Union[emojis.UnicodeEmoji, emojis.CustomEmoji]: + ) -> typing.Union[emoji_models.UnicodeEmoji, emoji_models.CustomEmoji]: if payload.get("id") is not None: return self.deserialize_custom_emoji(payload) return self.deserialize_unicode_emoji(payload) - ########### - # GATEWAY # - ########### + ################## + # GATEWAY MODELS # # TODO: rename to gateways? + ################## - def deserialize_gateway_bot(self, payload: data_binding.JSONObject) -> gateway.GatewayBot: - gateway_bot = gateway.GatewayBot() + def deserialize_gateway_bot(self, payload: data_binding.JSONObject) -> gateway_models.GatewayBot: + gateway_bot = gateway_models.GatewayBot() gateway_bot.url = payload["url"] gateway_bot.shard_count = int(payload["shards"]) session_start_limit_payload = payload["session_start_limit"] - session_start_limit = gateway.SessionStartLimit() + session_start_limit = gateway_models.SessionStartLimit() session_start_limit.total = int(session_start_limit_payload["total"]) session_start_limit.remaining = int(session_start_limit_payload["remaining"]) session_start_limit.reset_after = datetime.timedelta(milliseconds=session_start_limit_payload["reset_after"]) @@ -701,12 +715,12 @@ def deserialize_gateway_bot(self, payload: data_binding.JSONObject) -> gateway.G gateway_bot.session_start_limit = session_start_limit return gateway_bot - ########## - # GUILDS # - ########## + ################ + # GUILD MODELS # + ################ - def deserialize_guild_widget(self, payload: data_binding.JSONObject) -> guilds.GuildWidget: - guild_embed = guilds.GuildWidget() + def deserialize_guild_widget(self, payload: data_binding.JSONObject) -> guild_models.GuildWidget: + guild_embed = guild_models.GuildWidget() guild_embed.set_app(self._app) if (channel_id := payload["channel_id"]) is not None: @@ -720,33 +734,36 @@ def deserialize_member( self, payload: data_binding.JSONObject, *, - user: typing.Union[undefined.Undefined, users.User] = undefined.Undefined(), - ) -> guilds.Member: - guild_member = guilds.Member() + user: typing.Union[undefined.Undefined, user_models.User] = undefined.Undefined(), + ) -> guild_models.Member: + guild_member = guild_models.Member() guild_member.set_app(self._app) guild_member.user = user or self.deserialize_user(payload["user"]) - guild_member.nickname = payload.get("nick") guild_member.role_ids = {snowflake.Snowflake(role_id) for role_id in payload["roles"]} guild_member.joined_at = date.iso8601_datetime_string_to_datetime(payload["joined_at"]) - if (premium_since := payload.get("premium_since")) is not None: + guild_member.nickname = payload["nick"] if "nick" in payload else undefined.Undefined() + + if (premium_since := payload.get("premium_since", ...)) is not None and premium_since is not ...: premium_since = date.iso8601_datetime_string_to_datetime(premium_since) + elif premium_since is ...: + premium_since = undefined.Undefined() guild_member.premium_since = premium_since - guild_member.is_deaf = payload["deaf"] - guild_member.is_mute = payload["mute"] + guild_member.is_deaf = payload["deaf"] if "deaf" in payload else undefined.Undefined() + guild_member.is_mute = payload["mute"] if "mute" in payload else undefined.Undefined() return guild_member - def deserialize_role(self, payload: data_binding.JSONObject) -> guilds.Role: - guild_role = guilds.Role() + def deserialize_role(self, payload: data_binding.JSONObject) -> guild_models.Role: + guild_role = guild_models.Role() guild_role.set_app(self._app) guild_role.id = snowflake.Snowflake(payload["id"]) guild_role.name = payload["name"] - guild_role.color = colors.Color(payload["color"]) + guild_role.color = color_models.Color(payload["color"]) guild_role.is_hoisted = payload["hoist"] guild_role.position = int(payload["position"]) # noinspection PyArgumentList - guild_role.permissions = permissions.Permission(payload["permissions"]) + guild_role.permissions = permission_models.Permission(payload["permissions"]) guild_role.is_managed = payload["managed"] guild_role.is_mentionable = payload["mentionable"] return guild_role @@ -759,17 +776,17 @@ def _set_partial_integration_attributes( integration.name = payload["name"] integration.type = payload["type"] account_payload = payload["account"] - account = guilds.IntegrationAccount() + account = guild_models.IntegrationAccount() account.id = account_payload["id"] account.name = account_payload["name"] integration.account = account return integration - def deserialize_partial_integration(self, payload: data_binding.JSONObject) -> guilds.PartialIntegration: - return self._set_partial_integration_attributes(payload, guilds.PartialIntegration()) + def deserialize_partial_integration(self, payload: data_binding.JSONObject) -> guild_models.PartialIntegration: + return self._set_partial_integration_attributes(payload, guild_models.PartialIntegration()) - def deserialize_integration(self, payload: data_binding.JSONObject) -> guilds.Integration: - guild_integration = self._set_partial_integration_attributes(payload, guilds.Integration()) + def deserialize_integration(self, payload: data_binding.JSONObject) -> guild_models.Integration: + guild_integration = self._set_partial_integration_attributes(payload, guild_models.Integration()) guild_integration.is_enabled = payload["enabled"] guild_integration.is_syncing = payload["syncing"] @@ -779,7 +796,7 @@ def deserialize_integration(self, payload: data_binding.JSONObject) -> guilds.In guild_integration.is_emojis_enabled = payload.get("enable_emoticons") # noinspection PyArgumentList - guild_integration.expire_behavior = guilds.IntegrationExpireBehaviour(payload["expire_behavior"]) + guild_integration.expire_behavior = guild_models.IntegrationExpireBehaviour(payload["expire_behavior"]) guild_integration.expire_grace_period = datetime.timedelta(days=payload["expire_grace_period"]) guild_integration.user = self.deserialize_user(payload["user"]) @@ -789,14 +806,14 @@ def deserialize_integration(self, payload: data_binding.JSONObject) -> guilds.In return guild_integration - def deserialize_guild_member_ban(self, payload: data_binding.JSONObject) -> guilds.GuildMemberBan: - guild_member_ban = guilds.GuildMemberBan() + def deserialize_guild_member_ban(self, payload: data_binding.JSONObject) -> guild_models.GuildMemberBan: + guild_member_ban = guild_models.GuildMemberBan() guild_member_ban.reason = payload["reason"] guild_member_ban.user = self.deserialize_user(payload["user"]) return guild_member_ban - def deserialize_unavailable_guild(self, payload: data_binding.JSONObject) -> guilds.UnavailableGuild: - unavailable_guild = guilds.UnavailableGuild() + def deserialize_unavailable_guild(self, payload: data_binding.JSONObject) -> guild_models.UnavailableGuild: + unavailable_guild = guild_models.UnavailableGuild() unavailable_guild.set_app(self._app) unavailable_guild.id = snowflake.Snowflake(payload["id"]) return unavailable_guild @@ -811,15 +828,15 @@ def _set_partial_guild_attributes(self, payload: data_binding.JSONObject, guild: for feature in payload["features"]: try: # noinspection PyArgumentList - features.append(guilds.GuildFeature(feature)) + features.append(guild_models.GuildFeature(feature)) except ValueError: features.append(feature) guild.features = set(features) return guild - def deserialize_guild_preview(self, payload: data_binding.JSONObject) -> guilds.GuildPreview: - guild_preview = self._set_partial_guild_attributes(payload, guilds.GuildPreview()) + def deserialize_guild_preview(self, payload: data_binding.JSONObject) -> guild_models.GuildPreview: + guild_preview = self._set_partial_guild_attributes(payload, guild_models.GuildPreview()) guild_preview.splash_hash = payload["splash"] guild_preview.discovery_splash_hash = payload["discovery_splash"] guild_preview.emojis = { @@ -830,13 +847,15 @@ def deserialize_guild_preview(self, payload: data_binding.JSONObject) -> guilds. guild_preview.description = payload["description"] return guild_preview - def deserialize_guild(self, payload: data_binding.JSONObject) -> guilds.Guild: - guild = self._set_partial_guild_attributes(payload, guilds.Guild()) + def deserialize_guild(self, payload: data_binding.JSONObject) -> guild_models.Guild: + guild = self._set_partial_guild_attributes(payload, guild_models.Guild()) guild.splash_hash = payload["splash"] guild.discovery_splash_hash = payload["discovery_splash"] guild.owner_id = snowflake.Snowflake(payload["owner_id"]) # noinspection PyArgumentList - guild.my_permissions = permissions.Permission(payload["permissions"]) if "permissions" in payload else None + guild.my_permissions = ( + permission_models.Permission(payload["permissions"]) if "permissions" in payload else None + ) guild.region = payload["region"] if (afk_channel_id := payload["afk_channel_id"]) is not None: @@ -851,19 +870,19 @@ def deserialize_guild(self, payload: data_binding.JSONObject) -> guilds.Guild: guild.embed_channel_id = embed_channel_id # noinspection PyArgumentList - guild.verification_level = guilds.GuildVerificationLevel(payload["verification_level"]) + guild.verification_level = guild_models.GuildVerificationLevel(payload["verification_level"]) # noinspection PyArgumentList - guild.default_message_notifications = guilds.GuildMessageNotificationsLevel( + guild.default_message_notifications = guild_models.GuildMessageNotificationsLevel( payload["default_message_notifications"] ) # noinspection PyArgumentList - guild.explicit_content_filter = guilds.GuildExplicitContentFilterLevel(payload["explicit_content_filter"]) + guild.explicit_content_filter = guild_models.GuildExplicitContentFilterLevel(payload["explicit_content_filter"]) guild.roles = {snowflake.Snowflake(role["id"]): self.deserialize_role(role) for role in payload["roles"]} guild.emojis = { snowflake.Snowflake(emoji["id"]): self.deserialize_known_custom_emoji(emoji) for emoji in payload["emojis"] } # noinspection PyArgumentList - guild.mfa_level = guilds.GuildMFALevel(payload["mfa_level"]) + guild.mfa_level = guild_models.GuildMFALevel(payload["mfa_level"]) if (application_id := payload["application_id"]) is not None: application_id = snowflake.Snowflake(application_id) @@ -881,7 +900,7 @@ def deserialize_guild(self, payload: data_binding.JSONObject) -> guilds.Guild: guild.system_channel_id = system_channel_id # noinspection PyArgumentList - guild.system_channel_flags = guilds.GuildSystemChannelFlag(payload["system_channel_flags"]) + guild.system_channel_flags = guild_models.GuildSystemChannelFlag(payload["system_channel_flags"]) if (rules_channel_id := payload["rules_channel_id"]) is not None: rules_channel_id = snowflake.Snowflake(rules_channel_id) @@ -927,7 +946,7 @@ def deserialize_guild(self, payload: data_binding.JSONObject) -> guilds.Guild: guild.description = payload["description"] guild.banner_hash = payload["banner"] # noinspection PyArgumentList - guild.premium_tier = guilds.GuildPremiumTier(payload["premium_tier"]) + guild.premium_tier = guild_models.GuildPremiumTier(payload["premium_tier"]) if (premium_subscription_count := payload.get("premium_subscription_count")) is not None: premium_subscription_count = int(premium_subscription_count) @@ -947,12 +966,12 @@ def deserialize_guild(self, payload: data_binding.JSONObject) -> guilds.Guild: ) return guild - ########### - # INVITES # - ########### + ################# + # INVITE MODELS # + ################# - def deserialize_vanity_url(self, payload: data_binding.JSONObject) -> invites.VanityURL: - vanity_url = invites.VanityURL() + def deserialize_vanity_url(self, payload: data_binding.JSONObject) -> invite_model.VanityURL: + vanity_url = invite_model.VanityURL() vanity_url.set_app(self._app) vanity_url.code = payload["code"] vanity_url.uses = int(payload["uses"]) @@ -963,23 +982,34 @@ def _set_invite_attributes(self, payload: data_binding.JSONObject, invite: Invit invite.code = payload["code"] if (guild_payload := payload.get("guild", ...)) is not ...: - guild = self._set_partial_guild_attributes(guild_payload, invites.InviteGuild()) + guild = self._set_partial_guild_attributes(guild_payload, invite_model.InviteGuild()) guild.splash_hash = guild_payload["splash"] guild.banner_hash = guild_payload["banner"] guild.description = guild_payload["description"] # noinspection PyArgumentList - guild.verification_level = guilds.GuildVerificationLevel(guild_payload["verification_level"]) + guild.verification_level = guild_models.GuildVerificationLevel(guild_payload["verification_level"]) guild.vanity_url_code = guild_payload["vanity_url_code"] invite.guild = guild - else: + invite.guild_id = guild.id + elif (guild_id := payload.get("guild_id", ...)) is not ...: invite.guild = None + invite.guild_id = snowflake.Snowflake(guild_id) + else: + invite.guild = invite.guild_id = None + + if (channel := payload.get("channel")) is not None: + channel = self.deserialize_partial_channel(channel) + channel_id = channel.id + else: + channel_id = snowflake.Snowflake(payload["channel_id"]) + invite.channel = channel + invite.channel_id = channel_id - invite.channel = self.deserialize_partial_channel(payload["channel"]) invite.inviter = self.deserialize_user(payload["inviter"]) if "inviter" in payload else None invite.target_user = self.deserialize_user(payload["target_user"]) if "target_user" in payload else None # noinspection PyArgumentList invite.target_user_type = ( - invites.TargetUserType(payload["target_user_type"]) if "target_user_type" in payload else None + invite_model.TargetUserType(payload["target_user_type"]) if "target_user_type" in payload else None ) invite.approximate_presence_count = ( int(payload["approximate_presence_count"]) if "approximate_presence_count" in payload else None @@ -989,11 +1019,11 @@ def _set_invite_attributes(self, payload: data_binding.JSONObject, invite: Invit ) return invite - def deserialize_invite(self, payload: data_binding.JSONObject) -> invites.Invite: - return self._set_invite_attributes(payload, invites.Invite()) + def deserialize_invite(self, payload: data_binding.JSONObject) -> invite_model.Invite: + return self._set_invite_attributes(payload, invite_model.Invite()) - def deserialize_invite_with_metadata(self, payload: data_binding.JSONObject) -> invites.InviteWithMetadata: - invite_with_metadata = self._set_invite_attributes(payload, invites.InviteWithMetadata()) + def deserialize_invite_with_metadata(self, payload: data_binding.JSONObject) -> invite_model.InviteWithMetadata: + invite_with_metadata = self._set_invite_attributes(payload, invite_model.InviteWithMetadata()) invite_with_metadata.uses = int(payload["uses"]) invite_with_metadata.max_uses = int(payload["max_uses"]) max_age = payload["max_age"] @@ -1002,13 +1032,12 @@ def deserialize_invite_with_metadata(self, payload: data_binding.JSONObject) -> invite_with_metadata.created_at = date.iso8601_datetime_string_to_datetime(payload["created_at"]) return invite_with_metadata - ############ - # MESSAGES # - ############ + ################## + # MESSAGE MODELS # + ################## - # TODO: arbitrarily partial ver? - def deserialize_message(self, payload: data_binding.JSONObject) -> messages.Message: - message = messages.Message() + def deserialize_message(self, payload: data_binding.JSONObject) -> message_models.Message: + message = message_models.Message() message.set_app(self._app) message.id = snowflake.Snowflake(payload["id"]) message.channel_id = snowflake.Snowflake(payload["channel_id"]) @@ -1034,7 +1063,7 @@ def deserialize_message(self, payload: data_binding.JSONObject) -> messages.Mess attachments = [] for attachment_payload in payload["attachments"]: - attachment = messages.Attachment() + attachment = message_models.Attachment() attachment.id = snowflake.Snowflake(attachment_payload["id"]) attachment.filename = attachment_payload["filename"] attachment.size = int(attachment_payload["size"]) @@ -1049,7 +1078,7 @@ def deserialize_message(self, payload: data_binding.JSONObject) -> messages.Mess reactions = [] for reaction_payload in payload.get("reactions", ()): - reaction = messages.Reaction() + reaction = message_models.Reaction() reaction.count = int(reaction_payload["count"]) reaction.emoji = self.deserialize_emoji(reaction_payload["emoji"]) reaction.is_reacted_by_me = reaction_payload["me"] @@ -1059,12 +1088,12 @@ def deserialize_message(self, payload: data_binding.JSONObject) -> messages.Mess message.is_pinned = payload["pinned"] message.webhook_id = snowflake.Snowflake(payload["webhook_id"]) if "webhook_id" in payload else None # noinspection PyArgumentList - message.type = messages.MessageType(payload["type"]) + message.type = message_models.MessageType(payload["type"]) if (activity_payload := payload.get("activity", ...)) is not ...: - activity = messages.MessageActivity() + activity = message_models.MessageActivity() # noinspection PyArgumentList - activity.type = messages.MessageActivityType(activity_payload["type"]) + activity.type = message_models.MessageActivityType(activity_payload["type"]) activity.party_id = activity_payload.get("party_id") message.activity = activity else: @@ -1073,7 +1102,7 @@ def deserialize_message(self, payload: data_binding.JSONObject) -> messages.Mess message.application = self.deserialize_application(payload["application"]) if "application" in payload else None if (crosspost_payload := payload.get("message_reference", ...)) is not ...: - crosspost = messages.MessageCrosspost() + crosspost = message_models.MessageCrosspost() crosspost.set_app(self._app) crosspost.id = ( snowflake.Snowflake(crosspost_payload["message_id"]) if "message_id" in crosspost_payload else None @@ -1087,19 +1116,19 @@ def deserialize_message(self, payload: data_binding.JSONObject) -> messages.Mess message.message_reference = None # noinspection PyArgumentList - message.flags = messages.MessageFlag(payload["flags"]) if "flags" in payload else None + message.flags = message_models.MessageFlag(payload["flags"]) if "flags" in payload else None message.nonce = payload.get("nonce") return message - ############# - # PRESENCES # - ############# + ################### + # PRESENCE MODELS # + ################### - def deserialize_member_presence(self, payload: data_binding.JSONObject) -> presences_.MemberPresence: - guild_member_presence = presences_.MemberPresence() + def deserialize_member_presence(self, payload: data_binding.JSONObject) -> presence_models.MemberPresence: + guild_member_presence = presence_models.MemberPresence() guild_member_presence.set_app(self._app) user_payload = payload["user"] - user = presences_.PresenceUser() + user = presence_models.PresenceUser() user.set_app(self._app) user.id = snowflake.Snowflake(user_payload["id"]) user.discriminator = user_payload["discriminator"] if "discriminator" in user_payload else undefined.Undefined() @@ -1109,7 +1138,9 @@ def deserialize_member_presence(self, payload: data_binding.JSONObject) -> prese user.is_system = user_payload["system"] if "system" in user_payload else undefined.Undefined() # noinspection PyArgumentList user.flags = ( - users.UserFlag(user_payload["public_flags"]) if "public_flags" in user_payload else undefined.Undefined() + user_models.UserFlag(user_payload["public_flags"]) + if "public_flags" in user_payload + else undefined.Undefined() ) guild_member_presence.user = user @@ -1120,19 +1151,19 @@ def deserialize_member_presence(self, payload: data_binding.JSONObject) -> prese guild_member_presence.guild_id = snowflake.Snowflake(payload["guild_id"]) if "guild_id" in payload else None # noinspection PyArgumentList - guild_member_presence.visible_status = presences_.PresenceStatus(payload["status"]) + guild_member_presence.visible_status = presence_models.PresenceStatus(payload["status"]) activities = [] for activity_payload in payload["activities"]: - activity = presences_.RichActivity() + activity = presence_models.RichActivity() activity.name = activity_payload["name"] # noinspection PyArgumentList - activity.type = presences_.ActivityType(activity_payload["type"]) + activity.type = presence_models.ActivityType(activity_payload["type"]) activity.url = activity_payload.get("url") activity.created_at = date.unix_epoch_to_datetime(activity_payload["created_at"]) if (timestamps_payload := activity_payload.get("timestamps", ...)) is not ...: - timestamps = presences_.ActivityTimestamps() + timestamps = presence_models.ActivityTimestamps() timestamps.start = ( date.unix_epoch_to_datetime(timestamps_payload["start"]) if "start" in timestamps_payload else None ) @@ -1156,7 +1187,7 @@ def deserialize_member_presence(self, payload: data_binding.JSONObject) -> prese activity.emoji = emoji if (party_payload := activity_payload.get("party", ...)) is not ...: - party = presences_.ActivityParty() + party = presence_models.ActivityParty() party.id = party_payload.get("id") if (size := party_payload.get("size", ...)) is not ...: @@ -1170,7 +1201,7 @@ def deserialize_member_presence(self, payload: data_binding.JSONObject) -> prese activity.party = None if (assets_payload := activity_payload.get("assets", ...)) is not ...: - assets = presences_.ActivityAssets() + assets = presence_models.ActivityAssets() assets.large_image = assets_payload.get("large_image") assets.large_text = assets_payload.get("large_text") assets.small_image = assets_payload.get("small_image") @@ -1180,7 +1211,7 @@ def deserialize_member_presence(self, payload: data_binding.JSONObject) -> prese activity.assets = None if (secrets_payload := activity_payload.get("secrets", ...)) is not ...: - secret = presences_.ActivitySecret() + secret = presence_models.ActivitySecret() secret.join = secrets_payload.get("join") secret.spectate = secrets_payload.get("spectate") secret.match = secrets_payload.get("match") @@ -1190,29 +1221,31 @@ def deserialize_member_presence(self, payload: data_binding.JSONObject) -> prese activity.is_instance = activity_payload.get("instance") # TODO: can we safely default this to False? # noinspection PyArgumentList - activity.flags = presences_.ActivityFlag(activity_payload["flags"]) if "flags" in activity_payload else None + activity.flags = ( + presence_models.ActivityFlag(activity_payload["flags"]) if "flags" in activity_payload else None + ) activities.append(activity) guild_member_presence.activities = activities client_status_payload = payload["client_status"] - client_status = presences_.ClientStatus() + client_status = presence_models.ClientStatus() # noinspection PyArgumentList client_status.desktop = ( - presences_.PresenceStatus(client_status_payload["desktop"]) + presence_models.PresenceStatus(client_status_payload["desktop"]) if "desktop" in client_status_payload - else presences_.PresenceStatus.OFFLINE + else presence_models.PresenceStatus.OFFLINE ) # noinspection PyArgumentList client_status.mobile = ( - presences_.PresenceStatus(client_status_payload["mobile"]) + presence_models.PresenceStatus(client_status_payload["mobile"]) if "mobile" in client_status_payload - else presences_.PresenceStatus.OFFLINE + else presence_models.PresenceStatus.OFFLINE ) # noinspection PyArgumentList client_status.web = ( - presences_.PresenceStatus(client_status_payload["web"]) + presence_models.PresenceStatus(client_status_payload["web"]) if "web" in client_status_payload - else presences_.PresenceStatus.OFFLINE + else presence_models.PresenceStatus.OFFLINE ) guild_member_presence.client_status = client_status @@ -1225,9 +1258,9 @@ def deserialize_member_presence(self, payload: data_binding.JSONObject) -> prese guild_member_presence.nickname = payload.get("nick") return guild_member_presence - ######### - # USERS # - ######### + ############### + # USER MODELS # + ############### def _set_user_attributes(self, payload: data_binding.JSONObject, user: UserT) -> UserT: user.set_app(self._app) @@ -1239,30 +1272,32 @@ def _set_user_attributes(self, payload: data_binding.JSONObject, user: UserT) -> user.is_system = payload.get("system", False) return user - def deserialize_user(self, payload: data_binding.JSONObject) -> users.User: - user = self._set_user_attributes(payload, users.User()) + def deserialize_user(self, payload: data_binding.JSONObject) -> user_models.User: + user = self._set_user_attributes(payload, user_models.User()) # noinspection PyArgumentList - user.flags = users.UserFlag(payload["public_flags"]) if "public_flags" in payload else users.UserFlag.NONE + user.flags = ( + user_models.UserFlag(payload["public_flags"]) if "public_flags" in payload else user_models.UserFlag.NONE + ) return user - def deserialize_my_user(self, payload: data_binding.JSONObject) -> users.OwnUser: - my_user = self._set_user_attributes(payload, users.OwnUser()) + def deserialize_my_user(self, payload: data_binding.JSONObject) -> user_models.OwnUser: + my_user = self._set_user_attributes(payload, user_models.OwnUser()) my_user.is_mfa_enabled = payload["mfa_enabled"] my_user.locale = payload.get("locale") my_user.is_verified = payload.get("verified") my_user.email = payload.get("email") # noinspection PyArgumentList - my_user.flags = users.UserFlag(payload["flags"]) + my_user.flags = user_models.UserFlag(payload["flags"]) # noinspection PyArgumentList - my_user.premium_type = users.PremiumType(payload["premium_type"]) if "premium_type" in payload else None + my_user.premium_type = user_models.PremiumType(payload["premium_type"]) if "premium_type" in payload else None return my_user - ########## - # Voices # - ########## + ################ + # VOICE MODELS # + ################ - def deserialize_voice_state(self, payload: data_binding.JSONObject) -> voices.VoiceState: - voice_state = voices.VoiceState() + def deserialize_voice_state(self, payload: data_binding.JSONObject) -> voice_models.VoiceState: + voice_state = voice_models.VoiceState() voice_state.set_app(self._app) voice_state.guild_id = snowflake.Snowflake(payload["guild_id"]) if "guild_id" in payload else None @@ -1281,8 +1316,8 @@ def deserialize_voice_state(self, payload: data_binding.JSONObject) -> voices.Vo voice_state.is_suppressed = payload["suppress"] return voice_state - def deserialize_voice_region(self, payload: data_binding.JSONObject) -> voices.VoiceRegion: - voice_region = voices.VoiceRegion() + def deserialize_voice_region(self, payload: data_binding.JSONObject) -> voice_models.VoiceRegion: + voice_region = voice_models.VoiceRegion() voice_region.id = payload["id"] voice_region.name = payload["name"] voice_region.is_vip = payload["vip"] @@ -1291,15 +1326,15 @@ def deserialize_voice_region(self, payload: data_binding.JSONObject) -> voices.V voice_region.is_custom = payload["custom"] return voice_region - ############ - # WEBHOOKS # - ############ + ################## + # WEBHOOK MODELS # + ################## - def deserialize_webhook(self, payload: data_binding.JSONObject) -> webhooks.Webhook: - webhook = webhooks.Webhook() + def deserialize_webhook(self, payload: data_binding.JSONObject) -> webhook_models.Webhook: + webhook = webhook_models.Webhook() webhook.id = snowflake.Snowflake(payload["id"]) # noinspection PyArgumentList - webhook.type = webhooks.WebhookType(payload["type"]) + webhook.type = webhook_models.WebhookType(payload["type"]) webhook.guild_id = snowflake.Snowflake(payload["guild_id"]) if "guild_id" in payload else None webhook.channel_id = snowflake.Snowflake(payload["channel_id"]) webhook.author = self.deserialize_user(payload["user"]) if "user" in payload else None @@ -1307,3 +1342,852 @@ def deserialize_webhook(self, payload: data_binding.JSONObject) -> webhooks.Webh webhook.avatar_hash = payload["avatar"] webhook.token = payload.get("token") return webhook + + ################## + # CHANNEL EVENTS # # TODO: have event models that just contain one model relay attributes? + ################## + + def deserialize_channel_create_event(self, payload: data_binding.JSONObject) -> channel_events.ChannelCreateEvent: + """Parse a raw payload from Discord into a channel create event object. + + Parameters + ---------- + payload : Mapping[str, Any] + The dict payload to parse. + + Returns + ------- + hikari.events.channel.ChannelCreateEvent + The parsed channel create event object. + """ + channel_create_event = channel_events.ChannelCreateEvent() + channel_create_event.channel = self.deserialize_channel(payload) + return channel_create_event + + def deserialize_channel_update_event(self, payload: data_binding.JSONObject) -> channel_events.ChannelUpdateEvent: + """Parse a raw payload from Discord into a channel update event object. + + Parameters + ---------- + payload : Mapping[str, Any] + The dict payload to parse. + + Returns + ------- + hikari.events.channel.ChannelUpdateEvent + The parsed event object. + """ + channel_update_event = channel_events.ChannelUpdateEvent() + channel_update_event.channel = self.deserialize_channel(payload) + return channel_update_event + + def deserialize_channel_delete_event(self, payload: data_binding.JSONObject) -> channel_events.ChannelDeleteEvent: + """Parse a raw payload from Discord into a channel delete event object. + + Parameters + ---------- + payload : Mapping[str, Any] + The dict payload to parse. + + Returns + ------- + hikari.events.channel.ChannelDeleteEvent + The parsed channel delete event object. + """ + channel_delete_event = channel_events.ChannelDeleteEvent() + channel_delete_event.channel = self.deserialize_channel(payload) + return channel_delete_event + + def deserialize_channel_pins_update_event( + self, payload: data_binding.JSONObject + ) -> channel_events.ChannelPinsUpdateEvent: + """Parse a raw payload from Discord into a channel pins update event object. + + Parameters + ---------- + payload : Mapping[str, Any] + The dict payload to parse. + + Returns + ------- + hikari.events.channel.ChannelPinsUpdateEvent + The parsed channel pins update event object. + """ + channel_pins_update = channel_events.ChannelPinsUpdateEvent() + channel_pins_update.set_app(self._app) + channel_pins_update.guild_id = snowflake.Snowflake(payload["guild_id"]) if "guild_id" in payload else None + channel_pins_update.channel_id = snowflake.Snowflake(payload["channel_id"]) + + if (last_pin_timestamp := payload.get("last_pin_timestamp", ...)) is not ...: + channel_pins_update.last_pin_timestamp = date.iso8601_datetime_string_to_datetime(last_pin_timestamp) + else: + channel_pins_update.last_pin_timestamp = None + + return channel_pins_update + + def deserialize_webhook_update_event(self, payload: data_binding.JSONObject) -> channel_events.WebhookUpdateEvent: + """Parse a raw payload from Discord into a webhook update event object. + + Parameters + ---------- + payload : Mapping[str, Any] + The dict payload to parse. + + Returns + ------- + hikari.events.channel.WebhookUpdateEvent + The parsed webhook update event object. + """ + webhook_update = channel_events.WebhookUpdateEvent() + webhook_update.set_app(self._app) + webhook_update.guild_id = snowflake.Snowflake(payload["guild_id"]) + webhook_update.channel_id = snowflake.Snowflake(payload["channel_id"]) + return webhook_update + + def deserialize_typing_start_event(self, payload: data_binding.JSONObject) -> channel_events.TypingStartEvent: + """Parse a raw payload from Discord into a typing start event object. + + Parameters + ---------- + payload : Mapping[str, Any] + The dict payload to parse. + + Returns + ------- + hikari.events.channel.TypingStartEvent + The parsed typing start event object. + """ + typing_start = channel_events.TypingStartEvent() + typing_start.set_app(self._app) + typing_start.channel_id = snowflake.Snowflake(payload["channel_id"]) + typing_start.guild_id = snowflake.Snowflake(payload["guild_id"]) if "guild_id" in payload else None + typing_start.user_id = snowflake.Snowflake(payload["user_id"]) + # payload["timestamp"] varies from most unix epoches we see from discord as it is in seconds rather than + # milliseconds but it's easier to multiple it by 1k than to duplicate the logic in date.unix_epoch_to_datetime. + typing_start.timestamp = date.unix_epoch_to_datetime(payload["timestamp"] * 1000) + typing_start.member = self.deserialize_member(payload["member"]) if "member" in payload else None + return typing_start + + def deserialize_invite_create_event(self, payload: data_binding.JSONObject) -> channel_events.InviteCreateEvent: + """Parse a raw payload from Discord into an invite create event object. + + Parameters + ---------- + payload : Mapping[str, Any] + The dict payload to parse. + + Returns + ------- + hikari.events.channel.InviteCreateEvent + The parsed invite create event object. + """ + invite_create = channel_events.InviteCreateEvent() + invite_create.invite = self.deserialize_invite(payload) + return invite_create + + def deserialize_invite_delete_event(self, payload: data_binding.JSONObject) -> channel_events.InviteDeleteEvent: + """Parse a raw payload from Discord into an invite delete event object. + + Parameters + ---------- + payload : Mapping[str, Any] + The dict payload to parse. + + Returns + ------- + hikari.events.channel.InviteDeleteEvent + The parsed invite delete event object. + """ + invite_delete = channel_events.InviteDeleteEvent() + invite_delete.set_app(self._app) + invite_delete.code = payload["code"] + invite_delete.channel_id = snowflake.Snowflake(payload["channel_id"]) + invite_delete.guild_id = snowflake.Snowflake(payload["guild_id"]) if "guild_id" in payload else None + return invite_delete + + ################ + # GUILD EVENTS # + ################ + + def deserialize_guild_create_event(self, payload: data_binding.JSONObject) -> guild_events.GuildCreateEvent: + """Parse a raw payload from Discord into a guild create event object. + + Parameters + ---------- + payload : Mapping[str, Any] + The dict payload to parse. + + Returns + ------- + hikari.events.guild.GuildCreateEvent + The parsed guild create event object. + """ + guild_create = guild_events.GuildCreateEvent() + guild_create.guild = self.deserialize_guild(payload) + return guild_create + + def deserialize_guild_update_event(self, payload: data_binding.JSONObject) -> guild_events.GuildUpdateEvent: + """Parse a raw payload from Discord into a guild update event object. + + Parameters + ---------- + payload : Mapping[str, Any] + The dict payload to parse. + + Returns + ------- + hikari.events.guild.GuildUpdateEvent + The parsed guild update event object. + """ + guild_update = guild_events.GuildUpdateEvent() + guild_update.guild = self.deserialize_guild(payload) + return guild_update + + def deserialize_guild_leave_event(self, payload: data_binding.JSONObject) -> guild_events.GuildLeaveEvent: + """Parse a raw payload from Discord into a guild leave event object. + + Parameters + ---------- + payload : Mapping[str, Any] + The dict payload to parse. + + Returns + ------- + hikari.events.guild.GuildLeaveEvent + The parsed guild leave event object. + """ + guild_leave = guild_events.GuildLeaveEvent() + guild_leave.id = snowflake.Snowflake(payload["id"]) + return guild_leave + + def deserialize_guild_unavailable_event( + self, payload: data_binding.JSONObject + ) -> guild_events.GuildUnavailableEvent: + """Parse a raw payload from Discord into a guild unavailable event object. + + Parameters + ---------- + payload : Mapping[str, Any] + The dict payload to parse. + + Returns + ------- + hikari.events. + The parsed guild unavailable event object. + """ + guild_unavailable = guild_events.GuildUnavailableEvent() + guild_unavailable.set_app(self._app) + guild_unavailable.id = snowflake.Snowflake(payload["id"]) + return guild_unavailable + + def _set_base_guild_ban_event_fields( + self, payload: data_binding.JSONObject, guild_ban: GuildBanEventT + ) -> GuildBanEventT: + guild_ban.set_app(self._app) + guild_ban.guild_id = snowflake.Snowflake(payload["guild_id"]) + guild_ban.user = self.deserialize_user(payload["user"]) + return guild_ban + + def deserialize_guild_ban_add_event(self, payload: data_binding.JSONObject) -> guild_events.GuildBanAddEvent: + """Parse a raw payload from Discord into a guild ban add event object. + + Parameters + ---------- + payload : Mapping[str, Any] + The dict payload to parse. + + Returns + ------- + hikari.events.guild.GuildBanAddEvent + The parsed guild ban add event object. + """ + return self._set_base_guild_ban_event_fields(payload, guild_events.GuildBanAddEvent()) + + def deserialize_guild_ban_remove_event(self, payload: data_binding.JSONObject) -> guild_events.GuildBanRemoveEvent: + """Parse a raw payload from Discord into a guild ban remove event object. + + Parameters + ---------- + payload : Mapping[str, Any] + The dict payload to parse. + + Returns + ------- + hikari.events.guild.GuildBanRemoveEvent + The parsed guild ban remove event object. + """ + return self._set_base_guild_ban_event_fields(payload, guild_events.GuildBanRemoveEvent()) + + def deserialize_guild_emojis_update_event( + self, payload: data_binding.JSONObject + ) -> guild_events.GuildEmojisUpdateEvent: + """Parse a raw payload from Discord into a guild emojis update event object. + + Parameters + ---------- + payload : Mapping[str, Any] + The dict payload to parse. + + Returns + ------- + hikari.events.guild.GuildEmojisUpdateEvent + The parsed guild emojis update event object. + """ + guild_emojis_update = guild_events.GuildEmojisUpdateEvent() + guild_emojis_update.set_app(self._app) + guild_emojis_update.guild_id = snowflake.Snowflake(payload["guild_id"]) + guild_emojis_update.emojis = { + snowflake.Snowflake(emoji["id"]): self.deserialize_known_custom_emoji(emoji) for emoji in payload["emojis"] + } + return guild_emojis_update + + def deserialize_guild_integrations_update_event( + self, payload: data_binding.JSONObject + ) -> guild_events.GuildIntegrationsUpdateEvent: + """Parse a raw payload from Discord into a guilds integrations update event object. + + Parameters + ---------- + payload : Mapping[str, Any] + The dict payload to parse. + + Returns + ------- + hikari.events.guild.GuildIntegrationsUpdateEvent + The parsed guilds integrations update event object. + """ + guild_integrations_update = guild_events.GuildIntegrationsUpdateEvent() + guild_integrations_update.set_app(self._app) + guild_integrations_update.guild_id = snowflake.Snowflake(payload["guild_id"]) + return guild_integrations_update + + def deserialize_guild_member_add_event(self, payload: data_binding.JSONObject) -> guild_events.GuildMemberAddEvent: + """Parse a raw payload from Discord into a guild member add event object. + + Parameters + ---------- + payload : Mapping[str, Any] + The dict payload to parse. + + Returns + ------- + hikari.events.guild.GuildMemberAddEvent + The parsed guild member add event object. + """ + guild_member_add = guild_events.GuildMemberAddEvent() + guild_member_add.set_app(self._app) + guild_member_add.guild_id = snowflake.Snowflake(payload["guild_id"]) + guild_member_add.member = self.deserialize_member(payload) + return guild_member_add + + def deserialize_guild_member_update_event( + self, payload: data_binding.JSONObject + ) -> guild_events.GuildMemberUpdateEvent: + """Parse a raw payload from Discord into a guild member update event object. + + Parameters + ---------- + payload : Mapping[str, Any] + The dict payload to parse. + + Returns + ------- + hikari.events.guild.GuildMemberUpdateEvent + The parsed guild member update event object. + """ + guild_member_update = guild_events.GuildMemberUpdateEvent() + guild_member_update.member = self.deserialize_member(payload) + return guild_member_update + + def deserialize_guild_member_remove_event( + self, payload: data_binding.JSONObject + ) -> guild_events.GuildMemberRemoveEvent: + """Parse a raw payload from Discord into a guild member remove event object. + + Parameters + ---------- + payload : Mapping[str, Any] + The dict payload to parse. + + Returns + ------- + hikari.events.guild.GuildMemberRemoveEvent + The parsed guild member remove event object. + """ + guild_member_remove = guild_events.GuildMemberRemoveEvent() + guild_member_remove.set_app(self._app) + guild_member_remove.guild_id = snowflake.Snowflake(payload["guild_id"]) + guild_member_remove.user = self.deserialize_user(payload["user"]) + return guild_member_remove + + def deserialize_guild_role_create_event( + self, payload: data_binding.JSONObject + ) -> guild_events.GuildRoleCreateEvent: + """Parse a raw payload from Discord into a guild role create event object. + + Parameters + ---------- + payload : Mapping[str, Any] + The dict payload to parse. + + Returns + ------- + hikari.events.guild.GuildRoleCreateEvent + The parsed guild role create event object. + """ + guild_role_create = guild_events.GuildRoleCreateEvent() + guild_role_create.set_app(self._app) + guild_role_create.guild_id = snowflake.Snowflake(payload["guild_id"]) + guild_role_create.role = self.deserialize_role(payload["role"]) + return guild_role_create + + def deserialize_guild_role_update_event( + self, payload: data_binding.JSONObject + ) -> guild_events.GuildRoleUpdateEvent: + """Parse a raw payload from Discord into a guild role update event object. + + Parameters + ---------- + payload : Mapping[str, Any] + The dict payload to parse. + + Returns + ------- + hikari.events.guild.GuildRoleUpdateEvent + The parsed guild role update event object. + """ + guild_role_update = guild_events.GuildRoleUpdateEvent() + guild_role_update.set_app(self._app) + guild_role_update.guild_id = snowflake.Snowflake(payload["guild_id"]) + guild_role_update.role = self.deserialize_role(payload["role"]) + return guild_role_update + + def deserialize_guild_role_delete_event( + self, payload: data_binding.JSONObject + ) -> guild_events.GuildRoleDeleteEvent: + """Parse a raw payload from Discord into a guild role delete event object. + + Parameters + ---------- + payload : Mapping[str, Any] + The dict payload to parse. + + Returns + ------- + hikari.events.guild.GuildRoleDeleteEvent + The parsed guild role delete event object. + """ + guild_role_delete = guild_events.GuildRoleDeleteEvent() + guild_role_delete.set_app(self._app) + guild_role_delete.guild_id = snowflake.Snowflake(payload["guild_id"]) + guild_role_delete.role_id = snowflake.Snowflake(payload["role_id"]) + return guild_role_delete + + def deserialize_presence_update_event(self, payload: data_binding.JSONObject) -> guild_events.PresenceUpdateEvent: + """Parse a raw payload from Discord into a presence update event object. + + Parameters + ---------- + payload : Mapping[str, Any] + The dict payload to parse. + + Returns + ------- + hikari.events.guild.PresenceUpdateEvent + The parsed presence update event object. + """ + presence_update = guild_events.PresenceUpdateEvent() + presence_update.presence = self.deserialize_member_presence(payload) + return presence_update + + ################## + # MESSAGE EVENTS # + ################## + + def deserialize_message_create_event(self, payload: data_binding.JSONObject) -> message_events.MessageCreateEvent: + """Parse a raw payload from Discord into a message create event object. + + Parameters + ---------- + payload : Mapping[str, Any] + The dict payload to parse. + + Returns + ------- + hikari.events.message.MessageCreateEvent + The parsed message create event object. + """ + message_create = message_events.MessageCreateEvent() + message_create.message = self.deserialize_message(payload) + return message_create + + def deserialize_message_update_event(self, payload: data_binding.JSONObject) -> message_events.MessageUpdateEvent: + """Parse a raw payload from Discord into a message update event object. + + Parameters + ---------- + payload : Mapping[str, Any] + The dict payload to parse. + + Returns + ------- + hikari.events.message.MessageUpdateEvent + The parsed message update event object. + """ + message_update = message_events.MessageUpdateEvent() + + updated_message = message_events.UpdateMessage() + updated_message.set_app(self._app) + updated_message.id = snowflake.Snowflake(payload["id"]) + updated_message.channel_id = ( + snowflake.Snowflake(payload["channel_id"]) if "channel_id" in payload else undefined.Undefined() + ) + updated_message.guild_id = ( + snowflake.Snowflake(payload["guild_id"]) if "guild_id" in payload else undefined.Undefined() + ) + updated_message.author = ( + self.deserialize_user(payload["author"]) if "author" in payload else undefined.Undefined() + ) + # TODO: will we ever be given "member" but not "author"? + updated_message.member = ( + self.deserialize_member(payload["member"], user=updated_message.author) + if "member" in payload + else undefined.Undefined() + ) + updated_message.content = payload["content"] if "content" in payload else undefined.Undefined() + updated_message.timestamp = ( + date.iso8601_datetime_string_to_datetime(payload["timestamp"]) + if "timestamp" in payload + else undefined.Undefined() + ) + + if (edited_timestamp := payload.get("edited_timestamp", ...)) is not ... and edited_timestamp is not None: + edited_timestamp = date.iso8601_datetime_string_to_datetime(edited_timestamp) + elif edited_timestamp is ...: + edited_timestamp = undefined.Undefined() + updated_message.edited_timestamp = edited_timestamp + + updated_message.is_tts = payload["tts"] if "tts" in payload else undefined.Undefined() + updated_message.is_mentioning_everyone = ( + payload["mention_everyone"] if "mention_everyone" in payload else undefined.Undefined() + ) + updated_message.user_mentions = ( + {snowflake.Snowflake(mention["id"]) for mention in payload["mentions"]} + if "mentions" in payload + else undefined.Undefined() + ) + updated_message.role_mentions = ( + {snowflake.Snowflake(mention) for mention in payload["mention_roles"]} + if "mention_roles" in payload + else undefined.Undefined() + ) + updated_message.channel_mentions = ( + {snowflake.Snowflake(mention["id"]) for mention in payload["mention_channels"]} + if "mention_channels" in payload + else undefined.Undefined() + ) + + if "attachments" in payload: + attachments = [] + for attachment_payload in payload["attachments"]: + attachment = message_models.Attachment() + attachment.id = snowflake.Snowflake(attachment_payload["id"]) + attachment.filename = attachment_payload["filename"] + attachment.size = int(attachment_payload["size"]) + attachment.url = attachment_payload["url"] + attachment.proxy_url = attachment_payload["proxy_url"] + attachment.height = attachment_payload.get("height") + attachment.width = attachment_payload.get("width") + attachments.append(attachment) + updated_message.attachments = attachments + else: + updated_message.attachments = undefined.Undefined() + + updated_message.embeds = ( + [self.deserialize_embed(embed) for embed in payload["embeds"]] + if "embeds" in payload + else undefined.Undefined() + ) + + if "reactions" in payload: + reactions = [] + for reaction_payload in payload["reactions"]: + reaction = message_models.Reaction() + reaction.count = int(reaction_payload["count"]) + reaction.emoji = self.deserialize_emoji(reaction_payload["emoji"]) + reaction.is_reacted_by_me = reaction_payload["me"] + reactions.append(reaction) + updated_message.reactions = reactions + else: + updated_message.reactions = undefined.Undefined() + + updated_message.is_pinned = payload["pinned"] if "pinned" in payload else undefined.Undefined() + updated_message.webhook_id = ( + snowflake.Snowflake(payload["webhook_id"]) if "webhook_id" in payload else undefined.Undefined() + ) + # noinspection PyArgumentList + updated_message.type = ( + message_models.MessageType(payload["type"]) if "type" in payload else undefined.Undefined() + ) + + if (activity_payload := payload.get("activity", ...)) is not ...: + activity = message_models.MessageActivity() + # noinspection PyArgumentList + activity.type = message_models.MessageActivityType(activity_payload["type"]) + activity.party_id = activity_payload.get("party_id") + updated_message.activity = activity + else: + updated_message.activity = undefined.Undefined() + + updated_message.application = ( + self.deserialize_application(payload["application"]) if "application" in payload else undefined.Undefined() + ) + + if (crosspost_payload := payload.get("message_reference", ...)) is not ...: + crosspost = message_models.MessageCrosspost() + crosspost.set_app(self._app) + crosspost.id = ( + snowflake.Snowflake(crosspost_payload["message_id"]) if "message_id" in crosspost_payload else None + ) + crosspost.channel_id = snowflake.Snowflake(crosspost_payload["channel_id"]) + crosspost.guild_id = ( + snowflake.Snowflake(crosspost_payload["guild_id"]) if "guild_id" in crosspost_payload else None + ) + updated_message.message_reference = crosspost + else: + updated_message.message_reference = undefined.Undefined() + + # noinspection PyArgumentList + updated_message.flags = ( + message_models.MessageFlag(payload["flags"]) if "flags" in payload else undefined.Undefined() + ) + updated_message.nonce = payload["nonce"] if "nonce" in payload else undefined.Undefined() + + message_update.message = updated_message + return message_update + + def deserialize_message_delete_event(self, payload: data_binding.JSONObject) -> message_events.MessageDeleteEvent: + """Parse a raw payload from Discord into a message delete event object. + + Parameters + ---------- + payload : Mapping[str, Any] + The dict payload to parse. + + Returns + ------- + hikari.events.message.MessageDeleteEvent + The parsed message delete event object. + """ + message_delete = message_events.MessageDeleteEvent() + message_delete.set_app(self._app) + message_delete.channel_id = snowflake.Snowflake(payload["channel_id"]) + message_delete.guild_id = snowflake.Snowflake(payload["guild_id"]) if "guild_id" in payload else None + message_delete.message_id = snowflake.Snowflake(payload["id"]) + return message_delete + + def deserialize_message_delete_bulk_event( + self, payload: data_binding.JSONObject + ) -> message_events.MessageDeleteBulkEvent: + """Parse a raw payload from Discord into a message delete bulk event object. + + Parameters + ---------- + payload : Mapping[str, Any] + The dict payload to parse. + + Returns + ------- + hikari.events.message.MessageDeleteBulkEvent + The parsed message delete bulk event object. + """ + message_delete_bulk = message_events.MessageDeleteBulkEvent() + message_delete_bulk.set_app(self._app) + message_delete_bulk.channel_id = snowflake.Snowflake(payload["channel_id"]) + message_delete_bulk.guild_id = snowflake.Snowflake(payload["guild_id"]) if "guild_id" in payload else None + message_delete_bulk.message_ids = {snowflake.Snowflake(message_id) for message_id in payload["ids"]} + return message_delete_bulk + + def _set_base_message_reaction_fields( + self, payload: data_binding.JSONObject, reaction_event: ReactionEventT + ) -> ReactionEventT: + reaction_event.set_app(self._app) + reaction_event.channel_id = snowflake.Snowflake(payload["channel_id"]) + reaction_event.message_id = snowflake.Snowflake(payload["message_id"]) + reaction_event.guild_id = snowflake.Snowflake(payload["guild_id"]) if "guild_id" in payload else None + return reaction_event + + def deserialize_message_reaction_add_event( + self, payload: data_binding.JSONObject + ) -> message_events.MessageReactionAddEvent: + """Parse a raw payload from Discord into a message reaction add event object. + + Parameters + ---------- + payload : Mapping[str, Any] + The dict payload to parse. + + Returns + ------- + hikari.events.message.MessageReactionAddEvent + The parsed message reaction add event object. + """ + message_reaction_add = self._set_base_message_reaction_fields(payload, message_events.MessageReactionAddEvent()) + message_reaction_add.user_id = snowflake.Snowflake(payload["user_id"]) + message_reaction_add.member = self.deserialize_member(payload["member"]) if "member" in payload else None + message_reaction_add.emoji = self.deserialize_emoji(payload["emoji"]) + return message_reaction_add + + def deserialize_message_reaction_remove_event( + self, payload: data_binding.JSONObject + ) -> message_events.MessageReactionRemoveEvent: + """Parse a raw payload from Discord into a message reaction remove event object. + + Parameters + ---------- + payload : Mapping[str, Any] + The dict payload to parse. + + Returns + ------- + hikari.events.message.MessageReactionRemoveEvent + The parsed message reaction remove event object. + """ + message_reaction_remove = self._set_base_message_reaction_fields( + payload, message_events.MessageReactionRemoveEvent() + ) + message_reaction_remove.user_id = snowflake.Snowflake(payload["user_id"]) + message_reaction_remove.emoji = self.deserialize_emoji(payload["emoji"]) + return message_reaction_remove + + def deserialize_message_reaction_remove_all_event( + self, payload: data_binding.JSONObject + ) -> message_events.MessageReactionRemoveAllEvent: + """Parse a raw payload from Discord into a message reaction remove all event object. + + Parameters + ---------- + payload : Mapping[str, Any] + The dict payload to parse. + + Returns + ------- + hikari.events.message.MessageReactionRemoveAllEvent + The parsed message reaction remove all event object. + """ + return self._set_base_message_reaction_fields(payload, message_events.MessageReactionRemoveAllEvent()) + + def deserialize_message_reaction_remove_emoji_event( + self, payload: data_binding.JSONObject + ) -> message_events.MessageReactionRemoveEmojiEvent: + """Parse a raw payload from Discord into a message reaction remove emoji event object. + + Parameters + ---------- + payload : Mapping[str, Any] + The dict payload to parse. + + Returns + ------- + hikari.events.message.MessageReactionRemoveEmojiEvent + The parsed message reaction remove emoji event object. + """ + message_reaction_remove_emoji = self._set_base_message_reaction_fields( + payload, message_events.MessageReactionRemoveEmojiEvent() + ) + message_reaction_remove_emoji.emoji = self.deserialize_emoji(payload["emoji"]) + return message_reaction_remove_emoji + + ################ + # OTHER EVENTS # + ################ + + def deserialize_ready_event(self, payload: data_binding.JSONObject) -> other_events.ReadyEvent: + """Parse a raw payload from Discord into a ready event object. + + Parameters + ---------- + payload : Mapping[str, Any] + The dict payload to parse. + + Returns + ------- + hikari.events.other.ReadyEvent + The parsed ready event object. + """ + ready_event = other_events.ReadyEvent() + ready_event.gateway_version = int(payload["v"]) + ready_event.my_user = self.deserialize_my_user(payload["user"]) + ready_event.unavailable_guilds = { + snowflake.Snowflake(guild["id"]): self.deserialize_unavailable_guild(guild) for guild in payload["guilds"] + } + ready_event.session_id = payload["session_id"] + + if (shard := payload.get("shard", ...)) is not ...: + ready_event.shard_id = int(shard[0]) + ready_event.shard_count = int(shard[1]) + else: + ready_event.shard_id = ready_event.shard_count = None + + return ready_event + + def deserialize_my_user_update_event(self, payload: data_binding.JSONObject) -> other_events.MyUserUpdateEvent: + """Parse a raw payload from Discord into a my user update event object. + + Parameters + ---------- + payload : Mapping[str, Any] + The dict payload to parse. + + Returns + ------- + hikari.events.other.MyUserUpdateEvent + The parsed my user update event object. + """ + my_user_update = other_events.MyUserUpdateEvent() + my_user_update.my_user = self.deserialize_my_user(payload) + return my_user_update + + ################ + # VOICE EVENTS # + ################ + + def deserialize_voice_state_update_event( + self, payload: data_binding.JSONObject + ) -> voice_events.VoiceStateUpdateEvent: + """Parse a raw payload from Discord into a voice state update event object. + + Parameters + ---------- + payload : Mapping[str, Any] + The dict payload to parse. + + Returns + ------- + hikari.events.voice.VoiceStateUpdateEvent + The parsed voice state update event object. + """ + voice_state_update = voice_events.VoiceStateUpdateEvent() + voice_state_update.state = self.deserialize_voice_state(payload) + return voice_state_update + + def deserialize_voice_server_update_event( + self, payload: data_binding.JSONObject + ) -> voice_events.VoiceServerUpdateEvent: + """Parse a raw payload from Discord into a voice server update event object. + + Parameters + ---------- + payload : Mapping[str, Any] + The dict payload to parse. + + Returns + ------- + hikari.events.voice.VoiceServerUpdateEvent + The parsed voice server update event object. + """ + voice_server_update = voice_events.VoiceServerUpdateEvent() + voice_server_update.token = payload["token"] + voice_server_update.guild_id = snowflake.Snowflake(payload["guild_id"]) + voice_server_update.endpoint = payload["endpoint"] + return voice_server_update diff --git a/hikari/models/channels.py b/hikari/models/channels.py index fd125b82dc..df7ba8afac 100644 --- a/hikari/models/channels.py +++ b/hikari/models/channels.py @@ -157,6 +157,9 @@ class GroupDMChannel(DMChannel): icon_hash: typing.Optional[str] = attr.ib(eq=False, hash=False) """The hash of the icon of the group.""" + nicknames: typing.MutableMapping[snowflake.Snowflake, str] = attr.ib(eq=False, hash=False) + """A mapping of set nicknames within this group DMs to user IDs.""" + application_id: typing.Optional[snowflake.Snowflake] = attr.ib(eq=False, hash=False) """The ID of the application that created the group DM, if it's a bot based group DM.""" diff --git a/hikari/models/guilds.py b/hikari/models/guilds.py index adb188a04b..a797d50d69 100644 --- a/hikari/models/guilds.py +++ b/hikari/models/guilds.py @@ -61,6 +61,7 @@ from hikari.models import permissions as permissions_ from hikari.models import presences from hikari.utilities import snowflake + from hikari.utilities import undefined @enum.unique @@ -226,10 +227,14 @@ class Member(bases.Entity): This will be `None` when attached to Message Create and Update gateway events. """ - nickname: typing.Optional[str] = attr.ib( + nickname: typing.Union[str, None, undefined.Undefined] = attr.ib( eq=False, hash=False, repr=True, ) - """This member's nickname, if set.""" + """This member's nickname. + + This will be `None` if not set and `hikari.utilities.undefined.Undefined` + if it's state is unknown. + """ role_ids: typing.Set[snowflake.Snowflake] = attr.ib( eq=False, hash=False, @@ -239,17 +244,24 @@ class Member(bases.Entity): joined_at: datetime.datetime = attr.ib(eq=False, hash=False) """The datetime of when this member joined the guild they belong to.""" - premium_since: typing.Optional[datetime.datetime] = attr.ib(eq=False, hash=False) + premium_since: typing.Union[datetime.datetime, None, undefined.Undefined] = attr.ib(eq=False, hash=False) """The datetime of when this member started "boosting" this guild. - This will be `None` if they aren't boosting. + This will be `None` if they aren't boosting and + `hikari.utilities.undefined.Undefined` if their boosting status is unknown. + """ + + is_deaf: typing.Union[bool, undefined.Undefined] = attr.ib(eq=False, hash=False) + """Whether this member is deafened by this guild in it's voice channels. + + This will be `hikari.utilities.undefined.Undefined if it's state is unknown. """ - is_deaf: bool = attr.ib(eq=False, hash=False) - """Whether this member is deafened by this guild in it's voice channels.""" + is_mute: typing.Union[bool, undefined.Undefined] = attr.ib(eq=False, hash=False) + """Whether this member is muted by this guild in it's voice channels. - is_mute: bool = attr.ib(eq=False, hash=False) - """Whether this member is muted by this guild in it's voice channels.""" + This will be `hikari.utilities.undefined.Undefined if it's state is unknown. + """ @attr.s(eq=True, hash=True, init=False, kw_only=True, slots=True) diff --git a/hikari/models/invites.py b/hikari/models/invites.py index 0e7b46202f..90125de7bd 100644 --- a/hikari/models/invites.py +++ b/hikari/models/invites.py @@ -36,6 +36,7 @@ from hikari.models import channels from hikari.models import users + from hikari.utilities import snowflake @enum.unique @@ -158,46 +159,46 @@ class Invite(bases.Entity): code: str = attr.ib(eq=True, hash=True, repr=True) """The code for this invite.""" - guild: typing.Optional[InviteGuild] = attr.ib( - eq=False, hash=False, repr=True, - ) - """The partial object of the guild this dm belongs to. + guild: typing.Optional[InviteGuild] = attr.ib(eq=False, hash=False, repr=False) + """The partial object of the guild this invite belongs to. - Will be `None` for group dm invites. + Will be `None` for group DM invites and when attached to a gateway event; + for invites received over the gateway you should refer to `Invite.guild_id`. """ - channel: channels.PartialChannel = attr.ib( - eq=False, hash=False, repr=True, - ) - """The partial object of the channel this invite targets.""" + guild_id: typing.Optional[snowflake.Snowflake] = attr.ib(eq=False, hash=False, repr=True) + """The ID of the guild this invite belongs to. - inviter: typing.Optional[users.User] = attr.ib( - eq=False, hash=False, - ) + Will be `None` for group DM invites. + """ + + channel: typing.Optional[channels.PartialChannel] = attr.ib(eq=False, hash=False, repr=False) + """The partial object of the channel this invite targets. + + Will be `None` for invite objects that are attached to gateway events, + in which case you should refer to `Invite.channel_id`. + """ + + channel_id: snowflake.Snowflake = attr.ib(eq=False, hash=False, repr=True) + """The ID of the channel this invite targets.""" + + inviter: typing.Optional[users.User] = attr.ib(eq=False, hash=False) """The object of the user who created this invite.""" - target_user: typing.Optional[users.User] = attr.ib( - eq=False, hash=False, - ) + target_user: typing.Optional[users.User] = attr.ib(eq=False, hash=False) """The object of the user who this invite targets, if set.""" - target_user_type: typing.Optional[TargetUserType] = attr.ib( - eq=False, hash=False, - ) + target_user_type: typing.Optional[TargetUserType] = attr.ib(eq=False, hash=False) """The type of user target this invite is, if applicable.""" - approximate_presence_count: typing.Optional[int] = attr.ib( - eq=False, hash=False, - ) + approximate_presence_count: typing.Optional[int] = attr.ib(eq=False, hash=False) """The approximate amount of presences in this invite's guild. This is only present when `with_counts` is passed as `True` to the GET Invites endpoint. """ - approximate_member_count: typing.Optional[int] = attr.ib( - eq=False, hash=False, - ) + approximate_member_count: typing.Optional[int] = attr.ib(eq=False, hash=False) """The approximate amount of members in this invite's guild. This is only present when `with_counts` is passed as `True` to the GET diff --git a/tests/hikari/impl/test_entity_factory.py b/tests/hikari/impl/test_entity_factory.py index a76754f1af..dfa6770ac9 100644 --- a/tests/hikari/impl/test_entity_factory.py +++ b/tests/hikari/impl/test_entity_factory.py @@ -23,21 +23,26 @@ from hikari.api import app as app_ from hikari.impl import entity_factory -from hikari.models import applications -from hikari.models import audit_logs -from hikari.models import channels -from hikari.models import colors -from hikari.models import embeds -from hikari.models import emojis -from hikari.models import gateway -from hikari.models import guilds -from hikari.models import invites -from hikari.models import messages -from hikari.models import permissions -from hikari.models import presences -from hikari.models import webhooks -from hikari.models import users -from hikari.models import voices +from hikari.events import channel as channel_events +from hikari.events import guild as guild_events +from hikari.events import message as message_events +from hikari.events import other as other_events +from hikari.events import voice as voice_events +from hikari.models import applications as application_models +from hikari.models import audit_logs as audit_log_models +from hikari.models import channels as channel_models +from hikari.models import colors as color_models +from hikari.models import embeds as embed_models +from hikari.models import emojis as emoji_models +from hikari.models import gateway as gateway_models +from hikari.models import guilds as guild_models +from hikari.models import invites as invite_models +from hikari.models import messages as message_models +from hikari.models import permissions as permission_models +from hikari.models import presences as presence_models +from hikari.models import webhooks as webhook_models +from hikari.models import users as user_models +from hikari.models import voices as voice_models from hikari.utilities import undefined @@ -77,9 +82,9 @@ def entity_factory_impl(self, mock_app) -> entity_factory.EntityFactoryImpl: def test_app(self, entity_factory_impl, mock_app): assert entity_factory_impl.app is mock_app - ################ - # APPLICATIONS # - ################ + ###################### + # APPLICATION MODELS # + ###################### @pytest.fixture() def partial_integration(self): @@ -116,8 +121,8 @@ def test_deserialize_own_connection( assert own_connection.is_verified is True assert own_connection.is_friend_sync_enabled is False assert own_connection.is_activity_visible is True - assert own_connection.visibility == applications.ConnectionVisibility.NONE - assert isinstance(own_connection, applications.OwnConnection) + assert own_connection.visibility == application_models.ConnectionVisibility.NONE + assert isinstance(own_connection, application_models.OwnConnection) @pytest.fixture() def own_guild_payload(self): @@ -135,9 +140,9 @@ def test_deserialize_own_guild(self, entity_factory_impl, mock_app, own_guild_pa assert own_guild.id == 152559372126519269 assert own_guild.name == "Isopropyl" assert own_guild.icon_hash == "d4a983885dsaa7691ce8bcaaf945a" - assert own_guild.features == {guilds.GuildFeature.DISCOVERABLE, "FORCE_RELAY"} + assert own_guild.features == {guild_models.GuildFeature.DISCOVERABLE, "FORCE_RELAY"} assert own_guild.is_owner is False - assert own_guild.my_permissions == permissions.Permission(2147483647) + assert own_guild.my_permissions == permission_models.Permission(2147483647) def test_deserialize_own_guild_with_null_and_unset_fields(self, entity_factory_impl): own_guild = entity_factory_impl.deserialize_own_guild( @@ -152,10 +157,6 @@ def test_deserialize_own_guild_with_null_and_unset_fields(self, entity_factory_i ) assert own_guild.icon_hash is None - @pytest.fixture() - def user_payload(self): - return {"username": "agent 47", "avatar": "hashed", "discriminator": "4747", "id": "474747474"} - @pytest.fixture() def owner_payload(self, user_payload): return {**user_payload, "flags": 1 << 10} @@ -206,21 +207,21 @@ def test_deserialize_application( assert application.team.id == 202020202 assert application.team.icon_hash == "hashtag" assert application.team.owner_user_id == 393030292 - assert isinstance(application.team, applications.Team) + assert isinstance(application.team, application_models.Team) # TeamMember assert len(application.team.members) == 1 member = application.team.members[115590097100865541] - assert member.membership_state == applications.TeamMembershipState.INVITED + assert member.membership_state == application_models.TeamMembershipState.INVITED assert member.permissions == {"*"} assert member.team_id == 209333111222 assert member.user == entity_factory_impl.deserialize_user(user_payload) - assert isinstance(member, applications.TeamMember) + assert isinstance(member, application_models.TeamMember) assert application.guild_id == 2020293939 assert application.primary_sku_id == 2020202002 assert application.slug == "192.168.1.254" assert application.cover_image_hash == "hashmebaby" - assert isinstance(application, applications.Application) + assert isinstance(application, application_models.Application) def test_deserialize_application_with_unset_fields(self, entity_factory_impl, mock_app, owner_payload): application = entity_factory_impl.deserialize_application( @@ -257,9 +258,9 @@ def test_deserialize_application_with_null_fields(self, entity_factory_impl, moc assert application.icon_hash is None assert application.team is None - ############## - # AUDIT_LOGS # - ############## + ##################### + # AUDIT LOGS MODELS # + ##################### def test__deserialize_audit_log_change_roles(self, entity_factory_impl): test_role_payloads = [ @@ -270,7 +271,7 @@ def test__deserialize_audit_log_change_roles(self, entity_factory_impl): role = roles[24] assert role.id == 24 assert role.name == "roleA" - assert isinstance(role, guilds.PartialRole) + assert isinstance(role, guild_models.PartialRole) def test__deserialize_audit_log_overwrites(self, entity_factory_impl): test_overwrite_payloads = [ @@ -294,9 +295,9 @@ def overwrite_info_payload(self): def test__deserialize_channel_overwrite_entry_info(self, entity_factory_impl, overwrite_info_payload): overwrite_entry_info = entity_factory_impl._deserialize_channel_overwrite_entry_info(overwrite_info_payload) assert overwrite_entry_info.id == 123123123 - assert overwrite_entry_info.type is channels.PermissionOverwriteType.ROLE + assert overwrite_entry_info.type is channel_models.PermissionOverwriteType.ROLE assert overwrite_entry_info.role_name == "aRole" - assert isinstance(overwrite_entry_info, audit_logs.ChannelOverwriteEntryInfo) + assert isinstance(overwrite_entry_info, audit_log_models.ChannelOverwriteEntryInfo) @pytest.fixture() def message_pin_info_payload(self): @@ -309,7 +310,7 @@ def test__deserialize_message_pin_entry_info(self, entity_factory_impl, message_ message_pin_info = entity_factory_impl._deserialize_message_pin_entry_info(message_pin_info_payload) assert message_pin_info.channel_id == 123123123 assert message_pin_info.message_id == 69696969 - assert isinstance(message_pin_info, audit_logs.MessagePinEntryInfo) + assert isinstance(message_pin_info, audit_log_models.MessagePinEntryInfo) @pytest.fixture() def member_prune_info_payload(self): @@ -322,7 +323,7 @@ def test__deserialize_member_prune_entry_info(self, entity_factory_impl, member_ member_prune_info = entity_factory_impl._deserialize_member_prune_entry_info(member_prune_info_payload) assert member_prune_info.delete_member_days == datetime.timedelta(days=7) assert member_prune_info.members_removed == 1 - assert isinstance(member_prune_info, audit_logs.MemberPruneEntryInfo) + assert isinstance(member_prune_info, audit_log_models.MemberPruneEntryInfo) @pytest.fixture() def message_bulk_delete_info_payload(self): @@ -333,7 +334,7 @@ def test__deserialize_message_bulk_delete_entry_info(self, entity_factory_impl, message_bulk_delete_info_payload ) assert message_bulk_delete_entry_info.count == 42 - assert isinstance(message_bulk_delete_entry_info, audit_logs.MessageBulkDeleteEntryInfo) + assert isinstance(message_bulk_delete_entry_info, audit_log_models.MessageBulkDeleteEntryInfo) @pytest.fixture() def message_delete_info_payload(self): @@ -345,7 +346,7 @@ def test__deserialize_message_delete_entry_info(self, entity_factory_impl, messa ) assert message_delete_entry_info.count == 42 assert message_delete_entry_info.channel_id == 4206942069 - assert isinstance(message_delete_entry_info, audit_logs.MessageDeleteEntryInfo) + assert isinstance(message_delete_entry_info, audit_log_models.MessageDeleteEntryInfo) @pytest.fixture() def member_disconnect_info_payload(self): @@ -356,7 +357,7 @@ def test__deserialize_member_disconnect_entry_info(self, entity_factory_impl, me member_disconnect_info_payload ) assert member_disconnect_entry_info.count == 42 - assert isinstance(member_disconnect_entry_info, audit_logs.MemberDisconnectEntryInfo) + assert isinstance(member_disconnect_entry_info, audit_log_models.MemberDisconnectEntryInfo) @pytest.fixture() def member_move_info_payload(self): @@ -365,7 +366,7 @@ def member_move_info_payload(self): def test__deserialize_member_move_entry_info(self, entity_factory_impl, member_move_info_payload): member_move_entry_info = entity_factory_impl._deserialize_member_move_entry_info(member_move_info_payload) assert member_move_entry_info.channel_id == 22222222 - assert isinstance(member_move_entry_info, audit_logs.MemberMoveEntryInfo) + assert isinstance(member_move_entry_info, audit_log_models.MemberMoveEntryInfo) @pytest.fixture() def unrecognised_audit_log_entry(self): @@ -377,7 +378,7 @@ def test__deserialize_unrecognised_audit_log_entry_info(self, entity_factory_imp ) assert unrecognised_info.count == "5412" assert unrecognised_info.action == "nyaa'd" - assert isinstance(unrecognised_info, audit_logs.UnrecognisedAuditLogEntryInfo) + assert isinstance(unrecognised_info, audit_log_models.UnrecognisedAuditLogEntryInfo) @pytest.fixture() def audit_log_entry_payload(self): @@ -401,9 +402,9 @@ def audit_log_entry_payload(self): def test_deserialize_audit_log(self, entity_factory_impl, mock_app, audit_log_entry_payload): raise NotImplementedError # TODO: test coverage for audit log cases - ############ - # CHANNELS # - ############ + ################## + # CHANNEL MODELS # + ################## @pytest.fixture() def permission_overwrite_payload(self): @@ -411,13 +412,13 @@ def permission_overwrite_payload(self): def test_deserialize_permission_overwrite(self, entity_factory_impl, permission_overwrite_payload): overwrite = entity_factory_impl.deserialize_permission_overwrite(permission_overwrite_payload) - assert overwrite.type == channels.PermissionOverwriteType.MEMBER - assert overwrite.allow == permissions.Permission(65) - assert overwrite.deny == permissions.Permission(49152) - assert isinstance(overwrite, channels.PermissionOverwrite) + assert overwrite.type == channel_models.PermissionOverwriteType.MEMBER + assert overwrite.allow == permission_models.Permission(65) + assert overwrite.deny == permission_models.Permission(49152) + assert isinstance(overwrite, channel_models.PermissionOverwrite) def test_serialize_permission_overwrite(self, entity_factory_impl): - overwrite = channels.PermissionOverwrite(id=123123, type="member", allow=42, deny=62) + overwrite = channel_models.PermissionOverwrite(id=123123, type="member", allow=42, deny=62) payload = entity_factory_impl.serialize_permission_overwrite(overwrite) assert payload == {"id": "123123", "type": "member", "allow": 42, "deny": 62} @@ -430,8 +431,8 @@ def test_deserialize_partial_channel(self, entity_factory_impl, mock_app, partia assert partial_channel._app is mock_app assert partial_channel.id == 561884984214814750 assert partial_channel.name == "general" - assert partial_channel.type == channels.ChannelType.GUILD_TEXT - assert isinstance(partial_channel, channels.PartialChannel) + assert partial_channel.type == channel_models.ChannelType.GUILD_TEXT + assert isinstance(partial_channel, channel_models.PartialChannel) def test_deserialize_partial_channel_with_unset_fields(self, entity_factory_impl): assert entity_factory_impl.deserialize_partial_channel({"id": "22", "type": 0}).name is None @@ -451,9 +452,9 @@ def test_deserialize_dm_channel(self, entity_factory_impl, mock_app, dm_channel_ assert dm_channel.id == 123 assert dm_channel.name is None assert dm_channel.last_message_id == 456 - assert dm_channel.type is channels.ChannelType.DM + assert dm_channel.type is channel_models.ChannelType.DM assert dm_channel.recipients == {115590097100865541: entity_factory_impl.deserialize_user(user_payload)} - assert isinstance(dm_channel, channels.DMChannel) + assert isinstance(dm_channel, channel_models.DMChannel) def test_deserialize_dm_channel_with_null_fields(self, entity_factory_impl, user_payload): dm_channel = entity_factory_impl.deserialize_dm_channel( @@ -470,6 +471,7 @@ def group_dm_channel_payload(self, user_payload): "owner_id": "456", "application_id": "123789", "last_message_id": "456", + "nicks": [{"id": "115590097100865541", "nick": "nyaa"}], "type": 3, "recipients": [user_payload], } @@ -481,13 +483,14 @@ def test_deserialize_group_dm_channel(self, entity_factory_impl, mock_app, group assert group_dm.name == "Secret Developer Group" assert group_dm.icon_hash == "123asdf123adsf" assert group_dm.application_id == 123789 + assert group_dm.nicknames == {115590097100865541: "nyaa"} assert group_dm.last_message_id == 456 - assert group_dm.type == channels.ChannelType.GROUP_DM + assert group_dm.type == channel_models.ChannelType.GROUP_DM assert group_dm.recipients == {115590097100865541: entity_factory_impl.deserialize_user(user_payload)} - assert isinstance(group_dm, channels.GroupDMChannel) + assert isinstance(group_dm, channel_models.GroupDMChannel) def test_test_deserialize_group_dm_channel_with_unset_fields(self, entity_factory_impl, user_payload): - dm_channel = entity_factory_impl.deserialize_group_dm_channel( + group_dm = entity_factory_impl.deserialize_group_dm_channel( { "id": "123", "name": "Secret Developer Group", @@ -498,7 +501,8 @@ def test_test_deserialize_group_dm_channel_with_unset_fields(self, entity_factor "recipients": [user_payload], } ) - assert dm_channel.application_id is None + assert group_dm.nicknames == {} + assert group_dm.application_id is None @pytest.fixture() def guild_category_payload(self, permission_overwrite_payload): @@ -520,7 +524,7 @@ def test_deserialize_guild_category( assert guild_category._app is mock_app assert guild_category.id == 123 assert guild_category.name == "Test" - assert guild_category.type == channels.ChannelType.GUILD_CATEGORY + assert guild_category.type == channel_models.ChannelType.GUILD_CATEGORY assert guild_category.guild_id == 9876 assert guild_category.position == 3 assert guild_category.permission_overwrites == { @@ -528,7 +532,7 @@ def test_deserialize_guild_category( } assert guild_category.is_nsfw is True assert guild_category.parent_id == 664565 - assert isinstance(guild_category, channels.GuildCategory) + assert isinstance(guild_category, channel_models.GuildCategory) def test_deserialize_guild_category_with_unset_fields(self, entity_factory_impl, permission_overwrite_payload): guild_category = entity_factory_impl.deserialize_guild_category( @@ -583,7 +587,7 @@ def test_deserialize_guild_text_channel( assert guild_text_channel._app is mock_app assert guild_text_channel.id == 123 assert guild_text_channel.name == "general" - assert guild_text_channel.type == channels.ChannelType.GUILD_TEXT + assert guild_text_channel.type == channel_models.ChannelType.GUILD_TEXT assert guild_text_channel.guild_id == 567 assert guild_text_channel.position == 6 assert guild_text_channel.permission_overwrites == { @@ -597,7 +601,7 @@ def test_deserialize_guild_text_channel( assert guild_text_channel.last_pin_timestamp == datetime.datetime( 2020, 5, 27, 15, 58, 51, 545252, tzinfo=datetime.timezone.utc ) - assert isinstance(guild_text_channel, channels.GuildTextChannel) + assert isinstance(guild_text_channel, channel_models.GuildTextChannel) def test_deserialize_guild_text_channel_with_unset_fields(self, entity_factory_impl): guild_text_channel = entity_factory_impl.deserialize_guild_text_channel( @@ -642,7 +646,7 @@ def test_deserialize_guild_text_channel_with_null_fields(self, entity_factory_im @pytest.fixture() def guild_news_channel_payload(self, permission_overwrite_payload): return { - "id": "567", + "id": "7777", "guild_id": "123", "name": "Important Announcements", "type": 5, @@ -660,9 +664,9 @@ def test_deserialize_guild_news_channel( ): news_channel = entity_factory_impl.deserialize_guild_news_channel(guild_news_channel_payload) assert news_channel._app is mock_app - assert news_channel.id == 567 + assert news_channel.id == 7777 assert news_channel.name == "Important Announcements" - assert news_channel.type == channels.ChannelType.GUILD_NEWS + assert news_channel.type == channel_models.ChannelType.GUILD_NEWS assert news_channel.guild_id == 123 assert news_channel.position == 0 assert news_channel.permission_overwrites == { @@ -675,7 +679,7 @@ def test_deserialize_guild_news_channel( assert news_channel.last_pin_timestamp == datetime.datetime( 2020, 5, 27, 15, 58, 51, 545252, tzinfo=datetime.timezone.utc ) - assert isinstance(news_channel, channels.GuildNewsChannel) + assert isinstance(news_channel, channel_models.GuildNewsChannel) def test_deserialize_guild_news_channel_with_unset_fields(self, entity_factory_impl): news_channel = entity_factory_impl.deserialize_guild_news_channel( @@ -734,7 +738,7 @@ def test_deserialize_guild_store_channel( store_chanel = entity_factory_impl.deserialize_guild_store_channel(guild_store_channel_payload) assert store_chanel.id == 123 assert store_chanel.name == "Half Life 3" - assert store_chanel.type == channels.ChannelType.GUILD_STORE + assert store_chanel.type == channel_models.ChannelType.GUILD_STORE assert store_chanel.guild_id == 1234 assert store_chanel.position == 2 assert store_chanel.permission_overwrites == { @@ -742,7 +746,7 @@ def test_deserialize_guild_store_channel( } assert store_chanel.is_nsfw is True assert store_chanel.parent_id == 9876 - assert isinstance(store_chanel, channels.GuildStoreChannel) + assert isinstance(store_chanel, channel_models.GuildStoreChannel) def test_deserialize_guild_store_channel_with_unset_fields(self, entity_factory_impl): store_chanel = entity_factory_impl.deserialize_guild_store_channel( @@ -788,7 +792,7 @@ def test_deserialize_guild_voice_channel( voice_channel = entity_factory_impl.deserialize_guild_voice_channel(guild_voice_channel_payload) assert voice_channel.id == 555 assert voice_channel.name == "Secret Developer Discussions" - assert voice_channel.type == channels.ChannelType.GUILD_VOICE + assert voice_channel.type == channel_models.ChannelType.GUILD_VOICE assert voice_channel.guild_id == 789 assert voice_channel.position == 4 assert voice_channel.permission_overwrites == { @@ -798,7 +802,7 @@ def test_deserialize_guild_voice_channel( assert voice_channel.parent_id == 456 assert voice_channel.bitrate == 64000 assert voice_channel.user_limit == 3 - assert isinstance(voice_channel, channels.GuildVoiceChannel) + assert isinstance(voice_channel, channel_models.GuildVoiceChannel) def test_deserialize_guild_voice_channel_with_null_fields(self, entity_factory_impl): voice_channel = entity_factory_impl.deserialize_guild_voice_channel( @@ -845,19 +849,19 @@ def test_deserialize_channel_returns_right_type( guild_voice_channel_payload, ): for payload, expected_type in [ - (dm_channel_payload, channels.DMChannel), - (group_dm_channel_payload, channels.GroupDMChannel), - (guild_category_payload, channels.GuildCategory), - (guild_text_channel_payload, channels.GuildTextChannel), - (guild_news_channel_payload, channels.GuildNewsChannel), - (guild_store_channel_payload, channels.GuildStoreChannel), - (guild_voice_channel_payload, channels.GuildVoiceChannel), + (dm_channel_payload, channel_models.DMChannel), + (group_dm_channel_payload, channel_models.GroupDMChannel), + (guild_category_payload, channel_models.GuildCategory), + (guild_text_channel_payload, channel_models.GuildTextChannel), + (guild_news_channel_payload, channel_models.GuildNewsChannel), + (guild_store_channel_payload, channel_models.GuildStoreChannel), + (guild_voice_channel_payload, channel_models.GuildVoiceChannel), ]: assert isinstance(entity_factory_impl.deserialize_channel(payload), expected_type) - ########## - # EMBEDS # - ########## + ################ + # EMBED MODELS # + ################ @pytest.fixture def embed_payload(self): @@ -901,47 +905,47 @@ def test_deserialize_embed_with_full_embed(self, entity_factory_impl, embed_payl assert embed.description == "embed description" assert embed.url == "https://somewhere.com" assert embed.timestamp == datetime.datetime(2020, 3, 22, 16, 40, 39, 218000, tzinfo=datetime.timezone.utc) - assert embed.color == colors.Color(14014915) - assert isinstance(embed.color, colors.Color) + assert embed.color == color_models.Color(14014915) + assert isinstance(embed.color, color_models.Color) # EmbedFooter assert embed.footer.text == "footer text" assert embed.footer.icon_url == "https://somewhere.com/footer.png" assert embed.footer.proxy_icon_url == "https://media.somewhere.com/footer.png" - assert isinstance(embed.footer, embeds.EmbedFooter) + assert isinstance(embed.footer, embed_models.EmbedFooter) # EmbedImage assert embed.image.url == "https://somewhere.com/image.png" assert embed.image.proxy_url == "https://media.somewhere.com/image.png" assert embed.image.height == 122 assert embed.image.width == 133 - assert isinstance(embed.image, embeds.EmbedImage) + assert isinstance(embed.image, embed_models.EmbedImage) # EmbedThumbnail assert embed.thumbnail.url == "https://somewhere.com/thumbnail.png" assert embed.thumbnail.proxy_url == "https://media.somewhere.com/thumbnail.png" assert embed.thumbnail.height == 123 assert embed.thumbnail.width == 456 - assert isinstance(embed.thumbnail, embeds.EmbedThumbnail) + assert isinstance(embed.thumbnail, embed_models.EmbedThumbnail) # EmbedVideo assert embed.video.url == "https://somewhere.com/video.mp4" assert embed.video.height == 1234 assert embed.video.width == 4567 - assert isinstance(embed.video, embeds.EmbedVideo) + assert isinstance(embed.video, embed_models.EmbedVideo) # EmbedProvider assert embed.provider.name == "some name" assert embed.provider.url == "https://somewhere.com/provider" - assert isinstance(embed.provider, embeds.EmbedProvider) + assert isinstance(embed.provider, embed_models.EmbedProvider) # EmbedAuthor assert embed.author.name == "some name" assert embed.author.url == "https://somewhere.com/author" assert embed.author.icon_url == "https://somewhere.com/author.png" assert embed.author.proxy_icon_url == "https://media.somewhere.com/author.png" - assert isinstance(embed.author, embeds.EmbedAuthor) + assert isinstance(embed.author, embed_models.EmbedAuthor) # EmbedField assert len(embed.fields) == 1 field = embed.fields[0] assert field.name == "title" assert field.value == "some value" assert field.is_inline is True - assert isinstance(field, embeds.EmbedField) + assert isinstance(field, embed_models.EmbedField) def test_deserialize_embed_with_partial_fields(self, entity_factory_impl, embed_payload): embed = entity_factory_impl.deserialize_embed( @@ -959,41 +963,41 @@ def test_deserialize_embed_with_partial_fields(self, entity_factory_impl, embed_ assert embed.footer.text == "footer text" assert embed.footer.icon_url is None assert embed.footer.proxy_icon_url is None - assert isinstance(embed.footer, embeds.EmbedFooter) + assert isinstance(embed.footer, embed_models.EmbedFooter) # EmbedImage assert embed.image.url is None assert embed.image.proxy_url is None assert embed.image.height is None assert embed.image.width is None - assert isinstance(embed.image, embeds.EmbedImage) + assert isinstance(embed.image, embed_models.EmbedImage) # EmbedThumbnail assert embed.thumbnail.url is None assert embed.thumbnail.proxy_url is None assert embed.thumbnail.height is None assert embed.thumbnail.width is None - assert isinstance(embed.thumbnail, embeds.EmbedThumbnail) + assert isinstance(embed.thumbnail, embed_models.EmbedThumbnail) # EmbedVideo assert embed.video.url is None assert embed.video.height is None assert embed.video.width is None - assert isinstance(embed.video, embeds.EmbedVideo) + assert isinstance(embed.video, embed_models.EmbedVideo) # EmbedProvider assert embed.provider.name is None assert embed.provider.url is None - assert isinstance(embed.provider, embeds.EmbedProvider) + assert isinstance(embed.provider, embed_models.EmbedProvider) # EmbedAuthor assert embed.author.name is None assert embed.author.url is None assert embed.author.icon_url is None assert embed.author.proxy_icon_url is None - assert isinstance(embed.author, embeds.EmbedAuthor) + assert isinstance(embed.author, embed_models.EmbedAuthor) # EmbedField assert len(embed.fields) == 1 field = embed.fields[0] assert field.name == "title" assert field.value == "some value" assert field.is_inline is False - assert isinstance(field, embeds.EmbedField) + assert isinstance(field, embed_models.EmbedField) def test_deserialize_embed_with_empty_embed(self, entity_factory_impl): embed = entity_factory_impl.deserialize_embed({}) @@ -1012,17 +1016,17 @@ def test_deserialize_embed_with_empty_embed(self, entity_factory_impl): def test_serialize_embed(self, entity_factory_impl): payload = entity_factory_impl.serialize_embed( - embeds.Embed( + embed_models.Embed( title="Title", description="Nyaa", url="https://some-url", timestamp=datetime.datetime(2020, 5, 29, 20, 37, 22, 865139), - color=colors.Color(321321), - footer=embeds.EmbedFooter(text="TEXT", icon_url="httppppp"), - image=embeds.EmbedImage(url="https://///"), - thumbnail=embeds.EmbedThumbnail(url="wss://not-a-valid-url"), - author=embeds.EmbedAuthor(name="AUTH ME", url="wss://\\_/-_-\\_/", icon_url="icon://"), - fields=[embeds.EmbedField(value="VALUE", name="NAME", is_inline=True)], + color=color_models.Color(321321), + footer=embed_models.EmbedFooter(text="TEXT", icon_url="httppppp"), + image=embed_models.EmbedImage(url="https://///"), + thumbnail=embed_models.EmbedThumbnail(url="wss://not-a-valid-url"), + author=embed_models.EmbedAuthor(name="AUTH ME", url="wss://\\_/-_-\\_/", icon_url="icon://"), + fields=[embed_models.EmbedField(value="VALUE", name="NAME", is_inline=True)], ) ) assert payload == { @@ -1040,17 +1044,17 @@ def test_serialize_embed(self, entity_factory_impl): def test_serialize_embed_with_null_sub_fields(self, entity_factory_impl): payload = entity_factory_impl.serialize_embed( - embeds.Embed( + embed_models.Embed( title="Title", description="Nyaa", url="https://some-url", timestamp=datetime.datetime(2020, 5, 29, 20, 37, 22, 865139), - color=colors.Color(321321), - footer=embeds.EmbedFooter(), - image=embeds.EmbedImage(), - thumbnail=embeds.EmbedThumbnail(), - author=embeds.EmbedAuthor(), - fields=[embeds.EmbedField()], + color=color_models.Color(321321), + footer=embed_models.EmbedFooter(), + image=embed_models.EmbedImage(), + thumbnail=embed_models.EmbedThumbnail(), + author=embed_models.EmbedAuthor(), + fields=[embed_models.EmbedField()], ) ) assert payload == { @@ -1067,16 +1071,16 @@ def test_serialize_embed_with_null_sub_fields(self, entity_factory_impl): } def test_serialize_embed_with_null_attributes(self, entity_factory_impl): - assert entity_factory_impl.serialize_embed(embeds.Embed()) == {} + assert entity_factory_impl.serialize_embed(embed_models.Embed()) == {} - ########## - # EMOJIS # - ########## + ################ + # EMOJI MODELS # + ################ def test_deserialize_unicode_emoji(self, entity_factory_impl): emoji = entity_factory_impl.deserialize_unicode_emoji({"name": "🤷"}) assert emoji.name == "🤷" - assert isinstance(emoji, emojis.UnicodeEmoji) + assert isinstance(emoji, emoji_models.UnicodeEmoji) @pytest.fixture() def custom_emoji_payload(self): @@ -1088,7 +1092,7 @@ def test_deserialize_custom_emoji(self, entity_factory_impl, mock_app, custom_em assert emoji.id == 691225175349395456 assert emoji.name == "test" assert emoji.is_animated is True - assert isinstance(emoji, emojis.CustomEmoji) + assert isinstance(emoji, emoji_models.CustomEmoji) def test_deserialize_custom_emoji_with_unset_and_null_fields( self, entity_factory_impl, mock_app, custom_emoji_payload @@ -1123,7 +1127,7 @@ def test_deserialize_known_custom_emoji( assert emoji.is_colons_required is True assert emoji.is_managed is False assert emoji.is_available is True - assert isinstance(emoji, emojis.KnownCustomEmoji) + assert isinstance(emoji, emoji_models.KnownCustomEmoji) def test_deserialize_known_custom_emoji_with_unset_fields(self, entity_factory_impl): emoji = entity_factory_impl.deserialize_known_custom_emoji( @@ -1141,14 +1145,14 @@ def test_deserialize_known_custom_emoji_with_unset_fields(self, entity_factory_i @pytest.mark.parametrize( ["payload", "expected_type"], - [({"name": "🤷"}, emojis.UnicodeEmoji), ({"id": "1234", "name": "test"}, emojis.CustomEmoji)], + [({"name": "🤷"}, emoji_models.UnicodeEmoji), ({"id": "1234", "name": "test"}, emoji_models.CustomEmoji)], ) def test_deserialize_emoji_returns_expected_type(self, entity_factory_impl, payload, expected_type): isinstance(entity_factory_impl.deserialize_emoji(payload), expected_type) - ########### - # GATEWAY # - ########### + ################## + # GATEWAY MODELS # + ################## @pytest.fixture() def gateway_bot_payload(self): @@ -1160,19 +1164,19 @@ def gateway_bot_payload(self): def test_deserialize_gateway_bot(self, entity_factory_impl, gateway_bot_payload): gateway_bot = entity_factory_impl.deserialize_gateway_bot(gateway_bot_payload) - assert isinstance(gateway_bot, gateway.GatewayBot) + assert isinstance(gateway_bot, gateway_models.GatewayBot) assert gateway_bot.url == "wss://gateway.discord.gg" assert gateway_bot.shard_count == 1 # SessionStartLimit - assert isinstance(gateway_bot.session_start_limit, gateway.SessionStartLimit) + assert isinstance(gateway_bot.session_start_limit, gateway_models.SessionStartLimit) assert gateway_bot.session_start_limit.max_concurrency == 5 assert gateway_bot.session_start_limit.total == 1000 assert gateway_bot.session_start_limit.remaining == 991 assert gateway_bot.session_start_limit.reset_after == datetime.timedelta(milliseconds=14170186) - ########## - # GUILDS # - ########## + ################ + # GUILD MODELS # + ################ @pytest.fixture() def guild_embed_payload(self): @@ -1183,7 +1187,7 @@ def test_deserialize_widget_embed(self, entity_factory_impl, mock_app, guild_emb assert guild_embed._app is mock_app assert guild_embed.channel_id == 123123123 assert guild_embed.is_enabled is True - assert isinstance(guild_embed, guilds.GuildWidget) + assert isinstance(guild_embed, guild_models.GuildWidget) def test_deserialize_guild_embed_with_null_fields(self, entity_factory_impl, mock_app): assert entity_factory_impl.deserialize_guild_widget({"channel_id": None, "enabled": True}).channel_id is None @@ -1195,7 +1199,6 @@ def member_payload(self, user_payload): "roles": ["11111", "22222", "33333", "44444"], "joined_at": "2015-04-26T06:26:56.936000+00:00", "premium_since": "2019-05-17T06:26:56.936000+00:00", - # These should be completely ignored. "deaf": False, "mute": True, "user": user_payload, @@ -1211,7 +1214,53 @@ def test_deserialize_member(self, entity_factory_impl, mock_app, member_payload, assert member.premium_since == datetime.datetime(2019, 5, 17, 6, 26, 56, 936000, tzinfo=datetime.timezone.utc) assert member.is_deaf is False assert member.is_mute is True - assert isinstance(member, guilds.Member) + assert isinstance(member, guild_models.Member) + + def test_deserialize_member_with_null_fields(self, entity_factory_impl, user_payload): + member = entity_factory_impl.deserialize_member( + { + "nick": None, + "roles": ["11111", "22222", "33333", "44444"], + "joined_at": "2015-04-26T06:26:56.936000+00:00", + "premium_since": None, + "deaf": False, + "mute": True, + "user": user_payload, + } + ) + assert member.nickname is None + assert member.premium_since is None + assert member.is_deaf is False + assert member.is_mute is True + assert isinstance(member, guild_models.Member) + + def test_deserialize_member_with_undefined_fields(self, entity_factory_impl, user_payload): + member = entity_factory_impl.deserialize_member( + { + "roles": ["11111", "22222", "33333", "44444"], + "joined_at": "2015-04-26T06:26:56.936000+00:00", + "user": user_payload, + } + ) + assert member.nickname is undefined.Undefined() + assert member.premium_since is undefined.Undefined() + assert member.is_deaf is undefined.Undefined() + assert member.is_mute is undefined.Undefined() + + def test_deserialize_member_with_passed_through_user_object(self, entity_factory_impl): + mock_user = mock.MagicMock(user_models.User) + member = entity_factory_impl.deserialize_member( + { + "nick": "foobarbaz", + "roles": ["11111", "22222", "33333", "44444"], + "joined_at": "2015-04-26T06:26:56.936000+00:00", + "premium_since": "2019-05-17T06:26:56.936000+00:00", + "deaf": False, + "mute": True, + }, + user=mock_user, + ) + assert member.user is mock_user @pytest.fixture() def guild_role_payload(self): @@ -1231,427 +1280,159 @@ def test_deserialize_role(self, entity_factory_impl, mock_app, guild_role_payloa assert guild_role._app is mock_app assert guild_role.id == 41771983423143936 assert guild_role.name == "WE DEM BOYZZ!!!!!!" - assert guild_role.color == colors.Color(3_447_003) + assert guild_role.color == color_models.Color(3_447_003) assert guild_role.is_hoisted is True assert guild_role.position == 0 - assert guild_role.permissions == permissions.Permission(66_321_471) + assert guild_role.permissions == permission_models.Permission(66_321_471) assert guild_role.is_managed is False assert guild_role.is_mentionable is False - assert isinstance(guild_role, guilds.Role) + assert isinstance(guild_role, guild_models.Role) @pytest.fixture() - def presence_activity_payload(self, custom_emoji_payload): + def partial_integration_payload(self): return { - "name": "an activity", - "type": 1, - "url": "https://69.420.owouwunyaa", - "created_at": 1584996792798, - "timestamps": {"start": 1584996792798, "end": 1999999792798}, - "application_id": "40404040404040", - "details": "They are doing stuff", - "state": "STATED", - "emoji": custom_emoji_payload, - "party": {"id": "spotify:3234234234", "size": [2, 5]}, - "assets": { - "large_image": "34234234234243", - "large_text": "LARGE TEXT", - "small_image": "3939393", - "small_text": "small text", - }, - "secrets": {"join": "who's a good secret?", "spectate": "I'm a good secret", "match": "No."}, - "instance": True, - "flags": 3, + "id": "4949494949", + "name": "Blah blah", + "type": "twitch", + "account": {"id": "543453", "name": "Blam"}, } + def test_deserialize_partial_integration(self, entity_factory_impl, partial_integration_payload): + partial_integration = entity_factory_impl.deserialize_partial_integration(partial_integration_payload) + assert partial_integration.id == 4949494949 + assert partial_integration.name == "Blah blah" + assert partial_integration.type == "twitch" + assert isinstance(partial_integration, guild_models.PartialIntegration) + # IntegrationAccount + assert partial_integration.account.id == "543453" + assert partial_integration.account.name == "Blam" + assert isinstance(partial_integration.account, guild_models.IntegrationAccount) + @pytest.fixture() - def member_presence_payload(self, user_payload, presence_activity_payload): + def integration_payload(self, user_payload): return { + "id": "420", + "name": "blaze it", + "type": "youtube", + "account": {"id": "6969", "name": "Blaze it"}, + "enabled": True, + "syncing": False, + "role_id": "98494949", + "enable_emoticons": False, + "expire_behavior": 1, + "expire_grace_period": 7, "user": user_payload, - "roles": ["49494949"], - "game": presence_activity_payload, - "guild_id": "44004040", - "status": "dnd", - "activities": [presence_activity_payload], - "client_status": {"desktop": "online", "mobile": "idle", "web": "dnd"}, - "premium_since": "2015-04-26T06:26:56.936000+00:00", - "nick": "Nick", + "synced_at": "2015-04-26T06:26:56.936000+00:00", } - def test_deserialize_member_presence( - self, entity_factory_impl, mock_app, member_presence_payload, custom_emoji_payload, user_payload - ): - presence = entity_factory_impl.deserialize_member_presence(member_presence_payload) - assert presence._app is mock_app - # PresenceUser - assert presence.user._app is mock_app - assert presence.user.id == 115590097100865541 - assert presence.user.discriminator == "6127" - assert presence.user.username == "nyaa" - assert presence.user.avatar_hash == "b3b24c6d7cbcdec129d5d537067061a8" - assert presence.user.is_bot is True - assert presence.user.is_system is True - assert presence.user.flags == users.UserFlag(131072) + def test_deserialize_integration(self, entity_factory_impl, integration_payload, user_payload): + integration = entity_factory_impl.deserialize_integration(integration_payload) + assert integration.id == 420 + assert integration.name == "blaze it" + assert integration.type == "youtube" + # IntegrationAccount + assert integration.account.id == "6969" + assert integration.account.name == "Blaze it" + assert isinstance(integration.account, guild_models.IntegrationAccount) - assert isinstance(presence.user, presences.PresenceUser) - assert presence.role_ids == {49494949} - assert presence.guild_id == 44004040 - assert presence.visible_status == presences.PresenceStatus.DND - # PresenceActivity - assert len(presence.activities) == 1 - activity = presence.activities[0] - assert activity.name == "an activity" - assert activity.type == presences.ActivityType.STREAMING - assert activity.url == "https://69.420.owouwunyaa" - assert activity.created_at == datetime.datetime(2020, 3, 23, 20, 53, 12, 798000, tzinfo=datetime.timezone.utc) - # ActivityTimestamps - assert activity.timestamps.start == datetime.datetime( - 2020, 3, 23, 20, 53, 12, 798000, tzinfo=datetime.timezone.utc + assert integration.is_enabled is True + assert integration.is_syncing is False + assert integration.role_id == 98494949 + assert integration.is_emojis_enabled is False + assert integration.expire_behavior == guild_models.IntegrationExpireBehaviour.KICK + assert integration.expire_grace_period == datetime.timedelta(days=7) + assert integration.user == entity_factory_impl.deserialize_user(user_payload) + assert integration.last_synced_at == datetime.datetime( + 2015, 4, 26, 6, 26, 56, 936000, tzinfo=datetime.timezone.utc ) - assert activity.timestamps.end == datetime.datetime( - 2033, 5, 18, 3, 29, 52, 798000, tzinfo=datetime.timezone.utc + assert isinstance(integration, guild_models.Integration) + + def test_deserialize_guild_integration_with_null_and_unset_fields(self, entity_factory_impl, user_payload): + integration = entity_factory_impl.deserialize_integration( + { + "id": "420", + "name": "blaze it", + "type": "youtube", + "account": {"id": "6969", "name": "Blaze it"}, + "enabled": True, + "syncing": False, + "role_id": "98494949", + "expire_behavior": 1, + "expire_grace_period": 7, + "user": user_payload, + "synced_at": None, + } ) + assert integration.is_emojis_enabled is None + assert integration.last_synced_at is None - assert activity.application_id == 40404040404040 - assert activity.details == "They are doing stuff" - assert activity.state == "STATED" - assert activity.emoji == entity_factory_impl.deserialize_emoji(custom_emoji_payload) - # ActivityParty - assert activity.party.id == "spotify:3234234234" - assert activity.party.current_size == 2 - assert activity.party.max_size == 5 - assert isinstance(activity.party, presences.ActivityParty) - # ActivityAssets - assert activity.assets.large_image == "34234234234243" - assert activity.assets.large_text == "LARGE TEXT" - assert activity.assets.small_image == "3939393" - assert activity.assets.small_text == "small text" - assert isinstance(activity.assets, presences.ActivityAssets) - # ActivitySecrets - assert activity.secrets.join == "who's a good secret?" - assert activity.secrets.spectate == "I'm a good secret" - assert activity.secrets.match == "No." - assert isinstance(activity.secrets, presences.ActivitySecret) - assert activity.is_instance is True - assert activity.flags == presences.ActivityFlag(3) - assert isinstance(activity, presences.RichActivity) + @pytest.fixture() + def guild_member_ban_payload(self, user_payload): + return {"reason": "Get nyaa'ed", "user": user_payload} - # ClientStatus - assert presence.client_status.desktop == presences.PresenceStatus.ONLINE - assert presence.client_status.mobile == presences.PresenceStatus.IDLE - assert presence.client_status.web == presences.PresenceStatus.DND - assert isinstance(presence.client_status, presences.ClientStatus) + def test_deserialize_guild_member_ban(self, entity_factory_impl, guild_member_ban_payload, user_payload): + member_ban = entity_factory_impl.deserialize_guild_member_ban(guild_member_ban_payload) + assert member_ban.reason == "Get nyaa'ed" + assert member_ban.user == entity_factory_impl.deserialize_user(user_payload) + assert isinstance(member_ban, guild_models.GuildMemberBan) - assert presence.premium_since == datetime.datetime(2015, 4, 26, 6, 26, 56, 936000, tzinfo=datetime.timezone.utc) - assert presence.nickname == "Nick" - assert isinstance(presence, presences.MemberPresence) + def test_deserialize_guild_member_ban_with_null_fields(self, entity_factory_impl, user_payload): + assert entity_factory_impl.deserialize_guild_member_ban({"reason": None, "user": user_payload}).reason is None - def test_deserialize_member_presence_with_null_fields(self, entity_factory_impl, user_payload): - presence = entity_factory_impl.deserialize_member_presence( - { - "user": {"username": "agent 47", "avatar": None, "discriminator": "4747", "id": "474747474"}, - "roles": [], - "game": None, - "guild_id": "42", - "status": "dnd", - "activities": [], - "client_status": {}, - "premium_since": None, - "nick": None, - } - ) - assert presence.premium_since is None - assert presence.nickname is None - # PresenceUser - assert presence.user.avatar_hash is None + def test_deserialize_unavailable_guild(self, entity_factory_impl, mock_app): + unavailable_guild = entity_factory_impl.deserialize_unavailable_guild({"id": "42069", "unavailable": True}) + assert unavailable_guild._app is mock_app + assert unavailable_guild.id == 42069 + assert unavailable_guild.is_unavailable is True + assert isinstance(unavailable_guild, guild_models.UnavailableGuild) - def test_deserialize_member_presence_with_unset_fields( - self, entity_factory_impl, user_payload, presence_activity_payload + @pytest.fixture() + def guild_preview_payload(self, known_custom_emoji_payload): + return { + "id": "152559372126519269", + "name": "Isopropyl", + "icon": "d4a983885dsaa7691ce8bcaaf945a", + "splash": "dsa345tfcdg54b", + "discovery_splash": "lkodwaidi09239uid", + "emojis": [known_custom_emoji_payload], + "features": ["DISCOVERABLE", "FORCE_RELAY"], + "approximate_member_count": 69, + "approximate_presence_count": 42, + "description": "A DESCRIPTION.", + } + + def test_deserialize_guild_preview( + self, entity_factory_impl, mock_app, guild_preview_payload, known_custom_emoji_payload ): - presence = entity_factory_impl.deserialize_member_presence( + guild_preview = entity_factory_impl.deserialize_guild_preview(guild_preview_payload) + assert guild_preview._app is mock_app + assert guild_preview.id == 152559372126519269 + assert guild_preview.name == "Isopropyl" + assert guild_preview.icon_hash == "d4a983885dsaa7691ce8bcaaf945a" + assert guild_preview.features == {guild_models.GuildFeature.DISCOVERABLE, "FORCE_RELAY"} + assert guild_preview.splash_hash == "dsa345tfcdg54b" + assert guild_preview.discovery_splash_hash == "lkodwaidi09239uid" + assert guild_preview.emojis == { + 12345: entity_factory_impl.deserialize_known_custom_emoji(known_custom_emoji_payload) + } + assert guild_preview.approximate_member_count == 69 + assert guild_preview.approximate_presence_count == 42 + assert guild_preview.description == "A DESCRIPTION." + assert isinstance(guild_preview, guild_models.GuildPreview) + + def test_deserialize_guild_preview_with_null_fields(self, entity_factory_impl, mock_app, guild_preview_payload): + guild_preview = entity_factory_impl.deserialize_guild_preview( { - "user": {"id": "42"}, - "game": presence_activity_payload, - "guild_id": "44004040", - "status": "dnd", - "activities": [], - "client_status": {}, - } - ) - assert presence.premium_since is None - assert presence.nickname is None - assert presence.role_ids is None - # ClientStatus - assert presence.client_status.desktop is presences.PresenceStatus.OFFLINE - assert presence.client_status.mobile is presences.PresenceStatus.OFFLINE - assert presence.client_status.web is presences.PresenceStatus.OFFLINE - # PresenceUser - assert presence.user.id == 42 - assert presence.user.discriminator is undefined.Undefined() - assert presence.user.username is undefined.Undefined() - assert presence.user.avatar_hash is undefined.Undefined() - assert presence.user.is_bot is undefined.Undefined() - assert presence.user.is_system is undefined.Undefined() - assert presence.user.flags is undefined.Undefined() - - def test_deserialize_member_presence_with_unset_activity_fields(self, entity_factory_impl, user_payload): - presence = entity_factory_impl.deserialize_member_presence( - { - "user": user_payload, - "roles": ["49494949"], - "game": None, - "guild_id": "44004040", - "status": "dnd", - "activities": [{"name": "an activity", "type": 1, "created_at": 1584996792798,}], - "client_status": {}, - } - ) - assert len(presence.activities) == 1 - activity = presence.activities[0] - assert activity.url is None - assert activity.timestamps is None - assert activity.application_id is None - assert activity.details is None - assert activity.state is None - assert activity.emoji is None - assert activity.party is None - assert activity.assets is None - assert activity.secrets is None - assert activity.is_instance is None - assert activity.flags is None - - def test_deserialize_member_presence_with_null_activity_fields(self, entity_factory_impl, user_payload): - presence = entity_factory_impl.deserialize_member_presence( - { - "user": user_payload, - "roles": ["49494949"], - "game": None, - "guild_id": "44004040", - "status": "dnd", - "activities": [ - { - "name": "an activity", - "type": 1, - "url": None, - "created_at": 1584996792798, - "timestamps": {"start": 1584996792798, "end": 1999999792798,}, - "application_id": "40404040404040", - "details": None, - "state": None, - "emoji": None, - "party": {"id": "spotify:3234234234", "size": [2, 5]}, - "assets": { - "large_image": "34234234234243", - "large_text": "LARGE TEXT", - "small_image": "3939393", - "small_text": "small text", - }, - "secrets": {"join": "who's a good secret?", "spectate": "I'm a good secret", "match": "No."}, - "instance": True, - "flags": 3, - } - ], - "client_status": {}, - } - ) - assert len(presence.activities) == 1 - activity = presence.activities[0] - assert activity.url is None - assert activity.details is None - assert activity.state is None - assert activity.emoji is None - - def test_deserialize_member_presence_with_unset_activity_sub_fields(self, entity_factory_impl, user_payload): - presence = entity_factory_impl.deserialize_member_presence( - { - "user": user_payload, - "roles": ["49494949"], - "game": None, - "guild_id": "44004040", - "status": "dnd", - "activities": [ - { - "name": "an activity", - "type": 1, - "url": "https://69.420.owouwunyaa", - "created_at": 1584996792798, - "timestamps": {}, - "application_id": "40404040404040", - "details": "They are doing stuff", - "state": "STATED", - "emoji": None, - "party": {}, - "assets": {}, - "secrets": {}, - "instance": True, - "flags": 3, - } - ], - "client_status": {}, - } - ) - activity = presence.activities[0] - # ActivityTimestamps - assert activity.timestamps.start is None - assert activity.timestamps.end is None - # ActivityParty - assert activity.party.id is None - assert activity.party.max_size is None - assert activity.party.current_size is None - # ActivityAssets - assert activity.assets.small_text is None - assert activity.assets.small_image is None - assert activity.assets.large_text is None - assert activity.assets.large_image is None - # ActivitySecrets - assert activity.secrets.join is None - assert activity.secrets.spectate is None - assert activity.secrets.match is None - - @pytest.fixture() - def partial_integration_payload(self): - return { - "id": "4949494949", - "name": "Blah blah", - "type": "twitch", - "account": {"id": "543453", "name": "Blam"}, - } - - def test_deserialize_partial_integration(self, entity_factory_impl, partial_integration_payload): - partial_integration = entity_factory_impl.deserialize_partial_integration(partial_integration_payload) - assert partial_integration.id == 4949494949 - assert partial_integration.name == "Blah blah" - assert partial_integration.type == "twitch" - assert isinstance(partial_integration, guilds.PartialIntegration) - # IntegrationAccount - assert partial_integration.account.id == "543453" - assert partial_integration.account.name == "Blam" - assert isinstance(partial_integration.account, guilds.IntegrationAccount) - - @pytest.fixture() - def integration_payload(self, user_payload): - return { - "id": "420", - "name": "blaze it", - "type": "youtube", - "account": {"id": "6969", "name": "Blaze it"}, - "enabled": True, - "syncing": False, - "role_id": "98494949", - "enable_emoticons": False, - "expire_behavior": 1, - "expire_grace_period": 7, - "user": user_payload, - "synced_at": "2015-04-26T06:26:56.936000+00:00", - } - - def test_deserialize_integration(self, entity_factory_impl, integration_payload, user_payload): - integration = entity_factory_impl.deserialize_integration(integration_payload) - assert integration.id == 420 - assert integration.name == "blaze it" - assert integration.type == "youtube" - # IntegrationAccount - assert integration.account.id == "6969" - assert integration.account.name == "Blaze it" - assert isinstance(integration.account, guilds.IntegrationAccount) - - assert integration.is_enabled is True - assert integration.is_syncing is False - assert integration.role_id == 98494949 - assert integration.is_emojis_enabled is False - assert integration.expire_behavior == guilds.IntegrationExpireBehaviour.KICK - assert integration.expire_grace_period == datetime.timedelta(days=7) - assert integration.user == entity_factory_impl.deserialize_user(user_payload) - assert integration.last_synced_at == datetime.datetime( - 2015, 4, 26, 6, 26, 56, 936000, tzinfo=datetime.timezone.utc - ) - assert isinstance(integration, guilds.Integration) - - def test_deserialize_guild_integration_with_null_and_unset_fields(self, entity_factory_impl, user_payload): - integration = entity_factory_impl.deserialize_integration( - { - "id": "420", - "name": "blaze it", - "type": "youtube", - "account": {"id": "6969", "name": "Blaze it"}, - "enabled": True, - "syncing": False, - "role_id": "98494949", - "expire_behavior": 1, - "expire_grace_period": 7, - "user": user_payload, - "synced_at": None, - } - ) - assert integration.is_emojis_enabled is None - assert integration.last_synced_at is None - - @pytest.fixture() - def guild_member_ban_payload(self, user_payload): - return {"reason": "Get nyaa'ed", "user": user_payload} - - def test_deserialize_guild_member_ban(self, entity_factory_impl, guild_member_ban_payload, user_payload): - member_ban = entity_factory_impl.deserialize_guild_member_ban(guild_member_ban_payload) - assert member_ban.reason == "Get nyaa'ed" - assert member_ban.user == entity_factory_impl.deserialize_user(user_payload) - assert isinstance(member_ban, guilds.GuildMemberBan) - - def test_deserialize_guild_member_ban_with_null_fields(self, entity_factory_impl, user_payload): - assert entity_factory_impl.deserialize_guild_member_ban({"reason": None, "user": user_payload}).reason is None - - def test_deserialize_unavailable_guild(self, entity_factory_impl, mock_app): - unavailable_guild = entity_factory_impl.deserialize_unavailable_guild({"id": "42069", "unavailable": True}) - assert unavailable_guild._app is mock_app - assert unavailable_guild.id == 42069 - assert unavailable_guild.is_unavailable is True - assert isinstance(unavailable_guild, guilds.UnavailableGuild) - - @pytest.fixture() - def guild_preview_payload(self, known_custom_emoji_payload): - return { - "id": "152559372126519269", - "name": "Isopropyl", - "icon": "d4a983885dsaa7691ce8bcaaf945a", - "splash": "dsa345tfcdg54b", - "discovery_splash": "lkodwaidi09239uid", - "emojis": [known_custom_emoji_payload], - "features": ["DISCOVERABLE", "FORCE_RELAY"], - "approximate_member_count": 69, - "approximate_presence_count": 42, - "description": "A DESCRIPTION.", - } - - def test_deserialize_guild_preview( - self, entity_factory_impl, mock_app, guild_preview_payload, known_custom_emoji_payload - ): - guild_preview = entity_factory_impl.deserialize_guild_preview(guild_preview_payload) - assert guild_preview._app is mock_app - assert guild_preview.id == 152559372126519269 - assert guild_preview.name == "Isopropyl" - assert guild_preview.icon_hash == "d4a983885dsaa7691ce8bcaaf945a" - assert guild_preview.features == {guilds.GuildFeature.DISCOVERABLE, "FORCE_RELAY"} - assert guild_preview.splash_hash == "dsa345tfcdg54b" - assert guild_preview.discovery_splash_hash == "lkodwaidi09239uid" - assert guild_preview.emojis == { - 12345: entity_factory_impl.deserialize_known_custom_emoji(known_custom_emoji_payload) - } - assert guild_preview.approximate_member_count == 69 - assert guild_preview.approximate_presence_count == 42 - assert guild_preview.description == "A DESCRIPTION." - assert isinstance(guild_preview, guilds.GuildPreview) - - def test_deserialize_guild_preview_with_null_fields(self, entity_factory_impl, mock_app, guild_preview_payload): - guild_preview = entity_factory_impl.deserialize_guild_preview( - { - "id": "152559372126519269", - "name": "Isopropyl", - "icon": None, - "splash": None, - "discovery_splash": None, - "emojis": [], - "features": ["DISCOVERABLE", "FORCE_RELAY"], - "approximate_member_count": 69, - "approximate_presence_count": 42, - "description": None, + "id": "152559372126519269", + "name": "Isopropyl", + "icon": None, + "splash": None, + "discovery_splash": None, + "emojis": [], + "features": ["DISCOVERABLE", "FORCE_RELAY"], + "approximate_member_count": 69, + "approximate_presence_count": 42, + "description": None, } ) assert guild_preview.icon_hash is None @@ -1664,6 +1445,7 @@ def guild_payload( self, guild_text_channel_payload, guild_voice_channel_payload, + guild_news_channel_payload, known_custom_emoji_payload, member_payload, member_presence_payload, @@ -1677,7 +1459,7 @@ def guild_payload( "approximate_member_count": 15, "approximate_presence_count": 7, "banner": "1a2b3c", - "channels": [guild_text_channel_payload, guild_voice_channel_payload], + "channels": [guild_text_channel_payload, guild_voice_channel_payload, guild_news_channel_payload], "default_message_notifications": 1, "description": "This is a server I guess, its a bit crap though", "discovery_splash": "famfamFAMFAMfam", @@ -1725,6 +1507,7 @@ def test_deserialize_guild( guild_payload, guild_text_channel_payload, guild_voice_channel_payload, + guild_news_channel_payload, known_custom_emoji_payload, member_payload, member_presence_payload, @@ -1737,32 +1520,32 @@ def test_deserialize_guild( assert guild.name == "L33t guild" assert guild.icon_hash == "1a2b3c4d" assert guild.features == { - guilds.GuildFeature.ANIMATED_ICON, - guilds.GuildFeature.MORE_EMOJI, - guilds.GuildFeature.NEWS, + guild_models.GuildFeature.ANIMATED_ICON, + guild_models.GuildFeature.MORE_EMOJI, + guild_models.GuildFeature.NEWS, "SOME_UNDOCUMENTED_FEATURE", } assert guild.splash_hash == "0ff0ff0ff" assert guild.discovery_splash_hash == "famfamFAMFAMfam" assert guild.owner_id == 6969696 - assert guild.my_permissions == permissions.Permission(66_321_471) + assert guild.my_permissions == permission_models.Permission(66_321_471) assert guild.region == "eu-central" assert guild.afk_channel_id == 99998888777766 assert guild.afk_timeout == datetime.timedelta(seconds=1200) assert guild.is_embed_enabled is True assert guild.embed_channel_id == 9439394949 - assert guild.verification_level == guilds.GuildVerificationLevel.VERY_HIGH - assert guild.default_message_notifications == guilds.GuildMessageNotificationsLevel.ONLY_MENTIONS - assert guild.explicit_content_filter == guilds.GuildExplicitContentFilterLevel.ALL_MEMBERS + assert guild.verification_level == guild_models.GuildVerificationLevel.VERY_HIGH + assert guild.default_message_notifications == guild_models.GuildMessageNotificationsLevel.ONLY_MENTIONS + assert guild.explicit_content_filter == guild_models.GuildExplicitContentFilterLevel.ALL_MEMBERS assert guild.roles == {41771983423143936: entity_factory_impl.deserialize_role(guild_role_payload)} assert guild.emojis == {12345: entity_factory_impl.deserialize_known_custom_emoji(known_custom_emoji_payload)} - assert guild.mfa_level == guilds.GuildMFALevel.ELEVATED + assert guild.mfa_level == guild_models.GuildMFALevel.ELEVATED assert guild.application_id == 39494949 assert guild.is_unavailable is False assert guild.widget_channel_id == 9439394949 assert guild.is_widget_enabled is True assert guild.system_channel_id == 19216801 - assert guild.system_channel_flags == guilds.GuildSystemChannelFlag(3) + assert guild.system_channel_flags == guild_models.GuildSystemChannelFlag(3) assert guild.rules_channel_id == 42042069 assert guild.joined_at == datetime.datetime(2019, 5, 17, 6, 26, 56, 936000, tzinfo=datetime.timezone.utc) assert guild.is_large is False @@ -1771,6 +1554,7 @@ def test_deserialize_guild( assert guild.channels == { 123: entity_factory_impl.deserialize_guild_text_channel(guild_text_channel_payload), 555: entity_factory_impl.deserialize_guild_voice_channel(guild_voice_channel_payload), + 7777: entity_factory_impl.deserialize_guild_news_channel(guild_news_channel_payload), } assert guild.presences == { 115590097100865541: entity_factory_impl.deserialize_member_presence(member_presence_payload) @@ -1781,7 +1565,7 @@ def test_deserialize_guild( assert guild.vanity_url_code == "loool" assert guild.description == "This is a server I guess, its a bit crap though" assert guild.banner_hash == "1a2b3c" - assert guild.premium_tier == guilds.GuildPremiumTier.TIER_2 + assert guild.premium_tier == guild_models.GuildPremiumTier.TIER_2 assert guild.premium_subscription_count == 1 assert guild.preferred_locale == "en-GB" assert guild.public_updates_channel_id == 33333333 @@ -1904,9 +1688,9 @@ def test_deserialize_guild_with_null_fields(self, entity_factory_impl): assert guild.premium_subscription_count is None assert guild.public_updates_channel_id is None - ########### - # INVITES # - ########### + ################# + # INVITE MODELS # + ################# @pytest.fixture() def vanity_url_payload(self): @@ -1917,7 +1701,7 @@ def test_deserialize_vanity_url(self, entity_factory_impl, mock_app, vanity_url_ assert vanity_url._app is mock_app assert vanity_url.code == "iamacode" assert vanity_url.uses == 42 - assert isinstance(vanity_url, invites.VanityURL) + assert isinstance(vanity_url, invite_models.VanityURL) @pytest.fixture() def alternative_user_payload(self): @@ -1966,19 +1750,23 @@ def test_deserialize_invite( assert invite.guild.splash_hash == "aSplashForSure" assert invite.guild.banner_hash == "aBannerForSure" assert invite.guild.description == "Describe me cute kitty." - assert invite.guild.verification_level == guilds.GuildVerificationLevel.MEDIUM + assert invite.guild.verification_level == guild_models.GuildVerificationLevel.MEDIUM assert invite.guild.vanity_url_code == "I-am-very-vain" + assert invite.guild_id == 56188492224814744 assert invite.channel == entity_factory_impl.deserialize_partial_channel(partial_channel_payload) + assert invite.channel_id == 561884984214814750 assert invite.inviter == entity_factory_impl.deserialize_user(user_payload) assert invite.target_user == entity_factory_impl.deserialize_user(alternative_user_payload) - assert invite.target_user_type == invites.TargetUserType.STREAM + assert invite.target_user_type == invite_models.TargetUserType.STREAM assert invite.approximate_member_count == 84 assert invite.approximate_presence_count == 42 - assert isinstance(invite, invites.Invite) + assert isinstance(invite, invite_models.Invite) def test_deserialize_invite_with_null_and_unset_fields(self, entity_factory_impl, partial_channel_payload): - invite = entity_factory_impl.deserialize_invite({"code": "aCode", "channel": partial_channel_payload}) + invite = entity_factory_impl.deserialize_invite({"code": "aCode", "channel_id": "43123123"}) + assert invite.channel is None + assert invite.channel_id == 43123123 assert invite.guild is None assert invite.inviter is None assert invite.target_user is None @@ -1986,6 +1774,13 @@ def test_deserialize_invite_with_null_and_unset_fields(self, entity_factory_impl assert invite.approximate_presence_count is None assert invite.approximate_member_count is None + def test_deserialize_invite_with_guild_and_channel_ids_without_objects(self, entity_factory_impl): + invite = entity_factory_impl.deserialize_invite({"code": "aCode", "guild_id": "42", "channel_id": "202020"}) + assert invite.channel is None + assert invite.channel_id == 202020 + assert invite.guild is None + assert invite.guild_id == 42 + @pytest.fixture() def invite_with_metadata_payload(self, partial_channel_payload, user_payload, alternative_user_payload): return { @@ -2034,13 +1829,13 @@ def test_deserialize_invite_with_metadata( assert invite_with_metadata.guild.splash_hash == "aSplashForSure" assert invite_with_metadata.guild.banner_hash == "aBannerForSure" assert invite_with_metadata.guild.description == "Describe me cute kitty." - assert invite_with_metadata.guild.verification_level == guilds.GuildVerificationLevel.MEDIUM + assert invite_with_metadata.guild.verification_level == guild_models.GuildVerificationLevel.MEDIUM assert invite_with_metadata.guild.vanity_url_code == "I-am-very-vain" assert invite_with_metadata.channel == entity_factory_impl.deserialize_partial_channel(partial_channel_payload) assert invite_with_metadata.inviter == entity_factory_impl.deserialize_user(user_payload) assert invite_with_metadata.target_user == entity_factory_impl.deserialize_user(alternative_user_payload) - assert invite_with_metadata.target_user_type == invites.TargetUserType.STREAM + assert invite_with_metadata.target_user_type == invite_models.TargetUserType.STREAM assert invite_with_metadata.approximate_member_count == 84 assert invite_with_metadata.approximate_presence_count == 42 assert invite_with_metadata.uses == 3 @@ -2050,7 +1845,7 @@ def test_deserialize_invite_with_metadata( assert invite_with_metadata.created_at == datetime.datetime( 2015, 4, 26, 6, 26, 56, 936000, tzinfo=datetime.timezone.utc ) - assert isinstance(invite_with_metadata, invites.InviteWithMetadata) + assert isinstance(invite_with_metadata, invite_models.InviteWithMetadata) def test_deserialize_invite_with_metadata_with_null_and_unset_fields( self, entity_factory_impl, partial_channel_payload @@ -2077,9 +1872,9 @@ def test_max_age_when_zero(self, entity_factory_impl, invite_with_metadata_paylo invite_with_metadata_payload["max_age"] = 0 assert entity_factory_impl.deserialize_invite_with_metadata(invite_with_metadata_payload).max_age is None - ############ - # MESSAGES # - ############ + ################## + # MESSAGE MODELS # + ################## @pytest.fixture() def partial_application_payload(self): @@ -2178,7 +1973,7 @@ def test_deserialize_full_message( assert attachment.proxy_url == "https://media.somewhere.com/attachments/123/456/IMG.jpg" assert attachment.width == 1844 assert attachment.height == 2638 - assert isinstance(attachment, messages.Attachment) + assert isinstance(attachment, message_models.Attachment) assert message.embeds == [entity_factory_impl.deserialize_embed(embed_payload)] # Reaction @@ -2186,15 +1981,15 @@ def test_deserialize_full_message( assert reaction.count == 100 assert reaction.is_reacted_by_me is True assert reaction.emoji == entity_factory_impl.deserialize_emoji(custom_emoji_payload) - assert isinstance(reaction, messages.Reaction) + assert isinstance(reaction, message_models.Reaction) assert message.is_pinned is True assert message.webhook_id == 1234 - assert message.type == messages.MessageType.DEFAULT + assert message.type == message_models.MessageType.DEFAULT # Activity - assert message.activity.type == messages.MessageActivityType.JOIN_REQUEST + assert message.activity.type == message_models.MessageActivityType.JOIN_REQUEST assert message.activity.party_id == "ae488379-351d-4a4f-ad32-2b9b01c91657" - assert isinstance(message.activity, messages.MessageActivity) + assert isinstance(message.activity, message_models.MessageActivity) assert message.application == entity_factory_impl.deserialize_application(partial_application_payload) # MessageCrosspost @@ -2203,7 +1998,7 @@ def test_deserialize_full_message( assert message.message_reference.channel_id == 278325129692446722 assert message.message_reference.guild_id == 278325129692446720 - assert message.flags == messages.MessageFlag.IS_CROSSPOST + assert message.flags == message_models.MessageFlag.IS_CROSSPOST assert message.nonce == "171000788183678976" def test_deserialize_message_with_null_and_unset_fields( @@ -2226,26 +2021,298 @@ def test_deserialize_message_with_null_and_unset_fields( "type": 0, } - message = entity_factory_impl.deserialize_message(message_payload) - assert message._app is mock_app - assert message.guild_id is None - assert message.member is None - assert message.edited_timestamp is None - assert message.channel_mentions == set() - assert message.role_mentions == set() - assert message.channel_mentions == set() - assert message.attachments == [] - assert message.embeds == [] - assert message.reactions == [] - assert message.webhook_id is None - assert message.activity is None - assert message.application is None - assert message.message_reference is None - assert message.nonce is None + message = entity_factory_impl.deserialize_message(message_payload) + assert message._app is mock_app + assert message.guild_id is None + assert message.member is None + assert message.edited_timestamp is None + assert message.channel_mentions == set() + assert message.role_mentions == set() + assert message.channel_mentions == set() + assert message.attachments == [] + assert message.embeds == [] + assert message.reactions == [] + assert message.webhook_id is None + assert message.activity is None + assert message.application is None + assert message.message_reference is None + assert message.nonce is None + + ################### + # PRESENCE MODELS # + ################### + + @pytest.fixture() + def presence_activity_payload(self, custom_emoji_payload): + return { + "name": "an activity", + "type": 1, + "url": "https://69.420.owouwunyaa", + "created_at": 1584996792798, + "timestamps": {"start": 1584996792798, "end": 1999999792798}, + "application_id": "40404040404040", + "details": "They are doing stuff", + "state": "STATED", + "emoji": custom_emoji_payload, + "party": {"id": "spotify:3234234234", "size": [2, 5]}, + "assets": { + "large_image": "34234234234243", + "large_text": "LARGE TEXT", + "small_image": "3939393", + "small_text": "small text", + }, + "secrets": {"join": "who's a good secret?", "spectate": "I'm a good secret", "match": "No."}, + "instance": True, + "flags": 3, + } + + @pytest.fixture() + def member_presence_payload(self, user_payload, presence_activity_payload): + return { + "user": user_payload, + "roles": ["49494949"], + "game": presence_activity_payload, + "guild_id": "44004040", + "status": "dnd", + "activities": [presence_activity_payload], + "client_status": {"desktop": "online", "mobile": "idle", "web": "dnd"}, + "premium_since": "2015-04-26T06:26:56.936000+00:00", + "nick": "Nick", + } + + def test_deserialize_member_presence( + self, entity_factory_impl, mock_app, member_presence_payload, custom_emoji_payload, user_payload + ): + presence = entity_factory_impl.deserialize_member_presence(member_presence_payload) + assert presence._app is mock_app + # PresenceUser + assert presence.user._app is mock_app + assert presence.user.id == 115590097100865541 + assert presence.user.discriminator == "6127" + assert presence.user.username == "nyaa" + assert presence.user.avatar_hash == "b3b24c6d7cbcdec129d5d537067061a8" + assert presence.user.is_bot is True + assert presence.user.is_system is True + assert presence.user.flags == user_models.UserFlag(131072) + + assert isinstance(presence.user, presence_models.PresenceUser) + assert presence.role_ids == {49494949} + assert presence.guild_id == 44004040 + assert presence.visible_status == presence_models.PresenceStatus.DND + # PresenceActivity + assert len(presence.activities) == 1 + activity = presence.activities[0] + assert activity.name == "an activity" + assert activity.type == presence_models.ActivityType.STREAMING + assert activity.url == "https://69.420.owouwunyaa" + assert activity.created_at == datetime.datetime(2020, 3, 23, 20, 53, 12, 798000, tzinfo=datetime.timezone.utc) + # ActivityTimestamps + assert activity.timestamps.start == datetime.datetime( + 2020, 3, 23, 20, 53, 12, 798000, tzinfo=datetime.timezone.utc + ) + assert activity.timestamps.end == datetime.datetime( + 2033, 5, 18, 3, 29, 52, 798000, tzinfo=datetime.timezone.utc + ) + + assert activity.application_id == 40404040404040 + assert activity.details == "They are doing stuff" + assert activity.state == "STATED" + assert activity.emoji == entity_factory_impl.deserialize_emoji(custom_emoji_payload) + # ActivityParty + assert activity.party.id == "spotify:3234234234" + assert activity.party.current_size == 2 + assert activity.party.max_size == 5 + assert isinstance(activity.party, presence_models.ActivityParty) + # ActivityAssets + assert activity.assets.large_image == "34234234234243" + assert activity.assets.large_text == "LARGE TEXT" + assert activity.assets.small_image == "3939393" + assert activity.assets.small_text == "small text" + assert isinstance(activity.assets, presence_models.ActivityAssets) + # ActivitySecrets + assert activity.secrets.join == "who's a good secret?" + assert activity.secrets.spectate == "I'm a good secret" + assert activity.secrets.match == "No." + assert isinstance(activity.secrets, presence_models.ActivitySecret) + assert activity.is_instance is True + assert activity.flags == presence_models.ActivityFlag(3) + assert isinstance(activity, presence_models.RichActivity) + + # ClientStatus + assert presence.client_status.desktop == presence_models.PresenceStatus.ONLINE + assert presence.client_status.mobile == presence_models.PresenceStatus.IDLE + assert presence.client_status.web == presence_models.PresenceStatus.DND + assert isinstance(presence.client_status, presence_models.ClientStatus) + + assert presence.premium_since == datetime.datetime(2015, 4, 26, 6, 26, 56, 936000, tzinfo=datetime.timezone.utc) + assert presence.nickname == "Nick" + assert isinstance(presence, presence_models.MemberPresence) + + def test_deserialize_member_presence_with_null_fields(self, entity_factory_impl, user_payload): + presence = entity_factory_impl.deserialize_member_presence( + { + "user": {"username": "agent 47", "avatar": None, "discriminator": "4747", "id": "474747474"}, + "roles": [], + "game": None, + "guild_id": "42", + "status": "dnd", + "activities": [], + "client_status": {}, + "premium_since": None, + "nick": None, + } + ) + assert presence.premium_since is None + assert presence.nickname is None + # PresenceUser + assert presence.user.avatar_hash is None + + def test_deserialize_member_presence_with_unset_fields( + self, entity_factory_impl, user_payload, presence_activity_payload + ): + presence = entity_factory_impl.deserialize_member_presence( + { + "user": {"id": "42"}, + "game": presence_activity_payload, + "guild_id": "44004040", + "status": "dnd", + "activities": [], + "client_status": {}, + } + ) + assert presence.premium_since is None + assert presence.nickname is None + assert presence.role_ids is None + # ClientStatus + assert presence.client_status.desktop is presence_models.PresenceStatus.OFFLINE + assert presence.client_status.mobile is presence_models.PresenceStatus.OFFLINE + assert presence.client_status.web is presence_models.PresenceStatus.OFFLINE + # PresenceUser + assert presence.user.id == 42 + assert presence.user.discriminator is undefined.Undefined() + assert presence.user.username is undefined.Undefined() + assert presence.user.avatar_hash is undefined.Undefined() + assert presence.user.is_bot is undefined.Undefined() + assert presence.user.is_system is undefined.Undefined() + assert presence.user.flags is undefined.Undefined() + + def test_deserialize_member_presence_with_unset_activity_fields(self, entity_factory_impl, user_payload): + presence = entity_factory_impl.deserialize_member_presence( + { + "user": user_payload, + "roles": ["49494949"], + "game": None, + "guild_id": "44004040", + "status": "dnd", + "activities": [{"name": "an activity", "type": 1, "created_at": 1584996792798,}], + "client_status": {}, + } + ) + assert len(presence.activities) == 1 + activity = presence.activities[0] + assert activity.url is None + assert activity.timestamps is None + assert activity.application_id is None + assert activity.details is None + assert activity.state is None + assert activity.emoji is None + assert activity.party is None + assert activity.assets is None + assert activity.secrets is None + assert activity.is_instance is None + assert activity.flags is None + + def test_deserialize_member_presence_with_null_activity_fields(self, entity_factory_impl, user_payload): + presence = entity_factory_impl.deserialize_member_presence( + { + "user": user_payload, + "roles": ["49494949"], + "game": None, + "guild_id": "44004040", + "status": "dnd", + "activities": [ + { + "name": "an activity", + "type": 1, + "url": None, + "created_at": 1584996792798, + "timestamps": {"start": 1584996792798, "end": 1999999792798,}, + "application_id": "40404040404040", + "details": None, + "state": None, + "emoji": None, + "party": {"id": "spotify:3234234234", "size": [2, 5]}, + "assets": { + "large_image": "34234234234243", + "large_text": "LARGE TEXT", + "small_image": "3939393", + "small_text": "small text", + }, + "secrets": {"join": "who's a good secret?", "spectate": "I'm a good secret", "match": "No."}, + "instance": True, + "flags": 3, + } + ], + "client_status": {}, + } + ) + assert len(presence.activities) == 1 + activity = presence.activities[0] + assert activity.url is None + assert activity.details is None + assert activity.state is None + assert activity.emoji is None + + def test_deserialize_member_presence_with_unset_activity_sub_fields(self, entity_factory_impl, user_payload): + presence = entity_factory_impl.deserialize_member_presence( + { + "user": user_payload, + "roles": ["49494949"], + "game": None, + "guild_id": "44004040", + "status": "dnd", + "activities": [ + { + "name": "an activity", + "type": 1, + "url": "https://69.420.owouwunyaa", + "created_at": 1584996792798, + "timestamps": {}, + "application_id": "40404040404040", + "details": "They are doing stuff", + "state": "STATED", + "emoji": None, + "party": {}, + "assets": {}, + "secrets": {}, + "instance": True, + "flags": 3, + } + ], + "client_status": {}, + } + ) + activity = presence.activities[0] + # ActivityTimestamps + assert activity.timestamps.start is None + assert activity.timestamps.end is None + # ActivityParty + assert activity.party.id is None + assert activity.party.max_size is None + assert activity.party.current_size is None + # ActivityAssets + assert activity.assets.small_text is None + assert activity.assets.small_image is None + assert activity.assets.large_text is None + assert activity.assets.large_image is None + # ActivitySecrets + assert activity.secrets.join is None + assert activity.secrets.spectate is None + assert activity.secrets.match is None - ######### - # USERS # - ######### + ############### + # USER MODELS # + ############### @pytest.fixture() def user_payload(self): @@ -2256,7 +2323,7 @@ def user_payload(self): "discriminator": "6127", "bot": True, "system": True, - "public_flags": int(users.UserFlag.VERIFIED_BOT_DEVELOPER), + "public_flags": int(user_models.UserFlag.VERIFIED_BOT_DEVELOPER), } def test_deserialize_user(self, entity_factory_impl, mock_app, user_payload): @@ -2268,8 +2335,8 @@ def test_deserialize_user(self, entity_factory_impl, mock_app, user_payload): assert user.discriminator == "6127" assert user.is_bot is True assert user.is_system is True - assert user.flags == users.UserFlag.VERIFIED_BOT_DEVELOPER - assert isinstance(user, users.User) + assert user.flags == user_models.UserFlag.VERIFIED_BOT_DEVELOPER + assert isinstance(user, user_models.User) def test_deserialize_user_with_unset_fields(self, entity_factory_impl, mock_app, user_payload): user = entity_factory_impl.deserialize_user( @@ -2282,7 +2349,7 @@ def test_deserialize_user_with_unset_fields(self, entity_factory_impl, mock_app, ) assert user.is_bot is False assert user.is_system is False - assert user.flags == users.UserFlag.NONE + assert user.flags == user_models.UserFlag.NONE @pytest.fixture() def my_user_payload(self): @@ -2297,8 +2364,8 @@ def my_user_payload(self): "verified": True, "locale": "en-US", "mfa_enabled": True, - "public_flags": int(users.UserFlag.VERIFIED_BOT_DEVELOPER), - "flags": int(users.UserFlag.DISCORD_PARTNER | users.UserFlag.DISCORD_EMPLOYEE), + "public_flags": int(user_models.UserFlag.VERIFIED_BOT_DEVELOPER), + "flags": int(user_models.UserFlag.DISCORD_PARTNER | user_models.UserFlag.DISCORD_EMPLOYEE), "premium_type": 1, } @@ -2315,9 +2382,9 @@ def test_deserialize_my_user(self, entity_factory_impl, mock_app, my_user_payloa assert my_user.locale == "en-US" assert my_user.is_verified is True assert my_user.email == "blahblah@blah.blah" - assert my_user.flags == users.UserFlag.DISCORD_PARTNER | users.UserFlag.DISCORD_EMPLOYEE - assert my_user.premium_type is users.PremiumType.NITRO_CLASSIC - assert isinstance(my_user, users.OwnUser) + assert my_user.flags == user_models.UserFlag.DISCORD_PARTNER | user_models.UserFlag.DISCORD_EMPLOYEE + assert my_user.premium_type is user_models.PremiumType.NITRO_CLASSIC + assert isinstance(my_user, user_models.OwnUser) def test_deserialize_my_user_with_unset_fields(self, entity_factory_impl, mock_app, my_user_payload): my_user = entity_factory_impl.deserialize_my_user( @@ -2328,8 +2395,8 @@ def test_deserialize_my_user_with_unset_fields(self, entity_factory_impl, mock_a "discriminator": "2880", "locale": "en-US", "mfa_enabled": True, - "public_flags": int(users.UserFlag.VERIFIED_BOT_DEVELOPER), - "flags": int(users.UserFlag.DISCORD_PARTNER | users.UserFlag.DISCORD_EMPLOYEE), + "public_flags": int(user_models.UserFlag.VERIFIED_BOT_DEVELOPER), + "flags": int(user_models.UserFlag.DISCORD_PARTNER | user_models.UserFlag.DISCORD_EMPLOYEE), "premium_type": 1, } ) @@ -2338,11 +2405,11 @@ def test_deserialize_my_user_with_unset_fields(self, entity_factory_impl, mock_a assert my_user.is_system is False assert my_user.is_verified is None assert my_user.email is None - assert isinstance(my_user, users.OwnUser) + assert isinstance(my_user, user_models.OwnUser) - ########## - # Voices # - ########## + ################ + # VOICE MODELS # + ################ @pytest.fixture() def voice_state_payload(self, member_payload): @@ -2374,7 +2441,7 @@ def test_deserialize_voice_state(self, entity_factory_impl, mock_app, voice_stat assert voice_state.is_self_muted is True assert voice_state.is_streaming is True assert voice_state.is_suppressed is False - assert isinstance(voice_state, voices.VoiceState) + assert isinstance(voice_state, voice_models.VoiceState) def test_deserialize_voice_state_with_null_and_unset_fields(self, entity_factory_impl): voice_state = entity_factory_impl.deserialize_voice_state( @@ -2406,11 +2473,11 @@ def test_deserialize_voice_region(self, entity_factory_impl, voice_region_payloa assert voice_region.is_optimal_location is False assert voice_region.is_deprecated is True assert voice_region.is_custom is False - assert isinstance(voice_region, voices.VoiceRegion) + assert isinstance(voice_region, voice_models.VoiceRegion) - ############ - # WEBHOOKS # - ############ + ################## + # WEBHOOK MODELS # + ################## @pytest.fixture() def webhook_payload(self, user_payload): @@ -2428,14 +2495,14 @@ def webhook_payload(self, user_payload): def test_deserialize_webhook(self, entity_factory_impl, mock_app, webhook_payload, user_payload): webhook = entity_factory_impl.deserialize_webhook(webhook_payload) assert webhook.id == 1234 - assert webhook.type == webhooks.WebhookType.INCOMING + assert webhook.type == webhook_models.WebhookType.INCOMING assert webhook.guild_id == 123 assert webhook.channel_id == 456 assert webhook.author == entity_factory_impl.deserialize_user(user_payload) assert webhook.name == "hikari webhook" assert webhook.avatar_hash == "bb71f469c158984e265093a81b3397fb" assert webhook.token == "ueoqrialsdfaKJLKfajslkdf" - assert isinstance(webhook, webhooks.Webhook) + assert isinstance(webhook, webhook_models.Webhook) def test_deserialize_webhook_with_null_and_unset_fields(self, entity_factory_impl): webhook = entity_factory_impl.deserialize_webhook( @@ -2446,3 +2513,611 @@ def test_deserialize_webhook_with_null_and_unset_fields(self, entity_factory_imp assert webhook.name is None assert webhook.avatar_hash is None assert webhook.token is None + + ################## + # CHANNEL EVENTS # + ################## + + def test_deserialize_channel_create_event( + self, entity_factory_impl, guild_text_channel_payload, guild_voice_channel_payload, dm_channel_payload + ): + channel_create_event = entity_factory_impl.deserialize_channel_create_event(guild_text_channel_payload) + assert channel_create_event.channel == entity_factory_impl.deserialize_guild_text_channel( + guild_text_channel_payload + ) + assert isinstance(channel_create_event, channel_events.ChannelCreateEvent) + + assert entity_factory_impl.deserialize_channel_create_event( + guild_voice_channel_payload + ).channel == entity_factory_impl.deserialize_guild_voice_channel(guild_voice_channel_payload) + assert entity_factory_impl.deserialize_channel_create_event( + dm_channel_payload + ).channel == entity_factory_impl.deserialize_dm_channel(dm_channel_payload) + + def test_deserialize_channel_update_event( + self, entity_factory_impl, guild_text_channel_payload, guild_voice_channel_payload, dm_channel_payload + ): + channel_update_event = entity_factory_impl.deserialize_channel_update_event(guild_text_channel_payload) + assert channel_update_event.channel == entity_factory_impl.deserialize_guild_text_channel( + guild_text_channel_payload + ) + assert isinstance(channel_update_event, channel_events.ChannelUpdateEvent) + + assert entity_factory_impl.deserialize_channel_update_event( + guild_voice_channel_payload + ).channel == entity_factory_impl.deserialize_guild_voice_channel(guild_voice_channel_payload) + assert entity_factory_impl.deserialize_channel_update_event( + dm_channel_payload + ).channel == entity_factory_impl.deserialize_dm_channel(dm_channel_payload) + + def test_deserialize_channel_delete_event( + self, entity_factory_impl, guild_text_channel_payload, guild_voice_channel_payload, dm_channel_payload + ): + channel_delete_event = entity_factory_impl.deserialize_channel_delete_event(guild_text_channel_payload) + assert channel_delete_event.channel == entity_factory_impl.deserialize_guild_text_channel( + guild_text_channel_payload + ) + assert isinstance(channel_delete_event, channel_events.ChannelDeleteEvent) + + assert entity_factory_impl.deserialize_channel_delete_event( + guild_voice_channel_payload + ).channel == entity_factory_impl.deserialize_guild_voice_channel(guild_voice_channel_payload) + assert entity_factory_impl.deserialize_channel_delete_event( + dm_channel_payload + ).channel == entity_factory_impl.deserialize_dm_channel(dm_channel_payload) + + @pytest.fixture() + def channel_pins_update_payload(self): + return {"channel_id": "123123", "guild_id": "9439494", "last_pin_timestamp": "2020-05-27T15:58:51.545252+00:00"} + + def test_deserialize_channel_pins_update_event(self, entity_factory_impl, mock_app, channel_pins_update_payload): + channel_pins_update = entity_factory_impl.deserialize_channel_pins_update_event(channel_pins_update_payload) + assert channel_pins_update._app is mock_app + assert channel_pins_update.channel_id == 123123 + assert channel_pins_update.guild_id == 9439494 + assert channel_pins_update.last_pin_timestamp == datetime.datetime( + 2020, 5, 27, 15, 58, 51, 545252, tzinfo=datetime.timezone.utc + ) + assert isinstance(channel_pins_update, channel_events.ChannelPinsUpdateEvent) + + def test_deserialize_channel_pins_update_event_with_unset_fields(self, entity_factory_impl, mock_app): + channel_pins_update = entity_factory_impl.deserialize_channel_pins_update_event({"channel_id": "123123"}) + assert channel_pins_update._app is mock_app + assert channel_pins_update.guild_id is None + assert channel_pins_update.last_pin_timestamp is None + + @pytest.fixture() + def webhook_update_payload(self): + return {"guild_id": "123123123", "channel_id": "93493939"} + + def test_deserialize_webhook_update_event(self, entity_factory_impl, mock_app, webhook_update_payload): + webhook_update = entity_factory_impl.deserialize_webhook_update_event(webhook_update_payload) + assert webhook_update._app is mock_app + assert webhook_update.guild_id == 123123123 + assert webhook_update.channel_id == 93493939 + assert isinstance(webhook_update, channel_events.WebhookUpdateEvent) + + @pytest.fixture() + def typing_start_payload(self, member_payload): + return { + "channel_id": "123123", + "guild_id": "4542242", + "user_id": "29292929", + "timestamp": 1591122971, + "member": member_payload, + } + + def test_deserialize_typing_start_event(self, entity_factory_impl, mock_app, typing_start_payload, member_payload): + typing_start = entity_factory_impl.deserialize_typing_start_event(typing_start_payload) + assert typing_start._app is mock_app + assert typing_start.channel_id == 123123 + assert typing_start.guild_id == 4542242 + assert typing_start.user_id == 29292929 + assert typing_start.timestamp == datetime.datetime(2020, 6, 2, 18, 36, 11, tzinfo=datetime.timezone.utc) + assert typing_start.member == entity_factory_impl.deserialize_member(member_payload) + assert isinstance(typing_start, channel_events.TypingStartEvent) + + def test_deserialize_typing_start_event_with_unset_fields(self, entity_factory_impl): + typing_start = entity_factory_impl.deserialize_typing_start_event( + {"channel_id": "123123", "user_id": "4444", "timestamp": 1591122971} + ) + assert typing_start.guild_id is None + assert typing_start.member is None + + def test_deserialize_invite_create_event(self, entity_factory_impl, invite_payload): + invite_create = entity_factory_impl.deserialize_invite_create_event(invite_payload) + assert invite_create.invite == entity_factory_impl.deserialize_invite(invite_payload) + assert isinstance(invite_create, channel_events.InviteCreateEvent) + + @pytest.fixture() + def invite_delete_payload(self): + return {"channel_id": "123123", "guild_id": "93939393", "code": "Heck"} + + def test_deserialize_invite_delete_event(self, entity_factory_impl, mock_app, invite_delete_payload): + invite_delete = entity_factory_impl.deserialize_invite_delete_event(invite_delete_payload) + assert invite_delete._app is mock_app + assert invite_delete.code == "Heck" + assert invite_delete.channel_id == 123123 + assert invite_delete.guild_id == 93939393 + assert isinstance(invite_delete, channel_events.InviteDeleteEvent) + + def test_deserialize_invite_delete_event_with_unset_fields(self, entity_factory_impl): + assert entity_factory_impl.deserialize_invite_delete_event({"code": "OK", "channel_id": "123"}).guild_id is None + + ################ + # GUILD EVENTS # + ################ + + def test_deserialize_guild_create_event(self, entity_factory_impl, guild_payload): + guild_create = entity_factory_impl.deserialize_guild_create_event(guild_payload) + assert guild_create.guild == entity_factory_impl.deserialize_guild(guild_payload) + assert isinstance(guild_create, guild_events.GuildCreateEvent) + + def test_deserialize_guild_update_event(self, entity_factory_impl, guild_payload): + guild_update = entity_factory_impl.deserialize_guild_update_event(guild_payload) + assert guild_update.guild == entity_factory_impl.deserialize_guild(guild_payload) + assert isinstance(guild_update, guild_events.GuildUpdateEvent) + + @pytest.fixture() + def guild_leave_payload(self): + return {"id": "44332211"} + + def test_deserialize_guild_leave_event(self, entity_factory_impl, guild_leave_payload): + guild_leave = entity_factory_impl.deserialize_guild_leave_event(guild_leave_payload) + assert guild_leave.id == 44332211 + assert isinstance(guild_leave, guild_events.GuildLeaveEvent) + + @pytest.fixture() + def guild_unavailable_payload(self): + return {"id": "4123123", "unavailable": True} + + def test_deserialize_guild_unavailable_event(self, entity_factory_impl, mock_app, guild_unavailable_payload): + guild_unavailable = entity_factory_impl.deserialize_guild_unavailable_event(guild_unavailable_payload) + assert guild_unavailable._app is mock_app + assert guild_unavailable.id == 4123123 + assert isinstance(guild_unavailable, guild_events.GuildUnavailableEvent) + + @pytest.fixture() + def guild_ban_payload(self, user_payload): + return {"user": user_payload, "guild_id": "2002020202022"} + + def test_deserialize_guild_ban_add_event(self, entity_factory_impl, mock_app, guild_ban_payload, user_payload): + guild_ban_add = entity_factory_impl.deserialize_guild_ban_add_event(guild_ban_payload) + assert guild_ban_add._app is mock_app + assert guild_ban_add.guild_id == 2002020202022 + assert guild_ban_add.user == entity_factory_impl.deserialize_user(user_payload) + assert isinstance(guild_ban_add, guild_events.GuildBanAddEvent) + + def test_deserialize_guild_ban_remove_event(self, entity_factory_impl, mock_app, guild_ban_payload, user_payload): + guild_ban_add = entity_factory_impl.deserialize_guild_ban_remove_event(guild_ban_payload) + assert guild_ban_add._app is mock_app + assert guild_ban_add.guild_id == 2002020202022 + assert guild_ban_add.user == entity_factory_impl.deserialize_user(user_payload) + assert isinstance(guild_ban_add, guild_events.GuildBanRemoveEvent) + + @pytest.fixture() + def guild_emojis_update_payload(self, known_custom_emoji_payload): + return {"emojis": [known_custom_emoji_payload], "guild_id": "424242"} + + def test_deserialize_guild_emojis_update_event( + self, entity_factory_impl, mock_app, guild_emojis_update_payload, known_custom_emoji_payload + ): + guild_emoji_update = entity_factory_impl.deserialize_guild_emojis_update_event(guild_emojis_update_payload) + assert guild_emoji_update._app is mock_app + assert guild_emoji_update.guild_id == 424242 + assert guild_emoji_update.emojis == { + 12345: entity_factory_impl.deserialize_known_custom_emoji(known_custom_emoji_payload) + } + assert isinstance(guild_emoji_update, guild_events.GuildEmojisUpdateEvent) + + @pytest.fixture() + def guild_integrations_update_payload(self): + return {"guild_id": "439399393"} + + def test_deserialize_guild_integrations_update_event( + self, entity_factory_impl, mock_app, guild_integrations_update_payload + ): + guild_integrations_update = entity_factory_impl.deserialize_guild_integrations_update_event( + guild_integrations_update_payload + ) + assert guild_integrations_update._app is mock_app + assert guild_integrations_update.guild_id == 439399393 + assert isinstance(guild_integrations_update, guild_events.GuildIntegrationsUpdateEvent) + + @pytest.fixture() + def guild_member_add_payload(self, member_payload): + return {**member_payload, "guild_id": "20202020202020"} + + def test_deserialize_guild_member_add_event( + self, entity_factory_impl, mock_app, guild_member_add_payload, member_payload + ): + guild_member_add = entity_factory_impl.deserialize_guild_member_add_event(guild_member_add_payload) + assert guild_member_add._app is mock_app + assert guild_member_add.guild_id == 20202020202020 + assert guild_member_add.member == entity_factory_impl.deserialize_member(member_payload) + assert isinstance(guild_member_add, guild_events.GuildMemberAddEvent) + + def test_deserialize_guild_member_update_event(self, entity_factory_impl, member_payload): + member_update_event = entity_factory_impl.deserialize_guild_member_update_event(member_payload) + assert member_update_event.member == entity_factory_impl.deserialize_member(member_payload) + assert isinstance(member_update_event, guild_events.GuildMemberUpdateEvent) + + @pytest.fixture() + def guild_member_remove_payload(self, user_payload): + return {"guild_id": "20202020", "user": user_payload} + + def test_deserialize_guild_member_remove_event( + self, entity_factory_impl, mock_app, guild_member_remove_payload, user_payload + ): + guild_member_remove = entity_factory_impl.deserialize_guild_member_remove_event(guild_member_remove_payload) + assert guild_member_remove._app is mock_app + assert guild_member_remove.guild_id == 20202020 + assert guild_member_remove.user == entity_factory_impl.deserialize_user(user_payload) + assert isinstance(guild_member_remove, guild_events.GuildMemberRemoveEvent) + + @pytest.fixture() + def guild_role_create_update_payload(self, guild_role_payload): + return {"guild_id": "303030300303", "role": guild_role_payload} + + def test_deserialize_guild_role_create_event( + self, entity_factory_impl, mock_app, guild_role_create_update_payload, guild_role_payload + ): + guild_role_create = entity_factory_impl.deserialize_guild_role_create_event(guild_role_create_update_payload) + assert guild_role_create._app is mock_app + assert guild_role_create.guild_id == 303030300303 + assert guild_role_create.role == entity_factory_impl.deserialize_role(guild_role_payload) + assert isinstance(guild_role_create, guild_events.GuildRoleCreateEvent) + + def test_deserialize_guild_role_update_event( + self, entity_factory_impl, mock_app, guild_role_create_update_payload, guild_role_payload + ): + guild_role_create = entity_factory_impl.deserialize_guild_role_update_event(guild_role_create_update_payload) + assert guild_role_create._app is mock_app + assert guild_role_create.guild_id == 303030300303 + assert guild_role_create.role == entity_factory_impl.deserialize_role(guild_role_payload) + assert isinstance(guild_role_create, guild_events.GuildRoleUpdateEvent) + + @pytest.fixture() + def guild_role_delete_payload(self): + return {"guild_id": "93939393939", "role_id": "8383483848484848"} + + def test_deserialize_guild_role_delete_event(self, entity_factory_impl, mock_app, guild_role_delete_payload): + guild_role_delete = entity_factory_impl.deserialize_guild_role_delete_event(guild_role_delete_payload) + assert guild_role_delete._app is mock_app + assert guild_role_delete.guild_id == 93939393939 + assert guild_role_delete.role_id == 8383483848484848 + assert isinstance(guild_role_delete, guild_events.GuildRoleDeleteEvent) + + def test_deserialize_presence_update_event(self, entity_factory_impl, member_presence_payload): + presence_update = entity_factory_impl.deserialize_presence_update_event(member_presence_payload) + assert presence_update.presence == entity_factory_impl.deserialize_member_presence(member_presence_payload) + assert isinstance(presence_update, guild_events.PresenceUpdateEvent) + + ################## + # MESSAGE EVENTS # + ################## + + def test_deserialize_message_create_event(self, entity_factory_impl, message_payload): + message_create = entity_factory_impl.deserialize_message_create_event(message_payload) + assert message_create.message == entity_factory_impl.deserialize_message(message_payload) + assert isinstance(message_create, message_events.MessageCreateEvent) + + def test_deserialize_message_update_event_with_full_payload( + self, + entity_factory_impl, + mock_app, + message_payload, + user_payload, + member_payload, + partial_application_payload, + custom_emoji_payload, + embed_payload, + ): + message_update = entity_factory_impl.deserialize_message_update_event(message_payload) + + assert message_update.message._app is mock_app + assert message_update.message.id == 123 + assert message_update.message.channel_id == 456 + assert message_update.message.guild_id == 678 + assert message_update.message.author == entity_factory_impl.deserialize_user(user_payload) + assert message_update.message.member == entity_factory_impl.deserialize_member( + member_payload, user=message_update.message.author + ) + assert message_update.message.content == "some info" + assert message_update.message.timestamp == datetime.datetime( + 2020, 3, 21, 21, 20, 16, 510000, tzinfo=datetime.timezone.utc + ) + assert message_update.message.edited_timestamp == datetime.datetime( + 2020, 4, 21, 21, 20, 16, 510000, tzinfo=datetime.timezone.utc + ) + assert message_update.message.is_tts is True + assert message_update.message.is_mentioning_everyone is True + assert message_update.message.user_mentions == {5678} + assert message_update.message.role_mentions == {987} + assert message_update.message.channel_mentions == {456} + # Attachment + assert len(message_update.message.attachments) == 1 + attachment = message_update.message.attachments[0] + assert attachment.id == 690922406474154014 + assert attachment.filename == "IMG.jpg" + assert attachment.size == 660521 + assert attachment.url == "https://somewhere.com/attachments/123/456/IMG.jpg" + assert attachment.proxy_url == "https://media.somewhere.com/attachments/123/456/IMG.jpg" + assert attachment.width == 1844 + assert attachment.height == 2638 + assert isinstance(attachment, message_models.Attachment) + + assert message_update.message.embeds == [entity_factory_impl.deserialize_embed(embed_payload)] + # Reaction + reaction = message_update.message.reactions[0] + assert reaction.count == 100 + assert reaction.is_reacted_by_me is True + assert reaction.emoji == entity_factory_impl.deserialize_emoji(custom_emoji_payload) + assert isinstance(reaction, message_models.Reaction) + + assert message_update.message.is_pinned is True + assert message_update.message.webhook_id == 1234 + assert message_update.message.type == message_models.MessageType.DEFAULT + # Activity + assert message_update.message.activity.type == message_models.MessageActivityType.JOIN_REQUEST + assert message_update.message.activity.party_id == "ae488379-351d-4a4f-ad32-2b9b01c91657" + assert isinstance(message_update.message.activity, message_models.MessageActivity) + + assert message_update.message.application == entity_factory_impl.deserialize_application( + partial_application_payload + ) + # MessageCrosspost + assert message_update.message.message_reference._app is mock_app + assert message_update.message.message_reference.id == 306588351130107906 + assert message_update.message.message_reference.channel_id == 278325129692446722 + assert message_update.message.message_reference.guild_id == 278325129692446720 + + assert message_update.message.flags == message_models.MessageFlag.IS_CROSSPOST + assert message_update.message.nonce == "171000788183678976" + assert isinstance(message_update.message, message_events.UpdateMessage) + assert isinstance(message_update, message_events.MessageUpdateEvent) + + def test_deserialize_message_update_event_with_partial_payload(self, entity_factory_impl): + message_update = entity_factory_impl.deserialize_message_update_event({"id": "42424242"}) + + assert message_update.message.id == 42424242 + assert message_update.message.channel_id is undefined.Undefined() + assert message_update.message.guild_id is undefined.Undefined() + assert message_update.message.author is undefined.Undefined() + assert message_update.message.member is undefined.Undefined() + assert message_update.message.content is undefined.Undefined() + assert message_update.message.timestamp is undefined.Undefined() + assert message_update.message.edited_timestamp is undefined.Undefined() + assert message_update.message.is_tts is undefined.Undefined() + assert message_update.message.is_mentioning_everyone is undefined.Undefined() + assert message_update.message.user_mentions is undefined.Undefined() + assert message_update.message.role_mentions is undefined.Undefined() + assert message_update.message.channel_mentions is undefined.Undefined() + assert message_update.message.attachments is undefined.Undefined() + assert message_update.message.embeds is undefined.Undefined() + assert message_update.message.reactions is undefined.Undefined() + assert message_update.message.is_pinned is undefined.Undefined() + assert message_update.message.webhook_id is undefined.Undefined() + assert message_update.message.type is undefined.Undefined() + assert message_update.message.activity is undefined.Undefined() + assert message_update.message.application is undefined.Undefined() + assert message_update.message.message_reference is undefined.Undefined() + assert message_update.message.flags is undefined.Undefined() + assert message_update.message.nonce is undefined.Undefined() + + def test_deserialize_message_update_event_with_null_fields(self, entity_factory_impl): + message_update = entity_factory_impl.deserialize_message_update_event( + {"id": "42424242", "edited_timestamp": None} + ) + assert message_update.message.edited_timestamp is None + + @pytest.fixture() + def message_delete_payload(self): + return {"id": "123123", "channel_id": "9292929", "guild_id": "202020202"} + + def test_deserialize_message_delete_event(self, entity_factory_impl, mock_app, message_delete_payload): + message_delete = entity_factory_impl.deserialize_message_delete_event(message_delete_payload) + assert message_delete._app is mock_app + assert message_delete.message_id == 123123 + assert message_delete.channel_id == 9292929 + assert message_delete.guild_id == 202020202 + assert isinstance(message_delete, message_events.MessageDeleteEvent) + + @pytest.fixture() + def message_delete_bulk_payload(self): + return {"ids": ["123123", "9349299"], "channel_id": "92392929", "guild_id": "92929292"} + + def test_deserialize_message_delete_bulk_event(self, entity_factory_impl, mock_app, message_delete_bulk_payload): + message_delete_bulk = entity_factory_impl.deserialize_message_delete_bulk_event(message_delete_bulk_payload) + assert message_delete_bulk._app is mock_app + assert message_delete_bulk.message_ids == {123123, 9349299} + assert message_delete_bulk.channel_id == 92392929 + assert message_delete_bulk.guild_id == 92929292 + assert isinstance(message_delete_bulk, message_events.MessageDeleteBulkEvent) + + @pytest.fixture() + def message_reaction_add_payload(self, member_payload, custom_emoji_payload): + return { + "user_id": "202020", + "channel_id": "23848774", + "message_id": "484848", + "guild_id": "12123", + "member": member_payload, + "emoji": custom_emoji_payload, + } + + def test_deserialize_message_reaction_add_event( + self, entity_factory_impl, mock_app, message_reaction_add_payload, member_payload, custom_emoji_payload + ): + message_reaction_add = entity_factory_impl.deserialize_message_reaction_add_event(message_reaction_add_payload) + assert message_reaction_add._app is mock_app + assert message_reaction_add.user_id == 202020 + assert message_reaction_add.channel_id == 23848774 + assert message_reaction_add.message_id == 484848 + assert message_reaction_add.guild_id == 12123 + assert message_reaction_add.member == entity_factory_impl.deserialize_member(member_payload) + assert message_reaction_add.emoji == entity_factory_impl.deserialize_emoji(custom_emoji_payload) + assert isinstance(message_reaction_add, message_events.MessageReactionAddEvent) + + def test_deserialize_message_reaction_add_event_with_unset_fields( + self, entity_factory_impl, message_reaction_add_payload, custom_emoji_payload + ): + message_reaction_add = entity_factory_impl.deserialize_message_reaction_add_event( + {"user_id": "202020", "channel_id": "23848774", "message_id": "484848", "emoji": custom_emoji_payload} + ) + assert message_reaction_add.guild_id is None + assert message_reaction_add.member is None + + @pytest.fixture() + def reaction_remove_payload(self, custom_emoji_payload): + return { + "user_id": "945939389393939393", + "channel_id": "92903893923", + "message_id": "222222222", + "guild_id": "123123", + "emoji": custom_emoji_payload, + } + + def test_deserialize_message_reaction_remove_event( + self, entity_factory_impl, mock_app, reaction_remove_payload, custom_emoji_payload + ): + reaction_remove = entity_factory_impl.deserialize_message_reaction_remove_event(reaction_remove_payload) + assert reaction_remove._app is mock_app + assert reaction_remove.user_id == 945939389393939393 + assert reaction_remove.channel_id == 92903893923 + assert reaction_remove.message_id == 222222222 + assert reaction_remove.guild_id == 123123 + assert reaction_remove.emoji == entity_factory_impl.deserialize_emoji(custom_emoji_payload) + assert isinstance(reaction_remove, message_events.MessageReactionRemoveEvent) + + def test_deserialize_message_reaction_remove_event_with_unset_fields( + self, entity_factory_impl, custom_emoji_payload + ): + reaction_remove = entity_factory_impl.deserialize_message_reaction_remove_event( + { + "user_id": "945939389393939393", + "channel_id": "92903893923", + "message_id": "222222222", + "emoji": custom_emoji_payload, + } + ) + assert reaction_remove.guild_id is None + + @pytest.fixture() + def message_reaction_remove_all_payload(self, custom_emoji_payload): + return {"channel_id": "222293844884", "message_id": "93493939", "guild_id": "939393939390"} + + def test_deserialize_message_reaction_remove_all_event( + self, entity_factory_impl, mock_app, message_reaction_remove_all_payload + ): + message_reaction_remove_all = entity_factory_impl.deserialize_message_reaction_remove_all_event( + message_reaction_remove_all_payload + ) + assert message_reaction_remove_all._app is mock_app + assert message_reaction_remove_all.channel_id == 222293844884 + assert message_reaction_remove_all.message_id == 93493939 + assert message_reaction_remove_all.guild_id == 939393939390 + assert isinstance(message_reaction_remove_all, message_events.MessageReactionRemoveAllEvent) + + def test_deserialize_message_reaction_remove_all_event_with_unset_fields(self, entity_factory_impl, mock_app): + message_reaction_remove_all = entity_factory_impl.deserialize_message_reaction_remove_all_event( + {"channel_id": "222293844884", "message_id": "93493939"} + ) + assert message_reaction_remove_all.guild_id is None + + @pytest.fixture() + def message_reaction_remove_emoji_payload(self, custom_emoji_payload): + return { + "channel_id": "123123", + "guild_id": "495485494945", + "message_id": "93999328484", + "emoji": custom_emoji_payload, + } + + def test_deserialize_message_reaction_remove_emoji_event( + self, entity_factory_impl, mock_app, message_reaction_remove_emoji_payload, custom_emoji_payload + ): + message_reaction_remove_emoji = entity_factory_impl.deserialize_message_reaction_remove_emoji_event( + message_reaction_remove_emoji_payload + ) + assert message_reaction_remove_emoji._app is mock_app + assert message_reaction_remove_emoji.channel_id == 123123 + assert message_reaction_remove_emoji.guild_id == 495485494945 + assert message_reaction_remove_emoji.message_id == 93999328484 + assert isinstance(message_reaction_remove_emoji, message_events.MessageReactionRemoveEmojiEvent) + + def test_deserialize_message_reaction_remove_emoji_event_with_unset_fields( + self, entity_factory_impl, mock_app, custom_emoji_payload + ): + message_reaction_remove_emoji = entity_factory_impl.deserialize_message_reaction_remove_emoji_event( + {"channel_id": "123123", "message_id": "93999328484", "emoji": custom_emoji_payload} + ) + assert message_reaction_remove_emoji.guild_id is None + + ################ + # OTHER EVENTS # + ################ + + @pytest.fixture() + def ready_payload(self, my_user_payload, guild_unavailable_payload): + return { + "v": 6, + "user": my_user_payload, + "private_channels": [], + "guilds": [guild_unavailable_payload], + "session_id": "mlksdfoijpsdfioprewi09u43rw", + "shard": [0, 1], + } + + def test_deserialize_ready_event( + self, entity_factory_impl, ready_payload, my_user_payload, guild_unavailable_payload + ): + ready_event = entity_factory_impl.deserialize_ready_event(ready_payload) + assert ready_event.gateway_version == 6 + assert ready_event.my_user == entity_factory_impl.deserialize_my_user(my_user_payload) + assert ready_event.unavailable_guilds == { + 4123123: entity_factory_impl.deserialize_unavailable_guild(guild_unavailable_payload) + } + assert ready_event.session_id == "mlksdfoijpsdfioprewi09u43rw" + assert ready_event.shard_id == 0 + assert ready_event.shard_count == 1 + assert isinstance(ready_event, other_events.ReadyEvent) + + def test_deserialize_ready_event_with_unset_fields( + self, entity_factory_impl, ready_payload, my_user_payload, guild_unavailable_payload + ): + ready_event = entity_factory_impl.deserialize_ready_event( + { + "v": 6, + "user": my_user_payload, + "private_channels": [], + "guilds": [], + "session_id": "mlksdfoijpsdfioprewi09u43rw", + } + ) + assert ready_event.shard_id is None + assert ready_event.shard_count is None + + def test_deserialize_my_user_update_event(self, entity_factory_impl, my_user_payload): + my_user_update = entity_factory_impl.deserialize_my_user_update_event(my_user_payload) + assert my_user_update.my_user == entity_factory_impl.deserialize_my_user(my_user_payload) + assert isinstance(my_user_update, other_events.MyUserUpdateEvent) + + ################ + # VOICE EVENTS # + ################ + + def test_deserialize_voice_state_update_event(self, entity_factory_impl, voice_state_payload): + voice_state_update = entity_factory_impl.deserialize_voice_state_update_event(voice_state_payload) + assert voice_state_update.state == entity_factory_impl.deserialize_voice_state(voice_state_payload) + assert isinstance(voice_state_update, voice_events.VoiceStateUpdateEvent) + + @pytest.fixture() + def voice_server_update_payload(self): + return {"token": "9292929iiasdiasdio", "guild_id": "29219929", "endpoint": "wss:>...<"} + + def test_deserialize_voice_server_update_event(self, entity_factory_impl, mock_app, voice_server_update_payload): + voice_server_update = entity_factory_impl.deserialize_voice_server_update_event(voice_server_update_payload) + assert voice_server_update.token == "9292929iiasdiasdio" + assert voice_server_update.guild_id == 29219929 + assert voice_server_update.endpoint == "wss:>...<" From dc6823bc9475caea7a57aea33aabfaf204d6e121 Mon Sep 17 00:00:00 2001 From: Faster Speeding Date: Fri, 5 Jun 2020 11:20:53 +0100 Subject: [PATCH 452/922] Switch to passing through app to __init__ for models and events * Add base guild event class * Remove inconsistent use of auto_attribs --- hikari/api/entity_factory.py | 8 +- hikari/events/channel.py | 14 +- hikari/events/guild.py | 68 ++- hikari/events/message.py | 4 +- hikari/events/other.py | 32 +- hikari/impl/entity_factory.py | 579 +++-------------------- hikari/models/bases.py | 4 +- tests/hikari/impl/test_entity_factory.py | 6 +- 8 files changed, 137 insertions(+), 578 deletions(-) diff --git a/hikari/api/entity_factory.py b/hikari/api/entity_factory.py index 53034a0e5e..92c49dc229 100644 --- a/hikari/api/entity_factory.py +++ b/hikari/api/entity_factory.py @@ -1258,8 +1258,8 @@ def deserialize_ready_event(self, payload: data_binding.JSONObject) -> other_eve """ @abc.abstractmethod - def deserialize_my_user_update_event(self, payload: data_binding.JSONObject) -> other_events.MyUserUpdateEvent: - """Parse a raw payload from Discord into a my user update event object. + def deserialize_own_user_update_event(self, payload: data_binding.JSONObject) -> other_events.OwnUserUpdateEvent: + """Parse a raw payload from Discord into a own user update event object. Parameters ---------- @@ -1268,8 +1268,8 @@ def deserialize_my_user_update_event(self, payload: data_binding.JSONObject) -> Returns ------- - hikari.events.other.MyUserUpdateEvent - The parsed my user update event object. + hikari.events.other.OwnUserUpdateEvent + The parsed own user update event object. """ ################ diff --git a/hikari/events/channel.py b/hikari/events/channel.py index dd4916258e..4b2a28666f 100644 --- a/hikari/events/channel.py +++ b/hikari/events/channel.py @@ -50,16 +50,16 @@ @base_events.requires_intents(intents.Intent.GUILDS) # TODO: this intent doesn't account for DM channels. -@attr.s(auto_attribs=True, eq=False, hash=False, init=False, kw_only=True, slots=True) +@attr.s(eq=False, hash=False, init=False, kw_only=True, slots=True) class BaseChannelEvent(base_events.HikariEvent, abc.ABC): """A base object that Channel events will inherit from.""" - channel: channels.PartialChannel + channel: channels.PartialChannel = attr.ib() """The object of the channel this event involved.""" @base_events.requires_intents(intents.Intent.GUILDS) # TODO: this intent doesn't account for DM channels. -@attr.s(auto_attribs=True, eq=False, hash=False, init=False, kw_only=True, slots=True) +@attr.s(eq=False, hash=False, init=False, kw_only=True, slots=True) class ChannelCreateEvent(BaseChannelEvent): """Represents Channel Create gateway events. @@ -69,13 +69,13 @@ class ChannelCreateEvent(BaseChannelEvent): @base_events.requires_intents(intents.Intent.GUILDS) # TODO: this intent doesn't account for DM channels. -@attr.s(auto_attribs=True, eq=False, hash=False, init=False, kw_only=True, slots=True) +@attr.s(eq=False, hash=False, init=False, kw_only=True, slots=True) class ChannelUpdateEvent(BaseChannelEvent): """Represents Channel Update gateway events.""" @base_events.requires_intents(intents.Intent.GUILDS) # TODO: this intent doesn't account for DM channels. -@attr.s(auto_attribs=True, eq=False, hash=False, init=False, kw_only=True, slots=True) +@attr.s(eq=False, hash=False, init=False, kw_only=True, slots=True) class ChannelDeleteEvent(BaseChannelEvent): """Represents Channel Delete gateway events.""" @@ -151,11 +151,11 @@ class TypingStartEvent(base_events.HikariEvent, base_models.Entity): @base_events.requires_intents(intents.Intent.GUILD_INVITES) -@attr.s(auto_attribs=True, eq=False, hash=False, init=False, kw_only=True, slots=True) +@attr.s(eq=False, hash=False, init=False, kw_only=True, slots=True) class InviteCreateEvent(base_events.HikariEvent): """Represents a gateway Invite Create event.""" - invite: invites.InviteWithMetadata + invite: invites.InviteWithMetadata = attr.ib() """The object of the invite being created.""" diff --git a/hikari/events/guild.py b/hikari/events/guild.py index d675dfbb61..fb5d816906 100644 --- a/hikari/events/guild.py +++ b/hikari/events/guild.py @@ -21,11 +21,12 @@ from __future__ import annotations __all__ = [ + "GuildEvent", "GuildCreateEvent", "GuildUpdateEvent", "GuildLeaveEvent", "GuildUnavailableEvent", - "BaseGuildBanEvent", + "GuildBanEvent", "GuildBanAddEvent", "GuildBanRemoveEvent", "GuildEmojisUpdateEvent", @@ -57,30 +58,36 @@ @base_events.requires_intents(intents.Intent.GUILDS) -@attr.s(auto_attribs=True, eq=False, hash=False, init=False, kw_only=True, slots=True) -class GuildCreateEvent(base_events.HikariEvent): +@attr.s(eq=False, hash=False, init=False, kw_only=True, slots=True) +class GuildEvent(base_events.HikariEvent): + """A base object that all guild events will inherit from.""" + + +@base_events.requires_intents(intents.Intent.GUILDS) +@attr.s(eq=False, hash=False, init=False, kw_only=True, slots=True) +class GuildCreateEvent(GuildEvent): """Used to represent Guild Create gateway events. Will be received when the bot joins a guild, and when a guild becomes available to a guild (either due to outage or at startup). """ - guild: guilds.Guild + guild: guilds.Guild = attr.ib() """The object of the guild that's being created.""" @base_events.requires_intents(intents.Intent.GUILDS) -@attr.s(auto_attribs=True, eq=False, hash=False, init=False, kw_only=True, slots=True) -class GuildUpdateEvent(base_events.HikariEvent): +@attr.s(eq=False, hash=False, init=False, kw_only=True, slots=True) +class GuildUpdateEvent(GuildEvent): """Used to represent Guild Update gateway events.""" - guild: guilds.Guild + guild: guilds.Guild = attr.ib() """The object of the guild that's being updated.""" @base_events.requires_intents(intents.Intent.GUILDS) @attr.s(eq=False, hash=False, init=False, kw_only=True, slots=True) -class GuildLeaveEvent(base_events.HikariEvent, base_models.Unique): +class GuildLeaveEvent(GuildEvent, base_models.Unique): """Fired when the current user leaves the guild or is kicked/banned from it. !!! note @@ -90,7 +97,7 @@ class GuildLeaveEvent(base_events.HikariEvent, base_models.Unique): @base_events.requires_intents(intents.Intent.GUILDS) @attr.s(eq=False, hash=False, init=False, kw_only=True, slots=True) -class GuildUnavailableEvent(base_events.HikariEvent, base_models.Entity, base_models.Unique): +class GuildUnavailableEvent(GuildEvent, base_models.Entity, base_models.Unique): """Fired when a guild becomes temporarily unavailable due to an outage. !!! note @@ -100,7 +107,7 @@ class GuildUnavailableEvent(base_events.HikariEvent, base_models.Entity, base_mo @base_events.requires_intents(intents.Intent.GUILD_BANS) @attr.s(eq=False, hash=False, init=False, kw_only=True, slots=True) -class BaseGuildBanEvent(base_events.HikariEvent, base_models.Entity, abc.ABC): +class GuildBanEvent(GuildEvent, base_models.Entity, abc.ABC): """A base object that guild ban events will inherit from.""" guild_id: snowflake.Snowflake = attr.ib(repr=True) @@ -112,19 +119,19 @@ class BaseGuildBanEvent(base_events.HikariEvent, base_models.Entity, abc.ABC): @base_events.requires_intents(intents.Intent.GUILD_BANS) @attr.s(eq=False, hash=False, init=False, kw_only=True, slots=True) -class GuildBanAddEvent(BaseGuildBanEvent): +class GuildBanAddEvent(GuildBanEvent): """Used to represent a Guild Ban Add gateway event.""" @base_events.requires_intents(intents.Intent.GUILD_BANS) @attr.s(eq=False, hash=False, init=False, kw_only=True, slots=True) -class GuildBanRemoveEvent(BaseGuildBanEvent): +class GuildBanRemoveEvent(GuildBanEvent): """Used to represent a Guild Ban Remove gateway event.""" @base_events.requires_intents(intents.Intent.GUILD_EMOJIS) @attr.s(eq=False, hash=False, init=False, kw_only=True, slots=True) -class GuildEmojisUpdateEvent(base_events.HikariEvent, base_models.Entity): +class GuildEmojisUpdateEvent(GuildEvent, base_models.Entity): """Represents a Guild Emoji Update gateway event.""" guild_id: snowflake.Snowflake = attr.ib() @@ -136,7 +143,7 @@ class GuildEmojisUpdateEvent(base_events.HikariEvent, base_models.Entity): @base_events.requires_intents(intents.Intent.GUILD_INTEGRATIONS) @attr.s(eq=False, hash=False, init=False, kw_only=True, slots=True) -class GuildIntegrationsUpdateEvent(base_events.HikariEvent, base_models.Entity): +class GuildIntegrationsUpdateEvent(GuildEvent, base_models.Entity): """Used to represent Guild Integration Update gateway events.""" guild_id: snowflake.Snowflake = attr.ib(repr=True) @@ -145,7 +152,13 @@ class GuildIntegrationsUpdateEvent(base_events.HikariEvent, base_models.Entity): @base_events.requires_intents(intents.Intent.GUILD_MEMBERS) @attr.s(eq=False, hash=False, init=False, kw_only=True, slots=True) -class GuildMemberAddEvent(base_events.HikariEvent, base_models.Entity): +class GuildMemberEvent(GuildEvent): + """A base class that all guild member events will inherit from.""" + + +@base_events.requires_intents(intents.Intent.GUILD_MEMBERS) +@attr.s(eq=False, hash=False, init=False, kw_only=True, slots=True) +class GuildMemberAddEvent(GuildMemberEvent, base_models.Entity): """Used to represent a Guild Member Add gateway event.""" guild_id: snowflake.Snowflake = attr.ib(repr=True) # TODO: do we want to have guild_id on all members? @@ -156,19 +169,19 @@ class GuildMemberAddEvent(base_events.HikariEvent, base_models.Entity): @base_events.requires_intents(intents.Intent.GUILD_MEMBERS) -@attr.s(auto_attribs=True, eq=False, hash=False, init=False, kw_only=True, slots=True) -class GuildMemberUpdateEvent(base_events.HikariEvent): +@attr.s(eq=False, hash=False, init=False, kw_only=True, slots=True) +class GuildMemberUpdateEvent(GuildMemberEvent): """Used to represent a Guild Member Update gateway event. Sent when a guild member or their inner user object is updated. """ - member: guilds.Member + member: guilds.Member = attr.ib() @base_events.requires_intents(intents.Intent.GUILD_MEMBERS) @attr.s(eq=False, hash=False, init=False, kw_only=True, slots=True) -class GuildMemberRemoveEvent(base_events.HikariEvent, base_models.Entity): +class GuildMemberRemoveEvent(GuildMemberEvent, base_models.Entity): """Used to represent Guild Member Remove gateway events. Sent when a member is kicked, banned or leaves a guild. @@ -182,9 +195,14 @@ class GuildMemberRemoveEvent(base_events.HikariEvent, base_models.Entity): """The object of the user who was removed from this guild.""" +@base_events.requires_intents(intents.Intent.GUILDS) +class GuildRoleEvent(GuildEvent): + """A base class that all guild role events will inherit from.""" + + @base_events.requires_intents(intents.Intent.GUILDS) @attr.s(eq=False, hash=False, init=False, kw_only=True, slots=True) -class GuildRoleCreateEvent(base_events.HikariEvent, base_models.Entity): +class GuildRoleCreateEvent(GuildRoleEvent, base_models.Entity): """Used to represent a Guild Role Create gateway event.""" guild_id: snowflake.Snowflake = attr.ib(repr=True) @@ -196,7 +214,7 @@ class GuildRoleCreateEvent(base_events.HikariEvent, base_models.Entity): @base_events.requires_intents(intents.Intent.GUILDS) @attr.s(eq=False, hash=False, init=False, kw_only=True, slots=True) -class GuildRoleUpdateEvent(base_events.HikariEvent, base_models.Entity): +class GuildRoleUpdateEvent(GuildRoleEvent, base_models.Entity): """Used to represent a Guild Role Create gateway event.""" # TODO: make any event with a guild ID into a custom base event. @@ -210,7 +228,7 @@ class GuildRoleUpdateEvent(base_events.HikariEvent, base_models.Entity): @base_events.requires_intents(intents.Intent.GUILDS) @attr.s(eq=False, hash=False, init=False, kw_only=True, slots=True) -class GuildRoleDeleteEvent(base_events.HikariEvent, base_models.Entity): +class GuildRoleDeleteEvent(GuildRoleEvent, base_models.Entity): """Represents a gateway Guild Role Delete Event.""" guild_id: snowflake.Snowflake = attr.ib(repr=True) @@ -221,12 +239,12 @@ class GuildRoleDeleteEvent(base_events.HikariEvent, base_models.Entity): @base_events.requires_intents(intents.Intent.GUILD_PRESENCES) -@attr.s(auto_attribs=True, eq=False, hash=False, init=False, kw_only=True, slots=True) -class PresenceUpdateEvent(base_events.HikariEvent): +@attr.s(eq=False, hash=False, init=False, kw_only=True, slots=True) +class PresenceUpdateEvent(GuildEvent): """Used to represent Presence Update gateway events. Sent when a guild member changes their presence. """ - presence: presences.MemberPresence + presence: presences.MemberPresence = attr.ib() """The object of the presence being updated.""" diff --git a/hikari/events/message.py b/hikari/events/message.py index dc0f182add..80f23bedd4 100644 --- a/hikari/events/message.py +++ b/hikari/events/message.py @@ -54,11 +54,11 @@ @base_events.requires_intents(intents.Intent.GUILD_MESSAGES, intents.Intent.DIRECT_MESSAGES) -@attr.s(auto_attribs=True, eq=False, hash=False, init=False, kw_only=True, slots=True) +@attr.s(eq=False, hash=False, init=False, kw_only=True, slots=True) class MessageCreateEvent(base_events.HikariEvent): """Used to represent Message Create gateway events.""" - message: messages.Message + message: messages.Message = attr.ib() class UpdateMessage(messages.Message): diff --git a/hikari/events/other.py b/hikari/events/other.py index dfb537f061..ea49d24a78 100644 --- a/hikari/events/other.py +++ b/hikari/events/other.py @@ -48,68 +48,68 @@ # Synthetic event, is not deserialized, and is produced by the dispatcher. @base_events.no_catch() -@attr.s(auto_attribs=True, eq=False, hash=False, init=True, kw_only=True, slots=True) +@attr.s(eq=False, hash=False, init=True, kw_only=True, slots=True) class ExceptionEvent(base_events.HikariEvent): """Descriptor for an exception thrown while processing an event.""" - exception: Exception + exception: Exception = attr.ib() """The exception that was raised.""" - event: base_events.HikariEvent + event: base_events.HikariEvent = attr.ib() """The event that was being invoked when the exception occurred.""" - callback: typing.Callable[[base_events.HikariEvent], typing.Awaitable[None]] + callback: typing.Callable[[base_events.HikariEvent], typing.Awaitable[None]] = attr.ib() """The event that was being invoked when the exception occurred.""" # Synthetic event, is not deserialized -@attr.s(auto_attribs=True, eq=False, hash=False, init=False, kw_only=True, slots=True) +@attr.s(eq=False, hash=False, init=False, kw_only=True, slots=True) class StartingEvent(base_events.HikariEvent): """Event that is fired before the gateway client starts all shards.""" # Synthetic event, is not deserialized -@attr.s(auto_attribs=True, eq=False, hash=False, init=False, kw_only=True, slots=True) +@attr.s(eq=False, hash=False, init=False, kw_only=True, slots=True) class StartedEvent(base_events.HikariEvent): """Event that is fired when the gateway client starts all shards.""" # Synthetic event, is not deserialized -@attr.s(auto_attribs=True, eq=False, hash=False, init=False, kw_only=True, slots=True) +@attr.s(eq=False, hash=False, init=False, kw_only=True, slots=True) class StoppingEvent(base_events.HikariEvent): """Event that is fired when the gateway client is instructed to disconnect all shards.""" # Synthetic event, is not deserialized -@attr.s(auto_attribs=True, eq=False, hash=False, init=False, kw_only=True, slots=True) +@attr.s(eq=False, hash=False, init=False, kw_only=True, slots=True) class StoppedEvent(base_events.HikariEvent): """Event that is fired when the gateway client has finished disconnecting all shards.""" # Synthetic event, is not deserialized -@attr.s(auto_attribs=True, eq=False, hash=False, init=True, kw_only=True, slots=True) +@attr.s(eq=False, hash=False, init=True, kw_only=True, slots=True) class ConnectedEvent(base_events.HikariEvent): """Event invoked each time a shard connects.""" - shard: gateway_client.Gateway + shard: gateway_client.Gateway = attr.ib() """The shard that connected.""" # Synthetic event, is not deserialized -@attr.s(auto_attribs=True, eq=False, hash=False, init=True, kw_only=True, slots=True) +@attr.s(eq=False, hash=False, init=True, kw_only=True, slots=True) class DisconnectedEvent(base_events.HikariEvent): """Event invoked each time a shard disconnects.""" - shard: gateway_client.Gateway + shard: gateway_client.Gateway = attr.ib() """The shard that disconnected.""" # Synthetic event, is not deserialized -@attr.s(auto_attribs=True, eq=False, hash=False, init=True, kw_only=True, slots=True) +@attr.s(eq=False, hash=False, init=True, kw_only=True, slots=True) class ResumedEvent(base_events.HikariEvent): """Represents a gateway Resume event.""" - shard: gateway_client.Gateway + shard: gateway_client.Gateway = attr.ib() """The shard that reconnected.""" @@ -149,11 +149,11 @@ class ReadyEvent(base_events.HikariEvent): @attr.s(eq=False, hash=False, init=False, kw_only=True, slots=True) -class OwnUserUpdateEvent(base_events.HikariEvent, users.OwnUser): +class OwnUserUpdateEvent(base_events.HikariEvent): """Used to represent User Update gateway events. Sent when the current user is updated. """ - my_user: users.MyUser = attr.ib(repr=True) + my_user: users.OwnUser = attr.ib(repr=True) """The updated object of the current application's user.""" diff --git a/hikari/impl/entity_factory.py b/hikari/impl/entity_factory.py index 505b811b2e..86fc5a06d6 100644 --- a/hikari/impl/entity_factory.py +++ b/hikari/impl/entity_factory.py @@ -62,7 +62,7 @@ PartialGuildIntegrationT = typing.TypeVar("PartialGuildIntegrationT", bound=guild_models.PartialIntegration) UserT = typing.TypeVar("UserT", bound=user_models.User) ReactionEventT = typing.TypeVar("ReactionEventT", bound=message_events.BaseMessageReactionEvent) -GuildBanEventT = typing.TypeVar("GuildBanEventT", bound=guild_events.BaseGuildBanEvent) +GuildBanEventT = typing.TypeVar("GuildBanEventT", bound=guild_events.GuildBanEvent) def _deserialize_seconds_timedelta(seconds: typing.Union[str, int]) -> datetime.timedelta: @@ -169,15 +169,14 @@ def deserialize_own_connection(self, payload: data_binding.JSONObject) -> applic return own_connection def deserialize_own_guild(self, payload: data_binding.JSONObject) -> application_models.OwnGuild: - own_guild = self._set_partial_guild_attributes(payload, application_models.OwnGuild()) + own_guild = self._set_partial_guild_attributes(payload, application_models.OwnGuild(self._app)) own_guild.is_owner = bool(payload["owner"]) # noinspection PyArgumentList own_guild.my_permissions = permission_models.Permission(payload["permissions"]) return own_guild def deserialize_application(self, payload: data_binding.JSONObject) -> application_models.Application: - application = application_models.Application() - application.set_app(self._app) + application = application_models.Application(self._app) application.id = snowflake.Snowflake(payload["id"]) application.name = payload["name"] application.description = payload["description"] @@ -190,15 +189,13 @@ def deserialize_application(self, payload: data_binding.JSONObject) -> applicati application.icon_hash = payload.get("icon") if (team_payload := payload.get("team")) is not None: - team = application_models.Team() - team.set_app(self._app) + team = application_models.Team(self._app) team.id = snowflake.Snowflake(team_payload["id"]) team.icon_hash = team_payload["icon"] members = {} for member_payload in team_payload["members"]: - team_member = application_models.TeamMember() - team_member.set_app(self._app) + team_member = application_models.TeamMember(self._app) # noinspection PyArgumentList team_member.membership_state = application_models.TeamMembershipState( member_payload["membership_state"] @@ -231,8 +228,7 @@ def _deserialize_audit_log_change_roles( ) -> typing.Mapping[snowflake.Snowflake, guild_models.PartialRole]: roles = {} for role_payload in payload: - role = guild_models.PartialRole() - role.set_app(self._app) + role = guild_models.PartialRole(self._app) role.id = snowflake.Snowflake(role_payload["id"]) role.name = role_payload["name"] roles[role.id] = role @@ -314,8 +310,7 @@ def deserialize_audit_log(self, payload: data_binding.JSONObject) -> audit_log_m entries = {} for entry_payload in payload["audit_log_entries"]: - entry = audit_log_models.AuditLogEntry() - entry.set_app(self._app) + entry = audit_log_models.AuditLogEntry(self._app) entry.id = snowflake.Snowflake(entry_payload["id"]) if (target_id := entry_payload["target_id"]) is not None: @@ -396,7 +391,6 @@ def serialize_permission_overwrite(self, overwrite: channel_models.PermissionOve def _set_partial_channel_attributes( self, payload: data_binding.JSONObject, channel: PartialChannelT ) -> PartialChannelT: - channel.set_app(self._app) channel.id = snowflake.Snowflake(payload["id"]) channel.name = payload.get("name") # noinspection PyArgumentList @@ -404,7 +398,7 @@ def _set_partial_channel_attributes( return channel def deserialize_partial_channel(self, payload: data_binding.JSONObject) -> channel_models.PartialChannel: - return self._set_partial_channel_attributes(payload, channel_models.PartialChannel()) + return self._set_partial_channel_attributes(payload, channel_models.PartialChannel(self._app)) def _set_dm_channel_attributes(self, payload: data_binding.JSONObject, channel: DMChannelT) -> DMChannelT: channel = self._set_partial_channel_attributes(payload, channel) @@ -419,10 +413,10 @@ def _set_dm_channel_attributes(self, payload: data_binding.JSONObject, channel: return channel def deserialize_dm_channel(self, payload: data_binding.JSONObject) -> channel_models.DMChannel: - return self._set_dm_channel_attributes(payload, channel_models.DMChannel()) + return self._set_dm_channel_attributes(payload, channel_models.DMChannel(self._app)) def deserialize_group_dm_channel(self, payload: data_binding.JSONObject) -> channel_models.GroupDMChannel: - group_dm_channel = self._set_dm_channel_attributes(payload, channel_models.GroupDMChannel()) + group_dm_channel = self._set_dm_channel_attributes(payload, channel_models.GroupDMChannel(self._app)) group_dm_channel.owner_id = snowflake.Snowflake(payload["owner_id"]) group_dm_channel.icon_hash = payload["icon"] group_dm_channel.nicknames = { @@ -452,10 +446,10 @@ def _set_guild_channel_attributes(self, payload: data_binding.JSONObject, channe return channel def deserialize_guild_category(self, payload: data_binding.JSONObject) -> channel_models.GuildCategory: - return self._set_guild_channel_attributes(payload, channel_models.GuildCategory()) + return self._set_guild_channel_attributes(payload, channel_models.GuildCategory(self._app)) def deserialize_guild_text_channel(self, payload: data_binding.JSONObject) -> channel_models.GuildTextChannel: - guild_text_category = self._set_guild_channel_attributes(payload, channel_models.GuildTextChannel()) + guild_text_category = self._set_guild_channel_attributes(payload, channel_models.GuildTextChannel(self._app)) guild_text_category.topic = payload["topic"] if (last_message_id := payload["last_message_id"]) is not None: @@ -471,7 +465,7 @@ def deserialize_guild_text_channel(self, payload: data_binding.JSONObject) -> ch return guild_text_category def deserialize_guild_news_channel(self, payload: data_binding.JSONObject) -> channel_models.GuildNewsChannel: - guild_news_channel = self._set_guild_channel_attributes(payload, channel_models.GuildNewsChannel()) + guild_news_channel = self._set_guild_channel_attributes(payload, channel_models.GuildNewsChannel(self._app)) guild_news_channel.topic = payload["topic"] if (last_message_id := payload["last_message_id"]) is not None: @@ -485,10 +479,10 @@ def deserialize_guild_news_channel(self, payload: data_binding.JSONObject) -> ch return guild_news_channel def deserialize_guild_store_channel(self, payload: data_binding.JSONObject) -> channel_models.GuildStoreChannel: - return self._set_guild_channel_attributes(payload, channel_models.GuildStoreChannel()) + return self._set_guild_channel_attributes(payload, channel_models.GuildStoreChannel(self._app)) def deserialize_guild_voice_channel(self, payload: data_binding.JSONObject) -> channel_models.GuildVoiceChannel: - guild_voice_channel = self._set_guild_channel_attributes(payload, channel_models.GuildVoiceChannel()) + guild_voice_channel = self._set_guild_channel_attributes(payload, channel_models.GuildVoiceChannel(self._app)) guild_voice_channel.bitrate = int(payload["bitrate"]) guild_voice_channel.user_limit = int(payload["user_limit"]) return guild_voice_channel @@ -664,16 +658,14 @@ def deserialize_unicode_emoji(self, payload: data_binding.JSONObject) -> emoji_m return unicode_emoji def deserialize_custom_emoji(self, payload: data_binding.JSONObject) -> emoji_models.CustomEmoji: - custom_emoji = emoji_models.CustomEmoji() - custom_emoji.set_app(self._app) + custom_emoji = emoji_models.CustomEmoji(self._app) custom_emoji.id = snowflake.Snowflake(payload["id"]) custom_emoji.name = payload["name"] custom_emoji.is_animated = payload.get("animated", False) return custom_emoji def deserialize_known_custom_emoji(self, payload: data_binding.JSONObject) -> emoji_models.KnownCustomEmoji: - known_custom_emoji = emoji_models.KnownCustomEmoji() - known_custom_emoji.set_app(self._app) + known_custom_emoji = emoji_models.KnownCustomEmoji(self._app) known_custom_emoji.id = snowflake.Snowflake(payload["id"]) known_custom_emoji.name = payload["name"] known_custom_emoji.is_animated = payload.get("animated", False) @@ -720,8 +712,7 @@ def deserialize_gateway_bot(self, payload: data_binding.JSONObject) -> gateway_m ################ def deserialize_guild_widget(self, payload: data_binding.JSONObject) -> guild_models.GuildWidget: - guild_embed = guild_models.GuildWidget() - guild_embed.set_app(self._app) + guild_embed = guild_models.GuildWidget(self._app) if (channel_id := payload["channel_id"]) is not None: channel_id = snowflake.Snowflake(channel_id) @@ -736,8 +727,7 @@ def deserialize_member( *, user: typing.Union[undefined.Undefined, user_models.User] = undefined.Undefined(), ) -> guild_models.Member: - guild_member = guild_models.Member() - guild_member.set_app(self._app) + guild_member = guild_models.Member(self._app) guild_member.user = user or self.deserialize_user(payload["user"]) guild_member.role_ids = {snowflake.Snowflake(role_id) for role_id in payload["roles"]} guild_member.joined_at = date.iso8601_datetime_string_to_datetime(payload["joined_at"]) @@ -755,8 +745,7 @@ def deserialize_member( return guild_member def deserialize_role(self, payload: data_binding.JSONObject) -> guild_models.Role: - guild_role = guild_models.Role() - guild_role.set_app(self._app) + guild_role = guild_models.Role(self._app) guild_role.id = snowflake.Snowflake(payload["id"]) guild_role.name = payload["name"] guild_role.color = color_models.Color(payload["color"]) @@ -813,13 +802,11 @@ def deserialize_guild_member_ban(self, payload: data_binding.JSONObject) -> guil return guild_member_ban def deserialize_unavailable_guild(self, payload: data_binding.JSONObject) -> guild_models.UnavailableGuild: - unavailable_guild = guild_models.UnavailableGuild() - unavailable_guild.set_app(self._app) + unavailable_guild = guild_models.UnavailableGuild(self._app) unavailable_guild.id = snowflake.Snowflake(payload["id"]) return unavailable_guild def _set_partial_guild_attributes(self, payload: data_binding.JSONObject, guild: PartialGuildT) -> PartialGuildT: - guild.set_app(self._app) guild.id = snowflake.Snowflake(payload["id"]) guild.name = payload["name"] guild.icon_hash = payload["icon"] @@ -836,7 +823,7 @@ def _set_partial_guild_attributes(self, payload: data_binding.JSONObject, guild: return guild def deserialize_guild_preview(self, payload: data_binding.JSONObject) -> guild_models.GuildPreview: - guild_preview = self._set_partial_guild_attributes(payload, guild_models.GuildPreview()) + guild_preview = self._set_partial_guild_attributes(payload, guild_models.GuildPreview(self._app)) guild_preview.splash_hash = payload["splash"] guild_preview.discovery_splash_hash = payload["discovery_splash"] guild_preview.emojis = { @@ -848,7 +835,7 @@ def deserialize_guild_preview(self, payload: data_binding.JSONObject) -> guild_m return guild_preview def deserialize_guild(self, payload: data_binding.JSONObject) -> guild_models.Guild: - guild = self._set_partial_guild_attributes(payload, guild_models.Guild()) + guild = self._set_partial_guild_attributes(payload, guild_models.Guild(self._app)) guild.splash_hash = payload["splash"] guild.discovery_splash_hash = payload["discovery_splash"] guild.owner_id = snowflake.Snowflake(payload["owner_id"]) @@ -971,18 +958,16 @@ def deserialize_guild(self, payload: data_binding.JSONObject) -> guild_models.Gu ################# def deserialize_vanity_url(self, payload: data_binding.JSONObject) -> invite_model.VanityURL: - vanity_url = invite_model.VanityURL() - vanity_url.set_app(self._app) + vanity_url = invite_model.VanityURL(self._app) vanity_url.code = payload["code"] vanity_url.uses = int(payload["uses"]) return vanity_url def _set_invite_attributes(self, payload: data_binding.JSONObject, invite: InviteT) -> InviteT: - invite.set_app(self._app) invite.code = payload["code"] if (guild_payload := payload.get("guild", ...)) is not ...: - guild = self._set_partial_guild_attributes(guild_payload, invite_model.InviteGuild()) + guild = self._set_partial_guild_attributes(guild_payload, invite_model.InviteGuild(self._app)) guild.splash_hash = guild_payload["splash"] guild.banner_hash = guild_payload["banner"] guild.description = guild_payload["description"] @@ -1020,10 +1005,10 @@ def _set_invite_attributes(self, payload: data_binding.JSONObject, invite: Invit return invite def deserialize_invite(self, payload: data_binding.JSONObject) -> invite_model.Invite: - return self._set_invite_attributes(payload, invite_model.Invite()) + return self._set_invite_attributes(payload, invite_model.Invite(self._app)) def deserialize_invite_with_metadata(self, payload: data_binding.JSONObject) -> invite_model.InviteWithMetadata: - invite_with_metadata = self._set_invite_attributes(payload, invite_model.InviteWithMetadata()) + invite_with_metadata = self._set_invite_attributes(payload, invite_model.InviteWithMetadata(self._app)) invite_with_metadata.uses = int(payload["uses"]) invite_with_metadata.max_uses = int(payload["max_uses"]) max_age = payload["max_age"] @@ -1037,8 +1022,7 @@ def deserialize_invite_with_metadata(self, payload: data_binding.JSONObject) -> ################## def deserialize_message(self, payload: data_binding.JSONObject) -> message_models.Message: - message = message_models.Message() - message.set_app(self._app) + message = message_models.Message(self._app) message.id = snowflake.Snowflake(payload["id"]) message.channel_id = snowflake.Snowflake(payload["channel_id"]) message.guild_id = snowflake.Snowflake(payload["guild_id"]) if "guild_id" in payload else None @@ -1102,8 +1086,7 @@ def deserialize_message(self, payload: data_binding.JSONObject) -> message_model message.application = self.deserialize_application(payload["application"]) if "application" in payload else None if (crosspost_payload := payload.get("message_reference", ...)) is not ...: - crosspost = message_models.MessageCrosspost() - crosspost.set_app(self._app) + crosspost = message_models.MessageCrosspost(self._app) crosspost.id = ( snowflake.Snowflake(crosspost_payload["message_id"]) if "message_id" in crosspost_payload else None ) @@ -1125,11 +1108,9 @@ def deserialize_message(self, payload: data_binding.JSONObject) -> message_model ################### def deserialize_member_presence(self, payload: data_binding.JSONObject) -> presence_models.MemberPresence: - guild_member_presence = presence_models.MemberPresence() - guild_member_presence.set_app(self._app) + guild_member_presence = presence_models.MemberPresence(self._app) user_payload = payload["user"] - user = presence_models.PresenceUser() - user.set_app(self._app) + user = presence_models.PresenceUser(self._app) user.id = snowflake.Snowflake(user_payload["id"]) user.discriminator = user_payload["discriminator"] if "discriminator" in user_payload else undefined.Undefined() user.username = user_payload["username"] if "username" in user_payload else undefined.Undefined() @@ -1263,7 +1244,6 @@ def deserialize_member_presence(self, payload: data_binding.JSONObject) -> prese ############### def _set_user_attributes(self, payload: data_binding.JSONObject, user: UserT) -> UserT: - user.set_app(self._app) user.id = snowflake.Snowflake(payload["id"]) user.discriminator = payload["discriminator"] user.username = payload["username"] @@ -1273,7 +1253,7 @@ def _set_user_attributes(self, payload: data_binding.JSONObject, user: UserT) -> return user def deserialize_user(self, payload: data_binding.JSONObject) -> user_models.User: - user = self._set_user_attributes(payload, user_models.User()) + user = self._set_user_attributes(payload, user_models.User(self._app)) # noinspection PyArgumentList user.flags = ( user_models.UserFlag(payload["public_flags"]) if "public_flags" in payload else user_models.UserFlag.NONE @@ -1281,7 +1261,7 @@ def deserialize_user(self, payload: data_binding.JSONObject) -> user_models.User return user def deserialize_my_user(self, payload: data_binding.JSONObject) -> user_models.OwnUser: - my_user = self._set_user_attributes(payload, user_models.OwnUser()) + my_user = self._set_user_attributes(payload, user_models.OwnUser(self._app)) my_user.is_mfa_enabled = payload["mfa_enabled"] my_user.locale = payload.get("locale") my_user.is_verified = payload.get("verified") @@ -1297,8 +1277,7 @@ def deserialize_my_user(self, payload: data_binding.JSONObject) -> user_models.O ################ def deserialize_voice_state(self, payload: data_binding.JSONObject) -> voice_models.VoiceState: - voice_state = voice_models.VoiceState() - voice_state.set_app(self._app) + voice_state = voice_models.VoiceState(self._app) voice_state.guild_id = snowflake.Snowflake(payload["guild_id"]) if "guild_id" in payload else None if (channel_id := payload["channel_id"]) is not None: @@ -1331,7 +1310,7 @@ def deserialize_voice_region(self, payload: data_binding.JSONObject) -> voice_mo ################## def deserialize_webhook(self, payload: data_binding.JSONObject) -> webhook_models.Webhook: - webhook = webhook_models.Webhook() + webhook = webhook_models.Webhook(self._app) webhook.id = snowflake.Snowflake(payload["id"]) # noinspection PyArgumentList webhook.type = webhook_models.WebhookType(payload["type"]) @@ -1348,52 +1327,16 @@ def deserialize_webhook(self, payload: data_binding.JSONObject) -> webhook_model ################## def deserialize_channel_create_event(self, payload: data_binding.JSONObject) -> channel_events.ChannelCreateEvent: - """Parse a raw payload from Discord into a channel create event object. - - Parameters - ---------- - payload : Mapping[str, Any] - The dict payload to parse. - - Returns - ------- - hikari.events.channel.ChannelCreateEvent - The parsed channel create event object. - """ channel_create_event = channel_events.ChannelCreateEvent() channel_create_event.channel = self.deserialize_channel(payload) return channel_create_event def deserialize_channel_update_event(self, payload: data_binding.JSONObject) -> channel_events.ChannelUpdateEvent: - """Parse a raw payload from Discord into a channel update event object. - - Parameters - ---------- - payload : Mapping[str, Any] - The dict payload to parse. - - Returns - ------- - hikari.events.channel.ChannelUpdateEvent - The parsed event object. - """ channel_update_event = channel_events.ChannelUpdateEvent() channel_update_event.channel = self.deserialize_channel(payload) return channel_update_event def deserialize_channel_delete_event(self, payload: data_binding.JSONObject) -> channel_events.ChannelDeleteEvent: - """Parse a raw payload from Discord into a channel delete event object. - - Parameters - ---------- - payload : Mapping[str, Any] - The dict payload to parse. - - Returns - ------- - hikari.events.channel.ChannelDeleteEvent - The parsed channel delete event object. - """ channel_delete_event = channel_events.ChannelDeleteEvent() channel_delete_event.channel = self.deserialize_channel(payload) return channel_delete_event @@ -1401,20 +1344,7 @@ def deserialize_channel_delete_event(self, payload: data_binding.JSONObject) -> def deserialize_channel_pins_update_event( self, payload: data_binding.JSONObject ) -> channel_events.ChannelPinsUpdateEvent: - """Parse a raw payload from Discord into a channel pins update event object. - - Parameters - ---------- - payload : Mapping[str, Any] - The dict payload to parse. - - Returns - ------- - hikari.events.channel.ChannelPinsUpdateEvent - The parsed channel pins update event object. - """ - channel_pins_update = channel_events.ChannelPinsUpdateEvent() - channel_pins_update.set_app(self._app) + channel_pins_update = channel_events.ChannelPinsUpdateEvent(self._app) channel_pins_update.guild_id = snowflake.Snowflake(payload["guild_id"]) if "guild_id" in payload else None channel_pins_update.channel_id = snowflake.Snowflake(payload["channel_id"]) @@ -1426,39 +1356,13 @@ def deserialize_channel_pins_update_event( return channel_pins_update def deserialize_webhook_update_event(self, payload: data_binding.JSONObject) -> channel_events.WebhookUpdateEvent: - """Parse a raw payload from Discord into a webhook update event object. - - Parameters - ---------- - payload : Mapping[str, Any] - The dict payload to parse. - - Returns - ------- - hikari.events.channel.WebhookUpdateEvent - The parsed webhook update event object. - """ - webhook_update = channel_events.WebhookUpdateEvent() - webhook_update.set_app(self._app) + webhook_update = channel_events.WebhookUpdateEvent(self._app) webhook_update.guild_id = snowflake.Snowflake(payload["guild_id"]) webhook_update.channel_id = snowflake.Snowflake(payload["channel_id"]) return webhook_update def deserialize_typing_start_event(self, payload: data_binding.JSONObject) -> channel_events.TypingStartEvent: - """Parse a raw payload from Discord into a typing start event object. - - Parameters - ---------- - payload : Mapping[str, Any] - The dict payload to parse. - - Returns - ------- - hikari.events.channel.TypingStartEvent - The parsed typing start event object. - """ - typing_start = channel_events.TypingStartEvent() - typing_start.set_app(self._app) + typing_start = channel_events.TypingStartEvent(self._app) typing_start.channel_id = snowflake.Snowflake(payload["channel_id"]) typing_start.guild_id = snowflake.Snowflake(payload["guild_id"]) if "guild_id" in payload else None typing_start.user_id = snowflake.Snowflake(payload["user_id"]) @@ -1469,37 +1373,12 @@ def deserialize_typing_start_event(self, payload: data_binding.JSONObject) -> ch return typing_start def deserialize_invite_create_event(self, payload: data_binding.JSONObject) -> channel_events.InviteCreateEvent: - """Parse a raw payload from Discord into an invite create event object. - - Parameters - ---------- - payload : Mapping[str, Any] - The dict payload to parse. - - Returns - ------- - hikari.events.channel.InviteCreateEvent - The parsed invite create event object. - """ invite_create = channel_events.InviteCreateEvent() invite_create.invite = self.deserialize_invite(payload) return invite_create def deserialize_invite_delete_event(self, payload: data_binding.JSONObject) -> channel_events.InviteDeleteEvent: - """Parse a raw payload from Discord into an invite delete event object. - - Parameters - ---------- - payload : Mapping[str, Any] - The dict payload to parse. - - Returns - ------- - hikari.events.channel.InviteDeleteEvent - The parsed invite delete event object. - """ - invite_delete = channel_events.InviteDeleteEvent() - invite_delete.set_app(self._app) + invite_delete = channel_events.InviteDeleteEvent(self._app) invite_delete.code = payload["code"] invite_delete.channel_id = snowflake.Snowflake(payload["channel_id"]) invite_delete.guild_id = snowflake.Snowflake(payload["guild_id"]) if "guild_id" in payload else None @@ -1510,52 +1389,16 @@ def deserialize_invite_delete_event(self, payload: data_binding.JSONObject) -> c ################ def deserialize_guild_create_event(self, payload: data_binding.JSONObject) -> guild_events.GuildCreateEvent: - """Parse a raw payload from Discord into a guild create event object. - - Parameters - ---------- - payload : Mapping[str, Any] - The dict payload to parse. - - Returns - ------- - hikari.events.guild.GuildCreateEvent - The parsed guild create event object. - """ guild_create = guild_events.GuildCreateEvent() guild_create.guild = self.deserialize_guild(payload) return guild_create def deserialize_guild_update_event(self, payload: data_binding.JSONObject) -> guild_events.GuildUpdateEvent: - """Parse a raw payload from Discord into a guild update event object. - - Parameters - ---------- - payload : Mapping[str, Any] - The dict payload to parse. - - Returns - ------- - hikari.events.guild.GuildUpdateEvent - The parsed guild update event object. - """ guild_update = guild_events.GuildUpdateEvent() guild_update.guild = self.deserialize_guild(payload) return guild_update def deserialize_guild_leave_event(self, payload: data_binding.JSONObject) -> guild_events.GuildLeaveEvent: - """Parse a raw payload from Discord into a guild leave event object. - - Parameters - ---------- - payload : Mapping[str, Any] - The dict payload to parse. - - Returns - ------- - hikari.events.guild.GuildLeaveEvent - The parsed guild leave event object. - """ guild_leave = guild_events.GuildLeaveEvent() guild_leave.id = snowflake.Snowflake(payload["id"]) return guild_leave @@ -1563,78 +1406,27 @@ def deserialize_guild_leave_event(self, payload: data_binding.JSONObject) -> gui def deserialize_guild_unavailable_event( self, payload: data_binding.JSONObject ) -> guild_events.GuildUnavailableEvent: - """Parse a raw payload from Discord into a guild unavailable event object. - - Parameters - ---------- - payload : Mapping[str, Any] - The dict payload to parse. - - Returns - ------- - hikari.events. - The parsed guild unavailable event object. - """ - guild_unavailable = guild_events.GuildUnavailableEvent() - guild_unavailable.set_app(self._app) + guild_unavailable = guild_events.GuildUnavailableEvent(self._app) guild_unavailable.id = snowflake.Snowflake(payload["id"]) return guild_unavailable def _set_base_guild_ban_event_fields( self, payload: data_binding.JSONObject, guild_ban: GuildBanEventT ) -> GuildBanEventT: - guild_ban.set_app(self._app) guild_ban.guild_id = snowflake.Snowflake(payload["guild_id"]) guild_ban.user = self.deserialize_user(payload["user"]) return guild_ban def deserialize_guild_ban_add_event(self, payload: data_binding.JSONObject) -> guild_events.GuildBanAddEvent: - """Parse a raw payload from Discord into a guild ban add event object. - - Parameters - ---------- - payload : Mapping[str, Any] - The dict payload to parse. - - Returns - ------- - hikari.events.guild.GuildBanAddEvent - The parsed guild ban add event object. - """ - return self._set_base_guild_ban_event_fields(payload, guild_events.GuildBanAddEvent()) + return self._set_base_guild_ban_event_fields(payload, guild_events.GuildBanAddEvent(self._app)) def deserialize_guild_ban_remove_event(self, payload: data_binding.JSONObject) -> guild_events.GuildBanRemoveEvent: - """Parse a raw payload from Discord into a guild ban remove event object. - - Parameters - ---------- - payload : Mapping[str, Any] - The dict payload to parse. - - Returns - ------- - hikari.events.guild.GuildBanRemoveEvent - The parsed guild ban remove event object. - """ - return self._set_base_guild_ban_event_fields(payload, guild_events.GuildBanRemoveEvent()) + return self._set_base_guild_ban_event_fields(payload, guild_events.GuildBanRemoveEvent(self._app)) def deserialize_guild_emojis_update_event( self, payload: data_binding.JSONObject ) -> guild_events.GuildEmojisUpdateEvent: - """Parse a raw payload from Discord into a guild emojis update event object. - - Parameters - ---------- - payload : Mapping[str, Any] - The dict payload to parse. - - Returns - ------- - hikari.events.guild.GuildEmojisUpdateEvent - The parsed guild emojis update event object. - """ - guild_emojis_update = guild_events.GuildEmojisUpdateEvent() - guild_emojis_update.set_app(self._app) + guild_emojis_update = guild_events.GuildEmojisUpdateEvent(self._app) guild_emojis_update.guild_id = snowflake.Snowflake(payload["guild_id"]) guild_emojis_update.emojis = { snowflake.Snowflake(emoji["id"]): self.deserialize_known_custom_emoji(emoji) for emoji in payload["emojis"] @@ -1644,38 +1436,12 @@ def deserialize_guild_emojis_update_event( def deserialize_guild_integrations_update_event( self, payload: data_binding.JSONObject ) -> guild_events.GuildIntegrationsUpdateEvent: - """Parse a raw payload from Discord into a guilds integrations update event object. - - Parameters - ---------- - payload : Mapping[str, Any] - The dict payload to parse. - - Returns - ------- - hikari.events.guild.GuildIntegrationsUpdateEvent - The parsed guilds integrations update event object. - """ - guild_integrations_update = guild_events.GuildIntegrationsUpdateEvent() - guild_integrations_update.set_app(self._app) + guild_integrations_update = guild_events.GuildIntegrationsUpdateEvent(self._app) guild_integrations_update.guild_id = snowflake.Snowflake(payload["guild_id"]) return guild_integrations_update def deserialize_guild_member_add_event(self, payload: data_binding.JSONObject) -> guild_events.GuildMemberAddEvent: - """Parse a raw payload from Discord into a guild member add event object. - - Parameters - ---------- - payload : Mapping[str, Any] - The dict payload to parse. - - Returns - ------- - hikari.events.guild.GuildMemberAddEvent - The parsed guild member add event object. - """ - guild_member_add = guild_events.GuildMemberAddEvent() - guild_member_add.set_app(self._app) + guild_member_add = guild_events.GuildMemberAddEvent(self._app) guild_member_add.guild_id = snowflake.Snowflake(payload["guild_id"]) guild_member_add.member = self.deserialize_member(payload) return guild_member_add @@ -1683,18 +1449,6 @@ def deserialize_guild_member_add_event(self, payload: data_binding.JSONObject) - def deserialize_guild_member_update_event( self, payload: data_binding.JSONObject ) -> guild_events.GuildMemberUpdateEvent: - """Parse a raw payload from Discord into a guild member update event object. - - Parameters - ---------- - payload : Mapping[str, Any] - The dict payload to parse. - - Returns - ------- - hikari.events.guild.GuildMemberUpdateEvent - The parsed guild member update event object. - """ guild_member_update = guild_events.GuildMemberUpdateEvent() guild_member_update.member = self.deserialize_member(payload) return guild_member_update @@ -1702,20 +1456,7 @@ def deserialize_guild_member_update_event( def deserialize_guild_member_remove_event( self, payload: data_binding.JSONObject ) -> guild_events.GuildMemberRemoveEvent: - """Parse a raw payload from Discord into a guild member remove event object. - - Parameters - ---------- - payload : Mapping[str, Any] - The dict payload to parse. - - Returns - ------- - hikari.events.guild.GuildMemberRemoveEvent - The parsed guild member remove event object. - """ - guild_member_remove = guild_events.GuildMemberRemoveEvent() - guild_member_remove.set_app(self._app) + guild_member_remove = guild_events.GuildMemberRemoveEvent(self._app) guild_member_remove.guild_id = snowflake.Snowflake(payload["guild_id"]) guild_member_remove.user = self.deserialize_user(payload["user"]) return guild_member_remove @@ -1723,20 +1464,7 @@ def deserialize_guild_member_remove_event( def deserialize_guild_role_create_event( self, payload: data_binding.JSONObject ) -> guild_events.GuildRoleCreateEvent: - """Parse a raw payload from Discord into a guild role create event object. - - Parameters - ---------- - payload : Mapping[str, Any] - The dict payload to parse. - - Returns - ------- - hikari.events.guild.GuildRoleCreateEvent - The parsed guild role create event object. - """ - guild_role_create = guild_events.GuildRoleCreateEvent() - guild_role_create.set_app(self._app) + guild_role_create = guild_events.GuildRoleCreateEvent(self._app) guild_role_create.guild_id = snowflake.Snowflake(payload["guild_id"]) guild_role_create.role = self.deserialize_role(payload["role"]) return guild_role_create @@ -1744,20 +1472,7 @@ def deserialize_guild_role_create_event( def deserialize_guild_role_update_event( self, payload: data_binding.JSONObject ) -> guild_events.GuildRoleUpdateEvent: - """Parse a raw payload from Discord into a guild role update event object. - - Parameters - ---------- - payload : Mapping[str, Any] - The dict payload to parse. - - Returns - ------- - hikari.events.guild.GuildRoleUpdateEvent - The parsed guild role update event object. - """ - guild_role_update = guild_events.GuildRoleUpdateEvent() - guild_role_update.set_app(self._app) + guild_role_update = guild_events.GuildRoleUpdateEvent(self._app) guild_role_update.guild_id = snowflake.Snowflake(payload["guild_id"]) guild_role_update.role = self.deserialize_role(payload["role"]) return guild_role_update @@ -1765,37 +1480,12 @@ def deserialize_guild_role_update_event( def deserialize_guild_role_delete_event( self, payload: data_binding.JSONObject ) -> guild_events.GuildRoleDeleteEvent: - """Parse a raw payload from Discord into a guild role delete event object. - - Parameters - ---------- - payload : Mapping[str, Any] - The dict payload to parse. - - Returns - ------- - hikari.events.guild.GuildRoleDeleteEvent - The parsed guild role delete event object. - """ - guild_role_delete = guild_events.GuildRoleDeleteEvent() - guild_role_delete.set_app(self._app) + guild_role_delete = guild_events.GuildRoleDeleteEvent(self._app) guild_role_delete.guild_id = snowflake.Snowflake(payload["guild_id"]) guild_role_delete.role_id = snowflake.Snowflake(payload["role_id"]) return guild_role_delete def deserialize_presence_update_event(self, payload: data_binding.JSONObject) -> guild_events.PresenceUpdateEvent: - """Parse a raw payload from Discord into a presence update event object. - - Parameters - ---------- - payload : Mapping[str, Any] - The dict payload to parse. - - Returns - ------- - hikari.events.guild.PresenceUpdateEvent - The parsed presence update event object. - """ presence_update = guild_events.PresenceUpdateEvent() presence_update.presence = self.deserialize_member_presence(payload) return presence_update @@ -1805,39 +1495,14 @@ def deserialize_presence_update_event(self, payload: data_binding.JSONObject) -> ################## def deserialize_message_create_event(self, payload: data_binding.JSONObject) -> message_events.MessageCreateEvent: - """Parse a raw payload from Discord into a message create event object. - - Parameters - ---------- - payload : Mapping[str, Any] - The dict payload to parse. - - Returns - ------- - hikari.events.message.MessageCreateEvent - The parsed message create event object. - """ message_create = message_events.MessageCreateEvent() message_create.message = self.deserialize_message(payload) return message_create def deserialize_message_update_event(self, payload: data_binding.JSONObject) -> message_events.MessageUpdateEvent: - """Parse a raw payload from Discord into a message update event object. - - Parameters - ---------- - payload : Mapping[str, Any] - The dict payload to parse. - - Returns - ------- - hikari.events.message.MessageUpdateEvent - The parsed message update event object. - """ message_update = message_events.MessageUpdateEvent() - updated_message = message_events.UpdateMessage() - updated_message.set_app(self._app) + updated_message = message_events.UpdateMessage(self._app) updated_message.id = snowflake.Snowflake(payload["id"]) updated_message.channel_id = ( snowflake.Snowflake(payload["channel_id"]) if "channel_id" in payload else undefined.Undefined() @@ -1944,8 +1609,7 @@ def deserialize_message_update_event(self, payload: data_binding.JSONObject) -> ) if (crosspost_payload := payload.get("message_reference", ...)) is not ...: - crosspost = message_models.MessageCrosspost() - crosspost.set_app(self._app) + crosspost = message_models.MessageCrosspost(self._app) crosspost.id = ( snowflake.Snowflake(crosspost_payload["message_id"]) if "message_id" in crosspost_payload else None ) @@ -1967,20 +1631,7 @@ def deserialize_message_update_event(self, payload: data_binding.JSONObject) -> return message_update def deserialize_message_delete_event(self, payload: data_binding.JSONObject) -> message_events.MessageDeleteEvent: - """Parse a raw payload from Discord into a message delete event object. - - Parameters - ---------- - payload : Mapping[str, Any] - The dict payload to parse. - - Returns - ------- - hikari.events.message.MessageDeleteEvent - The parsed message delete event object. - """ - message_delete = message_events.MessageDeleteEvent() - message_delete.set_app(self._app) + message_delete = message_events.MessageDeleteEvent(self._app) message_delete.channel_id = snowflake.Snowflake(payload["channel_id"]) message_delete.guild_id = snowflake.Snowflake(payload["guild_id"]) if "guild_id" in payload else None message_delete.message_id = snowflake.Snowflake(payload["id"]) @@ -1989,20 +1640,7 @@ def deserialize_message_delete_event(self, payload: data_binding.JSONObject) -> def deserialize_message_delete_bulk_event( self, payload: data_binding.JSONObject ) -> message_events.MessageDeleteBulkEvent: - """Parse a raw payload from Discord into a message delete bulk event object. - - Parameters - ---------- - payload : Mapping[str, Any] - The dict payload to parse. - - Returns - ------- - hikari.events.message.MessageDeleteBulkEvent - The parsed message delete bulk event object. - """ - message_delete_bulk = message_events.MessageDeleteBulkEvent() - message_delete_bulk.set_app(self._app) + message_delete_bulk = message_events.MessageDeleteBulkEvent(self._app) message_delete_bulk.channel_id = snowflake.Snowflake(payload["channel_id"]) message_delete_bulk.guild_id = snowflake.Snowflake(payload["guild_id"]) if "guild_id" in payload else None message_delete_bulk.message_ids = {snowflake.Snowflake(message_id) for message_id in payload["ids"]} @@ -2011,7 +1649,6 @@ def deserialize_message_delete_bulk_event( def _set_base_message_reaction_fields( self, payload: data_binding.JSONObject, reaction_event: ReactionEventT ) -> ReactionEventT: - reaction_event.set_app(self._app) reaction_event.channel_id = snowflake.Snowflake(payload["channel_id"]) reaction_event.message_id = snowflake.Snowflake(payload["message_id"]) reaction_event.guild_id = snowflake.Snowflake(payload["guild_id"]) if "guild_id" in payload else None @@ -2020,19 +1657,9 @@ def _set_base_message_reaction_fields( def deserialize_message_reaction_add_event( self, payload: data_binding.JSONObject ) -> message_events.MessageReactionAddEvent: - """Parse a raw payload from Discord into a message reaction add event object. - - Parameters - ---------- - payload : Mapping[str, Any] - The dict payload to parse. - - Returns - ------- - hikari.events.message.MessageReactionAddEvent - The parsed message reaction add event object. - """ - message_reaction_add = self._set_base_message_reaction_fields(payload, message_events.MessageReactionAddEvent()) + message_reaction_add = self._set_base_message_reaction_fields( + payload, message_events.MessageReactionAddEvent(self._app) + ) message_reaction_add.user_id = snowflake.Snowflake(payload["user_id"]) message_reaction_add.member = self.deserialize_member(payload["member"]) if "member" in payload else None message_reaction_add.emoji = self.deserialize_emoji(payload["emoji"]) @@ -2041,20 +1668,8 @@ def deserialize_message_reaction_add_event( def deserialize_message_reaction_remove_event( self, payload: data_binding.JSONObject ) -> message_events.MessageReactionRemoveEvent: - """Parse a raw payload from Discord into a message reaction remove event object. - - Parameters - ---------- - payload : Mapping[str, Any] - The dict payload to parse. - - Returns - ------- - hikari.events.message.MessageReactionRemoveEvent - The parsed message reaction remove event object. - """ message_reaction_remove = self._set_base_message_reaction_fields( - payload, message_events.MessageReactionRemoveEvent() + payload, message_events.MessageReactionRemoveEvent(self._app) ) message_reaction_remove.user_id = snowflake.Snowflake(payload["user_id"]) message_reaction_remove.emoji = self.deserialize_emoji(payload["emoji"]) @@ -2063,37 +1678,13 @@ def deserialize_message_reaction_remove_event( def deserialize_message_reaction_remove_all_event( self, payload: data_binding.JSONObject ) -> message_events.MessageReactionRemoveAllEvent: - """Parse a raw payload from Discord into a message reaction remove all event object. - - Parameters - ---------- - payload : Mapping[str, Any] - The dict payload to parse. - - Returns - ------- - hikari.events.message.MessageReactionRemoveAllEvent - The parsed message reaction remove all event object. - """ - return self._set_base_message_reaction_fields(payload, message_events.MessageReactionRemoveAllEvent()) + return self._set_base_message_reaction_fields(payload, message_events.MessageReactionRemoveAllEvent(self._app)) def deserialize_message_reaction_remove_emoji_event( self, payload: data_binding.JSONObject ) -> message_events.MessageReactionRemoveEmojiEvent: - """Parse a raw payload from Discord into a message reaction remove emoji event object. - - Parameters - ---------- - payload : Mapping[str, Any] - The dict payload to parse. - - Returns - ------- - hikari.events.message.MessageReactionRemoveEmojiEvent - The parsed message reaction remove emoji event object. - """ message_reaction_remove_emoji = self._set_base_message_reaction_fields( - payload, message_events.MessageReactionRemoveEmojiEvent() + payload, message_events.MessageReactionRemoveEmojiEvent(self._app) ) message_reaction_remove_emoji.emoji = self.deserialize_emoji(payload["emoji"]) return message_reaction_remove_emoji @@ -2103,18 +1694,6 @@ def deserialize_message_reaction_remove_emoji_event( ################ def deserialize_ready_event(self, payload: data_binding.JSONObject) -> other_events.ReadyEvent: - """Parse a raw payload from Discord into a ready event object. - - Parameters - ---------- - payload : Mapping[str, Any] - The dict payload to parse. - - Returns - ------- - hikari.events.other.ReadyEvent - The parsed ready event object. - """ ready_event = other_events.ReadyEvent() ready_event.gateway_version = int(payload["v"]) ready_event.my_user = self.deserialize_my_user(payload["user"]) @@ -2131,20 +1710,8 @@ def deserialize_ready_event(self, payload: data_binding.JSONObject) -> other_eve return ready_event - def deserialize_my_user_update_event(self, payload: data_binding.JSONObject) -> other_events.MyUserUpdateEvent: - """Parse a raw payload from Discord into a my user update event object. - - Parameters - ---------- - payload : Mapping[str, Any] - The dict payload to parse. - - Returns - ------- - hikari.events.other.MyUserUpdateEvent - The parsed my user update event object. - """ - my_user_update = other_events.MyUserUpdateEvent() + def deserialize_own_user_update_event(self, payload: data_binding.JSONObject) -> other_events.OwnUserUpdateEvent: + my_user_update = other_events.OwnUserUpdateEvent() my_user_update.my_user = self.deserialize_my_user(payload) return my_user_update @@ -2155,18 +1722,6 @@ def deserialize_my_user_update_event(self, payload: data_binding.JSONObject) -> def deserialize_voice_state_update_event( self, payload: data_binding.JSONObject ) -> voice_events.VoiceStateUpdateEvent: - """Parse a raw payload from Discord into a voice state update event object. - - Parameters - ---------- - payload : Mapping[str, Any] - The dict payload to parse. - - Returns - ------- - hikari.events.voice.VoiceStateUpdateEvent - The parsed voice state update event object. - """ voice_state_update = voice_events.VoiceStateUpdateEvent() voice_state_update.state = self.deserialize_voice_state(payload) return voice_state_update @@ -2174,18 +1729,6 @@ def deserialize_voice_state_update_event( def deserialize_voice_server_update_event( self, payload: data_binding.JSONObject ) -> voice_events.VoiceServerUpdateEvent: - """Parse a raw payload from Discord into a voice server update event object. - - Parameters - ---------- - payload : Mapping[str, Any] - The dict payload to parse. - - Returns - ------- - hikari.events.voice.VoiceServerUpdateEvent - The parsed voice server update event object. - """ voice_server_update = voice_events.VoiceServerUpdateEvent() voice_server_update.token = payload["token"] voice_server_update.guild_id = snowflake.Snowflake(payload["guild_id"]) diff --git a/hikari/models/bases.py b/hikari/models/bases.py index 02c97d9a01..66e3a80b23 100644 --- a/hikari/models/bases.py +++ b/hikari/models/bases.py @@ -47,9 +47,7 @@ class Entity(abc.ABC): _app: typing.Optional[app_.IApp] = attr.ib(default=None, repr=False, eq=False, hash=False) """The client application that models may use for procedures.""" - def set_app(self, app: app_.IApp) -> None: - if hasattr(self, "_app"): - raise AttributeError("Protected attribute '_app' cannot be overwritten.") + def __init__(self, app: app_.IApp) -> None: self._app = app diff --git a/tests/hikari/impl/test_entity_factory.py b/tests/hikari/impl/test_entity_factory.py index dfa6770ac9..175b651434 100644 --- a/tests/hikari/impl/test_entity_factory.py +++ b/tests/hikari/impl/test_entity_factory.py @@ -3098,10 +3098,10 @@ def test_deserialize_ready_event_with_unset_fields( assert ready_event.shard_id is None assert ready_event.shard_count is None - def test_deserialize_my_user_update_event(self, entity_factory_impl, my_user_payload): - my_user_update = entity_factory_impl.deserialize_my_user_update_event(my_user_payload) + def test_deserialize_own_user_update_event(self, entity_factory_impl, my_user_payload): + my_user_update = entity_factory_impl.deserialize_own_user_update_event(my_user_payload) assert my_user_update.my_user == entity_factory_impl.deserialize_my_user(my_user_payload) - assert isinstance(my_user_update, other_events.MyUserUpdateEvent) + assert isinstance(my_user_update, other_events.OwnUserUpdateEvent) ################ # VOICE EVENTS # From 6965774fac24a236de7797ce931a4cde06a2396e Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Fri, 5 Jun 2020 13:34:22 +0100 Subject: [PATCH 453/922] Updated channels documentation. --- hikari/models/channels.py | 148 ++++++++++++++++++++++++++------------ 1 file changed, 104 insertions(+), 44 deletions(-) diff --git a/hikari/models/channels.py b/hikari/models/channels.py index df7ba8afac..8d2601d347 100644 --- a/hikari/models/channels.py +++ b/hikari/models/channels.py @@ -36,6 +36,7 @@ "GuildVoiceChannel", ] +import abc import enum import typing @@ -43,12 +44,12 @@ from hikari.models import bases from hikari.models import permissions +from hikari.models import users + from hikari.utilities import cdn if typing.TYPE_CHECKING: import datetime - - from hikari.models import users from hikari.utilities import snowflake @@ -94,7 +95,30 @@ def __str__(self) -> str: @attr.s(eq=True, hash=True, init=True, kw_only=True, slots=True) class PermissionOverwrite(bases.Unique): - """Represents permission overwrites for a channel or role in a channel.""" + """Represents permission overwrites for a channel or role in a channel. + + You may sometimes need to make instances of this object to add/edit + permission overwrites on channels. + + Example + ------- + Creating a permission overwrite. + + ```py + overwrite = PermissionOverwrite( + type=PermissionOverwriteType.MEMBER, + allow=( + Permissions.VIEW_CHANNEL + | Permissions.READ_MESSAGE_HISTORY + | Permissions.SEND_MESSAGES + ), + deny=( + Permissions.MANAGE_MESSAGES + | Permissions.SPEAK + ), + ) + ``` + """ type: PermissionOverwriteType = attr.ib(converter=PermissionOverwriteType, eq=True, hash=True) """The type of entity this overwrite targets.""" @@ -111,16 +135,12 @@ def unset(self) -> permissions.Permission: return typing.cast(permissions.Permission, (self.allow | self.deny)) -class TextChannel: - # This is a mixin, do not add slotted fields. - __slots__ = () - - @attr.s(eq=True, hash=True, init=False, kw_only=True, slots=True) class PartialChannel(bases.Entity, bases.Unique): - """Represents a channel where we've only received it's basic information. + """Channel representation for cases where further detail is not provided. - This is commonly received in RESTSession responses. + This is commonly received in REST API responses where full information is + not available from Discord. """ name: typing.Optional[str] = attr.ib(eq=False, hash=False, repr=True) @@ -130,15 +150,23 @@ class PartialChannel(bases.Entity, bases.Unique): """The channel's type.""" +class TextChannel(PartialChannel, abc.ABC): + """A channel that can have text messages in it.""" + + # This is a mixin, do not add slotted fields. + __slots__ = () + + @attr.s(eq=True, hash=True, init=False, kw_only=True, slots=True) -class DMChannel(PartialChannel, TextChannel): +class DMChannel(TextChannel): """Represents a DM channel.""" last_message_id: typing.Optional[snowflake.Snowflake] = attr.ib(eq=False, hash=False) """The ID of the last message sent in this channel. - !!! note - This might point to an invalid or deleted message. + !!! warning + This might point to an invalid or deleted message. Do not assume that + this will always be valid. """ recipients: typing.Mapping[snowflake.Snowflake, users.User] = attr.ib( @@ -155,25 +183,29 @@ class GroupDMChannel(DMChannel): """The ID of the owner of the group.""" icon_hash: typing.Optional[str] = attr.ib(eq=False, hash=False) - """The hash of the icon of the group.""" + """The CDN hash of the icon of the group, if an icon is set.""" nicknames: typing.MutableMapping[snowflake.Snowflake, str] = attr.ib(eq=False, hash=False) """A mapping of set nicknames within this group DMs to user IDs.""" application_id: typing.Optional[snowflake.Snowflake] = attr.ib(eq=False, hash=False) - """The ID of the application that created the group DM, if it's a bot based group DM.""" + """The ID of the application that created the group DM. + + If the group DM was not created by a bot, this will be `None`. + """ @property def icon_url(self) -> typing.Optional[str]: """URL for this DM channel's icon, if set.""" return self.format_icon_url() - def format_icon_url(self, *, format_: str = "png", size: int = 4096) -> typing.Optional[str]: + # noinspection PyShadowingBuiltins + def format_icon_url(self, *, format: str = "png", size: int = 4096) -> typing.Optional[str]: """Generate the URL for this group DM's icon, if set. Parameters ---------- - format_ : str + format : str The format to use for this URL, defaults to `png`. Supports `png`, `jpeg`, `jpg` and `webp`. size : int @@ -182,16 +214,16 @@ def format_icon_url(self, *, format_: str = "png", size: int = 4096) -> typing.O Returns ------- - str | None - The string URL. + str or None + The string URL, or `None` if no icon is present. Raises ------ ValueError - If `size` is not a power of two or not between 16 and 4096. + If `size` is not a power of two between 16 and 4096 (inclusive). """ if self.icon_hash: - return cdn.generate_cdn_url("channel-icons", str(self.id), self.icon_hash, format_=format_, size=size) + return cdn.generate_cdn_url("channel-icons", str(self.id), self.icon_hash, format_=format, size=size) return None @@ -202,30 +234,45 @@ class GuildChannel(PartialChannel): guild_id: typing.Optional[snowflake.Snowflake] = attr.ib(eq=False, hash=False, repr=True) """The ID of the guild the channel belongs to. - This will be `None` when received over the gateway in certain events (e.g. - Guild Create). + !!! warning + This will be `None` when received over the gateway in certain events + (e.g Guild Create). """ position: int = attr.ib(eq=False, hash=False) - """The sorting position of the channel.""" + """The sorting position of the channel. + + Higher numbers appear further down the channel list. + """ permission_overwrites: typing.Mapping[snowflake.Snowflake, PermissionOverwrite] = attr.ib(eq=False, hash=False) - """The permission overwrites for the channel.""" + """The permission overwrites for the channel. + + This maps the ID of the entity in the overwrite to the overwrite data. + """ is_nsfw: typing.Optional[bool] = attr.ib(eq=False, hash=False) """Whether the channel is marked as NSFW. - This will be `None` when received over the gateway in certain events (e.g - Guild Create). + !!! warning + This will be `None` when received over the gateway in certain events + (e.g Guild Create). """ parent_id: typing.Optional[snowflake.Snowflake] = attr.ib(eq=False, hash=False, repr=True) - """The ID of the parent category the channel belongs to.""" + """The ID of the parent category the channel belongs to. + + If no parent category is set for the channel, this will be `None`. + """ @attr.s(eq=True, hash=True, init=False, kw_only=True, slots=True) class GuildCategory(GuildChannel): - """Represents a guild category.""" + """Represents a guild category channel. + + These can contain other channels inside, and act as a method for + organisation. + """ @attr.s(eq=True, hash=True, init=False, kw_only=True, slots=True) @@ -238,23 +285,26 @@ class GuildTextChannel(GuildChannel, TextChannel): last_message_id: typing.Optional[snowflake.Snowflake] = attr.ib(eq=False, hash=False) """The ID of the last message sent in this channel. - !!! note - This might point to an invalid or deleted message. + !!! warning + This might point to an invalid or deleted message. Do not assume that + this will always be valid. """ rate_limit_per_user: datetime.timedelta = attr.ib(eq=False, hash=False) """The delay (in seconds) between a user can send a message to this channel. !!! note - Bots, as well as users with `MANAGE_MESSAGES` or `MANAGE_CHANNEL`, - are not affected by this. + Any user that has permissions allowing `MANAGE_MESSAGES`, + `MANAGE_CHANNEL`, `ADMINISTRATOR` will not be limited. Likewise, bots + will not be affected by this rate limit. """ last_pin_timestamp: typing.Optional[datetime.datetime] = attr.ib(eq=False, hash=False) """The timestamp of the last-pinned message. - - This may be `None` in several cases (currently undocumented clearly by - Discord). + + !!! note + This may be `None` in several cases; Discord does not document what + these cases are. Trust no one! """ @@ -267,22 +317,29 @@ class GuildNewsChannel(GuildChannel, TextChannel): last_message_id: typing.Optional[snowflake.Snowflake] = attr.ib(eq=False, hash=False) """The ID of the last message sent in this channel. - - !!! note - This might point to an invalid or deleted message. + + !!! warning + This might point to an invalid or deleted message. Do not assume that + this will always be valid. """ last_pin_timestamp: typing.Optional[datetime.datetime] = attr.ib(eq=False, hash=False) """The timestamp of the last-pinned message. - This may be `None` in several cases (currently undocumented clearly by - Discord). + !!! note + This may be `None` in several cases; Discord does not document what + these cases are. Trust no one! """ @attr.s(eq=True, hash=True, init=False, kw_only=True, slots=True) class GuildStoreChannel(GuildChannel): - """Represents a store channel.""" + """Represents a store channel. + + These were originally used to sell games when Discord had a game store. This + was scrapped at the end of 2019, so these may disappear from the platform + eventually. + """ @attr.s(eq=True, hash=True, init=False, kw_only=True, slots=True) @@ -290,7 +347,10 @@ class GuildVoiceChannel(GuildChannel): """Represents an voice channel.""" bitrate: int = attr.ib(eq=False, hash=False, repr=True) - """The bitrate for the voice channel (in bits).""" + """The bitrate for the voice channel (in bits per second).""" user_limit: int = attr.ib(eq=False, hash=False, repr=True) - """The user limit for the voice channel.""" + """The user limit for the voice channel. + + If this is `0`, then assume no limit. + """ From cafd42b13b99bea4bdef52448d4ce177d3cfd640 Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Fri, 5 Jun 2020 14:33:38 +0100 Subject: [PATCH 454/922] Color documentation fixes. --- hikari/models/colors.py | 185 +++++++++++++++++++++++++--------------- 1 file changed, 115 insertions(+), 70 deletions(-) diff --git a/hikari/models/colors.py b/hikari/models/colors.py index 14a926c9ac..eb3fa96b17 100644 --- a/hikari/models/colors.py +++ b/hikari/models/colors.py @@ -47,60 +47,68 @@ class Color(int): -------- Examples of conversions to given formats include: - >>> c = Color(0xFF051A) - Color(r=0xff, g=0x5, b=0x1a) - >>> hex(c) - 0xff051a - >>> c.hex_code - #FF051A - >>> str(c) - #FF051A - >>> int(c) - 16712986 - >>> c.rgb - (255, 5, 26) - >>> c.rgb_float - (1.0, 0.0196078431372549, 0.10196078431372549) - - Alternatively, if you have an arbitrary input in one of the above formats that you wish to become a color, you can - use `Color.of` function on the class itself to automatically attempt to resolve the color: - - >>> Color.of(0xFF051A) - Color(r=0xff, g=0x5, b=0x1a) - >>> Color.of(16712986) - Color(r=0xff, g=0x5, b=0x1a) - >>> c = Color.of((255, 5, 26)) - Color(r=0xff, g=0x5, b=1xa) - >>> c = Color.of(255, 5, 26) - Color(r=0xff, g=0x5, b=1xa) - >>> c = Color.of([0xFF, 0x5, 0x1a]) - Color(r=0xff, g=0x5, b=1xa) - >>> c = Color.of("#1a2b3c") - Color(r=0x1a, g=0x2b, b=0x3c) - >>> c = Color.of("#1AB") - Color(r=0x11, g=0xaa, b=0xbb) - >>> c = Color.of((1.0, 0.0196078431372549, 0.10196078431372549)) - Color(r=0xff, g=0x5, b=0x1a) - >>> c = Color.of([1.0, 0.0196078431372549, 0.10196078431372549]) - Color(r=0xff, g=0x5, b=0x1a) + ```py + + >>> c = Color(0xFF051A) + Color(r=0xff, g=0x5, b=0x1a) + >>> hex(c) + 0xff051a + >>> c.hex_code + #FF051A + >>> str(c) + #FF051A + >>> int(c) + 16712986 + >>> c.rgb + (255, 5, 26) + >>> c.rgb_float + (1.0, 0.0196078431372549, 0.10196078431372549) + ``` + + Alternatively, if you have an arbitrary input in one of the above formats + that you wish to become a color, you can use `Color.of` on the class itself + to automatically attempt to resolve the color: + + ```py + >>> Color.of(0xFF051A) + Color(r=0xff, g=0x5, b=0x1a) + >>> Color.of(16712986) + Color(r=0xff, g=0x5, b=0x1a) + >>> c = Color.of((255, 5, 26)) + Color(r=0xff, g=0x5, b=1xa) + >>> c = Color.of(255, 5, 26) + Color(r=0xff, g=0x5, b=1xa) + >>> c = Color.of([0xFF, 0x5, 0x1a]) + Color(r=0xff, g=0x5, b=1xa) + >>> c = Color.of("#1a2b3c") + Color(r=0x1a, g=0x2b, b=0x3c) + >>> c = Color.of("#1AB") + Color(r=0x11, g=0xaa, b=0xbb) + >>> c = Color.of((1.0, 0.0196078431372549, 0.10196078431372549)) + Color(r=0xff, g=0x5, b=0x1a) + >>> c = Color.of([1.0, 0.0196078431372549, 0.10196078431372549]) + Color(r=0xff, g=0x5, b=0x1a) + ``` Examples of initialization of Color objects from given formats include: - >>> c = Color(16712986) - Color(r=0xff, g=0x5, b=0x1a) - >>> c = Color.from_rgb(255, 5, 26) - Color(r=0xff, g=0x5, b=1xa) - >>> c = Color.from_hex_code("#1a2b3c") - Color(r=0x1a, g=0x2b, b=0x3c) - >>> c = Color.from_hex_code("#1AB") - Color(r=0x11, g=0xaa, b=0xbb) - >>> c = Color.from_rgb_float(1.0, 0.0196078431372549, 0.10196078431372549) - Color(r=0xff, g=0x5, b=0x1a) + ```py + >>> c = Color(16712986) + Color(r=0xff, g=0x5, b=0x1a) + >>> c = Color.from_rgb(255, 5, 26) + Color(r=0xff, g=0x5, b=1xa) + >>> c = Color.from_hex_code("#1a2b3c") + Color(r=0x1a, g=0x2b, b=0x3c) + >>> c = Color.from_hex_code("#1AB") + Color(r=0x11, g=0xaa, b=0xbb) + >>> c = Color.from_rgb_float(1.0, 0.0196078431372549, 0.10196078431372549) + Color(r=0xff, g=0x5, b=0x1a) + ``` """ __slots__ = () - def __new__(cls, raw_rgb: typing.SupportsInt) -> Color: + def __new__(cls, raw_rgb: typing.Union[int, typing.SupportsInt], /) -> Color: if not 0 <= int(raw_rgb) <= 0xFF_FF_FF: raise ValueError(f"raw_rgb must be in the exclusive range of 0 and {0xFF_FF_FF}") return super(Color, cls).__new__(cls, raw_rgb) @@ -119,6 +127,10 @@ def rgb(self) -> typing.Tuple[int, int, int]: # noqa: D401 Represented as a tuple of R, G, B. Each value is in the range [0, 0xFF]. + + Example + ------- + `(123, 234, 47)` """ return (self >> 16) & 0xFF, (self >> 8) & 0xFF, self & 0xFF @@ -127,6 +139,10 @@ def rgb_float(self) -> typing.Tuple[float, float, float]: """Return the floating-point RGB representation of this Color. Represented as a tuple of R, G, B. Each value is in the range [0, 1]. + + Example + ------- + `(0.1, 0.2, 0.76)` """ r, g, b = self.rgb return r / 0xFF, g / 0xFF, b / 0xFF @@ -137,8 +153,8 @@ def hex_code(self) -> str: This is prepended with a `#` symbol, and will be in upper case. - Examples - -------- + Example + ------- `#1A2B3C` """ return "#" + self.raw_hex_code @@ -147,8 +163,8 @@ def hex_code(self) -> str: def raw_hex_code(self) -> str: """Raw hex code. - Examples - -------- + Example + ------- `1A2B3C` """ components = self.rgb @@ -161,7 +177,7 @@ def is_web_safe(self) -> bool: # noqa: D401 return not (((self & 0xFF0000) % 0x110000) or ((self & 0xFF00) % 0x1100) or ((self & 0xFF) % 0x11)) @classmethod - def from_rgb(cls, red: int, green: int, blue: int) -> Color: + def from_rgb(cls, red: int, green: int, blue: int, /) -> Color: """Convert the given RGB to a `Color` object. Each channel must be withing the range [0, 255] (0x0, 0xFF). @@ -195,19 +211,19 @@ def from_rgb(cls, red: int, green: int, blue: int) -> Color: return cls((red << 16) | (green << 8) | blue) @classmethod - def from_rgb_float(cls, red_f: float, green_f: float, blue_f: float) -> Color: + def from_rgb_float(cls, red: float, green: float, blue: float, /) -> Color: """Convert the given RGB to a `Color` object. - The colorspace represented values have to be within the + The color-space represented values have to be within the range [0, 1]. Parameters ---------- - red_f : float + red : float Red channel. - green_f : float + green : float Green channel. - blue_f : float + blue : float Blue channel. Returns @@ -220,27 +236,28 @@ def from_rgb_float(cls, red_f: float, green_f: float, blue_f: float) -> Color: ValueError If red, green or blue are outside the range [0, 1]. """ - if not 0 <= red_f <= 1: + if not 0 <= red <= 1: raise ValueError("red must be in the inclusive range of 0 and 1.") - if not 0 <= green_f <= 1: + if not 0 <= green <= 1: raise ValueError("green must be in the inclusive range of 0 and 1.") - if not 0 <= blue_f <= 1: + if not 0 <= blue <= 1: raise ValueError("blue must be in the inclusive range of 0 and 1.") # noinspection PyTypeChecker - return cls.from_rgb(int(red_f * 0xFF), int(green_f * 0xFF), int(blue_f * 0xFF)) + return cls.from_rgb(int(red * 0xFF), int(green * 0xFF), int(blue * 0xFF)) @classmethod - def from_hex_code(cls, hex_code: str) -> Color: + def from_hex_code(cls, hex_code: str, /) -> Color: """Convert the given hexadecimal color code to a `Color`. The inputs may be of the following format (case insensitive): - `1a2`, `#1a2`, `0x1a2` (for websafe colors), or + `1a2`, `#1a2`, `0x1a2` (for web-safe colors), or `1a2b3c`, `#1a2b3c`, `0x1a2b3c` (for regular 3-byte color-codes). Parameters ---------- hex_code : str - A hexadecimal color code to parse. + A hexadecimal color code to parse. This may optionally start with + a case insensitive `0x` or `#`. Returns ------- @@ -262,9 +279,9 @@ def from_hex_code(cls, hex_code: str) -> Color: if len(hex_code) == 3: # Web-safe - components = (int(c, 16) for c in hex_code) + r, g, b = (c << 4 | c for c in (int(c, 16) for c in hex_code)) # noinspection PyTypeChecker - return cls.from_rgb(*[(c << 4 | c) for c in components]) + return cls.from_rgb(r, g, b) if len(hex_code) == 6: return cls.from_rgb(int(hex_code[:2], 16), int(hex_code[2:4], 16), int(hex_code[4:6], 16)) @@ -272,7 +289,7 @@ def from_hex_code(cls, hex_code: str) -> Color: raise ValueError("Color code is invalid length. Must be 3 or 6 digits") @classmethod - def from_int(cls, i: typing.SupportsInt) -> Color: + def from_int(cls, i: typing.SupportsInt, /) -> Color: """Convert the given `typing.SupportsInt` to a `Color`. Parameters @@ -326,10 +343,36 @@ def of( ) -> Color: """Convert the value to a `Color`. + This attempts to determine the correct data format based on the + information provided. + Parameters ---------- values : ColorCompatibleT - A color comapible values. + A color compatible values. + + Examples + -------- + ```py + >>> Color.of(0xFF051A) + Color(r=0xff, g=0x5, b=0x1a) + >>> Color.of(16712986) + Color(r=0xff, g=0x5, b=0x1a) + >>> c = Color.of((255, 5, 26)) + Color(r=0xff, g=0x5, b=1xa) + >>> c = Color.of(255, 5, 26) + Color(r=0xff, g=0x5, b=1xa) + >>> c = Color.of([0xFF, 0x5, 0x1a]) + Color(r=0xff, g=0x5, b=1xa) + >>> c = Color.of("#1a2b3c") + Color(r=0x1a, g=0x2b, b=0x3c) + >>> c = Color.of("#1AB") + Color(r=0x11, g=0xaa, b=0xbb) + >>> c = Color.of((1.0, 0.0196078431372549, 0.10196078431372549)) + Color(r=0xff, g=0x5, b=0x1a) + >>> c = Color.of([1.0, 0.0196078431372549, 0.10196078431372549]) + Color(r=0xff, g=0x5, b=0x1a) + ``` Returns ------- @@ -348,10 +391,12 @@ def of( raise ValueError(f"color must be an RGB triplet if set to a {type(values).__name__} type") if any(isinstance(c, float) for c in values): - return cls.from_rgb_float(*values) # pylint: disable=no-value-for-parameter + r, g, b = values + return cls.from_rgb_float(r, g, b) if all(isinstance(c, int) for c in values): - return cls.from_rgb(*values) # pylint: disable=no-value-for-parameter + r, g, b = values + return cls.from_rgb(r, g, b) if isinstance(values, str): is_start_hash_or_hex_literal = values.casefold().startswith(("#", "0x")) From aa445f5f326d6b43bf305d0bea27a25b0fccfdf8 Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Fri, 5 Jun 2020 15:17:37 +0100 Subject: [PATCH 455/922] Added 'shard' to ReadyEvent, implemented event dispatcher specifics. --- docs/documentation.mako | 24 +---- hikari/api/entity_factory.py | 8 +- hikari/events/other.py | 4 +- hikari/impl/entity_factory.py | 6 +- hikari/impl/event_manager.py | 120 ++++++++++++++++------- hikari/impl/event_manager_core.py | 2 +- hikari/models/channels.py | 26 ++--- hikari/models/guilds.py | 2 +- tests/hikari/impl/test_entity_factory.py | 9 +- 9 files changed, 123 insertions(+), 78 deletions(-) diff --git a/docs/documentation.mako b/docs/documentation.mako index 99d53a11a8..936ab97a2c 100644 --- a/docs/documentation.mako +++ b/docs/documentation.mako @@ -72,7 +72,6 @@ QUAL_NAMESPACE = "namespace" QUAL_PACKAGE = "package" QUAL_PROPERTY = "property" - QUAL_REF = "ref" QUAL_TYPEHINT = "type hint" QUAL_VAR = "var" @@ -97,10 +96,7 @@ if with_prefixes: if isinstance(dobj, pdoc.Function): - if not hide_ref and dobj.module.name != dobj.obj.__module__: - qual = QUAL_REF + " " + dobj.funcdef() - else: - qual = dobj.funcdef() + qual = dobj.funcdef() prefix = "" + qual + "" @@ -116,10 +112,7 @@ prefix = f"{QUAL_VAR}" elif isinstance(dobj, pdoc.Class): - if not hide_ref and dobj.module.name != dobj.obj.__module__: - qual = f"{QUAL_REF}" - else: - qual = "" + qual = "" if issubclass(dobj.obj, type): qual += QUAL_METACLASS @@ -142,12 +135,7 @@ prefix = f"{qual} " elif isinstance(dobj, pdoc.Module): - if dobj.module.name != dobj.obj.__name__: - qual = f"{QUAL_REF} " - else: - qual = "" - - qual += QUAL_PACKAGE if dobj.is_package else QUAL_NAMESPACE if dobj.is_namespace else QUAL_MODULE + qual = QUAL_PACKAGE if dobj.is_package else QUAL_NAMESPACE if dobj.is_namespace else QUAL_MODULE prefix = f"{qual} " else: @@ -660,12 +648,6 @@ and for static type-checkers. -
${QUAL_REF}
-
- Used to flag that an object is defined in a different file, and is just - referred to at the current location. -
-
${QUAL_VAR}
Variable or attribute. diff --git a/hikari/api/entity_factory.py b/hikari/api/entity_factory.py index 92c49dc229..464fc04bc8 100644 --- a/hikari/api/entity_factory.py +++ b/hikari/api/entity_factory.py @@ -47,6 +47,8 @@ from hikari.models import voices as voice_models from hikari.models import webhooks as webhook_models + from hikari.net import gateway + from hikari.utilities import data_binding @@ -1243,11 +1245,15 @@ def deserialize_message_reaction_remove_emoji_event( ################ @abc.abstractmethod - def deserialize_ready_event(self, payload: data_binding.JSONObject) -> other_events.ReadyEvent: + def deserialize_ready_event( + self, shard: gateway.Gateway, payload: data_binding.JSONObject, + ) -> other_events.ReadyEvent: """Parse a raw payload from Discord into a ready event object. Parameters ---------- + shard : hikari.net.gateway.Gateway + The shard that was ready. payload : Mapping[str, Any] The dict payload to parse. diff --git a/hikari/events/other.py b/hikari/events/other.py index ea49d24a78..ad38680249 100644 --- a/hikari/events/other.py +++ b/hikari/events/other.py @@ -104,7 +104,6 @@ class DisconnectedEvent(base_events.HikariEvent): """The shard that disconnected.""" -# Synthetic event, is not deserialized @attr.s(eq=False, hash=False, init=True, kw_only=True, slots=True) class ResumedEvent(base_events.HikariEvent): """Represents a gateway Resume event.""" @@ -120,6 +119,9 @@ class ReadyEvent(base_events.HikariEvent): This is received only when IDENTIFYing with the gateway. """ + shard: gateway_client.Gateway = attr.ib() + """The shard that is ready.""" + gateway_version: int = attr.ib(repr=True) """The gateway version this is currently connected to.""" diff --git a/hikari/impl/entity_factory.py b/hikari/impl/entity_factory.py index 86fc5a06d6..c43e3922ee 100644 --- a/hikari/impl/entity_factory.py +++ b/hikari/impl/entity_factory.py @@ -47,6 +47,7 @@ from hikari.models import users as user_models from hikari.models import voices as voice_models from hikari.models import webhooks as webhook_models +from hikari.net import gateway from hikari.utilities import date from hikari.utilities import snowflake from hikari.utilities import undefined @@ -1693,8 +1694,11 @@ def deserialize_message_reaction_remove_emoji_event( # OTHER EVENTS # ################ - def deserialize_ready_event(self, payload: data_binding.JSONObject) -> other_events.ReadyEvent: + def deserialize_ready_event( + self, shard: gateway.Gateway, payload: data_binding.JSONObject + ) -> other_events.ReadyEvent: ready_event = other_events.ReadyEvent() + ready_event.shard = shard ready_event.gateway_version = int(payload["v"]) ready_event.my_user = self.deserialize_my_user(payload["user"]) ready_event.unavailable_guilds = { diff --git a/hikari/impl/event_manager.py b/hikari/impl/event_manager.py index f843dd6164..1b956bb249 100644 --- a/hikari/impl/event_manager.py +++ b/hikari/impl/event_manager.py @@ -24,6 +24,7 @@ import typing +from hikari.events import other from hikari.impl import event_manager_core if typing.TYPE_CHECKING: @@ -34,124 +35,169 @@ class EventManagerImpl(event_manager_core.EventManagerCore): """Provides event handling logic for Discord events.""" - async def on_connect(self, shard: gateway.Gateway, payload: data_binding.JSONObject) -> None: + async def on_connect(self, shard: gateway.Gateway, _: data_binding.JSONObject) -> None: """Handle connection events. This is a synthetic event produced by the gateway implementation in Hikari. """ + # FIXME: this should be in entity factory + await self.dispatch(other.ConnectedEvent(shard=shard)) - async def on_disconnect(self, shard: gateway.Gateway, payload: data_binding.JSONObject) -> None: + async def on_disconnect(self, shard: gateway.Gateway, _: data_binding.JSONObject) -> None: """Handle disconnection events. This is a synthetic event produced by the gateway implementation in Hikari. """ + # FIXME: this should be in entity factory + await self.dispatch(other.DisconnectedEvent(shard=shard)) async def on_ready(self, shard: gateway.Gateway, payload: data_binding.JSONObject) -> None: """See https://discord.com/developers/docs/topics/gateway#ready for more info.""" + await self.dispatch(self.app.entity_factory.deserialize_ready_event(shard, payload)) - async def on_resumed(self, shard: gateway.Gateway, payload: data_binding.JSONObject) -> None: + async def on_resumed(self, shard: gateway.Gateway, _: data_binding.JSONObject) -> None: """See https://discord.com/developers/docs/topics/gateway#resumed for more info.""" + # FIXME: this should be in entity factory + await self.dispatch(other.ResumedEvent(shard=shard)) - async def on_channel_create(self, shard: gateway.Gateway, payload: data_binding.JSONObject) -> None: + async def on_channel_create(self, _: gateway.Gateway, payload: data_binding.JSONObject) -> None: """See https://discord.com/developers/docs/topics/gateway#channel-create for more info.""" + await self.dispatch(self.app.entity_factory.deserialize_channel_create_event(payload)) - async def on_channel_update(self, shard: gateway.Gateway, payload: data_binding.JSONObject) -> None: + async def on_channel_update(self, _: gateway.Gateway, payload: data_binding.JSONObject) -> None: """See https://discord.com/developers/docs/topics/gateway#channel-update for more info.""" + await self.dispatch(self.app.entity_factory.deserialize_channel_update_event(payload)) - async def on_channel_delete(self, shard: gateway.Gateway, payload: data_binding.JSONObject) -> None: + async def on_channel_delete(self, _: gateway.Gateway, payload: data_binding.JSONObject) -> None: """See https://discord.com/developers/docs/topics/gateway#channel-delete for more info.""" + await self.dispatch(self.app.entity_factory.deserialize_channel_delete_event(payload)) - async def on_channel_pins_update(self, shard: gateway.Gateway, payload: data_binding.JSONObject) -> None: + async def on_channel_pins_update(self, _: gateway.Gateway, payload: data_binding.JSONObject) -> None: """See https://discord.com/developers/docs/topics/gateway#channel-pins-update for more info.""" + await self.dispatch(self.app.entity_factory.deserialize_channel_pins_update_event(payload)) - async def on_guild_create(self, shard: gateway.Gateway, payload: data_binding.JSONObject) -> None: + async def on_guild_create(self, _: gateway.Gateway, payload: data_binding.JSONObject) -> None: """See https://discord.com/developers/docs/topics/gateway#guild-create for more info.""" + await self.dispatch(self.app.entity_factory.deserialize_guild_create_event(payload)) - async def on_guild_update(self, shard: gateway.Gateway, payload: data_binding.JSONObject) -> None: + async def on_guild_update(self, _: gateway.Gateway, payload: data_binding.JSONObject) -> None: """See https://discord.com/developers/docs/topics/gateway#guild-update for more info.""" + await self.dispatch(self.app.entity_factory.deserialize_guild_update_event(payload)) - async def on_guild_delete(self, shard: gateway.Gateway, payload: data_binding.JSONObject) -> None: + async def on_guild_delete(self, _: gateway.Gateway, payload: data_binding.JSONObject) -> None: """See https://discord.com/developers/docs/topics/gateway#guild-delete for more info.""" + if payload.get("unavailable", False): + await self.dispatch(self.app.entity_factory.deserialize_guild_unavailable_event(payload)) + else: + await self.dispatch(self.app.entity_factory.deserialize_guild_leave_event(payload)) - async def on_guild_ban_add(self, shard: gateway.Gateway, payload: data_binding.JSONObject) -> None: + async def on_guild_ban_add(self, _: gateway.Gateway, payload: data_binding.JSONObject) -> None: """See https://discord.com/developers/docs/topics/gateway#guild-ban-add for more info.""" + await self.dispatch(self.app.entity_factory.deserialize_guild_ban_add_event(payload)) - async def on_guild_ban_remove(self, shard: gateway.Gateway, payload: data_binding.JSONObject) -> None: + async def on_guild_ban_remove(self, _: gateway.Gateway, payload: data_binding.JSONObject) -> None: """See https://discord.com/developers/docs/topics/gateway#guild-ban-remove for more info.""" + await self.dispatch(self.app.entity_factory.deserialize_guild_ban_remove_event(payload)) - async def on_guild_emojis_update(self, shard: gateway.Gateway, payload: data_binding.JSONObject) -> None: + async def on_guild_emojis_update(self, _: gateway.Gateway, payload: data_binding.JSONObject) -> None: """See https://discord.com/developers/docs/topics/gateway#guild-emojis-update for more info.""" + await self.dispatch(self.app.entity_factory.deserialize_guild_emojis_update_event(payload)) - async def on_guild_integrations_update(self, shard: gateway.Gateway, payload: data_binding.JSONObject) -> None: + async def on_guild_integrations_update(self, _: gateway.Gateway, payload: data_binding.JSONObject) -> None: """See https://discord.com/developers/docs/topics/gateway#guild-integrations-update for more info.""" + await self.dispatch(self.app.entity_factory.deserialize_guild_integrations_update_event(payload)) - async def on_guild_member_add(self, shard: gateway.Gateway, payload: data_binding.JSONObject) -> None: + async def on_guild_member_add(self, _: gateway.Gateway, payload: data_binding.JSONObject) -> None: """See https://discord.com/developers/docs/topics/gateway#guild-member-add for more info.""" + await self.dispatch(self.app.entity_factory.deserialize_guild_member_add_event(payload)) - async def on_guild_member_remove(self, shard: gateway.Gateway, payload: data_binding.JSONObject) -> None: + async def on_guild_member_remove(self, _: gateway.Gateway, payload: data_binding.JSONObject) -> None: """See https://discord.com/developers/docs/topics/gateway#guild-member-remove for more info.""" + await self.dispatch(self.app.entity_factory.deserialize_guild_member_remove_event(payload)) - async def on_guild_member_update(self, shard: gateway.Gateway, payload: data_binding.JSONObject) -> None: + async def on_guild_member_update(self, _: gateway.Gateway, payload: data_binding.JSONObject) -> None: """See https://discord.com/developers/docs/topics/gateway#guild-member-update for more info.""" + await self.dispatch(self.app.entity_factory.deserialize_guild_member_update_event(payload)) - async def on_guild_members_chunk(self, shard: gateway.Gateway, payload: data_binding.JSONObject) -> None: + async def on_guild_members_chunk(self, _: gateway.Gateway, payload: data_binding.JSONObject) -> None: """See https://discord.com/developers/docs/topics/gateway#guild-members-chunk for more info.""" + # FIXME: implement model for this, and implement chunking components. + # await self.dispatch(self.app.entity_factory.deserialize_guild_member_chunk_event(payload)) - async def on_guild_role_create(self, shard: gateway.Gateway, payload: data_binding.JSONObject) -> None: + async def on_guild_role_create(self, _: gateway.Gateway, payload: data_binding.JSONObject) -> None: """See https://discord.com/developers/docs/topics/gateway#guild-role-create for more info.""" + await self.dispatch(self.app.entity_factory.deserialize_guild_role_create_event(payload)) - async def on_guild_role_update(self, shard: gateway.Gateway, payload: data_binding.JSONObject) -> None: + async def on_guild_role_update(self, _: gateway.Gateway, payload: data_binding.JSONObject) -> None: """See https://discord.com/developers/docs/topics/gateway#guild-role-update for more info.""" + await self.dispatch(self.app.entity_factory.deserialize_guild_role_update_event(payload)) - async def on_guild_role_delete(self, shard: gateway.Gateway, payload: data_binding.JSONObject) -> None: + async def on_guild_role_delete(self, _: gateway.Gateway, payload: data_binding.JSONObject) -> None: """See https://discord.com/developers/docs/topics/gateway#guild-role-delete for more info.""" + await self.dispatch(self.app.entity_factory.deserialize_guild_role_delete_event(payload)) - async def on_invite_create(self, shard: gateway.Gateway, payload: data_binding.JSONObject) -> None: + async def on_invite_create(self, _: gateway.Gateway, payload: data_binding.JSONObject) -> None: """See https://discord.com/developers/docs/topics/gateway#invite-create for more info.""" + await self.dispatch(self.app.entity_factory.deserialize_invite_create_event(payload)) - async def on_invite_delete(self, shard: gateway.Gateway, payload: data_binding.JSONObject) -> None: + async def on_invite_delete(self, _: gateway.Gateway, payload: data_binding.JSONObject) -> None: """See https://discord.com/developers/docs/topics/gateway#invite-delete for more info.""" + await self.dispatch(self.app.entity_factory.deserialize_invite_delete_event(payload)) - async def on_message_create(self, shard: gateway.Gateway, payload: data_binding.JSONObject) -> None: + async def on_message_create(self, _: gateway.Gateway, payload: data_binding.JSONObject) -> None: """See https://discord.com/developers/docs/topics/gateway#message-create for more info.""" + await self.dispatch(self.app.entity_factory.deserialize_message_create_event(payload)) - async def on_message_update(self, shard: gateway.Gateway, payload: data_binding.JSONObject) -> None: + async def on_message_update(self, _: gateway.Gateway, payload: data_binding.JSONObject) -> None: """See https://discord.com/developers/docs/topics/gateway#message-update for more info.""" + await self.dispatch(self.app.entity_factory.deserialize_message_update_event(payload)) - async def on_message_delete(self, shard: gateway.Gateway, payload: data_binding.JSONObject) -> None: + async def on_message_delete(self, _: gateway.Gateway, payload: data_binding.JSONObject) -> None: """See https://discord.com/developers/docs/topics/gateway#message-delete for more info.""" + await self.dispatch(self.app.entity_factory.deserialize_message_delete_event(payload)) - async def on_message_delete_bulk(self, shard: gateway.Gateway, payload: data_binding.JSONObject) -> None: + async def on_message_delete_bulk(self, _: gateway.Gateway, payload: data_binding.JSONObject) -> None: """See https://discord.com/developers/docs/topics/gateway#message-delete-bulk for more info.""" + await self.dispatch(self.app.entity_factory.deserialize_message_delete_bulk_event(payload)) - async def on_message_reaction_add(self, shard: gateway.Gateway, payload: data_binding.JSONObject) -> None: + async def on_message_reaction_add(self, _: gateway.Gateway, payload: data_binding.JSONObject) -> None: """See https://discord.com/developers/docs/topics/gateway#message-reaction-add for more info.""" + await self.dispatch(self.app.entity_factory.deserialize_message_reaction_add_event(payload)) - async def on_message_reaction_remove(self, shard: gateway.Gateway, payload: data_binding.JSONObject) -> None: + async def on_message_reaction_remove(self, _: gateway.Gateway, payload: data_binding.JSONObject) -> None: """See https://discord.com/developers/docs/topics/gateway#message-reaction-remove for more info.""" + await self.dispatch(self.app.entity_factory.deserialize_message_reaction_remove_event(payload)) - async def on_message_reaction_remove_all(self, shard: gateway.Gateway, payload: data_binding.JSONObject) -> None: + async def on_message_reaction_remove_all(self, _: gateway.Gateway, payload: data_binding.JSONObject) -> None: """See https://discord.com/developers/docs/topics/gateway#message-reaction-remove-all for more info.""" + await self.dispatch(self.app.entity_factory.deserialize_message_reaction_remove_all_event(payload)) - async def on_message_reaction_remove_emoji(self, shard: gateway.Gateway, payload: data_binding.JSONObject) -> None: + async def on_message_reaction_remove_emoji(self, _: gateway.Gateway, payload: data_binding.JSONObject) -> None: """See https://discord.com/developers/docs/topics/gateway#message-reaction-remove-emoji for more info.""" + await self.dispatch(self.app.entity_factory.deserialize_message_reaction_remove_emoji_event(payload)) - async def on_presence_update(self, shard: gateway.Gateway, payload: data_binding.JSONObject) -> None: + async def on_presence_update(self, _: gateway.Gateway, payload: data_binding.JSONObject) -> None: """See https://discord.com/developers/docs/topics/gateway#presence-update for more info.""" + await self.dispatch(self.app.entity_factory.deserialize_presence_update_event(payload)) - async def on_typing_start(self, shard: gateway.Gateway, payload: data_binding.JSONObject) -> None: + async def on_typing_start(self, _: gateway.Gateway, payload: data_binding.JSONObject) -> None: """See https://discord.com/developers/docs/topics/gateway#typing-start for more info.""" + await self.dispatch(self.app.entity_factory.deserialize_typing_start_event(payload)) - async def on_user_update(self, shard: gateway.Gateway, payload: data_binding.JSONObject) -> None: + async def on_user_update(self, _: gateway.Gateway, payload: data_binding.JSONObject) -> None: """See https://discord.com/developers/docs/topics/gateway#user-update for more info.""" + await self.dispatch(self.app.entity_factory.deserialize_own_user_update_event(payload)) - async def on_voice_state_update(self, shard: gateway.Gateway, payload: data_binding.JSONObject) -> None: + async def on_voice_state_update(self, _: gateway.Gateway, payload: data_binding.JSONObject) -> None: """See https://discord.com/developers/docs/topics/gateway#voice-state-update for more info.""" + await self.dispatch(self.app.entity_factory.deserialize_voice_state_update_event(payload)) - async def on_voice_server_update(self, shard: gateway.Gateway, payload: data_binding.JSONObject) -> None: + async def on_voice_server_update(self, _: gateway.Gateway, payload: data_binding.JSONObject) -> None: """See https://discord.com/developers/docs/topics/gateway#voice-server-update for more info.""" + await self.dispatch(self.app.entity_factory.deserialize_voice_server_update_event(payload)) - async def on_webhooks_update(self, shard: gateway.Gateway, payload: data_binding.JSONObject) -> None: + async def on_webhooks_update(self, _: gateway.Gateway, payload: data_binding.JSONObject) -> None: """See https://discord.com/developers/docs/topics/gateway#webhooks-update for more info.""" + await self.dispatch(self.app.entity_factory.deserialize_webhook_update_event(payload)) diff --git a/hikari/impl/event_manager_core.py b/hikari/impl/event_manager_core.py index 4bcb9608b6..01b6d3c809 100644 --- a/hikari/impl/event_manager_core.py +++ b/hikari/impl/event_manager_core.py @@ -184,7 +184,7 @@ async def _invoke_callback(self, callback: _CallbackT, event: _EventT) -> None: if base.is_no_catch_event(event): self.logger.error("an exception occurred handling an event, but it has been ignored", exc_info=ex) else: - await self.dispatch(other.ExceptionEvent(app=self._app, exception=ex, event=event, callback=callback)) + await self.dispatch(other.ExceptionEvent(exception=ex, event=event, callback=callback)) def dispatch(self, event: base.HikariEvent) -> aio.Future[typing.Any]: if not isinstance(event, base.HikariEvent): diff --git a/hikari/models/channels.py b/hikari/models/channels.py index 8d2601d347..aabeda581d 100644 --- a/hikari/models/channels.py +++ b/hikari/models/channels.py @@ -166,7 +166,7 @@ class DMChannel(TextChannel): !!! warning This might point to an invalid or deleted message. Do not assume that - this will always be valid. + this will always be valid. """ recipients: typing.Mapping[snowflake.Snowflake, users.User] = attr.ib( @@ -190,7 +190,7 @@ class GroupDMChannel(DMChannel): application_id: typing.Optional[snowflake.Snowflake] = attr.ib(eq=False, hash=False) """The ID of the application that created the group DM. - + If the group DM was not created by a bot, this will be `None`. """ @@ -235,19 +235,19 @@ class GuildChannel(PartialChannel): """The ID of the guild the channel belongs to. !!! warning - This will be `None` when received over the gateway in certain events + This will be `None` when received over the gateway in certain events (e.g Guild Create). """ position: int = attr.ib(eq=False, hash=False) """The sorting position of the channel. - + Higher numbers appear further down the channel list. """ permission_overwrites: typing.Mapping[snowflake.Snowflake, PermissionOverwrite] = attr.ib(eq=False, hash=False) """The permission overwrites for the channel. - + This maps the ID of the entity in the overwrite to the overwrite data. """ @@ -255,13 +255,13 @@ class GuildChannel(PartialChannel): """Whether the channel is marked as NSFW. !!! warning - This will be `None` when received over the gateway in certain events + This will be `None` when received over the gateway in certain events (e.g Guild Create). """ parent_id: typing.Optional[snowflake.Snowflake] = attr.ib(eq=False, hash=False, repr=True) """The ID of the parent category the channel belongs to. - + If no parent category is set for the channel, this will be `None`. """ @@ -287,21 +287,21 @@ class GuildTextChannel(GuildChannel, TextChannel): !!! warning This might point to an invalid or deleted message. Do not assume that - this will always be valid. + this will always be valid. """ rate_limit_per_user: datetime.timedelta = attr.ib(eq=False, hash=False) """The delay (in seconds) between a user can send a message to this channel. !!! note - Any user that has permissions allowing `MANAGE_MESSAGES`, + Any user that has permissions allowing `MANAGE_MESSAGES`, `MANAGE_CHANNEL`, `ADMINISTRATOR` will not be limited. Likewise, bots will not be affected by this rate limit. """ last_pin_timestamp: typing.Optional[datetime.datetime] = attr.ib(eq=False, hash=False) """The timestamp of the last-pinned message. - + !!! note This may be `None` in several cases; Discord does not document what these cases are. Trust no one! @@ -317,10 +317,10 @@ class GuildNewsChannel(GuildChannel, TextChannel): last_message_id: typing.Optional[snowflake.Snowflake] = attr.ib(eq=False, hash=False) """The ID of the last message sent in this channel. - + !!! warning This might point to an invalid or deleted message. Do not assume that - this will always be valid. + this will always be valid. """ last_pin_timestamp: typing.Optional[datetime.datetime] = attr.ib(eq=False, hash=False) @@ -351,6 +351,6 @@ class GuildVoiceChannel(GuildChannel): user_limit: int = attr.ib(eq=False, hash=False, repr=True) """The user limit for the voice channel. - + If this is `0`, then assume no limit. """ diff --git a/hikari/models/guilds.py b/hikari/models/guilds.py index a797d50d69..944d5c7fdd 100644 --- a/hikari/models/guilds.py +++ b/hikari/models/guilds.py @@ -233,7 +233,7 @@ class Member(bases.Entity): """This member's nickname. This will be `None` if not set and `hikari.utilities.undefined.Undefined` - if it's state is unknown. + if it's state is unknown. """ role_ids: typing.Set[snowflake.Snowflake] = attr.ib( diff --git a/tests/hikari/impl/test_entity_factory.py b/tests/hikari/impl/test_entity_factory.py index 175b651434..050992ec33 100644 --- a/tests/hikari/impl/test_entity_factory.py +++ b/tests/hikari/impl/test_entity_factory.py @@ -3072,7 +3072,9 @@ def ready_payload(self, my_user_payload, guild_unavailable_payload): def test_deserialize_ready_event( self, entity_factory_impl, ready_payload, my_user_payload, guild_unavailable_payload ): - ready_event = entity_factory_impl.deserialize_ready_event(ready_payload) + shard = mock.MagicMock() + ready_event = entity_factory_impl.deserialize_ready_event(shard, ready_payload) + assert ready_event.shard is shard assert ready_event.gateway_version == 6 assert ready_event.my_user == entity_factory_impl.deserialize_my_user(my_user_payload) assert ready_event.unavailable_guilds == { @@ -3086,15 +3088,18 @@ def test_deserialize_ready_event( def test_deserialize_ready_event_with_unset_fields( self, entity_factory_impl, ready_payload, my_user_payload, guild_unavailable_payload ): + shard = mock.MagicMock() ready_event = entity_factory_impl.deserialize_ready_event( + shard, { "v": 6, "user": my_user_payload, "private_channels": [], "guilds": [], "session_id": "mlksdfoijpsdfioprewi09u43rw", - } + }, ) + assert ready_event.shard is shard assert ready_event.shard_id is None assert ready_event.shard_count is None From 6d188225ce5f252bc16fb6a6893e06eb4034a501 Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Fri, 5 Jun 2020 15:25:26 +0100 Subject: [PATCH 456/922] Tweaked Message#reply and Message#edit signatures. --- hikari/models/messages.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/hikari/models/messages.py b/hikari/models/messages.py index a3b777622c..6bbd097d9f 100644 --- a/hikari/models/messages.py +++ b/hikari/models/messages.py @@ -318,8 +318,8 @@ async def fetch_channel(self) -> channels.PartialChannel: async def edit( # pylint:disable=line-too-long self, - *, content: str = ..., + *, embed: embeds_.Embed = ..., mentions_everyone: bool = True, user_mentions: typing.Union[ @@ -415,10 +415,11 @@ async def safe_edit( role_mentions=role_mentions, ) + # FIXME: use undefined, not ... async def reply( # pylint:disable=line-too-long self, - *, content: str = ..., + *, embed: embeds_.Embed = ..., files: typing.Sequence[files_.BaseStream] = ..., mentions_everyone: bool = True, From e79a6d4f575e881b8929993db1ca1221bd88d442 Mon Sep 17 00:00:00 2001 From: davfsa Date: Fri, 5 Jun 2020 19:13:51 +0200 Subject: [PATCH 457/922] Fix LGTM alerts --- ci/config.py | 1 - hikari/api/component.py | 4 +++- hikari/utilities/undefined.py | 3 --- 3 files changed, 3 insertions(+), 5 deletions(-) diff --git a/ci/config.py b/ci/config.py index 66c2ed18e1..8712518063 100644 --- a/ci/config.py +++ b/ci/config.py @@ -16,7 +16,6 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . -import os import os as _os IS_CI = "CI" in _os.environ diff --git a/hikari/api/component.py b/hikari/api/component.py index 267cb303eb..feb5ba752c 100644 --- a/hikari/api/component.py +++ b/hikari/api/component.py @@ -23,8 +23,10 @@ __all__ = ["IComponent"] import abc +import typing -from hikari.api import app +if typing.TYPE_CHECKING: + from hikari.api import app class IComponent(abc.ABC): diff --git a/hikari/utilities/undefined.py b/hikari/utilities/undefined.py index 4e46473898..32714ef218 100644 --- a/hikari/utilities/undefined.py +++ b/hikari/utilities/undefined.py @@ -80,9 +80,6 @@ class Undefined(klass.Singleton): def __bool__(self) -> bool: return False - def __eq__(self, other: typing.Any) -> bool: - return other is self - def __iter__(self) -> typing.Iterator[None]: yield from () From a523bff111a18e3fa6861262b0e90e9627c93c9d Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Fri, 5 Jun 2020 19:14:27 +0100 Subject: [PATCH 458/922] Fixed docstrings failing pydocstyle. --- hikari/impl/bot.py | 2 +- hikari/impl/cache.py | 6 +- hikari/impl/gateway_zookeeper.py | 17 +++-- hikari/impl/rest_app.py | 45 ++++++++++--- hikari/net/gateway.py | 2 +- hikari/net/rest.py | 110 +++++++++++++++---------------- 6 files changed, 108 insertions(+), 74 deletions(-) diff --git a/hikari/impl/bot.py b/hikari/impl/bot.py index c98afd312b..1c15a4c6b3 100644 --- a/hikari/impl/bot.py +++ b/hikari/impl/bot.py @@ -171,7 +171,7 @@ def __init__( config = http_settings_.HTTPSettings() if isinstance(config, undefined.Undefined) else config - self._cache = cache_impl.CacheImpl(app=self) + self._cache = cache_impl.InMemoryCacheImpl(app=self) self._config = config self._event_manager = event_manager.EventManagerImpl(app=self) self._entity_factory = entity_factory_impl.EntityFactoryImpl(app=self) diff --git a/hikari/impl/cache.py b/hikari/impl/cache.py index a3982b07f4..c9bdefa731 100644 --- a/hikari/impl/cache.py +++ b/hikari/impl/cache.py @@ -20,7 +20,7 @@ from __future__ import annotations -__all__ = ["CacheImpl"] +__all__ = ["InMemoryCacheImpl"] import typing @@ -30,7 +30,9 @@ from hikari.api import app as app_ -class CacheImpl(cache.ICache): +class InMemoryCacheImpl(cache.ICache): + """In-memory cache implementation.""" + def __init__(self, app: app_.IApp) -> None: self._app = app diff --git a/hikari/impl/gateway_zookeeper.py b/hikari/impl/gateway_zookeeper.py index 296091bf2a..e4ea1ec396 100644 --- a/hikari/impl/gateway_zookeeper.py +++ b/hikari/impl/gateway_zookeeper.py @@ -16,6 +16,7 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . +"""Abstract zookeeper implementation for multiple-sharded applications.""" from __future__ import annotations @@ -45,6 +46,14 @@ class AbstractGatewayZookeeper(app_.IGatewayZookeeper, abc.ABC): """Provides keep-alive logic for orchestrating multiple shards. + This provides the logic needed to keep multiple shards alive at once, and + correctly orchestrate their startup and shutdown. Applications that are + multi-sharded or auto-sharded can extend this functionality to acquire the + ability to manage sharding. + + !!! note + This does not provide REST API functionality. + Parameters ---------- compression : bool @@ -54,8 +63,7 @@ class AbstractGatewayZookeeper(app_.IGatewayZookeeper, abc.ABC): Optional aiohttp settings to apply to the created shards. debug : bool Defaulting to `False`, if `True`, then each payload sent and received - on the gateway will be dumped to debug logs, and every REST API request - and response will also be dumped to logs. This will provide useful + on the gateway will be dumped to debug logs. This will provide useful debugging context at the cost of performance. Generally you do not need to enable this. initial_activity : hikari.models.presences.OwnActivity or None or hikari.utilities.undefined.Undefined @@ -112,11 +120,10 @@ class AbstractGatewayZookeeper(app_.IGatewayZookeeper, abc.ABC): Raises ------ - TypeError - If sharding information is not specified correctly. ValueError If sharding information is provided, but is unfeasible or invalid. - + TypeError + If sharding information is not specified correctly. """ def __init__( diff --git a/hikari/impl/rest_app.py b/hikari/impl/rest_app.py index a08135c8b3..dd046b5f17 100644 --- a/hikari/impl/rest_app.py +++ b/hikari/impl/rest_app.py @@ -16,6 +16,11 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . +"""Implementation of a REST application. + +This provides functionality for projects that only need to use the RESTful +API, such as web dashboards and other OAuth2-based scripts. +""" from __future__ import annotations @@ -39,13 +44,41 @@ class RESTAppImpl(app_.IRESTApp): + """Application that only provides RESTful functionality. + + Parameters + ---------- + config : hikari.utilities.undefined.Undefined or hikari.net.http_settings.HTTPSettings + Optional aiohttp settings to apply to the REST components. If undefined, + then sane defaults are used. + debug : bool + Defaulting to `False`, if `True`, then each payload sent and received + in HTTP requests will be dumped to debug logs. This will provide useful + debugging context at the cost of performance. Generally you do not + need to enable this. + token : hikari.utilities.undefined.Undefined or str + If defined, the token to use. If not defined, no token will be injected + into the `Authorization` header for requests. + token_type : hikari.utilities.undefined.Undefined or str + The token type to use. If undefined, a default is used instead, which + will be `Bot`. If no `token` is provided, this is ignored. + url : hikari.utilities.undefined.Undefined or str + The API URL to hit. Generally you can leave this undefined and use the + default. + version : int + The API version to use. This is interpolated into the default `url` + to create the full URL. Currently this only supports `6` or `7`, and + defaults to `6` (since the v7 REST API is experimental, undocumented, + and subject to breaking change without prior notice at any time). + """ + def __init__( self, config: typing.Union[undefined.Undefined, http_settings_.HTTPSettings] = undefined.Undefined(), debug: bool = False, token: typing.Union[undefined.Undefined, str] = undefined.Undefined(), token_type: typing.Union[undefined.Undefined, str] = undefined.Undefined(), - rest_url: typing.Union[undefined.Undefined, str] = undefined.Undefined(), + url: typing.Union[undefined.Undefined, str] = undefined.Undefined(), version: int = 6, ) -> None: self._logger = klass.get_logger(self) @@ -53,15 +86,9 @@ def __init__( config = http_settings_.HTTPSettings() if isinstance(config, undefined.Undefined) else config self._rest = rest_.REST( - app=self, - config=config, - debug=debug, - token=token, - token_type=token_type, - rest_url=rest_url, - version=version, + app=self, config=config, debug=debug, token=token, token_type=token_type, rest_url=url, version=version, ) - self._cache = cache_impl.CacheImpl(self) + self._cache = cache_impl.InMemoryCacheImpl(self) self._entity_factory = entity_factory_impl.EntityFactoryImpl(self) @property diff --git a/hikari/net/gateway.py b/hikari/net/gateway.py index 5060da6e91..e6f2738fd7 100644 --- a/hikari/net/gateway.py +++ b/hikari/net/gateway.py @@ -16,7 +16,7 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . -"""Single-shard implementation for the V6 and V7 gateway.""" +"""Single-shard implementation for the V6 and V7 event gateway for Discord.""" from __future__ import annotations diff --git a/hikari/net/rest.py b/hikari/net/rest.py index c3e1fde7c4..edf4891380 100644 --- a/hikari/net/rest.py +++ b/hikari/net/rest.py @@ -16,6 +16,7 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . +"""Implementation of a V6 and V7 compatible REST API for Discord.""" from __future__ import annotations @@ -415,7 +416,7 @@ async def fetch_channel( Parameters ---------- - channel : hikari.models.channels.PartialChannel | hikari.utilities.snowflake.Snowflake | int | str + channel : hikari.models.channels.PartialChannel or hikari.utilities.snowflake.Snowflake or int or str The channel to fetch. This may be a channel object, or the ID of an existing channel. @@ -491,29 +492,29 @@ async def edit_channel( Parameters ---------- - channel : hikari.models.channels.PartialChannel | hikari.utilities.snowflake.Snowflake | int | str + channel : hikari.models.channels.PartialChannel or hikari.utilities.snowflake.Snowflake or int or str The channel to edit. This may be a channel object, or the ID of an existing channel. - name : hikari.utilities.undefined.Undefined | str + name : hikari.utilities.undefined.Undefined or str If provided, the new name for the channel. - position : hikari.utilities.undefined.Undefined | int + position : hikari.utilities.undefined.Undefined or int If provided, the new position for the channel. - topic : hikari.utilities.undefined.Undefined | str + topic : hikari.utilities.undefined.Undefined or str If provided, the new topic for the channel. - nsfw : hikari.utilities.undefined.Undefined | bool + nsfw : hikari.utilities.undefined.Undefined or bool If provided, whether the channel should be marked as NSFW or not. - bitrate : hikari.utilities.undefined.Undefined | int + bitrate : hikari.utilities.undefined.Undefined or int If provided, the new bitrate for the channel. - user_limit : hikari.utilities.undefined.Undefined | int + user_limit : hikari.utilities.undefined.Undefined or int If provided, the new user limit in the channel. - rate_limit_per_user : hikari.utilities.undefined.Undefined | datetime.timedelta | float | int + rate_limit_per_user : hikari.utilities.undefined.Undefined or datetime.timedelta or float or int If provided, the new rate limit per user in the channel. - permission_overwrites : hikari.utilities.undefined.Undefined | typing.Sequence[hikari.models.channels.PermissionOverwrite] + permission_overwrites : hikari.utilities.undefined.Undefined or typing.Sequence[hikari.models.channels.PermissionOverwrite] If provided, the new permission overwrites for the channel. - parent_category : hikari.utilities.undefined.Undefined | hikari.models.channels.GuildCategory | hikari.utilities.snowflake.Snowflake | int | str + parent_category : hikari.utilities.undefined.Undefined or hikari.models.channels.GuildCategory or hikari.utilities.snowflake.Snowflake or int or str If provided, the new guild category for the channel. This may be a category object, or the ID of an existing category. - reason : hikari.utilities.undefined.Undefined | str + reason : hikari.utilities.undefined.Undefined or str If provided, the reason that will be recorded in the audit logs. Returns @@ -556,7 +557,7 @@ async def delete_channel(self, channel: typing.Union[channels.PartialChannel, ba Parameters ---------- - channel : hikari.models.channels.PartialChannel | hikari.utilities.snowflake.Snowflake | int | str + channel : hikari.models.channels.PartialChannel or hikari.utilities.snowflake.Snowflake or int or str The channel to delete. This may be a channel object, or the ID of an existing channel. @@ -617,20 +618,20 @@ async def edit_permission_overwrites( Parameters ---------- - channel : hikari.models.channels.PartialChannel | hikari.utilities.snowflake.Snowflake | int | str + channel : hikari.models.channels.PartialChannel or hikari.utilities.snowflake.Snowflake or int or str The channel to edit a permission overwrite in. This may be a channel object, or the ID of an existing channel. - target : hikari.models.users.User | hikari.models.guidls.Role | hikari.models.channels.PermissionOverwrite | hikari.utilities.snowflake.Snowflake | int | str + target : hikari.models.users.User or hikari.models.guidls.Role or hikari.models.channels.PermissionOverwrite or hikari.utilities.snowflake.Snowflake or int or str The channel overwrite to edit. This may be a overwrite object, or the ID of an existing channel. - target_type : hikari.utilities.undefined.Undefined | hikari.models.channels.PermissionOverwriteType | str + target_type : hikari.utilities.undefined.Undefined or hikari.models.channels.PermissionOverwriteType or str If provided, the type of the target to update. If unset, will attempt to get the type from `target`. - allow : hikari.utilities.undefined.Undefined | hikari.models.permissions.Permission + allow : hikari.utilities.undefined.Undefined or hikari.models.permissions.Permission If provided, the new vale of all allowed permissions. - deny : hikari.utilities.undefined.Undefined | hikari.models.permissions.Permission + deny : hikari.utilities.undefined.Undefined or hikari.models.permissions.Permission If provided, the new vale of all disallowed permissions. - reason : hikari.utilities.undefined.Undefined | str + reason : hikari.utilities.undefined.Undefined or str If provided, the reason that will be recorded in the audit logs. Raises @@ -650,7 +651,6 @@ async def edit_permission_overwrites( hikari.errors.ServerHTTPErrorResponse If an internal error occurs on Discord while handling the request. """ - if isinstance(target_type, undefined.Undefined): if isinstance(target, users.User): target_type = channels.PermissionOverwriteType.MEMBER @@ -680,10 +680,10 @@ async def delete_permission_overwrite( Parameters ---------- - channel : hikari.models.channels.PartialChannel | hikari.utilities.snowflake.Snowflake | int | str + channel : hikari.models.channels.PartialChannel or hikari.utilities.snowflake.Snowflake or int or str The channel to delete a permission overwrite in. This may be a channel object, or the ID of an existing channel. - target : hikari.models.users.User | hikari.models.guidls.Role | hikari.models.channels.PermissionOverwrite | hikari.utilities.snowflake.Snowflake | int | str + target : hikari.models.users.User or hikari.models.guidls.Role or hikari.models.channels.PermissionOverwrite or hikari.utilities.snowflake.Snowflake or int or str The channel overwrite to delete. Raises @@ -707,7 +707,7 @@ async def fetch_channel_invites( Parameters ---------- - channel : hikari.models.channels.PartialChannel | hikari.utilities.snowflake.Snowflake | int | str + channel : hikari.models.channels.PartialChannel or hikari.utilities.snowflake.Snowflake or int or str The channel to fetch the invites from. This may be a channel object, or the ID of an existing channel. @@ -748,23 +748,23 @@ async def create_invite( Parameters ---------- - channel : hikari.models.channels.PartialChannel | hikari.utilities.snowflake.Snowflake | int | str + channel : hikari.models.channels.PartialChannel or hikari.utilities.snowflake.Snowflake or int or str The channel to create a invite for. This may be a channel object, or the ID of an existing channel. - max_age : hikari.utilities.undefined.Undefined | datetime.timedelta | float | int + max_age : hikari.utilities.undefined.Undefined or datetime.timedelta or float or int If provided, the duration of the invite before expiry. - max_uses : hikari.utilities.undefined.Undefined | int + max_uses : hikari.utilities.undefined.Undefined or int If provided, the max uses the invite can have. - temporary : hikari.utilities.undefined.Undefined | bool + temporary : hikari.utilities.undefined.Undefined or bool If provided, whether the invite only grants temporary membership. - unique : hikari.utilities.undefined.Undefined | bool + unique : hikari.utilities.undefined.Undefined or bool If provided, wheter the invite should be unique. - target_user : hikari.utilities.undefined.Undefined | hikari.models.users.User | hikari.utilities.snowflake.Snowflake | int | str + target_user : hikari.utilities.undefined.Undefined or hikari.models.users.User or hikari.utilities.snowflake.Snowflake or int or str If provided, the target user id for this invite. This may be a user object, or the ID of an existing user. - target_user_type : hikari.utilities.undefined.Undefined | hikari.models.invites.TargetUserType | int + target_user_type : hikari.utilities.undefined.Undefined or hikari.models.invites.TargetUserType or int If provided, the type of target user for this invite. - reason : hikari.utilities.undefined.Undefined | str + reason : hikari.utilities.undefined.Undefined or str If provided, the reason that will be recorded in the audit logs. Returns @@ -804,7 +804,7 @@ def trigger_typing( Parameters ---------- - channel : hikari.models.channels.PartialChannel | hikari.utilities.snowflake.Snowflake | int | str + channel : hikari.models.channels.PartialChannel or hikari.utilities.snowflake.Snowflake or int or str The channel to trigger typing in. This may be a channel object, or the ID of an existing channel. @@ -839,7 +839,7 @@ async def fetch_pins( Parameters ---------- - channel : hikari.models.channels.PartialChannel | hikari.utilities.snowflake.Snowflake | int | str + channel : hikari.models.channels.PartialChannel or hikari.utilities.snowflake.Snowflake or int or str The channel to fetch pins from. This may be a channel object, or the ID of an existing channel. @@ -873,10 +873,10 @@ async def pin_message( Parameters ---------- - channel : hikari.models.channels.PartialChannel | hikari.utilities.snowflake.Snowflake | int | str + channel : hikari.models.channels.PartialChannel or hikari.utilities.snowflake.Snowflake or int or str The channel to pin a message in. This may be a channel object, or the ID of an existing channel. - message : hikari.models.messges.Message | hikari.utilities.snowflake.Snowflake | int | str + message : hikari.models.messges.Message or hikari.utilities.snowflake.Snowflake or int or str The message to pin. This may be a message object, or the ID of an existing message. @@ -900,14 +900,14 @@ async def unpin_message( channel: typing.Union[channels.TextChannel, bases.UniqueObject], message: typing.Union[messages_.Message, bases.UniqueObject], ) -> None: - """ + """Unpin a given message from a given text channel. Parameters ---------- - channel : hikari.models.channels.PartialChannel | hikari.utilities.snowflake.Snowflake | int | str + channel : hikari.models.channels.PartialChannel or hikari.utilities.snowflake.Snowflake or int or str The channel to unpin a message in. This may be a channel object, or the ID of an existing channel. - message : hikari.models.messges.Message | hikari.utilities.snowflake.Snowflake | int | str + message : hikari.models.messges.Message or hikari.utilities.snowflake.Snowflake or int or str The message to unpin. This may be a message object, or the ID of an existing message. @@ -975,16 +975,16 @@ def fetch_messages( Parameters ---------- - channel : hikari.models.channels.PartialChannel | hikari.utilities.snowflake.Snowflake | int | str + channel : hikari.models.channels.PartialChannel or hikari.utilities.snowflake.Snowflake or int or str The channel to fetch messages in. This may be a channel object, or the ID of an existing channel. - before : hikari.utilities.undefined.Undefined | datetime.datetime | hikari.utilities.snowflake.Snowflake | int | str + before : hikari.utilities.undefined.Undefined or datetime.datetime or hikari.utilities.snowflake.Snowflake or int or str If provided, fetch messages before this snowflake. If you provide a datetime object, it will be transformed into a snowflake. - after : hikari.utilities.undefined.Undefined | datetime.datetime | hikari.utilities.snowflake.Snowflake | int | str + after : hikari.utilities.undefined.Undefined or datetime.datetime or hikari.utilities.snowflake.Snowflake or int or str If provided, fetch messages after this snowflake. If you provide a datetime object, it will be transformed into a snowflake. - around : hikari.utilities.undefined.Undefined | datetime.datetime | hikari.utilities.snowflake.Snowflake | int | str + around : hikari.utilities.undefined.Undefined or datetime.datetime or hikari.utilities.snowflake.Snowflake or int or str If provided, fetch messages around this snowflake. If you provide a datetime object, it will be transformed into a snowflake. @@ -1039,10 +1039,10 @@ async def fetch_message( Parameters ---------- - channel : hikari.models.channels.PartialChannel | hikari.utilities.snowflake.Snowflake | int | str + channel : hikari.models.channels.PartialChannel or hikari.utilities.snowflake.Snowflake or int or str The channel to fetch messages in. This may be a channel object, or the ID of an existing channel. - message : hikari.models.messages.Message | hikari.utilities.snowflake.Snowflake | int | str + message : hikari.models.messages.Message or hikari.utilities.snowflake.Snowflake or int or str The message to fetch. This may be a channel object, or the ID of an existing channel. @@ -1085,26 +1085,26 @@ async def create_message( Parameters ---------- - channel : hikari.models.channels.PartialChannel | hikari.utilities.snowflake.Snowflake | int | str + channel : hikari.models.channels.PartialChannel or hikari.utilities.snowflake.Snowflake or int or str The channel to create the message in. This may be a channel object, or the ID of an existing channel. - text : hikari.utilities.undefined.Undefined | str + text : hikari.utilities.undefined.Undefined or str If specified, the message contents. - embed : hikari.utilities.undefined.Undefined | hikari.models.embeds.Embed + embed : hikari.utilities.undefined.Undefined or hikari.models.embeds.Embed If specified, the message embed. - attachments : hikari.utilities.undefined.Undefined | typing.Sequence[hikari.models.files.BaseStream] + attachments : hikari.utilities.undefined.Undefined or typing.Sequence[hikari.models.files.BaseStream] If specified, the message attachments. - tts : hikari.utilities.undefined.Undefined | bool + tts : hikari.utilities.undefined.Undefined or bool If specified, whether the message will be TTS (Text To Speech). - nonce : hikari.utilities.undefined.Undefined | str + nonce : hikari.utilities.undefined.Undefined or str If specified, a nonce that can be used for optimistic message sending. mentions_everyone : bool If specified, whether the message should parse @everyone/@here mentions. - user_mentions : typing.Collection[hikari.models.users.User | hikari.utilities.snowflake.Snowflake | int | str] | bool + user_mentions : typing.Collection[hikari.models.users.User or hikari.utilities.snowflake.Snowflake or int or str] or bool If specified, and `bool`, whether to parse user mentions. If specified and `list`, the users to parse the mention for. This may be a user object, or the ID of an existing user. - role_mentions : typing.Collection[hikari.models.guilds.Role | hikari.utilities.snowflake.Snowflake | int | str] | bool + role_mentions : typing.Collection[hikari.models.guilds.Role or hikari.utilities.snowflake.Snowflake or int or str] or bool If specified and `bool`, whether to parse role mentions. If specified and `list`, the roles to parse the mention for. This may be a role object, or the ID of an existing role. @@ -1137,7 +1137,6 @@ async def create_message( You are expected to make a connection to the gateway and identify once before being able to use this endpoint for a bot. """ - route = routes.POST_CHANNEL_MESSAGES.compile(channel=channel) body = data_binding.JSONObjectBuilder() @@ -1178,10 +1177,10 @@ async def edit_message( Parameters ---------- - channel : hikari.models.channels.PartialChannel | hikari.utilities.snowflake.Snowflake | int | str + channel : hikari.models.channels.PartialChannel or hikari.utilities.snowflake.Snowflake or int or str The channel to edit the message in. This may be a channel object, or the ID of an existing channel. - message : hikari.models.messages.Messages | hikari.utilities.snowflake.Snowflake | int | str + message : hikari.models.messages.Messages or hikari.utilities.snowflake.Snowflake or int or str The message to fetch. text embed @@ -1216,7 +1215,6 @@ async def edit_message( hikari.errors.ServerHTTPErrorResponse If an internal error occurs on Discord while handling the request. """ - route = routes.PATCH_CHANNEL_MESSAGE.compile(channel=channel, message=message) body = data_binding.JSONObjectBuilder() body.put("content", text, str) From 7dad9985658f553853fa05fa768265a1fa106082 Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Fri, 5 Jun 2020 20:19:12 +0100 Subject: [PATCH 459/922] Fixed embed docstrings and changed validation errors to warnings. --- hikari/models/colors.py | 35 +++- hikari/models/embeds.py | 399 ++++++++++++++++++++++++---------------- 2 files changed, 274 insertions(+), 160 deletions(-) diff --git a/hikari/models/colors.py b/hikari/models/colors.py index eb3fa96b17..2bf2e8c91a 100644 --- a/hikari/models/colors.py +++ b/hikari/models/colors.py @@ -48,19 +48,24 @@ class Color(int): Examples of conversions to given formats include: ```py - >>> c = Color(0xFF051A) Color(r=0xff, g=0x5, b=0x1a) + >>> hex(c) 0xff051a + >>> c.hex_code #FF051A + >>> str(c) #FF051A + >>> int(c) 16712986 + >>> c.rgb (255, 5, 26) + >>> c.rgb_float (1.0, 0.0196078431372549, 0.10196078431372549) ``` @@ -72,20 +77,28 @@ class Color(int): ```py >>> Color.of(0xFF051A) Color(r=0xff, g=0x5, b=0x1a) + >>> Color.of(16712986) Color(r=0xff, g=0x5, b=0x1a) + >>> c = Color.of((255, 5, 26)) Color(r=0xff, g=0x5, b=1xa) + >>> c = Color.of(255, 5, 26) Color(r=0xff, g=0x5, b=1xa) + >>> c = Color.of([0xFF, 0x5, 0x1a]) Color(r=0xff, g=0x5, b=1xa) + >>> c = Color.of("#1a2b3c") Color(r=0x1a, g=0x2b, b=0x3c) + >>> c = Color.of("#1AB") Color(r=0x11, g=0xaa, b=0xbb) + >>> c = Color.of((1.0, 0.0196078431372549, 0.10196078431372549)) Color(r=0xff, g=0x5, b=0x1a) + >>> c = Color.of([1.0, 0.0196078431372549, 0.10196078431372549]) Color(r=0xff, g=0x5, b=0x1a) ``` @@ -95,12 +108,16 @@ class Color(int): ```py >>> c = Color(16712986) Color(r=0xff, g=0x5, b=0x1a) + >>> c = Color.from_rgb(255, 5, 26) Color(r=0xff, g=0x5, b=1xa) + >>> c = Color.from_hex_code("#1a2b3c") Color(r=0x1a, g=0x2b, b=0x3c) + >>> c = Color.from_hex_code("#1AB") Color(r=0x11, g=0xaa, b=0xbb) + >>> c = Color.from_rgb_float(1.0, 0.0196078431372549, 0.10196078431372549) Color(r=0xff, g=0x5, b=0x1a) ``` @@ -289,12 +306,12 @@ def from_hex_code(cls, hex_code: str, /) -> Color: raise ValueError("Color code is invalid length. Must be 3 or 6 digits") @classmethod - def from_int(cls, i: typing.SupportsInt, /) -> Color: + def from_int(cls, integer: typing.SupportsInt, /) -> Color: """Convert the given `typing.SupportsInt` to a `Color`. Parameters ---------- - i : typing.SupportsInt + integer : typing.SupportsInt The raw color integer. Returns @@ -302,7 +319,7 @@ def from_int(cls, i: typing.SupportsInt, /) -> Color: Color The Color object. """ - return cls(i) + return cls(integer) # Partially chose to override these as the docstrings contain typos according to Sphinx. @classmethod @@ -356,20 +373,28 @@ def of( ```py >>> Color.of(0xFF051A) Color(r=0xff, g=0x5, b=0x1a) + >>> Color.of(16712986) Color(r=0xff, g=0x5, b=0x1a) + >>> c = Color.of((255, 5, 26)) Color(r=0xff, g=0x5, b=1xa) + >>> c = Color.of(255, 5, 26) Color(r=0xff, g=0x5, b=1xa) + >>> c = Color.of([0xFF, 0x5, 0x1a]) Color(r=0xff, g=0x5, b=1xa) + >>> c = Color.of("#1a2b3c") Color(r=0x1a, g=0x2b, b=0x3c) + >>> c = Color.of("#1AB") Color(r=0x11, g=0xaa, b=0xbb) + >>> c = Color.of((1.0, 0.0196078431372549, 0.10196078431372549)) Color(r=0xff, g=0x5, b=0x1a) + >>> c = Color.of([1.0, 0.0196078431372549, 0.10196078431372549]) Color(r=0xff, g=0x5, b=0x1a) ``` @@ -413,11 +438,9 @@ def to_bytes(self, length: int, byteorder: str, *, signed: bool = True) -> bytes ---------- length : int The number of bytes to produce. Should be around `3`, but not less. - byteorder : str The endianess of the value represented by the bytes. Can be `"big"` endian or `"little"` endian. - signed : bool Whether the value is signed or unsigned. diff --git a/hikari/models/embeds.py b/hikari/models/embeds.py index 153b998f31..ffb75cb75c 100644 --- a/hikari/models/embeds.py +++ b/hikari/models/embeds.py @@ -31,11 +31,15 @@ "EmbedField", ] +import copy import datetime import typing +import warnings +import weakref import attr +from hikari import errors from hikari.models import colors from hikari.models import files @@ -57,17 +61,18 @@ class EmbedFooter: """Represents an embed footer.""" text: typing.Optional[str] = attr.ib(default=None, repr=True) - """The footer text.""" + """The footer text, or `None` if not present.""" icon_url: typing.Optional[str] = attr.ib(default=None) - """The URL of the footer icon.""" + """The URL of the footer icon, or `None` if not present.""" proxy_icon_url: typing.Optional[str] = attr.ib(default=None) - """The proxied URL of the footer icon. + """The proxied URL of the footer icon, or `None` if not present. !!! note This field cannot be set by bots or webhooks while sending an embed and - will be ignored during serialization. + will be ignored during serialization. Expect this to be populated on + any received embed attached to a message event. """ @@ -78,30 +83,33 @@ class EmbedImage: url: typing.Optional[str] = attr.ib( default=None, repr=True, ) - """The URL of the image.""" + """The URL of the image to show, or `None` if not present.""" proxy_url: typing.Optional[str] = attr.ib(default=None,) - """The proxied URL of the image. + """The proxied URL of the image, or `None` if not present. !!! note This field cannot be set by bots or webhooks while sending an embed and - will be ignored during serialization. + will be ignored during serialization. Expect this to be populated on + any received embed attached to a message event. """ height: typing.Optional[int] = attr.ib(default=None) - """The height of the image. + """The height of the image, if present and known, otherwise `None`. !!! note This field cannot be set by bots or webhooks while sending an embed and - will be ignored during serialization. + will be ignored during serialization. Expect this to be populated on + any received embed attached to a message event. """ width: typing.Optional[int] = attr.ib(default=None) - """The width of the image. + """The width of the image, if present and known, otherwise `None`. !!! note This field cannot be set by bots or webhooks while sending an embed and - will be ignored during serialization. + will be ignored during serialization. Expect this to be populated on + any received embed attached to a message event. """ @@ -112,30 +120,33 @@ class EmbedThumbnail: url: typing.Optional[str] = attr.ib( default=None, repr=True, ) - """The URL of the thumbnail.""" + """The URL of the thumbnail to display, or `None` if not present.""" proxy_url: typing.Optional[str] = attr.ib(default=None,) - """The proxied URL of the thumbnail. + """The proxied URL of the thumbnail, if present and known, otherwise `None`. !!! note This field cannot be set by bots or webhooks while sending an embed and - will be ignored during serialization. + will be ignored during serialization. Expect this to be populated on + any received embed attached to a message event. """ height: typing.Optional[int] = attr.ib(default=None) - """The height of the thumbnail. + """The height of the thumbnail, if present and known, otherwise `None`. !!! note This field cannot be set by bots or webhooks while sending an embed and - will be ignored during serialization. + will be ignored during serialization. Expect this to be populated on + any received embed attached to a message event. """ width: typing.Optional[int] = attr.ib(default=None) - """The width of the thumbnail. + """The width of the thumbnail, if present and known, otherwise `None`. !!! note This field cannot be set by bots or webhooks while sending an embed and - will be ignored during serialization. + will be ignored during serialization. Expect this to be populated on + any received embed attached to a message event. """ @@ -144,9 +155,9 @@ class EmbedVideo: """Represents an embed video. !!! note - This embed attached object cannot be sent by bots or webhooks while - sending an embed and therefore shouldn't be initiated like the other - embed objects. + This object cannot be set by bots or webhooks while sending an embed and + will be ignored during serialization. Expect this to be populated on + any received embed attached to a message event with a video attached. """ url: typing.Optional[str] = attr.ib(default=None, repr=True) @@ -164,9 +175,10 @@ class EmbedProvider: """Represents an embed provider. !!! note - This embed attached object cannot be sent by bots or webhooks while - sending an embed and therefore shouldn't be sent by your application. - You should still expect to receive these objects where appropriate. + This object cannot be set by bots or webhooks while sending an embed and + will be ignored during serialization. Expect this to be populated on + any received embed attached to a message event provided by an external + source. """ name: typing.Optional[str] = attr.ib(default=None, repr=True) @@ -178,23 +190,27 @@ class EmbedProvider: @attr.s(eq=True, hash=False, init=True, kw_only=True, slots=True) class EmbedAuthor: - """Represents an embed author.""" + """Represents an author of an embed.""" name: typing.Optional[str] = attr.ib(default=None, repr=True) - """The name of the author.""" + """The name of the author, or `None` if not specified.""" url: typing.Optional[str] = attr.ib(default=None, repr=True) - """The URL of the author.""" + """The URL that the author's name should act as a hyperlink to. + + This may be `None` if no hyperlink on the author's name is specified. + """ icon_url: typing.Optional[str] = attr.ib(default=None) - """The URL of the author icon.""" + """The URL of the author's icon, or `None` if not present.""" proxy_icon_url: typing.Optional[str] = attr.ib(default=None) - """The proxied URL of the author icon. + """The proxied URL of the author icon, or `None` if not present. !!! note This field cannot be set by bots or webhooks while sending an embed and - will be ignored during serialization. + will be ignored during serialization. Expect this to be populated on + any received embed attached to a message event. """ @@ -203,13 +219,13 @@ class EmbedField: """Represents a field in a embed.""" name: typing.Optional[str] = attr.ib(default=None, repr=True) - """The name of the field.""" + """The name of the field, or `None` if not present.""" value: typing.Optional[str] = attr.ib(default=None, repr=True) - """The value of the field.""" + """The value of the field, or `None` if not present.""" is_inline: bool = attr.ib(default=False, repr=True) - """Whether the field should display inline. Defaults to `False`.""" + """`True` if the field should display inline. Defaults to `False`.""" @attr.s(eq=True, hash=False, init=True, kw_only=True, slots=True) @@ -217,66 +233,138 @@ class Embed: """Represents an embed.""" title: typing.Optional[str] = attr.ib(default=None, repr=True) - """The title of the embed.""" + """The title of the embed, or `None` if not present.""" @title.validator def _title_check(self, _, value): # pylint:disable=unused-argument if value is not None and len(value) > _MAX_EMBED_TITLE: - raise ValueError(f"title must not exceed {_MAX_EMBED_TITLE} characters") + warnings.warn( + f"title must not exceed {_MAX_EMBED_TITLE} characters", category=errors.HikariWarning, + ) description: typing.Optional[str] = attr.ib(default=None) - """The description of the embed.""" + """The description of the embed, or `None` if not present.""" @description.validator def _description_check(self, _, value): # pylint:disable=unused-argument if value is not None and len(value) > _MAX_EMBED_DESCRIPTION: - raise ValueError(f"description must not exceed {_MAX_EMBED_DESCRIPTION} characters") + warnings.warn( + f"description must not exceed {_MAX_EMBED_DESCRIPTION} characters", category=errors.HikariWarning, + ) url: typing.Optional[str] = attr.ib(default=None) - """The URL of the embed.""" + """The URL of the embed, or `None` if not present.""" timestamp: typing.Optional[datetime.datetime] = attr.ib( default=None, repr=True, ) - """The timestamp of the embed.""" + """The timestamp of the embed, or `None` if not present. + + !!! note + If specified, this should be treated as a UTC timestamp. Ensure any + values you set here are either generated using + `datetime.datetime.utcnow`, or are treated as timezone-aware timestamps. + + You can generate a timezone-aware timestamp instead of a timezone-naive + one by specifying a timezone. Hikari will detect any difference in + timezone if the timestamp is non timezone-naive and fix it for you. + + # I am British, and it is June, so we are in daylight saving + # (UTC+1 or GMT+1, specifically). + >>> import datetime + + # This is timezone naive, notice no timezone in the repr that + # gets printed. This is no good to us, as Discord will interpret it + # as being in the future! + >>> datetime.datetime.now() + datetime.datetime(2020, 6, 5, 19, 29, 48, 281716) + + # Instead, this is a timezone-aware timestamp, and we can use this + # correctly. This will always return the current time in UTC. + >>> datetime.datetime.now(tz=datetime.timezone.utc) + datetime.datetime(2020, 6, 5, 18, 29, 56, 424744, tzinfo=datetime.timezone.utc) + + # We could instead use a custom timezone. Since the timezone is + # explicitly specified, Hikari will convert it to UTC for you when + # you send the embed. + >>> ... + + A library on PyPI called [tzlocal](...) also exists that may be useful + to you if you need to get your local timezone for any reason. + + >>> import datetime + >>> import tzlocal + + # Naive datetime that will show the wrong time on Discord. + >>> datetime.datetime.now() + datetime.datetime(2020, 6, 5, 19, 33, 21, 329950) + + # Timezone-aware datetime that uses my local timezone correctly. + >>> datetime.datetime.now(tz=tzlocal.get_localzone()) + datetime.datetime(2020, 6, 5, 19, 33, 40, 967939, tzinfo=) + + # Changing timezones. + >>> dt = datetime.datetime.now(tz=datetime.timezone.utc) + >>> print(dt) + datetime.datetime(2020, 6, 5, 18, 38, 27, 863990, tzinfo=datetime.timezone.utc) + >>> dt.astimezone(tzlocal.get_localzone()) + datetime.datetime(2020, 6, 5, 19, 38, 27, 863990, tzinfo=) + + ...this is not required, but you may find it more useful if using the + timestamps in debug logs, for example. + """ color: typing.Optional[colors.Color] = attr.ib( converter=attr.converters.optional(colors.Color.of), default=None, ) - """The colour of this embed's sidebar.""" + """The colour of this embed. - footer: typing.Optional[EmbedFooter] = attr.ib(default=None,) - """The footer of the embed.""" + If `None`, the default is used for the user's colour-scheme when viewing it + (off-white on light-theme and off-black on dark-theme). - image: typing.Optional[EmbedImage] = attr.ib(default=None,) - """The image of the embed.""" + !!! warning + Various bugs exist in the desktop client at the time of writing where + `#FFFFFF` is treated as as the default colour for your colour-scheme + rather than white. The current workaround appears to be using a slightly + off-white, such as `#DDDDDD` or `#FFFFFE` instead. + """ + + footer: typing.Optional[EmbedFooter] = attr.ib(default=None) + """The footer of the embed, if present, otherwise `None`.""" - thumbnail: typing.Optional[EmbedThumbnail] = attr.ib(default=None,) - """The thumbnail of the embed.""" + image: typing.Optional[EmbedImage] = attr.ib(default=None) + """The image to display in the embed, or `None` if not present.""" + + thumbnail: typing.Optional[EmbedThumbnail] = attr.ib(default=None) + """The thumbnail to show in the embed, or `None` if not present.""" video: typing.Optional[EmbedVideo] = attr.ib(default=None) - """The video of the embed. + """The video to show in the embed, or `None` if not present. !!! note - This field cannot be set by bots or webhooks while sending an embed and - will be ignored during serialization. + This object cannot be set by bots or webhooks while sending an embed and + will be ignored during serialization. Expect this to be populated on + any received embed attached to a message event with a video attached. """ provider: typing.Optional[EmbedProvider] = attr.ib(default=None) - """The provider of the embed. + """The provider of the embed, or `None if not present. !!! note - This field cannot be set by bots or webhooks while sending an embed and - will be ignored during serialization. + This object cannot be set by bots or webhooks while sending an embed and + will be ignored during serialization. Expect this to be populated on + any received embed attached to a message event with a custom provider + set. """ - author: typing.Optional[EmbedAuthor] = attr.ib(default=None,) - """The author of the embed.""" + author: typing.Optional[EmbedAuthor] = attr.ib(default=None) + """The author of the embed, or `None if not present.""" fields: typing.MutableSequence[EmbedField] = attr.ib(factory=list) - """The fields of the embed.""" + """The fields in the embed.""" - _assets_to_upload = attr.attrib(factory=list) + # Use a weakref so that clearing an image can pop the reference. + _assets_to_upload = attr.attrib(factory=weakref.WeakSet, repr=False) @property def assets_to_upload(self): @@ -293,36 +381,43 @@ def _extract_url(url) -> typing.Tuple[typing.Optional[str], typing.Optional[file def _maybe_ref_file_obj(self, file_obj) -> None: if file_obj is not None: - self._assets_to_upload.append(file_obj) + # Store a _copy_ so weakreffing works properly. + obj_copy = copy.copy(file_obj) + self._assets_to_upload.add(obj_copy) - def set_footer(self, *, text: str, icon: typing.Optional[str, files.BaseStream] = None) -> Embed: + def set_footer(self, *, text: typing.Optional[str], icon: typing.Optional[str, files.BaseStream] = None) -> Embed: """Set the embed footer. Parameters ---------- - text: str - The optional text to set for the footer. - icon: hikari.models.files.BaseStream | str | None - The optional `hikari.models.files.BaseStream` or URL to the image to set. + text : str or None + The optional text to set for the footer. If `None`, the content is + cleared. + icon : hikari.models.files.BaseStream or str or None + The optional `hikari.models.files.BaseStream` or URL to the image to + set. Returns ------- Embed This embed to allow method chaining. - - Raises - ------ - ValueError - If `text` exceeds 2048 characters or consists purely of whitespaces. """ - if not text.strip(): - raise ValueError("footer.text must not be empty or purely of whitespaces") - if len(text) > _MAX_FOOTER_TEXT: - raise ValueError(f"footer.text must not exceed {_MAX_FOOTER_TEXT} characters") - - icon, file = self._extract_url(icon) - self.footer = EmbedFooter(text=text, icon_url=icon) - self._maybe_ref_file_obj(file) + if text is not None: + # FIXME: move these validations to the dataclass. + if not text.strip(): + warnings.warn("footer.text must not be empty or purely of whitespaces", category=errors.HikariWarning) + elif len(text) > _MAX_FOOTER_TEXT: + warnings.warn( + f"footer.text must not exceed {_MAX_FOOTER_TEXT} characters", category=errors.HikariWarning + ) + + if icon is not None: + icon, file = self._extract_url(icon) + self.footer = EmbedFooter(text=text, icon_url=icon) + self._maybe_ref_file_obj(file) + elif self.footer is not None: + self.footer.icon_url = None + return self def set_image(self, image: typing.Optional[str, files.BaseStream] = None) -> Embed: @@ -330,17 +425,21 @@ def set_image(self, image: typing.Optional[str, files.BaseStream] = None) -> Emb Parameters ---------- - image: hikari.models.files.BaseStream | str | None - The optional `hikari.models.files.BaseStream` or URL to the image to set. + image : hikari.models.files.BaseStream or str or None + The optional `hikari.models.files.BaseStream` or URL to the image + to set. If `None`, the image is removed. Returns ------- Embed This embed to allow method chaining. """ - image, file = self._extract_url(image) - self.image = EmbedImage(url=image) - self._maybe_ref_file_obj(file) + if image is None: + self.image = None + else: + image, file = self._extract_url(image) + self.image = EmbedImage(url=image) + self._maybe_ref_file_obj(file) return self def set_thumbnail(self, image: typing.Optional[str, files.BaseStream] = None) -> Embed: @@ -348,17 +447,21 @@ def set_thumbnail(self, image: typing.Optional[str, files.BaseStream] = None) -> Parameters ---------- - image: hikari.models.files.BaseStream | str | None - The optional `hikari.models.files.BaseStream` or URL to the image to set. + image: hikari.models.files.BaseStream or str or None + The optional `hikari.models.files.BaseStream` or URL to the image + to set. If `None`, the thumbnail is removed. Returns ------- Embed This embed to allow method chaining. """ - image, file = self._extract_url(image) - self.thumbnail = EmbedThumbnail(url=image) - self._maybe_ref_file_obj(file) + if image is None: + self.thumbnail = None + else: + image, file = self._extract_url(image) + self.thumbnail = EmbedThumbnail(url=image) + self._maybe_ref_file_obj(file) return self def set_author( @@ -372,31 +475,35 @@ def set_author( Parameters ---------- - name: str | None - The optional authors name. - url: str | None + name: str or None + The optional authors name to display. + url: str or None The optional URL to make the author text link to. - icon: hikari.models.files.BaseStream | str | None - The optional `hikari.models.files.BaseStream` or URL to the icon to set. + icon: hikari.models.files.BaseStream or str or None + The optional `hikari.models.files.BaseStream` or URL to the icon + to set. Returns ------- Embed This embed to allow method chaining. - - Raises - ------ - ValueError - If `name` exceeds 256 characters or consists purely of whitespaces. """ - if name is not None and not name.strip(): - raise ValueError("author.name must not be empty or purely of whitespaces") - if name is not None and len(name) > _MAX_AUTHOR_NAME: - raise ValueError(f"author.name must not exceed {_MAX_AUTHOR_NAME} characters") - - icon, icon_file = self._extract_url(icon) - self.author = EmbedAuthor(name=name, url=url, icon_url=icon) - self._maybe_ref_file_obj(icon_file) + if name is not None: + # TODO: move validation to dataclass + if name is not None and not name.strip(): + warnings.warn("author.name must not be empty or purely of whitespaces", category=errors.HikariWarning) + if name is not None and len(name) > _MAX_AUTHOR_NAME: + warnings.warn( + f"author.name must not exceed {_MAX_AUTHOR_NAME} characters", category=errors.HikariWarning + ) + + if icon is not None: + icon, icon_file = self._extract_url(icon) + self.author = EmbedAuthor(name=name, url=url, icon_url=icon) + self._maybe_ref_file_obj(icon_file) + elif self.author is not None: + self.author.icon_url = None + return self def add_field(self, *, name: str, value: str, inline: bool = False, index: typing.Optional[int] = None) -> Embed: @@ -405,46 +512,58 @@ def add_field(self, *, name: str, value: str, inline: bool = False, index: typin Parameters ---------- name: str - The fields name (title). + The field name (title). value: str - The fields value. + The field value. inline: bool - Whether to set the field to behave as if it were inline or not. Defaults to `False`. - index: int | None - The optional index to insert the field at. If `None`, it will append to the end. + If `True`, multiple consecutive fields may be displayed on the same + line. This is not guaranteed behaviour and only occurs if viewing + on desktop clients. Defaults to `False`. + index: int or None + The optional index to insert the field at. If `None`, it will append + to the end. Returns ------- Embed This embed to allow method chaining. - - Raises - ------ - ValueError - If `title` exceeds 256 characters or `value` exceeds 2048 characters; if - the `name` or `value` consist purely of whitespace, or be zero characters in size; - 25 fields are present in the embed. """ index = index if index is not None else len(self.fields) if len(self.fields) >= _MAX_EMBED_FIELDS: - raise ValueError(f"no more than {_MAX_EMBED_FIELDS} fields can be stored") + warnings.warn(f"no more than {_MAX_EMBED_FIELDS} fields can be stored", category=errors.HikariWarning) + # TODO: move to dataclass. if not name.strip(): - raise ValueError("field.name must not be empty or purely of whitespaces") + warnings.warn("field.name must not be empty or purely of whitespaces", category=errors.HikariWarning) if len(name) > _MAX_FIELD_NAME: - raise ValueError(f"field.name must not exceed {_MAX_FIELD_NAME} characters") + warnings.warn(f"field.name must not exceed {_MAX_FIELD_NAME} characters", category=errors.HikariWarning) if not value.strip(): - raise ValueError("field.value must not be empty or purely of whitespaces") + warnings.warn("field.value must not be empty or purely of whitespaces", category=errors.HikariWarning) if len(value) > _MAX_FIELD_VALUE: - raise ValueError(f"field.value must not exceed {_MAX_FIELD_VALUE} characters") + warnings.warn(f"field.value must not exceed {_MAX_FIELD_VALUE} characters", category=errors.HikariWarning) self.fields.insert(index, EmbedField(name=name, value=value, is_inline=inline)) return self - def edit_field(self, index: int, *, name: str = ..., value: str = ..., inline: bool = ...) -> Embed: + # FIXME: use undefined.Undefined rather than `...` + def edit_field(self, index: int, /, *, name: str = ..., value: str = ..., inline: bool = ...) -> Embed: """Edit a field in this embed at the given index. + Unless you specify the attribute to change, it will not be changed. For + example, you can change a field value but not the field name + by simply specifying that parameter only. + + ```py + >>> embed = Embed() + >>> embed.add_field(name="foo", value="bar") + >>> embed.edit_field(0, value="baz") + >>> print(embed.fields[0].name) + foo + >>> print(embed.fields[0].value) + baz + ``` + Parameters ---------- index: int @@ -461,24 +580,17 @@ def edit_field(self, index: int, *, name: str = ..., value: str = ..., inline: b ------- Embed This embed to allow method chaining. - - Raises - ------ - IndexError - If you referred to an index that doesn't exist. - ValueError - If `title` exceeds 256 characters or `value` exceeds 2048 characters; if - the `name` or `value` consist purely of whitespace, or be zero characters in size. """ + # TODO: remove these checks entirely, they will be covered by the validation in the data class. if name is not ... and not name.strip(): - raise ValueError("field.name must not be empty or purely of whitespaces") + warnings.warn("field.name must not be empty or purely of whitespaces", category=errors.HikariWarning) if name is not ... and len(name.strip()) > _MAX_FIELD_NAME: - raise ValueError(f"field.name must not exceed {_MAX_FIELD_NAME} characters") + warnings.warn(f"field.name must not exceed {_MAX_FIELD_NAME} characters", category=errors.HikariWarning) if value is not ... and not value.strip(): - raise ValueError("field.value must not be empty or purely of whitespaces") + warnings.warn("field.value must not be empty or purely of whitespaces", category=errors.HikariWarning) if value is not ... and len(value) > _MAX_FIELD_VALUE: - raise ValueError(f"field.value must not exceed {_MAX_FIELD_VALUE} characters") + warnings.warn(f"field.value must not exceed {_MAX_FIELD_VALUE} characters", category=errors.HikariWarning) field = self.fields[index] @@ -507,24 +619,3 @@ def remove_field(self, index: int) -> Embed: """ del self.fields[index] return self - - @staticmethod - def _safe_len(item) -> int: - return len(item) if item is not None else 0 - - def _check_total_length(self) -> None: - total_size = self._safe_len(self.title) - total_size += self._safe_len(self.description) - total_size += self._safe_len(self.author.name) if self.author is not None else 0 - total_size += len(self.footer.text) if self.footer is not None else 0 - - for field in self.fields: - total_size += len(field.name) - total_size += len(field.value) - - if total_size > _MAX_EMBED_SIZE: - raise ValueError("Total characters in an embed can not exceed {_MAX_EMBED_SIZE}") - - def serialize(self) -> data_binding.JSONObject: - self._check_total_length() - return super().serialize() From 253d29a3a6919dd670f3f648254786ba76ad3c0a Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Fri, 5 Jun 2020 20:45:09 +0100 Subject: [PATCH 460/922] Fixed bug that forced you to use manual sharding. --- hikari/impl/bot.py | 4 ++-- hikari/impl/gateway_zookeeper.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/hikari/impl/bot.py b/hikari/impl/bot.py index 1c15a4c6b3..e2b8ff734d 100644 --- a/hikari/impl/bot.py +++ b/hikari/impl/bot.py @@ -158,8 +158,8 @@ def __init__( logging_level: typing.Optional[str] = "INFO", rest_version: int = 6, rest_url: typing.Union[undefined.Undefined, str] = undefined.Undefined(), - shard_ids: typing.Optional[typing.Set[int]], - shard_count: typing.Optional[int], + shard_ids: typing.Union[typing.Set[int], undefined.Undefined] = undefined.Undefined(), + shard_count: typing.Union[int, undefined.Undefined] = undefined.Undefined(), token: str, ): self._logger = klass.get_logger(self) diff --git a/hikari/impl/gateway_zookeeper.py b/hikari/impl/gateway_zookeeper.py index e4ea1ec396..2422a76e8c 100644 --- a/hikari/impl/gateway_zookeeper.py +++ b/hikari/impl/gateway_zookeeper.py @@ -143,7 +143,7 @@ def __init__( token: str, version: int, ) -> None: - if undefined.Undefined.count(shard_ids, shard_count): + if undefined.Undefined.count(shard_ids, shard_count) == 1: raise TypeError("You must provide values for both shard_ids and shard_count, or neither.") if not isinstance(shard_ids, undefined.Undefined): if not shard_ids: From 5c01381597f3852ce3705b485435849b36e0598c Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Fri, 5 Jun 2020 20:48:50 +0100 Subject: [PATCH 461/922] Fixed exceptions in first-order events not being logged. --- hikari/impl/event_manager_core.py | 1 + 1 file changed, 1 insertion(+) diff --git a/hikari/impl/event_manager_core.py b/hikari/impl/event_manager_core.py index 01b6d3c809..9a50a22a8a 100644 --- a/hikari/impl/event_manager_core.py +++ b/hikari/impl/event_manager_core.py @@ -184,6 +184,7 @@ async def _invoke_callback(self, callback: _CallbackT, event: _EventT) -> None: if base.is_no_catch_event(event): self.logger.error("an exception occurred handling an event, but it has been ignored", exc_info=ex) else: + self.logger.error("an exception occurred handling an event", exc_info=ex) await self.dispatch(other.ExceptionEvent(exception=ex, event=event, callback=callback)) def dispatch(self, event: base.HikariEvent) -> aio.Future[typing.Any]: From 462b5bbd679e84c6b3087e3ec91268a104d48ad6 Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Fri, 5 Jun 2020 21:00:42 +0100 Subject: [PATCH 462/922] Simplified event tracebacks. --- hikari/impl/event_manager_core.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/hikari/impl/event_manager_core.py b/hikari/impl/event_manager_core.py index 9a50a22a8a..5f44878e16 100644 --- a/hikari/impl/event_manager_core.py +++ b/hikari/impl/event_manager_core.py @@ -181,10 +181,13 @@ async def _invoke_callback(self, callback: _CallbackT, event: _EventT) -> None: await result except Exception as ex: + # Skip the first frame in logs, we don't care for it. + trio = type(ex), ex, ex.__traceback__.tb_next + if base.is_no_catch_event(event): - self.logger.error("an exception occurred handling an event, but it has been ignored", exc_info=ex) + self.logger.error("an exception occurred handling an event, but it has been ignored", exc_info=trio) else: - self.logger.error("an exception occurred handling an event", exc_info=ex) + self.logger.error("an exception occurred handling an event", exc_info=trio) await self.dispatch(other.ExceptionEvent(exception=ex, event=event, callback=callback)) def dispatch(self, event: base.HikariEvent) -> aio.Future[typing.Any]: From bb16c5bba7553ba95b90cfb60b00e78f1fa2f408 Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Fri, 5 Jun 2020 21:29:13 +0100 Subject: [PATCH 463/922] Fixed bugs with message sending and typing indicator, cleaned up '...'-removal techdebt. --- hikari/models/bases.py | 10 ++- hikari/models/messages.py | 148 ++++++++++++-------------------------- hikari/net/rest.py | 1 + hikari/net/rest_utils.py | 6 +- 4 files changed, 61 insertions(+), 104 deletions(-) diff --git a/hikari/models/bases.py b/hikari/models/bases.py index 66e3a80b23..c60231d99c 100644 --- a/hikari/models/bases.py +++ b/hikari/models/bases.py @@ -44,7 +44,15 @@ class Entity(abc.ABC): methods directly. """ - _app: typing.Optional[app_.IApp] = attr.ib(default=None, repr=False, eq=False, hash=False) + _app: typing.Union[ + None, + app_.IApp, + app_.IGatewayZookeeper, + app_.IGatewayConsumer, + app_.IGatewayDispatcher, + app_.IRESTApp, + app_.IBot, + ] = attr.ib(default=None, repr=False, eq=False, hash=False) """The client application that models may use for procedures.""" def __init__(self, app: app_.IApp) -> None: diff --git a/hikari/models/messages.py b/hikari/models/messages.py index 6bbd097d9f..3bc55cec5d 100644 --- a/hikari/models/messages.py +++ b/hikari/models/messages.py @@ -38,6 +38,7 @@ from hikari.models import bases from hikari.models import files as files_ +from hikari.utilities import undefined if typing.TYPE_CHECKING: import datetime @@ -314,14 +315,14 @@ async def fetch_channel(self) -> channels.PartialChannel: hikari.errors.NotFound If the channel this message was created in does not exist. """ - return await self._app.rest.fetch_channel(channel=self.channel_id) + return await self._app.rest.fetch_channel(self.channel_id) async def edit( # pylint:disable=line-too-long self, - content: str = ..., + text: typing.Union[undefined.Undefined, str, None] = undefined.Undefined(), *, - embed: embeds_.Embed = ..., - mentions_everyone: bool = True, + embed: typing.Union[undefined.Undefined, embeds_.Embed, None] = undefined.Undefined(), + mentions_everyone: bool = False, user_mentions: typing.Union[ typing.Collection[typing.Union[snowflake.Snowflake, int, str, users.User]], bool ] = True, @@ -336,18 +337,20 @@ async def edit( # pylint:disable=line-too-long Parameters ---------- - content : str - If specified, the message content to set on the message. - embed : hikari.models.embeds.Embed - If specified, the embed object to set on the message. + text : str or hikari.utilities.undefined.Undefined or None + If specified, the message text to set on the message. If `None`, + then the content is removed if already present. + embed : hikari.models.embeds.Embed or hikari.utilities.undefined.Undefined or None + If specified, the embed object to set on the message. If `None`, + then the embed is removed if already present. mentions_everyone : bool Whether `@everyone` and `@here` mentions should be resolved by - discord and lead to actual pings, defaults to `True`. - user_mentions : typing.Collection[hikari.models.users.User | hikari.models.snowflake.Snowflake | int | str] | bool + discord and lead to actual pings, defaults to `False`. + user_mentions : typing.Collection[hikari.models.users.User or hikari.models.snowflake.Snowflake or int or str] or bool Either an array of user objects/IDs to allow mentions for, `True` to allow all user mentions or `False` to block all user mentions from resolving, defaults to `True`. - role_mentions: typing.Collection[hikari.models.guilds.Role | hikari.models.snowflake.Snowflake | int | str] | bool + role_mentions: typing.Collection[hikari.models.guilds.Role or hikari.models.snowflake.Snowflake or int or str] or bool Either an array of guild role objects/IDs to allow mentions for, `True` to allow all role mentions or `False` to block all role mentions from resolving, defaults to `True`. @@ -377,87 +380,58 @@ async def edit( # pylint:disable=line-too-long If more than 100 unique objects/entities are passed for `role_mentions` or `user_mentions`. """ - return await self._app.rest.update_message( + return await self._app.rest.edit_message( message=self.id, channel=self.channel_id, - content=content, + text=text, embed=embed, mentions_everyone=mentions_everyone, user_mentions=user_mentions, role_mentions=role_mentions, ) - async def safe_edit( - self, - *, - content: str = ..., - embed: embeds_.Embed = ..., - mentions_everyone: bool = True, - user_mentions: typing.Union[ - typing.Collection[typing.Union[snowflake.Snowflake, int, str, users.User]], bool - ] = False, - role_mentions: typing.Union[ - typing.Collection[typing.Union[snowflake.Snowflake, int, str, guilds.Role]], bool - ] = False, - ) -> Message: - """Edit this message. - - This is the same as `edit`, but with all defaults set to prevent any - mentions from working by default. - """ - return await self._app.rest.safe_update_message( - message=self.id, - channel=self.channel_id, - content=content, - embed=embed, - mentions_everyone=mentions_everyone, - user_mentions=user_mentions, - role_mentions=role_mentions, - ) - - # FIXME: use undefined, not ... async def reply( # pylint:disable=line-too-long self, - content: str = ..., + text: typing.Union[undefined.Undefined, str] = undefined.Undefined(), *, - embed: embeds_.Embed = ..., - files: typing.Sequence[files_.BaseStream] = ..., - mentions_everyone: bool = True, + embed: typing.Union[undefined.Undefined, embeds_.Embed] = undefined.Undefined(), + attachments: typing.Sequence[files_.BaseStream] = undefined.Undefined(), + mentions_everyone: bool = False, user_mentions: typing.Union[ typing.Collection[typing.Union[snowflake.Snowflake, int, str, users.User]], bool ] = True, role_mentions: typing.Union[ typing.Collection[typing.Union[snowflake.Snowflake, int, str, guilds.Role]], bool ] = True, - nonce: str = ..., - tts: bool = ..., + nonce: typing.Union[undefined.Undefined, str] = undefined.Undefined(), + tts: typing.Union[undefined.Undefined, bool] = undefined.Undefined(), ) -> Message: """Create a message in the channel this message belongs to. Parameters ---------- - content : str - If specified, the message content to send with the message. - nonce : str + text : str or hikari.utilities.undefined.Undefined + If specified, the message text to send with the message. + nonce : str or hikari.utilities.undefined.Undefined If specified, an optional ID to send for opportunistic message creation. This doesn't serve any real purpose for general use, and can usually be ignored. - tts : bool + tts : bool or hikari.utilities.undefined.Undefined If specified, whether the message will be sent as a TTS message. - files : typing.Sequence[hikari.models.files.BaseStream] - If specified, a sequence of files to upload, if desired. Should be - between 1 and 10 objects in size (inclusive), also including embed - attachments. - embed : hikari.models.embeds.Embed + attachments : typing.Sequence[hikari.models.files.BaseStream] + If specified, a sequence of attachments to upload, if desired. + Should be between 1 and 10 objects in size (inclusive), also + including embed attachments. + embed : hikari.models.embeds.Embed or hikari.utilities.undefined.Undefined If specified, the embed object to send with the message. mentions_everyone : bool Whether `@everyone` and `@here` mentions should be resolved by - discord and lead to actual pings, defaults to `True`. - user_mentions : typing.Collection[hikari.models.users.User | hikari.models.snowflake.Snowflake | int | str] | bool + discord and lead to actual pings, defaults to `False`. + user_mentions : typing.Collection[hikari.models.users.User or hikari.models.snowflake.Snowflake or int or str] or bool Either an array of user objects/IDs to allow mentions for, `True` to allow all user mentions or `False` to block all user mentions from resolving, defaults to `True`. - role_mentions: typing.Collection[hikari.models.guilds.Role | hikari.models.snowflake.Snowflake | int | str] | bool + role_mentions: typing.Collection[hikari.models.guilds.Role or hikari.models.snowflake.Snowflake or int or str] or bool Either an array of guild role objects/IDs to allow mentions for, `True` to allow all role mentions or `False` to block all role mentions from resolving, defaults to `True`. @@ -489,43 +463,10 @@ async def reply( # pylint:disable=line-too-long """ return await self._app.rest.create_message( channel=self.channel_id, - content=content, - nonce=nonce, - tts=tts, - files=files, - embed=embed, - mentions_everyone=mentions_everyone, - user_mentions=user_mentions, - role_mentions=role_mentions, - ) - - async def safe_reply( - self, - *, - content: str = ..., - embed: embeds_.Embed = ..., - files: typing.Sequence[files_.BaseStream] = ..., - mentions_everyone: bool = True, - user_mentions: typing.Union[ - typing.Collection[typing.Union[snowflake.Snowflake, int, str, users.User]], bool - ] = False, - role_mentions: typing.Union[ - typing.Collection[typing.Union[snowflake.Snowflake, int, str, guilds.Role]], bool - ] = False, - nonce: str = ..., - tts: bool = ..., - ) -> Message: - """Reply to a message. - - This is the same as `reply`, but with all defaults set to prevent any - mentions from working by default. - """ - return await self._app.rest.safe_create_message( - channel=self.channel_id, - content=content, + text=text, nonce=nonce, tts=tts, - files=files, + attachments=attachments, embed=embed, mentions_everyone=mentions_everyone, user_mentions=user_mentions, @@ -543,14 +484,14 @@ async def delete(self) -> None: hikari.errors.Forbidden If you lack the permissions to delete the message. """ - await self._app.rest.delete_messages(channel=self.channel_id, message=self.id) + await self._app.rest.delete_message(self.channel_id, self.id) async def add_reaction(self, emoji: typing.Union[str, emojis_.Emoji]) -> None: r"""Add a reaction to this message. Parameters ---------- - emoji : hikari.models.emojis.Emoji | str + emoji : hikari.models.emojis.Emoji or str The emoji to add. Examples @@ -590,9 +531,9 @@ async def remove_reaction( Parameters ---------- - emoji : hikari.models.emojis.Emoji | str + emoji : hikari.models.emojis.Emoji or str The emoji to remove. - user : hikari.models.users.User | None + user : hikari.models.users.User or None The user of the reaction to remove. If `None`, then the bot's reaction is removed instead. @@ -625,14 +566,14 @@ async def remove_reaction( If any invalid snowflake IDs are passed; a snowflake may be invalid due to it being outside of the range of a 64 bit integer. """ - await self._app.rest.remove_reaction(channel=self.channel_id, message=self.id, emoji=emoji, user=user) + await self._app.rest.delete_reaction(channel=self.channel_id, message=self.id, emoji=emoji, user=user) async def remove_all_reactions(self, emoji: typing.Optional[typing.Union[str, emojis_.Emoji]] = None) -> None: r"""Remove all users' reactions for a specific emoji from the message. Parameters ---------- - emoji : hikari.models.emojis.Emoji | str | None + emoji : hikari.models.emojis.Emoji or str or None The emoji to remove all reactions for. If not specified, or `None`, then all emojis are removed. @@ -657,4 +598,7 @@ async def remove_all_reactions(self, emoji: typing.Optional[typing.Union[str, em If any invalid snowflake IDs are passed; a snowflake may be invalid due to it being outside of the range of a 64 bit integer. """ - await self._app.rest.remove_all_reactions(channel=self.channel_id, message=self.id, emoji=emoji) + if emoji is None: + await self._app.rest.delete_all_reactions(channel=self.channel_id, message=self.id) + else: + await self._app.rest.delete_all_reactions_for_emoji(channel=self.channel_id, message=self.id, emoji=emoji) diff --git a/hikari/net/rest.py b/hikari/net/rest.py index edf4891380..8be5c6b9e3 100644 --- a/hikari/net/rest.py +++ b/hikari/net/rest.py @@ -1254,6 +1254,7 @@ async def delete_message( async def delete_messages( self, channel: typing.Union[channels.GuildTextChannel, bases.UniqueObject], + /, *messages: typing.Union[messages_.Message, bases.UniqueObject], ) -> None: """Bulk-delete between 2 and 100 messages from the given guild channel. diff --git a/hikari/net/rest_utils.py b/hikari/net/rest_utils.py index 0566bd44d4..b46df3c8f5 100644 --- a/hikari/net/rest_utils.py +++ b/hikari/net/rest_utils.py @@ -73,6 +73,9 @@ def __await__(self) -> typing.Generator[None, typing.Any, None]: route = routes.POST_CHANNEL_TYPING.compile(channel=self._channel) yield from self._request_call(route).__await__() + def __enter__(self) -> typing.NoReturn: + raise TypeError("Use 'async with' rather than 'with' when triggering the typing indicator.") + async def __aenter__(self) -> None: if self._task is not None: raise TypeError("cannot enter a typing indicator context more than once.") @@ -85,7 +88,8 @@ async def __aexit__(self, ex_t: typing.Type[Exception], ex_v: Exception, exc_tb: async def _keep_typing(self) -> None: with contextlib.suppress(asyncio.CancelledError): - await asyncio.gather(self, asyncio.sleep(9.9), return_exceptions=True) + while True: + await asyncio.gather(self, asyncio.sleep(9.9), return_exceptions=True) @attr.s(auto_attribs=True, kw_only=True, slots=True) From 55a83adce347dcd154075ac861c2f4996b6803af Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Sat, 6 Jun 2020 11:08:55 +0100 Subject: [PATCH 464/922] Added bucket UTs. --- tests/hikari/net/test_buckets.py | 290 +++++++++++++++++++++++++++++++ 1 file changed, 290 insertions(+) diff --git a/tests/hikari/net/test_buckets.py b/tests/hikari/net/test_buckets.py index 1c1502a5ca..5dcb3f23c2 100644 --- a/tests/hikari/net/test_buckets.py +++ b/tests/hikari/net/test_buckets.py @@ -16,3 +16,293 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . +import asyncio +import datetime +import time + +import mock +import pytest + +from hikari.net import buckets +from hikari.net import routes +from tests.hikari import _helpers + + +class TestRESTBucket: + @pytest.fixture + def template(self): + return routes.Route("GET", "/foo/bar") + + @pytest.fixture + def compiled_route(self, template): + return routes.CompiledRoute(template, "/foo/bar", "1a2b3c") + + @pytest.mark.parametrize("name", ["spaghetti", buckets.UNKNOWN_HASH]) + def test_is_unknown(self, name, compiled_route): + with buckets.RESTBucket(name, compiled_route) as rl: + assert rl.is_unknown is (name == buckets.UNKNOWN_HASH) + + def test_update_rate_limit(self, compiled_route): + with buckets.RESTBucket(__name__, compiled_route) as rl: + rl.remaining = 1 + rl.limit = 2 + rl.reset_at = 3 + rl.period = 2 + + with mock.patch("time.perf_counter", return_value=4.20): + rl.update_rate_limit(9, 18, 27) + + assert rl.remaining == 9 + assert rl.limit == 18 + assert rl.reset_at == 27 + assert rl.period == 27 - 4.20 + + @pytest.mark.parametrize("name", ["spaghetti", buckets.UNKNOWN_HASH]) + def test_drip(self, name, compiled_route): + with buckets.RESTBucket(name, compiled_route) as rl: + rl.remaining = 1 + rl.drip() + assert rl.remaining == 0 if name != buckets.UNKNOWN_HASH else 1 + + +class TestRESTBucketManager: + @pytest.mark.asyncio + async def test_close_closes_all_buckets(self): + class MockBucket: + def __init__(self): + self.close = mock.MagicMock() + + buckets_array = [MockBucket() for _ in range(30)] + + mgr = buckets.RESTBucketManager() + # noinspection PyFinal + mgr.real_hashes_to_buckets = {f"blah{i}": bucket for i, bucket in enumerate(buckets_array)} + + mgr.close() + + for i, bucket in enumerate(buckets_array): + bucket.close.assert_called_once(), i + + @pytest.mark.asyncio + async def test_close_sets_closed_event(self): + mgr = buckets.RESTBucketManager() + assert not mgr.closed_event.is_set() + mgr.close() + assert mgr.closed_event.is_set() + + @pytest.mark.asyncio + async def test_start(self): + with buckets.RESTBucketManager() as mgr: + assert mgr.gc_task is None + mgr.start() + mgr.start() + mgr.start() + assert mgr.gc_task is not None + + @pytest.mark.asyncio + async def test_exit_closes(self): + with mock.patch.object(buckets.RESTBucketManager, "close") as close: + with mock.patch.object(buckets.RESTBucketManager, "gc") as gc: + with buckets.RESTBucketManager() as mgr: + mgr.start(0.01, 32) + gc.assert_called_once_with(0.01, 32) + close.assert_called() + + @pytest.mark.asyncio + async def test_gc_polls_until_closed_event_set(self): + # This is shit, but it is good shit. + with buckets.RESTBucketManager() as mgr: + mgr.start(0.01) + assert mgr.gc_task is not None + assert not mgr.gc_task.done() + await asyncio.sleep(0.1) + assert mgr.gc_task is not None + assert not mgr.gc_task.done() + await asyncio.sleep(0.1) + mgr.closed_event.set() + assert mgr.gc_task is not None + assert not mgr.gc_task.done() + task = mgr.gc_task + await asyncio.sleep(0.1) + assert mgr.gc_task is None + assert task.done() + + @pytest.mark.asyncio + async def test_gc_calls_do_pass(self): + with _helpers.unslot_class(buckets.RESTBucketManager)() as mgr: + mgr.do_gc_pass = mock.MagicMock() + mgr.start(0.01, 33) + try: + await asyncio.sleep(0.1) + mgr.do_gc_pass.assert_called_with(33) + finally: + mgr.gc_task.cancel() + + @pytest.mark.asyncio + async def test_do_gc_pass_any_buckets_that_are_empty_but_still_rate_limited_are_kept_alive(self): + with _helpers.unslot_class(buckets.RESTBucketManager)() as mgr: + bucket = mock.MagicMock() + bucket.is_empty = True + bucket.is_unknown = False + bucket.reset_at = time.perf_counter() + 999999999999999999999999999 + + mgr.real_hashes_to_buckets["foobar"] = bucket + + mgr.do_gc_pass(0) + + assert "foobar" in mgr.real_hashes_to_buckets + bucket.close.assert_not_called() + + @pytest.mark.asyncio + async def test_do_gc_pass_any_buckets_that_are_empty_but_not_rate_limited_and_not_expired_are_kept_alive(self): + with _helpers.unslot_class(buckets.RESTBucketManager)() as mgr: + bucket = mock.MagicMock() + bucket.is_empty = True + bucket.is_unknown = False + bucket.reset_at = time.perf_counter() + + mgr.real_hashes_to_buckets["foobar"] = bucket + + mgr.do_gc_pass(10) + + assert "foobar" in mgr.real_hashes_to_buckets + bucket.close.assert_not_called() + + @pytest.mark.asyncio + async def test_do_gc_pass_any_buckets_that_are_empty_but_not_rate_limited_and_expired_are_closed(self): + with _helpers.unslot_class(buckets.RESTBucketManager)() as mgr: + bucket = mock.MagicMock() + bucket.is_empty = True + bucket.is_unknown = False + bucket.reset_at = time.perf_counter() - 999999999999999999999999999 + + mgr.real_hashes_to_buckets["foobar"] = bucket + + mgr.do_gc_pass(0) + + assert "foobar" not in mgr.real_hashes_to_buckets + bucket.close.assert_called_once() + + @pytest.mark.asyncio + async def test_do_gc_pass_any_buckets_that_are_not_empty_are_kept_alive(self): + with _helpers.unslot_class(buckets.RESTBucketManager)() as mgr: + bucket = mock.MagicMock() + bucket.is_empty = False + bucket.is_unknown = True + bucket.reset_at = time.perf_counter() + + mgr.real_hashes_to_buckets["foobar"] = bucket + + mgr.do_gc_pass(0) + + assert "foobar" in mgr.real_hashes_to_buckets + bucket.close.assert_not_called() + + @pytest.mark.asyncio + async def test_acquire_route_when_not_in_routes_to_real_hashes_makes_new_bucket_using_initial_hash(self): + with buckets.RESTBucketManager() as mgr: + route = mock.MagicMock() + route.create_real_bucket_hash = mock.MagicMock(wraps=lambda intial_hash: intial_hash + ";bobs") + + # This isn't a coroutine; why would I await it? + # noinspection PyAsyncCall + mgr.acquire(route) + + assert "UNKNOWN;bobs" in mgr.real_hashes_to_buckets + assert isinstance(mgr.real_hashes_to_buckets["UNKNOWN;bobs"], buckets.RESTBucket) + + @pytest.mark.asyncio + async def test_acquire_route_when_not_in_routes_to_real_hashes_caches_route(self): + with buckets.RESTBucketManager() as mgr: + route = mock.MagicMock() + route.create_real_bucket_hash = mock.MagicMock(wraps=lambda intial_hash: intial_hash + ";bobs") + + # This isn't a coroutine; why would I await it? + # noinspection PyAsyncCall + mgr.acquire(route) + + assert mgr.routes_to_hashes[route.route] == "UNKNOWN" + + @pytest.mark.asyncio + async def test_acquire_route_when_route_cached_already_obtains_hash_from_route_and_bucket_from_hash(self): + with buckets.RESTBucketManager() as mgr: + route = mock.MagicMock() + route.create_real_bucket_hash = mock.MagicMock(return_value="eat pant;1234") + bucket = mock.MagicMock() + mgr.routes_to_hashes[route] = "eat pant" + mgr.real_hashes_to_buckets["eat pant;1234"] = bucket + + # This isn't a coroutine; why would I await it? + # noinspection PyAsyncCall + mgr.acquire(route) + + # yes i test this twice, sort of. no, there isn't another way to verify this. sue me. + bucket.acquire.assert_called_once() + + @pytest.mark.asyncio + async def test_acquire_route_returns_acquired_future(self): + with buckets.RESTBucketManager() as mgr: + route = mock.MagicMock() + + bucket = mock.MagicMock() + with mock.patch.object(buckets, "RESTBucket", return_value=bucket): + route.create_real_bucket_hash = mock.MagicMock(wraps=lambda intial_hash: intial_hash + ";bobs") + + f = mgr.acquire(route) + assert f is bucket.acquire() + + @pytest.mark.asyncio + async def test_acquire_route_returns_acquired_future_for_new_bucket(self): + with buckets.RESTBucketManager() as mgr: + route = mock.MagicMock() + route.create_real_bucket_hash = mock.MagicMock(return_value="eat pant;bobs") + bucket = mock.MagicMock() + mgr.routes_to_hashes[route.route] = "eat pant" + mgr.real_hashes_to_buckets["eat pant;bobs"] = bucket + + f = mgr.acquire(route) + assert f is bucket.acquire() + + @pytest.mark.asyncio + async def test_update_rate_limits_if_wrong_bucket_hash_reroutes_route(self): + with buckets.RESTBucketManager() as mgr: + route = mock.MagicMock() + route.create_real_bucket_hash = mock.MagicMock(wraps=lambda intial_hash: intial_hash + ";bobs") + mgr.routes_to_hashes[route.route] = "123" + mgr.update_rate_limits(route, "blep", 22, 23, datetime.datetime.now(), datetime.datetime.now()) + assert mgr.routes_to_hashes[route.route] == "blep" + assert isinstance(mgr.real_hashes_to_buckets["blep;bobs"], buckets.RESTBucket) + + @pytest.mark.asyncio + async def test_update_rate_limits_if_right_bucket_hash_does_nothing_to_hash(self): + with buckets.RESTBucketManager() as mgr: + route = mock.MagicMock() + route.create_real_bucket_hash = mock.MagicMock(wraps=lambda intial_hash: intial_hash + ";bobs") + mgr.routes_to_hashes[route.route] = "123" + bucket = mock.MagicMock() + mgr.real_hashes_to_buckets["123;bobs"] = bucket + mgr.update_rate_limits(route, "123", 22, 23, datetime.datetime.now(), datetime.datetime.now()) + assert mgr.routes_to_hashes[route.route] == "123" + assert mgr.real_hashes_to_buckets["123;bobs"] is bucket + + @pytest.mark.asyncio + async def test_update_rate_limits_updates_params(self): + with buckets.RESTBucketManager() as mgr: + route = mock.MagicMock() + route.create_real_bucket_hash = mock.MagicMock(wraps=lambda intial_hash: intial_hash + ";bobs") + mgr.routes_to_hashes[route.route] = "123" + bucket = mock.MagicMock() + mgr.real_hashes_to_buckets["123;bobs"] = bucket + date = datetime.datetime.now().replace(year=2004) + reset_at = datetime.datetime.now() + + with mock.patch("time.perf_counter", return_value=27): + expect_reset_at_monotonic = 27 + (reset_at - date).total_seconds() + mgr.update_rate_limits(route, "123", 22, 23, date, reset_at) + bucket.update_rate_limit.assert_called_once_with(22, 23, expect_reset_at_monotonic) + + @pytest.mark.parametrize(("gc_task", "is_started"), [(None, False), (object(), True)]) + def test_is_started(self, gc_task, is_started): + with buckets.RESTBucketManager() as mgr: + mgr.gc_task = gc_task + assert mgr.is_started is is_started From 196f4fff9cd96253e2687e49ce703ec514c1e73e Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Sat, 6 Jun 2020 11:17:31 +0100 Subject: [PATCH 465/922] Fixed twemoji test; added ci scripts to blackening. --- ci/black.nox.py | 3 +++ ci/docker.nox.py | 3 ++- ci/nox.py | 2 ++ ci/pylint.nox.py | 7 +------ ci/pytest.nox.py | 29 ++++++++++++++++++----------- scripts/test_twemoji_mapping.py | 19 ++++++++++++------- 6 files changed, 38 insertions(+), 25 deletions(-) diff --git a/ci/black.nox.py b/ci/black.nox.py index 993fc9c4b9..aea107b4ac 100644 --- a/ci/black.nox.py +++ b/ci/black.nox.py @@ -17,6 +17,8 @@ # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . """Black code-style jobs.""" +import os + from ci import nox @@ -25,6 +27,7 @@ "tests", "setup.py", "noxfile.py", + *(os.path.join("ci", f) for f in os.listdir("ci") if f.endswith(".py")), ] diff --git a/ci/docker.nox.py b/ci/docker.nox.py index d8c84f35de..def2aadc38 100644 --- a/ci/docker.nox.py +++ b/ci/docker.nox.py @@ -29,6 +29,7 @@ if shutil.which("docker"): + @nox.session(reuse_venv=True) def docker(session: nox.Session) -> None: """Run a nox session in a container that targets a specific Python version. @@ -74,6 +75,6 @@ def docker(session: nox.Session) -> None: "--rm", "-it", python, - f"/bin/sh -c 'cd hikari && pip install nox && nox {args}'" + f"/bin/sh -c 'cd hikari && pip install nox && nox {args}'", ) print("\033[33m<<<<<<<<<<<<<<<<<<< EXITING CONTAINER >>>>>>>>>>>>>>>>>>>\033[0m") diff --git a/ci/nox.py b/ci/nox.py index a4058c48f0..e723883444 100644 --- a/ci/nox.py +++ b/ci/nox.py @@ -37,6 +37,7 @@ def decorator(func: Callable[[Session], None]): _options.sessions.append(func.__name__) return _session(reuse_venv=reuse_venv, **kwargs)(func) if only_if() else func + return decorator @@ -46,6 +47,7 @@ def logic(session): for n, v in os.environ.items(): session.env[n] = v return func(session) + return logic diff --git a/ci/pylint.nox.py b/ci/pylint.nox.py index 2058e21f46..772f9089bb 100644 --- a/ci/pylint.nox.py +++ b/ci/pylint.nox.py @@ -21,12 +21,7 @@ from ci import config from ci import nox -FLAGS = [ - "pylint", - config.MAIN_PACKAGE, - "--rcfile", - config.PYLINT_INI -] +FLAGS = ["pylint", config.MAIN_PACKAGE, "--rcfile", config.PYLINT_INI] SUCCESS_CODES = list(range(0, 256)) diff --git a/ci/pytest.nox.py b/ci/pytest.nox.py index 5c8ff265bf..8e83eaa453 100644 --- a/ci/pytest.nox.py +++ b/ci/pytest.nox.py @@ -24,19 +24,27 @@ from ci import nox FLAGS = [ - "-c", config.PYTEST_INI, - "-r", "a", + "-c", + config.PYTEST_INI, + "-r", + "a", "--full-trace", - "-n", "auto", - "--cov", config.MAIN_PACKAGE, - "--cov-config", config.COVERAGE_INI, - "--cov-report", "term", - "--cov-report", f"html:{config.COVERAGE_HTML_PATH}", + "-n", + "auto", + "--cov", + config.MAIN_PACKAGE, + "--cov-config", + config.COVERAGE_INI, + "--cov-report", + "term", + "--cov-report", + f"html:{config.COVERAGE_HTML_PATH}", "--cov-branch", - "--junitxml", config.COVERAGE_JUNIT_PATH, + "--junitxml", + config.COVERAGE_JUNIT_PATH, "--force-testdox", "--showlocals", - config.TEST_PACKAGE + config.TEST_PACKAGE, ] @@ -44,8 +52,7 @@ def pytest(session: nox.Session) -> None: """Run unit tests and measure code coverage.""" session.install( - "-r", config.REQUIREMENTS, - "-r", config.DEV_REQUIREMENTS, + "-r", config.REQUIREMENTS, "-r", config.DEV_REQUIREMENTS, ) shutil.rmtree(".coverage", ignore_errors=True) session.run("python", "-m", "pytest", *FLAGS) diff --git a/scripts/test_twemoji_mapping.py b/scripts/test_twemoji_mapping.py index 599b460d4b..e6b4b3c769 100644 --- a/scripts/test_twemoji_mapping.py +++ b/scripts/test_twemoji_mapping.py @@ -28,7 +28,8 @@ sys.path.append(".") import aiohttp -import hikari +from hikari.models import emojis +from hikari import errors skipped_emojis = [] @@ -53,12 +54,13 @@ async def run(): task.add_done_callback(lambda _: semaphore.release()) tasks.append(task) - if i and i % 750 == 0: + if i and i % 250 == 0: print("Backing off so GitHub doesn't IP ban us") await asyncio.gather(*tasks) await asyncio.sleep(10) tasks.clear() + print("\033[0;38mCatching up...\033[0m\r") await asyncio.gather(*tasks) print("Results") @@ -76,7 +78,7 @@ async def run(): async def try_fetch(i, n, emoji_surrogates, name): - emoji = hikari.UnicodeEmoji(name=emoji_surrogates) + emoji = emojis.UnicodeEmoji.from_emoji(emoji_surrogates) ex = None for _ in range(5): try: @@ -87,16 +89,19 @@ async def try_fetch(i, n, emoji_surrogates, name): ex = None break - if isinstance(ex, hikari.errors.ServerHTTPErrorResponse): + if isinstance(ex, errors.ServerHTTPErrorResponse): skipped_emojis.append((emoji_surrogates, name)) - print("[ SKIP ]", f"{i}/{n}", name, *map(hex, map(ord, emoji_surrogates)), emoji.url, str(ex)) + print("\033[1;38m[ SKIP ]\033[0m", f"{i}/{n}", + name, *map(hex, map(ord, emoji_surrogates)), emoji.url, str(ex)) if ex is None: valid_emojis.append((emoji_surrogates, name)) - print("[ OK ]", f"{i}/{n}", name, *map(hex, map(ord, emoji_surrogates)), emoji.url) + print("\033[1;32m[ OK ]\033[0m", f"{i}/{n}", + name, *map(hex, map(ord, emoji_surrogates)), emoji.url) else: invalid_emojis.append((emoji_surrogates, name)) - print("[ FAIL ]", f"{i}/{n}", name, *map(hex, map(ord, emoji_surrogates)), type(ex), ex, emoji.url) + print("\033[1;31m[ FAIL ]\033[0m", f"{i}/{n}", + name, *map(hex, map(ord, emoji_surrogates)), type(ex), ex, emoji.url) asyncio.run(run()) From 5955622de66370212442b8561c1378fcc390ba6c Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Sat, 6 Jun 2020 11:27:56 +0100 Subject: [PATCH 466/922] 100% coverage in rate_limits.py again, removed voice_gateway for time being. --- hikari/net/buckets.py | 4 +- hikari/net/gateway.py | 6 +- hikari/net/{ratelimits.py => rate_limits.py} | 0 hikari/net/rest.py | 4 +- hikari/net/voice_gateway.py | 403 ------------------- tests/hikari/net/test_ratelimits.py | 53 +-- 6 files changed, 36 insertions(+), 434 deletions(-) rename hikari/net/{ratelimits.py => rate_limits.py} (100%) delete mode 100644 hikari/net/voice_gateway.py diff --git a/hikari/net/buckets.py b/hikari/net/buckets.py index cc31b9a886..7e708edd7b 100644 --- a/hikari/net/buckets.py +++ b/hikari/net/buckets.py @@ -213,7 +213,7 @@ import types import typing -from hikari.net import ratelimits +from hikari.net import rate_limits from hikari.net import routes from hikari.utilities import aio @@ -221,7 +221,7 @@ """The hash used for an unknown bucket that has not yet been resolved.""" -class RESTBucket(ratelimits.WindowedBurstRateLimiter): +class RESTBucket(rate_limits.WindowedBurstRateLimiter): """Represents a rate limit for an RESTSession endpoint. Component to represent an active rate limit bucket on a specific RESTSession _route diff --git a/hikari/net/gateway.py b/hikari/net/gateway.py index e6f2738fd7..81b9825df8 100644 --- a/hikari/net/gateway.py +++ b/hikari/net/gateway.py @@ -38,7 +38,7 @@ from hikari.api import component from hikari.models import presences from hikari.net import http_client -from hikari.net import ratelimits +from hikari.net import rate_limits from hikari.net import user_agents from hikari.utilities import data_binding from hikari.utilities import klass @@ -190,7 +190,7 @@ def __init__( ) self._activity = initial_activity self._app = app - self._backoff = ratelimits.ExponentialBackOff(base=1.85, maximum=600, initial_increment=2) + self._backoff = rate_limits.ExponentialBackOff(base=1.85, maximum=600, initial_increment=2) self._handshake_event = asyncio.Event() self._idle_since = initial_idle_since self._intents = intents @@ -214,7 +214,7 @@ def __init__( self.last_heartbeat_sent = float("nan") self.last_message_received = float("nan") self.large_threshold = large_threshold - self.ratelimiter = ratelimits.WindowedBurstRateLimiter(str(shard_id), 60.0, 120) + self.ratelimiter = rate_limits.WindowedBurstRateLimiter(str(shard_id), 60.0, 120) self.session_id = None scheme, netloc, path, params, _, _ = urllib.parse.urlparse(url, allow_fragments=True) diff --git a/hikari/net/ratelimits.py b/hikari/net/rate_limits.py similarity index 100% rename from hikari/net/ratelimits.py rename to hikari/net/rate_limits.py diff --git a/hikari/net/rest.py b/hikari/net/rest.py index 8be5c6b9e3..1e3dea570c 100644 --- a/hikari/net/rest.py +++ b/hikari/net/rest.py @@ -35,7 +35,7 @@ from hikari.net import http_client from hikari.net import http_settings from hikari.net import iterators -from hikari.net import ratelimits +from hikari.net import rate_limits from hikari.net import rest_utils from hikari.net import routes from hikari.utilities import data_binding @@ -137,7 +137,7 @@ def __init__( trust_env=config.trust_env, ) self.buckets = buckets.RESTBucketManager() - self.global_rate_limit = ratelimits.ManualRateLimiter() + self.global_rate_limit = rate_limits.ManualRateLimiter() self._invalid_requests = 0 self._invalid_request_window = -float("inf") self.version = version diff --git a/hikari/net/voice_gateway.py b/hikari/net/voice_gateway.py deleted file mode 100644 index af0d42b2ee..0000000000 --- a/hikari/net/voice_gateway.py +++ /dev/null @@ -1,403 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# Copyright © Nekoka.tt 2019-2020 -# -# This file is part of Hikari. -# -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . -"""Implementation of the V4 voice gateway.""" -from __future__ import annotations - -__all__ = ["VoiceGateway"] - -import asyncio -import enum -import math -import time -import typing -import urllib.parse - -import aiohttp -import attr - -from hikari import errors -from hikari.api import component -from hikari.net import http_client -from hikari.net import ratelimits -from hikari.utilities import data_binding -from hikari.utilities import klass - -if typing.TYPE_CHECKING: - from hikari.api import app as app_ - from hikari import bot - from hikari.net import http_settings - from hikari.models import bases - - -class VoiceGateway(http_client.HTTPClient, component.IComponent): - """Implementation of the V4 Voice Gateway.""" - - @enum.unique - class _GatewayCloseCode(enum.IntEnum): - """Reasons for closing a gateway connection.""" - - RFC_6455_NORMAL_CLOSURE = 1000 - RFC_6455_GOING_AWAY = 1001 - RFC_6455_PROTOCOL_ERROR = 1002 - RFC_6455_TYPE_ERROR = 1003 - RFC_6455_ENCODING_ERROR = 1007 - RFC_6455_POLICY_VIOLATION = 1008 - RFC_6455_TOO_BIG = 1009 - RFC_6455_UNEXPECTED_CONDITION = 1011 - - UNKNOWN_OPCODE = 4001 - NOT_AUTHENTICATED = 4003 - AUTHENTICATION_FAILED = 4004 - ALREADY_AUTHENTICATED = 4005 - SESSION_NO_LONGER_VALID = 4006 - SESSION_TIMEOUT = 4009 - SERVER_NOT_FOUND = 4011 - UNKNOWN_PROTOCOL = 4012 - DISCONNECTED = 4014 - VOICE_SERVER_CRASHED = 4015 - UNKNOWN_ENCRYPTION_MODE = 4016 - - @enum.unique - class _GatewayOpcode(enum.IntEnum): - IDENTIFY = 0 - SELECT_PROTOCOL = 1 - READY = 2 - HEARTBEAT = 3 - SESSION_DESCRIPTION = 4 - SPEAKING = 5 - HEARTBEAT_ACK = 6 - RESUME = 7 - HELLO = 8 - RESUMED = 9 - CLIENT_DISCONNECT = 13 - - class _Reconnect(RuntimeError): - __slots__ = () - - class _SocketClosed(RuntimeError): - __slots__ = () - - @attr.s(auto_attribs=True, slots=True) - class _InvalidSession(RuntimeError): - can_resume: bool = False - - def __init__( - self, - *, - app: bot.IBot, - config: http_settings.HTTPSettings, - debug: bool = False, - endpoint: str, - session_id: str, - user_id: bases.UniqueObject, - server_id: bases.UniqueObject, - token: str, - ) -> None: - super().__init__( - allow_redirects=config.allow_redirects, - connector=config.tcp_connector_factory() if config.tcp_connector_factory else None, - debug=debug, - # Use the server ID to identify each websocket based on a server. - logger=klass.get_logger(self, str(int(server_id))), - proxy_auth=config.proxy_auth, - proxy_headers=config.proxy_headers, - proxy_url=config.proxy_url, - ssl_context=config.ssl_context, - verify_ssl=config.verify_ssl, - timeout=config.request_timeout, - trust_env=config.trust_env, - ) - - # The port Discord gives me is plain wrong, which is helpful. - path = endpoint.rpartition(":")[0] - query = urllib.parse.urlencode({"v": "4"}) - self._url = f"wss://{path}?{query}" - - self._app = app - self._backoff = ratelimits.ExponentialBackOff(base=1.85, maximum=600, initial_increment=2) - self._last_run_started_at = float("nan") - self._nonce = None - self._request_close_event = asyncio.Event() - self._resumable = False - self._server_id = str(int(server_id)) - self._session_id = session_id - self._token = token - self._user_id = str(int(user_id)) - self._voice_ip = None - self._voice_modes = [] - self._voice_port = None - self._voice_ssrc = None - self._ws = None - self._zombied = False - - self.connected_at = float("nan") - self.heartbeat_interval = float("nan") - self.heartbeat_latency = float("nan") - self.last_heartbeat_sent = float("nan") - self.last_message_received = float("nan") - - @property - def is_alive(self) -> bool: - """Return whether the client is alive.""" - return not math.isnan(self.connected_at) - - @property - def app(self) -> app_.IApp: - return self._app - - async def run(self) -> None: - """Start the voice gateway client session.""" - try: - while not self._request_close_event.is_set() and await self._run_once(): - pass - finally: - # Close the aiohttp client session. - await super().close() - - async def close(self) -> None: - """Close the websocket.""" - if not self._request_close_event.is_set(): - if self.is_alive: - self.logger.info("received request to shut down voice gateway client") - else: - self.logger.debug("voice gateway client marked as closed before it was able to start") - self._request_close_event.set() - - if self._ws is not None: - self.logger.warning("voice gateway client closed by user, will not attempt to restart") - await self._close_ws(self._GatewayCloseCode.RFC_6455_NORMAL_CLOSURE, "user shut down application") - - async def _close_ws(self, code: int, message: str): - self.logger.debug("sending close frame with code %s and message %r", int(code), message) - # None if the websocket errored on initialziation. - if self._ws is not None: - await self._ws.close(code=code, message=bytes(message, "utf-8")) - - async def _run_once(self): - self._request_close_event.clear() - - if self._now() - self._last_run_started_at < 30: - # Interrupt sleep immediately if a request to close is fired. - wait_task = asyncio.create_task( - self._request_close_event.wait(), name=f"voice gateway client {self._server_id} backing off" - ) - try: - backoff = next(self._backoff) - self.logger.debug("backing off for %ss", backoff) - await asyncio.wait_for(wait_task, timeout=backoff) - return False - except asyncio.TimeoutError: - pass - - # Do this after; it prevents backing off on the first try. - self._last_run_started_at = self._now() - - try: - self.logger.debug("creating websocket connection to %s", self._url) - self._ws = await self._create_ws(self._url) - self.connected_at = self._now() - await self._handshake() - - # Technically we are connected after the hello, but this ensures we can send and receive - # before firing that event. - await self._on_connect() - - # We should ideally set this after HELLO, but it should be fine - # here as well. If we don't heartbeat in time, something probably - # went majorly wrong anyway. - heartbeat = asyncio.create_task(self._pulse(), name=f"voice gateway client {self._server_id} heartbeat") - - try: - await self._poll_events() - finally: - heartbeat.cancel() - except aiohttp.ClientConnectionError as ex: - self.logger.error( - "failed to connect to Discord because %s.%s: %s", type(ex).__module__, type(ex).__qualname__, str(ex), - ) - - except Exception as ex: - self.logger.error("unexpected exception occurred, shard will now die", exc_info=ex) - await self._close_ws(self._GatewayCloseCode.RFC_6455_UNEXPECTED_CONDITION, "unexpected error occurred") - raise - - finally: - if not math.isnan(self.connected_at): - # Only dispatch this if we actually connected before we failed! - await self._on_disconnect() - - self.connected_at = float("nan") - return True - - async def _poll_events(self): - while not self._request_close_event.is_set(): - message = await self._receive_json_payload() - - op = message["op"] - data = message["d"] - - if op == self._GatewayOpcode.READY: - self.logger.debug( - "voice websocket is ready [session_id:%s, url:%s]", self._session_id, self._url, - ) - elif op == self._GatewayOpcode.RESUMED: - self.logger.debug( - "voice websocket has resumed [session_id:%s, nonce:%s, url:%s]", - self._session_id, - self._nonce, - self._url, - ) - elif op == self._GatewayOpcode.HEARTBEAT: - self.logger.debug("received HEARTBEAT; sending HEARTBEAT ACK") - await self._send_json({"op": self._GatewayOpcode.HEARTBEAT_ACK, "d": self._nonce}) - elif op == self._GatewayOpcode.HEARTBEAT_ACK: - self.heartbeat_latency = self._now() - self.last_heartbeat_sent - self.logger.debug("received HEARTBEAT ACK [latency:%ss]", self.heartbeat_latency) - elif op == self._GatewayOpcode.SESSION_DESCRIPTION: - self.logger.debug("received session description data %s", data) - elif op == self._GatewayOpcode.SPEAKING: - self.logger.debug("someone is speaking with data %s", data) - elif op == self._GatewayOpcode.CLIENT_DISCONNECT: - self.logger.debug("a client has disconnected with data %s", data) - else: - self.logger.debug("ignoring unrecognised opcode %s", op) - - async def _pulse(self) -> None: - try: - while not self._request_close_event.is_set(): - now = self._now() - time_since_message = now - self.last_message_received - time_since_heartbeat_sent = now - self.last_heartbeat_sent - - if self.heartbeat_interval < time_since_message: - self.logger.error( - "connection is a zombie, haven't received any message for %ss, last heartbeat sent %ss ago", - time_since_message, - time_since_heartbeat_sent, - ) - self._zombied = True - await self._close_ws(self._GatewayCloseCode.DO_NOT_INVALIDATE_SESSION, "zombie connection") - return - - self.logger.debug( - "preparing to send HEARTBEAT [nonce:%s, interval:%ss]", self._nonce, self.heartbeat_interval - ) - await self._send_json({"op": self._GatewayOpcode.HEARTBEAT, "d": self._nonce}) - self.last_heartbeat_sent = self._now() - - try: - await asyncio.wait_for(self._request_close_event.wait(), timeout=self.heartbeat_interval) - except asyncio.TimeoutError: - pass - - except asyncio.CancelledError: - # This happens if the poll task has stopped. It isn't a problem we need to report. - pass - - async def _on_connect(self): - pass - - async def _on_disconnect(self): - pass - - async def _handshake(self): - # HELLO! - message = await self._receive_json_payload() - op = message["op"] - if message["op"] != self._GatewayOpcode.HELLO: - await self._close_ws(self._GatewayCloseCode.RFC_6455_POLICY_VIOLATION.value, "did not receive HELLO") - raise errors.GatewayError(f"Expected HELLO opcode {self._GatewayOpcode.HELLO.value} but received {op}") - - self.heartbeat_interval = message["d"]["heartbeat_interval"] - - self.logger.debug("received HELLO, heartbeat interval is %s", self.heartbeat_interval) - - if self._session_id is not None: - # RESUME! - await self._send_json( - { - "op": self._GatewayOpcode.RESUME, - "d": {"token": self._token, "server_id": self._server_id, "session_id": self._session_id}, - } - ) - else: - await self._send_json( - { - "op": self._GatewayOpcode.IDENTIFY, - "d": { - "token": self._token, - "server_id": self._server_id, - "user_id": self._user_id, - "session_id": self._session_id, - }, - } - ) - - async def _receive_json_payload(self) -> data_binding.JSONObject: - message = await self._ws.receive() - self.last_message_received = self._now() - - if message.type == aiohttp.WSMsgType.TEXT: - self._log_debug_payload(message.data, "received text payload") - return data_binding.load_json(message.data) - - elif message.type == aiohttp.WSMsgType.CLOSE: - close_code = self._ws.close_code - self.logger.debug("connection closed with code %s", close_code) - - if close_code in self._GatewayCloseCode.__members__.values(): - reason = self._GatewayCloseCode(close_code).name - else: - reason = f"unknown close code {close_code}" - - can_reconnect = close_code in ( - self._GatewayCloseCode.SESSION_NO_LONGER_VALID, - self._GatewayCloseCode.SESSION_TIMEOUT, - self._GatewayCloseCode.DISCONNECTED, - self._GatewayCloseCode.VOICE_SERVER_CRASHED, - ) - - raise errors.GatewayServerClosedConnectionError(reason, close_code, can_reconnect, False, True) - - elif message.type == aiohttp.WSMsgType.CLOSING or message.type == aiohttp.WSMsgType.CLOSED: - raise self._SocketClosed() - else: - # Assume exception for now. - ex = self._ws.exception() - self.logger.debug("encountered unexpected error", exc_info=ex) - raise errors.GatewayError("Unexpected websocket exception from gateway") from ex - - async def _send_json(self, payload: data_binding.JSONObject) -> None: - message = data_binding.dump_json(payload) - self._log_debug_payload(message, "sending json payload") - await self._ws.send_str(message) - - def _log_debug_payload(self, payload: str, message: str, *args: typing.Any) -> None: - message = f"{message} [nonce:%s, url:%s, session_id: %s, server: %s, size:%s]" - if self._debug: - message = f"{message} with raw payload: %s" - args = (*args, self._nonce, self._url, self._session_id, self._server_id, len(payload), payload) - else: - args = (*args, self._nonce, self._url, self._session_id, self._server_id, len(payload)) - - self.logger.debug(message, *args) - - @staticmethod - def _now() -> float: - return time.perf_counter() diff --git a/tests/hikari/net/test_ratelimits.py b/tests/hikari/net/test_ratelimits.py index d28288496e..b400a8356f 100644 --- a/tests/hikari/net/test_ratelimits.py +++ b/tests/hikari/net/test_ratelimits.py @@ -26,13 +26,13 @@ import mock import pytest -from hikari.net import ratelimits +from hikari.net import rate_limits from tests.hikari import _helpers class TestBaseRateLimiter: def test_context_management(self): - class MockedBaseRateLimiter(ratelimits.BaseRateLimiter): + class MockedBaseRateLimiter(rate_limits.BaseRateLimiter): close = mock.MagicMock() acquire = NotImplemented @@ -45,7 +45,7 @@ class MockedBaseRateLimiter(ratelimits.BaseRateLimiter): class TestBurstRateLimiter: @pytest.fixture def mock_burst_limiter(self): - class Impl(ratelimits.BurstRateLimiter): + class Impl(rate_limits.BurstRateLimiter): def acquire(self, *args, **kwargs) -> asyncio.Future: raise NotImplementedError @@ -82,25 +82,30 @@ def test_close_cancels_throttle_task_if_running(self, event_loop, mock_burst_lim mock_burst_limiter.close() assert mock_burst_limiter.throttle_task.cancelled(), "throttle_task is not cancelled" + def test_close_when_closed(self, mock_burst_limiter): + # Double-running shouldn't do anything adverse. + mock_burst_limiter.close() + mock_burst_limiter.close() + class TestManualRateLimiter: @pytest.mark.asyncio async def test_acquire_returns_completed_future_if_lock_task_is_None(self): - with ratelimits.ManualRateLimiter() as limiter: + with rate_limits.ManualRateLimiter() as limiter: limiter.throttle_task = None future = limiter.acquire() assert future.done() @pytest.mark.asyncio async def test_acquire_returns_incomplete_future_if_lock_task_is_not_None(self): - with ratelimits.ManualRateLimiter() as limiter: + with rate_limits.ManualRateLimiter() as limiter: limiter.throttle_task = asyncio.get_running_loop().create_future() future = limiter.acquire() assert not future.done() @pytest.mark.asyncio async def test_acquire_places_future_on_queue_if_lock_task_is_not_None(self): - with ratelimits.ManualRateLimiter() as limiter: + with rate_limits.ManualRateLimiter() as limiter: limiter.throttle_task = asyncio.get_running_loop().create_future() assert len(limiter.queue) == 0 future = limiter.acquire() @@ -110,7 +115,7 @@ async def test_acquire_places_future_on_queue_if_lock_task_is_not_None(self): @pytest.mark.asyncio async def test_lock_cancels_existing_task(self): - with ratelimits.ManualRateLimiter() as limiter: + with rate_limits.ManualRateLimiter() as limiter: limiter.throttle_task = asyncio.get_running_loop().create_future() old_task = limiter.throttle_task limiter.throttle(0) @@ -119,7 +124,7 @@ async def test_lock_cancels_existing_task(self): @pytest.mark.asyncio async def test_lock_schedules_throttle(self): - with _helpers.unslot_class(ratelimits.ManualRateLimiter)() as limiter: + with _helpers.unslot_class(rate_limits.ManualRateLimiter)() as limiter: limiter.unlock_later = mock.AsyncMock() limiter.throttle(0) await limiter.throttle_task @@ -127,7 +132,7 @@ async def test_lock_schedules_throttle(self): @pytest.mark.asyncio async def test_throttle_chews_queue_completing_futures(self, event_loop): - with ratelimits.ManualRateLimiter() as limiter: + with rate_limits.ManualRateLimiter() as limiter: futures = [event_loop.create_future() for _ in range(10)] limiter.queue = list(futures) await limiter.unlock_later(0.01) @@ -149,7 +154,7 @@ def pop(self, _=-1): popped_at.append(time.perf_counter()) return event_loop.create_future() - with _helpers.unslot_class(ratelimits.ManualRateLimiter)() as limiter: + with _helpers.unslot_class(rate_limits.ManualRateLimiter)() as limiter: with mock.patch("asyncio.sleep", wraps=mock_sleep): limiter.queue = MockList() @@ -162,7 +167,7 @@ def pop(self, _=-1): @pytest.mark.asyncio async def test_throttle_clears_throttle_task(self, event_loop): - with ratelimits.ManualRateLimiter() as limiter: + with rate_limits.ManualRateLimiter() as limiter: limiter.throttle_task = event_loop.create_future() await limiter.unlock_later(0) assert limiter.throttle_task is None @@ -171,7 +176,7 @@ async def test_throttle_clears_throttle_task(self, event_loop): class TestWindowedBurstRateLimiter: @pytest.fixture def ratelimiter(self): - inst = _helpers.unslot_class(ratelimits.WindowedBurstRateLimiter)(__name__, 3, 3) + inst = _helpers.unslot_class(rate_limits.WindowedBurstRateLimiter)(__name__, 3, 3) yield inst with contextlib.suppress(Exception): inst.close() @@ -258,7 +263,7 @@ async def test_future_is_added_to_queue_if_rate_limited(self, ratelimiter): @pytest.mark.asyncio async def test_throttle_consumes_queue(self, event_loop): - with ratelimits.WindowedBurstRateLimiter(__name__, 0.01, 1) as rl: + with rate_limits.WindowedBurstRateLimiter(__name__, 0.01, 1) as rl: rl.queue = [event_loop.create_future() for _ in range(15)] old_queue = list(rl.queue) await rl.throttle() @@ -287,7 +292,7 @@ def create_task(i): future.add_done_callback(lambda _: completion_times.append(time.perf_counter())) return future - with ratelimits.WindowedBurstRateLimiter(__name__, period, limit) as rl: + with rate_limits.WindowedBurstRateLimiter(__name__, period, limit) as rl: futures = [create_task(i) for i in range(total_requests)] rl.queue = list(futures) rl.reset_at = time.perf_counter() @@ -327,25 +332,25 @@ def create_task(i): @pytest.mark.asyncio async def test_throttle_resets_throttle_task(self, event_loop): - with ratelimits.WindowedBurstRateLimiter(__name__, 0.01, 1) as rl: + with rate_limits.WindowedBurstRateLimiter(__name__, 0.01, 1) as rl: rl.queue = [event_loop.create_future() for _ in range(15)] rl.throttle_task = None await rl.throttle() assert rl.throttle_task is None def test_get_time_until_reset_if_not_rate_limited(self): - with _helpers.unslot_class(ratelimits.WindowedBurstRateLimiter)(__name__, 0.01, 1) as rl: + with _helpers.unslot_class(rate_limits.WindowedBurstRateLimiter)(__name__, 0.01, 1) as rl: rl.is_rate_limited = mock.MagicMock(return_value=False) assert rl.get_time_until_reset(420) == 0.0 def test_get_time_until_reset_if_rate_limited(self): - with _helpers.unslot_class(ratelimits.WindowedBurstRateLimiter)(__name__, 0.01, 1) as rl: + with _helpers.unslot_class(rate_limits.WindowedBurstRateLimiter)(__name__, 0.01, 1) as rl: rl.is_rate_limited = mock.MagicMock(return_value=True) rl.reset_at = 420.4 assert rl.get_time_until_reset(69.8) == 420.4 - 69.8 def test_is_rate_limited_when_rate_limit_expired_resets_self(self): - with ratelimits.WindowedBurstRateLimiter(__name__, 403, 27) as rl: + with rate_limits.WindowedBurstRateLimiter(__name__, 403, 27) as rl: now = 180 rl.reset_at = 80 rl.remaining = 4 @@ -357,7 +362,7 @@ def test_is_rate_limited_when_rate_limit_expired_resets_self(self): @pytest.mark.parametrize("remaining", [-1, 0, 1]) def test_is_rate_limited_when_rate_limit_not_expired_only_returns_expr(self, remaining): - with ratelimits.WindowedBurstRateLimiter(__name__, 403, 27) as rl: + with rate_limits.WindowedBurstRateLimiter(__name__, 403, 27) as rl: now = 420 rl.reset_at = now + 69 rl.remaining = remaining @@ -366,14 +371,14 @@ def test_is_rate_limited_when_rate_limit_not_expired_only_returns_expr(self, rem class TestExponentialBackOff: def test_reset(self): - eb = ratelimits.ExponentialBackOff() + eb = rate_limits.ExponentialBackOff() eb.increment = 10 eb.reset() assert eb.increment == 0 @pytest.mark.parametrize(["iteration", "backoff"], enumerate((1, 2, 4, 8, 16, 32))) def test_increment_linear(self, iteration, backoff): - eb = ratelimits.ExponentialBackOff(2, 64, 0) + eb = rate_limits.ExponentialBackOff(2, 64, 0) for _ in range(iteration): next(eb) @@ -382,7 +387,7 @@ def test_increment_linear(self, iteration, backoff): def test_increment_maximum(self): max_bound = 64 - eb = ratelimits.ExponentialBackOff(2, max_bound, 0) + eb = rate_limits.ExponentialBackOff(2, max_bound, 0) iterations = math.ceil(math.log2(max_bound)) for _ in range(iterations): next(eb) @@ -396,7 +401,7 @@ def test_increment_maximum(self): @pytest.mark.parametrize(["iteration", "backoff"], enumerate((1, 2, 4, 8, 16, 32))) def test_increment_jitter(self, iteration, backoff): abs_tol = 1 - eb = ratelimits.ExponentialBackOff(2, 64, abs_tol) + eb = rate_limits.ExponentialBackOff(2, 64, abs_tol) for _ in range(iteration): next(eb) @@ -404,5 +409,5 @@ def test_increment_jitter(self, iteration, backoff): assert math.isclose(next(eb), backoff, abs_tol=abs_tol) def test_iter_returns_self(self): - eb = ratelimits.ExponentialBackOff(2, 64, 123) + eb = rate_limits.ExponentialBackOff(2, 64, 123) assert iter(eb) is eb From f48470163fa203448ac48b6301a02f60f603a3fb Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Sat, 6 Jun 2020 11:38:43 +0100 Subject: [PATCH 467/922] Fixed missing date.py test cases. --- tests/hikari/__init__.py | 14 ++++++++++++++ tests/hikari/net/test_ratelimits.py | 5 ++--- tests/hikari/utilities/test_date.py | 17 +++++++++++++++++ 3 files changed, 33 insertions(+), 3 deletions(-) diff --git a/tests/hikari/__init__.py b/tests/hikari/__init__.py index 47513b5a29..63253adf3b 100644 --- a/tests/hikari/__init__.py +++ b/tests/hikari/__init__.py @@ -20,6 +20,8 @@ import contextlib import sys +import pytest + _real_new_event_loop = asyncio.new_event_loop @@ -35,3 +37,15 @@ def _new_event_loop(): asyncio.new_event_loop = _new_event_loop + + +_pytest_parametrize = pytest.mark.parametrize + + +def parametrize(*args, **kwargs): + # Force ids to be strified by default for readability. + kwargs.setdefault("ids", repr) + return _pytest_parametrize(*args, **kwargs) + + +pytest.mark.parametrize = parametrize diff --git a/tests/hikari/net/test_ratelimits.py b/tests/hikari/net/test_ratelimits.py index b400a8356f..85288549be 100644 --- a/tests/hikari/net/test_ratelimits.py +++ b/tests/hikari/net/test_ratelimits.py @@ -273,9 +273,8 @@ async def test_throttle_consumes_queue(self, event_loop): assert future.done(), f"future {i} was incomplete!" @pytest.mark.asyncio - @pytest.mark.slow @_helpers.retry(5) - async def test_throttle_sleeps_each_time_limit_is_hit_and_releases_bursts_of_futures_periodically(self, event_loop): + async def test_throttle_when_limited_sleeps_then_bursts_repeatedly(self, event_loop): limit = 5 period = 3 total_requests = period * limit * 2 @@ -361,7 +360,7 @@ def test_is_rate_limited_when_rate_limit_expired_resets_self(self): assert rl.remaining == 27 @pytest.mark.parametrize("remaining", [-1, 0, 1]) - def test_is_rate_limited_when_rate_limit_not_expired_only_returns_expr(self, remaining): + def test_is_rate_limited_when_rate_limit_not_expired_only_returns_False(self, remaining): with rate_limits.WindowedBurstRateLimiter(__name__, 403, 27) as rl: now = 420 rl.reset_at = now + 69 diff --git a/tests/hikari/utilities/test_date.py b/tests/hikari/utilities/test_date.py index ea6c60de87..28f1cc6457 100644 --- a/tests/hikari/utilities/test_date.py +++ b/tests/hikari/utilities/test_date.py @@ -18,6 +18,8 @@ # along with Hikari. If not, see . import datetime +import pytest + from hikari.utilities import date as date_ @@ -117,3 +119,18 @@ def test_unix_epoch_to_datetime_with_out_of_range_positive_timestamp(): def test_unix_epoch_to_datetime_with_out_of_range_negative_timestamp(): assert date_.unix_epoch_to_datetime(-996877846784536) == datetime.datetime.min + + +@pytest.mark.parametrize( + ["input_value", "expected_result"], + [ + (5, 5), + (2.718281828459045, 2), + (datetime.timedelta(days=5, seconds=3, milliseconds=12), 432_003), + (-5, 0), + (-2.718281828459045, 0), + (datetime.timedelta(days=-5, seconds=-3, milliseconds=12), 0), + ], +) +def test_timespan_to_int(input_value, expected_result): + assert date_.timespan_to_int(input_value) == expected_result From e955164388633fabb1ebde437c785f2d139bef63 Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Sat, 6 Jun 2020 12:02:48 +0100 Subject: [PATCH 468/922] Added support for mintty on Windows for coloured logging. --- hikari/impl/bot.py | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/hikari/impl/bot.py b/hikari/impl/bot.py index e2b8ff734d..c7d6ab78ff 100644 --- a/hikari/impl/bot.py +++ b/hikari/impl/bot.py @@ -298,14 +298,26 @@ def __print_banner(self): @staticmethod def __get_logging_format(): + # Modified from # https://github.com/django/django/blob/master/django/core/management/color.py + plat = sys.platform - supported_platform = plat != "Pocket PC" and (plat != "win32" or "ANSICON" in os.environ) - # isatty is not always implemented, #6223. + supports_color = False + + # isatty is not always implemented, https://code.djangoproject.com/ticket/6223 is_a_tty = hasattr(sys.stdout, "isatty") and sys.stdout.isatty() - # https://youtrack.jetbrains.com/issue/PY-4853 - if supported_platform and is_a_tty or os.getenv("PYCHARM_HOSTED"): + if plat != "Pocket PC": + if plat == "win32": + supports_color |= os.getenv("TERM_PROGRAM", None) == "mintty" + supports_color |= "ANSICON" in os.environ + supports_color |= is_a_tty + else: + supports_color = is_a_tty + + supports_color |= bool(os.getenv("PYCHARM_HOSTED", "")) + + if supports_color: blue = "\033[1;35m" gray = "\033[1;37m" green = "\033[1;32m" From 2dfbca700616e73397156bf45fa3c5801da44ab9 Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Sat, 6 Jun 2020 12:23:53 +0100 Subject: [PATCH 469/922] Simplified reflect#resolve_signature --- hikari/utilities/reflect.py | 39 +++++++++++++------------------------ 1 file changed, 14 insertions(+), 25 deletions(-) diff --git a/hikari/utilities/reflect.py b/hikari/utilities/reflect.py index 5c48e63c7d..b9ae3486e3 100644 --- a/hikari/utilities/reflect.py +++ b/hikari/utilities/reflect.py @@ -29,7 +29,7 @@ """A singleton that empty annotations will be set to in `resolve_signature`.""" -def resolve_signature(func: typing.Callable) -> inspect.Signature: +def resolve_signature(func: typing.Callable[..., typing.Any]) -> inspect.Signature: """Get the `inspect.Signature` of `func` with resolved forward annotations. Parameters @@ -37,6 +37,11 @@ def resolve_signature(func: typing.Callable) -> inspect.Signature: func : typing.Callable[[...], ...] The function to get the resolved annotations from. + !!! warning + This will use `eval` to resolve string typehints and forward references. + This has a slight performance overhead, so attempt to cache this info + as much as possible. + Returns ------- typing.Signature @@ -44,29 +49,13 @@ def resolve_signature(func: typing.Callable) -> inspect.Signature: resolved. """ signature = inspect.signature(func) - resolved_type_hints = None - parameters = [] - for key, value in signature.parameters.items(): - if isinstance(value.annotation, str): - if resolved_type_hints is None: - resolved_type_hints = typing.get_type_hints(func) - value = value.replace(annotation=resolved_type_hints[key]) - - if value is type(None): - value = None - - parameters.append(value) - signature = signature.replace(parameters=parameters) - - if isinstance(signature.return_annotation, str): - if resolved_type_hints is None: - return_annotation = typing.get_type_hints(func)["return"] - else: - return_annotation = resolved_type_hints["return"] - - if return_annotation is type(None): - return_annotation = None + resolved_typehints = typing.get_type_hints(func) + return_annotation = resolved_typehints.pop("return") if "return" in resolved_typehints else EMPTY + params = [] - signature = signature.replace(return_annotation=return_annotation) + for name, param in signature.parameters.items(): + if isinstance(param.annotation, str): + param = param.replace(annotation=resolved_typehints[name] if name in resolved_typehints else EMPTY) + params.append(param) - return signature + return signature.replace(parameters=params, return_annotation=return_annotation) From 2f95a3a413c22a24fa39f88705a708b1fa2357e7 Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Sat, 6 Jun 2020 12:25:56 +0100 Subject: [PATCH 470/922] Suppressed warnings in test case. --- tests/hikari/utilities/test_reflect.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/hikari/utilities/test_reflect.py b/tests/hikari/utilities/test_reflect.py index d4937310ad..f449895ecf 100644 --- a/tests/hikari/utilities/test_reflect.py +++ b/tests/hikari/utilities/test_reflect.py @@ -21,6 +21,7 @@ from hikari.utilities import reflect +# noinspection PyUnusedLocal class TestResolveSignature: def test_handles_normal_references(self): def foo(bar: str, bat: int) -> str: From 803f7fba6991e6fce47ab467ecdbd657dc998c34 Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Sat, 6 Jun 2020 12:32:19 +0100 Subject: [PATCH 471/922] Finished test cases for undefined --- tests/hikari/utilities/test_undefined.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/tests/hikari/utilities/test_undefined.py b/tests/hikari/utilities/test_undefined.py index 165c1a3eee..5ace13a68f 100644 --- a/tests/hikari/utilities/test_undefined.py +++ b/tests/hikari/utilities/test_undefined.py @@ -16,6 +16,7 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . +import pytest from hikari.utilities import undefined from tests.hikari import _helpers @@ -45,3 +46,17 @@ class _(undefined.Undefined): def test_count(self): assert undefined.Undefined.count(9, 18, undefined.Undefined(), 36, undefined.Undefined(), 54) == 2 + + def test_iter(self): + with pytest.raises(StopIteration): + next(iter(undefined.Undefined())) + + def test_modify(self): + u = undefined.Undefined() + with pytest.raises(TypeError): + u.foo = 12 + + def test_delete(self): + u = undefined.Undefined() + with pytest.raises(TypeError): + del u.count From a6c9d6ceb1e6847a3ecf0d94e425c1e431272f39 Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Sat, 6 Jun 2020 12:34:29 +0100 Subject: [PATCH 472/922] Added snowflake test cases --- tests/hikari/utilities/test_snowflake.py | 87 ++++++++++++++++++++++++ 1 file changed, 87 insertions(+) create mode 100644 tests/hikari/utilities/test_snowflake.py diff --git a/tests/hikari/utilities/test_snowflake.py b/tests/hikari/utilities/test_snowflake.py new file mode 100644 index 0000000000..e07e41c093 --- /dev/null +++ b/tests/hikari/utilities/test_snowflake.py @@ -0,0 +1,87 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# Copyright © Nekoka.tt 2019-2020 +# +# This file is part of Hikari. +# +# Hikari is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Hikari is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Hikari. If not, see . +import datetime + +import pytest + +from hikari.utilities import snowflake + + +class TestSnowflake: + @pytest.fixture() + def raw_id(self): + return 537_340_989_808_050_216 + + @pytest.fixture() + def neko_snowflake(self, raw_id): + return snowflake.Snowflake(raw_id) + + def test_created_at(self, neko_snowflake): + assert neko_snowflake.created_at == datetime.datetime( + 2019, 1, 22, 18, 41, 15, 283_000, tzinfo=datetime.timezone.utc + ) + + def test_increment(self, neko_snowflake): + assert neko_snowflake.increment == 40 + + def test_internal_process_id(self, neko_snowflake): + assert neko_snowflake.internal_process_id == 0 + + def test_internal_worker_id(self, neko_snowflake): + assert neko_snowflake.internal_worker_id == 2 + + def test_hash(self, neko_snowflake, raw_id): + assert hash(neko_snowflake) == raw_id + + def test_int_cast(self, neko_snowflake, raw_id): + assert int(neko_snowflake) == raw_id + + def test_str_cast(self, neko_snowflake, raw_id): + assert str(neko_snowflake) == str(raw_id) + + def test_repr_cast(self, neko_snowflake, raw_id): + assert repr(neko_snowflake) == repr(raw_id) + + def test_eq(self, neko_snowflake, raw_id): + assert neko_snowflake == raw_id + assert neko_snowflake == snowflake.Snowflake(raw_id) + assert str(raw_id) != neko_snowflake + + def test_lt(self, neko_snowflake, raw_id): + assert neko_snowflake < raw_id + 1 + + def test_deserialize(self, neko_snowflake, raw_id): + assert neko_snowflake == snowflake.Snowflake(raw_id) + + def test_from_datetime(self): + result = snowflake.Snowflake.from_datetime( + datetime.datetime(2019, 1, 22, 18, 41, 15, 283_000, tzinfo=datetime.timezone.utc) + ) + assert result == 537340989807788032 + assert isinstance(result, snowflake.Snowflake) + + def test_min(self): + sf = snowflake.Snowflake.min() + assert sf == 0 + assert snowflake.Snowflake.min() is sf + + def test_max(self): + sf = snowflake.Snowflake.max() + assert sf == (1 << 63) - 1 + assert snowflake.Snowflake.max() is sf From e5ff000bfe3dd21a780b996fccc05a385ac0748e Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Sat, 6 Jun 2020 12:35:56 +0100 Subject: [PATCH 473/922] Added missing test cases for cdn.py --- tests/hikari/utilities/test_cdn.py | 41 ++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 tests/hikari/utilities/test_cdn.py diff --git a/tests/hikari/utilities/test_cdn.py b/tests/hikari/utilities/test_cdn.py new file mode 100644 index 0000000000..fdf200e1bd --- /dev/null +++ b/tests/hikari/utilities/test_cdn.py @@ -0,0 +1,41 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# Copyright © Nekoka.tt 2019-2020 +# +# This file is part of Hikari. +# +# Hikari is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Hikari is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Hikari. If not, see . +import pytest + +from hikari.utilities import cdn + + +def test_generate_cdn_url(): + url = cdn.generate_cdn_url("not", "a", "path", format_="neko", size=16) + assert url == "https://cdn.discordapp.com/not/a/path.neko?size=16" + + +def test_generate_cdn_url_with_size_set_to_none(): + url = cdn.generate_cdn_url("not", "a", "path", format_="neko", size=None) + assert url == "https://cdn.discordapp.com/not/a/path.neko" + + +def test_generate_cdn_url_with_invalid_size_out_of_limits(): + with pytest.raises(ValueError): + cdn.generate_cdn_url("not", "a", "path", format_="neko", size=11) + + +def test_generate_cdn_url_with_invalid_size_now_power_of_two(): + with pytest.raises(ValueError): + cdn.generate_cdn_url("not", "a", "path", format_="neko", size=111) From c1fc92a59e06bfc12c66f831af228ce306ef8b5d Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Sat, 6 Jun 2020 14:13:28 +0100 Subject: [PATCH 474/922] Added data_binding test cases. --- hikari/net/routes.py | 4 +- hikari/utilities/data_binding.py | 65 +++--- requirements.txt | 1 + tests/hikari/utilities/test_data_binding.py | 245 ++++++++++++++++++++ 4 files changed, 284 insertions(+), 31 deletions(-) create mode 100644 tests/hikari/utilities/test_data_binding.py diff --git a/hikari/net/routes.py b/hikari/net/routes.py index 06bc9928b5..49a5922b94 100644 --- a/hikari/net/routes.py +++ b/hikari/net/routes.py @@ -188,7 +188,9 @@ def compile(self, **kwargs: typing.Any) -> CompiledRoute: CompiledRoute The compiled route. """ - data = data_binding.StringMapBuilder.from_dict(kwargs) + data = data_binding.StringMapBuilder() + for k, v in kwargs.items(): + data.put(k, v) return CompiledRoute( self, self.path_template.format_map(data), data[self.major_param] if self.major_param is not None else "-", diff --git a/hikari/utilities/data_binding.py b/hikari/utilities/data_binding.py index 86c62976c9..d6e873f187 100644 --- a/hikari/utilities/data_binding.py +++ b/hikari/utilities/data_binding.py @@ -48,23 +48,23 @@ Headers = typing.Mapping[str, str] """Type hint for HTTP headers.""" -Query = typing.Dict[str, str] +Query = typing.Union[typing.Dict[str, str], multidict.MultiDict[str, str]] """Type hint for HTTP query string.""" URLEncodedForm = aiohttp.FormData -"""Type hint for content of type application/x-www-form-encoded""" +"""Type hint for content of type application/x-www-form-encoded.""" MultipartForm = aiohttp.FormData -"""Type hint for content of type multipart/form-data""" +"""Type hint for content of type multipart/form-data.""" # MyPy does not support recursive types yet. This has been ongoing for a long time, unfortunately. # See https://github.com/python/typing/issues/182 JSONObject = typing.Dict[str, typing.Any] -"""Type hint for a JSON-decoded object representation as a dict.""" +"""Type hint for a JSON-decoded object representation as a mapping.""" JSONArray = typing.List[typing.Any] -"""Type hint for a JSON-decoded array representation as a list.""" +"""Type hint for a JSON-decoded array representation as a sequence.""" JSONAny = typing.Union[str, int, float, bool, None, JSONArray, JSONObject] """Type hint for any valid JSON-decoded type.""" @@ -110,6 +110,8 @@ def put( self, key: str, value: typing.Union[undefined.Undefined, typing.Any], + /, + *, conversion: typing.Optional[typing.Callable[[typing.Any], typing.Any]] = None, ) -> None: """Add a key and value to the string map. @@ -144,27 +146,16 @@ def put( else: value = str(value) - self[key] = value - - @classmethod - def from_dict(cls, d: typing.Union[undefined.Undefined, typing.Dict[str, typing.Any]]) -> StringMapBuilder: - """Build a query from an existing dict.""" - sb = cls() - - if isinstance(d, undefined.Undefined): - return sb - - for k, v in d.items(): - sb.put(k, v) - - return sb + # __setitem__ just overwrites the previous value. + self.add(key, value) class JSONObjectBuilder(typing.Dict[str, JSONAny]): """Helper class used to quickly build JSON objects from various values. If provided with any values that are - `hikari.utilities.unspecified.Unspecified`, then these values will be ignored. + `hikari.utilities.unspecified.Unspecified`, then these values will be + ignored. This speeds up generation of JSON payloads for low level HTTP and websocket API interaction. @@ -181,7 +172,12 @@ def __init__(self): super().__init__() def put( - self, key: str, value: typing.Any, conversion: typing.Optional[typing.Callable[[typing.Any], JSONAny]] = None, + self, + key: str, + value: typing.Any, + /, + *, + conversion: typing.Optional[typing.Callable[[typing.Any], JSONAny]] = None, ) -> None: """Put a JSON value. @@ -191,11 +187,11 @@ def put( ---------- key : str The key to give the element. - value : JSONType | typing.Any | hikari.utilities.undefined.Undefined + value : JSONAny or typing.Any or hikari.utilities.undefined.Undefined The JSON type to put. This may be a non-JSON type if a conversion is also specified. This may alternatively be undefined. In the latter case, nothing is performed. - conversion : typing.Callable[[typing.Any], JSONType] | None + conversion : typing.Callable[[typing.Any], JSONAny] or None Optional conversion to apply. """ if not isinstance(value, undefined.Undefined): @@ -208,6 +204,8 @@ def put_array( self, key: str, values: typing.Union[undefined.Undefined, typing.Iterable[T]], + /, + *, conversion: typing.Optional[typing.Callable[[T], JSONAny]] = None, ) -> None: """Put a JSON array. @@ -233,14 +231,14 @@ def put_array( else: self[key] = list(values) - def put_snowflake(self, key: str, value: typing.Union[undefined.Undefined, typing.SupportsInt, int]) -> None: + def put_snowflake(self, key: str, value: typing.Union[undefined.Undefined, typing.SupportsInt, int], /) -> None: """Put a key with a snowflake value into the builder. Parameters ---------- key : str The key to give the element. - value : JSONType | hikari.utilities.undefined.Undefined + value : JSONAny or hikari.utilities.undefined.Undefined The JSON type to put. This may alternatively be undefined. In the latter case, nothing is performed. """ @@ -248,7 +246,7 @@ def put_snowflake(self, key: str, value: typing.Union[undefined.Undefined, typin self[key] = str(int(value)) def put_snowflake_array( - self, key: str, values: typing.Union[undefined.Undefined, typing.Iterable[typing.SupportsInt, int]] + self, key: str, values: typing.Union[undefined.Undefined, typing.Iterable[typing.SupportsInt, int]], / ) -> None: """Put an array of snowflakes with the given key into this builder. @@ -269,7 +267,7 @@ def put_snowflake_array( def cast_json_array( - array: JSONArray, cast: typing.Callable[[JSONAny], T], collection_type: typing.Type[CollectionT] = typing.List[T], + array: JSONArray, cast: typing.Callable[[JSONAny], T], collection_type: typing.Type[CollectionT] = list, ) -> CollectionT: """Cast a JSON array to a given generic collection type. @@ -294,13 +292,20 @@ def cast_json_array( collection_type : typing.Type[CollectionT] The container type to store the cast items within. `CollectionT` should be a concrete implementation that is - a subtype of `typing.Collection`, such as `typing.List`, - `typing.Set`, `typing.FrozenSet`, `typing.Tuple`, etc. - If unspecified, this defaults to `typing.List`. + a subtype of `typing.Collection`, such as `list`, `set`, `frozenSet`, + `tuple`, etc. If unspecified, this defaults to `list`. Returns ------- CollectionT The generated collection. + + Example + ------- + ```py + >>> arr = [123, 456, 789, 123] + >>> cast_json_array(arr, str, set) + {"456", "123", "789"} + ``` """ return collection_type(cast(item) for item in array) diff --git a/requirements.txt b/requirements.txt index 9f2c046784..51569d09ca 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,3 @@ aiohttp~=3.6.2 attrs~=19.3.0 +multidict~=4.7.6 diff --git a/tests/hikari/utilities/test_data_binding.py b/tests/hikari/utilities/test_data_binding.py new file mode 100644 index 0000000000..a57bdeb7de --- /dev/null +++ b/tests/hikari/utilities/test_data_binding.py @@ -0,0 +1,245 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# Copyright © Nekoka.tt 2019-2020 +# +# This file is part of Hikari. +# +# Hikari is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Hikari is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Hikari. If not, see . +import typing + +import mock +import multidict +import pytest + +from hikari.models import bases +from hikari.utilities import data_binding +from hikari.utilities import snowflake +from hikari.utilities import undefined + + +class MyUnique(bases.Unique): + def __init__(self, value): + self.id = snowflake.Snowflake(value) + + +class TestStringMapBuilder: + def test_is_mapping(self): + assert isinstance(data_binding.StringMapBuilder(), typing.Mapping) + + def test_is_multidict(self): + assert isinstance(data_binding.StringMapBuilder(), multidict.MultiDict) + + def test_starts_empty(self): + mapping = data_binding.StringMapBuilder() + assert mapping == {} + + def test_put_undefined(self): + mapping = data_binding.StringMapBuilder() + mapping.put("foo", undefined.Undefined()) + assert dict(mapping) == {} + + def test_put_general_value_casts_to_str(self): + m = mock.MagicMock() + mapping = data_binding.StringMapBuilder() + mapping.put("foo", m) + assert dict(mapping) == {"foo": m.__str__()} + + def test_duplicate_puts_stores_values_as_sequence(self): + m1 = mock.MagicMock() + m2 = mock.MagicMock() + mapping = data_binding.StringMapBuilder() + mapping.put("foo", m1) + mapping.put("foo", m2) + assert mapping.getall("foo") == [m1.__str__(), m2.__str__()] + assert list(mapping.keys()) == ["foo", "foo"] + + def test_put_Unique(self): + mapping = data_binding.StringMapBuilder() + + mapping.put("myunique", MyUnique(123)) + assert dict(mapping) == {"myunique": "123"} + + def test_put_int(self): + mapping = data_binding.StringMapBuilder() + mapping.put("yeet", 420_69) + assert dict(mapping) == {"yeet": "42069"} + + @pytest.mark.parametrize( + ["name", "input_val", "expect"], [("a", True, "true"), ("b", False, "false"), ("c", None, "null")] + ) + def test_put_py_singleton(self, name, input_val, expect): + mapping = data_binding.StringMapBuilder() + mapping.put(name, input_val) + assert dict(mapping) == {name: expect} + + def test_put_with_conversion_uses_return_value(self): + def convert(_): + return "yeah, i got called" + + mapping = data_binding.StringMapBuilder() + mapping.put("blep", "meow", conversion=convert) + assert dict(mapping) == {"blep": "yeah, i got called"} + + def test_put_with_conversion_passes_raw_input_to_converter(self): + mapping = data_binding.StringMapBuilder() + convert = mock.MagicMock() + + expect = object() + mapping.put("yaskjgakljglak", expect, conversion=convert) + convert.assert_called_once_with(expect) + + def test_put_py_singleton_conversion_runs_before_check(self): + def convert(_): + return True + + mapping = data_binding.StringMapBuilder() + mapping.put("im hungry", "yo", conversion=convert) + assert dict(mapping) == {"im hungry": "true"} + + +class TestJSONObjectBuilder: + def test_is_mapping(self): + assert isinstance(data_binding.JSONObjectBuilder(), typing.Mapping) + + def test_starts_empty(self): + assert data_binding.JSONObjectBuilder() == {} + + def test_put_undefined(self): + builder = data_binding.JSONObjectBuilder() + builder.put("foo", undefined.Undefined()) + assert builder == {} + + def test_put_defined(self): + m = mock.MagicMock() + builder = data_binding.JSONObjectBuilder() + builder.put("bar", m) + assert builder == {"bar": m} + + def test_put_with_conversion_uses_conversion_result(self): + m = mock.MagicMock() + convert = mock.MagicMock() + builder = data_binding.JSONObjectBuilder() + builder.put("rawr", m, conversion=convert) + assert builder == {"rawr": convert()} + + def test_put_with_conversion_passes_raw_input_to_converter(self): + m = mock.MagicMock() + convert = mock.MagicMock() + builder = data_binding.JSONObjectBuilder() + builder.put("bar", m, conversion=convert) + convert.assert_called_once_with(m) + + def test_put_array_undefined(self): + builder = data_binding.JSONObjectBuilder() + builder.put_array("dd", undefined.Undefined()) + assert builder == {} + + def test__put_array_defined(self): + m1 = mock.MagicMock() + m2 = mock.MagicMock() + m3 = mock.MagicMock() + builder = data_binding.JSONObjectBuilder() + builder.put_array("ttt", [m1, m2, m3]) + assert builder == {"ttt": [m1, m2, m3]} + + def test_put_array_with_conversion_uses_conversion_result(self): + r1 = mock.MagicMock() + r2 = mock.MagicMock() + r3 = mock.MagicMock() + + convert = mock.MagicMock(side_effect=[r1, r2, r3]) + builder = data_binding.JSONObjectBuilder() + builder.put_array("www", [object(), object(), object()], conversion=convert) + assert builder == {"www": [r1, r2, r3]} + + def test_put_array_with_conversion_passes_raw_input_to_converter(self): + m1 = mock.MagicMock() + m2 = mock.MagicMock() + m3 = mock.MagicMock() + + convert = mock.MagicMock() + builder = data_binding.JSONObjectBuilder() + builder.put_array("xxx", [m1, m2, m3], conversion=convert) + assert convert.call_args_list[0] == mock.call(m1) + assert convert.call_args_list[1] == mock.call(m2) + assert convert.call_args_list[2] == mock.call(m3) + + def test_put_snowflake_undefined(self): + builder = data_binding.JSONObjectBuilder() + builder.put_snowflake("nya!", undefined.Undefined()) + assert builder == {} + + @pytest.mark.parametrize( + ("input_value", "expected_str"), + [ + (100123, "100123"), + ("100124", "100124"), + (MyUnique(100127), "100127"), + (MyUnique("100129"), "100129"), + (snowflake.Snowflake(100125), "100125"), + (snowflake.Snowflake("100126"), "100126"), + ], + ) + def test_put_snowflake(self, input_value, expected_str): + builder = data_binding.JSONObjectBuilder() + builder.put_snowflake("WAWAWA!", input_value) + assert builder == {"WAWAWA!": expected_str} + + @pytest.mark.parametrize( + ("input_value", "expected_str"), + [ + (100123, "100123"), + ("100124", "100124"), + (MyUnique(100127), "100127"), + (MyUnique("100129"), "100129"), + (snowflake.Snowflake(100125), "100125"), + (snowflake.Snowflake("100126"), "100126"), + ], + ) + def test_put_snowflake_array_conversions(self, input_value, expected_str): + builder = data_binding.JSONObjectBuilder() + builder.put_snowflake_array("WAWAWAH!", [input_value] * 5) + assert builder == {"WAWAWAH!": [expected_str] * 5} + + def test_put_snowflake_array(self): + builder = data_binding.JSONObjectBuilder() + builder.put_snowflake_array("DESU!", [123, 456, 987, 115]) + assert builder == {"DESU!": ["123", "456", "987", "115"]} + + +class TestCastJSONArray: + def test_cast_is_invoked_with_each_item(self): + cast = mock.MagicMock() + arr = ["foo", "bar", "baz"] + + data_binding.cast_json_array(arr, cast) + + assert cast.call_args_list[0] == mock.call("foo") + assert cast.call_args_list[1] == mock.call("bar") + assert cast.call_args_list[2] == mock.call("baz") + + def test_cast_result_is_used_for_each_item(self): + r1 = mock.MagicMock() + r2 = mock.MagicMock() + r3 = mock.MagicMock() + cast = mock.MagicMock(side_effect=[r1, r2, r3]) + + arr = ["foo", "bar", "baz"] + + assert data_binding.cast_json_array(arr, cast) == [r1, r2, r3] + + def test_cast_with_custom_container(self): + cast = lambda obj: obj + arr = ["foo", "bar", "baz", "foo"] + assert data_binding.cast_json_array(arr, cast, set) == {"foo", "bar", "baz"} From ad29df6de7a305374e2bef83613fb5f04b0c179c Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Sat, 6 Jun 2020 14:54:42 +0100 Subject: [PATCH 475/922] PR feedback on reflect changes. --- hikari/utilities/reflect.py | 8 +++++++- tests/hikari/utilities/test_reflect.py | 16 ++++++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/hikari/utilities/reflect.py b/hikari/utilities/reflect.py index b9ae3486e3..0d81704b76 100644 --- a/hikari/utilities/reflect.py +++ b/hikari/utilities/reflect.py @@ -50,12 +50,18 @@ def resolve_signature(func: typing.Callable[..., typing.Any]) -> inspect.Signatu """ signature = inspect.signature(func) resolved_typehints = typing.get_type_hints(func) - return_annotation = resolved_typehints.pop("return") if "return" in resolved_typehints else EMPTY params = [] + none_type = type(None) for name, param in signature.parameters.items(): if isinstance(param.annotation, str): param = param.replace(annotation=resolved_typehints[name] if name in resolved_typehints else EMPTY) + if param.annotation is none_type: + param = param.replace(annotation=None) params.append(param) + return_annotation = resolved_typehints.get("return", EMPTY) + if return_annotation is none_type: + return_annotation = None + return signature.replace(parameters=params, return_annotation=return_annotation) diff --git a/tests/hikari/utilities/test_reflect.py b/tests/hikari/utilities/test_reflect.py index f449895ecf..97e0d2eeef 100644 --- a/tests/hikari/utilities/test_reflect.py +++ b/tests/hikari/utilities/test_reflect.py @@ -77,6 +77,22 @@ def foo(bar: str, bat: "int"): assert signature.parameters["bat"].annotation is int assert signature.return_annotation is reflect.EMPTY + def test_handles_None(self): + def foo(bar: None) -> None: + ... + + signature = reflect.resolve_signature(foo) + assert signature.parameters["bar"].annotation is None + assert signature.return_annotation is None + + def test_handles_NoneType(self): + def foo(bar: type(None)) -> type(None): + ... + + signature = reflect.resolve_signature(foo) + assert signature.parameters["bar"].annotation is None + assert signature.return_annotation is None + def test_handles_only_return_annotated(self): def foo(bar, bat) -> str: ... From 243f748a854dd7eafdeb10820b7d0d7cec9437fb Mon Sep 17 00:00:00 2001 From: Forbidden-A Date: Sat, 6 Jun 2020 16:25:12 +0300 Subject: [PATCH 476/922] Fix docs (Changed `\` to `or`) --- README.md | 4 ++-- hikari/errors.py | 4 ++-- hikari/models/files.py | 6 +++--- hikari/models/guilds.py | 12 ++++++------ hikari/models/invites.py | 4 ++-- hikari/models/presences.py | 2 +- hikari/models/webhooks.py | 16 ++++++++-------- hikari/net/gateway.py | 12 ++++++------ hikari/net/http_client.py | 12 ++++++------ hikari/net/rate_limits.py | 2 +- hikari/utilities/cdn.py | 2 +- 11 files changed, 38 insertions(+), 38 deletions(-) diff --git a/README.md b/README.md index 22de6c7d5a..bf002b2847 100644 --- a/README.md +++ b/README.md @@ -22,8 +22,8 @@ bot = hikari.Bot(token="...") async def ping(event): # If a non-bot user sends a message "hk.ping", respond with "Pong!" - if not event.author.is_bot and event.content.startswith("hk.ping"): - await event.reply("Pong!") + if not event.message.author.is_bot and event.message.content.startswith("hk.ping"): + await event.message.reply("Pong!") bot.run() diff --git a/hikari/errors.py b/hikari/errors.py index 7be8b10606..3ab32a1036 100644 --- a/hikari/errors.py +++ b/hikari/errors.py @@ -111,9 +111,9 @@ class GatewayServerClosedConnectionError(GatewayError): Parameters ---------- - reason : str | None + reason : str or None A string explaining the issue. - code : int | None + code : int or None The close code. """ diff --git a/hikari/models/files.py b/hikari/models/files.py index bf504dd1d8..91f4b0c442 100644 --- a/hikari/models/files.py +++ b/hikari/models/files.py @@ -473,13 +473,13 @@ class FileStream(BaseStream): Parameters ---------- - filename : str | None + filename : str or None The custom file name to give the file when uploading it. May be omitted. - path : os.PathLike | str + path : os.PathLike or str The path-like object that describes the file to upload. - executor : concurrent.futures.Executor | None + executor : concurrent.futures.Executor or None An optional executor to run the IO operations in. If not specified, the default executor for this loop will be used instead. diff --git a/hikari/models/guilds.py b/hikari/models/guilds.py index 944d5c7fdd..ab8d682e7f 100644 --- a/hikari/models/guilds.py +++ b/hikari/models/guilds.py @@ -434,7 +434,7 @@ def format_icon_url(self, *, format_: typing.Optional[str] = None, size: int = 4 Returns ------- - str | None + str or None The string URL. Raises @@ -494,7 +494,7 @@ def format_splash_url(self, *, format_: str = "png", size: int = 4096) -> typing Returns ------- - str | None + str or None The string URL. Raises @@ -525,7 +525,7 @@ def format_discovery_splash_url(self, *, format_: str = "png", size: int = 4096) Returns ------- - str | None + str or None The string URL. Raises @@ -857,7 +857,7 @@ def format_splash_url(self, *, format_: str = "png", size: int = 4096) -> typing Returns ------- - str | None + str or None The string URL. Raises @@ -888,7 +888,7 @@ def format_discovery_splash_url(self, *, format_: str = "png", size: int = 4096) Returns ------- - str | None + str or None The string URL. Raises @@ -921,7 +921,7 @@ def format_banner_url(self, *, format_: str = "png", size: int = 4096) -> typing Returns ------- - str | None + str or None The string URL. Raises diff --git a/hikari/models/invites.py b/hikari/models/invites.py index 90125de7bd..2e68c8ac26 100644 --- a/hikari/models/invites.py +++ b/hikari/models/invites.py @@ -103,7 +103,7 @@ def format_splash_url(self, *, format_: str = "png", size: int = 4096) -> typing Returns ------- - str | None + str or None The string URL. Raises @@ -134,7 +134,7 @@ def format_banner_url(self, *, format_: str = "png", size: int = 4096) -> typing Returns ------- - str | None + str or None The string URL. Raises diff --git a/hikari/models/presences.py b/hikari/models/presences.py index 3c3e4a3fcf..f47cb4dce4 100644 --- a/hikari/models/presences.py +++ b/hikari/models/presences.py @@ -306,7 +306,7 @@ def format_avatar_url( Returns ------- - hikari.models.undefined.Undefined | str + hikari.models.undefined.Undefined or str The string URL of the user's custom avatar if either `PresenceUser.avatar_hash` is set or their default avatar if `PresenceUser.discriminator` is set, else `hikari.models.undefined.Undefined`. diff --git a/hikari/models/webhooks.py b/hikari/models/webhooks.py index 90006321d4..4e118d099b 100644 --- a/hikari/models/webhooks.py +++ b/hikari/models/webhooks.py @@ -136,18 +136,18 @@ async def execute( mentions_everyone : bool Whether `@everyone` and `@here` mentions should be resolved by discord and lead to actual pings, defaults to `True`. - user_mentions : typing.Collection[hikari.models.users.User | hikari.models.snowflake.Snowflake | int | str] | bool + user_mentions : typing.Collection[hikari.models.users.User or hikari.utilities.snowflake.Snowflake or int or str] or bool Either an array of user objects/IDs to allow mentions for, `True` to allow all user mentions or `False` to block all user mentions from resolving, defaults to `True`. - role_mentions: typing.Collection[hikari.models.guilds.Role | hikari.models.snowflake.Snowflake | int | str] | bool + role_mentions: typing.Collection[hikari.models.guilds.Role or hikari.utilities.snowflake.Snowflake or int or str] or bool Either an array of guild role objects/IDs to allow mentions for, `True` to allow all role mentions or `False` to block all role mentions from resolving, defaults to `True`. Returns ------- - hikari.models.messages.Message | None + hikari.models.messages.Message or None The created message object, if `wait` is `True`, else `None`. Raises @@ -233,7 +233,7 @@ async def delete(self, *, use_token: typing.Optional[bool] = None,) -> None: Parameters ---------- - use_token : bool | None + use_token : bool or None If set to `True` then the webhook's token will be used for this request; if set to `False` then bot authorization will be used; if not specified then the webhook's token will be used for the @@ -272,17 +272,17 @@ async def edit( ---------- name : str If specified, the new name string. - avatar : hikari.models.files.BaseStream | None + avatar : hikari.models.files.BaseStream or None If specified, the new avatar image. If `None`, then it is removed. - channel : hikari.models.channels.GuildChannel | hikari.models.snowflake.Snowflake | int + channel : hikari.models.channels.GuildChannel or hikari.utilities.snowflake.Snowflake or int If specified, the object or ID of the new channel the given webhook should be moved to. reason : str If specified, the audit log reason explaining why the operation was performed. This field will be used when using the webhook's token rather than bot authorization. - use_token : bool | None + use_token : bool or None If set to `True` then the webhook's token will be used for this request; if set to `False` then bot authorization will be used; if not specified then the webhook's token will be used for the @@ -361,7 +361,7 @@ async def fetch_self(self, *, use_token: typing.Optional[bool] = None) -> Webhoo Parameters ---------- - use_token : bool | None + use_token : bool or None If set to `True` then the webhook's token will be used for this request; if set to `False` then bot authorization will be used; if not specified then the webhook's token will be used for the diff --git a/hikari/net/gateway.py b/hikari/net/gateway.py index 81b9825df8..7efe23b41a 100644 --- a/hikari/net/gateway.py +++ b/hikari/net/gateway.py @@ -76,7 +76,7 @@ class Gateway(http_client.HTTPClient, component.IComponent): Whether to appear to be AFK or not on login. initial_status : hikari.models.presences.PresenceStatus or hikari.utilities.undefined.Undefined The initial status to set on login for the shard. - intents : hikari.models.intents.Intent | None + intents : hikari.models.intents.Intent or None Collection of intents to use, or `None` to not use intents at all. large_threshold : int The number of members to have in a guild for it to be considered large. @@ -382,16 +382,16 @@ async def update_presence( Parameters ---------- - idle_since : datetime.datetime | None | hikari.utilities.undefined.Undefined + idle_since : datetime.datetime or None or hikari.utilities.undefined.Undefined The datetime that the user started being idle. If undefined, this will not be changed. - is_afk : bool | hikari.utilities.undefined.Undefined + is_afk : bool or hikari.utilities.undefined.Undefined If `True`, the user is marked as AFK. If `False`, the user is marked as being active. If undefined, this will not be changed. - activity : hikari.models.presences.OwnActivity | None | hikari.utilities.undefined.Undefined + activity : hikari.models.presences.OwnActivity or None or hikari.utilities.undefined.Undefined The activity to appear to be playing. If undefined, this will not be changed. - status : hikari.models.presences.PresenceStatus | hikari.utilities.undefined.Undefined + status : hikari.models.presences.PresenceStatus or hikari.utilities.undefined.Undefined The web status to show. If undefined, this will not be changed. """ payload = self._build_presence_payload(idle_since, is_afk, activity, status) @@ -415,7 +415,7 @@ async def update_voice_state( ---------- guild : hikari.models.guilds.PartialGuild or hikari.utilities.snowflake.Snowflake or int or str The guild or guild ID to update the voice state for. - channel : hikari.models.channels.GuildVoiceChannel | hikari.utilities.Snowflake | int | str | None + channel : hikari.models.channels.GuildVoiceChannel or hikari.utilities.Snowflake or int or str or None The channel or channel ID to update the voice state for. If `None` then the bot will leave the voice channel that it is in for the given guild. diff --git a/hikari/net/http_client.py b/hikari/net/http_client.py index 33cc932b88..7df312c47b 100644 --- a/hikari/net/http_client.py +++ b/hikari/net/http_client.py @@ -50,23 +50,23 @@ class HTTPClient(abc.ABC): # pylint:disable=too-many-instance-attributes ---------- allow_redirects : bool Whether to allow redirects or not. Defaults to `False`. - connector : aiohttp.BaseConnector | None + connector : aiohttp.BaseConnector or None Optional aiohttp _connector info for making an HTTP connection debug : bool Defaults to `False`. If `True`, then a lot of contextual information regarding low-level HTTP communication will be logged to the _debug logger on this class. - proxy_auth : aiohttp.BasicAuth | None + proxy_auth : aiohttp.BasicAuth or None Optional authorization to be used if using a proxy. - proxy_url : str | None + proxy_url : str or None Optional proxy URL to use for HTTP requests. - ssl_context : ssl.SSLContext | None + ssl_context : ssl.SSLContext or None The optional SSL context to be used. verify_ssl : bool Whether or not the client should enforce SSL signed certificate verification. If 1 it will ignore potentially malicious SSL certificates. - timeout : float | None + timeout : float or None The optional _request_timeout for all HTTP requests. trust_env : bool If `True`, and no proxy info is given, then `HTTP_PROXY` and @@ -234,7 +234,7 @@ async def _perform_request( The URL to hit. headers : typing.Dict[str, str] Headers to use when making the request. - body : aiohttp.FormData | dict | list | None + body : aiohttp.FormData or dict or list or None The body to send. Currently this will send the content in a form body if you pass an instance of `aiohttp.FormData`, or as a JSON body if you pass a `list` or `dict`. Any other types diff --git a/hikari/net/rate_limits.py b/hikari/net/rate_limits.py index be1c8b082b..138e3d76bf 100644 --- a/hikari/net/rate_limits.py +++ b/hikari/net/rate_limits.py @@ -434,7 +434,7 @@ class ExponentialBackOff: ---------- base : float The base to use. Defaults to `2`. - maximum : float | None + maximum : float or None If not `None`, then this is the max value the backoff can be in a single iteration before an `asyncio.TimeoutError` is raised. Defaults to `64` seconds. diff --git a/hikari/utilities/cdn.py b/hikari/utilities/cdn.py index c78e54ea7b..8b84b982bd 100644 --- a/hikari/utilities/cdn.py +++ b/hikari/utilities/cdn.py @@ -42,7 +42,7 @@ def generate_cdn_url(*route_parts: str, format_: str, size: typing.Optional[int] The format to use for the wanted cdn entity, will usually be one of `webp`, `png`, `jpeg`, `jpg` or `gif` (which will be invalid if the target entity doesn't have an animated version available). - size : int | None + size : int or None The size to specify for the image in the query string if applicable, should be passed through as None to avoid the param being set. Must be any power of two between 16 and 4096. From 4a166b6b1deae610a9b27d890bcf804198136331 Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Sat, 6 Jun 2020 15:46:13 +0100 Subject: [PATCH 477/922] Fixes #379 --- hikari/impl/entity_factory.py | 6 +++++- hikari/models/guilds.py | 2 +- tests/hikari/impl/test_entity_factory.py | 3 ++- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/hikari/impl/entity_factory.py b/hikari/impl/entity_factory.py index c43e3922ee..8350d98c61 100644 --- a/hikari/impl/entity_factory.py +++ b/hikari/impl/entity_factory.py @@ -731,7 +731,11 @@ def deserialize_member( guild_member = guild_models.Member(self._app) guild_member.user = user or self.deserialize_user(payload["user"]) guild_member.role_ids = {snowflake.Snowflake(role_id) for role_id in payload["roles"]} - guild_member.joined_at = date.iso8601_datetime_string_to_datetime(payload["joined_at"]) + + if (joined_at := payload.get("joined_at")) is not None: + guild_member.joined_at = date.iso8601_datetime_string_to_datetime(joined_at) + else: + guild_member.joined_at = undefined.Undefined() guild_member.nickname = payload["nick"] if "nick" in payload else undefined.Undefined() diff --git a/hikari/models/guilds.py b/hikari/models/guilds.py index ab8d682e7f..44d0cdeac1 100644 --- a/hikari/models/guilds.py +++ b/hikari/models/guilds.py @@ -241,7 +241,7 @@ class Member(bases.Entity): ) """A sequence of the IDs of the member's current roles.""" - joined_at: datetime.datetime = attr.ib(eq=False, hash=False) + joined_at: typing.Union[datetime.datetime, undefined.Undefined] = attr.ib(eq=False, hash=False) """The datetime of when this member joined the guild they belong to.""" premium_since: typing.Union[datetime.datetime, None, undefined.Undefined] = attr.ib(eq=False, hash=False) diff --git a/tests/hikari/impl/test_entity_factory.py b/tests/hikari/impl/test_entity_factory.py index 050992ec33..f4d4694a20 100644 --- a/tests/hikari/impl/test_entity_factory.py +++ b/tests/hikari/impl/test_entity_factory.py @@ -1221,7 +1221,7 @@ def test_deserialize_member_with_null_fields(self, entity_factory_impl, user_pay { "nick": None, "roles": ["11111", "22222", "33333", "44444"], - "joined_at": "2015-04-26T06:26:56.936000+00:00", + "joined_at": None, "premium_since": None, "deaf": False, "mute": True, @@ -1233,6 +1233,7 @@ def test_deserialize_member_with_null_fields(self, entity_factory_impl, user_pay assert member.is_deaf is False assert member.is_mute is True assert isinstance(member, guild_models.Member) + assert member.joined_at is undefined.Undefined() def test_deserialize_member_with_undefined_fields(self, entity_factory_impl, user_payload): member = entity_factory_impl.deserialize_member( From a1e2dae30054365c8e59c09e9817b14782e290d7 Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Sat, 6 Jun 2020 15:50:19 +0100 Subject: [PATCH 478/922] Windows runners for CI. --- ci/gitlab/bases.yml | 33 ++++++++---------- ci/gitlab/installations.yml | 47 +++++++++++-------------- ci/gitlab/integrations.yml | 20 ++++------- ci/gitlab/linting.yml | 17 ++------- ci/gitlab/pages.yml | 40 ++++++++------------- ci/gitlab/releases.yml | 65 +++++++++++++++------------------- ci/gitlab/tests.yml | 69 +++++++++++++++++-------------------- 7 files changed, 118 insertions(+), 173 deletions(-) diff --git a/ci/gitlab/bases.yml b/ci/gitlab/bases.yml index 5df39cc5da..58a2ef8258 100644 --- a/ci/gitlab/bases.yml +++ b/ci/gitlab/bases.yml @@ -45,31 +45,17 @@ extends: .any-job image: busybox:1.31.1-musl -### -### CPython 3.8.0 configuration. -### -.cpython:3.8.0: - extends: .any-job - image: python:3.8.0 - -### -### CPython 3.8.1 configuration. -### -.cpython:3.8.1: - extends: .any-job - image: python:3.8.1 - ### ### CPython 3.8.2 configuration. ### -.cpython:3.8.2: +.cpython:3.8: extends: .any-job - image: python:3.8.2 + image: python:3.8 ### ### CPython 3.9.0 configuration. ### -.cpython:3.9.0: +.cpython:3.9rc: extends: .any-job image: python:3.9-rc @@ -77,5 +63,14 @@ ### Most recent stable CPython release. ### .cpython: - extends: .any-job - extends: .cpython:3.8.2 + extends: + - .cpython:3.8 + +### +### Shared windows runner. +### +.win32: + tags: + - shared-windows + - windows + - windows-1809 diff --git a/ci/gitlab/installations.yml b/ci/gitlab/installations.yml index b87fa9acd5..07a217a69a 100644 --- a/ci/gitlab/installations.yml +++ b/ci/gitlab/installations.yml @@ -21,49 +21,44 @@ .pip: extends: .reactive-job script: - - |+ - set -e - set -x - apt install git -y - pip install nox 'virtualenv<20.0.19' - nox --sessions pip-sdist pip-bdist-wheel pip-git --no-error-on-external-run + - apt install git -y + - pip install nox + - nox --sessions pip-sdist pip-bdist-wheel pip-git --no-error-on-external-run stage: test -### -### Setup script tests for CPython 3.8.0 -### -### Test locally installing the library via pip in several formats -### -"install:3.8.0": - extends: - - .cpython:3.8.0 - - .pip ### -### Setup script tests for CPython 3.8.1 +### Setup script tests for Windows. +### +### This simply tests that stuff runs on a Windows runner. ### ### Test locally installing the library via pip in several formats ### -"install:3.8.1": - extends: - - .cpython:3.8.1 - - .pip +"install:win32": + extends: .win32 + script: + - choco install --no-progress python -y + - refreshenv + - py -3 -V + - py -3 -m pip install nox + - py -3 -m nox --sessions pip-sdist pip-bdist-wheel --no-error-on-external-run + stage: test ### -### Setup script tests for CPython 3.8.2 +### Setup script tests for CPython 3.8 ### ### Test locally installing the library via pip in several formats ### -"install:3.8.2": +"install:3.8": extends: - - .cpython:3.8.2 + - .cpython:3.8 - .pip ### -### Setup script tests for CPython 3.9.0 +### Setup script tests for CPython 3.9rc ### ### Test locally installing the library via pip in several formats ### -"install:3.9.0": +"install:3.9rc": extends: - - .cpython:3.9.0 + - .cpython:3.9rc - .pip diff --git a/ci/gitlab/integrations.yml b/ci/gitlab/integrations.yml index 7cb9e67108..49ec127f97 100644 --- a/ci/gitlab/integrations.yml +++ b/ci/gitlab/integrations.yml @@ -32,9 +32,8 @@ dependency_scanning: interruptible: true retry: 1 stage: test - # Do not enable until gitlab 13.0 on 2019-05-22. - #rules: - # - if: "$CI_PIPELINE_SOURCE == 'merge_request_event' || $CI_PIPELINE_SOURCE == 'schedule'" + rules: + - if: "$CI_PIPELINE_SOURCE == 'merge_request_event' || $CI_PIPELINE_SOURCE == 'schedule'" variables: DS_DEFAULT_ANALYZERS: "gemnasium-python" @@ -50,9 +49,8 @@ dependency_scanning: license_scanning: interruptible: true retry: 1 - # Do not enable until gitlab 13.0 on 2019-05-22. - #rules: - # - if: "$CI_PIPELINE_SOURCE == 'merge_request_event' || $CI_PIPELINE_SOURCE == 'schedule'" + rules: + - if: "$CI_PIPELINE_SOURCE == 'merge_request_event' || $CI_PIPELINE_SOURCE == 'schedule'" stage: test ### @@ -75,10 +73,7 @@ pages: rules: - if: "$CI_COMMIT_REF_NAME == 'staging'" script: - - |+ - set -e - set -x - ls public -R + - ls public -R stage: deploy ### @@ -92,9 +87,8 @@ pages: sast: interruptible: true retry: 1 - # Do not enable until gitlab 13.0 on 2019-05-22. - #rules: - # - if: "$CI_PIPELINE_SOURCE == 'merge_request_event' || $CI_PIPELINE_SOURCE == 'schedule'" + rules: + - if: "$CI_PIPELINE_SOURCE == 'merge_request_event' || $CI_PIPELINE_SOURCE == 'schedule'" stage: test variables: SAST_BANDIT_EXCLUDED_PATHS: tests/*,docs/*,ci/*,insomnia/*,public/*,tasks/*,noxfile.old.py diff --git a/ci/gitlab/linting.yml b/ci/gitlab/linting.yml index 0f7079c08a..09b4c41668 100644 --- a/ci/gitlab/linting.yml +++ b/ci/gitlab/linting.yml @@ -21,7 +21,7 @@ .lint: allow_failure: false before_script: - - pip install nox 'virtualenv<20.0.19' + - pip install nox - mkdir public || true extends: - .cpython @@ -45,7 +45,7 @@ pylint: junit: public/pylint.xml extends: .lint script: - - nox -s pylint --no-error-on-external-run + - nox -s pylint --no-error-on-external-run ### ### Documentation linting. @@ -70,7 +70,7 @@ black: - nox -s check-formatting --no-error-on-external-run -- --check --diff ### -### Security checks #1 +### Security checks. ### ### This runs static application security tests in addition to those that ### GitLab provides. Any issues are reported and the pipeline is aborted. @@ -80,14 +80,3 @@ safety: script: - nox -s safety --no-error-on-external-run -### -### Security checks #2. -### -### This runs static application security tests in addition to those that -### GitLab provides. Any issues are reported and the pipeline is aborted. -### Currently commented out as it is run by the GitLab SAST. -### -# bandit: -# extends: .lint -# script: -# - nox -s bandit --no-error-on-external-run diff --git a/ci/gitlab/pages.yml b/ci/gitlab/pages.yml index f1a748373c..b74227d97c 100644 --- a/ci/gitlab/pages.yml +++ b/ci/gitlab/pages.yml @@ -23,15 +23,13 @@ paths: - public/ before_script: - - |+ - set -e - set -x - apt-get install git -qy + - apt-get install git -qy + - > git log -n 1 "${CI_COMMIT_SHA}" --format="%B" \ | grep -iqE "\[\s*(skip|no|don'?t|do\s+not)\s+pages?\s*\]" \ - && echo "SKIPPING ${CI_JOB_STAGE} STAGE JOB" \ - && exit 0 - mkdir public || true + && echo "SKIPPING ${CI_JOB_STAGE} STAGE JOB" \ + && exit 0 + - mkdir public || true extends: .cpython stage: report @@ -44,12 +42,10 @@ max: 2 rules: - if: "$CI_COMMIT_REF_NAME == 'staging' && $CI_COMMIT_TAG == null" - script: |+ - set -x - apt-get update - apt-get install -qy graphviz git # FIXME: Remove once in master. - pip install nox 'virtualenv<20.0.19' - nox --sessions pages pdoc --no-error-on-external-run + script: + - apt-get update + - pip install nox + - nox --sessions pages pdoc --no-error-on-external-run ### ### Generate pages for staging branch and export them. @@ -70,13 +66,10 @@ pages:test: - if: "$CI_COMMIT_REF_NAME != 'master' && $CI_COMMIT_REF_NAME != 'staging'" - if: "$CI_PIPELINE_SOURCE == 'push' || $CI_PIPELINE_SOURCE == 'web'" script: - - |+ - set -e - set -x - apt-get update - apt-get install -qy graphviz # FIXME: remove once in master - pip install nox 'virtualenv<20.0.19' - nox --sessions pages pdoc --no-error-on-external-run + - apt-get update + - apt-get install -qy graphviz # FIXME: remove once in master + - pip install nox + - nox --sessions pages pdoc --no-error-on-external-run ### @@ -91,8 +84,5 @@ pages:feedback: rules: - if: "$CI_PIPELINE_SOURCE == 'merge_request_event'" script: - - |+ - set -e - set -x - pip install nox 'virtualenv<20.0.19' - nox --sessions pages pdoc --no-error-on-external-run + - pip install nox 'virtualenv<20.0.19' + - nox --sessions pages pdoc --no-error-on-external-run diff --git a/ci/gitlab/releases.yml b/ci/gitlab/releases.yml index 64e2ddb115..e72c6bf82f 100644 --- a/ci/gitlab/releases.yml +++ b/ci/gitlab/releases.yml @@ -21,20 +21,17 @@ .release: allow_failure: false before_script: - - |+ - set -e - set -x + - > git log -n 1 "${CI_COMMIT_SHA}" --format="%B" \ | grep -iqE "\[\s*(skip|no|don'?t|do\s+not)\s+deploy(ments?)?\s*\]" \ && echo "SKIPPING ${CI_JOB_STAGE} STAGE JOB" \ && exit 0 - - # We don't start on the branch, but a ref of it, which isn't useful - echo "Changing from ref to actual branch" - git fetch -ap - git checkout ${TARGET_BRANCH} - git reset --hard origin/${TARGET_BRANCH} - + # We don't start on the branch, but a ref of it, which isn't useful + - echo "Changing from ref to actual branch" + - git fetch -ap + - git checkout ${TARGET_BRANCH} + - git reset --hard origin/${TARGET_BRANCH} + - > echo echo "========================= hikari/_about.py =========================" echo @@ -42,8 +39,7 @@ echo echo "====================================================================" echo - - pip install twine wheel nox 'virtualenv<20.0.19' + - pip install twine wheel nox extends: .cpython interruptible: false resource_group: deploy @@ -62,10 +58,7 @@ release:staging: url: https://nekokatt.gitlab.io/hikari # FIXME: change to staging URL when appropriate. extends: .release script: - - |+ - set -e - set -x - nox -s deploy --no-error-on-external-run + - nox -s deploy --no-error-on-external-run rules: - if: "$CI_PIPELINE_SOURCE == 'push' && $CI_COMMIT_TAG == null && $CI_COMMIT_REF_NAME == 'staging'" variables: @@ -83,33 +76,29 @@ release:master: url: https://nekokatt.gitlab.io/hikari extends: .release script: - - |+ - set -e - set -x - - apt-get update - apt-get install curl openssh-client -qy + - apt-get update + - apt-get install curl openssh-client -qy - # Don't dox our keys, or spam echos - set +x + # Don't dox our keys, or spam echos + - set +x - # Init the SSH agent and add our gitlab private key to it. - echo ">>>> Starting SSH agent <<<<" - eval "$(ssh-agent -s)" - echo ">>>> Making ~/.ssh <<<<" - mkdir ~/.ssh || true - echo ">>>> Writing SSH private key to ~/.ssh/id_rsa <<<<" - echo "${GIT_SSH_PRIVATE_KEY}" > ~/.ssh/id_rsa - echo ">>>> SSH IS READY <<<<" + # Init the SSH agent and add our gitlab private key to it. + - echo ">>>> Starting SSH agent <<<<" + - eval "$(ssh-agent -s)" + - echo ">>>> Making ~/.ssh <<<<" + - mkdir ~/.ssh || true + - echo ">>>> Writing SSH private key to ~/.ssh/id_rsa <<<<" + - echo "${GIT_SSH_PRIVATE_KEY}" > ~/.ssh/id_rsa + - echo ">>>> SSH IS READY <<<<" - # Be verbose again. - set -x + # Be verbose again. + - set -x - chmod 600 ~/.ssh/id_rsa - ssh-keyscan -t rsa gitlab.com >> ~/.ssh/known_hosts - ssh-add ~/.ssh/id_rsa + - chmod 600 ~/.ssh/id_rsa + - ssh-keyscan -t rsa gitlab.com >> ~/.ssh/known_hosts + - ssh-add ~/.ssh/id_rsa - nox -s deploy --no-error-on-external-run + - nox -s deploy --no-error-on-external-run rules: - if: "$CI_PIPELINE_SOURCE == 'push' && $CI_COMMIT_TAG == null && $CI_COMMIT_REF_NAME == 'master'" variables: diff --git a/ci/gitlab/tests.yml b/ci/gitlab/tests.yml index cef1b58454..1c4c661c23 100644 --- a/ci/gitlab/tests.yml +++ b/ci/gitlab/tests.yml @@ -29,15 +29,12 @@ extends: .reactive-job interruptible: true script: - - |+ - set -e - set -x - apt-get install -qy git gcc g++ make - pip install nox 'virtualenv<20.0.19' - ls -arhl - nox -s pytest --no-error-on-external-run - ls -arhl - mv .coverage "public/${CI_JOB_NAME}.coverage" + - apt-get install -qy git gcc g++ make + - pip install nox + - ls -arhl + - nox -s pytest --no-error-on-external-run + - ls -arhl + - mv .coverage "public/${CI_JOB_NAME}.coverage" stage: test ### @@ -53,41 +50,40 @@ coverage:results: - .cpython - .reactive-job script: - - |+ - set -e - set -x - pip install nox 'virtualenv<20.0.19' - nox -s coalesce-coverage --no-error-on-external-run + - pip install nox virtualenv + - nox -s coalesce-coverage --no-error-on-external-run stage: pre-report -### -### Run CPython 3.8.0 unit tests and collect test coverage stats. -### -### Coverage is exported as an artifact with a name matching the job name. -### -test:3.8.0: - extends: - - .cpython:3.8.0 - - .test ### -### Run CPython 3.8.1 unit tests and collect test coverage stats. +### Run CPython unit tests and collect test coverage stats on Windows. ### ### Coverage is exported as an artifact with a name matching the job name. ### -test:3.8.1: - extends: - - .cpython:3.8.1 - - .test +test:win32: + artifacts: + paths: + - public/coverage + - public/*.coverage + reports: + junit: public/tests.xml + extends: .win32 + script: + - choco install --no-progress python -y + - py -3 -V + - py -3 -m pip install nox + - py -3 -m nox -s pytest --no-error-on-external-run + - mv .coverage "public/${CI_JOB_NAME}.coverage" + stage: test ### ### Run CPython 3.8.2 unit tests and collect test coverage stats. ### ### Coverage is exported as an artifact with a name matching the job name. ### -test:3.8.2: +test:3.8: extends: - - .cpython:3.8.2 + - .cpython:3.8 - .test ### @@ -95,9 +91,9 @@ test:3.8.2: ### ### Coverage is exported as an artifact with a name matching the job name. ### -test:3.9.0: +test:3.9rc: extends: - - .cpython:3.9.0 + - .cpython:3.9rc - .test @@ -111,10 +107,7 @@ test:twemoji_mapping: - hikari/emojis.py - hikari.utilities/urls.py script: - - |+ - set -e - set -x - apt-get install -qy git gcc g++ make - pip install nox 'virtualenv<20.0.19' - nox -s twemoji-test + - apt-get install -qy git gcc g++ make + - pip install nox + - nox -s twemoji-test stage: test From e79ab9c4c8ce2a4e5ac3b4945edd982e340e0ffb Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Sat, 6 Jun 2020 16:28:48 +0100 Subject: [PATCH 479/922] Addressing yet another Windows bug with tools. --- ci/gitlab/tests.yml | 3 ++- ci/pytest.nox.py | 2 -- dev-requirements.txt | 15 +++++---------- 3 files changed, 7 insertions(+), 13 deletions(-) diff --git a/ci/gitlab/tests.yml b/ci/gitlab/tests.yml index 1c4c661c23..7ab2bb118e 100644 --- a/ci/gitlab/tests.yml +++ b/ci/gitlab/tests.yml @@ -70,10 +70,11 @@ test:win32: extends: .win32 script: - choco install --no-progress python -y + - if (!(test-path "public")) { New-Item -ItemType Directory -Force -Path public } - py -3 -V - py -3 -m pip install nox - py -3 -m nox -s pytest --no-error-on-external-run - - mv .coverage "public/${CI_JOB_NAME}.coverage" + - mv .coverage ".\public\test-win32.coverage" stage: test ### diff --git a/ci/pytest.nox.py b/ci/pytest.nox.py index 8e83eaa453..36c23578f4 100644 --- a/ci/pytest.nox.py +++ b/ci/pytest.nox.py @@ -29,8 +29,6 @@ "-r", "a", "--full-trace", - "-n", - "auto", "--cov", config.MAIN_PACKAGE, "--cov-config", diff --git a/dev-requirements.txt b/dev-requirements.txt index 5ad1e9e1e4..189a714030 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -1,16 +1,11 @@ async-timeout~=3.0.1 coverage~=5.1 nox==2020.5.24 -# Virtualenv 20.0.19 breaks nox randomnly... virtualenv==20.0.21 -# Pylint 2.5.0 is broken pylint==2.5.2 -pylint-json2html-v2~=0.2.2 -pylint-junit~=0.2.0 -pytest~=5.4.3 -pytest-asyncio~=0.12.0 -pytest-cov~=2.9.0 -pytest-randomly~=3.4.0 -pytest-testdox~=1.2.1 -pytest-xdist~=1.32.0 +# 5.4.3 breaks stuff for us on Windows as usual... +pytest==5.4.2 +pytest-asyncio==0.12.0 +pytest-cov==2.9.0 +pytest-testdox==1.2.1 mock~=4.0.2 From 3a1b9f446676da7e027137781ef508dee30bc3e9 Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Sat, 6 Jun 2020 18:04:52 +0100 Subject: [PATCH 480/922] Moved pages pipelines around a little. --- .gitlab-ci.yml | 2 +- ...{integrations.yml => gitlab-templates.yml} | 23 ------- ci/gitlab/pages.yml | 62 +++++++------------ ci/gitlab/tests.yml | 2 +- 4 files changed, 26 insertions(+), 63 deletions(-) rename ci/gitlab/{integrations.yml => gitlab-templates.yml} (82%) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 4bdb0c9414..9c6bfc8684 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -29,8 +29,8 @@ variables: include: - local: "/ci/gitlab/bases.yml" + - local: "/ci/gitlab/gitlab-templates.yml" - local: "/ci/gitlab/installations.yml" - - local: "/ci/gitlab/integrations.yml" - local: "/ci/gitlab/linting.yml" - local: "/ci/gitlab/pages.yml" - local: "/ci/gitlab/releases.yml" diff --git a/ci/gitlab/integrations.yml b/ci/gitlab/gitlab-templates.yml similarity index 82% rename from ci/gitlab/integrations.yml rename to ci/gitlab/gitlab-templates.yml index 49ec127f97..6f3d0e522f 100644 --- a/ci/gitlab/integrations.yml +++ b/ci/gitlab/gitlab-templates.yml @@ -53,29 +53,6 @@ license_scanning: - if: "$CI_PIPELINE_SOURCE == 'merge_request_event' || $CI_PIPELINE_SOURCE == 'schedule'" stage: test -### -### Deploy the pages to GitLab. -### -### This pulls the results of generate-master-pages and generate-stating-pages -### and places them in the correct -### -### This must use a specific name. Do not rename it for consistency. -### -pages: - artifacts: - paths: - - public/ - dependencies: - - pylint - - coverage:results - - pages:staging - extends: .busybox:musl - rules: - - if: "$CI_COMMIT_REF_NAME == 'staging'" - script: - - ls public -R - stage: deploy - ### ### GitLab's Static Application Security Testing suite. ### diff --git a/ci/gitlab/pages.yml b/ci/gitlab/pages.yml index b74227d97c..662aa3adce 100644 --- a/ci/gitlab/pages.yml +++ b/ci/gitlab/pages.yml @@ -34,55 +34,41 @@ stage: report ### -### Base for generating documentation ready to be published. -### -.pages:generate: - extends: .pages - retry: - max: 2 - rules: - - if: "$CI_COMMIT_REF_NAME == 'staging' && $CI_COMMIT_TAG == null" - script: - - apt-get update - - pip install nox - - nox --sessions pages pdoc --no-error-on-external-run - -### -### Generate pages for staging branch and export them. -### -pages:staging: - extends: .pages:generate - variables: - TARGET_BRANCH: staging - -### -### Generate pages for the current branch as a test. +### Generate pages for the current branch as a test, enabling the visual feedback +### stuff to allow in-site comments being relayed back to the associated merge +### request. ### -pages:test: +pdoc3: extends: + # DO NOT EXTEND ANYTHING WITH AN only/except BLOCK! - .pages - - .reactive-job rules: - - if: "$CI_COMMIT_REF_NAME != 'master' && $CI_COMMIT_REF_NAME != 'staging'" + - if: "$CI_PIPELINE_SOURCE == 'merge_request_event'" + - if: "$CI_COMMIT_REF_NAME != 'master'" - if: "$CI_PIPELINE_SOURCE == 'push' || $CI_PIPELINE_SOURCE == 'web'" script: - - apt-get update - - apt-get install -qy graphviz # FIXME: remove once in master - pip install nox - nox --sessions pages pdoc --no-error-on-external-run ### -### Generate pages for the current branch as a test, enabling the visual feedback -### stuff to allow in-site comments being relayed back to the associated merge -### request. +### Deploy the pages to GitLab. ### -pages:feedback: - extends: - # DO NOT EXTEND ANYTHING WITH AN only/except BLOCK! - - .pages +### This pulls the results of generate-master-pages and generate-stating-pages +### and places them in the correct +### +### This must use a specific name. Do not rename it for consistency. +### +pages: + artifacts: + paths: + - public/ + dependencies: + - coverage:results + - pdoc3 + extends: .busybox:musl rules: - - if: "$CI_PIPELINE_SOURCE == 'merge_request_event'" + - if: "$CI_COMMIT_REF_NAME == 'staging'" script: - - pip install nox 'virtualenv<20.0.19' - - nox --sessions pages pdoc --no-error-on-external-run + - ls public -R + stage: deploy diff --git a/ci/gitlab/tests.yml b/ci/gitlab/tests.yml index 7ab2bb118e..c7f4e40586 100644 --- a/ci/gitlab/tests.yml +++ b/ci/gitlab/tests.yml @@ -52,7 +52,7 @@ coverage:results: script: - pip install nox virtualenv - nox -s coalesce-coverage --no-error-on-external-run - stage: pre-report + stage: report ### From d0098c121f2bf02e242e99623021924e13d6a285 Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Sat, 6 Jun 2020 20:32:09 +0100 Subject: [PATCH 481/922] Fixed bug in HTTP endpoint kwargs. --- hikari/net/rest.py | 26 +++++++++++++++----------- hikari/net/rest_utils.py | 14 ++++++++++---- 2 files changed, 25 insertions(+), 15 deletions(-) diff --git a/hikari/net/rest.py b/hikari/net/rest.py index 1e3dea570c..01d6a84a02 100644 --- a/hikari/net/rest.py +++ b/hikari/net/rest.py @@ -546,7 +546,9 @@ async def edit_channel( body.put("rate_limit_per_user", rate_limit_per_user) body.put_snowflake("parent_id", parent_category) body.put_array( - "permission_overwrites", permission_overwrites, self._app.entity_factory.serialize_permission_overwrite + "permission_overwrites", + permission_overwrites, + conversion=self._app.entity_factory.serialize_permission_overwrite, ) response = await self._request(route, body=body, reason=reason) @@ -788,7 +790,7 @@ async def create_invite( """ route = routes.POST_CHANNEL_INVITES.compile(channel=channel) body = data_binding.JSONObjectBuilder() - body.put("max_age", max_age, date.timespan_to_int) + body.put("max_age", max_age, conversion=date.timespan_to_int) body.put("max_uses", max_uses) body.put("temporary", temporary) body.put("unique", unique) @@ -1141,8 +1143,8 @@ async def create_message( body = data_binding.JSONObjectBuilder() body.put("allowed_mentions", self._generate_allowed_mentions(mentions_everyone, user_mentions, role_mentions)) - body.put("content", text, str) - body.put("embed", embed, self._app.entity_factory.serialize_embed) + body.put("content", text, conversion=str) + body.put("embed", embed, conversion=self._app.entity_factory.serialize_embed) body.put("nonce", nonce) body.put("tts", tts) @@ -1217,8 +1219,8 @@ async def edit_message( """ route = routes.PATCH_CHANNEL_MESSAGE.compile(channel=channel, message=message) body = data_binding.JSONObjectBuilder() - body.put("content", text, str) - body.put("embed", embed, self._app.entity_factory.serialize_embed) + body.put("content", text, conversion=str) + body.put("embed", embed, conversion=self._app.entity_factory.serialize_embed) body.put("flags", flags) body.put("allowed_mentions", self._generate_allowed_mentions(mentions_everyone, user_mentions, role_mentions)) response = await self._request(route, body=body) @@ -1520,7 +1522,7 @@ async def execute_webhook( body = data_binding.JSONObjectBuilder() body.put("mentions", self._generate_allowed_mentions(mentions_everyone, user_mentions, role_mentions)) - body.put("content", text, str) + body.put("content", text, conversion=str) body.put("embeds", serialized_embeds) body.put("username", username) body.put("avatar_url", avatar_url) @@ -1788,12 +1790,12 @@ async def edit_guild( route = routes.PATCH_GUILD.compile(guild=guild) body = data_binding.JSONObjectBuilder() body.put("name", name) - body.put("region", region, str) + body.put("region", region, conversion=str) body.put("verification", verification_level) body.put("notifications", default_message_notifications) body.put("explicit_content_filter", explicit_content_filter_level) body.put("afk_timeout", afk_timeout) - body.put("preferred_locale", preferred_locale, str) + body.put("preferred_locale", preferred_locale, conversion=str) body.put_snowflake("afk_channel_id", afk_channel) body.put_snowflake("owner_id", owner) body.put_snowflake("system_channel_id", system_channel) @@ -1966,7 +1968,9 @@ async def _create_guild_channel( body.put("rate_limit_per_user", rate_limit_per_user) body.put_snowflake("category_id", category) body.put_array( - "permission_overwrites", permission_overwrites, self._app.entity_factory.serialize_permission_overwrite + "permission_overwrites", + permission_overwrites, + conversion=self._app.entity_factory.serialize_permission_overwrite, ) response = await self._request(route, body=body, reason=reason) @@ -2240,7 +2244,7 @@ async def edit_integration( route = routes.PATCH_GUILD_INTEGRATION.compile(guild=guild, integration=integration) body = data_binding.JSONObjectBuilder() body.put("expire_behaviour", expire_behaviour) - body.put("expire_grace_period", expire_grace_period, date.timespan_to_int) + body.put("expire_grace_period", expire_grace_period, conversion=date.timespan_to_int) # Inconsistent naming in the API itself, so I have changed the name. body.put("enable_emoticons", enable_emojis) await self._request(route, body=body, reason=reason) diff --git a/hikari/net/rest_utils.py b/hikari/net/rest_utils.py index b46df3c8f5..d7dac608f1 100644 --- a/hikari/net/rest_utils.py +++ b/hikari/net/rest_utils.py @@ -186,7 +186,9 @@ def add_category( payload.put("nsfw", nsfw) payload.put_array( - "permission_overwrites", permission_overwrites, self._app.entity_factory.serialize_permission_overwrite + "permission_overwrites", + permission_overwrites, + conversion=self._app.entity_factory.serialize_permission_overwrite, ) self._channels.append(payload) @@ -212,13 +214,15 @@ def add_text_channel( payload.put("name", name) payload.put("type", channels.ChannelType.GUILD_TEXT) payload.put("topic", topic) - payload.put("rate_limit_per_user", rate_limit_per_user, date.timespan_to_int) + payload.put("rate_limit_per_user", rate_limit_per_user, conversion=date.timespan_to_int) payload.put("position", position) payload.put("nsfw", nsfw) payload.put_snowflake("parent_id", parent_id) payload.put_array( - "permission_overwrites", permission_overwrites, self._app.entity_factory.serialize_permission_overwrite + "permission_overwrites", + permission_overwrites, + conversion=self._app.entity_factory.serialize_permission_overwrite, ) self._channels.append(payload) @@ -250,7 +254,9 @@ def add_voice_channel( payload.put_snowflake("parent_id", parent_id) payload.put_array( - "permission_overwrites", permission_overwrites, self._app.entity_factory.serialize_permission_overwrite + "permission_overwrites", + permission_overwrites, + conversion=self._app.entity_factory.serialize_permission_overwrite, ) self._channels.append(payload) From 422490c87d2542fa0f0dcea466cbbf439d5d5a32 Mon Sep 17 00:00:00 2001 From: Faster Speeding Date: Sun, 7 Jun 2020 19:59:24 +0100 Subject: [PATCH 482/922] Explicitly pass repr=False to attr.ib in places for attributes we don't want to be in the repr. --- hikari/events/channel.py | 10 +-- hikari/events/guild.py | 12 +-- hikari/events/message.py | 48 ++++++------ hikari/events/other.py | 16 ++-- hikari/events/voice.py | 4 +- hikari/models/applications.py | 46 +++++------ hikari/models/audit_logs.py | 14 ++-- hikari/models/bases.py | 4 +- hikari/models/channels.py | 44 ++++++----- hikari/models/embeds.py | 56 ++++++-------- hikari/models/emojis.py | 18 ++--- hikari/models/guilds.py | 142 ++++++++++++++++------------------ hikari/models/invites.py | 22 +++--- hikari/models/messages.py | 44 +++++------ hikari/models/presences.py | 80 ++++++++----------- hikari/models/users.py | 20 ++--- hikari/models/voices.py | 22 +++--- hikari/models/webhooks.py | 8 +- 18 files changed, 286 insertions(+), 324 deletions(-) diff --git a/hikari/events/channel.py b/hikari/events/channel.py index 4b2a28666f..37e829f3ec 100644 --- a/hikari/events/channel.py +++ b/hikari/events/channel.py @@ -54,7 +54,7 @@ class BaseChannelEvent(base_events.HikariEvent, abc.ABC): """A base object that Channel events will inherit from.""" - channel: channels.PartialChannel = attr.ib() + channel: channels.PartialChannel = attr.ib(repr=True) """The object of the channel this event involved.""" @@ -89,7 +89,7 @@ class ChannelPinsUpdateEvent(base_events.HikariEvent, base_models.Entity): when a pinned message is deleted. """ - guild_id: typing.Optional[snowflake.Snowflake] = attr.ib() + guild_id: typing.Optional[snowflake.Snowflake] = attr.ib(repr=True) """The ID of the guild where this event happened. Will be `None` if this happened in a DM channel. @@ -140,10 +140,10 @@ class TypingStartEvent(base_events.HikariEvent, base_models.Entity): user_id: snowflake.Snowflake = attr.ib(repr=True) """The ID of the user who triggered this typing event.""" - timestamp: datetime.datetime = attr.ib() + timestamp: datetime.datetime = attr.ib(repr=False) """The datetime of when this typing event started.""" - member: typing.Optional[guilds.Member] = attr.ib() + member: typing.Optional[guilds.Member] = attr.ib(repr=False) """The member object of the user who triggered this typing event. Will be `None` if this was triggered in a DM. @@ -155,7 +155,7 @@ class TypingStartEvent(base_events.HikariEvent, base_models.Entity): class InviteCreateEvent(base_events.HikariEvent): """Represents a gateway Invite Create event.""" - invite: invites.InviteWithMetadata = attr.ib() + invite: invites.InviteWithMetadata = attr.ib(repr=True) """The object of the invite being created.""" diff --git a/hikari/events/guild.py b/hikari/events/guild.py index fb5d816906..7be5a8d2ff 100644 --- a/hikari/events/guild.py +++ b/hikari/events/guild.py @@ -72,7 +72,7 @@ class GuildCreateEvent(GuildEvent): available to a guild (either due to outage or at startup). """ - guild: guilds.Guild = attr.ib() + guild: guilds.Guild = attr.ib(repr=True) """The object of the guild that's being created.""" @@ -81,7 +81,7 @@ class GuildCreateEvent(GuildEvent): class GuildUpdateEvent(GuildEvent): """Used to represent Guild Update gateway events.""" - guild: guilds.Guild = attr.ib() + guild: guilds.Guild = attr.ib(repr=True) """The object of the guild that's being updated.""" @@ -134,7 +134,7 @@ class GuildBanRemoveEvent(GuildBanEvent): class GuildEmojisUpdateEvent(GuildEvent, base_models.Entity): """Represents a Guild Emoji Update gateway event.""" - guild_id: snowflake.Snowflake = attr.ib() + guild_id: snowflake.Snowflake = attr.ib(repr=True) """The ID of the guild this emoji was updated in.""" emojis: typing.Mapping[snowflake.Snowflake, emojis_models.KnownCustomEmoji] = attr.ib(repr=True) @@ -176,7 +176,7 @@ class GuildMemberUpdateEvent(GuildMemberEvent): Sent when a guild member or their inner user object is updated. """ - member: guilds.Member = attr.ib() + member: guilds.Member = attr.ib(repr=True) @base_events.requires_intents(intents.Intent.GUILD_MEMBERS) @@ -208,7 +208,7 @@ class GuildRoleCreateEvent(GuildRoleEvent, base_models.Entity): guild_id: snowflake.Snowflake = attr.ib(repr=True) """The ID of the guild where this role was created.""" - role: guilds.Role = attr.ib() + role: guilds.Role = attr.ib(repr=True) """The object of the role that was created.""" @@ -246,5 +246,5 @@ class PresenceUpdateEvent(GuildEvent): Sent when a guild member changes their presence. """ - presence: presences.MemberPresence = attr.ib() + presence: presences.MemberPresence = attr.ib(repr=True) """The object of the presence being updated.""" diff --git a/hikari/events/message.py b/hikari/events/message.py index 80f23bedd4..2fdf000fbd 100644 --- a/hikari/events/message.py +++ b/hikari/events/message.py @@ -58,7 +58,7 @@ class MessageCreateEvent(base_events.HikariEvent): """Used to represent Message Create gateway events.""" - message: messages.Message = attr.ib() + message: messages.Message = attr.ib(repr=True) class UpdateMessage(messages.Message): @@ -83,67 +83,67 @@ class UpdateMessage(messages.Message): # TODO: can we merge member and author together? # We could override deserialize to to this and then reorganise the payload, perhaps? - member: typing.Union[guilds.Member, undefined.Undefined] = attr.ib() + member: typing.Union[guilds.Member, undefined.Undefined] = attr.ib(repr=False) """The member properties for the message's author.""" - content: typing.Union[str, undefined.Undefined] = attr.ib() + content: typing.Union[str, undefined.Undefined] = attr.ib(repr=False) """The content of the message.""" - timestamp: typing.Union[datetime.datetime, undefined.Undefined] = attr.ib() + timestamp: typing.Union[datetime.datetime, undefined.Undefined] = attr.ib(repr=False) """The timestamp that the message was sent at.""" - edited_timestamp: typing.Union[datetime.datetime, undefined.Undefined, None] = attr.ib() + edited_timestamp: typing.Union[datetime.datetime, undefined.Undefined, None] = attr.ib(repr=False) """The timestamp that the message was last edited at. Will be `None` if the message wasn't ever edited. """ - is_tts: typing.Union[bool, undefined.Undefined] = attr.ib() + is_tts: typing.Union[bool, undefined.Undefined] = attr.ib(repr=False) """Whether the message is a TTS message.""" - is_mentioning_everyone: typing.Union[bool, undefined.Undefined] = attr.ib() + is_mentioning_everyone: typing.Union[bool, undefined.Undefined] = attr.ib(repr=False) """Whether the message mentions `@everyone` or `@here`.""" - user_mentions: typing.Union[typing.Set[snowflake.Snowflake], undefined.Undefined] = attr.ib() + user_mentions: typing.Union[typing.Set[snowflake.Snowflake], undefined.Undefined] = attr.ib(repr=False) """The users the message mentions.""" - role_mentions: typing.Union[typing.Set[snowflake.Snowflake], undefined.Undefined] = attr.ib() + role_mentions: typing.Union[typing.Set[snowflake.Snowflake], undefined.Undefined] = attr.ib(repr=False) """The roles the message mentions.""" - channel_mentions: typing.Union[typing.Set[snowflake.Snowflake], undefined.Undefined] = attr.ib() + channel_mentions: typing.Union[typing.Set[snowflake.Snowflake], undefined.Undefined] = attr.ib(repr=False) """The channels the message mentions.""" - attachments: typing.Union[typing.Sequence[messages.Attachment], undefined.Undefined] = attr.ib() + attachments: typing.Union[typing.Sequence[messages.Attachment], undefined.Undefined] = attr.ib(repr=False) """The message attachments.""" - embeds: typing.Union[typing.Sequence[embed_models.Embed], undefined.Undefined] = attr.ib() + embeds: typing.Union[typing.Sequence[embed_models.Embed], undefined.Undefined] = attr.ib(repr=False) """The message's embeds.""" - reactions: typing.Union[typing.Sequence[messages.Reaction], undefined.Undefined] = attr.ib() + reactions: typing.Union[typing.Sequence[messages.Reaction], undefined.Undefined] = attr.ib(repr=False) """The message's reactions.""" - is_pinned: typing.Union[bool, undefined.Undefined] = attr.ib() + is_pinned: typing.Union[bool, undefined.Undefined] = attr.ib(repr=False) """Whether the message is pinned.""" - webhook_id: typing.Union[snowflake.Snowflake, undefined.Undefined] = attr.ib() + webhook_id: typing.Union[snowflake.Snowflake, undefined.Undefined] = attr.ib(repr=False) """If the message was generated by a webhook, the webhook's ID.""" - type: typing.Union[messages.MessageType, undefined.Undefined] = attr.ib() + type: typing.Union[messages.MessageType, undefined.Undefined] = attr.ib(repr=False) """The message's type.""" - activity: typing.Union[messages.MessageActivity, undefined.Undefined] = attr.ib() + activity: typing.Union[messages.MessageActivity, undefined.Undefined] = attr.ib(repr=False) """The message's activity.""" - application: typing.Optional[applications.Application] = attr.ib() + application: typing.Optional[applications.Application] = attr.ib(repr=False) """The message's application.""" - message_reference: typing.Union[messages.MessageCrosspost, undefined.Undefined] = attr.ib() + message_reference: typing.Union[messages.MessageCrosspost, undefined.Undefined] = attr.ib(repr=False) """The message's cross-posted reference data.""" - flags: typing.Union[messages.MessageFlag, undefined.Undefined] = attr.ib() + flags: typing.Union[messages.MessageFlag, undefined.Undefined] = attr.ib(repr=False) """The message's flags.""" - nonce: typing.Union[str, undefined.Undefined] = attr.ib() + nonce: typing.Union[str, undefined.Undefined] = attr.ib(repr=False) """The message nonce. This is a string used for validating a message was sent. @@ -163,7 +163,7 @@ class MessageUpdateEvent(base_events.HikariEvent, base_models.Unique): it has not been changed. """ - message: UpdateMessage + message: UpdateMessage = attr.ib(repr=True) """The partial message object with all updated fields.""" @@ -208,7 +208,7 @@ class MessageDeleteBulkEvent(base_events.HikariEvent, base_models.Entity): This will be `None` if these messages were bulk deleted in a DM channel. """ - message_ids: typing.Set[snowflake.Snowflake] = attr.ib() + message_ids: typing.Set[snowflake.Snowflake] = attr.ib(repr=False) """A collection of the IDs of the messages that were deleted.""" @@ -237,7 +237,7 @@ class MessageReactionAddEvent(BaseMessageReactionEvent): """The ID of the user adding the reaction.""" # TODO: does this contain a user? If not, should it be a PartialGuildMember? - member: typing.Optional[guilds.Member] = attr.ib() + member: typing.Optional[guilds.Member] = attr.ib(repr=False) """The member object of the user who's adding this reaction. This will be `None` if this is happening in a DM channel. diff --git a/hikari/events/other.py b/hikari/events/other.py index ad38680249..bcbf207400 100644 --- a/hikari/events/other.py +++ b/hikari/events/other.py @@ -52,13 +52,13 @@ class ExceptionEvent(base_events.HikariEvent): """Descriptor for an exception thrown while processing an event.""" - exception: Exception = attr.ib() + exception: Exception = attr.ib(repr=True) """The exception that was raised.""" - event: base_events.HikariEvent = attr.ib() + event: base_events.HikariEvent = attr.ib(repr=True) """The event that was being invoked when the exception occurred.""" - callback: typing.Callable[[base_events.HikariEvent], typing.Awaitable[None]] = attr.ib() + callback: typing.Callable[[base_events.HikariEvent], typing.Awaitable[None]] = attr.ib(repr=False) """The event that was being invoked when the exception occurred.""" @@ -91,7 +91,7 @@ class StoppedEvent(base_events.HikariEvent): class ConnectedEvent(base_events.HikariEvent): """Event invoked each time a shard connects.""" - shard: gateway_client.Gateway = attr.ib() + shard: gateway_client.Gateway = attr.ib(repr=True) """The shard that connected.""" @@ -100,7 +100,7 @@ class ConnectedEvent(base_events.HikariEvent): class DisconnectedEvent(base_events.HikariEvent): """Event invoked each time a shard disconnects.""" - shard: gateway_client.Gateway = attr.ib() + shard: gateway_client.Gateway = attr.ib(repr=True) """The shard that disconnected.""" @@ -108,7 +108,7 @@ class DisconnectedEvent(base_events.HikariEvent): class ResumedEvent(base_events.HikariEvent): """Represents a gateway Resume event.""" - shard: gateway_client.Gateway = attr.ib() + shard: gateway_client.Gateway = attr.ib(repr=True) """The shard that reconnected.""" @@ -119,7 +119,7 @@ class ReadyEvent(base_events.HikariEvent): This is received only when IDENTIFYing with the gateway. """ - shard: gateway_client.Gateway = attr.ib() + shard: gateway_client.Gateway = attr.ib(repr=False) """The shard that is ready.""" gateway_version: int = attr.ib(repr=True) @@ -128,7 +128,7 @@ class ReadyEvent(base_events.HikariEvent): my_user: users.OwnUser = attr.ib(repr=True) """The object of the current bot account this connection is for.""" - unavailable_guilds: typing.Mapping[snowflake.Snowflake, guilds.UnavailableGuild] = attr.ib() + unavailable_guilds: typing.Mapping[snowflake.Snowflake, guilds.UnavailableGuild] = attr.ib(repr=False) """A mapping of the guilds this bot is currently in. All guilds will start off "unavailable". diff --git a/hikari/events/voice.py b/hikari/events/voice.py index ec63f3b445..d57830322c 100644 --- a/hikari/events/voice.py +++ b/hikari/events/voice.py @@ -42,7 +42,7 @@ class VoiceStateUpdateEvent(base_events.HikariEvent): Sent when a user joins, leaves or moves voice channel(s). """ - state: voices.VoiceState = attr.ib() + state: voices.VoiceState = attr.ib(repr=True) """The object of the voice state that's being updated.""" @@ -54,7 +54,7 @@ class VoiceServerUpdateEvent(base_events.HikariEvent): falls over to a new server. """ - token: str = attr.ib() + token: str = attr.ib(repr=False) """The voice connection's string token.""" guild_id: snowflake.Snowflake = attr.ib(repr=True) diff --git a/hikari/models/applications.py b/hikari/models/applications.py index 8570b9d406..43363e5e69 100644 --- a/hikari/models/applications.py +++ b/hikari/models/applications.py @@ -205,23 +205,19 @@ class OwnConnection: type: str = attr.ib(eq=False, hash=False, repr=True) """The type of service this connection is for.""" - is_revoked: bool = attr.ib( - eq=False, hash=False, - ) + is_revoked: bool = attr.ib(eq=False, hash=False, repr=False) """`True` if the connection has been revoked.""" - integrations: typing.Sequence[guilds.PartialIntegration] = attr.ib( - eq=False, hash=False, - ) + integrations: typing.Sequence[guilds.PartialIntegration] = attr.ib(eq=False, hash=False, repr=False) """A sequence of the partial guild integration objects this connection has.""" - is_verified: bool = attr.ib(eq=False, hash=False) + is_verified: bool = attr.ib(eq=False, hash=False, repr=False) """`True` if the connection has been verified.""" - is_friend_sync_enabled: bool = attr.ib(eq=False, hash=False) + is_friend_sync_enabled: bool = attr.ib(eq=False, hash=False, repr=False) """`True` if friends should be added based on this connection.""" - is_activity_visible: bool = attr.ib(eq=False, hash=False) + is_activity_visible: bool = attr.ib(eq=False, hash=False, repr=False) """`True` if this connection's activities are shown in the user's presence.""" visibility: ConnectionVisibility = attr.ib(eq=False, hash=False, repr=True) @@ -235,7 +231,7 @@ class OwnGuild(guilds.PartialGuild): is_owner: bool = attr.ib(eq=False, hash=False, repr=True) """`True` when the current user owns this guild.""" - my_permissions: permissions_.Permission = attr.ib(eq=False, hash=False) + my_permissions: permissions_.Permission = attr.ib(eq=False, hash=False, repr=False) """The guild-level permissions that apply to the current user or bot.""" @@ -254,10 +250,10 @@ class TeamMembershipState(int, enum.Enum): class TeamMember(bases.Entity): """Represents a member of a Team.""" - membership_state: TeamMembershipState = attr.ib(eq=False, hash=False) + membership_state: TeamMembershipState = attr.ib(eq=False, hash=False, repr=False) """The state of this user's membership.""" - permissions: typing.Set[str] = attr.ib(eq=False, hash=False) + permissions: typing.Set[str] = attr.ib(eq=False, hash=False, repr=False) """This member's permissions within a team. At the time of writing, this will always be a set of one `str`, which @@ -275,13 +271,13 @@ class TeamMember(bases.Entity): class Team(bases.Entity, bases.Unique): """Represents a development team, along with all its members.""" - icon_hash: typing.Optional[str] = attr.ib(eq=False, hash=False) + icon_hash: typing.Optional[str] = attr.ib(eq=False, hash=False, repr=False) """The CDN hash of this team's icon. If no icon is provided, this will be `None`. """ - members: typing.Mapping[snowflake.Snowflake, TeamMember] = attr.ib(eq=False, hash=False) + members: typing.Mapping[snowflake.Snowflake, TeamMember] = attr.ib(eq=False, hash=False, repr=False) """A mapping containing each member in this team. The mapping maps keys containing the member's ID to values containing the @@ -338,7 +334,7 @@ class Application(bases.Entity, bases.Unique): """The name of this application.""" # TODO: default to None for consistency? - description: str = attr.ib(eq=False, hash=False) + description: str = attr.ib(eq=False, hash=False, repr=False) """The description of this application, or an empty string if undefined.""" is_bot_public: typing.Optional[bool] = attr.ib(eq=False, hash=False, repr=True) @@ -347,7 +343,7 @@ class Application(bases.Entity, bases.Unique): Will be `None` if this application doesn't have an associated bot. """ - is_bot_code_grant_required: typing.Optional[bool] = attr.ib(eq=False, hash=False) + is_bot_code_grant_required: typing.Optional[bool] = attr.ib(eq=False, hash=False, repr=False) """`True` if this application's bot is requiring code grant for invites. Will be `None` if this application doesn't have a bot. @@ -360,40 +356,40 @@ class Application(bases.Entity, bases.Unique): Discord's oauth2 flow. """ - rpc_origins: typing.Optional[typing.Set[str]] = attr.ib(eq=False, hash=False) + rpc_origins: typing.Optional[typing.Set[str]] = attr.ib(eq=False, hash=False, repr=False) """A collection of this application's RPC origin URLs, if RPC is enabled.""" - summary: str = attr.ib(eq=False, hash=False) + summary: str = attr.ib(eq=False, hash=False, repr=False) """This summary for this application's primary SKU if it's sold on Discord. Will be an empty string if undefined. """ - verify_key: typing.Optional[bytes] = attr.ib(eq=False, hash=False) + verify_key: typing.Optional[bytes] = attr.ib(eq=False, hash=False, repr=False) """The base64 encoded key used for the GameSDK's `GetTicket`.""" - icon_hash: typing.Optional[str] = attr.ib(eq=False, hash=False) + icon_hash: typing.Optional[str] = attr.ib(eq=False, hash=False, repr=False) """The CDN hash of this application's icon, if set.""" - team: typing.Optional[Team] = attr.ib(eq=False, hash=False) + team: typing.Optional[Team] = attr.ib(eq=False, hash=False, repr=False) """The team this application belongs to. If the application is not part of a team, this will be `None`. """ - guild_id: typing.Optional[snowflake.Snowflake] = attr.ib(eq=False, hash=False) + guild_id: typing.Optional[snowflake.Snowflake] = attr.ib(eq=False, hash=False, repr=False) """The ID of the guild this application is linked to if sold on Discord.""" - primary_sku_id: typing.Optional[snowflake.Snowflake] = attr.ib(eq=False, hash=False) + primary_sku_id: typing.Optional[snowflake.Snowflake] = attr.ib(eq=False, hash=False, repr=False) """The ID of the primary "Game SKU" of a game that's sold on Discord.""" - slug: typing.Optional[str] = attr.ib(eq=False, hash=False) + slug: typing.Optional[str] = attr.ib(eq=False, hash=False, repr=False) """The URL "slug" that is used to point to this application's store page. Only applicable to applications sold on Discord. """ - cover_image_hash: typing.Optional[str] = attr.ib(eq=False, hash=False) + cover_image_hash: typing.Optional[str] = attr.ib(eq=False, hash=False, repr=False) """The CDN's hash of this application's cover image, used on the store.""" @property diff --git a/hikari/models/audit_logs.py b/hikari/models/audit_logs.py index 7be53fa98a..2b609dcf04 100644 --- a/hikari/models/audit_logs.py +++ b/hikari/models/audit_logs.py @@ -277,16 +277,16 @@ def __init__(self, payload: typing.Mapping[str, str]) -> None: class AuditLogEntry(bases.Entity, bases.Unique): """Represents an entry in a guild's audit log.""" - target_id: typing.Optional[snowflake.Snowflake] = attr.ib(eq=False, hash=False) + target_id: typing.Optional[snowflake.Snowflake] = attr.ib(eq=False, hash=False, repr=True) """The ID of the entity affected by this change, if applicable.""" changes: typing.Sequence[AuditLogChange] = attr.ib(eq=False, hash=False, repr=False) """A sequence of the changes made to `AuditLogEntry.target_id`.""" - user_id: typing.Optional[snowflake.Snowflake] = attr.ib(eq=False, hash=False) + user_id: typing.Optional[snowflake.Snowflake] = attr.ib(eq=False, hash=False, repr=True) """The ID of the user who made this change.""" - action_type: typing.Union[AuditLogEventType, str] = attr.ib(eq=False, hash=False) + action_type: typing.Union[AuditLogEventType, str] = attr.ib(eq=False, hash=False, repr=True) """The type of action this entry represents.""" options: typing.Optional[BaseAuditLogEntryInfo] = attr.ib(eq=False, hash=False, repr=False) @@ -301,16 +301,16 @@ class AuditLogEntry(bases.Entity, bases.Unique): class AuditLog: """Represents a guilds audit log.""" - entries: typing.Mapping[snowflake.Snowflake, AuditLogEntry] = attr.ib() + entries: typing.Mapping[snowflake.Snowflake, AuditLogEntry] = attr.ib(repr=False) """A sequence of the audit log's entries.""" - integrations: typing.Mapping[snowflake.Snowflake, guilds.Integration] = attr.ib() + integrations: typing.Mapping[snowflake.Snowflake, guilds.Integration] = attr.ib(repr=False) """A mapping of the partial objects of integrations found in this audit log.""" - users: typing.Mapping[snowflake.Snowflake, users_.User] = attr.ib() + users: typing.Mapping[snowflake.Snowflake, users_.User] = attr.ib(repr=False) """A mapping of the objects of users found in this audit log.""" - webhooks: typing.Mapping[snowflake.Snowflake, webhooks_.Webhook] = attr.ib() + webhooks: typing.Mapping[snowflake.Snowflake, webhooks_.Webhook] = attr.ib(repr=False) """A mapping of the objects of webhooks found in this audit log.""" def __iter__(self) -> typing.Iterable[AuditLogEntry]: diff --git a/hikari/models/bases.py b/hikari/models/bases.py index c60231d99c..946cc48aba 100644 --- a/hikari/models/bases.py +++ b/hikari/models/bases.py @@ -52,7 +52,7 @@ class Entity(abc.ABC): app_.IGatewayDispatcher, app_.IRESTApp, app_.IBot, - ] = attr.ib(default=None, repr=False, eq=False, hash=False) + ] = attr.ib(default=None, eq=False, hash=False, repr=False) """The client application that models may use for procedures.""" def __init__(self, app: app_.IApp) -> None: @@ -67,7 +67,7 @@ class Unique(typing.SupportsInt): integer ID of the object. """ - id: snowflake.Snowflake = attr.ib(converter=snowflake.Snowflake, hash=True, eq=True, repr=True) + id: snowflake.Snowflake = attr.ib(converter=snowflake.Snowflake, eq=True, hash=True, repr=True) """The ID of this entity.""" @property diff --git a/hikari/models/channels.py b/hikari/models/channels.py index aabeda581d..9da288ca7c 100644 --- a/hikari/models/channels.py +++ b/hikari/models/channels.py @@ -120,13 +120,17 @@ class PermissionOverwrite(bases.Unique): ``` """ - type: PermissionOverwriteType = attr.ib(converter=PermissionOverwriteType, eq=True, hash=True) + type: PermissionOverwriteType = attr.ib(converter=PermissionOverwriteType, eq=True, hash=True, repr=True) """The type of entity this overwrite targets.""" - allow: permissions.Permission = attr.ib(converter=permissions.Permission, default=0, eq=False, hash=False) + allow: permissions.Permission = attr.ib( + converter=permissions.Permission, default=0, eq=False, hash=False, repr=False + ) """The permissions this overwrite allows.""" - deny: permissions.Permission = attr.ib(converter=permissions.Permission, default=0, eq=False, hash=False) + deny: permissions.Permission = attr.ib( + converter=permissions.Permission, default=0, eq=False, hash=False, repr=False + ) """The permissions this overwrite denies.""" @property @@ -161,7 +165,7 @@ class TextChannel(PartialChannel, abc.ABC): class DMChannel(TextChannel): """Represents a DM channel.""" - last_message_id: typing.Optional[snowflake.Snowflake] = attr.ib(eq=False, hash=False) + last_message_id: typing.Optional[snowflake.Snowflake] = attr.ib(eq=False, hash=False, repr=False) """The ID of the last message sent in this channel. !!! warning @@ -169,9 +173,7 @@ class DMChannel(TextChannel): this will always be valid. """ - recipients: typing.Mapping[snowflake.Snowflake, users.User] = attr.ib( - eq=False, hash=False, - ) + recipients: typing.Mapping[snowflake.Snowflake, users.User] = attr.ib(eq=False, hash=False, repr=False) """The recipients of the DM.""" @@ -182,13 +184,13 @@ class GroupDMChannel(DMChannel): owner_id: snowflake.Snowflake = attr.ib(eq=False, hash=False, repr=True) """The ID of the owner of the group.""" - icon_hash: typing.Optional[str] = attr.ib(eq=False, hash=False) + icon_hash: typing.Optional[str] = attr.ib(eq=False, hash=False, repr=False) """The CDN hash of the icon of the group, if an icon is set.""" - nicknames: typing.MutableMapping[snowflake.Snowflake, str] = attr.ib(eq=False, hash=False) + nicknames: typing.MutableMapping[snowflake.Snowflake, str] = attr.ib(eq=False, hash=False, repr=False) """A mapping of set nicknames within this group DMs to user IDs.""" - application_id: typing.Optional[snowflake.Snowflake] = attr.ib(eq=False, hash=False) + application_id: typing.Optional[snowflake.Snowflake] = attr.ib(eq=False, hash=False, repr=False) """The ID of the application that created the group DM. If the group DM was not created by a bot, this will be `None`. @@ -239,19 +241,21 @@ class GuildChannel(PartialChannel): (e.g Guild Create). """ - position: int = attr.ib(eq=False, hash=False) + position: int = attr.ib(eq=False, hash=False, repr=False) """The sorting position of the channel. Higher numbers appear further down the channel list. """ - permission_overwrites: typing.Mapping[snowflake.Snowflake, PermissionOverwrite] = attr.ib(eq=False, hash=False) + permission_overwrites: typing.Mapping[snowflake.Snowflake, PermissionOverwrite] = attr.ib( + eq=False, hash=False, repr=False + ) """The permission overwrites for the channel. This maps the ID of the entity in the overwrite to the overwrite data. """ - is_nsfw: typing.Optional[bool] = attr.ib(eq=False, hash=False) + is_nsfw: typing.Optional[bool] = attr.ib(eq=False, hash=False, repr=False) """Whether the channel is marked as NSFW. !!! warning @@ -279,10 +283,10 @@ class GuildCategory(GuildChannel): class GuildTextChannel(GuildChannel, TextChannel): """Represents a guild text channel.""" - topic: typing.Optional[str] = attr.ib(eq=False, hash=False) + topic: typing.Optional[str] = attr.ib(eq=False, hash=False, repr=False) """The topic of the channel.""" - last_message_id: typing.Optional[snowflake.Snowflake] = attr.ib(eq=False, hash=False) + last_message_id: typing.Optional[snowflake.Snowflake] = attr.ib(eq=False, hash=False, repr=False) """The ID of the last message sent in this channel. !!! warning @@ -290,7 +294,7 @@ class GuildTextChannel(GuildChannel, TextChannel): this will always be valid. """ - rate_limit_per_user: datetime.timedelta = attr.ib(eq=False, hash=False) + rate_limit_per_user: datetime.timedelta = attr.ib(eq=False, hash=False, repr=False) """The delay (in seconds) between a user can send a message to this channel. !!! note @@ -299,7 +303,7 @@ class GuildTextChannel(GuildChannel, TextChannel): will not be affected by this rate limit. """ - last_pin_timestamp: typing.Optional[datetime.datetime] = attr.ib(eq=False, hash=False) + last_pin_timestamp: typing.Optional[datetime.datetime] = attr.ib(eq=False, hash=False, repr=False) """The timestamp of the last-pinned message. !!! note @@ -312,10 +316,10 @@ class GuildTextChannel(GuildChannel, TextChannel): class GuildNewsChannel(GuildChannel, TextChannel): """Represents an news channel.""" - topic: typing.Optional[str] = attr.ib(eq=False, hash=False) + topic: typing.Optional[str] = attr.ib(eq=False, hash=False, repr=False) """The topic of the channel.""" - last_message_id: typing.Optional[snowflake.Snowflake] = attr.ib(eq=False, hash=False) + last_message_id: typing.Optional[snowflake.Snowflake] = attr.ib(eq=False, hash=False, repr=False) """The ID of the last message sent in this channel. !!! warning @@ -323,7 +327,7 @@ class GuildNewsChannel(GuildChannel, TextChannel): this will always be valid. """ - last_pin_timestamp: typing.Optional[datetime.datetime] = attr.ib(eq=False, hash=False) + last_pin_timestamp: typing.Optional[datetime.datetime] = attr.ib(eq=False, hash=False, repr=False) """The timestamp of the last-pinned message. !!! note diff --git a/hikari/models/embeds.py b/hikari/models/embeds.py index ffb75cb75c..ef9d7bbd2d 100644 --- a/hikari/models/embeds.py +++ b/hikari/models/embeds.py @@ -63,10 +63,10 @@ class EmbedFooter: text: typing.Optional[str] = attr.ib(default=None, repr=True) """The footer text, or `None` if not present.""" - icon_url: typing.Optional[str] = attr.ib(default=None) + icon_url: typing.Optional[str] = attr.ib(default=None, repr=False) """The URL of the footer icon, or `None` if not present.""" - proxy_icon_url: typing.Optional[str] = attr.ib(default=None) + proxy_icon_url: typing.Optional[str] = attr.ib(default=None, repr=False) """The proxied URL of the footer icon, or `None` if not present. !!! note @@ -80,12 +80,10 @@ class EmbedFooter: class EmbedImage: """Represents an embed image.""" - url: typing.Optional[str] = attr.ib( - default=None, repr=True, - ) + url: typing.Optional[str] = attr.ib(default=None, repr=True) """The URL of the image to show, or `None` if not present.""" - proxy_url: typing.Optional[str] = attr.ib(default=None,) + proxy_url: typing.Optional[str] = attr.ib(default=None, repr=False) """The proxied URL of the image, or `None` if not present. !!! note @@ -94,7 +92,7 @@ class EmbedImage: any received embed attached to a message event. """ - height: typing.Optional[int] = attr.ib(default=None) + height: typing.Optional[int] = attr.ib(default=None, repr=False) """The height of the image, if present and known, otherwise `None`. !!! note @@ -103,7 +101,7 @@ class EmbedImage: any received embed attached to a message event. """ - width: typing.Optional[int] = attr.ib(default=None) + width: typing.Optional[int] = attr.ib(default=None, repr=False) """The width of the image, if present and known, otherwise `None`. !!! note @@ -117,12 +115,10 @@ class EmbedImage: class EmbedThumbnail: """Represents an embed thumbnail.""" - url: typing.Optional[str] = attr.ib( - default=None, repr=True, - ) + url: typing.Optional[str] = attr.ib(default=None, repr=True) """The URL of the thumbnail to display, or `None` if not present.""" - proxy_url: typing.Optional[str] = attr.ib(default=None,) + proxy_url: typing.Optional[str] = attr.ib(default=None, repr=False) """The proxied URL of the thumbnail, if present and known, otherwise `None`. !!! note @@ -131,7 +127,7 @@ class EmbedThumbnail: any received embed attached to a message event. """ - height: typing.Optional[int] = attr.ib(default=None) + height: typing.Optional[int] = attr.ib(default=None, repr=False) """The height of the thumbnail, if present and known, otherwise `None`. !!! note @@ -140,7 +136,7 @@ class EmbedThumbnail: any received embed attached to a message event. """ - width: typing.Optional[int] = attr.ib(default=None) + width: typing.Optional[int] = attr.ib(default=None, repr=False) """The width of the thumbnail, if present and known, otherwise `None`. !!! note @@ -163,10 +159,10 @@ class EmbedVideo: url: typing.Optional[str] = attr.ib(default=None, repr=True) """The URL of the video.""" - height: typing.Optional[int] = attr.ib(default=None) + height: typing.Optional[int] = attr.ib(default=None, repr=False) """The height of the video.""" - width: typing.Optional[int] = attr.ib(default=None) + width: typing.Optional[int] = attr.ib(default=None, repr=False) """The width of the video.""" @@ -201,10 +197,10 @@ class EmbedAuthor: This may be `None` if no hyperlink on the author's name is specified. """ - icon_url: typing.Optional[str] = attr.ib(default=None) + icon_url: typing.Optional[str] = attr.ib(default=None, repr=False) """The URL of the author's icon, or `None` if not present.""" - proxy_icon_url: typing.Optional[str] = attr.ib(default=None) + proxy_icon_url: typing.Optional[str] = attr.ib(default=None, repr=False) """The proxied URL of the author icon, or `None` if not present. !!! note @@ -242,7 +238,7 @@ def _title_check(self, _, value): # pylint:disable=unused-argument f"title must not exceed {_MAX_EMBED_TITLE} characters", category=errors.HikariWarning, ) - description: typing.Optional[str] = attr.ib(default=None) + description: typing.Optional[str] = attr.ib(default=None, repr=False) """The description of the embed, or `None` if not present.""" @description.validator @@ -252,12 +248,10 @@ def _description_check(self, _, value): # pylint:disable=unused-argument f"description must not exceed {_MAX_EMBED_DESCRIPTION} characters", category=errors.HikariWarning, ) - url: typing.Optional[str] = attr.ib(default=None) + url: typing.Optional[str] = attr.ib(default=None, repr=False) """The URL of the embed, or `None` if not present.""" - timestamp: typing.Optional[datetime.datetime] = attr.ib( - default=None, repr=True, - ) + timestamp: typing.Optional[datetime.datetime] = attr.ib(default=None, repr=True) """The timestamp of the embed, or `None` if not present. !!! note @@ -315,7 +309,7 @@ def _description_check(self, _, value): # pylint:disable=unused-argument """ color: typing.Optional[colors.Color] = attr.ib( - converter=attr.converters.optional(colors.Color.of), default=None, + converter=attr.converters.optional(colors.Color.of), default=None, repr=False ) """The colour of this embed. @@ -329,16 +323,16 @@ def _description_check(self, _, value): # pylint:disable=unused-argument off-white, such as `#DDDDDD` or `#FFFFFE` instead. """ - footer: typing.Optional[EmbedFooter] = attr.ib(default=None) + footer: typing.Optional[EmbedFooter] = attr.ib(default=None, repr=False) """The footer of the embed, if present, otherwise `None`.""" - image: typing.Optional[EmbedImage] = attr.ib(default=None) + image: typing.Optional[EmbedImage] = attr.ib(default=None, repr=False) """The image to display in the embed, or `None` if not present.""" - thumbnail: typing.Optional[EmbedThumbnail] = attr.ib(default=None) + thumbnail: typing.Optional[EmbedThumbnail] = attr.ib(default=None, repr=False) """The thumbnail to show in the embed, or `None` if not present.""" - video: typing.Optional[EmbedVideo] = attr.ib(default=None) + video: typing.Optional[EmbedVideo] = attr.ib(default=None, repr=False) """The video to show in the embed, or `None` if not present. !!! note @@ -347,7 +341,7 @@ def _description_check(self, _, value): # pylint:disable=unused-argument any received embed attached to a message event with a video attached. """ - provider: typing.Optional[EmbedProvider] = attr.ib(default=None) + provider: typing.Optional[EmbedProvider] = attr.ib(default=None, repr=False) """The provider of the embed, or `None if not present. !!! note @@ -357,10 +351,10 @@ def _description_check(self, _, value): # pylint:disable=unused-argument set. """ - author: typing.Optional[EmbedAuthor] = attr.ib(default=None) + author: typing.Optional[EmbedAuthor] = attr.ib(default=None, repr=False) """The author of the embed, or `None if not present.""" - fields: typing.MutableSequence[EmbedField] = attr.ib(factory=list) + fields: typing.MutableSequence[EmbedField] = attr.ib(factory=list, repr=False) """The fields in the embed.""" # Use a weakref so that clearing an image can pop the reference. diff --git a/hikari/models/emojis.py b/hikari/models/emojis.py index 3f5c6a044f..40bbc1abb2 100644 --- a/hikari/models/emojis.py +++ b/hikari/models/emojis.py @@ -212,9 +212,7 @@ class CustomEmoji(Emoji, bases.Entity, bases.Unique): name: typing.Optional[str] = attr.ib(eq=False, hash=False, repr=True) """The name of the emoji.""" - is_animated: typing.Optional[bool] = attr.ib( - eq=False, hash=False, repr=True, - ) + is_animated: typing.Optional[bool] = attr.ib(eq=False, hash=False, repr=True) """Whether the emoji is animated. Will be `None` when received in Message Reaction Remove and Message @@ -250,17 +248,13 @@ class KnownCustomEmoji(CustomEmoji): _are_ part of. Ass a result, it contains a lot more information with it. """ - role_ids: typing.Set[snowflake.Snowflake] = attr.ib( - eq=False, hash=False, - ) + role_ids: typing.Set[snowflake.Snowflake] = attr.ib(eq=False, hash=False, repr=False) """The IDs of the roles that are whitelisted to use this emoji. If this is empty then any user can use this emoji regardless of their roles. """ - user: typing.Optional[users.User] = attr.ib( - eq=False, hash=False, - ) + user: typing.Optional[users.User] = attr.ib(eq=False, hash=False, repr=False) """The user that created the emoji. !!! note @@ -274,13 +268,13 @@ class KnownCustomEmoji(CustomEmoji): Unlike in `CustomEmoji`, this information is always known, and will thus never be `None`. """ - is_colons_required: bool = attr.ib(eq=False, hash=False) + is_colons_required: bool = attr.ib(eq=False, hash=False, repr=False) """Whether this emoji must be wrapped in colons.""" - is_managed: bool = attr.ib(eq=False, hash=False) + is_managed: bool = attr.ib(eq=False, hash=False, repr=False) """Whether the emoji is managed by an integration.""" - is_available: bool = attr.ib(eq=False, hash=False) + is_available: bool = attr.ib(eq=False, hash=False, repr=False) """Whether this emoji can currently be used. May be `False` due to a loss of Sever Boosts on the emoji's guild. diff --git a/hikari/models/guilds.py b/hikari/models/guilds.py index 44d0cdeac1..46e1300a71 100644 --- a/hikari/models/guilds.py +++ b/hikari/models/guilds.py @@ -236,28 +236,28 @@ class Member(bases.Entity): if it's state is unknown. """ - role_ids: typing.Set[snowflake.Snowflake] = attr.ib( - eq=False, hash=False, - ) + role_ids: typing.Set[snowflake.Snowflake] = attr.ib(eq=False, hash=False, repr=False) """A sequence of the IDs of the member's current roles.""" - joined_at: typing.Union[datetime.datetime, undefined.Undefined] = attr.ib(eq=False, hash=False) + joined_at: typing.Union[datetime.datetime, undefined.Undefined] = attr.ib(eq=False, hash=False, repr=False) """The datetime of when this member joined the guild they belong to.""" - premium_since: typing.Union[datetime.datetime, None, undefined.Undefined] = attr.ib(eq=False, hash=False) + premium_since: typing.Union[datetime.datetime, None, undefined.Undefined] = attr.ib( + eq=False, hash=False, repr=False + ) """The datetime of when this member started "boosting" this guild. This will be `None` if they aren't boosting and `hikari.utilities.undefined.Undefined` if their boosting status is unknown. """ - is_deaf: typing.Union[bool, undefined.Undefined] = attr.ib(eq=False, hash=False) + is_deaf: typing.Union[bool, undefined.Undefined] = attr.ib(eq=False, hash=False, repr=False) """Whether this member is deafened by this guild in it's voice channels. This will be `hikari.utilities.undefined.Undefined if it's state is unknown. """ - is_mute: typing.Union[bool, undefined.Undefined] = attr.ib(eq=False, hash=False) + is_mute: typing.Union[bool, undefined.Undefined] = attr.ib(eq=False, hash=False, repr=False) """Whether this member is muted by this guild in it's voice channels. This will be `hikari.utilities.undefined.Undefined if it's state is unknown. @@ -276,9 +276,7 @@ class PartialRole(bases.Entity, bases.Unique): class Role(PartialRole): """Represents a guild bound Role object.""" - color: colors.Color = attr.ib( - eq=False, hash=False, repr=True, - ) + color: colors.Color = attr.ib(eq=False, hash=False, repr=True) """The colour of this role. This will be applied to a member's name in chat if it's their top coloured role. @@ -293,16 +291,16 @@ class Role(PartialRole): position: int = attr.ib(eq=False, hash=False, repr=True) """The position of this role in the role hierarchy.""" - permissions: permissions_.Permission = attr.ib(eq=False, hash=False) + permissions: permissions_.Permission = attr.ib(eq=False, hash=False, repr=False) """The guild wide permissions this role gives to the members it's attached to, This may be overridden by channel overwrites. """ - is_managed: bool = attr.ib(eq=False, hash=False) + is_managed: bool = attr.ib(eq=False, hash=False, repr=False) """Whether this role is managed by an integration.""" - is_mentionable: bool = attr.ib(eq=False, hash=False) + is_mentionable: bool = attr.ib(eq=False, hash=False, repr=False) """Whether this role can be mentioned by all regardless of permissions.""" @@ -321,10 +319,10 @@ class IntegrationExpireBehaviour(int, enum.Enum): class IntegrationAccount: """An account that's linked to an integration.""" - id: str = attr.ib(eq=True, hash=True) + id: str = attr.ib(eq=True, hash=True, repr=True) """The string ID of this (likely) third party account.""" - name: str = attr.ib(eq=False, hash=False) + name: str = attr.ib(eq=False, hash=False, repr=True) """The name of this account.""" @@ -338,7 +336,7 @@ class PartialIntegration(bases.Unique): type: str = attr.ib(eq=False, hash=False, repr=True) """The type of this integration.""" - account: IntegrationAccount = attr.ib(eq=False, hash=False) + account: IntegrationAccount = attr.ib(eq=False, hash=False, repr=False) """The account connected to this integration.""" @@ -349,31 +347,31 @@ class Integration(PartialIntegration): is_enabled: bool = attr.ib(eq=False, hash=False, repr=True) """Whether this integration is enabled.""" - is_syncing: bool = attr.ib(eq=False, hash=False) + is_syncing: bool = attr.ib(eq=False, hash=False, repr=False) """Whether this integration is syncing subscribers/emojis.""" - role_id: typing.Optional[snowflake.Snowflake] = attr.ib(eq=False, hash=False) + role_id: typing.Optional[snowflake.Snowflake] = attr.ib(eq=False, hash=False, repr=False) """The ID of the managed role used for this integration's subscribers.""" - is_emojis_enabled: typing.Optional[bool] = attr.ib(eq=False, hash=False) + is_emojis_enabled: typing.Optional[bool] = attr.ib(eq=False, hash=False, repr=False) """Whether users under this integration are allowed to use it's custom emojis.""" - expire_behavior: IntegrationExpireBehaviour = attr.ib(eq=False, hash=False) + expire_behavior: IntegrationExpireBehaviour = attr.ib(eq=False, hash=False, repr=False) """How members should be treated after their connected subscription expires. This won't be enacted until after `GuildIntegration.expire_grace_period` passes. """ - expire_grace_period: datetime.timedelta = attr.ib(eq=False, hash=False) + expire_grace_period: datetime.timedelta = attr.ib(eq=False, hash=False, repr=False) """How many days users with expired subscriptions are given until `GuildIntegration.expire_behavior` is enacted out on them """ - user: users.User = attr.ib(eq=False, hash=False) + user: users.User = attr.ib(eq=False, hash=False, repr=False) """The user this integration belongs to.""" - last_synced_at: datetime.datetime = attr.ib(eq=False, hash=False) + last_synced_at: datetime.datetime = attr.ib(eq=False, hash=False, repr=False) """The datetime of when this integration's subscribers were last synced.""" @@ -413,10 +411,10 @@ class PartialGuild(bases.Entity, bases.Unique): name: str = attr.ib(eq=False, hash=False, repr=True) """The name of the guild.""" - icon_hash: typing.Optional[str] = attr.ib(eq=False, hash=False) + icon_hash: typing.Optional[str] = attr.ib(eq=False, hash=False, repr=False) """The hash for the guild icon, if there is one.""" - features: typing.Set[typing.Union[GuildFeature, str]] = attr.ib(eq=False, hash=False) + features: typing.Set[typing.Union[GuildFeature, str]] = attr.ib(eq=False, hash=False, repr=False) """A set of the features in this guild.""" def format_icon_url(self, *, format_: typing.Optional[str] = None, size: int = 4096) -> typing.Optional[str]: @@ -458,17 +456,13 @@ def icon_url(self) -> typing.Optional[str]: class GuildPreview(PartialGuild): """A preview of a guild with the `GuildFeature.PUBLIC` feature.""" - splash_hash: typing.Optional[str] = attr.ib(eq=False, hash=False) + splash_hash: typing.Optional[str] = attr.ib(eq=False, hash=False, repr=False) """The hash of the splash for the guild, if there is one.""" - discovery_splash_hash: typing.Optional[str] = attr.ib( - eq=False, hash=False, - ) + discovery_splash_hash: typing.Optional[str] = attr.ib(eq=False, hash=False, repr=False) """The hash of the discovery splash for the guild, if there is one.""" - emojis: typing.Mapping[snowflake.Snowflake, emojis_.KnownCustomEmoji] = attr.ib( - eq=False, hash=False, - ) + emojis: typing.Mapping[snowflake.Snowflake, emojis_.KnownCustomEmoji] = attr.ib(eq=False, hash=False, repr=False) """The mapping of IDs to the emojis this guild provides.""" approximate_presence_count: int = attr.ib(eq=False, hash=False, repr=True) @@ -477,7 +471,7 @@ class GuildPreview(PartialGuild): approximate_member_count: int = attr.ib(eq=False, hash=False, repr=True) """The approximate amount of members in this guild.""" - description: typing.Optional[str] = attr.ib(eq=False, hash=False) + description: typing.Optional[str] = attr.ib(eq=False, hash=False, repr=False) """The guild's description, if set.""" def format_splash_url(self, *, format_: str = "png", size: int = 4096) -> typing.Optional[str]: @@ -556,18 +550,16 @@ class Guild(PartialGuild): # pylint:disable=too-many-instance-attributes any other fields should be ignored. """ - splash_hash: typing.Optional[str] = attr.ib(eq=False, hash=False) + splash_hash: typing.Optional[str] = attr.ib(eq=False, hash=False, repr=False) """The hash of the splash for the guild, if there is one.""" - discovery_splash_hash: typing.Optional[str] = attr.ib(eq=False, hash=False) + discovery_splash_hash: typing.Optional[str] = attr.ib(eq=False, hash=False, repr=False) """The hash of the discovery splash for the guild, if there is one.""" owner_id: snowflake.Snowflake = attr.ib(eq=False, hash=False, repr=True) """The ID of the owner of this guild.""" - my_permissions: permissions_.Permission = attr.ib( - eq=False, hash=False, - ) + my_permissions: permissions_.Permission = attr.ib(eq=False, hash=False, repr=False) """The guild-level permissions that apply to the bot user. This will not take into account permission overwrites or implied @@ -577,23 +569,23 @@ class Guild(PartialGuild): # pylint:disable=too-many-instance-attributes rather than from the gateway. """ - region: str = attr.ib(eq=False, hash=False) + region: str = attr.ib(eq=False, hash=False, repr=False) """The voice region for the guild.""" - afk_channel_id: typing.Optional[snowflake.Snowflake] = attr.ib(eq=False, hash=False) + afk_channel_id: typing.Optional[snowflake.Snowflake] = attr.ib(eq=False, hash=False, repr=False) """The ID for the channel that AFK voice users get sent to. If `None`, then no AFK channel is set up for this guild. """ - afk_timeout: datetime.timedelta = attr.ib(eq=False, hash=False) + afk_timeout: datetime.timedelta = attr.ib(eq=False, hash=False, repr=False) """Timeout for activity before a member is classed as AFK. How long a voice user has to be AFK for before they are classed as being AFK and are moved to the AFK channel (`Guild.afk_channel_id`). """ - is_embed_enabled: typing.Optional[bool] = attr.ib(eq=False, hash=False) + is_embed_enabled: typing.Optional[bool] = attr.ib(eq=False, hash=False, repr=False) """Defines if the guild embed is enabled or not. This information may not be present, in which case, it will be `None` @@ -603,7 +595,7 @@ class Guild(PartialGuild): # pylint:disable=too-many-instance-attributes Use `is_widget_enabled` instead. """ - embed_channel_id: typing.Optional[snowflake.Snowflake] = attr.ib(eq=False, hash=False) + embed_channel_id: typing.Optional[snowflake.Snowflake] = attr.ib(eq=False, hash=False, repr=False) """The channel ID that the guild embed will generate an invite to. Will be `None` if invites are disabled for this guild's embed. @@ -612,33 +604,31 @@ class Guild(PartialGuild): # pylint:disable=too-many-instance-attributes Use `widget_channel_id` instead. """ - verification_level: GuildVerificationLevel = attr.ib(eq=False, hash=False) + verification_level: GuildVerificationLevel = attr.ib(eq=False, hash=False, repr=False) """The verification level required for a user to participate in this guild.""" - default_message_notifications: GuildMessageNotificationsLevel = attr.ib(eq=False, hash=False) + default_message_notifications: GuildMessageNotificationsLevel = attr.ib(eq=False, hash=False, repr=False) """The default setting for message notifications in this guild.""" - explicit_content_filter: GuildExplicitContentFilterLevel = attr.ib(eq=False, hash=False) + explicit_content_filter: GuildExplicitContentFilterLevel = attr.ib(eq=False, hash=False, repr=False) """The setting for the explicit content filter in this guild.""" - roles: typing.Mapping[snowflake.Snowflake, Role] = attr.ib( - eq=False, hash=False, - ) + roles: typing.Mapping[snowflake.Snowflake, Role] = attr.ib(eq=False, hash=False, repr=False) """The roles in this guild, represented as a mapping of ID to role object.""" - emojis: typing.Mapping[snowflake.Snowflake, emojis_.KnownCustomEmoji] = attr.ib(eq=False, hash=False) + emojis: typing.Mapping[snowflake.Snowflake, emojis_.KnownCustomEmoji] = attr.ib(eq=False, hash=False, repr=False) """A mapping of IDs to the objects of the emojis this guild provides.""" - mfa_level: GuildMFALevel = attr.ib(eq=False, hash=False) + mfa_level: GuildMFALevel = attr.ib(eq=False, hash=False, repr=False) """The required MFA level for users wishing to participate in this guild.""" - application_id: typing.Optional[snowflake.Snowflake] = attr.ib(eq=False, hash=False) + application_id: typing.Optional[snowflake.Snowflake] = attr.ib(eq=False, hash=False, repr=False) """The ID of the application that created this guild. This will always be `None` for guilds that weren't created by a bot. """ - is_unavailable: typing.Optional[bool] = attr.ib(eq=False, hash=False) + is_unavailable: typing.Optional[bool] = attr.ib(eq=False, hash=False, repr=False) """Whether the guild is unavailable or not. This information is only available if the guild was sent via a @@ -649,36 +639,36 @@ class Guild(PartialGuild): # pylint:disable=too-many-instance-attributes be outdated if that is the case. """ - is_widget_enabled: typing.Optional[bool] = attr.ib(eq=False, hash=False) + is_widget_enabled: typing.Optional[bool] = attr.ib(eq=False, hash=False, repr=False) """Describes whether the guild widget is enabled or not. If this information is not present, this will be `None`. """ - widget_channel_id: typing.Optional[snowflake.Snowflake] = attr.ib(eq=False, hash=False) + widget_channel_id: typing.Optional[snowflake.Snowflake] = attr.ib(eq=False, hash=False, repr=False) """The channel ID that the widget's generated invite will send the user to. If this information is unavailable or this isn't enabled for the guild then this will be `None`. """ - system_channel_id: typing.Optional[snowflake.Snowflake] = attr.ib(eq=False, hash=False) + system_channel_id: typing.Optional[snowflake.Snowflake] = attr.ib(eq=False, hash=False, repr=False) """The ID of the system channel or `None` if it is not enabled. Welcome messages and Nitro boost messages may be sent to this channel. """ - system_channel_flags: GuildSystemChannelFlag = attr.ib(eq=False, hash=False) + system_channel_flags: GuildSystemChannelFlag = attr.ib(eq=False, hash=False, repr=False) """Flags for the guild system channel to describe which notifications are suppressed.""" - rules_channel_id: typing.Optional[snowflake.Snowflake] = attr.ib(eq=False, hash=False) + rules_channel_id: typing.Optional[snowflake.Snowflake] = attr.ib(eq=False, hash=False, repr=False) """The ID of the channel where guilds with the `GuildFeature.PUBLIC` `features` display rules and guidelines. If the `GuildFeature.PUBLIC` feature is not defined, then this is `None`. """ - joined_at: typing.Optional[datetime.datetime] = attr.ib(eq=False, hash=False) + joined_at: typing.Optional[datetime.datetime] = attr.ib(eq=False, hash=False, repr=False) """The date and time that the bot user joined this guild. This information is only available if the guild was sent via a `GUILD_CREATE` @@ -686,7 +676,7 @@ class Guild(PartialGuild): # pylint:disable=too-many-instance-attributes `None`. """ - is_large: typing.Optional[bool] = attr.ib(eq=False, hash=False) + is_large: typing.Optional[bool] = attr.ib(eq=False, hash=False, repr=False) """Whether the guild is considered to be large or not. This information is only available if the guild was sent via a `GUILD_CREATE` @@ -697,7 +687,7 @@ class Guild(PartialGuild): # pylint:disable=too-many-instance-attributes sent about members who are offline or invisible. """ - member_count: typing.Optional[int] = attr.ib(eq=False, hash=False) + member_count: typing.Optional[int] = attr.ib(eq=False, hash=False, repr=False) """The number of members in this guild. This information is only available if the guild was sent via a `GUILD_CREATE` @@ -705,7 +695,7 @@ class Guild(PartialGuild): # pylint:disable=too-many-instance-attributes `None`. """ - members: typing.Optional[typing.Mapping[snowflake.Snowflake, Member]] = attr.ib(eq=False, hash=False) + members: typing.Optional[typing.Mapping[snowflake.Snowflake, Member]] = attr.ib(eq=False, hash=False, repr=False) """A mapping of ID to the corresponding guild members in this guild. This information is only available if the guild was sent via a `GUILD_CREATE` @@ -725,7 +715,7 @@ class Guild(PartialGuild): # pylint:disable=too-many-instance-attributes """ channels: typing.Optional[typing.Mapping[snowflake.Snowflake, channels_.GuildChannel]] = attr.ib( - eq=False, hash=False, + eq=False, hash=False, repr=False ) """A mapping of ID to the corresponding guild channels in this guild. @@ -744,7 +734,7 @@ class Guild(PartialGuild): # pylint:disable=too-many-instance-attributes """ presences: typing.Optional[typing.Mapping[snowflake.Snowflake, presences.MemberPresence]] = attr.ib( - eq=False, hash=False, + eq=False, hash=False, repr=False ) """A mapping of member ID to the corresponding presence information for the given member, if available. @@ -763,62 +753,62 @@ class Guild(PartialGuild): # pylint:disable=too-many-instance-attributes appropriate API call to retrieve this information. """ - max_presences: typing.Optional[int] = attr.ib(eq=False, hash=False) + max_presences: typing.Optional[int] = attr.ib(eq=False, hash=False, repr=False) """The maximum number of presences for the guild. If this is `None`, then the default value is used (currently 25000). """ - max_members: typing.Optional[int] = attr.ib(eq=False, hash=False) + max_members: typing.Optional[int] = attr.ib(eq=False, hash=False, repr=False) """The maximum number of members allowed in this guild. This information may not be present, in which case, it will be `None`. """ - max_video_channel_users: typing.Optional[int] = attr.ib(eq=False, hash=False) + max_video_channel_users: typing.Optional[int] = attr.ib(eq=False, hash=False, repr=False) """The maximum number of users allowed in a video channel together. This information may not be present, in which case, it will be `None`. """ - vanity_url_code: typing.Optional[str] = attr.ib(eq=False, hash=False) + vanity_url_code: typing.Optional[str] = attr.ib(eq=False, hash=False, repr=False) """The vanity URL code for the guild's vanity URL. This is only present if `GuildFeature.VANITY_URL` is in `Guild.features` for this guild. If not, this will always be `None`. """ - description: typing.Optional[str] = attr.ib(eq=False, hash=False) + description: typing.Optional[str] = attr.ib(eq=False, hash=False, repr=False) """The guild's description. This is only present if certain `GuildFeature`'s are set in `Guild.features` for this guild. Otherwise, this will always be `None`. """ - banner_hash: typing.Optional[str] = attr.ib(eq=False, hash=False) + banner_hash: typing.Optional[str] = attr.ib(eq=False, hash=False, repr=False) """The hash for the guild's banner. This is only present if the guild has `GuildFeature.BANNER` in `Guild.features` for this guild. For all other purposes, it is `None`. """ - premium_tier: GuildPremiumTier = attr.ib(eq=False, hash=False) + premium_tier: GuildPremiumTier = attr.ib(eq=False, hash=False, repr=False) """The premium tier for this guild.""" - premium_subscription_count: typing.Optional[int] = attr.ib(eq=False, hash=False) + premium_subscription_count: typing.Optional[int] = attr.ib(eq=False, hash=False, repr=False) """The number of nitro boosts that the server currently has. This information may not be present, in which case, it will be `None`. """ - preferred_locale: str = attr.ib(eq=False, hash=False) + preferred_locale: str = attr.ib(eq=False, hash=False, repr=False) """The preferred locale to use for this guild. This can only be change if `GuildFeature.PUBLIC` is in `Guild.features` for this guild and will otherwise default to `en-US`. """ - public_updates_channel_id: typing.Optional[snowflake.Snowflake] = attr.ib(eq=False, hash=False) + public_updates_channel_id: typing.Optional[snowflake.Snowflake] = attr.ib(eq=False, hash=False, repr=False) """The channel ID of the channel where admins and moderators receive notices from Discord. @@ -827,7 +817,7 @@ class Guild(PartialGuild): # pylint:disable=too-many-instance-attributes """ # TODO: if this is `None`, then should we attempt to look at the known member count if present? - approximate_member_count: typing.Optional[int] = attr.ib(eq=False, hash=False) + approximate_member_count: typing.Optional[int] = attr.ib(eq=False, hash=False, repr=False) """The approximate number of members in the guild. This information will be provided by RESTSession API calls fetching the guilds that @@ -835,7 +825,7 @@ class Guild(PartialGuild): # pylint:disable=too-many-instance-attributes remain `None`. """ - approximate_active_member_count: typing.Optional[int] = attr.ib(eq=False, hash=False) + approximate_active_member_count: typing.Optional[int] = attr.ib(eq=False, hash=False, repr=False) """The approximate number of members in the guild that are not offline. This information will be provided by RESTSession API calls fetching the guilds that diff --git a/hikari/models/invites.py b/hikari/models/invites.py index 2e68c8ac26..8e0b467ff5 100644 --- a/hikari/models/invites.py +++ b/hikari/models/invites.py @@ -62,24 +62,24 @@ class VanityURL(bases.Entity): class InviteGuild(guilds.PartialGuild): """Represents the partial data of a guild that'll be attached to invites.""" - splash_hash: typing.Optional[str] = attr.ib(eq=False, hash=False) + splash_hash: typing.Optional[str] = attr.ib(eq=False, hash=False, repr=False) """The hash of the splash for the guild, if there is one.""" - banner_hash: typing.Optional[str] = attr.ib(eq=False, hash=False) + banner_hash: typing.Optional[str] = attr.ib(eq=False, hash=False, repr=False) """The hash for the guild's banner. This is only present if `hikari.models.guilds.GuildFeature.BANNER` is in the `features` for this guild. For all other purposes, it is `None`. """ - description: typing.Optional[str] = attr.ib(eq=False, hash=False) + description: typing.Optional[str] = attr.ib(eq=False, hash=False, repr=False) """The guild's description. This is only present if certain `features` are set in this guild. Otherwise, this will always be `None`. For all other purposes, it is `None`. """ - verification_level: guilds.GuildVerificationLevel = attr.ib(eq=False, hash=False) + verification_level: guilds.GuildVerificationLevel = attr.ib(eq=False, hash=False, repr=False) """The verification level required for a user to participate in this guild.""" vanity_url_code: typing.Optional[str] = attr.ib(eq=False, hash=False, repr=True) @@ -182,23 +182,23 @@ class Invite(bases.Entity): channel_id: snowflake.Snowflake = attr.ib(eq=False, hash=False, repr=True) """The ID of the channel this invite targets.""" - inviter: typing.Optional[users.User] = attr.ib(eq=False, hash=False) + inviter: typing.Optional[users.User] = attr.ib(eq=False, hash=False, repr=False) """The object of the user who created this invite.""" - target_user: typing.Optional[users.User] = attr.ib(eq=False, hash=False) + target_user: typing.Optional[users.User] = attr.ib(eq=False, hash=False, repr=False) """The object of the user who this invite targets, if set.""" - target_user_type: typing.Optional[TargetUserType] = attr.ib(eq=False, hash=False) + target_user_type: typing.Optional[TargetUserType] = attr.ib(eq=False, hash=False, repr=False) """The type of user target this invite is, if applicable.""" - approximate_presence_count: typing.Optional[int] = attr.ib(eq=False, hash=False) + approximate_presence_count: typing.Optional[int] = attr.ib(eq=False, hash=False, repr=False) """The approximate amount of presences in this invite's guild. This is only present when `with_counts` is passed as `True` to the GET Invites endpoint. """ - approximate_member_count: typing.Optional[int] = attr.ib(eq=False, hash=False) + approximate_member_count: typing.Optional[int] = attr.ib(eq=False, hash=False, repr=False) """The approximate amount of members in this invite's guild. This is only present when `with_counts` is passed as `True` to the GET @@ -223,7 +223,7 @@ class InviteWithMetadata(Invite): If set to `0` then this is unlimited. """ - max_age: typing.Optional[datetime.timedelta] = attr.attrib(eq=False, hash=False) + max_age: typing.Optional[datetime.timedelta] = attr.attrib(eq=False, hash=False, repr=False) """The timedelta of how long this invite will be valid for. If set to `None` then this is unlimited. @@ -232,7 +232,7 @@ class InviteWithMetadata(Invite): is_temporary: bool = attr.attrib(eq=False, hash=False, repr=True) """Whether this invite grants temporary membership.""" - created_at: datetime.datetime = attr.attrib(eq=False, hash=False) + created_at: datetime.datetime = attr.attrib(eq=False, hash=False, repr=False) """When this invite was created.""" @property diff --git a/hikari/models/messages.py b/hikari/models/messages.py index 3bc55cec5d..1675214321 100644 --- a/hikari/models/messages.py +++ b/hikari/models/messages.py @@ -157,13 +157,13 @@ class Attachment(bases.Unique, files_.BaseStream): url: str = attr.ib(repr=True) """The source URL of file.""" - proxy_url: str = attr.ib() + proxy_url: str = attr.ib(repr=False) """The proxied URL of file.""" - height: typing.Optional[int] = attr.ib() + height: typing.Optional[int] = attr.ib(repr=False) """The height of the image (if the file is an image).""" - width: typing.Optional[int] = attr.ib() + width: typing.Optional[int] = attr.ib(repr=False) """The width of the image (if the file is an image).""" def __aiter__(self) -> typing.AsyncGenerator[bytes]: @@ -180,7 +180,7 @@ class Reaction: emoji: typing.Union[emojis_.UnicodeEmoji, emojis_.CustomEmoji] = attr.ib(eq=True, hash=True, repr=True) """The emoji used to react.""" - is_reacted_by_me: bool = attr.ib(eq=False, hash=False) + is_reacted_by_me: bool = attr.ib(eq=False, hash=False, repr=False) """Whether the current user reacted using this emoji.""" @@ -237,64 +237,64 @@ class Message(bases.Entity, bases.Unique): member: typing.Optional[guilds.Member] = attr.ib(eq=False, hash=False, repr=True) """The member properties for the message's author.""" - content: str = attr.ib(eq=False, hash=False) + content: str = attr.ib(eq=False, hash=False, repr=False) """The content of the message.""" timestamp: datetime.datetime = attr.ib(eq=False, hash=False, repr=True) """The timestamp that the message was sent at.""" - edited_timestamp: typing.Optional[datetime.datetime] = attr.ib(eq=False, hash=False) + edited_timestamp: typing.Optional[datetime.datetime] = attr.ib(eq=False, hash=False, repr=False) """The timestamp that the message was last edited at. Will be `None` if it wasn't ever edited. """ - is_tts: bool = attr.ib(eq=False, hash=False) + is_tts: bool = attr.ib(eq=False, hash=False, repr=False) """Whether the message is a TTS message.""" - is_mentioning_everyone: bool = attr.ib(eq=False, hash=False) + is_mentioning_everyone: bool = attr.ib(eq=False, hash=False, repr=False) """Whether the message mentions `@everyone` or `@here`.""" - user_mentions: typing.Set[snowflake.Snowflake] = attr.ib(eq=False, hash=False) + user_mentions: typing.Set[snowflake.Snowflake] = attr.ib(eq=False, hash=False, repr=False) """The users the message mentions.""" - role_mentions: typing.Set[snowflake.Snowflake] = attr.ib(eq=False, hash=False) + role_mentions: typing.Set[snowflake.Snowflake] = attr.ib(eq=False, hash=False, repr=False) """The roles the message mentions.""" - channel_mentions: typing.Set[snowflake.Snowflake] = attr.ib(eq=False, hash=False) + channel_mentions: typing.Set[snowflake.Snowflake] = attr.ib(eq=False, hash=False, repr=False) """The channels the message mentions.""" - attachments: typing.Sequence[Attachment] = attr.ib(eq=False, hash=False) + attachments: typing.Sequence[Attachment] = attr.ib(eq=False, hash=False, repr=False) """The message attachments.""" - embeds: typing.Sequence[embeds_.Embed] = attr.ib(eq=False, hash=False) + embeds: typing.Sequence[embeds_.Embed] = attr.ib(eq=False, hash=False, repr=False) """The message embeds.""" - reactions: typing.Sequence[Reaction] = attr.ib(eq=False, hash=False) + reactions: typing.Sequence[Reaction] = attr.ib(eq=False, hash=False, repr=False) """The message reactions.""" - is_pinned: bool = attr.ib(eq=False, hash=False) + is_pinned: bool = attr.ib(eq=False, hash=False, repr=False) """Whether the message is pinned.""" - webhook_id: typing.Optional[snowflake.Snowflake] = attr.ib(eq=False, hash=False) + webhook_id: typing.Optional[snowflake.Snowflake] = attr.ib(eq=False, hash=False, repr=False) """If the message was generated by a webhook, the webhook's id.""" - type: MessageType = attr.ib(eq=False, hash=False) + type: MessageType = attr.ib(eq=False, hash=False, repr=False) """The message type.""" - activity: typing.Optional[MessageActivity] = attr.ib(eq=False, hash=False) + activity: typing.Optional[MessageActivity] = attr.ib(eq=False, hash=False, repr=False) """The message activity.""" - application: typing.Optional[applications.Application] = attr.ib(eq=False, hash=False) + application: typing.Optional[applications.Application] = attr.ib(eq=False, hash=False, repr=False) """The message application.""" - message_reference: typing.Optional[MessageCrosspost] = attr.ib(eq=False, hash=False) + message_reference: typing.Optional[MessageCrosspost] = attr.ib(eq=False, hash=False, repr=False) """The message crossposted reference data.""" - flags: typing.Optional[MessageFlag] = attr.ib(eq=False, hash=False) + flags: typing.Optional[MessageFlag] = attr.ib(eq=False, hash=False, repr=False) """The message flags.""" - nonce: typing.Optional[str] = attr.ib(eq=False, hash=False) + nonce: typing.Optional[str] = attr.ib(eq=False, hash=False, repr=False) """The message nonce. This is a string used for validating a message was sent.""" async def fetch_channel(self) -> channels.PartialChannel: diff --git a/hikari/models/presences.py b/hikari/models/presences.py index f47cb4dce4..e2dbc72267 100644 --- a/hikari/models/presences.py +++ b/hikari/models/presences.py @@ -97,10 +97,10 @@ class ActivityParty: id: typing.Optional[str] = attr.ib(eq=True, hash=True, repr=True) """The string id of this party instance, if set.""" - current_size: typing.Optional[int] = attr.ib(eq=False, hash=False) + current_size: typing.Optional[int] = attr.ib(eq=False, hash=False, repr=False) """Current size of this party, if applicable.""" - max_size: typing.Optional[int] = attr.ib(eq=False, hash=False) + max_size: typing.Optional[int] = attr.ib(eq=False, hash=False, repr=False) """Maximum size of this party, if applicable.""" @@ -108,16 +108,16 @@ class ActivityParty: class ActivityAssets: """Used to represent possible assets for an activity.""" - large_image: typing.Optional[str] = attr.ib() + large_image: typing.Optional[str] = attr.ib(repr=False) """The ID of the asset's large image, if set.""" - large_text: typing.Optional[str] = attr.ib() + large_text: typing.Optional[str] = attr.ib(repr=False) """The text that'll appear when hovering over the large image, if set.""" - small_image: typing.Optional[str] = attr.ib() + small_image: typing.Optional[str] = attr.ib(repr=False) """The ID of the asset's small image, if set.""" - small_text: typing.Optional[str] = attr.ib() + small_text: typing.Optional[str] = attr.ib(repr=False) """The text that'll appear when hovering over the small image, if set.""" @@ -125,13 +125,13 @@ class ActivityAssets: class ActivitySecret: """The secrets used for interacting with an activity party.""" - join: typing.Optional[str] = attr.ib() + join: typing.Optional[str] = attr.ib(repr=False) """The secret used for joining a party, if applicable.""" - spectate: typing.Optional[str] = attr.ib() + spectate: typing.Optional[str] = attr.ib(repr=False) """The secret used for spectating a party, if applicable.""" - match: typing.Optional[str] = attr.ib() + match: typing.Optional[str] = attr.ib(repr=False) """The secret used for joining a party, if applicable.""" @@ -171,42 +171,42 @@ class RichActivity: type: ActivityType = attr.ib(repr=True) """The activity's type.""" - url: typing.Optional[str] = attr.ib() + url: typing.Optional[str] = attr.ib(repr=False) """The URL for a `STREAM` type activity, if applicable.""" - created_at: datetime.datetime = attr.ib() + created_at: datetime.datetime = attr.ib(repr=False) """When this activity was added to the user's session.""" - timestamps: typing.Optional[ActivityTimestamps] = attr.ib() + timestamps: typing.Optional[ActivityTimestamps] = attr.ib(repr=False) """The timestamps for when this activity's current state will start and end, if applicable. """ - application_id: typing.Optional[snowflake.Snowflake] = attr.ib() + application_id: typing.Optional[snowflake.Snowflake] = attr.ib(repr=False) """The ID of the application this activity is for, if applicable.""" - details: typing.Optional[str] = attr.ib() + details: typing.Optional[str] = attr.ib(repr=False) """The text that describes what the activity's target is doing, if set.""" - state: typing.Optional[str] = attr.ib() + state: typing.Optional[str] = attr.ib(repr=False) """The current status of this activity's target, if set.""" - emoji: typing.Union[None, emojis_.UnicodeEmoji, emojis_.CustomEmoji] = attr.ib() + emoji: typing.Union[None, emojis_.UnicodeEmoji, emojis_.CustomEmoji] = attr.ib(repr=False) """The emoji of this activity, if it is a custom status and set.""" - party: typing.Optional[ActivityParty] = attr.ib() + party: typing.Optional[ActivityParty] = attr.ib(repr=False) """Information about the party associated with this activity, if set.""" - assets: typing.Optional[ActivityAssets] = attr.ib() + assets: typing.Optional[ActivityAssets] = attr.ib(repr=False) """Images and their hover over text for the activity.""" - secrets: typing.Optional[ActivitySecret] = attr.ib() + secrets: typing.Optional[ActivitySecret] = attr.ib(repr=False) """Secrets for Rich Presence joining and spectating.""" - is_instance: typing.Optional[bool] = attr.ib() + is_instance: typing.Optional[bool] = attr.ib(repr=False) """Whether this activity is an instanced game session.""" - flags: ActivityFlag = attr.ib() + flags: ActivityFlag = attr.ib(repr=False) """Flags that describe what the activity includes.""" @@ -260,22 +260,16 @@ class PresenceUser(users.User): username: typing.Union[str, undefined.Undefined] = attr.ib(eq=False, hash=False, repr=True) """This user's username.""" - avatar_hash: typing.Union[None, str, undefined.Undefined] = attr.ib( - eq=False, hash=False, repr=True, - ) + avatar_hash: typing.Union[None, str, undefined.Undefined] = attr.ib(eq=False, hash=False, repr=True) """This user's avatar hash, if set.""" - is_bot: typing.Union[bool, undefined.Undefined] = attr.ib( - eq=False, hash=False, repr=True, - ) + is_bot: typing.Union[bool, undefined.Undefined] = attr.ib(eq=False, hash=False, repr=True) """Whether this user is a bot account.""" - is_system: typing.Union[bool, undefined.Undefined] = attr.ib( - eq=False, hash=False, - ) + is_system: typing.Union[bool, undefined.Undefined] = attr.ib(eq=False, hash=False, repr=False) """Whether this user is a system account.""" - flags: typing.Union[users.UserFlag, undefined.Undefined] = attr.ib(eq=False, hash=False) + flags: typing.Union[users.UserFlag, undefined.Undefined] = attr.ib(eq=False, hash=False, repr=False) """The public flags for this user.""" @property @@ -358,9 +352,7 @@ class MemberPresence(bases.Entity): changed in an event. """ - role_ids: typing.Optional[typing.Sequence[snowflake.Snowflake]] = attr.ib( - eq=False, hash=False, - ) + role_ids: typing.Optional[typing.Sequence[snowflake.Snowflake]] = attr.ib(eq=False, hash=False, repr=False) """The ids of the user's current roles in the guild this presence belongs to. !!! info @@ -377,27 +369,21 @@ class MemberPresence(bases.Entity): visible_status: PresenceStatus = attr.ib(eq=False, hash=False, repr=True) """This user's current status being displayed by the client.""" - activities: typing.Sequence[RichActivity] = attr.ib(eq=False, hash=False) + activities: typing.Sequence[RichActivity] = attr.ib(eq=False, hash=False, repr=False) """An array of the user's activities, with the top one will being prioritised by the client. """ - client_status: ClientStatus = attr.ib( - eq=False, hash=False, - ) + client_status: ClientStatus = attr.ib(eq=False, hash=False, repr=False) """An object of the target user's client statuses.""" - premium_since: typing.Optional[datetime.datetime] = attr.ib( - eq=False, hash=False, - ) + premium_since: typing.Optional[datetime.datetime] = attr.ib(eq=False, hash=False, repr=False) """The datetime of when this member started "boosting" this guild. This will be `None` if they aren't boosting. """ - nickname: typing.Optional[str] = attr.ib( - eq=False, hash=False, repr=True, - ) + nickname: typing.Optional[str] = attr.ib(eq=False, hash=False, repr=True) """This member's nickname, if set.""" @@ -408,11 +394,11 @@ class OwnActivity: This will show the activity as the bot's presence. """ - name: str = attr.ib() + name: str = attr.ib(repr=True) """The activity name.""" - url: typing.Optional[str] = attr.ib(default=None) + url: typing.Optional[str] = attr.ib(default=None, repr=True) """The activity URL. Only valid for `STREAMING` activities.""" - type: ActivityType = attr.ib(converter=ActivityType) + type: ActivityType = attr.ib(converter=ActivityType, repr=True) """The activity type.""" diff --git a/hikari/models/users.py b/hikari/models/users.py index 457a41c02d..70128ea9c9 100644 --- a/hikari/models/users.py +++ b/hikari/models/users.py @@ -102,16 +102,16 @@ class User(bases.Entity, bases.Unique): username: str = attr.ib(eq=False, hash=False, repr=True) """This user's username.""" - avatar_hash: typing.Optional[str] = attr.ib(eq=False, hash=False) + avatar_hash: typing.Optional[str] = attr.ib(eq=False, hash=False, repr=False) """This user's avatar hash, if set.""" - is_bot: bool = attr.ib(eq=False, hash=False) + is_bot: bool = attr.ib(eq=False, hash=False, repr=False) """Whether this user is a bot account.""" - is_system: bool = attr.ib(eq=False, hash=False) + is_system: bool = attr.ib(eq=False, hash=False, repr=False) """Whether this user is a system account.""" - flags: UserFlag = attr.ib(eq=False, hash=False) + flags: UserFlag = attr.ib(eq=False, hash=False, repr=False) """The public flags for this user.""" async def fetch_self(self) -> User: @@ -183,30 +183,30 @@ def default_avatar_url(self) -> str: class OwnUser(User): """Represents a user with extended OAuth2 information.""" - is_mfa_enabled: bool = attr.ib(eq=False, hash=False) + is_mfa_enabled: bool = attr.ib(eq=False, hash=False, repr=False) """Whether the user's account has 2fa enabled.""" - locale: typing.Optional[str] = attr.ib(eq=False, hash=False) + locale: typing.Optional[str] = attr.ib(eq=False, hash=False, repr=False) """The user's set language. This is not provided by the `READY` event.""" - is_verified: typing.Optional[bool] = attr.ib(eq=False, hash=False) + is_verified: typing.Optional[bool] = attr.ib(eq=False, hash=False, repr=False) """Whether the email for this user's account has been verified. Will be `None` if retrieved through the oauth2 flow without the `email` scope. """ - email: typing.Optional[str] = attr.ib(eq=False, hash=False) + email: typing.Optional[str] = attr.ib(eq=False, hash=False, repr=False) """The user's set email. Will be `None` if retrieved through the oauth2 flow without the `email` scope and for bot users. """ - flags: UserFlag = attr.ib(eq=False, hash=False) + flags: UserFlag = attr.ib(eq=False, hash=False, repr=False) """This user account's flags.""" - premium_type: typing.Optional[PremiumType] = attr.ib(eq=False, hash=False) + premium_type: typing.Optional[PremiumType] = attr.ib(eq=False, hash=False, repr=False) """The type of Nitro Subscription this user account had. This will always be `None` for bots. diff --git a/hikari/models/voices.py b/hikari/models/voices.py index 31a13f3b59..a635796c49 100644 --- a/hikari/models/voices.py +++ b/hikari/models/voices.py @@ -49,28 +49,28 @@ class VoiceState(bases.Entity): user_id: snowflake.Snowflake = attr.ib(eq=False, hash=False, repr=True) """The ID of the user this voice state is for.""" - member: typing.Optional[guilds.Member] = attr.ib(eq=False, hash=False) + member: typing.Optional[guilds.Member] = attr.ib(eq=False, hash=False, repr=False) """The guild member this voice state is for if the voice state is in a guild.""" session_id: str = attr.ib(eq=True, hash=True, repr=True) """The string ID of this voice state's session.""" - is_guild_deafened: bool = attr.ib(eq=False, hash=False) + is_guild_deafened: bool = attr.ib(eq=False, hash=False, repr=False) """Whether this user is deafened by the guild.""" - is_guild_muted: bool = attr.ib(eq=False, hash=False) + is_guild_muted: bool = attr.ib(eq=False, hash=False, repr=False) """Whether this user is muted by the guild.""" - is_self_deafened: bool = attr.ib(eq=False, hash=False) + is_self_deafened: bool = attr.ib(eq=False, hash=False, repr=False) """Whether this user is deafened by their client.""" - is_self_muted: bool = attr.ib(eq=False, hash=False) + is_self_muted: bool = attr.ib(eq=False, hash=False, repr=False) """Whether this user is muted by their client.""" - is_streaming: bool = attr.ib(eq=False, hash=False) + is_streaming: bool = attr.ib(eq=False, hash=False, repr=False) """Whether this user is streaming using "Go Live".""" - is_suppressed: bool = attr.ib(eq=False, hash=False) + is_suppressed: bool = attr.ib(eq=False, hash=False, repr=False) """Whether this user is muted by the current user.""" @@ -89,16 +89,16 @@ class VoiceRegion: name: str = attr.ib(eq=False, hash=False, repr=True) """The name of this region.""" - is_vip: bool = attr.ib(eq=False, hash=False) + is_vip: bool = attr.ib(eq=False, hash=False, repr=False) """Whether this region is vip-only.""" - is_optimal_location: bool = attr.ib(eq=False, hash=False) + is_optimal_location: bool = attr.ib(eq=False, hash=False, repr=False) """Whether this region's server is closest to the current user's client.""" - is_deprecated: bool = attr.ib(eq=False, hash=False) + is_deprecated: bool = attr.ib(eq=False, hash=False, repr=False) """Whether this region is deprecated.""" - is_custom: bool = attr.ib(eq=False, hash=False) + is_custom: bool = attr.ib(eq=False, hash=False, repr=False) """Whether this region is custom (e.g. used for events).""" def __str__(self) -> str: diff --git a/hikari/models/webhooks.py b/hikari/models/webhooks.py index 4e118d099b..703d1d1960 100644 --- a/hikari/models/webhooks.py +++ b/hikari/models/webhooks.py @@ -69,9 +69,7 @@ class Webhook(bases.Entity, bases.Unique): channel_id: snowflake.Snowflake = attr.ib(eq=False, hash=False, repr=True) """The channel ID this webhook is for.""" - author: typing.Optional[users_.User] = attr.ib( - eq=False, hash=False, repr=True, - ) + author: typing.Optional[users_.User] = attr.ib(eq=False, hash=False, repr=True) """The user that created the webhook !!! info @@ -82,10 +80,10 @@ class Webhook(bases.Entity, bases.Unique): name: typing.Optional[str] = attr.ib(eq=False, hash=False, repr=True) """The name of the webhook.""" - avatar_hash: typing.Optional[str] = attr.ib(eq=False, hash=False) + avatar_hash: typing.Optional[str] = attr.ib(eq=False, hash=False, repr=False) """The avatar hash of the webhook.""" - token: typing.Optional[str] = attr.ib(eq=False, hash=False) + token: typing.Optional[str] = attr.ib(eq=False, hash=False, repr=False) """The token for the webhook. !!! info From f2cac8827620104419d0526664b420440e852f72 Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Sat, 6 Jun 2020 18:59:31 +0100 Subject: [PATCH 483/922] Started static type checking analysis. Fixed more errors on MyPy for webhooks and data_binding. --- ci/gitlab/linting.yml | 8 ++ ci/mypy.nox.py | 37 ++++++ hikari/__init__.py | 7 +- hikari/__main__.py | 16 ++- hikari/api/__init__.py | 5 +- hikari/api/app.py | 2 +- hikari/api/cache.py | 5 +- hikari/api/component.py | 2 +- hikari/api/entity_factory.py | 2 +- hikari/api/event_consumer.py | 2 +- hikari/api/event_dispatcher.py | 6 +- hikari/errors.py | 16 ++- hikari/events/__init__.py | 5 +- hikari/events/base.py | 14 ++- hikari/events/channel.py | 2 +- hikari/events/guild.py | 2 +- hikari/events/message.py | 2 +- hikari/events/other.py | 2 +- hikari/events/voice.py | 2 +- hikari/impl/__init__.py | 5 +- hikari/impl/bot.py | 2 +- hikari/impl/cache.py | 2 +- hikari/impl/entity_factory.py | 2 +- hikari/impl/event_manager.py | 2 +- hikari/impl/event_manager_core.py | 6 +- hikari/impl/gateway_zookeeper.py | 2 +- hikari/impl/rest_app.py | 2 +- hikari/models/__init__.py | 5 +- hikari/models/applications.py | 2 +- hikari/models/audit_logs.py | 2 +- hikari/models/bases.py | 18 +-- hikari/models/channels.py | 2 +- hikari/models/colors.py | 15 ++- hikari/models/colours.py | 5 +- hikari/models/embeds.py | 2 +- hikari/models/emojis.py | 2 +- hikari/models/files.py | 2 +- hikari/models/gateway.py | 2 +- hikari/models/guilds.py | 2 +- hikari/models/intents.py | 5 +- hikari/models/invites.py | 2 +- hikari/models/messages.py | 2 +- hikari/models/permissions.py | 5 +- hikari/models/presences.py | 2 +- hikari/models/users.py | 2 +- hikari/models/voices.py | 2 +- hikari/models/webhooks.py | 191 +++++++++++------------------ hikari/net/__init__.py | 5 +- hikari/net/buckets.py | 8 +- hikari/net/gateway.py | 5 +- hikari/net/http_client.py | 30 ++--- hikari/net/http_settings.py | 2 +- hikari/net/iterators.py | 2 +- hikari/net/rate_limits.py | 23 ++-- hikari/net/rest.py | 7 +- hikari/net/rest_utils.py | 2 +- hikari/net/routes.py | 2 +- hikari/net/tracing.py | 32 +++-- hikari/net/user_agents.py | 15 ++- hikari/utilities/__init__.py | 5 +- hikari/utilities/aio.py | 118 ++---------------- hikari/utilities/cdn.py | 4 +- hikari/utilities/data_binding.py | 29 ++--- hikari/utilities/date.py | 8 +- hikari/utilities/klass.py | 8 +- hikari/utilities/reflect.py | 2 +- hikari/utilities/snowflake.py | 8 +- hikari/utilities/undefined.py | 10 +- tests/hikari/utilities/test_aio.py | 22 ---- 69 files changed, 354 insertions(+), 423 deletions(-) create mode 100644 ci/mypy.nox.py diff --git a/ci/gitlab/linting.yml b/ci/gitlab/linting.yml index 09b4c41668..31bcc2547f 100644 --- a/ci/gitlab/linting.yml +++ b/ci/gitlab/linting.yml @@ -80,3 +80,11 @@ safety: script: - nox -s safety --no-error-on-external-run + +### +### MyPy static type checker. +### +mypy: + extends: .lint + script: + - nox -s mypy --no-error-on-external-run diff --git a/ci/mypy.nox.py b/ci/mypy.nox.py new file mode 100644 index 0000000000..881bb5836d --- /dev/null +++ b/ci/mypy.nox.py @@ -0,0 +1,37 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# Copyright © Nekoka.tt 2019-2020 +# +# This file is part of Hikari. +# +# Hikari is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Hikari is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Hikari. If not, see . + +from ci import config +from ci import nox + + +@nox.session(reuse_venv=True, default=True) +def mypy(session: nox.Session) -> None: + session.install("-r", "requirements.txt", "mypy==0.780") + session.run( + "mypy", + "-p", + config.MAIN_PACKAGE, + "--pretty", + "--show-error-codes", + "--show-column-numbers", + # "--show-error-context", + "--strict", + "--warn-redundant-casts", + ) diff --git a/hikari/__init__.py b/hikari/__init__.py index e4656326c3..458fb1428d 100644 --- a/hikari/__init__.py +++ b/hikari/__init__.py @@ -20,6 +20,11 @@ from __future__ import annotations +__all__: typing.List[str] = ["RESTClient", "Bot"] + +# noinspection PyUnresolvedReferences +import typing + from hikari._about import __author__ from hikari._about import __ci__ from hikari._about import __copyright__ @@ -33,5 +38,3 @@ from hikari.impl.bot import BotImpl as Bot from hikari.impl.rest_app import RESTAppImpl as RESTClient - -__all__ = ["RESTClient", "Bot"] diff --git a/hikari/__main__.py b/hikari/__main__.py index 0a7064402c..ba6330d678 100644 --- a/hikari/__main__.py +++ b/hikari/__main__.py @@ -17,19 +17,23 @@ # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . """Provides a command-line entry point that shows the library version and then exits.""" - from __future__ import annotations +__all__: typing.List[str] = [] + import inspect import os import platform +# noinspection PyUnresolvedReferences +import typing + from hikari import _about -version = _about.__version__ # noinspection PyTypeChecker -path = os.path.abspath(os.path.dirname(inspect.getsourcefile(_about))) -py_impl = platform.python_implementation() -py_ver = platform.python_version() -py_compiler = platform.python_compiler() +path: typing.Final[str] = os.path.abspath(os.path.dirname(inspect.getsourcefile(_about))) +version: typing.Final[str] = _about.__version__ +py_impl: typing.Final[str] = platform.python_implementation() +py_ver: typing.Final[str] = platform.python_version() +py_compiler: typing.Final[str] = platform.python_compiler() print(f"hikari v{version} (installed in {path}) ({py_impl} {py_ver} {py_compiler})") diff --git a/hikari/api/__init__.py b/hikari/api/__init__.py index cda5f6e30f..ad22681850 100644 --- a/hikari/api/__init__.py +++ b/hikari/api/__init__.py @@ -24,4 +24,7 @@ """ from __future__ import annotations -__all__ = [] +__all__: typing.List[str] = [] + +# noinspection PyUnresolvedReferences +import typing diff --git a/hikari/api/app.py b/hikari/api/app.py index d4879fde24..aec961a212 100644 --- a/hikari/api/app.py +++ b/hikari/api/app.py @@ -20,7 +20,7 @@ from __future__ import annotations -__all__ = ["IApp", "IRESTApp", "IGatewayConsumer", "IGatewayDispatcher", "IGatewayZookeeper", "IBot"] +__all__: typing.List[str] = ["IApp", "IRESTApp", "IGatewayConsumer", "IGatewayDispatcher", "IGatewayZookeeper", "IBot"] import abc import functools diff --git a/hikari/api/cache.py b/hikari/api/cache.py index c5a58c6a26..26a5397160 100644 --- a/hikari/api/cache.py +++ b/hikari/api/cache.py @@ -19,10 +19,13 @@ """Core interface for a cache implementation.""" from __future__ import annotations -__all__ = ["ICache"] +__all__: typing.List[str] = ["ICache"] import abc +# noinspection PyUnresolvedReferences +import typing + from hikari.api import component diff --git a/hikari/api/component.py b/hikari/api/component.py index feb5ba752c..71f6227378 100644 --- a/hikari/api/component.py +++ b/hikari/api/component.py @@ -20,7 +20,7 @@ from __future__ import annotations -__all__ = ["IComponent"] +__all__: typing.List[str] = ["IComponent"] import abc import typing diff --git a/hikari/api/entity_factory.py b/hikari/api/entity_factory.py index 464fc04bc8..21787e4cf2 100644 --- a/hikari/api/entity_factory.py +++ b/hikari/api/entity_factory.py @@ -19,7 +19,7 @@ """Core interface for an object that serializes/deserializes API objects.""" from __future__ import annotations -__all__ = ["IEntityFactory"] +__all__: typing.List[str] = ["IEntityFactory"] import abc import typing diff --git a/hikari/api/event_consumer.py b/hikari/api/event_consumer.py index 2081ff11b5..017a3e4c99 100644 --- a/hikari/api/event_consumer.py +++ b/hikari/api/event_consumer.py @@ -19,7 +19,7 @@ """Core interface for components that consume raw API event payloads.""" from __future__ import annotations -__all__ = ["IEventConsumer"] +__all__: typing.List[str] = ["IEventConsumer"] import abc import typing diff --git a/hikari/api/event_dispatcher.py b/hikari/api/event_dispatcher.py index 80cfbb5f8f..d1bb4c7175 100644 --- a/hikari/api/event_dispatcher.py +++ b/hikari/api/event_dispatcher.py @@ -19,9 +19,10 @@ """Core interface for components that dispatch events to the library.""" from __future__ import annotations -__all__ = ["IEventDispatcher"] +__all__: typing.List[str] = ["IEventDispatcher"] import abc +import asyncio import typing from hikari.api import component @@ -29,7 +30,6 @@ if typing.TYPE_CHECKING: from hikari.events import base - from hikari.utilities import aio _EventT = typing.TypeVar("_EventT", bound=base.HikariEvent, covariant=True) _PredicateT = typing.Callable[[_EventT], typing.Union[bool, typing.Coroutine[None, typing.Any, bool]]] @@ -49,7 +49,7 @@ class IEventDispatcher(component.IComponent, abc.ABC): __slots__ = () @abc.abstractmethod - def dispatch(self, event: base.HikariEvent) -> aio.Future[typing.Any]: + def dispatch(self, event: base.HikariEvent) -> asyncio.Future[typing.Any]: """Dispatch an event. Parameters diff --git a/hikari/errors.py b/hikari/errors.py index 3ab32a1036..358c9924c5 100644 --- a/hikari/errors.py +++ b/hikari/errors.py @@ -20,7 +20,7 @@ from __future__ import annotations -__all__ = [ +__all__: typing.List[str] = [ "HikariError", "HikariWarning", "NotFound", @@ -170,7 +170,7 @@ class HTTPErrorResponse(HTTPError): ---------- url : str The URL that produced the error message. - status : http.HTTPStatus + status : int or http.HTTPStatus The HTTP status code of the response that caused this error. headers : aiohttp.typedefs.LooseHeaders Any headers that were given in the response. @@ -180,7 +180,7 @@ class HTTPErrorResponse(HTTPError): __slots__ = ("status", "headers", "raw_body") - status: http.HTTPStatus + status: typing.Union[int, http.HTTPStatus] """The HTTP status code for the response.""" headers: aiohttp.typedefs.LooseHeaders @@ -209,8 +209,14 @@ def __str__(self) -> str: raw_body = str(self.raw_body) chomped = len(raw_body) > 200 - name = self.status.name.replace("_", " ").title() - return f"{self.status.value} {name}: {raw_body[:200]}{'...' if chomped else ''}" + + if isinstance(self.status, http.HTTPStatus): + name = self.status.name.replace("_", " ").title() + name_value = f"{name} {self.status.value}" + else: + name_value = str(self.status) + + return f"{name_value}: {raw_body[:200]}{'...' if chomped else ''}" class ClientHTTPErrorResponse(HTTPErrorResponse): diff --git a/hikari/events/__init__.py b/hikari/events/__init__.py index e6d867bd7f..cfacff330c 100644 --- a/hikari/events/__init__.py +++ b/hikari/events/__init__.py @@ -20,4 +20,7 @@ from __future__ import annotations -__all__ = [] +__all__: typing.List[str] = [] + +# noinspection PyUnresolvedReferences +import typing diff --git a/hikari/events/base.py b/hikari/events/base.py index 1e20d12015..bb804e6766 100644 --- a/hikari/events/base.py +++ b/hikari/events/base.py @@ -20,7 +20,13 @@ from __future__ import annotations -__all__ = ["HikariEvent", "get_required_intents_for", "requires_intents", "no_catch", "is_no_catch_event"] +__all__: typing.List[str] = [ + "HikariEvent", + "get_required_intents_for", + "requires_intents", + "no_catch", + "is_no_catch_event", +] import abc import typing @@ -56,7 +62,7 @@ def get_required_intents_for(event_type: typing.Type[HikariEvent]) -> typing.Col Collection of acceptable subset combinations of intent needed to be able to receive the given event type. """ - return getattr(event_type, _REQUIRED_INTENTS_ATTR, ()) + return typing.cast(typing.Collection[typing.Any], getattr(event_type, _REQUIRED_INTENTS_ATTR, ())) def requires_intents( @@ -82,7 +88,7 @@ def decorator(cls: typing.Type[_HikariEventT]) -> typing.Type[_HikariEventT]: return decorator -def no_catch(): +def no_catch() -> typing.Callable[[typing.Type[_HikariEventT]], typing.Type[_HikariEventT]]: """Decorate an event type to indicate errors should not be handled. This is useful for exception event types that you do not want to @@ -98,4 +104,4 @@ def decorator(cls: typing.Type[_HikariEventT]) -> typing.Type[_HikariEventT]: def is_no_catch_event(obj: typing.Union[_HikariEventT, typing.Type[_HikariEventT]]) -> bool: """Return True if this event is marked as `no_catch`.""" - return getattr(obj, _NO_THROW_ATTR, False) + return typing.cast(bool, getattr(obj, _NO_THROW_ATTR, False)) diff --git a/hikari/events/channel.py b/hikari/events/channel.py index 37e829f3ec..f8cc68411d 100644 --- a/hikari/events/channel.py +++ b/hikari/events/channel.py @@ -20,7 +20,7 @@ from __future__ import annotations -__all__ = [ +__all__: typing.List[str] = [ "BaseChannelEvent", "ChannelCreateEvent", "ChannelUpdateEvent", diff --git a/hikari/events/guild.py b/hikari/events/guild.py index 7be5a8d2ff..5d324a264b 100644 --- a/hikari/events/guild.py +++ b/hikari/events/guild.py @@ -20,7 +20,7 @@ from __future__ import annotations -__all__ = [ +__all__: typing.List[str] = [ "GuildEvent", "GuildCreateEvent", "GuildUpdateEvent", diff --git a/hikari/events/message.py b/hikari/events/message.py index 2fdf000fbd..3387d1fe50 100644 --- a/hikari/events/message.py +++ b/hikari/events/message.py @@ -20,7 +20,7 @@ from __future__ import annotations -__all__ = [ +__all__: typing.List[str] = [ "MessageCreateEvent", "UpdateMessage", "MessageUpdateEvent", diff --git a/hikari/events/other.py b/hikari/events/other.py index bcbf207400..90d3650198 100644 --- a/hikari/events/other.py +++ b/hikari/events/other.py @@ -20,7 +20,7 @@ from __future__ import annotations -__all__ = [ +__all__: typing.List[str] = [ "ExceptionEvent", "ConnectedEvent", "DisconnectedEvent", diff --git a/hikari/events/voice.py b/hikari/events/voice.py index d57830322c..702c3e3f8a 100644 --- a/hikari/events/voice.py +++ b/hikari/events/voice.py @@ -20,7 +20,7 @@ from __future__ import annotations -__all__ = ["VoiceStateUpdateEvent", "VoiceServerUpdateEvent"] +__all__: typing.List[str] = ["VoiceStateUpdateEvent", "VoiceServerUpdateEvent"] import typing diff --git a/hikari/impl/__init__.py b/hikari/impl/__init__.py index 8f9a70f321..633c2fb1fc 100644 --- a/hikari/impl/__init__.py +++ b/hikari/impl/__init__.py @@ -26,4 +26,7 @@ from __future__ import annotations -__all__ = [] +__all__: typing.List[str] = [] + +# noinspection PyUnresolvedReferences +import typing diff --git a/hikari/impl/bot.py b/hikari/impl/bot.py index c7d6ab78ff..72a963de05 100644 --- a/hikari/impl/bot.py +++ b/hikari/impl/bot.py @@ -20,7 +20,7 @@ from __future__ import annotations -__all__ = ["BotImpl"] +__all__: typing.List[str] = ["BotImpl"] import inspect import logging diff --git a/hikari/impl/cache.py b/hikari/impl/cache.py index c9bdefa731..54a4446c01 100644 --- a/hikari/impl/cache.py +++ b/hikari/impl/cache.py @@ -20,7 +20,7 @@ from __future__ import annotations -__all__ = ["InMemoryCacheImpl"] +__all__: typing.List[str] = ["InMemoryCacheImpl"] import typing diff --git a/hikari/impl/entity_factory.py b/hikari/impl/entity_factory.py index 8350d98c61..e48f928b9c 100644 --- a/hikari/impl/entity_factory.py +++ b/hikari/impl/entity_factory.py @@ -20,7 +20,7 @@ from __future__ import annotations -__all__ = ["EntityFactoryImpl"] +__all__: typing.List[str] = ["EntityFactoryImpl"] import datetime import typing diff --git a/hikari/impl/event_manager.py b/hikari/impl/event_manager.py index 1b956bb249..8d44b7fcf7 100644 --- a/hikari/impl/event_manager.py +++ b/hikari/impl/event_manager.py @@ -20,7 +20,7 @@ from __future__ import annotations -__all__ = ["EventManagerImpl"] +__all__: typing.List[str] = ["EventManagerImpl"] import typing diff --git a/hikari/impl/event_manager_core.py b/hikari/impl/event_manager_core.py index 5f44878e16..4756dad1d5 100644 --- a/hikari/impl/event_manager_core.py +++ b/hikari/impl/event_manager_core.py @@ -20,7 +20,7 @@ from __future__ import annotations -__all__ = ["EventManagerCore"] +__all__: typing.List[str] = ["EventManagerCore"] import asyncio import functools @@ -46,7 +46,7 @@ _AsyncCallbackT = typing.Callable[[_EventT], typing.Coroutine[None, typing.Any, None]] _CallbackT = typing.Union[_SyncCallbackT, _AsyncCallbackT] _ListenerMapT = typing.MutableMapping[typing.Type[_EventT], typing.MutableSequence[_CallbackT]] - _WaiterT = typing.Tuple[_PredicateT, aio.Future[_EventT]] + _WaiterT = typing.Tuple[_PredicateT, asyncio.Future[_EventT]] _WaiterMapT = typing.MutableMapping[typing.Type[_EventT], typing.MutableSet[_WaiterT]] @@ -190,7 +190,7 @@ async def _invoke_callback(self, callback: _CallbackT, event: _EventT) -> None: self.logger.error("an exception occurred handling an event", exc_info=trio) await self.dispatch(other.ExceptionEvent(exception=ex, event=event, callback=callback)) - def dispatch(self, event: base.HikariEvent) -> aio.Future[typing.Any]: + def dispatch(self, event: base.HikariEvent) -> asyncio.Future[typing.Any]: if not isinstance(event, base.HikariEvent): raise TypeError(f"Events must be subclasses of {base.HikariEvent.__name__}, not {type(event).__name__}") diff --git a/hikari/impl/gateway_zookeeper.py b/hikari/impl/gateway_zookeeper.py index 2422a76e8c..a7fee1f4e2 100644 --- a/hikari/impl/gateway_zookeeper.py +++ b/hikari/impl/gateway_zookeeper.py @@ -20,7 +20,7 @@ from __future__ import annotations -__all__ = ["AbstractGatewayZookeeper"] +__all__: typing.List[str] = ["AbstractGatewayZookeeper"] import abc import asyncio diff --git a/hikari/impl/rest_app.py b/hikari/impl/rest_app.py index dd046b5f17..da5184da63 100644 --- a/hikari/impl/rest_app.py +++ b/hikari/impl/rest_app.py @@ -24,7 +24,7 @@ from __future__ import annotations -__all__ = ["RESTAppImpl"] +__all__: typing.List[str] = ["RESTAppImpl"] import logging import typing diff --git a/hikari/models/__init__.py b/hikari/models/__init__.py index e98ab52cf1..a8730b310f 100644 --- a/hikari/models/__init__.py +++ b/hikari/models/__init__.py @@ -20,4 +20,7 @@ from __future__ import annotations -__all__ = [] +__all__: typing.List[str] = [] + +# noinspection PyUnresolvedReferences +import typing diff --git a/hikari/models/applications.py b/hikari/models/applications.py index 43363e5e69..d23280997f 100644 --- a/hikari/models/applications.py +++ b/hikari/models/applications.py @@ -20,7 +20,7 @@ from __future__ import annotations -__all__ = [ +__all__: typing.List[str] = [ "Application", "ConnectionVisibility", "OAuth2Scope", diff --git a/hikari/models/audit_logs.py b/hikari/models/audit_logs.py index 2b609dcf04..97e0a16597 100644 --- a/hikari/models/audit_logs.py +++ b/hikari/models/audit_logs.py @@ -20,7 +20,7 @@ from __future__ import annotations -__all__ = [ +__all__: typing.List[str] = [ "AuditLogChange", "AuditLogChangeKey", "AuditLogEntry", diff --git a/hikari/models/bases.py b/hikari/models/bases.py index 946cc48aba..f59d2f2305 100644 --- a/hikari/models/bases.py +++ b/hikari/models/bases.py @@ -20,20 +20,19 @@ from __future__ import annotations -__all__ = ["Entity", "Unique"] +__all__: typing.List[str] = ["Entity", "Unique"] import abc import typing import attr +from hikari.api import app as app_ from hikari.utilities import snowflake if typing.TYPE_CHECKING: import datetime - from hikari.api import app as app_ - @attr.s(eq=True, hash=False, init=False, kw_only=True, slots=False) class Entity(abc.ABC): @@ -43,19 +42,12 @@ class Entity(abc.ABC): application instance. This enables it to perform API calls from methods directly. """ + _AppT = typing.Union[app_.IRESTApp, app_.IBot] - _app: typing.Union[ - None, - app_.IApp, - app_.IGatewayZookeeper, - app_.IGatewayConsumer, - app_.IGatewayDispatcher, - app_.IRESTApp, - app_.IBot, - ] = attr.ib(default=None, eq=False, hash=False, repr=False) + _app: _AppT = attr.ib(default=None, repr=False, eq=False, hash=False) """The client application that models may use for procedures.""" - def __init__(self, app: app_.IApp) -> None: + def __init__(self, app: _AppT) -> None: self._app = app diff --git a/hikari/models/channels.py b/hikari/models/channels.py index 9da288ca7c..9c011e0d92 100644 --- a/hikari/models/channels.py +++ b/hikari/models/channels.py @@ -20,7 +20,7 @@ from __future__ import annotations -__all__ = [ +__all__: typing.List[str] = [ "ChannelType", "PermissionOverwrite", "PermissionOverwriteType", diff --git a/hikari/models/colors.py b/hikari/models/colors.py index 2bf2e8c91a..876c681270 100644 --- a/hikari/models/colors.py +++ b/hikari/models/colors.py @@ -20,7 +20,7 @@ from __future__ import annotations -__all__ = ["Color"] +__all__: typing.List[str] = ["Color"] import string import typing @@ -125,10 +125,12 @@ class Color(int): __slots__ = () - def __new__(cls, raw_rgb: typing.Union[int, typing.SupportsInt], /) -> Color: - if not 0 <= int(raw_rgb) <= 0xFF_FF_FF: + def __init__(self, raw_rgb: typing.SupportsInt) -> None: + if not (0 <= int(raw_rgb) <= 0xFFFFFF): raise ValueError(f"raw_rgb must be in the exclusive range of 0 and {0xFF_FF_FF}") - return super(Color, cls).__new__(cls, raw_rgb) + # The __new__ for `int` initializes the value for us, this super-call does nothing other + # than keeping the linter happy. + super().__init__() def __repr__(self) -> str: r, g, b = self.rgb @@ -323,7 +325,9 @@ def from_int(cls, integer: typing.SupportsInt, /) -> Color: # Partially chose to override these as the docstrings contain typos according to Sphinx. @classmethod - def from_bytes(cls, bytes_: typing.Sequence[int], byteorder: str, *, signed: bool = True) -> Color: + def from_bytes( + cls, bytes_: typing.Union[typing.Iterable[int], typing.SupportsBytes], byteorder: str, *, signed: bool = True + ) -> Color: """Convert the bytes to a `Color`. Parameters @@ -346,6 +350,7 @@ def from_bytes(cls, bytes_: typing.Sequence[int], byteorder: str, *, signed: boo return Color(int.from_bytes(bytes_, byteorder, signed=signed)) @classmethod + @typing.no_type_check def of( cls, *values: typing.Union[ diff --git a/hikari/models/colours.py b/hikari/models/colours.py index 1a0bb15e36..b2c1606097 100644 --- a/hikari/models/colours.py +++ b/hikari/models/colours.py @@ -20,6 +20,9 @@ from __future__ import annotations -__all__ = ["Colour"] +__all__: typing.List[str] = ["Colour"] + +# noinspection PyUnresolvedReferences +import typing from hikari.models.colors import Color as Colour diff --git a/hikari/models/embeds.py b/hikari/models/embeds.py index ef9d7bbd2d..a24ca4e6fe 100644 --- a/hikari/models/embeds.py +++ b/hikari/models/embeds.py @@ -20,7 +20,7 @@ from __future__ import annotations -__all__ = [ +__all__: typing.List[str] = [ "Embed", "EmbedThumbnail", "EmbedVideo", diff --git a/hikari/models/emojis.py b/hikari/models/emojis.py index 40bbc1abb2..dd4b7f8e8b 100644 --- a/hikari/models/emojis.py +++ b/hikari/models/emojis.py @@ -20,7 +20,7 @@ from __future__ import annotations -__all__ = ["Emoji", "UnicodeEmoji", "CustomEmoji", "KnownCustomEmoji"] +__all__: typing.List[str] = ["Emoji", "UnicodeEmoji", "CustomEmoji", "KnownCustomEmoji"] import abc import typing diff --git a/hikari/models/files.py b/hikari/models/files.py index 91f4b0c442..7f10d6182e 100644 --- a/hikari/models/files.py +++ b/hikari/models/files.py @@ -52,7 +52,7 @@ from __future__ import annotations -__all__ = ["BaseStream", "ByteStream", "WebResourceStream", "FileStream"] +__all__: typing.List[str] = ["BaseStream", "ByteStream", "WebResourceStream", "FileStream"] import abc import asyncio diff --git a/hikari/models/gateway.py b/hikari/models/gateway.py index 1c6e0fc652..dd33ca72e9 100644 --- a/hikari/models/gateway.py +++ b/hikari/models/gateway.py @@ -20,7 +20,7 @@ from __future__ import annotations -__all__ = ["GatewayBot", "SessionStartLimit"] +__all__: typing.List[str] = ["GatewayBot", "SessionStartLimit"] import typing diff --git a/hikari/models/guilds.py b/hikari/models/guilds.py index 46e1300a71..d4823172e4 100644 --- a/hikari/models/guilds.py +++ b/hikari/models/guilds.py @@ -20,7 +20,7 @@ from __future__ import annotations -__all__ = [ +__all__: typing.List[str] = [ "Guild", "GuildWidget", "Role", diff --git a/hikari/models/intents.py b/hikari/models/intents.py index e1067c3fd5..9367fb615a 100644 --- a/hikari/models/intents.py +++ b/hikari/models/intents.py @@ -20,10 +20,13 @@ from __future__ import annotations -__all__ = ["Intent"] +__all__: typing.List[str] = ["Intent"] import enum +# noinspection PyUnresolvedReferences +import typing + @enum.unique class Intent(enum.IntFlag): diff --git a/hikari/models/invites.py b/hikari/models/invites.py index 8e0b467ff5..c5b64baa30 100644 --- a/hikari/models/invites.py +++ b/hikari/models/invites.py @@ -20,7 +20,7 @@ from __future__ import annotations -__all__ = ["TargetUserType", "VanityURL", "InviteGuild", "Invite", "InviteWithMetadata"] +__all__: typing.List[str] = ["TargetUserType", "VanityURL", "InviteGuild", "Invite", "InviteWithMetadata"] import enum import typing diff --git a/hikari/models/messages.py b/hikari/models/messages.py index 1675214321..a3c1549703 100644 --- a/hikari/models/messages.py +++ b/hikari/models/messages.py @@ -20,7 +20,7 @@ from __future__ import annotations -__all__ = [ +__all__: typing.List[str] = [ "MessageType", "MessageFlag", "MessageActivityType", diff --git a/hikari/models/permissions.py b/hikari/models/permissions.py index 05d23ad3dd..a32ff915a0 100644 --- a/hikari/models/permissions.py +++ b/hikari/models/permissions.py @@ -20,10 +20,13 @@ from __future__ import annotations -__all__ = ["Permission"] +__all__: typing.List[str] = ["Permission"] import enum +# noinspection PyUnresolvedReferences +import typing + @enum.unique class Permission(enum.IntFlag): diff --git a/hikari/models/presences.py b/hikari/models/presences.py index e2dbc72267..18f976e579 100644 --- a/hikari/models/presences.py +++ b/hikari/models/presences.py @@ -20,7 +20,7 @@ from __future__ import annotations -__all__ = [ +__all__: typing.List[str] = [ "OwnActivity", "ActivityAssets", "ActivityFlag", diff --git a/hikari/models/users.py b/hikari/models/users.py index 70128ea9c9..62a29546bd 100644 --- a/hikari/models/users.py +++ b/hikari/models/users.py @@ -20,7 +20,7 @@ from __future__ import annotations -__all__ = ["User", "OwnUser", "UserFlag", "PremiumType"] +__all__: typing.List[str] = ["User", "OwnUser", "UserFlag", "PremiumType"] import enum import typing diff --git a/hikari/models/voices.py b/hikari/models/voices.py index a635796c49..a3942023fe 100644 --- a/hikari/models/voices.py +++ b/hikari/models/voices.py @@ -20,7 +20,7 @@ from __future__ import annotations -__all__ = ["VoiceRegion", "VoiceState"] +__all__: typing.List[str] = ["VoiceRegion", "VoiceState"] import typing diff --git a/hikari/models/webhooks.py b/hikari/models/webhooks.py index 703d1d1960..90d2570627 100644 --- a/hikari/models/webhooks.py +++ b/hikari/models/webhooks.py @@ -20,7 +20,7 @@ from __future__ import annotations -__all__ = ["WebhookType", "Webhook"] +__all__: typing.List[str] = ["WebhookType", "Webhook"] import enum import typing @@ -30,6 +30,7 @@ from hikari.models import bases from hikari.utilities import cdn from hikari.utilities import snowflake +from hikari.utilities import undefined if typing.TYPE_CHECKING: from hikari.models import channels as channels_ @@ -93,14 +94,13 @@ class Webhook(bases.Entity, bases.Unique): async def execute( self, + text: typing.Union[undefined.Undefined, typing.Any] = undefined.Undefined(), *, - content: str = ..., - username: str = ..., - avatar_url: str = ..., - tts: bool = ..., - wait: bool = False, - files: typing.Sequence[files_.BaseStream] = ..., - embeds: typing.Sequence[embeds_.Embed] = ..., + username: typing.Union[undefined.Undefined, str] = undefined.Undefined(), + avatar_url: typing.Union[undefined.Undefined, str] = undefined.Undefined(), + tts: typing.Union[undefined.Undefined, bool] = undefined.Undefined(), + attachments: typing.Union[undefined.Undefined, typing.Sequence[files_.BaseStream]] = undefined.Undefined(), + embeds: typing.Union[undefined.Undefined, typing.Sequence[embeds_.Embed]] = undefined.Undefined(), mentions_everyone: bool = True, user_mentions: typing.Union[ typing.Collection[typing.Union[snowflake.Snowflake, int, str, users_.User]], bool @@ -108,27 +108,24 @@ async def execute( role_mentions: typing.Union[ typing.Collection[typing.Union[snowflake.Snowflake, int, str, guilds_.Role]], bool ] = True, - ) -> typing.Optional[messages_.Message]: + ) -> messages_.Message: """Execute the webhook to create a message. Parameters ---------- - content : str + text : str or hikari.utilities.undefined.Undefined If specified, the message content to send with the message. - username : str + username : str or hikari.utilities.undefined.Undefined If specified, the username to override the webhook's username for this request. - avatar_url : str + avatar_url : str or hikari.utilities.undefined.Undefined If specified, the url of an image to override the webhook's avatar with for this request. - tts : bool + tts : bool or hikari.utilities.undefined.Undefined If specified, whether the message will be sent as a TTS message. - wait : bool - If specified, whether this request should wait for the webhook - to be executed and return the resultant message object. - files : typing.Sequence[hikari.models.files.BaseStream] - If specified, a sequence of files to upload. - embeds : typing.Sequence[hikari.models.embeds.Embed] + attachments : typing.Sequence[hikari.models.files.BaseStream] or hikari.utilities.undefined.Undefined + If specified, a sequence of attachments to upload. + embeds : typing.Sequence[hikari.models.embeds.Embed] or hikari.utilities.undefined.Undefined If specified, a sequence of between `1` to `10` embed objects (inclusive) to send with the embed. mentions_everyone : bool @@ -145,8 +142,8 @@ async def execute( Returns ------- - hikari.models.messages.Message or None - The created message object, if `wait` is `True`, else `None`. + hikari.models.messages.Message + The created message object. Raises ------ @@ -170,68 +167,24 @@ async def execute( return await self._app.rest.execute_webhook( webhook=self.id, - webhook_token=self.token, - content=content, + token=self.token, + text=text, username=username, avatar_url=avatar_url, tts=tts, - wait=wait, - files=files, + attachments=attachments, embeds=embeds, mentions_everyone=mentions_everyone, user_mentions=user_mentions, role_mentions=role_mentions, ) - async def safe_execute( - self, - *, - content: str = ..., - username: str = ..., - avatar_url: str = ..., - tts: bool = ..., - wait: bool = False, - files: typing.Sequence[files_.BaseStream] = ..., - embeds: typing.Sequence[embeds_.Embed] = ..., - mentions_everyone: bool = True, - user_mentions: typing.Union[ - typing.Collection[typing.Union[snowflake.Snowflake, int, str, users_.User]], bool - ] = False, - role_mentions: typing.Union[ - typing.Collection[typing.Union[snowflake.Snowflake, int, str, guilds_.Role]], bool - ] = False, - ) -> typing.Optional[messages_.Message]: - """Execute the webhook to create a message with mention safety. - - This endpoint has the same signature as - `Webhook.execute` with the only difference being - that `mentions_everyone`, `user_mentions` and `role_mentions` default to - `False`. - """ - if not self.token: - raise ValueError("Cannot execute a webhook with a unknown token (set to `None`).") - - return await self._app.rest.safe_webhook_execute( - webhook=self.id, - webhook_token=self.token, - content=content, - username=username, - avatar_url=avatar_url, - tts=tts, - wait=wait, - files=files, - embeds=embeds, - mentions_everyone=mentions_everyone, - user_mentions=user_mentions, - role_mentions=role_mentions, - ) - - async def delete(self, *, use_token: typing.Optional[bool] = None,) -> None: + async def delete(self, *, use_token: typing.Union[undefined.Undefined, bool] = undefined.Undefined()) -> None: """Delete this webhook. Parameters ---------- - use_token : bool or None + use_token : bool or hikari.utilities.undefined.Undefined If set to `True` then the webhook's token will be used for this request; if set to `False` then bot authorization will be used; if not specified then the webhook's token will be used for the @@ -247,40 +200,44 @@ async def delete(self, *, use_token: typing.Optional[bool] = None,) -> None: ValueError If `use_token` is passed as `True` when `Webhook.token` is `None`. """ - if use_token and not self.token: - raise ValueError("This webhook's token is unknown.") + if use_token and self.token is None: + raise ValueError("This webhook's token is unknown, so cannot be used.") + + token: typing.Union[undefined.Undefined, str] - if use_token is None and self.token: - use_token = True + if use_token: + token = typing.cast(str, self.token) + else: + token = undefined.Undefined() - await self._app.rest.delete_webhook(webhook=self.id, webhook_token=self.token if use_token else ...) + await self._app.rest.delete_webhook(self.id, token=token) async def edit( self, *, - name: str = ..., - avatar: typing.Optional[files_.BaseStream] = ..., - channel: typing.Union[snowflake.Snowflake, int, str, channels_.GuildChannel] = ..., - reason: str = ..., - use_token: typing.Optional[bool] = None, + name: typing.Union[undefined.Undefined, str] = undefined.Undefined(), + avatar: typing.Union[undefined.Undefined, None, files_.BaseStream] = undefined.Undefined(), + channel: typing.Union[undefined.Undefined, bases.UniqueObject, channels_.GuildChannel] = undefined.Undefined(), + reason: typing.Union[undefined.Undefined, str] = undefined.Undefined(), + use_token: typing.Union[undefined.Undefined, bool] = undefined.Undefined(), ) -> Webhook: """Edit this webhook. Parameters ---------- - name : str + name : str or hikari.utilities.undefined.Undefined If specified, the new name string. - avatar : hikari.models.files.BaseStream or None + avatar : hikari.models.files.BaseStream or None or hikari.utilities.undefined.Undefined If specified, the new avatar image. If `None`, then it is removed. - channel : hikari.models.channels.GuildChannel or hikari.utilities.snowflake.Snowflake or int + channel : hikari.models.channels.GuildChannel or hikari.models.bases.UniqueObject or hikari.utilities.undefined.Undefined If specified, the object or ID of the new channel the given webhook should be moved to. - reason : str + reason : str or hikari.utilities.undefined.Undefined If specified, the audit log reason explaining why the operation was performed. This field will be used when using the webhook's token rather than bot authorization. - use_token : bool or None + use_token : bool or hikari.utilities.undefined.Undefined If set to `True` then the webhook's token will be used for this request; if set to `False` then bot authorization will be used; if not specified then the webhook's token will be used for the @@ -306,19 +263,18 @@ async def edit( ValueError If `use_token` is passed as `True` when `Webhook.token` is `None`. """ - if use_token and not self.token: - raise ValueError("This webhook's token is unknown.") + if use_token and self.token is None: + raise ValueError("This webhook's token is unknown, so cannot be used.") - if use_token is None and self.token: - use_token = True + token: typing.Union[undefined.Undefined, str] - return await self._app.rest.update_webhook( - webhook=self.id, - webhook_token=self.token if use_token else ..., - name=name, - avatar=avatar, - channel=channel, - reason=reason, + if use_token: + token = typing.cast(str, self.token) + else: + token = undefined.Undefined() + + return await self._app.rest.edit_webhook( + self.id, token=token, name=name, avatar=avatar, channel=channel, reason=reason, ) async def fetch_channel(self) -> channels_.PartialChannel: @@ -336,30 +292,16 @@ async def fetch_channel(self) -> channels_.PartialChannel: hikari.errors.NotFound If the channel this message was created in does not exist. """ - return await self._app.rest.fetch_channel(channel=self.channel_id) - - async def fetch_guild(self) -> guilds_.Guild: - """Fetch the guild this webhook belongs to. - - Returns - ------- - hikari.models.guilds.Guild - The object of the channel this message belongs to. + return await self._app.rest.fetch_channel(self.channel_id) - Raises - ------ - hikari.errors.Forbidden - If you don't have access to the guild this webhook belongs to or it - doesn't exist. - """ - return await self._app.rest.fetch_guild(guild=self.guild_id) - - async def fetch_self(self, *, use_token: typing.Optional[bool] = None) -> Webhook: + async def fetch_self( + self, *, use_token: typing.Union[undefined.Undefined, bool] = undefined.Undefined() + ) -> Webhook: """Fetch this webhook. Parameters ---------- - use_token : bool or None + use_token : bool or hikari.utilities.undefined.Undefined If set to `True` then the webhook's token will be used for this request; if set to `False` then bot authorization will be used; if not specified then the webhook's token will be used for the @@ -386,12 +328,16 @@ async def fetch_self(self, *, use_token: typing.Optional[bool] = None) -> Webhoo If `use_token` is passed as `True` when `Webhook.token` is `None`. """ if use_token and not self.token: - raise ValueError("This webhook's token is unknown.") + raise ValueError("This webhook's token is unknown, so cannot be used.") + + token: typing.Union[undefined.Undefined, str] - if use_token is None and self.token: - use_token = True + if use_token: + token = typing.cast(str, self.token) + else: + token = undefined.Undefined() - return await self._app.rest.fetch_webhook(webhook=self.id, webhook_token=self.token if use_token else ...) + return await self._app.rest.fetch_webhook(self.id, token=token) @property def avatar_url(self) -> str: @@ -405,7 +351,10 @@ def default_avatar_index(self) -> int: @property def default_avatar_url(self) -> str: - """URL for this webhook's default avatar.""" + """URL for this webhook's default avatar. + + This is used if no avatar is set. + """ return cdn.generate_cdn_url("embed", "avatars", str(self.default_avatar_index), format_="png", size=None) def format_avatar_url(self, format_: str = "png", size: int = 4096) -> str: @@ -430,7 +379,7 @@ def format_avatar_url(self, format_: str = "png", size: int = 4096) -> str: Raises ------ ValueError - If `size` is not a power of two or not between 16 and 4096. + If `size` is not a power of two between 16 and 4096 (inclusive). """ if not self.avatar_hash: return self.default_avatar_url diff --git a/hikari/net/__init__.py b/hikari/net/__init__.py index a0304ad728..13147fe76a 100644 --- a/hikari/net/__init__.py +++ b/hikari/net/__init__.py @@ -20,4 +20,7 @@ from __future__ import annotations -__all__ = [] +__all__: typing.List[str] = [] + +# noinspection PyUnresolvedReferences +import typing diff --git a/hikari/net/buckets.py b/hikari/net/buckets.py index 7e708edd7b..c04d5cc2a7 100644 --- a/hikari/net/buckets.py +++ b/hikari/net/buckets.py @@ -204,7 +204,7 @@ from __future__ import annotations -__all__ = ["UNKNOWN_HASH", "RESTBucket", "RESTBucketManager"] +__all__: typing.List[str] = ["UNKNOWN_HASH", "RESTBucket", "RESTBucketManager"] import asyncio import datetime @@ -257,7 +257,7 @@ def is_unknown(self) -> bool: """Return `True` if the bucket represents an `UNKNOWN` bucket.""" return self.name.startswith(UNKNOWN_HASH) - def acquire(self) -> aio.Future[None]: + def acquire(self) -> asyncio.Future[None]: """Acquire time on this rate limiter. !!! note @@ -337,7 +337,7 @@ class RESTBucketManager: closed_event: typing.Final[asyncio.Event] """An internal event that is set when the object is shut down.""" - gc_task: typing.Optional[aio.Task[None]] + gc_task: typing.Optional[asyncio.Task[None]] """The internal garbage collector task.""" logger: typing.Final[logging.Logger] @@ -485,7 +485,7 @@ def do_gc_pass(self, expire_after: float) -> None: self.logger.debug("purged %s stale buckets, %s remain in survival, %s active", dead, survival, active) - def acquire(self, compiled_route: routes.CompiledRoute) -> aio.Future[None]: + def acquire(self, compiled_route: routes.CompiledRoute) -> asyncio.Future[None]: """Acquire a bucket for the given _route. Parameters diff --git a/hikari/net/gateway.py b/hikari/net/gateway.py index 7efe23b41a..29219a2b18 100644 --- a/hikari/net/gateway.py +++ b/hikari/net/gateway.py @@ -20,7 +20,7 @@ from __future__ import annotations -__all__ = ["Gateway"] +__all__: typing.List[str] = ["Gateway"] import asyncio import enum @@ -53,7 +53,6 @@ from hikari.models import guilds from hikari.models import intents as intents_ from hikari.utilities import snowflake - from hikari.utilities import aio class Gateway(http_client.HTTPClient, component.IComponent): @@ -237,7 +236,7 @@ def is_alive(self) -> bool: """Return whether the shard is alive.""" return not math.isnan(self.connected_at) - async def start(self) -> aio.Task[None]: + async def start(self) -> asyncio.Task[None]: """Start the shard, wait for it to become ready. Returns diff --git a/hikari/net/http_client.py b/hikari/net/http_client.py index 7df312c47b..54b94d259e 100644 --- a/hikari/net/http_client.py +++ b/hikari/net/http_client.py @@ -19,10 +19,9 @@ """Base functionality for any HTTP-based network component.""" from __future__ import annotations -__all__ = ["HTTPClient"] +__all__: typing.List[str] = ["HTTPClient"] import abc -import contextlib import json import logging import ssl @@ -30,10 +29,12 @@ import typing import aiohttp.typedefs -import multidict from hikari.net import tracing +if typing.TYPE_CHECKING: + from hikari.utilities import data_binding + class HTTPClient(abc.ABC): # pylint:disable=too-many-instance-attributes """An HTTP client base for Hikari. @@ -168,14 +169,14 @@ def __init__( ) -> None: self.logger = logger - self.__client_session = None + self.__client_session: typing.Optional[aiohttp.ClientSession] = None self._allow_redirects = allow_redirects self._connector = connector self._debug = debug self._proxy_auth = proxy_auth self._proxy_headers = proxy_headers self._proxy_url = proxy_url - self._ssl_context: ssl.SSLContext = ssl_context + self._ssl_context = ssl_context self._request_timeout = timeout self._trust_env = trust_env self._tracers = [(tracing.DebugTracer(self.logger) if debug else tracing.CFRayTracer(self.logger))] @@ -191,7 +192,7 @@ async def __aexit__( async def close(self) -> None: """Close the client safely.""" - with contextlib.suppress(Exception): + if self.__client_session is not None: await self.__client_session.close() self.logger.debug("closed client session object %r", self.__client_session) self.__client_session = None @@ -220,9 +221,11 @@ async def _perform_request( *, method: str, url: str, - headers: aiohttp.typedefs.LooseHeaders, - body: typing.Union[aiohttp.FormData, dict, list, None], - query: typing.Union[typing.Dict[str, str], multidict.MultiDict[str, str]], + headers: data_binding.Headers, + body: typing.Union[ + data_binding.JSONObjectBuilder, aiohttp.FormData, data_binding.JSONObject, data_binding.JSONArray, None + ], + query: typing.Union[data_binding.Query, data_binding.StringMapBuilder, None], ) -> aiohttp.ClientResponse: """Make an HTTP request and return the response. @@ -247,14 +250,13 @@ async def _perform_request( aiohttp.ClientResponse The HTTP response. """ + kwargs: typing.Dict[str, typing.Any] = {} + if isinstance(body, (dict, list)): - kwargs = {"json": body} + kwargs["json"] = body elif isinstance(body, aiohttp.FormData): - kwargs = {"data": body} - - else: - kwargs = {} + kwargs["data"] = body trace_request_ctx = types.SimpleNamespace() trace_request_ctx.request_body = body diff --git a/hikari/net/http_settings.py b/hikari/net/http_settings.py index f4f62f8629..d0faf17992 100644 --- a/hikari/net/http_settings.py +++ b/hikari/net/http_settings.py @@ -20,7 +20,7 @@ from __future__ import annotations -__all__ = ["HTTPSettings"] +__all__: typing.List[str] = ["HTTPSettings"] import typing diff --git a/hikari/net/iterators.py b/hikari/net/iterators.py index 8f84bb8702..4cc6d49475 100644 --- a/hikari/net/iterators.py +++ b/hikari/net/iterators.py @@ -19,7 +19,7 @@ """Lazy iterators for data that requires repeated API calls to retrieve.""" from __future__ import annotations -__all__ = ["LazyIterator"] +__all__: typing.List[str] = ["LazyIterator"] import abc import typing diff --git a/hikari/net/rate_limits.py b/hikari/net/rate_limits.py index 138e3d76bf..976ce0cb1a 100644 --- a/hikari/net/rate_limits.py +++ b/hikari/net/rate_limits.py @@ -23,7 +23,7 @@ from __future__ import annotations -__all__ = [ +__all__: typing.List[str] = [ "BaseRateLimiter", "BurstRateLimiter", "ManualRateLimiter", @@ -38,7 +38,10 @@ import time import typing -from hikari.utilities import aio + +if typing.TYPE_CHECKING: + import types + UNKNOWN_HASH: typing.Final[str] = "UNKNOWN" """The hash used for an unknown bucket that has not yet been resolved.""" @@ -56,7 +59,7 @@ class BaseRateLimiter(abc.ABC): __slots__ = () @abc.abstractmethod - def acquire(self) -> aio.Future[None]: + def acquire(self) -> asyncio.Future[None]: """Acquire permission to perform a task that needs to have rate limit management enforced. Returns @@ -70,10 +73,10 @@ def acquire(self) -> aio.Future[None]: def close(self) -> None: """Close the rate limiter, cancelling any internal tasks that are executing.""" - def __enter__(self): + def __enter__(self) -> BaseRateLimiter: return self - def __exit__(self, exc_type, exc_val, exc_tb): + def __exit__(self, exc_type: typing.Type[Exception], exc_val: Exception, exc_tb: types.TracebackType) -> None: self.close() @@ -89,10 +92,10 @@ class BurstRateLimiter(BaseRateLimiter, abc.ABC): name: typing.Final[str] """The name of the rate limiter.""" - throttle_task: typing.Optional[aio.Task[None]] + throttle_task: typing.Optional[asyncio.Task[typing.Any]] """The throttling task, or `None` if it isn't running.""" - queue: typing.Final[typing.List[aio.Future[None]]] + queue: typing.Final[typing.List[asyncio.Future[typing.Any]]] """The queue of any futures under a rate limit.""" logger: typing.Final[logging.Logger] @@ -106,7 +109,7 @@ def __init__(self, name: str) -> None: self._closed = False @abc.abstractmethod - def acquire(self) -> aio.Future[None]: + def acquire(self) -> asyncio.Future[typing.Any]: """Acquire time on this rate limiter. The implementation should define this. @@ -174,7 +177,7 @@ class ManualRateLimiter(BurstRateLimiter): def __init__(self) -> None: super().__init__("global") - def acquire(self) -> aio.Future[None]: + def acquire(self) -> asyncio.Future[typing.Any]: """Acquire time on this rate limiter. Returns @@ -303,7 +306,7 @@ def __init__(self, name: str, period: float, limit: int) -> None: self.limit = limit self.period = period - def acquire(self) -> aio.Future[None]: + def acquire(self) -> asyncio.Future[typing.Any]: """Acquire time on this rate limiter. Returns diff --git a/hikari/net/rest.py b/hikari/net/rest.py index 01d6a84a02..957ca1798d 100644 --- a/hikari/net/rest.py +++ b/hikari/net/rest.py @@ -20,7 +20,7 @@ from __future__ import annotations -__all__ = ["REST"] +__all__: typing.List[str] = ["REST"] import asyncio import datetime @@ -1458,7 +1458,7 @@ async def edit_webhook( *, token: typing.Union[undefined.Undefined, str] = undefined.Undefined(), name: typing.Union[undefined.Undefined, str] = undefined.Undefined(), - avatar: typing.Union[undefined.Undefined, files.BaseStream] = undefined.Undefined(), + avatar: typing.Union[None, undefined.Undefined, files.BaseStream] = undefined.Undefined(), channel: typing.Union[undefined.Undefined, channels.TextChannel, bases.UniqueObject] = undefined.Undefined(), reason: typing.Union[undefined.Undefined, str] = undefined.Undefined(), ) -> webhooks.Webhook: @@ -1500,7 +1500,6 @@ async def execute_webhook( embeds: typing.Union[undefined.Undefined, typing.Sequence[embeds_.Embed]] = undefined.Undefined(), attachments: typing.Union[undefined.Undefined, typing.Sequence[files.BaseStream]] = undefined.Undefined(), tts: typing.Union[undefined.Undefined, bool] = undefined.Undefined(), - wait: typing.Union[undefined.Undefined, bool] = undefined.Undefined(), mentions_everyone: bool = True, user_mentions: typing.Union[typing.Collection[typing.Union[users.User, bases.UniqueObject]], bool] = True, role_mentions: typing.Union[typing.Collection[typing.Union[bases.UniqueObject, guilds.Role]], bool] = True, @@ -1527,7 +1526,7 @@ async def execute_webhook( body.put("username", username) body.put("avatar_url", avatar_url) body.put("tts", tts) - body.put("wait", wait) + body.put("wait", True) response = await self._request( route, body=self._build_message_creation_form(body, attachments) if attachments else body, no_auth=no_auth, diff --git a/hikari/net/rest_utils.py b/hikari/net/rest_utils.py index d7dac608f1..886979db85 100644 --- a/hikari/net/rest_utils.py +++ b/hikari/net/rest_utils.py @@ -23,7 +23,7 @@ from __future__ import annotations # Do not document anything in here. -__all__ = [] +__all__: typing.List[str] = [] import asyncio import contextlib diff --git a/hikari/net/routes.py b/hikari/net/routes.py index 49a5922b94..982399411d 100644 --- a/hikari/net/routes.py +++ b/hikari/net/routes.py @@ -20,7 +20,7 @@ from __future__ import annotations -__all__ = ["CompiledRoute", "Route"] +__all__: typing.List[str] = ["CompiledRoute", "Route"] import re import typing diff --git a/hikari/net/tracing.py b/hikari/net/tracing.py index cf7c04cdce..918c7d8477 100644 --- a/hikari/net/tracing.py +++ b/hikari/net/tracing.py @@ -19,7 +19,7 @@ """Provides logging support for HTTP requests internally.""" from __future__ import annotations -__all__ = ["BaseTracer", "CFRayTracer", "DebugTracer"] +__all__: typing.List[str] = ["BaseTracer", "CFRayTracer", "DebugTracer"] import functools import io @@ -27,6 +27,9 @@ import time import uuid +# noinspection PyUnresolvedReferences +import typing + import aiohttp.abc @@ -37,7 +40,7 @@ def __init__(self, logger: logging.Logger) -> None: self.logger = logger @functools.cached_property - def trace_config(self): + def trace_config(self) -> aiohttp.TraceConfig: """Generate a trace config for aiohttp.""" tc = aiohttp.TraceConfig() @@ -55,6 +58,7 @@ class CFRayTracer(BaseTracer): Cloudflare rays in the response. """ + @typing.no_type_check async def on_request_start(self, _, ctx, params): """Log an outbound request.""" ctx.identifier = f"request_id:{uuid.uuid4()}" @@ -69,6 +73,7 @@ async def on_request_start(self, _, ctx, params): ctx.identifier, ) + @typing.no_type_check async def on_request_end(self, _, ctx, params): """Log an inbound response.""" latency = round((time.perf_counter() - ctx.start_time) * 1_000, 1) @@ -86,9 +91,12 @@ async def on_request_end(self, _, ctx, params): ) -class _ByteStreamWriter(io.BytesIO, aiohttp.abc.AbstractStreamWriter): - async def write(self, data) -> None: - io.BytesIO.write(self, data) +class _ByteStreamWriter(aiohttp.abc.AbstractStreamWriter): + def __init__(self) -> None: + self.bio = io.BytesIO() + + async def write(self, data: typing.Union[bytes, bytearray]) -> None: + self.bio.write(data) write_eof = NotImplemented drain = NotImplemented @@ -113,7 +121,7 @@ class DebugTracer(BaseTracer): """ @staticmethod - async def _format_body(body): + async def _format_body(body: typing.Any) -> str: if isinstance(body, aiohttp.FormData): # We have to either copy the internal multipart writer, or we have # to make a dummy second instance and read from that. I am putting @@ -124,9 +132,10 @@ async def _format_body(body): setattr(copy_of_data, "_fields", getattr(copy_of_data, "_fields")) byte_writer = _ByteStreamWriter() await copy_of_data().write(byte_writer) - return repr(byte_writer.read()) + return repr(byte_writer.bio.read()) return repr(body) + @typing.no_type_check async def on_request_start(self, _, ctx, params): """Log an outbound request.""" ctx.identifier = f"request_id:{uuid.uuid4()}" @@ -147,6 +156,7 @@ async def on_request_start(self, _, ctx, params): body, ) + @typing.no_type_check async def on_request_end(self, _, ctx, params): """Log an inbound response.""" latency = round((time.perf_counter() - ctx.start_time) * 1_000, 2) @@ -162,35 +172,43 @@ async def on_request_end(self, _, ctx, params): await self._format_body(await response.read()) if "content-type" in response.headers else "", ) + @typing.no_type_check async def on_request_exception(self, _, ctx, params): """Log an error while making a request.""" self.logger.debug("encountered exception [%s]", ctx.identifier, exc_info=params.exception) + @typing.no_type_check async def on_connection_queued_start(self, _, ctx, __): """Log when we have to wait for a new connection in the pool.""" self.logger.debug("is waiting for a connection [%s]", ctx.identifier) + @typing.no_type_check async def on_connection_reuseconn(self, _, ctx, __): """Log when we re-use an existing connection in the pool.""" self.logger.debug("has acquired an existing connection [%s]", ctx.identifier) + @typing.no_type_check async def on_connection_create_end(self, _, ctx, __): """Log when we create a new connection in the pool.""" self.logger.debug("has created a new connection [%s]", ctx.identifier) + @typing.no_type_check async def on_dns_cache_hit(self, _, ctx, params): """Log when we reuse the DNS cache and do not have to look up an IP.""" self.logger.debug("has retrieved the IP of %s from the DNS cache [%s]", params.host, ctx.identifier) + @typing.no_type_check async def on_dns_cache_miss(self, _, ctx, params): """Log when we have to query a DNS server for an IP address.""" self.logger.debug("will perform DNS lookup of new host %s [%s]", params.host, ctx.identifier) # noinspection PyMethodMayBeStatic + @typing.no_type_check async def on_dns_resolvehost_start(self, _, ctx, __): """Store the time the DNS lookup started at.""" ctx.dns_start_time = time.perf_counter() + @typing.no_type_check async def on_dns_resolvehost_end(self, _, ctx, params): """Log how long a DNS lookup of an IP took to perform.""" latency = round((time.perf_counter() - ctx.dns_start_time) * 1_000, 2) diff --git a/hikari/net/user_agents.py b/hikari/net/user_agents.py index 21d331212d..c25e5121ff 100644 --- a/hikari/net/user_agents.py +++ b/hikari/net/user_agents.py @@ -27,14 +27,14 @@ from __future__ import annotations -__all__ = ["UserAgent"] +__all__: typing.List[str] = ["UserAgent"] import typing from hikari.utilities import klass -class UserAgent(metaclass=klass.SingletonMeta): +class UserAgent(klass.Singleton): """Platform version info. !!! note @@ -73,7 +73,7 @@ class UserAgent(metaclass=klass.SingletonMeta): `"DiscordBot (https://gitlab.com/nekokatt/hikari; 1.0.1; Nekokatt) CPython 3.8.2 GCC 9.2.0 Linux"` """ - def __init__(self): + def __init__(self) -> None: from hikari._about import __author__, __url__, __version__ from platform import python_implementation, python_version, python_branch, python_compiler, platform @@ -85,16 +85,15 @@ def __init__(self): self.user_agent = f"DiscordBot ({__url__}; {__version__}; {__author__}) {python_version()} {self.system_type}" @staticmethod - def _join_strip(*args): + def _join_strip(*args: str) -> str: return " ".join((arg.strip() for arg in args if arg.strip())) - # Inore docstring not starting in an imperativge mood @property - def websocket_triplet(self) -> typing.Dict[str, str]: # noqa: D401 - """A dict representing device and library info. + def websocket_triplet(self) -> typing.Dict[str, str]: + """Generate a dict representing device and library info. This is the object to send to Discord representing device info when - IDENTIFYing with the gateway in the format `typing.Dict`[`str`, `str`] + IDENTIFYing with the gateway in the format `typing.Dict[str, str]` """ return { "$os": self.system_type, diff --git a/hikari/utilities/__init__.py b/hikari/utilities/__init__.py index 2cf29c2cb2..9836276fdb 100644 --- a/hikari/utilities/__init__.py +++ b/hikari/utilities/__init__.py @@ -20,4 +20,7 @@ from __future__ import annotations -__all__ = [] +__all__: typing.List[str] = [] + +# noinspection PyUnresolvedReferences +import typing diff --git a/hikari/utilities/aio.py b/hikari/utilities/aio.py index 89319f1d76..1aed3470dd 100644 --- a/hikari/utilities/aio.py +++ b/hikari/utilities/aio.py @@ -20,37 +20,35 @@ from __future__ import annotations -__all__ = ["completed_future", "is_async_iterator", "is_async_iterable", "Future", "Task"] +__all__: typing.List[str] = ["completed_future", "is_async_iterator", "is_async_iterable"] import asyncio import inspect import typing -if typing.TYPE_CHECKING: - import contextvars - import types +T_inv = typing.TypeVar("T_inv") -T_contra = typing.TypeVar("T_contra", contravariant=True) - -def completed_future(result: typing.Optional[T_contra] = None, /) -> Future[typing.Optional[T_contra]]: +def completed_future(result: typing.Optional[T_inv] = None, /) -> asyncio.Future[typing.Optional[T_inv]]: """Create a future on the current running loop that is completed, then return it. Parameters ---------- - result : T_contra or None + result : T_inv or None The value to set for the result of the future. - `T_contra` is a generic type placeholder for the type that + `T_inv` is a generic type placeholder for the type that the future will have set as the result. Returns ------- - Future[T_contra or None] + asyncio.Future[T_inv or None] The completed future. """ future = asyncio.get_event_loop().create_future() future.set_result(result) - return future + # MyPy pretends this type hint is valid when it isn't. Probably should be + # in the standard lib but whatever. + return typing.cast("asyncio.Future[typing.Optional[T_inv]]", future) # On Python3.8.2, there appears to be a bug with the typing module: @@ -79,101 +77,3 @@ def is_async_iterable(obj: typing.Any) -> bool: """Determine if the object is an async iterable or not.""" attr = getattr(obj, "__aiter__", None) return inspect.isfunction(attr) or inspect.ismethod(attr) - - -@typing.runtime_checkable -class Future(typing.Protocol[T_contra]): - """Typed protocol representation of an `asyncio.Future`. - - You should consult the documentation for `asyncio.Future` for usage. - """ - - def result(self) -> T_contra: - """See `asyncio.Future.result`.""" - - def set_result(self, result: T_contra, /) -> None: - """See `asyncio.Future.set_result`.""" - - def set_exception(self, exception: Exception, /) -> None: - """See `asyncio.Future.set_exception`.""" - - def done(self) -> bool: - """See `asyncio.Future.done`.""" - - def cancelled(self) -> bool: - """See `asyncio.Future.cancelled`.""" - - def add_done_callback( - self, callback: typing.Callable[[Future[T_contra]], None], /, *, context: typing.Optional[contextvars.Context], - ) -> None: - """See `asyncio.Future.add_done_callback`.""" - - def remove_done_callback(self, callback: typing.Callable[[Future[T_contra]], None], /) -> None: - """See `asyncio.Future.remove_done_callback`.""" - - def cancel(self) -> bool: - """See `asyncio.Future.cancel`.""" - - def exception(self) -> typing.Optional[Exception]: - """See `asyncio.Future.exception`.""" - - def get_loop(self) -> asyncio.AbstractEventLoop: - """See `asyncio.Future.get_loop`.""" - - def __await__(self) -> typing.Generator[T_contra, None, typing.Any]: - ... - - -@typing.runtime_checkable -class Task(typing.Protocol[T_contra]): - """Typed protocol representation of an `asyncio.Task`. - - You should consult the documentation for `asyncio.Task` for usage. - """ - - def result(self) -> T_contra: - """See`asyncio.Future.result`.""" - - def set_result(self, result: T_contra, /) -> None: - """See `asyncio.Future.set_result`.""" - - def set_exception(self, exception: Exception, /) -> None: - """See `asyncio.Future.set_exception`.""" - - def done(self) -> bool: - """See `asyncio.Future.done`.""" - - def cancelled(self) -> bool: - """See `asyncio.Future.cancelled`.""" - - def add_done_callback( - self, callback: typing.Callable[[Future[T_contra]], None], /, *, context: typing.Optional[contextvars.Context], - ) -> None: - """See `asyncio.Future.add_done_callback`.""" - - def remove_done_callback(self, callback: typing.Callable[[Future[T_contra]], None], /) -> None: - """See `asyncio.Future.remove_done_callback`.""" - - def cancel(self) -> bool: - """See `asyncio.Future.cancel`.""" - - def exception(self) -> typing.Optional[Exception]: - """See `asyncio.Future.exception`.""" - - def get_loop(self) -> asyncio.AbstractEventLoop: - """See `asyncio.Future.get_loop`.""" - - def get_stack(self, *, limit: typing.Optional[int] = None) -> typing.Sequence[types.FrameType]: - """See `asyncio.Task.get_stack`.""" - - def print_stack(self, *, limit: typing.Optional[int] = None, file: typing.Optional[typing.IO] = None) -> None: - """See `asyncio.Task.print_stack`.""" - - def get_name(self) -> str: - """See `asyncio.Task.get_name`.""" - - def set_name(self, value: str, /) -> None: - """See `asyncio.Task.set_name`.""" - - def __await__(self) -> typing.Generator[T_contra, None, typing.Any]: - ... diff --git a/hikari/utilities/cdn.py b/hikari/utilities/cdn.py index 8b84b982bd..89834303b5 100644 --- a/hikari/utilities/cdn.py +++ b/hikari/utilities/cdn.py @@ -20,9 +20,7 @@ from __future__ import annotations -__all__ = [ - "generate_cdn_url", -] +__all__: typing.List[str] = ["generate_cdn_url"] import typing import urllib.parse diff --git a/hikari/utilities/data_binding.py b/hikari/utilities/data_binding.py index d6e873f187..4c71529854 100644 --- a/hikari/utilities/data_binding.py +++ b/hikari/utilities/data_binding.py @@ -19,7 +19,7 @@ """Data binding utilities.""" from __future__ import annotations -__all__ = [ +__all__: typing.List[str] = [ "Headers", "Query", "JSONObject", @@ -43,12 +43,11 @@ from hikari.utilities import undefined T = typing.TypeVar("T", covariant=True) -CollectionT = typing.TypeVar("CollectionT", bound=typing.Collection, contravariant=True) Headers = typing.Mapping[str, str] """Type hint for HTTP headers.""" -Query = typing.Union[typing.Dict[str, str], multidict.MultiDict[str, str]] +Query = typing.Union[typing.Dict[str, str], multidict.MultiDict[str]] """Type hint for HTTP query string.""" URLEncodedForm = aiohttp.FormData @@ -86,7 +85,7 @@ def load_json(_: str) -> typing.Union[JSONArray, JSONObject]: """Convert a JSON string to a Python type.""" -class StringMapBuilder(multidict.MultiDict[str, str]): +class StringMapBuilder(multidict.MultiDict[str]): """Helper class used to quickly build query strings or header maps. This will consume any items that are not @@ -168,7 +167,7 @@ class JSONObjectBuilder(typing.Dict[str, JSONAny]): __slots__ = () - def __init__(self): + def __init__(self) -> None: super().__init__() def put( @@ -246,7 +245,7 @@ def put_snowflake(self, key: str, value: typing.Union[undefined.Undefined, typin self[key] = str(int(value)) def put_snowflake_array( - self, key: str, values: typing.Union[undefined.Undefined, typing.Iterable[typing.SupportsInt, int]], / + self, key: str, values: typing.Union[undefined.Undefined, typing.Iterable[typing.SupportsInt]], / ) -> None: """Put an array of snowflakes with the given key into this builder. @@ -258,7 +257,7 @@ def put_snowflake_array( ---------- key : str The key to give the element. - values : typing.Iterable[typing.SupportsInt or int] or hikari.utilities.undefined.Undefined + values : typing.Iterable[typing.SupportsInt] or hikari.utilities.undefined.Undefined The JSON snowflakes to put. This may alternatively be undefined. In the latter case, nothing is performed. """ @@ -266,9 +265,9 @@ def put_snowflake_array( self[key] = [str(int(value)) for value in values] -def cast_json_array( - array: JSONArray, cast: typing.Callable[[JSONAny], T], collection_type: typing.Type[CollectionT] = list, -) -> CollectionT: +# There isn't a nice way to type this correctly :( +@typing.no_type_check +def cast_json_array(array, cast, collection_type=list): """Cast a JSON array to a given generic collection type. This will perform casts on each internal item individually. @@ -289,15 +288,13 @@ def cast_json_array( The cast to apply to each item in the array. This should consume any valid JSON-decoded type and return the type corresponding to the generic type of the provided collection. - collection_type : typing.Type[CollectionT] - The container type to store the cast items within. - `CollectionT` should be a concrete implementation that is - a subtype of `typing.Collection`, such as `list`, `set`, `frozenSet`, - `tuple`, etc. If unspecified, this defaults to `list`. + collection_type : typing.Callable[[typing.Iterable[T]], typing.Collection[T]] + The container type to store the cast items within. This should be called + with options such as `list`, `set`, `frozenset`, `tuple`, etc. Returns ------- - CollectionT + typing.Collection[T] The generated collection. Example diff --git a/hikari/utilities/date.py b/hikari/utilities/date.py index 276bb00f85..840d4e3d50 100644 --- a/hikari/utilities/date.py +++ b/hikari/utilities/date.py @@ -20,7 +20,7 @@ from __future__ import annotations -__all__ = [ +__all__: typing.List[str] = [ "DISCORD_EPOCH", "rfc7231_datetime_string_to_datetime", "datetime_to_discord_epoch", @@ -53,9 +53,9 @@ * [Discord API documentation - Snowflakes](https://discord.com/developers/docs/reference#snowflakes) """ -ISO_8601_DATE_PART: typing.Final[typing.Pattern] = re.compile(r"^(\d{4})-(\d{2})-(\d{2})") -ISO_8601_TIME_PART: typing.Final[typing.Pattern] = re.compile(r"T(\d{2}):(\d{2}):(\d{2})(?:\.(\d{1,6}))?", re.I) -ISO_8601_TZ_PART: typing.Final[typing.Pattern] = re.compile(r"([+-])(\d{2}):(\d{2})$") +ISO_8601_DATE_PART: typing.Final[typing.Pattern[str]] = re.compile(r"^(\d{4})-(\d{2})-(\d{2})") +ISO_8601_TIME_PART: typing.Final[typing.Pattern[str]] = re.compile(r"T(\d{2}):(\d{2}):(\d{2})(?:\.(\d{1,6}))?", re.I) +ISO_8601_TZ_PART: typing.Final[typing.Pattern[str]] = re.compile(r"([+-])(\d{2}):(\d{2})$") def rfc7231_datetime_string_to_datetime(date_str: str, /) -> datetime.datetime: diff --git a/hikari/utilities/klass.py b/hikari/utilities/klass.py index 6a05c639d5..4b36266f21 100644 --- a/hikari/utilities/klass.py +++ b/hikari/utilities/klass.py @@ -20,14 +20,14 @@ from __future__ import annotations -__all__ = ["get_logger", "SingletonMeta", "Singleton"] +__all__: typing.List[str] = ["get_logger", "SingletonMeta", "Singleton"] import abc import logging import typing -def get_logger(cls: typing.Union[typing.Type, typing.Any], *additional_args: str) -> logging.Logger: +def get_logger(cls: typing.Union[typing.Type[typing.Any], typing.Any], *additional_args: str) -> logging.Logger: """Get an appropriately named logger for the given class or object. Parameters @@ -73,11 +73,11 @@ class SingletonMeta(abc.ABCMeta): __slots__ = () - ___instances___ = {} + ___instances___: typing.Dict[typing.Type[typing.Any], typing.Any] = {} # Disable type-checking to hide a bug in IntelliJ for the time being. @typing.no_type_check - def __call__(cls): + def __call__(cls) -> typing.Any: if cls not in SingletonMeta.___instances___: SingletonMeta.___instances___[cls] = super().__call__() return SingletonMeta.___instances___[cls] diff --git a/hikari/utilities/reflect.py b/hikari/utilities/reflect.py index 0d81704b76..6c1e862fa7 100644 --- a/hikari/utilities/reflect.py +++ b/hikari/utilities/reflect.py @@ -20,7 +20,7 @@ from __future__ import annotations -__all__ = ["resolve_signature", "EMPTY"] +__all__: typing.List[str] = ["resolve_signature", "EMPTY"] import inspect import typing diff --git a/hikari/utilities/snowflake.py b/hikari/utilities/snowflake.py index 78eb0de1c5..d3c9f739bc 100644 --- a/hikari/utilities/snowflake.py +++ b/hikari/utilities/snowflake.py @@ -20,9 +20,11 @@ from __future__ import annotations -__all__ = ["Snowflake"] +__all__: typing.List[str] = ["Snowflake"] import datetime + +# noinspection PyUnresolvedReferences import typing from hikari.utilities import date @@ -39,10 +41,6 @@ class Snowflake(int): ___MIN___: Snowflake ___MAX___: Snowflake - @staticmethod - def __new__(cls, value: typing.Union[int, str, typing.SupportsInt]) -> Snowflake: - return super(Snowflake, cls).__new__(cls, value) - @property def created_at(self) -> datetime.datetime: """When the object was created.""" diff --git a/hikari/utilities/undefined.py b/hikari/utilities/undefined.py index 32714ef218..537c20fc9b 100644 --- a/hikari/utilities/undefined.py +++ b/hikari/utilities/undefined.py @@ -20,7 +20,7 @@ from __future__ import annotations -__all__ = ["Undefined"] +__all__: typing.List[str] = ["Undefined"] import typing @@ -80,10 +80,10 @@ class Undefined(klass.Singleton): def __bool__(self) -> bool: return False - def __iter__(self) -> typing.Iterator[None]: + def __iter__(self) -> typing.Iterator[typing.Any]: yield from () - def __init_subclass__(cls, **kwargs: typing.Any) -> typing.NoReturn: + def __init_subclass__(cls, **kwargs: typing.Any) -> None: raise TypeError("Cannot subclass Undefined type") def __repr__(self) -> str: @@ -92,10 +92,10 @@ def __repr__(self) -> str: def __str__(self) -> str: return type(self).__name__.upper() - def __setattr__(self, _, __) -> typing.NoReturn: + def __setattr__(self, _: str, __: typing.Any) -> typing.NoReturn: raise TypeError("Cannot modify Undefined type") - def __delattr__(self, _) -> typing.NoReturn: + def __delattr__(self, _: str) -> typing.NoReturn: raise TypeError("Cannot modify Undefined type") @staticmethod diff --git a/tests/hikari/utilities/test_aio.py b/tests/hikari/utilities/test_aio.py index 0cd520b103..42fa77b3ee 100644 --- a/tests/hikari/utilities/test_aio.py +++ b/tests/hikari/utilities/test_aio.py @@ -190,25 +190,3 @@ async def __anext__(self): return ... assert not aio.is_async_iterable(AsyncIterator) - - -# noinspection PyProtocol -@pytest.mark.asyncio -class TestFuture: - async def test_is_instance(self, event_loop): - assert isinstance(event_loop.create_future(), aio.Future) - - async def nil(): - pass - - assert isinstance(asyncio.create_task(nil()), aio.Future) - - -# noinspection PyProtocol -@pytest.mark.asyncio -class TestTask: - async def test_is_instance(self, event_loop): - async def nil(): - pass - - assert isinstance(asyncio.create_task(nil()), aio.Task) From 12c103d55a7ca2b5783a6c57b088839b7f49f097 Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Sat, 6 Jun 2020 22:16:54 +0100 Subject: [PATCH 484/922] Amended useragent to not send verbose kernel name --- hikari/net/rest_utils.py | 1 + hikari/net/user_agents.py | 19 ++++++++++++++----- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/hikari/net/rest_utils.py b/hikari/net/rest_utils.py index 886979db85..5a08c078f4 100644 --- a/hikari/net/rest_utils.py +++ b/hikari/net/rest_utils.py @@ -92,6 +92,7 @@ async def _keep_typing(self) -> None: await asyncio.gather(self, asyncio.sleep(9.9), return_exceptions=True) +# TODO: document! @attr.s(auto_attribs=True, kw_only=True, slots=True) class GuildBuilder: _app: app_.IRESTApp diff --git a/hikari/net/user_agents.py b/hikari/net/user_agents.py index c25e5121ff..b1f51869b0 100644 --- a/hikari/net/user_agents.py +++ b/hikari/net/user_agents.py @@ -29,6 +29,7 @@ __all__: typing.List[str] = ["UserAgent"] +import aiohttp import typing from hikari.utilities import klass @@ -62,7 +63,7 @@ class UserAgent(klass.Singleton): Examples -------- - `"Linux-5.4.15-2-MANJARO-x86_64-with-glibc2.2.5"` + `"Linux 64bit"` """ user_agent: typing.Final[str] @@ -70,19 +71,27 @@ class UserAgent(klass.Singleton): Examples -------- - `"DiscordBot (https://gitlab.com/nekokatt/hikari; 1.0.1; Nekokatt) CPython 3.8.2 GCC 9.2.0 Linux"` + `"DiscordBot (https://gitlab.com/nekokatt/hikari; 1.0.1) Nekokatt Aiohttp/3.6.2 CPython/3.8.2 Linux 64bit"` """ def __init__(self) -> None: from hikari._about import __author__, __url__, __version__ - from platform import python_implementation, python_version, python_branch, python_compiler, platform + from platform import python_implementation, python_version, python_branch, python_compiler, architecture, system self.library_version = f"hikari {__version__}" self.platform_version = self._join_strip( python_implementation(), python_version(), python_branch(), python_compiler() ) - self.system_type = platform() - self.user_agent = f"DiscordBot ({__url__}; {__version__}; {__author__}) {python_version()} {self.system_type}" + self.system_type = " ".join((system(), architecture()[0])) + self.user_agent = ( + f"DiscordBot ({__url__}; {__version__}) {__author__} " + f"Aiohttp/{aiohttp.__version__} " + f"{python_implementation()}/{python_version()} " + f"{self.system_type}" + ) + + logger = klass.get_logger(self) + logger.debug("Using User-Agent %r", self.user_agent) @staticmethod def _join_strip(*args: str) -> str: From f3f7bbab2d57949ea575220a6bde320666ce2d1d Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Sun, 7 Jun 2020 10:57:59 +0100 Subject: [PATCH 485/922] Removed PresenceUser and replaced with PartialUser. - Removed PresenceUser from presences.py and implemented PartialUser in users.py as a replacement. - User now derives from PartialUser. - User format_avatar_url now returns None if the default avatar is in use. This is done since the default avatar currently cannot conform to the requested size and format parameters. - Moved user CDN code to cdn.py - Renamed presences.PresenceStatus to Status - RichActivity now derives from Activity. - More mypy updates... Documented rest_utils.GuildBuilder and renamed REST endpoint a little. Fixed missing documentation for GuildBuilder Fixed missing types on variables in documentation. More static-type-checking! Created mypy.ini and set up Cobertura reporting. Fixed pylint runner issues... Fixes lgtm https://lgtm.com/projects/gl/nekokatt/hikari/snapshot/2c0b23065b06861563fd3c819e97e60d36246005/files/hikari/net/rest.py?sort=name&dir=ASC&mode=heatmap#xd84cc937809e5422:1 --- ci/config.py | 1 + ci/gitlab/gitlab-templates.yml | 6 + ci/gitlab/linting.yml | 11 +- ci/mypy.nox.py | 13 +- ci/pylint.nox.py | 31 +- ci/safety.nox.py | 7 +- dev-requirements.txt | 4 +- docs/documentation.mako | 7 +- hikari/api/app.py | 43 ++- hikari/api/event_dispatcher.py | 8 +- hikari/impl/bot.py | 8 +- hikari/impl/entity_factory.py | 26 +- hikari/impl/gateway_zookeeper.py | 12 +- hikari/models/audit_logs.py | 2 +- hikari/models/bases.py | 1 + hikari/models/channels.py | 9 +- hikari/models/files.py | 25 +- hikari/models/presences.py | 172 +++-------- hikari/models/users.py | 87 ++++-- hikari/net/buckets.py | 7 +- hikari/net/gateway.py | 90 +++--- hikari/net/iterators.py | 85 +++--- hikari/net/rest.py | 268 ++++++++++-------- hikari/net/rest_utils.py | 298 ++++++++++++++++++-- hikari/net/routes.py | 10 +- hikari/utilities/cdn.py | 77 ++++- hikari/utilities/data_binding.py | 24 +- mypy.ini | 38 +++ pylint.ini | 8 +- tests/hikari/impl/test_entity_factory.py | 16 +- tests/hikari/utilities/test_data_binding.py | 5 - 31 files changed, 902 insertions(+), 497 deletions(-) create mode 100644 mypy.ini diff --git a/ci/config.py b/ci/config.py index 8712518063..bd9051e198 100644 --- a/ci/config.py +++ b/ci/config.py @@ -35,6 +35,7 @@ ROOT_INDEX_SOURCE = "index.html" # Linting and test configs. +MYPY_INI = "mypy.ini" PYDOCSTYLE_INI = "pydocstyle.ini" PYLINT_INI = "pylint.ini" PYLINT_JUNIT_OUTPUT_PATH = _os.path.join(ARTIFACT_DIRECTORY, "pylint.xml") diff --git a/ci/gitlab/gitlab-templates.yml b/ci/gitlab/gitlab-templates.yml index 6f3d0e522f..848f6ae38c 100644 --- a/ci/gitlab/gitlab-templates.yml +++ b/ci/gitlab/gitlab-templates.yml @@ -29,6 +29,8 @@ include: ### This must use a specific name. Do not rename it for consistency. ### dependency_scanning: + artifacts: + when: always interruptible: true retry: 1 stage: test @@ -47,6 +49,8 @@ dependency_scanning: ### This must use a specific name. Do not rename it for consistency. ### license_scanning: + artifacts: + when: always interruptible: true retry: 1 rules: @@ -62,6 +66,8 @@ license_scanning: ### This must use a specific name. Do not rename it for consistency. ### sast: + artifacts: + when: always interruptible: true retry: 1 rules: diff --git a/ci/gitlab/linting.yml b/ci/gitlab/linting.yml index 31bcc2547f..17cca6fc66 100644 --- a/ci/gitlab/linting.yml +++ b/ci/gitlab/linting.yml @@ -27,7 +27,7 @@ - .cpython - .reactive-job interruptible: true - retry: 2 + retry: 0 stage: test ### @@ -43,9 +43,10 @@ pylint: - public/pylint.html reports: junit: public/pylint.xml + when: always extends: .lint script: - - nox -s pylint --no-error-on-external-run + - nox -s pylint-junit --no-error-on-external-run ### ### Documentation linting. @@ -80,11 +81,15 @@ safety: script: - nox -s safety --no-error-on-external-run - ### ### MyPy static type checker. ### mypy: + allow_failure: true extends: .lint script: - nox -s mypy --no-error-on-external-run + artifacts: + reports: + cobertura: public/mypy/cobertura.xml + when: always diff --git a/ci/mypy.nox.py b/ci/mypy.nox.py index 881bb5836d..a4a728d4ac 100644 --- a/ci/mypy.nox.py +++ b/ci/mypy.nox.py @@ -23,15 +23,8 @@ @nox.session(reuse_venv=True, default=True) def mypy(session: nox.Session) -> None: - session.install("-r", "requirements.txt", "mypy==0.780") + # LXML is used for cobertura reporting. + session.install("-r", "requirements.txt", "mypy==0.780", "lxml") session.run( - "mypy", - "-p", - config.MAIN_PACKAGE, - "--pretty", - "--show-error-codes", - "--show-column-numbers", - # "--show-error-context", - "--strict", - "--warn-redundant-casts", + "mypy", "-p", config.MAIN_PACKAGE, "--config", config.MYPY_INI, ) diff --git a/ci/pylint.nox.py b/ci/pylint.nox.py index 772f9089bb..d04839bd1c 100644 --- a/ci/pylint.nox.py +++ b/ci/pylint.nox.py @@ -17,12 +17,14 @@ # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . """Pylint support.""" +import os import traceback from ci import config from ci import nox FLAGS = ["pylint", config.MAIN_PACKAGE, "--rcfile", config.PYLINT_INI] - +PYLINT_VER = "pylint==2.5.2" +PYLINT_JUNIT_VER = "pylint-junit==0.2.0" SUCCESS_CODES = list(range(0, 256)) @@ -30,12 +32,35 @@ def pylint(session: nox.Session) -> None: """Run pylint against the code base and report any code smells or issues.""" + session.install("-r", config.REQUIREMENTS, "-r", config.DEV_REQUIREMENTS, PYLINT_VER) + + try: + print("generating plaintext report") + session.run(*FLAGS, *session.posargs, success_codes=SUCCESS_CODES) + except Exception: + traceback.print_exc() + + +@nox.session(default=False, reuse_venv=True) +def pylint_junit(session: nox.Session) -> None: + """Runs `pylint', but produces JUnit reports instead of textual ones.""" + session.install( - "-r", config.REQUIREMENTS, "-r", config.DEV_REQUIREMENTS, + "-r", config.REQUIREMENTS, "-r", config.DEV_REQUIREMENTS, PYLINT_VER, PYLINT_JUNIT_VER, ) try: print("generating plaintext report") - session.run(*FLAGS, *session.posargs, success_codes=SUCCESS_CODES) + if not os.path.exists(config.ARTIFACT_DIRECTORY): + os.mkdir(config.ARTIFACT_DIRECTORY) + + with open(config.PYLINT_JUNIT_OUTPUT_PATH, "w+") as fp: + session.run( + *FLAGS, + "--output-format=pylint_junit.JUnitReporter", + *session.posargs, + success_codes=SUCCESS_CODES, + stdout=fp, + ) except Exception: traceback.print_exc() diff --git a/ci/safety.nox.py b/ci/safety.nox.py index c8fe26ac4d..33f1b268bc 100644 --- a/ci/safety.nox.py +++ b/ci/safety.nox.py @@ -22,9 +22,8 @@ from ci import nox -# Do not reuse venv, download new definitions each run. -@nox.session(reuse_venv=False, default=True) +@nox.session(reuse_venv=True, default=True) def safety(session: nox.Session) -> None: """Perform dependency scanning.""" - session.install("safety", "-r", config.REQUIREMENTS) - session.run("safety", "check") + session.install("safety", "-Ur", config.REQUIREMENTS) + session.run("safety", "check", "--full-report") diff --git a/dev-requirements.txt b/dev-requirements.txt index 189a714030..73b35d01d8 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -2,9 +2,7 @@ async-timeout~=3.0.1 coverage~=5.1 nox==2020.5.24 virtualenv==20.0.21 -pylint==2.5.2 -# 5.4.3 breaks stuff for us on Windows as usual... -pytest==5.4.2 +pytest==5.4.3 pytest-asyncio==0.12.0 pytest-cov==2.9.0 pytest-testdox==1.2.1 diff --git a/docs/documentation.mako b/docs/documentation.mako index 936ab97a2c..8bec95a43d 100644 --- a/docs/documentation.mako +++ b/docs/documentation.mako @@ -186,7 +186,8 @@ return name def get_annotation(bound_method, sep=':'): - annot = bound_method(link=link) or '' + annot = bound_method(link=link) or 'typing.Any' + annot = annot.replace("NoneType", "None") # Remove quotes. if annot.startswith("'") and annot.endswith("'"): @@ -241,6 +242,10 @@ <%def name="show_var(v, is_nested=False)"> <% return_type = get_annotation(v.type_annotation) + if return_type == "": + parent = v.cls.obj if v.cls is not None else v.module.obj + if hasattr(parent, "__annotations__") and v.name in parent.__annotations__: + return_type = get_annotation(lambda *_, **__: parent.__annotations__[v.name]) %>
${link(v, with_prefixes=True, anchor=True)}${return_type}
diff --git a/hikari/api/app.py b/hikari/api/app.py index aec961a212..3805834d4d 100644 --- a/hikari/api/app.py +++ b/hikari/api/app.py @@ -31,12 +31,15 @@ from hikari.utilities import undefined if typing.TYPE_CHECKING: - from concurrent import futures + import asyncio import datetime + from concurrent import futures + from hikari.api import cache as cache_ from hikari.api import entity_factory as entity_factory_ from hikari.api import event_consumer as event_consumer_ + from hikari.events import base from hikari.models import presences from hikari.net import http_settings as http_settings_ from hikari.net import gateway @@ -215,6 +218,13 @@ class IGatewayDispatcher(IApp, abc.ABC): __slots__ = () + if typing.TYPE_CHECKING: + _EventT = typing.TypeVar("_EventT", bound=base.HikariEvent) + _PredicateT = typing.Callable[[base.HikariEvent], typing.Union[bool, typing.Coroutine[None, typing.Any, bool]]] + _SyncCallbackT = typing.Callable[[base.HikariEvent], None] + _AsyncCallbackT = typing.Callable[[base.HikariEvent], typing.Coroutine[None, typing.Any, None]] + _CallbackT = typing.Union[_SyncCallbackT, _AsyncCallbackT] + @property @abc.abstractmethod def event_dispatcher(self) -> event_dispatcher_.IEventDispatcher: @@ -242,27 +252,38 @@ def event_dispatcher(self) -> event_dispatcher_.IEventDispatcher: # Do not add type hints to me! I delegate to a documented method elsewhere! @functools.wraps(event_dispatcher_.IEventDispatcher.listen) - def listen(self, event_type=undefined.Undefined()): + def listen( + self, event_type: typing.Union[undefined.Undefined, typing.Type[_EventT]] = undefined.Undefined(), + ) -> typing.Callable[[_CallbackT], _CallbackT]: ... # Do not add type hints to me! I delegate to a documented method elsewhere! @functools.wraps(event_dispatcher_.IEventDispatcher.subscribe) - def subscribe(self, event_type, callback): + def subscribe( + self, + event_type: typing.Type[_EventT], + callback: typing.Callable[[_EventT], typing.Union[typing.Coroutine[None, typing.Any, None], None]], + ) -> None: ... # Do not add type hints to me! I delegate to a documented method elsewhere! @functools.wraps(event_dispatcher_.IEventDispatcher.unsubscribe) - def unsubscribe(self, event_type, callback): + def unsubscribe( + self, + event_type: typing.Type[_EventT], + callback: typing.Callable[[_EventT], typing.Union[typing.Coroutine[None, typing.Any, None], None]], + ) -> None: ... # Do not add type hints to me! I delegate to a documented method elsewhere! @functools.wraps(event_dispatcher_.IEventDispatcher.wait_for) - async def wait_for(self, event_type, predicate, timeout): + async def wait_for( + self, event_type: typing.Type[_EventT], predicate: _PredicateT, timeout: typing.Union[float, int, None], + ) -> _EventT: ... - # Do not add type hints to me! I delegate to a documented method elsewhere! @functools.wraps(event_dispatcher_.IEventDispatcher.dispatch) - def dispatch(self, event): + def dispatch(self, event: base.HikariEvent) -> asyncio.Future[typing.Any]: ... @@ -329,8 +350,8 @@ async def join(self) -> None: async def update_presence( self, *, - status: typing.Union[undefined.Undefined, presences.PresenceStatus] = undefined.Undefined(), - activity: typing.Union[undefined.Undefined, presences.OwnActivity, None] = undefined.Undefined(), + status: typing.Union[undefined.Undefined, presences.Status] = undefined.Undefined(), + activity: typing.Union[undefined.Undefined, presences.Activity, None] = undefined.Undefined(), idle_since: typing.Union[undefined.Undefined, datetime.datetime, None] = undefined.Undefined(), is_afk: typing.Union[undefined.Undefined, bool] = undefined.Undefined(), ) -> None: @@ -350,9 +371,9 @@ async def update_presence( Parameters ---------- - status : hikari.models.presences.PresenceStatus or hikari.utilities.undefined.Undefined + status : hikari.models.presences.Status or hikari.utilities.undefined.Undefined If defined, the new status to set. - activity : hikari.models.presences.OwnActivity or None or hikari.utilities.undefined.Undefined + activity : hikari.models.presences.Activity or None or hikari.utilities.undefined.Undefined If defined, the new activity to set. idle_since : datetime.datetime or None or hikari.utilities.undefined.Undefined If defined, the time to show up as being idle since, or `None` if diff --git a/hikari/api/event_dispatcher.py b/hikari/api/event_dispatcher.py index d1bb4c7175..823efaf2ec 100644 --- a/hikari/api/event_dispatcher.py +++ b/hikari/api/event_dispatcher.py @@ -31,10 +31,10 @@ if typing.TYPE_CHECKING: from hikari.events import base - _EventT = typing.TypeVar("_EventT", bound=base.HikariEvent, covariant=True) - _PredicateT = typing.Callable[[_EventT], typing.Union[bool, typing.Coroutine[None, typing.Any, bool]]] - _SyncCallbackT = typing.Callable[[_EventT], None] - _AsyncCallbackT = typing.Callable[[_EventT], typing.Coroutine[None, typing.Any, None]] + _EventT = typing.TypeVar("_EventT", bound=base.HikariEvent) + _PredicateT = typing.Callable[[base.HikariEvent], typing.Union[bool, typing.Coroutine[None, typing.Any, bool]]] + _SyncCallbackT = typing.Callable[[base.HikariEvent], None] + _AsyncCallbackT = typing.Callable[[base.HikariEvent], typing.Coroutine[None, typing.Any, None]] _CallbackT = typing.Union[_SyncCallbackT, _AsyncCallbackT] diff --git a/hikari/impl/bot.py b/hikari/impl/bot.py index 72a963de05..ca751187a6 100644 --- a/hikari/impl/bot.py +++ b/hikari/impl/bot.py @@ -74,9 +74,9 @@ class BotImpl(gateway_zookeeper.AbstractGatewayZookeeper, app.IBot): The version of the gateway to connect to. At the time of writing, only version `6` and version `7` (undocumented development release) are supported. This defaults to using v6. - initial_activity : hikari.models.presences.OwnActivity or None or hikari.utilities.undefined.Undefined + initial_activity : hikari.models.presences.Activity or None or hikari.utilities.undefined.Undefined The initial activity to have on each shard. - initial_activity : hikari.models.presences.PresenceStatus or hikari.utilities.undefined.Undefined + initial_activity : hikari.models.presences.Status or hikari.utilities.undefined.Undefined The initial status to have on each shard. initial_idle_since : datetime.datetime or None or hikari.utilities.undefined.Undefined The initial time to show as being idle since, or `None` if not idle, @@ -149,10 +149,10 @@ def __init__( debug: bool = False, gateway_compression: bool = True, gateway_version: int = 6, - initial_activity: typing.Union[undefined.Undefined, presences.OwnActivity, None] = undefined.Undefined(), + initial_activity: typing.Union[undefined.Undefined, presences.Activity, None] = undefined.Undefined(), initial_idle_since: typing.Union[undefined.Undefined, datetime.datetime, None] = undefined.Undefined(), initial_is_afk: typing.Union[undefined.Undefined, bool] = undefined.Undefined(), - initial_status: typing.Union[undefined.Undefined, presences.PresenceStatus] = undefined.Undefined(), + initial_status: typing.Union[undefined.Undefined, presences.Status] = undefined.Undefined(), intents: typing.Optional[intents_.Intent] = None, large_threshold: int = 250, logging_level: typing.Optional[str] = "INFO", diff --git a/hikari/impl/entity_factory.py b/hikari/impl/entity_factory.py index e48f928b9c..629c7955df 100644 --- a/hikari/impl/entity_factory.py +++ b/hikari/impl/entity_factory.py @@ -1115,7 +1115,7 @@ def deserialize_message(self, payload: data_binding.JSONObject) -> message_model def deserialize_member_presence(self, payload: data_binding.JSONObject) -> presence_models.MemberPresence: guild_member_presence = presence_models.MemberPresence(self._app) user_payload = payload["user"] - user = presence_models.PresenceUser(self._app) + user = user_models.PartialUser(self._app) user.id = snowflake.Snowflake(user_payload["id"]) user.discriminator = user_payload["discriminator"] if "discriminator" in user_payload else undefined.Undefined() user.username = user_payload["username"] if "username" in user_payload else undefined.Undefined() @@ -1137,15 +1137,17 @@ def deserialize_member_presence(self, payload: data_binding.JSONObject) -> prese guild_member_presence.guild_id = snowflake.Snowflake(payload["guild_id"]) if "guild_id" in payload else None # noinspection PyArgumentList - guild_member_presence.visible_status = presence_models.PresenceStatus(payload["status"]) + guild_member_presence.visible_status = presence_models.Status(payload["status"]) activities = [] for activity_payload in payload["activities"]: - activity = presence_models.RichActivity() - activity.name = activity_payload["name"] # noinspection PyArgumentList - activity.type = presence_models.ActivityType(activity_payload["type"]) - activity.url = activity_payload.get("url") + activity = presence_models.RichActivity( + name=activity_payload["name"], + type=presence_models.ActivityType(activity_payload["type"]), + url=activity_payload.get("url"), + ) + activity.created_at = date.unix_epoch_to_datetime(activity_payload["created_at"]) if (timestamps_payload := activity_payload.get("timestamps", ...)) is not ...: @@ -1217,21 +1219,21 @@ def deserialize_member_presence(self, payload: data_binding.JSONObject) -> prese client_status = presence_models.ClientStatus() # noinspection PyArgumentList client_status.desktop = ( - presence_models.PresenceStatus(client_status_payload["desktop"]) + presence_models.Status(client_status_payload["desktop"]) if "desktop" in client_status_payload - else presence_models.PresenceStatus.OFFLINE + else presence_models.Status.OFFLINE ) # noinspection PyArgumentList client_status.mobile = ( - presence_models.PresenceStatus(client_status_payload["mobile"]) + presence_models.Status(client_status_payload["mobile"]) if "mobile" in client_status_payload - else presence_models.PresenceStatus.OFFLINE + else presence_models.Status.OFFLINE ) # noinspection PyArgumentList client_status.web = ( - presence_models.PresenceStatus(client_status_payload["web"]) + presence_models.Status(client_status_payload["web"]) if "web" in client_status_payload - else presence_models.PresenceStatus.OFFLINE + else presence_models.Status.OFFLINE ) guild_member_presence.client_status = client_status diff --git a/hikari/impl/gateway_zookeeper.py b/hikari/impl/gateway_zookeeper.py index a7fee1f4e2..e7f368cfcd 100644 --- a/hikari/impl/gateway_zookeeper.py +++ b/hikari/impl/gateway_zookeeper.py @@ -66,9 +66,9 @@ class AbstractGatewayZookeeper(app_.IGatewayZookeeper, abc.ABC): on the gateway will be dumped to debug logs. This will provide useful debugging context at the cost of performance. Generally you do not need to enable this. - initial_activity : hikari.models.presences.OwnActivity or None or hikari.utilities.undefined.Undefined + initial_activity : hikari.models.presences.Activity or None or hikari.utilities.undefined.Undefined The initial activity to have on each shard. - initial_activity : hikari.models.presences.PresenceStatus or hikari.utilities.undefined.Undefined + initial_activity : hikari.models.presences.Status or hikari.utilities.undefined.Undefined The initial status to have on each shard. initial_idle_since : datetime.datetime or None or hikari.utilities.undefined.Undefined The initial time to show as being idle since, or `None` if not idle, @@ -132,10 +132,10 @@ def __init__( compression: bool, config: http_settings.HTTPSettings, debug: bool, - initial_activity: typing.Union[undefined.Undefined, presences.OwnActivity, None] = undefined.Undefined(), + initial_activity: typing.Union[undefined.Undefined, presences.Activity, None] = undefined.Undefined(), initial_idle_since: typing.Union[undefined.Undefined, datetime.datetime, None] = undefined.Undefined(), initial_is_afk: typing.Union[undefined.Undefined, bool] = undefined.Undefined(), - initial_status: typing.Union[undefined.Undefined, presences.PresenceStatus] = undefined.Undefined(), + initial_status: typing.Union[undefined.Undefined, presences.Status] = undefined.Undefined(), intents: typing.Optional[intents_.Intent], large_threshold: int, shard_ids: typing.Set[int], @@ -295,8 +295,8 @@ def sigterm_handler(*_): async def update_presence( self, *, - status: typing.Union[undefined.Undefined, presences.PresenceStatus] = undefined.Undefined(), - activity: typing.Union[undefined.Undefined, presences.OwnActivity, None] = undefined.Undefined(), + status: typing.Union[undefined.Undefined, presences.Status] = undefined.Undefined(), + activity: typing.Union[undefined.Undefined, presences.Activity, None] = undefined.Undefined(), idle_since: typing.Union[undefined.Undefined, datetime.datetime] = undefined.Undefined(), is_afk: typing.Union[undefined.Undefined, bool] = undefined.Undefined(), ) -> None: diff --git a/hikari/models/audit_logs.py b/hikari/models/audit_logs.py index 97e0a16597..5b082341ff 100644 --- a/hikari/models/audit_logs.py +++ b/hikari/models/audit_logs.py @@ -316,5 +316,5 @@ class AuditLog: def __iter__(self) -> typing.Iterable[AuditLogEntry]: return self.entries.values() - def __len__(self): + def __len__(self) -> int: return len(self.entries) diff --git a/hikari/models/bases.py b/hikari/models/bases.py index f59d2f2305..fd0288dd9d 100644 --- a/hikari/models/bases.py +++ b/hikari/models/bases.py @@ -42,6 +42,7 @@ class Entity(abc.ABC): application instance. This enables it to perform API calls from methods directly. """ + _AppT = typing.Union[app_.IRESTApp, app_.IBot] _app: _AppT = attr.ib(default=None, repr=False, eq=False, hash=False) diff --git a/hikari/models/channels.py b/hikari/models/channels.py index 9c011e0d92..854d4d52ec 100644 --- a/hikari/models/channels.py +++ b/hikari/models/channels.py @@ -90,7 +90,7 @@ class PermissionOverwriteType(str, enum.Enum): """A permission overwrite that targets a specific guild member.""" def __str__(self) -> str: - return self.value + return str(self.value) @attr.s(eq=True, hash=True, init=True, kw_only=True, slots=True) @@ -124,19 +124,20 @@ class PermissionOverwrite(bases.Unique): """The type of entity this overwrite targets.""" allow: permissions.Permission = attr.ib( - converter=permissions.Permission, default=0, eq=False, hash=False, repr=False + converter=permissions.Permission, default=permissions.Permission.NONE, eq=False, hash=False, repr=False, ) """The permissions this overwrite allows.""" deny: permissions.Permission = attr.ib( - converter=permissions.Permission, default=0, eq=False, hash=False, repr=False + converter=permissions.Permission, default=permissions.Permission.NONE, eq=False, hash=False, repr=False ) """The permissions this overwrite denies.""" @property def unset(self) -> permissions.Permission: """Bitfield of all permissions not explicitly allowed or denied by this overwrite.""" - return typing.cast(permissions.Permission, (self.allow | self.deny)) + # noinspection PyArgumentList + return permissions.Permission(~(self.allow | self.deny)) @attr.s(eq=True, hash=True, init=False, kw_only=True, slots=True) diff --git a/hikari/models/files.py b/hikari/models/files.py index 7f10d6182e..cf41f09a44 100644 --- a/hikari/models/files.py +++ b/hikari/models/files.py @@ -165,7 +165,7 @@ async def read(self, count: int = -1) -> bytes: The bytes that were read. """ if count == -1: - count = float("inf") + count = float("inf") # type: ignore data = bytearray() async for chunk in self: @@ -184,7 +184,7 @@ class _AsyncByteIterable: def __init__(self, byte_content: bytes) -> None: self._byte_content = byte_content - async def __aiter__(self): + async def __aiter__(self) -> typing.AsyncGenerator[typing.Any, bytes]: for i in range(0, len(self._byte_content), MAGIC_NUMBER): yield self._byte_content[i : i + MAGIC_NUMBER] @@ -192,12 +192,12 @@ async def __aiter__(self): class _MemorizedAsyncIteratorDecorator: __slots__ = ("_async_iterator", "_exhausted", "_buff") - def __init__(self, async_iterator: typing.AsyncIterator) -> None: + def __init__(self, async_iterator: typing.AsyncIterator[bytes]) -> None: self._async_iterator = async_iterator self._exhausted = False self._buff = bytearray() - async def __aiter__(self): + async def __aiter__(self) -> typing.AsyncGenerator[typing.Any, bytes]: if self._exhausted: async for chunk in _AsyncByteIterable(self._buff): yield chunk @@ -340,6 +340,7 @@ class ByteStream(BaseStream): ] ___VALID_TYPES___ = typing.Union[ + typing.Callable[[], typing.AsyncGenerator[typing.Any, ___VALID_BYTE_TYPES___]], typing.AsyncGenerator[typing.Any, ___VALID_BYTE_TYPES___], typing.AsyncIterator[___VALID_BYTE_TYPES___], typing.AsyncIterable[___VALID_BYTE_TYPES___], @@ -349,18 +350,17 @@ class ByteStream(BaseStream): ] _obj: typing.Union[ - typing.AsyncGenerator[typing.Any, ___VALID_BYTE_TYPES___], - typing.AsyncIterable[typing.Any, ___VALID_BYTE_TYPES___], + typing.AsyncGenerator[typing.Any, ___VALID_BYTE_TYPES___], typing.AsyncIterable[___VALID_BYTE_TYPES___], ] def __init__(self, filename: str, obj: ___VALID_TYPES___) -> None: self._filename = filename if inspect.isasyncgenfunction(obj): - obj = obj() + obj = obj() # type: ignore if inspect.isasyncgen(obj) or aio.is_async_iterator(obj): - self._obj = _MemorizedAsyncIteratorDecorator(obj) + self._obj = _MemorizedAsyncIteratorDecorator(obj) # type: ignore return if aio.is_async_iterable(obj): @@ -379,7 +379,7 @@ def __init__(self, filename: str, obj: ___VALID_TYPES___) -> None: raise TypeError(f"Expected bytes-like object or async generator, got {type(obj).__qualname__}") - def __aiter__(self) -> typing.AsyncGenerator[bytes]: + def __aiter__(self) -> typing.AsyncGenerator[typing.Any, bytes]: return self._obj.__aiter__() @property @@ -387,7 +387,7 @@ def filename(self) -> str: return self._filename async def _aiter_async_iterator( - self, async_iterator: typing.AsyncGenerator[___VALID_BYTE_TYPES___] + self, async_iterator: typing.AsyncGenerator[typing.Any, ___VALID_BYTE_TYPES___] ) -> typing.AsyncIterator[bytes]: try: while True: @@ -434,7 +434,7 @@ def __init__(self, filename: str, url: str) -> None: self._filename = filename self.url = url - async def __aiter__(self) -> typing.AsyncGenerator[bytes]: + async def __aiter__(self) -> typing.Generator[bytes, None, None]: async with aiohttp.request("GET", self.url) as response: if 200 <= response.status < 300: async for chunk in response.content: @@ -461,6 +461,7 @@ async def __aiter__(self) -> typing.AsyncGenerator[bytes]: else: cls = errors.HTTPErrorResponse + # noinspection PyArgumentList raise cls(real_url, http.HTTPStatus(response.status), response.headers, raw_body) @property @@ -545,7 +546,7 @@ def __init__(self, *args, executor=None) -> None: self._filename, self.path = args self._executor = executor - def __aiter__(self) -> typing.AsyncGenerator[bytes]: + def __aiter__(self) -> typing.AsyncGenerator[typing.Any, bytes]: loop = asyncio.get_event_loop() # We cant use a process pool in the same way we do a thread pool, as # we cannot pickle file objects that we pass between threads. This diff --git a/hikari/models/presences.py b/hikari/models/presences.py index 18f976e579..e3119b5e5c 100644 --- a/hikari/models/presences.py +++ b/hikari/models/presences.py @@ -21,7 +21,7 @@ from __future__ import annotations __all__: typing.List[str] = [ - "OwnActivity", + "Activity", "ActivityAssets", "ActivityFlag", "ActivitySecret", @@ -31,8 +31,7 @@ "ClientStatus", "MemberPresence", "RichActivity", - "PresenceStatus", - "PresenceUser", + "Status", ] import enum @@ -42,7 +41,6 @@ from hikari.models import bases from hikari.models import users -from hikari.utilities import undefined if typing.TYPE_CHECKING: import datetime @@ -161,18 +159,39 @@ class ActivityFlag(enum.IntFlag): """Play""" -@attr.s(eq=True, hash=False, init=False, kw_only=True, slots=True) -class RichActivity: - """Represents an activity that will be attached to a member's presence.""" +# TODO: add strict type checking to gateway for this type in an invariant way. +@attr.s(eq=True, hash=False, kw_only=True, slots=True) +class Activity: + """An activity that the bot can set for one or more shards. + + !!! note + Bots cannot currently set custom presence statuses. + + !!! warning + Other activity types may derive from this one, but only their + name, url and type will be passed if used in a presence update + request. Passing a `RichActivity` or similar may cause an + `INVALID_OPCODE` to be raised which will result in the shard shutting + down. + """ + + name: str = attr.ib() + """The activity name.""" - name: str = attr.ib(repr=True) - """The activity's name.""" + url: typing.Optional[str] = attr.ib(default=None, repr=False) + """The activity URL. Only valid for `STREAMING` activities.""" - type: ActivityType = attr.ib(repr=True) - """The activity's type.""" + type: ActivityType = attr.ib(converter=ActivityType) + """The activity type.""" - url: typing.Optional[str] = attr.ib(repr=False) - """The URL for a `STREAM` type activity, if applicable.""" + +@attr.s(eq=True, hash=False, init=False, kw_only=True, slots=True) +class RichActivity(Activity): + """Represents an activity that will be attached to a member's presence. + + !!! warning + You can NOT use this in presence update requests. + """ created_at: datetime.datetime = attr.ib(repr=False) """When this activity was added to the user's session.""" @@ -210,7 +229,7 @@ class RichActivity: """Flags that describe what the activity includes.""" -class PresenceStatus(str, enum.Enum): +class Status(str, enum.Enum): """The status of a member.""" ONLINE = "online" @@ -223,7 +242,7 @@ class PresenceStatus(str, enum.Enum): """Do not disturb/red.""" DO_NOT_DISTURB = DND - """An alias for `PresenceStatus.DND`""" + """An alias for `Status.DND`""" OFFLINE = "offline" """Offline or invisible/grey.""" @@ -233,117 +252,21 @@ class PresenceStatus(str, enum.Enum): class ClientStatus: """The client statuses for this member.""" - desktop: PresenceStatus = attr.ib(repr=True) + desktop: Status = attr.ib(repr=True) """The status of the target user's desktop session.""" - mobile: PresenceStatus = attr.ib(repr=True) + mobile: Status = attr.ib(repr=True) """The status of the target user's mobile session.""" - web: PresenceStatus = attr.ib(repr=True) + web: Status = attr.ib(repr=True) """The status of the target user's web session.""" -# TODO: should this be an event instead? -@attr.s(eq=True, hash=True, init=False, kw_only=True, slots=True) -class PresenceUser(users.User): - """A user representation specifically used for presence updates. - - !!! warning - Every attribute except `PresenceUser.id` may be as - `hikari.utilities.undefined.Undefined` unless it is specifically being modified - for this update. - """ - - discriminator: typing.Union[str, undefined.Undefined] = attr.ib(eq=False, hash=False, repr=True) - """This user's discriminator.""" - - username: typing.Union[str, undefined.Undefined] = attr.ib(eq=False, hash=False, repr=True) - """This user's username.""" - - avatar_hash: typing.Union[None, str, undefined.Undefined] = attr.ib(eq=False, hash=False, repr=True) - """This user's avatar hash, if set.""" - - is_bot: typing.Union[bool, undefined.Undefined] = attr.ib(eq=False, hash=False, repr=True) - """Whether this user is a bot account.""" - - is_system: typing.Union[bool, undefined.Undefined] = attr.ib(eq=False, hash=False, repr=False) - """Whether this user is a system account.""" - - flags: typing.Union[users.UserFlag, undefined.Undefined] = attr.ib(eq=False, hash=False, repr=False) - """The public flags for this user.""" - - @property - def avatar_url(self) -> typing.Union[str, undefined.Undefined]: - """URL for this user's avatar if the relevant info is available. - - !!! note - This will be `hikari.models.undefined.Undefined` if both `PresenceUser.avatar_hash` - and `PresenceUser.discriminator` are `hikari.models.undefined.Undefined`. - """ - return self.format_avatar_url() - - def format_avatar_url( - self, *, format_: typing.Optional[str] = None, size: int = 4096 - ) -> typing.Union[str, undefined.Undefined]: - """Generate the avatar URL for this user's avatar if available. - - Parameters - ---------- - format_ : str - The format to use for this URL, defaults to `png` or `gif`. - Supports `png`, `jpeg`, `jpg`, `webp` and `gif` (when animated). - Will be ignored for default avatars which can only be `png`. - size : int - The size to set for the URL, defaults to `4096`. - Can be any power of two between 16 and 4096. - Will be ignored for default avatars. - - Returns - ------- - hikari.models.undefined.Undefined or str - The string URL of the user's custom avatar if - either `PresenceUser.avatar_hash` is set or their default avatar if - `PresenceUser.discriminator` is set, else `hikari.models.undefined.Undefined`. - - Raises - ------ - ValueError - If `size` is not a power of two or not between 16 and 4096. - """ - if self.discriminator is not undefined.Undefined() or self.avatar_hash is not undefined.Undefined(): - return super().format_avatar_url(format_=format_, size=size) - return undefined.Undefined() - - @property - def default_avatar_index(self) -> typing.Union[int, undefined.Undefined]: - """Integer representation of this user's default avatar. - - !!! note - This will be `hikari.models.undefined.Undefined` if `PresenceUser.discriminator` is - `hikari.models.undefined.Undefined`. - """ - if self.discriminator is not undefined.Undefined(): - return super().default_avatar_index - return undefined.Undefined() - - @property - def default_avatar_url(self) -> typing.Union[str, undefined.Undefined]: - """URL for this user's default avatar. - - !!! note - This will be `hikari.models.undefined.Undefined` if `PresenceUser.discriminator` is - `hikari.models.undefined.Undefined`. - """ - if self.discriminator is not undefined.Undefined(): - return super().default_avatar_url - return undefined.Undefined() - - @attr.s(eq=True, hash=True, init=False, kw_only=True, slots=True) class MemberPresence(bases.Entity): """Used to represent a guild member's presence.""" - user: PresenceUser = attr.ib(eq=True, hash=True, repr=True) + user: users.PartialUser = attr.ib(eq=True, hash=True, repr=True) """The object of the user who this presence is for. !!! info @@ -366,7 +289,7 @@ class MemberPresence(bases.Entity): object (e.g on Guild Create). """ - visible_status: PresenceStatus = attr.ib(eq=False, hash=False, repr=True) + visible_status: Status = attr.ib(eq=False, hash=False, repr=True) """This user's current status being displayed by the client.""" activities: typing.Sequence[RichActivity] = attr.ib(eq=False, hash=False, repr=False) @@ -385,20 +308,3 @@ class MemberPresence(bases.Entity): nickname: typing.Optional[str] = attr.ib(eq=False, hash=False, repr=True) """This member's nickname, if set.""" - - -@attr.s(eq=True, hash=False, kw_only=True, slots=True) -class OwnActivity: - """An activity that the bot can set for one or more shards. - - This will show the activity as the bot's presence. - """ - - name: str = attr.ib(repr=True) - """The activity name.""" - - url: typing.Optional[str] = attr.ib(default=None, repr=True) - """The activity URL. Only valid for `STREAMING` activities.""" - - type: ActivityType = attr.ib(converter=ActivityType, repr=True) - """The activity type.""" diff --git a/hikari/models/users.py b/hikari/models/users.py index 62a29546bd..13fa197f78 100644 --- a/hikari/models/users.py +++ b/hikari/models/users.py @@ -29,6 +29,7 @@ from hikari.models import bases from hikari.utilities import cdn +from hikari.utilities import undefined @enum.unique @@ -93,25 +94,56 @@ class PremiumType(int, enum.Enum): @attr.s(eq=True, hash=True, init=False, kw_only=True, slots=True) -class User(bases.Entity, bases.Unique): - """Represents a user.""" +class PartialUser(bases.Entity, bases.Unique): + """Represents partial information about a user. - discriminator: str = attr.ib(eq=False, hash=False, repr=True) + This is pretty much the same as a normal user, but information may not be + present. + """ + + discriminator: typing.Union[str, undefined.Undefined] = attr.ib(eq=False, hash=False, repr=True) """This user's discriminator.""" - username: str = attr.ib(eq=False, hash=False, repr=True) + username: typing.Union[str, undefined.Undefined] = attr.ib(eq=False, hash=False, repr=True) """This user's username.""" - avatar_hash: typing.Optional[str] = attr.ib(eq=False, hash=False, repr=False) + avatar_hash: typing.Union[None, str, undefined.Undefined] = attr.ib(eq=False, hash=False, repr=False) """This user's avatar hash, if set.""" - is_bot: bool = attr.ib(eq=False, hash=False, repr=False) + is_bot: typing.Union[bool, undefined.Undefined] = attr.ib(eq=False, hash=False, repr=False) """Whether this user is a bot account.""" - is_system: bool = attr.ib(eq=False, hash=False, repr=False) + is_system: typing.Union[bool, undefined.Undefined] = attr.ib(eq=False, hash=False) """Whether this user is a system account.""" - flags: UserFlag = attr.ib(eq=False, hash=False, repr=False) + flags: typing.Union[UserFlag, undefined.Undefined] = attr.ib(eq=False, hash=False) + """The public flags for this user.""" + + +@attr.s(eq=True, hash=True, init=False, kw_only=True, slots=True) +class User(PartialUser): + """Represents partial information about a user.""" + + # These are not attribs on purpose. The idea is to narrow the types of + # these fields without redefining them twice in the slots. This is + # compatible with MYPY, hence why I have done it like this... + + discriminator: str + """This user's discriminator.""" + + username: str + """This user's username.""" + + avatar_hash: typing.Optional[str] + """This user's avatar hash, if they have one, otherwise `None`.""" + + is_bot: bool + """`True` if this user is a bot account, `False` otherwise.""" + + is_system: bool + """`True` if this user is a system account, `False` otherwise.""" + + flags: UserFlag """The public flags for this user.""" async def fetch_self(self) -> User: @@ -130,12 +162,16 @@ async def fetch_self(self) -> User: return await self._app.rest.fetch_user(user=self.id) @property - def avatar_url(self) -> str: - """URL for this user's custom avatar if set, else default.""" + def avatar_url(self) -> typing.Optional[str]: + """URL for this user's custom avatar if set, else `None`.""" return self.format_avatar_url() - def format_avatar_url(self, *, format_: typing.Optional[str] = None, size: int = 4096) -> str: - """Generate the avatar URL for this user's custom avatar if set, else their default avatar. + def format_avatar_url(self, *, format_: typing.Optional[str] = None, size: int = 4096) -> typing.Optional[str]: + """Generate the avatar URL for this user's custom avatar if set. + + If no custom avatar is set, this returns `None`. You can then use the + `User.default_avatar_url` attribute instead to fetch the displayed + URL. Parameters ---------- @@ -152,31 +188,26 @@ def format_avatar_url(self, *, format_: typing.Optional[str] = None, size: int = Returns ------- str - The string URL. + The string URL, or `None` if not present. Raises ------ ValueError If `size` is not a power of two or not between 16 and 4096. """ - if not self.avatar_hash: - return self.default_avatar_url - - if format_ is None and self.avatar_hash.startswith("a_"): - format_ = "gif" - elif format_ is None: - format_ = "png" - return cdn.generate_cdn_url("avatars", str(self.id), self.avatar_hash, format_=format_, size=size) + if self.avatar_hash is not None: + return cdn.get_avatar_url(self.id, self.avatar_hash, format_=format_, size=size) + return None @property def default_avatar_index(self) -> int: """Integer representation of this user's default avatar.""" - return int(self.discriminator) % 5 + return cdn.get_default_avatar_index(self.discriminator) @property def default_avatar_url(self) -> str: """URL for this user's default avatar.""" - return cdn.generate_cdn_url("embed", "avatars", str(self.default_avatar_index), format_="png", size=None) + return cdn.get_default_avatar_url(self.discriminator) @attr.s(eq=True, hash=True, init=False, kw_only=True, slots=True) @@ -184,7 +215,7 @@ class OwnUser(User): """Represents a user with extended OAuth2 information.""" is_mfa_enabled: bool = attr.ib(eq=False, hash=False, repr=False) - """Whether the user's account has 2fa enabled.""" + """Whether the user's account has multi-factor authentication enabled.""" locale: typing.Optional[str] = attr.ib(eq=False, hash=False, repr=False) """The user's set language. This is not provided by the `READY` event.""" @@ -192,15 +223,15 @@ class OwnUser(User): is_verified: typing.Optional[bool] = attr.ib(eq=False, hash=False, repr=False) """Whether the email for this user's account has been verified. - Will be `None` if retrieved through the oauth2 flow without the `email` + Will be `None` if retrieved through the OAuth2 flow without the `email` scope. """ email: typing.Optional[str] = attr.ib(eq=False, hash=False, repr=False) """The user's set email. - Will be `None` if retrieved through the oauth2 flow without the `email` - scope and for bot users. + Will be `None` if retrieved through OAuth2 flow without the `email` + scope. Will always be `None` for bot users. """ flags: UserFlag = attr.ib(eq=False, hash=False, repr=False) @@ -220,4 +251,4 @@ async def fetch_self(self) -> OwnUser: hikari.models.users.User The requested user object. """ - return await self._app.rest.fetch_me() + return await self._app.rest.fetch_my_user() diff --git a/hikari/net/buckets.py b/hikari/net/buckets.py index c04d5cc2a7..c57b52703b 100644 --- a/hikari/net/buckets.py +++ b/hikari/net/buckets.py @@ -347,7 +347,7 @@ def __init__(self) -> None: self.routes_to_hashes = {} self.real_hashes_to_buckets = {} self.closed_event: asyncio.Event = asyncio.Event() - self.gc_task: typing.Optional[asyncio.Task] = None + self.gc_task: typing.Optional[asyncio.Task[None]] = None self.logger = logging.getLogger("hikari.rest.buckets.RESTBucketManager") def __enter__(self) -> RESTBucketManager: @@ -530,7 +530,7 @@ def acquire(self, compiled_route: routes.CompiledRoute) -> asyncio.Future[None]: def update_rate_limits( self, compiled_route: routes.CompiledRoute, - bucket_header: typing.Optional[str], + bucket_header: str, remaining_header: int, limit_header: int, date_header: datetime.datetime, @@ -543,8 +543,7 @@ def update_rate_limits( compiled_route : hikari.rest.routes.CompiledRoute The compiled _route to get the bucket for. bucket_header : str, optional - The `X-RateLimit-Bucket` header that was provided in the response, - or `None` if not present. + The `X-RateLimit-Bucket` header that was provided in the response. remaining_header : int The `X-RateLimit-Remaining` header cast to an `int`. limit_header : int diff --git a/hikari/net/gateway.py b/hikari/net/gateway.py index 29219a2b18..1339794ac8 100644 --- a/hikari/net/gateway.py +++ b/hikari/net/gateway.py @@ -67,13 +67,13 @@ class Gateway(http_client.HTTPClient, component.IComponent): debug : bool If `True`, each sent and received payload is dumped to the logs. If `False`, only the fact that data has been sent/received will be logged. - initial_activity : hikari.presences.OwnActivity or None or hikari.utilities.undefined.Undefined + initial_activity : hikari.presences.Activity or None or hikari.utilities.undefined.Undefined The initial activity to appear to have for this shard. initial_idle_since : datetime.datetime or None or hikari.utilities.undefined.Undefined The datetime to appear to be idle since. initial_is_afk : bool or hikari.utilities.undefined.Undefined Whether to appear to be AFK or not on login. - initial_status : hikari.models.presences.PresenceStatus or hikari.utilities.undefined.Undefined + initial_status : hikari.models.presences.Status or hikari.utilities.undefined.Undefined The initial status to set on login for the shard. intents : hikari.models.intents.Intent or None Collection of intents to use, or `None` to not use intents at all. @@ -161,10 +161,10 @@ def __init__( app: app_.IGatewayConsumer, config: http_settings.HTTPSettings, debug: bool = False, - initial_activity: typing.Union[undefined.Undefined, None, presences.OwnActivity] = undefined.Undefined(), + initial_activity: typing.Union[undefined.Undefined, None, presences.Activity] = undefined.Undefined(), initial_idle_since: typing.Union[undefined.Undefined, None, datetime.datetime] = undefined.Undefined(), initial_is_afk: typing.Union[undefined.Undefined, bool] = undefined.Undefined(), - initial_status: typing.Union[undefined.Undefined, presences.PresenceStatus] = undefined.Undefined(), + initial_status: typing.Union[undefined.Undefined, presences.Status] = undefined.Undefined(), intents: typing.Optional[intents_.Intent] = None, large_threshold: int = 250, shard_id: int = 0, @@ -187,24 +187,25 @@ def __init__( timeout=config.request_timeout, trust_env=config.trust_env, ) - self._activity = initial_activity + self._activity: typing.Union[undefined.Undefined, None, presences.Activity] = initial_activity self._app = app self._backoff = rate_limits.ExponentialBackOff(base=1.85, maximum=600, initial_increment=2) self._handshake_event = asyncio.Event() - self._idle_since = initial_idle_since - self._intents = intents - self._is_afk = initial_is_afk + self._idle_since: typing.Union[undefined.Undefined, None, datetime.datetime] = initial_idle_since + self._intents: typing.Optional[intents_.Intent] = intents + self._is_afk: typing.Union[undefined.Undefined, bool] = initial_is_afk self._last_run_started_at = float("nan") self._request_close_event = asyncio.Event() - self._seq = None - self._shard_id = shard_id - self._shard_count = shard_count - self._status = initial_status + self._seq: typing.Optional[str] = None + self._shard_id: int = shard_id + self._shard_count: int = shard_count + self._status: typing.Union[undefined.Undefined, presences.Status] = initial_status self._token = token self._use_compression = use_compression self._version = version - self._ws = None - self._zlib = None + self._ws: typing.Optional[aiohttp.ClientWebSocketResponse] = None + # No typeshed/stub. + self._zlib: typing.Any = None self._zombied = False self.connected_at = float("nan") @@ -214,7 +215,7 @@ def __init__( self.last_message_received = float("nan") self.large_threshold = large_threshold self.ratelimiter = rate_limits.WindowedBurstRateLimiter(str(shard_id), 60.0, 120) - self.session_id = None + self.session_id: typing.Optional[str] = None scheme, netloc, path, params, _, _ = urllib.parse.urlparse(url, allow_fragments=True) @@ -374,8 +375,8 @@ async def update_presence( *, idle_since: typing.Union[undefined.Undefined, typing.Optional[datetime.datetime]] = undefined.Undefined(), is_afk: typing.Union[undefined.Undefined, bool] = undefined.Undefined(), - activity: typing.Union[undefined.Undefined, typing.Optional[presences.OwnActivity]] = undefined.Undefined(), - status: typing.Union[undefined.Undefined, presences.PresenceStatus] = undefined.Undefined(), + activity: typing.Union[undefined.Undefined, typing.Optional[presences.Activity]] = undefined.Undefined(), + status: typing.Union[undefined.Undefined, presences.Status] = undefined.Undefined(), ) -> None: """Update the presence of the shard user. @@ -387,14 +388,15 @@ async def update_presence( is_afk : bool or hikari.utilities.undefined.Undefined If `True`, the user is marked as AFK. If `False`, the user is marked as being active. If undefined, this will not be changed. - activity : hikari.models.presences.OwnActivity or None or hikari.utilities.undefined.Undefined + activity : hikari.models.presences.Activity or None or hikari.utilities.undefined.Undefined The activity to appear to be playing. If undefined, this will not be changed. - status : hikari.models.presences.PresenceStatus or hikari.utilities.undefined.Undefined + status : hikari.models.presences.Status or hikari.utilities.undefined.Undefined The web status to show. If undefined, this will not be changed. """ - payload = self._build_presence_payload(idle_since, is_afk, activity, status) - await self._send_json({"op": self._GatewayOpcode.PRESENCE_UPDATE, "d": payload}) + presence = self._build_presence_payload(idle_since=idle_since, is_afk=is_afk, status=status, activity=activity) + payload: data_binding.JSONObject = {"op": self._GatewayOpcode.PRESENCE_UPDATE, "d": presence} + await self._send_json(payload) self._idle_since = idle_since if not isinstance(idle_since, undefined.Undefined) else self._idle_since self._is_afk = is_afk if not isinstance(is_afk, undefined.Undefined) else self._is_afk self._activity = activity if not isinstance(activity, undefined.Undefined) else self._activity @@ -425,7 +427,7 @@ async def update_voice_state( If `True`, the bot will deafen itself in that voice channel. If `False`, then it will undeafen itself. """ - payload = { + payload: data_binding.JSONObject = { "op": self._GatewayOpcode.VOICE_STATE_UPDATE, "d": { "guild_id": str(int(guild)), @@ -436,7 +438,7 @@ async def update_voice_state( } await self._send_json(payload) - async def _close_ws(self, code: int, message: str): + async def _close_ws(self, code: int, message: str) -> None: self.logger.debug("sending close frame with code %s and message %r", int(code), message) # None if the websocket error'ed on initialization. if self._ws is not None: @@ -466,7 +468,7 @@ async def _handshake(self) -> None: else: # IDENTIFY! # noinspection PyArgumentList - payload = { + payload: data_binding.JSONObject = { "op": self._GatewayOpcode.IDENTIFY, "d": { "token": self._token, @@ -593,9 +595,15 @@ async def _receive_json_payload(self) -> data_binding.JSONObject: self.logger.debug("encountered unexpected error", exc_info=ex) raise errors.GatewayError("Unexpected websocket exception from gateway") from ex - return data_binding.load_json(string) + # We assume this is always a JSON object, I'd rather not cast here and waste + # CPU time as this is somewhat performance critical for large bots. + return data_binding.load_json(string) # type: ignore async def _receive_zlib_message(self, first_packet: bytes) -> typing.Tuple[int, str]: + # Alloc new array each time; this prevents consuming a large amount of + # unused memory because of Discord sending massive payloads on connect + # initially before the payloads shrink in size. Python may not shrink + # this dynamically if not... buff = bytearray(first_packet) packets = 1 @@ -620,8 +628,8 @@ async def _send_json(self, payload: data_binding.JSONObject) -> None: self._log_debug_payload(message, "sending json payload") await self._ws.send_str(message) - def _dispatch(self, event_name: str, payload: data_binding.JSONObject) -> typing.Coroutine[None, typing.Any, None]: - return self._app.event_consumer.consume_raw_event(self, event_name, payload) + def _dispatch(self, event_name: str, event: data_binding.JSONObject) -> typing.Coroutine[None, typing.Any, None]: + return self._app.event_consumer.consume_raw_event(self, event_name, event) @staticmethod def _now() -> float: @@ -645,8 +653,8 @@ def _build_presence_payload( self, idle_since: typing.Union[undefined.Undefined, typing.Optional[datetime.datetime]] = undefined.Undefined(), is_afk: typing.Union[undefined.Undefined, bool] = undefined.Undefined(), - status: typing.Union[undefined.Undefined, presences.PresenceStatus] = undefined.Undefined(), - activity: typing.Union[undefined.Undefined, typing.Optional[presences.OwnActivity]] = undefined.Undefined(), + status: typing.Union[undefined.Undefined, presences.Status] = undefined.Undefined(), + activity: typing.Union[undefined.Undefined, typing.Optional[presences.Activity]] = undefined.Undefined(), ) -> data_binding.JSONObject: if isinstance(idle_since, undefined.Undefined): idle_since = self._idle_since @@ -657,20 +665,18 @@ def _build_presence_payload( if isinstance(activity, undefined.Undefined): activity = self._activity - activity = typing.cast(typing.Optional[presences.OwnActivity], activity) - - if activity is None: - game = None - else: - game = { + if activity is not None and not isinstance(activity, undefined.Undefined): + game: typing.Union[undefined.Undefined, None, data_binding.JSONObject] = { "name": activity.name, "url": activity.url, "type": activity.type, } - - return { - "since": idle_since.timestamp() if idle_since is not None else None, - "afk": is_afk if is_afk is not None else False, - "status": status.value if status is not None else presences.PresenceStatus.ONLINE.value, - "game": game, - } + else: + game = activity + + payload = data_binding.JSONObjectBuilder() + payload.put("since", idle_since, conversion=datetime.datetime.timestamp) + payload.put("afk", is_afk) + payload.put("status", status) + payload.put("game", game) + return payload diff --git a/hikari/net/iterators.py b/hikari/net/iterators.py index 4cc6d49475..2ab53b7ec0 100644 --- a/hikari/net/iterators.py +++ b/hikari/net/iterators.py @@ -40,10 +40,6 @@ _T = typing.TypeVar("_T") -def _empty_generator(): - yield from () - - class LazyIterator(typing.Generic[_T], abc.ABC): """A set of results that are fetched asynchronously from the API as needed. @@ -170,18 +166,15 @@ def __aiter__(self) -> LazyIterator[_T]: async def _fetch_all(self) -> typing.Sequence[_T]: return [item async for item in self] - def __await__(self): - return self._fetch_all().__await__() + def __await__(self) -> typing.Generator[typing.Sequence[_T], None, None]: + yield from self._fetch_all().__await__() @abc.abstractmethod async def __anext__(self) -> _T: ... -_EnumeratedT = typing.Tuple[int, _T] - - -class _EnumeratedLazyIterator(typing.Generic[_T], LazyIterator[_EnumeratedT]): +class _EnumeratedLazyIterator(typing.Generic[_T], LazyIterator[typing.Tuple[int, _T]]): __slots__ = ("_i", "_paginator") def __init__(self, paginator: LazyIterator[_T], *, start: int) -> None: @@ -217,10 +210,11 @@ class _BufferedLazyIterator(typing.Generic[_T], LazyIterator[_T]): __slots__ = ("_buffer",) def __init__(self) -> None: - self._buffer = _empty_generator() + empty_genexp = typing.cast(typing.Generator[_T, None, None], (_ for _ in ())) + self._buffer: typing.Optional[typing.Generator[_T, None, None]] = empty_genexp @abc.abstractmethod - async def _next_chunk(self) -> typing.Optional[typing.Generator[typing.Any, None, _T]]: + async def _next_chunk(self) -> typing.Optional[typing.Generator[_T, None, None]]: ... async def __anext__(self) -> _T: @@ -230,13 +224,13 @@ async def __anext__(self) -> _T: # history, we can use the same code and prefetch 100 without any # performance hit from it other than the JSON string response. try: - return next(self._buffer) + if self._buffer is not None: + return next(self._buffer) except StopIteration: self._buffer = await self._next_chunk() - if self._buffer is None: - self._complete() - else: + if self._buffer is not None: return next(self._buffer) + self._complete() # We use an explicit forward reference for this, since this breaks potential @@ -250,10 +244,12 @@ class MessageIterator(_BufferedLazyIterator["messages.Message"]): def __init__( self, app: app_.IRESTApp, - request_call: typing.Callable[..., typing.Coroutine[None, typing.Any, data_binding.JSONArray]], - channel_id: typing.Union[typing.SupportsInt, int], + request_call: typing.Callable[ + ..., typing.Coroutine[None, None, typing.Union[None, data_binding.JSONObject, data_binding.JSONArray]] + ], + channel_id: str, direction: str, - first_id: typing.Union[typing.SupportsInt, int], + first_id: str, ) -> None: super().__init__() self._app = app @@ -266,7 +262,9 @@ async def _next_chunk(self) -> typing.Optional[typing.Generator[messages.Message query = data_binding.StringMapBuilder() query.put(self._direction, self._first_id) query.put("limit", 100) - chunk = await self._request_call(self._route, query) + + raw_chunk = await self._request_call(self._route, query) + chunk = typing.cast(data_binding.JSONArray, raw_chunk) if not chunk: return None @@ -288,9 +286,11 @@ class ReactorIterator(_BufferedLazyIterator["users.User"]): def __init__( self, app: app_.IRESTApp, - request_call: typing.Callable[..., typing.Coroutine[None, typing.Any, data_binding.JSONArray]], - channel_id: typing.Union[typing.SupportsInt, int], - message_id: typing.Union[typing.SupportsInt, int], + request_call: typing.Callable[ + ..., typing.Coroutine[None, None, typing.Union[None, data_binding.JSONObject, data_binding.JSONArray]] + ], + channel_id: str, + message_id: str, emoji: str, ) -> None: super().__init__() @@ -303,7 +303,9 @@ async def _next_chunk(self) -> typing.Optional[typing.Generator[users.User, typi query = data_binding.StringMapBuilder() query.put("after", self._first_id) query.put("limit", 100) - chunk = await self._request_call(self._route, query=query) + + raw_chunk = await self._request_call(self._route, query=query) + chunk = typing.cast(data_binding.JSONArray, raw_chunk) if not chunk: return None @@ -323,9 +325,11 @@ class OwnGuildIterator(_BufferedLazyIterator["applications.OwnGuild"]): def __init__( self, app: app_.IRESTApp, - request_call: typing.Callable[..., typing.Coroutine[None, typing.Any, data_binding.JSONArray]], + request_call: typing.Callable[ + ..., typing.Coroutine[None, None, typing.Union[None, data_binding.JSONObject, data_binding.JSONArray]] + ], newest_first: bool, - first_id: typing.Union[typing.SupportsInt, int], + first_id: str, ) -> None: super().__init__() self._app = app @@ -339,7 +343,8 @@ async def _next_chunk(self) -> typing.Optional[typing.Generator[applications.Own query.put("before" if self._newest_first else "after", self._first_id) query.put("limit", 100) - chunk = await self._request_call(self._route, query=query) + raw_chunk = await self._request_call(self._route, query=query) + chunk = typing.cast(data_binding.JSONArray, raw_chunk) if not chunk: return None @@ -359,8 +364,10 @@ class MemberIterator(_BufferedLazyIterator["guilds.Member"]): def __init__( self, app: app_.IRESTApp, - request_call: typing.Callable[..., typing.Coroutine[None, typing.Any, data_binding.JSONArray]], - guild_id: typing.Union[typing.SupportsInt, int], + request_call: typing.Callable[ + ..., typing.Coroutine[None, None, typing.Union[None, data_binding.JSONObject, data_binding.JSONArray]] + ], + guild_id: str, ) -> None: super().__init__() self._route = routes.GET_GUILD_MEMBERS.compile(guild=guild_id) @@ -372,7 +379,9 @@ async def _next_chunk(self) -> typing.Optional[typing.Generator[guilds.Member, t query = data_binding.StringMapBuilder() query.put("after", self._first_id) query.put("limit", 100) - chunk = await self._request_call(self._route, query=query) + + raw_chunk = await self._request_call(self._route, query=query) + chunk = typing.cast(data_binding.JSONArray, raw_chunk) if not chunk: return None @@ -392,15 +401,17 @@ class AuditLogIterator(LazyIterator["audit_logs.AuditLog"]): def __init__( self, app: app_.IRESTApp, - request_call: typing.Callable[..., typing.Coroutine[None, typing.Any, data_binding.JSONObject]], - guild_id: typing.Union[typing.SupportsInt, int], - before: typing.Union[typing.SupportsInt, int], - user_id: typing.Union[typing.SupportsInt, int, undefined.Undefined], + request_call: typing.Callable[ + ..., typing.Coroutine[None, None, typing.Union[None, data_binding.JSONObject, data_binding.JSONArray]] + ], + guild_id: str, + before: str, + user_id: typing.Union[str, undefined.Undefined], action_type: typing.Union[int, undefined.Undefined], ) -> None: self._action_type = action_type self._app = app - self._first_id = before + self._first_id = str(before) self._request_call = request_call self._route = routes.GET_GUILD_AUDIT_LOGS.compile(guild=guild_id) self._user_id = user_id @@ -410,7 +421,9 @@ async def __anext__(self) -> audit_logs.AuditLog: query.put("limit", 100) query.put("user_id", self._user_id) query.put("event_type", self._action_type) - response = await self._request_call(self._route, query=query) + + raw_response = await self._request_call(self._route, query=query) + response = typing.cast(data_binding.JSONObject, raw_response) if not response["entries"]: raise StopAsyncIteration() diff --git a/hikari/net/rest.py b/hikari/net/rest.py index 957ca1798d..25bf914506 100644 --- a/hikari/net/rest.py +++ b/hikari/net/rest.py @@ -95,8 +95,9 @@ class REST(http_client.HTTPClient, component.IComponent): # pylint:disable=too- information useful for debugging this application. These logs will be written as DEBUG log entries. For most purposes, this should be left `False`. - token : str - The bot or bearer token. If no token is to be used, this can be `None`. + token : str or hikari.utilities.undefined.Undefined + The bot or bearer token. If no token is to be used, + this can be undefined. token_type : str or hikari.utilities.undefined.Undefined The type of token in use. If no token is used, this can be ignored and left to the default value. This can be `"Bot"` or `"Bearer"`. The @@ -145,12 +146,14 @@ def __init__( self._app = app if isinstance(token, undefined.Undefined): - self._token = None + full_token = None else: if isinstance(token_type, undefined.Undefined): token_type = "Bot" - self._token = f"{token_type.title()} {token}" if token is not None else None + full_token = f"{token_type.title()} {token}" + + self._token: typing.Optional[str] = full_token if isinstance(rest_url, undefined.Undefined): rest_url = _REST_API_URL @@ -169,13 +172,13 @@ async def _request( self, compiled_route: routes.CompiledRoute, *, - query: typing.Union[undefined.Undefined, data_binding.StringMapBuilder] = undefined.Undefined(), + query: typing.Union[undefined.Undefined, None, data_binding.StringMapBuilder] = undefined.Undefined(), body: typing.Union[ - undefined.Undefined, aiohttp.FormData, data_binding.JSONObjectBuilder, data_binding.JSONArray + undefined.Undefined, None, aiohttp.FormData, data_binding.JSONObjectBuilder, data_binding.JSONArray ] = undefined.Undefined(), reason: typing.Union[undefined.Undefined, str] = undefined.Undefined(), no_auth: bool = False, - ) -> typing.Optional[data_binding.JSONObject, data_binding.JSONArray, bytes, str]: + ) -> typing.Union[None, data_binding.JSONObject, data_binding.JSONArray]: # Make a ratelimit-protected HTTP request to a JSON endpoint and expect some form # of JSON response. If an error occurs, the response body is returned in the # raised exception as a bytes object. This is done since the differences between @@ -213,9 +216,9 @@ async def _request_once( self, compiled_route: routes.CompiledRoute, headers: data_binding.Headers, - body: typing.Optional[typing.Union[aiohttp.FormData, data_binding.JSONArray, data_binding.JSONObject]], - query: typing.Optional[typing.Dict[str, str]], - ) -> typing.Optional[data_binding.JSONObject, data_binding.JSONArray, bytes, str]: + body: typing.Union[None, aiohttp.FormData, data_binding.JSONArray, data_binding.JSONObject], + query: typing.Union[None, data_binding.StringMapBuilder], + ) -> typing.Union[None, data_binding.JSONObject, data_binding.JSONArray]: url = compiled_route.create_url(self._rest_url) # Wait for any ratelimits to finish. @@ -257,6 +260,7 @@ async def _request_once( # noinspection PyArgumentList status = http.HTTPStatus(response.status) + cls: typing.Type[errors.HikariError] if 400 <= status < 500: cls = errors.ClientHTTPErrorResponse elif 500 <= status < 600: @@ -351,12 +355,12 @@ async def _handle_rate_limits_for_response( def _generate_allowed_mentions( mentions_everyone: typing.Union[undefined.Undefined, bool], user_mentions: typing.Union[ - undefined.Undefined, typing.Collection[typing.Union[users.User, bases.UniqueObject]], bool + undefined.Undefined, typing.Collection[typing.Union[bases.UniqueObject, users.User]], bool ], role_mentions: typing.Union[ undefined.Undefined, typing.Collection[typing.Union[bases.UniqueObject, guilds.Role]], bool ], - ): + ) -> typing.Union[undefined.Undefined, data_binding.JSONObject]: parsed_mentions = [] allowed_mentions = {} @@ -365,32 +369,23 @@ def _generate_allowed_mentions( if user_mentions is True: parsed_mentions.append("users") - # This covers both `False` and an array of IDs/objs by using `user_mentions` or `EMPTY_SEQUENCE`, where a - # resultant empty list will mean that all user mentions are blacklisted. - elif not isinstance(user_mentions, undefined.Undefined): - allowed_mentions["users"] = list( - # dict.fromkeys is used to remove duplicate entries that would cause discord to return an error. - dict.fromkeys(str(int(m)) for m in user_mentions or ()) - ) - if len(allowed_mentions["users"]) > 100: - raise ValueError("Only up to 100 users can be provided.") + elif isinstance(user_mentions, typing.Collection): + # Duplicates are an error. + snowflakes = {str(int(u)) for u in user_mentions} + allowed_mentions["users"] = list(snowflakes) if role_mentions is True: parsed_mentions.append("roles") - # This covers both `False` and an array of IDs/objs by using `user_mentions` or `EMPTY_SEQUENCE`, where a - # resultant empty list will mean that all role mentions are blacklisted. - elif not isinstance(role_mentions, undefined.Undefined): - allowed_mentions["roles"] = list( - # dict.fromkeys is used to remove duplicate entries that would cause discord to return an error. - dict.fromkeys(str(int(m)) for m in role_mentions or ()) - ) - if len(allowed_mentions["roles"]) > 100: - raise ValueError("Only up to 100 roles can be provided.") + elif isinstance(role_mentions, typing.Collection): + snowflakes = {str(int(r)) for r in role_mentions} + allowed_mentions["roles"] = list(snowflakes) if not parsed_mentions and not allowed_mentions: return undefined.Undefined() - allowed_mentions["parse"] = parsed_mentions + if parsed_mentions: + allowed_mentions["parse"] = parsed_mentions + return allowed_mentions def _build_message_creation_form( @@ -437,37 +432,13 @@ async def fetch_channel( If an internal error occurs on Discord while handling the request. """ route = routes.GET_CHANNEL.compile(channel=channel) - response = await self._request(route) + raw_response = self._request(route) + response = typing.cast(data_binding.JSONObject, raw_response) return self._app.entity_factory.deserialize_channel(response) if typing.TYPE_CHECKING: _GuildChannelT = typing.TypeVar("_GuildChannelT", bound=channels.GuildChannel, contravariant=True) - # This overload just tells any static type checker that if we input, say, - # a GuildTextChannel, we should always expect a GuildTextChannel as the - # result. This only applies to actual Channel types... we cannot infer the - # result of calling this endpoint with a snowflake. - @typing.overload - async def edit_channel( - self, - channel: _GuildChannelT, - /, - *, - name: typing.Union[undefined.Undefined, str] = undefined.Undefined(), - position: typing.Union[undefined.Undefined, int] = undefined.Undefined(), - topic: typing.Union[undefined.Undefined, str] = undefined.Undefined(), - nsfw: typing.Union[undefined.Undefined, bool] = undefined.Undefined(), - bitrate: typing.Union[undefined.Undefined, int] = undefined.Undefined(), - user_limit: typing.Union[undefined.Undefined, int] = undefined.Undefined(), - rate_limit_per_user: typing.Union[undefined.Undefined, date.TimeSpan] = undefined.Undefined(), - permission_overwrites: typing.Union[ - undefined.Undefined, typing.Sequence[channels.PermissionOverwrite] - ] = undefined.Undefined(), - parent_category: typing.Union[undefined.Undefined, channels.GuildCategory] = undefined.Undefined(), - reason: typing.Union[undefined.Undefined, str] = undefined.Undefined(), - ) -> _GuildChannelT: - """Edit a guild channel, given an existing guild channel object.""" - async def edit_channel( self, channel: typing.Union[channels.PartialChannel, bases.UniqueObject], @@ -551,7 +522,8 @@ async def edit_channel( conversion=self._app.entity_factory.serialize_permission_overwrite, ) - response = await self._request(route, body=body, reason=reason) + raw_response = await self._request(route, body=body, reason=reason) + response = typing.cast(data_binding.JSONObject, raw_response) return self._app.entity_factory.deserialize_channel(response) async def delete_channel(self, channel: typing.Union[channels.PartialChannel, bases.UniqueObject], /) -> None: @@ -598,8 +570,8 @@ async def edit_permission_overwrites( self, channel: typing.Union[channels.GuildChannel, bases.UniqueObject], target: typing.Union[int, str, snowflake.Snowflake], - target_type: typing.Union[channels.PermissionOverwriteType, str], *, + target_type: typing.Union[channels.PermissionOverwriteType, str], allow: typing.Union[undefined.Undefined, permissions_.Permission] = undefined.Undefined(), deny: typing.Union[undefined.Undefined, permissions_.Permission] = undefined.Undefined(), reason: typing.Union[undefined.Undefined, str] = undefined.Undefined(), @@ -730,7 +702,8 @@ async def fetch_channel_invites( If an internal error occurs on Discord while handling the request. """ route = routes.GET_CHANNEL_INVITES.compile(channel=channel) - response = await self._request(route) + raw_response = await self._request(route) + response = typing.cast(data_binding.JSONArray, raw_response) return data_binding.cast_json_array(response, self._app.entity_factory.deserialize_invite_with_metadata) async def create_invite( @@ -796,7 +769,8 @@ async def create_invite( body.put("unique", unique) body.put_snowflake("target_user_id", target_user) body.put("target_user_type", target_user_type) - response = await self._request(route, body=body, reason=reason) + raw_response = await self._request(route, body=body, reason=reason) + response = typing.cast(data_binding.JSONObject, raw_response) return self._app.entity_factory.deserialize_invite_with_metadata(response) def trigger_typing( @@ -863,7 +837,8 @@ async def fetch_pins( If an internal error occurs on Discord while handling the request. """ route = routes.GET_CHANNEL_PINS.compile(channel=channel) - response = await self._request(route) + raw_response = await self._request(route) + response = typing.cast(data_binding.JSONArray, raw_response) return data_binding.cast_json_array(response, self._app.entity_factory.deserialize_message) async def pin_message( @@ -918,7 +893,7 @@ async def unpin_message( hikari.errors.Unauthorized If you are unauthorized to make the request (invalid/missing token). hikari.errors.Forbidden - If you lack permissions pin messages in the given channel. + If you lack permissions to pin messages in the given channel. hikari.errors.NotFound If the channel is not found or the message is not a pinned message in the given channel. @@ -1030,7 +1005,7 @@ def fetch_messages( if isinstance(timestamp, datetime.datetime): timestamp = snowflake.Snowflake.from_datetime(timestamp) - return iterators.MessageIterator(self._app, self._request, channel, direction, timestamp,) + return iterators.MessageIterator(self._app, self._request, str(int(channel)), direction, str(timestamp)) async def fetch_message( self, @@ -1067,7 +1042,8 @@ async def fetch_message( If an internal error occurs on Discord while handling the request. """ route = routes.GET_CHANNEL_MESSAGE.compile(channel=channel, message=message) - response = await self._request(route) + raw_response = await self._request(route) + response = typing.cast(data_binding.JSONObject, raw_response) return self._app.entity_factory.deserialize_message(response) async def create_message( @@ -1153,10 +1129,10 @@ async def create_message( if not isinstance(embed, undefined.Undefined): attachments.extend(embed.assets_to_upload) - response = await self._request( + raw_response = await self._request( route, body=self._build_message_creation_form(body, attachments) if attachments else body ) - + response = typing.cast(data_binding.JSONObject, raw_response) return self._app.entity_factory.deserialize_message(response) async def edit_message( @@ -1223,7 +1199,8 @@ async def edit_message( body.put("embed", embed, conversion=self._app.entity_factory.serialize_embed) body.put("flags", flags) body.put("allowed_mentions", self._generate_allowed_mentions(mentions_everyone, user_mentions, role_mentions)) - response = await self._request(route, body=body) + raw_response = await self._request(route, body=body) + response = typing.cast(data_binding.JSONObject, raw_response) return self._app.entity_factory.deserialize_message(response) async def delete_message( @@ -1401,8 +1378,8 @@ def fetch_reactions_for_emoji( return iterators.ReactorIterator( app=self._app, request_call=self._request, - channel_id=channel, - message_id=message, + channel_id=str(int(channel)), + message_id=str(int(message)), emoji=emoji.url_name if isinstance(emoji, emojis.KnownCustomEmoji) else str(emoji), ) @@ -1420,7 +1397,8 @@ async def create_webhook( if not isinstance(avatar, undefined.Undefined): body.put("avatar", await avatar.fetch_data_uri()) - response = await self._request(route, body=body, reason=reason) + raw_response = await self._request(route, body=body, reason=reason) + response = typing.cast(data_binding.JSONObject, raw_response) return self._app.entity_factory.deserialize_webhook(response) async def fetch_webhook( @@ -1434,21 +1412,24 @@ async def fetch_webhook( route = routes.GET_WEBHOOK.compile(webhook=webhook) else: route = routes.GET_WEBHOOK_WITH_TOKEN.compile(webhook=webhook, token=token) - response = await self._request(route) + raw_response = await self._request(route) + response = typing.cast(data_binding.JSONObject, raw_response) return self._app.entity_factory.deserialize_webhook(response) async def fetch_channel_webhooks( self, channel: typing.Union[channels.TextChannel, bases.UniqueObject], / ) -> typing.Sequence[webhooks.Webhook]: route = routes.GET_CHANNEL_WEBHOOKS.compile(channel=channel) - response = await self._request(route) + raw_response = await self._request(route) + response = typing.cast(data_binding.JSONArray, raw_response) return data_binding.cast_json_array(response, self._app.entity_factory.deserialize_webhook) async def fetch_guild_webhooks( self, guild: typing.Union[guilds.Guild, bases.UniqueObject], / ) -> typing.Sequence[webhooks.Webhook]: route = routes.GET_GUILD_WEBHOOKS.compile(channel=guild) - response = await self._request(route) + raw_response = await self._request(route) + response = typing.cast(data_binding.JSONArray, raw_response) return data_binding.cast_json_array(response, self._app.entity_factory.deserialize_webhook) async def edit_webhook( @@ -1471,9 +1452,10 @@ async def edit_webhook( body.put("name", name) body.put_snowflake("channel", channel) if not isinstance(avatar, undefined.Undefined): - body.put("avatar", await avatar.fetch_data_uri()) + body.put("avatar", await avatar.fetch_data_uri() if avatar is not None else None) - response = await self._request(route, body=body, reason=reason) + raw_response = await self._request(route, body=body, reason=reason) + response = typing.cast(data_binding.JSONObject, raw_response) return self._app.entity_factory.deserialize_webhook(response) async def delete_webhook( @@ -1528,28 +1510,31 @@ async def execute_webhook( body.put("tts", tts) body.put("wait", True) - response = await self._request( + raw_response = await self._request( route, body=self._build_message_creation_form(body, attachments) if attachments else body, no_auth=no_auth, ) - + response = typing.cast(data_binding.JSONObject, raw_response) return self._app.entity_factory.deserialize_message(response) async def fetch_gateway_url(self) -> str: route = routes.GET_GATEWAY.compile() # This doesn't need authorization. - response = await self._request(route, no_auth=True) + raw_response = await self._request(route, no_auth=True) + response = typing.cast(typing.Mapping[str, str], raw_response) return response["url"] async def fetch_gateway_bot(self) -> gateway.GatewayBot: route = routes.GET_GATEWAY_BOT.compile() - response = await self._request(route) + raw_response = await self._request(route) + response = typing.cast(data_binding.JSONObject, raw_response) return self._app.entity_factory.deserialize_gateway_bot(response) async def fetch_invite(self, invite: typing.Union[invites.Invite, str]) -> invites.Invite: route = routes.GET_INVITE.compile(invite_code=invite if isinstance(invite, str) else invite.code) query = data_binding.StringMapBuilder() query.put("with_counts", True) - response = await self._request(route, query=query) + raw_response = await self._request(route, query=query) + response = typing.cast(data_binding.JSONObject, raw_response) return self._app.entity_factory.deserialize_invite(response) async def delete_invite(self, invite: typing.Union[invites.Invite, str]) -> None: @@ -1558,28 +1543,33 @@ async def delete_invite(self, invite: typing.Union[invites.Invite, str]) -> None async def fetch_my_user(self) -> users.OwnUser: route = routes.GET_MY_USER.compile() - response = await self._request(route) + raw_response = await self._request(route) + response = typing.cast(data_binding.JSONObject, raw_response) return self._app.entity_factory.deserialize_my_user(response) async def edit_my_user( self, *, username: typing.Union[undefined.Undefined, str] = undefined.Undefined(), - avatar: typing.Union[undefined.Undefined, files.BaseStream] = undefined.Undefined(), + avatar: typing.Union[undefined.Undefined, None, files.BaseStream] = undefined.Undefined(), ) -> users.OwnUser: route = routes.PATCH_MY_USER.compile() body = data_binding.JSONObjectBuilder() body.put("username", username) - if not isinstance(username, undefined.Undefined): + if isinstance(avatar, files.BaseStream): body.put("avatar", await avatar.fetch_data_uri()) + else: + body.put("avatar", avatar) - response = await self._request(route, body=body) + raw_response = await self._request(route, body=body) + response = typing.cast(data_binding.JSONObject, raw_response) return self._app.entity_factory.deserialize_my_user(response) async def fetch_my_connections(self) -> typing.Sequence[applications.OwnConnection]: route = routes.GET_MY_CONNECTIONS.compile() - response = await self._request(route) + raw_response = await self._request(route) + response = typing.cast(data_binding.JSONArray, raw_response) return data_binding.cast_json_array(response, self._app.entity_factory.deserialize_own_connection) def fetch_my_guilds( @@ -1595,7 +1585,7 @@ def fetch_my_guilds( elif isinstance(start_at, datetime.datetime): start_at = snowflake.Snowflake.from_datetime(start_at) - return iterators.OwnGuildIterator(self._app, self._request, newest_first, start_at) + return iterators.OwnGuildIterator(self._app, self._request, newest_first, str(start_at)) async def leave_guild(self, guild: typing.Union[guilds.Guild, bases.UniqueObject], /) -> None: route = routes.DELETE_MY_GUILD.compile(guild=guild) @@ -1605,12 +1595,14 @@ async def create_dm_channel(self, user: typing.Union[users.User, bases.UniqueObj route = routes.POST_MY_CHANNELS.compile() body = data_binding.JSONObjectBuilder() body.put_snowflake("recipient_id", user) - response = await self._request(route, body=body) + raw_response = await self._request(route, body=body) + response = typing.cast(data_binding.JSONObject, raw_response) return self._app.entity_factory.deserialize_dm_channel(response) async def fetch_application(self) -> applications.Application: route = routes.GET_MY_APPLICATION.compile() - response = await self._request(route) + raw_response = await self._request(route) + response = typing.cast(data_binding.JSONObject, raw_response) return self._app.entity_factory.deserialize_application(response) async def add_user_to_guild( @@ -1634,7 +1626,8 @@ async def add_user_to_guild( body.put("deaf", deaf) body.put_snowflake_array("roles", roles) - if (response := await self._request(route, body=body)) is not None: + if (raw_response := await self._request(route, body=body)) is not None: + response = typing.cast(data_binding.JSONObject, raw_response) return self._app.entity_factory.deserialize_member(response) else: # User already is in the guild. @@ -1642,12 +1635,14 @@ async def add_user_to_guild( async def fetch_voice_regions(self) -> typing.Sequence[voices.VoiceRegion]: route = routes.GET_VOICE_REGIONS.compile() - response = await self._request(route) + raw_response = await self._request(route) + response = typing.cast(data_binding.JSONArray, raw_response) return data_binding.cast_json_array(response, self._app.entity_factory.deserialize_voice_region) async def fetch_user(self, user: typing.Union[users.User, bases.UniqueObject]) -> users.User: route = routes.GET_USER.compile(user=user) - response = await self._request(route) + raw_response = await self._request(route) + response = typing.cast(data_binding.JSONObject, raw_response) return self._app.entity_factory.deserialize_user(response) def fetch_audit_log( @@ -1659,10 +1654,17 @@ def fetch_audit_log( user: typing.Union[undefined.Undefined, users.User, bases.UniqueObject] = undefined.Undefined(), event_type: typing.Union[undefined.Undefined, audit_logs.AuditLogEventType] = undefined.Undefined(), ) -> iterators.LazyIterator[audit_logs.AuditLog]: + guild = str(int(guild)) + if isinstance(before, undefined.Undefined): - before = snowflake.Snowflake.max() + before = str(snowflake.Snowflake.max()) elif isinstance(before, datetime.datetime): - before = snowflake.Snowflake.from_datetime(before) + before = str(snowflake.Snowflake.from_datetime(before)) + else: + before = str(int(before)) + + if not isinstance(user, undefined.Undefined): + user = str(int(user)) return iterators.AuditLogIterator(self._app, self._request, guild, before, user, event_type) @@ -1675,15 +1677,17 @@ async def fetch_emoji( route = routes.GET_GUILD_EMOJI.compile( guild=guild, emoji=emoji.url_name if isinstance(emoji, emojis.Emoji) else emoji, ) - response = await self._request(route) + raw_response = await self._request(route) + response = typing.cast(data_binding.JSONObject, raw_response) return self._app.entity_factory.deserialize_known_custom_emoji(response) async def fetch_guild_emojis( self, guild: typing.Union[guilds.Guild, bases.UniqueObject], / ) -> typing.Set[emojis.KnownCustomEmoji]: route = routes.GET_GUILD_EMOJIS.compile(guild=guild) - response = await self._request(route) - return data_binding.cast_json_array(response, self._app.entity_factory.deserialize_known_custom_emoji, set) + raw_response = await self._request(route) + response = typing.cast(data_binding.JSONArray, raw_response) + return set(data_binding.cast_json_array(response, self._app.entity_factory.deserialize_known_custom_emoji)) async def create_emoji( self, @@ -1704,8 +1708,8 @@ async def create_emoji( body.put_snowflake_array("roles", roles) - response = await self._request(route, body=body, reason=reason) - + raw_response = await self._request(route, body=body, reason=reason) + response = typing.cast(data_binding.JSONObject, raw_response) return self._app.entity_factory.deserialize_known_custom_emoji(response) async def edit_emoji( @@ -1727,8 +1731,8 @@ async def edit_emoji( body.put("name", name) body.put_snowflake_array("roles", roles) - response = await self._request(route, body=body, reason=reason) - + raw_response = await self._request(route, body=body, reason=reason) + response = typing.cast(data_binding.JSONObject, raw_response) return self._app.entity_factory.deserialize_known_custom_emoji(response) async def delete_emoji( @@ -1743,19 +1747,21 @@ async def delete_emoji( ) await self._request(route) - def create_guild(self, name: str, /) -> rest_utils.GuildBuilder: + def guild_builder(self, name: str, /) -> rest_utils.GuildBuilder: return rest_utils.GuildBuilder(app=self._app, name=name, request_call=self._request) async def fetch_guild(self, guild: typing.Union[guilds.Guild, bases.UniqueObject], /) -> guilds.Guild: route = routes.GET_GUILD.compile(guild=guild) - response = await self._request(route) + raw_response = await self._request(route) + response = typing.cast(data_binding.JSONObject, raw_response) return self._app.entity_factory.deserialize_guild(response) async def fetch_guild_preview( self, guild: typing.Union[guilds.PartialGuild, bases.UniqueObject], / ) -> guilds.GuildPreview: route = routes.GET_GUILD_PREVIEW.compile(guild=guild) - response = await self._request(route) + raw_response = await self._request(route) + response = typing.cast(data_binding.JSONObject, raw_response) return self._app.entity_factory.deserialize_guild_preview(response) async def edit_guild( @@ -1810,8 +1816,8 @@ async def edit_guild( if not isinstance(banner, undefined.Undefined): body.put("banner", await banner.fetch_data_uri()) - response = await self._request(route, body=body, reason=reason) - + raw_response = await self._request(route, body=body, reason=reason) + response = typing.cast(data_binding.JSONObject, raw_response) return self._app.entity_factory.deserialize_guild(response) async def delete_guild(self, guild: typing.Union[guilds.Guild, bases.UniqueObject]) -> None: @@ -1822,7 +1828,8 @@ async def fetch_guild_channels( self, guild: typing.Union[guilds.Guild, bases.UniqueObject] ) -> typing.Sequence[channels.GuildChannel]: route = routes.GET_GUILD_CHANNELS.compile(guild=guild) - response = await self._request(route) + raw_response = await self._request(route) + response = typing.cast(data_binding.JSONArray, raw_response) channel_sequence = data_binding.cast_json_array(response, self._app.entity_factory.deserialize_channel) # Will always be guild channels unless Discord messes up severely on something! return typing.cast(typing.Sequence[channels.GuildChannel], channel_sequence) @@ -1972,7 +1979,8 @@ async def _create_guild_channel( conversion=self._app.entity_factory.serialize_permission_overwrite, ) - response = await self._request(route, body=body, reason=reason) + raw_response = await self._request(route, body=body, reason=reason) + response = typing.cast(data_binding.JSONObject, raw_response) channel = self._app.entity_factory.deserialize_channel(response) return typing.cast(channels.GuildChannel, channel) @@ -1989,13 +1997,14 @@ async def fetch_member( self, guild: typing.Union[guilds.Guild, bases.UniqueObject], user: typing.Union[users.User, bases.UniqueObject], ) -> guilds.Member: route = routes.GET_GUILD_MEMBER.compile(guild=guild, user=user) - response = await self._request(route) + raw_response = await self._request(route) + response = typing.cast(data_binding.JSONObject, raw_response) return self._app.entity_factory.deserialize_member(response) def fetch_members( self, guild: typing.Union[guilds.Guild, bases.UniqueObject], ) -> iterators.LazyIterator[guilds.Member]: - return iterators.MemberIterator(self._app, self._request, guild) + return iterators.MemberIterator(self._app, self._request, str(int(guild))) async def edit_member( self, @@ -2095,21 +2104,24 @@ async def fetch_ban( self, guild: typing.Union[guilds.Guild, bases.UniqueObject], user: typing.Union[users.User, bases.UniqueObject], ) -> guilds.GuildMemberBan: route = routes.GET_GUILD_BAN.compile(guild=guild, user=user) - response = await self._request(route) + raw_response = await self._request(route) + response = typing.cast(data_binding.JSONObject, raw_response) return self._app.entity_factory.deserialize_guild_member_ban(response) async def fetch_bans( self, guild: typing.Union[guilds.Guild, bases.UniqueObject], / ) -> typing.Sequence[guilds.GuildMemberBan]: route = routes.GET_GUILD_BANS.compile(guild=guild) - response = await self._request(route) + raw_response = await self._request(route) + response = typing.cast(data_binding.JSONArray, raw_response) return data_binding.cast_json_array(response, self._app.entity_factory.deserialize_guild_member_ban) async def fetch_roles( self, guild: typing.Union[guilds.Guild, bases.UniqueObject], / ) -> typing.Sequence[guilds.Role]: route = routes.GET_GUILD_ROLES.compile(guild=guild) - response = await self._request(route) + raw_response = await self._request(route) + response = typing.cast(data_binding.JSONArray, raw_response) return data_binding.cast_json_array(response, self._app.entity_factory.deserialize_role) async def create_role( @@ -2137,7 +2149,8 @@ async def create_role( body.put("hoist", hoist) body.put("mentionable", mentionable) - response = await self._request(route, body=body, reason=reason) + raw_response = await self._request(route, body=body, reason=reason) + response = typing.cast(data_binding.JSONObject, raw_response) return self._app.entity_factory.deserialize_role(response) async def reposition_roles( @@ -2175,7 +2188,8 @@ async def edit_role( body.put("hoist", hoist) body.put("mentionable", mentionable) - response = await self._request(route, body=body, reason=reason) + raw_response = await self._request(route, body=body, reason=reason) + response = typing.cast(data_binding.JSONObject, raw_response) return self._app.entity_factory.deserialize_role(response) async def delete_role( @@ -2192,7 +2206,8 @@ async def estimate_guild_prune_count( route = routes.GET_GUILD_PRUNE.compile(guild=guild) query = data_binding.StringMapBuilder() query.put("days", days) - response = await self._request(route, query=query) + raw_response = await self._request(route, query=query) + response = typing.cast(data_binding.JSONObject, raw_response) return int(response["pruned"]) async def begin_guild_prune( @@ -2206,28 +2221,32 @@ async def begin_guild_prune( query = data_binding.StringMapBuilder() query.put("compute_prune_count", True) query.put("days", days) - response = await self._request(route, query=query, reason=reason) + raw_response = await self._request(route, query=query, reason=reason) + response = typing.cast(data_binding.JSONObject, raw_response) return int(response["pruned"]) async def fetch_guild_voice_regions( self, guild: typing.Union[guilds.Guild, bases.UniqueObject], / ) -> typing.Sequence[voices.VoiceRegion]: route = routes.GET_GUILD_VOICE_REGIONS.compile(guild=guild) - response = await self._request(route) + raw_response = await self._request(route) + response = typing.cast(data_binding.JSONArray, raw_response) return data_binding.cast_json_array(response, self._app.entity_factory.deserialize_voice_region) async def fetch_guild_invites( self, guild: typing.Union[guilds.Guild, bases.UniqueObject], / ) -> typing.Sequence[invites.InviteWithMetadata]: route = routes.GET_GUILD_INVITES.compile(guild=guild) - response = await self._request(route) + raw_response = await self._request(route) + response = typing.cast(data_binding.JSONArray, raw_response) return data_binding.cast_json_array(response, self._app.entity_factory.deserialize_invite_with_metadata) async def fetch_integrations( self, guild: typing.Union[guilds.Guild, bases.UniqueObject], / ) -> typing.Sequence[guilds.Integration]: route = routes.GET_GUILD_INTEGRATIONS.compile(guild=guild) - response = await self._request(route) + raw_response = await self._request(route) + response = typing.cast(data_binding.JSONArray, raw_response) return data_binding.cast_json_array(response, self._app.entity_factory.deserialize_integration) async def edit_integration( @@ -2268,7 +2287,8 @@ async def sync_integration( async def fetch_widget(self, guild: typing.Union[guilds.Guild, bases.UniqueObject], /) -> guilds.GuildWidget: route = routes.GET_GUILD_WIDGET.compile(guild=guild) - response = await self._request(route) + raw_response = await self._request(route) + response = typing.cast(data_binding.JSONObject, raw_response) return self._app.entity_factory.deserialize_guild_widget(response) async def edit_widget( @@ -2291,10 +2311,12 @@ async def edit_widget( elif not isinstance(channel, undefined.Undefined): body.put_snowflake("channel", channel) - response = await self._request(route, body=body, reason=reason) + raw_response = await self._request(route, body=body, reason=reason) + response = typing.cast(data_binding.JSONObject, raw_response) return self._app.entity_factory.deserialize_guild_widget(response) async def fetch_vanity_url(self, guild: typing.Union[guilds.Guild, bases.UniqueObject], /) -> invites.VanityURL: route = routes.GET_GUILD_VANITY_URL.compile(guild=guild) - response = await self._request(route) + raw_response = await self._request(route) + response = typing.cast(data_binding.JSONObject, raw_response) return self._app.entity_factory.deserialize_vanity_url(response) diff --git a/hikari/net/rest_utils.py b/hikari/net/rest_utils.py index 5a08c078f4..31b242dd7d 100644 --- a/hikari/net/rest_utils.py +++ b/hikari/net/rest_utils.py @@ -22,8 +22,7 @@ """ from __future__ import annotations -# Do not document anything in here. -__all__: typing.List[str] = [] +__all__: typing.List[str] = ["TypingIndicator", "GuildBuilder"] import asyncio import contextlib @@ -63,11 +62,13 @@ class TypingIndicator: def __init__( self, channel: typing.Union[channels.TextChannel, bases.UniqueObject], - request_call: typing.Callable[..., typing.Coroutine[None, typing.Any, data_binding.JSONObject]], + request_call: typing.Callable[ + ..., typing.Coroutine[None, None, typing.Union[None, data_binding.JSONObject, data_binding.JSONArray]] + ], ) -> None: self._channel = channel self._request_call = request_call - self._task = None + self._task: typing.Optional[asyncio.Task[None]] = None def __await__(self) -> typing.Generator[None, typing.Any, None]: route = routes.POST_CHANNEL_TYPING.compile(channel=self._channel) @@ -82,44 +83,165 @@ async def __aenter__(self) -> None: self._task = asyncio.create_task(self._keep_typing(), name=f"repeatedly trigger typing in {self._channel}") async def __aexit__(self, ex_t: typing.Type[Exception], ex_v: Exception, exc_tb: types.TracebackType) -> None: - self._task.cancel() - # Prevent reusing this object by not setting it back to None. - self._task = NotImplemented + # This will always be true, but this keeps MyPy quiet. + if self._task is not None: + self._task.cancel() + # Prevent reusing this object by not setting it back to None. + self._task = NotImplemented async def _keep_typing(self) -> None: with contextlib.suppress(asyncio.CancelledError): while True: - await asyncio.gather(self, asyncio.sleep(9.9), return_exceptions=True) + # Use slightly less than 10s to ensure latency does not + # cause the typing indicator to stop showing for a split + # second if the request is slow to execute. + await asyncio.gather(self, asyncio.sleep(9)) -# TODO: document! @attr.s(auto_attribs=True, kw_only=True, slots=True) class GuildBuilder: + """A helper class used to construct a prototype for a guild. + + This is used to create a guild in a tidy way using the REST API, since + the logic behind creating a guild on an API level is somewhat confusing + and detailed. + + !!! note + This is a helper class that is used by `hikari.net.rest.REST`. + You should only ever need to use instances of this class that are + produced by that API, thus, any details about the constructor are + omitted from the following examples for brevity. + + Examples + -------- + Creating an empty guild. + + ```py + guild = await rest.guild_builder("My Server!").create() + ``` + + Creating a guild with an icon + + ```py + from hikari.models.files import WebResourceStream + + guild_builder = rest.guild_builder("My Server!") + guild_builder.icon = WebResourceStream("cat.png", "http://...") + guild = await guild_builder.create() + ``` + + Adding roles to your guild. + + ```py + from hikari.models.permissions import Permission + + guild_builder = rest.guild_builder("My Server!") + + everyone_role_id = guild_builder.add_role("@everyone") + admin_role_id = guild_builder.add_role("Admins", permissions=Permission.ADMINISTRATOR) + + await guild_builder.create() + ``` + + !!! warning + The first role must always be the `@everyone` role. + + !!! note + Functions that return a `hikari.utilities.snowflake.Snowflake` do + **not** provide the final ID that the object will have once the + API call is made. The returned IDs are only able to be used to + re-reference particular objects while building the guild format. + + This is provided to allow creation of channels within categories, + and to provide permission overwrites. + + Adding a text channel to your guild. + + ```py + guild_builder = rest.guild_builder("My Server!") + + category_id = guild_builder.add_category("My safe place") + channel_id = guild_builder.add_text_channel("general", parent_id=category_id) + + await guild_builder.create() + ``` + """ + + # Required arguments. _app: app_.IRESTApp + _name: str + + # Optional args that we kept hidden. _channels: typing.MutableSequence[data_binding.JSONObject] = attr.ib(factory=list) _counter: int = 0 - _name: typing.Union[undefined.Undefined, str] - _request_call: typing.Callable[..., typing.Coroutine[None, typing.Any, data_binding.JSONObject]] + _request_call: typing.Callable[ + ..., typing.Coroutine[None, None, typing.Union[None, data_binding.JSONObject, data_binding.JSONArray]] + ] _roles: typing.MutableSequence[data_binding.JSONObject] = attr.ib(factory=list) + default_message_notifications: typing.Union[ undefined.Undefined, guilds.GuildMessageNotificationsLevel ] = undefined.Undefined() + """Default message notification level that can be overwritten. + + If not overridden, this will use the Discord default level. + """ + explicit_content_filter_level: typing.Union[ undefined.Undefined, guilds.GuildExplicitContentFilterLevel ] = undefined.Undefined() + """Explicit content filter level that can be overwritten. + + If not overridden, this will use the Discord default level. + """ + icon: typing.Union[undefined.Undefined, files.BaseStream] = undefined.Undefined() + """Guild icon to use that can be overwritten. + + If not overridden, the guild will not have an icon. + """ + region: typing.Union[undefined.Undefined, str] = undefined.Undefined() + """Guild voice channel region to use that can be overwritten. + + If not overridden, the guild will use the default voice region for Discord. + """ + verification_level: typing.Union[undefined.Undefined, guilds.GuildVerificationLevel] = undefined.Undefined() + """Verification level required to join the guild that can be overwritten. + + If not overridden, the guild will use the default verification level for + Discord. + """ @property def name(self) -> str: - # Read-only! + """Guild name.""" return self._name - def __await__(self) -> typing.Generator[guilds.Guild, None, typing.Any]: - yield from self.create().__await__() - async def create(self) -> guilds.Guild: + """Send the request to Discord to create the guild. + + The application user will be added to this guild as soon as it is + created. All IDs that were provided when building this guild will + become invalid and will be replaced with real IDs. + + Returns + ------- + hikari.models.guilds.Guild + The created guild. + + Raises + ------ + hikari.errors.BadRequest + If any values set in the guild builder are invalid. + hikari.errors.Unauthorized + If you are unauthorized to make the request (invalid/missing token). + hikari.errors.Forbidden + If you are already in 10 guilds. + hikari.errors.ServerHTTPErrorResponse + If an internal error occurs on Discord while handling the request. + """ route = routes.POST_GUILDS.compile() payload = data_binding.JSONObjectBuilder() payload.put("name", self.name) @@ -133,7 +255,8 @@ async def create(self) -> guilds.Guild: if not isinstance(self.icon, undefined.Undefined): payload.put("icon", await self.icon.fetch_data_uri()) - response = await self._request_call(route, body=payload) + raw_response = await self._request_call(route, body=payload) + response = typing.cast(data_binding.JSONObject, raw_response) return self._app.entity_factory.deserialize_guild(response) def add_role( @@ -148,6 +271,55 @@ def add_role( permissions: typing.Union[undefined.Undefined, permissions_.Permission] = undefined.Undefined(), position: typing.Union[undefined.Undefined, int] = undefined.Undefined(), ) -> snowflake_.Snowflake: + """Create a role. + + !!! note + The first role you create must always be the `@everyone` role, and + must have that name. This role will ignore the `hoisted`, `color`, + `colour`, `mentionable` and `position` parameters. + + Parameters + ---------- + name : str + The role name. + color : hikari.utilities.undefined.Undefined or hikari.models.color.Color + The colour of the role to use. If unspecified, then the default + colour is used instead. + colour : hikari.utilities.undefined.Undefined or hikari.models.color.Color + Alias for the `color` parameter for non-american users. + hoisted : hikari.utilities.undefined.Undefined or bool + If `True`, the role will show up in the user sidebar in a separate + category if it is the highest hoisted role. If `False`, or + unspecified, then this will not occur. + mentionable : hikari.utilities.undefined.Undefined or bool + If `True`, then the role will be able to be mentioned. + permissions : hikari.utilities.undefined.Undefined or hikari.models.permissions.Permission + The optional permissions to enforce on the role. If unspecified, + the default permissions for roles will be used. + + !!! note + The default permissions are **NOT** the same as providing + zero permissions. To set no permissions, you should + pass `Permission(0)` explicitly. + position : hikari.utilities.undefined.Undefined or int + If specified, the position to place the role in. + + Returns + ------- + hikari.utilities.snowflake.Snowflake + The dummy ID for this role that can be used temporarily to refer + to this object while designing the guild layout. + + When the guild is created, this will be replaced with a different + ID. + + Raises + ------ + ValueError + If you are defining the first role, but did not name it `@everyone`. + TypeError + If you specify both `color` and `colour` together. + """ if len(self._roles) == 0 and name != "@everyone": raise ValueError("First role must always be the @everyone role") @@ -178,6 +350,30 @@ def add_category( ] = undefined.Undefined(), nsfw: typing.Union[undefined.Undefined, bool] = undefined.Undefined(), ) -> snowflake_.Snowflake: + """Create a category channel. + + Parameters + ---------- + name : str + The name of the category. + position : hikari.utilities.undefined.Undefined or int + The position to place the category in, if specified. + permission_overwrites : hikari.utilities.undefined.Undefined or typing.Collection[hikari.models.channels.PermissionOverwrite] + If defined, a collection of one or more + `hikari.models.channels.PermissionOverwrite` objects. + nsfw : hikari.utilities.undefined.Undefined or bool + If `True`, the channel is marked as NSFW and only users over + 18 years of age should be given access. + + Returns + ------- + hikari.utilities.snowflake.Snowflake + The dummy ID for this channel that can be used temporarily to refer + to this object while designing the guild layout. + + When the guild is created, this will be replaced with a different + ID. + """ snowflake = self._new_snowflake() payload = data_binding.JSONObjectBuilder() payload.put_snowflake("id", snowflake) @@ -200,7 +396,7 @@ def add_text_channel( name: str, /, *, - parent_id: snowflake_.Snowflake = undefined.Undefined(), + parent_id: typing.Union[undefined.Undefined, snowflake_.Snowflake] = undefined.Undefined(), topic: typing.Union[undefined.Undefined, str] = undefined.Undefined(), rate_limit_per_user: typing.Union[undefined.Undefined, date.TimeSpan] = undefined.Undefined(), position: typing.Union[undefined.Undefined, int] = undefined.Undefined(), @@ -209,6 +405,39 @@ def add_text_channel( ] = undefined.Undefined(), nsfw: typing.Union[undefined.Undefined, bool] = undefined.Undefined(), ) -> snowflake_.Snowflake: + """Create a text channel. + + Parameters + ---------- + name : str + The name of the category. + position : hikari.utilities.undefined.Undefined or int + The position to place the category in, if specified. + permission_overwrites : hikari.utilities.undefined.Undefined or typing.Collection[hikari.models.channels.PermissionOverwrite] + If defined, a collection of one or more + `hikari.models.channels.PermissionOverwrite` objects. + nsfw : hikari.utilities.undefined.Undefined or bool + If `True`, the channel is marked as NSFW and only users over + 18 years of age should be given access. + parent_id : hikari.utilities.undefined.Undefined or hikari.utilities.snowflake.Snowflake + If defined, should be a snowflake ID of a category channel + that was made with this builder. If provided, this channel will + become a child channel of that category. + topic : hikari.utilities.undefined.Undefined or str + If specified, the topic to set on the channel. + rate_limit_per_user : hikari.utilities.undefined.Undefined or hikari.utilities.date.TimeSpan + If specified, the time to wait between allowing consecutive messages + to be sent. If not specified, this will not be enabled. + + Returns + ------- + hikari.utilities.snowflake.Snowflake + The dummy ID for this channel that can be used temporarily to refer + to this object while designing the guild layout. + + When the guild is created, this will be replaced with a different + ID. + """ snowflake = self._new_snowflake() payload = data_binding.JSONObjectBuilder() payload.put_snowflake("id", snowflake) @@ -234,7 +463,7 @@ def add_voice_channel( name: str, /, *, - parent_id: snowflake_.Snowflake = undefined.Undefined(), + parent_id: typing.Union[undefined.Undefined, snowflake_.Snowflake] = undefined.Undefined(), bitrate: typing.Union[undefined.Undefined, int] = undefined.Undefined(), position: typing.Union[undefined.Undefined, int] = undefined.Undefined(), permission_overwrites: typing.Union[ @@ -243,6 +472,39 @@ def add_voice_channel( nsfw: typing.Union[undefined.Undefined, bool] = undefined.Undefined(), user_limit: typing.Union[undefined.Undefined, int] = undefined.Undefined(), ) -> snowflake_.Snowflake: + """Create a voice channel. + + Parameters + ---------- + name : str + The name of the category. + position : hikari.utilities.undefined.Undefined or int + The position to place the category in, if specified. + permission_overwrites : hikari.utilities.undefined.Undefined or typing.Collection[hikari.models.channels.PermissionOverwrite] + If defined, a collection of one or more + `hikari.models.channels.PermissionOverwrite` objects. + nsfw : hikari.utilities.undefined.Undefined or bool + If `True`, the channel is marked as NSFW and only users over + 18 years of age should be given access. + parent_id : hikari.utilities.undefined.Undefined or hikari.utilities.snowflake.Snowflake + If defined, should be a snowflake ID of a category channel + that was made with this builder. If provided, this channel will + become a child channel of that category. + bitrate : hikari.utilities.undefined.Undefined or int + If specified, the bitrate to set on the channel. + user_limit : hikari.utilities.undefined.Undefined or int + If specified, the maximum number of users to allow in the voice + channel. + + Returns + ------- + hikari.utilities.snowflake.Snowflake + The dummy ID for this channel that can be used temporarily to refer + to this object while designing the guild layout. + + When the guild is created, this will be replaced with a different + ID. + """ snowflake = self._new_snowflake() payload = data_binding.JSONObjectBuilder() payload.put_snowflake("id", snowflake) diff --git a/hikari/net/routes.py b/hikari/net/routes.py index 982399411d..b46624d684 100644 --- a/hikari/net/routes.py +++ b/hikari/net/routes.py @@ -107,7 +107,7 @@ def create_real_bucket_hash(self, initial_bucket_hash: str) -> str: def __hash__(self) -> int: return self.hash_code - def __eq__(self, other) -> bool: + def __eq__(self, other: typing.Any) -> bool: return ( isinstance(other, CompiledRoute) and self.route == other.route @@ -166,10 +166,12 @@ def __init__(self, method: str, path_template: str) -> None: self.method = method self.path_template = path_template + major_param: typing.Optional[str] if match := self._MAJOR_PARAM_REGEX.search(path_template): - self.major_param = match.group(1) + major_param = match.group(1) else: - self.major_param = None + major_param = None + self.major_param = major_param self.hash_code = hash((self.method, self.path_template)) @@ -205,7 +207,7 @@ def __str__(self) -> str: def __hash__(self) -> int: return self.hash_code - def __eq__(self, other) -> bool: + def __eq__(self, other: typing.Any) -> bool: return ( isinstance(other, Route) and self.method == other.method diff --git a/hikari/utilities/cdn.py b/hikari/utilities/cdn.py index 89834303b5..a01eb9f9e0 100644 --- a/hikari/utilities/cdn.py +++ b/hikari/utilities/cdn.py @@ -20,11 +20,16 @@ from __future__ import annotations -__all__: typing.List[str] = ["generate_cdn_url"] +__all__: typing.List[str] = ["generate_cdn_url", "get_avatar_url", "get_default_avatar_url", "get_default_avatar_index"] import typing import urllib.parse + +if typing.TYPE_CHECKING: + from hikari.utilities import snowflake + + BASE_CDN_URL: typing.Final[str] = "https://cdn.discordapp.com" """The URL for the CDN.""" @@ -64,3 +69,73 @@ def generate_cdn_url(*route_parts: str, format_: str, size: typing.Optional[int] url = urllib.parse.urljoin(BASE_CDN_URL, "/" + path) + "." + str(format_) query = urllib.parse.urlencode({"size": size}) if size is not None else None return f"{url}?{query}" if query else url + + +def get_avatar_url( + user_id: snowflake.Snowflake, avatar_hash: str, *, format_: typing.Optional[str] = None, size: int = 4096, +) -> str: + """Generate the avatar URL for this user's custom avatar if set, else their default avatar. + + Parameters + ---------- + user_id : hikari.utilities.snowflake.Snowflake + The user ID of the avatar to fetch. + avatar_hash : str + The avatar hash code. + format_ : str + The format to use for this URL, defaults to `png` or `gif`. + Supports `png`, `jpeg`, `jpg`, `webp` and `gif` (when + animated). Will be ignored for default avatars which can only be + `png`. + size : int + The size to set for the URL, defaults to `4096`. + Can be any power of two between 16 and 4096. + Will be ignored for default avatars. + + Returns + ------- + str + The string URL, or None if the default avatar is used instead. + + Raises + ------ + ValueError + If `size` is not a power of two or not between 16 and 4096. + """ + if format_ is None and avatar_hash.startswith("a_"): + format_ = "gif" + elif format_ is None: + format_ = "png" + return generate_cdn_url("avatars", str(user_id), avatar_hash, format_=format_, size=size) + + +def get_default_avatar_index(discriminator: str) -> int: + """Get the index of the default avatar for the given discriminator. + + Parameters + ---------- + discriminator : str + The integer discriminator, as a string. + + Returns + ------- + int + The index. + """ + return int(discriminator) % 5 + + +def get_default_avatar_url(discriminator: str) -> str: + """URL for this user's default avatar. + + Parameters + ---------- + discriminator : str + The integer discriminator, as a string. + + Returns + ------- + str + The avatar URL. + """ + return generate_cdn_url("embed", "avatars", str(get_default_avatar_index(discriminator)), format_="png", size=None,) diff --git a/hikari/utilities/data_binding.py b/hikari/utilities/data_binding.py index 4c71529854..25780d675e 100644 --- a/hikari/utilities/data_binding.py +++ b/hikari/utilities/data_binding.py @@ -42,6 +42,7 @@ from hikari.models import bases from hikari.utilities import undefined + T = typing.TypeVar("T", covariant=True) Headers = typing.Mapping[str, str] @@ -73,7 +74,7 @@ def dump_json(_: typing.Union[JSONArray, JSONObject]) -> str: """Convert a Python type to a JSON string.""" - def load_json(_: str) -> typing.Union[JSONArray, JSONObject]: + def load_json(_: typing.AnyStr) -> typing.Union[JSONArray, JSONObject]: """Convert a JSON string to a Python type.""" @@ -230,7 +231,7 @@ def put_array( else: self[key] = list(values) - def put_snowflake(self, key: str, value: typing.Union[undefined.Undefined, typing.SupportsInt, int], /) -> None: + def put_snowflake(self, key: str, value: typing.Union[undefined.Undefined, bases.UniqueObject], /) -> None: """Put a key with a snowflake value into the builder. Parameters @@ -245,7 +246,7 @@ def put_snowflake(self, key: str, value: typing.Union[undefined.Undefined, typin self[key] = str(int(value)) def put_snowflake_array( - self, key: str, values: typing.Union[undefined.Undefined, typing.Iterable[typing.SupportsInt]], / + self, key: str, values: typing.Union[undefined.Undefined, typing.Iterable[typing.Union[bases.UniqueObject]]], / ) -> None: """Put an array of snowflakes with the given key into this builder. @@ -265,9 +266,7 @@ def put_snowflake_array( self[key] = [str(int(value)) for value in values] -# There isn't a nice way to type this correctly :( -@typing.no_type_check -def cast_json_array(array, cast, collection_type=list): +def cast_json_array(array: JSONArray, cast: typing.Callable[[typing.Any], T]) -> typing.List[T]: """Cast a JSON array to a given generic collection type. This will perform casts on each internal item individually. @@ -288,21 +287,18 @@ def cast_json_array(array, cast, collection_type=list): The cast to apply to each item in the array. This should consume any valid JSON-decoded type and return the type corresponding to the generic type of the provided collection. - collection_type : typing.Callable[[typing.Iterable[T]], typing.Collection[T]] - The container type to store the cast items within. This should be called - with options such as `list`, `set`, `frozenset`, `tuple`, etc. Returns ------- - typing.Collection[T] - The generated collection. + typing.List[T] + The generated list. Example ------- ```py >>> arr = [123, 456, 789, 123] - >>> cast_json_array(arr, str, set) - {"456", "123", "789"} + >>> cast_json_array(arr, str) + ["123", "456", "789", "123"] ``` """ - return collection_type(cast(item) for item in array) + return [cast(item) for item in array] diff --git a/mypy.ini b/mypy.ini new file mode 100644 index 0000000000..9c4554d553 --- /dev/null +++ b/mypy.ini @@ -0,0 +1,38 @@ +[mypy] +# general settings +check_untyped_defs = true +incremental = true +namespace_packages = true +no_implicit_optional = true +pretty = true +python_version = 3.8 +show_column_numbers = true +show_error_codes = true +show_error_context = true + +# Output formats +cobertura_xml_report = public/mypy + +# stuff to allow +allow_untyped_globals = false +allow_redefinition = true + +# stuff to disallow +disallow_untyped_decorators = true +disallow_incomplete_defs = true +disallow_untyped_defs = true + +# warnings +warn_no_return = true +warn_redundant_casts = true +warn_return_any = true +warn_unreachable = true +warn_unused_configs = true +warn_unused_ignores = true + + +[mypy-hikari.net.gateway] +disallow_any_explicit = false +no_implicit_optional = false +strict_optional = false +warn_return_any = false diff --git a/pylint.ini b/pylint.ini index b33ee47569..8a1863ddbf 100644 --- a/pylint.ini +++ b/pylint.ini @@ -11,7 +11,7 @@ ignore=CVS # Add files or directories matching the regex patterns to the blacklist. The # regex matches against base names, not paths. -ignore-patterns=test.py +ignore-patterns=_about.py # Python code to execute, usually for sys.path manipulation such as # pygtk.require(). @@ -19,7 +19,9 @@ ignore-patterns=test.py # Use multiple processes to speed up Pylint. Specifying 0 will auto-detect the # number of processors available to use. -jobs=0 +# Theres a bug where multiple jobs report the same error a billion times, I +# ain't got time for that. +jobs=1 # Control the amount of potential inferred values when inferring a single # object. This can help the performance when dealing with large functions or @@ -28,7 +30,7 @@ limit-inference-results=100 # List of plugins (as comma separated values of python module names) to load, # usually to register additional checkers. -load-plugins=pylint_junit +load-plugins= # Pickle collected data for later comparisons. persistent=yes diff --git a/tests/hikari/impl/test_entity_factory.py b/tests/hikari/impl/test_entity_factory.py index f4d4694a20..c530a70335 100644 --- a/tests/hikari/impl/test_entity_factory.py +++ b/tests/hikari/impl/test_entity_factory.py @@ -2096,10 +2096,10 @@ def test_deserialize_member_presence( assert presence.user.is_system is True assert presence.user.flags == user_models.UserFlag(131072) - assert isinstance(presence.user, presence_models.PresenceUser) + assert isinstance(presence.user, user_models.PartialUser) assert presence.role_ids == {49494949} assert presence.guild_id == 44004040 - assert presence.visible_status == presence_models.PresenceStatus.DND + assert presence.visible_status == presence_models.Status.DND # PresenceActivity assert len(presence.activities) == 1 activity = presence.activities[0] @@ -2140,9 +2140,9 @@ def test_deserialize_member_presence( assert isinstance(activity, presence_models.RichActivity) # ClientStatus - assert presence.client_status.desktop == presence_models.PresenceStatus.ONLINE - assert presence.client_status.mobile == presence_models.PresenceStatus.IDLE - assert presence.client_status.web == presence_models.PresenceStatus.DND + assert presence.client_status.desktop == presence_models.Status.ONLINE + assert presence.client_status.mobile == presence_models.Status.IDLE + assert presence.client_status.web == presence_models.Status.DND assert isinstance(presence.client_status, presence_models.ClientStatus) assert presence.premium_since == datetime.datetime(2015, 4, 26, 6, 26, 56, 936000, tzinfo=datetime.timezone.utc) @@ -2185,9 +2185,9 @@ def test_deserialize_member_presence_with_unset_fields( assert presence.nickname is None assert presence.role_ids is None # ClientStatus - assert presence.client_status.desktop is presence_models.PresenceStatus.OFFLINE - assert presence.client_status.mobile is presence_models.PresenceStatus.OFFLINE - assert presence.client_status.web is presence_models.PresenceStatus.OFFLINE + assert presence.client_status.desktop is presence_models.Status.OFFLINE + assert presence.client_status.mobile is presence_models.Status.OFFLINE + assert presence.client_status.web is presence_models.Status.OFFLINE # PresenceUser assert presence.user.id == 42 assert presence.user.discriminator is undefined.Undefined() diff --git a/tests/hikari/utilities/test_data_binding.py b/tests/hikari/utilities/test_data_binding.py index a57bdeb7de..475e794177 100644 --- a/tests/hikari/utilities/test_data_binding.py +++ b/tests/hikari/utilities/test_data_binding.py @@ -238,8 +238,3 @@ def test_cast_result_is_used_for_each_item(self): arr = ["foo", "bar", "baz"] assert data_binding.cast_json_array(arr, cast) == [r1, r2, r3] - - def test_cast_with_custom_container(self): - cast = lambda obj: obj - arr = ["foo", "bar", "baz", "foo"] - assert data_binding.cast_json_array(arr, cast, set) == {"foo", "bar", "baz"} From c8f4e721cec85cac5fe175a5cef7e9031118fbb7 Mon Sep 17 00:00:00 2001 From: davfsa Date: Tue, 9 Jun 2020 11:12:01 +0200 Subject: [PATCH 486/922] Fix reference issues --- docs/config.mako | 2 +- docs/documentation.mako | 22 ++++++++++------------ hikari/api/app.py | 6 +++--- hikari/api/entity_factory.py | 10 +++++----- hikari/api/event_consumer.py | 2 +- hikari/events/message.py | 10 ++++------ hikari/models/audit_logs.py | 1 + hikari/models/guilds.py | 2 +- hikari/models/messages.py | 20 ++++++++++---------- hikari/models/webhooks.py | 2 +- hikari/net/buckets.py | 4 ++-- hikari/net/gateway.py | 8 ++++---- hikari/net/rest.py | 14 +++++++------- hikari/net/rest_utils.py | 4 ++-- hikari/utilities/data_binding.py | 12 +++++------- 15 files changed, 57 insertions(+), 62 deletions(-) diff --git a/docs/config.mako b/docs/config.mako index dcc102311e..848c3aa05c 100644 --- a/docs/config.mako +++ b/docs/config.mako @@ -28,7 +28,7 @@ show_source_code = True - git_link_template = "https://gitlab.com/nekokatt/hikari/blob/{commit}/{path}#L{start_line}" + git_link_template = "https://gitlab.com/nekokatt/hikari/blob/{commit}/{path}#L{start_line}-L{end_line}" link_prefix = "" diff --git a/docs/documentation.mako b/docs/documentation.mako index 8bec95a43d..a2b3fb49b9 100644 --- a/docs/documentation.mako +++ b/docs/documentation.mako @@ -148,21 +148,19 @@ url = dobj.url(relative_to=module, link_prefix=link_prefix, top_ancestor=not show_inherited_members) - if url.startswith("/"): - - if isinstance(dobj, pdoc.External): - if dobj.module: - fqn = dobj.module.name + "." + dobj.obj.__qualname__ - else: - fqn = dobj.name + if isinstance(dobj, pdoc.External): + if dobj.module: + fqn = dobj.module.name + "." + dobj.obj.__qualname__ + else: + fqn = dobj.name - url = discover_source(fqn) - if url is None: - url = discover_source(name) + url = discover_source(fqn) + if url is None: + url = discover_source(name) - if url is None: - return name if not with_prefixes else f"{QUAL_EXTERNAL} {name}" + if url is None: + return name if not with_prefixes else f"{QUAL_EXTERNAL} {name}" if simple_names: name = simple_name(name) diff --git a/hikari/api/app.py b/hikari/api/app.py index 3805834d4d..0d80f1d0f7 100644 --- a/hikari/api/app.py +++ b/hikari/api/app.py @@ -79,8 +79,8 @@ async def close(self): self._thread_pool.shutdown() ``` - If you are in any doubt, check out the `hikari.RESTApp` and `hikari.Bot` - implementations to see how they are pieced together! + If you are in any doubt, check out the `hikari.impl.rest_app.RESTAppImpl` and + `hikari.impl.bot.BotImpl` implementations to see how they are pieced together! """ __slots__ = () @@ -197,7 +197,7 @@ class IGatewayDispatcher(IApp, abc.ABC): that consume events from a message queue, for example. This purposely also implements some calls found in - `hikari.event_dispatcher.IEventDispatcher` with defaulting delegated calls + `hikari.api.event_dispatcher.IEventDispatcher` with defaulting delegated calls to the event dispatcher. This provides a more intuitive syntax for applications. diff --git a/hikari/api/entity_factory.py b/hikari/api/entity_factory.py index 21787e4cf2..55b9bc6c7a 100644 --- a/hikari/api/entity_factory.py +++ b/hikari/api/entity_factory.py @@ -140,7 +140,7 @@ def deserialize_permission_overwrite(self, payload: data_binding.JSONObject) -> Returns ------- - hikari.models.channels.PermissionOverwrote + hikari.models.channels.PermissionOverwrite The deserialized permission overwrite object. """ @@ -466,7 +466,7 @@ def deserialize_role(self, payload: data_binding.JSONObject) -> guild_models.Rol Returns ------- - hikari.models.guilds.GuildRole + hikari.models.guilds.Role The deserialized role object. """ @@ -511,7 +511,7 @@ def deserialize_guild_member_ban(self, payload: data_binding.JSONObject) -> guil Returns ------- - hikari.models.GuildMemberBan + hikari.models.guilds.GuildMemberBan The deserialized guild member ban object. """ @@ -575,7 +575,7 @@ def deserialize_vanity_url(self, payload: data_binding.JSONObject) -> invite_mod Returns ------- - hikari.models.invites.VanityUrl + hikari.models.invites.VanityURL The deserialized vanity url object. """ @@ -644,7 +644,7 @@ def deserialize_member_presence(self, payload: data_binding.JSONObject) -> prese Returns ------- - hikari.models.guilds.MemberPresence + hikari.models.presences.MemberPresence The deserialized member presence object. """ diff --git a/hikari/api/event_consumer.py b/hikari/api/event_consumer.py index 017a3e4c99..21babaf356 100644 --- a/hikari/api/event_consumer.py +++ b/hikari/api/event_consumer.py @@ -54,6 +54,6 @@ async def consume_raw_event( The gateway shard that emitted the event. event_name : str The event name. - payload : hikari.utility.data_binding.JSONObject + payload : hikari.utilities.data_binding.JSONObject The payload provided with the event. """ diff --git a/hikari/events/message.py b/hikari/events/message.py index 3387d1fe50..56f68feec4 100644 --- a/hikari/events/message.py +++ b/hikari/events/message.py @@ -65,9 +65,8 @@ class UpdateMessage(messages.Message): """An arbitrarily partial version of `hikari.models.messages.Message`. !!! warn - All fields on this model except `UpdateMessage.channel` and - `UpdateMessage.id` may be set to - `hikari.models.undefined.Undefined` (a singleton) if we have not + All fields on this model except `channel` and `id` may be set to + `hikari.utilities.undefined.Undefined` (a singleton) if we have not received information about their state from Discord alongside field nullability. """ @@ -158,9 +157,8 @@ class MessageUpdateEvent(base_events.HikariEvent, base_models.Unique): !!! warn Unlike `MessageCreateEvent`, `MessageUpdateEvent.message` is an arbitrarily partial version of `hikari.models.messages.Message` where - any field except `UpdateMessage.id` may be set to - `hikari.models.undefined.Undefined` (a singleton) to indicate that - it has not been changed. + any field except `id` may be set to `hikari.utilities.undefined.Undefined` + (a singleton) to indicate that it has not been changed. """ message: UpdateMessage = attr.ib(repr=True) diff --git a/hikari/models/audit_logs.py b/hikari/models/audit_logs.py index 5b082341ff..fb149b4d15 100644 --- a/hikari/models/audit_logs.py +++ b/hikari/models/audit_logs.py @@ -21,6 +21,7 @@ from __future__ import annotations __all__: typing.List[str] = [ + "AuditLog", "AuditLogChange", "AuditLogChangeKey", "AuditLogEntry", diff --git a/hikari/models/guilds.py b/hikari/models/guilds.py index d4823172e4..894387b99c 100644 --- a/hikari/models/guilds.py +++ b/hikari/models/guilds.py @@ -545,7 +545,7 @@ class Guild(PartialGuild): # pylint:disable=too-many-instance-attributes !!! note If a guild object is considered to be unavailable, then the state of any - other fields other than the `Guild.is_unavailable` and `Guild.id` are + other fields other than the `is_unavailable` and `id` are outdated or incorrect. If a guild is unavailable, then the contents of any other fields should be ignored. """ diff --git a/hikari/models/messages.py b/hikari/models/messages.py index a3c1549703..c32acf51cb 100644 --- a/hikari/models/messages.py +++ b/hikari/models/messages.py @@ -322,13 +322,13 @@ async def edit( # pylint:disable=line-too-long text: typing.Union[undefined.Undefined, str, None] = undefined.Undefined(), *, embed: typing.Union[undefined.Undefined, embeds_.Embed, None] = undefined.Undefined(), - mentions_everyone: bool = False, + mentions_everyone: typing.Union[bool, undefined.Undefined] = undefined.Undefined(), user_mentions: typing.Union[ - typing.Collection[typing.Union[snowflake.Snowflake, int, str, users.User]], bool - ] = True, + typing.Collection[typing.Union[snowflake.Snowflake, int, str, users.User]], bool, undefined.Undefined + ] = undefined.Undefined(), role_mentions: typing.Union[ - typing.Collection[typing.Union[snowflake.Snowflake, int, str, guilds.Role]], bool - ] = True, + typing.Collection[typing.Union[snowflake.Snowflake, int, str, guilds.Role]], bool, undefined.Undefined + ] = undefined.Undefined(), ) -> Message: """Edit this message. @@ -346,11 +346,11 @@ async def edit( # pylint:disable=line-too-long mentions_everyone : bool Whether `@everyone` and `@here` mentions should be resolved by discord and lead to actual pings, defaults to `False`. - user_mentions : typing.Collection[hikari.models.users.User or hikari.models.snowflake.Snowflake or int or str] or bool + user_mentions : typing.Collection[hikari.models.users.User or hikari.utilities.snowflake.Snowflake or int or str] or bool Either an array of user objects/IDs to allow mentions for, `True` to allow all user mentions or `False` to block all user mentions from resolving, defaults to `True`. - role_mentions: typing.Collection[hikari.models.guilds.Role or hikari.models.snowflake.Snowflake or int or str] or bool + role_mentions: typing.Collection[hikari.models.guilds.Role or hikari.utilities.snowflake.Snowflake or int or str] or bool Either an array of guild role objects/IDs to allow mentions for, `True` to allow all role mentions or `False` to block all role mentions from resolving, defaults to `True`. @@ -396,7 +396,7 @@ async def reply( # pylint:disable=line-too-long *, embed: typing.Union[undefined.Undefined, embeds_.Embed] = undefined.Undefined(), attachments: typing.Sequence[files_.BaseStream] = undefined.Undefined(), - mentions_everyone: bool = False, + mentions_everyone: bool = True, user_mentions: typing.Union[ typing.Collection[typing.Union[snowflake.Snowflake, int, str, users.User]], bool ] = True, @@ -427,11 +427,11 @@ async def reply( # pylint:disable=line-too-long mentions_everyone : bool Whether `@everyone` and `@here` mentions should be resolved by discord and lead to actual pings, defaults to `False`. - user_mentions : typing.Collection[hikari.models.users.User or hikari.models.snowflake.Snowflake or int or str] or bool + user_mentions : typing.Collection[hikari.models.users.User or hikari.utilities.snowflake.Snowflake or int or str] or bool Either an array of user objects/IDs to allow mentions for, `True` to allow all user mentions or `False` to block all user mentions from resolving, defaults to `True`. - role_mentions: typing.Collection[hikari.models.guilds.Role or hikari.models.snowflake.Snowflake or int or str] or bool + role_mentions: typing.Collection[hikari.models.guilds.Role or hikari.utilities.snowflake.Snowflake or int or str] or bool Either an array of guild role objects/IDs to allow mentions for, `True` to allow all role mentions or `False` to block all role mentions from resolving, defaults to `True`. diff --git a/hikari/models/webhooks.py b/hikari/models/webhooks.py index 90d2570627..79ec4fb16f 100644 --- a/hikari/models/webhooks.py +++ b/hikari/models/webhooks.py @@ -230,7 +230,7 @@ async def edit( avatar : hikari.models.files.BaseStream or None or hikari.utilities.undefined.Undefined If specified, the new avatar image. If `None`, then it is removed. - channel : hikari.models.channels.GuildChannel or hikari.models.bases.UniqueObject or hikari.utilities.undefined.Undefined + channel : hikari.utilities.undefined.Undefined or hikari.models.channels.GuildChannel or hikari.models.bases.Unique or hikari.utilities.snowflake.Snowflake or str or int If specified, the object or ID of the new channel the given webhook should be moved to. reason : str or hikari.utilities.undefined.Undefined diff --git a/hikari/net/buckets.py b/hikari/net/buckets.py index c57b52703b..2d90af85f3 100644 --- a/hikari/net/buckets.py +++ b/hikari/net/buckets.py @@ -490,7 +490,7 @@ def acquire(self, compiled_route: routes.CompiledRoute) -> asyncio.Future[None]: Parameters ---------- - compiled_route : hikari.rest.routes.CompiledRoute + compiled_route : hikari.net.routes.CompiledRoute The _route to get the bucket for. Returns @@ -540,7 +540,7 @@ def update_rate_limits( Parameters ---------- - compiled_route : hikari.rest.routes.CompiledRoute + compiled_route : hikari.net.routes.CompiledRoute The compiled _route to get the bucket for. bucket_header : str, optional The `X-RateLimit-Bucket` header that was provided in the response. diff --git a/hikari/net/gateway.py b/hikari/net/gateway.py index 1339794ac8..7d4efb9dcd 100644 --- a/hikari/net/gateway.py +++ b/hikari/net/gateway.py @@ -60,14 +60,14 @@ class Gateway(http_client.HTTPClient, component.IComponent): Parameters ---------- - app : hikari.gateway_dispatcher.IGatewayConsumer + app : hikari.api.app.IGatewayConsumer The base application. - config : hikari.http_settings.HTTPSettings + config : hikari.net.http_settings.HTTPSettings The AIOHTTP settings to use for the client session. debug : bool If `True`, each sent and received payload is dumped to the logs. If `False`, only the fact that data has been sent/received will be logged. - initial_activity : hikari.presences.Activity or None or hikari.utilities.undefined.Undefined + initial_activity : hikari.models.presences.Activity or None or hikari.utilities.undefined.Undefined The initial activity to appear to have for this shard. initial_idle_since : datetime.datetime or None or hikari.utilities.undefined.Undefined The datetime to appear to be idle since. @@ -416,7 +416,7 @@ async def update_voice_state( ---------- guild : hikari.models.guilds.PartialGuild or hikari.utilities.snowflake.Snowflake or int or str The guild or guild ID to update the voice state for. - channel : hikari.models.channels.GuildVoiceChannel or hikari.utilities.Snowflake or int or str or None + channel : hikari.models.channels.GuildVoiceChannel or hikari.utilities.snowflake.Snowflake or int or str or None The channel or channel ID to update the voice state for. If `None` then the bot will leave the voice channel that it is in for the given guild. diff --git a/hikari/net/rest.py b/hikari/net/rest.py index 25bf914506..b1842c3fe9 100644 --- a/hikari/net/rest.py +++ b/hikari/net/rest.py @@ -82,10 +82,10 @@ class REST(http_client.HTTPClient, component.IComponent): # pylint:disable=too- Parameters ---------- - app : hikari.rest_app.IRESTApp + app : hikari.api.app.IRESTApp The REST application containing all other application components that Hikari uses. - config : hikari.http_settings.HTTPSettings + config : hikari.net.http_settings.HTTPSettings The AIOHTTP-specific configuration settings. This is used to configure proxies, and specify TCP connectors to control the size of HTTP connection pools, etc. @@ -595,7 +595,7 @@ async def edit_permission_overwrites( channel : hikari.models.channels.PartialChannel or hikari.utilities.snowflake.Snowflake or int or str The channel to edit a permission overwrite in. This may be a channel object, or the ID of an existing channel. - target : hikari.models.users.User or hikari.models.guidls.Role or hikari.models.channels.PermissionOverwrite or hikari.utilities.snowflake.Snowflake or int or str + target : hikari.models.users.User or hikari.models.guilds.Role or hikari.models.channels.PermissionOverwrite or hikari.utilities.snowflake.Snowflake or int or str The channel overwrite to edit. This may be a overwrite object, or the ID of an existing channel. target_type : hikari.utilities.undefined.Undefined or hikari.models.channels.PermissionOverwriteType or str @@ -657,7 +657,7 @@ async def delete_permission_overwrite( channel : hikari.models.channels.PartialChannel or hikari.utilities.snowflake.Snowflake or int or str The channel to delete a permission overwrite in. This may be a channel object, or the ID of an existing channel. - target : hikari.models.users.User or hikari.models.guidls.Role or hikari.models.channels.PermissionOverwrite or hikari.utilities.snowflake.Snowflake or int or str + target : hikari.models.users.User or hikari.models.guilds.Role or hikari.models.channels.PermissionOverwrite or hikari.utilities.snowflake.Snowflake or int or str The channel overwrite to delete. Raises @@ -853,7 +853,7 @@ async def pin_message( channel : hikari.models.channels.PartialChannel or hikari.utilities.snowflake.Snowflake or int or str The channel to pin a message in. This may be a channel object, or the ID of an existing channel. - message : hikari.models.messges.Message or hikari.utilities.snowflake.Snowflake or int or str + message : hikari.models.messages.Message or hikari.utilities.snowflake.Snowflake or int or str The message to pin. This may be a message object, or the ID of an existing message. @@ -884,7 +884,7 @@ async def unpin_message( channel : hikari.models.channels.PartialChannel or hikari.utilities.snowflake.Snowflake or int or str The channel to unpin a message in. This may be a channel object, or the ID of an existing channel. - message : hikari.models.messges.Message or hikari.utilities.snowflake.Snowflake or int or str + message : hikari.models.messages.Message or hikari.utilities.snowflake.Snowflake or int or str The message to unpin. This may be a message object, or the ID of an existing message. @@ -1158,7 +1158,7 @@ async def edit_message( channel : hikari.models.channels.PartialChannel or hikari.utilities.snowflake.Snowflake or int or str The channel to edit the message in. This may be a channel object, or the ID of an existing channel. - message : hikari.models.messages.Messages or hikari.utilities.snowflake.Snowflake or int or str + message : hikari.models.messages.Message or hikari.utilities.snowflake.Snowflake or int or str The message to fetch. text embed diff --git a/hikari/net/rest_utils.py b/hikari/net/rest_utils.py index 31b242dd7d..93f256299e 100644 --- a/hikari/net/rest_utils.py +++ b/hikari/net/rest_utils.py @@ -282,10 +282,10 @@ def add_role( ---------- name : str The role name. - color : hikari.utilities.undefined.Undefined or hikari.models.color.Color + color : hikari.utilities.undefined.Undefined or hikari.models.colors.Color The colour of the role to use. If unspecified, then the default colour is used instead. - colour : hikari.utilities.undefined.Undefined or hikari.models.color.Color + colour : hikari.utilities.undefined.Undefined or hikari.models.colors.Color Alias for the `color` parameter for non-american users. hoisted : hikari.utilities.undefined.Undefined or bool If `True`, the role will show up in the user sidebar in a separate diff --git a/hikari/utilities/data_binding.py b/hikari/utilities/data_binding.py index 25780d675e..b017490327 100644 --- a/hikari/utilities/data_binding.py +++ b/hikari/utilities/data_binding.py @@ -89,10 +89,9 @@ def load_json(_: typing.AnyStr) -> typing.Union[JSONArray, JSONObject]: class StringMapBuilder(multidict.MultiDict[str]): """Helper class used to quickly build query strings or header maps. - This will consume any items that are not - `hikari.utilities.unspecified.Unspecified`. If a value _is_ unspecified, - it will be ignored when inserting it. This reduces the amount of - boilerplate needed for generating the headers and query strings for + This will consume any items that are not `hikari.utilities.undefined.Undefined`. + If a value _is_ unspecified, it will be ignored when inserting it. This reduces + the amount of boilerplate needed for generating the headers and query strings for low-level HTTP API interaction, amongst other things. !!! warn @@ -153,9 +152,8 @@ def put( class JSONObjectBuilder(typing.Dict[str, JSONAny]): """Helper class used to quickly build JSON objects from various values. - If provided with any values that are - `hikari.utilities.unspecified.Unspecified`, then these values will be - ignored. + If provided with any values that are `hikari.utilities.undefined.Undefined`, + then these values will be ignored. This speeds up generation of JSON payloads for low level HTTP and websocket API interaction. From 7f3c2bc34e07d70387ecd0026db2ef9e2b7d9376 Mon Sep 17 00:00:00 2001 From: davfsa Date: Tue, 9 Jun 2020 14:47:42 +0200 Subject: [PATCH 487/922] Implement "self_video" --- hikari/impl/entity_factory.py | 1 + hikari/models/voices.py | 3 +++ tests/hikari/impl/test_entity_factory.py | 3 +++ 3 files changed, 7 insertions(+) diff --git a/hikari/impl/entity_factory.py b/hikari/impl/entity_factory.py index 629c7955df..3f2ad4a4af 100644 --- a/hikari/impl/entity_factory.py +++ b/hikari/impl/entity_factory.py @@ -1299,6 +1299,7 @@ def deserialize_voice_state(self, payload: data_binding.JSONObject) -> voice_mod voice_state.is_self_deafened = payload["self_deaf"] voice_state.is_self_muted = payload["self_mute"] voice_state.is_streaming = payload.get("self_stream", False) + voice_state.is_video_enabled = payload["self_video"] voice_state.is_suppressed = payload["suppress"] return voice_state diff --git a/hikari/models/voices.py b/hikari/models/voices.py index a3942023fe..cb2a12dc89 100644 --- a/hikari/models/voices.py +++ b/hikari/models/voices.py @@ -70,6 +70,9 @@ class VoiceState(bases.Entity): is_streaming: bool = attr.ib(eq=False, hash=False, repr=False) """Whether this user is streaming using "Go Live".""" + is_video_enabled: bool = attr.ib(eq=False, hash=False, repr=False) + """Whether this user's camera is enabled.""" + is_suppressed: bool = attr.ib(eq=False, hash=False, repr=False) """Whether this user is muted by the current user.""" diff --git a/tests/hikari/impl/test_entity_factory.py b/tests/hikari/impl/test_entity_factory.py index c530a70335..8d9204c95e 100644 --- a/tests/hikari/impl/test_entity_factory.py +++ b/tests/hikari/impl/test_entity_factory.py @@ -2425,6 +2425,7 @@ def voice_state_payload(self, member_payload): "self_deaf": False, "self_mute": True, "self_stream": True, + "self_video": True, "suppress": False, } @@ -2441,6 +2442,7 @@ def test_deserialize_voice_state(self, entity_factory_impl, mock_app, voice_stat assert voice_state.is_self_deafened is False assert voice_state.is_self_muted is True assert voice_state.is_streaming is True + assert voice_state.is_video_enabled is True assert voice_state.is_suppressed is False assert isinstance(voice_state, voice_models.VoiceState) @@ -2454,6 +2456,7 @@ def test_deserialize_voice_state_with_null_and_unset_fields(self, entity_factory "mute": True, "self_deaf": False, "self_mute": True, + "self_video": False, "suppress": False, } ) From 71d1011ce85b0f34256682fd9cd14bd16168a301 Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Mon, 8 Jun 2020 09:51:17 +0100 Subject: [PATCH 488/922] Made mypy not exit with an error code. Switching mypy to junit output --- ci/config.py | 1 + ci/gitlab/linting.yml | 2 +- ci/mypy.nox.py | 9 +++++++- hikari/models/files.py | 49 ++++++++++++++++++------------------------ mypy.ini | 3 --- 5 files changed, 31 insertions(+), 33 deletions(-) diff --git a/ci/config.py b/ci/config.py index bd9051e198..47bc401367 100644 --- a/ci/config.py +++ b/ci/config.py @@ -36,6 +36,7 @@ # Linting and test configs. MYPY_INI = "mypy.ini" +MYPY_JUNIT_OUTPUT_PATH = _os.path.join(ARTIFACT_DIRECTORY, "mypy.xml") PYDOCSTYLE_INI = "pydocstyle.ini" PYLINT_INI = "pylint.ini" PYLINT_JUNIT_OUTPUT_PATH = _os.path.join(ARTIFACT_DIRECTORY, "pylint.xml") diff --git a/ci/gitlab/linting.yml b/ci/gitlab/linting.yml index 17cca6fc66..886c6789f3 100644 --- a/ci/gitlab/linting.yml +++ b/ci/gitlab/linting.yml @@ -91,5 +91,5 @@ mypy: - nox -s mypy --no-error-on-external-run artifacts: reports: - cobertura: public/mypy/cobertura.xml + junit: public/mypy.xml when: always diff --git a/ci/mypy.nox.py b/ci/mypy.nox.py index a4a728d4ac..df6e659417 100644 --- a/ci/mypy.nox.py +++ b/ci/mypy.nox.py @@ -26,5 +26,12 @@ def mypy(session: nox.Session) -> None: # LXML is used for cobertura reporting. session.install("-r", "requirements.txt", "mypy==0.780", "lxml") session.run( - "mypy", "-p", config.MAIN_PACKAGE, "--config", config.MYPY_INI, + "mypy", + "-p", + config.MAIN_PACKAGE, + "--config", + config.MYPY_INI, + "--junit-xml", + config.MYPY_JUNIT_OUTPUT_PATH, + success_codes=range(0, 256), ) diff --git a/hikari/models/files.py b/hikari/models/files.py index cf41f09a44..0ef6ce28aa 100644 --- a/hikari/models/files.py +++ b/hikari/models/files.py @@ -58,6 +58,7 @@ import asyncio import base64 import concurrent.futures +import contextlib import functools import http import inspect @@ -364,7 +365,7 @@ def __init__(self, filename: str, obj: ___VALID_TYPES___) -> None: return if aio.is_async_iterable(obj): - self._obj = obj + self._obj = obj # type: ignore return if isinstance(obj, (io.StringIO, io.BytesIO)): @@ -380,7 +381,7 @@ def __init__(self, filename: str, obj: ___VALID_TYPES___) -> None: raise TypeError(f"Expected bytes-like object or async generator, got {type(obj).__qualname__}") def __aiter__(self) -> typing.AsyncGenerator[typing.Any, bytes]: - return self._obj.__aiter__() + return self._obj.__aiter__() # type: ignore @property def filename(self) -> str: @@ -430,11 +431,11 @@ class WebResourceStream(BaseStream): url: str """The URL of the resource.""" - def __init__(self, filename: str, url: str) -> None: + def __init__(self, filename: str, url: str, /) -> None: self._filename = filename self.url = url - async def __aiter__(self) -> typing.Generator[bytes, None, None]: + async def __aiter__(self) -> typing.AsyncGenerator[typing.Any, bytes]: async with aiohttp.request("GET", self.url) as response: if 200 <= response.status < 300: async for chunk in response.content: @@ -454,6 +455,7 @@ async def __aiter__(self) -> typing.Generator[bytes, None, None]: if response.status == http.HTTPStatus.NOT_FOUND: raise errors.NotFound(real_url, response.headers, raw_body) + cls: typing.Type[errors.HikariError] if 400 <= response.status < 500: cls = errors.ClientHTTPErrorResponse elif 500 <= response.status < 600: @@ -527,6 +529,7 @@ def __init__( self, filename: str, path: typing.Union[str, os.PathLike], + /, *, executor: typing.Optional[concurrent.futures.Executor] = None, ) -> None: @@ -538,7 +541,9 @@ def __init__( ) -> None: ... - def __init__(self, *args, executor=None) -> None: + def __init__( + self, *args: typing.Union[str, os.PathLike], executor: typing.Optional[concurrent.futures.Executor] = None + ) -> None: if len(args) == 1: self._filename = os.path.basename(args[0]) self.path = args[0] @@ -546,33 +551,21 @@ def __init__(self, *args, executor=None) -> None: self._filename, self.path = args self._executor = executor - def __aiter__(self) -> typing.AsyncGenerator[typing.Any, bytes]: + async def __aiter__(self) -> typing.AsyncGenerator[typing.Any, bytes]: loop = asyncio.get_event_loop() # We cant use a process pool in the same way we do a thread pool, as - # we cannot pickle file objects that we pass between threads. This - # method instead will stream the data via a pipe to us. + # we cannot pickle file objects that we pass between threads. if isinstance(self._executor, concurrent.futures.ProcessPoolExecutor): - - return self._processpool_strategy(loop) - - return self._threadpool_strategy(loop) + raise NotImplementedError("Cannot currently use a ProcessPoolExecutor to perform IO in this implementation") + else: + fp = await loop.run_in_executor(self._executor, functools.partial(open, self.path, "rb")) + try: + while chunk := await loop.run_in_executor(self._executor, fp.read, MAGIC_NUMBER): + yield chunk + finally: + with contextlib.suppress(IOError): + await loop.run_in_executor(self._executor, fp.close) @property def filename(self) -> str: return self._filename - - async def _threadpool_strategy(self, loop): - fp = await loop.run_in_executor(self._executor, functools.partial(open, self.path, "rb")) - try: - while chunk := await loop.run_in_executor(self._executor, fp.read, MAGIC_NUMBER): - yield chunk - finally: - await loop.run_in_executor(self._executor, fp.close) - - async def _processpool_strategy(self, loop): - yield await loop.run_in_executor(self._executor, self._read_all, self.path) - - @staticmethod - def _read_all(path): - with open(path, "rb") as fp: - return fp.read() diff --git a/mypy.ini b/mypy.ini index 9c4554d553..35098376bc 100644 --- a/mypy.ini +++ b/mypy.ini @@ -10,9 +10,6 @@ show_column_numbers = true show_error_codes = true show_error_context = true -# Output formats -cobertura_xml_report = public/mypy - # stuff to allow allow_untyped_globals = false allow_redefinition = true From 850e2d43e5648676f1ea0278c79b45911198feb3 Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Mon, 8 Jun 2020 10:46:03 +0100 Subject: [PATCH 489/922] Added py.typed --- hikari/py.typed | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 hikari/py.typed diff --git a/hikari/py.typed b/hikari/py.typed new file mode 100644 index 0000000000..e69de29bb2 From d78a268ba6bb39dfe77e105654e54a27ed1962bd Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Mon, 8 Jun 2020 19:08:23 +0100 Subject: [PATCH 490/922] Embeds and color work. Added new file API. --- hikari/api/app.py | 2 +- hikari/impl/bot.py | 2 +- hikari/impl/entity_factory.py | 4 +- hikari/impl/rest_app.py | 2 +- hikari/models/bases.py | 4 +- hikari/models/colors.py | 70 ++-- hikari/models/colours.py | 3 +- hikari/models/embeds.py | 321 ++--------------- hikari/models/emojis.py | 7 +- hikari/models/files.py | 571 ------------------------------- hikari/models/messages.py | 85 +++-- hikari/net/http_client.py | 88 +++-- hikari/net/rest.py | 189 +++++----- hikari/utilities/data_binding.py | 5 + hikari/utilities/files.py | 352 +++++++++++++++++++ hikari/utilities/klass.py | 11 +- 16 files changed, 660 insertions(+), 1056 deletions(-) delete mode 100644 hikari/models/files.py create mode 100644 hikari/utilities/files.py diff --git a/hikari/api/app.py b/hikari/api/app.py index 0d80f1d0f7..3af431022e 100644 --- a/hikari/api/app.py +++ b/hikari/api/app.py @@ -120,7 +120,7 @@ def entity_factory(self) -> entity_factory_.IEntityFactory: @property @abc.abstractmethod - def thread_pool(self) -> typing.Optional[futures.ThreadPoolExecutor]: + def thread_pool_executor(self) -> typing.Optional[futures.ThreadPoolExecutor]: """Thread-pool to utilise for file IO within the library, if set. Returns diff --git a/hikari/impl/bot.py b/hikari/impl/bot.py index ca751187a6..19e3613d05 100644 --- a/hikari/impl/bot.py +++ b/hikari/impl/bot.py @@ -219,7 +219,7 @@ def entity_factory(self) -> entity_factory_.IEntityFactory: return self._entity_factory @property - def thread_pool(self) -> typing.Optional[futures.ThreadPoolExecutor]: + def thread_pool_executor(self) -> typing.Optional[futures.ThreadPoolExecutor]: # XXX: fixme return None diff --git a/hikari/impl/entity_factory.py b/hikari/impl/entity_factory.py index 3f2ad4a4af..dec0efaf8f 100644 --- a/hikari/impl/entity_factory.py +++ b/hikari/impl/entity_factory.py @@ -659,14 +659,14 @@ def deserialize_unicode_emoji(self, payload: data_binding.JSONObject) -> emoji_m return unicode_emoji def deserialize_custom_emoji(self, payload: data_binding.JSONObject) -> emoji_models.CustomEmoji: - custom_emoji = emoji_models.CustomEmoji(self._app) + custom_emoji = emoji_models.CustomEmoji() custom_emoji.id = snowflake.Snowflake(payload["id"]) custom_emoji.name = payload["name"] custom_emoji.is_animated = payload.get("animated", False) return custom_emoji def deserialize_known_custom_emoji(self, payload: data_binding.JSONObject) -> emoji_models.KnownCustomEmoji: - known_custom_emoji = emoji_models.KnownCustomEmoji(self._app) + known_custom_emoji = emoji_models.KnownCustomEmoji() known_custom_emoji.id = snowflake.Snowflake(payload["id"]) known_custom_emoji.name = payload["name"] known_custom_emoji.is_animated = payload.get("animated", False) diff --git a/hikari/impl/rest_app.py b/hikari/impl/rest_app.py index da5184da63..596efcc769 100644 --- a/hikari/impl/rest_app.py +++ b/hikari/impl/rest_app.py @@ -96,7 +96,7 @@ def logger(self) -> logging.Logger: return self._logger @property - def thread_pool(self) -> typing.Optional[futures.ThreadPoolExecutor]: + def thread_pool_executor(self) -> typing.Optional[futures.ThreadPoolExecutor]: return None @property diff --git a/hikari/models/bases.py b/hikari/models/bases.py index fd0288dd9d..a33cf5d306 100644 --- a/hikari/models/bases.py +++ b/hikari/models/bases.py @@ -60,7 +60,9 @@ class Unique(typing.SupportsInt): integer ID of the object. """ - id: snowflake.Snowflake = attr.ib(converter=snowflake.Snowflake, eq=True, hash=True, repr=True) + id: snowflake.Snowflake = attr.ib( + converter=snowflake.Snowflake, eq=True, hash=True, repr=True, default=snowflake.Snowflake(0), + ) """The ID of this entity.""" @property diff --git a/hikari/models/colors.py b/hikari/models/colors.py index 876c681270..0aaf497987 100644 --- a/hikari/models/colors.py +++ b/hikari/models/colors.py @@ -20,7 +20,7 @@ from __future__ import annotations -__all__: typing.List[str] = ["Color"] +__all__: typing.List[str] = ["Color", "ColorLike"] import string import typing @@ -350,19 +350,7 @@ def from_bytes( return Color(int.from_bytes(bytes_, byteorder, signed=signed)) @classmethod - @typing.no_type_check - def of( - cls, - *values: typing.Union[ - Color, - typing.SupportsInt, - typing.Tuple[typing.SupportsInt, typing.SupportsInt, typing.SupportsInt], - typing.Tuple[typing.SupportsFloat, typing.SupportsFloat, typing.SupportsFloat], - typing.Sequence[typing.SupportsInt], - typing.Sequence[typing.SupportsFloat], - str, - ], - ) -> Color: + def of(cls, value: ColorLike, /) -> Color: """Convert the value to a `Color`. This attempts to determine the correct data format based on the @@ -370,7 +358,7 @@ def of( Parameters ---------- - values : ColorCompatibleT + value : ColorLike A color compatible values. Examples @@ -385,9 +373,6 @@ def of( >>> c = Color.of((255, 5, 26)) Color(r=0xff, g=0x5, b=1xa) - >>> c = Color.of(255, 5, 26) - Color(r=0xff, g=0x5, b=1xa) - >>> c = Color.of([0xFF, 0x5, 0x1a]) Color(r=0xff, g=0x5, b=1xa) @@ -409,32 +394,29 @@ def of( Color The Color object. """ - if len(values) == 1: - values = values[0] - - if isinstance(values, cls): - return values - if isinstance(values, int): - return cls.from_int(values) - if isinstance(values, (list, tuple)): - if len(values) != 3: - raise ValueError(f"color must be an RGB triplet if set to a {type(values).__name__} type") - - if any(isinstance(c, float) for c in values): - r, g, b = values + if isinstance(value, cls): + return value + if isinstance(value, int): + return cls.from_int(value) + if isinstance(value, (list, tuple)): + if len(value) != 3: + raise ValueError(f"color must be an RGB triplet if set to a {type(value).__name__} type") + + if any(isinstance(c, float) for c in value): + r, g, b = value return cls.from_rgb_float(r, g, b) - if all(isinstance(c, int) for c in values): - r, g, b = values + if all(isinstance(c, int) for c in value): + r, g, b = value return cls.from_rgb(r, g, b) - if isinstance(values, str): - is_start_hash_or_hex_literal = values.casefold().startswith(("#", "0x")) - is_hex_digits = all(c in string.hexdigits for c in values) and len(values) in (3, 6) + if isinstance(value, str): + is_start_hash_or_hex_literal = value.casefold().startswith(("#", "0x")) + is_hex_digits = all(c in string.hexdigits for c in value) and len(value) in (3, 6) if is_start_hash_or_hex_literal or is_hex_digits: - return cls.from_hex_code(values) + return cls.from_hex_code(value) - raise ValueError(f"Could not transform {values!r} into a {cls.__qualname__} object") + raise ValueError(f"Could not transform {value!r} into a {cls.__qualname__} object") def to_bytes(self, length: int, byteorder: str, *, signed: bool = True) -> bytes: """Convert the color code to bytes. @@ -455,3 +437,15 @@ def to_bytes(self, length: int, byteorder: str, *, signed: bool = True) -> bytes The bytes representation of the Color. """ return int(self).to_bytes(length, byteorder, signed=signed) + + +ColorLike = typing.Union[ + Color, + typing.SupportsInt, + typing.Tuple[typing.SupportsInt, typing.SupportsInt, typing.SupportsInt], + typing.Tuple[typing.SupportsFloat, typing.SupportsFloat, typing.SupportsFloat], + typing.Sequence[typing.SupportsInt], + typing.Sequence[typing.SupportsFloat], + str, +] +"""Type hint representing types of value compatible with a color type.""" diff --git a/hikari/models/colours.py b/hikari/models/colours.py index b2c1606097..f1c34bf1a4 100644 --- a/hikari/models/colours.py +++ b/hikari/models/colours.py @@ -20,9 +20,10 @@ from __future__ import annotations -__all__: typing.List[str] = ["Colour"] +__all__: typing.List[str] = ["Colour", "ColourLike"] # noinspection PyUnresolvedReferences import typing from hikari.models.colors import Color as Colour +from hikari.models.colors import ColorLike as ColourLike diff --git a/hikari/models/embeds.py b/hikari/models/embeds.py index a24ca4e6fe..3e1bece2d3 100644 --- a/hikari/models/embeds.py +++ b/hikari/models/embeds.py @@ -31,20 +31,13 @@ "EmbedField", ] -import copy import datetime import typing -import warnings -import weakref import attr -from hikari import errors from hikari.models import colors -from hikari.models import files - -if typing.TYPE_CHECKING: - from hikari.utilities import data_binding +from hikari.utilities import files _MAX_FOOTER_TEXT: typing.Final[int] = 2048 _MAX_AUTHOR_NAME: typing.Final[int] = 256 @@ -63,7 +56,7 @@ class EmbedFooter: text: typing.Optional[str] = attr.ib(default=None, repr=True) """The footer text, or `None` if not present.""" - icon_url: typing.Optional[str] = attr.ib(default=None, repr=False) + icon: typing.Optional[str] = attr.ib(default=None, repr=False) """The URL of the footer icon, or `None` if not present.""" proxy_icon_url: typing.Optional[str] = attr.ib(default=None, repr=False) @@ -81,10 +74,10 @@ class EmbedImage: """Represents an embed image.""" url: typing.Optional[str] = attr.ib(default=None, repr=True) - """The URL of the image to show, or `None` if not present.""" + """The image to show, or `None` if not present.""" - proxy_url: typing.Optional[str] = attr.ib(default=None, repr=False) - """The proxied URL of the image, or `None` if not present. + proxy_image: typing.Optional[str] = attr.ib(default=None, repr=False) + """The proxy image, or `None` if not present. !!! note This field cannot be set by bots or webhooks while sending an embed and @@ -197,10 +190,10 @@ class EmbedAuthor: This may be `None` if no hyperlink on the author's name is specified. """ - icon_url: typing.Optional[str] = attr.ib(default=None, repr=False) + icon: typing.Optional[str] = attr.ib(default=None, repr=False) """The URL of the author's icon, or `None` if not present.""" - proxy_icon_url: typing.Optional[str] = attr.ib(default=None, repr=False) + proxy_icon: typing.Optional[str] = attr.ib(default=None, repr=False) """The proxied URL of the author icon, or `None` if not present. !!! note @@ -228,26 +221,14 @@ class EmbedField: class Embed: """Represents an embed.""" + _color: typing.Optional[colors.Color] = attr.ib(default=None, repr=False) + title: typing.Optional[str] = attr.ib(default=None, repr=True) """The title of the embed, or `None` if not present.""" - @title.validator - def _title_check(self, _, value): # pylint:disable=unused-argument - if value is not None and len(value) > _MAX_EMBED_TITLE: - warnings.warn( - f"title must not exceed {_MAX_EMBED_TITLE} characters", category=errors.HikariWarning, - ) - description: typing.Optional[str] = attr.ib(default=None, repr=False) """The description of the embed, or `None` if not present.""" - @description.validator - def _description_check(self, _, value): # pylint:disable=unused-argument - if value is not None and len(value) > _MAX_EMBED_DESCRIPTION: - warnings.warn( - f"description must not exceed {_MAX_EMBED_DESCRIPTION} characters", category=errors.HikariWarning, - ) - url: typing.Optional[str] = attr.ib(default=None, repr=False) """The URL of the embed, or `None` if not present.""" @@ -308,21 +289,6 @@ def _description_check(self, _, value): # pylint:disable=unused-argument timestamps in debug logs, for example. """ - color: typing.Optional[colors.Color] = attr.ib( - converter=attr.converters.optional(colors.Color.of), default=None, repr=False - ) - """The colour of this embed. - - If `None`, the default is used for the user's colour-scheme when viewing it - (off-white on light-theme and off-black on dark-theme). - - !!! warning - Various bugs exist in the desktop client at the time of writing where - `#FFFFFF` is treated as as the default colour for your colour-scheme - rather than white. The current workaround appears to be using a slightly - off-white, such as `#DDDDDD` or `#FFFFFE` instead. - """ - footer: typing.Optional[EmbedFooter] = attr.ib(default=None, repr=False) """The footer of the embed, if present, otherwise `None`.""" @@ -357,259 +323,18 @@ def _description_check(self, _, value): # pylint:disable=unused-argument fields: typing.MutableSequence[EmbedField] = attr.ib(factory=list, repr=False) """The fields in the embed.""" - # Use a weakref so that clearing an image can pop the reference. - _assets_to_upload = attr.attrib(factory=weakref.WeakSet, repr=False) - @property - def assets_to_upload(self): - """File assets that need to be uploaded when sending the embed.""" - return self._assets_to_upload - - @staticmethod - def _extract_url(url) -> typing.Tuple[typing.Optional[str], typing.Optional[files.BaseStream]]: - if url is None: - return None, None - if isinstance(url, files.BaseStream): - return f"attachment://{url.filename}", url - return url, None - - def _maybe_ref_file_obj(self, file_obj) -> None: - if file_obj is not None: - # Store a _copy_ so weakreffing works properly. - obj_copy = copy.copy(file_obj) - self._assets_to_upload.add(obj_copy) - - def set_footer(self, *, text: typing.Optional[str], icon: typing.Optional[str, files.BaseStream] = None) -> Embed: - """Set the embed footer. - - Parameters - ---------- - text : str or None - The optional text to set for the footer. If `None`, the content is - cleared. - icon : hikari.models.files.BaseStream or str or None - The optional `hikari.models.files.BaseStream` or URL to the image to - set. - - Returns - ------- - Embed - This embed to allow method chaining. - """ - if text is not None: - # FIXME: move these validations to the dataclass. - if not text.strip(): - warnings.warn("footer.text must not be empty or purely of whitespaces", category=errors.HikariWarning) - elif len(text) > _MAX_FOOTER_TEXT: - warnings.warn( - f"footer.text must not exceed {_MAX_FOOTER_TEXT} characters", category=errors.HikariWarning - ) - - if icon is not None: - icon, file = self._extract_url(icon) - self.footer = EmbedFooter(text=text, icon_url=icon) - self._maybe_ref_file_obj(file) - elif self.footer is not None: - self.footer.icon_url = None - - return self - - def set_image(self, image: typing.Optional[str, files.BaseStream] = None) -> Embed: - """Set the embed image. - - Parameters - ---------- - image : hikari.models.files.BaseStream or str or None - The optional `hikari.models.files.BaseStream` or URL to the image - to set. If `None`, the image is removed. - - Returns - ------- - Embed - This embed to allow method chaining. - """ - if image is None: - self.image = None - else: - image, file = self._extract_url(image) - self.image = EmbedImage(url=image) - self._maybe_ref_file_obj(file) - return self - - def set_thumbnail(self, image: typing.Optional[str, files.BaseStream] = None) -> Embed: - """Set the thumbnail image. - - Parameters - ---------- - image: hikari.models.files.BaseStream or str or None - The optional `hikari.models.files.BaseStream` or URL to the image - to set. If `None`, the thumbnail is removed. - - Returns - ------- - Embed - This embed to allow method chaining. - """ - if image is None: - self.thumbnail = None - else: - image, file = self._extract_url(image) - self.thumbnail = EmbedThumbnail(url=image) - self._maybe_ref_file_obj(file) - return self - - def set_author( - self, - *, - name: typing.Optional[str] = None, - url: typing.Optional[str] = None, - icon: typing.Optional[str, files.BaseStream] = None, - ) -> Embed: - """Set the author of this embed. - - Parameters - ---------- - name: str or None - The optional authors name to display. - url: str or None - The optional URL to make the author text link to. - icon: hikari.models.files.BaseStream or str or None - The optional `hikari.models.files.BaseStream` or URL to the icon - to set. - - Returns - ------- - Embed - This embed to allow method chaining. - """ - if name is not None: - # TODO: move validation to dataclass - if name is not None and not name.strip(): - warnings.warn("author.name must not be empty or purely of whitespaces", category=errors.HikariWarning) - if name is not None and len(name) > _MAX_AUTHOR_NAME: - warnings.warn( - f"author.name must not exceed {_MAX_AUTHOR_NAME} characters", category=errors.HikariWarning - ) - - if icon is not None: - icon, icon_file = self._extract_url(icon) - self.author = EmbedAuthor(name=name, url=url, icon_url=icon) - self._maybe_ref_file_obj(icon_file) - elif self.author is not None: - self.author.icon_url = None - - return self - - def add_field(self, *, name: str, value: str, inline: bool = False, index: typing.Optional[int] = None) -> Embed: - """Add a field to this embed. - - Parameters - ---------- - name: str - The field name (title). - value: str - The field value. - inline: bool - If `True`, multiple consecutive fields may be displayed on the same - line. This is not guaranteed behaviour and only occurs if viewing - on desktop clients. Defaults to `False`. - index: int or None - The optional index to insert the field at. If `None`, it will append - to the end. - - Returns - ------- - Embed - This embed to allow method chaining. - """ - index = index if index is not None else len(self.fields) - if len(self.fields) >= _MAX_EMBED_FIELDS: - warnings.warn(f"no more than {_MAX_EMBED_FIELDS} fields can be stored", category=errors.HikariWarning) - - # TODO: move to dataclass. - if not name.strip(): - warnings.warn("field.name must not be empty or purely of whitespaces", category=errors.HikariWarning) - if len(name) > _MAX_FIELD_NAME: - warnings.warn(f"field.name must not exceed {_MAX_FIELD_NAME} characters", category=errors.HikariWarning) - - if not value.strip(): - warnings.warn("field.value must not be empty or purely of whitespaces", category=errors.HikariWarning) - if len(value) > _MAX_FIELD_VALUE: - warnings.warn(f"field.value must not exceed {_MAX_FIELD_VALUE} characters", category=errors.HikariWarning) - - self.fields.insert(index, EmbedField(name=name, value=value, is_inline=inline)) - return self - - # FIXME: use undefined.Undefined rather than `...` - def edit_field(self, index: int, /, *, name: str = ..., value: str = ..., inline: bool = ...) -> Embed: - """Edit a field in this embed at the given index. - - Unless you specify the attribute to change, it will not be changed. For - example, you can change a field value but not the field name - by simply specifying that parameter only. - - ```py - >>> embed = Embed() - >>> embed.add_field(name="foo", value="bar") - >>> embed.edit_field(0, value="baz") - >>> print(embed.fields[0].name) - foo - >>> print(embed.fields[0].value) - baz - ``` - - Parameters - ---------- - index: int - The index to edit the field at. - name: str - If specified, the new fields name (title). - value: str - If specified, the new fields value. - inline: bool - If specified, the whether to set the field to behave as if it were - inline or not. - - Returns - ------- - Embed - This embed to allow method chaining. - """ - # TODO: remove these checks entirely, they will be covered by the validation in the data class. - if name is not ... and not name.strip(): - warnings.warn("field.name must not be empty or purely of whitespaces", category=errors.HikariWarning) - if name is not ... and len(name.strip()) > _MAX_FIELD_NAME: - warnings.warn(f"field.name must not exceed {_MAX_FIELD_NAME} characters", category=errors.HikariWarning) - - if value is not ... and not value.strip(): - warnings.warn("field.value must not be empty or purely of whitespaces", category=errors.HikariWarning) - if value is not ... and len(value) > _MAX_FIELD_VALUE: - warnings.warn(f"field.value must not exceed {_MAX_FIELD_VALUE} characters", category=errors.HikariWarning) - - field = self.fields[index] - - field.name = name if name is not ... else field.name - field.value = value if value is not ... else field.value - field.is_inline = inline if value is not ... else field.is_inline - return self - - def remove_field(self, index: int) -> Embed: - """Remove a field from this embed at the given index. - - Parameters - ---------- - index: int - The index of the field to remove. - - Returns - ------- - Embed - This embed to allow method chaining. - - Raises - ------ - IndexError - If you referred to an index that doesn't exist. - """ - del self.fields[index] - return self + def color(self) -> typing.Optional[colors.Color]: + """Embed color, or `None` if not present.""" + return self._color + + @color.setter + def color(self, color: colors.ColorLike) -> None: + self._color = colors.Color.of(color) + + @color.deleter + def color(self) -> None: + self._color = None + + colour = color + """An alias for `color`.""" diff --git a/hikari/models/emojis.py b/hikari/models/emojis.py index dd4b7f8e8b..49130d9b43 100644 --- a/hikari/models/emojis.py +++ b/hikari/models/emojis.py @@ -29,8 +29,8 @@ import attr from hikari.models import bases -from hikari.models import files from hikari.utilities import cdn +from hikari.utilities import files if typing.TYPE_CHECKING: from hikari.models import users @@ -44,7 +44,7 @@ @attr.s(eq=True, hash=True, init=False, kw_only=True, slots=True) -class Emoji(files.BaseStream, abc.ABC): +class Emoji(files.WebResource, abc.ABC): """Base class for all emojis. Any emoji implementation supports being used as a `hikari.models.files.BaseStream` @@ -72,9 +72,6 @@ def is_mentionable(self) -> bool: def mention(self) -> str: """Mention string to use to mention the emoji with.""" - def __aiter__(self) -> typing.AsyncIterator[bytes]: - return files.WebResourceStream(self.filename, self.url).__aiter__() - @attr.s(eq=True, hash=True, init=False, kw_only=True, slots=True) class UnicodeEmoji(Emoji): diff --git a/hikari/models/files.py b/hikari/models/files.py deleted file mode 100644 index 0ef6ce28aa..0000000000 --- a/hikari/models/files.py +++ /dev/null @@ -1,571 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# Copyright © Nekoka.tt 2019-2020 -# -# This file is part of Hikari. -# -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . -"""Application used to make uploading data simpler. - -What should I use? ------------------- - -- **"I have a file I want to read."** - Use `FileStream`. -- **"The data is on a website or network resource."** - Use `WebResourceStream`. -- **"The data is in an `io.BytesIO` or `io.StringIO`."** - Use `ByteStream`. -- **"The data is a `bytes`, `bytearray`, `memoryview`, or `str`."** - Use `ByteStream`. -- **"The data is provided in an async iterable or async iterator."** - Use `ByteStream`. -- **"The data is in some other format."** - Convert the data to one of the above descriptions, or implement your - own provider by subclassing `BaseStream`. - -How exactly do I use each one of these? ---------------------------------------- -Check the documentation for each implementation to see examples and caveats. - -Why is this so complicated? ---------------------------- -Unfortunately, Python deals with async file IO in a really bad way. This means -that it is very easy to let IO operations impede the performance of your -application. Using these implementations correctly will enable you to mostly -offset that overhead. - -General implications of not using these implementations can include increased -memory usage, and the application becoming unresponsive during IO. -""" - -from __future__ import annotations - -__all__: typing.List[str] = ["BaseStream", "ByteStream", "WebResourceStream", "FileStream"] - -import abc -import asyncio -import base64 -import concurrent.futures -import contextlib -import functools -import http -import inspect -import io -import math -import os -import typing - -import aiohttp - -from hikari import errors -from hikari.utilities import aio - -# XXX: find optimal size. -MAGIC_NUMBER: typing.Final[int] = 128 * 1024 - - -class BaseStream(abc.ABC, typing.AsyncIterable[bytes]): - """A data stream that can be uploaded in a message or downloaded. - - This is a wrapper for an async iterable of bytes. - - Implementations should provide an `__aiter__` method yielding chunks - to upload. Chunks can be any size that is non-zero, but for performance - should be around 64-256KiB in size. - - Example - ------- - class HelloWorldStream(BaseStream): - def __init__(self): - super().__init__("hello-world.txt") - - async def __aiter__(self): - for byte in b"hello, world!": - yield byte - - stream = HelloWorldStream() - - You can also use an implementation to read contents into memory - - >>> stream = HelloWorldStream() - >>> data = await stream.read() - >>> print(data) - b"hello, world!" - - """ - - @property - @abc.abstractmethod - def filename(self) -> str: - """Filename for the file object.""" - - def __repr__(self) -> str: - return f"{type(self).__name__}(filename={self.filename!r})" - - async def fetch_data_uri(self) -> str: - """Generate a data URI for the given resource. - - This will only work for select image types that Discord supports, - currently. - - The type is resolved by reading the first 20 bytes of the resource - asynchronously. - - Returns - ------- - str - A base-64 encoded data URI. - - Raises - ------ - TypeError - If the data format is not supported. - """ - buff = await self.read(20) - - if buff[:8] == b"\211PNG\r\n\032\n": - img_type = "image/png" - elif buff[6:10] in (b"Exif", b"JFIF"): - img_type = "image/jpeg" - elif buff[:6] in (b"GIF87a", b"GIF89a"): - img_type = "image/gif" - elif buff.startswith(b"RIFF") and buff[8:12] == b"WEBP": - img_type = "image/webp" - else: - raise TypeError("Unsupported image type passed") - - image_data = base64.b64encode(buff).decode() - - return f"data:{img_type};base64,{image_data}" - - async def read(self, count: int = -1) -> bytes: - """Read from the data stream. - - Parameters - ---------- - count : int - The max number of bytes to read. If unspecified, the entire file - will be read. If the count is larger than the number of bytes in - the entire stream, then the entire stream will be returned. - - Returns - ------- - bytes - The bytes that were read. - """ - if count == -1: - count = float("inf") # type: ignore - - data = bytearray() - async for chunk in self: - if len(data) >= count: - break - - data.extend(chunk) - - count = len(data) if math.isinf(count) else count - return data[:count] - - -class _AsyncByteIterable: - __slots__ = ("_byte_content",) - - def __init__(self, byte_content: bytes) -> None: - self._byte_content = byte_content - - async def __aiter__(self) -> typing.AsyncGenerator[typing.Any, bytes]: - for i in range(0, len(self._byte_content), MAGIC_NUMBER): - yield self._byte_content[i : i + MAGIC_NUMBER] - - -class _MemorizedAsyncIteratorDecorator: - __slots__ = ("_async_iterator", "_exhausted", "_buff") - - def __init__(self, async_iterator: typing.AsyncIterator[bytes]) -> None: - self._async_iterator = async_iterator - self._exhausted = False - self._buff = bytearray() - - async def __aiter__(self) -> typing.AsyncGenerator[typing.Any, bytes]: - if self._exhausted: - async for chunk in _AsyncByteIterable(self._buff): - yield chunk - else: - async for chunk in self._async_iterator: - self._buff.extend(chunk) - yield chunk - self._exhausted = True - - -class ByteStream(BaseStream): - """A simple data stream that wraps something that gives bytes. - - For any asyncio-compatible stream that is an async iterator, or for - any in-memory data, you can use this to safely upload the information - in an API call. - - Parameters - ---------- - filename : str - The file name to use. - obj : byte-like provider - A provider of bytes-like objects. See the following sections for - what is acceptable. - - A byte-like provider can be one of the following types: - - - A bytes-like object (see below) - - **`AsyncIterator[bytes-like object]`** - - **`AsyncIterable[bytes-like object]`** - - **`AsyncGenerator[Any, bytes-like object]`** - an async generator that - yields bytes-like objects. - - **`io.BytesIO`** - - **`io.StringIO`** - - A bytes-like object can be one of the following types: - - - **bytes** - - **bytearray** - - **str** - - **memoryview** - - !!! warning - Do not pass blocking IO streams! - - You should not use this to wrap any IO or file-like object other - Standard Python file objects perform blocking IO, which will block - the event loop each time a chunk is read. - - To read a file, use `FileStream` instead. This will read the file - object incrementally, reducing memory usage significantly. - - Passing a different type of file object to this class may result in - undefined behaviour. - - !!! note - String objects get treated as UTF-8. - - String objects will always be treated as UTF-8 encoded byte objects. - If you need to use a different encoding, you should transform the - data manually into a bytes object first and pass the result to this - class. - - !!! note - Additional notes about performance. - - If you pass a bytes-like object, `io.BytesIO`, or `io.StringIO`, the - resource will be transformed internally into a bytes object, and - read in larger chunks using an `io.BytesIO` under the hood. This is done - to increase performance, as yielding individual bytes would be very - slow. - - If you pass an async iterator/iterable/generator directly, this will not - be collected into chunks. Whatever is yielded will be the chunk that is - uploaded. This allows for bit-inception with async iterators provided - by other libraries tidily. - - Examples - -------- - Passing bytes-like objects: - - >>> # A stream of bytes. - >>> stream = ByteStream("hello.txt", b"hello, world!") - - >>> # A stream from a bytearray. - >>> stream = ByteStream("hello.txt", bytearray(b"hello, world!")) - - >>> # A stream from a string. This will be treated as UTF-8 always. - >>> stream = ByteStream("hello.txt", "hello, world!") - - >>> # A stream from an io.BytesIO - >>> obj = io.BytesIO(some_data) - >>> stream = ByteStream("cat.png", obj) - - >>> # A stream from an io.StringIO - >>> obj = io.StringIO(some_data) - >>> stream = ByteStream("some_text.txt", obj) - - Passing async iterators, iterables: - - >>> stream = ByteStream("cat.png", some_async_iterator) - - >>> stream = ByteStream("cat.png", some_async_iterable) - - >>> stream = ByteStream("cat.png", some_asyncio_stream_reader) - - Passing async generators: - - >>> async def asyncgen(): - ... yield b"foo " - ... yield b"bar " - - >>> # You can pass the generator directly... - >>> stream = ByteStream("foobar.txt", asyncgen()) - - >>> # Or, if the generator function takes no parameters, you can pass the - >>> # function reference instead. - >>> stream = ByteStream("foobar.txt", asyncgen) - - Using a third-party non-blocking library such as `aiofiles` is possible - if you can pass an async iterator: - - >>> async with aiofiles.open("cat.png", "rb") as afp: - ... stream = ByteStream("cat.png", afp) - - !!! warning - Async iterators are read lazily. You should ensure in the latter - example that the `afp` is not closed before you use the stream in a - request. - - !!! note - `aiofiles` is not included with this library, and serves as an example - only. You can make use of `FileStream` instead if you need to read a - file using non-blocking IO. - - """ - - ___VALID_BYTE_TYPES___ = typing.Union[ - bytes, bytearray, str, memoryview, - ] - - ___VALID_TYPES___ = typing.Union[ - typing.Callable[[], typing.AsyncGenerator[typing.Any, ___VALID_BYTE_TYPES___]], - typing.AsyncGenerator[typing.Any, ___VALID_BYTE_TYPES___], - typing.AsyncIterator[___VALID_BYTE_TYPES___], - typing.AsyncIterable[___VALID_BYTE_TYPES___], - ___VALID_BYTE_TYPES___, - io.BytesIO, - io.StringIO, - ] - - _obj: typing.Union[ - typing.AsyncGenerator[typing.Any, ___VALID_BYTE_TYPES___], typing.AsyncIterable[___VALID_BYTE_TYPES___], - ] - - def __init__(self, filename: str, obj: ___VALID_TYPES___) -> None: - self._filename = filename - - if inspect.isasyncgenfunction(obj): - obj = obj() # type: ignore - - if inspect.isasyncgen(obj) or aio.is_async_iterator(obj): - self._obj = _MemorizedAsyncIteratorDecorator(obj) # type: ignore - return - - if aio.is_async_iterable(obj): - self._obj = obj # type: ignore - return - - if isinstance(obj, (io.StringIO, io.BytesIO)): - obj = obj.getvalue() - - if isinstance(obj, (str, memoryview, bytearray)): - obj = self._to_bytes(obj) - - if isinstance(obj, bytes): - self._obj = _AsyncByteIterable(obj) - return - - raise TypeError(f"Expected bytes-like object or async generator, got {type(obj).__qualname__}") - - def __aiter__(self) -> typing.AsyncGenerator[typing.Any, bytes]: - return self._obj.__aiter__() # type: ignore - - @property - def filename(self) -> str: - return self._filename - - async def _aiter_async_iterator( - self, async_iterator: typing.AsyncGenerator[typing.Any, ___VALID_BYTE_TYPES___] - ) -> typing.AsyncIterator[bytes]: - try: - while True: - yield self._to_bytes(await async_iterator.__anext__()) - except StopAsyncIteration: - pass - - @staticmethod - def _to_bytes(byte_like: ___VALID_BYTE_TYPES___) -> bytes: - if isinstance(byte_like, str): - return bytes(byte_like, "utf-8") - if isinstance(byte_like, memoryview): - return byte_like.tobytes() - if isinstance(byte_like, bytearray): - return bytes(byte_like) - if isinstance(byte_like, bytes): - return byte_like - raise TypeError(f"Expected bytes-like chunks, got {type(byte_like).__qualname__}") - - -class WebResourceStream(BaseStream): - """An async iterable of bytes that is represented by a web resource. - - Using this to upload an attachment will lazily load chunks and send them - using bit-inception, vastly reducing the memory overhead by not storing the - entire resource in memory before sending it. - - Parameters - ---------- - filename : str - The file name to use. - url : str - The URL to the resource to stream. - - Example - ------- - >>> stream = WebResourceStream("cat-not-found.png", "https://http.cat/404") - """ - - url: str - """The URL of the resource.""" - - def __init__(self, filename: str, url: str, /) -> None: - self._filename = filename - self.url = url - - async def __aiter__(self) -> typing.AsyncGenerator[typing.Any, bytes]: - async with aiohttp.request("GET", self.url) as response: - if 200 <= response.status < 300: - async for chunk in response.content: - yield chunk - return - - raw_body = await response.read() - - real_url = str(response.real_url) - - if response.status == http.HTTPStatus.BAD_REQUEST: - raise errors.BadRequest(real_url, response.headers, raw_body) - if response.status == http.HTTPStatus.UNAUTHORIZED: - raise errors.Unauthorized(real_url, response.headers, raw_body) - if response.status == http.HTTPStatus.FORBIDDEN: - raise errors.Forbidden(real_url, response.headers, raw_body) - if response.status == http.HTTPStatus.NOT_FOUND: - raise errors.NotFound(real_url, response.headers, raw_body) - - cls: typing.Type[errors.HikariError] - if 400 <= response.status < 500: - cls = errors.ClientHTTPErrorResponse - elif 500 <= response.status < 600: - cls = errors.ServerHTTPErrorResponse - else: - cls = errors.HTTPErrorResponse - - # noinspection PyArgumentList - raise cls(real_url, http.HTTPStatus(response.status), response.headers, raw_body) - - @property - def filename(self) -> str: - return self._filename - - -class FileStream(BaseStream): - r"""Asynchronous reader for a local file. - - Parameters - ---------- - filename : str or None - The custom file name to give the file when uploading it. May be - omitted. - path : os.PathLike or str - The path-like object that describes the file to upload. - - executor : concurrent.futures.Executor or None - An optional executor to run the IO operations in. If not specified, the - default executor for this loop will be used instead. - - Examples - -------- - Providing an explicit custom filename: - - >>> # UNIX/Linux/MacOS users - >>> FileStream("kitteh.png", "path/to/cat.png") - >>> FileStream("kitteh.png", "/mnt/cat-pictures/cat.png") - - >>> # Windows users - >>> FileStream("kitteh.png", r"Pictures\Cat.png") - >>> FileStream("kitteh.png", r"C:\Users\CatPerson\Pictures\Cat.png") - - Inferring the filename from the file path: - - >>> # UNIX/Linux/MacOS users - >>> FileStream("path/to/cat.png") - >>> FileStream("/mnt/cat-pictures/cat.png") - - >>> # Windows users - >>> FileStream(r"Pictures\Cat.png") - >>> FileStream(r"C:\Users\CatPerson\Pictures\Cat.png") - - !!! note - This implementation only provides the basis for READING - a file without blocking. For writing to files asynchronously, - you should consider using a third-party library such as - `aiofiles` or `aiofile`, or using an `Executor` instead. - - !!! warning - While it is possible to use a ProcessPoolExecutor executor - implementation with this class, use is discouraged. Process pools - can only communicate using pipes or the pickle protocol, which - makes the vanilla Python implementation unsuitable for use with - async iterators. Since file handles cannot be pickled, the use - of a ProcessPoolExecutor will result in the entire file being read - in one chunk, which increases memory usage drastically. - """ - - @typing.overload - def __init__( - self, - filename: str, - path: typing.Union[str, os.PathLike], - /, - *, - executor: typing.Optional[concurrent.futures.Executor] = None, - ) -> None: - ... - - @typing.overload - def __init__( - self, path: typing.Union[str, os.PathLike], /, *, executor: typing.Optional[concurrent.futures.Executor] = None, - ) -> None: - ... - - def __init__( - self, *args: typing.Union[str, os.PathLike], executor: typing.Optional[concurrent.futures.Executor] = None - ) -> None: - if len(args) == 1: - self._filename = os.path.basename(args[0]) - self.path = args[0] - else: - self._filename, self.path = args - self._executor = executor - - async def __aiter__(self) -> typing.AsyncGenerator[typing.Any, bytes]: - loop = asyncio.get_event_loop() - # We cant use a process pool in the same way we do a thread pool, as - # we cannot pickle file objects that we pass between threads. - if isinstance(self._executor, concurrent.futures.ProcessPoolExecutor): - raise NotImplementedError("Cannot currently use a ProcessPoolExecutor to perform IO in this implementation") - else: - fp = await loop.run_in_executor(self._executor, functools.partial(open, self.path, "rb")) - try: - while chunk := await loop.run_in_executor(self._executor, fp.read, MAGIC_NUMBER): - yield chunk - finally: - with contextlib.suppress(IOError): - await loop.run_in_executor(self._executor, fp.close) - - @property - def filename(self) -> str: - return self._filename diff --git a/hikari/models/messages.py b/hikari/models/messages.py index c32acf51cb..aded2f4b3a 100644 --- a/hikari/models/messages.py +++ b/hikari/models/messages.py @@ -34,10 +34,11 @@ import enum import typing +import aiohttp import attr from hikari.models import bases -from hikari.models import files as files_ +from hikari.utilities import files as files_ from hikari.utilities import undefined if typing.TYPE_CHECKING: @@ -140,7 +141,7 @@ class MessageActivityType(int, enum.Enum): @attr.s(eq=True, hash=False, init=False, kw_only=True, slots=True) -class Attachment(bases.Unique, files_.BaseStream): +class Attachment(bases.Unique, files_.WebResource): """Represents a file attached to a message. You can use this object in the same way as a @@ -148,15 +149,15 @@ class Attachment(bases.Unique, files_.BaseStream): message, etc. """ + url: str = attr.ib(repr=True) + """The source URL of file.""" + filename: str = attr.ib(repr=True) """The name of the file.""" size: int = attr.ib(repr=True) """The size of the file in bytes.""" - url: str = attr.ib(repr=True) - """The source URL of file.""" - proxy_url: str = attr.ib(repr=False) """The proxied URL of file.""" @@ -166,9 +167,6 @@ class Attachment(bases.Unique, files_.BaseStream): width: typing.Optional[int] = attr.ib(repr=False) """The width of the image (if the file is an image).""" - def __aiter__(self) -> typing.AsyncGenerator[bytes]: - return files_.WebResourceStream(self.filename, self.url).__aiter__() - @attr.s(eq=True, hash=True, init=False, kw_only=True, slots=True) class Reaction: @@ -196,8 +194,12 @@ class MessageActivity: @attr.s(eq=True, hash=False, init=False, kw_only=True, slots=True) -class MessageCrosspost(bases.Entity, bases.Unique): - """Represents information about a cross-posted message and the origin of the original message.""" +class MessageCrosspost(bases.Entity): + """Represents information about a cross-posted message. + + This is a message that is sent in one channel/guild and may be + "published" to another. + """ id: typing.Optional[snowflake.Snowflake] = attr.ib(repr=True) """The ID of the message. @@ -395,8 +397,8 @@ async def reply( # pylint:disable=line-too-long text: typing.Union[undefined.Undefined, str] = undefined.Undefined(), *, embed: typing.Union[undefined.Undefined, embeds_.Embed] = undefined.Undefined(), - attachments: typing.Sequence[files_.BaseStream] = undefined.Undefined(), - mentions_everyone: bool = True, + attachments: typing.Union[undefined.Undefined, typing.Sequence[files_.Resource]] = undefined.Undefined(), + mentions_everyone: bool = False, user_mentions: typing.Union[ typing.Collection[typing.Union[snowflake.Snowflake, int, str, users.User]], bool ] = True, @@ -418,7 +420,7 @@ async def reply( # pylint:disable=line-too-long and can usually be ignored. tts : bool or hikari.utilities.undefined.Undefined If specified, whether the message will be sent as a TTS message. - attachments : typing.Sequence[hikari.models.files.BaseStream] + attachments : typing.Sequence[hikari.models.files.BaseStream] or hikari.utilities.undefined.Undefined If specified, a sequence of attachments to upload, if desired. Should be between 1 and 10 objects in size (inclusive), also including embed attachments. @@ -491,7 +493,7 @@ async def add_reaction(self, emoji: typing.Union[str, emojis_.Emoji]) -> None: Parameters ---------- - emoji : hikari.models.emojis.Emoji or str + emoji : str or hikari.models.emojis.Emoji The emoji to add. Examples @@ -510,31 +512,37 @@ async def add_reaction(self, emoji: typing.Union[str, emojis_.Emoji]) -> None: Raises ------ + hikari.errors.BadRequest + If the emoji is invalid, unknown, or formatted incorrectly. hikari.errors.Forbidden If this is the first reaction using this specific emoji on this message and you lack the `ADD_REACTIONS` permission. If you lack `READ_MESSAGE_HISTORY`, this may also raise this error. hikari.errors.NotFound - If the channel or message is not found, or if the emoji is not found. - hikari.errors.BadRequest - If the emoji is invalid, unknown, or formatted incorrectly. - If any invalid snowflake IDs are passed; a snowflake may be invalid - due to it being outside of the range of a 64 bit integer. + If the channel or message is not found, or if the emoji is not + found. + !!! note + This will also occur if you try to add an emoji from a + guild you are not part of if no one else has previously + reacted with the same emoji. """ await self._app.rest.add_reaction(channel=self.channel_id, message=self.id, emoji=emoji) async def remove_reaction( - self, emoji: typing.Union[str, emojis_.Emoji], *, user: typing.Optional[users.User] = None + self, + emoji: typing.Union[str, emojis_.Emoji], + *, + user: typing.Union[users.User, undefined.Undefined] = undefined.Undefined(), ) -> None: r"""Remove a reaction from this message. Parameters ---------- - emoji : hikari.models.emojis.Emoji or str + emoji : str or hikari.models.emojis.Emoji The emoji to remove. - user : hikari.models.users.User or None - The user of the reaction to remove. If `None`, then the bot's + user : hikari.models.users.User or hikari.utilities.undefined.Undefined + The user of the reaction to remove. If unspecified, then the bot's reaction is removed instead. Examples @@ -553,6 +561,10 @@ async def remove_reaction( Raises ------ + hikari.errors.BadRequest + If the emoji is invalid, unknown, or formatted incorrectly. + If any invalid snowflake IDs are passed; a snowflake may be invalid + due to it being outside of the range of a 64 bit integer. hikari.errors.Forbidden If this is the first reaction using this specific emoji on this message and you lack the `ADD_REACTIONS` permission. If you lack @@ -560,22 +572,24 @@ async def remove_reaction( remove the reaction of another user without `MANAGE_MESSAGES`, this will be raised. hikari.errors.NotFound - If the channel or message is not found, or if the emoji is not found. - hikari.errors.BadRequest - If the emoji is invalid, unknown, or formatted incorrectly. - If any invalid snowflake IDs are passed; a snowflake may be invalid - due to it being outside of the range of a 64 bit integer. + If the channel or message is not found, or if the emoji is not + found. """ - await self._app.rest.delete_reaction(channel=self.channel_id, message=self.id, emoji=emoji, user=user) + if isinstance(user, undefined.Undefined): + await self._app.rest.delete_my_reaction(channel=self.channel_id, message=self.id, emoji=emoji) + else: + await self._app.rest.delete_reaction(channel=self.channel_id, message=self.id, emoji=emoji, user=user) - async def remove_all_reactions(self, emoji: typing.Optional[typing.Union[str, emojis_.Emoji]] = None) -> None: + async def remove_all_reactions( + self, emoji: typing.Union[str, emojis_.Emoji, undefined.Undefined] = undefined.Undefined() + ) -> None: r"""Remove all users' reactions for a specific emoji from the message. Parameters ---------- - emoji : hikari.models.emojis.Emoji or str or None - The emoji to remove all reactions for. If not specified, or `None`, - then all emojis are removed. + emoji : str hikari.models.emojis.Emoji or hikari.utilities.undefined.Undefined + The emoji to remove all reactions for. If not specified, then all + emojis are removed. Example -------- @@ -592,13 +606,14 @@ async def remove_all_reactions(self, emoji: typing.Optional[typing.Union[str, em If you are missing the `MANAGE_MESSAGES` permission, or the permission to view the channel hikari.errors.NotFound - If the channel or message is not found, or if the emoji is not found. + If the channel or message is not found, or if the emoji is not + found. hikari.errors.BadRequest If the emoji is invalid, unknown, or formatted incorrectly. If any invalid snowflake IDs are passed; a snowflake may be invalid due to it being outside of the range of a 64 bit integer. """ - if emoji is None: + if isinstance(emoji, undefined.Undefined): await self._app.rest.delete_all_reactions(channel=self.channel_id, message=self.id) else: await self._app.rest.delete_all_reactions_for_emoji(channel=self.channel_id, message=self.id, emoji=emoji) diff --git a/hikari/net/http_client.py b/hikari/net/http_client.py index 54b94d259e..02c417c1cd 100644 --- a/hikari/net/http_client.py +++ b/hikari/net/http_client.py @@ -22,18 +22,31 @@ __all__: typing.List[str] = ["HTTPClient"] import abc +import http import json import logging import ssl import types import typing +import aiohttp.client import aiohttp.typedefs +from hikari import errors from hikari.net import tracing +from hikari.utilities import data_binding -if typing.TYPE_CHECKING: - from hikari.utilities import data_binding + +try: + # noinspection PyProtectedMember + RequestContextManager = aiohttp.client._RequestContextManager + """Type hint for an AIOHTTP session context manager. + + This is stored as aiohttp does not expose the type-hint directly, despite + exposing the rest of the API it is part of. + """ +except NameError: + RequestContextManager = typing.Any # type: ignore class HTTPClient(abc.ABC): # pylint:disable=too-many-instance-attributes @@ -80,7 +93,7 @@ class HTTPClient(abc.ABC): # pylint:disable=too-many-instance-attributes __slots__ = ( "logger", - "__client_session", + "_client_session", "_allow_redirects", "_connector", "_debug", @@ -169,7 +182,7 @@ def __init__( ) -> None: self.logger = logger - self.__client_session: typing.Optional[aiohttp.ClientSession] = None + self._client_session: typing.Optional[aiohttp.ClientSession] = None self._allow_redirects = allow_redirects self._connector = connector self._debug = debug @@ -192,41 +205,48 @@ async def __aexit__( async def close(self) -> None: """Close the client safely.""" - if self.__client_session is not None: - await self.__client_session.close() - self.logger.debug("closed client session object %r", self.__client_session) - self.__client_session = None + if self._client_session is not None: + await self._client_session.close() + self.logger.debug("closed client session object %r", self._client_session) + self._client_session = None - def _acquire_client_session(self) -> aiohttp.ClientSession: + def client_session(self) -> aiohttp.ClientSession: """Acquire a client session to make requests with. + !!! warning + This must be invoked within a coroutine running in an event loop, + or the behaviour will be undefined. + + Generally you should not need to use this unless you are interfacing + with the Hikari API directly. + Returns ------- aiohttp.ClientSession The client session to use for requests. """ - if self.__client_session is None: - self.__client_session = aiohttp.ClientSession( + if self._client_session is None: + self._client_session = aiohttp.ClientSession( connector=self._connector, trust_env=self._trust_env, version=aiohttp.HttpVersion11, json_serialize=json.dumps, trace_configs=[t.trace_config for t in self._tracers], ) - self.logger.debug("acquired new client session object %r", self.__client_session) - return self.__client_session + self.logger.debug("acquired new client session object %r", self._client_session) + return self._client_session - async def _perform_request( + def _perform_request( self, *, method: str, url: str, - headers: data_binding.Headers, + headers: data_binding.Headers = typing.cast(data_binding.Headers, types.MappingProxyType({})), body: typing.Union[ data_binding.JSONObjectBuilder, aiohttp.FormData, data_binding.JSONObject, data_binding.JSONArray, None - ], - query: typing.Union[data_binding.Query, data_binding.StringMapBuilder, None], - ) -> aiohttp.ClientResponse: + ] = None, + query: typing.Union[data_binding.Query, data_binding.StringMapBuilder, None] = None, + ) -> RequestContextManager: """Make an HTTP request and return the response. Parameters @@ -261,7 +281,7 @@ async def _perform_request( trace_request_ctx = types.SimpleNamespace() trace_request_ctx.request_body = body - return await self._acquire_client_session().request( + return self.client_session().request( method=method, url=url, params=query, @@ -302,7 +322,7 @@ async def _create_ws( The websocket to use. """ self.logger.debug("creating underlying websocket object from HTTP session") - return await self._acquire_client_session().ws_connect( + return await self.client_session().ws_connect( url=url, compress=compress, autoping=auto_ping, @@ -313,3 +333,31 @@ async def _create_ws( verify_ssl=self._verify_ssl, ssl_context=self._ssl_context, ) + + +async def parse_error_response(response: aiohttp.ClientResponse) -> typing.NoReturn: + """Given an erroneous HTTP response, raise a corresponding exception.""" + real_url = str(response.real_url) + raw_body = await response.read() + + if response.status == http.HTTPStatus.BAD_REQUEST: + raise errors.BadRequest(real_url, response.headers, raw_body) + if response.status == http.HTTPStatus.UNAUTHORIZED: + raise errors.Unauthorized(real_url, response.headers, raw_body) + if response.status == http.HTTPStatus.FORBIDDEN: + raise errors.Forbidden(real_url, response.headers, raw_body) + if response.status == http.HTTPStatus.NOT_FOUND: + raise errors.NotFound(real_url, response.headers, raw_body) + + # noinspection PyArgumentList + status = http.HTTPStatus(response.status) + + cls: typing.Type[errors.HikariError] + if 400 <= status < 500: + cls = errors.ClientHTTPErrorResponse + elif 500 <= status < 600: + cls = errors.ServerHTTPErrorResponse + else: + cls = errors.HTTPErrorResponse + + raise cls(real_url, status, response.headers, raw_body) diff --git a/hikari/net/rest.py b/hikari/net/rest.py index b1842c3fe9..5823cd0e41 100644 --- a/hikari/net/rest.py +++ b/hikari/net/rest.py @@ -23,6 +23,7 @@ __all__: typing.List[str] = ["REST"] import asyncio +import contextlib import datetime import http import typing @@ -40,6 +41,7 @@ from hikari.net import routes from hikari.utilities import data_binding from hikari.utilities import date +from hikari.utilities import files from hikari.utilities import klass from hikari.utilities import snowflake from hikari.utilities import undefined @@ -54,7 +56,6 @@ from hikari.models import colors from hikari.models import embeds as embeds_ from hikari.models import emojis - from hikari.models import files from hikari.models import gateway from hikari.models import guilds from hikari.models import invites @@ -64,6 +65,7 @@ from hikari.models import voices from hikari.models import webhooks + _REST_API_URL: typing.Final[str] = "https://discord.com/api/v{0.version}" """The URL for the RESTSession API. This contains a version number parameter that should be interpolated. @@ -225,12 +227,11 @@ async def _request_once( await asyncio.gather(self.buckets.acquire(compiled_route), self.global_rate_limit.acquire()) # Make the request. + # noinspection PyUnresolvedReferences response = await self._perform_request( method=compiled_route.method, url=url, headers=headers, body=body, query=query ) - real_url = str(response.real_url) - # Ensure we aren't rate limited, and update rate limiting headers where appropriate. await self._handle_rate_limits_for_response(compiled_route, response) @@ -239,36 +240,20 @@ async def _request_once( if response.status == http.HTTPStatus.NO_CONTENT: return None - raw_body = await response.read() - # Handle the response. if 200 <= response.status < 300: if response.content_type == self._APPLICATION_JSON: # Only deserializing here stops Cloudflare shenanigans messing us around. - return data_binding.load_json(raw_body) + return data_binding.load_json(await response.read()) + + real_url = str(response.real_url) raise errors.HTTPError(real_url, f"Expected JSON response but received {response.content_type}") - if response.status == http.HTTPStatus.BAD_REQUEST: - raise errors.BadRequest(real_url, response.headers, raw_body) - if response.status == http.HTTPStatus.UNAUTHORIZED: - raise errors.Unauthorized(real_url, response.headers, raw_body) - if response.status == http.HTTPStatus.FORBIDDEN: - raise errors.Forbidden(real_url, response.headers, raw_body) - if response.status == http.HTTPStatus.NOT_FOUND: - raise errors.NotFound(real_url, response.headers, raw_body) - - # noinspection PyArgumentList - status = http.HTTPStatus(response.status) - - cls: typing.Type[errors.HikariError] - if 400 <= status < 500: - cls = errors.ClientHTTPErrorResponse - elif 500 <= status < 600: - cls = errors.ServerHTTPErrorResponse - else: - cls = errors.HTTPErrorResponse + await self._handle_error_response(response) - raise cls(real_url, status, response.headers, raw_body) + @staticmethod + async def _handle_error_response(response) -> typing.NoReturn: + return await http_client.parse_error_response(response) async def _handle_rate_limits_for_response( self, compiled_route: routes.CompiledRoute, response: aiohttp.ClientResponse @@ -388,17 +373,6 @@ def _generate_allowed_mentions( return allowed_mentions - def _build_message_creation_form( - self, payload: data_binding.JSONObject, attachments: typing.Sequence[files.BaseStream], - ) -> aiohttp.FormData: - form = data_binding.URLEncodedForm() - form.add_field("payload_json", data_binding.dump_json(payload), content_type=self._APPLICATION_JSON) - for i, attachment in enumerate(attachments): - form.add_field( - f"file{i}", attachment, filename=attachment.filename, content_type=self._APPLICATION_OCTET_STREAM - ) - return form - async def close(self) -> None: """Close the REST client and any open HTTP connections.""" await super().close() @@ -1052,7 +1026,7 @@ async def create_message( text: typing.Union[undefined.Undefined, typing.Any] = undefined.Undefined(), *, embed: typing.Union[undefined.Undefined, embeds_.Embed] = undefined.Undefined(), - attachments: typing.Union[undefined.Undefined, typing.Sequence[files.BaseStream]] = undefined.Undefined(), + attachments: typing.Union[undefined.Undefined, typing.Sequence[files.Resource]] = undefined.Undefined(), tts: typing.Union[undefined.Undefined, bool] = undefined.Undefined(), nonce: typing.Union[undefined.Undefined, str] = undefined.Undefined(), mentions_everyone: bool = True, @@ -1070,7 +1044,7 @@ async def create_message( If specified, the message contents. embed : hikari.utilities.undefined.Undefined or hikari.models.embeds.Embed If specified, the message embed. - attachments : hikari.utilities.undefined.Undefined or typing.Sequence[hikari.models.files.BaseStream] + attachments : hikari.utilities.undefined.Undefined or typing.Sequence[hikari.utilities.files.Resource] If specified, the message attachments. tts : hikari.utilities.undefined.Undefined or bool If specified, whether the message will be TTS (Text To Speech). @@ -1126,12 +1100,27 @@ async def create_message( attachments = [] if isinstance(attachments, undefined.Undefined) else [a for a in attachments] - if not isinstance(embed, undefined.Undefined): - attachments.extend(embed.assets_to_upload) + # TODO: embed attachments... + + if attachments: + form = data_binding.URLEncodedForm() + form.add_field("payload_json", data_binding.dump_json(body), content_type=self._APPLICATION_JSON) + + stack = contextlib.AsyncExitStack() + + try: + for i, attachment in enumerate(attachments): + stream = await stack.enter_async_context(attachment.stream()) + form.add_field( + f"file{i}", stream, filename=stream.filename, content_type=self._APPLICATION_OCTET_STREAM + ) + + raw_response = await self._request(route, body=form) + finally: + await stack.aclose() + else: + raw_response = await self._request(route, body=body) - raw_response = await self._request( - route, body=self._build_message_creation_form(body, attachments) if attachments else body - ) response = typing.cast(data_binding.JSONObject, raw_response) return self._app.entity_factory.deserialize_message(response) @@ -1139,9 +1128,9 @@ async def edit_message( self, channel: typing.Union[channels.TextChannel, bases.UniqueObject], message: typing.Union[messages_.Message, bases.UniqueObject], - text: typing.Union[undefined.Undefined, typing.Any] = undefined.Undefined(), + text: typing.Union[undefined.Undefined, None, typing.Any] = undefined.Undefined(), *, - embed: typing.Union[undefined.Undefined, embeds_.Embed] = undefined.Undefined(), + embed: typing.Union[undefined.Undefined, None, embeds_.Embed] = undefined.Undefined(), mentions_everyone: typing.Union[undefined.Undefined, bool] = undefined.Undefined(), user_mentions: typing.Union[ undefined.Undefined, typing.Collection[typing.Union[users.User, bases.UniqueObject]], bool @@ -1195,10 +1184,19 @@ async def edit_message( """ route = routes.PATCH_CHANNEL_MESSAGE.compile(channel=channel, message=message) body = data_binding.JSONObjectBuilder() - body.put("content", text, conversion=str) - body.put("embed", embed, conversion=self._app.entity_factory.serialize_embed) body.put("flags", flags) body.put("allowed_mentions", self._generate_allowed_mentions(mentions_everyone, user_mentions, role_mentions)) + + if text is not None: + body.put("content", text, conversion=str) + else: + body.put("content", None) + + if embed is not None: + body.put("embed", embed, conversion=self._app.entity_factory.serialize_embed) + else: + body.put("embed", None) + raw_response = await self._request(route, body=body) response = typing.cast(data_binding.JSONObject, raw_response) return self._app.entity_factory.deserialize_message(response) @@ -1269,7 +1267,7 @@ async def add_reaction( self, channel: typing.Union[channels.TextChannel, bases.UniqueObject], message: typing.Union[messages_.Message, bases.UniqueObject], - emoji: typing.Union[str, emojis.UnicodeEmoji, emojis.KnownCustomEmoji], + emoji: typing.Union[str, emojis.Emoji], ) -> None: """Add a reaction emoji to a message in a given channel. @@ -1294,7 +1292,7 @@ async def add_reaction( If an internal error occurs on Discord while handling the request. """ route = routes.PUT_MY_REACTION.compile( - emoji=emoji.url_name if isinstance(emoji, emojis.KnownCustomEmoji) else str(emoji), + emoji=emoji.url_name if isinstance(emoji, emojis.CustomEmoji) else str(emoji), channel=channel, message=message, ) @@ -1304,7 +1302,7 @@ async def delete_my_reaction( self, channel: typing.Union[channels.TextChannel, bases.UniqueObject], message: typing.Union[messages_.Message, bases.UniqueObject], - emoji: typing.Union[str, emojis.UnicodeEmoji, emojis.KnownCustomEmoji], + emoji: typing.Union[str, emojis.Emoji], ) -> None: """Delete a reaction that your application user created. @@ -1327,7 +1325,7 @@ async def delete_my_reaction( If an internal error occurs on Discord while handling the request. """ route = routes.DELETE_MY_REACTION.compile( - emoji=emoji.url_name if isinstance(emoji, emojis.KnownCustomEmoji) else str(emoji), + emoji=emoji.url_name if isinstance(emoji, emojis.CustomEmoji) else str(emoji), channel=channel, message=message, ) @@ -1337,10 +1335,10 @@ async def delete_all_reactions_for_emoji( self, channel: typing.Union[channels.TextChannel, bases.UniqueObject], message: typing.Union[messages_.Message, bases.UniqueObject], - emoji: typing.Union[str, emojis.UnicodeEmoji, emojis.KnownCustomEmoji], + emoji: typing.Union[str, emojis.Emoji], ) -> None: route = routes.DELETE_REACTION_EMOJI.compile( - emoji=emoji.url_name if isinstance(emoji, emojis.KnownCustomEmoji) else str(emoji), + emoji=emoji.url_name if isinstance(emoji, emojis.CustomEmoji) else str(emoji), channel=channel, message=message, ) @@ -1350,11 +1348,11 @@ async def delete_reaction( self, channel: typing.Union[channels.TextChannel, bases.UniqueObject], message: typing.Union[messages_.Message, bases.UniqueObject], - emoji: typing.Union[str, emojis.UnicodeEmoji, emojis.KnownCustomEmoji], + emoji: typing.Union[str, emojis.Emoji], user: typing.Union[users.User, bases.UniqueObject], ) -> None: route = routes.DELETE_REACTION_USER.compile( - emoji=emoji.url_name if isinstance(emoji, emojis.KnownCustomEmoji) else str(emoji), + emoji=emoji.url_name if isinstance(emoji, emojis.CustomEmoji) else str(emoji), channel=channel, message=message, user=user, @@ -1373,14 +1371,14 @@ def fetch_reactions_for_emoji( self, channel: typing.Union[channels.TextChannel, bases.UniqueObject], message: typing.Union[messages_.Message, bases.UniqueObject], - emoji: typing.Union[str, emojis.UnicodeEmoji, emojis.KnownCustomEmoji], + emoji: typing.Union[str, emojis.Emoji], ) -> iterators.LazyIterator[users.User]: return iterators.ReactorIterator( app=self._app, request_call=self._request, channel_id=str(int(channel)), message_id=str(int(message)), - emoji=emoji.url_name if isinstance(emoji, emojis.KnownCustomEmoji) else str(emoji), + emoji=emoji.url_name if isinstance(emoji, emojis.CustomEmoji) else str(emoji), ) async def create_webhook( @@ -1388,14 +1386,15 @@ async def create_webhook( channel: typing.Union[channels.TextChannel, bases.UniqueObject], name: str, *, - avatar: typing.Union[undefined.Undefined, files.BaseStream] = undefined.Undefined(), + avatar: typing.Union[undefined.Undefined, files.Resource] = undefined.Undefined(), reason: typing.Union[undefined.Undefined, str] = undefined.Undefined(), ) -> webhooks.Webhook: route = routes.POST_WEBHOOK.compile(channel=channel) body = data_binding.JSONObjectBuilder() body.put("name", name) if not isinstance(avatar, undefined.Undefined): - body.put("avatar", await avatar.fetch_data_uri()) + async with avatar.stream() as stream: + body.put("avatar", await stream.data_uri()) raw_response = await self._request(route, body=body, reason=reason) response = typing.cast(data_binding.JSONObject, raw_response) @@ -1439,7 +1438,7 @@ async def edit_webhook( *, token: typing.Union[undefined.Undefined, str] = undefined.Undefined(), name: typing.Union[undefined.Undefined, str] = undefined.Undefined(), - avatar: typing.Union[None, undefined.Undefined, files.BaseStream] = undefined.Undefined(), + avatar: typing.Union[None, undefined.Undefined, files.Resource] = undefined.Undefined(), channel: typing.Union[undefined.Undefined, channels.TextChannel, bases.UniqueObject] = undefined.Undefined(), reason: typing.Union[undefined.Undefined, str] = undefined.Undefined(), ) -> webhooks.Webhook: @@ -1452,7 +1451,11 @@ async def edit_webhook( body.put("name", name) body.put_snowflake("channel", channel) if not isinstance(avatar, undefined.Undefined): - body.put("avatar", await avatar.fetch_data_uri() if avatar is not None else None) + if avatar is None: + body.put("avatar", None) + else: + async with avatar.stream() as stream: + body.put("avatar", await stream.data_uri()) raw_response = await self._request(route, body=body, reason=reason) response = typing.cast(data_binding.JSONObject, raw_response) @@ -1480,7 +1483,7 @@ async def execute_webhook( username: typing.Union[undefined.Undefined, str] = undefined.Undefined(), avatar_url: typing.Union[undefined.Undefined, str] = undefined.Undefined(), embeds: typing.Union[undefined.Undefined, typing.Sequence[embeds_.Embed]] = undefined.Undefined(), - attachments: typing.Union[undefined.Undefined, typing.Sequence[files.BaseStream]] = undefined.Undefined(), + attachments: typing.Union[undefined.Undefined, typing.Sequence[files.Resource]] = undefined.Undefined(), tts: typing.Union[undefined.Undefined, bool] = undefined.Undefined(), mentions_everyone: bool = True, user_mentions: typing.Union[typing.Collection[typing.Union[users.User, bases.UniqueObject]], bool] = True, @@ -1498,7 +1501,7 @@ async def execute_webhook( if not isinstance(embeds, undefined.Undefined): for embed in embeds: - attachments.extend(embed.assets_to_upload) + # TODO: embed attachments. serialized_embeds.append(self._app.entity_factory.serialize_embed(embed)) body = data_binding.JSONObjectBuilder() @@ -1510,9 +1513,23 @@ async def execute_webhook( body.put("tts", tts) body.put("wait", True) - raw_response = await self._request( - route, body=self._build_message_creation_form(body, attachments) if attachments else body, no_auth=no_auth, - ) + if attachments: + form = data_binding.URLEncodedForm() + form.add_field("payload_json", data_binding.dump_json(body), content_type=self._APPLICATION_JSON) + + stack = contextlib.AsyncExitStack() + + try: + for i, attachment in enumerate(attachments): + stream, filename = await stack.enter_async_context(attachment.to_attachment(self._app)) + form.add_field(f"file{i}", stream, filename=filename, content_type=self._APPLICATION_OCTET_STREAM) + + raw_response = await self._request(route, body=form, no_auth=no_auth) + finally: + await stack.aclose() + else: + raw_response = await self._request(route, body=body, no_auth=no_auth) + response = typing.cast(data_binding.JSONObject, raw_response) return self._app.entity_factory.deserialize_message(response) @@ -1551,14 +1568,15 @@ async def edit_my_user( self, *, username: typing.Union[undefined.Undefined, str] = undefined.Undefined(), - avatar: typing.Union[undefined.Undefined, None, files.BaseStream] = undefined.Undefined(), + avatar: typing.Union[undefined.Undefined, None, files.Resource] = undefined.Undefined(), ) -> users.OwnUser: route = routes.PATCH_MY_USER.compile() body = data_binding.JSONObjectBuilder() body.put("username", username) - if isinstance(avatar, files.BaseStream): - body.put("avatar", await avatar.fetch_data_uri()) + if isinstance(avatar, files.Resource): + async with avatar.stream() as stream: + body.put("avatar", await stream.data_uri()) else: body.put("avatar", avatar) @@ -1693,7 +1711,7 @@ async def create_emoji( self, guild: typing.Union[guilds.Guild, bases.UniqueObject], name: str, - image: files.BaseStream, + image: files.Resource, *, roles: typing.Union[ undefined.Undefined, typing.Collection[typing.Union[guilds.Role, bases.UniqueObject]] @@ -1704,7 +1722,8 @@ async def create_emoji( body = data_binding.JSONObjectBuilder() body.put("name", name) if not isinstance(image, undefined.Undefined): - body.put("image", await image.fetch_data_uri()) + async with image.stream() as stream: + body.put("image", await stream.data_uri()) body.put_snowflake_array("roles", roles) @@ -1782,10 +1801,10 @@ async def edit_guild( undefined.Undefined, channels.GuildVoiceChannel, bases.UniqueObject ] = undefined.Undefined(), afk_timeout: typing.Union[undefined.Undefined, date.TimeSpan] = undefined.Undefined(), - icon: typing.Union[undefined.Undefined, files.BaseStream] = undefined.Undefined(), + icon: typing.Union[undefined.Undefined, None, files.Resource] = undefined.Undefined(), owner: typing.Union[undefined.Undefined, users.User, bases.UniqueObject] = undefined.Undefined(), - splash: typing.Union[undefined.Undefined, files.BaseStream] = undefined.Undefined(), - banner: typing.Union[undefined.Undefined, files.BaseStream] = undefined.Undefined(), + splash: typing.Union[undefined.Undefined, None, files.Resource] = undefined.Undefined(), + banner: typing.Union[undefined.Undefined, None, files.Resource] = undefined.Undefined(), system_channel: typing.Union[undefined.Undefined, channels.GuildTextChannel] = undefined.Undefined(), rules_channel: typing.Union[undefined.Undefined, channels.GuildTextChannel] = undefined.Undefined(), public_updates_channel: typing.Union[undefined.Undefined, channels.GuildTextChannel] = undefined.Undefined(), @@ -1807,14 +1826,28 @@ async def edit_guild( body.put_snowflake("rules_channel_id", rules_channel) body.put_snowflake("public_updates_channel_id", public_updates_channel) + # FIXME: gather these futures simultaneously for a 3x speedup... + if not isinstance(icon, undefined.Undefined): - body.put("icon", await icon.fetch_data_uri()) + if icon is None: + body.put("icon", None) + else: + async with icon.stream() as stream: + body.put("icon", await stream.data_uri()) if not isinstance(splash, undefined.Undefined): - body.put("splash", await splash.fetch_data_uri()) + if splash is None: + body.put("splash", None) + else: + async with splash.stream() as stream: + body.put("splash", await stream.data_uri()) if not isinstance(banner, undefined.Undefined): - body.put("banner", await banner.fetch_data_uri()) + if banner is None: + body.put("banner", None) + else: + async with banner.stream() as stream: + body.put("banner", await stream.data_uri()) raw_response = await self._request(route, body=body, reason=reason) response = typing.cast(data_binding.JSONObject, raw_response) diff --git a/hikari/utilities/data_binding.py b/hikari/utilities/data_binding.py index b017490327..4264e53bb9 100644 --- a/hikari/utilities/data_binding.py +++ b/hikari/utilities/data_binding.py @@ -27,6 +27,7 @@ "JSONAny", "URLEncodedForm", "MultipartForm", + "ContentDisposition", "dump_json", "load_json", "JSONObjectBuilder", @@ -36,6 +37,7 @@ import json import typing +import aiohttp.client_reqrep import aiohttp.typedefs import multidict @@ -57,6 +59,9 @@ MultipartForm = aiohttp.FormData """Type hint for content of type multipart/form-data.""" +ContentDisposition = aiohttp.client_reqrep.ContentDisposition +"""Type hint for content disposition information.""" + # MyPy does not support recursive types yet. This has been ongoing for a long time, unfortunately. # See https://github.com/python/typing/issues/182 diff --git a/hikari/utilities/files.py b/hikari/utilities/files.py new file mode 100644 index 0000000000..eb16712708 --- /dev/null +++ b/hikari/utilities/files.py @@ -0,0 +1,352 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# Copyright © Nekoka.tt 2019-2020 +# +# This file is part of Hikari. +# +# Hikari is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Hikari is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Hikari. If not, see . +from __future__ import annotations + +__all__ = [] + +import abc +import asyncio +import base64 +import contextlib +import logging +import mimetypes +import os +import pathlib +import time +import typing + +import aiohttp.client +import attr + +from hikari.net import http_client +from hikari.utilities import klass + +if typing.TYPE_CHECKING: + import concurrent.futures + +_LOGGER: typing.Final[logging.Logger] = klass.get_logger(__name__) +_MAGIC: typing.Final[int] = 50 * 1024 + + +def guess_mimetype_from_filename(name: str) -> typing.Optional[str]: + """Guess the mimetype of an object given a filename. + + Returns + ------- + str or None + The closest guess to the given filename. May be `None` if + no match was found. + """ + return mimetypes.guess_type(name) + + +def guess_mimetype_from_data(data: bytes) -> typing.Optional[str]: + if data.startswith(b"\211PNG\r\n\032\n"): + return "image/png" + elif data[6:].startswith((b"Exif", b"JFIF")): + return "image/jpeg" + elif data.startswith((b"GIF87a", b"GIF89a")): + return "image/gif" + elif data.startswith(b"RIFF") and data[8:].startswith(b"WEBP"): + return "image/webp" + else: + return None + + +def guess_file_extension(mimetype: str) -> typing.Optional[str]: + return mimetypes.guess_extension(mimetype) + + +def generate_filename_from_details( + *, + mimetype: typing.Optional[str] = None, + extension: typing.Optional[str] = None, + data: typing.Optional[bytes] = None, +) -> str: + if data is not None and mimetype is None: + mimetype = guess_mimetype_from_data(data) + + if extension is None and mimetype is not None: + extension = guess_file_extension(mimetype) + + if extension is None or extension == "": + extension = "" + elif not extension.startswith("."): + extension = f".{extension}" + + return str(time.perf_counter_ns()) + extension + + +def to_data_uri(data: bytes, mimetype: typing.Optional[str]) -> str: + if mimetype is None: + mimetype = guess_mimetype_from_data(data) + + if mimetype is None: + raise TypeError("Cannot infer mimetype from input data, specify it manually.") + + b64 = base64.b64encode(data).decode() + return f"data:{mimetype};base64,{b64}" + + +@attr.s(auto_attribs=True, slots=True) +class AsyncReader(typing.AsyncIterable[bytes], abc.ABC): + filename: str + mimetype: typing.Optional[str] + + async def data_uri(self) -> str: + return to_data_uri(await self.read(), self.mimetype) + + async def read(self) -> bytes: + buff = bytearray() + async for chunk in self: + buff.extend(chunk) + return buff + + +@attr.s(auto_attribs=True, slots=True) +class ByteReader(AsyncReader): + data: bytes + + def __aiter__(self) -> typing.AsyncGenerator[typing.Any, bytes]: + for i in range(0, len(self.data), _MAGIC): + yield self.data[i : i + _MAGIC] + + +@attr.s(auto_attribs=True, slots=True) +class FileReader(AsyncReader): + executor: typing.Optional[concurrent.futures.Executor] + path: typing.Union[str, os.PathLike] + loop: asyncio.AbstractEventLoop = attr.ib(factory=asyncio.get_running_loop) + + async def __aiter__(self) -> typing.AsyncGenerator[typing.Any, bytes]: + fp = await self.loop.run_in_executor(self.executor, self._open, self.path) + try: + while True: + chunk = await self.loop.run_in_executor(self.executor, self._read_chunk, fp, _MAGIC) + yield chunk + if len(chunk) < _MAGIC: + break + finally: + await self.loop.run_in_executor(self.executor, self._close, fp) + + @staticmethod + def _read_chunk(fp: typing.IO[bytes], n: int = 10_000) -> bytes: + return fp.read(n) + + @staticmethod + def _open(path: typing.Union[str, os.PathLike]) -> typing.IO[bytes]: + return open(path, "rb") + + @staticmethod + def _close(fp: typing.IO[bytes]) -> None: + fp.close() + + +@attr.s(auto_attribs=True, slots=True) +class WebReader(AsyncReader): + stream: aiohttp.StreamReader + uri: str + status: int + reason: str + charset: typing.Optional[str] + size: typing.Optional[int] + + async def read(self) -> bytes: + return await self.stream.read() + + async def __aiter__(self) -> typing.AsyncGenerator[typing.Any, bytes]: + while not self.stream.at_eof(): + chunk = await self.stream.readchunk() + yield chunk[0] + + +@attr.s(auto_attribs=True) +class Resource(abc.ABC): + @property + @abc.abstractmethod + def url(self) -> str: + """The URL, if known.""" + + @property + @abc.abstractmethod + def filename(self) -> typing.Optional[str]: + """The filename, if known.""" + + @abc.abstractmethod + @contextlib.asynccontextmanager + async def stream(self) -> AsyncReader: + """Return an async iterable of bytes to stream.""" + + +class RawBytes(Resource): + def __init__( + self, + data: bytes, + /, + mimetype: typing.Optional[str] = None, + filename: typing.Optional[str] = None, + extension: typing.Optional[str] = None, + ) -> None: + self.data = data + + if filename is None: + filename = generate_filename_from_details(mimetype=mimetype, extension=extension, data=data) + elif mimetype is None: + mimetype = guess_mimetype_from_filename(filename) + + if extension is None and mimetype is not None: + extension = guess_file_extension(mimetype) + + if filename is None and mimetype is None: + if extension is None: + raise TypeError("Cannot infer data type details, please specify one of filetype, filename, extension") + else: + raise TypeError("Cannot infer data type details from extension. Please specify mimetype or filename") + + self._filename = filename + self.mimetype: str = mimetype + self.extension: typing.Optional[str] = extension + + @property + def url(self) -> str: + return to_data_uri(self.data, self.mimetype) + + @property + def filename(self) -> typing.Optional[str]: + return self._filename + + @contextlib.asynccontextmanager + async def stream(self) -> AsyncReader: + yield ByteReader(self.filename, self.mimetype, self.data) + + +class WebResource(Resource, abc.ABC): + __slots__ = () + + @contextlib.asynccontextmanager + async def stream(self) -> WebReader: + """Start streaming the content into memory by downloading it. + + You can use this to fetch the entire resource, parts of the resource, + or just to view any metadata that may be provided. + + Examples + -------- + Downloading an entire resource at once into memory: + ```py + async with obj.stream() as stream: + data = await stream.read() + ``` + Checking the metadata: + ```py + async with obj.stream() as stream: + mimetype = stream.mimetype + + if mimetype is None: + ... + elif mimetype not in whitelisted_mimetypes: + ... + else: + ... + ``` + Fetching the data-uri of a resource: + ```py + async with obj.stream() as stream: + data_uri = await stream.data_uri() + ``` + + Returns + ------- + WebReader + The download stream. + + Raises + ------ + hikari.errors.BadRequest + If a 400 is returned. + hikari.errors.Unauthorized + If a 401 is returned. + hikari.errors.Forbidden + If a 403 is returned. + hikari.errors.NotFound + If a 404 is returned. + hikari.errors.ClientHTTPErrorResponse + If any other 4xx is returned. + hikari.errors.ServerHTTPErrorResponse + If any other 5xx is returned. + hikari.errors.HTTPErrorResponse + If any other unexpected response code is returned. + """ + + async with aiohttp.ClientSession() as session: + async with session.request("get", self.url, raise_for_status=False) as resp: + if 200 <= resp.status < 400: + mimetype = None + filename = self.filename + + if resp.content_disposition is not None: + mimetype = resp.content_disposition.type + + if mimetype is None: + mimetype = resp.content_type + + if filename is None: + if resp.content_disposition is not None: + filename = resp.content_disposition.filename + + if filename is None: + filename = generate_filename_from_details(mimetype=mimetype) + + yield WebReader( + stream=resp.content, + uri=str(resp.real_url), + status=resp.status, + reason=resp.reason, + filename=filename, + charset=resp.charset, + mimetype=mimetype, + size=resp.content_length, + ) + else: + await http_client.parse_error_response(resp) + + +class File(Resource): + def __init__( + self, + path: typing.Union[str, os.PathLike], + *, + filename: typing.Optional[str] = None, + executor: typing.Optional[concurrent.futures.Executor] = None, + ) -> None: + self.path = path + self._filename = filename if filename is not None else os.path.basename(path) + self.executor = executor + + @property + def url(self) -> str: + return pathlib.PurePath(self.path).as_uri() + + @property + def filename(self) -> typing.Optional[str]: + return self._filename + + @contextlib.asynccontextmanager + async def stream(self) -> AsyncReader: + yield FileReader(self.filename, None, self.executor, self.path) diff --git a/hikari/utilities/klass.py b/hikari/utilities/klass.py index 4b36266f21..70040a2b1a 100644 --- a/hikari/utilities/klass.py +++ b/hikari/utilities/klass.py @@ -27,12 +27,12 @@ import typing -def get_logger(cls: typing.Union[typing.Type[typing.Any], typing.Any], *additional_args: str) -> logging.Logger: +def get_logger(obj: typing.Union[typing.Type[typing.Any], typing.Any], *additional_args: str) -> logging.Logger: """Get an appropriately named logger for the given class or object. Parameters ---------- - cls : typing.Type OR object + obj : typing.Type or object A type or instance of a type to make a logger in the name of. *additional_args : str Additional tokens to append onto the logger name, separated by `.`. @@ -44,8 +44,11 @@ def get_logger(cls: typing.Union[typing.Type[typing.Any], typing.Any], *addition logging.Logger The logger to use. """ - cls = cls if isinstance(cls, type) else type(cls) - return logging.getLogger(".".join((cls.__module__, cls.__qualname__, *additional_args))) + if isinstance(obj, str): + return logging.getLogger(obj) + else: + obj = obj if isinstance(obj, type) else type(obj) + return logging.getLogger(".".join((obj.__module__, obj.__qualname__, *additional_args))) class SingletonMeta(abc.ABCMeta): From 241a215a8adc9504ccb87daf58cb6af5c893124d Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Wed, 10 Jun 2020 18:47:07 +0100 Subject: [PATCH 491/922] Adjusted retry logic to fail if ratelimit lasts longer than 20 seconds. --- hikari/errors.py | 93 ++++++++++++++++++++++---- hikari/models/embeds.py | 19 +++--- hikari/net/http_settings.py | 9 ++- hikari/net/rest.py | 126 +++++++++++++++++++----------------- hikari/utilities/files.py | 30 ++++++++- 5 files changed, 190 insertions(+), 87 deletions(-) diff --git a/hikari/errors.py b/hikari/errors.py index 358c9924c5..5418a27f09 100644 --- a/hikari/errors.py +++ b/hikari/errors.py @@ -39,7 +39,10 @@ import http import typing -import aiohttp.typedefs +from hikari.net import routes + +if typing.TYPE_CHECKING: + from hikari.utilities import data_binding class HikariError(RuntimeError): @@ -172,7 +175,7 @@ class HTTPErrorResponse(HTTPError): The URL that produced the error message. status : int or http.HTTPStatus The HTTP status code of the response that caused this error. - headers : aiohttp.typedefs.LooseHeaders + headers : hikari.utilities.data_binding.Headers Any headers that were given in the response. raw_body : typing.Any The body that was received. @@ -183,7 +186,7 @@ class HTTPErrorResponse(HTTPError): status: typing.Union[int, http.HTTPStatus] """The HTTP status code for the response.""" - headers: aiohttp.typedefs.LooseHeaders + headers: data_binding.Headers """The headers received in the error response.""" raw_body: typing.Any @@ -193,7 +196,7 @@ def __init__( self, url: str, status: typing.Union[int, http.HTTPStatus], - headers: aiohttp.typedefs.LooseHeaders, + headers: data_binding.Headers, raw_body: typing.Any, reason: typing.Optional[str] = None, ) -> None: @@ -236,7 +239,7 @@ class BadRequest(ClientHTTPErrorResponse): ---------- url : str The URL that produced the error message. - headers : aiohttp.typedefs.LooseHeaders + headers : hikari.utilities.data_binding.Headers Any headers that were given in the response. raw_body : typing.Any The body that was received. @@ -244,7 +247,7 @@ class BadRequest(ClientHTTPErrorResponse): __slots__ = () - def __init__(self, url: str, headers: aiohttp.typedefs.LooseHeaders, raw_body: typing.AnyStr) -> None: + def __init__(self, url: str, headers: data_binding.Headers, raw_body: typing.AnyStr) -> None: status = http.HTTPStatus.BAD_REQUEST super().__init__(url, status, headers, raw_body) @@ -258,7 +261,7 @@ class Unauthorized(ClientHTTPErrorResponse): ---------- url : str The URL that produced the error message. - headers : aiohttp.typedefs.LooseHeaders + headers : hikari.utilities.data_binding.Headers Any headers that were given in the response. raw_body : typing.Any The body that was received. @@ -266,7 +269,7 @@ class Unauthorized(ClientHTTPErrorResponse): __slots__ = () - def __init__(self, url: str, headers: aiohttp.typedefs.LooseHeaders, raw_body: typing.AnyStr) -> None: + def __init__(self, url: str, headers: data_binding.Headers, raw_body: typing.AnyStr) -> None: status = http.HTTPStatus.UNAUTHORIZED super().__init__(url, status, headers, raw_body) @@ -282,7 +285,7 @@ class Forbidden(ClientHTTPErrorResponse): ---------- url : str The URL that produced the error message. - headers : aiohttp.typedefs.LooseHeaders + headers : hikari.utilities.data_binding.Headers Any headers that were given in the response. raw_body : typing.Any The body that was received. @@ -290,7 +293,7 @@ class Forbidden(ClientHTTPErrorResponse): __slots__ = () - def __init__(self, url: str, headers: aiohttp.typedefs.LooseHeaders, raw_body: typing.AnyStr) -> None: + def __init__(self, url: str, headers: data_binding.Headers, raw_body: typing.AnyStr) -> None: status = http.HTTPStatus.FORBIDDEN super().__init__(url, status, headers, raw_body) @@ -302,7 +305,7 @@ class NotFound(ClientHTTPErrorResponse): ---------- url : str The URL that produced the error message. - headers : aiohttp.typedefs.LooseHeaders + headers : hikari.utilities.data_binding.Headers Any headers that were given in the response. raw_body : typing.Any The body that was received. @@ -310,11 +313,77 @@ class NotFound(ClientHTTPErrorResponse): __slots__ = () - def __init__(self, url: str, headers: aiohttp.typedefs.LooseHeaders, raw_body: typing.AnyStr) -> None: + def __init__(self, url: str, headers: data_binding.Headers, raw_body: typing.AnyStr) -> None: status = http.HTTPStatus.NOT_FOUND super().__init__(url, status, headers, raw_body) + + +class RateLimited(ClientHTTPErrorResponse): + """Raised when a non-global ratelimit that cannot be handled occurs. + + This should only ever occur for specific routes that have additional + rate-limits applied to them by Discord. At the time of writing, the + PATCH CHANNEL endpoint is the only one that knowingly implements this, and + does so by implementing rate-limits on the usage of specific fields only. + + If you receive one of these, you should NOT try again until the given + time has passed, either discarding the operation you performed, or waiting + until the given time has passed first. Note that it may still be valid to + send requests with different attributes in them. + + A use case for this by Discord appears to be to stop abuse from bots that + change channel names, etc, regularly. This kind of action allegedly causes + a fair amount of overhead internally for Discord. In the case you encounter + this, you may be able to send different requests that manipulate the same + entities (in this case editing the same channel) that do not use the same + collection of attributes as the previous request. + + You should not usually see this occur, unless Discord vastly change their + ratelimit system without prior warning, which might happen in the future. + + If you receive this regularly, please file a bug report, or contact + Discord with the relevant debug information that can be obtained by + enabling debug logs and enabling the debug mode on the REST components. + Parameters + ---------- + url : str + The URL that produced the error message. + route : hikari.net.routes.CompiledRoute + The route that produced this error. + headers : hikari.utilities.data_binding.Headers + Any headers that were given in the response. + raw_body : typing.Any + The body that was received. + retry_after : float + How many seconds to wait before you can reuse the route with the + specific request. + """ + + __slots__ = () + def __init__( + self, + url: str, + route: routes.CompiledRoute, + headers: data_binding.Headers, + raw_body: typing.AnyStr, + retry_after: float, + ) -> None: + self.retry_after = retry_after + self.route = route + + status = http.HTTPStatus.TOO_MANY_REQUESTS + super().__init__( + url, + status, + headers, + raw_body, + f"You are being rate-limited for {self.retry_after:,} seconds on route {route}. Please slow down!" + ) + + + class ServerHTTPErrorResponse(HTTPErrorResponse): """Base exception for an erroneous HTTP response that is a server error. diff --git a/hikari/models/embeds.py b/hikari/models/embeds.py index 3e1bece2d3..5558f326e3 100644 --- a/hikari/models/embeds.py +++ b/hikari/models/embeds.py @@ -49,6 +49,7 @@ _MAX_EMBED_SIZE: typing.Final[int] = 6000 + @attr.s(eq=True, hash=False, init=True, kw_only=True, slots=True) class EmbedFooter: """Represents an embed footer.""" @@ -56,10 +57,10 @@ class EmbedFooter: text: typing.Optional[str] = attr.ib(default=None, repr=True) """The footer text, or `None` if not present.""" - icon: typing.Optional[str] = attr.ib(default=None, repr=False) + icon: typing.Optional[files.Resource] = attr.ib(default=None, repr=False, converter=files.ensure_resource) """The URL of the footer icon, or `None` if not present.""" - proxy_icon_url: typing.Optional[str] = attr.ib(default=None, repr=False) + proxy_icon: typing.Optional[files.Resource] = attr.ib(default=None, repr=False) """The proxied URL of the footer icon, or `None` if not present. !!! note @@ -73,10 +74,10 @@ class EmbedFooter: class EmbedImage: """Represents an embed image.""" - url: typing.Optional[str] = attr.ib(default=None, repr=True) + image: typing.Optional[files.Resource] = attr.ib(default=None, repr=True, converter=files.ensure_resource) """The image to show, or `None` if not present.""" - proxy_image: typing.Optional[str] = attr.ib(default=None, repr=False) + proxy_image: typing.Optional[files.Resource] = attr.ib(default=None, repr=False) """The proxy image, or `None` if not present. !!! note @@ -108,10 +109,10 @@ class EmbedImage: class EmbedThumbnail: """Represents an embed thumbnail.""" - url: typing.Optional[str] = attr.ib(default=None, repr=True) + image: typing.Optional[files.Resource] = attr.ib(default=None, repr=True) """The URL of the thumbnail to display, or `None` if not present.""" - proxy_url: typing.Optional[str] = attr.ib(default=None, repr=False) + proxy_image: typing.Optional[files.Resource] = attr.ib(default=None, repr=False) """The proxied URL of the thumbnail, if present and known, otherwise `None`. !!! note @@ -149,7 +150,7 @@ class EmbedVideo: any received embed attached to a message event with a video attached. """ - url: typing.Optional[str] = attr.ib(default=None, repr=True) + video: typing.Optional[files.Resource] = attr.ib(default=None, repr=True) """The URL of the video.""" height: typing.Optional[int] = attr.ib(default=None, repr=False) @@ -190,10 +191,10 @@ class EmbedAuthor: This may be `None` if no hyperlink on the author's name is specified. """ - icon: typing.Optional[str] = attr.ib(default=None, repr=False) + icon: typing.Optional[files.Resource] = attr.ib(default=None, repr=False, converter=files.ensure_resource) """The URL of the author's icon, or `None` if not present.""" - proxy_icon: typing.Optional[str] = attr.ib(default=None, repr=False) + proxy_icon: typing.Optional[files.Resource] = attr.ib(default=None, repr=False) """The proxied URL of the author icon, or `None` if not present. !!! note diff --git a/hikari/net/http_settings.py b/hikari/net/http_settings.py index d0faf17992..93f279b589 100644 --- a/hikari/net/http_settings.py +++ b/hikari/net/http_settings.py @@ -50,9 +50,12 @@ class HTTPSettings: proxy_url: typing.Optional[str] = None """The optional URL of the proxy to send requests via.""" - request_timeout: typing.Optional[float] = None - """Optional request _request_timeout to use. If an HTTP request takes longer than - this, it will be aborted. + request_timeout: typing.Optional[float] = 10.0 + """Optional request timeout to use. + + If an HTTP request takes longer than this, it will be aborted. + + Defaults to 10 seconds. If not `None`, the value represents a number of seconds as a floating point number. diff --git a/hikari/net/rest.py b/hikari/net/rest.py index 5823cd0e41..3cb4fe3427 100644 --- a/hikari/net/rest.py +++ b/hikari/net/rest.py @@ -111,7 +111,7 @@ class REST(http_client.HTTPClient, component.IComponent): # pylint:disable=too- The API version to use. """ - class _RateLimited(RuntimeError): + class _RetryRequest(RuntimeError): __slots__ = () def __init__( @@ -211,7 +211,7 @@ async def _request( try: # Moved to a separate method to keep branch counts down. return await self._request_once(compiled_route=compiled_route, headers=headers, body=body, query=query) - except self._RateLimited: + except self._RetryRequest: pass async def _request_once( @@ -233,7 +233,7 @@ async def _request_once( ) # Ensure we aren't rate limited, and update rate limiting headers where appropriate. - await self._handle_rate_limits_for_response(compiled_route, response) + await self._parse_ratelimits(compiled_route, response) # Don't bother processing any further if we got NO CONTENT. There's not anything # to check. @@ -255,9 +255,7 @@ async def _request_once( async def _handle_error_response(response) -> typing.NoReturn: return await http_client.parse_error_response(response) - async def _handle_rate_limits_for_response( - self, compiled_route: routes.CompiledRoute, response: aiohttp.ClientResponse - ) -> None: + async def _parse_ratelimits(self, compiled_route: routes.CompiledRoute, response: aiohttp.ClientResponse) -> None: # Worth noting there is some bug on V6 that rate limits me immediately if I have an invalid token. # https://github.com/discord/discord-api-docs/issues/1569 @@ -272,58 +270,6 @@ async def _handle_rate_limits_for_response( is_rate_limited = response.status == http.HTTPStatus.TOO_MANY_REQUESTS - if is_rate_limited: - if response.content_type != self._APPLICATION_JSON: - # We don't know exactly what this could imply. It is likely Cloudflare interfering - # but I'd rather we just give up than do something resulting in multiple failed - # requests repeatedly. - raise errors.HTTPErrorResponse( - str(response.real_url), - http.HTTPStatus.TOO_MANY_REQUESTS, - response.headers, - await response.read(), - f"received rate limited response with unexpected response type {response.content_type}", - ) - - body = await response.json() - - body_retry_after = float(body["retry_after"]) / 1_000 - - if body.get("global", False): - self.global_rate_limit.throttle(body_retry_after) - - self.logger.warning("you are being rate-limited globally - trying again after %ss", body_retry_after) - else: - # Discord can do a messed up thing where the headers suggest we aren't rate limited, - # but we still get 429s with a different rate limit. - # If this occurs, we need to take the rate limit that is furthest in the future - # to avoid excessive 429ing everywhere repeatedly, causing an API ban, - # since our logic assumes the rate limit info they give us is actually - # remotely correct. - # - # At the time of writing, editing a channel more than twice per 10 minutes seems - # to trigger this, which makes me nervous that the info we are receiving isn't - # correct, but whatever... this is the best we can do. - - header_reset_at = reset_at - body_retry_at = now_date.timestamp() + body_retry_after - - if body_retry_at > header_reset_at: - reset_date = datetime.datetime.fromtimestamp(body_retry_at, tz=datetime.timezone.utc) - - self.logger.warning( - "you are being rate-limited on bucket %s for route %s - trying again after %ss " - "(headers suggest %ss back-off finishing at %s; rate-limited response specifies %ss " - "back-off finishing at %s)", - bucket, - compiled_route, - reset_at, - header_reset_at - now_date.timestamp(), - header_reset_at, - body_retry_after, - body_retry_at, - ) - self.buckets.update_rate_limits( compiled_route=compiled_route, bucket_header=bucket, @@ -333,8 +279,66 @@ async def _handle_rate_limits_for_response( reset_at_header=reset_date, ) - if is_rate_limited: - raise self._RateLimited() + if not is_rate_limited: + return + + if response.content_type != self._APPLICATION_JSON: + # We don't know exactly what this could imply. It is likely Cloudflare interfering + # but I'd rather we just give up than do something resulting in multiple failed + # requests repeatedly. + raise errors.HTTPErrorResponse( + str(response.real_url), + http.HTTPStatus.TOO_MANY_REQUESTS, + response.headers, + await response.read(), + f"received rate limited response with unexpected response type {response.content_type}", + ) + + body = await response.json() + body_retry_after = float(body["retry_after"]) / 1_000 + + if body.get("global", False) is True: + self.global_rate_limit.throttle(body_retry_after) + + self.logger.warning("you are being rate-limited globally - trying again after %ss", body_retry_after) + return + + + # Discord have started applying ratelimits to operations on some endpoints + # based on specific fields used in the JSON body. + # This does not get reflected in the headers. The first we know is when we + # get a 429. + # The issue is that we may get the same response if Discord dynamically + # adjusts the bucket ratelimits. + # + # We have no mechanism for handing field-based ratelimits, so if we get + # to here, but notice remaining is greater than zero, we should just error. + # + # Worth noting we still ignore the retry_after in the body. I have no clue + # if there is some weird edge case where a bucket rate limit can occur on + # top of a non-global one, but in this case this check will misbehave and + # instead of erroring, will trigger a backoff that might be 10 minutes or + # more... + + # I realise remaining should never be less than zero, but quite frankly, I don't + # trust that voodoo type stuff won't ever occur with that value from them... + if remaining <= 0: + # We can retry and we will then abide by the updated bucket ratelimits. + self.logger.debug( + "ratelimited on bucket %s at %s. This is a bucket discrepancy, so we will retry at %s", + bucket, + compiled_route, + reset_date, + ) + raise self._RetryRequest() + + raise errors.RateLimited( + str(response.real_url), + compiled_route, + response.headers, + body, + body_retry_after, + ) @staticmethod def _generate_allowed_mentions( @@ -1100,7 +1104,7 @@ async def create_message( attachments = [] if isinstance(attachments, undefined.Undefined) else [a for a in attachments] - # TODO: embed attachments... + # TODO: embed handle images. if attachments: form = data_binding.URLEncodedForm() diff --git a/hikari/utilities/files.py b/hikari/utilities/files.py index eb16712708..f29a764910 100644 --- a/hikari/utilities/files.py +++ b/hikari/utilities/files.py @@ -30,6 +30,7 @@ import pathlib import time import typing +import urllib.parse import aiohttp.client import attr @@ -42,6 +43,17 @@ _LOGGER: typing.Final[logging.Logger] = klass.get_logger(__name__) _MAGIC: typing.Final[int] = 50 * 1024 +_FILE: typing.Final[str] = "file://" + + +def ensure_resource(url_or_resource: typing.Union[str, Resource]) -> Resource: + """Given a resource or string, convert it to a valid resource as needed.""" + if isinstance(url_or_resource, Resource): + return url_or_resource + else: + if url_or_resource.startswith(_FILE): + return File(url_or_resource[len(_FILE):]) + return URL(url_or_resource) def guess_mimetype_from_filename(name: str) -> typing.Optional[str]: @@ -237,8 +249,6 @@ async def stream(self) -> AsyncReader: class WebResource(Resource, abc.ABC): - __slots__ = () - @contextlib.asynccontextmanager async def stream(self) -> WebReader: """Start streaming the content into memory by downloading it. @@ -327,6 +337,22 @@ async def stream(self) -> WebReader: await http_client.parse_error_response(resp) +@attr.s(auto_attribs=True) +class URL(WebResource): + """A URL that represents a web resource.""" + + _url: str + + @property + def url(self) -> str: + return self._url + + @property + def filename(self) -> str: + url = urllib.parse.urlparse(self._url) + return os.path.basename(url.path) + + class File(Resource): def __init__( self, From c34c667665af41a1a3b6f29c60f2fabc3b227166 Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Thu, 11 Jun 2020 10:22:05 +0100 Subject: [PATCH 492/922] Moved constants to a file for HTTP. --- hikari/errors.py | 29 +++++----- hikari/models/embeds.py | 1 - hikari/net/gateway.py | 8 ++- hikari/net/rest.py | 58 ++++++++------------ hikari/net/strings.py | 73 +++++++++++++++++++++++++ hikari/net/tracing.py | 33 ++++++++---- hikari/net/user_agents.py | 111 -------------------------------------- hikari/utilities/files.py | 2 +- hikari/utilities/klass.py | 11 ++-- 9 files changed, 148 insertions(+), 178 deletions(-) create mode 100644 hikari/net/strings.py delete mode 100644 hikari/net/user_agents.py diff --git a/hikari/errors.py b/hikari/errors.py index 5418a27f09..d38ea6723f 100644 --- a/hikari/errors.py +++ b/hikari/errors.py @@ -316,8 +316,8 @@ class NotFound(ClientHTTPErrorResponse): def __init__(self, url: str, headers: data_binding.Headers, raw_body: typing.AnyStr) -> None: status = http.HTTPStatus.NOT_FOUND super().__init__(url, status, headers, raw_body) - - + + class RateLimited(ClientHTTPErrorResponse): """Raised when a non-global ratelimit that cannot be handled occurs. @@ -363,27 +363,26 @@ class RateLimited(ClientHTTPErrorResponse): __slots__ = () def __init__( - self, - url: str, + self, + url: str, route: routes.CompiledRoute, - headers: data_binding.Headers, - raw_body: typing.AnyStr, + headers: data_binding.Headers, + raw_body: typing.AnyStr, retry_after: float, ) -> None: self.retry_after = retry_after self.route = route - + status = http.HTTPStatus.TOO_MANY_REQUESTS super().__init__( - url, - status, - headers, - raw_body, - f"You are being rate-limited for {self.retry_after:,} seconds on route {route}. Please slow down!" + url, + status, + headers, + raw_body, + f"You are being rate-limited for {self.retry_after:,} seconds on route {route}. Please slow down!", ) - - - + + class ServerHTTPErrorResponse(HTTPErrorResponse): """Base exception for an erroneous HTTP response that is a server error. diff --git a/hikari/models/embeds.py b/hikari/models/embeds.py index 5558f326e3..ccbd6cf0ed 100644 --- a/hikari/models/embeds.py +++ b/hikari/models/embeds.py @@ -49,7 +49,6 @@ _MAX_EMBED_SIZE: typing.Final[int] = 6000 - @attr.s(eq=True, hash=False, init=True, kw_only=True, slots=True) class EmbedFooter: """Represents an embed footer.""" diff --git a/hikari/net/gateway.py b/hikari/net/gateway.py index 7d4efb9dcd..27b6ce5f9f 100644 --- a/hikari/net/gateway.py +++ b/hikari/net/gateway.py @@ -39,7 +39,7 @@ from hikari.models import presences from hikari.net import http_client from hikari.net import rate_limits -from hikari.net import user_agents +from hikari.net import strings from hikari.utilities import data_binding from hikari.utilities import klass from hikari.utilities import undefined @@ -474,7 +474,11 @@ async def _handshake(self) -> None: "token": self._token, "compress": False, "large_threshold": self.large_threshold, - "properties": user_agents.UserAgent().websocket_triplet, + "properties": { + "$os": strings.SYSTEM_TYPE, + "$browser": strings.AIOHTTP_VERSION, + "$device": strings.LIBRARY_VERSION, + }, "shard": [self._shard_id, self._shard_count], }, } diff --git a/hikari/net/rest.py b/hikari/net/rest.py index 3cb4fe3427..8f81c2eb84 100644 --- a/hikari/net/rest.py +++ b/hikari/net/rest.py @@ -39,6 +39,8 @@ from hikari.net import rate_limits from hikari.net import rest_utils from hikari.net import routes +from hikari.net import strings +from hikari.models import emojis from hikari.utilities import data_binding from hikari.utilities import date from hikari.utilities import files @@ -55,7 +57,6 @@ from hikari.models import channels from hikari.models import colors from hikari.models import embeds as embeds_ - from hikari.models import emojis from hikari.models import gateway from hikari.models import guilds from hikari.models import invites @@ -66,15 +67,6 @@ from hikari.models import webhooks -_REST_API_URL: typing.Final[str] = "https://discord.com/api/v{0.version}" -"""The URL for the RESTSession API. This contains a version number parameter that -should be interpolated. -""" - -_OAUTH2_API_URL: typing.Final[str] = "https://discord.com/api/oauth2" -"""The URL to the Discord OAuth2 API.""" - - class REST(http_client.HTTPClient, component.IComponent): # pylint:disable=too-many-public-methods """Implementation of the V6 and V7-compatible Discord REST API. @@ -151,17 +143,17 @@ def __init__( full_token = None else: if isinstance(token_type, undefined.Undefined): - token_type = "Bot" + token_type = strings.BOT_TOKEN full_token = f"{token_type.title()} {token}" self._token: typing.Optional[str] = full_token if isinstance(rest_url, undefined.Undefined): - rest_url = _REST_API_URL + rest_url = strings.REST_API_URL if isinstance(oauth2_url, undefined.Undefined): - oauth2_url = _OAUTH2_API_URL + oauth2_url = strings.OAUTH2_API_URL self._rest_url = rest_url.format(self) self._oauth2_url = oauth2_url.format(self) @@ -192,17 +184,18 @@ async def _request( self.buckets.start() headers = data_binding.StringMapBuilder() + headers.setdefault(strings.USER_AGENT_HEADER, strings.HTTP_USER_AGENT) - headers.put("x-ratelimit-precision", "millisecond") - headers.put("accept", self._APPLICATION_JSON) + headers.put(strings.X_RATELIMIT_PRECISION_HEADER, "millisecond") + headers.put(strings.ACCEPT_HEADER, strings.APPLICATION_JSON) if self._token is not None and not no_auth: - headers["authorization"] = self._token + headers[strings.AUTHORIZATION_HEADER] = self._token if isinstance(body, undefined.Undefined): body = None - headers.put("x-audit-log-reason", reason) + headers.put(strings.X_AUDIT_LOG_REASON_HEADER, reason) if isinstance(query, undefined.Undefined): query = None @@ -261,12 +254,12 @@ async def _parse_ratelimits(self, compiled_route: routes.CompiledRoute, response # Handle rate limiting. resp_headers = response.headers - limit = int(resp_headers.get("x-ratelimit-limit", "1")) - remaining = int(resp_headers.get("x-ratelimit-remaining", "1")) - bucket = resp_headers.get("x-ratelimit-bucket", "None") - reset_at = float(resp_headers.get("x-ratelimit-reset", "0")) + limit = int(resp_headers.get(strings.X_RATELIMIT_LIMIT_HEADER, "1")) + remaining = int(resp_headers.get(strings.X_RATELIMIT_REMAINING_HEADER, "1")) + bucket = resp_headers.get(strings.X_RATELIMIT_BUCKET_HEADER, "None") + reset_at = float(resp_headers.get(strings.X_RATELIMIT_RESET_HEADER, "0")) reset_date = datetime.datetime.fromtimestamp(reset_at, tz=datetime.timezone.utc) - now_date = date.rfc7231_datetime_string_to_datetime(resp_headers["date"]) + now_date = date.rfc7231_datetime_string_to_datetime(resp_headers[strings.DATE_HEADER]) is_rate_limited = response.status == http.HTTPStatus.TOO_MANY_REQUESTS @@ -303,7 +296,6 @@ async def _parse_ratelimits(self, compiled_route: routes.CompiledRoute, response self.logger.warning("you are being rate-limited globally - trying again after %ss", body_retry_after) return - # Discord have started applying ratelimits to operations on some endpoints # based on specific fields used in the JSON body. # This does not get reflected in the headers. The first we know is when we @@ -325,7 +317,7 @@ async def _parse_ratelimits(self, compiled_route: routes.CompiledRoute, response if remaining <= 0: # We can retry and we will then abide by the updated bucket ratelimits. self.logger.debug( - "ratelimited on bucket %s at %s. This is a bucket discrepancy, so we will retry at %s", + "rate-limited on bucket %s at %s. This is a bucket discrepancy, so we will retry at %s", bucket, compiled_route, reset_date, @@ -333,11 +325,7 @@ async def _parse_ratelimits(self, compiled_route: routes.CompiledRoute, response raise self._RetryRequest() raise errors.RateLimited( - str(response.real_url), - compiled_route, - response.headers, - body, - body_retry_after, + str(response.real_url), compiled_route, response.headers, body, body_retry_after, ) @staticmethod @@ -1694,10 +1682,10 @@ async def fetch_emoji( self, guild: typing.Union[guilds.Guild, bases.UniqueObject], # This is an emoji ID, which is the URL-safe emoji name, not the snowflake alone. - emoji: typing.Union[emojis.KnownCustomEmoji, str], + emoji: typing.Union[emojis.CustomEmoji, bases.UniqueObject], ) -> emojis.KnownCustomEmoji: route = routes.GET_GUILD_EMOJI.compile( - guild=guild, emoji=emoji.url_name if isinstance(emoji, emojis.Emoji) else emoji, + guild=guild, emoji=emoji.id if isinstance(emoji, emojis.CustomEmoji) else emoji, ) raw_response = await self._request(route) response = typing.cast(data_binding.JSONObject, raw_response) @@ -1739,7 +1727,7 @@ async def edit_emoji( self, guild: typing.Union[guilds.Guild, bases.UniqueObject], # This is an emoji ID, which is the URL-safe emoji name, not the snowflake alone. - emoji: typing.Union[emojis.KnownCustomEmoji, str], + emoji: typing.Union[emojis.CustomEmoji, bases.UniqueObject], *, name: typing.Union[undefined.Undefined, str] = undefined.Undefined(), roles: typing.Union[ @@ -1748,7 +1736,7 @@ async def edit_emoji( reason: typing.Union[undefined.Undefined, str] = undefined.Undefined(), ) -> emojis.KnownCustomEmoji: route = routes.PATCH_GUILD_EMOJI.compile( - guild=guild, emoji=emoji.url_name if isinstance(emoji, emojis.Emoji) else emoji, + guild=guild, emoji=emoji.id if isinstance(emoji, emojis.CustomEmoji) else emoji, ) body = data_binding.JSONObjectBuilder() body.put("name", name) @@ -1762,11 +1750,11 @@ async def delete_emoji( self, guild: typing.Union[guilds.Guild, bases.UniqueObject], # This is an emoji ID, which is the URL-safe emoji name, not the snowflake alone. - emoji: typing.Union[emojis.KnownCustomEmoji, str], + emoji: typing.Union[emojis.CustomEmoji, bases.UniqueObject], # Reason is not currently supported for some reason. See ) -> None: route = routes.DELETE_GUILD_EMOJI.compile( - guild=guild, emoji=emoji.url_name if isinstance(emoji, emojis.Emoji) else emoji, + guild=guild, emoji=emoji.id if isinstance(emoji, emojis.CustomEmoji) else emoji, ) await self._request(route) diff --git a/hikari/net/strings.py b/hikari/net/strings.py new file mode 100644 index 0000000000..33634aa323 --- /dev/null +++ b/hikari/net/strings.py @@ -0,0 +1,73 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# Copyright © Nekoka.tt 2019-2020 +# +# This file is part of Hikari. +# +# Hikari is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Hikari is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Hikari. If not, see . +from __future__ import annotations + +import platform +import typing + +import aiohttp + +import hikari + + +# Headers. +ACCEPT_HEADER: typing.Final[str] = "Accept" +AUTHORIZATION_HEADER: typing.Final[str] = "Authorization" +CF_RAY_HEADER: typing.Final[str] = "CF-Ray" +CF_REQUEST_ID_HEADER: typing.Final[str] = "CF-Request-ID" +CONTENT_LENGTH_HEADER: typing.Final[str] = "Content-Length" +CONTENT_TYPE_HEADER: typing.Final[str] = "Content-Type" +DATE_HEADER: typing.Final[str] = "Date" +USER_AGENT_HEADER: typing.Final[str] = "User-Agent" +X_AUDIT_LOG_REASON_HEADER: typing.Final[str] = "X-Audit-Log-Reason" +X_RATELIMIT_BUCKET_HEADER: typing.Final[str] = "X-RateLimit-Bucket" +X_RATELIMIT_LIMIT_HEADER: typing.Final[str] = "X-RateLimit-Limit" +X_RATELIMIT_PRECISION_HEADER: typing.Final[str] = "X-RateLimit-Precision" +X_RATELIMIT_REMAINING_HEADER: typing.Final[str] = "X-RateLimit-Remaining" +X_RATELIMIT_RESET_HEADER: typing.Final[str] = "X-RateLimit-Reset" + +# Mimetypes. +APPLICATION_JSON: typing.Final[str] = "application/json" +APPLICATION_XML: typing.Final[str] = "application/xml" +APPLICATION_OCTET_STREAM: typing.Final[str] = "application/octet-stream" + +# Bits of text. +BEARER_TOKEN: typing.Final[str] = "Bearer" +BOT_TOKEN: typing.Final[str] = "Bot" +MILLISECOND_PRECISION: typing.Final[str] = "millisecond" + +# User-agent info. +AIOHTTP_VERSION: typing.Final[str] = f"aiohttp {aiohttp.__version__}" +LIBRARY_VERSION: typing.Final[str] = f"hikari {hikari.__version__}" +SYSTEM_TYPE: typing.Final[str] = (f"{platform.system()} {platform.architecture()[0]}") +HTTP_USER_AGENT: typing.Final[str] = ( + f"DiscordBot ({hikari.__url__}, {hikari.__version__}) {hikari.__author__} " + f"Aiohttp/{aiohttp.__version__} " + f"{platform.python_implementation()}/{platform.python_version()} {SYSTEM_TYPE}" +) +PYTHON_PLATFORM_VERSION: typing.Final[str] = ( + f"{platform.python_implementation()} {platform.python_version()} " + f"{platform.python_branch()} {platform.python_compiler()}" +).replace(" ", " ") + +# URLs +REST_API_URL: typing.Final[str] = "https://discord.com/api/v{0.version}" +OAUTH2_API_URL: typing.Final[str] = f"{REST_API_URL}/oauth2" + +__all__: typing.List[str] = [attr for attr in globals() if all(c == "_" or c.isupper() for c in attr)] diff --git a/hikari/net/tracing.py b/hikari/net/tracing.py index 918c7d8477..62426625a3 100644 --- a/hikari/net/tracing.py +++ b/hikari/net/tracing.py @@ -32,6 +32,8 @@ import aiohttp.abc +from hikari.net import strings + class BaseTracer: """Base type for tracing HTTP requests.""" @@ -65,11 +67,13 @@ async def on_request_start(self, _, ctx, params): ctx.start_time = time.perf_counter() self.logger.debug( - "%s %s [content-type:%s, accept:%s] [%s]", + "%s %s [%s:%s, %s:%s] [%s]", params.method, params.url, - params.headers.get("content-type"), - params.headers.get("accept"), + strings.CONTENT_TYPE_HEADER, + params.headers.get(strings.CONTENT_TYPE_HEADER), + strings.ACCEPT_HEADER, + params.headers.get(strings.ACCEPT_HEADER), ctx.identifier, ) @@ -79,14 +83,18 @@ async def on_request_end(self, _, ctx, params): latency = round((time.perf_counter() - ctx.start_time) * 1_000, 1) response = params.response self.logger.debug( - "%s %s after %sms [content-type:%s, size:%s, cf-ray:%s, cf-request-id:%s] [%s]", + "%s %s after %sms [%s:%s, %s:%s, %s:%s, %s:%s] [%s]", response.status, response.reason, latency, - response.headers.get("content-type"), - response.headers.get("content-length", 0), - response.headers.get("cf-ray"), - response.headers.get("cf-request-id"), + strings.CONTENT_TYPE_HEADER, + response.headers.get(strings.CONTENT_TYPE_HEADER), + strings.CONTENT_LENGTH_HEADER, + response.headers.get(strings.CONTENT_LENGTH_HEADER, 0), + strings.CF_RAY_HEADER, + response.headers.get(strings.CF_RAY_HEADER), + strings.CF_REQUEST_ID_HEADER, + response.headers.get(strings.CF_REQUEST_ID_HEADER), ctx.identifier, ) @@ -161,6 +169,12 @@ async def on_request_end(self, _, ctx, params): """Log an inbound response.""" latency = round((time.perf_counter() - ctx.start_time) * 1_000, 2) response = params.response + + if strings.CONTENT_TYPE_HEADER in response.headers: + body = await self._format_body(await response.read()) + else: + body = "no-content" + self.logger.debug( "%s %s %s after %sms [%s]\n response headers: %s\n response body: %s", response.real_url, @@ -169,7 +183,6 @@ async def on_request_end(self, _, ctx, params): latency, ctx.identifier, dict(response.headers), - await self._format_body(await response.read()) if "content-type" in response.headers else "", ) @typing.no_type_check @@ -200,7 +213,7 @@ async def on_dns_cache_hit(self, _, ctx, params): @typing.no_type_check async def on_dns_cache_miss(self, _, ctx, params): """Log when we have to query a DNS server for an IP address.""" - self.logger.debug("will perform DNS lookup of new host %s [%s]", params.host, ctx.identifier) + self.logger.debug("will perform DNS lookup of new host %s [%s]", params.host, ctx.identifier) # noinspection PyMethodMayBeStatic @typing.no_type_check diff --git a/hikari/net/user_agents.py b/hikari/net/user_agents.py deleted file mode 100644 index b1f51869b0..0000000000 --- a/hikari/net/user_agents.py +++ /dev/null @@ -1,111 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# Copyright © Nekokatt 2019-2020 -# -# This file is part of Hikari. -# -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . -"""Anonymous system information that we have to provide to Discord when using their API. - -This information contains details such as the version of Python you are using, and -the version of this library, the OS you are making requests from, etc. - -This information is provided to enable Discord to detect that you are using a -valid bot and not attempting to abuse the API. -""" - -from __future__ import annotations - -__all__: typing.List[str] = ["UserAgent"] - -import aiohttp -import typing - -from hikari.utilities import klass - - -class UserAgent(klass.Singleton): - """Platform version info. - - !!! note - This is a singleton. - """ - - library_version: typing.Final[str] - """The version of the library. - - Examples - -------- - `"hikari 1.0.1"` - """ - - platform_version: typing.Final[str] - """The platform version. - - Examples - -------- - `"CPython 3.8.2 GCC 9.2.0"` - """ - - system_type: typing.Final[str] - """The operating system type. - - Examples - -------- - `"Linux 64bit"` - """ - - user_agent: typing.Final[str] - """The Hikari-specific user-agent to use in HTTP connections to Discord. - - Examples - -------- - `"DiscordBot (https://gitlab.com/nekokatt/hikari; 1.0.1) Nekokatt Aiohttp/3.6.2 CPython/3.8.2 Linux 64bit"` - """ - - def __init__(self) -> None: - from hikari._about import __author__, __url__, __version__ - from platform import python_implementation, python_version, python_branch, python_compiler, architecture, system - - self.library_version = f"hikari {__version__}" - self.platform_version = self._join_strip( - python_implementation(), python_version(), python_branch(), python_compiler() - ) - self.system_type = " ".join((system(), architecture()[0])) - self.user_agent = ( - f"DiscordBot ({__url__}; {__version__}) {__author__} " - f"Aiohttp/{aiohttp.__version__} " - f"{python_implementation()}/{python_version()} " - f"{self.system_type}" - ) - - logger = klass.get_logger(self) - logger.debug("Using User-Agent %r", self.user_agent) - - @staticmethod - def _join_strip(*args: str) -> str: - return " ".join((arg.strip() for arg in args if arg.strip())) - - @property - def websocket_triplet(self) -> typing.Dict[str, str]: - """Generate a dict representing device and library info. - - This is the object to send to Discord representing device info when - IDENTIFYing with the gateway in the format `typing.Dict[str, str]` - """ - return { - "$os": self.system_type, - "$browser": self.library_version, - "$device": self.platform_version, - } diff --git a/hikari/utilities/files.py b/hikari/utilities/files.py index f29a764910..826323180c 100644 --- a/hikari/utilities/files.py +++ b/hikari/utilities/files.py @@ -52,7 +52,7 @@ def ensure_resource(url_or_resource: typing.Union[str, Resource]) -> Resource: return url_or_resource else: if url_or_resource.startswith(_FILE): - return File(url_or_resource[len(_FILE):]) + return File(url_or_resource[len(_FILE) :]) return URL(url_or_resource) diff --git a/hikari/utilities/klass.py b/hikari/utilities/klass.py index 70040a2b1a..6bcd955bb4 100644 --- a/hikari/utilities/klass.py +++ b/hikari/utilities/klass.py @@ -78,9 +78,7 @@ class SingletonMeta(abc.ABCMeta): ___instances___: typing.Dict[typing.Type[typing.Any], typing.Any] = {} - # Disable type-checking to hide a bug in IntelliJ for the time being. - @typing.no_type_check - def __call__(cls) -> typing.Any: + def __call__(cls): if cls not in SingletonMeta.___instances___: SingletonMeta.___instances___[cls] = super().__call__() return SingletonMeta.___instances___[cls] @@ -109,3 +107,10 @@ class Singleton(abc.ABC, metaclass=SingletonMeta): """ __slots__ = () + + +class Static(type): + """Metaclass that prevents instantiation. Enables the use """ + + def __call__(cls) -> typing.NoReturn: + raise TypeError("This class is static-only, and cannot be instantiated.") From e789c620f4bee6dbbbf5ce9630135d6fb4bd4ad1 Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Thu, 11 Jun 2020 10:46:49 +0100 Subject: [PATCH 493/922] Fixed bug where server disconnect on gateway shut down entire bot. --- hikari/impl/event_manager.py | 4 ++-- hikari/impl/gateway_zookeeper.py | 3 ++- hikari/net/gateway.py | 21 ++++++++++++++++----- hikari/net/rest.py | 5 ----- 4 files changed, 20 insertions(+), 13 deletions(-) diff --git a/hikari/impl/event_manager.py b/hikari/impl/event_manager.py index 8d44b7fcf7..b416f6667c 100644 --- a/hikari/impl/event_manager.py +++ b/hikari/impl/event_manager.py @@ -35,7 +35,7 @@ class EventManagerImpl(event_manager_core.EventManagerCore): """Provides event handling logic for Discord events.""" - async def on_connect(self, shard: gateway.Gateway, _: data_binding.JSONObject) -> None: + async def on_connected(self, shard: gateway.Gateway, _: data_binding.JSONObject) -> None: """Handle connection events. This is a synthetic event produced by the gateway implementation in @@ -44,7 +44,7 @@ async def on_connect(self, shard: gateway.Gateway, _: data_binding.JSONObject) - # FIXME: this should be in entity factory await self.dispatch(other.ConnectedEvent(shard=shard)) - async def on_disconnect(self, shard: gateway.Gateway, _: data_binding.JSONObject) -> None: + async def on_disconnected(self, shard: gateway.Gateway, _: data_binding.JSONObject) -> None: """Handle disconnection events. This is a synthetic event produced by the gateway implementation in diff --git a/hikari/impl/gateway_zookeeper.py b/hikari/impl/gateway_zookeeper.py index e7f368cfcd..c47d025a5f 100644 --- a/hikari/impl/gateway_zookeeper.py +++ b/hikari/impl/gateway_zookeeper.py @@ -374,7 +374,8 @@ async def _fetch_gateway_recommendations(self) -> gateway_models.GatewayBot: async def _abort(self) -> None: for shard_id in self._tasks: - await self._shards[shard_id].close() + if self._shards[shard_id].is_alive: + await self._shards[shard_id].close() await asyncio.gather(*self._tasks.values(), return_exceptions=True) async def _gather(self) -> None: diff --git a/hikari/net/gateway.py b/hikari/net/gateway.py index 27b6ce5f9f..cda5caa48a 100644 --- a/hikari/net/gateway.py +++ b/hikari/net/gateway.py @@ -256,12 +256,12 @@ async def close(self) -> None: if self.is_alive: self.logger.info("received request to shut down shard") else: - self.logger.debug("shard marked as closed before it was able to start") + self.logger.debug("shard marked as closed when it was not running") self._request_close_event.set() if self._ws is not None: - self.logger.warning("gateway client closed by user, will not attempt to restart") - await self._close_ws(self._GatewayCloseCode.RFC_6455_NORMAL_CLOSURE, "user shut down application") + self.logger.warning("gateway client closed, will not attempt to restart") + await self._close_ws(self._GatewayCloseCode.RFC_6455_NORMAL_CLOSURE, "client shut down") async def _run(self) -> None: """Start the shard and wait for it to shut down.""" @@ -356,6 +356,16 @@ async def _run_once(self) -> bool: self._backoff.reset() return not self._request_close_event.is_set() + except errors.GatewayServerClosedConnectionError as ex: + if ex.can_reconnect: + self.logger.warning( + "server closed the connection with %s (%s), will attempt to reconnect", ex.code, ex.reason, + ) + await self._close_ws(self._GatewayCloseCode.RFC_6455_NORMAL_CLOSURE, "you hung up on me") + else: + await self._close_ws(self._GatewayCloseCode.RFC_6455_UNEXPECTED_CONDITION, "you failed the connection") + raise + except Exception as ex: self.logger.error("unexpected exception occurred, shard will now die", exc_info=ex) await self._close_ws(self._GatewayCloseCode.RFC_6455_UNEXPECTED_CONDITION, "unexpected error occurred") @@ -581,7 +591,7 @@ async def _receive_json_payload(self) -> data_binding.JSONObject: else: reason = f"unknown close code {close_code}" - can_reconnect = close_code in ( + can_reconnect = close_code < 4000 or close_code in ( self._GatewayCloseCode.DECODE_ERROR, self._GatewayCloseCode.INVALID_SEQ, self._GatewayCloseCode.UNKNOWN_ERROR, @@ -589,7 +599,8 @@ async def _receive_json_payload(self) -> data_binding.JSONObject: self._GatewayCloseCode.RATE_LIMITED, ) - raise errors.GatewayServerClosedConnectionError(reason, close_code, can_reconnect, False, True) + # Always try to resume if possible first. + raise errors.GatewayServerClosedConnectionError(reason, close_code, can_reconnect, can_reconnect, True) elif message.type == aiohttp.WSMsgType.CLOSING or message.type == aiohttp.WSMsgType.CLOSED: raise self._SocketClosed() diff --git a/hikari/net/rest.py b/hikari/net/rest.py index 8f81c2eb84..907508e4e3 100644 --- a/hikari/net/rest.py +++ b/hikari/net/rest.py @@ -115,7 +115,6 @@ def __init__( token: typing.Union[undefined.Undefined, str] = undefined.Undefined(), token_type: typing.Union[undefined.Undefined, str] = undefined.Undefined(), rest_url: typing.Union[undefined.Undefined, str] = undefined.Undefined(), - oauth2_url: typing.Union[undefined.Undefined, str] = undefined.Undefined(), version: int, ) -> None: super().__init__( @@ -152,11 +151,7 @@ def __init__( if isinstance(rest_url, undefined.Undefined): rest_url = strings.REST_API_URL - if isinstance(oauth2_url, undefined.Undefined): - oauth2_url = strings.OAUTH2_API_URL - self._rest_url = rest_url.format(self) - self._oauth2_url = oauth2_url.format(self) @property def app(self) -> app_.IRESTApp: From 51df429af2c96453d9aa22b7ddafa9f60e8b7a95 Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Thu, 11 Jun 2020 10:48:49 +0100 Subject: [PATCH 494/922] Fixed TypeError on receiving embed with no color set. Rewired embed upload code and verified it works. Support for relative paths in attachments. Finalised attachment upload interface. Reformatted. --- hikari/api/entity_factory.py | 11 +- hikari/impl/entity_factory.py | 77 ++++++++---- hikari/models/embeds.py | 229 +++++++++++++++++++--------------- hikari/models/emojis.py | 2 +- hikari/models/users.py | 17 ++- hikari/net/rest.py | 41 +++--- hikari/utilities/files.py | 94 +++++++++----- 7 files changed, 295 insertions(+), 176 deletions(-) diff --git a/hikari/api/entity_factory.py b/hikari/api/entity_factory.py index 55b9bc6c7a..fd11849222 100644 --- a/hikari/api/entity_factory.py +++ b/hikari/api/entity_factory.py @@ -50,6 +50,7 @@ from hikari.net import gateway from hikari.utilities import data_binding + from hikari.utilities import files class IEntityFactory(component.IComponent, abc.ABC): @@ -314,7 +315,9 @@ def deserialize_embed(self, payload: data_binding.JSONObject) -> embed_models.Em """ @abc.abstractmethod - def serialize_embed(self, embed: embed_models.Embed) -> data_binding.JSONObject: + def serialize_embed( + self, embed: embed_models.Embed + ) -> typing.Tuple[data_binding.JSONObject, typing.List[files.Resource]]: """Serialize an embed object to a json serializable dict. Parameters @@ -324,8 +327,10 @@ def serialize_embed(self, embed: embed_models.Embed) -> data_binding.JSONObject: Returns ------- - hikari.utilities.data_binding.JSONObject - The serialized object representation. + typing.Tuple[hikari.utilities.data_binding.JSONObject, typing.List[hikari.utilities.files.Resource]] + A tuple with two items in it. The first item will be the serialized + embed representation. The second item will be a list of resources + to upload with the embed. """ ################ diff --git a/hikari/impl/entity_factory.py b/hikari/impl/entity_factory.py index dec0efaf8f..f7daa6b337 100644 --- a/hikari/impl/entity_factory.py +++ b/hikari/impl/entity_factory.py @@ -36,6 +36,7 @@ from hikari.models import audit_logs as audit_log_models from hikari.models import channels as channel_models from hikari.models import colors as color_models +from hikari.models import embeds from hikari.models import embeds as embed_models from hikari.models import emojis as emoji_models from hikari.models import gateway as gateway_models @@ -49,6 +50,7 @@ from hikari.models import webhooks as webhook_models from hikari.net import gateway from hikari.utilities import date +from hikari.utilities import files from hikari.utilities import snowflake from hikari.utilities import undefined @@ -498,6 +500,7 @@ def deserialize_channel(self, payload: data_binding.JSONObject) -> channel_model def deserialize_embed(self, payload: data_binding.JSONObject) -> embed_models.Embed: embed = embed_models.Embed() + embed.type = payload["type"] embed.title = payload.get("title") embed.description = payload.get("description") embed.url = payload.get("url") @@ -506,19 +509,24 @@ def deserialize_embed(self, payload: data_binding.JSONObject) -> embed_models.Em ) embed.color = color_models.Color(payload["color"]) if "color" in payload else None + # TODO: @FasterSpeeding, can we use `None` here instead to keep MyPy happy? if (footer_payload := payload.get("footer", ...)) is not ...: footer = embed_models.EmbedFooter() footer.text = footer_payload["text"] - footer.icon_url = footer_payload.get("icon_url") - footer.proxy_icon_url = footer_payload.get("proxy_icon_url") + if (icon_url := footer_payload.get("icon")) is not None: + footer.icon = embed_models.EmbedImage() + footer.icon.resource = files.ensure_resource(icon_url) + footer.icon.proxy_resource = files.ensure_resource(footer_payload.get("proxy_icon_url")) + else: + footer.icon = None embed.footer = footer else: embed.footer = None if (image_payload := payload.get("image", ...)) is not ...: image = embed_models.EmbedImage() - image.url = image_payload.get("url") - image.proxy_url = image_payload.get("proxy_url") + image.resource = files.ensure_resource(image_payload.get("url")) + image.proxy_resource = files.ensure_resource(image_payload.get("proxy_url")) image.height = int(image_payload["height"]) if "height" in image_payload else None image.width = int(image_payload["width"]) if "width" in image_payload else None embed.image = image @@ -526,9 +534,9 @@ def deserialize_embed(self, payload: data_binding.JSONObject) -> embed_models.Em embed.image = None if (thumbnail_payload := payload.get("thumbnail", ...)) is not ...: - thumbnail = embed_models.EmbedThumbnail() - thumbnail.url = thumbnail_payload.get("url") - thumbnail.proxy_url = thumbnail_payload.get("proxy_url") + thumbnail = embed_models.EmbedImage() + thumbnail.resource = files.ensure_resource(thumbnail_payload.get("url")) + thumbnail.proxy_resource = files.ensure_resource(thumbnail_payload.get("proxy_url")) thumbnail.height = int(thumbnail_payload["height"]) if "height" in thumbnail_payload else None thumbnail.width = int(thumbnail_payload["width"]) if "width" in thumbnail_payload else None embed.thumbnail = thumbnail @@ -537,7 +545,8 @@ def deserialize_embed(self, payload: data_binding.JSONObject) -> embed_models.Em if (video_payload := payload.get("video", ...)) is not ...: video = embed_models.EmbedVideo() - video.url = video_payload.get("url") + video.resource = files.ensure_resource(thumbnail_payload.get("url")) + video.proxy_resource = None video.height = int(video_payload["height"]) if "height" in video_payload else None video.width = int(video_payload["width"]) if "width" in video_payload else None embed.video = video @@ -556,8 +565,12 @@ def deserialize_embed(self, payload: data_binding.JSONObject) -> embed_models.Em author = embed_models.EmbedAuthor() author.name = author_payload.get("name") author.url = author_payload.get("url") - author.icon_url = author_payload.get("icon_url") - author.proxy_icon_url = author_payload.get("proxy_icon_url") + if (icon_url := author_payload.get("icon")) is not None: + author.icon = embed_models.EmbedImage() + author.icon.resource = files.ensure_resource(icon_url) + author.icon.proxy_resource = files.ensure_resource(author_payload.get("proxy_icon_url")) + else: + author.icon = None embed.author = author else: embed.author = None @@ -573,8 +586,12 @@ def deserialize_embed(self, payload: data_binding.JSONObject) -> embed_models.Em return embed - def serialize_embed(self, embed: embed_models.Embed) -> data_binding.JSONObject: + def serialize_embed( + self, embed: embed_models.Embed, + ) -> typing.Tuple[data_binding.JSONObject, typing.List[files.Resource]]: + payload = {} + uploads = [] if embed.title is not None: payload["title"] = embed.title @@ -597,24 +614,36 @@ def serialize_embed(self, embed: embed_models.Embed) -> data_binding.JSONObject: if embed.footer.text is not None: footer_payload["text"] = embed.footer.text - if embed.footer.icon_url is not None: - footer_payload["icon_url"] = embed.footer.icon_url + if embed.footer.icon is not None: + if isinstance(embed.footer.icon.resource, files.WebResource): + footer_payload["icon_url"] = embed.footer.icon.url + else: + footer_payload["icon_url"] = f"attachment://{embed.footer.icon.filename}" + uploads.append(embed.footer.icon) payload["footer"] = footer_payload if embed.image is not None: image_payload = {} - if embed.image.url is not None: - image_payload["url"] = embed.image.url + if embed.image is not None: + if isinstance(embed.image.resource, files.WebResource): + image_payload["url"] = embed.image.url + else: + image_payload["url"] = f"attachment://{embed.image.filename}" + uploads.append(embed.image.resource) payload["image"] = image_payload if embed.thumbnail is not None: thumbnail_payload = {} - if embed.thumbnail.url is not None: - thumbnail_payload["url"] = embed.thumbnail.url + if embed.thumbnail is not None: + if isinstance(embed.thumbnail.resource, files.WebResource): + thumbnail_payload["url"] = embed.thumbnail.url + else: + thumbnail_payload["url"] = f"attachment://{embed.thumbnail.filename}" + uploads.append(embed.thumbnail.resource) payload["thumbnail"] = thumbnail_payload @@ -627,8 +656,12 @@ def serialize_embed(self, embed: embed_models.Embed) -> data_binding.JSONObject: if embed.author.url is not None: author_payload["url"] = embed.author.url - if embed.author.icon_url is not None: - author_payload["icon_url"] = embed.author.icon_url + if embed.author.icon is not None: + if isinstance(embed.footer.icon.resource, files.WebResource): + author_payload["icon_url"] = embed.author.icon.url + else: + author_payload["icon_url"] = f"attachment://{embed.author.icon.filename}" + uploads.append(embed.author.icon) payload["author"] = author_payload @@ -647,7 +680,7 @@ def serialize_embed(self, embed: embed_models.Embed) -> data_binding.JSONObject: field_payloads.append(field_payload) payload["fields"] = field_payloads - return payload + return payload, uploads ################ # EMOJI MODELS # @@ -659,14 +692,14 @@ def deserialize_unicode_emoji(self, payload: data_binding.JSONObject) -> emoji_m return unicode_emoji def deserialize_custom_emoji(self, payload: data_binding.JSONObject) -> emoji_models.CustomEmoji: - custom_emoji = emoji_models.CustomEmoji() + custom_emoji = emoji_models.CustomEmoji(self._app) custom_emoji.id = snowflake.Snowflake(payload["id"]) custom_emoji.name = payload["name"] custom_emoji.is_animated = payload.get("animated", False) return custom_emoji def deserialize_known_custom_emoji(self, payload: data_binding.JSONObject) -> emoji_models.KnownCustomEmoji: - known_custom_emoji = emoji_models.KnownCustomEmoji() + known_custom_emoji = emoji_models.KnownCustomEmoji(self._app) known_custom_emoji.id = snowflake.Snowflake(payload["id"]) known_custom_emoji.name = payload["name"] known_custom_emoji.is_animated = payload.get("animated", False) diff --git a/hikari/models/embeds.py b/hikari/models/embeds.py index ccbd6cf0ed..75630e6f0d 100644 --- a/hikari/models/embeds.py +++ b/hikari/models/embeds.py @@ -22,7 +22,7 @@ __all__: typing.List[str] = [ "Embed", - "EmbedThumbnail", + "EmbedResource", "EmbedVideo", "EmbedImage", "EmbedProvider", @@ -31,6 +31,7 @@ "EmbedField", ] +import contextlib import datetime import typing @@ -39,89 +40,58 @@ from hikari.models import colors from hikari.utilities import files -_MAX_FOOTER_TEXT: typing.Final[int] = 2048 -_MAX_AUTHOR_NAME: typing.Final[int] = 256 -_MAX_FIELD_NAME: typing.Final[int] = 256 -_MAX_FIELD_VALUE: typing.Final[int] = 1024 -_MAX_EMBED_TITLE: typing.Final[int] = 256 -_MAX_EMBED_DESCRIPTION: typing.Final[int] = 2048 -_MAX_EMBED_FIELDS: typing.Final[int] = 25 -_MAX_EMBED_SIZE: typing.Final[int] = 6000 +@attr.s(slots=True, kw_only=True, init=False) +class EmbedResource(files.Resource): + """A base type for any resource provided in an embed. -@attr.s(eq=True, hash=False, init=True, kw_only=True, slots=True) -class EmbedFooter: - """Represents an embed footer.""" - - text: typing.Optional[str] = attr.ib(default=None, repr=True) - """The footer text, or `None` if not present.""" - - icon: typing.Optional[files.Resource] = attr.ib(default=None, repr=False, converter=files.ensure_resource) - """The URL of the footer icon, or `None` if not present.""" - - proxy_icon: typing.Optional[files.Resource] = attr.ib(default=None, repr=False) - """The proxied URL of the footer icon, or `None` if not present. - - !!! note - This field cannot be set by bots or webhooks while sending an embed and - will be ignored during serialization. Expect this to be populated on - any received embed attached to a message event. + Resources can be downloaded and uploaded, and may also be provided from + Discord with an additional proxy URL internally. """ + resource: files.Resource = attr.ib(repr=True) + """The resource this object wraps around.""" -@attr.s(eq=True, hash=False, init=True, kw_only=True, slots=True) -class EmbedImage: - """Represents an embed image.""" - - image: typing.Optional[files.Resource] = attr.ib(default=None, repr=True, converter=files.ensure_resource) - """The image to show, or `None` if not present.""" - - proxy_image: typing.Optional[files.Resource] = attr.ib(default=None, repr=False) - """The proxy image, or `None` if not present. + proxy_resource: typing.Optional[files.Resource] = attr.ib(default=None, repr=False) + """The proxied version of the resource, or `None` if not present. !!! note - This field cannot be set by bots or webhooks while sending an embed and - will be ignored during serialization. Expect this to be populated on - any received embed attached to a message event. + This field cannot be set by bots or webhooks while sending an embed + and will be ignored during serialization. Expect this to be + populated on any received embed attached to a message event. """ - height: typing.Optional[int] = attr.ib(default=None, repr=False) - """The height of the image, if present and known, otherwise `None`. + @property + def url(self) -> str: + return self.resource.url - !!! note - This field cannot be set by bots or webhooks while sending an embed and - will be ignored during serialization. Expect this to be populated on - any received embed attached to a message event. - """ + @property + def filename(self) -> typing.Optional[str]: + return self.resource.filename - width: typing.Optional[int] = attr.ib(default=None, repr=False) - """The width of the image, if present and known, otherwise `None`. + @contextlib.asynccontextmanager + async def stream(self) -> files.AsyncReader: + async with self.resource.stream() as stream: + yield stream - !!! note - This field cannot be set by bots or webhooks while sending an embed and - will be ignored during serialization. Expect this to be populated on - any received embed attached to a message event. - """ +@attr.s(eq=True, hash=False, init=False, kw_only=True, slots=True) +class EmbedFooter: + """Represents an embed footer.""" -@attr.s(eq=True, hash=False, init=True, kw_only=True, slots=True) -class EmbedThumbnail: - """Represents an embed thumbnail.""" + text: typing.Optional[str] = attr.ib(default=None, repr=True) + """The footer text, or `None` if not present.""" - image: typing.Optional[files.Resource] = attr.ib(default=None, repr=True) - """The URL of the thumbnail to display, or `None` if not present.""" + icon: typing.Optional[EmbedResource] = attr.ib(default=None, repr=False) + """The URL of the footer icon, or `None` if not present.""" - proxy_image: typing.Optional[files.Resource] = attr.ib(default=None, repr=False) - """The proxied URL of the thumbnail, if present and known, otherwise `None`. - !!! note - This field cannot be set by bots or webhooks while sending an embed and - will be ignored during serialization. Expect this to be populated on - any received embed attached to a message event. - """ +@attr.s(eq=True, hash=False, init=False, kw_only=True, slots=True) +class EmbedImage(EmbedResource): + """Represents an embed image.""" height: typing.Optional[int] = attr.ib(default=None, repr=False) - """The height of the thumbnail, if present and known, otherwise `None`. + """The height of the image, if present and known, otherwise `None`. !!! note This field cannot be set by bots or webhooks while sending an embed and @@ -130,7 +100,7 @@ class EmbedThumbnail: """ width: typing.Optional[int] = attr.ib(default=None, repr=False) - """The width of the thumbnail, if present and known, otherwise `None`. + """The width of the image, if present and known, otherwise `None`. !!! note This field cannot be set by bots or webhooks while sending an embed and @@ -140,7 +110,7 @@ class EmbedThumbnail: @attr.s(eq=True, hash=False, init=False, kw_only=True, slots=True) -class EmbedVideo: +class EmbedVideo(EmbedResource): """Represents an embed video. !!! note @@ -149,9 +119,6 @@ class EmbedVideo: any received embed attached to a message event with a video attached. """ - video: typing.Optional[files.Resource] = attr.ib(default=None, repr=True) - """The URL of the video.""" - height: typing.Optional[int] = attr.ib(default=None, repr=False) """The height of the video.""" @@ -177,7 +144,7 @@ class EmbedProvider: """The URL of the provider.""" -@attr.s(eq=True, hash=False, init=True, kw_only=True, slots=True) +@attr.s(eq=True, hash=False, init=False, kw_only=True, slots=True) class EmbedAuthor: """Represents an author of an embed.""" @@ -190,20 +157,11 @@ class EmbedAuthor: This may be `None` if no hyperlink on the author's name is specified. """ - icon: typing.Optional[files.Resource] = attr.ib(default=None, repr=False, converter=files.ensure_resource) - """The URL of the author's icon, or `None` if not present.""" + icon: typing.Optional[EmbedResource] = attr.ib(default=None, repr=False) + """The author's icon, or `None` if not present.""" - proxy_icon: typing.Optional[files.Resource] = attr.ib(default=None, repr=False) - """The proxied URL of the author icon, or `None` if not present. - !!! note - This field cannot be set by bots or webhooks while sending an embed and - will be ignored during serialization. Expect this to be populated on - any received embed attached to a message event. - """ - - -@attr.s(eq=True, hash=False, init=True, kw_only=True, slots=True) +@attr.s(eq=True, hash=False, init=False, kw_only=True, slots=True) class EmbedField: """Represents a field in a embed.""" @@ -221,7 +179,24 @@ class EmbedField: class Embed: """Represents an embed.""" - _color: typing.Optional[colors.Color] = attr.ib(default=None, repr=False) + type: str = attr.ib(default="rich", repr=True) + """The type of the embed. + + Defaults to `"rich"`. + + !!! note + You can only specify `"rich"` when creating a new embed. Any other + value will be ignored. + """ + + color: typing.Optional[colors.Color] = attr.ib( + default=None, repr=False, converter=lambda c: colors.Color.of(c) if c is not None else None, + ) + """The colour of the embed, or `None` to use the default.""" + + colour: typing.Optional[colors.Color] = property( + lambda self: self.color, lambda self, colour: setattr(self, "color", colour) + ) title: typing.Optional[str] = attr.ib(default=None, repr=True) """The title of the embed, or `None` if not present.""" @@ -295,7 +270,7 @@ class Embed: image: typing.Optional[EmbedImage] = attr.ib(default=None, repr=False) """The image to display in the embed, or `None` if not present.""" - thumbnail: typing.Optional[EmbedThumbnail] = attr.ib(default=None, repr=False) + thumbnail: typing.Optional[EmbedImage] = attr.ib(default=None, repr=False) """The thumbnail to show in the embed, or `None` if not present.""" video: typing.Optional[EmbedVideo] = attr.ib(default=None, repr=False) @@ -323,18 +298,72 @@ class Embed: fields: typing.MutableSequence[EmbedField] = attr.ib(factory=list, repr=False) """The fields in the embed.""" - @property - def color(self) -> typing.Optional[colors.Color]: - """Embed color, or `None` if not present.""" - return self._color - - @color.setter - def color(self, color: colors.ColorLike) -> None: - self._color = colors.Color.of(color) - - @color.deleter - def color(self) -> None: - self._color = None - - colour = color - """An alias for `color`.""" + def set_author( + self, + *, + name: typing.Optional[str] = None, + url: typing.Optional[str] = None, + icon: typing.Union[None, str, files.Resource] = None, + ) -> Embed: + if name is None and url is None and icon is None: + self.author = None + else: + self.author = EmbedAuthor() + self.author.name = name + self.author.url = url + if icon is not None: + self.author.icon = EmbedResource() + self.author.icon.resource = files.ensure_resource(icon) + else: + self.author.icon = None + return self + + def set_footer( + self, *, text: typing.Optional[str] = None, icon: typing.Union[None, str, files.Resource] = None, + ) -> Embed: + if text is None and icon is None: + self.footer = None + else: + self.footer = EmbedFooter() + self.footer.text = text + if icon is not None: + self.footer.icon = EmbedResource() + self.footer.icon.resource = files.ensure_resource(icon) + else: + self.footer.icon = None + return self + + def set_image(self, image: typing.Union[None, str, files.Resource] = None, /) -> Embed: + if image is None: + self.image = None + else: + self.image = EmbedImage() + self.image.resource = files.ensure_resource(image) + return self + + def set_thumbnail(self, image: typing.Union[None, str, files.Resource] = None, /) -> Embed: + if image is None: + self.thumbnail = None + else: + self.thumbnail = EmbedImage() + self.thumbnail.resource = files.ensure_resource(image) + return self + + def add_field(self, name: str, value: str, *, inline: bool = False) -> Embed: + field = EmbedField() + field.name = name + field.value = value + field.is_inline = inline + self.fields.append(field) + return self + + def edit_field(self, index: int, name: str, value: str, /, *, inline: bool = False) -> Embed: + field = self.fields[index] + field.name = name + field.value = value + field.is_inline = inline + return self + + def remove_field(self, index: int, /) -> Embed: + del self.fields[index] + return self diff --git a/hikari/models/emojis.py b/hikari/models/emojis.py index 49130d9b43..a65a61e0e1 100644 --- a/hikari/models/emojis.py +++ b/hikari/models/emojis.py @@ -182,7 +182,7 @@ def from_unicode_escape(cls, escape: str) -> UnicodeEmoji: @attr.s(eq=True, hash=True, init=False, kw_only=True, slots=True) -class CustomEmoji(Emoji, bases.Entity, bases.Unique): +class CustomEmoji(bases.Entity, bases.Unique, Emoji): """Represents a custom emoji. This is a custom emoji that is from a guild you might not be part of. diff --git a/hikari/models/users.py b/hikari/models/users.py index 13fa197f78..fa1a6ef16f 100644 --- a/hikari/models/users.py +++ b/hikari/models/users.py @@ -29,6 +29,7 @@ from hikari.models import bases from hikari.utilities import cdn +from hikari.utilities import files from hikari.utilities import undefined @@ -162,11 +163,13 @@ async def fetch_self(self) -> User: return await self._app.rest.fetch_user(user=self.id) @property - def avatar_url(self) -> typing.Optional[str]: + def avatar(self) -> typing.Optional[files.URL]: """URL for this user's custom avatar if set, else `None`.""" return self.format_avatar_url() - def format_avatar_url(self, *, format_: typing.Optional[str] = None, size: int = 4096) -> typing.Optional[str]: + def format_avatar_url( + self, *, format_: typing.Optional[str] = None, size: int = 4096 + ) -> typing.Optional[files.URL]: """Generate the avatar URL for this user's custom avatar if set. If no custom avatar is set, this returns `None`. You can then use the @@ -187,7 +190,7 @@ def format_avatar_url(self, *, format_: typing.Optional[str] = None, size: int = Returns ------- - str + hikari.utilities.files.URL The string URL, or `None` if not present. Raises @@ -196,7 +199,8 @@ def format_avatar_url(self, *, format_: typing.Optional[str] = None, size: int = If `size` is not a power of two or not between 16 and 4096. """ if self.avatar_hash is not None: - return cdn.get_avatar_url(self.id, self.avatar_hash, format_=format_, size=size) + url = cdn.get_avatar_url(self.id, self.avatar_hash, format_=format_, size=size) + return files.URL(url) return None @property @@ -205,9 +209,10 @@ def default_avatar_index(self) -> int: return cdn.get_default_avatar_index(self.discriminator) @property - def default_avatar_url(self) -> str: + def default_avatar(self) -> files.URL: """URL for this user's default avatar.""" - return cdn.get_default_avatar_url(self.discriminator) + url = cdn.get_default_avatar_url(self.discriminator) + return files.URL(url) @attr.s(eq=True, hash=True, init=False, kw_only=True, slots=True) diff --git a/hikari/net/rest.py b/hikari/net/rest.py index 907508e4e3..6ddf3a741c 100644 --- a/hikari/net/rest.py +++ b/hikari/net/rest.py @@ -1013,7 +1013,9 @@ async def create_message( text: typing.Union[undefined.Undefined, typing.Any] = undefined.Undefined(), *, embed: typing.Union[undefined.Undefined, embeds_.Embed] = undefined.Undefined(), - attachments: typing.Union[undefined.Undefined, typing.Sequence[files.Resource]] = undefined.Undefined(), + attachments: typing.Union[ + undefined.Undefined, typing.Sequence[typing.Union[str, files.Resource]] + ] = undefined.Undefined(), tts: typing.Union[undefined.Undefined, bool] = undefined.Undefined(), nonce: typing.Union[undefined.Undefined, str] = undefined.Undefined(), mentions_everyone: bool = True, @@ -1031,8 +1033,9 @@ async def create_message( If specified, the message contents. embed : hikari.utilities.undefined.Undefined or hikari.models.embeds.Embed If specified, the message embed. - attachments : hikari.utilities.undefined.Undefined or typing.Sequence[hikari.utilities.files.Resource] - If specified, the message attachments. + attachments : hikari.utilities.undefined.Undefined or typing.Sequence[str or hikari.utilities.files.Resource] + If specified, the message attachments. These can be resources, or + strings consisting of paths on your computer or URLs. tts : hikari.utilities.undefined.Undefined or bool If specified, whether the message will be TTS (Text To Speech). nonce : hikari.utilities.undefined.Undefined or str @@ -1081,13 +1084,18 @@ async def create_message( body = data_binding.JSONObjectBuilder() body.put("allowed_mentions", self._generate_allowed_mentions(mentions_everyone, user_mentions, role_mentions)) body.put("content", text, conversion=str) - body.put("embed", embed, conversion=self._app.entity_factory.serialize_embed) body.put("nonce", nonce) body.put("tts", tts) - attachments = [] if isinstance(attachments, undefined.Undefined) else [a for a in attachments] + if isinstance(attachments, undefined.Undefined): + attachments = [] + else: + attachments = [files.ensure_resource(a) for a in attachments] - # TODO: embed handle images. + if not isinstance(embed, undefined.Undefined): + embed_payload, embed_attachments = self._app.entity_factory.serialize_embed(embed) + body.put("embed", embed_payload) + attachments.extend(embed_attachments) if attachments: form = data_binding.URLEncodedForm() @@ -1097,7 +1105,7 @@ async def create_message( try: for i, attachment in enumerate(attachments): - stream = await stack.enter_async_context(attachment.stream()) + stream = await stack.enter_async_context(attachment.stream(executor=self._app.thread_pool_executor)) form.add_field( f"file{i}", stream, filename=stream.filename, content_type=self._APPLICATION_OCTET_STREAM ) @@ -1380,7 +1388,7 @@ async def create_webhook( body = data_binding.JSONObjectBuilder() body.put("name", name) if not isinstance(avatar, undefined.Undefined): - async with avatar.stream() as stream: + async with avatar.stream(executor=self._app.thread_pool_executor) as stream: body.put("avatar", await stream.data_uri()) raw_response = await self._request(route, body=body, reason=reason) @@ -1441,7 +1449,7 @@ async def edit_webhook( if avatar is None: body.put("avatar", None) else: - async with avatar.stream() as stream: + async with avatar.stream(executor=self._app.thread_pool_executor) as stream: body.put("avatar", await stream.data_uri()) raw_response = await self._request(route, body=body, reason=reason) @@ -1488,8 +1496,9 @@ async def execute_webhook( if not isinstance(embeds, undefined.Undefined): for embed in embeds: - # TODO: embed attachments. - serialized_embeds.append(self._app.entity_factory.serialize_embed(embed)) + embed_payload, embed_attachments = self._app.entity_factory.serialize_embed(embed) + serialized_embeds.append(embed_payload) + attachments.extend(embed_attachments) body = data_binding.JSONObjectBuilder() body.put("mentions", self._generate_allowed_mentions(mentions_everyone, user_mentions, role_mentions)) @@ -1562,7 +1571,7 @@ async def edit_my_user( body.put("username", username) if isinstance(avatar, files.Resource): - async with avatar.stream() as stream: + async with avatar.stream(executor=self._app.thread_pool_executor) as stream: body.put("avatar", await stream.data_uri()) else: body.put("avatar", avatar) @@ -1709,7 +1718,7 @@ async def create_emoji( body = data_binding.JSONObjectBuilder() body.put("name", name) if not isinstance(image, undefined.Undefined): - async with image.stream() as stream: + async with image.stream(executor=self._app.thread_pool_executor) as stream: body.put("image", await stream.data_uri()) body.put_snowflake_array("roles", roles) @@ -1819,21 +1828,21 @@ async def edit_guild( if icon is None: body.put("icon", None) else: - async with icon.stream() as stream: + async with icon.stream(executor=self._app.thread_pool_executor) as stream: body.put("icon", await stream.data_uri()) if not isinstance(splash, undefined.Undefined): if splash is None: body.put("splash", None) else: - async with splash.stream() as stream: + async with splash.stream(executor=self._app.thread_pool_executor) as stream: body.put("splash", await stream.data_uri()) if not isinstance(banner, undefined.Undefined): if banner is None: body.put("banner", None) else: - async with banner.stream() as stream: + async with banner.stream(executor=self._app.thread_pool_executor) as stream: body.put("banner", await stream.data_uri()) raw_response = await self._request(route, body=body, reason=reason) diff --git a/hikari/utilities/files.py b/hikari/utilities/files.py index 826323180c..c824c0ac6a 100644 --- a/hikari/utilities/files.py +++ b/hikari/utilities/files.py @@ -18,7 +18,12 @@ # along with Hikari. If not, see . from __future__ import annotations -__all__ = [] +__all__ = [ + "Resource", + "Bytes", + "File", + "URL", +] import abc import asyncio @@ -43,17 +48,34 @@ _LOGGER: typing.Final[logging.Logger] = klass.get_logger(__name__) _MAGIC: typing.Final[int] = 50 * 1024 -_FILE: typing.Final[str] = "file://" -def ensure_resource(url_or_resource: typing.Union[str, Resource]) -> Resource: +@typing.overload +def ensure_resource(url_or_resource: None) -> None: + """Given None, return None.""" + + +@typing.overload +def ensure_resource(url_or_resource: str) -> Resource: + """Given a string, convert it to a resource.""" + + +@typing.overload +def ensure_resource(url_or_resource: Resource) -> Resource: + """Given a resource, return it..""" + + +def ensure_resource(url_or_resource: typing.Union[None, str, os.PathLike, Resource],) -> typing.Optional[Resource]: """Given a resource or string, convert it to a valid resource as needed.""" if isinstance(url_or_resource, Resource): return url_or_resource - else: - if url_or_resource.startswith(_FILE): - return File(url_or_resource[len(_FILE) :]) - return URL(url_or_resource) + elif url_or_resource is not None: + if url_or_resource.startswith(("https://", "http://")): + return URL(url_or_resource) + else: + path = pathlib.Path(url_or_resource) + return File(path, path.name) + return None def guess_mimetype_from_filename(name: str) -> typing.Optional[str]: @@ -143,11 +165,15 @@ def __aiter__(self) -> typing.AsyncGenerator[typing.Any, bytes]: @attr.s(auto_attribs=True, slots=True) class FileReader(AsyncReader): executor: typing.Optional[concurrent.futures.Executor] - path: typing.Union[str, os.PathLike] + path: typing.Union[str, os.PathLike, pathlib.Path] loop: asyncio.AbstractEventLoop = attr.ib(factory=asyncio.get_running_loop) async def __aiter__(self) -> typing.AsyncGenerator[typing.Any, bytes]: - fp = await self.loop.run_in_executor(self.executor, self._open, self.path) + path = self.path + if isinstance(path, pathlib.Path): + path = await self.loop.run_in_executor(self.executor, self._expand, self.path) + + fp = await self.loop.run_in_executor(self.executor, self._open, path) try: while True: chunk = await self.loop.run_in_executor(self.executor, self._read_chunk, fp, _MAGIC) @@ -157,6 +183,12 @@ async def __aiter__(self) -> typing.AsyncGenerator[typing.Any, bytes]: finally: await self.loop.run_in_executor(self.executor, self._close, fp) + @staticmethod + def _expand(path: pathlib.Path) -> pathlib.Path: + # .expanduser is Platform dependent. Will expand stuff like ~ to /home/ on posix. + # .resolve will follow symlinks and what-have-we to translate stuff like `..` to proper paths. + return path.expanduser().resolve() + @staticmethod def _read_chunk(fp: typing.IO[bytes], n: int = 10_000) -> bytes: return fp.read(n) @@ -188,8 +220,9 @@ async def __aiter__(self) -> typing.AsyncGenerator[typing.Any, bytes]: yield chunk[0] -@attr.s(auto_attribs=True) class Resource(abc.ABC): + __slots__ = () + @property @abc.abstractmethod def url(self) -> str: @@ -202,11 +235,14 @@ def filename(self) -> typing.Optional[str]: @abc.abstractmethod @contextlib.asynccontextmanager - async def stream(self) -> AsyncReader: + async def stream(self, *, executor: typing.Optional[concurrent.futures.ThreadPoolExecutor]) -> AsyncReader: """Return an async iterable of bytes to stream.""" -class RawBytes(Resource): +@attr.s(init=False, slots=True) +class Bytes(Resource): + data: bytes + def __init__( self, data: bytes, @@ -244,18 +280,25 @@ def filename(self) -> typing.Optional[str]: return self._filename @contextlib.asynccontextmanager - async def stream(self) -> AsyncReader: + async def stream(self, *, executor: typing.Optional[concurrent.futures.ThreadPoolExecutor] = None) -> AsyncReader: yield ByteReader(self.filename, self.mimetype, self.data) class WebResource(Resource, abc.ABC): + __slots__ = () + @contextlib.asynccontextmanager - async def stream(self) -> WebReader: + async def stream(self, *, executor: typing.Optional[concurrent.futures.ThreadPoolExecutor] = None) -> WebReader: """Start streaming the content into memory by downloading it. You can use this to fetch the entire resource, parts of the resource, or just to view any metadata that may be provided. + Parameters + ---------- + executor : + Not used. Provided only to match the underlying interface. + Examples -------- Downloading an entire resource at once into memory: @@ -337,11 +380,11 @@ async def stream(self) -> WebReader: await http_client.parse_error_response(resp) -@attr.s(auto_attribs=True) +@attr.s(slots=True) class URL(WebResource): """A URL that represents a web resource.""" - _url: str + _url: typing.Union[str] = attr.ib(init=True) @property def url(self) -> str: @@ -353,17 +396,10 @@ def filename(self) -> str: return os.path.basename(url.path) +@attr.s(slots=True) class File(Resource): - def __init__( - self, - path: typing.Union[str, os.PathLike], - *, - filename: typing.Optional[str] = None, - executor: typing.Optional[concurrent.futures.Executor] = None, - ) -> None: - self.path = path - self._filename = filename if filename is not None else os.path.basename(path) - self.executor = executor + path: typing.Union[str, os.PathLike, pathlib.Path] = attr.ib() + _filename: typing.Optional[str] = attr.ib(default=None) @property def url(self) -> str: @@ -371,8 +407,10 @@ def url(self) -> str: @property def filename(self) -> typing.Optional[str]: + if self._filename is None: + return os.path.basename(self.path) return self._filename @contextlib.asynccontextmanager - async def stream(self) -> AsyncReader: - yield FileReader(self.filename, None, self.executor, self.path) + async def stream(self, *, executor: typing.Optional[concurrent.futures.ThreadPoolExecutor] = None) -> AsyncReader: + yield FileReader(self.filename, None, executor, self.path) From 3bb1ea2aa7e6b7da16a73f93a26878b968fc76fc Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Thu, 11 Jun 2020 19:19:18 +0100 Subject: [PATCH 495/922] Docstrings in files.py. --- hikari/errors.py | 24 +-- hikari/models/embeds.py | 4 +- hikari/net/http_settings.py | 4 +- hikari/net/strings.py | 2 + hikari/utilities/files.py | 283 ++++++++++++++++++++++++++++++++---- hikari/utilities/klass.py | 7 - 6 files changed, 272 insertions(+), 52 deletions(-) diff --git a/hikari/errors.py b/hikari/errors.py index d38ea6723f..c7061f2fbf 100644 --- a/hikari/errors.py +++ b/hikari/errors.py @@ -320,27 +320,27 @@ def __init__(self, url: str, headers: data_binding.Headers, raw_body: typing.Any class RateLimited(ClientHTTPErrorResponse): """Raised when a non-global ratelimit that cannot be handled occurs. - - This should only ever occur for specific routes that have additional + + This should only ever occur for specific routes that have additional rate-limits applied to them by Discord. At the time of writing, the PATCH CHANNEL endpoint is the only one that knowingly implements this, and does so by implementing rate-limits on the usage of specific fields only. - + If you receive one of these, you should NOT try again until the given time has passed, either discarding the operation you performed, or waiting until the given time has passed first. Note that it may still be valid to - send requests with different attributes in them. - - A use case for this by Discord appears to be to stop abuse from bots that - change channel names, etc, regularly. This kind of action allegedly causes + send requests with different attributes in them. + + A use case for this by Discord appears to be to stop abuse from bots that + change channel names, etc, regularly. This kind of action allegedly causes a fair amount of overhead internally for Discord. In the case you encounter this, you may be able to send different requests that manipulate the same entities (in this case editing the same channel) that do not use the same - collection of attributes as the previous request. - + collection of attributes as the previous request. + You should not usually see this occur, unless Discord vastly change their - ratelimit system without prior warning, which might happen in the future. - + ratelimit system without prior warning, which might happen in the future. + If you receive this regularly, please file a bug report, or contact Discord with the relevant debug information that can be obtained by enabling debug logs and enabling the debug mode on the REST components. @@ -357,7 +357,7 @@ class RateLimited(ClientHTTPErrorResponse): The body that was received. retry_after : float How many seconds to wait before you can reuse the route with the - specific request. + specific request. """ __slots__ = () diff --git a/hikari/models/embeds.py b/hikari/models/embeds.py index 75630e6f0d..ceee925b37 100644 --- a/hikari/models/embeds.py +++ b/hikari/models/embeds.py @@ -181,9 +181,9 @@ class Embed: type: str = attr.ib(default="rich", repr=True) """The type of the embed. - + Defaults to `"rich"`. - + !!! note You can only specify `"rich"` when creating a new embed. Any other value will be ignored. diff --git a/hikari/net/http_settings.py b/hikari/net/http_settings.py index 93f279b589..2147dbd5ac 100644 --- a/hikari/net/http_settings.py +++ b/hikari/net/http_settings.py @@ -51,8 +51,8 @@ class HTTPSettings: """The optional URL of the proxy to send requests via.""" request_timeout: typing.Optional[float] = 10.0 - """Optional request timeout to use. - + """Optional request timeout to use. + If an HTTP request takes longer than this, it will be aborted. Defaults to 10 seconds. diff --git a/hikari/net/strings.py b/hikari/net/strings.py index 33634aa323..40e15433e8 100644 --- a/hikari/net/strings.py +++ b/hikari/net/strings.py @@ -16,6 +16,8 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . +"""Various strings used in multiple places.""" + from __future__ import annotations import platform diff --git a/hikari/utilities/files.py b/hikari/utilities/files.py index c824c0ac6a..ca5d9b9383 100644 --- a/hikari/utilities/files.py +++ b/hikari/utilities/files.py @@ -16,9 +16,13 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . +"""Utilities and classes for interacting with files and web resources.""" + from __future__ import annotations __all__ = [ + "ensure_resource", + "AsyncReader", "Resource", "Bytes", "File", @@ -51,22 +55,35 @@ @typing.overload -def ensure_resource(url_or_resource: None) -> None: +def ensure_resource(url_or_resource: None, /) -> None: """Given None, return None.""" @typing.overload -def ensure_resource(url_or_resource: str) -> Resource: +def ensure_resource(url_or_resource: str, /) -> Resource: """Given a string, convert it to a resource.""" @typing.overload -def ensure_resource(url_or_resource: Resource) -> Resource: - """Given a resource, return it..""" +def ensure_resource(url_or_resource: Resource, /) -> Resource: + """Given a resource, return it.""" + + +def ensure_resource(url_or_resource: typing.Union[None, str, os.PathLike, Resource], /) -> typing.Optional[Resource]: + """Given a resource or string, convert it to a valid resource as needed. + Parameters + ---------- + url_or_resource : None or str or os.PathLike or Resource + The item to convert. If the item is `None`, then `None` is returned. + Likewise if a `Resource` is passed, it is simply returned again. + Anything else is converted to a `Resource` first. -def ensure_resource(url_or_resource: typing.Union[None, str, os.PathLike, Resource],) -> typing.Optional[Resource]: - """Given a resource or string, convert it to a valid resource as needed.""" + Returns + ------- + Resource or None + The resource to use, or `None` if `None` was input. + """ if isinstance(url_or_resource, Resource): return url_or_resource elif url_or_resource is not None: @@ -78,9 +95,14 @@ def ensure_resource(url_or_resource: typing.Union[None, str, os.PathLike, Resour return None -def guess_mimetype_from_filename(name: str) -> typing.Optional[str]: +def guess_mimetype_from_filename(name: str, /) -> typing.Optional[str]: """Guess the mimetype of an object given a filename. + Parameters + ---------- + name : bytes + The filename to inspect. + Returns ------- str or None @@ -90,7 +112,24 @@ def guess_mimetype_from_filename(name: str) -> typing.Optional[str]: return mimetypes.guess_type(name) -def guess_mimetype_from_data(data: bytes) -> typing.Optional[str]: +def guess_mimetype_from_data(data: bytes, /) -> typing.Optional[str]: + """Guess the mimetype of some data from the header. + + !!! warning + This function only detects valid image headers that Discord allows + the use of. Anything else will go undetected. + + Parameters + ---------- + data : bytes + The byte content to inspect. + + Returns + ------- + str or None + The mimetype, if it was found. If the header is unrecognised, then + `None` is returned. + """ if data.startswith(b"\211PNG\r\n\032\n"): return "image/png" elif data[6:].startswith((b"Exif", b"JFIF")): @@ -104,6 +143,26 @@ def guess_mimetype_from_data(data: bytes) -> typing.Optional[str]: def guess_file_extension(mimetype: str) -> typing.Optional[str]: + """Guess the file extension for a given mimetype. + + Parameters + ---------- + mimetype : str + The mimetype to guess the extension for. + + Example + ------- + ```py + >>> guess_file_extension("image/png") + ".png" + ``` + + Returns + ------- + str or None + The file extension, prepended with a `.`. If no match was found, + return `None`. + """ return mimetypes.guess_extension(mimetype) @@ -113,6 +172,22 @@ def generate_filename_from_details( extension: typing.Optional[str] = None, data: typing.Optional[bytes] = None, ) -> str: + """Given optional information about a resource, generate a filename. + + Parameters + ---------- + mimetype : str or None + The mimetype of the content, or `None` if not known. + extension : str or None + The file extension to use, or `None` if not known. + data : bytes or None + The data to inspect, or `None` if not known. + + Returns + ------- + str + A generated quasi-unique filename. + """ if data is not None and mimetype is None: mimetype = guess_mimetype_from_data(data) @@ -128,6 +203,20 @@ def generate_filename_from_details( def to_data_uri(data: bytes, mimetype: typing.Optional[str]) -> str: + """Convert the data and mimetype to a data URI. + + Parameters + ---------- + data : bytes + The data to encode as base64. + mimetype : str or None + The mimetype, or `None` if we should attempt to guess it. + + Returns + ------- + str + A data URI string. + """ if mimetype is None: mimetype = guess_mimetype_from_data(data) @@ -140,13 +229,27 @@ def to_data_uri(data: bytes, mimetype: typing.Optional[str]) -> str: @attr.s(auto_attribs=True, slots=True) class AsyncReader(typing.AsyncIterable[bytes], abc.ABC): + """Protocol for reading a resource asynchronously using bit inception. + + This supports being used as an async iterable, although the implementation + detail is left to each implementation of this class to define. + """ + filename: str + """The filename of the resource.""" + mimetype: typing.Optional[str] + """The mimetype of the resource. May be `None` if not known.""" async def data_uri(self) -> str: + """Fetch the data URI. + + This reads the entire resource. + """ return to_data_uri(await self.read(), self.mimetype) async def read(self) -> bytes: + """Read the rest of the resource and return it in a `bytes` object.""" buff = bytearray() async for chunk in self: buff.extend(chunk) @@ -155,7 +258,10 @@ async def read(self) -> bytes: @attr.s(auto_attribs=True, slots=True) class ByteReader(AsyncReader): + """Asynchronous file reader that operates on in-memory data.""" + data: bytes + """The data that will be yielded in chunks.""" def __aiter__(self) -> typing.AsyncGenerator[typing.Any, bytes]: for i in range(0, len(self.data), _MAGIC): @@ -164,9 +270,18 @@ def __aiter__(self) -> typing.AsyncGenerator[typing.Any, bytes]: @attr.s(auto_attribs=True, slots=True) class FileReader(AsyncReader): - executor: typing.Optional[concurrent.futures.Executor] + """Asynchronous file reader that reads a resource from local storage.""" + + executor: typing.Optional[concurrent.futures.ThreadPoolExecutor] + """The associated `concurrent.futures.ThreadPoolExecutor` to use for + blocking IO. + """ + path: typing.Union[str, os.PathLike, pathlib.Path] + """The path to the resource to read.""" + loop: asyncio.AbstractEventLoop = attr.ib(factory=asyncio.get_running_loop) + """The event loop to use.""" async def __aiter__(self) -> typing.AsyncGenerator[typing.Any, bytes]: path = self.path @@ -204,12 +319,25 @@ def _close(fp: typing.IO[bytes]) -> None: @attr.s(auto_attribs=True, slots=True) class WebReader(AsyncReader): + """Asynchronous reader to use to read data from a web resource.""" + stream: aiohttp.StreamReader - uri: str + """The `aiohttp.StreamReader` to read the content from.""" + + url: str + """The URL being read from.""" + status: int + """The initial HTTP response status.""" + reason: str + """The HTTP response status reason.""" + charset: typing.Optional[str] + """Optional character set information, if known.""" + size: typing.Optional[int] + """The size of the resource, if known.""" async def read(self) -> bytes: return await self.stream.read() @@ -221,17 +349,24 @@ async def __aiter__(self) -> typing.AsyncGenerator[typing.Any, bytes]: class Resource(abc.ABC): + """Base for any uploadable or downloadable representation of information. + + These representations can be streamed using bit inception for performance, + which may result in significant decrease in memory usage for larger + resources. + """ + __slots__ = () @property @abc.abstractmethod def url(self) -> str: - """The URL, if known.""" + """URL of the resource.""" @property @abc.abstractmethod - def filename(self) -> typing.Optional[str]: - """The filename, if known.""" + def filename(self) -> str: + """Filename of the resource.""" @abc.abstractmethod @contextlib.asynccontextmanager @@ -239,9 +374,42 @@ async def stream(self, *, executor: typing.Optional[concurrent.futures.ThreadPoo """Return an async iterable of bytes to stream.""" -@attr.s(init=False, slots=True) class Bytes(Resource): + """Representation of in-memory data to upload. + + Parameters + ---------- + data : bytes + The raw data. + mimetype : str or None + The mimetype, or `None` if you do not wish to specify this. + filename : str or None + The filename to use, or `None` if one should be generated as needed. + extension : str or None + The file extension to use, or `None` if one should be determined + manually as needed. + + !!! note + You only need to provide one of `mimetype`, `filename`, or `extension`. + The other information will be determined using Python's `mimetypes` + module. + + If none of these three are provided, then a crude guess may be + made successfully for specific image types. If no file format + information can be calculated, then the resource will fail during + uploading. + """ + + __slots__ = ("data", "_filename", "mimetype", "extension") + data: bytes + """The raw data to upload.""" + + mimetype: typing.Optional[str] + """The provided mimetype, if specified. Otherwise `None`.""" + + extension: typing.Optional[str] + """The provided file extension, if specified. Otherwise `None`.""" def __init__( self, @@ -263,20 +431,20 @@ def __init__( if filename is None and mimetype is None: if extension is None: - raise TypeError("Cannot infer data type details, please specify one of filetype, filename, extension") + raise TypeError("Cannot infer data type details, please specify one of filetype, mimetype, extension") else: raise TypeError("Cannot infer data type details from extension. Please specify mimetype or filename") self._filename = filename - self.mimetype: str = mimetype - self.extension: typing.Optional[str] = extension + self.mimetype = mimetype + self.extension = extension @property def url(self) -> str: - return to_data_uri(self.data, self.mimetype) + return f"attachment://{self.filename}" @property - def filename(self) -> typing.Optional[str]: + def filename(self) -> str: return self._filename @contextlib.asynccontextmanager @@ -285,6 +453,23 @@ async def stream(self, *, executor: typing.Optional[concurrent.futures.ThreadPoo class WebResource(Resource, abc.ABC): + """Base class for a resource that resides on the internet. + + The logic for identifying this resource is left to each implementation + to define. + + !!! info + For a usable concrete implementation, use `URL` instead. + + !!! note + Some components may choose to not upload this resource directly and + instead simply refer to the URL as needed. The main place this will + occur is within embeds. + + If you need to re-upload the resource, you should download it into + a `Bytes` and pass that instead in these cases. + """ + __slots__ = () @contextlib.asynccontextmanager @@ -346,7 +531,6 @@ async def stream(self, *, executor: typing.Optional[concurrent.futures.ThreadPoo hikari.errors.HTTPErrorResponse If any other unexpected response code is returned. """ - async with aiohttp.ClientSession() as session: async with session.request("get", self.url, raise_for_status=False) as resp: if 200 <= resp.status < 400: @@ -368,7 +552,7 @@ async def stream(self, *, executor: typing.Optional[concurrent.futures.ThreadPoo yield WebReader( stream=resp.content, - uri=str(resp.real_url), + url=str(resp.real_url), status=resp.status, reason=resp.reason, filename=filename, @@ -380,11 +564,27 @@ async def stream(self, *, executor: typing.Optional[concurrent.futures.ThreadPoo await http_client.parse_error_response(resp) -@attr.s(slots=True) class URL(WebResource): - """A URL that represents a web resource.""" + """A URL that represents a web resource. + + Parameters + ---------- + url : str + The URL of the resource. - _url: typing.Union[str] = attr.ib(init=True) + !!! note + Some components may choose to not upload this resource directly and + instead simply refer to the URL as needed. The main place this will + occur is within embeds. + + If you need to re-upload the resource, you should download it into + a `Bytes` and pass that instead in these cases. + """ + + __slots__ = ("_url",) + + def __init__(self, url: str) -> None: + self._url = url @property def url(self) -> str: @@ -396,17 +596,42 @@ def filename(self) -> str: return os.path.basename(url.path) -@attr.s(slots=True) class File(Resource): - path: typing.Union[str, os.PathLike, pathlib.Path] = attr.ib() - _filename: typing.Optional[str] = attr.ib(default=None) + """A resource that exists on the local machine's storage to be uploaded. + + Parameters + ---------- + path : str or os.PathLike or pathlib.Path + The path to use. + + !!! note + If passing a `pathlib.Path`, this must not be a `pathlib.PurePath` + directly, as it will be used to expand tokens such as `~` that + denote the home directory, and `..` for relative paths. + + This will all be performed as required in an executor to prevent + blocking the event loop. + + filename : str or None + The filename to use. If this is `None`, the name of the file is taken + from the path instead. + """ + + __slots__ = ("path", "_filename") + + path: typing.Union[str, os.PathLike, pathlib.Path] + _filename: typing.Optional[str] + + def __init__(self, path: typing.Union[str, os.PathLike, pathlib.Path], filename: typing.Optional[str]) -> None: + self.path = path + self._filename = filename @property def url(self) -> str: - return pathlib.PurePath(self.path).as_uri() + return f"attachment://{self.filename}" @property - def filename(self) -> typing.Optional[str]: + def filename(self) -> str: if self._filename is None: return os.path.basename(self.path) return self._filename diff --git a/hikari/utilities/klass.py b/hikari/utilities/klass.py index 6bcd955bb4..abe8c1e7c2 100644 --- a/hikari/utilities/klass.py +++ b/hikari/utilities/klass.py @@ -107,10 +107,3 @@ class Singleton(abc.ABC, metaclass=SingletonMeta): """ __slots__ = () - - -class Static(type): - """Metaclass that prevents instantiation. Enables the use """ - - def __call__(cls) -> typing.NoReturn: - raise TypeError("This class is static-only, and cannot be instantiated.") From c0632bb6ee68d91824cab9d757daa448c63eb448 Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Thu, 11 Jun 2020 19:21:41 +0100 Subject: [PATCH 496/922] Auto attachment:// --- hikari/impl/entity_factory.py | 27 +++++++++++---------------- 1 file changed, 11 insertions(+), 16 deletions(-) diff --git a/hikari/impl/entity_factory.py b/hikari/impl/entity_factory.py index f7daa6b337..4b4aef337c 100644 --- a/hikari/impl/entity_factory.py +++ b/hikari/impl/entity_factory.py @@ -615,36 +615,33 @@ def serialize_embed( footer_payload["text"] = embed.footer.text if embed.footer.icon is not None: - if isinstance(embed.footer.icon.resource, files.WebResource): - footer_payload["icon_url"] = embed.footer.icon.url - else: - footer_payload["icon_url"] = f"attachment://{embed.footer.icon.filename}" + if not isinstance(embed.footer.icon.resource, files.WebResource): uploads.append(embed.footer.icon) + footer_payload["icon_url"] = embed.footer.icon.url + payload["footer"] = footer_payload if embed.image is not None: image_payload = {} if embed.image is not None: - if isinstance(embed.image.resource, files.WebResource): - image_payload["url"] = embed.image.url - else: - image_payload["url"] = f"attachment://{embed.image.filename}" + if not isinstance(embed.image.resource, files.WebResource): uploads.append(embed.image.resource) + image_payload["url"] = embed.image.url + payload["image"] = image_payload if embed.thumbnail is not None: thumbnail_payload = {} if embed.thumbnail is not None: - if isinstance(embed.thumbnail.resource, files.WebResource): - thumbnail_payload["url"] = embed.thumbnail.url - else: - thumbnail_payload["url"] = f"attachment://{embed.thumbnail.filename}" + if not isinstance(embed.thumbnail.resource, files.WebResource): uploads.append(embed.thumbnail.resource) + thumbnail_payload["url"] = embed.thumbnail.url + payload["thumbnail"] = thumbnail_payload if embed.author is not None: @@ -657,11 +654,9 @@ def serialize_embed( author_payload["url"] = embed.author.url if embed.author.icon is not None: - if isinstance(embed.footer.icon.resource, files.WebResource): - author_payload["icon_url"] = embed.author.icon.url - else: - author_payload["icon_url"] = f"attachment://{embed.author.icon.filename}" + if not isinstance(embed.footer.icon.resource, files.WebResource): uploads.append(embed.author.icon) + author_payload["icon_url"] = embed.author.icon.url payload["author"] = author_payload From a3b956edd6c3ef3535dd3d0b3f2357a4ee433e99 Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Thu, 11 Jun 2020 19:22:31 +0100 Subject: [PATCH 497/922] Added note to errors.py for RateLimited exception. --- hikari/errors.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/hikari/errors.py b/hikari/errors.py index c7061f2fbf..9f94d8b6e6 100644 --- a/hikari/errors.py +++ b/hikari/errors.py @@ -341,9 +341,10 @@ class RateLimited(ClientHTTPErrorResponse): You should not usually see this occur, unless Discord vastly change their ratelimit system without prior warning, which might happen in the future. - If you receive this regularly, please file a bug report, or contact - Discord with the relevant debug information that can be obtained by - enabling debug logs and enabling the debug mode on the REST components. + !!! note + If you receive this regularly, please file a bug report, or contact + Discord with the relevant debug information that can be obtained by + enabling debug logs and enabling the debug mode on the REST components. Parameters ---------- From ae5e85937bf737f1f630588d3619a595304f6d2e Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Thu, 11 Jun 2020 19:24:09 +0100 Subject: [PATCH 498/922] Fixes unused variables at https://lgtm.com/projects/gl/nekokatt/hikari/rev/pr-22194e439fa23eab9f85420b8a29874d0d616c5e --- hikari/net/tracing.py | 1 + 1 file changed, 1 insertion(+) diff --git a/hikari/net/tracing.py b/hikari/net/tracing.py index 62426625a3..bf316c9ea6 100644 --- a/hikari/net/tracing.py +++ b/hikari/net/tracing.py @@ -183,6 +183,7 @@ async def on_request_end(self, _, ctx, params): latency, ctx.identifier, dict(response.headers), + body, ) @typing.no_type_check From 2477ded1194c78b06f391c0c6b711ade5fe1a0c7 Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Thu, 11 Jun 2020 19:25:02 +0100 Subject: [PATCH 499/922] PR feedback for file changes. Removed note from Message#add_reaction per PR feedback. Removed garbage condition in files#generate_filename_from_details Removed garbage condition from klass#get_logger Fixed invalid annotation on errors#RateLimited::__init__ Fixed inconsistent naming. Moved import in hikari.errors to TYPE_CHECKING block. Removed spurious import in hikari.impl.entity_factory. Fixed typo in Message#remove_all_reactions. Changed bases.Unique snowflake field to be factory. Removed spurious else from files.py#Resource.__init__ Fixed docs for executor params in AsyncReader in files.py Reordered none/undefined checks in rest.py --- hikari/errors.py | 4 +- hikari/impl/entity_factory.py | 2 - hikari/models/applications.py | 58 +++++++++-------- hikari/models/bases.py | 2 +- hikari/models/channels.py | 16 +++-- hikari/models/embeds.py | 10 --- hikari/models/guilds.py | 118 ++++++++++++++++++---------------- hikari/models/invites.py | 39 +++++------ hikari/models/messages.py | 10 ++- hikari/models/users.py | 29 +++++---- hikari/models/webhooks.py | 44 ++++++++----- hikari/net/rest.py | 49 +++++++------- hikari/utilities/files.py | 41 ++++++++++-- hikari/utilities/klass.py | 6 +- 14 files changed, 234 insertions(+), 194 deletions(-) diff --git a/hikari/errors.py b/hikari/errors.py index 9f94d8b6e6..d7438262c5 100644 --- a/hikari/errors.py +++ b/hikari/errors.py @@ -39,9 +39,9 @@ import http import typing -from hikari.net import routes if typing.TYPE_CHECKING: + from hikari.net import routes from hikari.utilities import data_binding @@ -368,7 +368,7 @@ def __init__( url: str, route: routes.CompiledRoute, headers: data_binding.Headers, - raw_body: typing.AnyStr, + raw_body: typing.Any, retry_after: float, ) -> None: self.retry_after = retry_after diff --git a/hikari/impl/entity_factory.py b/hikari/impl/entity_factory.py index 4b4aef337c..a58926d5a7 100644 --- a/hikari/impl/entity_factory.py +++ b/hikari/impl/entity_factory.py @@ -36,7 +36,6 @@ from hikari.models import audit_logs as audit_log_models from hikari.models import channels as channel_models from hikari.models import colors as color_models -from hikari.models import embeds from hikari.models import embeds as embed_models from hikari.models import emojis as emoji_models from hikari.models import gateway as gateway_models @@ -500,7 +499,6 @@ def deserialize_channel(self, payload: data_binding.JSONObject) -> channel_model def deserialize_embed(self, payload: data_binding.JSONObject) -> embed_models.Embed: embed = embed_models.Embed() - embed.type = payload["type"] embed.title = payload.get("title") embed.description = payload.get("description") embed.url = payload.get("url") diff --git a/hikari/models/applications.py b/hikari/models/applications.py index d23280997f..288ac915de 100644 --- a/hikari/models/applications.py +++ b/hikari/models/applications.py @@ -39,6 +39,7 @@ from hikari.models import bases from hikari.models import guilds from hikari.utilities import cdn +from hikari.utilities import files if typing.TYPE_CHECKING: from hikari.models import permissions as permissions_ @@ -288,18 +289,18 @@ class Team(bases.Entity, bases.Unique): """The ID of this team's owner.""" @property - def icon_url(self) -> typing.Optional[str]: - """URL for this team's icon. + def icon_url(self) -> typing.Optional[files.URL]: + """Team icon. Returns ------- - str or None + hikari.utilities.files.URL or None The URL, or `None` if no icon exists. """ - return self.format_icon_url() + return self.format_icon() - def format_icon_url(self, *, format_: str = "png", size: int = 4096) -> typing.Optional[str]: - """Generate the icon URL for this team if set. + def format_icon(self, *, format_: str = "png", size: int = 4096) -> typing.Optional[files.URL]: + """Generate the icon for this team if set. Parameters ---------- @@ -312,8 +313,8 @@ def format_icon_url(self, *, format_: str = "png", size: int = 4096) -> typing.O Returns ------- - str or None - The string URL, or `None` if no icon exists. + hikari.utilities.files.URL or None + The URL, or `None` if no icon exists. Raises ------ @@ -322,7 +323,8 @@ def format_icon_url(self, *, format_: str = "png", size: int = 4096) -> typing.O (inclusive). """ if self.icon_hash: - return cdn.generate_cdn_url("team-icons", str(self.id), self.icon_hash, format_=format_, size=size) + url = cdn.generate_cdn_url("team-icons", str(self.id), self.icon_hash, format_=format_, size=size) + return files.URL(url) return None @@ -393,18 +395,18 @@ class Application(bases.Entity, bases.Unique): """The CDN's hash of this application's cover image, used on the store.""" @property - def icon_url(self) -> typing.Optional[str]: - """URL for the team's icon, if there is one. + def icon(self) -> typing.Optional[files.URL]: + """Team icon, if there is one. Returns ------- - str or None - The URL, or `None` if no icon is set. + hikari.utilities.files.URL or None + The URL, or `None` if no icon exists. """ - return self.format_icon_url() + return self.format_icon() - def format_icon_url(self, *, format_: str = "png", size: int = 4096) -> typing.Optional[str]: - """Generate the icon URL for this application. + def format_icon(self, *, format_: str = "png", size: int = 4096) -> typing.Optional[files.URL]: + """Generate the icon for this application. Parameters ---------- @@ -417,8 +419,8 @@ def format_icon_url(self, *, format_: str = "png", size: int = 4096) -> typing.O Returns ------- - str or None - The string URL, or `None` if no icon is set. + hikari.utilities.files.URL or None + The URL, or `None` if no icon exists. Raises ------ @@ -427,22 +429,23 @@ def format_icon_url(self, *, format_: str = "png", size: int = 4096) -> typing.O (inclusive). """ if self.icon_hash: - return cdn.generate_cdn_url("application-icons", str(self.id), self.icon_hash, format_=format_, size=size) + url = cdn.generate_cdn_url("application-icons", str(self.id), self.icon_hash, format_=format_, size=size) + return files.URL(url) return None @property - def cover_image_url(self) -> typing.Optional[str]: - """URL for the cover image used on the store. + def cover_image(self) -> typing.Optional[files.URL]: + """Cover image used on the store. Returns ------- - str or None + hikari.utilities.files.URL or None The URL, or `None` if no cover image exists. """ - return self.format_cover_image_url() + return self.format_cover_image() - def format_cover_image_url(self, *, format_: str = "png", size: int = 4096) -> typing.Optional[str]: - """Generate the URL for this application's store page's cover image is set and applicable. + def format_cover_image(self, *, format_: str = "png", size: int = 4096) -> typing.Optional[files.URL]: + """Generate the cover image used in the store, if set. Parameters ---------- @@ -455,7 +458,7 @@ def format_cover_image_url(self, *, format_: str = "png", size: int = 4096) -> t Returns ------- - str or None + hikari.utilities.files.URL or None The URL, or `None` if no cover image exists. Raises @@ -465,7 +468,8 @@ def format_cover_image_url(self, *, format_: str = "png", size: int = 4096) -> t (inclusive). """ if self.cover_image_hash: - return cdn.generate_cdn_url( + url = cdn.generate_cdn_url( "application-assets", str(self.id), self.cover_image_hash, format_=format_, size=size ) + return files.URL(url) return None diff --git a/hikari/models/bases.py b/hikari/models/bases.py index a33cf5d306..63caaaa71c 100644 --- a/hikari/models/bases.py +++ b/hikari/models/bases.py @@ -61,7 +61,7 @@ class Unique(typing.SupportsInt): """ id: snowflake.Snowflake = attr.ib( - converter=snowflake.Snowflake, eq=True, hash=True, repr=True, default=snowflake.Snowflake(0), + converter=snowflake.Snowflake, eq=True, hash=True, repr=True, factory=snowflake.Snowflake, ) """The ID of this entity.""" diff --git a/hikari/models/channels.py b/hikari/models/channels.py index 854d4d52ec..e372e48c57 100644 --- a/hikari/models/channels.py +++ b/hikari/models/channels.py @@ -47,6 +47,7 @@ from hikari.models import users from hikari.utilities import cdn +from hikari.utilities import files if typing.TYPE_CHECKING: import datetime @@ -198,13 +199,13 @@ class GroupDMChannel(DMChannel): """ @property - def icon_url(self) -> typing.Optional[str]: - """URL for this DM channel's icon, if set.""" + def icon(self) -> typing.Optional[files.URL]: + """Icon for this DM channel, if set.""" return self.format_icon_url() # noinspection PyShadowingBuiltins - def format_icon_url(self, *, format: str = "png", size: int = 4096) -> typing.Optional[str]: - """Generate the URL for this group DM's icon, if set. + def format_icon(self, *, format: str = "png", size: int = 4096) -> typing.Optional[files.URL]: + """Generate the icon for this DM, if set. Parameters ---------- @@ -217,8 +218,8 @@ def format_icon_url(self, *, format: str = "png", size: int = 4096) -> typing.Op Returns ------- - str or None - The string URL, or `None` if no icon is present. + hikari.utilities.files.URL or None + The URL, or `None` if no icon is present. Raises ------ @@ -226,7 +227,8 @@ def format_icon_url(self, *, format: str = "png", size: int = 4096) -> typing.Op If `size` is not a power of two between 16 and 4096 (inclusive). """ if self.icon_hash: - return cdn.generate_cdn_url("channel-icons", str(self.id), self.icon_hash, format_=format, size=size) + url = cdn.generate_cdn_url("channel-icons", str(self.id), self.icon_hash, format_=format, size=size) + return files.URL(url) return None diff --git a/hikari/models/embeds.py b/hikari/models/embeds.py index ceee925b37..e3d906c6d0 100644 --- a/hikari/models/embeds.py +++ b/hikari/models/embeds.py @@ -179,16 +179,6 @@ class EmbedField: class Embed: """Represents an embed.""" - type: str = attr.ib(default="rich", repr=True) - """The type of the embed. - - Defaults to `"rich"`. - - !!! note - You can only specify `"rich"` when creating a new embed. Any other - value will be ignored. - """ - color: typing.Optional[colors.Color] = attr.ib( default=None, repr=False, converter=lambda c: colors.Color.of(c) if c is not None else None, ) diff --git a/hikari/models/guilds.py b/hikari/models/guilds.py index 894387b99c..aeeb36dfce 100644 --- a/hikari/models/guilds.py +++ b/hikari/models/guilds.py @@ -51,6 +51,7 @@ from hikari.models import bases from hikari.models import users from hikari.utilities import cdn +from hikari.utilities import files if typing.TYPE_CHECKING: import datetime @@ -417,23 +418,31 @@ class PartialGuild(bases.Entity, bases.Unique): features: typing.Set[typing.Union[GuildFeature, str]] = attr.ib(eq=False, hash=False, repr=False) """A set of the features in this guild.""" - def format_icon_url(self, *, format_: typing.Optional[str] = None, size: int = 4096) -> typing.Optional[str]: - """Generate the URL for this guild's custom icon, if set. + @property + def icon_url(self) -> typing.Optional[files.URL]: + """Icon for the guild, if set; otherwise `None`.""" + return self.format_icon() + + def format_icon(self, *, format_: typing.Optional[str] = None, size: int = 4096) -> typing.Optional[files.URL]: + """Generate the guild's icon, if set. Parameters ---------- - format_ : str + format_ : str or None The format to use for this URL, defaults to `png` or `gif`. Supports `png`, `jpeg`, `jpg`, `webp` and `gif` (when animated). + + If `None`, then the correct default format is determined based on + whether the icon is animated or not. size : int The size to set for the URL, defaults to `4096`. Can be any power of two between 16 and 4096. Returns ------- - str or None - The string URL. + hikari.utilities.files.URL or None + The URL to the resource, or `None` if no icon is set. Raises ------ @@ -441,16 +450,10 @@ def format_icon_url(self, *, format_: typing.Optional[str] = None, size: int = 4 If `size` is not a power of two or not between 16 and 4096. """ if self.icon_hash: - if format_ is None: - format_ = "gif" if self.icon_hash.startswith("a_") else "png" - return cdn.generate_cdn_url("icons", str(self.id), self.icon_hash, format_=format_, size=size) + url = cdn.generate_cdn_url("icons", str(self.id), self.icon_hash, format_=format_, size=size) + return files.URL(url) return None - @property - def icon_url(self) -> typing.Optional[str]: - """URL for this guild's icon, if set.""" - return self.format_icon_url() - @attr.s(eq=True, hash=True, init=False, kw_only=True, slots=True) class GuildPreview(PartialGuild): @@ -474,8 +477,13 @@ class GuildPreview(PartialGuild): description: typing.Optional[str] = attr.ib(eq=False, hash=False, repr=False) """The guild's description, if set.""" - def format_splash_url(self, *, format_: str = "png", size: int = 4096) -> typing.Optional[str]: - """Generate the URL for this guild's splash image, if set. + @property + def splash_url(self) -> typing.Optional[files.URL]: + """Splash for the guild, if set.""" + return self.format_splash() + + def format_splash(self, *, format_: str = "png", size: int = 4096) -> typing.Optional[files.URL]: + """Generate the guild's splash image, if set. Parameters ---------- @@ -488,8 +496,8 @@ def format_splash_url(self, *, format_: str = "png", size: int = 4096) -> typing Returns ------- - str or None - The string URL. + hikari.utilities.files.URL or None + The URL to the splash, or `None` if not set. Raises ------ @@ -497,16 +505,17 @@ def format_splash_url(self, *, format_: str = "png", size: int = 4096) -> typing If `size` is not a power of two or not between 16 and 4096. """ if self.splash_hash: - return cdn.generate_cdn_url("splashes", str(self.id), self.splash_hash, format_=format_, size=size) + url = cdn.generate_cdn_url("splashes", str(self.id), self.splash_hash, format_=format_, size=size) + return files.URL(url) return None @property - def splash_url(self) -> typing.Optional[str]: - """URL for this guild's splash, if set.""" - return self.format_splash_url() + def discovery_splash(self) -> typing.Optional[files.URL]: + """Discovery splash for the guild, if set.""" + return self.format_discovery_splash() - def format_discovery_splash_url(self, *, format_: str = "png", size: int = 4096) -> typing.Optional[str]: - """Generate the URL for this guild's discovery splash image, if set. + def format_discovery_splash(self, *, format_: str = "png", size: int = 4096) -> typing.Optional[files.URL]: + """Generate the guild's discovery splash image, if set. Parameters ---------- @@ -519,7 +528,7 @@ def format_discovery_splash_url(self, *, format_: str = "png", size: int = 4096) Returns ------- - str or None + hikari.utilities.files.URL or None The string URL. Raises @@ -528,16 +537,12 @@ def format_discovery_splash_url(self, *, format_: str = "png", size: int = 4096) If `size` is not a power of two or not between 16 and 4096. """ if self.discovery_splash_hash: - return cdn.generate_cdn_url( + url = cdn.generate_cdn_url( "discovery-splashes", str(self.id), self.discovery_splash_hash, format_=format_, size=size ) + return files.URL(url) return None - @property - def discovery_splash_url(self) -> typing.Optional[str]: - """URL for this guild's discovery splash, if set.""" - return self.format_discovery_splash_url() - @attr.s(eq=True, hash=True, init=False, kw_only=True, slots=True) class Guild(PartialGuild): # pylint:disable=too-many-instance-attributes @@ -833,8 +838,13 @@ class Guild(PartialGuild): # pylint:disable=too-many-instance-attributes remain `None`. """ - def format_splash_url(self, *, format_: str = "png", size: int = 4096) -> typing.Optional[str]: - """Generate the URL for this guild's splash image, if set. + @property + def splash_url(self) -> typing.Optional[files.URL]: + """Splash for the guild, if set.""" + return self.format_splash() + + def format_splash(self, *, format_: str = "png", size: int = 4096) -> typing.Optional[files.URL]: + """Generate the guild's splash image, if set. Parameters ---------- @@ -847,8 +857,8 @@ def format_splash_url(self, *, format_: str = "png", size: int = 4096) -> typing Returns ------- - str or None - The string URL. + hikari.utilities.files.URL or None + The URL to the splash, or `None` if not set. Raises ------ @@ -856,16 +866,17 @@ def format_splash_url(self, *, format_: str = "png", size: int = 4096) -> typing If `size` is not a power of two or not between 16 and 4096. """ if self.splash_hash: - return cdn.generate_cdn_url("splashes", str(self.id), self.splash_hash, format_=format_, size=size) + url = cdn.generate_cdn_url("splashes", str(self.id), self.splash_hash, format_=format_, size=size) + return files.URL(url) return None @property - def splash_url(self) -> typing.Optional[str]: - """URL for this guild's splash, if set.""" - return self.format_splash_url() + def discovery_splash(self) -> typing.Optional[files.URL]: + """Discovery splash for the guild, if set.""" + return self.format_discovery_splash() - def format_discovery_splash_url(self, *, format_: str = "png", size: int = 4096) -> typing.Optional[str]: - """Generate the URL for this guild's discovery splash image, if set. + def format_discovery_splash(self, *, format_: str = "png", size: int = 4096) -> typing.Optional[files.URL]: + """Generate the guild's discovery splash image, if set. Parameters ---------- @@ -878,7 +889,7 @@ def format_discovery_splash_url(self, *, format_: str = "png", size: int = 4096) Returns ------- - str or None + hikari.utilities.files.URL or None The string URL. Raises @@ -887,18 +898,19 @@ def format_discovery_splash_url(self, *, format_: str = "png", size: int = 4096) If `size` is not a power of two or not between 16 and 4096. """ if self.discovery_splash_hash: - return cdn.generate_cdn_url( + url = cdn.generate_cdn_url( "discovery-splashes", str(self.id), self.discovery_splash_hash, format_=format_, size=size ) + return files.URL(url) return None @property - def discovery_splash_url(self) -> typing.Optional[str]: - """URL for this guild's discovery splash, if set.""" - return self.format_discovery_splash_url() + def banner(self) -> typing.Optional[files.URL]: + """Banner for the guild, if set.""" + return self.format_banner() - def format_banner_url(self, *, format_: str = "png", size: int = 4096) -> typing.Optional[str]: - """Generate the URL for this guild's banner image, if set. + def format_banner(self, *, format_: str = "png", size: int = 4096) -> typing.Optional[files.URL]: + """Generate the guild's banner image, if set. Parameters ---------- @@ -911,8 +923,8 @@ def format_banner_url(self, *, format_: str = "png", size: int = 4096) -> typing Returns ------- - str or None - The string URL. + hikari.utilities.files.URL or None + The URL of the banner, or `None` if no banner is set. Raises ------ @@ -920,10 +932,6 @@ def format_banner_url(self, *, format_: str = "png", size: int = 4096) -> typing If `size` is not a power of two or not between 16 and 4096. """ if self.banner_hash: - return cdn.generate_cdn_url("banners", str(self.id), self.banner_hash, format_=format_, size=size) + url = cdn.generate_cdn_url("banners", str(self.id), self.banner_hash, format_=format_, size=size) + return files.URL(url) return None - - @property - def banner_url(self) -> typing.Optional[str]: - """URL for this guild's banner, if set.""" - return self.format_banner_url() diff --git a/hikari/models/invites.py b/hikari/models/invites.py index c5b64baa30..d5b4379dad 100644 --- a/hikari/models/invites.py +++ b/hikari/models/invites.py @@ -30,6 +30,7 @@ from hikari.models import bases from hikari.models import guilds from hikari.utilities import cdn +from hikari.utilities import files if typing.TYPE_CHECKING: import datetime @@ -89,8 +90,13 @@ class InviteGuild(guilds.PartialGuild): `features` for this guild. If not, this will always be `None`. """ - def format_splash_url(self, *, format_: str = "png", size: int = 4096) -> typing.Optional[str]: - """Generate the URL for this guild's splash, if set. + @property + def splash_url(self) -> typing.Optional[files.URL]: + """Splash for the guild, if set.""" + return self.format_splash() + + def format_splash(self, *, format_: str = "png", size: int = 4096) -> typing.Optional[files.URL]: + """Generate the guild's splash image, if set. Parameters ---------- @@ -103,8 +109,8 @@ def format_splash_url(self, *, format_: str = "png", size: int = 4096) -> typing Returns ------- - str or None - The string URL. + hikari.utilities.files.URL or None + The URL to the splash, or `None` if not set. Raises ------ @@ -112,16 +118,17 @@ def format_splash_url(self, *, format_: str = "png", size: int = 4096) -> typing If `size` is not a power of two or not between 16 and 4096. """ if self.splash_hash: - return cdn.generate_cdn_url("splashes", str(self.id), self.splash_hash, format_=format_, size=size) + url = cdn.generate_cdn_url("splashes", str(self.id), self.splash_hash, format_=format_, size=size) + return files.URL(url) return None @property - def splash_url(self) -> typing.Optional[str]: - """URL for this guild's splash, if set.""" - return self.format_splash_url() + def banner(self) -> typing.Optional[files.URL]: + """Banner for the guild, if set.""" + return self.format_banner() - def format_banner_url(self, *, format_: str = "png", size: int = 4096) -> typing.Optional[str]: - """Generate the URL for this guild's banner, if set. + def format_banner(self, *, format_: str = "png", size: int = 4096) -> typing.Optional[files.URL]: + """Generate the guild's banner image, if set. Parameters ---------- @@ -134,8 +141,8 @@ def format_banner_url(self, *, format_: str = "png", size: int = 4096) -> typing Returns ------- - str or None - The string URL. + hikari.utilities.files.URL or None + The URL of the banner, or `None` if no banner is set. Raises ------ @@ -143,14 +150,10 @@ def format_banner_url(self, *, format_: str = "png", size: int = 4096) -> typing If `size` is not a power of two or not between 16 and 4096. """ if self.banner_hash: - return cdn.generate_cdn_url("banners", str(self.id), self.banner_hash, format_=format_, size=size) + url = cdn.generate_cdn_url("banners", str(self.id), self.banner_hash, format_=format_, size=size) + return files.URL(url) return None - @property - def banner_url(self) -> typing.Optional[str]: - """URL for this guild's banner, if set.""" - return self.format_banner_url() - @attr.s(eq=True, hash=True, init=False, kw_only=True, slots=True) class Invite(bases.Entity): diff --git a/hikari/models/messages.py b/hikari/models/messages.py index aded2f4b3a..22594bde4e 100644 --- a/hikari/models/messages.py +++ b/hikari/models/messages.py @@ -34,7 +34,6 @@ import enum import typing -import aiohttp import attr from hikari.models import bases @@ -522,10 +521,9 @@ async def add_reaction(self, emoji: typing.Union[str, emojis_.Emoji]) -> None: If the channel or message is not found, or if the emoji is not found. - !!! note - This will also occur if you try to add an emoji from a - guild you are not part of if no one else has previously - reacted with the same emoji. + This will also occur if you try to add an emoji from a + guild you are not part of if no one else has previously + reacted with the same emoji. """ await self._app.rest.add_reaction(channel=self.channel_id, message=self.id, emoji=emoji) @@ -587,7 +585,7 @@ async def remove_all_reactions( Parameters ---------- - emoji : str hikari.models.emojis.Emoji or hikari.utilities.undefined.Undefined + emoji : str or hikari.models.emojis.Emoji or hikari.utilities.undefined.Undefined The emoji to remove all reactions for. If not specified, then all emojis are removed. diff --git a/hikari/models/users.py b/hikari/models/users.py index fa1a6ef16f..2936f374ae 100644 --- a/hikari/models/users.py +++ b/hikari/models/users.py @@ -164,13 +164,11 @@ async def fetch_self(self) -> User: @property def avatar(self) -> typing.Optional[files.URL]: - """URL for this user's custom avatar if set, else `None`.""" - return self.format_avatar_url() + """Avatar for the user if set, else `None`.""" + return self.format_avatar() - def format_avatar_url( - self, *, format_: typing.Optional[str] = None, size: int = 4096 - ) -> typing.Optional[files.URL]: - """Generate the avatar URL for this user's custom avatar if set. + def format_avatar(self, *, format_: typing.Optional[str] = None, size: int = 4096) -> typing.Optional[files.URL]: + """Generate the avatar for this user, if set. If no custom avatar is set, this returns `None`. You can then use the `User.default_avatar_url` attribute instead to fetch the displayed @@ -178,11 +176,14 @@ def format_avatar_url( Parameters ---------- - format_ : str + format_ : str or `None` The format to use for this URL, defaults to `png` or `gif`. Supports `png`, `jpeg`, `jpg`, `webp` and `gif` (when animated). Will be ignored for default avatars which can only be `png`. + + If `None`, then the correct default format is determined based on + whether the icon is animated or not. size : int The size to set for the URL, defaults to `4096`. Can be any power of two between 16 and 4096. @@ -191,7 +192,7 @@ def format_avatar_url( Returns ------- hikari.utilities.files.URL - The string URL, or `None` if not present. + The URL to the avatar, or `None` if not present. Raises ------ @@ -203,17 +204,17 @@ def format_avatar_url( return files.URL(url) return None + @property + def default_avatar(self) -> files.URL: # noqa: D401 imperative mood check + """Placeholder default avatar for the user.""" + url = cdn.get_default_avatar_url(self.discriminator) + return files.URL(url) + @property def default_avatar_index(self) -> int: """Integer representation of this user's default avatar.""" return cdn.get_default_avatar_index(self.discriminator) - @property - def default_avatar(self) -> files.URL: - """URL for this user's default avatar.""" - url = cdn.get_default_avatar_url(self.discriminator) - return files.URL(url) - @attr.s(eq=True, hash=True, init=False, kw_only=True, slots=True) class OwnUser(User): diff --git a/hikari/models/webhooks.py b/hikari/models/webhooks.py index 79ec4fb16f..1289d98b24 100644 --- a/hikari/models/webhooks.py +++ b/hikari/models/webhooks.py @@ -29,13 +29,13 @@ from hikari.models import bases from hikari.utilities import cdn +from hikari.utilities import files as files_ from hikari.utilities import snowflake from hikari.utilities import undefined if typing.TYPE_CHECKING: from hikari.models import channels as channels_ from hikari.models import embeds as embeds_ - from hikari.models import files as files_ from hikari.models import guilds as guilds_ from hikari.models import messages as messages_ from hikari.models import users as users_ @@ -99,7 +99,7 @@ async def execute( username: typing.Union[undefined.Undefined, str] = undefined.Undefined(), avatar_url: typing.Union[undefined.Undefined, str] = undefined.Undefined(), tts: typing.Union[undefined.Undefined, bool] = undefined.Undefined(), - attachments: typing.Union[undefined.Undefined, typing.Sequence[files_.BaseStream]] = undefined.Undefined(), + attachments: typing.Union[undefined.Undefined, typing.Sequence[files_.Resource]] = undefined.Undefined(), embeds: typing.Union[undefined.Undefined, typing.Sequence[embeds_.Embed]] = undefined.Undefined(), mentions_everyone: bool = True, user_mentions: typing.Union[ @@ -123,7 +123,7 @@ async def execute( avatar with for this request. tts : bool or hikari.utilities.undefined.Undefined If specified, whether the message will be sent as a TTS message. - attachments : typing.Sequence[hikari.models.files.BaseStream] or hikari.utilities.undefined.Undefined + attachments : typing.Sequence[hikari.utilities.files.BaseStream] or hikari.utilities.undefined.Undefined If specified, a sequence of attachments to upload. embeds : typing.Sequence[hikari.models.embeds.Embed] or hikari.utilities.undefined.Undefined If specified, a sequence of between `1` to `10` embed objects @@ -216,7 +216,7 @@ async def edit( self, *, name: typing.Union[undefined.Undefined, str] = undefined.Undefined(), - avatar: typing.Union[undefined.Undefined, None, files_.BaseStream] = undefined.Undefined(), + avatar: typing.Union[undefined.Undefined, None, files_.Resource] = undefined.Undefined(), channel: typing.Union[undefined.Undefined, bases.UniqueObject, channels_.GuildChannel] = undefined.Undefined(), reason: typing.Union[undefined.Undefined, str] = undefined.Undefined(), use_token: typing.Union[undefined.Undefined, bool] = undefined.Undefined(), @@ -227,10 +227,10 @@ async def edit( ---------- name : str or hikari.utilities.undefined.Undefined If specified, the new name string. - avatar : hikari.models.files.BaseStream or None or hikari.utilities.undefined.Undefined + avatar : hikari.utilities.files.Resource or None or hikari.utilities.undefined.Undefined If specified, the new avatar image. If `None`, then - it is removed. - channel : hikari.utilities.undefined.Undefined or hikari.models.channels.GuildChannel or hikari.models.bases.Unique or hikari.utilities.snowflake.Snowflake or str or int + it is removed. If not specified, nothing is changed. + channel : hikari.models.channels.GuildChannel or hikari.models.bases.UniqueObject or hikari.utilities.undefined.Undefined If specified, the object or ID of the new channel the given webhook should be moved to. reason : str or hikari.utilities.undefined.Undefined @@ -340,9 +340,16 @@ async def fetch_self( return await self._app.rest.fetch_webhook(self.id, token=token) @property - def avatar_url(self) -> str: - """URL for this webhook's custom avatar if set, else default.""" - return self.format_avatar_url() + def avatar(self) -> files_.URL: + """URL for this webhook's custom avatar or default avatar. + + If the webhook has a custom avatar, a URL to this is returned. Otherwise + a URL to the default avatar is provided instead. + """ + url = self.format_avatar() + if url is None: + return self.default_avatar + return url @property def default_avatar_index(self) -> int: @@ -350,14 +357,15 @@ def default_avatar_index(self) -> int: return 0 @property - def default_avatar_url(self) -> str: + def default_avatar(self) -> files_.URL: """URL for this webhook's default avatar. This is used if no avatar is set. """ - return cdn.generate_cdn_url("embed", "avatars", str(self.default_avatar_index), format_="png", size=None) + url = cdn.generate_cdn_url("embed", "avatars", str(self.default_avatar_index), format_="png", size=None) + return files_.URL(url) - def format_avatar_url(self, format_: str = "png", size: int = 4096) -> str: + def format_avatar(self, format_: str = "png", size: int = 4096) -> typing.Optional[files_.URL]: """Generate the avatar URL for this webhook's custom avatar if set, else it's default avatar. Parameters @@ -373,8 +381,9 @@ def format_avatar_url(self, format_: str = "png", size: int = 4096) -> str: Returns ------- - str - The string URL. + hikari.utilities.files.URL or None + The URL of the resource. `None` if no avatar is set (in this case, + use the `default_avatar` instead). Raises ------ @@ -382,5 +391,6 @@ def format_avatar_url(self, format_: str = "png", size: int = 4096) -> str: If `size` is not a power of two between 16 and 4096 (inclusive). """ if not self.avatar_hash: - return self.default_avatar_url - return cdn.generate_cdn_url("avatars", str(self.id), self.avatar_hash, format_=format_, size=size) + url = cdn.generate_cdn_url("avatars", str(self.id), self.avatar_hash, format_=format_, size=size) + return files_.URL(url) + return None diff --git a/hikari/net/rest.py b/hikari/net/rest.py index 6ddf3a741c..c7577acbad 100644 --- a/hikari/net/rest.py +++ b/hikari/net/rest.py @@ -1445,12 +1445,12 @@ async def edit_webhook( body = data_binding.JSONObjectBuilder() body.put("name", name) body.put_snowflake("channel", channel) - if not isinstance(avatar, undefined.Undefined): - if avatar is None: - body.put("avatar", None) - else: - async with avatar.stream(executor=self._app.thread_pool_executor) as stream: - body.put("avatar", await stream.data_uri()) + + if avatar is None: + body.put("avatar", None) + elif not isinstance(avatar, undefined.Undefined): + async with avatar.stream(executor=self._app.thread_pool_executor) as stream: + body.put("avatar", await stream.data_uri()) raw_response = await self._request(route, body=body, reason=reason) response = typing.cast(data_binding.JSONObject, raw_response) @@ -1824,26 +1824,23 @@ async def edit_guild( # FIXME: gather these futures simultaneously for a 3x speedup... - if not isinstance(icon, undefined.Undefined): - if icon is None: - body.put("icon", None) - else: - async with icon.stream(executor=self._app.thread_pool_executor) as stream: - body.put("icon", await stream.data_uri()) - - if not isinstance(splash, undefined.Undefined): - if splash is None: - body.put("splash", None) - else: - async with splash.stream(executor=self._app.thread_pool_executor) as stream: - body.put("splash", await stream.data_uri()) - - if not isinstance(banner, undefined.Undefined): - if banner is None: - body.put("banner", None) - else: - async with banner.stream(executor=self._app.thread_pool_executor) as stream: - body.put("banner", await stream.data_uri()) + if icon is None: + body.put("icon", None) + elif not isinstance(icon, undefined.Undefined): + async with icon.stream(executor=self._app.thread_pool_executor) as stream: + body.put("icon", await stream.data_uri()) + + if splash is None: + body.put("splash", None) + elif not isinstance(splash, undefined.Undefined): + async with splash.stream(executor=self._app.thread_pool_executor) as stream: + body.put("splash", await stream.data_uri()) + + if banner is None: + body.put("banner", None) + elif not isinstance(banner, undefined.Undefined): + async with banner.stream(executor=self._app.thread_pool_executor) as stream: + body.put("banner", await stream.data_uri()) raw_response = await self._request(route, body=body, reason=reason) response = typing.cast(data_binding.JSONObject, raw_response) diff --git a/hikari/utilities/files.py b/hikari/utilities/files.py index ca5d9b9383..f67d77e94a 100644 --- a/hikari/utilities/files.py +++ b/hikari/utilities/files.py @@ -23,9 +23,13 @@ __all__ = [ "ensure_resource", "AsyncReader", + "ByteReader", + "FileReader", + "WebReader", "Resource", "Bytes", "File", + "WebResource", "URL", ] @@ -194,7 +198,7 @@ def generate_filename_from_details( if extension is None and mimetype is not None: extension = guess_file_extension(mimetype) - if extension is None or extension == "": + if not extension: extension = "" elif not extension.startswith("."): extension = f".{extension}" @@ -432,8 +436,7 @@ def __init__( if filename is None and mimetype is None: if extension is None: raise TypeError("Cannot infer data type details, please specify one of filetype, mimetype, extension") - else: - raise TypeError("Cannot infer data type details from extension. Please specify mimetype or filename") + raise TypeError("Cannot infer data type details from extension. Please specify mimetype or filename") self._filename = filename self.mimetype = mimetype @@ -448,7 +451,19 @@ def filename(self) -> str: return self._filename @contextlib.asynccontextmanager - async def stream(self, *, executor: typing.Optional[concurrent.futures.ThreadPoolExecutor] = None) -> AsyncReader: + async def stream(self, *, executor: typing.Optional[concurrent.futures.ThreadPoolExecutor] = None) -> ByteReader: + """Start streaming the content in chunks. + + Parameters + ---------- + executor : concurrent.futures.ThreadPoolExecutor or None + Not used. Provided only to match the underlying interface.executor + + Returns + ------- + ByteReader + The byte stream. + """ yield ByteReader(self.filename, self.mimetype, self.data) @@ -481,7 +496,7 @@ async def stream(self, *, executor: typing.Optional[concurrent.futures.ThreadPoo Parameters ---------- - executor : + executor : concurrent.futures.ThreadPoolExecutor or None Not used. Provided only to match the underlying interface. Examples @@ -637,5 +652,19 @@ def filename(self) -> str: return self._filename @contextlib.asynccontextmanager - async def stream(self, *, executor: typing.Optional[concurrent.futures.ThreadPoolExecutor] = None) -> AsyncReader: + async def stream(self, *, executor: typing.Optional[concurrent.futures.ThreadPoolExecutor] = None) -> FileReader: + """Start streaming the resource using a thread pool executor. + + Parameters + ---------- + executor : typing.Optional[concurrent.futures.ThreadPoolExecutor] + The thread pool to run the blocking read operations in. If + `None`, the default executor for the running event loop will be + used instead. + + Returns + ------- + FileReader + The file reader object. + """ yield FileReader(self.filename, None, executor, self.path) diff --git a/hikari/utilities/klass.py b/hikari/utilities/klass.py index abe8c1e7c2..81c9d2bfca 100644 --- a/hikari/utilities/klass.py +++ b/hikari/utilities/klass.py @@ -46,9 +46,9 @@ def get_logger(obj: typing.Union[typing.Type[typing.Any], typing.Any], *addition """ if isinstance(obj, str): return logging.getLogger(obj) - else: - obj = obj if isinstance(obj, type) else type(obj) - return logging.getLogger(".".join((obj.__module__, obj.__qualname__, *additional_args))) + + obj = obj if isinstance(obj, type) else type(obj) + return logging.getLogger(".".join((obj.__module__, obj.__qualname__, *additional_args))) class SingletonMeta(abc.ABCMeta): From a03d9b7ece9ccbe0322a511f53a6e069f7dfea42 Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Thu, 11 Jun 2020 20:41:32 +0100 Subject: [PATCH 500/922] Adjusted checking ratelimits for unbucketed unglobal ratelimits that occur on top of bucketed ones. Removed useless comment. --- hikari/impl/entity_factory.py | 1 - hikari/net/rest.py | 10 +++++++--- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/hikari/impl/entity_factory.py b/hikari/impl/entity_factory.py index a58926d5a7..cfceccf2d2 100644 --- a/hikari/impl/entity_factory.py +++ b/hikari/impl/entity_factory.py @@ -507,7 +507,6 @@ def deserialize_embed(self, payload: data_binding.JSONObject) -> embed_models.Em ) embed.color = color_models.Color(payload["color"]) if "color" in payload else None - # TODO: @FasterSpeeding, can we use `None` here instead to keep MyPy happy? if (footer_payload := payload.get("footer", ...)) is not ...: footer = embed_models.EmbedFooter() footer.text = footer_payload["text"] diff --git a/hikari/net/rest.py b/hikari/net/rest.py index c7577acbad..fe5a34f627 100644 --- a/hikari/net/rest.py +++ b/hikari/net/rest.py @@ -26,6 +26,7 @@ import contextlib import datetime import http +import math import typing import aiohttp @@ -317,11 +318,14 @@ async def _parse_ratelimits(self, compiled_route: routes.CompiledRoute, response compiled_route, reset_date, ) + + # If the values are within 20% of eachother by relativistic tolerance, it is probably + # safe to retry the request, as they are likely the same value just with some + # measuring difference. 20% was used as a rounded figure. + if math.isclose(body_retry_after, (reset_date - now_date).total_seconds(), rel_tol=0.20): raise self._RetryRequest() - raise errors.RateLimited( - str(response.real_url), compiled_route, response.headers, body, body_retry_after, - ) + raise errors.RateLimited(str(response.real_url), compiled_route, response.headers, body, body_retry_after) @staticmethod def _generate_allowed_mentions( From 0edf4e654db2f2bbb0f248f718337649b87fe56d Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Thu, 11 Jun 2020 21:15:28 +0100 Subject: [PATCH 501/922] Changed ratelimit logic to use X-RateLimit-Reset-After for comparisons. --- hikari/net/rest.py | 3 ++- hikari/net/strings.py | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/hikari/net/rest.py b/hikari/net/rest.py index fe5a34f627..ea51bf0c1e 100644 --- a/hikari/net/rest.py +++ b/hikari/net/rest.py @@ -254,6 +254,7 @@ async def _parse_ratelimits(self, compiled_route: routes.CompiledRoute, response remaining = int(resp_headers.get(strings.X_RATELIMIT_REMAINING_HEADER, "1")) bucket = resp_headers.get(strings.X_RATELIMIT_BUCKET_HEADER, "None") reset_at = float(resp_headers.get(strings.X_RATELIMIT_RESET_HEADER, "0")) + reset_after = float(resp_headers.get(strings.X_RATELIMIT_RESET_AFTER_HEADER, "0")) reset_date = datetime.datetime.fromtimestamp(reset_at, tz=datetime.timezone.utc) now_date = date.rfc7231_datetime_string_to_datetime(resp_headers[strings.DATE_HEADER]) @@ -322,7 +323,7 @@ async def _parse_ratelimits(self, compiled_route: routes.CompiledRoute, response # If the values are within 20% of eachother by relativistic tolerance, it is probably # safe to retry the request, as they are likely the same value just with some # measuring difference. 20% was used as a rounded figure. - if math.isclose(body_retry_after, (reset_date - now_date).total_seconds(), rel_tol=0.20): + if math.isclose(body_retry_after, reset_after, rel_tol=0.20): raise self._RetryRequest() raise errors.RateLimited(str(response.real_url), compiled_route, response.headers, body, body_retry_after) diff --git a/hikari/net/strings.py b/hikari/net/strings.py index 40e15433e8..7de4025c3c 100644 --- a/hikari/net/strings.py +++ b/hikari/net/strings.py @@ -43,6 +43,7 @@ X_RATELIMIT_PRECISION_HEADER: typing.Final[str] = "X-RateLimit-Precision" X_RATELIMIT_REMAINING_HEADER: typing.Final[str] = "X-RateLimit-Remaining" X_RATELIMIT_RESET_HEADER: typing.Final[str] = "X-RateLimit-Reset" +X_RATELIMIT_RESET_AFTER_HEADER: typing.Final[str] = "X-RateLimit-Reset-After" # Mimetypes. APPLICATION_JSON: typing.Final[str] = "application/json" From e5274ebe42b749682a5961384de3d80267cdf83d Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Thu, 11 Jun 2020 21:17:04 +0100 Subject: [PATCH 502/922] Added retry for global ratelimits --- hikari/net/rest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hikari/net/rest.py b/hikari/net/rest.py index ea51bf0c1e..878b4672ed 100644 --- a/hikari/net/rest.py +++ b/hikari/net/rest.py @@ -291,7 +291,7 @@ async def _parse_ratelimits(self, compiled_route: routes.CompiledRoute, response self.global_rate_limit.throttle(body_retry_after) self.logger.warning("you are being rate-limited globally - trying again after %ss", body_retry_after) - return + raise self._RetryRequest() # Discord have started applying ratelimits to operations on some endpoints # based on specific fields used in the JSON body. From ab8e1b2acd31cb0579fa0ab11a11a59f58a64a60 Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Thu, 11 Jun 2020 22:26:58 +0100 Subject: [PATCH 503/922] More MyPy fixes. --- hikari/impl/gateway_zookeeper.py | 21 ++++++++---------- hikari/models/applications.py | 4 ++-- hikari/models/channels.py | 2 +- hikari/models/embeds.py | 34 +++++++++++++++++++++------- hikari/models/guilds.py | 16 +++++++++----- hikari/models/invites.py | 6 ++--- hikari/models/users.py | 8 ++++++- hikari/models/webhooks.py | 2 +- hikari/net/http_client.py | 14 ++++++------ hikari/net/rest.py | 31 ++++++++++++++------------ hikari/net/rest_utils.py | 9 +++++--- hikari/utilities/cdn.py | 38 -------------------------------- hikari/utilities/files.py | 34 ++++++++++++++-------------- hikari/utilities/klass.py | 4 ++-- 14 files changed, 108 insertions(+), 115 deletions(-) diff --git a/hikari/impl/gateway_zookeeper.py b/hikari/impl/gateway_zookeeper.py index c47d025a5f..a2ed4cf0f7 100644 --- a/hikari/impl/gateway_zookeeper.py +++ b/hikari/impl/gateway_zookeeper.py @@ -155,7 +155,7 @@ def __init__( self._aiohttp_config = config self._debug = debug - self._gather_task = None + self._gather_task: typing.Optional[asyncio.Task[None]] = None self._initial_activity = initial_activity self._initial_idle_since = initial_idle_since self._initial_is_afk = initial_is_afk @@ -166,8 +166,8 @@ def __init__( self._request_close_event = asyncio.Event() self._shard_count = shard_count self._shard_ids = shard_ids - self._shards = {} - self._tasks = {} + self._shards: typing.Dict[int, gateway.Gateway] = {} + self._tasks: typing.Dict[int, asyncio.Task[typing.Any]] = {} self._token = token self._use_compression = compression self._version = version @@ -206,8 +206,9 @@ async def start(self) -> None: self._tasks.values(), timeout=5, return_when=asyncio.FIRST_COMPLETED ) - if completed: - raise completed.pop().exception() + while completed: + if (ex := completed.pop().exception()) is not None: + raise ex window = {} for shard_id in shard_ids: @@ -251,12 +252,8 @@ async def close(self) -> None: self.logger.info("stopping %s shard(s)", len(self._tasks)) - has_event_dispatcher = hasattr(self, "event_dispatcher") and isinstance( - self.event_dispatcher, event_dispatcher.IEventDispatcher - ) - try: - if has_event_dispatcher: + if isinstance(self, app_.IGatewayDispatcher): # noinspection PyUnresolvedReferences await self.event_dispatcher.dispatch(other.StoppingEvent()) @@ -264,14 +261,14 @@ async def close(self) -> None: finally: self._tasks.clear() - if has_event_dispatcher: + if isinstance(self, app_.IGatewayDispatcher): # noinspection PyUnresolvedReferences await self.event_dispatcher.dispatch(other.StoppedEvent()) def run(self) -> None: loop = asyncio.get_event_loop() - def sigterm_handler(*_): + def sigterm_handler(*_: typing.Any) -> None: loop.create_task(self.close()) try: diff --git a/hikari/models/applications.py b/hikari/models/applications.py index 288ac915de..0c688f643d 100644 --- a/hikari/models/applications.py +++ b/hikari/models/applications.py @@ -428,7 +428,7 @@ def format_icon(self, *, format_: str = "png", size: int = 4096) -> typing.Optio If the size is not an integer power of 2 between 16 and 4096 (inclusive). """ - if self.icon_hash: + if self.icon_hash is not None: url = cdn.generate_cdn_url("application-icons", str(self.id), self.icon_hash, format_=format_, size=size) return files.URL(url) return None @@ -467,7 +467,7 @@ def format_cover_image(self, *, format_: str = "png", size: int = 4096) -> typin If the size is not an integer power of 2 between 16 and 4096 (inclusive). """ - if self.cover_image_hash: + if self.cover_image_hash is not None: url = cdn.generate_cdn_url( "application-assets", str(self.id), self.cover_image_hash, format_=format_, size=size ) diff --git a/hikari/models/channels.py b/hikari/models/channels.py index e372e48c57..3c2613f95f 100644 --- a/hikari/models/channels.py +++ b/hikari/models/channels.py @@ -201,7 +201,7 @@ class GroupDMChannel(DMChannel): @property def icon(self) -> typing.Optional[files.URL]: """Icon for this DM channel, if set.""" - return self.format_icon_url() + return self.format_icon() # noinspection PyShadowingBuiltins def format_icon(self, *, format: str = "png", size: int = 4096) -> typing.Optional[files.URL]: diff --git a/hikari/models/embeds.py b/hikari/models/embeds.py index e3d906c6d0..3d7852d32d 100644 --- a/hikari/models/embeds.py +++ b/hikari/models/embeds.py @@ -41,6 +41,10 @@ from hikari.utilities import files +def _maybe_color(value: typing.Optional[colors.ColorLike]) -> typing.Optional[colors.Color]: + return colors.Color.of(value) if value is not None else None + + @attr.s(slots=True, kw_only=True, init=False) class EmbedResource(files.Resource): """A base type for any resource provided in an embed. @@ -66,10 +70,11 @@ def url(self) -> str: return self.resource.url @property - def filename(self) -> typing.Optional[str]: + def filename(self) -> str: return self.resource.filename @contextlib.asynccontextmanager + @typing.no_type_check async def stream(self) -> files.AsyncReader: async with self.resource.stream() as stream: yield stream @@ -179,14 +184,27 @@ class EmbedField: class Embed: """Represents an embed.""" - color: typing.Optional[colors.Color] = attr.ib( - default=None, repr=False, converter=lambda c: colors.Color.of(c) if c is not None else None, - ) - """The colour of the embed, or `None` to use the default.""" + color: typing.Optional[colors.Color] = attr.ib(default=None, repr=False, converter=_maybe_color) + """Colour of the embed, or `None` to use the default.""" - colour: typing.Optional[colors.Color] = property( - lambda self: self.color, lambda self, colour: setattr(self, "color", colour) - ) + @property + def colour(self) -> typing.Optional[colors.Color]: + """Colour of the embed, or `None` to use the default. + + !!! note + This is an alias for `color` for people who do not use Americanized + English. + """ + return self.color + + @colour.setter + def colour(self, value: typing.Optional[colors.ColorLike]) -> None: + # implicit attrs conversion. + self.color = value # type: ignore + + @colour.deleter + def colour(self) -> None: + del self.color title: typing.Optional[str] = attr.ib(default=None, repr=True) """The title of the embed, or `None` if not present.""" diff --git a/hikari/models/guilds.py b/hikari/models/guilds.py index aeeb36dfce..ffb5006303 100644 --- a/hikari/models/guilds.py +++ b/hikari/models/guilds.py @@ -449,7 +449,13 @@ def format_icon(self, *, format_: typing.Optional[str] = None, size: int = 4096) ValueError If `size` is not a power of two or not between 16 and 4096. """ - if self.icon_hash: + if self.icon_hash is not None: + if format_ is None: + if self.icon_hash.startswith("a_"): + format_ = "gif" + else: + format_ = "png" + url = cdn.generate_cdn_url("icons", str(self.id), self.icon_hash, format_=format_, size=size) return files.URL(url) return None @@ -504,7 +510,7 @@ def format_splash(self, *, format_: str = "png", size: int = 4096) -> typing.Opt ValueError If `size` is not a power of two or not between 16 and 4096. """ - if self.splash_hash: + if self.splash_hash is not None: url = cdn.generate_cdn_url("splashes", str(self.id), self.splash_hash, format_=format_, size=size) return files.URL(url) return None @@ -536,7 +542,7 @@ def format_discovery_splash(self, *, format_: str = "png", size: int = 4096) -> ValueError If `size` is not a power of two or not between 16 and 4096. """ - if self.discovery_splash_hash: + if self.discovery_splash_hash is not None: url = cdn.generate_cdn_url( "discovery-splashes", str(self.id), self.discovery_splash_hash, format_=format_, size=size ) @@ -865,7 +871,7 @@ def format_splash(self, *, format_: str = "png", size: int = 4096) -> typing.Opt ValueError If `size` is not a power of two or not between 16 and 4096. """ - if self.splash_hash: + if self.splash_hash is not None: url = cdn.generate_cdn_url("splashes", str(self.id), self.splash_hash, format_=format_, size=size) return files.URL(url) return None @@ -931,7 +937,7 @@ def format_banner(self, *, format_: str = "png", size: int = 4096) -> typing.Opt ValueError If `size` is not a power of two or not between 16 and 4096. """ - if self.banner_hash: + if self.banner_hash is not None: url = cdn.generate_cdn_url("banners", str(self.id), self.banner_hash, format_=format_, size=size) return files.URL(url) return None diff --git a/hikari/models/invites.py b/hikari/models/invites.py index d5b4379dad..1f4645785e 100644 --- a/hikari/models/invites.py +++ b/hikari/models/invites.py @@ -117,7 +117,7 @@ def format_splash(self, *, format_: str = "png", size: int = 4096) -> typing.Opt ValueError If `size` is not a power of two or not between 16 and 4096. """ - if self.splash_hash: + if self.splash_hash is not None: url = cdn.generate_cdn_url("splashes", str(self.id), self.splash_hash, format_=format_, size=size) return files.URL(url) return None @@ -149,7 +149,7 @@ def format_banner(self, *, format_: str = "png", size: int = 4096) -> typing.Opt ValueError If `size` is not a power of two or not between 16 and 4096. """ - if self.banner_hash: + if self.banner_hash is not None: url = cdn.generate_cdn_url("banners", str(self.id), self.banner_hash, format_=format_, size=size) return files.URL(url) return None @@ -244,6 +244,6 @@ def expires_at(self) -> typing.Optional[datetime.datetime]: If this invite doesn't have a set expiry then this will be `None`. """ - if self.max_age: + if self.max_age is not None: return self.created_at + self.max_age return None diff --git a/hikari/models/users.py b/hikari/models/users.py index 2936f374ae..71d047e9ea 100644 --- a/hikari/models/users.py +++ b/hikari/models/users.py @@ -200,7 +200,13 @@ def format_avatar(self, *, format_: typing.Optional[str] = None, size: int = 409 If `size` is not a power of two or not between 16 and 4096. """ if self.avatar_hash is not None: - url = cdn.get_avatar_url(self.id, self.avatar_hash, format_=format_, size=size) + if format_ is None: + if self.avatar_hash.startswith("a_"): + format_ = "gif" + else: + format_ = "png" + + url = cdn.generate_cdn_url("avatars", str(self.id), self.avatar_hash, format_=format_, size=size) return files.URL(url) return None diff --git a/hikari/models/webhooks.py b/hikari/models/webhooks.py index 1289d98b24..5520fd98bf 100644 --- a/hikari/models/webhooks.py +++ b/hikari/models/webhooks.py @@ -390,7 +390,7 @@ def format_avatar(self, format_: str = "png", size: int = 4096) -> typing.Option ValueError If `size` is not a power of two between 16 and 4096 (inclusive). """ - if not self.avatar_hash: + if self.avatar_hash is not None: url = cdn.generate_cdn_url("avatars", str(self.id), self.avatar_hash, format_=format_, size=size) return files_.URL(url) return None diff --git a/hikari/net/http_client.py b/hikari/net/http_client.py index 02c417c1cd..1502d28d88 100644 --- a/hikari/net/http_client.py +++ b/hikari/net/http_client.py @@ -335,19 +335,19 @@ async def _create_ws( ) -async def parse_error_response(response: aiohttp.ClientResponse) -> typing.NoReturn: - """Given an erroneous HTTP response, raise a corresponding exception.""" +async def generate_error_response(response: aiohttp.ClientResponse) -> errors.HTTPError: + """Given an erroneous HTTP response, return a corresponding exception.""" real_url = str(response.real_url) raw_body = await response.read() if response.status == http.HTTPStatus.BAD_REQUEST: - raise errors.BadRequest(real_url, response.headers, raw_body) + return errors.BadRequest(real_url, response.headers, raw_body) if response.status == http.HTTPStatus.UNAUTHORIZED: - raise errors.Unauthorized(real_url, response.headers, raw_body) + return errors.Unauthorized(real_url, response.headers, raw_body) if response.status == http.HTTPStatus.FORBIDDEN: - raise errors.Forbidden(real_url, response.headers, raw_body) + return errors.Forbidden(real_url, response.headers, raw_body) if response.status == http.HTTPStatus.NOT_FOUND: - raise errors.NotFound(real_url, response.headers, raw_body) + return errors.NotFound(real_url, response.headers, raw_body) # noinspection PyArgumentList status = http.HTTPStatus(response.status) @@ -360,4 +360,4 @@ async def parse_error_response(response: aiohttp.ClientResponse) -> typing.NoRet else: cls = errors.HTTPErrorResponse - raise cls(real_url, status, response.headers, raw_body) + return cls(real_url, status, response.headers, raw_body) diff --git a/hikari/net/rest.py b/hikari/net/rest.py index 878b4672ed..d5df1fb378 100644 --- a/hikari/net/rest.py +++ b/hikari/net/rest.py @@ -104,6 +104,8 @@ class REST(http_client.HTTPClient, component.IComponent): # pylint:disable=too- The API version to use. """ + __slots__ = ("buckets", "global_rate_limit", "version", "_app", "_rest_url", "_token") + class _RetryRequest(RuntimeError): __slots__ = () @@ -133,8 +135,6 @@ def __init__( ) self.buckets = buckets.RESTBucketManager() self.global_rate_limit = rate_limits.ManualRateLimiter() - self._invalid_requests = 0 - self._invalid_request_window = -float("inf") self.version = version self._app = app @@ -238,11 +238,11 @@ async def _request_once( real_url = str(response.real_url) raise errors.HTTPError(real_url, f"Expected JSON response but received {response.content_type}") - await self._handle_error_response(response) + return await self._handle_error_response(response) @staticmethod - async def _handle_error_response(response) -> typing.NoReturn: - return await http_client.parse_error_response(response) + async def _handle_error_response(response: aiohttp.ClientResponse) -> typing.NoReturn: + raise await http_client.generate_error_response(response) async def _parse_ratelimits(self, compiled_route: routes.CompiledRoute, response: aiohttp.ClientResponse) -> None: # Worth noting there is some bug on V6 that rate limits me immediately if I have an invalid token. @@ -1093,14 +1093,14 @@ async def create_message( body.put("tts", tts) if isinstance(attachments, undefined.Undefined): - attachments = [] + final_attachments: typing.List[files.Resource] = [] else: - attachments = [files.ensure_resource(a) for a in attachments] + final_attachments = [files.ensure_resource(a) for a in attachments] if not isinstance(embed, undefined.Undefined): embed_payload, embed_attachments = self._app.entity_factory.serialize_embed(embed) body.put("embed", embed_payload) - attachments.extend(embed_attachments) + final_attachments.extend(embed_attachments) if attachments: form = data_binding.URLEncodedForm() @@ -1109,7 +1109,7 @@ async def create_message( stack = contextlib.AsyncExitStack() try: - for i, attachment in enumerate(attachments): + for i, attachment in enumerate(final_attachments): stream = await stack.enter_async_context(attachment.stream(executor=self._app.thread_pool_executor)) form.add_field( f"file{i}", stream, filename=stream.filename, content_type=self._APPLICATION_OCTET_STREAM @@ -1192,9 +1192,10 @@ async def edit_message( else: body.put("content", None) - if embed is not None: - body.put("embed", embed, conversion=self._app.entity_factory.serialize_embed) - else: + if isinstance(embed, embeds_.Embed): + embed_payload, _ = self._app.entity_factory.serialize_embed(embed) + body.put("embed", embed_payload) + elif embed is None: body.put("embed", None) raw_response = await self._request(route, body=body) @@ -1522,8 +1523,10 @@ async def execute_webhook( try: for i, attachment in enumerate(attachments): - stream, filename = await stack.enter_async_context(attachment.to_attachment(self._app)) - form.add_field(f"file{i}", stream, filename=filename, content_type=self._APPLICATION_OCTET_STREAM) + stream = await stack.enter_async_context(attachment.stream(self._app.thread_pool_executor)) + form.add_field( + f"file{i}", stream, filename=stream.filename, content_type=self._APPLICATION_OCTET_STREAM + ) raw_response = await self._request(route, body=form, no_auth=no_auth) finally: diff --git a/hikari/net/rest_utils.py b/hikari/net/rest_utils.py index 93f256299e..0e9287d00e 100644 --- a/hikari/net/rest_utils.py +++ b/hikari/net/rest_utils.py @@ -44,9 +44,9 @@ from hikari.models import bases from hikari.models import channels from hikari.models import colors - from hikari.models import files from hikari.models import guilds from hikari.models import permissions as permissions_ + from hikari.utilities import files class TypingIndicator: @@ -195,7 +195,7 @@ class GuildBuilder: If not overridden, this will use the Discord default level. """ - icon: typing.Union[undefined.Undefined, files.BaseStream] = undefined.Undefined() + icon: typing.Union[undefined.Undefined, files.URL] = undefined.Undefined() """Guild icon to use that can be overwritten. If not overridden, the guild will not have an icon. @@ -253,7 +253,10 @@ async def create(self) -> guilds.Guild: payload.put("explicit_content_filter", self.explicit_content_filter_level) if not isinstance(self.icon, undefined.Undefined): - payload.put("icon", await self.icon.fetch_data_uri()) + # This isn't annotated properly in the standard library, apparently. + async with self.icon.stream(self._app.thread_pool_executor) as stream: + data_uri = await stream.data_uri() + payload.put("icon", data_uri) raw_response = await self._request_call(route, body=payload) response = typing.cast(data_binding.JSONObject, raw_response) diff --git a/hikari/utilities/cdn.py b/hikari/utilities/cdn.py index a01eb9f9e0..412065b576 100644 --- a/hikari/utilities/cdn.py +++ b/hikari/utilities/cdn.py @@ -71,44 +71,6 @@ def generate_cdn_url(*route_parts: str, format_: str, size: typing.Optional[int] return f"{url}?{query}" if query else url -def get_avatar_url( - user_id: snowflake.Snowflake, avatar_hash: str, *, format_: typing.Optional[str] = None, size: int = 4096, -) -> str: - """Generate the avatar URL for this user's custom avatar if set, else their default avatar. - - Parameters - ---------- - user_id : hikari.utilities.snowflake.Snowflake - The user ID of the avatar to fetch. - avatar_hash : str - The avatar hash code. - format_ : str - The format to use for this URL, defaults to `png` or `gif`. - Supports `png`, `jpeg`, `jpg`, `webp` and `gif` (when - animated). Will be ignored for default avatars which can only be - `png`. - size : int - The size to set for the URL, defaults to `4096`. - Can be any power of two between 16 and 4096. - Will be ignored for default avatars. - - Returns - ------- - str - The string URL, or None if the default avatar is used instead. - - Raises - ------ - ValueError - If `size` is not a power of two or not between 16 and 4096. - """ - if format_ is None and avatar_hash.startswith("a_"): - format_ = "gif" - elif format_ is None: - format_ = "png" - return generate_cdn_url("avatars", str(user_id), avatar_hash, format_=format_, size=size) - - def get_default_avatar_index(discriminator: str) -> int: """Get the index of the default avatar for the given discriminator. diff --git a/hikari/utilities/files.py b/hikari/utilities/files.py index f67d77e94a..a604b27bfb 100644 --- a/hikari/utilities/files.py +++ b/hikari/utilities/files.py @@ -73,12 +73,12 @@ def ensure_resource(url_or_resource: Resource, /) -> Resource: """Given a resource, return it.""" -def ensure_resource(url_or_resource: typing.Union[None, str, os.PathLike, Resource], /) -> typing.Optional[Resource]: +def ensure_resource(url_or_resource: typing.Union[None, str, Resource], /) -> typing.Optional[Resource]: """Given a resource or string, convert it to a valid resource as needed. Parameters ---------- - url_or_resource : None or str or os.PathLike or Resource + url_or_resource : None or str or Resource The item to convert. If the item is `None`, then `None` is returned. Likewise if a `Resource` is passed, it is simply returned again. Anything else is converted to a `Resource` first. @@ -113,7 +113,8 @@ def guess_mimetype_from_filename(name: str, /) -> typing.Optional[str]: The closest guess to the given filename. May be `None` if no match was found. """ - return mimetypes.guess_type(name) + guess, _ = mimetypes.guess_type(name) + return guess def guess_mimetype_from_data(data: bytes, /) -> typing.Optional[str]: @@ -267,7 +268,7 @@ class ByteReader(AsyncReader): data: bytes """The data that will be yielded in chunks.""" - def __aiter__(self) -> typing.AsyncGenerator[typing.Any, bytes]: + async def __aiter__(self) -> typing.AsyncGenerator[typing.Any, bytes]: for i in range(0, len(self.data), _MAGIC): yield self.data[i : i + _MAGIC] @@ -281,7 +282,7 @@ class FileReader(AsyncReader): blocking IO. """ - path: typing.Union[str, os.PathLike, pathlib.Path] + path: typing.Union[str, pathlib.Path] """The path to the resource to read.""" loop: asyncio.AbstractEventLoop = attr.ib(factory=asyncio.get_running_loop) @@ -374,6 +375,7 @@ def filename(self) -> str: @abc.abstractmethod @contextlib.asynccontextmanager + @typing.no_type_check async def stream(self, *, executor: typing.Optional[concurrent.futures.ThreadPoolExecutor]) -> AsyncReader: """Return an async iterable of bytes to stream.""" @@ -433,10 +435,10 @@ def __init__( if extension is None and mimetype is not None: extension = guess_file_extension(mimetype) - if filename is None and mimetype is None: + if mimetype is None: if extension is None: - raise TypeError("Cannot infer data type details, please specify one of filetype, mimetype, extension") - raise TypeError("Cannot infer data type details from extension. Please specify mimetype or filename") + raise TypeError("Cannot infer data type details, please specify a mimetype or an extension") + raise TypeError("Cannot infer data type details from extension. Please specify a mimetype") self._filename = filename self.mimetype = mimetype @@ -451,6 +453,7 @@ def filename(self) -> str: return self._filename @contextlib.asynccontextmanager + @typing.no_type_check async def stream(self, *, executor: typing.Optional[concurrent.futures.ThreadPoolExecutor] = None) -> ByteReader: """Start streaming the content in chunks. @@ -488,6 +491,7 @@ class WebResource(Resource, abc.ABC): __slots__ = () @contextlib.asynccontextmanager + @typing.no_type_check async def stream(self, *, executor: typing.Optional[concurrent.futures.ThreadPoolExecutor] = None) -> WebReader: """Start streaming the content into memory by downloading it. @@ -558,13 +562,6 @@ async def stream(self, *, executor: typing.Optional[concurrent.futures.ThreadPoo if mimetype is None: mimetype = resp.content_type - if filename is None: - if resp.content_disposition is not None: - filename = resp.content_disposition.filename - - if filename is None: - filename = generate_filename_from_details(mimetype=mimetype) - yield WebReader( stream=resp.content, url=str(resp.real_url), @@ -576,7 +573,7 @@ async def stream(self, *, executor: typing.Optional[concurrent.futures.ThreadPoo size=resp.content_length, ) else: - await http_client.parse_error_response(resp) + raise await http_client.generate_error_response(resp) class URL(WebResource): @@ -634,10 +631,10 @@ class File(Resource): __slots__ = ("path", "_filename") - path: typing.Union[str, os.PathLike, pathlib.Path] + path: typing.Union[str, pathlib.Path] _filename: typing.Optional[str] - def __init__(self, path: typing.Union[str, os.PathLike, pathlib.Path], filename: typing.Optional[str]) -> None: + def __init__(self, path: typing.Union[str, pathlib.Path], filename: typing.Optional[str]) -> None: self.path = path self._filename = filename @@ -652,6 +649,7 @@ def filename(self) -> str: return self._filename @contextlib.asynccontextmanager + @typing.no_type_check async def stream(self, *, executor: typing.Optional[concurrent.futures.ThreadPoolExecutor] = None) -> FileReader: """Start streaming the resource using a thread pool executor. diff --git a/hikari/utilities/klass.py b/hikari/utilities/klass.py index 81c9d2bfca..c93bcfcb47 100644 --- a/hikari/utilities/klass.py +++ b/hikari/utilities/klass.py @@ -78,9 +78,9 @@ class SingletonMeta(abc.ABCMeta): ___instances___: typing.Dict[typing.Type[typing.Any], typing.Any] = {} - def __call__(cls): + def __call__(cls, *args: typing.Any, **kwargs: typing.Any) -> typing.Any: if cls not in SingletonMeta.___instances___: - SingletonMeta.___instances___[cls] = super().__call__() + SingletonMeta.___instances___[cls] = super().__call__(*args, **kwargs) return SingletonMeta.___instances___[cls] From 1d391d4729ee868104a67b729c78854365ae7d33 Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Fri, 12 Jun 2020 10:17:02 +0100 Subject: [PATCH 504/922] Reworks singleton usage, fixes #384. --- hikari/__init__.py | 1 - hikari/api/app.py | 18 +- hikari/api/entity_factory.py | 4 +- hikari/api/event_dispatcher.py | 4 +- hikari/events/message.py | 48 +- hikari/impl/bot.py | 39 +- hikari/impl/entity_factory.py | 72 ++- hikari/impl/event_manager_core.py | 7 +- hikari/impl/gateway_zookeeper.py | 35 +- hikari/impl/rest_app.py | 21 +- hikari/models/channels.py | 1 - hikari/models/guilds.py | 18 +- hikari/models/messages.py | 50 +- hikari/models/users.py | 12 +- hikari/models/webhooks.py | 66 +-- hikari/net/gateway.py | 74 +-- hikari/net/http_client.py | 1 - hikari/net/iterators.py | 4 +- hikari/net/rest.py | 518 ++++++++++---------- hikari/net/rest_utils.py | 108 ++-- hikari/net/strings.py | 1 - hikari/net/tracing.py | 2 +- hikari/utilities/cdn.py | 2 +- hikari/utilities/data_binding.py | 36 +- hikari/utilities/files.py | 4 +- hikari/utilities/klass.py | 109 ---- hikari/utilities/reflect.py | 27 +- hikari/utilities/undefined.py | 103 ++-- tests/hikari/impl/test_entity_factory.py | 68 +-- tests/hikari/utilities/test_data_binding.py | 8 +- tests/hikari/utilities/test_klass.py | 52 -- tests/hikari/utilities/test_reflect.py | 19 + tests/hikari/utilities/test_undefined.py | 34 +- 33 files changed, 701 insertions(+), 865 deletions(-) delete mode 100644 hikari/utilities/klass.py delete mode 100644 tests/hikari/utilities/test_klass.py diff --git a/hikari/__init__.py b/hikari/__init__.py index 458fb1428d..62dab14814 100644 --- a/hikari/__init__.py +++ b/hikari/__init__.py @@ -35,6 +35,5 @@ from hikari._about import __license__ from hikari._about import __url__ from hikari._about import __version__ - from hikari.impl.bot import BotImpl as Bot from hikari.impl.rest_app import RESTAppImpl as RESTClient diff --git a/hikari/api/app.py b/hikari/api/app.py index 3af431022e..b37256a6e5 100644 --- a/hikari/api/app.py +++ b/hikari/api/app.py @@ -253,7 +253,7 @@ def event_dispatcher(self) -> event_dispatcher_.IEventDispatcher: # Do not add type hints to me! I delegate to a documented method elsewhere! @functools.wraps(event_dispatcher_.IEventDispatcher.listen) def listen( - self, event_type: typing.Union[undefined.Undefined, typing.Type[_EventT]] = undefined.Undefined(), + self, event_type: typing.Union[undefined.UndefinedType, typing.Type[_EventT]] = undefined.UNDEFINED, ) -> typing.Callable[[_CallbackT], _CallbackT]: ... @@ -350,10 +350,10 @@ async def join(self) -> None: async def update_presence( self, *, - status: typing.Union[undefined.Undefined, presences.Status] = undefined.Undefined(), - activity: typing.Union[undefined.Undefined, presences.Activity, None] = undefined.Undefined(), - idle_since: typing.Union[undefined.Undefined, datetime.datetime, None] = undefined.Undefined(), - is_afk: typing.Union[undefined.Undefined, bool] = undefined.Undefined(), + status: typing.Union[undefined.UndefinedType, presences.Status] = undefined.UNDEFINED, + activity: typing.Union[undefined.UndefinedType, presences.Activity, None] = undefined.UNDEFINED, + idle_since: typing.Union[undefined.UndefinedType, datetime.datetime, None] = undefined.UNDEFINED, + is_afk: typing.Union[undefined.UndefinedType, bool] = undefined.UNDEFINED, ) -> None: """Update the presence of the user for all shards. @@ -371,14 +371,14 @@ async def update_presence( Parameters ---------- - status : hikari.models.presences.Status or hikari.utilities.undefined.Undefined + status : hikari.models.presences.Status or hikari.utilities.undefined.UndefinedType If defined, the new status to set. - activity : hikari.models.presences.Activity or None or hikari.utilities.undefined.Undefined + activity : hikari.models.presences.Activity or None or hikari.utilities.undefined.UndefinedType If defined, the new activity to set. - idle_since : datetime.datetime or None or hikari.utilities.undefined.Undefined + idle_since : datetime.datetime or None or hikari.utilities.undefined.UndefinedType If defined, the time to show up as being idle since, or `None` if not applicable. If undefined, then it is not changed. - is_afk : bool or hikari.utilities.undefined.Undefined + is_afk : bool or hikari.utilities.undefined.UndefinedType If defined, `True` if the user should be marked as AFK, or `False` if not AFK. """ diff --git a/hikari/api/entity_factory.py b/hikari/api/entity_factory.py index fd11849222..96606906ad 100644 --- a/hikari/api/entity_factory.py +++ b/hikari/api/entity_factory.py @@ -442,7 +442,7 @@ def deserialize_member( self, payload: data_binding.JSONObject, *, - user: typing.Union[undefined.Undefined, user_models.User] = undefined.Undefined(), + user: typing.Union[undefined.UndefinedType, user_models.User] = undefined.UNDEFINED, ) -> guild_models.Member: """Parse a raw payload from Discord into a member object. @@ -450,7 +450,7 @@ def deserialize_member( ---------- payload : hikari.utilities.data_binding.JSONObject The JSON payload to deserialize. - user : hikari.models.users.User or hikari.utilities.undefined.Undefined + user : hikari.models.users.User or hikari.utilities.undefined.UndefinedType The user to attach to this member, should only be passed in situations where "user" is not included in the payload. diff --git a/hikari/api/event_dispatcher.py b/hikari/api/event_dispatcher.py index 823efaf2ec..126b2d07eb 100644 --- a/hikari/api/event_dispatcher.py +++ b/hikari/api/event_dispatcher.py @@ -196,7 +196,7 @@ async def on_message(event): @abc.abstractmethod def listen( - self, event_type: typing.Union[undefined.Undefined, typing.Type[_EventT]] = undefined.Undefined(), + self, event_type: typing.Union[undefined.UndefinedType, typing.Type[_EventT]] = undefined.UNDEFINED, ) -> typing.Callable[[_CallbackT], _CallbackT]: """Generate a decorator to subscribe a callback to an event type. @@ -204,7 +204,7 @@ def listen( Parameters ---------- - event_type : hikari.utilities.undefined.Undefined or typing.Type[hikari.events.base.HikariEvent] + event_type : hikari.utilities.undefined.UndefinedType or typing.Type[hikari.events.base.HikariEvent] The event type to subscribe to. The implementation may allow this to be undefined. If this is the case, the event type will be inferred instead from the type hints on the function signature. diff --git a/hikari/events/message.py b/hikari/events/message.py index 56f68feec4..e846652a1d 100644 --- a/hikari/events/message.py +++ b/hikari/events/message.py @@ -38,8 +38,8 @@ from hikari.events import base as base_events from hikari.models import bases as base_models -from hikari.models import messages from hikari.models import intents +from hikari.models import messages if typing.TYPE_CHECKING: import datetime @@ -66,7 +66,7 @@ class UpdateMessage(messages.Message): !!! warn All fields on this model except `channel` and `id` may be set to - `hikari.utilities.undefined.Undefined` (a singleton) if we have not + `hikari.utilities.undefined.UndefinedType` (a singleton) if we have not received information about their state from Discord alongside field nullability. """ @@ -74,75 +74,75 @@ class UpdateMessage(messages.Message): channel_id: snowflake.Snowflake = attr.ib(repr=True) """The ID of the channel that the message was sent in.""" - guild_id: typing.Union[snowflake.Snowflake, undefined.Undefined] = attr.ib(repr=True) + guild_id: typing.Union[snowflake.Snowflake, undefined.UndefinedType] = attr.ib(repr=True) """The ID of the guild that the message was sent in.""" - author: typing.Union[users.User, undefined.Undefined] = attr.ib(repr=True) + author: typing.Union[users.User, undefined.UndefinedType] = attr.ib(repr=True) """The author of this message.""" # TODO: can we merge member and author together? # We could override deserialize to to this and then reorganise the payload, perhaps? - member: typing.Union[guilds.Member, undefined.Undefined] = attr.ib(repr=False) + member: typing.Union[guilds.Member, undefined.UndefinedType] = attr.ib(repr=False) """The member properties for the message's author.""" - content: typing.Union[str, undefined.Undefined] = attr.ib(repr=False) + content: typing.Union[str, undefined.UndefinedType] = attr.ib(repr=False) """The content of the message.""" - timestamp: typing.Union[datetime.datetime, undefined.Undefined] = attr.ib(repr=False) + timestamp: typing.Union[datetime.datetime, undefined.UndefinedType] = attr.ib(repr=False) """The timestamp that the message was sent at.""" - edited_timestamp: typing.Union[datetime.datetime, undefined.Undefined, None] = attr.ib(repr=False) + edited_timestamp: typing.Union[datetime.datetime, undefined.UndefinedType, None] = attr.ib(repr=False) """The timestamp that the message was last edited at. Will be `None` if the message wasn't ever edited. """ - is_tts: typing.Union[bool, undefined.Undefined] = attr.ib(repr=False) + is_tts: typing.Union[bool, undefined.UndefinedType] = attr.ib(repr=False) """Whether the message is a TTS message.""" - is_mentioning_everyone: typing.Union[bool, undefined.Undefined] = attr.ib(repr=False) + is_mentioning_everyone: typing.Union[bool, undefined.UndefinedType] = attr.ib(repr=False) """Whether the message mentions `@everyone` or `@here`.""" - user_mentions: typing.Union[typing.Set[snowflake.Snowflake], undefined.Undefined] = attr.ib(repr=False) + user_mentions: typing.Union[typing.Set[snowflake.Snowflake], undefined.UndefinedType] = attr.ib(repr=False) """The users the message mentions.""" - role_mentions: typing.Union[typing.Set[snowflake.Snowflake], undefined.Undefined] = attr.ib(repr=False) + role_mentions: typing.Union[typing.Set[snowflake.Snowflake], undefined.UndefinedType] = attr.ib(repr=False) """The roles the message mentions.""" - channel_mentions: typing.Union[typing.Set[snowflake.Snowflake], undefined.Undefined] = attr.ib(repr=False) + channel_mentions: typing.Union[typing.Set[snowflake.Snowflake], undefined.UndefinedType] = attr.ib(repr=False) """The channels the message mentions.""" - attachments: typing.Union[typing.Sequence[messages.Attachment], undefined.Undefined] = attr.ib(repr=False) + attachments: typing.Union[typing.Sequence[messages.Attachment], undefined.UndefinedType] = attr.ib(repr=False) """The message attachments.""" - embeds: typing.Union[typing.Sequence[embed_models.Embed], undefined.Undefined] = attr.ib(repr=False) + embeds: typing.Union[typing.Sequence[embed_models.Embed], undefined.UndefinedType] = attr.ib(repr=False) """The message's embeds.""" - reactions: typing.Union[typing.Sequence[messages.Reaction], undefined.Undefined] = attr.ib(repr=False) + reactions: typing.Union[typing.Sequence[messages.Reaction], undefined.UndefinedType] = attr.ib(repr=False) """The message's reactions.""" - is_pinned: typing.Union[bool, undefined.Undefined] = attr.ib(repr=False) + is_pinned: typing.Union[bool, undefined.UndefinedType] = attr.ib(repr=False) """Whether the message is pinned.""" - webhook_id: typing.Union[snowflake.Snowflake, undefined.Undefined] = attr.ib(repr=False) + webhook_id: typing.Union[snowflake.Snowflake, undefined.UndefinedType] = attr.ib(repr=False) """If the message was generated by a webhook, the webhook's ID.""" - type: typing.Union[messages.MessageType, undefined.Undefined] = attr.ib(repr=False) + type: typing.Union[messages.MessageType, undefined.UndefinedType] = attr.ib(repr=False) """The message's type.""" - activity: typing.Union[messages.MessageActivity, undefined.Undefined] = attr.ib(repr=False) + activity: typing.Union[messages.MessageActivity, undefined.UndefinedType] = attr.ib(repr=False) """The message's activity.""" application: typing.Optional[applications.Application] = attr.ib(repr=False) """The message's application.""" - message_reference: typing.Union[messages.MessageCrosspost, undefined.Undefined] = attr.ib(repr=False) + message_reference: typing.Union[messages.MessageCrosspost, undefined.UndefinedType] = attr.ib(repr=False) """The message's cross-posted reference data.""" - flags: typing.Union[messages.MessageFlag, undefined.Undefined] = attr.ib(repr=False) + flags: typing.Union[messages.MessageFlag, undefined.UndefinedType] = attr.ib(repr=False) """The message's flags.""" - nonce: typing.Union[str, undefined.Undefined] = attr.ib(repr=False) + nonce: typing.Union[str, undefined.UndefinedType] = attr.ib(repr=False) """The message nonce. This is a string used for validating a message was sent. @@ -157,7 +157,7 @@ class MessageUpdateEvent(base_events.HikariEvent, base_models.Unique): !!! warn Unlike `MessageCreateEvent`, `MessageUpdateEvent.message` is an arbitrarily partial version of `hikari.models.messages.Message` where - any field except `id` may be set to `hikari.utilities.undefined.Undefined` + any field except `id` may be set to `hikari.utilities.undefined.UndefinedType` (a singleton) to indicate that it has not been changed. """ diff --git a/hikari/impl/bot.py b/hikari/impl/bot.py index 19e3613d05..ef7baf21fc 100644 --- a/hikari/impl/bot.py +++ b/hikari/impl/bot.py @@ -27,7 +27,6 @@ import os import platform import sys - import typing from concurrent import futures @@ -39,7 +38,7 @@ from hikari.models import presences from hikari.net import http_settings as http_settings_ from hikari.net import rest -from hikari.utilities import klass +from hikari.utilities import reflect from hikari.utilities import undefined if typing.TYPE_CHECKING: @@ -58,7 +57,7 @@ class BotImpl(gateway_zookeeper.AbstractGatewayZookeeper, app.IBot): Parameters ---------- - config : hikari.utilities.undefined.Undefined or hikari.net.http_settings.HTTPSettings + config : hikari.utilities.undefined.UndefinedType or hikari.net.http_settings.HTTPSettings Optional aiohttp settings to apply to the REST components, gateway shards, and voice websockets. If undefined, then sane defaults are used. debug : bool @@ -74,14 +73,14 @@ class BotImpl(gateway_zookeeper.AbstractGatewayZookeeper, app.IBot): The version of the gateway to connect to. At the time of writing, only version `6` and version `7` (undocumented development release) are supported. This defaults to using v6. - initial_activity : hikari.models.presences.Activity or None or hikari.utilities.undefined.Undefined + initial_activity : hikari.models.presences.Activity or None or hikari.utilities.undefined.UndefinedType The initial activity to have on each shard. - initial_activity : hikari.models.presences.Status or hikari.utilities.undefined.Undefined + initial_activity : hikari.models.presences.Status or hikari.utilities.undefined.UndefinedType The initial status to have on each shard. - initial_idle_since : datetime.datetime or None or hikari.utilities.undefined.Undefined + initial_idle_since : datetime.datetime or None or hikari.utilities.undefined.UndefinedType The initial time to show as being idle since, or `None` if not idle, for each shard. - initial_idle_since : bool or hikari.utilities.undefined.Undefined + initial_idle_since : bool or hikari.utilities.undefined.UndefinedType If `True`, each shard will appear as being AFK on startup. If `False`, each shard will appear as _not_ being AFK. intents : hikari.models.intents.Intent or None @@ -103,11 +102,11 @@ class BotImpl(gateway_zookeeper.AbstractGatewayZookeeper, app.IBot): The version of the REST API to connect to. At the time of writing, only version `6` and version `7` (undocumented development release) are supported. This defaults to v6. - shard_ids : typing.Set[int] or undefined.Undefined + shard_ids : typing.Set[int] or undefined.UndefinedType A set of every shard ID that should be created and started on startup. If left undefined along with `shard_count`, then auto-sharding is used instead, which is the default. - shard_count : int or undefined.Undefined + shard_count : int or undefined.UndefinedType The number of shards in the entire application. If left undefined along with `shard_ids`, then auto-sharding is used instead, which is the default. @@ -145,31 +144,31 @@ class BotImpl(gateway_zookeeper.AbstractGatewayZookeeper, app.IBot): def __init__( self, *, - config: typing.Union[undefined.Undefined, http_settings_.HTTPSettings] = undefined.Undefined(), + config: typing.Union[undefined.UndefinedType, http_settings_.HTTPSettings] = undefined.UNDEFINED, debug: bool = False, gateway_compression: bool = True, gateway_version: int = 6, - initial_activity: typing.Union[undefined.Undefined, presences.Activity, None] = undefined.Undefined(), - initial_idle_since: typing.Union[undefined.Undefined, datetime.datetime, None] = undefined.Undefined(), - initial_is_afk: typing.Union[undefined.Undefined, bool] = undefined.Undefined(), - initial_status: typing.Union[undefined.Undefined, presences.Status] = undefined.Undefined(), + initial_activity: typing.Union[undefined.UndefinedType, presences.Activity, None] = undefined.UNDEFINED, + initial_idle_since: typing.Union[undefined.UndefinedType, datetime.datetime, None] = undefined.UNDEFINED, + initial_is_afk: typing.Union[undefined.UndefinedType, bool] = undefined.UNDEFINED, + initial_status: typing.Union[undefined.UndefinedType, presences.Status] = undefined.UNDEFINED, intents: typing.Optional[intents_.Intent] = None, large_threshold: int = 250, logging_level: typing.Optional[str] = "INFO", rest_version: int = 6, - rest_url: typing.Union[undefined.Undefined, str] = undefined.Undefined(), - shard_ids: typing.Union[typing.Set[int], undefined.Undefined] = undefined.Undefined(), - shard_count: typing.Union[int, undefined.Undefined] = undefined.Undefined(), + rest_url: typing.Union[undefined.UndefinedType, str] = undefined.UNDEFINED, + shard_ids: typing.Union[typing.Set[int], undefined.UndefinedType] = undefined.UNDEFINED, + shard_count: typing.Union[int, undefined.UndefinedType] = undefined.UNDEFINED, token: str, ): - self._logger = klass.get_logger(self) + self._logger = reflect.get_logger(self) # If logging is already configured, then this does nothing. if logging_level is not None: logging.basicConfig(level=logging_level, format=self.__get_logging_format()) self.__print_banner() - config = http_settings_.HTTPSettings() if isinstance(config, undefined.Undefined) else config + config = http_settings_.HTTPSettings() if config is undefined.UNDEFINED else config self._cache = cache_impl.InMemoryCacheImpl(app=self) self._config = config @@ -235,7 +234,7 @@ def event_consumer(self) -> event_consumer_.IEventConsumer: def http_settings(self) -> http_settings_.HTTPSettings: return self._config - def listen(self, event_type=undefined.Undefined()): + def listen(self, event_type=undefined.UNDEFINED): return self.event_dispatcher.listen(event_type) def subscribe(self, event_type, callback): diff --git a/hikari/impl/entity_factory.py b/hikari/impl/entity_factory.py index cfceccf2d2..9074c8c8fe 100644 --- a/hikari/impl/entity_factory.py +++ b/hikari/impl/entity_factory.py @@ -751,7 +751,7 @@ def deserialize_member( self, payload: data_binding.JSONObject, *, - user: typing.Union[undefined.Undefined, user_models.User] = undefined.Undefined(), + user: typing.Union[undefined.UndefinedType, user_models.User] = undefined.UNDEFINED, ) -> guild_models.Member: guild_member = guild_models.Member(self._app) guild_member.user = user or self.deserialize_user(payload["user"]) @@ -760,18 +760,18 @@ def deserialize_member( if (joined_at := payload.get("joined_at")) is not None: guild_member.joined_at = date.iso8601_datetime_string_to_datetime(joined_at) else: - guild_member.joined_at = undefined.Undefined() + guild_member.joined_at = undefined.UNDEFINED - guild_member.nickname = payload["nick"] if "nick" in payload else undefined.Undefined() + guild_member.nickname = payload["nick"] if "nick" in payload else undefined.UNDEFINED if (premium_since := payload.get("premium_since", ...)) is not None and premium_since is not ...: premium_since = date.iso8601_datetime_string_to_datetime(premium_since) elif premium_since is ...: - premium_since = undefined.Undefined() + premium_since = undefined.UNDEFINED guild_member.premium_since = premium_since - guild_member.is_deaf = payload["deaf"] if "deaf" in payload else undefined.Undefined() - guild_member.is_mute = payload["mute"] if "mute" in payload else undefined.Undefined() + guild_member.is_deaf = payload["deaf"] if "deaf" in payload else undefined.UNDEFINED + guild_member.is_mute = payload["mute"] if "mute" in payload else undefined.UNDEFINED return guild_member def deserialize_role(self, payload: data_binding.JSONObject) -> guild_models.Role: @@ -1142,16 +1142,16 @@ def deserialize_member_presence(self, payload: data_binding.JSONObject) -> prese user_payload = payload["user"] user = user_models.PartialUser(self._app) user.id = snowflake.Snowflake(user_payload["id"]) - user.discriminator = user_payload["discriminator"] if "discriminator" in user_payload else undefined.Undefined() - user.username = user_payload["username"] if "username" in user_payload else undefined.Undefined() - user.avatar_hash = user_payload["avatar"] if "avatar" in user_payload else undefined.Undefined() - user.is_bot = user_payload["bot"] if "bot" in user_payload else undefined.Undefined() - user.is_system = user_payload["system"] if "system" in user_payload else undefined.Undefined() + user.discriminator = user_payload["discriminator"] if "discriminator" in user_payload else undefined.UNDEFINED + user.username = user_payload["username"] if "username" in user_payload else undefined.UNDEFINED + user.avatar_hash = user_payload["avatar"] if "avatar" in user_payload else undefined.UNDEFINED + user.is_bot = user_payload["bot"] if "bot" in user_payload else undefined.UNDEFINED + user.is_system = user_payload["system"] if "system" in user_payload else undefined.UNDEFINED # noinspection PyArgumentList user.flags = ( user_models.UserFlag(user_payload["public_flags"]) if "public_flags" in user_payload - else undefined.Undefined() + else undefined.UNDEFINED ) guild_member_presence.user = user @@ -1538,51 +1538,51 @@ def deserialize_message_update_event(self, payload: data_binding.JSONObject) -> updated_message = message_events.UpdateMessage(self._app) updated_message.id = snowflake.Snowflake(payload["id"]) updated_message.channel_id = ( - snowflake.Snowflake(payload["channel_id"]) if "channel_id" in payload else undefined.Undefined() + snowflake.Snowflake(payload["channel_id"]) if "channel_id" in payload else undefined.UNDEFINED ) updated_message.guild_id = ( - snowflake.Snowflake(payload["guild_id"]) if "guild_id" in payload else undefined.Undefined() + snowflake.Snowflake(payload["guild_id"]) if "guild_id" in payload else undefined.UNDEFINED ) updated_message.author = ( - self.deserialize_user(payload["author"]) if "author" in payload else undefined.Undefined() + self.deserialize_user(payload["author"]) if "author" in payload else undefined.UNDEFINED ) # TODO: will we ever be given "member" but not "author"? updated_message.member = ( self.deserialize_member(payload["member"], user=updated_message.author) if "member" in payload - else undefined.Undefined() + else undefined.UNDEFINED ) - updated_message.content = payload["content"] if "content" in payload else undefined.Undefined() + updated_message.content = payload["content"] if "content" in payload else undefined.UNDEFINED updated_message.timestamp = ( date.iso8601_datetime_string_to_datetime(payload["timestamp"]) if "timestamp" in payload - else undefined.Undefined() + else undefined.UNDEFINED ) if (edited_timestamp := payload.get("edited_timestamp", ...)) is not ... and edited_timestamp is not None: edited_timestamp = date.iso8601_datetime_string_to_datetime(edited_timestamp) elif edited_timestamp is ...: - edited_timestamp = undefined.Undefined() + edited_timestamp = undefined.UNDEFINED updated_message.edited_timestamp = edited_timestamp - updated_message.is_tts = payload["tts"] if "tts" in payload else undefined.Undefined() + updated_message.is_tts = payload["tts"] if "tts" in payload else undefined.UNDEFINED updated_message.is_mentioning_everyone = ( - payload["mention_everyone"] if "mention_everyone" in payload else undefined.Undefined() + payload["mention_everyone"] if "mention_everyone" in payload else undefined.UNDEFINED ) updated_message.user_mentions = ( {snowflake.Snowflake(mention["id"]) for mention in payload["mentions"]} if "mentions" in payload - else undefined.Undefined() + else undefined.UNDEFINED ) updated_message.role_mentions = ( {snowflake.Snowflake(mention) for mention in payload["mention_roles"]} if "mention_roles" in payload - else undefined.Undefined() + else undefined.UNDEFINED ) updated_message.channel_mentions = ( {snowflake.Snowflake(mention["id"]) for mention in payload["mention_channels"]} if "mention_channels" in payload - else undefined.Undefined() + else undefined.UNDEFINED ) if "attachments" in payload: @@ -1599,12 +1599,12 @@ def deserialize_message_update_event(self, payload: data_binding.JSONObject) -> attachments.append(attachment) updated_message.attachments = attachments else: - updated_message.attachments = undefined.Undefined() + updated_message.attachments = undefined.UNDEFINED updated_message.embeds = ( [self.deserialize_embed(embed) for embed in payload["embeds"]] if "embeds" in payload - else undefined.Undefined() + else undefined.UNDEFINED ) if "reactions" in payload: @@ -1617,16 +1617,14 @@ def deserialize_message_update_event(self, payload: data_binding.JSONObject) -> reactions.append(reaction) updated_message.reactions = reactions else: - updated_message.reactions = undefined.Undefined() + updated_message.reactions = undefined.UNDEFINED - updated_message.is_pinned = payload["pinned"] if "pinned" in payload else undefined.Undefined() + updated_message.is_pinned = payload["pinned"] if "pinned" in payload else undefined.UNDEFINED updated_message.webhook_id = ( - snowflake.Snowflake(payload["webhook_id"]) if "webhook_id" in payload else undefined.Undefined() + snowflake.Snowflake(payload["webhook_id"]) if "webhook_id" in payload else undefined.UNDEFINED ) # noinspection PyArgumentList - updated_message.type = ( - message_models.MessageType(payload["type"]) if "type" in payload else undefined.Undefined() - ) + updated_message.type = message_models.MessageType(payload["type"]) if "type" in payload else undefined.UNDEFINED if (activity_payload := payload.get("activity", ...)) is not ...: activity = message_models.MessageActivity() @@ -1635,10 +1633,10 @@ def deserialize_message_update_event(self, payload: data_binding.JSONObject) -> activity.party_id = activity_payload.get("party_id") updated_message.activity = activity else: - updated_message.activity = undefined.Undefined() + updated_message.activity = undefined.UNDEFINED updated_message.application = ( - self.deserialize_application(payload["application"]) if "application" in payload else undefined.Undefined() + self.deserialize_application(payload["application"]) if "application" in payload else undefined.UNDEFINED ) if (crosspost_payload := payload.get("message_reference", ...)) is not ...: @@ -1652,13 +1650,13 @@ def deserialize_message_update_event(self, payload: data_binding.JSONObject) -> ) updated_message.message_reference = crosspost else: - updated_message.message_reference = undefined.Undefined() + updated_message.message_reference = undefined.UNDEFINED # noinspection PyArgumentList updated_message.flags = ( - message_models.MessageFlag(payload["flags"]) if "flags" in payload else undefined.Undefined() + message_models.MessageFlag(payload["flags"]) if "flags" in payload else undefined.UNDEFINED ) - updated_message.nonce = payload["nonce"] if "nonce" in payload else undefined.Undefined() + updated_message.nonce = payload["nonce"] if "nonce" in payload else undefined.UNDEFINED message_update.message = updated_message return message_update diff --git a/hikari/impl/event_manager_core.py b/hikari/impl/event_manager_core.py index 4756dad1d5..9e5531cb6a 100644 --- a/hikari/impl/event_manager_core.py +++ b/hikari/impl/event_manager_core.py @@ -33,7 +33,6 @@ from hikari.net import gateway from hikari.utilities import aio from hikari.utilities import data_binding -from hikari.utilities import klass from hikari.utilities import reflect from hikari.utilities import undefined @@ -61,7 +60,7 @@ def __init__(self, app: app_.IApp) -> None: self._app = app self._listeners: _ListenerMapT = {} self._waiters: _WaiterMapT = {} - self.logger = klass.get_logger(self) + self.logger = reflect.get_logger(self) @property def app(self) -> app_.IApp: @@ -119,7 +118,7 @@ def unsubscribe( del self._listeners[event_type] def listen( - self, event_type: typing.Union[undefined.Undefined, typing.Type[_EventT]] = undefined.Undefined(), + self, event_type: typing.Union[undefined.UndefinedType, typing.Type[_EventT]] = undefined.UNDEFINED, ) -> typing.Callable[[_CallbackT], _CallbackT]: def decorator(callback: _CallbackT) -> _CallbackT: nonlocal event_type @@ -132,7 +131,7 @@ def decorator(callback: _CallbackT) -> _CallbackT: event_param = next(iter(params)) - if event_type is undefined.Undefined(): + if event_type is undefined.UNDEFINED: if event_param.annotation is event_param.empty: raise TypeError("Must provide the event type in the @listen decorator or as a type hint!") diff --git a/hikari/impl/gateway_zookeeper.py b/hikari/impl/gateway_zookeeper.py index a2ed4cf0f7..d56a5aee11 100644 --- a/hikari/impl/gateway_zookeeper.py +++ b/hikari/impl/gateway_zookeeper.py @@ -31,7 +31,6 @@ import typing from hikari.api import app as app_ -from hikari.api import event_dispatcher from hikari.events import other from hikari.net import gateway from hikari.utilities import undefined @@ -59,21 +58,21 @@ class AbstractGatewayZookeeper(app_.IGatewayZookeeper, abc.ABC): compression : bool Defaulting to `True`, if `True`, then zlib transport compression is used for each shard connection. If `False`, no compression is used. - config : hikari.utilities.undefined.Undefined or hikari.net.http_settings.HTTPSettings + config : hikari.utilities.undefined.UndefinedType or hikari.net.http_settings.HTTPSettings Optional aiohttp settings to apply to the created shards. debug : bool Defaulting to `False`, if `True`, then each payload sent and received on the gateway will be dumped to debug logs. This will provide useful debugging context at the cost of performance. Generally you do not need to enable this. - initial_activity : hikari.models.presences.Activity or None or hikari.utilities.undefined.Undefined + initial_activity : hikari.models.presences.Activity or None or hikari.utilities.undefined.UndefinedType The initial activity to have on each shard. - initial_activity : hikari.models.presences.Status or hikari.utilities.undefined.Undefined + initial_activity : hikari.models.presences.Status or hikari.utilities.undefined.UndefinedType The initial status to have on each shard. - initial_idle_since : datetime.datetime or None or hikari.utilities.undefined.Undefined + initial_idle_since : datetime.datetime or None or hikari.utilities.undefined.UndefinedType The initial time to show as being idle since, or `None` if not idle, for each shard. - initial_idle_since : bool or hikari.utilities.undefined.Undefined + initial_idle_since : bool or hikari.utilities.undefined.UndefinedType If `True`, each shard will appear as being AFK on startup. If `False`, each shard will appear as _not_ being AFK. intents : hikari.models.intents.Intent or None @@ -83,11 +82,11 @@ class AbstractGatewayZookeeper(app_.IGatewayZookeeper, abc.ABC): large_threshold : int The number of members that need to be in a guild for the guild to be considered large. Defaults to the maximum, which is `250`. - shard_ids : typing.Set[int] or undefined.Undefined + shard_ids : typing.Set[int] or undefined.UndefinedType A set of every shard ID that should be created and started on startup. If left undefined along with `shard_count`, then auto-sharding is used instead, which is the default. - shard_count : int or undefined.Undefined + shard_count : int or undefined.UndefinedType The number of shards in the entire application. If left undefined along with `shard_ids`, then auto-sharding is used instead, which is the default. @@ -132,10 +131,10 @@ def __init__( compression: bool, config: http_settings.HTTPSettings, debug: bool, - initial_activity: typing.Union[undefined.Undefined, presences.Activity, None] = undefined.Undefined(), - initial_idle_since: typing.Union[undefined.Undefined, datetime.datetime, None] = undefined.Undefined(), - initial_is_afk: typing.Union[undefined.Undefined, bool] = undefined.Undefined(), - initial_status: typing.Union[undefined.Undefined, presences.Status] = undefined.Undefined(), + initial_activity: typing.Union[undefined.UndefinedType, presences.Activity, None] = undefined.UNDEFINED, + initial_idle_since: typing.Union[undefined.UndefinedType, datetime.datetime, None] = undefined.UNDEFINED, + initial_is_afk: typing.Union[undefined.UndefinedType, bool] = undefined.UNDEFINED, + initial_status: typing.Union[undefined.UndefinedType, presences.Status] = undefined.UNDEFINED, intents: typing.Optional[intents_.Intent], large_threshold: int, shard_ids: typing.Set[int], @@ -143,9 +142,9 @@ def __init__( token: str, version: int, ) -> None: - if undefined.Undefined.count(shard_ids, shard_count) == 1: + if undefined.count(shard_ids, shard_count) == 1: raise TypeError("You must provide values for both shard_ids and shard_count, or neither.") - if not isinstance(shard_ids, undefined.Undefined): + if not shard_ids is undefined.UNDEFINED: if not shard_ids: raise ValueError("At least one shard ID must be specified if provided.") if not all(shard_id >= 0 for shard_id in shard_ids): @@ -292,10 +291,10 @@ def sigterm_handler(*_: typing.Any) -> None: async def update_presence( self, *, - status: typing.Union[undefined.Undefined, presences.Status] = undefined.Undefined(), - activity: typing.Union[undefined.Undefined, presences.Activity, None] = undefined.Undefined(), - idle_since: typing.Union[undefined.Undefined, datetime.datetime] = undefined.Undefined(), - is_afk: typing.Union[undefined.Undefined, bool] = undefined.Undefined(), + status: typing.Union[undefined.UndefinedType, presences.Status] = undefined.UNDEFINED, + activity: typing.Union[undefined.UndefinedType, presences.Activity, None] = undefined.UNDEFINED, + idle_since: typing.Union[undefined.UndefinedType, datetime.datetime] = undefined.UNDEFINED, + is_afk: typing.Union[undefined.UndefinedType, bool] = undefined.UNDEFINED, ) -> None: await asyncio.gather( *( diff --git a/hikari/impl/rest_app.py b/hikari/impl/rest_app.py index 596efcc769..e374da7522 100644 --- a/hikari/impl/rest_app.py +++ b/hikari/impl/rest_app.py @@ -35,7 +35,6 @@ from hikari.impl import entity_factory as entity_factory_impl from hikari.net import http_settings as http_settings_ from hikari.net import rest as rest_ -from hikari.utilities import klass from hikari.utilities import undefined if typing.TYPE_CHECKING: @@ -48,7 +47,7 @@ class RESTAppImpl(app_.IRESTApp): Parameters ---------- - config : hikari.utilities.undefined.Undefined or hikari.net.http_settings.HTTPSettings + config : hikari.utilities.undefined.UndefinedType or hikari.net.http_settings.HTTPSettings Optional aiohttp settings to apply to the REST components. If undefined, then sane defaults are used. debug : bool @@ -56,13 +55,13 @@ class RESTAppImpl(app_.IRESTApp): in HTTP requests will be dumped to debug logs. This will provide useful debugging context at the cost of performance. Generally you do not need to enable this. - token : hikari.utilities.undefined.Undefined or str + token : hikari.utilities.undefined.UndefinedType or str If defined, the token to use. If not defined, no token will be injected into the `Authorization` header for requests. - token_type : hikari.utilities.undefined.Undefined or str + token_type : hikari.utilities.undefined.UndefinedType or str The token type to use. If undefined, a default is used instead, which will be `Bot`. If no `token` is provided, this is ignored. - url : hikari.utilities.undefined.Undefined or str + url : hikari.utilities.undefined.UndefinedType or str The API URL to hit. Generally you can leave this undefined and use the default. version : int @@ -74,16 +73,16 @@ class RESTAppImpl(app_.IRESTApp): def __init__( self, - config: typing.Union[undefined.Undefined, http_settings_.HTTPSettings] = undefined.Undefined(), + config: typing.Union[undefined.UndefinedType, http_settings_.HTTPSettings] = undefined.UNDEFINED, debug: bool = False, - token: typing.Union[undefined.Undefined, str] = undefined.Undefined(), - token_type: typing.Union[undefined.Undefined, str] = undefined.Undefined(), - url: typing.Union[undefined.Undefined, str] = undefined.Undefined(), + token: typing.Union[undefined.UndefinedType, str] = undefined.UNDEFINED, + token_type: typing.Union[undefined.UndefinedType, str] = undefined.UNDEFINED, + url: typing.Union[undefined.UndefinedType, str] = undefined.UNDEFINED, version: int = 6, ) -> None: - self._logger = klass.get_logger(self) + self._logger = reflect.get_logger(self) - config = http_settings_.HTTPSettings() if isinstance(config, undefined.Undefined) else config + config = http_settings_.HTTPSettings() if config is undefined.UNDEFINED else config self._rest = rest_.REST( app=self, config=config, debug=debug, token=token, token_type=token_type, rest_url=url, version=version, diff --git a/hikari/models/channels.py b/hikari/models/channels.py index 3c2613f95f..162575635c 100644 --- a/hikari/models/channels.py +++ b/hikari/models/channels.py @@ -45,7 +45,6 @@ from hikari.models import bases from hikari.models import permissions from hikari.models import users - from hikari.utilities import cdn from hikari.utilities import files diff --git a/hikari/models/guilds.py b/hikari/models/guilds.py index ffb5006303..cea183c433 100644 --- a/hikari/models/guilds.py +++ b/hikari/models/guilds.py @@ -228,40 +228,40 @@ class Member(bases.Entity): This will be `None` when attached to Message Create and Update gateway events. """ - nickname: typing.Union[str, None, undefined.Undefined] = attr.ib( + nickname: typing.Union[str, None, undefined.UndefinedType] = attr.ib( eq=False, hash=False, repr=True, ) """This member's nickname. - This will be `None` if not set and `hikari.utilities.undefined.Undefined` + This will be `None` if not set and `hikari.utilities.undefined.UndefinedType` if it's state is unknown. """ role_ids: typing.Set[snowflake.Snowflake] = attr.ib(eq=False, hash=False, repr=False) """A sequence of the IDs of the member's current roles.""" - joined_at: typing.Union[datetime.datetime, undefined.Undefined] = attr.ib(eq=False, hash=False, repr=False) + joined_at: typing.Union[datetime.datetime, undefined.UndefinedType] = attr.ib(eq=False, hash=False, repr=False) """The datetime of when this member joined the guild they belong to.""" - premium_since: typing.Union[datetime.datetime, None, undefined.Undefined] = attr.ib( + premium_since: typing.Union[datetime.datetime, None, undefined.UndefinedType] = attr.ib( eq=False, hash=False, repr=False ) """The datetime of when this member started "boosting" this guild. This will be `None` if they aren't boosting and - `hikari.utilities.undefined.Undefined` if their boosting status is unknown. + `hikari.utilities.undefined.UndefinedType` if their boosting status is unknown. """ - is_deaf: typing.Union[bool, undefined.Undefined] = attr.ib(eq=False, hash=False, repr=False) + is_deaf: typing.Union[bool, undefined.UndefinedType] = attr.ib(eq=False, hash=False, repr=False) """Whether this member is deafened by this guild in it's voice channels. - This will be `hikari.utilities.undefined.Undefined if it's state is unknown. + This will be `hikari.utilities.undefined.UndefinedType if it's state is unknown. """ - is_mute: typing.Union[bool, undefined.Undefined] = attr.ib(eq=False, hash=False, repr=False) + is_mute: typing.Union[bool, undefined.UndefinedType] = attr.ib(eq=False, hash=False, repr=False) """Whether this member is muted by this guild in it's voice channels. - This will be `hikari.utilities.undefined.Undefined if it's state is unknown. + This will be `hikari.utilities.undefined.UndefinedType if it's state is unknown. """ diff --git a/hikari/models/messages.py b/hikari/models/messages.py index 22594bde4e..02edbf06e0 100644 --- a/hikari/models/messages.py +++ b/hikari/models/messages.py @@ -320,16 +320,16 @@ async def fetch_channel(self) -> channels.PartialChannel: async def edit( # pylint:disable=line-too-long self, - text: typing.Union[undefined.Undefined, str, None] = undefined.Undefined(), + text: typing.Union[undefined.UndefinedType, str, None] = undefined.UNDEFINED, *, - embed: typing.Union[undefined.Undefined, embeds_.Embed, None] = undefined.Undefined(), - mentions_everyone: typing.Union[bool, undefined.Undefined] = undefined.Undefined(), + embed: typing.Union[undefined.UndefinedType, embeds_.Embed, None] = undefined.UNDEFINED, + mentions_everyone: typing.Union[bool, undefined.UndefinedType] = undefined.UNDEFINED, user_mentions: typing.Union[ - typing.Collection[typing.Union[snowflake.Snowflake, int, str, users.User]], bool, undefined.Undefined - ] = undefined.Undefined(), + typing.Collection[typing.Union[snowflake.Snowflake, int, str, users.User]], bool, undefined.UndefinedType + ] = undefined.UNDEFINED, role_mentions: typing.Union[ - typing.Collection[typing.Union[snowflake.Snowflake, int, str, guilds.Role]], bool, undefined.Undefined - ] = undefined.Undefined(), + typing.Collection[typing.Union[snowflake.Snowflake, int, str, guilds.Role]], bool, undefined.UndefinedType + ] = undefined.UNDEFINED, ) -> Message: """Edit this message. @@ -338,10 +338,10 @@ async def edit( # pylint:disable=line-too-long Parameters ---------- - text : str or hikari.utilities.undefined.Undefined or None + text : str or hikari.utilities.undefined.UndefinedType or None If specified, the message text to set on the message. If `None`, then the content is removed if already present. - embed : hikari.models.embeds.Embed or hikari.utilities.undefined.Undefined or None + embed : hikari.models.embeds.Embed or hikari.utilities.undefined.UndefinedType or None If specified, the embed object to set on the message. If `None`, then the embed is removed if already present. mentions_everyone : bool @@ -393,10 +393,10 @@ async def edit( # pylint:disable=line-too-long async def reply( # pylint:disable=line-too-long self, - text: typing.Union[undefined.Undefined, str] = undefined.Undefined(), + text: typing.Union[undefined.UndefinedType, str] = undefined.UNDEFINED, *, - embed: typing.Union[undefined.Undefined, embeds_.Embed] = undefined.Undefined(), - attachments: typing.Union[undefined.Undefined, typing.Sequence[files_.Resource]] = undefined.Undefined(), + embed: typing.Union[undefined.UndefinedType, embeds_.Embed] = undefined.UNDEFINED, + attachments: typing.Union[undefined.UndefinedType, typing.Sequence[files_.Resource]] = undefined.UNDEFINED, mentions_everyone: bool = False, user_mentions: typing.Union[ typing.Collection[typing.Union[snowflake.Snowflake, int, str, users.User]], bool @@ -404,26 +404,26 @@ async def reply( # pylint:disable=line-too-long role_mentions: typing.Union[ typing.Collection[typing.Union[snowflake.Snowflake, int, str, guilds.Role]], bool ] = True, - nonce: typing.Union[undefined.Undefined, str] = undefined.Undefined(), - tts: typing.Union[undefined.Undefined, bool] = undefined.Undefined(), + nonce: typing.Union[undefined.UndefinedType, str] = undefined.UNDEFINED, + tts: typing.Union[undefined.UndefinedType, bool] = undefined.UNDEFINED, ) -> Message: """Create a message in the channel this message belongs to. Parameters ---------- - text : str or hikari.utilities.undefined.Undefined + text : str or hikari.utilities.undefined.UndefinedType If specified, the message text to send with the message. - nonce : str or hikari.utilities.undefined.Undefined + nonce : str or hikari.utilities.undefined.UndefinedType If specified, an optional ID to send for opportunistic message creation. This doesn't serve any real purpose for general use, and can usually be ignored. - tts : bool or hikari.utilities.undefined.Undefined + tts : bool or hikari.utilities.undefined.UndefinedType If specified, whether the message will be sent as a TTS message. - attachments : typing.Sequence[hikari.models.files.BaseStream] or hikari.utilities.undefined.Undefined + attachments : typing.Sequence[hikari.models.files.BaseStream] or hikari.utilities.undefined.UndefinedType If specified, a sequence of attachments to upload, if desired. Should be between 1 and 10 objects in size (inclusive), also including embed attachments. - embed : hikari.models.embeds.Embed or hikari.utilities.undefined.Undefined + embed : hikari.models.embeds.Embed or hikari.utilities.undefined.UndefinedType If specified, the embed object to send with the message. mentions_everyone : bool Whether `@everyone` and `@here` mentions should be resolved by @@ -531,7 +531,7 @@ async def remove_reaction( self, emoji: typing.Union[str, emojis_.Emoji], *, - user: typing.Union[users.User, undefined.Undefined] = undefined.Undefined(), + user: typing.Union[users.User, undefined.UndefinedType] = undefined.UNDEFINED, ) -> None: r"""Remove a reaction from this message. @@ -539,7 +539,7 @@ async def remove_reaction( ---------- emoji : str or hikari.models.emojis.Emoji The emoji to remove. - user : hikari.models.users.User or hikari.utilities.undefined.Undefined + user : hikari.models.users.User or hikari.utilities.undefined.UndefinedType The user of the reaction to remove. If unspecified, then the bot's reaction is removed instead. @@ -573,19 +573,19 @@ async def remove_reaction( If the channel or message is not found, or if the emoji is not found. """ - if isinstance(user, undefined.Undefined): + if user is undefined.UNDEFINED: await self._app.rest.delete_my_reaction(channel=self.channel_id, message=self.id, emoji=emoji) else: await self._app.rest.delete_reaction(channel=self.channel_id, message=self.id, emoji=emoji, user=user) async def remove_all_reactions( - self, emoji: typing.Union[str, emojis_.Emoji, undefined.Undefined] = undefined.Undefined() + self, emoji: typing.Union[str, emojis_.Emoji, undefined.UndefinedType] = undefined.UNDEFINED ) -> None: r"""Remove all users' reactions for a specific emoji from the message. Parameters ---------- - emoji : str or hikari.models.emojis.Emoji or hikari.utilities.undefined.Undefined + emoji : str or hikari.models.emojis.Emoji or hikari.utilities.undefined.UndefinedType The emoji to remove all reactions for. If not specified, then all emojis are removed. @@ -611,7 +611,7 @@ async def remove_all_reactions( If any invalid snowflake IDs are passed; a snowflake may be invalid due to it being outside of the range of a 64 bit integer. """ - if isinstance(emoji, undefined.Undefined): + if emoji is undefined.UNDEFINED: await self._app.rest.delete_all_reactions(channel=self.channel_id, message=self.id) else: await self._app.rest.delete_all_reactions_for_emoji(channel=self.channel_id, message=self.id, emoji=emoji) diff --git a/hikari/models/users.py b/hikari/models/users.py index 71d047e9ea..89f3fa0d18 100644 --- a/hikari/models/users.py +++ b/hikari/models/users.py @@ -102,22 +102,22 @@ class PartialUser(bases.Entity, bases.Unique): present. """ - discriminator: typing.Union[str, undefined.Undefined] = attr.ib(eq=False, hash=False, repr=True) + discriminator: typing.Union[str, undefined.UndefinedType] = attr.ib(eq=False, hash=False, repr=True) """This user's discriminator.""" - username: typing.Union[str, undefined.Undefined] = attr.ib(eq=False, hash=False, repr=True) + username: typing.Union[str, undefined.UndefinedType] = attr.ib(eq=False, hash=False, repr=True) """This user's username.""" - avatar_hash: typing.Union[None, str, undefined.Undefined] = attr.ib(eq=False, hash=False, repr=False) + avatar_hash: typing.Union[None, str, undefined.UndefinedType] = attr.ib(eq=False, hash=False, repr=False) """This user's avatar hash, if set.""" - is_bot: typing.Union[bool, undefined.Undefined] = attr.ib(eq=False, hash=False, repr=False) + is_bot: typing.Union[bool, undefined.UndefinedType] = attr.ib(eq=False, hash=False, repr=False) """Whether this user is a bot account.""" - is_system: typing.Union[bool, undefined.Undefined] = attr.ib(eq=False, hash=False) + is_system: typing.Union[bool, undefined.UndefinedType] = attr.ib(eq=False, hash=False) """Whether this user is a system account.""" - flags: typing.Union[UserFlag, undefined.Undefined] = attr.ib(eq=False, hash=False) + flags: typing.Union[UserFlag, undefined.UndefinedType] = attr.ib(eq=False, hash=False) """The public flags for this user.""" diff --git a/hikari/models/webhooks.py b/hikari/models/webhooks.py index 5520fd98bf..9b4d1e2bcf 100644 --- a/hikari/models/webhooks.py +++ b/hikari/models/webhooks.py @@ -94,13 +94,13 @@ class Webhook(bases.Entity, bases.Unique): async def execute( self, - text: typing.Union[undefined.Undefined, typing.Any] = undefined.Undefined(), + text: typing.Union[undefined.UndefinedType, typing.Any] = undefined.UNDEFINED, *, - username: typing.Union[undefined.Undefined, str] = undefined.Undefined(), - avatar_url: typing.Union[undefined.Undefined, str] = undefined.Undefined(), - tts: typing.Union[undefined.Undefined, bool] = undefined.Undefined(), - attachments: typing.Union[undefined.Undefined, typing.Sequence[files_.Resource]] = undefined.Undefined(), - embeds: typing.Union[undefined.Undefined, typing.Sequence[embeds_.Embed]] = undefined.Undefined(), + username: typing.Union[undefined.UndefinedType, str] = undefined.UNDEFINED, + avatar_url: typing.Union[undefined.UndefinedType, str] = undefined.UNDEFINED, + tts: typing.Union[undefined.UndefinedType, bool] = undefined.UNDEFINED, + attachments: typing.Union[undefined.UndefinedType, typing.Sequence[files_.Resource]] = undefined.UNDEFINED, + embeds: typing.Union[undefined.UndefinedType, typing.Sequence[embeds_.Embed]] = undefined.UNDEFINED, mentions_everyone: bool = True, user_mentions: typing.Union[ typing.Collection[typing.Union[snowflake.Snowflake, int, str, users_.User]], bool @@ -113,19 +113,19 @@ async def execute( Parameters ---------- - text : str or hikari.utilities.undefined.Undefined + text : str or hikari.utilities.undefined.UndefinedType If specified, the message content to send with the message. - username : str or hikari.utilities.undefined.Undefined + username : str or hikari.utilities.undefined.UndefinedType If specified, the username to override the webhook's username for this request. - avatar_url : str or hikari.utilities.undefined.Undefined + avatar_url : str or hikari.utilities.undefined.UndefinedType If specified, the url of an image to override the webhook's avatar with for this request. - tts : bool or hikari.utilities.undefined.Undefined + tts : bool or hikari.utilities.undefined.UndefinedType If specified, whether the message will be sent as a TTS message. - attachments : typing.Sequence[hikari.utilities.files.BaseStream] or hikari.utilities.undefined.Undefined + attachments : typing.Sequence[hikari.utilities.files.BaseStream] or hikari.utilities.undefined.UndefinedType If specified, a sequence of attachments to upload. - embeds : typing.Sequence[hikari.models.embeds.Embed] or hikari.utilities.undefined.Undefined + embeds : typing.Sequence[hikari.models.embeds.Embed] or hikari.utilities.undefined.UndefinedType If specified, a sequence of between `1` to `10` embed objects (inclusive) to send with the embed. mentions_everyone : bool @@ -179,12 +179,12 @@ async def execute( role_mentions=role_mentions, ) - async def delete(self, *, use_token: typing.Union[undefined.Undefined, bool] = undefined.Undefined()) -> None: + async def delete(self, *, use_token: typing.Union[undefined.UndefinedType, bool] = undefined.UNDEFINED) -> None: """Delete this webhook. Parameters ---------- - use_token : bool or hikari.utilities.undefined.Undefined + use_token : bool or hikari.utilities.undefined.UndefinedType If set to `True` then the webhook's token will be used for this request; if set to `False` then bot authorization will be used; if not specified then the webhook's token will be used for the @@ -203,41 +203,43 @@ async def delete(self, *, use_token: typing.Union[undefined.Undefined, bool] = u if use_token and self.token is None: raise ValueError("This webhook's token is unknown, so cannot be used.") - token: typing.Union[undefined.Undefined, str] + token: typing.Union[undefined.UndefinedType, str] if use_token: token = typing.cast(str, self.token) else: - token = undefined.Undefined() + token = undefined.UNDEFINED await self._app.rest.delete_webhook(self.id, token=token) async def edit( self, *, - name: typing.Union[undefined.Undefined, str] = undefined.Undefined(), - avatar: typing.Union[undefined.Undefined, None, files_.Resource] = undefined.Undefined(), - channel: typing.Union[undefined.Undefined, bases.UniqueObject, channels_.GuildChannel] = undefined.Undefined(), - reason: typing.Union[undefined.Undefined, str] = undefined.Undefined(), - use_token: typing.Union[undefined.Undefined, bool] = undefined.Undefined(), + name: typing.Union[undefined.UndefinedType, str] = undefined.UNDEFINED, + avatar: typing.Union[undefined.UndefinedType, None, files_.Resource] = undefined.UNDEFINED, + channel: typing.Union[ + undefined.UndefinedType, bases.UniqueObject, channels_.GuildChannel + ] = undefined.UNDEFINED, + reason: typing.Union[undefined.UndefinedType, str] = undefined.UNDEFINED, + use_token: typing.Union[undefined.UndefinedType, bool] = undefined.UNDEFINED, ) -> Webhook: """Edit this webhook. Parameters ---------- - name : str or hikari.utilities.undefined.Undefined + name : str or hikari.utilities.undefined.UndefinedType If specified, the new name string. - avatar : hikari.utilities.files.Resource or None or hikari.utilities.undefined.Undefined + avatar : hikari.utilities.files.Resource or None or hikari.utilities.undefined.UndefinedType If specified, the new avatar image. If `None`, then it is removed. If not specified, nothing is changed. - channel : hikari.models.channels.GuildChannel or hikari.models.bases.UniqueObject or hikari.utilities.undefined.Undefined + channel : hikari.models.channels.GuildChannel or hikari.models.bases.UniqueObject or hikari.utilities.undefined.UndefinedType If specified, the object or ID of the new channel the given webhook should be moved to. - reason : str or hikari.utilities.undefined.Undefined + reason : str or hikari.utilities.undefined.UndefinedType If specified, the audit log reason explaining why the operation was performed. This field will be used when using the webhook's token rather than bot authorization. - use_token : bool or hikari.utilities.undefined.Undefined + use_token : bool or hikari.utilities.undefined.UndefinedType If set to `True` then the webhook's token will be used for this request; if set to `False` then bot authorization will be used; if not specified then the webhook's token will be used for the @@ -266,12 +268,12 @@ async def edit( if use_token and self.token is None: raise ValueError("This webhook's token is unknown, so cannot be used.") - token: typing.Union[undefined.Undefined, str] + token: typing.Union[undefined.UndefinedType, str] if use_token: token = typing.cast(str, self.token) else: - token = undefined.Undefined() + token = undefined.UNDEFINED return await self._app.rest.edit_webhook( self.id, token=token, name=name, avatar=avatar, channel=channel, reason=reason, @@ -295,13 +297,13 @@ async def fetch_channel(self) -> channels_.PartialChannel: return await self._app.rest.fetch_channel(self.channel_id) async def fetch_self( - self, *, use_token: typing.Union[undefined.Undefined, bool] = undefined.Undefined() + self, *, use_token: typing.Union[undefined.UndefinedType, bool] = undefined.UNDEFINED ) -> Webhook: """Fetch this webhook. Parameters ---------- - use_token : bool or hikari.utilities.undefined.Undefined + use_token : bool or hikari.utilities.undefined.UndefinedType If set to `True` then the webhook's token will be used for this request; if set to `False` then bot authorization will be used; if not specified then the webhook's token will be used for the @@ -330,12 +332,12 @@ async def fetch_self( if use_token and not self.token: raise ValueError("This webhook's token is unknown, so cannot be used.") - token: typing.Union[undefined.Undefined, str] + token: typing.Union[undefined.UndefinedType, str] if use_token: token = typing.cast(str, self.token) else: - token = undefined.Undefined() + token = undefined.UNDEFINED return await self._app.rest.fetch_webhook(self.id, token=token) diff --git a/hikari/net/gateway.py b/hikari/net/gateway.py index cda5caa48a..b906115c54 100644 --- a/hikari/net/gateway.py +++ b/hikari/net/gateway.py @@ -41,7 +41,7 @@ from hikari.net import rate_limits from hikari.net import strings from hikari.utilities import data_binding -from hikari.utilities import klass +from hikari.utilities import reflect from hikari.utilities import undefined if typing.TYPE_CHECKING: @@ -67,13 +67,13 @@ class Gateway(http_client.HTTPClient, component.IComponent): debug : bool If `True`, each sent and received payload is dumped to the logs. If `False`, only the fact that data has been sent/received will be logged. - initial_activity : hikari.models.presences.Activity or None or hikari.utilities.undefined.Undefined + initial_activity : hikari.models.presences.Activity or None or hikari.utilities.undefined.UndefinedType The initial activity to appear to have for this shard. - initial_idle_since : datetime.datetime or None or hikari.utilities.undefined.Undefined + initial_idle_since : datetime.datetime or None or hikari.utilities.undefined.UndefinedType The datetime to appear to be idle since. - initial_is_afk : bool or hikari.utilities.undefined.Undefined + initial_is_afk : bool or hikari.utilities.undefined.UndefinedType Whether to appear to be AFK or not on login. - initial_status : hikari.models.presences.Status or hikari.utilities.undefined.Undefined + initial_status : hikari.models.presences.Status or hikari.utilities.undefined.UndefinedType The initial status to set on login for the shard. intents : hikari.models.intents.Intent or None Collection of intents to use, or `None` to not use intents at all. @@ -161,10 +161,10 @@ def __init__( app: app_.IGatewayConsumer, config: http_settings.HTTPSettings, debug: bool = False, - initial_activity: typing.Union[undefined.Undefined, None, presences.Activity] = undefined.Undefined(), - initial_idle_since: typing.Union[undefined.Undefined, None, datetime.datetime] = undefined.Undefined(), - initial_is_afk: typing.Union[undefined.Undefined, bool] = undefined.Undefined(), - initial_status: typing.Union[undefined.Undefined, presences.Status] = undefined.Undefined(), + initial_activity: typing.Union[undefined.UndefinedType, None, presences.Activity] = undefined.UNDEFINED, + initial_idle_since: typing.Union[undefined.UndefinedType, None, datetime.datetime] = undefined.UNDEFINED, + initial_is_afk: typing.Union[undefined.UndefinedType, bool] = undefined.UNDEFINED, + initial_status: typing.Union[undefined.UndefinedType, presences.Status] = undefined.UNDEFINED, intents: typing.Optional[intents_.Intent] = None, large_threshold: int = 250, shard_id: int = 0, @@ -178,7 +178,7 @@ def __init__( allow_redirects=config.allow_redirects, connector=config.tcp_connector_factory() if config.tcp_connector_factory else None, debug=debug, - logger=klass.get_logger(self, str(shard_id)), + logger=reflect.get_logger(self, str(shard_id)), proxy_auth=config.proxy_auth, proxy_headers=config.proxy_headers, proxy_url=config.proxy_url, @@ -187,19 +187,19 @@ def __init__( timeout=config.request_timeout, trust_env=config.trust_env, ) - self._activity: typing.Union[undefined.Undefined, None, presences.Activity] = initial_activity + self._activity: typing.Union[undefined.UndefinedType, None, presences.Activity] = initial_activity self._app = app self._backoff = rate_limits.ExponentialBackOff(base=1.85, maximum=600, initial_increment=2) self._handshake_event = asyncio.Event() - self._idle_since: typing.Union[undefined.Undefined, None, datetime.datetime] = initial_idle_since + self._idle_since: typing.Union[undefined.UndefinedType, None, datetime.datetime] = initial_idle_since self._intents: typing.Optional[intents_.Intent] = intents - self._is_afk: typing.Union[undefined.Undefined, bool] = initial_is_afk + self._is_afk: typing.Union[undefined.UndefinedType, bool] = initial_is_afk self._last_run_started_at = float("nan") self._request_close_event = asyncio.Event() self._seq: typing.Optional[str] = None self._shard_id: int = shard_id self._shard_count: int = shard_count - self._status: typing.Union[undefined.Undefined, presences.Status] = initial_status + self._status: typing.Union[undefined.UndefinedType, presences.Status] = initial_status self._token = token self._use_compression = use_compression self._version = version @@ -383,34 +383,34 @@ async def _run_once(self) -> bool: async def update_presence( self, *, - idle_since: typing.Union[undefined.Undefined, typing.Optional[datetime.datetime]] = undefined.Undefined(), - is_afk: typing.Union[undefined.Undefined, bool] = undefined.Undefined(), - activity: typing.Union[undefined.Undefined, typing.Optional[presences.Activity]] = undefined.Undefined(), - status: typing.Union[undefined.Undefined, presences.Status] = undefined.Undefined(), + idle_since: typing.Union[undefined.UndefinedType, typing.Optional[datetime.datetime]] = undefined.UNDEFINED, + is_afk: typing.Union[undefined.UndefinedType, bool] = undefined.UNDEFINED, + activity: typing.Union[undefined.UndefinedType, typing.Optional[presences.Activity]] = undefined.UNDEFINED, + status: typing.Union[undefined.UndefinedType, presences.Status] = undefined.UNDEFINED, ) -> None: """Update the presence of the shard user. Parameters ---------- - idle_since : datetime.datetime or None or hikari.utilities.undefined.Undefined + idle_since : datetime.datetime or None or hikari.utilities.undefined.UndefinedType The datetime that the user started being idle. If undefined, this will not be changed. - is_afk : bool or hikari.utilities.undefined.Undefined + is_afk : bool or hikari.utilities.undefined.UndefinedType If `True`, the user is marked as AFK. If `False`, the user is marked as being active. If undefined, this will not be changed. - activity : hikari.models.presences.Activity or None or hikari.utilities.undefined.Undefined + activity : hikari.models.presences.Activity or None or hikari.utilities.undefined.UndefinedType The activity to appear to be playing. If undefined, this will not be changed. - status : hikari.models.presences.Status or hikari.utilities.undefined.Undefined + status : hikari.models.presences.Status or hikari.utilities.undefined.UndefinedType The web status to show. If undefined, this will not be changed. """ presence = self._build_presence_payload(idle_since=idle_since, is_afk=is_afk, status=status, activity=activity) payload: data_binding.JSONObject = {"op": self._GatewayOpcode.PRESENCE_UPDATE, "d": presence} await self._send_json(payload) - self._idle_since = idle_since if not isinstance(idle_since, undefined.Undefined) else self._idle_since - self._is_afk = is_afk if not isinstance(is_afk, undefined.Undefined) else self._is_afk - self._activity = activity if not isinstance(activity, undefined.Undefined) else self._activity - self._status = status if not isinstance(status, undefined.Undefined) else self._status + self._idle_since = idle_since if not idle_since is undefined.UNDEFINED else self._idle_since + self._is_afk = is_afk if not is_afk is undefined.UNDEFINED else self._is_afk + self._activity = activity if not activity is undefined.UNDEFINED else self._activity + self._status = status if not status is undefined.UNDEFINED else self._status async def update_voice_state( self, @@ -496,7 +496,7 @@ async def _handshake(self) -> None: if self._intents is not None: payload["d"]["intents"] = self._intents - if undefined.Undefined.count(self._activity, self._status, self._idle_since, self._is_afk) != 4: + if undefined.count(self._activity, self._status, self._idle_since, self._is_afk) != 4: # noinspection PyTypeChecker payload["d"]["presence"] = self._build_presence_payload() @@ -666,22 +666,22 @@ def _log_debug_payload(self, payload: str, message: str, *args: typing.Any) -> N def _build_presence_payload( self, - idle_since: typing.Union[undefined.Undefined, typing.Optional[datetime.datetime]] = undefined.Undefined(), - is_afk: typing.Union[undefined.Undefined, bool] = undefined.Undefined(), - status: typing.Union[undefined.Undefined, presences.Status] = undefined.Undefined(), - activity: typing.Union[undefined.Undefined, typing.Optional[presences.Activity]] = undefined.Undefined(), + idle_since: typing.Union[undefined.UndefinedType, typing.Optional[datetime.datetime]] = undefined.UNDEFINED, + is_afk: typing.Union[undefined.UndefinedType, bool] = undefined.UNDEFINED, + status: typing.Union[undefined.UndefinedType, presences.Status] = undefined.UNDEFINED, + activity: typing.Union[undefined.UndefinedType, typing.Optional[presences.Activity]] = undefined.UNDEFINED, ) -> data_binding.JSONObject: - if isinstance(idle_since, undefined.Undefined): + if idle_since is undefined.UNDEFINED: idle_since = self._idle_since - if isinstance(is_afk, undefined.Undefined): + if is_afk is undefined.UNDEFINED: is_afk = self._is_afk - if isinstance(status, undefined.Undefined): + if status is undefined.UNDEFINED: status = self._status - if isinstance(activity, undefined.Undefined): + if activity is undefined.UNDEFINED: activity = self._activity - if activity is not None and not isinstance(activity, undefined.Undefined): - game: typing.Union[undefined.Undefined, None, data_binding.JSONObject] = { + if activity is not None and not activity is undefined.UNDEFINED: + game: typing.Union[undefined.UndefinedType, None, data_binding.JSONObject] = { "name": activity.name, "url": activity.url, "type": activity.type, diff --git a/hikari/net/http_client.py b/hikari/net/http_client.py index 1502d28d88..5c39dfa2c1 100644 --- a/hikari/net/http_client.py +++ b/hikari/net/http_client.py @@ -36,7 +36,6 @@ from hikari.net import tracing from hikari.utilities import data_binding - try: # noinspection PyProtectedMember RequestContextManager = aiohttp.client._RequestContextManager diff --git a/hikari/net/iterators.py b/hikari/net/iterators.py index 2ab53b7ec0..188a425e27 100644 --- a/hikari/net/iterators.py +++ b/hikari/net/iterators.py @@ -406,8 +406,8 @@ def __init__( ], guild_id: str, before: str, - user_id: typing.Union[str, undefined.Undefined], - action_type: typing.Union[int, undefined.Undefined], + user_id: typing.Union[str, undefined.UndefinedType], + action_type: typing.Union[int, undefined.UndefinedType], ) -> None: self._action_type = action_type self._app = app diff --git a/hikari/net/rest.py b/hikari/net/rest.py index d5df1fb378..791b5edf8a 100644 --- a/hikari/net/rest.py +++ b/hikari/net/rest.py @@ -33,6 +33,7 @@ from hikari import errors from hikari.api import component +from hikari.models import emojis from hikari.net import buckets from hikari.net import http_client from hikari.net import http_settings @@ -41,11 +42,10 @@ from hikari.net import rest_utils from hikari.net import routes from hikari.net import strings -from hikari.models import emojis from hikari.utilities import data_binding from hikari.utilities import date from hikari.utilities import files -from hikari.utilities import klass +from hikari.utilities import reflect from hikari.utilities import snowflake from hikari.utilities import undefined @@ -90,10 +90,10 @@ class REST(http_client.HTTPClient, component.IComponent): # pylint:disable=too- information useful for debugging this application. These logs will be written as DEBUG log entries. For most purposes, this should be left `False`. - token : str or hikari.utilities.undefined.Undefined + token : str or hikari.utilities.undefined.UndefinedType The bot or bearer token. If no token is to be used, this can be undefined. - token_type : str or hikari.utilities.undefined.Undefined + token_type : str or hikari.utilities.undefined.UndefinedType The type of token in use. If no token is used, this can be ignored and left to the default value. This can be `"Bot"` or `"Bearer"`. The default if not provided will be `"Bot"`. @@ -115,16 +115,16 @@ def __init__( app: app_.IRESTApp, config: http_settings.HTTPSettings, debug: bool = False, - token: typing.Union[undefined.Undefined, str] = undefined.Undefined(), - token_type: typing.Union[undefined.Undefined, str] = undefined.Undefined(), - rest_url: typing.Union[undefined.Undefined, str] = undefined.Undefined(), + token: typing.Union[undefined.UndefinedType, str] = undefined.UNDEFINED, + token_type: typing.Union[undefined.UndefinedType, str] = undefined.UNDEFINED, + rest_url: typing.Union[undefined.UndefinedType, str] = undefined.UNDEFINED, version: int, ) -> None: super().__init__( allow_redirects=config.allow_redirects, connector=config.tcp_connector_factory() if config.tcp_connector_factory else None, debug=debug, - logger=klass.get_logger(self), + logger=reflect.get_logger(self), proxy_auth=config.proxy_auth, proxy_headers=config.proxy_headers, proxy_url=config.proxy_url, @@ -139,17 +139,17 @@ def __init__( self._app = app - if isinstance(token, undefined.Undefined): + if token is undefined.UNDEFINED: full_token = None else: - if isinstance(token_type, undefined.Undefined): + if token_type is undefined.UNDEFINED: token_type = strings.BOT_TOKEN full_token = f"{token_type.title()} {token}" self._token: typing.Optional[str] = full_token - if isinstance(rest_url, undefined.Undefined): + if rest_url is undefined.UNDEFINED: rest_url = strings.REST_API_URL self._rest_url = rest_url.format(self) @@ -162,11 +162,11 @@ async def _request( self, compiled_route: routes.CompiledRoute, *, - query: typing.Union[undefined.Undefined, None, data_binding.StringMapBuilder] = undefined.Undefined(), + query: typing.Union[undefined.UndefinedType, None, data_binding.StringMapBuilder] = undefined.UNDEFINED, body: typing.Union[ - undefined.Undefined, None, aiohttp.FormData, data_binding.JSONObjectBuilder, data_binding.JSONArray - ] = undefined.Undefined(), - reason: typing.Union[undefined.Undefined, str] = undefined.Undefined(), + undefined.UndefinedType, None, aiohttp.FormData, data_binding.JSONObjectBuilder, data_binding.JSONArray + ] = undefined.UNDEFINED, + reason: typing.Union[undefined.UndefinedType, str] = undefined.UNDEFINED, no_auth: bool = False, ) -> typing.Union[None, data_binding.JSONObject, data_binding.JSONArray]: # Make a ratelimit-protected HTTP request to a JSON endpoint and expect some form @@ -188,12 +188,12 @@ async def _request( if self._token is not None and not no_auth: headers[strings.AUTHORIZATION_HEADER] = self._token - if isinstance(body, undefined.Undefined): + if body is undefined.UNDEFINED: body = None headers.put(strings.X_AUDIT_LOG_REASON_HEADER, reason) - if isinstance(query, undefined.Undefined): + if query is undefined.UNDEFINED: query = None while True: @@ -330,14 +330,14 @@ async def _parse_ratelimits(self, compiled_route: routes.CompiledRoute, response @staticmethod def _generate_allowed_mentions( - mentions_everyone: typing.Union[undefined.Undefined, bool], + mentions_everyone: typing.Union[undefined.UndefinedType, bool], user_mentions: typing.Union[ - undefined.Undefined, typing.Collection[typing.Union[bases.UniqueObject, users.User]], bool + undefined.UndefinedType, typing.Collection[typing.Union[bases.UniqueObject, users.User]], bool ], role_mentions: typing.Union[ - undefined.Undefined, typing.Collection[typing.Union[bases.UniqueObject, guilds.Role]], bool + undefined.UndefinedType, typing.Collection[typing.Union[bases.UniqueObject, guilds.Role]], bool ], - ) -> typing.Union[undefined.Undefined, data_binding.JSONObject]: + ) -> typing.Union[undefined.UndefinedType, data_binding.JSONObject]: parsed_mentions = [] allowed_mentions = {} @@ -358,7 +358,7 @@ def _generate_allowed_mentions( allowed_mentions["roles"] = list(snowflakes) if not parsed_mentions and not allowed_mentions: - return undefined.Undefined() + return undefined.UNDEFINED if parsed_mentions: allowed_mentions["parse"] = parsed_mentions @@ -410,20 +410,20 @@ async def edit_channel( channel: typing.Union[channels.PartialChannel, bases.UniqueObject], /, *, - name: typing.Union[undefined.Undefined, str] = undefined.Undefined(), - position: typing.Union[undefined.Undefined, int] = undefined.Undefined(), - topic: typing.Union[undefined.Undefined, str] = undefined.Undefined(), - nsfw: typing.Union[undefined.Undefined, bool] = undefined.Undefined(), - bitrate: typing.Union[undefined.Undefined, int] = undefined.Undefined(), - user_limit: typing.Union[undefined.Undefined, int] = undefined.Undefined(), - rate_limit_per_user: typing.Union[undefined.Undefined, date.TimeSpan] = undefined.Undefined(), + name: typing.Union[undefined.UndefinedType, str] = undefined.UNDEFINED, + position: typing.Union[undefined.UndefinedType, int] = undefined.UNDEFINED, + topic: typing.Union[undefined.UndefinedType, str] = undefined.UNDEFINED, + nsfw: typing.Union[undefined.UndefinedType, bool] = undefined.UNDEFINED, + bitrate: typing.Union[undefined.UndefinedType, int] = undefined.UNDEFINED, + user_limit: typing.Union[undefined.UndefinedType, int] = undefined.UNDEFINED, + rate_limit_per_user: typing.Union[undefined.UndefinedType, date.TimeSpan] = undefined.UNDEFINED, permission_overwrites: typing.Union[ - undefined.Undefined, typing.Sequence[channels.PermissionOverwrite] - ] = undefined.Undefined(), + undefined.UndefinedType, typing.Sequence[channels.PermissionOverwrite] + ] = undefined.UNDEFINED, parent_category: typing.Union[ - undefined.Undefined, channels.GuildCategory, bases.UniqueObject - ] = undefined.Undefined(), - reason: typing.Union[undefined.Undefined, str] = undefined.Undefined(), + undefined.UndefinedType, channels.GuildCategory, bases.UniqueObject + ] = undefined.UNDEFINED, + reason: typing.Union[undefined.UndefinedType, str] = undefined.UNDEFINED, ) -> channels.PartialChannel: """Edit a channel. @@ -432,26 +432,26 @@ async def edit_channel( channel : hikari.models.channels.PartialChannel or hikari.utilities.snowflake.Snowflake or int or str The channel to edit. This may be a channel object, or the ID of an existing channel. - name : hikari.utilities.undefined.Undefined or str + name : hikari.utilities.undefined.UndefinedType or str If provided, the new name for the channel. - position : hikari.utilities.undefined.Undefined or int + position : hikari.utilities.undefined.UndefinedType or int If provided, the new position for the channel. - topic : hikari.utilities.undefined.Undefined or str + topic : hikari.utilities.undefined.UndefinedType or str If provided, the new topic for the channel. - nsfw : hikari.utilities.undefined.Undefined or bool + nsfw : hikari.utilities.undefined.UndefinedType or bool If provided, whether the channel should be marked as NSFW or not. - bitrate : hikari.utilities.undefined.Undefined or int + bitrate : hikari.utilities.undefined.UndefinedType or int If provided, the new bitrate for the channel. - user_limit : hikari.utilities.undefined.Undefined or int + user_limit : hikari.utilities.undefined.UndefinedType or int If provided, the new user limit in the channel. - rate_limit_per_user : hikari.utilities.undefined.Undefined or datetime.timedelta or float or int + rate_limit_per_user : hikari.utilities.undefined.UndefinedType or datetime.timedelta or float or int If provided, the new rate limit per user in the channel. - permission_overwrites : hikari.utilities.undefined.Undefined or typing.Sequence[hikari.models.channels.PermissionOverwrite] + permission_overwrites : hikari.utilities.undefined.UndefinedType or typing.Sequence[hikari.models.channels.PermissionOverwrite] If provided, the new permission overwrites for the channel. - parent_category : hikari.utilities.undefined.Undefined or hikari.models.channels.GuildCategory or hikari.utilities.snowflake.Snowflake or int or str + parent_category : hikari.utilities.undefined.UndefinedType or hikari.models.channels.GuildCategory or hikari.utilities.snowflake.Snowflake or int or str If provided, the new guild category for the channel. This may be a category object, or the ID of an existing category. - reason : hikari.utilities.undefined.Undefined or str + reason : hikari.utilities.undefined.UndefinedType or str If provided, the reason that will be recorded in the audit logs. Returns @@ -525,9 +525,9 @@ async def edit_permission_overwrites( channel: typing.Union[channels.GuildChannel, bases.UniqueObject], target: typing.Union[channels.PermissionOverwrite, users.User, guilds.Role], *, - allow: typing.Union[undefined.Undefined, permissions_.Permission] = undefined.Undefined(), - deny: typing.Union[undefined.Undefined, permissions_.Permission] = undefined.Undefined(), - reason: typing.Union[undefined.Undefined, str] = undefined.Undefined(), + allow: typing.Union[undefined.UndefinedType, permissions_.Permission] = undefined.UNDEFINED, + deny: typing.Union[undefined.UndefinedType, permissions_.Permission] = undefined.UNDEFINED, + reason: typing.Union[undefined.UndefinedType, str] = undefined.UNDEFINED, ) -> None: """Edit permissions for a target entity.""" @@ -538,9 +538,9 @@ async def edit_permission_overwrites( target: typing.Union[int, str, snowflake.Snowflake], *, target_type: typing.Union[channels.PermissionOverwriteType, str], - allow: typing.Union[undefined.Undefined, permissions_.Permission] = undefined.Undefined(), - deny: typing.Union[undefined.Undefined, permissions_.Permission] = undefined.Undefined(), - reason: typing.Union[undefined.Undefined, str] = undefined.Undefined(), + allow: typing.Union[undefined.UndefinedType, permissions_.Permission] = undefined.UNDEFINED, + deny: typing.Union[undefined.UndefinedType, permissions_.Permission] = undefined.UNDEFINED, + reason: typing.Union[undefined.UndefinedType, str] = undefined.UNDEFINED, ) -> None: """Edit permissions for a given entity ID and type.""" @@ -549,10 +549,10 @@ async def edit_permission_overwrites( channel: typing.Union[channels.GuildChannel, bases.UniqueObject], target: typing.Union[bases.UniqueObject, users.User, guilds.Role, channels.PermissionOverwrite], *, - target_type: typing.Union[undefined.Undefined, channels.PermissionOverwriteType, str] = undefined.Undefined(), - allow: typing.Union[undefined.Undefined, permissions_.Permission] = undefined.Undefined(), - deny: typing.Union[undefined.Undefined, permissions_.Permission] = undefined.Undefined(), - reason: typing.Union[undefined.Undefined, str] = undefined.Undefined(), + target_type: typing.Union[undefined.UndefinedType, channels.PermissionOverwriteType, str] = undefined.UNDEFINED, + allow: typing.Union[undefined.UndefinedType, permissions_.Permission] = undefined.UNDEFINED, + deny: typing.Union[undefined.UndefinedType, permissions_.Permission] = undefined.UNDEFINED, + reason: typing.Union[undefined.UndefinedType, str] = undefined.UNDEFINED, ) -> None: """Edit permissions for a specific entity in the given guild channel. @@ -564,14 +564,14 @@ async def edit_permission_overwrites( target : hikari.models.users.User or hikari.models.guilds.Role or hikari.models.channels.PermissionOverwrite or hikari.utilities.snowflake.Snowflake or int or str The channel overwrite to edit. This may be a overwrite object, or the ID of an existing channel. - target_type : hikari.utilities.undefined.Undefined or hikari.models.channels.PermissionOverwriteType or str + target_type : hikari.utilities.undefined.UndefinedType or hikari.models.channels.PermissionOverwriteType or str If provided, the type of the target to update. If unset, will attempt to get the type from `target`. - allow : hikari.utilities.undefined.Undefined or hikari.models.permissions.Permission + allow : hikari.utilities.undefined.UndefinedType or hikari.models.permissions.Permission If provided, the new vale of all allowed permissions. - deny : hikari.utilities.undefined.Undefined or hikari.models.permissions.Permission + deny : hikari.utilities.undefined.UndefinedType or hikari.models.permissions.Permission If provided, the new vale of all disallowed permissions. - reason : hikari.utilities.undefined.Undefined or str + reason : hikari.utilities.undefined.UndefinedType or str If provided, the reason that will be recorded in the audit logs. Raises @@ -591,7 +591,7 @@ async def edit_permission_overwrites( hikari.errors.ServerHTTPErrorResponse If an internal error occurs on Discord while handling the request. """ - if isinstance(target_type, undefined.Undefined): + if target_type is undefined.UNDEFINED: if isinstance(target, users.User): target_type = channels.PermissionOverwriteType.MEMBER elif isinstance(target, guilds.Role): @@ -677,13 +677,13 @@ async def create_invite( channel: typing.Union[channels.GuildChannel, bases.UniqueObject], /, *, - max_age: typing.Union[undefined.Undefined, int, float, datetime.timedelta] = undefined.Undefined(), - max_uses: typing.Union[undefined.Undefined, int] = undefined.Undefined(), - temporary: typing.Union[undefined.Undefined, bool] = undefined.Undefined(), - unique: typing.Union[undefined.Undefined, bool] = undefined.Undefined(), - target_user: typing.Union[undefined.Undefined, users.User, bases.UniqueObject] = undefined.Undefined(), - target_user_type: typing.Union[undefined.Undefined, invites.TargetUserType] = undefined.Undefined(), - reason: typing.Union[undefined.Undefined, str] = undefined.Undefined(), + max_age: typing.Union[undefined.UndefinedType, int, float, datetime.timedelta] = undefined.UNDEFINED, + max_uses: typing.Union[undefined.UndefinedType, int] = undefined.UNDEFINED, + temporary: typing.Union[undefined.UndefinedType, bool] = undefined.UNDEFINED, + unique: typing.Union[undefined.UndefinedType, bool] = undefined.UNDEFINED, + target_user: typing.Union[undefined.UndefinedType, users.User, bases.UniqueObject] = undefined.UNDEFINED, + target_user_type: typing.Union[undefined.UndefinedType, invites.TargetUserType] = undefined.UNDEFINED, + reason: typing.Union[undefined.UndefinedType, str] = undefined.UNDEFINED, ) -> invites.InviteWithMetadata: """Create an invite to the given guild channel. @@ -692,20 +692,20 @@ async def create_invite( channel : hikari.models.channels.PartialChannel or hikari.utilities.snowflake.Snowflake or int or str The channel to create a invite for. This may be a channel object, or the ID of an existing channel. - max_age : hikari.utilities.undefined.Undefined or datetime.timedelta or float or int + max_age : hikari.utilities.undefined.UndefinedType or datetime.timedelta or float or int If provided, the duration of the invite before expiry. - max_uses : hikari.utilities.undefined.Undefined or int + max_uses : hikari.utilities.undefined.UndefinedType or int If provided, the max uses the invite can have. - temporary : hikari.utilities.undefined.Undefined or bool + temporary : hikari.utilities.undefined.UndefinedType or bool If provided, whether the invite only grants temporary membership. - unique : hikari.utilities.undefined.Undefined or bool + unique : hikari.utilities.undefined.UndefinedType or bool If provided, wheter the invite should be unique. - target_user : hikari.utilities.undefined.Undefined or hikari.models.users.User or hikari.utilities.snowflake.Snowflake or int or str + target_user : hikari.utilities.undefined.UndefinedType or hikari.models.users.User or hikari.utilities.snowflake.Snowflake or int or str If provided, the target user id for this invite. This may be a user object, or the ID of an existing user. - target_user_type : hikari.utilities.undefined.Undefined or hikari.models.invites.TargetUserType or int + target_user_type : hikari.utilities.undefined.UndefinedType or hikari.models.invites.TargetUserType or int If provided, the type of target user for this invite. - reason : hikari.utilities.undefined.Undefined or str + reason : hikari.utilities.undefined.UndefinedType or str If provided, the reason that will be recorded in the audit logs. Returns @@ -910,9 +910,9 @@ def fetch_messages( channel: typing.Union[channels.TextChannel, bases.UniqueObject], /, *, - before: typing.Union[undefined.Undefined, datetime.datetime, bases.UniqueObject] = undefined.Undefined(), - after: typing.Union[undefined.Undefined, datetime.datetime, bases.UniqueObject] = undefined.Undefined(), - around: typing.Union[undefined.Undefined, datetime.datetime, bases.UniqueObject] = undefined.Undefined(), + before: typing.Union[undefined.UndefinedType, datetime.datetime, bases.UniqueObject] = undefined.UNDEFINED, + after: typing.Union[undefined.UndefinedType, datetime.datetime, bases.UniqueObject] = undefined.UNDEFINED, + around: typing.Union[undefined.UndefinedType, datetime.datetime, bases.UniqueObject] = undefined.UNDEFINED, ) -> iterators.LazyIterator[messages_.Message]: """Browse the message history for a given text channel. @@ -921,13 +921,13 @@ def fetch_messages( channel : hikari.models.channels.PartialChannel or hikari.utilities.snowflake.Snowflake or int or str The channel to fetch messages in. This may be a channel object, or the ID of an existing channel. - before : hikari.utilities.undefined.Undefined or datetime.datetime or hikari.utilities.snowflake.Snowflake or int or str + before : hikari.utilities.undefined.UndefinedType or datetime.datetime or hikari.utilities.snowflake.Snowflake or int or str If provided, fetch messages before this snowflake. If you provide a datetime object, it will be transformed into a snowflake. - after : hikari.utilities.undefined.Undefined or datetime.datetime or hikari.utilities.snowflake.Snowflake or int or str + after : hikari.utilities.undefined.UndefinedType or datetime.datetime or hikari.utilities.snowflake.Snowflake or int or str If provided, fetch messages after this snowflake. If you provide a datetime object, it will be transformed into a snowflake. - around : hikari.utilities.undefined.Undefined or datetime.datetime or hikari.utilities.snowflake.Snowflake or int or str + around : hikari.utilities.undefined.UndefinedType or datetime.datetime or hikari.utilities.snowflake.Snowflake or int or str If provided, fetch messages around this snowflake. If you provide a datetime object, it will be transformed into a snowflake. @@ -956,14 +956,14 @@ def fetch_messages( this function itself will not raise anything (other than `TypeError`). """ - if undefined.Undefined.count(before, after, around) < 2: + if undefined.count(before, after, around) < 2: raise TypeError("Expected no kwargs, or maximum of one of 'before', 'after', 'around'") - if not isinstance(before, undefined.Undefined): + if not before is undefined.UNDEFINED: direction, timestamp = "before", before - elif not isinstance(after, undefined.Undefined): + elif not after is undefined.UNDEFINED: direction, timestamp = "after", after - elif not isinstance(around, undefined.Undefined): + elif not around is undefined.UNDEFINED: direction, timestamp = "around", around else: direction, timestamp = "before", snowflake.Snowflake.max() @@ -1015,14 +1015,14 @@ async def fetch_message( async def create_message( self, channel: typing.Union[channels.TextChannel, bases.UniqueObject], - text: typing.Union[undefined.Undefined, typing.Any] = undefined.Undefined(), + text: typing.Union[undefined.UndefinedType, typing.Any] = undefined.UNDEFINED, *, - embed: typing.Union[undefined.Undefined, embeds_.Embed] = undefined.Undefined(), + embed: typing.Union[undefined.UndefinedType, embeds_.Embed] = undefined.UNDEFINED, attachments: typing.Union[ - undefined.Undefined, typing.Sequence[typing.Union[str, files.Resource]] - ] = undefined.Undefined(), - tts: typing.Union[undefined.Undefined, bool] = undefined.Undefined(), - nonce: typing.Union[undefined.Undefined, str] = undefined.Undefined(), + undefined.UndefinedType, typing.Sequence[typing.Union[str, files.Resource]] + ] = undefined.UNDEFINED, + tts: typing.Union[undefined.UndefinedType, bool] = undefined.UNDEFINED, + nonce: typing.Union[undefined.UndefinedType, str] = undefined.UNDEFINED, mentions_everyone: bool = True, user_mentions: typing.Union[typing.Collection[typing.Union[users.User, bases.UniqueObject]], bool] = True, role_mentions: typing.Union[typing.Collection[typing.Union[guilds.Role, bases.UniqueObject]], bool] = True, @@ -1034,16 +1034,16 @@ async def create_message( channel : hikari.models.channels.PartialChannel or hikari.utilities.snowflake.Snowflake or int or str The channel to create the message in. This may be a channel object, or the ID of an existing channel. - text : hikari.utilities.undefined.Undefined or str + text : hikari.utilities.undefined.UndefinedType or str If specified, the message contents. - embed : hikari.utilities.undefined.Undefined or hikari.models.embeds.Embed + embed : hikari.utilities.undefined.UndefinedType or hikari.models.embeds.Embed If specified, the message embed. - attachments : hikari.utilities.undefined.Undefined or typing.Sequence[str or hikari.utilities.files.Resource] + attachments : hikari.utilities.undefined.UndefinedType or typing.Sequence[str or hikari.utilities.files.Resource] If specified, the message attachments. These can be resources, or strings consisting of paths on your computer or URLs. - tts : hikari.utilities.undefined.Undefined or bool + tts : hikari.utilities.undefined.UndefinedType or bool If specified, whether the message will be TTS (Text To Speech). - nonce : hikari.utilities.undefined.Undefined or str + nonce : hikari.utilities.undefined.UndefinedType or str If specified, a nonce that can be used for optimistic message sending. mentions_everyone : bool If specified, whether the message should parse @everyone/@here mentions. @@ -1092,12 +1092,12 @@ async def create_message( body.put("nonce", nonce) body.put("tts", tts) - if isinstance(attachments, undefined.Undefined): + if attachments is undefined.UNDEFINED: final_attachments: typing.List[files.Resource] = [] else: final_attachments = [files.ensure_resource(a) for a in attachments] - if not isinstance(embed, undefined.Undefined): + if not embed is undefined.UNDEFINED: embed_payload, embed_attachments = self._app.entity_factory.serialize_embed(embed) body.put("embed", embed_payload) final_attachments.extend(embed_attachments) @@ -1128,17 +1128,17 @@ async def edit_message( self, channel: typing.Union[channels.TextChannel, bases.UniqueObject], message: typing.Union[messages_.Message, bases.UniqueObject], - text: typing.Union[undefined.Undefined, None, typing.Any] = undefined.Undefined(), + text: typing.Union[undefined.UndefinedType, None, typing.Any] = undefined.UNDEFINED, *, - embed: typing.Union[undefined.Undefined, None, embeds_.Embed] = undefined.Undefined(), - mentions_everyone: typing.Union[undefined.Undefined, bool] = undefined.Undefined(), + embed: typing.Union[undefined.UndefinedType, None, embeds_.Embed] = undefined.UNDEFINED, + mentions_everyone: typing.Union[undefined.UndefinedType, bool] = undefined.UNDEFINED, user_mentions: typing.Union[ - undefined.Undefined, typing.Collection[typing.Union[users.User, bases.UniqueObject]], bool - ] = undefined.Undefined(), + undefined.UndefinedType, typing.Collection[typing.Union[users.User, bases.UniqueObject]], bool + ] = undefined.UNDEFINED, role_mentions: typing.Union[ - undefined.Undefined, typing.Collection[typing.Union[bases.UniqueObject, guilds.Role]], bool - ] = undefined.Undefined(), - flags: typing.Union[undefined.Undefined, messages_.MessageFlag] = undefined.Undefined(), + undefined.UndefinedType, typing.Collection[typing.Union[bases.UniqueObject, guilds.Role]], bool + ] = undefined.UNDEFINED, + flags: typing.Union[undefined.UndefinedType, messages_.MessageFlag] = undefined.UNDEFINED, ) -> messages_.Message: """Edit an existing message in a given channel. @@ -1387,13 +1387,13 @@ async def create_webhook( channel: typing.Union[channels.TextChannel, bases.UniqueObject], name: str, *, - avatar: typing.Union[undefined.Undefined, files.Resource] = undefined.Undefined(), - reason: typing.Union[undefined.Undefined, str] = undefined.Undefined(), + avatar: typing.Union[undefined.UndefinedType, files.Resource] = undefined.UNDEFINED, + reason: typing.Union[undefined.UndefinedType, str] = undefined.UNDEFINED, ) -> webhooks.Webhook: route = routes.POST_WEBHOOK.compile(channel=channel) body = data_binding.JSONObjectBuilder() body.put("name", name) - if not isinstance(avatar, undefined.Undefined): + if not avatar is undefined.UNDEFINED: async with avatar.stream(executor=self._app.thread_pool_executor) as stream: body.put("avatar", await stream.data_uri()) @@ -1406,9 +1406,9 @@ async def fetch_webhook( webhook: typing.Union[webhooks.Webhook, bases.UniqueObject], /, *, - token: typing.Union[undefined.Undefined, str] = undefined.Undefined(), + token: typing.Union[undefined.UndefinedType, str] = undefined.UNDEFINED, ) -> webhooks.Webhook: - if isinstance(token, undefined.Undefined): + if token is undefined.UNDEFINED: route = routes.GET_WEBHOOK.compile(webhook=webhook) else: route = routes.GET_WEBHOOK_WITH_TOKEN.compile(webhook=webhook, token=token) @@ -1437,13 +1437,13 @@ async def edit_webhook( webhook: typing.Union[webhooks.Webhook, bases.UniqueObject], /, *, - token: typing.Union[undefined.Undefined, str] = undefined.Undefined(), - name: typing.Union[undefined.Undefined, str] = undefined.Undefined(), - avatar: typing.Union[None, undefined.Undefined, files.Resource] = undefined.Undefined(), - channel: typing.Union[undefined.Undefined, channels.TextChannel, bases.UniqueObject] = undefined.Undefined(), - reason: typing.Union[undefined.Undefined, str] = undefined.Undefined(), + token: typing.Union[undefined.UndefinedType, str] = undefined.UNDEFINED, + name: typing.Union[undefined.UndefinedType, str] = undefined.UNDEFINED, + avatar: typing.Union[None, undefined.UndefinedType, files.Resource] = undefined.UNDEFINED, + channel: typing.Union[undefined.UndefinedType, channels.TextChannel, bases.UniqueObject] = undefined.UNDEFINED, + reason: typing.Union[undefined.UndefinedType, str] = undefined.UNDEFINED, ) -> webhooks.Webhook: - if isinstance(token, undefined.Undefined): + if token is undefined.UNDEFINED: route = routes.PATCH_WEBHOOK.compile(webhook=webhook) else: route = routes.PATCH_WEBHOOK_WITH_TOKEN.compile(webhook=webhook, token=token) @@ -1454,7 +1454,7 @@ async def edit_webhook( if avatar is None: body.put("avatar", None) - elif not isinstance(avatar, undefined.Undefined): + elif not avatar is undefined.UNDEFINED: async with avatar.stream(executor=self._app.thread_pool_executor) as stream: body.put("avatar", await stream.data_uri()) @@ -1467,9 +1467,9 @@ async def delete_webhook( webhook: typing.Union[webhooks.Webhook, bases.UniqueObject], /, *, - token: typing.Union[undefined.Undefined, str] = undefined.Undefined(), + token: typing.Union[undefined.UndefinedType, str] = undefined.UNDEFINED, ) -> None: - if isinstance(token, undefined.Undefined): + if token is undefined.UNDEFINED: route = routes.DELETE_WEBHOOK.compile(webhook=webhook) else: route = routes.DELETE_WEBHOOK_WITH_TOKEN.compile(webhook=webhook, token=token) @@ -1478,29 +1478,29 @@ async def delete_webhook( async def execute_webhook( self, webhook: typing.Union[webhooks.Webhook, bases.UniqueObject], - text: typing.Union[undefined.Undefined, typing.Any] = undefined.Undefined(), + text: typing.Union[undefined.UndefinedType, typing.Any] = undefined.UNDEFINED, *, - token: typing.Union[undefined.Undefined, str] = undefined.Undefined(), - username: typing.Union[undefined.Undefined, str] = undefined.Undefined(), - avatar_url: typing.Union[undefined.Undefined, str] = undefined.Undefined(), - embeds: typing.Union[undefined.Undefined, typing.Sequence[embeds_.Embed]] = undefined.Undefined(), - attachments: typing.Union[undefined.Undefined, typing.Sequence[files.Resource]] = undefined.Undefined(), - tts: typing.Union[undefined.Undefined, bool] = undefined.Undefined(), + token: typing.Union[undefined.UndefinedType, str] = undefined.UNDEFINED, + username: typing.Union[undefined.UndefinedType, str] = undefined.UNDEFINED, + avatar_url: typing.Union[undefined.UndefinedType, str] = undefined.UNDEFINED, + embeds: typing.Union[undefined.UndefinedType, typing.Sequence[embeds_.Embed]] = undefined.UNDEFINED, + attachments: typing.Union[undefined.UndefinedType, typing.Sequence[files.Resource]] = undefined.UNDEFINED, + tts: typing.Union[undefined.UndefinedType, bool] = undefined.UNDEFINED, mentions_everyone: bool = True, user_mentions: typing.Union[typing.Collection[typing.Union[users.User, bases.UniqueObject]], bool] = True, role_mentions: typing.Union[typing.Collection[typing.Union[bases.UniqueObject, guilds.Role]], bool] = True, ) -> messages_.Message: - if isinstance(token, undefined.Undefined): + if token is undefined.UNDEFINED: route = routes.POST_WEBHOOK.compile(webhook=webhook) no_auth = False else: route = routes.POST_WEBHOOK_WITH_TOKEN.compile(webhook=webhook, token=token) no_auth = True - attachments = [] if isinstance(attachments, undefined.Undefined) else [a for a in attachments] + attachments = [] if attachments is undefined.UNDEFINED else [a for a in attachments] serialized_embeds = [] - if not isinstance(embeds, undefined.Undefined): + if not embeds is undefined.UNDEFINED: for embed in embeds: embed_payload, embed_attachments = self._app.entity_factory.serialize_embed(embed) serialized_embeds.append(embed_payload) @@ -1571,8 +1571,8 @@ async def fetch_my_user(self) -> users.OwnUser: async def edit_my_user( self, *, - username: typing.Union[undefined.Undefined, str] = undefined.Undefined(), - avatar: typing.Union[undefined.Undefined, None, files.Resource] = undefined.Undefined(), + username: typing.Union[undefined.UndefinedType, str] = undefined.UNDEFINED, + avatar: typing.Union[undefined.UndefinedType, None, files.Resource] = undefined.UNDEFINED, ) -> users.OwnUser: route = routes.PATCH_MY_USER.compile() body = data_binding.JSONObjectBuilder() @@ -1599,10 +1599,10 @@ def fetch_my_guilds( *, newest_first: bool = False, start_at: typing.Union[ - undefined.Undefined, guilds.PartialGuild, bases.UniqueObject, datetime.datetime - ] = undefined.Undefined(), + undefined.UndefinedType, guilds.PartialGuild, bases.UniqueObject, datetime.datetime + ] = undefined.UNDEFINED, ) -> iterators.LazyIterator[applications.OwnGuild]: - if isinstance(start_at, undefined.Undefined): + if start_at is undefined.UNDEFINED: start_at = snowflake.Snowflake.max() if newest_first else snowflake.Snowflake.min() elif isinstance(start_at, datetime.datetime): start_at = snowflake.Snowflake.from_datetime(start_at) @@ -1633,12 +1633,12 @@ async def add_user_to_guild( guild: typing.Union[guilds.Guild, bases.UniqueObject], user: typing.Union[users.User, bases.UniqueObject], *, - nick: typing.Union[undefined.Undefined, str] = undefined.Undefined(), + nick: typing.Union[undefined.UndefinedType, str] = undefined.UNDEFINED, roles: typing.Union[ - undefined.Undefined, typing.Collection[typing.Union[guilds.Role, bases.UniqueObject]] - ] = undefined.Undefined(), - mute: typing.Union[undefined.Undefined, bool] = undefined.Undefined(), - deaf: typing.Union[undefined.Undefined, bool] = undefined.Undefined(), + undefined.UndefinedType, typing.Collection[typing.Union[guilds.Role, bases.UniqueObject]] + ] = undefined.UNDEFINED, + mute: typing.Union[undefined.UndefinedType, bool] = undefined.UNDEFINED, + deaf: typing.Union[undefined.UndefinedType, bool] = undefined.UNDEFINED, ) -> typing.Optional[guilds.Member]: route = routes.PUT_GUILD_MEMBER.compile(guild=guild, user=user) body = data_binding.JSONObjectBuilder() @@ -1672,20 +1672,20 @@ def fetch_audit_log( guild: typing.Union[guilds.Guild, bases.UniqueObject], /, *, - before: typing.Union[undefined.Undefined, datetime.datetime, bases.UniqueObject] = undefined.Undefined(), - user: typing.Union[undefined.Undefined, users.User, bases.UniqueObject] = undefined.Undefined(), - event_type: typing.Union[undefined.Undefined, audit_logs.AuditLogEventType] = undefined.Undefined(), + before: typing.Union[undefined.UndefinedType, datetime.datetime, bases.UniqueObject] = undefined.UNDEFINED, + user: typing.Union[undefined.UndefinedType, users.User, bases.UniqueObject] = undefined.UNDEFINED, + event_type: typing.Union[undefined.UndefinedType, audit_logs.AuditLogEventType] = undefined.UNDEFINED, ) -> iterators.LazyIterator[audit_logs.AuditLog]: guild = str(int(guild)) - if isinstance(before, undefined.Undefined): + if before is undefined.UNDEFINED: before = str(snowflake.Snowflake.max()) elif isinstance(before, datetime.datetime): before = str(snowflake.Snowflake.from_datetime(before)) else: before = str(int(before)) - if not isinstance(user, undefined.Undefined): + if not user is undefined.UNDEFINED: user = str(int(user)) return iterators.AuditLogIterator(self._app, self._request, guild, before, user, event_type) @@ -1718,14 +1718,14 @@ async def create_emoji( image: files.Resource, *, roles: typing.Union[ - undefined.Undefined, typing.Collection[typing.Union[guilds.Role, bases.UniqueObject]] - ] = undefined.Undefined(), - reason: typing.Union[undefined.Undefined, str] = undefined.Undefined(), + undefined.UndefinedType, typing.Collection[typing.Union[guilds.Role, bases.UniqueObject]] + ] = undefined.UNDEFINED, + reason: typing.Union[undefined.UndefinedType, str] = undefined.UNDEFINED, ) -> emojis.KnownCustomEmoji: route = routes.POST_GUILD_EMOJIS.compile(guild=guild) body = data_binding.JSONObjectBuilder() body.put("name", name) - if not isinstance(image, undefined.Undefined): + if not image is undefined.UNDEFINED: async with image.stream(executor=self._app.thread_pool_executor) as stream: body.put("image", await stream.data_uri()) @@ -1741,11 +1741,11 @@ async def edit_emoji( # This is an emoji ID, which is the URL-safe emoji name, not the snowflake alone. emoji: typing.Union[emojis.CustomEmoji, bases.UniqueObject], *, - name: typing.Union[undefined.Undefined, str] = undefined.Undefined(), + name: typing.Union[undefined.UndefinedType, str] = undefined.UNDEFINED, roles: typing.Union[ - undefined.Undefined, typing.Collection[typing.Union[guilds.Role, bases.UniqueObject]] - ] = undefined.Undefined(), - reason: typing.Union[undefined.Undefined, str] = undefined.Undefined(), + undefined.UndefinedType, typing.Collection[typing.Union[guilds.Role, bases.UniqueObject]] + ] = undefined.UNDEFINED, + reason: typing.Union[undefined.UndefinedType, str] = undefined.UNDEFINED, ) -> emojis.KnownCustomEmoji: route = routes.PATCH_GUILD_EMOJI.compile( guild=guild, emoji=emoji.id if isinstance(emoji, emojis.CustomEmoji) else emoji, @@ -1792,28 +1792,28 @@ async def edit_guild( guild: typing.Union[guilds.Guild, bases.UniqueObject], /, *, - name: typing.Union[undefined.Undefined, str] = undefined.Undefined(), - region: typing.Union[undefined.Undefined, voices.VoiceRegion, str] = undefined.Undefined(), - verification_level: typing.Union[undefined.Undefined, guilds.GuildVerificationLevel] = undefined.Undefined(), + name: typing.Union[undefined.UndefinedType, str] = undefined.UNDEFINED, + region: typing.Union[undefined.UndefinedType, voices.VoiceRegion, str] = undefined.UNDEFINED, + verification_level: typing.Union[undefined.UndefinedType, guilds.GuildVerificationLevel] = undefined.UNDEFINED, default_message_notifications: typing.Union[ - undefined.Undefined, guilds.GuildMessageNotificationsLevel - ] = undefined.Undefined(), + undefined.UndefinedType, guilds.GuildMessageNotificationsLevel + ] = undefined.UNDEFINED, explicit_content_filter_level: typing.Union[ - undefined.Undefined, guilds.GuildExplicitContentFilterLevel - ] = undefined.Undefined(), + undefined.UndefinedType, guilds.GuildExplicitContentFilterLevel + ] = undefined.UNDEFINED, afk_channel: typing.Union[ - undefined.Undefined, channels.GuildVoiceChannel, bases.UniqueObject - ] = undefined.Undefined(), - afk_timeout: typing.Union[undefined.Undefined, date.TimeSpan] = undefined.Undefined(), - icon: typing.Union[undefined.Undefined, None, files.Resource] = undefined.Undefined(), - owner: typing.Union[undefined.Undefined, users.User, bases.UniqueObject] = undefined.Undefined(), - splash: typing.Union[undefined.Undefined, None, files.Resource] = undefined.Undefined(), - banner: typing.Union[undefined.Undefined, None, files.Resource] = undefined.Undefined(), - system_channel: typing.Union[undefined.Undefined, channels.GuildTextChannel] = undefined.Undefined(), - rules_channel: typing.Union[undefined.Undefined, channels.GuildTextChannel] = undefined.Undefined(), - public_updates_channel: typing.Union[undefined.Undefined, channels.GuildTextChannel] = undefined.Undefined(), - preferred_locale: typing.Union[undefined.Undefined, str] = undefined.Undefined(), - reason: typing.Union[undefined.Undefined, str] = undefined.Undefined(), + undefined.UndefinedType, channels.GuildVoiceChannel, bases.UniqueObject + ] = undefined.UNDEFINED, + afk_timeout: typing.Union[undefined.UndefinedType, date.TimeSpan] = undefined.UNDEFINED, + icon: typing.Union[undefined.UndefinedType, None, files.Resource] = undefined.UNDEFINED, + owner: typing.Union[undefined.UndefinedType, users.User, bases.UniqueObject] = undefined.UNDEFINED, + splash: typing.Union[undefined.UndefinedType, None, files.Resource] = undefined.UNDEFINED, + banner: typing.Union[undefined.UndefinedType, None, files.Resource] = undefined.UNDEFINED, + system_channel: typing.Union[undefined.UndefinedType, channels.GuildTextChannel] = undefined.UNDEFINED, + rules_channel: typing.Union[undefined.UndefinedType, channels.GuildTextChannel] = undefined.UNDEFINED, + public_updates_channel: typing.Union[undefined.UndefinedType, channels.GuildTextChannel] = undefined.UNDEFINED, + preferred_locale: typing.Union[undefined.UndefinedType, str] = undefined.UNDEFINED, + reason: typing.Union[undefined.UndefinedType, str] = undefined.UNDEFINED, ) -> guilds.Guild: route = routes.PATCH_GUILD.compile(guild=guild) body = data_binding.JSONObjectBuilder() @@ -1834,19 +1834,19 @@ async def edit_guild( if icon is None: body.put("icon", None) - elif not isinstance(icon, undefined.Undefined): + elif not icon is undefined.UNDEFINED: async with icon.stream(executor=self._app.thread_pool_executor) as stream: body.put("icon", await stream.data_uri()) if splash is None: body.put("splash", None) - elif not isinstance(splash, undefined.Undefined): + elif not splash is undefined.UNDEFINED: async with splash.stream(executor=self._app.thread_pool_executor) as stream: body.put("splash", await stream.data_uri()) if banner is None: body.put("banner", None) - elif not isinstance(banner, undefined.Undefined): + elif not banner is undefined.UNDEFINED: async with banner.stream(executor=self._app.thread_pool_executor) as stream: body.put("banner", await stream.data_uri()) @@ -1873,15 +1873,17 @@ async def create_guild_text_channel( guild: typing.Union[guilds.Guild, bases.UniqueObject], name: str, *, - position: typing.Union[int, undefined.Undefined] = undefined.Undefined(), - topic: typing.Union[str, undefined.Undefined] = undefined.Undefined(), - nsfw: typing.Union[bool, undefined.Undefined] = undefined.Undefined(), - rate_limit_per_user: typing.Union[int, undefined.Undefined] = undefined.Undefined(), + position: typing.Union[int, undefined.UndefinedType] = undefined.UNDEFINED, + topic: typing.Union[str, undefined.UndefinedType] = undefined.UNDEFINED, + nsfw: typing.Union[bool, undefined.UndefinedType] = undefined.UNDEFINED, + rate_limit_per_user: typing.Union[int, undefined.UndefinedType] = undefined.UNDEFINED, permission_overwrites: typing.Union[ - typing.Sequence[channels.PermissionOverwrite], undefined.Undefined - ] = undefined.Undefined(), - category: typing.Union[channels.GuildCategory, bases.UniqueObject, undefined.Undefined] = undefined.Undefined(), - reason: typing.Union[str, undefined.Undefined] = undefined.Undefined(), + typing.Sequence[channels.PermissionOverwrite], undefined.UndefinedType + ] = undefined.UNDEFINED, + category: typing.Union[ + channels.GuildCategory, bases.UniqueObject, undefined.UndefinedType + ] = undefined.UNDEFINED, + reason: typing.Union[str, undefined.UndefinedType] = undefined.UNDEFINED, ) -> channels.GuildTextChannel: channel = await self._create_guild_channel( guild, @@ -1902,15 +1904,17 @@ async def create_guild_news_channel( guild: typing.Union[guilds.Guild, bases.UniqueObject], name: str, *, - position: typing.Union[int, undefined.Undefined] = undefined.Undefined(), - topic: typing.Union[str, undefined.Undefined] = undefined.Undefined(), - nsfw: typing.Union[bool, undefined.Undefined] = undefined.Undefined(), - rate_limit_per_user: typing.Union[int, undefined.Undefined] = undefined.Undefined(), + position: typing.Union[int, undefined.UndefinedType] = undefined.UNDEFINED, + topic: typing.Union[str, undefined.UndefinedType] = undefined.UNDEFINED, + nsfw: typing.Union[bool, undefined.UndefinedType] = undefined.UNDEFINED, + rate_limit_per_user: typing.Union[int, undefined.UndefinedType] = undefined.UNDEFINED, permission_overwrites: typing.Union[ - typing.Sequence[channels.PermissionOverwrite], undefined.Undefined - ] = undefined.Undefined(), - category: typing.Union[channels.GuildCategory, bases.UniqueObject, undefined.Undefined] = undefined.Undefined(), - reason: typing.Union[str, undefined.Undefined] = undefined.Undefined(), + typing.Sequence[channels.PermissionOverwrite], undefined.UndefinedType + ] = undefined.UNDEFINED, + category: typing.Union[ + channels.GuildCategory, bases.UniqueObject, undefined.UndefinedType + ] = undefined.UNDEFINED, + reason: typing.Union[str, undefined.UndefinedType] = undefined.UNDEFINED, ) -> channels.GuildNewsChannel: channel = await self._create_guild_channel( guild, @@ -1931,15 +1935,17 @@ async def create_guild_voice_channel( guild: typing.Union[guilds.Guild, bases.UniqueObject], name: str, *, - position: typing.Union[int, undefined.Undefined] = undefined.Undefined(), - nsfw: typing.Union[bool, undefined.Undefined] = undefined.Undefined(), - user_limit: typing.Union[int, undefined.Undefined] = undefined.Undefined(), - bitrate: typing.Union[int, undefined.Undefined] = undefined.Undefined(), + position: typing.Union[int, undefined.UndefinedType] = undefined.UNDEFINED, + nsfw: typing.Union[bool, undefined.UndefinedType] = undefined.UNDEFINED, + user_limit: typing.Union[int, undefined.UndefinedType] = undefined.UNDEFINED, + bitrate: typing.Union[int, undefined.UndefinedType] = undefined.UNDEFINED, permission_overwrites: typing.Union[ - typing.Sequence[channels.PermissionOverwrite], undefined.Undefined - ] = undefined.Undefined(), - category: typing.Union[channels.GuildCategory, bases.UniqueObject, undefined.Undefined] = undefined.Undefined(), - reason: typing.Union[str, undefined.Undefined] = undefined.Undefined(), + typing.Sequence[channels.PermissionOverwrite], undefined.UndefinedType + ] = undefined.UNDEFINED, + category: typing.Union[ + channels.GuildCategory, bases.UniqueObject, undefined.UndefinedType + ] = undefined.UNDEFINED, + reason: typing.Union[str, undefined.UndefinedType] = undefined.UNDEFINED, ) -> channels.GuildVoiceChannel: channel = await self._create_guild_channel( guild, @@ -1960,12 +1966,12 @@ async def create_guild_category( guild: typing.Union[guilds.Guild, bases.UniqueObject], name: str, *, - position: typing.Union[int, undefined.Undefined] = undefined.Undefined(), - nsfw: typing.Union[bool, undefined.Undefined] = undefined.Undefined(), + position: typing.Union[int, undefined.UndefinedType] = undefined.UNDEFINED, + nsfw: typing.Union[bool, undefined.UndefinedType] = undefined.UNDEFINED, permission_overwrites: typing.Union[ - typing.Sequence[channels.PermissionOverwrite], undefined.Undefined - ] = undefined.Undefined(), - reason: typing.Union[str, undefined.Undefined] = undefined.Undefined(), + typing.Sequence[channels.PermissionOverwrite], undefined.UndefinedType + ] = undefined.UNDEFINED, + reason: typing.Union[str, undefined.UndefinedType] = undefined.UNDEFINED, ) -> channels.GuildCategory: channel = await self._create_guild_channel( guild, @@ -1984,17 +1990,19 @@ async def _create_guild_channel( name: str, type_: channels.ChannelType, *, - position: typing.Union[int, undefined.Undefined] = undefined.Undefined(), - topic: typing.Union[str, undefined.Undefined] = undefined.Undefined(), - nsfw: typing.Union[bool, undefined.Undefined] = undefined.Undefined(), - bitrate: typing.Union[int, undefined.Undefined] = undefined.Undefined(), - user_limit: typing.Union[int, undefined.Undefined] = undefined.Undefined(), - rate_limit_per_user: typing.Union[int, undefined.Undefined] = undefined.Undefined(), + position: typing.Union[int, undefined.UndefinedType] = undefined.UNDEFINED, + topic: typing.Union[str, undefined.UndefinedType] = undefined.UNDEFINED, + nsfw: typing.Union[bool, undefined.UndefinedType] = undefined.UNDEFINED, + bitrate: typing.Union[int, undefined.UndefinedType] = undefined.UNDEFINED, + user_limit: typing.Union[int, undefined.UndefinedType] = undefined.UNDEFINED, + rate_limit_per_user: typing.Union[int, undefined.UndefinedType] = undefined.UNDEFINED, permission_overwrites: typing.Union[ - typing.Sequence[channels.PermissionOverwrite], undefined.Undefined - ] = undefined.Undefined(), - category: typing.Union[channels.GuildCategory, bases.UniqueObject, undefined.Undefined] = undefined.Undefined(), - reason: typing.Union[str, undefined.Undefined] = undefined.Undefined(), + typing.Sequence[channels.PermissionOverwrite], undefined.UndefinedType + ] = undefined.UNDEFINED, + category: typing.Union[ + channels.GuildCategory, bases.UniqueObject, undefined.UndefinedType + ] = undefined.UNDEFINED, + reason: typing.Union[str, undefined.UndefinedType] = undefined.UNDEFINED, ) -> channels.GuildChannel: route = routes.POST_GUILD_CHANNELS.compile(guild=guild) body = data_binding.JSONObjectBuilder() @@ -2045,16 +2053,16 @@ async def edit_member( guild: typing.Union[guilds.Guild, bases.UniqueObject], user: typing.Union[users.User, bases.UniqueObject], *, - nick: typing.Union[undefined.Undefined, str] = undefined.Undefined(), + nick: typing.Union[undefined.UndefinedType, str] = undefined.UNDEFINED, roles: typing.Union[ - undefined.Undefined, typing.Collection[typing.Union[guilds.Role, bases.UniqueObject]] - ] = undefined.Undefined(), - mute: typing.Union[undefined.Undefined, bool] = undefined.Undefined(), - deaf: typing.Union[undefined.Undefined, bool] = undefined.Undefined(), + undefined.UndefinedType, typing.Collection[typing.Union[guilds.Role, bases.UniqueObject]] + ] = undefined.UNDEFINED, + mute: typing.Union[undefined.UndefinedType, bool] = undefined.UNDEFINED, + deaf: typing.Union[undefined.UndefinedType, bool] = undefined.UNDEFINED, voice_channel: typing.Union[ - undefined.Undefined, channels.GuildVoiceChannel, bases.UniqueObject, None - ] = undefined.Undefined(), - reason: typing.Union[str, undefined.Undefined] = undefined.Undefined(), + undefined.UndefinedType, channels.GuildVoiceChannel, bases.UniqueObject, None + ] = undefined.UNDEFINED, + reason: typing.Union[str, undefined.UndefinedType] = undefined.UNDEFINED, ) -> None: route = routes.PATCH_GUILD_MEMBER.compile(guild=guild, user=user) body = data_binding.JSONObjectBuilder() @@ -2065,7 +2073,7 @@ async def edit_member( if voice_channel is None: body.put("channel_id", None) - elif not isinstance(voice_channel, undefined.Undefined): + elif not voice_channel is undefined.UNDEFINED: body.put_snowflake("channel_id", voice_channel) await self._request(route, body=body, reason=reason) @@ -2075,7 +2083,7 @@ async def edit_my_nick( guild: typing.Union[guilds.Guild, bases.UniqueObject], nick: typing.Optional[str], *, - reason: typing.Union[undefined.Undefined, str] = undefined.Undefined(), + reason: typing.Union[undefined.UndefinedType, str] = undefined.UNDEFINED, ) -> None: route = routes.PATCH_MY_GUILD_NICKNAME.compile(guild=guild) body = data_binding.JSONObjectBuilder() @@ -2088,7 +2096,7 @@ async def add_role_to_member( user: typing.Union[users.User, bases.UniqueObject], role: typing.Union[guilds.Role, bases.UniqueObject], *, - reason: typing.Union[undefined.Undefined, str] = undefined.Undefined(), + reason: typing.Union[undefined.UndefinedType, str] = undefined.UNDEFINED, ) -> None: route = routes.PUT_GUILD_MEMBER_ROLE.compile(guild=guild, user=user, role=role) await self._request(route, reason=reason) @@ -2099,7 +2107,7 @@ async def remove_role_from_member( user: typing.Union[users.User, bases.UniqueObject], role: typing.Union[guilds.Role, bases.UniqueObject], *, - reason: typing.Union[undefined.Undefined, str] = undefined.Undefined(), + reason: typing.Union[undefined.UndefinedType, str] = undefined.UNDEFINED, ) -> None: route = routes.DELETE_GUILD_MEMBER_ROLE.compile(guild=guild, user=user, role=role) await self._request(route, reason=reason) @@ -2109,7 +2117,7 @@ async def kick_member( guild: typing.Union[guilds.Guild, bases.UniqueObject], user: typing.Union[users.User, bases.UniqueObject], *, - reason: typing.Union[undefined.Undefined, str] = undefined.Undefined(), + reason: typing.Union[undefined.UndefinedType, str] = undefined.UNDEFINED, ) -> None: route = routes.DELETE_GUILD_MEMBER.compile(guild=guild, user=user,) await self._request(route, reason=reason) @@ -2119,7 +2127,7 @@ async def ban_user( guild: typing.Union[guilds.Guild, bases.UniqueObject], user: typing.Union[users.User, bases.UniqueObject], *, - reason: typing.Union[undefined.Undefined, str] = undefined.Undefined(), + reason: typing.Union[undefined.UndefinedType, str] = undefined.UNDEFINED, ) -> None: route = routes.PUT_GUILD_BAN.compile(guild=guild, user=user) await self._request(route, reason=reason) @@ -2129,7 +2137,7 @@ async def unban_user( guild: typing.Union[guilds.Guild, bases.UniqueObject], user: typing.Union[users.User, bases.UniqueObject], *, - reason: typing.Union[undefined.Undefined, str] = undefined.Undefined(), + reason: typing.Union[undefined.UndefinedType, str] = undefined.UNDEFINED, ) -> None: route = routes.DELETE_GUILD_BAN.compile(guild=guild, user=user) await self._request(route, reason=reason) @@ -2163,15 +2171,15 @@ async def create_role( guild: typing.Union[guilds.Guild, bases.UniqueObject], /, *, - name: typing.Union[undefined.Undefined, str] = undefined.Undefined(), - permissions: typing.Union[undefined.Undefined, permissions_.Permission] = undefined.Undefined(), - color: typing.Union[undefined.Undefined, colors.Color] = undefined.Undefined(), - colour: typing.Union[undefined.Undefined, colors.Color] = undefined.Undefined(), - hoist: typing.Union[undefined.Undefined, bool] = undefined.Undefined(), - mentionable: typing.Union[undefined.Undefined, bool] = undefined.Undefined(), - reason: typing.Union[undefined.Undefined, str] = undefined.Undefined(), + name: typing.Union[undefined.UndefinedType, str] = undefined.UNDEFINED, + permissions: typing.Union[undefined.UndefinedType, permissions_.Permission] = undefined.UNDEFINED, + color: typing.Union[undefined.UndefinedType, colors.Color] = undefined.UNDEFINED, + colour: typing.Union[undefined.UndefinedType, colors.Color] = undefined.UNDEFINED, + hoist: typing.Union[undefined.UndefinedType, bool] = undefined.UNDEFINED, + mentionable: typing.Union[undefined.UndefinedType, bool] = undefined.UNDEFINED, + reason: typing.Union[undefined.UndefinedType, str] = undefined.UNDEFINED, ) -> guilds.Role: - if not undefined.Undefined.count(color, colour): + if not undefined.count(color, colour): raise TypeError("Can not specify 'color' and 'colour' together.") route = routes.POST_GUILD_ROLES.compile(guild=guild) @@ -2201,15 +2209,15 @@ async def edit_role( guild: typing.Union[guilds.Guild, bases.UniqueObject], role: typing.Union[guilds.Role, bases.UniqueObject], *, - name: typing.Union[undefined.Undefined, str] = undefined.Undefined(), - permissions: typing.Union[undefined.Undefined, permissions_.Permission] = undefined.Undefined(), - color: typing.Union[undefined.Undefined, colors.Color] = undefined.Undefined(), - colour: typing.Union[undefined.Undefined, colors.Color] = undefined.Undefined(), - hoist: typing.Union[undefined.Undefined, bool] = undefined.Undefined(), - mentionable: typing.Union[undefined.Undefined, bool] = undefined.Undefined(), - reason: typing.Union[undefined.Undefined, str] = undefined.Undefined(), + name: typing.Union[undefined.UndefinedType, str] = undefined.UNDEFINED, + permissions: typing.Union[undefined.UndefinedType, permissions_.Permission] = undefined.UNDEFINED, + color: typing.Union[undefined.UndefinedType, colors.Color] = undefined.UNDEFINED, + colour: typing.Union[undefined.UndefinedType, colors.Color] = undefined.UNDEFINED, + hoist: typing.Union[undefined.UndefinedType, bool] = undefined.UNDEFINED, + mentionable: typing.Union[undefined.UndefinedType, bool] = undefined.UNDEFINED, + reason: typing.Union[undefined.UndefinedType, str] = undefined.UNDEFINED, ) -> guilds.Role: - if not undefined.Undefined.count(color, colour): + if not undefined.count(color, colour): raise TypeError("Can not specify 'color' and 'colour' together.") route = routes.PATCH_GUILD_ROLE.compile(guild=guild, role=role) @@ -2249,7 +2257,7 @@ async def begin_guild_prune( guild: typing.Union[guilds.Guild, bases.UniqueObject], days: int, *, - reason: typing.Union[undefined.Undefined, str], + reason: typing.Union[undefined.UndefinedType, str], ) -> int: route = routes.POST_GUILD_PRUNE.compile(guild=guild) query = data_binding.StringMapBuilder() @@ -2288,10 +2296,12 @@ async def edit_integration( guild: typing.Union[guilds.Guild, bases.UniqueObject], integration: typing.Union[guilds.Integration, bases.UniqueObject], *, - expire_behaviour: typing.Union[undefined.Undefined, guilds.IntegrationExpireBehaviour] = undefined.Undefined(), - expire_grace_period: typing.Union[undefined.Undefined, date.TimeSpan] = undefined.Undefined(), - enable_emojis: typing.Union[undefined.Undefined, bool] = undefined.Undefined(), - reason: typing.Union[undefined.Undefined, str] = undefined.Undefined(), + expire_behaviour: typing.Union[ + undefined.UndefinedType, guilds.IntegrationExpireBehaviour + ] = undefined.UNDEFINED, + expire_grace_period: typing.Union[undefined.UndefinedType, date.TimeSpan] = undefined.UNDEFINED, + enable_emojis: typing.Union[undefined.UndefinedType, bool] = undefined.UNDEFINED, + reason: typing.Union[undefined.UndefinedType, str] = undefined.UNDEFINED, ) -> None: route = routes.PATCH_GUILD_INTEGRATION.compile(guild=guild, integration=integration) body = data_binding.JSONObjectBuilder() @@ -2306,7 +2316,7 @@ async def delete_integration( guild: typing.Union[guilds.Guild, bases.UniqueObject], integration: typing.Union[guilds.Integration, bases.UniqueObject], *, - reason: typing.Union[undefined.Undefined, str] = undefined.Undefined(), + reason: typing.Union[undefined.UndefinedType, str] = undefined.UNDEFINED, ) -> None: route = routes.DELETE_GUILD_INTEGRATION.compile(guild=guild, integration=integration) await self._request(route, reason=reason) @@ -2331,10 +2341,10 @@ async def edit_widget( /, *, channel: typing.Union[ - undefined.Undefined, channels.GuildChannel, bases.UniqueObject, None - ] = undefined.Undefined(), - enabled: typing.Union[undefined.Undefined, bool] = undefined.Undefined(), - reason: typing.Union[undefined.Undefined, str] = undefined.Undefined(), + undefined.UndefinedType, channels.GuildChannel, bases.UniqueObject, None + ] = undefined.UNDEFINED, + enabled: typing.Union[undefined.UndefinedType, bool] = undefined.UNDEFINED, + reason: typing.Union[undefined.UndefinedType, str] = undefined.UNDEFINED, ) -> guilds.GuildWidget: route = routes.PATCH_GUILD_WIDGET.compile(guild=guild) @@ -2342,7 +2352,7 @@ async def edit_widget( body.put("enabled", enabled) if channel is None: body.put("channel", None) - elif not isinstance(channel, undefined.Undefined): + elif not channel is undefined.UNDEFINED: body.put_snowflake("channel", channel) raw_response = await self._request(route, body=body, reason=reason) diff --git a/hikari/net/rest_utils.py b/hikari/net/rest_utils.py index 0e9287d00e..6e987a65a9 100644 --- a/hikari/net/rest_utils.py +++ b/hikari/net/rest_utils.py @@ -180,34 +180,34 @@ class GuildBuilder: _roles: typing.MutableSequence[data_binding.JSONObject] = attr.ib(factory=list) default_message_notifications: typing.Union[ - undefined.Undefined, guilds.GuildMessageNotificationsLevel - ] = undefined.Undefined() + undefined.UndefinedType, guilds.GuildMessageNotificationsLevel + ] = undefined.UNDEFINED """Default message notification level that can be overwritten. If not overridden, this will use the Discord default level. """ explicit_content_filter_level: typing.Union[ - undefined.Undefined, guilds.GuildExplicitContentFilterLevel - ] = undefined.Undefined() + undefined.UndefinedType, guilds.GuildExplicitContentFilterLevel + ] = undefined.UNDEFINED """Explicit content filter level that can be overwritten. If not overridden, this will use the Discord default level. """ - icon: typing.Union[undefined.Undefined, files.URL] = undefined.Undefined() + icon: typing.Union[undefined.UndefinedType, files.URL] = undefined.UNDEFINED """Guild icon to use that can be overwritten. If not overridden, the guild will not have an icon. """ - region: typing.Union[undefined.Undefined, str] = undefined.Undefined() + region: typing.Union[undefined.UndefinedType, str] = undefined.UNDEFINED """Guild voice channel region to use that can be overwritten. If not overridden, the guild will use the default voice region for Discord. """ - verification_level: typing.Union[undefined.Undefined, guilds.GuildVerificationLevel] = undefined.Undefined() + verification_level: typing.Union[undefined.UndefinedType, guilds.GuildVerificationLevel] = undefined.UNDEFINED """Verification level required to join the guild that can be overwritten. If not overridden, the guild will use the default verification level for @@ -252,7 +252,7 @@ async def create(self) -> guilds.Guild: payload.put("default_message_notifications", self.default_message_notifications) payload.put("explicit_content_filter", self.explicit_content_filter_level) - if not isinstance(self.icon, undefined.Undefined): + if not self.icon is undefined.UNDEFINED: # This isn't annotated properly in the standard library, apparently. async with self.icon.stream(self._app.thread_pool_executor) as stream: data_uri = await stream.data_uri() @@ -267,12 +267,12 @@ def add_role( name: str, /, *, - color: typing.Union[undefined.Undefined, colors.Color] = undefined.Undefined(), - colour: typing.Union[undefined.Undefined, colors.Color] = undefined.Undefined(), - hoisted: typing.Union[undefined.Undefined, bool] = undefined.Undefined(), - mentionable: typing.Union[undefined.Undefined, bool] = undefined.Undefined(), - permissions: typing.Union[undefined.Undefined, permissions_.Permission] = undefined.Undefined(), - position: typing.Union[undefined.Undefined, int] = undefined.Undefined(), + color: typing.Union[undefined.UndefinedType, colors.Color] = undefined.UNDEFINED, + colour: typing.Union[undefined.UndefinedType, colors.Color] = undefined.UNDEFINED, + hoisted: typing.Union[undefined.UndefinedType, bool] = undefined.UNDEFINED, + mentionable: typing.Union[undefined.UndefinedType, bool] = undefined.UNDEFINED, + permissions: typing.Union[undefined.UndefinedType, permissions_.Permission] = undefined.UNDEFINED, + position: typing.Union[undefined.UndefinedType, int] = undefined.UNDEFINED, ) -> snowflake_.Snowflake: """Create a role. @@ -285,18 +285,18 @@ def add_role( ---------- name : str The role name. - color : hikari.utilities.undefined.Undefined or hikari.models.colors.Color + color : hikari.utilities.undefined.UndefinedType or hikari.models.colors.Color The colour of the role to use. If unspecified, then the default colour is used instead. - colour : hikari.utilities.undefined.Undefined or hikari.models.colors.Color + colour : hikari.utilities.undefined.UndefinedType or hikari.models.colors.Color Alias for the `color` parameter for non-american users. - hoisted : hikari.utilities.undefined.Undefined or bool + hoisted : hikari.utilities.undefined.UndefinedType or bool If `True`, the role will show up in the user sidebar in a separate category if it is the highest hoisted role. If `False`, or unspecified, then this will not occur. - mentionable : hikari.utilities.undefined.Undefined or bool + mentionable : hikari.utilities.undefined.UndefinedType or bool If `True`, then the role will be able to be mentioned. - permissions : hikari.utilities.undefined.Undefined or hikari.models.permissions.Permission + permissions : hikari.utilities.undefined.UndefinedType or hikari.models.permissions.Permission The optional permissions to enforce on the role. If unspecified, the default permissions for roles will be used. @@ -304,7 +304,7 @@ def add_role( The default permissions are **NOT** the same as providing zero permissions. To set no permissions, you should pass `Permission(0)` explicitly. - position : hikari.utilities.undefined.Undefined or int + position : hikari.utilities.undefined.UndefinedType or int If specified, the position to place the role in. Returns @@ -326,7 +326,7 @@ def add_role( if len(self._roles) == 0 and name != "@everyone": raise ValueError("First role must always be the @everyone role") - if not undefined.Undefined.count(color, colour): + if not undefined.count(color, colour): raise TypeError("Cannot specify 'color' and 'colour' together.") snowflake = self._new_snowflake() @@ -347,11 +347,11 @@ def add_category( name: str, /, *, - position: typing.Union[undefined.Undefined, int] = undefined.Undefined(), + position: typing.Union[undefined.UndefinedType, int] = undefined.UNDEFINED, permission_overwrites: typing.Union[ - undefined.Undefined, typing.Collection[channels.PermissionOverwrite] - ] = undefined.Undefined(), - nsfw: typing.Union[undefined.Undefined, bool] = undefined.Undefined(), + undefined.UndefinedType, typing.Collection[channels.PermissionOverwrite] + ] = undefined.UNDEFINED, + nsfw: typing.Union[undefined.UndefinedType, bool] = undefined.UNDEFINED, ) -> snowflake_.Snowflake: """Create a category channel. @@ -359,12 +359,12 @@ def add_category( ---------- name : str The name of the category. - position : hikari.utilities.undefined.Undefined or int + position : hikari.utilities.undefined.UndefinedType or int The position to place the category in, if specified. - permission_overwrites : hikari.utilities.undefined.Undefined or typing.Collection[hikari.models.channels.PermissionOverwrite] + permission_overwrites : hikari.utilities.undefined.UndefinedType or typing.Collection[hikari.models.channels.PermissionOverwrite] If defined, a collection of one or more `hikari.models.channels.PermissionOverwrite` objects. - nsfw : hikari.utilities.undefined.Undefined or bool + nsfw : hikari.utilities.undefined.UndefinedType or bool If `True`, the channel is marked as NSFW and only users over 18 years of age should be given access. @@ -399,14 +399,14 @@ def add_text_channel( name: str, /, *, - parent_id: typing.Union[undefined.Undefined, snowflake_.Snowflake] = undefined.Undefined(), - topic: typing.Union[undefined.Undefined, str] = undefined.Undefined(), - rate_limit_per_user: typing.Union[undefined.Undefined, date.TimeSpan] = undefined.Undefined(), - position: typing.Union[undefined.Undefined, int] = undefined.Undefined(), + parent_id: typing.Union[undefined.UndefinedType, snowflake_.Snowflake] = undefined.UNDEFINED, + topic: typing.Union[undefined.UndefinedType, str] = undefined.UNDEFINED, + rate_limit_per_user: typing.Union[undefined.UndefinedType, date.TimeSpan] = undefined.UNDEFINED, + position: typing.Union[undefined.UndefinedType, int] = undefined.UNDEFINED, permission_overwrites: typing.Union[ - undefined.Undefined, typing.Collection[channels.PermissionOverwrite] - ] = undefined.Undefined(), - nsfw: typing.Union[undefined.Undefined, bool] = undefined.Undefined(), + undefined.UndefinedType, typing.Collection[channels.PermissionOverwrite] + ] = undefined.UNDEFINED, + nsfw: typing.Union[undefined.UndefinedType, bool] = undefined.UNDEFINED, ) -> snowflake_.Snowflake: """Create a text channel. @@ -414,21 +414,21 @@ def add_text_channel( ---------- name : str The name of the category. - position : hikari.utilities.undefined.Undefined or int + position : hikari.utilities.undefined.UndefinedType or int The position to place the category in, if specified. - permission_overwrites : hikari.utilities.undefined.Undefined or typing.Collection[hikari.models.channels.PermissionOverwrite] + permission_overwrites : hikari.utilities.undefined.UndefinedType or typing.Collection[hikari.models.channels.PermissionOverwrite] If defined, a collection of one or more `hikari.models.channels.PermissionOverwrite` objects. - nsfw : hikari.utilities.undefined.Undefined or bool + nsfw : hikari.utilities.undefined.UndefinedType or bool If `True`, the channel is marked as NSFW and only users over 18 years of age should be given access. - parent_id : hikari.utilities.undefined.Undefined or hikari.utilities.snowflake.Snowflake + parent_id : hikari.utilities.undefined.UndefinedType or hikari.utilities.snowflake.Snowflake If defined, should be a snowflake ID of a category channel that was made with this builder. If provided, this channel will become a child channel of that category. - topic : hikari.utilities.undefined.Undefined or str + topic : hikari.utilities.undefined.UndefinedType or str If specified, the topic to set on the channel. - rate_limit_per_user : hikari.utilities.undefined.Undefined or hikari.utilities.date.TimeSpan + rate_limit_per_user : hikari.utilities.undefined.UndefinedType or hikari.utilities.date.TimeSpan If specified, the time to wait between allowing consecutive messages to be sent. If not specified, this will not be enabled. @@ -466,14 +466,14 @@ def add_voice_channel( name: str, /, *, - parent_id: typing.Union[undefined.Undefined, snowflake_.Snowflake] = undefined.Undefined(), - bitrate: typing.Union[undefined.Undefined, int] = undefined.Undefined(), - position: typing.Union[undefined.Undefined, int] = undefined.Undefined(), + parent_id: typing.Union[undefined.UndefinedType, snowflake_.Snowflake] = undefined.UNDEFINED, + bitrate: typing.Union[undefined.UndefinedType, int] = undefined.UNDEFINED, + position: typing.Union[undefined.UndefinedType, int] = undefined.UNDEFINED, permission_overwrites: typing.Union[ - undefined.Undefined, typing.Collection[channels.PermissionOverwrite] - ] = undefined.Undefined(), - nsfw: typing.Union[undefined.Undefined, bool] = undefined.Undefined(), - user_limit: typing.Union[undefined.Undefined, int] = undefined.Undefined(), + undefined.UndefinedType, typing.Collection[channels.PermissionOverwrite] + ] = undefined.UNDEFINED, + nsfw: typing.Union[undefined.UndefinedType, bool] = undefined.UNDEFINED, + user_limit: typing.Union[undefined.UndefinedType, int] = undefined.UNDEFINED, ) -> snowflake_.Snowflake: """Create a voice channel. @@ -481,21 +481,21 @@ def add_voice_channel( ---------- name : str The name of the category. - position : hikari.utilities.undefined.Undefined or int + position : hikari.utilities.undefined.UndefinedType or int The position to place the category in, if specified. - permission_overwrites : hikari.utilities.undefined.Undefined or typing.Collection[hikari.models.channels.PermissionOverwrite] + permission_overwrites : hikari.utilities.undefined.UndefinedType or typing.Collection[hikari.models.channels.PermissionOverwrite] If defined, a collection of one or more `hikari.models.channels.PermissionOverwrite` objects. - nsfw : hikari.utilities.undefined.Undefined or bool + nsfw : hikari.utilities.undefined.UndefinedType or bool If `True`, the channel is marked as NSFW and only users over 18 years of age should be given access. - parent_id : hikari.utilities.undefined.Undefined or hikari.utilities.snowflake.Snowflake + parent_id : hikari.utilities.undefined.UndefinedType or hikari.utilities.snowflake.Snowflake If defined, should be a snowflake ID of a category channel that was made with this builder. If provided, this channel will become a child channel of that category. - bitrate : hikari.utilities.undefined.Undefined or int + bitrate : hikari.utilities.undefined.UndefinedType or int If specified, the bitrate to set on the channel. - user_limit : hikari.utilities.undefined.Undefined or int + user_limit : hikari.utilities.undefined.UndefinedType or int If specified, the maximum number of users to allow in the voice channel. diff --git a/hikari/net/strings.py b/hikari/net/strings.py index 7de4025c3c..dcfaff4a6b 100644 --- a/hikari/net/strings.py +++ b/hikari/net/strings.py @@ -27,7 +27,6 @@ import hikari - # Headers. ACCEPT_HEADER: typing.Final[str] = "Accept" AUTHORIZATION_HEADER: typing.Final[str] = "Authorization" diff --git a/hikari/net/tracing.py b/hikari/net/tracing.py index bf316c9ea6..46f11fe52d 100644 --- a/hikari/net/tracing.py +++ b/hikari/net/tracing.py @@ -25,10 +25,10 @@ import io import logging import time -import uuid # noinspection PyUnresolvedReferences import typing +import uuid import aiohttp.abc diff --git a/hikari/utilities/cdn.py b/hikari/utilities/cdn.py index 412065b576..b2482672df 100644 --- a/hikari/utilities/cdn.py +++ b/hikari/utilities/cdn.py @@ -27,7 +27,7 @@ if typing.TYPE_CHECKING: - from hikari.utilities import snowflake + pass BASE_CDN_URL: typing.Final[str] = "https://cdn.discordapp.com" diff --git a/hikari/utilities/data_binding.py b/hikari/utilities/data_binding.py index 4264e53bb9..77b01f87ba 100644 --- a/hikari/utilities/data_binding.py +++ b/hikari/utilities/data_binding.py @@ -44,7 +44,6 @@ from hikari.models import bases from hikari.utilities import undefined - T = typing.TypeVar("T", covariant=True) Headers = typing.Mapping[str, str] @@ -94,7 +93,7 @@ def load_json(_: typing.AnyStr) -> typing.Union[JSONArray, JSONObject]: class StringMapBuilder(multidict.MultiDict[str]): """Helper class used to quickly build query strings or header maps. - This will consume any items that are not `hikari.utilities.undefined.Undefined`. + This will consume any items that are not `hikari.utilities.undefined.UndefinedType`. If a value _is_ unspecified, it will be ignored when inserting it. This reduces the amount of boilerplate needed for generating the headers and query strings for low-level HTTP API interaction, amongst other things. @@ -113,7 +112,7 @@ def __init__(self) -> None: def put( self, key: str, - value: typing.Union[undefined.Undefined, typing.Any], + value: typing.Union[undefined.UndefinedType, typing.Any], /, *, conversion: typing.Optional[typing.Callable[[typing.Any], typing.Any]] = None, @@ -124,7 +123,7 @@ def put( ---------- key : str The string key. - value : hikari.utilities.undefined.Undefined or typing.Any + value : hikari.utilities.undefined.UndefinedType or typing.Any The value to set. conversion : typing.Callable[[typing.Any], typing.Any] or None An optional conversion to perform. @@ -135,7 +134,7 @@ def put( `True` will be translated to `"true"`, `False` will be translated to `"false"`, and `None` will be translated to `"null"`. """ - if not isinstance(value, undefined.Undefined): + if not value is undefined.UNDEFINED: if conversion is not None: value = conversion(value) @@ -157,7 +156,7 @@ def put( class JSONObjectBuilder(typing.Dict[str, JSONAny]): """Helper class used to quickly build JSON objects from various values. - If provided with any values that are `hikari.utilities.undefined.Undefined`, + If provided with any values that are `hikari.utilities.undefined.UndefinedType`, then these values will be ignored. This speeds up generation of JSON payloads for low level HTTP and websocket @@ -190,14 +189,14 @@ def put( ---------- key : str The key to give the element. - value : JSONAny or typing.Any or hikari.utilities.undefined.Undefined + value : JSONAny or typing.Any or hikari.utilities.undefined.UndefinedType The JSON type to put. This may be a non-JSON type if a conversion is also specified. This may alternatively be undefined. In the latter case, nothing is performed. conversion : typing.Callable[[typing.Any], JSONAny] or None Optional conversion to apply. """ - if not isinstance(value, undefined.Undefined): + if not value is undefined.UNDEFINED: if conversion is not None: self[key] = conversion(value) else: @@ -206,7 +205,7 @@ def put( def put_array( self, key: str, - values: typing.Union[undefined.Undefined, typing.Iterable[T]], + values: typing.Union[undefined.UndefinedType, typing.Iterable[T]], /, *, conversion: typing.Optional[typing.Callable[[T], JSONAny]] = None, @@ -221,35 +220,38 @@ def put_array( ---------- key : str The key to give the element. - values : JSONAny or Any or hikari.utilities.undefined.Undefined + values : JSONAny or Any or hikari.utilities.undefined.UndefinedType The JSON types to put. This may be an iterable of non-JSON types if a conversion is also specified. This may alternatively be undefined. In the latter case, nothing is performed. conversion : typing.Callable[[typing.Any], JSONType] or None Optional conversion to apply. """ - if not isinstance(values, undefined.Undefined): + if not values is undefined.UNDEFINED: if conversion is not None: self[key] = [conversion(value) for value in values] else: self[key] = list(values) - def put_snowflake(self, key: str, value: typing.Union[undefined.Undefined, bases.UniqueObject], /) -> None: + def put_snowflake(self, key: str, value: typing.Union[undefined.UndefinedType, bases.UniqueObject], /) -> None: """Put a key with a snowflake value into the builder. Parameters ---------- key : str The key to give the element. - value : JSONAny or hikari.utilities.undefined.Undefined + value : JSONAny or hikari.utilities.undefined.UndefinedType The JSON type to put. This may alternatively be undefined. In the latter case, nothing is performed. """ - if not isinstance(value, undefined.Undefined): + if not value is undefined.UNDEFINED: self[key] = str(int(value)) def put_snowflake_array( - self, key: str, values: typing.Union[undefined.Undefined, typing.Iterable[typing.Union[bases.UniqueObject]]], / + self, + key: str, + values: typing.Union[undefined.UndefinedType, typing.Iterable[typing.Union[bases.UniqueObject]]], + /, ) -> None: """Put an array of snowflakes with the given key into this builder. @@ -261,11 +263,11 @@ def put_snowflake_array( ---------- key : str The key to give the element. - values : typing.Iterable[typing.SupportsInt] or hikari.utilities.undefined.Undefined + values : typing.Iterable[typing.SupportsInt] or hikari.utilities.undefined.UndefinedType The JSON snowflakes to put. This may alternatively be undefined. In the latter case, nothing is performed. """ - if not isinstance(values, undefined.Undefined): + if not values is undefined.UNDEFINED: self[key] = [str(int(value)) for value in values] diff --git a/hikari/utilities/files.py b/hikari/utilities/files.py index a604b27bfb..694cd4c03c 100644 --- a/hikari/utilities/files.py +++ b/hikari/utilities/files.py @@ -49,12 +49,12 @@ import attr from hikari.net import http_client -from hikari.utilities import klass +from hikari.utilities import reflect if typing.TYPE_CHECKING: import concurrent.futures -_LOGGER: typing.Final[logging.Logger] = klass.get_logger(__name__) +_LOGGER: typing.Final[logging.Logger] = reflect.get_logger(__name__) _MAGIC: typing.Final[int] = 50 * 1024 diff --git a/hikari/utilities/klass.py b/hikari/utilities/klass.py deleted file mode 100644 index c93bcfcb47..0000000000 --- a/hikari/utilities/klass.py +++ /dev/null @@ -1,109 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# Copyright © Nekokatt 2019-2020 -# -# This file is part of Hikari. -# -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . -"""Various metatypes and utilities for configuring classes.""" - -from __future__ import annotations - -__all__: typing.List[str] = ["get_logger", "SingletonMeta", "Singleton"] - -import abc -import logging -import typing - - -def get_logger(obj: typing.Union[typing.Type[typing.Any], typing.Any], *additional_args: str) -> logging.Logger: - """Get an appropriately named logger for the given class or object. - - Parameters - ---------- - obj : typing.Type or object - A type or instance of a type to make a logger in the name of. - *additional_args : str - Additional tokens to append onto the logger name, separated by `.`. - This is useful in some places to append info such as shard ID to each - logger to enable shard-specific logging, for example. - - Returns - ------- - logging.Logger - The logger to use. - """ - if isinstance(obj, str): - return logging.getLogger(obj) - - obj = obj if isinstance(obj, type) else type(obj) - return logging.getLogger(".".join((obj.__module__, obj.__qualname__, *additional_args))) - - -class SingletonMeta(abc.ABCMeta): - """Metaclass that makes the class a singleton. - - Once an instance has been defined at runtime, it will exist until the - interpreter that created it is terminated. - - Examples - -------- - >>> class Unknown(metaclass=SingletonMeta): - ... def __init__(self): - ... print("Initialized an Unknown!") - - >>> Unknown() is Unknown() # True - - !!! note - The constructors of instances of this metaclass must not take any - arguments other than `self`. - - !!! warning - Constructing instances of class instances of this metaclass may not be - thread safe. - """ - - __slots__ = () - - ___instances___: typing.Dict[typing.Type[typing.Any], typing.Any] = {} - - def __call__(cls, *args: typing.Any, **kwargs: typing.Any) -> typing.Any: - if cls not in SingletonMeta.___instances___: - SingletonMeta.___instances___[cls] = super().__call__(*args, **kwargs) - return SingletonMeta.___instances___[cls] - - -class Singleton(abc.ABC, metaclass=SingletonMeta): - """Base type for anything implementing the `SingletonMeta` metaclass. - - Once an instance has been defined at runtime, it will exist until the - interpreter that created it is terminated. - - Examples - -------- - >>> class MySingleton(Singleton): - ... pass - - >>> assert MySingleton() is MySingleton() - - !!! note - The constructors of child classes must not take any arguments other than - `self`. - - !!! warning - Constructing instances of this class or derived classes may not be - thread safe. - """ - - __slots__ = () diff --git a/hikari/utilities/reflect.py b/hikari/utilities/reflect.py index 6c1e862fa7..023cab43ff 100644 --- a/hikari/utilities/reflect.py +++ b/hikari/utilities/reflect.py @@ -20,9 +20,10 @@ from __future__ import annotations -__all__: typing.List[str] = ["resolve_signature", "EMPTY"] +__all__: typing.List[str] = ["resolve_signature", "EMPTY", "get_logger"] import inspect +import logging import typing EMPTY: typing.Final[inspect.Parameter.empty] = inspect.Parameter.empty @@ -65,3 +66,27 @@ def resolve_signature(func: typing.Callable[..., typing.Any]) -> inspect.Signatu return_annotation = None return signature.replace(parameters=params, return_annotation=return_annotation) + + +def get_logger(obj: typing.Union[typing.Type[typing.Any], typing.Any], *additional_args: str) -> logging.Logger: + """Get an appropriately named logger for the given class or object. + + Parameters + ---------- + obj : typing.Type or object + A type or instance of a type to make a logger in the name of. + *additional_args : str + Additional tokens to append onto the logger name, separated by `.`. + This is useful in some places to append info such as shard ID to each + logger to enable shard-specific logging, for example. + + Returns + ------- + logging.Logger + The logger to use. + """ + if isinstance(obj, str): + return logging.getLogger(obj) + + obj = obj if isinstance(obj, type) else type(obj) + return logging.getLogger(".".join((obj.__module__, obj.__qualname__, *additional_args))) diff --git a/hikari/utilities/undefined.py b/hikari/utilities/undefined.py index 537c20fc9b..5c52d05f33 100644 --- a/hikari/utilities/undefined.py +++ b/hikari/utilities/undefined.py @@ -20,92 +20,51 @@ from __future__ import annotations -__all__: typing.List[str] = ["Undefined"] +__all__: typing.List[str] = ["UndefinedType", "UNDEFINED"] +# noinspection PyUnresolvedReferences import typing -from hikari.utilities import klass - - -class Undefined(klass.Singleton): - """A singleton value that represents an undefined field or argument. - - Undefined will always have a falsy value. - - This type exists to allow differentiation between values that are - "optional" in the sense that they can be `None` (which is the - definition of "optional" in Python's `typing` module), and - values that are truly "optional" in that they may not be present. - - Some cases in Discord's API exist where passing a value as `None` or - `null` have totally different semantics to not passing the value at all. - The most noticeable case of this is for "patch" REST endpoints where - specifying a value will cause it to be updated, but not specifying it - will result in it being left alone. - - This type differs from `None` in a few different ways. Firstly, - it will only ever be considered equal to itself, thus the following will - always be false. - - >>> Undefined() == None - False - - The type will always be equatable to itself. - - >>> Undefined() == Undefined() - True - - The second differentiation is that you always instantiate this class to - obtain an instance of it. - - >>> undefined_value = Undefined() - - ...since this is a singleton, this value will always return the same - physical object. This improves efficiency. - - The third differentiation is that you can iterate across an undefined value. - This is used to simplify logic elsewhere. The behaviour of iterating across - an undefined value is to simply return an iterator that immediately - completes. - - >>> [*Undefined()] - [] - - This type cannot be mutated, subclassed, or have attributes removed from it - using conventional methods. - """ +class _UndefinedType: __slots__ = () def __bool__(self) -> bool: return False - def __iter__(self) -> typing.Iterator[typing.Any]: - yield from () - - def __init_subclass__(cls, **kwargs: typing.Any) -> None: - raise TypeError("Cannot subclass Undefined type") + def __init_subclass__(cls, **kwargs) -> None: + raise TypeError("Cannot subclass UndefinedType") def __repr__(self) -> str: - return f"{type(self).__name__}()" + return "" def __str__(self) -> str: - return type(self).__name__.upper() + return "UNDEFINED" + + +# Only expose correctly for static type checkers. Prevents anyone misusing it +# outside of simply checking `if value is UNDEFINED`. +UndefinedType = _UndefinedType if typing.TYPE_CHECKING else object() +"""Type hint describing the type of `UNDEFINED` used for type hints + +This is a purely sentinel type hint at runtime, and will not support instance +checking. +""" + +UNDEFINED: typing.Final[_UndefinedType] = _UndefinedType() +"""Undefined sentinel value. + +This will behave as a false value in conditions. +""" - def __setattr__(self, _: str, __: typing.Any) -> typing.NoReturn: - raise TypeError("Cannot modify Undefined type") +# Prevent making any more instances as much as possible. +_UndefinedType.__new__ = NotImplemented +_UndefinedType.__init__ = NotImplemented - def __delattr__(self, _: str) -> typing.NoReturn: - raise TypeError("Cannot modify Undefined type") +# Remove the reference here. +del _UndefinedType - @staticmethod - def count(*objs: typing.Any) -> int: - """Count how many of the given objects are undefined values. - Returns - ------- - int - The number of undefined values given. - """ - undefined = Undefined() - return sum(o is undefined for o in objs) +def count(*items: typing.Any) -> int: + """Count the number of items that are provided that are UNDEFINED.""" + return sum(1 for item in items if item is UNDEFINED) diff --git a/tests/hikari/impl/test_entity_factory.py b/tests/hikari/impl/test_entity_factory.py index 8d9204c95e..d7222f4f14 100644 --- a/tests/hikari/impl/test_entity_factory.py +++ b/tests/hikari/impl/test_entity_factory.py @@ -1233,7 +1233,7 @@ def test_deserialize_member_with_null_fields(self, entity_factory_impl, user_pay assert member.is_deaf is False assert member.is_mute is True assert isinstance(member, guild_models.Member) - assert member.joined_at is undefined.Undefined() + assert member.joined_at is undefined.UNDEFINED def test_deserialize_member_with_undefined_fields(self, entity_factory_impl, user_payload): member = entity_factory_impl.deserialize_member( @@ -1243,10 +1243,10 @@ def test_deserialize_member_with_undefined_fields(self, entity_factory_impl, use "user": user_payload, } ) - assert member.nickname is undefined.Undefined() - assert member.premium_since is undefined.Undefined() - assert member.is_deaf is undefined.Undefined() - assert member.is_mute is undefined.Undefined() + assert member.nickname is undefined.UNDEFINED + assert member.premium_since is undefined.UNDEFINED + assert member.is_deaf is undefined.UNDEFINED + assert member.is_mute is undefined.UNDEFINED def test_deserialize_member_with_passed_through_user_object(self, entity_factory_impl): mock_user = mock.MagicMock(user_models.User) @@ -2190,12 +2190,12 @@ def test_deserialize_member_presence_with_unset_fields( assert presence.client_status.web is presence_models.Status.OFFLINE # PresenceUser assert presence.user.id == 42 - assert presence.user.discriminator is undefined.Undefined() - assert presence.user.username is undefined.Undefined() - assert presence.user.avatar_hash is undefined.Undefined() - assert presence.user.is_bot is undefined.Undefined() - assert presence.user.is_system is undefined.Undefined() - assert presence.user.flags is undefined.Undefined() + assert presence.user.discriminator is undefined.UNDEFINED + assert presence.user.username is undefined.UNDEFINED + assert presence.user.avatar_hash is undefined.UNDEFINED + assert presence.user.is_bot is undefined.UNDEFINED + assert presence.user.is_system is undefined.UNDEFINED + assert presence.user.flags is undefined.UNDEFINED def test_deserialize_member_presence_with_unset_activity_fields(self, entity_factory_impl, user_payload): presence = entity_factory_impl.deserialize_member_presence( @@ -2885,29 +2885,29 @@ def test_deserialize_message_update_event_with_partial_payload(self, entity_fact message_update = entity_factory_impl.deserialize_message_update_event({"id": "42424242"}) assert message_update.message.id == 42424242 - assert message_update.message.channel_id is undefined.Undefined() - assert message_update.message.guild_id is undefined.Undefined() - assert message_update.message.author is undefined.Undefined() - assert message_update.message.member is undefined.Undefined() - assert message_update.message.content is undefined.Undefined() - assert message_update.message.timestamp is undefined.Undefined() - assert message_update.message.edited_timestamp is undefined.Undefined() - assert message_update.message.is_tts is undefined.Undefined() - assert message_update.message.is_mentioning_everyone is undefined.Undefined() - assert message_update.message.user_mentions is undefined.Undefined() - assert message_update.message.role_mentions is undefined.Undefined() - assert message_update.message.channel_mentions is undefined.Undefined() - assert message_update.message.attachments is undefined.Undefined() - assert message_update.message.embeds is undefined.Undefined() - assert message_update.message.reactions is undefined.Undefined() - assert message_update.message.is_pinned is undefined.Undefined() - assert message_update.message.webhook_id is undefined.Undefined() - assert message_update.message.type is undefined.Undefined() - assert message_update.message.activity is undefined.Undefined() - assert message_update.message.application is undefined.Undefined() - assert message_update.message.message_reference is undefined.Undefined() - assert message_update.message.flags is undefined.Undefined() - assert message_update.message.nonce is undefined.Undefined() + assert message_update.message.channel_id is undefined.UNDEFINED + assert message_update.message.guild_id is undefined.UNDEFINED + assert message_update.message.author is undefined.UNDEFINED + assert message_update.message.member is undefined.UNDEFINED + assert message_update.message.content is undefined.UNDEFINED + assert message_update.message.timestamp is undefined.UNDEFINED + assert message_update.message.edited_timestamp is undefined.UNDEFINED + assert message_update.message.is_tts is undefined.UNDEFINED + assert message_update.message.is_mentioning_everyone is undefined.UNDEFINED + assert message_update.message.user_mentions is undefined.UNDEFINED + assert message_update.message.role_mentions is undefined.UNDEFINED + assert message_update.message.channel_mentions is undefined.UNDEFINED + assert message_update.message.attachments is undefined.UNDEFINED + assert message_update.message.embeds is undefined.UNDEFINED + assert message_update.message.reactions is undefined.UNDEFINED + assert message_update.message.is_pinned is undefined.UNDEFINED + assert message_update.message.webhook_id is undefined.UNDEFINED + assert message_update.message.type is undefined.UNDEFINED + assert message_update.message.activity is undefined.UNDEFINED + assert message_update.message.application is undefined.UNDEFINED + assert message_update.message.message_reference is undefined.UNDEFINED + assert message_update.message.flags is undefined.UNDEFINED + assert message_update.message.nonce is undefined.UNDEFINED def test_deserialize_message_update_event_with_null_fields(self, entity_factory_impl): message_update = entity_factory_impl.deserialize_message_update_event( diff --git a/tests/hikari/utilities/test_data_binding.py b/tests/hikari/utilities/test_data_binding.py index 475e794177..d817693100 100644 --- a/tests/hikari/utilities/test_data_binding.py +++ b/tests/hikari/utilities/test_data_binding.py @@ -46,7 +46,7 @@ def test_starts_empty(self): def test_put_undefined(self): mapping = data_binding.StringMapBuilder() - mapping.put("foo", undefined.Undefined()) + mapping.put("foo", undefined.UNDEFINED) assert dict(mapping) == {} def test_put_general_value_casts_to_str(self): @@ -117,7 +117,7 @@ def test_starts_empty(self): def test_put_undefined(self): builder = data_binding.JSONObjectBuilder() - builder.put("foo", undefined.Undefined()) + builder.put("foo", undefined.UNDEFINED) assert builder == {} def test_put_defined(self): @@ -142,7 +142,7 @@ def test_put_with_conversion_passes_raw_input_to_converter(self): def test_put_array_undefined(self): builder = data_binding.JSONObjectBuilder() - builder.put_array("dd", undefined.Undefined()) + builder.put_array("dd", undefined.UNDEFINED) assert builder == {} def test__put_array_defined(self): @@ -177,7 +177,7 @@ def test_put_array_with_conversion_passes_raw_input_to_converter(self): def test_put_snowflake_undefined(self): builder = data_binding.JSONObjectBuilder() - builder.put_snowflake("nya!", undefined.Undefined()) + builder.put_snowflake("nya!", undefined.UNDEFINED) assert builder == {} @pytest.mark.parametrize( diff --git a/tests/hikari/utilities/test_klass.py b/tests/hikari/utilities/test_klass.py deleted file mode 100644 index 05fe19d8f7..0000000000 --- a/tests/hikari/utilities/test_klass.py +++ /dev/null @@ -1,52 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# Copyright © Nekokatt 2019-2020 -# -# This file is part of Hikari. -# -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . -import pytest - -from hikari.utilities import klass - - -def test_SingletonMeta(): - class StubSingleton(metaclass=klass.SingletonMeta): - pass - - assert StubSingleton() is StubSingleton() - - -def test_Singleton(): - class StubSingleton(klass.Singleton): - pass - - assert StubSingleton() is StubSingleton() - - -class Class: - pass - - -@pytest.mark.parametrize( - ["args", "expected_name"], - [ - ([Class], f"{__name__}.Class"), - ([Class()], f"{__name__}.Class"), - ([Class, "Foooo", "bar", "123"], f"{__name__}.Class.Foooo.bar.123"), - ([Class(), "qux", "QUx", "940"], f"{__name__}.Class.qux.QUx.940"), - ], -) -def test_get_logger(args, expected_name): - assert klass.get_logger(*args).name == expected_name diff --git a/tests/hikari/utilities/test_reflect.py b/tests/hikari/utilities/test_reflect.py index 97e0d2eeef..c261cc9475 100644 --- a/tests/hikari/utilities/test_reflect.py +++ b/tests/hikari/utilities/test_reflect.py @@ -18,6 +18,8 @@ # along with Hikari. If not, see . import typing +import pytest + from hikari.utilities import reflect @@ -108,3 +110,20 @@ def foo(bar: typing.Optional[typing.Iterator[int]]): signature = reflect.resolve_signature(foo) assert signature.parameters["bar"].annotation == typing.Optional[typing.Iterator[int]] + + +class Class: + pass + + +@pytest.mark.parametrize( + ["args", "expected_name"], + [ + ([Class], f"{__name__}.Class"), + ([Class()], f"{__name__}.Class"), + ([Class, "Foooo", "bar", "123"], f"{__name__}.Class.Foooo.bar.123"), + ([Class(), "qux", "QUx", "940"], f"{__name__}.Class.qux.QUx.940"), + ], +) +def test_get_logger(args, expected_name): + assert reflect.get_logger(*args).name == expected_name diff --git a/tests/hikari/utilities/test_undefined.py b/tests/hikari/utilities/test_undefined.py index 5ace13a68f..076b2a1fab 100644 --- a/tests/hikari/utilities/test_undefined.py +++ b/tests/hikari/utilities/test_undefined.py @@ -24,39 +24,29 @@ class TestUndefined: def test_repr(self): - assert repr(undefined.Undefined()) == "Undefined()" + assert repr(undefined.UNDEFINED) == "Undefined()" def test_str(self): - assert str(undefined.Undefined()) == "UNDEFINED" + assert str(undefined.UNDEFINED) == "UNDEFINED" def test_bool(self): - assert bool(undefined.Undefined()) is False + assert bool(undefined.UNDEFINED) is False # noinspection PyComparisonWithNone def test_singleton_behaviour(self): - assert undefined.Undefined() is undefined.Undefined() - assert undefined.Undefined() == undefined.Undefined() - assert undefined.Undefined() != None - assert undefined.Undefined() != False + assert undefined.UNDEFINED is undefined.UNDEFINED + assert undefined.UNDEFINED == undefined.UNDEFINED + assert undefined.UNDEFINED != None + assert undefined.UNDEFINED != False @_helpers.assert_raises(type_=TypeError) def test_cannot_subclass(self): - class _(undefined.Undefined): + class _(undefined.UndefinedType): pass def test_count(self): - assert undefined.Undefined.count(9, 18, undefined.Undefined(), 36, undefined.Undefined(), 54) == 2 + assert undefined.count(9, 18, undefined.UNDEFINED, 36, undefined.UNDEFINED, 54) == 2 - def test_iter(self): - with pytest.raises(StopIteration): - next(iter(undefined.Undefined())) - - def test_modify(self): - u = undefined.Undefined() - with pytest.raises(TypeError): - u.foo = 12 - - def test_delete(self): - u = undefined.Undefined() - with pytest.raises(TypeError): - del u.count + @_helpers.assert_raises(type_=TypeError) + def test_cannot_reinstatiate(self): + type(undefined.UNDEFINED)() From c6f3aa0b45c656db20d904a41e99c2022440429c Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Fri, 12 Jun 2020 10:20:10 +0100 Subject: [PATCH 505/922] Fixes 300 new errors caused by change to Undefined type. --- ci/utils.nox.py | 2 ++ hikari/utilities/data_binding.py | 19 ++++++++++++------- hikari/utilities/undefined.py | 24 +++++++----------------- tests/hikari/utilities/test_undefined.py | 2 +- 4 files changed, 22 insertions(+), 25 deletions(-) diff --git a/ci/utils.nox.py b/ci/utils.nox.py index 46e28f6635..39b9fbe108 100644 --- a/ci/utils.nox.py +++ b/ci/utils.nox.py @@ -29,6 +29,8 @@ "hikari.egg-info", "public", ".coverage", + ".pytest_cache", + ".mypy_cache", ] diff --git a/hikari/utilities/data_binding.py b/hikari/utilities/data_binding.py index 77b01f87ba..275280cdc8 100644 --- a/hikari/utilities/data_binding.py +++ b/hikari/utilities/data_binding.py @@ -227,7 +227,13 @@ def put_array( conversion : typing.Callable[[typing.Any], JSONType] or None Optional conversion to apply. """ - if not values is undefined.UNDEFINED: + if values is not undefined.UNDEFINED: + # MyPy cannot always determine that a singleton is used as a singleton, + # since it isn't really possible to make our own true singletons, hacks + # can always work around it. Thus, we have to cast. This is basically a + # no-op normally. + values = typing.cast("typing.Iterable[T]", values) + if conversion is not None: self[key] = [conversion(value) for value in values] else: @@ -244,14 +250,12 @@ def put_snowflake(self, key: str, value: typing.Union[undefined.UndefinedType, b The JSON type to put. This may alternatively be undefined. In the latter case, nothing is performed. """ - if not value is undefined.UNDEFINED: + if value is not undefined.UNDEFINED: + value = typing.cast("bases.UniqueObject", value) self[key] = str(int(value)) def put_snowflake_array( - self, - key: str, - values: typing.Union[undefined.UndefinedType, typing.Iterable[typing.Union[bases.UniqueObject]]], - /, + self, key: str, values: typing.Union[undefined.UndefinedType, typing.Iterable[bases.UniqueObject]], /, ) -> None: """Put an array of snowflakes with the given key into this builder. @@ -267,7 +271,8 @@ def put_snowflake_array( The JSON snowflakes to put. This may alternatively be undefined. In the latter case, nothing is performed. """ - if not values is undefined.UNDEFINED: + if values is not undefined.UNDEFINED: + values = typing.cast("typing.Iterable[bases.UniqueObject]", values) self[key] = [str(int(value)) for value in values] diff --git a/hikari/utilities/undefined.py b/hikari/utilities/undefined.py index 5c52d05f33..5816369c43 100644 --- a/hikari/utilities/undefined.py +++ b/hikari/utilities/undefined.py @@ -26,13 +26,15 @@ import typing -class _UndefinedType: +class UndefinedType: + """Type of the `UNDEFINED` sentinel value.""" + __slots__ = () def __bool__(self) -> bool: return False - def __init_subclass__(cls, **kwargs) -> None: + def __init_subclass__(cls, **kwargs: typing.Any) -> None: raise TypeError("Cannot subclass UndefinedType") def __repr__(self) -> str: @@ -42,27 +44,15 @@ def __str__(self) -> str: return "UNDEFINED" -# Only expose correctly for static type checkers. Prevents anyone misusing it -# outside of simply checking `if value is UNDEFINED`. -UndefinedType = _UndefinedType if typing.TYPE_CHECKING else object() -"""Type hint describing the type of `UNDEFINED` used for type hints - -This is a purely sentinel type hint at runtime, and will not support instance -checking. -""" - -UNDEFINED: typing.Final[_UndefinedType] = _UndefinedType() +# noinspection PyTypeChecker +UNDEFINED: typing.Final[UndefinedType] = UndefinedType() """Undefined sentinel value. This will behave as a false value in conditions. """ # Prevent making any more instances as much as possible. -_UndefinedType.__new__ = NotImplemented -_UndefinedType.__init__ = NotImplemented - -# Remove the reference here. -del _UndefinedType +setattr(UndefinedType, "__new__", NotImplemented) def count(*items: typing.Any) -> int: diff --git a/tests/hikari/utilities/test_undefined.py b/tests/hikari/utilities/test_undefined.py index 076b2a1fab..2ef5d7b594 100644 --- a/tests/hikari/utilities/test_undefined.py +++ b/tests/hikari/utilities/test_undefined.py @@ -24,7 +24,7 @@ class TestUndefined: def test_repr(self): - assert repr(undefined.UNDEFINED) == "Undefined()" + assert repr(undefined.UNDEFINED) == "" def test_str(self): assert str(undefined.UNDEFINED) == "UNDEFINED" From 0649b1e99f0bec1862c99920f04395e6c32bc02e Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Fri, 12 Jun 2020 10:36:55 +0100 Subject: [PATCH 506/922] Amended bug in REST where embeds module was only imported by static type checkers. More type checking. --- hikari/api/app.py | 105 +++++++---------------- hikari/api/component.py | 6 +- hikari/api/event_dispatcher.py | 12 ++- hikari/events/other.py | 2 +- hikari/impl/cache.py | 4 +- hikari/impl/entity_factory.py | 21 +++-- hikari/impl/event_manager_core.py | 44 ++++++---- hikari/impl/gateway_zookeeper.py | 11 +-- hikari/impl/rest_app.py | 1 + hikari/models/audit_logs.py | 4 +- hikari/models/bases.py | 6 +- hikari/models/webhooks.py | 2 + hikari/net/rest.py | 2 +- hikari/net/rest_utils.py | 3 +- hikari/utilities/data_binding.py | 8 -- hikari/utilities/files.py | 6 ++ hikari/utilities/reflect.py | 13 ++- hikari/utilities/undefined.py | 30 ++++--- tests/hikari/impl/test_entity_factory.py | 4 +- tests/hikari/utilities/test_undefined.py | 6 -- 20 files changed, 140 insertions(+), 150 deletions(-) diff --git a/hikari/api/app.py b/hikari/api/app.py index b37256a6e5..f3866219e1 100644 --- a/hikari/api/app.py +++ b/hikari/api/app.py @@ -20,14 +20,14 @@ from __future__ import annotations -__all__: typing.List[str] = ["IApp", "IRESTApp", "IGatewayConsumer", "IGatewayDispatcher", "IGatewayZookeeper", "IBot"] +__all__: typing.List[str] = ["IRESTApp", "IGatewayConsumer", "IGatewayDispatcher", "IGatewayZookeeper", "IBot"] import abc -import functools import logging import typing from hikari.api import event_dispatcher as event_dispatcher_ +from hikari.utilities import reflect from hikari.utilities import undefined if typing.TYPE_CHECKING: @@ -46,44 +46,29 @@ from hikari.net import rest as rest_ -class IApp(abc.ABC): - """The base for any type of Hikari application object. - - All types of Hikari-based application should derive from this type in order - to provide a consistent interface that is compatible with models and events - that make reference to it. +class IRESTApp(abc.ABC): + """Component specialization that is used for REST-only applications. - Following this pattern allows you to extend this library in pretty much - any direction you can think of without having to rewrite major piece of - this code base. + Examples may include web dashboards, or applications where no gateway + connection is required. As a result, no event conduit is provided by + these implementations. They do however provide a REST client, and the + general components defined in `IRESTApp` + """ - Example - ------- - A quick and dirty theoretical concrete implementation may look like the - following. + __slots__ = () - ```py - class MyApp(IApp): - def __init__(self): - self._logger = logging.getLogger(__name__) - self._cache = MyCacheImplementation(self) - self._entity_factory = MyEntityFactoryImplementation(self) - self._thread_pool = concurrent.futures.ThreadPoolExecutor() - - logger = property(lambda self: self._logger) - cache = property(lambda self: self._cache) - entity_factory = property(lambda self: self._entity_factory) - thread_pool = property(lambda self: self._thread_pool) - - async def close(self): - self._thread_pool.shutdown() - ``` + @property + @abc.abstractmethod + def rest(self) -> rest_.REST: + """REST API Client. - If you are in any doubt, check out the `hikari.impl.rest_app.RESTAppImpl` and - `hikari.impl.bot.BotImpl` implementations to see how they are pieced together! - """ + Use this to make calls to Discord's REST API over HTTPS. - __slots__ = () + Returns + ------- + hikari.net.rest.REST + The REST API client. + """ @property @abc.abstractmethod @@ -136,32 +121,7 @@ async def close(self) -> None: """Safely shut down all resources.""" -class IRESTApp(IApp, abc.ABC): - """Component specialization that is used for REST-only applications. - - Examples may include web dashboards, or applications where no gateway - connection is required. As a result, no event conduit is provided by - these implementations. They do however provide a REST client, and the - general components defined in `IApp` - """ - - __slots__ = () - - @property - @abc.abstractmethod - def rest(self) -> rest_.REST: - """REST API Client. - - Use this to make calls to Discord's REST API over HTTPS. - - Returns - ------- - hikari.net.rest.REST - The REST API client. - """ - - -class IGatewayConsumer(IApp, abc.ABC): +class IGatewayConsumer(IRESTApp, abc.ABC): """Component specialization that supports consumption of raw events. This may be combined with `IGatewayZookeeper` for most single-process @@ -186,7 +146,7 @@ def event_consumer(self) -> event_consumer_.IEventConsumer: """ -class IGatewayDispatcher(IApp, abc.ABC): +class IGatewayDispatcher(abc.ABC): """Component specialization that supports dispatching of events. These events are expected to be instances of @@ -213,7 +173,6 @@ class IGatewayDispatcher(IApp, abc.ABC): >>> @bot.listen(hikari.MessageCreateEvent) >>> async def on_message(event: hikari.MessageCreateEvent) -> None: ... ``` - """ __slots__ = () @@ -250,39 +209,35 @@ def event_dispatcher(self) -> event_dispatcher_.IEventDispatcher: The event dispatcher in use. """ - # Do not add type hints to me! I delegate to a documented method elsewhere! - @functools.wraps(event_dispatcher_.IEventDispatcher.listen) + @reflect.steal_docstring_from(event_dispatcher_.IEventDispatcher.listen) def listen( self, event_type: typing.Union[undefined.UndefinedType, typing.Type[_EventT]] = undefined.UNDEFINED, ) -> typing.Callable[[_CallbackT], _CallbackT]: ... - # Do not add type hints to me! I delegate to a documented method elsewhere! - @functools.wraps(event_dispatcher_.IEventDispatcher.subscribe) + @reflect.steal_docstring_from(event_dispatcher_.IEventDispatcher.subscribe) def subscribe( self, event_type: typing.Type[_EventT], callback: typing.Callable[[_EventT], typing.Union[typing.Coroutine[None, typing.Any, None], None]], - ) -> None: + ) -> typing.Callable[[_EventT], typing.Coroutine[None, typing.Any, None]]: ... - # Do not add type hints to me! I delegate to a documented method elsewhere! - @functools.wraps(event_dispatcher_.IEventDispatcher.unsubscribe) + @reflect.steal_docstring_from(event_dispatcher_.IEventDispatcher.unsubscribe) def unsubscribe( self, event_type: typing.Type[_EventT], - callback: typing.Callable[[_EventT], typing.Union[typing.Coroutine[None, typing.Any, None], None]], + callback: typing.Callable[[_EventT], typing.Coroutine[None, typing.Any, None]], ) -> None: ... - # Do not add type hints to me! I delegate to a documented method elsewhere! - @functools.wraps(event_dispatcher_.IEventDispatcher.wait_for) + @reflect.steal_docstring_from(event_dispatcher_.IEventDispatcher.wait_for) async def wait_for( self, event_type: typing.Type[_EventT], predicate: _PredicateT, timeout: typing.Union[float, int, None], ) -> _EventT: ... - @functools.wraps(event_dispatcher_.IEventDispatcher.dispatch) + @reflect.steal_docstring_from(event_dispatcher_.IEventDispatcher.dispatch) def dispatch(self, event: base.HikariEvent) -> asyncio.Future[typing.Any]: ... @@ -396,7 +351,7 @@ def run(self) -> None: """ -class IBot(IRESTApp, IGatewayZookeeper, IGatewayDispatcher, abc.ABC): +class IBot(IGatewayZookeeper, IGatewayDispatcher, abc.ABC): """Base for bot applications. Bots are components that have access to a REST API, an event dispatcher, diff --git a/hikari/api/component.py b/hikari/api/component.py index 71f6227378..4c335ecdfd 100644 --- a/hikari/api/component.py +++ b/hikari/api/component.py @@ -33,7 +33,7 @@ class IComponent(abc.ABC): """A component that makes up part of the application. Objects that derive from this should usually be attributes on the - `hikari.api.app.IApp` object. + `hikari.api.app.IRESTApp` object. Examples -------- @@ -46,11 +46,11 @@ class IComponent(abc.ABC): @property @abc.abstractmethod - def app(self) -> app.IApp: + def app(self) -> app.IRESTApp: """Return the Application that owns this component. Returns ------- - hikari.api.app.IApp + hikari.api.app.IRESTApp The application implementation that owns this component. """ diff --git a/hikari/api/event_dispatcher.py b/hikari/api/event_dispatcher.py index 126b2d07eb..abcd128d26 100644 --- a/hikari/api/event_dispatcher.py +++ b/hikari/api/event_dispatcher.py @@ -130,7 +130,7 @@ def subscribe( self, event_type: typing.Type[_EventT], callback: typing.Callable[[_EventT], typing.Union[typing.Coroutine[None, typing.Any, None], None]], - ) -> None: + ) -> typing.Callable[[_EventT], typing.Coroutine[None, typing.Any, None]]: """Subscribe a given callback to a given event type. Parameters @@ -157,6 +157,14 @@ async def on_message(event): bot.subscribe(MessageCreateEvent, on_message) ``` + Returns + ------- + typing.Callable[[T], typing.Coroutine[None, typing.Any, None] + The event callback. If you did not pass a callback that was a + coroutine function, then this will be a coroutine function + wrapping your callback instead. This enables you to correctly + unsubscribe from the event again later. + See Also -------- IEventDispatcher.listen @@ -167,7 +175,7 @@ async def on_message(event): def unsubscribe( self, event_type: typing.Type[_EventT], - callback: typing.Callable[[_EventT], typing.Union[typing.Coroutine[None, typing.Any, None], None]], + callback: typing.Callable[[_EventT], typing.Coroutine[None, typing.Any, None]], ) -> None: """Unsubscribe a given callback from a given event type, if present. diff --git a/hikari/events/other.py b/hikari/events/other.py index 90d3650198..6fe0f277f6 100644 --- a/hikari/events/other.py +++ b/hikari/events/other.py @@ -58,7 +58,7 @@ class ExceptionEvent(base_events.HikariEvent): event: base_events.HikariEvent = attr.ib(repr=True) """The event that was being invoked when the exception occurred.""" - callback: typing.Callable[[base_events.HikariEvent], typing.Awaitable[None]] = attr.ib(repr=False) + callback: typing.Callable[[base_events.HikariEvent], typing.Coroutine[None, typing.Any, None]] = attr.ib(repr=False) """The event that was being invoked when the exception occurred.""" diff --git a/hikari/impl/cache.py b/hikari/impl/cache.py index 54a4446c01..cbe7584404 100644 --- a/hikari/impl/cache.py +++ b/hikari/impl/cache.py @@ -33,9 +33,9 @@ class InMemoryCacheImpl(cache.ICache): """In-memory cache implementation.""" - def __init__(self, app: app_.IApp) -> None: + def __init__(self, app: app_.IRESTApp) -> None: self._app = app @property - def app(self) -> app_.IApp: + def app(self) -> app_.IRESTApp: return self._app diff --git a/hikari/impl/entity_factory.py b/hikari/impl/entity_factory.py index 9074c8c8fe..4d2a7efd99 100644 --- a/hikari/impl/entity_factory.py +++ b/hikari/impl/entity_factory.py @@ -89,9 +89,9 @@ class EntityFactoryImpl(entity_factory.IEntityFactory): This will convert objects to/from JSON compatible representations. """ - def __init__(self, app: app_.IApp) -> None: + def __init__(self, app: app_.IRESTApp) -> None: self._app = app - self._audit_log_entry_converters = { + self._audit_log_entry_converters: typing.Mapping[str, typing.Callable[[typing.Any], typing.Any]] = { audit_log_models.AuditLogChangeKey.OWNER_ID: snowflake.Snowflake, audit_log_models.AuditLogChangeKey.AFK_CHANNEL_ID: snowflake.Snowflake, audit_log_models.AuditLogChangeKey.AFK_TIMEOUT: _deserialize_seconds_timedelta, @@ -124,7 +124,10 @@ def __init__(self, app: app_.IApp) -> None: audit_log_models.AuditLogChangeKey.REMOVE_ROLE_FROM_MEMBER: self._deserialize_audit_log_change_roles, audit_log_models.AuditLogChangeKey.PERMISSION_OVERWRITES: self._deserialize_audit_log_overwrites, } - self._audit_log_event_mapping = { + self._audit_log_event_mapping: typing.Mapping[ + typing.Union[int, audit_log_models.AuditLogEventType], + typing.Callable[[data_binding.JSONObject], audit_log_models.BaseAuditLogEntryInfo], + ] = { audit_log_models.AuditLogEventType.CHANNEL_OVERWRITE_CREATE: self._deserialize_channel_overwrite_entry_info, audit_log_models.AuditLogEventType.CHANNEL_OVERWRITE_UPDATE: self._deserialize_channel_overwrite_entry_info, audit_log_models.AuditLogEventType.CHANNEL_OVERWRITE_DELETE: self._deserialize_channel_overwrite_entry_info, @@ -147,7 +150,7 @@ def __init__(self, app: app_.IApp) -> None: } @property - def app(self) -> app_.IApp: + def app(self) -> app_.IRESTApp: return self._app ###################### @@ -156,10 +159,10 @@ def app(self) -> app_.IApp: def deserialize_own_connection(self, payload: data_binding.JSONObject) -> application_models.OwnConnection: own_connection = application_models.OwnConnection() - own_connection.id = snowflake.Snowflake(payload["id"]) + own_connection.id = payload["id"] # this is not a snowflake! own_connection.name = payload["name"] own_connection.type = payload["type"] - own_connection.is_revoked = payload.get("revoked") + own_connection.is_revoked = payload["revoked"] own_connection.integrations = [ self.deserialize_partial_integration(integration) for integration in payload.get("integrations", ()) ] @@ -587,8 +590,8 @@ def serialize_embed( self, embed: embed_models.Embed, ) -> typing.Tuple[data_binding.JSONObject, typing.List[files.Resource]]: - payload = {} - uploads = [] + payload: data_binding.JSONObject = {} + uploads: typing.List[files.Resource] = [] if embed.title is not None: payload["title"] = embed.title @@ -603,7 +606,7 @@ def serialize_embed( payload["timestamp"] = embed.timestamp.isoformat() if embed.color is not None: - payload["color"] = embed.color + payload["color"] = str(embed.color) if embed.footer is not None: footer_payload = {} diff --git a/hikari/impl/event_manager_core.py b/hikari/impl/event_manager_core.py index 9e5531cb6a..6d0055c710 100644 --- a/hikari/impl/event_manager_core.py +++ b/hikari/impl/event_manager_core.py @@ -39,12 +39,12 @@ if typing.TYPE_CHECKING: from hikari.api import app as app_ - _EventT = typing.TypeVar("_EventT", bound=base.HikariEvent, covariant=True) + _EventT = typing.TypeVar("_EventT", bound=base.HikariEvent, contravariant=True) _PredicateT = typing.Callable[[_EventT], typing.Union[bool, typing.Coroutine[None, typing.Any, bool]]] _SyncCallbackT = typing.Callable[[_EventT], None] _AsyncCallbackT = typing.Callable[[_EventT], typing.Coroutine[None, typing.Any, None]] _CallbackT = typing.Union[_SyncCallbackT, _AsyncCallbackT] - _ListenerMapT = typing.MutableMapping[typing.Type[_EventT], typing.MutableSequence[_CallbackT]] + _ListenerMapT = typing.MutableMapping[typing.Type[_EventT], typing.MutableSequence[_AsyncCallbackT]] _WaiterT = typing.Tuple[_PredicateT, asyncio.Future[_EventT]] _WaiterMapT = typing.MutableMapping[typing.Type[_EventT], typing.MutableSet[_WaiterT]] @@ -56,14 +56,14 @@ class EventManagerCore(event_dispatcher.IEventDispatcher, event_consumer.IEventC is the raw event name being dispatched in lower-case. """ - def __init__(self, app: app_.IApp) -> None: + def __init__(self, app: app_.IRESTApp) -> None: self._app = app self._listeners: _ListenerMapT = {} self._waiters: _WaiterMapT = {} self.logger = reflect.get_logger(self) @property - def app(self) -> app_.IApp: + def app(self) -> app_.IRESTApp: return self._app async def consume_raw_event( @@ -79,17 +79,19 @@ def subscribe( self, event_type: typing.Type[_EventT], callback: typing.Callable[[_EventT], typing.Union[typing.Coroutine[None, typing.Any, None], None]], - ) -> None: + ) -> typing.Callable[[_EventT], typing.Coroutine[None, typing.Any, None]]: if event_type not in self._listeners: self._listeners[event_type] = [] if not asyncio.iscoroutinefunction(callback): @functools.wraps(callback) - async def wrapper(event): - return callback(event) + async def wrapper(event: _EventT) -> None: + callback(event) self.subscribe(event_type, wrapper) + + return wrapper else: self.logger.debug( "subscribing callback 'async def %s%s' to event-type %s.%s", @@ -98,12 +100,16 @@ async def wrapper(event): event_type.__module__, event_type.__qualname__, ) + + callback = typing.cast("typing.Callable[[_EventT], typing.Coroutine[None, typing.Any, None]]", callback) self._listeners[event_type].append(callback) + return callback + def unsubscribe( self, event_type: typing.Type[_EventT], - callback: typing.Callable[[_EventT], typing.Union[typing.Coroutine[None, typing.Any, None], None]], + callback: typing.Callable[[_EventT], typing.Coroutine[None, typing.Any, None]], ) -> None: if event_type in self._listeners: self.logger.debug( @@ -137,6 +143,9 @@ def decorator(callback: _CallbackT) -> _CallbackT: event_type = event_param.annotation + if not isinstance(event_type, type) or not issubclass(event_type, base.HikariEvent): + raise TypeError("Event type must derive from HikariEvent") + self.subscribe(event_type, callback) return callback @@ -146,7 +155,7 @@ async def wait_for( self, event_type: typing.Type[_EventT], predicate: _PredicateT, timeout: typing.Union[float, int, None] ) -> _EventT: - future = asyncio.get_event_loop().create_future() + future: asyncio.Future[_EventT] = asyncio.get_event_loop().create_future() if event_type not in self._waiters: self._waiters[event_type] = set() @@ -155,11 +164,13 @@ async def wait_for( return await asyncio.wait_for(future, timeout=timeout) if timeout is not None else await future - async def _test_waiter(self, cls, event, predicate, future): + async def _test_waiter( + self, cls: typing.Type[_EventT], event: _EventT, predicate: _PredicateT, future: asyncio.Future[_EventT] + ) -> None: try: result = predicate(event) - if asyncio.iscoroutinefunction(result): - result = await result + if asyncio.iscoroutine(result): + result = await result # type: ignore if not result: return @@ -173,7 +184,7 @@ async def _test_waiter(self, cls, event, predicate, future): if not self._waiters[cls]: del self._waiters[cls] - async def _invoke_callback(self, callback: _CallbackT, event: _EventT) -> None: + async def _invoke_callback(self, callback: _AsyncCallbackT, event: _EventT) -> None: try: result = callback(event) if asyncio.iscoroutine(result): @@ -181,7 +192,7 @@ async def _invoke_callback(self, callback: _CallbackT, event: _EventT) -> None: except Exception as ex: # Skip the first frame in logs, we don't care for it. - trio = type(ex), ex, ex.__traceback__.tb_next + trio = type(ex), ex, ex.__traceback__.tb_next if ex.__traceback__ is not None else None if base.is_no_catch_event(event): self.logger.error("an exception occurred handling an event, but it has been ignored", exc_info=trio) @@ -198,12 +209,11 @@ def dispatch(self, event: base.HikariEvent) -> asyncio.Future[typing.Any]: # not describe event types. This improves efficiency as well. mro = type(event).mro() - tasks = [] + tasks: typing.List[typing.Coroutine[None, typing.Any, None]] = [] for cls in mro[: mro.index(base.HikariEvent) + 1]: - cls: typing.Type[_EventT] - if cls in self._listeners: + for callback in self._listeners[cls]: tasks.append(self._invoke_callback(callback, event)) diff --git a/hikari/impl/gateway_zookeeper.py b/hikari/impl/gateway_zookeeper.py index d56a5aee11..2e1f343911 100644 --- a/hikari/impl/gateway_zookeeper.py +++ b/hikari/impl/gateway_zookeeper.py @@ -36,6 +36,7 @@ from hikari.utilities import undefined if typing.TYPE_CHECKING: + from hikari.events import base as base_events from hikari.net import http_settings from hikari.models import gateway as gateway_models from hikari.models import intents as intents_ @@ -293,7 +294,7 @@ async def update_presence( *, status: typing.Union[undefined.UndefinedType, presences.Status] = undefined.UNDEFINED, activity: typing.Union[undefined.UndefinedType, presences.Activity, None] = undefined.UNDEFINED, - idle_since: typing.Union[undefined.UndefinedType, datetime.datetime] = undefined.UNDEFINED, + idle_since: typing.Union[undefined.UndefinedType, datetime.datetime, None] = undefined.UNDEFINED, is_afk: typing.Union[undefined.UndefinedType, bool] = undefined.UNDEFINED, ) -> None: await asyncio.gather( @@ -304,7 +305,7 @@ async def update_presence( ) ) - async def _init(self): + async def _init(self) -> None: gw_recs = await self._fetch_gateway_recommendations() self.logger.info( @@ -315,7 +316,7 @@ async def _init(self): ) self._shard_count = self._shard_count if self._shard_count else gw_recs.shard_count - self._shard_ids = self._shard_ids if self._shard_ids else range(self._shard_count) + self._shard_ids = self._shard_ids if self._shard_ids else set(range(self._shard_count)) self._max_concurrency = gw_recs.session_start_limit.max_concurrency url = gw_recs.url @@ -385,7 +386,7 @@ async def _run(self) -> None: await self.start() await self.join() - async def _maybe_dispatch(self, event) -> None: - if hasattr(self, "event_dispatcher"): + async def _maybe_dispatch(self, event: base_events.HikariEvent) -> None: + if isinstance(self, app_.IGatewayDispatcher): # noinspection PyUnresolvedReferences await self.event_dispatcher.dispatch(event) diff --git a/hikari/impl/rest_app.py b/hikari/impl/rest_app.py index e374da7522..b2d64e471e 100644 --- a/hikari/impl/rest_app.py +++ b/hikari/impl/rest_app.py @@ -35,6 +35,7 @@ from hikari.impl import entity_factory as entity_factory_impl from hikari.net import http_settings as http_settings_ from hikari.net import rest as rest_ +from hikari.utilities import reflect from hikari.utilities import undefined if typing.TYPE_CHECKING: diff --git a/hikari/models/audit_logs.py b/hikari/models/audit_logs.py index fb149b4d15..e8a5992818 100644 --- a/hikari/models/audit_logs.py +++ b/hikari/models/audit_logs.py @@ -287,7 +287,7 @@ class AuditLogEntry(bases.Entity, bases.Unique): user_id: typing.Optional[snowflake.Snowflake] = attr.ib(eq=False, hash=False, repr=True) """The ID of the user who made this change.""" - action_type: typing.Union[AuditLogEventType, str] = attr.ib(eq=False, hash=False, repr=True) + action_type: typing.Union[AuditLogEventType, int] = attr.ib(eq=False, hash=False, repr=True) """The type of action this entry represents.""" options: typing.Optional[BaseAuditLogEntryInfo] = attr.ib(eq=False, hash=False, repr=False) @@ -305,7 +305,7 @@ class AuditLog: entries: typing.Mapping[snowflake.Snowflake, AuditLogEntry] = attr.ib(repr=False) """A sequence of the audit log's entries.""" - integrations: typing.Mapping[snowflake.Snowflake, guilds.Integration] = attr.ib(repr=False) + integrations: typing.Mapping[snowflake.Snowflake, guilds.PartialIntegration] = attr.ib(repr=False) """A mapping of the partial objects of integrations found in this audit log.""" users: typing.Mapping[snowflake.Snowflake, users_.User] = attr.ib(repr=False) diff --git a/hikari/models/bases.py b/hikari/models/bases.py index 63caaaa71c..4bb4d096be 100644 --- a/hikari/models/bases.py +++ b/hikari/models/bases.py @@ -43,12 +43,10 @@ class Entity(abc.ABC): methods directly. """ - _AppT = typing.Union[app_.IRESTApp, app_.IBot] - - _app: _AppT = attr.ib(default=None, repr=False, eq=False, hash=False) + _app: app_.IRESTApp = attr.ib(default=None, repr=False, eq=False, hash=False) """The client application that models may use for procedures.""" - def __init__(self, app: _AppT) -> None: + def __init__(self, app: app_.IRESTApp) -> None: self._app = app diff --git a/hikari/models/webhooks.py b/hikari/models/webhooks.py index 9b4d1e2bcf..f96c55b70f 100644 --- a/hikari/models/webhooks.py +++ b/hikari/models/webhooks.py @@ -27,6 +27,7 @@ import attr +from hikari.api import app from hikari.models import bases from hikari.utilities import cdn from hikari.utilities import files as files_ @@ -329,6 +330,7 @@ async def fetch_self( ValueError If `use_token` is passed as `True` when `Webhook.token` is `None`. """ + if use_token and not self.token: raise ValueError("This webhook's token is unknown, so cannot be used.") diff --git a/hikari/net/rest.py b/hikari/net/rest.py index 791b5edf8a..38c28ce8f8 100644 --- a/hikari/net/rest.py +++ b/hikari/net/rest.py @@ -34,6 +34,7 @@ from hikari import errors from hikari.api import component from hikari.models import emojis +from hikari.models import embeds as embeds_ from hikari.net import buckets from hikari.net import http_client from hikari.net import http_settings @@ -57,7 +58,6 @@ from hikari.models import bases from hikari.models import channels from hikari.models import colors - from hikari.models import embeds as embeds_ from hikari.models import gateway from hikari.models import guilds from hikari.models import invites diff --git a/hikari/net/rest_utils.py b/hikari/net/rest_utils.py index 6e987a65a9..06d23f912b 100644 --- a/hikari/net/rest_utils.py +++ b/hikari/net/rest_utils.py @@ -252,8 +252,7 @@ async def create(self) -> guilds.Guild: payload.put("default_message_notifications", self.default_message_notifications) payload.put("explicit_content_filter", self.explicit_content_filter_level) - if not self.icon is undefined.UNDEFINED: - # This isn't annotated properly in the standard library, apparently. + if self.icon is not undefined.UNDEFINED: async with self.icon.stream(self._app.thread_pool_executor) as stream: data_uri = await stream.data_uri() payload.put("icon", data_uri) diff --git a/hikari/utilities/data_binding.py b/hikari/utilities/data_binding.py index 275280cdc8..54c9d19091 100644 --- a/hikari/utilities/data_binding.py +++ b/hikari/utilities/data_binding.py @@ -228,12 +228,6 @@ def put_array( Optional conversion to apply. """ if values is not undefined.UNDEFINED: - # MyPy cannot always determine that a singleton is used as a singleton, - # since it isn't really possible to make our own true singletons, hacks - # can always work around it. Thus, we have to cast. This is basically a - # no-op normally. - values = typing.cast("typing.Iterable[T]", values) - if conversion is not None: self[key] = [conversion(value) for value in values] else: @@ -251,7 +245,6 @@ def put_snowflake(self, key: str, value: typing.Union[undefined.UndefinedType, b case, nothing is performed. """ if value is not undefined.UNDEFINED: - value = typing.cast("bases.UniqueObject", value) self[key] = str(int(value)) def put_snowflake_array( @@ -272,7 +265,6 @@ def put_snowflake_array( In the latter case, nothing is performed. """ if values is not undefined.UNDEFINED: - values = typing.cast("typing.Iterable[bases.UniqueObject]", values) self[key] = [str(int(value)) for value in values] diff --git a/hikari/utilities/files.py b/hikari/utilities/files.py index 694cd4c03c..51c570908a 100644 --- a/hikari/utilities/files.py +++ b/hikari/utilities/files.py @@ -379,6 +379,12 @@ def filename(self) -> str: async def stream(self, *, executor: typing.Optional[concurrent.futures.ThreadPoolExecutor]) -> AsyncReader: """Return an async iterable of bytes to stream.""" + def __str__(self) -> str: + return self.url + + def __repr__(self) -> str: + return f"{type(self).__name__}(url={self.url!r}, filename={self.filename!r})" + class Bytes(Resource): """Representation of in-memory data to upload. diff --git a/hikari/utilities/reflect.py b/hikari/utilities/reflect.py index 023cab43ff..992885b918 100644 --- a/hikari/utilities/reflect.py +++ b/hikari/utilities/reflect.py @@ -20,7 +20,7 @@ from __future__ import annotations -__all__: typing.List[str] = ["resolve_signature", "EMPTY", "get_logger"] +__all__: typing.List[str] = ["resolve_signature", "EMPTY", "get_logger", "steal_docstring_from"] import inspect import logging @@ -90,3 +90,14 @@ def get_logger(obj: typing.Union[typing.Type[typing.Any], typing.Any], *addition obj = obj if isinstance(obj, type) else type(obj) return logging.getLogger(".".join((obj.__module__, obj.__qualname__, *additional_args))) + + +T = typing.TypeVar("T") + + +def steal_docstring_from(source: typing.Any) -> typing.Callable[[T], T]: + def decorator(obj: T) -> T: + setattr(obj, "__doc__", getattr(source, "__doc__", None)) + return obj + + return decorator diff --git a/hikari/utilities/undefined.py b/hikari/utilities/undefined.py index 5816369c43..556866a3fd 100644 --- a/hikari/utilities/undefined.py +++ b/hikari/utilities/undefined.py @@ -22,21 +22,18 @@ __all__: typing.List[str] = ["UndefinedType", "UNDEFINED"] +import enum + # noinspection PyUnresolvedReferences import typing -class UndefinedType: - """Type of the `UNDEFINED` sentinel value.""" - +class _UndefinedType: __slots__ = () def __bool__(self) -> bool: return False - def __init_subclass__(cls, **kwargs: typing.Any) -> None: - raise TypeError("Cannot subclass UndefinedType") - def __repr__(self) -> str: return "" @@ -44,16 +41,29 @@ def __str__(self) -> str: return "UNDEFINED" +# Using an enum enables us to use typing.Literal. MyPy has a special case for +# assuming that the number of instances of a specific enum is limited by design, +# whereas using a constant value does not provide that. In short, this allows +# MyPy to determine it can statically cast a value to a different type when +# we do `is` and `is not` checks on values, which removes the need for casts. +class _UndefinedTypeWrapper(_UndefinedType, enum.Enum): + UNDEFINED_VALUE = _UndefinedType() + + +# Prevent making any more instances as much as possible. +setattr(_UndefinedType, "__new__", NotImplemented) +setattr(_UndefinedTypeWrapper, "__new__", NotImplemented) + +UndefinedType = typing.Literal[_UndefinedTypeWrapper.UNDEFINED_VALUE] +"""A type hint for the literal `UNDEFINED` object.""" + # noinspection PyTypeChecker -UNDEFINED: typing.Final[UndefinedType] = UndefinedType() +UNDEFINED: typing.Final[UndefinedType] = _UndefinedTypeWrapper.UNDEFINED_VALUE """Undefined sentinel value. This will behave as a false value in conditions. """ -# Prevent making any more instances as much as possible. -setattr(UndefinedType, "__new__", NotImplemented) - def count(*items: typing.Any) -> int: """Count the number of items that are provided that are UNDEFINED.""" diff --git a/tests/hikari/impl/test_entity_factory.py b/tests/hikari/impl/test_entity_factory.py index d7222f4f14..f31bda4738 100644 --- a/tests/hikari/impl/test_entity_factory.py +++ b/tests/hikari/impl/test_entity_factory.py @@ -72,8 +72,8 @@ def test__deserialize_max_age_returns_null(): class TestEntityFactoryImpl: @pytest.fixture() - def mock_app(self) -> app_.IApp: - return mock.MagicMock(app_.IApp) + def mock_app(self) -> app_.IRESTApp: + return mock.MagicMock(app_.IRESTApp) @pytest.fixture() def entity_factory_impl(self, mock_app) -> entity_factory.EntityFactoryImpl: diff --git a/tests/hikari/utilities/test_undefined.py b/tests/hikari/utilities/test_undefined.py index 2ef5d7b594..1a4090060d 100644 --- a/tests/hikari/utilities/test_undefined.py +++ b/tests/hikari/utilities/test_undefined.py @@ -16,7 +16,6 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . -import pytest from hikari.utilities import undefined from tests.hikari import _helpers @@ -39,11 +38,6 @@ def test_singleton_behaviour(self): assert undefined.UNDEFINED != None assert undefined.UNDEFINED != False - @_helpers.assert_raises(type_=TypeError) - def test_cannot_subclass(self): - class _(undefined.UndefinedType): - pass - def test_count(self): assert undefined.count(9, 18, undefined.UNDEFINED, 36, undefined.UNDEFINED, 54) == 2 From 1dc331540431648caf4be3408d988d231e6a0804 Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Sat, 13 Jun 2020 17:37:21 +0100 Subject: [PATCH 507/922] Reorganised some app structuring to fix some typing issues. --- README.md | 4 +- hikari/__init__.py | 4 +- hikari/_about.py | 2 +- hikari/api/app.py | 379 ----------------------- hikari/api/bot.py | 60 ++++ hikari/api/cache.py | 4 +- hikari/api/component.py | 15 +- hikari/api/entity_factory.py | 4 +- hikari/api/event_consumer.py | 32 +- hikari/api/event_dispatcher.py | 128 ++++++-- hikari/api/gateway_zookeeper.py | 144 +++++++++ hikari/api/rest.py | 110 +++++++ hikari/events/base.py | 20 +- hikari/events/channel.py | 12 +- hikari/events/guild.py | 2 +- hikari/events/message.py | 18 +- hikari/events/other.py | 24 +- hikari/events/voice.py | 4 +- hikari/impl/bot.py | 76 +++-- hikari/impl/cache.py | 10 +- hikari/impl/entity_factory.py | 113 ++++--- hikari/impl/event_manager.py | 2 +- hikari/impl/event_manager_core.py | 58 ++-- hikari/impl/gateway_zookeeper.py | 140 +++++---- hikari/impl/{rest_app.py => rest.py} | 20 +- hikari/models/bases.py | 6 +- hikari/models/guilds.py | 16 +- hikari/models/invites.py | 2 + hikari/models/presences.py | 6 +- hikari/models/webhooks.py | 1 - hikari/net/buckets.py | 10 +- hikari/net/gateway.py | 8 +- hikari/net/iterators.py | 50 +-- hikari/net/rate_limits.py | 6 +- hikari/net/rest.py | 26 +- hikari/net/rest_utils.py | 6 +- hikari/utilities/files.py | 28 +- tests/hikari/impl/test_entity_factory.py | 6 +- 38 files changed, 818 insertions(+), 738 deletions(-) delete mode 100644 hikari/api/app.py create mode 100644 hikari/api/bot.py create mode 100644 hikari/api/gateway_zookeeper.py create mode 100644 hikari/api/rest.py rename hikari/impl/{rest_app.py => rest.py} (89%) diff --git a/README.md b/README.md index bf002b2847..7b1e3ad60f 100644 --- a/README.md +++ b/README.md @@ -76,12 +76,12 @@ Most mainstream Python Discord APIs lack one or more of the following features. implement each feature as part of the design, rather than an additional component. This enables you to utilize these components as a black box where necessary. -- Low level RESTSession API implementation. +- Low level REST API implementation. - Low level gateway websocket shard implementation. - Rate limiting that complies with the `X-RateLimit-Bucket` header __properly__. - Gateway websocket ratelimiting (prevents your websocket getting completely invalidated). - Intents. -- Proxy support for websockets and RESTSession API. +- Proxy support for websockets and REST API. - File IO that doesn't block you. - Fluent Pythonic API that does not limit your creativity. diff --git a/hikari/__init__.py b/hikari/__init__.py index 62dab14814..8ddbcf81cc 100644 --- a/hikari/__init__.py +++ b/hikari/__init__.py @@ -35,5 +35,5 @@ from hikari._about import __license__ from hikari._about import __url__ from hikari._about import __version__ -from hikari.impl.bot import BotImpl as Bot -from hikari.impl.rest_app import RESTAppImpl as RESTClient +from hikari.impl.bot import BotAppImpl as Bot +from hikari.impl.rest import RESTAppImpl as RESTClient diff --git a/hikari/_about.py b/hikari/_about.py index 1faea49cd3..2b14a70b61 100644 --- a/hikari/_about.py +++ b/hikari/_about.py @@ -24,7 +24,7 @@ __ci__ = "https://gitlab.com/nekokatt/hikari/pipelines" __copyright__ = "© 2019-2020 Nekokatt" __discord_invite__ = "https://discord.gg/Jx4cNGG" -__docs__ = "https://nekokatt.gitlab.io/hikari/index.html" +__docs__ = "https://nekokatt.gitlab.io/hikari" __email__ = "3903853-nekokatt@users.noreply.gitlab.com" __issue_tracker__ = "https://gitlab.com/nekokatt/hikari/issues" __license__ = "LGPL-3.0-ONLY" diff --git a/hikari/api/app.py b/hikari/api/app.py deleted file mode 100644 index f3866219e1..0000000000 --- a/hikari/api/app.py +++ /dev/null @@ -1,379 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# Copyright © Nekoka.tt 2019-2020 -# -# This file is part of Hikari. -# -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . -"""Core interfaces for types of Hikari application.""" - -from __future__ import annotations - -__all__: typing.List[str] = ["IRESTApp", "IGatewayConsumer", "IGatewayDispatcher", "IGatewayZookeeper", "IBot"] - -import abc -import logging -import typing - -from hikari.api import event_dispatcher as event_dispatcher_ -from hikari.utilities import reflect -from hikari.utilities import undefined - -if typing.TYPE_CHECKING: - import asyncio - import datetime - - from concurrent import futures - - from hikari.api import cache as cache_ - from hikari.api import entity_factory as entity_factory_ - from hikari.api import event_consumer as event_consumer_ - from hikari.events import base - from hikari.models import presences - from hikari.net import http_settings as http_settings_ - from hikari.net import gateway - from hikari.net import rest as rest_ - - -class IRESTApp(abc.ABC): - """Component specialization that is used for REST-only applications. - - Examples may include web dashboards, or applications where no gateway - connection is required. As a result, no event conduit is provided by - these implementations. They do however provide a REST client, and the - general components defined in `IRESTApp` - """ - - __slots__ = () - - @property - @abc.abstractmethod - def rest(self) -> rest_.REST: - """REST API Client. - - Use this to make calls to Discord's REST API over HTTPS. - - Returns - ------- - hikari.net.rest.REST - The REST API client. - """ - - @property - @abc.abstractmethod - def logger(self) -> logging.Logger: - """Logger for logging messages. - - Returns - ------- - logging.Logger - The application-level logger. - """ - - @property - @abc.abstractmethod - def cache(self) -> cache_.ICache: - """Entity cache. - - Returns - ------- - hikari.api.cache.ICache - The cache implementation used in this application. - """ - - @property - @abc.abstractmethod - def entity_factory(self) -> entity_factory_.IEntityFactory: - """Entity creator and updater facility. - - Returns - ------- - hikari.api.entity_factory.IEntityFactory - The factory object used to produce and update Python entities. - """ - - @property - @abc.abstractmethod - def thread_pool_executor(self) -> typing.Optional[futures.ThreadPoolExecutor]: - """Thread-pool to utilise for file IO within the library, if set. - - Returns - ------- - concurrent.futures.ThreadPoolExecutor or None - The custom thread-pool being used for blocking IO. If the - default event loop thread-pool is being used, then this will - return `None` instead. - """ - - @abc.abstractmethod - async def close(self) -> None: - """Safely shut down all resources.""" - - -class IGatewayConsumer(IRESTApp, abc.ABC): - """Component specialization that supports consumption of raw events. - - This may be combined with `IGatewayZookeeper` for most single-process - bots, or may be a specific component for large distributed applications - that consume events from a message queue, for example. - """ - - __slots__ = () - - @property - @abc.abstractmethod - def event_consumer(self) -> event_consumer_.IEventConsumer: - """Raw event consumer. - - This should be passed raw event payloads from your gateway - websocket implementation. - - Returns - ------- - hikari.api.event_consumer.IEventConsumer - The event consumer implementation in-use. - """ - - -class IGatewayDispatcher(abc.ABC): - """Component specialization that supports dispatching of events. - - These events are expected to be instances of - `hikari.events.base.HikariEvent`. - - This may be combined with `IGatewayZookeeper` for most single-process - bots, or may be a specific component for large distributed applications - that consume events from a message queue, for example. - - This purposely also implements some calls found in - `hikari.api.event_dispatcher.IEventDispatcher` with defaulting delegated calls - to the event dispatcher. This provides a more intuitive syntax for - applications. - - ```py - - # We can now do this... - - >>> @bot.listen() - >>> async def on_message(event: hikari.MessageCreateEvent) -> None: ... - - # ...instead of having to do this... - - >>> @bot.listen(hikari.MessageCreateEvent) - >>> async def on_message(event: hikari.MessageCreateEvent) -> None: ... - ``` - """ - - __slots__ = () - - if typing.TYPE_CHECKING: - _EventT = typing.TypeVar("_EventT", bound=base.HikariEvent) - _PredicateT = typing.Callable[[base.HikariEvent], typing.Union[bool, typing.Coroutine[None, typing.Any, bool]]] - _SyncCallbackT = typing.Callable[[base.HikariEvent], None] - _AsyncCallbackT = typing.Callable[[base.HikariEvent], typing.Coroutine[None, typing.Any, None]] - _CallbackT = typing.Union[_SyncCallbackT, _AsyncCallbackT] - - @property - @abc.abstractmethod - def event_dispatcher(self) -> event_dispatcher_.IEventDispatcher: - """Event dispatcher and subscription manager. - - This stores every event you subscribe to in your application, and - manages invoking those subscribed callbacks when the corresponding - event occurs. - - Event dispatchers also provide a `wait_for` functionality that can be - used to wait for a one-off event that matches a certain criteria. This - is useful if waiting for user feedback for a specific procedure being - performed. - - Users may create their own events and trigger them using this as well, - thus providing a simple in-process event bus that can easily be extended - with a little work to span multiple applications in a distributed - cluster. - - Returns - ------- - hikari.api.event_dispatcher.IEventDispatcher - The event dispatcher in use. - """ - - @reflect.steal_docstring_from(event_dispatcher_.IEventDispatcher.listen) - def listen( - self, event_type: typing.Union[undefined.UndefinedType, typing.Type[_EventT]] = undefined.UNDEFINED, - ) -> typing.Callable[[_CallbackT], _CallbackT]: - ... - - @reflect.steal_docstring_from(event_dispatcher_.IEventDispatcher.subscribe) - def subscribe( - self, - event_type: typing.Type[_EventT], - callback: typing.Callable[[_EventT], typing.Union[typing.Coroutine[None, typing.Any, None], None]], - ) -> typing.Callable[[_EventT], typing.Coroutine[None, typing.Any, None]]: - ... - - @reflect.steal_docstring_from(event_dispatcher_.IEventDispatcher.unsubscribe) - def unsubscribe( - self, - event_type: typing.Type[_EventT], - callback: typing.Callable[[_EventT], typing.Coroutine[None, typing.Any, None]], - ) -> None: - ... - - @reflect.steal_docstring_from(event_dispatcher_.IEventDispatcher.wait_for) - async def wait_for( - self, event_type: typing.Type[_EventT], predicate: _PredicateT, timeout: typing.Union[float, int, None], - ) -> _EventT: - ... - - @reflect.steal_docstring_from(event_dispatcher_.IEventDispatcher.dispatch) - def dispatch(self, event: base.HikariEvent) -> asyncio.Future[typing.Any]: - ... - - -class IGatewayZookeeper(IGatewayConsumer, abc.ABC): - """Component specialization that looks after a set of shards. - - These events will be produced by a low-level gateway implementation, and - will produce `list` and `dict` types only. - - This may be combined with `IGatewayDispatcher` for most single-process - bots, or may be a specific component for large distributed applications - that feed new events into a message queue, for example. - """ - - __slots__ = () - - @property - @abc.abstractmethod - def gateway_shards(self) -> typing.Mapping[int, gateway.Gateway]: - """Map of each shard ID to the corresponding client for it. - - If the shards have not started, and auto=sharding is in-place, then it - is acceptable for this to return an empty mapping. - - !!! note - "Non-sharded" bots should expect one value to be in this mapping - under the shard ID `0`. - - >>> bot.gateway_shards[0].heartbeat_latency - 0.145612141 - - Returns - ------- - typing.Mapping[int, hikari.net.gateway.Gateway] - The mapping of shard IDs to gateway connections for the - corresponding shard. These shard IDs are 0-indexed. - """ - - @property - @abc.abstractmethod - def gateway_shard_count(self) -> int: - """Count the number of shards in the entire distributed application. - - If the shards have not started, and auto-sharding is in-place, then it - is acceptable for this to return `0`. When the application is running, - this should always be a non-zero natural number that is greater than the - maximum ID in `gateway_shards`. - - Returns - ------- - int - The number of shards in the entire application. - """ - - @abc.abstractmethod - async def start(self) -> None: - """Start all shards and wait for them to be READY.""" - - @abc.abstractmethod - async def join(self) -> None: - """Wait for all shards to shut down.""" - - @abc.abstractmethod - async def update_presence( - self, - *, - status: typing.Union[undefined.UndefinedType, presences.Status] = undefined.UNDEFINED, - activity: typing.Union[undefined.UndefinedType, presences.Activity, None] = undefined.UNDEFINED, - idle_since: typing.Union[undefined.UndefinedType, datetime.datetime, None] = undefined.UNDEFINED, - is_afk: typing.Union[undefined.UndefinedType, bool] = undefined.UNDEFINED, - ) -> None: - """Update the presence of the user for all shards. - - This will only update arguments that you explicitly specify a value for. - Any arguments that you do not explicitly provide some value for will - not be changed (these values will default to be `undefined`). - - !!! warning - This will only apply to connected shards. - - !!! note - If you wish to update a presence for a specific shard, you can do - this by using the `gateway_shards` `typing.Mapping` to find the - shard you wish to update. - - Parameters - ---------- - status : hikari.models.presences.Status or hikari.utilities.undefined.UndefinedType - If defined, the new status to set. - activity : hikari.models.presences.Activity or None or hikari.utilities.undefined.UndefinedType - If defined, the new activity to set. - idle_since : datetime.datetime or None or hikari.utilities.undefined.UndefinedType - If defined, the time to show up as being idle since, or `None` if - not applicable. If undefined, then it is not changed. - is_afk : bool or hikari.utilities.undefined.UndefinedType - If defined, `True` if the user should be marked as AFK, - or `False` if not AFK. - """ - - @abc.abstractmethod - def run(self) -> None: - """Execute this component on an event loop. - - Performs the same job as `RunnableClient.start`, but provides additional - preparation such as registering OS signal handlers for interrupts, - and preparing the initial event loop. - - This enables the client to be run immediately without having to - set up the `asyncio` event loop manually first. - """ - - -class IBot(IGatewayZookeeper, IGatewayDispatcher, abc.ABC): - """Base for bot applications. - - Bots are components that have access to a REST API, an event dispatcher, - and an event consumer. - - Additionally, bots will contain a collection of Gateway client objects. - """ - - __slots__ = () - - @property - @abc.abstractmethod - def http_settings(self) -> http_settings_.HTTPSettings: - """HTTP settings to use for the shards when they get created. - - !!! info - This is stored only for bots, since shards are generated lazily on - start-up once sharding information has been retrieved from the REST - API. To do this, an event loop has to be running first. - - Returns - ------- - hikari.net.http_settings.HTTPSettings - The HTTP settings to use. - """ diff --git a/hikari/api/bot.py b/hikari/api/bot.py new file mode 100644 index 0000000000..2c5b524c28 --- /dev/null +++ b/hikari/api/bot.py @@ -0,0 +1,60 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# Copyright © Nekoka.tt 2019-2020 +# +# This file is part of Hikari. +# +# Hikari is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Hikari is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Hikari. If not, see . +"""Core interfaces for types of Hikari application.""" + +from __future__ import annotations + +__all__: typing.List[str] = ["IBotApp"] + +import abc +import typing + +from hikari.api import event_consumer +from hikari.api import event_dispatcher + +if typing.TYPE_CHECKING: + from hikari.net import http_settings as http_settings_ + + +class IBotApp(event_consumer.IEventConsumerApp, event_dispatcher.IEventDispatcherApp, abc.ABC): + """Base for bot applications. + + Bots are components that have access to a REST API, an event dispatcher, + and an event consumer. + + Additionally, bots will contain a collection of Gateway client objects. + """ + + __slots__ = () + + @property + @abc.abstractmethod + def http_settings(self) -> http_settings_.HTTPSettings: + """HTTP settings to use for the shards when they get created. + + !!! info + This is stored only for bots, since shards are generated lazily on + start-up once sharding information has been retrieved from the REST + API on startup. + + Returns + ------- + hikari.net.http_settings.HTTPSettings + The HTTP settings to use. + """ diff --git a/hikari/api/cache.py b/hikari/api/cache.py index 26a5397160..61235f9e64 100644 --- a/hikari/api/cache.py +++ b/hikari/api/cache.py @@ -19,7 +19,7 @@ """Core interface for a cache implementation.""" from __future__ import annotations -__all__: typing.List[str] = ["ICache"] +__all__: typing.List[str] = ["ICacheComponent"] import abc @@ -29,7 +29,7 @@ from hikari.api import component -class ICache(component.IComponent, abc.ABC): +class ICacheComponent(component.IComponent, abc.ABC): """Interface describing the operations a cache component should provide. This will be used by the gateway and REST API to cache specific types of diff --git a/hikari/api/component.py b/hikari/api/component.py index 4c335ecdfd..635391fc58 100644 --- a/hikari/api/component.py +++ b/hikari/api/component.py @@ -26,31 +26,32 @@ import typing if typing.TYPE_CHECKING: - from hikari.api import app + from hikari.api import rest class IComponent(abc.ABC): """A component that makes up part of the application. Objects that derive from this should usually be attributes on the - `hikari.api.app.IRESTApp` object. + `hikari.api.rest.IRESTApp` object. Examples -------- - See the source code for `hikari.api.entity_factory.IEntityFactory`, - `hikari.api.cache.ICache`, and - `hikari.api.event_dispatcher.IEventDispatcher` for examples of usage. + See the source code for `hikari.api.entity_factory.IEntityFactoryComponent`, + `hikari.api.cache.ICacheComponent`, and + `hikari.api.event_dispatcher.IEventDispatcherComponent` + for examples of usage. """ __slots__ = () @property @abc.abstractmethod - def app(self) -> app.IRESTApp: + def app(self) -> rest.IRESTApp: """Return the Application that owns this component. Returns ------- - hikari.api.app.IRESTApp + hikari.api.rest.IRESTApp The application implementation that owns this component. """ diff --git a/hikari/api/entity_factory.py b/hikari/api/entity_factory.py index 96606906ad..f6bd6c725c 100644 --- a/hikari/api/entity_factory.py +++ b/hikari/api/entity_factory.py @@ -19,7 +19,7 @@ """Core interface for an object that serializes/deserializes API objects.""" from __future__ import annotations -__all__: typing.List[str] = ["IEntityFactory"] +__all__: typing.List[str] = ["IEntityFactoryComponent"] import abc import typing @@ -53,7 +53,7 @@ from hikari.utilities import files -class IEntityFactory(component.IComponent, abc.ABC): +class IEntityFactoryComponent(component.IComponent, abc.ABC): """Interface for components that serialize and deserialize JSON payloads.""" __slots__ = () diff --git a/hikari/api/event_consumer.py b/hikari/api/event_consumer.py index 21babaf356..3f3b722483 100644 --- a/hikari/api/event_consumer.py +++ b/hikari/api/event_consumer.py @@ -19,23 +19,24 @@ """Core interface for components that consume raw API event payloads.""" from __future__ import annotations -__all__: typing.List[str] = ["IEventConsumer"] +__all__: typing.List[str] = ["IEventConsumerComponent", "IEventConsumerApp"] import abc import typing from hikari.api import component +from hikari.api import rest if typing.TYPE_CHECKING: from hikari.net import gateway from hikari.utilities import data_binding -class IEventConsumer(component.IComponent, abc.ABC): +class IEventConsumerComponent(component.IComponent, abc.ABC): """Interface describing a component that can consume raw gateway events. Implementations will usually want to combine this with a - `hikari.api.event_dispatcher.IEventDispatcher` for a basic in-memory + `hikari.api.event_dispatcher.IEventDispatcherBase` for a basic in-memory single-app event management system. You may in some cases implement this separately if you are passing events onto a system such as a message queue. """ @@ -57,3 +58,28 @@ async def consume_raw_event( payload : hikari.utilities.data_binding.JSONObject The payload provided with the event. """ + + +class IEventConsumerApp(rest.IRESTApp, abc.ABC): + """Application specialization that supports consumption of raw events. + + This may be combined with `IGatewayZookeeperApp` for most single-process + bots, or may be a specific component for large distributed applications + that consume events from a message queue, for example. + """ + + __slots__ = () + + @property + @abc.abstractmethod + def event_consumer(self) -> IEventConsumerComponent: + """Raw event consumer. + + This should be passed raw event payloads from your gateway + websocket implementation. + + Returns + ------- + IEventConsumerComponent + The event consumer implementation in-use. + """ diff --git a/hikari/api/event_dispatcher.py b/hikari/api/event_dispatcher.py index abcd128d26..89d58718fb 100644 --- a/hikari/api/event_dispatcher.py +++ b/hikari/api/event_dispatcher.py @@ -19,29 +19,28 @@ """Core interface for components that dispatch events to the library.""" from __future__ import annotations -__all__: typing.List[str] = ["IEventDispatcher"] +__all__: typing.List[str] = ["IEventDispatcherBase", "IEventDispatcherApp", "IEventDispatcherComponent"] import abc import asyncio import typing -from hikari.api import component from hikari.utilities import undefined if typing.TYPE_CHECKING: from hikari.events import base - _EventT = typing.TypeVar("_EventT", bound=base.HikariEvent) - _PredicateT = typing.Callable[[base.HikariEvent], typing.Union[bool, typing.Coroutine[None, typing.Any, bool]]] - _SyncCallbackT = typing.Callable[[base.HikariEvent], None] - _AsyncCallbackT = typing.Callable[[base.HikariEvent], typing.Coroutine[None, typing.Any, None]] + _EventT = typing.TypeVar("_EventT", bound=base.Event) + _PredicateT = typing.Callable[[base.Event], typing.Union[bool, typing.Coroutine[None, typing.Any, bool]]] + _SyncCallbackT = typing.Callable[[base.Event], None] + _AsyncCallbackT = typing.Callable[[base.Event], typing.Coroutine[None, typing.Any, None]] _CallbackT = typing.Union[_SyncCallbackT, _AsyncCallbackT] -class IEventDispatcher(component.IComponent, abc.ABC): - """Interface for event dispatchers. +class IEventDispatcherBase(abc.ABC): + """Base interface for event dispatcher implementations. - This is a consumer of a `hikari.events.base.HikariEvent` object, and is + This is a consumer of a `hikari.events.base.Event` object, and is expected to invoke one or more corresponding event listeners where appropriate. """ @@ -49,28 +48,28 @@ class IEventDispatcher(component.IComponent, abc.ABC): __slots__ = () @abc.abstractmethod - def dispatch(self, event: base.HikariEvent) -> asyncio.Future[typing.Any]: + def dispatch(self, event: base.Event) -> asyncio.Future[typing.Any]: """Dispatch an event. Parameters ---------- - event : hikari.events.base.HikariEvent + event : hikari.events.base.Event The event to dispatch. Example ------- We can dispatch custom events by first defining a class that - derives from `hikari.events.base.HikariEvent`. + derives from `hikari.events.base.Event`. ```py import attr - from hikari.events.base import HikariEvent + from hikari.events.base import Event from hikari.models.users import User from hikari.utilities.snowflake import Snowflake @attr.s(auto_attribs=True) - class EveryoneMentionedEvent(HikariEvent): + class EveryoneMentionedEvent(Event): author: User '''The user who mentioned everyone.''' @@ -103,7 +102,7 @@ async def on_message(event): ``` This event can be listened to elsewhere by subscribing to it with - `IEventDispatcher.subscribe`. + `IEventDispatcherBase.subscribe`. ```py @bot.listen(EveryoneMentionedEvent) @@ -121,8 +120,8 @@ async def on_everyone_mentioned(event): See Also -------- - IEventDispatcher.subscribe - IEventDispatcher.wait_for + IEventDispatcherBase.subscribe + IEventDispatcherBase.wait_for """ @abc.abstractmethod @@ -135,7 +134,7 @@ def subscribe( Parameters ---------- - event_type : typing.Type[hikari.events.base.HikariEvent] + event_type : typing.Type[hikari.events.base.Event] The event type to listen for. This will also listen for any subclasses of the given type. callback @@ -167,8 +166,8 @@ async def on_message(event): See Also -------- - IEventDispatcher.listen - IEventDispatcher.wait_for + IEventDispatcherBase.listen + IEventDispatcherBase.wait_for """ @abc.abstractmethod @@ -181,7 +180,7 @@ def unsubscribe( Parameters ---------- - event_type : typing.Type[hikari.events.base.HikariEvent] + event_type : typing.Type[hikari.events.base.Event] The event type to unsubscribe from. This must be the same exact type as was originally subscribed with to be removed correctly. callback @@ -212,7 +211,7 @@ def listen( Parameters ---------- - event_type : hikari.utilities.undefined.UndefinedType or typing.Type[hikari.events.base.HikariEvent] + event_type : hikari.utilities.undefined.UndefinedType or typing.Type[hikari.events.base.Event] The event type to subscribe to. The implementation may allow this to be undefined. If this is the case, the event type will be inferred instead from the type hints on the function signature. @@ -221,15 +220,15 @@ def listen( ------- typing.Callable[[Callback], Callback] A decorator for a function or coroutine function that passes it - to `IEventDispatcher.subscribe` before returning the function + to `IEventDispatcherBase.subscribe` before returning the function reference. See Also -------- - IEventDispatcher.dispatch - IEventDispatcher.subscribe - IEventDispatcher.unsubscribe - IEventDispatcher.wait_for + IEventDispatcherBase.dispatch + IEventDispatcherBase.subscribe + IEventDispatcherBase.unsubscribe + IEventDispatcherBase.wait_for """ @abc.abstractmethod @@ -240,7 +239,7 @@ async def wait_for( Parameters ---------- - event_type : typing.Type[hikari.events.base.HikariEvent] + event_type : typing.Type[hikari.events.base.Event] The event type to listen for. This will listen for subclasses of this type additionally. predicate @@ -256,7 +255,7 @@ async def wait_for( Returns ------- - hikari.events.base.HikariEvent + hikari.events.base.Event The event that was provided. Raises @@ -267,7 +266,72 @@ async def wait_for( See Also -------- - IEventDispatcher.listen - IEventDispatcher.subscribe - IEventDispatcher.dispatch + IEventDispatcherBase.listen + IEventDispatcherBase.subscribe + IEventDispatcherBase.dispatch + """ + + +class IEventDispatcherComponent(IEventDispatcherBase, abc.ABC): + """Base interface for event dispatcher implementations that are components. + + This is a consumer of a `hikari.events.base.Event` object, and is + expected to invoke one or more corresponding event listeners where + appropriate. + """ + + +class IEventDispatcherApp(IEventDispatcherBase, abc.ABC): + """Application specialization that supports dispatching of events. + + These events are expected to be instances of + `hikari.events.base.Event`. + + This may be combined with `IGatewayZookeeperApp` for most single-process + bots, or may be a specific component for large distributed applications + that consume events from a message queue, for example. + + This acts as an event dispatcher-like object that can simply delegate to + the implementation, which makes event-based tasks like adding listeners + and waiting for events much tidier. + + ```py + + # ... this means we can do this... + + >>> @bot.listen() + >>> async def on_message(event: hikari.MessageCreateEvent) -> None: ... + + # ...instead of having to do this... + + >>> @bot.event_dispatcher.listen(hikari.MessageCreateEvent) + >>> async def on_message(event: hikari.MessageCreateEvent) -> None: ... + ``` + """ + + __slots__ = () + + @property + @abc.abstractmethod + def event_dispatcher(self) -> IEventDispatcherComponent: + """Event dispatcher and subscription manager. + + This stores every event you subscribe to in your application, and + manages invoking those subscribed callbacks when the corresponding + event occurs. + + Event dispatchers also provide a `wait_for` functionality that can be + used to wait for a one-off event that matches a certain criteria. This + is useful if waiting for user feedback for a specific procedure being + performed. + + Users may create their own events and trigger them using this as well, + thus providing a simple in-process event bus that can easily be extended + with a little work to span multiple applications in a distributed + cluster. + + Returns + ------- + hikari.api.event_dispatcher.IEventDispatcherBase + The event dispatcher in use. """ diff --git a/hikari/api/gateway_zookeeper.py b/hikari/api/gateway_zookeeper.py new file mode 100644 index 0000000000..dab8d855f6 --- /dev/null +++ b/hikari/api/gateway_zookeeper.py @@ -0,0 +1,144 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# Copyright © Nekoka.tt 2019-2020 +# +# This file is part of Hikari. +# +# Hikari is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Hikari is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Hikari. If not, see . +"""Interface for gateway zookeeper applications.""" + +from __future__ import annotations + +__all__: typing.List[str] = ["IGatewayZookeeperApp"] + +import abc +import typing + +from hikari.api import event_consumer +from hikari.utilities import undefined + +if typing.TYPE_CHECKING: + import datetime + + from hikari.models import presences + from hikari.net import gateway + + +class IGatewayZookeeperApp(event_consumer.IEventConsumerApp, abc.ABC): + """Component specialization that looks after a set of shards. + + These events will be produced by a low-level gateway implementation, and + will produce `list` and `dict` types only. + + This may be combined with `IEventDispatcherApp` for most single-process + bots, or may be a specific component for large distributed applications + that feed new events into a message queue, for example. + """ + + __slots__ = () + + @property + @abc.abstractmethod + def shards(self) -> typing.Mapping[int, gateway.Gateway]: + """Map of each shard ID to the corresponding client for it. + + If the shards have not started, and auto=sharding is in-place, then it + is acceptable for this to return an empty mapping. + + !!! note + "Non-sharded" bots should expect one value to be in this mapping + under the shard ID `0`. + + >>> bot.gateway_shards[0].heartbeat_latency + 0.145612141 + + Returns + ------- + typing.Mapping[int, hikari.net.gateway.Gateway] + The mapping of shard IDs to gateway connections for the + corresponding shard. These shard IDs are 0-indexed. + """ + + @property + @abc.abstractmethod + def shard_count(self) -> int: + """Count the number of shards in the entire distributed application. + + If the shards have not started, and auto-sharding is in-place, then it + is acceptable for this to return `0`. When the application is running, + this should always be a non-zero natural number that is greater than the + maximum ID in `gateway_shards`. + + Returns + ------- + int + The number of shards in the entire application. + """ + + @abc.abstractmethod + async def start(self) -> None: + """Start all shards and wait for them to be READY.""" + + @abc.abstractmethod + async def join(self) -> None: + """Wait for all shards to shut down.""" + + @abc.abstractmethod + async def update_presence( + self, + *, + status: typing.Union[undefined.UndefinedType, presences.Status] = undefined.UNDEFINED, + activity: typing.Union[undefined.UndefinedType, presences.Activity, None] = undefined.UNDEFINED, + idle_since: typing.Union[undefined.UndefinedType, datetime.datetime, None] = undefined.UNDEFINED, + is_afk: typing.Union[undefined.UndefinedType, bool] = undefined.UNDEFINED, + ) -> None: + """Update the presence of the user for all shards. + + This will only update arguments that you explicitly specify a value for. + Any arguments that you do not explicitly provide some value for will + not be changed (these values will default to be `undefined`). + + !!! warning + This will only apply to connected shards. + + !!! note + If you wish to update a presence for a specific shard, you can do + this by using the `gateway_shards` `typing.Mapping` to find the + shard you wish to update. + + Parameters + ---------- + status : hikari.models.presences.Status or hikari.utilities.undefined.UndefinedType + If defined, the new status to set. + activity : hikari.models.presences.Activity or None or hikari.utilities.undefined.UndefinedType + If defined, the new activity to set. + idle_since : datetime.datetime or None or hikari.utilities.undefined.UndefinedType + If defined, the time to show up as being idle since, or `None` if + not applicable. If undefined, then it is not changed. + is_afk : bool or hikari.utilities.undefined.UndefinedType + If defined, `True` if the user should be marked as AFK, + or `False` if not AFK. + """ + + @abc.abstractmethod + def run(self) -> None: + """Execute this component on an event loop. + + Performs the same job as `RunnableClient.start`, but provides additional + preparation such as registering OS signal handlers for interrupts, + and preparing the initial event loop. + + This enables the client to be run immediately without having to + set up the `asyncio` event loop manually first. + """ diff --git a/hikari/api/rest.py b/hikari/api/rest.py new file mode 100644 index 0000000000..84f4ac21a2 --- /dev/null +++ b/hikari/api/rest.py @@ -0,0 +1,110 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# Copyright © Nekoka.tt 2019-2020 +# +# This file is part of Hikari. +# +# Hikari is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Hikari is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Hikari. If not, see . +"""REST application interface.""" + +from __future__ import annotations + +__all__: typing.List[str] = ["IRESTApp"] + +import abc +import logging +import typing + + +if typing.TYPE_CHECKING: + from concurrent import futures + + from hikari.api import cache as cache_ + from hikari.api import entity_factory as entity_factory_ + from hikari.net import rest as rest_ + + +class IRESTApp(abc.ABC): + """Component specialization that is used for REST-only applications. + + Examples may include web dashboards, or applications where no gateway + connection is required. As a result, no event conduit is provided by + these implementations. They do however provide a REST client, and the + general components defined in `IRESTApp` + """ + + __slots__ = () + + @property + @abc.abstractmethod + def rest(self) -> rest_.REST: + """REST API Client. + + Use this to make calls to Discord's REST API over HTTPS. + + Returns + ------- + hikari.net.rest.REST + The REST API client. + """ + + @property + @abc.abstractmethod + def logger(self) -> logging.Logger: + """Logger for logging messages. + + Returns + ------- + logging.Logger + The application-level logger. + """ + + @property + @abc.abstractmethod + def cache(self) -> cache_.ICacheComponent: + """Entity cache. + + Returns + ------- + hikari.api.cache.ICacheComponent + The cache implementation used in this application. + """ + + @property + @abc.abstractmethod + def entity_factory(self) -> entity_factory_.IEntityFactoryComponent: + """Entity creator and updater facility. + + Returns + ------- + hikari.api.entity_factory.IEntityFactoryComponent + The factory object used to produce and update Python entities. + """ + + @property + @abc.abstractmethod + def executor(self) -> typing.Optional[futures.Executor]: + """Thread-pool to utilise for file IO within the library, if set. + + Returns + ------- + concurrent.futures.Executor or None + The custom thread-pool being used for blocking IO. If the + default event loop thread-pool is being used, then this will + return `None` instead. + """ + + @abc.abstractmethod + async def close(self) -> None: + """Safely shut down all resources.""" diff --git a/hikari/events/base.py b/hikari/events/base.py index bb804e6766..0d30d698cd 100644 --- a/hikari/events/base.py +++ b/hikari/events/base.py @@ -21,7 +21,7 @@ from __future__ import annotations __all__: typing.List[str] = [ - "HikariEvent", + "Event", "get_required_intents_for", "requires_intents", "no_catch", @@ -39,21 +39,21 @@ # Base event, is not deserialized @attr.s(eq=False, hash=False, init=False, kw_only=True, slots=True) -class HikariEvent(abc.ABC): +class Event(abc.ABC): """The base class that all events inherit from.""" -_HikariEventT = typing.TypeVar("_HikariEventT", contravariant=True) +_EventT = typing.TypeVar("_EventT", contravariant=True) _REQUIRED_INTENTS_ATTR: typing.Final[str] = "___required_intents___" _NO_THROW_ATTR: typing.Final[str] = "___no_throw___" -def get_required_intents_for(event_type: typing.Type[HikariEvent]) -> typing.Collection[intents.Intent]: +def get_required_intents_for(event_type: typing.Type[Event]) -> typing.Collection[intents.Intent]: """Retrieve the intents that are required to listen to an event type. Parameters ---------- - event_type : typing.Type[HikariEvent] + event_type : typing.Type[Event] The event type to get required intents for. Returns @@ -67,7 +67,7 @@ def get_required_intents_for(event_type: typing.Type[HikariEvent]) -> typing.Col def requires_intents( first: intents.Intent, *rest: intents.Intent -) -> typing.Callable[[typing.Type[_HikariEventT]], typing.Type[_HikariEventT]]: +) -> typing.Callable[[typing.Type[_EventT]], typing.Type[_EventT]]: """Decorate an event type to define what intents it requires. Parameters @@ -81,27 +81,27 @@ def requires_intents( """ - def decorator(cls: typing.Type[_HikariEventT]) -> typing.Type[_HikariEventT]: + def decorator(cls: typing.Type[_EventT]) -> typing.Type[_EventT]: setattr(cls, _REQUIRED_INTENTS_ATTR, [first, *rest]) return cls return decorator -def no_catch() -> typing.Callable[[typing.Type[_HikariEventT]], typing.Type[_HikariEventT]]: +def no_catch() -> typing.Callable[[typing.Type[_EventT]], typing.Type[_EventT]]: """Decorate an event type to indicate errors should not be handled. This is useful for exception event types that you do not want to have invoked recursively. """ - def decorator(cls: typing.Type[_HikariEventT]) -> typing.Type[_HikariEventT]: + def decorator(cls: typing.Type[_EventT]) -> typing.Type[_EventT]: setattr(cls, _NO_THROW_ATTR, True) return cls return decorator -def is_no_catch_event(obj: typing.Union[_HikariEventT, typing.Type[_HikariEventT]]) -> bool: +def is_no_catch_event(obj: typing.Union[_EventT, typing.Type[_EventT]]) -> bool: """Return True if this event is marked as `no_catch`.""" return typing.cast(bool, getattr(obj, _NO_THROW_ATTR, False)) diff --git a/hikari/events/channel.py b/hikari/events/channel.py index f8cc68411d..efa4ed376b 100644 --- a/hikari/events/channel.py +++ b/hikari/events/channel.py @@ -51,7 +51,7 @@ @base_events.requires_intents(intents.Intent.GUILDS) # TODO: this intent doesn't account for DM channels. @attr.s(eq=False, hash=False, init=False, kw_only=True, slots=True) -class BaseChannelEvent(base_events.HikariEvent, abc.ABC): +class BaseChannelEvent(base_events.Event, abc.ABC): """A base object that Channel events will inherit from.""" channel: channels.PartialChannel = attr.ib(repr=True) @@ -82,7 +82,7 @@ class ChannelDeleteEvent(BaseChannelEvent): @base_events.requires_intents(intents.Intent.GUILDS) # TODO: this intent doesn't account for DM channels. @attr.s(eq=False, hash=False, init=False, kw_only=True, slots=True) -class ChannelPinsUpdateEvent(base_events.HikariEvent, base_models.Entity): +class ChannelPinsUpdateEvent(base_events.Event, base_models.Entity): """Used to represent the Channel Pins Update gateway event. Sent when a message is pinned or unpinned in a channel but not @@ -107,7 +107,7 @@ class ChannelPinsUpdateEvent(base_events.HikariEvent, base_models.Entity): @base_events.requires_intents(intents.Intent.GUILD_WEBHOOKS) @attr.s(eq=False, hash=False, init=False, kw_only=True, slots=True) -class WebhookUpdateEvent(base_events.HikariEvent, base_models.Entity): +class WebhookUpdateEvent(base_events.Event, base_models.Entity): """Used to represent webhook update gateway events. Sent when a webhook is updated, created or deleted in a guild. @@ -122,7 +122,7 @@ class WebhookUpdateEvent(base_events.HikariEvent, base_models.Entity): @base_events.requires_intents(intents.Intent.GUILD_MESSAGE_TYPING, intents.Intent.DIRECT_MESSAGE_TYPING) @attr.s(eq=False, hash=False, init=False, kw_only=True, slots=True) -class TypingStartEvent(base_events.HikariEvent, base_models.Entity): +class TypingStartEvent(base_events.Event, base_models.Entity): """Used to represent typing start gateway events. Received when a user or bot starts "typing" in a channel. @@ -152,7 +152,7 @@ class TypingStartEvent(base_events.HikariEvent, base_models.Entity): @base_events.requires_intents(intents.Intent.GUILD_INVITES) @attr.s(eq=False, hash=False, init=False, kw_only=True, slots=True) -class InviteCreateEvent(base_events.HikariEvent): +class InviteCreateEvent(base_events.Event): """Represents a gateway Invite Create event.""" invite: invites.InviteWithMetadata = attr.ib(repr=True) @@ -161,7 +161,7 @@ class InviteCreateEvent(base_events.HikariEvent): @base_events.requires_intents(intents.Intent.GUILD_INVITES) @attr.s(eq=False, hash=False, init=False, kw_only=True, slots=True) -class InviteDeleteEvent(base_events.HikariEvent, base_models.Entity): +class InviteDeleteEvent(base_events.Event, base_models.Entity): """Used to represent Invite Delete gateway events. Sent when an invite is deleted for a channel we can access. diff --git a/hikari/events/guild.py b/hikari/events/guild.py index 5d324a264b..6894bc7cff 100644 --- a/hikari/events/guild.py +++ b/hikari/events/guild.py @@ -59,7 +59,7 @@ @base_events.requires_intents(intents.Intent.GUILDS) @attr.s(eq=False, hash=False, init=False, kw_only=True, slots=True) -class GuildEvent(base_events.HikariEvent): +class GuildEvent(base_events.Event): """A base object that all guild events will inherit from.""" diff --git a/hikari/events/message.py b/hikari/events/message.py index e846652a1d..2aa65ece42 100644 --- a/hikari/events/message.py +++ b/hikari/events/message.py @@ -22,7 +22,7 @@ __all__: typing.List[str] = [ "MessageCreateEvent", - "UpdateMessage", + "UpdatedMessage", "MessageUpdateEvent", "MessageDeleteEvent", "MessageDeleteBulkEvent", @@ -55,13 +55,13 @@ @base_events.requires_intents(intents.Intent.GUILD_MESSAGES, intents.Intent.DIRECT_MESSAGES) @attr.s(eq=False, hash=False, init=False, kw_only=True, slots=True) -class MessageCreateEvent(base_events.HikariEvent): +class MessageCreateEvent(base_events.Event): """Used to represent Message Create gateway events.""" message: messages.Message = attr.ib(repr=True) -class UpdateMessage(messages.Message): +class UpdatedMessage: """An arbitrarily partial version of `hikari.models.messages.Message`. !!! warn @@ -74,6 +74,8 @@ class UpdateMessage(messages.Message): channel_id: snowflake.Snowflake = attr.ib(repr=True) """The ID of the channel that the message was sent in.""" + # FIXME: This differs from Message, where the field could only be None or a Snowflake + # so this breaks stuff using inheritance. guild_id: typing.Union[snowflake.Snowflake, undefined.UndefinedType] = attr.ib(repr=True) """The ID of the guild that the message was sent in.""" @@ -151,7 +153,7 @@ class UpdateMessage(messages.Message): @base_events.requires_intents(intents.Intent.GUILD_MESSAGES, intents.Intent.DIRECT_MESSAGES) @attr.s(eq=False, hash=False, init=False, kw_only=True, slots=True) -class MessageUpdateEvent(base_events.HikariEvent, base_models.Unique): +class MessageUpdateEvent(base_events.Event, base_models.Unique): """Represents Message Update gateway events. !!! warn @@ -161,13 +163,13 @@ class MessageUpdateEvent(base_events.HikariEvent, base_models.Unique): (a singleton) to indicate that it has not been changed. """ - message: UpdateMessage = attr.ib(repr=True) + message: UpdatedMessage = attr.ib(repr=True) """The partial message object with all updated fields.""" @base_events.requires_intents(intents.Intent.GUILD_MESSAGES, intents.Intent.DIRECT_MESSAGES) @attr.s(eq=False, hash=False, init=False, kw_only=True, slots=True) -class MessageDeleteEvent(base_events.HikariEvent, base_models.Entity): +class MessageDeleteEvent(base_events.Event, base_models.Entity): """Used to represent Message Delete gateway events. Sent when a message is deleted in a channel we have access to. @@ -191,7 +193,7 @@ class MessageDeleteEvent(base_events.HikariEvent, base_models.Entity): # TODO: if this doesn't apply to DMs then does guild_id need to be nullable here? @base_events.requires_intents(intents.Intent.GUILD_MESSAGES) @attr.s(eq=False, hash=False, init=False, kw_only=True, slots=True) -class MessageDeleteBulkEvent(base_events.HikariEvent, base_models.Entity): +class MessageDeleteBulkEvent(base_events.Event, base_models.Entity): """Used to represent Message Bulk Delete gateway events. Sent when multiple messages are deleted in a channel at once. @@ -210,7 +212,7 @@ class MessageDeleteBulkEvent(base_events.HikariEvent, base_models.Entity): """A collection of the IDs of the messages that were deleted.""" -class BaseMessageReactionEvent(base_events.HikariEvent, base_models.Entity): +class BaseMessageReactionEvent(base_events.Event, base_models.Entity): """A base class that all message reaction events will inherit from.""" channel_id: snowflake.Snowflake = attr.ib(repr=True) diff --git a/hikari/events/other.py b/hikari/events/other.py index 6fe0f277f6..bb8a35c50f 100644 --- a/hikari/events/other.py +++ b/hikari/events/other.py @@ -49,46 +49,46 @@ # Synthetic event, is not deserialized, and is produced by the dispatcher. @base_events.no_catch() @attr.s(eq=False, hash=False, init=True, kw_only=True, slots=True) -class ExceptionEvent(base_events.HikariEvent): +class ExceptionEvent(base_events.Event): """Descriptor for an exception thrown while processing an event.""" exception: Exception = attr.ib(repr=True) """The exception that was raised.""" - event: base_events.HikariEvent = attr.ib(repr=True) + event: base_events.Event = attr.ib(repr=True) """The event that was being invoked when the exception occurred.""" - callback: typing.Callable[[base_events.HikariEvent], typing.Coroutine[None, typing.Any, None]] = attr.ib(repr=False) + callback: typing.Callable[[base_events.Event], typing.Coroutine[None, typing.Any, None]] = attr.ib(repr=False) """The event that was being invoked when the exception occurred.""" # Synthetic event, is not deserialized @attr.s(eq=False, hash=False, init=False, kw_only=True, slots=True) -class StartingEvent(base_events.HikariEvent): +class StartingEvent(base_events.Event): """Event that is fired before the gateway client starts all shards.""" # Synthetic event, is not deserialized @attr.s(eq=False, hash=False, init=False, kw_only=True, slots=True) -class StartedEvent(base_events.HikariEvent): +class StartedEvent(base_events.Event): """Event that is fired when the gateway client starts all shards.""" # Synthetic event, is not deserialized @attr.s(eq=False, hash=False, init=False, kw_only=True, slots=True) -class StoppingEvent(base_events.HikariEvent): +class StoppingEvent(base_events.Event): """Event that is fired when the gateway client is instructed to disconnect all shards.""" # Synthetic event, is not deserialized @attr.s(eq=False, hash=False, init=False, kw_only=True, slots=True) -class StoppedEvent(base_events.HikariEvent): +class StoppedEvent(base_events.Event): """Event that is fired when the gateway client has finished disconnecting all shards.""" # Synthetic event, is not deserialized @attr.s(eq=False, hash=False, init=True, kw_only=True, slots=True) -class ConnectedEvent(base_events.HikariEvent): +class ConnectedEvent(base_events.Event): """Event invoked each time a shard connects.""" shard: gateway_client.Gateway = attr.ib(repr=True) @@ -97,7 +97,7 @@ class ConnectedEvent(base_events.HikariEvent): # Synthetic event, is not deserialized @attr.s(eq=False, hash=False, init=True, kw_only=True, slots=True) -class DisconnectedEvent(base_events.HikariEvent): +class DisconnectedEvent(base_events.Event): """Event invoked each time a shard disconnects.""" shard: gateway_client.Gateway = attr.ib(repr=True) @@ -105,7 +105,7 @@ class DisconnectedEvent(base_events.HikariEvent): @attr.s(eq=False, hash=False, init=True, kw_only=True, slots=True) -class ResumedEvent(base_events.HikariEvent): +class ResumedEvent(base_events.Event): """Represents a gateway Resume event.""" shard: gateway_client.Gateway = attr.ib(repr=True) @@ -113,7 +113,7 @@ class ResumedEvent(base_events.HikariEvent): @attr.s(eq=False, hash=False, init=False, kw_only=True, slots=True) -class ReadyEvent(base_events.HikariEvent): +class ReadyEvent(base_events.Event): """Represents the gateway Ready event. This is received only when IDENTIFYing with the gateway. @@ -151,7 +151,7 @@ class ReadyEvent(base_events.HikariEvent): @attr.s(eq=False, hash=False, init=False, kw_only=True, slots=True) -class OwnUserUpdateEvent(base_events.HikariEvent): +class OwnUserUpdateEvent(base_events.Event): """Used to represent User Update gateway events. Sent when the current user is updated. diff --git a/hikari/events/voice.py b/hikari/events/voice.py index 702c3e3f8a..155f056366 100644 --- a/hikari/events/voice.py +++ b/hikari/events/voice.py @@ -36,7 +36,7 @@ @base_events.requires_intents(intents.Intent.GUILD_VOICE_STATES) @attr.s(eq=False, hash=False, init=False, kw_only=True, slots=True) -class VoiceStateUpdateEvent(base_events.HikariEvent): +class VoiceStateUpdateEvent(base_events.Event): """Used to represent voice state update gateway events. Sent when a user joins, leaves or moves voice channel(s). @@ -47,7 +47,7 @@ class VoiceStateUpdateEvent(base_events.HikariEvent): @attr.s(eq=False, hash=False, init=False, kw_only=True, slots=True) -class VoiceServerUpdateEvent(base_events.HikariEvent): +class VoiceServerUpdateEvent(base_events.Event): """Used to represent voice server update gateway events. Sent when initially connecting to voice and when the current voice instance diff --git a/hikari/impl/bot.py b/hikari/impl/bot.py index ef7baf21fc..206883d50b 100644 --- a/hikari/impl/bot.py +++ b/hikari/impl/bot.py @@ -20,17 +20,19 @@ from __future__ import annotations -__all__: typing.List[str] = ["BotImpl"] +__all__: typing.List[str] = ["BotAppImpl"] +import concurrent.futures import inspect import logging import os import platform import sys import typing -from concurrent import futures -from hikari.api import app +import asyncio + +from hikari.api import bot from hikari.impl import cache as cache_impl from hikari.impl import entity_factory as entity_factory_impl from hikari.impl import event_manager @@ -48,11 +50,18 @@ from hikari.api import entity_factory as entity_factory_ from hikari.api import event_consumer as event_consumer_ from hikari.api import event_dispatcher + from hikari.events import base as base_events from hikari.models import gateway as gateway_models from hikari.models import intents as intents_ + _EventT = typing.TypeVar("_EventT", bound=base_events.Event) + _PredicateT = typing.Callable[[base_events.Event], typing.Union[bool, typing.Coroutine[None, typing.Any, bool]]] + _SyncCallbackT = typing.Callable[[base_events.Event], None] + _AsyncCallbackT = typing.Callable[[base_events.Event], typing.Coroutine[None, typing.Any, None]] + _CallbackT = typing.Union[_SyncCallbackT, _AsyncCallbackT] + -class BotImpl(gateway_zookeeper.AbstractGatewayZookeeper, app.IBot): +class BotAppImpl(gateway_zookeeper.AbstractGatewayZookeeper, bot.IBotApp): """Implementation of an auto-sharded bot application. Parameters @@ -159,8 +168,9 @@ def __init__( rest_url: typing.Union[undefined.UndefinedType, str] = undefined.UNDEFINED, shard_ids: typing.Union[typing.Set[int], undefined.UndefinedType] = undefined.UNDEFINED, shard_count: typing.Union[int, undefined.UndefinedType] = undefined.UNDEFINED, + thread_pool_executor: typing.Optional[concurrent.futures.Executor] = None, token: str, - ): + ) -> None: self._logger = reflect.get_logger(self) # If logging is already configured, then this does nothing. @@ -170,11 +180,10 @@ def __init__( config = http_settings_.HTTPSettings() if config is undefined.UNDEFINED else config - self._cache = cache_impl.InMemoryCacheImpl(app=self) + self._cache = cache_impl.InMemoryCacheComponentImpl(app=self) self._config = config self._event_manager = event_manager.EventManagerImpl(app=self) - self._entity_factory = entity_factory_impl.EntityFactoryImpl(app=self) - + self._entity_factory = entity_factory_impl.EntityFactoryComponentImpl(app=self) self._rest = rest.REST( # nosec app=self, config=config, @@ -184,6 +193,7 @@ def __init__( rest_url=rest_url, version=rest_version, ) + self._thread_pool_executor = thread_pool_executor super().__init__( config=config, @@ -202,7 +212,7 @@ def __init__( ) @property - def event_dispatcher(self) -> event_dispatcher.IEventDispatcher: + def event_dispatcher(self) -> event_dispatcher.IEventDispatcherComponent: return self._event_manager @property @@ -210,58 +220,69 @@ def logger(self) -> logging.Logger: return self._logger @property - def cache(self) -> cache_.ICache: + def cache(self) -> cache_.ICacheComponent: return self._cache @property - def entity_factory(self) -> entity_factory_.IEntityFactory: + def entity_factory(self) -> entity_factory_.IEntityFactoryComponent: return self._entity_factory @property - def thread_pool_executor(self) -> typing.Optional[futures.ThreadPoolExecutor]: - # XXX: fixme - return None + def executor(self) -> typing.Optional[concurrent.futures.Executor]: + return self._thread_pool_executor @property def rest(self) -> rest.REST: return self._rest @property - def event_consumer(self) -> event_consumer_.IEventConsumer: + def event_consumer(self) -> event_consumer_.IEventConsumerComponent: return self._event_manager @property def http_settings(self) -> http_settings_.HTTPSettings: return self._config - def listen(self, event_type=undefined.UNDEFINED): + def listen( + self, event_type: typing.Union[undefined.UndefinedType, typing.Type[_EventT]] = undefined.UNDEFINED, + ) -> typing.Callable[[_CallbackT], _CallbackT]: return self.event_dispatcher.listen(event_type) - def subscribe(self, event_type, callback): + def subscribe( + self, + event_type: typing.Type[_EventT], + callback: typing.Callable[[_EventT], typing.Union[typing.Coroutine[None, typing.Any, None], None]], + ) -> typing.Callable[[_EventT], typing.Coroutine[None, typing.Any, None]]: return self.event_dispatcher.subscribe(event_type, callback) - def unsubscribe(self, event_type, callback): + def unsubscribe( + self, + event_type: typing.Type[_EventT], + callback: typing.Callable[[_EventT], typing.Coroutine[None, typing.Any, None]], + ) -> None: return self.event_dispatcher.unsubscribe(event_type, callback) - async def wait_for(self, event_type, predicate, timeout): + async def wait_for( + self, event_type: typing.Type[_EventT], predicate: _PredicateT, timeout: typing.Union[float, int, None], + ) -> _EventT: return await self.event_dispatcher.wait_for(event_type, predicate, timeout) - def dispatch(self, event): + def dispatch(self, event: base_events.Event) -> asyncio.Future[typing.Any]: return self.event_dispatcher.dispatch(event) async def close(self) -> None: await super().close() await self._rest.close() - async def _fetch_gateway_recommendations(self) -> gateway_models.GatewayBot: + async def fetch_sharding_settings(self) -> gateway_models.GatewayBot: return await self.rest.fetch_gateway_bot() - def __print_banner(self): + def __print_banner(self) -> None: from hikari import _about version = _about.__version__ - # noinspection PyTypeChecker - path = os.path.abspath(os.path.dirname(inspect.getsourcefile(_about))) + sourcefile = typing.cast(str, inspect.getsourcefile(_about)) + path = os.path.abspath(os.path.dirname(sourcefile)) python_implementation = platform.python_implementation() python_version = platform.python_version() operating_system = " ".join((platform.system(), *platform.architecture())) @@ -284,8 +305,11 @@ def __print_banner(self): top_line = "//" + ("=" * line_len) + r"\\" bottom_line = r"\\" + ("=" * line_len) + "//" + # The \r at the start will return to the start of the line for Unix + # consoles; for anything else that is logged, it will just act as + # a newline still. self.logger.info( - "\n%s\n%s\n%s\n%s\n%s\n%s\n%s\n", + "\r%s\n%s\n%s\n%s\n%s\n%s\n%s", top_line, version_str, copyright_str, @@ -296,7 +320,7 @@ def __print_banner(self): ) @staticmethod - def __get_logging_format(): + def __get_logging_format() -> str: # Modified from # https://github.com/django/django/blob/master/django/core/management/color.py diff --git a/hikari/impl/cache.py b/hikari/impl/cache.py index cbe7584404..0112553e97 100644 --- a/hikari/impl/cache.py +++ b/hikari/impl/cache.py @@ -20,22 +20,22 @@ from __future__ import annotations -__all__: typing.List[str] = ["InMemoryCacheImpl"] +__all__: typing.List[str] = ["InMemoryCacheComponentImpl"] import typing from hikari.api import cache if typing.TYPE_CHECKING: - from hikari.api import app as app_ + from hikari.api import rest -class InMemoryCacheImpl(cache.ICache): +class InMemoryCacheComponentImpl(cache.ICacheComponent): """In-memory cache implementation.""" - def __init__(self, app: app_.IRESTApp) -> None: + def __init__(self, app: rest.IRESTApp) -> None: self._app = app @property - def app(self) -> app_.IRESTApp: + def app(self) -> rest.IRESTApp: return self._app diff --git a/hikari/impl/entity_factory.py b/hikari/impl/entity_factory.py index 4d2a7efd99..e56e287304 100644 --- a/hikari/impl/entity_factory.py +++ b/hikari/impl/entity_factory.py @@ -20,13 +20,13 @@ from __future__ import annotations -__all__: typing.List[str] = ["EntityFactoryImpl"] +__all__: typing.List[str] = ["EntityFactoryComponentImpl"] import datetime import typing -from hikari.api import app as app_ from hikari.api import entity_factory +from hikari.api import rest from hikari.events import channel as channel_events from hikari.events import guild as guild_events from hikari.events import message as message_events @@ -56,16 +56,6 @@ if typing.TYPE_CHECKING: from hikari.utilities import data_binding -DMChannelT = typing.TypeVar("DMChannelT", bound=channel_models.DMChannel) -GuildChannelT = typing.TypeVar("GuildChannelT", bound=channel_models.GuildChannel) -InviteT = typing.TypeVar("InviteT", bound=invite_model.Invite) -PartialChannelT = typing.TypeVar("PartialChannelT", bound=channel_models.PartialChannel) -PartialGuildT = typing.TypeVar("PartialGuildT", bound=guild_models.PartialGuild) -PartialGuildIntegrationT = typing.TypeVar("PartialGuildIntegrationT", bound=guild_models.PartialIntegration) -UserT = typing.TypeVar("UserT", bound=user_models.User) -ReactionEventT = typing.TypeVar("ReactionEventT", bound=message_events.BaseMessageReactionEvent) -GuildBanEventT = typing.TypeVar("GuildBanEventT", bound=guild_events.GuildBanEvent) - def _deserialize_seconds_timedelta(seconds: typing.Union[str, int]) -> datetime.timedelta: return datetime.timedelta(seconds=int(seconds)) @@ -83,13 +73,23 @@ def _deserialize_max_age(seconds: int) -> typing.Optional[datetime.timedelta]: return datetime.timedelta(seconds=seconds) if seconds > 0 else None -class EntityFactoryImpl(entity_factory.IEntityFactory): +class EntityFactoryComponentImpl(entity_factory.IEntityFactoryComponent): """Standard implementation for a serializer/deserializer. This will convert objects to/from JSON compatible representations. """ - def __init__(self, app: app_.IRESTApp) -> None: + DMChannelT = typing.TypeVar("DMChannelT", bound=channel_models.DMChannel) + GuildChannelT = typing.TypeVar("GuildChannelT", bound=channel_models.GuildChannel) + InviteT = typing.TypeVar("InviteT", bound=invite_model.Invite) + PartialChannelT = typing.TypeVar("PartialChannelT", bound=channel_models.PartialChannel) + PartialGuildT = typing.TypeVar("PartialGuildT", bound=guild_models.PartialGuild) + PartialGuildIntegrationT = typing.TypeVar("PartialGuildIntegrationT", bound=guild_models.PartialIntegration) + UserT = typing.TypeVar("UserT", bound=user_models.User) + ReactionEventT = typing.TypeVar("ReactionEventT", bound=message_events.BaseMessageReactionEvent) + GuildBanEventT = typing.TypeVar("GuildBanEventT", bound=guild_events.GuildBanEvent) + + def __init__(self, app: rest.IRESTApp) -> None: self._app = app self._audit_log_entry_converters: typing.Mapping[str, typing.Callable[[typing.Any], typing.Any]] = { audit_log_models.AuditLogChangeKey.OWNER_ID: snowflake.Snowflake, @@ -150,7 +150,7 @@ def __init__(self, app: app_.IRESTApp) -> None: } @property - def app(self) -> app_.IRESTApp: + def app(self) -> rest.IRESTApp: return self._app ###################### @@ -393,9 +393,8 @@ def deserialize_permission_overwrite(self, payload: data_binding.JSONObject) -> def serialize_permission_overwrite(self, overwrite: channel_models.PermissionOverwrite) -> data_binding.JSONObject: return {"id": str(overwrite.id), "type": overwrite.type, "allow": overwrite.allow, "deny": overwrite.deny} - def _set_partial_channel_attributes( - self, payload: data_binding.JSONObject, channel: PartialChannelT - ) -> PartialChannelT: + @staticmethod + def _set_partial_channel_attributes(payload: data_binding.JSONObject, channel: PartialChannelT) -> PartialChannelT: channel.id = snowflake.Snowflake(payload["id"]) channel.name = payload.get("name") # noinspection PyArgumentList @@ -609,7 +608,7 @@ def serialize_embed( payload["color"] = str(embed.color) if embed.footer is not None: - footer_payload = {} + footer_payload: data_binding.JSONObject = {} if embed.footer.text is not None: footer_payload["text"] = embed.footer.text @@ -623,7 +622,7 @@ def serialize_embed( payload["footer"] = footer_payload if embed.image is not None: - image_payload = {} + image_payload: data_binding.JSONObject = {} if embed.image is not None: if not isinstance(embed.image.resource, files.WebResource): @@ -634,7 +633,7 @@ def serialize_embed( payload["image"] = image_payload if embed.thumbnail is not None: - thumbnail_payload = {} + thumbnail_payload: data_binding.JSONObject = {} if embed.thumbnail is not None: if not isinstance(embed.thumbnail.resource, files.WebResource): @@ -645,7 +644,7 @@ def serialize_embed( payload["thumbnail"] = thumbnail_payload if embed.author is not None: - author_payload = {} + author_payload: data_binding.JSONObject = {} if embed.author.name is not None: author_payload["name"] = embed.author.name @@ -654,16 +653,16 @@ def serialize_embed( author_payload["url"] = embed.author.url if embed.author.icon is not None: - if not isinstance(embed.footer.icon.resource, files.WebResource): + if not isinstance(embed.author.icon.resource, files.WebResource): uploads.append(embed.author.icon) author_payload["icon_url"] = embed.author.icon.url payload["author"] = author_payload if embed.fields: - field_payloads = [] + field_payloads: data_binding.JSONArray = [] for field in embed.fields: - field_payload = {} + field_payload: data_binding.JSONObject = {} if field.name: field_payload["name"] = field.name @@ -839,7 +838,8 @@ def deserialize_unavailable_guild(self, payload: data_binding.JSONObject) -> gui unavailable_guild.id = snowflake.Snowflake(payload["id"]) return unavailable_guild - def _set_partial_guild_attributes(self, payload: data_binding.JSONObject, guild: PartialGuildT) -> PartialGuildT: + @staticmethod + def _set_partial_guild_attributes(payload: data_binding.JSONObject, guild: PartialGuildT) -> PartialGuildT: guild.id = snowflake.Snowflake(payload["id"]) guild.name = payload["name"] guild.icon_hash = payload["icon"] @@ -873,9 +873,12 @@ def deserialize_guild(self, payload: data_binding.JSONObject) -> guild_models.Gu guild.discovery_splash_hash = payload["discovery_splash"] guild.owner_id = snowflake.Snowflake(payload["owner_id"]) # noinspection PyArgumentList - guild.my_permissions = ( - permission_models.Permission(payload["permissions"]) if "permissions" in payload else None - ) + + if (perms := payload.get("permissions")) is not None: + guild.my_permissions = permission_models.Permission(perms) + else: + guild.my_permissions = undefined.UNDEFINED + guild.region = payload["region"] if (afk_channel_id := payload["afk_channel_id"]) is not None: @@ -933,29 +936,37 @@ def deserialize_guild(self, payload: data_binding.JSONObject) -> guild_models.Gu guild.member_count = int(payload["member_count"]) if "member_count" in payload else None if (members := payload.get("members", ...)) is not ...: - guild.members = { - snowflake.Snowflake(member["user"]["id"]): self.deserialize_member(member) for member in members - } + guild.members = {} + for member_payload in members: + member = self.deserialize_member(member_payload) + # Could be None, so cast to avoid. + user_id = typing.cast("user_models.User", member.user).id + guild.members[user_id] = member else: + # FIXME: should this be an empty dict instead? guild.members = None if (channels := payload.get("channels", ...)) is not ...: - guild.channels = { - snowflake.Snowflake(channel["id"]): self.deserialize_channel(channel) for channel in channels - } + guild.channels = {} + for channel_payload in channels: + channel = typing.cast("channel_models.GuildChannel", self.deserialize_partial_channel(channel_payload)) + guild.channels[channel.id] = channel else: + # FIXME: should this be an empty dict instead? guild.channels = None if (presences := payload.get("presences", ...)) is not ...: - guild.presences = { - snowflake.Snowflake(presence["user"]["id"]): self.deserialize_member_presence(presence) - for presence in presences - } + guild.presences = {} + for presence_payload in presences: + presence = self.deserialize_member_presence(presence_payload) + guild.presences[presence.user.id] = presence else: + # FIXME: should this be an empty dict instead? guild.presences = None if (max_presences := payload.get("max_presences")) is not None: max_presences = int(max_presences) + guild.max_presences = max_presences guild.max_members = int(payload["max_members"]) if "max_members" in payload else None @@ -965,7 +976,6 @@ def deserialize_guild(self, payload: data_binding.JSONObject) -> guild_models.Gu guild.vanity_url_code = payload["vanity_url_code"] guild.description = payload["description"] guild.banner_hash = payload["banner"] - # noinspection PyArgumentList guild.premium_tier = guild_models.GuildPremiumTier(payload["premium_tier"]) if (premium_subscription_count := payload.get("premium_subscription_count")) is not None: @@ -1161,6 +1171,7 @@ def deserialize_member_presence(self, payload: data_binding.JSONObject) -> prese if (role_ids := payload.get("roles", ...)) is not ...: guild_member_presence.role_ids = {snowflake.Snowflake(role_id) for role_id in role_ids} else: + # FIXME: should this be an empty set? guild_member_presence.role_ids = None guild_member_presence.guild_id = snowflake.Snowflake(payload["guild_id"]) if "guild_id" in payload else None @@ -1278,7 +1289,8 @@ def deserialize_member_presence(self, payload: data_binding.JSONObject) -> prese # USER MODELS # ############### - def _set_user_attributes(self, payload: data_binding.JSONObject, user: UserT) -> UserT: + @staticmethod + def _set_user_attributes(payload: data_binding.JSONObject, user: UserT) -> UserT: user.id = snowflake.Snowflake(payload["id"]) user.discriminator = payload["discriminator"] user.username = payload["username"] @@ -1410,7 +1422,7 @@ def deserialize_typing_start_event(self, payload: data_binding.JSONObject) -> ch def deserialize_invite_create_event(self, payload: data_binding.JSONObject) -> channel_events.InviteCreateEvent: invite_create = channel_events.InviteCreateEvent() - invite_create.invite = self.deserialize_invite(payload) + invite_create.invite = self.deserialize_invite_with_metadata(payload) return invite_create def deserialize_invite_delete_event(self, payload: data_binding.JSONObject) -> channel_events.InviteDeleteEvent: @@ -1538,11 +1550,9 @@ def deserialize_message_create_event(self, payload: data_binding.JSONObject) -> def deserialize_message_update_event(self, payload: data_binding.JSONObject) -> message_events.MessageUpdateEvent: message_update = message_events.MessageUpdateEvent() - updated_message = message_events.UpdateMessage(self._app) + updated_message = message_events.UpdatedMessage(self._app) updated_message.id = snowflake.Snowflake(payload["id"]) - updated_message.channel_id = ( - snowflake.Snowflake(payload["channel_id"]) if "channel_id" in payload else undefined.UNDEFINED - ) + updated_message.channel_id = snowflake.Snowflake(payload["channel_id"]) updated_message.guild_id = ( snowflake.Snowflake(payload["guild_id"]) if "guild_id" in payload else undefined.UNDEFINED ) @@ -1639,7 +1649,7 @@ def deserialize_message_update_event(self, payload: data_binding.JSONObject) -> updated_message.activity = undefined.UNDEFINED updated_message.application = ( - self.deserialize_application(payload["application"]) if "application" in payload else undefined.UNDEFINED + self.deserialize_application(payload["application"]) if "application" in payload else None ) if (crosspost_payload := payload.get("message_reference", ...)) is not ...: @@ -1680,8 +1690,9 @@ def deserialize_message_delete_bulk_event( message_delete_bulk.message_ids = {snowflake.Snowflake(message_id) for message_id in payload["ids"]} return message_delete_bulk + @staticmethod def _set_base_message_reaction_fields( - self, payload: data_binding.JSONObject, reaction_event: ReactionEventT + payload: data_binding.JSONObject, reaction_event: ReactionEventT ) -> ReactionEventT: reaction_event.channel_id = snowflake.Snowflake(payload["channel_id"]) reaction_event.message_id = snowflake.Snowflake(payload["message_id"]) @@ -1739,9 +1750,9 @@ def deserialize_ready_event( } ready_event.session_id = payload["session_id"] - if (shard := payload.get("shard", ...)) is not ...: - ready_event.shard_id = int(shard[0]) - ready_event.shard_count = int(shard[1]) + if (shard_data := payload.get("shard", ...)) is not ...: + ready_event.shard_id = int(shard_data[0]) + ready_event.shard_count = int(shard_data[1]) else: ready_event.shard_id = ready_event.shard_count = None diff --git a/hikari/impl/event_manager.py b/hikari/impl/event_manager.py index b416f6667c..3d62933f1a 100644 --- a/hikari/impl/event_manager.py +++ b/hikari/impl/event_manager.py @@ -32,7 +32,7 @@ from hikari.utilities import data_binding -class EventManagerImpl(event_manager_core.EventManagerCore): +class EventManagerImpl(event_manager_core.EventManagerCoreComponent): """Provides event handling logic for Discord events.""" async def on_connected(self, shard: gateway.Gateway, _: data_binding.JSONObject) -> None: diff --git a/hikari/impl/event_manager_core.py b/hikari/impl/event_manager_core.py index 6d0055c710..c837818582 100644 --- a/hikari/impl/event_manager_core.py +++ b/hikari/impl/event_manager_core.py @@ -20,7 +20,7 @@ from __future__ import annotations -__all__: typing.List[str] = ["EventManagerCore"] +__all__: typing.List[str] = ["EventManagerCoreComponent"] import asyncio import functools @@ -37,9 +37,9 @@ from hikari.utilities import undefined if typing.TYPE_CHECKING: - from hikari.api import app as app_ + from hikari.api import rest - _EventT = typing.TypeVar("_EventT", bound=base.HikariEvent, contravariant=True) + _EventT = typing.TypeVar("_EventT", bound=base.Event, contravariant=True) _PredicateT = typing.Callable[[_EventT], typing.Union[bool, typing.Coroutine[None, typing.Any, bool]]] _SyncCallbackT = typing.Callable[[_EventT], None] _AsyncCallbackT = typing.Callable[[_EventT], typing.Coroutine[None, typing.Any, None]] @@ -49,21 +49,21 @@ _WaiterMapT = typing.MutableMapping[typing.Type[_EventT], typing.MutableSet[_WaiterT]] -class EventManagerCore(event_dispatcher.IEventDispatcher, event_consumer.IEventConsumer): +class EventManagerCoreComponent(event_dispatcher.IEventDispatcherComponent, event_consumer.IEventConsumerComponent): """Provides functionality to consume and dispatch events. Specific event handlers should be in functions named `on_xxx` where `xxx` is the raw event name being dispatched in lower-case. """ - def __init__(self, app: app_.IRESTApp) -> None: + def __init__(self, app: rest.IRESTApp) -> None: self._app = app self._listeners: _ListenerMapT = {} self._waiters: _WaiterMapT = {} self.logger = reflect.get_logger(self) @property - def app(self) -> app_.IRESTApp: + def app(self) -> rest.IRESTApp: return self._app async def consume_raw_event( @@ -143,8 +143,8 @@ def decorator(callback: _CallbackT) -> _CallbackT: event_type = event_param.annotation - if not isinstance(event_type, type) or not issubclass(event_type, base.HikariEvent): - raise TypeError("Event type must derive from HikariEvent") + if not isinstance(event_type, type) or not issubclass(event_type, base.Event): + raise TypeError("Event type must derive from Event") self.subscribe(event_type, callback) return callback @@ -184,34 +184,18 @@ async def _test_waiter( if not self._waiters[cls]: del self._waiters[cls] - async def _invoke_callback(self, callback: _AsyncCallbackT, event: _EventT) -> None: - try: - result = callback(event) - if asyncio.iscoroutine(result): - await result - - except Exception as ex: - # Skip the first frame in logs, we don't care for it. - trio = type(ex), ex, ex.__traceback__.tb_next if ex.__traceback__ is not None else None - - if base.is_no_catch_event(event): - self.logger.error("an exception occurred handling an event, but it has been ignored", exc_info=trio) - else: - self.logger.error("an exception occurred handling an event", exc_info=trio) - await self.dispatch(other.ExceptionEvent(exception=ex, event=event, callback=callback)) - - def dispatch(self, event: base.HikariEvent) -> asyncio.Future[typing.Any]: - if not isinstance(event, base.HikariEvent): - raise TypeError(f"Events must be subclasses of {base.HikariEvent.__name__}, not {type(event).__name__}") + def dispatch(self, event: base.Event) -> asyncio.Future[typing.Any]: + if not isinstance(event, base.Event): + raise TypeError(f"Events must be subclasses of {base.Event.__name__}, not {type(event).__name__}") - # We only need to iterate through the MRO until we hit HikariEvent, as + # We only need to iterate through the MRO until we hit Event, as # anything after that is random garbage we don't care about, as they do # not describe event types. This improves efficiency as well. mro = type(event).mro() tasks: typing.List[typing.Coroutine[None, typing.Any, None]] = [] - for cls in mro[: mro.index(base.HikariEvent) + 1]: + for cls in mro[: mro.index(base.Event) + 1]: if cls in self._listeners: for callback in self._listeners[cls]: @@ -222,3 +206,19 @@ def dispatch(self, event: base.HikariEvent) -> asyncio.Future[typing.Any]: tasks.append(self._test_waiter(cls, event, predicate, future)) return asyncio.gather(*tasks) if tasks else aio.completed_future() + + async def _invoke_callback(self, callback: _AsyncCallbackT, event: _EventT) -> None: + try: + result = callback(event) + if asyncio.iscoroutine(result): + await result + + except Exception as ex: + # Skip the first frame in logs, we don't care for it. + trio = type(ex), ex, ex.__traceback__.tb_next if ex.__traceback__ is not None else None + + if base.is_no_catch_event(event): + self.logger.error("an exception occurred handling an event, but it has been ignored", exc_info=trio) + else: + self.logger.error("an exception occurred handling an event", exc_info=trio) + await self.dispatch(other.ExceptionEvent(exception=ex, event=event, callback=callback)) diff --git a/hikari/impl/gateway_zookeeper.py b/hikari/impl/gateway_zookeeper.py index 2e1f343911..a758d5546a 100644 --- a/hikari/impl/gateway_zookeeper.py +++ b/hikari/impl/gateway_zookeeper.py @@ -24,15 +24,16 @@ import abc import asyncio -import contextlib import datetime import signal import time import typing -from hikari.api import app as app_ +from hikari.api import event_dispatcher +from hikari.api import gateway_zookeeper from hikari.events import other from hikari.net import gateway +from hikari.utilities import aio from hikari.utilities import undefined if typing.TYPE_CHECKING: @@ -43,7 +44,7 @@ from hikari.models import presences -class AbstractGatewayZookeeper(app_.IGatewayZookeeper, abc.ABC): +class AbstractGatewayZookeeper(gateway_zookeeper.IGatewayZookeeperApp, abc.ABC): """Provides keep-alive logic for orchestrating multiple shards. This provides the logic needed to keep multiple shards alive at once, and @@ -126,6 +127,10 @@ class AbstractGatewayZookeeper(app_.IGatewayZookeeper, abc.ABC): If sharding information is not specified correctly. """ + # We do not bother with SIGINT here, since we can catch it as a KeyboardInterrupt + # instead and provide tidier handling of the stacktrace as a result. + _SIGNALS: typing.Final[typing.ClassVar[typing.Sequence[str]]] = ["SIGQUIT", "SIGTERM"] + def __init__( self, *, @@ -138,19 +143,19 @@ def __init__( initial_status: typing.Union[undefined.UndefinedType, presences.Status] = undefined.UNDEFINED, intents: typing.Optional[intents_.Intent], large_threshold: int, - shard_ids: typing.Set[int], - shard_count: int, + shard_ids: typing.Union[typing.Set[int], undefined.UndefinedType] = undefined.UNDEFINED, + shard_count: typing.Union[int, undefined.UndefinedType] = undefined.UNDEFINED, token: str, version: int, ) -> None: if undefined.count(shard_ids, shard_count) == 1: raise TypeError("You must provide values for both shard_ids and shard_count, or neither.") - if not shard_ids is undefined.UNDEFINED: + if shard_ids is not undefined.UNDEFINED: if not shard_ids: raise ValueError("At least one shard ID must be specified if provided.") if not all(shard_id >= 0 for shard_id in shard_ids): raise ValueError("shard_ids must be greater than or equal to 0.") - if not all(shard_id < shard_count for shard_id in shard_ids): + if shard_count is not undefined.UNDEFINED and not all(shard_id < shard_count for shard_id in shard_ids): raise ValueError("shard_ids must be less than the total shard_count.") self._aiohttp_config = config @@ -164,8 +169,8 @@ def __init__( self._large_threshold = large_threshold self._max_concurrency = 1 self._request_close_event = asyncio.Event() - self._shard_count = shard_count - self._shard_ids = shard_ids + self._shard_count = shard_count if shard_count is not undefined.UNDEFINED else 0 + self._shard_ids = set() if shard_ids is undefined.UNDEFINED else shard_ids self._shards: typing.Dict[int, gateway.Gateway] = {} self._tasks: typing.Dict[int, asyncio.Task[typing.Any]] = {} self._token = token @@ -173,13 +178,34 @@ def __init__( self._version = version @property - def gateway_shards(self) -> typing.Mapping[int, gateway.Gateway]: + def shards(self) -> typing.Mapping[int, gateway.Gateway]: return self._shards @property - def gateway_shard_count(self) -> int: + def shard_count(self) -> int: return self._shard_count + def run(self) -> None: + loop = asyncio.get_event_loop() + + def on_interrupt() -> None: + loop.create_task(self.close(), name="signal interrupt shutting down application") + + try: + self._map_signal_handlers(loop.add_signal_handler, on_interrupt) + loop.run_until_complete(self._run()) + except KeyboardInterrupt as ex: + self.logger.info("received signal to shut down client") + if self._debug: + raise + else: + # The user won't care where this gets raised from, unless we are + # debugging. It just causes a lot of confusing spam. + raise ex.with_traceback(None) + finally: + self._map_signal_handlers(loop.remove_signal_handler) + self.logger.info("client has shut down") + async def start(self) -> None: self._tasks.clear() self._gather_task = None @@ -190,7 +216,10 @@ async def start(self) -> None: await self._maybe_dispatch(other.StartingEvent()) - self.logger.info("starting %s shard(s)", len(self._shards)) + if self._shard_count > 1: + self.logger.info("starting %s shard(s)", len(self._shards)) + else: + self.logger.info("this application will be single-sharded") start_time = time.perf_counter() @@ -234,9 +263,7 @@ async def start(self) -> None: raise finish_time = time.perf_counter() - self._gather_task = asyncio.create_task( - self._gather(), name=f"shard zookeeper for {len(self._shards)} shard(s)" - ) + self._gather_task = asyncio.create_task(self._gather(), name=f"zookeeper for {len(self._shards)} shard(s)") self.logger.info("started %s shard(s) in approx %.2fs", len(self._shards), finish_time - start_time) await self._maybe_dispatch(other.StartedEvent()) @@ -253,41 +280,11 @@ async def close(self) -> None: self.logger.info("stopping %s shard(s)", len(self._tasks)) try: - if isinstance(self, app_.IGatewayDispatcher): - # noinspection PyUnresolvedReferences - await self.event_dispatcher.dispatch(other.StoppingEvent()) - + await self._maybe_dispatch(other.StoppingEvent()) await self._abort() finally: self._tasks.clear() - - if isinstance(self, app_.IGatewayDispatcher): - # noinspection PyUnresolvedReferences - await self.event_dispatcher.dispatch(other.StoppedEvent()) - - def run(self) -> None: - loop = asyncio.get_event_loop() - - def sigterm_handler(*_: typing.Any) -> None: - loop.create_task(self.close()) - - try: - with contextlib.suppress(NotImplementedError): - # Not implemented on Windows - loop.add_signal_handler(signal.SIGTERM, sigterm_handler) - - loop.run_until_complete(self._run()) - - except KeyboardInterrupt: - self.logger.info("received signal to shut down client") - raise - - finally: - loop.run_until_complete(self.close()) - with contextlib.suppress(NotImplementedError): - # Not implemented on Windows - loop.remove_signal_handler(signal.SIGTERM) - self.logger.info("client has shut down") + await self._maybe_dispatch(other.StoppedEvent()) async def update_presence( self, @@ -305,14 +302,25 @@ async def update_presence( ) ) + @abc.abstractmethod + async def fetch_sharding_settings(self) -> gateway_models.GatewayBot: + """Fetch the recommended sharding settings and gateway URL from Discord. + + Returns + ------- + hikari.models.gateway.GatewayBot + The recommended sharding settings and configuration for the + bot account. + """ + async def _init(self) -> None: - gw_recs = await self._fetch_gateway_recommendations() + gw_recs = await self.fetch_sharding_settings() self.logger.info( - "you have sent an IDENTIFY %s time(s) before now, and have %s remaining. This will reset at %s.", + "you have opened %s session(s) recently, you can open %s more before %s", gw_recs.session_start_limit.total - gw_recs.session_start_limit.remaining, - gw_recs.session_start_limit.remaining, - datetime.datetime.now() + gw_recs.session_start_limit.reset_after, + gw_recs.session_start_limit.remaining if gw_recs.session_start_limit.remaining > 0 else "no", + (datetime.datetime.now() + gw_recs.session_start_limit.reset_after).strftime("%c"), ) self._shard_count = self._shard_count if self._shard_count else gw_recs.shard_count @@ -321,7 +329,7 @@ async def _init(self) -> None: url = gw_recs.url self.logger.info( - "will connect shards to %s. max_concurrency while connecting is %s, contact Discord to get this increased", + "will connect shards to %s at a rate of %s shard(s) per 5 seconds (contact Discord to increase this rate)", url, self._max_concurrency, ) @@ -365,10 +373,6 @@ def _max_concurrency_chunker(self) -> typing.Iterator[typing.Iterator[int]]: yield iter(next_window) n += self._max_concurrency - @abc.abstractmethod - async def _fetch_gateway_recommendations(self) -> gateway_models.GatewayBot: - ... - async def _abort(self) -> None: for shard_id in self._tasks: if self._shards[shard_id].is_alive: @@ -383,10 +387,22 @@ async def _gather(self) -> None: await self.close() async def _run(self) -> None: - await self.start() - await self.join() + try: + await self.start() + await self.join() + finally: + await self.close() - async def _maybe_dispatch(self, event: base_events.HikariEvent) -> None: - if isinstance(self, app_.IGatewayDispatcher): - # noinspection PyUnresolvedReferences - await self.event_dispatcher.dispatch(event) + def _maybe_dispatch(self, event: base_events.Event) -> typing.Awaitable[typing.Any]: + if isinstance(self, event_dispatcher.IEventDispatcherApp): + return self.event_dispatcher.dispatch(event) + else: + return aio.completed_future() + + def _map_signal_handlers( + self, mapping_function: typing.Callable[..., None], *args: typing.Callable[[], typing.Any], + ) -> None: + valid_interrupts = signal.valid_signals() + for interrupt in self._SIGNALS: + if (code := getattr(signal, interrupt, None)) is not None and code in valid_interrupts: + mapping_function(code, *args) diff --git a/hikari/impl/rest_app.py b/hikari/impl/rest.py similarity index 89% rename from hikari/impl/rest_app.py rename to hikari/impl/rest.py index b2d64e471e..dc229f7b23 100644 --- a/hikari/impl/rest_app.py +++ b/hikari/impl/rest.py @@ -30,11 +30,11 @@ import typing from concurrent import futures -from hikari.api import app as app_ +from hikari.api import rest as rest_api from hikari.impl import cache as cache_impl from hikari.impl import entity_factory as entity_factory_impl from hikari.net import http_settings as http_settings_ -from hikari.net import rest as rest_ +from hikari.net import rest as rest_component from hikari.utilities import reflect from hikari.utilities import undefined @@ -43,7 +43,7 @@ from hikari.api import entity_factory as entity_factory_ -class RESTAppImpl(app_.IRESTApp): +class RESTAppImpl(rest_api.IRESTApp): """Application that only provides RESTful functionality. Parameters @@ -85,30 +85,30 @@ def __init__( config = http_settings_.HTTPSettings() if config is undefined.UNDEFINED else config - self._rest = rest_.REST( + self._rest = rest_component.REST( app=self, config=config, debug=debug, token=token, token_type=token_type, rest_url=url, version=version, ) - self._cache = cache_impl.InMemoryCacheImpl(self) - self._entity_factory = entity_factory_impl.EntityFactoryImpl(self) + self._cache = cache_impl.InMemoryCacheComponentImpl(self) + self._entity_factory = entity_factory_impl.EntityFactoryComponentImpl(self) @property def logger(self) -> logging.Logger: return self._logger @property - def thread_pool_executor(self) -> typing.Optional[futures.ThreadPoolExecutor]: + def executor(self) -> typing.Optional[futures.Executor]: return None @property - def rest(self) -> rest_.REST: + def rest(self) -> rest_component.REST: return self._rest @property - def cache(self) -> cache_.ICache: + def cache(self) -> cache_.ICacheComponent: return self._cache @property - def entity_factory(self) -> entity_factory_.IEntityFactory: + def entity_factory(self) -> entity_factory_.IEntityFactoryComponent: return self._entity_factory async def close(self) -> None: diff --git a/hikari/models/bases.py b/hikari/models/bases.py index 4bb4d096be..2843ba39b3 100644 --- a/hikari/models/bases.py +++ b/hikari/models/bases.py @@ -27,10 +27,10 @@ import attr -from hikari.api import app as app_ from hikari.utilities import snowflake if typing.TYPE_CHECKING: + from hikari.api import rest import datetime @@ -43,10 +43,10 @@ class Entity(abc.ABC): methods directly. """ - _app: app_.IRESTApp = attr.ib(default=None, repr=False, eq=False, hash=False) + _app: rest.IRESTApp = attr.ib(default=None, repr=False, eq=False, hash=False) """The client application that models may use for procedures.""" - def __init__(self, app: app_.IRESTApp) -> None: + def __init__(self, app: rest.IRESTApp) -> None: self._app = app diff --git a/hikari/models/guilds.py b/hikari/models/guilds.py index cea183c433..60c5d2ea54 100644 --- a/hikari/models/guilds.py +++ b/hikari/models/guilds.py @@ -222,10 +222,10 @@ class Member(bases.Entity): # TODO: make Member delegate to user and implement a common base class # this allows members and users to be used interchangeably. - user: users.User = attr.ib(eq=True, hash=True, repr=True) + user: typing.Union[undefined.UndefinedType, users.User] = attr.ib(eq=True, hash=True, repr=True) """This member's user object. - This will be `None` when attached to Message Create and Update gateway events. + This will be undefined when attached to Message Create and Update gateway events. """ nickname: typing.Union[str, None, undefined.UndefinedType] = attr.ib( @@ -570,13 +570,15 @@ class Guild(PartialGuild): # pylint:disable=too-many-instance-attributes owner_id: snowflake.Snowflake = attr.ib(eq=False, hash=False, repr=True) """The ID of the owner of this guild.""" - my_permissions: permissions_.Permission = attr.ib(eq=False, hash=False, repr=False) + my_permissions: typing.Union[undefined.UndefinedType, permissions_.Permission] = attr.ib( + eq=False, hash=False, repr=False + ) """The guild-level permissions that apply to the bot user. This will not take into account permission overwrites or implied - permissions (for example, ADMINISTRATOR implies all other permissions). + permissions (for example, `ADMINISTRATOR` implies all other permissions). - This will be `None` when this object is retrieved through a RESTSession request + This will be `None` when this object is retrieved through a REST request rather than from the gateway. """ @@ -831,7 +833,7 @@ class Guild(PartialGuild): # pylint:disable=too-many-instance-attributes approximate_member_count: typing.Optional[int] = attr.ib(eq=False, hash=False, repr=False) """The approximate number of members in the guild. - This information will be provided by RESTSession API calls fetching the guilds that + This information will be provided by REST API calls fetching the guilds that a bot account is in. For all other purposes, this should be expected to remain `None`. """ @@ -839,7 +841,7 @@ class Guild(PartialGuild): # pylint:disable=too-many-instance-attributes approximate_active_member_count: typing.Optional[int] = attr.ib(eq=False, hash=False, repr=False) """The approximate number of members in the guild that are not offline. - This information will be provided by RESTSession API calls fetching the guilds that + This information will be provided by REST API calls fetching the guilds that a bot account is in. For all other purposes, this should be expected to remain `None`. """ diff --git a/hikari/models/invites.py b/hikari/models/invites.py index 1f4645785e..bfda7b336a 100644 --- a/hikari/models/invites.py +++ b/hikari/models/invites.py @@ -226,6 +226,8 @@ class InviteWithMetadata(Invite): If set to `0` then this is unlimited. """ + # FIXME: can we use a non-None value to represent infinity here somehow, or + # make a timedelta that is infinite for comparisons? max_age: typing.Optional[datetime.timedelta] = attr.attrib(eq=False, hash=False, repr=False) """The timedelta of how long this invite will be valid for. diff --git a/hikari/models/presences.py b/hikari/models/presences.py index e3119b5e5c..492b4a32ab 100644 --- a/hikari/models/presences.py +++ b/hikari/models/presences.py @@ -225,8 +225,8 @@ class RichActivity(Activity): is_instance: typing.Optional[bool] = attr.ib(repr=False) """Whether this activity is an instanced game session.""" - flags: ActivityFlag = attr.ib(repr=False) - """Flags that describe what the activity includes.""" + flags: typing.Optional[ActivityFlag] = attr.ib(repr=False) + """Flags that describe what the activity includes, if present.""" class Status(str, enum.Enum): @@ -275,7 +275,7 @@ class MemberPresence(bases.Entity): changed in an event. """ - role_ids: typing.Optional[typing.Sequence[snowflake.Snowflake]] = attr.ib(eq=False, hash=False, repr=False) + role_ids: typing.Optional[typing.Set[snowflake.Snowflake]] = attr.ib(eq=False, hash=False, repr=False) """The ids of the user's current roles in the guild this presence belongs to. !!! info diff --git a/hikari/models/webhooks.py b/hikari/models/webhooks.py index f96c55b70f..2f1a4cf330 100644 --- a/hikari/models/webhooks.py +++ b/hikari/models/webhooks.py @@ -27,7 +27,6 @@ import attr -from hikari.api import app from hikari.models import bases from hikari.utilities import cdn from hikari.utilities import files as files_ diff --git a/hikari/net/buckets.py b/hikari/net/buckets.py index 2d90af85f3..e2d6d90806 100644 --- a/hikari/net/buckets.py +++ b/hikari/net/buckets.py @@ -222,9 +222,9 @@ class RESTBucket(rate_limits.WindowedBurstRateLimiter): - """Represents a rate limit for an RESTSession endpoint. + """Represents a rate limit for an REST endpoint. - Component to represent an active rate limit bucket on a specific RESTSession _route + Component to represent an active rate limit bucket on a specific REST _route with a specific major parameter combo. This is somewhat similar to the `WindowedBurstRateLimiter` in how it @@ -307,9 +307,9 @@ def drip(self) -> None: class RESTBucketManager: - """The main rate limiter implementation for RESTSession clients. + """The main rate limiter implementation for REST clients. - This is designed to provide bucketed rate limiting for Discord RESTSession + This is designed to provide bucketed rate limiting for Discord REST endpoints that respects the `X-RateLimit-Bucket` rate limit header. To do this, it makes the assumption that any limit can change at any time. """ @@ -502,7 +502,7 @@ def acquire(self, compiled_route: routes.CompiledRoute) -> asyncio.Future[None]: !!! note The returned future MUST be awaited, and will complete when your turn to make a call comes along. You are expected to await this and - then immediately make your RESTSession call. The returned future may + then immediately make your REST call. The returned future may already be completed if you can make the call immediately. """ # Returns a future to await on to wait to be allowed to send the request, and a diff --git a/hikari/net/gateway.py b/hikari/net/gateway.py index b906115c54..760b36a7a0 100644 --- a/hikari/net/gateway.py +++ b/hikari/net/gateway.py @@ -47,7 +47,7 @@ if typing.TYPE_CHECKING: import datetime - from hikari.api import app as app_ + from hikari.api import event_consumer from hikari.net import http_settings from hikari.models import channels from hikari.models import guilds @@ -60,7 +60,7 @@ class Gateway(http_client.HTTPClient, component.IComponent): Parameters ---------- - app : hikari.api.app.IGatewayConsumer + app : hikari.api.event_consumer.IEventConsumerApp The base application. config : hikari.net.http_settings.HTTPSettings The AIOHTTP settings to use for the client session. @@ -158,7 +158,7 @@ class _InvalidSession(RuntimeError): def __init__( self, *, - app: app_.IGatewayConsumer, + app: event_consumer.IEventConsumerApp, config: http_settings.HTTPSettings, debug: bool = False, initial_activity: typing.Union[undefined.UndefinedType, None, presences.Activity] = undefined.UNDEFINED, @@ -229,7 +229,7 @@ def __init__( self.url = urllib.parse.urlunparse((scheme, netloc, path, params, new_query, "")) @property - def app(self) -> app_.IGatewayConsumer: + def app(self) -> event_consumer.IEventConsumerApp: return self._app @property diff --git a/hikari/net/iterators.py b/hikari/net/iterators.py index 188a425e27..003e019ace 100644 --- a/hikari/net/iterators.py +++ b/hikari/net/iterators.py @@ -24,23 +24,23 @@ import abc import typing +from hikari.api import rest from hikari.net import routes from hikari.utilities import data_binding from hikari.utilities import snowflake from hikari.utilities import undefined if typing.TYPE_CHECKING: - from hikari.api import app as app_ from hikari.models import applications from hikari.models import audit_logs from hikari.models import guilds from hikari.models import messages from hikari.models import users -_T = typing.TypeVar("_T") +IteratorT = typing.TypeVar("IteratorT") -class LazyIterator(typing.Generic[_T], abc.ABC): +class LazyIterator(typing.Generic[IteratorT], abc.ABC): """A set of results that are fetched asynchronously from the API as needed. This is a `typing.AsyncIterable` and `typing.AsyncIterator` with several @@ -90,7 +90,7 @@ class LazyIterator(typing.Generic[_T], abc.ABC): __slots__ = () - def enumerate(self, *, start: int = 0) -> LazyIterator[typing.Tuple[int, _T]]: + def enumerate(self, *, start: int = 0) -> LazyIterator[typing.Tuple[int, IteratorT]]: """Enumerate the paginated results lazily. This behaves as an asyncio-friendly version of `builtins.enumerate` @@ -134,7 +134,7 @@ def enumerate(self, *, start: int = 0) -> LazyIterator[typing.Tuple[int, _T]]: """ return _EnumeratedLazyIterator(self, start=start) - def limit(self, limit: int) -> LazyIterator[_T]: + def limit(self, limit: int) -> LazyIterator[IteratorT]: """Limit the number of items you receive from this async iterator. Parameters @@ -159,45 +159,45 @@ def limit(self, limit: int) -> LazyIterator[_T]: def _complete(self) -> typing.NoReturn: raise StopAsyncIteration("No more items exist in this paginator. It has been exhausted.") from None - def __aiter__(self) -> LazyIterator[_T]: + def __aiter__(self) -> LazyIterator[IteratorT]: # We are our own iterator. return self - async def _fetch_all(self) -> typing.Sequence[_T]: + async def _fetch_all(self) -> typing.Sequence[IteratorT]: return [item async for item in self] - def __await__(self) -> typing.Generator[typing.Sequence[_T], None, None]: + def __await__(self) -> typing.Generator[typing.Sequence[IteratorT], None, None]: yield from self._fetch_all().__await__() @abc.abstractmethod - async def __anext__(self) -> _T: + async def __anext__(self) -> IteratorT: ... -class _EnumeratedLazyIterator(typing.Generic[_T], LazyIterator[typing.Tuple[int, _T]]): +class _EnumeratedLazyIterator(typing.Generic[IteratorT], LazyIterator[typing.Tuple[int, IteratorT]]): __slots__ = ("_i", "_paginator") - def __init__(self, paginator: LazyIterator[_T], *, start: int) -> None: + def __init__(self, paginator: LazyIterator[IteratorT], *, start: int) -> None: self._i = start self._paginator = paginator - async def __anext__(self) -> typing.Tuple[int, _T]: + async def __anext__(self) -> typing.Tuple[int, IteratorT]: pair = self._i, await self._paginator.__anext__() self._i += 1 return pair -class _LimitedLazyIterator(typing.Generic[_T], LazyIterator[_T]): +class _LimitedLazyIterator(typing.Generic[IteratorT], LazyIterator[IteratorT]): __slots__ = ("_paginator", "_count", "_limit") - def __init__(self, paginator: LazyIterator[_T], limit: int) -> None: + def __init__(self, paginator: LazyIterator[IteratorT], limit: int) -> None: if limit <= 0: raise ValueError("limit must be positive and non-zero") self._paginator = paginator self._count = 0 self._limit = limit - async def __anext__(self) -> _T: + async def __anext__(self) -> IteratorT: if self._count >= self._limit: self._complete() @@ -206,18 +206,18 @@ async def __anext__(self) -> _T: return next_item -class _BufferedLazyIterator(typing.Generic[_T], LazyIterator[_T]): +class _BufferedLazyIterator(typing.Generic[IteratorT], LazyIterator[IteratorT]): __slots__ = ("_buffer",) def __init__(self) -> None: - empty_genexp = typing.cast(typing.Generator[_T, None, None], (_ for _ in ())) - self._buffer: typing.Optional[typing.Generator[_T, None, None]] = empty_genexp + empty_genexp = typing.cast(typing.Generator[IteratorT, None, None], (_ for _ in ())) + self._buffer: typing.Optional[typing.Generator[IteratorT, None, None]] = empty_genexp @abc.abstractmethod - async def _next_chunk(self) -> typing.Optional[typing.Generator[_T, None, None]]: + async def _next_chunk(self) -> typing.Optional[typing.Generator[IteratorT, None, None]]: ... - async def __anext__(self) -> _T: + async def __anext__(self) -> IteratorT: # This sneaky snippet of code lets us use generators rather than lists. # This is important, as we can use this to make generators that # deserialize loads of items lazy. If we only want 10 messages of @@ -243,7 +243,7 @@ class MessageIterator(_BufferedLazyIterator["messages.Message"]): def __init__( self, - app: app_.IRESTApp, + app: rest.IRESTApp, request_call: typing.Callable[ ..., typing.Coroutine[None, None, typing.Union[None, data_binding.JSONObject, data_binding.JSONArray]] ], @@ -285,7 +285,7 @@ class ReactorIterator(_BufferedLazyIterator["users.User"]): def __init__( self, - app: app_.IRESTApp, + app: rest.IRESTApp, request_call: typing.Callable[ ..., typing.Coroutine[None, None, typing.Union[None, data_binding.JSONObject, data_binding.JSONArray]] ], @@ -324,7 +324,7 @@ class OwnGuildIterator(_BufferedLazyIterator["applications.OwnGuild"]): def __init__( self, - app: app_.IRESTApp, + app: rest.IRESTApp, request_call: typing.Callable[ ..., typing.Coroutine[None, None, typing.Union[None, data_binding.JSONObject, data_binding.JSONArray]] ], @@ -363,7 +363,7 @@ class MemberIterator(_BufferedLazyIterator["guilds.Member"]): def __init__( self, - app: app_.IRESTApp, + app: rest.IRESTApp, request_call: typing.Callable[ ..., typing.Coroutine[None, None, typing.Union[None, data_binding.JSONObject, data_binding.JSONArray]] ], @@ -400,7 +400,7 @@ class AuditLogIterator(LazyIterator["audit_logs.AuditLog"]): def __init__( self, - app: app_.IRESTApp, + app: rest.IRESTApp, request_call: typing.Callable[ ..., typing.Coroutine[None, None, typing.Union[None, data_binding.JSONObject, data_binding.JSONArray]] ], diff --git a/hikari/net/rate_limits.py b/hikari/net/rate_limits.py index 976ce0cb1a..c3eebf7e99 100644 --- a/hikari/net/rate_limits.py +++ b/hikari/net/rate_limits.py @@ -153,7 +153,7 @@ def is_empty(self) -> bool: class ManualRateLimiter(BurstRateLimiter): - """Rate limit handler for the global RESTSession rate limit. + """Rate limit handler for the global REST rate limit. This is a non-preemptive rate limiting algorithm that will always return completed futures until `ManualRateLimiter.throttle` is invoked. Once this @@ -166,8 +166,8 @@ class ManualRateLimiter(BurstRateLimiter): Triggering a throttle when it is already set will cancel the current throttle task that is sleeping and replace it. - This is used to enforce the global RESTSession rate limit that will occur - "randomly" during RESTSession API interaction. + This is used to enforce the global REST rate limit that will occur + "randomly" during REST API interaction. Expect random occurrences. """ diff --git a/hikari/net/rest.py b/hikari/net/rest.py index 38c28ce8f8..f6213c01d8 100644 --- a/hikari/net/rest.py +++ b/hikari/net/rest.py @@ -51,7 +51,7 @@ from hikari.utilities import undefined if typing.TYPE_CHECKING: - from hikari.api import app as app_ + from hikari.api import rest from hikari.models import applications from hikari.models import audit_logs @@ -77,7 +77,7 @@ class REST(http_client.HTTPClient, component.IComponent): # pylint:disable=too- Parameters ---------- - app : hikari.api.app.IRESTApp + app : hikari.api.rest.IRESTApp The REST application containing all other application components that Hikari uses. config : hikari.net.http_settings.HTTPSettings @@ -112,7 +112,7 @@ class _RetryRequest(RuntimeError): def __init__( self, *, - app: app_.IRESTApp, + app: rest.IRESTApp, config: http_settings.HTTPSettings, debug: bool = False, token: typing.Union[undefined.UndefinedType, str] = undefined.UNDEFINED, @@ -155,7 +155,7 @@ def __init__( self._rest_url = rest_url.format(self) @property - def app(self) -> app_.IRESTApp: + def app(self) -> rest.IRESTApp: return self._app async def _request( @@ -1110,7 +1110,7 @@ async def create_message( try: for i, attachment in enumerate(final_attachments): - stream = await stack.enter_async_context(attachment.stream(executor=self._app.thread_pool_executor)) + stream = await stack.enter_async_context(attachment.stream(executor=self._app.executor)) form.add_field( f"file{i}", stream, filename=stream.filename, content_type=self._APPLICATION_OCTET_STREAM ) @@ -1394,7 +1394,7 @@ async def create_webhook( body = data_binding.JSONObjectBuilder() body.put("name", name) if not avatar is undefined.UNDEFINED: - async with avatar.stream(executor=self._app.thread_pool_executor) as stream: + async with avatar.stream(executor=self._app.executor) as stream: body.put("avatar", await stream.data_uri()) raw_response = await self._request(route, body=body, reason=reason) @@ -1455,7 +1455,7 @@ async def edit_webhook( if avatar is None: body.put("avatar", None) elif not avatar is undefined.UNDEFINED: - async with avatar.stream(executor=self._app.thread_pool_executor) as stream: + async with avatar.stream(executor=self._app.executor) as stream: body.put("avatar", await stream.data_uri()) raw_response = await self._request(route, body=body, reason=reason) @@ -1523,7 +1523,7 @@ async def execute_webhook( try: for i, attachment in enumerate(attachments): - stream = await stack.enter_async_context(attachment.stream(self._app.thread_pool_executor)) + stream = await stack.enter_async_context(attachment.stream(self._app.executor)) form.add_field( f"file{i}", stream, filename=stream.filename, content_type=self._APPLICATION_OCTET_STREAM ) @@ -1579,7 +1579,7 @@ async def edit_my_user( body.put("username", username) if isinstance(avatar, files.Resource): - async with avatar.stream(executor=self._app.thread_pool_executor) as stream: + async with avatar.stream(executor=self._app.executor) as stream: body.put("avatar", await stream.data_uri()) else: body.put("avatar", avatar) @@ -1726,7 +1726,7 @@ async def create_emoji( body = data_binding.JSONObjectBuilder() body.put("name", name) if not image is undefined.UNDEFINED: - async with image.stream(executor=self._app.thread_pool_executor) as stream: + async with image.stream(executor=self._app.executor) as stream: body.put("image", await stream.data_uri()) body.put_snowflake_array("roles", roles) @@ -1835,19 +1835,19 @@ async def edit_guild( if icon is None: body.put("icon", None) elif not icon is undefined.UNDEFINED: - async with icon.stream(executor=self._app.thread_pool_executor) as stream: + async with icon.stream(executor=self._app.executor) as stream: body.put("icon", await stream.data_uri()) if splash is None: body.put("splash", None) elif not splash is undefined.UNDEFINED: - async with splash.stream(executor=self._app.thread_pool_executor) as stream: + async with splash.stream(executor=self._app.executor) as stream: body.put("splash", await stream.data_uri()) if banner is None: body.put("banner", None) elif not banner is undefined.UNDEFINED: - async with banner.stream(executor=self._app.thread_pool_executor) as stream: + async with banner.stream(executor=self._app.executor) as stream: body.put("banner", await stream.data_uri()) raw_response = await self._request(route, body=body, reason=reason) diff --git a/hikari/net/rest_utils.py b/hikari/net/rest_utils.py index 06d23f912b..51d0706290 100644 --- a/hikari/net/rest_utils.py +++ b/hikari/net/rest_utils.py @@ -40,7 +40,7 @@ if typing.TYPE_CHECKING: import types - from hikari.api import app as app_ + from hikari.api import rest from hikari.models import bases from hikari.models import channels from hikari.models import colors @@ -168,7 +168,7 @@ class GuildBuilder: """ # Required arguments. - _app: app_.IRESTApp + _app: rest.IRESTApp _name: str # Optional args that we kept hidden. @@ -253,7 +253,7 @@ async def create(self) -> guilds.Guild: payload.put("explicit_content_filter", self.explicit_content_filter_level) if self.icon is not undefined.UNDEFINED: - async with self.icon.stream(self._app.thread_pool_executor) as stream: + async with self.icon.stream(self._app.executor) as stream: data_uri = await stream.data_uri() payload.put("icon", data_uri) diff --git a/hikari/utilities/files.py b/hikari/utilities/files.py index 51c570908a..dbdae058ff 100644 --- a/hikari/utilities/files.py +++ b/hikari/utilities/files.py @@ -277,10 +277,8 @@ async def __aiter__(self) -> typing.AsyncGenerator[typing.Any, bytes]: class FileReader(AsyncReader): """Asynchronous file reader that reads a resource from local storage.""" - executor: typing.Optional[concurrent.futures.ThreadPoolExecutor] - """The associated `concurrent.futures.ThreadPoolExecutor` to use for - blocking IO. - """ + executor: typing.Optional[concurrent.futures.Executor] + """The associated `concurrent.futures.Executor` to use for blocking IO.""" path: typing.Union[str, pathlib.Path] """The path to the resource to read.""" @@ -376,7 +374,7 @@ def filename(self) -> str: @abc.abstractmethod @contextlib.asynccontextmanager @typing.no_type_check - async def stream(self, *, executor: typing.Optional[concurrent.futures.ThreadPoolExecutor]) -> AsyncReader: + async def stream(self, *, executor: typing.Optional[concurrent.futures.Executor]) -> AsyncReader: """Return an async iterable of bytes to stream.""" def __str__(self) -> str: @@ -460,13 +458,13 @@ def filename(self) -> str: @contextlib.asynccontextmanager @typing.no_type_check - async def stream(self, *, executor: typing.Optional[concurrent.futures.ThreadPoolExecutor] = None) -> ByteReader: + async def stream(self, *, executor: typing.Optional[concurrent.futures.Executor] = None) -> ByteReader: """Start streaming the content in chunks. Parameters ---------- - executor : concurrent.futures.ThreadPoolExecutor or None - Not used. Provided only to match the underlying interface.executor + executor : concurrent.futures.Executor or None + Not used. Provided only to match the underlying interface. Returns ------- @@ -498,7 +496,7 @@ class WebResource(Resource, abc.ABC): @contextlib.asynccontextmanager @typing.no_type_check - async def stream(self, *, executor: typing.Optional[concurrent.futures.ThreadPoolExecutor] = None) -> WebReader: + async def stream(self, *, executor: typing.Optional[concurrent.futures.Executor] = None) -> WebReader: """Start streaming the content into memory by downloading it. You can use this to fetch the entire resource, parts of the resource, @@ -506,7 +504,7 @@ async def stream(self, *, executor: typing.Optional[concurrent.futures.ThreadPoo Parameters ---------- - executor : concurrent.futures.ThreadPoolExecutor or None + executor : concurrent.futures.Executor or None Not used. Provided only to match the underlying interface. Examples @@ -656,15 +654,15 @@ def filename(self) -> str: @contextlib.asynccontextmanager @typing.no_type_check - async def stream(self, *, executor: typing.Optional[concurrent.futures.ThreadPoolExecutor] = None) -> FileReader: + async def stream(self, *, executor: typing.Optional[concurrent.futures.Executor] = None) -> FileReader: """Start streaming the resource using a thread pool executor. Parameters ---------- - executor : typing.Optional[concurrent.futures.ThreadPoolExecutor] - The thread pool to run the blocking read operations in. If - `None`, the default executor for the running event loop will be - used instead. + executor : typing.Optional[concurrent.futures.Executor] + The executor to run the blocking read operations in. If `None`, + the default executor for the running event loop will be used + instead. Returns ------- diff --git a/tests/hikari/impl/test_entity_factory.py b/tests/hikari/impl/test_entity_factory.py index f31bda4738..4a6bb0fd9f 100644 --- a/tests/hikari/impl/test_entity_factory.py +++ b/tests/hikari/impl/test_entity_factory.py @@ -76,8 +76,8 @@ def mock_app(self) -> app_.IRESTApp: return mock.MagicMock(app_.IRESTApp) @pytest.fixture() - def entity_factory_impl(self, mock_app) -> entity_factory.EntityFactoryImpl: - return entity_factory.EntityFactoryImpl(app=mock_app) + def entity_factory_impl(self, mock_app) -> entity_factory.EntityFactoryComponentImpl: + return entity_factory.EntityFactoryComponentImpl(app=mock_app) def test_app(self, entity_factory_impl, mock_app): assert entity_factory_impl.app is mock_app @@ -2878,7 +2878,7 @@ def test_deserialize_message_update_event_with_full_payload( assert message_update.message.flags == message_models.MessageFlag.IS_CROSSPOST assert message_update.message.nonce == "171000788183678976" - assert isinstance(message_update.message, message_events.UpdateMessage) + assert isinstance(message_update.message, message_events.UpdatedMessage) assert isinstance(message_update, message_events.MessageUpdateEvent) def test_deserialize_message_update_event_with_partial_payload(self, entity_factory_impl): From edf8fb46e5358c05986d0581d0df9bc0436cf113 Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Sat, 13 Jun 2020 17:45:51 +0100 Subject: [PATCH 508/922] Fixed static typing issues globally. I need a drink. --- hikari/__main__.py | 4 ++-- hikari/events/message.py | 18 ++++++++++-------- hikari/impl/entity_factory.py | 2 +- tests/hikari/impl/test_entity_factory.py | 2 +- 4 files changed, 14 insertions(+), 12 deletions(-) diff --git a/hikari/__main__.py b/hikari/__main__.py index ba6330d678..0c14153e02 100644 --- a/hikari/__main__.py +++ b/hikari/__main__.py @@ -30,8 +30,8 @@ from hikari import _about -# noinspection PyTypeChecker -path: typing.Final[str] = os.path.abspath(os.path.dirname(inspect.getsourcefile(_about))) +sourcefile = typing.cast(str, inspect.getsourcefile(_about)) +path: typing.Final[str] = os.path.abspath(os.path.dirname(sourcefile)) version: typing.Final[str] = _about.__version__ py_impl: typing.Final[str] = platform.python_implementation() py_ver: typing.Final[str] = platform.python_version() diff --git a/hikari/events/message.py b/hikari/events/message.py index 2aa65ece42..793501ffb7 100644 --- a/hikari/events/message.py +++ b/hikari/events/message.py @@ -22,7 +22,7 @@ __all__: typing.List[str] = [ "MessageCreateEvent", - "UpdatedMessage", + "UpdatedMessageFields", "MessageUpdateEvent", "MessageDeleteEvent", "MessageDeleteBulkEvent", @@ -61,9 +61,14 @@ class MessageCreateEvent(base_events.Event): message: messages.Message = attr.ib(repr=True) -class UpdatedMessage: +@attr.s(slots=True, init=False, repr=True, eq=False) +class UpdatedMessageFields(base_models.Entity, base_models.Unique): """An arbitrarily partial version of `hikari.models.messages.Message`. + This contains arbitrary fields that may be updated in a + `MessageUpdateEvent`, but for all other purposes should be treated as + being optionally specified. + !!! warn All fields on this model except `channel` and `id` may be set to `hikari.utilities.undefined.UndefinedType` (a singleton) if we have not @@ -74,16 +79,13 @@ class UpdatedMessage: channel_id: snowflake.Snowflake = attr.ib(repr=True) """The ID of the channel that the message was sent in.""" - # FIXME: This differs from Message, where the field could only be None or a Snowflake - # so this breaks stuff using inheritance. + # So this breaks stuff using inheritance. guild_id: typing.Union[snowflake.Snowflake, undefined.UndefinedType] = attr.ib(repr=True) """The ID of the guild that the message was sent in.""" author: typing.Union[users.User, undefined.UndefinedType] = attr.ib(repr=True) """The author of this message.""" - # TODO: can we merge member and author together? - # We could override deserialize to to this and then reorganise the payload, perhaps? member: typing.Union[guilds.Member, undefined.UndefinedType] = attr.ib(repr=False) """The member properties for the message's author.""" @@ -96,7 +98,7 @@ class UpdatedMessage: edited_timestamp: typing.Union[datetime.datetime, undefined.UndefinedType, None] = attr.ib(repr=False) """The timestamp that the message was last edited at. - Will be `None` if the message wasn't ever edited. + Will be `None` if the message wasn't ever edited. """ is_tts: typing.Union[bool, undefined.UndefinedType] = attr.ib(repr=False) @@ -163,7 +165,7 @@ class MessageUpdateEvent(base_events.Event, base_models.Unique): (a singleton) to indicate that it has not been changed. """ - message: UpdatedMessage = attr.ib(repr=True) + message: UpdatedMessageFields = attr.ib(repr=True) """The partial message object with all updated fields.""" diff --git a/hikari/impl/entity_factory.py b/hikari/impl/entity_factory.py index e56e287304..3116b115b4 100644 --- a/hikari/impl/entity_factory.py +++ b/hikari/impl/entity_factory.py @@ -1550,7 +1550,7 @@ def deserialize_message_create_event(self, payload: data_binding.JSONObject) -> def deserialize_message_update_event(self, payload: data_binding.JSONObject) -> message_events.MessageUpdateEvent: message_update = message_events.MessageUpdateEvent() - updated_message = message_events.UpdatedMessage(self._app) + updated_message = message_events.UpdatedMessageFields(self._app) updated_message.id = snowflake.Snowflake(payload["id"]) updated_message.channel_id = snowflake.Snowflake(payload["channel_id"]) updated_message.guild_id = ( diff --git a/tests/hikari/impl/test_entity_factory.py b/tests/hikari/impl/test_entity_factory.py index 4a6bb0fd9f..514629f36f 100644 --- a/tests/hikari/impl/test_entity_factory.py +++ b/tests/hikari/impl/test_entity_factory.py @@ -2878,7 +2878,7 @@ def test_deserialize_message_update_event_with_full_payload( assert message_update.message.flags == message_models.MessageFlag.IS_CROSSPOST assert message_update.message.nonce == "171000788183678976" - assert isinstance(message_update.message, message_events.UpdatedMessage) + assert isinstance(message_update.message, message_events.UpdatedMessageFields) assert isinstance(message_update, message_events.MessageUpdateEvent) def test_deserialize_message_update_event_with_partial_payload(self, entity_factory_impl): From fbe3d26fc61b4acfb63b7ac4d54485404cb67481 Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Sat, 13 Jun 2020 17:49:10 +0100 Subject: [PATCH 509/922] Fixed pydocstyle issues and reorganised imports. --- hikari/impl/bot.py | 3 +-- hikari/models/webhooks.py | 1 - hikari/net/rest.py | 2 +- hikari/utilities/reflect.py | 10 +--------- 4 files changed, 3 insertions(+), 13 deletions(-) diff --git a/hikari/impl/bot.py b/hikari/impl/bot.py index 206883d50b..2c7c168a8a 100644 --- a/hikari/impl/bot.py +++ b/hikari/impl/bot.py @@ -22,6 +22,7 @@ __all__: typing.List[str] = ["BotAppImpl"] +import asyncio import concurrent.futures import inspect import logging @@ -30,8 +31,6 @@ import sys import typing -import asyncio - from hikari.api import bot from hikari.impl import cache as cache_impl from hikari.impl import entity_factory as entity_factory_impl diff --git a/hikari/models/webhooks.py b/hikari/models/webhooks.py index 2f1a4cf330..9b4d1e2bcf 100644 --- a/hikari/models/webhooks.py +++ b/hikari/models/webhooks.py @@ -329,7 +329,6 @@ async def fetch_self( ValueError If `use_token` is passed as `True` when `Webhook.token` is `None`. """ - if use_token and not self.token: raise ValueError("This webhook's token is unknown, so cannot be used.") diff --git a/hikari/net/rest.py b/hikari/net/rest.py index f6213c01d8..645493b613 100644 --- a/hikari/net/rest.py +++ b/hikari/net/rest.py @@ -33,8 +33,8 @@ from hikari import errors from hikari.api import component -from hikari.models import emojis from hikari.models import embeds as embeds_ +from hikari.models import emojis from hikari.net import buckets from hikari.net import http_client from hikari.net import http_settings diff --git a/hikari/utilities/reflect.py b/hikari/utilities/reflect.py index 992885b918..3c2fc166d5 100644 --- a/hikari/utilities/reflect.py +++ b/hikari/utilities/reflect.py @@ -20,7 +20,7 @@ from __future__ import annotations -__all__: typing.List[str] = ["resolve_signature", "EMPTY", "get_logger", "steal_docstring_from"] +__all__: typing.List[str] = ["resolve_signature", "EMPTY", "get_logger"] import inspect import logging @@ -93,11 +93,3 @@ def get_logger(obj: typing.Union[typing.Type[typing.Any], typing.Any], *addition T = typing.TypeVar("T") - - -def steal_docstring_from(source: typing.Any) -> typing.Callable[[T], T]: - def decorator(obj: T) -> T: - setattr(obj, "__doc__", getattr(source, "__doc__", None)) - return obj - - return decorator From c699ab0be7f5c5cad38ea849e42a2a5762627bff Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Sat, 13 Jun 2020 17:55:05 +0100 Subject: [PATCH 510/922] Made MyPy passing a mandatory constraint. --- ci/mypy.nox.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/ci/mypy.nox.py b/ci/mypy.nox.py index df6e659417..ec124c0406 100644 --- a/ci/mypy.nox.py +++ b/ci/mypy.nox.py @@ -23,8 +23,7 @@ @nox.session(reuse_venv=True, default=True) def mypy(session: nox.Session) -> None: - # LXML is used for cobertura reporting. - session.install("-r", "requirements.txt", "mypy==0.780", "lxml") + session.install("-r", "requirements.txt", "mypy==0.780") session.run( "mypy", "-p", @@ -33,5 +32,4 @@ def mypy(session: nox.Session) -> None: config.MYPY_INI, "--junit-xml", config.MYPY_JUNIT_OUTPUT_PATH, - success_codes=range(0, 256), ) From 16ba7daef0107ba7d256071150b4a41462b10e85 Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Sun, 14 Jun 2020 10:07:49 +0100 Subject: [PATCH 511/922] Fixes https://gitlab.com/nekokatt/hikari/-/merge_requests/648#note_360618236 Fixes https://gitlab.com/nekokatt/hikari/-/merge_requests/648#note_360618336 Fixes https://gitlab.com/nekokatt/hikari/-/merge_requests/648#note_360618496 Fixes https://gitlab.com/nekokatt/hikari/-/merge_requests/648#note_360618634 Fixes https://gitlab.com/nekokatt/hikari/-/merge_requests/648#note_360618850 Refactored usage of `__all__` to make it final, moved CDN url to strings. Fixed failing tests, reworked Embed constructors to hide fields we don't want set. Fixed invite deserialization problems. Fixes https://gitlab.com/nekokatt/hikari/-/merge_requests/648#note_360619268 Fixes #https://gitlab.com/nekokatt/hikari/-/merge_requests/648#note_360619725 Fixes https://gitlab.com/nekokatt/hikari/-/merge_requests/648#note_360621991 Fixes https://gitlab.com/nekokatt/hikari/-/merge_requests/648#note_360622740 Fixes https://gitlab.com/nekokatt/hikari/-/merge_requests/648#note_360623103 Fixes https://gitlab.com/nekokatt/hikari/-/merge_requests/648#note_360623644 Fixes https://gitlab.com/nekokatt/hikari/-/merge_requests/648#note_360623814 Fixes https://gitlab.com/nekokatt/hikari/-/merge_requests/648#note_360624164 Moved spaces around in event_manager_core.py Removed fixme from role_id check. Fixes https://gitlab.com/nekokatt/hikari/-/merge_requests/648#note_360625820 Fixed https://gitlab.com/nekokatt/hikari/-/merge_requests/648#note_360626077 --- ci/mypy.nox.py | 8 +- hikari/__init__.py | 2 +- hikari/__main__.py | 2 +- hikari/api/__init__.py | 2 +- hikari/api/bot.py | 2 +- hikari/api/cache.py | 2 +- hikari/api/component.py | 2 +- hikari/api/entity_factory.py | 4 +- hikari/api/event_consumer.py | 2 +- hikari/api/event_dispatcher.py | 2 +- hikari/api/gateway_zookeeper.py | 2 +- hikari/api/rest.py | 2 +- hikari/errors.py | 2 +- hikari/events/__init__.py | 2 +- hikari/events/base.py | 2 +- hikari/events/channel.py | 2 +- hikari/events/guild.py | 2 +- hikari/events/message.py | 7 +- hikari/events/other.py | 2 +- hikari/events/voice.py | 2 +- hikari/impl/__init__.py | 2 +- hikari/impl/bot.py | 4 +- hikari/impl/cache.py | 2 +- hikari/impl/entity_factory.py | 138 ++++++++-------- hikari/impl/event_manager.py | 2 +- hikari/impl/event_manager_core.py | 4 +- hikari/impl/gateway_zookeeper.py | 4 +- hikari/impl/rest.py | 2 +- hikari/models/__init__.py | 2 +- hikari/models/applications.py | 2 +- hikari/models/audit_logs.py | 2 +- hikari/models/bases.py | 5 +- hikari/models/channels.py | 2 +- hikari/models/colors.py | 2 +- hikari/models/colours.py | 2 +- hikari/models/embeds.py | 98 +++++++----- hikari/models/emojis.py | 2 +- hikari/models/gateway.py | 2 +- hikari/models/guilds.py | 77 ++++----- hikari/models/intents.py | 2 +- hikari/models/invites.py | 6 +- hikari/models/messages.py | 2 +- hikari/models/permissions.py | 2 +- hikari/models/presences.py | 2 +- hikari/models/users.py | 23 +-- hikari/models/voices.py | 2 +- hikari/models/webhooks.py | 2 +- hikari/net/__init__.py | 2 +- hikari/net/buckets.py | 2 +- hikari/net/gateway.py | 10 +- hikari/net/http_client.py | 2 +- hikari/net/http_settings.py | 2 +- hikari/net/iterators.py | 5 +- hikari/net/rate_limits.py | 2 +- hikari/net/rest.py | 42 ++--- hikari/net/rest_utils.py | 2 +- hikari/net/routes.py | 2 +- hikari/net/strings.py | 5 +- hikari/net/tracing.py | 4 +- hikari/utilities/__init__.py | 2 +- hikari/utilities/aio.py | 2 +- hikari/utilities/cdn.py | 12 +- hikari/utilities/data_binding.py | 6 +- hikari/utilities/date.py | 2 +- hikari/utilities/files.py | 36 +++-- hikari/utilities/reflect.py | 2 +- hikari/utilities/snowflake.py | 2 +- hikari/utilities/undefined.py | 2 +- mypy.ini | 2 +- tests/__init__.py | 18 --- tests/hikari/impl/__init__.py | 18 --- tests/hikari/impl/test_entity_factory.py | 193 ++++++++++++++--------- tests/hikari/models/.gitkeep | 0 tests/hikari/models/__init__.py | 18 --- tests/hikari/net/__init__.py | 18 --- tests/hikari/net/test_http_client.py | 18 +-- tests/hikari/net/test_user_agents.py | 39 ----- tests/hikari/utilities/__init__.py | 18 --- 78 files changed, 434 insertions(+), 502 deletions(-) delete mode 100644 tests/__init__.py delete mode 100644 tests/hikari/impl/__init__.py create mode 100644 tests/hikari/models/.gitkeep delete mode 100644 tests/hikari/models/__init__.py delete mode 100644 tests/hikari/net/__init__.py delete mode 100644 tests/hikari/net/test_user_agents.py delete mode 100644 tests/hikari/utilities/__init__.py diff --git a/ci/mypy.nox.py b/ci/mypy.nox.py index ec124c0406..7fc092f74c 100644 --- a/ci/mypy.nox.py +++ b/ci/mypy.nox.py @@ -25,11 +25,5 @@ def mypy(session: nox.Session) -> None: session.install("-r", "requirements.txt", "mypy==0.780") session.run( - "mypy", - "-p", - config.MAIN_PACKAGE, - "--config", - config.MYPY_INI, - "--junit-xml", - config.MYPY_JUNIT_OUTPUT_PATH, + "mypy", "-p", config.MAIN_PACKAGE, "--config", config.MYPY_INI, "--junit-xml", config.MYPY_JUNIT_OUTPUT_PATH, ) diff --git a/hikari/__init__.py b/hikari/__init__.py index 8ddbcf81cc..fd16e38ada 100644 --- a/hikari/__init__.py +++ b/hikari/__init__.py @@ -20,7 +20,7 @@ from __future__ import annotations -__all__: typing.List[str] = ["RESTClient", "Bot"] +__all__: typing.Final[typing.List[str]] = ["RESTClient", "Bot"] # noinspection PyUnresolvedReferences import typing diff --git a/hikari/__main__.py b/hikari/__main__.py index 0c14153e02..0b1b2a053a 100644 --- a/hikari/__main__.py +++ b/hikari/__main__.py @@ -19,7 +19,7 @@ """Provides a command-line entry point that shows the library version and then exits.""" from __future__ import annotations -__all__: typing.List[str] = [] +__all__: typing.Final[typing.List[str]] = [] import inspect import os diff --git a/hikari/api/__init__.py b/hikari/api/__init__.py index ad22681850..1445a927c3 100644 --- a/hikari/api/__init__.py +++ b/hikari/api/__init__.py @@ -24,7 +24,7 @@ """ from __future__ import annotations -__all__: typing.List[str] = [] +__all__: typing.Final[typing.List[str]] = [] # noinspection PyUnresolvedReferences import typing diff --git a/hikari/api/bot.py b/hikari/api/bot.py index 2c5b524c28..6de88a020f 100644 --- a/hikari/api/bot.py +++ b/hikari/api/bot.py @@ -20,7 +20,7 @@ from __future__ import annotations -__all__: typing.List[str] = ["IBotApp"] +__all__: typing.Final[typing.List[str]] = ["IBotApp"] import abc import typing diff --git a/hikari/api/cache.py b/hikari/api/cache.py index 61235f9e64..9a1fea6c32 100644 --- a/hikari/api/cache.py +++ b/hikari/api/cache.py @@ -19,7 +19,7 @@ """Core interface for a cache implementation.""" from __future__ import annotations -__all__: typing.List[str] = ["ICacheComponent"] +__all__: typing.Final[typing.List[str]] = ["ICacheComponent"] import abc diff --git a/hikari/api/component.py b/hikari/api/component.py index 635391fc58..9635abd367 100644 --- a/hikari/api/component.py +++ b/hikari/api/component.py @@ -20,7 +20,7 @@ from __future__ import annotations -__all__: typing.List[str] = ["IComponent"] +__all__: typing.Final[typing.List[str]] = ["IComponent"] import abc import typing diff --git a/hikari/api/entity_factory.py b/hikari/api/entity_factory.py index f6bd6c725c..919d04de3d 100644 --- a/hikari/api/entity_factory.py +++ b/hikari/api/entity_factory.py @@ -19,7 +19,7 @@ """Core interface for an object that serializes/deserializes API objects.""" from __future__ import annotations -__all__: typing.List[str] = ["IEntityFactoryComponent"] +__all__: typing.Final[typing.List[str]] = ["IEntityFactoryComponent"] import abc import typing @@ -46,9 +46,7 @@ from hikari.models import users as user_models from hikari.models import voices as voice_models from hikari.models import webhooks as webhook_models - from hikari.net import gateway - from hikari.utilities import data_binding from hikari.utilities import files diff --git a/hikari/api/event_consumer.py b/hikari/api/event_consumer.py index 3f3b722483..66b3c385c9 100644 --- a/hikari/api/event_consumer.py +++ b/hikari/api/event_consumer.py @@ -19,7 +19,7 @@ """Core interface for components that consume raw API event payloads.""" from __future__ import annotations -__all__: typing.List[str] = ["IEventConsumerComponent", "IEventConsumerApp"] +__all__: typing.Final[typing.List[str]] = ["IEventConsumerComponent", "IEventConsumerApp"] import abc import typing diff --git a/hikari/api/event_dispatcher.py b/hikari/api/event_dispatcher.py index 89d58718fb..08b4c2351b 100644 --- a/hikari/api/event_dispatcher.py +++ b/hikari/api/event_dispatcher.py @@ -19,7 +19,7 @@ """Core interface for components that dispatch events to the library.""" from __future__ import annotations -__all__: typing.List[str] = ["IEventDispatcherBase", "IEventDispatcherApp", "IEventDispatcherComponent"] +__all__: typing.Final[typing.List[str]] = ["IEventDispatcherBase", "IEventDispatcherApp", "IEventDispatcherComponent"] import abc import asyncio diff --git a/hikari/api/gateway_zookeeper.py b/hikari/api/gateway_zookeeper.py index dab8d855f6..1606f7458a 100644 --- a/hikari/api/gateway_zookeeper.py +++ b/hikari/api/gateway_zookeeper.py @@ -20,7 +20,7 @@ from __future__ import annotations -__all__: typing.List[str] = ["IGatewayZookeeperApp"] +__all__: typing.Final[typing.List[str]] = ["IGatewayZookeeperApp"] import abc import typing diff --git a/hikari/api/rest.py b/hikari/api/rest.py index 84f4ac21a2..d735257247 100644 --- a/hikari/api/rest.py +++ b/hikari/api/rest.py @@ -20,7 +20,7 @@ from __future__ import annotations -__all__: typing.List[str] = ["IRESTApp"] +__all__: typing.Final[typing.List[str]] = ["IRESTApp"] import abc import logging diff --git a/hikari/errors.py b/hikari/errors.py index d7438262c5..4a64677146 100644 --- a/hikari/errors.py +++ b/hikari/errors.py @@ -20,7 +20,7 @@ from __future__ import annotations -__all__: typing.List[str] = [ +__all__: typing.Final[typing.List[str]] = [ "HikariError", "HikariWarning", "NotFound", diff --git a/hikari/events/__init__.py b/hikari/events/__init__.py index cfacff330c..e4044a59c0 100644 --- a/hikari/events/__init__.py +++ b/hikari/events/__init__.py @@ -20,7 +20,7 @@ from __future__ import annotations -__all__: typing.List[str] = [] +__all__: typing.Final[typing.List[str]] = [] # noinspection PyUnresolvedReferences import typing diff --git a/hikari/events/base.py b/hikari/events/base.py index 0d30d698cd..fa633541f1 100644 --- a/hikari/events/base.py +++ b/hikari/events/base.py @@ -20,7 +20,7 @@ from __future__ import annotations -__all__: typing.List[str] = [ +__all__: typing.Final[typing.List[str]] = [ "Event", "get_required_intents_for", "requires_intents", diff --git a/hikari/events/channel.py b/hikari/events/channel.py index efa4ed376b..1c0b0c5449 100644 --- a/hikari/events/channel.py +++ b/hikari/events/channel.py @@ -20,7 +20,7 @@ from __future__ import annotations -__all__: typing.List[str] = [ +__all__: typing.Final[typing.List[str]] = [ "BaseChannelEvent", "ChannelCreateEvent", "ChannelUpdateEvent", diff --git a/hikari/events/guild.py b/hikari/events/guild.py index 6894bc7cff..249c378b55 100644 --- a/hikari/events/guild.py +++ b/hikari/events/guild.py @@ -20,7 +20,7 @@ from __future__ import annotations -__all__: typing.List[str] = [ +__all__: typing.Final[typing.List[str]] = [ "GuildEvent", "GuildCreateEvent", "GuildUpdateEvent", diff --git a/hikari/events/message.py b/hikari/events/message.py index 793501ffb7..42ce9c98e2 100644 --- a/hikari/events/message.py +++ b/hikari/events/message.py @@ -20,7 +20,7 @@ from __future__ import annotations -__all__: typing.List[str] = [ +__all__: typing.Final[typing.List[str]] = [ "MessageCreateEvent", "UpdatedMessageFields", "MessageUpdateEvent", @@ -98,7 +98,8 @@ class UpdatedMessageFields(base_models.Entity, base_models.Unique): edited_timestamp: typing.Union[datetime.datetime, undefined.UndefinedType, None] = attr.ib(repr=False) """The timestamp that the message was last edited at. - Will be `None` if the message wasn't ever edited. + Will be `None` if the message wasn't ever edited, or `undefined` if the + info is not available. """ is_tts: typing.Union[bool, undefined.UndefinedType] = attr.ib(repr=False) @@ -137,7 +138,7 @@ class UpdatedMessageFields(base_models.Entity, base_models.Unique): activity: typing.Union[messages.MessageActivity, undefined.UndefinedType] = attr.ib(repr=False) """The message's activity.""" - application: typing.Optional[applications.Application] = attr.ib(repr=False) + application: typing.Union[applications.Application, undefined.UndefinedType] = attr.ib(repr=False) """The message's application.""" message_reference: typing.Union[messages.MessageCrosspost, undefined.UndefinedType] = attr.ib(repr=False) diff --git a/hikari/events/other.py b/hikari/events/other.py index bb8a35c50f..f60dea57a8 100644 --- a/hikari/events/other.py +++ b/hikari/events/other.py @@ -20,7 +20,7 @@ from __future__ import annotations -__all__: typing.List[str] = [ +__all__: typing.Final[typing.List[str]] = [ "ExceptionEvent", "ConnectedEvent", "DisconnectedEvent", diff --git a/hikari/events/voice.py b/hikari/events/voice.py index 155f056366..7099a2ba00 100644 --- a/hikari/events/voice.py +++ b/hikari/events/voice.py @@ -20,7 +20,7 @@ from __future__ import annotations -__all__: typing.List[str] = ["VoiceStateUpdateEvent", "VoiceServerUpdateEvent"] +__all__: typing.Final[typing.List[str]] = ["VoiceStateUpdateEvent", "VoiceServerUpdateEvent"] import typing diff --git a/hikari/impl/__init__.py b/hikari/impl/__init__.py index 633c2fb1fc..0524a63b4c 100644 --- a/hikari/impl/__init__.py +++ b/hikari/impl/__init__.py @@ -26,7 +26,7 @@ from __future__ import annotations -__all__: typing.List[str] = [] +__all__: typing.Final[typing.List[str]] = [] # noinspection PyUnresolvedReferences import typing diff --git a/hikari/impl/bot.py b/hikari/impl/bot.py index 2c7c168a8a..d34a12a71d 100644 --- a/hikari/impl/bot.py +++ b/hikari/impl/bot.py @@ -20,10 +20,9 @@ from __future__ import annotations -__all__: typing.List[str] = ["BotAppImpl"] +__all__: typing.Final[typing.List[str]] = ["BotAppImpl"] import asyncio -import concurrent.futures import inspect import logging import os @@ -43,6 +42,7 @@ from hikari.utilities import undefined if typing.TYPE_CHECKING: + import concurrent.futures import datetime from hikari.api import cache as cache_ diff --git a/hikari/impl/cache.py b/hikari/impl/cache.py index 0112553e97..b24377d9b1 100644 --- a/hikari/impl/cache.py +++ b/hikari/impl/cache.py @@ -20,7 +20,7 @@ from __future__ import annotations -__all__: typing.List[str] = ["InMemoryCacheComponentImpl"] +__all__: typing.Final[typing.List[str]] = ["InMemoryCacheComponentImpl"] import typing diff --git a/hikari/impl/entity_factory.py b/hikari/impl/entity_factory.py index 3116b115b4..812511b901 100644 --- a/hikari/impl/entity_factory.py +++ b/hikari/impl/entity_factory.py @@ -20,7 +20,7 @@ from __future__ import annotations -__all__: typing.List[str] = ["EntityFactoryComponentImpl"] +__all__: typing.Final[typing.List[str]] = ["EntityFactoryComponentImpl"] import datetime import typing @@ -65,8 +65,8 @@ def _deserialize_day_timedelta(days: typing.Union[str, int]) -> datetime.timedel return datetime.timedelta(days=int(days)) -def _deserialize_max_uses(age: int) -> typing.Union[int, float]: - return age if age > 0 else float("inf") +def _deserialize_max_uses(age: int) -> typing.Optional[int]: + return age if age > 0 else None def _deserialize_max_age(seconds: int) -> typing.Optional[datetime.timedelta]: @@ -79,15 +79,16 @@ class EntityFactoryComponentImpl(entity_factory.IEntityFactoryComponent): This will convert objects to/from JSON compatible representations. """ - DMChannelT = typing.TypeVar("DMChannelT", bound=channel_models.DMChannel) - GuildChannelT = typing.TypeVar("GuildChannelT", bound=channel_models.GuildChannel) - InviteT = typing.TypeVar("InviteT", bound=invite_model.Invite) - PartialChannelT = typing.TypeVar("PartialChannelT", bound=channel_models.PartialChannel) - PartialGuildT = typing.TypeVar("PartialGuildT", bound=guild_models.PartialGuild) - PartialGuildIntegrationT = typing.TypeVar("PartialGuildIntegrationT", bound=guild_models.PartialIntegration) - UserT = typing.TypeVar("UserT", bound=user_models.User) - ReactionEventT = typing.TypeVar("ReactionEventT", bound=message_events.BaseMessageReactionEvent) - GuildBanEventT = typing.TypeVar("GuildBanEventT", bound=guild_events.GuildBanEvent) + if typing.TYPE_CHECKING: + DMChannelT = typing.TypeVar("DMChannelT", bound=channel_models.DMChannel) + GuildChannelT = typing.TypeVar("GuildChannelT", bound=channel_models.GuildChannel) + InviteT = typing.TypeVar("InviteT", bound=invite_model.Invite) + PartialChannelT = typing.TypeVar("PartialChannelT", bound=channel_models.PartialChannel) + PartialGuildT = typing.TypeVar("PartialGuildT", bound=guild_models.PartialGuild) + PartialGuildIntegrationT = typing.TypeVar("PartialGuildIntegrationT", bound=guild_models.PartialIntegration) + UserT = typing.TypeVar("UserT", bound=user_models.User) + ReactionEventT = typing.TypeVar("ReactionEventT", bound=message_events.BaseMessageReactionEvent) + GuildBanEventT = typing.TypeVar("GuildBanEventT", bound=guild_events.GuildBanEvent) def __init__(self, app: rest.IRESTApp) -> None: self._app = app @@ -509,22 +510,22 @@ def deserialize_embed(self, payload: data_binding.JSONObject) -> embed_models.Em ) embed.color = color_models.Color(payload["color"]) if "color" in payload else None - if (footer_payload := payload.get("footer", ...)) is not ...: + if footer_payload := payload.get("footer"): footer = embed_models.EmbedFooter() footer.text = footer_payload["text"] - if (icon_url := footer_payload.get("icon")) is not None: - footer.icon = embed_models.EmbedImage() - footer.icon.resource = files.ensure_resource(icon_url) + + if icon_url := footer_payload.get("icon_url"): + footer.icon = embed_models.EmbedResource(resource=files.ensure_resource(icon_url)) footer.icon.proxy_resource = files.ensure_resource(footer_payload.get("proxy_icon_url")) else: footer.icon = None + embed.footer = footer else: embed.footer = None - if (image_payload := payload.get("image", ...)) is not ...: - image = embed_models.EmbedImage() - image.resource = files.ensure_resource(image_payload.get("url")) + if image_payload := payload.get("image"): + image = embed_models.EmbedImage(resource=files.ensure_resource(image_payload.get("url"))) image.proxy_resource = files.ensure_resource(image_payload.get("proxy_url")) image.height = int(image_payload["height"]) if "height" in image_payload else None image.width = int(image_payload["width"]) if "width" in image_payload else None @@ -532,9 +533,8 @@ def deserialize_embed(self, payload: data_binding.JSONObject) -> embed_models.Em else: embed.image = None - if (thumbnail_payload := payload.get("thumbnail", ...)) is not ...: - thumbnail = embed_models.EmbedImage() - thumbnail.resource = files.ensure_resource(thumbnail_payload.get("url")) + if thumbnail_payload := payload.get("thumbnail"): + thumbnail = embed_models.EmbedImage(resource=files.ensure_resource(thumbnail_payload.get("url"))) thumbnail.proxy_resource = files.ensure_resource(thumbnail_payload.get("proxy_url")) thumbnail.height = int(thumbnail_payload["height"]) if "height" in thumbnail_payload else None thumbnail.width = int(thumbnail_payload["width"]) if "width" in thumbnail_payload else None @@ -542,17 +542,17 @@ def deserialize_embed(self, payload: data_binding.JSONObject) -> embed_models.Em else: embed.thumbnail = None - if (video_payload := payload.get("video", ...)) is not ...: - video = embed_models.EmbedVideo() - video.resource = files.ensure_resource(thumbnail_payload.get("url")) - video.proxy_resource = None - video.height = int(video_payload["height"]) if "height" in video_payload else None - video.width = int(video_payload["width"]) if "width" in video_payload else None + if video_payload := payload.get("video"): + video = embed_models.EmbedVideo( + resource=files.ensure_resource(video_payload.get("url")), + height=int(video_payload["height"]) if "height" in video_payload else None, + width=int(video_payload["width"]) if "width" in video_payload else None, + ) embed.video = video else: embed.video = None - if (provider_payload := payload.get("provider", ...)) is not ...: + if provider_payload := payload.get("provider"): provider = embed_models.EmbedProvider() provider.name = provider_payload.get("name") provider.url = provider_payload.get("url") @@ -560,27 +560,28 @@ def deserialize_embed(self, payload: data_binding.JSONObject) -> embed_models.Em else: embed.provider = None - if (author_payload := payload.get("author", ...)) is not ...: + if author_payload := payload.get("author"): author = embed_models.EmbedAuthor() author.name = author_payload.get("name") author.url = author_payload.get("url") - if (icon_url := author_payload.get("icon")) is not None: - author.icon = embed_models.EmbedImage() - author.icon.resource = files.ensure_resource(icon_url) + + if (icon_url := author_payload.get("icon_url")) is not None: + author.icon = embed_models.EmbedResource(resource=files.ensure_resource(icon_url)) author.icon.proxy_resource = files.ensure_resource(author_payload.get("proxy_icon_url")) else: author.icon = None + embed.author = author else: embed.author = None fields = [] for field_payload in payload.get("fields", ()): - field = embed_models.EmbedField() - field.name = field_payload["name"] - field.value = field_payload["value"] - field.is_inline = field_payload.get("inline", False) - fields.append(field) + fields.append( + embed_models.EmbedField( + name=field_payload["name"], value=field_payload["value"], inline=field_payload.get("inline", False) + ) + ) embed.fields = fields return embed @@ -605,9 +606,9 @@ def serialize_embed( payload["timestamp"] = embed.timestamp.isoformat() if embed.color is not None: - payload["color"] = str(embed.color) + payload["color"] = int(embed.color) - if embed.footer is not None: + if embed.footer: footer_payload: data_binding.JSONObject = {} if embed.footer.text is not None: @@ -615,35 +616,31 @@ def serialize_embed( if embed.footer.icon is not None: if not isinstance(embed.footer.icon.resource, files.WebResource): - uploads.append(embed.footer.icon) + uploads.append(embed.footer.icon.resource) footer_payload["icon_url"] = embed.footer.icon.url payload["footer"] = footer_payload - if embed.image is not None: + if embed.image: image_payload: data_binding.JSONObject = {} - if embed.image is not None: - if not isinstance(embed.image.resource, files.WebResource): - uploads.append(embed.image.resource) - - image_payload["url"] = embed.image.url + if not isinstance(embed.image.resource, files.WebResource): + uploads.append(embed.image.resource) + image_payload["url"] = embed.image.url payload["image"] = image_payload - if embed.thumbnail is not None: + if embed.thumbnail: thumbnail_payload: data_binding.JSONObject = {} - if embed.thumbnail is not None: - if not isinstance(embed.thumbnail.resource, files.WebResource): - uploads.append(embed.thumbnail.resource) - - thumbnail_payload["url"] = embed.thumbnail.url + if not isinstance(embed.thumbnail.resource, files.WebResource): + uploads.append(embed.thumbnail.resource) + thumbnail_payload["url"] = embed.thumbnail.url payload["thumbnail"] = thumbnail_payload - if embed.author is not None: + if embed.author: author_payload: data_binding.JSONObject = {} if embed.author.name is not None: @@ -654,7 +651,7 @@ def serialize_embed( if embed.author.icon is not None: if not isinstance(embed.author.icon.resource, files.WebResource): - uploads.append(embed.author.icon) + uploads.append(embed.author.icon.resource) author_payload["icon_url"] = embed.author.icon.url payload["author"] = author_payload @@ -662,16 +659,11 @@ def serialize_embed( if embed.fields: field_payloads: data_binding.JSONArray = [] for field in embed.fields: - field_payload: data_binding.JSONObject = {} - - if field.name: - field_payload["name"] = field.name - - if field.value: - field_payload["value"] = field.value - - field_payload["inline"] = field.is_inline - field_payloads.append(field_payload) + # Name and value always have to be specified; we can always + # send a default `inline` value also just to keep this simpler. + field_payloads.append( + {"name": field.name, "value": field.value, "inline": field.is_inline,} + ) payload["fields"] = field_payloads return payload, uploads @@ -877,7 +869,7 @@ def deserialize_guild(self, payload: data_binding.JSONObject) -> guild_models.Gu if (perms := payload.get("permissions")) is not None: guild.my_permissions = permission_models.Permission(perms) else: - guild.my_permissions = undefined.UNDEFINED + guild.my_permissions = None guild.region = payload["region"] @@ -935,7 +927,7 @@ def deserialize_guild(self, payload: data_binding.JSONObject) -> guild_models.Gu guild.is_large = payload["large"] if "large" in payload else None guild.member_count = int(payload["member_count"]) if "member_count" in payload else None - if (members := payload.get("members", ...)) is not ...: + if members := payload.get("members"): guild.members = {} for member_payload in members: member = self.deserialize_member(member_payload) @@ -946,16 +938,15 @@ def deserialize_guild(self, payload: data_binding.JSONObject) -> guild_models.Gu # FIXME: should this be an empty dict instead? guild.members = None - if (channels := payload.get("channels", ...)) is not ...: + if channels := payload.get("channels"): guild.channels = {} for channel_payload in channels: - channel = typing.cast("channel_models.GuildChannel", self.deserialize_partial_channel(channel_payload)) + channel = typing.cast("channel_models.GuildChannel", self.deserialize_channel(channel_payload)) guild.channels[channel.id] = channel else: - # FIXME: should this be an empty dict instead? guild.channels = None - if (presences := payload.get("presences", ...)) is not ...: + if presences := payload.get("presences"): guild.presences = {} for presence_payload in presences: presence = self.deserialize_member_presence(presence_payload) @@ -1168,10 +1159,9 @@ def deserialize_member_presence(self, payload: data_binding.JSONObject) -> prese ) guild_member_presence.user = user - if (role_ids := payload.get("roles", ...)) is not ...: + if (role_ids := payload.get("roles")) is not None: guild_member_presence.role_ids = {snowflake.Snowflake(role_id) for role_id in role_ids} else: - # FIXME: should this be an empty set? guild_member_presence.role_ids = None guild_member_presence.guild_id = snowflake.Snowflake(payload["guild_id"]) if "guild_id" in payload else None @@ -1649,7 +1639,7 @@ def deserialize_message_update_event(self, payload: data_binding.JSONObject) -> updated_message.activity = undefined.UNDEFINED updated_message.application = ( - self.deserialize_application(payload["application"]) if "application" in payload else None + self.deserialize_application(payload["application"]) if "application" in payload else undefined.UNDEFINED ) if (crosspost_payload := payload.get("message_reference", ...)) is not ...: diff --git a/hikari/impl/event_manager.py b/hikari/impl/event_manager.py index 3d62933f1a..72e2e30c0f 100644 --- a/hikari/impl/event_manager.py +++ b/hikari/impl/event_manager.py @@ -20,7 +20,7 @@ from __future__ import annotations -__all__: typing.List[str] = ["EventManagerImpl"] +__all__: typing.Final[typing.List[str]] = ["EventManagerImpl"] import typing diff --git a/hikari/impl/event_manager_core.py b/hikari/impl/event_manager_core.py index c837818582..6352590410 100644 --- a/hikari/impl/event_manager_core.py +++ b/hikari/impl/event_manager_core.py @@ -20,7 +20,7 @@ from __future__ import annotations -__all__: typing.List[str] = ["EventManagerCoreComponent"] +__all__: typing.Final[typing.List[str]] = ["EventManagerCoreComponent"] import asyncio import functools @@ -196,8 +196,8 @@ def dispatch(self, event: base.Event) -> asyncio.Future[typing.Any]: tasks: typing.List[typing.Coroutine[None, typing.Any, None]] = [] for cls in mro[: mro.index(base.Event) + 1]: - if cls in self._listeners: + if cls in self._listeners: for callback in self._listeners[cls]: tasks.append(self._invoke_callback(callback, event)) diff --git a/hikari/impl/gateway_zookeeper.py b/hikari/impl/gateway_zookeeper.py index a758d5546a..35239e925b 100644 --- a/hikari/impl/gateway_zookeeper.py +++ b/hikari/impl/gateway_zookeeper.py @@ -20,7 +20,7 @@ from __future__ import annotations -__all__: typing.List[str] = ["AbstractGatewayZookeeper"] +__all__: typing.Final[typing.List[str]] = ["AbstractGatewayZookeeper"] import abc import asyncio @@ -404,5 +404,5 @@ def _map_signal_handlers( ) -> None: valid_interrupts = signal.valid_signals() for interrupt in self._SIGNALS: - if (code := getattr(signal, interrupt, None)) is not None and code in valid_interrupts: + if (code := getattr(signal, interrupt, None)) in valid_interrupts: mapping_function(code, *args) diff --git a/hikari/impl/rest.py b/hikari/impl/rest.py index dc229f7b23..428ef7f626 100644 --- a/hikari/impl/rest.py +++ b/hikari/impl/rest.py @@ -24,7 +24,7 @@ from __future__ import annotations -__all__: typing.List[str] = ["RESTAppImpl"] +__all__: typing.Final[typing.List[str]] = ["RESTAppImpl"] import logging import typing diff --git a/hikari/models/__init__.py b/hikari/models/__init__.py index a8730b310f..66facd58d7 100644 --- a/hikari/models/__init__.py +++ b/hikari/models/__init__.py @@ -20,7 +20,7 @@ from __future__ import annotations -__all__: typing.List[str] = [] +__all__: typing.Final[typing.List[str]] = [] # noinspection PyUnresolvedReferences import typing diff --git a/hikari/models/applications.py b/hikari/models/applications.py index 0c688f643d..ff45883bbf 100644 --- a/hikari/models/applications.py +++ b/hikari/models/applications.py @@ -20,7 +20,7 @@ from __future__ import annotations -__all__: typing.List[str] = [ +__all__: typing.Final[typing.List[str]] = [ "Application", "ConnectionVisibility", "OAuth2Scope", diff --git a/hikari/models/audit_logs.py b/hikari/models/audit_logs.py index e8a5992818..a60e06eb09 100644 --- a/hikari/models/audit_logs.py +++ b/hikari/models/audit_logs.py @@ -20,7 +20,7 @@ from __future__ import annotations -__all__: typing.List[str] = [ +__all__: typing.Final[typing.List[str]] = [ "AuditLog", "AuditLogChange", "AuditLogChangeKey", diff --git a/hikari/models/bases.py b/hikari/models/bases.py index 2843ba39b3..1ae83f1bf6 100644 --- a/hikari/models/bases.py +++ b/hikari/models/bases.py @@ -20,7 +20,7 @@ from __future__ import annotations -__all__: typing.List[str] = ["Entity", "Unique"] +__all__: typing.Final[typing.List[str]] = ["Entity", "Unique"] import abc import typing @@ -30,9 +30,10 @@ from hikari.utilities import snowflake if typing.TYPE_CHECKING: - from hikari.api import rest import datetime + from hikari.api import rest + @attr.s(eq=True, hash=False, init=False, kw_only=True, slots=False) class Entity(abc.ABC): diff --git a/hikari/models/channels.py b/hikari/models/channels.py index 162575635c..e61a1526f7 100644 --- a/hikari/models/channels.py +++ b/hikari/models/channels.py @@ -20,7 +20,7 @@ from __future__ import annotations -__all__: typing.List[str] = [ +__all__: typing.Final[typing.List[str]] = [ "ChannelType", "PermissionOverwrite", "PermissionOverwriteType", diff --git a/hikari/models/colors.py b/hikari/models/colors.py index 0aaf497987..8704ae4452 100644 --- a/hikari/models/colors.py +++ b/hikari/models/colors.py @@ -20,7 +20,7 @@ from __future__ import annotations -__all__: typing.List[str] = ["Color", "ColorLike"] +__all__: typing.Final[typing.List[str]] = ["Color", "ColorLike"] import string import typing diff --git a/hikari/models/colours.py b/hikari/models/colours.py index f1c34bf1a4..8499e57006 100644 --- a/hikari/models/colours.py +++ b/hikari/models/colours.py @@ -20,7 +20,7 @@ from __future__ import annotations -__all__: typing.List[str] = ["Colour", "ColourLike"] +__all__: typing.Final[typing.List[str]] = ["Colour", "ColourLike"] # noinspection PyUnresolvedReferences import typing diff --git a/hikari/models/embeds.py b/hikari/models/embeds.py index 3d7852d32d..1ff64850dd 100644 --- a/hikari/models/embeds.py +++ b/hikari/models/embeds.py @@ -20,7 +20,7 @@ from __future__ import annotations -__all__: typing.List[str] = [ +__all__: typing.Final[typing.List[str]] = [ "Embed", "EmbedResource", "EmbedVideo", @@ -45,7 +45,16 @@ def _maybe_color(value: typing.Optional[colors.ColorLike]) -> typing.Optional[co return colors.Color.of(value) if value is not None else None -@attr.s(slots=True, kw_only=True, init=False) +class _TruthyEmbedComponentMixin: + __slots__ = () + + __attrs_attrs__: typing.ClassVar[typing.Tuple[attr.Attribute, ...]] + + def __bool__(self) -> bool: + return any(getattr(self, attrib.name, None) for attrib in self.__attrs_attrs__) + + +@attr.s(eq=True, slots=True, kw_only=True) class EmbedResource(files.Resource): """A base type for any resource provided in an embed. @@ -56,7 +65,7 @@ class EmbedResource(files.Resource): resource: files.Resource = attr.ib(repr=True) """The resource this object wraps around.""" - proxy_resource: typing.Optional[files.Resource] = attr.ib(default=None, repr=False) + proxy_resource: typing.Optional[files.Resource] = attr.ib(default=None, repr=False, init=False) """The proxied version of the resource, or `None` if not present. !!! note @@ -80,8 +89,8 @@ async def stream(self) -> files.AsyncReader: yield stream -@attr.s(eq=True, hash=False, init=False, kw_only=True, slots=True) -class EmbedFooter: +@attr.s(eq=True, hash=False, kw_only=True, slots=True) +class EmbedFooter(_TruthyEmbedComponentMixin): """Represents an embed footer.""" text: typing.Optional[str] = attr.ib(default=None, repr=True) @@ -91,11 +100,11 @@ class EmbedFooter: """The URL of the footer icon, or `None` if not present.""" -@attr.s(eq=True, hash=False, init=False, kw_only=True, slots=True) -class EmbedImage(EmbedResource): +@attr.s(eq=True, hash=False, kw_only=True, slots=True) +class EmbedImage(EmbedResource, _TruthyEmbedComponentMixin): """Represents an embed image.""" - height: typing.Optional[int] = attr.ib(default=None, repr=False) + height: typing.Optional[int] = attr.ib(default=None, repr=False, init=False) """The height of the image, if present and known, otherwise `None`. !!! note @@ -104,7 +113,7 @@ class EmbedImage(EmbedResource): any received embed attached to a message event. """ - width: typing.Optional[int] = attr.ib(default=None, repr=False) + width: typing.Optional[int] = attr.ib(default=None, repr=False, init=False) """The width of the image, if present and known, otherwise `None`. !!! note @@ -114,14 +123,17 @@ class EmbedImage(EmbedResource): """ -@attr.s(eq=True, hash=False, init=False, kw_only=True, slots=True) -class EmbedVideo(EmbedResource): +@attr.s(eq=True, hash=False, kw_only=True, slots=True) +class EmbedVideo(EmbedResource, _TruthyEmbedComponentMixin): """Represents an embed video. !!! note This object cannot be set by bots or webhooks while sending an embed and will be ignored during serialization. Expect this to be populated on any received embed attached to a message event with a video attached. + + **Therefore, you should never need to initialize an instance of this + class yourself.** """ height: typing.Optional[int] = attr.ib(default=None, repr=False) @@ -131,8 +143,8 @@ class EmbedVideo(EmbedResource): """The width of the video.""" -@attr.s(eq=True, hash=False, init=False, kw_only=True, slots=True) -class EmbedProvider: +@attr.s(eq=True, hash=False, kw_only=True, slots=True) +class EmbedProvider(_TruthyEmbedComponentMixin): """Represents an embed provider. !!! note @@ -140,6 +152,9 @@ class EmbedProvider: will be ignored during serialization. Expect this to be populated on any received embed attached to a message event provided by an external source. + + **Therefore, you should never need to initialize an instance of this + class yourself.** """ name: typing.Optional[str] = attr.ib(default=None, repr=True) @@ -149,8 +164,8 @@ class EmbedProvider: """The URL of the provider.""" -@attr.s(eq=True, hash=False, init=False, kw_only=True, slots=True) -class EmbedAuthor: +@attr.s(eq=True, hash=False, kw_only=True, slots=True) +class EmbedAuthor(_TruthyEmbedComponentMixin): """Represents an author of an embed.""" name: typing.Optional[str] = attr.ib(default=None, repr=True) @@ -166,18 +181,31 @@ class EmbedAuthor: """The author's icon, or `None` if not present.""" -@attr.s(eq=True, hash=False, init=False, kw_only=True, slots=True) +@attr.s(eq=True, hash=False, kw_only=True, slots=True) class EmbedField: """Represents a field in a embed.""" - name: typing.Optional[str] = attr.ib(default=None, repr=True) - """The name of the field, or `None` if not present.""" + name: str = attr.ib(repr=True) + """The name of the field.""" - value: typing.Optional[str] = attr.ib(default=None, repr=True) - """The value of the field, or `None` if not present.""" + value: str = attr.ib(repr=True) + """The value of the field.""" - is_inline: bool = attr.ib(default=False, repr=True) - """`True` if the field should display inline. Defaults to `False`.""" + _inline: bool = attr.ib(default=False, repr=True) + + # Use a property since we then keep the consistency of not using `is_` + # in the constructor for `_inline`. + @property + def is_inline(self) -> bool: + """Return `True` if the field should display inline. + + Defaults to False. + """ + return self._inline + + @is_inline.setter + def is_inline(self, value: bool) -> None: + self._inline = value @attr.s(eq=True, hash=False, init=True, kw_only=True, slots=True) @@ -202,10 +230,6 @@ def colour(self, value: typing.Optional[colors.ColorLike]) -> None: # implicit attrs conversion. self.color = value # type: ignore - @colour.deleter - def colour(self) -> None: - del self.color - title: typing.Optional[str] = attr.ib(default=None, repr=True) """The title of the embed, or `None` if not present.""" @@ -281,7 +305,7 @@ def colour(self) -> None: thumbnail: typing.Optional[EmbedImage] = attr.ib(default=None, repr=False) """The thumbnail to show in the embed, or `None` if not present.""" - video: typing.Optional[EmbedVideo] = attr.ib(default=None, repr=False) + video: typing.Optional[EmbedVideo] = attr.ib(default=None, repr=False, init=False) """The video to show in the embed, or `None` if not present. !!! note @@ -290,7 +314,7 @@ def colour(self) -> None: any received embed attached to a message event with a video attached. """ - provider: typing.Optional[EmbedProvider] = attr.ib(default=None, repr=False) + provider: typing.Optional[EmbedProvider] = attr.ib(default=None, repr=False, init=False) """The provider of the embed, or `None if not present. !!! note @@ -320,8 +344,7 @@ def set_author( self.author.name = name self.author.url = url if icon is not None: - self.author.icon = EmbedResource() - self.author.icon.resource = files.ensure_resource(icon) + self.author.icon = EmbedResource(resource=files.ensure_resource(icon)) else: self.author.icon = None return self @@ -335,8 +358,7 @@ def set_footer( self.footer = EmbedFooter() self.footer.text = text if icon is not None: - self.footer.icon = EmbedResource() - self.footer.icon.resource = files.ensure_resource(icon) + self.footer.icon = EmbedResource(resource=files.ensure_resource(icon)) else: self.footer.icon = None return self @@ -345,24 +367,18 @@ def set_image(self, image: typing.Union[None, str, files.Resource] = None, /) -> if image is None: self.image = None else: - self.image = EmbedImage() - self.image.resource = files.ensure_resource(image) + self.image = EmbedImage(resource=files.ensure_resource(image)) return self def set_thumbnail(self, image: typing.Union[None, str, files.Resource] = None, /) -> Embed: if image is None: self.thumbnail = None else: - self.thumbnail = EmbedImage() - self.thumbnail.resource = files.ensure_resource(image) + self.thumbnail = EmbedImage(resource=files.ensure_resource(image)) return self def add_field(self, name: str, value: str, *, inline: bool = False) -> Embed: - field = EmbedField() - field.name = name - field.value = value - field.is_inline = inline - self.fields.append(field) + self.fields.append(EmbedField(name=name, value=value, inline=inline)) return self def edit_field(self, index: int, name: str, value: str, /, *, inline: bool = False) -> Embed: diff --git a/hikari/models/emojis.py b/hikari/models/emojis.py index a65a61e0e1..4f1e04473e 100644 --- a/hikari/models/emojis.py +++ b/hikari/models/emojis.py @@ -20,7 +20,7 @@ from __future__ import annotations -__all__: typing.List[str] = ["Emoji", "UnicodeEmoji", "CustomEmoji", "KnownCustomEmoji"] +__all__: typing.Final[typing.List[str]] = ["Emoji", "UnicodeEmoji", "CustomEmoji", "KnownCustomEmoji"] import abc import typing diff --git a/hikari/models/gateway.py b/hikari/models/gateway.py index dd33ca72e9..99c08788f5 100644 --- a/hikari/models/gateway.py +++ b/hikari/models/gateway.py @@ -20,7 +20,7 @@ from __future__ import annotations -__all__: typing.List[str] = ["GatewayBot", "SessionStartLimit"] +__all__: typing.Final[typing.List[str]] = ["GatewayBot", "SessionStartLimit"] import typing diff --git a/hikari/models/guilds.py b/hikari/models/guilds.py index 60c5d2ea54..d40c7ac33b 100644 --- a/hikari/models/guilds.py +++ b/hikari/models/guilds.py @@ -20,7 +20,7 @@ from __future__ import annotations -__all__: typing.List[str] = [ +__all__: typing.Final[typing.List[str]] = [ "Guild", "GuildWidget", "Role", @@ -449,16 +449,17 @@ def format_icon(self, *, format_: typing.Optional[str] = None, size: int = 4096) ValueError If `size` is not a power of two or not between 16 and 4096. """ - if self.icon_hash is not None: - if format_ is None: - if self.icon_hash.startswith("a_"): - format_ = "gif" - else: - format_ = "png" + if self.icon_hash is None: + return None - url = cdn.generate_cdn_url("icons", str(self.id), self.icon_hash, format_=format_, size=size) - return files.URL(url) - return None + if format_ is None: + if self.icon_hash.startswith("a_"): + format_ = "gif" + else: + format_ = "png" + + url = cdn.generate_cdn_url("icons", str(self.id), self.icon_hash, format_=format_, size=size) + return files.URL(url) @attr.s(eq=True, hash=True, init=False, kw_only=True, slots=True) @@ -510,10 +511,11 @@ def format_splash(self, *, format_: str = "png", size: int = 4096) -> typing.Opt ValueError If `size` is not a power of two or not between 16 and 4096. """ - if self.splash_hash is not None: - url = cdn.generate_cdn_url("splashes", str(self.id), self.splash_hash, format_=format_, size=size) - return files.URL(url) - return None + if self.splash_hash is None: + return None + + url = cdn.generate_cdn_url("splashes", str(self.id), self.splash_hash, format_=format_, size=size) + return files.URL(url) @property def discovery_splash(self) -> typing.Optional[files.URL]: @@ -542,12 +544,13 @@ def format_discovery_splash(self, *, format_: str = "png", size: int = 4096) -> ValueError If `size` is not a power of two or not between 16 and 4096. """ - if self.discovery_splash_hash is not None: - url = cdn.generate_cdn_url( - "discovery-splashes", str(self.id), self.discovery_splash_hash, format_=format_, size=size - ) - return files.URL(url) - return None + if self.discovery_splash_hash is None: + return None + + url = cdn.generate_cdn_url( + "discovery-splashes", str(self.id), self.discovery_splash_hash, format_=format_, size=size + ) + return files.URL(url) @attr.s(eq=True, hash=True, init=False, kw_only=True, slots=True) @@ -570,9 +573,7 @@ class Guild(PartialGuild): # pylint:disable=too-many-instance-attributes owner_id: snowflake.Snowflake = attr.ib(eq=False, hash=False, repr=True) """The ID of the owner of this guild.""" - my_permissions: typing.Union[undefined.UndefinedType, permissions_.Permission] = attr.ib( - eq=False, hash=False, repr=False - ) + my_permissions: typing.Optional[permissions_.Permission] = attr.ib(eq=False, hash=False, repr=False) """The guild-level permissions that apply to the bot user. This will not take into account permission overwrites or implied @@ -873,10 +874,11 @@ def format_splash(self, *, format_: str = "png", size: int = 4096) -> typing.Opt ValueError If `size` is not a power of two or not between 16 and 4096. """ - if self.splash_hash is not None: - url = cdn.generate_cdn_url("splashes", str(self.id), self.splash_hash, format_=format_, size=size) - return files.URL(url) - return None + if self.splash_hash is None: + return None + + url = cdn.generate_cdn_url("splashes", str(self.id), self.splash_hash, format_=format_, size=size) + return files.URL(url) @property def discovery_splash(self) -> typing.Optional[files.URL]: @@ -905,12 +907,12 @@ def format_discovery_splash(self, *, format_: str = "png", size: int = 4096) -> ValueError If `size` is not a power of two or not between 16 and 4096. """ - if self.discovery_splash_hash: - url = cdn.generate_cdn_url( - "discovery-splashes", str(self.id), self.discovery_splash_hash, format_=format_, size=size - ) - return files.URL(url) - return None + if self.discovery_splash_hash is None: + return None + url = cdn.generate_cdn_url( + "discovery-splashes", str(self.id), self.discovery_splash_hash, format_=format_, size=size + ) + return files.URL(url) @property def banner(self) -> typing.Optional[files.URL]: @@ -939,7 +941,8 @@ def format_banner(self, *, format_: str = "png", size: int = 4096) -> typing.Opt ValueError If `size` is not a power of two or not between 16 and 4096. """ - if self.banner_hash is not None: - url = cdn.generate_cdn_url("banners", str(self.id), self.banner_hash, format_=format_, size=size) - return files.URL(url) - return None + if self.banner_hash is None: + return None + + url = cdn.generate_cdn_url("banners", str(self.id), self.banner_hash, format_=format_, size=size) + return files.URL(url) diff --git a/hikari/models/intents.py b/hikari/models/intents.py index 9367fb615a..7269d91098 100644 --- a/hikari/models/intents.py +++ b/hikari/models/intents.py @@ -20,7 +20,7 @@ from __future__ import annotations -__all__: typing.List[str] = ["Intent"] +__all__: typing.Final[typing.List[str]] = ["Intent"] import enum diff --git a/hikari/models/invites.py b/hikari/models/invites.py index bfda7b336a..048a357f04 100644 --- a/hikari/models/invites.py +++ b/hikari/models/invites.py @@ -20,7 +20,7 @@ from __future__ import annotations -__all__: typing.List[str] = ["TargetUserType", "VanityURL", "InviteGuild", "Invite", "InviteWithMetadata"] +__all__: typing.Final[typing.List[str]] = ["TargetUserType", "VanityURL", "InviteGuild", "Invite", "InviteWithMetadata"] import enum import typing @@ -220,10 +220,10 @@ class InviteWithMetadata(Invite): uses: int = attr.ib(eq=False, hash=False, repr=True) """The amount of times this invite has been used.""" - max_uses: int = attr.attrib(eq=False, hash=False, repr=True) + max_uses: typing.Optional[int] = attr.attrib(eq=False, hash=False, repr=True) """The limit for how many times this invite can be used before it expires. - If set to `0` then this is unlimited. + If set to `None` then this is unlimited. """ # FIXME: can we use a non-None value to represent infinity here somehow, or diff --git a/hikari/models/messages.py b/hikari/models/messages.py index 02edbf06e0..ec8669ebcb 100644 --- a/hikari/models/messages.py +++ b/hikari/models/messages.py @@ -20,7 +20,7 @@ from __future__ import annotations -__all__: typing.List[str] = [ +__all__: typing.Final[typing.List[str]] = [ "MessageType", "MessageFlag", "MessageActivityType", diff --git a/hikari/models/permissions.py b/hikari/models/permissions.py index a32ff915a0..1ce37904d6 100644 --- a/hikari/models/permissions.py +++ b/hikari/models/permissions.py @@ -20,7 +20,7 @@ from __future__ import annotations -__all__: typing.List[str] = ["Permission"] +__all__: typing.Final[typing.List[str]] = ["Permission"] import enum diff --git a/hikari/models/presences.py b/hikari/models/presences.py index 492b4a32ab..53379fb2d2 100644 --- a/hikari/models/presences.py +++ b/hikari/models/presences.py @@ -20,7 +20,7 @@ from __future__ import annotations -__all__: typing.List[str] = [ +__all__: typing.Final[typing.List[str]] = [ "Activity", "ActivityAssets", "ActivityFlag", diff --git a/hikari/models/users.py b/hikari/models/users.py index 89f3fa0d18..b109ff1608 100644 --- a/hikari/models/users.py +++ b/hikari/models/users.py @@ -20,7 +20,7 @@ from __future__ import annotations -__all__: typing.List[str] = ["User", "OwnUser", "UserFlag", "PremiumType"] +__all__: typing.Final[typing.List[str]] = ["User", "OwnUser", "UserFlag", "PremiumType"] import enum import typing @@ -199,16 +199,17 @@ def format_avatar(self, *, format_: typing.Optional[str] = None, size: int = 409 ValueError If `size` is not a power of two or not between 16 and 4096. """ - if self.avatar_hash is not None: - if format_ is None: - if self.avatar_hash.startswith("a_"): - format_ = "gif" - else: - format_ = "png" - - url = cdn.generate_cdn_url("avatars", str(self.id), self.avatar_hash, format_=format_, size=size) - return files.URL(url) - return None + if self.avatar_hash is None: + return None + + if format_ is None: + if self.avatar_hash.startswith("a_"): + format_ = "gif" + else: + format_ = "png" + + url = cdn.generate_cdn_url("avatars", str(self.id), self.avatar_hash, format_=format_, size=size) + return files.URL(url) @property def default_avatar(self) -> files.URL: # noqa: D401 imperative mood check diff --git a/hikari/models/voices.py b/hikari/models/voices.py index cb2a12dc89..09317271ca 100644 --- a/hikari/models/voices.py +++ b/hikari/models/voices.py @@ -20,7 +20,7 @@ from __future__ import annotations -__all__: typing.List[str] = ["VoiceRegion", "VoiceState"] +__all__: typing.Final[typing.List[str]] = ["VoiceRegion", "VoiceState"] import typing diff --git a/hikari/models/webhooks.py b/hikari/models/webhooks.py index 9b4d1e2bcf..2ed57dda4b 100644 --- a/hikari/models/webhooks.py +++ b/hikari/models/webhooks.py @@ -20,7 +20,7 @@ from __future__ import annotations -__all__: typing.List[str] = ["WebhookType", "Webhook"] +__all__: typing.Final[typing.List[str]] = ["WebhookType", "Webhook"] import enum import typing diff --git a/hikari/net/__init__.py b/hikari/net/__init__.py index 13147fe76a..05135ccf98 100644 --- a/hikari/net/__init__.py +++ b/hikari/net/__init__.py @@ -20,7 +20,7 @@ from __future__ import annotations -__all__: typing.List[str] = [] +__all__: typing.Final[typing.List[str]] = [] # noinspection PyUnresolvedReferences import typing diff --git a/hikari/net/buckets.py b/hikari/net/buckets.py index e2d6d90806..02813e8ecd 100644 --- a/hikari/net/buckets.py +++ b/hikari/net/buckets.py @@ -204,7 +204,7 @@ from __future__ import annotations -__all__: typing.List[str] = ["UNKNOWN_HASH", "RESTBucket", "RESTBucketManager"] +__all__: typing.Final[typing.List[str]] = ["UNKNOWN_HASH", "RESTBucket", "RESTBucketManager"] import asyncio import datetime diff --git a/hikari/net/gateway.py b/hikari/net/gateway.py index 760b36a7a0..d050693ff2 100644 --- a/hikari/net/gateway.py +++ b/hikari/net/gateway.py @@ -20,7 +20,7 @@ from __future__ import annotations -__all__: typing.List[str] = ["Gateway"] +__all__: typing.Final[typing.List[str]] = ["Gateway"] import asyncio import enum @@ -407,10 +407,10 @@ async def update_presence( presence = self._build_presence_payload(idle_since=idle_since, is_afk=is_afk, status=status, activity=activity) payload: data_binding.JSONObject = {"op": self._GatewayOpcode.PRESENCE_UPDATE, "d": presence} await self._send_json(payload) - self._idle_since = idle_since if not idle_since is undefined.UNDEFINED else self._idle_since - self._is_afk = is_afk if not is_afk is undefined.UNDEFINED else self._is_afk - self._activity = activity if not activity is undefined.UNDEFINED else self._activity - self._status = status if not status is undefined.UNDEFINED else self._status + self._idle_since = idle_since if idle_since is not undefined.UNDEFINED else self._idle_since + self._is_afk = is_afk if is_afk is not undefined.UNDEFINED else self._is_afk + self._activity = activity if activity is not undefined.UNDEFINED else self._activity + self._status = status if status is not undefined.UNDEFINED else self._status async def update_voice_state( self, diff --git a/hikari/net/http_client.py b/hikari/net/http_client.py index 5c39dfa2c1..358eb2d2b3 100644 --- a/hikari/net/http_client.py +++ b/hikari/net/http_client.py @@ -19,7 +19,7 @@ """Base functionality for any HTTP-based network component.""" from __future__ import annotations -__all__: typing.List[str] = ["HTTPClient"] +__all__: typing.Final[typing.List[str]] = ["HTTPClient"] import abc import http diff --git a/hikari/net/http_settings.py b/hikari/net/http_settings.py index 2147dbd5ac..13591dd948 100644 --- a/hikari/net/http_settings.py +++ b/hikari/net/http_settings.py @@ -20,7 +20,7 @@ from __future__ import annotations -__all__: typing.List[str] = ["HTTPSettings"] +__all__: typing.Final[typing.List[str]] = ["HTTPSettings"] import typing diff --git a/hikari/net/iterators.py b/hikari/net/iterators.py index 003e019ace..ffa3391875 100644 --- a/hikari/net/iterators.py +++ b/hikari/net/iterators.py @@ -19,18 +19,19 @@ """Lazy iterators for data that requires repeated API calls to retrieve.""" from __future__ import annotations -__all__: typing.List[str] = ["LazyIterator"] +__all__: typing.Final[typing.List[str]] = ["LazyIterator"] import abc import typing -from hikari.api import rest from hikari.net import routes from hikari.utilities import data_binding from hikari.utilities import snowflake from hikari.utilities import undefined if typing.TYPE_CHECKING: + from hikari.api import rest + from hikari.models import applications from hikari.models import audit_logs from hikari.models import guilds diff --git a/hikari/net/rate_limits.py b/hikari/net/rate_limits.py index c3eebf7e99..8c10d97169 100644 --- a/hikari/net/rate_limits.py +++ b/hikari/net/rate_limits.py @@ -23,7 +23,7 @@ from __future__ import annotations -__all__: typing.List[str] = [ +__all__: typing.Final[typing.List[str]] = [ "BaseRateLimiter", "BurstRateLimiter", "ManualRateLimiter", diff --git a/hikari/net/rest.py b/hikari/net/rest.py index 645493b613..1fdf643b0b 100644 --- a/hikari/net/rest.py +++ b/hikari/net/rest.py @@ -20,7 +20,7 @@ from __future__ import annotations -__all__: typing.List[str] = ["REST"] +__all__: typing.Final[typing.List[str]] = ["REST"] import asyncio import contextlib @@ -959,11 +959,11 @@ def fetch_messages( if undefined.count(before, after, around) < 2: raise TypeError("Expected no kwargs, or maximum of one of 'before', 'after', 'around'") - if not before is undefined.UNDEFINED: + if before is not undefined.UNDEFINED: direction, timestamp = "before", before - elif not after is undefined.UNDEFINED: + elif after is not undefined.UNDEFINED: direction, timestamp = "after", after - elif not around is undefined.UNDEFINED: + elif around is not undefined.UNDEFINED: direction, timestamp = "around", around else: direction, timestamp = "before", snowflake.Snowflake.max() @@ -1097,7 +1097,7 @@ async def create_message( else: final_attachments = [files.ensure_resource(a) for a in attachments] - if not embed is undefined.UNDEFINED: + if embed is not undefined.UNDEFINED: embed_payload, embed_attachments = self._app.entity_factory.serialize_embed(embed) body.put("embed", embed_payload) final_attachments.extend(embed_attachments) @@ -1393,7 +1393,7 @@ async def create_webhook( route = routes.POST_WEBHOOK.compile(channel=channel) body = data_binding.JSONObjectBuilder() body.put("name", name) - if not avatar is undefined.UNDEFINED: + if avatar is not undefined.UNDEFINED: async with avatar.stream(executor=self._app.executor) as stream: body.put("avatar", await stream.data_uri()) @@ -1454,7 +1454,7 @@ async def edit_webhook( if avatar is None: body.put("avatar", None) - elif not avatar is undefined.UNDEFINED: + elif avatar is not undefined.UNDEFINED: async with avatar.stream(executor=self._app.executor) as stream: body.put("avatar", await stream.data_uri()) @@ -1497,14 +1497,18 @@ async def execute_webhook( route = routes.POST_WEBHOOK_WITH_TOKEN.compile(webhook=webhook, token=token) no_auth = True - attachments = [] if attachments is undefined.UNDEFINED else [a for a in attachments] + if attachments is undefined.UNDEFINED: + final_attachments: typing.List[files.Resource] = [] + else: + final_attachments = [files.ensure_resource(a) for a in attachments] + serialized_embeds = [] - if not embeds is undefined.UNDEFINED: + if embeds is not undefined.UNDEFINED: for embed in embeds: embed_payload, embed_attachments = self._app.entity_factory.serialize_embed(embed) serialized_embeds.append(embed_payload) - attachments.extend(embed_attachments) + final_attachments.extend(embed_attachments) body = data_binding.JSONObjectBuilder() body.put("mentions", self._generate_allowed_mentions(mentions_everyone, user_mentions, role_mentions)) @@ -1515,14 +1519,14 @@ async def execute_webhook( body.put("tts", tts) body.put("wait", True) - if attachments: + if final_attachments: form = data_binding.URLEncodedForm() form.add_field("payload_json", data_binding.dump_json(body), content_type=self._APPLICATION_JSON) stack = contextlib.AsyncExitStack() try: - for i, attachment in enumerate(attachments): + for i, attachment in enumerate(final_attachments): stream = await stack.enter_async_context(attachment.stream(self._app.executor)) form.add_field( f"file{i}", stream, filename=stream.filename, content_type=self._APPLICATION_OCTET_STREAM @@ -1685,7 +1689,7 @@ def fetch_audit_log( else: before = str(int(before)) - if not user is undefined.UNDEFINED: + if user is not undefined.UNDEFINED: user = str(int(user)) return iterators.AuditLogIterator(self._app, self._request, guild, before, user, event_type) @@ -1725,7 +1729,7 @@ async def create_emoji( route = routes.POST_GUILD_EMOJIS.compile(guild=guild) body = data_binding.JSONObjectBuilder() body.put("name", name) - if not image is undefined.UNDEFINED: + if image is not undefined.UNDEFINED: async with image.stream(executor=self._app.executor) as stream: body.put("image", await stream.data_uri()) @@ -1834,19 +1838,19 @@ async def edit_guild( if icon is None: body.put("icon", None) - elif not icon is undefined.UNDEFINED: + elif icon is not undefined.UNDEFINED: async with icon.stream(executor=self._app.executor) as stream: body.put("icon", await stream.data_uri()) if splash is None: body.put("splash", None) - elif not splash is undefined.UNDEFINED: + elif splash is not undefined.UNDEFINED: async with splash.stream(executor=self._app.executor) as stream: body.put("splash", await stream.data_uri()) if banner is None: body.put("banner", None) - elif not banner is undefined.UNDEFINED: + elif banner is not undefined.UNDEFINED: async with banner.stream(executor=self._app.executor) as stream: body.put("banner", await stream.data_uri()) @@ -2073,7 +2077,7 @@ async def edit_member( if voice_channel is None: body.put("channel_id", None) - elif not voice_channel is undefined.UNDEFINED: + elif voice_channel is not undefined.UNDEFINED: body.put_snowflake("channel_id", voice_channel) await self._request(route, body=body, reason=reason) @@ -2352,7 +2356,7 @@ async def edit_widget( body.put("enabled", enabled) if channel is None: body.put("channel", None) - elif not channel is undefined.UNDEFINED: + elif channel is not undefined.UNDEFINED: body.put_snowflake("channel", channel) raw_response = await self._request(route, body=body, reason=reason) diff --git a/hikari/net/rest_utils.py b/hikari/net/rest_utils.py index 51d0706290..ab767ab141 100644 --- a/hikari/net/rest_utils.py +++ b/hikari/net/rest_utils.py @@ -22,7 +22,7 @@ """ from __future__ import annotations -__all__: typing.List[str] = ["TypingIndicator", "GuildBuilder"] +__all__: typing.Final[typing.List[str]] = ["TypingIndicator", "GuildBuilder"] import asyncio import contextlib diff --git a/hikari/net/routes.py b/hikari/net/routes.py index b46624d684..387b09d408 100644 --- a/hikari/net/routes.py +++ b/hikari/net/routes.py @@ -20,7 +20,7 @@ from __future__ import annotations -__all__: typing.List[str] = ["CompiledRoute", "Route"] +__all__: typing.Final[typing.List[str]] = ["CompiledRoute", "Route"] import re import typing diff --git a/hikari/net/strings.py b/hikari/net/strings.py index dcfaff4a6b..b29d9cc6bd 100644 --- a/hikari/net/strings.py +++ b/hikari/net/strings.py @@ -57,7 +57,7 @@ # User-agent info. AIOHTTP_VERSION: typing.Final[str] = f"aiohttp {aiohttp.__version__}" LIBRARY_VERSION: typing.Final[str] = f"hikari {hikari.__version__}" -SYSTEM_TYPE: typing.Final[str] = (f"{platform.system()} {platform.architecture()[0]}") +SYSTEM_TYPE: typing.Final[str] = f"{platform.system()} {platform.architecture()[0]}" HTTP_USER_AGENT: typing.Final[str] = ( f"DiscordBot ({hikari.__url__}, {hikari.__version__}) {hikari.__author__} " f"Aiohttp/{aiohttp.__version__} " @@ -71,5 +71,6 @@ # URLs REST_API_URL: typing.Final[str] = "https://discord.com/api/v{0.version}" OAUTH2_API_URL: typing.Final[str] = f"{REST_API_URL}/oauth2" +CDN_URL: typing.Final[str] = "https://cdn.discordapp.com" -__all__: typing.List[str] = [attr for attr in globals() if all(c == "_" or c.isupper() for c in attr)] +__all__: typing.Final[typing.List[str]] = [attr for attr in globals() if not any(c.islower() for c in attr)] diff --git a/hikari/net/tracing.py b/hikari/net/tracing.py index 46f11fe52d..0eebbec5e2 100644 --- a/hikari/net/tracing.py +++ b/hikari/net/tracing.py @@ -19,14 +19,12 @@ """Provides logging support for HTTP requests internally.""" from __future__ import annotations -__all__: typing.List[str] = ["BaseTracer", "CFRayTracer", "DebugTracer"] +__all__: typing.Final[typing.List[str]] = ["BaseTracer", "CFRayTracer", "DebugTracer"] import functools import io import logging import time - -# noinspection PyUnresolvedReferences import typing import uuid diff --git a/hikari/utilities/__init__.py b/hikari/utilities/__init__.py index 9836276fdb..67828f7c44 100644 --- a/hikari/utilities/__init__.py +++ b/hikari/utilities/__init__.py @@ -20,7 +20,7 @@ from __future__ import annotations -__all__: typing.List[str] = [] +__all__: typing.Final[typing.List[str]] = [] # noinspection PyUnresolvedReferences import typing diff --git a/hikari/utilities/aio.py b/hikari/utilities/aio.py index 1aed3470dd..9089d9be35 100644 --- a/hikari/utilities/aio.py +++ b/hikari/utilities/aio.py @@ -20,7 +20,7 @@ from __future__ import annotations -__all__: typing.List[str] = ["completed_future", "is_async_iterator", "is_async_iterable"] +__all__: typing.Final[typing.List[str]] = ["completed_future", "is_async_iterator", "is_async_iterable"] import asyncio import inspect diff --git a/hikari/utilities/cdn.py b/hikari/utilities/cdn.py index b2482672df..b615019a5e 100644 --- a/hikari/utilities/cdn.py +++ b/hikari/utilities/cdn.py @@ -20,18 +20,12 @@ from __future__ import annotations -__all__: typing.List[str] = ["generate_cdn_url", "get_avatar_url", "get_default_avatar_url", "get_default_avatar_index"] +__all__: typing.Final[typing.List[str]] = ["generate_cdn_url", "get_default_avatar_url", "get_default_avatar_index"] import typing import urllib.parse - -if typing.TYPE_CHECKING: - pass - - -BASE_CDN_URL: typing.Final[str] = "https://cdn.discordapp.com" -"""The URL for the CDN.""" +from hikari.net import strings def generate_cdn_url(*route_parts: str, format_: str, size: typing.Optional[int]) -> str: @@ -66,7 +60,7 @@ def generate_cdn_url(*route_parts: str, format_: str, size: typing.Optional[int] raise ValueError("Size must be an integer power of 2") path = "/".join(urllib.parse.unquote(part) for part in route_parts) - url = urllib.parse.urljoin(BASE_CDN_URL, "/" + path) + "." + str(format_) + url = urllib.parse.urljoin(strings.CDN_URL, "/" + path) + "." + str(format_) query = urllib.parse.urlencode({"size": size}) if size is not None else None return f"{url}?{query}" if query else url diff --git a/hikari/utilities/data_binding.py b/hikari/utilities/data_binding.py index 54c9d19091..8047da219e 100644 --- a/hikari/utilities/data_binding.py +++ b/hikari/utilities/data_binding.py @@ -19,7 +19,7 @@ """Data binding utilities.""" from __future__ import annotations -__all__: typing.List[str] = [ +__all__: typing.Final[typing.List[str]] = [ "Headers", "Query", "JSONObject", @@ -134,7 +134,7 @@ def put( `True` will be translated to `"true"`, `False` will be translated to `"false"`, and `None` will be translated to `"null"`. """ - if not value is undefined.UNDEFINED: + if value is not undefined.UNDEFINED: if conversion is not None: value = conversion(value) @@ -196,7 +196,7 @@ def put( conversion : typing.Callable[[typing.Any], JSONAny] or None Optional conversion to apply. """ - if not value is undefined.UNDEFINED: + if value is not undefined.UNDEFINED: if conversion is not None: self[key] = conversion(value) else: diff --git a/hikari/utilities/date.py b/hikari/utilities/date.py index 840d4e3d50..2cced21e07 100644 --- a/hikari/utilities/date.py +++ b/hikari/utilities/date.py @@ -20,7 +20,7 @@ from __future__ import annotations -__all__: typing.List[str] = [ +__all__: typing.Final[typing.List[str]] = [ "DISCORD_EPOCH", "rfc7231_datetime_string_to_datetime", "datetime_to_discord_epoch", diff --git a/hikari/utilities/files.py b/hikari/utilities/files.py index dbdae058ff..024c0df43e 100644 --- a/hikari/utilities/files.py +++ b/hikari/utilities/files.py @@ -90,13 +90,15 @@ def ensure_resource(url_or_resource: typing.Union[None, str, Resource], /) -> ty """ if isinstance(url_or_resource, Resource): return url_or_resource - elif url_or_resource is not None: - if url_or_resource.startswith(("https://", "http://")): - return URL(url_or_resource) - else: - path = pathlib.Path(url_or_resource) - return File(path, path.name) - return None + + if url_or_resource is None: + return None + + if url_or_resource.startswith(("https://", "http://")): + return URL(url_or_resource) + + path = pathlib.Path(url_or_resource) + return File(path, path.name) def guess_mimetype_from_filename(name: str, /) -> typing.Optional[str]: @@ -137,14 +139,13 @@ def guess_mimetype_from_data(data: bytes, /) -> typing.Optional[str]: """ if data.startswith(b"\211PNG\r\n\032\n"): return "image/png" - elif data[6:].startswith((b"Exif", b"JFIF")): + if data[6:].startswith((b"Exif", b"JFIF")): return "image/jpeg" - elif data.startswith((b"GIF87a", b"GIF89a")): + if data.startswith((b"GIF87a", b"GIF89a")): return "image/gif" - elif data.startswith(b"RIFF") and data[8:].startswith(b"WEBP"): + if data.startswith(b"RIFF") and data[8:].startswith(b"WEBP"): return "image/webp" - else: - return None + return None def guess_file_extension(mimetype: str) -> typing.Optional[str]: @@ -383,6 +384,14 @@ def __str__(self) -> str: def __repr__(self) -> str: return f"{type(self).__name__}(url={self.url!r}, filename={self.filename!r})" + def __eq__(self, other: typing.Any) -> bool: + if isinstance(other, Resource): + return self.url == other.url + return False + + def __hash__(self) -> int: + return hash(self.url) + class Bytes(Resource): """Representation of in-memory data to upload. @@ -440,6 +449,7 @@ def __init__( extension = guess_file_extension(mimetype) if mimetype is None: + # TODO: should I just default to application/octet-stream here? if extension is None: raise TypeError("Cannot infer data type details, please specify a mimetype or an extension") raise TypeError("Cannot infer data type details from extension. Please specify a mimetype") @@ -638,7 +648,7 @@ class File(Resource): path: typing.Union[str, pathlib.Path] _filename: typing.Optional[str] - def __init__(self, path: typing.Union[str, pathlib.Path], filename: typing.Optional[str]) -> None: + def __init__(self, path: typing.Union[str, pathlib.Path], filename: typing.Optional[str] = None) -> None: self.path = path self._filename = filename diff --git a/hikari/utilities/reflect.py b/hikari/utilities/reflect.py index 3c2fc166d5..72db2872b9 100644 --- a/hikari/utilities/reflect.py +++ b/hikari/utilities/reflect.py @@ -20,7 +20,7 @@ from __future__ import annotations -__all__: typing.List[str] = ["resolve_signature", "EMPTY", "get_logger"] +__all__: typing.Final[typing.List[str]] = ["resolve_signature", "EMPTY", "get_logger"] import inspect import logging diff --git a/hikari/utilities/snowflake.py b/hikari/utilities/snowflake.py index d3c9f739bc..8c51a89637 100644 --- a/hikari/utilities/snowflake.py +++ b/hikari/utilities/snowflake.py @@ -20,7 +20,7 @@ from __future__ import annotations -__all__: typing.List[str] = ["Snowflake"] +__all__: typing.Final[typing.List[str]] = ["Snowflake"] import datetime diff --git a/hikari/utilities/undefined.py b/hikari/utilities/undefined.py index 556866a3fd..fe904c7a73 100644 --- a/hikari/utilities/undefined.py +++ b/hikari/utilities/undefined.py @@ -20,7 +20,7 @@ from __future__ import annotations -__all__: typing.List[str] = ["UndefinedType", "UNDEFINED"] +__all__: typing.Final[typing.List[str]] = ["UndefinedType", "UNDEFINED"] import enum diff --git a/mypy.ini b/mypy.ini index 35098376bc..523f7bbc3f 100644 --- a/mypy.ini +++ b/mypy.ini @@ -1,7 +1,7 @@ [mypy] # general settings check_untyped_defs = true -incremental = true +incremental = false namespace_packages = true no_implicit_optional = true pretty = true diff --git a/tests/__init__.py b/tests/__init__.py deleted file mode 100644 index 1c1502a5ca..0000000000 --- a/tests/__init__.py +++ /dev/null @@ -1,18 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# Copyright © Nekoka.tt 2019-2020 -# -# This file is part of Hikari. -# -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . diff --git a/tests/hikari/impl/__init__.py b/tests/hikari/impl/__init__.py deleted file mode 100644 index 1c1502a5ca..0000000000 --- a/tests/hikari/impl/__init__.py +++ /dev/null @@ -1,18 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# Copyright © Nekoka.tt 2019-2020 -# -# This file is part of Hikari. -# -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . diff --git a/tests/hikari/impl/test_entity_factory.py b/tests/hikari/impl/test_entity_factory.py index 514629f36f..b7ec15200a 100644 --- a/tests/hikari/impl/test_entity_factory.py +++ b/tests/hikari/impl/test_entity_factory.py @@ -21,7 +21,8 @@ import mock import pytest -from hikari.api import app as app_ + +from hikari.api import rest from hikari.impl import entity_factory from hikari.events import channel as channel_events from hikari.events import guild as guild_events @@ -40,9 +41,10 @@ from hikari.models import messages as message_models from hikari.models import permissions as permission_models from hikari.models import presences as presence_models -from hikari.models import webhooks as webhook_models from hikari.models import users as user_models from hikari.models import voices as voice_models +from hikari.models import webhooks as webhook_models +from hikari.utilities import files from hikari.utilities import undefined @@ -58,8 +60,11 @@ def test__deserialize_max_uses_returns_int(): assert entity_factory._deserialize_max_uses(120) == 120 -def test__deserialize_max_uses_returns_infinity(): - assert entity_factory._deserialize_max_uses(0) == float("inf") +def test__deserialize_max_uses_returns_none_when_zero(): + # Yes, I changed this from float("inf") so that it returns None. I did this + # to provide some level of consistency with `max_age`. We need to revisit + # this if possible. + assert entity_factory._deserialize_max_uses(0) is None def test__deserialize_max_age_returns_timedelta(): @@ -72,8 +77,8 @@ def test__deserialize_max_age_returns_null(): class TestEntityFactoryImpl: @pytest.fixture() - def mock_app(self) -> app_.IRESTApp: - return mock.MagicMock(app_.IRESTApp) + def mock_app(self) -> rest.IRESTApp: + return mock.MagicMock(rest.IRESTApp) @pytest.fixture() def entity_factory_impl(self, mock_app) -> entity_factory.EntityFactoryComponentImpl: @@ -99,7 +104,7 @@ def partial_integration(self): def own_connection_payload(self, partial_integration): return { "friend_sync": False, - "id": "2513849648", + "id": "2513849648abc", "integrations": [partial_integration], "name": "FS", "revoked": False, @@ -113,7 +118,7 @@ def test_deserialize_own_connection( self, entity_factory_impl, mock_app, own_connection_payload, partial_integration ): own_connection = entity_factory_impl.deserialize_own_connection(own_connection_payload) - assert own_connection.id == 2513849648 + assert own_connection.id == "2513849648abc" assert own_connection.name == "FS" assert own_connection.type == "twitter" assert own_connection.is_revoked is False @@ -892,7 +897,7 @@ def embed_payload(self): "provider": {"name": "some name", "url": "https://somewhere.com/provider"}, "author": { "name": "some name", - "url": "https://somewhere.com/author", + "url": "https://somewhere.com/author-url", "icon_url": "https://somewhere.com/author.png", "proxy_icon_url": "https://media.somewhere.com/author.png", }, @@ -909,21 +914,21 @@ def test_deserialize_embed_with_full_embed(self, entity_factory_impl, embed_payl assert isinstance(embed.color, color_models.Color) # EmbedFooter assert embed.footer.text == "footer text" - assert embed.footer.icon_url == "https://somewhere.com/footer.png" - assert embed.footer.proxy_icon_url == "https://media.somewhere.com/footer.png" + assert embed.footer.icon.resource.url == "https://somewhere.com/footer.png" + assert embed.footer.icon.proxy_resource.url == "https://media.somewhere.com/footer.png" assert isinstance(embed.footer, embed_models.EmbedFooter) # EmbedImage assert embed.image.url == "https://somewhere.com/image.png" - assert embed.image.proxy_url == "https://media.somewhere.com/image.png" + assert embed.image.proxy_resource.url == "https://media.somewhere.com/image.png" assert embed.image.height == 122 assert embed.image.width == 133 assert isinstance(embed.image, embed_models.EmbedImage) # EmbedThumbnail assert embed.thumbnail.url == "https://somewhere.com/thumbnail.png" - assert embed.thumbnail.proxy_url == "https://media.somewhere.com/thumbnail.png" + assert embed.thumbnail.proxy_resource.url == "https://media.somewhere.com/thumbnail.png" assert embed.thumbnail.height == 123 assert embed.thumbnail.width == 456 - assert isinstance(embed.thumbnail, embed_models.EmbedThumbnail) + assert isinstance(embed.thumbnail, embed_models.EmbedImage) # EmbedVideo assert embed.video.url == "https://somewhere.com/video.mp4" assert embed.video.height == 1234 @@ -935,9 +940,9 @@ def test_deserialize_embed_with_full_embed(self, entity_factory_impl, embed_payl assert isinstance(embed.provider, embed_models.EmbedProvider) # EmbedAuthor assert embed.author.name == "some name" - assert embed.author.url == "https://somewhere.com/author" - assert embed.author.icon_url == "https://somewhere.com/author.png" - assert embed.author.proxy_icon_url == "https://media.somewhere.com/author.png" + assert embed.author.url == "https://somewhere.com/author-url" + assert embed.author.icon.url == "https://somewhere.com/author.png" + assert embed.author.icon.proxy_resource.url == "https://media.somewhere.com/author.png" assert isinstance(embed.author, embed_models.EmbedAuthor) # EmbedField assert len(embed.fields) == 1 @@ -961,36 +966,17 @@ def test_deserialize_embed_with_partial_fields(self, entity_factory_impl, embed_ ) # EmbedFooter assert embed.footer.text == "footer text" - assert embed.footer.icon_url is None - assert embed.footer.proxy_icon_url is None - assert isinstance(embed.footer, embed_models.EmbedFooter) + assert embed.footer.icon is None # EmbedImage - assert embed.image.url is None - assert embed.image.proxy_url is None - assert embed.image.height is None - assert embed.image.width is None - assert isinstance(embed.image, embed_models.EmbedImage) + assert embed.image is None # EmbedThumbnail - assert embed.thumbnail.url is None - assert embed.thumbnail.proxy_url is None - assert embed.thumbnail.height is None - assert embed.thumbnail.width is None - assert isinstance(embed.thumbnail, embed_models.EmbedThumbnail) + assert embed.thumbnail is None # EmbedVideo - assert embed.video.url is None - assert embed.video.height is None - assert embed.video.width is None - assert isinstance(embed.video, embed_models.EmbedVideo) + assert embed.video is None # EmbedProvider - assert embed.provider.name is None - assert embed.provider.url is None - assert isinstance(embed.provider, embed_models.EmbedProvider) + assert embed.provider is None # EmbedAuthor - assert embed.author.name is None - assert embed.author.url is None - assert embed.author.icon_url is None - assert embed.author.proxy_icon_url is None - assert isinstance(embed.author, embed_models.EmbedAuthor) + assert embed.author is None # EmbedField assert len(embed.fields) == 1 field = embed.fields[0] @@ -1014,36 +1000,99 @@ def test_deserialize_embed_with_empty_embed(self, entity_factory_impl): assert embed.author is None assert embed.fields == [] - def test_serialize_embed(self, entity_factory_impl): - payload = entity_factory_impl.serialize_embed( + def test_serialize_embed_with_non_url_resources_provides_attachments(self, entity_factory_impl): + footer_icon = embed_models.EmbedResource(resource=files.File("cat.png")) + thumbnail = embed_models.EmbedImage(resource=files.File("dog.png")) + image = embed_models.EmbedImage(resource=files.Bytes(b"potato kung fu", filename="sushi.pdf")) + author_icon = embed_models.EmbedResource(resource=files.Bytes(b"potato kung fu^2", filename="sushi².jpg")) + + payload, resources = entity_factory_impl.serialize_embed( embed_models.Embed( title="Title", description="Nyaa", url="https://some-url", timestamp=datetime.datetime(2020, 5, 29, 20, 37, 22, 865139), color=color_models.Color(321321), - footer=embed_models.EmbedFooter(text="TEXT", icon_url="httppppp"), - image=embed_models.EmbedImage(url="https://///"), - thumbnail=embed_models.EmbedThumbnail(url="wss://not-a-valid-url"), - author=embed_models.EmbedAuthor(name="AUTH ME", url="wss://\\_/-_-\\_/", icon_url="icon://"), - fields=[embed_models.EmbedField(value="VALUE", name="NAME", is_inline=True)], + footer=embed_models.EmbedFooter(text="TEXT", icon=footer_icon), + image=image, + thumbnail=thumbnail, + author=embed_models.EmbedAuthor(name="AUTH ME", url="wss://\\_/-_-\\_/", icon=author_icon), + fields=[embed_models.EmbedField(value="VALUE", name="NAME", inline=True)], ) ) + + # Non URL bois should be returned in the resources container. + assert len(resources) == 4 + assert footer_icon.resource in resources + assert thumbnail.resource in resources + assert image.resource in resources + assert author_icon.resource in resources + assert payload == { "title": "Title", "description": "Nyaa", "url": "https://some-url", "timestamp": "2020-05-29T20:37:22.865139", "color": 321321, - "footer": {"text": "TEXT", "icon_url": "httppppp"}, - "image": {"url": "https://///"}, - "thumbnail": {"url": "wss://not-a-valid-url"}, - "author": {"name": "AUTH ME", "url": "wss://\\_/-_-\\_/", "icon_url": "icon://"}, + "footer": {"text": "TEXT", "icon_url": footer_icon.url}, + "image": {"url": image.url}, + "thumbnail": {"url": thumbnail.url}, + "author": {"name": "AUTH ME", "url": "wss://\\_/-_-\\_/", "icon_url": author_icon.url}, + "fields": [{"value": "VALUE", "name": "NAME", "inline": True}], + } + + def test_serialize_embed_with_url_resources_does_not_provide_attachments(self, entity_factory_impl): + class DummyWebResource(files.WebResource): + @property + def url(self) -> str: + return "http://lolbook.com" + + @property + def filename(self) -> str: + return "lolbook.png" + + footer_icon = embed_models.EmbedResource(resource=files.URL("http://http.cat")) + thumbnail = embed_models.EmbedImage(resource=DummyWebResource()) + image = embed_models.EmbedImage(resource=files.URL("http://bazbork.com")) + author_icon = embed_models.EmbedResource(resource=files.URL("http://foobar.com")) + + payload, resources = entity_factory_impl.serialize_embed( + embed_models.Embed( + title="Title", + description="Nyaa", + url="https://some-url", + timestamp=datetime.datetime(2020, 5, 29, 20, 37, 22, 865139), + color=color_models.Color(321321), + footer=embed_models.EmbedFooter(text="TEXT", icon=footer_icon), + image=image, + thumbnail=thumbnail, + author=embed_models.EmbedAuthor(name="AUTH ME", url="wss://\\_/-_-\\_/", icon=author_icon), + fields=[embed_models.EmbedField(value="VALUE", name="NAME", inline=True)], + ) + ) + + # Non URL bois should be returned in the resources container. + assert footer_icon.resource not in resources + assert thumbnail.resource not in resources + assert image.resource not in resources + assert author_icon.resource not in resources + assert not resources + + assert payload == { + "title": "Title", + "description": "Nyaa", + "url": "https://some-url", + "timestamp": "2020-05-29T20:37:22.865139", + "color": 321321, + "footer": {"text": "TEXT", "icon_url": footer_icon.url}, + "image": {"url": image.url}, + "thumbnail": {"url": thumbnail.url}, + "author": {"name": "AUTH ME", "url": "wss://\\_/-_-\\_/", "icon_url": author_icon.url}, "fields": [{"value": "VALUE", "name": "NAME", "inline": True}], } def test_serialize_embed_with_null_sub_fields(self, entity_factory_impl): - payload = entity_factory_impl.serialize_embed( + payload, resources = entity_factory_impl.serialize_embed( embed_models.Embed( title="Title", description="Nyaa", @@ -1051,10 +1100,9 @@ def test_serialize_embed_with_null_sub_fields(self, entity_factory_impl): timestamp=datetime.datetime(2020, 5, 29, 20, 37, 22, 865139), color=color_models.Color(321321), footer=embed_models.EmbedFooter(), - image=embed_models.EmbedImage(), - thumbnail=embed_models.EmbedThumbnail(), + image=None, + thumbnail=None, author=embed_models.EmbedAuthor(), - fields=[embed_models.EmbedField()], ) ) assert payload == { @@ -1063,15 +1111,11 @@ def test_serialize_embed_with_null_sub_fields(self, entity_factory_impl): "url": "https://some-url", "timestamp": "2020-05-29T20:37:22.865139", "color": 321321, - "footer": {}, - "image": {}, - "thumbnail": {}, - "author": {}, - "fields": [{"inline": False}], } + assert resources == [] def test_serialize_embed_with_null_attributes(self, entity_factory_impl): - assert entity_factory_impl.serialize_embed(embed_models.Embed()) == {} + assert entity_factory_impl.serialize_embed(embed_models.Embed()) == ({}, []) ################ # EMOJI MODELS # @@ -1675,7 +1719,7 @@ def test_deserialize_guild_with_null_fields(self, entity_factory_impl): ) assert guild.icon_hash is None assert guild.splash_hash is None - assert guild.discovery_splash_url is None + assert guild.discovery_splash_hash is None assert guild.afk_channel_id is None assert guild.embed_channel_id is None assert guild.application_id is None @@ -1976,17 +2020,20 @@ def test_deserialize_full_message( assert attachment.height == 2638 assert isinstance(attachment, message_models.Attachment) - assert message.embeds == [entity_factory_impl.deserialize_embed(embed_payload)] + expected_embed = entity_factory_impl.deserialize_embed(embed_payload) + assert message.embeds == [expected_embed] # Reaction reaction = message.reactions[0] assert reaction.count == 100 assert reaction.is_reacted_by_me is True - assert reaction.emoji == entity_factory_impl.deserialize_emoji(custom_emoji_payload) + expected_emoji = entity_factory_impl.deserialize_emoji(custom_emoji_payload) + assert reaction.emoji == expected_emoji assert isinstance(reaction, message_models.Reaction) assert message.is_pinned is True assert message.webhook_id == 1234 assert message.type == message_models.MessageType.DEFAULT + # Activity assert message.activity.type == message_models.MessageActivityType.JOIN_REQUEST assert message.activity.party_id == "ae488379-351d-4a4f-ad32-2b9b01c91657" @@ -2628,9 +2675,11 @@ def test_deserialize_typing_start_event_with_unset_fields(self, entity_factory_i assert typing_start.guild_id is None assert typing_start.member is None - def test_deserialize_invite_create_event(self, entity_factory_impl, invite_payload): - invite_create = entity_factory_impl.deserialize_invite_create_event(invite_payload) - assert invite_create.invite == entity_factory_impl.deserialize_invite(invite_payload) + def test_deserialize_invite_create_event(self, entity_factory_impl, invite_with_metadata_payload): + invite_create = entity_factory_impl.deserialize_invite_create_event(invite_with_metadata_payload) + assert invite_create.invite == entity_factory_impl.deserialize_invite_with_metadata( + invite_with_metadata_payload + ) assert isinstance(invite_create, channel_events.InviteCreateEvent) @pytest.fixture() @@ -2882,10 +2931,10 @@ def test_deserialize_message_update_event_with_full_payload( assert isinstance(message_update, message_events.MessageUpdateEvent) def test_deserialize_message_update_event_with_partial_payload(self, entity_factory_impl): - message_update = entity_factory_impl.deserialize_message_update_event({"id": "42424242"}) + message_update = entity_factory_impl.deserialize_message_update_event({"id": "42424242", "channel_id": "99420"}) assert message_update.message.id == 42424242 - assert message_update.message.channel_id is undefined.UNDEFINED + assert message_update.message.channel_id == 99420 assert message_update.message.guild_id is undefined.UNDEFINED assert message_update.message.author is undefined.UNDEFINED assert message_update.message.member is undefined.UNDEFINED @@ -2911,7 +2960,7 @@ def test_deserialize_message_update_event_with_partial_payload(self, entity_fact def test_deserialize_message_update_event_with_null_fields(self, entity_factory_impl): message_update = entity_factory_impl.deserialize_message_update_event( - {"id": "42424242", "edited_timestamp": None} + {"id": "42424242", "edited_timestamp": None, "channel_id": "69420"} ) assert message_update.message.edited_timestamp is None diff --git a/tests/hikari/models/.gitkeep b/tests/hikari/models/.gitkeep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/hikari/models/__init__.py b/tests/hikari/models/__init__.py deleted file mode 100644 index 1c1502a5ca..0000000000 --- a/tests/hikari/models/__init__.py +++ /dev/null @@ -1,18 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# Copyright © Nekoka.tt 2019-2020 -# -# This file is part of Hikari. -# -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . diff --git a/tests/hikari/net/__init__.py b/tests/hikari/net/__init__.py deleted file mode 100644 index 1c1502a5ca..0000000000 --- a/tests/hikari/net/__init__.py +++ /dev/null @@ -1,18 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# Copyright © Nekoka.tt 2019-2020 -# -# This file is part of Hikari. -# -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . diff --git a/tests/hikari/net/test_http_client.py b/tests/hikari/net/test_http_client.py index 898693a453..5de357d131 100644 --- a/tests/hikari/net/test_http_client.py +++ b/tests/hikari/net/test_http_client.py @@ -61,9 +61,9 @@ async def test_acquire_creates_new_session_if_one_does_not_exist(self, client): client._connector = mock.MagicMock() client._trust_env = mock.MagicMock() - _helpers.set_private_attr(client, "client_session", None) - cs = client._acquire_client_session() - assert _helpers.get_private_attr(client, "client_session") is cs + client._client_session = None + cs = client.client_session() + assert client._client_session is cs aiohttp.ClientSession.assert_called_once_with( connector=client._connector, trust_env=client._trust_env, @@ -73,25 +73,25 @@ async def test_acquire_creates_new_session_if_one_does_not_exist(self, client): ) async def test_acquire_repeated_calls_caches_client_session(self, client): - cs = client._acquire_client_session() + cs = client.client_session() for i in range(10): aiohttp.ClientSession.reset_mock() - assert cs is client._acquire_client_session() + assert cs is client.client_session() aiohttp.ClientSession.assert_not_called() @pytest.mark.asyncio class TestClose: async def test_close_when_not_running(self, client, client_session): - _helpers.set_private_attr(client, "client_session", None) + client._client_session = None await client.close() - assert _helpers.get_private_attr(client, "client_session") is None + assert client._client_session is None async def test_close_when_running(self, client, client_session): - _helpers.set_private_attr(client, "client_session", client_session) + client._client_session = client_session await client.close() - assert _helpers.get_private_attr(client, "client_session") is None + assert client._client_session is None client_session.close.assert_awaited_once_with() diff --git a/tests/hikari/net/test_user_agents.py b/tests/hikari/net/test_user_agents.py deleted file mode 100644 index c10e2388b9..0000000000 --- a/tests/hikari/net/test_user_agents.py +++ /dev/null @@ -1,39 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# Copyright © Nekoka.tt 2019-2020 -# -# This file is part of Hikari. -# -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . -from hikari.net import user_agents - - -def test_library_version_is_callable_and_produces_string(): - assert isinstance(user_agents.UserAgent().library_version, str) - - -def test_platform_version_is_callable_and_produces_string(): - assert isinstance(user_agents.UserAgent().platform_version, str) - - -def test_system_type_produces_string(): - assert isinstance(user_agents.UserAgent().system_type, str) - - -def test_websocket_triplet_produces_trio(): - assert user_agents.UserAgent().websocket_triplet == { - "$os": user_agents.UserAgent().system_type, - "$browser": user_agents.UserAgent().library_version, - "$device": user_agents.UserAgent().platform_version, - } diff --git a/tests/hikari/utilities/__init__.py b/tests/hikari/utilities/__init__.py deleted file mode 100644 index 1c1502a5ca..0000000000 --- a/tests/hikari/utilities/__init__.py +++ /dev/null @@ -1,18 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# Copyright © Nekoka.tt 2019-2020 -# -# This file is part of Hikari. -# -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . From e2061ff633a53ed7f49682f90a178f47742fd44b Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Sun, 14 Jun 2020 17:56:06 +0100 Subject: [PATCH 512/922] Fixes #370 and nukes PyLint for Flake8. Also simplifies CI in the process --- .flake8 | 22 ++++++ black_test.py | 0 ci.dockerfile | 4 +- ci/bandit.nox.py | 30 -------- ci/black.nox.py | 8 -- ci/build.nox.py | 46 ----------- ci/clang-tidy.nox.py | 41 ---------- ci/config.py | 13 +--- ci/deploy.nox.py | 1 - ci/docker.nox.py | 1 - ci/{pydocstyle.nox.py => flake8.nox.py} | 21 ++++-- ci/gitlab/linting.yml | 37 ++------- ci/mypy.nox.py | 3 +- ci/nox.py | 6 +- ci/pages.nox.py | 1 - ci/pdoc.nox.py | 3 +- ci/pip.nox.py | 1 - ci/pylint.nox.py | 66 ---------------- ci/pytest.nox.py | 3 +- ci/safety.nox.py | 3 +- ci/twemoji.nox.py | 1 - ci/utils.nox.py | 1 - dev-requirements.txt | 7 +- docs/config.mako | 2 +- docs/html.mako | 4 +- flake-requirements.txt | 28 +++++++ hikari/__init__.py | 3 +- hikari/__main__.py | 6 +- hikari/_about.py | 3 +- hikari/api/__init__.py | 1 - hikari/api/bot.py | 1 - hikari/api/cache.py | 1 - hikari/api/component.py | 1 - hikari/api/entity_factory.py | 1 - hikari/api/event_consumer.py | 1 - hikari/api/event_dispatcher.py | 1 - hikari/api/gateway_zookeeper.py | 1 - hikari/api/rest.py | 1 - hikari/errors.py | 3 +- hikari/events/__init__.py | 1 - hikari/events/base.py | 1 - hikari/events/channel.py | 1 - hikari/events/guild.py | 1 - hikari/events/message.py | 1 - hikari/events/other.py | 1 - hikari/events/voice.py | 1 - hikari/impl/__init__.py | 1 - hikari/impl/bot.py | 3 +- hikari/impl/cache.py | 1 - hikari/impl/entity_factory.py | 1 - hikari/impl/event_manager.py | 1 - hikari/impl/event_manager_core.py | 1 - hikari/impl/gateway_zookeeper.py | 1 - hikari/impl/rest.py | 1 - hikari/models/__init__.py | 1 - hikari/models/applications.py | 1 - hikari/models/audit_logs.py | 1 - hikari/models/bases.py | 3 +- hikari/models/channels.py | 1 - hikari/models/colors.py | 3 +- hikari/models/colours.py | 3 +- hikari/models/embeds.py | 1 - hikari/models/emojis.py | 1 - hikari/models/gateway.py | 1 - hikari/models/guilds.py | 1 - hikari/models/intents.py | 1 - hikari/models/invites.py | 1 - hikari/models/messages.py | 1 - hikari/models/permissions.py | 1 - hikari/models/presences.py | 1 - hikari/models/users.py | 1 - hikari/models/voices.py | 1 - hikari/models/webhooks.py | 1 - hikari/net/__init__.py | 1 - hikari/net/buckets.py | 1 - hikari/net/gateway.py | 1 - hikari/net/http_client.py | 1 - hikari/net/http_settings.py | 1 - hikari/net/iterators.py | 1 - hikari/net/rate_limits.py | 5 +- hikari/net/rest.py | 1 - hikari/net/rest_utils.py | 1 - hikari/net/routes.py | 1 - hikari/net/strings.py | 1 - hikari/net/tracing.py | 1 - hikari/utilities/__init__.py | 3 +- hikari/utilities/aio.py | 3 +- hikari/utilities/cdn.py | 3 +- hikari/utilities/data_binding.py | 1 - hikari/utilities/date.py | 1 - hikari/utilities/files.py | 1 - hikari/utilities/reflect.py | 1 - hikari/utilities/snowflake.py | 1 - hikari/utilities/undefined.py | 1 - mypy-requirements.txt | 1 + noxfile.py | 1 - pydocstyle.ini | 4 - scripts/test_twemoji_mapping.py | 1 - setup.py | 84 +-------------------- tests/hikari/__init__.py | 1 - tests/hikari/_helpers.py | 1 - tests/hikari/impl/test_entity_factory.py | 1 - tests/hikari/net/test_buckets.py | 1 - tests/hikari/net/test_gateway.py | 1 - tests/hikari/net/test_http_client.py | 1 - tests/hikari/net/test_iterators.py | 1 - tests/hikari/net/test_ratelimits.py | 1 - tests/hikari/net/test_rest.py | 1 - tests/hikari/net/test_rest_utils.py | 1 - tests/hikari/net/test_routes.py | 1 - tests/hikari/net/test_tracing.py | 1 - tests/hikari/net/test_urls.py | 1 - tests/hikari/utilities/test_aio.py | 3 +- tests/hikari/utilities/test_cdn.py | 1 - tests/hikari/utilities/test_data_binding.py | 1 - tests/hikari/utilities/test_date.py | 1 - tests/hikari/utilities/test_reflect.py | 1 - tests/hikari/utilities/test_snowflake.py | 1 - tests/hikari/utilities/test_undefined.py | 1 - 119 files changed, 109 insertions(+), 454 deletions(-) create mode 100644 .flake8 delete mode 100644 black_test.py delete mode 100644 ci/bandit.nox.py delete mode 100644 ci/build.nox.py delete mode 100644 ci/clang-tidy.nox.py rename ci/{pydocstyle.nox.py => flake8.nox.py} (54%) delete mode 100644 ci/pylint.nox.py create mode 100644 flake-requirements.txt create mode 100644 mypy-requirements.txt delete mode 100644 pydocstyle.ini diff --git a/.flake8 b/.flake8 new file mode 100644 index 0000000000..3bcc3dde42 --- /dev/null +++ b/.flake8 @@ -0,0 +1,22 @@ +[flake8] +count = true + +ignore = + E402, # Module level import not at top of file (isn't compatible with our import style). + D105, # Magic methods not having a docstring + D102, # Missing docstring in public method + +per-file-ignores = + # f-string missing prefix. + hikari/net/routes.py:FS003 + # complaints about importing stuff and not using it afterwards + hikari/__init__.py:F401 + +max-complexity = 15 +max-line-length = 120 +show_source = False +statistics = False + +accept-encodings = utf-8 + +docstring-convention = numpy diff --git a/black_test.py b/black_test.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/ci.dockerfile b/ci.dockerfile index 82969804bd..a5c87ff651 100644 --- a/ci.dockerfile +++ b/ci.dockerfile @@ -1,4 +1,4 @@ -FROM python:3.8.2 +FROM python:3.8.3 COPY . . -RUN pip install nox 'virtualenv<20.0.19' +RUN pip install nox RUN nox diff --git a/ci/bandit.nox.py b/ci/bandit.nox.py deleted file mode 100644 index 19ba850083..0000000000 --- a/ci/bandit.nox.py +++ /dev/null @@ -1,30 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# Copyright © Nekoka.tt 2019-2020 -# -# This file is part of Hikari. -# -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . -"""Static application security testing.""" - -from ci import config -from ci import nox - - -# Do not reuse venv, download new definitions each run. -@nox.session(reuse_venv=False, default=True) -def bandit(session: nox.Session) -> None: - """Run static application security tests.""" - session.install("bandit") - session.run("bandit", config.MAIN_PACKAGE, "-r") diff --git a/ci/black.nox.py b/ci/black.nox.py index aea107b4ac..6aaedf5501 100644 --- a/ci/black.nox.py +++ b/ci/black.nox.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python3 # -*- coding: utf-8 -*- # Copyright © Nekoka.tt 2019-2020 # @@ -36,10 +35,3 @@ def reformat_code(session: nox.Session) -> None: """Run black code formatter.""" session.install("black") session.run("black", *PATHS) - - -@nox.session(reuse_venv=True) -def check_formatting(session: nox.Session) -> None: - """Check that the code matches the black code style.""" - session.install("black") - session.run("black", *PATHS, "--check") diff --git a/ci/build.nox.py b/ci/build.nox.py deleted file mode 100644 index 70f0713c45..0000000000 --- a/ci/build.nox.py +++ /dev/null @@ -1,46 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# Copyright © Nekoka.tt 2019-2020 -# -# This file is part of Hikari. -# -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . -import os -import shutil - -from ci import config -from ci import nox - - -@nox.session(reuse_venv=True) -@nox.inherit_environment_vars -def build_ext(session: nox.Session) -> None: - """Compile C++ extensions in-place.""" - session.run("python", "setup.py", "build_ext", "--inplace") - - -@nox.session(reuse_venv=True) -@nox.inherit_environment_vars -def clean_ext(session: nox.Session) -> None: - """Clean any compiled C++ extensions.""" - print("rm", "build", "-r") - shutil.rmtree("build", ignore_errors=True) - print("rm", "dist", "-r") - shutil.rmtree("dist", ignore_errors=True) - for parent, _, files in os.walk(config.MAIN_PACKAGE): - for file in files: - if file.endswith((".so", ".pyd", ".lib", ".dll", ".PYD", ".DLL")): - path = os.path.join(parent, file) - print("rm", path) - os.remove(path) diff --git a/ci/clang-tidy.nox.py b/ci/clang-tidy.nox.py deleted file mode 100644 index 8625a9dbba..0000000000 --- a/ci/clang-tidy.nox.py +++ /dev/null @@ -1,41 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# Copyright © Nekoka.tt 2019-2020 -# -# This file is part of Hikari. -# -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . -"""Clang-tidy.""" -import subprocess - -from ci import config -from ci import nox - - -def _clang_tidy(*args): - invocation = f"clang-tidy $(find {config.MAIN_PACKAGE} -name '*.c' -o -name '*.h') " - invocation += " ".join(args) - print(subprocess.check_output(invocation, shell=True)) - - -@nox.session(reuse_venv=True) -def clang_tidy_check(session: nox.Session) -> None: - """Check C and C++ sources match the correct format for this library.""" - _clang_tidy() - - -@nox.session(reuse_venv=True) -def clang_tidy_fix(session: nox.Session) -> None: - """Reformat C and C++ sources.""" - _clang_tidy("--fix") diff --git a/ci/config.py b/ci/config.py index 47bc401367..555dad496a 100644 --- a/ci/config.py +++ b/ci/config.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python3 # -*- coding: utf-8 -*- # Copyright © Nekoka.tt 2019-2020 # @@ -20,10 +19,6 @@ IS_CI = "CI" in _os.environ -# PyPI dependencies -REQUIREMENTS = "requirements.txt" -DEV_REQUIREMENTS = "dev-requirements.txt" - # Packaging MAIN_PACKAGE = "hikari" TEST_PACKAGE = "tests" @@ -35,13 +30,12 @@ ROOT_INDEX_SOURCE = "index.html" # Linting and test configs. +FLAKE8_CODECLIMATE = "public/flake8.json" +FLAKE8_HTML = "public/flake8" +FLAKE8_TXT = "public/flake8.txt" MYPY_INI = "mypy.ini" MYPY_JUNIT_OUTPUT_PATH = _os.path.join(ARTIFACT_DIRECTORY, "mypy.xml") PYDOCSTYLE_INI = "pydocstyle.ini" -PYLINT_INI = "pylint.ini" -PYLINT_JUNIT_OUTPUT_PATH = _os.path.join(ARTIFACT_DIRECTORY, "pylint.xml") -PYLINT_JSON_OUTPUT_PATH = _os.path.join(ARTIFACT_DIRECTORY, "pylint.json") -PYLINT_HTML_OUTPUT_PATH = _os.path.join(ARTIFACT_DIRECTORY, "pylint.html") PYTEST_INI = "pytest.ini" PYTEST_HTML_OUTPUT_PATH = _os.path.join(ARTIFACT_DIRECTORY, "pytest.html") COVERAGE_HTML_PATH = _os.path.join(ARTIFACT_DIRECTORY, "coverage", "html") @@ -76,5 +70,6 @@ "python:3.8.0", "python:3.8.1", "python:3.8.2", + "python:3.8.3", "python:3.9-rc", ] diff --git a/ci/deploy.nox.py b/ci/deploy.nox.py index 9e0375a172..69ccecda89 100644 --- a/ci/deploy.nox.py +++ b/ci/deploy.nox.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python3 # -*- coding: utf-8 -*- # Copyright © Nekoka.tt 2019-2020 # diff --git a/ci/docker.nox.py b/ci/docker.nox.py index def2aadc38..682e3ceec2 100644 --- a/ci/docker.nox.py +++ b/ci/docker.nox.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python3 # -*- coding: utf-8 -*- # Copyright © Nekoka.tt 2019-2020 # diff --git a/ci/pydocstyle.nox.py b/ci/flake8.nox.py similarity index 54% rename from ci/pydocstyle.nox.py rename to ci/flake8.nox.py index d34b3d99b3..907fd97c5c 100644 --- a/ci/pydocstyle.nox.py +++ b/ci/flake8.nox.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python3 # -*- coding: utf-8 -*- # Copyright © Nekoka.tt 2019-2020 # @@ -22,10 +21,16 @@ from ci import nox -@nox.session(default=True, reuse_venv=True) -def pydocstyle(session: nox.Session) -> None: - """Check documentation is formatted correctly.""" - session.install("pydocstyle") - session.chdir(config.MAIN_PACKAGE) - ini = os.path.join(os.path.pardir, config.PYDOCSTYLE_INI) - session.run("pydocstyle", "--config", ini) +@nox.session(reuse_venv=True, default=True) +def flake8(session: nox.Session) -> None: + session.install("-r", "requirements.txt", "-r", "flake-requirements.txt") + + if "GITLAB_CI" in os.environ or "--gitlab" in session.posargs: + print("Detected GitLab, will output CodeClimate report instead!") + format_args = ["--format=gl-codeclimate", f"--output-file={config.FLAKE8_CODECLIMATE}"] + else: + format_args = [f"--output-file={config.FLAKE8_TXT}", "--statistics", "--show-source"] + + session.run( + "flake8", "--exit-zero", "--format=html", f"--htmldir={config.FLAKE8_HTML}", *format_args, config.MAIN_PACKAGE, + ) diff --git a/ci/gitlab/linting.yml b/ci/gitlab/linting.yml index 886c6789f3..d05d80a6a5 100644 --- a/ci/gitlab/linting.yml +++ b/ci/gitlab/linting.yml @@ -30,45 +30,20 @@ retry: 0 stage: test + ### -### Code linting. -### -### Looks for code style problems, code smells, and general bad things. -### Failures are added as warnings to the pipeline. +### Flake8 linter. ### -pylint: +flake8: allow_failure: true artifacts: paths: - - public/pylint.html + - public/ reports: - junit: public/pylint.xml - when: always - extends: .lint - script: - - nox -s pylint-junit --no-error-on-external-run - -### -### Documentation linting. -### -### If the documentation is clear and formatted correctly. Any failures -### become warnings on the pipeline. -### -pydocstyle: - allow_failure: true - extends: .lint - script: - - nox -s pydocstyle --no-error-on-external-run - -### -### Code formatting. -### -### If the code style is not the Black code style, fail the pipeline. -### -black: + codequality: public/flake8.json extends: .lint script: - - nox -s check-formatting --no-error-on-external-run -- --check --diff + - nox -s flake8 --no-error-on-external-run ### ### Security checks. diff --git a/ci/mypy.nox.py b/ci/mypy.nox.py index 7fc092f74c..3dc67ce548 100644 --- a/ci/mypy.nox.py +++ b/ci/mypy.nox.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python3 # -*- coding: utf-8 -*- # Copyright © Nekoka.tt 2019-2020 # @@ -23,7 +22,7 @@ @nox.session(reuse_venv=True, default=True) def mypy(session: nox.Session) -> None: - session.install("-r", "requirements.txt", "mypy==0.780") + session.install("-r", "requirements.txt", "-r", "mypy-requirements.txt") session.run( "mypy", "-p", config.MAIN_PACKAGE, "--config", config.MYPY_INI, "--junit-xml", config.MYPY_JUNIT_OUTPUT_PATH, ) diff --git a/ci/nox.py b/ci/nox.py index e723883444..82e9f18d85 100644 --- a/ci/nox.py +++ b/ci/nox.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python3 # -*- coding: utf-8 -*- # Copyright © Nekoka.tt 2019-2020 # @@ -26,6 +25,7 @@ from nox import session as _session from nox import options as _options +from ci import config _options.sessions = [] @@ -55,3 +55,7 @@ def shell(arg, *args): command = " ".join((arg, *args)) print("\033[35mnox > shell >\033[0m", command) return subprocess.check_call(command, shell=True) + + +if not os.path.isdir(config.ARTIFACT_DIRECTORY): + os.mkdir(config.ARTIFACT_DIRECTORY) diff --git a/ci/pages.nox.py b/ci/pages.nox.py index a0b4f3f5f2..c2b37c2914 100644 --- a/ci/pages.nox.py +++ b/ci/pages.nox.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python3 # -*- coding: utf-8 -*- # Copyright © Nekoka.tt 2019-2020 # diff --git a/ci/pdoc.nox.py b/ci/pdoc.nox.py index 5f0f4260fa..189c6388da 100644 --- a/ci/pdoc.nox.py +++ b/ci/pdoc.nox.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python3 # -*- coding: utf-8 -*- # Copyright © Nekoka.tt 2019-2020 # @@ -25,7 +24,7 @@ @nox.inherit_environment_vars def pdoc(session: nox.Session) -> None: """Generate documentation with pdoc.""" - session.install("-r", config.REQUIREMENTS) + session.install("-r", "requirements.txt") session.install("pdoc3") session.install("sphobjinv") diff --git a/ci/pip.nox.py b/ci/pip.nox.py index dc4f2b84d7..a68b750961 100644 --- a/ci/pip.nox.py +++ b/ci/pip.nox.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python3 # -*- coding: utf-8 -*- # Copyright © Nekoka.tt 2019-2020 # diff --git a/ci/pylint.nox.py b/ci/pylint.nox.py deleted file mode 100644 index d04839bd1c..0000000000 --- a/ci/pylint.nox.py +++ /dev/null @@ -1,66 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# Copyright © Nekoka.tt 2019-2020 -# -# This file is part of Hikari. -# -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . -"""Pylint support.""" -import os -import traceback -from ci import config -from ci import nox - -FLAGS = ["pylint", config.MAIN_PACKAGE, "--rcfile", config.PYLINT_INI] -PYLINT_VER = "pylint==2.5.2" -PYLINT_JUNIT_VER = "pylint-junit==0.2.0" -SUCCESS_CODES = list(range(0, 256)) - - -@nox.session(default=True, reuse_venv=True) -def pylint(session: nox.Session) -> None: - """Run pylint against the code base and report any code smells or issues.""" - - session.install("-r", config.REQUIREMENTS, "-r", config.DEV_REQUIREMENTS, PYLINT_VER) - - try: - print("generating plaintext report") - session.run(*FLAGS, *session.posargs, success_codes=SUCCESS_CODES) - except Exception: - traceback.print_exc() - - -@nox.session(default=False, reuse_venv=True) -def pylint_junit(session: nox.Session) -> None: - """Runs `pylint', but produces JUnit reports instead of textual ones.""" - - session.install( - "-r", config.REQUIREMENTS, "-r", config.DEV_REQUIREMENTS, PYLINT_VER, PYLINT_JUNIT_VER, - ) - - try: - print("generating plaintext report") - if not os.path.exists(config.ARTIFACT_DIRECTORY): - os.mkdir(config.ARTIFACT_DIRECTORY) - - with open(config.PYLINT_JUNIT_OUTPUT_PATH, "w+") as fp: - session.run( - *FLAGS, - "--output-format=pylint_junit.JUnitReporter", - *session.posargs, - success_codes=SUCCESS_CODES, - stdout=fp, - ) - except Exception: - traceback.print_exc() diff --git a/ci/pytest.nox.py b/ci/pytest.nox.py index 36c23578f4..9833694c61 100644 --- a/ci/pytest.nox.py +++ b/ci/pytest.nox.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python3 # -*- coding: utf-8 -*- # Copyright © Nekoka.tt 2019-2020 # @@ -50,7 +49,7 @@ def pytest(session: nox.Session) -> None: """Run unit tests and measure code coverage.""" session.install( - "-r", config.REQUIREMENTS, "-r", config.DEV_REQUIREMENTS, + "-r", "requirements.txt", "-r", "dev-requirements.txt", ) shutil.rmtree(".coverage", ignore_errors=True) session.run("python", "-m", "pytest", *FLAGS) diff --git a/ci/safety.nox.py b/ci/safety.nox.py index 33f1b268bc..4fd4437a2e 100644 --- a/ci/safety.nox.py +++ b/ci/safety.nox.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python3 # -*- coding: utf-8 -*- # Copyright © Nekoka.tt 2019-2020 # @@ -25,5 +24,5 @@ @nox.session(reuse_venv=True, default=True) def safety(session: nox.Session) -> None: """Perform dependency scanning.""" - session.install("safety", "-Ur", config.REQUIREMENTS) + session.install("safety", "-Ur", "requirements.txt") session.run("safety", "check", "--full-report") diff --git a/ci/twemoji.nox.py b/ci/twemoji.nox.py index 097b5b59d7..f50ee21519 100644 --- a/ci/twemoji.nox.py +++ b/ci/twemoji.nox.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python3 # -*- coding: utf-8 -*- # Copyright © Nekoka.tt 2019-2020 # diff --git a/ci/utils.nox.py b/ci/utils.nox.py index 39b9fbe108..6f1398e0c7 100644 --- a/ci/utils.nox.py +++ b/ci/utils.nox.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python3 # -*- coding: utf-8 -*- # Copyright © Nekoka.tt 2019-2020 # diff --git a/dev-requirements.txt b/dev-requirements.txt index 73b35d01d8..91ad81aa64 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -1,9 +1,10 @@ async-timeout~=3.0.1 coverage~=5.1 nox==2020.5.24 -virtualenv==20.0.21 +mock~=4.0.2 pytest==5.4.3 pytest-asyncio==0.12.0 -pytest-cov==2.9.0 +pytest-cov==2.10.0 pytest-testdox==1.2.1 -mock~=4.0.2 +virtualenv==20.0.23 + diff --git a/docs/config.mako b/docs/config.mako index 848c3aa05c..2865b56ce1 100644 --- a/docs/config.mako +++ b/docs/config.mako @@ -1,4 +1,4 @@ -## Copyright © Nekokatt 2019-2020 +## Copyright © Nekoka.tt 2019-2020 ## ## This file is part of Hikari. ## diff --git a/docs/html.mako b/docs/html.mako index 8308d0c84a..2590fefba0 100644 --- a/docs/html.mako +++ b/docs/html.mako @@ -1,4 +1,4 @@ -## Copyright © Nekokatt 2019-2020 +## Copyright © Nekoka.tt 2019-2020 ## ## This file is part of Hikari. ## @@ -95,4 +95,4 @@ - \ No newline at end of file + diff --git a/flake-requirements.txt b/flake-requirements.txt new file mode 100644 index 0000000000..86f49ad140 --- /dev/null +++ b/flake-requirements.txt @@ -0,0 +1,28 @@ +flake8~=3.8.3 + +# Plugins +# Ref: https://github.com/DmytroLitvinov/awesome-flake8-extensions + +flake8-bandit~=2.1.2 # runs bandit +flake8-black~=0.2.0 # runs black +flake8-broken-line~=0.2.0 # forbey "\" linebreaks +flake8-builtins~=1.5.3 # builtin shadowing checks +flake8-coding~=1.3.2 # coding magic-comment detectiong +# flake8-commas~=2.0.0 # enforce trailing commas +flake8-comprehensions~=3.2.3 # comprehension checks +flake8-deprecated~=1.3 # deprecated call checks +flake8-docstrings~=1.5.0 # pydocstyle support +flake8-eradicate~=0.4.0 # find commented out code +flake8-executable~=2.0.3 # shebangs +flake8-fixme~=1.1.1 # "fix me" counter +flake8-functions~=0.0.4 # function linting +flake8-gl-codeclimate~=0.1.4 # gitlab codeclimate output format +flake8-html~=0.4.1 # html output +flake8-if-statements~=0.1.0 # condition linting +flake8-mutable~=1.2.0 # mutable default argument detection +flake8-pep3101~=1.3.0 # new-style format strings only +flake8-print~=3.1.4 # complain about print statements in code +flake8-printf-formatting~=1.1.0 # forbey printf-style python2 string formatting +flake8-pytest-style~=1.2.0 # pytest checks +flake8-raise~=0.0.5 # exception raising linting +flake8-use-fstring~=1.1 # format string checking diff --git a/hikari/__init__.py b/hikari/__init__.py index fd16e38ada..64d62c239a 100644 --- a/hikari/__init__.py +++ b/hikari/__init__.py @@ -1,6 +1,5 @@ -#!/usr/bin/env python3 # -*- coding: utf-8 -*- -# Copyright © Nekokatt 2019-2020 +# Copyright © Nekoka.tt 2019-2020 # # This file is part of Hikari. # diff --git a/hikari/__main__.py b/hikari/__main__.py index 0b1b2a053a..4139f17d6b 100644 --- a/hikari/__main__.py +++ b/hikari/__main__.py @@ -1,6 +1,5 @@ -#!/usr/bin/env python3 # -*- coding: utf-8 -*- -# Copyright © Nekokatt 2019-2020 +# Copyright © Nekoka.tt 2019-2020 # # This file is part of Hikari. # @@ -24,6 +23,7 @@ import inspect import os import platform +import sys # noinspection PyUnresolvedReferences import typing @@ -36,4 +36,4 @@ py_impl: typing.Final[str] = platform.python_implementation() py_ver: typing.Final[str] = platform.python_version() py_compiler: typing.Final[str] = platform.python_compiler() -print(f"hikari v{version} (installed in {path}) ({py_impl} {py_ver} {py_compiler})") +sys.stderr.write(f"hikari v{version} (installed in {path}) ({py_impl} {py_ver} {py_compiler})\n") diff --git a/hikari/_about.py b/hikari/_about.py index 2b14a70b61..79e4fd1a05 100644 --- a/hikari/_about.py +++ b/hikari/_about.py @@ -1,6 +1,5 @@ -#!/usr/bin/env python3 # -*- coding: utf-8 -*- -# Copyright © Nekokatt 2019-2020 +# Copyright © Nekoka.tt 2019-2020 # # This file is part of Hikari. # diff --git a/hikari/api/__init__.py b/hikari/api/__init__.py index 1445a927c3..e45ad286f5 100644 --- a/hikari/api/__init__.py +++ b/hikari/api/__init__.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python3 # -*- coding: utf-8 -*- # Copyright © Nekoka.tt 2019-2020 # diff --git a/hikari/api/bot.py b/hikari/api/bot.py index 6de88a020f..3374ae167b 100644 --- a/hikari/api/bot.py +++ b/hikari/api/bot.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python3 # -*- coding: utf-8 -*- # Copyright © Nekoka.tt 2019-2020 # diff --git a/hikari/api/cache.py b/hikari/api/cache.py index 9a1fea6c32..8668684ba8 100644 --- a/hikari/api/cache.py +++ b/hikari/api/cache.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python3 # -*- coding: utf-8 -*- # Copyright © Nekoka.tt 2019-2020 # diff --git a/hikari/api/component.py b/hikari/api/component.py index 9635abd367..59e4111bfd 100644 --- a/hikari/api/component.py +++ b/hikari/api/component.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python3 # -*- coding: utf-8 -*- # Copyright © Nekoka.tt 2019-2020 # diff --git a/hikari/api/entity_factory.py b/hikari/api/entity_factory.py index 919d04de3d..517e03308b 100644 --- a/hikari/api/entity_factory.py +++ b/hikari/api/entity_factory.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python3 # -*- coding: utf-8 -*- # Copyright © Nekoka.tt 2019-2020 # diff --git a/hikari/api/event_consumer.py b/hikari/api/event_consumer.py index 66b3c385c9..4ca259f35e 100644 --- a/hikari/api/event_consumer.py +++ b/hikari/api/event_consumer.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python3 # -*- coding: utf-8 -*- # Copyright © Nekoka.tt 2019-2020 # diff --git a/hikari/api/event_dispatcher.py b/hikari/api/event_dispatcher.py index 08b4c2351b..1ebd7d3115 100644 --- a/hikari/api/event_dispatcher.py +++ b/hikari/api/event_dispatcher.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python3 # -*- coding: utf-8 -*- # Copyright © Nekoka.tt 2019-2020 # diff --git a/hikari/api/gateway_zookeeper.py b/hikari/api/gateway_zookeeper.py index 1606f7458a..1806c786db 100644 --- a/hikari/api/gateway_zookeeper.py +++ b/hikari/api/gateway_zookeeper.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python3 # -*- coding: utf-8 -*- # Copyright © Nekoka.tt 2019-2020 # diff --git a/hikari/api/rest.py b/hikari/api/rest.py index d735257247..df915f0aa5 100644 --- a/hikari/api/rest.py +++ b/hikari/api/rest.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python3 # -*- coding: utf-8 -*- # Copyright © Nekoka.tt 2019-2020 # diff --git a/hikari/errors.py b/hikari/errors.py index 4a64677146..4b9bd87e83 100644 --- a/hikari/errors.py +++ b/hikari/errors.py @@ -1,6 +1,5 @@ -#!/usr/bin/env python3 # -*- coding: utf-8 -*- -# Copyright © Nekokatt 2019-2020 +# Copyright © Nekoka.tt 2019-2020 # # This file is part of Hikari. # diff --git a/hikari/events/__init__.py b/hikari/events/__init__.py index e4044a59c0..1aaf887951 100644 --- a/hikari/events/__init__.py +++ b/hikari/events/__init__.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python3 # -*- coding: utf-8 -*- # Copyright © Nekoka.tt 2019-2020 # diff --git a/hikari/events/base.py b/hikari/events/base.py index fa633541f1..0648ab1b21 100644 --- a/hikari/events/base.py +++ b/hikari/events/base.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python3 # -*- coding: utf-8 -*- # Copyright © Nekoka.tt 2019-2020 # diff --git a/hikari/events/channel.py b/hikari/events/channel.py index 1c0b0c5449..1d7435610f 100644 --- a/hikari/events/channel.py +++ b/hikari/events/channel.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python3 # -*- coding: utf-8 -*- # Copyright © Nekoka.tt 2019-2020 # diff --git a/hikari/events/guild.py b/hikari/events/guild.py index 249c378b55..14913c3e42 100644 --- a/hikari/events/guild.py +++ b/hikari/events/guild.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python3 # -*- coding: utf-8 -*- # Copyright © Nekoka.tt 2019-2020 # diff --git a/hikari/events/message.py b/hikari/events/message.py index 42ce9c98e2..859d094b5a 100644 --- a/hikari/events/message.py +++ b/hikari/events/message.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python3 # -*- coding: utf-8 -*- # Copyright © Nekoka.tt 2019-2020 # diff --git a/hikari/events/other.py b/hikari/events/other.py index f60dea57a8..6a77b1681b 100644 --- a/hikari/events/other.py +++ b/hikari/events/other.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python3 # -*- coding: utf-8 -*- # Copyright © Nekoka.tt 2019-2020 # diff --git a/hikari/events/voice.py b/hikari/events/voice.py index 7099a2ba00..d02e97bc43 100644 --- a/hikari/events/voice.py +++ b/hikari/events/voice.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python3 # -*- coding: utf-8 -*- # Copyright © Nekoka.tt 2019-2020 # diff --git a/hikari/impl/__init__.py b/hikari/impl/__init__.py index 0524a63b4c..d2a6a355f1 100644 --- a/hikari/impl/__init__.py +++ b/hikari/impl/__init__.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python3 # -*- coding: utf-8 -*- # Copyright © Nekoka.tt 2019-2020 # diff --git a/hikari/impl/bot.py b/hikari/impl/bot.py index d34a12a71d..e56af5511a 100644 --- a/hikari/impl/bot.py +++ b/hikari/impl/bot.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python3 # -*- coding: utf-8 -*- # Copyright © Nekoka.tt 2019-2020 # @@ -183,7 +182,7 @@ def __init__( self._config = config self._event_manager = event_manager.EventManagerImpl(app=self) self._entity_factory = entity_factory_impl.EntityFactoryComponentImpl(app=self) - self._rest = rest.REST( # nosec + self._rest = rest.REST( # noqa S106 possible hardcoded password app=self, config=config, debug=debug, diff --git a/hikari/impl/cache.py b/hikari/impl/cache.py index b24377d9b1..43f85f2dbd 100644 --- a/hikari/impl/cache.py +++ b/hikari/impl/cache.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python3 # -*- coding: utf-8 -*- # Copyright © Nekoka.tt 2019-2020 # diff --git a/hikari/impl/entity_factory.py b/hikari/impl/entity_factory.py index 812511b901..3b9f72b302 100644 --- a/hikari/impl/entity_factory.py +++ b/hikari/impl/entity_factory.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python3 # -*- coding: utf-8 -*- # Copyright © Nekoka.tt 2019-2020 # diff --git a/hikari/impl/event_manager.py b/hikari/impl/event_manager.py index 72e2e30c0f..967fd89ff1 100644 --- a/hikari/impl/event_manager.py +++ b/hikari/impl/event_manager.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python3 # -*- coding: utf-8 -*- # Copyright © Nekoka.tt 2019-2020 # diff --git a/hikari/impl/event_manager_core.py b/hikari/impl/event_manager_core.py index 6352590410..a018f69f64 100644 --- a/hikari/impl/event_manager_core.py +++ b/hikari/impl/event_manager_core.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python3 # -*- coding: utf-8 -*- # Copyright © Nekoka.tt 2019-2020 # diff --git a/hikari/impl/gateway_zookeeper.py b/hikari/impl/gateway_zookeeper.py index 35239e925b..7f831d75ea 100644 --- a/hikari/impl/gateway_zookeeper.py +++ b/hikari/impl/gateway_zookeeper.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python3 # -*- coding: utf-8 -*- # Copyright © Nekoka.tt 2019-2020 # diff --git a/hikari/impl/rest.py b/hikari/impl/rest.py index 428ef7f626..bcce26676c 100644 --- a/hikari/impl/rest.py +++ b/hikari/impl/rest.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python3 # -*- coding: utf-8 -*- # Copyright © Nekoka.tt 2019-2020 # diff --git a/hikari/models/__init__.py b/hikari/models/__init__.py index 66facd58d7..d2b482a62c 100644 --- a/hikari/models/__init__.py +++ b/hikari/models/__init__.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python3 # -*- coding: utf-8 -*- # Copyright © Nekoka.tt 2019-2020 # diff --git a/hikari/models/applications.py b/hikari/models/applications.py index ff45883bbf..587c0bb864 100644 --- a/hikari/models/applications.py +++ b/hikari/models/applications.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python3 # -*- coding: utf-8 -*- # Copyright © Nekoka.tt 2019-2020 # diff --git a/hikari/models/audit_logs.py b/hikari/models/audit_logs.py index a60e06eb09..142fb6a479 100644 --- a/hikari/models/audit_logs.py +++ b/hikari/models/audit_logs.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python3 # -*- coding: utf-8 -*- # Copyright © Nekoka.tt 2019-2020 # diff --git a/hikari/models/bases.py b/hikari/models/bases.py index 1ae83f1bf6..babc395f94 100644 --- a/hikari/models/bases.py +++ b/hikari/models/bases.py @@ -1,6 +1,5 @@ -#!/usr/bin/env python3 # -*- coding: utf-8 -*- -# Copyright © Nekokatt 2019-2020 +# Copyright © Nekoka.tt 2019-2020 # # This file is part of Hikari. # diff --git a/hikari/models/channels.py b/hikari/models/channels.py index e61a1526f7..9d49ce2efb 100644 --- a/hikari/models/channels.py +++ b/hikari/models/channels.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python3 # -*- coding: utf-8 -*- # Copyright © Nekoka.tt 2019-2020 # diff --git a/hikari/models/colors.py b/hikari/models/colors.py index 8704ae4452..e8f3f4d41f 100644 --- a/hikari/models/colors.py +++ b/hikari/models/colors.py @@ -1,6 +1,5 @@ -#!/usr/bin/env python3 # -*- coding: utf-8 -*- -# Copyright © Nekokatt 2019-2020 +# Copyright © Nekoka.tt 2019-2020 # # This file is part of Hikari. # diff --git a/hikari/models/colours.py b/hikari/models/colours.py index 8499e57006..6a065dd2e9 100644 --- a/hikari/models/colours.py +++ b/hikari/models/colours.py @@ -1,6 +1,5 @@ -#!/usr/bin/env python3 # -*- coding: utf-8 -*- -# Copyright © Nekokatt 2019-2020 +# Copyright © Nekoka.tt 2019-2020 # # This file is part of Hikari. # diff --git a/hikari/models/embeds.py b/hikari/models/embeds.py index 1ff64850dd..fa4ca3d1f5 100644 --- a/hikari/models/embeds.py +++ b/hikari/models/embeds.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python3 # -*- coding: utf-8 -*- # Copyright © Nekoka.tt 2019-2020 # diff --git a/hikari/models/emojis.py b/hikari/models/emojis.py index 4f1e04473e..c45cc1126b 100644 --- a/hikari/models/emojis.py +++ b/hikari/models/emojis.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python3 # -*- coding: utf-8 -*- # Copyright © Nekoka.tt 2019-2020 # diff --git a/hikari/models/gateway.py b/hikari/models/gateway.py index 99c08788f5..98457a03a3 100644 --- a/hikari/models/gateway.py +++ b/hikari/models/gateway.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python3 # -*- coding: utf-8 -*- # Copyright © Nekoka.tt 2019-2020 # diff --git a/hikari/models/guilds.py b/hikari/models/guilds.py index d40c7ac33b..349f032d82 100644 --- a/hikari/models/guilds.py +++ b/hikari/models/guilds.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python3 # -*- coding: utf-8 -*- # Copyright © Nekoka.tt 2019-2020 # diff --git a/hikari/models/intents.py b/hikari/models/intents.py index 7269d91098..3b231dbe42 100644 --- a/hikari/models/intents.py +++ b/hikari/models/intents.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python3 # -*- coding: utf-8 -*- # Copyright © Nekoka.tt 2019-2020 # diff --git a/hikari/models/invites.py b/hikari/models/invites.py index 048a357f04..05d0f444a1 100644 --- a/hikari/models/invites.py +++ b/hikari/models/invites.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python3 # -*- coding: utf-8 -*- # Copyright © Nekoka.tt 2019-2020 # diff --git a/hikari/models/messages.py b/hikari/models/messages.py index ec8669ebcb..128302ea6b 100644 --- a/hikari/models/messages.py +++ b/hikari/models/messages.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python3 # -*- coding: utf-8 -*- # Copyright © Nekoka.tt 2019-2020 # diff --git a/hikari/models/permissions.py b/hikari/models/permissions.py index 1ce37904d6..4a86c1105c 100644 --- a/hikari/models/permissions.py +++ b/hikari/models/permissions.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python3 # -*- coding: utf-8 -*- # Copyright © Nekoka.tt 2019-2020 # diff --git a/hikari/models/presences.py b/hikari/models/presences.py index 53379fb2d2..c09829985d 100644 --- a/hikari/models/presences.py +++ b/hikari/models/presences.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python3 # -*- coding: utf-8 -*- # Copyright © Nekoka.tt 2019-2020 # diff --git a/hikari/models/users.py b/hikari/models/users.py index b109ff1608..9649aa61ad 100644 --- a/hikari/models/users.py +++ b/hikari/models/users.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python3 # -*- coding: utf-8 -*- # Copyright © Nekoka.tt 2019-2020 # diff --git a/hikari/models/voices.py b/hikari/models/voices.py index 09317271ca..0c7c69a801 100644 --- a/hikari/models/voices.py +++ b/hikari/models/voices.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python3 # -*- coding: utf-8 -*- # Copyright © Nekoka.tt 2019-2020 # diff --git a/hikari/models/webhooks.py b/hikari/models/webhooks.py index 2ed57dda4b..f97f453302 100644 --- a/hikari/models/webhooks.py +++ b/hikari/models/webhooks.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python3 # -*- coding: utf-8 -*- # Copyright © Nekoka.tt 2019-2020 # diff --git a/hikari/net/__init__.py b/hikari/net/__init__.py index 05135ccf98..1cff1090f3 100644 --- a/hikari/net/__init__.py +++ b/hikari/net/__init__.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python3 # -*- coding: utf-8 -*- # Copyright © Nekoka.tt 2019-2020 # diff --git a/hikari/net/buckets.py b/hikari/net/buckets.py index 02813e8ecd..0d06f9c2ee 100644 --- a/hikari/net/buckets.py +++ b/hikari/net/buckets.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python3 # -*- coding: utf-8 -*- # Copyright © Nekoka.tt 2019-2020 # diff --git a/hikari/net/gateway.py b/hikari/net/gateway.py index d050693ff2..71f1c7c837 100644 --- a/hikari/net/gateway.py +++ b/hikari/net/gateway.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python3 # -*- coding: utf-8 -*- # Copyright © Nekoka.tt 2019-2020 # diff --git a/hikari/net/http_client.py b/hikari/net/http_client.py index 358eb2d2b3..ff23535221 100644 --- a/hikari/net/http_client.py +++ b/hikari/net/http_client.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python3 # -*- coding: utf-8 -*- # Copyright © Nekoka.tt 2019-2020 # diff --git a/hikari/net/http_settings.py b/hikari/net/http_settings.py index 13591dd948..ed74300186 100644 --- a/hikari/net/http_settings.py +++ b/hikari/net/http_settings.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python3 # -*- coding: utf-8 -*- # Copyright © Nekoka.tt 2019-2020 # diff --git a/hikari/net/iterators.py b/hikari/net/iterators.py index ffa3391875..142c172573 100644 --- a/hikari/net/iterators.py +++ b/hikari/net/iterators.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python3 # -*- coding: utf-8 -*- # Copyright © Nekoka.tt 2019-2020 # diff --git a/hikari/net/rate_limits.py b/hikari/net/rate_limits.py index 8c10d97169..47faea3de3 100644 --- a/hikari/net/rate_limits.py +++ b/hikari/net/rate_limits.py @@ -1,6 +1,5 @@ -#!/usr/bin/env python3 # -*- coding: utf-8 -*- -# Copyright © Nekokatt 2019-2020 +# Copyright © Nekoka.tt 2019-2020 # # This file is part of Hikari. # @@ -488,7 +487,7 @@ def __next__(self) -> float: if self.maximum is not None and value >= self.maximum: raise asyncio.TimeoutError() - value += random.random() * self.jitter_multiplier # nosec + value += random.random() * self.jitter_multiplier # # noqa S311 rng for cryptography return value def __iter__(self) -> ExponentialBackOff: diff --git a/hikari/net/rest.py b/hikari/net/rest.py index 1fdf643b0b..5bb1d5e565 100644 --- a/hikari/net/rest.py +++ b/hikari/net/rest.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python3 # -*- coding: utf-8 -*- # Copyright © Nekoka.tt 2019-2020 # diff --git a/hikari/net/rest_utils.py b/hikari/net/rest_utils.py index ab767ab141..3c3ce10d26 100644 --- a/hikari/net/rest_utils.py +++ b/hikari/net/rest_utils.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python3 # -*- coding: utf-8 -*- # Copyright © Nekoka.tt 2019-2020 # diff --git a/hikari/net/routes.py b/hikari/net/routes.py index 387b09d408..1a97dec742 100644 --- a/hikari/net/routes.py +++ b/hikari/net/routes.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python3 # -*- coding: utf-8 -*- # Copyright © Nekoka.tt 2019-2020 # diff --git a/hikari/net/strings.py b/hikari/net/strings.py index b29d9cc6bd..260f094031 100644 --- a/hikari/net/strings.py +++ b/hikari/net/strings.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python3 # -*- coding: utf-8 -*- # Copyright © Nekoka.tt 2019-2020 # diff --git a/hikari/net/tracing.py b/hikari/net/tracing.py index 0eebbec5e2..4056b5af21 100644 --- a/hikari/net/tracing.py +++ b/hikari/net/tracing.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python3 # -*- coding: utf-8 -*- # Copyright © Nekoka.tt 2019-2020 # diff --git a/hikari/utilities/__init__.py b/hikari/utilities/__init__.py index 67828f7c44..394dc66bb8 100644 --- a/hikari/utilities/__init__.py +++ b/hikari/utilities/__init__.py @@ -1,6 +1,5 @@ -#!/usr/bin/env python3 # -*- coding: utf-8 -*- -# Copyright © Nekokatt 2019-2020 +# Copyright © Nekoka.tt 2019-2020 # # This file is part of Hikari. # diff --git a/hikari/utilities/aio.py b/hikari/utilities/aio.py index 9089d9be35..e97268ea57 100644 --- a/hikari/utilities/aio.py +++ b/hikari/utilities/aio.py @@ -1,6 +1,5 @@ -#!/usr/bin/env python3 # -*- coding: utf-8 -*- -# Copyright © Nekokatt 2019-2020 +# Copyright © Nekoka.tt 2019-2020 # # This file is part of Hikari. # diff --git a/hikari/utilities/cdn.py b/hikari/utilities/cdn.py index b615019a5e..7a15da7c47 100644 --- a/hikari/utilities/cdn.py +++ b/hikari/utilities/cdn.py @@ -1,6 +1,5 @@ -#!/usr/bin/env python3 # -*- coding: utf-8 -*- -# Copyright © Nekokatt 2019-2020 +# Copyright © Nekoka.tt 2019-2020 # # This file is part of Hikari. # diff --git a/hikari/utilities/data_binding.py b/hikari/utilities/data_binding.py index 8047da219e..2259609e3b 100644 --- a/hikari/utilities/data_binding.py +++ b/hikari/utilities/data_binding.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python3 # -*- coding: utf-8 -*- # Copyright © Nekoka.tt 2019-2020 # diff --git a/hikari/utilities/date.py b/hikari/utilities/date.py index 2cced21e07..ad592a2632 100644 --- a/hikari/utilities/date.py +++ b/hikari/utilities/date.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python3 # -*- coding: utf-8 -*- # Copyright © Nekoka.tt 2019-2020 # diff --git a/hikari/utilities/files.py b/hikari/utilities/files.py index 024c0df43e..3ca624b95f 100644 --- a/hikari/utilities/files.py +++ b/hikari/utilities/files.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python3 # -*- coding: utf-8 -*- # Copyright © Nekoka.tt 2019-2020 # diff --git a/hikari/utilities/reflect.py b/hikari/utilities/reflect.py index 72db2872b9..b7e66eed6d 100644 --- a/hikari/utilities/reflect.py +++ b/hikari/utilities/reflect.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python3 # -*- coding: utf-8 -*- # Copyright © Nekoka.tt 2019-2020 # diff --git a/hikari/utilities/snowflake.py b/hikari/utilities/snowflake.py index 8c51a89637..010b418f49 100644 --- a/hikari/utilities/snowflake.py +++ b/hikari/utilities/snowflake.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python3 # -*- coding: utf-8 -*- # Copyright © Nekoka.tt 2019-2020 # diff --git a/hikari/utilities/undefined.py b/hikari/utilities/undefined.py index fe904c7a73..22f8eff991 100644 --- a/hikari/utilities/undefined.py +++ b/hikari/utilities/undefined.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python3 # -*- coding: utf-8 -*- # Copyright © Nekoka.tt 2019-2020 # diff --git a/mypy-requirements.txt b/mypy-requirements.txt new file mode 100644 index 0000000000..9e7ef885f9 --- /dev/null +++ b/mypy-requirements.txt @@ -0,0 +1 @@ +mypy==0.780 diff --git a/noxfile.py b/noxfile.py index 151a190ec8..1ec857254c 100644 --- a/noxfile.py +++ b/noxfile.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python3 # -*- coding: utf-8 -*- # Copyright © Nekoka.tt 2019-2020 # diff --git a/pydocstyle.ini b/pydocstyle.ini deleted file mode 100644 index 819e2dad1b..0000000000 --- a/pydocstyle.ini +++ /dev/null @@ -1,4 +0,0 @@ -[pydocstyle] -convention=numpy -add-ignore=D105, # Magic methods not having a docstring - D102, # Missing docstring in public method diff --git a/scripts/test_twemoji_mapping.py b/scripts/test_twemoji_mapping.py index e6b4b3c769..a2f458e3de 100644 --- a/scripts/test_twemoji_mapping.py +++ b/scripts/test_twemoji_mapping.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python3 # -*- coding: utf-8 -*- # Copyright © Nekoka.tt 2019-2020 # diff --git a/setup.py b/setup.py index 738e6a24ac..f0a10b8c47 100644 --- a/setup.py +++ b/setup.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python3 # -*- coding: utf-8 -*- # Copyright © Nekoka.tt 2019-2020 # @@ -25,81 +24,6 @@ name = "hikari" -# """Acceleration stuff for the future.""" -# -# from distutils import ccompiler -# from distutils import log -# from distutils import errors -# from setuptools.command import build_ext -# -# should_accelerate = "ACCELERATE_HIKARI" in os.environ -# -# -# class Accelerator(setuptools.Extension): -# def __init__(self, name, sources, **kwargs): -# super().__init__(name, sources, **kwargs) -# -# -# class BuildCommand(build_ext.build_ext): -# def build_extensions(self): -# if should_accelerate: -# for ext in self.extensions: -# if isinstance(ext, Accelerator): -# self.build_accelerator(ext) -# else: -# self.build_extension(ext) -# -# def build_accelerator(self, ext): -# try: -# self.build_extension(ext) -# except errors.CompileError as ex: -# log.warn("Compilation of %s failed, so this module will not be accelerated: %s", ext, ex) -# except errors.LinkError as ex: -# log.warn("Linking of %s failed, so this module will not be accelerated: %s", ext, ex) -# -# -# if should_accelerate: -# log.warn("!!!!!!!!!!!!!!!!!!!!EXPERIMENTAL!!!!!!!!!!!!!!!!!!!!") -# log.warn("HIKARI ACCELERATION SUPPORT IS ENABLED: YOUR MILEAGE MAY VARY :^)") -# -# extensions = [Accelerator("hikari.utilities.marshaller", ["hikari.utilities/marshaller.cpp"], **cxx_compile_kwargs)] -# -# cxx_spec = "c++17" -# compiler_type = ccompiler.get_default_compiler() -# -# if compiler_type in ("unix", "cygwin", "mingw32"): -# log.warn("using unix-style compiler toolchain: %s", compiler_type) -# cxx_debug_flags = f"-Wall -Wextra -Wpedantic -std={cxx_spec} -ggdb -DDEBUG -O0".split() -# cxx_release_flags = f"-Wall -Wextra -Wpedantic -std={cxx_spec} -O3 -DNDEBUG".split() -# cxx_debug_linker_flags = [] -# cxx_release_linker_flags = [] -# elif compiler_type == "msvc": -# # compiler flags: -# # https://docs.microsoft.com/en-us/cpp/build/reference/compiler-options-listed-alphabetically?view=vs-2019 -# # linker flags: -# # https://docs.microsoft.com/en-us/cpp/build/reference/opt-optimizations?view=vs-2019 -# log.warn("using Microsoft Visual C/C++ compiler toolchain: %s", compiler_type) -# cxx_debug_flags = f"/D DEBUG=1 /Od /Wall /std:{cxx_spec}".split() -# cxx_release_flags = f"/D NDEBUG=1 /O2 /Qspectre /Wall /std:{cxx_spec}".split() -# cxx_debug_linker_flags = "/DEBUG /OPT:NOREF,NOICF,NOLBR".split() -# cxx_release_linker_flags = "/OPT:REF,ICF,LBR".split() -# -# if "DEBUG_HIKARI" in os.environ: -# cxx_compile_kwargs = dict( -# extra_compile_args=cxx_debug_flags, extra_link_args=cxx_debug_linker_flags, language="c++", -# ) -# else: -# cxx_compile_kwargs = dict( -# extra_compile_args=cxx_release_flags, extra_link_args=cxx_release_linker_flags, language="c++", -# ) -# -# log.warn("Building c++ with opts: %s", cxx_compile_kwargs) -# -# log.warn("!!!!!!!!!!!!!!!!!!!!EXPERIMENTAL!!!!!!!!!!!!!!!!!!!!") -# else: -# log.warn("skipping building of accelerators for %s", name) -# extensions = [] - def long_description(): with open("README.md") as fp: @@ -160,10 +84,7 @@ def parse_requirements(): "Operating System :: OS Independent", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", - # "Programming Language :: C", - # "Programming Language :: C++", "Programming Language :: Python :: Implementation :: CPython", - # "Programming Language :: Python :: Implementation :: Stackless", "Programming Language :: Python :: 3 :: Only", "Topic :: Communications :: Chat", "Topic :: Internet :: WWW/HTTP", @@ -172,9 +93,6 @@ def parse_requirements(): "Topic :: Software Development :: Libraries :: Python Modules", "Typing :: Typed", ], - entry_points={"console_scripts": ["hikari = hikari.__main__:main", "hikari-test = hikari.clients.test:main",]}, + entry_points={"console_scripts": ["hikari = hikari.__main__:main", "hikari-test = hikari.clients.test:main"]}, provides="hikari", - # """Acceleration stuff for the future.""" - # ext_modules=extensions, - # cmdclass={"build_ext": BuildCommand}, ) diff --git a/tests/hikari/__init__.py b/tests/hikari/__init__.py index 63253adf3b..aea66f3ef9 100644 --- a/tests/hikari/__init__.py +++ b/tests/hikari/__init__.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python3 # -*- coding: utf-8 -*- # Copyright © Nekoka.tt 2019-2020 # diff --git a/tests/hikari/_helpers.py b/tests/hikari/_helpers.py index b96819d266..6cacd848e8 100644 --- a/tests/hikari/_helpers.py +++ b/tests/hikari/_helpers.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python3 # -*- coding: utf-8 -*- # Copyright © Nekoka.tt 2019-2020 # diff --git a/tests/hikari/impl/test_entity_factory.py b/tests/hikari/impl/test_entity_factory.py index b7ec15200a..ee2b83760e 100644 --- a/tests/hikari/impl/test_entity_factory.py +++ b/tests/hikari/impl/test_entity_factory.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python3 # -*- coding: utf-8 -*- # Copyright © Nekoka.tt 2019-2020 # diff --git a/tests/hikari/net/test_buckets.py b/tests/hikari/net/test_buckets.py index 5dcb3f23c2..224bbc862f 100644 --- a/tests/hikari/net/test_buckets.py +++ b/tests/hikari/net/test_buckets.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python3 # -*- coding: utf-8 -*- # Copyright © Nekoka.tt 2019-2020 # diff --git a/tests/hikari/net/test_gateway.py b/tests/hikari/net/test_gateway.py index 1c1502a5ca..3b080b938f 100644 --- a/tests/hikari/net/test_gateway.py +++ b/tests/hikari/net/test_gateway.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python3 # -*- coding: utf-8 -*- # Copyright © Nekoka.tt 2019-2020 # diff --git a/tests/hikari/net/test_http_client.py b/tests/hikari/net/test_http_client.py index 5de357d131..aa093da96d 100644 --- a/tests/hikari/net/test_http_client.py +++ b/tests/hikari/net/test_http_client.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python3 # -*- coding: utf-8 -*- # Copyright © Nekoka.tt 2019-2020 # diff --git a/tests/hikari/net/test_iterators.py b/tests/hikari/net/test_iterators.py index 1c1502a5ca..3b080b938f 100644 --- a/tests/hikari/net/test_iterators.py +++ b/tests/hikari/net/test_iterators.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python3 # -*- coding: utf-8 -*- # Copyright © Nekoka.tt 2019-2020 # diff --git a/tests/hikari/net/test_ratelimits.py b/tests/hikari/net/test_ratelimits.py index 85288549be..0207418cfc 100644 --- a/tests/hikari/net/test_ratelimits.py +++ b/tests/hikari/net/test_ratelimits.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python3 # -*- coding: utf-8 -*- # Copyright © Nekoka.tt 2019-2020 # diff --git a/tests/hikari/net/test_rest.py b/tests/hikari/net/test_rest.py index 1c1502a5ca..3b080b938f 100644 --- a/tests/hikari/net/test_rest.py +++ b/tests/hikari/net/test_rest.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python3 # -*- coding: utf-8 -*- # Copyright © Nekoka.tt 2019-2020 # diff --git a/tests/hikari/net/test_rest_utils.py b/tests/hikari/net/test_rest_utils.py index 1c1502a5ca..3b080b938f 100644 --- a/tests/hikari/net/test_rest_utils.py +++ b/tests/hikari/net/test_rest_utils.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python3 # -*- coding: utf-8 -*- # Copyright © Nekoka.tt 2019-2020 # diff --git a/tests/hikari/net/test_routes.py b/tests/hikari/net/test_routes.py index 1c1502a5ca..3b080b938f 100644 --- a/tests/hikari/net/test_routes.py +++ b/tests/hikari/net/test_routes.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python3 # -*- coding: utf-8 -*- # Copyright © Nekoka.tt 2019-2020 # diff --git a/tests/hikari/net/test_tracing.py b/tests/hikari/net/test_tracing.py index 1c1502a5ca..3b080b938f 100644 --- a/tests/hikari/net/test_tracing.py +++ b/tests/hikari/net/test_tracing.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python3 # -*- coding: utf-8 -*- # Copyright © Nekoka.tt 2019-2020 # diff --git a/tests/hikari/net/test_urls.py b/tests/hikari/net/test_urls.py index 95edea8cba..fc3077e858 100644 --- a/tests/hikari/net/test_urls.py +++ b/tests/hikari/net/test_urls.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python3 # -*- coding: utf-8 -*- # Copyright © Nekoka.tt 2019-2020 # diff --git a/tests/hikari/utilities/test_aio.py b/tests/hikari/utilities/test_aio.py index 42fa77b3ee..c359b8e469 100644 --- a/tests/hikari/utilities/test_aio.py +++ b/tests/hikari/utilities/test_aio.py @@ -1,6 +1,5 @@ -#!/usr/bin/env python3 # -*- coding: utf-8 -*- -# Copyright © Nekokatt 2019-2020 +# Copyright © Nekoka.tt 2019-2020 # # This file is part of Hikari. # diff --git a/tests/hikari/utilities/test_cdn.py b/tests/hikari/utilities/test_cdn.py index fdf200e1bd..46bc3edf5f 100644 --- a/tests/hikari/utilities/test_cdn.py +++ b/tests/hikari/utilities/test_cdn.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python3 # -*- coding: utf-8 -*- # Copyright © Nekoka.tt 2019-2020 # diff --git a/tests/hikari/utilities/test_data_binding.py b/tests/hikari/utilities/test_data_binding.py index d817693100..39b5fb35f7 100644 --- a/tests/hikari/utilities/test_data_binding.py +++ b/tests/hikari/utilities/test_data_binding.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python3 # -*- coding: utf-8 -*- # Copyright © Nekoka.tt 2019-2020 # diff --git a/tests/hikari/utilities/test_date.py b/tests/hikari/utilities/test_date.py index 28f1cc6457..5f5068ba80 100644 --- a/tests/hikari/utilities/test_date.py +++ b/tests/hikari/utilities/test_date.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python3 # -*- coding: utf-8 -*- # Copyright © Nekoka.tt 2019-2020 # diff --git a/tests/hikari/utilities/test_reflect.py b/tests/hikari/utilities/test_reflect.py index c261cc9475..c6bdff644a 100644 --- a/tests/hikari/utilities/test_reflect.py +++ b/tests/hikari/utilities/test_reflect.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python3 # -*- coding: utf-8 -*- # Copyright © Nekoka.tt 2019-2020 # diff --git a/tests/hikari/utilities/test_snowflake.py b/tests/hikari/utilities/test_snowflake.py index e07e41c093..88aef07aef 100644 --- a/tests/hikari/utilities/test_snowflake.py +++ b/tests/hikari/utilities/test_snowflake.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python3 # -*- coding: utf-8 -*- # Copyright © Nekoka.tt 2019-2020 # diff --git a/tests/hikari/utilities/test_undefined.py b/tests/hikari/utilities/test_undefined.py index 1a4090060d..37e06408c1 100644 --- a/tests/hikari/utilities/test_undefined.py +++ b/tests/hikari/utilities/test_undefined.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python3 # -*- coding: utf-8 -*- # Copyright © Nekoka.tt 2019-2020 # From 11f192e20c05673a00608670da3f68011b15d31d Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Sun, 14 Jun 2020 18:37:25 +0100 Subject: [PATCH 513/922] Fixed LGTM https://lgtm.com/projects/gl/nekokatt/hikari/rev/pr-74a9d146070b73de836b4281b255bf7e1acc4fcc --- ci/config.py | 2 +- ci/flake8.nox.py | 17 +++++++++++++++-- ci/gitlab/linting.yml | 5 +++-- ci/safety.nox.py | 1 - flake-requirements.txt | 2 +- 5 files changed, 20 insertions(+), 7 deletions(-) diff --git a/ci/config.py b/ci/config.py index 555dad496a..6f5339c0b3 100644 --- a/ci/config.py +++ b/ci/config.py @@ -30,7 +30,7 @@ ROOT_INDEX_SOURCE = "index.html" # Linting and test configs. -FLAKE8_CODECLIMATE = "public/flake8.json" +FLAKE8_JUNIT = "public/flake8-junit.xml" FLAKE8_HTML = "public/flake8" FLAKE8_TXT = "public/flake8.txt" MYPY_INI = "mypy.ini" diff --git a/ci/flake8.nox.py b/ci/flake8.nox.py index 907fd97c5c..0a398d00d2 100644 --- a/ci/flake8.nox.py +++ b/ci/flake8.nox.py @@ -25,12 +25,25 @@ def flake8(session: nox.Session) -> None: session.install("-r", "requirements.txt", "-r", "flake-requirements.txt") + session.run( + "flake8", + "--format=html", + f"--htmldir={config.FLAKE8_HTML}", + config.MAIN_PACKAGE, + success_codes=range(0, 256), + ) + if "GITLAB_CI" in os.environ or "--gitlab" in session.posargs: print("Detected GitLab, will output CodeClimate report instead!") - format_args = ["--format=gl-codeclimate", f"--output-file={config.FLAKE8_CODECLIMATE}"] + # If we add the args for --statistics or --show-source, the thing breaks + # silently, and I cant find another decent package that actually works + # in any of the gitlab-supported formats :( + format_args = ["--format=junit-xml", f"--output-file={config.FLAKE8_JUNIT}"] else: format_args = [f"--output-file={config.FLAKE8_TXT}", "--statistics", "--show-source"] session.run( - "flake8", "--exit-zero", "--format=html", f"--htmldir={config.FLAKE8_HTML}", *format_args, config.MAIN_PACKAGE, + "flake8", + *format_args, + config.MAIN_PACKAGE, ) diff --git a/ci/gitlab/linting.yml b/ci/gitlab/linting.yml index d05d80a6a5..f842e9edee 100644 --- a/ci/gitlab/linting.yml +++ b/ci/gitlab/linting.yml @@ -37,10 +37,11 @@ flake8: allow_failure: true artifacts: + when: always paths: - - public/ + - public/flake8 reports: - codequality: public/flake8.json + junit: public/flake8-junit.xml extends: .lint script: - nox -s flake8 --no-error-on-external-run diff --git a/ci/safety.nox.py b/ci/safety.nox.py index 4fd4437a2e..7710d289e4 100644 --- a/ci/safety.nox.py +++ b/ci/safety.nox.py @@ -17,7 +17,6 @@ # along with Hikari. If not, see . """Dependency scanning.""" -from ci import config from ci import nox diff --git a/flake-requirements.txt b/flake-requirements.txt index 86f49ad140..ca9ca9203c 100644 --- a/flake-requirements.txt +++ b/flake-requirements.txt @@ -16,9 +16,9 @@ flake8-eradicate~=0.4.0 # find commented out code flake8-executable~=2.0.3 # shebangs flake8-fixme~=1.1.1 # "fix me" counter flake8-functions~=0.0.4 # function linting -flake8-gl-codeclimate~=0.1.4 # gitlab codeclimate output format flake8-html~=0.4.1 # html output flake8-if-statements~=0.1.0 # condition linting +flake8_formatter_junit_xml~=0.0.6 # junit flake8-mutable~=1.2.0 # mutable default argument detection flake8-pep3101~=1.3.0 # new-style format strings only flake8-print~=3.1.4 # complain about print statements in code From 12dea6431e888fe6ce9a2dcc64daec1a63f67f89 Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Mon, 15 Jun 2020 08:33:54 +0100 Subject: [PATCH 514/922] Replaced use of contextlib.asynccontextmanager in files.py with classes. This allows me to use static type checking on those stream methods now, albeit at a cost of complexity, but it isn't really an issue for us. --- ci/flake8.nox.py | 10 +- hikari/net/rest.py | 2 +- hikari/net/rest_utils.py | 2 +- hikari/utilities/files.py | 248 +++++++++++++++++++++++--------- hikari/utilities/undefined.py | 2 +- scripts/test_twemoji_mapping.py | 3 +- 6 files changed, 184 insertions(+), 83 deletions(-) diff --git a/ci/flake8.nox.py b/ci/flake8.nox.py index 0a398d00d2..379f8b8c1d 100644 --- a/ci/flake8.nox.py +++ b/ci/flake8.nox.py @@ -26,11 +26,7 @@ def flake8(session: nox.Session) -> None: session.install("-r", "requirements.txt", "-r", "flake-requirements.txt") session.run( - "flake8", - "--format=html", - f"--htmldir={config.FLAKE8_HTML}", - config.MAIN_PACKAGE, - success_codes=range(0, 256), + "flake8", "--format=html", f"--htmldir={config.FLAKE8_HTML}", config.MAIN_PACKAGE, success_codes=range(0, 256), ) if "GITLAB_CI" in os.environ or "--gitlab" in session.posargs: @@ -43,7 +39,5 @@ def flake8(session: nox.Session) -> None: format_args = [f"--output-file={config.FLAKE8_TXT}", "--statistics", "--show-source"] session.run( - "flake8", - *format_args, - config.MAIN_PACKAGE, + "flake8", *format_args, config.MAIN_PACKAGE, ) diff --git a/hikari/net/rest.py b/hikari/net/rest.py index 5bb1d5e565..337e4def75 100644 --- a/hikari/net/rest.py +++ b/hikari/net/rest.py @@ -1526,7 +1526,7 @@ async def execute_webhook( try: for i, attachment in enumerate(final_attachments): - stream = await stack.enter_async_context(attachment.stream(self._app.executor)) + stream = await stack.enter_async_context(attachment.stream(executor=self._app.executor)) form.add_field( f"file{i}", stream, filename=stream.filename, content_type=self._APPLICATION_OCTET_STREAM ) diff --git a/hikari/net/rest_utils.py b/hikari/net/rest_utils.py index 3c3ce10d26..39fb283bb7 100644 --- a/hikari/net/rest_utils.py +++ b/hikari/net/rest_utils.py @@ -252,7 +252,7 @@ async def create(self) -> guilds.Guild: payload.put("explicit_content_filter", self.explicit_content_filter_level) if self.icon is not undefined.UNDEFINED: - async with self.icon.stream(self._app.executor) as stream: + async with self.icon.stream(executor=self._app.executor) as stream: data_uri = await stream.data_uri() payload.put("icon", data_uri) diff --git a/hikari/utilities/files.py b/hikari/utilities/files.py index 3ca624b95f..3d43291bf2 100644 --- a/hikari/utilities/files.py +++ b/hikari/utilities/files.py @@ -25,6 +25,7 @@ "ByteReader", "FileReader", "WebReader", + "AsyncReaderContextManager", "Resource", "Bytes", "File", @@ -35,7 +36,6 @@ import abc import asyncio import base64 -import contextlib import logging import mimetypes import os @@ -52,6 +52,7 @@ if typing.TYPE_CHECKING: import concurrent.futures + import types _LOGGER: typing.Final[logging.Logger] = reflect.get_logger(__name__) _MAGIC: typing.Final[int] = 50 * 1024 @@ -261,6 +262,9 @@ async def read(self) -> bytes: return buff +ReaderImplT = typing.TypeVar("ReaderImplT", bound=AsyncReader) + + @attr.s(auto_attribs=True, slots=True) class ByteReader(AsyncReader): """Asynchronous file reader that operates on in-memory data.""" @@ -273,6 +277,37 @@ async def __aiter__(self) -> typing.AsyncGenerator[typing.Any, bytes]: yield self.data[i : i + _MAGIC] +@attr.s(auto_attribs=True, slots=True) +class WebReader(AsyncReader): + """Asynchronous reader to use to read data from a web resource.""" + + stream: aiohttp.StreamReader + """The `aiohttp.StreamReader` to read the content from.""" + + url: str + """The URL being read from.""" + + status: int + """The initial HTTP response status.""" + + reason: str + """The HTTP response status reason.""" + + charset: typing.Optional[str] + """Optional character set information, if known.""" + + size: typing.Optional[int] + """The size of the resource, if known.""" + + async def read(self) -> bytes: + return await self.stream.read() + + async def __aiter__(self) -> typing.AsyncGenerator[typing.Any, bytes]: + while not self.stream.at_eof(): + chunk = await self.stream.readchunk() + yield chunk[0] + + @attr.s(auto_attribs=True, slots=True) class FileReader(AsyncReader): """Asynchronous file reader that reads a resource from local storage.""" @@ -320,38 +355,104 @@ def _close(fp: typing.IO[bytes]) -> None: fp.close() -@attr.s(auto_attribs=True, slots=True) -class WebReader(AsyncReader): - """Asynchronous reader to use to read data from a web resource.""" +class AsyncReaderContextManager(typing.Generic[ReaderImplT]): + """Context manager that returns a reader.""" - stream: aiohttp.StreamReader - """The `aiohttp.StreamReader` to read the content from.""" + __slots__ = () - url: str - """The URL being read from.""" + @abc.abstractmethod + async def __aenter__(self) -> ReaderImplT: + ... - status: int - """The initial HTTP response status.""" + @abc.abstractmethod + async def __aexit__( + self, + exc_type: typing.Optional[typing.Type[BaseException]], + exc: typing.Optional[BaseException], + exc_tb: typing.Optional[types.TracebackType], + ) -> None: + ... - reason: str - """The HTTP response status reason.""" - charset: typing.Optional[str] - """Optional character set information, if known.""" +class _NoOpAsyncReaderContextManagerImpl(typing.Generic[ReaderImplT], AsyncReaderContextManager[ReaderImplT]): + __slots__ = ("impl",) - size: typing.Optional[int] - """The size of the resource, if known.""" + def __init__(self, impl: ReaderImplT) -> None: + self.impl = impl - async def read(self) -> bytes: - return await self.stream.read() + async def __aenter__(self) -> ReaderImplT: + return self.impl - async def __aiter__(self) -> typing.AsyncGenerator[typing.Any, bytes]: - while not self.stream.at_eof(): - chunk = await self.stream.readchunk() - yield chunk[0] + async def __aexit__( + self, + exc_type: typing.Optional[typing.Type[BaseException]], + exc: typing.Optional[BaseException], + exc_tb: typing.Optional[types.TracebackType], + ) -> None: + pass -class Resource(abc.ABC): +class _WebReaderAsyncReaderContextManagerImpl(AsyncReaderContextManager[WebReader]): + __slots__ = ("_web_resource", "_head_only", "_client_response_ctx", "_client_session") + + def __init__(self, web_resource: WebResource, head_only: bool) -> None: + self._web_resource = web_resource + self._head_only = head_only + self._client_session: aiohttp.ClientSession = NotImplemented + self._client_response_ctx: typing.AsyncContextManager = NotImplemented + + async def __aenter__(self) -> WebReader: + client_session = aiohttp.ClientSession() + + method = "HEAD" if self._head_only else "GET" + + ctx = client_session.request(method, self._web_resource.url, raise_for_status=False) + + try: + resp: aiohttp.ClientResponse = await ctx.__aenter__() + + if 200 <= resp.status < 400: + mimetype = None + filename = self._web_resource.filename + + if resp.content_disposition is not None: + mimetype = resp.content_disposition.type + + if mimetype is None: + mimetype = resp.content_type + + self._client_response_ctx = ctx + self._client_session = client_session + + return WebReader( + stream=resp.content, + url=str(resp.real_url), + status=resp.status, + reason=str(resp.reason), + filename=filename, + charset=resp.charset, + mimetype=mimetype, + size=resp.content_length, + ) + else: + raise await http_client.generate_error_response(resp) + + except Exception as ex: + await ctx.__aexit__(type(ex), ex, ex.__traceback__) + await client_session.close() + raise + + async def __aexit__( + self, + exc_type: typing.Optional[typing.Type[BaseException]], + exc: typing.Optional[BaseException], + exc_tb: typing.Optional[types.TracebackType], + ) -> None: + await self._client_response_ctx.__aexit__(exc_type, exc, exc_tb) + await self._client_session.close() + + +class Resource(typing.Generic[ReaderImplT], abc.ABC): """Base for any uploadable or downloadable representation of information. These representations can be streamed using bit inception for performance, @@ -372,10 +473,27 @@ def filename(self) -> str: """Filename of the resource.""" @abc.abstractmethod - @contextlib.asynccontextmanager - @typing.no_type_check - async def stream(self, *, executor: typing.Optional[concurrent.futures.Executor]) -> AsyncReader: - """Return an async iterable of bytes to stream.""" + def stream( + self, *, executor: typing.Optional[concurrent.futures.Executor] = None, head_only: bool = False, + ) -> AsyncReaderContextManager[ReaderImplT]: + """Produce a stream of data for the resource. + + Parameters + ---------- + executor : concurrent.futures.Executor or None + The executor to run in for blocking operations. + If `None`, then the default executor is used for the current + event loop. + head_only : bool + Defaults to `False`. If `True`, then the implementation may + only retrieve HEAD information if supported. This currently + only has any effect for web requests. + + Returns + ------- + AsyncReaderContextManager[AsyncReader] + An async iterable of bytes to stream. + """ def __str__(self) -> str: return self.url @@ -392,7 +510,7 @@ def __hash__(self) -> int: return hash(self.url) -class Bytes(Resource): +class Bytes(Resource[ByteReader]): """Representation of in-memory data to upload. Parameters @@ -465,25 +583,28 @@ def url(self) -> str: def filename(self) -> str: return self._filename - @contextlib.asynccontextmanager - @typing.no_type_check - async def stream(self, *, executor: typing.Optional[concurrent.futures.Executor] = None) -> ByteReader: + def stream( + self, *, executor: typing.Optional[concurrent.futures.Executor] = None, head_only: bool = False, + ) -> AsyncReaderContextManager[ByteReader]: """Start streaming the content in chunks. Parameters ---------- executor : concurrent.futures.Executor or None Not used. Provided only to match the underlying interface. + head_only : bool + Not used. Provided only to match the underlying interface. Returns ------- - ByteReader - The byte stream. + AsyncReaderContextManager[ByteReader] + An async context manager that when entered, produces the + data stream. """ - yield ByteReader(self.filename, self.mimetype, self.data) + return _NoOpAsyncReaderContextManagerImpl(ByteReader(self.filename, self.mimetype, self.data)) -class WebResource(Resource, abc.ABC): +class WebResource(Resource[WebReader], abc.ABC): """Base class for a resource that resides on the internet. The logic for identifying this resource is left to each implementation @@ -503,9 +624,9 @@ class WebResource(Resource, abc.ABC): __slots__ = () - @contextlib.asynccontextmanager - @typing.no_type_check - async def stream(self, *, executor: typing.Optional[concurrent.futures.Executor] = None) -> WebReader: + def stream( + self, *, executor: typing.Optional[concurrent.futures.Executor] = None, head_only: bool = False, + ) -> AsyncReaderContextManager[WebReader]: """Start streaming the content into memory by downloading it. You can use this to fetch the entire resource, parts of the resource, @@ -515,6 +636,10 @@ async def stream(self, *, executor: typing.Optional[concurrent.futures.Executor] ---------- executor : concurrent.futures.Executor or None Not used. Provided only to match the underlying interface. + head_only : bool + Defaults to `False`. If `True`, then the implementation may + only retrieve HEAD information if supported. This currently + only has any effect for web requests. Examples -------- @@ -543,8 +668,9 @@ async def stream(self, *, executor: typing.Optional[concurrent.futures.Executor] Returns ------- - WebReader - The download stream. + AsyncReaderContextManager[WebReader] + An async context manager that when entered, produces the + data stream. Raises ------ @@ -563,30 +689,7 @@ async def stream(self, *, executor: typing.Optional[concurrent.futures.Executor] hikari.errors.HTTPErrorResponse If any other unexpected response code is returned. """ - async with aiohttp.ClientSession() as session: - async with session.request("get", self.url, raise_for_status=False) as resp: - if 200 <= resp.status < 400: - mimetype = None - filename = self.filename - - if resp.content_disposition is not None: - mimetype = resp.content_disposition.type - - if mimetype is None: - mimetype = resp.content_type - - yield WebReader( - stream=resp.content, - url=str(resp.real_url), - status=resp.status, - reason=resp.reason, - filename=filename, - charset=resp.charset, - mimetype=mimetype, - size=resp.content_length, - ) - else: - raise await http_client.generate_error_response(resp) + return _WebReaderAsyncReaderContextManagerImpl(self, head_only) class URL(WebResource): @@ -621,7 +724,7 @@ def filename(self) -> str: return os.path.basename(url.path) -class File(Resource): +class File(Resource[FileReader]): """A resource that exists on the local machine's storage to be uploaded. Parameters @@ -661,9 +764,9 @@ def filename(self) -> str: return os.path.basename(self.path) return self._filename - @contextlib.asynccontextmanager - @typing.no_type_check - async def stream(self, *, executor: typing.Optional[concurrent.futures.Executor] = None) -> FileReader: + def stream( + self, *, executor: typing.Optional[concurrent.futures.Executor] = None, head_only: bool = False, + ) -> AsyncReaderContextManager[FileReader]: """Start streaming the resource using a thread pool executor. Parameters @@ -672,10 +775,13 @@ async def stream(self, *, executor: typing.Optional[concurrent.futures.Executor] The executor to run the blocking read operations in. If `None`, the default executor for the running event loop will be used instead. + head_only : bool + Not used. Provided only to match the underlying interface. Returns ------- - FileReader - The file reader object. + AsyncReaderContextManager[FileReader] + An async context manager that when entered, produces the + data stream. """ - yield FileReader(self.filename, None, executor, self.path) + return _NoOpAsyncReaderContextManagerImpl(FileReader(self.filename, None, executor, self.path)) diff --git a/hikari/utilities/undefined.py b/hikari/utilities/undefined.py index 22f8eff991..458a8e18e2 100644 --- a/hikari/utilities/undefined.py +++ b/hikari/utilities/undefined.py @@ -54,7 +54,7 @@ class _UndefinedTypeWrapper(_UndefinedType, enum.Enum): setattr(_UndefinedTypeWrapper, "__new__", NotImplemented) UndefinedType = typing.Literal[_UndefinedTypeWrapper.UNDEFINED_VALUE] -"""A type hint for the literal `UNDEFINED` object.""" +"""Type hint for the literal `UNDEFINED` object.""" # noinspection PyTypeChecker UNDEFINED: typing.Final[UndefinedType] = _UndefinedTypeWrapper.UNDEFINED_VALUE diff --git a/scripts/test_twemoji_mapping.py b/scripts/test_twemoji_mapping.py index a2f458e3de..7837ad9888 100644 --- a/scripts/test_twemoji_mapping.py +++ b/scripts/test_twemoji_mapping.py @@ -81,7 +81,8 @@ async def try_fetch(i, n, emoji_surrogates, name): ex = None for _ in range(5): try: - await emoji.__aiter__().__anext__() + async with emoji.stream(): + pass except Exception as _ex: ex = _ex else: From 473e202ae1a7a34ca4aa468e6a81d65a807bec7f Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Mon, 15 Jun 2020 08:47:47 +0100 Subject: [PATCH 515/922] Added head-only to WebReader, fixed twemoji mapping. --- ci/flake8.nox.py | 1 + ci/mypy.nox.py | 1 + hikari/utilities/files.py | 21 +++++++++++++++++---- scripts/test_twemoji_mapping.py | 2 +- 4 files changed, 20 insertions(+), 5 deletions(-) diff --git a/ci/flake8.nox.py b/ci/flake8.nox.py index 379f8b8c1d..c6df28b101 100644 --- a/ci/flake8.nox.py +++ b/ci/flake8.nox.py @@ -23,6 +23,7 @@ @nox.session(reuse_venv=True, default=True) def flake8(session: nox.Session) -> None: + """Run code linting, SAST, and analysis.""" session.install("-r", "requirements.txt", "-r", "flake-requirements.txt") session.run( diff --git a/ci/mypy.nox.py b/ci/mypy.nox.py index 3dc67ce548..0165127171 100644 --- a/ci/mypy.nox.py +++ b/ci/mypy.nox.py @@ -22,6 +22,7 @@ @nox.session(reuse_venv=True, default=True) def mypy(session: nox.Session) -> None: + """Perform static type analysis on Python source code.""" session.install("-r", "requirements.txt", "-r", "mypy-requirements.txt") session.run( "mypy", "-p", config.MAIN_PACKAGE, "--config", config.MYPY_INI, "--junit-xml", config.MYPY_JUNIT_OUTPUT_PATH, diff --git a/hikari/utilities/files.py b/hikari/utilities/files.py index 3d43291bf2..9735c2e01c 100644 --- a/hikari/utilities/files.py +++ b/hikari/utilities/files.py @@ -299,13 +299,23 @@ class WebReader(AsyncReader): size: typing.Optional[int] """The size of the resource, if known.""" + head_only: bool + """If `True`, then only the HEAD was requested. + + In this case, neither `__aiter__` nor `read` would return anything other + than an empty byte string. + """ + async def read(self) -> bytes: - return await self.stream.read() + return b"" if self.head_only else await self.stream.read() async def __aiter__(self) -> typing.AsyncGenerator[typing.Any, bytes]: - while not self.stream.at_eof(): - chunk = await self.stream.readchunk() - yield chunk[0] + if self.head_only: + yield b"" + else: + while not self.stream.at_eof(): + chunk, _ = await self.stream.readchunk() + yield chunk @attr.s(auto_attribs=True, slots=True) @@ -327,12 +337,14 @@ async def __aiter__(self) -> typing.AsyncGenerator[typing.Any, bytes]: path = await self.loop.run_in_executor(self.executor, self._expand, self.path) fp = await self.loop.run_in_executor(self.executor, self._open, path) + try: while True: chunk = await self.loop.run_in_executor(self.executor, self._read_chunk, fp, _MAGIC) yield chunk if len(chunk) < _MAGIC: break + finally: await self.loop.run_in_executor(self.executor, self._close, fp) @@ -433,6 +445,7 @@ async def __aenter__(self) -> WebReader: charset=resp.charset, mimetype=mimetype, size=resp.content_length, + head_only=self._head_only, ) else: raise await http_client.generate_error_response(resp) diff --git a/scripts/test_twemoji_mapping.py b/scripts/test_twemoji_mapping.py index 7837ad9888..4248774360 100644 --- a/scripts/test_twemoji_mapping.py +++ b/scripts/test_twemoji_mapping.py @@ -81,7 +81,7 @@ async def try_fetch(i, n, emoji_surrogates, name): ex = None for _ in range(5): try: - async with emoji.stream(): + async with emoji.stream(head_only=True): pass except Exception as _ex: ex = _ex From 577736378cdf099f062746ff2b8d58b962d0821d Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Mon, 15 Jun 2020 08:49:31 +0100 Subject: [PATCH 516/922] Fixes #385 where Windows Runners were not running in CI merge trains. --- ci/gitlab/bases.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ci/gitlab/bases.yml b/ci/gitlab/bases.yml index 58a2ef8258..beebeca080 100644 --- a/ci/gitlab/bases.yml +++ b/ci/gitlab/bases.yml @@ -70,6 +70,8 @@ ### Shared windows runner. ### .win32: + extends: + - .reactive-job tags: - shared-windows - windows From b7f83a3e47b23d3afb0669e0532025a8c5310981 Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Mon, 15 Jun 2020 08:54:45 +0100 Subject: [PATCH 517/922] Fixes #387 logging reconnections after DNS resolution failure. --- hikari/net/gateway.py | 36 +++++++++++++++++++++--------------- 1 file changed, 21 insertions(+), 15 deletions(-) diff --git a/hikari/net/gateway.py b/hikari/net/gateway.py index 71f1c7c837..b3a223e99c 100644 --- a/hikari/net/gateway.py +++ b/hikari/net/gateway.py @@ -301,6 +301,7 @@ async def _run_once(self) -> bool: try: self.logger.debug("creating websocket connection to %s", self.url) self._ws = await self._create_ws(self.url) + self.connected_at = self._now() self._zlib = zlib.decompressobj() @@ -353,6 +354,7 @@ async def _run_once(self) -> bool: self.logger.warning("unexpected socket closure, will attempt to reconnect") else: self._backoff.reset() + return not self._request_close_event.is_set() except errors.GatewayServerClosedConnectionError as ex: @@ -463,7 +465,7 @@ async def _handshake(self) -> None: self.heartbeat_interval = message["d"]["heartbeat_interval"] / 1_000.0 - self.logger.debug("received HELLO, heartbeat interval is %s", self.heartbeat_interval) + self.logger.info("received HELLO, heartbeat interval is %s", self.heartbeat_interval) if self.session_id is not None: # RESUME! @@ -577,11 +579,19 @@ async def _receive_json_payload(self) -> data_binding.JSONObject: if message.type == aiohttp.WSMsgType.BINARY: n, string = await self._receive_zlib_message(message.data) - self._log_debug_payload(string, "received %s zlib encoded packets", n) - elif message.type == aiohttp.WSMsgType.TEXT: + payload = data_binding.load_json(string) # type: ignore + self._log_debug_payload( + string, "received %s zlib encoded packets [t:%s, op:%s]", n, payload.get("t"), payload.get("op"), + ) + return payload + + if message.type == aiohttp.WSMsgType.TEXT: string = message.data - self._log_debug_payload(string, "received text payload") - elif message.type == aiohttp.WSMsgType.CLOSE: + payload = data_binding.load_json(string) # type: ignore + self._log_debug_payload(string, "received text payload [t:%s, op:%s]", payload.get("t"), payload.get("op")) + return payload + + if message.type == aiohttp.WSMsgType.CLOSE: close_code = self._ws.close_code self.logger.debug("connection closed with code %s", close_code) @@ -601,17 +611,13 @@ async def _receive_json_payload(self) -> data_binding.JSONObject: # Always try to resume if possible first. raise errors.GatewayServerClosedConnectionError(reason, close_code, can_reconnect, can_reconnect, True) - elif message.type == aiohttp.WSMsgType.CLOSING or message.type == aiohttp.WSMsgType.CLOSED: + if message.type == aiohttp.WSMsgType.CLOSING or message.type == aiohttp.WSMsgType.CLOSED: raise self._SocketClosed() - else: - # Assume exception for now. - ex = self._ws.exception() - self.logger.debug("encountered unexpected error", exc_info=ex) - raise errors.GatewayError("Unexpected websocket exception from gateway") from ex - # We assume this is always a JSON object, I'd rather not cast here and waste - # CPU time as this is somewhat performance critical for large bots. - return data_binding.load_json(string) # type: ignore + # Assume exception for now. + ex = self._ws.exception() + self.logger.debug("encountered unexpected error", exc_info=ex) + raise errors.GatewayError("Unexpected websocket exception from gateway") from ex async def _receive_zlib_message(self, first_packet: bytes) -> typing.Tuple[int, str]: # Alloc new array each time; this prevents consuming a large amount of @@ -639,7 +645,7 @@ async def _receive_raw(self) -> aiohttp.WSMessage: async def _send_json(self, payload: data_binding.JSONObject) -> None: await self.ratelimiter.acquire() message = data_binding.dump_json(payload) - self._log_debug_payload(message, "sending json payload") + self._log_debug_payload(message, "sending json payload [t:%s]", payload.get("t")) await self._ws.send_str(message) def _dispatch(self, event_name: str, event: data_binding.JSONObject) -> typing.Coroutine[None, typing.Any, None]: From be25a43af5513df109e70bb224da55dd50b74f0d Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Mon, 15 Jun 2020 08:58:45 +0100 Subject: [PATCH 518/922] Fixes #388, Windows raising NotImplementedError on startup. --- hikari/impl/gateway_zookeeper.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/hikari/impl/gateway_zookeeper.py b/hikari/impl/gateway_zookeeper.py index 7f831d75ea..7937c1b067 100644 --- a/hikari/impl/gateway_zookeeper.py +++ b/hikari/impl/gateway_zookeeper.py @@ -23,6 +23,7 @@ import abc import asyncio +import contextlib import datetime import signal import time @@ -404,4 +405,5 @@ def _map_signal_handlers( valid_interrupts = signal.valid_signals() for interrupt in self._SIGNALS: if (code := getattr(signal, interrupt, None)) in valid_interrupts: - mapping_function(code, *args) + with contextlib.suppress(NotImplementedError): + mapping_function(code, *args) From bb51b38deacbaefb82bd3230f6b7cd0a1e144c5f Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Mon, 15 Jun 2020 10:18:53 +0100 Subject: [PATCH 519/922] Fixed typechecking in gateway.py --- hikari/net/gateway.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/hikari/net/gateway.py b/hikari/net/gateway.py index b3a223e99c..3ceb647587 100644 --- a/hikari/net/gateway.py +++ b/hikari/net/gateway.py @@ -579,7 +579,7 @@ async def _receive_json_payload(self) -> data_binding.JSONObject: if message.type == aiohttp.WSMsgType.BINARY: n, string = await self._receive_zlib_message(message.data) - payload = data_binding.load_json(string) # type: ignore + payload: data_binding.JSONObject = data_binding.load_json(string) # type: ignore self._log_debug_payload( string, "received %s zlib encoded packets [t:%s, op:%s]", n, payload.get("t"), payload.get("op"), ) @@ -587,7 +587,7 @@ async def _receive_json_payload(self) -> data_binding.JSONObject: if message.type == aiohttp.WSMsgType.TEXT: string = message.data - payload = data_binding.load_json(string) # type: ignore + payload: data_binding.JSONObject = data_binding.load_json(string) # type: ignore self._log_debug_payload(string, "received text payload [t:%s, op:%s]", payload.get("t"), payload.get("op")) return payload From 6c48ba5a1cd241c7f37483d75de2e69b0cc8d66d Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Mon, 15 Jun 2020 13:40:04 +0100 Subject: [PATCH 520/922] Removed kwargs from http_client constructor and replaced with HTTPSettings obj. --- hikari/net/gateway.py | 14 +-- hikari/net/http_client.py | 140 ++++++--------------------- hikari/net/rest.py | 14 +-- tests/hikari/net/test_http_client.py | 99 ++++++++++--------- 4 files changed, 86 insertions(+), 181 deletions(-) diff --git a/hikari/net/gateway.py b/hikari/net/gateway.py index 3ceb647587..68fc9a8f6d 100644 --- a/hikari/net/gateway.py +++ b/hikari/net/gateway.py @@ -173,19 +173,7 @@ def __init__( use_compression: bool = True, version: int = 6, ) -> None: - super().__init__( - allow_redirects=config.allow_redirects, - connector=config.tcp_connector_factory() if config.tcp_connector_factory else None, - debug=debug, - logger=reflect.get_logger(self, str(shard_id)), - proxy_auth=config.proxy_auth, - proxy_headers=config.proxy_headers, - proxy_url=config.proxy_url, - ssl_context=config.ssl_context, - verify_ssl=config.verify_ssl, - timeout=config.request_timeout, - trust_env=config.trust_env, - ) + super().__init__(config=config, debug=debug, logger=reflect.get_logger(self, str(shard_id))) self._activity: typing.Union[undefined.UndefinedType, None, presences.Activity] = initial_activity self._app = app self._backoff = rate_limits.ExponentialBackOff(base=1.85, maximum=600, initial_increment=2) diff --git a/hikari/net/http_client.py b/hikari/net/http_client.py index ff23535221..5e3054b394 100644 --- a/hikari/net/http_client.py +++ b/hikari/net/http_client.py @@ -24,7 +24,6 @@ import http import json import logging -import ssl import types import typing @@ -32,6 +31,7 @@ import aiohttp.typedefs from hikari import errors +from hikari.net import http_settings from hikari.net import tracing from hikari.utilities import data_binding @@ -60,51 +60,21 @@ class HTTPClient(abc.ABC): # pylint:disable=too-many-instance-attributes Parameters ---------- - allow_redirects : bool - Whether to allow redirects or not. Defaults to `False`. - connector : aiohttp.BaseConnector or None - Optional aiohttp _connector info for making an HTTP connection + config : hikari.net.http_settings.HTTPSettings or None + Optional aiohttp settings for making HTTP connections. + If `None`, defaults are used. debug : bool Defaults to `False`. If `True`, then a lot of contextual information regarding low-level HTTP communication will be logged to the _debug logger on this class. - proxy_auth : aiohttp.BasicAuth or None - Optional authorization to be used if using a proxy. - proxy_url : str or None - Optional proxy URL to use for HTTP requests. - ssl_context : ssl.SSLContext or None - The optional SSL context to be used. - verify_ssl : bool - Whether or not the client should enforce SSL signed certificate - verification. If 1 it will ignore potentially malicious - SSL certificates. - timeout : float or None - The optional _request_timeout for all HTTP requests. - trust_env : bool - If `True`, and no proxy info is given, then `HTTP_PROXY` and - `HTTPS_PROXY` will be used from the environment variables if present. - Any proxy credentials will be read from the user's `netrc` file - (https://www.gnu.org/software/inetutils/manual/html_node/The-_002enetrc-file.html) - If `False`, then this information is instead ignored. - Defaults to `False`. """ __slots__ = ( "logger", "_client_session", - "_allow_redirects", - "_connector", + "_config", "_debug", - "_json_deserialize", - "_json_serialize", - "_proxy_auth", - "_proxy_headers", - "_proxy_url", - "_ssl_context", - "_request_timeout", "_tracers", - "_trust_env", - "_verify_ssl", ) _APPLICATION_JSON: typing.Final[str] = "application/json" @@ -115,83 +85,34 @@ class HTTPClient(abc.ABC): # pylint:disable=too-many-instance-attributes logger: logging.Logger """The logger to use for this object.""" - _allow_redirects: bool - """`True` if HTTP redirects are enabled, or `False` otherwise.""" - - _connector: typing.Optional[aiohttp.BaseConnector] - """The base _connector for the `aiohttp.ClientSession`, if provided.""" + _config: http_settings.HTTPSettings + """HTTP settings in-use.""" _debug: bool """`True` if _debug mode is enabled. `False` otherwise.""" - _proxy_auth: typing.Optional[aiohttp.BasicAuth] - """Proxy authorization to use.""" - - _proxy_headers: typing.Optional[typing.Mapping[str, str]] - """A set of headers to provide to a proxy server.""" - - _proxy_url: typing.Optional[str] - """An optional proxy URL to send requests to.""" - - _ssl_context: typing.Optional[ssl.SSLContext] - """The custom SSL context to use.""" - - _request_timeout: typing.Optional[float] - """The HTTP request _request_timeout to abort requests after.""" - _tracers: typing.List[tracing.BaseTracer] """Request _tracers. These can be used to intercept HTTP request events on a low level. """ - _trust_env: bool - """Whether to take notice of proxy environment variables. - - If `True`, and no proxy info is given, then `HTTP_PROXY` and - `HTTPS_PROXY` will be used from the environment variables if present. - Any proxy credentials will be read from the user's `netrc` file - (https://www.gnu.org/software/inetutils/manual/html_node/The-_002enetrc-file.html) - If `False`, then this information is instead ignored. - """ - - _verify_ssl: bool - """Whether SSL certificates should be verified for each request. - - When this is `True` then an exception will be raised whenever invalid SSL - certificates are received. When this is `False` unrecognised certificates - that may be illegitimate are accepted and ignored. - """ - def __init__( self, logger: logging.Logger, *, - allow_redirects: bool = False, - connector: typing.Optional[aiohttp.BaseConnector] = None, + config: typing.Optional[http_settings.HTTPSettings] = None, debug: bool = False, - proxy_auth: typing.Optional[aiohttp.BasicAuth] = None, - proxy_headers: typing.Optional[aiohttp.typedefs.LooseHeaders] = None, - proxy_url: typing.Optional[str] = None, - ssl_context: typing.Optional[ssl.SSLContext] = None, - verify_ssl: bool = True, - timeout: typing.Optional[float] = None, - trust_env: bool = False, ) -> None: self.logger = logger + if config is None: + config = http_settings.HTTPSettings() + self._client_session: typing.Optional[aiohttp.ClientSession] = None - self._allow_redirects = allow_redirects - self._connector = connector + self._config = config self._debug = debug - self._proxy_auth = proxy_auth - self._proxy_headers = proxy_headers - self._proxy_url = proxy_url - self._ssl_context = ssl_context - self._request_timeout = timeout - self._trust_env = trust_env self._tracers = [(tracing.DebugTracer(self.logger) if debug else tracing.CFRayTracer(self.logger))] - self._verify_ssl = verify_ssl async def __aenter__(self) -> HTTPClient: return self @@ -208,7 +129,7 @@ async def close(self) -> None: self.logger.debug("closed client session object %r", self._client_session) self._client_session = None - def client_session(self) -> aiohttp.ClientSession: + def get_client_session(self) -> aiohttp.ClientSession: """Acquire a client session to make requests with. !!! warning @@ -218,15 +139,18 @@ def client_session(self) -> aiohttp.ClientSession: Generally you should not need to use this unless you are interfacing with the Hikari API directly. + This is not thread-safe. + Returns ------- aiohttp.ClientSession The client session to use for requests. """ if self._client_session is None: + connector = self._config.tcp_connector_factory() if self._config.tcp_connector_factory is not None else None self._client_session = aiohttp.ClientSession( - connector=self._connector, - trust_env=self._trust_env, + connector=connector, + trust_env=self._config.trust_env, version=aiohttp.HttpVersion11, json_serialize=json.dumps, trace_configs=[t.trace_config for t in self._tracers], @@ -279,18 +203,18 @@ def _perform_request( trace_request_ctx = types.SimpleNamespace() trace_request_ctx.request_body = body - return self.client_session().request( + return self.get_client_session().request( method=method, url=url, params=query, headers=headers, - allow_redirects=self._allow_redirects, - proxy=self._proxy_url, - proxy_auth=self._proxy_auth, - proxy_headers=self._proxy_headers, - verify_ssl=self._verify_ssl, - ssl_context=self._ssl_context, - timeout=self._request_timeout, + allow_redirects=self._config.allow_redirects, + proxy=self._config.proxy_url, + proxy_auth=self._config.proxy_auth, + proxy_headers=self._config.proxy_headers, + verify_ssl=self._config.verify_ssl, + ssl_context=self._config.ssl_context, + timeout=self._config.request_timeout, trace_request_ctx=trace_request_ctx, **kwargs, ) @@ -320,16 +244,16 @@ async def _create_ws( The websocket to use. """ self.logger.debug("creating underlying websocket object from HTTP session") - return await self.client_session().ws_connect( + return await self.get_client_session().ws_connect( url=url, compress=compress, autoping=auto_ping, max_msg_size=max_msg_size, - proxy=self._proxy_url, - proxy_auth=self._proxy_auth, - proxy_headers=self._proxy_headers, - verify_ssl=self._verify_ssl, - ssl_context=self._ssl_context, + proxy=self._config.proxy_url, + proxy_auth=self._config.proxy_auth, + proxy_headers=self._config.proxy_headers, + verify_ssl=self._config.verify_ssl, + ssl_context=self._config.ssl_context, ) diff --git a/hikari/net/rest.py b/hikari/net/rest.py index 337e4def75..26d0031242 100644 --- a/hikari/net/rest.py +++ b/hikari/net/rest.py @@ -119,19 +119,7 @@ def __init__( rest_url: typing.Union[undefined.UndefinedType, str] = undefined.UNDEFINED, version: int, ) -> None: - super().__init__( - allow_redirects=config.allow_redirects, - connector=config.tcp_connector_factory() if config.tcp_connector_factory else None, - debug=debug, - logger=reflect.get_logger(self), - proxy_auth=config.proxy_auth, - proxy_headers=config.proxy_headers, - proxy_url=config.proxy_url, - ssl_context=config.ssl_context, - verify_ssl=config.verify_ssl, - timeout=config.request_timeout, - trust_env=config.trust_env, - ) + super().__init__(config=config, debug=debug, logger=reflect.get_logger(self)) self.buckets = buckets.RESTBucketManager() self.global_rate_limit = rate_limits.ManualRateLimiter() self.version = version diff --git a/tests/hikari/net/test_http_client.py b/tests/hikari/net/test_http_client.py index aa093da96d..9eb6e379ad 100644 --- a/tests/hikari/net/test_http_client.py +++ b/tests/hikari/net/test_http_client.py @@ -23,6 +23,7 @@ import pytest from hikari.net import http_client +from hikari.net import http_settings from hikari.net import tracing from tests.hikari import _helpers @@ -57,26 +58,27 @@ async def test_DebugTracer_used_for_debug(self): @pytest.mark.asyncio class TestAcquireClientSession: async def test_acquire_creates_new_session_if_one_does_not_exist(self, client): - client._connector = mock.MagicMock() - client._trust_env = mock.MagicMock() + client._config = http_settings.HTTPSettings() + client._config.tcp_connector_factory = mock.MagicMock() + client._config.trust_env = mock.MagicMock() client._client_session = None - cs = client.client_session() + cs = client.get_client_session() assert client._client_session is cs aiohttp.ClientSession.assert_called_once_with( - connector=client._connector, - trust_env=client._trust_env, + connector=client._config.tcp_connector_factory(), + trust_env=client._config.trust_env, version=aiohttp.HttpVersion11, json_serialize=json.dumps, trace_configs=[t.trace_config for t in client._tracers], ) async def test_acquire_repeated_calls_caches_client_session(self, client): - cs = client.client_session() + cs = client.get_client_session() for i in range(10): aiohttp.ClientSession.reset_mock() - assert cs is client.client_session() + assert cs is client.get_client_session() aiohttp.ClientSession.assert_not_called() @@ -97,13 +99,14 @@ async def test_close_when_running(self, client, client_session): @pytest.mark.asyncio class TestPerformRequest: async def test_perform_request_form_data(self, client, client_session): - client._allow_redirects = mock.MagicMock() - client._proxy_url = mock.MagicMock() - client._proxy_auth = mock.MagicMock() - client._proxy_headers = mock.MagicMock() - client._verify_ssl = mock.MagicMock() - client._ssl_context = mock.MagicMock() - client._request_timeout = mock.MagicMock() + client._config = http_settings.HTTPSettings() + client._config.allow_redirects = mock.MagicMock() + client._config.proxy_url = mock.MagicMock() + client._config.proxy_auth = mock.MagicMock() + client._config.proxy_headers = mock.MagicMock() + client._config.verify_ssl = mock.MagicMock() + client._config.ssl_context = mock.MagicMock() + client._config.request_timeout = mock.MagicMock() form_data = aiohttp.FormData() @@ -124,24 +127,25 @@ async def test_perform_request_form_data(self, client, client_session): params={"foo": "bar"}, headers={"X-Foo-Count": "122"}, data=form_data, - allow_redirects=client._allow_redirects, - proxy=client._proxy_url, - proxy_auth=client._proxy_auth, - proxy_headers=client._proxy_headers, - verify_ssl=client._verify_ssl, - ssl_context=client._ssl_context, - timeout=client._request_timeout, + allow_redirects=client._config.allow_redirects, + proxy=client._config.proxy_url, + proxy_auth=client._config.proxy_auth, + proxy_headers=client._config.proxy_headers, + verify_ssl=client._config.verify_ssl, + ssl_context=client._config.ssl_context, + timeout=client._config.request_timeout, trace_request_ctx=trace_request_ctx, ) async def test_perform_request_json(self, client, client_session): - client._allow_redirects = mock.MagicMock() - client._proxy_url = mock.MagicMock() - client._proxy_auth = mock.MagicMock() - client._proxy_headers = mock.MagicMock() - client._verify_ssl = mock.MagicMock() - client._ssl_context = mock.MagicMock() - client._request_timeout = mock.MagicMock() + client._config = http_settings.HTTPSettings() + client._config.allow_redirects = mock.MagicMock() + client._config.proxy_url = mock.MagicMock() + client._config.proxy_auth = mock.MagicMock() + client._config.proxy_headers = mock.MagicMock() + client._config.verify_ssl = mock.MagicMock() + client._config.ssl_context = mock.MagicMock() + client._config.request_timeout = mock.MagicMock() req = {"hello": "world"} @@ -162,13 +166,13 @@ async def test_perform_request_json(self, client, client_session): params={"foo": "bar"}, headers={"X-Foo-Count": "122"}, json=req, - allow_redirects=client._allow_redirects, - proxy=client._proxy_url, - proxy_auth=client._proxy_auth, - proxy_headers=client._proxy_headers, - verify_ssl=client._verify_ssl, - ssl_context=client._ssl_context, - timeout=client._request_timeout, + allow_redirects=client._config.allow_redirects, + proxy=client._config.proxy_url, + proxy_auth=client._config.proxy_auth, + proxy_headers=client._config.proxy_headers, + verify_ssl=client._config.verify_ssl, + ssl_context=client._config.ssl_context, + timeout=client._config.request_timeout, trace_request_ctx=trace_request_ctx, ) @@ -176,13 +180,14 @@ async def test_perform_request_json(self, client, client_session): @pytest.mark.asyncio class TestCreateWs: async def test_create_ws(self, client, client_session): - client._allow_redirects = mock.MagicMock() - client._proxy_url = mock.MagicMock() - client._proxy_auth = mock.MagicMock() - client._proxy_headers = mock.MagicMock() - client._verify_ssl = mock.MagicMock() - client._ssl_context = mock.MagicMock() - client._request_timeout = mock.MagicMock() + client._config = http_settings.HTTPSettings() + client._config.allow_redirects = mock.MagicMock() + client._config.proxy_url = mock.MagicMock() + client._config.proxy_auth = mock.MagicMock() + client._config.proxy_headers = mock.MagicMock() + client._config.verify_ssl = mock.MagicMock() + client._config.ssl_context = mock.MagicMock() + client._config.request_timeout = mock.MagicMock() expected_ws = mock.MagicMock() client_session.ws_connect = mock.AsyncMock(return_value=expected_ws) @@ -196,9 +201,9 @@ async def test_create_ws(self, client, client_session): compress=5, autoping=True, max_msg_size=3, - proxy=client._proxy_url, - proxy_auth=client._proxy_auth, - proxy_headers=client._proxy_headers, - verify_ssl=client._verify_ssl, - ssl_context=client._ssl_context, + proxy=client._config.proxy_url, + proxy_auth=client._config.proxy_auth, + proxy_headers=client._config.proxy_headers, + verify_ssl=client._config.verify_ssl, + ssl_context=client._config.ssl_context, ) From 2576344c1ec11d4d5f1ec848c98c9e2a8fb7ec51 Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Mon, 15 Jun 2020 16:39:09 +0100 Subject: [PATCH 521/922] Added `@typing.final` to stuff and added missing typehints. --- hikari/_about.py | 3 + hikari/api/event_dispatcher.py | 31 ++-- hikari/events/base.py | 12 +- hikari/impl/bot.py | 31 ++-- hikari/impl/cache.py | 1 + hikari/impl/entity_factory.py | 1 + hikari/impl/event_manager_core.py | 54 +++--- hikari/impl/gateway_zookeeper.py | 2 +- hikari/models/applications.py | 2 + hikari/models/audit_logs.py | 2 + hikari/models/bases.py | 1 + hikari/models/channels.py | 2 + hikari/models/embeds.py | 28 ++- hikari/models/emojis.py | 15 +- hikari/models/guilds.py | 9 + hikari/models/intents.py | 1 + hikari/models/invites.py | 1 + hikari/models/messages.py | 3 + hikari/models/permissions.py | 1 + hikari/models/presences.py | 3 + hikari/models/users.py | 2 + hikari/models/webhooks.py | 1 + hikari/net/gateway.py | 5 + hikari/net/http_client.py | 10 +- hikari/net/http_settings.py | 1 + hikari/net/rate_limits.py | 2 + hikari/net/rest.py | 29 ++- hikari/net/rest_utils.py | 1 + hikari/net/routes.py | 288 ++++++++++++------------------ hikari/net/strings.py | 4 +- hikari/net/tracing.py | 6 + hikari/utilities/data_binding.py | 2 + hikari/utilities/date.py | 18 +- hikari/utilities/files.py | 7 +- hikari/utilities/reflect.py | 3 - hikari/utilities/snowflake.py | 1 + hikari/utilities/undefined.py | 1 + 37 files changed, 321 insertions(+), 263 deletions(-) diff --git a/hikari/_about.py b/hikari/_about.py index 79e4fd1a05..f65121ebc5 100644 --- a/hikari/_about.py +++ b/hikari/_about.py @@ -19,6 +19,9 @@ from __future__ import annotations +# DO NOT ADD TYPE HINTS TO THESE FIELDS. THESE ARE AUTOMATICALLY UPDATED +# FROM THE CI SCRIPT AND DOING THIS MAY LEAD TO THE DEPLOY PROCESS FAILING. + __author__ = "Nekokatt" __ci__ = "https://gitlab.com/nekokatt/hikari/pipelines" __copyright__ = "© 2019-2020 Nekokatt" diff --git a/hikari/api/event_dispatcher.py b/hikari/api/event_dispatcher.py index 1ebd7d3115..f9a380d5c8 100644 --- a/hikari/api/event_dispatcher.py +++ b/hikari/api/event_dispatcher.py @@ -29,12 +29,6 @@ if typing.TYPE_CHECKING: from hikari.events import base - _EventT = typing.TypeVar("_EventT", bound=base.Event) - _PredicateT = typing.Callable[[base.Event], typing.Union[bool, typing.Coroutine[None, typing.Any, bool]]] - _SyncCallbackT = typing.Callable[[base.Event], None] - _AsyncCallbackT = typing.Callable[[base.Event], typing.Coroutine[None, typing.Any, None]] - _CallbackT = typing.Union[_SyncCallbackT, _AsyncCallbackT] - class IEventDispatcherBase(abc.ABC): """Base interface for event dispatcher implementations. @@ -46,6 +40,13 @@ class IEventDispatcherBase(abc.ABC): __slots__ = () + if typing.TYPE_CHECKING: + EventT = typing.TypeVar("EventT", bound=base.Event) + PredicateT = typing.Callable[[base.Event], typing.Union[bool, typing.Coroutine[None, typing.Any, bool]]] + SyncCallbackT = typing.Callable[[base.Event], None] + AsyncCallbackT = typing.Callable[[base.Event], typing.Coroutine[None, typing.Any, None]] + CallbackT = typing.Union[SyncCallbackT, AsyncCallbackT] + @abc.abstractmethod def dispatch(self, event: base.Event) -> asyncio.Future[typing.Any]: """Dispatch an event. @@ -126,9 +127,9 @@ async def on_everyone_mentioned(event): @abc.abstractmethod def subscribe( self, - event_type: typing.Type[_EventT], - callback: typing.Callable[[_EventT], typing.Union[typing.Coroutine[None, typing.Any, None], None]], - ) -> typing.Callable[[_EventT], typing.Coroutine[None, typing.Any, None]]: + event_type: typing.Type[EventT], + callback: typing.Callable[[EventT], typing.Union[typing.Coroutine[None, typing.Any, None], None]], + ) -> typing.Callable[[EventT], typing.Coroutine[None, typing.Any, None]]: """Subscribe a given callback to a given event type. Parameters @@ -172,8 +173,8 @@ async def on_message(event): @abc.abstractmethod def unsubscribe( self, - event_type: typing.Type[_EventT], - callback: typing.Callable[[_EventT], typing.Coroutine[None, typing.Any, None]], + event_type: typing.Type[EventT], + callback: typing.Callable[[EventT], typing.Coroutine[None, typing.Any, None]], ) -> None: """Unsubscribe a given callback from a given event type, if present. @@ -202,8 +203,8 @@ async def on_message(event): @abc.abstractmethod def listen( - self, event_type: typing.Union[undefined.UndefinedType, typing.Type[_EventT]] = undefined.UNDEFINED, - ) -> typing.Callable[[_CallbackT], _CallbackT]: + self, event_type: typing.Union[undefined.UndefinedType, typing.Type[EventT]] = undefined.UNDEFINED, + ) -> typing.Callable[[CallbackT], CallbackT]: """Generate a decorator to subscribe a callback to an event type. This is a second-order decorator. @@ -232,8 +233,8 @@ def listen( @abc.abstractmethod async def wait_for( - self, event_type: typing.Type[_EventT], predicate: _PredicateT, timeout: typing.Union[float, int, None], - ) -> _EventT: + self, event_type: typing.Type[EventT], predicate: PredicateT, timeout: typing.Union[float, int, None], + ) -> EventT: """Wait for a given event to occur once, then return the event. Parameters diff --git a/hikari/events/base.py b/hikari/events/base.py index 0648ab1b21..6d1dbd23f5 100644 --- a/hikari/events/base.py +++ b/hikari/events/base.py @@ -42,7 +42,7 @@ class Event(abc.ABC): """The base class that all events inherit from.""" -_EventT = typing.TypeVar("_EventT", contravariant=True) +EventT = typing.TypeVar("EventT", contravariant=True) _REQUIRED_INTENTS_ATTR: typing.Final[str] = "___required_intents___" _NO_THROW_ATTR: typing.Final[str] = "___no_throw___" @@ -66,7 +66,7 @@ def get_required_intents_for(event_type: typing.Type[Event]) -> typing.Collectio def requires_intents( first: intents.Intent, *rest: intents.Intent -) -> typing.Callable[[typing.Type[_EventT]], typing.Type[_EventT]]: +) -> typing.Callable[[typing.Type[EventT]], typing.Type[EventT]]: """Decorate an event type to define what intents it requires. Parameters @@ -80,27 +80,27 @@ def requires_intents( """ - def decorator(cls: typing.Type[_EventT]) -> typing.Type[_EventT]: + def decorator(cls: typing.Type[EventT]) -> typing.Type[EventT]: setattr(cls, _REQUIRED_INTENTS_ATTR, [first, *rest]) return cls return decorator -def no_catch() -> typing.Callable[[typing.Type[_EventT]], typing.Type[_EventT]]: +def no_catch() -> typing.Callable[[typing.Type[EventT]], typing.Type[EventT]]: """Decorate an event type to indicate errors should not be handled. This is useful for exception event types that you do not want to have invoked recursively. """ - def decorator(cls: typing.Type[_EventT]) -> typing.Type[_EventT]: + def decorator(cls: typing.Type[EventT]) -> typing.Type[EventT]: setattr(cls, _NO_THROW_ATTR, True) return cls return decorator -def is_no_catch_event(obj: typing.Union[_EventT, typing.Type[_EventT]]) -> bool: +def is_no_catch_event(obj: typing.Union[EventT, typing.Type[EventT]]) -> bool: """Return True if this event is marked as `no_catch`.""" return typing.cast(bool, getattr(obj, _NO_THROW_ATTR, False)) diff --git a/hikari/impl/bot.py b/hikari/impl/bot.py index e56af5511a..0c61d9e82d 100644 --- a/hikari/impl/bot.py +++ b/hikari/impl/bot.py @@ -52,12 +52,6 @@ from hikari.models import gateway as gateway_models from hikari.models import intents as intents_ - _EventT = typing.TypeVar("_EventT", bound=base_events.Event) - _PredicateT = typing.Callable[[base_events.Event], typing.Union[bool, typing.Coroutine[None, typing.Any, bool]]] - _SyncCallbackT = typing.Callable[[base_events.Event], None] - _AsyncCallbackT = typing.Callable[[base_events.Event], typing.Coroutine[None, typing.Any, None]] - _CallbackT = typing.Union[_SyncCallbackT, _AsyncCallbackT] - class BotAppImpl(gateway_zookeeper.AbstractGatewayZookeeper, bot.IBotApp): """Implementation of an auto-sharded bot application. @@ -148,6 +142,13 @@ class BotAppImpl(gateway_zookeeper.AbstractGatewayZookeeper, bot.IBotApp): If sharding information is provided, but is unfeasible or invalid. """ + if typing.TYPE_CHECKING: + EventT = typing.TypeVar("EventT", bound=base_events.Event) + PredicateT = typing.Callable[[base_events.Event], typing.Union[bool, typing.Coroutine[None, typing.Any, bool]]] + SyncCallbackT = typing.Callable[[base_events.Event], None] + AsyncCallbackT = typing.Callable[[base_events.Event], typing.Coroutine[None, typing.Any, None]] + CallbackT = typing.Union[SyncCallbackT, AsyncCallbackT] + def __init__( self, *, @@ -242,27 +243,27 @@ def http_settings(self) -> http_settings_.HTTPSettings: return self._config def listen( - self, event_type: typing.Union[undefined.UndefinedType, typing.Type[_EventT]] = undefined.UNDEFINED, - ) -> typing.Callable[[_CallbackT], _CallbackT]: + self, event_type: typing.Union[undefined.UndefinedType, typing.Type[EventT]] = undefined.UNDEFINED, + ) -> typing.Callable[[CallbackT], CallbackT]: return self.event_dispatcher.listen(event_type) def subscribe( self, - event_type: typing.Type[_EventT], - callback: typing.Callable[[_EventT], typing.Union[typing.Coroutine[None, typing.Any, None], None]], - ) -> typing.Callable[[_EventT], typing.Coroutine[None, typing.Any, None]]: + event_type: typing.Type[EventT], + callback: typing.Callable[[EventT], typing.Union[typing.Coroutine[None, typing.Any, None], None]], + ) -> typing.Callable[[EventT], typing.Coroutine[None, typing.Any, None]]: return self.event_dispatcher.subscribe(event_type, callback) def unsubscribe( self, - event_type: typing.Type[_EventT], - callback: typing.Callable[[_EventT], typing.Coroutine[None, typing.Any, None]], + event_type: typing.Type[EventT], + callback: typing.Callable[[EventT], typing.Coroutine[None, typing.Any, None]], ) -> None: return self.event_dispatcher.unsubscribe(event_type, callback) async def wait_for( - self, event_type: typing.Type[_EventT], predicate: _PredicateT, timeout: typing.Union[float, int, None], - ) -> _EventT: + self, event_type: typing.Type[EventT], predicate: PredicateT, timeout: typing.Union[float, int, None], + ) -> EventT: return await self.event_dispatcher.wait_for(event_type, predicate, timeout) def dispatch(self, event: base_events.Event) -> asyncio.Future[typing.Any]: diff --git a/hikari/impl/cache.py b/hikari/impl/cache.py index 43f85f2dbd..09165c87b1 100644 --- a/hikari/impl/cache.py +++ b/hikari/impl/cache.py @@ -36,5 +36,6 @@ def __init__(self, app: rest.IRESTApp) -> None: self._app = app @property + @typing.final def app(self) -> rest.IRESTApp: return self._app diff --git a/hikari/impl/entity_factory.py b/hikari/impl/entity_factory.py index 3b9f72b302..b7a6e800ff 100644 --- a/hikari/impl/entity_factory.py +++ b/hikari/impl/entity_factory.py @@ -150,6 +150,7 @@ def __init__(self, app: rest.IRESTApp) -> None: } @property + @typing.final def app(self) -> rest.IRESTApp: return self._app diff --git a/hikari/impl/event_manager_core.py b/hikari/impl/event_manager_core.py index a018f69f64..4231c06773 100644 --- a/hikari/impl/event_manager_core.py +++ b/hikari/impl/event_manager_core.py @@ -38,14 +38,16 @@ if typing.TYPE_CHECKING: from hikari.api import rest - _EventT = typing.TypeVar("_EventT", bound=base.Event, contravariant=True) - _PredicateT = typing.Callable[[_EventT], typing.Union[bool, typing.Coroutine[None, typing.Any, bool]]] - _SyncCallbackT = typing.Callable[[_EventT], None] - _AsyncCallbackT = typing.Callable[[_EventT], typing.Coroutine[None, typing.Any, None]] - _CallbackT = typing.Union[_SyncCallbackT, _AsyncCallbackT] - _ListenerMapT = typing.MutableMapping[typing.Type[_EventT], typing.MutableSequence[_AsyncCallbackT]] - _WaiterT = typing.Tuple[_PredicateT, asyncio.Future[_EventT]] - _WaiterMapT = typing.MutableMapping[typing.Type[_EventT], typing.MutableSet[_WaiterT]] + +if typing.TYPE_CHECKING: + EventT = typing.TypeVar("EventT", bound=base.Event, contravariant=True) + PredicateT = typing.Callable[[EventT], typing.Union[bool, typing.Coroutine[None, typing.Any, bool]]] + SyncCallbackT = typing.Callable[[EventT], None] + AsyncCallbackT = typing.Callable[[EventT], typing.Coroutine[None, typing.Any, None]] + CallbackT = typing.Union[SyncCallbackT, AsyncCallbackT] + ListenerMapT = typing.MutableMapping[typing.Type[EventT], typing.MutableSequence[AsyncCallbackT]] + WaiterT = typing.Tuple[PredicateT, asyncio.Future[EventT]] + WaiterMapT = typing.MutableMapping[typing.Type[EventT], typing.MutableSet[WaiterT]] class EventManagerCoreComponent(event_dispatcher.IEventDispatcherComponent, event_consumer.IEventConsumerComponent): @@ -57,11 +59,12 @@ class EventManagerCoreComponent(event_dispatcher.IEventDispatcherComponent, even def __init__(self, app: rest.IRESTApp) -> None: self._app = app - self._listeners: _ListenerMapT = {} - self._waiters: _WaiterMapT = {} + self._listeners: ListenerMapT = {} + self._waiters: WaiterMapT = {} self.logger = reflect.get_logger(self) @property + @typing.final def app(self) -> rest.IRESTApp: return self._app @@ -76,16 +79,16 @@ async def consume_raw_event( def subscribe( self, - event_type: typing.Type[_EventT], - callback: typing.Callable[[_EventT], typing.Union[typing.Coroutine[None, typing.Any, None], None]], - ) -> typing.Callable[[_EventT], typing.Coroutine[None, typing.Any, None]]: + event_type: typing.Type[EventT], + callback: typing.Callable[[EventT], typing.Union[typing.Coroutine[None, typing.Any, None], None]], + ) -> typing.Callable[[EventT], typing.Coroutine[None, typing.Any, None]]: if event_type not in self._listeners: self._listeners[event_type] = [] if not asyncio.iscoroutinefunction(callback): @functools.wraps(callback) - async def wrapper(event: _EventT) -> None: + async def wrapper(event: EventT) -> None: callback(event) self.subscribe(event_type, wrapper) @@ -100,15 +103,15 @@ async def wrapper(event: _EventT) -> None: event_type.__qualname__, ) - callback = typing.cast("typing.Callable[[_EventT], typing.Coroutine[None, typing.Any, None]]", callback) + callback = typing.cast("typing.Callable[[EventT], typing.Coroutine[None, typing.Any, None]]", callback) self._listeners[event_type].append(callback) return callback def unsubscribe( self, - event_type: typing.Type[_EventT], - callback: typing.Callable[[_EventT], typing.Coroutine[None, typing.Any, None]], + event_type: typing.Type[EventT], + callback: typing.Callable[[EventT], typing.Coroutine[None, typing.Any, None]], ) -> None: if event_type in self._listeners: self.logger.debug( @@ -123,9 +126,9 @@ def unsubscribe( del self._listeners[event_type] def listen( - self, event_type: typing.Union[undefined.UndefinedType, typing.Type[_EventT]] = undefined.UNDEFINED, - ) -> typing.Callable[[_CallbackT], _CallbackT]: - def decorator(callback: _CallbackT) -> _CallbackT: + self, event_type: typing.Union[undefined.UndefinedType, typing.Type[EventT]] = undefined.UNDEFINED, + ) -> typing.Callable[[CallbackT], CallbackT]: + def decorator(callback: CallbackT) -> CallbackT: nonlocal event_type signature = reflect.resolve_signature(callback) @@ -151,10 +154,10 @@ def decorator(callback: _CallbackT) -> _CallbackT: return decorator async def wait_for( - self, event_type: typing.Type[_EventT], predicate: _PredicateT, timeout: typing.Union[float, int, None] - ) -> _EventT: + self, event_type: typing.Type[EventT], predicate: PredicateT, timeout: typing.Union[float, int, None] + ) -> EventT: - future: asyncio.Future[_EventT] = asyncio.get_event_loop().create_future() + future: asyncio.Future[EventT] = asyncio.get_event_loop().create_future() if event_type not in self._waiters: self._waiters[event_type] = set() @@ -164,7 +167,7 @@ async def wait_for( return await asyncio.wait_for(future, timeout=timeout) if timeout is not None else await future async def _test_waiter( - self, cls: typing.Type[_EventT], event: _EventT, predicate: _PredicateT, future: asyncio.Future[_EventT] + self, cls: typing.Type[EventT], event: EventT, predicate: PredicateT, future: asyncio.Future[EventT] ) -> None: try: result = predicate(event) @@ -202,11 +205,12 @@ def dispatch(self, event: base.Event) -> asyncio.Future[typing.Any]: if cls in self._waiters: for predicate, future in self._waiters[cls]: + # noinspection PyTypeChecker tasks.append(self._test_waiter(cls, event, predicate, future)) return asyncio.gather(*tasks) if tasks else aio.completed_future() - async def _invoke_callback(self, callback: _AsyncCallbackT, event: _EventT) -> None: + async def _invoke_callback(self, callback: AsyncCallbackT, event: EventT) -> None: try: result = callback(event) if asyncio.iscoroutine(result): diff --git a/hikari/impl/gateway_zookeeper.py b/hikari/impl/gateway_zookeeper.py index 7937c1b067..7febd3352c 100644 --- a/hikari/impl/gateway_zookeeper.py +++ b/hikari/impl/gateway_zookeeper.py @@ -129,7 +129,7 @@ class AbstractGatewayZookeeper(gateway_zookeeper.IGatewayZookeeperApp, abc.ABC): # We do not bother with SIGINT here, since we can catch it as a KeyboardInterrupt # instead and provide tidier handling of the stacktrace as a result. - _SIGNALS: typing.Final[typing.ClassVar[typing.Sequence[str]]] = ["SIGQUIT", "SIGTERM"] + _SIGNALS: typing.Final[typing.ClassVar[typing.Sequence[str]]] = ("SIGQUIT", "SIGTERM") def __init__( self, diff --git a/hikari/models/applications.py b/hikari/models/applications.py index 587c0bb864..05c81f4d3f 100644 --- a/hikari/models/applications.py +++ b/hikari/models/applications.py @@ -47,6 +47,7 @@ @enum.unique +@typing.final class OAuth2Scope(str, enum.Enum): """OAuth2 Scopes that Discord allows. @@ -175,6 +176,7 @@ class OAuth2Scope(str, enum.Enum): @enum.unique +@typing.final class ConnectionVisibility(int, enum.Enum): """Describes who can see a connection with a third party account.""" diff --git a/hikari/models/audit_logs.py b/hikari/models/audit_logs.py index 142fb6a479..6290216338 100644 --- a/hikari/models/audit_logs.py +++ b/hikari/models/audit_logs.py @@ -53,6 +53,7 @@ from hikari.utilities import snowflake +@typing.final class AuditLogChangeKey(str, enum.Enum): """Commonly known and documented keys for audit log change objects. @@ -135,6 +136,7 @@ class AuditLogChange: @enum.unique +@typing.final class AuditLogEventType(enum.IntEnum): """The type of event that occurred.""" diff --git a/hikari/models/bases.py b/hikari/models/bases.py index babc395f94..759df9b7ea 100644 --- a/hikari/models/bases.py +++ b/hikari/models/bases.py @@ -68,6 +68,7 @@ def created_at(self) -> datetime.datetime: """When the object was created.""" return self.id.created_at + @typing.final def __int__(self) -> int: return int(self.id) diff --git a/hikari/models/channels.py b/hikari/models/channels.py index 9d49ce2efb..3aa447b478 100644 --- a/hikari/models/channels.py +++ b/hikari/models/channels.py @@ -53,6 +53,7 @@ @enum.unique +@typing.final class ChannelType(int, enum.Enum): """The known channel types that are exposed to us by the API.""" @@ -79,6 +80,7 @@ class ChannelType(int, enum.Enum): @enum.unique +@typing.final class PermissionOverwriteType(str, enum.Enum): """The type of entity a Permission Overwrite targets.""" diff --git a/hikari/models/embeds.py b/hikari/models/embeds.py index fa4ca3d1f5..f99761c87d 100644 --- a/hikari/models/embeds.py +++ b/hikari/models/embeds.py @@ -30,7 +30,6 @@ "EmbedField", ] -import contextlib import datetime import typing @@ -40,6 +39,10 @@ from hikari.utilities import files +if typing.TYPE_CHECKING: + import concurrent.futures + + def _maybe_color(value: typing.Optional[colors.ColorLike]) -> typing.Optional[colors.Color]: return colors.Color.of(value) if value is not None else None @@ -74,6 +77,7 @@ class EmbedResource(files.Resource): """ @property + @typing.final def url(self) -> str: return self.resource.url @@ -81,11 +85,23 @@ def url(self) -> str: def filename(self) -> str: return self.resource.filename - @contextlib.asynccontextmanager - @typing.no_type_check - async def stream(self) -> files.AsyncReader: - async with self.resource.stream() as stream: - yield stream + def stream( + self, *, executor: typing.Optional[concurrent.futures.Executor] = None, head_only: bool = False, + ) -> files.AsyncReaderContextManager[files.ReaderImplT]: + """Produce a stream of data for the resource. + + Parameters + ---------- + executor : concurrent.futures.Executor or None + The executor to run in for blocking operations. + If `None`, then the default executor is used for the current + event loop. + head_only : bool + Defaults to `False`. If `True`, then the implementation may + only retrieve HEAD information if supported. This currently + only has any effect for web requests. + """ + return self.resource.stream(executor=executor, head_only=head_only) @attr.s(eq=True, hash=False, kw_only=True, slots=True) diff --git a/hikari/models/emojis.py b/hikari/models/emojis.py index c45cc1126b..7da7c888ea 100644 --- a/hikari/models/emojis.py +++ b/hikari/models/emojis.py @@ -35,11 +35,12 @@ from hikari.models import users from hikari.utilities import snowflake + _TWEMOJI_PNG_BASE_URL: typing.Final[str] = "https://github.com/twitter/twemoji/raw/master/assets/72x72/" -"""The URL for Twemoji PNG artwork for built-in emojis.""" +"""URL for Twemoji PNG artwork for built-in emojis.""" _TWEMOJI_SVG_BASE_URL: typing.Final[str] = "https://github.com/twitter/twemoji/raw/master/assets/svg/" -"""The URL for Twemoji SVG artwork for built-in emojis.""" +"""URL for Twemoji SVG artwork for built-in emojis.""" @attr.s(eq=True, hash=True, init=False, kw_only=True, slots=True) @@ -96,6 +97,7 @@ class UnicodeEmoji(Emoji): """The code points that form the emoji.""" @property + @typing.final def url_name(self) -> str: return self.name @@ -104,6 +106,7 @@ def mention(self) -> str: return self.name @property + @typing.final def codepoints(self) -> typing.Sequence[int]: """Integer codepoints that make up this emoji, as UTF-8.""" return [ord(c) for c in self.name] @@ -145,6 +148,7 @@ def url(self) -> str: return _TWEMOJI_PNG_BASE_URL + self.filename @property + @typing.final def unicode_names(self) -> typing.Sequence[str]: """Get the unicode name of the emoji as a sequence. @@ -154,11 +158,13 @@ def unicode_names(self) -> typing.Sequence[str]: return [unicodedata.name(c) for c in self.name] @property + @typing.final def unicode_escape(self) -> str: """Get the unicode escape string for this emoji.""" return bytes(self.name, "unicode_escape").decode("utf-8") @classmethod + @typing.final def from_codepoints(cls, codepoint: int, *codepoints: int) -> UnicodeEmoji: """Create a unicode emoji from one or more UTF-32 codepoints.""" unicode_emoji = cls() @@ -166,6 +172,7 @@ def from_codepoints(cls, codepoint: int, *codepoints: int) -> UnicodeEmoji: return unicode_emoji @classmethod + @typing.final def from_emoji(cls, emoji: str) -> UnicodeEmoji: """Create a unicode emoji from a raw emoji.""" unicode_emoji = cls() @@ -173,6 +180,7 @@ def from_emoji(cls, emoji: str) -> UnicodeEmoji: return unicode_emoji @classmethod + @typing.final def from_unicode_escape(cls, escape: str) -> UnicodeEmoji: """Create a unicode emoji from a unicode escape string.""" unicode_emoji = cls() @@ -220,10 +228,12 @@ def filename(self) -> str: return str(self.id) + (".gif" if self.is_animated else ".png") @property + @typing.final def url_name(self) -> str: return f"{self.name}:{self.id}" @property + @typing.final def mention(self) -> str: return f"<{'a' if self.is_animated else ''}:{self.url_name}>" @@ -232,6 +242,7 @@ def is_mentionable(self) -> bool: return self.is_animated is not None @property + @typing.final def url(self) -> str: return cdn.generate_cdn_url("emojis", str(self.id), format_="gif" if self.is_animated else "png", size=None) diff --git a/hikari/models/guilds.py b/hikari/models/guilds.py index 349f032d82..7fd0ca0cd8 100644 --- a/hikari/models/guilds.py +++ b/hikari/models/guilds.py @@ -65,6 +65,7 @@ @enum.unique +@typing.final class GuildExplicitContentFilterLevel(int, enum.Enum): """Represents the explicit content filter setting for a guild.""" @@ -79,6 +80,7 @@ class GuildExplicitContentFilterLevel(int, enum.Enum): @enum.unique +@typing.final class GuildFeature(str, enum.Enum): """Features that a guild can provide.""" @@ -135,6 +137,7 @@ class GuildFeature(str, enum.Enum): @enum.unique +@typing.final class GuildMessageNotificationsLevel(int, enum.Enum): """Represents the default notification level for new messages in a guild.""" @@ -146,6 +149,7 @@ class GuildMessageNotificationsLevel(int, enum.Enum): @enum.unique +@typing.final class GuildMFALevel(int, enum.Enum): """Represents the multi-factor authorization requirement for a guild.""" @@ -157,6 +161,7 @@ class GuildMFALevel(int, enum.Enum): @enum.unique +@typing.final class GuildPremiumTier(int, enum.Enum): """Tier for Discord Nitro boosting in a guild.""" @@ -174,6 +179,7 @@ class GuildPremiumTier(int, enum.Enum): @enum.unique +@typing.final class GuildSystemChannelFlag(enum.IntFlag): """Defines which features are suppressed in the system channel.""" @@ -185,6 +191,7 @@ class GuildSystemChannelFlag(enum.IntFlag): @enum.unique +@typing.final class GuildVerificationLevel(int, enum.Enum): """Represents the level of verification of a guild.""" @@ -305,6 +312,7 @@ class Role(PartialRole): @enum.unique +@typing.final class IntegrationExpireBehaviour(int, enum.Enum): """Behavior for expiring integration subscribers.""" @@ -387,6 +395,7 @@ class GuildMemberBan: @attr.s(eq=True, hash=True, init=False, kw_only=True, slots=True) +@typing.final class UnavailableGuild(bases.Entity, bases.Unique): """An unavailable guild object, received during gateway events such as READY. diff --git a/hikari/models/intents.py b/hikari/models/intents.py index 3b231dbe42..ef92df63e1 100644 --- a/hikari/models/intents.py +++ b/hikari/models/intents.py @@ -28,6 +28,7 @@ @enum.unique +@typing.final class Intent(enum.IntFlag): """Represents an intent on the gateway. diff --git a/hikari/models/invites.py b/hikari/models/invites.py index 05d0f444a1..3c825599ea 100644 --- a/hikari/models/invites.py +++ b/hikari/models/invites.py @@ -40,6 +40,7 @@ @enum.unique +@typing.final class TargetUserType(int, enum.Enum): """The reason a invite targets a user.""" diff --git a/hikari/models/messages.py b/hikari/models/messages.py index 128302ea6b..42fd8a4971 100644 --- a/hikari/models/messages.py +++ b/hikari/models/messages.py @@ -52,6 +52,7 @@ @enum.unique +@typing.final class MessageType(int, enum.Enum): """The type of a message.""" @@ -96,6 +97,7 @@ class MessageType(int, enum.Enum): @enum.unique +@typing.final class MessageFlag(enum.IntFlag): """Additional flags for message options.""" @@ -119,6 +121,7 @@ class MessageFlag(enum.IntFlag): @enum.unique +@typing.final class MessageActivityType(int, enum.Enum): """The type of a rich presence message activity.""" diff --git a/hikari/models/permissions.py b/hikari/models/permissions.py index 4a86c1105c..fc8cb4a87e 100644 --- a/hikari/models/permissions.py +++ b/hikari/models/permissions.py @@ -28,6 +28,7 @@ @enum.unique +@typing.final class Permission(enum.IntFlag): """Represents the permissions available in a given channel or guild. diff --git a/hikari/models/presences.py b/hikari/models/presences.py index c09829985d..7e17e2dfda 100644 --- a/hikari/models/presences.py +++ b/hikari/models/presences.py @@ -49,6 +49,7 @@ @enum.unique +@typing.final class ActivityType(int, enum.Enum): """The activity type.""" @@ -133,6 +134,7 @@ class ActivitySecret: @enum.unique +@typing.final class ActivityFlag(enum.IntFlag): """Flags that describe what an activity includes. @@ -228,6 +230,7 @@ class RichActivity(Activity): """Flags that describe what the activity includes, if present.""" +@typing.final class Status(str, enum.Enum): """The status of a member.""" diff --git a/hikari/models/users.py b/hikari/models/users.py index 9649aa61ad..1f68944094 100644 --- a/hikari/models/users.py +++ b/hikari/models/users.py @@ -33,6 +33,7 @@ @enum.unique +@typing.final class UserFlag(enum.IntFlag): """The known user flags that represent account badges.""" @@ -80,6 +81,7 @@ class UserFlag(enum.IntFlag): @enum.unique +@typing.final class PremiumType(int, enum.Enum): """The types of Nitro.""" diff --git a/hikari/models/webhooks.py b/hikari/models/webhooks.py index f97f453302..a259adee84 100644 --- a/hikari/models/webhooks.py +++ b/hikari/models/webhooks.py @@ -41,6 +41,7 @@ @enum.unique +@typing.final class WebhookType(int, enum.Enum): """Types of webhook.""" diff --git a/hikari/net/gateway.py b/hikari/net/gateway.py index 68fc9a8f6d..7efa4c4db7 100644 --- a/hikari/net/gateway.py +++ b/hikari/net/gateway.py @@ -100,6 +100,7 @@ class Gateway(http_client.HTTPClient, component.IComponent): """ @enum.unique + @typing.final class _GatewayCloseCode(enum.IntEnum): RFC_6455_NORMAL_CLOSURE = 1000 RFC_6455_GOING_AWAY = 1001 @@ -131,6 +132,7 @@ class _GatewayCloseCode(enum.IntEnum): DISALLOWED_INTENT = 4014 @enum.unique + @typing.final class _GatewayOpcode(enum.IntEnum): DISPATCH = 0 HEARTBEAT = 1 @@ -144,9 +146,11 @@ class _GatewayOpcode(enum.IntEnum): HELLO = 10 HEARTBEAT_ACK = 11 + @typing.final class _Reconnect(RuntimeError): __slots__ = () + @typing.final class _SocketClosed(RuntimeError): __slots__ = () @@ -216,6 +220,7 @@ def __init__( self.url = urllib.parse.urlunparse((scheme, netloc, path, params, new_query, "")) @property + @typing.final def app(self) -> event_consumer.IEventConsumerApp: return self._app diff --git a/hikari/net/http_client.py b/hikari/net/http_client.py index 5e3054b394..55843788f5 100644 --- a/hikari/net/http_client.py +++ b/hikari/net/http_client.py @@ -77,11 +77,6 @@ class HTTPClient(abc.ABC): # pylint:disable=too-many-instance-attributes "_tracers", ) - _APPLICATION_JSON: typing.Final[str] = "application/json" - _APPLICATION_X_WWW_FORM_URLENCODED: typing.Final[str] = "application/x-www-form-urlencoded" - _APPLICATION_OCTET_STREAM: typing.Final[str] = "application/octet-stream" - _MULTIPART_FORM_DATA: typing.Final[str] = "multipart/form-data" - logger: logging.Logger """The logger to use for this object.""" @@ -114,9 +109,11 @@ def __init__( self._debug = debug self._tracers = [(tracing.DebugTracer(self.logger) if debug else tracing.CFRayTracer(self.logger))] + @typing.final async def __aenter__(self) -> HTTPClient: return self + @typing.final async def __aexit__( self, exc_type: typing.Type[BaseException], exc_val: BaseException, exc_tb: types.TracebackType ) -> None: @@ -129,6 +126,7 @@ async def close(self) -> None: self.logger.debug("closed client session object %r", self._client_session) self._client_session = None + @typing.final def get_client_session(self) -> aiohttp.ClientSession: """Acquire a client session to make requests with. @@ -158,6 +156,7 @@ def get_client_session(self) -> aiohttp.ClientSession: self.logger.debug("acquired new client session object %r", self._client_session) return self._client_session + @typing.final def _perform_request( self, *, @@ -219,6 +218,7 @@ def _perform_request( **kwargs, ) + @typing.final async def _create_ws( self, url: str, *, compress: int = 0, auto_ping: bool = True, max_msg_size: int = 0 ) -> aiohttp.ClientWebSocketResponse: diff --git a/hikari/net/http_settings.py b/hikari/net/http_settings.py index ed74300186..3c6d6a4769 100644 --- a/hikari/net/http_settings.py +++ b/hikari/net/http_settings.py @@ -31,6 +31,7 @@ @attr.s(kw_only=True, repr=False, auto_attribs=True) +@typing.final class HTTPSettings: """Config for application that use AIOHTTP.""" diff --git a/hikari/net/rate_limits.py b/hikari/net/rate_limits.py index 47faea3de3..2996cbae23 100644 --- a/hikari/net/rate_limits.py +++ b/hikari/net/rate_limits.py @@ -151,6 +151,7 @@ def is_empty(self) -> bool: return len(self.queue) == 0 +@typing.final class ManualRateLimiter(BurstRateLimiter): """Rate limit handler for the global REST rate limit. @@ -420,6 +421,7 @@ async def throttle(self) -> None: self.throttle_task = None +@typing.final class ExponentialBackOff: r"""Implementation of an asyncio-compatible exponential back-off algorithm with random jitter. diff --git a/hikari/net/rest.py b/hikari/net/rest.py index 26d0031242..59e2ac5eed 100644 --- a/hikari/net/rest.py +++ b/hikari/net/rest.py @@ -105,6 +105,16 @@ class REST(http_client.HTTPClient, component.IComponent): # pylint:disable=too- __slots__ = ("buckets", "global_rate_limit", "version", "_app", "_rest_url", "_token") + buckets: buckets.RESTBucketManager + """Bucket ratelimiter manager.""" + + global_rate_limit: rate_limits.ManualRateLimiter + """Global ratelimiter.""" + + version: int + """API version in-use.""" + + @typing.final class _RetryRequest(RuntimeError): __slots__ = () @@ -142,9 +152,11 @@ def __init__( self._rest_url = rest_url.format(self) @property + @typing.final def app(self) -> rest.IRESTApp: return self._app + @typing.final async def _request( self, compiled_route: routes.CompiledRoute, @@ -190,6 +202,7 @@ async def _request( except self._RetryRequest: pass + @typing.final async def _request_once( self, compiled_route: routes.CompiledRoute, @@ -218,7 +231,7 @@ async def _request_once( # Handle the response. if 200 <= response.status < 300: - if response.content_type == self._APPLICATION_JSON: + if response.content_type == strings.APPLICATION_JSON: # Only deserializing here stops Cloudflare shenanigans messing us around. return data_binding.load_json(await response.read()) @@ -228,9 +241,11 @@ async def _request_once( return await self._handle_error_response(response) @staticmethod + @typing.final async def _handle_error_response(response: aiohttp.ClientResponse) -> typing.NoReturn: raise await http_client.generate_error_response(response) + @typing.final async def _parse_ratelimits(self, compiled_route: routes.CompiledRoute, response: aiohttp.ClientResponse) -> None: # Worth noting there is some bug on V6 that rate limits me immediately if I have an invalid token. # https://github.com/discord/discord-api-docs/issues/1569 @@ -259,7 +274,7 @@ async def _parse_ratelimits(self, compiled_route: routes.CompiledRoute, response if not is_rate_limited: return - if response.content_type != self._APPLICATION_JSON: + if response.content_type != strings.APPLICATION_JSON: # We don't know exactly what this could imply. It is likely Cloudflare interfering # but I'd rather we just give up than do something resulting in multiple failed # requests repeatedly. @@ -316,6 +331,7 @@ async def _parse_ratelimits(self, compiled_route: routes.CompiledRoute, response raise errors.RateLimited(str(response.real_url), compiled_route, response.headers, body, body_retry_after) @staticmethod + @typing.final def _generate_allowed_mentions( mentions_everyone: typing.Union[undefined.UndefinedType, bool], user_mentions: typing.Union[ @@ -352,6 +368,7 @@ def _generate_allowed_mentions( return allowed_mentions + @typing.final async def close(self) -> None: """Close the REST client and any open HTTP connections.""" await super().close() @@ -1091,7 +1108,7 @@ async def create_message( if attachments: form = data_binding.URLEncodedForm() - form.add_field("payload_json", data_binding.dump_json(body), content_type=self._APPLICATION_JSON) + form.add_field("payload_json", data_binding.dump_json(body), content_type=strings.APPLICATION_JSON) stack = contextlib.AsyncExitStack() @@ -1099,7 +1116,7 @@ async def create_message( for i, attachment in enumerate(final_attachments): stream = await stack.enter_async_context(attachment.stream(executor=self._app.executor)) form.add_field( - f"file{i}", stream, filename=stream.filename, content_type=self._APPLICATION_OCTET_STREAM + f"file{i}", stream, filename=stream.filename, content_type=strings.APPLICATION_OCTET_STREAM ) raw_response = await self._request(route, body=form) @@ -1508,7 +1525,7 @@ async def execute_webhook( if final_attachments: form = data_binding.URLEncodedForm() - form.add_field("payload_json", data_binding.dump_json(body), content_type=self._APPLICATION_JSON) + form.add_field("payload_json", data_binding.dump_json(body), content_type=strings.APPLICATION_JSON) stack = contextlib.AsyncExitStack() @@ -1516,7 +1533,7 @@ async def execute_webhook( for i, attachment in enumerate(final_attachments): stream = await stack.enter_async_context(attachment.stream(executor=self._app.executor)) form.add_field( - f"file{i}", stream, filename=stream.filename, content_type=self._APPLICATION_OCTET_STREAM + f"file{i}", stream, filename=stream.filename, content_type=strings.APPLICATION_OCTET_STREAM ) raw_response = await self._request(route, body=form, no_auth=no_auth) diff --git a/hikari/net/rest_utils.py b/hikari/net/rest_utils.py index 39fb283bb7..8f3c9e2c2d 100644 --- a/hikari/net/rest_utils.py +++ b/hikari/net/rest_utils.py @@ -48,6 +48,7 @@ from hikari.utilities import files +@typing.final class TypingIndicator: """Result type of `hiarki.net.rest.trigger_typing`. diff --git a/hikari/net/routes.py b/hikari/net/routes.py index 1a97dec742..0eba58dde0 100644 --- a/hikari/net/routes.py +++ b/hikari/net/routes.py @@ -24,45 +24,34 @@ import re import typing +import attr + from hikari.utilities import data_binding DEFAULT_MAJOR_PARAMS: typing.Final[typing.Set[str]] = {"channel", "guild", "webhook"} HASH_SEPARATOR: typing.Final[str] = ";" +# This could be frozen, except attrs' docs advise against this for performance +# reasons when using slotted classes. +@attr.s(slots=True) +@typing.final class CompiledRoute: - """A compiled representation of a route ready to be made into a full e and to be used for a request. + """A compiled representation of a route to a specific resource. - Parameters - ---------- - route : Route - The route used to make this compiled route. - path : str - The path with any major parameters interpolated in. - major_params_hash : str - The part of the hash identifier to use for the compiled set of major parameters. + This is a similar representation to what `Route` provides, except + `Route` is treated as a template, this is treated as an instance. """ - __slots__ = ("route", "major_param_hash", "compiled_path", "hash_code") - - route: typing.Final[Route] + route: Route = attr.ib() """The route this compiled route was created from.""" - major_param_hash: typing.Final[str] + major_param_hash: str = attr.ib() """The major parameters in a bucket hash-compatible representation.""" - compiled_path: typing.Final[str] + compiled_path: str = attr.ib() """The compiled route path to use.""" - hash_code: typing.Final[int] - """The hash code.""" - - def __init__(self, route: Route, path: str, major_params_hash: str) -> None: - self.route = route - self.major_param_hash = major_params_hash - self.compiled_path = path - self.hash_code = hash((self.method, self.route.path_template, major_params_hash)) - @property def method(self) -> str: """Return the HTTP method of this compiled route.""" @@ -103,33 +92,12 @@ def create_real_bucket_hash(self, initial_bucket_hash: str) -> str: """ return initial_bucket_hash + HASH_SEPARATOR + self.major_param_hash - def __hash__(self) -> int: - return self.hash_code - - def __eq__(self, other: typing.Any) -> bool: - return ( - isinstance(other, CompiledRoute) - and self.route == other.route - and self.major_param_hash == other.major_param_hash - and self.compiled_path == other.compiled_path - and self.hash_code == other.hash_code - ) - - def __repr__(self) -> str: - this_type = type(self).__name__ - major_params = ", ".join( - ( - f"method={self.method!r}", - f"compiled_path={self.compiled_path!r}", - f"major_params_hash={self.major_param_hash!r}", - ) - ) - return f"{this_type}({major_params})" - def __str__(self) -> str: return f"{self.method} {self.compiled_path}" +@attr.s(init=False, slots=True) +@typing.final class Route: """A template used to create compiled routes for specific parameters. @@ -144,22 +112,17 @@ class Route: The template string for the path to use. """ - # noinspection RegExpRedundantEscape - _MAJOR_PARAM_REGEX = re.compile(r"\{(.*?)\}") - - __slots__ = ("method", "path_template", "major_param", "hash_code") - - method: str + method: str = attr.ib(hash=True, eq=True) """The HTTP method.""" - path_template: typing.Final[str] + path_template: str = attr.ib(hash=True, eq=True) """The template string used for the path.""" - major_param: typing.Final[typing.Optional[str]] + major_param: typing.Optional[str] = attr.ib(hash=False, eq=False) """The optional major parameter name.""" - hash_code: typing.Final[int] - """The hash code.""" + # noinspection RegExpRedundantEscape + _MAJOR_PARAM_REGEX: typing.Final[typing.ClassVar[typing.Pattern[str]]] = re.compile(r"\{(.*?)\}") # noqa: FS003 def __init__(self, method: str, path_template: str) -> None: self.method = method @@ -172,8 +135,6 @@ def __init__(self, method: str, path_template: str) -> None: major_param = None self.major_param = major_param - self.hash_code = hash((self.method, self.path_template)) - def compile(self, **kwargs: typing.Any) -> CompiledRoute: """Generate a formatted `CompiledRoute` for this route. @@ -197,180 +158,169 @@ def compile(self, **kwargs: typing.Any) -> CompiledRoute: self, self.path_template.format_map(data), data[self.major_param] if self.major_param is not None else "-", ) - def __repr__(self) -> str: - return f"{type(self).__name__}(path_template={self.path_template!r}, major_param={self.major_param!r})" - def __str__(self) -> str: return self.path_template - def __hash__(self) -> int: - return self.hash_code - - def __eq__(self, other: typing.Any) -> bool: - return ( - isinstance(other, Route) - and self.method == other.method - and self.major_param == other.major_param - and self.path_template == other.path_template - and self.hash_code == other.hash_code - ) +GET: typing.Final[str] = "GET" +PATCH: typing.Final[str] = "PATCH" +DELETE: typing.Final[str] = "DELETE" +PUT: typing.Final[str] = "PUT" +POST: typing.Final[str] = "POST" -GET = "GET" -PATCH = "PATCH" -DELETE = "DELETE" -PUT = "PUT" -POST = "POST" +_R = typing.Final[Route] # Channels -GET_CHANNEL = Route(GET, "/channels/{channel}") -PATCH_CHANNEL = Route(PATCH, "/channels/{channel}") -DELETE_CHANNEL = Route(DELETE, "/channels/{channel}") +GET_CHANNEL: typing.Final[Route] = Route(GET, "/channels/{channel}") +PATCH_CHANNEL: typing.Final[Route] = Route(PATCH, "/channels/{channel}") +DELETE_CHANNEL: typing.Final[Route] = Route(DELETE, "/channels/{channel}") -GET_CHANNEL_INVITES = Route(GET, "/channels/{channel}/invites") -POST_CHANNEL_INVITES = Route(POST, "/channels/{channel}/invites") +GET_CHANNEL_INVITES: typing.Final[Route] = Route(GET, "/channels/{channel}/invites") +POST_CHANNEL_INVITES: typing.Final[Route] = Route(POST, "/channels/{channel}/invites") -GET_CHANNEL_MESSAGE = Route(GET, "/channels/{channel}/messages/{message}") -PATCH_CHANNEL_MESSAGE = Route(PATCH, "/channels/{channel}/messages/{message}") -DELETE_CHANNEL_MESSAGE = Route(DELETE, "/channels/{channel}/messages/{message}") +GET_CHANNEL_MESSAGE: typing.Final[Route] = Route(GET, "/channels/{channel}/messages/{message}") +PATCH_CHANNEL_MESSAGE: typing.Final[Route] = Route(PATCH, "/channels/{channel}/messages/{message}") +DELETE_CHANNEL_MESSAGE: typing.Final[Route] = Route(DELETE, "/channels/{channel}/messages/{message}") -GET_CHANNEL_MESSAGES = Route(GET, "/channels/{channel}/messages") -POST_CHANNEL_MESSAGES = Route(POST, "/channels/{channel}/messages") +GET_CHANNEL_MESSAGES: typing.Final[Route] = Route(GET, "/channels/{channel}/messages") +POST_CHANNEL_MESSAGES: typing.Final[Route] = Route(POST, "/channels/{channel}/messages") -POST_DELETE_CHANNEL_MESSAGES_BULK = Route(POST, "/channels/{channel}/messages/bulk-delete") +POST_DELETE_CHANNEL_MESSAGES_BULK: typing.Final[Route] = Route(POST, "/channels/{channel}/messages/bulk-delete") -PATCH_CHANNEL_PERMISSIONS = Route(PATCH, "/channels/{channel}/permissions/{overwrite}") -DELETE_CHANNEL_PERMISSIONS = Route(DELETE, "/channels/{channel}/permissions/{overwrite}") +PATCH_CHANNEL_PERMISSIONS: typing.Final[Route] = Route(PATCH, "/channels/{channel}/permissions/{overwrite}") +DELETE_CHANNEL_PERMISSIONS: typing.Final[Route] = Route(DELETE, "/channels/{channel}/permissions/{overwrite}") -DELETE_CHANNEL_PIN = Route(DELETE, "/channels/{channel}/pins/{message}") +DELETE_CHANNEL_PIN: typing.Final[Route] = Route(DELETE, "/channels/{channel}/pins/{message}") -GET_CHANNEL_PINS = Route(GET, "/channels/{channel}/pins") -PUT_CHANNEL_PINS = Route(PUT, "/channels/{channel}/pins/{message}") +GET_CHANNEL_PINS: typing.Final[Route] = Route(GET, "/channels/{channel}/pins") +PUT_CHANNEL_PINS: typing.Final[Route] = Route(PUT, "/channels/{channel}/pins/{message}") -POST_CHANNEL_TYPING = Route(POST, "/channels/{channel}/typing") +POST_CHANNEL_TYPING: typing.Final[Route] = Route(POST, "/channels/{channel}/typing") -POST_CHANNEL_WEBHOOKS = Route(POST, "/channels/{channel}/webhooks") -GET_CHANNEL_WEBHOOKS = Route(GET, "/channels/{channel}/webhooks") +POST_CHANNEL_WEBHOOKS: typing.Final[Route] = Route(POST, "/channels/{channel}/webhooks") +GET_CHANNEL_WEBHOOKS: typing.Final[Route] = Route(GET, "/channels/{channel}/webhooks") # Reactions -DELETE_ALL_REACTIONS = Route(DELETE, "/channels/{channel}/messages/{message}/reactions") +DELETE_ALL_REACTIONS: typing.Final[Route] = Route(DELETE, "/channels/{channel}/messages/{message}/reactions") -DELETE_REACTION_EMOJI = Route(DELETE, "/channels/{channel}/messages/{message}/reactions/{emoji}") -DELETE_REACTION_USER = Route(DELETE, "/channels/{channel}/messages/{message}/reactions/{emoji}/{used}") -GET_REACTIONS = Route(GET, "/channels/{channel}/messages/{message}/reactions/{emoji}") +DELETE_REACTION_EMOJI: typing.Final[Route] = Route(DELETE, "/channels/{channel}/messages/{message}/reactions/{emoji}") +DELETE_REACTION_USER: typing.Final[Route] = Route( + DELETE, "/channels/{channel}/messages/{message}/reactions/{emoji}/{used}" +) +GET_REACTIONS: typing.Final[Route] = Route(GET, "/channels/{channel}/messages/{message}/reactions/{emoji}") # Guilds -GET_GUILD = Route(GET, "/guilds/{guild}") -PATCH_GUILD = Route(PATCH, "/guilds/{guild}") -DELETE_GUILD = Route(DELETE, "/guilds/{guild}") +GET_GUILD: typing.Final[Route] = Route(GET, "/guilds/{guild}") +PATCH_GUILD: typing.Final[Route] = Route(PATCH, "/guilds/{guild}") +DELETE_GUILD: typing.Final[Route] = Route(DELETE, "/guilds/{guild}") -POST_GUILDS = Route(POST, "/guilds") +POST_GUILDS: typing.Final[Route] = Route(POST, "/guilds") -GET_GUILD_AUDIT_LOGS = Route(GET, "/guilds/{guild}/audit-logs") +GET_GUILD_AUDIT_LOGS: typing.Final[Route] = Route(GET, "/guilds/{guild}/audit-logs") -GET_GUILD_BAN = Route(GET, "/guilds/{guild}/bans/{user}") -PUT_GUILD_BAN = Route(PUT, "/guilds/{guild}/bans/{user}") -DELETE_GUILD_BAN = Route(DELETE, "/guilds/{guild}/bans/{user}") +GET_GUILD_BAN: typing.Final[Route] = Route(GET, "/guilds/{guild}/bans/{user}") +PUT_GUILD_BAN: typing.Final[Route] = Route(PUT, "/guilds/{guild}/bans/{user}") +DELETE_GUILD_BAN: typing.Final[Route] = Route(DELETE, "/guilds/{guild}/bans/{user}") -GET_GUILD_BANS = Route(GET, "/guilds/{guild}/bans") +GET_GUILD_BANS: typing.Final[Route] = Route(GET, "/guilds/{guild}/bans") -GET_GUILD_CHANNELS = Route(GET, "/guilds/{guild}/channels") -POST_GUILD_CHANNELS = Route(POST, "/guilds/{guild}/channels") -PATCH_GUILD_CHANNELS = Route(PATCH, "/guilds/{guild}/channels") +GET_GUILD_CHANNELS: typing.Final[Route] = Route(GET, "/guilds/{guild}/channels") +POST_GUILD_CHANNELS: typing.Final[Route] = Route(POST, "/guilds/{guild}/channels") +PATCH_GUILD_CHANNELS: typing.Final[Route] = Route(PATCH, "/guilds/{guild}/channels") -GET_GUILD_WIDGET = Route(GET, "/guilds/{guild}/widget") -PATCH_GUILD_WIDGET = Route(PATCH, "/guilds/{guild}/widget") +GET_GUILD_WIDGET: typing.Final[Route] = Route(GET, "/guilds/{guild}/widget") +PATCH_GUILD_WIDGET: typing.Final[Route] = Route(PATCH, "/guilds/{guild}/widget") -GET_GUILD_EMOJI = Route(GET, "/guilds/{guild}/emojis/{emoji}") -PATCH_GUILD_EMOJI = Route(PATCH, "/guilds/{guild}/emojis/{emoji}") -DELETE_GUILD_EMOJI = Route(DELETE, "/guilds/{guild}/emojis/{emoji}") +GET_GUILD_EMOJI: typing.Final[Route] = Route(GET, "/guilds/{guild}/emojis/{emoji}") +PATCH_GUILD_EMOJI: typing.Final[Route] = Route(PATCH, "/guilds/{guild}/emojis/{emoji}") +DELETE_GUILD_EMOJI: typing.Final[Route] = Route(DELETE, "/guilds/{guild}/emojis/{emoji}") -GET_GUILD_EMOJIS = Route(GET, "/guilds/{guild}/emojis") -POST_GUILD_EMOJIS = Route(POST, "/guilds/{guild}/emojis") +GET_GUILD_EMOJIS: typing.Final[Route] = Route(GET, "/guilds/{guild}/emojis") +POST_GUILD_EMOJIS: typing.Final[Route] = Route(POST, "/guilds/{guild}/emojis") -PATCH_GUILD_INTEGRATION = Route(PATCH, "/guilds/{guild}/integrations/{integration}") -DELETE_GUILD_INTEGRATION = Route(DELETE, "/guilds/{guild}/integrations/{integration}") +PATCH_GUILD_INTEGRATION: typing.Final[Route] = Route(PATCH, "/guilds/{guild}/integrations/{integration}") +DELETE_GUILD_INTEGRATION: typing.Final[Route] = Route(DELETE, "/guilds/{guild}/integrations/{integration}") -GET_GUILD_INTEGRATIONS = Route(GET, "/guilds/{guild}/integrations") +GET_GUILD_INTEGRATIONS: typing.Final[Route] = Route(GET, "/guilds/{guild}/integrations") -POST_GUILD_INTEGRATION_SYNC = Route(POST, "/guilds/{guild}/integrations/{integration}") +POST_GUILD_INTEGRATION_SYNC: typing.Final[Route] = Route(POST, "/guilds/{guild}/integrations/{integration}") -GET_GUILD_INVITES = Route(GET, "/guilds/{guild}/invites") +GET_GUILD_INVITES: typing.Final[Route] = Route(GET, "/guilds/{guild}/invites") -GET_GUILD_MEMBERS = Route(GET, "/guilds/{guild}/members") +GET_GUILD_MEMBERS: typing.Final[Route] = Route(GET, "/guilds/{guild}/members") -GET_GUILD_MEMBER = Route(GET, "/guilds/{guild}/members/{user}") -PATCH_GUILD_MEMBER = Route(PATCH, "/guilds/{guild}/members/{user}") -PUT_GUILD_MEMBER = Route(PUT, "/guilds/{guild}/members/{user}") -DELETE_GUILD_MEMBER = Route(DELETE, "/guilds/{guild}/members/{user}") +GET_GUILD_MEMBER: typing.Final[Route] = Route(GET, "/guilds/{guild}/members/{user}") +PATCH_GUILD_MEMBER: typing.Final[Route] = Route(PATCH, "/guilds/{guild}/members/{user}") +PUT_GUILD_MEMBER: typing.Final[Route] = Route(PUT, "/guilds/{guild}/members/{user}") +DELETE_GUILD_MEMBER: typing.Final[Route] = Route(DELETE, "/guilds/{guild}/members/{user}") -PUT_GUILD_MEMBER_ROLE = Route(PUT, "/guilds/{guild}/members/{user}/roles/{role}") -DELETE_GUILD_MEMBER_ROLE = Route(DELETE, "/guilds/{guild}/members/{user}/roles/{role}") +PUT_GUILD_MEMBER_ROLE: typing.Final[Route] = Route(PUT, "/guilds/{guild}/members/{user}/roles/{role}") +DELETE_GUILD_MEMBER_ROLE: typing.Final[Route] = Route(DELETE, "/guilds/{guild}/members/{user}/roles/{role}") -GET_GUILD_PREVIEW = Route(GET, "/guilds/{guild}/preview") +GET_GUILD_PREVIEW: typing.Final[Route] = Route(GET, "/guilds/{guild}/preview") -GET_GUILD_PRUNE = Route(GET, "/guilds/{guild}/prune") -POST_GUILD_PRUNE = Route(POST, "/guilds/{guild}/prune") +GET_GUILD_PRUNE: typing.Final[Route] = Route(GET, "/guilds/{guild}/prune") +POST_GUILD_PRUNE: typing.Final[Route] = Route(POST, "/guilds/{guild}/prune") -PATCH_GUILD_ROLE = Route(PATCH, "/guilds/{guild}/roles/{role}") -DELETE_GUILD_ROLE = Route(DELETE, "/guilds/{guild}/roles/{role}") +PATCH_GUILD_ROLE: typing.Final[Route] = Route(PATCH, "/guilds/{guild}/roles/{role}") +DELETE_GUILD_ROLE: typing.Final[Route] = Route(DELETE, "/guilds/{guild}/roles/{role}") -GET_GUILD_ROLES = Route(GET, "/guilds/{guild}/roles") -POST_GUILD_ROLES = Route(POST, "/guilds/{guild}/roles") -PATCH_GUILD_ROLES = Route(PATCH, "/guilds/{guild}/roles") +GET_GUILD_ROLES: typing.Final[Route] = Route(GET, "/guilds/{guild}/roles") +POST_GUILD_ROLES: typing.Final[Route] = Route(POST, "/guilds/{guild}/roles") +PATCH_GUILD_ROLES: typing.Final[Route] = Route(PATCH, "/guilds/{guild}/roles") -GET_GUILD_VANITY_URL = Route(GET, "/guilds/{guild}/vanity-url") +GET_GUILD_VANITY_URL: typing.Final[Route] = Route(GET, "/guilds/{guild}/vanity-url") -GET_GUILD_VOICE_REGIONS = Route(GET, "/guilds/{guild}/regions") +GET_GUILD_VOICE_REGIONS: typing.Final[Route] = Route(GET, "/guilds/{guild}/regions") -GET_GUILD_WEBHOOKS = Route(GET, "/guilds/{guild}/webhooks") +GET_GUILD_WEBHOOKS: typing.Final[Route] = Route(GET, "/guilds/{guild}/webhooks") -GET_GUILD_BANNER_IMAGE = Route(GET, "/guilds/{guild}/widget.png") +GET_GUILD_BANNER_IMAGE: typing.Final[Route] = Route(GET, "/guilds/{guild}/widget.png") # Invites -GET_INVITE = Route(GET, "/invites/{invite_code}") -DELETE_INVITE = Route(DELETE, "/invites/{invite_code}") +GET_INVITE: typing.Final[Route] = Route(GET, "/invites/{invite_code}") +DELETE_INVITE: typing.Final[Route] = Route(DELETE, "/invites/{invite_code}") # Users -GET_USER = Route(GET, "/users/{user}") +GET_USER: typing.Final[Route] = Route(GET, "/users/{user}") # @me -DELETE_MY_GUILD = Route(DELETE, "/users/@me/guilds/{guild}") +DELETE_MY_GUILD: typing.Final[Route] = Route(DELETE, "/users/@me/guilds/{guild}") -GET_MY_CONNECTIONS = Route(GET, "/users/@me/connections") # OAuth2 only +GET_MY_CONNECTIONS: typing.Final[Route] = Route(GET, "/users/@me/connections") # OAuth2 only -POST_MY_CHANNELS = Route(POST, "/users/@me/channels") +POST_MY_CHANNELS: typing.Final[Route] = Route(POST, "/users/@me/channels") -GET_MY_GUILDS = Route(GET, "/users/@me/guilds") +GET_MY_GUILDS: typing.Final[Route] = Route(GET, "/users/@me/guilds") -PATCH_MY_GUILD_NICKNAME = Route(PATCH, "/guilds/{guild}/members/@me/nick") +PATCH_MY_GUILD_NICKNAME: typing.Final[Route] = Route(PATCH, "/guilds/{guild}/members/@me/nick") -GET_MY_USER = Route(GET, "/users/@me") -PATCH_MY_USER = Route(PATCH, "/users/@me") +GET_MY_USER: typing.Final[Route] = Route(GET, "/users/@me") +PATCH_MY_USER: typing.Final[Route] = Route(PATCH, "/users/@me") -PUT_MY_REACTION = Route(PUT, "/channels/{channel}/messages/{message}/reactions/{emoji}/@me") -DELETE_MY_REACTION = Route(DELETE, "/channels/{channel}/messages/{message}/reactions/{emoji}/@me") +PUT_MY_REACTION: typing.Final[Route] = Route(PUT, "/channels/{channel}/messages/{message}/reactions/{emoji}/@me") +DELETE_MY_REACTION: typing.Final[Route] = Route(DELETE, "/channels/{channel}/messages/{message}/reactions/{emoji}/@me") # Voice -GET_VOICE_REGIONS = Route(GET, "/voice/regions") +GET_VOICE_REGIONS: typing.Final[Route] = Route(GET, "/voice/regions") # Webhooks -GET_WEBHOOK = Route(GET, "/webhooks/{webhook}") -PATCH_WEBHOOK = Route(PATCH, "/webhooks/{webhook}") -POST_WEBHOOK = Route(POST, "/webhooks/{webhook}") -DELETE_WEBHOOK = Route(DELETE, "/webhooks/{webhook}") +GET_WEBHOOK: typing.Final[Route] = Route(GET, "/webhooks/{webhook}") +PATCH_WEBHOOK: typing.Final[Route] = Route(PATCH, "/webhooks/{webhook}") +POST_WEBHOOK: typing.Final[Route] = Route(POST, "/webhooks/{webhook}") +DELETE_WEBHOOK: typing.Final[Route] = Route(DELETE, "/webhooks/{webhook}") -GET_WEBHOOK_WITH_TOKEN = Route(GET, "/webhooks/{webhook}/{token}") -PATCH_WEBHOOK_WITH_TOKEN = Route(PATCH, "/webhooks/{webhook}/{token}") -DELETE_WEBHOOK_WITH_TOKEN = Route(DELETE, "/webhooks/{webhook}/{token}") -POST_WEBHOOK_WITH_TOKEN = Route(POST, "/webhooks/{webhook}/{token}") +GET_WEBHOOK_WITH_TOKEN: typing.Final[Route] = Route(GET, "/webhooks/{webhook}/{token}") +PATCH_WEBHOOK_WITH_TOKEN: typing.Final[Route] = Route(PATCH, "/webhooks/{webhook}/{token}") +DELETE_WEBHOOK_WITH_TOKEN: typing.Final[Route] = Route(DELETE, "/webhooks/{webhook}/{token}") +POST_WEBHOOK_WITH_TOKEN: typing.Final[Route] = Route(POST, "/webhooks/{webhook}/{token}") -POST_WEBHOOK_WITH_TOKEN_GITHUB = Route(POST, "/webhooks/{webhook}/{token}/github") -POST_WEBHOOK_WITH_TOKEN_SLACK = Route(POST, "/webhooks/{webhook}/{token}/slack") +POST_WEBHOOK_WITH_TOKEN_GITHUB: typing.Final[Route] = Route(POST, "/webhooks/{webhook}/{token}/github") +POST_WEBHOOK_WITH_TOKEN_SLACK: typing.Final[Route] = Route(POST, "/webhooks/{webhook}/{token}/slack") # OAuth2 API -GET_MY_APPLICATION = Route(GET, "/oauth2/applications/@me") +GET_MY_APPLICATION: typing.Final[Route] = Route(GET, "/oauth2/applications/@me") # Gateway -GET_GATEWAY = Route(GET, "/gateway") -GET_GATEWAY_BOT = Route(GET, "/gateway/bot") +GET_GATEWAY: typing.Final[Route] = Route(GET, "/gateway") +GET_GATEWAY_BOT: typing.Final[Route] = Route(GET, "/gateway/bot") diff --git a/hikari/net/strings.py b/hikari/net/strings.py index 260f094031..ea0fa4ff32 100644 --- a/hikari/net/strings.py +++ b/hikari/net/strings.py @@ -65,10 +65,10 @@ PYTHON_PLATFORM_VERSION: typing.Final[str] = ( f"{platform.python_implementation()} {platform.python_version()} " f"{platform.python_branch()} {platform.python_compiler()}" -).replace(" ", " ") +).replace(" " * 2, " ") # URLs -REST_API_URL: typing.Final[str] = "https://discord.com/api/v{0.version}" +REST_API_URL: typing.Final[str] = "https://discord.com/api/v{0.version}" # noqa: FS003 fstring missing prefix OAUTH2_API_URL: typing.Final[str] = f"{REST_API_URL}/oauth2" CDN_URL: typing.Final[str] = "https://cdn.discordapp.com" diff --git a/hikari/net/tracing.py b/hikari/net/tracing.py index 4056b5af21..d9c4bb12c7 100644 --- a/hikari/net/tracing.py +++ b/hikari/net/tracing.py @@ -35,6 +35,8 @@ class BaseTracer: """Base type for tracing HTTP requests.""" + __slots__ = ("logger",) + def __init__(self, logger: logging.Logger) -> None: self.logger = logger @@ -57,6 +59,8 @@ class CFRayTracer(BaseTracer): Cloudflare rays in the response. """ + __slots__ = () + @typing.no_type_check async def on_request_start(self, _, ctx, params): """Log an outbound request.""" @@ -125,6 +129,8 @@ class DebugTracer(BaseTracer): to send logs to anyone. """ + __slots__ = () + @staticmethod async def _format_body(body: typing.Any) -> str: if isinstance(body, aiohttp.FormData): diff --git a/hikari/utilities/data_binding.py b/hikari/utilities/data_binding.py index 2259609e3b..2d79311c71 100644 --- a/hikari/utilities/data_binding.py +++ b/hikari/utilities/data_binding.py @@ -89,6 +89,7 @@ def load_json(_: typing.AnyStr) -> typing.Union[JSONArray, JSONObject]: """Convert a JSON string to a Python type.""" +@typing.final class StringMapBuilder(multidict.MultiDict[str]): """Helper class used to quickly build query strings or header maps. @@ -152,6 +153,7 @@ def put( self.add(key, value) +@typing.final class JSONObjectBuilder(typing.Dict[str, JSONAny]): """Helper class used to quickly build JSON objects from various values. diff --git a/hikari/utilities/date.py b/hikari/utilities/date.py index ad592a2632..d6e640524d 100644 --- a/hikari/utilities/date.py +++ b/hikari/utilities/date.py @@ -34,6 +34,7 @@ import re import typing + TimeSpan = typing.Union[int, float, datetime.timedelta] """Type hint representing a naive time period or time span. @@ -42,7 +43,7 @@ """ DISCORD_EPOCH: typing.Final[int] = 1_420_070_400 -"""The Discord epoch used within snowflake identifiers. +"""Discord epoch used within snowflake identifiers. This is defined as the number of seconds between `1/1/1970 00:00:00 UTC` and `1/1/2015 00:00:00 UTC`. @@ -52,9 +53,12 @@ * [Discord API documentation - Snowflakes](https://discord.com/developers/docs/reference#snowflakes) """ -ISO_8601_DATE_PART: typing.Final[typing.Pattern[str]] = re.compile(r"^(\d{4})-(\d{2})-(\d{2})") -ISO_8601_TIME_PART: typing.Final[typing.Pattern[str]] = re.compile(r"T(\d{2}):(\d{2}):(\d{2})(?:\.(\d{1,6}))?", re.I) -ISO_8601_TZ_PART: typing.Final[typing.Pattern[str]] = re.compile(r"([+-])(\d{2}):(\d{2})$") +# FS003 - f-string missing prefix. +_ISO_8601_DATE: typing.Final[typing.Pattern[str]] = re.compile(r"^(\d{4})-(\d{2})-(\d{2})") # noqa: FS003 +_ISO_8601_TIME: typing.Final[typing.Pattern[str]] = re.compile( + r"T(\d{2}):(\d{2}):(\d{2})(?:\.(\d{1,6}))?", re.I +) # noqa: FS003 +_ISO_8601_TZ: typing.Final[typing.Pattern[str]] = re.compile(r"([+-])(\d{2}):(\d{2})$") # noqa: FS003 def rfc7231_datetime_string_to_datetime(date_str: str, /) -> datetime.datetime: @@ -96,9 +100,9 @@ def iso8601_datetime_string_to_datetime(date_string: str, /) -> datetime.datetim ---------- * [ISO-8601](https://en.wikipedia.org/wiki/ISO_8601) """ - year, month, day = map(int, ISO_8601_DATE_PART.findall(date_string)[0]) + year, month, day = map(int, _ISO_8601_DATE.findall(date_string)[0]) - time_part = ISO_8601_TIME_PART.findall(date_string)[0] + time_part = _ISO_8601_TIME.findall(date_string)[0] hour, minute, second, partial = time_part # Pad the millisecond part if it is not in microseconds, otherwise Python will complain. @@ -107,7 +111,7 @@ def iso8601_datetime_string_to_datetime(date_string: str, /) -> datetime.datetim if date_string.endswith(("Z", "z")): timezone = datetime.timezone.utc else: - sign, tz_hour, tz_minute = ISO_8601_TZ_PART.findall(date_string)[0] + sign, tz_hour, tz_minute = _ISO_8601_TZ.findall(date_string)[0] tz_hour, tz_minute = int(tz_hour), int(tz_minute) offset = datetime.timedelta(hours=tz_hour, minutes=tz_minute) if sign == "-": diff --git a/hikari/utilities/files.py b/hikari/utilities/files.py index 9735c2e01c..6bf9c5b290 100644 --- a/hikari/utilities/files.py +++ b/hikari/utilities/files.py @@ -355,6 +355,7 @@ def _expand(path: pathlib.Path) -> pathlib.Path: return path.expanduser().resolve() @staticmethod + @typing.final def _read_chunk(fp: typing.IO[bytes], n: int = 10_000) -> bytes: return fp.read(n) @@ -386,6 +387,7 @@ async def __aexit__( ... +@typing.final class _NoOpAsyncReaderContextManagerImpl(typing.Generic[ReaderImplT], AsyncReaderContextManager[ReaderImplT]): __slots__ = ("impl",) @@ -404,6 +406,7 @@ async def __aexit__( pass +@typing.final class _WebReaderAsyncReaderContextManagerImpl(AsyncReaderContextManager[WebReader]): __slots__ = ("_web_resource", "_head_only", "_client_response_ctx", "_client_session") @@ -520,7 +523,7 @@ def __eq__(self, other: typing.Any) -> bool: return False def __hash__(self) -> int: - return hash(self.url) + return hash((self.__class__, self.url)) class Bytes(Resource[ByteReader]): @@ -705,6 +708,7 @@ def stream( return _WebReaderAsyncReaderContextManagerImpl(self, head_only) +@typing.final class URL(WebResource): """A URL that represents a web resource. @@ -768,6 +772,7 @@ def __init__(self, path: typing.Union[str, pathlib.Path], filename: typing.Optio self._filename = filename @property + @typing.final def url(self) -> str: return f"attachment://{self.filename}" diff --git a/hikari/utilities/reflect.py b/hikari/utilities/reflect.py index b7e66eed6d..beb9f0d3cb 100644 --- a/hikari/utilities/reflect.py +++ b/hikari/utilities/reflect.py @@ -89,6 +89,3 @@ def get_logger(obj: typing.Union[typing.Type[typing.Any], typing.Any], *addition obj = obj if isinstance(obj, type) else type(obj) return logging.getLogger(".".join((obj.__module__, obj.__qualname__, *additional_args))) - - -T = typing.TypeVar("T") diff --git a/hikari/utilities/snowflake.py b/hikari/utilities/snowflake.py index 010b418f49..b83367fb31 100644 --- a/hikari/utilities/snowflake.py +++ b/hikari/utilities/snowflake.py @@ -29,6 +29,7 @@ from hikari.utilities import date +@typing.final class Snowflake(int): """A concrete representation of a unique identifier for an object on Discord. diff --git a/hikari/utilities/undefined.py b/hikari/utilities/undefined.py index 458a8e18e2..a4ac8b7ffd 100644 --- a/hikari/utilities/undefined.py +++ b/hikari/utilities/undefined.py @@ -45,6 +45,7 @@ def __str__(self) -> str: # whereas using a constant value does not provide that. In short, this allows # MyPy to determine it can statically cast a value to a different type when # we do `is` and `is not` checks on values, which removes the need for casts. +@typing.final class _UndefinedTypeWrapper(_UndefinedType, enum.Enum): UNDEFINED_VALUE = _UndefinedType() From 03e5c5078dd797aae2cda7190268d09727daef49 Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Mon, 15 Jun 2020 16:41:36 +0100 Subject: [PATCH 522/922] Removed slots for tracing classes. --- hikari/net/tracing.py | 4 ---- tests/hikari/net/test_http_client.py | 2 +- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/hikari/net/tracing.py b/hikari/net/tracing.py index d9c4bb12c7..d99a57ca4a 100644 --- a/hikari/net/tracing.py +++ b/hikari/net/tracing.py @@ -59,8 +59,6 @@ class CFRayTracer(BaseTracer): Cloudflare rays in the response. """ - __slots__ = () - @typing.no_type_check async def on_request_start(self, _, ctx, params): """Log an outbound request.""" @@ -129,8 +127,6 @@ class DebugTracer(BaseTracer): to send logs to anyone. """ - __slots__ = () - @staticmethod async def _format_body(body: typing.Any) -> str: if isinstance(body, aiohttp.FormData): diff --git a/tests/hikari/net/test_http_client.py b/tests/hikari/net/test_http_client.py index 9eb6e379ad..4349f9b740 100644 --- a/tests/hikari/net/test_http_client.py +++ b/tests/hikari/net/test_http_client.py @@ -38,7 +38,7 @@ def client_session(): @pytest.fixture def client(client_session): assert client_session, "this param is needed, it ensures aiohttp is patched for the test" - client = http_client.HTTPClient(mock.MagicMock()) + client = _helpers.unslot_class(http_client.HTTPClient)(mock.MagicMock()) yield client From 10d12f8089ae372d292be798ec0af5801cb23fff Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Mon, 15 Jun 2020 16:45:39 +0100 Subject: [PATCH 523/922] Deleted pytest.ini --- pylint.ini | 531 ----------------------------------------------------- 1 file changed, 531 deletions(-) delete mode 100644 pylint.ini diff --git a/pylint.ini b/pylint.ini deleted file mode 100644 index 8a1863ddbf..0000000000 --- a/pylint.ini +++ /dev/null @@ -1,531 +0,0 @@ -[MASTER] - -# A comma-separated list of package or module names from where C extensions may -# be loaded. Extensions are loading into the active Python interpreter and may -# run arbitrary code. -extension-pkg-whitelist= - -# Add files or directories to the blacklist. They should be base names, not -# paths. -ignore=CVS - -# Add files or directories matching the regex patterns to the blacklist. The -# regex matches against base names, not paths. -ignore-patterns=_about.py - -# Python code to execute, usually for sys.path manipulation such as -# pygtk.require(). -#init-hook= - -# Use multiple processes to speed up Pylint. Specifying 0 will auto-detect the -# number of processors available to use. -# Theres a bug where multiple jobs report the same error a billion times, I -# ain't got time for that. -jobs=1 - -# Control the amount of potential inferred values when inferring a single -# object. This can help the performance when dealing with large functions or -# complex, nested conditions. -limit-inference-results=100 - -# List of plugins (as comma separated values of python module names) to load, -# usually to register additional checkers. -load-plugins= - -# Pickle collected data for later comparisons. -persistent=yes - -# Specify a configuration file. -#rcfile= - -# When enabled, pylint would attempt to guess common misconfiguration and emit -# user-friendly hints instead of false-positive error messages. -suggestion-mode=yes - -# Allow loading of arbitrary C extensions. Extensions are imported into the -# active Python interpreter and may run arbitrary code. -unsafe-load-any-extension=no - - -[MESSAGES CONTROL] - -# Only show warnings with the listed confidence levels. Leave empty to show -# all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED. -confidence= - -# Disable the message, report, category or checker with the given id(s). You -# can either give multiple identifiers separated by comma (,) or put this -# option multiple times (only on the command line, not in the configuration -# file where it should appear only once). You can also use "--disable=all" to -# disable everything first and then reenable specific checks. For example, if -# you want to run only the similarities checker, you can use "--disable=all -# --enable=similarities". If you want to run only the classes checker, but have -# no Warning level messages displayed, use "--disable=all --enable=classes -# --disable=W". -disable=bad-continuation, - bad-whitespace, - broad-except, - c-extension-no-member, - import-outside-toplevel, - import-star-module-level, - invalid-name, - no-member, - no-method-argument, # glitched. - no-self-use, - invalid-str-returned, # broken in 2.5.2 for enums - round-builtin, - unused-import, - too-many-ancestors, - too-few-public-methods -# unused-import seems to throw false positives with no rhyme or reason. - - -[REPORTS] - -# Python expression which should return a score less than or equal to 10. You -# have access to the variables 'error', 'warning', 'refactor', and 'convention' -# which contain the number of messages in each category, as well as 'statement' -# which is the total number of statements analyzed. This score is used by the -# global evaluation report (RP0004). -evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10) - -# Template used to display messages. This is a python new-style format string -# used to format the message information. See doc for all details. -#msg-template= - -# Set the output format. Available formats are text, parseable, colorized, json -# and msvs (visual studio). You can also give a reporter class, e.g. -# mypackage.mymodule.MyReporterClass. -output-format=colorized - -# Tells whether to display a full report or only the messages. -reports=yes - -# Activate the evaluation score. -score=yes - - -[REFACTORING] - -# Maximum number of nested blocks for function / method body -max-nested-blocks=6 - -# Complete name of functions that never returns. When checking for -# inconsistent-return-statements if a never returning function is called then -# it will be considered as an explicit return statement and no message will be -# printed. -never-returning-functions=sys.exit, - exit, - quit - - -[SPELLING] - -# Limits count of emitted suggestions for spelling mistakes. -max-spelling-suggestions=4 - -# Spelling dictionary name. Available dictionaries: none. To make it work, -# install the python-enchant package. -spelling-dict= - -# List of comma separated words that should not be checked. -spelling-ignore-words= - -# A path to a file that contains the private dictionary; one word per line. -spelling-private-dict-file= - -# Tells whether to store unknown words to the private dictionary (see the -# --spelling-private-dict-file option) instead of raising a message. -spelling-store-unknown-words=no - - -[SIMILARITIES] - -# Ignore comments when computing similarities. -ignore-comments=yes - -# Ignore docstrings when computing similarities. -ignore-docstrings=yes - -# Ignore imports when computing similarities. -ignore-imports=yes - -# Minimum lines number of a similarity. -min-similarity-lines=10 - - -[TYPECHECK] - -# List of decorators that produce context managers, such as -# contextlib.contextmanager. Add to this list to register other decorators that -# produce valid context managers. -contextmanager-decorators=contextlib.contextmanager, - contextlib.asynccontextmanager - -# List of members which are set dynamically and missed by pylint inference -# system, and so shouldn't trigger E1101 when accessed. Python regular -# expressions are accepted. -generated-members= - -# Tells whether missing members accessed in mixin class should be ignored. A -# mixin class is detected if its name ends with "mixin" (case insensitive). -ignore-mixin-members=yes - -# Tells whether to warn about missing members when the owner of the attribute -# is inferred to be None. -ignore-none=yes - -# This flag controls whether pylint should warn about no-member and similar -# checks whenever an opaque object is returned when inferring. The inference -# can return multiple potential results while evaluating a Python object, but -# some branches might not be evaluated, which results in partial inference. In -# that case, it might be useful to still emit no-member and other checks for -# the rest of the inferred objects. -ignore-on-opaque-inference=yes - -# List of class names for which member attributes should not be checked (useful -# for classes with dynamically set attributes). This supports the use of -# qualified names. -ignored-classes=optparse.Values,thread._local,_thread._local - -# List of module names for which member attributes should not be checked -# (useful for modules/projects where namespaces are manipulated during runtime -# and thus existing member attributes cannot be deduced by static analysis). It -# supports qualified module names, as well as Unix pattern matching. -ignored-modules= - -# Show a hint with possible names when a member name was not found. The aspect -# of finding the hint is based on edit distance. -missing-member-hint=yes - -# The minimum edit distance a name should have in order to be considered a -# similar match for a missing member name. -missing-member-hint-distance=1 - -# The total number of similar names that should be taken in consideration when -# showing a hint for a missing member. -missing-member-max-choices=1 - -# List of decorators that change the signature of a decorated function. -signature-mutators=attr.s, - marshaller.attrs, - functools.wraps, - click.command, - click.option - - -[BASIC] - -# Naming style matching correct argument names. -argument-naming-style=snake_case - -# Regular expression matching correct argument names. Overrides argument- -# naming-style. -#argument-rgx= - -# Naming style matching correct attribute names. -attr-naming-style=snake_case - -# Regular expression matching correct attribute names. Overrides attr-naming- -# style. -#attr-rgx= - -# Bad variable names which should always be refused, separated by a comma. -bad-names=foo, - bar, - baz, - toto, - tutu, - tata - -# Naming style matching correct class attribute names. -class-attribute-naming-style=any - -# Regular expression matching correct class attribute names. Overrides class- -# attribute-naming-style. -#class-attribute-rgx= - -# Naming style matching correct class names. -class-naming-style=PascalCase - -# Regular expression matching correct class names. Overrides class-naming- -# style. -#class-rgx= - -# Naming style matching correct constant names. -const-naming-style=UPPER_CASE - -# Regular expression matching correct constant names. Overrides const-naming- -# style. -#const-rgx= - -# Minimum line length for functions/classes that require docstrings, shorter -# ones are exempt. -docstring-min-length=-1 - -# Naming style matching correct function names. -function-naming-style=snake_case - -# Regular expression matching correct function names. Overrides function- -# naming-style. -#function-rgx= - -# Good variable names which should always be accepted, separated by a comma. -good-names=i, - j, - k, - ex, - T, - U, - V, - _, - __ - -# Include a hint for the correct naming format with invalid-name. -include-naming-hint=no - -# Naming style matching correct inline iteration names. -inlinevar-naming-style=any - -# Regular expression matching correct inline iteration names. Overrides -# inlinevar-naming-style. -#inlinevar-rgx= - -# Naming style matching correct method names. -method-naming-style=snake_case - -# Regular expression matching correct method names. Overrides method-naming- -# style. -#method-rgx= - -# Naming style matching correct module names. -module-naming-style=snake_case - -# Regular expression matching correct module names. Overrides module-naming- -# style. -#module-rgx= - -# Colon-delimited sets of names that determine each other's naming style when -# the name regexes allow several styles. -name-group= - -# Regular expression which should only match function or class names that do -# not require a docstring. -no-docstring-rgx=^_ - -# List of decorators that produce properties, such as abc.abstractproperty. Add -# to this list to register other decorators that produce valid properties. -# These decorators are taken in consideration only for invalid-name. -property-classes= - -# Naming style matching correct variable names. -variable-naming-style=snake_case - -# Regular expression matching correct variable names. Overrides variable- -# naming-style. -#variable-rgx= - - -[STRING] - -# This flag controls whether the implicit-str-concat-in-sequence should -# generate a warning on implicit string concatenation in sequences defined over -# several lines. -check-str-concat-over-line-jumps=no - - -[MISCELLANEOUS] - -# List of note tags to take in consideration, separated by a comma. -notes=FIXME, - XXX, - TODO, - TO-DO, - FIX-ME, - BUG, - ???, - - -[VARIABLES] - -# List of additional names supposed to be defined in builtins. Remember that -# you should avoid defining new builtins when possible. -additional-builtins= - -# Tells whether unused global variables should be treated as a violation. -allow-global-unused-variables=yes - -# List of strings which can identify a callback function by name. A callback -# name must start or end with one of those strings. -callbacks=cb_, - _cb - -# A regular expression matching the name of dummy variables (i.e. expected to -# not be used). -dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_ - -# Argument names that match this expression will be ignored. Default to name -# with leading underscore. -ignored-argument-names=_.*|^ignored_|^unused_ - -# Tells whether we should check for unused import in __init__ files. -init-import=no - -# List of qualified module names which can have objects that can redefine -# builtins. -redefining-builtins-modules=six.moves,past.builtins,future.builtins,builtins,io - - -[FORMAT] - -# Expected format of line ending, e.g. empty (any line ending), LF or CRLF. -expected-line-ending-format=LF - -# Regexp for a line that is allowed to be longer than the limit. -ignore-long-lines=^\s*?(#(:\s+:type:|:?\s+https?://).*?) - -# Number of spaces of indent required inside a hanging or continued line. -indent-after-paren=0 - -# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 -# tab). -indent-string=' ' - -# Maximum number of characters on a single line. -max-line-length=120 - -# Maximum number of lines in a module. -max-module-lines=4000 - -# List of optional constructs for which whitespace checking is disabled. `dict- -# separator` is used to allow tabulation in dicts, etc.: {1 : 1,\n222: 2}. -# `trailing-comma` allows a space between comma and closing bracket: (a, ). -# `empty-line` allows space-only lines. -no-space-check=trailing-comma, - dict-separator - -# Allow the body of a class to be on the same line as the declaration if body -# contains single statement. -single-line-class-stmt=no - -# Allow the body of an if to be on the same line as the test if there is no -# else. -single-line-if-stmt=no - - -[LOGGING] - -# Format style used to check logging format string. `old` means using % -# formatting, `new` is for `{}` formatting,and `fstr` is for f-strings. -logging-format-style=old - -# Logging modules to check that the string format arguments are in logging -# function parameter format. -logging-modules=logging - - -[CLASSES] - -# List of method names used to declare (i.e. assign) instance attributes. -defining-attr-methods=__init__, - __new__, - setUp, - __post_init__ - -# List of member names, which should be excluded from the protected access -# warning. -exclude-protected=_asdict, - _fields, - _replace, - _source, - _make, - _internal, - -# List of valid names for the first argument in a class method. -valid-classmethod-first-arg=cls - -# List of valid names for the first argument in a metaclass class method. -valid-metaclass-classmethod-first-arg=mcs - - -[IMPORTS] - -# List of modules that can be imported at any level, not just the top level -# one. -allow-any-import-level= - -# Allow wildcard imports from modules that define __all__. -allow-wildcard-with-all=yes - -# Analyse import fallback blocks. This can be used to support both Python 2 and -# 3 compatible code, which means that the block might have code that exists -# only in one or another interpreter, leading to false positives when analysed. -analyse-fallback-blocks=no - -# Deprecated modules which should not be used, separated by a comma. -deprecated-modules=optparse, - tkinter.tix, - collections.abc - -# Create a graph of external dependencies in the given file (report RP0402 must -# not be disabled). -ext-import-graph= - -# Create a graph of every (i.e. internal and external) dependencies in the -# given file (report RP0402 must not be disabled). -import-graph= - -# Create a graph of internal dependencies in the given file (report RP0402 must -# not be disabled). -int-import-graph= - -# Force import order to recognize a module as part of the standard -# compatibility libraries. -known-standard-library= - -# Force import order to recognize a module as part of a third party library. -known-third-party= - -# Couples of modules and preferred modules, separated by a comma. -preferred-modules= - - -[DESIGN] - -# Maximum number of arguments for function / method. -max-args=7 # Originally 5 - -# Maximum number of attributes for a class (see R0902). -max-attributes=10 - -# Maximum number of boolean expressions in an if statement (see R0916). -max-bool-expr=3 - -# Maximum number of branch for function / method body. -max-branches=12 - -# Maximum number of locals for function / method body. -max-locals=15 - -# Maximum number of parents for a class (see R0901). -max-parents=7 # `too-many-ancestors` is currently disabled. - -# Maximum number of public methods for a class (see R0904). -max-public-methods=20 - -# Maximum number of return / yield for function / method body. -max-returns=6 - -# Maximum number of statements in function / method body. -max-statements=60 # originally 50 - -# Minimum number of public methods for a class (see R0903). -min-public-methods=2 # `too-few-public-methods` is currently disabled. - - -[EXCEPTIONS] - -# Exceptions that will emit a warning when being caught. Defaults to -# "BaseException, Exception". -overgeneral-exceptions=BaseException, - Exception From 8608b8ad8999be3c190fd82c9f04d1517d903593 Mon Sep 17 00:00:00 2001 From: A5rocks <5684371-A5rocks@users.noreply.gitlab.com> Date: Tue, 16 Jun 2020 01:26:39 +0900 Subject: [PATCH 524/922] Add tests for hikari/events/base These tests are just testing whether the decorators work as stated and verification that inheritance does indeed work on `Event`s. --- tests/hikari/events/test_base.py | 57 ++++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 tests/hikari/events/test_base.py diff --git a/tests/hikari/events/test_base.py b/tests/hikari/events/test_base.py new file mode 100644 index 0000000000..be500f10a7 --- /dev/null +++ b/tests/hikari/events/test_base.py @@ -0,0 +1,57 @@ +import pytest +import attr + +import hikari.events.base as base +from hikari.models import intents + + +@base.requires_intents(intents.Intent.GUILDS) +@attr.s(eq=False, hash=False, init=False, kw_only=True, slots=True) +class FooEvent(base.Event): + pass + + +@base.no_catch() +@base.requires_intents(intents.Intent.GUILD_PRESENCES) +@attr.s(eq=False, hash=False, init=False, kw_only=True, slots=True) +class BarEvent(base.Event): + pass + + +@base.no_catch() +@attr.s(eq=False, hash=False, init=False, kw_only=True, slots=True) +class ErrorEvent(base.Event): + pass + + +@attr.s(eq=False, hash=False, init=False, kw_only=True, slots=True) +class FooInheritedEvent(FooEvent): + pass + + +@attr.s(eq=False, hash=False, init=False, kw_only=True, slots=True) +class BarInheritedEvent(BarEvent): + pass + + +def test_no_catch_marked(): + assert base.is_no_catch_event(BarEvent) + assert base.is_no_catch_event(ErrorEvent) + assert not base.is_no_catch_event(FooEvent) + assert not base.is_no_catch_event(FooInheritedEvent) + + +def test_requires_intents(): + assert list(base.get_required_intents_for(FooEvent)) == [intents.Intent.GUILDS] + assert list(base.get_required_intents_for(BarEvent)) == [intents.Intent.GUILD_PRESENCES] + assert list(base.get_required_intents_for(ErrorEvent)) == [] + + +def test_inherited_requires_intents(): + assert list(base.get_required_intents_for(BarInheritedEvent)) == [intents.Intent.GUILD_PRESENCES] + assert list(base.get_required_intents_for(FooInheritedEvent)) == [intents.Intent.GUILDS] + + +def test_inherited_no_catch(): + assert base.is_no_catch_event(BarInheritedEvent) + assert not base.is_no_catch_event(FooInheritedEvent) From 45aebe16cbd763af5fa10ce91d6f3af20327c52e Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Mon, 15 Jun 2020 17:55:35 +0100 Subject: [PATCH 525/922] Added aiohttp speedup optionality during installation (includes cchardet which is around 4000x faster for encoding operations. --- MANIFEST.in | 1 + README.md | 54 +++++++++++++++++++++++++++++++++++++--- pages/index.html | 44 ++++++++++++++++---------------- setup.py | 12 ++++----- speedup-requirements.txt | 1 + 5 files changed, 79 insertions(+), 33 deletions(-) create mode 100644 speedup-requirements.txt diff --git a/MANIFEST.in b/MANIFEST.in index 18cbd41513..def1a5a24c 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -2,6 +2,7 @@ graft hikari include COPYING.md include COPYING.LESSER.md include requirements.txt +include speedup-requirements.txt include setup.py include README.md include CONTRIBUTING.md diff --git a/README.md b/README.md index 7b1e3ad60f..fffbda8427 100644 --- a/README.md +++ b/README.md @@ -8,8 +8,10 @@ level may be! :-) # _hikari_ -An opinionated Discord API for Python 3 and asyncio. Built on good intentions -and the hope that it will be extendable and reusable, rather than an obstacle. +An opinionated, static typed Discord API for Python3 and asyncio. + +Built on good intentions and the hope that it will be extendable and reusable, +rather than an obstacle for future development. ```py import hikari @@ -18,7 +20,7 @@ from hikari.events.message import MessageCreateEvent bot = hikari.Bot(token="...") -@bot.event(MessageCreateEvent) +@bot.listen(MessageCreateEvent) async def ping(event): # If a non-bot user sends a message "hk.ping", respond with "Pong!" @@ -29,6 +31,46 @@ async def ping(event): bot.run() ``` +---- + +## Installation + +Install hikari from PyPI with the following command: + +```bash +python -m pip install hikari -U --pre +# Windows users may need to run this instead... +py -3 -m pip install hikari -U --pre +``` + +### Moar poweeeerrrr + +If you wish to get the most out of your bot, you should opt-in to +installing the speedups extensions. + +```bash +python -m pip install hikari[speedups] -U --pre +``` + +This may take a little longer to install, but will replace several dependencies +with much faster alternatives, including: + +- [`aiodns`](https://pypi.org/project/aiodns/) - Asynchronous DNS lookups using + `pycares` (`libcares` Python bindings). +- [`cchardet`](https://pypi.org/project/cchardet/) - a compiled C implementation + of the [`chardet`](https://pypi.org/project/chardet/) module. Claims + to handle almost 1468 calls per second in a benchmark, compared to + 0.35 calls per second from the default `chardet` module, which is around + 4193x faster.\* + +\* _`cchardet` v2.1.6 Python 3.6.1, Intel(R) Core(TM) i5-4690 CPU @ 3.50GHz, +16GB 1.6GHz DDR3, Ubuntu 16.04 AMD64._ + +Note that you may find you need to install a C compiler on your machine to make +use of these extensions. + +---- + ## What does _hikari_ aim to do? - **Provide 100% documentation for the entire library.** Build your application @@ -49,7 +91,9 @@ bot.run() fix, and quickly. You do not want to wait for weeks for a usable solution to be released. _hikari_ is developed using a fully automated CI pipeline with extensive quality assurance. This enables bugfixes and new features to be - shipped within 30 minutes, not 30 days. + shipped within 30 minutes of coding them, not 30 days. + +---- ## What does _hikari_ currently support? @@ -110,6 +154,8 @@ to utilize these components as a black box where necessary. where you want them. Let _hikari_ work out how to put it together!) - Full voice transcoding support, natively in your application. Do not rely on invoking ffmpeg in a subprocess ever again! + +---- ## Getting started diff --git a/pages/index.html b/pages/index.html index 495191e1c3..86d25e1e7c 100644 --- a/pages/index.html +++ b/pages/index.html @@ -31,7 +31,7 @@ - Hikari.py + Hikari @@ -46,52 +46,50 @@ # -*- coding: utf-8 -*- import asyncio import functools - import logging import os import hikari + from hikari.events.message import MessageCreateEvent - logging.basicConfig(level="INFO") - - bot = hikari.StatelessBot(token=os.environ["BOT_TOKEN"]) + bot = hikari.Bot(token=os.environ["BOT_TOKEN"]) def command(name): def decorator(fn): @functools.wraps(fn) async def wrapper(event): - if not event.author.is_bot and event.content.startswith(name): + if not event.message.author.is_bot and event.message.content.startswith(name): await fn(event) return wrapper return decorator - @bot.event() + @bot.listen() @command("hk.ping") - async def ping(message: hikari.MessageCreateEvent) -> None: - await event.reply(content="Pong!") + async def ping(message: MessageCreateEvent) -> None: + await event.message.reply("Pong!") @bot.event() @command("hk.add") - async def add_numbers(message: hikari.MessageCreateEvent) -> None: - await event.reply(content="Please enter the first number") + async def add_numbers(event: MessageCreateEvent) -> None: + await event.message.reply("Please enter the first number") - def is_number_check(event): - return event.author == message.author and message.content.isdigit() + def is_number_check(e): + return event.message.author == e.message.author and e.message.content.isdigit() try: - m1 = await bot.wait_for(hikari.MessageCreateEvent, predicate=is_number_check, timeout=30) - await bot.rest.create_message(message.channel_id, content="Please enter the second number") - m2 = await bot.wait_for(hikari.MessageCreateEvent, predicate=is_number_check, timeout=30) + e1 = await bot.wait_for(MessageCreateEvent, predicate=is_number_check, timeout=30) + await bot.rest.create_message(event.message.channel_id, content="Please enter the second number") + e2 = await bot.wait_for(MessageCreateEvent, predicate=is_number_check, timeout=30) except asyncio.TimeoutError: - await event.reply("You took too long...") + await event.message.reply("You took too long...") else: - val1 = int(m1.content) - val2 = int(m2.content) + val1 = int(e1.message.content) + val2 = int(e2.message.content) embed = hikari.Embed(title="Result!", description=f"{val1} + {val2}" color="#3f3") - await event.reply(embed=embed) + await event.message.reply(embed=embed) bot.run() @@ -119,9 +117,9 @@

pip install --pre
- hikari + hikari[speedups]

-

A new, powerful, sane Python API for writing Discord bots.

+

A new, powerful, static-typed Python API for writing Discord bots.

This API is still in a pre-alpha state, and is a work in progress! Features may change or undergo improvements before the design is finalized. Until then, why not join our Discord? Feel free @@ -164,4 +162,4 @@

- \ No newline at end of file + diff --git a/setup.py b/setup.py index f0a10b8c47..db910fa556 100644 --- a/setup.py +++ b/setup.py @@ -45,8 +45,8 @@ def parse_meta(): return types.SimpleNamespace(**groups) -def parse_requirements(): - with open("requirements.txt") as fp: +def parse_requirements_file(path): + with open(path) as fp: dependencies = (d.strip() for d in fp.read().split("\n") if d.strip()) return [d for d in dependencies if not d.startswith("#")] @@ -72,7 +72,7 @@ def parse_requirements(): }, packages=setuptools.find_namespace_packages(include=[name + "*"]), python_requires=">=3.8.0,<3.10", - install_requires=parse_requirements(), + install_requires=parse_requirements_file("requirements.txt"), include_package_data=True, test_suite="tests", zip_safe=False, @@ -85,14 +85,14 @@ def parse_requirements(): "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: Implementation :: CPython", - "Programming Language :: Python :: 3 :: Only", "Topic :: Communications :: Chat", "Topic :: Internet :: WWW/HTTP", - "Topic :: Software Development :: Libraries :: Application Frameworks", "Topic :: Software Development :: Libraries", + "Topic :: Software Development :: Libraries :: Application Frameworks", "Topic :: Software Development :: Libraries :: Python Modules", "Typing :: Typed", ], - entry_points={"console_scripts": ["hikari = hikari.__main__:main", "hikari-test = hikari.clients.test:main"]}, + entry_points={"console_scripts": ["hikari = hikari.__main__:main"]}, + extras_require={"speedups": parse_requirements_file("speedup-requirements.txt"),}, provides="hikari", ) diff --git a/speedup-requirements.txt b/speedup-requirements.txt new file mode 100644 index 0000000000..52ca66a0d3 --- /dev/null +++ b/speedup-requirements.txt @@ -0,0 +1 @@ +aiohttp[speedups]~=3.6.2 From f653a59ecca483a309fd0c1431103b5e805a96e9 Mon Sep 17 00:00:00 2001 From: davfsa Date: Tue, 16 Jun 2020 10:44:16 +0200 Subject: [PATCH 526/922] Changed CDN helper to return files.URL --- hikari/models/applications.py | 28 ++++++++++----------- hikari/models/channels.py | 8 +++--- hikari/models/guilds.py | 19 ++++++--------- hikari/models/invites.py | 16 ++++++------ hikari/models/users.py | 6 ++--- hikari/models/webhooks.py | 11 ++++----- hikari/utilities/cdn.py | 13 +++++----- tests/hikari/net/test_urls.py | 39 ------------------------------ tests/hikari/utilities/test_cdn.py | 5 ++-- 9 files changed, 50 insertions(+), 95 deletions(-) delete mode 100644 tests/hikari/net/test_urls.py diff --git a/hikari/models/applications.py b/hikari/models/applications.py index 05c81f4d3f..070ae06247 100644 --- a/hikari/models/applications.py +++ b/hikari/models/applications.py @@ -323,10 +323,10 @@ def format_icon(self, *, format_: str = "png", size: int = 4096) -> typing.Optio If the size is not an integer power of 2 between 16 and 4096 (inclusive). """ - if self.icon_hash: - url = cdn.generate_cdn_url("team-icons", str(self.id), self.icon_hash, format_=format_, size=size) - return files.URL(url) - return None + if self.icon_hash is None: + return None + + return cdn.generate_cdn_url("team-icons", str(self.id), self.icon_hash, format_=format_, size=size) @attr.s(eq=True, hash=True, init=False, kw_only=True, slots=True) @@ -429,10 +429,10 @@ def format_icon(self, *, format_: str = "png", size: int = 4096) -> typing.Optio If the size is not an integer power of 2 between 16 and 4096 (inclusive). """ - if self.icon_hash is not None: - url = cdn.generate_cdn_url("application-icons", str(self.id), self.icon_hash, format_=format_, size=size) - return files.URL(url) - return None + if self.icon_hash is None: + return None + + return cdn.generate_cdn_url("application-icons", str(self.id), self.icon_hash, format_=format_, size=size) @property def cover_image(self) -> typing.Optional[files.URL]: @@ -468,9 +468,9 @@ def format_cover_image(self, *, format_: str = "png", size: int = 4096) -> typin If the size is not an integer power of 2 between 16 and 4096 (inclusive). """ - if self.cover_image_hash is not None: - url = cdn.generate_cdn_url( - "application-assets", str(self.id), self.cover_image_hash, format_=format_, size=size - ) - return files.URL(url) - return None + if self.cover_image_hash is None: + return None + + return cdn.generate_cdn_url( + "application-assets", str(self.id), self.cover_image_hash, format_=format_, size=size + ) diff --git a/hikari/models/channels.py b/hikari/models/channels.py index 3aa447b478..8f6bd4aafd 100644 --- a/hikari/models/channels.py +++ b/hikari/models/channels.py @@ -226,10 +226,10 @@ def format_icon(self, *, format: str = "png", size: int = 4096) -> typing.Option ValueError If `size` is not a power of two between 16 and 4096 (inclusive). """ - if self.icon_hash: - url = cdn.generate_cdn_url("channel-icons", str(self.id), self.icon_hash, format_=format, size=size) - return files.URL(url) - return None + if self.icon_hash is None: + return None + + return cdn.generate_cdn_url("channel-icons", str(self.id), self.icon_hash, format_=format, size=size) @attr.s(eq=True, hash=True, init=False, kw_only=True, slots=True) diff --git a/hikari/models/guilds.py b/hikari/models/guilds.py index 7fd0ca0cd8..a8facf0921 100644 --- a/hikari/models/guilds.py +++ b/hikari/models/guilds.py @@ -466,8 +466,7 @@ def format_icon(self, *, format_: typing.Optional[str] = None, size: int = 4096) else: format_ = "png" - url = cdn.generate_cdn_url("icons", str(self.id), self.icon_hash, format_=format_, size=size) - return files.URL(url) + return cdn.generate_cdn_url("icons", str(self.id), self.icon_hash, format_=format_, size=size) @attr.s(eq=True, hash=True, init=False, kw_only=True, slots=True) @@ -522,8 +521,7 @@ def format_splash(self, *, format_: str = "png", size: int = 4096) -> typing.Opt if self.splash_hash is None: return None - url = cdn.generate_cdn_url("splashes", str(self.id), self.splash_hash, format_=format_, size=size) - return files.URL(url) + return cdn.generate_cdn_url("splashes", str(self.id), self.splash_hash, format_=format_, size=size) @property def discovery_splash(self) -> typing.Optional[files.URL]: @@ -555,10 +553,9 @@ def format_discovery_splash(self, *, format_: str = "png", size: int = 4096) -> if self.discovery_splash_hash is None: return None - url = cdn.generate_cdn_url( + return cdn.generate_cdn_url( "discovery-splashes", str(self.id), self.discovery_splash_hash, format_=format_, size=size ) - return files.URL(url) @attr.s(eq=True, hash=True, init=False, kw_only=True, slots=True) @@ -885,8 +882,7 @@ def format_splash(self, *, format_: str = "png", size: int = 4096) -> typing.Opt if self.splash_hash is None: return None - url = cdn.generate_cdn_url("splashes", str(self.id), self.splash_hash, format_=format_, size=size) - return files.URL(url) + return cdn.generate_cdn_url("splashes", str(self.id), self.splash_hash, format_=format_, size=size) @property def discovery_splash(self) -> typing.Optional[files.URL]: @@ -917,10 +913,10 @@ def format_discovery_splash(self, *, format_: str = "png", size: int = 4096) -> """ if self.discovery_splash_hash is None: return None - url = cdn.generate_cdn_url( + + return cdn.generate_cdn_url( "discovery-splashes", str(self.id), self.discovery_splash_hash, format_=format_, size=size ) - return files.URL(url) @property def banner(self) -> typing.Optional[files.URL]: @@ -952,5 +948,4 @@ def format_banner(self, *, format_: str = "png", size: int = 4096) -> typing.Opt if self.banner_hash is None: return None - url = cdn.generate_cdn_url("banners", str(self.id), self.banner_hash, format_=format_, size=size) - return files.URL(url) + return cdn.generate_cdn_url("banners", str(self.id), self.banner_hash, format_=format_, size=size) diff --git a/hikari/models/invites.py b/hikari/models/invites.py index 3c825599ea..87f58c23ed 100644 --- a/hikari/models/invites.py +++ b/hikari/models/invites.py @@ -117,10 +117,10 @@ def format_splash(self, *, format_: str = "png", size: int = 4096) -> typing.Opt ValueError If `size` is not a power of two or not between 16 and 4096. """ - if self.splash_hash is not None: - url = cdn.generate_cdn_url("splashes", str(self.id), self.splash_hash, format_=format_, size=size) - return files.URL(url) - return None + if self.splash_hash is None: + return None + + return cdn.generate_cdn_url("splashes", str(self.id), self.splash_hash, format_=format_, size=size) @property def banner(self) -> typing.Optional[files.URL]: @@ -149,10 +149,10 @@ def format_banner(self, *, format_: str = "png", size: int = 4096) -> typing.Opt ValueError If `size` is not a power of two or not between 16 and 4096. """ - if self.banner_hash is not None: - url = cdn.generate_cdn_url("banners", str(self.id), self.banner_hash, format_=format_, size=size) - return files.URL(url) - return None + if self.banner_hash is None: + return None + + return cdn.generate_cdn_url("banners", str(self.id), self.banner_hash, format_=format_, size=size) @attr.s(eq=True, hash=True, init=False, kw_only=True, slots=True) diff --git a/hikari/models/users.py b/hikari/models/users.py index 1f68944094..9749198f4c 100644 --- a/hikari/models/users.py +++ b/hikari/models/users.py @@ -209,14 +209,12 @@ def format_avatar(self, *, format_: typing.Optional[str] = None, size: int = 409 else: format_ = "png" - url = cdn.generate_cdn_url("avatars", str(self.id), self.avatar_hash, format_=format_, size=size) - return files.URL(url) + return cdn.generate_cdn_url("avatars", str(self.id), self.avatar_hash, format_=format_, size=size) @property def default_avatar(self) -> files.URL: # noqa: D401 imperative mood check """Placeholder default avatar for the user.""" - url = cdn.get_default_avatar_url(self.discriminator) - return files.URL(url) + return cdn.get_default_avatar_url(self.discriminator) @property def default_avatar_index(self) -> int: diff --git a/hikari/models/webhooks.py b/hikari/models/webhooks.py index a259adee84..f00f0c483a 100644 --- a/hikari/models/webhooks.py +++ b/hikari/models/webhooks.py @@ -364,8 +364,7 @@ def default_avatar(self) -> files_.URL: This is used if no avatar is set. """ - url = cdn.generate_cdn_url("embed", "avatars", str(self.default_avatar_index), format_="png", size=None) - return files_.URL(url) + return cdn.generate_cdn_url("embed", "avatars", str(self.default_avatar_index), format_="png", size=None) def format_avatar(self, format_: str = "png", size: int = 4096) -> typing.Optional[files_.URL]: """Generate the avatar URL for this webhook's custom avatar if set, else it's default avatar. @@ -392,7 +391,7 @@ def format_avatar(self, format_: str = "png", size: int = 4096) -> typing.Option ValueError If `size` is not a power of two between 16 and 4096 (inclusive). """ - if self.avatar_hash is not None: - url = cdn.generate_cdn_url("avatars", str(self.id), self.avatar_hash, format_=format_, size=size) - return files_.URL(url) - return None + if self.avatar_hash is None: + return None + + return cdn.generate_cdn_url("avatars", str(self.id), self.avatar_hash, format_=format_, size=size) diff --git a/hikari/utilities/cdn.py b/hikari/utilities/cdn.py index 7a15da7c47..7f0325d1aa 100644 --- a/hikari/utilities/cdn.py +++ b/hikari/utilities/cdn.py @@ -25,9 +25,10 @@ import urllib.parse from hikari.net import strings +from hikari.utilities import files -def generate_cdn_url(*route_parts: str, format_: str, size: typing.Optional[int]) -> str: +def generate_cdn_url(*route_parts: str, format_: str, size: typing.Optional[int]) -> files.URL: """Generate a link for a Discord CDN media resource. Parameters @@ -45,7 +46,7 @@ def generate_cdn_url(*route_parts: str, format_: str, size: typing.Optional[int] Returns ------- - str + hikari.utilities.files.URL The URL to the resource on the Discord CDN. Raises @@ -61,7 +62,7 @@ def generate_cdn_url(*route_parts: str, format_: str, size: typing.Optional[int] path = "/".join(urllib.parse.unquote(part) for part in route_parts) url = urllib.parse.urljoin(strings.CDN_URL, "/" + path) + "." + str(format_) query = urllib.parse.urlencode({"size": size}) if size is not None else None - return f"{url}?{query}" if query else url + return files.URL(f"{url}?{query}" if query else url) def get_default_avatar_index(discriminator: str) -> int: @@ -80,7 +81,7 @@ def get_default_avatar_index(discriminator: str) -> int: return int(discriminator) % 5 -def get_default_avatar_url(discriminator: str) -> str: +def get_default_avatar_url(discriminator: str) -> files.URL: """URL for this user's default avatar. Parameters @@ -90,7 +91,7 @@ def get_default_avatar_url(discriminator: str) -> str: Returns ------- - str + hikari.utilities.files.URL The avatar URL. """ - return generate_cdn_url("embed", "avatars", str(get_default_avatar_index(discriminator)), format_="png", size=None,) + return generate_cdn_url("embed", "avatars", str(get_default_avatar_index(discriminator)), format_="png", size=None) diff --git a/tests/hikari/net/test_urls.py b/tests/hikari/net/test_urls.py deleted file mode 100644 index fc3077e858..0000000000 --- a/tests/hikari/net/test_urls.py +++ /dev/null @@ -1,39 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright © Nekoka.tt 2019-2020 -# -# This file is part of Hikari. -# -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . -from hikari.utilities import cdn -from tests.hikari import _helpers - - -def test_generate_cdn_url(): - url = cdn.generate_cdn_url("not", "a", "path", format_="neko", size=16) - assert url == "https://cdn.discordapp.com/not/a/path.neko?size=16" - - -def test_generate_cdn_url_with_size_set_to_none(): - url = cdn.generate_cdn_url("not", "a", "path", format_="neko", size=None) - assert url == "https://cdn.discordapp.com/not/a/path.neko" - - -@_helpers.assert_raises(type_=ValueError) -def test_generate_cdn_url_with_invalid_size_out_of_limits(): - cdn.generate_cdn_url("not", "a", "path", format_="neko", size=11) - - -@_helpers.assert_raises(type_=ValueError) -def test_generate_cdn_url_with_invalid_size_now_power_of_two(): - cdn.generate_cdn_url("not", "a", "path", format_="neko", size=111) diff --git a/tests/hikari/utilities/test_cdn.py b/tests/hikari/utilities/test_cdn.py index 46bc3edf5f..8ce3713c26 100644 --- a/tests/hikari/utilities/test_cdn.py +++ b/tests/hikari/utilities/test_cdn.py @@ -17,17 +17,18 @@ # along with Hikari. If not, see . import pytest +from hikari.utilities import files from hikari.utilities import cdn def test_generate_cdn_url(): url = cdn.generate_cdn_url("not", "a", "path", format_="neko", size=16) - assert url == "https://cdn.discordapp.com/not/a/path.neko?size=16" + assert url == files.URL("https://cdn.discordapp.com/not/a/path.neko?size=16") def test_generate_cdn_url_with_size_set_to_none(): url = cdn.generate_cdn_url("not", "a", "path", format_="neko", size=None) - assert url == "https://cdn.discordapp.com/not/a/path.neko" + assert url == files.URL("https://cdn.discordapp.com/not/a/path.neko") def test_generate_cdn_url_with_invalid_size_out_of_limits(): From 385ecdb14db61ab25bb9c52f92d7cf1fc661f9d0 Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Tue, 16 Jun 2020 09:49:35 +0100 Subject: [PATCH 527/922] Fixed a regression in routes.py, added URLs to HTTP errors. - Regression after switch to autogenerated hashing changed parameter order. Will rewrite UTs for this later when I have time. - Added URL to HTTP errors. --- hikari/errors.py | 2 +- hikari/net/routes.py | 18 +++++++++--------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/hikari/errors.py b/hikari/errors.py index 4b9bd87e83..ca27599f67 100644 --- a/hikari/errors.py +++ b/hikari/errors.py @@ -218,7 +218,7 @@ def __str__(self) -> str: else: name_value = str(self.status) - return f"{name_value}: {raw_body[:200]}{'...' if chomped else ''}" + return f"{name_value}: {raw_body[:200]}{'...' if chomped else ''} for {self.url}" class ClientHTTPErrorResponse(HTTPErrorResponse): diff --git a/hikari/net/routes.py b/hikari/net/routes.py index 0eba58dde0..5ef882655f 100644 --- a/hikari/net/routes.py +++ b/hikari/net/routes.py @@ -34,7 +34,7 @@ # This could be frozen, except attrs' docs advise against this for performance # reasons when using slotted classes. -@attr.s(slots=True) +@attr.s(init=True, slots=True, hash=True) @typing.final class CompiledRoute: """A compiled representation of a route to a specific resource. @@ -43,12 +43,12 @@ class CompiledRoute: `Route` is treated as a template, this is treated as an instance. """ - route: Route = attr.ib() - """The route this compiled route was created from.""" - major_param_hash: str = attr.ib() """The major parameters in a bucket hash-compatible representation.""" + route: Route = attr.ib() + """The route this compiled route was created from.""" + compiled_path: str = attr.ib() """The compiled route path to use.""" @@ -96,7 +96,7 @@ def __str__(self) -> str: return f"{self.method} {self.compiled_path}" -@attr.s(init=False, slots=True) +@attr.s(hash=True, init=False, slots=True) @typing.final class Route: """A template used to create compiled routes for specific parameters. @@ -155,7 +155,9 @@ def compile(self, **kwargs: typing.Any) -> CompiledRoute: data.put(k, v) return CompiledRoute( - self, self.path_template.format_map(data), data[self.major_param] if self.major_param is not None else "-", + route=self, + compiled_path=self.path_template.format_map(data), + major_param_hash=data[self.major_param] if self.major_param is not None else "-", ) def __str__(self) -> str: @@ -168,8 +170,6 @@ def __str__(self) -> str: PUT: typing.Final[str] = "PUT" POST: typing.Final[str] = "POST" -_R = typing.Final[Route] - # Channels GET_CHANNEL: typing.Final[Route] = Route(GET, "/channels/{channel}") PATCH_CHANNEL: typing.Final[Route] = Route(PATCH, "/channels/{channel}") @@ -205,7 +205,7 @@ def __str__(self) -> str: DELETE_REACTION_EMOJI: typing.Final[Route] = Route(DELETE, "/channels/{channel}/messages/{message}/reactions/{emoji}") DELETE_REACTION_USER: typing.Final[Route] = Route( - DELETE, "/channels/{channel}/messages/{message}/reactions/{emoji}/{used}" + DELETE, "/channels/{channel}/messages/{message}/reactions/{emoji}/{user}" ) GET_REACTIONS: typing.Final[Route] = Route(GET, "/channels/{channel}/messages/{message}/reactions/{emoji}") From 80ba06f371e83ce929597935ac17df9bbb696a2f Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Tue, 16 Jun 2020 10:05:18 +0100 Subject: [PATCH 528/922] Fixed formatting bug in logs when using docker-compose to view logs on Linux. --- hikari/impl/bot.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/hikari/impl/bot.py b/hikari/impl/bot.py index 0c61d9e82d..6477063fc0 100644 --- a/hikari/impl/bot.py +++ b/hikari/impl/bot.py @@ -304,11 +304,12 @@ def __print_banner(self) -> None: top_line = "//" + ("=" * line_len) + r"\\" bottom_line = r"\\" + ("=" * line_len) + "//" - # The \r at the start will return to the start of the line for Unix - # consoles; for anything else that is logged, it will just act as - # a newline still. + # Start on a newline, this prevents logging formatting messing with the + # layout of the banner; before we used \r but this probably isn't great + # since with systems like docker-compose that prepend to each line of + # logs, we end up with a mess. self.logger.info( - "\r%s\n%s\n%s\n%s\n%s\n%s\n%s", + "\n%s\n%s\n%s\n%s\n%s\n%s\n%s", top_line, version_str, copyright_str, From d1cc1d459e0b70201cdcabb71c42bb339da3a730 Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Tue, 16 Jun 2020 13:15:17 +0100 Subject: [PATCH 529/922] Added "abstract" to documented functions, shortened reference glimse, fixed wrapping on sidebar for long function names. --- docs/documentation.mako | 122 +++++++++++++++++++++++----------------- 1 file changed, 69 insertions(+), 53 deletions(-) diff --git a/docs/documentation.mako b/docs/documentation.mako index a2b3fb49b9..319f2ec147 100644 --- a/docs/documentation.mako +++ b/docs/documentation.mako @@ -98,18 +98,27 @@ if isinstance(dobj, pdoc.Function): qual = dobj.funcdef() - prefix = "" + qual + "" + if getattr(dobj.obj, "__isabstractmethod__", False): + prefix = f"{QUAL_ABC} " + else: + prefix = "" + + prefix = "" + prefix + qual + "" elif isinstance(dobj, pdoc.Variable): - if hasattr(dobj.cls, "obj") and (descriptor := dobj.cls.obj.__dict__.get(dobj.name)) and isinstance(descriptor, property): - prefix = f"{QUAL_PROPERTY}" + if getattr(dobj.obj, "__isabstractmethod__", False): + prefix = f"{QUAL_ABC} " + else: + prefix = "" + if hasattr(dobj.cls, "obj") and (descriptor := dobj.cls.obj.__dict__.get(dobj.name)) and isinstance(descriptor, property): + prefix = f"{prefix}{QUAL_PROPERTY}" elif dobj.module.name == "typing" or dobj.docstring and dobj.docstring.casefold().startswith(("type hint", "typehint", "type alias")): - prefix = F"{QUAL_TYPEHINT} " + prefix = F"{prefix}{QUAL_TYPEHINT} " elif all(not c.isalpha() or c.isupper() for c in dobj.name): - prefix = f"{QUAL_CONST}" + prefix = f"{prefix}{QUAL_CONST}" else: - prefix = f"{QUAL_VAR}" + prefix = f"{prefix}{QUAL_VAR}" elif isinstance(dobj, pdoc.Class): qual = "" @@ -177,7 +186,7 @@ anchor = "" if not anchor else f'id="{dobj.refname}"' - return '{} {}'.format(prefix, glimpse(dobj.docstring), url, anchor, class_str, name) + return '{} {}'.format(prefix, dobj.name + " -- " + glimpse(dobj.docstring), url, anchor, class_str, name) def simple_name(s): _, _, name = s.rpartition(".") @@ -284,9 +293,12 @@
${representation}

+ % if inspect.isabstract(f.obj): + This function is abstract! + % endif % if redirect: - ${show_desc(f) | glimpse, to_html} - This class is defined explicitly at ${link(ref, with_prefixes=False, fully_qualified=True)}. Visit that link to view the full documentation! + ${show_desc(f, short=True)} + This function is defined explicitly at ${link(ref, with_prefixes=False, fully_qualified=True)}. Visit that link to view the full documentation! % else: ${show_desc(f)} @@ -319,30 +331,33 @@ representation = f"{QUAL_CLASS} {c.name} (" + ", ".join(params) + "): ..." else: representation = f"{QUAL_CLASS} {c.name}: ..." + + if c.module.name != c.obj.__module__: + try: + ref = pdoc._global_context[c.obj.__module__ + "." + c.obj.__qualname__] + redirect = True + except KeyError: + redirect = False + else: + redirect = False %>
-

${link(c, with_prefixes=True, simple_names=True)}

+

reference to ${link(c, with_prefixes=True, simple_names=True)}

-
${representation}
- - <% - if c.module.name != c.obj.__module__: - try: - ref = pdoc._global_context[c.obj.__module__ + "." + c.obj.__qualname__] - redirect = True - except KeyError: - redirect = False - else: - redirect = False - %> - + % if redirect: +
+ + Expand signature + + % endif +
${representation}
% if redirect: - ${show_desc(c) | glimpse, to_html} +
+ ${show_desc(c, short=True)} This class is defined explicitly at ${link(ref, with_prefixes=False, fully_qualified=True)}. Visit that link to view the full documentation! % else: - ${show_desc(c)}
${show_source(c)} @@ -419,21 +434,24 @@ <% inherits = ' inherited' if d.inherits else '' - # docstring = glimpse(d.docstring) if short or inherits else d.docstring docstring = d.docstring %> - % if d.inherits: -

- Inherited from: - % if hasattr(d.inherits, 'cls'): - ${link(d.inherits.cls, with_prefixes=False)}.${link(d.inherits, name=d.name, with_prefixes=False)} - % else: - ${link(d.inherits, with_prefixes=False)} - % endif -

- % endif + % if not short: + % if d.inherits: +

+ Inherited from: + % if hasattr(d.inherits, 'cls'): + ${link(d.inherits.cls, with_prefixes=False)}.${link(d.inherits, name=d.name, with_prefixes=False)} + % else: + ${link(d.inherits, with_prefixes=False)} + % endif +

+ % endif - ${docstring | to_html} + ${docstring | to_html} + % else: + ${docstring | glimpse, to_html} + % endif <%def name="show_source(d)"> @@ -474,11 +492,11 @@ %>
- +
@@ -606,10 +624,8 @@
${QUAL_ABC}
- Abstract base class. These are partially implemented classes that require - additional implementation details to be fully usable. Generally these are - used to represet a subset of behaviour common between different - implementations. + Abstract member. These must be subclassed/overridden with a + concrete implementation elsewhere to be used.
${QUAL_DATACLASS}
From 34f8bc9e9b68d6397abf49a8bc10bc035c6b5098 Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Tue, 16 Jun 2020 13:30:54 +0100 Subject: [PATCH 530/922] Fixed reference heading URLs. --- docs/documentation.mako | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/docs/documentation.mako b/docs/documentation.mako index 319f2ec147..d32971630e 100644 --- a/docs/documentation.mako +++ b/docs/documentation.mako @@ -44,7 +44,7 @@ return located_external_refs[fqn] except KeyError: print("blacklisting", fqn, "as it cannot be dereferenced from external documentation") - unlocatable_external_refs.add(fqn) + unlocatable_external_refs.add(fqn) %> <% @@ -155,21 +155,24 @@ if fully_qualified and not simple_names: name = dobj.module.name + "." + dobj.obj.__qualname__ - url = dobj.url(relative_to=module, link_prefix=link_prefix, top_ancestor=not show_inherited_members) - if isinstance(dobj, pdoc.External): if dobj.module: fqn = dobj.module.name + "." + dobj.obj.__qualname__ else: fqn = dobj.name - url = discover_source(fqn) if url is None: url = discover_source(name) if url is None: return name if not with_prefixes else f"{QUAL_EXTERNAL} {name}" + else: + try: + ref = dobj if not hasattr(dobj.obj, "__module__") else pdoc._global_context[dobj.obj.__module__ + "." + dobj.obj.__qualname__] + url = ref.url(relative_to=module, link_prefix=link_prefix, top_ancestor=not show_inherited_members) + except Exception: + url = dobj.url(relative_to=module, link_prefix=link_prefix, top_ancestor=not show_inherited_members) if simple_names: name = simple_name(name) @@ -342,7 +345,10 @@ redirect = False %>
-

reference to ${link(c, with_prefixes=True, simple_names=True)}

+ <% + prefix = "reference to " if redirect else "" + %> +

${prefix}${link(c, with_prefixes=True, simple_names=True)}

% if redirect: From dec08f77b98f6df324770400d46181936709f326 Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Tue, 16 Jun 2020 14:43:56 +0100 Subject: [PATCH 531/922] Started implementing objects.inv generation for docs. --- docs/documentation.mako | 63 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 62 insertions(+), 1 deletion(-) diff --git a/docs/documentation.mako b/docs/documentation.mako index d32971630e..b4e11721e3 100644 --- a/docs/documentation.mako +++ b/docs/documentation.mako @@ -45,6 +45,22 @@ except KeyError: print("blacklisting", fqn, "as it cannot be dereferenced from external documentation") unlocatable_external_refs.add(fqn) + + project_inventory = sphobjinv.Inventory() + + import atexit + + @atexit.register + def dump_inventory(): + import hikari + + project_inventory.project = "hikari" + project_inventory.version = hikari.__version__ + + text = project_inventory.data_file(contract=True) + ztext = sphobjinv.compress(text) + sphobjinv.writebytes('public/hikari/objects.inv', ztext) + %> <% @@ -256,6 +272,17 @@ parent = v.cls.obj if v.cls is not None else v.module.obj if hasattr(parent, "__annotations__") and v.name in parent.__annotations__: return_type = get_annotation(lambda *_, **__: parent.__annotations__[v.name]) + + project_inventory.objects.append( + sphobjinv.DataObjStr( + name = v.name, + domain = "py", + role = "var", + uri = v.url(), + priority = "1", + dispname = "-", + ) + ) %>
${link(v, with_prefixes=True, anchor=True)}${return_type}
@@ -291,6 +318,18 @@ redirect = False else: redirect = False + + if not redirect: + project_inventory.objects.append( + sphobjinv.DataObjStr( + name = f.name, + domain = "py", + role = "func", + uri = f.url(), + priority = "1", + dispname = "-", + ) + ) %>
${representation}
@@ -343,6 +382,18 @@ redirect = False else: redirect = False + + if not redirect: + project_inventory.objects.append( + sphobjinv.DataObjStr( + name = c.name, + domain = "py", + role = "class", + uri = c.url(), + priority = "1", + dispname = "-", + ) + ) %>
<% @@ -437,7 +488,6 @@ <%def name="show_desc(d, short=False)"> - <% inherits = ' inherited' if d.inherits else '' docstring = d.docstring @@ -495,6 +545,17 @@ functions = module.functions(sort=sort_identifiers) submodules = module.submodules() supermodule = module.supermodule + + project_inventory.objects.append( + sphobjinv.DataObjStr( + name = module.name, + domain = "py", + role = "module", + uri = module.url(), + priority = "1", + dispname = "-", + ) + ) %>
From 4b454ce023e7b726ab6721215abd4f0a30d8c231 Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Tue, 16 Jun 2020 20:40:51 +0100 Subject: [PATCH 532/922] Added tests for hikari.net.tracing.BaseTracer --- coverage.ini | 1 - tests/hikari/net/test_tracing.py | 34 ++++++++++++++++++++++++++++++++ 2 files changed, 34 insertions(+), 1 deletion(-) diff --git a/coverage.ini b/coverage.ini index f2a0177384..554789ab1d 100644 --- a/coverage.ini +++ b/coverage.ini @@ -7,7 +7,6 @@ source = hikari omit = hikari/__main__.py hikari/_about.py - hikari/net/tracing.py .nox/* [report] diff --git a/tests/hikari/net/test_tracing.py b/tests/hikari/net/test_tracing.py index 3b080b938f..a56d120410 100644 --- a/tests/hikari/net/test_tracing.py +++ b/tests/hikari/net/test_tracing.py @@ -15,3 +15,37 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . +import aiohttp +import mock + +from hikari.net import tracing + + +class TestBaseTracer: + def test_sets_logger(self): + logger = mock.MagicMock() + impl = type("Impl", (tracing.BaseTracer,), {})(logger) + assert impl.logger is logger + + def test_trace_config_is_cached(self): + logger = mock.MagicMock() + impl = type("Impl", (tracing.BaseTracer,), {})(logger) + tc = impl.trace_config + assert impl.trace_config is tc + + def test_trace_config_is_instance_of_TraceConfig(self): + logger = mock.MagicMock() + impl = type("Impl", (tracing.BaseTracer,), {})(logger) + assert isinstance(impl.trace_config, aiohttp.TraceConfig) + + def test_trace_config_collects_methods_matching_name_prefix(self): + class Impl(tracing.BaseTracer): + def on_connection_create_end(self): + pass + + def this_should_be_ignored(self): + pass + + i = Impl(mock.MagicMock()) + + assert i.on_connection_create_end in i.trace_config.on_connection_create_end From 60349f8669c19bd583171478c7dbcb8c62f94c78 Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Tue, 16 Jun 2020 21:11:12 +0100 Subject: [PATCH 533/922] Fixed bug in emojis with static typechecking. --- hikari/models/emojis.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/hikari/models/emojis.py b/hikari/models/emojis.py index 7da7c888ea..1a701f137f 100644 --- a/hikari/models/emojis.py +++ b/hikari/models/emojis.py @@ -244,7 +244,8 @@ def is_mentionable(self) -> bool: @property @typing.final def url(self) -> str: - return cdn.generate_cdn_url("emojis", str(self.id), format_="gif" if self.is_animated else "png", size=None) + # TODO, change this as it is a bad line of code and I don't like it. + return cdn.generate_cdn_url("emojis", str(self.id), format_="gif" if self.is_animated else "png", size=None).url @attr.s(eq=True, hash=True, init=False, kw_only=True, slots=True) From 3045d849430ac1da7edf07884e5cef5e689f735c Mon Sep 17 00:00:00 2001 From: Chubonyo Date: Tue, 16 Jun 2020 20:36:35 +0000 Subject: [PATCH 534/922] added a missing doc string in messages.py --- hikari/events/message.py | 1 + 1 file changed, 1 insertion(+) diff --git a/hikari/events/message.py b/hikari/events/message.py index 859d094b5a..d4a2a3acdd 100644 --- a/hikari/events/message.py +++ b/hikari/events/message.py @@ -58,6 +58,7 @@ class MessageCreateEvent(base_events.Event): """Used to represent Message Create gateway events.""" message: messages.Message = attr.ib(repr=True) + """The message that was sent.""" @attr.s(slots=True, init=False, repr=True, eq=False) From e1c0c7438914246decce99aa296993ea3b3e6158 Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Tue, 16 Jun 2020 21:49:42 +0100 Subject: [PATCH 535/922] Re-added Python 3.8.x jobs to CI pipeline. --- ci/gitlab/bases.yml | 18 +++++++++++++++++- ci/gitlab/tests.yml | 21 ++++++++++++++++++--- 2 files changed, 35 insertions(+), 4 deletions(-) diff --git a/ci/gitlab/bases.yml b/ci/gitlab/bases.yml index beebeca080..e26c736c9d 100644 --- a/ci/gitlab/bases.yml +++ b/ci/gitlab/bases.yml @@ -46,8 +46,24 @@ image: busybox:1.31.1-musl ### -### CPython 3.8.2 configuration. +### CPython 3.8 configurations. ### +.cpython:3.8.0: + extends: .any-job + image: python:3.8.0 + +.cpython:3.8.1: + extends: .any-job + image: python:3.8.1 + +.cpython:3.8.2: + extends: .any-job + image: python:3.8.2 + +.cpython:3.8.3: + extends: .any-job + image: python:3.8.3 + .cpython:3.8: extends: .any-job image: python:3.8 diff --git a/ci/gitlab/tests.yml b/ci/gitlab/tests.yml index c7f4e40586..98d8524068 100644 --- a/ci/gitlab/tests.yml +++ b/ci/gitlab/tests.yml @@ -78,13 +78,28 @@ test:win32: stage: test ### -### Run CPython 3.8.2 unit tests and collect test coverage stats. +### Run CPython 3.8 unit tests and collect test coverage stats. ### ### Coverage is exported as an artifact with a name matching the job name. ### -test:3.8: +test:3.8.0: extends: - - .cpython:3.8 + - .cpython:3.8.0 + - .test + +test:3.8.1: + extends: + - .cpython:3.8.1 + - .test + +test:3.8.2: + extends: + - .cpython:3.8.2 + - .test + +test:3.8.3: + extends: + - .cpython:3.8.3 - .test ### From 5c195c3ea3a242aad0405568ae8bbb95394eb61b Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Tue, 16 Jun 2020 22:21:13 +0100 Subject: [PATCH 536/922] Fixed missing class in hikari/events/message.py [skip ci] --- hikari/events/message.py | 1 + 1 file changed, 1 insertion(+) diff --git a/hikari/events/message.py b/hikari/events/message.py index d4a2a3acdd..7ed5d5bb81 100644 --- a/hikari/events/message.py +++ b/hikari/events/message.py @@ -20,6 +20,7 @@ from __future__ import annotations __all__: typing.Final[typing.List[str]] = [ + "BaseMessageReactionEvent", "MessageCreateEvent", "UpdatedMessageFields", "MessageUpdateEvent", From e3b147c530bfed5565d547345c634bebb43d46de Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Wed, 17 Jun 2020 15:01:39 +0100 Subject: [PATCH 537/922] Removes auto-enabling of logging from bots to prevent overriding logger settings elsewhere; Closes #386. --- hikari/impl/bot.py | 97 ---------------------------------------------- 1 file changed, 97 deletions(-) diff --git a/hikari/impl/bot.py b/hikari/impl/bot.py index 6477063fc0..6b8354f580 100644 --- a/hikari/impl/bot.py +++ b/hikari/impl/bot.py @@ -22,11 +22,7 @@ __all__: typing.Final[typing.List[str]] = ["BotAppImpl"] import asyncio -import inspect import logging -import os -import platform -import sys import typing from hikari.api import bot @@ -91,14 +87,6 @@ class BotAppImpl(gateway_zookeeper.AbstractGatewayZookeeper, bot.IBotApp): large_threshold : int The number of members that need to be in a guild for the guild to be considered large. Defaults to the maximum, which is `250`. - logging_level : str or None - If not `None`, then this will be the logging level set if you have not - enabled logging already. In this case, it should be a valid - `logging` level that can be passed to `logging.basicConfig`. If you have - already initialized logging, then this is irrelevant and this - parameter can be safely ignored. If you set this to `None`, then no - logging will initialize if you have a reason to not use any logging - or simply wish to initialize it in your own time instead. rest_version : int The version of the REST API to connect to. At the time of writing, only version `6` and version `7` (undocumented development release) @@ -162,7 +150,6 @@ def __init__( initial_status: typing.Union[undefined.UndefinedType, presences.Status] = undefined.UNDEFINED, intents: typing.Optional[intents_.Intent] = None, large_threshold: int = 250, - logging_level: typing.Optional[str] = "INFO", rest_version: int = 6, rest_url: typing.Union[undefined.UndefinedType, str] = undefined.UNDEFINED, shard_ids: typing.Union[typing.Set[int], undefined.UndefinedType] = undefined.UNDEFINED, @@ -172,11 +159,6 @@ def __init__( ) -> None: self._logger = reflect.get_logger(self) - # If logging is already configured, then this does nothing. - if logging_level is not None: - logging.basicConfig(level=logging_level, format=self.__get_logging_format()) - self.__print_banner() - config = http_settings_.HTTPSettings() if config is undefined.UNDEFINED else config self._cache = cache_impl.InMemoryCacheComponentImpl(app=self) @@ -275,82 +257,3 @@ async def close(self) -> None: async def fetch_sharding_settings(self) -> gateway_models.GatewayBot: return await self.rest.fetch_gateway_bot() - - def __print_banner(self) -> None: - from hikari import _about - - version = _about.__version__ - sourcefile = typing.cast(str, inspect.getsourcefile(_about)) - path = os.path.abspath(os.path.dirname(sourcefile)) - python_implementation = platform.python_implementation() - python_version = platform.python_version() - operating_system = " ".join((platform.system(), *platform.architecture())) - python_compiler = platform.python_compiler() - - copyright_str = f"{_about.__copyright__}, licensed under {_about.__license__}" - version_str = f"hikari v{version} (installed in {path})" - impl_str = f"Running on {python_implementation} v{python_version}, {python_compiler}, ({operating_system})" - doc_line = f"Documentation: {_about.__docs__}" - guild_line = f"Support: {_about.__discord_invite__}" - line_len = max(len(version_str), len(copyright_str), len(impl_str), len(guild_line), len(doc_line)) - - copyright_str = f"|* {copyright_str:^{line_len}} *|" - impl_str = f"|* {impl_str:^{line_len}} *|" - version_str = f"|* {version_str:^{line_len}} *|" - doc_line = f"|* {doc_line:^{line_len}} *|" - guild_line = f"|* {guild_line:^{line_len}} *|" - line_len = max(len(version_str), len(copyright_str), len(impl_str), len(guild_line), len(doc_line)) - 4 - - top_line = "//" + ("=" * line_len) + r"\\" - bottom_line = r"\\" + ("=" * line_len) + "//" - - # Start on a newline, this prevents logging formatting messing with the - # layout of the banner; before we used \r but this probably isn't great - # since with systems like docker-compose that prepend to each line of - # logs, we end up with a mess. - self.logger.info( - "\n%s\n%s\n%s\n%s\n%s\n%s\n%s", - top_line, - version_str, - copyright_str, - impl_str, - doc_line, - guild_line, - bottom_line, - ) - - @staticmethod - def __get_logging_format() -> str: - # Modified from - # https://github.com/django/django/blob/master/django/core/management/color.py - - plat = sys.platform - supports_color = False - - # isatty is not always implemented, https://code.djangoproject.com/ticket/6223 - is_a_tty = hasattr(sys.stdout, "isatty") and sys.stdout.isatty() - - if plat != "Pocket PC": - if plat == "win32": - supports_color |= os.getenv("TERM_PROGRAM", None) == "mintty" - supports_color |= "ANSICON" in os.environ - supports_color |= is_a_tty - else: - supports_color = is_a_tty - - supports_color |= bool(os.getenv("PYCHARM_HOSTED", "")) - - if supports_color: - blue = "\033[1;35m" - gray = "\033[1;37m" - green = "\033[1;32m" - red = "\033[1;31m" - yellow = "\033[1;33m" - default = "\033[0m" - else: - blue = gray = green = red = yellow = default = "" - - return ( - f"{red}%(levelname)4.4s {yellow}%(name)-20.20s {green}#%(lineno)-4d {blue}%(asctime)23.23s " - f"{default}:: {gray}%(message)s{default}" - ) From a1c4925714675ae834fbffe09c0e59602b6f1e2d Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Wed, 17 Jun 2020 15:30:33 +0000 Subject: [PATCH 538/922] Update documentation.mako to fix objects.inv [skip deploy] --- docs/documentation.mako | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/documentation.mako b/docs/documentation.mako index b4e11721e3..d393f092db 100644 --- a/docs/documentation.mako +++ b/docs/documentation.mako @@ -59,7 +59,7 @@ text = project_inventory.data_file(contract=True) ztext = sphobjinv.compress(text) - sphobjinv.writebytes('public/hikari/objects.inv', ztext) + sphobjinv.writebytes('public/objects.inv', ztext) %> From d0584f1d9cb9592343a96bb4d35d4a029089d6c2 Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Wed, 17 Jun 2020 17:06:56 +0000 Subject: [PATCH 539/922] Fixed missing await in rest for fetch_channel. (cherry picked from commit 821800c8bef27124e115cf1ec0f9470aa054ea15) --- hikari/net/rest.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/hikari/net/rest.py b/hikari/net/rest.py index 59e2ac5eed..019e372296 100644 --- a/hikari/net/rest.py +++ b/hikari/net/rest.py @@ -31,7 +31,6 @@ import aiohttp from hikari import errors -from hikari.api import component from hikari.models import embeds as embeds_ from hikari.models import emojis from hikari.net import buckets @@ -402,7 +401,7 @@ async def fetch_channel( If an internal error occurs on Discord while handling the request. """ route = routes.GET_CHANNEL.compile(channel=channel) - raw_response = self._request(route) + raw_response = await self._request(route) response = typing.cast(data_binding.JSONObject, raw_response) return self._app.entity_factory.deserialize_channel(response) From f0dfc4c0cf91e678aa77a2ca63fc936354934173 Mon Sep 17 00:00:00 2001 From: davfsa Date: Wed, 17 Jun 2020 23:29:25 +0200 Subject: [PATCH 540/922] Fix accidental import removal --- hikari/net/rest.py | 1 + 1 file changed, 1 insertion(+) diff --git a/hikari/net/rest.py b/hikari/net/rest.py index 019e372296..892d996d4d 100644 --- a/hikari/net/rest.py +++ b/hikari/net/rest.py @@ -31,6 +31,7 @@ import aiohttp from hikari import errors +from hikari.api import component from hikari.models import embeds as embeds_ from hikari.models import emojis from hikari.net import buckets From 5a55adefd70eeb4fa738cff868c8863f93f42701 Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Thu, 18 Jun 2020 18:19:00 +0100 Subject: [PATCH 541/922] Removed entity and re-enabled slotting on objects. This had been disabled by accident to stop errors caused by multiple inheritance with slots triggering class layout errors on initialization of the classes. The solution has been to remove the Entity entirely. I took this chance to remove Unique and put it with the Snowflake class instead. Each class that has an app or an ID now explicitly declares this. This reduces the memory usage for some models by more than 50% in some cases, so this is a pretty big deal. It also reduces the time complexity for any attribute access. --- hikari/events/channel.py | 24 +- hikari/events/guild.py | 61 +++- hikari/events/message.py | 48 ++- hikari/impl/entity_factory.py | 291 ++++++++++++------- hikari/models/applications.py | 29 +- hikari/models/audit_logs.py | 21 +- hikari/models/bases.py | 77 ----- hikari/models/channels.py | 21 +- hikari/models/embeds.py | 1 - hikari/models/emojis.py | 14 +- hikari/models/guilds.py | 48 ++- hikari/models/invites.py | 14 +- hikari/models/messages.py | 44 ++- hikari/models/presences.py | 9 +- hikari/models/users.py | 19 +- hikari/models/voices.py | 8 +- hikari/models/webhooks.py | 26 +- hikari/net/rest.py | 307 ++++++++++---------- hikari/net/rest_utils.py | 3 +- hikari/utilities/data_binding.py | 8 +- hikari/utilities/snowflake.py | 35 +++ tests/hikari/_helpers.py | 50 ---- tests/hikari/impl/test_entity_factory.py | 99 ++++--- tests/hikari/utilities/test_data_binding.py | 8 +- 24 files changed, 728 insertions(+), 537 deletions(-) delete mode 100644 hikari/models/bases.py diff --git a/hikari/events/channel.py b/hikari/events/channel.py index 1d7435610f..261a78fdad 100644 --- a/hikari/events/channel.py +++ b/hikari/events/channel.py @@ -38,14 +38,14 @@ import attr from hikari.events import base as base_events -from hikari.models import bases as base_models from hikari.models import intents +from hikari.utilities import snowflake if typing.TYPE_CHECKING: + from hikari.api import rest from hikari.models import channels from hikari.models import guilds from hikari.models import invites - from hikari.utilities import snowflake @base_events.requires_intents(intents.Intent.GUILDS) # TODO: this intent doesn't account for DM channels. @@ -81,13 +81,16 @@ class ChannelDeleteEvent(BaseChannelEvent): @base_events.requires_intents(intents.Intent.GUILDS) # TODO: this intent doesn't account for DM channels. @attr.s(eq=False, hash=False, init=False, kw_only=True, slots=True) -class ChannelPinsUpdateEvent(base_events.Event, base_models.Entity): +class ChannelPinsUpdateEvent(base_events.Event): """Used to represent the Channel Pins Update gateway event. Sent when a message is pinned or unpinned in a channel but not when a pinned message is deleted. """ + app: rest.IRESTApp = attr.ib(default=None, repr=False, eq=False, hash=False) + """The client application that models may use for procedures.""" + guild_id: typing.Optional[snowflake.Snowflake] = attr.ib(repr=True) """The ID of the guild where this event happened. @@ -106,12 +109,15 @@ class ChannelPinsUpdateEvent(base_events.Event, base_models.Entity): @base_events.requires_intents(intents.Intent.GUILD_WEBHOOKS) @attr.s(eq=False, hash=False, init=False, kw_only=True, slots=True) -class WebhookUpdateEvent(base_events.Event, base_models.Entity): +class WebhookUpdateEvent(base_events.Event): """Used to represent webhook update gateway events. Sent when a webhook is updated, created or deleted in a guild. """ + app: rest.IRESTApp = attr.ib(default=None, repr=False, eq=False, hash=False) + """The client application that models may use for procedures.""" + guild_id: snowflake.Snowflake = attr.ib(repr=True) """The ID of the guild this webhook is being updated in.""" @@ -121,12 +127,15 @@ class WebhookUpdateEvent(base_events.Event, base_models.Entity): @base_events.requires_intents(intents.Intent.GUILD_MESSAGE_TYPING, intents.Intent.DIRECT_MESSAGE_TYPING) @attr.s(eq=False, hash=False, init=False, kw_only=True, slots=True) -class TypingStartEvent(base_events.Event, base_models.Entity): +class TypingStartEvent(base_events.Event): """Used to represent typing start gateway events. Received when a user or bot starts "typing" in a channel. """ + app: rest.IRESTApp = attr.ib(default=None, repr=False, eq=False, hash=False) + """The client application that models may use for procedures.""" + channel_id: snowflake.Snowflake = attr.ib(repr=True) """The ID of the channel this typing event is occurring in.""" @@ -160,12 +169,15 @@ class InviteCreateEvent(base_events.Event): @base_events.requires_intents(intents.Intent.GUILD_INVITES) @attr.s(eq=False, hash=False, init=False, kw_only=True, slots=True) -class InviteDeleteEvent(base_events.Event, base_models.Entity): +class InviteDeleteEvent(base_events.Event): """Used to represent Invite Delete gateway events. Sent when an invite is deleted for a channel we can access. """ + app: rest.IRESTApp = attr.ib(default=None, repr=False, eq=False, hash=False) + """The client application that models may use for procedures.""" + channel_id: snowflake.Snowflake = attr.ib(repr=True) """The ID of the channel this ID was attached to.""" diff --git a/hikari/events/guild.py b/hikari/events/guild.py index 14913c3e42..cb294eba68 100644 --- a/hikari/events/guild.py +++ b/hikari/events/guild.py @@ -45,15 +45,15 @@ import attr from hikari.events import base as base_events -from hikari.models import bases as base_models from hikari.models import intents +from hikari.utilities import snowflake if typing.TYPE_CHECKING: + from hikari.api import rest from hikari.models import emojis as emojis_models from hikari.models import guilds from hikari.models import presences from hikari.models import users - from hikari.utilities import snowflake @base_events.requires_intents(intents.Intent.GUILDS) @@ -86,29 +86,45 @@ class GuildUpdateEvent(GuildEvent): @base_events.requires_intents(intents.Intent.GUILDS) @attr.s(eq=False, hash=False, init=False, kw_only=True, slots=True) -class GuildLeaveEvent(GuildEvent, base_models.Unique): +class GuildLeaveEvent(GuildEvent, snowflake.Unique): """Fired when the current user leaves the guild or is kicked/banned from it. !!! note This is fired based on Discord's Guild Delete gateway event. """ + id: snowflake.Snowflake = attr.ib( + converter=snowflake.Snowflake, eq=True, hash=True, repr=True, factory=snowflake.Snowflake, + ) + """The ID of this entity.""" + @base_events.requires_intents(intents.Intent.GUILDS) @attr.s(eq=False, hash=False, init=False, kw_only=True, slots=True) -class GuildUnavailableEvent(GuildEvent, base_models.Entity, base_models.Unique): +class GuildUnavailableEvent(GuildEvent, snowflake.Unique): """Fired when a guild becomes temporarily unavailable due to an outage. !!! note This is fired based on Discord's Guild Delete gateway event. """ + id: snowflake.Snowflake = attr.ib( + converter=snowflake.Snowflake, eq=True, hash=True, repr=True, factory=snowflake.Snowflake, + ) + """The ID of this entity.""" + + app: rest.IRESTApp = attr.ib(default=None, repr=False, eq=False, hash=False) + """The client application that models may use for procedures.""" + @base_events.requires_intents(intents.Intent.GUILD_BANS) @attr.s(eq=False, hash=False, init=False, kw_only=True, slots=True) -class GuildBanEvent(GuildEvent, base_models.Entity, abc.ABC): +class GuildBanEvent(GuildEvent, abc.ABC): """A base object that guild ban events will inherit from.""" + app: rest.IRESTApp = attr.ib(default=None, repr=False, eq=False, hash=False) + """The client application that models may use for procedures.""" + guild_id: snowflake.Snowflake = attr.ib(repr=True) """The ID of the guild this ban is in.""" @@ -130,9 +146,12 @@ class GuildBanRemoveEvent(GuildBanEvent): @base_events.requires_intents(intents.Intent.GUILD_EMOJIS) @attr.s(eq=False, hash=False, init=False, kw_only=True, slots=True) -class GuildEmojisUpdateEvent(GuildEvent, base_models.Entity): +class GuildEmojisUpdateEvent(GuildEvent): """Represents a Guild Emoji Update gateway event.""" + app: rest.IRESTApp = attr.ib(default=None, repr=False, eq=False, hash=False) + """The client application that models may use for procedures.""" + guild_id: snowflake.Snowflake = attr.ib(repr=True) """The ID of the guild this emoji was updated in.""" @@ -142,9 +161,12 @@ class GuildEmojisUpdateEvent(GuildEvent, base_models.Entity): @base_events.requires_intents(intents.Intent.GUILD_INTEGRATIONS) @attr.s(eq=False, hash=False, init=False, kw_only=True, slots=True) -class GuildIntegrationsUpdateEvent(GuildEvent, base_models.Entity): +class GuildIntegrationsUpdateEvent(GuildEvent): """Used to represent Guild Integration Update gateway events.""" + app: rest.IRESTApp = attr.ib(default=None, repr=False, eq=False, hash=False) + """The client application that models may use for procedures.""" + guild_id: snowflake.Snowflake = attr.ib(repr=True) """The ID of the guild the integration was updated in.""" @@ -157,9 +179,12 @@ class GuildMemberEvent(GuildEvent): @base_events.requires_intents(intents.Intent.GUILD_MEMBERS) @attr.s(eq=False, hash=False, init=False, kw_only=True, slots=True) -class GuildMemberAddEvent(GuildMemberEvent, base_models.Entity): +class GuildMemberAddEvent(GuildMemberEvent): """Used to represent a Guild Member Add gateway event.""" + app: rest.IRESTApp = attr.ib(default=None, repr=False, eq=False, hash=False) + """The client application that models may use for procedures.""" + guild_id: snowflake.Snowflake = attr.ib(repr=True) # TODO: do we want to have guild_id on all members? """The ID of the guild where this member was added.""" @@ -180,12 +205,15 @@ class GuildMemberUpdateEvent(GuildMemberEvent): @base_events.requires_intents(intents.Intent.GUILD_MEMBERS) @attr.s(eq=False, hash=False, init=False, kw_only=True, slots=True) -class GuildMemberRemoveEvent(GuildMemberEvent, base_models.Entity): +class GuildMemberRemoveEvent(GuildMemberEvent): """Used to represent Guild Member Remove gateway events. Sent when a member is kicked, banned or leaves a guild. """ + app: rest.IRESTApp = attr.ib(default=None, repr=False, eq=False, hash=False) + """The client application that models may use for procedures.""" + # TODO: make GuildMember event into common base class. guild_id: snowflake.Snowflake = attr.ib(repr=True) """The ID of the guild this user was removed from.""" @@ -201,9 +229,12 @@ class GuildRoleEvent(GuildEvent): @base_events.requires_intents(intents.Intent.GUILDS) @attr.s(eq=False, hash=False, init=False, kw_only=True, slots=True) -class GuildRoleCreateEvent(GuildRoleEvent, base_models.Entity): +class GuildRoleCreateEvent(GuildRoleEvent): """Used to represent a Guild Role Create gateway event.""" + app: rest.IRESTApp = attr.ib(default=None, repr=False, eq=False, hash=False) + """The client application that models may use for procedures.""" + guild_id: snowflake.Snowflake = attr.ib(repr=True) """The ID of the guild where this role was created.""" @@ -213,9 +244,12 @@ class GuildRoleCreateEvent(GuildRoleEvent, base_models.Entity): @base_events.requires_intents(intents.Intent.GUILDS) @attr.s(eq=False, hash=False, init=False, kw_only=True, slots=True) -class GuildRoleUpdateEvent(GuildRoleEvent, base_models.Entity): +class GuildRoleUpdateEvent(GuildRoleEvent): """Used to represent a Guild Role Create gateway event.""" + app: rest.IRESTApp = attr.ib(default=None, repr=False, eq=False, hash=False) + """The client application that models may use for procedures.""" + # TODO: make any event with a guild ID into a custom base event. # https://pypi.org/project/stupid/ could this work around the multiple inheritance problem? guild_id: snowflake.Snowflake = attr.ib(repr=True) @@ -227,9 +261,12 @@ class GuildRoleUpdateEvent(GuildRoleEvent, base_models.Entity): @base_events.requires_intents(intents.Intent.GUILDS) @attr.s(eq=False, hash=False, init=False, kw_only=True, slots=True) -class GuildRoleDeleteEvent(GuildRoleEvent, base_models.Entity): +class GuildRoleDeleteEvent(GuildRoleEvent): """Represents a gateway Guild Role Delete Event.""" + app: rest.IRESTApp = attr.ib(default=None, repr=False, eq=False, hash=False) + """The client application that models may use for procedures.""" + guild_id: snowflake.Snowflake = attr.ib(repr=True) """The ID of the guild where this role is being deleted.""" diff --git a/hikari/events/message.py b/hikari/events/message.py index 7ed5d5bb81..a85f208b68 100644 --- a/hikari/events/message.py +++ b/hikari/events/message.py @@ -20,7 +20,7 @@ from __future__ import annotations __all__: typing.Final[typing.List[str]] = [ - "BaseMessageReactionEvent", + "MessageReactionEvent", "MessageCreateEvent", "UpdatedMessageFields", "MessageUpdateEvent", @@ -37,19 +37,19 @@ import attr from hikari.events import base as base_events -from hikari.models import bases as base_models from hikari.models import intents from hikari.models import messages +from hikari.utilities import snowflake if typing.TYPE_CHECKING: import datetime + from hikari.api import rest from hikari.models import applications from hikari.models import embeds as embed_models from hikari.models import emojis from hikari.models import guilds from hikari.models import users - from hikari.utilities import snowflake from hikari.utilities import undefined @@ -62,8 +62,8 @@ class MessageCreateEvent(base_events.Event): """The message that was sent.""" -@attr.s(slots=True, init=False, repr=True, eq=False) -class UpdatedMessageFields(base_models.Entity, base_models.Unique): +@attr.s(slots=True, kw_only=True, init=False, repr=True, eq=False) +class UpdatedMessageFields(snowflake.Unique): """An arbitrarily partial version of `hikari.models.messages.Message`. This contains arbitrary fields that may be updated in a @@ -77,6 +77,14 @@ class UpdatedMessageFields(base_models.Entity, base_models.Unique): nullability. """ + id: snowflake.Snowflake = attr.ib( + converter=snowflake.Snowflake, eq=True, hash=True, repr=True, factory=snowflake.Snowflake, + ) + """The ID of this entity.""" + + app: rest.IRESTApp = attr.ib(default=None, repr=False, eq=False, hash=False) + """The client application that models may use for procedures.""" + channel_id: snowflake.Snowflake = attr.ib(repr=True) """The ID of the channel that the message was sent in.""" @@ -157,7 +165,7 @@ class UpdatedMessageFields(base_models.Entity, base_models.Unique): @base_events.requires_intents(intents.Intent.GUILD_MESSAGES, intents.Intent.DIRECT_MESSAGES) @attr.s(eq=False, hash=False, init=False, kw_only=True, slots=True) -class MessageUpdateEvent(base_events.Event, base_models.Unique): +class MessageUpdateEvent(base_events.Event, snowflake.Unique): """Represents Message Update gateway events. !!! warn @@ -167,18 +175,26 @@ class MessageUpdateEvent(base_events.Event, base_models.Unique): (a singleton) to indicate that it has not been changed. """ + id: snowflake.Snowflake = attr.ib( + converter=snowflake.Snowflake, eq=True, hash=True, repr=True, factory=snowflake.Snowflake, + ) + """The ID of this entity.""" + message: UpdatedMessageFields = attr.ib(repr=True) """The partial message object with all updated fields.""" @base_events.requires_intents(intents.Intent.GUILD_MESSAGES, intents.Intent.DIRECT_MESSAGES) @attr.s(eq=False, hash=False, init=False, kw_only=True, slots=True) -class MessageDeleteEvent(base_events.Event, base_models.Entity): +class MessageDeleteEvent(base_events.Event): """Used to represent Message Delete gateway events. Sent when a message is deleted in a channel we have access to. """ + app: rest.IRESTApp = attr.ib(default=None, repr=False, eq=False, hash=False) + """The client application that models may use for procedures.""" + # TODO: common base class for Message events. channel_id: snowflake.Snowflake = attr.ib(repr=True) @@ -197,12 +213,15 @@ class MessageDeleteEvent(base_events.Event, base_models.Entity): # TODO: if this doesn't apply to DMs then does guild_id need to be nullable here? @base_events.requires_intents(intents.Intent.GUILD_MESSAGES) @attr.s(eq=False, hash=False, init=False, kw_only=True, slots=True) -class MessageDeleteBulkEvent(base_events.Event, base_models.Entity): +class MessageDeleteBulkEvent(base_events.Event): """Used to represent Message Bulk Delete gateway events. Sent when multiple messages are deleted in a channel at once. """ + app: rest.IRESTApp = attr.ib(default=None, repr=False, eq=False, hash=False) + """The client application that models may use for procedures.""" + channel_id: snowflake.Snowflake = attr.ib(repr=True) """The ID of the channel these messages have been deleted in.""" @@ -216,9 +235,12 @@ class MessageDeleteBulkEvent(base_events.Event, base_models.Entity): """A collection of the IDs of the messages that were deleted.""" -class BaseMessageReactionEvent(base_events.Event, base_models.Entity): +class MessageReactionEvent(base_events.Event): """A base class that all message reaction events will inherit from.""" + app: rest.IRESTApp = attr.ib(default=None, repr=False, eq=False, hash=False) + """The client application that models may use for procedures.""" + channel_id: snowflake.Snowflake = attr.ib(repr=True) """The ID of the channel where this reaction is happening.""" @@ -234,7 +256,7 @@ class BaseMessageReactionEvent(base_events.Event, base_models.Entity): @base_events.requires_intents(intents.Intent.GUILD_MESSAGE_REACTIONS, intents.Intent.DIRECT_MESSAGE_REACTIONS) @attr.s(eq=False, hash=False, init=False, kw_only=True, slots=True) -class MessageReactionAddEvent(BaseMessageReactionEvent): +class MessageReactionAddEvent(MessageReactionEvent): """Used to represent Message Reaction Add gateway events.""" user_id: snowflake.Snowflake = attr.ib(repr=True) @@ -253,7 +275,7 @@ class MessageReactionAddEvent(BaseMessageReactionEvent): @base_events.requires_intents(intents.Intent.GUILD_MESSAGE_REACTIONS, intents.Intent.DIRECT_MESSAGE_REACTIONS) @attr.s(eq=False, hash=False, init=False, kw_only=True, slots=True) -class MessageReactionRemoveEvent(BaseMessageReactionEvent): +class MessageReactionRemoveEvent(MessageReactionEvent): """Used to represent Message Reaction Remove gateway events.""" user_id: snowflake.Snowflake = attr.ib(repr=True) @@ -265,7 +287,7 @@ class MessageReactionRemoveEvent(BaseMessageReactionEvent): @base_events.requires_intents(intents.Intent.GUILD_MESSAGE_REACTIONS, intents.Intent.DIRECT_MESSAGE_REACTIONS) @attr.s(eq=False, hash=False, init=False, kw_only=True, slots=True) -class MessageReactionRemoveAllEvent(BaseMessageReactionEvent): +class MessageReactionRemoveAllEvent(MessageReactionEvent): """Used to represent Message Reaction Remove All gateway events. Sent when all the reactions are removed from a message, regardless of emoji. @@ -274,7 +296,7 @@ class MessageReactionRemoveAllEvent(BaseMessageReactionEvent): @base_events.requires_intents(intents.Intent.GUILD_MESSAGE_REACTIONS, intents.Intent.DIRECT_MESSAGE_REACTIONS) @attr.s(eq=False, hash=False, init=False, kw_only=True, slots=True) -class MessageReactionRemoveEmojiEvent(BaseMessageReactionEvent): +class MessageReactionRemoveEmojiEvent(MessageReactionEvent): """Represents Message Reaction Remove Emoji events. Sent when all the reactions for a single emoji are removed from a message. diff --git a/hikari/impl/entity_factory.py b/hikari/impl/entity_factory.py index b7a6e800ff..7941fb4a3b 100644 --- a/hikari/impl/entity_factory.py +++ b/hikari/impl/entity_factory.py @@ -39,7 +39,7 @@ from hikari.models import emojis as emoji_models from hikari.models import gateway as gateway_models from hikari.models import guilds as guild_models -from hikari.models import invites as invite_model +from hikari.models import invites as invite_models from hikari.models import messages as message_models from hikari.models import permissions as permission_models from hikari.models import presences as presence_models @@ -81,12 +81,12 @@ class EntityFactoryComponentImpl(entity_factory.IEntityFactoryComponent): if typing.TYPE_CHECKING: DMChannelT = typing.TypeVar("DMChannelT", bound=channel_models.DMChannel) GuildChannelT = typing.TypeVar("GuildChannelT", bound=channel_models.GuildChannel) - InviteT = typing.TypeVar("InviteT", bound=invite_model.Invite) + InviteT = typing.TypeVar("InviteT", bound=invite_models.Invite) PartialChannelT = typing.TypeVar("PartialChannelT", bound=channel_models.PartialChannel) PartialGuildT = typing.TypeVar("PartialGuildT", bound=guild_models.PartialGuild) PartialGuildIntegrationT = typing.TypeVar("PartialGuildIntegrationT", bound=guild_models.PartialIntegration) UserT = typing.TypeVar("UserT", bound=user_models.User) - ReactionEventT = typing.TypeVar("ReactionEventT", bound=message_events.BaseMessageReactionEvent) + ReactionEventT = typing.TypeVar("ReactionEventT", bound=message_events.MessageReactionEvent) GuildBanEventT = typing.TypeVar("GuildBanEventT", bound=guild_events.GuildBanEvent) def __init__(self, app: rest.IRESTApp) -> None: @@ -175,14 +175,17 @@ def deserialize_own_connection(self, payload: data_binding.JSONObject) -> applic return own_connection def deserialize_own_guild(self, payload: data_binding.JSONObject) -> application_models.OwnGuild: - own_guild = self._set_partial_guild_attributes(payload, application_models.OwnGuild(self._app)) + own_guild = application_models.OwnGuild() + own_guild.app = self._app + self._set_partial_guild_attributes(payload, own_guild) own_guild.is_owner = bool(payload["owner"]) # noinspection PyArgumentList own_guild.my_permissions = permission_models.Permission(payload["permissions"]) return own_guild def deserialize_application(self, payload: data_binding.JSONObject) -> application_models.Application: - application = application_models.Application(self._app) + application = application_models.Application() + application.app = self._app application.id = snowflake.Snowflake(payload["id"]) application.name = payload["name"] application.description = payload["description"] @@ -195,13 +198,15 @@ def deserialize_application(self, payload: data_binding.JSONObject) -> applicati application.icon_hash = payload.get("icon") if (team_payload := payload.get("team")) is not None: - team = application_models.Team(self._app) + team = application_models.Team() + team.app = self._app team.id = snowflake.Snowflake(team_payload["id"]) team.icon_hash = team_payload["icon"] members = {} for member_payload in team_payload["members"]: - team_member = application_models.TeamMember(self._app) + team_member = application_models.TeamMember() + team_member.app = self.app # noinspection PyArgumentList team_member.membership_state = application_models.TeamMembershipState( member_payload["membership_state"] @@ -234,7 +239,8 @@ def _deserialize_audit_log_change_roles( ) -> typing.Mapping[snowflake.Snowflake, guild_models.PartialRole]: roles = {} for role_payload in payload: - role = guild_models.PartialRole(self._app) + role = guild_models.PartialRole() + role.app = self._app role.id = snowflake.Snowflake(role_payload["id"]) role.name = role_payload["name"] roles[role.id] = role @@ -316,7 +322,8 @@ def deserialize_audit_log(self, payload: data_binding.JSONObject) -> audit_log_m entries = {} for entry_payload in payload["audit_log_entries"]: - entry = audit_log_models.AuditLogEntry(self._app) + entry = audit_log_models.AuditLogEntry() + entry.app = self._app entry.id = snowflake.Snowflake(entry_payload["id"]) if (target_id := entry_payload["target_id"]) is not None: @@ -395,33 +402,43 @@ def serialize_permission_overwrite(self, overwrite: channel_models.PermissionOve return {"id": str(overwrite.id), "type": overwrite.type, "allow": overwrite.allow, "deny": overwrite.deny} @staticmethod - def _set_partial_channel_attributes(payload: data_binding.JSONObject, channel: PartialChannelT) -> PartialChannelT: + def _set_partial_channel_attributes( + payload: data_binding.JSONObject, channel: channel_models.PartialChannel + ) -> None: channel.id = snowflake.Snowflake(payload["id"]) channel.name = payload.get("name") # noinspection PyArgumentList channel.type = channel_models.ChannelType(payload["type"]) - return channel def deserialize_partial_channel(self, payload: data_binding.JSONObject) -> channel_models.PartialChannel: - return self._set_partial_channel_attributes(payload, channel_models.PartialChannel(self._app)) + partial_channel = channel_models.PartialChannel() + partial_channel.app = self._app + self._set_partial_channel_attributes(payload, partial_channel) + return partial_channel - def _set_dm_channel_attributes(self, payload: data_binding.JSONObject, channel: DMChannelT) -> DMChannelT: - channel = self._set_partial_channel_attributes(payload, channel) + def _set_dm_channel_attributes( + self, payload: data_binding.JSONObject, dm_channel: channel_models.DMChannel + ) -> None: + self._set_partial_channel_attributes(payload, dm_channel) if (last_message_id := payload["last_message_id"]) is not None: last_message_id = snowflake.Snowflake(last_message_id) - channel.last_message_id = last_message_id - channel.recipients = { + dm_channel.last_message_id = last_message_id + dm_channel.recipients = { snowflake.Snowflake(user["id"]): self.deserialize_user(user) for user in payload["recipients"] } - return channel def deserialize_dm_channel(self, payload: data_binding.JSONObject) -> channel_models.DMChannel: - return self._set_dm_channel_attributes(payload, channel_models.DMChannel(self._app)) + dm_channel = channel_models.DMChannel() + dm_channel.app = self._app + self._set_dm_channel_attributes(payload, dm_channel) + return dm_channel def deserialize_group_dm_channel(self, payload: data_binding.JSONObject) -> channel_models.GroupDMChannel: - group_dm_channel = self._set_dm_channel_attributes(payload, channel_models.GroupDMChannel(self._app)) + group_dm_channel = channel_models.GroupDMChannel() + group_dm_channel.app = self._app + self._set_dm_channel_attributes(payload, group_dm_channel) group_dm_channel.owner_id = snowflake.Snowflake(payload["owner_id"]) group_dm_channel.icon_hash = payload["icon"] group_dm_channel.nicknames = { @@ -432,29 +449,34 @@ def deserialize_group_dm_channel(self, payload: data_binding.JSONObject) -> chan ) return group_dm_channel - def _set_guild_channel_attributes(self, payload: data_binding.JSONObject, channel: GuildChannelT) -> GuildChannelT: - channel = self._set_partial_channel_attributes(payload, channel) - channel.guild_id = snowflake.Snowflake(payload["guild_id"]) if "guild_id" in payload else None - channel.position = int(payload["position"]) - channel.permission_overwrites = { + def _set_guild_channel_attributes( + self, payload: data_binding.JSONObject, guild_channel: channel_models.GuildChannel + ) -> None: + self._set_partial_channel_attributes(payload, guild_channel) + guild_channel.guild_id = snowflake.Snowflake(payload["guild_id"]) if "guild_id" in payload else None + guild_channel.position = int(payload["position"]) + guild_channel.permission_overwrites = { snowflake.Snowflake(overwrite["id"]): self.deserialize_permission_overwrite(overwrite) for overwrite in payload["permission_overwrites"] } # TODO: while snowflakes are guaranteed to be unique within their own resource, there is no guarantee for # across between resources (user and role in this case); while in practice we won't get overlap there is a # chance that this may happen in the future, would it be more sensible to use a Sequence here? - channel.is_nsfw = payload.get("nsfw") + guild_channel.is_nsfw = payload.get("nsfw") if (parent_id := payload.get("parent_id")) is not None: parent_id = snowflake.Snowflake(parent_id) - channel.parent_id = parent_id - - return channel + guild_channel.parent_id = parent_id def deserialize_guild_category(self, payload: data_binding.JSONObject) -> channel_models.GuildCategory: - return self._set_guild_channel_attributes(payload, channel_models.GuildCategory(self._app)) + category = channel_models.GuildCategory() + category.app = self._app + self._set_guild_channel_attributes(payload, category) + return category def deserialize_guild_text_channel(self, payload: data_binding.JSONObject) -> channel_models.GuildTextChannel: - guild_text_category = self._set_guild_channel_attributes(payload, channel_models.GuildTextChannel(self._app)) + guild_text_category = channel_models.GuildTextChannel() + guild_text_category.app = self._app + self._set_guild_channel_attributes(payload, guild_text_category) guild_text_category.topic = payload["topic"] if (last_message_id := payload["last_message_id"]) is not None: @@ -470,7 +492,9 @@ def deserialize_guild_text_channel(self, payload: data_binding.JSONObject) -> ch return guild_text_category def deserialize_guild_news_channel(self, payload: data_binding.JSONObject) -> channel_models.GuildNewsChannel: - guild_news_channel = self._set_guild_channel_attributes(payload, channel_models.GuildNewsChannel(self._app)) + guild_news_channel = channel_models.GuildNewsChannel() + guild_news_channel.app = self._app + self._set_guild_channel_attributes(payload, guild_news_channel) guild_news_channel.topic = payload["topic"] if (last_message_id := payload["last_message_id"]) is not None: @@ -484,10 +508,15 @@ def deserialize_guild_news_channel(self, payload: data_binding.JSONObject) -> ch return guild_news_channel def deserialize_guild_store_channel(self, payload: data_binding.JSONObject) -> channel_models.GuildStoreChannel: - return self._set_guild_channel_attributes(payload, channel_models.GuildStoreChannel(self._app)) + guild_store_channel = channel_models.GuildStoreChannel() + guild_store_channel.app = self._app + self._set_guild_channel_attributes(payload, guild_store_channel) + return guild_store_channel def deserialize_guild_voice_channel(self, payload: data_binding.JSONObject) -> channel_models.GuildVoiceChannel: - guild_voice_channel = self._set_guild_channel_attributes(payload, channel_models.GuildVoiceChannel(self._app)) + guild_voice_channel = channel_models.GuildVoiceChannel() + guild_voice_channel.app = self._app + self._set_guild_channel_attributes(payload, guild_voice_channel) guild_voice_channel.bitrate = int(payload["bitrate"]) guild_voice_channel.user_limit = int(payload["user_limit"]) return guild_voice_channel @@ -678,14 +707,16 @@ def deserialize_unicode_emoji(self, payload: data_binding.JSONObject) -> emoji_m return unicode_emoji def deserialize_custom_emoji(self, payload: data_binding.JSONObject) -> emoji_models.CustomEmoji: - custom_emoji = emoji_models.CustomEmoji(self._app) + custom_emoji = emoji_models.CustomEmoji() + custom_emoji.app = self._app custom_emoji.id = snowflake.Snowflake(payload["id"]) custom_emoji.name = payload["name"] custom_emoji.is_animated = payload.get("animated", False) return custom_emoji def deserialize_known_custom_emoji(self, payload: data_binding.JSONObject) -> emoji_models.KnownCustomEmoji: - known_custom_emoji = emoji_models.KnownCustomEmoji(self._app) + known_custom_emoji = emoji_models.KnownCustomEmoji() + known_custom_emoji.app = self._app known_custom_emoji.id = snowflake.Snowflake(payload["id"]) known_custom_emoji.name = payload["name"] known_custom_emoji.is_animated = payload.get("animated", False) @@ -732,14 +763,15 @@ def deserialize_gateway_bot(self, payload: data_binding.JSONObject) -> gateway_m ################ def deserialize_guild_widget(self, payload: data_binding.JSONObject) -> guild_models.GuildWidget: - guild_embed = guild_models.GuildWidget(self._app) + guild_widget = guild_models.GuildWidget() + guild_widget.app = self._app if (channel_id := payload["channel_id"]) is not None: channel_id = snowflake.Snowflake(channel_id) - guild_embed.channel_id = channel_id + guild_widget.channel_id = channel_id - guild_embed.is_enabled = payload["enabled"] - return guild_embed + guild_widget.is_enabled = payload["enabled"] + return guild_widget def deserialize_member( self, @@ -747,7 +779,8 @@ def deserialize_member( *, user: typing.Union[undefined.UndefinedType, user_models.User] = undefined.UNDEFINED, ) -> guild_models.Member: - guild_member = guild_models.Member(self._app) + guild_member = guild_models.Member() + guild_member.app = self._app guild_member.user = user or self.deserialize_user(payload["user"]) guild_member.role_ids = {snowflake.Snowflake(role_id) for role_id in payload["roles"]} @@ -769,7 +802,8 @@ def deserialize_member( return guild_member def deserialize_role(self, payload: data_binding.JSONObject) -> guild_models.Role: - guild_role = guild_models.Role(self._app) + guild_role = guild_models.Role() + guild_role.app = self._app guild_role.id = snowflake.Snowflake(payload["id"]) guild_role.name = payload["name"] guild_role.color = color_models.Color(payload["color"]) @@ -783,8 +817,8 @@ def deserialize_role(self, payload: data_binding.JSONObject) -> guild_models.Rol @staticmethod def _set_partial_integration_attributes( - payload: data_binding.JSONObject, integration: PartialGuildIntegrationT - ) -> PartialGuildIntegrationT: + payload: data_binding.JSONObject, integration: guild_models.PartialIntegration + ) -> None: integration.id = snowflake.Snowflake(payload["id"]) integration.name = payload["name"] integration.type = payload["type"] @@ -793,13 +827,15 @@ def _set_partial_integration_attributes( account.id = account_payload["id"] account.name = account_payload["name"] integration.account = account - return integration def deserialize_partial_integration(self, payload: data_binding.JSONObject) -> guild_models.PartialIntegration: - return self._set_partial_integration_attributes(payload, guild_models.PartialIntegration()) + partial_integration = guild_models.PartialIntegration() + self._set_partial_integration_attributes(payload, partial_integration) + return partial_integration def deserialize_integration(self, payload: data_binding.JSONObject) -> guild_models.Integration: - guild_integration = self._set_partial_integration_attributes(payload, guild_models.Integration()) + guild_integration = guild_models.Integration() + self._set_partial_integration_attributes(payload, guild_integration) guild_integration.is_enabled = payload["enabled"] guild_integration.is_syncing = payload["syncing"] @@ -826,12 +862,12 @@ def deserialize_guild_member_ban(self, payload: data_binding.JSONObject) -> guil return guild_member_ban def deserialize_unavailable_guild(self, payload: data_binding.JSONObject) -> guild_models.UnavailableGuild: - unavailable_guild = guild_models.UnavailableGuild(self._app) + unavailable_guild = guild_models.UnavailableGuild() unavailable_guild.id = snowflake.Snowflake(payload["id"]) return unavailable_guild @staticmethod - def _set_partial_guild_attributes(payload: data_binding.JSONObject, guild: PartialGuildT) -> PartialGuildT: + def _set_partial_guild_attributes(payload: data_binding.JSONObject, guild: guild_models.PartialGuild) -> None: guild.id = snowflake.Snowflake(payload["id"]) guild.name = payload["name"] guild.icon_hash = payload["icon"] @@ -845,10 +881,10 @@ def _set_partial_guild_attributes(payload: data_binding.JSONObject, guild: Parti features.append(feature) guild.features = set(features) - return guild - def deserialize_guild_preview(self, payload: data_binding.JSONObject) -> guild_models.GuildPreview: - guild_preview = self._set_partial_guild_attributes(payload, guild_models.GuildPreview(self._app)) + guild_preview = guild_models.GuildPreview() + guild_preview.app = self._app + self._set_partial_guild_attributes(payload, guild_preview) guild_preview.splash_hash = payload["splash"] guild_preview.discovery_splash_hash = payload["discovery_splash"] guild_preview.emojis = { @@ -860,13 +896,16 @@ def deserialize_guild_preview(self, payload: data_binding.JSONObject) -> guild_m return guild_preview def deserialize_guild(self, payload: data_binding.JSONObject) -> guild_models.Guild: - guild = self._set_partial_guild_attributes(payload, guild_models.Guild(self._app)) + guild = guild_models.Guild() + self._set_partial_guild_attributes(payload, guild) + guild.app = self._app guild.splash_hash = payload["splash"] guild.discovery_splash_hash = payload["discovery_splash"] guild.owner_id = snowflake.Snowflake(payload["owner_id"]) # noinspection PyArgumentList if (perms := payload.get("permissions")) is not None: + # noinspection PyArgumentList guild.my_permissions = permission_models.Permission(perms) else: guild.my_permissions = None @@ -967,6 +1006,7 @@ def deserialize_guild(self, payload: data_binding.JSONObject) -> guild_models.Gu guild.vanity_url_code = payload["vanity_url_code"] guild.description = payload["description"] guild.banner_hash = payload["banner"] + # noinspection PyArgumentList guild.premium_tier = guild_models.GuildPremiumTier(payload["premium_tier"]) if (premium_subscription_count := payload.get("premium_subscription_count")) is not None: @@ -991,17 +1031,20 @@ def deserialize_guild(self, payload: data_binding.JSONObject) -> guild_models.Gu # INVITE MODELS # ################# - def deserialize_vanity_url(self, payload: data_binding.JSONObject) -> invite_model.VanityURL: - vanity_url = invite_model.VanityURL(self._app) + def deserialize_vanity_url(self, payload: data_binding.JSONObject) -> invite_models.VanityURL: + vanity_url = invite_models.VanityURL() + vanity_url.app = self._app vanity_url.code = payload["code"] vanity_url.uses = int(payload["uses"]) return vanity_url - def _set_invite_attributes(self, payload: data_binding.JSONObject, invite: InviteT) -> InviteT: + def _set_invite_attributes(self, payload: data_binding.JSONObject, invite: invite_models.Invite) -> None: invite.code = payload["code"] if (guild_payload := payload.get("guild", ...)) is not ...: - guild = self._set_partial_guild_attributes(guild_payload, invite_model.InviteGuild(self._app)) + guild = invite_models.InviteGuild() + guild.app = self._app + self._set_partial_guild_attributes(guild_payload, guild) guild.splash_hash = guild_payload["splash"] guild.banner_hash = guild_payload["banner"] guild.description = guild_payload["description"] @@ -1028,7 +1071,7 @@ def _set_invite_attributes(self, payload: data_binding.JSONObject, invite: Invit invite.target_user = self.deserialize_user(payload["target_user"]) if "target_user" in payload else None # noinspection PyArgumentList invite.target_user_type = ( - invite_model.TargetUserType(payload["target_user_type"]) if "target_user_type" in payload else None + invite_models.TargetUserType(payload["target_user_type"]) if "target_user_type" in payload else None ) invite.approximate_presence_count = ( int(payload["approximate_presence_count"]) if "approximate_presence_count" in payload else None @@ -1036,13 +1079,17 @@ def _set_invite_attributes(self, payload: data_binding.JSONObject, invite: Invit invite.approximate_member_count = ( int(payload["approximate_member_count"]) if "approximate_member_count" in payload else None ) - return invite - def deserialize_invite(self, payload: data_binding.JSONObject) -> invite_model.Invite: - return self._set_invite_attributes(payload, invite_model.Invite(self._app)) + def deserialize_invite(self, payload: data_binding.JSONObject) -> invite_models.Invite: + invite = invite_models.Invite() + invite.app = self._app + self._set_invite_attributes(payload, invite) + return invite - def deserialize_invite_with_metadata(self, payload: data_binding.JSONObject) -> invite_model.InviteWithMetadata: - invite_with_metadata = self._set_invite_attributes(payload, invite_model.InviteWithMetadata(self._app)) + def deserialize_invite_with_metadata(self, payload: data_binding.JSONObject) -> invite_models.InviteWithMetadata: + invite_with_metadata = invite_models.InviteWithMetadata() + invite_with_metadata.app = self._app + self._set_invite_attributes(payload, invite_with_metadata) invite_with_metadata.uses = int(payload["uses"]) invite_with_metadata.max_uses = int(payload["max_uses"]) max_age = payload["max_age"] @@ -1056,7 +1103,8 @@ def deserialize_invite_with_metadata(self, payload: data_binding.JSONObject) -> ################## def deserialize_message(self, payload: data_binding.JSONObject) -> message_models.Message: - message = message_models.Message(self._app) + message = message_models.Message() + message.app = self._app message.id = snowflake.Snowflake(payload["id"]) message.channel_id = snowflake.Snowflake(payload["channel_id"]) message.guild_id = snowflake.Snowflake(payload["guild_id"]) if "guild_id" in payload else None @@ -1120,7 +1168,8 @@ def deserialize_message(self, payload: data_binding.JSONObject) -> message_model message.application = self.deserialize_application(payload["application"]) if "application" in payload else None if (crosspost_payload := payload.get("message_reference", ...)) is not ...: - crosspost = message_models.MessageCrosspost(self._app) + crosspost = message_models.MessageCrosspost() + crosspost.app = self._app crosspost.id = ( snowflake.Snowflake(crosspost_payload["message_id"]) if "message_id" in crosspost_payload else None ) @@ -1142,9 +1191,11 @@ def deserialize_message(self, payload: data_binding.JSONObject) -> message_model ################### def deserialize_member_presence(self, payload: data_binding.JSONObject) -> presence_models.MemberPresence: - guild_member_presence = presence_models.MemberPresence(self._app) + guild_member_presence = presence_models.MemberPresence() + guild_member_presence.app = self._app user_payload = payload["user"] - user = user_models.PartialUser(self._app) + user = user_models.PartialUser() + user.app = self._app user.id = snowflake.Snowflake(user_payload["id"]) user.discriminator = user_payload["discriminator"] if "discriminator" in user_payload else undefined.UNDEFINED user.username = user_payload["username"] if "username" in user_payload else undefined.UNDEFINED @@ -1280,17 +1331,18 @@ def deserialize_member_presence(self, payload: data_binding.JSONObject) -> prese ############### @staticmethod - def _set_user_attributes(payload: data_binding.JSONObject, user: UserT) -> UserT: + def _set_user_attributes(payload: data_binding.JSONObject, user: user_models.User) -> None: user.id = snowflake.Snowflake(payload["id"]) user.discriminator = payload["discriminator"] user.username = payload["username"] user.avatar_hash = payload["avatar"] user.is_bot = payload.get("bot", False) user.is_system = payload.get("system", False) - return user def deserialize_user(self, payload: data_binding.JSONObject) -> user_models.User: - user = self._set_user_attributes(payload, user_models.User(self._app)) + user = user_models.User() + user.app = self._app + self._set_user_attributes(payload, user) # noinspection PyArgumentList user.flags = ( user_models.UserFlag(payload["public_flags"]) if "public_flags" in payload else user_models.UserFlag.NONE @@ -1298,7 +1350,9 @@ def deserialize_user(self, payload: data_binding.JSONObject) -> user_models.User return user def deserialize_my_user(self, payload: data_binding.JSONObject) -> user_models.OwnUser: - my_user = self._set_user_attributes(payload, user_models.OwnUser(self._app)) + my_user = user_models.OwnUser() + my_user.app = self._app + self._set_user_attributes(payload, my_user) my_user.is_mfa_enabled = payload["mfa_enabled"] my_user.locale = payload.get("locale") my_user.is_verified = payload.get("verified") @@ -1314,7 +1368,8 @@ def deserialize_my_user(self, payload: data_binding.JSONObject) -> user_models.O ################ def deserialize_voice_state(self, payload: data_binding.JSONObject) -> voice_models.VoiceState: - voice_state = voice_models.VoiceState(self._app) + voice_state = voice_models.VoiceState() + voice_state.app = self._app voice_state.guild_id = snowflake.Snowflake(payload["guild_id"]) if "guild_id" in payload else None if (channel_id := payload["channel_id"]) is not None: @@ -1348,7 +1403,8 @@ def deserialize_voice_region(self, payload: data_binding.JSONObject) -> voice_mo ################## def deserialize_webhook(self, payload: data_binding.JSONObject) -> webhook_models.Webhook: - webhook = webhook_models.Webhook(self._app) + webhook = webhook_models.Webhook() + webhook.app = self._app webhook.id = snowflake.Snowflake(payload["id"]) # noinspection PyArgumentList webhook.type = webhook_models.WebhookType(payload["type"]) @@ -1382,7 +1438,8 @@ def deserialize_channel_delete_event(self, payload: data_binding.JSONObject) -> def deserialize_channel_pins_update_event( self, payload: data_binding.JSONObject ) -> channel_events.ChannelPinsUpdateEvent: - channel_pins_update = channel_events.ChannelPinsUpdateEvent(self._app) + channel_pins_update = channel_events.ChannelPinsUpdateEvent() + channel_pins_update.app = self._app channel_pins_update.guild_id = snowflake.Snowflake(payload["guild_id"]) if "guild_id" in payload else None channel_pins_update.channel_id = snowflake.Snowflake(payload["channel_id"]) @@ -1394,13 +1451,15 @@ def deserialize_channel_pins_update_event( return channel_pins_update def deserialize_webhook_update_event(self, payload: data_binding.JSONObject) -> channel_events.WebhookUpdateEvent: - webhook_update = channel_events.WebhookUpdateEvent(self._app) + webhook_update = channel_events.WebhookUpdateEvent() + webhook_update.app = self._app webhook_update.guild_id = snowflake.Snowflake(payload["guild_id"]) webhook_update.channel_id = snowflake.Snowflake(payload["channel_id"]) return webhook_update def deserialize_typing_start_event(self, payload: data_binding.JSONObject) -> channel_events.TypingStartEvent: - typing_start = channel_events.TypingStartEvent(self._app) + typing_start = channel_events.TypingStartEvent() + typing_start.app = self._app typing_start.channel_id = snowflake.Snowflake(payload["channel_id"]) typing_start.guild_id = snowflake.Snowflake(payload["guild_id"]) if "guild_id" in payload else None typing_start.user_id = snowflake.Snowflake(payload["user_id"]) @@ -1416,7 +1475,8 @@ def deserialize_invite_create_event(self, payload: data_binding.JSONObject) -> c return invite_create def deserialize_invite_delete_event(self, payload: data_binding.JSONObject) -> channel_events.InviteDeleteEvent: - invite_delete = channel_events.InviteDeleteEvent(self._app) + invite_delete = channel_events.InviteDeleteEvent() + invite_delete.app = self._app invite_delete.code = payload["code"] invite_delete.channel_id = snowflake.Snowflake(payload["channel_id"]) invite_delete.guild_id = snowflake.Snowflake(payload["guild_id"]) if "guild_id" in payload else None @@ -1444,27 +1504,34 @@ def deserialize_guild_leave_event(self, payload: data_binding.JSONObject) -> gui def deserialize_guild_unavailable_event( self, payload: data_binding.JSONObject ) -> guild_events.GuildUnavailableEvent: - guild_unavailable = guild_events.GuildUnavailableEvent(self._app) + guild_unavailable = guild_events.GuildUnavailableEvent() + guild_unavailable.app = self._app guild_unavailable.id = snowflake.Snowflake(payload["id"]) return guild_unavailable def _set_base_guild_ban_event_fields( - self, payload: data_binding.JSONObject, guild_ban: GuildBanEventT - ) -> GuildBanEventT: + self, payload: data_binding.JSONObject, guild_ban: guild_events.GuildBanEvent + ) -> None: guild_ban.guild_id = snowflake.Snowflake(payload["guild_id"]) guild_ban.user = self.deserialize_user(payload["user"]) - return guild_ban def deserialize_guild_ban_add_event(self, payload: data_binding.JSONObject) -> guild_events.GuildBanAddEvent: - return self._set_base_guild_ban_event_fields(payload, guild_events.GuildBanAddEvent(self._app)) + guild_ban_add_event = guild_events.GuildBanAddEvent() + guild_ban_add_event.app = self._app + self._set_base_guild_ban_event_fields(payload, guild_ban_add_event) + return guild_ban_add_event def deserialize_guild_ban_remove_event(self, payload: data_binding.JSONObject) -> guild_events.GuildBanRemoveEvent: - return self._set_base_guild_ban_event_fields(payload, guild_events.GuildBanRemoveEvent(self._app)) + guild_ban_remove_event = guild_events.GuildBanRemoveEvent() + guild_ban_remove_event.app = self._app + self._set_base_guild_ban_event_fields(payload, guild_ban_remove_event) + return guild_ban_remove_event def deserialize_guild_emojis_update_event( self, payload: data_binding.JSONObject ) -> guild_events.GuildEmojisUpdateEvent: - guild_emojis_update = guild_events.GuildEmojisUpdateEvent(self._app) + guild_emojis_update = guild_events.GuildEmojisUpdateEvent() + guild_emojis_update.app = self._app guild_emojis_update.guild_id = snowflake.Snowflake(payload["guild_id"]) guild_emojis_update.emojis = { snowflake.Snowflake(emoji["id"]): self.deserialize_known_custom_emoji(emoji) for emoji in payload["emojis"] @@ -1474,12 +1541,14 @@ def deserialize_guild_emojis_update_event( def deserialize_guild_integrations_update_event( self, payload: data_binding.JSONObject ) -> guild_events.GuildIntegrationsUpdateEvent: - guild_integrations_update = guild_events.GuildIntegrationsUpdateEvent(self._app) + guild_integrations_update = guild_events.GuildIntegrationsUpdateEvent() + guild_integrations_update.app = self._app guild_integrations_update.guild_id = snowflake.Snowflake(payload["guild_id"]) return guild_integrations_update def deserialize_guild_member_add_event(self, payload: data_binding.JSONObject) -> guild_events.GuildMemberAddEvent: - guild_member_add = guild_events.GuildMemberAddEvent(self._app) + guild_member_add = guild_events.GuildMemberAddEvent() + guild_member_add.app = self._app guild_member_add.guild_id = snowflake.Snowflake(payload["guild_id"]) guild_member_add.member = self.deserialize_member(payload) return guild_member_add @@ -1494,7 +1563,8 @@ def deserialize_guild_member_update_event( def deserialize_guild_member_remove_event( self, payload: data_binding.JSONObject ) -> guild_events.GuildMemberRemoveEvent: - guild_member_remove = guild_events.GuildMemberRemoveEvent(self._app) + guild_member_remove = guild_events.GuildMemberRemoveEvent() + guild_member_remove.app = self._app guild_member_remove.guild_id = snowflake.Snowflake(payload["guild_id"]) guild_member_remove.user = self.deserialize_user(payload["user"]) return guild_member_remove @@ -1502,7 +1572,8 @@ def deserialize_guild_member_remove_event( def deserialize_guild_role_create_event( self, payload: data_binding.JSONObject ) -> guild_events.GuildRoleCreateEvent: - guild_role_create = guild_events.GuildRoleCreateEvent(self._app) + guild_role_create = guild_events.GuildRoleCreateEvent() + guild_role_create.app = self._app guild_role_create.guild_id = snowflake.Snowflake(payload["guild_id"]) guild_role_create.role = self.deserialize_role(payload["role"]) return guild_role_create @@ -1510,7 +1581,8 @@ def deserialize_guild_role_create_event( def deserialize_guild_role_update_event( self, payload: data_binding.JSONObject ) -> guild_events.GuildRoleUpdateEvent: - guild_role_update = guild_events.GuildRoleUpdateEvent(self._app) + guild_role_update = guild_events.GuildRoleUpdateEvent() + guild_role_update.app = self._app guild_role_update.guild_id = snowflake.Snowflake(payload["guild_id"]) guild_role_update.role = self.deserialize_role(payload["role"]) return guild_role_update @@ -1518,7 +1590,8 @@ def deserialize_guild_role_update_event( def deserialize_guild_role_delete_event( self, payload: data_binding.JSONObject ) -> guild_events.GuildRoleDeleteEvent: - guild_role_delete = guild_events.GuildRoleDeleteEvent(self._app) + guild_role_delete = guild_events.GuildRoleDeleteEvent() + guild_role_delete.app = self._app guild_role_delete.guild_id = snowflake.Snowflake(payload["guild_id"]) guild_role_delete.role_id = snowflake.Snowflake(payload["role_id"]) return guild_role_delete @@ -1540,7 +1613,8 @@ def deserialize_message_create_event(self, payload: data_binding.JSONObject) -> def deserialize_message_update_event(self, payload: data_binding.JSONObject) -> message_events.MessageUpdateEvent: message_update = message_events.MessageUpdateEvent() - updated_message = message_events.UpdatedMessageFields(self._app) + updated_message = message_events.UpdatedMessageFields() + updated_message.app = self._app updated_message.id = snowflake.Snowflake(payload["id"]) updated_message.channel_id = snowflake.Snowflake(payload["channel_id"]) updated_message.guild_id = ( @@ -1643,7 +1717,8 @@ def deserialize_message_update_event(self, payload: data_binding.JSONObject) -> ) if (crosspost_payload := payload.get("message_reference", ...)) is not ...: - crosspost = message_models.MessageCrosspost(self._app) + crosspost = message_models.MessageCrosspost() + crosspost.app = self._app crosspost.id = ( snowflake.Snowflake(crosspost_payload["message_id"]) if "message_id" in crosspost_payload else None ) @@ -1665,7 +1740,8 @@ def deserialize_message_update_event(self, payload: data_binding.JSONObject) -> return message_update def deserialize_message_delete_event(self, payload: data_binding.JSONObject) -> message_events.MessageDeleteEvent: - message_delete = message_events.MessageDeleteEvent(self._app) + message_delete = message_events.MessageDeleteEvent() + message_delete.app = self._app message_delete.channel_id = snowflake.Snowflake(payload["channel_id"]) message_delete.guild_id = snowflake.Snowflake(payload["guild_id"]) if "guild_id" in payload else None message_delete.message_id = snowflake.Snowflake(payload["id"]) @@ -1674,7 +1750,8 @@ def deserialize_message_delete_event(self, payload: data_binding.JSONObject) -> def deserialize_message_delete_bulk_event( self, payload: data_binding.JSONObject ) -> message_events.MessageDeleteBulkEvent: - message_delete_bulk = message_events.MessageDeleteBulkEvent(self._app) + message_delete_bulk = message_events.MessageDeleteBulkEvent() + message_delete_bulk.app = self._app message_delete_bulk.channel_id = snowflake.Snowflake(payload["channel_id"]) message_delete_bulk.guild_id = snowflake.Snowflake(payload["guild_id"]) if "guild_id" in payload else None message_delete_bulk.message_ids = {snowflake.Snowflake(message_id) for message_id in payload["ids"]} @@ -1682,19 +1759,18 @@ def deserialize_message_delete_bulk_event( @staticmethod def _set_base_message_reaction_fields( - payload: data_binding.JSONObject, reaction_event: ReactionEventT - ) -> ReactionEventT: + payload: data_binding.JSONObject, reaction_event: message_events.MessageReactionEvent + ) -> None: reaction_event.channel_id = snowflake.Snowflake(payload["channel_id"]) reaction_event.message_id = snowflake.Snowflake(payload["message_id"]) reaction_event.guild_id = snowflake.Snowflake(payload["guild_id"]) if "guild_id" in payload else None - return reaction_event def deserialize_message_reaction_add_event( self, payload: data_binding.JSONObject ) -> message_events.MessageReactionAddEvent: - message_reaction_add = self._set_base_message_reaction_fields( - payload, message_events.MessageReactionAddEvent(self._app) - ) + message_reaction_add = message_events.MessageReactionAddEvent() + message_reaction_add.app = self._app + self._set_base_message_reaction_fields(payload, message_reaction_add) message_reaction_add.user_id = snowflake.Snowflake(payload["user_id"]) message_reaction_add.member = self.deserialize_member(payload["member"]) if "member" in payload else None message_reaction_add.emoji = self.deserialize_emoji(payload["emoji"]) @@ -1703,9 +1779,9 @@ def deserialize_message_reaction_add_event( def deserialize_message_reaction_remove_event( self, payload: data_binding.JSONObject ) -> message_events.MessageReactionRemoveEvent: - message_reaction_remove = self._set_base_message_reaction_fields( - payload, message_events.MessageReactionRemoveEvent(self._app) - ) + message_reaction_remove = message_events.MessageReactionRemoveEvent() + message_reaction_remove.app = self._app + self._set_base_message_reaction_fields(payload, message_reaction_remove) message_reaction_remove.user_id = snowflake.Snowflake(payload["user_id"]) message_reaction_remove.emoji = self.deserialize_emoji(payload["emoji"]) return message_reaction_remove @@ -1713,14 +1789,17 @@ def deserialize_message_reaction_remove_event( def deserialize_message_reaction_remove_all_event( self, payload: data_binding.JSONObject ) -> message_events.MessageReactionRemoveAllEvent: - return self._set_base_message_reaction_fields(payload, message_events.MessageReactionRemoveAllEvent(self._app)) + message_reaction_event = message_events.MessageReactionRemoveAllEvent() + message_reaction_event.app = self._app + self._set_base_message_reaction_fields(payload, message_reaction_event) + return message_reaction_event def deserialize_message_reaction_remove_emoji_event( self, payload: data_binding.JSONObject ) -> message_events.MessageReactionRemoveEmojiEvent: - message_reaction_remove_emoji = self._set_base_message_reaction_fields( - payload, message_events.MessageReactionRemoveEmojiEvent(self._app) - ) + message_reaction_remove_emoji = message_events.MessageReactionRemoveEmojiEvent() + message_reaction_remove_emoji.app = self._app + self._set_base_message_reaction_fields(payload, message_reaction_remove_emoji) message_reaction_remove_emoji.emoji = self.deserialize_emoji(payload["emoji"]) return message_reaction_remove_emoji diff --git a/hikari/models/applications.py b/hikari/models/applications.py index 070ae06247..0ca1a73b6d 100644 --- a/hikari/models/applications.py +++ b/hikari/models/applications.py @@ -35,15 +35,15 @@ import attr -from hikari.models import bases from hikari.models import guilds from hikari.utilities import cdn from hikari.utilities import files +from hikari.utilities import snowflake if typing.TYPE_CHECKING: + from hikari.api import rest from hikari.models import permissions as permissions_ from hikari.models import users - from hikari.utilities import snowflake @enum.unique @@ -249,9 +249,12 @@ class TeamMembershipState(int, enum.Enum): @attr.s(eq=True, hash=True, init=False, kw_only=True, slots=True) -class TeamMember(bases.Entity): +class TeamMember: """Represents a member of a Team.""" + app: rest.IRESTApp = attr.ib(default=None, repr=False, eq=False, hash=False) + """The client application that models may use for procedures.""" + membership_state: TeamMembershipState = attr.ib(eq=False, hash=False, repr=False) """The state of this user's membership.""" @@ -270,9 +273,17 @@ class TeamMember(bases.Entity): @attr.s(eq=True, hash=True, init=False, kw_only=True, slots=True) -class Team(bases.Entity, bases.Unique): +class Team(snowflake.Unique): """Represents a development team, along with all its members.""" + app: rest.IRESTApp = attr.ib(default=None, repr=False, eq=False, hash=False) + """The client application that models may use for procedures.""" + + id: snowflake.Snowflake = attr.ib( + converter=snowflake.Snowflake, eq=True, hash=True, repr=True, factory=snowflake.Snowflake, + ) + """The ID of this entity.""" + icon_hash: typing.Optional[str] = attr.ib(eq=False, hash=False, repr=False) """The CDN hash of this team's icon. @@ -330,9 +341,17 @@ def format_icon(self, *, format_: str = "png", size: int = 4096) -> typing.Optio @attr.s(eq=True, hash=True, init=False, kw_only=True, slots=True) -class Application(bases.Entity, bases.Unique): +class Application(snowflake.Unique): """Represents the information of an Oauth2 Application.""" + app: rest.IRESTApp = attr.ib(default=None, repr=False, eq=False, hash=False) + """The client application that models may use for procedures.""" + + id: snowflake.Snowflake = attr.ib( + converter=snowflake.Snowflake, eq=True, hash=True, repr=True, factory=snowflake.Snowflake, + ) + """The ID of this entity.""" + name: str = attr.ib(eq=False, hash=False, repr=True) """The name of this application.""" diff --git a/hikari/models/audit_logs.py b/hikari/models/audit_logs.py index 6290216338..b528f12e5e 100644 --- a/hikari/models/audit_logs.py +++ b/hikari/models/audit_logs.py @@ -43,14 +43,14 @@ import attr -from hikari.models import bases +from hikari.utilities import snowflake if typing.TYPE_CHECKING: + from hikari.api import rest from hikari.models import channels from hikari.models import guilds from hikari.models import users as users_ from hikari.models import webhooks as webhooks_ - from hikari.utilities import snowflake @typing.final @@ -186,13 +186,18 @@ class BaseAuditLogEntryInfo(abc.ABC): @attr.s(eq=True, hash=False, init=False, kw_only=True, slots=True) -class ChannelOverwriteEntryInfo(BaseAuditLogEntryInfo, bases.Unique): +class ChannelOverwriteEntryInfo(BaseAuditLogEntryInfo, snowflake.Unique): """Represents the extra information for overwrite related audit log entries. Will be attached to the overwrite create, update and delete audit log entries. """ + id: snowflake.Snowflake = attr.ib( + converter=snowflake.Snowflake, eq=True, hash=True, repr=True, factory=snowflake.Snowflake, + ) + """The ID of this entity.""" + type: channels.PermissionOverwriteType = attr.ib(repr=True) """The type of entity this overwrite targets.""" @@ -276,9 +281,17 @@ def __init__(self, payload: typing.Mapping[str, str]) -> None: @attr.s(eq=True, hash=True, init=False, kw_only=True, slots=True) -class AuditLogEntry(bases.Entity, bases.Unique): +class AuditLogEntry(snowflake.Unique): """Represents an entry in a guild's audit log.""" + app: rest.IRESTApp = attr.ib(default=None, repr=False, eq=False, hash=False) + """The client application that models may use for procedures.""" + + id: snowflake.Snowflake = attr.ib( + converter=snowflake.Snowflake, eq=True, hash=True, repr=True, factory=snowflake.Snowflake, + ) + """The ID of this entity.""" + target_id: typing.Optional[snowflake.Snowflake] = attr.ib(eq=False, hash=False, repr=True) """The ID of the entity affected by this change, if applicable.""" diff --git a/hikari/models/bases.py b/hikari/models/bases.py deleted file mode 100644 index 759df9b7ea..0000000000 --- a/hikari/models/bases.py +++ /dev/null @@ -1,77 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright © Nekoka.tt 2019-2020 -# -# This file is part of Hikari. -# -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . -"""Datastructure bases.""" - -from __future__ import annotations - -__all__: typing.Final[typing.List[str]] = ["Entity", "Unique"] - -import abc -import typing - -import attr - -from hikari.utilities import snowflake - -if typing.TYPE_CHECKING: - import datetime - - from hikari.api import rest - - -@attr.s(eq=True, hash=False, init=False, kw_only=True, slots=False) -class Entity(abc.ABC): - """The base for an entity used in this API. - - An entity is a managed object that contains a binding to the owning - application instance. This enables it to perform API calls from - methods directly. - """ - - _app: rest.IRESTApp = attr.ib(default=None, repr=False, eq=False, hash=False) - """The client application that models may use for procedures.""" - - def __init__(self, app: rest.IRESTApp) -> None: - self._app = app - - -@attr.s(eq=True, hash=True, init=False, kw_only=True, slots=False) -class Unique(typing.SupportsInt): - """A base for an entity that has an integer ID of some sort. - - Casting an object of this type to an `int` will produce the - integer ID of the object. - """ - - id: snowflake.Snowflake = attr.ib( - converter=snowflake.Snowflake, eq=True, hash=True, repr=True, factory=snowflake.Snowflake, - ) - """The ID of this entity.""" - - @property - def created_at(self) -> datetime.datetime: - """When the object was created.""" - return self.id.created_at - - @typing.final - def __int__(self) -> int: - return int(self.id) - - -UniqueObject = typing.Union[Unique, snowflake.Snowflake, int, str] -"""Type hint representing a unique object entity.""" diff --git a/hikari/models/channels.py b/hikari/models/channels.py index 8f6bd4aafd..e80c985b54 100644 --- a/hikari/models/channels.py +++ b/hikari/models/channels.py @@ -41,15 +41,15 @@ import attr -from hikari.models import bases from hikari.models import permissions from hikari.models import users from hikari.utilities import cdn from hikari.utilities import files +from hikari.utilities import snowflake if typing.TYPE_CHECKING: import datetime - from hikari.utilities import snowflake + from hikari.api import rest @enum.unique @@ -95,7 +95,7 @@ def __str__(self) -> str: @attr.s(eq=True, hash=True, init=True, kw_only=True, slots=True) -class PermissionOverwrite(bases.Unique): +class PermissionOverwrite(snowflake.Unique): """Represents permission overwrites for a channel or role in a channel. You may sometimes need to make instances of this object to add/edit @@ -121,6 +121,11 @@ class PermissionOverwrite(bases.Unique): ``` """ + id: snowflake.Snowflake = attr.ib( + converter=snowflake.Snowflake, eq=True, hash=True, repr=True, factory=snowflake.Snowflake, + ) + """The ID of this entity.""" + type: PermissionOverwriteType = attr.ib(converter=PermissionOverwriteType, eq=True, hash=True, repr=True) """The type of entity this overwrite targets.""" @@ -142,13 +147,21 @@ def unset(self) -> permissions.Permission: @attr.s(eq=True, hash=True, init=False, kw_only=True, slots=True) -class PartialChannel(bases.Entity, bases.Unique): +class PartialChannel(snowflake.Unique): """Channel representation for cases where further detail is not provided. This is commonly received in REST API responses where full information is not available from Discord. """ + id: snowflake.Snowflake = attr.ib( + converter=snowflake.Snowflake, eq=True, hash=True, repr=True, factory=snowflake.Snowflake, + ) + """The ID of this entity.""" + + app: rest.IRESTApp = attr.ib(default=None, repr=False, eq=False, hash=False) + """The client application that models may use for procedures.""" + name: typing.Optional[str] = attr.ib(eq=False, hash=False, repr=True) """The channel's name. This will be missing for DM channels.""" diff --git a/hikari/models/embeds.py b/hikari/models/embeds.py index f99761c87d..48f26d09da 100644 --- a/hikari/models/embeds.py +++ b/hikari/models/embeds.py @@ -38,7 +38,6 @@ from hikari.models import colors from hikari.utilities import files - if typing.TYPE_CHECKING: import concurrent.futures diff --git a/hikari/models/emojis.py b/hikari/models/emojis.py index 1a701f137f..52d5376192 100644 --- a/hikari/models/emojis.py +++ b/hikari/models/emojis.py @@ -27,13 +27,13 @@ import attr -from hikari.models import bases from hikari.utilities import cdn from hikari.utilities import files +from hikari.utilities import snowflake if typing.TYPE_CHECKING: + from hikari.api import rest from hikari.models import users - from hikari.utilities import snowflake _TWEMOJI_PNG_BASE_URL: typing.Final[str] = "https://github.com/twitter/twemoji/raw/master/assets/72x72/" @@ -189,7 +189,7 @@ def from_unicode_escape(cls, escape: str) -> UnicodeEmoji: @attr.s(eq=True, hash=True, init=False, kw_only=True, slots=True) -class CustomEmoji(bases.Entity, bases.Unique, Emoji): +class CustomEmoji(snowflake.Unique, Emoji): """Represents a custom emoji. This is a custom emoji that is from a guild you might not be part of. @@ -213,6 +213,14 @@ class CustomEmoji(bases.Entity, bases.Unique, Emoji): https://github.com/discord/discord-api-docs/issues/1614 """ + app: rest.IRESTApp = attr.ib(default=None, repr=False, eq=False, hash=False, init=True) + """The client application that models may use for procedures.""" + + id: snowflake.Snowflake = attr.ib( + converter=snowflake.Snowflake, eq=True, hash=True, repr=True, factory=snowflake.Snowflake, + ) + """The ID of this entity.""" + name: typing.Optional[str] = attr.ib(eq=False, hash=False, repr=True) """The name of the emoji.""" diff --git a/hikari/models/guilds.py b/hikari/models/guilds.py index a8facf0921..6aec8a0dc4 100644 --- a/hikari/models/guilds.py +++ b/hikari/models/guilds.py @@ -47,20 +47,20 @@ import attr -from hikari.models import bases from hikari.models import users from hikari.utilities import cdn from hikari.utilities import files +from hikari.utilities import snowflake if typing.TYPE_CHECKING: import datetime + from hikari.api import rest from hikari.models import channels as channels_ from hikari.models import colors from hikari.models import emojis as emojis_ from hikari.models import permissions as permissions_ from hikari.models import presences - from hikari.utilities import snowflake from hikari.utilities import undefined @@ -212,9 +212,12 @@ class GuildVerificationLevel(int, enum.Enum): @attr.s(eq=True, hash=False, init=False, kw_only=True, slots=True) -class GuildWidget(bases.Entity): +class GuildWidget: """Represents a guild embed.""" + app: rest.IRESTApp = attr.ib(default=None, repr=False, eq=False, hash=False) + """The client application that models may use for procedures.""" + channel_id: typing.Optional[snowflake.Snowflake] = attr.ib(repr=True) """The ID of the channel the invite for this embed targets, if enabled.""" @@ -223,9 +226,12 @@ class GuildWidget(bases.Entity): @attr.s(eq=True, hash=True, init=False, kw_only=True, slots=True) -class Member(bases.Entity): +class Member: """Used to represent a guild bound member.""" + app: rest.IRESTApp = attr.ib(default=None, repr=False, eq=False, hash=False) + """The client application that models may use for procedures.""" + # TODO: make Member delegate to user and implement a common base class # this allows members and users to be used interchangeably. user: typing.Union[undefined.UndefinedType, users.User] = attr.ib(eq=True, hash=True, repr=True) @@ -272,9 +278,17 @@ class Member(bases.Entity): @attr.s(eq=True, hash=True, init=False, kw_only=True, slots=True) -class PartialRole(bases.Entity, bases.Unique): +class PartialRole(snowflake.Unique): """Represents a partial guild bound Role object.""" + id: snowflake.Snowflake = attr.ib( + converter=snowflake.Snowflake, eq=True, hash=True, repr=True, factory=snowflake.Snowflake, + ) + """The ID of this entity.""" + + app: rest.IRESTApp = attr.ib(default=None, repr=False, eq=False, hash=False) + """The client application that models may use for procedures.""" + name: str = attr.ib(eq=False, hash=False, repr=True) """The role's name.""" @@ -335,9 +349,14 @@ class IntegrationAccount: @attr.s(eq=True, hash=True, init=False, kw_only=True, slots=True) -class PartialIntegration(bases.Unique): +class PartialIntegration(snowflake.Unique): """A partial representation of an integration, found in audit logs.""" + id: snowflake.Snowflake = attr.ib( + converter=snowflake.Snowflake, eq=True, hash=True, repr=True, factory=snowflake.Snowflake, + ) + """The ID of this entity.""" + name: str = attr.ib(eq=False, hash=False, repr=True) """The name of this integration.""" @@ -396,13 +415,18 @@ class GuildMemberBan: @attr.s(eq=True, hash=True, init=False, kw_only=True, slots=True) @typing.final -class UnavailableGuild(bases.Entity, bases.Unique): +class UnavailableGuild(snowflake.Unique): """An unavailable guild object, received during gateway events such as READY. An unavailable guild cannot be interacted with, and most information may be outdated if that is the case. """ + id: snowflake.Snowflake = attr.ib( + converter=snowflake.Snowflake, eq=True, hash=True, repr=True, factory=snowflake.Snowflake, + ) + """The ID of this entity.""" + # Ignore docstring not starting in an imperative mood @property def is_unavailable(self) -> bool: # noqa: D401 @@ -414,9 +438,17 @@ def is_unavailable(self) -> bool: # noqa: D401 @attr.s(eq=True, hash=True, init=False, kw_only=True, slots=True) -class PartialGuild(bases.Entity, bases.Unique): +class PartialGuild(snowflake.Unique): """Base object for any partial guild objects.""" + id: snowflake.Snowflake = attr.ib( + converter=snowflake.Snowflake, eq=True, hash=True, repr=True, factory=snowflake.Snowflake, + ) + """The ID of this entity.""" + + app: rest.IRESTApp = attr.ib(default=None, repr=False, eq=False, hash=False) + """The client application that models may use for procedures.""" + name: str = attr.ib(eq=False, hash=False, repr=True) """The name of the guild.""" diff --git a/hikari/models/invites.py b/hikari/models/invites.py index 87f58c23ed..a0349eb2d5 100644 --- a/hikari/models/invites.py +++ b/hikari/models/invites.py @@ -26,17 +26,17 @@ import attr -from hikari.models import bases from hikari.models import guilds from hikari.utilities import cdn from hikari.utilities import files +from hikari.utilities import snowflake if typing.TYPE_CHECKING: import datetime + from hikari.api import rest from hikari.models import channels from hikari.models import users - from hikari.utilities import snowflake @enum.unique @@ -49,9 +49,12 @@ class TargetUserType(int, enum.Enum): @attr.s(eq=True, hash=True, init=False, kw_only=True, slots=True) -class VanityURL(bases.Entity): +class VanityURL: """A special case invite object, that represents a guild's vanity url.""" + app: rest.IRESTApp = attr.ib(default=None, repr=False, eq=False, hash=False) + """The client application that models may use for procedures.""" + code: str = attr.ib(eq=True, hash=True, repr=True) """The code for this invite.""" @@ -156,9 +159,12 @@ def format_banner(self, *, format_: str = "png", size: int = 4096) -> typing.Opt @attr.s(eq=True, hash=True, init=False, kw_only=True, slots=True) -class Invite(bases.Entity): +class Invite: """Represents an invite that's used to add users to a guild or group dm.""" + app: rest.IRESTApp = attr.ib(default=None, repr=False, eq=False, hash=False) + """The client application that models may use for procedures.""" + code: str = attr.ib(eq=True, hash=True, repr=True) """The code for this invite.""" diff --git a/hikari/models/messages.py b/hikari/models/messages.py index 42fd8a4971..ad5c848f85 100644 --- a/hikari/models/messages.py +++ b/hikari/models/messages.py @@ -35,20 +35,20 @@ import attr -from hikari.models import bases from hikari.utilities import files as files_ +from hikari.utilities import snowflake from hikari.utilities import undefined if typing.TYPE_CHECKING: import datetime + from hikari.api import rest from hikari.models import applications from hikari.models import channels from hikari.models import embeds as embeds_ from hikari.models import emojis as emojis_ from hikari.models import guilds from hikari.models import users - from hikari.utilities import snowflake @enum.unique @@ -142,7 +142,7 @@ class MessageActivityType(int, enum.Enum): @attr.s(eq=True, hash=False, init=False, kw_only=True, slots=True) -class Attachment(bases.Unique, files_.WebResource): +class Attachment(snowflake.Unique, files_.WebResource): """Represents a file attached to a message. You can use this object in the same way as a @@ -150,6 +150,11 @@ class Attachment(bases.Unique, files_.WebResource): message, etc. """ + id: snowflake.Snowflake = attr.ib( + converter=snowflake.Snowflake, eq=True, hash=True, repr=True, factory=snowflake.Snowflake, + ) + """The ID of this entity.""" + url: str = attr.ib(repr=True) """The source URL of file.""" @@ -195,13 +200,16 @@ class MessageActivity: @attr.s(eq=True, hash=False, init=False, kw_only=True, slots=True) -class MessageCrosspost(bases.Entity): +class MessageCrosspost: """Represents information about a cross-posted message. This is a message that is sent in one channel/guild and may be "published" to another. """ + app: rest.IRESTApp = attr.ib(default=None, repr=False, eq=False, hash=False) + """The client application that models may use for procedures.""" + id: typing.Optional[snowflake.Snowflake] = attr.ib(repr=True) """The ID of the message. @@ -225,9 +233,17 @@ class MessageCrosspost(bases.Entity): @attr.s(eq=True, hash=True, init=False, kw_only=True, slots=True) -class Message(bases.Entity, bases.Unique): +class Message(snowflake.Unique): """Represents a message.""" + app: rest.IRESTApp = attr.ib(default=None, repr=False, eq=False, hash=False) + """The client application that models may use for procedures.""" + + id: snowflake.Snowflake = attr.ib( + converter=snowflake.Snowflake, eq=True, hash=True, repr=True, factory=snowflake.Snowflake, + ) + """The ID of this entity.""" + channel_id: snowflake.Snowflake = attr.ib(eq=False, hash=False, repr=True) """The ID of the channel that the message was sent in.""" @@ -318,7 +334,7 @@ async def fetch_channel(self) -> channels.PartialChannel: hikari.errors.NotFound If the channel this message was created in does not exist. """ - return await self._app.rest.fetch_channel(self.channel_id) + return await self.app.rest.fetch_channel(self.channel_id) async def edit( # pylint:disable=line-too-long self, @@ -383,7 +399,7 @@ async def edit( # pylint:disable=line-too-long If more than 100 unique objects/entities are passed for `role_mentions` or `user_mentions`. """ - return await self._app.rest.edit_message( + return await self.app.rest.edit_message( message=self.id, channel=self.channel_id, text=text, @@ -464,7 +480,7 @@ async def reply( # pylint:disable=line-too-long If more than 100 unique objects/entities are passed for `role_mentions` or `user_mentions`. """ - return await self._app.rest.create_message( + return await self.app.rest.create_message( channel=self.channel_id, text=text, nonce=nonce, @@ -487,7 +503,7 @@ async def delete(self) -> None: hikari.errors.Forbidden If you lack the permissions to delete the message. """ - await self._app.rest.delete_message(self.channel_id, self.id) + await self.app.rest.delete_message(self.channel_id, self.id) async def add_reaction(self, emoji: typing.Union[str, emojis_.Emoji]) -> None: r"""Add a reaction to this message. @@ -527,7 +543,7 @@ async def add_reaction(self, emoji: typing.Union[str, emojis_.Emoji]) -> None: guild you are not part of if no one else has previously reacted with the same emoji. """ - await self._app.rest.add_reaction(channel=self.channel_id, message=self.id, emoji=emoji) + await self.app.rest.add_reaction(channel=self.channel_id, message=self.id, emoji=emoji) async def remove_reaction( self, @@ -576,9 +592,9 @@ async def remove_reaction( found. """ if user is undefined.UNDEFINED: - await self._app.rest.delete_my_reaction(channel=self.channel_id, message=self.id, emoji=emoji) + await self.app.rest.delete_my_reaction(channel=self.channel_id, message=self.id, emoji=emoji) else: - await self._app.rest.delete_reaction(channel=self.channel_id, message=self.id, emoji=emoji, user=user) + await self.app.rest.delete_reaction(channel=self.channel_id, message=self.id, emoji=emoji, user=user) async def remove_all_reactions( self, emoji: typing.Union[str, emojis_.Emoji, undefined.UndefinedType] = undefined.UNDEFINED @@ -614,6 +630,6 @@ async def remove_all_reactions( due to it being outside of the range of a 64 bit integer. """ if emoji is undefined.UNDEFINED: - await self._app.rest.delete_all_reactions(channel=self.channel_id, message=self.id) + await self.app.rest.delete_all_reactions(channel=self.channel_id, message=self.id) else: - await self._app.rest.delete_all_reactions_for_emoji(channel=self.channel_id, message=self.id, emoji=emoji) + await self.app.rest.delete_all_reactions_for_emoji(channel=self.channel_id, message=self.id, emoji=emoji) diff --git a/hikari/models/presences.py b/hikari/models/presences.py index 7e17e2dfda..10e8e7cec3 100644 --- a/hikari/models/presences.py +++ b/hikari/models/presences.py @@ -38,14 +38,14 @@ import attr -from hikari.models import bases from hikari.models import users +from hikari.utilities import snowflake if typing.TYPE_CHECKING: import datetime + from hikari.api import rest from hikari.models import emojis as emojis_ - from hikari.utilities import snowflake @enum.unique @@ -265,9 +265,12 @@ class ClientStatus: @attr.s(eq=True, hash=True, init=False, kw_only=True, slots=True) -class MemberPresence(bases.Entity): +class MemberPresence: """Used to represent a guild member's presence.""" + app: rest.IRESTApp = attr.ib(default=None, repr=False, eq=False, hash=False) + """The client application that models may use for procedures.""" + user: users.PartialUser = attr.ib(eq=True, hash=True, repr=True) """The object of the user who this presence is for. diff --git a/hikari/models/users.py b/hikari/models/users.py index 9749198f4c..d9c825d412 100644 --- a/hikari/models/users.py +++ b/hikari/models/users.py @@ -26,11 +26,14 @@ import attr -from hikari.models import bases from hikari.utilities import cdn from hikari.utilities import files +from hikari.utilities import snowflake from hikari.utilities import undefined +if typing.TYPE_CHECKING: + from hikari.api import rest + @enum.unique @typing.final @@ -96,13 +99,21 @@ class PremiumType(int, enum.Enum): @attr.s(eq=True, hash=True, init=False, kw_only=True, slots=True) -class PartialUser(bases.Entity, bases.Unique): +class PartialUser(snowflake.Unique): """Represents partial information about a user. This is pretty much the same as a normal user, but information may not be present. """ + id: snowflake.Snowflake = attr.ib( + converter=snowflake.Snowflake, eq=True, hash=True, repr=True, factory=snowflake.Snowflake, + ) + """The ID of this entity.""" + + app: rest.IRESTApp = attr.ib(default=None, repr=False, eq=False, hash=False) + """The client application that models may use for procedures.""" + discriminator: typing.Union[str, undefined.UndefinedType] = attr.ib(eq=False, hash=False, repr=True) """This user's discriminator.""" @@ -161,7 +172,7 @@ async def fetch_self(self) -> User: hikari.errors.NotFound If the user is not found. """ - return await self._app.rest.fetch_user(user=self.id) + return await self.app.rest.fetch_user(user=self.id) @property def avatar(self) -> typing.Optional[files.URL]: @@ -263,4 +274,4 @@ async def fetch_self(self) -> OwnUser: hikari.models.users.User The requested user object. """ - return await self._app.rest.fetch_my_user() + return await self.app.rest.fetch_my_user() diff --git a/hikari/models/voices.py b/hikari/models/voices.py index 0c7c69a801..7443497d69 100644 --- a/hikari/models/voices.py +++ b/hikari/models/voices.py @@ -25,17 +25,19 @@ import attr -from hikari.models import bases - if typing.TYPE_CHECKING: + from hikari.api import rest from hikari.models import guilds from hikari.utilities import snowflake @attr.s(eq=True, hash=True, init=False, kw_only=True, slots=True) -class VoiceState(bases.Entity): +class VoiceState: """Represents a user's voice connection status.""" + app: rest.IRESTApp = attr.ib(default=None, repr=False, eq=False, hash=False) + """The client application that models may use for procedures.""" + guild_id: typing.Optional[snowflake.Snowflake] = attr.ib(eq=False, hash=False, repr=True) """The ID of the guild this voice state is in, if applicable.""" diff --git a/hikari/models/webhooks.py b/hikari/models/webhooks.py index f00f0c483a..cf8060e328 100644 --- a/hikari/models/webhooks.py +++ b/hikari/models/webhooks.py @@ -26,13 +26,13 @@ import attr -from hikari.models import bases from hikari.utilities import cdn from hikari.utilities import files as files_ from hikari.utilities import snowflake from hikari.utilities import undefined if typing.TYPE_CHECKING: + from hikari.api import rest from hikari.models import channels as channels_ from hikari.models import embeds as embeds_ from hikari.models import guilds as guilds_ @@ -53,7 +53,7 @@ class WebhookType(int, enum.Enum): @attr.s(eq=True, hash=True, init=False, kw_only=True, slots=True) -class Webhook(bases.Entity, bases.Unique): +class Webhook(snowflake.Unique): """Represents a webhook object on Discord. This is an endpoint that can have messages sent to it using standard @@ -61,6 +61,14 @@ class Webhook(bases.Entity, bases.Unique): send informational messages to specific channels. """ + app: rest.IRESTApp = attr.ib(default=None, repr=False, eq=False, hash=False) + """The client application that models may use for procedures.""" + + id: snowflake.Snowflake = attr.ib( + converter=snowflake.Snowflake, eq=True, hash=True, repr=True, factory=snowflake.Snowflake, + ) + """The ID of this entity.""" + type: WebhookType = attr.ib(eq=False, hash=False, repr=True) """The type of the webhook.""" @@ -165,7 +173,7 @@ async def execute( if not self.token: raise ValueError("Cannot send a message using a webhook where we don't know it's token.") - return await self._app.rest.execute_webhook( + return await self.app.rest.execute_webhook( webhook=self.id, token=self.token, text=text, @@ -210,7 +218,7 @@ async def delete(self, *, use_token: typing.Union[undefined.UndefinedType, bool] else: token = undefined.UNDEFINED - await self._app.rest.delete_webhook(self.id, token=token) + await self.app.rest.delete_webhook(self.id, token=token) async def edit( self, @@ -218,7 +226,7 @@ async def edit( name: typing.Union[undefined.UndefinedType, str] = undefined.UNDEFINED, avatar: typing.Union[undefined.UndefinedType, None, files_.Resource] = undefined.UNDEFINED, channel: typing.Union[ - undefined.UndefinedType, bases.UniqueObject, channels_.GuildChannel + undefined.UndefinedType, snowflake.UniqueObject, channels_.GuildChannel ] = undefined.UNDEFINED, reason: typing.Union[undefined.UndefinedType, str] = undefined.UNDEFINED, use_token: typing.Union[undefined.UndefinedType, bool] = undefined.UNDEFINED, @@ -232,7 +240,7 @@ async def edit( avatar : hikari.utilities.files.Resource or None or hikari.utilities.undefined.UndefinedType If specified, the new avatar image. If `None`, then it is removed. If not specified, nothing is changed. - channel : hikari.models.channels.GuildChannel or hikari.models.bases.UniqueObject or hikari.utilities.undefined.UndefinedType + channel : hikari.models.channels.GuildChannel or hikari.models.snowflake.UniqueObject or hikari.utilities.undefined.UndefinedType If specified, the object or ID of the new channel the given webhook should be moved to. reason : str or hikari.utilities.undefined.UndefinedType @@ -275,7 +283,7 @@ async def edit( else: token = undefined.UNDEFINED - return await self._app.rest.edit_webhook( + return await self.app.rest.edit_webhook( self.id, token=token, name=name, avatar=avatar, channel=channel, reason=reason, ) @@ -294,7 +302,7 @@ async def fetch_channel(self) -> channels_.PartialChannel: hikari.errors.NotFound If the channel this message was created in does not exist. """ - return await self._app.rest.fetch_channel(self.channel_id) + return await self.app.rest.fetch_channel(self.channel_id) async def fetch_self( self, *, use_token: typing.Union[undefined.UndefinedType, bool] = undefined.UNDEFINED @@ -339,7 +347,7 @@ async def fetch_self( else: token = undefined.UNDEFINED - return await self._app.rest.fetch_webhook(self.id, token=token) + return await self.app.rest.fetch_webhook(self.id, token=token) @property def avatar(self) -> files_.URL: diff --git a/hikari/net/rest.py b/hikari/net/rest.py index 892d996d4d..04c920ab89 100644 --- a/hikari/net/rest.py +++ b/hikari/net/rest.py @@ -54,7 +54,6 @@ from hikari.models import applications from hikari.models import audit_logs - from hikari.models import bases from hikari.models import channels from hikari.models import colors from hikari.models import gateway @@ -335,10 +334,10 @@ async def _parse_ratelimits(self, compiled_route: routes.CompiledRoute, response def _generate_allowed_mentions( mentions_everyone: typing.Union[undefined.UndefinedType, bool], user_mentions: typing.Union[ - undefined.UndefinedType, typing.Collection[typing.Union[bases.UniqueObject, users.User]], bool + undefined.UndefinedType, typing.Collection[typing.Union[snowflake.UniqueObject, users.User]], bool ], role_mentions: typing.Union[ - undefined.UndefinedType, typing.Collection[typing.Union[bases.UniqueObject, guilds.Role]], bool + undefined.UndefinedType, typing.Collection[typing.Union[snowflake.UniqueObject, guilds.Role]], bool ], ) -> typing.Union[undefined.UndefinedType, data_binding.JSONObject]: parsed_mentions = [] @@ -375,7 +374,7 @@ async def close(self) -> None: self.buckets.close() async def fetch_channel( - self, channel: typing.Union[channels.PartialChannel, bases.UniqueObject], /, + self, channel: typing.Union[channels.PartialChannel, snowflake.UniqueObject], /, ) -> channels.PartialChannel: """Fetch a channel. @@ -411,7 +410,7 @@ async def fetch_channel( async def edit_channel( self, - channel: typing.Union[channels.PartialChannel, bases.UniqueObject], + channel: typing.Union[channels.PartialChannel, snowflake.UniqueObject], /, *, name: typing.Union[undefined.UndefinedType, str] = undefined.UNDEFINED, @@ -425,7 +424,7 @@ async def edit_channel( undefined.UndefinedType, typing.Sequence[channels.PermissionOverwrite] ] = undefined.UNDEFINED, parent_category: typing.Union[ - undefined.UndefinedType, channels.GuildCategory, bases.UniqueObject + undefined.UndefinedType, channels.GuildCategory, snowflake.UniqueObject ] = undefined.UNDEFINED, reason: typing.Union[undefined.UndefinedType, str] = undefined.UNDEFINED, ) -> channels.PartialChannel: @@ -496,7 +495,7 @@ async def edit_channel( response = typing.cast(data_binding.JSONObject, raw_response) return self._app.entity_factory.deserialize_channel(response) - async def delete_channel(self, channel: typing.Union[channels.PartialChannel, bases.UniqueObject], /) -> None: + async def delete_channel(self, channel: typing.Union[channels.PartialChannel, snowflake.UniqueObject], /) -> None: """Delete a channel in a guild, or close a DM. Parameters @@ -526,7 +525,7 @@ async def delete_channel(self, channel: typing.Union[channels.PartialChannel, ba @typing.overload async def edit_permission_overwrites( self, - channel: typing.Union[channels.GuildChannel, bases.UniqueObject], + channel: typing.Union[channels.GuildChannel, snowflake.UniqueObject], target: typing.Union[channels.PermissionOverwrite, users.User, guilds.Role], *, allow: typing.Union[undefined.UndefinedType, permissions_.Permission] = undefined.UNDEFINED, @@ -538,7 +537,7 @@ async def edit_permission_overwrites( @typing.overload async def edit_permission_overwrites( self, - channel: typing.Union[channels.GuildChannel, bases.UniqueObject], + channel: typing.Union[channels.GuildChannel, snowflake.UniqueObject], target: typing.Union[int, str, snowflake.Snowflake], *, target_type: typing.Union[channels.PermissionOverwriteType, str], @@ -550,8 +549,8 @@ async def edit_permission_overwrites( async def edit_permission_overwrites( self, - channel: typing.Union[channels.GuildChannel, bases.UniqueObject], - target: typing.Union[bases.UniqueObject, users.User, guilds.Role, channels.PermissionOverwrite], + channel: typing.Union[channels.GuildChannel, snowflake.UniqueObject], + target: typing.Union[snowflake.UniqueObject, users.User, guilds.Role, channels.PermissionOverwrite], *, target_type: typing.Union[undefined.UndefinedType, channels.PermissionOverwriteType, str] = undefined.UNDEFINED, allow: typing.Union[undefined.UndefinedType, permissions_.Permission] = undefined.UNDEFINED, @@ -617,8 +616,8 @@ async def edit_permission_overwrites( async def delete_permission_overwrite( self, - channel: typing.Union[channels.GuildChannel, bases.UniqueObject], - target: typing.Union[channels.PermissionOverwrite, guilds.Role, users.User, bases.UniqueObject], + channel: typing.Union[channels.GuildChannel, snowflake.UniqueObject], + target: typing.Union[channels.PermissionOverwrite, guilds.Role, users.User, snowflake.UniqueObject], ) -> None: """Delete a custom permission for an entity in a given guild channel. @@ -645,7 +644,7 @@ async def delete_permission_overwrite( await self._request(route) async def fetch_channel_invites( - self, channel: typing.Union[channels.GuildChannel, bases.UniqueObject], / + self, channel: typing.Union[channels.GuildChannel, snowflake.UniqueObject], / ) -> typing.Sequence[invites.InviteWithMetadata]: """Fetch all invites pointing to the given guild channel. @@ -678,14 +677,14 @@ async def fetch_channel_invites( async def create_invite( self, - channel: typing.Union[channels.GuildChannel, bases.UniqueObject], + channel: typing.Union[channels.GuildChannel, snowflake.UniqueObject], /, *, max_age: typing.Union[undefined.UndefinedType, int, float, datetime.timedelta] = undefined.UNDEFINED, max_uses: typing.Union[undefined.UndefinedType, int] = undefined.UNDEFINED, temporary: typing.Union[undefined.UndefinedType, bool] = undefined.UNDEFINED, unique: typing.Union[undefined.UndefinedType, bool] = undefined.UNDEFINED, - target_user: typing.Union[undefined.UndefinedType, users.User, bases.UniqueObject] = undefined.UNDEFINED, + target_user: typing.Union[undefined.UndefinedType, users.User, snowflake.UniqueObject] = undefined.UNDEFINED, target_user_type: typing.Union[undefined.UndefinedType, invites.TargetUserType] = undefined.UNDEFINED, reason: typing.Union[undefined.UndefinedType, str] = undefined.UNDEFINED, ) -> invites.InviteWithMetadata: @@ -744,7 +743,7 @@ async def create_invite( return self._app.entity_factory.deserialize_invite_with_metadata(response) def trigger_typing( - self, channel: typing.Union[channels.TextChannel, bases.UniqueObject], / + self, channel: typing.Union[channels.TextChannel, snowflake.UniqueObject], / ) -> rest_utils.TypingIndicator: """Trigger typing in a text channel. @@ -779,7 +778,7 @@ def trigger_typing( return rest_utils.TypingIndicator(channel, self._request) async def fetch_pins( - self, channel: typing.Union[channels.TextChannel, bases.UniqueObject], / + self, channel: typing.Union[channels.TextChannel, snowflake.UniqueObject], / ) -> typing.Sequence[messages_.Message]: """Fetch the pinned messages in this text channel. @@ -813,8 +812,8 @@ async def fetch_pins( async def pin_message( self, - channel: typing.Union[channels.TextChannel, bases.UniqueObject], - message: typing.Union[messages_.Message, bases.UniqueObject], + channel: typing.Union[channels.TextChannel, snowflake.UniqueObject], + message: typing.Union[messages_.Message, snowflake.UniqueObject], ) -> None: """Pin an existing message in the given text channel. @@ -844,8 +843,8 @@ async def pin_message( async def unpin_message( self, - channel: typing.Union[channels.TextChannel, bases.UniqueObject], - message: typing.Union[messages_.Message, bases.UniqueObject], + channel: typing.Union[channels.TextChannel, snowflake.UniqueObject], + message: typing.Union[messages_.Message, snowflake.UniqueObject], ) -> None: """Unpin a given message from a given text channel. @@ -875,48 +874,48 @@ async def unpin_message( @typing.overload def fetch_messages( - self, channel: typing.Union[channels.TextChannel, bases.UniqueObject], / + self, channel: typing.Union[channels.TextChannel, snowflake.UniqueObject], / ) -> iterators.LazyIterator[messages_.Message]: """Fetch messages, newest first, sent in the given channel.""" @typing.overload def fetch_messages( self, - channel: typing.Union[channels.TextChannel, bases.UniqueObject], + channel: typing.Union[channels.TextChannel, snowflake.UniqueObject], /, *, - before: typing.Union[datetime.datetime, bases.UniqueObject], + before: typing.Union[datetime.datetime, snowflake.UniqueObject], ) -> iterators.LazyIterator[messages_.Message]: """Fetch messages, newest first, sent before a timestamp in the channel.""" @typing.overload def fetch_messages( self, - channel: typing.Union[channels.TextChannel, bases.UniqueObject], + channel: typing.Union[channels.TextChannel, snowflake.UniqueObject], /, *, - around: typing.Union[datetime.datetime, bases.UniqueObject], + around: typing.Union[datetime.datetime, snowflake.UniqueObject], ) -> iterators.LazyIterator[messages_.Message]: """Fetch messages sent around a given time in the channel.""" @typing.overload def fetch_messages( self, - channel: typing.Union[channels.TextChannel, bases.UniqueObject], + channel: typing.Union[channels.TextChannel, snowflake.UniqueObject], /, *, - after: typing.Union[datetime.datetime, bases.UniqueObject], + after: typing.Union[datetime.datetime, snowflake.UniqueObject], ) -> iterators.LazyIterator[messages_.Message]: """Fetch messages, oldest first, sent after a timestamp in the channel.""" def fetch_messages( self, - channel: typing.Union[channels.TextChannel, bases.UniqueObject], + channel: typing.Union[channels.TextChannel, snowflake.UniqueObject], /, *, - before: typing.Union[undefined.UndefinedType, datetime.datetime, bases.UniqueObject] = undefined.UNDEFINED, - after: typing.Union[undefined.UndefinedType, datetime.datetime, bases.UniqueObject] = undefined.UNDEFINED, - around: typing.Union[undefined.UndefinedType, datetime.datetime, bases.UniqueObject] = undefined.UNDEFINED, + before: typing.Union[undefined.UndefinedType, datetime.datetime, snowflake.UniqueObject] = undefined.UNDEFINED, + after: typing.Union[undefined.UndefinedType, datetime.datetime, snowflake.UniqueObject] = undefined.UNDEFINED, + around: typing.Union[undefined.UndefinedType, datetime.datetime, snowflake.UniqueObject] = undefined.UNDEFINED, ) -> iterators.LazyIterator[messages_.Message]: """Browse the message history for a given text channel. @@ -979,8 +978,8 @@ def fetch_messages( async def fetch_message( self, - channel: typing.Union[channels.TextChannel, bases.UniqueObject], - message: typing.Union[messages_.Message, bases.UniqueObject], + channel: typing.Union[channels.TextChannel, snowflake.UniqueObject], + message: typing.Union[messages_.Message, snowflake.UniqueObject], ) -> messages_.Message: """Fetch a specific message in the given text channel. @@ -1018,7 +1017,7 @@ async def fetch_message( async def create_message( self, - channel: typing.Union[channels.TextChannel, bases.UniqueObject], + channel: typing.Union[channels.TextChannel, snowflake.UniqueObject], text: typing.Union[undefined.UndefinedType, typing.Any] = undefined.UNDEFINED, *, embed: typing.Union[undefined.UndefinedType, embeds_.Embed] = undefined.UNDEFINED, @@ -1028,8 +1027,8 @@ async def create_message( tts: typing.Union[undefined.UndefinedType, bool] = undefined.UNDEFINED, nonce: typing.Union[undefined.UndefinedType, str] = undefined.UNDEFINED, mentions_everyone: bool = True, - user_mentions: typing.Union[typing.Collection[typing.Union[users.User, bases.UniqueObject]], bool] = True, - role_mentions: typing.Union[typing.Collection[typing.Union[guilds.Role, bases.UniqueObject]], bool] = True, + user_mentions: typing.Union[typing.Collection[typing.Union[users.User, snowflake.UniqueObject]], bool] = True, + role_mentions: typing.Union[typing.Collection[typing.Union[guilds.Role, snowflake.UniqueObject]], bool] = True, ) -> messages_.Message: """Create a message in the given channel. @@ -1130,17 +1129,17 @@ async def create_message( async def edit_message( self, - channel: typing.Union[channels.TextChannel, bases.UniqueObject], - message: typing.Union[messages_.Message, bases.UniqueObject], + channel: typing.Union[channels.TextChannel, snowflake.UniqueObject], + message: typing.Union[messages_.Message, snowflake.UniqueObject], text: typing.Union[undefined.UndefinedType, None, typing.Any] = undefined.UNDEFINED, *, embed: typing.Union[undefined.UndefinedType, None, embeds_.Embed] = undefined.UNDEFINED, mentions_everyone: typing.Union[undefined.UndefinedType, bool] = undefined.UNDEFINED, user_mentions: typing.Union[ - undefined.UndefinedType, typing.Collection[typing.Union[users.User, bases.UniqueObject]], bool + undefined.UndefinedType, typing.Collection[typing.Union[users.User, snowflake.UniqueObject]], bool ] = undefined.UNDEFINED, role_mentions: typing.Union[ - undefined.UndefinedType, typing.Collection[typing.Union[bases.UniqueObject, guilds.Role]], bool + undefined.UndefinedType, typing.Collection[typing.Union[snowflake.UniqueObject, guilds.Role]], bool ] = undefined.UNDEFINED, flags: typing.Union[undefined.UndefinedType, messages_.MessageFlag] = undefined.UNDEFINED, ) -> messages_.Message: @@ -1208,8 +1207,8 @@ async def edit_message( async def delete_message( self, - channel: typing.Union[channels.TextChannel, bases.UniqueObject], - message: typing.Union[messages_.Message, bases.UniqueObject], + channel: typing.Union[channels.TextChannel, snowflake.UniqueObject], + message: typing.Union[messages_.Message, snowflake.UniqueObject], ) -> None: """Delete a given message in a given channel. @@ -1235,9 +1234,9 @@ async def delete_message( async def delete_messages( self, - channel: typing.Union[channels.GuildTextChannel, bases.UniqueObject], + channel: typing.Union[channels.GuildTextChannel, snowflake.UniqueObject], /, - *messages: typing.Union[messages_.Message, bases.UniqueObject], + *messages: typing.Union[messages_.Message, snowflake.UniqueObject], ) -> None: """Bulk-delete between 2 and 100 messages from the given guild channel. @@ -1270,8 +1269,8 @@ async def delete_messages( async def add_reaction( self, - channel: typing.Union[channels.TextChannel, bases.UniqueObject], - message: typing.Union[messages_.Message, bases.UniqueObject], + channel: typing.Union[channels.TextChannel, snowflake.UniqueObject], + message: typing.Union[messages_.Message, snowflake.UniqueObject], emoji: typing.Union[str, emojis.Emoji], ) -> None: """Add a reaction emoji to a message in a given channel. @@ -1305,8 +1304,8 @@ async def add_reaction( async def delete_my_reaction( self, - channel: typing.Union[channels.TextChannel, bases.UniqueObject], - message: typing.Union[messages_.Message, bases.UniqueObject], + channel: typing.Union[channels.TextChannel, snowflake.UniqueObject], + message: typing.Union[messages_.Message, snowflake.UniqueObject], emoji: typing.Union[str, emojis.Emoji], ) -> None: """Delete a reaction that your application user created. @@ -1338,8 +1337,8 @@ async def delete_my_reaction( async def delete_all_reactions_for_emoji( self, - channel: typing.Union[channels.TextChannel, bases.UniqueObject], - message: typing.Union[messages_.Message, bases.UniqueObject], + channel: typing.Union[channels.TextChannel, snowflake.UniqueObject], + message: typing.Union[messages_.Message, snowflake.UniqueObject], emoji: typing.Union[str, emojis.Emoji], ) -> None: route = routes.DELETE_REACTION_EMOJI.compile( @@ -1351,10 +1350,10 @@ async def delete_all_reactions_for_emoji( async def delete_reaction( self, - channel: typing.Union[channels.TextChannel, bases.UniqueObject], - message: typing.Union[messages_.Message, bases.UniqueObject], + channel: typing.Union[channels.TextChannel, snowflake.UniqueObject], + message: typing.Union[messages_.Message, snowflake.UniqueObject], emoji: typing.Union[str, emojis.Emoji], - user: typing.Union[users.User, bases.UniqueObject], + user: typing.Union[users.User, snowflake.UniqueObject], ) -> None: route = routes.DELETE_REACTION_USER.compile( emoji=emoji.url_name if isinstance(emoji, emojis.CustomEmoji) else str(emoji), @@ -1366,16 +1365,16 @@ async def delete_reaction( async def delete_all_reactions( self, - channel: typing.Union[channels.TextChannel, bases.UniqueObject], - message: typing.Union[messages_.Message, bases.UniqueObject], + channel: typing.Union[channels.TextChannel, snowflake.UniqueObject], + message: typing.Union[messages_.Message, snowflake.UniqueObject], ) -> None: route = routes.DELETE_ALL_REACTIONS.compile(channel=channel, message=message) await self._request(route) def fetch_reactions_for_emoji( self, - channel: typing.Union[channels.TextChannel, bases.UniqueObject], - message: typing.Union[messages_.Message, bases.UniqueObject], + channel: typing.Union[channels.TextChannel, snowflake.UniqueObject], + message: typing.Union[messages_.Message, snowflake.UniqueObject], emoji: typing.Union[str, emojis.Emoji], ) -> iterators.LazyIterator[users.User]: return iterators.ReactorIterator( @@ -1388,7 +1387,7 @@ def fetch_reactions_for_emoji( async def create_webhook( self, - channel: typing.Union[channels.TextChannel, bases.UniqueObject], + channel: typing.Union[channels.TextChannel, snowflake.UniqueObject], name: str, *, avatar: typing.Union[undefined.UndefinedType, files.Resource] = undefined.UNDEFINED, @@ -1407,7 +1406,7 @@ async def create_webhook( async def fetch_webhook( self, - webhook: typing.Union[webhooks.Webhook, bases.UniqueObject], + webhook: typing.Union[webhooks.Webhook, snowflake.UniqueObject], /, *, token: typing.Union[undefined.UndefinedType, str] = undefined.UNDEFINED, @@ -1421,7 +1420,7 @@ async def fetch_webhook( return self._app.entity_factory.deserialize_webhook(response) async def fetch_channel_webhooks( - self, channel: typing.Union[channels.TextChannel, bases.UniqueObject], / + self, channel: typing.Union[channels.TextChannel, snowflake.UniqueObject], / ) -> typing.Sequence[webhooks.Webhook]: route = routes.GET_CHANNEL_WEBHOOKS.compile(channel=channel) raw_response = await self._request(route) @@ -1429,7 +1428,7 @@ async def fetch_channel_webhooks( return data_binding.cast_json_array(response, self._app.entity_factory.deserialize_webhook) async def fetch_guild_webhooks( - self, guild: typing.Union[guilds.Guild, bases.UniqueObject], / + self, guild: typing.Union[guilds.Guild, snowflake.UniqueObject], / ) -> typing.Sequence[webhooks.Webhook]: route = routes.GET_GUILD_WEBHOOKS.compile(channel=guild) raw_response = await self._request(route) @@ -1438,13 +1437,15 @@ async def fetch_guild_webhooks( async def edit_webhook( self, - webhook: typing.Union[webhooks.Webhook, bases.UniqueObject], + webhook: typing.Union[webhooks.Webhook, snowflake.UniqueObject], /, *, token: typing.Union[undefined.UndefinedType, str] = undefined.UNDEFINED, name: typing.Union[undefined.UndefinedType, str] = undefined.UNDEFINED, avatar: typing.Union[None, undefined.UndefinedType, files.Resource] = undefined.UNDEFINED, - channel: typing.Union[undefined.UndefinedType, channels.TextChannel, bases.UniqueObject] = undefined.UNDEFINED, + channel: typing.Union[ + undefined.UndefinedType, channels.TextChannel, snowflake.UniqueObject + ] = undefined.UNDEFINED, reason: typing.Union[undefined.UndefinedType, str] = undefined.UNDEFINED, ) -> webhooks.Webhook: if token is undefined.UNDEFINED: @@ -1468,7 +1469,7 @@ async def edit_webhook( async def delete_webhook( self, - webhook: typing.Union[webhooks.Webhook, bases.UniqueObject], + webhook: typing.Union[webhooks.Webhook, snowflake.UniqueObject], /, *, token: typing.Union[undefined.UndefinedType, str] = undefined.UNDEFINED, @@ -1481,7 +1482,7 @@ async def delete_webhook( async def execute_webhook( self, - webhook: typing.Union[webhooks.Webhook, bases.UniqueObject], + webhook: typing.Union[webhooks.Webhook, snowflake.UniqueObject], text: typing.Union[undefined.UndefinedType, typing.Any] = undefined.UNDEFINED, *, token: typing.Union[undefined.UndefinedType, str] = undefined.UNDEFINED, @@ -1491,8 +1492,8 @@ async def execute_webhook( attachments: typing.Union[undefined.UndefinedType, typing.Sequence[files.Resource]] = undefined.UNDEFINED, tts: typing.Union[undefined.UndefinedType, bool] = undefined.UNDEFINED, mentions_everyone: bool = True, - user_mentions: typing.Union[typing.Collection[typing.Union[users.User, bases.UniqueObject]], bool] = True, - role_mentions: typing.Union[typing.Collection[typing.Union[bases.UniqueObject, guilds.Role]], bool] = True, + user_mentions: typing.Union[typing.Collection[typing.Union[users.User, snowflake.UniqueObject]], bool] = True, + role_mentions: typing.Union[typing.Collection[typing.Union[snowflake.UniqueObject, guilds.Role]], bool] = True, ) -> messages_.Message: if token is undefined.UNDEFINED: route = routes.POST_WEBHOOK.compile(webhook=webhook) @@ -1607,7 +1608,7 @@ def fetch_my_guilds( *, newest_first: bool = False, start_at: typing.Union[ - undefined.UndefinedType, guilds.PartialGuild, bases.UniqueObject, datetime.datetime + undefined.UndefinedType, guilds.PartialGuild, snowflake.UniqueObject, datetime.datetime ] = undefined.UNDEFINED, ) -> iterators.LazyIterator[applications.OwnGuild]: if start_at is undefined.UNDEFINED: @@ -1617,11 +1618,11 @@ def fetch_my_guilds( return iterators.OwnGuildIterator(self._app, self._request, newest_first, str(start_at)) - async def leave_guild(self, guild: typing.Union[guilds.Guild, bases.UniqueObject], /) -> None: + async def leave_guild(self, guild: typing.Union[guilds.Guild, snowflake.UniqueObject], /) -> None: route = routes.DELETE_MY_GUILD.compile(guild=guild) await self._request(route) - async def create_dm_channel(self, user: typing.Union[users.User, bases.UniqueObject], /) -> channels.DMChannel: + async def create_dm_channel(self, user: typing.Union[users.User, snowflake.UniqueObject], /) -> channels.DMChannel: route = routes.POST_MY_CHANNELS.compile() body = data_binding.JSONObjectBuilder() body.put_snowflake("recipient_id", user) @@ -1638,12 +1639,12 @@ async def fetch_application(self) -> applications.Application: async def add_user_to_guild( self, access_token: str, - guild: typing.Union[guilds.Guild, bases.UniqueObject], - user: typing.Union[users.User, bases.UniqueObject], + guild: typing.Union[guilds.Guild, snowflake.UniqueObject], + user: typing.Union[users.User, snowflake.UniqueObject], *, nick: typing.Union[undefined.UndefinedType, str] = undefined.UNDEFINED, roles: typing.Union[ - undefined.UndefinedType, typing.Collection[typing.Union[guilds.Role, bases.UniqueObject]] + undefined.UndefinedType, typing.Collection[typing.Union[guilds.Role, snowflake.UniqueObject]] ] = undefined.UNDEFINED, mute: typing.Union[undefined.UndefinedType, bool] = undefined.UNDEFINED, deaf: typing.Union[undefined.UndefinedType, bool] = undefined.UNDEFINED, @@ -1669,7 +1670,7 @@ async def fetch_voice_regions(self) -> typing.Sequence[voices.VoiceRegion]: response = typing.cast(data_binding.JSONArray, raw_response) return data_binding.cast_json_array(response, self._app.entity_factory.deserialize_voice_region) - async def fetch_user(self, user: typing.Union[users.User, bases.UniqueObject]) -> users.User: + async def fetch_user(self, user: typing.Union[users.User, snowflake.UniqueObject]) -> users.User: route = routes.GET_USER.compile(user=user) raw_response = await self._request(route) response = typing.cast(data_binding.JSONObject, raw_response) @@ -1677,11 +1678,11 @@ async def fetch_user(self, user: typing.Union[users.User, bases.UniqueObject]) - def fetch_audit_log( self, - guild: typing.Union[guilds.Guild, bases.UniqueObject], + guild: typing.Union[guilds.Guild, snowflake.UniqueObject], /, *, - before: typing.Union[undefined.UndefinedType, datetime.datetime, bases.UniqueObject] = undefined.UNDEFINED, - user: typing.Union[undefined.UndefinedType, users.User, bases.UniqueObject] = undefined.UNDEFINED, + before: typing.Union[undefined.UndefinedType, datetime.datetime, snowflake.UniqueObject] = undefined.UNDEFINED, + user: typing.Union[undefined.UndefinedType, users.User, snowflake.UniqueObject] = undefined.UNDEFINED, event_type: typing.Union[undefined.UndefinedType, audit_logs.AuditLogEventType] = undefined.UNDEFINED, ) -> iterators.LazyIterator[audit_logs.AuditLog]: guild = str(int(guild)) @@ -1700,9 +1701,9 @@ def fetch_audit_log( async def fetch_emoji( self, - guild: typing.Union[guilds.Guild, bases.UniqueObject], + guild: typing.Union[guilds.Guild, snowflake.UniqueObject], # This is an emoji ID, which is the URL-safe emoji name, not the snowflake alone. - emoji: typing.Union[emojis.CustomEmoji, bases.UniqueObject], + emoji: typing.Union[emojis.CustomEmoji, snowflake.UniqueObject], ) -> emojis.KnownCustomEmoji: route = routes.GET_GUILD_EMOJI.compile( guild=guild, emoji=emoji.id if isinstance(emoji, emojis.CustomEmoji) else emoji, @@ -1712,7 +1713,7 @@ async def fetch_emoji( return self._app.entity_factory.deserialize_known_custom_emoji(response) async def fetch_guild_emojis( - self, guild: typing.Union[guilds.Guild, bases.UniqueObject], / + self, guild: typing.Union[guilds.Guild, snowflake.UniqueObject], / ) -> typing.Set[emojis.KnownCustomEmoji]: route = routes.GET_GUILD_EMOJIS.compile(guild=guild) raw_response = await self._request(route) @@ -1721,12 +1722,12 @@ async def fetch_guild_emojis( async def create_emoji( self, - guild: typing.Union[guilds.Guild, bases.UniqueObject], + guild: typing.Union[guilds.Guild, snowflake.UniqueObject], name: str, image: files.Resource, *, roles: typing.Union[ - undefined.UndefinedType, typing.Collection[typing.Union[guilds.Role, bases.UniqueObject]] + undefined.UndefinedType, typing.Collection[typing.Union[guilds.Role, snowflake.UniqueObject]] ] = undefined.UNDEFINED, reason: typing.Union[undefined.UndefinedType, str] = undefined.UNDEFINED, ) -> emojis.KnownCustomEmoji: @@ -1745,13 +1746,13 @@ async def create_emoji( async def edit_emoji( self, - guild: typing.Union[guilds.Guild, bases.UniqueObject], + guild: typing.Union[guilds.Guild, snowflake.UniqueObject], # This is an emoji ID, which is the URL-safe emoji name, not the snowflake alone. - emoji: typing.Union[emojis.CustomEmoji, bases.UniqueObject], + emoji: typing.Union[emojis.CustomEmoji, snowflake.UniqueObject], *, name: typing.Union[undefined.UndefinedType, str] = undefined.UNDEFINED, roles: typing.Union[ - undefined.UndefinedType, typing.Collection[typing.Union[guilds.Role, bases.UniqueObject]] + undefined.UndefinedType, typing.Collection[typing.Union[guilds.Role, snowflake.UniqueObject]] ] = undefined.UNDEFINED, reason: typing.Union[undefined.UndefinedType, str] = undefined.UNDEFINED, ) -> emojis.KnownCustomEmoji: @@ -1768,9 +1769,9 @@ async def edit_emoji( async def delete_emoji( self, - guild: typing.Union[guilds.Guild, bases.UniqueObject], + guild: typing.Union[guilds.Guild, snowflake.UniqueObject], # This is an emoji ID, which is the URL-safe emoji name, not the snowflake alone. - emoji: typing.Union[emojis.CustomEmoji, bases.UniqueObject], + emoji: typing.Union[emojis.CustomEmoji, snowflake.UniqueObject], # Reason is not currently supported for some reason. See ) -> None: route = routes.DELETE_GUILD_EMOJI.compile( @@ -1781,14 +1782,14 @@ async def delete_emoji( def guild_builder(self, name: str, /) -> rest_utils.GuildBuilder: return rest_utils.GuildBuilder(app=self._app, name=name, request_call=self._request) - async def fetch_guild(self, guild: typing.Union[guilds.Guild, bases.UniqueObject], /) -> guilds.Guild: + async def fetch_guild(self, guild: typing.Union[guilds.Guild, snowflake.UniqueObject], /) -> guilds.Guild: route = routes.GET_GUILD.compile(guild=guild) raw_response = await self._request(route) response = typing.cast(data_binding.JSONObject, raw_response) return self._app.entity_factory.deserialize_guild(response) async def fetch_guild_preview( - self, guild: typing.Union[guilds.PartialGuild, bases.UniqueObject], / + self, guild: typing.Union[guilds.PartialGuild, snowflake.UniqueObject], / ) -> guilds.GuildPreview: route = routes.GET_GUILD_PREVIEW.compile(guild=guild) raw_response = await self._request(route) @@ -1797,7 +1798,7 @@ async def fetch_guild_preview( async def edit_guild( self, - guild: typing.Union[guilds.Guild, bases.UniqueObject], + guild: typing.Union[guilds.Guild, snowflake.UniqueObject], /, *, name: typing.Union[undefined.UndefinedType, str] = undefined.UNDEFINED, @@ -1810,11 +1811,11 @@ async def edit_guild( undefined.UndefinedType, guilds.GuildExplicitContentFilterLevel ] = undefined.UNDEFINED, afk_channel: typing.Union[ - undefined.UndefinedType, channels.GuildVoiceChannel, bases.UniqueObject + undefined.UndefinedType, channels.GuildVoiceChannel, snowflake.UniqueObject ] = undefined.UNDEFINED, afk_timeout: typing.Union[undefined.UndefinedType, date.TimeSpan] = undefined.UNDEFINED, icon: typing.Union[undefined.UndefinedType, None, files.Resource] = undefined.UNDEFINED, - owner: typing.Union[undefined.UndefinedType, users.User, bases.UniqueObject] = undefined.UNDEFINED, + owner: typing.Union[undefined.UndefinedType, users.User, snowflake.UniqueObject] = undefined.UNDEFINED, splash: typing.Union[undefined.UndefinedType, None, files.Resource] = undefined.UNDEFINED, banner: typing.Union[undefined.UndefinedType, None, files.Resource] = undefined.UNDEFINED, system_channel: typing.Union[undefined.UndefinedType, channels.GuildTextChannel] = undefined.UNDEFINED, @@ -1862,12 +1863,12 @@ async def edit_guild( response = typing.cast(data_binding.JSONObject, raw_response) return self._app.entity_factory.deserialize_guild(response) - async def delete_guild(self, guild: typing.Union[guilds.Guild, bases.UniqueObject]) -> None: + async def delete_guild(self, guild: typing.Union[guilds.Guild, snowflake.UniqueObject]) -> None: route = routes.DELETE_GUILD.compile(guild=guild) await self._request(route) async def fetch_guild_channels( - self, guild: typing.Union[guilds.Guild, bases.UniqueObject] + self, guild: typing.Union[guilds.Guild, snowflake.UniqueObject] ) -> typing.Sequence[channels.GuildChannel]: route = routes.GET_GUILD_CHANNELS.compile(guild=guild) raw_response = await self._request(route) @@ -1878,7 +1879,7 @@ async def fetch_guild_channels( async def create_guild_text_channel( self, - guild: typing.Union[guilds.Guild, bases.UniqueObject], + guild: typing.Union[guilds.Guild, snowflake.UniqueObject], name: str, *, position: typing.Union[int, undefined.UndefinedType] = undefined.UNDEFINED, @@ -1889,7 +1890,7 @@ async def create_guild_text_channel( typing.Sequence[channels.PermissionOverwrite], undefined.UndefinedType ] = undefined.UNDEFINED, category: typing.Union[ - channels.GuildCategory, bases.UniqueObject, undefined.UndefinedType + channels.GuildCategory, snowflake.UniqueObject, undefined.UndefinedType ] = undefined.UNDEFINED, reason: typing.Union[str, undefined.UndefinedType] = undefined.UNDEFINED, ) -> channels.GuildTextChannel: @@ -1909,7 +1910,7 @@ async def create_guild_text_channel( async def create_guild_news_channel( self, - guild: typing.Union[guilds.Guild, bases.UniqueObject], + guild: typing.Union[guilds.Guild, snowflake.UniqueObject], name: str, *, position: typing.Union[int, undefined.UndefinedType] = undefined.UNDEFINED, @@ -1920,7 +1921,7 @@ async def create_guild_news_channel( typing.Sequence[channels.PermissionOverwrite], undefined.UndefinedType ] = undefined.UNDEFINED, category: typing.Union[ - channels.GuildCategory, bases.UniqueObject, undefined.UndefinedType + channels.GuildCategory, snowflake.UniqueObject, undefined.UndefinedType ] = undefined.UNDEFINED, reason: typing.Union[str, undefined.UndefinedType] = undefined.UNDEFINED, ) -> channels.GuildNewsChannel: @@ -1940,7 +1941,7 @@ async def create_guild_news_channel( async def create_guild_voice_channel( self, - guild: typing.Union[guilds.Guild, bases.UniqueObject], + guild: typing.Union[guilds.Guild, snowflake.UniqueObject], name: str, *, position: typing.Union[int, undefined.UndefinedType] = undefined.UNDEFINED, @@ -1951,7 +1952,7 @@ async def create_guild_voice_channel( typing.Sequence[channels.PermissionOverwrite], undefined.UndefinedType ] = undefined.UNDEFINED, category: typing.Union[ - channels.GuildCategory, bases.UniqueObject, undefined.UndefinedType + channels.GuildCategory, snowflake.UniqueObject, undefined.UndefinedType ] = undefined.UNDEFINED, reason: typing.Union[str, undefined.UndefinedType] = undefined.UNDEFINED, ) -> channels.GuildVoiceChannel: @@ -1971,7 +1972,7 @@ async def create_guild_voice_channel( async def create_guild_category( self, - guild: typing.Union[guilds.Guild, bases.UniqueObject], + guild: typing.Union[guilds.Guild, snowflake.UniqueObject], name: str, *, position: typing.Union[int, undefined.UndefinedType] = undefined.UNDEFINED, @@ -1994,7 +1995,7 @@ async def create_guild_category( async def _create_guild_channel( self, - guild: typing.Union[guilds.Guild, bases.UniqueObject], + guild: typing.Union[guilds.Guild, snowflake.UniqueObject], name: str, type_: channels.ChannelType, *, @@ -2008,7 +2009,7 @@ async def _create_guild_channel( typing.Sequence[channels.PermissionOverwrite], undefined.UndefinedType ] = undefined.UNDEFINED, category: typing.Union[ - channels.GuildCategory, bases.UniqueObject, undefined.UndefinedType + channels.GuildCategory, snowflake.UniqueObject, undefined.UndefinedType ] = undefined.UNDEFINED, reason: typing.Union[str, undefined.UndefinedType] = undefined.UNDEFINED, ) -> channels.GuildChannel: @@ -2036,15 +2037,17 @@ async def _create_guild_channel( async def reposition_channels( self, - guild: typing.Union[guilds.Guild, bases.UniqueObject], - positions: typing.Mapping[int, typing.Union[channels.GuildChannel, bases.UniqueObject]], + guild: typing.Union[guilds.Guild, snowflake.UniqueObject], + positions: typing.Mapping[int, typing.Union[channels.GuildChannel, snowflake.UniqueObject]], ) -> None: route = routes.POST_GUILD_CHANNELS.compile(guild=guild) body = [{"id": str(int(channel)), "position": pos} for pos, channel in positions.items()] await self._request(route, body=body) async def fetch_member( - self, guild: typing.Union[guilds.Guild, bases.UniqueObject], user: typing.Union[users.User, bases.UniqueObject], + self, + guild: typing.Union[guilds.Guild, snowflake.UniqueObject], + user: typing.Union[users.User, snowflake.UniqueObject], ) -> guilds.Member: route = routes.GET_GUILD_MEMBER.compile(guild=guild, user=user) raw_response = await self._request(route) @@ -2052,23 +2055,23 @@ async def fetch_member( return self._app.entity_factory.deserialize_member(response) def fetch_members( - self, guild: typing.Union[guilds.Guild, bases.UniqueObject], + self, guild: typing.Union[guilds.Guild, snowflake.UniqueObject], ) -> iterators.LazyIterator[guilds.Member]: return iterators.MemberIterator(self._app, self._request, str(int(guild))) async def edit_member( self, - guild: typing.Union[guilds.Guild, bases.UniqueObject], - user: typing.Union[users.User, bases.UniqueObject], + guild: typing.Union[guilds.Guild, snowflake.UniqueObject], + user: typing.Union[users.User, snowflake.UniqueObject], *, nick: typing.Union[undefined.UndefinedType, str] = undefined.UNDEFINED, roles: typing.Union[ - undefined.UndefinedType, typing.Collection[typing.Union[guilds.Role, bases.UniqueObject]] + undefined.UndefinedType, typing.Collection[typing.Union[guilds.Role, snowflake.UniqueObject]] ] = undefined.UNDEFINED, mute: typing.Union[undefined.UndefinedType, bool] = undefined.UNDEFINED, deaf: typing.Union[undefined.UndefinedType, bool] = undefined.UNDEFINED, voice_channel: typing.Union[ - undefined.UndefinedType, channels.GuildVoiceChannel, bases.UniqueObject, None + undefined.UndefinedType, channels.GuildVoiceChannel, snowflake.UniqueObject, None ] = undefined.UNDEFINED, reason: typing.Union[str, undefined.UndefinedType] = undefined.UNDEFINED, ) -> None: @@ -2088,7 +2091,7 @@ async def edit_member( async def edit_my_nick( self, - guild: typing.Union[guilds.Guild, bases.UniqueObject], + guild: typing.Union[guilds.Guild, snowflake.UniqueObject], nick: typing.Optional[str], *, reason: typing.Union[undefined.UndefinedType, str] = undefined.UNDEFINED, @@ -2100,9 +2103,9 @@ async def edit_my_nick( async def add_role_to_member( self, - guild: typing.Union[guilds.Guild, bases.UniqueObject], - user: typing.Union[users.User, bases.UniqueObject], - role: typing.Union[guilds.Role, bases.UniqueObject], + guild: typing.Union[guilds.Guild, snowflake.UniqueObject], + user: typing.Union[users.User, snowflake.UniqueObject], + role: typing.Union[guilds.Role, snowflake.UniqueObject], *, reason: typing.Union[undefined.UndefinedType, str] = undefined.UNDEFINED, ) -> None: @@ -2111,9 +2114,9 @@ async def add_role_to_member( async def remove_role_from_member( self, - guild: typing.Union[guilds.Guild, bases.UniqueObject], - user: typing.Union[users.User, bases.UniqueObject], - role: typing.Union[guilds.Role, bases.UniqueObject], + guild: typing.Union[guilds.Guild, snowflake.UniqueObject], + user: typing.Union[users.User, snowflake.UniqueObject], + role: typing.Union[guilds.Role, snowflake.UniqueObject], *, reason: typing.Union[undefined.UndefinedType, str] = undefined.UNDEFINED, ) -> None: @@ -2122,8 +2125,8 @@ async def remove_role_from_member( async def kick_member( self, - guild: typing.Union[guilds.Guild, bases.UniqueObject], - user: typing.Union[users.User, bases.UniqueObject], + guild: typing.Union[guilds.Guild, snowflake.UniqueObject], + user: typing.Union[users.User, snowflake.UniqueObject], *, reason: typing.Union[undefined.UndefinedType, str] = undefined.UNDEFINED, ) -> None: @@ -2132,8 +2135,8 @@ async def kick_member( async def ban_user( self, - guild: typing.Union[guilds.Guild, bases.UniqueObject], - user: typing.Union[users.User, bases.UniqueObject], + guild: typing.Union[guilds.Guild, snowflake.UniqueObject], + user: typing.Union[users.User, snowflake.UniqueObject], *, reason: typing.Union[undefined.UndefinedType, str] = undefined.UNDEFINED, ) -> None: @@ -2142,8 +2145,8 @@ async def ban_user( async def unban_user( self, - guild: typing.Union[guilds.Guild, bases.UniqueObject], - user: typing.Union[users.User, bases.UniqueObject], + guild: typing.Union[guilds.Guild, snowflake.UniqueObject], + user: typing.Union[users.User, snowflake.UniqueObject], *, reason: typing.Union[undefined.UndefinedType, str] = undefined.UNDEFINED, ) -> None: @@ -2151,7 +2154,9 @@ async def unban_user( await self._request(route, reason=reason) async def fetch_ban( - self, guild: typing.Union[guilds.Guild, bases.UniqueObject], user: typing.Union[users.User, bases.UniqueObject], + self, + guild: typing.Union[guilds.Guild, snowflake.UniqueObject], + user: typing.Union[users.User, snowflake.UniqueObject], ) -> guilds.GuildMemberBan: route = routes.GET_GUILD_BAN.compile(guild=guild, user=user) raw_response = await self._request(route) @@ -2159,7 +2164,7 @@ async def fetch_ban( return self._app.entity_factory.deserialize_guild_member_ban(response) async def fetch_bans( - self, guild: typing.Union[guilds.Guild, bases.UniqueObject], / + self, guild: typing.Union[guilds.Guild, snowflake.UniqueObject], / ) -> typing.Sequence[guilds.GuildMemberBan]: route = routes.GET_GUILD_BANS.compile(guild=guild) raw_response = await self._request(route) @@ -2167,7 +2172,7 @@ async def fetch_bans( return data_binding.cast_json_array(response, self._app.entity_factory.deserialize_guild_member_ban) async def fetch_roles( - self, guild: typing.Union[guilds.Guild, bases.UniqueObject], / + self, guild: typing.Union[guilds.Guild, snowflake.UniqueObject], / ) -> typing.Sequence[guilds.Role]: route = routes.GET_GUILD_ROLES.compile(guild=guild) raw_response = await self._request(route) @@ -2176,7 +2181,7 @@ async def fetch_roles( async def create_role( self, - guild: typing.Union[guilds.Guild, bases.UniqueObject], + guild: typing.Union[guilds.Guild, snowflake.UniqueObject], /, *, name: typing.Union[undefined.UndefinedType, str] = undefined.UNDEFINED, @@ -2205,8 +2210,8 @@ async def create_role( async def reposition_roles( self, - guild: typing.Union[guilds.Guild, bases.UniqueObject], - positions: typing.Mapping[int, typing.Union[guilds.Role, bases.UniqueObject]], + guild: typing.Union[guilds.Guild, snowflake.UniqueObject], + positions: typing.Mapping[int, typing.Union[guilds.Role, snowflake.UniqueObject]], ) -> None: route = routes.POST_GUILD_ROLES.compile(guild=guild) body = [{"id": str(int(role)), "position": pos} for pos, role in positions.items()] @@ -2214,8 +2219,8 @@ async def reposition_roles( async def edit_role( self, - guild: typing.Union[guilds.Guild, bases.UniqueObject], - role: typing.Union[guilds.Role, bases.UniqueObject], + guild: typing.Union[guilds.Guild, snowflake.UniqueObject], + role: typing.Union[guilds.Role, snowflake.UniqueObject], *, name: typing.Union[undefined.UndefinedType, str] = undefined.UNDEFINED, permissions: typing.Union[undefined.UndefinedType, permissions_.Permission] = undefined.UNDEFINED, @@ -2244,14 +2249,14 @@ async def edit_role( async def delete_role( self, - guild: typing.Union[guilds.Guild, bases.UniqueObject], - role: typing.Union[guilds.Role, bases.UniqueObject], + guild: typing.Union[guilds.Guild, snowflake.UniqueObject], + role: typing.Union[guilds.Role, snowflake.UniqueObject], ) -> None: route = routes.DELETE_GUILD_ROLE.compile(guild=guild, role=role) await self._request(route) async def estimate_guild_prune_count( - self, guild: typing.Union[guilds.Guild, bases.UniqueObject], days: int, + self, guild: typing.Union[guilds.Guild, snowflake.UniqueObject], days: int, ) -> int: route = routes.GET_GUILD_PRUNE.compile(guild=guild) query = data_binding.StringMapBuilder() @@ -2262,7 +2267,7 @@ async def estimate_guild_prune_count( async def begin_guild_prune( self, - guild: typing.Union[guilds.Guild, bases.UniqueObject], + guild: typing.Union[guilds.Guild, snowflake.UniqueObject], days: int, *, reason: typing.Union[undefined.UndefinedType, str], @@ -2276,7 +2281,7 @@ async def begin_guild_prune( return int(response["pruned"]) async def fetch_guild_voice_regions( - self, guild: typing.Union[guilds.Guild, bases.UniqueObject], / + self, guild: typing.Union[guilds.Guild, snowflake.UniqueObject], / ) -> typing.Sequence[voices.VoiceRegion]: route = routes.GET_GUILD_VOICE_REGIONS.compile(guild=guild) raw_response = await self._request(route) @@ -2284,7 +2289,7 @@ async def fetch_guild_voice_regions( return data_binding.cast_json_array(response, self._app.entity_factory.deserialize_voice_region) async def fetch_guild_invites( - self, guild: typing.Union[guilds.Guild, bases.UniqueObject], / + self, guild: typing.Union[guilds.Guild, snowflake.UniqueObject], / ) -> typing.Sequence[invites.InviteWithMetadata]: route = routes.GET_GUILD_INVITES.compile(guild=guild) raw_response = await self._request(route) @@ -2292,7 +2297,7 @@ async def fetch_guild_invites( return data_binding.cast_json_array(response, self._app.entity_factory.deserialize_invite_with_metadata) async def fetch_integrations( - self, guild: typing.Union[guilds.Guild, bases.UniqueObject], / + self, guild: typing.Union[guilds.Guild, snowflake.UniqueObject], / ) -> typing.Sequence[guilds.Integration]: route = routes.GET_GUILD_INTEGRATIONS.compile(guild=guild) raw_response = await self._request(route) @@ -2301,8 +2306,8 @@ async def fetch_integrations( async def edit_integration( self, - guild: typing.Union[guilds.Guild, bases.UniqueObject], - integration: typing.Union[guilds.Integration, bases.UniqueObject], + guild: typing.Union[guilds.Guild, snowflake.UniqueObject], + integration: typing.Union[guilds.Integration, snowflake.UniqueObject], *, expire_behaviour: typing.Union[ undefined.UndefinedType, guilds.IntegrationExpireBehaviour @@ -2321,8 +2326,8 @@ async def edit_integration( async def delete_integration( self, - guild: typing.Union[guilds.Guild, bases.UniqueObject], - integration: typing.Union[guilds.Integration, bases.UniqueObject], + guild: typing.Union[guilds.Guild, snowflake.UniqueObject], + integration: typing.Union[guilds.Integration, snowflake.UniqueObject], *, reason: typing.Union[undefined.UndefinedType, str] = undefined.UNDEFINED, ) -> None: @@ -2331,13 +2336,13 @@ async def delete_integration( async def sync_integration( self, - guild: typing.Union[guilds.Guild, bases.UniqueObject], - integration: typing.Union[guilds.Integration, bases.UniqueObject], + guild: typing.Union[guilds.Guild, snowflake.UniqueObject], + integration: typing.Union[guilds.Integration, snowflake.UniqueObject], ) -> None: route = routes.POST_GUILD_INTEGRATION_SYNC.compile(guild=guild, integration=integration) await self._request(route) - async def fetch_widget(self, guild: typing.Union[guilds.Guild, bases.UniqueObject], /) -> guilds.GuildWidget: + async def fetch_widget(self, guild: typing.Union[guilds.Guild, snowflake.UniqueObject], /) -> guilds.GuildWidget: route = routes.GET_GUILD_WIDGET.compile(guild=guild) raw_response = await self._request(route) response = typing.cast(data_binding.JSONObject, raw_response) @@ -2345,11 +2350,11 @@ async def fetch_widget(self, guild: typing.Union[guilds.Guild, bases.UniqueObjec async def edit_widget( self, - guild: typing.Union[guilds.Guild, bases.UniqueObject], + guild: typing.Union[guilds.Guild, snowflake.UniqueObject], /, *, channel: typing.Union[ - undefined.UndefinedType, channels.GuildChannel, bases.UniqueObject, None + undefined.UndefinedType, channels.GuildChannel, snowflake.UniqueObject, None ] = undefined.UNDEFINED, enabled: typing.Union[undefined.UndefinedType, bool] = undefined.UNDEFINED, reason: typing.Union[undefined.UndefinedType, str] = undefined.UNDEFINED, @@ -2367,7 +2372,7 @@ async def edit_widget( response = typing.cast(data_binding.JSONObject, raw_response) return self._app.entity_factory.deserialize_guild_widget(response) - async def fetch_vanity_url(self, guild: typing.Union[guilds.Guild, bases.UniqueObject], /) -> invites.VanityURL: + async def fetch_vanity_url(self, guild: typing.Union[guilds.Guild, snowflake.UniqueObject], /) -> invites.VanityURL: route = routes.GET_GUILD_VANITY_URL.compile(guild=guild) raw_response = await self._request(route) response = typing.cast(data_binding.JSONObject, raw_response) diff --git a/hikari/net/rest_utils.py b/hikari/net/rest_utils.py index 8f3c9e2c2d..d4e851703d 100644 --- a/hikari/net/rest_utils.py +++ b/hikari/net/rest_utils.py @@ -40,7 +40,6 @@ import types from hikari.api import rest - from hikari.models import bases from hikari.models import channels from hikari.models import colors from hikari.models import guilds @@ -61,7 +60,7 @@ class TypingIndicator: def __init__( self, - channel: typing.Union[channels.TextChannel, bases.UniqueObject], + channel: typing.Union[channels.TextChannel, snowflake_.UniqueObject], request_call: typing.Callable[ ..., typing.Coroutine[None, None, typing.Union[None, data_binding.JSONObject, data_binding.JSONArray]] ], diff --git a/hikari/utilities/data_binding.py b/hikari/utilities/data_binding.py index 2d79311c71..7a4a34df5c 100644 --- a/hikari/utilities/data_binding.py +++ b/hikari/utilities/data_binding.py @@ -40,7 +40,7 @@ import aiohttp.typedefs import multidict -from hikari.models import bases +from hikari.utilities import snowflake from hikari.utilities import undefined T = typing.TypeVar("T", covariant=True) @@ -144,7 +144,7 @@ def put( value = "false" elif value is None: value = "null" - elif isinstance(value, bases.Unique): + elif isinstance(value, snowflake.Unique): value = str(value.id) else: value = str(value) @@ -234,7 +234,7 @@ def put_array( else: self[key] = list(values) - def put_snowflake(self, key: str, value: typing.Union[undefined.UndefinedType, bases.UniqueObject], /) -> None: + def put_snowflake(self, key: str, value: typing.Union[undefined.UndefinedType, snowflake.UniqueObject], /) -> None: """Put a key with a snowflake value into the builder. Parameters @@ -249,7 +249,7 @@ def put_snowflake(self, key: str, value: typing.Union[undefined.UndefinedType, b self[key] = str(int(value)) def put_snowflake_array( - self, key: str, values: typing.Union[undefined.UndefinedType, typing.Iterable[bases.UniqueObject]], /, + self, key: str, values: typing.Union[undefined.UndefinedType, typing.Iterable[snowflake.UniqueObject]], /, ) -> None: """Put an array of snowflakes with the given key into this builder. diff --git a/hikari/utilities/snowflake.py b/hikari/utilities/snowflake.py index b83367fb31..94ae3fb7ae 100644 --- a/hikari/utilities/snowflake.py +++ b/hikari/utilities/snowflake.py @@ -21,6 +21,7 @@ __all__: typing.Final[typing.List[str]] = ["Snowflake"] +import abc import datetime # noinspection PyUnresolvedReferences @@ -87,3 +88,37 @@ def from_data(cls, timestamp: datetime.datetime, worker_id: int, process_id: int return cls( (date.datetime_to_discord_epoch(timestamp) << 22) | (worker_id << 17) | (process_id << 12) | increment ) + + +class Unique(abc.ABC): + """Mixin for a class that enforces uniqueness by a snowflake ID.""" + + __slots__ = () + + @property + @abc.abstractmethod + def id(self) -> Snowflake: + """The ID of this entity.""" + + @id.setter + def id(self, value: Snowflake) -> None: + """Set the ID on this entity.""" + + @property + def created_at(self) -> datetime.datetime: + """When the object was created.""" + return self.id.created_at + + @typing.final + def __int__(self) -> int: + return int(self.id) + + def __hash__(self) -> int: + return hash(self.id) + + def __eq__(self, other: typing.Any) -> bool: + return type(self) is type(other) and self.id == other.id + + +UniqueObject = typing.Union[Unique, Snowflake, int, str] +"""Type hint representing a unique object entity.""" diff --git a/tests/hikari/_helpers.py b/tests/hikari/_helpers.py index 6cacd848e8..7e12a419e7 100644 --- a/tests/hikari/_helpers.py +++ b/tests/hikari/_helpers.py @@ -34,8 +34,6 @@ import mock import pytest -from hikari.models import bases - _LOGGER = logging.getLogger(__name__) @@ -229,54 +227,6 @@ def _maybe_mock_type_name(value): ) -def _parameterize_ids_id(param_name): - def ids(param): - type_name = type(param).__name__ if isinstance(param, (str, int)) else _maybe_mock_type_name(param) - return f" type({param_name}) is {type_name} " - - return ids - - -def parametrize_valid_id_formats_for_models(param_name, id, model_type1, *model_types, **kwargs): - """ - @pytest.mark.parameterize for a param that is an id-able object, but could be the ID in a string, the ID in an int, - or the ID in a given model type... - - For example - - >>> @parametrize_valid_id_formats_for_models("guild", 1234, guilds.Guild, unavailable=False) - - ...would be the same as... - - >>> @pytest.mark.parametrize( - ... "guild", - ... [ - ... 1234, - ... snowflakes.Snowflake(1234), - ... mock_model(guilds.Guild, id=snowflakes.Snowflake(1234), unavailable=False) - ... ], - ... id=lambda ...: ... - ... ) - - These are stackable as long as the parameter name is unique, as expected. - """ - model_types = [model_type1, *model_types] - - def decorator(func): - mock_models = [] - for model_type in model_types: - assert bases.Unique.__name__ in map( - lambda mro: mro.__name__, model_type.mro() - ), f"model must be a {bases.Unique.__name__} derivative" - mock_models.append(mock_model(model_type, id=bases.Snowflake(id), **kwargs)) - - return pytest.mark.parametrize( - param_name, [int(id), bases.Snowflake(id), *mock_models], ids=_parameterize_ids_id(param_name) - )(func) - - return decorator - - def todo_implement(fn=...): def decorator(fn): return pytest.mark.xfail(reason="Code for test case not yet implemented.")(fn) diff --git a/tests/hikari/impl/test_entity_factory.py b/tests/hikari/impl/test_entity_factory.py index ee2b83760e..920d8d1479 100644 --- a/tests/hikari/impl/test_entity_factory.py +++ b/tests/hikari/impl/test_entity_factory.py @@ -196,7 +196,7 @@ def test_deserialize_application( self, entity_factory_impl, mock_app, application_information_payload, owner_payload, user_payload ): application = entity_factory_impl.deserialize_application(application_information_payload) - assert application._app is mock_app + assert application.app is mock_app assert application.id == 209333111222 assert application.name == "Dream Sweet in Sea Major" assert application.description == "I am an application" @@ -432,7 +432,7 @@ def partial_channel_payload(self): def test_deserialize_partial_channel(self, entity_factory_impl, mock_app, partial_channel_payload): partial_channel = entity_factory_impl.deserialize_partial_channel(partial_channel_payload) - assert partial_channel._app is mock_app + assert partial_channel.app is mock_app assert partial_channel.id == 561884984214814750 assert partial_channel.name == "general" assert partial_channel.type == channel_models.ChannelType.GUILD_TEXT @@ -452,7 +452,7 @@ def dm_channel_payload(self, user_payload): def test_deserialize_dm_channel(self, entity_factory_impl, mock_app, dm_channel_payload, user_payload): dm_channel = entity_factory_impl.deserialize_dm_channel(dm_channel_payload) - assert dm_channel._app is mock_app + assert dm_channel.app is mock_app assert dm_channel.id == 123 assert dm_channel.name is None assert dm_channel.last_message_id == 456 @@ -482,7 +482,7 @@ def group_dm_channel_payload(self, user_payload): def test_deserialize_group_dm_channel(self, entity_factory_impl, mock_app, group_dm_channel_payload, user_payload): group_dm = entity_factory_impl.deserialize_group_dm_channel(group_dm_channel_payload) - assert group_dm._app is mock_app + assert group_dm.app is mock_app assert group_dm.id == 123 assert group_dm.name == "Secret Developer Group" assert group_dm.icon_hash == "123asdf123adsf" @@ -525,7 +525,7 @@ def test_deserialize_guild_category( self, entity_factory_impl, mock_app, guild_category_payload, permission_overwrite_payload ): guild_category = entity_factory_impl.deserialize_guild_category(guild_category_payload) - assert guild_category._app is mock_app + assert guild_category.app is mock_app assert guild_category.id == 123 assert guild_category.name == "Test" assert guild_category.type == channel_models.ChannelType.GUILD_CATEGORY @@ -588,7 +588,7 @@ def test_deserialize_guild_text_channel( self, entity_factory_impl, mock_app, guild_text_channel_payload, permission_overwrite_payload ): guild_text_channel = entity_factory_impl.deserialize_guild_text_channel(guild_text_channel_payload) - assert guild_text_channel._app is mock_app + assert guild_text_channel.app is mock_app assert guild_text_channel.id == 123 assert guild_text_channel.name == "general" assert guild_text_channel.type == channel_models.ChannelType.GUILD_TEXT @@ -667,7 +667,7 @@ def test_deserialize_guild_news_channel( self, entity_factory_impl, mock_app, guild_news_channel_payload, permission_overwrite_payload ): news_channel = entity_factory_impl.deserialize_guild_news_channel(guild_news_channel_payload) - assert news_channel._app is mock_app + assert news_channel.app is mock_app assert news_channel.id == 7777 assert news_channel.name == "Important Announcements" assert news_channel.type == channel_models.ChannelType.GUILD_NEWS @@ -1131,7 +1131,7 @@ def custom_emoji_payload(self): def test_deserialize_custom_emoji(self, entity_factory_impl, mock_app, custom_emoji_payload): emoji = entity_factory_impl.deserialize_custom_emoji(custom_emoji_payload) - assert emoji._app is mock_app + assert emoji.app is mock_app assert emoji.id == 691225175349395456 assert emoji.name == "test" assert emoji.is_animated is True @@ -1161,7 +1161,7 @@ def test_deserialize_known_custom_emoji( self, entity_factory_impl, mock_app, user_payload, known_custom_emoji_payload ): emoji = entity_factory_impl.deserialize_known_custom_emoji(known_custom_emoji_payload) - assert emoji._app is mock_app + assert emoji.app is mock_app assert emoji.id == 12345 assert emoji.name == "testing" assert emoji.is_animated is False @@ -1227,7 +1227,7 @@ def guild_embed_payload(self): def test_deserialize_widget_embed(self, entity_factory_impl, mock_app, guild_embed_payload): guild_embed = entity_factory_impl.deserialize_guild_widget(guild_embed_payload) - assert guild_embed._app is mock_app + assert guild_embed.app is mock_app assert guild_embed.channel_id == 123123123 assert guild_embed.is_enabled is True assert isinstance(guild_embed, guild_models.GuildWidget) @@ -1249,7 +1249,7 @@ def member_payload(self, user_payload): def test_deserialize_member(self, entity_factory_impl, mock_app, member_payload, user_payload): member = entity_factory_impl.deserialize_member(member_payload) - assert member._app is mock_app + assert member.app is mock_app assert member.user == entity_factory_impl.deserialize_user(user_payload) assert member.nickname == "foobarbaz" assert member.role_ids == {11111, 22222, 33333, 44444} @@ -1321,7 +1321,7 @@ def guild_role_payload(self): def test_deserialize_role(self, entity_factory_impl, mock_app, guild_role_payload): guild_role = entity_factory_impl.deserialize_role(guild_role_payload) - assert guild_role._app is mock_app + assert guild_role.app is mock_app assert guild_role.id == 41771983423143936 assert guild_role.name == "WE DEM BOYZZ!!!!!!" assert guild_role.color == color_models.Color(3_447_003) @@ -1425,7 +1425,6 @@ def test_deserialize_guild_member_ban_with_null_fields(self, entity_factory_impl def test_deserialize_unavailable_guild(self, entity_factory_impl, mock_app): unavailable_guild = entity_factory_impl.deserialize_unavailable_guild({"id": "42069", "unavailable": True}) - assert unavailable_guild._app is mock_app assert unavailable_guild.id == 42069 assert unavailable_guild.is_unavailable is True assert isinstance(unavailable_guild, guild_models.UnavailableGuild) @@ -1449,7 +1448,7 @@ def test_deserialize_guild_preview( self, entity_factory_impl, mock_app, guild_preview_payload, known_custom_emoji_payload ): guild_preview = entity_factory_impl.deserialize_guild_preview(guild_preview_payload) - assert guild_preview._app is mock_app + assert guild_preview.app is mock_app assert guild_preview.id == 152559372126519269 assert guild_preview.name == "Isopropyl" assert guild_preview.icon_hash == "d4a983885dsaa7691ce8bcaaf945a" @@ -1559,7 +1558,7 @@ def test_deserialize_guild( voice_state_payload, ): guild = entity_factory_impl.deserialize_guild(guild_payload) - assert guild._app is mock_app + assert guild.app is mock_app assert guild.id == 265828729970753537 assert guild.name == "L33t guild" assert guild.icon_hash == "1a2b3c4d" @@ -1742,7 +1741,7 @@ def vanity_url_payload(self): def test_deserialize_vanity_url(self, entity_factory_impl, mock_app, vanity_url_payload): vanity_url = entity_factory_impl.deserialize_vanity_url(vanity_url_payload) - assert vanity_url._app is mock_app + assert vanity_url.app is mock_app assert vanity_url.code == "iamacode" assert vanity_url.uses == 42 assert isinstance(vanity_url, invite_models.VanityURL) @@ -1784,7 +1783,7 @@ def test_deserialize_invite( alternative_user_payload, ): invite = entity_factory_impl.deserialize_invite(invite_payload) - assert invite._app is mock_app + assert invite.app is mock_app assert invite.code == "aCode" # InviteGuild assert invite.guild.id == 56188492224814744 @@ -1863,7 +1862,7 @@ def test_deserialize_invite_with_metadata( alternative_user_payload, ): invite_with_metadata = entity_factory_impl.deserialize_invite_with_metadata(invite_with_metadata_payload) - assert invite_with_metadata._app is mock_app + assert invite_with_metadata.app is mock_app assert invite_with_metadata.code == "aCode" # InviteGuild assert invite_with_metadata.guild.id == 56188492224814744 @@ -1991,7 +1990,7 @@ def test_deserialize_full_message( embed_payload, ): message = entity_factory_impl.deserialize_message(message_payload) - assert message._app is mock_app + assert message.app is mock_app assert message.id == 123 assert message.channel_id == 456 assert message.guild_id == 678 @@ -2040,7 +2039,7 @@ def test_deserialize_full_message( assert message.application == entity_factory_impl.deserialize_application(partial_application_payload) # MessageCrosspost - assert message.message_reference._app is mock_app + assert message.message_reference.app is mock_app assert message.message_reference.id == 306588351130107906 assert message.message_reference.channel_id == 278325129692446722 assert message.message_reference.guild_id == 278325129692446720 @@ -2069,7 +2068,7 @@ def test_deserialize_message_with_null_and_unset_fields( } message = entity_factory_impl.deserialize_message(message_payload) - assert message._app is mock_app + assert message.app is mock_app assert message.guild_id is None assert message.member is None assert message.edited_timestamp is None @@ -2131,9 +2130,9 @@ def test_deserialize_member_presence( self, entity_factory_impl, mock_app, member_presence_payload, custom_emoji_payload, user_payload ): presence = entity_factory_impl.deserialize_member_presence(member_presence_payload) - assert presence._app is mock_app + assert presence.app is mock_app # PresenceUser - assert presence.user._app is mock_app + assert presence.user.app is mock_app assert presence.user.id == 115590097100865541 assert presence.user.discriminator == "6127" assert presence.user.username == "nyaa" @@ -2375,7 +2374,7 @@ def user_payload(self): def test_deserialize_user(self, entity_factory_impl, mock_app, user_payload): user = entity_factory_impl.deserialize_user(user_payload) - assert user._app is mock_app + assert user.app is mock_app assert user.id == 115590097100865541 assert user.username == "nyaa" assert user.avatar_hash == "b3b24c6d7cbcdec129d5d537067061a8" @@ -2418,7 +2417,7 @@ def my_user_payload(self): def test_deserialize_my_user(self, entity_factory_impl, mock_app, my_user_payload): my_user = entity_factory_impl.deserialize_my_user(my_user_payload) - assert my_user._app is mock_app + assert my_user.app is mock_app assert my_user.id == 379953393319542784 assert my_user.username == "qt pi" assert my_user.avatar_hash == "820d0e50543216e812ad94e6ab7" @@ -2447,7 +2446,7 @@ def test_deserialize_my_user_with_unset_fields(self, entity_factory_impl, mock_a "premium_type": 1, } ) - assert my_user._app is mock_app + assert my_user.app is mock_app assert my_user.is_bot is False assert my_user.is_system is False assert my_user.is_verified is None @@ -2477,7 +2476,7 @@ def voice_state_payload(self, member_payload): def test_deserialize_voice_state(self, entity_factory_impl, mock_app, voice_state_payload, member_payload): voice_state = entity_factory_impl.deserialize_voice_state(voice_state_payload) - assert voice_state._app is mock_app + assert voice_state.app is mock_app assert voice_state.guild_id == 929292929292992 assert voice_state.channel_id == 157733188964188161 assert voice_state.user_id == 80351110224678912 @@ -2622,7 +2621,7 @@ def channel_pins_update_payload(self): def test_deserialize_channel_pins_update_event(self, entity_factory_impl, mock_app, channel_pins_update_payload): channel_pins_update = entity_factory_impl.deserialize_channel_pins_update_event(channel_pins_update_payload) - assert channel_pins_update._app is mock_app + assert channel_pins_update.app is mock_app assert channel_pins_update.channel_id == 123123 assert channel_pins_update.guild_id == 9439494 assert channel_pins_update.last_pin_timestamp == datetime.datetime( @@ -2632,7 +2631,7 @@ def test_deserialize_channel_pins_update_event(self, entity_factory_impl, mock_a def test_deserialize_channel_pins_update_event_with_unset_fields(self, entity_factory_impl, mock_app): channel_pins_update = entity_factory_impl.deserialize_channel_pins_update_event({"channel_id": "123123"}) - assert channel_pins_update._app is mock_app + assert channel_pins_update.app is mock_app assert channel_pins_update.guild_id is None assert channel_pins_update.last_pin_timestamp is None @@ -2642,7 +2641,7 @@ def webhook_update_payload(self): def test_deserialize_webhook_update_event(self, entity_factory_impl, mock_app, webhook_update_payload): webhook_update = entity_factory_impl.deserialize_webhook_update_event(webhook_update_payload) - assert webhook_update._app is mock_app + assert webhook_update.app is mock_app assert webhook_update.guild_id == 123123123 assert webhook_update.channel_id == 93493939 assert isinstance(webhook_update, channel_events.WebhookUpdateEvent) @@ -2659,7 +2658,7 @@ def typing_start_payload(self, member_payload): def test_deserialize_typing_start_event(self, entity_factory_impl, mock_app, typing_start_payload, member_payload): typing_start = entity_factory_impl.deserialize_typing_start_event(typing_start_payload) - assert typing_start._app is mock_app + assert typing_start.app is mock_app assert typing_start.channel_id == 123123 assert typing_start.guild_id == 4542242 assert typing_start.user_id == 29292929 @@ -2687,7 +2686,7 @@ def invite_delete_payload(self): def test_deserialize_invite_delete_event(self, entity_factory_impl, mock_app, invite_delete_payload): invite_delete = entity_factory_impl.deserialize_invite_delete_event(invite_delete_payload) - assert invite_delete._app is mock_app + assert invite_delete.app is mock_app assert invite_delete.code == "Heck" assert invite_delete.channel_id == 123123 assert invite_delete.guild_id == 93939393 @@ -2725,7 +2724,7 @@ def guild_unavailable_payload(self): def test_deserialize_guild_unavailable_event(self, entity_factory_impl, mock_app, guild_unavailable_payload): guild_unavailable = entity_factory_impl.deserialize_guild_unavailable_event(guild_unavailable_payload) - assert guild_unavailable._app is mock_app + assert guild_unavailable.app is mock_app assert guild_unavailable.id == 4123123 assert isinstance(guild_unavailable, guild_events.GuildUnavailableEvent) @@ -2735,14 +2734,14 @@ def guild_ban_payload(self, user_payload): def test_deserialize_guild_ban_add_event(self, entity_factory_impl, mock_app, guild_ban_payload, user_payload): guild_ban_add = entity_factory_impl.deserialize_guild_ban_add_event(guild_ban_payload) - assert guild_ban_add._app is mock_app + assert guild_ban_add.app is mock_app assert guild_ban_add.guild_id == 2002020202022 assert guild_ban_add.user == entity_factory_impl.deserialize_user(user_payload) assert isinstance(guild_ban_add, guild_events.GuildBanAddEvent) def test_deserialize_guild_ban_remove_event(self, entity_factory_impl, mock_app, guild_ban_payload, user_payload): guild_ban_add = entity_factory_impl.deserialize_guild_ban_remove_event(guild_ban_payload) - assert guild_ban_add._app is mock_app + assert guild_ban_add.app is mock_app assert guild_ban_add.guild_id == 2002020202022 assert guild_ban_add.user == entity_factory_impl.deserialize_user(user_payload) assert isinstance(guild_ban_add, guild_events.GuildBanRemoveEvent) @@ -2755,7 +2754,7 @@ def test_deserialize_guild_emojis_update_event( self, entity_factory_impl, mock_app, guild_emojis_update_payload, known_custom_emoji_payload ): guild_emoji_update = entity_factory_impl.deserialize_guild_emojis_update_event(guild_emojis_update_payload) - assert guild_emoji_update._app is mock_app + assert guild_emoji_update.app is mock_app assert guild_emoji_update.guild_id == 424242 assert guild_emoji_update.emojis == { 12345: entity_factory_impl.deserialize_known_custom_emoji(known_custom_emoji_payload) @@ -2772,7 +2771,7 @@ def test_deserialize_guild_integrations_update_event( guild_integrations_update = entity_factory_impl.deserialize_guild_integrations_update_event( guild_integrations_update_payload ) - assert guild_integrations_update._app is mock_app + assert guild_integrations_update.app is mock_app assert guild_integrations_update.guild_id == 439399393 assert isinstance(guild_integrations_update, guild_events.GuildIntegrationsUpdateEvent) @@ -2784,7 +2783,7 @@ def test_deserialize_guild_member_add_event( self, entity_factory_impl, mock_app, guild_member_add_payload, member_payload ): guild_member_add = entity_factory_impl.deserialize_guild_member_add_event(guild_member_add_payload) - assert guild_member_add._app is mock_app + assert guild_member_add.app is mock_app assert guild_member_add.guild_id == 20202020202020 assert guild_member_add.member == entity_factory_impl.deserialize_member(member_payload) assert isinstance(guild_member_add, guild_events.GuildMemberAddEvent) @@ -2802,7 +2801,7 @@ def test_deserialize_guild_member_remove_event( self, entity_factory_impl, mock_app, guild_member_remove_payload, user_payload ): guild_member_remove = entity_factory_impl.deserialize_guild_member_remove_event(guild_member_remove_payload) - assert guild_member_remove._app is mock_app + assert guild_member_remove.app is mock_app assert guild_member_remove.guild_id == 20202020 assert guild_member_remove.user == entity_factory_impl.deserialize_user(user_payload) assert isinstance(guild_member_remove, guild_events.GuildMemberRemoveEvent) @@ -2815,7 +2814,7 @@ def test_deserialize_guild_role_create_event( self, entity_factory_impl, mock_app, guild_role_create_update_payload, guild_role_payload ): guild_role_create = entity_factory_impl.deserialize_guild_role_create_event(guild_role_create_update_payload) - assert guild_role_create._app is mock_app + assert guild_role_create.app is mock_app assert guild_role_create.guild_id == 303030300303 assert guild_role_create.role == entity_factory_impl.deserialize_role(guild_role_payload) assert isinstance(guild_role_create, guild_events.GuildRoleCreateEvent) @@ -2824,7 +2823,7 @@ def test_deserialize_guild_role_update_event( self, entity_factory_impl, mock_app, guild_role_create_update_payload, guild_role_payload ): guild_role_create = entity_factory_impl.deserialize_guild_role_update_event(guild_role_create_update_payload) - assert guild_role_create._app is mock_app + assert guild_role_create.app is mock_app assert guild_role_create.guild_id == 303030300303 assert guild_role_create.role == entity_factory_impl.deserialize_role(guild_role_payload) assert isinstance(guild_role_create, guild_events.GuildRoleUpdateEvent) @@ -2835,7 +2834,7 @@ def guild_role_delete_payload(self): def test_deserialize_guild_role_delete_event(self, entity_factory_impl, mock_app, guild_role_delete_payload): guild_role_delete = entity_factory_impl.deserialize_guild_role_delete_event(guild_role_delete_payload) - assert guild_role_delete._app is mock_app + assert guild_role_delete.app is mock_app assert guild_role_delete.guild_id == 93939393939 assert guild_role_delete.role_id == 8383483848484848 assert isinstance(guild_role_delete, guild_events.GuildRoleDeleteEvent) @@ -2867,7 +2866,7 @@ def test_deserialize_message_update_event_with_full_payload( ): message_update = entity_factory_impl.deserialize_message_update_event(message_payload) - assert message_update.message._app is mock_app + assert message_update.message.app is mock_app assert message_update.message.id == 123 assert message_update.message.channel_id == 456 assert message_update.message.guild_id == 678 @@ -2919,7 +2918,7 @@ def test_deserialize_message_update_event_with_full_payload( partial_application_payload ) # MessageCrosspost - assert message_update.message.message_reference._app is mock_app + assert message_update.message.message_reference.app is mock_app assert message_update.message.message_reference.id == 306588351130107906 assert message_update.message.message_reference.channel_id == 278325129692446722 assert message_update.message.message_reference.guild_id == 278325129692446720 @@ -2969,7 +2968,7 @@ def message_delete_payload(self): def test_deserialize_message_delete_event(self, entity_factory_impl, mock_app, message_delete_payload): message_delete = entity_factory_impl.deserialize_message_delete_event(message_delete_payload) - assert message_delete._app is mock_app + assert message_delete.app is mock_app assert message_delete.message_id == 123123 assert message_delete.channel_id == 9292929 assert message_delete.guild_id == 202020202 @@ -2981,7 +2980,7 @@ def message_delete_bulk_payload(self): def test_deserialize_message_delete_bulk_event(self, entity_factory_impl, mock_app, message_delete_bulk_payload): message_delete_bulk = entity_factory_impl.deserialize_message_delete_bulk_event(message_delete_bulk_payload) - assert message_delete_bulk._app is mock_app + assert message_delete_bulk.app is mock_app assert message_delete_bulk.message_ids == {123123, 9349299} assert message_delete_bulk.channel_id == 92392929 assert message_delete_bulk.guild_id == 92929292 @@ -3002,7 +3001,7 @@ def test_deserialize_message_reaction_add_event( self, entity_factory_impl, mock_app, message_reaction_add_payload, member_payload, custom_emoji_payload ): message_reaction_add = entity_factory_impl.deserialize_message_reaction_add_event(message_reaction_add_payload) - assert message_reaction_add._app is mock_app + assert message_reaction_add.app is mock_app assert message_reaction_add.user_id == 202020 assert message_reaction_add.channel_id == 23848774 assert message_reaction_add.message_id == 484848 @@ -3034,7 +3033,7 @@ def test_deserialize_message_reaction_remove_event( self, entity_factory_impl, mock_app, reaction_remove_payload, custom_emoji_payload ): reaction_remove = entity_factory_impl.deserialize_message_reaction_remove_event(reaction_remove_payload) - assert reaction_remove._app is mock_app + assert reaction_remove.app is mock_app assert reaction_remove.user_id == 945939389393939393 assert reaction_remove.channel_id == 92903893923 assert reaction_remove.message_id == 222222222 @@ -3065,7 +3064,7 @@ def test_deserialize_message_reaction_remove_all_event( message_reaction_remove_all = entity_factory_impl.deserialize_message_reaction_remove_all_event( message_reaction_remove_all_payload ) - assert message_reaction_remove_all._app is mock_app + assert message_reaction_remove_all.app is mock_app assert message_reaction_remove_all.channel_id == 222293844884 assert message_reaction_remove_all.message_id == 93493939 assert message_reaction_remove_all.guild_id == 939393939390 @@ -3092,7 +3091,7 @@ def test_deserialize_message_reaction_remove_emoji_event( message_reaction_remove_emoji = entity_factory_impl.deserialize_message_reaction_remove_emoji_event( message_reaction_remove_emoji_payload ) - assert message_reaction_remove_emoji._app is mock_app + assert message_reaction_remove_emoji.app is mock_app assert message_reaction_remove_emoji.channel_id == 123123 assert message_reaction_remove_emoji.guild_id == 495485494945 assert message_reaction_remove_emoji.message_id == 93999328484 diff --git a/tests/hikari/utilities/test_data_binding.py b/tests/hikari/utilities/test_data_binding.py index 39b5fb35f7..2deb50d197 100644 --- a/tests/hikari/utilities/test_data_binding.py +++ b/tests/hikari/utilities/test_data_binding.py @@ -17,19 +17,19 @@ # along with Hikari. If not, see . import typing +import attr import mock import multidict import pytest -from hikari.models import bases from hikari.utilities import data_binding from hikari.utilities import snowflake from hikari.utilities import undefined -class MyUnique(bases.Unique): - def __init__(self, value): - self.id = snowflake.Snowflake(value) +@attr.s(slots=True) +class MyUnique(snowflake.Unique): + id: snowflake.Snowflake = attr.ib(converter=snowflake.Snowflake) class TestStringMapBuilder: From 1d3291fa2a86d880adf78b6e8957a3fbbb43ffad Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Thu, 18 Jun 2020 18:35:12 +0100 Subject: [PATCH 542/922] Fixed inconsistency in documentation font, added fqns to RTFM. --- docs/css.mako | 6 +++++- docs/documentation.mako | 16 ++++++++-------- 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/docs/css.mako b/docs/css.mako index 2c01556af4..8b64261488 100644 --- a/docs/css.mako +++ b/docs/css.mako @@ -30,6 +30,10 @@ h4 { margin-top: 2em; } +.monospaced { + font-family: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; +} + a.sidebar-nav-pill, a.sidebar-nav-pill:active, a.sidebar-nav-pill:hover { @@ -156,4 +160,4 @@ a.dotted { .gsc-control-cse .gsc-control-cse-en { margin-top: 0 !important; -} \ No newline at end of file +} diff --git a/docs/documentation.mako b/docs/documentation.mako index b4e11721e3..e4ee6a20c2 100644 --- a/docs/documentation.mako +++ b/docs/documentation.mako @@ -275,7 +275,7 @@ project_inventory.objects.append( sphobjinv.DataObjStr( - name = v.name, + name = f"{v.module.name}.{v.name}", domain = "py", role = "var", uri = v.url(), @@ -322,7 +322,7 @@ if not redirect: project_inventory.objects.append( sphobjinv.DataObjStr( - name = f.name, + name = f"{f.module.name}.{f.name}", domain = "py", role = "func", uri = f.url(), @@ -386,7 +386,7 @@ if not redirect: project_inventory.objects.append( sphobjinv.DataObjStr( - name = c.name, + name = f"{c.module.name}.{c.name}", domain = "py", role = "class", uri = c.url(), @@ -563,7 +563,7 @@ % if submodules:
    % for child_module in submodules: -
  • ${link(child_module, with_prefixes=True, css_classes="sidebar-nav-pill", dotted=False)}
  • +
  • ${link(child_module, with_prefixes=True, css_classes="sidebar-nav-pill", dotted=False)}
  • % endfor
% endif @@ -575,7 +575,7 @@ % if variables:
    % for variable in variables: -
  • ${link(variable, with_prefixes=True, css_classes="sidebar-nav-pill", dotted=False)}
  • +
  • ${link(variable, with_prefixes=True, css_classes="sidebar-nav-pill", dotted=False)}
  • % endfor
% endif @@ -583,7 +583,7 @@ % if functions:
    % for function in functions: -
  • ${link(function, with_prefixes=True, css_classes="sidebar-nav-pill", dotted=False)}
  • +
  • ${link(function, with_prefixes=True, css_classes="sidebar-nav-pill", dotted=False)}
  • % endfor
% endif @@ -592,7 +592,7 @@ % for c in classes: ## Purposely using one item per list for layout reasons.
    -
  • +
  • ${link(c, with_prefixes=True, css_classes="sidebar-nav-pill", dotted=False)} <% @@ -611,7 +611,7 @@
      % if members: % for member in members: -
    • ${link(member, with_prefixes=True, css_classes="sidebar-nav-pill", dotted=False)}
    • +
    • ${link(member, with_prefixes=True, css_classes="sidebar-nav-pill", dotted=False)}
    • % endfor % endif
    From f0edbc665d2db054077c92002d994ec1ff11c424 Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Thu, 18 Jun 2020 18:46:09 +0100 Subject: [PATCH 543/922] Removed unused typehints from entity_factory.py --- hikari/impl/entity_factory.py | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/hikari/impl/entity_factory.py b/hikari/impl/entity_factory.py index 7941fb4a3b..0b6600bf0e 100644 --- a/hikari/impl/entity_factory.py +++ b/hikari/impl/entity_factory.py @@ -78,17 +78,6 @@ class EntityFactoryComponentImpl(entity_factory.IEntityFactoryComponent): This will convert objects to/from JSON compatible representations. """ - if typing.TYPE_CHECKING: - DMChannelT = typing.TypeVar("DMChannelT", bound=channel_models.DMChannel) - GuildChannelT = typing.TypeVar("GuildChannelT", bound=channel_models.GuildChannel) - InviteT = typing.TypeVar("InviteT", bound=invite_models.Invite) - PartialChannelT = typing.TypeVar("PartialChannelT", bound=channel_models.PartialChannel) - PartialGuildT = typing.TypeVar("PartialGuildT", bound=guild_models.PartialGuild) - PartialGuildIntegrationT = typing.TypeVar("PartialGuildIntegrationT", bound=guild_models.PartialIntegration) - UserT = typing.TypeVar("UserT", bound=user_models.User) - ReactionEventT = typing.TypeVar("ReactionEventT", bound=message_events.MessageReactionEvent) - GuildBanEventT = typing.TypeVar("GuildBanEventT", bound=guild_events.GuildBanEvent) - def __init__(self, app: rest.IRESTApp) -> None: self._app = app self._audit_log_entry_converters: typing.Mapping[str, typing.Callable[[typing.Any], typing.Any]] = { From 027a653c3149262bafb40d3146ec59d6999beb54 Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Thu, 18 Jun 2020 19:15:46 +0100 Subject: [PATCH 544/922] Made entity factory clearer so I don't make idiotic mistakes when misunderstanding logic. --- hikari/impl/entity_factory.py | 70 +++++++++++++++--------- tests/hikari/impl/test_entity_factory.py | 7 +++ 2 files changed, 52 insertions(+), 25 deletions(-) diff --git a/hikari/impl/entity_factory.py b/hikari/impl/entity_factory.py index 0b6600bf0e..54e8136d64 100644 --- a/hikari/impl/entity_factory.py +++ b/hikari/impl/entity_factory.py @@ -780,11 +780,13 @@ def deserialize_member( guild_member.nickname = payload["nick"] if "nick" in payload else undefined.UNDEFINED - if (premium_since := payload.get("premium_since", ...)) is not None and premium_since is not ...: - premium_since = date.iso8601_datetime_string_to_datetime(premium_since) - elif premium_since is ...: - premium_since = undefined.UNDEFINED - guild_member.premium_since = premium_since + if "premium_since" in payload: + raw_premium_since = payload["premium_since"] + guild_member.premium_since = ( + date.iso8601_datetime_string_to_datetime(raw_premium_since) if raw_premium_since is not None else None + ) + else: + guild_member.premium_since = undefined.UNDEFINED guild_member.is_deaf = payload["deaf"] if "deaf" in payload else undefined.UNDEFINED guild_member.is_mute = payload["mute"] if "mute" in payload else undefined.UNDEFINED @@ -1030,7 +1032,8 @@ def deserialize_vanity_url(self, payload: data_binding.JSONObject) -> invite_mod def _set_invite_attributes(self, payload: data_binding.JSONObject, invite: invite_models.Invite) -> None: invite.code = payload["code"] - if (guild_payload := payload.get("guild", ...)) is not ...: + if "guild" in payload: + guild_payload = payload["guild"] guild = invite_models.InviteGuild() guild.app = self._app self._set_partial_guild_attributes(guild_payload, guild) @@ -1042,9 +1045,9 @@ def _set_invite_attributes(self, payload: data_binding.JSONObject, invite: invit guild.vanity_url_code = guild_payload["vanity_url_code"] invite.guild = guild invite.guild_id = guild.id - elif (guild_id := payload.get("guild_id", ...)) is not ...: + elif "guild_id" in payload: invite.guild = None - invite.guild_id = snowflake.Snowflake(guild_id) + invite.guild_id = snowflake.Snowflake(payload["guild_id"]) else: invite.guild = invite.guild_id = None @@ -1145,7 +1148,8 @@ def deserialize_message(self, payload: data_binding.JSONObject) -> message_model # noinspection PyArgumentList message.type = message_models.MessageType(payload["type"]) - if (activity_payload := payload.get("activity", ...)) is not ...: + if "activity" in payload: + activity_payload = payload["activity"] activity = message_models.MessageActivity() # noinspection PyArgumentList activity.type = message_models.MessageActivityType(activity_payload["type"]) @@ -1156,7 +1160,8 @@ def deserialize_message(self, payload: data_binding.JSONObject) -> message_model message.application = self.deserialize_application(payload["application"]) if "application" in payload else None - if (crosspost_payload := payload.get("message_reference", ...)) is not ...: + if "message_reference" in payload: + crosspost_payload = payload["message_reference"] crosspost = message_models.MessageCrosspost() crosspost.app = self._app crosspost.id = ( @@ -1219,7 +1224,8 @@ def deserialize_member_presence(self, payload: data_binding.JSONObject) -> prese activity.created_at = date.unix_epoch_to_datetime(activity_payload["created_at"]) - if (timestamps_payload := activity_payload.get("timestamps", ...)) is not ...: + if "timestamps" in activity_payload: + timestamps_payload = activity_payload["timestamps"] timestamps = presence_models.ActivityTimestamps() timestamps.start = ( date.unix_epoch_to_datetime(timestamps_payload["start"]) if "start" in timestamps_payload else None @@ -1241,15 +1247,18 @@ def deserialize_member_presence(self, payload: data_binding.JSONObject) -> prese if (emoji := activity_payload.get("emoji")) is not None: emoji = self.deserialize_emoji(emoji) + activity.emoji = emoji - if (party_payload := activity_payload.get("party", ...)) is not ...: + if "party" in activity_payload: + party_payload = activity_payload["party"] party = presence_models.ActivityParty() party.id = party_payload.get("id") - if (size := party_payload.get("size", ...)) is not ...: - party.current_size = int(size[0]) - party.max_size = int(size[1]) + if "size" in party_payload: + current_size, max_size = party_payload["size"] + party.current_size = int(current_size) + party.max_size = int(max_size) else: party.current_size = party.max_size = None @@ -1257,7 +1266,8 @@ def deserialize_member_presence(self, payload: data_binding.JSONObject) -> prese else: activity.party = None - if (assets_payload := activity_payload.get("assets", ...)) is not ...: + if "assets" in activity_payload: + assets_payload = activity_payload["assets"] assets = presence_models.ActivityAssets() assets.large_image = assets_payload.get("large_image") assets.large_text = assets_payload.get("large_text") @@ -1267,7 +1277,8 @@ def deserialize_member_presence(self, payload: data_binding.JSONObject) -> prese else: activity.assets = None - if (secrets_payload := activity_payload.get("secrets", ...)) is not ...: + if "secrets" in activity_payload: + secrets_payload = activity_payload["secrets"] secret = presence_models.ActivitySecret() secret.join = secrets_payload.get("join") secret.spectate = secrets_payload.get("spectate") @@ -1432,8 +1443,10 @@ def deserialize_channel_pins_update_event( channel_pins_update.guild_id = snowflake.Snowflake(payload["guild_id"]) if "guild_id" in payload else None channel_pins_update.channel_id = snowflake.Snowflake(payload["channel_id"]) - if (last_pin_timestamp := payload.get("last_pin_timestamp", ...)) is not ...: - channel_pins_update.last_pin_timestamp = date.iso8601_datetime_string_to_datetime(last_pin_timestamp) + if "last_pin_timestamp" in payload: + channel_pins_update.last_pin_timestamp = date.iso8601_datetime_string_to_datetime( + payload["last_pin_timestamp"] + ) else: channel_pins_update.last_pin_timestamp = None @@ -1625,10 +1638,14 @@ def deserialize_message_update_event(self, payload: data_binding.JSONObject) -> else undefined.UNDEFINED ) - if (edited_timestamp := payload.get("edited_timestamp", ...)) is not ... and edited_timestamp is not None: - edited_timestamp = date.iso8601_datetime_string_to_datetime(edited_timestamp) - elif edited_timestamp is ...: + if "edited_timestamp" in payload: + if (edited_timestamp := payload["edited_timestamp"]) is not None: + edited_timestamp = date.iso8601_datetime_string_to_datetime(edited_timestamp) + else: + edited_timestamp = None + else: edited_timestamp = undefined.UNDEFINED + updated_message.edited_timestamp = edited_timestamp updated_message.is_tts = payload["tts"] if "tts" in payload else undefined.UNDEFINED @@ -1692,7 +1709,8 @@ def deserialize_message_update_event(self, payload: data_binding.JSONObject) -> # noinspection PyArgumentList updated_message.type = message_models.MessageType(payload["type"]) if "type" in payload else undefined.UNDEFINED - if (activity_payload := payload.get("activity", ...)) is not ...: + if "activity" in payload: + activity_payload = payload["activity"] activity = message_models.MessageActivity() # noinspection PyArgumentList activity.type = message_models.MessageActivityType(activity_payload["type"]) @@ -1705,7 +1723,8 @@ def deserialize_message_update_event(self, payload: data_binding.JSONObject) -> self.deserialize_application(payload["application"]) if "application" in payload else undefined.UNDEFINED ) - if (crosspost_payload := payload.get("message_reference", ...)) is not ...: + if "message_reference" in payload: + crosspost_payload = payload["message_reference"] crosspost = message_models.MessageCrosspost() crosspost.app = self._app crosspost.id = ( @@ -1808,7 +1827,8 @@ def deserialize_ready_event( } ready_event.session_id = payload["session_id"] - if (shard_data := payload.get("shard", ...)) is not ...: + # Shouldn't ever be none, but if it is, we don't care. + if (shard_data := payload.get("shard")) is not None: ready_event.shard_id = int(shard_data[0]) ready_event.shard_count = int(shard_data[1]) else: diff --git a/tests/hikari/impl/test_entity_factory.py b/tests/hikari/impl/test_entity_factory.py index 920d8d1479..291b600d15 100644 --- a/tests/hikari/impl/test_entity_factory.py +++ b/tests/hikari/impl/test_entity_factory.py @@ -2165,17 +2165,20 @@ def test_deserialize_member_presence( assert activity.state == "STATED" assert activity.emoji == entity_factory_impl.deserialize_emoji(custom_emoji_payload) # ActivityParty + assert activity.party is not None assert activity.party.id == "spotify:3234234234" assert activity.party.current_size == 2 assert activity.party.max_size == 5 assert isinstance(activity.party, presence_models.ActivityParty) # ActivityAssets + assert activity.assets is not None assert activity.assets.large_image == "34234234234243" assert activity.assets.large_text == "LARGE TEXT" assert activity.assets.small_image == "3939393" assert activity.assets.small_text == "small text" assert isinstance(activity.assets, presence_models.ActivityAssets) # ActivitySecrets + assert activity.secrets is not None assert activity.secrets.join == "who's a good secret?" assert activity.secrets.spectate == "I'm a good secret" assert activity.secrets.match == "No." @@ -2340,18 +2343,22 @@ def test_deserialize_member_presence_with_unset_activity_sub_fields(self, entity ) activity = presence.activities[0] # ActivityTimestamps + assert activity.timestamps is not None assert activity.timestamps.start is None assert activity.timestamps.end is None # ActivityParty + assert activity.party is not None assert activity.party.id is None assert activity.party.max_size is None assert activity.party.current_size is None # ActivityAssets + assert activity.assets is not None assert activity.assets.small_text is None assert activity.assets.small_image is None assert activity.assets.large_text is None assert activity.assets.large_image is None # ActivitySecrets + assert activity.secrets is not None assert activity.secrets.join is None assert activity.secrets.spectate is None assert activity.secrets.match is None From 84658b983cf7adfd0a4d8be0cb41a6a5ae532e52 Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Thu, 18 Jun 2020 19:43:34 +0100 Subject: [PATCH 545/922] Fixed other issues Snab doesn't want. --- hikari/impl/entity_factory.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/hikari/impl/entity_factory.py b/hikari/impl/entity_factory.py index 54e8136d64..44ea41dab8 100644 --- a/hikari/impl/entity_factory.py +++ b/hikari/impl/entity_factory.py @@ -957,9 +957,9 @@ def deserialize_guild(self, payload: data_binding.JSONObject) -> guild_models.Gu guild.is_large = payload["large"] if "large" in payload else None guild.member_count = int(payload["member_count"]) if "member_count" in payload else None - if members := payload.get("members"): + if "members" in payload: guild.members = {} - for member_payload in members: + for member_payload in payload["members"]: member = self.deserialize_member(member_payload) # Could be None, so cast to avoid. user_id = typing.cast("user_models.User", member.user).id @@ -968,17 +968,17 @@ def deserialize_guild(self, payload: data_binding.JSONObject) -> guild_models.Gu # FIXME: should this be an empty dict instead? guild.members = None - if channels := payload.get("channels"): + if "channels" in payload: guild.channels = {} - for channel_payload in channels: + for channel_payload in payload["channels"]: channel = typing.cast("channel_models.GuildChannel", self.deserialize_channel(channel_payload)) guild.channels[channel.id] = channel else: guild.channels = None - if presences := payload.get("presences"): + if "presences" in payload: guild.presences = {} - for presence_payload in presences: + for presence_payload in payload["presences"]: presence = self.deserialize_member_presence(presence_payload) guild.presences[presence.user.id] = presence else: From 31e250699d1863dfcc980eb53108148728ed1c58 Mon Sep 17 00:00:00 2001 From: davfsa Date: Thu, 18 Jun 2020 19:22:00 +0200 Subject: [PATCH 546/922] Implement update in guild ban + prune endpoints --- hikari/net/rest.py | 107 ++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 97 insertions(+), 10 deletions(-) diff --git a/hikari/net/rest.py b/hikari/net/rest.py index 04c920ab89..f5f69f40c6 100644 --- a/hikari/net/rest.py +++ b/hikari/net/rest.py @@ -2138,10 +2138,13 @@ async def ban_user( guild: typing.Union[guilds.Guild, snowflake.UniqueObject], user: typing.Union[users.User, snowflake.UniqueObject], *, + delete_message_days: typing.Union[undefined.UndefinedType, int] = undefined.UNDEFINED, reason: typing.Union[undefined.UndefinedType, str] = undefined.UNDEFINED, ) -> None: + body = data_binding.JSONObjectBuilder() + body.put("delete_message_days", delete_message_days) route = routes.PUT_GUILD_BAN.compile(guild=guild, user=user) - await self._request(route, reason=reason) + await self._request(route, reason=reason, body=body) async def unban_user( self, @@ -2256,11 +2259,52 @@ async def delete_role( await self._request(route) async def estimate_guild_prune_count( - self, guild: typing.Union[guilds.Guild, snowflake.UniqueObject], days: int, + self, + guild: typing.Union[guilds.Guild, snowflake.UniqueObject], + *, + days: typing.Union[undefined.UndefinedType, int] = undefined.UNDEFINED, + include_roles: typing.Union[ + undefined.UndefinedType, typing.Collection[typing.Union[guilds.Role, snowflake.UniqueObject]] + ] = undefined.UNDEFINED, ) -> int: + """Estimate the guild prune count. + + Parameters + ---------- + guild : hikari.models.guilds.Guild or hikari.utilities.snowflake.Snowflake or int or str + The guild to estimate the guild prune count for. This may be a guild object, + or the ID of an existing channel. + days : hikari.utilities.undefined.UndefinedType or int + If provided, number of days to count prune for. + include_roles : hikari.utilities.undefined.UndefinedType or typing.Collection[hikari.guilds.Role or hikari.utilities.snowflake.Snowflake or int or str] + If provided, the role(s) to include. By default, this endpoint will not count + users with roles. Providing roles using this attribute will make members with + the specified roles also get included into the count. + + Returns + ------- + int + The estimated guild prune count. + + Raises + ------ + hikari.errors.BadRequest + If any of the fields that are passed have an invalid value. + hikari.errors.Unauthorized + If you are unauthorized to make the request (invalid/missing token). + hikari.errors.Forbidden + If you lack the `KICK_MEMBERS` permission. + hikari.errors.NotFound + If the guild is not found. + hikari.errors.ServerHTTPErrorResponse + If an internal error occurs on Discord while handling the request. + """ # noqa: E501 - Line too long route = routes.GET_GUILD_PRUNE.compile(guild=guild) query = data_binding.StringMapBuilder() query.put("days", days) + if include_roles is not undefined.UNDEFINED: + roles = ",".join(str(int(role)) for role in include_roles) + query.put("include_roles", roles) raw_response = await self._request(route, query=query) response = typing.cast(data_binding.JSONObject, raw_response) return int(response["pruned"]) @@ -2268,17 +2312,60 @@ async def estimate_guild_prune_count( async def begin_guild_prune( self, guild: typing.Union[guilds.Guild, snowflake.UniqueObject], - days: int, *, - reason: typing.Union[undefined.UndefinedType, str], - ) -> int: + days: typing.Union[undefined.UndefinedType, int] = undefined.UNDEFINED, + compute_prune_count: typing.Union[undefined.UndefinedType, bool] = undefined.UNDEFINED, + include_roles: typing.Union[ + undefined.UndefinedType, typing.Collection[typing.Union[guilds.Role, snowflake.UniqueObject]] + ] = undefined.UNDEFINED, + reason: typing.Union[undefined.UndefinedType, str] = undefined.UNDEFINED, + ) -> typing.Optional[int]: + """Begin the guild prune. + + Parameters + ---------- + guild : hikari.models.guilds.Guild or hikari.utilities.snowflake.Snowflake or int or str + The guild to begin the guild prune in. This may be a guild object, + or the ID of an existing channel. + days : hikari.utilities.undefined.UndefinedType or int + If provided, number of days to count prune for. + compute_prune_count: hikari.utilities.undefined.UndefinedType or bool + If provided, whether to return the prune count. This is discouraged for large + guilds. + include_roles : hikari.utilities.undefined.UndefinedType or typing.Collection[hikari.guilds.Role or hikari.utilities.snowflake.Snowflake or int or str] + If provided, the role(s) to include. By default, this endpoint will not count + users with roles. Providing roles using this attribute will make members with + the specified roles also get included into the count. + reason : hikari.utilities.undefined.UndefinedType or str + If provided, the reason that will be recorded in the audit logs. + + Returns + ------- + int or None + If `compute_prune_count` is not provided or `True`, the number of members pruned. + + Raises + ------ + hikari.errors.BadRequest + If any of the fields that are passed have an invalid value. + hikari.errors.Unauthorized + If you are unauthorized to make the request (invalid/missing token). + hikari.errors.Forbidden + If you lack the `KICK_MEMBERS` permission. + hikari.errors.NotFound + If the guild is not found. + hikari.errors.ServerHTTPErrorResponse + If an internal error occurs on Discord while handling the request. + """ # noqa: E501 - Line too long route = routes.POST_GUILD_PRUNE.compile(guild=guild) - query = data_binding.StringMapBuilder() - query.put("compute_prune_count", True) - query.put("days", days) - raw_response = await self._request(route, query=query, reason=reason) + body = data_binding.JSONObjectBuilder() + body.put("days", days) + body.put("compute_prune_count", compute_prune_count) + body.put_snowflake_array("include_roles", include_roles) + raw_response = await self._request(route, body=body, reason=reason) response = typing.cast(data_binding.JSONObject, raw_response) - return int(response["pruned"]) + pruned = response.get("pruned") + return int(pruned) if pruned is not None else None async def fetch_guild_voice_regions( self, guild: typing.Union[guilds.Guild, snowflake.UniqueObject], / From 35a27452236accec87dd90743146d5d8a1256cae Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Thu, 18 Jun 2020 19:05:06 +0000 Subject: [PATCH 547/922] Update __main__.py to fix entrypoint bug. --- hikari/__main__.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/hikari/__main__.py b/hikari/__main__.py index 4139f17d6b..eadae7d7a2 100644 --- a/hikari/__main__.py +++ b/hikari/__main__.py @@ -30,6 +30,9 @@ from hikari import _about +def main(): + pass # exists to keep setup.py happy for entrypoint + sourcefile = typing.cast(str, inspect.getsourcefile(_about)) path: typing.Final[str] = os.path.abspath(os.path.dirname(sourcefile)) version: typing.Final[str] = _about.__version__ From 62ea2a22a8b89bbca7774035e5ca65c2583bee8f Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Thu, 18 Jun 2020 20:56:06 +0100 Subject: [PATCH 548/922] Changed console entrypoint to use cli-pattern. --- README.md | 26 ------------------------- hikari/__main__.py | 24 ++--------------------- hikari/cli.py | 41 ++++++++++++++++++++++++++++++++++++++++ hikari/models/guilds.py | 2 ++ setup.py | 3 +-- speedup-requirements.txt | 1 - 6 files changed, 46 insertions(+), 51 deletions(-) create mode 100644 hikari/cli.py delete mode 100644 speedup-requirements.txt diff --git a/README.md b/README.md index fffbda8427..4ec7f78f3b 100644 --- a/README.md +++ b/README.md @@ -43,32 +43,6 @@ python -m pip install hikari -U --pre py -3 -m pip install hikari -U --pre ``` -### Moar poweeeerrrr - -If you wish to get the most out of your bot, you should opt-in to -installing the speedups extensions. - -```bash -python -m pip install hikari[speedups] -U --pre -``` - -This may take a little longer to install, but will replace several dependencies -with much faster alternatives, including: - -- [`aiodns`](https://pypi.org/project/aiodns/) - Asynchronous DNS lookups using - `pycares` (`libcares` Python bindings). -- [`cchardet`](https://pypi.org/project/cchardet/) - a compiled C implementation - of the [`chardet`](https://pypi.org/project/chardet/) module. Claims - to handle almost 1468 calls per second in a benchmark, compared to - 0.35 calls per second from the default `chardet` module, which is around - 4193x faster.\* - -\* _`cchardet` v2.1.6 Python 3.6.1, Intel(R) Core(TM) i5-4690 CPU @ 3.50GHz, -16GB 1.6GHz DDR3, Ubuntu 16.04 AMD64._ - -Note that you may find you need to install a C compiler on your machine to make -use of these extensions. - ---- ## What does _hikari_ aim to do? diff --git a/hikari/__main__.py b/hikari/__main__.py index eadae7d7a2..ba737e1c73 100644 --- a/hikari/__main__.py +++ b/hikari/__main__.py @@ -16,27 +16,7 @@ # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . """Provides a command-line entry point that shows the library version and then exits.""" -from __future__ import annotations +from hikari import cli -__all__: typing.Final[typing.List[str]] = [] -import inspect -import os -import platform -import sys - -# noinspection PyUnresolvedReferences -import typing - -from hikari import _about - -def main(): - pass # exists to keep setup.py happy for entrypoint - -sourcefile = typing.cast(str, inspect.getsourcefile(_about)) -path: typing.Final[str] = os.path.abspath(os.path.dirname(sourcefile)) -version: typing.Final[str] = _about.__version__ -py_impl: typing.Final[str] = platform.python_implementation() -py_ver: typing.Final[str] = platform.python_version() -py_compiler: typing.Final[str] = platform.python_compiler() -sys.stderr.write(f"hikari v{version} (installed in {path}) ({py_impl} {py_ver} {py_compiler})\n") +cli.main() diff --git a/hikari/cli.py b/hikari/cli.py new file mode 100644 index 0000000000..8b2a9bbb39 --- /dev/null +++ b/hikari/cli.py @@ -0,0 +1,41 @@ +# -*- coding: utf-8 -*- +# Copyright © Nekoka.tt 2019-2020 +# +# This file is part of Hikari. +# +# Hikari is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Hikari is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Hikari. If not, see . +"""Provides the `python -m hikari` and `hikari` commands to the shell.""" + +from __future__ import annotations + +import inspect +import os +import platform +import sys + +# noinspection PyUnresolvedReferences +import typing + +from hikari import _about + + +def main() -> None: + # noinspection PyTypeChecker + sourcefile = typing.cast(str, inspect.getsourcefile(_about)) + path: typing.Final[str] = os.path.abspath(os.path.dirname(sourcefile)) + version: typing.Final[str] = _about.__version__ + py_impl: typing.Final[str] = platform.python_implementation() + py_ver: typing.Final[str] = platform.python_version() + py_compiler: typing.Final[str] = platform.python_compiler() + sys.stderr.write(f"hikari v{version} (installed in {path}) ({py_impl} {py_ver} {py_compiler})\n") diff --git a/hikari/models/guilds.py b/hikari/models/guilds.py index 6aec8a0dc4..38d966cba3 100644 --- a/hikari/models/guilds.py +++ b/hikari/models/guilds.py @@ -768,6 +768,7 @@ class Guild(PartialGuild): # pylint:disable=too-many-instance-attributes channels: typing.Optional[typing.Mapping[snowflake.Snowflake, channels_.GuildChannel]] = attr.ib( eq=False, hash=False, repr=False ) + """A mapping of ID to the corresponding guild channels in this guild. This information is only available if the guild was sent via a `GUILD_CREATE` @@ -787,6 +788,7 @@ class Guild(PartialGuild): # pylint:disable=too-many-instance-attributes presences: typing.Optional[typing.Mapping[snowflake.Snowflake, presences.MemberPresence]] = attr.ib( eq=False, hash=False, repr=False ) + """A mapping of member ID to the corresponding presence information for the given member, if available. diff --git a/setup.py b/setup.py index db910fa556..2696ef1730 100644 --- a/setup.py +++ b/setup.py @@ -92,7 +92,6 @@ def parse_requirements_file(path): "Topic :: Software Development :: Libraries :: Python Modules", "Typing :: Typed", ], - entry_points={"console_scripts": ["hikari = hikari.__main__:main"]}, - extras_require={"speedups": parse_requirements_file("speedup-requirements.txt"),}, + entry_points={"console_scripts": ["hikari = hikari.cli:main"]}, provides="hikari", ) diff --git a/speedup-requirements.txt b/speedup-requirements.txt deleted file mode 100644 index 52ca66a0d3..0000000000 --- a/speedup-requirements.txt +++ /dev/null @@ -1 +0,0 @@ -aiohttp[speedups]~=3.6.2 From 0e72fa0ac48606cce787e391e5cb2bdb9e8a1f38 Mon Sep 17 00:00:00 2001 From: davfsa Date: Thu, 18 Jun 2020 23:11:31 +0200 Subject: [PATCH 549/922] Fixed flake8 violations --- .flake8 | 10 +- ci/flake8.nox.py | 6 +- hikari/__main__.py | 1 - hikari/cli.py | 1 + hikari/events/message.py | 2 +- hikari/impl/bot.py | 4 +- hikari/impl/entity_factory.py | 162 ++++++++++------------- hikari/impl/event_manager_core.py | 2 +- hikari/impl/gateway_zookeeper.py | 4 +- hikari/models/embeds.py | 10 +- hikari/models/guilds.py | 6 +- hikari/models/messages.py | 8 +- hikari/models/webhooks.py | 23 +--- hikari/net/gateway.py | 8 +- hikari/net/http_client.py | 2 +- hikari/net/iterators.py | 2 +- hikari/net/rate_limits.py | 2 +- hikari/net/rest.py | 96 ++++++-------- hikari/net/rest_utils.py | 6 +- hikari/net/routes.py | 11 +- hikari/net/tracing.py | 9 +- hikari/utilities/aio.py | 4 +- hikari/utilities/date.py | 9 +- hikari/utilities/files.py | 6 +- hikari/utilities/snowflake.py | 2 +- tests/hikari/impl/test_entity_factory.py | 6 +- 26 files changed, 177 insertions(+), 225 deletions(-) diff --git a/.flake8 b/.flake8 index 3bcc3dde42..bb83a0d63a 100644 --- a/.flake8 +++ b/.flake8 @@ -3,12 +3,18 @@ count = true ignore = E402, # Module level import not at top of file (isn't compatible with our import style). - D105, # Magic methods not having a docstring - D102, # Missing docstring in public method + D105, # Magic methods not having a docstring. + D102, # Missing docstring in public method. + A002, # Argument is shadowing a python builtin. + A003, # Class attribute is shadowing a python builtin. + CFQ002, # Function has too many arguments. + per-file-ignores = # f-string missing prefix. hikari/net/routes.py:FS003 + # f-string missing prefix. + hikari/utilities/date.py:FS003 # complaints about importing stuff and not using it afterwards hikari/__init__.py:F401 diff --git a/ci/flake8.nox.py b/ci/flake8.nox.py index c6df28b101..09416bcdbe 100644 --- a/ci/flake8.nox.py +++ b/ci/flake8.nox.py @@ -16,6 +16,7 @@ # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . import os +import shutil from ci import config from ci import nox @@ -27,7 +28,7 @@ def flake8(session: nox.Session) -> None: session.install("-r", "requirements.txt", "-r", "flake-requirements.txt") session.run( - "flake8", "--format=html", f"--htmldir={config.FLAKE8_HTML}", config.MAIN_PACKAGE, success_codes=range(0, 256), + "flake8", "--exit-zero", "--format=html", f"--htmldir={config.FLAKE8_HTML}", config.MAIN_PACKAGE, ) if "GITLAB_CI" in os.environ or "--gitlab" in session.posargs: @@ -38,6 +39,9 @@ def flake8(session: nox.Session) -> None: format_args = ["--format=junit-xml", f"--output-file={config.FLAKE8_JUNIT}"] else: format_args = [f"--output-file={config.FLAKE8_TXT}", "--statistics", "--show-source"] + # This is because flake8 just appends to the file, so you can end up with + # a huge file with the same errors if you run it a couple of times. + shutil.rmtree(config.FLAKE8_TXT, ignore_errors=True) session.run( "flake8", *format_args, config.MAIN_PACKAGE, diff --git a/hikari/__main__.py b/hikari/__main__.py index ba737e1c73..726adafb8b 100644 --- a/hikari/__main__.py +++ b/hikari/__main__.py @@ -18,5 +18,4 @@ """Provides a command-line entry point that shows the library version and then exits.""" from hikari import cli - cli.main() diff --git a/hikari/cli.py b/hikari/cli.py index 8b2a9bbb39..d64b51c86b 100644 --- a/hikari/cli.py +++ b/hikari/cli.py @@ -31,6 +31,7 @@ def main() -> None: + """Print package info and exit.""" # noinspection PyTypeChecker sourcefile = typing.cast(str, inspect.getsourcefile(_about)) path: typing.Final[str] = os.path.abspath(os.path.dirname(sourcefile)) diff --git a/hikari/events/message.py b/hikari/events/message.py index a85f208b68..be0c277eeb 100644 --- a/hikari/events/message.py +++ b/hikari/events/message.py @@ -107,7 +107,7 @@ class UpdatedMessageFields(snowflake.Unique): edited_timestamp: typing.Union[datetime.datetime, undefined.UndefinedType, None] = attr.ib(repr=False) """The timestamp that the message was last edited at. - Will be `None` if the message wasn't ever edited, or `undefined` if the + Will be `None` if the message wasn't ever edited, or `undefined` if the info is not available. """ diff --git a/hikari/impl/bot.py b/hikari/impl/bot.py index 6b8354f580..1f7ea56aa4 100644 --- a/hikari/impl/bot.py +++ b/hikari/impl/bot.py @@ -43,7 +43,7 @@ from hikari.api import cache as cache_ from hikari.api import entity_factory as entity_factory_ from hikari.api import event_consumer as event_consumer_ - from hikari.api import event_dispatcher + from hikari.api import event_dispatcher as event_dispatcher_ from hikari.events import base as base_events from hikari.models import gateway as gateway_models from hikari.models import intents as intents_ @@ -193,7 +193,7 @@ def __init__( ) @property - def event_dispatcher(self) -> event_dispatcher.IEventDispatcherComponent: + def event_dispatcher(self) -> event_dispatcher_.IEventDispatcherComponent: return self._event_manager @property diff --git a/hikari/impl/entity_factory.py b/hikari/impl/entity_factory.py index 44ea41dab8..afcbb0b51b 100644 --- a/hikari/impl/entity_factory.py +++ b/hikari/impl/entity_factory.py @@ -87,7 +87,7 @@ def __init__(self, app: rest.IRESTApp) -> None: audit_log_models.AuditLogChangeKey.MFA_LEVEL: guild_models.GuildMFALevel, audit_log_models.AuditLogChangeKey.VERIFICATION_LEVEL: guild_models.GuildVerificationLevel, audit_log_models.AuditLogChangeKey.EXPLICIT_CONTENT_FILTER: guild_models.GuildExplicitContentFilterLevel, - audit_log_models.AuditLogChangeKey.DEFAULT_MESSAGE_NOTIFICATIONS: guild_models.GuildMessageNotificationsLevel, + audit_log_models.AuditLogChangeKey.DEFAULT_MESSAGE_NOTIFICATIONS: guild_models.GuildMessageNotificationsLevel, # noqa: E501 - Line too long audit_log_models.AuditLogChangeKey.PRUNE_DELETE_DAYS: _deserialize_day_timedelta, audit_log_models.AuditLogChangeKey.WIDGET_CHANNEL_ID: snowflake.Snowflake, audit_log_models.AuditLogChangeKey.POSITION: int, @@ -353,7 +353,7 @@ def deserialize_audit_log(self, payload: data_binding.JSONObject) -> audit_log_m if (options := entry_payload.get("options")) is not None: option_converter = ( self._audit_log_event_mapping.get(entry.action_type) - or self._deserialize_unrecognised_audit_log_entry_info + or self._deserialize_unrecognised_audit_log_entry_info # noqa: W503 ) options = option_converter(options) entry.options = options @@ -604,7 +604,7 @@ def deserialize_embed(self, payload: data_binding.JSONObject) -> embed_models.Em return embed - def serialize_embed( + def serialize_embed( # noqa: C901 self, embed: embed_models.Embed, ) -> typing.Tuple[data_binding.JSONObject, typing.List[files.Resource]]: @@ -679,9 +679,7 @@ def serialize_embed( for field in embed.fields: # Name and value always have to be specified; we can always # send a default `inline` value also just to keep this simpler. - field_payloads.append( - {"name": field.name, "value": field.value, "inline": field.is_inline,} - ) + field_payloads.append({"name": field.name, "value": field.value, "inline": field.is_inline}) payload["fields"] = field_payloads return payload, uploads @@ -729,7 +727,7 @@ def deserialize_emoji( return self.deserialize_unicode_emoji(payload) ################## - # GATEWAY MODELS # # TODO: rename to gateways? + # GATEWAY MODELS # ################## def deserialize_gateway_bot(self, payload: data_binding.JSONObject) -> gateway_models.GatewayBot: @@ -773,10 +771,10 @@ def deserialize_member( guild_member.user = user or self.deserialize_user(payload["user"]) guild_member.role_ids = {snowflake.Snowflake(role_id) for role_id in payload["roles"]} - if (joined_at := payload.get("joined_at")) is not None: - guild_member.joined_at = date.iso8601_datetime_string_to_datetime(joined_at) - else: - guild_member.joined_at = undefined.UNDEFINED + joined_at = payload.get("joined_at") + guild_member.joined_at = ( + date.iso8601_datetime_string_to_datetime(joined_at) if joined_at is not None else undefined.UNDEFINED + ) guild_member.nickname = payload["nick"] if "nick" in payload else undefined.UNDEFINED @@ -886,7 +884,7 @@ def deserialize_guild_preview(self, payload: data_binding.JSONObject) -> guild_m guild_preview.description = payload["description"] return guild_preview - def deserialize_guild(self, payload: data_binding.JSONObject) -> guild_models.Guild: + def deserialize_guild(self, payload: data_binding.JSONObject) -> guild_models.Guild: # noqa: CFQ001 guild = guild_models.Guild() self._set_partial_guild_attributes(payload, guild) guild.app = self._app @@ -895,24 +893,20 @@ def deserialize_guild(self, payload: data_binding.JSONObject) -> guild_models.Gu guild.owner_id = snowflake.Snowflake(payload["owner_id"]) # noinspection PyArgumentList - if (perms := payload.get("permissions")) is not None: - # noinspection PyArgumentList - guild.my_permissions = permission_models.Permission(perms) - else: - guild.my_permissions = None + guild.my_permissions = ( + permission_models.Permission(payload["permissions"]) if "permissions" in payload else None + ) guild.region = payload["region"] - if (afk_channel_id := payload["afk_channel_id"]) is not None: - afk_channel_id = snowflake.Snowflake(afk_channel_id) - guild.afk_channel_id = afk_channel_id + afk_channel_id = payload["afk_channel_id"] + guild.afk_channel_id = snowflake.Snowflake(afk_channel_id) if afk_channel_id is not None else None guild.afk_timeout = datetime.timedelta(seconds=payload["afk_timeout"]) guild.is_embed_enabled = payload.get("embed_enabled", False) - if (embed_channel_id := payload.get("embed_channel_id")) is not None: - embed_channel_id = snowflake.Snowflake(embed_channel_id) - guild.embed_channel_id = embed_channel_id + embed_channel_id = payload.get("embed_channel_id") + guild.embed_channel_id = snowflake.Snowflake(embed_channel_id) if embed_channel_id is not None else None # noinspection PyArgumentList guild.verification_level = guild_models.GuildVerificationLevel(payload["verification_level"]) @@ -929,27 +923,23 @@ def deserialize_guild(self, payload: data_binding.JSONObject) -> guild_models.Gu # noinspection PyArgumentList guild.mfa_level = guild_models.GuildMFALevel(payload["mfa_level"]) - if (application_id := payload["application_id"]) is not None: - application_id = snowflake.Snowflake(application_id) - guild.application_id = application_id + application_id = payload["application_id"] + guild.application_id = snowflake.Snowflake(application_id) if application_id is not None else None guild.is_unavailable = payload["unavailable"] if "unavailable" in payload else None guild.is_widget_enabled = payload["widget_enabled"] if "widget_enabled" in payload else None - if (widget_channel_id := payload.get("widget_channel_id")) is not None: - widget_channel_id = snowflake.Snowflake(widget_channel_id) - guild.widget_channel_id = widget_channel_id + widget_channel_id = payload.get("widget_channel_id") + guild.widget_channel_id = snowflake.Snowflake(widget_channel_id) if widget_channel_id is not None else None - if (system_channel_id := payload["system_channel_id"]) is not None: - system_channel_id = snowflake.Snowflake(system_channel_id) - guild.system_channel_id = system_channel_id + system_channel_id = payload["system_channel_id"] + guild.system_channel_id = snowflake.Snowflake(system_channel_id) if system_channel_id is not None else None # noinspection PyArgumentList guild.system_channel_flags = guild_models.GuildSystemChannelFlag(payload["system_channel_flags"]) - if (rules_channel_id := payload["rules_channel_id"]) is not None: - rules_channel_id = snowflake.Snowflake(rules_channel_id) - guild.rules_channel_id = rules_channel_id + rules_channel_id = payload["rules_channel_id"] + guild.rules_channel_id = snowflake.Snowflake(rules_channel_id) if rules_channel_id is not None else None guild.joined_at = ( date.iso8601_datetime_string_to_datetime(payload["joined_at"]) if "joined_at" in payload else None @@ -957,38 +947,28 @@ def deserialize_guild(self, payload: data_binding.JSONObject) -> guild_models.Gu guild.is_large = payload["large"] if "large" in payload else None guild.member_count = int(payload["member_count"]) if "member_count" in payload else None + guild.members = {} if "members" in payload: - guild.members = {} for member_payload in payload["members"]: member = self.deserialize_member(member_payload) # Could be None, so cast to avoid. user_id = typing.cast("user_models.User", member.user).id guild.members[user_id] = member - else: - # FIXME: should this be an empty dict instead? - guild.members = None + guild.channels = {} if "channels" in payload: - guild.channels = {} for channel_payload in payload["channels"]: channel = typing.cast("channel_models.GuildChannel", self.deserialize_channel(channel_payload)) guild.channels[channel.id] = channel - else: - guild.channels = None + guild.presences = {} if "presences" in payload: - guild.presences = {} for presence_payload in payload["presences"]: presence = self.deserialize_member_presence(presence_payload) guild.presences[presence.user.id] = presence - else: - # FIXME: should this be an empty dict instead? - guild.presences = None - - if (max_presences := payload.get("max_presences")) is not None: - max_presences = int(max_presences) - guild.max_presences = max_presences + max_presences = payload.get("max_presences") + guild.max_presences = int(max_presences) if max_presences is not None else None guild.max_members = int(payload["max_members"]) if "max_members" in payload else None guild.max_video_channel_users = ( @@ -1000,15 +980,14 @@ def deserialize_guild(self, payload: data_binding.JSONObject) -> guild_models.Gu # noinspection PyArgumentList guild.premium_tier = guild_models.GuildPremiumTier(payload["premium_tier"]) - if (premium_subscription_count := payload.get("premium_subscription_count")) is not None: - premium_subscription_count = int(premium_subscription_count) - guild.premium_subscription_count = premium_subscription_count + guild.premium_subscription_count = payload.get("premium_subscription_count") guild.preferred_locale = payload["preferred_locale"] - if (public_updates_channel_id := payload["public_updates_channel_id"]) is not None: - public_updates_channel_id = snowflake.Snowflake(public_updates_channel_id) - guild.public_updates_channel_id = public_updates_channel_id + public_updates_channel_id = payload["public_updates_channel_id"] + guild.public_updates_channel_id = ( + snowflake.Snowflake(public_updates_channel_id) if public_updates_channel_id is not None else None + ) guild.approximate_member_count = ( int(payload["approximate_member_count"]) if "approximate_member_count" in payload else None @@ -1184,7 +1163,9 @@ def deserialize_message(self, payload: data_binding.JSONObject) -> message_model # PRESENCE MODELS # ################### - def deserialize_member_presence(self, payload: data_binding.JSONObject) -> presence_models.MemberPresence: + def deserialize_member_presence( # noqa: CFQ001 + self, payload: data_binding.JSONObject + ) -> presence_models.MemberPresence: guild_member_presence = presence_models.MemberPresence() guild_member_presence.app = self._app user_payload = payload["user"] @@ -1204,10 +1185,11 @@ def deserialize_member_presence(self, payload: data_binding.JSONObject) -> prese ) guild_member_presence.user = user - if (role_ids := payload.get("roles")) is not None: - guild_member_presence.role_ids = {snowflake.Snowflake(role_id) for role_id in role_ids} - else: - guild_member_presence.role_ids = None + guild_member_presence.role_ids = ( + {snowflake.Snowflake(role_id) for role_id in role_ids} + if (role_ids := payload.get("roles")) is not None + else None + ) guild_member_presence.guild_id = snowflake.Snowflake(payload["guild_id"]) if "guild_id" in payload else None # noinspection PyArgumentList @@ -1224,6 +1206,7 @@ def deserialize_member_presence(self, payload: data_binding.JSONObject) -> prese activity.created_at = date.unix_epoch_to_datetime(activity_payload["created_at"]) + timestamps = None if "timestamps" in activity_payload: timestamps_payload = activity_payload["timestamps"] timestamps = presence_models.ActivityTimestamps() @@ -1233,9 +1216,7 @@ def deserialize_member_presence(self, payload: data_binding.JSONObject) -> prese timestamps.end = ( date.unix_epoch_to_datetime(timestamps_payload["end"]) if "end" in timestamps_payload else None ) - activity.timestamps = timestamps - else: - activity.timestamps = None + activity.timestamps = timestamps activity.application_id = ( snowflake.Snowflake(activity_payload["application_id"]) @@ -1245,11 +1226,10 @@ def deserialize_member_presence(self, payload: data_binding.JSONObject) -> prese activity.details = activity_payload.get("details") activity.state = activity_payload.get("state") - if (emoji := activity_payload.get("emoji")) is not None: - emoji = self.deserialize_emoji(emoji) - - activity.emoji = emoji + emoji = activity_payload.get("emoji") + activity.emoji = self.deserialize_emoji(emoji) if emoji is not None else None + party = None if "party" in activity_payload: party_payload = activity_payload["party"] party = presence_models.ActivityParty() @@ -1261,11 +1241,9 @@ def deserialize_member_presence(self, payload: data_binding.JSONObject) -> prese party.max_size = int(max_size) else: party.current_size = party.max_size = None + activity.party = party - activity.party = party - else: - activity.party = None - + assets = None if "assets" in activity_payload: assets_payload = activity_payload["assets"] assets = presence_models.ActivityAssets() @@ -1273,19 +1251,16 @@ def deserialize_member_presence(self, payload: data_binding.JSONObject) -> prese assets.large_text = assets_payload.get("large_text") assets.small_image = assets_payload.get("small_image") assets.small_text = assets_payload.get("small_text") - activity.assets = assets - else: - activity.assets = None + activity.assets = assets + secrets = None if "secrets" in activity_payload: secrets_payload = activity_payload["secrets"] - secret = presence_models.ActivitySecret() - secret.join = secrets_payload.get("join") - secret.spectate = secrets_payload.get("spectate") - secret.match = secrets_payload.get("match") - activity.secrets = secret - else: - activity.secrets = None + secrets = presence_models.ActivitySecret() + secrets.join = secrets_payload.get("join") + secrets.spectate = secrets_payload.get("spectate") + secrets.match = secrets_payload.get("match") + activity.secrets = secrets activity.is_instance = activity_payload.get("instance") # TODO: can we safely default this to False? # noinspection PyArgumentList @@ -1318,9 +1293,10 @@ def deserialize_member_presence(self, payload: data_binding.JSONObject) -> prese guild_member_presence.client_status = client_status # TODO: do we want to differentiate between undefined and null here? - if (premium_since := payload.get("premium_since")) is not None: - premium_since = date.iso8601_datetime_string_to_datetime(premium_since) - guild_member_presence.premium_since = premium_since + premium_since = payload.get("premium_since") + guild_member_presence.premium_since = ( + date.iso8601_datetime_string_to_datetime(premium_since) if premium_since is not None else None + ) # TODO: do we want to differentiate between undefined and null here? guild_member_presence.nickname = payload.get("nick") @@ -1443,12 +1419,11 @@ def deserialize_channel_pins_update_event( channel_pins_update.guild_id = snowflake.Snowflake(payload["guild_id"]) if "guild_id" in payload else None channel_pins_update.channel_id = snowflake.Snowflake(payload["channel_id"]) - if "last_pin_timestamp" in payload: - channel_pins_update.last_pin_timestamp = date.iso8601_datetime_string_to_datetime( - payload["last_pin_timestamp"] - ) - else: - channel_pins_update.last_pin_timestamp = None + channel_pins_update.last_pin_timestamp = ( + date.iso8601_datetime_string_to_datetime(payload["last_pin_timestamp"]) + if "last_pin_timestamp" in payload + else None + ) return channel_pins_update @@ -1612,7 +1587,9 @@ def deserialize_message_create_event(self, payload: data_binding.JSONObject) -> message_create.message = self.deserialize_message(payload) return message_create - def deserialize_message_update_event(self, payload: data_binding.JSONObject) -> message_events.MessageUpdateEvent: + def deserialize_message_update_event( # noqa: CFQ001 + self, payload: data_binding.JSONObject + ) -> message_events.MessageUpdateEvent: message_update = message_events.MessageUpdateEvent() updated_message = message_events.UpdatedMessageFields() @@ -1625,7 +1602,6 @@ def deserialize_message_update_event(self, payload: data_binding.JSONObject) -> updated_message.author = ( self.deserialize_user(payload["author"]) if "author" in payload else undefined.UNDEFINED ) - # TODO: will we ever be given "member" but not "author"? updated_message.member = ( self.deserialize_member(payload["member"], user=updated_message.author) if "member" in payload diff --git a/hikari/impl/event_manager_core.py b/hikari/impl/event_manager_core.py index 4231c06773..2cc2ed2f98 100644 --- a/hikari/impl/event_manager_core.py +++ b/hikari/impl/event_manager_core.py @@ -135,7 +135,7 @@ def decorator(callback: CallbackT) -> CallbackT: params = signature.parameters.values() if len(params) != 1: - raise TypeError("Event listener must have one parameter, the event object.") + raise TypeError("Event listener must have exactly one parameter, the event object.") event_param = next(iter(params)) diff --git a/hikari/impl/gateway_zookeeper.py b/hikari/impl/gateway_zookeeper.py index 7febd3352c..0260741164 100644 --- a/hikari/impl/gateway_zookeeper.py +++ b/hikari/impl/gateway_zookeeper.py @@ -201,7 +201,7 @@ def on_interrupt() -> None: else: # The user won't care where this gets raised from, unless we are # debugging. It just causes a lot of confusing spam. - raise ex.with_traceback(None) + raise ex.with_traceback(None) # noqa: R100 raise in except handler without fromflake8 finally: self._map_signal_handlers(loop.remove_signal_handler) self.logger.info("client has shut down") @@ -355,7 +355,7 @@ async def _init(self) -> None: ) shard_clients[shard_id] = shard - self._shards = shard_clients # pylint: disable=attribute-defined-outside-init + self._shards = shard_clients def _max_concurrency_chunker(self) -> typing.Iterator[typing.Iterator[int]]: """Yield generators of shard IDs. diff --git a/hikari/models/embeds.py b/hikari/models/embeds.py index 48f26d09da..0bf9fb4440 100644 --- a/hikari/models/embeds.py +++ b/hikari/models/embeds.py @@ -378,17 +378,11 @@ def set_footer( return self def set_image(self, image: typing.Union[None, str, files.Resource] = None, /) -> Embed: - if image is None: - self.image = None - else: - self.image = EmbedImage(resource=files.ensure_resource(image)) + self.image = EmbedImage(resource=files.ensure_resource(image)) if image is not None else None return self def set_thumbnail(self, image: typing.Union[None, str, files.Resource] = None, /) -> Embed: - if image is None: - self.thumbnail = None - else: - self.thumbnail = EmbedImage(resource=files.ensure_resource(image)) + self.thumbnail = EmbedImage(resource=files.ensure_resource(image)) if image is not None else None return self def add_field(self, name: str, value: str, *, inline: bool = False) -> Embed: diff --git a/hikari/models/guilds.py b/hikari/models/guilds.py index 38d966cba3..5c42fe6cfa 100644 --- a/hikari/models/guilds.py +++ b/hikari/models/guilds.py @@ -60,7 +60,7 @@ from hikari.models import colors from hikari.models import emojis as emojis_ from hikari.models import permissions as permissions_ - from hikari.models import presences + from hikari.models import presences as presences_ from hikari.utilities import undefined @@ -591,7 +591,7 @@ def format_discovery_splash(self, *, format_: str = "png", size: int = 4096) -> @attr.s(eq=True, hash=True, init=False, kw_only=True, slots=True) -class Guild(PartialGuild): # pylint:disable=too-many-instance-attributes +class Guild(PartialGuild): """A representation of a guild on Discord. !!! note @@ -785,7 +785,7 @@ class Guild(PartialGuild): # pylint:disable=too-many-instance-attributes appropriate API call to retrieve this information. """ - presences: typing.Optional[typing.Mapping[snowflake.Snowflake, presences.MemberPresence]] = attr.ib( + presences: typing.Optional[typing.Mapping[snowflake.Snowflake, presences_.MemberPresence]] = attr.ib( eq=False, hash=False, repr=False ) diff --git a/hikari/models/messages.py b/hikari/models/messages.py index ad5c848f85..683f3c30ca 100644 --- a/hikari/models/messages.py +++ b/hikari/models/messages.py @@ -336,7 +336,7 @@ async def fetch_channel(self) -> channels.PartialChannel: """ return await self.app.rest.fetch_channel(self.channel_id) - async def edit( # pylint:disable=line-too-long + async def edit( self, text: typing.Union[undefined.UndefinedType, str, None] = undefined.UNDEFINED, *, @@ -398,7 +398,7 @@ async def edit( # pylint:disable=line-too-long ValueError If more than 100 unique objects/entities are passed for `role_mentions` or `user_mentions`. - """ + """ # noqa: E501 - Line too long return await self.app.rest.edit_message( message=self.id, channel=self.channel_id, @@ -409,7 +409,7 @@ async def edit( # pylint:disable=line-too-long role_mentions=role_mentions, ) - async def reply( # pylint:disable=line-too-long + async def reply( self, text: typing.Union[undefined.UndefinedType, str] = undefined.UNDEFINED, *, @@ -479,7 +479,7 @@ async def reply( # pylint:disable=line-too-long ValueError If more than 100 unique objects/entities are passed for `role_mentions` or `user_mentions`. - """ + """ # noqa: E501 - Line too long return await self.app.rest.create_message( channel=self.channel_id, text=text, diff --git a/hikari/models/webhooks.py b/hikari/models/webhooks.py index cf8060e328..73829ae43f 100644 --- a/hikari/models/webhooks.py +++ b/hikari/models/webhooks.py @@ -169,14 +169,13 @@ async def execute( ValueError If either `Webhook.token` is `None` or more than 100 unique objects/entities are passed for `role_mentions` or `user_mentions. - """ + """ # noqa: E501 - Line too long if not self.token: raise ValueError("Cannot send a message using a webhook where we don't know it's token.") return await self.app.rest.execute_webhook( webhook=self.id, token=self.token, - text=text, username=username, avatar_url=avatar_url, tts=tts, @@ -212,11 +211,7 @@ async def delete(self, *, use_token: typing.Union[undefined.UndefinedType, bool] raise ValueError("This webhook's token is unknown, so cannot be used.") token: typing.Union[undefined.UndefinedType, str] - - if use_token: - token = typing.cast(str, self.token) - else: - token = undefined.UNDEFINED + token = typing.cast(str, self.token) if use_token else undefined.UNDEFINED await self.app.rest.delete_webhook(self.id, token=token) @@ -272,16 +267,12 @@ async def edit( If you pass a token that's invalid for the target webhook. ValueError If `use_token` is passed as `True` when `Webhook.token` is `None`. - """ + """ # noqa: E501 - Line too long if use_token and self.token is None: raise ValueError("This webhook's token is unknown, so cannot be used.") token: typing.Union[undefined.UndefinedType, str] - - if use_token: - token = typing.cast(str, self.token) - else: - token = undefined.UNDEFINED + token = typing.cast(str, self.token) if use_token else undefined.UNDEFINED return await self.app.rest.edit_webhook( self.id, token=token, name=name, avatar=avatar, channel=channel, reason=reason, @@ -341,11 +332,7 @@ async def fetch_self( raise ValueError("This webhook's token is unknown, so cannot be used.") token: typing.Union[undefined.UndefinedType, str] - - if use_token: - token = typing.cast(str, self.token) - else: - token = undefined.UNDEFINED + token = typing.cast(str, self.token) if use_token else undefined.UNDEFINED return await self.app.rest.fetch_webhook(self.id, token=token) diff --git a/hikari/net/gateway.py b/hikari/net/gateway.py index 7efa4c4db7..961a0cb796 100644 --- a/hikari/net/gateway.py +++ b/hikari/net/gateway.py @@ -263,7 +263,7 @@ async def _run(self) -> None: pass # Allow zookeepers to stop gathering tasks for each shard. - raise errors.GatewayClientClosedError() + raise errors.GatewayClientClosedError finally: # This is set to ensure that the `start' waiter does not deadlock if # we cannot connect successfully. It is a hack, but it works. @@ -558,7 +558,7 @@ async def _poll_events(self) -> None: elif op == self._GatewayOpcode.RECONNECT: self.logger.debug("RECONNECT") - raise self._Reconnect() + raise self._Reconnect elif op == self._GatewayOpcode.INVALID_SESSION: self.logger.debug("INVALID SESSION [resume:%s]", data) @@ -605,7 +605,7 @@ async def _receive_json_payload(self) -> data_binding.JSONObject: raise errors.GatewayServerClosedConnectionError(reason, close_code, can_reconnect, can_reconnect, True) if message.type == aiohttp.WSMsgType.CLOSING or message.type == aiohttp.WSMsgType.CLOSED: - raise self._SocketClosed() + raise self._SocketClosed # Assume exception for now. ex = self._ws.exception() @@ -678,7 +678,7 @@ def _build_presence_payload( if activity is undefined.UNDEFINED: activity = self._activity - if activity is not None and not activity is undefined.UNDEFINED: + if activity is not None and activity is not undefined.UNDEFINED: game: typing.Union[undefined.UndefinedType, None, data_binding.JSONObject] = { "name": activity.name, "url": activity.url, diff --git a/hikari/net/http_client.py b/hikari/net/http_client.py index 55843788f5..b76792bbee 100644 --- a/hikari/net/http_client.py +++ b/hikari/net/http_client.py @@ -47,7 +47,7 @@ RequestContextManager = typing.Any # type: ignore -class HTTPClient(abc.ABC): # pylint:disable=too-many-instance-attributes +class HTTPClient(abc.ABC): """An HTTP client base for Hikari. The purpose of this is to provide a consistent interface for any network diff --git a/hikari/net/iterators.py b/hikari/net/iterators.py index 142c172573..3e4cf7972f 100644 --- a/hikari/net/iterators.py +++ b/hikari/net/iterators.py @@ -426,7 +426,7 @@ async def __anext__(self) -> audit_logs.AuditLog: response = typing.cast(data_binding.JSONObject, raw_response) if not response["entries"]: - raise StopAsyncIteration() + raise StopAsyncIteration log = self._app.entity_factory.deserialize_audit_log(response) self._first_id = str(min(log.entries.keys())) diff --git a/hikari/net/rate_limits.py b/hikari/net/rate_limits.py index 2996cbae23..74849c17fa 100644 --- a/hikari/net/rate_limits.py +++ b/hikari/net/rate_limits.py @@ -487,7 +487,7 @@ def __next__(self) -> float: self.increment += 1 if self.maximum is not None and value >= self.maximum: - raise asyncio.TimeoutError() + raise asyncio.TimeoutError value += random.random() * self.jitter_multiplier # # noqa S311 rng for cryptography return value diff --git a/hikari/net/rest.py b/hikari/net/rest.py index f5f69f40c6..5dbd58b5b6 100644 --- a/hikari/net/rest.py +++ b/hikari/net/rest.py @@ -66,7 +66,7 @@ from hikari.models import webhooks -class REST(http_client.HTTPClient, component.IComponent): # pylint:disable=too-many-public-methods +class REST(http_client.HTTPClient, component.IComponent): """Implementation of the V6 and V7-compatible Discord REST API. This manages making HTTP/1.1 requests to the API and using the entity @@ -292,7 +292,7 @@ async def _parse_ratelimits(self, compiled_route: routes.CompiledRoute, response self.global_rate_limit.throttle(body_retry_after) self.logger.warning("you are being rate-limited globally - trying again after %ss", body_retry_after) - raise self._RetryRequest() + raise self._RetryRequest # Discord have started applying ratelimits to operations on some endpoints # based on specific fields used in the JSON body. @@ -325,7 +325,7 @@ async def _parse_ratelimits(self, compiled_route: routes.CompiledRoute, response # safe to retry the request, as they are likely the same value just with some # measuring difference. 20% was used as a rounded figure. if math.isclose(body_retry_after, reset_after, rel_tol=0.20): - raise self._RetryRequest() + raise self._RetryRequest raise errors.RateLimited(str(response.real_url), compiled_route, response.headers, body, body_retry_after) @@ -374,7 +374,7 @@ async def close(self) -> None: self.buckets.close() async def fetch_channel( - self, channel: typing.Union[channels.PartialChannel, snowflake.UniqueObject], /, + self, channel: typing.Union[channels.PartialChannel, snowflake.UniqueObject] ) -> channels.PartialChannel: """Fetch a channel. @@ -474,7 +474,7 @@ async def edit_channel( If the channel is not found. hikari.errors.ServerHTTPErrorResponse If an internal error occurs on Discord while handling the request. - """ + """ # noqa: E501 - Line too long route = routes.PATCH_CHANNEL.compile(channel=channel) body = data_binding.JSONObjectBuilder() body.put("name", name) @@ -495,7 +495,7 @@ async def edit_channel( response = typing.cast(data_binding.JSONObject, raw_response) return self._app.entity_factory.deserialize_channel(response) - async def delete_channel(self, channel: typing.Union[channels.PartialChannel, snowflake.UniqueObject], /) -> None: + async def delete_channel(self, channel: typing.Union[channels.PartialChannel, snowflake.UniqueObject]) -> None: """Delete a channel in a guild, or close a DM. Parameters @@ -593,7 +593,7 @@ async def edit_permission_overwrites( a role. hikari.errors.ServerHTTPErrorResponse If an internal error occurs on Discord while handling the request. - """ + """ # noqa: E501 - Line too long if target_type is undefined.UNDEFINED: if isinstance(target, users.User): target_type = channels.PermissionOverwriteType.MEMBER @@ -639,12 +639,12 @@ async def delete_permission_overwrite( If the channel is not found or the target is not found. hikari.errors.ServerHTTPErrorResponse If an internal error occurs on Discord while handling the request. - """ + """ # noqa: E501 - Line too long route = routes.DELETE_CHANNEL_PERMISSIONS.compile(channel=channel, overwrite=target) await self._request(route) async def fetch_channel_invites( - self, channel: typing.Union[channels.GuildChannel, snowflake.UniqueObject], / + self, channel: typing.Union[channels.GuildChannel, snowflake.UniqueObject] ) -> typing.Sequence[invites.InviteWithMetadata]: """Fetch all invites pointing to the given guild channel. @@ -678,7 +678,6 @@ async def fetch_channel_invites( async def create_invite( self, channel: typing.Union[channels.GuildChannel, snowflake.UniqueObject], - /, *, max_age: typing.Union[undefined.UndefinedType, int, float, datetime.timedelta] = undefined.UNDEFINED, max_uses: typing.Union[undefined.UndefinedType, int] = undefined.UNDEFINED, @@ -729,7 +728,7 @@ async def create_invite( if specified. hikari.errors.ServerHTTPErrorResponse If an internal error occurs on Discord while handling the request. - """ + """ # noqa: E501 - Line too long route = routes.POST_CHANNEL_INVITES.compile(channel=channel) body = data_binding.JSONObjectBuilder() body.put("max_age", max_age, conversion=date.timespan_to_int) @@ -743,7 +742,7 @@ async def create_invite( return self._app.entity_factory.deserialize_invite_with_metadata(response) def trigger_typing( - self, channel: typing.Union[channels.TextChannel, snowflake.UniqueObject], / + self, channel: typing.Union[channels.TextChannel, snowflake.UniqueObject] ) -> rest_utils.TypingIndicator: """Trigger typing in a text channel. @@ -778,7 +777,7 @@ def trigger_typing( return rest_utils.TypingIndicator(channel, self._request) async def fetch_pins( - self, channel: typing.Union[channels.TextChannel, snowflake.UniqueObject], / + self, channel: typing.Union[channels.TextChannel, snowflake.UniqueObject] ) -> typing.Sequence[messages_.Message]: """Fetch the pinned messages in this text channel. @@ -874,7 +873,7 @@ async def unpin_message( @typing.overload def fetch_messages( - self, channel: typing.Union[channels.TextChannel, snowflake.UniqueObject], / + self, channel: typing.Union[channels.TextChannel, snowflake.UniqueObject] ) -> iterators.LazyIterator[messages_.Message]: """Fetch messages, newest first, sent in the given channel.""" @@ -882,7 +881,6 @@ def fetch_messages( def fetch_messages( self, channel: typing.Union[channels.TextChannel, snowflake.UniqueObject], - /, *, before: typing.Union[datetime.datetime, snowflake.UniqueObject], ) -> iterators.LazyIterator[messages_.Message]: @@ -892,7 +890,6 @@ def fetch_messages( def fetch_messages( self, channel: typing.Union[channels.TextChannel, snowflake.UniqueObject], - /, *, around: typing.Union[datetime.datetime, snowflake.UniqueObject], ) -> iterators.LazyIterator[messages_.Message]: @@ -902,7 +899,6 @@ def fetch_messages( def fetch_messages( self, channel: typing.Union[channels.TextChannel, snowflake.UniqueObject], - /, *, after: typing.Union[datetime.datetime, snowflake.UniqueObject], ) -> iterators.LazyIterator[messages_.Message]: @@ -911,7 +907,6 @@ def fetch_messages( def fetch_messages( self, channel: typing.Union[channels.TextChannel, snowflake.UniqueObject], - /, *, before: typing.Union[undefined.UndefinedType, datetime.datetime, snowflake.UniqueObject] = undefined.UNDEFINED, after: typing.Union[undefined.UndefinedType, datetime.datetime, snowflake.UniqueObject] = undefined.UNDEFINED, @@ -958,7 +953,7 @@ def fetch_messages( be raised once the result is awaited or interacted with. Invoking this function itself will not raise anything (other than `TypeError`). - """ + """ # noqa: E501 - Line too long if undefined.count(before, after, around) < 2: raise TypeError("Expected no kwargs, or maximum of one of 'before', 'after', 'around'") @@ -1086,7 +1081,7 @@ async def create_message( !!! warning You are expected to make a connection to the gateway and identify once before being able to use this endpoint for a bot. - """ + """ # noqa: E501 - Line too long route = routes.POST_CHANNEL_MESSAGES.compile(channel=channel) body = data_binding.JSONObjectBuilder() @@ -1407,20 +1402,20 @@ async def create_webhook( async def fetch_webhook( self, webhook: typing.Union[webhooks.Webhook, snowflake.UniqueObject], - /, *, token: typing.Union[undefined.UndefinedType, str] = undefined.UNDEFINED, ) -> webhooks.Webhook: - if token is undefined.UNDEFINED: - route = routes.GET_WEBHOOK.compile(webhook=webhook) - else: - route = routes.GET_WEBHOOK_WITH_TOKEN.compile(webhook=webhook, token=token) + route = ( + routes.GET_WEBHOOK.compile(webhook=webhook) + if token is undefined.UNDEFINED + else routes.GET_WEBHOOK_WITH_TOKEN.compile(webhook=webhook, token=token) + ) raw_response = await self._request(route) response = typing.cast(data_binding.JSONObject, raw_response) return self._app.entity_factory.deserialize_webhook(response) async def fetch_channel_webhooks( - self, channel: typing.Union[channels.TextChannel, snowflake.UniqueObject], / + self, channel: typing.Union[channels.TextChannel, snowflake.UniqueObject] ) -> typing.Sequence[webhooks.Webhook]: route = routes.GET_CHANNEL_WEBHOOKS.compile(channel=channel) raw_response = await self._request(route) @@ -1428,7 +1423,7 @@ async def fetch_channel_webhooks( return data_binding.cast_json_array(response, self._app.entity_factory.deserialize_webhook) async def fetch_guild_webhooks( - self, guild: typing.Union[guilds.Guild, snowflake.UniqueObject], / + self, guild: typing.Union[guilds.Guild, snowflake.UniqueObject] ) -> typing.Sequence[webhooks.Webhook]: route = routes.GET_GUILD_WEBHOOKS.compile(channel=guild) raw_response = await self._request(route) @@ -1438,7 +1433,6 @@ async def fetch_guild_webhooks( async def edit_webhook( self, webhook: typing.Union[webhooks.Webhook, snowflake.UniqueObject], - /, *, token: typing.Union[undefined.UndefinedType, str] = undefined.UNDEFINED, name: typing.Union[undefined.UndefinedType, str] = undefined.UNDEFINED, @@ -1448,11 +1442,11 @@ async def edit_webhook( ] = undefined.UNDEFINED, reason: typing.Union[undefined.UndefinedType, str] = undefined.UNDEFINED, ) -> webhooks.Webhook: - if token is undefined.UNDEFINED: - route = routes.PATCH_WEBHOOK.compile(webhook=webhook) - else: - route = routes.PATCH_WEBHOOK_WITH_TOKEN.compile(webhook=webhook, token=token) - + route = ( + routes.PATCH_WEBHOOK.compile(webhook=webhook) + if token is undefined.UNDEFINED + else routes.PATCH_WEBHOOK_WITH_TOKEN.compile(webhook=webhook, token=token) + ) body = data_binding.JSONObjectBuilder() body.put("name", name) body.put_snowflake("channel", channel) @@ -1470,14 +1464,14 @@ async def edit_webhook( async def delete_webhook( self, webhook: typing.Union[webhooks.Webhook, snowflake.UniqueObject], - /, *, token: typing.Union[undefined.UndefinedType, str] = undefined.UNDEFINED, ) -> None: - if token is undefined.UNDEFINED: - route = routes.DELETE_WEBHOOK.compile(webhook=webhook) - else: - route = routes.DELETE_WEBHOOK_WITH_TOKEN.compile(webhook=webhook, token=token) + route = ( + routes.DELETE_WEBHOOK.compile(webhook=webhook) + if token is undefined.UNDEFINED + else routes.DELETE_WEBHOOK_WITH_TOKEN.compile(webhook=webhook, token=token) + ) await self._request(route) async def execute_webhook( @@ -1679,7 +1673,6 @@ async def fetch_user(self, user: typing.Union[users.User, snowflake.UniqueObject def fetch_audit_log( self, guild: typing.Union[guilds.Guild, snowflake.UniqueObject], - /, *, before: typing.Union[undefined.UndefinedType, datetime.datetime, snowflake.UniqueObject] = undefined.UNDEFINED, user: typing.Union[undefined.UndefinedType, users.User, snowflake.UniqueObject] = undefined.UNDEFINED, @@ -1713,7 +1706,7 @@ async def fetch_emoji( return self._app.entity_factory.deserialize_known_custom_emoji(response) async def fetch_guild_emojis( - self, guild: typing.Union[guilds.Guild, snowflake.UniqueObject], / + self, guild: typing.Union[guilds.Guild, snowflake.UniqueObject] ) -> typing.Set[emojis.KnownCustomEmoji]: route = routes.GET_GUILD_EMOJIS.compile(guild=guild) raw_response = await self._request(route) @@ -1782,14 +1775,14 @@ async def delete_emoji( def guild_builder(self, name: str, /) -> rest_utils.GuildBuilder: return rest_utils.GuildBuilder(app=self._app, name=name, request_call=self._request) - async def fetch_guild(self, guild: typing.Union[guilds.Guild, snowflake.UniqueObject], /) -> guilds.Guild: + async def fetch_guild(self, guild: typing.Union[guilds.Guild, snowflake.UniqueObject]) -> guilds.Guild: route = routes.GET_GUILD.compile(guild=guild) raw_response = await self._request(route) response = typing.cast(data_binding.JSONObject, raw_response) return self._app.entity_factory.deserialize_guild(response) async def fetch_guild_preview( - self, guild: typing.Union[guilds.PartialGuild, snowflake.UniqueObject], / + self, guild: typing.Union[guilds.PartialGuild, snowflake.UniqueObject] ) -> guilds.GuildPreview: route = routes.GET_GUILD_PREVIEW.compile(guild=guild) raw_response = await self._request(route) @@ -1799,7 +1792,6 @@ async def fetch_guild_preview( async def edit_guild( self, guild: typing.Union[guilds.Guild, snowflake.UniqueObject], - /, *, name: typing.Union[undefined.UndefinedType, str] = undefined.UNDEFINED, region: typing.Union[undefined.UndefinedType, voices.VoiceRegion, str] = undefined.UNDEFINED, @@ -2055,7 +2047,7 @@ async def fetch_member( return self._app.entity_factory.deserialize_member(response) def fetch_members( - self, guild: typing.Union[guilds.Guild, snowflake.UniqueObject], + self, guild: typing.Union[guilds.Guild, snowflake.UniqueObject] ) -> iterators.LazyIterator[guilds.Member]: return iterators.MemberIterator(self._app, self._request, str(int(guild))) @@ -2167,7 +2159,7 @@ async def fetch_ban( return self._app.entity_factory.deserialize_guild_member_ban(response) async def fetch_bans( - self, guild: typing.Union[guilds.Guild, snowflake.UniqueObject], / + self, guild: typing.Union[guilds.Guild, snowflake.UniqueObject] ) -> typing.Sequence[guilds.GuildMemberBan]: route = routes.GET_GUILD_BANS.compile(guild=guild) raw_response = await self._request(route) @@ -2175,7 +2167,7 @@ async def fetch_bans( return data_binding.cast_json_array(response, self._app.entity_factory.deserialize_guild_member_ban) async def fetch_roles( - self, guild: typing.Union[guilds.Guild, snowflake.UniqueObject], / + self, guild: typing.Union[guilds.Guild, snowflake.UniqueObject] ) -> typing.Sequence[guilds.Role]: route = routes.GET_GUILD_ROLES.compile(guild=guild) raw_response = await self._request(route) @@ -2185,7 +2177,6 @@ async def fetch_roles( async def create_role( self, guild: typing.Union[guilds.Guild, snowflake.UniqueObject], - /, *, name: typing.Union[undefined.UndefinedType, str] = undefined.UNDEFINED, permissions: typing.Union[undefined.UndefinedType, permissions_.Permission] = undefined.UNDEFINED, @@ -2368,7 +2359,7 @@ async def begin_guild_prune( return int(pruned) if pruned is not None else None async def fetch_guild_voice_regions( - self, guild: typing.Union[guilds.Guild, snowflake.UniqueObject], / + self, guild: typing.Union[guilds.Guild, snowflake.UniqueObject] ) -> typing.Sequence[voices.VoiceRegion]: route = routes.GET_GUILD_VOICE_REGIONS.compile(guild=guild) raw_response = await self._request(route) @@ -2376,7 +2367,7 @@ async def fetch_guild_voice_regions( return data_binding.cast_json_array(response, self._app.entity_factory.deserialize_voice_region) async def fetch_guild_invites( - self, guild: typing.Union[guilds.Guild, snowflake.UniqueObject], / + self, guild: typing.Union[guilds.Guild, snowflake.UniqueObject] ) -> typing.Sequence[invites.InviteWithMetadata]: route = routes.GET_GUILD_INVITES.compile(guild=guild) raw_response = await self._request(route) @@ -2384,7 +2375,7 @@ async def fetch_guild_invites( return data_binding.cast_json_array(response, self._app.entity_factory.deserialize_invite_with_metadata) async def fetch_integrations( - self, guild: typing.Union[guilds.Guild, snowflake.UniqueObject], / + self, guild: typing.Union[guilds.Guild, snowflake.UniqueObject] ) -> typing.Sequence[guilds.Integration]: route = routes.GET_GUILD_INTEGRATIONS.compile(guild=guild) raw_response = await self._request(route) @@ -2429,7 +2420,7 @@ async def sync_integration( route = routes.POST_GUILD_INTEGRATION_SYNC.compile(guild=guild, integration=integration) await self._request(route) - async def fetch_widget(self, guild: typing.Union[guilds.Guild, snowflake.UniqueObject], /) -> guilds.GuildWidget: + async def fetch_widget(self, guild: typing.Union[guilds.Guild, snowflake.UniqueObject]) -> guilds.GuildWidget: route = routes.GET_GUILD_WIDGET.compile(guild=guild) raw_response = await self._request(route) response = typing.cast(data_binding.JSONObject, raw_response) @@ -2438,7 +2429,6 @@ async def fetch_widget(self, guild: typing.Union[guilds.Guild, snowflake.UniqueO async def edit_widget( self, guild: typing.Union[guilds.Guild, snowflake.UniqueObject], - /, *, channel: typing.Union[ undefined.UndefinedType, channels.GuildChannel, snowflake.UniqueObject, None @@ -2459,7 +2449,7 @@ async def edit_widget( response = typing.cast(data_binding.JSONObject, raw_response) return self._app.entity_factory.deserialize_guild_widget(response) - async def fetch_vanity_url(self, guild: typing.Union[guilds.Guild, snowflake.UniqueObject], /) -> invites.VanityURL: + async def fetch_vanity_url(self, guild: typing.Union[guilds.Guild, snowflake.UniqueObject]) -> invites.VanityURL: route = routes.GET_GUILD_VANITY_URL.compile(guild=guild) raw_response = await self._request(route) response = typing.cast(data_binding.JSONObject, raw_response) diff --git a/hikari/net/rest_utils.py b/hikari/net/rest_utils.py index d4e851703d..9d30d88dfe 100644 --- a/hikari/net/rest_utils.py +++ b/hikari/net/rest_utils.py @@ -374,7 +374,7 @@ def add_category( When the guild is created, this will be replaced with a different ID. - """ + """ # noqa: E501 - Line too long snowflake = self._new_snowflake() payload = data_binding.JSONObjectBuilder() payload.put_snowflake("id", snowflake) @@ -438,7 +438,7 @@ def add_text_channel( When the guild is created, this will be replaced with a different ID. - """ + """ # noqa: E501 - Line too long snowflake = self._new_snowflake() payload = data_binding.JSONObjectBuilder() payload.put_snowflake("id", snowflake) @@ -505,7 +505,7 @@ def add_voice_channel( When the guild is created, this will be replaced with a different ID. - """ + """ # noqa: E501 - Line too long snowflake = self._new_snowflake() payload = data_binding.JSONObjectBuilder() payload.put_snowflake("id", snowflake) diff --git a/hikari/net/routes.py b/hikari/net/routes.py index 5ef882655f..c4497ceb59 100644 --- a/hikari/net/routes.py +++ b/hikari/net/routes.py @@ -122,18 +122,15 @@ class Route: """The optional major parameter name.""" # noinspection RegExpRedundantEscape - _MAJOR_PARAM_REGEX: typing.Final[typing.ClassVar[typing.Pattern[str]]] = re.compile(r"\{(.*?)\}") # noqa: FS003 + _MAJOR_PARAM_REGEX: typing.Final[typing.ClassVar[typing.Pattern[str]]] = re.compile(r"\{(.*?)\}") def __init__(self, method: str, path_template: str) -> None: self.method = method self.path_template = path_template - major_param: typing.Optional[str] - if match := self._MAJOR_PARAM_REGEX.search(path_template): - major_param = match.group(1) - else: - major_param = None - self.major_param = major_param + self.major_param: typing.Optional[str] + match = self._MAJOR_PARAM_REGEX.search(path_template) + self.major_param = match.group(1) if match else None def compile(self, **kwargs: typing.Any) -> CompiledRoute: """Generate a formatted `CompiledRoute` for this route. diff --git a/hikari/net/tracing.py b/hikari/net/tracing.py index d99a57ca4a..2993a08fe8 100644 --- a/hikari/net/tracing.py +++ b/hikari/net/tracing.py @@ -169,10 +169,11 @@ async def on_request_end(self, _, ctx, params): latency = round((time.perf_counter() - ctx.start_time) * 1_000, 2) response = params.response - if strings.CONTENT_TYPE_HEADER in response.headers: - body = await self._format_body(await response.read()) - else: - body = "no-content" + body = ( + (await self._format_body(await response.read())) + if strings.CONTENT_TYPE_HEADER in response.headers + else "no-content" + ) self.logger.debug( "%s %s %s after %sms [%s]\n response headers: %s\n response body: %s", diff --git a/hikari/utilities/aio.py b/hikari/utilities/aio.py index e97268ea57..50b54e9ee9 100644 --- a/hikari/utilities/aio.py +++ b/hikari/utilities/aio.py @@ -53,13 +53,13 @@ def completed_future(result: typing.Optional[T_inv] = None, /) -> asyncio.Future # On Python3.8.2, there appears to be a bug with the typing module: # >>> class Aiterable: -# ... async def __aiter__(self): +# ... async def __aiter__(self): # noqa: E800 # ... yield ... # >>> isinstance(Aiterable(), typing.AsyncIterable) # True # >>> class Aiterator: -# ... async def __anext__(self): +# ... async def __anext__(self): # noqa: E800 # ... return ... # >>> isinstance(Aiterator(), typing.AsyncIterator) # False diff --git a/hikari/utilities/date.py b/hikari/utilities/date.py index d6e640524d..0424bb0f19 100644 --- a/hikari/utilities/date.py +++ b/hikari/utilities/date.py @@ -53,12 +53,9 @@ * [Discord API documentation - Snowflakes](https://discord.com/developers/docs/reference#snowflakes) """ -# FS003 - f-string missing prefix. -_ISO_8601_DATE: typing.Final[typing.Pattern[str]] = re.compile(r"^(\d{4})-(\d{2})-(\d{2})") # noqa: FS003 -_ISO_8601_TIME: typing.Final[typing.Pattern[str]] = re.compile( - r"T(\d{2}):(\d{2}):(\d{2})(?:\.(\d{1,6}))?", re.I -) # noqa: FS003 -_ISO_8601_TZ: typing.Final[typing.Pattern[str]] = re.compile(r"([+-])(\d{2}):(\d{2})$") # noqa: FS003 +_ISO_8601_DATE: typing.Final[typing.Pattern[str]] = re.compile(r"^(\d{4})-(\d{2})-(\d{2})") +_ISO_8601_TIME: typing.Final[typing.Pattern[str]] = re.compile(r"T(\d{2}):(\d{2}):(\d{2})(?:\.(\d{1,6}))?", re.I) +_ISO_8601_TZ: typing.Final[typing.Pattern[str]] = re.compile(r"([+-])(\d{2}):(\d{2})$") def rfc7231_datetime_string_to_datetime(date_str: str, /) -> datetime.datetime: diff --git a/hikari/utilities/files.py b/hikari/utilities/files.py index 6bf9c5b290..36b4bd0f91 100644 --- a/hikari/utilities/files.py +++ b/hikari/utilities/files.py @@ -274,7 +274,7 @@ class ByteReader(AsyncReader): async def __aiter__(self) -> typing.AsyncGenerator[typing.Any, bytes]: for i in range(0, len(self.data), _MAGIC): - yield self.data[i : i + _MAGIC] + yield self.data[i : i + _MAGIC] # noqa: E203 @attr.s(auto_attribs=True, slots=True) @@ -300,8 +300,8 @@ class WebReader(AsyncReader): """The size of the resource, if known.""" head_only: bool - """If `True`, then only the HEAD was requested. - + """If `True`, then only the HEAD was requested. + In this case, neither `__aiter__` nor `read` would return anything other than an empty byte string. """ diff --git a/hikari/utilities/snowflake.py b/hikari/utilities/snowflake.py index 94ae3fb7ae..65c8152af7 100644 --- a/hikari/utilities/snowflake.py +++ b/hikari/utilities/snowflake.py @@ -98,7 +98,7 @@ class Unique(abc.ABC): @property @abc.abstractmethod def id(self) -> Snowflake: - """The ID of this entity.""" + """The ID of this entity.""" # noqa: D401 - Not imperative mood @id.setter def id(self, value: Snowflake) -> None: diff --git a/tests/hikari/impl/test_entity_factory.py b/tests/hikari/impl/test_entity_factory.py index 291b600d15..76f0a2143a 100644 --- a/tests/hikari/impl/test_entity_factory.py +++ b/tests/hikari/impl/test_entity_factory.py @@ -1646,7 +1646,7 @@ def test_deserialize_guild_with_unset_fields(self, entity_factory_impl): "verification_level": 4, } ) - assert guild.channels is None + assert guild.channels == {} assert guild.embed_channel_id is None assert guild.is_embed_enabled is False assert guild.joined_at is None @@ -1655,10 +1655,10 @@ def test_deserialize_guild_with_unset_fields(self, entity_factory_impl): assert guild.max_presences is None assert guild.max_video_channel_users is None assert guild.member_count is None - assert guild.members is None + assert guild.members == {} assert guild.my_permissions is None assert guild.premium_subscription_count is None - assert guild.presences is None + assert guild.presences == {} assert guild.is_unavailable is None assert guild.widget_channel_id is None assert guild.is_widget_enabled is None From db056bdb10d69f891aa9e324b3fdb2064c6b30ae Mon Sep 17 00:00:00 2001 From: davfsa Date: Fri, 19 Jun 2020 09:02:04 +0200 Subject: [PATCH 550/922] Fix documentation references --- hikari/models/emojis.py | 10 +++++----- hikari/models/messages.py | 7 +++---- hikari/models/users.py | 3 +-- hikari/models/webhooks.py | 4 ++-- hikari/net/rest.py | 4 ++-- hikari/utilities/reflect.py | 4 ++-- 6 files changed, 15 insertions(+), 17 deletions(-) diff --git a/hikari/models/emojis.py b/hikari/models/emojis.py index 52d5376192..4dfc2baddb 100644 --- a/hikari/models/emojis.py +++ b/hikari/models/emojis.py @@ -47,9 +47,9 @@ class Emoji(files.WebResource, abc.ABC): """Base class for all emojis. - Any emoji implementation supports being used as a `hikari.models.files.BaseStream` + Any emoji implementation supports being used as a `hikari.utilities.files.Resource` when uploading an attachment to the API. This is achieved in the same - way as using a `hikari.models.files.WebResourceStream` would achieve this. + way as using a `hikari.utilities.files.WebResource` would achieve this. """ @property @@ -195,7 +195,7 @@ class CustomEmoji(snowflake.Unique, Emoji): This is a custom emoji that is from a guild you might not be part of. All CustomEmoji objects and their derivatives act as valid - `hikari.models.files.BaseStream` objects. This means you can use them as a + `hikari.utilities.files.Resource` objects. This means you can use them as a file when sending a message. >>> emojis = await bot.rest.fetch_guild_emojis(12345) @@ -252,8 +252,8 @@ def is_mentionable(self) -> bool: @property @typing.final def url(self) -> str: - # TODO, change this as it is a bad line of code and I don't like it. - return cdn.generate_cdn_url("emojis", str(self.id), format_="gif" if self.is_animated else "png", size=None).url + ext = "gif" if self.is_animated else "png" + return cdn.generate_cdn_url("emojis", str(self.id), format_=ext, size=None).url @attr.s(eq=True, hash=True, init=False, kw_only=True, slots=True) diff --git a/hikari/models/messages.py b/hikari/models/messages.py index 683f3c30ca..38fa008eb1 100644 --- a/hikari/models/messages.py +++ b/hikari/models/messages.py @@ -145,9 +145,8 @@ class MessageActivityType(int, enum.Enum): class Attachment(snowflake.Unique, files_.WebResource): """Represents a file attached to a message. - You can use this object in the same way as a - `hikari.models.files.BaseStream`, by passing it as an attached file when creating a - message, etc. + You can use this object in the same way as a `hikari.utilities.files.WebResource`, + by passing it as an attached file when creating a message, etc. """ id: snowflake.Snowflake = attr.ib( @@ -437,7 +436,7 @@ async def reply( and can usually be ignored. tts : bool or hikari.utilities.undefined.UndefinedType If specified, whether the message will be sent as a TTS message. - attachments : typing.Sequence[hikari.models.files.BaseStream] or hikari.utilities.undefined.UndefinedType + attachments : typing.Sequence[hikari.utilities.files.Resource] or hikari.utilities.undefined.UndefinedType If specified, a sequence of attachments to upload, if desired. Should be between 1 and 10 objects in size (inclusive), also including embed attachments. diff --git a/hikari/models/users.py b/hikari/models/users.py index d9c825d412..7381005c43 100644 --- a/hikari/models/users.py +++ b/hikari/models/users.py @@ -183,8 +183,7 @@ def format_avatar(self, *, format_: typing.Optional[str] = None, size: int = 409 """Generate the avatar for this user, if set. If no custom avatar is set, this returns `None`. You can then use the - `User.default_avatar_url` attribute instead to fetch the displayed - URL. + `default_avatar_url` attribute instead to fetch the displayed URL. Parameters ---------- diff --git a/hikari/models/webhooks.py b/hikari/models/webhooks.py index 73829ae43f..b72d360312 100644 --- a/hikari/models/webhooks.py +++ b/hikari/models/webhooks.py @@ -131,7 +131,7 @@ async def execute( avatar with for this request. tts : bool or hikari.utilities.undefined.UndefinedType If specified, whether the message will be sent as a TTS message. - attachments : typing.Sequence[hikari.utilities.files.BaseStream] or hikari.utilities.undefined.UndefinedType + attachments : typing.Sequence[hikari.utilities.files.Resource] or hikari.utilities.undefined.UndefinedType If specified, a sequence of attachments to upload. embeds : typing.Sequence[hikari.models.embeds.Embed] or hikari.utilities.undefined.UndefinedType If specified, a sequence of between `1` to `10` embed objects @@ -235,7 +235,7 @@ async def edit( avatar : hikari.utilities.files.Resource or None or hikari.utilities.undefined.UndefinedType If specified, the new avatar image. If `None`, then it is removed. If not specified, nothing is changed. - channel : hikari.models.channels.GuildChannel or hikari.models.snowflake.UniqueObject or hikari.utilities.undefined.UndefinedType + channel : hikari.models.channels.GuildChannel or hikari.utilities.snowflake.Snowflake or str or int or hikari.utilities.undefined.UndefinedType If specified, the object or ID of the new channel the given webhook should be moved to. reason : str or hikari.utilities.undefined.UndefinedType diff --git a/hikari/net/rest.py b/hikari/net/rest.py index 5dbd58b5b6..02569e0258 100644 --- a/hikari/net/rest.py +++ b/hikari/net/rest.py @@ -2267,7 +2267,7 @@ async def estimate_guild_prune_count( or the ID of an existing channel. days : hikari.utilities.undefined.UndefinedType or int If provided, number of days to count prune for. - include_roles : hikari.utilities.undefined.UndefinedType or typing.Collection[hikari.guilds.Role or hikari.utilities.snowflake.Snowflake or int or str] + include_roles : hikari.utilities.undefined.UndefinedType or typing.Collection[hikari.models.guilds.Role or hikari.utilities.snowflake.Snowflake or int or str] If provided, the role(s) to include. By default, this endpoint will not count users with roles. Providing roles using this attribute will make members with the specified roles also get included into the count. @@ -2323,7 +2323,7 @@ async def begin_guild_prune( compute_prune_count: hikari.utilities.undefined.UndefinedType or bool If provided, whether to return the prune count. This is discouraged for large guilds. - include_roles : hikari.utilities.undefined.UndefinedType or typing.Collection[hikari.guilds.Role or hikari.utilities.snowflake.Snowflake or int or str] + include_roles : hikari.utilities.undefined.UndefinedType or typing.Collection[hikari.models.guilds.Role or hikari.utilities.snowflake.Snowflake or int or str] If provided, the role(s) to include. By default, this endpoint will not count users with roles. Providing roles using this attribute will make members with the specified roles also get included into the count. diff --git a/hikari/utilities/reflect.py b/hikari/utilities/reflect.py index beb9f0d3cb..597843fa81 100644 --- a/hikari/utilities/reflect.py +++ b/hikari/utilities/reflect.py @@ -44,8 +44,8 @@ def resolve_signature(func: typing.Callable[..., typing.Any]) -> inspect.Signatu Returns ------- - typing.Signature - A `typing.Signature` object with all forward reference annotations + inspect.Signature + A `inspect.Signature` object with all forward reference annotations resolved. """ signature = inspect.signature(func) From e99844ef190fbbd5c0f120e1f7ef238f4adbe159 Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Fri, 19 Jun 2020 10:58:48 +0100 Subject: [PATCH 551/922] Fixed bug with '__await__' in lazy iterators always producing None; added stream ops. --- hikari/net/iterators.py | 163 +++++++++++++++++++++++++++++++++++----- 1 file changed, 143 insertions(+), 20 deletions(-) diff --git a/hikari/net/iterators.py b/hikari/net/iterators.py index 3e4cf7972f..1009480780 100644 --- a/hikari/net/iterators.py +++ b/hikari/net/iterators.py @@ -21,6 +21,7 @@ __all__: typing.Final[typing.List[str]] = ["LazyIterator"] import abc +import operator import typing from hikari.net import routes @@ -37,10 +38,34 @@ from hikari.models import messages from hikari.models import users -IteratorT = typing.TypeVar("IteratorT") +ValueT = typing.TypeVar("ValueT") +AnotherValueT = typing.TypeVar("AnotherValueT") -class LazyIterator(typing.Generic[IteratorT], abc.ABC): +class _AllConditions(typing.Generic[ValueT]): + __slots__ = ("conditions",) + + def __init__(self, conditions: typing.Collection[typing.Callable[[ValueT], bool]]) -> None: + self.conditions = conditions + + def __call__(self, item: ValueT) -> bool: + return all(condition(item) for condition in self.conditions) + + +class _AttrComparator(typing.Generic[ValueT]): + __slots__ = ("getter", "expected_value") + + def __init__(self, attr_name: str, expected_value: typing.Any) -> None: + if attr_name.startswith("."): + attr_name = attr_name[1:] + self.getter = operator.attrgetter(attr_name) + self.expected_value = expected_value + + def __call__(self, item: ValueT) -> bool: + return bool(self.getter(item) == self.expected_value) + + +class LazyIterator(typing.Generic[ValueT], abc.ABC): """A set of results that are fetched asynchronously from the API as needed. This is a `typing.AsyncIterable` and `typing.AsyncIterator` with several @@ -90,7 +115,78 @@ class LazyIterator(typing.Generic[IteratorT], abc.ABC): __slots__ = () - def enumerate(self, *, start: int = 0) -> LazyIterator[typing.Tuple[int, IteratorT]]: + def map( + self, transformation: typing.Union[typing.Callable[[ValueT], AnotherValueT], str], + ) -> LazyIterator[AnotherValueT]: + """Map the values to a different value. + + Parameters + ---------- + transformation : typing.Callable[[ValueT], bool] or str + The function to use to map the attribute. This may alternatively + be a string attribute name to replace the input value with. You + can provide nested attributes using the `.` operator. + + Returns + ------- + LazyIterator[AnotherValueT] + LazyIterator that maps each value to another value. + """ + if isinstance(transformation, str): + if transformation.startswith("."): + transformation = transformation[1:] + transformation = operator.attrgetter(transformation) + return _MappingLazyIterator(self, transformation) + + def filter( + self, + *predicates: typing.Union[typing.Tuple[str, typing.Any], typing.Callable[[ValueT], bool]], + **attrs: typing.Any, + ) -> LazyIterator[ValueT]: + """Filter the items by one or more conditions that must all be `True`. + + Parameters + ---------- + *predicates : typing.Callable[[ValueT], bool] or typing.Tuple[str, typing.Any] + Predicates to invoke. These are functions that take a value and + return `True` if it is of interest, or `False` otherwise. These + may instead include 2-`tuple` objects consisting of a `str` + attribute name (nested attributes are referred to using the `.` + operator), and values to compare for equality. This allows you + to specify conditions such as `members.filter(("user.bot", True))`. + **attrs : typing.Any + Alternative to passing 2-tuples. Cannot specify nested attributes + using this method. + + Returns + ------- + LazyIterator[ValueT] + LazyIterator that only emits values where all conditions are + matched. + """ + if not predicates and not attrs: + raise TypeError("You should provide at least one predicate to filter()") + + conditions: typing.List[typing.Callable[[ValueT], bool]] = [] + + for p in predicates: + if isinstance(p, tuple): + name, value = p + tuple_comparator: _AttrComparator[ValueT] = _AttrComparator(name, value) + conditions.append(tuple_comparator) + else: + conditions.append(p) + + for name, value in attrs.items(): + attr_comparator: _AttrComparator[ValueT] = _AttrComparator(name, value) + conditions.append(attr_comparator) + + if len(conditions) > 1: + return _FilteredLazyIterator(self, _AllConditions(conditions)) + else: + return _FilteredLazyIterator(self, conditions[0]) + + def enumerate(self, *, start: int = 0) -> LazyIterator[typing.Tuple[int, ValueT]]: """Enumerate the paginated results lazily. This behaves as an asyncio-friendly version of `builtins.enumerate` @@ -134,7 +230,7 @@ def enumerate(self, *, start: int = 0) -> LazyIterator[typing.Tuple[int, Iterato """ return _EnumeratedLazyIterator(self, start=start) - def limit(self, limit: int) -> LazyIterator[IteratorT]: + def limit(self, limit: int) -> LazyIterator[ValueT]: """Limit the number of items you receive from this async iterator. Parameters @@ -159,45 +255,45 @@ def limit(self, limit: int) -> LazyIterator[IteratorT]: def _complete(self) -> typing.NoReturn: raise StopAsyncIteration("No more items exist in this paginator. It has been exhausted.") from None - def __aiter__(self) -> LazyIterator[IteratorT]: + def __aiter__(self) -> LazyIterator[ValueT]: # We are our own iterator. return self - async def _fetch_all(self) -> typing.Sequence[IteratorT]: + async def _fetch_all(self) -> typing.Sequence[ValueT]: return [item async for item in self] - def __await__(self) -> typing.Generator[typing.Sequence[IteratorT], None, None]: - yield from self._fetch_all().__await__() + def __await__(self) -> typing.Generator[None, None, typing.Sequence[ValueT]]: + return self._fetch_all().__await__() @abc.abstractmethod - async def __anext__(self) -> IteratorT: + async def __anext__(self) -> ValueT: ... -class _EnumeratedLazyIterator(typing.Generic[IteratorT], LazyIterator[typing.Tuple[int, IteratorT]]): +class _EnumeratedLazyIterator(typing.Generic[ValueT], LazyIterator[typing.Tuple[int, ValueT]]): __slots__ = ("_i", "_paginator") - def __init__(self, paginator: LazyIterator[IteratorT], *, start: int) -> None: + def __init__(self, paginator: LazyIterator[ValueT], *, start: int) -> None: self._i = start self._paginator = paginator - async def __anext__(self) -> typing.Tuple[int, IteratorT]: + async def __anext__(self) -> typing.Tuple[int, ValueT]: pair = self._i, await self._paginator.__anext__() self._i += 1 return pair -class _LimitedLazyIterator(typing.Generic[IteratorT], LazyIterator[IteratorT]): +class _LimitedLazyIterator(typing.Generic[ValueT], LazyIterator[ValueT]): __slots__ = ("_paginator", "_count", "_limit") - def __init__(self, paginator: LazyIterator[IteratorT], limit: int) -> None: + def __init__(self, paginator: LazyIterator[ValueT], limit: int) -> None: if limit <= 0: raise ValueError("limit must be positive and non-zero") self._paginator = paginator self._count = 0 self._limit = limit - async def __anext__(self) -> IteratorT: + async def __anext__(self) -> ValueT: if self._count >= self._limit: self._complete() @@ -206,18 +302,45 @@ async def __anext__(self) -> IteratorT: return next_item -class _BufferedLazyIterator(typing.Generic[IteratorT], LazyIterator[IteratorT]): +class _FilteredLazyIterator(typing.Generic[ValueT], LazyIterator[ValueT]): + __slots__ = ("_paginator", "_predicate") + + def __init__(self, paginator: LazyIterator[ValueT], predicate: typing.Callable[[ValueT], bool]) -> None: + self._paginator = paginator + self._predicate = predicate + + async def __anext__(self) -> ValueT: + async for item in self._paginator: + if self._predicate(item): + return item + raise StopAsyncIteration + + +class _MappingLazyIterator(typing.Generic[AnotherValueT, ValueT], LazyIterator[ValueT]): + __slots__ = ("_paginator", "_transformation") + + def __init__( + self, paginator: LazyIterator[AnotherValueT], transformation: typing.Callable[[AnotherValueT], ValueT], + ) -> None: + self._paginator = paginator + self._transformation = transformation + + async def __anext__(self) -> ValueT: + return self._transformation(await self._paginator.__anext__()) + + +class _BufferedLazyIterator(typing.Generic[ValueT], LazyIterator[ValueT]): __slots__ = ("_buffer",) def __init__(self) -> None: - empty_genexp = typing.cast(typing.Generator[IteratorT, None, None], (_ for _ in ())) - self._buffer: typing.Optional[typing.Generator[IteratorT, None, None]] = empty_genexp + empty_genexp = typing.cast(typing.Generator[ValueT, None, None], (_ for _ in ())) + self._buffer: typing.Optional[typing.Generator[ValueT, None, None]] = empty_genexp @abc.abstractmethod - async def _next_chunk(self) -> typing.Optional[typing.Generator[IteratorT, None, None]]: + async def _next_chunk(self) -> typing.Optional[typing.Generator[ValueT, None, None]]: ... - async def __anext__(self) -> IteratorT: + async def __anext__(self) -> ValueT: # This sneaky snippet of code lets us use generators rather than lists. # This is important, as we can use this to make generators that # deserialize loads of items lazy. If we only want 10 messages of From 1aa7880c021a511d1edb7a42d99ae0f8ead4fc7b Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Fri, 19 Jun 2020 11:57:31 +0000 Subject: [PATCH 552/922] Update tests.yml to fix twemoji mapping ruleset. --- ci/gitlab/tests.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/ci/gitlab/tests.yml b/ci/gitlab/tests.yml index 98d8524068..0f786b74de 100644 --- a/ci/gitlab/tests.yml +++ b/ci/gitlab/tests.yml @@ -120,8 +120,9 @@ test:twemoji_mapping: rules: - if: "$CI_PIPELINE_SOURCE == 'schedule' && $TEST_TWEMOJI_MAPPING != null" - changes: - - hikari/emojis.py - - hikari.utilities/urls.py + - hikari/models/emojis.py + - hikari/utilities/files.py + - ci/twemoji.nox.py script: - apt-get install -qy git gcc g++ make - pip install nox From eaf09aaafb8e1441d4927c1e41059c0d6ffecf2a Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Sat, 20 Jun 2020 08:48:26 +0100 Subject: [PATCH 553/922] Fixed issue with naming in sphinx inventory. --- docs/documentation.mako | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/documentation.mako b/docs/documentation.mako index 3ef3474727..002435df83 100644 --- a/docs/documentation.mako +++ b/docs/documentation.mako @@ -275,7 +275,7 @@ project_inventory.objects.append( sphobjinv.DataObjStr( - name = f"{v.module.name}.{v.name}", + name = f"{v.module.name}.{v.qualname}", domain = "py", role = "var", uri = v.url(), @@ -322,7 +322,7 @@ if not redirect: project_inventory.objects.append( sphobjinv.DataObjStr( - name = f"{f.module.name}.{f.name}", + name = f"{f.module.name}.{f.qualname}", domain = "py", role = "func", uri = f.url(), @@ -386,7 +386,7 @@ if not redirect: project_inventory.objects.append( sphobjinv.DataObjStr( - name = f"{c.module.name}.{c.name}", + name = f"{c.module.name}.{c.qualname}", domain = "py", role = "class", uri = c.url(), @@ -555,7 +555,7 @@ priority = "1", dispname = "-", ) - ) + ) %>
    From ce31a2b47109284de2a268cf53f9283f35dc0325 Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Sat, 20 Jun 2020 18:58:36 +0100 Subject: [PATCH 554/922] Removed tracing voodoo. --- hikari/net/http_client.py | 20 +-- hikari/net/rest.py | 26 +++ hikari/net/tracing.py | 229 --------------------------- tests/hikari/net/test_http_client.py | 23 --- tests/hikari/net/test_tracing.py | 51 ------ 5 files changed, 31 insertions(+), 318 deletions(-) delete mode 100644 hikari/net/tracing.py delete mode 100644 tests/hikari/net/test_tracing.py diff --git a/hikari/net/http_client.py b/hikari/net/http_client.py index b76792bbee..1424af239d 100644 --- a/hikari/net/http_client.py +++ b/hikari/net/http_client.py @@ -28,13 +28,15 @@ import typing import aiohttp.client +import aiohttp.connector +import aiohttp.http_writer import aiohttp.typedefs from hikari import errors from hikari.net import http_settings -from hikari.net import tracing from hikari.utilities import data_binding + try: # noinspection PyProtectedMember RequestContextManager = aiohttp.client._RequestContextManager @@ -65,7 +67,7 @@ class HTTPClient(abc.ABC): If `None`, defaults are used. debug : bool Defaults to `False`. If `True`, then a lot of contextual information - regarding low-level HTTP communication will be logged to the _debug + regarding low-level HTTP communication will be logged to the debug logger on this class. """ @@ -84,13 +86,7 @@ class HTTPClient(abc.ABC): """HTTP settings in-use.""" _debug: bool - """`True` if _debug mode is enabled. `False` otherwise.""" - - _tracers: typing.List[tracing.BaseTracer] - """Request _tracers. - - These can be used to intercept HTTP request events on a low level. - """ + """`True` if debug mode is enabled. `False` otherwise.""" def __init__( self, @@ -107,7 +103,6 @@ def __init__( self._client_session: typing.Optional[aiohttp.ClientSession] = None self._config = config self._debug = debug - self._tracers = [(tracing.DebugTracer(self.logger) if debug else tracing.CFRayTracer(self.logger))] @typing.final async def __aenter__(self) -> HTTPClient: @@ -151,7 +146,6 @@ def get_client_session(self) -> aiohttp.ClientSession: trust_env=self._config.trust_env, version=aiohttp.HttpVersion11, json_serialize=json.dumps, - trace_configs=[t.trace_config for t in self._tracers], ) self.logger.debug("acquired new client session object %r", self._client_session) return self._client_session @@ -199,9 +193,6 @@ def _perform_request( elif isinstance(body, aiohttp.FormData): kwargs["data"] = body - trace_request_ctx = types.SimpleNamespace() - trace_request_ctx.request_body = body - return self.get_client_session().request( method=method, url=url, @@ -214,7 +205,6 @@ def _perform_request( verify_ssl=self._config.verify_ssl, ssl_context=self._config.ssl_context, timeout=self._config.request_timeout, - trace_request_ctx=trace_request_ctx, **kwargs, ) diff --git a/hikari/net/rest.py b/hikari/net/rest.py index 02569e0258..f673f3234e 100644 --- a/hikari/net/rest.py +++ b/hikari/net/rest.py @@ -27,6 +27,7 @@ import http import math import typing +import uuid import aiohttp @@ -214,12 +215,37 @@ async def _request_once( # Wait for any ratelimits to finish. await asyncio.gather(self.buckets.acquire(compiled_route), self.global_rate_limit.acquire()) + uuid4 = str(uuid.uuid4()) + + if self._debug: + headers_str = "\n".join(f"\t\t{name}:{value}" for name, value in headers.items()) + self.logger.debug( + "%s %s %s\n\theaders:\n%s\n\tbody:\n\t\t%r", uuid4, compiled_route.method, url, headers_str, body + ) + else: + self.logger.debug("%s %s %s", uuid4, compiled_route.method, url) + # Make the request. # noinspection PyUnresolvedReferences response = await self._perform_request( method=compiled_route.method, url=url, headers=headers, body=body, query=query ) + if self._debug: + headers_str = "\n".join( + f"\t\t{name.decode('utf-8')}:{value.decode('utf-8')}" for name, value in response.raw_headers + ) + self.logger.debug( + "%s %s %s\n\theaders:\n%s\n\tbody:\n\t\t%r", + uuid4, + response.status, + response.reason, + headers_str, + await response.read(), + ) + else: + self.logger.debug("%s %s %s", uuid4, response.status, response.reason) + # Ensure we aren't rate limited, and update rate limiting headers where appropriate. await self._parse_ratelimits(compiled_route, response) diff --git a/hikari/net/tracing.py b/hikari/net/tracing.py deleted file mode 100644 index 2993a08fe8..0000000000 --- a/hikari/net/tracing.py +++ /dev/null @@ -1,229 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright © Nekoka.tt 2019-2020 -# -# This file is part of Hikari. -# -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . -"""Provides logging support for HTTP requests internally.""" -from __future__ import annotations - -__all__: typing.Final[typing.List[str]] = ["BaseTracer", "CFRayTracer", "DebugTracer"] - -import functools -import io -import logging -import time -import typing -import uuid - -import aiohttp.abc - -from hikari.net import strings - - -class BaseTracer: - """Base type for tracing HTTP requests.""" - - __slots__ = ("logger",) - - def __init__(self, logger: logging.Logger) -> None: - self.logger = logger - - @functools.cached_property - def trace_config(self) -> aiohttp.TraceConfig: - """Generate a trace config for aiohttp.""" - tc = aiohttp.TraceConfig() - - for name in dir(self): - if name.startswith("on_") and name in dir(tc): - getattr(tc, name).append(getattr(self, name)) - - return tc - - -class CFRayTracer(BaseTracer): - """Regular _debug logging of requests to a Cloudflare resource. - - Logs information about endpoints being hit, response latency, and any - Cloudflare rays in the response. - """ - - @typing.no_type_check - async def on_request_start(self, _, ctx, params): - """Log an outbound request.""" - ctx.identifier = f"request_id:{uuid.uuid4()}" - ctx.start_time = time.perf_counter() - - self.logger.debug( - "%s %s [%s:%s, %s:%s] [%s]", - params.method, - params.url, - strings.CONTENT_TYPE_HEADER, - params.headers.get(strings.CONTENT_TYPE_HEADER), - strings.ACCEPT_HEADER, - params.headers.get(strings.ACCEPT_HEADER), - ctx.identifier, - ) - - @typing.no_type_check - async def on_request_end(self, _, ctx, params): - """Log an inbound response.""" - latency = round((time.perf_counter() - ctx.start_time) * 1_000, 1) - response = params.response - self.logger.debug( - "%s %s after %sms [%s:%s, %s:%s, %s:%s, %s:%s] [%s]", - response.status, - response.reason, - latency, - strings.CONTENT_TYPE_HEADER, - response.headers.get(strings.CONTENT_TYPE_HEADER), - strings.CONTENT_LENGTH_HEADER, - response.headers.get(strings.CONTENT_LENGTH_HEADER, 0), - strings.CF_RAY_HEADER, - response.headers.get(strings.CF_RAY_HEADER), - strings.CF_REQUEST_ID_HEADER, - response.headers.get(strings.CF_REQUEST_ID_HEADER), - ctx.identifier, - ) - - -class _ByteStreamWriter(aiohttp.abc.AbstractStreamWriter): - def __init__(self) -> None: - self.bio = io.BytesIO() - - async def write(self, data: typing.Union[bytes, bytearray]) -> None: - self.bio.write(data) - - write_eof = NotImplemented - drain = NotImplemented - enable_compression = NotImplemented - enable_chunking = NotImplemented - write_headers = NotImplemented - - -class DebugTracer(BaseTracer): - """Provides verbose _debug logging of requests. - - This logs several pieces of information during an AIOHTTP request such as - request headers and body chunks, response headers, response body chunks, - and other events such as DNS cache hits/misses, connection pooling events, - and other pieces of information that can be incredibly useful for debugging - performance issues and API issues. - - !!! warning - This may log potentially sensitive information such as authorization - tokens, so ensure those are removed from _debug logs before proceeding - to send logs to anyone. - """ - - @staticmethod - async def _format_body(body: typing.Any) -> str: - if isinstance(body, aiohttp.FormData): - # We have to either copy the internal multipart writer, or we have - # to make a dummy second instance and read from that. I am putting - # my bets on the second option, simply because it reduces the - # risk of screwing up the original payload in some weird edge case. - # These objects have stateful stuff somewhere by the looks. - copy_of_data = aiohttp.FormData() - setattr(copy_of_data, "_fields", getattr(copy_of_data, "_fields")) - byte_writer = _ByteStreamWriter() - await copy_of_data().write(byte_writer) - return repr(byte_writer.bio.read()) - return repr(body) - - @typing.no_type_check - async def on_request_start(self, _, ctx, params): - """Log an outbound request.""" - ctx.identifier = f"request_id:{uuid.uuid4()}" - ctx.start_time = time.perf_counter() - - body = ( - await self._format_body(ctx.trace_request_ctx.request_body) - if hasattr(ctx.trace_request_ctx, "request_body") - else "" - ) - - self.logger.debug( - "%s %s [%s]\n request headers: %s\n request body: %s", - params.method, - params.url, - ctx.identifier, - dict(params.headers), - body, - ) - - @typing.no_type_check - async def on_request_end(self, _, ctx, params): - """Log an inbound response.""" - latency = round((time.perf_counter() - ctx.start_time) * 1_000, 2) - response = params.response - - body = ( - (await self._format_body(await response.read())) - if strings.CONTENT_TYPE_HEADER in response.headers - else "no-content" - ) - - self.logger.debug( - "%s %s %s after %sms [%s]\n response headers: %s\n response body: %s", - response.real_url, - response.status, - response.reason, - latency, - ctx.identifier, - dict(response.headers), - body, - ) - - @typing.no_type_check - async def on_request_exception(self, _, ctx, params): - """Log an error while making a request.""" - self.logger.debug("encountered exception [%s]", ctx.identifier, exc_info=params.exception) - - @typing.no_type_check - async def on_connection_queued_start(self, _, ctx, __): - """Log when we have to wait for a new connection in the pool.""" - self.logger.debug("is waiting for a connection [%s]", ctx.identifier) - - @typing.no_type_check - async def on_connection_reuseconn(self, _, ctx, __): - """Log when we re-use an existing connection in the pool.""" - self.logger.debug("has acquired an existing connection [%s]", ctx.identifier) - - @typing.no_type_check - async def on_connection_create_end(self, _, ctx, __): - """Log when we create a new connection in the pool.""" - self.logger.debug("has created a new connection [%s]", ctx.identifier) - - @typing.no_type_check - async def on_dns_cache_hit(self, _, ctx, params): - """Log when we reuse the DNS cache and do not have to look up an IP.""" - self.logger.debug("has retrieved the IP of %s from the DNS cache [%s]", params.host, ctx.identifier) - - @typing.no_type_check - async def on_dns_cache_miss(self, _, ctx, params): - """Log when we have to query a DNS server for an IP address.""" - self.logger.debug("will perform DNS lookup of new host %s [%s]", params.host, ctx.identifier) - - # noinspection PyMethodMayBeStatic - @typing.no_type_check - async def on_dns_resolvehost_start(self, _, ctx, __): - """Store the time the DNS lookup started at.""" - ctx.dns_start_time = time.perf_counter() - - @typing.no_type_check - async def on_dns_resolvehost_end(self, _, ctx, params): - """Log how long a DNS lookup of an IP took to perform.""" - latency = round((time.perf_counter() - ctx.dns_start_time) * 1_000, 2) - self.logger.debug("DNS lookup of host %s took %sms [%s]", params.host, latency, ctx.identifier) diff --git a/tests/hikari/net/test_http_client.py b/tests/hikari/net/test_http_client.py index 4349f9b740..8809dea722 100644 --- a/tests/hikari/net/test_http_client.py +++ b/tests/hikari/net/test_http_client.py @@ -24,7 +24,6 @@ from hikari.net import http_client from hikari.net import http_settings -from hikari.net import tracing from tests.hikari import _helpers @@ -42,19 +41,6 @@ def client(client_session): yield client -@pytest.mark.asyncio -class TestInit: - async def test_CFRayTracer_used_for_non_debug(self): - async with http_client.HTTPClient(debug=False, logger=mock.MagicMock()) as client: - assert len(client._tracers) == 1 - assert isinstance(client._tracers[0], tracing.CFRayTracer) - - async def test_DebugTracer_used_for_debug(self): - async with http_client.HTTPClient(debug=True, logger=mock.MagicMock()) as client: - assert len(client._tracers) == 1 - assert isinstance(client._tracers[0], tracing.DebugTracer) - - @pytest.mark.asyncio class TestAcquireClientSession: async def test_acquire_creates_new_session_if_one_does_not_exist(self, client): @@ -70,7 +56,6 @@ async def test_acquire_creates_new_session_if_one_does_not_exist(self, client): trust_env=client._config.trust_env, version=aiohttp.HttpVersion11, json_serialize=json.dumps, - trace_configs=[t.trace_config for t in client._tracers], ) async def test_acquire_repeated_calls_caches_client_session(self, client): @@ -110,9 +95,6 @@ async def test_perform_request_form_data(self, client, client_session): form_data = aiohttp.FormData() - trace_request_ctx = types.SimpleNamespace() - trace_request_ctx.request_body = form_data - expected_response = mock.MagicMock() client_session.request = mock.AsyncMock(return_value=expected_response) @@ -134,7 +116,6 @@ async def test_perform_request_form_data(self, client, client_session): verify_ssl=client._config.verify_ssl, ssl_context=client._config.ssl_context, timeout=client._config.request_timeout, - trace_request_ctx=trace_request_ctx, ) async def test_perform_request_json(self, client, client_session): @@ -149,9 +130,6 @@ async def test_perform_request_json(self, client, client_session): req = {"hello": "world"} - trace_request_ctx = types.SimpleNamespace() - trace_request_ctx.request_body = req - expected_response = mock.MagicMock() client_session.request = mock.AsyncMock(return_value=expected_response) @@ -173,7 +151,6 @@ async def test_perform_request_json(self, client, client_session): verify_ssl=client._config.verify_ssl, ssl_context=client._config.ssl_context, timeout=client._config.request_timeout, - trace_request_ctx=trace_request_ctx, ) diff --git a/tests/hikari/net/test_tracing.py b/tests/hikari/net/test_tracing.py deleted file mode 100644 index a56d120410..0000000000 --- a/tests/hikari/net/test_tracing.py +++ /dev/null @@ -1,51 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright © Nekoka.tt 2019-2020 -# -# This file is part of Hikari. -# -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . -import aiohttp -import mock - -from hikari.net import tracing - - -class TestBaseTracer: - def test_sets_logger(self): - logger = mock.MagicMock() - impl = type("Impl", (tracing.BaseTracer,), {})(logger) - assert impl.logger is logger - - def test_trace_config_is_cached(self): - logger = mock.MagicMock() - impl = type("Impl", (tracing.BaseTracer,), {})(logger) - tc = impl.trace_config - assert impl.trace_config is tc - - def test_trace_config_is_instance_of_TraceConfig(self): - logger = mock.MagicMock() - impl = type("Impl", (tracing.BaseTracer,), {})(logger) - assert isinstance(impl.trace_config, aiohttp.TraceConfig) - - def test_trace_config_collects_methods_matching_name_prefix(self): - class Impl(tracing.BaseTracer): - def on_connection_create_end(self): - pass - - def this_should_be_ignored(self): - pass - - i = Impl(mock.MagicMock()) - - assert i.on_connection_create_end in i.trace_config.on_connection_create_end From 3f9f946fc8fc3599aec5e97c5e696dcae6fb52ad Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Sat, 20 Jun 2020 19:57:45 +0100 Subject: [PATCH 555/922] Began gateway retesting. --- hikari/models/guilds.py | 2 +- hikari/net/gateway.py | 10 +++- tests/hikari/net/test_gateway.py | 99 ++++++++++++++++++++++++++++++++ 3 files changed, 109 insertions(+), 2 deletions(-) diff --git a/hikari/models/guilds.py b/hikari/models/guilds.py index 5c42fe6cfa..7e9ff7adcf 100644 --- a/hikari/models/guilds.py +++ b/hikari/models/guilds.py @@ -196,7 +196,7 @@ class GuildVerificationLevel(int, enum.Enum): """Represents the level of verification of a guild.""" NONE = 0 - """Unrestricted""" + """Unrestricted.""" LOW = 1 """Must have a verified email on their account.""" diff --git a/hikari/net/gateway.py b/hikari/net/gateway.py index 961a0cb796..47dfc0799d 100644 --- a/hikari/net/gateway.py +++ b/hikari/net/gateway.py @@ -89,6 +89,10 @@ class Gateway(http_client.HTTPClient, component.IComponent): fragments. use_compression : bool If `True`, then transport compression is enabled. + use_etf : bool + If `True`, ETF is used to receive payloads instead of JSON. Defaults to + `False`. Currently, setting this to `True` will raise a + `NotImplementedError`. version : int Gateway API version to use. @@ -175,6 +179,7 @@ def __init__( token: str, url: str, use_compression: bool = True, + use_etf: bool = False, version: int = 6, ) -> None: super().__init__(config=config, debug=debug, logger=reflect.get_logger(self, str(shard_id))) @@ -210,7 +215,10 @@ def __init__( scheme, netloc, path, params, _, _ = urllib.parse.urlparse(url, allow_fragments=True) - new_query = dict(v=int(version), encoding="json") + if use_etf: + raise NotImplementedError("ETF support is not available currently") + + new_query = dict(v=int(version), encoding="etf" if use_etf else "json") if use_compression: # payload compression new_query["compress"] = "zlib-stream" diff --git a/tests/hikari/net/test_gateway.py b/tests/hikari/net/test_gateway.py index 3b080b938f..02f147b17b 100644 --- a/tests/hikari/net/test_gateway.py +++ b/tests/hikari/net/test_gateway.py @@ -15,3 +15,102 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . +import asyncio + +import mock +import pytest + +from hikari.net import gateway + + +@pytest.fixture() +def client(): + return gateway.Gateway(url="wss://gateway.discord.gg", token="lol", app=mock.MagicMock(), config=mock.MagicMock(),) + + +class TestInit: + @pytest.mark.parametrize( + ["v", "use_compression", "expect"], + [ + (6, False, "v=6&encoding=json"), + (6, True, "v=6&encoding=json&compress=zlib-stream"), + (7, False, "v=7&encoding=json"), + (7, True, "v=7&encoding=json&compress=zlib-stream"), + ], + ) + def test_url_is_correct_json(self, v, use_compression, expect): + g = gateway.Gateway( + app=mock.MagicMock(), + config=mock.MagicMock(), + token=mock.MagicMock(), + url="wss://gaytewhuy.discord.meh", + version=v, + use_etf=False, + use_compression=use_compression, + ) + + assert g.url == f"wss://gaytewhuy.discord.meh?{expect}" + + @pytest.mark.parametrize(["v", "use_compression"], [(6, False), (6, True), (7, False), (7, True),]) + def test_using_etf_is_unsupported(self, v, use_compression): + with pytest.raises(NotImplementedError): + gateway.Gateway( + app=mock.MagicMock(), + config=mock.MagicMock(), + token=mock.MagicMock(), + url="wss://erlpack-is-broken-lol.discord.meh", + version=v, + use_etf=True, + use_compression=use_compression, + ) + + +class TestAppProperty: + def test_returns_app(self): + app = mock.MagicMock() + g = gateway.Gateway(url="wss://gateway.discord.gg", token="lol", app=app, config=mock.MagicMock()) + assert g.app is app + + +class TestIsAliveProperty: + def test_is_alive(self, client): + client.connected_at = 1234 + assert client.is_alive + + def test_not_is_alive(self, client): + client.connected_at = float("nan") + assert not client.is_alive + + +@pytest.mark.asyncio +class TestStart: + @pytest.mark.parametrize("shard_id", [0, 1, 2]) + async def test_starts_task(self, event_loop, shard_id): + g = gateway.Gateway( + url="wss://gateway.discord.gg", + token="lol", + app=mock.MagicMock(), + config=mock.MagicMock(), + shard_id=shard_id, + shard_count=100, + ) + + g._handshake_event = mock.MagicMock() + g._handshake_event.wait = mock.AsyncMock() + g._run = mock.MagicMock() + + future = event_loop.create_future() + future.set_result(None) + + with mock.patch.object(asyncio, "create_task", return_value=future) as create_task: + result = await g.start() + assert result is future + create_task.assert_called_once_with(g._run(), name=f"shard {shard_id} keep-alive") + + async def test_waits_for_ready(self, client): + client._handshake_event = mock.MagicMock() + client._handshake_event.wait = mock.AsyncMock() + client._run = mock.AsyncMock() + + await client.start() + client._handshake_event.wait.assert_awaited_once_with() From 48348365bf535a9e36f698c05d84abbcf6f35f84 Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Sun, 21 Jun 2020 10:26:07 +0000 Subject: [PATCH 556/922] Update releases.yml --- ci/gitlab/releases.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ci/gitlab/releases.yml b/ci/gitlab/releases.yml index e72c6bf82f..44c691fdb7 100644 --- a/ci/gitlab/releases.yml +++ b/ci/gitlab/releases.yml @@ -55,7 +55,7 @@ release:staging: environment: name: staging - url: https://nekokatt.gitlab.io/hikari # FIXME: change to staging URL when appropriate. + url: https://nekokatt.gitlab.io/hikari extends: .release script: - nox -s deploy --no-error-on-external-run From 21c623add5253526b4a2dc178761d42a9ebd0461 Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Sun, 21 Jun 2020 15:35:52 +0100 Subject: [PATCH 557/922] Changed http_client to store weakrefs and destroy client sessions on finalization. --- hikari/net/gateway.py | 21 +- hikari/net/http_client.py | 15 +- tests/hikari/_helpers.py | 462 ----------------------- tests/hikari/hikari_test_helpers.py | 237 ++++++++++++ tests/hikari/net/test_buckets.py | 20 +- tests/hikari/net/test_gateway.py | 243 +++++++++++- tests/hikari/net/test_http_client.py | 27 +- tests/hikari/net/test_ratelimits.py | 16 +- tests/hikari/utilities/test_aio.py | 3 +- tests/hikari/utilities/test_undefined.py | 7 +- 10 files changed, 555 insertions(+), 496 deletions(-) delete mode 100644 tests/hikari/_helpers.py create mode 100644 tests/hikari/hikari_test_helpers.py diff --git a/hikari/net/gateway.py b/hikari/net/gateway.py index 47dfc0799d..50cff2656f 100644 --- a/hikari/net/gateway.py +++ b/hikari/net/gateway.py @@ -162,6 +162,15 @@ class _SocketClosed(RuntimeError): class _InvalidSession(RuntimeError): can_resume: bool = False + _RESTART_RATELIMIT_WINDOW: typing.Final[typing.ClassVar[float]] = 30.0 + """If the shard restarts more than once within this period of time, then + exponentially back-off to prevent spamming the gateway or tanking the CPU. + + This is potentially important if the internet connection turns off, as the + bot will simply attempt to reconnect repeatedly until the connection + resumes. + """ + def __init__( self, *, @@ -277,21 +286,27 @@ async def _run(self) -> None: # we cannot connect successfully. It is a hack, but it works. self._handshake_event.set() # Close the aiohttp client session. - await super().close() + # Didn't use `super` as I can mock this to check without breaking + # the entire inheritance conduit in a patch context. + await http_client.HTTPClient.close(self) async def _run_once(self) -> bool: # returns `True` if we can reconnect, or `False` otherwise. self._request_close_event.clear() - if self._now() - self._last_run_started_at < 30: + if self._now() - self._last_run_started_at < self._RESTART_RATELIMIT_WINDOW: # Interrupt sleep immediately if a request to close is fired. wait_task = asyncio.create_task( - self._request_close_event.wait(), name=f"gateway client {self._shard_id} backing off" + self._request_close_event.wait(), name=f"gateway shard {self._shard_id} backing off" ) try: backoff = next(self._backoff) self.logger.debug("backing off for %ss", backoff) await asyncio.wait_for(wait_task, timeout=backoff) + + # If this line gets reached, the wait didn't time out, meaning + # the user told the client to shut down gracefully before the + # backoff completed. return False except asyncio.TimeoutError: pass diff --git a/hikari/net/http_client.py b/hikari/net/http_client.py index 1424af239d..e87909796a 100644 --- a/hikari/net/http_client.py +++ b/hikari/net/http_client.py @@ -26,6 +26,7 @@ import logging import types import typing +import weakref import aiohttp.client import aiohttp.connector @@ -74,6 +75,7 @@ class HTTPClient(abc.ABC): __slots__ = ( "logger", "_client_session", + "_client_session_ref", "_config", "_debug", "_tracers", @@ -101,6 +103,7 @@ def __init__( config = http_settings.HTTPSettings() self._client_session: typing.Optional[aiohttp.ClientSession] = None + self._client_session_ref: typing.Optional[weakref.ProxyType] = None self._config = config self._debug = debug @@ -114,6 +117,11 @@ async def __aexit__( ) -> None: await self.close() + def __del__(self) -> None: + # Let the client session get garbage collected. + self._client_session = None + self._client_session_ref = None + async def close(self) -> None: """Close the client safely.""" if self._client_session is not None: @@ -136,7 +144,7 @@ def get_client_session(self) -> aiohttp.ClientSession: Returns ------- - aiohttp.ClientSession + weakref.proxy of aiohttp.ClientSession The client session to use for requests. """ if self._client_session is None: @@ -147,8 +155,11 @@ def get_client_session(self) -> aiohttp.ClientSession: version=aiohttp.HttpVersion11, json_serialize=json.dumps, ) + self._client_session_ref = weakref.proxy(self._client_session) self.logger.debug("acquired new client session object %r", self._client_session) - return self._client_session + + # Only return a weakref, to prevent callees obtaining ownership. + return typing.cast(aiohttp.ClientSession, self._client_session_ref) @typing.final def _perform_request( diff --git a/tests/hikari/_helpers.py b/tests/hikari/_helpers.py deleted file mode 100644 index 7e12a419e7..0000000000 --- a/tests/hikari/_helpers.py +++ /dev/null @@ -1,462 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright © Nekoka.tt 2019-2020 -# -# This file is part of Hikari. -# -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . - -import asyncio -import copy -import functools -import inspect -import logging -import os -import queue -import re -import threading -import time -import typing -import warnings -import weakref - -import async_timeout -import mock -import pytest - -_LOGGER = logging.getLogger(__name__) - - -def purge_loop(): - """Empties the event loop properly.""" - loop = asyncio.get_event_loop() - for item in loop._scheduled: - _LOGGER.info("Cancelling scheduled item in event loop {}", item) - item.cancel() - for item in loop._ready: - _LOGGER.info("Cancelling ready item in event loop {}", item) - item.cancel() - loop._scheduled.clear() - loop._ready.clear() - loop.close() - - -def mock_methods_on(obj, except_=(), also_mock=()): - # Mock any methods we don't care about. also_mock is a collection of attribute names that we can eval to access - # and mock specific application with a coroutine mock to mock other external application quickly :) - magics = ["__enter__", "__exit__", "__aenter__", "__aexit__", "__iter__", "__aiter__"] - - except_ = set(except_) - also_mock = set(also_mock) - checked = set() - - def predicate(name, member): - is_callable = callable(member) - has_name = bool(name) - name_is_allowed = name not in except_ - - if not name_is_allowed: - checked.add(name) - - is_not_disallowed_magic = not name.startswith("__") or name in magics - # print(name, is_callable, has_name, name_is_allowed, is_not_disallowed_magic) - return is_callable and has_name and name_is_allowed and is_not_disallowed_magic - - copy_ = copy.copy(obj) - for name, method in inspect.getmembers(obj): - if predicate(name, method): - # print('Mocking', name, 'on', type(obj)) - - if asyncio.iscoroutinefunction(method): - _mock = mock.AsyncMock() - else: - _mock = mock.MagicMock() - - setattr(copy_, name, _mock) - - for expr in also_mock: - owner, _, attr = ("copy_." + expr).rpartition(".") - # sue me. - owner = eval(owner) - setattr(owner, attr, mock.MagicMock()) - - assert not (except_ - checked), f"Some attributes didn't exist, so were not mocked: {except_ - checked}" - - return copy_ - - -def assert_raises(test=None, *, type_, checks=()): - def decorator(test): - @pytest.mark.asyncio - @functools.wraps(test) - async def impl(*args, **kwargs): - try: - result = test(*args, **kwargs) - if asyncio.iscoroutine(result): - await result - assert False, f"{type_.__name__} was not raised." - except type_ as ex: - logging.exception("Caught exception within test type raising bounds", exc_info=ex) - for i, check in enumerate(checks, start=1): - assert check(ex), f"Check #{i} ({check}) failed" - except AssertionError as ex: - raise ex - except BaseException as ex: - raise AssertionError(f"Expected {type_.__name__} to be raised but got {type(ex).__name__}") from ex - - return impl - - if test is not None: - return decorator(test) - else: - return decorator - - -def assert_does_not_raise(test=None, *, type_=Exception, excludes=(AssertionError,)): - def decorator(test): - @pytest.mark.asyncio - @functools.wraps(test) - async def impl(*args, **kwargs): - try: - result = test(*args, **kwargs) - if asyncio.iscoroutine(result): - await result - except type_ as ex: - if not any(isinstance(ex, exclude) for exclude in excludes): - assert False, f"{type_.__qualname__} thrown unexpectedly" - else: - raise ex - - return impl - - if test is not None: - return decorator(test) - else: - return decorator - - -def fqn1(obj_): - return obj_.__module__ + "." + obj_.__qualname__ - - -def fqn2(module, item_identifier): - return module.__name__ + "." + item_identifier - - -T = typing.TypeVar("T") - - -def _can_weakref(spec_set): - for cls in spec_set.mro()[:-1]: - if "__weakref__" in getattr(cls, "__slots__", ()): - return True - return False - - -def mock_model(spec_set: typing.Type[T] = object, hash_code_provider=None, **kwargs) -> T: - # Enables type hinting for my own reference, and quick attribute setting. - obj = mock.MagicMock(spec_set) - for name, value in kwargs.items(): - setattr(obj, name, value) - - obj.__eq__ = lambda self, other: other is self - obj.__ne__ = lambda self, other: other is not self - obj.__hash__ = hash_code_provider or spec_set.__hash__ - - special_attrs = ["__int__"] - for attr in special_attrs: - if hasattr(spec_set, attr): - setattr(obj, attr, lambda *args, **kws: getattr(spec_set, attr)(*args, **kws)) - return obj - - -def unslot_class(klass): - return type(klass.__name__ + "Unslotted", (klass,), {}) - - -def mock_patch(what, *args, **kwargs): - # If something refers to a strong reference, e.g. aiofiles.open is just a reference to aiofile.threadpool.open, - # you will need to pass a string to patch it... - if isinstance(what, str): - fqn = what - else: - fqn = fqn1(what) - - return mock.patch(fqn, *args, **kwargs) - - -class StrongWeakValuedDict(typing.MutableMapping): - def __init__(self): - self.strong = {} - self.weak = weakref.WeakValueDictionary() - - def __setitem__(self, k, v) -> None: - self.strong[k] = v - self.weak[k] = v - - def __delitem__(self, k) -> None: - del self.strong[k] - - def __getitem__(self, k): - return self.strong[k] - - def __len__(self) -> int: - assert len(self.strong) == len(self.weak) - return len(self.strong) - - def __iter__(self) -> typing.Iterator: - return iter(self.strong) - - -def _maybe_mock_type_name(value): - # noinspection PyProtectedMember - return ( - value._spec_class.__name__ - if any(mro.__name__ == "MagicMock" for mro in type(value).mro()) - else type(value).__name__ - ) - - -def todo_implement(fn=...): - def decorator(fn): - return pytest.mark.xfail(reason="Code for test case not yet implemented.")(fn) - - return fn is ... and decorator or decorator(fn) - - -class AssertWarns: - def __init__(self, *, pattern=r".*", category=Warning): - self.pattern = pattern - self.category = category - - def __enter__(self): - self.old_warning = warnings.warn_explicit - self.mocked_warning = mock.MagicMock(warnings.warn) - self.context = mock.patch("warnings.warn", new=self.mocked_warning) - self.context.__enter__() - return self - - def __exit__(self, exc_type, exc_val, exc_tb): - self.context.__exit__(exc_type, exc_val, exc_tb) - - calls = [] - for call_args, call_kwargs in self.mocked_warning.call_args_list: - message, category = call_args[:2] - calls.append((message, category)) - if re.search(self.pattern, message, re.I) and issubclass(category, self.category): - self.matched = (message, category) - return - - assert False, ( - f"No warning with message pattern /{self.pattern}/ig and category subclassing {self.category} " - f"was found. There were {len(calls)} other warnings invoked in this time:\n" - + "\n".join(f"Category: {c}, Message: {m}" for m, c in calls) - ) - - def matched_message_contains(self, pattern): - assert re.search(pattern, self.matched[0], re.I), f"/{pattern}/ig does not match message {self.matched[0]!r}" - - -def run_in_own_thread(func): - assert not asyncio.iscoroutinefunction(func), "Cannot run coroutine in thread directly" - - @functools.wraps(func) - def delegator(*args, **kwargs): - q = queue.SimpleQueue() - - class Raiser: - def __init__(self, ex): - self.ex = ex - - def raise_again(self): - raise self.ex - - def consumer(): - try: - q.put(func(*args, **kwargs)) - except BaseException as ex: - q.put(Raiser(ex)) - - t = threading.Thread(target=consumer, daemon=True) - t.start() - t.join() - result = q.get() - if isinstance(result, Raiser): - result.raise_again() - - return delegator - - -class AwaitableMock: - def __init__(self, return_value=None): - self.await_count = 0 - self.return_value = return_value - - def _is_exception(self, obj): - return isinstance(obj, BaseException) or isinstance(obj, type) and issubclass(obj, BaseException) - - def __await__(self): - if False: - yield - self.await_count += 1 - if self._is_exception(self.return_value): - raise self.return_value - return self.return_value - - def assert_awaited_once(self): - assert self.await_count == 1 - - def assert_not_awaited(self): - assert self.await_count == 0 - - is_resolved = False - - -class AsyncWithContextMock: - def __init__(self, return_value=None): - self.return_value = return_value - - async def __aenter__(self): - return self.return_value - - async def __aexit__(self, exc_type, exc_val, exc_tb): - pass - - -def retry(max_retries): - def decorator(func): - assert asyncio.iscoroutinefunction(func), "retry only supports coroutine functions currently" - - @functools.wraps(func) - async def retry_wrapper(*args, **kwargs): - ex = None - for i in range(max_retries + 1): - if i: - print("retry", i, "of", max_retries) - try: - await func(*args, **kwargs) - return - except AssertionError as exc: - ex = exc # local variable 'ex' referenced before assignment: wtf? - raise AssertionError(f"all {max_retries} retries failed") from ex - - return retry_wrapper - - return decorator - - -class AsyncContextManagerMock: - def __init__(self, callback=lambda: None): - self.awaited_aenter = False - self.awaited_aexit = False - self.called = False - self.call_args = [] - self.call_kwargs = {} - self.aexit_exc = None - self.callback = callback - - async def __aenter__(self): - self.awaited_aenter = time.perf_counter() - return self.callback() - - async def __aexit__(self, exc_type, exc_val, exc_tb): - self.aexit_exc = exc_val - self.awaited_aexit = time.perf_counter() - - def __call__(self, *args, **kwargs): - self.called = time.perf_counter() - self.call_args = args - self.call_kwargs = kwargs - return self - - -def timeout_after(time_period): - def decorator(func): - @functools.wraps(func) - async def wrapper(*args, **kwargs): - thrown_timeout_error = None - - try: - async with async_timeout.timeout(time_period): - try: - await func(*args, **kwargs) - except asyncio.TimeoutError as ex: - thrown_timeout_error = ex - except asyncio.TimeoutError as ex: - raise AssertionError(f"Test took too long (> {time_period}s) and thus failed.") from ex - - if thrown_timeout_error is not None: - raise thrown_timeout_error - - return wrapper - - return decorator - - -def stupid_windows_please_stop_breaking_my_tests(test): - return pytest.mark.skipif(os.name == "nt", reason="This test will not pass on Windows :(")(test) - - -def has_sem_open_impl(): - try: - import multiprocessing - - multiprocessing.RLock() - except ImportError: - return False - else: - return True - - -def skip_if_no_sem_open(test): - return pytest.mark.skipif(not has_sem_open_impl(), reason="Your platform lacks a sem_open implementation")(test) - - -def patch_marshal_attr(target_entity, field_name, *args, deserializer=None, serializer=None, **kwargs): - if not (deserializer or serializer): - raise TypeError("patch_marshal_attr() Missing required keyword-only argument: 'deserializer' or 'serializer'") - if deserializer and serializer: - raise TypeError( - "patch_marshal_attr() Expected one of either keyword-arguments 'deserializer' or 'serializer', not both." - ) - - target_type = "deserializer" if deserializer else "serializer" - # noinspection PyProtectedMember - for attr in marshaller.HIKARI_ENTITY_MARSHALLER._registered_entities[target_entity].attribs: - if attr.field_name == field_name and (serializer or deserializer) == getattr(attr, target_type): - target = attr - break - elif attr.field_name == field_name: - raise TypeError( - f"{target_type.capitalize()} mismatch found on `{target_entity.__name__}" - f".{attr.field_name}`; expected `{deserializer or serializer}` but got `{getattr(attr, target_type)}`." - ) - else: - raise LookupError(f"Failed to find a `{field_name}` field on `{target_entity.__name__}`.") - return mock.patch.object(target, target_type, *args, **kwargs) - - -def min_python_version(*mmm): - return pytest.mark.skipif(f"__import__('sys').version_info < {mmm!r}", reason="Unsupported for your Python version") - - -def max_python_version(*mmm): - return pytest.mark.skipif(f"__import__('sys').version_info > {mmm!r}", reason="Unsupported for your Python version") - - -def set_private_attr(owner, name, value): - setattr(owner, f"_{type(owner).__name__}__{name}", value) - - -def get_private_attr(owner, name, **kwargs): - return getattr(owner, f"_{type(owner).__name__}__{name}", **kwargs) diff --git a/tests/hikari/hikari_test_helpers.py b/tests/hikari/hikari_test_helpers.py new file mode 100644 index 0000000000..f3f6606ae0 --- /dev/null +++ b/tests/hikari/hikari_test_helpers.py @@ -0,0 +1,237 @@ +# -*- coding: utf-8 -*- +# Copyright © Nekoka.tt 2019-2020 +# +# This file is part of Hikari. +# +# Hikari is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Hikari is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Hikari. If not, see . + +import asyncio +import contextlib +import copy +import functools +import inspect +import os +import re +import warnings + +import async_timeout +import mock +import pytest + + +# Value that is considered a reasonable time to wait for something asyncio-based +# to occur in the background. This is long enough that a shit computer will +# generally still be able to do some stuff with asyncio even if being tanked, +# but at the same time not so long that the tests take forever to run. I am +# aware waiting for anything in unit tests is evil, but there isn't really a +# good way to advance the state of an asyncio coroutine without manually +# iterating it, which I consider to be far more evil and will vary in results +# if unrelated changes are made in the same function. +REASONABLE_SLEEP_TIME = 0.5 + +# How long to reasonably expect something to take if it is considered instant. +REASONABLE_QUICK_RESPONSE_TIME = 0.05 + +# How long to wait for before considering a test to be jammed in an unbreakable +# condition, and thus acceptable to terminate the test and fail it. +REASONABLE_TIMEOUT_AFTER = 10 + + +def mock_methods_on(obj, except_=(), also_mock=()): + # Mock any methods we don't care about. also_mock is a collection of attribute names that we can eval to access + # and mock specific application with a coroutine mock to mock other external application quickly :) + magics = ["__enter__", "__exit__", "__aenter__", "__aexit__", "__iter__", "__aiter__"] + + except_ = set(except_) + also_mock = set(also_mock) + checked = set() + + def predicate(name, member): + is_callable = callable(member) + has_name = bool(name) + name_is_allowed = name not in except_ + + if not name_is_allowed: + checked.add(name) + + is_not_disallowed_magic = not name.startswith("__") or name in magics + # print(name, is_callable, has_name, name_is_allowed, is_not_disallowed_magic) + return is_callable and has_name and name_is_allowed and is_not_disallowed_magic + + copy_ = copy.copy(obj) + for name, method in inspect.getmembers(obj): + if predicate(name, method): + # print('Mocking', name, 'on', type(obj)) + + if asyncio.iscoroutinefunction(method): + _mock = mock.AsyncMock() + else: + _mock = mock.MagicMock() + + copy_.__dict__[name] = _mock + + for expr in also_mock: + owner, _, attr = ("copy_." + expr).rpartition(".") + # sue me. + owner = eval(owner) + setattr(owner, attr, mock.MagicMock()) + + assert not (except_ - checked), f"Some attributes didn't exist, so were not mocked: {except_ - checked}" + + return copy_ + + +def fqn1(obj_): + return obj_.__module__ + "." + obj_.__qualname__ + + +def fqn2(module, item_identifier): + return module.__name__ + "." + item_identifier + + +def unslot_class(klass): + return type(klass.__name__ + "Unslotted", (klass,), {}) + + +def mock_patch(what, *args, **kwargs): + # If something refers to a strong reference, e.g. aiofiles.open is just a reference to aiofile.threadpool.open, + # you will need to pass a string to patch it... + if isinstance(what, str): + fqn = what + else: + fqn = fqn1(what) + + return mock.patch(fqn, *args, **kwargs) + + +def todo_implement(fn=...): + def decorator(fn): + return pytest.mark.xfail(reason="Code for test case not yet implemented.")(fn) + + return fn is ... and decorator or decorator(fn) + + +class AssertWarns: + def __init__(self, *, pattern=r".*", category=Warning): + self.pattern = pattern + self.category = category + + def __enter__(self): + self.old_warning = warnings.warn_explicit + self.mocked_warning = mock.MagicMock(warnings.warn) + self.context = mock.patch("warnings.warn", new=self.mocked_warning) + self.context.__enter__() + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + self.context.__exit__(exc_type, exc_val, exc_tb) + + calls = [] + for call_args, call_kwargs in self.mocked_warning.call_args_list: + message, category = call_args[:2] + calls.append((message, category)) + if re.search(self.pattern, message, re.I) and issubclass(category, self.category): + self.matched = (message, category) + return + + assert False, ( + f"No warning with message pattern /{self.pattern}/ig and category subclassing {self.category} " + f"was found. There were {len(calls)} other warnings invoked in this time:\n" + + "\n".join(f"Category: {c}, Message: {m}" for m, c in calls) + ) + + def matched_message_contains(self, pattern): + assert re.search(pattern, self.matched[0], re.I), f"/{pattern}/ig does not match message {self.matched[0]!r}" + + +def retry(max_retries): + def decorator(func): + assert asyncio.iscoroutinefunction(func), "retry only supports coroutine functions currently" + + @functools.wraps(func) + async def retry_wrapper(*args, **kwargs): + ex = None + for i in range(max_retries + 1): + if i: + print("retry", i, "of", max_retries) + try: + await func(*args, **kwargs) + return + except AssertionError as exc: + ex = exc # local variable 'ex' referenced before assignment: wtf? + raise AssertionError(f"all {max_retries} retries failed") from ex + + return retry_wrapper + + return decorator + + +def timeout(time_period=REASONABLE_TIMEOUT_AFTER): + def decorator(func): + @functools.wraps(func) + async def wrapper(*args, **kwargs): + thrown_timeout_error = None + + try: + async with async_timeout.timeout(time_period): + try: + await func(*args, **kwargs) + except asyncio.TimeoutError as ex: + thrown_timeout_error = ex + except asyncio.TimeoutError as ex: + raise AssertionError(f"Test took too long (> {time_period}s) and thus failed.") from ex + + if thrown_timeout_error is not None: + raise thrown_timeout_error + + return wrapper + + return decorator + + +def stupid_windows_please_stop_breaking_my_tests(test): + return pytest.mark.skipif(os.name == "nt", reason="This test will not pass on Windows :(")(test) + + +def has_sem_open_impl(): + try: + import multiprocessing + + multiprocessing.RLock() + except ImportError: + return False + else: + return True + + +def skip_if_no_sem_open(test): + return pytest.mark.skipif(not has_sem_open_impl(), reason="Your platform lacks a sem_open implementation")(test) + + +def set_private_attr(owner, name, value): + setattr(owner, f"_{type(owner).__name__}__{name}", value) + + +def get_private_attr(owner, name, **kwargs): + return getattr(owner, f"_{type(owner).__name__}__{name}", **kwargs) + + +async def idle(): + await asyncio.sleep(REASONABLE_SLEEP_TIME) + + +@contextlib.contextmanager +def ensure_occurs_quickly(): + with async_timeout.timeout(REASONABLE_QUICK_RESPONSE_TIME): + yield diff --git a/tests/hikari/net/test_buckets.py b/tests/hikari/net/test_buckets.py index 224bbc862f..ea886ad4af 100644 --- a/tests/hikari/net/test_buckets.py +++ b/tests/hikari/net/test_buckets.py @@ -24,7 +24,7 @@ from hikari.net import buckets from hikari.net import routes -from tests.hikari import _helpers +from tests.hikari import hikari_test_helpers class TestRESTBucket: @@ -114,32 +114,32 @@ async def test_gc_polls_until_closed_event_set(self): mgr.start(0.01) assert mgr.gc_task is not None assert not mgr.gc_task.done() - await asyncio.sleep(0.1) + await hikari_test_helpers.idle() assert mgr.gc_task is not None assert not mgr.gc_task.done() - await asyncio.sleep(0.1) + await hikari_test_helpers.idle() mgr.closed_event.set() assert mgr.gc_task is not None assert not mgr.gc_task.done() task = mgr.gc_task - await asyncio.sleep(0.1) + await hikari_test_helpers.idle() assert mgr.gc_task is None assert task.done() @pytest.mark.asyncio async def test_gc_calls_do_pass(self): - with _helpers.unslot_class(buckets.RESTBucketManager)() as mgr: + with hikari_test_helpers.unslot_class(buckets.RESTBucketManager)() as mgr: mgr.do_gc_pass = mock.MagicMock() mgr.start(0.01, 33) try: - await asyncio.sleep(0.1) + await hikari_test_helpers.idle() mgr.do_gc_pass.assert_called_with(33) finally: mgr.gc_task.cancel() @pytest.mark.asyncio async def test_do_gc_pass_any_buckets_that_are_empty_but_still_rate_limited_are_kept_alive(self): - with _helpers.unslot_class(buckets.RESTBucketManager)() as mgr: + with hikari_test_helpers.unslot_class(buckets.RESTBucketManager)() as mgr: bucket = mock.MagicMock() bucket.is_empty = True bucket.is_unknown = False @@ -154,7 +154,7 @@ async def test_do_gc_pass_any_buckets_that_are_empty_but_still_rate_limited_are_ @pytest.mark.asyncio async def test_do_gc_pass_any_buckets_that_are_empty_but_not_rate_limited_and_not_expired_are_kept_alive(self): - with _helpers.unslot_class(buckets.RESTBucketManager)() as mgr: + with hikari_test_helpers.unslot_class(buckets.RESTBucketManager)() as mgr: bucket = mock.MagicMock() bucket.is_empty = True bucket.is_unknown = False @@ -169,7 +169,7 @@ async def test_do_gc_pass_any_buckets_that_are_empty_but_not_rate_limited_and_no @pytest.mark.asyncio async def test_do_gc_pass_any_buckets_that_are_empty_but_not_rate_limited_and_expired_are_closed(self): - with _helpers.unslot_class(buckets.RESTBucketManager)() as mgr: + with hikari_test_helpers.unslot_class(buckets.RESTBucketManager)() as mgr: bucket = mock.MagicMock() bucket.is_empty = True bucket.is_unknown = False @@ -184,7 +184,7 @@ async def test_do_gc_pass_any_buckets_that_are_empty_but_not_rate_limited_and_ex @pytest.mark.asyncio async def test_do_gc_pass_any_buckets_that_are_not_empty_are_kept_alive(self): - with _helpers.unslot_class(buckets.RESTBucketManager)() as mgr: + with hikari_test_helpers.unslot_class(buckets.RESTBucketManager)() as mgr: bucket = mock.MagicMock() bucket.is_empty = False bucket.is_unknown = True diff --git a/tests/hikari/net/test_gateway.py b/tests/hikari/net/test_gateway.py index 02f147b17b..7697588067 100644 --- a/tests/hikari/net/test_gateway.py +++ b/tests/hikari/net/test_gateway.py @@ -16,16 +16,22 @@ # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . import asyncio +import contextlib +import math +import async_timeout import mock import pytest +from hikari import errors from hikari.net import gateway +from hikari.net import http_client +from tests.hikari import hikari_test_helpers @pytest.fixture() def client(): - return gateway.Gateway(url="wss://gateway.discord.gg", token="lol", app=mock.MagicMock(), config=mock.MagicMock(),) + return gateway.Gateway(url="wss://gateway.discord.gg", token="lol", app=mock.MagicMock(), config=mock.MagicMock()) class TestInit: @@ -85,6 +91,7 @@ def test_not_is_alive(self, client): @pytest.mark.asyncio class TestStart: @pytest.mark.parametrize("shard_id", [0, 1, 2]) + @hikari_test_helpers.timeout() async def test_starts_task(self, event_loop, shard_id): g = gateway.Gateway( url="wss://gateway.discord.gg", @@ -95,8 +102,7 @@ async def test_starts_task(self, event_loop, shard_id): shard_count=100, ) - g._handshake_event = mock.MagicMock() - g._handshake_event.wait = mock.AsyncMock() + g._handshake_event = mock.MagicMock(asyncio.Event) g._run = mock.MagicMock() future = event_loop.create_future() @@ -107,6 +113,7 @@ async def test_starts_task(self, event_loop, shard_id): assert result is future create_task.assert_called_once_with(g._run(), name=f"shard {shard_id} keep-alive") + @hikari_test_helpers.timeout() async def test_waits_for_ready(self, client): client._handshake_event = mock.MagicMock() client._handshake_event.wait = mock.AsyncMock() @@ -114,3 +121,233 @@ async def test_waits_for_ready(self, client): await client.start() client._handshake_event.wait.assert_awaited_once_with() + + +@pytest.mark.asyncio +class TestClose: + async def test_when_already_closed_does_nothing(self, client): + client._request_close_event = mock.MagicMock(asyncio.Event) + client._request_close_event.is_set = mock.MagicMock(return_value=True) + + await client.close() + + client._request_close_event.set.assert_not_called() + + @pytest.mark.parametrize("is_alive", [True, False]) + async def test_close_sets_request_close_event(self, client, is_alive): + client.__dict__["is_alive"] = is_alive + client._request_close_event = mock.MagicMock(asyncio.Event) + client._request_close_event.is_set = mock.MagicMock(return_value=False) + + await client.close() + + client._request_close_event.set.assert_called_once_with() + + @pytest.mark.parametrize("is_alive", [True, False]) + async def test_websocket_closed_if_not_None(self, client, is_alive): + client.__dict__["is_alive"] = is_alive + client._request_close_event = mock.MagicMock(asyncio.Event) + client._request_close_event.is_set = mock.MagicMock(return_value=False) + client._close_ws = mock.AsyncMock() + client._ws = mock.MagicMock() + + await client.close() + + client._close_ws.assert_awaited_once_with(client._GatewayCloseCode.RFC_6455_NORMAL_CLOSURE, "client shut down") + + @pytest.mark.parametrize("is_alive", [True, False]) + async def test_websocket_not_closed_if_None(self, client, is_alive): + client.__dict__["is_alive"] = is_alive + client._request_close_event = mock.MagicMock(asyncio.Event) + client._request_close_event.is_set = mock.MagicMock(return_value=False) + client._close_ws = mock.AsyncMock() + client._ws = None + + await client.close() + + client._close_ws.assert_not_called() + + +@pytest.mark.asyncio +class TestRun: + @hikari_test_helpers.timeout() + async def test_repeatedly_invokes_run_once_while_request_close_event_not_set(self, client): + i = 0 + + def is_set(): + nonlocal i + + if i >= 5: + return True + else: + i += 1 + return False + + client._request_close_event = mock.MagicMock(asyncio.Event) + client._request_close_event.is_set = is_set + client._run_once = mock.AsyncMock() + + with pytest.raises(errors.GatewayClientClosedError): + await client._run() + + assert i == 5 + assert client._run_once.call_count == i + + @hikari_test_helpers.timeout() + async def test_sets_handshake_event_on_finish(self, client): + client._request_close_event = mock.MagicMock(asyncio.Event) + client._handshake_event = mock.MagicMock(asyncio.Event) + client._request_close_event.is_set = mock.MagicMock(return_value=True) + client._run_once = mock.AsyncMock() + + with pytest.raises(errors.GatewayClientClosedError): + await client._run() + + client._handshake_event.set.assert_called_once_with() + + @hikari_test_helpers.timeout() + async def test_closes_super_on_finish(self, client): + client._request_close_event = mock.MagicMock(asyncio.Event) + client._handshake_event = mock.MagicMock(asyncio.Event) + client._request_close_event.is_set = mock.MagicMock(return_value=True) + client._run_once = mock.AsyncMock() + + with mock.patch.object(http_client.HTTPClient, "close") as close_mock: + with pytest.raises(errors.GatewayClientClosedError): + await client._run() + + close_mock.assert_awaited_once_with(client) + + +@pytest.mark.asyncio +class TestRunOnce: + @pytest.fixture + def client(self): + client = hikari_test_helpers.unslot_class(gateway.Gateway)( + url="wss://gateway.discord.gg", + token="lol", + app=mock.MagicMock(), + config=mock.MagicMock(), + shard_id=3, + shard_count=17, + ) + client = hikari_test_helpers.mock_methods_on( + client, + except_=("_run_once", "_InvalidSession", "_Reconnect", "_SocketClosed", "_dispatch", "_now"), + also_mock=["_backoff", "_handshake_event", "_request_close_event", "logger",], + ) + client._dispatch = mock.AsyncMock() + # Disable backoff checking by making the condition a negative tautology. + client._RESTART_RATELIMIT_WINDOW = -1 + # First call is used for backoff checks, the second call is used + # for updating the _last_run_started_at attribute. + # 0.5, 1.0, 1.5, 2.0, 2.5, 3.0, ..., ..., ... + client._now = mock.MagicMock(side_effect=map(lambda n: n / 2, range(1, 100))) + return client + + @hikari_test_helpers.timeout() + async def test_resets_close_event(self, client): + with contextlib.suppress(Exception): + await client._run_once() + + client._request_close_event.clear.assert_called_with() + + @hikari_test_helpers.timeout() + async def test_backoff_and_waits_if_restarted_too_quickly(self, client): + client._now = mock.MagicMock(return_value=60) + client._RESTART_RATELIMIT_WINDOW = 30 + client._last_run_started_at = 40 + client._backoff.__next__ = mock.MagicMock(return_value=24.37) + + stack = contextlib.ExitStack() + wait_for = stack.enter_context(mock.patch.object(asyncio, "wait_for")) + create_task = stack.enter_context(mock.patch.object(asyncio, "create_task")) + + with stack: + await client._run_once() + + client._backoff.__next__.assert_called_once_with() + create_task.assert_called_once_with(client._request_close_event.wait(), name="gateway shard 3 backing off") + wait_for.assert_called_once_with(create_task(), timeout=24.37) + + @hikari_test_helpers.timeout() + async def test_closing_bot_during_backoff_immediately_interrupts_it(self, client): + client._now = mock.MagicMock(return_value=60) + client._RESTART_RATELIMIT_WINDOW = 30 + client._last_run_started_at = 40 + client._backoff.__next__ = mock.MagicMock(return_value=24.37) + client._request_close_event = asyncio.Event() + + task = asyncio.create_task(client._run_once()) + + try: + # Let the backoff spin up and start waiting in the background. + await hikari_test_helpers.idle() + + # Should be pretty much immediate. + with hikari_test_helpers.ensure_occurs_quickly(): + assert task.done() is False + client._request_close_event.set() + await task + + # The false instructs the caller to not restart again, but to just + # drop everything and stop execution. + assert task.result() is False + + finally: + task.cancel() + + @hikari_test_helpers.timeout() + async def test_backoff_does_not_trigger_if_not_restarting_in_small_window(self, client): + client._now = mock.MagicMock(return_value=60) + client._last_run_started_at = 40 + client._backoff.__next__ = mock.MagicMock( + side_effect=AssertionError( + "backoff was incremented, but this is not expected to occur in this test case scenario!" + ) + ) + + stack = contextlib.ExitStack() + stack.enter_context(mock.patch.object(asyncio, "wait_for")) + stack.enter_context(mock.patch.object(asyncio, "create_task")) + + with stack: + # This will raise an assertion error if the backoff is incremented. + await client._run_once() + + @hikari_test_helpers.timeout() + async def test_last_run_started_at_set_to_current_time(self, client): + # Windows does some batshit crazy stuff in perf_counter, like only + # returning process time elapsed rather than monotonic time since + # startup, so I guess I will put this random value here to show the + # code doesn't really care what this value is contextually. + client._last_run_started_at = -100_000 + + await client._run_once() + + assert client._last_run_started_at == 1.0 + + @hikari_test_helpers.timeout() + async def test_ws_gets_created(self, client): + await client._run_once() + client._create_ws.assert_awaited_once_with(client.url) + + @hikari_test_helpers.timeout() + async def test_connected_at_is_set_before_handshake_and_is_cancelled_after(self, client): + assert math.isnan(client.connected_at) + + initial = -2.718281828459045 + client.connected_at = initial + + def ensure_connected_at_set(): + assert client.connected_at != initial + + client._handshake = mock.AsyncMock(wraps=ensure_connected_at_set) + + await client._run_once() + + assert math.isnan(client.connected_at) + + @hikari_test_helpers.timeout() + async def test_zlib_decompressobj_set(self, client): + pass diff --git a/tests/hikari/net/test_http_client.py b/tests/hikari/net/test_http_client.py index 8809dea722..0f022694a6 100644 --- a/tests/hikari/net/test_http_client.py +++ b/tests/hikari/net/test_http_client.py @@ -16,7 +16,7 @@ # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . import json -import types +import weakref import aiohttp import mock @@ -24,7 +24,7 @@ from hikari.net import http_client from hikari.net import http_settings -from tests.hikari import _helpers +from tests.hikari import hikari_test_helpers @pytest.fixture @@ -37,10 +37,26 @@ def client_session(): @pytest.fixture def client(client_session): assert client_session, "this param is needed, it ensures aiohttp is patched for the test" - client = _helpers.unslot_class(http_client.HTTPClient)(mock.MagicMock()) + client = hikari_test_helpers.unslot_class(http_client.HTTPClient)(mock.MagicMock()) yield client +class TestFinalizer: + def test_when_existing_client_session(self, client): + client._client_session = mock.MagicMock() + client._client_session_ref = weakref.proxy(client._client_session) + client.__del__() + assert client._client_session is None + assert client._client_session_ref is None + + def test_when_no_client_session(self, client): + client._client_session = None + client._client_session_ref = None + client.__del__() + assert client._client_session is None + assert client._client_session_ref is None + + @pytest.mark.asyncio class TestAcquireClientSession: async def test_acquire_creates_new_session_if_one_does_not_exist(self, client): @@ -50,7 +66,10 @@ async def test_acquire_creates_new_session_if_one_does_not_exist(self, client): client._client_session = None cs = client.get_client_session() - assert client._client_session is cs + + assert client._client_session == cs + assert cs in weakref.getweakrefs(client._client_session), "did not return correct weakref" + aiohttp.ClientSession.assert_called_once_with( connector=client._config.tcp_connector_factory(), trust_env=client._config.trust_env, diff --git a/tests/hikari/net/test_ratelimits.py b/tests/hikari/net/test_ratelimits.py index 0207418cfc..2b21eac08f 100644 --- a/tests/hikari/net/test_ratelimits.py +++ b/tests/hikari/net/test_ratelimits.py @@ -26,7 +26,7 @@ import pytest from hikari.net import rate_limits -from tests.hikari import _helpers +from tests.hikari import hikari_test_helpers class TestBaseRateLimiter: @@ -123,7 +123,7 @@ async def test_lock_cancels_existing_task(self): @pytest.mark.asyncio async def test_lock_schedules_throttle(self): - with _helpers.unslot_class(rate_limits.ManualRateLimiter)() as limiter: + with hikari_test_helpers.unslot_class(rate_limits.ManualRateLimiter)() as limiter: limiter.unlock_later = mock.AsyncMock() limiter.throttle(0) await limiter.throttle_task @@ -153,7 +153,7 @@ def pop(self, _=-1): popped_at.append(time.perf_counter()) return event_loop.create_future() - with _helpers.unslot_class(rate_limits.ManualRateLimiter)() as limiter: + with hikari_test_helpers.unslot_class(rate_limits.ManualRateLimiter)() as limiter: with mock.patch("asyncio.sleep", wraps=mock_sleep): limiter.queue = MockList() @@ -175,7 +175,7 @@ async def test_throttle_clears_throttle_task(self, event_loop): class TestWindowedBurstRateLimiter: @pytest.fixture def ratelimiter(self): - inst = _helpers.unslot_class(rate_limits.WindowedBurstRateLimiter)(__name__, 3, 3) + inst = hikari_test_helpers.unslot_class(rate_limits.WindowedBurstRateLimiter)(__name__, 3, 3) yield inst with contextlib.suppress(Exception): inst.close() @@ -222,7 +222,7 @@ async def test_task_scheduled_if_rate_limited_and_throttle_task_is_None(self, ra try: assert ratelimiter.throttle_task is not None - await asyncio.sleep(0.01) + await hikari_test_helpers.idle() ratelimiter.throttle.assert_called() finally: @@ -272,7 +272,7 @@ async def test_throttle_consumes_queue(self, event_loop): assert future.done(), f"future {i} was incomplete!" @pytest.mark.asyncio - @_helpers.retry(5) + @hikari_test_helpers.retry(5) async def test_throttle_when_limited_sleeps_then_bursts_repeatedly(self, event_loop): limit = 5 period = 3 @@ -337,12 +337,12 @@ async def test_throttle_resets_throttle_task(self, event_loop): assert rl.throttle_task is None def test_get_time_until_reset_if_not_rate_limited(self): - with _helpers.unslot_class(rate_limits.WindowedBurstRateLimiter)(__name__, 0.01, 1) as rl: + with hikari_test_helpers.unslot_class(rate_limits.WindowedBurstRateLimiter)(__name__, 0.01, 1) as rl: rl.is_rate_limited = mock.MagicMock(return_value=False) assert rl.get_time_until_reset(420) == 0.0 def test_get_time_until_reset_if_rate_limited(self): - with _helpers.unslot_class(rate_limits.WindowedBurstRateLimiter)(__name__, 0.01, 1) as rl: + with hikari_test_helpers.unslot_class(rate_limits.WindowedBurstRateLimiter)(__name__, 0.01, 1) as rl: rl.is_rate_limited = mock.MagicMock(return_value=True) rl.reset_at = 420.4 assert rl.get_time_until_reset(69.8) == 420.4 - 69.8 diff --git a/tests/hikari/utilities/test_aio.py b/tests/hikari/utilities/test_aio.py index c359b8e469..7dc34174b1 100644 --- a/tests/hikari/utilities/test_aio.py +++ b/tests/hikari/utilities/test_aio.py @@ -20,6 +20,7 @@ import pytest from hikari.utilities import aio +from tests.hikari import hikari_test_helpers class CoroutineStub: @@ -33,7 +34,7 @@ def __eq__(self, other): def __await__(self): self.awaited = True - yield from asyncio.sleep(0.01).__await__() + return hikari_test_helpers.idle().__await__() def __repr__(self): args = ", ".join(map(repr, self.args)) diff --git a/tests/hikari/utilities/test_undefined.py b/tests/hikari/utilities/test_undefined.py index 37e06408c1..510a220eb8 100644 --- a/tests/hikari/utilities/test_undefined.py +++ b/tests/hikari/utilities/test_undefined.py @@ -15,9 +15,10 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . +import pytest from hikari.utilities import undefined -from tests.hikari import _helpers +from tests.hikari import hikari_test_helpers class TestUndefined: @@ -40,6 +41,6 @@ def test_singleton_behaviour(self): def test_count(self): assert undefined.count(9, 18, undefined.UNDEFINED, 36, undefined.UNDEFINED, 54) == 2 - @_helpers.assert_raises(type_=TypeError) def test_cannot_reinstatiate(self): - type(undefined.UNDEFINED)() + with pytest.raises(TypeError): + type(undefined.UNDEFINED)() From 384bcc73a57b29145fe99897a8e173e6a7afb867 Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Sun, 21 Jun 2020 21:43:07 +0100 Subject: [PATCH 558/922] More gateway test cases. --- tests/hikari/net/test_gateway.py | 26 ++++++++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/tests/hikari/net/test_gateway.py b/tests/hikari/net/test_gateway.py index 7697588067..0d8d2903de 100644 --- a/tests/hikari/net/test_gateway.py +++ b/tests/hikari/net/test_gateway.py @@ -19,7 +19,6 @@ import contextlib import math -import async_timeout import mock import pytest @@ -350,4 +349,27 @@ def ensure_connected_at_set(): @hikari_test_helpers.timeout() async def test_zlib_decompressobj_set(self, client): - pass + assert client._zlib is None + await client._run_once() + assert client._zlib is not None + + @hikari_test_helpers.timeout() + async def test_handshake_event_cleared(self, client): + client._handshake_event = asyncio.Event() + client._handshake_event.set() + await client._run_once() + assert not client._handshake_event.is_set() + + @hikari_test_helpers.timeout() + async def test_handshake_invoked(self, client): + await client._run_once() + client._handshake.assert_awaited_once_with() + + @hikari_test_helpers.timeout() + async def test_poll_events_invoked(self, client): + await client._run_once() + client._poll_events.assert_awaited_once_with() + + @hikari_test_helpers.timeout() + async def test_happy_path_returns_False(self, client): + assert await client._run_once() is False From 1bfac648b61bb8051a4a85f7bc5e03e7caf53da2 Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Sun, 21 Jun 2020 21:57:59 +0100 Subject: [PATCH 559/922] Removed instance-specific loggers in several places. --- hikari/api/rest.py | 12 ------ hikari/impl/bot.py | 8 ---- hikari/impl/event_manager_core.py | 15 ++++--- hikari/impl/gateway_zookeeper.py | 26 +++++++----- hikari/impl/rest.py | 4 -- hikari/net/buckets.py | 22 +++++----- hikari/net/gateway.py | 57 +++++++++++++------------- hikari/net/http_client.py | 18 ++++---- hikari/net/rate_limits.py | 18 +++----- hikari/net/rest.py | 20 +++++---- hikari/utilities/reflect.py | 17 ++++---- tests/hikari/net/test_gateway.py | 2 +- tests/hikari/net/test_http_client.py | 2 +- tests/hikari/utilities/test_reflect.py | 8 ++-- 14 files changed, 104 insertions(+), 125 deletions(-) diff --git a/hikari/api/rest.py b/hikari/api/rest.py index df915f0aa5..39f326e5cb 100644 --- a/hikari/api/rest.py +++ b/hikari/api/rest.py @@ -22,7 +22,6 @@ __all__: typing.Final[typing.List[str]] = ["IRESTApp"] import abc -import logging import typing @@ -58,17 +57,6 @@ def rest(self) -> rest_.REST: The REST API client. """ - @property - @abc.abstractmethod - def logger(self) -> logging.Logger: - """Logger for logging messages. - - Returns - ------- - logging.Logger - The application-level logger. - """ - @property @abc.abstractmethod def cache(self) -> cache_.ICacheComponent: diff --git a/hikari/impl/bot.py b/hikari/impl/bot.py index 1f7ea56aa4..caad108a4c 100644 --- a/hikari/impl/bot.py +++ b/hikari/impl/bot.py @@ -22,7 +22,6 @@ __all__: typing.Final[typing.List[str]] = ["BotAppImpl"] import asyncio -import logging import typing from hikari.api import bot @@ -33,7 +32,6 @@ from hikari.models import presences from hikari.net import http_settings as http_settings_ from hikari.net import rest -from hikari.utilities import reflect from hikari.utilities import undefined if typing.TYPE_CHECKING: @@ -157,8 +155,6 @@ def __init__( thread_pool_executor: typing.Optional[concurrent.futures.Executor] = None, token: str, ) -> None: - self._logger = reflect.get_logger(self) - config = http_settings_.HTTPSettings() if config is undefined.UNDEFINED else config self._cache = cache_impl.InMemoryCacheComponentImpl(app=self) @@ -196,10 +192,6 @@ def __init__( def event_dispatcher(self) -> event_dispatcher_.IEventDispatcherComponent: return self._event_manager - @property - def logger(self) -> logging.Logger: - return self._logger - @property def cache(self) -> cache_.ICacheComponent: return self._cache diff --git a/hikari/impl/event_manager_core.py b/hikari/impl/event_manager_core.py index 2cc2ed2f98..33bfd75f5d 100644 --- a/hikari/impl/event_manager_core.py +++ b/hikari/impl/event_manager_core.py @@ -23,6 +23,7 @@ import asyncio import functools +import logging import typing from hikari.api import event_consumer @@ -39,6 +40,9 @@ from hikari.api import rest +_LOGGER: typing.Final[logging.Logger] = logging.getLogger(__name__) + + if typing.TYPE_CHECKING: EventT = typing.TypeVar("EventT", bound=base.Event, contravariant=True) PredicateT = typing.Callable[[EventT], typing.Union[bool, typing.Coroutine[None, typing.Any, bool]]] @@ -61,7 +65,6 @@ def __init__(self, app: rest.IRESTApp) -> None: self._app = app self._listeners: ListenerMapT = {} self._waiters: WaiterMapT = {} - self.logger = reflect.get_logger(self) @property @typing.final @@ -75,7 +78,7 @@ async def consume_raw_event( callback = getattr(self, "on_" + event_name.lower()) await callback(shard, payload) except AttributeError: - self.logger.debug("ignoring unknown event %s", event_name) + _LOGGER.debug("ignoring unknown event %s", event_name) def subscribe( self, @@ -95,7 +98,7 @@ async def wrapper(event: EventT) -> None: return wrapper else: - self.logger.debug( + _LOGGER.debug( "subscribing callback 'async def %s%s' to event-type %s.%s", getattr(callback, "__name__", ""), reflect.resolve_signature(callback), @@ -114,7 +117,7 @@ def unsubscribe( callback: typing.Callable[[EventT], typing.Coroutine[None, typing.Any, None]], ) -> None: if event_type in self._listeners: - self.logger.debug( + _LOGGER.debug( "unsubscribing callback %s%s from event-type %s.%s", getattr(callback, "__name__", ""), reflect.resolve_signature(callback), @@ -221,7 +224,7 @@ async def _invoke_callback(self, callback: AsyncCallbackT, event: EventT) -> Non trio = type(ex), ex, ex.__traceback__.tb_next if ex.__traceback__ is not None else None if base.is_no_catch_event(event): - self.logger.error("an exception occurred handling an event, but it has been ignored", exc_info=trio) + _LOGGER.error("an exception occurred handling an event, but it has been ignored", exc_info=trio) else: - self.logger.error("an exception occurred handling an event", exc_info=trio) + _LOGGER.error("an exception occurred handling an event", exc_info=trio) await self.dispatch(other.ExceptionEvent(exception=ex, event=event, callback=callback)) diff --git a/hikari/impl/gateway_zookeeper.py b/hikari/impl/gateway_zookeeper.py index 0260741164..5b7c0a8fd3 100644 --- a/hikari/impl/gateway_zookeeper.py +++ b/hikari/impl/gateway_zookeeper.py @@ -25,6 +25,7 @@ import asyncio import contextlib import datetime +import logging import signal import time import typing @@ -44,6 +45,9 @@ from hikari.models import presences +_LOGGER: typing.Final[logging.Logger] = logging.getLogger(__name__) + + class AbstractGatewayZookeeper(gateway_zookeeper.IGatewayZookeeperApp, abc.ABC): """Provides keep-alive logic for orchestrating multiple shards. @@ -195,7 +199,7 @@ def on_interrupt() -> None: self._map_signal_handlers(loop.add_signal_handler, on_interrupt) loop.run_until_complete(self._run()) except KeyboardInterrupt as ex: - self.logger.info("received signal to shut down client") + _LOGGER.info("received signal to shut down client") if self._debug: raise else: @@ -204,7 +208,7 @@ def on_interrupt() -> None: raise ex.with_traceback(None) # noqa: R100 raise in except handler without fromflake8 finally: self._map_signal_handlers(loop.remove_signal_handler) - self.logger.info("client has shut down") + _LOGGER.info("client has shut down") async def start(self) -> None: self._tasks.clear() @@ -217,9 +221,9 @@ async def start(self) -> None: await self._maybe_dispatch(other.StartingEvent()) if self._shard_count > 1: - self.logger.info("starting %s shard(s)", len(self._shards)) + _LOGGER.info("starting %s shard(s)", len(self._shards)) else: - self.logger.info("this application will be single-sharded") + _LOGGER.info("this application will be single-sharded") start_time = time.perf_counter() @@ -229,7 +233,7 @@ async def start(self) -> None: break if i > 0: - self.logger.info("waiting for 5 seconds until next shard can start") + _LOGGER.info("waiting for 5 seconds until next shard can start") completed, _ = await asyncio.wait( self._tasks.values(), timeout=5, return_when=asyncio.FIRST_COMPLETED @@ -253,7 +257,7 @@ async def start(self) -> None: finally: if len(self._tasks) != len(self._shards): - self.logger.warning( + _LOGGER.warning( "application aborted midway through initialization, will begin shutting down %s shard(s)", len(self._tasks), ) @@ -264,7 +268,7 @@ async def start(self) -> None: finish_time = time.perf_counter() self._gather_task = asyncio.create_task(self._gather(), name=f"zookeeper for {len(self._shards)} shard(s)") - self.logger.info("started %s shard(s) in approx %.2fs", len(self._shards), finish_time - start_time) + _LOGGER.info("started %s shard(s) in approx %.2fs", len(self._shards), finish_time - start_time) await self._maybe_dispatch(other.StartedEvent()) @@ -277,7 +281,7 @@ async def close(self) -> None: # This way if we cancel the stopping task, we still shut down properly. self._request_close_event.set() - self.logger.info("stopping %s shard(s)", len(self._tasks)) + _LOGGER.info("stopping %s shard(s)", len(self._tasks)) try: await self._maybe_dispatch(other.StoppingEvent()) @@ -316,7 +320,7 @@ async def fetch_sharding_settings(self) -> gateway_models.GatewayBot: async def _init(self) -> None: gw_recs = await self.fetch_sharding_settings() - self.logger.info( + _LOGGER.info( "you have opened %s session(s) recently, you can open %s more before %s", gw_recs.session_start_limit.total - gw_recs.session_start_limit.remaining, gw_recs.session_start_limit.remaining if gw_recs.session_start_limit.remaining > 0 else "no", @@ -328,7 +332,7 @@ async def _init(self) -> None: self._max_concurrency = gw_recs.session_start_limit.max_concurrency url = gw_recs.url - self.logger.info( + _LOGGER.info( "will connect shards to %s at a rate of %s shard(s) per 5 seconds (contact Discord to increase this rate)", url, self._max_concurrency, @@ -383,7 +387,7 @@ async def _gather(self) -> None: try: await asyncio.gather(*self._tasks.values()) finally: - self.logger.debug("gather failed, shutting down shard(s)") + _LOGGER.debug("gather failed, shutting down shard(s)") await self.close() async def _run(self) -> None: diff --git a/hikari/impl/rest.py b/hikari/impl/rest.py index bcce26676c..4cea200b65 100644 --- a/hikari/impl/rest.py +++ b/hikari/impl/rest.py @@ -90,10 +90,6 @@ def __init__( self._cache = cache_impl.InMemoryCacheComponentImpl(self) self._entity_factory = entity_factory_impl.EntityFactoryComponentImpl(self) - @property - def logger(self) -> logging.Logger: - return self._logger - @property def executor(self) -> typing.Optional[futures.Executor]: return None diff --git a/hikari/net/buckets.py b/hikari/net/buckets.py index 0d06f9c2ee..b3b4663e70 100644 --- a/hikari/net/buckets.py +++ b/hikari/net/buckets.py @@ -220,6 +220,9 @@ """The hash used for an unknown bucket that has not yet been resolved.""" +_LOGGER: typing.Final[logging.Logger] = logging.getLogger(__name__) + + class RESTBucket(rate_limits.WindowedBurstRateLimiter): """Represents a rate limit for an REST endpoint. @@ -321,7 +324,6 @@ class RESTBucketManager: "real_hashes_to_buckets", "closed_event", "gc_task", - "logger", ) routes_to_hashes: typing.Final[typing.MutableMapping[routes.Route, str]] @@ -339,15 +341,11 @@ class RESTBucketManager: gc_task: typing.Optional[asyncio.Task[None]] """The internal garbage collector task.""" - logger: typing.Final[logging.Logger] - """The logger to use for this object.""" - def __init__(self) -> None: self.routes_to_hashes = {} self.real_hashes_to_buckets = {} self.closed_event: asyncio.Event = asyncio.Event() self.gc_task: typing.Optional[asyncio.Task[None]] = None - self.logger = logging.getLogger("hikari.rest.buckets.RESTBucketManager") def __enter__(self) -> RESTBucketManager: return self @@ -420,12 +418,12 @@ async def gc(self, poll_period: float, expire_after: float) -> None: # noqa: D4 """ # Prevent filling memory increasingly until we run out by removing dead buckets every 20s # Allocations are somewhat cheap if we only do them every so-many seconds, after all. - self.logger.debug("rate limit garbage collector started") + _LOGGER.debug("rate limit garbage collector started") while not self.closed_event.is_set(): try: await asyncio.wait_for(self.closed_event.wait(), timeout=poll_period) except asyncio.TimeoutError: - self.logger.debug("performing rate limit garbage collection pass") + _LOGGER.debug("performing rate limit garbage collection pass") self.do_gc_pass(expire_after) self.gc_task = None @@ -482,7 +480,7 @@ def do_gc_pass(self, expire_after: float) -> None: self.real_hashes_to_buckets[full_hash].close() del self.real_hashes_to_buckets[full_hash] - self.logger.debug("purged %s stale buckets, %s remain in survival, %s active", dead, survival, active) + _LOGGER.debug("purged %s stale buckets, %s remain in survival, %s active", dead, survival, active) def acquire(self, compiled_route: routes.CompiledRoute) -> asyncio.Future[None]: """Acquire a bucket for the given _route. @@ -518,9 +516,9 @@ def acquire(self, compiled_route: routes.CompiledRoute) -> asyncio.Future[None]: try: bucket = self.real_hashes_to_buckets[real_bucket_hash] - self.logger.debug("%s is being mapped to existing bucket %s", compiled_route, real_bucket_hash) + _LOGGER.debug("%s is being mapped to existing bucket %s", compiled_route, real_bucket_hash) except KeyError: - self.logger.debug("%s is being mapped to new bucket %s", compiled_route, real_bucket_hash) + _LOGGER.debug("%s is being mapped to new bucket %s", compiled_route, real_bucket_hash) bucket = RESTBucket(real_bucket_hash, compiled_route) self.real_hashes_to_buckets[real_bucket_hash] = bucket @@ -561,7 +559,7 @@ def update_rate_limits( if real_bucket_hash in self.real_hashes_to_buckets: bucket = self.real_hashes_to_buckets[real_bucket_hash] - self.logger.debug( + _LOGGER.debug( "updating %s with bucket %s [reset-after:%ss, limit:%s, remaining:%s]", compiled_route, real_bucket_hash, @@ -572,7 +570,7 @@ def update_rate_limits( else: bucket = RESTBucket(real_bucket_hash, compiled_route) self.real_hashes_to_buckets[real_bucket_hash] = bucket - self.logger.debug( + _LOGGER.debug( "remapping %s with bucket %s [reset-after:%ss, limit:%s, remaining:%s]", compiled_route, real_bucket_hash, diff --git a/hikari/net/gateway.py b/hikari/net/gateway.py index 50cff2656f..fa5981427a 100644 --- a/hikari/net/gateway.py +++ b/hikari/net/gateway.py @@ -191,7 +191,7 @@ def __init__( use_etf: bool = False, version: int = 6, ) -> None: - super().__init__(config=config, debug=debug, logger=reflect.get_logger(self, str(shard_id))) + super().__init__(config=config, debug=debug) self._activity: typing.Union[undefined.UndefinedType, None, presences.Activity] = initial_activity self._app = app self._backoff = rate_limits.ExponentialBackOff(base=1.85, maximum=600, initial_increment=2) @@ -200,6 +200,7 @@ def __init__( self._intents: typing.Optional[intents_.Intent] = intents self._is_afk: typing.Union[undefined.UndefinedType, bool] = initial_is_afk self._last_run_started_at = float("nan") + self._logger = reflect.get_logger(self, str(shard_id)) self._request_close_event = asyncio.Event() self._seq: typing.Optional[str] = None self._shard_id: int = shard_id @@ -263,13 +264,13 @@ async def close(self) -> None: """Close the websocket.""" if not self._request_close_event.is_set(): if self.is_alive: - self.logger.info("received request to shut down shard") + self._logger.info("received request to shut down shard") else: - self.logger.debug("shard marked as closed when it was not running") + self._logger.debug("shard marked as closed when it was not running") self._request_close_event.set() if self._ws is not None: - self.logger.warning("gateway client closed, will not attempt to restart") + self._logger.warning("gateway client closed, will not attempt to restart") await self._close_ws(self._GatewayCloseCode.RFC_6455_NORMAL_CLOSURE, "client shut down") async def _run(self) -> None: @@ -301,7 +302,7 @@ async def _run_once(self) -> bool: ) try: backoff = next(self._backoff) - self.logger.debug("backing off for %ss", backoff) + self._logger.debug("backing off for %ss", backoff) await asyncio.wait_for(wait_task, timeout=backoff) # If this line gets reached, the wait didn't time out, meaning @@ -315,7 +316,7 @@ async def _run_once(self) -> bool: self._last_run_started_at = self._now() try: - self.logger.debug("creating websocket connection to %s", self.url) + self._logger.debug("creating websocket connection to %s", self.url) self._ws = await self._create_ws(self.url) self.connected_at = self._now() @@ -344,22 +345,22 @@ async def _run_once(self) -> bool: return False except aiohttp.ClientConnectorError as ex: - self.logger.error( + self._logger.error( "failed to connect to Discord because %s.%s: %s", type(ex).__module__, type(ex).__qualname__, str(ex), ) except self._InvalidSession as ex: if ex.can_resume: - self.logger.warning("invalid session, so will attempt to resume session %s", self.session_id) + self._logger.warning("invalid session, so will attempt to resume session %s", self.session_id) await self._close_ws(self._GatewayCloseCode.DO_NOT_INVALIDATE_SESSION, "invalid session (resume)") else: - self.logger.warning("invalid session, so will attempt to reconnect with new session") + self._logger.warning("invalid session, so will attempt to reconnect with new session") await self._close_ws(self._GatewayCloseCode.RFC_6455_NORMAL_CLOSURE, "invalid session (no resume)") self._seq = None self.session_id = None except self._Reconnect: - self.logger.warning("instructed by Discord to reconnect and resume session %s", self.session_id) + self._logger.warning("instructed by Discord to reconnect and resume session %s", self.session_id) self._backoff.reset() await self._close_ws(self._GatewayCloseCode.DO_NOT_INVALIDATE_SESSION, "reconnecting") @@ -367,7 +368,7 @@ async def _run_once(self) -> bool: # The socket has already closed, so no need to close it again. if not self._zombied and not self._request_close_event.is_set(): # This will occur due to a network issue such as a network adapter going down. - self.logger.warning("unexpected socket closure, will attempt to reconnect") + self._logger.warning("unexpected socket closure, will attempt to reconnect") else: self._backoff.reset() @@ -375,7 +376,7 @@ async def _run_once(self) -> bool: except errors.GatewayServerClosedConnectionError as ex: if ex.can_reconnect: - self.logger.warning( + self._logger.warning( "server closed the connection with %s (%s), will attempt to reconnect", ex.code, ex.reason, ) await self._close_ws(self._GatewayCloseCode.RFC_6455_NORMAL_CLOSURE, "you hung up on me") @@ -384,7 +385,7 @@ async def _run_once(self) -> bool: raise except Exception as ex: - self.logger.error("unexpected exception occurred, shard will now die", exc_info=ex) + self._logger.error("unexpected exception occurred, shard will now die", exc_info=ex) await self._close_ws(self._GatewayCloseCode.RFC_6455_UNEXPECTED_CONDITION, "unexpected error occurred") raise @@ -466,7 +467,7 @@ async def update_voice_state( await self._send_json(payload) async def _close_ws(self, code: int, message: str) -> None: - self.logger.debug("sending close frame with code %s and message %r", int(code), message) + self._logger.debug("sending close frame with code %s and message %r", int(code), message) # None if the websocket error'ed on initialization. if self._ws is not None: await self._ws.close(code=code, message=bytes(message, "utf-8")) @@ -481,7 +482,7 @@ async def _handshake(self) -> None: self.heartbeat_interval = message["d"]["heartbeat_interval"] / 1_000.0 - self.logger.info("received HELLO, heartbeat interval is %s", self.heartbeat_interval) + self._logger.info("received HELLO, heartbeat interval is %s", self.heartbeat_interval) if self.session_id is not None: # RESUME! @@ -527,7 +528,7 @@ async def _pulse(self) -> None: time_since_heartbeat_sent = now - self.last_heartbeat_sent if self.heartbeat_interval < time_since_message: - self.logger.error( + self._logger.error( "connection is a zombie, haven't received any message for %ss, last heartbeat sent %ss ago", time_since_message, time_since_heartbeat_sent, @@ -536,7 +537,7 @@ async def _pulse(self) -> None: await self._close_ws(self._GatewayCloseCode.DO_NOT_INVALIDATE_SESSION, "zombie connection") return - self.logger.debug( + self._logger.debug( "preparing to send HEARTBEAT [s:%s, interval:%ss]", self._seq, self.heartbeat_interval ) await self._send_json({"op": self._GatewayOpcode.HEARTBEAT, "d": self._seq}) @@ -563,32 +564,32 @@ async def _poll_events(self) -> None: self._seq = message["s"] if event == "READY": self.session_id = data["session_id"] - self.logger.info("connection is ready [session:%s]", self.session_id) + self._logger.info("connection is ready [session:%s]", self.session_id) self._handshake_event.set() elif event == "RESUME": - self.logger.info("connection has resumed [session:%s, seq:%s]", self.session_id, self._seq) + self._logger.info("connection has resumed [session:%s, seq:%s]", self.session_id, self._seq) self._handshake_event.set() asyncio.create_task(self._dispatch(event, data), name=f"shard {self._shard_id} {event}") elif op == self._GatewayOpcode.HEARTBEAT: - self.logger.debug("received HEARTBEAT; sending HEARTBEAT ACK") + self._logger.debug("received HEARTBEAT; sending HEARTBEAT ACK") await self._send_json({"op": self._GatewayOpcode.HEARTBEAT_ACK}) elif op == self._GatewayOpcode.HEARTBEAT_ACK: self.heartbeat_latency = self._now() - self.last_heartbeat_sent - self.logger.debug("received HEARTBEAT ACK [latency:%ss]", self.heartbeat_latency) + self._logger.debug("received HEARTBEAT ACK [latency:%ss]", self.heartbeat_latency) elif op == self._GatewayOpcode.RECONNECT: - self.logger.debug("RECONNECT") + self._logger.debug("RECONNECT") raise self._Reconnect elif op == self._GatewayOpcode.INVALID_SESSION: - self.logger.debug("INVALID SESSION [resume:%s]", data) + self._logger.debug("INVALID SESSION [resume:%s]", data) raise self._InvalidSession(data) else: - self.logger.debug("ignoring unrecognised opcode %s", op) + self._logger.debug("ignoring unrecognised opcode %s", op) async def _receive_json_payload(self) -> data_binding.JSONObject: message = await self._receive_raw() @@ -609,7 +610,7 @@ async def _receive_json_payload(self) -> data_binding.JSONObject: if message.type == aiohttp.WSMsgType.CLOSE: close_code = self._ws.close_code - self.logger.debug("connection closed with code %s", close_code) + self._logger.debug("connection closed with code %s", close_code) if close_code in self._GatewayCloseCode.__members__.values(): reason = self._GatewayCloseCode(close_code).name @@ -632,7 +633,7 @@ async def _receive_json_payload(self) -> data_binding.JSONObject: # Assume exception for now. ex = self._ws.exception() - self.logger.debug("encountered unexpected error", exc_info=ex) + self._logger.debug("encountered unexpected error", exc_info=ex) raise errors.GatewayError("Unexpected websocket exception from gateway") from ex async def _receive_zlib_message(self, first_packet: bytes) -> typing.Tuple[int, str]: @@ -673,7 +674,7 @@ def _now() -> float: def _log_debug_payload(self, payload: str, message: str, *args: typing.Any) -> None: # Prevent logging these payloads if logging isn't enabled. This aids performance a little. - if not self.logger.isEnabledFor(logging.DEBUG): + if not self._logger.isEnabledFor(logging.DEBUG): return message = f"{message} [seq:%s, session:%s, size:%s]" @@ -683,7 +684,7 @@ def _log_debug_payload(self, payload: str, message: str, *args: typing.Any) -> N else: args = (*args, self._seq, self.session_id, len(payload)) - self.logger.debug(message, *args) + self._logger.debug(message, *args) def _build_presence_payload( self, diff --git a/hikari/net/http_client.py b/hikari/net/http_client.py index e87909796a..09ed7f0dac 100644 --- a/hikari/net/http_client.py +++ b/hikari/net/http_client.py @@ -48,6 +48,9 @@ """ except NameError: RequestContextManager = typing.Any # type: ignore + + +_LOGGER: typing.Final[logging.Logger] = logging.getLogger(__name__) class HTTPClient(abc.ABC): @@ -69,11 +72,10 @@ class HTTPClient(abc.ABC): debug : bool Defaults to `False`. If `True`, then a lot of contextual information regarding low-level HTTP communication will be logged to the debug - logger on this class. + _logger on this class. """ __slots__ = ( - "logger", "_client_session", "_client_session_ref", "_config", @@ -81,9 +83,6 @@ class HTTPClient(abc.ABC): "_tracers", ) - logger: logging.Logger - """The logger to use for this object.""" - _config: http_settings.HTTPSettings """HTTP settings in-use.""" @@ -92,13 +91,10 @@ class HTTPClient(abc.ABC): def __init__( self, - logger: logging.Logger, *, config: typing.Optional[http_settings.HTTPSettings] = None, debug: bool = False, ) -> None: - self.logger = logger - if config is None: config = http_settings.HTTPSettings() @@ -126,7 +122,7 @@ async def close(self) -> None: """Close the client safely.""" if self._client_session is not None: await self._client_session.close() - self.logger.debug("closed client session object %r", self._client_session) + _LOGGER.debug("closed client session object %r", self._client_session) self._client_session = None @typing.final @@ -156,7 +152,7 @@ def get_client_session(self) -> aiohttp.ClientSession: json_serialize=json.dumps, ) self._client_session_ref = weakref.proxy(self._client_session) - self.logger.debug("acquired new client session object %r", self._client_session) + _LOGGER.debug("acquired new client session object %r", self._client_session) # Only return a weakref, to prevent callees obtaining ownership. return typing.cast(aiohttp.ClientSession, self._client_session_ref) @@ -244,7 +240,7 @@ async def _create_ws( aiohttp.ClientWebsocketResponse The websocket to use. """ - self.logger.debug("creating underlying websocket object from HTTP session") + _LOGGER.debug("creating underlying websocket object from HTTP session") return await self.get_client_session().ws_connect( url=url, compress=compress, diff --git a/hikari/net/rate_limits.py b/hikari/net/rate_limits.py index 74849c17fa..c74c9e94cb 100644 --- a/hikari/net/rate_limits.py +++ b/hikari/net/rate_limits.py @@ -37,13 +37,11 @@ import time import typing - if typing.TYPE_CHECKING: import types -UNKNOWN_HASH: typing.Final[str] = "UNKNOWN" -"""The hash used for an unknown bucket that has not yet been resolved.""" +_LOGGER: typing.Final[logging.Logger] = logging.getLogger(__name__) class BaseRateLimiter(abc.ABC): @@ -86,7 +84,7 @@ class BurstRateLimiter(BaseRateLimiter, abc.ABC): complete logic for safely aborting any pending tasks when being shut down. """ - __slots__ = ("name", "throttle_task", "queue", "logger", "_closed") + __slots__ = ("name", "throttle_task", "queue", "_closed") name: typing.Final[str] """The name of the rate limiter.""" @@ -97,14 +95,10 @@ class BurstRateLimiter(BaseRateLimiter, abc.ABC): queue: typing.Final[typing.List[asyncio.Future[typing.Any]]] """The queue of any futures under a rate limit.""" - logger: typing.Final[logging.Logger] - """The logger used by this rate limiter.""" - def __init__(self, name: str) -> None: self.name = name self.throttle_task = None self.queue = [] - self.logger = logging.getLogger(f"hikari.utilities.ratelimits.{type(self).__qualname__}.{name}") self._closed = False @abc.abstractmethod @@ -140,9 +134,9 @@ def close(self) -> None: future.cancel() if failed_tasks: - self.logger.debug("%s rate limiter closed with %s pending tasks!", self.name, failed_tasks) + _LOGGER.debug("%s rate limiter closed with %s pending tasks!", self.name, failed_tasks) else: - self.logger.debug("%s rate limiter closed", self.name) + _LOGGER.debug("%s rate limiter closed", self.name) self._closed = True @property @@ -244,7 +238,7 @@ async def unlock_later(self, retry_after: float) -> None: `None`. This means you can check if throttling is occurring by checking if `throttle_task` is not `None`. """ - self.logger.warning("you are being globally rate limited for %ss", retry_after) + _LOGGER.warning("you are being globally rate limited for %ss", retry_after) await asyncio.sleep(retry_after) while self.queue: next_future = self.queue.pop(0) @@ -404,7 +398,7 @@ async def throttle(self) -> None: `throttle_task` to `None`. This means you can check if throttling is occurring by checking if `throttle_task` is not `None`. """ - self.logger.debug( + _LOGGER.debug( "you are being rate limited on bucket %s, backing off for %ss", self.name, self.get_time_until_reset(time.perf_counter()), diff --git a/hikari/net/rest.py b/hikari/net/rest.py index f673f3234e..3c99ada201 100644 --- a/hikari/net/rest.py +++ b/hikari/net/rest.py @@ -25,6 +25,7 @@ import contextlib import datetime import http +import logging import math import typing import uuid @@ -65,8 +66,13 @@ from hikari.models import users from hikari.models import voices from hikari.models import webhooks + + +_LOGGER: typing.Final[logging.Logger] = logging.getLogger(__name__) +# TODO: make a mechanism to allow me to share the same client session but +# use various tokens for REST-only apps. class REST(http_client.HTTPClient, component.IComponent): """Implementation of the V6 and V7-compatible Discord REST API. @@ -129,7 +135,7 @@ def __init__( rest_url: typing.Union[undefined.UndefinedType, str] = undefined.UNDEFINED, version: int, ) -> None: - super().__init__(config=config, debug=debug, logger=reflect.get_logger(self)) + super().__init__(config=config, debug=debug) self.buckets = buckets.RESTBucketManager() self.global_rate_limit = rate_limits.ManualRateLimiter() self.version = version @@ -219,11 +225,11 @@ async def _request_once( if self._debug: headers_str = "\n".join(f"\t\t{name}:{value}" for name, value in headers.items()) - self.logger.debug( + _LOGGER.debug( "%s %s %s\n\theaders:\n%s\n\tbody:\n\t\t%r", uuid4, compiled_route.method, url, headers_str, body ) else: - self.logger.debug("%s %s %s", uuid4, compiled_route.method, url) + _LOGGER.debug("%s %s %s", uuid4, compiled_route.method, url) # Make the request. # noinspection PyUnresolvedReferences @@ -235,7 +241,7 @@ async def _request_once( headers_str = "\n".join( f"\t\t{name.decode('utf-8')}:{value.decode('utf-8')}" for name, value in response.raw_headers ) - self.logger.debug( + _LOGGER.debug( "%s %s %s\n\theaders:\n%s\n\tbody:\n\t\t%r", uuid4, response.status, @@ -244,7 +250,7 @@ async def _request_once( await response.read(), ) else: - self.logger.debug("%s %s %s", uuid4, response.status, response.reason) + _LOGGER.debug("%s %s %s", uuid4, response.status, response.reason) # Ensure we aren't rate limited, and update rate limiting headers where appropriate. await self._parse_ratelimits(compiled_route, response) @@ -317,7 +323,7 @@ async def _parse_ratelimits(self, compiled_route: routes.CompiledRoute, response if body.get("global", False) is True: self.global_rate_limit.throttle(body_retry_after) - self.logger.warning("you are being rate-limited globally - trying again after %ss", body_retry_after) + _LOGGER.warning("you are being rate-limited globally - trying again after %ss", body_retry_after) raise self._RetryRequest # Discord have started applying ratelimits to operations on some endpoints @@ -340,7 +346,7 @@ async def _parse_ratelimits(self, compiled_route: routes.CompiledRoute, response # trust that voodoo type stuff won't ever occur with that value from them... if remaining <= 0: # We can retry and we will then abide by the updated bucket ratelimits. - self.logger.debug( + _LOGGER.debug( "rate-limited on bucket %s at %s. This is a bucket discrepancy, so we will retry at %s", bucket, compiled_route, diff --git a/hikari/utilities/reflect.py b/hikari/utilities/reflect.py index 597843fa81..507ad194bd 100644 --- a/hikari/utilities/reflect.py +++ b/hikari/utilities/reflect.py @@ -68,24 +68,25 @@ def resolve_signature(func: typing.Callable[..., typing.Any]) -> inspect.Signatu def get_logger(obj: typing.Union[typing.Type[typing.Any], typing.Any], *additional_args: str) -> logging.Logger: - """Get an appropriately named logger for the given class or object. + """Get an appropriately named _LOGGER for the given class or object. Parameters ---------- obj : typing.Type or object - A type or instance of a type to make a logger in the name of. + A type or instance of a type to make a _LOGGER in the name of. *additional_args : str - Additional tokens to append onto the logger name, separated by `.`. + Additional tokens to append onto the _LOGGER name, separated by `.`. This is useful in some places to append info such as shard ID to each - logger to enable shard-specific logging, for example. + _LOGGER to enable shard-specific logging, for example. Returns ------- logging.Logger - The logger to use. + The _LOGGER to use. """ if isinstance(obj, str): - return logging.getLogger(obj) + str_obj = obj + else: + str_obj = (obj if isinstance(obj, type) else type(obj)).__module__ - obj = obj if isinstance(obj, type) else type(obj) - return logging.getLogger(".".join((obj.__module__, obj.__qualname__, *additional_args))) + return logging.getLogger(".".join((str_obj, *additional_args))) diff --git a/tests/hikari/net/test_gateway.py b/tests/hikari/net/test_gateway.py index 0d8d2903de..602a279518 100644 --- a/tests/hikari/net/test_gateway.py +++ b/tests/hikari/net/test_gateway.py @@ -233,7 +233,7 @@ def client(self): client = hikari_test_helpers.mock_methods_on( client, except_=("_run_once", "_InvalidSession", "_Reconnect", "_SocketClosed", "_dispatch", "_now"), - also_mock=["_backoff", "_handshake_event", "_request_close_event", "logger",], + also_mock=["_backoff", "_handshake_event", "_request_close_event", "_logger",], ) client._dispatch = mock.AsyncMock() # Disable backoff checking by making the condition a negative tautology. diff --git a/tests/hikari/net/test_http_client.py b/tests/hikari/net/test_http_client.py index 0f022694a6..d6d8e26b43 100644 --- a/tests/hikari/net/test_http_client.py +++ b/tests/hikari/net/test_http_client.py @@ -37,7 +37,7 @@ def client_session(): @pytest.fixture def client(client_session): assert client_session, "this param is needed, it ensures aiohttp is patched for the test" - client = hikari_test_helpers.unslot_class(http_client.HTTPClient)(mock.MagicMock()) + client = hikari_test_helpers.unslot_class(http_client.HTTPClient)() yield client diff --git a/tests/hikari/utilities/test_reflect.py b/tests/hikari/utilities/test_reflect.py index c6bdff644a..949d25108e 100644 --- a/tests/hikari/utilities/test_reflect.py +++ b/tests/hikari/utilities/test_reflect.py @@ -118,10 +118,10 @@ class Class: @pytest.mark.parametrize( ["args", "expected_name"], [ - ([Class], f"{__name__}.Class"), - ([Class()], f"{__name__}.Class"), - ([Class, "Foooo", "bar", "123"], f"{__name__}.Class.Foooo.bar.123"), - ([Class(), "qux", "QUx", "940"], f"{__name__}.Class.qux.QUx.940"), + ([Class], __name__), + ([Class()], __name__), + ([Class, "Foooo", "bar", "123"], f"{__name__}.Foooo.bar.123"), + ([Class(), "qux", "QUx", "940"], f"{__name__}.qux.QUx.940"), ], ) def test_get_logger(args, expected_name): From 95609c7ec65a94c7653d9d2094498da09aa98e19 Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Mon, 22 Jun 2020 09:56:32 +0100 Subject: [PATCH 560/922] More test cases for Gateway#_run_once --- hikari/errors.py | 10 +- hikari/net/gateway.py | 37 ++++--- hikari/net/http_client.py | 11 +-- hikari/net/rest.py | 4 +- tests/hikari/net/test_gateway.py | 160 ++++++++++++++++++++++++++++++- 5 files changed, 187 insertions(+), 35 deletions(-) diff --git a/hikari/errors.py b/hikari/errors.py index ca27599f67..000892679b 100644 --- a/hikari/errors.py +++ b/hikari/errors.py @@ -120,19 +120,13 @@ class GatewayServerClosedConnectionError(GatewayError): """ - __slots__ = ("code", "can_reconnect", "can_resume", "should_backoff") + __slots__ = ("code", "can_reconnect", "should_backoff") def __init__( - self, - reason: str, - code: typing.Optional[int] = None, - can_reconnect: bool = False, - can_resume: bool = False, - should_backoff: bool = True, + self, reason: str, code: typing.Optional[int] = None, can_reconnect: bool = False, should_backoff: bool = True, ) -> None: self.code = code self.can_reconnect = can_reconnect - self.can_resume = can_resume self.should_backoff = should_backoff super().__init__(reason) diff --git a/hikari/net/gateway.py b/hikari/net/gateway.py index fa5981427a..a76dc099ec 100644 --- a/hikari/net/gateway.py +++ b/hikari/net/gateway.py @@ -295,6 +295,8 @@ async def _run_once(self) -> bool: # returns `True` if we can reconnect, or `False` otherwise. self._request_close_event.clear() + self._zombied = False + if self._now() - self._last_run_started_at < self._RESTART_RATELIMIT_WINDOW: # Interrupt sleep immediately if a request to close is fired. wait_task = asyncio.create_task( @@ -330,12 +332,14 @@ async def _run_once(self) -> bool: # Technically we are connected after the hello, but this ensures we can send and receive # before firing that event. - asyncio.create_task(self._dispatch("CONNECTED", {}), name=f"shard {self._shard_id} CONNECTED") + self._dispatch("CONNECTED", {}) # We should ideally set this after HELLO, but it should be fine # here as well. If we don't heartbeat in time, something probably # went majorly wrong anyway. - heartbeat = asyncio.create_task(self._pulse(), name=f"shard {self._shard_id} heartbeat") + heartbeat = asyncio.create_task( + self._heartbeat_keepalive(), name=f"gateway shard {self._shard_id} heartbeat" + ) try: await self._poll_events() @@ -366,12 +370,12 @@ async def _run_once(self) -> bool: except self._SocketClosed: # The socket has already closed, so no need to close it again. - if not self._zombied and not self._request_close_event.is_set(): - # This will occur due to a network issue such as a network adapter going down. - self._logger.warning("unexpected socket closure, will attempt to reconnect") - else: + if self._zombied: self._backoff.reset() + if not self._request_close_event.is_set(): + self._logger.warning("unexpected socket closure, will attempt to reconnect") + return not self._request_close_event.is_set() except errors.GatewayServerClosedConnectionError as ex: @@ -381,7 +385,9 @@ async def _run_once(self) -> bool: ) await self._close_ws(self._GatewayCloseCode.RFC_6455_NORMAL_CLOSURE, "you hung up on me") else: - await self._close_ws(self._GatewayCloseCode.RFC_6455_UNEXPECTED_CONDITION, "you failed the connection") + await self._close_ws(self._GatewayCloseCode.RFC_6455_UNEXPECTED_CONDITION, "you broke the connection") + self._seq = None + self.session_id = None raise except Exception as ex: @@ -392,7 +398,7 @@ async def _run_once(self) -> bool: finally: if not math.isnan(self.connected_at): # Only dispatch this if we actually connected before we failed! - asyncio.create_task(self._dispatch("DISCONNECTED", {}), name=f"shard {self._shard_id} DISCONNECTED") + self._dispatch("DISCONNECTED", {}) self.connected_at = float("nan") @@ -520,7 +526,7 @@ async def _handshake(self) -> None: await self._send_json(payload) - async def _pulse(self) -> None: + async def _heartbeat_keepalive(self) -> None: try: while not self._request_close_event.is_set(): now = self._now() @@ -570,7 +576,7 @@ async def _poll_events(self) -> None: self._logger.info("connection has resumed [session:%s, seq:%s]", self.session_id, self._seq) self._handshake_event.set() - asyncio.create_task(self._dispatch(event, data), name=f"shard {self._shard_id} {event}") + self._dispatch(event, data) elif op == self._GatewayOpcode.HEARTBEAT: self._logger.debug("received HEARTBEAT; sending HEARTBEAT ACK") @@ -625,8 +631,8 @@ async def _receive_json_payload(self) -> data_binding.JSONObject: self._GatewayCloseCode.RATE_LIMITED, ) - # Always try to resume if possible first. - raise errors.GatewayServerClosedConnectionError(reason, close_code, can_reconnect, can_reconnect, True) + # Assume we can always resume first. + raise errors.GatewayServerClosedConnectionError(reason, close_code, can_reconnect, True) if message.type == aiohttp.WSMsgType.CLOSING or message.type == aiohttp.WSMsgType.CLOSED: raise self._SocketClosed @@ -665,8 +671,11 @@ async def _send_json(self, payload: data_binding.JSONObject) -> None: self._log_debug_payload(message, "sending json payload [t:%s]", payload.get("t")) await self._ws.send_str(message) - def _dispatch(self, event_name: str, event: data_binding.JSONObject) -> typing.Coroutine[None, typing.Any, None]: - return self._app.event_consumer.consume_raw_event(self, event_name, event) + def _dispatch(self, event_name: str, event: data_binding.JSONObject) -> asyncio.Task[None]: + return asyncio.create_task( + self._app.event_consumer.consume_raw_event(self, event_name, event), + name=f"gateway shard {self._shard_id} dispatch {event}", + ) @staticmethod def _now() -> float: diff --git a/hikari/net/http_client.py b/hikari/net/http_client.py index 09ed7f0dac..0ae4867637 100644 --- a/hikari/net/http_client.py +++ b/hikari/net/http_client.py @@ -48,8 +48,8 @@ """ except NameError: RequestContextManager = typing.Any # type: ignore - - + + _LOGGER: typing.Final[logging.Logger] = logging.getLogger(__name__) @@ -89,12 +89,7 @@ class HTTPClient(abc.ABC): _debug: bool """`True` if debug mode is enabled. `False` otherwise.""" - def __init__( - self, - *, - config: typing.Optional[http_settings.HTTPSettings] = None, - debug: bool = False, - ) -> None: + def __init__(self, *, config: typing.Optional[http_settings.HTTPSettings] = None, debug: bool = False,) -> None: if config is None: config = http_settings.HTTPSettings() diff --git a/hikari/net/rest.py b/hikari/net/rest.py index 3c99ada201..6220ab7127 100644 --- a/hikari/net/rest.py +++ b/hikari/net/rest.py @@ -66,8 +66,8 @@ from hikari.models import users from hikari.models import voices from hikari.models import webhooks - - + + _LOGGER: typing.Final[logging.Logger] = logging.getLogger(__name__) diff --git a/tests/hikari/net/test_gateway.py b/tests/hikari/net/test_gateway.py index 602a279518..077b536501 100644 --- a/tests/hikari/net/test_gateway.py +++ b/tests/hikari/net/test_gateway.py @@ -19,6 +19,7 @@ import contextlib import math +import aiohttp.client_reqrep import mock import pytest @@ -232,7 +233,16 @@ def client(self): ) client = hikari_test_helpers.mock_methods_on( client, - except_=("_run_once", "_InvalidSession", "_Reconnect", "_SocketClosed", "_dispatch", "_now"), + except_=( + "_run_once", + "_InvalidSession", + "_Reconnect", + "_SocketClosed", + "_dispatch", + "_now", + "_GatewayCloseCode", + "_GatewayOpcode", + ), also_mock=["_backoff", "_handshake_event", "_request_close_event", "_logger",], ) client._dispatch = mock.AsyncMock() @@ -246,11 +256,18 @@ def client(self): @hikari_test_helpers.timeout() async def test_resets_close_event(self, client): - with contextlib.suppress(Exception): - await client._run_once() + await client._run_once() client._request_close_event.clear.assert_called_with() + @hikari_test_helpers.timeout() + async def test_resets_zombie_status(self, client): + client._zombied = True + + await client._run_once() + + assert client._zombied is False + @hikari_test_helpers.timeout() async def test_backoff_and_waits_if_restarted_too_quickly(self, client): client._now = mock.MagicMock(return_value=60) @@ -365,11 +382,148 @@ async def test_handshake_invoked(self, client): await client._run_once() client._handshake.assert_awaited_once_with() + @hikari_test_helpers.timeout() + async def test_connected_event_dispatched_before_polling_events(self, client): + class Error(Exception): + pass + + client._dispatch = mock.MagicMock() + client._poll_events = mock.AsyncMock(side_effect=Error) + + with pytest.raises(Error): + await client._run_once() + + client._dispatch.assert_any_call("CONNECTED", {}) + + @hikari_test_helpers.timeout() + async def test_heartbeat_is_not_started_before_handshake_completes(self, client): + class Error(Exception): + pass + + client._heartbeat_keepalive = mock.MagicMock() + + client._handshake = mock.AsyncMock(side_effect=Error) + + with mock.patch.object(asyncio, "create_task") as create_task: + with pytest.raises(Error): + await client._run_once() + + call = mock.call(client._heartbeat_keepalive(), name=mock.ANY) + assert call not in create_task.call_args_list + + @hikari_test_helpers.timeout() + async def test_heartbeat_is_started(self, client): + client._heartbeat_keepalive = mock.MagicMock() + + with mock.patch.object(asyncio, "create_task") as create_task: + await client._run_once() + + call = mock.call(client._heartbeat_keepalive(), name="gateway shard 3 heartbeat") + assert call in create_task.call_args_list + @hikari_test_helpers.timeout() async def test_poll_events_invoked(self, client): await client._run_once() client._poll_events.assert_awaited_once_with() + @hikari_test_helpers.timeout() + async def test_heartbeat_is_stopped_when_poll_events_stops(self, client): + client._heartbeat_keepalive = mock.MagicMock() + client._poll_events = mock.AsyncMock(side_effect=Exception) + + task = mock.create_autospec(asyncio.Task) + + with mock.patch.object(asyncio, "create_task", return_value=task): + with pytest.raises(Exception): + await client._run_once() + + task.cancel.assert_called_once_with() + @hikari_test_helpers.timeout() async def test_happy_path_returns_False(self, client): assert await client._run_once() is False + + @hikari_test_helpers.timeout() + async def test_ClientConnectionError_is_restartable(self, client): + key = aiohttp.client_reqrep.ConnectionKey( + host="localhost", port=6996, is_ssl=False, ssl=None, proxy=None, proxy_auth=None, proxy_headers_hash=69420, + ) + error = aiohttp.ClientConnectorError(key, OSError()) + + client._handshake = mock.AsyncMock(side_effect=error) + assert await client._run_once() is True + + @hikari_test_helpers.timeout() + async def test_invalid_session_is_restartable(self, client): + client._poll_events = mock.AsyncMock(side_effect=gateway.Gateway._InvalidSession()) + assert await client._run_once() is True + + @hikari_test_helpers.timeout() + async def test_invalid_session_resume_does_not_invalidate_session(self, client): + client._poll_events = mock.AsyncMock(side_effect=gateway.Gateway._InvalidSession(True)) + await client._run_once() + client._close_ws.assert_awaited_once_with( + gateway.Gateway._GatewayCloseCode.DO_NOT_INVALIDATE_SESSION, "invalid session (resume)" + ) + + @hikari_test_helpers.timeout() + async def test_invalid_session_no_resume_invalidates_session(self, client): + client._poll_events = mock.AsyncMock(side_effect=gateway.Gateway._InvalidSession(False)) + await client._run_once() + client._close_ws.assert_awaited_once_with( + gateway.Gateway._GatewayCloseCode.RFC_6455_NORMAL_CLOSURE, "invalid session (no resume)" + ) + + @hikari_test_helpers.timeout() + async def test_invalid_session_resume_does_not_clear_seq_or_session_id(self, client): + client._poll_events = mock.AsyncMock(side_effect=gateway.Gateway._InvalidSession(True)) + client._seq = 1234 + client.session_id = "69420" + await client._run_once() + assert client._seq == 1234 + assert client.session_id == "69420" + + @hikari_test_helpers.timeout() + async def test_invalid_session_no_resume_clears_seq_and_session_id(self, client): + client._poll_events = mock.AsyncMock(side_effect=gateway.Gateway._InvalidSession(False)) + client._seq = 1234 + client.session_id = "69420" + await client._run_once() + assert client._seq is None + assert client.session_id is None + + @hikari_test_helpers.timeout() + async def test_reconnect_is_restartable(self, client): + client._poll_events = mock.AsyncMock(side_effect=gateway.Gateway._Reconnect()) + assert await client._run_once() is True + + @pytest.mark.parametrize("request_close", [True, False]) + @hikari_test_helpers.timeout() + async def test_socket_closed_is_restartable_if_no_closure_request(self, client, request_close): + client._request_close_event.is_set = mock.MagicMock(return_value=request_close) + client._poll_events = mock.AsyncMock(side_effect=gateway.Gateway._SocketClosed()) + assert await client._run_once() is not request_close + + @pytest.mark.parametrize( + ["zombied", "request_close", "expect_backoff_called"], + [(True, True, True), (True, False, True), (False, True, False), (False, False, False),], + ) + @hikari_test_helpers.timeout() + async def test_socket_closed_resets_backoff(self, client, zombied, request_close, expect_backoff_called): + client._request_close_event.is_set = mock.MagicMock(return_value=request_close) + + def poll_events(): + client._zombied = zombied + raise gateway.Gateway._SocketClosed() + + client._poll_events = mock.AsyncMock(wraps=poll_events) + await client._run_once() + + if expect_backoff_called: + client._backoff.reset.assert_called_once_with() + else: + client._backoff.reset.assert_not_called() + + @hikari_test_helpers.timeout() + async def test_server_connection_error_resumes_if_reconnectable(self): + ... From 1dbf2e555681d370ae9e02b50d829d69a6612edd Mon Sep 17 00:00:00 2001 From: davfsa Date: Mon, 22 Jun 2020 14:42:53 +0200 Subject: [PATCH 561/922] Re-add auto logging with a warning for extreme case --- .gitignore | 5 +- hikari/impl/bot.py | 108 +++++++++++++++++++++++++++++++++++++++- hikari/models/emojis.py | 4 +- 3 files changed, 110 insertions(+), 7 deletions(-) diff --git a/.gitignore b/.gitignore index a16738ed35..451fb6a7cd 100644 --- a/.gitignore +++ b/.gitignore @@ -42,14 +42,11 @@ pip-wheel-metadata/ .pytest/ .pytest_cache/ -# Poetry lockfile -poetry.lock - # Container logs **.container.log # Docker secrets credentials.env -# We don't use this usually, but regardless... +# Mypy Cache .mypy_cache/ diff --git a/hikari/impl/bot.py b/hikari/impl/bot.py index caad108a4c..5a7893ad84 100644 --- a/hikari/impl/bot.py +++ b/hikari/impl/bot.py @@ -22,6 +22,11 @@ __all__: typing.Final[typing.List[str]] = ["BotAppImpl"] import asyncio +import inspect +import logging +import os +import platform +import sys import typing from hikari.api import bot @@ -46,6 +51,8 @@ from hikari.models import gateway as gateway_models from hikari.models import intents as intents_ +_LOGGER: typing.Final[logging.Logger] = logging.getLogger("hikari") + class BotAppImpl(gateway_zookeeper.AbstractGatewayZookeeper, bot.IBotApp): """Implementation of an auto-sharded bot application. @@ -85,6 +92,19 @@ class BotAppImpl(gateway_zookeeper.AbstractGatewayZookeeper, bot.IBotApp): large_threshold : int The number of members that need to be in a guild for the guild to be considered large. Defaults to the maximum, which is `250`. + logging_level : str or int or None + If not `None`, then this will be the logging level set if you have not + enabled logging already. In this case, it should be a valid + `logging` level that can be passed to `logging.basicConfig`. If you have + already initialized logging, then this is irrelevant and this + parameter can be safely ignored. If you set this to `None`, then no + logging will initialize if you have a reason to not use any logging + or simply wish to initialize it in your own time instead. + + !!! note + Initializating logging means already have a handler in the root logger. + This is usually achived by calling `logging.basicConfig` or adding the + handler another way. rest_version : int The version of the REST API to connect to. At the time of writing, only version `6` and version `7` (undocumented development release) @@ -148,6 +168,7 @@ def __init__( initial_status: typing.Union[undefined.UndefinedType, presences.Status] = undefined.UNDEFINED, intents: typing.Optional[intents_.Intent] = None, large_threshold: int = 250, + logging_level: typing.Union[str, int, None] = "INFO", rest_version: int = 6, rest_url: typing.Union[undefined.UndefinedType, str] = undefined.UNDEFINED, shard_ids: typing.Union[typing.Set[int], undefined.UndefinedType] = undefined.UNDEFINED, @@ -155,13 +176,19 @@ def __init__( thread_pool_executor: typing.Optional[concurrent.futures.Executor] = None, token: str, ) -> None: + if logging_level is not None and not _LOGGER.hasHandlers(): + logging.basicConfig(format=self.__get_logging_format()) + _LOGGER.setLevel(logging_level) + + self.__print_banner() + config = http_settings_.HTTPSettings() if config is undefined.UNDEFINED else config self._cache = cache_impl.InMemoryCacheComponentImpl(app=self) self._config = config self._event_manager = event_manager.EventManagerImpl(app=self) self._entity_factory = entity_factory_impl.EntityFactoryComponentImpl(app=self) - self._rest = rest.REST( # noqa S106 possible hardcoded password + self._rest = rest.REST( # noqa: S106 - Possible hardcoded password app=self, config=config, debug=debug, @@ -249,3 +276,82 @@ async def close(self) -> None: async def fetch_sharding_settings(self) -> gateway_models.GatewayBot: return await self.rest.fetch_gateway_bot() + + def __print_banner(self) -> None: + from hikari import _about + + version = _about.__version__ + sourcefile = typing.cast(str, inspect.getsourcefile(_about)) + path = os.path.abspath(os.path.dirname(sourcefile)) + python_implementation = platform.python_implementation() + python_version = platform.python_version() + operating_system = " ".join((platform.system(), *platform.architecture())) + python_compiler = platform.python_compiler() + + copyright_str = f"{_about.__copyright__}, licensed under {_about.__license__}" + version_str = f"hikari v{version} (installed in {path})" + impl_str = f"Running on {python_implementation} v{python_version}, {python_compiler}, ({operating_system})" + doc_line = f"Documentation: {_about.__docs__}" + guild_line = f"Support: {_about.__discord_invite__}" + line_len = max(len(version_str), len(copyright_str), len(impl_str), len(guild_line), len(doc_line)) + + copyright_str = f"|* {copyright_str:^{line_len}} *|" + impl_str = f"|* {impl_str:^{line_len}} *|" + version_str = f"|* {version_str:^{line_len}} *|" + doc_line = f"|* {doc_line:^{line_len}} *|" + guild_line = f"|* {guild_line:^{line_len}} *|" + line_len = max(len(version_str), len(copyright_str), len(impl_str), len(guild_line), len(doc_line)) - 4 + + top_line = "//" + ("=" * line_len) + r"\\" + bottom_line = r"\\" + ("=" * line_len) + "//" + + # Start on a newline, this prevents logging formatting messing with the + # layout of the banner; before we used \r but this probably isn't great + # since with systems like docker-compose that prepend to each line of + # logs, we end up with a mess. + _LOGGER.info( + "\n%s\n%s\n%s\n%s\n%s\n%s\n%s", + top_line, + version_str, + copyright_str, + impl_str, + doc_line, + guild_line, + bottom_line, + ) + + @staticmethod + def __get_logging_format() -> str: + # Modified from + # https://github.com/django/django/blob/master/django/core/management/color.py + + plat = sys.platform + supports_color = False + + # isatty is not always implemented, https://code.djangoproject.com/ticket/6223 + is_a_tty = hasattr(sys.stdout, "isatty") and sys.stdout.isatty() + + if plat != "Pocket PC": + if plat == "win32": + supports_color |= os.getenv("TERM_PROGRAM", None) == "mintty" + supports_color |= "ANSICON" in os.environ + supports_color |= is_a_tty + else: + supports_color = is_a_tty + + supports_color |= bool(os.getenv("PYCHARM_HOSTED", "")) + + if supports_color: + blue = "\033[1;35m" + gray = "\033[1;37m" + green = "\033[1;32m" + red = "\033[1;31m" + yellow = "\033[1;33m" + default = "\033[0m" + else: + blue = gray = green = red = yellow = default = "" + + return ( + f"{red}%(levelname)-8.8s {yellow}%(name)-30.30s {green}#%(lineno)-4d {blue}%(asctime)23.23s" + f"{default}:: {gray}%(message)s{default}" + ) diff --git a/hikari/models/emojis.py b/hikari/models/emojis.py index 4dfc2baddb..e70d19c692 100644 --- a/hikari/models/emojis.py +++ b/hikari/models/emojis.py @@ -209,8 +209,8 @@ class CustomEmoji(snowflake.Unique, Emoji): removed. The side effect of this means that mentions for animated emojis will not be correct. - Track this issue here: - https://github.com/discord/discord-api-docs/issues/1614 + This will not be changed as stated here: + https://github.com/discord/discord-api-docs/issues/1614#issuecomment-628548913 """ app: rest.IRESTApp = attr.ib(default=None, repr=False, eq=False, hash=False, init=True) From a46ae6cc3b84a4a2d69337d74c177d74fb5ea7d7 Mon Sep 17 00:00:00 2001 From: davfsa Date: Wed, 24 Jun 2020 12:20:21 +0200 Subject: [PATCH 562/922] Implemented being able to use urls and paths in rest avatars --- hikari/net/rest.py | 42 ++++++++++++++++++++++++------------------ 1 file changed, 24 insertions(+), 18 deletions(-) diff --git a/hikari/net/rest.py b/hikari/net/rest.py index 6220ab7127..025514e44e 100644 --- a/hikari/net/rest.py +++ b/hikari/net/rest.py @@ -47,7 +47,6 @@ from hikari.utilities import data_binding from hikari.utilities import date from hikari.utilities import files -from hikari.utilities import reflect from hikari.utilities import snowflake from hikari.utilities import undefined @@ -1417,14 +1416,15 @@ async def create_webhook( channel: typing.Union[channels.TextChannel, snowflake.UniqueObject], name: str, *, - avatar: typing.Union[undefined.UndefinedType, files.Resource] = undefined.UNDEFINED, + avatar: typing.Union[undefined.UndefinedType, files.Resource, str] = undefined.UNDEFINED, reason: typing.Union[undefined.UndefinedType, str] = undefined.UNDEFINED, ) -> webhooks.Webhook: route = routes.POST_WEBHOOK.compile(channel=channel) body = data_binding.JSONObjectBuilder() body.put("name", name) if avatar is not undefined.UNDEFINED: - async with avatar.stream(executor=self._app.executor) as stream: + avatar_resouce = files.ensure_resource(avatar) + async with avatar_resouce.stream(executor=self._app.executor) as stream: body.put("avatar", await stream.data_uri()) raw_response = await self._request(route, body=body, reason=reason) @@ -1468,7 +1468,7 @@ async def edit_webhook( *, token: typing.Union[undefined.UndefinedType, str] = undefined.UNDEFINED, name: typing.Union[undefined.UndefinedType, str] = undefined.UNDEFINED, - avatar: typing.Union[None, undefined.UndefinedType, files.Resource] = undefined.UNDEFINED, + avatar: typing.Union[None, undefined.UndefinedType, files.Resource, str] = undefined.UNDEFINED, channel: typing.Union[ undefined.UndefinedType, channels.TextChannel, snowflake.UniqueObject ] = undefined.UNDEFINED, @@ -1486,7 +1486,8 @@ async def edit_webhook( if avatar is None: body.put("avatar", None) elif avatar is not undefined.UNDEFINED: - async with avatar.stream(executor=self._app.executor) as stream: + avatar_resource = files.ensure_resource(avatar) + async with avatar_resource.stream(executor=self._app.executor) as stream: body.put("avatar", await stream.data_uri()) raw_response = await self._request(route, body=body, reason=reason) @@ -1607,17 +1608,18 @@ async def edit_my_user( self, *, username: typing.Union[undefined.UndefinedType, str] = undefined.UNDEFINED, - avatar: typing.Union[undefined.UndefinedType, None, files.Resource] = undefined.UNDEFINED, + avatar: typing.Union[undefined.UndefinedType, None, files.Resource, str] = undefined.UNDEFINED, ) -> users.OwnUser: route = routes.PATCH_MY_USER.compile() body = data_binding.JSONObjectBuilder() body.put("username", username) - if isinstance(avatar, files.Resource): - async with avatar.stream(executor=self._app.executor) as stream: + if avatar is None: + body.put("avatar", None) + elif avatar is not undefined.UNDEFINED: + avatar_resouce = files.ensure_resource(avatar) + async with avatar_resouce.stream(executor=self._app.executor) as stream: body.put("avatar", await stream.data_uri()) - else: - body.put("avatar", avatar) raw_response = await self._request(route, body=body) response = typing.cast(data_binding.JSONObject, raw_response) @@ -1749,7 +1751,7 @@ async def create_emoji( self, guild: typing.Union[guilds.Guild, snowflake.UniqueObject], name: str, - image: files.Resource, + image: typing.Union[files.Resource, str], *, roles: typing.Union[ undefined.UndefinedType, typing.Collection[typing.Union[guilds.Role, snowflake.UniqueObject]] @@ -1760,7 +1762,8 @@ async def create_emoji( body = data_binding.JSONObjectBuilder() body.put("name", name) if image is not undefined.UNDEFINED: - async with image.stream(executor=self._app.executor) as stream: + image_resource = files.ensure_resource(image) + async with image_resource.stream(executor=self._app.executor) as stream: body.put("image", await stream.data_uri()) body.put_snowflake_array("roles", roles) @@ -1838,10 +1841,10 @@ async def edit_guild( undefined.UndefinedType, channels.GuildVoiceChannel, snowflake.UniqueObject ] = undefined.UNDEFINED, afk_timeout: typing.Union[undefined.UndefinedType, date.TimeSpan] = undefined.UNDEFINED, - icon: typing.Union[undefined.UndefinedType, None, files.Resource] = undefined.UNDEFINED, + icon: typing.Union[undefined.UndefinedType, None, files.Resource, str] = undefined.UNDEFINED, owner: typing.Union[undefined.UndefinedType, users.User, snowflake.UniqueObject] = undefined.UNDEFINED, - splash: typing.Union[undefined.UndefinedType, None, files.Resource] = undefined.UNDEFINED, - banner: typing.Union[undefined.UndefinedType, None, files.Resource] = undefined.UNDEFINED, + splash: typing.Union[undefined.UndefinedType, None, files.Resource, str] = undefined.UNDEFINED, + banner: typing.Union[undefined.UndefinedType, None, files.Resource, str] = undefined.UNDEFINED, system_channel: typing.Union[undefined.UndefinedType, channels.GuildTextChannel] = undefined.UNDEFINED, rules_channel: typing.Union[undefined.UndefinedType, channels.GuildTextChannel] = undefined.UNDEFINED, public_updates_channel: typing.Union[undefined.UndefinedType, channels.GuildTextChannel] = undefined.UNDEFINED, @@ -1868,19 +1871,22 @@ async def edit_guild( if icon is None: body.put("icon", None) elif icon is not undefined.UNDEFINED: - async with icon.stream(executor=self._app.executor) as stream: + icon_resource = files.ensure_resource(icon) + async with icon_resource.stream(executor=self._app.executor) as stream: body.put("icon", await stream.data_uri()) if splash is None: body.put("splash", None) elif splash is not undefined.UNDEFINED: - async with splash.stream(executor=self._app.executor) as stream: + splash_resource = files.ensure_resource(splash) + async with splash_resource.stream(executor=self._app.executor) as stream: body.put("splash", await stream.data_uri()) if banner is None: body.put("banner", None) elif banner is not undefined.UNDEFINED: - async with banner.stream(executor=self._app.executor) as stream: + banner_resource = files.ensure_resource(banner) + async with banner_resource.stream(executor=self._app.executor) as stream: body.put("banner", await stream.data_uri()) raw_response = await self._request(route, body=body, reason=reason) From 33ac0a3533bffbc2eb18aca1157cc1d46e8f29cf Mon Sep 17 00:00:00 2001 From: davfsa Date: Wed, 24 Jun 2020 16:19:47 +0200 Subject: [PATCH 563/922] Added the ability to send only 1 attachment without having to have a dict - Also fixes some flake8 issues --- hikari/impl/rest.py | 1 - hikari/models/messages.py | 16 ++++++++++++--- hikari/models/webhooks.py | 16 ++++++++++++--- hikari/net/gateway.py | 6 +++--- hikari/net/rest.py | 40 +++++++++++++++++++++++++++---------- hikari/utilities/files.py | 3 +-- hikari/utilities/reflect.py | 2 +- 7 files changed, 61 insertions(+), 23 deletions(-) diff --git a/hikari/impl/rest.py b/hikari/impl/rest.py index 4cea200b65..ae6599ed5c 100644 --- a/hikari/impl/rest.py +++ b/hikari/impl/rest.py @@ -25,7 +25,6 @@ __all__: typing.Final[typing.List[str]] = ["RESTAppImpl"] -import logging import typing from concurrent import futures diff --git a/hikari/models/messages.py b/hikari/models/messages.py index 38fa008eb1..1afc589477 100644 --- a/hikari/models/messages.py +++ b/hikari/models/messages.py @@ -413,7 +413,10 @@ async def reply( text: typing.Union[undefined.UndefinedType, str] = undefined.UNDEFINED, *, embed: typing.Union[undefined.UndefinedType, embeds_.Embed] = undefined.UNDEFINED, - attachments: typing.Union[undefined.UndefinedType, typing.Sequence[files_.Resource]] = undefined.UNDEFINED, + attachment: typing.Union[undefined.UndefinedType, str, files_.Resource] = undefined.UNDEFINED, + attachments: typing.Union[ + undefined.UndefinedType, typing.Sequence[typing.Union[str, files_.Resource]] + ] = undefined.UNDEFINED, mentions_everyone: bool = False, user_mentions: typing.Union[ typing.Collection[typing.Union[snowflake.Snowflake, int, str, users.User]], bool @@ -436,10 +439,14 @@ async def reply( and can usually be ignored. tts : bool or hikari.utilities.undefined.UndefinedType If specified, whether the message will be sent as a TTS message. - attachments : typing.Sequence[hikari.utilities.files.Resource] or hikari.utilities.undefined.UndefinedType + attachment : hikari.utilities.files.Resource or str or hikari.utilities.undefined.UndefinedType + If specified, a attachment to upload, if desired. This can + be a resource, or string of a path on your computer or a URL. + attachments : typing.Sequence[hikari.utilities.files.Resource or str] or hikari.utilities.undefined.UndefinedType If specified, a sequence of attachments to upload, if desired. Should be between 1 and 10 objects in size (inclusive), also - including embed attachments. + including embed attachments. These can be resources, or + strings consisting of paths on your computer or URLs. embed : hikari.models.embeds.Embed or hikari.utilities.undefined.UndefinedType If specified, the embed object to send with the message. mentions_everyone : bool @@ -478,12 +485,15 @@ async def reply( ValueError If more than 100 unique objects/entities are passed for `role_mentions` or `user_mentions`. + TypeError + If both `attachment` and `attachments` are specified. """ # noqa: E501 - Line too long return await self.app.rest.create_message( channel=self.channel_id, text=text, nonce=nonce, tts=tts, + attachment=attachment, attachments=attachments, embed=embed, mentions_everyone=mentions_everyone, diff --git a/hikari/models/webhooks.py b/hikari/models/webhooks.py index b72d360312..6605cd7376 100644 --- a/hikari/models/webhooks.py +++ b/hikari/models/webhooks.py @@ -107,7 +107,10 @@ async def execute( username: typing.Union[undefined.UndefinedType, str] = undefined.UNDEFINED, avatar_url: typing.Union[undefined.UndefinedType, str] = undefined.UNDEFINED, tts: typing.Union[undefined.UndefinedType, bool] = undefined.UNDEFINED, - attachments: typing.Union[undefined.UndefinedType, typing.Sequence[files_.Resource]] = undefined.UNDEFINED, + attachment: typing.Union[undefined.UndefinedType, str, files_.Resource] = undefined.UNDEFINED, + attachments: typing.Union[ + undefined.UndefinedType, typing.Sequence[typing.Union[str, files_.Resource]] + ] = undefined.UNDEFINED, embeds: typing.Union[undefined.UndefinedType, typing.Sequence[embeds_.Embed]] = undefined.UNDEFINED, mentions_everyone: bool = True, user_mentions: typing.Union[ @@ -131,8 +134,12 @@ async def execute( avatar with for this request. tts : bool or hikari.utilities.undefined.UndefinedType If specified, whether the message will be sent as a TTS message. - attachments : typing.Sequence[hikari.utilities.files.Resource] or hikari.utilities.undefined.UndefinedType - If specified, a sequence of attachments to upload. + attachment : hikari.utilities.undefined.UndefinedType or str or hikari.utilities.files.Resource + If specified, the message attachment. This can be a resource, + or string of a path on your computer or a URL. + attachments : hikari.utilities.undefined.UndefinedType or typing.Sequence[str or hikari.utilities.files.Resource] + If specified, the message attachments. These can be resources, or + strings consisting of paths on your computer or URLs. embeds : typing.Sequence[hikari.models.embeds.Embed] or hikari.utilities.undefined.UndefinedType If specified, a sequence of between `1` to `10` embed objects (inclusive) to send with the embed. @@ -169,6 +176,8 @@ async def execute( ValueError If either `Webhook.token` is `None` or more than 100 unique objects/entities are passed for `role_mentions` or `user_mentions. + TypeError + If both `attachment` and `attachments` are specified. """ # noqa: E501 - Line too long if not self.token: raise ValueError("Cannot send a message using a webhook where we don't know it's token.") @@ -179,6 +188,7 @@ async def execute( username=username, avatar_url=avatar_url, tts=tts, + attachment=attachment, attachments=attachments, embeds=embeds, mentions_everyone=mentions_everyone, diff --git a/hikari/net/gateway.py b/hikari/net/gateway.py index a76dc099ec..9a97ee1bb3 100644 --- a/hikari/net/gateway.py +++ b/hikari/net/gateway.py @@ -165,9 +165,9 @@ class _InvalidSession(RuntimeError): _RESTART_RATELIMIT_WINDOW: typing.Final[typing.ClassVar[float]] = 30.0 """If the shard restarts more than once within this period of time, then exponentially back-off to prevent spamming the gateway or tanking the CPU. - + This is potentially important if the internet connection turns off, as the - bot will simply attempt to reconnect repeatedly until the connection + bot will simply attempt to reconnect repeatedly until the connection resumes. """ @@ -291,7 +291,7 @@ async def _run(self) -> None: # the entire inheritance conduit in a patch context. await http_client.HTTPClient.close(self) - async def _run_once(self) -> bool: + async def _run_once(self) -> bool: # noqa: C901, CFQ001 - Too complex, exceeds max allowed length # returns `True` if we can reconnect, or `False` otherwise. self._request_close_event.clear() diff --git a/hikari/net/rest.py b/hikari/net/rest.py index 025514e44e..1690f1976e 100644 --- a/hikari/net/rest.py +++ b/hikari/net/rest.py @@ -1047,6 +1047,7 @@ async def create_message( text: typing.Union[undefined.UndefinedType, typing.Any] = undefined.UNDEFINED, *, embed: typing.Union[undefined.UndefinedType, embeds_.Embed] = undefined.UNDEFINED, + attachment: typing.Union[undefined.UndefinedType, str, files.Resource] = undefined.UNDEFINED, attachments: typing.Union[ undefined.UndefinedType, typing.Sequence[typing.Union[str, files.Resource]] ] = undefined.UNDEFINED, @@ -1067,6 +1068,9 @@ async def create_message( If specified, the message contents. embed : hikari.utilities.undefined.UndefinedType or hikari.models.embeds.Embed If specified, the message embed. + attachment : hikari.utilities.undefined.UndefinedType or str or hikari.utilities.files.Resource + If specified, the message attachment. This can be a resource, + or string of a path on your computer or a URL. attachments : hikari.utilities.undefined.UndefinedType or typing.Sequence[str or hikari.utilities.files.Resource] If specified, the message attachments. These can be resources, or strings consisting of paths on your computer or URLs. @@ -1108,11 +1112,19 @@ async def create_message( If the channel is not found. hikari.errors.ServerHTTPErrorResponse If an internal error occurs on Discord while handling the request. + ValueError + If more than 100 unique objects/entities are passed for + `role_mentions` or `user_mentions`. + TypeError + If both `attachment` and `attachments` are specified. !!! warning You are expected to make a connection to the gateway and identify once before being able to use this endpoint for a bot. """ # noqa: E501 - Line too long + if attachment is not undefined.UNDEFINED and attachments is not undefined.UNDEFINED: + raise ValueError("You may only specify one of 'attachment' or 'attachments', not both") + route = routes.POST_CHANNEL_MESSAGES.compile(channel=channel) body = data_binding.JSONObjectBuilder() @@ -1121,17 +1133,18 @@ async def create_message( body.put("nonce", nonce) body.put("tts", tts) - if attachments is undefined.UNDEFINED: - final_attachments: typing.List[files.Resource] = [] - else: - final_attachments = [files.ensure_resource(a) for a in attachments] + final_attachments: typing.List[files.Resource] = [] + if attachment is not undefined.UNDEFINED: + final_attachments.append(files.ensure_resource(attachment)) + if attachments is not undefined.UNDEFINED: + final_attachments.extend([files.ensure_resource(a) for a in attachments]) if embed is not undefined.UNDEFINED: embed_payload, embed_attachments = self._app.entity_factory.serialize_embed(embed) body.put("embed", embed_payload) final_attachments.extend(embed_attachments) - if attachments: + if final_attachments: form = data_binding.URLEncodedForm() form.add_field("payload_json", data_binding.dump_json(body), content_type=strings.APPLICATION_JSON) @@ -1516,12 +1529,18 @@ async def execute_webhook( username: typing.Union[undefined.UndefinedType, str] = undefined.UNDEFINED, avatar_url: typing.Union[undefined.UndefinedType, str] = undefined.UNDEFINED, embeds: typing.Union[undefined.UndefinedType, typing.Sequence[embeds_.Embed]] = undefined.UNDEFINED, - attachments: typing.Union[undefined.UndefinedType, typing.Sequence[files.Resource]] = undefined.UNDEFINED, + attachment: typing.Union[undefined.UndefinedType, str, files.Resource] = undefined.UNDEFINED, + attachments: typing.Union[ + undefined.UndefinedType, typing.Sequence[typing.Union[str, files.Resource]] + ] = undefined.UNDEFINED, tts: typing.Union[undefined.UndefinedType, bool] = undefined.UNDEFINED, mentions_everyone: bool = True, user_mentions: typing.Union[typing.Collection[typing.Union[users.User, snowflake.UniqueObject]], bool] = True, role_mentions: typing.Union[typing.Collection[typing.Union[snowflake.UniqueObject, guilds.Role]], bool] = True, ) -> messages_.Message: + if attachment is not undefined.UNDEFINED and attachments is not undefined.UNDEFINED: + raise ValueError("You may only specify one of 'attachment' or 'attachments', not both") + if token is undefined.UNDEFINED: route = routes.POST_WEBHOOK.compile(webhook=webhook) no_auth = False @@ -1529,10 +1548,11 @@ async def execute_webhook( route = routes.POST_WEBHOOK_WITH_TOKEN.compile(webhook=webhook, token=token) no_auth = True - if attachments is undefined.UNDEFINED: - final_attachments: typing.List[files.Resource] = [] - else: - final_attachments = [files.ensure_resource(a) for a in attachments] + final_attachments: typing.List[files.Resource] = [] + if attachment is not undefined.UNDEFINED: + final_attachments.append(files.ensure_resource(attachment)) + if attachments is not undefined.UNDEFINED: + final_attachments.extend([files.ensure_resource(a) for a in attachments]) serialized_embeds = [] diff --git a/hikari/utilities/files.py b/hikari/utilities/files.py index 36b4bd0f91..b1407ea889 100644 --- a/hikari/utilities/files.py +++ b/hikari/utilities/files.py @@ -48,13 +48,12 @@ import attr from hikari.net import http_client -from hikari.utilities import reflect if typing.TYPE_CHECKING: import concurrent.futures import types -_LOGGER: typing.Final[logging.Logger] = reflect.get_logger(__name__) +_LOGGER: typing.Final[logging.Logger] = logging.getLogger(__name__) _MAGIC: typing.Final[int] = 50 * 1024 diff --git a/hikari/utilities/reflect.py b/hikari/utilities/reflect.py index 507ad194bd..c23f8e87a3 100644 --- a/hikari/utilities/reflect.py +++ b/hikari/utilities/reflect.py @@ -84,7 +84,7 @@ def get_logger(obj: typing.Union[typing.Type[typing.Any], typing.Any], *addition logging.Logger The _LOGGER to use. """ - if isinstance(obj, str): + if isinstance(obj, str): # noqa: IFSTMT001 - Oneliner (makes it unreadable) str_obj = obj else: str_obj = (obj if isinstance(obj, type) else type(obj)).__module__ From ef46377e91f555a03c825ac68cdb1c48c2a5476b Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Mon, 22 Jun 2020 12:36:29 +0100 Subject: [PATCH 564/922] Finished _run_once test cases; fixed other missing lines upto L443 (update presence) --- hikari/errors.py | 11 ++--- hikari/net/gateway.py | 12 +++-- tests/hikari/net/test_gateway.py | 82 ++++++++++++++++++++++++++++---- 3 files changed, 84 insertions(+), 21 deletions(-) diff --git a/hikari/errors.py b/hikari/errors.py index 000892679b..f5037e7b4d 100644 --- a/hikari/errors.py +++ b/hikari/errors.py @@ -117,17 +117,16 @@ class GatewayServerClosedConnectionError(GatewayError): A string explaining the issue. code : int or None The close code. - + can_reconnect : bool + If `True`, a reconnect will occur after this is raised rather than + it being propagated to the caller. If `False`, this will be raised. """ - __slots__ = ("code", "can_reconnect", "should_backoff") + __slots__ = ("code", "can_reconnect") - def __init__( - self, reason: str, code: typing.Optional[int] = None, can_reconnect: bool = False, should_backoff: bool = True, - ) -> None: + def __init__(self, reason: str, code: typing.Optional[int] = None, can_reconnect: bool = False) -> None: self.code = code self.can_reconnect = can_reconnect - self.should_backoff = should_backoff super().__init__(reason) def __str__(self) -> str: diff --git a/hikari/net/gateway.py b/hikari/net/gateway.py index 9a97ee1bb3..0c8805268e 100644 --- a/hikari/net/gateway.py +++ b/hikari/net/gateway.py @@ -323,6 +323,10 @@ async def _run_once(self) -> bool: # noqa: C901, CFQ001 - Too complex, exceeds self.connected_at = self._now() + # Technically we are connected after the hello, but this ensures we can send and receive + # before firing that event. + self._dispatch("CONNECTED", {}) + self._zlib = zlib.decompressobj() self._handshake_event.clear() @@ -330,10 +334,6 @@ async def _run_once(self) -> bool: # noqa: C901, CFQ001 - Too complex, exceeds await self._handshake() - # Technically we are connected after the hello, but this ensures we can send and receive - # before firing that event. - self._dispatch("CONNECTED", {}) - # We should ideally set this after HELLO, but it should be fine # here as well. If we don't heartbeat in time, something probably # went majorly wrong anyway. @@ -388,6 +388,8 @@ async def _run_once(self) -> bool: # noqa: C901, CFQ001 - Too complex, exceeds await self._close_ws(self._GatewayCloseCode.RFC_6455_UNEXPECTED_CONDITION, "you broke the connection") self._seq = None self.session_id = None + self._backoff.reset() + self._request_close_event.set() raise except Exception as ex: @@ -632,7 +634,7 @@ async def _receive_json_payload(self) -> data_binding.JSONObject: ) # Assume we can always resume first. - raise errors.GatewayServerClosedConnectionError(reason, close_code, can_reconnect, True) + raise errors.GatewayServerClosedConnectionError(reason, close_code, can_reconnect) if message.type == aiohttp.WSMsgType.CLOSING or message.type == aiohttp.WSMsgType.CLOSED: raise self._SocketClosed diff --git a/tests/hikari/net/test_gateway.py b/tests/hikari/net/test_gateway.py index 077b536501..8655f76857 100644 --- a/tests/hikari/net/test_gateway.py +++ b/tests/hikari/net/test_gateway.py @@ -125,6 +125,20 @@ async def test_waits_for_ready(self, client): @pytest.mark.asyncio class TestClose: + @pytest.fixture + def client(self): + class GatewayStub(gateway.Gateway): + @property + def is_alive(self): + return getattr(self, "_is_alive", False) + + return GatewayStub( + url="wss://gateway.discord.gg", + token="lol", + app=mock.MagicMock(), + config=mock.MagicMock(), + ) + async def test_when_already_closed_does_nothing(self, client): client._request_close_event = mock.MagicMock(asyncio.Event) client._request_close_event.is_set = mock.MagicMock(return_value=True) @@ -135,7 +149,7 @@ async def test_when_already_closed_does_nothing(self, client): @pytest.mark.parametrize("is_alive", [True, False]) async def test_close_sets_request_close_event(self, client, is_alive): - client.__dict__["is_alive"] = is_alive + client.__dict__["_is_alive"] = is_alive client._request_close_event = mock.MagicMock(asyncio.Event) client._request_close_event.is_set = mock.MagicMock(return_value=False) @@ -145,7 +159,7 @@ async def test_close_sets_request_close_event(self, client, is_alive): @pytest.mark.parametrize("is_alive", [True, False]) async def test_websocket_closed_if_not_None(self, client, is_alive): - client.__dict__["is_alive"] = is_alive + client.__dict__["_is_alive"] = is_alive client._request_close_event = mock.MagicMock(asyncio.Event) client._request_close_event.is_set = mock.MagicMock(return_value=False) client._close_ws = mock.AsyncMock() @@ -157,7 +171,7 @@ async def test_websocket_closed_if_not_None(self, client, is_alive): @pytest.mark.parametrize("is_alive", [True, False]) async def test_websocket_not_closed_if_None(self, client, is_alive): - client.__dict__["is_alive"] = is_alive + client.__dict__["_is_alive"] = is_alive client._request_close_event = mock.MagicMock(asyncio.Event) client._request_close_event.is_set = mock.MagicMock(return_value=False) client._close_ws = mock.AsyncMock() @@ -238,14 +252,12 @@ def client(self): "_InvalidSession", "_Reconnect", "_SocketClosed", - "_dispatch", "_now", "_GatewayCloseCode", "_GatewayOpcode", ), also_mock=["_backoff", "_handshake_event", "_request_close_event", "_logger",], ) - client._dispatch = mock.AsyncMock() # Disable backoff checking by making the condition a negative tautology. client._RESTART_RATELIMIT_WINDOW = -1 # First call is used for backoff checks, the second call is used @@ -276,14 +288,14 @@ async def test_backoff_and_waits_if_restarted_too_quickly(self, client): client._backoff.__next__ = mock.MagicMock(return_value=24.37) stack = contextlib.ExitStack() - wait_for = stack.enter_context(mock.patch.object(asyncio, "wait_for")) + wait_for = stack.enter_context(mock.patch.object(asyncio, "wait_for", side_effect=asyncio.TimeoutError)) create_task = stack.enter_context(mock.patch.object(asyncio, "create_task")) with stack: await client._run_once() client._backoff.__next__.assert_called_once_with() - create_task.assert_called_once_with(client._request_close_event.wait(), name="gateway shard 3 backing off") + create_task.assert_any_call(client._request_close_event.wait(), name="gateway shard 3 backing off") wait_for.assert_called_once_with(create_task(), timeout=24.37) @hikari_test_helpers.timeout() @@ -387,7 +399,6 @@ async def test_connected_event_dispatched_before_polling_events(self, client): class Error(Exception): pass - client._dispatch = mock.MagicMock() client._poll_events = mock.AsyncMock(side_effect=Error) with pytest.raises(Error): @@ -525,5 +536,56 @@ def poll_events(): client._backoff.reset.assert_not_called() @hikari_test_helpers.timeout() - async def test_server_connection_error_resumes_if_reconnectable(self): - ... + async def test_server_connection_error_resumes_if_reconnectable(self, client): + client._poll_events = mock.AsyncMock(side_effect=errors.GatewayServerClosedConnectionError("blah", None, True)) + client._seq = 1234 + client.session_id = "69420" + assert await client._run_once() is True + assert client._seq == 1234 + assert client.session_id == "69420" + + async def test_server_connection_error_closes_websocket_if_reconnectable(self, client): + client._poll_events = mock.AsyncMock(side_effect=errors.GatewayServerClosedConnectionError("blah", None, True)) + assert await client._run_once() is True + client._close_ws.assert_awaited_once_with(gateway.Gateway._GatewayCloseCode.RFC_6455_NORMAL_CLOSURE, "you hung up on me") + + @hikari_test_helpers.timeout() + async def test_server_connection_error_does_not_reconnect_if_not_reconnectable(self, client): + client._poll_events = mock.AsyncMock(side_effect=errors.GatewayServerClosedConnectionError("blah", None, False)) + client._seq = 1234 + client.session_id = "69420" + with pytest.raises(errors.GatewayServerClosedConnectionError): + await client._run_once() + client._request_close_event.set.assert_called_once_with() + assert client._seq is None + assert client.session_id is None + client._backoff.reset.assert_called_once_with() + + async def test_server_connection_error_closes_websocket_if_not_reconnectable(self, client): + client._poll_events = mock.AsyncMock(side_effect=errors.GatewayServerClosedConnectionError("blah", None, False)) + with pytest.raises(errors.GatewayServerClosedConnectionError): + await client._run_once() + client._close_ws.assert_awaited_once_with(gateway.Gateway._GatewayCloseCode.RFC_6455_UNEXPECTED_CONDITION, "you broke the connection") + + async def test_other_exception_closes_websocket(self, client): + client._poll_events = mock.AsyncMock(side_effect=RuntimeError()) + + with pytest.raises(RuntimeError): + await client._run_once() + + client._close_ws.assert_awaited_once_with(gateway.Gateway._GatewayCloseCode.RFC_6455_UNEXPECTED_CONDITION, "unexpected error occurred") + + async def test_dispatches_disconnect_if_connected(self, client): + await client._run_once() + client._dispatch.assert_any_call("CONNECTED", {}) + client._dispatch.assert_any_call("DISCONNECTED", {}) + + async def test_no_dispatch_disconnect_if_not_connected(self, client): + client._create_ws = mock.MagicMock(side_effect=RuntimeError) + with pytest.raises(RuntimeError): + await client._run_once() + client._dispatch.assert_not_called() + + async def test_connected_at_reset_to_nan_on_exit(self, client): + await client._run_once() + assert math.isnan(client.connected_at) From 54caac02de46a501a93127771c92c5ae2cc71486 Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Mon, 22 Jun 2020 15:06:08 +0100 Subject: [PATCH 565/922] Moved conversion of activity and voice state gateway payloads to entity factory. --- hikari/api/entity_factory.py | 67 ++++++++++++++++++++++++++++++++ hikari/impl/entity_factory.py | 39 +++++++++++++++++++ hikari/net/gateway.py | 67 +++++++++++--------------------- tests/hikari/net/test_gateway.py | 48 ++++++++++++++++++----- 4 files changed, 166 insertions(+), 55 deletions(-) diff --git a/hikari/api/entity_factory.py b/hikari/api/entity_factory.py index 517e03308b..e971825dc1 100644 --- a/hikari/api/entity_factory.py +++ b/hikari/api/entity_factory.py @@ -27,6 +27,8 @@ from hikari.utilities import undefined if typing.TYPE_CHECKING: + import datetime + from hikari.events import channel as channel_events from hikari.events import guild as guild_events from hikari.events import message as message_events @@ -48,6 +50,7 @@ from hikari.net import gateway from hikari.utilities import data_binding from hikari.utilities import files + from hikari.utilities import snowflake class IEntityFactoryComponent(component.IComponent, abc.ABC): @@ -1317,3 +1320,67 @@ def deserialize_voice_server_update_event( hikari.events.voice.VoiceServerUpdateEvent The parsed voice server update event object. """ + + ############################### + # GATEWAY-SPECIFIC UTILITIES. # + ############################### + + @abc.abstractmethod + def serialize_gateway_presence( + self, + idle_since: typing.Union[undefined.UndefinedType, None, datetime.datetime] = undefined.UNDEFINED, + is_afk: typing.Union[undefined.UndefinedType, bool] = undefined.UNDEFINED, + status: typing.Union[undefined.UndefinedType, presence_models.Status] = undefined.UNDEFINED, + activity: typing.Union[undefined.UndefinedType, None, presence_models.Activity] = undefined.UNDEFINED, + ) -> data_binding.JSONObject: + """Serialize a set of presence parameters into a raw gateway payload. + + Any parameters that are left to be unspecified are omitted from the + generated payload. + + Parameters + ---------- + idle_since : hikari.utilities.undefined.UndefinedType or None or datetime.datetime + The time that the user should appear to be idle since. If `None`, + then the user is marked as not being idle. + is_afk : hikari.utilities.undefined.UndefinedType or bool + If `True`, the user becomes AFK. This will move them + + status : hikari.utilities.undefined.UndefinedType or hikari.models.presences.Status + activity : hikari.utilities.undefined.UndefinedType or None or hikari.models.presences.Activity + + Returns + ------- + hikari.utilities.data_binding.JSONObject + The serialized presence. + """ + + @abc.abstractmethod + def serialize_gateway_voice_state_update( + self, + guild: typing.Union[guild_models.Guild, snowflake.UniqueObject], + channel: typing.Union[channel_models.GuildVoiceChannel, snowflake.UniqueObject, None], + self_mute: bool, + self_deaf: bool, + ) -> data_binding.JSONObject: + """Serialize a voice state update payload into a raw gateway payload. + + Parameters + ---------- + guild : hikari.models.guilds.Guild or hikari.utilities.snowflake.UniqueObject + The guild to update the voice state in. + channel : hikari.models.channels.GuildVoiceChannel or hikari.utilities.snowflake.UniqueObject or None + The voice channel to change to, or `None` if attempting to leave a + voice channel and disconnect entirely. + self_mute : bool + `True` if the user should be muted, `False` if they should be + unmuted. + self_deaf : bool + `True` if the user should be deafened, `False` if they should be + able to hear other users. + + Returns + ------- + hikari.utilities.data_binding.JSONObject + The serialized payload. + """ diff --git a/hikari/impl/entity_factory.py b/hikari/impl/entity_factory.py index afcbb0b51b..e1236fbec8 100644 --- a/hikari/impl/entity_factory.py +++ b/hikari/impl/entity_factory.py @@ -1836,3 +1836,42 @@ def deserialize_voice_server_update_event( voice_server_update.guild_id = snowflake.Snowflake(payload["guild_id"]) voice_server_update.endpoint = payload["endpoint"] return voice_server_update + + def serialize_gateway_presence( + self, + idle_since: typing.Union[undefined.UndefinedType, typing.Optional[datetime.datetime]] = undefined.UNDEFINED, + is_afk: typing.Union[undefined.UndefinedType, bool] = undefined.UNDEFINED, + status: typing.Union[undefined.UndefinedType, presence_models.Status] = undefined.UNDEFINED, + activity: typing.Union[ + undefined.UndefinedType, typing.Optional[presence_models.Activity] + ] = undefined.UNDEFINED, + ) -> data_binding.JSONObject: + if activity is not None and activity is not undefined.UNDEFINED: + game: typing.Union[undefined.UndefinedType, None, data_binding.JSONObject] = { + "name": activity.name, + "url": activity.url, + "type": activity.type, + } + else: + game = activity + + payload = data_binding.JSONObjectBuilder() + payload.put("since", idle_since, conversion=datetime.datetime.timestamp) + payload.put("afk", is_afk) + payload.put("status", status) + payload.put("game", game) + return payload + + def serialize_gateway_voice_state_update( + self, + guild: typing.Union[guild_models.Guild, snowflake.UniqueObject], + channel: typing.Union[channel_models.GuildVoiceChannel, snowflake.UniqueObject, None], + self_mute: bool, + self_deaf: bool, + ) -> data_binding.JSONObject: + return { + "guild_id": str(int(guild)), + "channel_id": str(int(channel)) if channel is not None else None, + "self_mute": self_mute, + "self_deaf": self_deaf, + } diff --git a/hikari/net/gateway.py b/hikari/net/gateway.py index 0c8805268e..45fb1fefa1 100644 --- a/hikari/net/gateway.py +++ b/hikari/net/gateway.py @@ -409,9 +409,9 @@ async def _run_once(self) -> bool: # noqa: C901, CFQ001 - Too complex, exceeds async def update_presence( self, *, - idle_since: typing.Union[undefined.UndefinedType, typing.Optional[datetime.datetime]] = undefined.UNDEFINED, + idle_since: typing.Union[undefined.UndefinedType, None, datetime.datetime] = undefined.UNDEFINED, is_afk: typing.Union[undefined.UndefinedType, bool] = undefined.UNDEFINED, - activity: typing.Union[undefined.UndefinedType, typing.Optional[presences.Activity]] = undefined.UNDEFINED, + activity: typing.Union[undefined.UndefinedType, None, presences.Activity] = undefined.UNDEFINED, status: typing.Union[undefined.UndefinedType, presences.Status] = undefined.UNDEFINED, ) -> None: """Update the presence of the shard user. @@ -430,9 +430,24 @@ async def update_presence( status : hikari.models.presences.Status or hikari.utilities.undefined.UndefinedType The web status to show. If undefined, this will not be changed. """ - presence = self._build_presence_payload(idle_since=idle_since, is_afk=is_afk, status=status, activity=activity) + if idle_since is undefined.UNDEFINED: + idle_since = self._idle_since + if is_afk is undefined.UNDEFINED: + is_afk = self._is_afk + if status is undefined.UNDEFINED: + status = self._status + if activity is undefined.UNDEFINED: + activity = self._activity + + presence = self._app.entity_factory.serialize_gateway_presence( + idle_since=idle_since, is_afk=is_afk, status=status, activity=activity + ) + payload: data_binding.JSONObject = {"op": self._GatewayOpcode.PRESENCE_UPDATE, "d": presence} + await self._send_json(payload) + + # Update internal status. self._idle_since = idle_since if idle_since is not undefined.UNDEFINED else self._idle_since self._is_afk = is_afk if is_afk is not undefined.UNDEFINED else self._is_afk self._activity = activity if activity is not undefined.UNDEFINED else self._activity @@ -463,15 +478,7 @@ async def update_voice_state( If `True`, the bot will deafen itself in that voice channel. If `False`, then it will undeafen itself. """ - payload: data_binding.JSONObject = { - "op": self._GatewayOpcode.VOICE_STATE_UPDATE, - "d": { - "guild_id": str(int(guild)), - "channel_id": str(int(channel)) if channel is not None else None, - "self_mute": self_mute, - "self_deaf": self_deaf, - }, - } + payload = self._app.entity_factory.serialize_gateway_voice_state_update(guild, channel, self_mute, self_deaf) await self._send_json(payload) async def _close_ws(self, code: int, message: str) -> None: @@ -524,7 +531,9 @@ async def _handshake(self) -> None: if undefined.count(self._activity, self._status, self._idle_since, self._is_afk) != 4: # noinspection PyTypeChecker - payload["d"]["presence"] = self._build_presence_payload() + payload["d"]["presence"] = self._app.entity_factory.serialize_gateway_presence( + self._idle_since, self._is_afk, self._status, self._activity, + ) await self._send_json(payload) @@ -696,35 +705,3 @@ def _log_debug_payload(self, payload: str, message: str, *args: typing.Any) -> N args = (*args, self._seq, self.session_id, len(payload)) self._logger.debug(message, *args) - - def _build_presence_payload( - self, - idle_since: typing.Union[undefined.UndefinedType, typing.Optional[datetime.datetime]] = undefined.UNDEFINED, - is_afk: typing.Union[undefined.UndefinedType, bool] = undefined.UNDEFINED, - status: typing.Union[undefined.UndefinedType, presences.Status] = undefined.UNDEFINED, - activity: typing.Union[undefined.UndefinedType, typing.Optional[presences.Activity]] = undefined.UNDEFINED, - ) -> data_binding.JSONObject: - if idle_since is undefined.UNDEFINED: - idle_since = self._idle_since - if is_afk is undefined.UNDEFINED: - is_afk = self._is_afk - if status is undefined.UNDEFINED: - status = self._status - if activity is undefined.UNDEFINED: - activity = self._activity - - if activity is not None and activity is not undefined.UNDEFINED: - game: typing.Union[undefined.UndefinedType, None, data_binding.JSONObject] = { - "name": activity.name, - "url": activity.url, - "type": activity.type, - } - else: - game = activity - - payload = data_binding.JSONObjectBuilder() - payload.put("since", idle_since, conversion=datetime.datetime.timestamp) - payload.put("afk", is_afk) - payload.put("status", status) - payload.put("game", game) - return payload diff --git a/tests/hikari/net/test_gateway.py b/tests/hikari/net/test_gateway.py index 8655f76857..e1e4755805 100644 --- a/tests/hikari/net/test_gateway.py +++ b/tests/hikari/net/test_gateway.py @@ -17,6 +17,7 @@ # along with Hikari. If not, see . import asyncio import contextlib +import datetime import math import aiohttp.client_reqrep @@ -24,6 +25,7 @@ import pytest from hikari import errors +from hikari.models import presences from hikari.net import gateway from hikari.net import http_client from tests.hikari import hikari_test_helpers @@ -132,12 +134,7 @@ class GatewayStub(gateway.Gateway): def is_alive(self): return getattr(self, "_is_alive", False) - return GatewayStub( - url="wss://gateway.discord.gg", - token="lol", - app=mock.MagicMock(), - config=mock.MagicMock(), - ) + return GatewayStub(url="wss://gateway.discord.gg", token="lol", app=mock.MagicMock(), config=mock.MagicMock(),) async def test_when_already_closed_does_nothing(self, client): client._request_close_event = mock.MagicMock(asyncio.Event) @@ -517,7 +514,7 @@ async def test_socket_closed_is_restartable_if_no_closure_request(self, client, @pytest.mark.parametrize( ["zombied", "request_close", "expect_backoff_called"], - [(True, True, True), (True, False, True), (False, True, False), (False, False, False),], + [(True, True, True), (True, False, True), (False, True, False), (False, False, False)], ) @hikari_test_helpers.timeout() async def test_socket_closed_resets_backoff(self, client, zombied, request_close, expect_backoff_called): @@ -547,7 +544,9 @@ async def test_server_connection_error_resumes_if_reconnectable(self, client): async def test_server_connection_error_closes_websocket_if_reconnectable(self, client): client._poll_events = mock.AsyncMock(side_effect=errors.GatewayServerClosedConnectionError("blah", None, True)) assert await client._run_once() is True - client._close_ws.assert_awaited_once_with(gateway.Gateway._GatewayCloseCode.RFC_6455_NORMAL_CLOSURE, "you hung up on me") + client._close_ws.assert_awaited_once_with( + gateway.Gateway._GatewayCloseCode.RFC_6455_NORMAL_CLOSURE, "you hung up on me" + ) @hikari_test_helpers.timeout() async def test_server_connection_error_does_not_reconnect_if_not_reconnectable(self, client): @@ -565,7 +564,9 @@ async def test_server_connection_error_closes_websocket_if_not_reconnectable(sel client._poll_events = mock.AsyncMock(side_effect=errors.GatewayServerClosedConnectionError("blah", None, False)) with pytest.raises(errors.GatewayServerClosedConnectionError): await client._run_once() - client._close_ws.assert_awaited_once_with(gateway.Gateway._GatewayCloseCode.RFC_6455_UNEXPECTED_CONDITION, "you broke the connection") + client._close_ws.assert_awaited_once_with( + gateway.Gateway._GatewayCloseCode.RFC_6455_UNEXPECTED_CONDITION, "you broke the connection" + ) async def test_other_exception_closes_websocket(self, client): client._poll_events = mock.AsyncMock(side_effect=RuntimeError()) @@ -573,7 +574,9 @@ async def test_other_exception_closes_websocket(self, client): with pytest.raises(RuntimeError): await client._run_once() - client._close_ws.assert_awaited_once_with(gateway.Gateway._GatewayCloseCode.RFC_6455_UNEXPECTED_CONDITION, "unexpected error occurred") + client._close_ws.assert_awaited_once_with( + gateway.Gateway._GatewayCloseCode.RFC_6455_UNEXPECTED_CONDITION, "unexpected error occurred" + ) async def test_dispatches_disconnect_if_connected(self, client): await client._run_once() @@ -589,3 +592,28 @@ async def test_no_dispatch_disconnect_if_not_connected(self, client): async def test_connected_at_reset_to_nan_on_exit(self, client): await client._run_once() assert math.isnan(client.connected_at) + + +@pytest.mark.asyncio +class TestUpdatePresence: + @pytest.fixture + def client(self): + client = hikari_test_helpers.unslot_class(gateway.Gateway)( + url="wss://gateway.discord.gg", + token="lol", + app=mock.MagicMock(), + config=mock.MagicMock(), + shard_id=3, + shard_count=17, + ) + return hikari_test_helpers.mock_methods_on(client, except_=("update_presence",)) + + async def test_update_presence_sends_all_params(self, client): + now = datetime.datetime.now() + + await client.update_presence( + idle_since=now, + is_afk=False, + activity=presences.Activity(type=presences.ActivityType.PLAYING, name="with my saxaphone"), + status=presences.Status.DO_NOT_DISTURB, + ) From 933c286efcf772361c65fa9efbdce15e477cd43d Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Wed, 24 Jun 2020 12:55:05 +0100 Subject: [PATCH 566/922] More gateway tests. --- hikari/api/entity_factory.py | 4 ++-- hikari/api/gateway_zookeeper.py | 4 ++-- hikari/impl/gateway_zookeeper.py | 4 ++-- hikari/net/gateway.py | 12 ++++++------ tests/hikari/net/test_gateway.py | 32 ++++++++++++++++++++++++++------ 5 files changed, 38 insertions(+), 18 deletions(-) diff --git a/hikari/api/entity_factory.py b/hikari/api/entity_factory.py index e971825dc1..270fda32df 100644 --- a/hikari/api/entity_factory.py +++ b/hikari/api/entity_factory.py @@ -1329,7 +1329,7 @@ def deserialize_voice_server_update_event( def serialize_gateway_presence( self, idle_since: typing.Union[undefined.UndefinedType, None, datetime.datetime] = undefined.UNDEFINED, - is_afk: typing.Union[undefined.UndefinedType, bool] = undefined.UNDEFINED, + afk: typing.Union[undefined.UndefinedType, bool] = undefined.UNDEFINED, status: typing.Union[undefined.UndefinedType, presence_models.Status] = undefined.UNDEFINED, activity: typing.Union[undefined.UndefinedType, None, presence_models.Activity] = undefined.UNDEFINED, ) -> data_binding.JSONObject: @@ -1343,7 +1343,7 @@ def serialize_gateway_presence( idle_since : hikari.utilities.undefined.UndefinedType or None or datetime.datetime The time that the user should appear to be idle since. If `None`, then the user is marked as not being idle. - is_afk : hikari.utilities.undefined.UndefinedType or bool + afk : hikari.utilities.undefined.UndefinedType or bool If `True`, the user becomes AFK. This will move them status : hikari.utilities.undefined.UndefinedType or hikari.models.presences.Status diff --git a/hikari/api/gateway_zookeeper.py b/hikari/api/gateway_zookeeper.py index 1806c786db..df8b184d3e 100644 --- a/hikari/api/gateway_zookeeper.py +++ b/hikari/api/gateway_zookeeper.py @@ -100,7 +100,7 @@ async def update_presence( status: typing.Union[undefined.UndefinedType, presences.Status] = undefined.UNDEFINED, activity: typing.Union[undefined.UndefinedType, presences.Activity, None] = undefined.UNDEFINED, idle_since: typing.Union[undefined.UndefinedType, datetime.datetime, None] = undefined.UNDEFINED, - is_afk: typing.Union[undefined.UndefinedType, bool] = undefined.UNDEFINED, + afk: typing.Union[undefined.UndefinedType, bool] = undefined.UNDEFINED, ) -> None: """Update the presence of the user for all shards. @@ -125,7 +125,7 @@ async def update_presence( idle_since : datetime.datetime or None or hikari.utilities.undefined.UndefinedType If defined, the time to show up as being idle since, or `None` if not applicable. If undefined, then it is not changed. - is_afk : bool or hikari.utilities.undefined.UndefinedType + afk : bool or hikari.utilities.undefined.UndefinedType If defined, `True` if the user should be marked as AFK, or `False` if not AFK. """ diff --git a/hikari/impl/gateway_zookeeper.py b/hikari/impl/gateway_zookeeper.py index 5b7c0a8fd3..078db37189 100644 --- a/hikari/impl/gateway_zookeeper.py +++ b/hikari/impl/gateway_zookeeper.py @@ -296,11 +296,11 @@ async def update_presence( status: typing.Union[undefined.UndefinedType, presences.Status] = undefined.UNDEFINED, activity: typing.Union[undefined.UndefinedType, presences.Activity, None] = undefined.UNDEFINED, idle_since: typing.Union[undefined.UndefinedType, datetime.datetime, None] = undefined.UNDEFINED, - is_afk: typing.Union[undefined.UndefinedType, bool] = undefined.UNDEFINED, + afk: typing.Union[undefined.UndefinedType, bool] = undefined.UNDEFINED, ) -> None: await asyncio.gather( *( - s.update_presence(status=status, activity=activity, idle_since=idle_since, is_afk=is_afk) + s.update_presence(status=status, activity=activity, idle_since=idle_since, afk=afk) for s in self._shards.values() if s.is_alive ) diff --git a/hikari/net/gateway.py b/hikari/net/gateway.py index 45fb1fefa1..9528acbf8a 100644 --- a/hikari/net/gateway.py +++ b/hikari/net/gateway.py @@ -410,7 +410,7 @@ async def update_presence( self, *, idle_since: typing.Union[undefined.UndefinedType, None, datetime.datetime] = undefined.UNDEFINED, - is_afk: typing.Union[undefined.UndefinedType, bool] = undefined.UNDEFINED, + afk: typing.Union[undefined.UndefinedType, bool] = undefined.UNDEFINED, activity: typing.Union[undefined.UndefinedType, None, presences.Activity] = undefined.UNDEFINED, status: typing.Union[undefined.UndefinedType, presences.Status] = undefined.UNDEFINED, ) -> None: @@ -421,7 +421,7 @@ async def update_presence( idle_since : datetime.datetime or None or hikari.utilities.undefined.UndefinedType The datetime that the user started being idle. If undefined, this will not be changed. - is_afk : bool or hikari.utilities.undefined.UndefinedType + afk : bool or hikari.utilities.undefined.UndefinedType If `True`, the user is marked as AFK. If `False`, the user is marked as being active. If undefined, this will not be changed. activity : hikari.models.presences.Activity or None or hikari.utilities.undefined.UndefinedType @@ -432,15 +432,15 @@ async def update_presence( """ if idle_since is undefined.UNDEFINED: idle_since = self._idle_since - if is_afk is undefined.UNDEFINED: - is_afk = self._is_afk + if afk is undefined.UNDEFINED: + afk = self._is_afk if status is undefined.UNDEFINED: status = self._status if activity is undefined.UNDEFINED: activity = self._activity presence = self._app.entity_factory.serialize_gateway_presence( - idle_since=idle_since, is_afk=is_afk, status=status, activity=activity + idle_since=idle_since, afk=afk, status=status, activity=activity ) payload: data_binding.JSONObject = {"op": self._GatewayOpcode.PRESENCE_UPDATE, "d": presence} @@ -449,7 +449,7 @@ async def update_presence( # Update internal status. self._idle_since = idle_since if idle_since is not undefined.UNDEFINED else self._idle_since - self._is_afk = is_afk if is_afk is not undefined.UNDEFINED else self._is_afk + self._is_afk = afk if afk is not undefined.UNDEFINED else self._is_afk self._activity = activity if activity is not undefined.UNDEFINED else self._activity self._status = status if status is not undefined.UNDEFINED else self._status diff --git a/tests/hikari/net/test_gateway.py b/tests/hikari/net/test_gateway.py index e1e4755805..3950d772e8 100644 --- a/tests/hikari/net/test_gateway.py +++ b/tests/hikari/net/test_gateway.py @@ -606,14 +606,34 @@ def client(self): shard_id=3, shard_count=17, ) - return hikari_test_helpers.mock_methods_on(client, except_=("update_presence",)) + return hikari_test_helpers.mock_methods_on( + client, + except_=( + "update_presence", + "_InvalidSession", + "_Reconnect", + "_SocketClosed", + "_GatewayCloseCode", + "_GatewayOpcode", + ), + ) - async def test_update_presence_sends_all_params(self, client): + async def test_update_presence_transforms_all_params(self, client): now = datetime.datetime.now() + idle_since = now + afk = False + activity = presences.Activity(type=presences.ActivityType.PLAYING, name="with my saxaphone") + status = presences.Status.DO_NOT_DISTURB + + result = object() + client._app.entity_factory.serialize_gateway_presence = mock.MagicMock(return_value=result) await client.update_presence( - idle_since=now, - is_afk=False, - activity=presences.Activity(type=presences.ActivityType.PLAYING, name="with my saxaphone"), - status=presences.Status.DO_NOT_DISTURB, + idle_since=idle_since, afk=afk, activity=activity, status=status, ) + + client._app.entity_factory.serialize_gateway_presence.assert_called_once_with( + idle_since=idle_since, afk=afk, activity=activity, status=status, + ) + + client._send_json.assert_awaited_once_with({"op": gateway.Gateway._GatewayOpcode.PRESENCE_UPDATE, "d": result}) From b508159ecf5fba7b47d1d0d4d5c3a2d25a1bafa8 Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Wed, 24 Jun 2020 18:28:41 +0100 Subject: [PATCH 567/922] Fixes #399 by implementing shared pooling for TCPConnectors for non-bots. Fixes all flake issues. Fixes other voodoo that was found under the carpet as well, and renames some stuff to reflect the former changes. --- .flake8 | 11 +- ci/flake8.nox.py | 2 +- hikari/__init__.py | 2 +- hikari/api/bot.py | 2 +- hikari/api/cache.py | 2 +- hikari/api/component.py | 8 +- hikari/api/entity_factory.py | 2 +- hikari/api/event_consumer.py | 6 +- hikari/api/event_dispatcher.py | 4 +- hikari/api/gateway_zookeeper.py | 2 +- hikari/api/rest.py | 78 ++++- hikari/errors.py | 30 +- hikari/events/channel.py | 8 +- hikari/events/guild.py | 18 +- hikari/events/message.py | 8 +- hikari/impl/bot.py | 8 +- hikari/impl/cache.py | 4 +- hikari/impl/entity_factory.py | 4 +- hikari/impl/event_manager_core.py | 4 +- hikari/impl/rest.py | 136 ++++++++- hikari/models/applications.py | 6 +- hikari/models/audit_logs.py | 2 +- hikari/models/channels.py | 4 +- hikari/models/colors.py | 2 +- hikari/models/embeds.py | 2 +- hikari/models/emojis.py | 2 +- hikari/models/guilds.py | 8 +- hikari/models/invites.py | 4 +- hikari/models/messages.py | 4 +- hikari/models/presences.py | 2 +- hikari/models/users.py | 2 +- hikari/models/voices.py | 2 +- hikari/models/webhooks.py | 2 +- hikari/net/buckets.py | 4 +- hikari/net/gateway.py | 124 ++++---- hikari/net/http_client.py | 5 +- hikari/net/http_settings.py | 22 +- hikari/net/iterators.py | 34 +-- hikari/net/rate_limits.py | 10 +- hikari/net/rest.py | 28 +- hikari/net/rest_utils.py | 4 +- hikari/utilities/data_binding.py | 4 +- hikari/utilities/files.py | 16 +- hikari/utilities/snowflake.py | 4 +- hikari/utilities/undefined.py | 2 +- tests/hikari/impl/test_entity_factory.py | 4 +- tests/hikari/net/test_gateway.py | 348 +++++++++++++---------- tests/hikari/net/test_http_client.py | 9 +- 48 files changed, 628 insertions(+), 371 deletions(-) diff --git a/.flake8 b/.flake8 index bb83a0d63a..7b5c20915d 100644 --- a/.flake8 +++ b/.flake8 @@ -2,12 +2,15 @@ count = true ignore = - E402, # Module level import not at top of file (isn't compatible with our import style). - D105, # Magic methods not having a docstring. - D102, # Missing docstring in public method. A002, # Argument is shadowing a python builtin. A003, # Class attribute is shadowing a python builtin. CFQ002, # Function has too many arguments. + D102, # Missing docstring in public method. + D105, # Magic methods not having a docstring. + E402, # Module level import not at top of file (isn't compatible with our import style). + IFSTMT001 # "use a oneliner here" + T100, # FIX ME comments + T101, # TO DO comments per-file-ignores = @@ -18,7 +21,7 @@ per-file-ignores = # complaints about importing stuff and not using it afterwards hikari/__init__.py:F401 -max-complexity = 15 +max-complexity = 20 max-line-length = 120 show_source = False statistics = False diff --git a/ci/flake8.nox.py b/ci/flake8.nox.py index 09416bcdbe..981c072560 100644 --- a/ci/flake8.nox.py +++ b/ci/flake8.nox.py @@ -38,7 +38,7 @@ def flake8(session: nox.Session) -> None: # in any of the gitlab-supported formats :( format_args = ["--format=junit-xml", f"--output-file={config.FLAKE8_JUNIT}"] else: - format_args = [f"--output-file={config.FLAKE8_TXT}", "--statistics", "--show-source"] + format_args = [f"--output-file={config.FLAKE8_TXT}", "--statistics", "--show-source", "--tee"] # This is because flake8 just appends to the file, so you can end up with # a huge file with the same errors if you run it a couple of times. shutil.rmtree(config.FLAKE8_TXT, ignore_errors=True) diff --git a/hikari/__init__.py b/hikari/__init__.py index 64d62c239a..89378b9857 100644 --- a/hikari/__init__.py +++ b/hikari/__init__.py @@ -35,4 +35,4 @@ from hikari._about import __url__ from hikari._about import __version__ from hikari.impl.bot import BotAppImpl as Bot -from hikari.impl.rest import RESTAppImpl as RESTClient +from hikari.impl.rest import RESTClientFactoryImpl as RESTClientFactory diff --git a/hikari/api/bot.py b/hikari/api/bot.py index 3374ae167b..64d418b049 100644 --- a/hikari/api/bot.py +++ b/hikari/api/bot.py @@ -40,7 +40,7 @@ class IBotApp(event_consumer.IEventConsumerApp, event_dispatcher.IEventDispatche Additionally, bots will contain a collection of Gateway client objects. """ - __slots__ = () + __slots__: typing.Sequence[str] = () @property @abc.abstractmethod diff --git a/hikari/api/cache.py b/hikari/api/cache.py index 8668684ba8..962f597172 100644 --- a/hikari/api/cache.py +++ b/hikari/api/cache.py @@ -42,4 +42,4 @@ class ICacheComponent(component.IComponent, abc.ABC): for cross-process bots. """ - __slots__ = () + __slots__: typing.Sequence[str] = () diff --git a/hikari/api/component.py b/hikari/api/component.py index 59e4111bfd..022f5310f3 100644 --- a/hikari/api/component.py +++ b/hikari/api/component.py @@ -32,7 +32,7 @@ class IComponent(abc.ABC): """A component that makes up part of the application. Objects that derive from this should usually be attributes on the - `hikari.api.rest.IRESTApp` object. + `hikari.api.rest.IRESTClient` object. Examples -------- @@ -42,15 +42,15 @@ class IComponent(abc.ABC): for examples of usage. """ - __slots__ = () + __slots__: typing.Sequence[str] = () @property @abc.abstractmethod - def app(self) -> rest.IRESTApp: + def app(self) -> rest.IRESTClient: """Return the Application that owns this component. Returns ------- - hikari.api.rest.IRESTApp + hikari.api.rest.IRESTClient The application implementation that owns this component. """ diff --git a/hikari/api/entity_factory.py b/hikari/api/entity_factory.py index 270fda32df..85238ad0da 100644 --- a/hikari/api/entity_factory.py +++ b/hikari/api/entity_factory.py @@ -56,7 +56,7 @@ class IEntityFactoryComponent(component.IComponent, abc.ABC): """Interface for components that serialize and deserialize JSON payloads.""" - __slots__ = () + __slots__: typing.Sequence[str] = () ###################### # APPLICATION MODELS # diff --git a/hikari/api/event_consumer.py b/hikari/api/event_consumer.py index 4ca259f35e..905dedf721 100644 --- a/hikari/api/event_consumer.py +++ b/hikari/api/event_consumer.py @@ -40,7 +40,7 @@ class IEventConsumerComponent(component.IComponent, abc.ABC): separately if you are passing events onto a system such as a message queue. """ - __slots__ = () + __slots__: typing.Sequence[str] = () @abc.abstractmethod async def consume_raw_event( @@ -59,7 +59,7 @@ async def consume_raw_event( """ -class IEventConsumerApp(rest.IRESTApp, abc.ABC): +class IEventConsumerApp(rest.IRESTClient, abc.ABC): """Application specialization that supports consumption of raw events. This may be combined with `IGatewayZookeeperApp` for most single-process @@ -67,7 +67,7 @@ class IEventConsumerApp(rest.IRESTApp, abc.ABC): that consume events from a message queue, for example. """ - __slots__ = () + __slots__: typing.Sequence[str] = () @property @abc.abstractmethod diff --git a/hikari/api/event_dispatcher.py b/hikari/api/event_dispatcher.py index f9a380d5c8..0e143f7c14 100644 --- a/hikari/api/event_dispatcher.py +++ b/hikari/api/event_dispatcher.py @@ -38,7 +38,7 @@ class IEventDispatcherBase(abc.ABC): appropriate. """ - __slots__ = () + __slots__: typing.Sequence[str] = () if typing.TYPE_CHECKING: EventT = typing.TypeVar("EventT", bound=base.Event) @@ -309,7 +309,7 @@ class IEventDispatcherApp(IEventDispatcherBase, abc.ABC): ``` """ - __slots__ = () + __slots__: typing.Sequence[str] = () @property @abc.abstractmethod diff --git a/hikari/api/gateway_zookeeper.py b/hikari/api/gateway_zookeeper.py index df8b184d3e..b5af0f3c1e 100644 --- a/hikari/api/gateway_zookeeper.py +++ b/hikari/api/gateway_zookeeper.py @@ -45,7 +45,7 @@ class IGatewayZookeeperApp(event_consumer.IEventConsumerApp, abc.ABC): that feed new events into a message queue, for example. """ - __slots__ = () + __slots__: typing.Sequence[str] = () @property @abc.abstractmethod diff --git a/hikari/api/rest.py b/hikari/api/rest.py index 39f326e5cb..0c9270c4f0 100644 --- a/hikari/api/rest.py +++ b/hikari/api/rest.py @@ -19,13 +19,16 @@ from __future__ import annotations -__all__: typing.Final[typing.List[str]] = ["IRESTApp"] +__all__: typing.Final[typing.List[str]] = ["IRESTClient", "IRESTClientFactory"] import abc import typing +from hikari.net import strings if typing.TYPE_CHECKING: + import types + from concurrent import futures from hikari.api import cache as cache_ @@ -33,16 +36,15 @@ from hikari.net import rest as rest_ -class IRESTApp(abc.ABC): +class IRESTClient(abc.ABC): """Component specialization that is used for REST-only applications. - Examples may include web dashboards, or applications where no gateway - connection is required. As a result, no event conduit is provided by - these implementations. They do however provide a REST client, and the - general components defined in `IRESTApp` + This is a specific instance of a REST-only client provided by pooled + implementations of `IRESTClientFactory`. It may also be used by bots + as a base if they require REST-API access. """ - __slots__ = () + __slots__: typing.Sequence[str] = () @property @abc.abstractmethod @@ -95,3 +97,65 @@ def executor(self) -> typing.Optional[futures.Executor]: @abc.abstractmethod async def close(self) -> None: """Safely shut down all resources.""" + + +class IRESTClientContextManager(IRESTClient): + """An IRESTClient that may behave as a context manager.""" + + @abc.abstractmethod + async def __aenter__(self) -> IRESTClientContextManager: + ... + + @abc.abstractmethod + async def __aexit__( + self, + exc_type: typing.Optional[typing.Type[BaseException]], + exc_val: typing.Optional[BaseException], + exc_tb: typing.Optional[types.TracebackType], + ) -> None: + ... + + +class IRESTClientFactory(abc.ABC): + """A client factory that emits clients. + + This enables a connection pool to be shared for stateless REST-only + applications such as web dashboards, while still using the HTTP architecture + that the bot system will use. + """ + + __slots__: typing.Sequence[str] = () + + @abc.abstractmethod + def acquire(self, token: str, token_type: str = strings.BEARER_TOKEN) -> IRESTClient: + """Acquire a REST client for the given authentication details. + + Parameters + ---------- + token : str + The token to use. + token_type : str + The token type to use. Defaults to `"Bearer"`. + + Returns + ------- + IRESTClient + The REST client to use. + """ + + @abc.abstractmethod + async def close(self) -> None: + """Safely shut down all resources.""" + + @abc.abstractmethod + async def __aenter__(self) -> IRESTClientFactory: + ... + + @abc.abstractmethod + async def __aexit__( + self, + exc_type: typing.Optional[typing.Type[BaseException]], + exc_val: typing.Optional[BaseException], + exc_tb: typing.Optional[types.TracebackType], + ) -> None: + ... diff --git a/hikari/errors.py b/hikari/errors.py index f5037e7b4d..0ea9f3ebeb 100644 --- a/hikari/errors.py +++ b/hikari/errors.py @@ -53,7 +53,7 @@ class HikariError(RuntimeError): You should never initialize this exception directly. """ - __slots__ = () + __slots__: typing.Sequence[str] = () def __repr__(self) -> str: return f"{type(self).__name__}({str(self)!r})" @@ -68,7 +68,7 @@ class HikariWarning(RuntimeWarning): You should never initialize this warning directly. """ - __slots__ = () + __slots__: typing.Sequence[str] = () class GatewayError(HikariError): @@ -80,7 +80,7 @@ class GatewayError(HikariError): A string explaining the issue. """ - __slots__ = ("reason",) + __slots__: typing.Sequence[str] = ("reason",) reason: str """A string to explain the issue.""" @@ -102,7 +102,7 @@ class GatewayClientClosedError(GatewayError): A string explaining the issue. """ - __slots__ = () + __slots__: typing.Sequence[str] = () def __init__(self, reason: str = "The gateway client has been closed") -> None: super().__init__(reason) @@ -122,7 +122,7 @@ class GatewayServerClosedConnectionError(GatewayError): it being propagated to the caller. If `False`, this will be raised. """ - __slots__ = ("code", "can_reconnect") + __slots__: typing.Sequence[str] = ("code", "can_reconnect") def __init__(self, reason: str, code: typing.Optional[int] = None, can_reconnect: bool = False) -> None: self.code = code @@ -144,7 +144,7 @@ class HTTPError(HikariError): The URL that produced this error. """ - __slots__ = ("message", "url") + __slots__: typing.Sequence[str] = ("message", "url") message: str """The error message.""" @@ -173,7 +173,7 @@ class HTTPErrorResponse(HTTPError): The body that was received. """ - __slots__ = ("status", "headers", "raw_body") + __slots__: typing.Sequence[str] = ("status", "headers", "raw_body") status: typing.Union[int, http.HTTPStatus] """The HTTP status code for the response.""" @@ -221,7 +221,7 @@ class ClientHTTPErrorResponse(HTTPErrorResponse): errors when encountered. """ - __slots__ = () + __slots__: typing.Sequence[str] = () class BadRequest(ClientHTTPErrorResponse): @@ -237,7 +237,7 @@ class BadRequest(ClientHTTPErrorResponse): The body that was received. """ - __slots__ = () + __slots__: typing.Sequence[str] = () def __init__(self, url: str, headers: data_binding.Headers, raw_body: typing.AnyStr) -> None: status = http.HTTPStatus.BAD_REQUEST @@ -259,7 +259,7 @@ class Unauthorized(ClientHTTPErrorResponse): The body that was received. """ - __slots__ = () + __slots__: typing.Sequence[str] = () def __init__(self, url: str, headers: data_binding.Headers, raw_body: typing.AnyStr) -> None: status = http.HTTPStatus.UNAUTHORIZED @@ -283,7 +283,7 @@ class Forbidden(ClientHTTPErrorResponse): The body that was received. """ - __slots__ = () + __slots__: typing.Sequence[str] = () def __init__(self, url: str, headers: data_binding.Headers, raw_body: typing.AnyStr) -> None: status = http.HTTPStatus.FORBIDDEN @@ -303,7 +303,7 @@ class NotFound(ClientHTTPErrorResponse): The body that was received. """ - __slots__ = () + __slots__: typing.Sequence[str] = () def __init__(self, url: str, headers: data_binding.Headers, raw_body: typing.AnyStr) -> None: status = http.HTTPStatus.NOT_FOUND @@ -353,7 +353,7 @@ class RateLimited(ClientHTTPErrorResponse): specific request. """ - __slots__ = () + __slots__: typing.Sequence[str] = () def __init__( self, @@ -383,7 +383,7 @@ class ServerHTTPErrorResponse(HTTPErrorResponse): errors when encountered. If you get one of these, it isn't your fault! """ - __slots__ = () + __slots__: typing.Sequence[str] = () class IntentWarning(HikariWarning): @@ -392,4 +392,4 @@ class IntentWarning(HikariWarning): This is caused by your application missing certain intents. """ - __slots__ = () + __slots__: typing.Sequence[str] = () diff --git a/hikari/events/channel.py b/hikari/events/channel.py index 261a78fdad..459eb8c719 100644 --- a/hikari/events/channel.py +++ b/hikari/events/channel.py @@ -88,7 +88,7 @@ class ChannelPinsUpdateEvent(base_events.Event): when a pinned message is deleted. """ - app: rest.IRESTApp = attr.ib(default=None, repr=False, eq=False, hash=False) + app: rest.IRESTClient = attr.ib(default=None, repr=False, eq=False, hash=False) """The client application that models may use for procedures.""" guild_id: typing.Optional[snowflake.Snowflake] = attr.ib(repr=True) @@ -115,7 +115,7 @@ class WebhookUpdateEvent(base_events.Event): Sent when a webhook is updated, created or deleted in a guild. """ - app: rest.IRESTApp = attr.ib(default=None, repr=False, eq=False, hash=False) + app: rest.IRESTClient = attr.ib(default=None, repr=False, eq=False, hash=False) """The client application that models may use for procedures.""" guild_id: snowflake.Snowflake = attr.ib(repr=True) @@ -133,7 +133,7 @@ class TypingStartEvent(base_events.Event): Received when a user or bot starts "typing" in a channel. """ - app: rest.IRESTApp = attr.ib(default=None, repr=False, eq=False, hash=False) + app: rest.IRESTClient = attr.ib(default=None, repr=False, eq=False, hash=False) """The client application that models may use for procedures.""" channel_id: snowflake.Snowflake = attr.ib(repr=True) @@ -175,7 +175,7 @@ class InviteDeleteEvent(base_events.Event): Sent when an invite is deleted for a channel we can access. """ - app: rest.IRESTApp = attr.ib(default=None, repr=False, eq=False, hash=False) + app: rest.IRESTClient = attr.ib(default=None, repr=False, eq=False, hash=False) """The client application that models may use for procedures.""" channel_id: snowflake.Snowflake = attr.ib(repr=True) diff --git a/hikari/events/guild.py b/hikari/events/guild.py index cb294eba68..9f6e8c5f6e 100644 --- a/hikari/events/guild.py +++ b/hikari/events/guild.py @@ -113,7 +113,7 @@ class GuildUnavailableEvent(GuildEvent, snowflake.Unique): ) """The ID of this entity.""" - app: rest.IRESTApp = attr.ib(default=None, repr=False, eq=False, hash=False) + app: rest.IRESTClient = attr.ib(default=None, repr=False, eq=False, hash=False) """The client application that models may use for procedures.""" @@ -122,7 +122,7 @@ class GuildUnavailableEvent(GuildEvent, snowflake.Unique): class GuildBanEvent(GuildEvent, abc.ABC): """A base object that guild ban events will inherit from.""" - app: rest.IRESTApp = attr.ib(default=None, repr=False, eq=False, hash=False) + app: rest.IRESTClient = attr.ib(default=None, repr=False, eq=False, hash=False) """The client application that models may use for procedures.""" guild_id: snowflake.Snowflake = attr.ib(repr=True) @@ -149,7 +149,7 @@ class GuildBanRemoveEvent(GuildBanEvent): class GuildEmojisUpdateEvent(GuildEvent): """Represents a Guild Emoji Update gateway event.""" - app: rest.IRESTApp = attr.ib(default=None, repr=False, eq=False, hash=False) + app: rest.IRESTClient = attr.ib(default=None, repr=False, eq=False, hash=False) """The client application that models may use for procedures.""" guild_id: snowflake.Snowflake = attr.ib(repr=True) @@ -164,7 +164,7 @@ class GuildEmojisUpdateEvent(GuildEvent): class GuildIntegrationsUpdateEvent(GuildEvent): """Used to represent Guild Integration Update gateway events.""" - app: rest.IRESTApp = attr.ib(default=None, repr=False, eq=False, hash=False) + app: rest.IRESTClient = attr.ib(default=None, repr=False, eq=False, hash=False) """The client application that models may use for procedures.""" guild_id: snowflake.Snowflake = attr.ib(repr=True) @@ -182,7 +182,7 @@ class GuildMemberEvent(GuildEvent): class GuildMemberAddEvent(GuildMemberEvent): """Used to represent a Guild Member Add gateway event.""" - app: rest.IRESTApp = attr.ib(default=None, repr=False, eq=False, hash=False) + app: rest.IRESTClient = attr.ib(default=None, repr=False, eq=False, hash=False) """The client application that models may use for procedures.""" guild_id: snowflake.Snowflake = attr.ib(repr=True) # TODO: do we want to have guild_id on all members? @@ -211,7 +211,7 @@ class GuildMemberRemoveEvent(GuildMemberEvent): Sent when a member is kicked, banned or leaves a guild. """ - app: rest.IRESTApp = attr.ib(default=None, repr=False, eq=False, hash=False) + app: rest.IRESTClient = attr.ib(default=None, repr=False, eq=False, hash=False) """The client application that models may use for procedures.""" # TODO: make GuildMember event into common base class. @@ -232,7 +232,7 @@ class GuildRoleEvent(GuildEvent): class GuildRoleCreateEvent(GuildRoleEvent): """Used to represent a Guild Role Create gateway event.""" - app: rest.IRESTApp = attr.ib(default=None, repr=False, eq=False, hash=False) + app: rest.IRESTClient = attr.ib(default=None, repr=False, eq=False, hash=False) """The client application that models may use for procedures.""" guild_id: snowflake.Snowflake = attr.ib(repr=True) @@ -247,7 +247,7 @@ class GuildRoleCreateEvent(GuildRoleEvent): class GuildRoleUpdateEvent(GuildRoleEvent): """Used to represent a Guild Role Create gateway event.""" - app: rest.IRESTApp = attr.ib(default=None, repr=False, eq=False, hash=False) + app: rest.IRESTClient = attr.ib(default=None, repr=False, eq=False, hash=False) """The client application that models may use for procedures.""" # TODO: make any event with a guild ID into a custom base event. @@ -264,7 +264,7 @@ class GuildRoleUpdateEvent(GuildRoleEvent): class GuildRoleDeleteEvent(GuildRoleEvent): """Represents a gateway Guild Role Delete Event.""" - app: rest.IRESTApp = attr.ib(default=None, repr=False, eq=False, hash=False) + app: rest.IRESTClient = attr.ib(default=None, repr=False, eq=False, hash=False) """The client application that models may use for procedures.""" guild_id: snowflake.Snowflake = attr.ib(repr=True) diff --git a/hikari/events/message.py b/hikari/events/message.py index be0c277eeb..b7bdcd0245 100644 --- a/hikari/events/message.py +++ b/hikari/events/message.py @@ -82,7 +82,7 @@ class UpdatedMessageFields(snowflake.Unique): ) """The ID of this entity.""" - app: rest.IRESTApp = attr.ib(default=None, repr=False, eq=False, hash=False) + app: rest.IRESTClient = attr.ib(default=None, repr=False, eq=False, hash=False) """The client application that models may use for procedures.""" channel_id: snowflake.Snowflake = attr.ib(repr=True) @@ -192,7 +192,7 @@ class MessageDeleteEvent(base_events.Event): Sent when a message is deleted in a channel we have access to. """ - app: rest.IRESTApp = attr.ib(default=None, repr=False, eq=False, hash=False) + app: rest.IRESTClient = attr.ib(default=None, repr=False, eq=False, hash=False) """The client application that models may use for procedures.""" # TODO: common base class for Message events. @@ -219,7 +219,7 @@ class MessageDeleteBulkEvent(base_events.Event): Sent when multiple messages are deleted in a channel at once. """ - app: rest.IRESTApp = attr.ib(default=None, repr=False, eq=False, hash=False) + app: rest.IRESTClient = attr.ib(default=None, repr=False, eq=False, hash=False) """The client application that models may use for procedures.""" channel_id: snowflake.Snowflake = attr.ib(repr=True) @@ -238,7 +238,7 @@ class MessageDeleteBulkEvent(base_events.Event): class MessageReactionEvent(base_events.Event): """A base class that all message reaction events will inherit from.""" - app: rest.IRESTApp = attr.ib(default=None, repr=False, eq=False, hash=False) + app: rest.IRESTClient = attr.ib(default=None, repr=False, eq=False, hash=False) """The client application that models may use for procedures.""" channel_id: snowflake.Snowflake = attr.ib(repr=True) diff --git a/hikari/impl/bot.py b/hikari/impl/bot.py index 5a7893ad84..9157ce635f 100644 --- a/hikari/impl/bot.py +++ b/hikari/impl/bot.py @@ -36,6 +36,7 @@ from hikari.impl import gateway_zookeeper from hikari.models import presences from hikari.net import http_settings as http_settings_ +from hikari.net import rate_limits from hikari.net import rest from hikari.utilities import undefined @@ -188,10 +189,13 @@ def __init__( self._config = config self._event_manager = event_manager.EventManagerImpl(app=self) self._entity_factory = entity_factory_impl.EntityFactoryComponentImpl(app=self) + self._global_ratelimit = rate_limits.ManualRateLimiter() + self._rest = rest.REST( # noqa: S106 - Possible hardcoded password app=self, config=config, debug=debug, + global_ratelimit=self._global_ratelimit, token=token, token_type="Bot", rest_url=rest_url, @@ -273,11 +277,13 @@ def dispatch(self, event: base_events.Event) -> asyncio.Future[typing.Any]: async def close(self) -> None: await super().close() await self._rest.close() + self._global_ratelimit.close() async def fetch_sharding_settings(self) -> gateway_models.GatewayBot: return await self.rest.fetch_gateway_bot() - def __print_banner(self) -> None: + @staticmethod + def __print_banner() -> None: from hikari import _about version = _about.__version__ diff --git a/hikari/impl/cache.py b/hikari/impl/cache.py index 09165c87b1..c583330218 100644 --- a/hikari/impl/cache.py +++ b/hikari/impl/cache.py @@ -32,10 +32,10 @@ class InMemoryCacheComponentImpl(cache.ICacheComponent): """In-memory cache implementation.""" - def __init__(self, app: rest.IRESTApp) -> None: + def __init__(self, app: rest.IRESTClient) -> None: self._app = app @property @typing.final - def app(self) -> rest.IRESTApp: + def app(self) -> rest.IRESTClient: return self._app diff --git a/hikari/impl/entity_factory.py b/hikari/impl/entity_factory.py index e1236fbec8..d5fc957f41 100644 --- a/hikari/impl/entity_factory.py +++ b/hikari/impl/entity_factory.py @@ -78,7 +78,7 @@ class EntityFactoryComponentImpl(entity_factory.IEntityFactoryComponent): This will convert objects to/from JSON compatible representations. """ - def __init__(self, app: rest.IRESTApp) -> None: + def __init__(self, app: rest.IRESTClient) -> None: self._app = app self._audit_log_entry_converters: typing.Mapping[str, typing.Callable[[typing.Any], typing.Any]] = { audit_log_models.AuditLogChangeKey.OWNER_ID: snowflake.Snowflake, @@ -140,7 +140,7 @@ def __init__(self, app: rest.IRESTApp) -> None: @property @typing.final - def app(self) -> rest.IRESTApp: + def app(self) -> rest.IRESTClient: return self._app ###################### diff --git a/hikari/impl/event_manager_core.py b/hikari/impl/event_manager_core.py index 33bfd75f5d..e3567320c0 100644 --- a/hikari/impl/event_manager_core.py +++ b/hikari/impl/event_manager_core.py @@ -61,14 +61,14 @@ class EventManagerCoreComponent(event_dispatcher.IEventDispatcherComponent, even is the raw event name being dispatched in lower-case. """ - def __init__(self, app: rest.IRESTApp) -> None: + def __init__(self, app: rest.IRESTClient) -> None: self._app = app self._listeners: ListenerMapT = {} self._waiters: WaiterMapT = {} @property @typing.final - def app(self) -> rest.IRESTApp: + def app(self) -> rest.IRESTClient: return self._app async def consume_raw_event( diff --git a/hikari/impl/rest.py b/hikari/impl/rest.py index ae6599ed5c..31e003e026 100644 --- a/hikari/impl/rest.py +++ b/hikari/impl/rest.py @@ -23,26 +23,33 @@ from __future__ import annotations -__all__: typing.Final[typing.List[str]] = ["RESTAppImpl"] +__all__: typing.Final[typing.List[str]] = ["RESTClientFactoryImpl", "RESTClientImpl"] +import copy import typing + from concurrent import futures +import aiohttp + from hikari.api import rest as rest_api -from hikari.impl import cache as cache_impl from hikari.impl import entity_factory as entity_factory_impl from hikari.net import http_settings as http_settings_ +from hikari.net import rate_limits from hikari.net import rest as rest_component +from hikari.net import strings from hikari.utilities import reflect from hikari.utilities import undefined if typing.TYPE_CHECKING: + import types + from hikari.api import cache as cache_ from hikari.api import entity_factory as entity_factory_ -class RESTAppImpl(rest_api.IRESTApp): - """Application that only provides RESTful functionality. +class RESTClientImpl(rest_api.IRESTClientContextManager): + """Client for a specific set of credentials within a REST-only application. Parameters ---------- @@ -72,26 +79,33 @@ class RESTAppImpl(rest_api.IRESTApp): def __init__( self, - config: typing.Union[undefined.UndefinedType, http_settings_.HTTPSettings] = undefined.UNDEFINED, + *, + config: http_settings_.HTTPSettings, debug: bool = False, + global_ratelimit: rate_limits.ManualRateLimiter, token: typing.Union[undefined.UndefinedType, str] = undefined.UNDEFINED, token_type: typing.Union[undefined.UndefinedType, str] = undefined.UNDEFINED, url: typing.Union[undefined.UndefinedType, str] = undefined.UNDEFINED, version: int = 6, ) -> None: self._logger = reflect.get_logger(self) - - config = http_settings_.HTTPSettings() if config is undefined.UNDEFINED else config - + self._cache: cache_.ICacheComponent = NotImplemented + self._entity_factory = entity_factory_impl.EntityFactoryComponentImpl(self) + self._executor = None self._rest = rest_component.REST( - app=self, config=config, debug=debug, token=token, token_type=token_type, rest_url=url, version=version, + app=self, + config=config, + debug=debug, + global_ratelimit=global_ratelimit, + token=token, + token_type=token_type, + rest_url=url, + version=version, ) - self._cache = cache_impl.InMemoryCacheComponentImpl(self) - self._entity_factory = entity_factory_impl.EntityFactoryComponentImpl(self) @property def executor(self) -> typing.Optional[futures.Executor]: - return None + return self._executor @property def rest(self) -> rest_component.REST: @@ -99,6 +113,11 @@ def rest(self) -> rest_component.REST: @property def cache(self) -> cache_.ICacheComponent: + """Return the cache component. + + !!! warn + This will always return `NotImplemented` for REST-only applications. + """ return self._cache @property @@ -107,3 +126,96 @@ def entity_factory(self) -> entity_factory_.IEntityFactoryComponent: async def close(self) -> None: await self._rest.close() + + async def __aenter__(self) -> rest_api.IRESTClientContextManager: + return self + + async def __aexit__( + self, + exc_type: typing.Optional[typing.Type[BaseException]], + exc_val: typing.Optional[BaseException], + exc_tb: typing.Optional[types.TracebackType], + ) -> None: + await self.close() + + +class RESTClientFactoryImpl(rest_api.IRESTClientFactory): + """The base for a REST-only Discord application. + + This comprises of a shared TCP connector connection pool, and can have + `hikari.api.rest.IRESTClient` instances for specific credentials acquired + from it. + + Parameters + ---------- + config : hikari.net.http_settings.HTTPSettings or hikari.utilities.undefined.UndefinedType + The config to use for HTTP settings. If `undefined`, then defaults are + used instead. + debug : bool + If `True`, then much more information is logged each time a request is + made. Generally you do not need this to be on, so it will default to + `False` instead. + url : str or hikari.utilities.undefined.UndefinedType + The base URL for the API. You can generally leave this as being + `undefined` and the correct default API base URL will be generated. + version : int + The Discord API version to use. Can be `6` (stable, default), or `7` + (undocumented development release). + """ + + def __init__( + self, + config: typing.Union[undefined.UndefinedType, http_settings_.HTTPSettings] = undefined.UNDEFINED, + debug: bool = False, + url: typing.Union[undefined.UndefinedType, str] = undefined.UNDEFINED, + version: int = 6, + ) -> None: + config = http_settings_.HTTPSettings() if config is undefined.UNDEFINED else config + + # Copy this, since we mutate the connector attribute on it in some cases. + self._config = copy.copy(config) + self._debug = debug + self._global_ratelimit = rate_limits.ManualRateLimiter() + self._url = url + self._version = version + + # We should use a shared connector between clients, since we want to share + # the connection pool, so tweak the defaults a little bit to achieve this. + # I should probably separate this option out eventually. + if self._config.connector_owner is True or self._config.tcp_connector is None: + self._config.connector_owner = False + self._connector_owner = True + else: + self._connector_owner = False + + self._tcp_connector = ( + aiohttp.TCPConnector() if self._config.tcp_connector is None else self._config.tcp_connector + ) + self._config.tcp_connector = self._tcp_connector + + def acquire(self, token: str, token_type: str = strings.BEARER_TOKEN) -> rest_api.IRESTClientContextManager: + return RESTClientImpl( + config=self._config, + debug=self._debug, + global_ratelimit=self._global_ratelimit, + token=token, + token_type=token_type, + url=self._url, + version=self._version, + ) + + async def close(self) -> None: + if self._connector_owner: + await self._tcp_connector.close() + self._global_ratelimit.close() + + async def __aenter__(self) -> RESTClientFactoryImpl: + return self + + async def __aexit__( + self, + exc_type: typing.Optional[typing.Type[BaseException]], + exc_val: typing.Optional[BaseException], + exc_tb: typing.Optional[types.TracebackType], + ) -> None: + await self.close() diff --git a/hikari/models/applications.py b/hikari/models/applications.py index 0ca1a73b6d..d2baaf6970 100644 --- a/hikari/models/applications.py +++ b/hikari/models/applications.py @@ -252,7 +252,7 @@ class TeamMembershipState(int, enum.Enum): class TeamMember: """Represents a member of a Team.""" - app: rest.IRESTApp = attr.ib(default=None, repr=False, eq=False, hash=False) + app: rest.IRESTClient = attr.ib(default=None, repr=False, eq=False, hash=False) """The client application that models may use for procedures.""" membership_state: TeamMembershipState = attr.ib(eq=False, hash=False, repr=False) @@ -276,7 +276,7 @@ class TeamMember: class Team(snowflake.Unique): """Represents a development team, along with all its members.""" - app: rest.IRESTApp = attr.ib(default=None, repr=False, eq=False, hash=False) + app: rest.IRESTClient = attr.ib(default=None, repr=False, eq=False, hash=False) """The client application that models may use for procedures.""" id: snowflake.Snowflake = attr.ib( @@ -344,7 +344,7 @@ def format_icon(self, *, format_: str = "png", size: int = 4096) -> typing.Optio class Application(snowflake.Unique): """Represents the information of an Oauth2 Application.""" - app: rest.IRESTApp = attr.ib(default=None, repr=False, eq=False, hash=False) + app: rest.IRESTClient = attr.ib(default=None, repr=False, eq=False, hash=False) """The client application that models may use for procedures.""" id: snowflake.Snowflake = attr.ib( diff --git a/hikari/models/audit_logs.py b/hikari/models/audit_logs.py index b528f12e5e..62fc921e1b 100644 --- a/hikari/models/audit_logs.py +++ b/hikari/models/audit_logs.py @@ -284,7 +284,7 @@ def __init__(self, payload: typing.Mapping[str, str]) -> None: class AuditLogEntry(snowflake.Unique): """Represents an entry in a guild's audit log.""" - app: rest.IRESTApp = attr.ib(default=None, repr=False, eq=False, hash=False) + app: rest.IRESTClient = attr.ib(default=None, repr=False, eq=False, hash=False) """The client application that models may use for procedures.""" id: snowflake.Snowflake = attr.ib( diff --git a/hikari/models/channels.py b/hikari/models/channels.py index e80c985b54..c1239b3320 100644 --- a/hikari/models/channels.py +++ b/hikari/models/channels.py @@ -159,7 +159,7 @@ class PartialChannel(snowflake.Unique): ) """The ID of this entity.""" - app: rest.IRESTApp = attr.ib(default=None, repr=False, eq=False, hash=False) + app: rest.IRESTClient = attr.ib(default=None, repr=False, eq=False, hash=False) """The client application that models may use for procedures.""" name: typing.Optional[str] = attr.ib(eq=False, hash=False, repr=True) @@ -173,7 +173,7 @@ class TextChannel(PartialChannel, abc.ABC): """A channel that can have text messages in it.""" # This is a mixin, do not add slotted fields. - __slots__ = () + __slots__: typing.Sequence[str] = () @attr.s(eq=True, hash=True, init=False, kw_only=True, slots=True) diff --git a/hikari/models/colors.py b/hikari/models/colors.py index e8f3f4d41f..c47b8c14ef 100644 --- a/hikari/models/colors.py +++ b/hikari/models/colors.py @@ -122,7 +122,7 @@ class Color(int): ``` """ - __slots__ = () + __slots__: typing.Sequence[str] = () def __init__(self, raw_rgb: typing.SupportsInt) -> None: if not (0 <= int(raw_rgb) <= 0xFFFFFF): diff --git a/hikari/models/embeds.py b/hikari/models/embeds.py index 0bf9fb4440..b19aea70bd 100644 --- a/hikari/models/embeds.py +++ b/hikari/models/embeds.py @@ -47,7 +47,7 @@ def _maybe_color(value: typing.Optional[colors.ColorLike]) -> typing.Optional[co class _TruthyEmbedComponentMixin: - __slots__ = () + __slots__: typing.Sequence[str] = () __attrs_attrs__: typing.ClassVar[typing.Tuple[attr.Attribute, ...]] diff --git a/hikari/models/emojis.py b/hikari/models/emojis.py index e70d19c692..0f5a2e992e 100644 --- a/hikari/models/emojis.py +++ b/hikari/models/emojis.py @@ -213,7 +213,7 @@ class CustomEmoji(snowflake.Unique, Emoji): https://github.com/discord/discord-api-docs/issues/1614#issuecomment-628548913 """ - app: rest.IRESTApp = attr.ib(default=None, repr=False, eq=False, hash=False, init=True) + app: rest.IRESTClient = attr.ib(default=None, repr=False, eq=False, hash=False, init=True) """The client application that models may use for procedures.""" id: snowflake.Snowflake = attr.ib( diff --git a/hikari/models/guilds.py b/hikari/models/guilds.py index 7e9ff7adcf..e528b6ef66 100644 --- a/hikari/models/guilds.py +++ b/hikari/models/guilds.py @@ -215,7 +215,7 @@ class GuildVerificationLevel(int, enum.Enum): class GuildWidget: """Represents a guild embed.""" - app: rest.IRESTApp = attr.ib(default=None, repr=False, eq=False, hash=False) + app: rest.IRESTClient = attr.ib(default=None, repr=False, eq=False, hash=False) """The client application that models may use for procedures.""" channel_id: typing.Optional[snowflake.Snowflake] = attr.ib(repr=True) @@ -229,7 +229,7 @@ class GuildWidget: class Member: """Used to represent a guild bound member.""" - app: rest.IRESTApp = attr.ib(default=None, repr=False, eq=False, hash=False) + app: rest.IRESTClient = attr.ib(default=None, repr=False, eq=False, hash=False) """The client application that models may use for procedures.""" # TODO: make Member delegate to user and implement a common base class @@ -286,7 +286,7 @@ class PartialRole(snowflake.Unique): ) """The ID of this entity.""" - app: rest.IRESTApp = attr.ib(default=None, repr=False, eq=False, hash=False) + app: rest.IRESTClient = attr.ib(default=None, repr=False, eq=False, hash=False) """The client application that models may use for procedures.""" name: str = attr.ib(eq=False, hash=False, repr=True) @@ -446,7 +446,7 @@ class PartialGuild(snowflake.Unique): ) """The ID of this entity.""" - app: rest.IRESTApp = attr.ib(default=None, repr=False, eq=False, hash=False) + app: rest.IRESTClient = attr.ib(default=None, repr=False, eq=False, hash=False) """The client application that models may use for procedures.""" name: str = attr.ib(eq=False, hash=False, repr=True) diff --git a/hikari/models/invites.py b/hikari/models/invites.py index a0349eb2d5..8f6a0b3a90 100644 --- a/hikari/models/invites.py +++ b/hikari/models/invites.py @@ -52,7 +52,7 @@ class TargetUserType(int, enum.Enum): class VanityURL: """A special case invite object, that represents a guild's vanity url.""" - app: rest.IRESTApp = attr.ib(default=None, repr=False, eq=False, hash=False) + app: rest.IRESTClient = attr.ib(default=None, repr=False, eq=False, hash=False) """The client application that models may use for procedures.""" code: str = attr.ib(eq=True, hash=True, repr=True) @@ -162,7 +162,7 @@ def format_banner(self, *, format_: str = "png", size: int = 4096) -> typing.Opt class Invite: """Represents an invite that's used to add users to a guild or group dm.""" - app: rest.IRESTApp = attr.ib(default=None, repr=False, eq=False, hash=False) + app: rest.IRESTClient = attr.ib(default=None, repr=False, eq=False, hash=False) """The client application that models may use for procedures.""" code: str = attr.ib(eq=True, hash=True, repr=True) diff --git a/hikari/models/messages.py b/hikari/models/messages.py index 1afc589477..793ccc056a 100644 --- a/hikari/models/messages.py +++ b/hikari/models/messages.py @@ -206,7 +206,7 @@ class MessageCrosspost: "published" to another. """ - app: rest.IRESTApp = attr.ib(default=None, repr=False, eq=False, hash=False) + app: rest.IRESTClient = attr.ib(default=None, repr=False, eq=False, hash=False) """The client application that models may use for procedures.""" id: typing.Optional[snowflake.Snowflake] = attr.ib(repr=True) @@ -235,7 +235,7 @@ class MessageCrosspost: class Message(snowflake.Unique): """Represents a message.""" - app: rest.IRESTApp = attr.ib(default=None, repr=False, eq=False, hash=False) + app: rest.IRESTClient = attr.ib(default=None, repr=False, eq=False, hash=False) """The client application that models may use for procedures.""" id: snowflake.Snowflake = attr.ib( diff --git a/hikari/models/presences.py b/hikari/models/presences.py index 10e8e7cec3..9aff7fc7ef 100644 --- a/hikari/models/presences.py +++ b/hikari/models/presences.py @@ -268,7 +268,7 @@ class ClientStatus: class MemberPresence: """Used to represent a guild member's presence.""" - app: rest.IRESTApp = attr.ib(default=None, repr=False, eq=False, hash=False) + app: rest.IRESTClient = attr.ib(default=None, repr=False, eq=False, hash=False) """The client application that models may use for procedures.""" user: users.PartialUser = attr.ib(eq=True, hash=True, repr=True) diff --git a/hikari/models/users.py b/hikari/models/users.py index 7381005c43..922fb6ce5d 100644 --- a/hikari/models/users.py +++ b/hikari/models/users.py @@ -111,7 +111,7 @@ class PartialUser(snowflake.Unique): ) """The ID of this entity.""" - app: rest.IRESTApp = attr.ib(default=None, repr=False, eq=False, hash=False) + app: rest.IRESTClient = attr.ib(default=None, repr=False, eq=False, hash=False) """The client application that models may use for procedures.""" discriminator: typing.Union[str, undefined.UndefinedType] = attr.ib(eq=False, hash=False, repr=True) diff --git a/hikari/models/voices.py b/hikari/models/voices.py index 7443497d69..4d95cfab77 100644 --- a/hikari/models/voices.py +++ b/hikari/models/voices.py @@ -35,7 +35,7 @@ class VoiceState: """Represents a user's voice connection status.""" - app: rest.IRESTApp = attr.ib(default=None, repr=False, eq=False, hash=False) + app: rest.IRESTClient = attr.ib(default=None, repr=False, eq=False, hash=False) """The client application that models may use for procedures.""" guild_id: typing.Optional[snowflake.Snowflake] = attr.ib(eq=False, hash=False, repr=True) diff --git a/hikari/models/webhooks.py b/hikari/models/webhooks.py index 6605cd7376..17fc1a5e0c 100644 --- a/hikari/models/webhooks.py +++ b/hikari/models/webhooks.py @@ -61,7 +61,7 @@ class Webhook(snowflake.Unique): send informational messages to specific channels. """ - app: rest.IRESTApp = attr.ib(default=None, repr=False, eq=False, hash=False) + app: rest.IRESTClient = attr.ib(default=None, repr=False, eq=False, hash=False) """The client application that models may use for procedures.""" id: snowflake.Snowflake = attr.ib( diff --git a/hikari/net/buckets.py b/hikari/net/buckets.py index b3b4663e70..956b3ef259 100644 --- a/hikari/net/buckets.py +++ b/hikari/net/buckets.py @@ -245,7 +245,7 @@ class RESTBucket(rate_limits.WindowedBurstRateLimiter): which allows dynamically changing the enforced rate limits at any time. """ - __slots__ = ("compiled_route",) + __slots__: typing.Sequence[str] = ("compiled_route",) compiled_route: typing.Final[routes.CompiledRoute] """The compiled _route that this rate limit is covering.""" @@ -319,7 +319,7 @@ class RESTBucketManager: _POLL_PERIOD: typing.Final[typing.ClassVar[int]] = 20 _EXPIRE_PERIOD: typing.Final[typing.ClassVar[int]] = 10 - __slots__ = ( + __slots__: typing.Sequence[str] = ( "routes_to_hashes", "real_hashes_to_buckets", "closed_event", diff --git a/hikari/net/gateway.py b/hikari/net/gateway.py index 9528acbf8a..1c73f629c3 100644 --- a/hikari/net/gateway.py +++ b/hikari/net/gateway.py @@ -152,11 +152,11 @@ class _GatewayOpcode(enum.IntEnum): @typing.final class _Reconnect(RuntimeError): - __slots__ = () + __slots__: typing.Sequence[str] = () @typing.final class _SocketClosed(RuntimeError): - __slots__ = () + __slots__: typing.Sequence[str] = () @attr.s(auto_attribs=True, slots=True) class _InvalidSession(RuntimeError): @@ -277,7 +277,7 @@ async def _run(self) -> None: """Start the shard and wait for it to shut down.""" try: # This may be set if we are stuck in a reconnect loop. - while not self._request_close_event.is_set() and await self._run_once(): + while not self._request_close_event.is_set() and await self._run_once_shielded(): pass # Allow zookeepers to stop gathering tasks for each shard. @@ -291,63 +291,12 @@ async def _run(self) -> None: # the entire inheritance conduit in a patch context. await http_client.HTTPClient.close(self) - async def _run_once(self) -> bool: # noqa: C901, CFQ001 - Too complex, exceeds max allowed length - # returns `True` if we can reconnect, or `False` otherwise. - self._request_close_event.clear() - - self._zombied = False - - if self._now() - self._last_run_started_at < self._RESTART_RATELIMIT_WINDOW: - # Interrupt sleep immediately if a request to close is fired. - wait_task = asyncio.create_task( - self._request_close_event.wait(), name=f"gateway shard {self._shard_id} backing off" - ) - try: - backoff = next(self._backoff) - self._logger.debug("backing off for %ss", backoff) - await asyncio.wait_for(wait_task, timeout=backoff) - - # If this line gets reached, the wait didn't time out, meaning - # the user told the client to shut down gracefully before the - # backoff completed. - return False - except asyncio.TimeoutError: - pass - - # Do this after; it prevents backing off on the first try. - self._last_run_started_at = self._now() - + async def _run_once_shielded(self) -> bool: + # Returns `True` if we can reconnect, or `False` otherwise. + # Wraps the runner logic in the standard exception handling mechanisms. try: - self._logger.debug("creating websocket connection to %s", self.url) - self._ws = await self._create_ws(self.url) - - self.connected_at = self._now() - - # Technically we are connected after the hello, but this ensures we can send and receive - # before firing that event. - self._dispatch("CONNECTED", {}) - - self._zlib = zlib.decompressobj() - - self._handshake_event.clear() - self._request_close_event.clear() - - await self._handshake() - - # We should ideally set this after HELLO, but it should be fine - # here as well. If we don't heartbeat in time, something probably - # went majorly wrong anyway. - heartbeat = asyncio.create_task( - self._heartbeat_keepalive(), name=f"gateway shard {self._shard_id} heartbeat" - ) - - try: - await self._poll_events() - finally: - heartbeat.cancel() - + await self._run_once() return False - except aiohttp.ClientConnectorError as ex: self._logger.error( "failed to connect to Discord because %s.%s: %s", type(ex).__module__, type(ex).__qualname__, str(ex), @@ -397,6 +346,63 @@ async def _run_once(self) -> bool: # noqa: C901, CFQ001 - Too complex, exceeds await self._close_ws(self._GatewayCloseCode.RFC_6455_UNEXPECTED_CONDITION, "unexpected error occurred") raise + return True + + async def _run_once(self) -> None: + # Physical runner logic without error handling. + self._request_close_event.clear() + + self._zombied = False + + if self._now() - self._last_run_started_at < self._RESTART_RATELIMIT_WINDOW: + # Interrupt sleep immediately if a request to close is fired. + wait_task = asyncio.create_task( + self._request_close_event.wait(), name=f"gateway shard {self._shard_id} backing off" + ) + try: + backoff = next(self._backoff) + self._logger.debug("backing off for %ss", backoff) + await asyncio.wait_for(wait_task, timeout=backoff) + + # If this line gets reached, the wait didn't time out, meaning + # the user told the client to shut down gracefully before the + # backoff completed. + return + except asyncio.TimeoutError: + pass + + # Do this after; it prevents backing off on the first try. + self._last_run_started_at = self._now() + + self._logger.debug("creating websocket connection to %s", self.url) + self._ws = await self._create_ws(self.url) + + self.connected_at = self._now() + + # Technically we are connected after the hello, but this ensures we can send and receive + # before firing that event. + self._dispatch("CONNECTED", {}) + + try: + + self._zlib = zlib.decompressobj() + + self._handshake_event.clear() + self._request_close_event.clear() + + await self._handshake() + + # We should ideally set this after HELLO, but it should be fine + # here as well. If we don't heartbeat in time, something probably + # went majorly wrong anyway. + heartbeat = asyncio.create_task( + self._heartbeat_keepalive(), name=f"gateway shard {self._shard_id} heartbeat" + ) + + try: + await self._poll_events() + finally: + heartbeat.cancel() finally: if not math.isnan(self.connected_at): # Only dispatch this if we actually connected before we failed! @@ -404,8 +410,6 @@ async def _run_once(self) -> bool: # noqa: C901, CFQ001 - Too complex, exceeds self.connected_at = float("nan") - return True - async def update_presence( self, *, diff --git a/hikari/net/http_client.py b/hikari/net/http_client.py index 0ae4867637..19d61e38ad 100644 --- a/hikari/net/http_client.py +++ b/hikari/net/http_client.py @@ -75,7 +75,7 @@ class HTTPClient(abc.ABC): _logger on this class. """ - __slots__ = ( + __slots__: typing.Sequence[str] = ( "_client_session", "_client_session_ref", "_config", @@ -139,12 +139,13 @@ def get_client_session(self) -> aiohttp.ClientSession: The client session to use for requests. """ if self._client_session is None: - connector = self._config.tcp_connector_factory() if self._config.tcp_connector_factory is not None else None + connector = self._config.tcp_connector if self._config.tcp_connector is not None else None self._client_session = aiohttp.ClientSession( connector=connector, trust_env=self._config.trust_env, version=aiohttp.HttpVersion11, json_serialize=json.dumps, + connector_owner=self._config.connector_owner if self._config.tcp_connector is not None else True, ) self._client_session_ref = weakref.proxy(self._client_session) _LOGGER.debug("acquired new client session object %r", self._client_session) diff --git a/hikari/net/http_settings.py b/hikari/net/http_settings.py index 3c6d6a4769..f667cf00f4 100644 --- a/hikari/net/http_settings.py +++ b/hikari/net/http_settings.py @@ -41,6 +41,19 @@ class HTTPSettings: Generally you do not want to enable this unless you have a good reason to. """ + connector_owner: bool = True + """Determines whether objects take ownership of their connectors. + + If `True`, the component consuming any connector will close the + connector when closed. + + If you set this to `False`, and you provide a `tcp_connector_factory`, + this will prevent the connector being closed by each component. + + Note that unless you provide a `tcp_connector_factory`, this will be + ignored. + """ + proxy_auth: typing.Optional[aiohttp.BasicAuth] = None """Optional proxy authorization to provide in any HTTP requests.""" @@ -64,9 +77,12 @@ class HTTPSettings: ssl_context: typing.Optional[ssl.SSLContext] = None """The optional SSL context to use.""" - tcp_connector_factory: typing.Optional[typing.Callable[[], aiohttp.TCPConnector]] = None - """An optional TCP connector factory to use. A connector will be created - for each component (each shard, and each REST instance). + tcp_connector: typing.Optional[aiohttp.TCPConnector] = None + """An optional TCP connector to use. + + The client session will default to closing this connector on close unless + you set the `connector_owner` to `False`. If you are planning to share + the connector between clients, you should set that to `False`. """ trust_env: bool = False diff --git a/hikari/net/iterators.py b/hikari/net/iterators.py index 1009480780..3d5fa509fa 100644 --- a/hikari/net/iterators.py +++ b/hikari/net/iterators.py @@ -43,7 +43,7 @@ class _AllConditions(typing.Generic[ValueT]): - __slots__ = ("conditions",) + __slots__: typing.Sequence[str] = ("conditions",) def __init__(self, conditions: typing.Collection[typing.Callable[[ValueT], bool]]) -> None: self.conditions = conditions @@ -53,7 +53,7 @@ def __call__(self, item: ValueT) -> bool: class _AttrComparator(typing.Generic[ValueT]): - __slots__ = ("getter", "expected_value") + __slots__: typing.Sequence[str] = ("getter", "expected_value") def __init__(self, attr_name: str, expected_value: typing.Any) -> None: if attr_name.startswith("."): @@ -113,7 +113,7 @@ class LazyIterator(typing.Generic[ValueT], abc.ABC): ... process(item) """ - __slots__ = () + __slots__: typing.Sequence[str] = () def map( self, transformation: typing.Union[typing.Callable[[ValueT], AnotherValueT], str], @@ -271,7 +271,7 @@ async def __anext__(self) -> ValueT: class _EnumeratedLazyIterator(typing.Generic[ValueT], LazyIterator[typing.Tuple[int, ValueT]]): - __slots__ = ("_i", "_paginator") + __slots__: typing.Sequence[str] = ("_i", "_paginator") def __init__(self, paginator: LazyIterator[ValueT], *, start: int) -> None: self._i = start @@ -284,7 +284,7 @@ async def __anext__(self) -> typing.Tuple[int, ValueT]: class _LimitedLazyIterator(typing.Generic[ValueT], LazyIterator[ValueT]): - __slots__ = ("_paginator", "_count", "_limit") + __slots__: typing.Sequence[str] = ("_paginator", "_count", "_limit") def __init__(self, paginator: LazyIterator[ValueT], limit: int) -> None: if limit <= 0: @@ -303,7 +303,7 @@ async def __anext__(self) -> ValueT: class _FilteredLazyIterator(typing.Generic[ValueT], LazyIterator[ValueT]): - __slots__ = ("_paginator", "_predicate") + __slots__: typing.Sequence[str] = ("_paginator", "_predicate") def __init__(self, paginator: LazyIterator[ValueT], predicate: typing.Callable[[ValueT], bool]) -> None: self._paginator = paginator @@ -317,7 +317,7 @@ async def __anext__(self) -> ValueT: class _MappingLazyIterator(typing.Generic[AnotherValueT, ValueT], LazyIterator[ValueT]): - __slots__ = ("_paginator", "_transformation") + __slots__: typing.Sequence[str] = ("_paginator", "_transformation") def __init__( self, paginator: LazyIterator[AnotherValueT], transformation: typing.Callable[[AnotherValueT], ValueT], @@ -330,7 +330,7 @@ async def __anext__(self) -> ValueT: class _BufferedLazyIterator(typing.Generic[ValueT], LazyIterator[ValueT]): - __slots__ = ("_buffer",) + __slots__: typing.Sequence[str] = ("_buffer",) def __init__(self) -> None: empty_genexp = typing.cast(typing.Generator[ValueT, None, None], (_ for _ in ())) @@ -362,11 +362,11 @@ async def __anext__(self) -> ValueT: class MessageIterator(_BufferedLazyIterator["messages.Message"]): """Implementation of an iterator for message history.""" - __slots__ = ("_app", "_request_call", "_direction", "_first_id", "_route") + __slots__: typing.Sequence[str] = ("_app", "_request_call", "_direction", "_first_id", "_route") def __init__( self, - app: rest.IRESTApp, + app: rest.IRESTClient, request_call: typing.Callable[ ..., typing.Coroutine[None, None, typing.Union[None, data_binding.JSONObject, data_binding.JSONArray]] ], @@ -404,11 +404,11 @@ async def _next_chunk(self) -> typing.Optional[typing.Generator[messages.Message class ReactorIterator(_BufferedLazyIterator["users.User"]): """Implementation of an iterator for message reactions.""" - __slots__ = ("_app", "_first_id", "_route", "_request_call") + __slots__: typing.Sequence[str] = ("_app", "_first_id", "_route", "_request_call") def __init__( self, - app: rest.IRESTApp, + app: rest.IRESTClient, request_call: typing.Callable[ ..., typing.Coroutine[None, None, typing.Union[None, data_binding.JSONObject, data_binding.JSONArray]] ], @@ -443,11 +443,11 @@ async def _next_chunk(self) -> typing.Optional[typing.Generator[users.User, typi class OwnGuildIterator(_BufferedLazyIterator["applications.OwnGuild"]): """Implementation of an iterator for retrieving guilds you are in.""" - __slots__ = ("_app", "_request_call", "_route", "_newest_first", "_first_id") + __slots__: typing.Sequence[str] = ("_app", "_request_call", "_route", "_newest_first", "_first_id") def __init__( self, - app: rest.IRESTApp, + app: rest.IRESTClient, request_call: typing.Callable[ ..., typing.Coroutine[None, None, typing.Union[None, data_binding.JSONObject, data_binding.JSONArray]] ], @@ -482,11 +482,11 @@ async def _next_chunk(self) -> typing.Optional[typing.Generator[applications.Own class MemberIterator(_BufferedLazyIterator["guilds.Member"]): """Implementation of an iterator for retrieving members in a guild.""" - __slots__ = ("_app", "_request_call", "_route", "_first_id") + __slots__: typing.Sequence[str] = ("_app", "_request_call", "_route", "_first_id") def __init__( self, - app: rest.IRESTApp, + app: rest.IRESTClient, request_call: typing.Callable[ ..., typing.Coroutine[None, None, typing.Union[None, data_binding.JSONObject, data_binding.JSONArray]] ], @@ -523,7 +523,7 @@ class AuditLogIterator(LazyIterator["audit_logs.AuditLog"]): def __init__( self, - app: rest.IRESTApp, + app: rest.IRESTClient, request_call: typing.Callable[ ..., typing.Coroutine[None, None, typing.Union[None, data_binding.JSONObject, data_binding.JSONArray]] ], diff --git a/hikari/net/rate_limits.py b/hikari/net/rate_limits.py index c74c9e94cb..ff6c0f0a74 100644 --- a/hikari/net/rate_limits.py +++ b/hikari/net/rate_limits.py @@ -53,7 +53,7 @@ class BaseRateLimiter(abc.ABC): Async context manager support is not supported and will not be supported. """ - __slots__ = () + __slots__: typing.Sequence[str] = () @abc.abstractmethod def acquire(self) -> asyncio.Future[None]: @@ -84,7 +84,7 @@ class BurstRateLimiter(BaseRateLimiter, abc.ABC): complete logic for safely aborting any pending tasks when being shut down. """ - __slots__ = ("name", "throttle_task", "queue", "_closed") + __slots__: typing.Sequence[str] = ("name", "throttle_task", "queue", "_closed") name: typing.Final[str] """The name of the rate limiter.""" @@ -166,7 +166,7 @@ class ManualRateLimiter(BurstRateLimiter): Expect random occurrences. """ - __slots__ = () + __slots__: typing.Sequence[str] = () def __init__(self) -> None: super().__init__("global") @@ -275,7 +275,7 @@ class WindowedBurstRateLimiter(BurstRateLimiter): that a unit has been placed into the bucket. """ - __slots__ = ("reset_at", "remaining", "limit", "period") + __slots__: typing.Sequence[str] = ("reset_at", "remaining", "limit", "period") reset_at: float """The `time.perf_counter` that the limit window ends at.""" @@ -443,7 +443,7 @@ class ExponentialBackOff: The initial increment to start at. Defaults to `0`. """ - __slots__ = ("base", "increment", "maximum", "jitter_multiplier") + __slots__: typing.Sequence[str] = ("base", "increment", "maximum", "jitter_multiplier") base: typing.Final[float] """The base to use. Defaults to 2.""" diff --git a/hikari/net/rest.py b/hikari/net/rest.py index 1690f1976e..3f53c646a0 100644 --- a/hikari/net/rest.py +++ b/hikari/net/rest.py @@ -81,7 +81,7 @@ class REST(http_client.HTTPClient, component.IComponent): Parameters ---------- - app : hikari.api.rest.IRESTApp + app : hikari.api.rest.IRESTClient The REST application containing all other application components that Hikari uses. config : hikari.net.http_settings.HTTPSettings @@ -93,14 +93,15 @@ class REST(http_client.HTTPClient, component.IComponent): as well as information such as DNS cache hits and misses, and other information useful for debugging this application. These logs will be written as DEBUG log entries. For most purposes, this should be - left `False`. + left `False`.eee + global_ratelimit : hikari.net.rate_limits.ManualRateLimiter + The shared ratelimiter to use for the application. token : str or hikari.utilities.undefined.UndefinedType The bot or bearer token. If no token is to be used, this can be undefined. token_type : str or hikari.utilities.undefined.UndefinedType The type of token in use. If no token is used, this can be ignored and - left to the default value. This can be `"Bot"` or `"Bearer"`. The - default if not provided will be `"Bot"`. + left to the default value. This can be `"Bot"` or `"Bearer"`. rest_url : str The REST API base URL. This can contain format-string specifiers to interpolate information such as API version in use. @@ -108,7 +109,7 @@ class REST(http_client.HTTPClient, component.IComponent): The API version to use. """ - __slots__ = ("buckets", "global_rate_limit", "version", "_app", "_rest_url", "_token") + __slots__: typing.Sequence[str] = ("buckets", "global_rate_limit", "version", "_app", "_rest_url", "_token") buckets: buckets.RESTBucketManager """Bucket ratelimiter manager.""" @@ -121,22 +122,23 @@ class REST(http_client.HTTPClient, component.IComponent): @typing.final class _RetryRequest(RuntimeError): - __slots__ = () + __slots__: typing.Sequence[str] = () def __init__( self, *, - app: rest.IRESTApp, + app: rest.IRESTClient, config: http_settings.HTTPSettings, - debug: bool = False, - token: typing.Union[undefined.UndefinedType, str] = undefined.UNDEFINED, - token_type: typing.Union[undefined.UndefinedType, str] = undefined.UNDEFINED, - rest_url: typing.Union[undefined.UndefinedType, str] = undefined.UNDEFINED, + global_ratelimit: rate_limits.ManualRateLimiter, + debug: bool, + token: typing.Union[undefined.UndefinedType, str], + token_type: typing.Union[undefined.UndefinedType, str], + rest_url: typing.Union[undefined.UndefinedType, str], version: int, ) -> None: super().__init__(config=config, debug=debug) self.buckets = buckets.RESTBucketManager() - self.global_rate_limit = rate_limits.ManualRateLimiter() + self.global_rate_limit = global_ratelimit self.version = version self._app = app @@ -158,7 +160,7 @@ def __init__( @property @typing.final - def app(self) -> rest.IRESTApp: + def app(self) -> rest.IRESTClient: return self._app @typing.final diff --git a/hikari/net/rest_utils.py b/hikari/net/rest_utils.py index 9d30d88dfe..be366a9c88 100644 --- a/hikari/net/rest_utils.py +++ b/hikari/net/rest_utils.py @@ -56,7 +56,7 @@ class TypingIndicator: the typing indicator repeatedly until the context finishes. """ - __slots__ = ("_channel", "_request_call", "_task") + __slots__: typing.Sequence[str] = ("_channel", "_request_call", "_task") def __init__( self, @@ -167,7 +167,7 @@ class GuildBuilder: """ # Required arguments. - _app: rest.IRESTApp + _app: rest.IRESTClient _name: str # Optional args that we kept hidden. diff --git a/hikari/utilities/data_binding.py b/hikari/utilities/data_binding.py index 7a4a34df5c..3071dd59f6 100644 --- a/hikari/utilities/data_binding.py +++ b/hikari/utilities/data_binding.py @@ -104,7 +104,7 @@ class StringMapBuilder(multidict.MultiDict[str]): form of validation on the type. Use the `put*` methods instead. """ - __slots__ = () + __slots__: typing.Sequence[str] = () def __init__(self) -> None: super().__init__() @@ -169,7 +169,7 @@ class JSONObjectBuilder(typing.Dict[str, JSONAny]): form of validation on the type. Use the `put*` methods instead. """ - __slots__ = () + __slots__: typing.Sequence[str] = () def __init__(self) -> None: super().__init__() diff --git a/hikari/utilities/files.py b/hikari/utilities/files.py index b1407ea889..afc08f0f91 100644 --- a/hikari/utilities/files.py +++ b/hikari/utilities/files.py @@ -370,7 +370,7 @@ def _close(fp: typing.IO[bytes]) -> None: class AsyncReaderContextManager(typing.Generic[ReaderImplT]): """Context manager that returns a reader.""" - __slots__ = () + __slots__: typing.Sequence[str] = () @abc.abstractmethod async def __aenter__(self) -> ReaderImplT: @@ -388,7 +388,7 @@ async def __aexit__( @typing.final class _NoOpAsyncReaderContextManagerImpl(typing.Generic[ReaderImplT], AsyncReaderContextManager[ReaderImplT]): - __slots__ = ("impl",) + __slots__: typing.Sequence[str] = ("impl",) def __init__(self, impl: ReaderImplT) -> None: self.impl = impl @@ -407,7 +407,7 @@ async def __aexit__( @typing.final class _WebReaderAsyncReaderContextManagerImpl(AsyncReaderContextManager[WebReader]): - __slots__ = ("_web_resource", "_head_only", "_client_response_ctx", "_client_session") + __slots__: typing.Sequence[str] = ("_web_resource", "_head_only", "_client_response_ctx", "_client_session") def __init__(self, web_resource: WebResource, head_only: bool) -> None: self._web_resource = web_resource @@ -475,7 +475,7 @@ class Resource(typing.Generic[ReaderImplT], abc.ABC): resources. """ - __slots__ = () + __slots__: typing.Sequence[str] = () @property @abc.abstractmethod @@ -551,7 +551,7 @@ class Bytes(Resource[ByteReader]): uploading. """ - __slots__ = ("data", "_filename", "mimetype", "extension") + __slots__: typing.Sequence[str] = ("data", "_filename", "mimetype", "extension") data: bytes """The raw data to upload.""" @@ -637,7 +637,7 @@ class WebResource(Resource[WebReader], abc.ABC): a `Bytes` and pass that instead in these cases. """ - __slots__ = () + __slots__: typing.Sequence[str] = () def stream( self, *, executor: typing.Optional[concurrent.futures.Executor] = None, head_only: bool = False, @@ -725,7 +725,7 @@ class URL(WebResource): a `Bytes` and pass that instead in these cases. """ - __slots__ = ("_url",) + __slots__: typing.Sequence[str] = ("_url",) def __init__(self, url: str) -> None: self._url = url @@ -761,7 +761,7 @@ class File(Resource[FileReader]): from the path instead. """ - __slots__ = ("path", "_filename") + __slots__: typing.Sequence[str] = ("path", "_filename") path: typing.Union[str, pathlib.Path] _filename: typing.Optional[str] diff --git a/hikari/utilities/snowflake.py b/hikari/utilities/snowflake.py index 65c8152af7..6061a38c61 100644 --- a/hikari/utilities/snowflake.py +++ b/hikari/utilities/snowflake.py @@ -37,7 +37,7 @@ class Snowflake(int): This object can be treated as a regular `int` for most purposes. """ - __slots__ = () + __slots__: typing.Sequence[str] = () ___MIN___: Snowflake ___MAX___: Snowflake @@ -93,7 +93,7 @@ def from_data(cls, timestamp: datetime.datetime, worker_id: int, process_id: int class Unique(abc.ABC): """Mixin for a class that enforces uniqueness by a snowflake ID.""" - __slots__ = () + __slots__: typing.Sequence[str] = () @property @abc.abstractmethod diff --git a/hikari/utilities/undefined.py b/hikari/utilities/undefined.py index a4ac8b7ffd..cc6a3b4420 100644 --- a/hikari/utilities/undefined.py +++ b/hikari/utilities/undefined.py @@ -28,7 +28,7 @@ class _UndefinedType: - __slots__ = () + __slots__: typing.Sequence[str] = () def __bool__(self) -> bool: return False diff --git a/tests/hikari/impl/test_entity_factory.py b/tests/hikari/impl/test_entity_factory.py index 76f0a2143a..6ca40aba11 100644 --- a/tests/hikari/impl/test_entity_factory.py +++ b/tests/hikari/impl/test_entity_factory.py @@ -76,8 +76,8 @@ def test__deserialize_max_age_returns_null(): class TestEntityFactoryImpl: @pytest.fixture() - def mock_app(self) -> rest.IRESTApp: - return mock.MagicMock(rest.IRESTApp) + def mock_app(self) -> rest.IRESTClient: + return mock.MagicMock(rest.IRESTClient) @pytest.fixture() def entity_factory_impl(self, mock_app) -> entity_factory.EntityFactoryComponentImpl: diff --git a/tests/hikari/net/test_gateway.py b/tests/hikari/net/test_gateway.py index 3950d772e8..2f05d614ae 100644 --- a/tests/hikari/net/test_gateway.py +++ b/tests/hikari/net/test_gateway.py @@ -182,7 +182,7 @@ async def test_websocket_not_closed_if_None(self, client, is_alive): @pytest.mark.asyncio class TestRun: @hikari_test_helpers.timeout() - async def test_repeatedly_invokes_run_once_while_request_close_event_not_set(self, client): + async def test_repeatedly_invokes_run_once_shielded_while_request_close_event_not_set(self, client): i = 0 def is_set(): @@ -196,13 +196,13 @@ def is_set(): client._request_close_event = mock.MagicMock(asyncio.Event) client._request_close_event.is_set = is_set - client._run_once = mock.AsyncMock() + client._run_once_shielded = mock.AsyncMock() with pytest.raises(errors.GatewayClientClosedError): await client._run() assert i == 5 - assert client._run_once.call_count == i + assert client._run_once_shielded.call_count == i @hikari_test_helpers.timeout() async def test_sets_handshake_event_on_finish(self, client): @@ -230,6 +230,198 @@ async def test_closes_super_on_finish(self, client): close_mock.assert_awaited_once_with(client) +@pytest.mark.asyncio +class TestRunOnceShielded: + @pytest.fixture + def client(self): + client = hikari_test_helpers.unslot_class(gateway.Gateway)( + url="wss://gateway.discord.gg", + token="lol", + app=mock.MagicMock(), + config=mock.MagicMock(), + shard_id=3, + shard_count=17, + ) + client = hikari_test_helpers.mock_methods_on( + client, + except_=( + "_run_once_shielded", + "_InvalidSession", + "_Reconnect", + "_SocketClosed", + "_dispatch", + "_now", + "_GatewayCloseCode", + "_GatewayOpcode", + ), + also_mock=["_backoff", "_handshake_event", "_request_close_event", "_logger",], + ) + client._dispatch = mock.AsyncMock() + # Disable backoff checking by making the condition a negative tautology. + client._RESTART_RATELIMIT_WINDOW = -1 + # First call is used for backoff checks, the second call is used + # for updating the _last_run_started_at attribute. + # 0.5, 1.0, 1.5, 2.0, 2.5, 3.0, ..., ..., ... + client._now = mock.MagicMock(side_effect=map(lambda n: n / 2, range(1, 100))) + return client + + @hikari_test_helpers.timeout() + async def test_invokes_run_once_shielded(self, client): + await client._run_once_shielded() + client._run_once.assert_awaited_once_with() + + @hikari_test_helpers.timeout() + async def test_happy_path_returns_False(self, client): + assert await client._run_once_shielded() is False + + @pytest.mark.parametrize( + ["zombied", "request_close", "expect_backoff_called"], + [(True, True, True), (True, False, True), (False, True, False), (False, False, False),], + ) + @hikari_test_helpers.timeout() + async def test_socket_closed_resets_backoff(self, client, zombied, request_close, expect_backoff_called): + client._request_close_event.is_set = mock.MagicMock(return_value=request_close) + + def run_once(): + client._zombied = zombied + raise gateway.Gateway._SocketClosed() + + client._run_once = mock.AsyncMock(wraps=run_once) + await client._run_once_shielded() + + if expect_backoff_called: + client._backoff.reset.assert_called_once_with() + else: + client._backoff.reset.assert_not_called() + + @hikari_test_helpers.timeout() + async def test_invalid_session_resume_does_not_clear_seq_or_session_id(self, client): + client._run_once = mock.AsyncMock(side_effect=gateway.Gateway._InvalidSession(True)) + client._seq = 1234 + client.session_id = "69420" + await client._run_once_shielded() + assert client._seq == 1234 + assert client.session_id == "69420" + + @pytest.mark.parametrize("request_close", [True, False]) + @hikari_test_helpers.timeout() + async def test_socket_closed_is_restartable_if_no_closure_request(self, client, request_close): + client._request_close_event.is_set = mock.MagicMock(return_value=request_close) + client._run_once = mock.AsyncMock(side_effect=gateway.Gateway._SocketClosed()) + assert await client._run_once_shielded() is not request_close + + @hikari_test_helpers.timeout() + async def test_ClientConnectionError_is_restartable(self, client): + key = aiohttp.client_reqrep.ConnectionKey( + host="localhost", port=6996, is_ssl=False, ssl=None, proxy=None, proxy_auth=None, proxy_headers_hash=69420, + ) + error = aiohttp.ClientConnectorError(key, OSError()) + + client._run_once = mock.AsyncMock(side_effect=error) + assert await client._run_once_shielded() is True + + @hikari_test_helpers.timeout() + async def test_invalid_session_is_restartable(self, client): + client._run_once = mock.AsyncMock(side_effect=gateway.Gateway._InvalidSession()) + assert await client._run_once_shielded() is True + + @hikari_test_helpers.timeout() + async def test_invalid_session_resume_does_not_invalidate_session(self, client): + client._run_once = mock.AsyncMock(side_effect=gateway.Gateway._InvalidSession(True)) + await client._run_once_shielded() + client._close_ws.assert_awaited_once_with( + gateway.Gateway._GatewayCloseCode.DO_NOT_INVALIDATE_SESSION, "invalid session (resume)" + ) + + @hikari_test_helpers.timeout() + async def test_invalid_session_no_resume_invalidates_session(self, client): + client._run_once = mock.AsyncMock(side_effect=gateway.Gateway._InvalidSession(False)) + await client._run_once_shielded() + client._close_ws.assert_awaited_once_with( + gateway.Gateway._GatewayCloseCode.RFC_6455_NORMAL_CLOSURE, "invalid session (no resume)" + ) + + @hikari_test_helpers.timeout() + async def test_invalid_session_no_resume_clears_seq_and_session_id(self, client): + client._run_once = mock.AsyncMock(side_effect=gateway.Gateway._InvalidSession(False)) + client._seq = 1234 + client.session_id = "69420" + await client._run_once_shielded() + assert client._seq is None + assert client.session_id is None + + @hikari_test_helpers.timeout() + async def test_reconnect_is_restartable(self, client): + client._run_once = mock.AsyncMock(side_effect=gateway.Gateway._Reconnect()) + assert await client._run_once_shielded() is True + + @hikari_test_helpers.timeout() + async def test_server_connection_error_resumes_if_reconnectable(self, client): + client._run_once = mock.AsyncMock(side_effect=errors.GatewayServerClosedConnectionError("blah", None, True)) + client._seq = 1234 + client.session_id = "69420" + assert await client._run_once_shielded() is True + assert client._seq == 1234 + assert client.session_id == "69420" + + async def test_server_connection_error_closes_websocket_if_reconnectable(self, client): + client._run_once = mock.AsyncMock(side_effect=errors.GatewayServerClosedConnectionError("blah", None, True)) + assert await client._run_once_shielded() is True + client._close_ws.assert_awaited_once_with( + gateway.Gateway._GatewayCloseCode.RFC_6455_NORMAL_CLOSURE, "you hung up on me" + ) + + @hikari_test_helpers.timeout() + async def test_server_connection_error_does_not_reconnect_if_not_reconnectable(self, client): + client._run_once = mock.AsyncMock(side_effect=errors.GatewayServerClosedConnectionError("blah", None, False)) + client._seq = 1234 + client.session_id = "69420" + with pytest.raises(errors.GatewayServerClosedConnectionError): + await client._run_once_shielded() + client._request_close_event.set.assert_called_once_with() + assert client._seq is None + assert client.session_id is None + client._backoff.reset.assert_called_once_with() + + async def test_server_connection_error_closes_websocket_if_not_reconnectable(self, client): + client._run_once = mock.AsyncMock(side_effect=errors.GatewayServerClosedConnectionError("blah", None, False)) + with pytest.raises(errors.GatewayServerClosedConnectionError): + await client._run_once_shielded() + client._close_ws.assert_awaited_once_with( + gateway.Gateway._GatewayCloseCode.RFC_6455_UNEXPECTED_CONDITION, "you broke the connection" + ) + + @pytest.mark.parametrize( + ["zombied", "request_close", "expect_backoff_called"], + [(True, True, True), (True, False, True), (False, True, False), (False, False, False)], + ) + @hikari_test_helpers.timeout() + async def test_socket_closed_resets_backoff(self, client, zombied, request_close, expect_backoff_called): + client._request_close_event.is_set = mock.MagicMock(return_value=request_close) + + def poll_events(): + client._zombied = zombied + raise gateway.Gateway._SocketClosed() + + client._run_once = mock.AsyncMock(wraps=poll_events) + await client._run_once_shielded() + + if expect_backoff_called: + client._backoff.reset.assert_called_once_with() + else: + client._backoff.reset.assert_not_called() + + async def test_other_exception_closes_websocket(self, client): + client._run_once = mock.AsyncMock(side_effect=RuntimeError()) + + with pytest.raises(RuntimeError): + await client._run_once_shielded() + + client._close_ws.assert_awaited_once_with( + gateway.Gateway._GatewayCloseCode.RFC_6455_UNEXPECTED_CONDITION, "unexpected error occurred" + ) + + @pytest.mark.asyncio class TestRunOnce: @pytest.fixture @@ -317,7 +509,8 @@ async def test_closing_bot_during_backoff_immediately_interrupts_it(self, client # The false instructs the caller to not restart again, but to just # drop everything and stop execution. - assert task.result() is False + # We never return a value on this task anymore. + assert task.result() is None finally: task.cancel() @@ -357,22 +550,6 @@ async def test_ws_gets_created(self, client): await client._run_once() client._create_ws.assert_awaited_once_with(client.url) - @hikari_test_helpers.timeout() - async def test_connected_at_is_set_before_handshake_and_is_cancelled_after(self, client): - assert math.isnan(client.connected_at) - - initial = -2.718281828459045 - client.connected_at = initial - - def ensure_connected_at_set(): - assert client.connected_at != initial - - client._handshake = mock.AsyncMock(wraps=ensure_connected_at_set) - - await client._run_once() - - assert math.isnan(client.connected_at) - @hikari_test_helpers.timeout() async def test_zlib_decompressobj_set(self, client): assert client._zlib is None @@ -447,137 +624,6 @@ async def test_heartbeat_is_stopped_when_poll_events_stops(self, client): task.cancel.assert_called_once_with() - @hikari_test_helpers.timeout() - async def test_happy_path_returns_False(self, client): - assert await client._run_once() is False - - @hikari_test_helpers.timeout() - async def test_ClientConnectionError_is_restartable(self, client): - key = aiohttp.client_reqrep.ConnectionKey( - host="localhost", port=6996, is_ssl=False, ssl=None, proxy=None, proxy_auth=None, proxy_headers_hash=69420, - ) - error = aiohttp.ClientConnectorError(key, OSError()) - - client._handshake = mock.AsyncMock(side_effect=error) - assert await client._run_once() is True - - @hikari_test_helpers.timeout() - async def test_invalid_session_is_restartable(self, client): - client._poll_events = mock.AsyncMock(side_effect=gateway.Gateway._InvalidSession()) - assert await client._run_once() is True - - @hikari_test_helpers.timeout() - async def test_invalid_session_resume_does_not_invalidate_session(self, client): - client._poll_events = mock.AsyncMock(side_effect=gateway.Gateway._InvalidSession(True)) - await client._run_once() - client._close_ws.assert_awaited_once_with( - gateway.Gateway._GatewayCloseCode.DO_NOT_INVALIDATE_SESSION, "invalid session (resume)" - ) - - @hikari_test_helpers.timeout() - async def test_invalid_session_no_resume_invalidates_session(self, client): - client._poll_events = mock.AsyncMock(side_effect=gateway.Gateway._InvalidSession(False)) - await client._run_once() - client._close_ws.assert_awaited_once_with( - gateway.Gateway._GatewayCloseCode.RFC_6455_NORMAL_CLOSURE, "invalid session (no resume)" - ) - - @hikari_test_helpers.timeout() - async def test_invalid_session_resume_does_not_clear_seq_or_session_id(self, client): - client._poll_events = mock.AsyncMock(side_effect=gateway.Gateway._InvalidSession(True)) - client._seq = 1234 - client.session_id = "69420" - await client._run_once() - assert client._seq == 1234 - assert client.session_id == "69420" - - @hikari_test_helpers.timeout() - async def test_invalid_session_no_resume_clears_seq_and_session_id(self, client): - client._poll_events = mock.AsyncMock(side_effect=gateway.Gateway._InvalidSession(False)) - client._seq = 1234 - client.session_id = "69420" - await client._run_once() - assert client._seq is None - assert client.session_id is None - - @hikari_test_helpers.timeout() - async def test_reconnect_is_restartable(self, client): - client._poll_events = mock.AsyncMock(side_effect=gateway.Gateway._Reconnect()) - assert await client._run_once() is True - - @pytest.mark.parametrize("request_close", [True, False]) - @hikari_test_helpers.timeout() - async def test_socket_closed_is_restartable_if_no_closure_request(self, client, request_close): - client._request_close_event.is_set = mock.MagicMock(return_value=request_close) - client._poll_events = mock.AsyncMock(side_effect=gateway.Gateway._SocketClosed()) - assert await client._run_once() is not request_close - - @pytest.mark.parametrize( - ["zombied", "request_close", "expect_backoff_called"], - [(True, True, True), (True, False, True), (False, True, False), (False, False, False)], - ) - @hikari_test_helpers.timeout() - async def test_socket_closed_resets_backoff(self, client, zombied, request_close, expect_backoff_called): - client._request_close_event.is_set = mock.MagicMock(return_value=request_close) - - def poll_events(): - client._zombied = zombied - raise gateway.Gateway._SocketClosed() - - client._poll_events = mock.AsyncMock(wraps=poll_events) - await client._run_once() - - if expect_backoff_called: - client._backoff.reset.assert_called_once_with() - else: - client._backoff.reset.assert_not_called() - - @hikari_test_helpers.timeout() - async def test_server_connection_error_resumes_if_reconnectable(self, client): - client._poll_events = mock.AsyncMock(side_effect=errors.GatewayServerClosedConnectionError("blah", None, True)) - client._seq = 1234 - client.session_id = "69420" - assert await client._run_once() is True - assert client._seq == 1234 - assert client.session_id == "69420" - - async def test_server_connection_error_closes_websocket_if_reconnectable(self, client): - client._poll_events = mock.AsyncMock(side_effect=errors.GatewayServerClosedConnectionError("blah", None, True)) - assert await client._run_once() is True - client._close_ws.assert_awaited_once_with( - gateway.Gateway._GatewayCloseCode.RFC_6455_NORMAL_CLOSURE, "you hung up on me" - ) - - @hikari_test_helpers.timeout() - async def test_server_connection_error_does_not_reconnect_if_not_reconnectable(self, client): - client._poll_events = mock.AsyncMock(side_effect=errors.GatewayServerClosedConnectionError("blah", None, False)) - client._seq = 1234 - client.session_id = "69420" - with pytest.raises(errors.GatewayServerClosedConnectionError): - await client._run_once() - client._request_close_event.set.assert_called_once_with() - assert client._seq is None - assert client.session_id is None - client._backoff.reset.assert_called_once_with() - - async def test_server_connection_error_closes_websocket_if_not_reconnectable(self, client): - client._poll_events = mock.AsyncMock(side_effect=errors.GatewayServerClosedConnectionError("blah", None, False)) - with pytest.raises(errors.GatewayServerClosedConnectionError): - await client._run_once() - client._close_ws.assert_awaited_once_with( - gateway.Gateway._GatewayCloseCode.RFC_6455_UNEXPECTED_CONDITION, "you broke the connection" - ) - - async def test_other_exception_closes_websocket(self, client): - client._poll_events = mock.AsyncMock(side_effect=RuntimeError()) - - with pytest.raises(RuntimeError): - await client._run_once() - - client._close_ws.assert_awaited_once_with( - gateway.Gateway._GatewayCloseCode.RFC_6455_UNEXPECTED_CONDITION, "unexpected error occurred" - ) - async def test_dispatches_disconnect_if_connected(self, client): await client._run_once() client._dispatch.assert_any_call("CONNECTED", {}) diff --git a/tests/hikari/net/test_http_client.py b/tests/hikari/net/test_http_client.py index d6d8e26b43..8281edf952 100644 --- a/tests/hikari/net/test_http_client.py +++ b/tests/hikari/net/test_http_client.py @@ -59,10 +59,12 @@ def test_when_no_client_session(self, client): @pytest.mark.asyncio class TestAcquireClientSession: - async def test_acquire_creates_new_session_if_one_does_not_exist(self, client): + @pytest.mark.parametrize("connector_owner", [True, False]) + async def test_acquire_creates_new_session_if_one_does_not_exist(self, client, connector_owner): client._config = http_settings.HTTPSettings() - client._config.tcp_connector_factory = mock.MagicMock() + client._config.tcp_connector = mock.MagicMock() client._config.trust_env = mock.MagicMock() + client._config.connector_owner = connector_owner client._client_session = None cs = client.get_client_session() @@ -71,10 +73,11 @@ async def test_acquire_creates_new_session_if_one_does_not_exist(self, client): assert cs in weakref.getweakrefs(client._client_session), "did not return correct weakref" aiohttp.ClientSession.assert_called_once_with( - connector=client._config.tcp_connector_factory(), + connector=client._config.tcp_connector, trust_env=client._config.trust_env, version=aiohttp.HttpVersion11, json_serialize=json.dumps, + connector_owner=connector_owner, ) async def test_acquire_repeated_calls_caches_client_session(self, client): From 243f15ff728085e49bd480780571ce0ad9720b1d Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Wed, 24 Jun 2020 19:02:41 +0100 Subject: [PATCH 568/922] Reclassified FIXMEs as TODOs because I don't know what 'FIXME' means. --- .flake8 | 3 +-- hikari/api/rest.py | 5 ++--- hikari/impl/event_manager.py | 8 ++++---- hikari/impl/rest.py | 5 ++--- hikari/models/invites.py | 2 +- hikari/net/gateway.py | 2 +- hikari/net/rest.py | 4 ++-- 7 files changed, 13 insertions(+), 16 deletions(-) diff --git a/.flake8 b/.flake8 index 7b5c20915d..fe8afc36b6 100644 --- a/.flake8 +++ b/.flake8 @@ -9,8 +9,7 @@ ignore = D105, # Magic methods not having a docstring. E402, # Module level import not at top of file (isn't compatible with our import style). IFSTMT001 # "use a oneliner here" - T100, # FIX ME comments - T101, # TO DO comments + T101, # TO DO comments only per-file-ignores = diff --git a/hikari/api/rest.py b/hikari/api/rest.py index 0c9270c4f0..e805741d8e 100644 --- a/hikari/api/rest.py +++ b/hikari/api/rest.py @@ -27,10 +27,9 @@ from hikari.net import strings if typing.TYPE_CHECKING: + import concurrent.futures import types - from concurrent import futures - from hikari.api import cache as cache_ from hikari.api import entity_factory as entity_factory_ from hikari.net import rest as rest_ @@ -83,7 +82,7 @@ def entity_factory(self) -> entity_factory_.IEntityFactoryComponent: @property @abc.abstractmethod - def executor(self) -> typing.Optional[futures.Executor]: + def executor(self) -> typing.Optional[concurrent.futures.Executor]: """Thread-pool to utilise for file IO within the library, if set. Returns diff --git a/hikari/impl/event_manager.py b/hikari/impl/event_manager.py index 967fd89ff1..420a32e351 100644 --- a/hikari/impl/event_manager.py +++ b/hikari/impl/event_manager.py @@ -40,7 +40,7 @@ async def on_connected(self, shard: gateway.Gateway, _: data_binding.JSONObject) This is a synthetic event produced by the gateway implementation in Hikari. """ - # FIXME: this should be in entity factory + # TODO: this should be in entity factory await self.dispatch(other.ConnectedEvent(shard=shard)) async def on_disconnected(self, shard: gateway.Gateway, _: data_binding.JSONObject) -> None: @@ -49,7 +49,7 @@ async def on_disconnected(self, shard: gateway.Gateway, _: data_binding.JSONObje This is a synthetic event produced by the gateway implementation in Hikari. """ - # FIXME: this should be in entity factory + # TODO: this should be in entity factory await self.dispatch(other.DisconnectedEvent(shard=shard)) async def on_ready(self, shard: gateway.Gateway, payload: data_binding.JSONObject) -> None: @@ -58,7 +58,7 @@ async def on_ready(self, shard: gateway.Gateway, payload: data_binding.JSONObjec async def on_resumed(self, shard: gateway.Gateway, _: data_binding.JSONObject) -> None: """See https://discord.com/developers/docs/topics/gateway#resumed for more info.""" - # FIXME: this should be in entity factory + # TODO: this should be in entity factory await self.dispatch(other.ResumedEvent(shard=shard)) async def on_channel_create(self, _: gateway.Gateway, payload: data_binding.JSONObject) -> None: @@ -122,7 +122,7 @@ async def on_guild_member_update(self, _: gateway.Gateway, payload: data_binding async def on_guild_members_chunk(self, _: gateway.Gateway, payload: data_binding.JSONObject) -> None: """See https://discord.com/developers/docs/topics/gateway#guild-members-chunk for more info.""" - # FIXME: implement model for this, and implement chunking components. + # TODO: implement model for this, and implement chunking components. # await self.dispatch(self.app.entity_factory.deserialize_guild_member_chunk_event(payload)) async def on_guild_role_create(self, _: gateway.Gateway, payload: data_binding.JSONObject) -> None: diff --git a/hikari/impl/rest.py b/hikari/impl/rest.py index 31e003e026..becc83971b 100644 --- a/hikari/impl/rest.py +++ b/hikari/impl/rest.py @@ -25,11 +25,10 @@ __all__: typing.Final[typing.List[str]] = ["RESTClientFactoryImpl", "RESTClientImpl"] +import concurrent.futures import copy import typing -from concurrent import futures - import aiohttp from hikari.api import rest as rest_api @@ -104,7 +103,7 @@ def __init__( ) @property - def executor(self) -> typing.Optional[futures.Executor]: + def executor(self) -> typing.Optional[concurrent.futures.Executor]: return self._executor @property diff --git a/hikari/models/invites.py b/hikari/models/invites.py index 8f6a0b3a90..3b98a9fbe9 100644 --- a/hikari/models/invites.py +++ b/hikari/models/invites.py @@ -232,7 +232,7 @@ class InviteWithMetadata(Invite): If set to `None` then this is unlimited. """ - # FIXME: can we use a non-None value to represent infinity here somehow, or + # TODO: can we use a non-None value to represent infinity here somehow, or # make a timedelta that is infinite for comparisons? max_age: typing.Optional[datetime.timedelta] = attr.attrib(eq=False, hash=False, repr=False) """The timedelta of how long this invite will be valid for. diff --git a/hikari/net/gateway.py b/hikari/net/gateway.py index 1c73f629c3..72a3731a67 100644 --- a/hikari/net/gateway.py +++ b/hikari/net/gateway.py @@ -371,7 +371,7 @@ async def _run_once(self) -> None: except asyncio.TimeoutError: pass - # Do this after; it prevents backing off on the first try. + # Do this after. It prevents backing off on the first try. self._last_run_started_at = self._now() self._logger.debug("creating websocket connection to %s", self.url) diff --git a/hikari/net/rest.py b/hikari/net/rest.py index 3f53c646a0..6867046bf9 100644 --- a/hikari/net/rest.py +++ b/hikari/net/rest.py @@ -93,7 +93,7 @@ class REST(http_client.HTTPClient, component.IComponent): as well as information such as DNS cache hits and misses, and other information useful for debugging this application. These logs will be written as DEBUG log entries. For most purposes, this should be - left `False`.eee + left `False`. global_ratelimit : hikari.net.rate_limits.ManualRateLimiter The shared ratelimiter to use for the application. token : str or hikari.utilities.undefined.UndefinedType @@ -1888,7 +1888,7 @@ async def edit_guild( body.put_snowflake("rules_channel_id", rules_channel) body.put_snowflake("public_updates_channel_id", public_updates_channel) - # FIXME: gather these futures simultaneously for a 3x speedup... + # TODO: gather these futures simultaneously for a 3x speedup... if icon is None: body.put("icon", None) From badc8e5f23ccce43366ae106b2e2b73ca111dcaf Mon Sep 17 00:00:00 2001 From: davfsa Date: Wed, 24 Jun 2020 21:11:07 +0200 Subject: [PATCH 569/922] Fixed webhook endpoints --- hikari/net/rest.py | 57 ++++++++++++++++++++++---------------------- hikari/net/routes.py | 1 - 2 files changed, 29 insertions(+), 29 deletions(-) diff --git a/hikari/net/rest.py b/hikari/net/rest.py index 6867046bf9..2abcdcd477 100644 --- a/hikari/net/rest.py +++ b/hikari/net/rest.py @@ -1434,7 +1434,7 @@ async def create_webhook( avatar: typing.Union[undefined.UndefinedType, files.Resource, str] = undefined.UNDEFINED, reason: typing.Union[undefined.UndefinedType, str] = undefined.UNDEFINED, ) -> webhooks.Webhook: - route = routes.POST_WEBHOOK.compile(channel=channel) + route = routes.POST_CHANNEL_WEBHOOKS.compile(channel=channel) body = data_binding.JSONObjectBuilder() body.put("name", name) if avatar is not undefined.UNDEFINED: @@ -1452,12 +1452,14 @@ async def fetch_webhook( *, token: typing.Union[undefined.UndefinedType, str] = undefined.UNDEFINED, ) -> webhooks.Webhook: - route = ( - routes.GET_WEBHOOK.compile(webhook=webhook) - if token is undefined.UNDEFINED - else routes.GET_WEBHOOK_WITH_TOKEN.compile(webhook=webhook, token=token) - ) - raw_response = await self._request(route) + if token is undefined.UNDEFINED: + route = routes.GET_WEBHOOK.compile(webhook=webhook) + no_auth = False + else: + route = routes.GET_WEBHOOK_WITH_TOKEN.compile(webhook=webhook, token=token) + no_auth = True + + raw_response = await self._request(route, no_auth=no_auth) response = typing.cast(data_binding.JSONObject, raw_response) return self._app.entity_factory.deserialize_webhook(response) @@ -1489,11 +1491,13 @@ async def edit_webhook( ] = undefined.UNDEFINED, reason: typing.Union[undefined.UndefinedType, str] = undefined.UNDEFINED, ) -> webhooks.Webhook: - route = ( - routes.PATCH_WEBHOOK.compile(webhook=webhook) - if token is undefined.UNDEFINED - else routes.PATCH_WEBHOOK_WITH_TOKEN.compile(webhook=webhook, token=token) - ) + if token is undefined.UNDEFINED: + route = routes.PATCH_WEBHOOK.compile(webhook=webhook) + no_auth = False + else: + route = routes.PATCH_WEBHOOK_WITH_TOKEN.compile(webhook=webhook, token=token) + no_auth = True + body = data_binding.JSONObjectBuilder() body.put("name", name) body.put_snowflake("channel", channel) @@ -1505,7 +1509,7 @@ async def edit_webhook( async with avatar_resource.stream(executor=self._app.executor) as stream: body.put("avatar", await stream.data_uri()) - raw_response = await self._request(route, body=body, reason=reason) + raw_response = await self._request(route, body=body, reason=reason, no_auth=no_auth) response = typing.cast(data_binding.JSONObject, raw_response) return self._app.entity_factory.deserialize_webhook(response) @@ -1515,19 +1519,21 @@ async def delete_webhook( *, token: typing.Union[undefined.UndefinedType, str] = undefined.UNDEFINED, ) -> None: - route = ( - routes.DELETE_WEBHOOK.compile(webhook=webhook) - if token is undefined.UNDEFINED - else routes.DELETE_WEBHOOK_WITH_TOKEN.compile(webhook=webhook, token=token) - ) - await self._request(route) + if token is undefined.UNDEFINED: + route = routes.DELETE_WEBHOOK.compile(webhook=webhook) + no_auth = False + else: + route = routes.DELETE_WEBHOOK_WITH_TOKEN.compile(webhook=webhook, token=token) + no_auth = True + + await self._request(route, no_auth=no_auth) async def execute_webhook( self, webhook: typing.Union[webhooks.Webhook, snowflake.UniqueObject], + token: str, text: typing.Union[undefined.UndefinedType, typing.Any] = undefined.UNDEFINED, *, - token: typing.Union[undefined.UndefinedType, str] = undefined.UNDEFINED, username: typing.Union[undefined.UndefinedType, str] = undefined.UNDEFINED, avatar_url: typing.Union[undefined.UndefinedType, str] = undefined.UNDEFINED, embeds: typing.Union[undefined.UndefinedType, typing.Sequence[embeds_.Embed]] = undefined.UNDEFINED, @@ -1543,12 +1549,7 @@ async def execute_webhook( if attachment is not undefined.UNDEFINED and attachments is not undefined.UNDEFINED: raise ValueError("You may only specify one of 'attachment' or 'attachments', not both") - if token is undefined.UNDEFINED: - route = routes.POST_WEBHOOK.compile(webhook=webhook) - no_auth = False - else: - route = routes.POST_WEBHOOK_WITH_TOKEN.compile(webhook=webhook, token=token) - no_auth = True + route = routes.POST_WEBHOOK_WITH_TOKEN.compile(webhook=webhook, token=token) final_attachments: typing.List[files.Resource] = [] if attachment is not undefined.UNDEFINED: @@ -1586,11 +1587,11 @@ async def execute_webhook( f"file{i}", stream, filename=stream.filename, content_type=strings.APPLICATION_OCTET_STREAM ) - raw_response = await self._request(route, body=form, no_auth=no_auth) + raw_response = await self._request(route, body=form, no_auth=True) finally: await stack.aclose() else: - raw_response = await self._request(route, body=body, no_auth=no_auth) + raw_response = await self._request(route, body=body, no_auth=True) response = typing.cast(data_binding.JSONObject, raw_response) return self._app.entity_factory.deserialize_message(response) diff --git a/hikari/net/routes.py b/hikari/net/routes.py index c4497ceb59..deda10602e 100644 --- a/hikari/net/routes.py +++ b/hikari/net/routes.py @@ -304,7 +304,6 @@ def __str__(self) -> str: # Webhooks GET_WEBHOOK: typing.Final[Route] = Route(GET, "/webhooks/{webhook}") PATCH_WEBHOOK: typing.Final[Route] = Route(PATCH, "/webhooks/{webhook}") -POST_WEBHOOK: typing.Final[Route] = Route(POST, "/webhooks/{webhook}") DELETE_WEBHOOK: typing.Final[Route] = Route(DELETE, "/webhooks/{webhook}") GET_WEBHOOK_WITH_TOKEN: typing.Final[Route] = Route(GET, "/webhooks/{webhook}/{token}") From ff9b9b33415ee59d6259da020982b2ad6a0170b3 Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Wed, 24 Jun 2020 20:35:09 +0000 Subject: [PATCH 570/922] Update permissions.py to include VIEW_GUILD_INSIGHTS, fixes #406. --- hikari/models/permissions.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/hikari/models/permissions.py b/hikari/models/permissions.py index fc8cb4a87e..e060197780 100644 --- a/hikari/models/permissions.py +++ b/hikari/models/permissions.py @@ -146,7 +146,10 @@ class Permission(enum.IntFlag): """ USE_EXTERNAL_EMOJIS = 1 << 18 - """Allows the usage of custom emojis from other servers.""" + """Allows the usage of custom emojis from other guilds.""" + + VIEW_GUILD_INSIGHTS = 1 << 19 + """Allows the user to view guild insights for eligible guilds.""" CONNECT = 1 << 20 """Allows for joining of a voice channel.""" From e08244c40d8cbdd61907a52d66c5070196b8059e Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Wed, 24 Jun 2020 22:08:10 +0100 Subject: [PATCH 571/922] Fixed docstrings in pdoc3 missing typehints for variables in classes. --- ci/gitlab/tests.yml | 2 ++ docs/documentation.mako | 47 ++++++++++++++++++++++-------------- hikari/models/permissions.py | 2 +- 3 files changed, 32 insertions(+), 19 deletions(-) diff --git a/ci/gitlab/tests.yml b/ci/gitlab/tests.yml index 0f786b74de..6ee7096c35 100644 --- a/ci/gitlab/tests.yml +++ b/ci/gitlab/tests.yml @@ -28,6 +28,7 @@ junit: public/tests.xml extends: .reactive-job interruptible: true + retry: 1 script: - apt-get install -qy git gcc g++ make - pip install nox @@ -68,6 +69,7 @@ test:win32: reports: junit: public/tests.xml extends: .win32 + retry: 1 script: - choco install --no-progress python -y - if (!(test-path "public")) { New-Item -ItemType Directory -Force -Path public } diff --git a/docs/documentation.mako b/docs/documentation.mako index 002435df83..5c468aa9e0 100644 --- a/docs/documentation.mako +++ b/docs/documentation.mako @@ -212,7 +212,7 @@ return name def get_annotation(bound_method, sep=':'): - annot = bound_method(link=link) or 'typing.Any' + annot = bound_method(link=link) annot = annot.replace("NoneType", "None") # Remove quotes. @@ -220,6 +220,9 @@ annot = annot[1:-1] if annot: annot = ' ' + sep + '\N{NBSP}' + annot + + annot = annot.replace("[ ", "[") # FIXME: whatever causes space between [ and link so I don't have to use this hack. + return annot def to_html(text): @@ -270,6 +273,14 @@ return_type = get_annotation(v.type_annotation) if return_type == "": parent = v.cls.obj if v.cls is not None else v.module.obj + + if hasattr(parent, "mro"): + for cls in parent.mro(): + if hasattr(cls, "__annotations__") and v.name in cls.__annotations__: + return_type = get_annotation(lambda *_, **__: cls.__annotations__[v.name]) + if return_type != "": + break + if hasattr(parent, "__annotations__") and v.name in parent.__annotations__: return_type = get_annotation(lambda *_, **__: parent.__annotations__[v.name]) @@ -363,7 +374,7 @@ params = c.params(annotate=show_type_annotations, link=link) example_str = f"{QUAL_CLASS} " + c.name + "(" + ", ".join(params) + ")" - if len(params) > 4 or len(example_str) > 70: + if len(params) > 4 or len(example_str) > 70 and len(params) > 0: representation = "\n".join(( f"{QUAL_CLASS} {c.name} (", *(f" {p}," for p in params), @@ -444,41 +455,41 @@
    % endif - % if class_vars: -
    Class variables
    + % if methods: +
    Instance methods
    - % for cv in class_vars: - ${show_var(cv)} + % for m in methods: + ${show_func(m)} % endfor
    % endif - % if smethods: -
    Class methods
    + % if inst_vars: +
    Instance variables and properties
    - % for m in smethods: - ${show_func(m)} + % for i in inst_vars: + ${show_var(i)} % endfor
    % endif - % if inst_vars: -
    Instance variables
    + % if smethods: +
    Class methods
    - % for i in inst_vars: - ${show_var(i)} + % for m in smethods: + ${show_func(m)} % endfor
    % endif - % if methods: -
    Instance methods
    + % if class_vars: +
    Class variables and properties
    - % for m in methods: - ${show_func(m)} + % for cv in class_vars: + ${show_var(cv)} % endfor
    diff --git a/hikari/models/permissions.py b/hikari/models/permissions.py index e060197780..c62da8cece 100644 --- a/hikari/models/permissions.py +++ b/hikari/models/permissions.py @@ -147,7 +147,7 @@ class Permission(enum.IntFlag): USE_EXTERNAL_EMOJIS = 1 << 18 """Allows the usage of custom emojis from other guilds.""" - + VIEW_GUILD_INSIGHTS = 1 << 19 """Allows the user to view guild insights for eligible guilds.""" From d5ce4ed3f84fe42b64032bcf89a034c82cbc55e0 Mon Sep 17 00:00:00 2001 From: davfsa Date: Thu, 25 Jun 2020 10:45:59 +0200 Subject: [PATCH 572/922] Fix execute webhook enpoint as it was returning payload to be `None` --- hikari/net/rest.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/hikari/net/rest.py b/hikari/net/rest.py index 2abcdcd477..cdf8edbb83 100644 --- a/hikari/net/rest.py +++ b/hikari/net/rest.py @@ -1572,7 +1572,8 @@ async def execute_webhook( body.put("username", username) body.put("avatar_url", avatar_url) body.put("tts", tts) - body.put("wait", True) + query = data_binding.StringMapBuilder() + query.put("wait", True) if final_attachments: form = data_binding.URLEncodedForm() @@ -1587,11 +1588,11 @@ async def execute_webhook( f"file{i}", stream, filename=stream.filename, content_type=strings.APPLICATION_OCTET_STREAM ) - raw_response = await self._request(route, body=form, no_auth=True) + raw_response = await self._request(route, query=query, body=form, no_auth=True) finally: await stack.aclose() else: - raw_response = await self._request(route, body=body, no_auth=True) + raw_response = await self._request(route, query=query, body=body, no_auth=True) response = typing.cast(data_binding.JSONObject, raw_response) return self._app.entity_factory.deserialize_message(response) From 12225e59c93048f5919be43c822ccab3b7ccfc56 Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Thu, 25 Jun 2020 12:41:20 +0100 Subject: [PATCH 573/922] Made type of `__all__` into sequence; made bot able to be stateless, made RESTClient stateless, fixed logging of banner. --- hikari/__init__.py | 2 +- hikari/api/__init__.py | 2 +- hikari/api/bot.py | 2 +- hikari/api/cache.py | 2 +- hikari/api/component.py | 2 +- hikari/api/entity_factory.py | 2 +- hikari/api/event_consumer.py | 2 +- hikari/api/event_dispatcher.py | 6 ++- hikari/api/gateway_zookeeper.py | 2 +- hikari/api/rest.py | 2 +- hikari/errors.py | 2 +- hikari/events/__init__.py | 2 +- hikari/events/base.py | 2 +- hikari/events/channel.py | 2 +- hikari/events/guild.py | 2 +- hikari/events/message.py | 2 +- hikari/events/other.py | 2 +- hikari/events/voice.py | 2 +- hikari/impl/__init__.py | 2 +- hikari/impl/bot.py | 33 ++++++------ hikari/impl/cache.py | 2 +- hikari/impl/entity_factory.py | 2 +- hikari/impl/event_manager.py | 2 +- hikari/impl/event_manager_core.py | 2 +- hikari/impl/gateway_zookeeper.py | 2 +- hikari/impl/rest.py | 5 +- hikari/impl/stateless_cache.py | 85 +++++++++++++++++++++++++++++++ hikari/models/__init__.py | 2 +- hikari/models/applications.py | 2 +- hikari/models/audit_logs.py | 2 +- hikari/models/channels.py | 2 +- hikari/models/colors.py | 2 +- hikari/models/colours.py | 2 +- hikari/models/embeds.py | 2 +- hikari/models/emojis.py | 2 +- hikari/models/gateway.py | 2 +- hikari/models/guilds.py | 2 +- hikari/models/intents.py | 2 +- hikari/models/invites.py | 8 ++- hikari/models/messages.py | 2 +- hikari/models/permissions.py | 2 +- hikari/models/presences.py | 2 +- hikari/models/users.py | 2 +- hikari/models/voices.py | 2 +- hikari/models/webhooks.py | 2 +- hikari/net/__init__.py | 2 +- hikari/net/buckets.py | 2 +- hikari/net/gateway.py | 2 +- hikari/net/http_client.py | 2 +- hikari/net/http_settings.py | 2 +- hikari/net/iterators.py | 2 +- hikari/net/rate_limits.py | 2 +- hikari/net/rest.py | 2 +- hikari/net/rest_utils.py | 2 +- hikari/net/routes.py | 2 +- hikari/net/strings.py | 2 +- hikari/utilities/__init__.py | 2 +- hikari/utilities/aio.py | 2 +- hikari/utilities/cdn.py | 2 +- hikari/utilities/data_binding.py | 2 +- hikari/utilities/date.py | 2 +- hikari/utilities/reflect.py | 2 +- hikari/utilities/snowflake.py | 2 +- hikari/utilities/undefined.py | 2 +- 64 files changed, 177 insertions(+), 78 deletions(-) create mode 100644 hikari/impl/stateless_cache.py diff --git a/hikari/__init__.py b/hikari/__init__.py index 89378b9857..52e160a80f 100644 --- a/hikari/__init__.py +++ b/hikari/__init__.py @@ -19,7 +19,7 @@ from __future__ import annotations -__all__: typing.Final[typing.List[str]] = ["RESTClient", "Bot"] +__all__: typing.Final[typing.Sequence[str]] = ["RESTClient", "Bot"] # noinspection PyUnresolvedReferences import typing diff --git a/hikari/api/__init__.py b/hikari/api/__init__.py index e45ad286f5..4bebfabac5 100644 --- a/hikari/api/__init__.py +++ b/hikari/api/__init__.py @@ -23,7 +23,7 @@ """ from __future__ import annotations -__all__: typing.Final[typing.List[str]] = [] +__all__: typing.Final[typing.Sequence[str]] = [] # noinspection PyUnresolvedReferences import typing diff --git a/hikari/api/bot.py b/hikari/api/bot.py index 64d418b049..3ba3bec5b6 100644 --- a/hikari/api/bot.py +++ b/hikari/api/bot.py @@ -19,7 +19,7 @@ from __future__ import annotations -__all__: typing.Final[typing.List[str]] = ["IBotApp"] +__all__: typing.Final[typing.Sequence[str]] = ["IBotApp"] import abc import typing diff --git a/hikari/api/cache.py b/hikari/api/cache.py index 962f597172..28363f1016 100644 --- a/hikari/api/cache.py +++ b/hikari/api/cache.py @@ -18,7 +18,7 @@ """Core interface for a cache implementation.""" from __future__ import annotations -__all__: typing.Final[typing.List[str]] = ["ICacheComponent"] +__all__: typing.Final[typing.Sequence[str]] = ["ICacheComponent"] import abc diff --git a/hikari/api/component.py b/hikari/api/component.py index 022f5310f3..5b97f71189 100644 --- a/hikari/api/component.py +++ b/hikari/api/component.py @@ -19,7 +19,7 @@ from __future__ import annotations -__all__: typing.Final[typing.List[str]] = ["IComponent"] +__all__: typing.Final[typing.Sequence[str]] = ["IComponent"] import abc import typing diff --git a/hikari/api/entity_factory.py b/hikari/api/entity_factory.py index 85238ad0da..5843303ced 100644 --- a/hikari/api/entity_factory.py +++ b/hikari/api/entity_factory.py @@ -18,7 +18,7 @@ """Core interface for an object that serializes/deserializes API objects.""" from __future__ import annotations -__all__: typing.Final[typing.List[str]] = ["IEntityFactoryComponent"] +__all__: typing.Final[typing.Sequence[str]] = ["IEntityFactoryComponent"] import abc import typing diff --git a/hikari/api/event_consumer.py b/hikari/api/event_consumer.py index 905dedf721..5368d1dad4 100644 --- a/hikari/api/event_consumer.py +++ b/hikari/api/event_consumer.py @@ -18,7 +18,7 @@ """Core interface for components that consume raw API event payloads.""" from __future__ import annotations -__all__: typing.Final[typing.List[str]] = ["IEventConsumerComponent", "IEventConsumerApp"] +__all__: typing.Final[typing.Sequence[str]] = ["IEventConsumerComponent", "IEventConsumerApp"] import abc import typing diff --git a/hikari/api/event_dispatcher.py b/hikari/api/event_dispatcher.py index 0e143f7c14..991aff9158 100644 --- a/hikari/api/event_dispatcher.py +++ b/hikari/api/event_dispatcher.py @@ -18,7 +18,11 @@ """Core interface for components that dispatch events to the library.""" from __future__ import annotations -__all__: typing.Final[typing.List[str]] = ["IEventDispatcherBase", "IEventDispatcherApp", "IEventDispatcherComponent"] +__all__: typing.Final[typing.Sequence[str]] = [ + "IEventDispatcherBase", + "IEventDispatcherApp", + "IEventDispatcherComponent", +] import abc import asyncio diff --git a/hikari/api/gateway_zookeeper.py b/hikari/api/gateway_zookeeper.py index b5af0f3c1e..b403a8e227 100644 --- a/hikari/api/gateway_zookeeper.py +++ b/hikari/api/gateway_zookeeper.py @@ -19,7 +19,7 @@ from __future__ import annotations -__all__: typing.Final[typing.List[str]] = ["IGatewayZookeeperApp"] +__all__: typing.Final[typing.Sequence[str]] = ["IGatewayZookeeperApp"] import abc import typing diff --git a/hikari/api/rest.py b/hikari/api/rest.py index e805741d8e..f4ea30ece2 100644 --- a/hikari/api/rest.py +++ b/hikari/api/rest.py @@ -19,7 +19,7 @@ from __future__ import annotations -__all__: typing.Final[typing.List[str]] = ["IRESTClient", "IRESTClientFactory"] +__all__: typing.Final[typing.Sequence[str]] = ["IRESTClient", "IRESTClientFactory"] import abc import typing diff --git a/hikari/errors.py b/hikari/errors.py index 0ea9f3ebeb..373efe02b9 100644 --- a/hikari/errors.py +++ b/hikari/errors.py @@ -19,7 +19,7 @@ from __future__ import annotations -__all__: typing.Final[typing.List[str]] = [ +__all__: typing.Final[typing.Sequence[str]] = [ "HikariError", "HikariWarning", "NotFound", diff --git a/hikari/events/__init__.py b/hikari/events/__init__.py index 1aaf887951..334f957cf9 100644 --- a/hikari/events/__init__.py +++ b/hikari/events/__init__.py @@ -19,7 +19,7 @@ from __future__ import annotations -__all__: typing.Final[typing.List[str]] = [] +__all__: typing.Final[typing.Sequence[str]] = [] # noinspection PyUnresolvedReferences import typing diff --git a/hikari/events/base.py b/hikari/events/base.py index 6d1dbd23f5..cbd34a6f11 100644 --- a/hikari/events/base.py +++ b/hikari/events/base.py @@ -19,7 +19,7 @@ from __future__ import annotations -__all__: typing.Final[typing.List[str]] = [ +__all__: typing.Final[typing.Sequence[str]] = [ "Event", "get_required_intents_for", "requires_intents", diff --git a/hikari/events/channel.py b/hikari/events/channel.py index 459eb8c719..ed7f4d2833 100644 --- a/hikari/events/channel.py +++ b/hikari/events/channel.py @@ -19,7 +19,7 @@ from __future__ import annotations -__all__: typing.Final[typing.List[str]] = [ +__all__: typing.Final[typing.Sequence[str]] = [ "BaseChannelEvent", "ChannelCreateEvent", "ChannelUpdateEvent", diff --git a/hikari/events/guild.py b/hikari/events/guild.py index 9f6e8c5f6e..f660c105c2 100644 --- a/hikari/events/guild.py +++ b/hikari/events/guild.py @@ -19,7 +19,7 @@ from __future__ import annotations -__all__: typing.Final[typing.List[str]] = [ +__all__: typing.Final[typing.Sequence[str]] = [ "GuildEvent", "GuildCreateEvent", "GuildUpdateEvent", diff --git a/hikari/events/message.py b/hikari/events/message.py index b7bdcd0245..6620d0bda1 100644 --- a/hikari/events/message.py +++ b/hikari/events/message.py @@ -19,7 +19,7 @@ from __future__ import annotations -__all__: typing.Final[typing.List[str]] = [ +__all__: typing.Final[typing.Sequence[str]] = [ "MessageReactionEvent", "MessageCreateEvent", "UpdatedMessageFields", diff --git a/hikari/events/other.py b/hikari/events/other.py index 6a77b1681b..761c9e9d22 100644 --- a/hikari/events/other.py +++ b/hikari/events/other.py @@ -19,7 +19,7 @@ from __future__ import annotations -__all__: typing.Final[typing.List[str]] = [ +__all__: typing.Final[typing.Sequence[str]] = [ "ExceptionEvent", "ConnectedEvent", "DisconnectedEvent", diff --git a/hikari/events/voice.py b/hikari/events/voice.py index d02e97bc43..8d1828e281 100644 --- a/hikari/events/voice.py +++ b/hikari/events/voice.py @@ -19,7 +19,7 @@ from __future__ import annotations -__all__: typing.Final[typing.List[str]] = ["VoiceStateUpdateEvent", "VoiceServerUpdateEvent"] +__all__: typing.Final[typing.Sequence[str]] = ["VoiceStateUpdateEvent", "VoiceServerUpdateEvent"] import typing diff --git a/hikari/impl/__init__.py b/hikari/impl/__init__.py index d2a6a355f1..3f8f88ab4f 100644 --- a/hikari/impl/__init__.py +++ b/hikari/impl/__init__.py @@ -25,7 +25,7 @@ from __future__ import annotations -__all__: typing.Final[typing.List[str]] = [] +__all__: typing.Final[typing.Sequence[str]] = [] # noinspection PyUnresolvedReferences import typing diff --git a/hikari/impl/bot.py b/hikari/impl/bot.py index 9157ce635f..66a12d91c5 100644 --- a/hikari/impl/bot.py +++ b/hikari/impl/bot.py @@ -19,7 +19,7 @@ from __future__ import annotations -__all__: typing.Final[typing.List[str]] = ["BotAppImpl"] +__all__: typing.Final[typing.Sequence[str]] = ["BotAppImpl"] import asyncio import inspect @@ -34,6 +34,7 @@ from hikari.impl import entity_factory as entity_factory_impl from hikari.impl import event_manager from hikari.impl import gateway_zookeeper +from hikari.impl import stateless_cache as stateless_cache_impl from hikari.models import presences from hikari.net import http_settings as http_settings_ from hikari.net import rate_limits @@ -118,6 +119,9 @@ class BotAppImpl(gateway_zookeeper.AbstractGatewayZookeeper, bot.IBotApp): The number of shards in the entire application. If left undefined along with `shard_ids`, then auto-sharding is used instead, which is the default. + stateless : bool + If `True`, the bot will not implement a cache, and will be considered + stateless. If `False`, then a cache will be used (this is the default). token : str The bot token to use. This should not start with a prefix such as `Bot `, but instead only contain the token itself. @@ -174,6 +178,7 @@ def __init__( rest_url: typing.Union[undefined.UndefinedType, str] = undefined.UNDEFINED, shard_ids: typing.Union[typing.Set[int], undefined.UndefinedType] = undefined.UNDEFINED, shard_count: typing.Union[int, undefined.UndefinedType] = undefined.UNDEFINED, + stateless: bool = False, thread_pool_executor: typing.Optional[concurrent.futures.Executor] = None, token: str, ) -> None: @@ -185,7 +190,12 @@ def __init__( config = http_settings_.HTTPSettings() if config is undefined.UNDEFINED else config - self._cache = cache_impl.InMemoryCacheComponentImpl(app=self) + if stateless: + self._cache = stateless_cache_impl.StatelessCacheImpl() + _LOGGER.info("this application will be stateless! Cache-based operations will be unavailable!") + else: + self._cache = cache_impl.InMemoryCacheComponentImpl(app=self) + self._config = config self._event_manager = event_manager.EventManagerImpl(app=self) self._entity_factory = entity_factory_impl.EntityFactoryComponentImpl(app=self) @@ -311,21 +321,14 @@ def __print_banner() -> None: top_line = "//" + ("=" * line_len) + r"\\" bottom_line = r"\\" + ("=" * line_len) + "//" - # Start on a newline, this prevents logging formatting messing with the - # layout of the banner; before we used \r but this probably isn't great - # since with systems like docker-compose that prepend to each line of - # logs, we end up with a mess. - _LOGGER.info( - "\n%s\n%s\n%s\n%s\n%s\n%s\n%s", - top_line, - version_str, - copyright_str, - impl_str, - doc_line, - guild_line, - bottom_line, + lines = "".join( + f"{line}\n" for line in (top_line, version_str, copyright_str, impl_str, doc_line, guild_line, bottom_line) ) + for handler in _LOGGER.handlers or ([logging.lastResort] if logging.lastResort is not None else []): + if isinstance(handler, logging.StreamHandler): + handler.stream.write(lines) + @staticmethod def __get_logging_format() -> str: # Modified from diff --git a/hikari/impl/cache.py b/hikari/impl/cache.py index c583330218..2f21c402fc 100644 --- a/hikari/impl/cache.py +++ b/hikari/impl/cache.py @@ -19,7 +19,7 @@ from __future__ import annotations -__all__: typing.Final[typing.List[str]] = ["InMemoryCacheComponentImpl"] +__all__: typing.Final[typing.Sequence[str]] = ["InMemoryCacheComponentImpl"] import typing diff --git a/hikari/impl/entity_factory.py b/hikari/impl/entity_factory.py index d5fc957f41..ec1eb2e5af 100644 --- a/hikari/impl/entity_factory.py +++ b/hikari/impl/entity_factory.py @@ -19,7 +19,7 @@ from __future__ import annotations -__all__: typing.Final[typing.List[str]] = ["EntityFactoryComponentImpl"] +__all__: typing.Final[typing.Sequence[str]] = ["EntityFactoryComponentImpl"] import datetime import typing diff --git a/hikari/impl/event_manager.py b/hikari/impl/event_manager.py index 420a32e351..e0e3bd50aa 100644 --- a/hikari/impl/event_manager.py +++ b/hikari/impl/event_manager.py @@ -19,7 +19,7 @@ from __future__ import annotations -__all__: typing.Final[typing.List[str]] = ["EventManagerImpl"] +__all__: typing.Final[typing.Sequence[str]] = ["EventManagerImpl"] import typing diff --git a/hikari/impl/event_manager_core.py b/hikari/impl/event_manager_core.py index e3567320c0..bc4af3b2f8 100644 --- a/hikari/impl/event_manager_core.py +++ b/hikari/impl/event_manager_core.py @@ -19,7 +19,7 @@ from __future__ import annotations -__all__: typing.Final[typing.List[str]] = ["EventManagerCoreComponent"] +__all__: typing.Final[typing.Sequence[str]] = ["EventManagerCoreComponent"] import asyncio import functools diff --git a/hikari/impl/gateway_zookeeper.py b/hikari/impl/gateway_zookeeper.py index 078db37189..98a039da5e 100644 --- a/hikari/impl/gateway_zookeeper.py +++ b/hikari/impl/gateway_zookeeper.py @@ -19,7 +19,7 @@ from __future__ import annotations -__all__: typing.Final[typing.List[str]] = ["AbstractGatewayZookeeper"] +__all__: typing.Final[typing.Sequence[str]] = ["AbstractGatewayZookeeper"] import abc import asyncio diff --git a/hikari/impl/rest.py b/hikari/impl/rest.py index becc83971b..6088bd37a3 100644 --- a/hikari/impl/rest.py +++ b/hikari/impl/rest.py @@ -23,7 +23,7 @@ from __future__ import annotations -__all__: typing.Final[typing.List[str]] = ["RESTClientFactoryImpl", "RESTClientImpl"] +__all__: typing.Final[typing.Sequence[str]] = ["RESTClientFactoryImpl", "RESTClientImpl"] import concurrent.futures import copy @@ -33,6 +33,7 @@ from hikari.api import rest as rest_api from hikari.impl import entity_factory as entity_factory_impl +from hikari.impl import stateless_cache from hikari.net import http_settings as http_settings_ from hikari.net import rate_limits from hikari.net import rest as rest_component @@ -88,7 +89,7 @@ def __init__( version: int = 6, ) -> None: self._logger = reflect.get_logger(self) - self._cache: cache_.ICacheComponent = NotImplemented + self._cache: cache_.ICacheComponent = stateless_cache.StatelessCacheImpl() self._entity_factory = entity_factory_impl.EntityFactoryComponentImpl(self) self._executor = None self._rest = rest_component.REST( diff --git a/hikari/impl/stateless_cache.py b/hikari/impl/stateless_cache.py new file mode 100644 index 0000000000..25460f1278 --- /dev/null +++ b/hikari/impl/stateless_cache.py @@ -0,0 +1,85 @@ +# -*- coding: utf-8 -*- +# Copyright © Nekoka.tt 2019-2020 +# +# This file is part of Hikari. +# +# Hikari is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Hikari is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Hikari. If not, see . +"""Barebones implementation of a cache that never stores anything. + +This is used to enable compatibility with REST applications and stateless +bots where desired. +""" +from __future__ import annotations + +__all__: typing.Final[typing.Sequence[str]] = [] + +import functools +import inspect +import typing + +from hikari import errors +from hikari.api import cache + + +def _fail(*_: typing.Any, **__: typing.Any) -> typing.NoReturn: + raise errors.HikariError( + "This component has not got a cache enabled, and is stateless. " + "Operations relying on a cache will always fail." + ) + + +# Generate a stub from the implementation dynamically. This way it is one less +# thing to keep up to date in the future. +@typing.no_type_check +def _generate(): + namespace = { + "__doc__": ( + "A stateless cache implementation that implements dummy operations for " + "each of the required attributes of a functional cache implementation. " + "Any descriptors will always return `NotImplemented`, and any methods " + "will always raise `hikari.errors.HikariError` when being invoked." + ), + "__init__": lambda *_, **__: None, + "__init_subclass__": staticmethod(lambda *args, **kwargs: _fail(*args, **kwargs)), + "__slots__": (), + "__module__": __name__, + } + + for name, member in inspect.getmembers(cache.ICacheComponent): + if name in namespace: + # Skip stuff we already have defined above. + continue + + if inspect.isabstract(member): + namespace[name] = NotImplemented + + if getattr(member, "__isabstractmethod__", False) is True: + # If we were to inspect an instance of the class, it would invoke the properties to + # get their value. Thus, it is not considered a safe thing to do to have them raise + # exceptions on invocation. + if hasattr(member, "__get__"): + doc = getattr(member, "__doc__", None) + new_member = property(lambda self: NotImplemented, _fail, lambda self: NotImplemented, doc) + else: + new_member = functools.wraps(member)(_fail) + + namespace[name] = new_member + + return typing.final(type("StatelessCacheImpl", (cache.ICacheComponent,), namespace)) + + +# noinspection PyTypeChecker +StatelessCacheImpl: typing.Final[typing.Type[cache.ICacheComponent]] = _generate() + +del _generate diff --git a/hikari/models/__init__.py b/hikari/models/__init__.py index d2b482a62c..f48e31707d 100644 --- a/hikari/models/__init__.py +++ b/hikari/models/__init__.py @@ -19,7 +19,7 @@ from __future__ import annotations -__all__: typing.Final[typing.List[str]] = [] +__all__: typing.Final[typing.Sequence[str]] = [] # noinspection PyUnresolvedReferences import typing diff --git a/hikari/models/applications.py b/hikari/models/applications.py index d2baaf6970..0c795ab512 100644 --- a/hikari/models/applications.py +++ b/hikari/models/applications.py @@ -19,7 +19,7 @@ from __future__ import annotations -__all__: typing.Final[typing.List[str]] = [ +__all__: typing.Final[typing.Sequence[str]] = [ "Application", "ConnectionVisibility", "OAuth2Scope", diff --git a/hikari/models/audit_logs.py b/hikari/models/audit_logs.py index 62fc921e1b..31472b5840 100644 --- a/hikari/models/audit_logs.py +++ b/hikari/models/audit_logs.py @@ -19,7 +19,7 @@ from __future__ import annotations -__all__: typing.Final[typing.List[str]] = [ +__all__: typing.Final[typing.Sequence[str]] = [ "AuditLog", "AuditLogChange", "AuditLogChangeKey", diff --git a/hikari/models/channels.py b/hikari/models/channels.py index c1239b3320..63b8efe414 100644 --- a/hikari/models/channels.py +++ b/hikari/models/channels.py @@ -19,7 +19,7 @@ from __future__ import annotations -__all__: typing.Final[typing.List[str]] = [ +__all__: typing.Final[typing.Sequence[str]] = [ "ChannelType", "PermissionOverwrite", "PermissionOverwriteType", diff --git a/hikari/models/colors.py b/hikari/models/colors.py index c47b8c14ef..9c6b72f5dc 100644 --- a/hikari/models/colors.py +++ b/hikari/models/colors.py @@ -19,7 +19,7 @@ from __future__ import annotations -__all__: typing.Final[typing.List[str]] = ["Color", "ColorLike"] +__all__: typing.Final[typing.Sequence[str]] = ["Color", "ColorLike"] import string import typing diff --git a/hikari/models/colours.py b/hikari/models/colours.py index 6a065dd2e9..98ea0c9135 100644 --- a/hikari/models/colours.py +++ b/hikari/models/colours.py @@ -19,7 +19,7 @@ from __future__ import annotations -__all__: typing.Final[typing.List[str]] = ["Colour", "ColourLike"] +__all__: typing.Final[typing.Sequence[str]] = ["Colour", "ColourLike"] # noinspection PyUnresolvedReferences import typing diff --git a/hikari/models/embeds.py b/hikari/models/embeds.py index b19aea70bd..ae9e4d4c54 100644 --- a/hikari/models/embeds.py +++ b/hikari/models/embeds.py @@ -19,7 +19,7 @@ from __future__ import annotations -__all__: typing.Final[typing.List[str]] = [ +__all__: typing.Final[typing.Sequence[str]] = [ "Embed", "EmbedResource", "EmbedVideo", diff --git a/hikari/models/emojis.py b/hikari/models/emojis.py index 0f5a2e992e..a1d2ac53cc 100644 --- a/hikari/models/emojis.py +++ b/hikari/models/emojis.py @@ -19,7 +19,7 @@ from __future__ import annotations -__all__: typing.Final[typing.List[str]] = ["Emoji", "UnicodeEmoji", "CustomEmoji", "KnownCustomEmoji"] +__all__: typing.Final[typing.Sequence[str]] = ["Emoji", "UnicodeEmoji", "CustomEmoji", "KnownCustomEmoji"] import abc import typing diff --git a/hikari/models/gateway.py b/hikari/models/gateway.py index 98457a03a3..33ce06acdf 100644 --- a/hikari/models/gateway.py +++ b/hikari/models/gateway.py @@ -19,7 +19,7 @@ from __future__ import annotations -__all__: typing.Final[typing.List[str]] = ["GatewayBot", "SessionStartLimit"] +__all__: typing.Final[typing.Sequence[str]] = ["GatewayBot", "SessionStartLimit"] import typing diff --git a/hikari/models/guilds.py b/hikari/models/guilds.py index e528b6ef66..566e4ae783 100644 --- a/hikari/models/guilds.py +++ b/hikari/models/guilds.py @@ -19,7 +19,7 @@ from __future__ import annotations -__all__: typing.Final[typing.List[str]] = [ +__all__: typing.Final[typing.Sequence[str]] = [ "Guild", "GuildWidget", "Role", diff --git a/hikari/models/intents.py b/hikari/models/intents.py index ef92df63e1..58da308d63 100644 --- a/hikari/models/intents.py +++ b/hikari/models/intents.py @@ -19,7 +19,7 @@ from __future__ import annotations -__all__: typing.Final[typing.List[str]] = ["Intent"] +__all__: typing.Final[typing.Sequence[str]] = ["Intent"] import enum diff --git a/hikari/models/invites.py b/hikari/models/invites.py index 3b98a9fbe9..b7ebec93c4 100644 --- a/hikari/models/invites.py +++ b/hikari/models/invites.py @@ -19,7 +19,13 @@ from __future__ import annotations -__all__: typing.Final[typing.List[str]] = ["TargetUserType", "VanityURL", "InviteGuild", "Invite", "InviteWithMetadata"] +__all__: typing.Final[typing.Sequence[str]] = [ + "TargetUserType", + "VanityURL", + "InviteGuild", + "Invite", + "InviteWithMetadata", +] import enum import typing diff --git a/hikari/models/messages.py b/hikari/models/messages.py index 793ccc056a..538e133c5d 100644 --- a/hikari/models/messages.py +++ b/hikari/models/messages.py @@ -19,7 +19,7 @@ from __future__ import annotations -__all__: typing.Final[typing.List[str]] = [ +__all__: typing.Final[typing.Sequence[str]] = [ "MessageType", "MessageFlag", "MessageActivityType", diff --git a/hikari/models/permissions.py b/hikari/models/permissions.py index c62da8cece..0f0c27a3d8 100644 --- a/hikari/models/permissions.py +++ b/hikari/models/permissions.py @@ -19,7 +19,7 @@ from __future__ import annotations -__all__: typing.Final[typing.List[str]] = ["Permission"] +__all__: typing.Final[typing.Sequence[str]] = ["Permission"] import enum diff --git a/hikari/models/presences.py b/hikari/models/presences.py index 9aff7fc7ef..efca97cf40 100644 --- a/hikari/models/presences.py +++ b/hikari/models/presences.py @@ -19,7 +19,7 @@ from __future__ import annotations -__all__: typing.Final[typing.List[str]] = [ +__all__: typing.Final[typing.Sequence[str]] = [ "Activity", "ActivityAssets", "ActivityFlag", diff --git a/hikari/models/users.py b/hikari/models/users.py index 922fb6ce5d..663cdc9b8e 100644 --- a/hikari/models/users.py +++ b/hikari/models/users.py @@ -19,7 +19,7 @@ from __future__ import annotations -__all__: typing.Final[typing.List[str]] = ["User", "OwnUser", "UserFlag", "PremiumType"] +__all__: typing.Final[typing.Sequence[str]] = ["User", "OwnUser", "UserFlag", "PremiumType"] import enum import typing diff --git a/hikari/models/voices.py b/hikari/models/voices.py index 4d95cfab77..8d7ccfb7bf 100644 --- a/hikari/models/voices.py +++ b/hikari/models/voices.py @@ -19,7 +19,7 @@ from __future__ import annotations -__all__: typing.Final[typing.List[str]] = ["VoiceRegion", "VoiceState"] +__all__: typing.Final[typing.Sequence[str]] = ["VoiceRegion", "VoiceState"] import typing diff --git a/hikari/models/webhooks.py b/hikari/models/webhooks.py index 17fc1a5e0c..bcd9258d4b 100644 --- a/hikari/models/webhooks.py +++ b/hikari/models/webhooks.py @@ -19,7 +19,7 @@ from __future__ import annotations -__all__: typing.Final[typing.List[str]] = ["WebhookType", "Webhook"] +__all__: typing.Final[typing.Sequence[str]] = ["WebhookType", "Webhook"] import enum import typing diff --git a/hikari/net/__init__.py b/hikari/net/__init__.py index 1cff1090f3..a617151012 100644 --- a/hikari/net/__init__.py +++ b/hikari/net/__init__.py @@ -19,7 +19,7 @@ from __future__ import annotations -__all__: typing.Final[typing.List[str]] = [] +__all__: typing.Final[typing.Sequence[str]] = [] # noinspection PyUnresolvedReferences import typing diff --git a/hikari/net/buckets.py b/hikari/net/buckets.py index 956b3ef259..cdef2f2dc9 100644 --- a/hikari/net/buckets.py +++ b/hikari/net/buckets.py @@ -203,7 +203,7 @@ from __future__ import annotations -__all__: typing.Final[typing.List[str]] = ["UNKNOWN_HASH", "RESTBucket", "RESTBucketManager"] +__all__: typing.Final[typing.Sequence[str]] = ["UNKNOWN_HASH", "RESTBucket", "RESTBucketManager"] import asyncio import datetime diff --git a/hikari/net/gateway.py b/hikari/net/gateway.py index 72a3731a67..1c828f9b91 100644 --- a/hikari/net/gateway.py +++ b/hikari/net/gateway.py @@ -19,7 +19,7 @@ from __future__ import annotations -__all__: typing.Final[typing.List[str]] = ["Gateway"] +__all__: typing.Final[typing.Sequence[str]] = ["Gateway"] import asyncio import enum diff --git a/hikari/net/http_client.py b/hikari/net/http_client.py index 19d61e38ad..93ab3d96cd 100644 --- a/hikari/net/http_client.py +++ b/hikari/net/http_client.py @@ -18,7 +18,7 @@ """Base functionality for any HTTP-based network component.""" from __future__ import annotations -__all__: typing.Final[typing.List[str]] = ["HTTPClient"] +__all__: typing.Final[typing.Sequence[str]] = ["HTTPClient"] import abc import http diff --git a/hikari/net/http_settings.py b/hikari/net/http_settings.py index f667cf00f4..3cc627d318 100644 --- a/hikari/net/http_settings.py +++ b/hikari/net/http_settings.py @@ -19,7 +19,7 @@ from __future__ import annotations -__all__: typing.Final[typing.List[str]] = ["HTTPSettings"] +__all__: typing.Final[typing.Sequence[str]] = ["HTTPSettings"] import typing diff --git a/hikari/net/iterators.py b/hikari/net/iterators.py index 3d5fa509fa..b00c6a51f3 100644 --- a/hikari/net/iterators.py +++ b/hikari/net/iterators.py @@ -18,7 +18,7 @@ """Lazy iterators for data that requires repeated API calls to retrieve.""" from __future__ import annotations -__all__: typing.Final[typing.List[str]] = ["LazyIterator"] +__all__: typing.Final[typing.Sequence[str]] = ["LazyIterator"] import abc import operator diff --git a/hikari/net/rate_limits.py b/hikari/net/rate_limits.py index ff6c0f0a74..0d8bbb7696 100644 --- a/hikari/net/rate_limits.py +++ b/hikari/net/rate_limits.py @@ -22,7 +22,7 @@ from __future__ import annotations -__all__: typing.Final[typing.List[str]] = [ +__all__: typing.Final[typing.Sequence[str]] = [ "BaseRateLimiter", "BurstRateLimiter", "ManualRateLimiter", diff --git a/hikari/net/rest.py b/hikari/net/rest.py index cdf8edbb83..996f8b67a4 100644 --- a/hikari/net/rest.py +++ b/hikari/net/rest.py @@ -19,7 +19,7 @@ from __future__ import annotations -__all__: typing.Final[typing.List[str]] = ["REST"] +__all__: typing.Final[typing.Sequence[str]] = ["REST"] import asyncio import contextlib diff --git a/hikari/net/rest_utils.py b/hikari/net/rest_utils.py index be366a9c88..f8d0471974 100644 --- a/hikari/net/rest_utils.py +++ b/hikari/net/rest_utils.py @@ -21,7 +21,7 @@ """ from __future__ import annotations -__all__: typing.Final[typing.List[str]] = ["TypingIndicator", "GuildBuilder"] +__all__: typing.Final[typing.Sequence[str]] = ["TypingIndicator", "GuildBuilder"] import asyncio import contextlib diff --git a/hikari/net/routes.py b/hikari/net/routes.py index deda10602e..2a6c202760 100644 --- a/hikari/net/routes.py +++ b/hikari/net/routes.py @@ -19,7 +19,7 @@ from __future__ import annotations -__all__: typing.Final[typing.List[str]] = ["CompiledRoute", "Route"] +__all__: typing.Final[typing.Sequence[str]] = ["CompiledRoute", "Route"] import re import typing diff --git a/hikari/net/strings.py b/hikari/net/strings.py index ea0fa4ff32..9bdc057107 100644 --- a/hikari/net/strings.py +++ b/hikari/net/strings.py @@ -72,4 +72,4 @@ OAUTH2_API_URL: typing.Final[str] = f"{REST_API_URL}/oauth2" CDN_URL: typing.Final[str] = "https://cdn.discordapp.com" -__all__: typing.Final[typing.List[str]] = [attr for attr in globals() if not any(c.islower() for c in attr)] +__all__: typing.Final[typing.Sequence[str]] = [attr for attr in globals() if not any(c.islower() for c in attr)] diff --git a/hikari/utilities/__init__.py b/hikari/utilities/__init__.py index 394dc66bb8..a959fa75d0 100644 --- a/hikari/utilities/__init__.py +++ b/hikari/utilities/__init__.py @@ -19,7 +19,7 @@ from __future__ import annotations -__all__: typing.Final[typing.List[str]] = [] +__all__: typing.Final[typing.Sequence[str]] = [] # noinspection PyUnresolvedReferences import typing diff --git a/hikari/utilities/aio.py b/hikari/utilities/aio.py index 50b54e9ee9..f3dfbf431e 100644 --- a/hikari/utilities/aio.py +++ b/hikari/utilities/aio.py @@ -19,7 +19,7 @@ from __future__ import annotations -__all__: typing.Final[typing.List[str]] = ["completed_future", "is_async_iterator", "is_async_iterable"] +__all__: typing.Final[typing.Sequence[str]] = ["completed_future", "is_async_iterator", "is_async_iterable"] import asyncio import inspect diff --git a/hikari/utilities/cdn.py b/hikari/utilities/cdn.py index 7f0325d1aa..f2d876ea56 100644 --- a/hikari/utilities/cdn.py +++ b/hikari/utilities/cdn.py @@ -19,7 +19,7 @@ from __future__ import annotations -__all__: typing.Final[typing.List[str]] = ["generate_cdn_url", "get_default_avatar_url", "get_default_avatar_index"] +__all__: typing.Final[typing.Sequence[str]] = ["generate_cdn_url", "get_default_avatar_url", "get_default_avatar_index"] import typing import urllib.parse diff --git a/hikari/utilities/data_binding.py b/hikari/utilities/data_binding.py index 3071dd59f6..50cc6abc57 100644 --- a/hikari/utilities/data_binding.py +++ b/hikari/utilities/data_binding.py @@ -18,7 +18,7 @@ """Data binding utilities.""" from __future__ import annotations -__all__: typing.Final[typing.List[str]] = [ +__all__: typing.Final[typing.Sequence[str]] = [ "Headers", "Query", "JSONObject", diff --git a/hikari/utilities/date.py b/hikari/utilities/date.py index 0424bb0f19..7cbd8512e3 100644 --- a/hikari/utilities/date.py +++ b/hikari/utilities/date.py @@ -19,7 +19,7 @@ from __future__ import annotations -__all__: typing.Final[typing.List[str]] = [ +__all__: typing.Final[typing.Sequence[str]] = [ "DISCORD_EPOCH", "rfc7231_datetime_string_to_datetime", "datetime_to_discord_epoch", diff --git a/hikari/utilities/reflect.py b/hikari/utilities/reflect.py index c23f8e87a3..70b2e90224 100644 --- a/hikari/utilities/reflect.py +++ b/hikari/utilities/reflect.py @@ -19,7 +19,7 @@ from __future__ import annotations -__all__: typing.Final[typing.List[str]] = ["resolve_signature", "EMPTY", "get_logger"] +__all__: typing.Final[typing.Sequence[str]] = ["resolve_signature", "EMPTY", "get_logger"] import inspect import logging diff --git a/hikari/utilities/snowflake.py b/hikari/utilities/snowflake.py index 6061a38c61..f6b97d0273 100644 --- a/hikari/utilities/snowflake.py +++ b/hikari/utilities/snowflake.py @@ -19,7 +19,7 @@ from __future__ import annotations -__all__: typing.Final[typing.List[str]] = ["Snowflake"] +__all__: typing.Final[typing.Sequence[str]] = ["Snowflake"] import abc import datetime diff --git a/hikari/utilities/undefined.py b/hikari/utilities/undefined.py index cc6a3b4420..682b877867 100644 --- a/hikari/utilities/undefined.py +++ b/hikari/utilities/undefined.py @@ -19,7 +19,7 @@ from __future__ import annotations -__all__: typing.Final[typing.List[str]] = ["UndefinedType", "UNDEFINED"] +__all__: typing.Final[typing.Sequence[str]] = ["UndefinedType", "UNDEFINED"] import enum From a36ade80b6046ded3b0606a0da7936a013e43921 Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Thu, 25 Jun 2020 12:54:26 +0100 Subject: [PATCH 574/922] Simplified logger names. --- hikari/cli.py | 2 -- hikari/impl/bot.py | 4 ++-- hikari/impl/event_manager_core.py | 2 +- hikari/impl/gateway_zookeeper.py | 2 +- hikari/impl/rest.py | 2 -- hikari/net/buckets.py | 2 +- hikari/net/gateway.py | 3 +-- hikari/net/http_client.py | 3 +-- hikari/net/rate_limits.py | 2 +- hikari/net/rest.py | 2 +- 10 files changed, 9 insertions(+), 15 deletions(-) diff --git a/hikari/cli.py b/hikari/cli.py index d64b51c86b..8abc8ae8e6 100644 --- a/hikari/cli.py +++ b/hikari/cli.py @@ -23,8 +23,6 @@ import os import platform import sys - -# noinspection PyUnresolvedReferences import typing from hikari import _about diff --git a/hikari/impl/bot.py b/hikari/impl/bot.py index 66a12d91c5..ac6ec99dff 100644 --- a/hikari/impl/bot.py +++ b/hikari/impl/bot.py @@ -361,6 +361,6 @@ def __get_logging_format() -> str: blue = gray = green = red = yellow = default = "" return ( - f"{red}%(levelname)-8.8s {yellow}%(name)-30.30s {green}#%(lineno)-4d {blue}%(asctime)23.23s" - f"{default}:: {gray}%(message)s{default}" + f"{red}%(levelname)-1.1s {yellow}%(name)-25.25s {green}#%(lineno)-4d {blue}%(asctime)23.23s" + f"{default} :: {gray}%(message)s{default}" ) diff --git a/hikari/impl/event_manager_core.py b/hikari/impl/event_manager_core.py index bc4af3b2f8..2a9e88caee 100644 --- a/hikari/impl/event_manager_core.py +++ b/hikari/impl/event_manager_core.py @@ -40,7 +40,7 @@ from hikari.api import rest -_LOGGER: typing.Final[logging.Logger] = logging.getLogger(__name__) +_LOGGER: typing.Final[logging.Logger] = logging.getLogger("hikari") if typing.TYPE_CHECKING: diff --git a/hikari/impl/gateway_zookeeper.py b/hikari/impl/gateway_zookeeper.py index 98a039da5e..7c31564b27 100644 --- a/hikari/impl/gateway_zookeeper.py +++ b/hikari/impl/gateway_zookeeper.py @@ -45,7 +45,7 @@ from hikari.models import presences -_LOGGER: typing.Final[logging.Logger] = logging.getLogger(__name__) +_LOGGER: typing.Final[logging.Logger] = logging.getLogger("hikari") class AbstractGatewayZookeeper(gateway_zookeeper.IGatewayZookeeperApp, abc.ABC): diff --git a/hikari/impl/rest.py b/hikari/impl/rest.py index 6088bd37a3..8b4c7cd3f7 100644 --- a/hikari/impl/rest.py +++ b/hikari/impl/rest.py @@ -38,7 +38,6 @@ from hikari.net import rate_limits from hikari.net import rest as rest_component from hikari.net import strings -from hikari.utilities import reflect from hikari.utilities import undefined if typing.TYPE_CHECKING: @@ -88,7 +87,6 @@ def __init__( url: typing.Union[undefined.UndefinedType, str] = undefined.UNDEFINED, version: int = 6, ) -> None: - self._logger = reflect.get_logger(self) self._cache: cache_.ICacheComponent = stateless_cache.StatelessCacheImpl() self._entity_factory = entity_factory_impl.EntityFactoryComponentImpl(self) self._executor = None diff --git a/hikari/net/buckets.py b/hikari/net/buckets.py index cdef2f2dc9..ad23a97dd0 100644 --- a/hikari/net/buckets.py +++ b/hikari/net/buckets.py @@ -220,7 +220,7 @@ """The hash used for an unknown bucket that has not yet been resolved.""" -_LOGGER: typing.Final[logging.Logger] = logging.getLogger(__name__) +_LOGGER: typing.Final[logging.Logger] = logging.getLogger("hikari.net.rest") class RESTBucket(rate_limits.WindowedBurstRateLimiter): diff --git a/hikari/net/gateway.py b/hikari/net/gateway.py index 1c828f9b91..216d8ecdca 100644 --- a/hikari/net/gateway.py +++ b/hikari/net/gateway.py @@ -40,7 +40,6 @@ from hikari.net import rate_limits from hikari.net import strings from hikari.utilities import data_binding -from hikari.utilities import reflect from hikari.utilities import undefined if typing.TYPE_CHECKING: @@ -200,7 +199,7 @@ def __init__( self._intents: typing.Optional[intents_.Intent] = intents self._is_afk: typing.Union[undefined.UndefinedType, bool] = initial_is_afk self._last_run_started_at = float("nan") - self._logger = reflect.get_logger(self, str(shard_id)) + self._logger = logging.getLogger(f"hikari.net.gateway.{shard_id}") self._request_close_event = asyncio.Event() self._seq: typing.Optional[str] = None self._shard_id: int = shard_id diff --git a/hikari/net/http_client.py b/hikari/net/http_client.py index 93ab3d96cd..23d24dde61 100644 --- a/hikari/net/http_client.py +++ b/hikari/net/http_client.py @@ -37,7 +37,6 @@ from hikari.net import http_settings from hikari.utilities import data_binding - try: # noinspection PyProtectedMember RequestContextManager = aiohttp.client._RequestContextManager @@ -50,7 +49,7 @@ RequestContextManager = typing.Any # type: ignore -_LOGGER: typing.Final[logging.Logger] = logging.getLogger(__name__) +_LOGGER: typing.Final[logging.Logger] = logging.getLogger("hikari.net") class HTTPClient(abc.ABC): diff --git a/hikari/net/rate_limits.py b/hikari/net/rate_limits.py index 0d8bbb7696..4f1eb2eae2 100644 --- a/hikari/net/rate_limits.py +++ b/hikari/net/rate_limits.py @@ -41,7 +41,7 @@ import types -_LOGGER: typing.Final[logging.Logger] = logging.getLogger(__name__) +_LOGGER: typing.Final[logging.Logger] = logging.getLogger("hikari.net.ratelimits") class BaseRateLimiter(abc.ABC): diff --git a/hikari/net/rest.py b/hikari/net/rest.py index 996f8b67a4..96181ef691 100644 --- a/hikari/net/rest.py +++ b/hikari/net/rest.py @@ -67,7 +67,7 @@ from hikari.models import webhooks -_LOGGER: typing.Final[logging.Logger] = logging.getLogger(__name__) +_LOGGER: typing.Final[logging.Logger] = logging.getLogger("hikari.net.rest") # TODO: make a mechanism to allow me to share the same client session but From 1beba0f202e0f7d862e3dc2711d3de77dff0f774 Mon Sep 17 00:00:00 2001 From: davfsa Date: Thu, 25 Jun 2020 15:31:50 +0200 Subject: [PATCH 575/922] Fixed documentation --- docs/documentation.mako | 24 ++++------- hikari/__init__.py | 2 +- hikari/api/entity_factory.py | 74 ++++++++++++++++----------------- hikari/api/event_consumer.py | 2 +- hikari/api/event_dispatcher.py | 22 +++++----- hikari/api/gateway_zookeeper.py | 4 +- hikari/api/rest.py | 2 +- hikari/models/webhooks.py | 1 + hikari/utilities/snowflake.py | 2 +- 9 files changed, 64 insertions(+), 69 deletions(-) diff --git a/docs/documentation.mako b/docs/documentation.mako index 5c468aa9e0..4b0d08aed8 100644 --- a/docs/documentation.mako +++ b/docs/documentation.mako @@ -28,7 +28,7 @@ return if fqn not in located_external_refs: - print("attempting to find intersphinx reference for", fqn) + # print("attempting to find intersphinx reference for", fqn) for base_url, inv in inventories.items(): for obj in inv.values(): if isinstance(obj, dict) and obj["name"] == fqn: @@ -37,7 +37,7 @@ uri_frag = uri_frag[:-1] + fqn url = base_url + uri_frag - print("discovered", fqn, "at", url) + # print("discovered", fqn, "at", url) located_external_refs[fqn] = url break try: @@ -116,25 +116,21 @@ if getattr(dobj.obj, "__isabstractmethod__", False): prefix = f"{QUAL_ABC} " - else: - prefix = "" - prefix = "" + prefix + qual + "" + prefix = "" + prefix + qual + " " elif isinstance(dobj, pdoc.Variable): if getattr(dobj.obj, "__isabstractmethod__", False): prefix = f"{QUAL_ABC} " - else: - prefix = "" - if hasattr(dobj.cls, "obj") and (descriptor := dobj.cls.obj.__dict__.get(dobj.name)) and isinstance(descriptor, property): - prefix = f"{prefix}{QUAL_PROPERTY}" + if hasattr(dobj.obj, "__get__"): + prefix = f"{prefix}{QUAL_PROPERTY} " elif dobj.module.name == "typing" or dobj.docstring and dobj.docstring.casefold().startswith(("type hint", "typehint", "type alias")): - prefix = F"{prefix}{QUAL_TYPEHINT} " + prefix = f"{prefix}{QUAL_TYPEHINT} " elif all(not c.isalpha() or c.isupper() for c in dobj.name): - prefix = f"{prefix}{QUAL_CONST}" + prefix = f"{prefix}{QUAL_CONST} " else: - prefix = f"{prefix}{QUAL_VAR}" + prefix = f"{prefix}{QUAL_VAR} " elif isinstance(dobj, pdoc.Class): qual = "" @@ -205,7 +201,7 @@ anchor = "" if not anchor else f'id="{dobj.refname}"' - return '{} {}'.format(prefix, dobj.name + " -- " + glimpse(dobj.docstring), url, anchor, class_str, name) + return '{}{}'.format(prefix, dobj.name + " -- " + glimpse(dobj.docstring), url, anchor, class_str, name) def simple_name(s): _, _, name = s.rpartition(".") @@ -221,8 +217,6 @@ if annot: annot = ' ' + sep + '\N{NBSP}' + annot - annot = annot.replace("[ ", "[") # FIXME: whatever causes space between [ and link so I don't have to use this hack. - return annot def to_html(text): diff --git a/hikari/__init__.py b/hikari/__init__.py index 52e160a80f..12b43dd431 100644 --- a/hikari/__init__.py +++ b/hikari/__init__.py @@ -19,7 +19,7 @@ from __future__ import annotations -__all__: typing.Final[typing.Sequence[str]] = ["RESTClient", "Bot"] +__all__: typing.Final[typing.Sequence[str]] = ["RESTClientFactory", "Bot"] # noinspection PyUnresolvedReferences import typing diff --git a/hikari/api/entity_factory.py b/hikari/api/entity_factory.py index 5843303ced..8656ff1689 100644 --- a/hikari/api/entity_factory.py +++ b/hikari/api/entity_factory.py @@ -750,7 +750,7 @@ def deserialize_channel_create_event(self, payload: data_binding.JSONObject) -> Parameters ---------- - payload : Mapping[str, Any] + payload : typing.Mapping[str, typing.Any] The dict payload to parse. Returns @@ -765,7 +765,7 @@ def deserialize_channel_update_event(self, payload: data_binding.JSONObject) -> Parameters ---------- - payload : Mapping[str, Any] + payload : typing.Mapping[str, typing.Any] The dict payload to parse. Returns @@ -780,7 +780,7 @@ def deserialize_channel_delete_event(self, payload: data_binding.JSONObject) -> Parameters ---------- - payload : Mapping[str, Any] + payload : typing.Mapping[str, typing.Any] The dict payload to parse. Returns @@ -797,7 +797,7 @@ def deserialize_channel_pins_update_event( Parameters ---------- - payload : Mapping[str, Any] + payload : typing.Mapping[str, typing.Any] The dict payload to parse. Returns @@ -812,7 +812,7 @@ def deserialize_webhook_update_event(self, payload: data_binding.JSONObject) -> Parameters ---------- - payload : Mapping[str, Any] + payload : typing.Mapping[str, typing.Any] The dict payload to parse. Returns @@ -827,7 +827,7 @@ def deserialize_typing_start_event(self, payload: data_binding.JSONObject) -> ch Parameters ---------- - payload : Mapping[str, Any] + payload : typing.Mapping[str, typing.Any] The dict payload to parse. Returns @@ -842,7 +842,7 @@ def deserialize_invite_create_event(self, payload: data_binding.JSONObject) -> c Parameters ---------- - payload : Mapping[str, Any] + payload : typing.Mapping[str, typing.Any] The dict payload to parse. Returns @@ -857,7 +857,7 @@ def deserialize_invite_delete_event(self, payload: data_binding.JSONObject) -> c Parameters ---------- - payload : Mapping[str, Any] + payload : typing.Mapping[str, typing.Any] The dict payload to parse. Returns @@ -876,7 +876,7 @@ def deserialize_guild_create_event(self, payload: data_binding.JSONObject) -> gu Parameters ---------- - payload : Mapping[str, Any] + payload : typing.Mapping[str, typing.Any] The dict payload to parse. Returns @@ -891,7 +891,7 @@ def deserialize_guild_update_event(self, payload: data_binding.JSONObject) -> gu Parameters ---------- - payload : Mapping[str, Any] + payload : typing.Mapping[str, typing.Any] The dict payload to parse. Returns @@ -906,7 +906,7 @@ def deserialize_guild_leave_event(self, payload: data_binding.JSONObject) -> gui Parameters ---------- - payload : Mapping[str, Any] + payload : typing.Mapping[str, typing.Any] The dict payload to parse. Returns @@ -923,7 +923,7 @@ def deserialize_guild_unavailable_event( Parameters ---------- - payload : Mapping[str, Any] + payload : typing.Mapping[str, typing.Any] The dict payload to parse. Returns @@ -938,7 +938,7 @@ def deserialize_guild_ban_add_event(self, payload: data_binding.JSONObject) -> g Parameters ---------- - payload : Mapping[str, Any] + payload : typing.Mapping[str, typing.Any] The dict payload to parse. Returns @@ -953,7 +953,7 @@ def deserialize_guild_ban_remove_event(self, payload: data_binding.JSONObject) - Parameters ---------- - payload : Mapping[str, Any] + payload : typing.Mapping[str, typing.Any] The dict payload to parse. Returns @@ -970,7 +970,7 @@ def deserialize_guild_emojis_update_event( Parameters ---------- - payload : Mapping[str, Any] + payload : typing.Mapping[str, typing.Any] The dict payload to parse. Returns @@ -987,7 +987,7 @@ def deserialize_guild_integrations_update_event( Parameters ---------- - payload : Mapping[str, Any] + payload : typing.Mapping[str, typing.Any] The dict payload to parse. Returns @@ -1002,7 +1002,7 @@ def deserialize_guild_member_add_event(self, payload: data_binding.JSONObject) - Parameters ---------- - payload : Mapping[str, Any] + payload : typing.Mapping[str, typing.Any] The dict payload to parse. Returns @@ -1019,7 +1019,7 @@ def deserialize_guild_member_update_event( Parameters ---------- - payload : Mapping[str, Any] + payload : typing.Mapping[str, typing.Any] The dict payload to parse. Returns @@ -1036,7 +1036,7 @@ def deserialize_guild_member_remove_event( Parameters ---------- - payload : Mapping[str, Any] + payload : typing.Mapping[str, typing.Any] The dict payload to parse. Returns @@ -1053,7 +1053,7 @@ def deserialize_guild_role_create_event( Parameters ---------- - payload : Mapping[str, Any] + payload : typing.Mapping[str, typing.Any] The dict payload to parse. Returns @@ -1070,7 +1070,7 @@ def deserialize_guild_role_update_event( Parameters ---------- - payload : Mapping[str, Any] + payload : typing.Mapping[str, typing.Any] The dict payload to parse. Returns @@ -1087,7 +1087,7 @@ def deserialize_guild_role_delete_event( Parameters ---------- - payload : Mapping[str, Any] + payload : typing.Mapping[str, typing.Any] The dict payload to parse. Returns @@ -1102,7 +1102,7 @@ def deserialize_presence_update_event(self, payload: data_binding.JSONObject) -> Parameters ---------- - payload : Mapping[str, Any] + payload : typing.Mapping[str, typing.Any] The dict payload to parse. Returns @@ -1121,7 +1121,7 @@ def deserialize_message_create_event(self, payload: data_binding.JSONObject) -> Parameters ---------- - payload : Mapping[str, Any] + payload : typing.Mapping[str, typing.Any] The dict payload to parse. Returns @@ -1136,7 +1136,7 @@ def deserialize_message_update_event(self, payload: data_binding.JSONObject) -> Parameters ---------- - payload : Mapping[str, Any] + payload : typing.Mapping[str, typing.Any] The dict payload to parse. Returns @@ -1151,7 +1151,7 @@ def deserialize_message_delete_event(self, payload: data_binding.JSONObject) -> Parameters ---------- - payload : Mapping[str, Any] + payload : typing.Mapping[str, typing.Any] The dict payload to parse. Returns @@ -1168,7 +1168,7 @@ def deserialize_message_delete_bulk_event( Parameters ---------- - payload : Mapping[str, Any] + payload : typing.Mapping[str, typing.Any] The dict payload to parse. Returns @@ -1185,7 +1185,7 @@ def deserialize_message_reaction_add_event( Parameters ---------- - payload : Mapping[str, Any] + payload : typing.Mapping[str, typing.Any] The dict payload to parse. Returns @@ -1202,7 +1202,7 @@ def deserialize_message_reaction_remove_event( Parameters ---------- - payload : Mapping[str, Any] + payload : typing.Mapping[str, typing.Any] The dict payload to parse. Returns @@ -1219,7 +1219,7 @@ def deserialize_message_reaction_remove_all_event( Parameters ---------- - payload : Mapping[str, Any] + payload : typing.Mapping[str, typing.Any] The dict payload to parse. Returns @@ -1236,7 +1236,7 @@ def deserialize_message_reaction_remove_emoji_event( Parameters ---------- - payload : Mapping[str, Any] + payload : typing.Mapping[str, typing.Any] The dict payload to parse. Returns @@ -1259,7 +1259,7 @@ def deserialize_ready_event( ---------- shard : hikari.net.gateway.Gateway The shard that was ready. - payload : Mapping[str, Any] + payload : typing.Mapping[str, typing.Any] The dict payload to parse. Returns @@ -1274,7 +1274,7 @@ def deserialize_own_user_update_event(self, payload: data_binding.JSONObject) -> Parameters ---------- - payload : Mapping[str, Any] + payload : typing.Mapping[str, typing.Any] The dict payload to parse. Returns @@ -1295,7 +1295,7 @@ def deserialize_voice_state_update_event( Parameters ---------- - payload : Mapping[str, Any] + payload : typing.Mapping[str, typing.Any] The dict payload to parse. Returns @@ -1312,7 +1312,7 @@ def deserialize_voice_server_update_event( Parameters ---------- - payload : Mapping[str, Any] + payload : typing.Mapping[str, typing.Any] The dict payload to parse. Returns @@ -1367,9 +1367,9 @@ def serialize_gateway_voice_state_update( Parameters ---------- - guild : hikari.models.guilds.Guild or hikari.utilities.snowflake.UniqueObject + guild : hikari.models.guilds.Guild or hikari.utilities.snowflake.Snowflake or str or int The guild to update the voice state in. - channel : hikari.models.channels.GuildVoiceChannel or hikari.utilities.snowflake.UniqueObject or None + channel : hikari.models.channels.GuildVoiceChannel or hikari.utilities.snowflake.Snowflake or str or int or None The voice channel to change to, or `None` if attempting to leave a voice channel and disconnect entirely. self_mute : bool diff --git a/hikari/api/event_consumer.py b/hikari/api/event_consumer.py index 5368d1dad4..359de03741 100644 --- a/hikari/api/event_consumer.py +++ b/hikari/api/event_consumer.py @@ -79,6 +79,6 @@ def event_consumer(self) -> IEventConsumerComponent: Returns ------- - IEventConsumerComponent + hikari.api.event_consumer.IEventConsumerComponent The event consumer implementation in-use. """ diff --git a/hikari/api/event_dispatcher.py b/hikari/api/event_dispatcher.py index 991aff9158..4b1dc90d95 100644 --- a/hikari/api/event_dispatcher.py +++ b/hikari/api/event_dispatcher.py @@ -124,8 +124,8 @@ async def on_everyone_mentioned(event): See Also -------- - IEventDispatcherBase.subscribe - IEventDispatcherBase.wait_for + `hikari.api.event_dispatcher.IEventDispatcherBase.subscribe` + `hikari.api.event_dispatcher.IEventDispatcherBase.wait_for` """ @abc.abstractmethod @@ -170,8 +170,8 @@ async def on_message(event): See Also -------- - IEventDispatcherBase.listen - IEventDispatcherBase.wait_for + `hikari.api.event_dispatcher.IEventDispatcherBase.listen` + `hikari.api.event_dispatcher.IEventDispatcherBase.wait_for` """ @abc.abstractmethod @@ -229,10 +229,10 @@ def listen( See Also -------- - IEventDispatcherBase.dispatch - IEventDispatcherBase.subscribe - IEventDispatcherBase.unsubscribe - IEventDispatcherBase.wait_for + `hikari.api.event_dispatcher.IEventDispatcherBase.dispatch` + `hikari.api.event_dispatcher.IEventDispatcherBase.subscribe` + `hikari.api.event_dispatcher.IEventDispatcherBase.unsubscribe` + `hikari.api.event_dispatcher.IEventDispatcherBase.wait_for` """ @abc.abstractmethod @@ -270,9 +270,9 @@ async def wait_for( See Also -------- - IEventDispatcherBase.listen - IEventDispatcherBase.subscribe - IEventDispatcherBase.dispatch + `hikari.api.event_dispatcher.IEventDispatcherBase.listen` + `hikari.api.event_dispatcher.IEventDispatcherBase.subscribe` + `hikari.api.event_dispatcher.IEventDispatcherBase.dispatch` """ diff --git a/hikari/api/gateway_zookeeper.py b/hikari/api/gateway_zookeeper.py index b403a8e227..f6871c32cf 100644 --- a/hikari/api/gateway_zookeeper.py +++ b/hikari/api/gateway_zookeeper.py @@ -113,8 +113,8 @@ async def update_presence( !!! note If you wish to update a presence for a specific shard, you can do - this by using the `gateway_shards` `typing.Mapping` to find the - shard you wish to update. + this by using `gateway_shards` to find the shard you wish to + update. Parameters ---------- diff --git a/hikari/api/rest.py b/hikari/api/rest.py index f4ea30ece2..5b611cdd86 100644 --- a/hikari/api/rest.py +++ b/hikari/api/rest.py @@ -19,7 +19,7 @@ from __future__ import annotations -__all__: typing.Final[typing.Sequence[str]] = ["IRESTClient", "IRESTClientFactory"] +__all__: typing.Final[typing.Sequence[str]] = ["IRESTClient", "IRESTClientFactory", "IRESTClientContextManager"] import abc import typing diff --git a/hikari/models/webhooks.py b/hikari/models/webhooks.py index bcd9258d4b..0d1e1792e9 100644 --- a/hikari/models/webhooks.py +++ b/hikari/models/webhooks.py @@ -185,6 +185,7 @@ async def execute( return await self.app.rest.execute_webhook( webhook=self.id, token=self.token, + text=text, username=username, avatar_url=avatar_url, tts=tts, diff --git a/hikari/utilities/snowflake.py b/hikari/utilities/snowflake.py index f6b97d0273..0bedcb04f5 100644 --- a/hikari/utilities/snowflake.py +++ b/hikari/utilities/snowflake.py @@ -19,7 +19,7 @@ from __future__ import annotations -__all__: typing.Final[typing.Sequence[str]] = ["Snowflake"] +__all__: typing.Final[typing.Sequence[str]] = ["Snowflake", "Unique"] import abc import datetime From 19abff19ffeba91251506333b962d1fe59b1b9a2 Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Thu, 25 Jun 2020 18:53:11 +0100 Subject: [PATCH 576/922] Added history and send endpoints to text channels. --- hikari/models/channels.py | 128 ++++++++++++++++++++++++++++++++++++++ hikari/net/rest.py | 33 ---------- 2 files changed, 128 insertions(+), 33 deletions(-) diff --git a/hikari/models/channels.py b/hikari/models/channels.py index 63b8efe414..fb8278aca7 100644 --- a/hikari/models/channels.py +++ b/hikari/models/channels.py @@ -50,6 +50,11 @@ if typing.TYPE_CHECKING: import datetime from hikari.api import rest + from hikari.models import embeds + from hikari.models import guilds + from hikari.models import messages + from hikari.net import iterators + from hikari.utilities import undefined @enum.unique @@ -175,6 +180,129 @@ class TextChannel(PartialChannel, abc.ABC): # This is a mixin, do not add slotted fields. __slots__: typing.Sequence[str] = () + async def send( + self, + text: typing.Union[undefined.UndefinedType, str] = undefined.UNDEFINED, + *, + embed: typing.Union[undefined.UndefinedType, embeds.Embed] = undefined.UNDEFINED, + attachment: typing.Union[undefined.UndefinedType, str, files.Resource] = undefined.UNDEFINED, + attachments: typing.Union[ + undefined.UndefinedType, typing.Sequence[typing.Union[str, files.Resource]] + ] = undefined.UNDEFINED, + mentions_everyone: bool = False, + user_mentions: typing.Union[ + typing.Collection[typing.Union[snowflake.Snowflake, int, str, users.User]], bool + ] = True, + role_mentions: typing.Union[ + typing.Collection[typing.Union[snowflake.Snowflake, int, str, guilds.Role]], bool + ] = True, + nonce: typing.Union[undefined.UndefinedType, str] = undefined.UNDEFINED, + tts: typing.Union[undefined.UndefinedType, bool] = undefined.UNDEFINED, + ) -> messages.Message: + """Create a message in this channel. + + Parameters + ---------- + text : str or hikari.utilities.undefined.UndefinedType + If specified, the message text to send with the message. + nonce : str or hikari.utilities.undefined.UndefinedType + If specified, an optional ID to send for opportunistic message + creation. This doesn't serve any real purpose for general use, + and can usually be ignored. + tts : bool or hikari.utilities.undefined.UndefinedType + If specified, whether the message will be sent as a TTS message. + attachment : hikari.utilities.files.Resource or str or hikari.utilities.undefined.UndefinedType + If specified, a attachment to upload, if desired. This can + be a resource, or string of a path on your computer or a URL. + attachments : typing.Sequence[hikari.utilities.files.Resource or str] or hikari.utilities.undefined.UndefinedType + If specified, a sequence of attachments to upload, if desired. + Should be between 1 and 10 objects in size (inclusive), also + including embed attachments. These can be resources, or + strings consisting of paths on your computer or URLs. + embed : hikari.models.embeds.Embed or hikari.utilities.undefined.UndefinedType + If specified, the embed object to send with the message. + mentions_everyone : bool + Whether `@everyone` and `@here` mentions should be resolved by + discord and lead to actual pings, defaults to `False`. + user_mentions : typing.Collection[hikari.models.users.User or hikari.utilities.snowflake.Snowflake or int or str] or bool + Either an array of user objects/IDs to allow mentions for, + `True` to allow all user mentions or `False` to block all + user mentions from resolving, defaults to `True`. + role_mentions: typing.Collection[hikari.models.guilds.Role or hikari.utilities.snowflake.Snowflake or int or str] or bool + Either an array of guild role objects/IDs to allow mentions for, + `True` to allow all role mentions or `False` to block all + role mentions from resolving, defaults to `True`. + + Returns + ------- + hikari.models.messages.Message + The created message object. + + Raises + ------ + hikari.errors.NotFound + If the channel this message was created in is not found. + hikari.errors.BadRequest + This can be raised if the file is too large; if the embed exceeds + the defined limits; if the message content is specified only and + empty or greater than `2000` characters; if neither content, files + or embed are specified. + If any invalid snowflake IDs are passed; a snowflake may be invalid + due to it being outside of the range of a 64 bit integer. + If you are trying to upload more than 10 files in total (including + embed attachments). + hikari.errors.Forbidden + If you lack permissions to send to the channel this message belongs + to. + ValueError + If more than 100 unique objects/entities are passed for + `role_mentions` or `user_mentions`. + TypeError + If both `attachment` and `attachments` are specified. + """ # noqa: E501 - Line too long + return await self.app.rest.create_message( + channel=self.id, + text=text, + nonce=nonce, + tts=tts, + attachment=attachment, + attachments=attachments, + embed=embed, + mentions_everyone=mentions_everyone, + user_mentions=user_mentions, + role_mentions=role_mentions, + ) + + # TODO: examples + def history( + self, + *, + before: typing.Union[undefined.UndefinedType, datetime.datetime, snowflake.UniqueObject] = undefined.UNDEFINED, + after: typing.Union[undefined.UndefinedType, datetime.datetime, snowflake.UniqueObject] = undefined.UNDEFINED, + around: typing.Union[undefined.UndefinedType, datetime.datetime, snowflake.UniqueObject] = undefined.UNDEFINED, + ) -> iterators.LazyIterator[messages.Message]: + """Get a lazy iterator across the message history for this channel. + + Parameters + ---------- + before : hikari.utilities.undefined.UndefinedType or datetime.datetime or hikari.utilities.snowflake.UniqueObject + The message or object to find messages BEFORE. + after : hikari.utilities.undefined.UndefinedType or datetime.datetime or hikari.utilities.snowflake.UniqueObject + The message or object to find messages AFTER. + around : hikari.utilities.undefined.UndefinedType or datetime.datetime or hikari.utilities.snowflake.UniqueObject + The message or object to find messages AROUND. + + !!! warn + You may provide a maximum of one of the parameters for this method + only. + + Returns + ------- + hikari.net.iterators.LazyIterator[hikari.models.messages.Message] + A lazy async iterator across the messages. + """ # noqa: E501 - Line too long + return self.app.rest.fetch_messages(self.id, before=before, after=after, around=around) + @attr.s(eq=True, hash=True, init=False, kw_only=True, slots=True) class DMChannel(TextChannel): diff --git a/hikari/net/rest.py b/hikari/net/rest.py index 96181ef691..694aa2b0bd 100644 --- a/hikari/net/rest.py +++ b/hikari/net/rest.py @@ -904,39 +904,6 @@ async def unpin_message( route = routes.DELETE_CHANNEL_PIN.compile(channel=channel, message=message) await self._request(route) - @typing.overload - def fetch_messages( - self, channel: typing.Union[channels.TextChannel, snowflake.UniqueObject] - ) -> iterators.LazyIterator[messages_.Message]: - """Fetch messages, newest first, sent in the given channel.""" - - @typing.overload - def fetch_messages( - self, - channel: typing.Union[channels.TextChannel, snowflake.UniqueObject], - *, - before: typing.Union[datetime.datetime, snowflake.UniqueObject], - ) -> iterators.LazyIterator[messages_.Message]: - """Fetch messages, newest first, sent before a timestamp in the channel.""" - - @typing.overload - def fetch_messages( - self, - channel: typing.Union[channels.TextChannel, snowflake.UniqueObject], - *, - around: typing.Union[datetime.datetime, snowflake.UniqueObject], - ) -> iterators.LazyIterator[messages_.Message]: - """Fetch messages sent around a given time in the channel.""" - - @typing.overload - def fetch_messages( - self, - channel: typing.Union[channels.TextChannel, snowflake.UniqueObject], - *, - after: typing.Union[datetime.datetime, snowflake.UniqueObject], - ) -> iterators.LazyIterator[messages_.Message]: - """Fetch messages, oldest first, sent after a timestamp in the channel.""" - def fetch_messages( self, channel: typing.Union[channels.TextChannel, snowflake.UniqueObject], From 7cca1e7a4f667f234133ebdbbb7cc20a43db2dcc Mon Sep 17 00:00:00 2001 From: davfsa Date: Thu, 25 Jun 2020 20:12:51 +0200 Subject: [PATCH 577/922] Fix undefined name --- hikari/models/channels.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/hikari/models/channels.py b/hikari/models/channels.py index fb8278aca7..d1819c70ec 100644 --- a/hikari/models/channels.py +++ b/hikari/models/channels.py @@ -46,6 +46,7 @@ from hikari.utilities import cdn from hikari.utilities import files from hikari.utilities import snowflake +from hikari.utilities import undefined if typing.TYPE_CHECKING: import datetime @@ -54,7 +55,6 @@ from hikari.models import guilds from hikari.models import messages from hikari.net import iterators - from hikari.utilities import undefined @enum.unique @@ -273,7 +273,7 @@ async def send( role_mentions=role_mentions, ) - # TODO: examples + # TODO: add examples def history( self, *, From 0a3939ce61944374f5360722d74e1972109904cc Mon Sep 17 00:00:00 2001 From: "thomm.o" Date: Fri, 26 Jun 2020 06:41:07 +0000 Subject: [PATCH 578/922] #342 str overloads --- hikari/models/applications.py | 18 +++++ hikari/models/channels.py | 14 +++- hikari/models/emojis.py | 6 ++ hikari/models/guilds.py | 36 ++++++++++ hikari/models/intents.py | 3 + hikari/models/invites.py | 9 +++ hikari/models/messages.py | 18 +++++ hikari/models/permissions.py | 3 + hikari/models/presences.py | 12 ++++ hikari/models/users.py | 9 +++ hikari/models/webhooks.py | 6 ++ tests/hikari/models/test_applications.py | 56 +++++++++++++++ tests/hikari/models/test_audit_logs.py | 29 ++++++++ tests/hikari/models/test_channels.py | 70 +++++++++++++++++++ tests/hikari/models/test_colors.py | 24 +++++++ tests/hikari/models/test_emojis.py | 38 ++++++++++ tests/hikari/models/test_guilds.py | 88 ++++++++++++++++++++++++ tests/hikari/models/test_intents.py | 24 +++++++ tests/hikari/models/test_invites.py | 36 ++++++++++ tests/hikari/models/test_messages.py | 55 +++++++++++++++ tests/hikari/models/test_permissions.py | 24 +++++++ tests/hikari/models/test_presences.py | 39 +++++++++++ tests/hikari/models/test_users.py | 36 ++++++++++ tests/hikari/models/test_voices.py | 25 +++++++ tests/hikari/models/test_webhooks.py | 37 ++++++++++ 25 files changed, 714 insertions(+), 1 deletion(-) create mode 100644 tests/hikari/models/test_applications.py create mode 100644 tests/hikari/models/test_audit_logs.py create mode 100644 tests/hikari/models/test_channels.py create mode 100644 tests/hikari/models/test_colors.py create mode 100644 tests/hikari/models/test_emojis.py create mode 100644 tests/hikari/models/test_guilds.py create mode 100644 tests/hikari/models/test_intents.py create mode 100644 tests/hikari/models/test_invites.py create mode 100644 tests/hikari/models/test_messages.py create mode 100644 tests/hikari/models/test_permissions.py create mode 100644 tests/hikari/models/test_presences.py create mode 100644 tests/hikari/models/test_users.py create mode 100644 tests/hikari/models/test_voices.py create mode 100644 tests/hikari/models/test_webhooks.py diff --git a/hikari/models/applications.py b/hikari/models/applications.py index 0c795ab512..16af9f70b4 100644 --- a/hikari/models/applications.py +++ b/hikari/models/applications.py @@ -174,6 +174,9 @@ class OAuth2Scope(str, enum.Enum): This is used during authorization code grants. """ + def __str__(self) -> str: + return self.name + @enum.unique @typing.final @@ -186,6 +189,9 @@ class ConnectionVisibility(int, enum.Enum): EVERYONE = 1 """Everyone can see the connection.""" + def __str__(self) -> str: + return self.name + @attr.s(eq=True, hash=True, init=False, kw_only=True, slots=True) class OwnConnection: @@ -247,6 +253,9 @@ class TeamMembershipState(int, enum.Enum): ACCEPTED = 2 """Denotes the user has accepted the invite and is now a member.""" + def __str__(self) -> str: + return self.name + @attr.s(eq=True, hash=True, init=False, kw_only=True, slots=True) class TeamMember: @@ -271,6 +280,9 @@ class TeamMember: user: users.User = attr.ib(eq=True, hash=True, repr=True) """The user representation of this team member.""" + def __str__(self) -> str: + return str(self.user) + @attr.s(eq=True, hash=True, init=False, kw_only=True, slots=True) class Team(snowflake.Unique): @@ -300,6 +312,9 @@ class Team(snowflake.Unique): owner_user_id: snowflake.Snowflake = attr.ib(eq=False, hash=False, repr=True) """The ID of this team's owner.""" + def __str__(self) -> str: + return f"Team {self.id}" + @property def icon_url(self) -> typing.Optional[files.URL]: """Team icon. @@ -414,6 +429,9 @@ class Application(snowflake.Unique): cover_image_hash: typing.Optional[str] = attr.ib(eq=False, hash=False, repr=False) """The CDN's hash of this application's cover image, used on the store.""" + def __str__(self) -> str: + return self.name + @property def icon(self) -> typing.Optional[files.URL]: """Team icon, if there is one. diff --git a/hikari/models/channels.py b/hikari/models/channels.py index d1819c70ec..c3af991b52 100644 --- a/hikari/models/channels.py +++ b/hikari/models/channels.py @@ -83,6 +83,9 @@ class ChannelType(int, enum.Enum): GUILD_STORE = 6 """A channel that show's a game's store page.""" + def __str__(self) -> str: + return self.name + @enum.unique @typing.final @@ -96,7 +99,7 @@ class PermissionOverwriteType(str, enum.Enum): """A permission overwrite that targets a specific guild member.""" def __str__(self) -> str: - return str(self.value) + return self.name @attr.s(eq=True, hash=True, init=True, kw_only=True, slots=True) @@ -173,6 +176,9 @@ class PartialChannel(snowflake.Unique): type: ChannelType = attr.ib(eq=False, hash=False, repr=True) """The channel's type.""" + def __str__(self) -> str: + return self.name if self.name is not None else f"Unnamed channel ID {self.id}" + class TextChannel(PartialChannel, abc.ABC): """A channel that can have text messages in it.""" @@ -319,6 +325,9 @@ class DMChannel(TextChannel): recipients: typing.Mapping[snowflake.Snowflake, users.User] = attr.ib(eq=False, hash=False, repr=False) """The recipients of the DM.""" + def __str__(self) -> str: + return f"{self.__class__.__name__} with: {', '.join(str(user) for user in self.recipients.values())}" + @attr.s(eq=True, hash=True, init=False, kw_only=True, slots=True) class GroupDMChannel(DMChannel): @@ -339,6 +348,9 @@ class GroupDMChannel(DMChannel): If the group DM was not created by a bot, this will be `None`. """ + def __str__(self) -> str: + return self.name if self.name is not None else super().__str__() + @property def icon(self) -> typing.Optional[files.URL]: """Icon for this DM channel, if set.""" diff --git a/hikari/models/emojis.py b/hikari/models/emojis.py index a1d2ac53cc..53669a6b58 100644 --- a/hikari/models/emojis.py +++ b/hikari/models/emojis.py @@ -96,6 +96,9 @@ class UnicodeEmoji(Emoji): name: str = attr.ib(eq=True, hash=True, repr=True) """The code points that form the emoji.""" + def __str__(self) -> str: + return self.name + @property @typing.final def url_name(self) -> str: @@ -231,6 +234,9 @@ class CustomEmoji(snowflake.Unique, Emoji): Reaction Remove Emoji events. """ + def __str__(self) -> str: + return self.name if self.name is not None else f"Unnamed emoji ID {self.id}" + @property def filename(self) -> str: return str(self.id) + (".gif" if self.is_animated else ".png") diff --git a/hikari/models/guilds.py b/hikari/models/guilds.py index 566e4ae783..824e48f4e0 100644 --- a/hikari/models/guilds.py +++ b/hikari/models/guilds.py @@ -78,6 +78,9 @@ class GuildExplicitContentFilterLevel(int, enum.Enum): ALL_MEMBERS = 2 """Filter all posts.""" + def __str__(self) -> str: + return self.name + @enum.unique @typing.final @@ -135,6 +138,9 @@ class GuildFeature(str, enum.Enum): WELCOME_SCREEN_ENABLED = "WELCOME_SCREEN_ENABLED" """Guild has enabled the welcome screen.""" + def __str__(self) -> str: + return self.name + @enum.unique @typing.final @@ -147,6 +153,9 @@ class GuildMessageNotificationsLevel(int, enum.Enum): ONLY_MENTIONS = 1 """Only notify users when they are @mentioned.""" + def __str__(self) -> str: + return self.name + @enum.unique @typing.final @@ -159,6 +168,9 @@ class GuildMFALevel(int, enum.Enum): ELEVATED = 1 """MFA requirement.""" + def __str__(self) -> str: + return self.name + @enum.unique @typing.final @@ -177,6 +189,9 @@ class GuildPremiumTier(int, enum.Enum): TIER_3 = 3 """Level 3 Nitro boost.""" + def __str__(self) -> str: + return self.name + @enum.unique @typing.final @@ -189,6 +204,9 @@ class GuildSystemChannelFlag(enum.IntFlag): SUPPRESS_PREMIUM_SUBSCRIPTION = 1 << 1 """Display a message when the guild is Nitro boosted.""" + def __str__(self) -> str: + return self.name + @enum.unique @typing.final @@ -210,6 +228,9 @@ class GuildVerificationLevel(int, enum.Enum): VERY_HIGH = 4 """┻━┻ミヽ(ಠ益ಠ)ノ彡┻━┻ - must have a verified phone number.""" + def __str__(self) -> str: + return self.name + @attr.s(eq=True, hash=False, init=False, kw_only=True, slots=True) class GuildWidget: @@ -276,6 +297,9 @@ class Member: This will be `hikari.utilities.undefined.UndefinedType if it's state is unknown. """ + def __str__(self) -> str: + return str(self.user) + @attr.s(eq=True, hash=True, init=False, kw_only=True, slots=True) class PartialRole(snowflake.Unique): @@ -292,6 +316,9 @@ class PartialRole(snowflake.Unique): name: str = attr.ib(eq=False, hash=False, repr=True) """The role's name.""" + def __str__(self) -> str: + return self.name + @attr.s(eq=True, hash=True, init=False, kw_only=True, slots=True) class Role(PartialRole): @@ -347,6 +374,9 @@ class IntegrationAccount: name: str = attr.ib(eq=False, hash=False, repr=True) """The name of this account.""" + def __str__(self) -> str: + return self.name + @attr.s(eq=True, hash=True, init=False, kw_only=True, slots=True) class PartialIntegration(snowflake.Unique): @@ -366,6 +396,9 @@ class PartialIntegration(snowflake.Unique): account: IntegrationAccount = attr.ib(eq=False, hash=False, repr=False) """The account connected to this integration.""" + def __str__(self) -> str: + return self.name + @attr.s(eq=True, hash=True, init=False, kw_only=True, slots=True) class Integration(PartialIntegration): @@ -458,6 +491,9 @@ class PartialGuild(snowflake.Unique): features: typing.Set[typing.Union[GuildFeature, str]] = attr.ib(eq=False, hash=False, repr=False) """A set of the features in this guild.""" + def __str__(self) -> str: + return self.name + @property def icon_url(self) -> typing.Optional[files.URL]: """Icon for the guild, if set; otherwise `None`.""" diff --git a/hikari/models/intents.py b/hikari/models/intents.py index 58da308d63..4f9b15274a 100644 --- a/hikari/models/intents.py +++ b/hikari/models/intents.py @@ -173,6 +173,9 @@ class Intent(enum.IntFlag): * TYPING_START """ + def __str__(self) -> str: + return self.name + @property def is_privileged(self) -> bool: """Whether the intent requires elevated privileges. diff --git a/hikari/models/invites.py b/hikari/models/invites.py index b7ebec93c4..35d878b667 100644 --- a/hikari/models/invites.py +++ b/hikari/models/invites.py @@ -53,6 +53,9 @@ class TargetUserType(int, enum.Enum): STREAM = 1 """This invite is targeting a "Go Live" stream.""" + def __str__(self) -> str: + return self.name + @attr.s(eq=True, hash=True, init=False, kw_only=True, slots=True) class VanityURL: @@ -67,6 +70,9 @@ class VanityURL: uses: int = attr.ib(eq=False, hash=False, repr=True) """The amount of times this invite has been used.""" + def __str__(self) -> str: + return f"https://discord.gg/{self.code}" + @attr.s(eq=True, hash=True, init=False, kw_only=True, slots=True) class InviteGuild(guilds.PartialGuild): @@ -220,6 +226,9 @@ class Invite: Invites endpoint. """ + def __str__(self) -> str: + return f"https://discord.gg/{self.code}" + @attr.s(eq=True, hash=True, init=False, kw_only=True, slots=True) class InviteWithMetadata(Invite): diff --git a/hikari/models/messages.py b/hikari/models/messages.py index 538e133c5d..97e7a1971c 100644 --- a/hikari/models/messages.py +++ b/hikari/models/messages.py @@ -95,6 +95,9 @@ class MessageType(int, enum.Enum): CHANNEL_FOLLOW_ADD = 12 """Channel follow add.""" + def __str__(self) -> str: + return self.name + @enum.unique @typing.final @@ -119,6 +122,9 @@ class MessageFlag(enum.IntFlag): URGENT = 1 << 4 """This message came from the urgent message system.""" + def __str__(self) -> str: + return self.name + @enum.unique @typing.final @@ -140,6 +146,9 @@ class MessageActivityType(int, enum.Enum): JOIN_REQUEST = 5 """Request to join an activity.""" + def __str__(self) -> str: + return self.name + @attr.s(eq=True, hash=False, init=False, kw_only=True, slots=True) class Attachment(snowflake.Unique, files_.WebResource): @@ -172,6 +181,9 @@ class Attachment(snowflake.Unique, files_.WebResource): width: typing.Optional[int] = attr.ib(repr=False) """The width of the image (if the file is an image).""" + def __str__(self) -> str: + return self.filename + @attr.s(eq=True, hash=True, init=False, kw_only=True, slots=True) class Reaction: @@ -186,6 +198,9 @@ class Reaction: is_reacted_by_me: bool = attr.ib(eq=False, hash=False, repr=False) """Whether the current user reacted using this emoji.""" + def __str__(self) -> str: + return str(self.emoji) + @attr.s(eq=True, hash=False, init=False, kw_only=True, slots=True) class MessageActivity: @@ -315,6 +330,9 @@ class Message(snowflake.Unique): nonce: typing.Optional[str] = attr.ib(eq=False, hash=False, repr=False) """The message nonce. This is a string used for validating a message was sent.""" + def __str__(self) -> str: + return self.content + async def fetch_channel(self) -> channels.PartialChannel: """Fetch the channel this message was created in. diff --git a/hikari/models/permissions.py b/hikari/models/permissions.py index 0f0c27a3d8..e6b6f85070 100644 --- a/hikari/models/permissions.py +++ b/hikari/models/permissions.py @@ -183,3 +183,6 @@ class Permission(enum.IntFlag): MANAGE_EMOJIS = 1 << 30 """Allows management and editing of emojis.""" + + def __str__(self) -> str: + return self.name diff --git a/hikari/models/presences.py b/hikari/models/presences.py index efca97cf40..a73f2c6cc6 100644 --- a/hikari/models/presences.py +++ b/hikari/models/presences.py @@ -76,6 +76,9 @@ class ActivityType(int, enum.Enum): (`:smiley:`) as the first part of the status activity name. """ + def __str__(self) -> str: + return self.name + @attr.s(eq=True, hash=False, init=False, kw_only=True, slots=True) class ActivityTimestamps: @@ -159,6 +162,9 @@ class ActivityFlag(enum.IntFlag): PLAY = 1 << 5 """Play""" + def __str__(self) -> str: + return self.name + # TODO: add strict type checking to gateway for this type in an invariant way. @attr.s(eq=True, hash=False, kw_only=True, slots=True) @@ -185,6 +191,9 @@ class Activity: type: ActivityType = attr.ib(converter=ActivityType) """The activity type.""" + def __str__(self) -> str: + return self.name + @attr.s(eq=True, hash=False, init=False, kw_only=True, slots=True) class RichActivity(Activity): @@ -249,6 +258,9 @@ class Status(str, enum.Enum): OFFLINE = "offline" """Offline or invisible/grey.""" + def __str__(self) -> str: + return self.name + @attr.s(eq=True, hash=False, init=False, kw_only=True, slots=True) class ClientStatus: diff --git a/hikari/models/users.py b/hikari/models/users.py index 663cdc9b8e..fd49841302 100644 --- a/hikari/models/users.py +++ b/hikari/models/users.py @@ -82,6 +82,9 @@ class UserFlag(enum.IntFlag): VERIFIED_BOT_DEVELOPER = 1 << 17 """Verified Bot Developer""" + def __str__(self) -> str: + return self.name + @enum.unique @typing.final @@ -97,6 +100,9 @@ class PremiumType(int, enum.Enum): NITRO = 2 """Premium including all perks (e.g. 2 server boosts).""" + def __str__(self) -> str: + return self.name + @attr.s(eq=True, hash=True, init=False, kw_only=True, slots=True) class PartialUser(snowflake.Unique): @@ -132,6 +138,9 @@ class PartialUser(snowflake.Unique): flags: typing.Union[UserFlag, undefined.UndefinedType] = attr.ib(eq=False, hash=False) """The public flags for this user.""" + def __str__(self) -> str: + return f"{self.username}#{self.discriminator}" + @attr.s(eq=True, hash=True, init=False, kw_only=True, slots=True) class User(PartialUser): diff --git a/hikari/models/webhooks.py b/hikari/models/webhooks.py index 0d1e1792e9..8cbc9f903e 100644 --- a/hikari/models/webhooks.py +++ b/hikari/models/webhooks.py @@ -51,6 +51,9 @@ class WebhookType(int, enum.Enum): CHANNEL_FOLLOWER = 2 """Channel Follower webhook.""" + def __str__(self) -> str: + return self.name + @attr.s(eq=True, hash=True, init=False, kw_only=True, slots=True) class Webhook(snowflake.Unique): @@ -100,6 +103,9 @@ class Webhook(snowflake.Unique): channel settings. """ + def __str__(self) -> str: + return self.name if self.name is not None else f"Unnamed webhook ID {self.id}" + async def execute( self, text: typing.Union[undefined.UndefinedType, typing.Any] = undefined.UNDEFINED, diff --git a/tests/hikari/models/test_applications.py b/tests/hikari/models/test_applications.py new file mode 100644 index 0000000000..ec8e698a27 --- /dev/null +++ b/tests/hikari/models/test_applications.py @@ -0,0 +1,56 @@ +# -*- coding: utf-8 -*- +# Copyright © Nekoka.tt 2019-2020 +# +# This file is part of Hikari. +# +# Hikari is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Hikari is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Hikari. If not, see . + +from hikari.models import applications +from hikari.models import users + + +def test_OAuth2Scope_str_operator(): + scope = applications.OAuth2Scope("activities.read") + assert str(scope) == "ACTIVITIES_READ" + + +def test_ConnectionVisibility_str_operator(): + connection_visibility = applications.ConnectionVisibility(1) + assert str(connection_visibility) == "EVERYONE" + + +def test_TeamMembershipState_str_operator(): + state = applications.TeamMembershipState(2) + assert str(state) == "ACCEPTED" + + +def test_TeamMember_str_operator(): + team_member = applications.TeamMember() + team_member_user = users.User() + team_member_user.username = "mario" + team_member_user.discriminator = "1234" + team_member.user = team_member_user + assert str(team_member) == "mario#1234" + + +def test_Team_str_operator(): + team = applications.Team() + team.id = 696969 + assert str(team) == "Team 696969" + + +def test_Application_str_operator(): + application = applications.Application() + application.name = "beans" + assert str(application) == "beans" diff --git a/tests/hikari/models/test_audit_logs.py b/tests/hikari/models/test_audit_logs.py new file mode 100644 index 0000000000..88ccf72081 --- /dev/null +++ b/tests/hikari/models/test_audit_logs.py @@ -0,0 +1,29 @@ +# -*- coding: utf-8 -*- +# Copyright © Nekoka.tt 2019-2020 +# +# This file is part of Hikari. +# +# Hikari is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Hikari is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Hikari. If not, see . + +from hikari.models import audit_logs + + +def test_AuditLogChangeKey_str_operator(): + change_key = audit_logs.AuditLogChangeKey("owner_id") + assert str(change_key) == "OWNER_ID" + + +def test_AuditLogEventType_str_operator(): + event_type = audit_logs.AuditLogEventType(80) + assert str(event_type) == "INTEGRATION_CREATE" diff --git a/tests/hikari/models/test_channels.py b/tests/hikari/models/test_channels.py new file mode 100644 index 0000000000..00340c29bb --- /dev/null +++ b/tests/hikari/models/test_channels.py @@ -0,0 +1,70 @@ +# -*- coding: utf-8 -*- +# Copyright © Nekoka.tt 2019-2020 +# +# This file is part of Hikari. +# +# Hikari is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Hikari is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Hikari. If not, see . + +from hikari.models import channels +from hikari.models import users + + +def test_ChannelType_str_operator(): + channel_type = channels.ChannelType(1) + assert str(channel_type) == "DM" + + +def test_PermissionOverwriteType_str_operator(): + overwrite_type = channels.PermissionOverwriteType("role") + assert str(overwrite_type) == "ROLE" + + +def test_PartialChannel_str_operator(): + channel = channels.PartialChannel() + channel.name = "foo" + assert str(channel) == "foo" + + +def test_PartialChannel_str_operator_when_name_is_None(): + channel = channels.PartialChannel() + channel.id = 1234567 + channel.name = None + assert str(channel) == "Unnamed channel ID 1234567" + + +def test_DMChannel_str_operator(): + channel = channels.DMChannel() + user = users.User() + user.discriminator = "0420" + user.username = "snoop" + channel.recipients = {1: user} + assert str(channel) == "DMChannel with: snoop#0420" + + +def test_GroupDMChannel_str_operator(): + channel = channels.GroupDMChannel() + channel.name = "super cool group dm" + assert str(channel) == "super cool group dm" + + +def test_GroupDMChannel_str_operator_when_name_is_None(): + channel = channels.GroupDMChannel() + channel.name = None + user, other_user = users.User(), users.User() + user.discriminator = "0420" + user.username = "snoop" + other_user.discriminator = "6969" + other_user.username = "nice" + channel.recipients = {1: user, 2: other_user} + assert str(channel) == "GroupDMChannel with: snoop#0420, nice#6969" diff --git a/tests/hikari/models/test_colors.py b/tests/hikari/models/test_colors.py new file mode 100644 index 0000000000..12ecee3d23 --- /dev/null +++ b/tests/hikari/models/test_colors.py @@ -0,0 +1,24 @@ +# -*- coding: utf-8 -*- +# Copyright © Nekoka.tt 2019-2020 +# +# This file is part of Hikari. +# +# Hikari is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Hikari is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Hikari. If not, see . + +from hikari.models import colors + + +def test_Color_str_operator(): + color = colors.Color(0xFFFFFF) + assert str(color) == "#FFFFFF" diff --git a/tests/hikari/models/test_emojis.py b/tests/hikari/models/test_emojis.py new file mode 100644 index 0000000000..c08db4f02d --- /dev/null +++ b/tests/hikari/models/test_emojis.py @@ -0,0 +1,38 @@ +# -*- coding: utf-8 -*- +# Copyright © Nekoka.tt 2019-2020 +# +# This file is part of Hikari. +# +# Hikari is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Hikari is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Hikari. If not, see . + +from hikari.models import emojis + + +def test_UnicodeEmoji_str_operator(): + emoji = emojis.UnicodeEmoji() + emoji.name = "\N{OK HAND SIGN}" + assert str(emoji) == "\N{OK HAND SIGN}" + + +def test_CustomEmoji_str_operator(): + emoji = emojis.CustomEmoji() + emoji.name = "peepoSad" + assert str(emoji) == "peepoSad" + + +def test_CustomEmoji_str_operator_when_name_is_None(): + emoji = emojis.CustomEmoji() + emoji.name = None + emoji.id = 42069 + assert str(emoji) == "Unnamed emoji ID 42069" diff --git a/tests/hikari/models/test_guilds.py b/tests/hikari/models/test_guilds.py new file mode 100644 index 0000000000..921f657435 --- /dev/null +++ b/tests/hikari/models/test_guilds.py @@ -0,0 +1,88 @@ +# -*- coding: utf-8 -*- +# Copyright © Nekoka.tt 2019-2020 +# +# This file is part of Hikari. +# +# Hikari is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Hikari is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Hikari. If not, see . + +from hikari.models import guilds +from hikari.models import users + + +def test_GuildExplicitContentFilterLevel_str_operator(): + level = guilds.GuildExplicitContentFilterLevel(1) + assert str(level) == "MEMBERS_WITHOUT_ROLES" + + +def test_GuildFeature_str_operator(): + feature = guilds.GuildFeature("ANIMATED_ICON") + assert str(feature) == "ANIMATED_ICON" + + +def test_GuildMessageNotificationsLevel_str_operator(): + level = guilds.GuildMessageNotificationsLevel(1) + assert str(level) == "ONLY_MENTIONS" + + +def test_GuildMFALevel_str_operator(): + level = guilds.GuildMFALevel(1) + assert str(level) == "ELEVATED" + + +def test_GuildPremiumTier_str_operator(): + level = guilds.GuildPremiumTier(1) + assert str(level) == "TIER_1" + + +def test_GuildSystemChannelFlag_str_operator(): + flag = guilds.GuildSystemChannelFlag(1 << 0) + assert str(flag) == "SUPPRESS_USER_JOIN" + + +def test_GuildVerificationLevel_str_operator(): + level = guilds.GuildVerificationLevel(0) + assert str(level) == "NONE" + + +def test_Member_str_operator(): + member = guilds.Member() + user = users.User() + user.username = "thomm.o" + user.discriminator = "8637" + member.user = user + assert str(member) == "thomm.o#8637" + + +def test_PartialRole_str_operator(): + role = guilds.PartialRole() + role.name = "The Big Cool" + assert str(role) == "The Big Cool" + + +def test_IntegrationAccount_str_operator(): + account = guilds.IntegrationAccount() + account.name = "your mother" + assert str(account) == "your mother" + + +def test_PartialIntegration_str_operator(): + integration = guilds.PartialIntegration() + integration.name = "not an integration" + assert str(integration) == "not an integration" + + +def test_PartialGuild_str_operator(): + guild = guilds.PartialGuild() + guild.name = "hikari" + assert str(guild) == "hikari" diff --git a/tests/hikari/models/test_intents.py b/tests/hikari/models/test_intents.py new file mode 100644 index 0000000000..c21a2518ca --- /dev/null +++ b/tests/hikari/models/test_intents.py @@ -0,0 +1,24 @@ +# -*- coding: utf-8 -*- +# Copyright © Nekoka.tt 2019-2020 +# +# This file is part of Hikari. +# +# Hikari is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Hikari is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Hikari. If not, see . + +from hikari.models import intents + + +def test_Intent_str_operator(): + intent = intents.Intent(1 << 9) + assert str(intent) == "GUILD_MESSAGES" diff --git a/tests/hikari/models/test_invites.py b/tests/hikari/models/test_invites.py new file mode 100644 index 0000000000..4fa139fdc0 --- /dev/null +++ b/tests/hikari/models/test_invites.py @@ -0,0 +1,36 @@ +# -*- coding: utf-8 -*- +# Copyright © Nekoka.tt 2019-2020 +# +# This file is part of Hikari. +# +# Hikari is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Hikari is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Hikari. If not, see . + +from hikari.models import invites + + +def test_TargetUserType_str_operator(): + type = invites.TargetUserType(1) + assert str(type) == "STREAM" + + +def test_VanityURL_str_operator(): + url = invites.VanityURL() + url.code = "hikari" + assert str(url) == "https://discord.gg/hikari" + + +def test_Invite_str_operator(): + invite = invites.Invite() + invite.code = "abcdef" + assert str(invite) == "https://discord.gg/abcdef" diff --git a/tests/hikari/models/test_messages.py b/tests/hikari/models/test_messages.py new file mode 100644 index 0000000000..a886a8530c --- /dev/null +++ b/tests/hikari/models/test_messages.py @@ -0,0 +1,55 @@ +# -*- coding: utf-8 -*- +# Copyright © Nekoka.tt 2019-2020 +# +# This file is part of Hikari. +# +# Hikari is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Hikari is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Hikari. If not, see . + +from hikari.models import messages +from hikari.models import emojis + + +def test_MessageType_str_operator(): + type = messages.MessageType(10) + assert str(type) == "USER_PREMIUM_GUILD_SUBSCRIPTION_TIER_2" + + +def test_MessageFlag_str_operator(): + flag = messages.MessageFlag(0) + assert str(flag) == "NONE" + + +def test_MessageActivityType_str_operator(): + type = messages.MessageActivityType(5) + assert str(type) == "JOIN_REQUEST" + + +def test_Attachment_str_operator(): + attachment = messages.Attachment() + attachment.filename = "super_cool_file.cool" + assert str(attachment) == "super_cool_file.cool" + + +def test_Reaction_str_operator(): + reaction = messages.Reaction() + emoji = emojis.UnicodeEmoji() + emoji.name = "\N{OK HAND SIGN}" + reaction.emoji = emoji + assert str(reaction) == "\N{OK HAND SIGN}" + + +def test_Message_str_operator(): + message = messages.Message() + message.content = "espy is super cool" + assert str(message) == "espy is super cool" diff --git a/tests/hikari/models/test_permissions.py b/tests/hikari/models/test_permissions.py new file mode 100644 index 0000000000..17195091b4 --- /dev/null +++ b/tests/hikari/models/test_permissions.py @@ -0,0 +1,24 @@ +# -*- coding: utf-8 -*- +# Copyright © Nekoka.tt 2019-2020 +# +# This file is part of Hikari. +# +# Hikari is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Hikari is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Hikari. If not, see . + +from hikari.models import permissions + + +def test_Permission_str_operator(): + permission = permissions.Permission(1 << 30) + assert str(permission) == "MANAGE_EMOJIS" diff --git a/tests/hikari/models/test_presences.py b/tests/hikari/models/test_presences.py new file mode 100644 index 0000000000..8b403cd9c0 --- /dev/null +++ b/tests/hikari/models/test_presences.py @@ -0,0 +1,39 @@ +# -*- coding: utf-8 -*- +# Copyright © Nekoka.tt 2019-2020 +# +# This file is part of Hikari. +# +# Hikari is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Hikari is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Hikari. If not, see . + +from hikari.models import presences + + +def test_ActivityType_str_operator(): + type = presences.ActivityType(4) + assert str(type) == "CUSTOM" + + +def test_ActivityFlag_str_operator(): + flag = presences.ActivityFlag(1 << 4) + assert str(flag) == "SYNC" + + +def test_Activity_str_operator(): + activity = presences.Activity(name="something", type=presences.ActivityType(1)) + assert str(activity) == "something" + + +def test_Status_str_operator(): + status = presences.Status("idle") + assert str(status) == "IDLE" diff --git a/tests/hikari/models/test_users.py b/tests/hikari/models/test_users.py new file mode 100644 index 0000000000..4ad4459b61 --- /dev/null +++ b/tests/hikari/models/test_users.py @@ -0,0 +1,36 @@ +# -*- coding: utf-8 -*- +# Copyright © Nekoka.tt 2019-2020 +# +# This file is part of Hikari. +# +# Hikari is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Hikari is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Hikari. If not, see . + +from hikari.models import users + + +def test_UserFlag_str_operator(): + flag = users.UserFlag(1 << 17) + assert str(flag) == "VERIFIED_BOT_DEVELOPER" + + +def test_PremiumType_str_operator(): + type = users.PremiumType(1) + assert str(type) == "NITRO_CLASSIC" + + +def test_PartialUser_str_operator(): + user = users.PartialUser() + user.username = "thomm.o" + user.discriminator = "8637" + assert str(user) == "thomm.o#8637" diff --git a/tests/hikari/models/test_voices.py b/tests/hikari/models/test_voices.py new file mode 100644 index 0000000000..a64cf43f57 --- /dev/null +++ b/tests/hikari/models/test_voices.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- +# Copyright © Nekoka.tt 2019-2020 +# +# This file is part of Hikari. +# +# Hikari is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Hikari is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Hikari. If not, see . + +from hikari.models import voices + + +def test_VoiceRegion_str_operator(): + region = voices.VoiceRegion() + region.id = "eu or something idk" + assert str(region) == "eu or something idk" diff --git a/tests/hikari/models/test_webhooks.py b/tests/hikari/models/test_webhooks.py new file mode 100644 index 0000000000..34c3e8dc27 --- /dev/null +++ b/tests/hikari/models/test_webhooks.py @@ -0,0 +1,37 @@ +# -*- coding: utf-8 -*- +# Copyright © Nekoka.tt 2019-2020 +# +# This file is part of Hikari. +# +# Hikari is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Hikari is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Hikari. If not, see . + +from hikari.models import webhooks + + +def test_WebhookType_str_operator(): + type = webhooks.WebhookType(1) + assert str(type) == "INCOMING" + + +def test_Webhook_str_operator(): + webhook = webhooks.Webhook() + webhook.name = "not a webhook" + assert str(webhook) == "not a webhook" + + +def test_Webhook_str_operator_when_name_is_None(): + webhook = webhooks.Webhook() + webhook.name = None + webhook.id = 987654321 + assert str(webhook) == "Unnamed webhook ID 987654321" From f21b6d6ac9df3a07d84736e6948f8de647ce3b72 Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Fri, 26 Jun 2020 08:27:13 +0100 Subject: [PATCH 579/922] Froze requirements and fixed security false positives. --- hikari/impl/bot.py | 3 ++- hikari/net/strings.py | 4 ++-- requirements.txt | 6 +++--- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/hikari/impl/bot.py b/hikari/impl/bot.py index ac6ec99dff..b6ea8b7315 100644 --- a/hikari/impl/bot.py +++ b/hikari/impl/bot.py @@ -39,6 +39,7 @@ from hikari.net import http_settings as http_settings_ from hikari.net import rate_limits from hikari.net import rest +from hikari.net import strings from hikari.utilities import undefined if typing.TYPE_CHECKING: @@ -207,7 +208,7 @@ def __init__( debug=debug, global_ratelimit=self._global_ratelimit, token=token, - token_type="Bot", + token_type=strings.BOT_TOKEN, # nosec rest_url=rest_url, version=rest_version, ) diff --git a/hikari/net/strings.py b/hikari/net/strings.py index 9bdc057107..a8dae76571 100644 --- a/hikari/net/strings.py +++ b/hikari/net/strings.py @@ -49,8 +49,8 @@ APPLICATION_OCTET_STREAM: typing.Final[str] = "application/octet-stream" # Bits of text. -BEARER_TOKEN: typing.Final[str] = "Bearer" -BOT_TOKEN: typing.Final[str] = "Bot" +BEARER_TOKEN: typing.Final[str] = "Bearer" # nosec +BOT_TOKEN: typing.Final[str] = "Bot" # nosec MILLISECOND_PRECISION: typing.Final[str] = "millisecond" # User-agent info. diff --git a/requirements.txt b/requirements.txt index 51569d09ca..f39aab71ea 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,3 @@ -aiohttp~=3.6.2 -attrs~=19.3.0 -multidict~=4.7.6 +aiohttp==3.6.2 +attrs==19.3.0 +multidict==4.7.6 From 08f58a13de6673939e8acd0136ed0f00aa93ab00 Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Fri, 26 Jun 2020 10:15:53 +0100 Subject: [PATCH 580/922] Bugfix for message iterator request call parameter mismatch. --- hikari/net/iterators.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/hikari/net/iterators.py b/hikari/net/iterators.py index b00c6a51f3..267326abd1 100644 --- a/hikari/net/iterators.py +++ b/hikari/net/iterators.py @@ -386,7 +386,7 @@ async def _next_chunk(self) -> typing.Optional[typing.Generator[messages.Message query.put(self._direction, self._first_id) query.put("limit", 100) - raw_chunk = await self._request_call(self._route, query) + raw_chunk = await self._request_call(compiled_route=self._route, query=query) chunk = typing.cast(data_binding.JSONArray, raw_chunk) if not chunk: @@ -427,7 +427,7 @@ async def _next_chunk(self) -> typing.Optional[typing.Generator[users.User, typi query.put("after", self._first_id) query.put("limit", 100) - raw_chunk = await self._request_call(self._route, query=query) + raw_chunk = await self._request_call(compiled_route=self._route, query=query) chunk = typing.cast(data_binding.JSONArray, raw_chunk) if not chunk: @@ -466,7 +466,7 @@ async def _next_chunk(self) -> typing.Optional[typing.Generator[applications.Own query.put("before" if self._newest_first else "after", self._first_id) query.put("limit", 100) - raw_chunk = await self._request_call(self._route, query=query) + raw_chunk = await self._request_call(compiled_route=self._route, query=query) chunk = typing.cast(data_binding.JSONArray, raw_chunk) if not chunk: @@ -503,7 +503,7 @@ async def _next_chunk(self) -> typing.Optional[typing.Generator[guilds.Member, t query.put("after", self._first_id) query.put("limit", 100) - raw_chunk = await self._request_call(self._route, query=query) + raw_chunk = await self._request_call(compiled_route=self._route, query=query) chunk = typing.cast(data_binding.JSONArray, raw_chunk) if not chunk: @@ -545,7 +545,7 @@ async def __anext__(self) -> audit_logs.AuditLog: query.put("user_id", self._user_id) query.put("event_type", self._action_type) - raw_response = await self._request_call(self._route, query=query) + raw_response = await self._request_call(compiled_route=self._route, query=query) response = typing.cast(data_binding.JSONObject, raw_response) if not response["entries"]: From 0fa769f6f2ca90fffe4ef9375d00ffa95906e0b0 Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Sat, 27 Jun 2020 20:40:56 +0100 Subject: [PATCH 581/922] Added the ability to determine if a listener is registered or not. --- hikari/api/event_dispatcher.py | 7 +++++++ hikari/impl/event_manager_core.py | 9 +++++++++ 2 files changed, 16 insertions(+) diff --git a/hikari/api/event_dispatcher.py b/hikari/api/event_dispatcher.py index 4b1dc90d95..6aa6232592 100644 --- a/hikari/api/event_dispatcher.py +++ b/hikari/api/event_dispatcher.py @@ -174,6 +174,13 @@ async def on_message(event): `hikari.api.event_dispatcher.IEventDispatcherBase.wait_for` """ + def has_listener( + self, + event_type: typing.Type[EventT], + callback: typing.Callable[[EventT], typing.Coroutine[None, typing.Any, None]], + ) -> bool: + """Returns `True` if the callback is subscribed for the given event.""" + @abc.abstractmethod def unsubscribe( self, diff --git a/hikari/impl/event_manager_core.py b/hikari/impl/event_manager_core.py index 2a9e88caee..57197b8f22 100644 --- a/hikari/impl/event_manager_core.py +++ b/hikari/impl/event_manager_core.py @@ -111,6 +111,15 @@ async def wrapper(event: EventT) -> None: return callback + def has_listener( + self, + event_type: typing.Type[EventT], + callback: typing.Callable[[EventT], typing.Coroutine[None, typing.Any, None]], + ) -> bool: + if event_type not in self._listeners: + return False + return callback in self._listeners[event_type] + def unsubscribe( self, event_type: typing.Type[EventT], From 051851f0882f30a27561ffd36381f01cbc4f0764 Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Sat, 27 Jun 2020 20:48:24 +0100 Subject: [PATCH 582/922] Added ability to fetch listener list. --- hikari/api/event_dispatcher.py | 17 +++++++++++++++++ hikari/impl/event_manager_core.py | 8 ++++++++ 2 files changed, 25 insertions(+) diff --git a/hikari/api/event_dispatcher.py b/hikari/api/event_dispatcher.py index 6aa6232592..79ed73b443 100644 --- a/hikari/api/event_dispatcher.py +++ b/hikari/api/event_dispatcher.py @@ -174,6 +174,23 @@ async def on_message(event): `hikari.api.event_dispatcher.IEventDispatcherBase.wait_for` """ + def get_listeners( + self, event_type: typing.Type[EventT], + ) -> typing.Optional[typing.Collection[typing.Callable[[EventT], typing.Coroutine[None, typing.Any, None]]]]: + """Get the listeners for a given event type, if there are any. + + Parameters + ---------- + event_type : typing.Type[T] + The event type to look for. + + Returns + ------- + typing.Optional[typing.Collection[typing.Callable[[EventT], typing.Coroutine[None, typing.Any, None]]] + A copy of the collection of listeners for the event, or `None` if + none are registered. + """ + def has_listener( self, event_type: typing.Type[EventT], diff --git a/hikari/impl/event_manager_core.py b/hikari/impl/event_manager_core.py index 57197b8f22..f6cf064d5b 100644 --- a/hikari/impl/event_manager_core.py +++ b/hikari/impl/event_manager_core.py @@ -111,6 +111,14 @@ async def wrapper(event: EventT) -> None: return callback + def get_listeners( + self, event_type: typing.Type[EventT], + ) -> typing.Optional[typing.Collection[typing.Callable[[EventT], typing.Coroutine[None, typing.Any, None]]]]: + items = self._listeners.get(event_type) + if items is not None: + return items[:] + return None + def has_listener( self, event_type: typing.Type[EventT], From 1caac8b49f288e8ef20971392c524a8354bcbbb1 Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Sat, 27 Jun 2020 12:23:20 +0100 Subject: [PATCH 583/922] Added artifact expiry limits. --- ci/gitlab/bases.yml | 15 --------------- ci/gitlab/gitlab-templates.yml | 3 +++ ci/gitlab/linting.yml | 6 +++--- ci/gitlab/pages.yml | 1 + ci/gitlab/tests.yml | 4 ++-- 5 files changed, 9 insertions(+), 20 deletions(-) diff --git a/ci/gitlab/bases.yml b/ci/gitlab/bases.yml index e26c736c9d..a5bd599b9d 100644 --- a/ci/gitlab/bases.yml +++ b/ci/gitlab/bases.yml @@ -15,19 +15,11 @@ # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . -### -### All job settings. -### -.any-job: - artifacts: - expire_in: 1 month - ### ### Mark a job as having reactive defaults, such as firing on merge requests, ### new tags, new branches, schedules, new commits; all by default. ### .reactive-job: - extends: .any-job rules: - if: "$CI_PIPELINE_SOURCE == 'web'" - if: "$CI_PIPELINE_SOURCE == 'schedule'" @@ -42,37 +34,30 @@ ### the correct directory structure. ### .busybox:musl: - extends: .any-job image: busybox:1.31.1-musl ### ### CPython 3.8 configurations. ### .cpython:3.8.0: - extends: .any-job image: python:3.8.0 .cpython:3.8.1: - extends: .any-job image: python:3.8.1 .cpython:3.8.2: - extends: .any-job image: python:3.8.2 .cpython:3.8.3: - extends: .any-job image: python:3.8.3 .cpython:3.8: - extends: .any-job image: python:3.8 ### ### CPython 3.9.0 configuration. ### .cpython:3.9rc: - extends: .any-job image: python:3.9-rc ### diff --git a/ci/gitlab/gitlab-templates.yml b/ci/gitlab/gitlab-templates.yml index 848f6ae38c..cbc60e9cba 100644 --- a/ci/gitlab/gitlab-templates.yml +++ b/ci/gitlab/gitlab-templates.yml @@ -30,6 +30,7 @@ include: ### dependency_scanning: artifacts: + expire_in: 3 days when: always interruptible: true retry: 1 @@ -50,6 +51,7 @@ dependency_scanning: ### license_scanning: artifacts: + expire_in: 3 days when: always interruptible: true retry: 1 @@ -67,6 +69,7 @@ license_scanning: ### sast: artifacts: + expire_in: 3 days when: always interruptible: true retry: 1 diff --git a/ci/gitlab/linting.yml b/ci/gitlab/linting.yml index f842e9edee..e45ce4725d 100644 --- a/ci/gitlab/linting.yml +++ b/ci/gitlab/linting.yml @@ -37,11 +37,10 @@ flake8: allow_failure: true artifacts: - when: always - paths: - - public/flake8 + expire_in: 2 days reports: junit: public/flake8-junit.xml + when: always extends: .lint script: - nox -s flake8 --no-error-on-external-run @@ -66,6 +65,7 @@ mypy: script: - nox -s mypy --no-error-on-external-run artifacts: + expire_in: 2 days reports: junit: public/mypy.xml when: always diff --git a/ci/gitlab/pages.yml b/ci/gitlab/pages.yml index 662aa3adce..4dfb7df0a1 100644 --- a/ci/gitlab/pages.yml +++ b/ci/gitlab/pages.yml @@ -61,6 +61,7 @@ pdoc3: ### pages: artifacts: + expire_in: 2 days paths: - public/ dependencies: diff --git a/ci/gitlab/tests.yml b/ci/gitlab/tests.yml index 6ee7096c35..417dc472e3 100644 --- a/ci/gitlab/tests.yml +++ b/ci/gitlab/tests.yml @@ -21,8 +21,8 @@ ### .test: artifacts: + expire_in: 2 days paths: - - public/coverage - public/*.coverage reports: junit: public/tests.xml @@ -63,8 +63,8 @@ coverage:results: ### test:win32: artifacts: + expire_in: 2 days paths: - - public/coverage - public/*.coverage reports: junit: public/tests.xml From 1c67676a211a8dc8d6bbd68d7ce39a3702aa1e93 Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Sat, 27 Jun 2020 12:26:43 +0100 Subject: [PATCH 584/922] Fixed CI issue. --- ci/gitlab/tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ci/gitlab/tests.yml b/ci/gitlab/tests.yml index 417dc472e3..bf9f69e504 100644 --- a/ci/gitlab/tests.yml +++ b/ci/gitlab/tests.yml @@ -21,7 +21,7 @@ ### .test: artifacts: - expire_in: 2 days + expire_in: 2 days paths: - public/*.coverage reports: From 7586677c138834651b2bcd107cbcef7900b8b277 Mon Sep 17 00:00:00 2001 From: davfsa Date: Sat, 27 Jun 2020 16:00:35 +0200 Subject: [PATCH 585/922] Added some missing tests --- coverage.ini | 1 + hikari/cli.py | 1 - hikari/impl/entity_factory.py | 12 +-- hikari/models/audit_logs.py | 4 +- hikari/models/embeds.py | 19 +--- hikari/utilities/reflect.py | 2 +- hikari/utilities/undefined.py | 2 +- tests/hikari/impl/test_entity_factory.py | 13 ++- tests/hikari/models/.gitkeep | 0 tests/hikari/models/test_audit_logs.py | 14 +++ tests/hikari/models/test_channels.py | 104 ++++++++++++++++++++ tests/hikari/models/test_colours.py | 23 +++++ tests/hikari/models/test_intents.py | 12 ++- tests/hikari/net/test_routes.py | 37 +++++++ tests/hikari/utilities/test_cdn.py | 8 ++ tests/hikari/utilities/test_data_binding.py | 5 + tests/hikari/utilities/test_reflect.py | 2 + tests/hikari/utilities/test_snowflake.py | 47 +++++++-- tests/hikari/utilities/test_undefined.py | 7 +- 19 files changed, 271 insertions(+), 42 deletions(-) delete mode 100644 tests/hikari/models/.gitkeep create mode 100644 tests/hikari/models/test_colours.py diff --git a/coverage.ini b/coverage.ini index 554789ab1d..e8577629ae 100644 --- a/coverage.ini +++ b/coverage.ini @@ -6,6 +6,7 @@ timid = false source = hikari omit = hikari/__main__.py + hikari/cli.py hikari/_about.py .nox/* diff --git a/hikari/cli.py b/hikari/cli.py index 8abc8ae8e6..d6f6c886dc 100644 --- a/hikari/cli.py +++ b/hikari/cli.py @@ -16,7 +16,6 @@ # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . """Provides the `python -m hikari` and `hikari` commands to the shell.""" - from __future__ import annotations import inspect diff --git a/hikari/impl/entity_factory.py b/hikari/impl/entity_factory.py index ec1eb2e5af..37fcfce56d 100644 --- a/hikari/impl/entity_factory.py +++ b/hikari/impl/entity_factory.py @@ -583,7 +583,7 @@ def deserialize_embed(self, payload: data_binding.JSONObject) -> embed_models.Em author.name = author_payload.get("name") author.url = author_payload.get("url") - if (icon_url := author_payload.get("icon_url")) is not None: + if icon_url := author_payload.get("icon_url"): author.icon = embed_models.EmbedResource(resource=files.ensure_resource(icon_url)) author.icon.proxy_resource = files.ensure_resource(author_payload.get("proxy_icon_url")) else: @@ -626,7 +626,7 @@ def serialize_embed( # noqa: C901 if embed.color is not None: payload["color"] = int(embed.color) - if embed.footer: + if embed.footer is not None: footer_payload: data_binding.JSONObject = {} if embed.footer.text is not None: @@ -640,7 +640,7 @@ def serialize_embed( # noqa: C901 payload["footer"] = footer_payload - if embed.image: + if embed.image is not None: image_payload: data_binding.JSONObject = {} if not isinstance(embed.image.resource, files.WebResource): @@ -649,7 +649,7 @@ def serialize_embed( # noqa: C901 image_payload["url"] = embed.image.url payload["image"] = image_payload - if embed.thumbnail: + if embed.thumbnail is not None: thumbnail_payload: data_binding.JSONObject = {} if not isinstance(embed.thumbnail.resource, files.WebResource): @@ -658,7 +658,7 @@ def serialize_embed( # noqa: C901 thumbnail_payload["url"] = embed.thumbnail.url payload["thumbnail"] = thumbnail_payload - if embed.author: + if embed.author is not None: author_payload: data_binding.JSONObject = {} if embed.author.name is not None: @@ -838,7 +838,7 @@ def deserialize_integration(self, payload: data_binding.JSONObject) -> guild_mod guild_integration.expire_grace_period = datetime.timedelta(days=payload["expire_grace_period"]) guild_integration.user = self.deserialize_user(payload["user"]) - if (last_synced_at := payload["synced_at"]) is not None: + if (last_synced_at := payload.get("synced_at")) is not None: last_synced_at = date.iso8601_datetime_string_to_datetime(last_synced_at) guild_integration.last_synced_at = last_synced_at diff --git a/hikari/models/audit_logs.py b/hikari/models/audit_logs.py index 31472b5840..42d346f5a1 100644 --- a/hikari/models/audit_logs.py +++ b/hikari/models/audit_logs.py @@ -328,8 +328,8 @@ class AuditLog: webhooks: typing.Mapping[snowflake.Snowflake, webhooks_.Webhook] = attr.ib(repr=False) """A mapping of the objects of webhooks found in this audit log.""" - def __iter__(self) -> typing.Iterable[AuditLogEntry]: - return self.entries.values() + def __iter__(self) -> typing.Iterator[AuditLogEntry]: + return iter(self.entries.values()) def __len__(self) -> int: return len(self.entries) diff --git a/hikari/models/embeds.py b/hikari/models/embeds.py index ae9e4d4c54..960f880da5 100644 --- a/hikari/models/embeds.py +++ b/hikari/models/embeds.py @@ -46,15 +46,6 @@ def _maybe_color(value: typing.Optional[colors.ColorLike]) -> typing.Optional[co return colors.Color.of(value) if value is not None else None -class _TruthyEmbedComponentMixin: - __slots__: typing.Sequence[str] = () - - __attrs_attrs__: typing.ClassVar[typing.Tuple[attr.Attribute, ...]] - - def __bool__(self) -> bool: - return any(getattr(self, attrib.name, None) for attrib in self.__attrs_attrs__) - - @attr.s(eq=True, slots=True, kw_only=True) class EmbedResource(files.Resource): """A base type for any resource provided in an embed. @@ -104,7 +95,7 @@ def stream( @attr.s(eq=True, hash=False, kw_only=True, slots=True) -class EmbedFooter(_TruthyEmbedComponentMixin): +class EmbedFooter: """Represents an embed footer.""" text: typing.Optional[str] = attr.ib(default=None, repr=True) @@ -115,7 +106,7 @@ class EmbedFooter(_TruthyEmbedComponentMixin): @attr.s(eq=True, hash=False, kw_only=True, slots=True) -class EmbedImage(EmbedResource, _TruthyEmbedComponentMixin): +class EmbedImage(EmbedResource): """Represents an embed image.""" height: typing.Optional[int] = attr.ib(default=None, repr=False, init=False) @@ -138,7 +129,7 @@ class EmbedImage(EmbedResource, _TruthyEmbedComponentMixin): @attr.s(eq=True, hash=False, kw_only=True, slots=True) -class EmbedVideo(EmbedResource, _TruthyEmbedComponentMixin): +class EmbedVideo(EmbedResource): """Represents an embed video. !!! note @@ -158,7 +149,7 @@ class yourself.** @attr.s(eq=True, hash=False, kw_only=True, slots=True) -class EmbedProvider(_TruthyEmbedComponentMixin): +class EmbedProvider: """Represents an embed provider. !!! note @@ -179,7 +170,7 @@ class yourself.** @attr.s(eq=True, hash=False, kw_only=True, slots=True) -class EmbedAuthor(_TruthyEmbedComponentMixin): +class EmbedAuthor: """Represents an author of an embed.""" name: typing.Optional[str] = attr.ib(default=None, repr=True) diff --git a/hikari/utilities/reflect.py b/hikari/utilities/reflect.py index 70b2e90224..3ec5a00620 100644 --- a/hikari/utilities/reflect.py +++ b/hikari/utilities/reflect.py @@ -84,7 +84,7 @@ def get_logger(obj: typing.Union[typing.Type[typing.Any], typing.Any], *addition logging.Logger The _LOGGER to use. """ - if isinstance(obj, str): # noqa: IFSTMT001 - Oneliner (makes it unreadable) + if isinstance(obj, str): str_obj = obj else: str_obj = (obj if isinstance(obj, type) else type(obj)).__module__ diff --git a/hikari/utilities/undefined.py b/hikari/utilities/undefined.py index 682b877867..507e279fac 100644 --- a/hikari/utilities/undefined.py +++ b/hikari/utilities/undefined.py @@ -34,7 +34,7 @@ def __bool__(self) -> bool: return False def __repr__(self) -> str: - return "" + return "UNDEFINED" def __str__(self) -> str: return "UNDEFINED" diff --git a/tests/hikari/impl/test_entity_factory.py b/tests/hikari/impl/test_entity_factory.py index 6ca40aba11..3df13e24b0 100644 --- a/tests/hikari/impl/test_entity_factory.py +++ b/tests/hikari/impl/test_entity_factory.py @@ -959,7 +959,7 @@ def test_deserialize_embed_with_partial_fields(self, entity_factory_impl, embed_ "thumbnail": {}, "video": {}, "provider": {}, - "author": {}, + "author": {"name": "author name"}, "fields": [{"name": "title", "value": "some value"}], } ) @@ -975,7 +975,9 @@ def test_deserialize_embed_with_partial_fields(self, entity_factory_impl, embed_ # EmbedProvider assert embed.provider is None # EmbedAuthor - assert embed.author is None + assert embed.author.name == "author name" + assert embed.author.url is None + assert embed.author.icon is None # EmbedField assert len(embed.fields) == 1 field = embed.fields[0] @@ -1099,8 +1101,6 @@ def test_serialize_embed_with_null_sub_fields(self, entity_factory_impl): timestamp=datetime.datetime(2020, 5, 29, 20, 37, 22, 865139), color=color_models.Color(321321), footer=embed_models.EmbedFooter(), - image=None, - thumbnail=None, author=embed_models.EmbedAuthor(), ) ) @@ -1110,6 +1110,8 @@ def test_serialize_embed_with_null_sub_fields(self, entity_factory_impl): "url": "https://some-url", "timestamp": "2020-05-29T20:37:22.865139", "color": 321321, + "author": {}, + "footer": {}, } assert resources == [] @@ -1400,7 +1402,7 @@ def test_deserialize_guild_integration_with_null_and_unset_fields(self, entity_f "account": {"id": "6969", "name": "Blaze it"}, "enabled": True, "syncing": False, - "role_id": "98494949", + "role_id": None, "expire_behavior": 1, "expire_grace_period": 7, "user": user_payload, @@ -1408,6 +1410,7 @@ def test_deserialize_guild_integration_with_null_and_unset_fields(self, entity_f } ) assert integration.is_emojis_enabled is None + assert integration.role_id is None assert integration.last_synced_at is None @pytest.fixture() diff --git a/tests/hikari/models/.gitkeep b/tests/hikari/models/.gitkeep deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests/hikari/models/test_audit_logs.py b/tests/hikari/models/test_audit_logs.py index 88ccf72081..17bd0da276 100644 --- a/tests/hikari/models/test_audit_logs.py +++ b/tests/hikari/models/test_audit_logs.py @@ -27,3 +27,17 @@ def test_AuditLogChangeKey_str_operator(): def test_AuditLogEventType_str_operator(): event_type = audit_logs.AuditLogEventType(80) assert str(event_type) == "INTEGRATION_CREATE" + + +def test_AuditLog_itter(): + entry = audit_logs.AuditLogEntry() + entry.id = 1 + entry2 = audit_logs.AuditLogEntry() + entry2.id = 2 + entry3 = audit_logs.AuditLogEntry() + entry3.id = 3 + audit_log = audit_logs.AuditLog() + audit_log.entries = {1: entry, 2: entry2, 3: entry3} + + assert len(audit_log) == 3 + assert [*audit_log] == [entry, entry2, entry3] diff --git a/tests/hikari/models/test_channels.py b/tests/hikari/models/test_channels.py index 00340c29bb..8b3db8f35e 100644 --- a/tests/hikari/models/test_channels.py +++ b/tests/hikari/models/test_channels.py @@ -15,9 +15,16 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . +import datetime + +import pytest +import mock from hikari.models import channels from hikari.models import users +from hikari.models import permissions +from hikari.utilities import files +from tests.hikari import hikari_test_helpers def test_ChannelType_str_operator(): @@ -68,3 +75,100 @@ def test_GroupDMChannel_str_operator_when_name_is_None(): other_user.username = "nice" channel.recipients = {1: user, 2: other_user} assert str(channel) == "GroupDMChannel with: snoop#0420, nice#6969" + + +def test_PermissionOverwrite_unset(): + overwrite = channels.PermissionOverwrite(type=channels.PermissionOverwriteType.MEMBER) + overwrite.allow = permissions.Permission.CREATE_INSTANT_INVITE + overwrite.deny = permissions.Permission.CHANGE_NICKNAME + assert overwrite.unset == permissions.Permission(-67108866) + + +@pytest.mark.asyncio +async def test_TextChannel_send(): + channel = channels.TextChannel() + channel.id = 123 + channel.app = mock.Mock() + channel.app.rest.create_message = mock.AsyncMock() + mock_attachment = mock.Mock() + mock_embed = mock.Mock() + mock_attachments = [mock.Mock(), mock.Mock(), mock.Mock()] + + await channel.send( + text="test content", + nonce="abc123", + tts=True, + attachment=mock_attachment, + attachments=mock_attachments, + embed=mock_embed, + mentions_everyone=False, + user_mentions=[123, 456], + role_mentions=[789, 567], + ) + + channel.app.rest.create_message.assert_called_once_with( + channel=123, + text="test content", + nonce="abc123", + tts=True, + attachment=mock_attachment, + attachments=mock_attachments, + embed=mock_embed, + mentions_everyone=False, + user_mentions=[123, 456], + role_mentions=[789, 567], + ) + + +@pytest.mark.asyncio +async def test_TextChannel_history(): + channel = channels.TextChannel() + channel.id = 123 + channel.app = mock.Mock() + channel.app.rest.fetch_messages = mock.AsyncMock() + + await channel.history( + before=datetime.datetime(2020, 4, 1, 1, 0, 0), + after=datetime.datetime(2020, 4, 1, 0, 0, 0), + around=datetime.datetime(2020, 4, 1, 0, 30, 0), + ) + + channel.app.rest.fetch_messages.assert_called_once_with( + 123, + before=datetime.datetime(2020, 4, 1, 1, 0, 0), + after=datetime.datetime(2020, 4, 1, 0, 0, 0), + around=datetime.datetime(2020, 4, 1, 0, 30, 0), + ) + + +def test_GroupDMChannel_icon(): + channel = hikari_test_helpers.unslot_class(channels.GroupDMChannel)() + channel.format_icon = mock.Mock(return_value="icon") + + assert channel.icon == "icon" + channel.format_icon.assert_called_once() + + +def test_GroupDMChannel_format_icon(): + channel = channels.GroupDMChannel() + channel.id = 123 + channel.icon_hash = "456abc" + + assert channel.format_icon(format="jpeg", size=16) == files.URL( + "https://cdn.discordapp.com/channel-icons/123/456abc.jpeg?size=16" + ) + + +def test_GroupDMChannel_format_icon_without_optionals(): + channel = channels.GroupDMChannel() + channel.id = 123 + channel.icon_hash = "456abc" + + assert channel.format_icon() == files.URL("https://cdn.discordapp.com/channel-icons/123/456abc.png?size=4096") + + +def test_GroupDMChannel_format_icon_when_hash_is_None(): + channel = channels.GroupDMChannel() + channel.icon_hash = None + + assert channel.format_icon() is None diff --git a/tests/hikari/models/test_colours.py b/tests/hikari/models/test_colours.py new file mode 100644 index 0000000000..a8110b90ec --- /dev/null +++ b/tests/hikari/models/test_colours.py @@ -0,0 +1,23 @@ +# -*- coding: utf-8 -*- +# Copyright © Nekoka.tt 2019-2020 +# +# This file is part of Hikari. +# +# Hikari is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Hikari is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Hikari. If not, see . +from hikari.models import colors +from hikari.models import colours + + +def test_colours(): + assert colors.Color is colours.Colour diff --git a/tests/hikari/models/test_intents.py b/tests/hikari/models/test_intents.py index c21a2518ca..2cf3ca18bf 100644 --- a/tests/hikari/models/test_intents.py +++ b/tests/hikari/models/test_intents.py @@ -20,5 +20,15 @@ def test_Intent_str_operator(): - intent = intents.Intent(1 << 9) + intent = intents.Intent.GUILD_MESSAGES assert str(intent) == "GUILD_MESSAGES" + + +def test_Intent_is_privileged(): + intent = intents.Intent.GUILD_MESSAGES + intent2 = intents.Intent.GUILD_MEMBERS + intent3 = intents.Intent.GUILD_PRESENCES + + assert not intent.is_privileged + assert intent2.is_privileged + assert intent3.is_privileged diff --git a/tests/hikari/net/test_routes.py b/tests/hikari/net/test_routes.py index 3b080b938f..24710e62bf 100644 --- a/tests/hikari/net/test_routes.py +++ b/tests/hikari/net/test_routes.py @@ -15,3 +15,40 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . +import pytest +import mock + +from hikari.net import routes + + +class TestCompiledRoute: + @pytest.fixture + def compiled_route(self): + return routes.CompiledRoute( + major_param_hash="abc123", route=mock.Mock(method="GET"), compiled_path="/some/endpoint" + ) + + def test_method(self, compiled_route): + assert compiled_route.method == "GET" + + def test_create_url(self, compiled_route): + assert compiled_route.create_url("https://some.url/api") == "https://some.url/api/some/endpoint" + + def test_create_real_bucket_hash(self, compiled_route): + assert compiled_route.create_real_bucket_hash("UNKNOWN") == "UNKNOWN;abc123" + + def test__str__(self, compiled_route): + assert str(compiled_route) == "GET /some/endpoint" + + +class TestRoute: + @pytest.fixture + def route(self): + return routes.Route(method="GET", path_template="/some/endpoint/{channel}") + + def test_compile(self, route): + expected = routes.CompiledRoute(route=route, compiled_path="/some/endpoint/1234", major_param_hash="1234") + assert route.compile(channel=1234) == expected + + def test__str__(self, route): + assert str(route) == "/some/endpoint/{channel}" diff --git a/tests/hikari/utilities/test_cdn.py b/tests/hikari/utilities/test_cdn.py index 8ce3713c26..c6adfe55bd 100644 --- a/tests/hikari/utilities/test_cdn.py +++ b/tests/hikari/utilities/test_cdn.py @@ -39,3 +39,11 @@ def test_generate_cdn_url_with_invalid_size_out_of_limits(): def test_generate_cdn_url_with_invalid_size_now_power_of_two(): with pytest.raises(ValueError): cdn.generate_cdn_url("not", "a", "path", format_="neko", size=111) + + +def test_get_default_avatar_index(): + assert cdn.get_default_avatar_index("1234") == 4 + + +def test_get_default_avatar_url(): + assert cdn.get_default_avatar_url("1234") == files.URL("https://cdn.discordapp.com/embed/avatars/4.png") diff --git a/tests/hikari/utilities/test_data_binding.py b/tests/hikari/utilities/test_data_binding.py index 2deb50d197..ab01e427a2 100644 --- a/tests/hikari/utilities/test_data_binding.py +++ b/tests/hikari/utilities/test_data_binding.py @@ -216,6 +216,11 @@ def test_put_snowflake_array(self): builder.put_snowflake_array("DESU!", [123, 456, 987, 115]) assert builder == {"DESU!": ["123", "456", "987", "115"]} + def test_put_snowflake_array_undefined(self): + builder = data_binding.JSONObjectBuilder() + builder.put_snowflake_array("test", undefined.UNDEFINED) + assert builder == {} + class TestCastJSONArray: def test_cast_is_invoked_with_each_item(self): diff --git a/tests/hikari/utilities/test_reflect.py b/tests/hikari/utilities/test_reflect.py index 949d25108e..333aa67522 100644 --- a/tests/hikari/utilities/test_reflect.py +++ b/tests/hikari/utilities/test_reflect.py @@ -122,6 +122,8 @@ class Class: ([Class()], __name__), ([Class, "Foooo", "bar", "123"], f"{__name__}.Foooo.bar.123"), ([Class(), "qux", "QUx", "940"], f"{__name__}.qux.QUx.940"), + (["test"], "test"), + (["test", "testing"], "test.testing"), ], ) def test_get_logger(args, expected_name): diff --git a/tests/hikari/utilities/test_snowflake.py b/tests/hikari/utilities/test_snowflake.py index 88aef07aef..9ab1e0fd07 100644 --- a/tests/hikari/utilities/test_snowflake.py +++ b/tests/hikari/utilities/test_snowflake.py @@ -22,15 +22,17 @@ from hikari.utilities import snowflake -class TestSnowflake: - @pytest.fixture() - def raw_id(self): - return 537_340_989_808_050_216 +@pytest.fixture() +def raw_id(): + return 537_340_989_808_050_216 + - @pytest.fixture() - def neko_snowflake(self, raw_id): - return snowflake.Snowflake(raw_id) +@pytest.fixture() +def neko_snowflake(raw_id): + return snowflake.Snowflake(raw_id) + +class TestSnowflake: def test_created_at(self, neko_snowflake): assert neko_snowflake.created_at == datetime.datetime( 2019, 1, 22, 18, 41, 15, 283_000, tzinfo=datetime.timezone.utc @@ -84,3 +86,34 @@ def test_max(self): sf = snowflake.Snowflake.max() assert sf == (1 << 63) - 1 assert snowflake.Snowflake.max() is sf + + +class TestUnique: + @pytest.fixture + def neko_unique(self, neko_snowflake): + class NekoUnique(snowflake.Unique): + id = neko_snowflake + + return NekoUnique() + + def test_created_at(self, neko_unique): + assert neko_unique.created_at == datetime.datetime( + 2019, 1, 22, 18, 41, 15, 283_000, tzinfo=datetime.timezone.utc + ) + + def test__hash__(self, neko_unique, raw_id): + assert hash(neko_unique) == raw_id + + def test__eq__(self, neko_snowflake, raw_id): + class NekoUnique(snowflake.Unique): + id = neko_snowflake + + class NekoUnique2(snowflake.Unique): + id = neko_snowflake + + unique1 = NekoUnique() + unique2 = NekoUnique() + + assert unique1 == unique2 + assert unique1 != NekoUnique2() + assert unique1 != raw_id diff --git a/tests/hikari/utilities/test_undefined.py b/tests/hikari/utilities/test_undefined.py index 510a220eb8..d790ab36e7 100644 --- a/tests/hikari/utilities/test_undefined.py +++ b/tests/hikari/utilities/test_undefined.py @@ -18,12 +18,11 @@ import pytest from hikari.utilities import undefined -from tests.hikari import hikari_test_helpers class TestUndefined: def test_repr(self): - assert repr(undefined.UNDEFINED) == "" + assert repr(undefined.UNDEFINED) == "UNDEFINED" def test_str(self): assert str(undefined.UNDEFINED) == "UNDEFINED" @@ -35,8 +34,8 @@ def test_bool(self): def test_singleton_behaviour(self): assert undefined.UNDEFINED is undefined.UNDEFINED assert undefined.UNDEFINED == undefined.UNDEFINED - assert undefined.UNDEFINED != None - assert undefined.UNDEFINED != False + assert undefined.UNDEFINED is not None + assert undefined.UNDEFINED is not False def test_count(self): assert undefined.count(9, 18, undefined.UNDEFINED, 36, undefined.UNDEFINED, 54) == 2 From d81fd8dbef6ae965f7c24616b0ee9c18c3d7f88b Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Sun, 28 Jun 2020 11:14:12 +0100 Subject: [PATCH 586/922] Fixed an invalid typehint in hikari.api.rest for the context manager. --- hikari/api/rest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hikari/api/rest.py b/hikari/api/rest.py index 5b611cdd86..683c1bd3e3 100644 --- a/hikari/api/rest.py +++ b/hikari/api/rest.py @@ -126,7 +126,7 @@ class IRESTClientFactory(abc.ABC): __slots__: typing.Sequence[str] = () @abc.abstractmethod - def acquire(self, token: str, token_type: str = strings.BEARER_TOKEN) -> IRESTClient: + def acquire(self, token: str, token_type: str = strings.BEARER_TOKEN) -> IRESTClientContextManager: """Acquire a REST client for the given authentication details. Parameters From bda5c59d6e0cf890d51695e742fb26b493f5b400 Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Fri, 26 Jun 2020 16:51:54 +0100 Subject: [PATCH 587/922] HTTP pattern tidyup - Removed http_client.py - Split http_settings into proxy and http settings - Removed connector option to make settings pickleable - Made gateway and rest manage their own client sessions - Made gateways reserve a single connection in a pool rather than reserving like 100 pooled connections for no reason. - Removed implicit http proxy basicauth and provided custom implementation - Provided custom timeout management settings. - Updated docs. - Moved iterators to a separate file. - Fixed other minor buglets. Added some more stream ops. Amended issues with crypto false pos. Amended typehints in iterators.py Moved files back to right place. --- ci/gitlab/tests.yml | 2 +- ci/pdoc.nox.py | 2 +- docs/documentation.mako | 10 +- hikari/__init__.py | 1 + hikari/api/bot.py | 18 +- hikari/api/rest.py | 21 + hikari/impl/bot.py | 81 ++- hikari/impl/entity_factory.py | 2 +- hikari/impl/gateway_zookeeper.py | 50 +- hikari/impl/rest.py | 116 ++-- hikari/models/applications.py | 2 +- hikari/models/channels.py | 4 +- hikari/models/emojis.py | 2 +- hikari/models/guilds.py | 2 +- hikari/models/invites.py | 2 +- hikari/models/users.py | 2 +- hikari/models/webhooks.py | 2 +- hikari/net/config.py | 133 ++++ hikari/net/gateway.py | 84 ++- hikari/net/helpers.py | 57 ++ hikari/net/http_client.py | 277 -------- hikari/net/http_settings.py | 104 --- hikari/net/iterators.py | 556 ---------------- hikari/net/rate_limits.py | 2 +- hikari/net/rest.py | 306 ++++----- .../{rest_utils.py => special_endpoints.py} | 232 ++++++- hikari/net/strings.py | 2 + hikari/utilities/cdn.py | 2 +- hikari/utilities/files.py | 4 +- hikari/utilities/iterators.py | 592 ++++++++++++++++++ hikari/utilities/spel.py | 93 +++ pages/index.html | 2 +- tests/hikari/client_session_stub.py | 64 ++ tests/hikari/net/test_gateway.py | 267 ++++---- tests/hikari/net/test_http_client.py | 208 ------ 35 files changed, 1726 insertions(+), 1578 deletions(-) create mode 100644 hikari/net/config.py create mode 100644 hikari/net/helpers.py delete mode 100644 hikari/net/http_client.py delete mode 100644 hikari/net/http_settings.py delete mode 100644 hikari/net/iterators.py rename hikari/net/{rest_utils.py => special_endpoints.py} (71%) create mode 100644 hikari/utilities/iterators.py create mode 100644 hikari/utilities/spel.py create mode 100644 tests/hikari/client_session_stub.py delete mode 100644 tests/hikari/net/test_http_client.py diff --git a/ci/gitlab/tests.yml b/ci/gitlab/tests.yml index bf9f69e504..4a8e17092c 100644 --- a/ci/gitlab/tests.yml +++ b/ci/gitlab/tests.yml @@ -123,7 +123,7 @@ test:twemoji_mapping: - if: "$CI_PIPELINE_SOURCE == 'schedule' && $TEST_TWEMOJI_MAPPING != null" - changes: - hikari/models/emojis.py - - hikari/utilities/files.py + - hikari.utilities.files.py - ci/twemoji.nox.py script: - apt-get install -qy git gcc g++ make diff --git a/ci/pdoc.nox.py b/ci/pdoc.nox.py index 189c6388da..6a4c5b339d 100644 --- a/ci/pdoc.nox.py +++ b/ci/pdoc.nox.py @@ -25,7 +25,7 @@ def pdoc(session: nox.Session) -> None: """Generate documentation with pdoc.""" session.install("-r", "requirements.txt") - session.install("pdoc3") + session.install("git+https://github.com/pdoc3/pdoc@83a8c400bcf9109d4753c46ad2f71a4e57114871") session.install("sphobjinv") session.run( diff --git a/docs/documentation.mako b/docs/documentation.mako index 4b0d08aed8..98b0d33ccb 100644 --- a/docs/documentation.mako +++ b/docs/documentation.mako @@ -66,6 +66,7 @@ <% import abc import enum + import functools import inspect import textwrap import typing @@ -78,6 +79,7 @@ QUAL_ASYNC_DEF = "async def" QUAL_CLASS = "class" QUAL_DATACLASS = "dataclass" + QUAL_CACHED_PROPERTY = "cached property" QUAL_CONST = "const" QUAL_DEF = "def" QUAL_ENUM = "enum" @@ -123,8 +125,12 @@ if getattr(dobj.obj, "__isabstractmethod__", False): prefix = f"{QUAL_ABC} " - if hasattr(dobj.obj, "__get__"): - prefix = f"{prefix}{QUAL_PROPERTY} " + descriptor = None + is_descriptor = hasattr(dobj.cls, "obj") and (descriptor := dobj.cls.obj.__dict__.get(dobj.name)) + + if is_descriptor and isinstance(descriptor, (property, functools.cached_property)): + qual = QUAL_CACHED_PROPERTY if isinstance(descriptor, functools.cached_property) else QUAL_PROPERTY + prefix = f"{prefix}{qual} " elif dobj.module.name == "typing" or dobj.docstring and dobj.docstring.casefold().startswith(("type hint", "typehint", "type alias")): prefix = f"{prefix}{QUAL_TYPEHINT} " elif all(not c.isalpha() or c.isupper() for c in dobj.name): diff --git a/hikari/__init__.py b/hikari/__init__.py index 12b43dd431..76fde1c1d0 100644 --- a/hikari/__init__.py +++ b/hikari/__init__.py @@ -34,5 +34,6 @@ from hikari._about import __license__ from hikari._about import __url__ from hikari._about import __version__ + from hikari.impl.bot import BotAppImpl as Bot from hikari.impl.rest import RESTClientFactoryImpl as RESTClientFactory diff --git a/hikari/api/bot.py b/hikari/api/bot.py index 3ba3bec5b6..bdb4b432d4 100644 --- a/hikari/api/bot.py +++ b/hikari/api/bot.py @@ -28,7 +28,7 @@ from hikari.api import event_dispatcher if typing.TYPE_CHECKING: - from hikari.net import http_settings as http_settings_ + pass class IBotApp(event_consumer.IEventConsumerApp, event_dispatcher.IEventDispatcherApp, abc.ABC): @@ -41,19 +41,3 @@ class IBotApp(event_consumer.IEventConsumerApp, event_dispatcher.IEventDispatche """ __slots__: typing.Sequence[str] = () - - @property - @abc.abstractmethod - def http_settings(self) -> http_settings_.HTTPSettings: - """HTTP settings to use for the shards when they get created. - - !!! info - This is stored only for bots, since shards are generated lazily on - start-up once sharding information has been retrieved from the REST - API on startup. - - Returns - ------- - hikari.net.http_settings.HTTPSettings - The HTTP settings to use. - """ diff --git a/hikari/api/rest.py b/hikari/api/rest.py index 683c1bd3e3..fb5173e5e6 100644 --- a/hikari/api/rest.py +++ b/hikari/api/rest.py @@ -32,6 +32,7 @@ from hikari.api import cache as cache_ from hikari.api import entity_factory as entity_factory_ + from hikari.net import config from hikari.net import rest as rest_ @@ -93,6 +94,16 @@ def executor(self) -> typing.Optional[concurrent.futures.Executor]: return `None` instead. """ + @property + @abc.abstractmethod + def http_settings(self) -> config.HTTPSettings: + """HTTP-specific settings.""" + + @property + @abc.abstractmethod + def proxy_settings(self) -> config.ProxySettings: + """Proxy-specific settings.""" + @abc.abstractmethod async def close(self) -> None: """Safely shut down all resources.""" @@ -146,6 +157,16 @@ def acquire(self, token: str, token_type: str = strings.BEARER_TOKEN) -> IRESTCl async def close(self) -> None: """Safely shut down all resources.""" + @property + @abc.abstractmethod + def http_settings(self) -> config.HTTPSettings: + """HTTP-specific settings.""" + + @property + @abc.abstractmethod + def proxy_settings(self) -> config.ProxySettings: + """Proxy-specific settings.""" + @abc.abstractmethod async def __aenter__(self) -> IRESTClientFactory: ... diff --git a/hikari/impl/bot.py b/hikari/impl/bot.py index b6ea8b7315..332906b31e 100644 --- a/hikari/impl/bot.py +++ b/hikari/impl/bot.py @@ -36,7 +36,7 @@ from hikari.impl import gateway_zookeeper from hikari.impl import stateless_cache as stateless_cache_impl from hikari.models import presences -from hikari.net import http_settings as http_settings_ +from hikari.net import config from hikari.net import rate_limits from hikari.net import rest from hikari.net import strings @@ -62,9 +62,6 @@ class BotAppImpl(gateway_zookeeper.AbstractGatewayZookeeper, bot.IBotApp): Parameters ---------- - config : hikari.utilities.undefined.UndefinedType or hikari.net.http_settings.HTTPSettings - Optional aiohttp settings to apply to the REST components, gateway - shards, and voice websockets. If undefined, then sane defaults are used. debug : bool Defaulting to `False`, if `True`, then each payload sent and received on the gateway will be dumped to debug logs, and every REST API request @@ -78,6 +75,8 @@ class BotAppImpl(gateway_zookeeper.AbstractGatewayZookeeper, bot.IBotApp): The version of the gateway to connect to. At the time of writing, only version `6` and version `7` (undocumented development release) are supported. This defaults to using v6. + http_settings : hikari.net.config.HTTPSettings or None + The HTTP-related settings to use. initial_activity : hikari.models.presences.Activity or None or hikari.utilities.undefined.UndefinedType The initial activity to have on each shard. initial_activity : hikari.models.presences.Status or hikari.utilities.undefined.UndefinedType @@ -108,16 +107,18 @@ class BotAppImpl(gateway_zookeeper.AbstractGatewayZookeeper, bot.IBotApp): Initializating logging means already have a handler in the root logger. This is usually achived by calling `logging.basicConfig` or adding the handler another way. + proxy_settings : hikari.net.config.ProxySettings or None + Settings to use for the proxy. rest_version : int The version of the REST API to connect to. At the time of writing, only version `6` and version `7` (undocumented development release) are supported. This defaults to v6. - shard_ids : typing.Set[int] or undefined.UndefinedType + shard_ids : typing.Set[int] or None A set of every shard ID that should be created and started on startup. - If left undefined along with `shard_count`, then auto-sharding is used + If left None along with `shard_count`, then auto-sharding is used instead, which is the default. - shard_count : int or undefined.UndefinedType - The number of shards in the entire application. If left undefined along + shard_count : int or None + The number of shards in the entire application. If left None along with `shard_ids`, then auto-sharding is used instead, which is the default. stateless : bool @@ -164,10 +165,11 @@ class BotAppImpl(gateway_zookeeper.AbstractGatewayZookeeper, bot.IBotApp): def __init__( self, *, - config: typing.Union[undefined.UndefinedType, http_settings_.HTTPSettings] = undefined.UNDEFINED, debug: bool = False, + executor: typing.Optional[concurrent.futures.Executor] = None, gateway_compression: bool = True, gateway_version: int = 6, + http_settings: typing.Optional[config.HTTPSettings] = None, initial_activity: typing.Union[undefined.UndefinedType, presences.Activity, None] = undefined.UNDEFINED, initial_idle_since: typing.Union[undefined.UndefinedType, datetime.datetime, None] = undefined.UNDEFINED, initial_is_afk: typing.Union[undefined.UndefinedType, bool] = undefined.UNDEFINED, @@ -175,12 +177,12 @@ def __init__( intents: typing.Optional[intents_.Intent] = None, large_threshold: int = 250, logging_level: typing.Union[str, int, None] = "INFO", + proxy_settings: typing.Optional[config.ProxySettings] = None, rest_version: int = 6, - rest_url: typing.Union[undefined.UndefinedType, str] = undefined.UNDEFINED, - shard_ids: typing.Union[typing.Set[int], undefined.UndefinedType] = undefined.UNDEFINED, - shard_count: typing.Union[int, undefined.UndefinedType] = undefined.UNDEFINED, + rest_url: typing.Optional[str] = None, + shard_ids: typing.Optional[typing.Set[int]] = None, + shard_count: typing.Optional[int] = None, stateless: bool = False, - thread_pool_executor: typing.Optional[concurrent.futures.Executor] = None, token: str, ) -> None: if logging_level is not None and not _LOGGER.hasHandlers(): @@ -189,8 +191,6 @@ def __init__( self.__print_banner() - config = http_settings_.HTTPSettings() if config is undefined.UNDEFINED else config - if stateless: self._cache = stateless_cache_impl.StatelessCacheImpl() _LOGGER.info("this application will be stateless! Cache-based operations will be unavailable!") @@ -202,27 +202,21 @@ def __init__( self._entity_factory = entity_factory_impl.EntityFactoryComponentImpl(app=self) self._global_ratelimit = rate_limits.ManualRateLimiter() - self._rest = rest.REST( # noqa: S106 - Possible hardcoded password - app=self, - config=config, - debug=debug, - global_ratelimit=self._global_ratelimit, - token=token, - token_type=strings.BOT_TOKEN, # nosec - rest_url=rest_url, - version=rest_version, - ) - self._thread_pool_executor = thread_pool_executor + self._executor = executor + + http_settings = config.HTTPSettings() if http_settings is None else http_settings + proxy_settings = config.ProxySettings() if proxy_settings is None else proxy_settings super().__init__( - config=config, debug=debug, + http_settings=http_settings, initial_activity=initial_activity, initial_idle_since=initial_idle_since, initial_is_afk=initial_is_afk, initial_status=initial_status, intents=intents, large_threshold=large_threshold, + proxy_settings=proxy_settings, shard_ids=shard_ids, shard_count=shard_count, token=token, @@ -230,6 +224,20 @@ def __init__( version=gateway_version, ) + self._rest = rest.REST( # noqa: S106 - Possible hardcoded password + app=self, + connector=None, + connector_owner=True, + debug=debug, + http_settings=self._http_settings, + global_ratelimit=self._global_ratelimit, + proxy_settings=self._proxy_settings, + token=token, + token_type=strings.BOT_TOKEN, # nosec + rest_url=rest_url, + version=rest_version, + ) + @property def event_dispatcher(self) -> event_dispatcher_.IEventDispatcherComponent: return self._event_manager @@ -242,21 +250,25 @@ def cache(self) -> cache_.ICacheComponent: def entity_factory(self) -> entity_factory_.IEntityFactoryComponent: return self._entity_factory + @property + def event_consumer(self) -> event_consumer_.IEventConsumerComponent: + return self._event_manager + @property def executor(self) -> typing.Optional[concurrent.futures.Executor]: - return self._thread_pool_executor + return self._executor @property - def rest(self) -> rest.REST: - return self._rest + def http_settings(self) -> config.HTTPSettings: + return self._http_settings @property - def event_consumer(self) -> event_consumer_.IEventConsumerComponent: - return self._event_manager + def proxy_settings(self) -> config.ProxySettings: + return self._proxy_settings @property - def http_settings(self) -> http_settings_.HTTPSettings: - return self._config + def rest(self) -> rest.REST: + return self._rest def listen( self, event_type: typing.Union[undefined.UndefinedType, typing.Type[EventT]] = undefined.UNDEFINED, @@ -298,6 +310,7 @@ def __print_banner() -> None: from hikari import _about version = _about.__version__ + # noinspection PyTypeChecker sourcefile = typing.cast(str, inspect.getsourcefile(_about)) path = os.path.abspath(os.path.dirname(sourcefile)) python_implementation = platform.python_implementation() diff --git a/hikari/impl/entity_factory.py b/hikari/impl/entity_factory.py index 37fcfce56d..dc5e3dfa60 100644 --- a/hikari/impl/entity_factory.py +++ b/hikari/impl/entity_factory.py @@ -46,9 +46,9 @@ from hikari.models import users as user_models from hikari.models import voices as voice_models from hikari.models import webhooks as webhook_models +from hikari.utilities import files from hikari.net import gateway from hikari.utilities import date -from hikari.utilities import files from hikari.utilities import snowflake from hikari.utilities import undefined diff --git a/hikari/impl/gateway_zookeeper.py b/hikari/impl/gateway_zookeeper.py index 7c31564b27..65be39a623 100644 --- a/hikari/impl/gateway_zookeeper.py +++ b/hikari/impl/gateway_zookeeper.py @@ -39,7 +39,7 @@ if typing.TYPE_CHECKING: from hikari.events import base as base_events - from hikari.net import http_settings + from hikari.net import config from hikari.models import gateway as gateway_models from hikari.models import intents as intents_ from hikari.models import presences @@ -64,13 +64,13 @@ class AbstractGatewayZookeeper(gateway_zookeeper.IGatewayZookeeperApp, abc.ABC): compression : bool Defaulting to `True`, if `True`, then zlib transport compression is used for each shard connection. If `False`, no compression is used. - config : hikari.utilities.undefined.UndefinedType or hikari.net.http_settings.HTTPSettings - Optional aiohttp settings to apply to the created shards. debug : bool Defaulting to `False`, if `True`, then each payload sent and received on the gateway will be dumped to debug logs. This will provide useful debugging context at the cost of performance. Generally you do not need to enable this. + http_settings : hikari.net.config.HTTPSettings + HTTP-related configuration. initial_activity : hikari.models.presences.Activity or None or hikari.utilities.undefined.UndefinedType The initial activity to have on each shard. initial_activity : hikari.models.presences.Status or hikari.utilities.undefined.UndefinedType @@ -88,12 +88,14 @@ class AbstractGatewayZookeeper(gateway_zookeeper.IGatewayZookeeperApp, abc.ABC): large_threshold : int The number of members that need to be in a guild for the guild to be considered large. Defaults to the maximum, which is `250`. - shard_ids : typing.Set[int] or undefined.UndefinedType + proxy_settings : hikari.net.config.ProxySettings + Proxy-related configuration. + shard_ids : typing.Set[int] or None A set of every shard ID that should be created and started on startup. - If left undefined along with `shard_count`, then auto-sharding is used + If left None along with `shard_count`, then auto-sharding is used instead, which is the default. - shard_count : int or undefined.UndefinedType - The number of shards in the entire application. If left undefined along + shard_count : int or None + The number of shards in the entire application. If left None along with `shard_ids`, then auto-sharding is used instead, which is the default. token : str @@ -139,32 +141,26 @@ def __init__( self, *, compression: bool, - config: http_settings.HTTPSettings, debug: bool, - initial_activity: typing.Union[undefined.UndefinedType, presences.Activity, None] = undefined.UNDEFINED, - initial_idle_since: typing.Union[undefined.UndefinedType, datetime.datetime, None] = undefined.UNDEFINED, - initial_is_afk: typing.Union[undefined.UndefinedType, bool] = undefined.UNDEFINED, - initial_status: typing.Union[undefined.UndefinedType, presences.Status] = undefined.UNDEFINED, + http_settings: config.HTTPSettings, + initial_activity: typing.Union[undefined.UndefinedType, presences.Activity, None], + initial_idle_since: typing.Union[undefined.UndefinedType, datetime.datetime, None], + initial_is_afk: typing.Union[undefined.UndefinedType, bool], + initial_status: typing.Union[undefined.UndefinedType, presences.Status], intents: typing.Optional[intents_.Intent], large_threshold: int, - shard_ids: typing.Union[typing.Set[int], undefined.UndefinedType] = undefined.UNDEFINED, - shard_count: typing.Union[int, undefined.UndefinedType] = undefined.UNDEFINED, + proxy_settings: config.ProxySettings, + shard_ids: typing.Optional[typing.Set[int]], + shard_count: typing.Optional[int], token: str, version: int, ) -> None: if undefined.count(shard_ids, shard_count) == 1: raise TypeError("You must provide values for both shard_ids and shard_count, or neither.") - if shard_ids is not undefined.UNDEFINED: - if not shard_ids: - raise ValueError("At least one shard ID must be specified if provided.") - if not all(shard_id >= 0 for shard_id in shard_ids): - raise ValueError("shard_ids must be greater than or equal to 0.") - if shard_count is not undefined.UNDEFINED and not all(shard_id < shard_count for shard_id in shard_ids): - raise ValueError("shard_ids must be less than the total shard_count.") - - self._aiohttp_config = config + self._debug = debug self._gather_task: typing.Optional[asyncio.Task[None]] = None + self._http_settings = http_settings self._initial_activity = initial_activity self._initial_idle_since = initial_idle_since self._initial_is_afk = initial_is_afk @@ -172,9 +168,10 @@ def __init__( self._intents = intents self._large_threshold = large_threshold self._max_concurrency = 1 + self._proxy_settings = proxy_settings self._request_close_event = asyncio.Event() - self._shard_count = shard_count if shard_count is not undefined.UNDEFINED else 0 - self._shard_ids = set() if shard_ids is undefined.UNDEFINED else shard_ids + self._shard_count: int = shard_count if shard_count is not None else 0 + self._shard_ids: typing.Set[int] = set() if shard_ids is None else shard_ids self._shards: typing.Dict[int, gateway.Gateway] = {} self._tasks: typing.Dict[int, asyncio.Task[typing.Any]] = {} self._token = token @@ -342,14 +339,15 @@ async def _init(self) -> None: for shard_id in self._shard_ids: shard = gateway.Gateway( app=self, - config=self._aiohttp_config, debug=self._debug, + http_settings=self._http_settings, initial_activity=self._initial_activity, initial_idle_since=self._initial_idle_since, initial_is_afk=self._initial_is_afk, initial_status=self._initial_status, intents=self._intents, large_threshold=self._large_threshold, + proxy_settings=self._proxy_settings, shard_id=shard_id, shard_count=self._shard_count, token=self._token, diff --git a/hikari/impl/rest.py b/hikari/impl/rest.py index 8b4c7cd3f7..f545d8dd93 100644 --- a/hikari/impl/rest.py +++ b/hikari/impl/rest.py @@ -26,7 +26,6 @@ __all__: typing.Final[typing.Sequence[str]] = ["RESTClientFactoryImpl", "RESTClientImpl"] import concurrent.futures -import copy import typing import aiohttp @@ -34,11 +33,10 @@ from hikari.api import rest as rest_api from hikari.impl import entity_factory as entity_factory_impl from hikari.impl import stateless_cache -from hikari.net import http_settings as http_settings_ +from hikari.net import config from hikari.net import rate_limits from hikari.net import rest as rest_component from hikari.net import strings -from hikari.utilities import undefined if typing.TYPE_CHECKING: import types @@ -52,63 +50,68 @@ class RESTClientImpl(rest_api.IRESTClientContextManager): Parameters ---------- - config : hikari.utilities.undefined.UndefinedType or hikari.net.http_settings.HTTPSettings - Optional aiohttp settings to apply to the REST components. If undefined, - then sane defaults are used. debug : bool Defaulting to `False`, if `True`, then each payload sent and received in HTTP requests will be dumped to debug logs. This will provide useful debugging context at the cost of performance. Generally you do not need to enable this. - token : hikari.utilities.undefined.UndefinedType or str + connector : aiohttp.BaseConnector + The AIOHTTP connector to use. This must be closed by the caller, and + will not be terminated when this class closes (since you will generally + expect this to be a connection pool). + global_ratelimit : hikari.net.rate_limits.ManualRateLimiter + The global ratelimiter. + http_settings : hikari.net.config.HTTPSettings + HTTP-related settings. + proxy_settings : hikari.net.config.ProxySettings + Proxy-related settings. + token : str or None If defined, the token to use. If not defined, no token will be injected into the `Authorization` header for requests. - token_type : hikari.utilities.undefined.UndefinedType or str + token_type : str or None The token type to use. If undefined, a default is used instead, which will be `Bot`. If no `token` is provided, this is ignored. - url : hikari.utilities.undefined.UndefinedType or str + url : str or None The API URL to hit. Generally you can leave this undefined and use the default. version : int The API version to use. This is interpolated into the default `url` - to create the full URL. Currently this only supports `6` or `7`, and - defaults to `6` (since the v7 REST API is experimental, undocumented, - and subject to breaking change without prior notice at any time). + to create the full URL. Currently this only supports `6` or `7`. """ def __init__( self, *, - config: http_settings_.HTTPSettings, debug: bool = False, + connector: aiohttp.BaseConnector, global_ratelimit: rate_limits.ManualRateLimiter, - token: typing.Union[undefined.UndefinedType, str] = undefined.UNDEFINED, - token_type: typing.Union[undefined.UndefinedType, str] = undefined.UNDEFINED, - url: typing.Union[undefined.UndefinedType, str] = undefined.UNDEFINED, - version: int = 6, + http_settings: config.HTTPSettings, + proxy_settings: config.ProxySettings, + token: typing.Optional[str], + token_type: typing.Optional[str], + url: typing.Optional[str], + version: int, ) -> None: self._cache: cache_.ICacheComponent = stateless_cache.StatelessCacheImpl() self._entity_factory = entity_factory_impl.EntityFactoryComponentImpl(self) self._executor = None + self._http_settings = http_settings + self._proxy_settings = proxy_settings + self._rest = rest_component.REST( app=self, - config=config, + connector=connector, + connector_owner=False, debug=debug, + http_settings=http_settings, global_ratelimit=global_ratelimit, + proxy_settings=proxy_settings, token=token, token_type=token_type, rest_url=url, version=version, ) - @property - def executor(self) -> typing.Optional[concurrent.futures.Executor]: - return self._executor - - @property - def rest(self) -> rest_component.REST: - return self._rest - @property def cache(self) -> cache_.ICacheComponent: """Return the cache component. @@ -118,10 +121,26 @@ def cache(self) -> cache_.ICacheComponent: """ return self._cache + @property + def executor(self) -> typing.Optional[concurrent.futures.Executor]: + return self._executor + @property def entity_factory(self) -> entity_factory_.IEntityFactoryComponent: return self._entity_factory + @property + def http_settings(self) -> config.HTTPSettings: + return self._http_settings + + @property + def proxy_settings(self) -> config.ProxySettings: + return self._proxy_settings + + @property + def rest(self) -> rest_component.REST: + return self._rest + async def close(self) -> None: await self._rest.close() @@ -146,9 +165,6 @@ class RESTClientFactoryImpl(rest_api.IRESTClientFactory): Parameters ---------- - config : hikari.net.http_settings.HTTPSettings or hikari.utilities.undefined.UndefinedType - The config to use for HTTP settings. If `undefined`, then defaults are - used instead. debug : bool If `True`, then much more information is logged each time a request is made. Generally you do not need this to be on, so it will default to @@ -163,39 +179,39 @@ class RESTClientFactoryImpl(rest_api.IRESTClientFactory): def __init__( self, - config: typing.Union[undefined.UndefinedType, http_settings_.HTTPSettings] = undefined.UNDEFINED, + *, + connector: typing.Optional[aiohttp.BaseConnector] = None, + connector_owner: bool = True, debug: bool = False, - url: typing.Union[undefined.UndefinedType, str] = undefined.UNDEFINED, + http_settings: typing.Optional[config.HTTPSettings] = None, + proxy_settings: typing.Optional[config.ProxySettings] = None, + url: typing.Optional[str] = None, version: int = 6, ) -> None: - config = http_settings_.HTTPSettings() if config is undefined.UNDEFINED else config - - # Copy this, since we mutate the connector attribute on it in some cases. - self._config = copy.copy(config) + self._connector = aiohttp.TCPConnector() if connector is None else connector + self._connector_owner = connector_owner self._debug = debug self._global_ratelimit = rate_limits.ManualRateLimiter() + self._http_settings = config.HTTPSettings() if http_settings is None else http_settings + self._proxy_settings = config.ProxySettings() if proxy_settings is None else proxy_settings self._url = url self._version = version - # We should use a shared connector between clients, since we want to share - # the connection pool, so tweak the defaults a little bit to achieve this. - # I should probably separate this option out eventually. - if self._config.connector_owner is True or self._config.tcp_connector is None: - self._config.connector_owner = False - self._connector_owner = True - else: - self._connector_owner = False - - self._tcp_connector = ( - aiohttp.TCPConnector() if self._config.tcp_connector is None else self._config.tcp_connector - ) - self._config.tcp_connector = self._tcp_connector + @property + def http_settings(self) -> config.HTTPSettings: + return self._http_settings + + @property + def proxy_settings(self) -> config.ProxySettings: + return self._proxy_settings def acquire(self, token: str, token_type: str = strings.BEARER_TOKEN) -> rest_api.IRESTClientContextManager: return RESTClientImpl( - config=self._config, + connector=self._connector, debug=self._debug, + http_settings=self._http_settings, global_ratelimit=self._global_ratelimit, + proxy_settings=self._proxy_settings, token=token, token_type=token_type, url=self._url, @@ -204,7 +220,7 @@ def acquire(self, token: str, token_type: str = strings.BEARER_TOKEN) -> rest_ap async def close(self) -> None: if self._connector_owner: - await self._tcp_connector.close() + await self._connector.close() self._global_ratelimit.close() async def __aenter__(self) -> RESTClientFactoryImpl: diff --git a/hikari/models/applications.py b/hikari/models/applications.py index 16af9f70b4..68d00c0ae7 100644 --- a/hikari/models/applications.py +++ b/hikari/models/applications.py @@ -36,8 +36,8 @@ import attr from hikari.models import guilds -from hikari.utilities import cdn from hikari.utilities import files +from hikari.utilities import cdn from hikari.utilities import snowflake if typing.TYPE_CHECKING: diff --git a/hikari/models/channels.py b/hikari/models/channels.py index c3af991b52..2e4e23ab7b 100644 --- a/hikari/models/channels.py +++ b/hikari/models/channels.py @@ -43,8 +43,8 @@ from hikari.models import permissions from hikari.models import users -from hikari.utilities import cdn from hikari.utilities import files +from hikari.utilities import cdn from hikari.utilities import snowflake from hikari.utilities import undefined @@ -54,7 +54,7 @@ from hikari.models import embeds from hikari.models import guilds from hikari.models import messages - from hikari.net import iterators + from hikari.utilities import iterators @enum.unique diff --git a/hikari/models/emojis.py b/hikari/models/emojis.py index 53669a6b58..9d3a8788be 100644 --- a/hikari/models/emojis.py +++ b/hikari/models/emojis.py @@ -27,8 +27,8 @@ import attr -from hikari.utilities import cdn from hikari.utilities import files +from hikari.utilities import cdn from hikari.utilities import snowflake if typing.TYPE_CHECKING: diff --git a/hikari/models/guilds.py b/hikari/models/guilds.py index 824e48f4e0..d51608fdd5 100644 --- a/hikari/models/guilds.py +++ b/hikari/models/guilds.py @@ -48,8 +48,8 @@ import attr from hikari.models import users -from hikari.utilities import cdn from hikari.utilities import files +from hikari.utilities import cdn from hikari.utilities import snowflake if typing.TYPE_CHECKING: diff --git a/hikari/models/invites.py b/hikari/models/invites.py index 35d878b667..8f4bc08e13 100644 --- a/hikari/models/invites.py +++ b/hikari/models/invites.py @@ -33,8 +33,8 @@ import attr from hikari.models import guilds -from hikari.utilities import cdn from hikari.utilities import files +from hikari.utilities import cdn from hikari.utilities import snowflake if typing.TYPE_CHECKING: diff --git a/hikari/models/users.py b/hikari/models/users.py index fd49841302..a1a9405d75 100644 --- a/hikari/models/users.py +++ b/hikari/models/users.py @@ -26,8 +26,8 @@ import attr -from hikari.utilities import cdn from hikari.utilities import files +from hikari.utilities import cdn from hikari.utilities import snowflake from hikari.utilities import undefined diff --git a/hikari/models/webhooks.py b/hikari/models/webhooks.py index 8cbc9f903e..70aa282bee 100644 --- a/hikari/models/webhooks.py +++ b/hikari/models/webhooks.py @@ -26,8 +26,8 @@ import attr -from hikari.utilities import cdn from hikari.utilities import files as files_ +from hikari.utilities import cdn from hikari.utilities import snowflake from hikari.utilities import undefined diff --git a/hikari/net/config.py b/hikari/net/config.py new file mode 100644 index 0000000000..fe39475de0 --- /dev/null +++ b/hikari/net/config.py @@ -0,0 +1,133 @@ +# -*- coding: utf-8 -*- +# Copyright © Nekoka.tt 2019-2020 +# +# This file is part of Hikari. +# +# Hikari is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Hikari is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Hikari. If not, see . +"""Data class containing network-related configuration settings.""" + +from __future__ import annotations + +__all__: typing.Final[typing.Sequence[str]] = ["BasicAuthHeader", "ProxySettings", "HTTPSettings"] + +import base64 +import typing + +import attr + +from hikari.net import strings +from hikari.utilities import data_binding + + +@attr.s(slots=True, kw_only=True, auto_attribs=True, repr=False) +class BasicAuthHeader: + """An object that can be set as a producer for a basic auth header.""" + + username: str + """Username for the header.""" + + password: str + """Password for the header.""" + + @property + def header(self) -> str: + """Generate the header value and return it.""" + raw_token = f"{self.username}:{self.password}".encode("ascii") + token_part = base64.b64encode(raw_token).decode("ascii") + return f"{strings.BASICAUTH_TOKEN} {token_part}" + + def __str__(self) -> str: + return self.header + + __repr__ = __str__ + + +@attr.s(slots=True, kw_only=True, auto_attribs=True) +class ProxySettings: + auth: typing.Optional[typing.Any] = None + """An object that when cast to a string, yields the proxy auth header.""" + + headers: typing.Optional[data_binding.Headers] = None + """Additional headers to use for requests via a proxy, if required.""" + + url: typing.Optional[str] = None + """The URL of the proxy to use.""" + + trust_env: bool = False + """If `True`, and no proxy info is given, then `HTTP_PROXY` and + `HTTPS_PROXY` will be used from the environment variables if present. + + Any proxy credentials will be read from the user's `netrc` file + (https://www.gnu.org/software/inetutils/manual/html_node/The-_002enetrc-file.html) + If `False`, then this information is instead ignored. + Defaults to `False` if unspecified. + """ + + @property + def all_headers(self) -> typing.Optional[data_binding.Headers]: + """Returns all proxy headers.""" + if self.headers is None: + if self.auth is None: + return None + return {strings.PROXY_AUTHENTICATION_HEADER: self.auth} + + if self.auth is None: + return self.headers + return {**self.headers, strings.PROXY_AUTHENTICATION_HEADER: self.auth} + + +@attr.s(slots=True, kw_only=True, auto_attribs=True) +class HTTPTimeoutSettings: + """Settings to control HTTP request timeouts.""" + + acquire_and_connect: typing.Optional[float] = None + """Timeout for `request_socket_connect` PLUS connection acquisition.""" + + request_socket_connect: typing.Optional[float] = None + """Timeout for connecting a socket.""" + + request_socket_read: typing.Optional[float] = None + """Timeout for reading a socket.""" + + total: typing.Optional[float] = 60.0 + """Total timeout for entire request. + + Defaults to one minute. + """ + + +@attr.s(slots=True, kw_only=True, auto_attribs=True) +class HTTPSettings: + allow_redirects: bool = False + """If `True`, allow following redirects from `3xx` HTTP responses. + + Generally you do not want to enable this unless you have a good reason to. + """ + + max_redirects: int = 10 + """The maximum number of redirects to allow. + + If `allow_redirects` is `False`, then this is ignored. + """ + + timeouts: HTTPTimeoutSettings = attr.ib(factory=HTTPTimeoutSettings) + """Settings to control HTTP request timeouts.""" + + verify_ssl: bool = True + """If `True`, then responses with invalid SSL certificates will be + rejected. Generally you want to keep this enabled unless you have a + problem with SSL and you know exactly what you are doing by disabling + this. Disabling SSL verification can have major security implications. + You turn this off at your own risk. + """ diff --git a/hikari/net/gateway.py b/hikari/net/gateway.py index 216d8ecdca..f6f1171884 100644 --- a/hikari/net/gateway.py +++ b/hikari/net/gateway.py @@ -34,9 +34,7 @@ import attr from hikari import errors -from hikari.api import component from hikari.models import presences -from hikari.net import http_client from hikari.net import rate_limits from hikari.net import strings from hikari.utilities import data_binding @@ -46,25 +44,25 @@ import datetime from hikari.api import event_consumer - from hikari.net import http_settings + from hikari.net import config from hikari.models import channels from hikari.models import guilds from hikari.models import intents as intents_ from hikari.utilities import snowflake -class Gateway(http_client.HTTPClient, component.IComponent): +class Gateway: """Implementation of a V6 and V7 compatible gateway. Parameters ---------- app : hikari.api.event_consumer.IEventConsumerApp The base application. - config : hikari.net.http_settings.HTTPSettings - The AIOHTTP settings to use for the client session. debug : bool If `True`, each sent and received payload is dumped to the logs. If `False`, only the fact that data has been sent/received will be logged. + http_settings : hikari.net.config.HTTPSettings + The HTTP-related settings to use while negotiating a websocket. initial_activity : hikari.models.presences.Activity or None or hikari.utilities.undefined.UndefinedType The initial activity to appear to have for this shard. initial_idle_since : datetime.datetime or None or hikari.utilities.undefined.UndefinedType @@ -77,6 +75,8 @@ class Gateway(http_client.HTTPClient, component.IComponent): Collection of intents to use, or `None` to not use intents at all. large_threshold : int The number of members to have in a guild for it to be considered large. + proxy_settings : hikari.net.config.ProxySettings + The proxy settings to use while negotiating a websocket. shard_id : int The shard ID. shard_count : int @@ -174,14 +174,15 @@ def __init__( self, *, app: event_consumer.IEventConsumerApp, - config: http_settings.HTTPSettings, debug: bool = False, + http_settings: config.HTTPSettings, initial_activity: typing.Union[undefined.UndefinedType, None, presences.Activity] = undefined.UNDEFINED, initial_idle_since: typing.Union[undefined.UndefinedType, None, datetime.datetime] = undefined.UNDEFINED, initial_is_afk: typing.Union[undefined.UndefinedType, bool] = undefined.UNDEFINED, initial_status: typing.Union[undefined.UndefinedType, presences.Status] = undefined.UNDEFINED, intents: typing.Optional[intents_.Intent] = None, large_threshold: int = 250, + proxy_settings: config.ProxySettings, shard_id: int = 0, shard_count: int = 1, token: str, @@ -190,16 +191,18 @@ def __init__( use_etf: bool = False, version: int = 6, ) -> None: - super().__init__(config=config, debug=debug) self._activity: typing.Union[undefined.UndefinedType, None, presences.Activity] = initial_activity self._app = app self._backoff = rate_limits.ExponentialBackOff(base=1.85, maximum=600, initial_increment=2) + self._debug = debug self._handshake_event = asyncio.Event() + self._http_settings = http_settings self._idle_since: typing.Union[undefined.UndefinedType, None, datetime.datetime] = initial_idle_since self._intents: typing.Optional[intents_.Intent] = intents self._is_afk: typing.Union[undefined.UndefinedType, bool] = initial_is_afk self._last_run_started_at = float("nan") self._logger = logging.getLogger(f"hikari.net.gateway.{shard_id}") + self._proxy_settings = proxy_settings self._request_close_event = asyncio.Event() self._seq: typing.Optional[str] = None self._shard_id: int = shard_id @@ -274,27 +277,41 @@ async def close(self) -> None: async def _run(self) -> None: """Start the shard and wait for it to shut down.""" - try: - # This may be set if we are stuck in a reconnect loop. - while not self._request_close_event.is_set() and await self._run_once_shielded(): - pass + async with aiohttp.ClientSession( + connector_owner=True, + connector=aiohttp.TCPConnector( + verify_ssl=self._http_settings.verify_ssl, + # We are never going to want more than one connection. This will be spammy on + # big sharded bots and waste a lot of time, so theres no reason to bother. + limit=1, + limit_per_host=1, + ), + version=aiohttp.HttpVersion11, + timeout=aiohttp.ClientTimeout( + total=self._http_settings.timeouts.total, + connect=self._http_settings.timeouts.acquire_and_connect, + sock_read=self._http_settings.timeouts.request_socket_read, + sock_connect=self._http_settings.timeouts.request_socket_connect, + ), + trust_env=self._proxy_settings.trust_env, + ) as client_session: + try: + # This may be set if we are stuck in a reconnect loop. + while not self._request_close_event.is_set() and await self._run_once_shielded(client_session): + pass - # Allow zookeepers to stop gathering tasks for each shard. - raise errors.GatewayClientClosedError - finally: - # This is set to ensure that the `start' waiter does not deadlock if - # we cannot connect successfully. It is a hack, but it works. - self._handshake_event.set() - # Close the aiohttp client session. - # Didn't use `super` as I can mock this to check without breaking - # the entire inheritance conduit in a patch context. - await http_client.HTTPClient.close(self) - - async def _run_once_shielded(self) -> bool: + # Allow zookeepers to stop gathering tasks for each shard. + raise errors.GatewayClientClosedError + finally: + # This is set to ensure that the `start' waiter does not deadlock if + # we cannot connect successfully. It is a hack, but it works. + self._handshake_event.set() + + async def _run_once_shielded(self, client_session: aiohttp.ClientSession) -> bool: # Returns `True` if we can reconnect, or `False` otherwise. # Wraps the runner logic in the standard exception handling mechanisms. try: - await self._run_once() + await self._run_once(client_session) return False except aiohttp.ClientConnectorError as ex: self._logger.error( @@ -347,7 +364,7 @@ async def _run_once_shielded(self) -> bool: return True - async def _run_once(self) -> None: + async def _run_once(self, client_session: aiohttp.ClientSession) -> None: # Physical runner logic without error handling. self._request_close_event.clear() @@ -374,7 +391,18 @@ async def _run_once(self) -> None: self._last_run_started_at = self._now() self._logger.debug("creating websocket connection to %s", self.url) - self._ws = await self._create_ws(self.url) + self._ws = await client_session.ws_connect( + url=self.url, + autoping=True, + autoclose=True, + proxy=self._proxy_settings.url, + proxy_headers=self._proxy_settings.all_headers, + verify_ssl=self._http_settings.verify_ssl, + # Discord can send massive messages that lead us to being disconnected + # without this. It is a bit shit that there is no guarantee of the size + # of these messages, but there isn't much we can do about this one. + max_msg_size=0, + ) self.connected_at = self._now() @@ -500,7 +528,7 @@ async def _handshake(self) -> None: self.heartbeat_interval = message["d"]["heartbeat_interval"] / 1_000.0 - self._logger.info("received HELLO, heartbeat interval is %s", self.heartbeat_interval) + self._logger.info("received HELLO, heartbeat interval is %ss", self.heartbeat_interval) if self.session_id is not None: # RESUME! diff --git a/hikari/net/helpers.py b/hikari/net/helpers.py new file mode 100644 index 0000000000..be8b008a52 --- /dev/null +++ b/hikari/net/helpers.py @@ -0,0 +1,57 @@ +# -*- coding: utf-8 -*- +# Copyright © Nekoka.tt 2019-2020 +# +# This file is part of Hikari. +# +# Hikari is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Hikari is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Hikari. If not, see . +"""General bits and pieces that are reused between components.""" + +from __future__ import annotations + +__all__: typing.Final[typing.Sequence[str]] = ["generate_error_response"] + +import http +import typing + +import aiohttp + +from hikari import errors + + +async def generate_error_response(response: aiohttp.ClientResponse) -> errors.HTTPError: + """Given an erroneous HTTP response, return a corresponding exception.""" + real_url = str(response.real_url) + raw_body = await response.read() + + if response.status == http.HTTPStatus.BAD_REQUEST: + return errors.BadRequest(real_url, response.headers, raw_body) + if response.status == http.HTTPStatus.UNAUTHORIZED: + return errors.Unauthorized(real_url, response.headers, raw_body) + if response.status == http.HTTPStatus.FORBIDDEN: + return errors.Forbidden(real_url, response.headers, raw_body) + if response.status == http.HTTPStatus.NOT_FOUND: + return errors.NotFound(real_url, response.headers, raw_body) + + # noinspection PyArgumentList + status = http.HTTPStatus(response.status) + + cls: typing.Type[errors.HikariError] + if 400 <= status < 500: + cls = errors.ClientHTTPErrorResponse + elif 500 <= status < 600: + cls = errors.ServerHTTPErrorResponse + else: + cls = errors.HTTPErrorResponse + + return cls(real_url, status, response.headers, raw_body) diff --git a/hikari/net/http_client.py b/hikari/net/http_client.py deleted file mode 100644 index 23d24dde61..0000000000 --- a/hikari/net/http_client.py +++ /dev/null @@ -1,277 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright © Nekoka.tt 2019-2020 -# -# This file is part of Hikari. -# -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . -"""Base functionality for any HTTP-based network component.""" -from __future__ import annotations - -__all__: typing.Final[typing.Sequence[str]] = ["HTTPClient"] - -import abc -import http -import json -import logging -import types -import typing -import weakref - -import aiohttp.client -import aiohttp.connector -import aiohttp.http_writer -import aiohttp.typedefs - -from hikari import errors -from hikari.net import http_settings -from hikari.utilities import data_binding - -try: - # noinspection PyProtectedMember - RequestContextManager = aiohttp.client._RequestContextManager - """Type hint for an AIOHTTP session context manager. - - This is stored as aiohttp does not expose the type-hint directly, despite - exposing the rest of the API it is part of. - """ -except NameError: - RequestContextManager = typing.Any # type: ignore - - -_LOGGER: typing.Final[logging.Logger] = logging.getLogger("hikari.net") - - -class HTTPClient(abc.ABC): - """An HTTP client base for Hikari. - - The purpose of this is to provide a consistent interface for any network - facing application that need an HTTP connection or websocket connection. - - This class takes care of initializing the underlying client session, etc. - - This class will also register interceptors for HTTP requests to produce an - appropriate level of debugging context when needed. - - Parameters - ---------- - config : hikari.net.http_settings.HTTPSettings or None - Optional aiohttp settings for making HTTP connections. - If `None`, defaults are used. - debug : bool - Defaults to `False`. If `True`, then a lot of contextual information - regarding low-level HTTP communication will be logged to the debug - _logger on this class. - """ - - __slots__: typing.Sequence[str] = ( - "_client_session", - "_client_session_ref", - "_config", - "_debug", - "_tracers", - ) - - _config: http_settings.HTTPSettings - """HTTP settings in-use.""" - - _debug: bool - """`True` if debug mode is enabled. `False` otherwise.""" - - def __init__(self, *, config: typing.Optional[http_settings.HTTPSettings] = None, debug: bool = False,) -> None: - if config is None: - config = http_settings.HTTPSettings() - - self._client_session: typing.Optional[aiohttp.ClientSession] = None - self._client_session_ref: typing.Optional[weakref.ProxyType] = None - self._config = config - self._debug = debug - - @typing.final - async def __aenter__(self) -> HTTPClient: - return self - - @typing.final - async def __aexit__( - self, exc_type: typing.Type[BaseException], exc_val: BaseException, exc_tb: types.TracebackType - ) -> None: - await self.close() - - def __del__(self) -> None: - # Let the client session get garbage collected. - self._client_session = None - self._client_session_ref = None - - async def close(self) -> None: - """Close the client safely.""" - if self._client_session is not None: - await self._client_session.close() - _LOGGER.debug("closed client session object %r", self._client_session) - self._client_session = None - - @typing.final - def get_client_session(self) -> aiohttp.ClientSession: - """Acquire a client session to make requests with. - - !!! warning - This must be invoked within a coroutine running in an event loop, - or the behaviour will be undefined. - - Generally you should not need to use this unless you are interfacing - with the Hikari API directly. - - This is not thread-safe. - - Returns - ------- - weakref.proxy of aiohttp.ClientSession - The client session to use for requests. - """ - if self._client_session is None: - connector = self._config.tcp_connector if self._config.tcp_connector is not None else None - self._client_session = aiohttp.ClientSession( - connector=connector, - trust_env=self._config.trust_env, - version=aiohttp.HttpVersion11, - json_serialize=json.dumps, - connector_owner=self._config.connector_owner if self._config.tcp_connector is not None else True, - ) - self._client_session_ref = weakref.proxy(self._client_session) - _LOGGER.debug("acquired new client session object %r", self._client_session) - - # Only return a weakref, to prevent callees obtaining ownership. - return typing.cast(aiohttp.ClientSession, self._client_session_ref) - - @typing.final - def _perform_request( - self, - *, - method: str, - url: str, - headers: data_binding.Headers = typing.cast(data_binding.Headers, types.MappingProxyType({})), - body: typing.Union[ - data_binding.JSONObjectBuilder, aiohttp.FormData, data_binding.JSONObject, data_binding.JSONArray, None - ] = None, - query: typing.Union[data_binding.Query, data_binding.StringMapBuilder, None] = None, - ) -> RequestContextManager: - """Make an HTTP request and return the response. - - Parameters - ---------- - method : str - The verb to use. - url : str - The URL to hit. - headers : typing.Dict[str, str] - Headers to use when making the request. - body : aiohttp.FormData or dict or list or None - The body to send. Currently this will send the content in - a form body if you pass an instance of `aiohttp.FormData`, or - as a JSON body if you pass a `list` or `dict`. Any other types - will be ignored. - query : typing.Dict[str, str] - Mapping of query string arguments to pass in the URL. - - Returns - ------- - aiohttp.ClientResponse - The HTTP response. - """ - kwargs: typing.Dict[str, typing.Any] = {} - - if isinstance(body, (dict, list)): - kwargs["json"] = body - - elif isinstance(body, aiohttp.FormData): - kwargs["data"] = body - - return self.get_client_session().request( - method=method, - url=url, - params=query, - headers=headers, - allow_redirects=self._config.allow_redirects, - proxy=self._config.proxy_url, - proxy_auth=self._config.proxy_auth, - proxy_headers=self._config.proxy_headers, - verify_ssl=self._config.verify_ssl, - ssl_context=self._config.ssl_context, - timeout=self._config.request_timeout, - **kwargs, - ) - - @typing.final - async def _create_ws( - self, url: str, *, compress: int = 0, auto_ping: bool = True, max_msg_size: int = 0 - ) -> aiohttp.ClientWebSocketResponse: - """Create a websocket. - - Parameters - ---------- - url : str - The URL to connect the websocket to. - compress : int - The compression type to use, as an int value. Use `0` to disable - compression. - auto_ping : bool - If `True`, the client will manage automatically pinging/ponging - in the background. If `False`, this will not occur. - max_msg_size : int - The maximum message size to allow to be received. If `0`, then - no max limit is set. - - Returns - ------- - aiohttp.ClientWebsocketResponse - The websocket to use. - """ - _LOGGER.debug("creating underlying websocket object from HTTP session") - return await self.get_client_session().ws_connect( - url=url, - compress=compress, - autoping=auto_ping, - max_msg_size=max_msg_size, - proxy=self._config.proxy_url, - proxy_auth=self._config.proxy_auth, - proxy_headers=self._config.proxy_headers, - verify_ssl=self._config.verify_ssl, - ssl_context=self._config.ssl_context, - ) - - -async def generate_error_response(response: aiohttp.ClientResponse) -> errors.HTTPError: - """Given an erroneous HTTP response, return a corresponding exception.""" - real_url = str(response.real_url) - raw_body = await response.read() - - if response.status == http.HTTPStatus.BAD_REQUEST: - return errors.BadRequest(real_url, response.headers, raw_body) - if response.status == http.HTTPStatus.UNAUTHORIZED: - return errors.Unauthorized(real_url, response.headers, raw_body) - if response.status == http.HTTPStatus.FORBIDDEN: - return errors.Forbidden(real_url, response.headers, raw_body) - if response.status == http.HTTPStatus.NOT_FOUND: - return errors.NotFound(real_url, response.headers, raw_body) - - # noinspection PyArgumentList - status = http.HTTPStatus(response.status) - - cls: typing.Type[errors.HikariError] - if 400 <= status < 500: - cls = errors.ClientHTTPErrorResponse - elif 500 <= status < 600: - cls = errors.ServerHTTPErrorResponse - else: - cls = errors.HTTPErrorResponse - - return cls(real_url, status, response.headers, raw_body) diff --git a/hikari/net/http_settings.py b/hikari/net/http_settings.py deleted file mode 100644 index 3cc627d318..0000000000 --- a/hikari/net/http_settings.py +++ /dev/null @@ -1,104 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright © Nekoka.tt 2019-2020 -# -# This file is part of Hikari. -# -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . -"""Data class containing AIOHTTP-specific configuration settings.""" - -from __future__ import annotations - -__all__: typing.Final[typing.Sequence[str]] = ["HTTPSettings"] - -import typing - -import aiohttp -import attr - -if typing.TYPE_CHECKING: - import ssl - - -@attr.s(kw_only=True, repr=False, auto_attribs=True) -@typing.final -class HTTPSettings: - """Config for application that use AIOHTTP.""" - - allow_redirects: bool = False - """If `True`, allow following redirects from `3xx` HTTP responses. - - Generally you do not want to enable this unless you have a good reason to. - """ - - connector_owner: bool = True - """Determines whether objects take ownership of their connectors. - - If `True`, the component consuming any connector will close the - connector when closed. - - If you set this to `False`, and you provide a `tcp_connector_factory`, - this will prevent the connector being closed by each component. - - Note that unless you provide a `tcp_connector_factory`, this will be - ignored. - """ - - proxy_auth: typing.Optional[aiohttp.BasicAuth] = None - """Optional proxy authorization to provide in any HTTP requests.""" - - proxy_headers: typing.Optional[typing.Mapping[str, str]] = None - """Optional proxy headers to provide in any HTTP requests.""" - - proxy_url: typing.Optional[str] = None - """The optional URL of the proxy to send requests via.""" - - request_timeout: typing.Optional[float] = 10.0 - """Optional request timeout to use. - - If an HTTP request takes longer than this, it will be aborted. - - Defaults to 10 seconds. - - If not `None`, the value represents a number of seconds as a floating - point number. - """ - - ssl_context: typing.Optional[ssl.SSLContext] = None - """The optional SSL context to use.""" - - tcp_connector: typing.Optional[aiohttp.TCPConnector] = None - """An optional TCP connector to use. - - The client session will default to closing this connector on close unless - you set the `connector_owner` to `False`. If you are planning to share - the connector between clients, you should set that to `False`. - """ - - trust_env: bool = False - """If `True`, and no proxy info is given, then `HTTP_PROXY` and - `HTTPS_PROXY` will be used from the environment variables if present. - - Any proxy credentials will be read from the user's `netrc` file - (https://www.gnu.org/software/inetutils/manual/html_node/The-_002enetrc-file.html) - If `False`, then this information is instead ignored. - Defaults to `False` if unspecified. - """ - - verify_ssl: bool = True - """If `True`, then responses with invalid SSL certificates will be - rejected. Generally you want to keep this enabled unless you have a - problem with SSL and you know exactly what you are doing by disabling - this. Disabling SSL verification can have major security implications. - You turn this off at your own risk. - """ diff --git a/hikari/net/iterators.py b/hikari/net/iterators.py deleted file mode 100644 index 267326abd1..0000000000 --- a/hikari/net/iterators.py +++ /dev/null @@ -1,556 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright © Nekoka.tt 2019-2020 -# -# This file is part of Hikari. -# -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . -"""Lazy iterators for data that requires repeated API calls to retrieve.""" -from __future__ import annotations - -__all__: typing.Final[typing.Sequence[str]] = ["LazyIterator"] - -import abc -import operator -import typing - -from hikari.net import routes -from hikari.utilities import data_binding -from hikari.utilities import snowflake -from hikari.utilities import undefined - -if typing.TYPE_CHECKING: - from hikari.api import rest - - from hikari.models import applications - from hikari.models import audit_logs - from hikari.models import guilds - from hikari.models import messages - from hikari.models import users - -ValueT = typing.TypeVar("ValueT") -AnotherValueT = typing.TypeVar("AnotherValueT") - - -class _AllConditions(typing.Generic[ValueT]): - __slots__: typing.Sequence[str] = ("conditions",) - - def __init__(self, conditions: typing.Collection[typing.Callable[[ValueT], bool]]) -> None: - self.conditions = conditions - - def __call__(self, item: ValueT) -> bool: - return all(condition(item) for condition in self.conditions) - - -class _AttrComparator(typing.Generic[ValueT]): - __slots__: typing.Sequence[str] = ("getter", "expected_value") - - def __init__(self, attr_name: str, expected_value: typing.Any) -> None: - if attr_name.startswith("."): - attr_name = attr_name[1:] - self.getter = operator.attrgetter(attr_name) - self.expected_value = expected_value - - def __call__(self, item: ValueT) -> bool: - return bool(self.getter(item) == self.expected_value) - - -class LazyIterator(typing.Generic[ValueT], abc.ABC): - """A set of results that are fetched asynchronously from the API as needed. - - This is a `typing.AsyncIterable` and `typing.AsyncIterator` with several - additional helpful methods provided for convenience. - - Examples - -------- - You can use this in multiple ways. - - As an async iterable: - - >>> async for item in paginated_results: - ... process(item) - - As an eagerly retrieved set of results (performs all API calls at once, - which may be slow for large sets of data): - - >>> results = await paginated_results - >>> # ... which is equivalent to this... - >>> results = [item async for item in paginated_results] - - As an async iterator (not recommended): - - >>> try: - ... while True: - ... process(await paginated_results.__anext__()) - ... except StopAsyncIteration: - ... pass - - Additionally, you can make use of some of the provided helper methods - on this class to perform basic operations easily. - - Iterating across the items with indexes (like `enumerate` for normal - iterables): - - >>> async for i, item in paginated_results.enumerate(): - ... print(i, item) - (0, foo) - (1, bar) - (2, baz) - - Limiting the number of results you iterate across: - - >>> async for item in paginated_results.limit(3): - ... process(item) - """ - - __slots__: typing.Sequence[str] = () - - def map( - self, transformation: typing.Union[typing.Callable[[ValueT], AnotherValueT], str], - ) -> LazyIterator[AnotherValueT]: - """Map the values to a different value. - - Parameters - ---------- - transformation : typing.Callable[[ValueT], bool] or str - The function to use to map the attribute. This may alternatively - be a string attribute name to replace the input value with. You - can provide nested attributes using the `.` operator. - - Returns - ------- - LazyIterator[AnotherValueT] - LazyIterator that maps each value to another value. - """ - if isinstance(transformation, str): - if transformation.startswith("."): - transformation = transformation[1:] - transformation = operator.attrgetter(transformation) - return _MappingLazyIterator(self, transformation) - - def filter( - self, - *predicates: typing.Union[typing.Tuple[str, typing.Any], typing.Callable[[ValueT], bool]], - **attrs: typing.Any, - ) -> LazyIterator[ValueT]: - """Filter the items by one or more conditions that must all be `True`. - - Parameters - ---------- - *predicates : typing.Callable[[ValueT], bool] or typing.Tuple[str, typing.Any] - Predicates to invoke. These are functions that take a value and - return `True` if it is of interest, or `False` otherwise. These - may instead include 2-`tuple` objects consisting of a `str` - attribute name (nested attributes are referred to using the `.` - operator), and values to compare for equality. This allows you - to specify conditions such as `members.filter(("user.bot", True))`. - **attrs : typing.Any - Alternative to passing 2-tuples. Cannot specify nested attributes - using this method. - - Returns - ------- - LazyIterator[ValueT] - LazyIterator that only emits values where all conditions are - matched. - """ - if not predicates and not attrs: - raise TypeError("You should provide at least one predicate to filter()") - - conditions: typing.List[typing.Callable[[ValueT], bool]] = [] - - for p in predicates: - if isinstance(p, tuple): - name, value = p - tuple_comparator: _AttrComparator[ValueT] = _AttrComparator(name, value) - conditions.append(tuple_comparator) - else: - conditions.append(p) - - for name, value in attrs.items(): - attr_comparator: _AttrComparator[ValueT] = _AttrComparator(name, value) - conditions.append(attr_comparator) - - if len(conditions) > 1: - return _FilteredLazyIterator(self, _AllConditions(conditions)) - else: - return _FilteredLazyIterator(self, conditions[0]) - - def enumerate(self, *, start: int = 0) -> LazyIterator[typing.Tuple[int, ValueT]]: - """Enumerate the paginated results lazily. - - This behaves as an asyncio-friendly version of `builtins.enumerate` - which uses much less memory than collecting all the results first and - calling `enumerate` across them. - - Parameters - ---------- - start : int - Optional int to start at. If omitted, this is `0`. - - Examples - -------- - >>> async for i, item in paginated_results.enumerate(): - ... print(i, item) - (0, foo) - (1, bar) - (2, baz) - (3, bork) - (4, qux) - - >>> async for i, item in paginated_results.enumerate(start=9): - ... print(i, item) - (9, foo) - (10, bar) - (11, baz) - (12, bork) - (13, qux) - - >>> async for i, item in paginated_results.enumerate(start=9).limit(3): - ... print(i, item) - (9, foo) - (10, bar) - (11, baz) - - Returns - ------- - LazyIterator[typing.Tuple[int, T]] - A paginated results view that asynchronously yields an increasing - counter in a tuple with each result, lazily. - """ - return _EnumeratedLazyIterator(self, start=start) - - def limit(self, limit: int) -> LazyIterator[ValueT]: - """Limit the number of items you receive from this async iterator. - - Parameters - ---------- - limit : int - The number of items to get. This must be greater than zero. - - Examples - -------- - >>> async for item in paginated_results.limit(3): - ... print(item) - - - Returns - ------- - LazyIterator[T] - A paginated results view that asynchronously yields a maximum - of the given number of items before completing. - """ - return _LimitedLazyIterator(self, limit) - - def _complete(self) -> typing.NoReturn: - raise StopAsyncIteration("No more items exist in this paginator. It has been exhausted.") from None - - def __aiter__(self) -> LazyIterator[ValueT]: - # We are our own iterator. - return self - - async def _fetch_all(self) -> typing.Sequence[ValueT]: - return [item async for item in self] - - def __await__(self) -> typing.Generator[None, None, typing.Sequence[ValueT]]: - return self._fetch_all().__await__() - - @abc.abstractmethod - async def __anext__(self) -> ValueT: - ... - - -class _EnumeratedLazyIterator(typing.Generic[ValueT], LazyIterator[typing.Tuple[int, ValueT]]): - __slots__: typing.Sequence[str] = ("_i", "_paginator") - - def __init__(self, paginator: LazyIterator[ValueT], *, start: int) -> None: - self._i = start - self._paginator = paginator - - async def __anext__(self) -> typing.Tuple[int, ValueT]: - pair = self._i, await self._paginator.__anext__() - self._i += 1 - return pair - - -class _LimitedLazyIterator(typing.Generic[ValueT], LazyIterator[ValueT]): - __slots__: typing.Sequence[str] = ("_paginator", "_count", "_limit") - - def __init__(self, paginator: LazyIterator[ValueT], limit: int) -> None: - if limit <= 0: - raise ValueError("limit must be positive and non-zero") - self._paginator = paginator - self._count = 0 - self._limit = limit - - async def __anext__(self) -> ValueT: - if self._count >= self._limit: - self._complete() - - next_item = await self._paginator.__anext__() - self._count += 1 - return next_item - - -class _FilteredLazyIterator(typing.Generic[ValueT], LazyIterator[ValueT]): - __slots__: typing.Sequence[str] = ("_paginator", "_predicate") - - def __init__(self, paginator: LazyIterator[ValueT], predicate: typing.Callable[[ValueT], bool]) -> None: - self._paginator = paginator - self._predicate = predicate - - async def __anext__(self) -> ValueT: - async for item in self._paginator: - if self._predicate(item): - return item - raise StopAsyncIteration - - -class _MappingLazyIterator(typing.Generic[AnotherValueT, ValueT], LazyIterator[ValueT]): - __slots__: typing.Sequence[str] = ("_paginator", "_transformation") - - def __init__( - self, paginator: LazyIterator[AnotherValueT], transformation: typing.Callable[[AnotherValueT], ValueT], - ) -> None: - self._paginator = paginator - self._transformation = transformation - - async def __anext__(self) -> ValueT: - return self._transformation(await self._paginator.__anext__()) - - -class _BufferedLazyIterator(typing.Generic[ValueT], LazyIterator[ValueT]): - __slots__: typing.Sequence[str] = ("_buffer",) - - def __init__(self) -> None: - empty_genexp = typing.cast(typing.Generator[ValueT, None, None], (_ for _ in ())) - self._buffer: typing.Optional[typing.Generator[ValueT, None, None]] = empty_genexp - - @abc.abstractmethod - async def _next_chunk(self) -> typing.Optional[typing.Generator[ValueT, None, None]]: - ... - - async def __anext__(self) -> ValueT: - # This sneaky snippet of code lets us use generators rather than lists. - # This is important, as we can use this to make generators that - # deserialize loads of items lazy. If we only want 10 messages of - # history, we can use the same code and prefetch 100 without any - # performance hit from it other than the JSON string response. - try: - if self._buffer is not None: - return next(self._buffer) - except StopIteration: - self._buffer = await self._next_chunk() - if self._buffer is not None: - return next(self._buffer) - self._complete() - - -# We use an explicit forward reference for this, since this breaks potential -# circular import issues (once the file has executed, using those resources is -# not an issue for us). -class MessageIterator(_BufferedLazyIterator["messages.Message"]): - """Implementation of an iterator for message history.""" - - __slots__: typing.Sequence[str] = ("_app", "_request_call", "_direction", "_first_id", "_route") - - def __init__( - self, - app: rest.IRESTClient, - request_call: typing.Callable[ - ..., typing.Coroutine[None, None, typing.Union[None, data_binding.JSONObject, data_binding.JSONArray]] - ], - channel_id: str, - direction: str, - first_id: str, - ) -> None: - super().__init__() - self._app = app - self._request_call = request_call - self._direction = direction - self._first_id = first_id - self._route = routes.GET_CHANNEL_MESSAGES.compile(channel=channel_id) - - async def _next_chunk(self) -> typing.Optional[typing.Generator[messages.Message, typing.Any, None]]: - query = data_binding.StringMapBuilder() - query.put(self._direction, self._first_id) - query.put("limit", 100) - - raw_chunk = await self._request_call(compiled_route=self._route, query=query) - chunk = typing.cast(data_binding.JSONArray, raw_chunk) - - if not chunk: - return None - if self._direction == "after": - chunk.reverse() - - self._first_id = chunk[-1]["id"] - return (self._app.entity_factory.deserialize_message(m) for m in chunk) - - -# We use an explicit forward reference for this, since this breaks potential -# circular import issues (once the file has executed, using those resources is -# not an issue for us). -class ReactorIterator(_BufferedLazyIterator["users.User"]): - """Implementation of an iterator for message reactions.""" - - __slots__: typing.Sequence[str] = ("_app", "_first_id", "_route", "_request_call") - - def __init__( - self, - app: rest.IRESTClient, - request_call: typing.Callable[ - ..., typing.Coroutine[None, None, typing.Union[None, data_binding.JSONObject, data_binding.JSONArray]] - ], - channel_id: str, - message_id: str, - emoji: str, - ) -> None: - super().__init__() - self._app = app - self._request_call = request_call - self._first_id = snowflake.Snowflake.min() - self._route = routes.GET_REACTIONS.compile(channel=channel_id, message=message_id, emoji=emoji) - - async def _next_chunk(self) -> typing.Optional[typing.Generator[users.User, typing.Any, None]]: - query = data_binding.StringMapBuilder() - query.put("after", self._first_id) - query.put("limit", 100) - - raw_chunk = await self._request_call(compiled_route=self._route, query=query) - chunk = typing.cast(data_binding.JSONArray, raw_chunk) - - if not chunk: - return None - - self._first_id = chunk[-1]["id"] - return (self._app.entity_factory.deserialize_user(u) for u in chunk) - - -# We use an explicit forward reference for this, since this breaks potential -# circular import issues (once the file has executed, using those resources is -# not an issue for us). -class OwnGuildIterator(_BufferedLazyIterator["applications.OwnGuild"]): - """Implementation of an iterator for retrieving guilds you are in.""" - - __slots__: typing.Sequence[str] = ("_app", "_request_call", "_route", "_newest_first", "_first_id") - - def __init__( - self, - app: rest.IRESTClient, - request_call: typing.Callable[ - ..., typing.Coroutine[None, None, typing.Union[None, data_binding.JSONObject, data_binding.JSONArray]] - ], - newest_first: bool, - first_id: str, - ) -> None: - super().__init__() - self._app = app - self._newest_first = newest_first - self._request_call = request_call - self._first_id = first_id - self._route = routes.GET_MY_GUILDS.compile() - - async def _next_chunk(self) -> typing.Optional[typing.Generator[applications.OwnGuild, typing.Any, None]]: - query = data_binding.StringMapBuilder() - query.put("before" if self._newest_first else "after", self._first_id) - query.put("limit", 100) - - raw_chunk = await self._request_call(compiled_route=self._route, query=query) - chunk = typing.cast(data_binding.JSONArray, raw_chunk) - - if not chunk: - return None - - self._first_id = chunk[-1]["id"] - return (self._app.entity_factory.deserialize_own_guild(g) for g in chunk) - - -# We use an explicit forward reference for this, since this breaks potential -# circular import issues (once the file has executed, using those resources is -# not an issue for us). -class MemberIterator(_BufferedLazyIterator["guilds.Member"]): - """Implementation of an iterator for retrieving members in a guild.""" - - __slots__: typing.Sequence[str] = ("_app", "_request_call", "_route", "_first_id") - - def __init__( - self, - app: rest.IRESTClient, - request_call: typing.Callable[ - ..., typing.Coroutine[None, None, typing.Union[None, data_binding.JSONObject, data_binding.JSONArray]] - ], - guild_id: str, - ) -> None: - super().__init__() - self._route = routes.GET_GUILD_MEMBERS.compile(guild=guild_id) - self._request_call = request_call - self._app = app - self._first_id = snowflake.Snowflake.min() - - async def _next_chunk(self) -> typing.Optional[typing.Generator[guilds.Member, typing.Any, None]]: - query = data_binding.StringMapBuilder() - query.put("after", self._first_id) - query.put("limit", 100) - - raw_chunk = await self._request_call(compiled_route=self._route, query=query) - chunk = typing.cast(data_binding.JSONArray, raw_chunk) - - if not chunk: - return None - - # noinspection PyTypeChecker - self._first_id = chunk[-1]["user"]["id"] - - return (self._app.entity_factory.deserialize_member(m) for m in chunk) - - -# We use an explicit forward reference for this, since this breaks potential -# circular import issues (once the file has executed, using those resources is -# not an issue for us). -class AuditLogIterator(LazyIterator["audit_logs.AuditLog"]): - """Iterator implementation for an audit log.""" - - def __init__( - self, - app: rest.IRESTClient, - request_call: typing.Callable[ - ..., typing.Coroutine[None, None, typing.Union[None, data_binding.JSONObject, data_binding.JSONArray]] - ], - guild_id: str, - before: str, - user_id: typing.Union[str, undefined.UndefinedType], - action_type: typing.Union[int, undefined.UndefinedType], - ) -> None: - self._action_type = action_type - self._app = app - self._first_id = str(before) - self._request_call = request_call - self._route = routes.GET_GUILD_AUDIT_LOGS.compile(guild=guild_id) - self._user_id = user_id - - async def __anext__(self) -> audit_logs.AuditLog: - query = data_binding.StringMapBuilder() - query.put("limit", 100) - query.put("user_id", self._user_id) - query.put("event_type", self._action_type) - - raw_response = await self._request_call(compiled_route=self._route, query=query) - response = typing.cast(data_binding.JSONObject, raw_response) - - if not response["entries"]: - raise StopAsyncIteration - - log = self._app.entity_factory.deserialize_audit_log(response) - self._first_id = str(min(log.entries.keys())) - return log diff --git a/hikari/net/rate_limits.py b/hikari/net/rate_limits.py index 4f1eb2eae2..9b11dcc387 100644 --- a/hikari/net/rate_limits.py +++ b/hikari/net/rate_limits.py @@ -483,7 +483,7 @@ def __next__(self) -> float: if self.maximum is not None and value >= self.maximum: raise asyncio.TimeoutError - value += random.random() * self.jitter_multiplier # # noqa S311 rng for cryptography + value += random.random() * self.jitter_multiplier # nosec # noqa S311 rng for cryptography return value def __iter__(self) -> ExponentialBackOff: diff --git a/hikari/net/rest.py b/hikari/net/rest.py index 694aa2b0bd..a2639efd79 100644 --- a/hikari/net/rest.py +++ b/hikari/net/rest.py @@ -33,20 +33,19 @@ import aiohttp from hikari import errors -from hikari.api import component from hikari.models import embeds as embeds_ from hikari.models import emojis from hikari.net import buckets -from hikari.net import http_client -from hikari.net import http_settings -from hikari.net import iterators +from hikari.net import config +from hikari.utilities import files +from hikari.net import helpers from hikari.net import rate_limits -from hikari.net import rest_utils from hikari.net import routes +from hikari.net import special_endpoints from hikari.net import strings from hikari.utilities import data_binding from hikari.utilities import date -from hikari.utilities import files +from hikari.utilities import iterators from hikari.utilities import snowflake from hikari.utilities import undefined @@ -72,7 +71,7 @@ # TODO: make a mechanism to allow me to share the same client session but # use various tokens for REST-only apps. -class REST(http_client.HTTPClient, component.IComponent): +class REST: """Implementation of the V6 and V7-compatible Discord REST API. This manages making HTTP/1.1 requests to the API and using the entity @@ -84,10 +83,6 @@ class REST(http_client.HTTPClient, component.IComponent): app : hikari.api.rest.IRESTClient The REST application containing all other application components that Hikari uses. - config : hikari.net.http_settings.HTTPSettings - The AIOHTTP-specific configuration settings. This is used to configure - proxies, and specify TCP connectors to control the size of HTTP - connection pools, etc. debug : bool If `True`, this will enable logging of each payload sent and received, as well as information such as DNS cache hits and misses, and other @@ -109,7 +104,20 @@ class REST(http_client.HTTPClient, component.IComponent): The API version to use. """ - __slots__: typing.Sequence[str] = ("buckets", "global_rate_limit", "version", "_app", "_rest_url", "_token") + __slots__: typing.Sequence[str] = ( + "buckets", + "global_rate_limit", + "version", + "_app", + "_client_session", + "_connector", + "_connector_owner", + "_debug", + "_http_settings", + "_proxy_settings", + "_rest_url", + "_token", + ) buckets: buckets.RESTBucketManager """Bucket ratelimiter manager.""" @@ -128,51 +136,70 @@ def __init__( self, *, app: rest.IRESTClient, - config: http_settings.HTTPSettings, - global_ratelimit: rate_limits.ManualRateLimiter, + connector: typing.Optional[aiohttp.BaseConnector], + connector_owner: bool, debug: bool, - token: typing.Union[undefined.UndefinedType, str], - token_type: typing.Union[undefined.UndefinedType, str], - rest_url: typing.Union[undefined.UndefinedType, str], + global_ratelimit: rate_limits.ManualRateLimiter, + http_settings: config.HTTPSettings, + proxy_settings: config.ProxySettings, + token: typing.Optional[str], + token_type: typing.Optional[str], + rest_url: typing.Optional[str], version: int, ) -> None: - super().__init__(config=config, debug=debug) self.buckets = buckets.RESTBucketManager() self.global_rate_limit = global_ratelimit self.version = version self._app = app - - if token is undefined.UNDEFINED: + self._client_session: typing.Optional[aiohttp.ClientSession] = None + self._connector = connector + self._connector_owner = connector_owner + self._debug = debug + self._http_settings = http_settings + self._proxy_settings = proxy_settings + + if token is None: full_token = None else: - if token_type is undefined.UNDEFINED: + if token_type is None: token_type = strings.BOT_TOKEN full_token = f"{token_type.title()} {token}" self._token: typing.Optional[str] = full_token - if rest_url is undefined.UNDEFINED: + if rest_url is None: rest_url = strings.REST_API_URL self._rest_url = rest_url.format(self) - @property @typing.final - def app(self) -> rest.IRESTClient: - return self._app + def _acquire_client_session(self) -> aiohttp.ClientSession: + if self._client_session is None: + self._client_session = aiohttp.ClientSession( + connector=self._connector, + version=aiohttp.HttpVersion11, + timeout=aiohttp.ClientTimeout( + total=self._http_settings.timeouts.total, + connect=self._http_settings.timeouts.acquire_and_connect, + sock_read=self._http_settings.timeouts.request_socket_read, + sock_connect=self._http_settings.timeouts.request_socket_connect, + ), + trust_env=self._proxy_settings.trust_env, + ) + + return self._client_session @typing.final async def _request( self, compiled_route: routes.CompiledRoute, *, - query: typing.Union[undefined.UndefinedType, None, data_binding.StringMapBuilder] = undefined.UNDEFINED, - body: typing.Union[ - undefined.UndefinedType, None, aiohttp.FormData, data_binding.JSONObjectBuilder, data_binding.JSONArray - ] = undefined.UNDEFINED, - reason: typing.Union[undefined.UndefinedType, str] = undefined.UNDEFINED, + query: typing.Optional[data_binding.StringMapBuilder] = None, + form: typing.Optional[aiohttp.FormData] = None, + json: typing.Union[data_binding.JSONObjectBuilder, data_binding.JSONArray, None] = None, + reason: typing.Union[str, undefined.UndefinedType] = undefined.UNDEFINED, no_auth: bool = False, ) -> typing.Union[None, data_binding.JSONObject, data_binding.JSONArray]: # Make a ratelimit-protected HTTP request to a JSON endpoint and expect some form @@ -187,95 +214,91 @@ async def _request( headers = data_binding.StringMapBuilder() headers.setdefault(strings.USER_AGENT_HEADER, strings.HTTP_USER_AGENT) - - headers.put(strings.X_RATELIMIT_PRECISION_HEADER, "millisecond") - headers.put(strings.ACCEPT_HEADER, strings.APPLICATION_JSON) + headers.put(strings.X_RATELIMIT_PRECISION_HEADER, strings.MILLISECOND_PRECISION) if self._token is not None and not no_auth: headers[strings.AUTHORIZATION_HEADER] = self._token - if body is undefined.UNDEFINED: - body = None - headers.put(strings.X_AUDIT_LOG_REASON_HEADER, reason) - if query is undefined.UNDEFINED: - query = None - while True: try: - # Moved to a separate method to keep branch counts down. - return await self._request_once(compiled_route=compiled_route, headers=headers, body=body, query=query) - except self._RetryRequest: - pass - - @typing.final - async def _request_once( - self, - compiled_route: routes.CompiledRoute, - headers: data_binding.Headers, - body: typing.Union[None, aiohttp.FormData, data_binding.JSONArray, data_binding.JSONObject], - query: typing.Union[None, data_binding.StringMapBuilder], - ) -> typing.Union[None, data_binding.JSONObject, data_binding.JSONArray]: - url = compiled_route.create_url(self._rest_url) - - # Wait for any ratelimits to finish. - await asyncio.gather(self.buckets.acquire(compiled_route), self.global_rate_limit.acquire()) - - uuid4 = str(uuid.uuid4()) - - if self._debug: - headers_str = "\n".join(f"\t\t{name}:{value}" for name, value in headers.items()) - _LOGGER.debug( - "%s %s %s\n\theaders:\n%s\n\tbody:\n\t\t%r", uuid4, compiled_route.method, url, headers_str, body - ) - else: - _LOGGER.debug("%s %s %s", uuid4, compiled_route.method, url) + url = compiled_route.create_url(self._rest_url) + + # Wait for any rate-limits to finish. + await asyncio.gather(self.buckets.acquire(compiled_route), self.global_rate_limit.acquire()) + + uuid4 = str(uuid.uuid4()) + + if self._debug: + headers_str = "\n".join(f"\t\t{name}:{value}" for name, value in headers.items()) + _LOGGER.debug( + "%s %s %s\n\theaders:\n%s\n\tbody:\n\t\t%r", + uuid4, + compiled_route.method, + url, + headers_str, + json, + ) + else: + _LOGGER.debug("%s %s %s", uuid4, compiled_route.method, url) + + # Make the request. + response = await self._acquire_client_session().request( + compiled_route.method, + url, + headers=headers, + params=query, + json=json, + data=form, + allow_redirects=self._http_settings.allow_redirects, + max_redirects=self._http_settings.max_redirects, + proxy=self._proxy_settings.url, + proxy_headers=self._proxy_settings.all_headers, + verify_ssl=self._http_settings.verify_ssl, + ) - # Make the request. - # noinspection PyUnresolvedReferences - response = await self._perform_request( - method=compiled_route.method, url=url, headers=headers, body=body, query=query - ) + if self._debug: + headers_str = "\n".join( + f"\t\t{name.decode('utf-8')}:{value.decode('utf-8')}" for name, value in response.raw_headers + ) + _LOGGER.debug( + "%s %s %s\n\theaders:\n%s\n\tbody:\n\t\t%r", + uuid4, + response.status, + response.reason, + headers_str, + await response.read(), + ) + else: + _LOGGER.debug("%s %s %s", uuid4, response.status, response.reason) - if self._debug: - headers_str = "\n".join( - f"\t\t{name.decode('utf-8')}:{value.decode('utf-8')}" for name, value in response.raw_headers - ) - _LOGGER.debug( - "%s %s %s\n\theaders:\n%s\n\tbody:\n\t\t%r", - uuid4, - response.status, - response.reason, - headers_str, - await response.read(), - ) - else: - _LOGGER.debug("%s %s %s", uuid4, response.status, response.reason) + # Ensure we aren't rate limited, and update rate limiting headers where appropriate. + await self._parse_ratelimits(compiled_route, response) - # Ensure we aren't rate limited, and update rate limiting headers where appropriate. - await self._parse_ratelimits(compiled_route, response) + # Don't bother processing any further if we got NO CONTENT. There's not anything + # to check. + if response.status == http.HTTPStatus.NO_CONTENT: + return None - # Don't bother processing any further if we got NO CONTENT. There's not anything - # to check. - if response.status == http.HTTPStatus.NO_CONTENT: - return None + # Handle the response. + if 200 <= response.status < 300: + if response.content_type == strings.APPLICATION_JSON: + # Only deserializing here stops Cloudflare shenanigans messing us around. + return data_binding.load_json(await response.read()) - # Handle the response. - if 200 <= response.status < 300: - if response.content_type == strings.APPLICATION_JSON: - # Only deserializing here stops Cloudflare shenanigans messing us around. - return data_binding.load_json(await response.read()) + real_url = str(response.real_url) + raise errors.HTTPError(real_url, f"Expected JSON response but received {response.content_type}") - real_url = str(response.real_url) - raise errors.HTTPError(real_url, f"Expected JSON response but received {response.content_type}") + return await self._handle_error_response(response) - return await self._handle_error_response(response) + except self._RetryRequest: + pass @staticmethod @typing.final async def _handle_error_response(response: aiohttp.ClientResponse) -> typing.NoReturn: - raise await http_client.generate_error_response(response) + raise await helpers.generate_error_response(response) @typing.final async def _parse_ratelimits(self, compiled_route: routes.CompiledRoute, response: aiohttp.ClientResponse) -> None: @@ -403,7 +426,8 @@ def _generate_allowed_mentions( @typing.final async def close(self) -> None: """Close the REST client and any open HTTP connections.""" - await super().close() + if self._client_session is not None: + await self._client_session.close() self.buckets.close() async def fetch_channel( @@ -524,7 +548,7 @@ async def edit_channel( conversion=self._app.entity_factory.serialize_permission_overwrite, ) - raw_response = await self._request(route, body=body, reason=reason) + raw_response = await self._request(route, json=body, reason=reason) response = typing.cast(data_binding.JSONObject, raw_response) return self._app.entity_factory.deserialize_channel(response) @@ -645,7 +669,7 @@ async def edit_permission_overwrites( body.put("allow", allow) body.put("deny", deny) - await self._request(route, body=body, reason=reason) + await self._request(route, json=body, reason=reason) async def delete_permission_overwrite( self, @@ -770,13 +794,13 @@ async def create_invite( body.put("unique", unique) body.put_snowflake("target_user_id", target_user) body.put("target_user_type", target_user_type) - raw_response = await self._request(route, body=body, reason=reason) + raw_response = await self._request(route, json=body, reason=reason) response = typing.cast(data_binding.JSONObject, raw_response) return self._app.entity_factory.deserialize_invite_with_metadata(response) def trigger_typing( self, channel: typing.Union[channels.TextChannel, snowflake.UniqueObject] - ) -> rest_utils.TypingIndicator: + ) -> special_endpoints.TypingIndicator: """Trigger typing in a text channel. Parameters @@ -787,7 +811,7 @@ def trigger_typing( Returns ------- - hikari.net.rest_utils.TypingIndicator + hikari.net.special_endpoints.TypingIndicator A typing indicator to use. Raises @@ -807,7 +831,7 @@ def trigger_typing( is awaited or interacted with. Invoking this function itself will not raise any of the above types. """ - return rest_utils.TypingIndicator(channel, self._request) + return special_endpoints.TypingIndicator(channel, self._request) async def fetch_pins( self, channel: typing.Union[channels.TextChannel, snowflake.UniqueObject] @@ -969,7 +993,7 @@ def fetch_messages( if isinstance(timestamp, datetime.datetime): timestamp = snowflake.Snowflake.from_datetime(timestamp) - return iterators.MessageIterator(self._app, self._request, str(int(channel)), direction, str(timestamp)) + return special_endpoints.MessageIterator(self._app, self._request, str(int(channel)), direction, str(timestamp)) async def fetch_message( self, @@ -1126,11 +1150,11 @@ async def create_message( f"file{i}", stream, filename=stream.filename, content_type=strings.APPLICATION_OCTET_STREAM ) - raw_response = await self._request(route, body=form) + raw_response = await self._request(route, form=form) finally: await stack.aclose() else: - raw_response = await self._request(route, body=body) + raw_response = await self._request(route, json=body) response = typing.cast(data_binding.JSONObject, raw_response) return self._app.entity_factory.deserialize_message(response) @@ -1209,7 +1233,7 @@ async def edit_message( elif embed is None: body.put("embed", None) - raw_response = await self._request(route, body=body) + raw_response = await self._request(route, json=body) response = typing.cast(data_binding.JSONObject, raw_response) return self._app.entity_factory.deserialize_message(response) @@ -1271,7 +1295,7 @@ async def delete_messages( route = routes.POST_DELETE_CHANNEL_MESSAGES_BULK.compile(channel=channel) body = data_binding.JSONObjectBuilder() body.put_snowflake_array("messages", messages) - await self._request(route, body=body) + await self._request(route, json=body) else: raise TypeError("Must delete a minimum of 2 messages and a maximum of 100") @@ -1385,7 +1409,7 @@ def fetch_reactions_for_emoji( message: typing.Union[messages_.Message, snowflake.UniqueObject], emoji: typing.Union[str, emojis.Emoji], ) -> iterators.LazyIterator[users.User]: - return iterators.ReactorIterator( + return special_endpoints.ReactorIterator( app=self._app, request_call=self._request, channel_id=str(int(channel)), @@ -1405,11 +1429,11 @@ async def create_webhook( body = data_binding.JSONObjectBuilder() body.put("name", name) if avatar is not undefined.UNDEFINED: - avatar_resouce = files.ensure_resource(avatar) - async with avatar_resouce.stream(executor=self._app.executor) as stream: + avatar_resource = files.ensure_resource(avatar) + async with avatar_resource.stream(executor=self._app.executor) as stream: body.put("avatar", await stream.data_uri()) - raw_response = await self._request(route, body=body, reason=reason) + raw_response = await self._request(route, json=body, reason=reason) response = typing.cast(data_binding.JSONObject, raw_response) return self._app.entity_factory.deserialize_webhook(response) @@ -1476,7 +1500,7 @@ async def edit_webhook( async with avatar_resource.stream(executor=self._app.executor) as stream: body.put("avatar", await stream.data_uri()) - raw_response = await self._request(route, body=body, reason=reason, no_auth=no_auth) + raw_response = await self._request(route, json=body, reason=reason, no_auth=no_auth) response = typing.cast(data_binding.JSONObject, raw_response) return self._app.entity_factory.deserialize_webhook(response) @@ -1555,11 +1579,11 @@ async def execute_webhook( f"file{i}", stream, filename=stream.filename, content_type=strings.APPLICATION_OCTET_STREAM ) - raw_response = await self._request(route, query=query, body=form, no_auth=True) + raw_response = await self._request(route, query=query, form=form, no_auth=True) finally: await stack.aclose() else: - raw_response = await self._request(route, query=query, body=body, no_auth=True) + raw_response = await self._request(route, query=query, json=body, no_auth=True) response = typing.cast(data_binding.JSONObject, raw_response) return self._app.entity_factory.deserialize_message(response) @@ -1612,7 +1636,7 @@ async def edit_my_user( async with avatar_resouce.stream(executor=self._app.executor) as stream: body.put("avatar", await stream.data_uri()) - raw_response = await self._request(route, body=body) + raw_response = await self._request(route, json=body) response = typing.cast(data_binding.JSONObject, raw_response) return self._app.entity_factory.deserialize_my_user(response) @@ -1635,7 +1659,7 @@ def fetch_my_guilds( elif isinstance(start_at, datetime.datetime): start_at = snowflake.Snowflake.from_datetime(start_at) - return iterators.OwnGuildIterator(self._app, self._request, newest_first, str(start_at)) + return special_endpoints.OwnGuildIterator(self._app, self._request, newest_first, str(start_at)) async def leave_guild(self, guild: typing.Union[guilds.Guild, snowflake.UniqueObject], /) -> None: route = routes.DELETE_MY_GUILD.compile(guild=guild) @@ -1645,7 +1669,7 @@ async def create_dm_channel(self, user: typing.Union[users.User, snowflake.Uniqu route = routes.POST_MY_CHANNELS.compile() body = data_binding.JSONObjectBuilder() body.put_snowflake("recipient_id", user) - raw_response = await self._request(route, body=body) + raw_response = await self._request(route, json=body) response = typing.cast(data_binding.JSONObject, raw_response) return self._app.entity_factory.deserialize_dm_channel(response) @@ -1676,7 +1700,7 @@ async def add_user_to_guild( body.put("deaf", deaf) body.put_snowflake_array("roles", roles) - if (raw_response := await self._request(route, body=body)) is not None: + if (raw_response := await self._request(route, json=body)) is not None: response = typing.cast(data_binding.JSONObject, raw_response) return self._app.entity_factory.deserialize_member(response) else: @@ -1715,7 +1739,7 @@ def fetch_audit_log( if user is not undefined.UNDEFINED: user = str(int(user)) - return iterators.AuditLogIterator(self._app, self._request, guild, before, user, event_type) + return special_endpoints.AuditLogIterator(self._app, self._request, guild, before, user, event_type) async def fetch_emoji( self, @@ -1759,7 +1783,7 @@ async def create_emoji( body.put_snowflake_array("roles", roles) - raw_response = await self._request(route, body=body, reason=reason) + raw_response = await self._request(route, json=body, reason=reason) response = typing.cast(data_binding.JSONObject, raw_response) return self._app.entity_factory.deserialize_known_custom_emoji(response) @@ -1782,7 +1806,7 @@ async def edit_emoji( body.put("name", name) body.put_snowflake_array("roles", roles) - raw_response = await self._request(route, body=body, reason=reason) + raw_response = await self._request(route, json=body, reason=reason) response = typing.cast(data_binding.JSONObject, raw_response) return self._app.entity_factory.deserialize_known_custom_emoji(response) @@ -1798,8 +1822,8 @@ async def delete_emoji( ) await self._request(route) - def guild_builder(self, name: str, /) -> rest_utils.GuildBuilder: - return rest_utils.GuildBuilder(app=self._app, name=name, request_call=self._request) + def guild_builder(self, name: str, /) -> special_endpoints.GuildBuilder: + return special_endpoints.GuildBuilder(app=self._app, name=name, request_call=self._request) async def fetch_guild(self, guild: typing.Union[guilds.Guild, snowflake.UniqueObject]) -> guilds.Guild: route = routes.GET_GUILD.compile(guild=guild) @@ -1880,7 +1904,7 @@ async def edit_guild( async with banner_resource.stream(executor=self._app.executor) as stream: body.put("banner", await stream.data_uri()) - raw_response = await self._request(route, body=body, reason=reason) + raw_response = await self._request(route, json=body, reason=reason) response = typing.cast(data_binding.JSONObject, raw_response) return self._app.entity_factory.deserialize_guild(response) @@ -2051,7 +2075,7 @@ async def _create_guild_channel( conversion=self._app.entity_factory.serialize_permission_overwrite, ) - raw_response = await self._request(route, body=body, reason=reason) + raw_response = await self._request(route, json=body, reason=reason) response = typing.cast(data_binding.JSONObject, raw_response) channel = self._app.entity_factory.deserialize_channel(response) return typing.cast(channels.GuildChannel, channel) @@ -2063,7 +2087,7 @@ async def reposition_channels( ) -> None: route = routes.POST_GUILD_CHANNELS.compile(guild=guild) body = [{"id": str(int(channel)), "position": pos} for pos, channel in positions.items()] - await self._request(route, body=body) + await self._request(route, json=body) async def fetch_member( self, @@ -2078,7 +2102,7 @@ async def fetch_member( def fetch_members( self, guild: typing.Union[guilds.Guild, snowflake.UniqueObject] ) -> iterators.LazyIterator[guilds.Member]: - return iterators.MemberIterator(self._app, self._request, str(int(guild))) + return special_endpoints.MemberIterator(self._app, self._request, str(int(guild))) async def edit_member( self, @@ -2108,7 +2132,7 @@ async def edit_member( elif voice_channel is not undefined.UNDEFINED: body.put_snowflake("channel_id", voice_channel) - await self._request(route, body=body, reason=reason) + await self._request(route, json=body, reason=reason) async def edit_my_nick( self, @@ -2120,7 +2144,7 @@ async def edit_my_nick( route = routes.PATCH_MY_GUILD_NICKNAME.compile(guild=guild) body = data_binding.JSONObjectBuilder() body.put("nick", nick) - await self._request(route, body=body, reason=reason) + await self._request(route, json=body, reason=reason) async def add_role_to_member( self, @@ -2165,7 +2189,7 @@ async def ban_user( body = data_binding.JSONObjectBuilder() body.put("delete_message_days", delete_message_days) route = routes.PUT_GUILD_BAN.compile(guild=guild, user=user) - await self._request(route, reason=reason, body=body) + await self._request(route, reason=reason, json=body) async def unban_user( self, @@ -2227,7 +2251,7 @@ async def create_role( body.put("hoist", hoist) body.put("mentionable", mentionable) - raw_response = await self._request(route, body=body, reason=reason) + raw_response = await self._request(route, json=body, reason=reason) response = typing.cast(data_binding.JSONObject, raw_response) return self._app.entity_factory.deserialize_role(response) @@ -2238,7 +2262,7 @@ async def reposition_roles( ) -> None: route = routes.POST_GUILD_ROLES.compile(guild=guild) body = [{"id": str(int(role)), "position": pos} for pos, role in positions.items()] - await self._request(route, body=body) + await self._request(route, json=body) async def edit_role( self, @@ -2266,7 +2290,7 @@ async def edit_role( body.put("hoist", hoist) body.put("mentionable", mentionable) - raw_response = await self._request(route, body=body, reason=reason) + raw_response = await self._request(route, json=body, reason=reason) response = typing.cast(data_binding.JSONObject, raw_response) return self._app.entity_factory.deserialize_role(response) @@ -2382,7 +2406,7 @@ async def begin_guild_prune( body.put("days", days) body.put("compute_prune_count", compute_prune_count) body.put_snowflake_array("include_roles", include_roles) - raw_response = await self._request(route, body=body, reason=reason) + raw_response = await self._request(route, json=body, reason=reason) response = typing.cast(data_binding.JSONObject, raw_response) pruned = response.get("pruned") return int(pruned) if pruned is not None else None @@ -2429,7 +2453,7 @@ async def edit_integration( body.put("expire_grace_period", expire_grace_period, conversion=date.timespan_to_int) # Inconsistent naming in the API itself, so I have changed the name. body.put("enable_emoticons", enable_emojis) - await self._request(route, body=body, reason=reason) + await self._request(route, json=body, reason=reason) async def delete_integration( self, @@ -2474,7 +2498,7 @@ async def edit_widget( elif channel is not undefined.UNDEFINED: body.put_snowflake("channel", channel) - raw_response = await self._request(route, body=body, reason=reason) + raw_response = await self._request(route, json=body, reason=reason) response = typing.cast(data_binding.JSONObject, raw_response) return self._app.entity_factory.deserialize_guild_widget(response) diff --git a/hikari/net/rest_utils.py b/hikari/net/special_endpoints.py similarity index 71% rename from hikari/net/rest_utils.py rename to hikari/net/special_endpoints.py index f8d0471974..8b6c70506c 100644 --- a/hikari/net/rest_utils.py +++ b/hikari/net/special_endpoints.py @@ -15,7 +15,7 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . -"""Internal utilities used by the REST API. +"""Special endpoint implementations. You should never need to make any of these objects manually. """ @@ -33,6 +33,8 @@ from hikari.net import routes from hikari.utilities import data_binding from hikari.utilities import date +from hikari.utilities import iterators +from hikari.utilities import snowflake from hikari.utilities import snowflake as snowflake_ from hikari.utilities import undefined @@ -40,10 +42,14 @@ import types from hikari.api import rest + from hikari.models import applications + from hikari.models import audit_logs from hikari.models import channels from hikari.models import colors from hikari.models import guilds + from hikari.models import messages from hikari.models import permissions as permissions_ + from hikari.models import users from hikari.utilities import files @@ -327,9 +333,9 @@ def add_role( if not undefined.count(color, colour): raise TypeError("Cannot specify 'color' and 'colour' together.") - snowflake = self._new_snowflake() + snowflake_id = self._new_snowflake() payload = data_binding.JSONObjectBuilder() - payload.put_snowflake("id", snowflake) + payload.put_snowflake("id", snowflake_id) payload.put("name", name) payload.put("color", color) payload.put("color", colour) @@ -338,7 +344,7 @@ def add_role( payload.put("permissions", permissions) payload.put("position", position) self._roles.append(payload) - return snowflake + return snowflake_id def add_category( self, @@ -375,9 +381,9 @@ def add_category( When the guild is created, this will be replaced with a different ID. """ # noqa: E501 - Line too long - snowflake = self._new_snowflake() + snowflake_id = self._new_snowflake() payload = data_binding.JSONObjectBuilder() - payload.put_snowflake("id", snowflake) + payload.put_snowflake("id", snowflake_id) payload.put("name", name) payload.put("type", channels.ChannelType.GUILD_CATEGORY) payload.put("position", position) @@ -390,7 +396,7 @@ def add_category( ) self._channels.append(payload) - return snowflake + return snowflake_id def add_text_channel( self, @@ -439,9 +445,9 @@ def add_text_channel( When the guild is created, this will be replaced with a different ID. """ # noqa: E501 - Line too long - snowflake = self._new_snowflake() + snowflake_id = self._new_snowflake() payload = data_binding.JSONObjectBuilder() - payload.put_snowflake("id", snowflake) + payload.put_snowflake("id", snowflake_id) payload.put("name", name) payload.put("type", channels.ChannelType.GUILD_TEXT) payload.put("topic", topic) @@ -457,7 +463,7 @@ def add_text_channel( ) self._channels.append(payload) - return snowflake + return snowflake_id def add_voice_channel( self, @@ -506,9 +512,9 @@ def add_voice_channel( When the guild is created, this will be replaced with a different ID. """ # noqa: E501 - Line too long - snowflake = self._new_snowflake() + snowflake_id = self._new_snowflake() payload = data_binding.JSONObjectBuilder() - payload.put_snowflake("id", snowflake) + payload.put_snowflake("id", snowflake_id) payload.put("name", name) payload.put("type", channels.ChannelType.GUILD_VOICE) payload.put("bitrate", bitrate) @@ -524,9 +530,209 @@ def add_voice_channel( ) self._channels.append(payload) - return snowflake + return snowflake_id def _new_snowflake(self) -> snowflake_.Snowflake: value = self._counter self._counter += 1 return snowflake_.Snowflake.from_data(datetime.datetime.now(tz=datetime.timezone.utc), 0, 0, value,) + + +# We use an explicit forward reference for this, since this breaks potential +# circular import issues (once the file has executed, using those resources is +# not an issue for us). +class MessageIterator(iterators.BufferedLazyIterator["messages.Message"]): + """Implementation of an iterator for message history.""" + + __slots__: typing.Sequence[str] = ("_app", "_request_call", "_direction", "_first_id", "_route") + + def __init__( + self, + app: rest.IRESTClient, + request_call: typing.Callable[ + ..., typing.Coroutine[None, None, typing.Union[None, data_binding.JSONObject, data_binding.JSONArray]] + ], + channel_id: str, + direction: str, + first_id: str, + ) -> None: + super().__init__() + self._app = app + self._request_call = request_call + self._direction = direction + self._first_id = first_id + self._route = routes.GET_CHANNEL_MESSAGES.compile(channel=channel_id) + + async def _next_chunk(self) -> typing.Optional[typing.Generator[messages.Message, typing.Any, None]]: + query = data_binding.StringMapBuilder() + query.put(self._direction, self._first_id) + query.put("limit", 100) + + raw_chunk = await self._request_call(compiled_route=self._route, query=query) + chunk = typing.cast(data_binding.JSONArray, raw_chunk) + + if not chunk: + return None + if self._direction == "after": + chunk.reverse() + + self._first_id = chunk[-1]["id"] + return (self._app.entity_factory.deserialize_message(m) for m in chunk) + + +# We use an explicit forward reference for this, since this breaks potential +# circular import issues (once the file has executed, using those resources is +# not an issue for us). +class ReactorIterator(iterators.BufferedLazyIterator["users.User"]): + """Implementation of an iterator for message reactions.""" + + __slots__: typing.Sequence[str] = ("_app", "_first_id", "_route", "_request_call") + + def __init__( + self, + app: rest.IRESTClient, + request_call: typing.Callable[ + ..., typing.Coroutine[None, None, typing.Union[None, data_binding.JSONObject, data_binding.JSONArray]] + ], + channel_id: str, + message_id: str, + emoji: str, + ) -> None: + super().__init__() + self._app = app + self._request_call = request_call + self._first_id = snowflake.Snowflake.min() + self._route = routes.GET_REACTIONS.compile(channel=channel_id, message=message_id, emoji=emoji) + + async def _next_chunk(self) -> typing.Optional[typing.Generator[users.User, typing.Any, None]]: + query = data_binding.StringMapBuilder() + query.put("after", self._first_id) + query.put("limit", 100) + + raw_chunk = await self._request_call(compiled_route=self._route, query=query) + chunk = typing.cast(data_binding.JSONArray, raw_chunk) + + if not chunk: + return None + + self._first_id = chunk[-1]["id"] + return (self._app.entity_factory.deserialize_user(u) for u in chunk) + + +# We use an explicit forward reference for this, since this breaks potential +# circular import issues (once the file has executed, using those resources is +# not an issue for us). +class OwnGuildIterator(iterators.BufferedLazyIterator["applications.OwnGuild"]): + """Implementation of an iterator for retrieving guilds you are in.""" + + __slots__: typing.Sequence[str] = ("_app", "_request_call", "_route", "_newest_first", "_first_id") + + def __init__( + self, + app: rest.IRESTClient, + request_call: typing.Callable[ + ..., typing.Coroutine[None, None, typing.Union[None, data_binding.JSONObject, data_binding.JSONArray]] + ], + newest_first: bool, + first_id: str, + ) -> None: + super().__init__() + self._app = app + self._newest_first = newest_first + self._request_call = request_call + self._first_id = first_id + self._route = routes.GET_MY_GUILDS.compile() + + async def _next_chunk(self) -> typing.Optional[typing.Generator[applications.OwnGuild, typing.Any, None]]: + query = data_binding.StringMapBuilder() + query.put("before" if self._newest_first else "after", self._first_id) + query.put("limit", 100) + + raw_chunk = await self._request_call(compiled_route=self._route, query=query) + chunk = typing.cast(data_binding.JSONArray, raw_chunk) + + if not chunk: + return None + + self._first_id = chunk[-1]["id"] + return (self._app.entity_factory.deserialize_own_guild(g) for g in chunk) + + +# We use an explicit forward reference for this, since this breaks potential +# circular import issues (once the file has executed, using those resources is +# not an issue for us). +class MemberIterator(iterators.BufferedLazyIterator["guilds.Member"]): + """Implementation of an iterator for retrieving members in a guild.""" + + __slots__: typing.Sequence[str] = ("_app", "_request_call", "_route", "_first_id") + + def __init__( + self, + app: rest.IRESTClient, + request_call: typing.Callable[ + ..., typing.Coroutine[None, None, typing.Union[None, data_binding.JSONObject, data_binding.JSONArray]] + ], + guild_id: str, + ) -> None: + super().__init__() + self._route = routes.GET_GUILD_MEMBERS.compile(guild=guild_id) + self._request_call = request_call + self._app = app + self._first_id = snowflake.Snowflake.min() + + async def _next_chunk(self) -> typing.Optional[typing.Generator[guilds.Member, typing.Any, None]]: + query = data_binding.StringMapBuilder() + query.put("after", self._first_id) + query.put("limit", 100) + + raw_chunk = await self._request_call(compiled_route=self._route, query=query) + chunk = typing.cast(data_binding.JSONArray, raw_chunk) + + if not chunk: + return None + + # noinspection PyTypeChecker + self._first_id = chunk[-1]["user"]["id"] + + return (self._app.entity_factory.deserialize_member(m) for m in chunk) + + +# We use an explicit forward reference for this, since this breaks potential +# circular import issues (once the file has executed, using those resources is +# not an issue for us). +class AuditLogIterator(iterators.LazyIterator["audit_logs.AuditLog"]): + """Iterator implementation for an audit log.""" + + def __init__( + self, + app: rest.IRESTClient, + request_call: typing.Callable[ + ..., typing.Coroutine[None, None, typing.Union[None, data_binding.JSONObject, data_binding.JSONArray]] + ], + guild_id: str, + before: str, + user_id: typing.Union[str, undefined.UndefinedType], + action_type: typing.Union[int, undefined.UndefinedType], + ) -> None: + self._action_type = action_type + self._app = app + self._first_id = str(before) + self._request_call = request_call + self._route = routes.GET_GUILD_AUDIT_LOGS.compile(guild=guild_id) + self._user_id = user_id + + async def __anext__(self) -> audit_logs.AuditLog: + query = data_binding.StringMapBuilder() + query.put("limit", 100) + query.put("user_id", self._user_id) + query.put("event_type", self._action_type) + + raw_response = await self._request_call(compiled_route=self._route, query=query) + response = typing.cast(data_binding.JSONObject, raw_response) + + if not response["entries"]: + raise StopAsyncIteration + + log = self._app.entity_factory.deserialize_audit_log(response) + self._first_id = str(min(log.entries.keys())) + return log diff --git a/hikari/net/strings.py b/hikari/net/strings.py index a8dae76571..42ab622711 100644 --- a/hikari/net/strings.py +++ b/hikari/net/strings.py @@ -34,6 +34,7 @@ CONTENT_LENGTH_HEADER: typing.Final[str] = "Content-Length" CONTENT_TYPE_HEADER: typing.Final[str] = "Content-Type" DATE_HEADER: typing.Final[str] = "Date" +PROXY_AUTHENTICATION_HEADER: typing.Final[str] = "Proxy-Authentication" USER_AGENT_HEADER: typing.Final[str] = "User-Agent" X_AUDIT_LOG_REASON_HEADER: typing.Final[str] = "X-Audit-Log-Reason" X_RATELIMIT_BUCKET_HEADER: typing.Final[str] = "X-RateLimit-Bucket" @@ -49,6 +50,7 @@ APPLICATION_OCTET_STREAM: typing.Final[str] = "application/octet-stream" # Bits of text. +BASICAUTH_TOKEN: typing.Final[str] = "Basic" # nosec BEARER_TOKEN: typing.Final[str] = "Bearer" # nosec BOT_TOKEN: typing.Final[str] = "Bot" # nosec MILLISECOND_PRECISION: typing.Final[str] = "millisecond" diff --git a/hikari/utilities/cdn.py b/hikari/utilities/cdn.py index f2d876ea56..3019f0d6dc 100644 --- a/hikari/utilities/cdn.py +++ b/hikari/utilities/cdn.py @@ -24,8 +24,8 @@ import typing import urllib.parse -from hikari.net import strings from hikari.utilities import files +from hikari.net import strings def generate_cdn_url(*route_parts: str, format_: str, size: typing.Optional[int]) -> files.URL: diff --git a/hikari/utilities/files.py b/hikari/utilities/files.py index afc08f0f91..4102fad740 100644 --- a/hikari/utilities/files.py +++ b/hikari/utilities/files.py @@ -47,7 +47,7 @@ import aiohttp.client import attr -from hikari.net import http_client +from hikari.net import helpers if typing.TYPE_CHECKING: import concurrent.futures @@ -450,7 +450,7 @@ async def __aenter__(self) -> WebReader: head_only=self._head_only, ) else: - raise await http_client.generate_error_response(resp) + raise await helpers.generate_error_response(resp) except Exception as ex: await ctx.__aexit__(type(ex), ex, ex.__traceback__) diff --git a/hikari/utilities/iterators.py b/hikari/utilities/iterators.py new file mode 100644 index 0000000000..38da54aeed --- /dev/null +++ b/hikari/utilities/iterators.py @@ -0,0 +1,592 @@ +# -*- coding: utf-8 -*- +# Copyright © Nekoka.tt 2019-2020 +# +# This file is part of Hikari. +# +# Hikari is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Hikari is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Hikari. If not, see . +"""Lazy iterators for data that requires repeated API calls to retrieve. + +For consumers of this API, the only class you need to worry about is +`LazyIterator`. Everything else is internal detail only exposed for people who +wish to extend this API further! +""" +from __future__ import annotations + +__all__: typing.Final[typing.Sequence[str]] = [ + "LazyIterator", + "All", + "AttrComparator", + "BufferedLazyIterator", + "DropCountLazyIterator", + "DropWhileLazyIterator", + "EnumeratedLazyIterator", + "FilteredLazyIterator", + "LimitedLazyIterator", + "MappingLazyIterator", + "TakeWhileLazyIterator", +] + +import abc +import typing + +from hikari.utilities import spel + +ValueT = typing.TypeVar("ValueT") +AnotherValueT = typing.TypeVar("AnotherValueT") + + +class All(typing.Generic[ValueT]): + """Helper that wraps predicates and invokes them together. + + Calling this object will pass the input item to each item, returning + `True` only when all wrapped predicates return True when called with the + given item. + + For example... + + ```py + if w(foo) and x(foo) and y(foo) and z(foo): + ... + ``` + is equivalent to + ```py + condition = All([w, x, y, z]) + + if condition(foo): + ... + ``` + + This behaves like a lazy wrapper implementation of the `all` builtin. + + !!! note + Like the rest of the standard library, this is a short-circuiting + operation. This means that if a predicate returns `False`, no predicates + after this are invoked, as the result is already known. In this sense, + they are invoked in-order. + + !!! warning + You shouldn't generally need to use this outside of extending the + iterators API in this library! + + Operators + --------- + * `this(value : T) -> bool`: + Return `True` if all conditions return `True` when invoked with the + given value. + * `~this`: + Return a condition that, when invoked with the value, returns `False` + if all conditions were `True` in this object. + + Parameters + ---------- + *conditions : typing.Callable[[T], bool] + The predicates to wrap. + """ + + __slots__: typing.Sequence[str] = ("conditions",) + + def __init__(self, conditions: typing.Collection[typing.Callable[[ValueT], bool]]) -> None: + self.conditions = conditions + + def __call__(self, item: ValueT) -> bool: + return all(condition(item) for condition in self.conditions) + + def __invert__(self) -> typing.Callable[[ValueT], bool]: + return lambda item: not self(item) + + +class AttrComparator(typing.Generic[ValueT]): + """A comparator that compares the result of a call with something else. + + This uses the `spel` module internally. + + Parameters + ---------- + attr_name : str + The attribute name. Can be prepended with a `.` optionally. + If the attribute name ends with a `()`, then the call is invoked + rather than treated as a property (useful for methods like + `str.isupper`, for example). + expected_value : T + The expected value. + """ + + __slots__: typing.Sequence[str] = ("attr_getter", "expected_value") + + def __init__(self, attr_name: str, expected_value: typing.Any) -> None: + self.expected_value = expected_value + self.attr_getter: spel.AttrGetter[ValueT, typing.Any] = spel.AttrGetter(attr_name) + + def __call__(self, item: ValueT) -> bool: + return bool(self.attr_getter(item)) + + +class LazyIterator(typing.Generic[ValueT], abc.ABC): + """A set of results that are fetched asynchronously from the API as needed. + + This is a `typing.AsyncIterable` and `typing.AsyncIterator` with several + additional helpful methods provided for convenience. + + Examples + -------- + You can use this in multiple ways. + + As an async iterable: + + ```py + >>> async for item in paginated_results: + ... process(item) + ``` + + As an eagerly retrieved set of results (performs all API calls at once, + which may be slow for large sets of data): + + ```py + >>> results = await paginated_results + >>> # ... which is equivalent to this... + >>> results = [item async for item in paginated_results] + ``` + + As an async iterator (not recommended): + + ```py + >>> try: + ... while True: + ... process(await paginated_results.__anext__()) + ... except StopAsyncIteration: + ... pass + ``` + + Additionally, you can make use of some of the provided helper methods + on this class to perform basic operations easily. + + Iterating across the items with indexes (like `enumerate` for normal + iterables): + + ```py + >>> async for i, item in paginated_results.enumerate(): + ... print(i, item) + (0, foo) + (1, bar) + (2, baz) + ``` + + Limiting the number of results you iterate across: + + ```py + >>> async for item in paginated_results.limit(3): + ... process(item) + ``` + """ + + __slots__: typing.Sequence[str] = () + + def map( + self, transformation: typing.Union[typing.Callable[[ValueT], AnotherValueT], str], + ) -> LazyIterator[AnotherValueT]: + """Map the values to a different value. + + Parameters + ---------- + transformation : typing.Callable[[ValueT], bool] or str + The function to use to map the attribute. This may alternatively + be a string attribute name to replace the input value with. You + can provide nested attributes using the `.` operator. + + Returns + ------- + LazyIterator[AnotherValueT] + LazyIterator that maps each value to another value. + """ + if isinstance(transformation, str): + transformation = typing.cast("spel.AttrGetter[ValueT, AnotherValueT]", spel.AttrGetter(transformation)) + + return MappingLazyIterator(self, transformation) + + def filter( + self, + *predicates: typing.Union[typing.Tuple[str, typing.Any], typing.Callable[[ValueT], bool]], + **attrs: typing.Any, + ) -> LazyIterator[ValueT]: + """Filter the items by one or more conditions that must all be `True`. + + Parameters + ---------- + *predicates : typing.Callable[[ValueT], bool] or typing.Tuple[str, typing.Any] + Predicates to invoke. These are functions that take a value and + return `True` if it is of interest, or `False` otherwise. These + may instead include 2-`tuple` objects consisting of a `str` + attribute name (nested attributes are referred to using the `.` + operator), and values to compare for equality. This allows you + to specify conditions such as `members.filter(("user.bot", True))`. + **attrs : typing.Any + Alternative to passing 2-tuples. Cannot specify nested attributes + using this method. + + Returns + ------- + LazyIterator[ValueT] + LazyIterator that only emits values where all conditions are + matched. + """ + conditions = self._map_predicates_and_attr_getters("filter", *predicates, **attrs) + return FilteredLazyIterator(self, conditions) + + def take_while( + self, + *predicates: typing.Union[typing.Tuple[str, typing.Any], typing.Callable[[ValueT], bool]], + **attrs: typing.Any, + ) -> LazyIterator[ValueT]: + """Return each item until any conditions fail or the end is reached. + + Parameters + ---------- + *predicates : typing.Callable[[ValueT], bool] or typing.Tuple[str, typing.Any] + Predicates to invoke. These are functions that take a value and + return `True` if it is of interest, or `False` otherwise. These + may instead include 2-`tuple` objects consisting of a `str` + attribute name (nested attributes are referred to using the `.` + operator), and values to compare for equality. This allows you + to specify conditions such as `members.take_while(("user.bot", True))`. + **attrs : typing.Any + Alternative to passing 2-tuples. Cannot specify nested attributes + using this method. + + Returns + ------- + LazyIterator[ValueT] + LazyIterator that only emits values until any conditions are not + matched. + """ + conditions = self._map_predicates_and_attr_getters("take_while", *predicates, **attrs) + return TakeWhileLazyIterator(self, conditions) + + def take_until( + self, + *predicates: typing.Union[typing.Tuple[str, typing.Any], typing.Callable[[ValueT], bool]], + **attrs: typing.Any, + ) -> LazyIterator[ValueT]: + """Returns the inverse of `take_while`.""" + conditions = self._map_predicates_and_attr_getters("take_until", *predicates, **attrs) + return TakeWhileLazyIterator(self, ~conditions) + + def skip_while( + self, + *predicates: typing.Union[typing.Tuple[str, typing.Any], typing.Callable[[ValueT], bool]], + **attrs: typing.Any, + ) -> LazyIterator[ValueT]: + """Discard items while all conditions are True. + + Items after this will be yielded as normal. + + Parameters + ---------- + *predicates : typing.Callable[[ValueT], bool] or typing.Tuple[str, typing.Any] + Predicates to invoke. These are functions that take a value and + return `True` if it is of interest, or `False` otherwise. These + may instead include 2-`tuple` objects consisting of a `str` + attribute name (nested attributes are referred to using the `.` + operator), and values to compare for equality. This allows you + to specify conditions such as `members.take_while(("user.bot", True))`. + **attrs : typing.Any + Alternative to passing 2-tuples. Cannot specify nested attributes + using this method. + + Returns + ------- + LazyIterator[ValueT] + LazyIterator that only emits values once a condition has been met. + All items before this are discarded. + """ + conditions = self._map_predicates_and_attr_getters("drop_while", *predicates, **attrs) + return DropWhileLazyIterator(self, conditions) + + def skip_until( + self, + *predicates: typing.Union[typing.Tuple[str, typing.Any], typing.Callable[[ValueT], bool]], + **attrs: typing.Any, + ) -> LazyIterator[ValueT]: + """Returns the inverse of `drop_while`.""" + conditions = self._map_predicates_and_attr_getters("drop_while", *predicates, **attrs) + return DropWhileLazyIterator(self, ~conditions) + + def enumerate(self, *, start: int = 0) -> LazyIterator[typing.Tuple[int, ValueT]]: + """Enumerate the paginated results lazily. + + This behaves as an asyncio-friendly version of `builtins.enumerate` + which uses much less memory than collecting all the results first and + calling `enumerate` across them. + + Parameters + ---------- + start : int + Optional int to start at. If omitted, this is `0`. + + Examples + -------- + >>> async for i, item in paginated_results.enumerate(): + ... print(i, item) + (0, foo) + (1, bar) + (2, baz) + (3, bork) + (4, qux) + + >>> async for i, item in paginated_results.enumerate(start=9): + ... print(i, item) + (9, foo) + (10, bar) + (11, baz) + (12, bork) + (13, qux) + + >>> async for i, item in paginated_results.enumerate(start=9).limit(3): + ... print(i, item) + (9, foo) + (10, bar) + (11, baz) + + Returns + ------- + LazyIterator[typing.Tuple[int, T]] + A paginated results view that asynchronously yields an increasing + counter in a tuple with each result, lazily. + """ + return EnumeratedLazyIterator(self, start=start) + + def limit(self, limit: int) -> LazyIterator[ValueT]: + """Limit the number of items you receive from this async iterator. + + Parameters + ---------- + limit : int + The number of items to get. This must be greater than zero. + + Examples + -------- + >>> async for item in paginated_results.limit(3): + ... print(item) + + Returns + ------- + LazyIterator[T] + A paginated results view that asynchronously yields a maximum + of the given number of items before completing. + """ + return LimitedLazyIterator(self, limit) + + def skip(self, number: int) -> LazyIterator[ValueT]: + """Drop the given number of items, then yield anything after. + + Parameters + ---------- + number : int + The max number of items to drop before any items are yielded. + + Returns + ------- + LazyIterator[T] + A paginated results view that asynchronously yields all items + AFTER the given number of items are discarded first. + """ + return DropCountLazyIterator(self, number) + + async def first(self) -> ValueT: + """Return the first element of this iterator only.""" + return await self.__anext__() + + @staticmethod + def _map_predicates_and_attr_getters( + alg_name: str, + *predicates: typing.Union[str, typing.Tuple[str, typing.Any], typing.Callable[[ValueT], bool]], + **attrs: typing.Any, + ) -> All[ValueT]: + if not predicates and not attrs: + raise TypeError(f"You should provide at least one predicate to {alg_name}()") + + conditions: typing.List[typing.Callable[[ValueT], bool]] = [] + + for p in predicates: + if isinstance(p, tuple): + name, value = p + tuple_comparator: AttrComparator[ValueT] = AttrComparator(name, value) + conditions.append(tuple_comparator) + elif isinstance(p, str): + comparator: AttrComparator[ValueT] = AttrComparator(p, bool) + conditions.append(comparator) + else: + conditions.append(p) + + for name, value in attrs.items(): + attr_comparator: AttrComparator[ValueT] = AttrComparator(name, value) + conditions.append(attr_comparator) + + return All(conditions) + + def _complete(self) -> typing.NoReturn: + raise StopAsyncIteration("No more items exist in this paginator. It has been exhausted.") from None + + def __aiter__(self) -> LazyIterator[ValueT]: + # We are our own iterator. + return self + + async def _fetch_all(self) -> typing.Sequence[ValueT]: + return [item async for item in self] + + def __await__(self) -> typing.Generator[None, None, typing.Sequence[ValueT]]: + return self._fetch_all().__await__() + + @abc.abstractmethod + async def __anext__(self) -> ValueT: + ... + + +class EnumeratedLazyIterator(typing.Generic[ValueT], LazyIterator[typing.Tuple[int, ValueT]]): + __slots__: typing.Sequence[str] = ("_i", "_paginator") + + def __init__(self, paginator: LazyIterator[ValueT], *, start: int) -> None: + self._i = start + self._paginator = paginator + + async def __anext__(self) -> typing.Tuple[int, ValueT]: + pair = self._i, await self._paginator.__anext__() + self._i += 1 + return pair + + +class LimitedLazyIterator(typing.Generic[ValueT], LazyIterator[ValueT]): + __slots__: typing.Sequence[str] = ("_paginator", "_count", "_limit") + + def __init__(self, paginator: LazyIterator[ValueT], limit: int) -> None: + if limit <= 0: + raise ValueError("limit must be positive and non-zero") + self._paginator = paginator + self._count = 0 + self._limit = limit + + async def __anext__(self) -> ValueT: + if self._count >= self._limit: + self._complete() + + next_item = await self._paginator.__anext__() + self._count += 1 + return next_item + + +class DropCountLazyIterator(typing.Generic[ValueT], LazyIterator[ValueT]): + __slots__: typing.Sequence[str] = ("_paginator", "_count", "_number") + + def __init__(self, paginator: LazyIterator[ValueT], number: int) -> None: + if number <= 0: + raise ValueError("number must be positive and non-zero") + self._paginator = paginator + self._count = 0 + self._number = number + + async def __anext__(self) -> ValueT: + while self._count < self._number: + self._count += 1 + await self._paginator.__anext__() + + next_item = await self._paginator.__anext__() + return next_item + + +class FilteredLazyIterator(typing.Generic[ValueT], LazyIterator[ValueT]): + __slots__: typing.Sequence[str] = ("_paginator", "_predicate") + + def __init__(self, paginator: LazyIterator[ValueT], predicate: typing.Callable[[ValueT], bool]) -> None: + self._paginator = paginator + self._predicate = predicate + + async def __anext__(self) -> ValueT: + async for item in self._paginator: + if self._predicate(item): + return item + raise StopAsyncIteration + + +class MappingLazyIterator(typing.Generic[AnotherValueT, ValueT], LazyIterator[ValueT]): + __slots__: typing.Sequence[str] = ("_paginator", "_transformation") + + def __init__( + self, paginator: LazyIterator[AnotherValueT], transformation: typing.Callable[[AnotherValueT], ValueT], + ) -> None: + self._paginator = paginator + self._transformation = transformation + + async def __anext__(self) -> ValueT: + return self._transformation(await self._paginator.__anext__()) + + +class TakeWhileLazyIterator(typing.Generic[ValueT], LazyIterator[ValueT]): + __slots__: typing.Sequence[str] = ("_paginator", "_condition") + + def __init__(self, paginator: LazyIterator[ValueT], condition: typing.Callable[[ValueT], bool],) -> None: + self._paginator = paginator + self._condition = condition + + async def __anext__(self) -> ValueT: + item = await self._paginator.__anext__() + + if self._condition(item): + return item + + self._complete() + + +class DropWhileLazyIterator(typing.Generic[ValueT], LazyIterator[ValueT]): + __slots__: typing.Sequence[str] = ("_paginator", "_condition", "_has_dropped") + + def __init__(self, paginator: LazyIterator[ValueT], condition: typing.Callable[[ValueT], bool],) -> None: + self._paginator = paginator + self._condition = condition + self._has_dropped = False + + async def __anext__(self) -> ValueT: + if not self._has_dropped: + while not self._condition(item := await self._paginator.__anext__()): + pass + + self._has_dropped = True + return item + + return await self._paginator.__anext__() + + +class BufferedLazyIterator(typing.Generic[ValueT], LazyIterator[ValueT]): + __slots__: typing.Sequence[str] = ("_buffer",) + + def __init__(self) -> None: + empty_genexp = typing.cast(typing.Generator[ValueT, None, None], (_ for _ in ())) + self._buffer: typing.Optional[typing.Generator[ValueT, None, None]] = empty_genexp + + @abc.abstractmethod + async def _next_chunk(self) -> typing.Optional[typing.Generator[ValueT, None, None]]: + ... + + async def __anext__(self) -> ValueT: + # This sneaky snippet of code lets us use generators rather than lists. + # This is important, as we can use this to make generators that + # deserialize loads of items lazy. If we only want 10 messages of + # history, we can use the same code and prefetch 100 without any + # performance hit from it other than the JSON string response. + try: + if self._buffer is not None: + return next(self._buffer) + except StopIteration: + self._buffer = await self._next_chunk() + if self._buffer is not None: + return next(self._buffer) + self._complete() diff --git a/hikari/utilities/spel.py b/hikari/utilities/spel.py new file mode 100644 index 0000000000..b4311fbb77 --- /dev/null +++ b/hikari/utilities/spel.py @@ -0,0 +1,93 @@ +# -*- coding: utf-8 -*- +# Copyright © Nekoka.tt 2019-2020 +# +# This file is part of Hikari. +# +# Hikari is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Hikari is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Hikari. If not, see . +"""HikariSPEL (Hikari SimPle Expression Language). + +HikariSPEL (Hikari SimPle Expression Language) is a super-simple expression +language used in this module for quickly mapping values to other values and +producing streams of changes. This somewhat mirrors other programming languages +like Java which have a proper Stream API. + +The concept of HikariSPEL is that you are trying to look at the attribute +of something. So, running `"bar.baz.bork"` against an object `foo` would be +equivalent to `foo.bar.baz.bork` in pure Python. The reason for doing this is +Python lambdas are clunky, and using a nested function is nasty boilerplate. + +For applying `"bar.baz"` to `foo`, we assume `bar` is an attribute or property +of `foo`, and `baz` is an attribute or property of `foo.bar`. We may instead +want to invoke a method that takes no parameters (looking at `str.islower`, as +an example. To do this, we append `()` onto the attribute name. For example, +applying `author.username.islower()` to a `hikari.models.messages.Message` +object. + +You may also want to negate a condition. To do this, prepend `!` to the +attribute name. For example, to check if a message was not made by a bot, +you could run `author.!is_bot` on a `Message` object. Likewise, to check if +the input was not a number, you could run `content.!isdigit()`. + +This expression language is highly experimental and may change without +prior notice for the time being while I play with getting something usable +and nice to work with. +""" + +import operator +import typing + + +InputValueT = typing.TypeVar("InputValueT") +ReturnValueT = typing.TypeVar("ReturnValueT") + + +class AttrGetter(typing.Generic[InputValueT, ReturnValueT]): + """An attribute getter that can resolve nested attributes and methods.""" + + __slots__: typing.Sequence[str] = ("pipeline",) + + def __init__(self, attr_name: str) -> None: + if attr_name.startswith("."): + attr_name = attr_name[1:] + + self.pipeline: typing.List[typing.Callable[[typing.Any], typing.Any]] = [] + + for operation in attr_name.split("."): + self.pipeline.append(self._transform(operation)) + + def _transform(self, attr_name: str) -> typing.Callable[[typing.Any], typing.Any]: + if attr_name.startswith("!"): + attr_name = attr_name[1:] + invert = True + else: + invert = False + + op = self._to_op(attr_name) + + if invert: + return lambda value: not op(value) + + return op + + @staticmethod + def _to_op(attr_name: str) -> typing.Callable[[typing.Any], typing.Any]: + op = operator.methodcaller(attr_name[:-2]) if attr_name.endswith("()") else operator.attrgetter(attr_name) + return typing.cast("typing.Callable[[typing.Any], typing.Any]", op) + + def __call__(self, item: InputValueT) -> ReturnValueT: + result: typing.Any = item + for op in self.pipeline: + result = op(result) + + return typing.cast(ReturnValueT, result) diff --git a/pages/index.html b/pages/index.html index 86d25e1e7c..7412a25d76 100644 --- a/pages/index.html +++ b/pages/index.html @@ -117,7 +117,7 @@

    pip install --pre
    - hikari[speedups] + hikari

    A new, powerful, static-typed Python API for writing Discord bots.

    diff --git a/tests/hikari/client_session_stub.py b/tests/hikari/client_session_stub.py new file mode 100644 index 0000000000..7740174d11 --- /dev/null +++ b/tests/hikari/client_session_stub.py @@ -0,0 +1,64 @@ +# -*- coding: utf-8 -*- +# Copyright © Nekoka.tt 2019-2020 +# +# This file is part of Hikari. +# +# Hikari is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Hikari is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Hikari. If not, see . +import aiohttp +import asyncio +import mock + + +class RequestContextStub: + def __init__(self, response_getter) -> None: + self.response_getter = response_getter + self.await_count = 0 + + async def __aenter__(self) -> None: + return self.response_getter() + + async def __aexit__(self, exc_type, exc_val, exc_tb) -> None: + pass + + def __await__(self) -> aiohttp.ClientResponse: + # noinspection PyUnreachableCode + if False: + yield # Turns this into a generator. + self.await_count += 1 + return self.response_getter() + + def assert_awaited_once(self): + assert self.await_count == 1 + + +class ClientSessionStub: + def __init__(self) -> None: + self.close = mock.AsyncMock() + self.closed = mock.PropertyMock() + self.connector = mock.create_autospec(aiohttp.BaseConnector) + self.cookie_jar = mock.create_autospec(aiohttp.CookieJar) + self.version = aiohttp.HttpVersion11 + + self.response_stub = mock.create_autospec(aiohttp.ClientResponse) + self.websocket_stub = mock.create_autospec(aiohttp.ClientWebSocketResponse) + + self.request_context_stub = RequestContextStub(lambda: self.response_stub) + self.ws_connect_stub = RequestContextStub(lambda: self.websocket_stub) + + self.request = mock.MagicMock(wraps=lambda *args, **kwargs: self.request_context_stub) + self.ws_connect = mock.MagicMock(wraps=lambda *args, **kwargs: self.ws_connect_stub) + + @property + def loop(self): + return asyncio.current_task().get_loop() diff --git a/tests/hikari/net/test_gateway.py b/tests/hikari/net/test_gateway.py index 2f05d614ae..26bc387923 100644 --- a/tests/hikari/net/test_gateway.py +++ b/tests/hikari/net/test_gateway.py @@ -26,14 +26,38 @@ from hikari import errors from hikari.models import presences +from hikari.net import config from hikari.net import gateway -from hikari.net import http_client +from tests.hikari import client_session_stub from tests.hikari import hikari_test_helpers @pytest.fixture() -def client(): - return gateway.Gateway(url="wss://gateway.discord.gg", token="lol", app=mock.MagicMock(), config=mock.MagicMock()) +def http_settings(): + return mock.create_autospec(config.HTTPSettings) + + +@pytest.fixture() +def proxy_settings(): + return mock.create_autospec(config.ProxySettings) + + +@pytest.fixture() +def client_session(): + stub = client_session_stub.ClientSessionStub() + with mock.patch.object(aiohttp, "ClientSession", new=stub): + yield stub + + +@pytest.fixture() +def client(http_settings, proxy_settings): + return gateway.Gateway( + url="wss://gateway.discord.gg", + token="lol", + app=mock.MagicMock(), + http_settings=http_settings, + proxy_settings=proxy_settings, + ) class TestInit: @@ -46,11 +70,12 @@ class TestInit: (7, True, "v=7&encoding=json&compress=zlib-stream"), ], ) - def test_url_is_correct_json(self, v, use_compression, expect): + def test_url_is_correct_json(self, v, use_compression, expect, http_settings, proxy_settings): g = gateway.Gateway( app=mock.MagicMock(), - config=mock.MagicMock(), token=mock.MagicMock(), + http_settings=http_settings, + proxy_settings=proxy_settings, url="wss://gaytewhuy.discord.meh", version=v, use_etf=False, @@ -60,11 +85,12 @@ def test_url_is_correct_json(self, v, use_compression, expect): assert g.url == f"wss://gaytewhuy.discord.meh?{expect}" @pytest.mark.parametrize(["v", "use_compression"], [(6, False), (6, True), (7, False), (7, True),]) - def test_using_etf_is_unsupported(self, v, use_compression): + def test_using_etf_is_unsupported(self, v, use_compression, http_settings, proxy_settings): with pytest.raises(NotImplementedError): gateway.Gateway( app=mock.MagicMock(), - config=mock.MagicMock(), + http_settings=http_settings, + proxy_settings=proxy_settings, token=mock.MagicMock(), url="wss://erlpack-is-broken-lol.discord.meh", version=v, @@ -74,9 +100,15 @@ def test_using_etf_is_unsupported(self, v, use_compression): class TestAppProperty: - def test_returns_app(self): + def test_returns_app(self, http_settings, proxy_settings): app = mock.MagicMock() - g = gateway.Gateway(url="wss://gateway.discord.gg", token="lol", app=app, config=mock.MagicMock()) + g = gateway.Gateway( + url="wss://gateway.discord.gg", + token="lol", + app=app, + http_settings=http_settings, + proxy_settings=proxy_settings, + ) assert g.app is app @@ -94,12 +126,13 @@ def test_not_is_alive(self, client): class TestStart: @pytest.mark.parametrize("shard_id", [0, 1, 2]) @hikari_test_helpers.timeout() - async def test_starts_task(self, event_loop, shard_id): + async def test_starts_task(self, event_loop, shard_id, http_settings=http_settings, proxy_settings=proxy_settings): g = gateway.Gateway( url="wss://gateway.discord.gg", token="lol", app=mock.MagicMock(), - config=mock.MagicMock(), + http_settings=http_settings, + proxy_settings=proxy_settings, shard_id=shard_id, shard_count=100, ) @@ -134,7 +167,13 @@ class GatewayStub(gateway.Gateway): def is_alive(self): return getattr(self, "_is_alive", False) - return GatewayStub(url="wss://gateway.discord.gg", token="lol", app=mock.MagicMock(), config=mock.MagicMock(),) + return GatewayStub( + url="wss://gateway.discord.gg", + token="lol", + app=mock.MagicMock(), + http_settings=http_settings, + proxy_settings=proxy_settings, + ) async def test_when_already_closed_does_nothing(self, client): client._request_close_event = mock.MagicMock(asyncio.Event) @@ -216,31 +255,19 @@ async def test_sets_handshake_event_on_finish(self, client): client._handshake_event.set.assert_called_once_with() - @hikari_test_helpers.timeout() - async def test_closes_super_on_finish(self, client): - client._request_close_event = mock.MagicMock(asyncio.Event) - client._handshake_event = mock.MagicMock(asyncio.Event) - client._request_close_event.is_set = mock.MagicMock(return_value=True) - client._run_once = mock.AsyncMock() - - with mock.patch.object(http_client.HTTPClient, "close") as close_mock: - with pytest.raises(errors.GatewayClientClosedError): - await client._run() - - close_mock.assert_awaited_once_with(client) - @pytest.mark.asyncio class TestRunOnceShielded: @pytest.fixture - def client(self): + def client(self, http_settings=http_settings, proxy_settings=proxy_settings): client = hikari_test_helpers.unslot_class(gateway.Gateway)( url="wss://gateway.discord.gg", token="lol", app=mock.MagicMock(), - config=mock.MagicMock(), shard_id=3, shard_count=17, + http_settings=http_settings, + proxy_settings=proxy_settings, ) client = hikari_test_helpers.mock_methods_on( client, @@ -266,20 +293,22 @@ def client(self): return client @hikari_test_helpers.timeout() - async def test_invokes_run_once_shielded(self, client): - await client._run_once_shielded() - client._run_once.assert_awaited_once_with() + async def test_invokes_run_once_shielded(self, client, client_session): + await client._run_once_shielded(client_session) + client._run_once.assert_awaited_once_with(client_session) @hikari_test_helpers.timeout() - async def test_happy_path_returns_False(self, client): - assert await client._run_once_shielded() is False + async def test_happy_path_returns_False(self, client, client_session): + assert await client._run_once_shielded(client_session) is False @pytest.mark.parametrize( ["zombied", "request_close", "expect_backoff_called"], [(True, True, True), (True, False, True), (False, True, False), (False, False, False),], ) @hikari_test_helpers.timeout() - async def test_socket_closed_resets_backoff(self, client, zombied, request_close, expect_backoff_called): + async def test_socket_closed_resets_backoff( + self, client, zombied, request_close, expect_backoff_called, client_session + ): client._request_close_event.is_set = mock.MagicMock(return_value=request_close) def run_once(): @@ -287,7 +316,7 @@ def run_once(): raise gateway.Gateway._SocketClosed() client._run_once = mock.AsyncMock(wraps=run_once) - await client._run_once_shielded() + await client._run_once_shielded(client_session) if expect_backoff_called: client._backoff.reset.assert_called_once_with() @@ -295,98 +324,98 @@ def run_once(): client._backoff.reset.assert_not_called() @hikari_test_helpers.timeout() - async def test_invalid_session_resume_does_not_clear_seq_or_session_id(self, client): + async def test_invalid_session_resume_does_not_clear_seq_or_session_id(self, client, client_session): client._run_once = mock.AsyncMock(side_effect=gateway.Gateway._InvalidSession(True)) client._seq = 1234 client.session_id = "69420" - await client._run_once_shielded() + await client._run_once_shielded(client_session) assert client._seq == 1234 assert client.session_id == "69420" @pytest.mark.parametrize("request_close", [True, False]) @hikari_test_helpers.timeout() - async def test_socket_closed_is_restartable_if_no_closure_request(self, client, request_close): + async def test_socket_closed_is_restartable_if_no_closure_request(self, client, request_close, client_session): client._request_close_event.is_set = mock.MagicMock(return_value=request_close) client._run_once = mock.AsyncMock(side_effect=gateway.Gateway._SocketClosed()) - assert await client._run_once_shielded() is not request_close + assert await client._run_once_shielded(client_session) is not request_close @hikari_test_helpers.timeout() - async def test_ClientConnectionError_is_restartable(self, client): + async def test_ClientConnectionError_is_restartable(self, client, client_session): key = aiohttp.client_reqrep.ConnectionKey( host="localhost", port=6996, is_ssl=False, ssl=None, proxy=None, proxy_auth=None, proxy_headers_hash=69420, ) error = aiohttp.ClientConnectorError(key, OSError()) client._run_once = mock.AsyncMock(side_effect=error) - assert await client._run_once_shielded() is True + assert await client._run_once_shielded(client_session) is True @hikari_test_helpers.timeout() - async def test_invalid_session_is_restartable(self, client): + async def test_invalid_session_is_restartable(self, client, client_session): client._run_once = mock.AsyncMock(side_effect=gateway.Gateway._InvalidSession()) - assert await client._run_once_shielded() is True + assert await client._run_once_shielded(client_session) is True @hikari_test_helpers.timeout() - async def test_invalid_session_resume_does_not_invalidate_session(self, client): + async def test_invalid_session_resume_does_not_invalidate_session(self, client, client_session): client._run_once = mock.AsyncMock(side_effect=gateway.Gateway._InvalidSession(True)) - await client._run_once_shielded() + await client._run_once_shielded(client_session) client._close_ws.assert_awaited_once_with( gateway.Gateway._GatewayCloseCode.DO_NOT_INVALIDATE_SESSION, "invalid session (resume)" ) @hikari_test_helpers.timeout() - async def test_invalid_session_no_resume_invalidates_session(self, client): + async def test_invalid_session_no_resume_invalidates_session(self, client, client_session): client._run_once = mock.AsyncMock(side_effect=gateway.Gateway._InvalidSession(False)) - await client._run_once_shielded() + await client._run_once_shielded(client_session) client._close_ws.assert_awaited_once_with( gateway.Gateway._GatewayCloseCode.RFC_6455_NORMAL_CLOSURE, "invalid session (no resume)" ) @hikari_test_helpers.timeout() - async def test_invalid_session_no_resume_clears_seq_and_session_id(self, client): + async def test_invalid_session_no_resume_clears_seq_and_session_id(self, client, client_session): client._run_once = mock.AsyncMock(side_effect=gateway.Gateway._InvalidSession(False)) client._seq = 1234 client.session_id = "69420" - await client._run_once_shielded() + await client._run_once_shielded(client_session) assert client._seq is None assert client.session_id is None @hikari_test_helpers.timeout() - async def test_reconnect_is_restartable(self, client): + async def test_reconnect_is_restartable(self, client, client_session): client._run_once = mock.AsyncMock(side_effect=gateway.Gateway._Reconnect()) - assert await client._run_once_shielded() is True + assert await client._run_once_shielded(client_session) is True @hikari_test_helpers.timeout() - async def test_server_connection_error_resumes_if_reconnectable(self, client): + async def test_server_connection_error_resumes_if_reconnectable(self, client, client_session): client._run_once = mock.AsyncMock(side_effect=errors.GatewayServerClosedConnectionError("blah", None, True)) client._seq = 1234 client.session_id = "69420" - assert await client._run_once_shielded() is True + assert await client._run_once_shielded(client_session) is True assert client._seq == 1234 assert client.session_id == "69420" - async def test_server_connection_error_closes_websocket_if_reconnectable(self, client): + async def test_server_connection_error_closes_websocket_if_reconnectable(self, client, client_session): client._run_once = mock.AsyncMock(side_effect=errors.GatewayServerClosedConnectionError("blah", None, True)) - assert await client._run_once_shielded() is True + assert await client._run_once_shielded(client_session) is True client._close_ws.assert_awaited_once_with( gateway.Gateway._GatewayCloseCode.RFC_6455_NORMAL_CLOSURE, "you hung up on me" ) @hikari_test_helpers.timeout() - async def test_server_connection_error_does_not_reconnect_if_not_reconnectable(self, client): + async def test_server_connection_error_does_not_reconnect_if_not_reconnectable(self, client, client_session): client._run_once = mock.AsyncMock(side_effect=errors.GatewayServerClosedConnectionError("blah", None, False)) client._seq = 1234 client.session_id = "69420" with pytest.raises(errors.GatewayServerClosedConnectionError): - await client._run_once_shielded() + await client._run_once_shielded(client_session) client._request_close_event.set.assert_called_once_with() assert client._seq is None assert client.session_id is None client._backoff.reset.assert_called_once_with() - async def test_server_connection_error_closes_websocket_if_not_reconnectable(self, client): + async def test_server_connection_error_closes_websocket_if_not_reconnectable(self, client, client_session): client._run_once = mock.AsyncMock(side_effect=errors.GatewayServerClosedConnectionError("blah", None, False)) with pytest.raises(errors.GatewayServerClosedConnectionError): - await client._run_once_shielded() + await client._run_once_shielded(client_session) client._close_ws.assert_awaited_once_with( gateway.Gateway._GatewayCloseCode.RFC_6455_UNEXPECTED_CONDITION, "you broke the connection" ) @@ -396,26 +425,28 @@ async def test_server_connection_error_closes_websocket_if_not_reconnectable(sel [(True, True, True), (True, False, True), (False, True, False), (False, False, False)], ) @hikari_test_helpers.timeout() - async def test_socket_closed_resets_backoff(self, client, zombied, request_close, expect_backoff_called): + async def test_socket_closed_resets_backoff( + self, client, zombied, request_close, expect_backoff_called, client_session + ): client._request_close_event.is_set = mock.MagicMock(return_value=request_close) - def poll_events(): + def run_once(_): client._zombied = zombied raise gateway.Gateway._SocketClosed() - client._run_once = mock.AsyncMock(wraps=poll_events) - await client._run_once_shielded() + client._run_once = mock.AsyncMock(wraps=run_once) + await client._run_once_shielded(client_session) if expect_backoff_called: client._backoff.reset.assert_called_once_with() else: client._backoff.reset.assert_not_called() - async def test_other_exception_closes_websocket(self, client): + async def test_other_exception_closes_websocket(self, client, client_session): client._run_once = mock.AsyncMock(side_effect=RuntimeError()) with pytest.raises(RuntimeError): - await client._run_once_shielded() + await client._run_once_shielded(client_session) client._close_ws.assert_awaited_once_with( gateway.Gateway._GatewayCloseCode.RFC_6455_UNEXPECTED_CONDITION, "unexpected error occurred" @@ -425,14 +456,15 @@ async def test_other_exception_closes_websocket(self, client): @pytest.mark.asyncio class TestRunOnce: @pytest.fixture - def client(self): + def client(self, http_settings=http_settings, proxy_settings=proxy_settings): client = hikari_test_helpers.unslot_class(gateway.Gateway)( url="wss://gateway.discord.gg", token="lol", app=mock.MagicMock(), - config=mock.MagicMock(), shard_id=3, shard_count=17, + http_settings=http_settings, + proxy_settings=proxy_settings, ) client = hikari_test_helpers.mock_methods_on( client, @@ -456,21 +488,21 @@ def client(self): return client @hikari_test_helpers.timeout() - async def test_resets_close_event(self, client): - await client._run_once() + async def test_resets_close_event(self, client, client_session): + await client._run_once(client_session) client._request_close_event.clear.assert_called_with() @hikari_test_helpers.timeout() - async def test_resets_zombie_status(self, client): + async def test_resets_zombie_status(self, client, client_session): client._zombied = True - await client._run_once() + await client._run_once(client_session) assert client._zombied is False @hikari_test_helpers.timeout() - async def test_backoff_and_waits_if_restarted_too_quickly(self, client): + async def test_backoff_and_waits_if_restarted_too_quickly(self, client, client_session): client._now = mock.MagicMock(return_value=60) client._RESTART_RATELIMIT_WINDOW = 30 client._last_run_started_at = 40 @@ -481,21 +513,21 @@ async def test_backoff_and_waits_if_restarted_too_quickly(self, client): create_task = stack.enter_context(mock.patch.object(asyncio, "create_task")) with stack: - await client._run_once() + await client._run_once(client_session) client._backoff.__next__.assert_called_once_with() create_task.assert_any_call(client._request_close_event.wait(), name="gateway shard 3 backing off") wait_for.assert_called_once_with(create_task(), timeout=24.37) @hikari_test_helpers.timeout() - async def test_closing_bot_during_backoff_immediately_interrupts_it(self, client): + async def test_closing_bot_during_backoff_immediately_interrupts_it(self, client, client_session): client._now = mock.MagicMock(return_value=60) client._RESTART_RATELIMIT_WINDOW = 30 client._last_run_started_at = 40 client._backoff.__next__ = mock.MagicMock(return_value=24.37) client._request_close_event = asyncio.Event() - task = asyncio.create_task(client._run_once()) + task = asyncio.create_task(client._run_once(client_session)) try: # Let the backoff spin up and start waiting in the background. @@ -516,7 +548,7 @@ async def test_closing_bot_during_backoff_immediately_interrupts_it(self, client task.cancel() @hikari_test_helpers.timeout() - async def test_backoff_does_not_trigger_if_not_restarting_in_small_window(self, client): + async def test_backoff_does_not_trigger_if_not_restarting_in_small_window(self, client, client_session): client._now = mock.MagicMock(return_value=60) client._last_run_started_at = 40 client._backoff.__next__ = mock.MagicMock( @@ -531,57 +563,79 @@ async def test_backoff_does_not_trigger_if_not_restarting_in_small_window(self, with stack: # This will raise an assertion error if the backoff is incremented. - await client._run_once() + await client._run_once(client_session) @hikari_test_helpers.timeout() - async def test_last_run_started_at_set_to_current_time(self, client): + async def test_last_run_started_at_set_to_current_time(self, client, client_session): # Windows does some batshit crazy stuff in perf_counter, like only # returning process time elapsed rather than monotonic time since # startup, so I guess I will put this random value here to show the # code doesn't really care what this value is contextually. client._last_run_started_at = -100_000 - await client._run_once() + await client._run_once(client_session) assert client._last_run_started_at == 1.0 @hikari_test_helpers.timeout() - async def test_ws_gets_created(self, client): - await client._run_once() - client._create_ws.assert_awaited_once_with(client.url) + async def test_ws_gets_created(self, client, client_session): + proxy_settings = config.ProxySettings( + url="http://my-proxy.net", + headers={"foo": "bar"}, + trust_env=True, + auth=config.BasicAuthHeader(username="banana", password="fan fo"), + ) + http_settings = config.HTTPSettings(verify_ssl=False) + client._http_settings = http_settings + client._proxy_settings = proxy_settings + + await client._run_once(client_session) + client_session.ws_connect.assert_called_once_with( + url=client.url, + autoping=True, + autoclose=True, + proxy=proxy_settings.url, + proxy_headers=proxy_settings.all_headers, + verify_ssl=http_settings.verify_ssl, + # Discord can send massive messages that lead us to being disconnected + # without this. It is a bit shit that there is no guarantee of the size + # of these messages, but there isn't much we can do about this one. + max_msg_size=0, + ) + client_session.ws_connect_stub.assert_awaited_once() @hikari_test_helpers.timeout() - async def test_zlib_decompressobj_set(self, client): + async def test_zlib_decompressobj_set(self, client, client_session): assert client._zlib is None - await client._run_once() + await client._run_once(client_session) assert client._zlib is not None @hikari_test_helpers.timeout() - async def test_handshake_event_cleared(self, client): + async def test_handshake_event_cleared(self, client, client_session): client._handshake_event = asyncio.Event() client._handshake_event.set() - await client._run_once() + await client._run_once(client_session) assert not client._handshake_event.is_set() @hikari_test_helpers.timeout() - async def test_handshake_invoked(self, client): - await client._run_once() + async def test_handshake_invoked(self, client, client_session): + await client._run_once(client_session) client._handshake.assert_awaited_once_with() @hikari_test_helpers.timeout() - async def test_connected_event_dispatched_before_polling_events(self, client): + async def test_connected_event_dispatched_before_polling_events(self, client, client_session): class Error(Exception): pass client._poll_events = mock.AsyncMock(side_effect=Error) with pytest.raises(Error): - await client._run_once() + await client._run_once(client_session) client._dispatch.assert_any_call("CONNECTED", {}) @hikari_test_helpers.timeout() - async def test_heartbeat_is_not_started_before_handshake_completes(self, client): + async def test_heartbeat_is_not_started_before_handshake_completes(self, client, client_session): class Error(Exception): pass @@ -591,28 +645,28 @@ class Error(Exception): with mock.patch.object(asyncio, "create_task") as create_task: with pytest.raises(Error): - await client._run_once() + await client._run_once(client_session) call = mock.call(client._heartbeat_keepalive(), name=mock.ANY) assert call not in create_task.call_args_list @hikari_test_helpers.timeout() - async def test_heartbeat_is_started(self, client): + async def test_heartbeat_is_started(self, client, client_session): client._heartbeat_keepalive = mock.MagicMock() with mock.patch.object(asyncio, "create_task") as create_task: - await client._run_once() + await client._run_once(client_session) call = mock.call(client._heartbeat_keepalive(), name="gateway shard 3 heartbeat") assert call in create_task.call_args_list @hikari_test_helpers.timeout() - async def test_poll_events_invoked(self, client): - await client._run_once() + async def test_poll_events_invoked(self, client, client_session): + await client._run_once(client_session) client._poll_events.assert_awaited_once_with() @hikari_test_helpers.timeout() - async def test_heartbeat_is_stopped_when_poll_events_stops(self, client): + async def test_heartbeat_is_stopped_when_poll_events_stops(self, client, client_session): client._heartbeat_keepalive = mock.MagicMock() client._poll_events = mock.AsyncMock(side_effect=Exception) @@ -620,35 +674,36 @@ async def test_heartbeat_is_stopped_when_poll_events_stops(self, client): with mock.patch.object(asyncio, "create_task", return_value=task): with pytest.raises(Exception): - await client._run_once() + await client._run_once(client_session) task.cancel.assert_called_once_with() - async def test_dispatches_disconnect_if_connected(self, client): - await client._run_once() + async def test_dispatches_disconnect_if_connected(self, client, client_session): + await client._run_once(client_session) client._dispatch.assert_any_call("CONNECTED", {}) client._dispatch.assert_any_call("DISCONNECTED", {}) - async def test_no_dispatch_disconnect_if_not_connected(self, client): - client._create_ws = mock.MagicMock(side_effect=RuntimeError) + async def test_no_dispatch_disconnect_if_not_connected(self, client, client_session): + client_session.ws_connect = mock.MagicMock(side_effect=RuntimeError) with pytest.raises(RuntimeError): - await client._run_once() + await client._run_once(client_session) client._dispatch.assert_not_called() - async def test_connected_at_reset_to_nan_on_exit(self, client): - await client._run_once() + async def test_connected_at_reset_to_nan_on_exit(self, client, client_session): + await client._run_once(client_session) assert math.isnan(client.connected_at) @pytest.mark.asyncio class TestUpdatePresence: @pytest.fixture - def client(self): + def client(self, proxy_settings, http_settings): client = hikari_test_helpers.unslot_class(gateway.Gateway)( url="wss://gateway.discord.gg", token="lol", app=mock.MagicMock(), - config=mock.MagicMock(), + http_settings=http_settings, + proxy_settings=proxy_settings, shard_id=3, shard_count=17, ) diff --git a/tests/hikari/net/test_http_client.py b/tests/hikari/net/test_http_client.py deleted file mode 100644 index 8281edf952..0000000000 --- a/tests/hikari/net/test_http_client.py +++ /dev/null @@ -1,208 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright © Nekoka.tt 2019-2020 -# -# This file is part of Hikari. -# -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . -import json -import weakref - -import aiohttp -import mock -import pytest - -from hikari.net import http_client -from hikari.net import http_settings -from tests.hikari import hikari_test_helpers - - -@pytest.fixture -def client_session(): - client_session = mock.create_autospec(aiohttp.ClientSession, spec_set=True) - with mock.patch.object(aiohttp, "ClientSession", return_value=client_session): - yield client_session - - -@pytest.fixture -def client(client_session): - assert client_session, "this param is needed, it ensures aiohttp is patched for the test" - client = hikari_test_helpers.unslot_class(http_client.HTTPClient)() - yield client - - -class TestFinalizer: - def test_when_existing_client_session(self, client): - client._client_session = mock.MagicMock() - client._client_session_ref = weakref.proxy(client._client_session) - client.__del__() - assert client._client_session is None - assert client._client_session_ref is None - - def test_when_no_client_session(self, client): - client._client_session = None - client._client_session_ref = None - client.__del__() - assert client._client_session is None - assert client._client_session_ref is None - - -@pytest.mark.asyncio -class TestAcquireClientSession: - @pytest.mark.parametrize("connector_owner", [True, False]) - async def test_acquire_creates_new_session_if_one_does_not_exist(self, client, connector_owner): - client._config = http_settings.HTTPSettings() - client._config.tcp_connector = mock.MagicMock() - client._config.trust_env = mock.MagicMock() - client._config.connector_owner = connector_owner - - client._client_session = None - cs = client.get_client_session() - - assert client._client_session == cs - assert cs in weakref.getweakrefs(client._client_session), "did not return correct weakref" - - aiohttp.ClientSession.assert_called_once_with( - connector=client._config.tcp_connector, - trust_env=client._config.trust_env, - version=aiohttp.HttpVersion11, - json_serialize=json.dumps, - connector_owner=connector_owner, - ) - - async def test_acquire_repeated_calls_caches_client_session(self, client): - cs = client.get_client_session() - - for i in range(10): - aiohttp.ClientSession.reset_mock() - assert cs is client.get_client_session() - aiohttp.ClientSession.assert_not_called() - - -@pytest.mark.asyncio -class TestClose: - async def test_close_when_not_running(self, client, client_session): - client._client_session = None - await client.close() - assert client._client_session is None - - async def test_close_when_running(self, client, client_session): - client._client_session = client_session - await client.close() - assert client._client_session is None - client_session.close.assert_awaited_once_with() - - -@pytest.mark.asyncio -class TestPerformRequest: - async def test_perform_request_form_data(self, client, client_session): - client._config = http_settings.HTTPSettings() - client._config.allow_redirects = mock.MagicMock() - client._config.proxy_url = mock.MagicMock() - client._config.proxy_auth = mock.MagicMock() - client._config.proxy_headers = mock.MagicMock() - client._config.verify_ssl = mock.MagicMock() - client._config.ssl_context = mock.MagicMock() - client._config.request_timeout = mock.MagicMock() - - form_data = aiohttp.FormData() - - expected_response = mock.MagicMock() - client_session.request = mock.AsyncMock(return_value=expected_response) - - actual_response = await client._perform_request( - method="POST", url="http://foo.bar", headers={"X-Foo-Count": "122"}, body=form_data, query={"foo": "bar"}, - ) - - assert expected_response is actual_response - client_session.request.assert_awaited_once_with( - method="POST", - url="http://foo.bar", - params={"foo": "bar"}, - headers={"X-Foo-Count": "122"}, - data=form_data, - allow_redirects=client._config.allow_redirects, - proxy=client._config.proxy_url, - proxy_auth=client._config.proxy_auth, - proxy_headers=client._config.proxy_headers, - verify_ssl=client._config.verify_ssl, - ssl_context=client._config.ssl_context, - timeout=client._config.request_timeout, - ) - - async def test_perform_request_json(self, client, client_session): - client._config = http_settings.HTTPSettings() - client._config.allow_redirects = mock.MagicMock() - client._config.proxy_url = mock.MagicMock() - client._config.proxy_auth = mock.MagicMock() - client._config.proxy_headers = mock.MagicMock() - client._config.verify_ssl = mock.MagicMock() - client._config.ssl_context = mock.MagicMock() - client._config.request_timeout = mock.MagicMock() - - req = {"hello": "world"} - - expected_response = mock.MagicMock() - client_session.request = mock.AsyncMock(return_value=expected_response) - - actual_response = await client._perform_request( - method="POST", url="http://foo.bar", headers={"X-Foo-Count": "122"}, body=req, query={"foo": "bar"}, - ) - - assert expected_response is actual_response - client_session.request.assert_awaited_once_with( - method="POST", - url="http://foo.bar", - params={"foo": "bar"}, - headers={"X-Foo-Count": "122"}, - json=req, - allow_redirects=client._config.allow_redirects, - proxy=client._config.proxy_url, - proxy_auth=client._config.proxy_auth, - proxy_headers=client._config.proxy_headers, - verify_ssl=client._config.verify_ssl, - ssl_context=client._config.ssl_context, - timeout=client._config.request_timeout, - ) - - -@pytest.mark.asyncio -class TestCreateWs: - async def test_create_ws(self, client, client_session): - client._config = http_settings.HTTPSettings() - client._config.allow_redirects = mock.MagicMock() - client._config.proxy_url = mock.MagicMock() - client._config.proxy_auth = mock.MagicMock() - client._config.proxy_headers = mock.MagicMock() - client._config.verify_ssl = mock.MagicMock() - client._config.ssl_context = mock.MagicMock() - client._config.request_timeout = mock.MagicMock() - - expected_ws = mock.MagicMock() - client_session.ws_connect = mock.AsyncMock(return_value=expected_ws) - - actual_ws = await client._create_ws("foo://bar", compress=5, auto_ping=True, max_msg_size=3) - - assert expected_ws is actual_ws - - client_session.ws_connect.assert_awaited_once_with( - url="foo://bar", - compress=5, - autoping=True, - max_msg_size=3, - proxy=client._config.proxy_url, - proxy_auth=client._config.proxy_auth, - proxy_headers=client._config.proxy_headers, - verify_ssl=client._config.verify_ssl, - ssl_context=client._config.ssl_context, - ) From 6c399337d2bcc3f6a214ba007cb686f7882b2572 Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Sun, 28 Jun 2020 11:37:57 +0100 Subject: [PATCH 588/922] Added !. to spel operations. --- hikari/utilities/spel.py | 24 ++++++++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/hikari/utilities/spel.py b/hikari/utilities/spel.py index b4311fbb77..61a7fef932 100644 --- a/hikari/utilities/spel.py +++ b/hikari/utilities/spel.py @@ -34,6 +34,9 @@ applying `author.username.islower()` to a `hikari.models.messages.Message` object. +All expressions may start with a `.`. You can negate the whole expression +by instead starting them with `!.`. + You may also want to negate a condition. To do this, prepend `!` to the attribute name. For example, to check if a message was not made by a bot, you could run `author.!is_bot` on a `Message` object. Likewise, to check if @@ -53,12 +56,22 @@ class AttrGetter(typing.Generic[InputValueT, ReturnValueT]): - """An attribute getter that can resolve nested attributes and methods.""" + """An attribute getter that can resolve nested attributes and methods. + + This follows the SPEL definition for how to define expressions. Expressions + may be preceeded with an optional `.` to aid in readability. + """ - __slots__: typing.Sequence[str] = ("pipeline",) + __slots__: typing.Sequence[str] = ("pipeline", "invert_all") def __init__(self, attr_name: str) -> None: - if attr_name.startswith("."): + self.invert_all = False + + if attr_name.startswith("!."): + attr_name = attr_name[2:] + self.invert_all = True + + elif attr_name.startswith("."): attr_name = attr_name[1:] self.pipeline: typing.List[typing.Callable[[typing.Any], typing.Any]] = [] @@ -90,4 +103,7 @@ def __call__(self, item: InputValueT) -> ReturnValueT: for op in self.pipeline: result = op(result) - return typing.cast(ReturnValueT, result) + if self.invert_all: + return typing.cast(ReturnValueT, not result) + else: + return typing.cast(ReturnValueT, result) From ecd654f6f7d13caf3e717855649426dffab317da Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Sun, 28 Jun 2020 11:50:42 +0100 Subject: [PATCH 589/922] Documented iterators fully. --- hikari/net/config.py | 4 +- hikari/utilities/iterators.py | 138 +++++++++++++++++++++++++++------- 2 files changed, 112 insertions(+), 30 deletions(-) diff --git a/hikari/net/config.py b/hikari/net/config.py index fe39475de0..8d0c5c48e0 100644 --- a/hikari/net/config.py +++ b/hikari/net/config.py @@ -116,8 +116,8 @@ class HTTPSettings: """ max_redirects: int = 10 - """The maximum number of redirects to allow. - + """The maximum number of redirects to allow. + If `allow_redirects` is `False`, then this is ignored. """ diff --git a/hikari/utilities/iterators.py b/hikari/utilities/iterators.py index 38da54aeed..b39e1d08af 100644 --- a/hikari/utilities/iterators.py +++ b/hikari/utilities/iterators.py @@ -28,13 +28,6 @@ "All", "AttrComparator", "BufferedLazyIterator", - "DropCountLazyIterator", - "DropWhileLazyIterator", - "EnumeratedLazyIterator", - "FilteredLazyIterator", - "LimitedLazyIterator", - "MappingLazyIterator", - "TakeWhileLazyIterator", ] import abc @@ -212,7 +205,7 @@ def map( if isinstance(transformation, str): transformation = typing.cast("spel.AttrGetter[ValueT, AnotherValueT]", spel.AttrGetter(transformation)) - return MappingLazyIterator(self, transformation) + return _MappingLazyIterator(self, transformation) def filter( self, @@ -241,7 +234,7 @@ def filter( matched. """ conditions = self._map_predicates_and_attr_getters("filter", *predicates, **attrs) - return FilteredLazyIterator(self, conditions) + return _FilteredLazyIterator(self, conditions) def take_while( self, @@ -258,7 +251,8 @@ def take_while( may instead include 2-`tuple` objects consisting of a `str` attribute name (nested attributes are referred to using the `.` operator), and values to compare for equality. This allows you - to specify conditions such as `members.take_while(("user.bot", True))`. + to specify conditions such as + `members.take_while(("user.bot", True))`. **attrs : typing.Any Alternative to passing 2-tuples. Cannot specify nested attributes using this method. @@ -270,16 +264,37 @@ def take_while( matched. """ conditions = self._map_predicates_and_attr_getters("take_while", *predicates, **attrs) - return TakeWhileLazyIterator(self, conditions) + return _TakeWhileLazyIterator(self, conditions) def take_until( self, *predicates: typing.Union[typing.Tuple[str, typing.Any], typing.Callable[[ValueT], bool]], **attrs: typing.Any, ) -> LazyIterator[ValueT]: - """Returns the inverse of `take_while`.""" + """Return each item until any conditions pass or the end is reached. + + Parameters + ---------- + *predicates : typing.Callable[[ValueT], bool] or typing.Tuple[str, typing.Any] + Predicates to invoke. These are functions that take a value and + return `True` if it is of interest, or `False` otherwise. These + may instead include 2-`tuple` objects consisting of a `str` + attribute name (nested attributes are referred to using the `.` + operator), and values to compare for equality. This allows you + to specify conditions such as + `members.take_until(("user.bot", True))`. + **attrs : typing.Any + Alternative to passing 2-tuples. Cannot specify nested attributes + using this method. + + Returns + ------- + LazyIterator[ValueT] + LazyIterator that only emits values until any conditions are + matched. + """ conditions = self._map_predicates_and_attr_getters("take_until", *predicates, **attrs) - return TakeWhileLazyIterator(self, ~conditions) + return _TakeWhileLazyIterator(self, ~conditions) def skip_while( self, @@ -298,7 +313,8 @@ def skip_while( may instead include 2-`tuple` objects consisting of a `str` attribute name (nested attributes are referred to using the `.` operator), and values to compare for equality. This allows you - to specify conditions such as `members.take_while(("user.bot", True))`. + to specify conditions such as + `members.skip_while(("user.bot", True))`. **attrs : typing.Any Alternative to passing 2-tuples. Cannot specify nested attributes using this method. @@ -310,16 +326,39 @@ def skip_while( All items before this are discarded. """ conditions = self._map_predicates_and_attr_getters("drop_while", *predicates, **attrs) - return DropWhileLazyIterator(self, conditions) + return _DropWhileLazyIterator(self, conditions) def skip_until( self, *predicates: typing.Union[typing.Tuple[str, typing.Any], typing.Callable[[ValueT], bool]], **attrs: typing.Any, ) -> LazyIterator[ValueT]: - """Returns the inverse of `drop_while`.""" + """Discard items while all conditions are False. + + Items after this will be yielded as normal. + + Parameters + ---------- + *predicates : typing.Callable[[ValueT], bool] or typing.Tuple[str, typing.Any] + Predicates to invoke. These are functions that take a value and + return `True` if it is of interest, or `False` otherwise. These + may instead include 2-`tuple` objects consisting of a `str` + attribute name (nested attributes are referred to using the `.` + operator), and values to compare for equality. This allows you + to specify conditions such as + `members.skip_until(("user.bot", True))`. + **attrs : typing.Any + Alternative to passing 2-tuples. Cannot specify nested attributes + using this method. + + Returns + ------- + LazyIterator[ValueT] + LazyIterator that only emits values once a condition has failed. + All items before this are discarded. + """ conditions = self._map_predicates_and_attr_getters("drop_while", *predicates, **attrs) - return DropWhileLazyIterator(self, ~conditions) + return _DropWhileLazyIterator(self, ~conditions) def enumerate(self, *, start: int = 0) -> LazyIterator[typing.Tuple[int, ValueT]]: """Enumerate the paginated results lazily. @@ -363,7 +402,7 @@ def enumerate(self, *, start: int = 0) -> LazyIterator[typing.Tuple[int, ValueT] A paginated results view that asynchronously yields an increasing counter in a tuple with each result, lazily. """ - return EnumeratedLazyIterator(self, start=start) + return _EnumeratedLazyIterator(self, start=start) def limit(self, limit: int) -> LazyIterator[ValueT]: """Limit the number of items you receive from this async iterator. @@ -384,7 +423,7 @@ def limit(self, limit: int) -> LazyIterator[ValueT]: A paginated results view that asynchronously yields a maximum of the given number of items before completing. """ - return LimitedLazyIterator(self, limit) + return _LimitedLazyIterator(self, limit) def skip(self, number: int) -> LazyIterator[ValueT]: """Drop the given number of items, then yield anything after. @@ -400,7 +439,7 @@ def skip(self, number: int) -> LazyIterator[ValueT]: A paginated results view that asynchronously yields all items AFTER the given number of items are discarded first. """ - return DropCountLazyIterator(self, number) + return _DropCountLazyIterator(self, number) async def first(self) -> ValueT: """Return the first element of this iterator only.""" @@ -452,7 +491,7 @@ async def __anext__(self) -> ValueT: ... -class EnumeratedLazyIterator(typing.Generic[ValueT], LazyIterator[typing.Tuple[int, ValueT]]): +class _EnumeratedLazyIterator(typing.Generic[ValueT], LazyIterator[typing.Tuple[int, ValueT]]): __slots__: typing.Sequence[str] = ("_i", "_paginator") def __init__(self, paginator: LazyIterator[ValueT], *, start: int) -> None: @@ -465,7 +504,7 @@ async def __anext__(self) -> typing.Tuple[int, ValueT]: return pair -class LimitedLazyIterator(typing.Generic[ValueT], LazyIterator[ValueT]): +class _LimitedLazyIterator(typing.Generic[ValueT], LazyIterator[ValueT]): __slots__: typing.Sequence[str] = ("_paginator", "_count", "_limit") def __init__(self, paginator: LazyIterator[ValueT], limit: int) -> None: @@ -484,7 +523,7 @@ async def __anext__(self) -> ValueT: return next_item -class DropCountLazyIterator(typing.Generic[ValueT], LazyIterator[ValueT]): +class _DropCountLazyIterator(typing.Generic[ValueT], LazyIterator[ValueT]): __slots__: typing.Sequence[str] = ("_paginator", "_count", "_number") def __init__(self, paginator: LazyIterator[ValueT], number: int) -> None: @@ -503,7 +542,7 @@ async def __anext__(self) -> ValueT: return next_item -class FilteredLazyIterator(typing.Generic[ValueT], LazyIterator[ValueT]): +class _FilteredLazyIterator(typing.Generic[ValueT], LazyIterator[ValueT]): __slots__: typing.Sequence[str] = ("_paginator", "_predicate") def __init__(self, paginator: LazyIterator[ValueT], predicate: typing.Callable[[ValueT], bool]) -> None: @@ -517,7 +556,7 @@ async def __anext__(self) -> ValueT: raise StopAsyncIteration -class MappingLazyIterator(typing.Generic[AnotherValueT, ValueT], LazyIterator[ValueT]): +class _MappingLazyIterator(typing.Generic[AnotherValueT, ValueT], LazyIterator[ValueT]): __slots__: typing.Sequence[str] = ("_paginator", "_transformation") def __init__( @@ -530,7 +569,7 @@ async def __anext__(self) -> ValueT: return self._transformation(await self._paginator.__anext__()) -class TakeWhileLazyIterator(typing.Generic[ValueT], LazyIterator[ValueT]): +class _TakeWhileLazyIterator(typing.Generic[ValueT], LazyIterator[ValueT]): __slots__: typing.Sequence[str] = ("_paginator", "_condition") def __init__(self, paginator: LazyIterator[ValueT], condition: typing.Callable[[ValueT], bool],) -> None: @@ -546,7 +585,7 @@ async def __anext__(self) -> ValueT: self._complete() -class DropWhileLazyIterator(typing.Generic[ValueT], LazyIterator[ValueT]): +class _DropWhileLazyIterator(typing.Generic[ValueT], LazyIterator[ValueT]): __slots__: typing.Sequence[str] = ("_paginator", "_condition", "_has_dropped") def __init__(self, paginator: LazyIterator[ValueT], condition: typing.Callable[[ValueT], bool],) -> None: @@ -565,7 +604,50 @@ async def __anext__(self) -> ValueT: return await self._paginator.__anext__() -class BufferedLazyIterator(typing.Generic[ValueT], LazyIterator[ValueT]): +class BufferedLazyIterator(typing.Generic[ValueT], LazyIterator[ValueT], abc.ABC): + """A special kind of lazy iterator that is used by internal components. + + The purpose of this is to provide an interface to lazily deserialize + collections of payloads received from paginated API endpoints such as + `GET /channels/{channel_id}/messages`, which will return a certain number + of messages at a time on a low level. This class provides the base interface + for handling lazily decoding each item in those responses and returning them + in the expected format when iterating across this object. + + Implementations are expected to provide a `_next_chunk` private method + which when awaited returns a lazy generator of each deserialized object + to later yield. This will be iterated across lazily by this implementation, + thus reducing the amount of work needed if only a few objects out of, say, + 100, need to be deserialized. + + This `_next_chunk` should return `None` once the end of all items has been + reached. + + An example would look like the following: + + ```py + async def some_http_call(i): + ... + + + class SomeEndpointLazyIterator(BufferedLazyIterator[SomeObject]): + def __init__(self): + super().__init__() + self._i = 0 + + + def _next_chunk(self) -> typing.Optional[typing.Generator[ValueT, None, None]]: + raw_items = await some_http_call(self._i) + self._i += 1 + + if not raw_items: + return None + + generator = (SomeObject(raw_item) for raw_item in raw_items) + return generator + ``` + """ + __slots__: typing.Sequence[str] = ("_buffer",) def __init__(self) -> None: From 8c1344babd91adbf32b19140af7a2f0d95945659 Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Sun, 28 Jun 2020 12:32:47 +0100 Subject: [PATCH 590/922] Removed duplicate slot for users.py --- hikari/models/users.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/hikari/models/users.py b/hikari/models/users.py index a1a9405d75..73724801cf 100644 --- a/hikari/models/users.py +++ b/hikari/models/users.py @@ -265,9 +265,6 @@ class OwnUser(User): scope. Will always be `None` for bot users. """ - flags: UserFlag = attr.ib(eq=False, hash=False, repr=False) - """This user account's flags.""" - premium_type: typing.Optional[PremiumType] = attr.ib(eq=False, hash=False, repr=False) """The type of Nitro Subscription this user account had. From 4555fda45b6ea43752a9372642660946f8437359 Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Sun, 28 Jun 2020 12:59:09 +0100 Subject: [PATCH 591/922] Added timing logic to rest API calls. --- hikari/net/rest.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/hikari/net/rest.py b/hikari/net/rest.py index a2639efd79..adce45f535 100644 --- a/hikari/net/rest.py +++ b/hikari/net/rest.py @@ -27,6 +27,7 @@ import http import logging import math +import time import typing import uuid @@ -37,7 +38,6 @@ from hikari.models import emojis from hikari.net import buckets from hikari.net import config -from hikari.utilities import files from hikari.net import helpers from hikari.net import rate_limits from hikari.net import routes @@ -45,6 +45,7 @@ from hikari.net import strings from hikari.utilities import data_binding from hikari.utilities import date +from hikari.utilities import files from hikari.utilities import iterators from hikari.utilities import snowflake from hikari.utilities import undefined @@ -65,7 +66,6 @@ from hikari.models import voices from hikari.models import webhooks - _LOGGER: typing.Final[logging.Logger] = logging.getLogger("hikari.net.rest") @@ -244,7 +244,9 @@ async def _request( _LOGGER.debug("%s %s %s", uuid4, compiled_route.method, url) # Make the request. - response = await self._acquire_client_session().request( + session = self._acquire_client_session() + start = time.perf_counter() + response = await session.request( compiled_route.method, url, headers=headers, @@ -257,21 +259,23 @@ async def _request( proxy_headers=self._proxy_settings.all_headers, verify_ssl=self._http_settings.verify_ssl, ) + time_taken = (time.perf_counter() - start) * 1_000 if self._debug: headers_str = "\n".join( f"\t\t{name.decode('utf-8')}:{value.decode('utf-8')}" for name, value in response.raw_headers ) _LOGGER.debug( - "%s %s %s\n\theaders:\n%s\n\tbody:\n\t\t%r", + "%s %s %s in %sms\n\theaders:\n%s\n\tbody:\n\t\t%r", uuid4, response.status, response.reason, + time_taken, headers_str, await response.read(), ) else: - _LOGGER.debug("%s %s %s", uuid4, response.status, response.reason) + _LOGGER.debug("%s %s %s in %sms", uuid4, response.status, response.reason, time_taken) # Ensure we aren't rate limited, and update rate limiting headers where appropriate. await self._parse_ratelimits(compiled_route, response) @@ -2175,7 +2179,7 @@ async def kick_member( *, reason: typing.Union[undefined.UndefinedType, str] = undefined.UNDEFINED, ) -> None: - route = routes.DELETE_GUILD_MEMBER.compile(guild=guild, user=user,) + route = routes.DELETE_GUILD_MEMBER.compile(guild=guild, user=user, ) await self._request(route, reason=reason) async def ban_user( From f4bcdf0b0384b8d9dda79143951fa0de16ddfc57 Mon Sep 17 00:00:00 2001 From: davfsa Date: Sun, 28 Jun 2020 22:25:29 +0200 Subject: [PATCH 592/922] Fixed bugs and flake8 issues - Also fixed some documentation generation bugs. - Fixed bug with requests taking longer than expected --- ci/config.py | 1 + ci/gitlab/tests.yml | 2 +- ci/pdoc.nox.py | 9 ++- docs/config.mako | 2 +- docs/documentation.mako | 15 +++- docs/logo.png | Bin 0 -> 174180 bytes hikari/api/bot.py | 3 - hikari/api/entity_factory.py | 4 +- hikari/api/event_dispatcher.py | 4 +- hikari/impl/bot.py | 8 +-- hikari/impl/entity_factory.py | 2 +- hikari/impl/gateway_zookeeper.py | 10 +-- hikari/impl/rest.py | 2 +- hikari/models/applications.py | 2 +- hikari/models/channels.py | 6 +- hikari/models/messages.py | 10 +-- hikari/models/webhooks.py | 6 +- hikari/net/config.py | 13 +++- hikari/net/gateway.py | 6 +- hikari/net/rest.py | 85 ++++++++++++----------- hikari/net/special_endpoints.py | 34 ++++----- hikari/utilities/snowflake.py | 2 +- pages/index.html | 8 +-- tests/hikari/impl/test_entity_factory.py | 2 +- 24 files changed, 131 insertions(+), 105 deletions(-) create mode 100644 docs/logo.png diff --git a/ci/config.py b/ci/config.py index 6f5339c0b3..b75827076e 100644 --- a/ci/config.py +++ b/ci/config.py @@ -28,6 +28,7 @@ PAGES_DIRECTORY = "pages" DOCUMENTATION_DIRECTORY = "docs" ROOT_INDEX_SOURCE = "index.html" +LOGO_SOURCE = "logo.png" # Linting and test configs. FLAKE8_JUNIT = "public/flake8-junit.xml" diff --git a/ci/gitlab/tests.yml b/ci/gitlab/tests.yml index 4a8e17092c..bf9f69e504 100644 --- a/ci/gitlab/tests.yml +++ b/ci/gitlab/tests.yml @@ -123,7 +123,7 @@ test:twemoji_mapping: - if: "$CI_PIPELINE_SOURCE == 'schedule' && $TEST_TWEMOJI_MAPPING != null" - changes: - hikari/models/emojis.py - - hikari.utilities.files.py + - hikari/utilities/files.py - ci/twemoji.nox.py script: - apt-get install -qy git gcc g++ make diff --git a/ci/pdoc.nox.py b/ci/pdoc.nox.py index 6a4c5b339d..47373e6752 100644 --- a/ci/pdoc.nox.py +++ b/ci/pdoc.nox.py @@ -16,6 +16,9 @@ # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . """Pdoc documentation generation.""" +import os +import shutil + from ci import config from ci import nox @@ -25,7 +28,7 @@ def pdoc(session: nox.Session) -> None: """Generate documentation with pdoc.""" session.install("-r", "requirements.txt") - session.install("git+https://github.com/pdoc3/pdoc@83a8c400bcf9109d4753c46ad2f71a4e57114871") + session.install("pdoc3") session.install("sphobjinv") session.run( @@ -40,3 +43,7 @@ def pdoc(session: nox.Session) -> None: config.DOCUMENTATION_DIRECTORY, "--force", ) + shutil.copyfile( + os.path.join(config.DOCUMENTATION_DIRECTORY, config.LOGO_SOURCE), + os.path.join(config.ARTIFACT_DIRECTORY, config.LOGO_SOURCE), + ) diff --git a/docs/config.mako b/docs/config.mako index 2865b56ce1..8430940e4e 100644 --- a/docs/config.mako +++ b/docs/config.mako @@ -40,7 +40,7 @@ search_query = "inurl:github.com/nekokatt/hikari site:nekokatt.gitlab.io/hikari/hikari" site_accent = "#ff029a" - site_logo = "https://assets.gitlab-static.net/uploads/-/system/project/avatar/12050696/Hikari-Logo_1.png" + site_logo = "/logo.png" site_description = "A Discord Bot framework for modern Python and asyncio built on good intentions" # Versions of stuff diff --git a/docs/documentation.mako b/docs/documentation.mako index 98b0d33ccb..81fec0cff6 100644 --- a/docs/documentation.mako +++ b/docs/documentation.mako @@ -64,12 +64,15 @@ %> <% + import typing + + typing.TYPE_CHECKING = True + import abc import enum import functools import inspect import textwrap - import typing import pdoc @@ -112,7 +115,8 @@ if name.startswith("builtins."): _, _, name = name.partition("builtins.") - if with_prefixes: + show_object = False + if with_prefixes: if isinstance(dobj, pdoc.Function): qual = dobj.funcdef() @@ -132,6 +136,7 @@ qual = QUAL_CACHED_PROPERTY if isinstance(descriptor, functools.cached_property) else QUAL_PROPERTY prefix = f"{prefix}{qual} " elif dobj.module.name == "typing" or dobj.docstring and dobj.docstring.casefold().startswith(("type hint", "typehint", "type alias")): + show_object = True prefix = f"{prefix}{QUAL_TYPEHINT} " elif all(not c.isalpha() or c.isupper() for c in dobj.name): prefix = f"{prefix}{QUAL_CONST} " @@ -195,6 +200,10 @@ if simple_names: name = simple_name(name) + extra = "" + if show_object: + extra = f" = {dobj.obj}" + classes = [] if dotted: classes.append("dotted") @@ -207,7 +216,7 @@ anchor = "" if not anchor else f'id="{dobj.refname}"' - return '{}{}'.format(prefix, dobj.name + " -- " + glimpse(dobj.docstring), url, anchor, class_str, name) + return '{}{}{}'.format(prefix, dobj.name + " -- " + glimpse(dobj.docstring), url, anchor, class_str, name, extra) def simple_name(s): _, _, name = s.rpartition(".") diff --git a/docs/logo.png b/docs/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..73392f24dda8e55e80e34ba1956ef56132fcea2c GIT binary patch literal 174180 zcmV)5K*_&}P)8>f{G|@l(=$7H1F!+Ri?9HZxByAeVuWo{AO!|7h)PA5B}$NJ|G}bVt4xRr zMa#BmC95R+A4#;TM9P)`0t|p4N-zTiBJKiMU>6%E_RMtmmv40a&U;VKhb@(7(U_j@ z@4MfwxH+**Zw|`$(welj@Ti^OtJ@d@x zHD62>$I=_$_(px<3t!M|x|ZF3m)Lz4gG^1+oIDTLD$CNIgD>vcpMk&QC{fddXX1DM zwOA~a=Xw7ek=>JxN!%%pBl}D~hx_#3@%q(jW%n^04(*yeGq2+w_V)JlhBv%HpZLTl zPR5q^b6u%Zo)`A>y1F;UoA=7K{Ij@bk|o-%GHF$-OCohyszqC?%MxY7f#&m>iqTNz zx>CoOq>09DtgMSvwzU?kg?3Kws_j}W>Pl@CsZlGXahMb4qFpOa8hhTTV?JATU8_iu zcDA-PozIjg((9jmQrE9v*VR|9D$7#UO=IKD^OQUv*GyCUEUuxpQ`dG{Dp5MzvWsVN zsziy()mlZIDsH5%Zloqs9`h-&s%EXGsg=gDk|Jwn`N%i&A^mEQuAV)hwtm#_QHdFNhv&W;D)_!FKv>=V`p&&8ZEMoF4zUHLj-&3B5E$ew$(TG;FI zJghr^PZTo-o$_qN9jR^j+|JesuajD}9oGz>SFRh4MkD*Y2OfApU;EnEG@sAy^*cK| zn$2c<$2;DkuYdjPx^d%%?JL#_zdJrYwkKg-j>ltr55F4>2DTZnBft0WeXrDQ z7*GGh&&_tcChJ2XpjnHvuT7GEFRd6);AdGmW6W9X#0ZnfpjCmJ+RoLBp6h# z!NTzS<@YQIKAXkmlM@!8Y~k_z+uPgrJY2)SfPW$Udv;xc|s}Q)$Lf zisIM|BF2obxRqXxTg`?8H8?gi!l~9}t0ZNObg{t zM0Ve-`KGCQhmNBQ4wOyL@1iIetTY`+U39Yk%n+NiXFcL1FqFX=^I5E`Z~fM9)ki=2 zQCnZkKZeE(XAd6SA@lq1`JV65v(G-O=bwN6#QE}!tRMC~jwOm^25T{2nd459%s!L* z8;>ShtuQpK^Kk#nX&iTEfN;=!v3K2Q`F;+@KL8=V=QZ5_ojZ4I5Ab@oG2D*>1-_5d zU>x1n`0t{(U-+?(3;6~Po3Hn-J(6NDcnvq^gTi3=CMxnm>vDa9jKb!ORTPDejt(^( z4)pZX@6yeiH}uu7enqQQ@FJTQ)IGo^!U_*6MajTWn^Y;wQ-dtog>^h(W+@x z42No>PK)(IX&P(Ad~~f&PbQij-qr44q^PddU|cW&b*&$G|NHg&M_;FZ{L6n=sw;a< z)=C35#-K1n!+*O?qcNy~m3`tiL7A}`nba0>qOx2o&r&mvby;CWl;FhTM6Ej2_WuT~ z3}s=sV#S)4wc0e(s;N}fwaTv3@p7dyYPD#jRa-fWKFJ8Q81_CtZQ48ynJ#psD8FMr@ zZd_BEh4svPaNe5+kk9vsD1Z!xJB%TFDZD43rIWSVuUX$yn^DN+%a`@0H@!(8`p}2I z!6Ey8Wj+)KptSjZvl&nAzuyp+Z{h$+!^DM+7Jlazl;3SyPV+up2$kFbm<8@3F&+bp z=?IGug$H(nvUx*(jm>b-ykV5t#!y!)%OfpTclB5Q>i?kEzv+^G`cMBA-Mn#ISr!l$ zi(s`lwu$3&F&H2ojEOH&K6BI1Fd1o*+5LNnYVecL3~S$qrw->?L?F%JEyJ~VEk=%o z;mGX5g$pMzo6qMy9(w2@eed^vum0ZO`@1&5KCbr`1PiTI*M>0D4ZAsqK3qS6+jw?D z@F>!BxllStb>;F!z43`B_3Iz`u;ON|WL+yGY{)Y06+=T;XC`CC%S!M5)^E|{_rFG; z`0$6MMWtP(D!ND`#TrM6@-9+bw_2~Js@qasA5&Rq2^us*6{`k`W8{GSWo;qc056^J z43S#iM_5?bst8vxQy_;VRufCRO|EgAYgLsN(jK>s<{0p;q2`e^Z7NNhTC2L&vR>(h zw9(528>TViwrYZrTQsd23|E$G6&9JIBGaU7wY930LsshqT;zEiXVG*H+br+`B zLqd!y>k6k=htRMa*qUa2VUR#7eE&A-<=p~n~y$f;hsR*zRovMTJ)yVf6PYGt$Q$3VFNlp!$#yyTxc^k^#>u=4OTY5 zI&7Fh+cfJ3zA=Qv1}A(3V&3y+_qZCr4}U-p3G0^71aH}@s+TGTss8Cd|6g?bV5YzL z7ygRMH9SuS%eMFi?(++*>dyn?JKjh=OPN zPlbi<<-Ys7zgsW8^pbw}cYoJl?_@Hu*K^(1yyi9f%x6AxGU37C-Qy1pZDA}xI6V?& zkwk28j9I_A;7)k%vRP>{Ki2nr&v)vnCtt63z4IiNOS*2O7C{I;1w^r3GH6Cs0zKa)iaBxS5caJn0 zZ>fRXA!Ijn7pZD06{}c-^+2mwns$v2%SyMKT6ddT^F%s~q@%jhI*C<9(mIdT7Kys1 z3mlw-zNR)4F{dhaHT^$#eC_ zZ+Cas44JQa-**_9{r!DgV<50RAF3?bqXA4n#5hG0Lgrv2+(BoAvc274oHipcci3(} z91!uD+%J&^2$j!2Iyws6R8Kwe9)6eRArg8uIEvnCqE$gcB8HiB(&}2uG8aBJfZ*M`@T;f{>ZQE z_doRqIyKo*!{To`^Qd=^4z;trrK%3737^k@iXovvBAxQ0P?|u-uGLkIV19;atg2k9 zff(%?6?v+`c&saBtx4PI;YS~_aO`!jeXXv){F3foyP-!Od_Zr1+uQWJ|K`K$R;5<6 znT8k*IK3j#Vztu63m0|ul~Bm{$^#%QZK*oqF#9ZYf59d7B()J zEK=P?Y6=_KoMUGMWTGg|jN+;= zR#{;oNYw_y-CmN%YRa{?w{~=Va9e{UwGg?fYBQSgFjo~z%eYiq5`Td(^N?Vw)?r_O z$}F_UQRS-2+6e44C6OgdL}Y1hYtW6O8;htn^N{XrTDUGpNP$PFkOV@?9BN;*+jz&5@IttuRzs8LFZ5!hyZJ(Ll2?jHI)0rRs zhL8}VuWhG_qg-(`RHR($ScoBPYE??nTI6=xZfT1)iB5k5LFQI|^MTyz8kC0U>xq1i zgBb+p5{R+GAgwlty!a&RhR67h@AxAIcb|FYS)Ds~PPcB|IfifhoLKsQ4DTbhghrx5Jv&Au6%rzX1wR3h~S8rT5ex+!z z+ZZ32(XvI6q*4n9gfFw5Ym1zWge=AfAAMY3dFBgB;as@?c5V0ueg|jtz=IE3*2%oy-oCOd z5b?YgB+;%bMes!lYanbcUvz{BRUOcEmgR=bhVxS6HqtVSEToImfoceN?EA8``)*Vl zsSl4eua`>FNLOC>xUPQbOBNoj<};0kBYTf6-q^WDg9tsv z0ny(q*Lr??pl2pywPmT*y3xFnmK~qlXax|=Mk=FN%Ni;pQI~hBvz2y6W4-+?Z`QM) z{+!ka#~MYkn$=qCX03J6shniGo7C#ookmTjq{t1Ob|ggXz72bwPzP{B&?gwgv`?ZhXc~w80*9`LXSQAs3E>DeD-seNlbcl zObA%yAc)q;W}FMCG0n0QN5k(yU>FjN@0zfr=WU`kQd)P)*vL_92nIj6B0!8{)zArS z=oD1f8#j^`d0}Cv%>#F4d!7(1GGZNv6jJw7EmtdU{e(j#WM;(<>Qtw?Z7t(ghh3wC zZmp{-b*)+JYE)}Fh;+N_bfi}6BGPU>)cz{hI%#z7;Rp4?^{ZMk7TjJPD*^GwvGO=m z%_m?WqgY++#65FC#*+vnF(YVbj*Z^OA!#21I%mq-bk4|8$>tt{4sf!Zy+Rnr+V*IG z`C!BI@9+Aq@6vzxoqvBKBQOL-Ql@EwQ=QFDvWtAK@w*X;hkKc9#*_@>umAe5n*m{; zIkkb|Bh1}&@D5Jgmbj7ehz2BUw8jQd;r+g6`QAUrjkX(WPf*#O1z|aybM)cq;;xI$ zl9Jyc6a-nt=|Yq3RJ*4WmDQoa&-LN!PSB0Vt!^Bgc-th-Fl?KI8lP)Wcta1i`F>9X z@KC^9Qt@tRxE6~EFTgeVJrn8h_tkur-%l_|L6(6xa?+Er@cY>GVD2sK9&E#-{eeS@ zgt81H4YWE2#|qC=wRX|8e^j1h~8KkKs8Y`HK9Z{yZ$v9R>+kyrDD@mgG~Njue1+b<)~UZG)> zDXtndT)VBcb9PrZ7l&HLjUq_#9I8KP*6to1>GJstcE9McxOeCUHal{OhDF~S074cv z*U@yU#bO>pi#Q4)g-H}@HPf{QRibEEs3}q{qDV(ctwTwNNvB!V=x$x<_IjyBvsP5* z8b`ajx;oZzRH@k+X|%he#cZts)I$@iXcHwQm%7Nv^^!0Vi)OqSR7fC6EIkh@Wo}6Q z!x6&sH6g*X$w|2r_q7)I>6^YupZe6N%)l|uWWWh6F{Qx@B}D;hTu`w-bk(2*z*LAC&7bI6RE;7`?Y=5=h=Mn?m^YS*_rkv4jDTmNqea4M%3@ocmuRl&B_*IicAqQ3j9q4u(oE)>#UG0|(7JOI2#Lb#_O0>V=|HBdu>7YBw1h`Xf<;fxLY2qNYd3Ap_-H83s*k z{>B8TwxHP{X=-ROL}5MG)(eCQk7L%*a&739pn7AtDiQ>QpK+%JS;uOvRaNMA znd_zXR7V5pc$BFc zj)YBfFNtLir6-ZT)+y8BPz~Sg5Nyarj zGVzeo5goD!yYBMHkU47xqXgRVNQa-fWXEMX7%cuc)#DM1M@Vk4PK-|f+Reztvyvg| z3hUK>wu#<01?(pX%VSOu%OD$XAdZlN1%AG5dnpu0iv z@G4w85V_ni8N*<_df{K6ZSaQ)qRLCq1kEv+?q#?JlNFT2xjn3FQ21AGj2P#7wbCMOwcr_(RKqA& zw{Dd+u`*QXNuovC=qRZ)dGw<8AALa2edv?gS&+&PUfDyT^>TTF@*JV#6_F3*1#2lW zZj7+B?L~hPGFbY{eUoZTe{loQfh$QyLrLSf-hAJWb{ zV&S@dma#W%ZXAN*J%nZf*))bs$XEpu4i7T5(1XEqWaU*9WpkscN!nkkb@7z)RJoh6CU{ zi?UJ%kHHua_El@`w5_(fNEZe}ozDk4KN#qAnrjFb(zKeaR;ob+gpGqjah|EOELLS3 z6va3p4=_7QBjrt|rfe*90`)lWw}}jG^6T0R)|n?C*8Z6tz5MkTbm7cJef`T{wv_+w zaAL;l@bK8gif~da)P-t2^^Ujc`nBtN>FZzDC<{X2Q9cNKlVv}{JXEWd)^V$vbUew0 zE>%}WhDON5CWK^3t*@7dy8QMh_3*cRlm6pB`F}LIRT@%a0kddNk}ilpEd&h6DLe-X zYbpW|xAz84(gP1ZV2b(|UwrYz*p_9bd|a3VVQqOS&v-acR}zX_d}~PsRjoOSStlLu zOw?=*bckFck`CrG9V}L=(}9+8q(zeG5aX6fMVi~Z@qA6)YBb(9Ww>XRF^)~SRMa*i zLu!=@Rw5KaK3`vMw0upwak5Y<4G!F+249;Po2Q?CT4?@$(_A3%YTN!GC4tyT)95i;g1_V{$_ssb*~OZoliTt|2_=1=L13YT1^id-6*^L@y}+C z`I$Q|M{O2re$)9MJ26;>WLO-ejtw~kWi#>y5BHL!RoyO>W}SwkzD90IJyf4|+$iI5 znCAuyuM*ctn`xnQ zCzV~RHR*Oslcz?CZ=%E#gA2#efNoYw#{-q~rOs~cX?o|bcDJ@QOmgGl4v(fM8-6ex zDjpRk9WXsl3jz9=++$%OlW&q9jk&UcubHKHkgQ4t&Fblm>}NbB7J>5 zQ*~xful=@f){VH-Z++-DwOywYj-{nkZEV>0&?M_ZGvd$o>zeXMhw&L4jRQEuf#K{w`x(m#*a zJ1^zGbK1m1SO*~fItME!95!{78x;jOB~LPVGQgGmeog<|$H>+(25Xb6G4Y_#V}J^B z0v)!CO@!+`P_Yvr=2@oMd?rJ`efrjK)Z^045}r^W6;!=JqaDIX9}o{AH-Bb^5dGCj ztz9r!o7yO!K!2YP9+S6;l5CUplNSb_XAQzct2295!KVM)k(Gxfd7sI&hysIW^>I8= zC=06BQ@!D_NA*KL@Sp1!f8np`!6wxsqdncfcUI?*@6Vz81@+IjOz9(>lI=Td%!zMSG`D>!nv-*2@Psb^k*TT6Mr| zwbrVxRGU;qnpdU9aieWMi|D&y2V`oyNNF+Bagpeytk&S0UZ?N<%YR1CCdc|)Klh7z zu-wzvUVKS$HqdNUX|lbqJ9nlbvkF>(EH_bYV;Nf%{j5_qBV*{!ax+Q9&wbwD!5kDg za`XM-#si~8_7DSxA?Mm2d2AXfH!4n%*zc?qD4^AQ>)q=q@6{8#5hbg<8F@IG_iW@Q z5pReIi_tdRl!eJn!M&9t-zx~@Mo{8T)001BWNkl5F@4TBCFi3Yt5-c_h z!=QjcCp$<+C`cPvl4y3!N<(&MNbE0IhlZ~`|7BZX-oaqgy81(TxMzbBfpUUH_i~&d zL<|TlK*k^}Wd|95hKOmyD88Cwa!bx+lyR)meCOg_{lfau) zjZ)jtGVvyyh7yQdkcm0wK-QXZh%KB1b)-xd8XkRHqYkVu5rRkZ1-Ch- z_WA&bbf=x_iKpMG(@#FBPyM5RtSl{$5uo<0828mcnij2QW9i#|==-$w!m*zF_5W9g(<9}B zff4O{x6t6t59^IT{9e5{to0lJ z>|d$)(p`Q1>J8P&K(`mAlnqoxnTcI-{tN(O--OAd(+5(HUarBIIqwtpV378BQL$}) z7f!ya(}YJ0&e?%LO$q|2vE&-uKr%qZz{4SHIq+6^0g# z!|LmS(;zhV&-Sp7Ii;=;V@p+tM@=qE;&t2uYrxhp9F~V=yogPi8|fT$Rmd88{1ifw zC>R+G3JPgr<}W_;xsy%OzqmIrL9ReK4)5jqptQDuud*d>H@JyU8=KK6{RqYVHSk~Wx8D`RsjzL zp0!Y@(Yh}lw)(BU8Xzt*`)oY4jRgq|HXrR`$Y7bY;a=6e8CAm9KJ2jPu4~QWQkNfn zSQj6BNT2@jN0cI9WmD8aiUBI3#*d=oVmz{X!qqWII+TWRKkg;D4$?5%7L`^$OAe(lMX)joEFQKadtt=8yOLd2Zq1mS>c839=KNu zxV>rt4{l^CpBL5%|HhzExAx+Vt6DTz0OD&Y;w zM$743O&)1IXjOEvhDublk@B+BobCiW6J3o~T0XF+@BHZ>*VRd@-~E>#)%@dM)SMc~ zuFz2v>(;VVe(JR5Rj0N9p%J!IfE<}1){HC&72eiMt*o*DLlgKLLsETL3+II_F>$gN zBBMA0*F-p*>0M7htv~pKKhR<}KUvQ{XTH}qBL^r1(A#nq_$sG?91yS3h{>5YD&RXAu%^Zd4;~t?)B!(y%x1 zW}TKxRNRHNmcWQ`BWR$~c=yC$xh40aX;a8sia8hwLo(D;n0F58cQ=?GKbtA*by*g+ zIVZBAk@A6Zp34ng__?%LEPK65Q08K|+@pjAO%T_29H?Lw`XF@!iMUa;5+rgvEFus> z4v)vCOw9{t%UXNtv|UGfd}muv?C+|$bD&4IwzR%|M@JVjEBZxK#LX2H7RQAm+M}2l z5uPyg<-+qb7y$@ZT{{bO!k2r%6`bdj%uE}%G(LdFdFCA+&D0C-Ebtfuxz3gU*nGxn*`b?Uk$i9bMcwN}O?873<_2lcH z(C0q?d1Vo@qadZ&La%!{iKoIJil=JGydo;~{21gaNT`qBKW}PRTe$!)Gnm zu%IEL@z2y}k$~I1^B~` zxUWq(>Y*gpq8kQe@$~puLqw+RogDeXsy9pUDF`MJ^^u^)vqh>XOJYDV6ofZQG+)lF z-f2bsWYU>BeFuVaHdkDA8mZORy3r&1dwTfvd2O#(+KE#YuhXSRVXwTQP0;!W<2MT)^P!D-X;3w;X{6zyzWj2Wi3U593RgdT7Ugv5+6a}JuA@cSW|0Ee}12#*R4kf4xZBzw#CQMuD@h%g=`)VI*_ z6g;(#plZad#PfK5^j%TeXb2)Gml~hm)%N+*`s(d#%BQ7<6{Sg`Cl_NCrLl#b$Fr$P zD6%{Y$!Z)QYYu3OMzj|xI?q8ruC4LJ7C+;|YgHf`jn(3qj03AJtTtn@rVB-{M(-Qj zE!$QL3_d(ARRH|FOVvaHS%Q=gnx!UJ&S-q`jK1{APwDa^Q^Rc8NHw*VJ&CQ6X1-LOCiaY=TD};!BF$7Sk!TEqgY6p$jv1WwWpecAw%EdA z{*Rg;9fnpP#+&uoC2HAdHxg6p2RD?c%4l~&Z3jG2X9jQozO(x2zwnd#(7*ln`pw__ zq%;}m;WE8)dO-o?}_7Kwi-=pa8}1 zWmH4E8Uy^csOlzkHwDchonX@@Q5s4|&?*gvO8bK6?a{;xHQbdG#J+L(8@`W1TNasx zhHUg&8X}@yEzJR-Nvc*$4O431DlG`twuf35i7vnKG2Qy~m(|{yp47;Z5kn>d5;#0O zG$UmhLHGdZOH5)iTf?Q1IYizQx;Bi{fs(FLIh|SSoSd4<@kF=URlVKLE+(jXE>WTsb@ z@{D!fmp=6rPLxqp9$MSb6T`!=5bA|z_3$n<@1xu%2yStZW(~Xa1ai4<=qG|I3C_PFXcDOzi8!69zZ1Qis&twbfPzMLM^v8bieb$+g z#S7sLGCAR*dR4S>9>a;1){K{pk{YtO(>!ZcpBd@k&Vl~qPyGqK?ZF52SAY5gy3ma@ zOlm!H;hfHG?`SWP29ScQnXa6DIbAYYt2?i>Q=2L19WE;_lAT(;1j$_VrX&e zp3i%OIU-`*;UZmQ6UI0sJgmxq7A<}AxJ@{3WC1bi%{pRg^&D}So`ul=mI(f3zK>vivAtM#S zCPM$|@zivD*l5%8<*8O(qv;VXbs~-OLKPiy`^|(bgl2=5u;6D2SuI*H(`q@i&_S`! z*n@u16rI0xNiW~HrulMV$tXBPc16}zYF10@Gyt(qi<@;?XjK$Cs2km0R$6qC?yPIe z)~+U*bx=LEcS>>F>G=9>ZN-JECejK+JRX?AEUXl1ZDmeWSEOmt>*Q!VMEFN1gwXg8 zCzz93?lZS;tQ=*zA+mi&CB|f|>0*9D%{W^11OWTbMWmi}M{43xk?+?{Zp>*Wmk!Yx zgssMFO#78#xZpI&`kR8W=U)jkLGR-}vbxtu(Ks`&C3K@@d{at)n$Y9MdUr>%uAzrV z$AJt)?qDP@Ux5_&EFSf7MG-=oyLXTD13&OS{ox;eL0^C2>qfF8>2JHxar1;s?z2ly zvbho3G+3Tdac|+VxFf?&At@XM8J~lLo1!0(r~0j5HMM)TkwGh+5tyquH$*kUV1ag$ zPOCziXO$MKBfaP8ckBD#_;&r$fB)a;TSkxT@z>t3f?Bz1sl7p_p;~R{sin^wt7|-^ z^}13%m?%!jK+Z!0LTU-=Akeki-rcjYv~j2?p(#yXU}Pr3CNh)A-1{&(rl&R`p>@s# z!4X$O+%K&C3_J{7A_%EG8|n*|{pdQnlio`Z{CQ}FjS&gqf9PKo!t1~h*?8MC+zT(< z(S%BbAZ>7>)Y_S>)}haMwbI=~+F1`m;WzDcthZICHbZ^B(nC)@sq4=^uljIqigoX_ zfaZ&k(3RD`DQvt@lhf+iJ~ycO*`6TB)2H`!d~~es-JO$KI#5wE%yoF@&`OHnh6oj* zDcC&B2L{W?Z?e%Jt|=rw+@X2q8Dq1_QMvPa_0L?^Wb}ohV7SUqgne)S6FgvNy1pSIU2Mm; z78w%@EqncC^pg4a974{ei8EUbOG-2Xx>*fcWv2$ZapS69Gv3mhN2m1AJk_K5X=QP% z!gMR8*7ZVLS**vOew&{A!_QcUgPomInjO!r+XY=FmgTV-e8NvFn$9wv+COdWad>vf zYUCFnEEXRkQcDB0gsP!K0Yo#s!Y%7(8L-H%6S`)~M1$wgU$l83jAB8UK$i@o^?3@5 zHuN>;0EOD>=0Gd_d@>nXa>KKX?f|TTM+BN5*UljVX>@em^fv_XGo$k|xVY(K3i`zL?us)BV)Oiovulsew}rd16*a zh(17&^X1z3PDt(H);P4dfv+r~I%2J6#~M>(Il_3iDy`jeVpS1wK2n|zRV^BQtvu8h z(wSZ!HCjyIh$?O6TdMBPHA(507HhJ*XGx0TAhOT;(I5U{oj7k z7RDA-$ehx%&Yk=J-`|Yq<@>Mb(&bCm0hBR!xsh?6eb0F49h}Ht`b&SwIygN0?3YgF z-S?~UnmwWKq`spxng9eu&w(|evqXrR;6C68J3ylg7-6ec|dA+5{8csa2)jKsB zj$>=r@xX)k>wo;~Gis3*N|S+x z!;u**6KZx4_aVjYgL9IiepOweXB}O(;DhhT%G13kA&ChYL+|V1WMWzy&>$T%&03gp zABx&f0D;p(9vW}!fuzt=_rFG4b*yrJtbKC+=v;)TaiBRJ z+|tO>;vl=-@s4iayiM-S2EQ$8oj$X#!=pnTFJ@*ad3N)PXtR2RNjPericp6Rs~9Xo zB#f3eY5&VIv<38n^8-G`~w&cw?62b z&@GoULvZ{oPDdxiKlis;DuMU)43>Adzz86RAbjF8`8}_7aBCRVq(&Cg<1A8=k5pwN&C8|ku9iB8D_tuh9c~qR?YF&K zzxm1EQ`}@4FH@arhgz)WDu*i-!-0P4PyKPd>s_zYFaMoiwW4JMhphP;{*!7*P<*K_ z2{ey~gEo#EXEX3`cQ`E4PzDz?Pn$9Z)~TZ;-v8eBzSlZ9y!hfvCq%ddflYyDa5N#| z!y3rwW#3Cta3*|Sh?+`6jb^am4sjo*E)Q*?JnZB?41vO7`NMcRks@d6_kQZ9enKDk zz)$Gb&4vD}|M~;Eas7_9InbQbM6yE1^SKQiR^GZkP~KuKmC=<& zT2@loqByRVx3%(et+Q>dH=nzt`)cXYQ>WF<7uwGAP(!z@H5v|VGn7$ji@A#_Im!&G z=Si--O|>1zmt12%#WGu}^feB(k$P#cJBgEG(c(X#)E4g&5FUK~ufHhdA>?cx64Z z#++sY`8~DI_fX!%nZ4Ne)ySCa6&b5K4jUXJM1T10!8o8uP+~0Io*wDKYah^!tJjq! znPnYW3uNaYyFs3Y^h}g$>fxZqCL7oB^RSLER)^E$aBu?$G3;#V`pug*xA3oo%OmEd zSAOrD+O^HHSS~^$iHsGYl9i4$W&kl*mXt6A#~6i>&>RinEeBo9riP5UcMJwjX7}`t z@=>N+w-3zF7&@y#(Wyp*iJGcZ$+20*#8B9}ZnSLah}!6IYoNV1J*kg>_A^@1cWa$0 zM;;Y5iYN5g>a?}JttGyu`_fDR{ zvxPwz5qe*Hix~ zHO{9akwJ754BdJWju>+)$N^3C)(b!0xdpW>9dAl1E#@=5|NTFtpZUO#>6I66=+FMy zpVeZ%v}$$hjtPg=F`?{=z=4p;=0kvaex_gGJAiJ~s0YP7q2p^``&!!o96`iFaI3z_ zUwIhgVS#TJBe45)wuMx$Ixu2LsS4@g)!KxJHNs5D*coa3!Y0{B*EZ51AL%Y39+{-& zT={&aN4IwL)ZS^`A1B%?I}NSD9o?D;9~hPTD-2Q-X*KkOPDC*SxWmHvRNi~ASDlB%|@b)%X(!* zF`JTPNN7FX^B8?cNGGMO4f9EdNH=J;Ep9SE;S`k0&V01OVjzg5(KuJz4RmmehH)Iv zcCXjD>tieo-{|d>^@ZRl_X(ZYIP|fa#JTFl(ohSpxwDv(PSno!u4eN^=o&y*3GNd) z0-Q(hmB~OMv^3os2CE^xmfAUwNFNSoQ_JGPDIv+YeDRVwll@Bjgbyv5Sy~LQZ7Q{2DQE&&D9zI^z%Re^ZMWiKd4t; zdBu#pm(XCuO&wjykhsBOB7NgiOMZBGNAG#hx9V^H^3Uoc zzxHqS^Z(u7vev7_-wDmTOuGc>89I-iWZ!6ETjNF6+NIA@U!@hu{M zO^;1_z~ZI#>{egf7oOja6g}~*b&bQgcQ7sEhyk%yKgKVD3`5!*LfLt6# z=i^W;bKLj+2c-m#h7geBy@GTDBgFHN3B-t?J;Fb}<=dWCQ?~jKAODz6Z|@p{^?OSO zxn||cp3$5%nKk4OASuR<=}Y>9PRTmwnGTPqAsNw0lkt{`T4zVcR{m88acOMt<8+}V z;%Mjv#<6R3>de00_a}eS5)J?0Z~xCq*0cl~X;GIZqZ#J|A!&(Lt%k{1M@^;YyGqaJ zjh;)9fOia*{l!w-mXBj&(?*Av08XPwVtRC5?)gO=IVt(7PRt5@Ds~scTi~TKiF=ci(qKuTRDr z93Lr%!=OYEZi|COn^;47ch`wtjtMmrLu(iqK9`MxxDMlEnJ_p5WC|E;N{evvc9KwP zP2LCzopA@l z#xZbC=H(E|(xJ5PM;06uVO}_6()v2}8eqar7BCC*csA2uFtP>ehjucp#vg4IuRFiR z=C|w891jT<3zlP|Fq~CmJ>p3mfMiN~elaI;SO;7WNdsfS{V`rFRy05UZ)yRt9Bf`3 zhJ`Jb5(qpPJmF&xKVo&3&wS}wo#N2WzMvb@92ADZvZJKD(`~QX=00l5uaa;w5NL(^ zN6KY*UJ%#TcuVKboYnkzsylb?TF2R85o#AP%xeyfh2}KDa<1cgrH3AQM3cR3{r1Oy zTl?F4N@lg!l{6v2A+h9+U;yh1>m#* zas=a1`%)<0Yg|=-GP$FxlfBwIVG~MYROzT;ycz}v3W?S>ftp~OQnlvB&)0xOVeh>1 zQx+1sq<{|?6x&*@IX{r>OJ!M=(PU)mSD-u@5x5{>*KMzbCVL?zPC7N5MiagdM7{K>%yoprplW&-_bw(vUp=kK#_0#@SUo`(fvbW;eOk%f}645cou zZLkG1X$Gs)mDfD1m#V!(07*naR9Pl9R12_1FodfvR44G6jJbt`{rEPd z<@q<6ITP~}{3Gb~H49z`fQ{gDR~5be(ode8{2b1pfGY?$!$~HD z1n5a1VK<>6r5;H#wBO^6gMkUhi#tI^<|?Fy!3~HJ5i}BuqZJBhWpnw;Q*XGO9=+gpzC zjw+QY@&rQ4M)zr8*`i`HG6LGl84!{7MZ7Gw&>KGJpwg(Y;^VHtkdUz;R+OgmsnQW* z)K-&Ir}e~BZ`N~PeL>T&zodbMY}7i^3W!dZq4<}1Ize()Z%Fa%%ECYItHlB)o5;8Z z{S60GfR@9YpE|XxYn%>@!5@XzyLiEH4rM?ogj^x3m>6e7s~S68!;nU(XQP4P07d?~ zw7ENT<$@M%t>xiN-HJmSs?c$gq^UFTOUgbt*W&a)yf(Ok}0kFk~X+DYjLz% zTlZI9ms*Qt_GL(v*eOlDqMc*{$cmq1oNz>E_xJVKqmSt$ANi>L8{=f%PP`Sh^8qKV zj(d&T#<6)^&w-DT>%Ev94mP2s(MGh@)PS4zGn)O->QL z)_yoY3hg#zJhXcfbOb4f$X zpnwwaUcu82iqOf`ex7GqQuEjM^2dnM2=1XbJ)sv~c~J*94%EUel{6L%T$rIYBpv)m zNJ?2v)7g<&bfmy=lKMAeV5^bh_ke`yjRl$k9Z?j`<~ceFwSzqW=`&|6(~QIhL%}oc z?Ch#*OC25EF~iDzv$5v0*~uAuj>xzM++~T;pzjp@;#+tlDams)iq`1Q^n&3~2?DG| zxzg7DuD7AvojZTw_NGw zdZ|~cm1YzakO55#yASqR$}wFD+2^(DAz9xxkp3XVJ|sh4!Mu}ArWgNP-||+SJ9nS{ zuYd6?Asj?61uwlxbZ;0d=dnV0UXbC~*jaz9kf*B!7=*tK8~h zSV&E~W(QM5A*CNF@7HzXX#-(~Ay#9j3ZwD`r?Cga286t&B?{i07B5&g2=yl~?Ca*y zk*+P5I_8`|_@FGeBIWyI>4{;cH{_WfmbBG1x`RN`db_X9vRaE5TA^@FlR>bc*=}J2 zq7PzjcxDeNOvjR>dgS#_=!>8EjBP?Y_b-k$Z)htLHmn^LMOwVqykM*ccQXqabxm?G)EOBXG4d+Eiirr)E*4IwM! zEDIQ86TP|K6iwe zlrl8cdgPJEEQ@W5{xTp=j1grxaB>MXo|It0&lMavQEJ^RH5}y{Q$T!tj1tzI9bdCF z(~J?VmpWaqRg(S93M;i*fcoulSGuJ{CSQ2)fd}=K=blqFUFgC}x}C*(DU+VhTD_FV zI>=L18N6wtY}IOO+3Lx&_v_lt>$)Cspa%!da`s}?>-DJLrR!;K$dA0hh5h@iD&V=V zK4*JsbN?D=M-h1sBz%uMKjl%7|IK?5oPXd0e^LMNAO7!p{`u!aVK)1fBgm}Qm`an- zjTQ7f8Bc7?Zyg-iIJ0kD=jVwxu74tHK!@cFzxnc>?DmofC(WGDel?lKd$5$2p6>}v zzlMt=o#)`+fo~#22pSLc9y}CY(k<<)2MzMngr>CEuoQZz3ADDs)*;L)8$%;L+6|`x z3PVIDU8Yf!Xt1tyWw@nBPoL6j#si(|YVDwqUe7c&F56Gf2?L7QkLw3TKFBSgKMW20 z5MdYphTkF_qN7AUp6JnUeOjOY&5xagVHlA*B10H5WhOSPJ3|acW@aHQ2AMqh)SGnU zl~;84m0L#GpM+BV3Anf*Q#R*;@C4BstnLYFI2Ys`pb|DBV~q%tK2z`m^ntqXzWY?t z6_o=mEG><8ZfI2jI1~~IWO&&$TzhLU(o5A;XRlmTT6H?SexR*f=IIuCIaYsJA<9 zhHtt!vV%OVyTfW7x;h*mhs-Jt2etKyLny70Aa$K&&sNkt5>i33TRZPY+dDf(W)p%y zEs)(L6gM6W`e9J0Sq0Pq*HwVL2?uS}1rD=oYE@aMWwo^K1mn>_^?a!qjUR&PIMdpW zh=cB`v{kUCsKQuUT?0BVbb{Oib%Kl?Xvg}e*R3*6?S}jxkF*?)^-8tYwdG1jcCKZn zuE=%y{`>VO{`8OO-~8*3=wtu-w=~)w+cRRA@x6WsuB~IwQdlQg&Ljx@vV#{l5}P5O zg9*0@CunQKa>0AO{q1izQLF2~(7R=pP(c?K8v6OQE?>TEw&N>b`HE#;IY$$_fUS77 z!LsNIr8_t3g~yv^7kux=#{b^5uAa_j^LTT6BT6^)8EzSUF#I9?^+*jcRGS$;H=Op` zUNmS1f)W;MY0MdKeUo~OY~M8is?*^4PGiSyor#AURgosMN{{WG)kBkAoy~I%IMJqA zYX^~QyVi6W5&(vX{Gdk+WKU8X+64cC;JF-yh01CQR zhNgncTv`(5u1byap*m_FIk+>;m4SL`w*#kw5rm+puUFEO)NJaLzVGPH2%Uq`;2Cf@ zge~4()aiq8rYfOv=v~HRPquc=Fx)=8C5mY|>dFrm@#1VVlHT(kUWDgGvt=H>HAlfS z;?Z#;Br--5bX78^2?_*(rGvzZs0&V?PH9TW8Q-DR3E`g2z5T}_EC_#BYV*X<43Y~J z?^>1NwQ-^8Y^fULNN8IwH5x#1aL7lfgJi5}J#Dl?jKQVYITVqTyrIRi#TSE-luvZ0 zu5_oYb(q9@etK8SL86D=@<#p4&;1$w?f>bY=nEhJlm?TrB_OQT6Phqu<9L@-J0h(= z90o^dJ?_x^r6>HBYaKT=R>6XPZVV$Q^>%(;REB4Fq)^93EOUaL)*ZsEi1J`yzyN zBgHj00h1d~i|X89ZCY1X!G;YJ;(Pz|jUi#sRfLB5W_JP{HlvG{2%S~JZ=*AUQyG{LsGr=v`zB*!jof;{9;Gi9 zQlWSkg5JiY9B?Yr#Ue;6%2x0C!5`FfpZNE>cI~DS?L{`wjpc2nqg%fdM&rKhzWnCKz3Ef`Nirs4sgh3Pio5;j*z^olvKMsn0#sV=5 zbBAF9!3_e3Vs(|ab{Gc?@I9o?v%=F+m}}0jZvttXjq;qX)Vd1iPLkQeOMm*)pEk~Q z^H_D~S30=VI5v3(p&&wA<^^NL`mzH~?bzcu92sUEGGk4rbGygEcx3H&Od>#w7Nuse zP~KSr2=1j^sVWG;*!+D53z|qO)$U+hw{G25lnzzE(=CtGW}(}xDeId~7w@~M`%drc zi(mYrAxy$kDj{&9wnjMPg0nB{R9T%_C|`A%Zp4kQMx|cLDy`0M>yP}YAJM=3m%pa= zi-$JWrvIgrEU3yNhF)?)DU+3;^mUd9#yi=2NK%;7>+1qHqpJUu9dKy_zjNU)j)dO& zIC8$9?6Q9D@m?mg=_NtIaD`LKL0Lpyeqy^vTY-Yw=s3D*IUN-_DdypxBPYMHfYl5Z15B4m3%y!17Q+}%R9XF(9eG72 ziXL-OYeed}nrU3tx~Nodzi>%!+1*!i{ia5QDn+KPy*ja()?6$Y zEdJP$Vl=DkQ{IFwCJ*Rh(*;?gqCZT4_p#UwHQCvKl_`n|b0}su!%xPPc-}ImVe;T@ zfXr0O)3$^~G;C8V&c)-g$gmPN7fc$Wy2V11tu0f5+i~cpQr$kjWzqmNN6lKA&lZ7y zf`qLrjfUG=u0p6h?`Eo^O55AJdgr^J)<=KiH%Eq2w8nh%^)hUJ4t1qdCvAud0J5?KP!j3P**(4eolo15VW0WzXRZF!dbsgSrYpn{CuWQb zHd48?rhgpDN-_Z%2=$PJ&pFi({Xq;?p4Bc@=AlRaRyNS|@JNro=25-o{s;7%ANi;y zv%EK)b*W8L9j6QBHCn((4HcOsk*w-9CnVL4BAV)0l={pU4i9zq{5cJ`w{`oam-VJU`+gn2^t^ue-+VY^yeLJ= zM?q$>ELG$KEoQ55kMLciR;y}h-t=g`w6FsMfq{j$njXzG%nQ3;I5%n}=}-W-Q%uIz zrwOA&NU>fnbpG^dwPZnQA%a2cXdh>p0#jhyvs=M=2sts*whpW~mhd$X-9K#|^`SmS zu3widY)UyW&LEH28W+-HCXnv-K5H=@_(w~r_Y0knbNlR`RS%GGFtpNlDs}pUb%O)s zeIXlJYQ384JHG3?b?5NVB7m#cZ)#^}+uB@bQ5N)aw97#Umc$yyotB*P6UAB}&nU)W zT&Hs-HRyL_j_UwJJ1TUzoEzHxws*c$JL7GA`SV{=IbUc&PkF1;3kJjxXrAdfS{juw zDr1c}$A@~}DAPQWg@cQ&fv(mIy)eC_<2cf6vTsg;c$0{jj5ZF4q!EXUKvOV|AtTF+ z0gYPnu09pon9B~xWYIN3|I zR){y(m8vBza6=FDvM0!C+5%d!15EptEoZj3?A*OmoE`*1$tcSS|}BFr6uMtdjn@DCdXu*Jn;}x>x{1<-kvx%MBa0JpecmDmagv& zp{;SA#oE;bX^pj8wybQYdbLnaZ+M*7V4$qvfZ0QnXh6Et+spPqlB!4$7FSv&v_MWZ zN2W2}v6ejt*RH9^X*a#n*4Zu1s)f)X4F`1exTE_op3|w(j$Z!iiy9=U85T3n)^Dm; zdsCpFEKMs%i5Mki$amnzO88t(7s7o^#=3EEpxL<4CsVS~><{|SL4B~_ve4*{8rC8t zuSlBkOzk|hSGIlH9}(s!rhC-E?_6%cvu|?M4osYT@{sc6y`b8@<0+gL5!L2vJgW03 zjq7a+UESW;Aru}bdYIwwNN~rYck68;F+`fk?Ks)k7=p0S*pug4f+Lcixw0T z=PJ^XQWjr_7Ag;VpZQ(rS!bggF7LuKPZNHp+YL2+u>_$ZCo zG@_R_q3NiwLn#86lBmH@895wVXnK76KwG1s8V-wqOB?QnR5o%Fs|O774of=l#S@v$ z2B{IQ46GX|8;JTyGFyzA6@bIrW%2*V)_Xu(ew|gG`{sP_i7EW@IlduiO z0b?v0q@F_qs6>#3xYtb;Fkkhxn@lE>kk9;IJ5TTz& z#}H7wP*Rzu10^9t-YCDqKQc#+Yn*AC?MwBXUiDq~YyWMBbpEL`%3mz$p-=o{5MD&j zsy}uXYMzt@9YY|cp~U@guLaH{${Z7nU%~)6nrU#0XM>3<7~CQ~Qm^Zy<*v2s7ln$Z zD4s5|8u>*qHi0+_HaoU(B-wnU62R~M2li_?7&@RN+xV2g7_W!+N#6|2Y{g_1j!yQz9hEQNH)@crrg(t9VariY)?e9>NTZEt3jz%e3X zim*Jmz3H46(;f$$lu4yhGktHWlhX%IfiGod>HmBGKFy8Iu#>lID{W+{0;_pEQgJlU zIurV<9Sr|qF~!{A zv17+v4t)LevuftcT5c_S6A|123O$w>he63c{N*_>YKKUgON2~&aRmEyZ~S6Go1Ko9 z*Ou*@*SUPfE;gKmr10=nQbQtn@!>K}IaI6GY{ZJuLT__NRs8S<1GTD6Pc48^Kqh6v zpV>XucZqp70)W+FOa2^%awRMzx=LgZY#6#r1gu)YWLQeMJh)adSe65hHd{?~ceaG} z`%1H}gNG03nXf(T5+geeVvf$RXexlYnMfE+Rn4TDQXSY#)sAsNy&vAlnE^5D>mVo4o1%AR@Ume+`FV(UUHkh?FZkX z>B>U0&6zI#%^CgKhd->9dege?^?b=nAE7(U^>Wb6L;0FT#h|Pl&4bxYgCU1$XzmoV z43k5d$tX9?Xw)5QUMlK~4TigC+L=n7OkRUrQKJlpO;~p{nbulMz`*M4bbRj#M&P#W zL9$7{68OD$lqbc86s-1ERa_tY;MK2wwKlgl_0*G3MMB=p_6s z{e8l+{QO>i{SSEfnO}URj$Xo#c$0)JB{`9laRsn0aD!5Y^v$JTN*hxYO=IOB^|9-wYtsF`=Qy+jOAk+-O>zC(J3CriU-t>N({8Ij9GDPCFhlaUmMK_?g09iE zYuCc5<9VrY>ifn<9jZ_Y2Ev{@Ooia>#}I8GO)^|7jTUp2s(~=6UT+r{w4rFkWcYsO z=A32Ks^%V1zFixJ*}TjIj!@u16=5yc8|D`gY(`KH*tm$bQKrRm2!=kx!enh}41>b} z{43;!IV%r9ie#Y!dIzJ|FcgdG1|aU%)-@eIa!}V_cT|sk<*Qa8;Dpx)C1?f46$i7t+(YUc|YaM%~a zbGCtm3ov3c{3cG?dL11G5^@#26en;fM3F12;SyluY?Wna5ZLt$p*Ma(OMStZHX_XO}c; zB5tVLG|N#o&CSWmXY$G~0_A~n$_u)Z(`Y)P($VN8j-;A5iNhhjpde)|H3P>8XGBDeGn(SwEoOwYG{JBF@BvJNBHQ zg(e@td!E&#PcX;H2mH!>7GKZttYm4Gguy_oOUvGPlin!IZt>ai_4A0xIN%}X)i6_; zGE*4yxZZ|2M8=*>MPQR~(i95X>G#xFYPnTkuGXwd$xNCbR9r;Bqm-%OScxuS`Q(MP zNhdEK0qd+`wOApU@_C-^< zXau-iw6=RN+i-?Xu%)volo_fTVHrPyJYYdxXTlh$din96$JN0Nw`-0oV!%3v~h9qw5Y-1AO!BE$C?}kq7=96*2B!39(I)KUWE!ye9C zB&|}~6R;4De6CRvwNGO_NX~5tjYj4VzehA*n_k0fowLjI# zlP6Wj&ODxIDHY!>W1?&^51voY2gy}S9_^?h(J>eqw-_*Ug_c^S9@^eyQ{BD_nX(Ga zC3?uRFyExFa&D$(X0GQSIjUs_fj7IVk0;unYj^{9b5k>Dy?}ipGRqa@Dkz}g!Jr^$4Cgc~WK>&fYO~W;yVF)JUowN)9-fTuvnh5X*3obG+bNRrPX43$wFEXmPnqh_yy`~(J%~R#^ZzIu~%`En2p}AZ( zFtZ6P;6&q@#-*I{rCvR19jFz$Byds z#q&CR_^|fv+oz|#`h*tD=Fw54Q<}}Fx0o20+bv6MO#R%jeZomXb{3vLiV}r#(bqzA zgX{66#dPL?&w(-O^b9CN7Uf?#_zZS)n2-o%a+>7EI<)_wzU9@g)W+GTbmA)~RO!W$ zEY=^{C)pZu(sa%FoH$|ASu!KJU}q;vdngE$#~=sHNm9O6OGN|sP-b`@1kz%0Nm(k1 z=|t_!P@Ps$=c-xFZ$6?gox7k;d!_@0bycnb>`!&2e9c|Rm1@I{q0x{T!h$BKl{mXC znwGIZK_bgx`kd=k!V3v(2?GwfOZ1G4JS*-k_n$BBcf!T?(xy@jLT=jGd#oyUpASs; zy#Tw@JjN&Xh;t=SEfKnSA!ddE=3KK*0-liq9~Wh^SiAiV1JrzvgC3p5Tu#k$Ra4w$ z5Vu48k4&w^C`wL~!Az}6Mcti_=8dXK*{sU_p{l*E4it;JskNjQ_FB1&jvhRqm%QqA z`nx~*L*q&-<$A!1zDe$a>r3`XWH}ycCF?;(2VEkbe;;R+Fpq`=%&{Ae>*=ST_TX$C zW6Q}z{Zd9d-JT|+iQfF(@6?wb`kY;qxes)DC=oCqu-V3lHhTT_jQo*jPQSK zsy`SxBeaW4Z(unY7JeR?WhgS!sZB|X^@=9IWMsW`vKCourC;o9DPJjj0~yGU?l_P< zg9&M1MvuA22;M~%Q_5YM5$+UXE1yBfJfygud31MEx-#sWyLRu(UuIs?31qFo#s))Y z2IYKE5oJV$6PC@(Aw>5P!M+k1EY|Q)S?%fqC=*D(%s);S0Mr~TSqF6wm=h?tX3A&y zYK+TZOI52{nwV!-Z8dadd()JU_ONf9kvX7t?T4zjb)g_dfKFdGUbrIgpz_LdIpKcoTNzBqz~gf*mC z78cwqve50`j&ie{mNUVZa&>jZ5Cqu`L5r%!j@ovpo_N9n1BZDYTFWrbKt>}CbC#G3 zL^>&ul5NV1$UtjVIlh04jP^{$dR6D=V_lq$wV|B0l+`3x)@Y23b3wyJH+G$e;V{e< zLxp4yhM;HXqc)2_BbXo3QO`}m>;=yE1|U~nd$^dXftNUoX+$sm{BucDz{Yo*ClOxB zyzJNq47#U`iXT7?Gm$~l3#5pdf)X!8EowPc^rj*&gYP5C@^E>LG(qpwF@8Z!K>Uo; zV71ot;O&cFey@s4OZu~q{)LA?C&QtZD^2C*b8Tm)%1p*u8_#q&Thy^~RoCT9sx77( zb+&ca9k=U$`0*dq>CgO=KKYj)(aJ1r-Zc5mCSoF-YxW5i?V*tI&Z zw>zDXMDlql)jYtXz|4->jI*qmReQRLc0Wo2@jfvRHsJakBxkzo1usy)Kk%?B3L0Q} zkUsHaS5VlDf!v%+rM8cjh0S6`IIX8f*PfY%X*qG3176< zIwc(#85zHY2?KS&6-CVGSR9NbJ@5I?)8{_-d2{d>bMd%ptsX=HfjPKuRpu&-F6&sk z&g7!TK4U)Zen;6Hji0R6SN7|^7v86jfBfTKqlw{77n(K3VM)+KH;ij7=D|S2^@6{$ zj+xu!&@lZ486)5Y?Cn`AnXIm9t~vwx(}kvV(u*Z+k+IclIz1TaJi>(4y4qlg1^8bf z{(;jr1+Z`CMkTWdvL|>z2BFzt5lm0rghVn+9R&}t#?+7v+NjjE zeQigp&6c*iJNlWQ``5brzN32A_xyVeMjh)duGuT#pKGDJ3kE?vsF@5&of6&2B!5hKpDeZXVZS%P>&}(B@#yrNsy#}GOJR>fX zz$C_U94oB;C`h6pU)yYG(Cz!Pmf%e_vZ^n)w7jyeQ%^sw*3yc4Z3ux7pEyx)rU@EQ zi2Ti%%QLYtFp0sI7{*=$EJst!>A6W13 zn-D@P=iSNbGMZ8i*(E>I-suKxf%i;uz#b)WIvjMfLZTl%6NB@y7t=TdKgti3E@|&m zTEU11li>bB=P>z~@~qzv6bN@n7ph=n!Jj5G74yZwbdPN6U@K3_#|h1~)5Q$>;1ur0 z`U$8O?klJ}80;!~;D}?&1!YgUtPUeWm9jd+s0U96tlM|SN9ITAYH1_b(_mt8qi_#h zo>O8G%xR!R|H4o$B>Xj}Lx&IP)TvWJirJs1;HE{d)3IpZ>Y)RA$s1p%zxv}p)wnnG zA!SxS!P{i>Sb^lY=AufuvTizltDboBgzqsyi)ZG}TAWEWy#y>0LB~ZwIhK~^!a`@B z8CX`30|Ri01tr7W=8Ueb<|@))8BUZLj2hQW}`}UAq|^Th!i%3PNEc z9#Gaiqc?r`ck0e>xl6zBi~rf_e~kb`$~(=dIO%JxSklo-Lx+{qzVSl!$wKq)j+XbY zXj;zc^!A1}W_^`vHLdqUz~Z#>rX=mphx*#-DYH~au$n*(nK4Tg>+^}pxVtOFIsa_|P^ z8gIMv4xKrBMrY1luxV{~r)LskHBY}Oo|s6VVEbOot82Fy0--F?j4}^^LQqB7$g79gdr6~;$`NxAIF$7W!`Tq-8&bA^A zg~##@7#9M5DYEURUh!g5%D1^mD*b>QGZ zyVc~IH4SnL_2x6RAU96YF3;Jvyh_Kd-M1!I5-H4OLJJ+E$eG=Rg2E^F4-G7sNe~u; zHa?n5IRAoq6vfYyJ;A5qT1sfeGgO4rOQ)pmPZR~Yav*EAZ2 z48#(>Fx}}&B0h@Q1Uh6hqf5>xCb`hob|}U8GfKZ$Y8g5NcoNNuVi3F|81;DGm7=kq zkhX_%%9)H$V`eRbBMq|DR#|mvTX4DU4956OD31x4xfhiLx#j;f`sIj{^nO9`)8J@a`AEGP9=f=WoWo1bhFJ5o} zdE>j@u17xcDLwt~1v` zMX)Rh&P_CFngR3laQ!^1N>3J6oac+`&{Vi?pPtPObb70+S+V8yy4~-ow!GwKNEbYp zAPHgEn9$5-xaf+ZoSH!Ny|jabbEmS8@3AmnWVxr3F+oHrOG~lGuSEqk31n%$(&uwv zhUvaHiW$9+1E}enJ&q7BFuhSCn(}AV7?Z-n-Yy}3m)esFcyl>XAhXTdatlo$=nUP- zP>^3ZG2pP2X^Zy7Cn%mFr%$d_ZvDPMZX%@=XOP;iM@MV*7p-4tArrOa&)ua1W-nXvBBae9k z9?!$fBB${n%zn9xq;WVdMbFCn`Xgr4=*| znOUw<*1Vk8)tzm#vB6OQvMGCJg@E;-1<&h`h9O-iFuNp1Ag+MYLSHX@>5H`S>_u%} z+4S%@#fnQta~?CsbSRN{Uf@%734S^N9%NS&Z zg0@!33_)CcRK@WbGN}n=Dtx(UWWn`105C&p<CL_!se-=Jx|a~q*{(PN?M$k z_)K*3(Bl~8#;TPnL|;uuQ&uBB3*z zx>@8=xTr7@%F^iL7d7Sz7`(Uf;#9-YN7BbH>I9D!>lt&FCcFIP0Km9;tUJE>^S z^C}Ms#zIFSf#kK3)};`yC#mrJ7164$Ki&hMUXm) zDPSc)%pmP3=#-}pz}}#hBB+Jv*c2|wg0Vn$M)^=s7iC5?r?V(rmNoBn)SAz=QY`9l zt*N_b4TId<{T&sX73InqbBHR%tQK&mhEr9PSJ`C8y!xep9}y&cl4gR=4@67TE3z3K zyWyBxtreF#nWr!M`n%bgcE$s%a`v`&G;HIkmsex|n$8V(bo{;->4k58y*~f%e?{|7 z-|o>PbF1h$VyP8eKrn`c&fqc=4v{61!tnz{qyoRV7<; z7Z41tP^&1*urgQ}9md{3xSRsK71e+%4AT$&xsb(Rd z@^f^PF^R<~r&JCO#m1Ki>afRAgjmdiHC8m*wBZD$Ozi6U?<3uSY$`G51I`f)OT)=b zM>R+ik)gQkfYU|5%7Hfra}6>#f|u9!9tjgZcUrGtkA_F0tgqE)%~8u60$B{l#-5N& zo@S=%PC8m%T2_9B$;Q+(esuXNZeY>j%&J%|>i##rUZ48JCp6#eS#Lgc!uSZl1VNU_ z!jT^84^X$SgYh!p4IG$jH3wR_fn>rCu%_@^P)GUMLi?4~u#(fDoL74`HU$L1#oTBn zM3uqEP+`UHY>><9QIGl*wT;4VrK%nm1+L~1DmD^X@?6tiFg(pvs7`{Mahf?!u^~0L z$gruh_FBUwfIdM^DQ4^qe0&d60O@u2;^Q98F$kS#46L~*@8BC1hZR$nwfBe91Lk;6 z{E7r}vlv(usaqJz%BdOT+r3NPsDOz~lXno-E73+Bo64}*+0mgwQHPoh?Jrifwl#AX zXlK;bq<{hEOj*je**M@M1k8z4%M?{X)W05dq#YnkMh$FkkC$Y^eAKngt9rxR-li*; zuc-adqrU!Wtz-|mTW`D7f#9hxJ?aihbKj~?wy$XU?wj?tpZpO$`fvYtZC$t|Fm~fZ zfYRxn?J76~XIn4JpKZgXytPyM_c+mhL!Dc0W%mJP~ zecC3O*B?2mVXv>N7cZE*MaG33i+LRM!r~$)Gw(9*=&UW6hB9K~5hr6+L2ZFe4(-Eb zuL+mU0W`Y{=n%igGm<=WG_Lv+~yivb&(=8AwlasLv4 zmH@LtF4~Jn&$GRx(?lc#>X=Lhy7PI@*Nt}`(PNK4qGwN@)p$UJWT`gACJ%2?|Jm?5+PL0z|M&x4e)gQP z96(cupQ%IQ%rsgSK_WnVh>SI*zUJC6oloX6By$NKkCaWRq`m*$Z`QMCp3#Xfe?`;I zz>Aq@;ZhI{C>(J(Nhb>xz^KSpMRuZ_Z@od|^F0lB z1`bvh5JWd?K6d+>&L!_8<0(IlMUt79(J_4 zw(d^l#?>o66qf6CEg)7-CaM=v%pkLd2nu<07R*fnizgEYZ}TQ2E8g0pNoEF+nqhmg zQV)m+9eW$w#)grJr(D~7svGaVQ|+A{oj-fdQveV@;Ty4uQd&ti7Eb)&Uha(%s}Fa- zfC}I^S@t1b>RT)SBgen2&_$%cRECf_W3wkm2cc-Q1|ugKH64 z8`nXj$Zs1#om6IxvlEw)$O_$h`yKkym%gM+7cW~@nR~#v6xf+*BlC$yc|;ZS#?{Dv z2a}OTaM9RbXnqX`I#6nuxJ^mEd2m&|;YcU1ZmK=cYDgoXRJ8^g!j33_eC@GERj*W? zDenxQn;%XX7S9iYG=x*WUi#9PTJ_1=kWrCYAob(aR}?Q}E&%Ya}9qZ(xb^Leb$?QRww zK{BxE93#%)w8Jj&$VixD^0$-LbMo=9p&LwES?zSTRnJ!f^LRM6jn(Vk{3dN&x}ZnD z`k2%H(lq0;a12k)$_~5+Sqj{)oC8)Lm+kC8)7(V0m6G1~^FO1neE8!!{rN{VM704W z2)IO6j{pneR4ce;??GZ*YYJ0kO9gjNlL#SQN|%Hj04O@6g9Y|wG8ce7B}*q5c;Mzs zg@F_FaBLEA1?&iSVH@m{<%E;l9U`(Mm_E_5z)Wx&7_l52Ah2ik4FU+DaPUY3wC8%r z)~&Jtp zE?&6kWDOp`sI=h}xWoM+N zQPx1a9eIj5U77YZt#U9)MJkiQ)L7%v^KMXQG|{>4NS6mgo$L11QBlK8M*sGwe?p~P zPXFbX|Et#>Yj()kP&~BU^^QEu^!pdQ;05}zfAL=Z@rVCjAN$zfo5GT?^7K7>c~;-R z=_0F5iYGB}r%q_*oOR8Yo)L-qWcAHWkB=c zC|F^5K9>`kxi$bZKwl5~hK1DW_VmhEzCu6t)8C`xH#hZ#&t2Bf{M7%bwbnizYAx%r zhd-xd^`@?`Rkg2}SGC_)DWBCveE0GN3!LHmH$}-a4z7~^&lnVS>Kqz8c0rCDA3l$9 z4`WDVS>=Frz!_l-W;oOlGQ8PLo6~`VUlpT*(ae5-WD|T}x3EyBl+kRtqNR%iXA#Cf ztaK4)2cx@peg5 zWBTfYpV#R}PiO#k=xmUI@`Azl0T@a2iKA3P4!EC1Mi`zCy83}1X5uOM3*=5%{gy_i zoOU`k21(g)eB|+PX7))(%}v@E^LM<`;n> zG2{&OWw-gn=Y!Y8jNsh&0M!U2T|C~ysLCRkN6g0<_f}LX*VOOzHAH8NeE1&B0X0W`lO>T|rNM0k?0HZ)$zp&A$ZJi*% z1$`gVoqO+lq2BUcuhIF9zCQFjf27T`7q!}7=*C<{HY|x>3;W0x-9xG|c-#(Rx;Zr<54}%u!ai;?EfT2RU&~!GPX_ z1s7RQsbF9k{OI@%H|qRXpYdtzl4U{bH&|?&$j2ZwT|-zJ$Q1Kmo}dt#7(xD)R&A)y zt!UuG2e3R4T&vlVc~+gpK%Hz?m)lo$=kssTn}7D-=)qt84L$amuV}5ZtY)sM!)u3h z;p#=pF}p(vX2sAm#78>;WzC!hub~sQy1b$xEc{|#ThE+T4Q4YZKGMF6iO2!wn82Ec zl~raqQ>hl6P}t0|6BW2JsAx8OigWv=Zd@LE4DHqhpm7MvqxJ^iojzNinkUD#& zF${<5PsY0Yo_loV%4J=;bV;pdfH>e)Ej3#@@#G1uEU!3F*k%iP0qzE)j$`vXH<}GD_y?$t)4jwq5#~**(8D4LPVdJXl z31pUFf7~C|;BY$9^6{pI+e6JKW%}g0&>iVop`=rzu3r6)w=(6VPk-$1l^@L1K;kGH zoSZ%X8WR-<7D?M%J9_I|zC*2MLx28ff2mrdX5U3yhmxSh%sdQLhwDh!V!k3BNh@=N z3^$dGCYZkb<{!4ai~)8s5KI=a!|qR1uPv)mK?-8iz~Wp39XYzs1Hh+GpVn+jNm$Wn z8f?Fl2OU6odJ{6AW@|~^Arh}yZMSz+ub~dmGP$jHZBx1NScfx3-B@etI#pEydzo>n z2TFl)4NL5rukVwJg_A(iU3RoJ-bq!_POAB2GUz`Vg_$wt*-%_0SVT2SpT=Vf7=mN4 zEO*L^ZWKeUsEtNVq|^nQwkh2>jgikK%fuZ!7*4uA2Xea9yqpe6QH=; zQ3Y^Kgk2O80G3NxjZ0Gv<^#R{fw$}U>tCyn{?ae&iO+mhE7fJq+f%Kqu7nXNPW)I- z`ToSR>d@iC{`{A(T=BJ_rM-G!pUeC!=Pr1ffWSc8e-1ZiF;!PH(?`aGk;$|Bj~>$I z)>XB)w+to|#I23!O@ia|LW@GqWi{aZkc2y%Gbl}G4q%uP;pr1kdE+F4WWUpOL!r{B z>N3`7R(1$vQY*1`I%+H}saPw+9@Mp+EqAq+%XN1i6X=`3TsBPVTCq8~$0#gx2mN3q z8bMs^h?UD~)$3YcTi0WcJ!UM3jMtWP#e({?fwNx@wKU3=r&;VD5Xy&Mr<=qSG@A z1J8>}U7Puwu4$%ih@+*NCc}yLFYVLj<~4gTdYwV;X2i;+eK6a=b6}~pU8>GK`MlRWwPWrC^6b~hHMQfkT`u$o0nvr*79sRRE629d`U-&B^}I^ zv^LF{;SP|>NiiYP7Q%$S4e4Em#nFuDe&S9J>88o6MO)K5dZWF=0Cl&k+i$;J&wTBq zAC{;7eLBS%Lb`Z#SOCA{jp7CxuXootXAiIPjJEB^d!i2Vl+xU7KR#;W=?+zzg!Zfv3R)5$F(#z5Hn)hLhi1g43mayiH*UdaaG=WD)2~;Y1;gZQrm^Y+k zFPFo;j;B09a*S~UGBa@#(5wUF$CcRPg!!y?x?Oe0XzJ5Ko%*xzW#D{|VFCBlGkNd; zM?;tScCrEAtW=yuHBokLcLHB8lkqx%gbO!|fLv__Is%6r8_`y8$l*-JZnW5eW{UbJ zbEfN-+Ii}!r$Y7uU1KgE_Ju;puSd{A)s-=ecU8+Nt-@Bx)8MY}~;%dN21sRV2!60#NVi&)MFTF2Dp zvV-D62bR{feRW&S`jW6uxw4pP(yVKfM%x0uT2|FU-MHW=AI6Omb%f{YXORzfXvu__ zgApSOnnmf0eUNAj>3Q$IR=iG{ReKNl^fQ@P-15&B$p?%@O^Uw0+R%d^|D^J{in$rC z>#a%FQ6?9hONhdSeAR;bWJ11xMUVvctQZsTJe*q1EnS>_5e_R=N^aNJ%0ff_l$oyiZja>LEbvDzc6?s?<_rNqo@w|@OMRhu_7n1tbU8Y_^g+#sQ;!=ZxmYg8JhK{Kz{ zG=phBnW&35lnZ9`PNu9WONsHEs+9PS4!F&bSpW}7%MX-J1S|CEouO3m`$9;K!8me99CdQr|iU|3=uoweax;^IMqqFxiNsBhYugp z_O;DGA+U~6D24dgL)z|bdjw`M#?CwpBNtIVm|^aa z*L;MmNKSor;DzC81LJ#wrOTQKYyt)&oxG@&)LD!)DCYFidta=hx7?sV z`t{#YaXfd2sdaE&{mD=}JKG`fK%z`tE(_rW@Hx1>K;tMn(ce zJY*ECJNRWxIjpX%x*JNctBz!=EQ^LToW`5WU*naprAn5HzyK# zoqEMylaP!N3cT~K+x5Wry+^2*vE9<(TK^F6{ ziRy*CI-8gDn)_d^t2SMLm75Fk;7B~Iy(OgA%!`h_&%{xF7$EXH-t_Id{`v#@)&KfyDivEA z48~Rl?De`@TCQm{MzsLOHp+`t$0o+g2vk8lpJ637CbA#{z3Mfu(W_tcD*f^={gTUs zd1j{CsHpsSto`+(ZeDMyJRPXi?W#(9eN4N$tXa0C-e{o=3$skwIrlu3-Dx46a&l7= zKvIKT7$@$-WM0XOjT>c>TAa(Wdovh=OU~y^3E+U0tUek}fl%6obUYCLNXdhs8rvUP z7tj3p#6pIn7qACF^E7Hea{w1%Mjgz44{Yodpo07eQ#nBo_^{nKdVB4Imsx*N4F8_ zp2Yi#z&o!IGPbh$`TDeZjMGgf5R!9gYy4Ja>oncqUZaA#(egF6ClYjr9 z&VBlvdXug*D;uOx?KJCde39{)YT*uk_s1drY9>-Z-1~_}OGPdxi=^xl0#1t^KqY*blpIIMEw|bxO00W=1LrcX3_< z*4+o+y+~ezya2(eke_OMv#UaZ)@kUJFm8gz5jV&w+MEr`tQkOFJ4|mG^rbEiYJ`yF zUU1Jn`rwcJGkx%%f6(p!J8r*Sn@^ur3u%@7LixC9RYiogUQ-|ck+-G)FGLGeUPcDa_z z>W(|^(AABr7Q_46(_eF`xLdAxjx>xOrR{f+&B_FYX(pQ$}mxp0S>Xqk&MfAZU?EVI*9xmGo_If0HhszNnKQ z|GG0T?k8)D{X<#FG|3$B9lX;r&d^~X5ThDAQ+D3<9BeVqOl@AH@l2U~L%l*(Z51`n zR5YbL&Bene8dCTWOFH^{WMP~Y`2YxtC41)yy>^Gstpz6v(0q!8AP}%e31Sz+df|27 zzevgpJZLspK8H9M-3l1mA@%0sdwmQ?!}vl*bX7?EIAPK>Jexh0Bk)H0#&Ev%wXfC3 zKlZo2(LeG-Kddi&>=Wvoy`YssPI)>BVoqscH`)p6oO2|45zni;V=^jd_2L)5SkIn% zMq5`ltYScx(C_!OzPjeY+jJUIzU4|GgTMmIt?8(EKqeIeR0s}SkTC;-EFC&<0a(KX zH4FViod|@;WQs~jST>eJwg;&YJS|~SFsH*zAyg1l7RW|11_;KOVK&q15fOwml_Bzm zIGGHH&Za7ZYYfLG%LXkixL)ynjK3{>YLUg1%SBs14F;ni1+%qfxnMd~wDBp$9E5GN z6~=gQ1seAU+QHenQMImAFSpSC>kc~*Up{+IFMHk#^yF6_RjyK0tyVF!-^Lv7W(Fk) zHUpTH$3r}R#=tvf95HA`>$D`1P3F4PY8lXmyh|Wwga~;VV^+*oMs5EzV#9TFzsx-( zb7L{*a?Wh|c`|9T6syTbo?VyOMNVk~if0i6UI#}o;A)}E$n#>(k@ZdX4UY+GNxfAS zuYoj+HzL8;1Jgxc3zdMOg~%)iYh)<9TB&&yW%KIQV4>CK6+d z_}~vy0hX~2fa&f$u^|w=6XP1SxIk2&Fg6&Wk(6bn7?ef;Asg=p4Ax_nQG^XvaW$=8!Bb?1srEy3=%F z{>4B4QC)uQgdTtBvuYqvHy*2A0@xpFmd85}LS0SMAt!S|neklxNzI4glkPweIxZC7AWqqXA?Z)nO4s9k2jeK||z00Pg8v6-z-A z3t>4-E2DvG0F|lRGpf^`1-F1mNtaDIV^(BERwO{5TvLOobnr%GMDTc+mz)JFB{Er; zMM$K0b}!hj!+#@ zs1#|D*je)wK~%bQ*=;s^!Nk$TYjZoDO%4}~ZYx0(N293y6wGQ%HsFBNYgJTuSQvlw zdsnblY zHgdUi@sbZ1GC+hPJvCt=yx?WYnj)}P-~y6SdXPSf6JSDH!DC>8IGQHYp(dG$wcc9U zB|ihyE@*UBYjs}-d`R##OYTGmp&U*~lVDOZ%AzY4WP+g5=4UuT(*c&csC#6x>g9_X zmV}Z<^oFWUE8$g~7hYm`e zoo&7P6|YcvYg<>pep*Y&ao~6xnZVu5OvOe;ncQ3j+Q@^kiH%pYIUPHCRF}?Oa1GB% z#z>0Cez>uml0!t~Cc5p;=V@Ll+Z-ba`U7yAZUGq*@|`!uEEs%xgadrVbxhDFCkRC+ z5HbRQUQ`LfP^MJQ5}d{!#{pQUET(i$B5#w~zzU{hKHQT6Zmx^Ky5>QV5hOf|GSOul zSxTfLU^D?=?M3&z$e$VQCzMWImbgenpq6+U26Gp5RFd8kS#mC?^?m!i;WjQ`F+YI! zG%ya$1g}8s(nk32g*i|TE3(@`W^Swl{tm;=bd7415VlW&Z-@SV1Zx?u0l;-5B=3M> z4A6;SC086Rq2F2F4KX>XVu}LUD!NmwTTkV}t#?e7E4isEs$VllPrj9DtOi*Ft zSDBlO*i6!CPtVN&GJyrbz$O{$j^`uST5Gy;@v?z>zNc1e__>du`VgE;C4I|1_h>$u z=yU({d9Ov*Aop=)b=AI?$(+G5Kan$4$ib$E=LCl^=e0**=iYh$g1Kd8>I~Zf3mpKj zU<4cQDJ8w985n^I>))k4SrApjXrwWLrI^z+c$_H@PfS53$M`f%fy`X(Qc>;fis6#o zt~X?CvSqK)Pyw)Bc2!))-21Sv(igu^dm`P}G_C^{&T~y!nXyuW7d)(lbFgEk(~)u+ zxK8{nr*fduPo_O*SY!?X1S8o-*Ql&HV;COSff)jTjE5al@iUDf2O`+dm;+Png}m0Y z6SZbVi{wqWM;en^!!B-?bp6f8_4HGZsX|FLovM||Xe*B_?$mlij1-JJeeL7qg*hDg zHf(&Z>-fz#YEmle%$YMG?Zoub`tBpA(FWb)$q+KHg7FismD3U?HyTF)5T4r}%$iGZ zaOfT`GAfjV?h;v57^{H^&PF2(z~W)(j4_i9-Nf=VC<6eVvQaQ+ebEbF=!tuT-Dsl& zM)SQ~Pd~~UA6{jIWbA+ya8j!=Im;QN1A$Vga~~v0m;^xThq9$)3w1a{WSCCXaUp{6 zNhVm#sDc6~^10oSee8_uA%W7YsWTbs(7{74RW>hQRhHL;$^!N7~#_?R0H&<=2=nfMT~ZdvSF1*M759cCSnDWOnt?C5pQ zbZk;vuPR$+vViYQ@xi-rsL}je{S@@=TYmUR1SI()!ApMuV}%`{1lGk_9xu ztYoLh9->KsYtmfxdMLB05IFdjmY38U_8eRt@?(Z!>~aXUmQ^8Bbl|clLL}(YCHCo^ zk%yd3=7l!F$Q|Tf2FbzG5GS<0Uw{QdW>YX;)f|r1x>I2mfwL=f!Umzk2$SCrD=b<9-U$1! z(E19-n9wr}@CNcALCWsT(~<8-79Vqfekc@Jfb&3oppaE?iopGvsa)X+6SN7I!9C0i zHlJ6mRuvqf;V@7hn1JVSplLD0-wrFXOBX%h%BHgTpi{7D1WTJcnyW766HP`#HQ)|S zraEx&kX>gkoIK^Mk&Y6W5~UTG7-7Yy_h2l>lIY@5Dk8y*`wZah_rL#rdiK;Q{nO_@ zui>>Vy8uzLQS2T#a@d(schq;82WCTnapw_mHO{Vt_b#(=9jq&!^9v zHqUK7fG>oSrv0A1p;&wGe$V&nk*_?WPyNF`=;)Ed)|sC}5n+&*je$>LwQR}^Qpy-u zG#V`j9lF|-RjgeO9WWdYeFB*82^LmhPB!)2@x3)4ip@;cNWx%nJ(PVYeH>ih=M4wd zz{VR^H7UBA$xt&Yt7JDEtgO#gqp6*pP8fK1$%zVXXv8q0v$eCOVhNe)obMg$4RD;y z5jkr+3Lyf>+8)E7!z6_uSqxmnMaG8>{JW-4;QNN+euAbsd~a z-^WRiFrt#7Oi#pam-G>5G+V*`2Z0ik0QwZT+$?(W@ekOSFm=s%A%JrJ9JH^4$eg<_ zap47XMd-ypZuwx(!+oIIeq%! zhb?%P+THGiP^0>_$%+CF2Dq9vn;Zp8Ty_JIl}$fm(3I#tTi6r;kue~&yn*jfM>{i$ zB6Qp+t8kLGqB~R|`sXwRQ!(DPu*xW}X~L-qm>I+X3%hrY0~hx0)^~E}g$< zCtiY_2VYr$qmfN1H!ffDA~h}(6*9SgvZ-KD5X&cKBIjU0baGG*2zuJiV4?yjn`}7z8{SSSD=g_t%ufSBmxP-I!U67QM(K=?# z%@y*x=S45lk)ucS;Kv^{@o%!zRVkZMqgK~ux2@N_?zQ^j7r&^H@i|IgO1fd3 z1?8czggQ0Mcfu3PN`-P+FTL;OI(Y1;KJ+`kE0h;jY4l8opuw8X=(tiwmUY{0x9jwo zGwOERTA)b5kTLV5vz%uAsd807{)J}QiN^D+1|yn7b?tPzK1?!Ju^9QBEoA8og>~!A z6;2ojqOF}B}SI|A#~@12Gql>E z$sPWW9Tt-hk%N~p;RdaM$RD0KK~|dZ0_jUa~G5?)HK30x7^SeuLUQrG_c?U z!`oZfSCi(IV@T{4CT!WUCXNS&LxN^Vpg|<9Ebc0SD?^5r_z?*RBcdnLT;f2aRE_`^ zoNy5K3M4(m@A;Z%^TK7Ag+aPa=kGwZq!mVMy4xy^Mmn^AUDZre*S0V<$hZ>|-Y?29 z!y5rJL(0S{F&z;w0Ca~!ix^w{n~R205sZsYVPfqjV3J{BvK!{+*!e)COw_RMK- z$Xjo}O^^N47p&`zx++>qF0o_xG<7@~zrYfUa+fTQ#t6>>@jo-*S-_FFc=+fMz3vUK z*KhvTZ}||Y&_S9)4yFUf$r@%t4H|bkTHCj({*V@Z&dr_NIIm;-j_LfB(;7|sDm5ya zGN-yj7EAP1NpORVhbjFLYZE9XhBBmoD4ikN}156$9LC`p zOG|4$<-g}$59m)n{Kx9|y5^o?szEu}VP3A;^vKOptEukJb_fWOSZbh{pg@TQKImx? zN*o$hz$FCKAa-bg*={%ltw7gddIX&jerCJVwch{>5Q7bX_wk^=`+flfUid(;ItD>~ z6r6(0Y>%X8XEkxM!p#u$?Pvy3Hi~9YH&h^0D)0yCxX$KkHX4Bh+7CjTG`2=dHMKhf zwV_u`bDCxfnxJK0rqL7~e*F13eB-QYj1#H+OZ1pTGfGqxVuATsaW*#*N-dZCk{fO+mn?Hy-$8V)lnYKP&Nj)&kUh@J zo^q{vL&tBpK_^apJxbk%t{V70QX2$L0_kmc+^!QRP6Xm7tnM(U9YF7V*5x7(5esqy z%PxhgK@9-e=R$Ct71S|2dcujB;XX1aI8zNxb&pgbe;Zye*|1q(Y%#MB z192q(C$py0GtokmmLiyAi;Wk6Y(D&f*cxR@U-t{tl?vc+}GACCeb7t^& zpk`~I-tdMu>0^KYQO%|U^@j{30~^R)R-B;;GKo;>xl-JS`hDH`ygN4al8>&zos0d3Q*Lrj7}+lExqm2!f|LlAG7yJ5rpXOgUA3F72z8wO%VZ?H>a?PM@3GO6g%D z6{gM-@5^H5{V0Jr?RnVL_XI3%8aLOYG=qs+wN~tA%rz>7q3xvWZs9j&)SCsnb57HD zzUu)!^w2|k`s69C=E}CJH&r0cn7N*|{L93j&8Gqx77#b`H`LewtA)^dCCE-DBd5=U zb~l)506-5%Dwmf`Ff~8R8sUN2&I}-yQEz+4+x549`#0LSd^xo0`%zEar*h8eu`^y8 z7g3R6d1c8N3|JDM3GUAR)m2^HylPn(@D&@uRvG>tsI`AxFMZKV^mm_lPz$(0NC8E+ z6>AVNW;7mab#2XAO_&@AWp%yP(4gP5voKvCo3-ZirjD@KjvP6nnN9X`FBfu>1^-1~vj9IbwQN_~4pcriX#^we@A)eCsXx%s)MBhw9t!yiNDM z?zQ^;U;S@t4@xNTKnV_*bcOm;zP`OesSpJLdE3{XXw!_U=7L-|Nww z6y5d)tn>mYg-d*i6jV|mTa<%gq&Xr}1kvUyOc%@Q;qGmCxdB0;@t~%5IxzJwc znOR~NkT9A?_&SokwFU6`;7rO#RZ7u8%DHnx*9KZO4|2KDa?l{%#!LY$#^gqWzCOx= zktQQP<)Z%mFZ@S+zzB9p@8LQQ-`$Tc~k{~f;7A?5B0Kz*p=47UH zi3$%aGJ-WHtxYbXfh8bpkhdwn6l`nnSm}9L{ad~w1%vno`pz3N>Pq~018anZ{6DIQhtDpO6HE&wiuYB--Qg5>> zMp^Uug;hP~^Be*W+P8j0FMY`?^f!O~SIXz<(lWLr^}E3hh)k6Yxo`ivu5N7_2LkLT zs4#zO+|)A5VHCzfkS@<5C!>~-vxXHxisARyzV@~H(igs{PPen0(fHtxdTTe|e6yZ; z<{1ZV-q+8PvJX8+GH6Rt=Twe@m%~xe%%WVy!i+ukU2lK8{^(Es+`q?%5}i5LzOiQm z)O4l;H;<~p9nwh{`wB4bbA!*`(}3^{dRNQlN7<8{F{PN1o2n9(Tb;~6XR&m8=$uo2 zy8F(EShcP~wV};k&%tWSy&_=Q+CKPr`Zc0-QYr&e_m91w#-|cRg7+X$CLWfFvuTQi zfR(VQlzycbOLX|qPDk@8rRkyF742@6ICWIw+C z=+eT;U?l)b+9>_u&owOvj!~3Pc01iP5a!>i+SbL)|UsHL;!7_I@$s8&s z>my#k05DmGEwM<&DY;8I4?pw8eeP|wb^rbM>&svKl5rZ!f7tHeSd=VdFpI?MkF$o( zHX&$68+Q0r1PDsoeh(`x@Rq5^O>7-+Ro1yKZd}yb!F6Ys@B5J-Ql?SRhkp6jH0}>o zZ9+J`rd+wC_4UKr*zBm^o2XG+QLozu%G6>$QmxihzfTUv2A{cG`ofpISSOx*GKdMb zS`H|j>Xm9mZD zH#(E=blPsb5S#|nk%!F@!o%Ewj0tHS8)iU0u2r>#ZdfU}e!uto-mBmH-9HF3f6?)$ zQX~TlrJ<`3UWaB7Y=53nY=&@J8_$c+*@tT!^P&`Vc8WZ92q_QkC>4I6Fe&~VXecrG?mF;< zzvqk3mh8ejcbGD)^g{`oN-u5N-}C+7r*f;Q-~Wx@wnjYI6F}s6 zI8g;)brf2SF;L=z}90JBdt=4LKh;!qHc5lbhL_Ggm9U}v2XJ<=|dR}jM z%NtZ`R`o|8`j8sUmd>Bs(xJo0wV2BMEsSjog_#DBC{+!-G)q(mp+%QwF`G}PJa7_9 z370NkjW{jY6`k4;z@T?9Zk0-wgzbXmfhVN{FMW1*v?HrO1T$$c9ccgYL;8XDe?b5E z;3xF(CqLyTND`L}SsvYNu-t`%3TC}7-7(Mr1P{){J|xHvJ+>09!njYpsFuo_3?o9= ziWPSgjZ`6Uc*6KEMhJw@=}uvs1wC^8b*_dkUcPMZ;Nwp|p{>m=m1a42?oC3a3yy#z z*bt@{AVVluKpJN^P#!0}{@D3T)f@LK9mt*|bTg{Fn_;EuF2#q;OK4g)UINxlT57NS3opBjbY|I^C{8$MS%mRTnqQ*5&=+bH>By z0_Ya=9z}3=35bqy0(Ca*U6ibneh(WE6rD^14Jf)lxr}GRy4{XTBDZAS<%w)uPQrf2 zH{RK5Hg--%V^wMu4LWV(72tCHZpRX`x7-*8UpKG9_zvc>H{5W8&Ye5!5fuRXV3kzx z0>0^{o18H`^9)?8F!c^E2f}C}7n%-al3W`+Gn_xU?*5>wea&TOlt>QS$BuR}RY6iD zAft{t{5VfHj~>w5-}w&x`mg`GyPs^5mE~pQeLm62jtEwqs7xqesYS_4iDB(~aGG#h z8hb%se=-Sj#N3};N#$BSm{d%{fH)c5VA%2IQl?IimU=zlhnc{@ceYro>466x@O}UA zhd*qQM=(K}3Za4sBT)g*4P_(4$dqkCAtFfrm=H-LV~-_OdXE&W2zCilnKCHd%IjNS z^Ljn{$RoOX>59udW*V)A5fw$4J}|XN3%mUIesVZgf_xHR3`w&l`B{dpp`M_!x_a=S z8vj;idR>k&DFSC})N_Li+|!3{om!)5l0Svo%+R0Js?1h@e=^s#q&}j)@`N zd2$qy%A%|y|uOJw!YO!=@=r>G0!`*poLto*DN(m!yu?kGCV!k^Y6S<+gsOk;rw|u zR#vsKan1Au^K9nflm_^&*BikJ8dXoSBe+BmDZ%O%IrELsX657qLm*qkk8zfrT7-|e zPY8jytAKHE?Fr+6$m+}>O6>T#OlPVcTK8)~^nB-cewY5_PyWQM?tp_?1kGSHp!Eb} zgbF8XaJeDyx#J39F%k!%X0QPW_yG_`yvEPREZQH+HqX6UK=cz6Nseurgz2 zv3ZrSUEZ$}`GrAx2a;q~g>p{Y{c9@a%YnR{3$iR>HntoX|JE}L_>qU>r*n0OegE0Y z85}Yu0E7Qgxy(E)52>0!H*y9aLl6Kmg!G|&!Ui7&0u&C}*@W9(dH{wi|BLvrnD0TrHc6)-ymR>HxIX zhjZ|(f(n!HS;1XAS`=;3%}z$b1{)|E&%?2S(z{-CkAcTW9(iQ9t4du@Ff>Ln>>D^f zc<7+EHc=pjeZ_uZ6@1;Uqv~9>B6Az#YUUi1HN3d zyKEs-@sOy2hM7;tT&Cnq(VSgMRj>wRMBvWN$690=r^#3j8k{lAVp=YCSHc;tktI`T zoqJw#sQryrNPm7-cZgna0?j!fVsA6Z%PWCa-&C14;O zhcYxwU?i8CD8}jahWSP^;RxBC@jP&57-Vq(<@SA?wBB2DJB5$dJSM39e{`J(n51Qu zwom1*&OOsTkr^0r8p$9_1_4nK1O!<`R}ljS#I(9ef7ALay1MJH=&ox30|wNQHLa|O z5*A^IBXO94875EXP}PweCAsvE)P;+mQ6uBz{QzxO@iInQ}cQU$~c>p{-USUpoz zS$cVYuXVtani#VM@OP@CQj+W*=(L`pUYn>**|weAtb3HkS63>KQA(1wk?*&u4XHXu z2@gP=%~Gk>)xnP;17HU`=`560AtY)6Zx{w_Z8YtobI-Rw-hQX;M#`h7S3P+c6^sqc zbv80Qt?rg%4;V5Dr3ox_UYH1Go;HdHM%e6 zU~oW<+Rtp-Y$L-c!g^a%UvH-^9vl>W&qn}$&ygCkV*aQGoGTOOjY9N|2) zuef+0)m~I892S|A^3@?vQ(|y($REdERz*1aRW&@tt8(6i@AMa@meY4iN(@cMMI5tw zW-Y*TR4w#|PN&H>XVj4ag@J?ru6JK;Pd>HY*8Fsht7K$hqStKkf`tO-w=yUK2!cuK z3G4?iX1{3wMY?|(1$cek==M)bg4;*+IZUAYPL0RxQH`CK9q`^t=q5N(su9wxZ%`>D zx+>O^0jMyW?DoA>ebjQh^#eVkQyQKjJ{4_$4K zJ@hB5j!(L*vT&4wCYKaC%T}pXMH}nQx&*J+_I$(_YZ72L%y$eThZr5><{S*6~_ z6ele$jXGJo8NZ<*$AvP=DF7MKapQ9(R;Tus^@~SGHpLo;E!(ZGF9i zwrj_3D=+G@d|$?%-MYnUwMol&l&!aS(8fllZD?@0O^#I*X*2ABmL+;bW;4z~lAvR1 z2AAMqCL=tEXUCunsb^?vk{1b}Q;5H%Xkx~-rAQG|dsv2O9RsIHPIKecBm3ZfVTY^l zE)5_Ct7DQ9El5WmahQ}Bo_OpDJs0-n#LSdPz8HY6z8(>a2fFh1+dtf(wX$P3?v&cZ zRM*meZvT+N##H7<5|dwZ=P{ic)@b ztdd;FeC0?5xjEJ*4FyPpE(DNfb87tEci;WgKelP}Mk_(D7#_9dOO{FQ_rZ1Rq<+Ac zR2WE|lG-KLhNF`@27~X?cycHsLraGAJV=-Mnor0SqZ@^SG!lw^O7sRMNTkQJ)ALHz zRU#5n+Nk34I?`9Fq(mTmPI!G`YB}`F5q@llsLY||x`Sv*q{13GvpuzlNk7dUepDXS z1)z@Qj3OiF)6v$11Q1*~A795aj50p2WsU*Ff_!I(82E3z;tf{JOxxH0^BbbCK*qf2 zqKjTgwqM5I<$DPopHg-Y|T%9YP)vt zvYw7Eo2BnDlecb&zf)-1xkw_5b4-w>;80JLWS+84Vsz+F>?nIhKiK#LE-|K0Q7L@6qzaSdic~#4;gEMY3X)VUb7A8sS}?RMn}syVD97vr1Ldm-BeA#OxMJ71kGL4_;qm#ZQ1;Yp#c{5F6hC3x4D#uw=0c+0$^c0pJ!mV4ks(h_Q zPz~LyN5W4MUy5jKwg$0|CS(ep?`fb~>X{&GwqL+mFYCRVYF0>`fm)n-@TW!m>ZK~|$s-$f`eVFK|)ZJ+V z3kGca_HAlREwDzFS=)DyJ*9WW%sn+3qzFP}A;R!9tB^EU>ywO4NtmAxG}SsuW!4$y z-X7(~h3L$Qs%##oDR7@r?INWk0zBtcFIC&cZNI)vQ zT!sYY+BA6?;NGjxd8Pm8adB-BWZNng@bwQfZq5JzAOJ~3K~%^9(0)?g7=YL{lcHmS zb{QDx9MtFsm5w>WgQR`Z>UWHYD3Q-2OHgWCL+8so8K@3eoz z(7`X$G#VzzBm%H1ZIWhkd1et(IcEUSnu6mPVGOU3LnU2HQ-@I8pfb8_=@K*Glx<93 z?~;*V?RV}Rc6k`J*ua)gL$KwPywRAb-D@9ngHw4H=!da@tO-%$VaFe7f4uvTHaR{n zUK+)PgZA5B7%>qRn6UN=oV$*at=x0S)<3yHQ>;k z>yf=+0ER$$za9e3;>25IE`e}Eo78q^Lt0|p9 zsAO_7AaM<8nIM0q@+TF8*=g%x1{-=xp8hY%jW9|a zITVD^1VDSoAp94P(mcAr@Bvz)g|wh=p&Onw-bmz4HU*egqh`5M$wsFqjG2H}z4J=D z`Rm`cXE!{fjfn)WPsrgwG+Ya*T(+Gf!?t9}VzIAFO{8?n63T;Qh=3sJl;mzG9N^s> zO*?r118w7z>#dO{3yYzwN|hTdkw!`9i+QgqxkQ6-3Jg}X!AK=C@$D21Na6#N;4&;M zA1v=kBe-toEfE=1?9`J_mIA{4_pFs6p`Q}xq4y_+85pqDs}Hq1?!42RbBUyqa)u5( z4iIKG&BY?`x(Y=^3(=&_^W@N|RjI*0~F7=E7_DZIwo<w z!3%Sw6CzDo=K47Ud`??mzf!UOfd$h4qEC|al{0~{U$J}-d-j>l?ZLKDQRe-6yTG%m z!p*df!bGWVnjt_*?y^S7^=03jCBqh zmU;1kZ&PbdL|7OqW)EVyb)#t%6NkHyD+bEf0}ejWc0aSp#&?hCU`aw3PXf^s9u`f*rGj<$_t~L`9%{e%)vv9u!y9i| z9~zfw?nT7`1%}lJA7WjFlHGsLA9W#Ag$!9AHfn^Ml63L@PBo#%jK9=^RjqglT`fXM z+Lk>Ka^rybowJkOV0g3%=Ed630L~O|28phJ^2wC$)g>b%>If3qJv|z*z@Smy zlY)HU72@#_=_E#TRE}y0C#K%j0J}pIAI7z&GQqk|q)hH3<{)w?X>vxyk?)O8jH~%J z+HbfXoL1WVhwwUT0lGB_TpHOvtux~(6N}K1F>;%T0{~8U3Z8%KjsXTB=Z7#x! z#rBZjwXtP1G_}}!lwk6b*J>alV7vwow5+R9QI1P{20d2*dqe`=i1KNQt;@|2xu_>d zS%QO_SRM5YB|Yf2v1;dfs2uvW6!k{rU3F@{M@cCJeR*n6D1ia^h^h#YC1{8!UkaNcdeW9a zs=+lOhzL8F@SpWiRDltJMbCRrBB8QKDU-abSo@6TNc!i9&@~(4VZG+!i>34av!DFb z2KyI?BL;LJU2TdbN`JkAMKy{|tUkSDxIE2vT`yffMxp5Vj1Eg`z?FJr!g?U#PR}?_ z$11DvfWrNBRYz9oRuqeszLP&KJC6l#aoQSdxKm2ez?)c;Fhd2F528?y1$TT)Jp?#~sh2b;Oy2s&6 z%SI6CVT8CAHG?NEnHtJ_7OHSYYDtj{+zkidg$frQ0FEJbW%t(W-98{#fV;P%sk(a1 zes9*8FM7KGI6b0pgC(z;%DLLiM6bWpg#}R;p4n;KGTH z@QQ$qR6~@8QH6XCC&MGFI7lA%_=aE+)FjLI>P;C(aikKDs2$Q1G0|F_4qUu{=!1)u z4l@`qwd@P$B?LEm5sC57!NEaM6@uVco2`3wlOY2s-Qs>kz=UViAf+jIIar1nkB54t zgPaH>Lc`!*jz0Qmv4#nOA$Gp%q6_UuH~qxMCnv-fALttp2(HvXT3R_(t4j1~)n>ig zh#D#d2sG2tgqPZ@YgMI;*|}=~qwsAsnhV8?DQ#BP2%{-{3&Vy(NMo5av_TP}k2h0g zsO8LWpRVN5zMthC}!hqqtFIWpH z2Ng?i&+}^_bzps6?6}ni&WY5VRzZ)Pv(b4jV0on1C@VPj=)GzYv1wvKiXc!rU@ee4 zhKHr$7WKOAx#vnv_1>{#ryQRTt94~s@U9AKwH#n^nta-ldNFfu5QWtw9eDAp3N6)l zsP6Ghb#PmrfpF-FfZ0pP#b79xEm|sL2Kkxisrih0Hqa#e97LxAe7{Q79SjjNBAFU_ zM?{s@h;`}jAFv%ec6i^YdJ0`30i2BgM-fhJwQP?SE5yMY9v!vCixw#=8{09OM!s?1 z4t4MhMW~$^cP)Bhs6IF0>^Hp0ZvNTNZDM>1TDiVtF77RocP&eEeFzK+(lv z2$8T6$Rxr#V$~a4U<|Z+Jo@OP+QjtW5gmwkrIr~+Vr5*BkV(G6P+a}7uiRy2ct?(- zIy*NRIXoypIZ>Amt2C2PLGXtdspx!yYVD!y@u9@#%PRz_Zj;7lYY@B$)RCQCSam>1+`u2i|sOSgnHjE+_tHhoT zd<=udnwCgw5?z7m)-vRZQNf)YOXIbym!fnjFErG!uF>;-JvK?xwyLJS#*$mY;FD$0 zpGU-~HX9G&8+p%Aq-K#6Ex_`EWU90?iP$OIsr%sCh}JxXg)-qp>d>^ukSbz9!ka38 zrK;APv(7qGtyLH(pNO8-WF^E3yP4pWj1`}c6Xm+m*h0gMFZRR2p&`5S%~#ruH{NK^ zKKX207Moqe=K-&reA0=Mw3b?L;%{N}&%5%?_U#+KYg6E-nS%B8GnLD;7K|KxH#!{D z6(;<2Kn6@10Q&SgJ`JZeoOt4i_WR%eUN=qUg2}=%IwZnb2eJvt0M^-XlqG5JC>8S7 zSL(Dg(iYY>)5~TkW4CLKwB%#45=St@%jWd^kUy8OMR3=j^!^;N61H|E|R%%iiz*9_3 zPH2(}UVvqd-%+_fQ8K`2Aed_ATsCWCwW?Cf3d1H!WwG4bQA(3>_jPHo;{YhxS0>OP zLY$2TQwKmDjCR0T6|HJT4;DjKDs|en9Xo|ROrZ+d#SFZLopSQ&cFwuS*vCKkd8N@4 zW8)&wl5!BqF}a4a{m{^$jZIDJKw>bsHm_=BBxB8SZr_|e==*MK4*=hzS# zNm#I45cam$o_pE_7re@T@Pi-PrcIla4#4eVlo~0Mdht|!%PD#x?cy^IJn#U!!ebIG~2TESsyirRvR-Arl;+&!wypq?r(nc zoA!QXE!vO(iYTdqs;W9`p3iA$h6b>tYT|*19Bh|de6fAyD_^mZtuDMs$0vSar$R|AO#fKbZn>KHgbIlh!U6Pt^2WEw4T~)UsLUtnEwgy~QO5H8aBv8b5 z(&~4$KK$Vi%lgvPNCd>-12Nx8#Y(yMgbmg3K&WRM^q&ex>dY0aH_3KjnbS;B$EL*F zV$f$vWHzKaa9p7Z5K)##WZtwKV}Am(5YCWyDP~oZ2*DxMC5$aRmKf@(RNiXNn?09O zH(>-xOpeb{R6%P=k+OE0Mk52~*c3l!6OE5gh}cQw+=kRXZ zzJ0sy5hJE31L+Wz(X+N|*RTx?^ts?(x!e}-V_j+2Qp(%eY5VT8uig9b{bI+1hpbq+ z(zZUkMTdv`+iS1A?18oSY5iEMwlPk?wpVcbl?Jg+N=G$*&Z96H(qwovFk+q>LJ%8S z*DzQ(4B{G6LcWx?s>R7u^T0Q`wgBE)-}MeCuu=Vp z?8A||VWCS83C!-}CZr;p387!xodz6uxt4?CCa9pH&tRaEH9xImE z;fEh#zy9?v9cIyx700kRdl*@sQ|xQ*MVKtaP;}kX5Z{FZ3v73aG8YaF>b;2?yL-CT zGr8BwmG;c0O;)JqRT0-1JncQwI)^@B#CMgz;HJH@pUc~}TEzwihHMt8^Fo)VgR2PF z;Z?~FRD1JH>+bRD=H|_`mhrrZu>Cm+vpoIC_qLWSS!7$cZf`%Ba=yc+(V-?JBlH$F zg5J4Vk-WR!w9Yi)qQK~`B>aU{h7Tkyq$E*+cO*KQ8g;#Bc}=D~t6jrG?ZAz=Q7S6J z!&13Zgv^FlMLdLdx~h=Pn^C-g0KSZ}uj z$~(4gRXYQe1}FjLZW^|?tI1oV!O1h-aszNq^)~ zN883t&)DwK5gkyXEjUat>6z)*k;z#nZ9tRL*1u@bS^h+{Decp905OEoP3gOXzC_2( zz*?kZmDV((V5*u=KmD|xRbU}UA9v*RFY^QRUlfz@DFN;6y;feKq(3v0rc%l^)3E`{TxN*HZOj@eP= z^Ur~IgmEPW!$8wypD+0V4C9BBn-5*Y9#th*)ylk5cMdl15$t(B%O@AxewD|egGO@nuI+AKI!RyVAR{T zZk5xB^^tZO_b+K}cxfoZy2?gil#U@~r4uG>5lRHnB{mLf23)%&$IX5wRR;d4O?&GV zd_h!iXS3j+T{c~*o8`;a5_3MU*H>p*KhAc=ddfKHmTepM83+;~NR?ov!>1hf?v?}4 zE@dl%Sk>o_E9H^UxeImzaTdUzsy>2?F7r&KX2n@=w55?4@h-}mcn7iCF-(n?rf$H? zA|<5k8e#1GNQ%uDKyH+)`8`5kJP={H+9^`^zqmpPH^W#tDHYy>M@Nw&IH2d4Lf`;0 z7T`iPCZjBptnf@_R;6tCB~&L=QBkD}OYiTO$>yz(N!P8GWPnM12-!78ih&B{vJ^Ry z_8?JG1i%<3jU+Q0MN6nuQVBUQBec6|$sl$({fyIW$BtpU|Ni?Wa=2>c9@5N0eZW;2 zC@ExAp<$gdutdcdU39Vi@&2{;z`BPtLFMFAPq7V8Z?wlBenhGj`9{lniXBP|5yvAM ziwJ?%A5ugPAJ-3_1rW;nf`_pH0KG)F7!-^P_!}M=gTwcTB)pTqp&m~r=uj2nb4Uqd z7|4?4%ak5M?vj-9^rWyY#@A0tQNTT^PtA7vfS0w?;1Q)mq)E=#;&(ADa|#ShTOr*H z-dJM&m-5n5XD@LeNy}9LlA(6P=PdG;(p{$Xa9@13BF+@Gfn{<|Fi1<5ELA~9(VAt1 zc|QKc6QU;s-$_PJ1Ws^9--l9;O|%l7(?lOqWtY@Pjiy;??&B1BEpkm>*>o(241*RQ z0CjZK)Q?Ik+oY1n#N{3tETSidOLTP+LArz#YetLNQyVrYEncx=58HRIy`)&-qt2M- z(@@I|Ra;FxPvV{0#Hf%|+4p%Xc9$hD1HqM|0whu#*`p8rNmF)em71M)#u@gf#~!dv zTee!h*sUo$O;xnLx03tGW~eq!SZC22_Gg=2>Qj^*RRunkOVf_C z8rbROevfKs!O73ot&{c;ykE6uIW|W!7_hDdUem10fCM~VooUCN)~Hkk-eM%cv!oxC z81oVO1`j2e@LH_Y7UPw;gq4vVCVe24^Ck1^fH^Qnx$G0G+wYf1lWH)g^lETj%JSkp zePwMj?w>=<;b8M34?|SMK|=3`Xo!@t*s8T9ooAQ{NVz#}N(vZs#P8g(L!mJbyw%~% ze!B9J`5^3O3JXPo*=Ef$#jH)&t9JC!$J)E!cbV1dP5Z)UzHJ-UKV|jlS>^a_B0L*7 zg%U&Z`lRMb&m!r-`VCLnj*;D(u!2X#gMm5Dq6Nk%v_eV1x$Z-T)&XU4l9ZC5yI$7N zO%*SQ4w?Ui6|D*_W^TvhXJ;AHqFTN=lT`-_@smL_vY~V{cI{ zMD}=cYQlPZ`)qV#)D|pUpaV3rYuFjorBa*g#0GV~kgKfXx!GelK2a_pmeSyrqWe(8 zB`S>cp4;3)Xb6r`CL=mc&Q3YyRQt^>|E;Pa z!%HX@;e3Q2Cz=k=H2N*o`jZM9V3_rZDP>W#l{K;%AD){k(uW+f+SWbvuw<4H*1YiI zi|p=u?y!f~Jz_n5gO)3HSpy0O#BEx>$WC%CtIjZS3BwM4T#{Oa2Hz$!Gk?zfRc2Rk@ZAXL<)fHq!Ke|GQ%`y-Wk=6 zS!cGh*r{jH!8{M&*<`MbUL zwCreyl^P`p;WcSqE;Ou(dWFWHwX9xkSh=TLqwN(*HkxYpp_K|57nR!6ph~SX=Ku^6 z#fAj(D+Pl;2WN^(R3cF-T7^ee>X4=ws|F|MfP)UQU;p~o;*g1WsHrLqr6O?2Dl@20 zl%4+t^;ulI)FHu0pcX=gImsKp;SH~|U;N_NwtX8aqJ4?)g@N}X1G?eND8x|+f65eW zyfSWua#4-RQT1i*DI!o^^hh`(XOYb?KcHfTj=ZK~mysnVT0*5&>8kg~>R!{nV+}it z`4l1MWt17X(beU`h(uQu2fV^a*GAP^*-6V~Q9>|VedrOkZru|$IbBIj-W9ETM;QY= zqYjcq0|VAKFd%A6gSmvlWZ}|5Pe}zK@tFuDFg|`i;xaK6Dh+#4TN@FqC@M*x3`5>1 zSO;V0@!lDCyyK%2R_gG6LXYY^nhZH)*3b>Q!wwOR@$Yatthc5Bk$oYdKWXZDf4ZDiz{IXf)ho(K z>+~c4R*y*~!(7YxMYS}dhYwWmRmqs{fk0SSv8Z8=Oqmh2LFxwHXQeVbRu&*S>0o>L zoy{@pU@5#RxG4#cZ(|JeCi8@W30$HL>{5>NCRfVs`xWx zLnQm!AeZjY2&;t)2ejBbrY7Va3UIJ>8%=3!9d`JO?5QW$D|Zk1m;Dboz@B{KQT56N zzeZtx!s76Hl=)FsP*jBsatHm9S?erx+6CvHXTQJgHnFgY>N*#ctZS&l_B(VxJLri0 zZT~~}(?Q?*#5VifM?Y_~J8QPAe}zqsPpJTc!O9d`((vvoby==iu+hn3G0Ur9DTTb! zdai?egnPsW-??k2ME5Z0)8iAWO!3@FK-4%~^^sDPvY|W>W$^{kmI=n-mH^j0FVZ57 zYsa&qXj+|B3JFF@gy71rPHs7?w*CVRFqqaH;j&}@Eon~jf83V)(HN*6^8 zDM6Qtre5z~rAk_~efHnaHgDaeA+P+K^qc3heAzO4?9s;r*asW{1TbBh5>8U4gP_w# z+K-I7LId~g+E*SWN?Ha*TBt5bB#*S#Q$N=y^U7sbSeGWgv{2?4u^dy9an5z3{j7C$ zxs);T&ZM1C5SXyts8wvmvgP9W{qf%W72)JdCEGnbqSuH!h1w`x4$`FJ;GhfE)z+Nx7U zF1duGQnaV$=R>bofM`{m)kdx<_4vVW>I}cLVTog z`|op*Enc|P?z#Ve#fYYHmCY1itRovwy`ZI%9enV?`uF3HJgT*ekztyKf`Nx~lcG`4 zDO60d^hss(y{h(?~ z7I)d^Ejz8FgfayKN5DwEVHlP@o?YoPcrS;)h6rX=TK2#t6H`R_uA9YLiIS5h&B$?Q zYBtc*~~4FD&A6(F?c`b^+TN*hsFBnM6mc$Z#p>uU#hJR_x7Eh@1xlcpCl1o!8zD`L=?KeXmVvu zQbhxqWEPsub^fc)x9t;K?CyK+vb~q@V;5ie8oT-CpV&Qryw9$A-#hJ=-~Yz``VE)Z zvQjom3N=YfESVDR80?YqxDcIRDpIlUn{nz&aC zKQs*;SW@{?QEX_Y_UzoXOB>l|?19fH@b!l5q@Dq%4_pX?&0f)D=b33q8R6+we3=C% zNIa?}{X>TYhWFGnPP3;sKW&dc{#biZX{0kzSP^FmqAqD_B~8Wd>e0Or9is20d6kOJ z!9ZQjLNt-KR#QvzIlK==8J?+E1R*nGz@`Kd^!AqRwU=LHBg5l%}QAfho|KOQyX^wefQ}(kZlD_BNd6HIKl60RBwU| zMAZs`!$#fELmM6*v89U^*}pKs|4%as_)F z_B9$hfRVMP!cIK7HdM zJz;OW{Eha=!;gs3PNa6?$$w?H-F2%Sck=Oe=#dB8>cbC`ybRNGGmVUW@85r5Yj1nN zN``VH9;jy4fz}WkvTh5y7vgJdwlN{gD%DBo6!a{{$g;HQh>keSY(k`SkyMW9D4v$5 z*J$Ey*$zJVAiMY8d$f*I6W%BcmZ-y!DsYsz`L_xc8EX%UD1comjCW(zY&d?!_b7&h zG0_ON>5BC&=$3@@=;)Y}jwyL-(l^)a*p=g19Jr!mXXx^iZb@d@WsOOL@mIoFW}UX- zjsto`!Fqf9?a&v!NF-^s$ghh z;6hupbdgkRIdep?WCt2@8Wascd##yE$>g#w3C*4)?Iz0Ey>qt+n&P3U@wnLD*Az0A zuh_#*I{75~@l8KgR)aI)Tx()b!hS_Xq7n=aMKq)-<3w{wrPO?q4@S7Vj4|(#!YuSC z@D_$G*Qr+KJFSe|wu)8o85s@dR1brkH9zMquOW&#kw#^V*eH3*sUD8AK)i8xXQ#AsBCO2OJ|YrW z#-KD9y2V;oPjf>EegZ&$a?FbO!9lQb~-J-v;D`9zq! ze~?fW>18sC;-t!%O2K_g`qeyr^*i5e-}u^he3V$TVH4vMR_e;z==88ELMNSmqP^}7 zuhm%gRx@kA{K0?Q?{2=sGBvyn(|*RS0Jo}!p1#>Yd7*^wa)r8$F#4*8=)@ySqN@Oh z;p}id@qK`JO}GyJT9Wz=5@w{4(SOE0qEAJnz=j3G8Xfmh=(^v&-Z^g%*jH8i znJ^t`oVn(i7}`nujMSxJv%rq}^6xXw5{wWR>!3GjVMNU1ljGLG^r18~!zT*O=(^NU z%jQoM7NXv!W-8XLr+2{4J^wtr;ak`1F!&&hNh@`A+p0bHk|Cm35DbO=%X{*fC$5=QwNb~#ivB)ZE}3nid`Mn)7fJ?cI>h;)0Df&P%7=dKOC$m5cJ|| zKnNHt($0B()*Lm%owkN{v|38n_S|<*yY;uX%0WXwX7B4P;53v%r{;I1n{}(#oZkgV zf86oMNH=ZA)@?Ghq=%djwCdC?fg^$qmz)S3ONTzSenj;`27*$(`U%~_hhfwP zYqqrDybnO)8{Y5+`|4M}D$CFJ6k6jIQk#nmsH6$!fJMeG(Y&oO;z=%7tbN)_G;P3x zL@=}%WgjZ0M5{Y@?rLv9CgQ?)2LQ)#4iAqyPEe~_?}9#Sv}zg{I<#c5UGkb&*)b;_ zY#X26W;gxkPwc668?CF{D<#i~*-;y>Oj)*=wb_}PE$UrlGvf@nG*2~z9XfeW=|)ma zn9oULv$28N*rE3_>F@;KDVzqchjo6=Ip^5-zyE#r-p!_i?o*T6P#8Uak4S4uEQ% zh|j5n<_)Z17~naC;eeV;mojK!o%Q$jOMOz@5z+D)JLL>=7J~0hH32K@A|S>>ME5)I zi6;_IC_Nk=1dD;Op%TRcV*q)$ZhS<7@QQ|L4L%%#4NR19)KN#-z4zX$n+_c&MF&`S z(xjsEky5*vA9lj-a;G-p@UC5YkV*mIcWE?ys!U3t(_hIolM?q2IBT2KM!M-Gw3OH! z&MEPB8>dN}dG=ZM^fMc!B~`6gT|?{eJ#BPs*q(ZLqZOev7_ucTs}-tNY0lU_2kc|{ zX3^HKd&)W(VO0ZrB1CaXIXGJal+(nT=xI~$CA5tsz89={U%RHu0)bC;!cp=Rm_y3+ z;%{Aly@R|QL=C{0^o^QuJU)+94ddX4kci65<6hzMen5bDaG#lJa30sJFO+juYkA5} z7O=m7}1ExBR!9*k|KF)T32VmhKI*QWYu+`{E>KQ`PAYf zgQb~(uGQ~tfIcISA-2N9W~STf9Yk|iUG+Bm=}*_#?pk^vn*N4+4-5;+ zff)Rx?-^UTU_enE*ddvZaKR2AB+R|2?^gH}lqGt8Ggejw6ahb=9wgDnRn6xsjIs>y zOOAbi3;m{<%BX&k1)iy9t;S*(3mROhoakgyPDiuw<^K@&s zEnc$NY&vVL3hB{|)_8bkmmPD$5q8mQF0}5BynXonpR&neA_bp~80BOa*z-zL7%i?N z(n9fO(kOX1)BE;-AUG7_|7?KmL(9s_4pgvn%Wk3E)4P2@MX+~Sp^G)6ug znhtnce;LbpQ&zZtB#svJcc~vzQ5cG*sy;fl#H4UJPC6D6CW+&e^mXcEgWSkrgO^87 zrP9F3Nn5&fv5k(7TfK%O@}9R`fvB54yIDDsFpQO1KNqT2H@%b}4wqty_L5R8m)lWU zM3wNYhLpA)ZfZ&5_nCXBdrG=w&Zo@UbgilYgZBu)KK`x=&JV9sm%++C_q3yqIKsa3 zgByi82kysb1zSG`XegSJk;lRK^-2>@GR#Ek&RSQu#LSHD11=(&)9|==3kb*7R4T*^ zVIa|;i#kb{6_jSuHr2Fx%f%V_jGzqZ^G-kNZ(F=EQ+C+^l*j2Kq$!Zz!Pacu`pTVF zO47uN7*IMuL4ZTiXjtFU1@@s2z1zO}rLWn;53jRq#w9gWL85&prHfoG|E_0hl1YVe z&BbsqP&i!uqC@N_KSo%uDb7$>)*y3=Q-Y)h_}OgIBj<(^8yZj(Lx)lUujPhWEDpRM zQsDp~ zT_Fw-W8~3gLOxg3BaJ`z_7qYT$fP__%G~*$rX))?Kk=-{x=haj=b6pU*cESoy`6O0 zQP$nnuupv8>vqrWcUw=_penF*k44!p)lx&F8c1c}PPvtWZEj}vRk zJ@zgf&@^E$_ajB@;5%tyL{Op%rPdSn4gz!;m@TF+^B15L(L} zdbSWpIfxxZ(Gk_isX<9Ft7?J}QM{`lupS=4gCBLOxjc;4(BLwwOcNb|4beqVu}sd5 zHTO?^)!mJp3UmZ|bE<Axu-M6q$ID0s<&{=ISm7b zL$#1XBK0T8q>c^o`-Ni8Nu=ZBRxYDT;hiiDz7TaKeR;J%X{05nBnoFzGHH2XD;zdZ zk%y!VO-(8+v=D*uW^*<+S#>RarKp}d>X~H=wsOT@HZfkc$tj}yUR^<@KDW1mQlZrN z#6#cHqH+T(75qUDg|hj~l!ghEM0hNU=}t(F1ablG8nm%doWwX3n|8wSe`WWrz0XEQ z#?*2^9?B$QE;h8d@id(!#%s;k zXpvIVZQHl&fN|4!cuC9#e6!_UK&)V?rVM*sgDTx$1nu7sY^*yEOfalJxF7x{k7S*qpQ_7qt zMNi5IL{S`6QU)CsHGb#k=oFfS0|As`gmb9HY07XYv*OZWurt|$4fHXXbkasA$87QP zW%lfrXH>1j!4RdzRBgWZidVeCZo26v+p%kh6*2|2e2{|o^z=H{NrHS1H1ntCB&L2+ z3Q3s^2it7n;Gm7u7R9vch~l9bv?t*>*|pZ5q~q!s?TTWGgovInWHKf*v)0$M$i~M< zjAva1Pt1EsLsi0NU8L2@OlZP;yNsNf(pwCQLmR9S%ma_Eyd6$GotcIliPAj!4Z)E} z(Ig5GRatMWmFOc8jOIqV;z2Zi6Kzrm;PZQWy8Mj5fve1D-J^#yD&FDvxh|X`Ss<@r z4y15ujRJH7E4F)5!GK`vRrE9goo!QOhqk)8Gbk#>AHCSr_DsW=$b z_w=(~q9*JA`mg`e2IG*2){j!smf7@_mv^J(0Jb;L5DSTc(%^}yN!=S|Xc;UxL`gG< zRMQnYy_r~YyG$gmgM%d!T#6LHI~p`~wm zAHJV8Ds_A3oBz&cMo_0LSyv~LHDfloaL7LJ!FSnBKl-`d|KNSPUrOd|w$PpuYa9b4 zzMPBHVStGqG(|u{g!B~pP)(g6i54=^reGsUDhObgb;X!>mb-jDo^VCNcv1yaF^T4g zc3snmbXDY2G|V9!Lt^ahwo5L1joo_dZ^cx9|Hm$~eGgo253OBqpa16{2;|>;)!w${ z+0AN^8R%Qkt`JhWWh28x=RUpliZ(AFv>>IAT*f9RM^sj!VSD$;h`@FBE^9za7(`l9 zLgI{o6#(3$QpMqO5yytAgrt2$YD|#oEca=Eia4fGPvZpV9^UD+o{pOan_Rs@QdP^2uD~y+lQArF_%yT!0tnZ|# zMNY@Ta5z7&ZpO58oRJ#n2`n5};$9Sa*N7myU5s&JqOC$25Ci75uczO3k4_5834_F% za~`-N_N(SxCYf^Zz)ZI6$`TrP=9vfzCfQuUdb)cpn}KUdq*xN~vRZdus0??i`nth7 z;+mG{rLO8n?-fldGn33B^l84l2%}Mjq244Fq#}WbOA$bWzAh>O-WuZ#miacbT*sPo zK$9n+cqy-jM|q2eHz?z+J^%KwoQ;8ZIp(-yY+-M|{o)tD(8l3+sIXCPWkUu(C;k*u zn}tiggnVC~Drv^I33v55!9k7ZqOy}DZvxv8{*wBtd|0YtFGU|R%_#*8A$<(tXTJ3g z+iU-2?R4?$pS#&^`sOW)%p!$kt)(WKDr#hkwDTP-r zXR7R})ucis0C?LW$JIB`uc|F0z$k=3riIeLGgR8Aaa?}*VokuOFowv6M`bzoSXA=( zH_x!Qzt^(GjP2gB(^``8hVtVP$n;djx_Y1>Od%%cRj?+-g|gP2XX=z42ipazXEHW4 z)NkLo@k5$!?fb^;+E0Gh*4*?v=Q1+fG;!@X&@tc7oiY;6oGa)9Zg}8$h2FYnex5~$ z7-}u`a6;%k?6AY_@WT(&egFOse`puQIG0k!dvUquEK#QV zx*;WFAgRDofbnO?Wne^EPx?r;u2nCn#8|Ru=2k|8+dS=<_iK=9{I7$f30+?LMXX*AvHJA^*~7su2^M1Q}@XD27AK zP(`8hoQ7ugZaLv_GF3{Vtjm;JQcf9HnEf3k57Sen>_tcKXCJ-pO3%&H*QO0S>>U?h z+olnS8Ue`5;8aC;Uj7avDJlXj9hwqC6{pMgTD3wE%43f`YUOg59rxnn?dG4|Y<;~0 zwrlrpJ?v%6mZ?GoCdXjY?K`&G#MHPVM2wz{ZFyZFmBfAdA#_~dp)9ByFpGFSl+$F=kCSVw6+(T_)E6Rd`)f=H26 zu;G*st{LuHNH9l7M9?`j=tSgX~kF)xM8HuI_oTvtC0`-&Ue1kzULTG&wVIuM1m?g zInq1~mZxoXaUC~r+AKqc5k#hDa?+=0M|$l}7@V6ZKPOlOngh14Jr6TE)Oc?>r8AHJ zQ0qCM76wR@SK(>YHQ@q~wL0ZWg1jTcGU*SbHND9&Y9=|uMDRK25J6+b4E zAn?z?Oy%6(Uq{>s`IF6vxtd=c(ZLoE#v=+Dq-IkBbflo0w{DS4aOfg&!1=ukUw4|l z{#`G7&Vgf0{U@)!(ME>H#K96fIq5q^wUR?cskg7Ek5*)>%{FCxAPk=S%5!w+H*VZu zci(-Fl{<@4eqa+)nZoGuzLH;&d@oJO+?|(?WdKo1O%**=ElJ&y2w)uYsJIDKFSxXB zcVCaHJz05uq zHHGnIE&RS+FU%cSwA64e{d=TmP&oKq(r@-W&sS$A^%kl-z&yLb@D@uh18xeM5 zP>OkvN*L6u=@!|7Ri+T>gHuJaj4CJTgtw?5(aQyD8gG&g27_@>F&V7XKBL2*e$RE9 zeIl=GH~VZk%{NPpxRBZUFb;`zOj#M)1Fs8X6_qMgyu=XpX)nGYUftpE%CDJ7Ohb3B zs45o`%q*3=RPl)Si1!X)mg~Cwb(h)qzWaSuSooaKKUd36E@$Vy<|Mo7Ll-{hz)ele z*jq37tW8c#xfojFB@rdC0fHNa0pvjPUT~C1Yx(SdzV?0g?Qi|ees}BtXvzr(5fW)O zOQ>J5MMK_Uk5Pk&%l$4`Flak=Y!?t85ee%RP0;Y}f?g3(SKKexz($pjTblY)?C6vR zmDUl$XBsi3s3AokOu})6(2}j%XCJ%&AsWbO#i)67MdN%o#||;>(Orw-uYuceuR%Jdy2x|lnm7R7`QTN* zVA+SE2xk@CH5FRM$2CAh(fG_vI)HN{?c*Ya+h`~(jn!lw-a5ltYr^vQm=z1a6fK*a z8n>>ll1)ujtknPmEl6@kGCa+uEn3`f|9-;1nV(T5ct7D>&arX~D&Ib5DuSdSf&HN9O9o4ZB}FW} zQOm~Uy|RWXV?mwY48KE$0~~{j3IIMA$D7d#Bff~L0LY&DsPjmTa$-}vhBbfkfW@;4 zb%vyNpx&zFc>>=N_N8&&bNW-_hhWf>3T5Qu!r{Dh)8>6iB;t%_n-H?2;@yBw-+CKygl^L!!|jA zGOAbM5Te67f`;|>3@9b+D0L|%jtEgvEZn*(5tf(5C4ZgLYK#hv&pUVQR2oGAVCm8& zwt34o2^c!v##>1o2(4OwAAo6ADYhhwnHo&7qq|#CG~_{HC268D(|`c;-Po06m~o1v z$FIEj1bg$e3}I&g03ZNKL_t)0o*!X-@NHkU^^b0I*7(@CrwsEV2=0r07!H87eB~=& zDY7vAcSy=m?AT-F9=3hk4&iASvcRGwSeG1DaM!@^_TPVhJ==)Lg4-8PM+Zo2=zogy zg#l*`pr~||j43<|7ozhurb;PU!{Fc&8yl(0IW1T)pl81KzRT^4|8cd9`wM;j^5*}w zPk!VZT6foBcUn(Gm_b2^``}FQ_lW4*cxA$N!@%)=Zfu-h6w`yE+Rd7Ky6iaU~%rH~QN zG=5JhVHm8)gB9uIhOJtmx&qU?po(4201I12PoVfbDdi)ypPr#&fgoN{?Jk&8Gyv8d?5v`Z(8+%F>(8(&uX*l)BPG1-r5{s+IA4J)aH#m~C@!#89C{8#0mqQZ+TaiZ zZ|gmME9DeHjgF28AA#fxpsUTRX(NRCSQKPf+&g(|htEp%@##Laf{tldt@OEs!V**a zgXkF2$Eu2xg}UhNcif$nC>%qQBTDJL9S4vr=IE90O?S|5>X zuHd8I=STfK<7#z6rTpQMZFcN&N2yip)KgBk8@}~pTeohVEgbB(-oCPZ^ZWnc4EY!M zy7?z}*rz{!y^IFnIYxzRV=aQ06`*vaV>}n=Ekva2*RR)W=c2-S{HcU1`uTT<6piF$ zG8Bi{Cq(xcB2|~u{&xvFra=3i;4`3Tu&>qUSYl8{QO}KdMS7FPq3|ix$H|&DqW!qt=qBJ4%mO!JN%V<~VQtJ-yNx zBJ`Y|s!C;m$Vpl>No*?sP(?QC)^M_(chh^dDe_ak$3n!R_66i7sGc~-2DbC-UVpKD zcwO5>coq}{SiDJhPUSL2D84@gB0qGaO6`LI)$ziCS?r-REiu}Fap#QKD1 z4R6Ee1`x~QUv&AY_NI3~7sH}b_Sa{9%qA!O!Sh-U0DqF+h6fAJ8H7C^G9fhlE!G+S z44=;dI{x^R?4Cc~t0r5blch_CaSl|q9p_vX(aqKUYN zcz>QjRN657#iGkCO8`-ouB^cQ_Ti~CR`pXVDr|B$sai9b>%QztB zI7kkWwhnvw1xMRkt~uv7;~<{iv{5NmU@-HU&|!dL9i@-OoXVWp zM?e1e_LG}_ZEM#2MAL1QE~n@$s>&QB{trVC`yg7#0{he9oHy(OYET$+=c%#QTq7ev zXe-Ov>;C3aTfKU!MQXpF4l@lS3E*KLi)ipRKn+VO;q;sW4v{E52~*imH>oSQr_P*qqwqJ*;U= z-|UPs51$I<83TK5$>Kdl2idi2M_b6ztFlm!x@>$5=Z5@Ghwy76^*CSq?YEyIWuTAD zD^C4Lo8AgvhA3s}QZ}>M=*WbybxA-;vetX`B#$A+w0GfG>60Tq;-cHqD^4)eA!ldR zn_{PPqeLf2>q@sea#ewW9-dOtyNv`g|h!Oca`$kid>A@%H2qILbLw@Vwu z%wB%sarX8Pod2AChrzn^?2k!pK<|U(a95{u*}zv+{e%e(=;+eQ7*m>p>8$S*=7IPc z<$4ZM&h}olpKaRota{@(9Obf4QmIy1GfK@4da@W48kj8IZ1Id}{_U}`k*Rj?CF>K_ zzyPeHk1v!d0o)ldBA9>)$7+{y@-09%odR859U^5u`S_#O**9d>S+m|AS}ib?)ii3H z3bU)aI?u&Bt-Nb1Yv*6`S9aBVpPv$b;Hv+y`|o+muD<%M!im0o-M?GD15=!2h*Eo2 z6iQe_Ul*!+OaNjU@uXdR$*U#pvv9#ed-BOA+Nn$Ed_{E6bp<|&BM>eIm4gZ!nCSc` z90P<~bqz7mh%3}1rXBa9^{zx$o<+mlahwmnxZ5y1YJA}i9sYd`Tl z$8w5Zpb8?V&Pf8JIcE_;#WVB@W}=y-5p@_kakM7qoJb5|<+%{`f**TfgLCJ^PyZtG z@qS*egNQO5x)g=ekXtG<(#y~1t#80P8rZ)}7q1X$m>yXwhZtxm9gc^R4KRAIV4a;k zlC+tg@!*328FZZ7aKjBwU3vK_ANIb?l!H6aMGFC=;5qb+cgk6*z{FdRbX-`_EiC2f zT;1smn)spl+oH2+0hFlm0>bTCDmyi$V4i=^mH&yh)S8f@v%uA4!8OF7YRi}Jp{c-Y z*RD-fMdz+~bm-}(JSEk^m^>?{y|cRcOf^vF@)C$sPZULAE!abMe(Xbn`FFqmw3 zCq z3~{7D!6V}}aW7s0@(7qxJ?77a85wHp;&SS~Z~8OcXP;Gyi{8)Fn0U__Pe^sfUhpyR)M zkJPZp;NP~Yg-3KA-Fl?R&w6@0Wt2pTXq9Zqk|9ObL|NTQ=c!3V$~CY=Zm#w=uZ(sZN2md0#{H{cDqvge^D#Csv#tc+&SZoLd2hPJ|k3At9p-THrdc(W&}>y`#<;&D|SrTCqI0H<+5mx$D|?Oq>)0fF*ILM6q8A5z>VorZothl_jWph9kp!VIsA zXURH5gyFm*ukMD!6jU*f0>k??bsjLFzJGBTvWiQIv3{iBA(U2&A(`w=$$t*Ehe4g&qYl&X9O3F=thonqy?S~?J@BZUGw7*x z^kYW!%jX0Y2E&3t!S8#3rz2rJTnu+a5}k8r0f4$zKPHOz6TG$c(WxSR?{ z>AR~rc}`!jUQ?@w(k48pun*D=S4SMHRmAr8MwcX;95|AqGVeQ!F9gU!{Sxem>jnFv zvc{JH`*dB<6Hsd?NsB7U0)sn<=d#$zpp~jNwH|zuQ=T8C#o`C}9%*M7u-MT285^1R z!eG7rZKwa4!J4(p&;5*T-|jQt@IG8uc;6^T^P)H$tW#wQ&2t8gPT0zod)nDAJT+G{SoMA+jE-}<_B^enOQ$vHq9gN0$^ zX{!pF8ejw9hKB>&{i4^LU|0WHO3gKX=$h;8{(Cp7Ul3dmyaap2L5qS*d+#Q%j|?_z z^ys6H3Y!cgtd^EEPEM83G_NnBg=oWx!x(8M2PT|rJYe>rXCfRlbfcW(l`nM2L2<+I zvqVR1)NI)v3+yZ3y4n>KUf^rZkN#kv`_zr?;tcQ4HHrXRq)ObU!_U2F{G2q`JhgVi zjV$tT4_GJ@k-XgAu3l`>+4{ulhN^?+(`^xNuzDcd+_-KR)-^V~~=A4s1+KM#Hy-dT(T}@lGXo+2N={YvCd)n5l`5)W8Yq#gH^`w%h3C=z&!oz;Q zaDl2C>gZJxzR!)bXkZ6usr&kRYXTc4DpHY}x)=2v7eKGsbnT^Bh%hceX2Y0afC&4w z@YT9h7QxU+i89RdYt?P>qQx3vwQJ|Fa_icxdsd>9Nc&>L2DKu>Vm?!;6CSwUkYq67 zG@l!W=z>d6w6|RI(&rhh3(okMFf7L!=Bkg;gXI-1XS)-wb1(z{VVw>*U|)Oh2d=b_ z{NoqYFUZ<(uOdaJsW?)7koWAity`V5q>avCj0B(tVC!vEPO1%W9YjYeu}A<1&Ew>g zPO@D)x7#D@=pFM7&h>@_H84ROBwovg?(1JD*_f#E@cT*;Qkx5gchAsXeqdA2T|NROaX7x*1T z+9`60s$ise5k>R6L~@N9?JD4$Xts@6PY;sG(@K{!YQF(P>rm!Ft6FsyN<)&NP0^!A z2p&Pa^kt{nJKy`#zi6y({^?!z|NiC2THAOX*Q&h^Pf(bp8>gtIM@l_sbespKJ=^r? zkZuY7zxn2y_00I6^MbL9E)#W+q^hqP?eiEhCp9Yu$rpT0>di!sWq_hrti34abP&EG zD&x5dWyW`ws{of^nN_AH$sL~aZDS**tlmR zB){mQi|qRAuh&I~oXI7ZT+&uz5H6F-wOkXg(FplzZ_^T0N5D9d+xcX9f9e_ zTHaiYg~woJbEG^J5h^w?&~HJ0qz!6fa!i|zrePjBRjs3sKGJ@B z>uv3@-K&b7no7+9h2VPPi6?3{-#scL1r64F-+GPD?}hbC3eRw;y_R7>^P(?2!eW?Y9%hEUOlI|Cw) zvtD|Vz5gFBXius6bBny@raSC&|Mz=(t@EhnCO*jsFvqd#1%)^R@mZ>JCkM~^bFWfL zNSwxqXdSVhT-SN$ zzs&BtZ>?>5X0wbH22k=h3fbM3_&7Hoy4&A3^qefKy@$>|@gprZ4S78w-_m7E?B<_; z*A_2YW(OX4fX&R*C4EeUz+eO?9=cAAM>-C|a}!~-cv#4?dJ5$|c+m6t9Da{HRmLxy zwJWZ;!gY=q?Vd`ek!E?D1?@Dc(v+-igzvmB&CEU&fE?H>hFER7#35V4v@U4yC}~m6 zdV1OP-UsJckOPB(OT@zP@8H!$N%nUSKPtO=YDDRh9>yg#W0 z*9>^e^TP4007UbCuH5Wv-#b;y12 zS%AnK8nod_MYNA{CT)7uW2w-F@!}pLE#&%+JMK6&;r`EWZqvbqp$?$l+uNn+h54MKK7%q$`gsUeak_`r7;S_ZRy5)qmYZweT)H2uN*s{5$^_DOyNm6V}(;ZzIE_HaIk-jR3SHKp4PU z6a?45_RtXz;JQ962x%AuJ-)wl=k{bwoZk@v{I-a$@9V!=X~)Nb_n9HK9uRP9Xg7$wD`2lyH((fm!O`=#Xpa2tM4I`nZ+ zE_=gE?8k5-kB3h+D$-`I znP-Y2p!FnLR$M^?EpsqKClyAj#h?5h*N0KzK6wv~%qo`zqF?yeC);ILocz3V{rB(w zhTVO~BZ_31CCU3^u&5MHPL8R#z*=$-^eaB|%q9(o=<6G>bq_zHJ%LffA=9qILFT?9 z)!=oU0cox0^AeUsOpf$&bZn=Hpa1;H57`fY_)Ghr-`-*!9bF>lqIv5p!=FI@X0J_w2ZWMmEXMp)P2qCA-u6s!DCjps3I=d-czCy3F(e?zTt9P-bi8N)lo&)d zJog)$AUu~xFrJS31N8y20Z2`@q8 zeI$}=tR45mhUI5LOpL++*UA4x3qCBt)162od;?ajOZV@hbmF;{kl}Ie59til7_dLU z)dLSWK>O(NC!TOXAI{X|gl6Zx;#J4mOV2y_c?Rn}Z@bPOT)RPz#tEIY;B;G|$n!!6 zpUACG*Tp{K_c#nhSo`gJfLdV2#%Z2*DIah7@;${hI`rTg?H+Ly)r5;qIMb<*Jo0e6 z?D7ljlb`sk3@lg+)CJ%XP1QwNGv`q^&;wt4tQ@dU|LdFnqOtnXjkno!a&SV!2G+hz222VKeFe<<7`REhmzQZx|3`KLuJE4>+E)$qA zpsWcpGtOwIrx+~Bv0O5Zzyaq!Fcsnwt`!)WueG0@;NtN<5vk7^EQeae6lirAQ=>z> zi~C}~XU==cr$lRrgDeoXMJ0$}r_=+iTBNHZag@k3asn9%`hUJgitNjQ{4%o3vT@JK9Bj@$+cp z^>lkyL<^L~)$r=d|1 zM4z4SKla#Tx^@72@Dj+V=e+VLd)ND)yMEl``>y(`-TOy3aQ^(E8DNMpE?G5+A9Bb+ ziZIu%y;lQSxz0lmeUY7g_L=tSPhYFgam*ymO9*YS1Y}3F!TPEhyV{VH4@QfoKtgijyUyiJ9`u^|j-@o+p_MZ00-Z@{6rMXnHNA$k$ zs})bd_+DUHtVLu3;c#NV5T%6bhMz}|(1>hqgWXUF(DX80h1_%3!_Xv`@4R zR@)*Lu)s{eK&*bu&N$;tJ*-_jcPHG+S=D^GwfFLVeBj)CJi=M;_$SPao8Wh&ViOn5 z_X0?aP@SKt=dI?~69>R=+$$3-Hed5g^O3Gb#Nhe8b63D)z+#3L4X9Pd+l|1Lk_64X zN=z84=(S=4_(UHn9X{VMxGUTnOzjskSg$(k6XLUZ!>9Aj&OZBWd*aE*?ZF2hvn;{e5HM+Al? zCnDln8S)_YfXNA4ykw!Be%hJ#(=|V`@i90h-WoLjA;f;8l+HsXbdCscJ{}XP;;Sz` z@p(KV1{7R=!RKw~&XELAyBrO}U3oy?yYV}A+;PX+>eYwZ_1E9v5mw#-bnYp*F|io} zP-nfuAW`6;!pQds>*2OwL@-)jeVO}RJ(s#a7v76!%FjKGpW`*jkd#V_u9se$a6_Fnp zVje0a001BWNklx8JDh+gRVP_(xxq>NSF7D2r9sI&K z)C=O>#Y+pkP7$LQ*51JRKM%xzuQgc4!VWunw>a_Cm3UYK!*Sfg|Bj_gmgjXnuADG? zjWwnwUUQB~4i3tub@vta$6KxAHkkE1hXM zJ+avq>&CzT*YT@0RySYwKwR?kKV|g7nt2_nnMGBXjC-wMwG3`3s3|tAnUd9HBnH=c z9nYAx9U2ywk$-0)sz`yI2J#wzR=zjbF9$YpVrSAs)UzrFE}8B21m1~GGvX`;Uho=p z!>GLSew;<)YK9{+uox@`?B=|O+KQs~h>HeNENzPMQOWw)sW_!A?e(#*m)BZr<_df! zds`b5HoXaw?(K85llf;3k<_B(2Ckex{xI-os*7%P4~p%Ikq;;UHLx zj`oSMWXZCM0pa&t#q)z>vw3p@4672Tbkd$!YpZ#MfaYi#t+URoc>T3EvU5Il>=Ps| z=d@>1PK-XO9aSbO#E|_OvhiI!BR?Cb_c8#gu4aT!orW@@x`_H2=1|j+UP)hz)Y{L3`%H00q?w9YoLR9i z0C+x^Ia!V<(@+m7&|vfz5i2P=Yl0IaN=<%LF$*A#9Vu9m4Z#GUEsHM;{MqR%=z#w% zMu9RJ@iQgjM;jOwO`SoES?Ze&wPeLt(nwW0N9o{w|GliqHSkxY+suZi2~h`8JQWOr zH&_J_EUY$+UmbMyKBf9#Q7zO%FQ@q2d+#ldX=}iOreRj#v%L+k^G3F1PlK5~Y^eph z0^8l;xw%QsaUE*Q*jj?sWKDGt4Gq(QAF=k@>&82aUXLilh?iHZO*>HOlX1#ftAg%l zp7e`&`6W#h7yiXuLM97o1|WTsTWqm;?7sVMam_W?=5u=a<-Zr;`Dj8A#(U(rsw2>C z^!ZFU56N}Q(7U->U_JX=DX!bb0Ft-ae#7|Tf2`hE-F*FD;^GVcSiz(cGactMtB}L{ z>fm#Bgb#TrNv51h#zxjgXQg(J@>xVFk@ItEm_?g$O|`Dw&$a2i4YqO^%`+4MTqz;n zz>z4bFBC~w_hBl>h$sr9>yF1VTK2PYR9`yg;Qubu=axF@u+l4%F9KDa!c@~YD+pA3mzX71n<{en4Dj(M zI#l-Dq=x)lp`I|PG;%4~!t`TG#Ds{##xz%s4OSJ_hs%LQ7Un{wPJ~f|x_+qY;gaFP zN)z=Z&eOLnu`D7J^rbSrC>FRj!s0p*LK!8`<`JkY@Yr~PqXr(%AxC^_)d&lDnFD@U z8D|8jZ7k580o;F*6#M(ru#>{l#0ObywXNm;KM5DvlOQ1f7Lz@_g9inu4ryKSql~D@bM}sVvTD z=-$w0&|p+YhIW94Q&OUOWH={h<7CB6>!{$H><`ZlaJ!WB!_WpBM>B17gy$*tD29 zk}!ti5S!}@uvGraqM9DyyxUb43|1wduL0!!kUeHS$A1?L^0ro_kPD*;%9_z1`cSGw zsEM1QmFT*YT1l9XKY7T;GI(IKX3fg)c|M)fW#5yd82&x~(2vJgR#F=v!a97;c`-C3 zI)yn;T+_fd>*Jr<-I4%&py&U_|(zXxMj z7pWZ3)Hp4MMi$4&__AnfW%3f&Z;iU>jFF*+*m}ElOAQIN1Qjc`}wTFA$xYz+f`rFJnev?PbM)|ZH8R8=gOhhlKJCz_gz z!K6c^_LH&*(A=U3$nb!RAIL!w5U7eu8xIad#BaV5LSUe29m0+bdy#G&Yh@Thc#|rP ztUGIMewB?JdjVAjo^L9JrreYU(V~+@ixuLC8PS!?`D_+0JS?7j?%5ow{Id%$j>(g! z=EUrD6eZ?ZHb?24Z{A+M@4kCu>#es>V%-bRzpz5sUyEJTR(h0jDTU%gCzF;9Gcu~Q zqZoW>&t{O`>7FY~0KB%;Hi~RGhs(~t`L~Risv=GgDxqT0IiALKCC-)&u05$+fSdwz zU?G^|k!?r}0y_71I8J)VJ-KpZ{E7#5?C180fBMQkD{S9X9G!Q5bazWo4g<(Wfw9iR z^QN(}P0yNZr%rgM4#L!$Gm>CaK<@K_2jinSLdA@Mm~18t8Hez__uh{+rp?F+J1x!d zUjR(Oqm&F75A*~h4!I`)Q+)(`AXTC2#A{)_lX9_!&p!Kf4w7hU(E*_E5c&%daA$jG zR%5;P9LoNFDoBb{Fmfr-mh&#b2wP*n1GoRs2ebig$S$jusfG)8HIjpM$|Q_aX&s+=WUNQ` z)x;dgp~C2o$ApPZF)-Mhy7?Hi-X6_ABx-S?#+7FFGzA%=P(X55c@yd>f)d_ec&KO# zKDRg;lTj5p73#nRHpcgCwAjDA7p+&UFGU@FHi7GM!1PEWexNEfKc7E;e(qO3`(GY- zB&&rquY;YT#|NfWI`p9P%IGZZw%Z;VWjz1f^OZ=&H<3AU@nG5JE@8V&>JZsssf~!! zJs*09hMb5w3SXO|nt#*SSMa|VV39u|gzLuBo2K7$X6u?6FY7*e7f zeP2MP_*Rf()rA1gz{4hNMh_d1KKtoHh7D{0CqfF^(LOn=rEF-7$yiwvn2l}ag;NDq zl|76Ko-yAO(qJKYC+{h(EUm}#G&Un+nzr(ZPkbUCc;J3X-lBS%LoVpb*FEbwld3AU z^0Z^8S)V>=hdAY|gI9@H&OGUV;<;zu$Tdzva+x6qB2hXTt_Ads_o)KOIudOO^yNO> zBgq?aO-I!jeb#S;@pcW9VyxU9gF_!gYdfm7{C-1hx7`-;{qv4dYuAT-UH!|u8M(<=!0U-caA3gF8@gV36^yyYCcl zzV&)aKBI6jK3<;%6)wr==gSZV&l7+=qw}7gN|7ve>Iz)Z#Ux(CJ(EuI~WkW_s_oI{iQ%6nv$!a6heR@%=e;F&9lkWysVL9P<-fP#K^PXO9$@t zy|PG?l$IsQ!5ay5)!&`6IkFM(D34$~#*53pvp=lp_OAf11&_+ch7b^b69BdifB{__ zCmS0_Sx&=j`1rH-d*1k|Zd%-*_f+C!1^!^}OEwzkh81_jm;lMOuL$zid3(xsW}NaGp#!fqdX?58fhNCN2Ep+ z#$W(}(RfHrL=n4QZ7Cg{EoneFoU9WwEQg1yLUV44=K4+f7g%)4q=0jtds8z{b>61HL~@I zqSw<;Kbg-VWtIB6EA3h|m231DP4sFu&q2%c_C2S}Mh45^fn~2+ad`Pa2GSh1@iQ9< zMJEE@?CCgklzIs}eHfkUu_bK4Sj6nq@9L_wqOe#t5KcJ_a@zpaMpKSDY?S;ysG0hH zZS=VA%GEQ=H4+0=q01FJ3qr`9M}Vz_ALC51im$Rp#rYj4Pu@Zbe=@f zN^v;85H^ibvA23ZJ z`g&Ehvs1@-0M&3m2HS0X454HbbWsI%RLH*#{nX;M*_&HCk-0d#K8p$}guxPBZL#Hs z8Rft6#_OW-^hmIe;D+-Agh9m@pMinvXrGi3R(o4#riF++rb}6{{}1tvssF3_Ddb`^^Iu<`U~bXsXc&Ea9)NueAO%J>%L?XsB5^c6A@hKIZ6TGL|D zqIYxZ2;3Q3Y5>Mb!IA@$Ehmzu<1jeD!Oek8l6$PvY%&-j;rQsWH}=wr0{;k_%U)Vk%Kf2Mhx$udaSMqB@xF zC!euumGF#{ei3iJLF>@8-0(yyd|stlB1-b7(;d9HAppKFAX_vIzV9Li&#ZR;$%vvk z-z>T5Vfx?3W9Lt76z87*xq{l@9asJH+F#!nS6*^g3PmcdA4((g(o28Mbw&k+jNO$1hDni28DTXyVMryNURtpK zkID)mNm<06cA1hlCbeVHobwth-~;j>U%b9H2j=yJe~!NR;4ZIMfklPCyoT!tZ)mQF zkvSrA6y@ zIhQa0AclvS7OjD-N&YQbGN?waz4pxb`ZvE6-~QH5;ytvEP~<=gdsHLg6eWt&g>&o~*wnb4~Si0=}d@h1h zn9nB$cUFA|sf=ok?G$ky&Mrvzh*S*)pv)>VD*&{xosqHb7#;76x$|bnZo7XfCrSO` z);~v6LuV{rv?QrI{w&5ujR8A~s+^r7C}Cf*o(?qJCE>KzP8Lkla}rszT3pz6Krn+! zo{Z5OAMb+55~VUu15B_{q~(G&)AoZmQw;gn{hI6Rm>Du^eO|6&|hyVw?6pL1L+im&4D{8-2p2@ zd=Z0XPl~aCSH(5h#PFmlDPp033NTGt0+!4*3<4gIO079N&9?aqP6? zPBDA$J>&oU?83Zu;(3Xc$ss8MVuMmuuF^6RyDCHt>l;fM&0w&;a{8*RIbS>dig@bD z*A(poK@`2AwNbmO$cmu&lYLf&Go(VW*Is+2*rNx3u+P{RJZ}>evmj7Eo|4C(+GYFr z*7sMR23~j7{c+hvw?J~-X#uyyX01x($ zG{}_t%Xtr`G0}f{$Y4D3k$qv&$+kc(>vQ&IUtest>1Hu9 zyd>Uu;~!b6#^5qjfX4B3WQm_*yF{nRX7wSWex-zTETsMz&TZdazg=!=0pnl>p*EM( zJcry)34Mra)4y6Uj?K!U=R@F~*|7Xwd2DY5bhbCN@iA9~*Yd#=m@J|;-plEkL36H4 z-%#U7Z~)RuOA5V&{aZU*LEX*!gY z@T?V^cLCk!?elNaPb!>&LvtHDzo#(*2^^{=gH30wkaUdxKGMLyzw);tFPj=# zN>^7;j!EKq8k^(Z4FHqkWfQC4qu>#tc$ww@;o<0G2H@TBD~iOVhAHPT8gu6! z7#n`KQRVb>cwGK=7CqwVFPtHbSi71X@uxKN(CO%OF_vngtKIg`A?7HVB z@%3+grs70aP6Qu)=#BW!H!jMl*nPe1TjqBn8I9I{Ppq@<%-Cj|t+VCo&O7eR>oX*j z`)O@K@uOT30g&p2Kz9MvN^{wdCVvuPF(4QI8_ub_`~7I`sEdF4;%5@XzUii$1&hUy0YCLbw3;%Y1tY1^xKWtl&kPg$L$3$H)PHl(l^pQVt zco^8Dk3KpkPb$a77hRghn#Off!o~(hu7RO~(SjG!9;7ZA3KJMCc!Q+E7`lwY_WH!x zpF~jZh1=I~wq7oc6IuHdp}IfBfSg^FtApShtCX&!U)_?e}wp zUwP$~T<1)!+gqc* zZzw*#%ewL1b5HnCV}-q3^4?&)@!Ik@??*q6?(U&zX`YzT0)|cz{6I{a+!==-erWvd zZ!g82civs>Lsi~4{K%%UbV*lEL9PPY+Jo$eS+hthkwNOsN;-R%k#9sza&qOIGdX!m zdz^UE=ks%&d(OG>-s0X^Z{00YeRj#xMY-Qh;i$!iYqN&TWC|ug?U{B;=d;hhdGAci z!L$pux*&%YJ4M_E#hE0nMyX|k4Lah7=UkA+dSrw-4UF>wSl2LK{zE}lEc@|jY=ECd z2Mic!#LvXDnGln+9oEXR@q0qA>6g z+-EVZbehDxVluS72KQousliYF%nC*a1WXkncz+wdg&3@rA}p#wm;Usbxap=_D!r1% z3LMdK<q2QP6djBD+6Ry_7^ssr@}9(46Yvdh4&9oTyj+{(7{xO^D{^_QX_Zr^(G( zpXR*ify+_1h0#*8GVhc9aY?#GPpUJi5lL+rLcMZ0d-a-{o3aeeedlmYex9i$oFLK% zjf)x`VJsDc1SY35Sh}<;!G3r9(|$EJrq7d7FMzM(V{!OVyT-{Mk`jLP^ef}>M>T|k zd+1)aJVu8{lBn!2J5%M2?{c-Z4Gd^_M)v7a6(tpB z8eUVT!QTHKS1(+NK`Np^xIL?}8E8OlXuKAhMq7J*oP6q+GIF}?vR}rsB}1{^I$Om1 z@4Xw1Eu%TRKx*Br6p2jIT3vgCaiL@F}V3705RIz9|qT*U`aDL+XvT8%m%`0acv|{`2B2lF2uap47>)z;s_|y!cju$ z{%C7$OZtBjBI$Mp%L|Q6QcH)r;}s=Jq0v|uSc49M2Sw<0o)DCuDW}&?jpfUh=LSD; z?hbM4*H)}@fmK}i<45AUYj0FhVF2$tsd!JYCH0U|olQDMF?<3nN~7TEg%eKInEGU5 zbn`jbXnel*_Le-5OP4ImDcz(V%qAp_!ZM7MW!ANK%O$$o-lTRBHEw%gRx<(s3|6mI@NI$qe+XP0JBImCD3`a{fSX zPKcf{V~sSf+LuJby;&7L?8pV#?(@vk&&EA>KNjt+)AM>A6Y3Mkgt(jS0I@($zc@wj zuCP$eh*ZNf4hQ4Wm_-oOR2+nue`&r}77U7}`f{WAV6}k@*QNapLr1}(If=4F=|q-G zqf}WNv zj}gOTgFOf<02~t!%heg>6$+`1J@LV~rc*!vy*3&VKrGC`X&1le5ZOOZ&PjbkB}%J( zy`!WE2dgW~fR>=dW0)Jw?JoOz!tAu{?Q!*$_vSpuIdk`mz4qQV zzW=>n#Nu}sRfbVG6_PO*CdGYWPcR7$$2y@RiN`(QCSvaMlYPfFh?@alj*5;g; z?#xB!PZSJ{Gu7=4HrOz3x#i|O_!tbH1Cy7*h01}U98M#;;f5QxMN>XAdR{89k z1>mSS0r)W(?sNlS59rY7h>ng)d9Bj?eg3`7hjgDEUVwRQY%Wm^T9TDp4YjLkf?2l2 zE|%52YY;s@WKFfscnq6_8?I%1XGBC4QS=9zOzLa%^S0LZSl-o@!wzixFl706w>4xg z?1tyMtgs9WcE_@1ACO38)NsYG|0RBP#ba^pH8;ehiPNNg#O7&ihyfzZ=9Wset@dF! zpHG01p$e!AWK(KnWz|Lo9@baR_>d~$0Y8Yrp^>a!I<;nf;J=tnY>dv)DfdtMx@_sv zj8<6}8yjPS{8k1g!@JLUzw5rm}J~AZ0`!E=U50~?So_ohSS^-hWT(@`Lh0nd=LMZt$EVR8yb%28Ul z|BqACU1=Lul1(aRb(UEh5T_SGPl&!Yw5x-Ovy`+s-11NxDbcmhzyZF@>u?n159w6W zM$vlX+21#DVz!h}-D5Hg(|-;AC5@7*P)#e2HgGwnPMH=+a}1AkB{g9C?RUsg z@g~f|gJLkq4L977DK1WA*|OzP9*3;i6o(xCiB;4txR0~Ga(O)S|oRI3Ui2KP1k%3f)vn27hUf8G4Su*Ymj#|#iOeF-A3sk8F z&*9l?KN8iHqg2fAN*y~Ha;Vpba% zUx|4Y^5jV(PWHSi+?mad)kjBrXH1wdIY!6IQ9AU1ALs5gXsIr0l`IWf_H;{Ehc}aY zgZNrhm2&D#QA)-G)Uwi9bUOPnSk8-Ujg-8JI@HAx(qM<$&~o0qdGX>4f6cCUhngm) zx~k^L%C32^pC)54m)@8>Srs&1v+usM!+QGrm=$t_LwJQy>MBF?x zt*S`DX5%${?#_u5@_J5X{k}v9r<}PetzrIt=VXc2IGc+G{2rgh(XE@xsb~q0QMJ%G z8wSKgN50R-<$D8zOt+TUo;&nS)8KyjdxFZHo$=y}FXn5ip0cVX&i&crDPlq8l@gF*r(#q2s7VjH5hts#~!dpG$%sIwxKqaMDNTmzs64iyzF z@#N5)YKR5!*iKt9LQR8p@@4VDb8lBTdwBG;sU`k&^=c$FopP>Te_u-dvR4O(`l7k1 zBO^FzqKSCPn(n^$*73EkFIa74b@?TC#w|BKl7}a8Bd7_T9jU?9-qw-tmxE<0JCBv!v#9>s0H!o zKi`%f3pO%dN+ND`IRYNw6YsOxW}9X6Ca+5g;DXEK2Q}}IomU+~(LWq>_W528DR709 zb)jm6_aPmHLO@ln0~$abbc}(RHWd-Q!Hn)$9~o&K@SMAiEODkDU`lP#)7zIDJJ-3O z3wSDt0bTw~MGT@Q_n8%`3+btimAyOb&q({mdO8rOXffDxIC}fweNBmF%a#_S#WkZ3 z;5pgr4dqz2?7hU&P!{0NsZ*vV|A{m=qjHF&1wby2R$a_r_^}T)SZ99a(s=QCh?p|q z^6o?<;_VPQqCviF`Le`y_}X^cZE|=o2D-g{Lhjk@(JX<~5yk94 zC^bcI&p-~FrQ$2u=W2(Aqj?*vTD&-el>(e+ub6&RdvBTIWt*1vRc{|vQ>~G0gJ&=} z;&SFDM+x*qN(hG4nU(&L(Gn@)1sP$ve96Mm@M-cWxCIAo)><>;l~>=$(eF7rDe985 zg5d(#b_vvYkJCPC@K`+c z)YBFHAvTB)hlhkZElWy7F$>Zj+ndN6-jRZy6OIWGohXjOqZS>aR=V3(W=JT2~CsC;Oi`JOpgbMNLaB|+FB%0BXqQds- ztFPp1+uB=H(JXjzT+_u7pE3T;S`!SUGsr-P36f~#;o=r(SA-?)n_Xm~ECh{lX;kZjZvurH)=(hC(HNC2?7M-1Ck>MYroD%jAW%DzJ z%NxOMO`=m&$GP)9tbZ{)xTb8c&*Sy5(dRk|m`6h_S+=b5bJmwl#4{l3Vl#smS)ssl z?`&wf2i7#f-DVBcbjq0hcY~uDVL_oF%Hlprxez_ z<=Vy}tGFbbG!2A7CYr$lC*z4HUyh&u?5f#I zQV%>19}F*(?E35(a@L&($Y3LFtn~d`C6ov-90FM zY9j)CgCWkk0P;LV|^2hQ$NvBh;Zla)!0yUX(rvZxqjn z)HVlDByUWhy}hne3f9n7Pg6B`9nbs#cx-3Qt2_h$1%vhI!z$b;>Y;$sT8Z!sY3TL9 z83Mmab(p3tDn%*w+Go2s`&)-r0N>g-;u;tK>`(FAn;y;eYi?D`j$~ex73dk%$5vZ! z8K3z0N8@KdzfAl%qLe}LrdT5kgzzeOOa0(Jmxbapa~f@*h5lukVE zit?Lpy_p|^k|P^|7J&5VEp^e?-yO#vw=j0yb<6n8Z~hWD-}1kyRYS-9gwClcWAfg+ zZ%OB8d_1ize|Lo`ag~OK%5VTjQ)YRykvMP|2%;5Nd46ok9ywPNcO18;q`sLKN~>RF&R2tw=w1)`iT!USjWu!aUeQp zyus0@BP#C2(NeDA5Kv}Q4pyPX)~!>ORL6)v8MKf_PjLX@;&AYk~(Qt zA?bkhzKw-=Bh^O2GLZ0^CM8aj5$(9hv-akEc16l{!C+Ky63B-t&5O-$FqKu*eJWz( zi=i7t7Caa9uul81QS~QZl4=dLdn=t^5s~^3`<{HjbjYYmjmKX5Y?YXmqsoV+{>p`t!1GID65CwNa1VWngbJgmZ?a8JXSRkun)R7Py%noVkVib*sy$PJ|KX1mVNO=!;n-dM{BGYmbagZ zMoukJ_^fiKjDxYI+QU%E?V~8i7aS?>C+Q2t6d;G{U@i!|CE;;}N~yeniUDfkts2Zy z3=VYV#!^MmUca0v;Ylm$IT2wUJ@@=%p!+>)@hTfZgPVLVcfdQ%^#f%M&BP~C;?R)l zZM@0G@u%BvQ#m^;Q|Mu-vg>N00ncnt$!2%WGUUs+grgufuCd2VBatOXAmd>td!5{^N`OL+RTV5 z?{MjylJc6w`8?vQBy8M)0o<$1stRaD$5~Ud_}#==!R;6dGSHPcF`W`XP(-{a!IrN0 z`BUY`9(^jOoV@(o_fGX*lR%>g;Tb?YG-5ZoBQa7-y1lnZ7V)-Pgr>>#i4@ zZTgY;&2N5_;UOEyavjEvGB++}9;T&kXh?n)?IRy7Tbf=3u7w{43qiWW1eR^K-a78R z_rBE5NpGGOxjq|dmn>aU`MyKea$PCLVkoOsN&^e-NKt0?;l%SL)4Ogwh6Wr($s;I@ zB%+WafelB>w1DHpNDL1S#GZTZ8SAaTUR-hc717k%5(|&mHNJT2O2pALSQiu>^j17r zk-A}Q%D{N~q#~kWtw`Z8)*L8$)!0OAs6~qx#ipBX7Ka^qc$|O1`MH6-QTfxDm8&>1 z-1^M5y=yK2m}bJCq>sohXK|!|AR3z5vhUDQjH0c5KVFve|s&8>5_B&|1 zIOCt^Rg@=Ij;#Lcg5Sh7zq%_vy2B1>@E?ET332<1j*gV&Eqd`pFB!=ZTC8oQNJ4}( zE`~Y0Ea`@UZT720r;_$50w#6#Bq36N&ID1)v}se~gcDAPi!Z(?*D4KhJ7nh3B;T|1%V-)WS5l*V8ly4w z-DVmJR*b~6c6LrkuZ27Yr9>91T;fKNdl=7F9q+Nc_M>PRRzQKs^0?X`h*$js+s9c!_M zS?jJHFTe8lig2ALOB0PD!DGb=97=}WC8Aj2bLIvS`S5q{384L>AKgCgxZ|!0`?{%- zIgZl$mlt)Dt2TWerUcGZzIWK~>XID_KTG}Y-rjts>1}iDc`5dv`_T_o9R2Dk7soSC zy;0zd99E5;uXyFeVJQ1+$OFcTuLI}p7$={(|7s(vpZ(V#;x{)wRN(?bv*B=L`9J4f z3SX7cQZz^P8Kdm#E9*(7S66B*oQVK__JihFj%I6(E6N>iALP;z0$iDtBUXt1rMIGLL( zKcy55hfB@WW3ksByT+kM?4CFJ+_OvKe|~;Z(bpyqjn_+sfMGjVI*3bOVQocoftqz`;=@G0cUW?oRyZE$_IwQghhv@j!Nt${C(qIHez4_ zycSPPmFuy`9;+xm@H_j@+y28Pk`FvDaY3b`G7{Q&5q&xOiVe;o?x~POsluV4!DfYm zLGeQ6XpuVsI9aEWA<{ajH{xJMN9lg2-AGCg!!$dWz)#>lsS4!5A+@|1r!hDtuAc53O>U!Oe8n45DMd8`7$e1jMuH4*QO?(z3L8v1Bj7K_7pdK{%KlUdPd;;1 z(EV$tfMF4dX=)HdHKu2RwV?MkW*V|%?Es|{O z&Jjl=O~r5+_5t{*=^bAC)KAY&HLWk51~y`~Mol!Kaykq<*5$B-escB^4M;>?PLD~c8}@PC&eFb{YyOf(1W?T z0W2Y_!s{WrA(|ni9UkqGQ4HABI&*;gK=o!c;$C6Q?{!N!mgftUdPFEuMJt z@#tFKoup18XAkY-r% zdMuCZ6qR)(9Zi5ZpP>#I>oE6_kH?80GK2zy^~_VRS6WygX-=9rHHo6!J6Ikpd{x_q zWB)l)%~+kW`soF?#_w-=tTNSH9UI`Ja+uuT;80ISnnd|XBr!)4L(aPCpjBxAL`e-z z4bh+YAicN3yE28$Ym0J|bdhY`K_$a&H>GGu-FxjVe$N7twSh!SF$OOOu#QoL(iCN- z=%S?%Fc=S`CC84|2FpfH>m=jM&-{HyInHv>$Y+o9bM^|K2ZJRlha8&szt@Akfs^Aa zKWFhLwgMuuXJJ|`1pXb|Jr7m(>s#9+owKMB5u0tgmH3bI_x@p-H|C}Yb?F(KG_57R z`Ss7ov(LU2S6p#*dL95TjK%nvdVz?()G9>dvaDueR@pVrrY4GL&WRa03iBb_X$%(9F9Ax}>C1av?Afb-H|$&WwK_807rsl-?dXh`y3Ymu1!UIBSR3upp*IS6_uiPh(yD3=r=9q-c=hEEqN{6pEMMLg9c`0ijp@@WRVm4uaDWw6q2XG&d(fOd&tVL%FXh9SY z-IoOY#-pi4T5p!tZf}$GF3sEP2WhbEIq(qcZ!|Uj=WW&zIs897+-JQkHla8a(|^-;VD1HPFM^R zTn(HGOs#gvC&*c7tUKp-*lh2^;w9s^nB2nq>_2z=_~J_PC>X4x=3J1o;uOg#I!c4l z1}4&%>}E!D9eVVqQW}M?b=F=xuMK{HXULwtY|yUKD_AcgAl51$W--SwiN8fD8{^&D z2JNBB|H%kB!FSR`T`&1l`8`$GEW`%(AmU`Tb%%}Wbi>X7=j9T%JcRy_Dv&uxTswb| z|0_6e6c-S+8ySv`HklPWer#iBZk0d5uwH!b?KI5WZnsso`8@dG6B&VvxGX&Epo4de zFP-w~)dK9ly6oO~@{zZ47AE)a^vJ-qtHExqsis?sLPTMxVxl$zHdww77oJ2?RvVg~ zfzzctLxnS48IN+ug%5l>)1qpv$eiiaV<{;_)l$wD1E636gF-8evjZCtJLTF!h#d_O zfUD3chg0wivy&yO&b^{K75&1^O?8#mGivVETW`(1%Flgv{5}c1H`Zk(IV0;l2OdGD z3VveEr1Kc8vGHi@029@~3LeSpAEuF)$-`=~&Gtd1aUViBx zIe~-9A)bsrx9oc=6_vHLnqrFc<+PA3S>ITnBYyBweSO{Osqi|31CUeOleT~q2G@fv zopzSPmnw)5Hx*3{k9242LpgCQ9tu^6;b?3ci)qs)$A0_Ij%%;JImX9m8Q>|@Ml|DWo2|0-5KNZfG!|%d~ zQmN|fz{I+m1Y=-8saA4N39k%V)>x1N--Cn20nRhWe$1W|iLvN4O_j64du6sTFj(A+ z6h}n_mE$)a$Vhi%Sb_%3iNZLT^sIuKA~B*c@gGIg7*EnT5vHyw&KyvVWxF#i92|)H zlDLul-WFSK5wE@aT1=QYAq#U<1Q7$p5v3#L)HcenI>IOJmhq}q=!qUvYAOl3#R#xQ zpcc9hvnQcD~e?TvB_ zCO9ezKtqg=Vo)R6JJeocuf~0#0sq`{&!yK{c~rSW;aa@Pq)D$$E!TwC;eoiWD<}MY z&LKO;7fxAqRQ+cc{8u)ba!<5kU_40~=yjXW*%7l3*gn2|)~YH6|F0RV)Sm~}Tj}dx zPP;t*`n-AxahjbSle2>4X-E9I&q3S7DQB%Z(~y1h(+hqZci;YugzXAaF#A!uVVZEP z^hVlHU@fTwR}3X_b=x4jqgg{Kybg&aML*gbe6c5Z<}!SWER6ZUg-c^oDE4wcj&`^P zSf(}Tlj}49%z_}EB!#p2ebf@&Pz?lFJIY05G4HD=hS(8v@7#yeU(p_bgfv)}tVi7h z%ACAj&N}=V$408-y<9>@|OQ7N33!B6S1eaP4%sP=KXK0$j@#hCa2!AXC)sqWO)31q2ZZ(c{7G}-kYhEq`2Xdv|C_;j;?b8Y zPH$+4kyo?<6(Wjtb*WbRg|DoNf3eQDT=!sHdC6`0>`~t!GU2&{y==SfwmC|Ry~#d@ z+Cx>GhHppXh7olBL?SkR!{4*jYjn6mvdk2ynD1%)ir1@+N$E7zQ4^%S|7{cui)8y) zQTZHCDu*O>aCEKh!or{!Ahg)T&=IPK!$pf(>Z4cNZn!55rz@3C+mZ}ig0|FMA$aDD zgkp*m_vYdi=Z5{B6NkFH3L-YZxNuBkf_!FwUSpja=xf|c0*8G0cRbjaLXUzQ^@wW3 zS8I}m5nCu7zVG>!208(NmL{ z{dWfwH)?KBcxkXY*}VlWq^ONhj|dI2X#wCDH_rzHaD_AmrlV`9rNUmojB|PzbCwz4 z;#gmdjP+({kKq6GY3rxJ{Xjps39T(IpUPAC!DCpcexSObkk}+qh0oqBwVIO?AR6jQvEByLMI2`pSsZGT)JsAb#q+II|1jQVew`^mj=KDF8#o)s z+-G&@z7J${MfEqvhhyR-1rZQ0!3YuJgSsMvMekr0Th3VwJ`dR{RaK5q#e6J5@usZa zmZOxj15~wi<&ty5df=#ON$PB6_M{3EPCZ#q<8}T#4hE#{rhD`6aR%H$z|Ww1r2J4w z8COask}_XojTt%1F(WL$VJD*OvB<2hJo~GZeFiu(xN0>C^6*Bw=CaEq%Nbc z(Vp0Q_D(VP&`-w2|L4|t>d6Wy9X=AD+I#&%$4daV zys@wX(&vWcfoV+*dJF8d zKqt)?7+(w_NYzO7b!szmAXE{h?=C_2tY9h)5+0(P=6>{hV=-}3N36Zxv{kIu*Gc%I8-1f?k%rdPU`Hw|%ybGrqB)0%os#p=++ZBYt)9ofRo*4k6KlwC2^|Z52`9 zpAD6(!bD(>6r4)dGdq&@n_@7lhq!XM(fl%9AhD_U-Vz{a?%6HbFu(vE7b*Ir$X_e!J(*c z#QOKgyajv5L5J-eS6}kyxZ}@{MU1yaDKJMv$yon3||M7k9#~U!Cy&~^7 zo?uUt7+T>S4A`?_d64{@O?n^(>np1otk2H-&mc9l0pdDR7X_3QcEIPvP!ok_1M$Fc zMw}91(`YeaCc{}B;yLj8j2ufEz<^=^+Ts*2C{CoRhWjDQsvN>7_n=lc6Ju-{d`ocz zWmgU-6Ndm-2ZsjJNevD3yScwYJazO;ih_)_QXFO@Qu(FljzJ*MPr9R}wJA2*Xrru# zE?d$S^^F}-9)Up#Hq$8px9&V5xyLaYd`AH@+{?CHBcfW$HMSC{Y=P)=I0WT5Y_SCSa;o7c|IVi zCV_Yuycqr=lb`GIb)Dps1iD*8}h`9W!vtNt(NP)V>n%%U?*R zR~N&nMxYO{?*92&;R%+#gfdm=Q}pcsH6F zC&lO>kU6j6=m5LiI-oPv~Nxs(>VDo*t-4R>p6-Y*8r*@t;sJdfJo$3~Cu zIT{yUUi6^m8CM{J!zExzrQf8`kaCt&5CP{$kdzS?2Fqb!_Gi`A#Q=KHDs;r$gJ#E` zpWZ6!TRw>K(%YF-fZ8{XZxYvB`BePz*88KqK|r-roScJKukLF;ls>dlA|eI>?uw#v zPnCopY1)bOrtFdM*d1*%quA1@fl=cyjyPEpB1Ya9rZMj~Jj6sH8Gqz=Ie_k^Ti&%SrcGTV7A;y_ zaBZlvVX)d0USI$M_~n*cZI+Y*5koZ**osI^Zrzm931XTneBs**d@Mj@~Wfi zzx~x;#$yjLhP|BYUYS*f=dbrnJ8EZ~WY#E2w)I_Tc1a3_&;yKM)dqzfsHX`27RtByG#?j)c zZxB(F_Amae`*gZaOrt%c^Ux3trK(hni(1u=8J%BeNf{XzvrbK%R!{?mg&b8|63&?x z&bg%M@z$G*l57ozMSz5W9|ZN%QTzR{+||82D<+8SCslr+BZfx1qh;cF9B}CDn0?T8 z(bcm&8XCmQXeqTu^LR^K|LYs#{(B#bu`&XH1G35xGh0+S;3j!!^wII)EMXy_t+6sV z)F7pep2?9Jk${Di`qG&V4-tJ3R>IGcia|~!>F-HJ2@X$|WElvMbKhijxo3%^3AFW) z*aY`cU>Kt0N|k3~r2}0JwqkEPlsBgfd08H84hv{UH;6fR1F`SCo#LNXiV!eZUs`x! zrfVrfl7uond?HE;1dUDfevPHnG&(>-eNEBOh$x>2UCIFafMn!QOy!2}GB#2S@T{Yy zghSAj?IFCLduD@S@HuR3a8d}jbJTi4B4L>1itKb?Gxf?yR5T8r6`O3rL}XQlQn>(! zu=B^ZiYK3ZG3P%vH%*GBhPJ#%q^D=BxmHY`JTad+Dx)8Kuq3|m&9B6{=lnE!dU~@T z5`68DV|I(tzp7Q)fwXjx})-N}QI8kwB*lt?g_?^5%sFY$y0XxEOKmnM(=8O%bw7JDv%SYNf}dmk z8*ccKWS=u8&f=igoN{+}SDbm~ zY4O|(FT}lf-ybdIju9G^##O)kRlM_$ zl_3^_un?XMW#^VQ z4dsNJ)!xyfF+vP=C5WiO0g~@gY%CVgAu6u{V3v5HDws|ajak)V05+%$vCt6Ga&9Ow z%S*BEypP7`PFYdU34?XQyq_e-ka9JtasabD8!~dhCA0p@B{*mh7@?G~AvJ}Wbp;zi z-qs2LRJw?ClJbF|Tp4K-t-pa#M~)aMRd2+tVIw+K@_O4F@^hXY>!kq}G{tMktQvn6 z6=D=PoHz;Oh}K(w-T3wokBy)F==yl@FAqn1`;-_PkvtDd1$^Z_)HlWlOWx0G9dq;% zaq}&|&B!be`{I(H{vo4K=3lZV%$^({ui|5lJ{>VwbIWU`?~mt< zLW((4r%sEmF38S;CFGD*b;D@jR8lCgT%Dz$NKgHYV!<<@#6=GgER~U%4y26K;5hGP z!^*WSuKUBf02B#{=n^&Zzia(u)^w49OcZ8{d!8JW-j< z$@00;pMo)z$$?`Jq*M}yDuoQq&B7s#jprorh8FD`&I`f`9c`1cJ*~8GkMEQj-#zD` z1LC>oU&t`BxzrHrtT!vh8wX;y{dSBkw%Z_cXCi_G(ECTC>%D=v?y75(So+@k%4@lS zjAX#001{b{Y_WksFG3k#*`83elH>qSK1fZ8G-Xu^`4IJ7rG#HJw>Cw0S5M+=gphn_ z8h~NJx_X#~DO*oec`{}|#KokVR5d81S$SUoWv-5Kum|+RR3Z6d;*e57ja=jJimZC_I!p-$_{K@q zMprg=fbTTldI*TpoVt`=RtwEgb}3AmGC8`sm*s#5((|2n+9Ce-(yQ_Ad+(*92V5aXBhTUV z2;ajPcT8xBo*tT_Atp1VW8;`Sy*1=x|Rh74^`nrp}FO=Uyi zb>vT+3(v__SUXm?+VC3g#YT&JH%4otYM?(Oq&$D#tEvPO%@JJzoM-VW+qER?V`*}t zG=647W{1f2tiQ(3+#8uv%SDwq@v?lMoJW<^icGJ`mgu8xosiLA>A3w>CA1{Q3jc(@BQG6m@us~UV7#4vBSr=%m^S$+~eiwZ0U^O-~6Yz?~Z$-sc~|A@V@62ij(0` zj|XSm$Jmj>M5lqyr44AiRBXUJOTS|Rycs{nh^evh{%izJR`U2rw6;vll#8K@8OE0i zz1kYn;Ro$Sm#)0o&Z8dots$2BcMBL~nB%}s5&Nqc&_(r78A z9gZneCgqI6Ofi}nQ%`xnCF(+j4~z@mnM37gOI7Kx!w!oJFT61CnK4)xmwo1ZEWU91 zs%-V|o_1xt`1G5(q*NoPOq-JB{0}}@T4_S(J#=Wn+1`&?YtM|q!QS{_$$JUdesZrJ z;?$%(tjeV4x;Op$fw=0TyNi~b#uy!!k*$qjoX$Bl=JzZC&S%32!%;F`%J(o5e9_p> z8lL#MBbq5;jB=cnm3Hg4QM&&27V zLGzr#oW?;|l_yQ<*$YY`Adpkz??j(6WOjOnD|}6#b7wTeV^#VC(34W-VW2F1jhAE6 zWE^X6rl;`QcujM2TXw>*r*O=rBWM4hj3uY9Z$c+(09`@Xz)7ba9W&QkJD&gRi?P!u zw~L;B7{F~QCfxNwPh9*zSH>HEe=Vj=ni-20D+(|;?J$@RWAQjwbl6zjs6$w%YBq9a zo>xo3Tpdd}p>#o0c%$vVoJn`6HMh!gLz9T2*!}I8DpclaSu4;<8j0n_E;st`lkiwX9Ibr z%!!#R^j8Fj!4ftEjUhd7PhYGxb7tZ_7#}t}>xN=LEi<3@T)u2sUTdv2XC{-|)k|-3 z-#^BUlu~$uQdvhmE_Qh{t6Q`#UR;<8k!o4~hlH?Y6@D@sIDEiotq2qYwrW5X~-IzBK1z zB0Px^1+Qd(_Vg^z`vzMZ7~ml5cbZySW0$?Qi7%dcz-o=wZ?1kQe)aP^Q=hA;MV~dJ z;F@N5MRJ-h#OE`q)D#ic8;o!L^*6}%LK21VO`A47KfjIkcp3wxQD8@zGA48(Y>EnE z#i10{Wf>E3*HRF#AUZ>%5H>1BUYrr{?Ew*FF~-0!ApSh8tqX3JVAk&R{$PKD&kiLp zPHM>KO!`XEAyP?E3%pZ(m`W#A&4yx8j=ljXF2b$JzA{oB9*M~lC&sLqvvO|}ZMU_w z=Intx@4hQ%VovCo7;WtxQ5Vb!V=m$D-z#G+r8v4QCQX_cy~`KHS5En2Y_i3=@yrV^ z#hNpx#9HgDnI%#-#~*)pcRc#gtOO`B(O*h{t{^c8=i(7y5cwB$|Eirl0j2Ib) zssQLOzEy^<=7tsFy$z8gJN`YPL-NcXIYj}&5s5%ic{vPDsz{EhBcQR7O+k~VDtEKz z>=2(>smeu4_@#wE&cjtr!Nk-iH~0O3zftWWifwIc%E>L6_7u`TY!cQ{bM}~`ti!^8 z&5hzfIR&tuJNJ}7FD7cc8cJxda9*WrZ!kxptiOZh3@(`m>N zPD_?7%{Ablu&McdSmYQ{oR*(0@5%iepyj>SnmIjY&YB*FAHQ#Gw4KJV*M5EP^k2o_ zo_{Yncv-DNuWE3hqPCFC^i~;Ej1=h?hKi>0kzx34%}G1py?5Pf+c@!S2d&m<{q~xN zALz9DV1J@?!_-hA`TjJV*#;Z%T2ni`q{!9U2R z7)B00gh`n1**BZ7BejB8W%6)UW!BEt_r)`FHe%zNtnn5(VFw5NjPqz?Rom~!J+u}B zU44Cu4vA?}y)HpJP{hf8Fic{w-MU@8hhY@{R5~mJ50>dYz&*9Mwk7es70w@W-#F*G z`rIp@ZSwIkW`K}6~65*1Na2VjQs;Gq!f zufKjg@x#yoFQQLO{ zWE{SJ6^?0+$rGle+6hytOAB}VR+-Jp=4qrNFjkJ{1_{&w^1kr5=W@eOpFzt+DV8l= zo>eIx0HiJ`3q1oGWXVC|#u;bs=D|4Rn0?~t6IbP7edpvW;)N&Q z%w9JRF}8ew!!(X_vM3_Yubfc}+lq}3JkZkE8YT8nxauY6xWmUcj3~+3$l=~o4zj|pvF_j7; z^vszvVv8*{kBv9pC?n86{qfFt?KL=IO|ky^>*Z_df20zb^A(D;i|eC`iotsE#TT-Q zii5D>aztfvC;v9y<Xvb{*EK#^ROS_(BbGXiab|vEIaespd?euq9SkS z%Ad$8sTy2#jEpM#$wupXIy@TkwE6zB|B->fZJjZ(b5a_tw&wOIeg41;${3D;LSd4F zni4LQOi6W$H)j(P3UjF1TNxLxa24;;Kh%|>9Ft5o*<_Po7T)#OU!Pvrmg}1vAt)E|mj zv)0OuWj!bxb~Y>(B^OELmD$jW#&|Y-&n+xgrS$q_S-Y{?Tx&4aM&GG|sRGFXN&CmrToitt^ijRC`y*T8MLt@I*$+@rZz2||r?N4{5jLzpje?qo5-E`yc zvqDScE2(Q01)LU=s!Q!aX`9cH@7s~NzwhUn)pUwIMK_8KR_?Q;R+Q5ivvzJx60N5? zQQd3nENbO7GX9wXKv4}%2b=?VMMCA@PP_f@i4!Krgm!RmCg?Op=}(uvR$gY}%+JCS-UZFe70fP~H5cw67tm>-0Qg^f-{iz-%n;XAHpY_^NeVB(d&0TGuozP^c3{A> zA){`K!j6{QP!Pan4%5Ga0D_nAKP`aIQ3f#a=kz7>z1qTk6V8IVA1ng5*S{LcGm%e zs=&|`fNHekn$oyzz4d0X;YJ(AEjQnsBk>}dqM@D!A=k5ohySqP*IO6c6qTGM~1G%m_MW=H@J`>}{#)yS~jm4U4&d30Q zXYRw#&(y=Jtj0z~@Pw3Lh1(%0n1h3Iy)~6eimS?^Fwl~*VZS;m66}!SNTnY#v#6r) zHP_Bf@5NN}%vx%ux?{{zL#yDgVX$!G*&B<&y7|Iq%d=)pO#Sx9AAdZ$ySlQcum}As zh7h#0C)b5M(_wKVQ(?!*y%piq)wkwhVj+mCjG1J}SIEQK6Yz0rt*9$e)j^Y5%v!Ls zA*C`*Z)l3fhIRqgV?y;Q{gX8-4MITknY%UVJ{6XEcYgM$DW! zGd0sC>#Wof$8P2kYkdAt8jIscG_^K(<&amSxy!Et-Xkzd?rg?$f( zb>@+m1P#9C7Or?D|QCv?Q}uH_l^LQ8OkjR(s;xl*WT zfpT0XSv9-5Ecq= zXq5q)M9jB!p$zC<4(mtC#cZWFk_CIe@|Lj9Cf zX{Cg#UQNW}An9fr*4&Z;a05)DF4B8Ey$&8dk0LT|v-A3K*1xU3jpwFc-WNAqac`v; zlyx(HBT_Vs_4YJY-95A}MQpXz7MZ@Gsh{aI73>DcvrWZxX+^q3=KVtyFXbv&FlDiZIMdGZ!E?TKq%f{Qa2He zP$W`eRH>op@ti{j)+lYyD&pj+8Qmk3oU%fn+3!c?3SgGj6R;ORwO?bU>YJjiMHQ+9 zT*0}jmNISwgGuo-sWB?D17Mbzm=CC;16hBJH z&4IG2HsNrHs?sxCHz_nXFz59Z@-;jc-Ulw!MjLICP1BkF)kTgNPZikoqB

    XTdtI zEvLjNIXrldAc(!^Zy(2gWktnSo-+pP<>%fkiS^6VEFv zkFPg@lfA6Y{7+R^b$9iCH@NJeqU<1>tcobRA|O%0;FyU?T$0RWMibYWEHg&4nSUlG z8j~59M2(4p5CtRRhDHTJQIvhZtat6b_W$SmJnyM4He31Jd%LTve!utkzGr#PbDqN) zW}-FE3_6Lx_l~2ntmd<~QqnvlPqK$}WYaU#;#vXT3kO7#N#Au<>XZ68t z9UiqMkhTcuNqVEzmUr}GfWOm-37*nq8OEG;jolu3J!JfaRAjCu%cr75}E z7XS;<;CatGFit(?C9%|8jeq_8zr@*RpA}#I%C-7E3?Ac@czCFmD%H_~H=^BJ70$uK zO>Bs3$3xRzlzbw}!rYtE#7ezBsSM;?7N_S$n# zecpG!``rQmXKHpPI;&Jo>yF+slBmI^3GsnemR91fyYAM#j*m^NfC2Uczl*hb?D5C+ zOolNJjC690oW@&v#kdDU5L(fg99Qc^Hh)X&zx4X07a#5v$hFG@lLZJlgy_X-uQ)1R z{|pA}gKz)Gxa9|T=~~l+lWN$Yy5))gSt--Ka8kQ~{7cHxN0x=QC?Ci*P}mVQn&#}& z<4-*(-uB)Le}U2ZZe0198?1FMi64ln$oW)awB|BCQ4jZnObd;?Q~^yGcKXe}WB%l( zO&f&?lj=iD@q7c)Ly8zsfU-r${j$yOf+7rzaHmbD2aW(Z`0pQ-zInbMW%E z>z*lI`I=UuR{C$*yBpW7ck{IPQjE~+N2lK$q?&6}c|1Xf-pKi#t1&w_6U!?*wZ0iS zac_Bkk}bf`Ndu)$Au0-lWr*IM%HvGISwaRc!n%7A6Jrg%4#NuxnLWqPDsOn%`}^Ds zhZ$C*y`Fhg*=Lj~f+|qbfbvF2?$y+^`vte}*e)w>Pg{{8!cwY2p$1%L4Wi*bRoW5g ztwKAy=06$xhlRoVt&9KE!!}w_HyV?D1D zbk2MI*uc84m6ZW&Jc|fM3`SyfWGw1q*%wAUVMa4bntU$9X2BjwkytNRERADWD2?af zv?8CoaXZg4q*6$@l;CRe!`P%wpN)BHFV$+|^yM|H5nY;7E<6z7B>1fe(901oedP<| zvfqAsmGBRL>;J~jZn}3+a!+7a0B*s_vSOD;ke>~zSjjo_FUXLq)1mPHgzuH8<@gsL z6#wOaUabB5OkZF4)c4}w{`m)P{|X`>fMfCc2WdBk$Za_zGU#sO^13^6cSOIE9STRS)vBAldp)Mfc+Mq-DD|b%j?_yvTly)RGEk=NpaJd@~9&&1F1a3|dR?(nJ z(AW5Y8O9zA0IRwm9S$(1>^g@I!_kvx0!!h*xl6ns?LM1=!#x|#6%*hvSkn_zk|@!F zb)0{z(Xt+rXW{!q5gu%6#xB>wzmGrucu@;%M0~K(1HbjwTL%QWmtSyfT=Lc@Pt8Gy z@6Z0*=LC}e{AYK1&f*f9UQjh4InrFb8pj-SRGjwG*q_v%bV^sw9y7?KbiRzB0zBsLRddqvVF@4Bgm0iQ}oEqRH z5-S-lp5$08r>Sn}#k&(>cs^f4f2Q(9rrrFT&sCbx>&F=v9T%5AW&OCuKmMIh#t*;y zb44eKD<;Otfzo@Fw1Vq$L{q#rDKGYreKb^ZcXiTb{47Z1^<4Ukg2Zuo~J#*Pcl}*J&RIH60dy}zsq~dD>9&(Ris8R>bgGQ++df;lc zmeR%;8B#(!2&epDi-cTG_0^rA!7R`Q(8z)cV@)*9P7@(K49!Si{4w?d^duL8(1-_H zGpl}XmU)1*f1$dv6q8dkj$nIE6%#NYt^y}EnGn$C-8aYf9b02z-cFO>Azj8UV9Z5| zUS8JoAiD#e0pggOfiO{c#&+57u7O=hklOXZLm_0Q6hkRP50CBeFEF(7)v zz)?BN;iX*AnP;9U5FdkOFKBT!fJmt$7RGF*9vKq43bED}Vf@1!FQ2tGq0&p54 zhTO1z zS}ib+Nyy~-sg81wFFxK=GMn;x80$%3Ic(POkT!d(a5_n7NQPa~C3MLN2Ss(2Q>ZyqL%lE*_A9+>u8QJW=7!PsNu z`ICt9%(Gt{=UsAqOzrM}KhxKhpZQ)~{!d@mnsatKt8P=`y)(kfsEI$>PZ&p>IKDy( zvCXC;daj>mmCskvL}I_CvLXJTk|3Okt8+;$lEG4?jS93g2%c$1pQe6*0TZotqTeAH z@IL4MIlC34TPjd_CR7aYwP(c5UPAiC95ARA6y4JCWQv^?V4uxq%+AaTV%oN2Tg=W( z>#PzK7?z=lo-?qdpeMx-l-;3y7bCG@Zk?eU;#jJ%pj^Lt&Y$+duSOuT6|3BnPB!e| zHSv8wa;&2sb{|P!Hy96x_AEMOZf%%C!px21aIvGDcce0^#pl9ZUG&4I5(4}x_05te zczEmDkW*gE0;&2~^&}F=f@xSe(-n#sT8N)!<$hCK?7}FQfK|MIhwSR&ry_I(ygQo@E%g;K=xUK6}RQdu@uz^_BS5-+AWVLyY)UpS?c*<&$5HI+D$O-WTS&)jG}7 zYeb_Ip7N})R*Choci2;X%`&v6d^!TB<{mz(14j^np=mn`c68;m-cgn5GJ-h5;(%PE zG^}v&$S(Cdv9h#i9XatUiJE9}V&7F2^-O3GjGq$~SUN8BoR(X1%#T05RWQTosJkd! z05G0RbVd}OoJ-CpJjC&dF*!6TJSejwx;0cZIWZACcJ3VPDM{F9Xz*yvP0uPl);^|7 zfz}*AcPJR*Ar&c@RymNvY+5dk1s=u^29Z!`Hmt=HUG+ib0hSm#n~ zJ?0nY<>dy=!5W&Hax9j+undQXFmNb23^}F8MynbX#O#X)<>Cj!QigZ&*{Y00u1w z{+x5pj^{papZGuj=Wld>>Amx?CE1kC!~^3`G^oJx@Q#GC=|N9C>yWtgDWEYJ*1O;E zvDmp|-i&@-udyST(BKT}M=aiqwIcmgDY=q!0rT)?1cFEa2k`V)9sM+e&ujkT_dXkU-13ksnkCXsW99v^Mn2}6 zlJJ|67l1lh1J^vvwad>Z0%RTNdvtWtjCORQS{aRFPkDa4`CaEfW2D9BUjC`;;;Ucy zK}Za=tO*AZgOxnKT+@uKMI03sV{HZ?*OKQZQ?0B{3qxku>B?Ff%V&WJro+fHp?L}; zOwI9>ZAFc0v^JH7)T*gIOrJZVtP@W>F>bu^rdV8bE0&_`s#QX5fN_eBz%g#ZgR4G+ z`IXOMSjvdV2}h^GhLla*C6VWh-=$!6JE=3D*?51J)Lg#Dh)mo7h3GkR16pUx{-tZ38O*z~a)(h{SET==>D0kX*D{ zX3Vvk95y!osFgz%$+!&e_b~`99H)hN8>yYs6XPl!^b=Ic1O-*7UALgU4p_~ACSH+0 zQ4iR>d7}<*-ml88s3&3=ORSe3n}5p-mDDw(J?T`PdwBfPQz8T~thZnCw<;NP-LRke zVpCzHxi1$NTsRPoqG-Agr(O!_3joi7#AuOTa9!P&$JpbHr4HFfADeZ=tHR=E(*kCC znvn(Stgrz#$GsS2r$jK7IO@db$6Md~>_+R0|M-o#=Cj`) zbcLiL%)Y#}y_qVu>}hMR3&$xDN=EHob7PE|*4M_yi`F@PeKvHN`ZJ{8mR+qDwAc0J2wL3FP{FC)z%>6ajTqB~nqR&n{rl+RiHphl_h7d4xS`8fU>regt zKFoH2Tev~;Kww%$hT^FYH@!uMto$61n+>j?X+b#X{dm)x-V|TC<|{7UB1fVM$!bhZ z&M9oQ6f9LDDk&lQKdlIGJRts&8jQM~RqpG+KW zt#;xam;9~ecgsO0bY?x^kx8CKJQ>qF@LuC%ZZZXP;q$Wng@}--g|Cbh#E%1aB1*2? zh#SLl#n4m$cr#87Nqg3BKog7CRpO$HE{tz}^IOuFVxuLM!_kPHleNjM=;}5fKzx)t z)>J1m+O_~&0g9`}fD%th?@g-9^|MJO2Lx`>>Bi|7922kkjVBMHmBD()r{m6_Y#A6V zu7PNm&!jq=bQ;o&{r2Bq!ho4F>rg3eHJt(QS(U1yPT*(;BW}tBd!I&g_8sY$qNZLi zjydJ|@z(cU@GOy5#5Mo?z4-EHzawMFT1h{O!#U!S^1vOy6`Gk)W(V+`P$IBwiz9E- zQp^M7Z@DkWC#SW~ll+TyBE^$T5xZIVRz+vUEp_J3`JnV-q_D}^Z@dSy5pd9Z?zLx} zbN0FM)oZVfEe}7WbwQJD@4fd@CP9RcoN=v&yi8B3BYdvr9(HUmL~JkG`U+S`{mQTn z%?DzQwa!k-bFYvxUv}AL>RIJK6$Ze!R+_PS(;li4Do~;t*~cLFqVh&Scd)CD^>j=Q z^oJzKx`s75B+uMm7y%1Ww9^xGBLcv?(lE*9!NUUPSp%ApM9#YLMFvi}E%kGDy0h)vOoaa18Om{+NJ|BaL!Qy%FmCuJ%9-Iz- z3=?WRVTBOf_=2jZidHd--Po{^X3|By&!v}M8dqLjfQFKI??cy1$Ip!Y_M_P~=PFwc1{G4GFA4Pt%2wpL z`)<3(uATFCEKn#qtCE?*Fr}6|7${)BI?vU4V+TK6Wz8%k*EOR=?N!aPba5c>jdg#~ z@h_5c?oWU66Ric+bjZ-_W_K$yI$;NxM=83)e}4G~`u%2MpAtP3jigLJL?;*;hh;96 zD-e93Ht^8UR4$T@NT>;5%%)FZp4XES&hK*WZT=`FQdr!bShrzAEY2@Pa}^RG*oenf zXSgZrU1rg_npan@?t|viIUDtt-*8b(%}mGNf9#W*c9t6CkBCFvP&*fNX97mq^oH+YGJ$d9j7}h&4{lD^*V`D^9G$6O5)m_uI zmiHZ>5>~)o;=xNps#Lmo|7^#}-Y(WeOzmnlrY9z2Vb^?2O+!FjirJYNNf^^li3cb3 zBCVsJkqNr<^E)JPN0b6F_3Epy9&o;fX&v034onAab}Nn!S9LRmdMd6dEOQj1qKh!R zPYxi}wBlAv`-8nfUm)NcV!5ZBeOSEqDF#0c)?Gh&Xh1c|J(Y%=YvQwbZi%6=E=t8p z>Xa}=YH_8(N~|vNI@DQN+lL<7;>x*t>Rd(VwTB+JU%ctvzaY~3?DyiD&;5rE@~Nku z8ejX`*W|EOkt@@}DT{&P=Zlw{qTJ6Q+(he1{2nxs?pdnCs zXC^0=Z%Hc$hur9bHCS(sRogJM+Gv9yu$waU{lNxG^lPzN;d?UC8;ID zmN`Rf^ACn;CFxl6`q>*YQUcvAOvoNixp2!vyJ(ggZ~T4prprvP@?yah8$T%LphsJB(^7Ja*0G@`OK*?+MlEqLzofoH~jmMfqCCMZoA!WLy`i9>*c;#KI5p9 zUl70YEFyra9)9ZE@o%60w%#XcF?>cg9)n|AU4(WTHzVgk=XLokGLzBa=l>(zS2G$D zHT9uB`q(2rGadKY@*p~HAl}_RtwM`Z8Tx3mLfuW2r)rE5<`|S z2w4gStJAVBJ%dxYRz>iIvgE<0W*9gsFn5fz-j|I*R>!7F`CCd!=WY|h4etM_Gc(}#{)v%=Udga3L zbO|uiAEmS^kOzjqW>&qciuLGq)KwIH87#*ASo~fhdwqlJ4&Tj`#hY*2x?Ni5Z~3i@ z2Lu? z({N+3F8GZngT@%F_r3n(Dhs3C1b>Ba+69A(*!9phNSA5HfKx@7u4-w;jjYKg&qJP( zq0*`4WqJQG^oE3KDDBs#b6VGkb#ohI%a(^lL|vF)6z7X)K0N72MT{bQi!meuA}zJ7 z@vKsG3|6M3T)Qip4)N2_kxn5)|uVa=6xvxEv!g*$El|s8K3*ym*Ph^-yDs`w28qzj38+zQL%Nm*c&#S<~D*|dGgUH%|8XW zXIL(sE6r$iT(zds%bM6xrYHbhC<{aOB{JzOE2BW{LOPT*wxU#^yikNBGu=%mhtuLd z6NcdXOhDQ&w^8R$pReMk3t^QvpZ3Rt=4azm0BQ#7#Px7{|&OYYddOVfn%niM7Xx^e0&kBV2l>FJv8AAa}e zcDIpaX+LQj zL`NvA3N&Whiw`KEOHA@CH2GWoIQ)d?#;^SDE1wmJzv`3UifccA!+?sB&OD0*Y3NnC zWa@d#MjS_AN`|2q97|)&xE8+)Rpk8h&yVY_yG|V@X@v8A<;+vTwve#xU?W;Y_@y|e z>U0?_L4T?4zc?3h$Qq3Sxs&t6b#Q$OR4S=Q52k5a3i$6xP`z~Jn_^IM?W#3e?#3j- z2t1)GEMwp_qcJP8ZtKGFPBzv>y$rmVE*kPOSZ_W3gZHOGrOIR|OLthOQf))w-clH`BHmjBYjXc2p}9ipqOkr#{}ts1oqGL=ntx993n@(W&**)$7;uP!WUMtR;#h z5nYAi?ih5IU|NhKAc6e`><>MD|M=B+K0Bs$)hE9lU;6?L)9wJ^EBRNsZ(I>x8)m}Z z<9k^c!Ev`%7iGx6hf_q4DlHgT?$30n3J}8G28kOJF^QJKgqeZ!b%dmhCD!!c6K~_1 zlY!5BlVbD5=RW^=2dwFBsY75S*y}uNp1JmU%_*^ZB4bfvitbMYdNP)UaU-+waAK2> z*R5X{Sj)-dl@~YtSk4RQh&b{J%O5*F;O(s>&0mo9`iJVg$~yH zUiXpcXprifIwXI$QLz_FK|DU&fWHS+$Z(FlOt_-{V!h_-}%msQjdWBEbNj#)XsAB zD(=&Rrhz{bmeaR_EUB+Kj3vsZji|XAFMIXz@yh>vildsLuaCa(i*e_VA5w&+9=frG z7@KISeW~57MSXbKJ~-Ga|_7E2E1{I|k{R8ZH@;ZBM1mqC?4o!^Evyb4slQu4bj-%xEbw zi2QdRY$6I9BO4#Cg}?J)hYf)bdo?N}-RM_)3V$VY+yn1w$4g&*Y+U%3Cy)7pIQrh# zd_*Do@O!g4xSsT88IfibC*#^TYa4^LbLVz#3{o%(2y9xl$&h-Dnw$01s5VpYyw5a3 zip&4r_mOtj$7shQrzat)f{E?D28)R*DmLsZ# zyVay1;(-@5z0=dEYA77IbY{YI+VSG^kBL|O>eCI@$KH2k-1AdweQ6eDbtx(%tD?VD z`sgR2-$gXV^RUPvvM%UN)yHV+r+F4i2dOH$_59oss3PkYc+Hq`Qm7@}Rv@TR_?R+S z{hrfFV^9T1R-uN1O~eaNenDLJEF;8tZWyhvU-831TFKtbb0_B`r3FWDiB*zWKG5niJS?}C;dWB9?2VxkgK|lg^^)pWIXFn3JgyR=b(maVNpF5urR#FhWUJ_!uolE zYq;(DiB|Fm8eao5!N6=vJtd8LJ3tBzKxCqP1B93$vE_|Lt^``?Q!>= zcj@NGTWhkb=|$3e=HbsUHy;QelPL=aAQvdLrx;4f@1T%CC5dzp&qoha8XVF^3`M_R z6R5tj;vTv}jh9|@TwL_lry8twy!+z6Rzx8lNof%Q!1i^Z8|G-%taN)uB017>Go20m zvXSAQvGFii;z5bvZoZfm7)qrNqgxahJPF%!zDf^8q%^LSQ5l<$zq2_2Jt^fw5@gMB z292?Xq7q?>1MTloHNyh33kRa)&F}S^if)k7(a;6Pjn_#HD2wA1I0y!Zc}{BH9f>&M z?8D=tr}Sd7FaGYmSHzt^dT21oyEe`!H<{bkGZ;HX3p3tv`xx zi=qUQ!Wi6|bk{u?Gc=?RvU>i0}uR?H|RR-c>XV6qRBSEq<+#DiB^ucgH$ zWkM71659yxQ4#bWR>szkXmuA=1yovGS^)Ws#;8?R_`ThB-#ylESno;Jt%#Nc7JVM3 zrzXsyd)rGt*iU1XDdC6&v$}L2X><`1r{wgLGmSX=oO9yX;|`8yXE{Fpseg@Ie*B}@ zP~ClC@Y0(nHk47$FkY3_$LzJ@@bS9*Hwy!ykq7aa&wNIk8-)R?fgD`7vbc4DO*cEU zKK9*rzj*M$hgI1c8!_!50sh2r{CY=T2|N!2?HTRyy2Oamz~=Ap@Tb-}J;gwZ)sNxo zkHpEZI4UmwttVqxsuI5LZ>35(yavMqG59{lDk~x8Laj&w#Mka}@mdkX?d@E+WEt~+lhV=+Wy$aTm~3TvD-guDt!zH{5oXiS)V$stK+lM!%wV_`?t zezb*QO}ceRP0)z_CGpsYLH8}KB?>XsIOdFl9w@UqDals%z)0RkQ(H_e~=@L6>ITM=fID$$!^8!l+l>5@X zD1Eb%svG3bV(55IO4P6QHBv~bN(<` z7_N*=GA&GuDItLTc^0uOg=C}Zv*_%@%EBeelcRLREBvceza351294Z)j-D?_VJ_dZ zx#A*$)I1g`(QR5?_X{{(XOiqnQuiB4|yX_vo{@WMF-1>=VHzPjtxi7~z zzV^-7II?Mwl4P`|en4(^*RI&Gaical1_>*fhDjGzxE$*0FAVN1ePT33)nDz#NZk#b z&iP5zAhUr<)MEYmO|jp;`^6JaY>x*YcrfN>Cf%Bo4Z4N3oDl_;u;i1mIq(jNo5^S= z*{v{+lR#;;g+`2ww_>Ha9IaKiCrnSR6QKN(3y+Hn-}-b~!yoq=wSGkb{2YNkNuL5V#iry>7BqKeXs zdT%|-4J-=<7^S}8p$+dXuFL7Fd1&kxoCL4ONiv09dR`r4wQiyEeQGcNmzV#UBCJ7L z2&uAT42LN%$jc%OhIwpEOvbBUcU~NH=sxktBa88W|KBGxWoNQIQ7q@$tTsN(ggkp? zEGi3%VM?|<^8lsHz%IiwtBmQce-5Tn9*!F|X1NZCpH3nA0#qGJ>fu3Z>+}Q+F3j(; z#!rbt5~NS>A>*iybOXt%)DOHTK$D{U^OQc!JY(7K(^u<8GUDEssC; za3Ei#)Gd1~^)m34u5bk^n(ApQikBP&j6S$SN(fPnWPR?x@BR{*Vzf$%9ZosCalP3R zLsUdfn^U2DIPHz-^r_Y&Sm&Y*sk^+h(#iVTz+Hj+JolA2>6I^xSH1b^2J2(*{zBYS z3>Ftb<2m~=9Shgs%}=kV`D;G3WwMhBBcwa71TxZ!J!g>HTr6@@D$Xj+qbZI=7K`5horP7ysr#8rDutAg;mk~F5VU2E>>J0*eaaA~mv`fz8W@eH3$8vqP5dvQ(tP^u<^LsSmd ze&M+Zx+ud>a1Nq;C?oKnf@f4n@p`GqBV$FgHG^HOB0{gIj30Vw1$37qpbM|)dy!Hi zHmKr<6e1`%xytBhx)PP&KFzd-3?=zHTok26^enYkV+AefdMzfW8!@+TgMdp?!n^Lg zLzIHCuFIFX(3xHU7_)$QXrhY*+&NgPR1{;8)|NkqqYhqw=%I(j?YG@sDjUee5_!1e z9FLPSs{zlZUx~vGJ2W1D>@kJ0d9e61Fc=07i+PIc4Qo=vF^dpjHPA#ciZ$5|5z$;UprsBH1v=om& zw$-{|*-oJimn?J}tOUe6XomOE&SsK~y`78+j)!T;NLef8^7HW~<7dfqPaI%3l6qQ*$^XEm*bQRj*Sc6 z`sC(u-uL70`(oT%nC?BMwQxXD6fMOOmAj>uX&x2^WsR~yYC!}sE(2itNET&$-M%KP zWC-U&`BO&2K)apDnlN5g8~q%O9cn0Q)oo(A&yRV@A@PQ1VZ`%Dzw+tt$CaPFPKJv8 zYz~%(jbPMsT)e8os*oEt5UyJGB!8M_VT?1YYB|O&+5XUd_?*p~cZ;ptp3qck)?dY? zRiu=yrl9*R_!U2sd{XHM8SVz@NZpj^6@x{Dk3&Vbiam$hax_eo$|G5BT3cReeXmUM zfmy$}qIS8))RZQiqkJs1SE-{1tx5;1TPpu=JN5k{j(U^wCok||ND~E_4zsGwiFC(f zk3AZ9-gTG6+{P!+C-1~)N&gI5uc;++Lu^#U?>x)TYJT&Y~C{acg)WKio>jbkTU zU_^QvSR0Ex=nOw$Fi1^A)&$tKrUg$@ic_3rg7A)-ZOpThn9lG8$Lt$#e9trUv@lwq z|H!rR{cC*b^D@9&v5-1JdZI zrmRyf){x|0C7QHmMK#u~-w=}x#0#Dr{%n$V)3OcHC+C+aFJ;5Fg zl_l>Q1Uz{#FbeA?agn3rPDe7r(!nksfZv6c`}MCg#dcnYHq$;{#{% z5nHxA5d9G_M4urhxPIId?D_+Ws7xa6Fe^Edf_Azs&iz?bQT?Esbh>P zU0GULj@2&sBjnMFWRzTaHF1?F-GK#$lgj9rLE+L-;K*@Ja+ue#_vA1~m=VAZ7gCMvyc z4VE1?qJA$5SD7t9{KeT~Y{n`ce2X?&Vq*h`Rp=+fpHKUquCF+Eduh@=8oOQ590waK z&``BD;XngERlk7#?ZQp$^fZ4E{K~1HQ&k`u?b{M*$)Kgd^5%1-Wxb$rsYs#&&1OLO zo-cat2q7hchqxZztx@vR9AZ(hfs9xf!A*qq#V>wQsgLvBwdu3zIWk{Q{*bigr8GFD z+*TD2`VZy7t&Npj7HK^#FWV?E;iKDCnKkcAAZEEgLQ??R{2b->C7lr4y+mPH`D|n0 zwai|`gEKwH5vi#X_C66Ozv_s1^=~~@UZxd)@VdW=qmDRQ!g5)u<1^XZtV{M&v7WRa ziKbbaMtOK!_%Xh|7dP;WlljL#dqx{zJ92HcI7dd4Rt_#bBl!a~7AD z7qrhH1=7IFu+L?rr=u6ATyR{R{R{$rGFU%Gt-+OH4wW$(cMqgoGlqwDAR7-0H}Z@q zko3@p!kCr0n;0&PswX#7AR@h`8qL0FM8Bkjd1$TJY4*D(1dRyl;O*cjf}dji;U?zq z;Gk8cs8-{MlMjs7|AL0;FaF)v;+lV^VY*(1!9r~HfM2l4$)f)03b|9jtl|l@FY?d4 zR+34LQLPbVOc|15IH>eO8M^15yG6)m{b-@lWSTN1i69{(EB?%?Wg0R$TVbh=?vybx z-MLteDxClTAOJ~3K~z?cJvMKOM<3au^>0k#Fx=eSs~DRCgC5mdSEC1QMkC`ov0-k* zK+ecPk#SO;tu7*!1=e``Oa7#AcyUurp3aFgU6Pkkn&`?5i@|30jTO&o$-^SZ zR~0zOLuG;Rrf37rt(!IYEao!}orLOmmhxFiC@X_8kin=`EGl^P(Z|H?ci$dcpMYsS zt_mjJ#1V_DDg3?Ob(=~)R37Nyp<;L%44iD~!J_+v!l8$dt_kBAxW%XvZLO&!t~Qt9 zxJz}@<#fa2&?`=S$dt%O>r}Q)_^R*C&x6-DJ zLEBtHoB%0L7I64e5J06DN1pP$c*F1h0?NZjzZ&1U@+NZxHE}c~ZmpZ(L`bRdULxW{hc46lV$io8X*rV_;JR4wKW2X1p>$C-E0;{zSm{NL)4=K~g%7JeY89h3G7@|5vv1sg|9$TM?oh$( zMPq_-S)SmLtsrB4yo+}q8%q_y647APvLedoF`Sf21knY*LbT(m;-aF!B=oTybV ze!mTB#mHDqS$Rr0N6!Si7rz?N_!*e)7yVi6wqf_!x^1f_i6ATofpRbup=P0dUzg=i z!~+9>WJBm6IF#p}CP_yZt%AU2pbXQ|0cK;ajgrr3i?zf+4}|Gx7-vo*4YzI2psS6N z3U*?4W-gxh{O8AQcidxQW}mJ!SJe2=K(HI+b2BT&hbdm9om-?|6COUHbc3{g-s9eQ z4m7~>m?;v-$q@PWyK(e+N5r|$pojIz_goS8-m<0aG4eF@vy3NZ0R!hGEQz$VfYUml zW8>P#ZKhGHD(2ob8Fm@0(dbDjWNi|;Q-2!?!4EPN<>bbEF#WH8eRX{IOE=rV3@M6Wqy;{US?|iWF;>qd%#>e7 zO92P_#y5$B@L4d3El{8o6hui7CJfBO@3~t+6~-ddgA-s)G&8hT6IZG#ne!obK64-t zPTX(bc<{jo1(^u6U$u;R9cFMDc+&3lH9?6&-0ipBp^EqDh|lca`|K^n(e00IHQ87_ zYi0zfO8A~L|FVxE(LpXot5t9e zF+F46Qz|NCw|U}O8B*IgI*b(GrRK6PDxC0$z*U5~frS+nPkwh|#HrvgU4!o@mLn%U}*ig1cD9a|*a(Ug|$#oP?6?bTS=wJXMuTb`WOgU!n8 zF!-zWD`0R&okXmobpTq}%V0~MeoYGvpM}Sxs1|?t1MHQGYRMR+JfKoL&ekdlNtC69$a~)Ki7`(o z6{&DSL}Chx(ixr$1tSo+ilelnPdPX)`@Lt6w7&4+uf(^nyfNzKJq1Nls)|TXV0v8d!;`>FHoj!yUKZ5#vZ( zm$X!SS9rID1QO#Drmc>OZdAJN29Vk8^;MMy9=h%4x5eIj?W5@a@WT&@Q_ns-C&JkI-ge7^}_u%7Yi)O>$iD$auPnWMlDK=9azebtlC6 zZ+R+lv=x7P*+*l?)}3+O@y978y#Ij*Vij3nun1M&P$nuQ8-3myQ4bks3-rFo14(-&*M_N~Y z?W~Uv;9CdUYd-8Gdh0pwJ?0jOoASG30du^!#tLpfUm;8xF)sy$S zJXwjIOA8t^1$L99tt{JSB*39p=@ZOIeR*9~nZURV#yeuu7%0ircvxiS-|j7Q4ld6r zz_g6{5leT()*t5mIMb48^sVYO>T@hUl(n8jYmT{@);iywzRk6YnA(QTj2!W(u%AwA zH8$&_is1r zc|zD!CA5sD7oLa1VSJXVy4kF3tuHY$OVwYgGZ7wpUg^D`mnDng*MFVxC5wzlT1g*H$hZ_?4+cCG^hxe|Jl_<0c)ditU-@C;sy-$g{2pXo5(yotb z()5-G9~3^uHQ}g%3}m=TS>JW~pZD=f18_Ymu|k!h5DgQ2TRZqp8+g4#W4Aky4q%YD ziR|~w3*&cr?QmNTi<%sg47ZaCk(N6fk^$ZD9xNX8eY_&&@k~oug>g;xsKvIeTjlZj zyd-`OU%Rfim;qwEqDmYoswFlX8U1WdGOtM&T|foM;>`NjydO$ zc-2#S2{Bk7y!6A-Y4%h?CPm`Dsb1Om<1sC5!sjaDD=BKGI@Z~zEeWmni5RO}>qr{h zt#+)6(QR7E0LvLN>W&P}q0*LW_hM#hI+j*0J9;JlE|a?Aw(xC7buc zDDeKqWr@;^B?^Zv%C}s<7}`uJ?;Gz_8b?YbV}(I*>lDU^YAXBwuoIsbuY3F1&pP+; ziVuG!esJ}V2hT$1k%-?owWOr1+da4Chi(IHc=L9=0OxR#b*T&fE{=Q1I#`j zlR;bwG(@=u93{hO2_ROfp#RQ^|En*S>yR=b%V7$D)oMTKhP#@*t?lTMB+uei#+kn6;2WYBZdx6wE>Yu2o`wk(r5o$p`@=j}zIhwqumG6{>}$ z`Pga;F1#eeJ1HaNN;O7%6~=b%E9r`-oUV9JUJtc#)T*&_en*_~hSTHJrwsAn0R7W@ zu8g~W{D6sy1*=Manu;@J?*cJ!uQ}L(y#Q?l;xd&;q6$UVMS@1HC#$Qm$DVtu@%GkR zZ`Fa)0MK$^6aFA7vB5W$Lk-e_dd8#-fg(v|2BT3aYh}!oG_w;k>tH(YazDw&eOhPPJIIX)XFl6aDwFy|wj4$4`mGs4m|8kk&! z1BlXd2r(H>ru|S7c)n^lUPq{Vh%NzlRh5cyBJCLrvM(X1NP{Uh zSVZUA9Y;(2?l6ahdrVM}&BSXInnELh_f&V3;!SFU%mbIEeEAF*;sn-9OK$EZ`X()8 zGo1F?M`mkqp-ZC4dy8(*w_9?Y+s;7$;7XMF!tXU1eg;mn$Ao-$NG(4iBjYqthn<(3_%s zuvYU6wAd83dy$8sgs-u1g`Sc|i_OMo4RExA3xT`w4s1vaMP3i<&Tc&V6-UO)e(lMf z0z98jzUvDH-G|yycsAaDPCNLIXMlV#ElxlF@s9_bsHKHP6)-X_7Ri+;gu~u!NivB@ zjl(H2Ee-l(jVEDmkfXCuqrFffaGo26m_wzEfq}JEs@OKl=JeHB-RN{yVx=_DD@D>Y z8mDeYo$~y6{j*M(j=228*TnT#{J?1*lbzwe5fO?Y>Nx~Nb4apSw#XnojzUljr(IYz zK>Qjz-RYu(5+Y3%v4(^OlN-tVb3dFXMOuaM%zJL%zEx5;2OO|p-1hUIiDhq?rL;bA z9&1d`GAyEbq2uEUaQGfz{wXJ)8dqI)mDUgeKWV^oB08krh0u;`e#gmwEDWpDwBWnS zoXE^3%d~8a^fVdD@!E`PCZ{}2nUsf$p~Sex>4JAts6b7H$^Pp0{8^b6@blzJesZZxv50IcX5Dec*uyqgF!r9PjP} z;8548uUk|c-60!`DgeffO>6_iW;EQJ7ftTV%`k|nWP(ipAS@=W zsW--BesMvO5XOYxS=hNlo|C@=Qp0UZsgr(%-!<2YFE$tVN+UQ?0|qCH8cG=zDDe8e z((8PdL|N0b(_+CNbkKou&A(qAue$KOxaY3BZ9V# zlV{N33^5LTDWz++RCEyuvgUTHBhNW3&Uo{aX(2LL@BDn+eba+hMJSPlo50tY;EF6p zMl`kz>29>nc`eb>A_PvOIKo?%LY8Se?6--TY2h>&Y_P2P`Gpv*qiGJt=IR$3S{ycy zoqIB|(rJ(lL>7V!MB@!DI3yQunRhtqLr^V&sGM&m)(1xZN;|Pd+-Edbne0*25%Mzz)QN&SNMMcJ7~BEEDs~EgPzZpjupl7S zE8!|mZA8=u3=>8)`}GE6x2x5-@PZ5C_B-wn`@F`K=@MC3gpNGl;jB^s2yZ!A1#G6< zi_M$&jKdE*BEIpBZ zgAw1>7zs~fG?5Lg{RvWOnoGM#c8|jxWzBs;p{q^zaBaaN2!!jhL zaaa)(K(0XOie!Y4@mXyy#JUaZ;=Tv&iC125PCR(;eR12Z_b39a1{4R=Ii$K*q?5te zaCSKky^@oFDI@?blyZxlK+*i>p21njj!?0E>18jD6D~PsZI_n6efqsu#$7kwr++a} zlsnFe$Z4q9lN`8{Pd-@$-6RVO7#D`KHm6WLzHUbb6DK#jZcbdKo%6e*hJ*=DPjf8S z3K*V3ZQ(lut z4T^KWfUvhl9Ui)>oQ`AS|;FF#cm;Av6*&(gpnSWwFKJ@wzYi^v_>!qzi(M?G| zStGC<^=AQ|)~d0%vMl2SNe|iwd@1Ggl;}%H&w3FRGSiPpHrFyeHxZWkPI#@gDcZEb zNC2Wlh~?y|^0~A!ADj2s5Ic5lk2B7CN&N7KH_FqsR$(Or(n9^<09a{t?PeWOX)Y}X zu=Mr`Mo@X*Kr{yjJc)I{AOrdmiR@b36{lQyVx0cgr>BJf{6D@F_usN*P~nqtC>}oN zU+TTP6hNM%;TaHWGmTkZT2?xZ0kL|3vje;mO~>r5x!F10KlfN?W}}>#D~i5$gUYIu z7IDqAkv-fJGR;3R6B9P2`9we_uM0~$%I$L;?~ydMZevNLr*y< zE_wIUd0G$N{&4)&Z+>jht|NSx!#v`_EIi~SMpKo{!pZdHl!{U4j)^u(5kSt;RY!9n zd|&Kw$quzzikxr)a{5l|<`Q^kVFqER($Q*+ znU3WNENDAvuq8cfmaCw(gf~ob>WbcXjn5^WakU6|v^*PS>-HyN{krw8M9^A8v|&j^ zvwVvNk5V{IU-7I)a2@1$St|pL*0%?s?nW#A;LGojQe}Sq^qaTFzy7Z)6~Ux8c3NoF z2tJ=S5-OWj(m$dIdhfuQ#1b}mu6`;aEVU}F$?%Mz7gUUoq*LHdjj+xh7sY>6x$yKv zn05yCR!lXC3K0dH9%>5ZeFWhaW2LniZ+pwH#SeaTL)`YWpGUtPazG;^<1sqsp%3jQ zgd<=Z(nN3-o|jmYQ5XB(OmT_1k@Av`r0Q!)8NI%w^PQO8JQr{Nhc`aS;PZU{_=3L; zhIw$vV2&FoE_hgJBxk+TnP!U0{n9Q{QTtSxL0OpUtOFc0wctQUAgbc;1v;pXIDJuh zUEnlDpDak2b0qR;Aubc4ft{KVY-J51Qt3}sS<_|R<8FKH7LPu*wWQTRC2qO$XuY1G z8J_uJr@Y{4k=C`J`gZ*LC$1advvOi8klA2ixQJM|S@tRCfvAOG;Kw)KD4a=HHO4^a z$(2B$2Ks|>CZy2*ebbgyNdzy2R#67yVz6c!W2!u|#u-hK_BwU6u~a0cFX$dqqSBBl zO;61zmE`Bpl~~)^xw+W6Yp3pAEj=9zaPk&96@?2~`NNm}RbOChiGWP5g#c%*DMa(w zAj$T1WLEkKb-W^0+KVC$CP2pl76ZA?|94GvSKBhI zGJ16bkE?X$%Rqt~mIGaeYgU-uRvO7&AgL&1huo_TSG<ktLz#k#K(&;imMH6LEYnY+}R*88ql97?vzIAKNPR_*A zuKAd5G^Eh!Y8ilbH%7)rmCCY#w{F`OyKUMO+qQ0tb#rsVudL|MLs?53mne*{HhSaT zw$}S)Z+uCd^7<3BHYibk`uF}N9{R~cPRELR22PYdQja{(ishQx$S@{~u}*CzVJ+jx z=}<|=07J!?P~?&DhFf$%>go{_^=L*Xmb)vlZf+{(x9^PU`k2~GOc<4%gV3?nrCl+* zX}#uSo^#$S;~Uq1CwA`G>0U+nh74>VIXU_g-SHpy#}BW$r4(w+lXDwe4)aL3#59psdqL3@8ul0#WL$70wK2G4BkJ2@ z?I3JQHys_Yf)jH;;;z-}W$q+$;7f8s>;nz45Y<49KW-s18&sbaZBJNNYaY+prIuJ( zA@iA5VTb~VjBCFtXX+`>j@bEgwY3m6QD=zCt*Nhw>km%*OA3eU0(|^ig9WNfA_~&D z%SKsUv3S2K5hDAU+W}8WQP46?x84Ng>#PIAI(eNZc6bSGcq`EgkF?@}lCB_(FS6Y9 zbj&X-#2Ag%(N}&C-ZL&ToA2F~zQX)0hE_mwxfgH}9yDG|9y=>)jFRG^sdWL^t*UZ6 zo124+Dx)zWXabHN0jdScbMN#{7X52@Km7{n&9V+38xjEYp)FfnG^ldGcyFS$v4|d; zNfw$j@fQI=S9b{qe8{sWv>olyk=SFOz2b?7AC2}(Gb){~S~Ex^%vtNIU5PlR)rHe! z3WN+Go4l+z>nm!4O7B)N;AbtGs!JJLk3U8%4re_A;*A5-axn6bAMRC=azZ?%eMA|nh{jGkR$?B2`jP6J; zT6Jp8#xQkuoOFt-Rju`7%a;4&h$9b+Q%*iHet6?eao=r^51hMFV)X`keb>GWGq|^~ zErvg53Ct3MiL!_OT?;vBU=<=Kj7zUXQeMNtco*%aWwC>PFDB_Btc|GBxvVB}2L>2o z9H)rLPMrpg?!uqSWE}cur>AGixH=3IuFGS=aH?cq#?MBg*_~H&zdl3rE-<3~sJ!ch z4@kfdSy)z0?BHHE#!BNMl?@`4tOyFU#YdtlUzWM8?k-z;1CI`G$Ho{!VEn}5aztBr zc{fnYJ{SF_p;aGH9rzw>=%wZkZ7M|=3S&JUNHYTQTu9xFAa?JTqSlhfQTZ@|lQ~SL zm>_~BdOQ*|p(G#C(nzYYc+oM(#FmG)#C`YNtLsv^zksW3kb@36I2M9aF*Na>lfFQWTwI0V#)WcePSM+C}dQUfZRPZLet04)M( zX;H|DEAsTDmyO9-US5caNmLNJF*`S{Xb>Z32^CoBmRqTyyot^g=~{{IRNEVHjSAc; zt@Cc|T3v~K4%jbtZr>40yB4%@yQ{4-HJB2AR}I9NZM3Jr;?}Qsm6oHpWBd00U{c-y z03ZNKL_t(&jE+g5EwNFI755|KQbFR)4h*cRgxCcK=&7fU=a$74j57v{bSPRw=-c&fC}#4MLj6Xjz(DvgE3k;}|jWo#;YoQLTpv$chS7Vl+xjZEL1p zV8n2goRY@aWK2#$WpK@py%;^uCQM9R+%!_q9_wIYtSaT1^1tsSB$6>$=2vmZ1TmK3 zg%~WtN_aq&du5Qy)Lsq5l#w!LX)VIC;mRH|79pb!y?&@yZrI3KXWKlTd@yRP?k~tA zW9{kmPB778a$K|DpXnS-YGJIpS?)A+!uMIxp&He6WS8bu9kYg}L}4QjoQW@cmC_U&aZ8dWiuZ}ISW;p9~%S#y~8BqLblKUBdI!PD4+n|0UHK)0yaUI}I#|eYN)01+F4I`u zmxvgxE_ert0ggHCB>HSjj>qOb_K3%}ZHwg{+l2wyPa^or-Y;6ERkueyl*z=pz7GQ?zBiAd!-JcTf&Eq(CfE9brF80>`{N4I{0%O~(cp zHCPuws@0yVnCz|5B1V~+OgeP$ZR2=)f1S#R@iNEK=#is4kNB#JFx>8;$}uJQVNmr< zJg_+9G~PHK=R=Oj zle&uL>em|HrIf13rew7tiEJDS^6#t)W>rB3Vra4qV0yFX_5koKt_81^eS!n|5|v#Y zZB;ZBBZ+SIxcc{##(|5qqV#aNv+A}S7`a6dj%eb50}hDCpLim6EzB$RQ@M3wJjU6q zE3_7QDo}Ew)R>)uZG|AI{xoc*TAbjA z1L~_7)FiXQ%Emj*01VIXk~FOLt$k}sXdtidRO2(NYrG?mRZTL@#60~*pl6e7y&B4 zm|0IzkyRRU8?0_j&LCI5s_q8XLeE`=Crck^m5!jq5ugdCG>2on($ML%OS_zLI9Ksm z`&N;i*)%pAvokZsJE4v!1E`A&Cu5E32+Fqy>>s!O>{bEpgJ@Ds+7V)q$eE zpa`sJkdHUUJ$k8Pjd4e321YejT*EWr*AT&&S?l!5ay5Cw9$)TgX_bMjb%U|{?7MF~ z@%Yx5UtSV8iWj2tCOMr}JKk{FWpTxoSH=!z_~9XWDCF_Y)p)`4U!c#s_ul(-Utnp2 z_60?Hc(T6w+RnV-jJW*s{}zi2OEJo(AUr2+(}*PB2OFA{&x-3%HJX1NHSW{l*^MPL) z!jB~rjvRH(EjN-7#|cQV(-rsKrZ&pp@F8oB)E>)bF#t=TXV^TVHaWu-QuHakw3R4W`h=7<;@^+1)19#*zS2ezXp7Sq)#FWA_aLDYJ+6DOW{k|G~9kQR|TFb!2ga??rj z)QXVlEfO5}082kSu0ePLQ3err%_Ywyuqn(fNH?Ly@v*-_P zxld9w7hZHxT>a%Q$79=e7>Gp~5xwzo=i$7bYk^QWK1E*5w4CwSeQqX>JMOr6;DP(% z+OJ-xemj&;OQ>orq6n@{P0z%uF1|3n{N*pj5-Oq4J4#V;j3&=Q`@jHUR7W{PUB;!a z>cCs#hp5vSox0!4y!RVM@tg zB3Sm(Ll52WRHfrV9jf$|8F)&yp>!x>=!#O%)q%4VimJke?tydIZMWUE2Mzu@1!n+7 zzwY^!@W@C*D3vorggrVVa_1AmEZHaR_DZbGZ;!c+>tpL8)vA$LnO}&_>o-MvVI`_P z_;dZ};0UDF3MAk|EfrN&U}2cMQzM71x}VS;4_;%;1f$tZTy|-n=j$Fpup|#tb^DT2 z<06eyrq-zHT0{zz{_8p-ChHVvAcPiK8CknBq(txtSXoChIx>_+pI=6TRw-2%f$~;j zLd0d%WBcMr?Y3!SEUwPW8DO;L7nZF2yW&A31TP2@2rlaNV{&{V<|gI_5u8V@nU&A^ zbg`)nu_^pVa~XMlHdspcG4_9S(g*t(8ZHph7-PRG?(U#$nW=bn4&&kWB~VU>-?drDkzY9bzf_+fc|6&0vz zCEJ=b5O2w#$vP=YEqZOz47WK9dclfVp_C!u#kE@w%}Fn*cqui3cSc%;keWah?bLbx3hfn21^i_ClIuUmSGs^VP`v!2S2f?DV*b4hzl2sK8}f z=|sQPiP`bV=(VAq^rJF55!I;)8K0HbveSIc^=w*juE+wZrAabO{b(abM^#kZJq#{G zh(j2p^w`T7d5j7DbUEH#b+w^J?_sD#IHbb0(%_6{Tdnk37Fq~*l0jtB{%uY|o z)QG2=YhLEiG@zXEnD?!Fq_SHk@<^ej&|#E%{=tjh-=ABzPMZ^40)s5vVSLnj;gp-1 zEmq3EV#1Fz_pciZG>O(mV{&Y5P^PT4rgW?+5++7wU@)R7Mz%+3;U&dj*OX|qgi82P z8Xs+Fro>kv!ZA&vh}S7fFo%Urqxa{bkdJG@lWl8br|`4gjQS`K)6J;hAe~l2#&;4$ z&t^0B-E;2&C4tG-qm6MFAUd5m=0(TH!q)Ba!2J&>TEIgmPQwGk=u1$o95UzLM2E%+ zq{vWkuHokIoSe+}AUu#hY3d7FOp@ts38ckvnhs%oDdCQtipZ`rv(u71vB)0SqN8F) zS8-VB?VOsJP?>*eby=R7>o=@zju_WV1jo;a6s1kxT8%2>!a0EGe?zq?1&2)a@v1Yk zvl;|j??jAMsRq@B$Em3sCnaNP(jwe8K7+qw4Z$@yaB$0P{LAyqYb%jIDLWG0On*)+ zC#e~%$KHGI6}R5{GgY;P*-%~|tH#cyZ81Iy_lo_-Y{JltfbHA1iMPi3GS9NrS&dyg zcWM8u#Iooo^UJ$p{ls*P(>Ez$N{o3wDih<;pRnlUYI{XJmT;duDBTqS4ZOg}GWCY_ zqySaxM6B5$cb4 zuAzW8a@XR8!HInFu5zjDu9S(-vwUs4g-D=>ye7DZ71V<>#1ehHS>d>FKYY)7mx>`_ zHsxc!hrt<(Zi^`*tI<%AAYv9Fv!Zd$HP^;Hof+nL8xPTJ3G2#c@N5ZUZSd>>;#n0H zsEz^#ktha8yNuneWyl#>`Q9 zBFfAtP*K z16Q0TMj3+z#b>M%?e5MJfup?z`0jmg1C{e0*W{4Ls%0XPlk)QAEG3H+3b0D2O0Tl2 zlu!(Nkq3-rSzEpm6)MO$o>EuyGzDvVPP>g zZQ4zr1DP28H)aO#`?YF48j}+-)0l|n3f0Uqz(%!9r@uNMno4DfAq~f1#)NVL>)t{2 zapYBr%KK0LKp!1+YjRV08CAHR8tqJ2&Nt7ELr>YsC%-uM-g}?8_S&z-()LHB1ezso z@>nL>WO_6pv}MRTibpfGK^v@KR_QI6Cn$g&mBXOVsZ(amnRqHfToLFus651$hmTh? z|0w!^o5ni0294_;CE5ZnLkO)Cn>X(fyLMI869^NTP`1s;C+Qr)EgDlJvTzl`{a!cf zeY}YA6Al&Ese3Ln{t$EH`UlPAt+w8m26OIrVHqjnNrmQ${D>?`3n(_!Mq-LaR7%6@ znR7$8w8qCrRfcvd;m;mxOvG5F7RwsyYNb>>q{)Y_*077!tlV?o9)!VEGZ1D}T0^p` zn~IZ4|1oH-b`bc2kXT1;@YVKwU`G;uR8ZorU2QvS;~eqgtcL-OOq`}*qDpPyBVs&N zA?|fzyy_<8fgvAC5%c?Eq|1BmBwKkDhf(clvifsC|ScexRgpY8r`+*Mz7EG zP`$gx5XBJ#x~NrRA?D+`&v|Yfa_C|4AK$({c5T}!GB%wQu=$~SB7aQV7n2J*bXgE3 zlqzcS-ejN=BuwD}DY8T?D^hhvrearN)>l4o;`{llC*5jVD;%KlSXo$fd6UXaV^qvo zsvPs3SvO8eP4?>HrWt`f^sg=NM*6Dw2TO2>$V*+vvE3h&btyp41D6%99`7IIC6c>mG?TAQ@xl@g5zauYB3e3~O z`00AUp%iwT2d5N{))o#kcn}+nD5!#`YQ;>$(GT}Utl2>FM+To}Uu(L1lZWJKAv|)> z;9+@cHf=#v0hz+>(s%x+KDMDw1v8S}K#tMCxDfRvkyE!_s<#8GMk&Y;>9LMXPl2jp z1sdPC6^ZSXh`i~=6BcCtzEvkQx!7$;)e40M3}QRhub-3V{K6uJs0bByyRo45Eu?bb zbgdPYxP}I)s5z7rm%c)z$c5>#aoS{bP+(|G#nj|9`AICUF2+d1LwHBIPtK{#64{r_x5yzUg7kIiVoEWe_I_?iBeh#W>fT+!u%lo7O3p=-qKnYKYjS>*LMl12#zyCXN z=fii$XFl@Tm|FIPS2iJGeWz*lQi;Y_M#ODW#e#~#WaQuYhy*!`~BFsequmoWHYi6c*ayB#Wz>a zF>)^MiyW=asbxZV7`OqEn5tGSy+)n%oNGxEASj4&wlXM-toyFl#oOBY$YZgvV`nt# z<1(JQ7g)QJCowtK03#J7##W*5S(P!){xGI(*NYT1VYnw88_VEXxdg6VE<-pDM8Y<# z_LO$?AjpovEFOvZojYP`v>r24lQF;3Ei%2f$$K zGx)S?iS1qsY@cb=L46SOY6i>E^|cLsP)s3m$1pE9m&JSSr8p*gianM@ zSfUH~ct9c?8}><&Y|*upiGw^Nf&v|OX{EUvv5E)Z0Cnhdz+FiddB!;KijAiBnNmm(6REuag#X3L4@hOI zJh2F*Af#g}OX`E8lE5KY?Y3fMq8{&j|GVP<=jzR4F1@e&zH_(duJ1PckQ@$4Q46(@ zmPDD7ESa($r*Yjlg_E?f(G+b0#6?mdKv5KlouH0e!~UagQ8Z|Q7H+dRiDST)ms;{7 z$%lT>fwf5A)=og?MfVB=bz)5Sp zNHW%;(~KB2M{<%>U?-JgzEH;M)5Bxw2N}PMut&SnG@IX5g40w?6XE^)nehE+WBGjo z=~sA&0p4fOz=<6&*P=e5aHMPyQq4$|WPRh~N+ z0GF26ij$8G{K7rzv^=q7&1skec34yx(wDkgmNDQOsf)>$I}zO=W8F<;FzMHNi}?8Xh7p1C?m!8~jtM|`Q8&q;Zix*MOmm%z@?7OcWu)+9~a z|66s|Nf54GGc58pyudq!xUA(bR}ps~N_o$a4HKU;UaX23KHiPL`s05+dh34t+W+&b z@%js|$6kLgHgl*CLk;Gn^%ae2Em}#3>2ZqInW(C`LANL|H}Al*Jq?n``b3H*27tAR z8d8;(uq+}LEMsHZfwrTc1HKTY8)?2{!Z+(h{KVh*iTKVdFURLU|7Wqc>G=VyHZtcb z=_NMj!gj{2b|-r<;!T%@^;+it;2+>VJTrd(@$tkgYHkxArUDn7VThgWhoAmXT)loX zzVhX-s1*~CjGTkTq7{(3E4>2~_*AW_lF;_BZ+#wOt#w}&iVgvmj#6>4O99LUw}E80 z(c!j=W$d>1RGD!*h)t8cBsMuLaAK_L#Zr|f6oYx~{6K9C9fZcf*nkp-%YM1YlaKY__4qGqw(FVe;!B2 z`!SjA$F&mvQKI;Hs@2h?!56^}jk*0^%zJF-jRy@*RC!93uq zTac2xr1+@36AN`ht#`_Jcn0O*GQTX-ttF6e#Si?z55&Lyx1ZB{6QMP;5p^)S6Y?{Z z14qx5)ymQIat>hIYctZCS>fQzq2QdJaB3B}bkP6y6<$JD*qHWj^9E)O8Q##pYgF9B zD9Sy4Os7l*sU$q1ro%wri6<4+72g*M1+c_oJ=Mvi3r(%H>I=gPSxEw~Ak|V#7`!%W z#$5xzVzPA@Z-3++QLQ@ho#(GaV-azB>x?4yFz-a6f^9deL7hPpszfuI=}pF; z$AKkHF0~cYNxc(yK=Q#OwVSF{+EVk;8bAN!&(*M0HJy{^brnaLe=(8TL>wI4iD6|O zkdkBuy*Qj6#b|dc_9sVLWIIbc5AXokSX4?P?15#K(L3h+D7~?4O`Q;5WZ3IFt8U9d zsJHLl(SpXdxh!Zl5`Ule;eiJ($6x!qKN;Wp%JcENB-X%CZ?$52JXR@huP9rKu#rqq zv;PwRT`#V1!QhruomkDA(N)ZOz7SdPE~C@$==N)1*;2JNotmvC(VchIj&YN7knzlT z+Dyg3&cH2TWOF-eQB?OpMcp|3Oj-2UO@MY%G6regm|~T>RFTI303ZNKL_t)DboP_k znlRO^GGJdCzv{Gy{ysDGkDHjSC(-IPqkF0oM~nTqeRw_o%b)tsf>zpnYwyqY0EQYuA&V-9e{$qa4TONKWzH{X}X0wk*>1*zUUxCCDfG6<} zuAF2#u9Au&9tT#Ls_HYpi9n3D5Db*V?86`aaD3qlUl6v$gwJv8#x?AJ)s7=ah<@msY-$Mji)uzp48`yqWE(vUYBemITI0QmT$Zgxah!38A;v_rqdvOkRRsfnq zTuSKQES%TDtdN8{nl?M}+HaW%$>B{GAuFEDLF}sPSYu$ULr)QBazXHTpNkYM)i{;L z#i}MT!YrYxsyI^&tZAr`Y*)&u!DY_obJ+9&h@8A*NUdO0w0FEu4sE1f?ey1Td6{tS zpGC_lEDLIri0I(S-8gC=#Noj>y3vWVJ7=S_ZpV7Qj-A1tiWYchNPf_uYG^W_snrwp z0%HW>$UIB_99t}@%FIp#_Yn`&>{y~|pS$6gV7deTX3iVn-~?57yP?Wz4IV{nxLU-Z z&j{Ql0T3{FC9rxQ;<{dBzaoh$Lx zXP#ACsEUX$2EMVR=cgB*$GP(t;y?b0zplgRU;Oev)uct-5k}rhzUi0?BcnWH6$Lj^ zpH3T>B3DhB*mRS*0Djs*799^tO!KN8v&|}6J%S!bI`puY*&J=6)3nHwUwl`7bu)}Y z<9YO7@SUiVM57?i%HwwpIh0=Pnto)dpUH(Bgzybx0&}U9)6DMTD8Ew zNm(=c`@(o!C94u`CHA&j3D4v?a*2w&*6mvQ0gHd(C^HWW0RI`NSdwEUN5TWCAn<2z zYJ=%gs2SfEhf)qT*w}F5aHB{pGI;(TfQaO|u{$i6W*JV1g-+B~hHdaH_TVgy57o96 z4x0+S6YVRQQHd10*KQ-)Dl`CH!1}YYqsYUnc;#yVbwG;0%GgRqeFED0`B` zeRy;rY;4f!MzwUXtS7_1JY0ZKWzy*wHqs$RTwKeJ4hUu2&2Ch6M+yg=heenLGsfD9 z_L44uu0ZeEZW5*PB@Rw0KpAqO>YL$(h8S+q%>uCJR^yjG{EIcCaYeo}(Hf6qEos0? z&YB!rh&;p>g0Pu+_lJ{XoxY08TGje@AsjM2<3n*XtHf^krY@kdCLvl^Il%)ou6l9d z!uhy`(D?S z!W(jWCb4z)`N}_IvD0$GA4-XYFRWSz){<(AL#=7gC$*)GVXS9tU=!isAxVZJQLjg^ z1!vH7wZ`s+L;YTOY5`oEgiIn|Nhb>R>1%jFXyYnjcDHZeQtOP%0OM~q4Lmrp`I)qA zj-zg`RE3Cbr547ns5PKwwxVdqxSJR4b@T>(;daXj@40WECK8@y*K47x0A7X)Z}RhI z!E#9y%~;7eEUbFIzb+`+9q=9OO5Vh(FI0qbiEtd5Kxkzkl`h*BvaY*m;H-m>`lNP)X{7E)TAq4rX!(H+4L4>5}~435wOf z13(V1YCert+rtnlH`+li668G0CBYz9D<)bDRk#G1aw7mYtN+ahGVj6a3|E{yKh}g~ zYbo5;RFFeTU74BxI;K-JuLFvmpWyn5a2@r7($>9>xv2RiOn8A+3lx^V=`=k)v)3V< zF&a=G&ej>Wu)CIsz)l^;ub@@f*Pfrn#99YVv!yBv-dU?|X+lqo7lnqUc5Q|9GPTCS zW;Q;KVcTxO<*-#(opK@mLM6Jlv#s~0O0(+e{qdRky-W?gGdTUstD^UHnQ=}-ku2&3pN)IY;$Bi+@u3?Ix8kM|GsqCDlv$zrhz;bnm_0DyPfQBNYeEMnQiaE^PCa+8)@ z&~1{1+4vf1F(xRg!;GpnD;wc2fWJ#v(0JG*CO;M)wzIn}2L|1CI`H7?7-Z(qVvx~4 zhm2$$)A2aw)^wToy5Y7M=acwYRy zGC8!Cj!Lzqb_^78W{vp7nlQFidoXiE4;MBbEaTAc!`VoztJobI@|k?1DE_9SRLbl?Vs* zBDI>5k!9=3L{GNc#?=}d<4#)VB|B3J5yJ=q$um}qmJ_tquv9HvXlrYvR3JJPZxZ8i zR8{PrF4$l(D3D^r?!fE?RXLt&rB>0-wVH&kxEyF>J7NHEf${txjMA0yo_ZXTSe9hV z$~hoq1x-7VfW+D%f8+fZ&2Xm|t){t?{5f=CI@JHCr+%@fa#0U0lX;hpPBW>aMp~dm z8tdGswD9WY)IK~o&;}jIUAUD0CWD$pIf}>L@$Puxg%|Wt9f*Km6>Rk)xFu!9(+Auc z7GNlVwTHWXQhp zE;gYSTv~bXofx&7fN3%<&mO;YC>1O;*&Yj&x4jDl2J^6JnV}yl)F!7(WNGW(Y#~$*sd6bvio$^dA7`^U5u7s`H<8mhd9+ew zU6MJd>8je&OgG}fl>902BRs)pnaI?tqD83a+90|x!cI3HdF!L`^{+ptih*~;FHN+2 zJq6X?x^Yz(D1PG{V&loo1YSUQ=_2v`FS(T=!?_gIWE*4m1zImvkxEud+i|s(IS^pq zNk^De37vRmnX}HzIxdq~E#o3mXZ@rVlMgbt&s@xFk!5|k5Gn)J1Z`!bO5I_05o$}J zf}n=*$kiZuR#na@B(dEcLV53S6%`Z4G_UFjXxAEvrmS%Q_TgX21sFVhZF!ouFnFg7 zJsM&V;M_4prJ+0pcN?oV)8h>L#a$T54o~=h`Oq)csI8e?^oNl2cnE-eoSqwb{P3kj z$je?|hIyNfQaC8(xHvTdj5<#5osv5H<(FQOvjJjQ)ZW}k0(k}GZ6vVf)6<5jeMxH& zxfKFrOSaQ;5)N9pRHut+qKdDMVYlzYl<_S$g?yi!j9VT)Arso@RE(VLdFUzcP()0f z5dt^yBIob~gro$gtmV&QLc?#Ny+xYy^pE_ICT3s$;uoUZ8OZdHPgL|%8skFU07~0Qf>7UxNLg4=R0vEtakGB8@!ja3ZpGQ%i%}m(%x=$PGslvo6%U-f7>#x#uHU>G z>v|d8VJ{Yhe6TSjFCqxkBWM1KqCpjJP7ftw7st&mpuBIYc3O8&vrHq9c2d;@4hPBx zlgOP`@9^L-4iB90!_T7CQY9igA)WzFoajRFF>3`gj|HG^dbo2TEO0u&$hg^8$3Se< zY`u!n05^e!NTD}wTo)Ugj!cAUV&mWYJ#p&nx%ljFe%vxCM7_&p9I1likmfMJWUuF# zI)ybj`8i_|PB>cNWr9+u=8NE+Y{0ZqlB)%;2}nR|#)r44Q(&5`a)jzUhooE-TWIzR zm?=RWrGnL^tQ?;_;p zC9Acl;Q+WGmnUYKLpl9YiSk$6QhDcSTXCFVLfUC|48?3$-)!?IO0hXoG$2AnLiLDeKu>)6OzFFQi0(e~pQo24V%!>TvaFoM&qP zB)6!_&BJ}N(a$LNnKX}z5KDtOp1nzZUig%MjVc2xSPddt+F?|!yh?x0sf{L`IBoVG z?ZtaO{5^5@{>$;gXP=EPefo>hYj&fv?rNQ`AYcY8c&u&~dd9p%4*)hNI>;gq()UQI zkQOtSM|#EkpmCMq!M|xGs+e=YLFq4i6ld2yeryZH!ttxLhqsF7?L{tE8s3Ms9?Yn)1JT{28-IndgQMq?d&5JJmNYxy7ig&&ST`Qtwk_9}CO zrm3K;=||r3mU!gB%kjC-d?psNah%%Pjq&j$n!|xEc?u;c6?{Jx1pmhm{;&0q{zrc~ z{>`udy8eJ>RmBciQJ%bOF?+cY(IBx|n8@^Ed^~ogAVIm#A74E)I2?{mFDAcK#j|_fY;OLrJ3Fw1AklzXuVXm0m@RobHiC&x|nkF zViLDd8uQ`NZBGnqv!-PqO)X?Sy)c!r-A^0JeVM9k+&o*Aypf&x&ZA3 zh!^0>2`lq&Q%d-pWka|!^nBXyhgPG?RF{Hlu^|j*yY+QNK-vmF```d8AKpLyX5E<0 zj$^%xtf@8de4i6(pxtby%CdqoG!IA7*vh^2DNV-cP7Ryj{D5wlFokhbLHf3Jc zCN>*n2C`Tpxl)x=9BbE_K2yzdvEtLFVnV4lj-j)xwvO@gNLtCKo_Z=i`H4@)f~p7m zZ=5O;r`3U_3AAyr^2c79TL}+&n{(gEL7Pi%NdB7o7mdk)`C1fg81fYYg{k&056`u z7(e-U|5kkF*MBFz`L%Dx7XD9lE&E=~%!IGjbqplq;bb?r*ZE~wuUOt7(jE7U80lA+}wBzr|v@!UKY0lA?v6=h|UpSQG+<1G01Po@U0qE8tb?43F5${^<4Uv z;Rd7)8g%<{e6SxwCQ|Z_vh77tuQNenM80X`H?=fj^jGnP|kGyfJ|B1>h+;iy30U3IL15 zU6m?tGC7X>AG{QQ@kf6o>R~@#{@OR=`fIPnwX1K$;xD4Zerf$`3iI|xOjpTtB5pbq<^SWWRBqXV~xI?uoA zu{O}LPMw0YT*r2IsKFD}g1KgZ{aK}_q(+h~eotc+(YJ*K7dygJh8N&yya%2IE+Hf& z&u129^Ust-lk9cyTOBmVV_|gy&QslxDpolru;vMer3#U~ld}6-nPX{vAPcBtG2v+VWkAcxCA*f?=%4RxVrby(;?qCIdX$j82tssY#r zw+Z{%`l=Y()v;5_mwIq@dfPlEqWH2%pG zKU-J5e(WC~=?~o6-HLT{Y|}e*ZuFIG-UK>Xcr;dHQi;JSdfk{PC50-d5g+~N_s26| z{G$AsP~NjTSd_DgaW4@>XVy81c&UDx>J6Gc+9(GzIJw!kY*pD^K)tm1D10ZZY++*e zsBD!Cm9MOky@4$K&7gM4Y>LA^zx({zys%rgTm42~-5jMzk{TZbebnXRY`~=V$O@?(wn= zXY*VrT-U74CZE%C91$jSFm(R@i}CX7Z&=RJY{cnPXX3`qtJco7y3t$;Jczx~?nyFs z#o9n@F7|n?Kc`AtHnrV_YggA&WE3ca?EJ{g^6;uR;_vw)pct4Vrj4WrHU zYch!z7)5Iu!x7a(ffbekhmWiAf4bGTj-s%tiR z!K%t45>}TB*e#=VY5co=ct#nz$wAHNT3SR+Dw{g2Xx&H&j!rXH;5WtpthCnvy~$mY zAzp4KH4SVeuvCI|nZCAg?ikAX_L(m~8;jb&{v(e(8sB*CtEO2rt3bv|JGr}kT5E^@ z$gzS7i!8qu{sIvUU0Ia2>0$U&X;V^JD(qFsMNEAmAI~Ey)_3P5QJl7x@5`kMmIQ}s zv6@G_YQ&?DJ{s3<-&Vy_4toB|&*q{lgyeEqm8#Qtry1%_OM-=N`Yjzg2H)usa2gK4b-VybnkCiU{(ZAIhDvEW$1Lv$1uPM@{ZXt8h+BKKHh zROLI-X%wuLiLMiZd|;Jz!t*KEoG$OAR>=h;X+U&k{`Wf3_~j4$LOlU!hl#Ao7Oo7( z#(d!LDAGOS-2wJ=)fp7zx|2OvO(g&q2G;o|Mt$s37jp1~Om?_`5aaPA%T6ki^r#Z( z%_eaOgMay?^=Q?Ll+mgGk*WK%Cgz7B7=|-uvoGwNRYLBOiMNSvJ&aMc6SJe4(`TEw zj8c6pV~r(ArytEW`GxS0G8mNazg))medHr?<@s;K%^NrMndHx1Z{+!O`^XVrQUgj_ z<(r93MC(T@cK_u|YM-xs_a*rQ@j@}R}nnLp6@-yi?)Yea?zM@Mny+&KY{FsD%{=@)JSnbBwe{|Iy-Ob1uf6)3#@c|-^LeHwv&FE@kd1_` zd5f0lWC|iQ(SWq<*x`uD8p9JYPF>l4C<)logV_ZfGF!1ZYR9NGk}#8-e6v9dH;AfV z#dx-gqZz(RY%ohIx`Tc!W-~qf4}S2e_||jJS(J?pN{`#jX*!!G27}yWp8n$R!^@@0 zk_B?TivIRioIZ0pzH{YDYz?+_kV2;A)?pLx?;qHNPHcG-GodYjlTBu3hnEAQ5D`rY zD8_u&WjTrsYaN8=$Wv&o0l+re$P7Nje4fqp=fW|f{ATi3e8u&wAq6Y#=UzNo0g7cy zoZ$&#!6yM%x3SMX>&9{{4vl4YJW`h7X@(_-x+*OEFyoIX>{ zRpq26lD`%@h7OYS?1%qFV+f(w7ii(E6*rL_;o}j1a1g`2?KrM8W0B4Pc}gCN7tUXh z-7xE=$e>DF`sNF!w1lNXE^EXZ+K4qC^HQal4nmd3T9(P9TDyA_PF3M~`ANknhJBh| zCU;Q0u~t)rP_W)XZJGiXv`iS22&`5w%>WAi#QGz|l`6G-PZuaf>+6Jfe(9aaS@uo5 zzc-~VHU6(Bey)c5f}j++00EfoK9v zBd?+D0}SVuJ$vSih+*Tyu^kwc5}Xp1^mpc=Q)1J08LKj$V)B|}!ErudqjkHsqNTlI z0qKy?=F4z{ZYQo^ydjH=$?LP|u6wZ^{bpraA9$^`#Y&|BW;8!kHJgvlQ8nj=?)(B@Y|LXJDBp%c?Mt~TG;aG zN-?h3^^Atw3g^_K%F06bMp6Sxxm`+XiENYCBUyzEj8|6kXkRxTBy>F?Chs z${datx}aLvm|G<7($U>`>LVYGw?6c6{J$Un_(>KSFGnu|XUmkmI331mFQpuHZ{j#7 z9x&4|(GLi<>{h}5>BXmEnqWabEBIRR?-5eS1aK9%F5QYVr_aaX%g51Q4PsdLqA>$M zStz`LR)Tn@#RkvuPIMq%rm$A{64--?jX5Pv^L(lgs-jic)|o?+Hck~o2CWK{V!{E- zn2L-|y)enP1i(tLostJ(3!|YXDg3*X*^pCV`*pzp001BWNklOjB;M2Em~N3 znb9a~=8=XM{2VsRNw_G}#I)3rZL$(f%xjWEW_IAts`D=>8fDzB6Ip2`>8FH+3<7Sd zJXMjKm_mb5?3g)Pu)47dZZg4BXe-s9FCekZ(`H(BD&h?}loOmq=6B0g?CtI&<)7D;}9LTyrwvSOGPM-m}26XP~Hph#TJf`m220ox`J2S8EwDm z3OW>+>M^L;tc8QoE-QQ^E4vaXA#gv*jNcwU8)U{ZI+wDCDurBsw0mc)5w^$hOH zd*M)E$R(a56TPe~r-gvqY?^ulonh+X!S>d+4`W2v5`@duQh%<9-T7=FOv~a&igiyV zxz%m5c`unsLBCxtNJATXp0EdSInDuq-hl_`{;&V)13xc1r5LxtLNT`GkfP^{09EwRdiRK zXw7l)1n05E%$!t|C0wOmbQ%tM;GtW)WgZy7G=T+d(lQ~DQXOw}4gh#L$?cNqTPeYr zU@{zlZ$l&*BlNYoJ1hc8mz4A7fLAIpeo1fHwZA)ysSF@WXGjy!2(%=08J~S5|W}_A3$)xeCC2!$%BG6#iiY+1G3jhH?3!%L=Mx)4;Fez=9g0 z3Ty39gYs-jRW8x4_(Lmsx;^x^x5c|Z{K5Fl$A3eIIw87TQUKs=G!8Dj-f0sh+9&Fc zigMv9w`@W_C)>Q>M?7Z^IWBDUrIVBF12wz$N@L@x8Y?Nuq4Llr_;w0=%3{U~2CmE^h+vGN;lj^YT$+4b4?PX3D%b>t<_1IN8fEY+m?xKlD zl}oZa=ozffuE1h)rj}r&$=!FW<@Nl}-}{e?dzrzwXvCyBRT<6#)?%~%l z>TaEIG63ZDPh*!ZQoawS`Ng=616+rO*z>6yqXJxex<`t$%#pnF; z<;(HN+us(?e(6gmPOIFc;7qC#(pp(~K>p&t18y33DKTfM{mK^4zt#T^zn=}GT*)MH zs;y)(vZeAiR)ta80Q=(EORTi2yvuYUpoP=>&+kdgM`b?P*^=)$n|M5*DJ2CilOY8r zf?f~&a}f~PP)xJ1F~JxyNrAeN<+urHkW0>$l34Uq6Ob5zAkYjd&D$+|ff3a1MhL4> z;O}A)mmYZ}KJ+(!BEImiel=eG?u&B!gb-`be`BdknLB~HA%wHS4=fl_IUxAi7w)^L z@9;8*C=)Un9NGbQ(Y9b%&t1-(j>qHT#-65#@}-Vi&Z51Bgo%Yp4wkaTW8%g^%g;T1 z=1g396SToM8S}Jty3|X7USyn-sQymD>H)S)24=+>{0mvavPT{hn0gYno|D5 zlhWj{NI#O1trlF9pmf}XZ7;!m9izP+O||%Qr?^veTGDm$sW_;>`M^P>1PzD*ilC#?eDolT}u{Av|H^k4nWc(ucPfx)_XYF+pUSi_~wXj_*4@Fuu zuq?EL2tai@r(H&l*5S1%05gs6DIpccna@V!QW>aoSi!8sd)s4N&3V!u zPnOTB0PXyJs^M}F@b8K{)Uq{40XGG0t1ZNH4WL*Kat2gY)#M-pVkc~MT_6@&^WIfC zIH}GDJL^LfkVPDfj|0ClHrJG@crlGpw<|9-Sy#`O(NhqK*N8S8RjCZ_9D#K_naN&6 z?K-V-R^1xuC-2HuF^!l^j-pc;J1gL`CV6R|H`?0LK_pV=Y7tMp@5#7v=T^M*#!LFL z`15b=z|n&{NPa*knrkMKwc@#!v*N;V&t93snkYLFA#)(XFiCEjDN+_yfGW$%bC2b7 zg6xDNTtiV-EMl{^+b8R{R4LjUBK@v)NU>(9s;SIRoja?_QLsTi8&8c*uq_vrBsLrd z^P>|;pomt`4;l{U!;ZiKRS2!onmP*ZwLtL=lZ&dG|l8m&w~v&>@w< zwj;TaPShBC0ix;_Mwmy>>I|_Njo|M;09H z^-Tlsop!YNymM|96yfb?caLKkn6O!3>#=mp<8f)CC|GUjirvbp=KLyD&%Q_~0Pk_kj;6h4`hHUsk1IUD&ycZ0M83RWm(T=GI9^kOw5uFdUfnR<_|!;^if3RfKk?}&{{^@TxV zC0us0KJ)>pc(;3wyyAuB0EU(oJd2BP`_!%uEDwYHxm=AK?B~LjHywosBinX>5zBU%<7V5=H|wad9!@7R2# zBiKELE~hjlA;R6nnIO)wZYol9vSm!k-HCczx+9h=CFyz{mo8n3%O89)zWE2g7yDz_ z#p`Hvt2mmDWx)b5kO$A_shU0cTelZ*MEY_jc*o<9iQ@6iZ@&;-N@1uCT$~&5Dk_R~ zv{!B2s#u#eNoHa5)=R~ecZZcKEDlfI_+so1-EMEzv7n8WAeK!;%f@hG9zXu~{&w7a z_4W97zwtYg5hV8@P7b5Phz+fEu+qT8avZ^1O`vk;qV?-MW1{ zMk9D~9{YoH@Z6|M7^w>wP-<^W1W>?T`Fs^Ia}q|uAXw)WhHVfqR?TQsR>8e&D`xE8&Q@c+)TnefC!q=+3m(OX384Du&`5D0?S_#CkHN>7J_az4?+1=3H0Z?`En}lpqzelGRXQV%=+1i zoCREWY5$hdXIh|{&r;ZHaEf>aFBo2#Xu;3WUbH?Bgl z!RK8?YiYtVY6fT{bGhbjqVcDH>*MuvUwlTBms7ow7U;ohl3qEcBTyx+Nc~uC^k7u9 zWuT*Cww0o6-jqpczIBvLlX!%9Q!+loBg#Tam&_TZ&8C-JgkXrmGf5SBww`&0`?S^c z!$xR|83)|ltaZCyy#HdnbmfZ3t(b_>ax}X>y>VHzv7$E(6=Hu*iCo2Mv%=-Z5T{=Z)xr#xi81n(g$H%(p3K3HEYN@bUGopxqdC)ddk@&FY zHYa1yjA0p)Xt>hYYR3NZC}P!#$>B5xt*vNnI?<}Bo*9qRx3k{hx^ejS^bWOwmC%yY zG3<&^J>*2!e_wp@SY*V!FAks!-}RcXDSeyw*R;TyXgpGl#$yq8?R-i_<$O-73Y;a> z07-z;lv74T{)a!tPx106#!`-lB`iC%S$Ywe)S{2P>&A| zW2@U&JD^st2jFfYq5wLUFSaQ3)_CfODeb9`KCE;Wz9ma9ZSdRq5;de_RMM1iO<5)q!9n;Y3!z+iIxVV_(|2hyiDJL-Il5|V_M3+>*aC<|sEICc z5yQbS>SY}h@SILRc81%!ZRyjeRDs-198{1`Y0m&{Wg=){=iA=?c99C13Z3t5nP5j1 zo>UWlA8iL3#ZkKwEdsXpR(~Y1^&AhPj3#HI2FZ^F4o*}&$JEMkvn}hWFu_y0wQ;Gt z2X{`wKUz%g7IPURYQ!(_Hbv=@WU+Dl9w$&Wwzjme^<9|G?H|NuMWv0kPdgrX;F2mW zBqtgOWW1Uex;KElRIzyx1pu2IuNUS=*p!e8q)MH#YSfIQ%{V&Eek_lc(d%$vWm+&( zF105NAC}ST(q>W}1L!wnzQh*BhLqr2v`@54REYd}CWNk{Ub`jcgGha$^^&c&G6Nb) zL&A#r^!amf{((#J%oo29qtQ@K3Q9s>%*7wm-Vpew`outd30h4;TxmU}BFJfVsWJ<{ zs_LOvHTcf6h9U#bTkuZNLAHmg<|fB{Pyd-zF=c}4%+H3%t3~84546LD4vJy7m-Ln- zkP7fhjJsu?Anyzy9+;unTjOvl%NX`3QoTf)G;-9SwoRYZ3#>|!aTLkl){udf@N zKD!7`Szz5hWj!weqGuft;zUg4)_^<3pP8WZV1z`&L z;@TV6bl3}w%@+0HhaZa9Uw>8V{f;8V=CXbkKA2f@(wb$uGt>ksE9Q9+irMgcr}hFo z^yv0&iJSTC;7{6pk_cxvn^{~oE?x$X?_bPvzkUd zY{YTQquc1kY7Ad!7&V~~d|F-AREY3+aJZy;<6E@tttA^}217PI1UIE6l-8BC#`Jr! zidK!eq=7PZ3K_=0!40$_9$u#%I9fb#`N4Sfoo|m%ed-UyWp&}&!Rsgt=+(9ta!suz~i2&dG#dDhMo>EzOY4FxEcXTXE*>S?M|*^gt|IJGS?B;`@K_>G%q6+xpeEKBcw(4bTJeDMmvZL1~xCWIrrUvpZvb=+`J)s7^XpR%fPl$be~|j58`}*%s#dKTu>UDnbPB+0r!Yd29xaIND638(46!qt}9w-H0kJ zk~K`=kZ7{mkoRiu2tLKPbz;z}ATONU+(#YFedU<(0o$)&QDjH7+abc42&!wo=LGn;$1*}k!mh0t3o zp2<^4+Zp@M-`U$$QiyJ@loV)TxS1DD1UQWskZus^z&dd}9qX{tL`@M~KCslf@aLV` z#Od>AWBd3(ennmC%g(^94*&!Lb+;n*Pg z;jodD<>KD6F9)?OJ_O#HEiEtnw1uH!AYx2a*ooZa9_NYbN}>mXwMou9Z0fMW)dA1(+qZApVkYH9n$n5OWD=^dAC1xOUAP>)Y)pB> zfhAJmqT0x?UQGs5s^X+6CyBcavR)DI{^Dfd^zbC*W|Z7X<&y+o@E@FB`P|Pw`)us) z?&wk}+B;fNhAMn-risI?k<*k5HxGy>DJ-mehF`)9;S%E6)g78c1ttxku0WcOe`azN zIj{=oE_V$ye^jfwn2amq_A-_6BidS9CW(5pfeV=F&iQ@IWs2G1{r6wg&w1nZYc5)5 z-fMa;t&-GTN8{7){JDBYOGmN@FS*^4DAml8GScKa{DJT^_MJri_J^tdT5Lj1=c4HVV&@wa8ot*r{{7 z+Q`TIU={Y_Vw%^hI&ppfZVY!uF&19JjTsabakR?DwQ(Te zIKZwpF+hLAT00SzH!VqHR4Gc&88HMd$W-P=7Yii3rslCm)Xre2iA1T;Kp<<*9lFp# z$c#L!u+Ghsd+4Eu#&vnkrB2tEeg$}^2*Bw#u`pG| zS#`D~nl3>u3I_FrUyck6WS9usOAne3c23LLOy>dZqSg_WU`CU@=dt%h=kf#b+^7FA z2##qG;H;=QKC>_5R=W|6&%En@r~!oM0I(`o8@9$x^Q^+f(VQdsF?bBU#w{TL*=yP?LnFPCVwOydggknMH7I_VwF)}dNu*z%Q2FN<}_b>0Qr>XmKH^za5HF$ zPIyQ6jN{6pV{uC2C$J=)CYX9*0G7BFt(9IBbV4HJZhCL;X3ELLIVHMl$htVr~gy zElPp%Y$=<;>QoJg6lwftx7&|R8oSkvW{kE+sxA+OANVpUY-jx}K_nJ2E9*_gyW7AT znGBVyI8a!)w4*fvoNmMp7}e6`S`H#?jrsee(rAW;H8GWt+Im8o?DwSt!}XGSZ4w;CYsbU{)u7X&n!Su|EiqPKK{2ZS`-WXRYH98sr1j%SKMj?J9EJ4;_?GuSu8W+ zEu*YlF~KX;E^B4rRDM7K;!724 z3l1jU`pUEhkyNSr6$I;+xjZkeCt-cf7-!(H4(~iGOmfArpb>l9TXEsR2ja>%zvZqh zecxofD92NqQ#L;HuAc?3l+41c)6~Yj>A>L2lv_e2bGl>-v|B8rzcC1?wk=yL`d(g| z;_8)Skia}&cxHxr!75!U((QM|J6c_vM2pQO%^w;| z<9X}WCKioFG)9e>HI@;Bh~}UZqnBIJBc&PABcooTxWP4w>?Zw(benP*gTnce>zlH?G zhSw4g)*Y$Vt&JyLtTjR@T*dl8l68s#Tn%&ZF=oPz?!~n8ZfLEU`e6ah<;v#~FwI1g zibIFL+elXqS@CJZWjq#B@O2~h8hx!zt;N*o!iAITMhbz|fn;l0oFz)jD*2c8oLf!4 zi;99YS8R{i=%xovjva!sA-|I;Y>N7cmFJ=?Q#$q)a7n4^$^cDXXFMByGXEN-n}o<3 z=>kjv!iANS4+p&Dp>^h=`{Qst7UaP}S_T1yn@6>U-j>fRvTVgb$nTd5nvPF@&Zpn~ zk87d!3H;hiDNVS9DdlR&_ORIF@&^m!YBZy#G-6LV%FikmB*GmM_!YK=V6@@GgI6OMuZG&s<9F+YqdfIpWip}C5h+q&Bp`4B5;*_1S@=%PndUu7aFhg)Zeqr)ufYOM+0 z;5HAu6&tEgHnnE>l;q}ONdO85M}Y4#+A;-n%c#j;x*GUEs3ji0dvp*#{&)UPy!!Hs z@#63OQS>RDb@9;VnWazbKsAE%0|yXo1PX*8V3mbi4!0s`rOM3d)CTLAoJ{sbuHpSM z-4PKW){rt$C8WNvC~En91bnCpp*f6_cdP0JyYm!c=WONBf}!afUGf=~Q4~Jarwzb}y=i6QmJN+aT8K!xPGT&X-z` zJTuh8a>20f6>ytp$29%%C*Bj!Klk-GI-aO)Sx#p$YWGge3^TjghX>z6K2SLcRE}oE z5bYw*1uv|k5zQ*JSg;8DRbmx%BADPPG14g%@LSa?YLHAz*8yjC{HC{#2kyV5D(}_r zzHVLTl7qjmY9Ba(NudH}xNwty$Hk^jCLJ7#%0T=1O~*lpAyRRimA=$N0O@BHddxvq z0xdT2-XHsmaph~@h&$J>D|1to>}nO;Io0HXhLQy2#P?iaa}LCzHP}#BrmpJ~kNu-s zC1HAO7xR9_bf=vL8v$%YqsYXQTq>@Fb8NMTj(^YALsb?BMQz;`z$_+T!n8O^Ta~H^ zeja!RElCkMO6)4B1=XgpEfn`Ke(}JTkh<4X`-Cx_=oe)=l$I#Ow3E=Rg~o}UiHuXxn!m=Pp2xbiRxiK3=u6&2yGWT&#Y12Oi6U5?IBmJs(149SfZ1OL5(!!g$f@CQ zEd*fSI@*uF{4@V;y!!1I;+22>36cJkWd|c$gge{hu=$LO*&mC82LMz&FfpK26FE{H zeq1uzX$P?eQrPDPi|9^TJCg)t#|5f9lmy}zr7JC)Xpy|B?r@;V(quW;?}wafyXlS{ zK$wWGIe-_6RyS~d4Fi@<$760BY4I9{<_8!^8#zE`_tvzDH;*H_=JtHiXdeM5c55*uD1qF>DP4#A;#X zDXems_A-5X_^YT6n>xBOH89tRlF+;ygx3ZZ*-XX&jH~RNKCN2@kdY0+i{msd9!p~6 zD*A~K%S8;UfgdK}0{ndr3d;4iT`bu!JRD&`$XGT=89LEvRRU-g@x_6=o(lt0yV6<5 zy6GG9dhdpCqET}%j_*ukb$1iPdMgI)fqH9<)8>n*DkS-RRUT{eJlNy}dxGmgROKZX z_CHo0U^6?NzGOG5W=r5GL87sa35@hXS2yCjo_IX29o&h@H(rWfy1{OUlxo!aGt?G? zDWtVB{2TXmQC6%s>jss#`1yM(tpr-jq5JeA}sjXi3CPdNhS8s`nIAms$+UI5hshF70$G+AvR5o=o3WS z?AlVCHOAfE^E%|cfKjqfMQg~RnOthq+Fo4(DuSdYXkRBVFSMBd z(u9^8$e?^s0dqVycQ9cfhHgxo=MC`MDk|j

    GZ7Q%B?XzUOCa5fXuk6r~OZhK{@t{{HJ+w+6>Z}NfF9~gH#$`>4w+=3G9aUo zjo`-+HC87OXTb@fPjG4SY~D-3zZ>qF>YrD zJ3KcqP&uK}%4+)Ti?hf>_>Q{lvTCHLCIpA-5H1@WMY(gz8PjV~SP!SbnqkCtY@gkV zSHAnI4tflYIFzXTc|ZJzt^gQ-i=7sS%T_oVlewG@IK678omx_1R z>L@IjOOuO!2@Ejmiw;xCoz_$!@yvK%RXeI-U-h2a)dcGKj$Al8;8Hb#mz)lDX)Fy+ zFiqA0h8DEjh;FMDm!ACIxb^IpB@J8>aQj>D{0FsS%|#3~g`Znjn~LfKY-mFeqSIvb zK)GoZU@3c_I3Q)17q^HQ5Qn#hOVf^dvm47EB{5%UKibX1=%AaLpGs03x+Pa(5`^kT zqK&$Tg|X2o=edzi!1$D#lnP?Xlu4Kg=&cV=F>*8LwD7~!15<6#BJz;r>9vYsbD$oX zrk4#1n1vjqKk6wEm4_EqGsde$bk?o-)Q^29x~B)RbFCBAckB4R)9;Vdi_`JV&%Y4u zc26{nM%9Q5moJFvJHLHs;wu%UG-?>vB_9b*nqWfOBe+JZSXTp4EsB+k1g2?Uth68o z82DD@jT(%@X5jn7Y+tw#uYT*#oCDiZu2dvYy~zDCeZdqlHM0j(k#BL(!N37Kk;{$w0i3?gOjROu zi!BTYlqK4Zy0;z6$s~HvL|V<*r4p2bN2XLI?t~1Dp^g&2al2(o4{U&ng5e0&qi7YY zSg;HU*~Rt=+6nilrpq335!VJengh%Doo^?wPA)58CFn66$erBdT%@$=Kt>Ey&OLBZ zNmcXBGVVKjDjxjdzZ6%0{}1A5e?QiARloh%Kd42{lSnh!)|tAj*mv3pVx*WZQOZ+7 z%V6Fos4Q>Ugq)BZ8W2C3psi7T+jT&(Pz_Gbg&hu7BVixLCGN#2fjbl&OrA?DDn{f> zPYN`lNDplOx%6weBeJ=D_3 zZcioPoXn@O=`}?71QF>*H@^7M7o$Du#Hri;Xur0OxAoo?g*0qm2T>W+Sb##pTmw%olZ-%-n1Z&0-e&n<^MFI!VB6HYSH+HT%%; zqIY{Bon~$br9|Vx`S8PW{{8QdFaOqWN3YY3@!fp|$C6h_wXJ~F>rw z_*v48JU!+>pncK6rQMJ`ht1h(W0_0>^S~t6oc1d6T(U0S@k$sAt%}?mpnNRn3M5tK z028|$yxh|2*{Ybcc0`9^=u#ruO_7%bglRLFi|mMWR;hAS;&eKz+EKvPFsvI%bx1O9 zwqtrY)?q_c#DxckPMkn0F7^SRw^$TIw`k5a+1wtE^k)FXTOaAc1nhIFJPrlZ*pUxe z^5Utt;SXWN`Rv`6n)`IvtfN;uz;v>IsKAsNk#*YtYkmG91DrgtmN=-w*Xs0C-R+DZ zk{a(4gP#9TF{KDy_$w@a@I9U>L#0h4M!*~Ca$D_g#o-EV;3|4)9_c|49%-JMvvLgo zY(!&{BuM#`_P6&rLm_S4nsbhWiZE6Py^sCi$Ku)x--?%C{!Tn_|9$bm6(=`^c3OgX6LFJtB0Aw#DHjlbJf#pSiq-htGfV_1!(V917)n3J_TE(ip zkQGaFRmawP5Y>|LHJqPStU7RGcrrbwwrC=$`$VN8H?XJ>$#7-k0i5m*MtUmh)6+yy zYV%ZkOK-=*bGKC|rq6E1C-4&;iYPP= zFRDxqWMpEBrnjXD7t6F>#s>G@eou!>y^w*7%?L^RF7fVZ`lw?fTWVUM1+CS36W{yv zN8*KVJ{Obu#0QvWmsVXM7gA*e`M4cwpLD3r=6aS?SR(2+n+lN-PYEoNwkT;Q!heeI zeac}paW9kzE@Ud?%jaXbtmDqL8?j4EKf@+x8J#}E326nQ&lGVA>9ZY!;b|R;Opp2V z8AePegvRDm*HFNnjETbV=19ac4^0#K1^mH|tcM`iPTPDIrq+$vQibJ9s6aR}=F9fm z)&lnqEPTo|d9)EvRLR$Bw;ORhohEmyj{DyFaO~f@75m4BT5lKjPQ~@>*TkXhGu8a$ zV?R@icah1eU2vW8vA&>4n@osUBt>EaHpK4EvX2Mbo3~@xUdC~E5tHsrTq1%vlqahrP1^v&jhSW=$8sgCWoIz56Y86s6${ai8%v{f4kLPB z=mENA4Dt)WPg@&qM-lGC)v?vHHl#$i5ZY){TK1J<^N~u;?b@>Zf-!x!JB$}keMh9j z_T4(_TM^rht*90pJ^-k+GBE0=3ccXv;?AHXU6w_|HxB%VO8dCBwVjzObaj^s3d37Czn@oi!5XohQ*=$4(hmbY8Lz3W0m^@*sC`?acVw@?s^!& zzY`e2^ttV|06`YNwgo4=te-q?7sH*liYW%T3yZZd>9g-Yv83Q1@Qk{aq%ayjW)g+L zf3kAq*&HSyC@(XaMl=>^6fUO=+)p2xSH}Zt=SUj zefe_Sxp^xN_wQ)b%VOf$QS5ZO18sh!4?H^-9h)8(P{zbmjQ8Jvf4ua{%ksA;=MfSl zfv0=JEy*N+2KY?Em?+(Kpf6%WLP|PLvfvb%o-U{Q9>x3<46+Tmb1p!fO;1(Qu5fio zouu9$@5h=>{F0jZZE@?uW~cR5TVy|Saq(oqQj5*6=_ze2F1DLGwyJ(KH_O;14t~9i zyT^B<9=2lAT&p5z@Aaa)tfIcl#I~W4Ucxa1phKxirsUW=S(wTufF&MDXw zH&G60o{d^~Q5R$_A#VsAQx!cXch92ZGy#}@=R~pw8B(ltYZJA6bD&ZVExUr6svvX4 z^Jn7w{{BzJ)h~Z7p84HRMI0Q({oAKytHjS|f=Jsp;CU-Nq8n#-&WMl40S1sg8uZnz zbh}I{%>^{|!t1iy_+mwkA>Cxb*!Wj?u?H8drb)H{$MBzn(M;i?n6` ztC;x`biy8U@o{3MoZW;D^bu)+C5~S%}kY|1;yu+k$p3&!%ZAki#R&Jj>UZd^i8qZ8-IQjd(%O5 zSHsw}syL?FXhm%G0QB<)0i2Ybg$1#4UC5G~sseU9_8&U+i!>(_VM_9gFaS;7(qDB@XTOGC>;_)3zW1cx;S91TCEV+16DNv;qaz<1CRYVs8%z; z+eWmRqB1p2JCAC;jIBmX=_R+88=FfEFun1x1ucxb6V`be0w}OP~q!ey2rl*SV!dX>1zeTKP3mq29 zfnyR(P9z(TgQI}KJY$svbu@cmY7I@1c`h!_9AhC748Ky@%F0VB(|JyuPUTsKO2gC|h90Yhg~i?! z#%N?zhkZ;~@ld#Q(quxJ;czWMAz5XJJj-Uq;I4R>r(%QWSPnK0uTqt?cLwqPzxSWT z{_|Jj^S|@S*jhwX%gkVesg81D&Y`{S;aQ`K$G-Oiv47)6+<5iX)S!YvCi6ZGB&9w{ z%jbTxBfi{{%0a2-b~V99N)nuQE4ufciOV1PP%N)qi%mdJbk zGxoEBFzhTLxmQlP+27z-z&m^g{xg}+G|i+q64*b3pz1Y(fkET5@A{cqLgkFl5Flx3T8L$4tJ z!7F6rw_2h-h-6EJi6WlXqTa+9CO4cgL&VW+bu=j8#oCLz72V2-xbx{GYT9U~Tttqg z9EBtp)cOF{^P>Zu6cQ0J%3jp5;)0;f6v&Hw$}bp9hsCsibf}w7q)H}VlD>Gn-v`X3 zC75ExWAN0W6>fLdUogEwPSld+ljYn~rfG2VlSnGn?7wdmJ?Dc`UoM$%{DL zEMjnaCmuY1De61RcK$i&Iwk~Sh)^k<7e0CP8 z8TYohhHPA0aH^K(TvahE7C(K$FnEXl&K(hW zJqcuhMOVY*91}-jba3{1EmeDXr?hV#nn#)H3t3pxe!@LO1h21YyjFZJETJ{=HHM{7 zWG?S+H|`$Z5t*96+rr~yny7Snex8b6bMP9UdCPx?A*0GU4j@jmnz(vKq;mRCW4>&g zXrYQL0z43T@{E@;X)j{Fy^Pf7H(|_*H&$@Q{O-ky_)btrxR57{o&#_&~h)$A2PD56W{D z4JGv@0aMyn#b2tTNiCkt>RkQ3*>DUnd9y^b5t;UQzWtZ}2nO>WF_C-D+DepW7I<06(9DKZm}I*PKhQBx{BUMMzx172}@+^24(NHzAV}<-}?o z4?X;#BIdDv0k?#$&5s9Y=9aG39uN&iBT-nCB!i_305v0jyqbvLn$Kdew;S(#;{7q~ zbmN7uJR5he-}D3->IIY;=oTgE8<+!}6dac>fk~v`2x)ZenAx_bjq|>6r8pwGouT%{ z@nq~OHDxvc#k7h=LI&8+J73^t@Ft%)J>YU+<%LiVGkhnGhigskw#i+%b0jNpIDV{L ztYo%2JyC9k4I2JBZXVu|0A6c>AP6*ka2C^8pw;ghYlIfUg*98+oPgny%qm0LUhJdz zKJ2GeRmc8fraZ~B4e}P<+9~-P{1T-A=Xpkmm9~!qYk_?-7gmYIlTaOvF1YhXxEgXY z1_&IiTr7%jG6^`e$P`+2{`!2U40igMilE(xSP~*UwkTgRL!?hVcswW*c)ssv>&( z6u0US$z)@RXqmjwCN!II8@4PJ3IPh^Y3y$ANM#alo2{qdFG{zQ!L-i*<36vJsmXG68)1IrBVc654` zHjoh@$IBCMH9eS!MGq~y4~=H zr@t&}fxaF*y4|)mtjR)5!XT&ggw}}+N@C_)p~Hv?((PL}wSd7d}1UUR1 zZN3s`N7sz{yZyD{hcfjHwCCS1iNf&GPrlu2gWd34|S^J(6O6rBOWx8w~bFW6rUhB3W+VVQes~=QIUi zR|ySoird^#?vhH}wEzGh07*naRQiuf#5Tf0-8#H1EhMUpm$xyI2W&8c`$944JM#0BHo}$CU41aDv5?4eVg+oJ$ zjt)|e6mALm?>f6O{6X(nx-l<24j7zS^`?sEG)b3|Fd4UXFI+Ms%+pjmSnIQylO$RL zJTc@+!mv0TTQL)s8M+iZy-plX2z0R6SFu%)24%2FKT)a${F4irigg*C9@EE+ z-0M_vJX^%ufAHzJ@4fGful~xvjO|u0I+ImYQ`>v*x9|#cG7pzP5m?jVFqm34HgWOE z_r{^@{5we z<#DfSoIs{b+KwwPD>%3qu_prmBKB@L9L2>a-xs$&`vtCZ?THo%MY2_DMdS02{Y=ef zD5Gh1GZYJrf?1rY^e+yC4YfQThYI@=el(9BJec_+Iz%zy9MB!a&1D@M5PxoKBCwWY z5gBJ_?(*Q#mrU`pXbXv1sNECuy^akUFL{V$#CrOji_g+&YqKiIRiN-DR=ax&CIy&A zlE9<^fSPGZk#`tB!uJUW(4)K|JX%+fsAACBbjh{Jr%%+^8eEHN2s1zqrgq4kp)=@d zVx(ykn`c9wu){>TTZI298f%~%1%bH8e3c-{f>7y(l;ws~um~3*;qFBJK6~Mkb=*9z2J{e#C)bC46M!T=g!3|29b!qi8 zhTitPB=WR0#p|e&@TD*J%zQTdJvpe`P>#%BB9pDkS#0Q9x!+i_h-{DhazVhNHPGz~kStjj0zXq$&SMRGoDmAjP-Mh) zI6D9sYuiK*uRffvktIJkh;`kGgJ++P;f)({|GVFhlX?{KcoEm4AG5_Q)<>u_6zf|I zJ#6x;LkJn9*u;&W{6O^DHcsDsI|lQeR5glhBAfg*=ya)q19W~y_h#bP{dYH0K!m9mQuy!>+9yK^Ttr*kvbX^^peQ9P)+u>dVQ zv@s)O;F^)HkkFKfDu~>nWwNCvt{L;nr=(M4g@IAyrXTXY^fn3#UInRgd*3KPk5XOe zqjES@8qb3_V67my4c@dZx62d*!f8~?l`qRLKyU{BNf(~RZ#oZiFPL0X65=LA`AL6+ zASZqmnTiGg7XAm=$k>zw6d+0*OxS<~w%Pnro9$?Fut$+2;foFzR4I9I zYm@hd!y@CC1 zXC?;(&2{zcC4-HyAf?P|9_I&?eU#>HoXm|iqV6~w*z|)I#kB>0-xY*H`dsSYsOWKU zkWD@1r!C;Mxh2rI(%O_b9euF`i`w1BeLsRdgnVtpiwkAc>=7o{97J$?xxXAuU6@tmMl3^hdpz^}L?Z4IAvLsG-=cP{Xi9`3A^BzmD_8 zJf8aK2jbd4|6Dxy`ZwZ-zxTi5C;EpmS~215#^r9U=`2B6gR_-&$5sq*rH|*nf_i{t z)PtU29_N>Upz9bLf>OiUgF9 zHcL^bsg|0Rs2WJ_JT(B$F_dU`UTiddSt#O_v8Wqo6Dmn{4^xo>cVx<2f(aX@+s1se zjGk>=$Z}Y(N;0_|I5@y) zLZ<>xd^vQx8nPA4C%6`YW1+HGu9wd$BCg8xHg23;4=mso$}ai$?6YNWQ$Bt|y8Av^ zYz|Q7R@2c%hrSt#v$lY*aRurfM6)ojY_V;m=rCK(WHoY$kLDqR-1s2TfFkPXB(tbSa~Q{l>ZKfn zd<6!n*l#tSUX2e9;`(#X#GO}PRWPJ1RTEe|ET_~0-?b)b^I!;AEXeh0Bj;S*H2s~C z+`85#GqbcHQrRe-?5_HGScb8Q7xYmi#ep00QV)W@%A`Mx4&1VYf3Ps=SrsS%UU-~7 z1QX`)qod=vI6u>8?)K?_IM5LAvH9w4syID8kEfq{Iv!t~DL?~WWb=){f%ONLG2S4S zhaa4=R+@0?l<&#fOCpGJkM=M^;|+Q#WO^wpA{I_M>=JQrz3(~jD)HmL`HmmCwG zD_&XwlEu^Xm_vm!up#Kr8ywcVv81JuE;xnJpx!yd zll3~TT|brqumlyef9L1Ny|Ch}?*AXX@+&PT2mLDQd(jRA_s_+tn`+FB27;T7G<-O7 z5#kqbjv_9Rxojjhw`GgS2%HLnG%t%idH_#-8p>;c2g=li%_Tmek>7a4)9V>Drl{P0 z>7_XS$cN*ve)mhNsTYz@vD~jG^xm}C3`qkjNu|>AQTpQszuqTVLHsP`L;2bKYn=iF zW(n(yk7eR8ncJi7nI;@32ZyF#&{SShLOC$#h9mNiq9$pqOglHy*1lX_#M#9x4i2Z* zP!GyH+=NYRW+K)jt_{ZV{L3%LkG}Oys}ig^JWwL2HEQNyAX8|79MqUXa0^lxjZBt| zEyd@=(M{Q66cm64s1GGRrAfyeK$jO63U=c@0pl_z!%3VyJ{1?x8b<~~YI2xmpbrT* zkIUDPWzI%{*-&|s$NGR$M6VGH6s57Iap2T?O`I<-xw}(@0O_5VHABwbQefRadi2;4b?3ocP&9 z=O7?UTsQGx0A0pjkkd<(l6khL^!;5p#4+M{Aog|mqCek>v8IxnIKMd0($hlDdJj1e zfi1GIol9veJ4gy!(`@i*-58H1F&<7dYzeY@BxH)WCls)R6O@1@pr?lSWR2*(*|->^ z5Ee)=FSeJ0vr{SY*1K2@S1O+=$ytX4a~z>$xGRieM|*C5ei_}9gIK(D6!%|yJ#MUe z@pNaXdj|Aj#<>H=44PY8#RNlIDyM^?sxXoB)80$NEhM~!&NtDW{$iQ1T5&an1=GZ) z`oow1y#f{+0k%Fpl@Sze#5bM}xa8N0d2w4fl=>r>L&jIKu z$S!sR`D)qGhKNO4+#hHP!--L2>m#5dWo4ztCjpK~ee0QL;@;bL?YhjAPQlQcLGB7O z+UJC^&X9r#X?X@Lt2Q!_bT%ee+@w+zuA-(MVzXCq`}y1P_T9T`fEVw;^%U9WZXAyf zVl`XDX0g%~3t1EMAfXkd`2|4dz{Bv#9L zx~!P!%VF0D)ec3A&rj=NnTMP59n1f+4hY_S)~1PbIHUlot12doB-QuoIPbNhu<$j) zjYp!79!4-dRQVE{ST;*>PSw%FXHPD6F438M^@NQPaKYAU9N=ziyd6Fij(UCXH1c&V&cpKIFsHOQ$&| z+)lR_k55l!>IegWJ9XcIrK`po51!t^YNgjLJU#^wKKvL!s0{@cOL&U48tU9!YIq5+ z(Ib#-D@}O^H=(hcRU920`2@f}t(0MT;&LOX{fqVw!Gm;C4cFAslCT;Wm@>sER!T{( zw%UR*IWyW%mM5q!pTM!SE0FH_cVUyUQee}{j5&llm=GGom{le zs1~e(lv)`kSp*tqWO~B-I6#T|mTtXIYWUkMz&hz5>Ln`JLZBUM^VdE49`pkZ6cj)- zOHeWjzMlnKfRL25+7!JiwlviBBiq<67WPvlJCAi-t`~9RLm!M6|H(g!)%`p1-T(f( z(Y=hAtU57@k;=0*+G~;~1qkb`eU}3dsUHXd4aoIMM+)>>ahTDP-FpEhYeU!SXQfP< zKP!kE_}b+ja7P*v0fc}vt+jpT-HC^wW7qU zS5+WDqWcsGM`L9a+8Sho6-FyuFTtd4_1SsOJpTz8O)!U;LnO3lUiT$Ru(?<$Yn`Lx zKkUY`XDMYkt>axMjzs%vV?N9l37-asu4I}}Mn3c6i!prWR-C=|YCL-9z7wJO!q6tZ zsB+Q~yZVEV{M%L~oN!H~w_9=WEX`|CZ*2$sVY?S#)T9zYHiC&xe)n5+Lh~P*Q+WD`FKF-{k}jT5nesz+ ztkE_$U#Vz{))il5wjz%+9h=W%JRL-UOeWKjiAEix(L`pa2)=bkG*rn#m&%|qZ)KQs zwm6UHKl#Zx`TWnu>6@>`gFpVuXfM_=S^(#>>~dT8V$PspJkWZf;aW2dMuH~IO}KV8 zohHq!4f#h{$6neXh*Bb*JsxN{!c!j>ne2Xlud}b&E}u(S4J$H8LtcFii9X5IX$3q2 z`b!sq$C)cmTG-l=MV?haBuNrErt7L?<#P4@o1|WR+-@ zr6(!(>ABmtqknuDr*FO)5AVLCgsA|>b*<2N#A=DCzVh*3ZG~=zCXhn^^cn;eNMNC6 z7{U#Zy3+VVg2mG`m0(kv(oUUOB$5QdvWo?!&8FiyiI#P={Z4%NBOi`8-+VLPxp!Aq zTcW@~^T?lD*vQ8;ooPNZ^+==3m zQ8UK4{c@1${%kWS_TqfKh^C8dHB;1OOmGR8L@=&K$PI-gb-OnYk?gDE5Da| zkV`UYdScL8+@QxLn&(6|i})XV40)uwrRmOe*@3MAl%3#S4o*>(P@2e!#(~FWqQ&$4 z{nB{XXS$-)3BL%&O;|(su|_ntxpwr#I;j6Ld#U33wUf9weH6{bWlSmQ5qX4v*>SI_ z+&?6z0H05;9qD;6r^qSJdsd+otPa?9l)3deNmGmQJYpjJp(YUb(DsW*#mRt=9^WTmNe2& zj*jEK_wH-qO-DmTB<3g04>*Tbf6&8F zgOiPl)_fW}FbRU62d7OFq*QiHmmo?s1yHoTNl!^{MqRXg>4k&UP!%jzQ0`4Duqw{7SEy(qLH>TGPV>er>-$&LW_oSQG;H_V^cxMB}?0d!Jb+N0z}%f$Xf8uKFj3zVtpC)m~wli z;CnEf#$q-%kC04~iM8BrEQ!M51*Ie|DTgCg(<#W~QHvZ$u9MQc+7CzM!&rJ+>`m=9T{~!ZCfqy^Fp~_}F3!$k zyIRGu2-Qwfw&26ERU=asK%QiKOvh!r<_w>nA=k4Lk2mcDVUc7OG#K?ozY|j>Re~kh zsJ&ENgJ@5I+uB9-XP^F}T6Ex1RL66R8b5r!W}2pQLl|+(i51)O&04r2vMnS_XatK? zoDwfwaH&Dv6!j+40;AbZzlVoE9mRS%->Wm~K_F>Mld>oT3>iuLVHI(|@_*}DnOae5 z^=4KcZr1E|NAA>!=gWLR=8Oih@hKPbp396ZC79(poP8{&C1hB7{VfDhXHqcpr%caS z<#`SE?lbyZ8z?z;QgP7q%mIrN9C(4sIzYP4SM;E1%(LF~nTpvc{K(?Rz7Bcu?;7L%SBJ7Muak-eQU&wS3z~_(+zincU%(C!wE^*+$3EfZDfgA}NCnbCq z?MfNaM}P4b;=6zJHR18rbHKLPn9bTP=LI}5xrCff*}p;rC7?sG91y*<0BxE zzLaB^i@C+ zXVyfehYz-J*N!DSSvo)O1wso>%0gIP2k@|yj`w*!?;F9 z#1pZMEo_DpNvf{sB$FmKrU_9e7rMJSI| zS|j~y&p7nKnhv1r^SJfYQ}N8_KNoNP>DQxq_kjinKrKeSK`d4?i~~bwDm@JX$_7<{ z0pw7Xj10fJlSw?hIE&HP(>>^BE1A63f<4SB^M-?%U0%c#U(5L_jwrKvZ6?lXAxBoH zl)5;StK3(85bNDKmSBS>yZHHk_bYMtKmV8b!Rv3vv^TNXou-C@fodajkHIf(JKb14 zJdGxnG5zq<@$$d;r5JqXQ?Y&R)%ezL{g2qadOu$N>CeU+Z~Qpk_|XsJa4?P?z*K^d zhK+?pU1%%&LSgS`yvWm&seR5{;3roiOWN%*Z!!ho-sSax<)_Ih6$p9mMQj1_sp)8*B3YFJ7Cj{z z$PMWS3L@zE_} zsBv)pI1X=IkHy7htT=c;=!1^}$O@6W2QV}wQ7pm2*)pmX)(HJzR)(s<;kDy9M~zRB z3;+Ni07*naRGlM9)qFOz;sL2HyWKclU&brQ1AXV4vfb)0o48Tc@ff85GFy78z$4&T zrWvZrt0$bD$wts>TTgcJ@-Kcq?tJafH@NpgVTyR#b6TG zhKEr#+gL6L2>u@S5@UT1bozkOlAFaOp`vG3gN7H)Z_L3(Gm0~ zpZHB>SS!I8AR?1M)O+_1jw)?JY@^(W(QasEO-=e z>v^=DjX|NLoIp-Sr&^m#D%DpCqiFPKx@#o2B>*g;V!Rkfghj&uXm%N^i$U&;jV#dq zcG$~{I3ztLzYZhgfrP|RLv&BTv;qPU{Q-I=%1%U|ps|H=CzBQ677&u5hCtTlH0)_Z z3%`TmDeQbEEV*#Vu1I<1{id=GjG#yV@^~ZjOWB$-$WX?|_?~+z1q0w8yl1)wozIn0 z7aKNpu|$x(LW3J};GU%Pv6)Z`Vc)3l#A`w4;XaA-B6bzB&6NZiDNWb}(mdiikd1~L zI0EvpmJzRje$b2H#!0mz<%1YVpwACX1J`c^4Dsq4jV-N9Uqrzt$?jx+oo$RKqqzOc zzZ`G>-tWZaq;nhO@k3esL+cjQOFYIY7BOWwT)sf4%A?sY(`3N zjyxTp>FF7p5QYQb+HP#owifZ^(p# z>rk1uS;c|sf0q{G#-W7CB#H>1y4y~;%_^@kIMlR)1lCS9WD!oeuOJg@kW-Q!y3weN z1VvLKez%U(^O=401gLW_NMiN`EM!`^`I5HCfzImt$Lt1hoJMs&!Y!Ujg6#^`In+Xa4PX2 zMA!`BQDhteJtM;0sVWSOtSd81m9AP4SGM03$_7L*ZiVD!QEjYa2}TYC`wok86dv%% zL-##RV|TlF_I=OC>3a_a0G5xQh~*qWIP(~)B3&V2+Lq>^S;k6}HhM7Fv@e=PT*C1i z4`aF-Mu(mXrBj*AYE^jxNe@Q6v~*1_P1gd_^+q&5(>yXa#@&;tqF$#8Wd>kz9GTb+ zyYc?dd?sH1#$Uwc!nRvoCepJ_#L|+;$dRaAMe%~*$6C--73>b<|A_T!E$cZ9WNFr` z45A!jh_Ktl$@P=?$WOf--}=U1#e+xhMSn1gPG!crifo#-BmSS{deQ}b*DA=Ng>`gztowd8zw|+zL8COuHRaR+4zZg<3zB^m z=jV^L7DpqC14>;)&mzS(Y17bil}l5$8H$9sm?W+~d_ZvE+X+Z=M`?^&`c0H}DwTC0 z5NO>SoNtX|`NmuD zkO9(BQkizsB&TT`C-qR!0^%@Cew|HhbnO68pBb=H2<{wETh{9U0s!$5iIwH#WRk3g zi$Y03XzGHJEo4iO&h$+R$-4vT()v!TolC8w!$Y5P>vdc_I#a4GZF^z-?BpvQEym3{ zy>K3;e9}lxB`SRhWgS%&v)MdQb}V9ox)k2uf?iknGZqfQe@Sg@7w{;-o;aOn@d$}d zW`!$Gruy7NTip+Y3da=n=0|@j9)9OlHPyGEu)Cgil9hmp*CdtGn4XsUbqMNRDP)rE z@x0hXZ2Ffz{i%5P#@(3RyRZJ7(a{D`?qMSdt5?a9QYrkt^Z`wgvgH#CPfE*4rC}rU zyeNG+xXQSj17K5nCqg2Cl^euhJdD!^??rb@+3(l$#z@>y21l3>yuT)!OAa^^PM3dd z6b+hWP%+`6Pg=}> zjV<(N0P`p4Y^RQAKKPOjwm1IbuQZeZRw8T}6^`gr#i#;x3bg;nI%HsOA6>s5r+44B zpJu{4==+o}djit;bZQ}@dMa|1-mND3>04l`Nr=N(MLdQgkr*(71`=&rsGO8+hSWYSO>6YZm9uATm(;5TY!x z59nJB4Y|-s19c_&mWEw8xQnS)yy~=pEw}6;gzr1Afust-Lh>>NR>I%_)PHp1>F1t{ zhwr|dr!6?R%-ra_OYGE6IPzUogO)7JS|7sC(Q6xzVzyjLKmPWIJ`@+Hk0q;I_~Kq{ zCK!8I;AZfCV@hV-KvLg*X9`7CL(hiIm?f?xgh}x=RDepe6*+z|8N~%tDI}Dcu1zO# zHou77Y$f?z#^}lpbRAo>0h08rO)R-+bgMX=O!AhjQIa5a47htf8W;v7glQ2{QA3E~Fi$b$WjlJw& z@iRxOw1nVvh3raGn(5wl6*Yoi)V_6*xQIAbj zq<{fyW^W{?pCXEa8xrb2X$$Z=n%rb((XM9RWw{$kKd zc9y_QZEPlEBA~D1ay8e%hgQLW!)q}=n~B2mzW2Q^?%cT(%M}tk=0kEV4xsDw zvh@3Eb7iTbCN5?Z%}Q7)na*MpcKTSPVMGU2#wy{@j8umgVA?eQK$~hfkh;X6az!Gm zFa1g@D_}nb)0pKj@2x=xf{qNa@;-`wqI+UUjry;7-8?fge}XD%ALzJ0dh0EfeQ0V? zM3X_YHuTw&SPUD%TRltg>}M%xC03C80JpM0g$y<&2`nCpZUqPuAw>xkK&-;6>f;HS zrvj_F0)0AjtB2`#m{>=}ltRx@7bryo5{oEy1$&hWA%mxo_h0$=7nBKF64??E&3dU1 zDaZmBh$R<#QYh08Lyl>Ml@iv*v>%r$NC+CdswQZb9su;nfRw%xn3E^z zMeH5L1Bc>qdowuIU0?i4Llz;`x%^wYU5svNwN4h<`kIC#OD>Igc*JnLu|g58-3AC=ASqLs4V2(XFi>B%-;cAiQwif4FkMUAU5N5@Qj)%VLTCncN9v#MHCeH>N8V4m zEmncoqYl;lOkzG{lUfiHO(qRzDM3nmE=wl<&gEfx7xYbxR(8ygg6m<>!*o{=7TPn} zoKCY=wH*?=BivrDW4&);8V$6~2|IySR3xOe2{1sJR_b|{>+l-cD7)qN+`m%p^IQcn z$-}Ai!PGCM;UlJo?c^Mcb6+KSUI~Kwi#bfuecY+d!xslo_2rNLn!*jXRT83W5%-6o zoe^H;lY2d!{8Dz{oMAC0^L&*qCBg}5`sZ{Xgfo*3aSP>i>{$wBk#%n92N>+gMWBAD zrY4gh^~7BZyQgIfr6G3J>Q6XWv2hEvhz+EhCcnR~8twES_`7SzCvkakW-mD22Z~Iu zpTzEt`b{zn7?0xiCq5YufB1T=AHC;DoI2d9`O#%Z9hAuw8(13V<|nFL0C&?L$xWwO z8jiprS4gP7@u^ewM4&DGAU2Qs0HB@Iv4{musX)`&_sW5+c;AVi#bl4(OK+gw6`OfJ zTc}sX^W&6PnIi4-M&+^&p0-ZsUvuPm+Cmi*8^AtcKmvS9Kus?wW8z%47%pMaztUJQ z>;{34i+!B*z?^}V5%vDWE+s`aY(0c9!G`Y#xJY0lYlY{w1sE({tL!^*u)^18^YD4I zb}jeGn*64yWU{x{8)rJF-V^W@Nzp(R8K@}^5`GWf1&OV`l+{59UQ&uC@Ndo0qzn_l zIQx6LSE<+TOxj-#7)w+Pq0B6FU z6!bduySGkW)JE~4#~0@c3BY=dYGC$P^B$Ef(`^&0USu+%%fy;BA=V z@P+;pJUl>gFZ}JFi~E1`-B`Tyju(>pWbWf_7EPR@Oa?N1GKz`3-j=amEZm6av!eS% zR{)zYGFN3M$(kqo%YaoDrfEG+uS==7T-e5)FBXj@eI59W7rW{k6ss_!qzw*bq0kRd zU;~I(F?15F89PGxyrpKC}s3UbU@_2 zDnkGT4+WbdcvpbWF8wtr6F1f+8Px~4J(phqHxY@|kvX!i} z1S&tEE3beu#3gD!-z~$z)KZ)NScOl@MsU`wV^Uw)?*PZATuhAka8%QF|)H;FpdAL zg0vsBr)<=$Qu~P}ez^~Z@IdDxkk(Os_2a+R0v79`-JGWYavpjhE_`(Y>mkx35hB&& zY@u7LPCOh~`h|ZXU6#HS06-oxBYKJWRZ<}2>+yt(%+Wpy5SvX}T=%4J&D+Y6B-7TT zU9E<_9y%hG_Ga%H4xF#1k_T(NWw?`5{R;Cua#O^U~gDJr8lF^`J`z9zHTZ-D8YR&Fij=XV$&0d1*q_x zJl04Wvv5=UNM#UevpL0++5_^z>rE_BOO2kUbCiK17-vsS88vVTjitSTb?O#3Z|Qft zhmlCp}jKniw>2=MfQGc+?iq;3& zlvIH{)ZmL?tHVMSND55mdQyu-#aU%)j;3ysgE7Os8uxv;=PAnq)Q2WBdBV?5QIIC_ zs8;sF!+>U^>6;V_EuBm-p`6e-pKJ!aPXa0Bnw(P18ib0FOE7ceP?U|CRRuV~K+xX) zxzEOp8#m(XU-})p+(LgLVDXR{Gj{;m4MaBG(oB>D#voc{D!###Q0|v>WY;y&%@Vn) zd_s+_J!muvU%f0eqsHb^3gpRZI`0ejv)fpduNEu=Af*GY`Bht;z{)I9 zIFPdtxS(xFph+ls_r6MYR5qY2)3aB?H6XL>Xii$~=CTG7=wr$A(T5DL!}|&PLAEIX zt9uaqj24=-%6EH3O-Rn&O(%{ure`Xc@Vr?^NV}*fQ=G(O8`s}|TO7@|U;Dn+ zhYU(M471K=YD>V7D#Bu^^*4~3>y@6E;WK`u%6f_WLnMLL@$|=DiF^O|FJpRmpuq>3 zA9yqE8!$@=q2Rz$L6cltWBL4ijQukq-h0?a7$xYi=1L$c62-n6XGB`d1qhzyUpcHq z-AUEZGU+7if=MI9y+k{0Kd(a?b_;E!WGa*ljHx3d-v9{OAgc=WRp z;-I%rNm>N$kf4~Mr&M?1L%;B|(%SjUKmD3B1EM_NUmQxhb9 z^%&R$={Go(ucAS53bZ+CQZrJzNO>~scG7P@-7bP~Rrgs*d1_01%^LKhY80Cl&BgGd zZnO6Q_9r0<+*kTcqCY4YR?(u>!~eh|Ag+biWg`@ls|*NI5U>O|UP#*A5^(f7;o)k?k=#-REHYo3#CiQ?Ktbv3tOn(a zwE`-j%W+QPEU4vds@v&XgDhcE(6b~+D9P{4+7biDL8ekR6+mSAnnt_ae~_EV!t~iY zcp+Y1#I>U%n|!b?*)tg|DaSXG@7d`&lu4lWP-=Q9*obE1(QQrK7(APMLmMWFi?L>>J>c< zVtX>oJ=`9}ZqB$pO;Bkebn9p+(}i-r>Umt1Xdb-y2=}Hj1frw&X5*kZTS^b%!4d>) zDI61nb->7dhZ7nvN0+i>)&!${Mv)`Dvu-YkKDHbze8 zoJBY}C{s&kOj&M99g^TiuM^g2qX8Jf7HUz+z|q#?_Y4x$o=Gfac?Mz`q{ftGiX7|& zHIae?KktC0lDYK6l0{Mis;8?!V-gCLoWjy~D~C_{eEdDn(cjO0l3=>0ivr1lr!?e< zy-8*@7`Z@;|n~6(DKKI09{Dl~lIUl4J!MS);KRJ9Vr?_E=0$n_T#LxW8e;mu_u1EF8gSh+Ke;Dl#A4GRLkr5;g9_zDu5{TdByk!(fES_xi$L4FS;&$cj1}nIvB`l1sxEtZ<_2SHPNbE=1>#&Qzb7nzZuw6R?2vq1c&`78WX(zkejHOH4t zY*CO9Eal4lR%JtaT&lm4eGqtY(hE=Cs++~$tfr7yMS%cJX#s=;X(HTUe1bcWD=U!A z7THPS>GIDC7EHtVzPjh0se+Q*Ac6p}76mEsK#9$*Ni6#yGXOWAO_bMxDm6_|`6+kY z!$@QxHC8~L=!EIuV{Homm)2H5Si}#_QGaG|Y*tZc3f54xhALX*Wtf!DE@C=8h;{|c z2mo9+<~(0)?0G(bOPLn0jFSS~SuEUGFqEI50F)giq(MDKgb(=M9H>dmQ~hI%GPbAM9E8#$g95l(DdsTHQoADi z*yZ_FfBnj@x6o%u%Q)Q$FpFvz$H&J?XQ5~mH(M5vq)TNFtStIV(>E01)B5+6&h#r!%LB{)rAlSmrmT$sP!4st)2|*meN)G74SIshr%%Y6=wMoeo zck5^X5GNmXfQ~tW98jmCC!aksE3rjXQbJs}BeD#BGjJ=bqKPkScqF;|Dmy{Aue%Gw z4zd+9NlBFDpo$p>$F7Z|wi~kv0Y2jJ(mM6J$Eqw!7iy=WkMCQS&VpaiXLf0+Oh~1T zRZOH{s~#I~?!IHQy4>&if=BZj^m}=|8d+b;Lw-(v5eXiZrI*Yxd@p$460iWqmJE)c z+Xt+#edJeKTXnVK{gFkmQK1TuWt`7vN-^ngJjvbTO^eho!t^EmlciH}u>~2Q;4-Dx z>FR-)nQH3{SnB>KSy>fC;i)Mi*9wS*8z$|p^+vx&15nr|Ta(WFsN6;W#c|yFr~e?j zFTWI<@4X%K-}=MYy?QS;gMK{RtfCqB;)G4OT1IcwkI|wrHjIFUaV9+mri;?)Vaiu2 zt=BJ2L0sz2wo7YkkuADzYc$VMS(na{zurPOahXzNMG=v~s^5OzzWZ-xK>2QNuP zK~zhWQ}^k5!-4d*SjKA3%7MEhhJpG|2)ofM5@o~uJsq0KoG3w$p3}b3pQfQ)*9uoA zUXz8ADy?D^<^fAUR=cD01yfN3r|GLh0p$?)Y}sV$-KGGZj8iCQm@vz90wQrTF_>ug zhw`xG?G+Gk=+V^8WmlQ5Iy-iGgzh{Mj^IzwC3wwuw!K%d0-)3z$uGg4NdX* z$Uf0~uVOc|{4UeNZS9h9t3Heui24YUFQF`evsuM(I*IYK&&IuPf7_4_;3T2}vFMy7 zh&x8fBZCN^=w$(_{77&q-e!`fqxZoyC`(~yY>Dks0LoFeAHRJA*pwZH6WM2B0!miE zwPp#zy%#Z>!vS*)Xjp?QcbRfs`^+^7@03a+9Tb#LXiCZBFa_@zAXi0!LcTld|EkA> z_P;oTR+c3=vvvh&O^OeLkKwSd17WpZSuI1QnZdr2#nyW{oAp5VMpfSSO-gr z3JDF`A54B>uE=_3SI7H&%!BY0`2m4s8>~N-38QZ zV~G~kkPnbTd07f!I;ju311FV=p=z3Xa;bk)^To~R$Mq`S+skb5` z*eK1n1n{i8i%0!k{KU+Z3ia41d3f$g`6)&kXlD@^$!f+M_vpw5R&4ZMB`Fl9M4~m2 zFYuYy_Fe1r;QOt#u}WiEG95{*mTl7$lgJXJNXwmBIm;?2X3*z8?O61~h?@SNVI3ta zDCJ3^N+?AacMSV*u8?4X#4_QEpWj9oe>k!Y98ffYvm7t-EGIXvNAKt)?tlNaIGi3@ zQ4{Y;^qKaH(buU4YW6w|hPuimw8>DY%tbCAW;FH-s+_-XJnbrATHb>|qH>fBgr@^{ zN_K9PAZ|7rz(_C2eJ#F01BVXYja`*xWt3msptR`{&O-l6Yy|~(-}f?_=g*#mrCW26 z1L8SvQIJKI#O4xZipKSA=n+cA!dZi-a9OH7F=^vDTUgJpVQV%A4?#MF8mBy?msiIO zo%hW)hTDh{pq zBdB(KauVa=FlKk&m9vE!kYpWT41|_7L6qe#hCQ0q18mw;oG+W6DhqOqT}Ikv3Q|fx z>5r327RMf0f^}nha1hO=iHpmbQ&Ol71Q`XdL;)uNm6TFF1`Rneq(E4%j>Sb2x5kH3 zxRfUvrQhL**-EtYWgP9gvF^6f``~l&sMo~7tB>`0Nb6Pa=QL50&;$#(f-L6r1sqp> z8$&<`QJ`(2e>jSZ1_G`#P;BIDdRlDwo;S8e`dW~_U*;!X>2#N~4Yfy$@R1G^%-p-; z^e*M07LDjBHl{2+7)~*lEXfFGwN#P_(*8_YD^TVg|L6VfM2gG&x71ydp4X^w*@ z8d;(_$+>0Yl|SzRu)Us;|6<^$B(A2!!VCGcE2B2}NhL8BQNkmMhz%KfEdZ_##>91* zUIXWqsVh!6Y%-T&p6nUmYcpJL3gdg+(A{=6nccd7C^c6ZCiu^4SELA~8pIt>&}=RX ze$3?>G?H?Eg?l6zZQ2-A*vb>E`P>ACjrw)SqL4KSZlOVo#W2Ng$Ucv`b8f2?rh^VD zHOcs_Tj`){&49MwRdL!UaJO-5(~})M!RnpCCZ1kc&15!Q$*D*CPbHlL?EodvEW7Dp zDJV;pnW2RT0Q~cwg3PC literal 0 HcmV?d00001 diff --git a/hikari/api/bot.py b/hikari/api/bot.py index bdb4b432d4..bc5da5e012 100644 --- a/hikari/api/bot.py +++ b/hikari/api/bot.py @@ -27,9 +27,6 @@ from hikari.api import event_consumer from hikari.api import event_dispatcher -if typing.TYPE_CHECKING: - pass - class IBotApp(event_consumer.IEventConsumerApp, event_dispatcher.IEventDispatcherApp, abc.ABC): """Base for bot applications. diff --git a/hikari/api/entity_factory.py b/hikari/api/entity_factory.py index 8656ff1689..464d522e52 100644 --- a/hikari/api/entity_factory.py +++ b/hikari/api/entity_factory.py @@ -1367,9 +1367,9 @@ def serialize_gateway_voice_state_update( Parameters ---------- - guild : hikari.models.guilds.Guild or hikari.utilities.snowflake.Snowflake or str or int + guild : hikari.models.guilds.Guild or hikari.utilities.snowflake.UniqueObject The guild to update the voice state in. - channel : hikari.models.channels.GuildVoiceChannel or hikari.utilities.snowflake.Snowflake or str or int or None + channel : hikari.models.channels.GuildVoiceChannel or hikari.utilities.snowflake.UniqueObject or None The voice channel to change to, or `None` if attempting to leave a voice channel and disconnect entirely. self_mute : bool diff --git a/hikari/api/event_dispatcher.py b/hikari/api/event_dispatcher.py index 79ed73b443..fe9ce038ad 100644 --- a/hikari/api/event_dispatcher.py +++ b/hikari/api/event_dispatcher.py @@ -174,6 +174,7 @@ async def on_message(event): `hikari.api.event_dispatcher.IEventDispatcherBase.wait_for` """ + @abc.abstractmethod def get_listeners( self, event_type: typing.Type[EventT], ) -> typing.Optional[typing.Collection[typing.Callable[[EventT], typing.Coroutine[None, typing.Any, None]]]]: @@ -191,12 +192,13 @@ def get_listeners( none are registered. """ + @abc.abstractmethod def has_listener( self, event_type: typing.Type[EventT], callback: typing.Callable[[EventT], typing.Coroutine[None, typing.Any, None]], ) -> bool: - """Returns `True` if the callback is subscribed for the given event.""" + """Check whether the callback is subscribed to the given event.""" @abc.abstractmethod def unsubscribe( diff --git a/hikari/impl/bot.py b/hikari/impl/bot.py index 332906b31e..2ffea59166 100644 --- a/hikari/impl/bot.py +++ b/hikari/impl/bot.py @@ -115,12 +115,12 @@ class BotAppImpl(gateway_zookeeper.AbstractGatewayZookeeper, bot.IBotApp): are supported. This defaults to v6. shard_ids : typing.Set[int] or None A set of every shard ID that should be created and started on startup. - If left None along with `shard_count`, then auto-sharding is used + If left to `None` along with `shard_count`, then auto-sharding is used instead, which is the default. shard_count : int or None - The number of shards in the entire application. If left None along - with `shard_ids`, then auto-sharding is used instead, which is the - default. + The number of shards in the entire application. If left to `None` + along with `shard_ids`, then auto-sharding is used instead, which is + the default. stateless : bool If `True`, the bot will not implement a cache, and will be considered stateless. If `False`, then a cache will be used (this is the default). diff --git a/hikari/impl/entity_factory.py b/hikari/impl/entity_factory.py index dc5e3dfa60..41a81a1556 100644 --- a/hikari/impl/entity_factory.py +++ b/hikari/impl/entity_factory.py @@ -206,7 +206,7 @@ def deserialize_application(self, payload: data_binding.JSONObject) -> applicati members[team_member.user.id] = team_member team.members = members - team.owner_user_id = snowflake.Snowflake(team_payload["owner_user_id"]) + team.owner_id = snowflake.Snowflake(team_payload["owner_user_id"]) application.team = team else: application.team = None diff --git a/hikari/impl/gateway_zookeeper.py b/hikari/impl/gateway_zookeeper.py index 65be39a623..8de180a028 100644 --- a/hikari/impl/gateway_zookeeper.py +++ b/hikari/impl/gateway_zookeeper.py @@ -92,12 +92,12 @@ class AbstractGatewayZookeeper(gateway_zookeeper.IGatewayZookeeperApp, abc.ABC): Proxy-related configuration. shard_ids : typing.Set[int] or None A set of every shard ID that should be created and started on startup. - If left None along with `shard_count`, then auto-sharding is used + If left to `None` along with `shard_count`, then auto-sharding is used instead, which is the default. shard_count : int or None - The number of shards in the entire application. If left None along - with `shard_ids`, then auto-sharding is used instead, which is the - default. + The number of shards in the entire application. If left to `None` + along with `shard_ids`, then auto-sharding is used instead, which is + the default. token : str The bot token to use. This should not start with a prefix such as `Bot `, but instead only contain the token itself. @@ -202,7 +202,7 @@ def on_interrupt() -> None: else: # The user won't care where this gets raised from, unless we are # debugging. It just causes a lot of confusing spam. - raise ex.with_traceback(None) # noqa: R100 raise in except handler without fromflake8 + raise ex.with_traceback(None) # noqa: R100 raise in except handler without from finally: self._map_signal_handlers(loop.remove_signal_handler) _LOGGER.info("client has shut down") diff --git a/hikari/impl/rest.py b/hikari/impl/rest.py index f545d8dd93..3cff4ebd63 100644 --- a/hikari/impl/rest.py +++ b/hikari/impl/rest.py @@ -25,7 +25,6 @@ __all__: typing.Final[typing.Sequence[str]] = ["RESTClientFactoryImpl", "RESTClientImpl"] -import concurrent.futures import typing import aiohttp @@ -39,6 +38,7 @@ from hikari.net import strings if typing.TYPE_CHECKING: + import concurrent.futures import types from hikari.api import cache as cache_ diff --git a/hikari/models/applications.py b/hikari/models/applications.py index 68d00c0ae7..44c3947d76 100644 --- a/hikari/models/applications.py +++ b/hikari/models/applications.py @@ -309,7 +309,7 @@ class Team(snowflake.Unique): member object. """ - owner_user_id: snowflake.Snowflake = attr.ib(eq=False, hash=False, repr=True) + owner_id: snowflake.Snowflake = attr.ib(eq=False, hash=False, repr=True) """The ID of this team's owner.""" def __str__(self) -> str: diff --git a/hikari/models/channels.py b/hikari/models/channels.py index 2e4e23ab7b..3f2e64b01a 100644 --- a/hikari/models/channels.py +++ b/hikari/models/channels.py @@ -230,11 +230,11 @@ async def send( mentions_everyone : bool Whether `@everyone` and `@here` mentions should be resolved by discord and lead to actual pings, defaults to `False`. - user_mentions : typing.Collection[hikari.models.users.User or hikari.utilities.snowflake.Snowflake or int or str] or bool + user_mentions : typing.Collection[hikari.models.users.User or hikari.utilities.snowflake.UniqueObject] or bool Either an array of user objects/IDs to allow mentions for, `True` to allow all user mentions or `False` to block all user mentions from resolving, defaults to `True`. - role_mentions: typing.Collection[hikari.models.guilds.Role or hikari.utilities.snowflake.Snowflake or int or str] or bool + role_mentions: typing.Collection[hikari.models.guilds.Role or hikari.utilities.snowflake.UniqueObject] or bool Either an array of guild role objects/IDs to allow mentions for, `True` to allow all role mentions or `False` to block all role mentions from resolving, defaults to `True`. @@ -304,7 +304,7 @@ def history( Returns ------- - hikari.net.iterators.LazyIterator[hikari.models.messages.Message] + hikari.utilities.iterators.LazyIterator[hikari.models.messages.Message] A lazy async iterator across the messages. """ # noqa: E501 - Line too long return self.app.rest.fetch_messages(self.id, before=before, after=after, around=around) diff --git a/hikari/models/messages.py b/hikari/models/messages.py index 97e7a1971c..fd503278be 100644 --- a/hikari/models/messages.py +++ b/hikari/models/messages.py @@ -382,11 +382,11 @@ async def edit( mentions_everyone : bool Whether `@everyone` and `@here` mentions should be resolved by discord and lead to actual pings, defaults to `False`. - user_mentions : typing.Collection[hikari.models.users.User or hikari.utilities.snowflake.Snowflake or int or str] or bool + user_mentions : typing.Collection[hikari.models.users.User or hikari.utilities.snowflake.UniqueObject] or bool Either an array of user objects/IDs to allow mentions for, `True` to allow all user mentions or `False` to block all user mentions from resolving, defaults to `True`. - role_mentions: typing.Collection[hikari.models.guilds.Role or hikari.utilities.snowflake.Snowflake or int or str] or bool + role_mentions: typing.Collection[hikari.models.guilds.Role or hikari.utilities.snowflake.UniqueObject] or bool Either an array of guild role objects/IDs to allow mentions for, `True` to allow all role mentions or `False` to block all role mentions from resolving, defaults to `True`. @@ -415,7 +415,7 @@ async def edit( ValueError If more than 100 unique objects/entities are passed for `role_mentions` or `user_mentions`. - """ # noqa: E501 - Line too long + """ return await self.app.rest.edit_message( message=self.id, channel=self.channel_id, @@ -470,11 +470,11 @@ async def reply( mentions_everyone : bool Whether `@everyone` and `@here` mentions should be resolved by discord and lead to actual pings, defaults to `False`. - user_mentions : typing.Collection[hikari.models.users.User or hikari.utilities.snowflake.Snowflake or int or str] or bool + user_mentions : typing.Collection[hikari.models.users.User or hikari.utilities.snowflake.UniqueObject] or bool Either an array of user objects/IDs to allow mentions for, `True` to allow all user mentions or `False` to block all user mentions from resolving, defaults to `True`. - role_mentions: typing.Collection[hikari.models.guilds.Role or hikari.utilities.snowflake.Snowflake or int or str] or bool + role_mentions: typing.Collection[hikari.models.guilds.Role or hikari.utilities.snowflake.UniqueObject] or bool Either an array of guild role objects/IDs to allow mentions for, `True` to allow all role mentions or `False` to block all role mentions from resolving, defaults to `True`. diff --git a/hikari/models/webhooks.py b/hikari/models/webhooks.py index 70aa282bee..8480026e15 100644 --- a/hikari/models/webhooks.py +++ b/hikari/models/webhooks.py @@ -152,11 +152,11 @@ async def execute( mentions_everyone : bool Whether `@everyone` and `@here` mentions should be resolved by discord and lead to actual pings, defaults to `True`. - user_mentions : typing.Collection[hikari.models.users.User or hikari.utilities.snowflake.Snowflake or int or str] or bool + user_mentions : typing.Collection[hikari.models.users.User or hikari.utilities.snowflake.UniqueObject] or bool Either an array of user objects/IDs to allow mentions for, `True` to allow all user mentions or `False` to block all user mentions from resolving, defaults to `True`. - role_mentions: typing.Collection[hikari.models.guilds.Role or hikari.utilities.snowflake.Snowflake or int or str] or bool + role_mentions: typing.Collection[hikari.models.guilds.Role or hikari.utilities.snowflake.UniqueObject] or bool Either an array of guild role objects/IDs to allow mentions for, `True` to allow all role mentions or `False` to block all role mentions from resolving, defaults to `True`. @@ -252,7 +252,7 @@ async def edit( avatar : hikari.utilities.files.Resource or None or hikari.utilities.undefined.UndefinedType If specified, the new avatar image. If `None`, then it is removed. If not specified, nothing is changed. - channel : hikari.models.channels.GuildChannel or hikari.utilities.snowflake.Snowflake or str or int or hikari.utilities.undefined.UndefinedType + channel : hikari.models.channels.GuildChannel or hikari.utilities.snowflake.UniqueObject or hikari.utilities.undefined.UndefinedType If specified, the object or ID of the new channel the given webhook should be moved to. reason : str or hikari.utilities.undefined.UndefinedType diff --git a/hikari/net/config.py b/hikari/net/config.py index 8d0c5c48e0..97814e30cf 100644 --- a/hikari/net/config.py +++ b/hikari/net/config.py @@ -19,7 +19,12 @@ from __future__ import annotations -__all__: typing.Final[typing.Sequence[str]] = ["BasicAuthHeader", "ProxySettings", "HTTPSettings"] +__all__: typing.Final[typing.Sequence[str]] = [ + "BasicAuthHeader", + "ProxySettings", + "HTTPTimeoutSettings", + "HTTPSettings", +] import base64 import typing @@ -55,6 +60,8 @@ def __str__(self) -> str: @attr.s(slots=True, kw_only=True, auto_attribs=True) class ProxySettings: + """The proxy settings to use.""" + auth: typing.Optional[typing.Any] = None """An object that when cast to a string, yields the proxy auth header.""" @@ -76,7 +83,7 @@ class ProxySettings: @property def all_headers(self) -> typing.Optional[data_binding.Headers]: - """Returns all proxy headers.""" + """Get all proxy headers.""" if self.headers is None: if self.auth is None: return None @@ -109,6 +116,8 @@ class HTTPTimeoutSettings: @attr.s(slots=True, kw_only=True, auto_attribs=True) class HTTPSettings: + """Settings to control the HTTP client.""" + allow_redirects: bool = False """If `True`, allow following redirects from `3xx` HTTP responses. diff --git a/hikari/net/gateway.py b/hikari/net/gateway.py index f6f1171884..34e7d1cc2e 100644 --- a/hikari/net/gateway.py +++ b/hikari/net/gateway.py @@ -496,9 +496,9 @@ async def update_voice_state( Parameters ---------- - guild : hikari.models.guilds.PartialGuild or hikari.utilities.snowflake.Snowflake or int or str + guild : hikari.models.guilds.PartialGuild or hikari.utilities.snowflake.UniqueObject The guild or guild ID to update the voice state for. - channel : hikari.models.channels.GuildVoiceChannel or hikari.utilities.snowflake.Snowflake or int or str or None + channel : hikari.models.channels.GuildVoiceChannel or hikari.utilities.snowflake.UniqueObject or None The channel or channel ID to update the voice state for. If `None` then the bot will leave the voice channel that it is in for the given guild. @@ -716,7 +716,7 @@ async def _send_json(self, payload: data_binding.JSONObject) -> None: def _dispatch(self, event_name: str, event: data_binding.JSONObject) -> asyncio.Task[None]: return asyncio.create_task( self._app.event_consumer.consume_raw_event(self, event_name, event), - name=f"gateway shard {self._shard_id} dispatch {event}", + name=f"gateway shard {self._shard_id} dispatch {event_name}", ) @staticmethod diff --git a/hikari/net/rest.py b/hikari/net/rest.py index adce45f535..87730f6200 100644 --- a/hikari/net/rest.py +++ b/hikari/net/rest.py @@ -441,7 +441,7 @@ async def fetch_channel( Parameters ---------- - channel : hikari.models.channels.PartialChannel or hikari.utilities.snowflake.Snowflake or int or str + channel : hikari.models.channels.PartialChannel or hikari.utilities.snowflake.UniqueObject The channel to fetch. This may be a channel object, or the ID of an existing channel. @@ -493,7 +493,7 @@ async def edit_channel( Parameters ---------- - channel : hikari.models.channels.PartialChannel or hikari.utilities.snowflake.Snowflake or int or str + channel : hikari.models.channels.PartialChannel or hikari.utilities.snowflake.UniqueObject The channel to edit. This may be a channel object, or the ID of an existing channel. name : hikari.utilities.undefined.UndefinedType or str @@ -512,7 +512,7 @@ async def edit_channel( If provided, the new rate limit per user in the channel. permission_overwrites : hikari.utilities.undefined.UndefinedType or typing.Sequence[hikari.models.channels.PermissionOverwrite] If provided, the new permission overwrites for the channel. - parent_category : hikari.utilities.undefined.UndefinedType or hikari.models.channels.GuildCategory or hikari.utilities.snowflake.Snowflake or int or str + parent_category : hikari.utilities.undefined.UndefinedType or hikari.models.channels.GuildCategory or hikari.utilities.snowflake.UniqueObject If provided, the new guild category for the channel. This may be a category object, or the ID of an existing category. reason : hikari.utilities.undefined.UndefinedType or str @@ -561,7 +561,7 @@ async def delete_channel(self, channel: typing.Union[channels.PartialChannel, sn Parameters ---------- - channel : hikari.models.channels.PartialChannel or hikari.utilities.snowflake.Snowflake or int or str + channel : hikari.models.channels.PartialChannel or hikari.utilities.snowflake.UniqueObject The channel to delete. This may be a channel object, or the ID of an existing channel. @@ -622,10 +622,10 @@ async def edit_permission_overwrites( Parameters ---------- - channel : hikari.models.channels.PartialChannel or hikari.utilities.snowflake.Snowflake or int or str + channel : hikari.models.channels.PartialChannel or hikari.utilities.snowflake.UniqueObject The channel to edit a permission overwrite in. This may be a channel object, or the ID of an existing channel. - target : hikari.models.users.User or hikari.models.guilds.Role or hikari.models.channels.PermissionOverwrite or hikari.utilities.snowflake.Snowflake or int or str + target : hikari.models.users.User or hikari.models.guilds.Role or hikari.models.channels.PermissionOverwrite or hikari.utilities.snowflake.UniqueObject The channel overwrite to edit. This may be a overwrite object, or the ID of an existing channel. target_type : hikari.utilities.undefined.UndefinedType or hikari.models.channels.PermissionOverwriteType or str @@ -684,10 +684,10 @@ async def delete_permission_overwrite( Parameters ---------- - channel : hikari.models.channels.PartialChannel or hikari.utilities.snowflake.Snowflake or int or str + channel : hikari.models.channels.PartialChannel or hikari.utilities.snowflake.UniqueObject The channel to delete a permission overwrite in. This may be a channel object, or the ID of an existing channel. - target : hikari.models.users.User or hikari.models.guilds.Role or hikari.models.channels.PermissionOverwrite or hikari.utilities.snowflake.Snowflake or int or str + target : hikari.models.users.User or hikari.models.guilds.Role or hikari.models.channels.PermissionOverwrite or hikari.utilities.snowflake.UniqueObject The channel overwrite to delete. Raises @@ -711,7 +711,7 @@ async def fetch_channel_invites( Parameters ---------- - channel : hikari.models.channels.PartialChannel or hikari.utilities.snowflake.Snowflake or int or str + channel : hikari.models.channels.PartialChannel or hikari.utilities.snowflake.UniqueObject The channel to fetch the invites from. This may be a channel object, or the ID of an existing channel. @@ -752,7 +752,7 @@ async def create_invite( Parameters ---------- - channel : hikari.models.channels.PartialChannel or hikari.utilities.snowflake.Snowflake or int or str + channel : hikari.models.channels.PartialChannel or hikari.utilities.snowflake.UniqueObject The channel to create a invite for. This may be a channel object, or the ID of an existing channel. max_age : hikari.utilities.undefined.UndefinedType or datetime.timedelta or float or int @@ -763,7 +763,7 @@ async def create_invite( If provided, whether the invite only grants temporary membership. unique : hikari.utilities.undefined.UndefinedType or bool If provided, wheter the invite should be unique. - target_user : hikari.utilities.undefined.UndefinedType or hikari.models.users.User or hikari.utilities.snowflake.Snowflake or int or str + target_user : hikari.utilities.undefined.UndefinedType or hikari.models.users.User or hikari.utilities.snowflake.UniqueObject If provided, the target user id for this invite. This may be a user object, or the ID of an existing user. target_user_type : hikari.utilities.undefined.UndefinedType or hikari.models.invites.TargetUserType or int @@ -809,7 +809,7 @@ def trigger_typing( Parameters ---------- - channel : hikari.models.channels.PartialChannel or hikari.utilities.snowflake.Snowflake or int or str + channel : hikari.models.channels.PartialChannel or hikari.utilities.snowflake.UniqueObject The channel to trigger typing in. This may be a channel object, or the ID of an existing channel. @@ -844,7 +844,7 @@ async def fetch_pins( Parameters ---------- - channel : hikari.models.channels.PartialChannel or hikari.utilities.snowflake.Snowflake or int or str + channel : hikari.models.channels.PartialChannel or hikari.utilities.snowflake.UniqueObject The channel to fetch pins from. This may be a channel object, or the ID of an existing channel. @@ -879,10 +879,10 @@ async def pin_message( Parameters ---------- - channel : hikari.models.channels.PartialChannel or hikari.utilities.snowflake.Snowflake or int or str + channel : hikari.models.channels.PartialChannel or hikari.utilities.snowflake.UniqueObject The channel to pin a message in. This may be a channel object, or the ID of an existing channel. - message : hikari.models.messages.Message or hikari.utilities.snowflake.Snowflake or int or str + message : hikari.models.messages.Message or hikari.utilities.snowflake.UniqueObject The message to pin. This may be a message object, or the ID of an existing message. @@ -910,10 +910,10 @@ async def unpin_message( Parameters ---------- - channel : hikari.models.channels.PartialChannel or hikari.utilities.snowflake.Snowflake or int or str + channel : hikari.models.channels.PartialChannel or hikari.utilities.snowflake.UniqueObject The channel to unpin a message in. This may be a channel object, or the ID of an existing channel. - message : hikari.models.messages.Message or hikari.utilities.snowflake.Snowflake or int or str + message : hikari.models.messages.Message or hikari.utilities.snowflake.UniqueObject The message to unpin. This may be a message object, or the ID of an existing message. @@ -944,22 +944,22 @@ def fetch_messages( Parameters ---------- - channel : hikari.models.channels.PartialChannel or hikari.utilities.snowflake.Snowflake or int or str + channel : hikari.models.channels.PartialChannel or hikari.utilities.snowflake.UniqueObject The channel to fetch messages in. This may be a channel object, or the ID of an existing channel. - before : hikari.utilities.undefined.UndefinedType or datetime.datetime or hikari.utilities.snowflake.Snowflake or int or str + before : hikari.utilities.undefined.UndefinedType or datetime.datetime or hikari.utilities.snowflake.UniqueObject If provided, fetch messages before this snowflake. If you provide a datetime object, it will be transformed into a snowflake. - after : hikari.utilities.undefined.UndefinedType or datetime.datetime or hikari.utilities.snowflake.Snowflake or int or str + after : hikari.utilities.undefined.UndefinedType or datetime.datetime or hikari.utilities.snowflake.UniqueObject If provided, fetch messages after this snowflake. If you provide a datetime object, it will be transformed into a snowflake. - around : hikari.utilities.undefined.UndefinedType or datetime.datetime or hikari.utilities.snowflake.Snowflake or int or str + around : hikari.utilities.undefined.UndefinedType or datetime.datetime or hikari.utilities.snowflake.UniqueObject If provided, fetch messages around this snowflake. If you provide a datetime object, it will be transformed into a snowflake. Returns ------- - hikari.net.iterators.LazyIterator[hikari.models.messages.Message] + hikari.utilities.iterators.LazyIterator[hikari.models.messages.Message] A iterator to fetch the messages. Raises @@ -985,6 +985,7 @@ def fetch_messages( if undefined.count(before, after, around) < 2: raise TypeError("Expected no kwargs, or maximum of one of 'before', 'after', 'around'") + timestamp: typing.Union[undefined.UndefinedType, datetime.datetime, snowflake.Unique, int, str] if before is not undefined.UNDEFINED: direction, timestamp = "before", before elif after is not undefined.UNDEFINED: @@ -992,12 +993,14 @@ def fetch_messages( elif around is not undefined.UNDEFINED: direction, timestamp = "around", around else: - direction, timestamp = "before", snowflake.Snowflake.max() + direction, timestamp = "before", undefined.UNDEFINED if isinstance(timestamp, datetime.datetime): - timestamp = snowflake.Snowflake.from_datetime(timestamp) + timestamp = str(snowflake.Snowflake.from_datetime(timestamp)) + elif timestamp is not undefined.UNDEFINED: + timestamp = str(timestamp) - return special_endpoints.MessageIterator(self._app, self._request, str(int(channel)), direction, str(timestamp)) + return special_endpoints.MessageIterator(self._app, self._request, str(int(channel)), direction, timestamp) async def fetch_message( self, @@ -1008,10 +1011,10 @@ async def fetch_message( Parameters ---------- - channel : hikari.models.channels.PartialChannel or hikari.utilities.snowflake.Snowflake or int or str + channel : hikari.models.channels.PartialChannel or hikari.utilities.snowflake.UniqueObject The channel to fetch messages in. This may be a channel object, or the ID of an existing channel. - message : hikari.models.messages.Message or hikari.utilities.snowflake.Snowflake or int or str + message : hikari.models.messages.Message or hikari.utilities.snowflake.UniqueObject The message to fetch. This may be a channel object, or the ID of an existing channel. @@ -1058,7 +1061,7 @@ async def create_message( Parameters ---------- - channel : hikari.models.channels.PartialChannel or hikari.utilities.snowflake.Snowflake or int or str + channel : hikari.models.channels.PartialChannel or hikari.utilities.snowflake.UniqueObject The channel to create the message in. This may be a channel object, or the ID of an existing channel. text : hikari.utilities.undefined.UndefinedType or str @@ -1077,11 +1080,11 @@ async def create_message( If specified, a nonce that can be used for optimistic message sending. mentions_everyone : bool If specified, whether the message should parse @everyone/@here mentions. - user_mentions : typing.Collection[hikari.models.users.User or hikari.utilities.snowflake.Snowflake or int or str] or bool + user_mentions : typing.Collection[hikari.models.users.User or hikari.utilities.snowflake.UniqueObject] or bool If specified, and `bool`, whether to parse user mentions. If specified and `list`, the users to parse the mention for. This may be a user object, or the ID of an existing user. - role_mentions : typing.Collection[hikari.models.guilds.Role or hikari.utilities.snowflake.Snowflake or int or str] or bool + role_mentions : typing.Collection[hikari.models.guilds.Role or hikari.utilities.snowflake.UniqueObject] or bool If specified and `bool`, whether to parse role mentions. If specified and `list`, the roles to parse the mention for. This may be a role object, or the ID of an existing role. @@ -1183,10 +1186,10 @@ async def edit_message( Parameters ---------- - channel : hikari.models.channels.PartialChannel or hikari.utilities.snowflake.Snowflake or int or str + channel : hikari.models.channels.PartialChannel or hikari.utilities.snowflake.UniqueObject The channel to edit the message in. This may be a channel object, or the ID of an existing channel. - message : hikari.models.messages.Message or hikari.utilities.snowflake.Snowflake or int or str + message : hikari.models.messages.Message or hikari.utilities.snowflake.UniqueObject The message to fetch. text embed @@ -1733,12 +1736,10 @@ def fetch_audit_log( ) -> iterators.LazyIterator[audit_logs.AuditLog]: guild = str(int(guild)) - if before is undefined.UNDEFINED: - before = str(snowflake.Snowflake.max()) - elif isinstance(before, datetime.datetime): + if isinstance(before, datetime.datetime): before = str(snowflake.Snowflake.from_datetime(before)) - else: - before = str(int(before)) + elif before is not undefined.UNDEFINED: + before = str(before) if user is not undefined.UNDEFINED: user = str(int(user)) @@ -2179,7 +2180,7 @@ async def kick_member( *, reason: typing.Union[undefined.UndefinedType, str] = undefined.UNDEFINED, ) -> None: - route = routes.DELETE_GUILD_MEMBER.compile(guild=guild, user=user, ) + route = routes.DELETE_GUILD_MEMBER.compile(guild=guild, user=user) await self._request(route, reason=reason) async def ban_user( @@ -2319,12 +2320,12 @@ async def estimate_guild_prune_count( Parameters ---------- - guild : hikari.models.guilds.Guild or hikari.utilities.snowflake.Snowflake or int or str + guild : hikari.models.guilds.Guild or hikari.utilities.snowflake.UniqueObject The guild to estimate the guild prune count for. This may be a guild object, or the ID of an existing channel. days : hikari.utilities.undefined.UndefinedType or int If provided, number of days to count prune for. - include_roles : hikari.utilities.undefined.UndefinedType or typing.Collection[hikari.models.guilds.Role or hikari.utilities.snowflake.Snowflake or int or str] + include_roles : hikari.utilities.undefined.UndefinedType or typing.Collection[hikari.models.guilds.Role or hikari.utilities.snowflake.UniqueObject] If provided, the role(s) to include. By default, this endpoint will not count users with roles. Providing roles using this attribute will make members with the specified roles also get included into the count. @@ -2372,7 +2373,7 @@ async def begin_guild_prune( Parameters ---------- - guild : hikari.models.guilds.Guild or hikari.utilities.snowflake.Snowflake or int or str + guild : hikari.models.guilds.Guild or hikari.utilities.snowflake.UniqueObject The guild to begin the guild prune in. This may be a guild object, or the ID of an existing channel. days : hikari.utilities.undefined.UndefinedType or int @@ -2380,7 +2381,7 @@ async def begin_guild_prune( compute_prune_count: hikari.utilities.undefined.UndefinedType or bool If provided, whether to return the prune count. This is discouraged for large guilds. - include_roles : hikari.utilities.undefined.UndefinedType or typing.Collection[hikari.models.guilds.Role or hikari.utilities.snowflake.Snowflake or int or str] + include_roles : hikari.utilities.undefined.UndefinedType or typing.Collection[hikari.models.guilds.Role or hikari.utilities.snowflake.UniqueObject] If provided, the role(s) to include. By default, this endpoint will not count users with roles. Providing roles using this attribute will make members with the specified roles also get included into the count. diff --git a/hikari/net/special_endpoints.py b/hikari/net/special_endpoints.py index 8b6c70506c..d7184d9e80 100644 --- a/hikari/net/special_endpoints.py +++ b/hikari/net/special_endpoints.py @@ -34,8 +34,6 @@ from hikari.utilities import data_binding from hikari.utilities import date from hikari.utilities import iterators -from hikari.utilities import snowflake -from hikari.utilities import snowflake as snowflake_ from hikari.utilities import undefined if typing.TYPE_CHECKING: @@ -51,6 +49,7 @@ from hikari.models import permissions as permissions_ from hikari.models import users from hikari.utilities import files + from hikari.utilities import snowflake @typing.final @@ -66,7 +65,7 @@ class TypingIndicator: def __init__( self, - channel: typing.Union[channels.TextChannel, snowflake_.UniqueObject], + channel: typing.Union[channels.TextChannel, snowflake.UniqueObject], request_call: typing.Callable[ ..., typing.Coroutine[None, None, typing.Union[None, data_binding.JSONObject, data_binding.JSONArray]] ], @@ -277,7 +276,7 @@ def add_role( mentionable: typing.Union[undefined.UndefinedType, bool] = undefined.UNDEFINED, permissions: typing.Union[undefined.UndefinedType, permissions_.Permission] = undefined.UNDEFINED, position: typing.Union[undefined.UndefinedType, int] = undefined.UNDEFINED, - ) -> snowflake_.Snowflake: + ) -> snowflake.Snowflake: """Create a role. !!! note @@ -356,7 +355,7 @@ def add_category( undefined.UndefinedType, typing.Collection[channels.PermissionOverwrite] ] = undefined.UNDEFINED, nsfw: typing.Union[undefined.UndefinedType, bool] = undefined.UNDEFINED, - ) -> snowflake_.Snowflake: + ) -> snowflake.Snowflake: """Create a category channel. Parameters @@ -403,7 +402,7 @@ def add_text_channel( name: str, /, *, - parent_id: typing.Union[undefined.UndefinedType, snowflake_.Snowflake] = undefined.UNDEFINED, + parent_id: typing.Union[undefined.UndefinedType, snowflake.Snowflake] = undefined.UNDEFINED, topic: typing.Union[undefined.UndefinedType, str] = undefined.UNDEFINED, rate_limit_per_user: typing.Union[undefined.UndefinedType, date.TimeSpan] = undefined.UNDEFINED, position: typing.Union[undefined.UndefinedType, int] = undefined.UNDEFINED, @@ -411,7 +410,7 @@ def add_text_channel( undefined.UndefinedType, typing.Collection[channels.PermissionOverwrite] ] = undefined.UNDEFINED, nsfw: typing.Union[undefined.UndefinedType, bool] = undefined.UNDEFINED, - ) -> snowflake_.Snowflake: + ) -> snowflake.Snowflake: """Create a text channel. Parameters @@ -470,7 +469,7 @@ def add_voice_channel( name: str, /, *, - parent_id: typing.Union[undefined.UndefinedType, snowflake_.Snowflake] = undefined.UNDEFINED, + parent_id: typing.Union[undefined.UndefinedType, snowflake.Snowflake] = undefined.UNDEFINED, bitrate: typing.Union[undefined.UndefinedType, int] = undefined.UNDEFINED, position: typing.Union[undefined.UndefinedType, int] = undefined.UNDEFINED, permission_overwrites: typing.Union[ @@ -478,7 +477,7 @@ def add_voice_channel( ] = undefined.UNDEFINED, nsfw: typing.Union[undefined.UndefinedType, bool] = undefined.UNDEFINED, user_limit: typing.Union[undefined.UndefinedType, int] = undefined.UNDEFINED, - ) -> snowflake_.Snowflake: + ) -> snowflake.Snowflake: """Create a voice channel. Parameters @@ -532,10 +531,10 @@ def add_voice_channel( self._channels.append(payload) return snowflake_id - def _new_snowflake(self) -> snowflake_.Snowflake: + def _new_snowflake(self) -> snowflake.Snowflake: value = self._counter self._counter += 1 - return snowflake_.Snowflake.from_data(datetime.datetime.now(tz=datetime.timezone.utc), 0, 0, value,) + return snowflake.Snowflake.from_data(datetime.datetime.now(tz=datetime.timezone.utc), 0, 0, value,) # We use an explicit forward reference for this, since this breaks potential @@ -554,7 +553,7 @@ def __init__( ], channel_id: str, direction: str, - first_id: str, + first_id: typing.Union[str, undefined.UndefinedType], ) -> None: super().__init__() self._app = app @@ -601,7 +600,7 @@ def __init__( super().__init__() self._app = app self._request_call = request_call - self._first_id = snowflake.Snowflake.min() + self._first_id = undefined.UNDEFINED self._route = routes.GET_REACTIONS.compile(channel=channel_id, message=message_id, emoji=emoji) async def _next_chunk(self) -> typing.Optional[typing.Generator[users.User, typing.Any, None]]: @@ -678,7 +677,9 @@ def __init__( self._route = routes.GET_GUILD_MEMBERS.compile(guild=guild_id) self._request_call = request_call self._app = app - self._first_id = snowflake.Snowflake.min() + # This starts at the default provided by discord instead of the max snowflake + # because that caused discord to take about 2 seconds more to return the first response. + self._first_id = undefined.UNDEFINED async def _next_chunk(self) -> typing.Optional[typing.Generator[guilds.Member, typing.Any, None]]: query = data_binding.StringMapBuilder() @@ -710,13 +711,13 @@ def __init__( ..., typing.Coroutine[None, None, typing.Union[None, data_binding.JSONObject, data_binding.JSONArray]] ], guild_id: str, - before: str, + before: typing.Union[str, undefined.UndefinedType], user_id: typing.Union[str, undefined.UndefinedType], action_type: typing.Union[int, undefined.UndefinedType], ) -> None: self._action_type = action_type self._app = app - self._first_id = str(before) + self._first_id = before self._request_call = request_call self._route = routes.GET_GUILD_AUDIT_LOGS.compile(guild=guild_id) self._user_id = user_id @@ -726,6 +727,7 @@ async def __anext__(self) -> audit_logs.AuditLog: query.put("limit", 100) query.put("user_id", self._user_id) query.put("event_type", self._action_type) + query.put("before", self._first_id) raw_response = await self._request_call(compiled_route=self._route, query=query) response = typing.cast(data_binding.JSONObject, raw_response) diff --git a/hikari/utilities/snowflake.py b/hikari/utilities/snowflake.py index 0bedcb04f5..a4833a23fb 100644 --- a/hikari/utilities/snowflake.py +++ b/hikari/utilities/snowflake.py @@ -19,7 +19,7 @@ from __future__ import annotations -__all__: typing.Final[typing.Sequence[str]] = ["Snowflake", "Unique"] +__all__: typing.Final[typing.Sequence[str]] = ["Snowflake", "Unique", "UniqueObject"] import abc import datetime diff --git a/pages/index.html b/pages/index.html index 7412a25d76..463a4b54e5 100644 --- a/pages/index.html +++ b/pages/index.html @@ -20,12 +20,10 @@ | You should have received a copy of the GNU Lesser General Public License | along with Hikari. If not, see . !--> - + - + @@ -162,4 +160,4 @@

    - + \ No newline at end of file diff --git a/tests/hikari/impl/test_entity_factory.py b/tests/hikari/impl/test_entity_factory.py index 3df13e24b0..eb514ce0b8 100644 --- a/tests/hikari/impl/test_entity_factory.py +++ b/tests/hikari/impl/test_entity_factory.py @@ -210,7 +210,7 @@ def test_deserialize_application( # Team assert application.team.id == 202020202 assert application.team.icon_hash == "hashtag" - assert application.team.owner_user_id == 393030292 + assert application.team.owner_id == 393030292 assert isinstance(application.team, application_models.Team) # TeamMember assert len(application.team.members) == 1 From b654d8ad97836def9c015dce24f70c54011164a1 Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Mon, 29 Jun 2020 08:59:23 +0100 Subject: [PATCH 593/922] Added missing methods to BotAppImpl for new event stuff. --- hikari/impl/bot.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/hikari/impl/bot.py b/hikari/impl/bot.py index 2ffea59166..afbe363157 100644 --- a/hikari/impl/bot.py +++ b/hikari/impl/bot.py @@ -275,6 +275,18 @@ def listen( ) -> typing.Callable[[CallbackT], CallbackT]: return self.event_dispatcher.listen(event_type) + def get_listeners( + self, event_type: typing.Type[EventT], + ) -> typing.Optional[typing.Collection[typing.Callable[[EventT], typing.Coroutine[None, typing.Any, None]]]]: + return self.event_dispatcher.get_listeners(event_type) + + def has_listener( + self, + event_type: typing.Type[EventT], + callback: typing.Callable[[EventT], typing.Coroutine[None, typing.Any, None]], + ) -> bool: + return self.event_dispatcher.has_listener(event_type, callback) + def subscribe( self, event_type: typing.Type[EventT], From bb8e17aa047a6a987ba7985ce9705f9b2b471a2b Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Mon, 29 Jun 2020 08:00:52 +0000 Subject: [PATCH 594/922] Delete Makefile --- Makefile | 13 ------------- 1 file changed, 13 deletions(-) delete mode 100644 Makefile diff --git a/Makefile b/Makefile deleted file mode 100644 index 99be660a42..0000000000 --- a/Makefile +++ /dev/null @@ -1,13 +0,0 @@ -release: - ACCELERATE_HIKARI=1 nox -s build-ext - -debug: - DEBUG_HIKARI=1 ACCELERATE_HIKARI=1 nox -s build-ext - -clean: - nox -s clean-ext - -rebuild: clean release -debug-rebuild: clean debug - -.PHONY: release debug clean rebuild debug-rebuild From fed59423bc7d912d312c7f6dee6f9141e95aeb1b Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Mon, 29 Jun 2020 09:27:37 +0100 Subject: [PATCH 595/922] Changed behaviour of get_listeners and has_listener to be polymorphic at a cost of O(n) time complexity instead of O(1). --- hikari/api/event_dispatcher.py | 34 ++++++++++++++++++++++++------- hikari/impl/bot.py | 10 +++++---- hikari/impl/event_manager_core.py | 32 +++++++++++++++++++++-------- 3 files changed, 57 insertions(+), 19 deletions(-) diff --git a/hikari/api/event_dispatcher.py b/hikari/api/event_dispatcher.py index fe9ce038ad..ee4ef7623c 100644 --- a/hikari/api/event_dispatcher.py +++ b/hikari/api/event_dispatcher.py @@ -176,20 +176,25 @@ async def on_message(event): @abc.abstractmethod def get_listeners( - self, event_type: typing.Type[EventT], - ) -> typing.Optional[typing.Collection[typing.Callable[[EventT], typing.Coroutine[None, typing.Any, None]]]]: + self, event_type: typing.Type[EventT], *, polymorphic: bool = True, + ) -> typing.Collection[typing.Callable[[EventT], typing.Coroutine[None, typing.Any, None]]]: """Get the listeners for a given event type, if there are any. Parameters ---------- - event_type : typing.Type[T] + event_type : typing.Type[hikari.events.base.Event] The event type to look for. + polymorphic : bool + If `True`, this will return `True` if a subclass of the given + event type has a listener registered. If `False`, then only + listeners for this class specifically are returned. The default + is `True`. Returns ------- - typing.Optional[typing.Collection[typing.Callable[[EventT], typing.Coroutine[None, typing.Any, None]]] - A copy of the collection of listeners for the event, or `None` if - none are registered. + typing.Collection[typing.Callable[[EventT], typing.Coroutine[None, typing.Any, None]] + A copy of the collection of listeners for the event. Will return + an empty collection if nothing is registered. """ @abc.abstractmethod @@ -197,8 +202,23 @@ def has_listener( self, event_type: typing.Type[EventT], callback: typing.Callable[[EventT], typing.Coroutine[None, typing.Any, None]], + *, + polymorphic: bool = True, ) -> bool: - """Check whether the callback is subscribed to the given event.""" + """Check whether the callback is subscribed to the given event. + + Parameters + ---------- + event_type : typing.Type[hikari.events.base.Event] + The event type to look for. + callback : + The callback to look for. + polymorphic : bool + If `True`, this will return `True` if a subclass of the given + event type has a listener registered. If `False`, then only + listeners for this class specifically are checked. The default + is `True`. + """ @abc.abstractmethod def unsubscribe( diff --git a/hikari/impl/bot.py b/hikari/impl/bot.py index afbe363157..369fc13d0d 100644 --- a/hikari/impl/bot.py +++ b/hikari/impl/bot.py @@ -276,16 +276,18 @@ def listen( return self.event_dispatcher.listen(event_type) def get_listeners( - self, event_type: typing.Type[EventT], - ) -> typing.Optional[typing.Collection[typing.Callable[[EventT], typing.Coroutine[None, typing.Any, None]]]]: - return self.event_dispatcher.get_listeners(event_type) + self, event_type: typing.Type[EventT], *, polymorphic: bool = True, + ) -> typing.Collection[typing.Callable[[EventT], typing.Coroutine[None, typing.Any, None]]]: + return self.event_dispatcher.get_listeners(event_type, polymorphic=polymorphic) def has_listener( self, event_type: typing.Type[EventT], callback: typing.Callable[[EventT], typing.Coroutine[None, typing.Any, None]], + *, + polymorphic: bool = True, ) -> bool: - return self.event_dispatcher.has_listener(event_type, callback) + return self.event_dispatcher.has_listener(event_type, callback, polymorphic=polymorphic) def subscribe( self, diff --git a/hikari/impl/event_manager_core.py b/hikari/impl/event_manager_core.py index f6cf064d5b..0824b18d66 100644 --- a/hikari/impl/event_manager_core.py +++ b/hikari/impl/event_manager_core.py @@ -112,21 +112,37 @@ async def wrapper(event: EventT) -> None: return callback def get_listeners( - self, event_type: typing.Type[EventT], - ) -> typing.Optional[typing.Collection[typing.Callable[[EventT], typing.Coroutine[None, typing.Any, None]]]]: - items = self._listeners.get(event_type) - if items is not None: - return items[:] - return None + self, event_type: typing.Type[EventT], *, polymorphic: bool = True, + ) -> typing.Collection[typing.Callable[[EventT], typing.Coroutine[None, typing.Any, None]]]: + if polymorphic: + listeners: typing.List[typing.Callable[[EventT], typing.Coroutine[None, typing.Any, None]]] = [] + for subscribed_event_type, subscribed_listeners in self._listeners.items(): + if issubclass(subscribed_event_type, event_type): + listeners += subscribed_listeners + return listeners + else: + items = self._listeners.get(event_type) + if items is not None: + return items[:] + + return [] def has_listener( self, event_type: typing.Type[EventT], callback: typing.Callable[[EventT], typing.Coroutine[None, typing.Any, None]], + *, + polymorphic: bool = True, ) -> bool: - if event_type not in self._listeners: + if polymorphic: + for subscribed_event_type, listeners in self._listeners.items(): + if issubclass(subscribed_event_type, event_type) and callback in listeners: + return True return False - return callback in self._listeners[event_type] + else: + if event_type not in self._listeners: + return False + return callback in self._listeners[event_type] def unsubscribe( self, From 35a87020caaeb5a9567b51e6279252fc91fc83e9 Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Mon, 29 Jun 2020 10:52:16 +0100 Subject: [PATCH 596/922] Updated logging to keep it tidier for single-sharded bots. --- hikari/impl/bot.py | 19 +++++++++++- hikari/impl/entity_factory.py | 23 +++++++-------- hikari/impl/gateway_zookeeper.py | 46 ++++++++++++++++++----------- hikari/models/gateway.py | 18 +++++++++-- hikari/utilities/date.py | 12 ++++++++ tests/hikari/utilities/test_date.py | 37 +++++++++++++++++++++++ 6 files changed, 122 insertions(+), 33 deletions(-) diff --git a/hikari/impl/bot.py b/hikari/impl/bot.py index 369fc13d0d..2cc5eee1ef 100644 --- a/hikari/impl/bot.py +++ b/hikari/impl/bot.py @@ -22,6 +22,7 @@ __all__: typing.Final[typing.Sequence[str]] = ["BotAppImpl"] import asyncio +import contextlib import inspect import logging import os @@ -193,7 +194,7 @@ def __init__( if stateless: self._cache = stateless_cache_impl.StatelessCacheImpl() - _LOGGER.info("this application will be stateless! Cache-based operations will be unavailable!") + _LOGGER.info("this application is stateless, cache-based operations will not be available") else: self._cache = cache_impl.InMemoryCacheComponentImpl(app=self) @@ -270,6 +271,22 @@ def proxy_settings(self) -> config.ProxySettings: def rest(self) -> rest.REST: return self._rest + def start(self) -> typing.Coroutine[None, typing.Any, None]: + if self._debug is True: + _LOGGER.warning("debug mode is enabled, performance may be affected") + + # If possible, set the coroutine origin tracking depth to a larger value. + # This feature is provisional, so don't hold your breath if it doesn't + # exist. + with contextlib.suppress(AttributeError, NameError): + # noinspection PyUnresolvedReferences + sys.set_coroutine_origin_tracking_depth(40) # type: ignore[attr-defined] + + # Set debugging on the event loop. + asyncio.get_event_loop().set_debug(True) + + return super().start() + def listen( self, event_type: typing.Union[undefined.UndefinedType, typing.Type[EventT]] = undefined.UNDEFINED, ) -> typing.Callable[[CallbackT], CallbackT]: diff --git a/hikari/impl/entity_factory.py b/hikari/impl/entity_factory.py index 41a81a1556..f781d9e928 100644 --- a/hikari/impl/entity_factory.py +++ b/hikari/impl/entity_factory.py @@ -731,19 +731,18 @@ def deserialize_emoji( ################## def deserialize_gateway_bot(self, payload: data_binding.JSONObject) -> gateway_models.GatewayBot: - gateway_bot = gateway_models.GatewayBot() - gateway_bot.url = payload["url"] - gateway_bot.shard_count = int(payload["shards"]) session_start_limit_payload = payload["session_start_limit"] - session_start_limit = gateway_models.SessionStartLimit() - session_start_limit.total = int(session_start_limit_payload["total"]) - session_start_limit.remaining = int(session_start_limit_payload["remaining"]) - session_start_limit.reset_after = datetime.timedelta(milliseconds=session_start_limit_payload["reset_after"]) - # I do not trust that this may never be zero for some unknown reason. If it was 0, it - # would hang the application on start up, so I enforce it is at least 1. - session_start_limit.max_concurrency = max(session_start_limit_payload.get("max_concurrency", 0), 1) - gateway_bot.session_start_limit = session_start_limit - return gateway_bot + session_start_limit = gateway_models.SessionStartLimit( + total=int(session_start_limit_payload["total"]), + remaining=int(session_start_limit_payload["remaining"]), + reset_after=datetime.timedelta(milliseconds=session_start_limit_payload["reset_after"]), + # I do not trust that this may never be zero for some unknown reason. If it was 0, it + # would hang the application on start up, so I enforce it is at least 1. + max_concurrency=max(session_start_limit_payload.get("max_concurrency", 0), 1), + ) + return gateway_models.GatewayBot( + url=payload["url"], shard_count=int(payload["shards"]), session_start_limit=session_start_limit, + ) ################ # GUILD MODELS # diff --git a/hikari/impl/gateway_zookeeper.py b/hikari/impl/gateway_zookeeper.py index 8de180a028..ba86bc78bb 100644 --- a/hikari/impl/gateway_zookeeper.py +++ b/hikari/impl/gateway_zookeeper.py @@ -26,6 +26,7 @@ import contextlib import datetime import logging +import reprlib import signal import time import typing @@ -217,11 +218,6 @@ async def start(self) -> None: await self._maybe_dispatch(other.StartingEvent()) - if self._shard_count > 1: - _LOGGER.info("starting %s shard(s)", len(self._shards)) - else: - _LOGGER.info("this application will be single-sharded") - start_time = time.perf_counter() try: @@ -265,7 +261,10 @@ async def start(self) -> None: finish_time = time.perf_counter() self._gather_task = asyncio.create_task(self._gather(), name=f"zookeeper for {len(self._shards)} shard(s)") - _LOGGER.info("started %s shard(s) in approx %.2fs", len(self._shards), finish_time - start_time) + + # Don't bother logging this if we are single sharded. It is useless information. + if len(self._shard_ids) > 1: + _LOGGER.info("started %s shard(s) in approx %.2fs", len(self._shards), finish_time - start_time) await self._maybe_dispatch(other.StartedEvent()) @@ -317,23 +316,12 @@ async def fetch_sharding_settings(self) -> gateway_models.GatewayBot: async def _init(self) -> None: gw_recs = await self.fetch_sharding_settings() - _LOGGER.info( - "you have opened %s session(s) recently, you can open %s more before %s", - gw_recs.session_start_limit.total - gw_recs.session_start_limit.remaining, - gw_recs.session_start_limit.remaining if gw_recs.session_start_limit.remaining > 0 else "no", - (datetime.datetime.now() + gw_recs.session_start_limit.reset_after).strftime("%c"), - ) - self._shard_count = self._shard_count if self._shard_count else gw_recs.shard_count self._shard_ids = self._shard_ids if self._shard_ids else set(range(self._shard_count)) self._max_concurrency = gw_recs.session_start_limit.max_concurrency url = gw_recs.url - _LOGGER.info( - "will connect shards to %s at a rate of %s shard(s) per 5 seconds (contact Discord to increase this rate)", - url, - self._max_concurrency, - ) + reset_at = gw_recs.session_start_limit.reset_at.strftime("%d/%m/%y %H:%M:%S %Z").rstrip() shard_clients: typing.Dict[int, gateway.Gateway] = {} for shard_id in self._shard_ids: @@ -359,6 +347,28 @@ async def _init(self) -> None: self._shards = shard_clients + if len(self._shard_ids) == 1 and self._shard_ids == {0}: + _LOGGER.info( + "single-sharded configuration -- you have started %s/%s sessions prior to connecting (resets at %s)", + gw_recs.session_start_limit.used, + gw_recs.session_start_limit.total, + reset_at, + ) + else: + _LOGGER.info( + "max_concurrency: %s (contact Discord for an increase) -- " + "will connect %s shards %s; the distributed application should have %s shards in total -- " + "you have started %s/%s sessions prior to connecting (resets at %s)", + url, + gw_recs.session_start_limit.max_concurrency, + len(self._shard_ids), + reprlib.repr(sorted(self._shard_ids)), + self._shard_count, + gw_recs.session_start_limit.used, + gw_recs.session_start_limit.total, + reset_at, + ) + def _max_concurrency_chunker(self) -> typing.Iterator[typing.Iterator[int]]: """Yield generators of shard IDs. diff --git a/hikari/models/gateway.py b/hikari/models/gateway.py index 33ce06acdf..2c46a2165e 100644 --- a/hikari/models/gateway.py +++ b/hikari/models/gateway.py @@ -25,11 +25,13 @@ import attr +from hikari.utilities import date + if typing.TYPE_CHECKING: import datetime -@attr.s(eq=True, hash=False, init=False, kw_only=True, slots=True) +@attr.s(eq=True, hash=False, init=True, kw_only=True, slots=True) class SessionStartLimit: """Used to represent information about the current session start limits.""" @@ -56,8 +58,20 @@ class SessionStartLimit: more information. """ + _created_at: datetime.datetime = attr.ib(factory=date.local_datetime, init=False) + + @property + def used(self) -> int: + """Return how many times you have sent an IDENTIFY in the window.""" + return self.total - self.remaining + + @property + def reset_at(self) -> datetime.datetime: + """Return the approximate time that the IDENTIFY limit resets at.""" + return self._created_at + self.reset_after + -@attr.s(eq=True, hash=False, init=False, kw_only=True, slots=True) +@attr.s(eq=True, hash=False, init=True, kw_only=True, slots=True) class GatewayBot: """Used to represent gateway information for the connected bot.""" diff --git a/hikari/utilities/date.py b/hikari/utilities/date.py index 7cbd8512e3..498901214a 100644 --- a/hikari/utilities/date.py +++ b/hikari/utilities/date.py @@ -27,6 +27,8 @@ "unix_epoch_to_datetime", "TimeSpan", "timespan_to_int", + "local_datetime", + "utc_datetime", ] import datetime @@ -196,3 +198,13 @@ def timespan_to_int(value: TimeSpan, /) -> int: if isinstance(value, datetime.timedelta): value = value.total_seconds() return int(max(0, value)) + + +def local_datetime() -> datetime.datetime: + """Return the current date/time for the system's time zone.""" + return utc_datetime().astimezone() + + +def utc_datetime() -> datetime.datetime: + """Return the current date/time for UTC (GMT+0).""" + return datetime.datetime.now(tz=datetime.timezone.utc) diff --git a/tests/hikari/utilities/test_date.py b/tests/hikari/utilities/test_date.py index 5f5068ba80..95b56179f9 100644 --- a/tests/hikari/utilities/test_date.py +++ b/tests/hikari/utilities/test_date.py @@ -17,6 +17,7 @@ # along with Hikari. If not, see . import datetime +import mock import pytest from hikari.utilities import date as date_ @@ -133,3 +134,39 @@ def test_unix_epoch_to_datetime_with_out_of_range_negative_timestamp(): ) def test_timespan_to_int(input_value, expected_result): assert date_.timespan_to_int(input_value) == expected_result + + +def test_utc_datetime(): + current_datetime = datetime.datetime.now(tz=datetime.timezone.utc) + + # We can't mock datetime normally as it is a C module :( + class datetime_module: + timezone = datetime.timezone + + class datetime: + now = mock.MagicMock(return_value=current_datetime) + + with mock.patch.object(date_, "datetime", datetime_module): + result = date_.utc_datetime() + + datetime_module.datetime.now.assert_called_once_with(tz=datetime.timezone.utc) + + assert result == current_datetime + + +def test_local_datetime(): + current_datetime = datetime.datetime.now(tz=datetime.timezone.utc) + + # We can't mock datetime normally as it is a C module :( + class datetime_module: + timezone = datetime.timezone + + class datetime: + now = mock.MagicMock(return_value=current_datetime) + + with mock.patch.object(date_, "datetime", datetime_module): + result = date_.local_datetime() + + datetime_module.datetime.now.assert_called_once_with(tz=datetime.timezone.utc) + + assert result == current_datetime.astimezone() From 184adbdf749cf5cb12fb5c9bfef3c8b18d211dd9 Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Mon, 29 Jun 2020 10:59:07 +0100 Subject: [PATCH 597/922] Fixes pytest warnings due to non-awaited mocks, closes #407 --- tests/hikari/__init__.py | 19 +++++++------------ tests/hikari/net/test_gateway.py | 6 ++++++ tests/hikari/net/test_iterators.py | 17 ----------------- 3 files changed, 13 insertions(+), 29 deletions(-) delete mode 100644 tests/hikari/net/test_iterators.py diff --git a/tests/hikari/__init__.py b/tests/hikari/__init__.py index aea66f3ef9..7c55fb3a3e 100644 --- a/tests/hikari/__init__.py +++ b/tests/hikari/__init__.py @@ -15,27 +15,22 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . -import asyncio -import contextlib import sys +import asyncio import pytest -_real_new_event_loop = asyncio.new_event_loop - -def _new_event_loop(): - loop = _real_new_event_loop() - loop.set_debug(True) +sys.set_coroutine_origin_tracking_depth(100) - with contextlib.suppress(AttributeError): - # provisional since py37 - sys.set_coroutine_origin_tracking_depth(20) - return loop +class TestingPolicy(asyncio.DefaultEventLoopPolicy): + def set_event_loop(self, loop) -> None: + loop.set_debug(True) + super().set_event_loop(loop) -asyncio.new_event_loop = _new_event_loop +asyncio.set_event_loop_policy(TestingPolicy()) _pytest_parametrize = pytest.mark.parametrize diff --git a/tests/hikari/net/test_gateway.py b/tests/hikari/net/test_gateway.py index 26bc387923..006ea273f9 100644 --- a/tests/hikari/net/test_gateway.py +++ b/tests/hikari/net/test_gateway.py @@ -508,6 +508,9 @@ async def test_backoff_and_waits_if_restarted_too_quickly(self, client, client_s client._last_run_started_at = 40 client._backoff.__next__ = mock.MagicMock(return_value=24.37) + # We mock create_task, so this will never be awaited if not. + client._heartbeat_keepalive = mock.MagicMock() + stack = contextlib.ExitStack() wait_for = stack.enter_context(mock.patch.object(asyncio, "wait_for", side_effect=asyncio.TimeoutError)) create_task = stack.enter_context(mock.patch.object(asyncio, "create_task")) @@ -557,6 +560,9 @@ async def test_backoff_does_not_trigger_if_not_restarting_in_small_window(self, ) ) + # We mock create_task, so this will never be awaited if not. + client._heartbeat_keepalive = mock.MagicMock() + stack = contextlib.ExitStack() stack.enter_context(mock.patch.object(asyncio, "wait_for")) stack.enter_context(mock.patch.object(asyncio, "create_task")) diff --git a/tests/hikari/net/test_iterators.py b/tests/hikari/net/test_iterators.py deleted file mode 100644 index 3b080b938f..0000000000 --- a/tests/hikari/net/test_iterators.py +++ /dev/null @@ -1,17 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright © Nekoka.tt 2019-2020 -# -# This file is part of Hikari. -# -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . From 8cd7aca9b82f05f877ef784bf2ae6e898e1a15ce Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Mon, 29 Jun 2020 11:07:00 +0100 Subject: [PATCH 598/922] Fixes pytest error on new modules in tests/hikari, closes #408 --- tests/__init__.py | 22 ++++++++++++++++++++++ tests/hikari/test_errors.py | 21 +++++++++++++++++++++ 2 files changed, 43 insertions(+) create mode 100644 tests/__init__.py create mode 100644 tests/hikari/test_errors.py diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000000..f8ff46825e --- /dev/null +++ b/tests/__init__.py @@ -0,0 +1,22 @@ +# -*- coding: utf-8 -*- +# Copyright © Nekoka.tt 2019-2020 +# +# This file is part of Hikari. +# +# Hikari is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Hikari is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Hikari. If not, see . + +"""Pytest fails without this file in some cases. + +Fixes https://gitlab.com/nekokatt/hikari/-/issues/408. +""" diff --git a/tests/hikari/test_errors.py b/tests/hikari/test_errors.py new file mode 100644 index 0000000000..0b09e698ba --- /dev/null +++ b/tests/hikari/test_errors.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +# Copyright © Nekoka.tt 2019-2020 +# +# This file is part of Hikari. +# +# Hikari is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Hikari is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Hikari. If not, see . + + +def test_issue_408_is_fixed(): + assert True From f06df018e26994294c20784ca60f0ce230dc9ac3 Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Mon, 29 Jun 2020 11:38:24 +0100 Subject: [PATCH 599/922] Made processpoolexecutors work for file IO where needed, closes #409. --- hikari/utilities/files.py | 66 +++++++++++++++++++++++++++++++++------ 1 file changed, 56 insertions(+), 10 deletions(-) diff --git a/hikari/utilities/files.py b/hikari/utilities/files.py index 4102fad740..3b22dc2da3 100644 --- a/hikari/utilities/files.py +++ b/hikari/utilities/files.py @@ -36,6 +36,7 @@ import abc import asyncio import base64 +import concurrent.futures import logging import mimetypes import os @@ -50,9 +51,9 @@ from hikari.net import helpers if typing.TYPE_CHECKING: - import concurrent.futures import types + _LOGGER: typing.Final[logging.Logger] = logging.getLogger(__name__) _MAGIC: typing.Final[int] = 50 * 1024 @@ -318,8 +319,13 @@ async def __aiter__(self) -> typing.AsyncGenerator[typing.Any, bytes]: @attr.s(auto_attribs=True, slots=True) -class FileReader(AsyncReader): - """Asynchronous file reader that reads a resource from local storage.""" +class FileReader(AsyncReader, abc.ABC): + """Abstract base for a file reader object. + + Various implementations have to exist in order to cater for situations + where we cannot pass IO objects around (e.g. ProcessPoolExecutors, since + they pickle things). + """ executor: typing.Optional[concurrent.futures.Executor] """The associated `concurrent.futures.Executor` to use for blocking IO.""" @@ -327,25 +333,34 @@ class FileReader(AsyncReader): path: typing.Union[str, pathlib.Path] """The path to the resource to read.""" - loop: asyncio.AbstractEventLoop = attr.ib(factory=asyncio.get_running_loop) - """The event loop to use.""" + +@attr.s(auto_attribs=True, slots=True) +class ThreadedFileReader(FileReader): + """Asynchronous file reader that reads a resource from local storage. + + This implementation works with pools that exist in the same interpreter + instance as the caller, namely thread pool executors, where objects + do not need to be pickled to be communicated. + """ async def __aiter__(self) -> typing.AsyncGenerator[typing.Any, bytes]: + loop = asyncio.get_running_loop() + path = self.path if isinstance(path, pathlib.Path): - path = await self.loop.run_in_executor(self.executor, self._expand, self.path) + path = await loop.run_in_executor(self.executor, self._expand, self.path) - fp = await self.loop.run_in_executor(self.executor, self._open, path) + fp = await loop.run_in_executor(self.executor, self._open, path) try: while True: - chunk = await self.loop.run_in_executor(self.executor, self._read_chunk, fp, _MAGIC) + chunk = await loop.run_in_executor(self.executor, self._read_chunk, fp, _MAGIC) yield chunk if len(chunk) < _MAGIC: break finally: - await self.loop.run_in_executor(self.executor, self._close, fp) + await loop.run_in_executor(self.executor, self._close, fp) @staticmethod def _expand(path: pathlib.Path) -> pathlib.Path: @@ -367,6 +382,33 @@ def _close(fp: typing.IO[bytes]) -> None: fp.close() +@attr.s(auto_attribs=True, slots=False) +class MultiprocessingFileReader(FileReader): + """Asynchronous file reader that reads a resource from local storage. + + This implementation works with pools that exist in a different interpreter + instance to the caller. Currently this only includes ProcessPoolExecutors + and custom implementations where objects have to be pickled to be used + by the pool. + """ + + async def __aiter__(self) -> typing.AsyncGenerator[typing.Any, bytes]: + yield await asyncio.get_running_loop().run_in_executor(self.executor, self._read_all) + + def __getstate__(self) -> typing.Dict[str, typing.Any]: + return {"path": self.path, "filename": self.filename} + + def __setstate__(self, state: typing.Dict[str, typing.Any]) -> None: + self.path = state["path"] + self.filename = state["filename"] + self.executor = None + self.mimetype = None + + def _read_all(self) -> bytes: + with open(self.path, "rb") as fp: + return fp.read() + + class AsyncReaderContextManager(typing.Generic[ReaderImplT]): """Context manager that returns a reader.""" @@ -801,4 +843,8 @@ def stream( An async context manager that when entered, produces the data stream. """ - return _NoOpAsyncReaderContextManagerImpl(FileReader(self.filename, None, executor, self.path)) + # asyncio forces the default executor when this is None to always be a thread pool executor anyway, + # so this is safe enough to do.: + is_threaded = executor is None or isinstance(executor, concurrent.futures.ThreadPoolExecutor) + impl = ThreadedFileReader if is_threaded else MultiprocessingFileReader + return _NoOpAsyncReaderContextManagerImpl(impl(self.filename, None, executor, self.path)) From 29ed8921be9f086792ca005ca4d572bb5ece8f2f Mon Sep 17 00:00:00 2001 From: davfsa Date: Mon, 29 Jun 2020 15:59:09 +0200 Subject: [PATCH 600/922] Added `__init__.py` in the test folders --- tests/hikari/events/__init__.py | 17 +++++++++++++++++ tests/hikari/impl/__init__.py | 17 +++++++++++++++++ tests/hikari/models/__init__.py | 17 +++++++++++++++++ tests/hikari/net/__init__.py | 17 +++++++++++++++++ tests/hikari/utilities/__init__.py | 17 +++++++++++++++++ 5 files changed, 85 insertions(+) create mode 100644 tests/hikari/events/__init__.py create mode 100644 tests/hikari/impl/__init__.py create mode 100644 tests/hikari/models/__init__.py create mode 100644 tests/hikari/net/__init__.py create mode 100644 tests/hikari/utilities/__init__.py diff --git a/tests/hikari/events/__init__.py b/tests/hikari/events/__init__.py new file mode 100644 index 0000000000..3b080b938f --- /dev/null +++ b/tests/hikari/events/__init__.py @@ -0,0 +1,17 @@ +# -*- coding: utf-8 -*- +# Copyright © Nekoka.tt 2019-2020 +# +# This file is part of Hikari. +# +# Hikari is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Hikari is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Hikari. If not, see . diff --git a/tests/hikari/impl/__init__.py b/tests/hikari/impl/__init__.py new file mode 100644 index 0000000000..3b080b938f --- /dev/null +++ b/tests/hikari/impl/__init__.py @@ -0,0 +1,17 @@ +# -*- coding: utf-8 -*- +# Copyright © Nekoka.tt 2019-2020 +# +# This file is part of Hikari. +# +# Hikari is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Hikari is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Hikari. If not, see . diff --git a/tests/hikari/models/__init__.py b/tests/hikari/models/__init__.py new file mode 100644 index 0000000000..3b080b938f --- /dev/null +++ b/tests/hikari/models/__init__.py @@ -0,0 +1,17 @@ +# -*- coding: utf-8 -*- +# Copyright © Nekoka.tt 2019-2020 +# +# This file is part of Hikari. +# +# Hikari is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Hikari is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Hikari. If not, see . diff --git a/tests/hikari/net/__init__.py b/tests/hikari/net/__init__.py new file mode 100644 index 0000000000..3b080b938f --- /dev/null +++ b/tests/hikari/net/__init__.py @@ -0,0 +1,17 @@ +# -*- coding: utf-8 -*- +# Copyright © Nekoka.tt 2019-2020 +# +# This file is part of Hikari. +# +# Hikari is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Hikari is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Hikari. If not, see . diff --git a/tests/hikari/utilities/__init__.py b/tests/hikari/utilities/__init__.py new file mode 100644 index 0000000000..3b080b938f --- /dev/null +++ b/tests/hikari/utilities/__init__.py @@ -0,0 +1,17 @@ +# -*- coding: utf-8 -*- +# Copyright © Nekoka.tt 2019-2020 +# +# This file is part of Hikari. +# +# Hikari is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Hikari is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Hikari. If not, see . From a1e9f10965e9c6521e6a8eb4ed46e3df77242796 Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Mon, 29 Jun 2020 17:24:06 +0100 Subject: [PATCH 601/922] More gateway tests! --- hikari/models/presences.py | 2 +- hikari/net/gateway.py | 76 +++++++++++----------- tests/hikari/net/test_gateway.py | 108 ++++++++++++++++++++++++++++++- 3 files changed, 143 insertions(+), 43 deletions(-) diff --git a/hikari/models/presences.py b/hikari/models/presences.py index a73f2c6cc6..b42e70f554 100644 --- a/hikari/models/presences.py +++ b/hikari/models/presences.py @@ -188,7 +188,7 @@ class Activity: url: typing.Optional[str] = attr.ib(default=None, repr=False) """The activity URL. Only valid for `STREAMING` activities.""" - type: ActivityType = attr.ib(converter=ActivityType) + type: ActivityType = attr.ib(converter=ActivityType, default=ActivityType.PLAYING) """The activity type.""" def __str__(self) -> str: diff --git a/hikari/net/gateway.py b/hikari/net/gateway.py index 34e7d1cc2e..fe103131de 100644 --- a/hikari/net/gateway.py +++ b/hikari/net/gateway.py @@ -431,10 +431,7 @@ async def _run_once(self, client_session: aiohttp.ClientSession) -> None: finally: heartbeat.cancel() finally: - if not math.isnan(self.connected_at): - # Only dispatch this if we actually connected before we failed! - self._dispatch("DISCONNECTED", {}) - + self._dispatch("DISCONNECTED", {}) self.connected_at = float("nan") async def update_presence( @@ -510,7 +507,7 @@ async def update_voice_state( `False`, then it will undeafen itself. """ payload = self._app.entity_factory.serialize_gateway_voice_state_update(guild, channel, self_mute, self_deaf) - await self._send_json(payload) + await self._send_json({"op": self._GatewayOpcode.VOICE_STATE_UPDATE, "d": payload}) async def _close_ws(self, code: int, message: str) -> None: self._logger.debug("sending close frame with code %s and message %r", int(code), message) @@ -519,7 +516,10 @@ async def _close_ws(self, code: int, message: str) -> None: await self._ws.close(code=code, message=bytes(message, "utf-8")) async def _handshake(self) -> None: - # HELLO! + await self._hello() + await self._identify() if self.session_id is None else await self._resume() + + async def _hello(self) -> None: message = await self._receive_json_payload() op = message["op"] if message["op"] != self._GatewayOpcode.HELLO: @@ -527,46 +527,42 @@ async def _handshake(self) -> None: raise errors.GatewayError(f"Expected HELLO opcode {self._GatewayOpcode.HELLO.value} but received {op}") self.heartbeat_interval = message["d"]["heartbeat_interval"] / 1_000.0 - self._logger.info("received HELLO, heartbeat interval is %ss", self.heartbeat_interval) - if self.session_id is not None: - # RESUME! - await self._send_json( - { - "op": self._GatewayOpcode.RESUME, - "d": {"token": self._token, "seq": self._seq, "session_id": self.session_id}, - } - ) - - else: - # IDENTIFY! - # noinspection PyArgumentList - payload: data_binding.JSONObject = { - "op": self._GatewayOpcode.IDENTIFY, - "d": { - "token": self._token, - "compress": False, - "large_threshold": self.large_threshold, - "properties": { - "$os": strings.SYSTEM_TYPE, - "$browser": strings.AIOHTTP_VERSION, - "$device": strings.LIBRARY_VERSION, - }, - "shard": [self._shard_id, self._shard_count], + async def _identify(self) -> None: + payload: data_binding.JSONObject = { + "op": self._GatewayOpcode.IDENTIFY, + "d": { + "token": self._token, + "compress": False, + "large_threshold": self.large_threshold, + "properties": { + "$os": strings.SYSTEM_TYPE, + "$browser": strings.AIOHTTP_VERSION, + "$device": strings.LIBRARY_VERSION, }, - } + "shard": [self._shard_id, self._shard_count], + }, + } - if self._intents is not None: - payload["d"]["intents"] = self._intents + if self._intents is not None: + payload["d"]["intents"] = self._intents - if undefined.count(self._activity, self._status, self._idle_since, self._is_afk) != 4: - # noinspection PyTypeChecker - payload["d"]["presence"] = self._app.entity_factory.serialize_gateway_presence( - self._idle_since, self._is_afk, self._status, self._activity, - ) + if undefined.count(self._activity, self._status, self._idle_since, self._is_afk) != 4: + # noinspection PyTypeChecker + payload["d"]["presence"] = self._app.entity_factory.serialize_gateway_presence( + self._idle_since, self._is_afk, self._status, self._activity, + ) - await self._send_json(payload) + await self._send_json(payload) + + async def _resume(self) -> None: + await self._send_json( + { + "op": self._GatewayOpcode.RESUME, + "d": {"token": self._token, "seq": self._seq, "session_id": self.session_id}, + } + ) async def _heartbeat_keepalive(self) -> None: try: diff --git a/tests/hikari/net/test_gateway.py b/tests/hikari/net/test_gateway.py index 006ea273f9..0c229572f1 100644 --- a/tests/hikari/net/test_gateway.py +++ b/tests/hikari/net/test_gateway.py @@ -28,18 +28,19 @@ from hikari.models import presences from hikari.net import config from hikari.net import gateway +from hikari.utilities import undefined from tests.hikari import client_session_stub from tests.hikari import hikari_test_helpers @pytest.fixture() def http_settings(): - return mock.create_autospec(config.HTTPSettings) + return mock.create_autospec(spec=config.HTTPSettings, spec_set=True) @pytest.fixture() def proxy_settings(): - return mock.create_autospec(config.ProxySettings) + return mock.create_autospec(spec=config.ProxySettings, spec_set=True) @pytest.fixture() @@ -744,3 +745,106 @@ async def test_update_presence_transforms_all_params(self, client): ) client._send_json.assert_awaited_once_with({"op": gateway.Gateway._GatewayOpcode.PRESENCE_UPDATE, "d": result}) + + @pytest.mark.parametrize("idle_since", [undefined.UNDEFINED, datetime.datetime.now()]) + @pytest.mark.parametrize("afk", [undefined.UNDEFINED, True, False]) + @pytest.mark.parametrize( + "status", + [ + undefined.UNDEFINED, + presences.Status.DND, + presences.Status.IDLE, + presences.Status.ONLINE, + presences.Status.OFFLINE, + ], + ) + @pytest.mark.parametrize("activity", [undefined.UNDEFINED, presences.Activity(name="foo"), None]) + async def test_update_presence_ignores_undefined(self, client, idle_since, afk, status, activity): + result = object() + client_activity = mock.MagicMock() + client_idle_since = mock.MagicMock() + client_afk = mock.MagicMock() + client_status = mock.MagicMock() + + client._activity = client_activity + client._idle_since = client_idle_since + client._is_afk = client_afk + client._status = client_status + + client._app.entity_factory.serialize_gateway_presence = mock.MagicMock(return_value=result) + + await client.update_presence( + idle_since=idle_since, afk=afk, status=status, activity=activity, + ) + + client._app.entity_factory.serialize_gateway_presence.assert_called_once_with( + idle_since=idle_since if idle_since is not undefined.UNDEFINED else client_idle_since, + afk=afk if afk is not undefined.UNDEFINED else client_afk, + activity=activity if activity is not undefined.UNDEFINED else client_activity, + status=status if status is not undefined.UNDEFINED else client_status, + ) + + +@pytest.mark.asyncio +class TestUpdateVoiceState: + @pytest.fixture + def client(self, proxy_settings, http_settings): + client = hikari_test_helpers.unslot_class(gateway.Gateway)( + url="wss://gateway.discord.gg", + token="lol", + app=mock.MagicMock(), + http_settings=http_settings, + proxy_settings=proxy_settings, + shard_id=3, + shard_count=17, + ) + return hikari_test_helpers.mock_methods_on( + client, + except_=( + "update_voice_state", + "_InvalidSession", + "_Reconnect", + "_SocketClosed", + "_GatewayCloseCode", + "_GatewayOpcode", + ), + ) + + @pytest.mark.parametrize("channel", ["12345", None]) + @pytest.mark.parametrize("self_deaf", [True, False]) + @pytest.mark.parametrize("self_mute", [True, False]) + async def test_invoked(self, client, channel, self_deaf, self_mute): + await client.update_voice_state("69696", channel, self_deaf=self_deaf, self_mute=self_mute) + client._app.entity_factory.serialize_gateway_voice_state_update("69696", channel) + + async def test_serialized_result_sent_on_websocket(self, client): + payload = mock.MagicMock() + client._app.entity_factory.serialize_gateway_voice_state_update = mock.MagicMock(return_value=payload) + + await client.update_voice_state("6969420", "12345") + + client._send_json.assert_awaited_once_with( + {"op": gateway.Gateway._GatewayOpcode.VOICE_STATE_UPDATE, "d": payload} + ) + + +@pytest.mark.asyncio +class TestCloseWs: + async def test_when_connected(self, client): + client._ws = mock.create_autospec(aiohttp.ClientWebSocketResponse, spec_set=True) + + await client._close_ws(6969420, "you got yeeted") + + client._ws.close.assert_awaited_once_with(code=6969420, message=b"you got yeeted") + + async def test_when_disconnected(self, client): + client._ws = None + await client._close_ws(6969420, "you got yeeted") + # Do not expect any error or anything to happen. + assert True + + +@pytest.mark.asyncio +class TestHandshake: + # TODO: this + ... From f7953e9327e970be48fc0062e4020441e5e40972 Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Mon, 29 Jun 2020 17:40:56 +0100 Subject: [PATCH 602/922] Added logged in user to gateway shard logs. --- hikari/net/gateway.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/hikari/net/gateway.py b/hikari/net/gateway.py index fe103131de..8270d514c2 100644 --- a/hikari/net/gateway.py +++ b/hikari/net/gateway.py @@ -606,12 +606,22 @@ async def _poll_events(self) -> None: if op == self._GatewayOpcode.DISPATCH: event = message["t"] self._seq = message["s"] + if event == "READY": self.session_id = data["session_id"] - self._logger.info("connection is ready [session:%s]", self.session_id) + user_pl = data["user"] + user_id = user_pl["id"] + tag = user_pl["username"] + "#" + user_pl["discriminator"] + self._logger.info( + "shard is ready [session:%s, user_id:%s, tag:%s]", + self.session_id, + user_id, + tag, + ) self._handshake_event.set() + elif event == "RESUME": - self._logger.info("connection has resumed [session:%s, seq:%s]", self.session_id, self._seq) + self._logger.info("shard has resumed [session:%s, seq:%s]", self.session_id, self._seq) self._handshake_event.set() self._dispatch(event, data) From f15a4b1934dafed78ff1c266b19ddee6e86b0b51 Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Mon, 29 Jun 2020 18:00:34 +0100 Subject: [PATCH 603/922] Allowed unicode emoji to be comparable to string. --- hikari/models/emojis.py | 17 +++++++++++++++-- hikari/net/gateway.py | 5 +---- 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/hikari/models/emojis.py b/hikari/models/emojis.py index 9d3a8788be..5dd57fe10a 100644 --- a/hikari/models/emojis.py +++ b/hikari/models/emojis.py @@ -52,6 +52,11 @@ class Emoji(files.WebResource, abc.ABC): way as using a `hikari.utilities.files.WebResource` would achieve this. """ + @property + @abc.abstractmethod + def name(self) -> typing.Optional[str]: + """Generic name for the emoji, or the unicode representation.""" + @property @abc.abstractmethod def url(self) -> str: @@ -73,7 +78,7 @@ def mention(self) -> str: """Mention string to use to mention the emoji with.""" -@attr.s(eq=True, hash=True, init=False, kw_only=True, slots=True) +@attr.s(hash=True, init=False, kw_only=True, slots=True, eq=False) class UnicodeEmoji(Emoji): """Represents a unicode emoji. @@ -93,12 +98,19 @@ class UnicodeEmoji(Emoji): removed in a future release after a deprecation period. """ - name: str = attr.ib(eq=True, hash=True, repr=True) + name: str = attr.ib(repr=True, hash=True) """The code points that form the emoji.""" def __str__(self) -> str: return self.name + def __eq__(self, other: typing.Any) -> bool: + if isinstance(other, Emoji): + return self.name == other.name + if isinstance(other, str): + return self.name == other + return False + @property @typing.final def url_name(self) -> str: @@ -224,6 +236,7 @@ class CustomEmoji(snowflake.Unique, Emoji): ) """The ID of this entity.""" + # TODO: document when this is None, or fix it to not be optional? name: typing.Optional[str] = attr.ib(eq=False, hash=False, repr=True) """The name of the emoji.""" diff --git a/hikari/net/gateway.py b/hikari/net/gateway.py index 8270d514c2..3f06a34ba7 100644 --- a/hikari/net/gateway.py +++ b/hikari/net/gateway.py @@ -613,10 +613,7 @@ async def _poll_events(self) -> None: user_id = user_pl["id"] tag = user_pl["username"] + "#" + user_pl["discriminator"] self._logger.info( - "shard is ready [session:%s, user_id:%s, tag:%s]", - self.session_id, - user_id, - tag, + "shard is ready [session:%s, user_id:%s, tag:%s]", self.session_id, user_id, tag, ) self._handshake_event.set() From 077db3d111e1ad45141e1e23011a283504b8ff27 Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Mon, 29 Jun 2020 18:57:23 +0100 Subject: [PATCH 604/922] Added uptime and started_at properties to hikari.api.bot.IBotApp --- hikari/api/bot.py | 22 ++++++++++++++++++++++ hikari/impl/bot.py | 19 ++++++++++++++++++- 2 files changed, 40 insertions(+), 1 deletion(-) diff --git a/hikari/api/bot.py b/hikari/api/bot.py index bc5da5e012..ecd699ebb7 100644 --- a/hikari/api/bot.py +++ b/hikari/api/bot.py @@ -28,6 +28,10 @@ from hikari.api import event_dispatcher +if typing.TYPE_CHECKING: + import datetime + + class IBotApp(event_consumer.IEventConsumerApp, event_dispatcher.IEventDispatcherApp, abc.ABC): """Base for bot applications. @@ -38,3 +42,21 @@ class IBotApp(event_consumer.IEventConsumerApp, event_dispatcher.IEventDispatche """ __slots__: typing.Sequence[str] = () + + @property + @abc.abstractmethod + def uptime(self) -> datetime.timedelta: + """Return how long the bot has been alive for. + + If the application has not been started, then this will return + a `datetime.timedelta` of 0 seconds. + """ + + @property + @abc.abstractmethod + def started_at(self) -> typing.Optional[datetime.datetime]: + """Return the timestamp when the bot was started. + + If the application has not been started, then this will return + `None`. + """ diff --git a/hikari/impl/bot.py b/hikari/impl/bot.py index 2cc5eee1ef..669f5f2868 100644 --- a/hikari/impl/bot.py +++ b/hikari/impl/bot.py @@ -23,11 +23,13 @@ import asyncio import contextlib +import datetime import inspect import logging import os import platform import sys +import time import typing from hikari.api import bot @@ -41,11 +43,11 @@ from hikari.net import rate_limits from hikari.net import rest from hikari.net import strings +from hikari.utilities import date from hikari.utilities import undefined if typing.TYPE_CHECKING: import concurrent.futures - import datetime from hikari.api import cache as cache_ from hikari.api import entity_factory as entity_factory_ @@ -203,6 +205,9 @@ def __init__( self._entity_factory = entity_factory_impl.EntityFactoryComponentImpl(app=self) self._global_ratelimit = rate_limits.ManualRateLimiter() + self._started_at_monotonic: typing.Optional[float] = None + self._started_at_timestamp: typing.Optional[datetime.datetime] = None + self._executor = executor http_settings = config.HTTPSettings() if http_settings is None else http_settings @@ -271,7 +276,19 @@ def proxy_settings(self) -> config.ProxySettings: def rest(self) -> rest.REST: return self._rest + @property + def uptime(self) -> datetime.timedelta: + raw_uptime = time.perf_counter() - self._started_at_monotonic if self._started_at_monotonic is not None else 0.0 + return datetime.timedelta(seconds=raw_uptime) + + @property + def started_at(self) -> typing.Optional[datetime.datetime]: + return self._started_at_timestamp + def start(self) -> typing.Coroutine[None, typing.Any, None]: + self._started_at_monotonic = time.perf_counter() + self._started_at_timestamp = date.local_datetime() + if self._debug is True: _LOGGER.warning("debug mode is enabled, performance may be affected") From a7cbbd4bf168fe3b7ea41ad8db9566c6777e9caf Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Mon, 29 Jun 2020 19:17:53 +0100 Subject: [PATCH 605/922] Fixed flake8 documentation mood issue. --- hikari/models/emojis.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hikari/models/emojis.py b/hikari/models/emojis.py index 5dd57fe10a..c018dc17e6 100644 --- a/hikari/models/emojis.py +++ b/hikari/models/emojis.py @@ -55,7 +55,7 @@ class Emoji(files.WebResource, abc.ABC): @property @abc.abstractmethod def name(self) -> typing.Optional[str]: - """Generic name for the emoji, or the unicode representation.""" + """Return the generic name/representation for this emoji.""" @property @abc.abstractmethod From fc9d9d21c4ff02b17b81a750fc8542c8b65a9cbb Mon Sep 17 00:00:00 2001 From: davfsa Date: Mon, 29 Jun 2020 20:56:48 +0200 Subject: [PATCH 606/922] Made the linting pipelines stricter --- ci/gitlab/linting.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/ci/gitlab/linting.yml b/ci/gitlab/linting.yml index e45ce4725d..539086479a 100644 --- a/ci/gitlab/linting.yml +++ b/ci/gitlab/linting.yml @@ -35,7 +35,6 @@ ### Flake8 linter. ### flake8: - allow_failure: true artifacts: expire_in: 2 days reports: @@ -60,7 +59,6 @@ safety: ### MyPy static type checker. ### mypy: - allow_failure: true extends: .lint script: - nox -s mypy --no-error-on-external-run From e02eb4f029fc108d537e5eafddc2d228430d6154 Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Tue, 30 Jun 2020 15:04:48 +0100 Subject: [PATCH 607/922] Coalesced the 'net' module into impl. This has been done to make it easier to provide a consistent separation between API and implementation which I will be focusing more on in the next couple of days. --- .flake8 | 2 +- README.md | 6 +-- hikari/__init__.py | 3 +- hikari/api/bot.py | 3 +- hikari/api/cache.py | 2 +- hikari/api/entity_factory.py | 4 +- hikari/api/event_consumer.py | 4 +- hikari/api/gateway_zookeeper.py | 4 +- hikari/api/rest.py | 30 ++++++------- hikari/errors.py | 7 ++- hikari/events/other.py | 2 +- hikari/impl/bot.py | 20 ++++----- hikari/{net => impl}/buckets.py | 43 +++++++++---------- hikari/{net => impl}/config.py | 2 +- hikari/impl/entity_factory.py | 9 ++-- hikari/impl/event_manager.py | 2 +- hikari/impl/event_manager_core.py | 4 +- hikari/{net => impl}/gateway.py | 12 +++--- hikari/impl/gateway_zookeeper.py | 11 +++-- hikari/{net => impl}/helpers.py | 0 hikari/{net/rest.py => impl/http.py} | 36 ++++++++-------- hikari/{net => impl}/rate_limits.py | 11 +++-- hikari/impl/{rest.py => rest_app.py} | 26 +++++------ hikari/{net => impl}/routes.py | 0 hikari/{net => impl}/special_endpoints.py | 6 +-- hikari/impl/stateless_cache.py | 2 +- hikari/{net => impl}/strings.py | 0 hikari/models/applications.py | 2 +- hikari/models/channels.py | 4 +- hikari/models/emojis.py | 3 +- hikari/models/guilds.py | 8 ++-- hikari/models/invites.py | 2 +- hikari/models/users.py | 2 +- hikari/models/webhooks.py | 2 +- hikari/net/__init__.py | 25 ----------- hikari/utilities/cdn.py | 2 +- hikari/utilities/date.py | 1 - hikari/utilities/files.py | 3 +- hikari/utilities/spel.py | 1 - mypy.ini | 2 +- tests/hikari/{net => impl}/test_buckets.py | 4 +- tests/hikari/{net => impl}/test_gateway.py | 4 +- .../{net/test_rest.py => impl/test_http.py} | 0 tests/hikari/{net => impl}/test_ratelimits.py | 2 +- tests/hikari/{net => impl}/test_routes.py | 2 +- tests/hikari/net/test_rest_utils.py | 17 -------- 46 files changed, 142 insertions(+), 195 deletions(-) rename hikari/{net => impl}/buckets.py (95%) rename hikari/{net => impl}/config.py (99%) rename hikari/{net => impl}/gateway.py (99%) rename hikari/{net => impl}/helpers.py (100%) rename hikari/{net/rest.py => impl/http.py} (99%) rename hikari/{net => impl}/rate_limits.py (98%) rename hikari/impl/{rest.py => rest_app.py} (92%) rename hikari/{net => impl}/routes.py (100%) rename hikari/{net => impl}/special_endpoints.py (99%) rename hikari/{net => impl}/strings.py (100%) delete mode 100644 hikari/net/__init__.py rename tests/hikari/{net => impl}/test_buckets.py (99%) rename tests/hikari/{net => impl}/test_gateway.py (99%) rename tests/hikari/{net/test_rest.py => impl/test_http.py} (100%) rename tests/hikari/{net => impl}/test_ratelimits.py (99%) rename tests/hikari/{net => impl}/test_routes.py (98%) delete mode 100644 tests/hikari/net/test_rest_utils.py diff --git a/.flake8 b/.flake8 index fe8afc36b6..30fb97209a 100644 --- a/.flake8 +++ b/.flake8 @@ -14,7 +14,7 @@ ignore = per-file-ignores = # f-string missing prefix. - hikari/net/routes.py:FS003 + hikari/impl/routes.py:FS003 # f-string missing prefix. hikari/utilities/date.py:FS003 # complaints about importing stuff and not using it afterwards diff --git a/README.md b/README.md index 4ec7f78f3b..844db230bd 100644 --- a/README.md +++ b/README.md @@ -94,12 +94,12 @@ Most mainstream Python Discord APIs lack one or more of the following features. implement each feature as part of the design, rather than an additional component. This enables you to utilize these components as a black box where necessary. -- Low level REST API implementation. +- Low level HTTP API implementation. - Low level gateway websocket shard implementation. - Rate limiting that complies with the `X-RateLimit-Bucket` header __properly__. - Gateway websocket ratelimiting (prevents your websocket getting completely invalidated). - Intents. -- Proxy support for websockets and REST API. +- Proxy support for websockets and HTTP API. - File IO that doesn't block you. - Fluent Pythonic API that does not limit your creativity. @@ -113,7 +113,7 @@ to utilize these components as a black box where necessary. to the original format of information provided by Discord as possible ensures that minimal changes are required when a breaking API design is introduced. This reduces the amount of stuff you need to fix in your applications as a result. -- Standalone REST client. Not writing a bot, but need to use the API anyway? Simply +- Standalone HTTP client. Not writing a bot, but need to use the API anyway? Simply initialize a `hikari.RESTClient` and away you go. ### Stuff coming soon diff --git a/hikari/__init__.py b/hikari/__init__.py index 76fde1c1d0..a14fe51fce 100644 --- a/hikari/__init__.py +++ b/hikari/__init__.py @@ -34,6 +34,5 @@ from hikari._about import __license__ from hikari._about import __url__ from hikari._about import __version__ - from hikari.impl.bot import BotAppImpl as Bot -from hikari.impl.rest import RESTClientFactoryImpl as RESTClientFactory +from hikari.impl.rest_app import RESTClientFactoryImpl as RESTClientFactory diff --git a/hikari/api/bot.py b/hikari/api/bot.py index ecd699ebb7..9de499ac46 100644 --- a/hikari/api/bot.py +++ b/hikari/api/bot.py @@ -27,7 +27,6 @@ from hikari.api import event_consumer from hikari.api import event_dispatcher - if typing.TYPE_CHECKING: import datetime @@ -35,7 +34,7 @@ class IBotApp(event_consumer.IEventConsumerApp, event_dispatcher.IEventDispatcherApp, abc.ABC): """Base for bot applications. - Bots are components that have access to a REST API, an event dispatcher, + Bots are components that have access to a HTTP API, an event dispatcher, and an event consumer. Additionally, bots will contain a collection of Gateway client objects. diff --git a/hikari/api/cache.py b/hikari/api/cache.py index 28363f1016..cfc7cfe5e4 100644 --- a/hikari/api/cache.py +++ b/hikari/api/cache.py @@ -31,7 +31,7 @@ class ICacheComponent(component.IComponent, abc.ABC): """Interface describing the operations a cache component should provide. - This will be used by the gateway and REST API to cache specific types of + This will be used by the gateway and HTTP API to cache specific types of objects that the application should attempt to remember for later, depending on how this is implemented. The requirement for this stems from the assumption by Discord that bot applications will maintain some form of diff --git a/hikari/api/entity_factory.py b/hikari/api/entity_factory.py index 464d522e52..d22b3c4682 100644 --- a/hikari/api/entity_factory.py +++ b/hikari/api/entity_factory.py @@ -47,7 +47,7 @@ from hikari.models import users as user_models from hikari.models import voices as voice_models from hikari.models import webhooks as webhook_models - from hikari.net import gateway + from hikari.impl import gateway from hikari.utilities import data_binding from hikari.utilities import files from hikari.utilities import snowflake @@ -1257,7 +1257,7 @@ def deserialize_ready_event( Parameters ---------- - shard : hikari.net.gateway.Gateway + shard : hikari.impl.gateway.Gateway The shard that was ready. payload : typing.Mapping[str, typing.Any] The dict payload to parse. diff --git a/hikari/api/event_consumer.py b/hikari/api/event_consumer.py index 359de03741..cc8301c35b 100644 --- a/hikari/api/event_consumer.py +++ b/hikari/api/event_consumer.py @@ -27,7 +27,7 @@ from hikari.api import rest if typing.TYPE_CHECKING: - from hikari.net import gateway + from hikari.impl import gateway from hikari.utilities import data_binding @@ -50,7 +50,7 @@ async def consume_raw_event( Parameters ---------- - shard : hikari.net.gateway.Gateway + shard : hikari.impl.gateway.Gateway The gateway shard that emitted the event. event_name : str The event name. diff --git a/hikari/api/gateway_zookeeper.py b/hikari/api/gateway_zookeeper.py index f6871c32cf..91cdd43daf 100644 --- a/hikari/api/gateway_zookeeper.py +++ b/hikari/api/gateway_zookeeper.py @@ -31,7 +31,7 @@ import datetime from hikari.models import presences - from hikari.net import gateway + from hikari.impl import gateway class IGatewayZookeeperApp(event_consumer.IEventConsumerApp, abc.ABC): @@ -64,7 +64,7 @@ def shards(self) -> typing.Mapping[int, gateway.Gateway]: Returns ------- - typing.Mapping[int, hikari.net.gateway.Gateway] + typing.Mapping[int, hikari.impl.gateway.Gateway] The mapping of shard IDs to gateway connections for the corresponding shard. These shard IDs are 0-indexed. """ diff --git a/hikari/api/rest.py b/hikari/api/rest.py index fb5173e5e6..a6f5ab3ada 100644 --- a/hikari/api/rest.py +++ b/hikari/api/rest.py @@ -15,7 +15,7 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . -"""REST application interface.""" +"""HTTP application interface.""" from __future__ import annotations @@ -24,7 +24,7 @@ import abc import typing -from hikari.net import strings +from hikari.impl import strings if typing.TYPE_CHECKING: import concurrent.futures @@ -32,31 +32,31 @@ from hikari.api import cache as cache_ from hikari.api import entity_factory as entity_factory_ - from hikari.net import config - from hikari.net import rest as rest_ + from hikari.impl import config + from hikari.impl import http as rest_ class IRESTClient(abc.ABC): - """Component specialization that is used for REST-only applications. + """Component specialization that is used for HTTP-only applications. - This is a specific instance of a REST-only client provided by pooled + This is a specific instance of a HTTP-only client provided by pooled implementations of `IRESTClientFactory`. It may also be used by bots - as a base if they require REST-API access. + as a base if they require HTTP-API access. """ __slots__: typing.Sequence[str] = () @property @abc.abstractmethod - def rest(self) -> rest_.REST: - """REST API Client. + def rest(self) -> rest_.HTTP: + """HTTP API Client. - Use this to make calls to Discord's REST API over HTTPS. + Use this to make calls to Discord's HTTP API over HTTPS. Returns ------- - hikari.net.rest.REST - The REST API client. + hikari.impl.http.HTTP + The HTTP API client. """ @property @@ -129,7 +129,7 @@ async def __aexit__( class IRESTClientFactory(abc.ABC): """A client factory that emits clients. - This enables a connection pool to be shared for stateless REST-only + This enables a connection pool to be shared for stateless HTTP-only applications such as web dashboards, while still using the HTTP architecture that the bot system will use. """ @@ -138,7 +138,7 @@ class IRESTClientFactory(abc.ABC): @abc.abstractmethod def acquire(self, token: str, token_type: str = strings.BEARER_TOKEN) -> IRESTClientContextManager: - """Acquire a REST client for the given authentication details. + """Acquire a HTTP client for the given authentication details. Parameters ---------- @@ -150,7 +150,7 @@ def acquire(self, token: str, token_type: str = strings.BEARER_TOKEN) -> IRESTCl Returns ------- IRESTClient - The REST client to use. + The HTTP client to use. """ @abc.abstractmethod diff --git a/hikari/errors.py b/hikari/errors.py index 373efe02b9..500f561282 100644 --- a/hikari/errors.py +++ b/hikari/errors.py @@ -38,9 +38,8 @@ import http import typing - if typing.TYPE_CHECKING: - from hikari.net import routes + from hikari.impl import routes from hikari.utilities import data_binding @@ -336,13 +335,13 @@ class RateLimited(ClientHTTPErrorResponse): !!! note If you receive this regularly, please file a bug report, or contact Discord with the relevant debug information that can be obtained by - enabling debug logs and enabling the debug mode on the REST components. + enabling debug logs and enabling the debug mode on the HTTP components. Parameters ---------- url : str The URL that produced the error message. - route : hikari.net.routes.CompiledRoute + route : hikari.impl.routes.CompiledRoute The route that produced this error. headers : hikari.utilities.data_binding.Headers Any headers that were given in the response. diff --git a/hikari/events/other.py b/hikari/events/other.py index 761c9e9d22..34560bd3b3 100644 --- a/hikari/events/other.py +++ b/hikari/events/other.py @@ -41,7 +41,7 @@ if typing.TYPE_CHECKING: from hikari.models import guilds from hikari.models import users - from hikari.net import gateway as gateway_client + from hikari.impl import gateway as gateway_client from hikari.utilities import snowflake diff --git a/hikari/impl/bot.py b/hikari/impl/bot.py index 669f5f2868..1623e627c2 100644 --- a/hikari/impl/bot.py +++ b/hikari/impl/bot.py @@ -34,15 +34,15 @@ from hikari.api import bot from hikari.impl import cache as cache_impl +from hikari.impl import config from hikari.impl import entity_factory as entity_factory_impl from hikari.impl import event_manager from hikari.impl import gateway_zookeeper +from hikari.impl import http +from hikari.impl import rate_limits from hikari.impl import stateless_cache as stateless_cache_impl +from hikari.impl import strings from hikari.models import presences -from hikari.net import config -from hikari.net import rate_limits -from hikari.net import rest -from hikari.net import strings from hikari.utilities import date from hikari.utilities import undefined @@ -67,7 +67,7 @@ class BotAppImpl(gateway_zookeeper.AbstractGatewayZookeeper, bot.IBotApp): ---------- debug : bool Defaulting to `False`, if `True`, then each payload sent and received - on the gateway will be dumped to debug logs, and every REST API request + on the gateway will be dumped to debug logs, and every HTTP API request and response will also be dumped to logs. This will provide useful debugging context at the cost of performance. Generally you do not need to enable this. @@ -78,7 +78,7 @@ class BotAppImpl(gateway_zookeeper.AbstractGatewayZookeeper, bot.IBotApp): The version of the gateway to connect to. At the time of writing, only version `6` and version `7` (undocumented development release) are supported. This defaults to using v6. - http_settings : hikari.net.config.HTTPSettings or None + http_settings : hikari.impl.config.HTTPSettings or None The HTTP-related settings to use. initial_activity : hikari.models.presences.Activity or None or hikari.utilities.undefined.UndefinedType The initial activity to have on each shard. @@ -110,10 +110,10 @@ class BotAppImpl(gateway_zookeeper.AbstractGatewayZookeeper, bot.IBotApp): Initializating logging means already have a handler in the root logger. This is usually achived by calling `logging.basicConfig` or adding the handler another way. - proxy_settings : hikari.net.config.ProxySettings or None + proxy_settings : hikari.impl.config.ProxySettings or None Settings to use for the proxy. rest_version : int - The version of the REST API to connect to. At the time of writing, + The version of the HTTP API to connect to. At the time of writing, only version `6` and version `7` (undocumented development release) are supported. This defaults to v6. shard_ids : typing.Set[int] or None @@ -230,7 +230,7 @@ def __init__( version=gateway_version, ) - self._rest = rest.REST( # noqa: S106 - Possible hardcoded password + self._rest = http.HTTP( # noqa: S106 - Possible hardcoded password app=self, connector=None, connector_owner=True, @@ -273,7 +273,7 @@ def proxy_settings(self) -> config.ProxySettings: return self._proxy_settings @property - def rest(self) -> rest.REST: + def rest(self) -> http.HTTP: return self._rest @property diff --git a/hikari/net/buckets.py b/hikari/impl/buckets.py similarity index 95% rename from hikari/net/buckets.py rename to hikari/impl/buckets.py index ad23a97dd0..8f7ac4865f 100644 --- a/hikari/net/buckets.py +++ b/hikari/impl/buckets.py @@ -30,16 +30,16 @@ What is the theory behind this implementation? ---------------------------------------------- -In this module, we refer to a `hikari.net.routes.CompiledRoute` as a definition +In this module, we refer to a `hikari.impl.routes.CompiledRoute` as a definition of a route with specific major parameter values included (e.g. -`POST /channels/123/messages`), and a `hikari.net.routes.Route` as a +`POST /channels/123/messages`), and a `hikari.impl.routes.Route` as a definition of a route without specific parameter values included (e.g. `POST /channels/{channel}/messages`). We can compile a -`hikari.net.routes.CompiledRoute` from a `hikari.net.routes.Route` +`hikari.impl.routes.CompiledRoute` from a `hikari.impl.routes.Route` by providing the corresponding parameters as kwargs, as you may already know. In this module, a "bucket" is an internal data structure that tracks and -enforces the rate limit state for a specific `hikari.net.routes.CompiledRoute`, +enforces the rate limit state for a specific `hikari.impl.routes.CompiledRoute`, and can manage delaying tasks in the event that we begin to get rate limited. It also supports providing in-order execution of queued tasks. @@ -64,7 +64,7 @@ module. One issue that occurs from this is that we cannot effectively hash a -`hikari.net.routes.CompiledRoute` that has not yet been hit, meaning that +`hikari.impl.routes.CompiledRoute` that has not yet been hit, meaning that until we receive a response from this endpoint, we have no idea what our rate limits could be, nor the bucket that they sit in. This is usually not problematic, as the first request to an endpoint should never be rate limited @@ -78,13 +78,13 @@ ------------------------------------ Each time you `BaseRateLimiter.acquire()` a request timeslice for a given -`hikari.net.routes.Route`, several things happen. The first is that we +`hikari.impl.routes.Route`, several things happen. The first is that we attempt to find the existing bucket for that route, if there is one, or get an unknown bucket otherwise. This is done by creating a real bucket hash from the compiled route. The initial hash is calculated using a lookup table that maps -`hikari.net.routes.CompiledRoute` objects to their corresponding initial hash +`hikari.impl.routes.CompiledRoute` objects to their corresponding initial hash codes, or to the unknown bucket hash code if not yet known. This initial hash is -processed by the `hikari.net.routes.CompiledRoute` to provide the real bucket +processed by the `hikari.impl.routes.CompiledRoute` to provide the real bucket hash we need to get the route's bucket object internally. The `BaseRateLimiter.acquire()` method will take the bucket and acquire a new @@ -165,7 +165,7 @@ --------------------------------- As of the start of June, 2020, Discord appears to be enforcing another layer -of rate limiting logic to their REST APIs which is field-specific. This means +of rate limiting logic to their HTTP APIs which is field-specific. This means that special rate limits will also exist on some endpoints that limit based on what attributes you send in a JSON or form data payload. @@ -175,7 +175,7 @@ `"reset_after"` attribute that differs entirely to the `X-RateLimit-Reset` header. Thus, it is important to not assume the value in the 429 response for the reset time is the same as the one in the bucket headers. Hikari's -`hikari.net.rest.REST` implementation specifically uses the value furthest +`hikari.impl.http.HTTP` implementation specifically uses the value furthest in the future when working out which bucket to adhere to. It is worth remembering that there is an API limit to the number of 401s, @@ -193,7 +193,7 @@ These implementations rely on Discord sending consistent buckets back to us. -This also begins to crumble if more than one REST client is in use, since +This also begins to crumble if more than one HTTP client is in use, since there is no performant way to communicate shared rate limits between distributed applications. The general concept to follow is that if you are making repeated API calls, or calls that are not event-based (e.g. @@ -212,21 +212,20 @@ import types import typing -from hikari.net import rate_limits -from hikari.net import routes +from hikari.impl import rate_limits +from hikari.impl import routes from hikari.utilities import aio UNKNOWN_HASH: typing.Final[str] = "UNKNOWN" """The hash used for an unknown bucket that has not yet been resolved.""" - -_LOGGER: typing.Final[logging.Logger] = logging.getLogger("hikari.net.rest") +_LOGGER: typing.Final[logging.Logger] = logging.getLogger("hikari.impl.http") class RESTBucket(rate_limits.WindowedBurstRateLimiter): - """Represents a rate limit for an REST endpoint. + """Represents a rate limit for an HTTP endpoint. - Component to represent an active rate limit bucket on a specific REST _route + Component to represent an active rate limit bucket on a specific HTTP _route with a specific major parameter combo. This is somewhat similar to the `WindowedBurstRateLimiter` in how it @@ -309,9 +308,9 @@ def drip(self) -> None: class RESTBucketManager: - """The main rate limiter implementation for REST clients. + """The main rate limiter implementation for HTTP clients. - This is designed to provide bucketed rate limiting for Discord REST + This is designed to provide bucketed rate limiting for Discord HTTP endpoints that respects the `X-RateLimit-Bucket` rate limit header. To do this, it makes the assumption that any limit can change at any time. """ @@ -487,7 +486,7 @@ def acquire(self, compiled_route: routes.CompiledRoute) -> asyncio.Future[None]: Parameters ---------- - compiled_route : hikari.net.routes.CompiledRoute + compiled_route : hikari.impl.routes.CompiledRoute The _route to get the bucket for. Returns @@ -499,7 +498,7 @@ def acquire(self, compiled_route: routes.CompiledRoute) -> asyncio.Future[None]: !!! note The returned future MUST be awaited, and will complete when your turn to make a call comes along. You are expected to await this and - then immediately make your REST call. The returned future may + then immediately make your HTTP call. The returned future may already be completed if you can make the call immediately. """ # Returns a future to await on to wait to be allowed to send the request, and a @@ -537,7 +536,7 @@ def update_rate_limits( Parameters ---------- - compiled_route : hikari.net.routes.CompiledRoute + compiled_route : hikari.impl.routes.CompiledRoute The compiled _route to get the bucket for. bucket_header : str, optional The `X-RateLimit-Bucket` header that was provided in the response. diff --git a/hikari/net/config.py b/hikari/impl/config.py similarity index 99% rename from hikari/net/config.py rename to hikari/impl/config.py index 97814e30cf..b0c49f0e41 100644 --- a/hikari/net/config.py +++ b/hikari/impl/config.py @@ -31,7 +31,7 @@ import attr -from hikari.net import strings +from hikari.impl import strings from hikari.utilities import data_binding diff --git a/hikari/impl/entity_factory.py b/hikari/impl/entity_factory.py index f781d9e928..21f31c6f47 100644 --- a/hikari/impl/entity_factory.py +++ b/hikari/impl/entity_factory.py @@ -15,7 +15,7 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . -"""Basic implementation of an entity factory for general bots and REST apps.""" +"""Basic implementation of an entity factory for general bots and HTTP apps.""" from __future__ import annotations @@ -31,6 +31,7 @@ from hikari.events import message as message_events from hikari.events import other as other_events from hikari.events import voice as voice_events +from hikari.impl import gateway from hikari.models import applications as application_models from hikari.models import audit_logs as audit_log_models from hikari.models import channels as channel_models @@ -46,9 +47,8 @@ from hikari.models import users as user_models from hikari.models import voices as voice_models from hikari.models import webhooks as webhook_models -from hikari.utilities import files -from hikari.net import gateway from hikari.utilities import date +from hikari.utilities import files from hikari.utilities import snowflake from hikari.utilities import undefined @@ -87,7 +87,8 @@ def __init__(self, app: rest.IRESTClient) -> None: audit_log_models.AuditLogChangeKey.MFA_LEVEL: guild_models.GuildMFALevel, audit_log_models.AuditLogChangeKey.VERIFICATION_LEVEL: guild_models.GuildVerificationLevel, audit_log_models.AuditLogChangeKey.EXPLICIT_CONTENT_FILTER: guild_models.GuildExplicitContentFilterLevel, - audit_log_models.AuditLogChangeKey.DEFAULT_MESSAGE_NOTIFICATIONS: guild_models.GuildMessageNotificationsLevel, # noqa: E501 - Line too long + audit_log_models.AuditLogChangeKey.DEFAULT_MESSAGE_NOTIFICATIONS: guild_models.GuildMessageNotificationsLevel, + # noqa: E501 - Line too long audit_log_models.AuditLogChangeKey.PRUNE_DELETE_DAYS: _deserialize_day_timedelta, audit_log_models.AuditLogChangeKey.WIDGET_CHANNEL_ID: snowflake.Snowflake, audit_log_models.AuditLogChangeKey.POSITION: int, diff --git a/hikari/impl/event_manager.py b/hikari/impl/event_manager.py index e0e3bd50aa..28a8e8296c 100644 --- a/hikari/impl/event_manager.py +++ b/hikari/impl/event_manager.py @@ -27,7 +27,7 @@ from hikari.impl import event_manager_core if typing.TYPE_CHECKING: - from hikari.net import gateway + from hikari.impl import gateway from hikari.utilities import data_binding diff --git a/hikari/impl/event_manager_core.py b/hikari/impl/event_manager_core.py index 0824b18d66..d2ce433e8a 100644 --- a/hikari/impl/event_manager_core.py +++ b/hikari/impl/event_manager_core.py @@ -30,7 +30,7 @@ from hikari.api import event_dispatcher from hikari.events import base from hikari.events import other -from hikari.net import gateway +from hikari.impl import gateway from hikari.utilities import aio from hikari.utilities import data_binding from hikari.utilities import reflect @@ -39,10 +39,8 @@ if typing.TYPE_CHECKING: from hikari.api import rest - _LOGGER: typing.Final[logging.Logger] = logging.getLogger("hikari") - if typing.TYPE_CHECKING: EventT = typing.TypeVar("EventT", bound=base.Event, contravariant=True) PredicateT = typing.Callable[[EventT], typing.Union[bool, typing.Coroutine[None, typing.Any, bool]]] diff --git a/hikari/net/gateway.py b/hikari/impl/gateway.py similarity index 99% rename from hikari/net/gateway.py rename to hikari/impl/gateway.py index 3f06a34ba7..ef998084bb 100644 --- a/hikari/net/gateway.py +++ b/hikari/impl/gateway.py @@ -34,9 +34,9 @@ import attr from hikari import errors +from hikari.impl import rate_limits +from hikari.impl import strings from hikari.models import presences -from hikari.net import rate_limits -from hikari.net import strings from hikari.utilities import data_binding from hikari.utilities import undefined @@ -44,7 +44,7 @@ import datetime from hikari.api import event_consumer - from hikari.net import config + from hikari.impl import config from hikari.models import channels from hikari.models import guilds from hikari.models import intents as intents_ @@ -61,7 +61,7 @@ class Gateway: debug : bool If `True`, each sent and received payload is dumped to the logs. If `False`, only the fact that data has been sent/received will be logged. - http_settings : hikari.net.config.HTTPSettings + http_settings : hikari.impl.config.HTTPSettings The HTTP-related settings to use while negotiating a websocket. initial_activity : hikari.models.presences.Activity or None or hikari.utilities.undefined.UndefinedType The initial activity to appear to have for this shard. @@ -75,7 +75,7 @@ class Gateway: Collection of intents to use, or `None` to not use intents at all. large_threshold : int The number of members to have in a guild for it to be considered large. - proxy_settings : hikari.net.config.ProxySettings + proxy_settings : hikari.impl.config.ProxySettings The proxy settings to use while negotiating a websocket. shard_id : int The shard ID. @@ -201,7 +201,7 @@ def __init__( self._intents: typing.Optional[intents_.Intent] = intents self._is_afk: typing.Union[undefined.UndefinedType, bool] = initial_is_afk self._last_run_started_at = float("nan") - self._logger = logging.getLogger(f"hikari.net.gateway.{shard_id}") + self._logger = logging.getLogger(f"hikari.gateway.{shard_id}") self._proxy_settings = proxy_settings self._request_close_event = asyncio.Event() self._seq: typing.Optional[str] = None diff --git a/hikari/impl/gateway_zookeeper.py b/hikari/impl/gateway_zookeeper.py index ba86bc78bb..8f9a305f16 100644 --- a/hikari/impl/gateway_zookeeper.py +++ b/hikari/impl/gateway_zookeeper.py @@ -34,18 +34,17 @@ from hikari.api import event_dispatcher from hikari.api import gateway_zookeeper from hikari.events import other -from hikari.net import gateway +from hikari.impl import gateway from hikari.utilities import aio from hikari.utilities import undefined if typing.TYPE_CHECKING: from hikari.events import base as base_events - from hikari.net import config + from hikari.impl import config from hikari.models import gateway as gateway_models from hikari.models import intents as intents_ from hikari.models import presences - _LOGGER: typing.Final[logging.Logger] = logging.getLogger("hikari") @@ -58,7 +57,7 @@ class AbstractGatewayZookeeper(gateway_zookeeper.IGatewayZookeeperApp, abc.ABC): ability to manage sharding. !!! note - This does not provide REST API functionality. + This does not provide HTTP API functionality. Parameters ---------- @@ -70,7 +69,7 @@ class AbstractGatewayZookeeper(gateway_zookeeper.IGatewayZookeeperApp, abc.ABC): on the gateway will be dumped to debug logs. This will provide useful debugging context at the cost of performance. Generally you do not need to enable this. - http_settings : hikari.net.config.HTTPSettings + http_settings : hikari.impl.config.HTTPSettings HTTP-related configuration. initial_activity : hikari.models.presences.Activity or None or hikari.utilities.undefined.UndefinedType The initial activity to have on each shard. @@ -89,7 +88,7 @@ class AbstractGatewayZookeeper(gateway_zookeeper.IGatewayZookeeperApp, abc.ABC): large_threshold : int The number of members that need to be in a guild for the guild to be considered large. Defaults to the maximum, which is `250`. - proxy_settings : hikari.net.config.ProxySettings + proxy_settings : hikari.impl.config.ProxySettings Proxy-related configuration. shard_ids : typing.Set[int] or None A set of every shard ID that should be created and started on startup. diff --git a/hikari/net/helpers.py b/hikari/impl/helpers.py similarity index 100% rename from hikari/net/helpers.py rename to hikari/impl/helpers.py diff --git a/hikari/net/rest.py b/hikari/impl/http.py similarity index 99% rename from hikari/net/rest.py rename to hikari/impl/http.py index 87730f6200..2be6db696d 100644 --- a/hikari/net/rest.py +++ b/hikari/impl/http.py @@ -15,11 +15,11 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . -"""Implementation of a V6 and V7 compatible REST API for Discord.""" +"""Implementation of a V6 and V7 compatible HTTP API for Discord.""" from __future__ import annotations -__all__: typing.Final[typing.Sequence[str]] = ["REST"] +__all__: typing.Final[typing.Sequence[str]] = ["HTTP"] import asyncio import contextlib @@ -34,15 +34,15 @@ import aiohttp from hikari import errors +from hikari.impl import buckets +from hikari.impl import config +from hikari.impl import helpers +from hikari.impl import rate_limits +from hikari.impl import routes +from hikari.impl import special_endpoints +from hikari.impl import strings from hikari.models import embeds as embeds_ from hikari.models import emojis -from hikari.net import buckets -from hikari.net import config -from hikari.net import helpers -from hikari.net import rate_limits -from hikari.net import routes -from hikari.net import special_endpoints -from hikari.net import strings from hikari.utilities import data_binding from hikari.utilities import date from hikari.utilities import files @@ -66,13 +66,13 @@ from hikari.models import voices from hikari.models import webhooks -_LOGGER: typing.Final[logging.Logger] = logging.getLogger("hikari.net.rest") +_LOGGER: typing.Final[logging.Logger] = logging.getLogger("hikari.impl.http") # TODO: make a mechanism to allow me to share the same client session but -# use various tokens for REST-only apps. -class REST: - """Implementation of the V6 and V7-compatible Discord REST API. +# use various tokens for HTTP-only apps. +class HTTP: + """Implementation of the V6 and V7-compatible Discord HTTP API. This manages making HTTP/1.1 requests to the API and using the entity factory within the passed application instance to deserialize JSON responses @@ -81,7 +81,7 @@ class REST: Parameters ---------- app : hikari.api.rest.IRESTClient - The REST application containing all other application components + The HTTP application containing all other application components that Hikari uses. debug : bool If `True`, this will enable logging of each payload sent and received, @@ -89,7 +89,7 @@ class REST: information useful for debugging this application. These logs will be written as DEBUG log entries. For most purposes, this should be left `False`. - global_ratelimit : hikari.net.rate_limits.ManualRateLimiter + global_ratelimit : hikari.impl.rate_limits.ManualRateLimiter The shared ratelimiter to use for the application. token : str or hikari.utilities.undefined.UndefinedType The bot or bearer token. If no token is to be used, @@ -98,7 +98,7 @@ class REST: The type of token in use. If no token is used, this can be ignored and left to the default value. This can be `"Bot"` or `"Bearer"`. rest_url : str - The REST API base URL. This can contain format-string specifiers to + The HTTP API base URL. This can contain format-string specifiers to interpolate information such as API version in use. version : int The API version to use. @@ -429,7 +429,7 @@ def _generate_allowed_mentions( @typing.final async def close(self) -> None: - """Close the REST client and any open HTTP connections.""" + """Close the HTTP client and any open HTTP connections.""" if self._client_session is not None: await self._client_session.close() self.buckets.close() @@ -815,7 +815,7 @@ def trigger_typing( Returns ------- - hikari.net.special_endpoints.TypingIndicator + hikari.impl.special_endpoints.TypingIndicator A typing indicator to use. Raises diff --git a/hikari/net/rate_limits.py b/hikari/impl/rate_limits.py similarity index 98% rename from hikari/net/rate_limits.py rename to hikari/impl/rate_limits.py index 9b11dcc387..e5c730ddc5 100644 --- a/hikari/net/rate_limits.py +++ b/hikari/impl/rate_limits.py @@ -17,7 +17,7 @@ # along with Hikari. If not, see . """Basic lazy ratelimit systems for asyncio. -See `hikari.net.buckets` for REST-specific rate-limiting logic. +See `hikari.impl.buckets` for HTTP-specific rate-limiting logic. """ from __future__ import annotations @@ -40,8 +40,7 @@ if typing.TYPE_CHECKING: import types - -_LOGGER: typing.Final[logging.Logger] = logging.getLogger("hikari.net.ratelimits") +_LOGGER: typing.Final[logging.Logger] = logging.getLogger("hikari.impl.ratelimits") class BaseRateLimiter(abc.ABC): @@ -147,7 +146,7 @@ def is_empty(self) -> bool: @typing.final class ManualRateLimiter(BurstRateLimiter): - """Rate limit handler for the global REST rate limit. + """Rate limit handler for the global HTTP rate limit. This is a non-preemptive rate limiting algorithm that will always return completed futures until `ManualRateLimiter.throttle` is invoked. Once this @@ -160,8 +159,8 @@ class ManualRateLimiter(BurstRateLimiter): Triggering a throttle when it is already set will cancel the current throttle task that is sleeping and replace it. - This is used to enforce the global REST rate limit that will occur - "randomly" during REST API interaction. + This is used to enforce the global HTTP rate limit that will occur + "randomly" during HTTP API interaction. Expect random occurrences. """ diff --git a/hikari/impl/rest.py b/hikari/impl/rest_app.py similarity index 92% rename from hikari/impl/rest.py rename to hikari/impl/rest_app.py index 3cff4ebd63..27c3fb1930 100644 --- a/hikari/impl/rest.py +++ b/hikari/impl/rest_app.py @@ -15,7 +15,7 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Hikari. If not, see . -"""Implementation of a REST application. +"""Implementation of a HTTP application. This provides functionality for projects that only need to use the RESTful API, such as web dashboards and other OAuth2-based scripts. @@ -30,12 +30,12 @@ import aiohttp from hikari.api import rest as rest_api +from hikari.impl import config from hikari.impl import entity_factory as entity_factory_impl +from hikari.impl import http as rest_component +from hikari.impl import rate_limits from hikari.impl import stateless_cache -from hikari.net import config -from hikari.net import rate_limits -from hikari.net import rest as rest_component -from hikari.net import strings +from hikari.impl import strings if typing.TYPE_CHECKING: import concurrent.futures @@ -46,7 +46,7 @@ class RESTClientImpl(rest_api.IRESTClientContextManager): - """Client for a specific set of credentials within a REST-only application. + """Client for a specific set of credentials within a HTTP-only application. Parameters ---------- @@ -59,11 +59,11 @@ class RESTClientImpl(rest_api.IRESTClientContextManager): The AIOHTTP connector to use. This must be closed by the caller, and will not be terminated when this class closes (since you will generally expect this to be a connection pool). - global_ratelimit : hikari.net.rate_limits.ManualRateLimiter + global_ratelimit : hikari.impl.rate_limits.ManualRateLimiter The global ratelimiter. - http_settings : hikari.net.config.HTTPSettings + http_settings : hikari.impl.config.HTTPSettings HTTP-related settings. - proxy_settings : hikari.net.config.ProxySettings + proxy_settings : hikari.impl.config.ProxySettings Proxy-related settings. token : str or None If defined, the token to use. If not defined, no token will be injected @@ -98,7 +98,7 @@ def __init__( self._http_settings = http_settings self._proxy_settings = proxy_settings - self._rest = rest_component.REST( + self._rest = rest_component.HTTP( app=self, connector=connector, connector_owner=False, @@ -117,7 +117,7 @@ def cache(self) -> cache_.ICacheComponent: """Return the cache component. !!! warn - This will always return `NotImplemented` for REST-only applications. + This will always return `NotImplemented` for HTTP-only applications. """ return self._cache @@ -138,7 +138,7 @@ def proxy_settings(self) -> config.ProxySettings: return self._proxy_settings @property - def rest(self) -> rest_component.REST: + def rest(self) -> rest_component.HTTP: return self._rest async def close(self) -> None: @@ -157,7 +157,7 @@ async def __aexit__( class RESTClientFactoryImpl(rest_api.IRESTClientFactory): - """The base for a REST-only Discord application. + """The base for a HTTP-only Discord application. This comprises of a shared TCP connector connection pool, and can have `hikari.api.rest.IRESTClient` instances for specific credentials acquired diff --git a/hikari/net/routes.py b/hikari/impl/routes.py similarity index 100% rename from hikari/net/routes.py rename to hikari/impl/routes.py diff --git a/hikari/net/special_endpoints.py b/hikari/impl/special_endpoints.py similarity index 99% rename from hikari/net/special_endpoints.py rename to hikari/impl/special_endpoints.py index d7184d9e80..60387a48c9 100644 --- a/hikari/net/special_endpoints.py +++ b/hikari/impl/special_endpoints.py @@ -30,7 +30,7 @@ import attr -from hikari.net import routes +from hikari.impl import routes from hikari.utilities import data_binding from hikari.utilities import date from hikari.utilities import iterators @@ -106,12 +106,12 @@ async def _keep_typing(self) -> None: class GuildBuilder: """A helper class used to construct a prototype for a guild. - This is used to create a guild in a tidy way using the REST API, since + This is used to create a guild in a tidy way using the HTTP API, since the logic behind creating a guild on an API level is somewhat confusing and detailed. !!! note - This is a helper class that is used by `hikari.net.rest.REST`. + This is a helper class that is used by `hikari.impl.http.HTTP`. You should only ever need to use instances of this class that are produced by that API, thus, any details about the constructor are omitted from the following examples for brevity. diff --git a/hikari/impl/stateless_cache.py b/hikari/impl/stateless_cache.py index 25460f1278..0afc5b1f75 100644 --- a/hikari/impl/stateless_cache.py +++ b/hikari/impl/stateless_cache.py @@ -17,7 +17,7 @@ # along with Hikari. If not, see . """Barebones implementation of a cache that never stores anything. -This is used to enable compatibility with REST applications and stateless +This is used to enable compatibility with HTTP applications and stateless bots where desired. """ from __future__ import annotations diff --git a/hikari/net/strings.py b/hikari/impl/strings.py similarity index 100% rename from hikari/net/strings.py rename to hikari/impl/strings.py diff --git a/hikari/models/applications.py b/hikari/models/applications.py index 44c3947d76..f894c633f1 100644 --- a/hikari/models/applications.py +++ b/hikari/models/applications.py @@ -36,8 +36,8 @@ import attr from hikari.models import guilds -from hikari.utilities import files from hikari.utilities import cdn +from hikari.utilities import files from hikari.utilities import snowflake if typing.TYPE_CHECKING: diff --git a/hikari/models/channels.py b/hikari/models/channels.py index 3f2e64b01a..f3531e9def 100644 --- a/hikari/models/channels.py +++ b/hikari/models/channels.py @@ -43,8 +43,8 @@ from hikari.models import permissions from hikari.models import users -from hikari.utilities import files from hikari.utilities import cdn +from hikari.utilities import files from hikari.utilities import snowflake from hikari.utilities import undefined @@ -158,7 +158,7 @@ def unset(self) -> permissions.Permission: class PartialChannel(snowflake.Unique): """Channel representation for cases where further detail is not provided. - This is commonly received in REST API responses where full information is + This is commonly received in HTTP API responses where full information is not available from Discord. """ diff --git a/hikari/models/emojis.py b/hikari/models/emojis.py index c018dc17e6..8f9818fe89 100644 --- a/hikari/models/emojis.py +++ b/hikari/models/emojis.py @@ -27,15 +27,14 @@ import attr -from hikari.utilities import files from hikari.utilities import cdn +from hikari.utilities import files from hikari.utilities import snowflake if typing.TYPE_CHECKING: from hikari.api import rest from hikari.models import users - _TWEMOJI_PNG_BASE_URL: typing.Final[str] = "https://github.com/twitter/twemoji/raw/master/assets/72x72/" """URL for Twemoji PNG artwork for built-in emojis.""" diff --git a/hikari/models/guilds.py b/hikari/models/guilds.py index d51608fdd5..c83dc4c5bc 100644 --- a/hikari/models/guilds.py +++ b/hikari/models/guilds.py @@ -48,8 +48,8 @@ import attr from hikari.models import users -from hikari.utilities import files from hikari.utilities import cdn +from hikari.utilities import files from hikari.utilities import snowflake if typing.TYPE_CHECKING: @@ -652,7 +652,7 @@ class Guild(PartialGuild): This will not take into account permission overwrites or implied permissions (for example, `ADMINISTRATOR` implies all other permissions). - This will be `None` when this object is retrieved through a REST request + This will be `None` when this object is retrieved through a HTTP request rather than from the gateway. """ @@ -909,7 +909,7 @@ class Guild(PartialGuild): approximate_member_count: typing.Optional[int] = attr.ib(eq=False, hash=False, repr=False) """The approximate number of members in the guild. - This information will be provided by REST API calls fetching the guilds that + This information will be provided by HTTP API calls fetching the guilds that a bot account is in. For all other purposes, this should be expected to remain `None`. """ @@ -917,7 +917,7 @@ class Guild(PartialGuild): approximate_active_member_count: typing.Optional[int] = attr.ib(eq=False, hash=False, repr=False) """The approximate number of members in the guild that are not offline. - This information will be provided by REST API calls fetching the guilds that + This information will be provided by HTTP API calls fetching the guilds that a bot account is in. For all other purposes, this should be expected to remain `None`. """ diff --git a/hikari/models/invites.py b/hikari/models/invites.py index 8f4bc08e13..35d878b667 100644 --- a/hikari/models/invites.py +++ b/hikari/models/invites.py @@ -33,8 +33,8 @@ import attr from hikari.models import guilds -from hikari.utilities import files from hikari.utilities import cdn +from hikari.utilities import files from hikari.utilities import snowflake if typing.TYPE_CHECKING: diff --git a/hikari/models/users.py b/hikari/models/users.py index 73724801cf..0cc54a0813 100644 --- a/hikari/models/users.py +++ b/hikari/models/users.py @@ -26,8 +26,8 @@ import attr -from hikari.utilities import files from hikari.utilities import cdn +from hikari.utilities import files from hikari.utilities import snowflake from hikari.utilities import undefined diff --git a/hikari/models/webhooks.py b/hikari/models/webhooks.py index 8480026e15..6908bfe385 100644 --- a/hikari/models/webhooks.py +++ b/hikari/models/webhooks.py @@ -26,8 +26,8 @@ import attr -from hikari.utilities import files as files_ from hikari.utilities import cdn +from hikari.utilities import files as files_ from hikari.utilities import snowflake from hikari.utilities import undefined diff --git a/hikari/net/__init__.py b/hikari/net/__init__.py deleted file mode 100644 index a617151012..0000000000 --- a/hikari/net/__init__.py +++ /dev/null @@ -1,25 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright © Nekoka.tt 2019-2020 -# -# This file is part of Hikari. -# -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . -"""Communication tools for Discord's network-level API endpoints.""" - -from __future__ import annotations - -__all__: typing.Final[typing.Sequence[str]] = [] - -# noinspection PyUnresolvedReferences -import typing diff --git a/hikari/utilities/cdn.py b/hikari/utilities/cdn.py index 3019f0d6dc..b8d8875118 100644 --- a/hikari/utilities/cdn.py +++ b/hikari/utilities/cdn.py @@ -24,8 +24,8 @@ import typing import urllib.parse +from hikari.impl import strings from hikari.utilities import files -from hikari.net import strings def generate_cdn_url(*route_parts: str, format_: str, size: typing.Optional[int]) -> files.URL: diff --git a/hikari/utilities/date.py b/hikari/utilities/date.py index 498901214a..57441e7047 100644 --- a/hikari/utilities/date.py +++ b/hikari/utilities/date.py @@ -36,7 +36,6 @@ import re import typing - TimeSpan = typing.Union[int, float, datetime.timedelta] """Type hint representing a naive time period or time span. diff --git a/hikari/utilities/files.py b/hikari/utilities/files.py index 3b22dc2da3..ab48d998b7 100644 --- a/hikari/utilities/files.py +++ b/hikari/utilities/files.py @@ -48,12 +48,11 @@ import aiohttp.client import attr -from hikari.net import helpers +from hikari.impl import helpers if typing.TYPE_CHECKING: import types - _LOGGER: typing.Final[logging.Logger] = logging.getLogger(__name__) _MAGIC: typing.Final[int] = 50 * 1024 diff --git a/hikari/utilities/spel.py b/hikari/utilities/spel.py index 61a7fef932..785066b434 100644 --- a/hikari/utilities/spel.py +++ b/hikari/utilities/spel.py @@ -50,7 +50,6 @@ import operator import typing - InputValueT = typing.TypeVar("InputValueT") ReturnValueT = typing.TypeVar("ReturnValueT") diff --git a/mypy.ini b/mypy.ini index 523f7bbc3f..6932e9a1a8 100644 --- a/mypy.ini +++ b/mypy.ini @@ -28,7 +28,7 @@ warn_unused_configs = true warn_unused_ignores = true -[mypy-hikari.net.gateway] +[mypy-hikari.impl.gateway] disallow_any_explicit = false no_implicit_optional = false strict_optional = false diff --git a/tests/hikari/net/test_buckets.py b/tests/hikari/impl/test_buckets.py similarity index 99% rename from tests/hikari/net/test_buckets.py rename to tests/hikari/impl/test_buckets.py index ea886ad4af..e0d2aafb1b 100644 --- a/tests/hikari/net/test_buckets.py +++ b/tests/hikari/impl/test_buckets.py @@ -22,8 +22,8 @@ import mock import pytest -from hikari.net import buckets -from hikari.net import routes +from hikari.impl import buckets +from hikari.impl import routes from tests.hikari import hikari_test_helpers diff --git a/tests/hikari/net/test_gateway.py b/tests/hikari/impl/test_gateway.py similarity index 99% rename from tests/hikari/net/test_gateway.py rename to tests/hikari/impl/test_gateway.py index 0c229572f1..dacb1f930f 100644 --- a/tests/hikari/net/test_gateway.py +++ b/tests/hikari/impl/test_gateway.py @@ -26,8 +26,8 @@ from hikari import errors from hikari.models import presences -from hikari.net import config -from hikari.net import gateway +from hikari.impl import config +from hikari.impl import gateway from hikari.utilities import undefined from tests.hikari import client_session_stub from tests.hikari import hikari_test_helpers diff --git a/tests/hikari/net/test_rest.py b/tests/hikari/impl/test_http.py similarity index 100% rename from tests/hikari/net/test_rest.py rename to tests/hikari/impl/test_http.py diff --git a/tests/hikari/net/test_ratelimits.py b/tests/hikari/impl/test_ratelimits.py similarity index 99% rename from tests/hikari/net/test_ratelimits.py rename to tests/hikari/impl/test_ratelimits.py index 2b21eac08f..da6becbd64 100644 --- a/tests/hikari/net/test_ratelimits.py +++ b/tests/hikari/impl/test_ratelimits.py @@ -25,7 +25,7 @@ import mock import pytest -from hikari.net import rate_limits +from hikari.impl import rate_limits from tests.hikari import hikari_test_helpers diff --git a/tests/hikari/net/test_routes.py b/tests/hikari/impl/test_routes.py similarity index 98% rename from tests/hikari/net/test_routes.py rename to tests/hikari/impl/test_routes.py index 24710e62bf..d5f8182e18 100644 --- a/tests/hikari/net/test_routes.py +++ b/tests/hikari/impl/test_routes.py @@ -18,7 +18,7 @@ import pytest import mock -from hikari.net import routes +from hikari.impl import routes class TestCompiledRoute: diff --git a/tests/hikari/net/test_rest_utils.py b/tests/hikari/net/test_rest_utils.py deleted file mode 100644 index 3b080b938f..0000000000 --- a/tests/hikari/net/test_rest_utils.py +++ /dev/null @@ -1,17 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright © Nekoka.tt 2019-2020 -# -# This file is part of Hikari. -# -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . From 10c7fa75000c5f32fcbfa8cbe46b6a66f9093e95 Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Tue, 30 Jun 2020 16:07:29 +0100 Subject: [PATCH 608/922] Added API components for old net modules, added wireframe for voice support. --- .flake8 | 3 +- hikari/__init__.py | 2 +- hikari/api/cache.py | 2 - hikari/api/component.py | 8 +- hikari/api/entity_factory.py | 6 +- hikari/api/event_consumer.py | 10 +- hikari/api/gateway.py | 118 ++ hikari/api/gateway_zookeeper.py | 6 +- hikari/api/{rest.py => rest_app.py} | 30 +- hikari/api/rest_client.py | 1576 +++++++++++++++++ hikari/api/special_endpoints.py | 398 +++++ hikari/api/voice.py | 57 + hikari/{impl => }/config.py | 8 +- hikari/events/channel.py | 10 +- hikari/events/guild.py | 20 +- hikari/events/message.py | 10 +- hikari/events/other.py | 10 +- hikari/impl/bot.py | 23 +- hikari/impl/cache.py | 6 +- hikari/impl/{strings.py => constants.py} | 0 hikari/impl/entity_factory.py | 11 +- hikari/impl/event_manager.py | 80 +- hikari/impl/event_manager_core.py | 10 +- hikari/impl/gateway.py | 62 +- hikari/impl/gateway_zookeeper.py | 17 +- hikari/impl/rate_limits.py | 2 +- .../impl/{helpers.py => response_handler.py} | 0 hikari/impl/rest_app.py | 31 +- hikari/impl/{http.py => rest_client.py} | 756 +------- hikari/impl/special_endpoints.py | 280 +-- hikari/models/applications.py | 8 +- hikari/models/audit_logs.py | 4 +- hikari/models/channels.py | 4 +- hikari/models/emojis.py | 4 +- hikari/models/guilds.py | 10 +- hikari/models/invites.py | 6 +- hikari/models/messages.py | 6 +- hikari/models/presences.py | 4 +- hikari/models/users.py | 4 +- hikari/models/voices.py | 4 +- hikari/models/webhooks.py | 4 +- hikari/utilities/cdn.py | 4 +- hikari/utilities/files.py | 4 +- tests/hikari/impl/test_entity_factory.py | 6 +- tests/hikari/impl/test_gateway.py | 56 +- 45 files changed, 2439 insertions(+), 1241 deletions(-) create mode 100644 hikari/api/gateway.py rename hikari/api/{rest.py => rest_app.py} (85%) create mode 100644 hikari/api/rest_client.py create mode 100644 hikari/api/special_endpoints.py create mode 100644 hikari/api/voice.py rename hikari/{impl => }/config.py (94%) rename hikari/impl/{strings.py => constants.py} (100%) rename hikari/impl/{helpers.py => response_handler.py} (100%) rename hikari/impl/{http.py => rest_client.py} (71%) diff --git a/.flake8 b/.flake8 index 30fb97209a..b8af0cfee7 100644 --- a/.flake8 +++ b/.flake8 @@ -21,7 +21,8 @@ per-file-ignores = hikari/__init__.py:F401 max-complexity = 20 -max-line-length = 120 +# Technically this is 120, but black has a policy of "1 or 2 over is fine if it is tidier", so we have to raise this. +max-line-length = 130 show_source = False statistics = False diff --git a/hikari/__init__.py b/hikari/__init__.py index a14fe51fce..77d41d9dcf 100644 --- a/hikari/__init__.py +++ b/hikari/__init__.py @@ -35,4 +35,4 @@ from hikari._about import __url__ from hikari._about import __version__ from hikari.impl.bot import BotAppImpl as Bot -from hikari.impl.rest_app import RESTClientFactoryImpl as RESTClientFactory +from hikari.impl.rest_app import RESTAppFactoryImpl as RESTClientFactory diff --git a/hikari/api/cache.py b/hikari/api/cache.py index cfc7cfe5e4..73cf56c877 100644 --- a/hikari/api/cache.py +++ b/hikari/api/cache.py @@ -21,8 +21,6 @@ __all__: typing.Final[typing.Sequence[str]] = ["ICacheComponent"] import abc - -# noinspection PyUnresolvedReferences import typing from hikari.api import component diff --git a/hikari/api/component.py b/hikari/api/component.py index 5b97f71189..e511d6bdfa 100644 --- a/hikari/api/component.py +++ b/hikari/api/component.py @@ -25,14 +25,14 @@ import typing if typing.TYPE_CHECKING: - from hikari.api import rest + from hikari.api import rest_app class IComponent(abc.ABC): """A component that makes up part of the application. Objects that derive from this should usually be attributes on the - `hikari.api.rest.IRESTClient` object. + `hikari.api.rest.IRESTApp` object. Examples -------- @@ -46,11 +46,11 @@ class IComponent(abc.ABC): @property @abc.abstractmethod - def app(self) -> rest.IRESTClient: + def app(self) -> rest_app.IRESTApp: """Return the Application that owns this component. Returns ------- - hikari.api.rest.IRESTClient + hikari.api.rest_app.IRESTApp The application implementation that owns this component. """ diff --git a/hikari/api/entity_factory.py b/hikari/api/entity_factory.py index d22b3c4682..4315d562b2 100644 --- a/hikari/api/entity_factory.py +++ b/hikari/api/entity_factory.py @@ -29,6 +29,7 @@ if typing.TYPE_CHECKING: import datetime + from hikari.api import gateway from hikari.events import channel as channel_events from hikari.events import guild as guild_events from hikari.events import message as message_events @@ -47,7 +48,6 @@ from hikari.models import users as user_models from hikari.models import voices as voice_models from hikari.models import webhooks as webhook_models - from hikari.impl import gateway from hikari.utilities import data_binding from hikari.utilities import files from hikari.utilities import snowflake @@ -1251,13 +1251,13 @@ def deserialize_message_reaction_remove_emoji_event( @abc.abstractmethod def deserialize_ready_event( - self, shard: gateway.Gateway, payload: data_binding.JSONObject, + self, shard: gateway.IGatewayShard, payload: data_binding.JSONObject, ) -> other_events.ReadyEvent: """Parse a raw payload from Discord into a ready event object. Parameters ---------- - shard : hikari.impl.gateway.Gateway + shard : hikari.api.gateway.IGatewayShard The shard that was ready. payload : typing.Mapping[str, typing.Any] The dict payload to parse. diff --git a/hikari/api/event_consumer.py b/hikari/api/event_consumer.py index cc8301c35b..b24f773800 100644 --- a/hikari/api/event_consumer.py +++ b/hikari/api/event_consumer.py @@ -24,10 +24,10 @@ import typing from hikari.api import component -from hikari.api import rest +from hikari.api import rest_app if typing.TYPE_CHECKING: - from hikari.impl import gateway + from hikari.api import gateway from hikari.utilities import data_binding @@ -44,13 +44,13 @@ class IEventConsumerComponent(component.IComponent, abc.ABC): @abc.abstractmethod async def consume_raw_event( - self, shard: gateway.Gateway, event_name: str, payload: data_binding.JSONObject + self, shard: gateway.IGatewayShard, event_name: str, payload: data_binding.JSONObject ) -> None: """Process a raw event from a gateway shard and process it. Parameters ---------- - shard : hikari.impl.gateway.Gateway + shard : hikari.api.gateway.IGatewayShard The gateway shard that emitted the event. event_name : str The event name. @@ -59,7 +59,7 @@ async def consume_raw_event( """ -class IEventConsumerApp(rest.IRESTClient, abc.ABC): +class IEventConsumerApp(rest_app.IRESTApp, abc.ABC): """Application specialization that supports consumption of raw events. This may be combined with `IGatewayZookeeperApp` for most single-process diff --git a/hikari/api/gateway.py b/hikari/api/gateway.py new file mode 100644 index 0000000000..fe1d728683 --- /dev/null +++ b/hikari/api/gateway.py @@ -0,0 +1,118 @@ +# -*- coding: utf-8 -*- +# Copyright © Nekoka.tt 2019-2020 +# +# This file is part of Hikari. +# +# Hikari is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Hikari is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Hikari. If not, see . +"""Provides an interface for gateway shard implementations to conform to.""" +from __future__ import annotations + +__all__: typing.Final[typing.Sequence[str]] = ["IGatewayShard"] + +import abc +import typing + +from hikari.api import component +from hikari.utilities import undefined + +if typing.TYPE_CHECKING: + import asyncio + import datetime + + from hikari.models import channels + from hikari.models import guilds + from hikari.models import presences + from hikari.utilities import snowflake + + +class IGatewayShard(component.IComponent, abc.ABC): + """Interface for a definition of a V6/V7 compatible websocket gateway. + + Each instance should represent a single shard. + """ + + __slots__ = () + + @property + @abc.abstractmethod + def is_alive(self) -> bool: + """Return `True` if the shard is alive and connected.""" + + @abc.abstractmethod + async def start(self) -> asyncio.Task[None]: + """Start the shard, wait for it to become ready. + + Returns + ------- + asyncio.Task + The task containing the shard running logic. Awaiting this will + wait until the shard has shut down before returning. + """ + + @abc.abstractmethod + async def close(self) -> None: + """Close the websocket if it is connected, otherwise do nothing.""" + + @abc.abstractmethod + async def update_presence( + self, + *, + idle_since: typing.Union[undefined.UndefinedType, None, datetime.datetime] = undefined.UNDEFINED, + afk: typing.Union[undefined.UndefinedType, bool] = undefined.UNDEFINED, + activity: typing.Union[undefined.UndefinedType, None, presences.Activity] = undefined.UNDEFINED, + status: typing.Union[undefined.UndefinedType, presences.Status] = undefined.UNDEFINED, + ) -> None: + """Update the presence of the shard user. + + Parameters + ---------- + idle_since : datetime.datetime or None or hikari.utilities.undefined.UndefinedType + The datetime that the user started being idle. If undefined, this + will not be changed. + afk : bool or hikari.utilities.undefined.UndefinedType + If `True`, the user is marked as AFK. If `False`, the user is marked + as being active. If undefined, this will not be changed. + activity : hikari.models.presences.Activity or None or hikari.utilities.undefined.UndefinedType + The activity to appear to be playing. If undefined, this will not be + changed. + status : hikari.models.presences.Status or hikari.utilities.undefined.UndefinedType + The web status to show. If undefined, this will not be changed. + """ + + @abc.abstractmethod + async def update_voice_state( + self, + guild: typing.Union[guilds.PartialGuild, snowflake.Snowflake, int, str], + channel: typing.Union[channels.GuildVoiceChannel, snowflake.Snowflake, int, str, None], + *, + self_mute: bool = False, + self_deaf: bool = False, + ) -> None: + """Update the voice state for this shard in a given guild. + + Parameters + ---------- + guild : hikari.models.guilds.PartialGuild or hikari.utilities.snowflake.UniqueObject + The guild or guild ID to update the voice state for. + channel : hikari.models.channels.GuildVoiceChannel or hikari.utilities.snowflake.UniqueObject or None + The channel or channel ID to update the voice state for. If `None` + then the bot will leave the voice channel that it is in for the + given guild. + self_mute : bool + If `True`, the bot will mute itself in that voice channel. If + `False`, then it will unmute itself. + self_deaf : bool + If `True`, the bot will deafen itself in that voice channel. If + `False`, then it will undeafen itself. + """ diff --git a/hikari/api/gateway_zookeeper.py b/hikari/api/gateway_zookeeper.py index 91cdd43daf..b39dfec747 100644 --- a/hikari/api/gateway_zookeeper.py +++ b/hikari/api/gateway_zookeeper.py @@ -30,8 +30,8 @@ if typing.TYPE_CHECKING: import datetime + from hikari.api import gateway from hikari.models import presences - from hikari.impl import gateway class IGatewayZookeeperApp(event_consumer.IEventConsumerApp, abc.ABC): @@ -49,7 +49,7 @@ class IGatewayZookeeperApp(event_consumer.IEventConsumerApp, abc.ABC): @property @abc.abstractmethod - def shards(self) -> typing.Mapping[int, gateway.Gateway]: + def shards(self) -> typing.Mapping[int, gateway.IGatewayShard]: """Map of each shard ID to the corresponding client for it. If the shards have not started, and auto=sharding is in-place, then it @@ -64,7 +64,7 @@ def shards(self) -> typing.Mapping[int, gateway.Gateway]: Returns ------- - typing.Mapping[int, hikari.impl.gateway.Gateway] + typing.Mapping[int, hikari.api.gateway.IGatewayShard] The mapping of shard IDs to gateway connections for the corresponding shard. These shard IDs are 0-indexed. """ diff --git a/hikari/api/rest.py b/hikari/api/rest_app.py similarity index 85% rename from hikari/api/rest.py rename to hikari/api/rest_app.py index a6f5ab3ada..5690552fc1 100644 --- a/hikari/api/rest.py +++ b/hikari/api/rest_app.py @@ -19,28 +19,28 @@ from __future__ import annotations -__all__: typing.Final[typing.Sequence[str]] = ["IRESTClient", "IRESTClientFactory", "IRESTClientContextManager"] +__all__: typing.Final[typing.Sequence[str]] = ["IRESTApp", "IRESTAppFactory", "IRESTAppContextManager"] import abc import typing -from hikari.impl import strings +from hikari.impl import constants if typing.TYPE_CHECKING: import concurrent.futures import types + from hikari import config from hikari.api import cache as cache_ from hikari.api import entity_factory as entity_factory_ - from hikari.impl import config - from hikari.impl import http as rest_ + from hikari.api import rest_client -class IRESTClient(abc.ABC): +class IRESTApp(abc.ABC): """Component specialization that is used for HTTP-only applications. This is a specific instance of a HTTP-only client provided by pooled - implementations of `IRESTClientFactory`. It may also be used by bots + implementations of `IRESTAppFactory`. It may also be used by bots as a base if they require HTTP-API access. """ @@ -48,14 +48,14 @@ class IRESTClient(abc.ABC): @property @abc.abstractmethod - def rest(self) -> rest_.HTTP: + def rest(self) -> rest_client.IRESTClient: """HTTP API Client. Use this to make calls to Discord's HTTP API over HTTPS. Returns ------- - hikari.impl.http.HTTP + hikari.api.rest_client.IRESTClient The HTTP API client. """ @@ -109,11 +109,11 @@ async def close(self) -> None: """Safely shut down all resources.""" -class IRESTClientContextManager(IRESTClient): - """An IRESTClient that may behave as a context manager.""" +class IRESTAppContextManager(IRESTApp): + """An IRESTApp that may behave as a context manager.""" @abc.abstractmethod - async def __aenter__(self) -> IRESTClientContextManager: + async def __aenter__(self) -> IRESTAppContextManager: ... @abc.abstractmethod @@ -126,7 +126,7 @@ async def __aexit__( ... -class IRESTClientFactory(abc.ABC): +class IRESTAppFactory(abc.ABC): """A client factory that emits clients. This enables a connection pool to be shared for stateless HTTP-only @@ -137,7 +137,7 @@ class IRESTClientFactory(abc.ABC): __slots__: typing.Sequence[str] = () @abc.abstractmethod - def acquire(self, token: str, token_type: str = strings.BEARER_TOKEN) -> IRESTClientContextManager: + def acquire(self, token: str, token_type: str = constants.BEARER_TOKEN) -> IRESTAppContextManager: """Acquire a HTTP client for the given authentication details. Parameters @@ -149,7 +149,7 @@ def acquire(self, token: str, token_type: str = strings.BEARER_TOKEN) -> IRESTCl Returns ------- - IRESTClient + IRESTApp The HTTP client to use. """ @@ -168,7 +168,7 @@ def proxy_settings(self) -> config.ProxySettings: """Proxy-specific settings.""" @abc.abstractmethod - async def __aenter__(self) -> IRESTClientFactory: + async def __aenter__(self) -> IRESTAppFactory: ... @abc.abstractmethod diff --git a/hikari/api/rest_client.py b/hikari/api/rest_client.py new file mode 100644 index 0000000000..945a4676aa --- /dev/null +++ b/hikari/api/rest_client.py @@ -0,0 +1,1576 @@ +# -*- coding: utf-8 -*- +# Copyright © Nekoka.tt 2019-2020 +# +# This file is part of Hikari. +# +# Hikari is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Hikari is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Hikari. If not, see . +"""Provides an interface for REST API implementations to follow.""" +from __future__ import annotations + +__all__: typing.Final[typing.Sequence[str]] = ["IRESTClient"] + +import abc +import typing + +from hikari.api import component +from hikari.utilities import undefined + +if typing.TYPE_CHECKING: + import datetime + + from hikari.api import special_endpoints + from hikari.models import applications + from hikari.models import audit_logs + from hikari.models import channels + from hikari.models import colors + from hikari.models import emojis + from hikari.models import embeds as embeds_ + from hikari.models import gateway + from hikari.models import guilds + from hikari.models import invites + from hikari.models import messages as messages_ + from hikari.models import permissions as permissions_ + from hikari.models import users + from hikari.models import voices + from hikari.models import webhooks + from hikari.utilities import date + from hikari.utilities import files + from hikari.utilities import iterators + from hikari.utilities import snowflake + + +class IRESTClient(component.IComponent, abc.ABC): + """Interface for functionality that a REST API implementation provides.""" + + __slots__ = () + + @abc.abstractmethod + async def close(self) -> None: + """Close the client session.""" + + @abc.abstractmethod + async def fetch_channel( + self, channel: typing.Union[channels.PartialChannel, snowflake.UniqueObject] + ) -> channels.PartialChannel: + """Fetch a channel. + + Parameters + ---------- + channel : hikari.models.channels.PartialChannel or hikari.utilities.snowflake.UniqueObject + The channel to fetch. This may be a channel object, or the ID of an + existing channel. + + Returns + ------- + hikari.models.channels.PartialChannel + The fetched channel. + + Raises + ------ + hikari.errors.Unauthorized + If you are unauthorized to make the request (invalid/missing token). + hikari.errors.Forbidden + If you lack permissions to access the channel. + hikari.errors.NotFound + If the channel is not found. + hikari.errors.ServerHTTPErrorResponse + If an internal error occurs on Discord while handling the request. + """ + + if typing.TYPE_CHECKING: + _GuildChannelT = typing.TypeVar("_GuildChannelT", bound=channels.GuildChannel, contravariant=True) + + @abc.abstractmethod + async def edit_channel( + self, + channel: typing.Union[channels.PartialChannel, snowflake.UniqueObject], + /, + *, + name: typing.Union[undefined.UndefinedType, str] = undefined.UNDEFINED, + position: typing.Union[undefined.UndefinedType, int] = undefined.UNDEFINED, + topic: typing.Union[undefined.UndefinedType, str] = undefined.UNDEFINED, + nsfw: typing.Union[undefined.UndefinedType, bool] = undefined.UNDEFINED, + bitrate: typing.Union[undefined.UndefinedType, int] = undefined.UNDEFINED, + user_limit: typing.Union[undefined.UndefinedType, int] = undefined.UNDEFINED, + rate_limit_per_user: typing.Union[undefined.UndefinedType, date.TimeSpan] = undefined.UNDEFINED, + permission_overwrites: typing.Union[ + undefined.UndefinedType, typing.Sequence[channels.PermissionOverwrite] + ] = undefined.UNDEFINED, + parent_category: typing.Union[ + undefined.UndefinedType, channels.GuildCategory, snowflake.UniqueObject + ] = undefined.UNDEFINED, + reason: typing.Union[undefined.UndefinedType, str] = undefined.UNDEFINED, + ) -> channels.PartialChannel: + """Edit a channel. + + Parameters + ---------- + channel : hikari.models.channels.PartialChannel or hikari.utilities.snowflake.UniqueObject + The channel to edit. This may be a channel object, or the ID of an + existing channel. + name : hikari.utilities.undefined.UndefinedType or str + If provided, the new name for the channel. + position : hikari.utilities.undefined.UndefinedType or int + If provided, the new position for the channel. + topic : hikari.utilities.undefined.UndefinedType or str + If provided, the new topic for the channel. + nsfw : hikari.utilities.undefined.UndefinedType or bool + If provided, whether the channel should be marked as NSFW or not. + bitrate : hikari.utilities.undefined.UndefinedType or int + If provided, the new bitrate for the channel. + user_limit : hikari.utilities.undefined.UndefinedType or int + If provided, the new user limit in the channel. + rate_limit_per_user : hikari.utilities.undefined.UndefinedType or datetime.timedelta or float or int + If provided, the new rate limit per user in the channel. + permission_overwrites : hikari.utilities.undefined.UndefinedType or typing.Sequence[hikari.models.channels.PermissionOverwrite] + If provided, the new permission overwrites for the channel. + parent_category : hikari.utilities.undefined.UndefinedType or hikari.models.channels.GuildCategory or hikari.utilities.snowflake.UniqueObject + If provided, the new guild category for the channel. This may be + a category object, or the ID of an existing category. + reason : hikari.utilities.undefined.UndefinedType or str + If provided, the reason that will be recorded in the audit logs. + + Returns + ------- + hikari.models.channels.PartialChannel + The edited channel. + + Raises + ------ + hikari.errors.BadRequest + If any of the fields that are passed have an invalid value. + hikari.errors.Unauthorized + If you are unauthorized to make the request (invalid/missing token). + hikari.errors.Forbidden + If you lack permissions to edit the channel + hikari.errors.NotFound + If the channel is not found. + hikari.errors.ServerHTTPErrorResponse + If an internal error occurs on Discord while handling the request. + """ # noqa: E501 - Line too long + + @abc.abstractmethod + async def delete_channel(self, channel: typing.Union[channels.PartialChannel, snowflake.UniqueObject]) -> None: + """Delete a channel in a guild, or close a DM. + + Parameters + ---------- + channel : hikari.models.channels.PartialChannel or hikari.utilities.snowflake.UniqueObject + The channel to delete. This may be a channel object, or the ID of an + existing channel. + + Raises + ------ + hikari.errors.Unauthorized + If you are unauthorized to make the request (invalid/missing token). + hikari.errors.Forbidden + If you lack permissions to delete the channel in a guild. + hikari.errors.NotFound + If the channel is not found. + hikari.errors.ServerHTTPErrorResponse + If an internal error occurs on Discord while handling the request. + + !!! note + For Public servers, the set 'Rules' or 'Guidelines' channels and the + 'Public Server Updates' channel cannot be deleted. + """ + + @typing.overload + @abc.abstractmethod + async def edit_permission_overwrites( + self, + channel: typing.Union[channels.GuildChannel, snowflake.UniqueObject], + target: typing.Union[channels.PermissionOverwrite, users.User, guilds.Role], + *, + allow: typing.Union[undefined.UndefinedType, permissions_.Permission] = undefined.UNDEFINED, + deny: typing.Union[undefined.UndefinedType, permissions_.Permission] = undefined.UNDEFINED, + reason: typing.Union[undefined.UndefinedType, str] = undefined.UNDEFINED, + ) -> None: + """Edit permissions for a target entity.""" + + @typing.overload + @abc.abstractmethod + async def edit_permission_overwrites( + self, + channel: typing.Union[channels.GuildChannel, snowflake.UniqueObject], + target: typing.Union[int, str, snowflake.Snowflake], + *, + target_type: typing.Union[channels.PermissionOverwriteType, str], + allow: typing.Union[undefined.UndefinedType, permissions_.Permission] = undefined.UNDEFINED, + deny: typing.Union[undefined.UndefinedType, permissions_.Permission] = undefined.UNDEFINED, + reason: typing.Union[undefined.UndefinedType, str] = undefined.UNDEFINED, + ) -> None: + """Edit permissions for a given entity ID and type.""" + + @abc.abstractmethod + async def edit_permission_overwrites( + self, + channel: typing.Union[channels.GuildChannel, snowflake.UniqueObject], + target: typing.Union[snowflake.UniqueObject, users.User, guilds.Role, channels.PermissionOverwrite], + *, + target_type: typing.Union[undefined.UndefinedType, channels.PermissionOverwriteType, str] = undefined.UNDEFINED, + allow: typing.Union[undefined.UndefinedType, permissions_.Permission] = undefined.UNDEFINED, + deny: typing.Union[undefined.UndefinedType, permissions_.Permission] = undefined.UNDEFINED, + reason: typing.Union[undefined.UndefinedType, str] = undefined.UNDEFINED, + ) -> None: + """Edit permissions for a specific entity in the given guild channel. + + Parameters + ---------- + channel : hikari.models.channels.PartialChannel or hikari.utilities.snowflake.UniqueObject + The channel to edit a permission overwrite in. This may be a channel object, or + the ID of an existing channel. + target : hikari.models.users.User or hikari.models.guilds.Role or hikari.models.channels.PermissionOverwrite or hikari.utilities.snowflake.UniqueObject + The channel overwrite to edit. This may be a overwrite object, or the ID of an + existing channel. + target_type : hikari.utilities.undefined.UndefinedType or hikari.models.channels.PermissionOverwriteType or str + If provided, the type of the target to update. If unset, will attempt to get + the type from `target`. + allow : hikari.utilities.undefined.UndefinedType or hikari.models.permissions.Permission + If provided, the new vale of all allowed permissions. + deny : hikari.utilities.undefined.UndefinedType or hikari.models.permissions.Permission + If provided, the new vale of all disallowed permissions. + reason : hikari.utilities.undefined.UndefinedType or str + If provided, the reason that will be recorded in the audit logs. + + Raises + ------ + TypeError + If `target_type` is unset and we were unable to determine the type + from `target`. + hikari.errors.BadRequest + If any of the fields that are passed have an invalid value. + hikari.errors.Unauthorized + If you are unauthorized to make the request (invalid/missing token). + hikari.errors.Forbidden + If you lack permissions to edit the permission overwrites. + hikari.errors.NotFound + If the channel is not found or the target is not found if it is + a role. + hikari.errors.ServerHTTPErrorResponse + If an internal error occurs on Discord while handling the request. + """ # noqa: E501 - Line too long + + @abc.abstractmethod + async def delete_permission_overwrite( + self, + channel: typing.Union[channels.GuildChannel, snowflake.UniqueObject], + target: typing.Union[channels.PermissionOverwrite, guilds.Role, users.User, snowflake.UniqueObject], + ) -> None: + """Delete a custom permission for an entity in a given guild channel. + + Parameters + ---------- + channel : hikari.models.channels.PartialChannel or hikari.utilities.snowflake.UniqueObject + The channel to delete a permission overwrite in. This may be a channel + object, or the ID of an existing channel. + target : hikari.models.users.User or hikari.models.guilds.Role or hikari.models.channels.PermissionOverwrite or hikari.utilities.snowflake.UniqueObject + The channel overwrite to delete. + + Raises + ------ + hikari.errors.Unauthorized + If you are unauthorized to make the request (invalid/missing token). + hikari.errors.Forbidden + If you lack permissions to delete the permission overwrite. + hikari.errors.NotFound + If the channel is not found or the target is not found. + hikari.errors.ServerHTTPErrorResponse + If an internal error occurs on Discord while handling the request. + """ # noqa: E501 - Line too long + + @abc.abstractmethod + async def fetch_channel_invites( + self, channel: typing.Union[channels.GuildChannel, snowflake.UniqueObject] + ) -> typing.Sequence[invites.InviteWithMetadata]: + """Fetch all invites pointing to the given guild channel. + + Parameters + ---------- + channel : hikari.models.channels.PartialChannel or hikari.utilities.snowflake.UniqueObject + The channel to fetch the invites from. This may be a channel + object, or the ID of an existing channel. + + Returns + ------- + typing.Sequence[hikari.models.invites.InviteWithMetadata] + The invites pointing to the given guild channel. + + Raises + ------ + hikari.errors.Unauthorized + If you are unauthorized to make the request (invalid/missing token). + hikari.errors.Forbidden + If you lack permissions to view the invites for the given channel. + hikari.errors.NotFound + If the channel is not found in any guilds you are a member of. + hikari.errors.ServerHTTPErrorResponse + If an internal error occurs on Discord while handling the request. + """ + + @abc.abstractmethod + async def create_invite( + self, + channel: typing.Union[channels.GuildChannel, snowflake.UniqueObject], + *, + max_age: typing.Union[undefined.UndefinedType, int, float, datetime.timedelta] = undefined.UNDEFINED, + max_uses: typing.Union[undefined.UndefinedType, int] = undefined.UNDEFINED, + temporary: typing.Union[undefined.UndefinedType, bool] = undefined.UNDEFINED, + unique: typing.Union[undefined.UndefinedType, bool] = undefined.UNDEFINED, + target_user: typing.Union[undefined.UndefinedType, users.User, snowflake.UniqueObject] = undefined.UNDEFINED, + target_user_type: typing.Union[undefined.UndefinedType, invites.TargetUserType] = undefined.UNDEFINED, + reason: typing.Union[undefined.UndefinedType, str] = undefined.UNDEFINED, + ) -> invites.InviteWithMetadata: + """Create an invite to the given guild channel. + + Parameters + ---------- + channel : hikari.models.channels.PartialChannel or hikari.utilities.snowflake.UniqueObject + The channel to create a invite for. This may be a channel object, + or the ID of an existing channel. + max_age : hikari.utilities.undefined.UndefinedType or datetime.timedelta or float or int + If provided, the duration of the invite before expiry. + max_uses : hikari.utilities.undefined.UndefinedType or int + If provided, the max uses the invite can have. + temporary : hikari.utilities.undefined.UndefinedType or bool + If provided, whether the invite only grants temporary membership. + unique : hikari.utilities.undefined.UndefinedType or bool + If provided, wheter the invite should be unique. + target_user : hikari.utilities.undefined.UndefinedType or hikari.models.users.User or hikari.utilities.snowflake.UniqueObject + If provided, the target user id for this invite. This may be a + user object, or the ID of an existing user. + target_user_type : hikari.utilities.undefined.UndefinedType or hikari.models.invites.TargetUserType or int + If provided, the type of target user for this invite. + reason : hikari.utilities.undefined.UndefinedType or str + If provided, the reason that will be recorded in the audit logs. + + Returns + ------- + hikari.models.invites.InviteWithMetadata + The invite to the given guild channel. + + Raises + ------ + hikari.errors.BadRequest + If any of the fields that are passed have an invalid value. + hikari.errors.Unauthorized + If you are unauthorized to make the request (invalid/missing token). + hikari.errors.Forbidden + If you lack permissions to create the given channel. + hikari.errors.NotFound + If the channel is not found, or if the target user does not exist, + if specified. + hikari.errors.ServerHTTPErrorResponse + If an internal error occurs on Discord while handling the request. + """ # noqa: E501 - Line too long + + @abc.abstractmethod + def trigger_typing( + self, channel: typing.Union[channels.TextChannel, snowflake.UniqueObject] + ) -> special_endpoints.TypingIndicator: + """Trigger typing in a text channel. + + Parameters + ---------- + channel : hikari.models.channels.PartialChannel or hikari.utilities.snowflake.UniqueObject + The channel to trigger typing in. This may be a channel object, or + the ID of an existing channel. + + Returns + ------- + hikari.impl.special_endpoints.TypingIndicator + A typing indicator to use. + + Raises + ------ + hikari.errors.Unauthorized + If you are unauthorized to make the request (invalid/missing token). + hikari.errors.Forbidden + If you lack permissions to read messages or send messages in the + text channel. + hikari.errors.NotFound + If the channel is not found. + hikari.errors.ServerHTTPErrorResponse + If an internal error occurs on Discord while handling the request. + + !!! note + The exceptions on this endpoint will only be raised once the result + is awaited or interacted with. Invoking this function itself will + not raise any of the above types. + """ + + @abc.abstractmethod + async def fetch_pins( + self, channel: typing.Union[channels.TextChannel, snowflake.UniqueObject] + ) -> typing.Sequence[messages_.Message]: + """Fetch the pinned messages in this text channel. + + Parameters + ---------- + channel : hikari.models.channels.PartialChannel or hikari.utilities.snowflake.UniqueObject + The channel to fetch pins from. This may be a channel object, or + the ID of an existing channel. + + Returns + ------- + typing.Sequence[hikari.models.messages.Message] + The pinned messages in this text channel. + + Raises + ------ + hikari.errors.Unauthorized + If you are unauthorized to make the request (invalid/missing token). + hikari.errors.Forbidden + If you lack permissions to read messages or send messages in the + text channel. + hikari.errors.NotFound + If the channel is not found. + hikari.errors.ServerHTTPErrorResponse + If an internal error occurs on Discord while handling the request. + """ + + @abc.abstractmethod + async def pin_message( + self, + channel: typing.Union[channels.TextChannel, snowflake.UniqueObject], + message: typing.Union[messages_.Message, snowflake.UniqueObject], + ) -> None: + """Pin an existing message in the given text channel. + + Parameters + ---------- + channel : hikari.models.channels.PartialChannel or hikari.utilities.snowflake.UniqueObject + The channel to pin a message in. This may be a channel object, or + the ID of an existing channel. + message : hikari.models.messages.Message or hikari.utilities.snowflake.UniqueObject + The message to pin. This may be a message object, + or the ID of an existing message. + + Raises + ------ + hikari.errors.Unauthorized + If you are unauthorized to make the request (invalid/missing token). + hikari.errors.Forbidden + If you lack permissions to pin messages in the given channel. + hikari.errors.NotFound + If the channel is not found, or if the message does not exist in + the given channel. + hikari.errors.ServerHTTPErrorResponse + If an internal error occurs on Discord while handling the request. + """ + + @abc.abstractmethod + async def unpin_message( + self, + channel: typing.Union[channels.TextChannel, snowflake.UniqueObject], + message: typing.Union[messages_.Message, snowflake.UniqueObject], + ) -> None: + """Unpin a given message from a given text channel. + + Parameters + ---------- + channel : hikari.models.channels.PartialChannel or hikari.utilities.snowflake.UniqueObject + The channel to unpin a message in. This may be a channel object, or + the ID of an existing channel. + message : hikari.models.messages.Message or hikari.utilities.snowflake.UniqueObject + The message to unpin. This may be a message object, or the ID of an + existing message. + + Raises + ------ + hikari.errors.Unauthorized + If you are unauthorized to make the request (invalid/missing token). + hikari.errors.Forbidden + If you lack permissions to pin messages in the given channel. + hikari.errors.NotFound + If the channel is not found or the message is not a pinned message + in the given channel. + hikari.errors.ServerHTTPErrorResponse + If an internal error occurs on Discord while handling the request. + """ + + @abc.abstractmethod + def fetch_messages( + self, + channel: typing.Union[channels.TextChannel, snowflake.UniqueObject], + *, + before: typing.Union[undefined.UndefinedType, datetime.datetime, snowflake.UniqueObject] = undefined.UNDEFINED, + after: typing.Union[undefined.UndefinedType, datetime.datetime, snowflake.UniqueObject] = undefined.UNDEFINED, + around: typing.Union[undefined.UndefinedType, datetime.datetime, snowflake.UniqueObject] = undefined.UNDEFINED, + ) -> iterators.LazyIterator[messages_.Message]: + """Browse the message history for a given text channel. + + Parameters + ---------- + channel : hikari.models.channels.PartialChannel or hikari.utilities.snowflake.UniqueObject + The channel to fetch messages in. This may be a channel object, or + the ID of an existing channel. + before : hikari.utilities.undefined.UndefinedType or datetime.datetime or hikari.utilities.snowflake.UniqueObject + If provided, fetch messages before this snowflake. If you provide + a datetime object, it will be transformed into a snowflake. + after : hikari.utilities.undefined.UndefinedType or datetime.datetime or hikari.utilities.snowflake.UniqueObject + If provided, fetch messages after this snowflake. If you provide + a datetime object, it will be transformed into a snowflake. + around : hikari.utilities.undefined.UndefinedType or datetime.datetime or hikari.utilities.snowflake.UniqueObject + If provided, fetch messages around this snowflake. If you provide + a datetime object, it will be transformed into a snowflake. + + Returns + ------- + hikari.utilities.iterators.LazyIterator[hikari.models.messages.Message] + A iterator to fetch the messages. + + Raises + ------ + TypeError + If you specify more than one of `before`, `after`, `about`. + hikari.errors.Unauthorized + If you are unauthorized to make the request (invalid/missing token). + hikari.errors.Forbidden + If you lack permissions to read message history in the given + channel. + hikari.errors.NotFound + If the channel is not found. + hikari.errors.ServerHTTPErrorResponse + If an internal error occurs on Discord while handling the request. + + !!! note + The exceptions on this endpoint (other than `TypeError`) will only + be raised once the result is awaited or interacted with. Invoking + this function itself will not raise anything (other than + `TypeError`). + """ # noqa: E501 - Line too long + + @abc.abstractmethod + async def fetch_message( + self, + channel: typing.Union[channels.TextChannel, snowflake.UniqueObject], + message: typing.Union[messages_.Message, snowflake.UniqueObject], + ) -> messages_.Message: + """Fetch a specific message in the given text channel. + + Parameters + ---------- + channel : hikari.models.channels.PartialChannel or hikari.utilities.snowflake.UniqueObject + The channel to fetch messages in. This may be a channel object, or + the ID of an existing channel. + message : hikari.models.messages.Message or hikari.utilities.snowflake.UniqueObject + The message to fetch. This may be a channel object, or the ID of an + existing channel. + + Returns + ------- + hikari.models.messages.Message + The requested message. + + Raises + ------ + hikari.errors.Unauthorized + If you are unauthorized to make the request (invalid/missing token). + hikari.errors.Forbidden + If you lack permissions to read message history in the given + channel. + hikari.errors.NotFound + If the channel is not found or the message is not found in the + given text channel. + hikari.errors.ServerHTTPErrorResponse + If an internal error occurs on Discord while handling the request. + """ + + @abc.abstractmethod + async def create_message( + self, + channel: typing.Union[channels.TextChannel, snowflake.UniqueObject], + text: typing.Union[undefined.UndefinedType, typing.Any] = undefined.UNDEFINED, + *, + embed: typing.Union[undefined.UndefinedType, embeds_.Embed] = undefined.UNDEFINED, + attachment: typing.Union[undefined.UndefinedType, str, files.Resource] = undefined.UNDEFINED, + attachments: typing.Union[ + undefined.UndefinedType, typing.Sequence[typing.Union[str, files.Resource]] + ] = undefined.UNDEFINED, + tts: typing.Union[undefined.UndefinedType, bool] = undefined.UNDEFINED, + nonce: typing.Union[undefined.UndefinedType, str] = undefined.UNDEFINED, + mentions_everyone: bool = True, + user_mentions: typing.Union[typing.Collection[typing.Union[users.User, snowflake.UniqueObject]], bool] = True, + role_mentions: typing.Union[typing.Collection[typing.Union[guilds.Role, snowflake.UniqueObject]], bool] = True, + ) -> messages_.Message: + """Create a message in the given channel. + + Parameters + ---------- + channel : hikari.models.channels.PartialChannel or hikari.utilities.snowflake.UniqueObject + The channel to create the message in. This may be a channel object, or + the ID of an existing channel. + text : hikari.utilities.undefined.UndefinedType or str + If specified, the message contents. + embed : hikari.utilities.undefined.UndefinedType or hikari.models.embeds.Embed + If specified, the message embed. + attachment : hikari.utilities.undefined.UndefinedType or str or hikari.utilities.files.Resource + If specified, the message attachment. This can be a resource, + or string of a path on your computer or a URL. + attachments : hikari.utilities.undefined.UndefinedType or typing.Sequence[str or hikari.utilities.files.Resource] + If specified, the message attachments. These can be resources, or + strings consisting of paths on your computer or URLs. + tts : hikari.utilities.undefined.UndefinedType or bool + If specified, whether the message will be TTS (Text To Speech). + nonce : hikari.utilities.undefined.UndefinedType or str + If specified, a nonce that can be used for optimistic message sending. + mentions_everyone : bool + If specified, whether the message should parse @everyone/@here mentions. + user_mentions : typing.Collection[hikari.models.users.User or hikari.utilities.snowflake.UniqueObject] or bool + If specified, and `bool`, whether to parse user mentions. If specified and + `list`, the users to parse the mention for. This may be a user object, or + the ID of an existing user. + role_mentions : typing.Collection[hikari.models.guilds.Role or hikari.utilities.snowflake.UniqueObject] or bool + If specified and `bool`, whether to parse role mentions. If specified and + `list`, the roles to parse the mention for. This may be a role object, or + the ID of an existing role. + + Returns + ------- + hikari.models.messages.Message + The created message. + + Raises + ------ + hikari.errors.BadRequest + This may be raised in several discrete situations, such as messages + being empty with no attachments or embeds; messages with more than + 2000 characters in them, embeds that exceed one of the many embed + limits; too many attachments; attachments that are too large; + invalid image URLs in embeds; users in `user_mentions` not being + mentioned in the message content; roles in `role_mentions` not + being mentioned in the message content. + hikari.errors.Unauthorized + If you are unauthorized to make the request (invalid/missing token). + hikari.errors.Forbidden + If you lack permissions to send messages in the given channel. + hikari.errors.NotFound + If the channel is not found. + hikari.errors.ServerHTTPErrorResponse + If an internal error occurs on Discord while handling the request. + ValueError + If more than 100 unique objects/entities are passed for + `role_mentions` or `user_mentions`. + TypeError + If both `attachment` and `attachments` are specified. + + !!! warning + You are expected to make a connection to the gateway and identify + once before being able to use this endpoint for a bot. + """ # noqa: E501 - Line too long + + @abc.abstractmethod + async def edit_message( + self, + channel: typing.Union[channels.TextChannel, snowflake.UniqueObject], + message: typing.Union[messages_.Message, snowflake.UniqueObject], + text: typing.Union[undefined.UndefinedType, None, typing.Any] = undefined.UNDEFINED, + *, + embed: typing.Union[undefined.UndefinedType, None, embeds_.Embed] = undefined.UNDEFINED, + mentions_everyone: typing.Union[undefined.UndefinedType, bool] = undefined.UNDEFINED, + user_mentions: typing.Union[ + undefined.UndefinedType, typing.Collection[typing.Union[users.User, snowflake.UniqueObject]], bool + ] = undefined.UNDEFINED, + role_mentions: typing.Union[ + undefined.UndefinedType, typing.Collection[typing.Union[snowflake.UniqueObject, guilds.Role]], bool + ] = undefined.UNDEFINED, + flags: typing.Union[undefined.UndefinedType, messages_.MessageFlag] = undefined.UNDEFINED, + ) -> messages_.Message: + """Edit an existing message in a given channel. + + Parameters + ---------- + channel : hikari.models.channels.PartialChannel or hikari.utilities.snowflake.UniqueObject + The channel to edit the message in. This may be a channel object, or + the ID of an existing channel. + message : hikari.models.messages.Message or hikari.utilities.snowflake.UniqueObject + The message to fetch. + text + embed + mentions_everyone + user_mentions + role_mentions + flags + + Returns + ------- + hikari.models.messages.Message + The edited message. + + Raises + ------ + hikari.errors.BadRequest + This may be raised in several discrete situations, such as messages + being empty with no embeds; messages with more than 2000 characters + in them, embeds that exceed one of the many embed + limits; invalid image URLs in embeds; users in `user_mentions` not + being mentioned in the message content; roles in `role_mentions` not + being mentioned in the message content. + hikari.errors.Unauthorized + If you are unauthorized to make the request (invalid/missing token). + hikari.errors.Forbidden + If you lack permissions to send messages in the given channel; if + you try to change the contents of another user's message; or if you + try to edit the flags on another user's message without the + permissions to manage messages_. + hikari.errors.NotFound + If the channel or message is not found. + hikari.errors.ServerHTTPErrorResponse + If an internal error occurs on Discord while handling the request. + """ + + @abc.abstractmethod + async def delete_message( + self, + channel: typing.Union[channels.TextChannel, snowflake.UniqueObject], + message: typing.Union[messages_.Message, snowflake.UniqueObject], + ) -> None: + """Delete a given message in a given channel. + + Parameters + ---------- + channel + message + + Raises + ------ + hikari.errors.Unauthorized + If you are unauthorized to make the request (invalid/missing token). + hikari.errors.Forbidden + If you lack the permissions to manage messages, and the message is + not composed by your associated user. + hikari.errors.NotFound + If the channel or message is not found. + hikari.errors.ServerHTTPErrorResponse + If an internal error occurs on Discord while handling the request. + """ + + @abc.abstractmethod + async def delete_messages( + self, + channel: typing.Union[channels.GuildTextChannel, snowflake.UniqueObject], + /, + *messages: typing.Union[messages_.Message, snowflake.UniqueObject], + ) -> None: + """Bulk-delete between 2 and 100 messages from the given guild channel. + + Parameters + ---------- + channel + *messages + + Raises + ------ + hikari.errors.Unauthorized + If you are unauthorized to make the request (invalid/missing token). + hikari.errors.Forbidden + If you lack the permissions to manage messages, and the message is + not composed by your associated user. + hikari.errors.NotFound + If the channel or message is not found. + hikari.errors.ServerHTTPErrorResponse + If an internal error occurs on Discord while handling the request. + TypeError + If you do not provide between 2 and 100 messages (inclusive). + """ + + @abc.abstractmethod + async def add_reaction( + self, + channel: typing.Union[channels.TextChannel, snowflake.UniqueObject], + message: typing.Union[messages_.Message, snowflake.UniqueObject], + emoji: typing.Union[str, emojis.Emoji], + ) -> None: + """Add a reaction emoji to a message in a given channel. + + Parameters + ---------- + channel + message + emoji + + Raises + ------ + hikari.errors.BadRequest + If an invalid unicode emoji is given, or if the given custom emoji + does not exist. + hikari.errors.Unauthorized + If you are unauthorized to make the request (invalid/missing token). + hikari.errors.Forbidden + If you lack permissions to add reactions to messages. + hikari.errors.NotFound + If the channel or message is not found. + hikari.errors.ServerHTTPErrorResponse + If an internal error occurs on Discord while handling the request. + """ + + @abc.abstractmethod + async def delete_my_reaction( + self, + channel: typing.Union[channels.TextChannel, snowflake.UniqueObject], + message: typing.Union[messages_.Message, snowflake.UniqueObject], + emoji: typing.Union[str, emojis.Emoji], + ) -> None: + """Delete a reaction that your application user created. + + Parameters + ---------- + channel + message + emoji + + Raises + ------ + hikari.errors.BadRequest + If an invalid unicode emoji is given, or if the given custom emoji + does not exist. + hikari.errors.Unauthorized + If you are unauthorized to make the request (invalid/missing token). + hikari.errors.NotFound + If the channel or message is not found. + hikari.errors.ServerHTTPErrorResponse + If an internal error occurs on Discord while handling the request. + """ + + @abc.abstractmethod + async def delete_all_reactions_for_emoji( + self, + channel: typing.Union[channels.TextChannel, snowflake.UniqueObject], + message: typing.Union[messages_.Message, snowflake.UniqueObject], + emoji: typing.Union[str, emojis.Emoji], + ) -> None: + ... + + @abc.abstractmethod + async def delete_reaction( + self, + channel: typing.Union[channels.TextChannel, snowflake.UniqueObject], + message: typing.Union[messages_.Message, snowflake.UniqueObject], + emoji: typing.Union[str, emojis.Emoji], + user: typing.Union[users.User, snowflake.UniqueObject], + ) -> None: + ... + + @abc.abstractmethod + async def delete_all_reactions( + self, + channel: typing.Union[channels.TextChannel, snowflake.UniqueObject], + message: typing.Union[messages_.Message, snowflake.UniqueObject], + ) -> None: + ... + + @abc.abstractmethod + def fetch_reactions_for_emoji( + self, + channel: typing.Union[channels.TextChannel, snowflake.UniqueObject], + message: typing.Union[messages_.Message, snowflake.UniqueObject], + emoji: typing.Union[str, emojis.Emoji], + ) -> iterators.LazyIterator[users.User]: + ... + + @abc.abstractmethod + async def create_webhook( + self, + channel: typing.Union[channels.TextChannel, snowflake.UniqueObject], + name: str, + *, + avatar: typing.Union[undefined.UndefinedType, files.Resource, str] = undefined.UNDEFINED, + reason: typing.Union[undefined.UndefinedType, str] = undefined.UNDEFINED, + ) -> webhooks.Webhook: + ... + + @abc.abstractmethod + async def fetch_webhook( + self, + webhook: typing.Union[webhooks.Webhook, snowflake.UniqueObject], + *, + token: typing.Union[undefined.UndefinedType, str] = undefined.UNDEFINED, + ) -> webhooks.Webhook: + ... + + @abc.abstractmethod + async def fetch_channel_webhooks( + self, channel: typing.Union[channels.TextChannel, snowflake.UniqueObject] + ) -> typing.Sequence[webhooks.Webhook]: + ... + + @abc.abstractmethod + async def fetch_guild_webhooks( + self, guild: typing.Union[guilds.Guild, snowflake.UniqueObject] + ) -> typing.Sequence[webhooks.Webhook]: + ... + + @abc.abstractmethod + async def edit_webhook( + self, + webhook: typing.Union[webhooks.Webhook, snowflake.UniqueObject], + *, + token: typing.Union[undefined.UndefinedType, str] = undefined.UNDEFINED, + name: typing.Union[undefined.UndefinedType, str] = undefined.UNDEFINED, + avatar: typing.Union[None, undefined.UndefinedType, files.Resource, str] = undefined.UNDEFINED, + channel: typing.Union[ + undefined.UndefinedType, channels.TextChannel, snowflake.UniqueObject + ] = undefined.UNDEFINED, + reason: typing.Union[undefined.UndefinedType, str] = undefined.UNDEFINED, + ) -> webhooks.Webhook: + ... + + @abc.abstractmethod + async def delete_webhook( + self, + webhook: typing.Union[webhooks.Webhook, snowflake.UniqueObject], + *, + token: typing.Union[undefined.UndefinedType, str] = undefined.UNDEFINED, + ) -> None: + ... + + @abc.abstractmethod + async def execute_webhook( + self, + webhook: typing.Union[webhooks.Webhook, snowflake.UniqueObject], + token: str, + text: typing.Union[undefined.UndefinedType, typing.Any] = undefined.UNDEFINED, + *, + username: typing.Union[undefined.UndefinedType, str] = undefined.UNDEFINED, + avatar_url: typing.Union[undefined.UndefinedType, str] = undefined.UNDEFINED, + embeds: typing.Union[undefined.UndefinedType, typing.Sequence[embeds_.Embed]] = undefined.UNDEFINED, + attachment: typing.Union[undefined.UndefinedType, str, files.Resource] = undefined.UNDEFINED, + attachments: typing.Union[ + undefined.UndefinedType, typing.Sequence[typing.Union[str, files.Resource]] + ] = undefined.UNDEFINED, + tts: typing.Union[undefined.UndefinedType, bool] = undefined.UNDEFINED, + mentions_everyone: bool = True, + user_mentions: typing.Union[typing.Collection[typing.Union[users.User, snowflake.UniqueObject]], bool] = True, + role_mentions: typing.Union[typing.Collection[typing.Union[snowflake.UniqueObject, guilds.Role]], bool] = True, + ) -> messages_.Message: + ... + + @abc.abstractmethod + async def fetch_gateway_url(self) -> str: + ... + + @abc.abstractmethod + async def fetch_gateway_bot(self) -> gateway.GatewayBot: + ... + + @abc.abstractmethod + async def fetch_invite(self, invite: typing.Union[invites.Invite, str]) -> invites.Invite: + ... + + @abc.abstractmethod + async def delete_invite(self, invite: typing.Union[invites.Invite, str]) -> None: + ... + + @abc.abstractmethod + async def fetch_my_user(self) -> users.OwnUser: + ... + + @abc.abstractmethod + async def edit_my_user( + self, + *, + username: typing.Union[undefined.UndefinedType, str] = undefined.UNDEFINED, + avatar: typing.Union[undefined.UndefinedType, None, files.Resource, str] = undefined.UNDEFINED, + ) -> users.OwnUser: + ... + + @abc.abstractmethod + async def fetch_my_connections(self) -> typing.Sequence[applications.OwnConnection]: + ... + + @abc.abstractmethod + def fetch_my_guilds( + self, + *, + newest_first: bool = False, + start_at: typing.Union[ + undefined.UndefinedType, guilds.PartialGuild, snowflake.UniqueObject, datetime.datetime + ] = undefined.UNDEFINED, + ) -> iterators.LazyIterator[applications.OwnGuild]: + ... + + @abc.abstractmethod + async def leave_guild(self, guild: typing.Union[guilds.Guild, snowflake.UniqueObject], /) -> None: + ... + + @abc.abstractmethod + async def create_dm_channel(self, user: typing.Union[users.User, snowflake.UniqueObject], /) -> channels.DMChannel: + ... + + @abc.abstractmethod + async def fetch_application(self) -> applications.Application: + ... + + @abc.abstractmethod + async def add_user_to_guild( + self, + access_token: str, + guild: typing.Union[guilds.Guild, snowflake.UniqueObject], + user: typing.Union[users.User, snowflake.UniqueObject], + *, + nick: typing.Union[undefined.UndefinedType, str] = undefined.UNDEFINED, + roles: typing.Union[ + undefined.UndefinedType, typing.Collection[typing.Union[guilds.Role, snowflake.UniqueObject]] + ] = undefined.UNDEFINED, + mute: typing.Union[undefined.UndefinedType, bool] = undefined.UNDEFINED, + deaf: typing.Union[undefined.UndefinedType, bool] = undefined.UNDEFINED, + ) -> typing.Optional[guilds.Member]: + ... + + @abc.abstractmethod + async def fetch_voice_regions(self) -> typing.Sequence[voices.VoiceRegion]: + ... + + @abc.abstractmethod + async def fetch_user(self, user: typing.Union[users.User, snowflake.UniqueObject]) -> users.User: + ... + + def fetch_audit_log( + self, + guild: typing.Union[guilds.Guild, snowflake.UniqueObject], + *, + before: typing.Union[undefined.UndefinedType, datetime.datetime, snowflake.UniqueObject] = undefined.UNDEFINED, + user: typing.Union[undefined.UndefinedType, users.User, snowflake.UniqueObject] = undefined.UNDEFINED, + event_type: typing.Union[undefined.UndefinedType, audit_logs.AuditLogEventType] = undefined.UNDEFINED, + ) -> iterators.LazyIterator[audit_logs.AuditLog]: + ... + + @abc.abstractmethod + async def fetch_emoji( + self, + guild: typing.Union[guilds.Guild, snowflake.UniqueObject], + # This is an emoji ID, which is the URL-safe emoji name, not the snowflake alone. + emoji: typing.Union[emojis.CustomEmoji, snowflake.UniqueObject], + ) -> emojis.KnownCustomEmoji: + ... + + @abc.abstractmethod + async def fetch_guild_emojis( + self, guild: typing.Union[guilds.Guild, snowflake.UniqueObject] + ) -> typing.Set[emojis.KnownCustomEmoji]: + ... + + @abc.abstractmethod + async def create_emoji( + self, + guild: typing.Union[guilds.Guild, snowflake.UniqueObject], + name: str, + image: typing.Union[files.Resource, str], + *, + roles: typing.Union[ + undefined.UndefinedType, typing.Collection[typing.Union[guilds.Role, snowflake.UniqueObject]] + ] = undefined.UNDEFINED, + reason: typing.Union[undefined.UndefinedType, str] = undefined.UNDEFINED, + ) -> emojis.KnownCustomEmoji: + ... + + @abc.abstractmethod + async def edit_emoji( + self, + guild: typing.Union[guilds.Guild, snowflake.UniqueObject], + # This is an emoji ID, which is the URL-safe emoji name, not the snowflake alone. + emoji: typing.Union[emojis.CustomEmoji, snowflake.UniqueObject], + *, + name: typing.Union[undefined.UndefinedType, str] = undefined.UNDEFINED, + roles: typing.Union[ + undefined.UndefinedType, typing.Collection[typing.Union[guilds.Role, snowflake.UniqueObject]] + ] = undefined.UNDEFINED, + reason: typing.Union[undefined.UndefinedType, str] = undefined.UNDEFINED, + ) -> emojis.KnownCustomEmoji: + ... + + @abc.abstractmethod + async def delete_emoji( + self, + guild: typing.Union[guilds.Guild, snowflake.UniqueObject], + # This is an emoji ID, which is the URL-safe emoji name, not the snowflake alone. + emoji: typing.Union[emojis.CustomEmoji, snowflake.UniqueObject], + # Reason is not currently supported for some reason. See + ) -> None: + ... + + @abc.abstractmethod + def guild_builder(self, name: str, /) -> special_endpoints.GuildBuilder: + ... + + @abc.abstractmethod + async def fetch_guild(self, guild: typing.Union[guilds.Guild, snowflake.UniqueObject]) -> guilds.Guild: + ... + + @abc.abstractmethod + async def fetch_guild_preview( + self, guild: typing.Union[guilds.PartialGuild, snowflake.UniqueObject] + ) -> guilds.GuildPreview: + ... + + @abc.abstractmethod + async def edit_guild( + self, + guild: typing.Union[guilds.Guild, snowflake.UniqueObject], + *, + name: typing.Union[undefined.UndefinedType, str] = undefined.UNDEFINED, + region: typing.Union[undefined.UndefinedType, voices.VoiceRegion, str] = undefined.UNDEFINED, + verification_level: typing.Union[undefined.UndefinedType, guilds.GuildVerificationLevel] = undefined.UNDEFINED, + default_message_notifications: typing.Union[ + undefined.UndefinedType, guilds.GuildMessageNotificationsLevel + ] = undefined.UNDEFINED, + explicit_content_filter_level: typing.Union[ + undefined.UndefinedType, guilds.GuildExplicitContentFilterLevel + ] = undefined.UNDEFINED, + afk_channel: typing.Union[ + undefined.UndefinedType, channels.GuildVoiceChannel, snowflake.UniqueObject + ] = undefined.UNDEFINED, + afk_timeout: typing.Union[undefined.UndefinedType, date.TimeSpan] = undefined.UNDEFINED, + icon: typing.Union[undefined.UndefinedType, None, files.Resource, str] = undefined.UNDEFINED, + owner: typing.Union[undefined.UndefinedType, users.User, snowflake.UniqueObject] = undefined.UNDEFINED, + splash: typing.Union[undefined.UndefinedType, None, files.Resource, str] = undefined.UNDEFINED, + banner: typing.Union[undefined.UndefinedType, None, files.Resource, str] = undefined.UNDEFINED, + system_channel: typing.Union[undefined.UndefinedType, channels.GuildTextChannel] = undefined.UNDEFINED, + rules_channel: typing.Union[undefined.UndefinedType, channels.GuildTextChannel] = undefined.UNDEFINED, + public_updates_channel: typing.Union[undefined.UndefinedType, channels.GuildTextChannel] = undefined.UNDEFINED, + preferred_locale: typing.Union[undefined.UndefinedType, str] = undefined.UNDEFINED, + reason: typing.Union[undefined.UndefinedType, str] = undefined.UNDEFINED, + ) -> guilds.Guild: + ... + + @abc.abstractmethod + async def delete_guild(self, guild: typing.Union[guilds.Guild, snowflake.UniqueObject]) -> None: + ... + + @abc.abstractmethod + async def fetch_guild_channels( + self, guild: typing.Union[guilds.Guild, snowflake.UniqueObject] + ) -> typing.Sequence[channels.GuildChannel]: + ... + + @abc.abstractmethod + async def create_guild_text_channel( + self, + guild: typing.Union[guilds.Guild, snowflake.UniqueObject], + name: str, + *, + position: typing.Union[int, undefined.UndefinedType] = undefined.UNDEFINED, + topic: typing.Union[str, undefined.UndefinedType] = undefined.UNDEFINED, + nsfw: typing.Union[bool, undefined.UndefinedType] = undefined.UNDEFINED, + rate_limit_per_user: typing.Union[int, undefined.UndefinedType] = undefined.UNDEFINED, + permission_overwrites: typing.Union[ + typing.Sequence[channels.PermissionOverwrite], undefined.UndefinedType + ] = undefined.UNDEFINED, + category: typing.Union[ + channels.GuildCategory, snowflake.UniqueObject, undefined.UndefinedType + ] = undefined.UNDEFINED, + reason: typing.Union[str, undefined.UndefinedType] = undefined.UNDEFINED, + ) -> channels.GuildTextChannel: + ... + + @abc.abstractmethod + async def create_guild_news_channel( + self, + guild: typing.Union[guilds.Guild, snowflake.UniqueObject], + name: str, + *, + position: typing.Union[int, undefined.UndefinedType] = undefined.UNDEFINED, + topic: typing.Union[str, undefined.UndefinedType] = undefined.UNDEFINED, + nsfw: typing.Union[bool, undefined.UndefinedType] = undefined.UNDEFINED, + rate_limit_per_user: typing.Union[int, undefined.UndefinedType] = undefined.UNDEFINED, + permission_overwrites: typing.Union[ + typing.Sequence[channels.PermissionOverwrite], undefined.UndefinedType + ] = undefined.UNDEFINED, + category: typing.Union[ + channels.GuildCategory, snowflake.UniqueObject, undefined.UndefinedType + ] = undefined.UNDEFINED, + reason: typing.Union[str, undefined.UndefinedType] = undefined.UNDEFINED, + ) -> channels.GuildNewsChannel: + ... + + @abc.abstractmethod + async def create_guild_voice_channel( + self, + guild: typing.Union[guilds.Guild, snowflake.UniqueObject], + name: str, + *, + position: typing.Union[int, undefined.UndefinedType] = undefined.UNDEFINED, + nsfw: typing.Union[bool, undefined.UndefinedType] = undefined.UNDEFINED, + user_limit: typing.Union[int, undefined.UndefinedType] = undefined.UNDEFINED, + bitrate: typing.Union[int, undefined.UndefinedType] = undefined.UNDEFINED, + permission_overwrites: typing.Union[ + typing.Sequence[channels.PermissionOverwrite], undefined.UndefinedType + ] = undefined.UNDEFINED, + category: typing.Union[ + channels.GuildCategory, snowflake.UniqueObject, undefined.UndefinedType + ] = undefined.UNDEFINED, + reason: typing.Union[str, undefined.UndefinedType] = undefined.UNDEFINED, + ) -> channels.GuildVoiceChannel: + ... + + @abc.abstractmethod + async def create_guild_category( + self, + guild: typing.Union[guilds.Guild, snowflake.UniqueObject], + name: str, + *, + position: typing.Union[int, undefined.UndefinedType] = undefined.UNDEFINED, + nsfw: typing.Union[bool, undefined.UndefinedType] = undefined.UNDEFINED, + permission_overwrites: typing.Union[ + typing.Sequence[channels.PermissionOverwrite], undefined.UndefinedType + ] = undefined.UNDEFINED, + reason: typing.Union[str, undefined.UndefinedType] = undefined.UNDEFINED, + ) -> channels.GuildCategory: + ... + + @abc.abstractmethod + async def reposition_channels( + self, + guild: typing.Union[guilds.Guild, snowflake.UniqueObject], + positions: typing.Mapping[int, typing.Union[channels.GuildChannel, snowflake.UniqueObject]], + ) -> None: + ... + + @abc.abstractmethod + async def fetch_member( + self, + guild: typing.Union[guilds.Guild, snowflake.UniqueObject], + user: typing.Union[users.User, snowflake.UniqueObject], + ) -> guilds.Member: + ... + + @abc.abstractmethod + def fetch_members( + self, guild: typing.Union[guilds.Guild, snowflake.UniqueObject] + ) -> iterators.LazyIterator[guilds.Member]: + ... + + @abc.abstractmethod + async def edit_member( + self, + guild: typing.Union[guilds.Guild, snowflake.UniqueObject], + user: typing.Union[users.User, snowflake.UniqueObject], + *, + nick: typing.Union[undefined.UndefinedType, str] = undefined.UNDEFINED, + roles: typing.Union[ + undefined.UndefinedType, typing.Collection[typing.Union[guilds.Role, snowflake.UniqueObject]] + ] = undefined.UNDEFINED, + mute: typing.Union[undefined.UndefinedType, bool] = undefined.UNDEFINED, + deaf: typing.Union[undefined.UndefinedType, bool] = undefined.UNDEFINED, + voice_channel: typing.Union[ + undefined.UndefinedType, channels.GuildVoiceChannel, snowflake.UniqueObject, None + ] = undefined.UNDEFINED, + reason: typing.Union[str, undefined.UndefinedType] = undefined.UNDEFINED, + ) -> None: + ... + + @abc.abstractmethod + async def edit_my_nick( + self, + guild: typing.Union[guilds.Guild, snowflake.UniqueObject], + nick: typing.Optional[str], + *, + reason: typing.Union[undefined.UndefinedType, str] = undefined.UNDEFINED, + ) -> None: + ... + + @abc.abstractmethod + async def add_role_to_member( + self, + guild: typing.Union[guilds.Guild, snowflake.UniqueObject], + user: typing.Union[users.User, snowflake.UniqueObject], + role: typing.Union[guilds.Role, snowflake.UniqueObject], + *, + reason: typing.Union[undefined.UndefinedType, str] = undefined.UNDEFINED, + ) -> None: + ... + + @abc.abstractmethod + async def remove_role_from_member( + self, + guild: typing.Union[guilds.Guild, snowflake.UniqueObject], + user: typing.Union[users.User, snowflake.UniqueObject], + role: typing.Union[guilds.Role, snowflake.UniqueObject], + *, + reason: typing.Union[undefined.UndefinedType, str] = undefined.UNDEFINED, + ) -> None: + ... + + @abc.abstractmethod + async def kick_user( + self, + guild: typing.Union[guilds.Guild, snowflake.UniqueObject], + user: typing.Union[users.User, snowflake.UniqueObject], + *, + reason: typing.Union[undefined.UndefinedType, str] = undefined.UNDEFINED, + ) -> None: + ... + + kick_member = kick_user + """This is simply an alias for readability.""" + + @abc.abstractmethod + async def ban_user( + self, + guild: typing.Union[guilds.Guild, snowflake.UniqueObject], + user: typing.Union[users.User, snowflake.UniqueObject], + *, + delete_message_days: typing.Union[undefined.UndefinedType, int] = undefined.UNDEFINED, + reason: typing.Union[undefined.UndefinedType, str] = undefined.UNDEFINED, + ) -> None: + ... + + ban_member = ban_user + """This is simply an alias for readability.""" + + @abc.abstractmethod + async def unban_user( + self, + guild: typing.Union[guilds.Guild, snowflake.UniqueObject], + user: typing.Union[users.User, snowflake.UniqueObject], + *, + reason: typing.Union[undefined.UndefinedType, str] = undefined.UNDEFINED, + ) -> None: + ... + + unban_member = unban_user + """This is simply an alias for readability.""" + + @abc.abstractmethod + async def fetch_ban( + self, + guild: typing.Union[guilds.Guild, snowflake.UniqueObject], + user: typing.Union[users.User, snowflake.UniqueObject], + ) -> guilds.GuildMemberBan: + ... + + @abc.abstractmethod + async def fetch_bans( + self, guild: typing.Union[guilds.Guild, snowflake.UniqueObject] + ) -> typing.Sequence[guilds.GuildMemberBan]: + ... + + @abc.abstractmethod + async def fetch_roles( + self, guild: typing.Union[guilds.Guild, snowflake.UniqueObject] + ) -> typing.Sequence[guilds.Role]: + ... + + @abc.abstractmethod + async def create_role( + self, + guild: typing.Union[guilds.Guild, snowflake.UniqueObject], + *, + name: typing.Union[undefined.UndefinedType, str] = undefined.UNDEFINED, + permissions: typing.Union[undefined.UndefinedType, permissions_.Permission] = undefined.UNDEFINED, + color: typing.Union[undefined.UndefinedType, colors.Color] = undefined.UNDEFINED, + colour: typing.Union[undefined.UndefinedType, colors.Color] = undefined.UNDEFINED, + hoist: typing.Union[undefined.UndefinedType, bool] = undefined.UNDEFINED, + mentionable: typing.Union[undefined.UndefinedType, bool] = undefined.UNDEFINED, + reason: typing.Union[undefined.UndefinedType, str] = undefined.UNDEFINED, + ) -> guilds.Role: + ... + + @abc.abstractmethod + async def reposition_roles( + self, + guild: typing.Union[guilds.Guild, snowflake.UniqueObject], + positions: typing.Mapping[int, typing.Union[guilds.Role, snowflake.UniqueObject]], + ) -> None: + ... + + @abc.abstractmethod + async def edit_role( + self, + guild: typing.Union[guilds.Guild, snowflake.UniqueObject], + role: typing.Union[guilds.Role, snowflake.UniqueObject], + *, + name: typing.Union[undefined.UndefinedType, str] = undefined.UNDEFINED, + permissions: typing.Union[undefined.UndefinedType, permissions_.Permission] = undefined.UNDEFINED, + color: typing.Union[undefined.UndefinedType, colors.Color] = undefined.UNDEFINED, + colour: typing.Union[undefined.UndefinedType, colors.Color] = undefined.UNDEFINED, + hoist: typing.Union[undefined.UndefinedType, bool] = undefined.UNDEFINED, + mentionable: typing.Union[undefined.UndefinedType, bool] = undefined.UNDEFINED, + reason: typing.Union[undefined.UndefinedType, str] = undefined.UNDEFINED, + ) -> guilds.Role: + ... + + @abc.abstractmethod + async def delete_role( + self, + guild: typing.Union[guilds.Guild, snowflake.UniqueObject], + role: typing.Union[guilds.Role, snowflake.UniqueObject], + ) -> None: + ... + + @abc.abstractmethod + async def estimate_guild_prune_count( + self, + guild: typing.Union[guilds.Guild, snowflake.UniqueObject], + *, + days: typing.Union[undefined.UndefinedType, int] = undefined.UNDEFINED, + include_roles: typing.Union[ + undefined.UndefinedType, typing.Collection[typing.Union[guilds.Role, snowflake.UniqueObject]] + ] = undefined.UNDEFINED, + ) -> int: + """Estimate the guild prune count. + + Parameters + ---------- + guild : hikari.models.guilds.Guild or hikari.utilities.snowflake.UniqueObject + The guild to estimate the guild prune count for. This may be a guild object, + or the ID of an existing channel. + days : hikari.utilities.undefined.UndefinedType or int + If provided, number of days to count prune for. + include_roles : hikari.utilities.undefined.UndefinedType or typing.Collection[hikari.models.guilds.Role or hikari.utilities.snowflake.UniqueObject] + If provided, the role(s) to include. By default, this endpoint will not count + users with roles. Providing roles using this attribute will make members with + the specified roles also get included into the count. + + Returns + ------- + int + The estimated guild prune count. + + Raises + ------ + hikari.errors.BadRequest + If any of the fields that are passed have an invalid value. + hikari.errors.Unauthorized + If you are unauthorized to make the request (invalid/missing token). + hikari.errors.Forbidden + If you lack the `KICK_MEMBERS` permission. + hikari.errors.NotFound + If the guild is not found. + hikari.errors.ServerHTTPErrorResponse + If an internal error occurs on Discord while handling the request. + """ # noqa: E501 - Line too long + + @abc.abstractmethod + async def begin_guild_prune( + self, + guild: typing.Union[guilds.Guild, snowflake.UniqueObject], + *, + days: typing.Union[undefined.UndefinedType, int] = undefined.UNDEFINED, + compute_prune_count: typing.Union[undefined.UndefinedType, bool] = undefined.UNDEFINED, + include_roles: typing.Union[ + undefined.UndefinedType, typing.Collection[typing.Union[guilds.Role, snowflake.UniqueObject]] + ] = undefined.UNDEFINED, + reason: typing.Union[undefined.UndefinedType, str] = undefined.UNDEFINED, + ) -> typing.Optional[int]: + """Begin the guild prune. + + Parameters + ---------- + guild : hikari.models.guilds.Guild or hikari.utilities.snowflake.UniqueObject + The guild to begin the guild prune in. This may be a guild object, + or the ID of an existing channel. + days : hikari.utilities.undefined.UndefinedType or int + If provided, number of days to count prune for. + compute_prune_count: hikari.utilities.undefined.UndefinedType or bool + If provided, whether to return the prune count. This is discouraged for large + guilds. + include_roles : hikari.utilities.undefined.UndefinedType or typing.Collection[hikari.models.guilds.Role or hikari.utilities.snowflake.UniqueObject] + If provided, the role(s) to include. By default, this endpoint will not count + users with roles. Providing roles using this attribute will make members with + the specified roles also get included into the count. + reason : hikari.utilities.undefined.UndefinedType or str + If provided, the reason that will be recorded in the audit logs. + + Returns + ------- + int or None + If `compute_prune_count` is not provided or `True`, the number of members pruned. + + Raises + ------ + hikari.errors.BadRequest + If any of the fields that are passed have an invalid value. + hikari.errors.Unauthorized + If you are unauthorized to make the request (invalid/missing token). + hikari.errors.Forbidden + If you lack the `KICK_MEMBERS` permission. + hikari.errors.NotFound + If the guild is not found. + hikari.errors.ServerHTTPErrorResponse + If an internal error occurs on Discord while handling the request. + """ # noqa: E501 - Line too long + + @abc.abstractmethod + async def fetch_guild_voice_regions( + self, guild: typing.Union[guilds.Guild, snowflake.UniqueObject] + ) -> typing.Sequence[voices.VoiceRegion]: + ... + + @abc.abstractmethod + async def fetch_guild_invites( + self, guild: typing.Union[guilds.Guild, snowflake.UniqueObject] + ) -> typing.Sequence[invites.InviteWithMetadata]: + ... + + @abc.abstractmethod + async def fetch_integrations( + self, guild: typing.Union[guilds.Guild, snowflake.UniqueObject] + ) -> typing.Sequence[guilds.Integration]: + ... + + @abc.abstractmethod + async def edit_integration( + self, + guild: typing.Union[guilds.Guild, snowflake.UniqueObject], + integration: typing.Union[guilds.Integration, snowflake.UniqueObject], + *, + expire_behaviour: typing.Union[ + undefined.UndefinedType, guilds.IntegrationExpireBehaviour + ] = undefined.UNDEFINED, + expire_grace_period: typing.Union[undefined.UndefinedType, date.TimeSpan] = undefined.UNDEFINED, + enable_emojis: typing.Union[undefined.UndefinedType, bool] = undefined.UNDEFINED, + reason: typing.Union[undefined.UndefinedType, str] = undefined.UNDEFINED, + ) -> None: + ... + + @abc.abstractmethod + async def delete_integration( + self, + guild: typing.Union[guilds.Guild, snowflake.UniqueObject], + integration: typing.Union[guilds.Integration, snowflake.UniqueObject], + *, + reason: typing.Union[undefined.UndefinedType, str] = undefined.UNDEFINED, + ) -> None: + ... + + @abc.abstractmethod + async def sync_integration( + self, + guild: typing.Union[guilds.Guild, snowflake.UniqueObject], + integration: typing.Union[guilds.Integration, snowflake.UniqueObject], + ) -> None: + ... + + @abc.abstractmethod + async def fetch_widget(self, guild: typing.Union[guilds.Guild, snowflake.UniqueObject]) -> guilds.GuildWidget: + ... + + @abc.abstractmethod + async def edit_widget( + self, + guild: typing.Union[guilds.Guild, snowflake.UniqueObject], + *, + channel: typing.Union[ + undefined.UndefinedType, channels.GuildChannel, snowflake.UniqueObject, None + ] = undefined.UNDEFINED, + enabled: typing.Union[undefined.UndefinedType, bool] = undefined.UNDEFINED, + reason: typing.Union[undefined.UndefinedType, str] = undefined.UNDEFINED, + ) -> guilds.GuildWidget: + ... + + @abc.abstractmethod + async def fetch_vanity_url(self, guild: typing.Union[guilds.Guild, snowflake.UniqueObject]) -> invites.VanityURL: + ... diff --git a/hikari/api/special_endpoints.py b/hikari/api/special_endpoints.py new file mode 100644 index 0000000000..b736dc7eaa --- /dev/null +++ b/hikari/api/special_endpoints.py @@ -0,0 +1,398 @@ +# -*- coding: utf-8 -*- +# Copyright © Nekoka.tt 2019-2020 +# +# This file is part of Hikari. +# +# Hikari is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Hikari is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Hikari. If not, see . +"""Special additional endpoints used by the REST API.""" +from __future__ import annotations + +__all__: typing.Final[typing.Sequence[str]] = ["TypingIndicator", "GuildBuilder"] + +import abc +import typing + +import attr + +from hikari.utilities import undefined + +if typing.TYPE_CHECKING: + import types + + from hikari.models import channels + from hikari.models import colors + from hikari.models import guilds + from hikari.models import permissions as permissions_ + from hikari.utilities import date + from hikari.utilities import files + from hikari.utilities import snowflake + + +class TypingIndicator(abc.ABC): + """Result type of `hiarki.net.rest.trigger_typing`. + + This is an object that can either be awaited like a coroutine to trigger + the typing indicator once, or an async context manager to keep triggering + the typing indicator repeatedly until the context finishes. + """ + + __slots__ = () + + def __enter__(self) -> typing.NoReturn: + raise TypeError("Use 'async with' rather than 'with' when triggering the typing indicator.") + + @abc.abstractmethod + def __await__(self) -> typing.Generator[None, typing.Any, None]: + ... + + @abc.abstractmethod + async def __aenter__(self) -> None: + ... + + @abc.abstractmethod + async def __aexit__(self, ex_t: typing.Type[Exception], ex_v: Exception, exc_tb: types.TracebackType) -> None: + ... + + +@attr.s(auto_attribs=True, kw_only=True, slots=True) +class GuildBuilder: + """A helper class used to construct a prototype for a guild. + + This is used to create a guild in a tidy way using the HTTP API, since + the logic behind creating a guild on an API level is somewhat confusing + and detailed. + + !!! note + This is a helper class that is used by `hikari.impl.http.HTTP`. + You should only ever need to use instances of this class that are + produced by that API, thus, any details about the constructor are + omitted from the following examples for brevity. + + Examples + -------- + Creating an empty guild. + + ```py + guild = await rest.guild_builder("My Server!").create() + ``` + + Creating a guild with an icon + + ```py + from hikari.models.files import WebResourceStream + + guild_builder = rest.guild_builder("My Server!") + guild_builder.icon = WebResourceStream("cat.png", "http://...") + guild = await guild_builder.create() + ``` + + Adding roles to your guild. + + ```py + from hikari.models.permissions import Permission + + guild_builder = rest.guild_builder("My Server!") + + everyone_role_id = guild_builder.add_role("@everyone") + admin_role_id = guild_builder.add_role("Admins", permissions=Permission.ADMINISTRATOR) + + await guild_builder.create() + ``` + + !!! warning + The first role must always be the `@everyone` role. + + !!! note + Functions that return a `hikari.utilities.snowflake.Snowflake` do + **not** provide the final ID that the object will have once the + API call is made. The returned IDs are only able to be used to + re-reference particular objects while building the guild format. + + This is provided to allow creation of channels within categories, + and to provide permission overwrites. + + Adding a text channel to your guild. + + ```py + guild_builder = rest.guild_builder("My Server!") + + category_id = guild_builder.add_category("My safe place") + channel_id = guild_builder.add_text_channel("general", parent_id=category_id) + + await guild_builder.create() + ``` + """ + + default_message_notifications: typing.Union[ + undefined.UndefinedType, guilds.GuildMessageNotificationsLevel + ] = undefined.UNDEFINED + """Default message notification level that can be overwritten. + + If not overridden, this will use the Discord default level. + """ + + explicit_content_filter_level: typing.Union[ + undefined.UndefinedType, guilds.GuildExplicitContentFilterLevel + ] = undefined.UNDEFINED + """Explicit content filter level that can be overwritten. + + If not overridden, this will use the Discord default level. + """ + + icon: typing.Union[undefined.UndefinedType, files.URL] = undefined.UNDEFINED + """Guild icon to use that can be overwritten. + + If not overridden, the guild will not have an icon. + """ + + region: typing.Union[undefined.UndefinedType, str] = undefined.UNDEFINED + """Guild voice channel region to use that can be overwritten. + + If not overridden, the guild will use the default voice region for Discord. + """ + + verification_level: typing.Union[undefined.UndefinedType, guilds.GuildVerificationLevel] = undefined.UNDEFINED + """Verification level required to join the guild that can be overwritten. + + If not overridden, the guild will use the default verification level for + Discord. + """ + + @property + @abc.abstractmethod + def name(self) -> str: + """Guild name.""" + + @abc.abstractmethod + async def create(self) -> guilds.Guild: + """Send the request to Discord to create the guild. + + The application user will be added to this guild as soon as it is + created. All IDs that were provided when building this guild will + become invalid and will be replaced with real IDs. + + Returns + ------- + hikari.models.guilds.Guild + The created guild. + + Raises + ------ + hikari.errors.BadRequest + If any values set in the guild builder are invalid. + hikari.errors.Unauthorized + If you are unauthorized to make the request (invalid/missing token). + hikari.errors.Forbidden + If you are already in 10 guilds. + hikari.errors.ServerHTTPErrorResponse + If an internal error occurs on Discord while handling the request. + """ + + @abc.abstractmethod + def add_role( + self, + name: str, + /, + *, + color: typing.Union[undefined.UndefinedType, colors.Color] = undefined.UNDEFINED, + colour: typing.Union[undefined.UndefinedType, colors.Color] = undefined.UNDEFINED, + hoisted: typing.Union[undefined.UndefinedType, bool] = undefined.UNDEFINED, + mentionable: typing.Union[undefined.UndefinedType, bool] = undefined.UNDEFINED, + permissions: typing.Union[undefined.UndefinedType, permissions_.Permission] = undefined.UNDEFINED, + position: typing.Union[undefined.UndefinedType, int] = undefined.UNDEFINED, + ) -> snowflake.Snowflake: + """Create a role. + + !!! note + The first role you create must always be the `@everyone` role, and + must have that name. This role will ignore the `hoisted`, `color`, + `colour`, `mentionable` and `position` parameters. + + Parameters + ---------- + name : str + The role name. + color : hikari.utilities.undefined.UndefinedType or hikari.models.colors.Color + The colour of the role to use. If unspecified, then the default + colour is used instead. + colour : hikari.utilities.undefined.UndefinedType or hikari.models.colors.Color + Alias for the `color` parameter for non-american users. + hoisted : hikari.utilities.undefined.UndefinedType or bool + If `True`, the role will show up in the user sidebar in a separate + category if it is the highest hoisted role. If `False`, or + unspecified, then this will not occur. + mentionable : hikari.utilities.undefined.UndefinedType or bool + If `True`, then the role will be able to be mentioned. + permissions : hikari.utilities.undefined.UndefinedType or hikari.models.permissions.Permission + The optional permissions to enforce on the role. If unspecified, + the default permissions for roles will be used. + + !!! note + The default permissions are **NOT** the same as providing + zero permissions. To set no permissions, you should + pass `Permission(0)` explicitly. + position : hikari.utilities.undefined.UndefinedType or int + If specified, the position to place the role in. + + Returns + ------- + hikari.utilities.snowflake.Snowflake + The dummy ID for this role that can be used temporarily to refer + to this object while designing the guild layout. + + When the guild is created, this will be replaced with a different + ID. + + Raises + ------ + ValueError + If you are defining the first role, but did not name it `@everyone`. + TypeError + If you specify both `color` and `colour` together. + """ + + @abc.abstractmethod + def add_category( + self, + name: str, + /, + *, + position: typing.Union[undefined.UndefinedType, int] = undefined.UNDEFINED, + permission_overwrites: typing.Union[ + undefined.UndefinedType, typing.Collection[channels.PermissionOverwrite] + ] = undefined.UNDEFINED, + nsfw: typing.Union[undefined.UndefinedType, bool] = undefined.UNDEFINED, + ) -> snowflake.Snowflake: + """Create a category channel. + + Parameters + ---------- + name : str + The name of the category. + position : hikari.utilities.undefined.UndefinedType or int + The position to place the category in, if specified. + permission_overwrites : hikari.utilities.undefined.UndefinedType or typing.Collection[hikari.models.channels.PermissionOverwrite] + If defined, a collection of one or more + `hikari.models.channels.PermissionOverwrite` objects. + nsfw : hikari.utilities.undefined.UndefinedType or bool + If `True`, the channel is marked as NSFW and only users over + 18 years of age should be given access. + + Returns + ------- + hikari.utilities.snowflake.Snowflake + The dummy ID for this channel that can be used temporarily to refer + to this object while designing the guild layout. + + When the guild is created, this will be replaced with a different + ID. + """ # noqa: E501 - Line too long + + @abc.abstractmethod + def add_text_channel( + self, + name: str, + /, + *, + parent_id: typing.Union[undefined.UndefinedType, snowflake.Snowflake] = undefined.UNDEFINED, + topic: typing.Union[undefined.UndefinedType, str] = undefined.UNDEFINED, + rate_limit_per_user: typing.Union[undefined.UndefinedType, date.TimeSpan] = undefined.UNDEFINED, + position: typing.Union[undefined.UndefinedType, int] = undefined.UNDEFINED, + permission_overwrites: typing.Union[ + undefined.UndefinedType, typing.Collection[channels.PermissionOverwrite] + ] = undefined.UNDEFINED, + nsfw: typing.Union[undefined.UndefinedType, bool] = undefined.UNDEFINED, + ) -> snowflake.Snowflake: + """Create a text channel. + + Parameters + ---------- + name : str + The name of the category. + position : hikari.utilities.undefined.UndefinedType or int + The position to place the category in, if specified. + permission_overwrites : hikari.utilities.undefined.UndefinedType or typing.Collection[hikari.models.channels.PermissionOverwrite] + If defined, a collection of one or more + `hikari.models.channels.PermissionOverwrite` objects. + nsfw : hikari.utilities.undefined.UndefinedType or bool + If `True`, the channel is marked as NSFW and only users over + 18 years of age should be given access. + parent_id : hikari.utilities.undefined.UndefinedType or hikari.utilities.snowflake.Snowflake + If defined, should be a snowflake ID of a category channel + that was made with this builder. If provided, this channel will + become a child channel of that category. + topic : hikari.utilities.undefined.UndefinedType or str + If specified, the topic to set on the channel. + rate_limit_per_user : hikari.utilities.undefined.UndefinedType or hikari.utilities.date.TimeSpan + If specified, the time to wait between allowing consecutive messages + to be sent. If not specified, this will not be enabled. + + Returns + ------- + hikari.utilities.snowflake.Snowflake + The dummy ID for this channel that can be used temporarily to refer + to this object while designing the guild layout. + + When the guild is created, this will be replaced with a different + ID. + """ # noqa: E501 - Line too long + + @abc.abstractmethod + def add_voice_channel( + self, + name: str, + /, + *, + parent_id: typing.Union[undefined.UndefinedType, snowflake.Snowflake] = undefined.UNDEFINED, + bitrate: typing.Union[undefined.UndefinedType, int] = undefined.UNDEFINED, + position: typing.Union[undefined.UndefinedType, int] = undefined.UNDEFINED, + permission_overwrites: typing.Union[ + undefined.UndefinedType, typing.Collection[channels.PermissionOverwrite] + ] = undefined.UNDEFINED, + nsfw: typing.Union[undefined.UndefinedType, bool] = undefined.UNDEFINED, + user_limit: typing.Union[undefined.UndefinedType, int] = undefined.UNDEFINED, + ) -> snowflake.Snowflake: + """Create a voice channel. + + Parameters + ---------- + name : str + The name of the category. + position : hikari.utilities.undefined.UndefinedType or int + The position to place the category in, if specified. + permission_overwrites : hikari.utilities.undefined.UndefinedType or typing.Collection[hikari.models.channels.PermissionOverwrite] + If defined, a collection of one or more + `hikari.models.channels.PermissionOverwrite` objects. + nsfw : hikari.utilities.undefined.UndefinedType or bool + If `True`, the channel is marked as NSFW and only users over + 18 years of age should be given access. + parent_id : hikari.utilities.undefined.UndefinedType or hikari.utilities.snowflake.Snowflake + If defined, should be a snowflake ID of a category channel + that was made with this builder. If provided, this channel will + become a child channel of that category. + bitrate : hikari.utilities.undefined.UndefinedType or int + If specified, the bitrate to set on the channel. + user_limit : hikari.utilities.undefined.UndefinedType or int + If specified, the maximum number of users to allow in the voice + channel. + + Returns + ------- + hikari.utilities.snowflake.Snowflake + The dummy ID for this channel that can be used temporarily to refer + to this object while designing the guild layout. + + When the guild is created, this will be replaced with a different + ID. + """ # noqa: E501 - Line too long diff --git a/hikari/api/voice.py b/hikari/api/voice.py new file mode 100644 index 0000000000..bef64ee073 --- /dev/null +++ b/hikari/api/voice.py @@ -0,0 +1,57 @@ +# -*- coding: utf-8 -*- +# Copyright © Nekoka.tt 2019-2020 +# +# This file is part of Hikari. +# +# Hikari is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Hikari is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Hikari. If not, see . +"""Interfaces used to describe voice client implementations.""" +from __future__ import annotations + +__all__: typing.Final[typing.Sequence[str]] = ["IVoiceComponent", "IVoiceConnection"] + +import abc +import typing + +from hikari.api import component + +if typing.TYPE_CHECKING: + from hikari.models import channels + + +class IVoiceComponent(component.IComponent, abc.ABC): + """Interface for a voice system implementation.""" + + __slots__ = () + + @abc.abstractmethod + async def connect_to( + self, channel: channels.GuildVoiceChannel, *, deaf: bool = False, mute: bool = False, + ) -> IVoiceConnection: + """Connect to a given voice channel. + + Parameters + ---------- + channel : hikari.models.channels.GuildVoiceChannel or hikari.utilities.snowflake.UniqueObject + The channel or channel ID to connect to. + deaf : bool + Defaulting to `False`, if `True`, the client will enter the voice + channel deafened (thus unable to hear other users). + mute : bool + Defaulting to `False`, if `True`, the client will enter the voice + channel muted (thus unable to send audio). + """ + + +class IVoiceConnection(abc.ABC): + """Interface for defining what a voice connection should look like.""" diff --git a/hikari/impl/config.py b/hikari/config.py similarity index 94% rename from hikari/impl/config.py rename to hikari/config.py index b0c49f0e41..41a1fefb51 100644 --- a/hikari/impl/config.py +++ b/hikari/config.py @@ -31,7 +31,7 @@ import attr -from hikari.impl import strings +from hikari.impl import constants from hikari.utilities import data_binding @@ -50,7 +50,7 @@ def header(self) -> str: """Generate the header value and return it.""" raw_token = f"{self.username}:{self.password}".encode("ascii") token_part = base64.b64encode(raw_token).decode("ascii") - return f"{strings.BASICAUTH_TOKEN} {token_part}" + return f"{constants.BASICAUTH_TOKEN} {token_part}" def __str__(self) -> str: return self.header @@ -87,11 +87,11 @@ def all_headers(self) -> typing.Optional[data_binding.Headers]: if self.headers is None: if self.auth is None: return None - return {strings.PROXY_AUTHENTICATION_HEADER: self.auth} + return {constants.PROXY_AUTHENTICATION_HEADER: self.auth} if self.auth is None: return self.headers - return {**self.headers, strings.PROXY_AUTHENTICATION_HEADER: self.auth} + return {**self.headers, constants.PROXY_AUTHENTICATION_HEADER: self.auth} @attr.s(slots=True, kw_only=True, auto_attribs=True) diff --git a/hikari/events/channel.py b/hikari/events/channel.py index ed7f4d2833..fc938b84c7 100644 --- a/hikari/events/channel.py +++ b/hikari/events/channel.py @@ -42,7 +42,7 @@ from hikari.utilities import snowflake if typing.TYPE_CHECKING: - from hikari.api import rest + from hikari.api import rest_app from hikari.models import channels from hikari.models import guilds from hikari.models import invites @@ -88,7 +88,7 @@ class ChannelPinsUpdateEvent(base_events.Event): when a pinned message is deleted. """ - app: rest.IRESTClient = attr.ib(default=None, repr=False, eq=False, hash=False) + app: rest_app.IRESTApp = attr.ib(default=None, repr=False, eq=False, hash=False) """The client application that models may use for procedures.""" guild_id: typing.Optional[snowflake.Snowflake] = attr.ib(repr=True) @@ -115,7 +115,7 @@ class WebhookUpdateEvent(base_events.Event): Sent when a webhook is updated, created or deleted in a guild. """ - app: rest.IRESTClient = attr.ib(default=None, repr=False, eq=False, hash=False) + app: rest_app.IRESTApp = attr.ib(default=None, repr=False, eq=False, hash=False) """The client application that models may use for procedures.""" guild_id: snowflake.Snowflake = attr.ib(repr=True) @@ -133,7 +133,7 @@ class TypingStartEvent(base_events.Event): Received when a user or bot starts "typing" in a channel. """ - app: rest.IRESTClient = attr.ib(default=None, repr=False, eq=False, hash=False) + app: rest_app.IRESTApp = attr.ib(default=None, repr=False, eq=False, hash=False) """The client application that models may use for procedures.""" channel_id: snowflake.Snowflake = attr.ib(repr=True) @@ -175,7 +175,7 @@ class InviteDeleteEvent(base_events.Event): Sent when an invite is deleted for a channel we can access. """ - app: rest.IRESTClient = attr.ib(default=None, repr=False, eq=False, hash=False) + app: rest_app.IRESTApp = attr.ib(default=None, repr=False, eq=False, hash=False) """The client application that models may use for procedures.""" channel_id: snowflake.Snowflake = attr.ib(repr=True) diff --git a/hikari/events/guild.py b/hikari/events/guild.py index f660c105c2..3a8b09e47a 100644 --- a/hikari/events/guild.py +++ b/hikari/events/guild.py @@ -49,7 +49,7 @@ from hikari.utilities import snowflake if typing.TYPE_CHECKING: - from hikari.api import rest + from hikari.api import rest_app from hikari.models import emojis as emojis_models from hikari.models import guilds from hikari.models import presences @@ -113,7 +113,7 @@ class GuildUnavailableEvent(GuildEvent, snowflake.Unique): ) """The ID of this entity.""" - app: rest.IRESTClient = attr.ib(default=None, repr=False, eq=False, hash=False) + app: rest_app.IRESTApp = attr.ib(default=None, repr=False, eq=False, hash=False) """The client application that models may use for procedures.""" @@ -122,7 +122,7 @@ class GuildUnavailableEvent(GuildEvent, snowflake.Unique): class GuildBanEvent(GuildEvent, abc.ABC): """A base object that guild ban events will inherit from.""" - app: rest.IRESTClient = attr.ib(default=None, repr=False, eq=False, hash=False) + app: rest_app.IRESTApp = attr.ib(default=None, repr=False, eq=False, hash=False) """The client application that models may use for procedures.""" guild_id: snowflake.Snowflake = attr.ib(repr=True) @@ -149,7 +149,7 @@ class GuildBanRemoveEvent(GuildBanEvent): class GuildEmojisUpdateEvent(GuildEvent): """Represents a Guild Emoji Update gateway event.""" - app: rest.IRESTClient = attr.ib(default=None, repr=False, eq=False, hash=False) + app: rest_app.IRESTApp = attr.ib(default=None, repr=False, eq=False, hash=False) """The client application that models may use for procedures.""" guild_id: snowflake.Snowflake = attr.ib(repr=True) @@ -164,7 +164,7 @@ class GuildEmojisUpdateEvent(GuildEvent): class GuildIntegrationsUpdateEvent(GuildEvent): """Used to represent Guild Integration Update gateway events.""" - app: rest.IRESTClient = attr.ib(default=None, repr=False, eq=False, hash=False) + app: rest_app.IRESTApp = attr.ib(default=None, repr=False, eq=False, hash=False) """The client application that models may use for procedures.""" guild_id: snowflake.Snowflake = attr.ib(repr=True) @@ -182,7 +182,7 @@ class GuildMemberEvent(GuildEvent): class GuildMemberAddEvent(GuildMemberEvent): """Used to represent a Guild Member Add gateway event.""" - app: rest.IRESTClient = attr.ib(default=None, repr=False, eq=False, hash=False) + app: rest_app.IRESTApp = attr.ib(default=None, repr=False, eq=False, hash=False) """The client application that models may use for procedures.""" guild_id: snowflake.Snowflake = attr.ib(repr=True) # TODO: do we want to have guild_id on all members? @@ -211,7 +211,7 @@ class GuildMemberRemoveEvent(GuildMemberEvent): Sent when a member is kicked, banned or leaves a guild. """ - app: rest.IRESTClient = attr.ib(default=None, repr=False, eq=False, hash=False) + app: rest_app.IRESTApp = attr.ib(default=None, repr=False, eq=False, hash=False) """The client application that models may use for procedures.""" # TODO: make GuildMember event into common base class. @@ -232,7 +232,7 @@ class GuildRoleEvent(GuildEvent): class GuildRoleCreateEvent(GuildRoleEvent): """Used to represent a Guild Role Create gateway event.""" - app: rest.IRESTClient = attr.ib(default=None, repr=False, eq=False, hash=False) + app: rest_app.IRESTApp = attr.ib(default=None, repr=False, eq=False, hash=False) """The client application that models may use for procedures.""" guild_id: snowflake.Snowflake = attr.ib(repr=True) @@ -247,7 +247,7 @@ class GuildRoleCreateEvent(GuildRoleEvent): class GuildRoleUpdateEvent(GuildRoleEvent): """Used to represent a Guild Role Create gateway event.""" - app: rest.IRESTClient = attr.ib(default=None, repr=False, eq=False, hash=False) + app: rest_app.IRESTApp = attr.ib(default=None, repr=False, eq=False, hash=False) """The client application that models may use for procedures.""" # TODO: make any event with a guild ID into a custom base event. @@ -264,7 +264,7 @@ class GuildRoleUpdateEvent(GuildRoleEvent): class GuildRoleDeleteEvent(GuildRoleEvent): """Represents a gateway Guild Role Delete Event.""" - app: rest.IRESTClient = attr.ib(default=None, repr=False, eq=False, hash=False) + app: rest_app.IRESTApp = attr.ib(default=None, repr=False, eq=False, hash=False) """The client application that models may use for procedures.""" guild_id: snowflake.Snowflake = attr.ib(repr=True) diff --git a/hikari/events/message.py b/hikari/events/message.py index 6620d0bda1..c9e4a944b5 100644 --- a/hikari/events/message.py +++ b/hikari/events/message.py @@ -44,7 +44,7 @@ if typing.TYPE_CHECKING: import datetime - from hikari.api import rest + from hikari.api import rest_app from hikari.models import applications from hikari.models import embeds as embed_models from hikari.models import emojis @@ -82,7 +82,7 @@ class UpdatedMessageFields(snowflake.Unique): ) """The ID of this entity.""" - app: rest.IRESTClient = attr.ib(default=None, repr=False, eq=False, hash=False) + app: rest_app.IRESTApp = attr.ib(default=None, repr=False, eq=False, hash=False) """The client application that models may use for procedures.""" channel_id: snowflake.Snowflake = attr.ib(repr=True) @@ -192,7 +192,7 @@ class MessageDeleteEvent(base_events.Event): Sent when a message is deleted in a channel we have access to. """ - app: rest.IRESTClient = attr.ib(default=None, repr=False, eq=False, hash=False) + app: rest_app.IRESTApp = attr.ib(default=None, repr=False, eq=False, hash=False) """The client application that models may use for procedures.""" # TODO: common base class for Message events. @@ -219,7 +219,7 @@ class MessageDeleteBulkEvent(base_events.Event): Sent when multiple messages are deleted in a channel at once. """ - app: rest.IRESTClient = attr.ib(default=None, repr=False, eq=False, hash=False) + app: rest_app.IRESTApp = attr.ib(default=None, repr=False, eq=False, hash=False) """The client application that models may use for procedures.""" channel_id: snowflake.Snowflake = attr.ib(repr=True) @@ -238,7 +238,7 @@ class MessageDeleteBulkEvent(base_events.Event): class MessageReactionEvent(base_events.Event): """A base class that all message reaction events will inherit from.""" - app: rest.IRESTClient = attr.ib(default=None, repr=False, eq=False, hash=False) + app: rest_app.IRESTApp = attr.ib(default=None, repr=False, eq=False, hash=False) """The client application that models may use for procedures.""" channel_id: snowflake.Snowflake = attr.ib(repr=True) diff --git a/hikari/events/other.py b/hikari/events/other.py index 34560bd3b3..920ab4f749 100644 --- a/hikari/events/other.py +++ b/hikari/events/other.py @@ -36,12 +36,12 @@ import attr +from hikari.api import gateway as gateway_client from hikari.events import base as base_events if typing.TYPE_CHECKING: from hikari.models import guilds from hikari.models import users - from hikari.impl import gateway as gateway_client from hikari.utilities import snowflake @@ -90,7 +90,7 @@ class StoppedEvent(base_events.Event): class ConnectedEvent(base_events.Event): """Event invoked each time a shard connects.""" - shard: gateway_client.Gateway = attr.ib(repr=True) + shard: gateway_client.IGatewayShard = attr.ib(repr=True) """The shard that connected.""" @@ -99,7 +99,7 @@ class ConnectedEvent(base_events.Event): class DisconnectedEvent(base_events.Event): """Event invoked each time a shard disconnects.""" - shard: gateway_client.Gateway = attr.ib(repr=True) + shard: gateway_client.IGatewayShard = attr.ib(repr=True) """The shard that disconnected.""" @@ -107,7 +107,7 @@ class DisconnectedEvent(base_events.Event): class ResumedEvent(base_events.Event): """Represents a gateway Resume event.""" - shard: gateway_client.Gateway = attr.ib(repr=True) + shard: gateway_client.IGatewayShard = attr.ib(repr=True) """The shard that reconnected.""" @@ -118,7 +118,7 @@ class ReadyEvent(base_events.Event): This is received only when IDENTIFYing with the gateway. """ - shard: gateway_client.Gateway = attr.ib(repr=False) + shard: gateway_client.IGatewayShard = attr.ib(repr=False) """The shard that is ready.""" gateway_version: int = attr.ib(repr=True) diff --git a/hikari/impl/bot.py b/hikari/impl/bot.py index 1623e627c2..aa6e6c6ef0 100644 --- a/hikari/impl/bot.py +++ b/hikari/impl/bot.py @@ -32,16 +32,16 @@ import time import typing +from hikari import config from hikari.api import bot from hikari.impl import cache as cache_impl -from hikari.impl import config +from hikari.impl import constants from hikari.impl import entity_factory as entity_factory_impl from hikari.impl import event_manager from hikari.impl import gateway_zookeeper -from hikari.impl import http from hikari.impl import rate_limits +from hikari.impl import rest_client as rest_client_impl from hikari.impl import stateless_cache as stateless_cache_impl -from hikari.impl import strings from hikari.models import presences from hikari.utilities import date from hikari.utilities import undefined @@ -53,6 +53,7 @@ from hikari.api import entity_factory as entity_factory_ from hikari.api import event_consumer as event_consumer_ from hikari.api import event_dispatcher as event_dispatcher_ + from hikari.api import rest_client from hikari.events import base as base_events from hikari.models import gateway as gateway_models from hikari.models import intents as intents_ @@ -78,7 +79,7 @@ class BotAppImpl(gateway_zookeeper.AbstractGatewayZookeeper, bot.IBotApp): The version of the gateway to connect to. At the time of writing, only version `6` and version `7` (undocumented development release) are supported. This defaults to using v6. - http_settings : hikari.impl.config.HTTPSettings or None + http_settings : hikari.config.HTTPSettings or None The HTTP-related settings to use. initial_activity : hikari.models.presences.Activity or None or hikari.utilities.undefined.UndefinedType The initial activity to have on each shard. @@ -107,10 +108,10 @@ class BotAppImpl(gateway_zookeeper.AbstractGatewayZookeeper, bot.IBotApp): or simply wish to initialize it in your own time instead. !!! note - Initializating logging means already have a handler in the root logger. - This is usually achived by calling `logging.basicConfig` or adding the - handler another way. - proxy_settings : hikari.impl.config.ProxySettings or None + Initializing logging means already have a handler in the root + logger. This is usually achieved by calling `logging.basicConfig` + or adding the handler manually. + proxy_settings : hikari.config.ProxySettings or None Settings to use for the proxy. rest_version : int The version of the HTTP API to connect to. At the time of writing, @@ -230,7 +231,7 @@ def __init__( version=gateway_version, ) - self._rest = http.HTTP( # noqa: S106 - Possible hardcoded password + self._rest = rest_client_impl.RESTClientImpl( # noqa: S106 - Possible hardcoded password app=self, connector=None, connector_owner=True, @@ -239,7 +240,7 @@ def __init__( global_ratelimit=self._global_ratelimit, proxy_settings=self._proxy_settings, token=token, - token_type=strings.BOT_TOKEN, # nosec + token_type=constants.BOT_TOKEN, # nosec rest_url=rest_url, version=rest_version, ) @@ -273,7 +274,7 @@ def proxy_settings(self) -> config.ProxySettings: return self._proxy_settings @property - def rest(self) -> http.HTTP: + def rest(self) -> rest_client.IRESTClient: return self._rest @property diff --git a/hikari/impl/cache.py b/hikari/impl/cache.py index 2f21c402fc..e334210afc 100644 --- a/hikari/impl/cache.py +++ b/hikari/impl/cache.py @@ -26,16 +26,16 @@ from hikari.api import cache if typing.TYPE_CHECKING: - from hikari.api import rest + from hikari.api import rest_app class InMemoryCacheComponentImpl(cache.ICacheComponent): """In-memory cache implementation.""" - def __init__(self, app: rest.IRESTClient) -> None: + def __init__(self, app: rest_app.IRESTApp) -> None: self._app = app @property @typing.final - def app(self) -> rest.IRESTClient: + def app(self) -> rest_app.IRESTApp: return self._app diff --git a/hikari/impl/strings.py b/hikari/impl/constants.py similarity index 100% rename from hikari/impl/strings.py rename to hikari/impl/constants.py diff --git a/hikari/impl/entity_factory.py b/hikari/impl/entity_factory.py index 21f31c6f47..a72e2f489e 100644 --- a/hikari/impl/entity_factory.py +++ b/hikari/impl/entity_factory.py @@ -25,13 +25,13 @@ import typing from hikari.api import entity_factory -from hikari.api import rest +from hikari.api import gateway +from hikari.api import rest_app from hikari.events import channel as channel_events from hikari.events import guild as guild_events from hikari.events import message as message_events from hikari.events import other as other_events from hikari.events import voice as voice_events -from hikari.impl import gateway from hikari.models import applications as application_models from hikari.models import audit_logs as audit_log_models from hikari.models import channels as channel_models @@ -78,7 +78,7 @@ class EntityFactoryComponentImpl(entity_factory.IEntityFactoryComponent): This will convert objects to/from JSON compatible representations. """ - def __init__(self, app: rest.IRESTClient) -> None: + def __init__(self, app: rest_app.IRESTApp) -> None: self._app = app self._audit_log_entry_converters: typing.Mapping[str, typing.Callable[[typing.Any], typing.Any]] = { audit_log_models.AuditLogChangeKey.OWNER_ID: snowflake.Snowflake, @@ -141,7 +141,7 @@ def __init__(self, app: rest.IRESTClient) -> None: @property @typing.final - def app(self) -> rest.IRESTClient: + def app(self) -> rest_app.IRESTApp: return self._app ###################### @@ -706,6 +706,7 @@ def deserialize_known_custom_emoji(self, payload: data_binding.JSONObject) -> em known_custom_emoji = emoji_models.KnownCustomEmoji() known_custom_emoji.app = self._app known_custom_emoji.id = snowflake.Snowflake(payload["id"]) + # noinspection PyPropertyAccess known_custom_emoji.name = payload["name"] known_custom_emoji.is_animated = payload.get("animated", False) known_custom_emoji.role_ids = {snowflake.Snowflake(role_id) for role_id in payload.get("roles", ())} @@ -1792,7 +1793,7 @@ def deserialize_message_reaction_remove_emoji_event( ################ def deserialize_ready_event( - self, shard: gateway.Gateway, payload: data_binding.JSONObject + self, shard: gateway.IGatewayShard, payload: data_binding.JSONObject ) -> other_events.ReadyEvent: ready_event = other_events.ReadyEvent() ready_event.shard = shard diff --git a/hikari/impl/event_manager.py b/hikari/impl/event_manager.py index 28a8e8296c..2e76117ac5 100644 --- a/hikari/impl/event_manager.py +++ b/hikari/impl/event_manager.py @@ -27,14 +27,14 @@ from hikari.impl import event_manager_core if typing.TYPE_CHECKING: - from hikari.impl import gateway + from hikari.api import gateway from hikari.utilities import data_binding class EventManagerImpl(event_manager_core.EventManagerCoreComponent): """Provides event handling logic for Discord events.""" - async def on_connected(self, shard: gateway.Gateway, _: data_binding.JSONObject) -> None: + async def on_connected(self, shard: gateway.IGatewayShard, _: data_binding.JSONObject) -> None: """Handle connection events. This is a synthetic event produced by the gateway implementation in @@ -43,7 +43,7 @@ async def on_connected(self, shard: gateway.Gateway, _: data_binding.JSONObject) # TODO: this should be in entity factory await self.dispatch(other.ConnectedEvent(shard=shard)) - async def on_disconnected(self, shard: gateway.Gateway, _: data_binding.JSONObject) -> None: + async def on_disconnected(self, shard: gateway.IGatewayShard, _: data_binding.JSONObject) -> None: """Handle disconnection events. This is a synthetic event produced by the gateway implementation in @@ -52,151 +52,153 @@ async def on_disconnected(self, shard: gateway.Gateway, _: data_binding.JSONObje # TODO: this should be in entity factory await self.dispatch(other.DisconnectedEvent(shard=shard)) - async def on_ready(self, shard: gateway.Gateway, payload: data_binding.JSONObject) -> None: + async def on_ready(self, shard: gateway.IGatewayShard, payload: data_binding.JSONObject) -> None: """See https://discord.com/developers/docs/topics/gateway#ready for more info.""" await self.dispatch(self.app.entity_factory.deserialize_ready_event(shard, payload)) - async def on_resumed(self, shard: gateway.Gateway, _: data_binding.JSONObject) -> None: + async def on_resumed(self, shard: gateway.IGatewayShard, _: data_binding.JSONObject) -> None: """See https://discord.com/developers/docs/topics/gateway#resumed for more info.""" # TODO: this should be in entity factory await self.dispatch(other.ResumedEvent(shard=shard)) - async def on_channel_create(self, _: gateway.Gateway, payload: data_binding.JSONObject) -> None: + async def on_channel_create(self, _: gateway.IGatewayShard, payload: data_binding.JSONObject) -> None: """See https://discord.com/developers/docs/topics/gateway#channel-create for more info.""" await self.dispatch(self.app.entity_factory.deserialize_channel_create_event(payload)) - async def on_channel_update(self, _: gateway.Gateway, payload: data_binding.JSONObject) -> None: + async def on_channel_update(self, _: gateway.IGatewayShard, payload: data_binding.JSONObject) -> None: """See https://discord.com/developers/docs/topics/gateway#channel-update for more info.""" await self.dispatch(self.app.entity_factory.deserialize_channel_update_event(payload)) - async def on_channel_delete(self, _: gateway.Gateway, payload: data_binding.JSONObject) -> None: + async def on_channel_delete(self, _: gateway.IGatewayShard, payload: data_binding.JSONObject) -> None: """See https://discord.com/developers/docs/topics/gateway#channel-delete for more info.""" await self.dispatch(self.app.entity_factory.deserialize_channel_delete_event(payload)) - async def on_channel_pins_update(self, _: gateway.Gateway, payload: data_binding.JSONObject) -> None: + async def on_channel_pins_update(self, _: gateway.IGatewayShard, payload: data_binding.JSONObject) -> None: """See https://discord.com/developers/docs/topics/gateway#channel-pins-update for more info.""" await self.dispatch(self.app.entity_factory.deserialize_channel_pins_update_event(payload)) - async def on_guild_create(self, _: gateway.Gateway, payload: data_binding.JSONObject) -> None: + async def on_guild_create(self, _: gateway.IGatewayShard, payload: data_binding.JSONObject) -> None: """See https://discord.com/developers/docs/topics/gateway#guild-create for more info.""" await self.dispatch(self.app.entity_factory.deserialize_guild_create_event(payload)) - async def on_guild_update(self, _: gateway.Gateway, payload: data_binding.JSONObject) -> None: + async def on_guild_update(self, _: gateway.IGatewayShard, payload: data_binding.JSONObject) -> None: """See https://discord.com/developers/docs/topics/gateway#guild-update for more info.""" await self.dispatch(self.app.entity_factory.deserialize_guild_update_event(payload)) - async def on_guild_delete(self, _: gateway.Gateway, payload: data_binding.JSONObject) -> None: + async def on_guild_delete(self, _: gateway.IGatewayShard, payload: data_binding.JSONObject) -> None: """See https://discord.com/developers/docs/topics/gateway#guild-delete for more info.""" if payload.get("unavailable", False): await self.dispatch(self.app.entity_factory.deserialize_guild_unavailable_event(payload)) else: await self.dispatch(self.app.entity_factory.deserialize_guild_leave_event(payload)) - async def on_guild_ban_add(self, _: gateway.Gateway, payload: data_binding.JSONObject) -> None: + async def on_guild_ban_add(self, _: gateway.IGatewayShard, payload: data_binding.JSONObject) -> None: """See https://discord.com/developers/docs/topics/gateway#guild-ban-add for more info.""" await self.dispatch(self.app.entity_factory.deserialize_guild_ban_add_event(payload)) - async def on_guild_ban_remove(self, _: gateway.Gateway, payload: data_binding.JSONObject) -> None: + async def on_guild_ban_remove(self, _: gateway.IGatewayShard, payload: data_binding.JSONObject) -> None: """See https://discord.com/developers/docs/topics/gateway#guild-ban-remove for more info.""" await self.dispatch(self.app.entity_factory.deserialize_guild_ban_remove_event(payload)) - async def on_guild_emojis_update(self, _: gateway.Gateway, payload: data_binding.JSONObject) -> None: + async def on_guild_emojis_update(self, _: gateway.IGatewayShard, payload: data_binding.JSONObject) -> None: """See https://discord.com/developers/docs/topics/gateway#guild-emojis-update for more info.""" await self.dispatch(self.app.entity_factory.deserialize_guild_emojis_update_event(payload)) - async def on_guild_integrations_update(self, _: gateway.Gateway, payload: data_binding.JSONObject) -> None: + async def on_guild_integrations_update(self, _: gateway.IGatewayShard, payload: data_binding.JSONObject) -> None: """See https://discord.com/developers/docs/topics/gateway#guild-integrations-update for more info.""" await self.dispatch(self.app.entity_factory.deserialize_guild_integrations_update_event(payload)) - async def on_guild_member_add(self, _: gateway.Gateway, payload: data_binding.JSONObject) -> None: + async def on_guild_member_add(self, _: gateway.IGatewayShard, payload: data_binding.JSONObject) -> None: """See https://discord.com/developers/docs/topics/gateway#guild-member-add for more info.""" await self.dispatch(self.app.entity_factory.deserialize_guild_member_add_event(payload)) - async def on_guild_member_remove(self, _: gateway.Gateway, payload: data_binding.JSONObject) -> None: + async def on_guild_member_remove(self, _: gateway.IGatewayShard, payload: data_binding.JSONObject) -> None: """See https://discord.com/developers/docs/topics/gateway#guild-member-remove for more info.""" await self.dispatch(self.app.entity_factory.deserialize_guild_member_remove_event(payload)) - async def on_guild_member_update(self, _: gateway.Gateway, payload: data_binding.JSONObject) -> None: + async def on_guild_member_update(self, _: gateway.IGatewayShard, payload: data_binding.JSONObject) -> None: """See https://discord.com/developers/docs/topics/gateway#guild-member-update for more info.""" await self.dispatch(self.app.entity_factory.deserialize_guild_member_update_event(payload)) - async def on_guild_members_chunk(self, _: gateway.Gateway, payload: data_binding.JSONObject) -> None: + async def on_guild_members_chunk(self, _: gateway.IGatewayShard, payload: data_binding.JSONObject) -> None: """See https://discord.com/developers/docs/topics/gateway#guild-members-chunk for more info.""" # TODO: implement model for this, and implement chunking components. # await self.dispatch(self.app.entity_factory.deserialize_guild_member_chunk_event(payload)) - async def on_guild_role_create(self, _: gateway.Gateway, payload: data_binding.JSONObject) -> None: + async def on_guild_role_create(self, _: gateway.IGatewayShard, payload: data_binding.JSONObject) -> None: """See https://discord.com/developers/docs/topics/gateway#guild-role-create for more info.""" await self.dispatch(self.app.entity_factory.deserialize_guild_role_create_event(payload)) - async def on_guild_role_update(self, _: gateway.Gateway, payload: data_binding.JSONObject) -> None: + async def on_guild_role_update(self, _: gateway.IGatewayShard, payload: data_binding.JSONObject) -> None: """See https://discord.com/developers/docs/topics/gateway#guild-role-update for more info.""" await self.dispatch(self.app.entity_factory.deserialize_guild_role_update_event(payload)) - async def on_guild_role_delete(self, _: gateway.Gateway, payload: data_binding.JSONObject) -> None: + async def on_guild_role_delete(self, _: gateway.IGatewayShard, payload: data_binding.JSONObject) -> None: """See https://discord.com/developers/docs/topics/gateway#guild-role-delete for more info.""" await self.dispatch(self.app.entity_factory.deserialize_guild_role_delete_event(payload)) - async def on_invite_create(self, _: gateway.Gateway, payload: data_binding.JSONObject) -> None: + async def on_invite_create(self, _: gateway.IGatewayShard, payload: data_binding.JSONObject) -> None: """See https://discord.com/developers/docs/topics/gateway#invite-create for more info.""" await self.dispatch(self.app.entity_factory.deserialize_invite_create_event(payload)) - async def on_invite_delete(self, _: gateway.Gateway, payload: data_binding.JSONObject) -> None: + async def on_invite_delete(self, _: gateway.IGatewayShard, payload: data_binding.JSONObject) -> None: """See https://discord.com/developers/docs/topics/gateway#invite-delete for more info.""" await self.dispatch(self.app.entity_factory.deserialize_invite_delete_event(payload)) - async def on_message_create(self, _: gateway.Gateway, payload: data_binding.JSONObject) -> None: + async def on_message_create(self, _: gateway.IGatewayShard, payload: data_binding.JSONObject) -> None: """See https://discord.com/developers/docs/topics/gateway#message-create for more info.""" await self.dispatch(self.app.entity_factory.deserialize_message_create_event(payload)) - async def on_message_update(self, _: gateway.Gateway, payload: data_binding.JSONObject) -> None: + async def on_message_update(self, _: gateway.IGatewayShard, payload: data_binding.JSONObject) -> None: """See https://discord.com/developers/docs/topics/gateway#message-update for more info.""" await self.dispatch(self.app.entity_factory.deserialize_message_update_event(payload)) - async def on_message_delete(self, _: gateway.Gateway, payload: data_binding.JSONObject) -> None: + async def on_message_delete(self, _: gateway.IGatewayShard, payload: data_binding.JSONObject) -> None: """See https://discord.com/developers/docs/topics/gateway#message-delete for more info.""" await self.dispatch(self.app.entity_factory.deserialize_message_delete_event(payload)) - async def on_message_delete_bulk(self, _: gateway.Gateway, payload: data_binding.JSONObject) -> None: + async def on_message_delete_bulk(self, _: gateway.IGatewayShard, payload: data_binding.JSONObject) -> None: """See https://discord.com/developers/docs/topics/gateway#message-delete-bulk for more info.""" await self.dispatch(self.app.entity_factory.deserialize_message_delete_bulk_event(payload)) - async def on_message_reaction_add(self, _: gateway.Gateway, payload: data_binding.JSONObject) -> None: + async def on_message_reaction_add(self, _: gateway.IGatewayShard, payload: data_binding.JSONObject) -> None: """See https://discord.com/developers/docs/topics/gateway#message-reaction-add for more info.""" await self.dispatch(self.app.entity_factory.deserialize_message_reaction_add_event(payload)) - async def on_message_reaction_remove(self, _: gateway.Gateway, payload: data_binding.JSONObject) -> None: + async def on_message_reaction_remove(self, _: gateway.IGatewayShard, payload: data_binding.JSONObject) -> None: """See https://discord.com/developers/docs/topics/gateway#message-reaction-remove for more info.""" await self.dispatch(self.app.entity_factory.deserialize_message_reaction_remove_event(payload)) - async def on_message_reaction_remove_all(self, _: gateway.Gateway, payload: data_binding.JSONObject) -> None: + async def on_message_reaction_remove_all(self, _: gateway.IGatewayShard, payload: data_binding.JSONObject) -> None: """See https://discord.com/developers/docs/topics/gateway#message-reaction-remove-all for more info.""" await self.dispatch(self.app.entity_factory.deserialize_message_reaction_remove_all_event(payload)) - async def on_message_reaction_remove_emoji(self, _: gateway.Gateway, payload: data_binding.JSONObject) -> None: + async def on_message_reaction_remove_emoji( + self, _: gateway.IGatewayShard, payload: data_binding.JSONObject + ) -> None: """See https://discord.com/developers/docs/topics/gateway#message-reaction-remove-emoji for more info.""" await self.dispatch(self.app.entity_factory.deserialize_message_reaction_remove_emoji_event(payload)) - async def on_presence_update(self, _: gateway.Gateway, payload: data_binding.JSONObject) -> None: + async def on_presence_update(self, _: gateway.IGatewayShard, payload: data_binding.JSONObject) -> None: """See https://discord.com/developers/docs/topics/gateway#presence-update for more info.""" await self.dispatch(self.app.entity_factory.deserialize_presence_update_event(payload)) - async def on_typing_start(self, _: gateway.Gateway, payload: data_binding.JSONObject) -> None: + async def on_typing_start(self, _: gateway.IGatewayShard, payload: data_binding.JSONObject) -> None: """See https://discord.com/developers/docs/topics/gateway#typing-start for more info.""" await self.dispatch(self.app.entity_factory.deserialize_typing_start_event(payload)) - async def on_user_update(self, _: gateway.Gateway, payload: data_binding.JSONObject) -> None: + async def on_user_update(self, _: gateway.IGatewayShard, payload: data_binding.JSONObject) -> None: """See https://discord.com/developers/docs/topics/gateway#user-update for more info.""" await self.dispatch(self.app.entity_factory.deserialize_own_user_update_event(payload)) - async def on_voice_state_update(self, _: gateway.Gateway, payload: data_binding.JSONObject) -> None: + async def on_voice_state_update(self, _: gateway.IGatewayShard, payload: data_binding.JSONObject) -> None: """See https://discord.com/developers/docs/topics/gateway#voice-state-update for more info.""" await self.dispatch(self.app.entity_factory.deserialize_voice_state_update_event(payload)) - async def on_voice_server_update(self, _: gateway.Gateway, payload: data_binding.JSONObject) -> None: + async def on_voice_server_update(self, _: gateway.IGatewayShard, payload: data_binding.JSONObject) -> None: """See https://discord.com/developers/docs/topics/gateway#voice-server-update for more info.""" await self.dispatch(self.app.entity_factory.deserialize_voice_server_update_event(payload)) - async def on_webhooks_update(self, _: gateway.Gateway, payload: data_binding.JSONObject) -> None: + async def on_webhooks_update(self, _: gateway.IGatewayShard, payload: data_binding.JSONObject) -> None: """See https://discord.com/developers/docs/topics/gateway#webhooks-update for more info.""" await self.dispatch(self.app.entity_factory.deserialize_webhook_update_event(payload)) diff --git a/hikari/impl/event_manager_core.py b/hikari/impl/event_manager_core.py index d2ce433e8a..aa6d4cf01a 100644 --- a/hikari/impl/event_manager_core.py +++ b/hikari/impl/event_manager_core.py @@ -30,14 +30,14 @@ from hikari.api import event_dispatcher from hikari.events import base from hikari.events import other -from hikari.impl import gateway from hikari.utilities import aio from hikari.utilities import data_binding from hikari.utilities import reflect from hikari.utilities import undefined if typing.TYPE_CHECKING: - from hikari.api import rest + from hikari.api import gateway + from hikari.api import rest_app _LOGGER: typing.Final[logging.Logger] = logging.getLogger("hikari") @@ -59,18 +59,18 @@ class EventManagerCoreComponent(event_dispatcher.IEventDispatcherComponent, even is the raw event name being dispatched in lower-case. """ - def __init__(self, app: rest.IRESTClient) -> None: + def __init__(self, app: rest_app.IRESTApp) -> None: self._app = app self._listeners: ListenerMapT = {} self._waiters: WaiterMapT = {} @property @typing.final - def app(self) -> rest.IRESTClient: + def app(self) -> rest_app.IRESTApp: return self._app async def consume_raw_event( - self, shard: gateway.Gateway, event_name: str, payload: data_binding.JSONObject + self, shard: gateway.IGatewayShard, event_name: str, payload: data_binding.JSONObject ) -> None: try: callback = getattr(self, "on_" + event_name.lower()) diff --git a/hikari/impl/gateway.py b/hikari/impl/gateway.py index ef998084bb..74d3cab6de 100644 --- a/hikari/impl/gateway.py +++ b/hikari/impl/gateway.py @@ -19,7 +19,7 @@ from __future__ import annotations -__all__: typing.Final[typing.Sequence[str]] = ["Gateway"] +__all__: typing.Final[typing.Sequence[str]] = ["GatewayShardImpl"] import asyncio import enum @@ -34,8 +34,9 @@ import attr from hikari import errors +from hikari.api import gateway +from hikari.impl import constants from hikari.impl import rate_limits -from hikari.impl import strings from hikari.models import presences from hikari.utilities import data_binding from hikari.utilities import undefined @@ -43,15 +44,15 @@ if typing.TYPE_CHECKING: import datetime + from hikari import config from hikari.api import event_consumer - from hikari.impl import config from hikari.models import channels from hikari.models import guilds from hikari.models import intents as intents_ from hikari.utilities import snowflake -class Gateway: +class GatewayShardImpl(gateway.IGatewayShard): """Implementation of a V6 and V7 compatible gateway. Parameters @@ -61,7 +62,7 @@ class Gateway: debug : bool If `True`, each sent and received payload is dumped to the logs. If `False`, only the fact that data has been sent/received will be logged. - http_settings : hikari.impl.config.HTTPSettings + http_settings : hikari.config.HTTPSettings The HTTP-related settings to use while negotiating a websocket. initial_activity : hikari.models.presences.Activity or None or hikari.utilities.undefined.UndefinedType The initial activity to appear to have for this shard. @@ -75,7 +76,7 @@ class Gateway: Collection of intents to use, or `None` to not use intents at all. large_threshold : int The number of members to have in a guild for it to be considered large. - proxy_settings : hikari.impl.config.ProxySettings + proxy_settings : hikari.config.ProxySettings The proxy settings to use while negotiating a websocket. shard_id : int The shard ID. @@ -245,19 +246,11 @@ def app(self) -> event_consumer.IEventConsumerApp: return self._app @property + @typing.final def is_alive(self) -> bool: - """Return whether the shard is alive.""" return not math.isnan(self.connected_at) async def start(self) -> asyncio.Task[None]: - """Start the shard, wait for it to become ready. - - Returns - ------- - asyncio.Task - The task containing the shard running logic. Awaiting this will - wait until the shard has shut down before returning. - """ run_task = asyncio.create_task(self._run(), name=f"shard {self._shard_id} keep-alive") await self._handshake_event.wait() return run_task @@ -442,22 +435,6 @@ async def update_presence( activity: typing.Union[undefined.UndefinedType, None, presences.Activity] = undefined.UNDEFINED, status: typing.Union[undefined.UndefinedType, presences.Status] = undefined.UNDEFINED, ) -> None: - """Update the presence of the shard user. - - Parameters - ---------- - idle_since : datetime.datetime or None or hikari.utilities.undefined.UndefinedType - The datetime that the user started being idle. If undefined, this - will not be changed. - afk : bool or hikari.utilities.undefined.UndefinedType - If `True`, the user is marked as AFK. If `False`, the user is marked - as being active. If undefined, this will not be changed. - activity : hikari.models.presences.Activity or None or hikari.utilities.undefined.UndefinedType - The activity to appear to be playing. If undefined, this will not be - changed. - status : hikari.models.presences.Status or hikari.utilities.undefined.UndefinedType - The web status to show. If undefined, this will not be changed. - """ if idle_since is undefined.UNDEFINED: idle_since = self._idle_since if afk is undefined.UNDEFINED: @@ -489,23 +466,6 @@ async def update_voice_state( self_mute: bool = False, self_deaf: bool = False, ) -> None: - """Update the voice state for this shard in a given guild. - - Parameters - ---------- - guild : hikari.models.guilds.PartialGuild or hikari.utilities.snowflake.UniqueObject - The guild or guild ID to update the voice state for. - channel : hikari.models.channels.GuildVoiceChannel or hikari.utilities.snowflake.UniqueObject or None - The channel or channel ID to update the voice state for. If `None` - then the bot will leave the voice channel that it is in for the - given guild. - self_mute : bool - If `True`, the bot will mute itself in that voice channel. If - `False`, then it will unmute itself. - self_deaf : bool - If `True`, the bot will deafen itself in that voice channel. If - `False`, then it will undeafen itself. - """ payload = self._app.entity_factory.serialize_gateway_voice_state_update(guild, channel, self_mute, self_deaf) await self._send_json({"op": self._GatewayOpcode.VOICE_STATE_UPDATE, "d": payload}) @@ -537,9 +497,9 @@ async def _identify(self) -> None: "compress": False, "large_threshold": self.large_threshold, "properties": { - "$os": strings.SYSTEM_TYPE, - "$browser": strings.AIOHTTP_VERSION, - "$device": strings.LIBRARY_VERSION, + "$os": constants.SYSTEM_TYPE, + "$browser": constants.AIOHTTP_VERSION, + "$device": constants.LIBRARY_VERSION, }, "shard": [self._shard_id, self._shard_count], }, diff --git a/hikari/impl/gateway_zookeeper.py b/hikari/impl/gateway_zookeeper.py index 8f9a305f16..fbfa14f2a3 100644 --- a/hikari/impl/gateway_zookeeper.py +++ b/hikari/impl/gateway_zookeeper.py @@ -32,15 +32,16 @@ import typing from hikari.api import event_dispatcher +from hikari.api import gateway from hikari.api import gateway_zookeeper from hikari.events import other -from hikari.impl import gateway +from hikari.impl import gateway as gateway_impl from hikari.utilities import aio from hikari.utilities import undefined if typing.TYPE_CHECKING: + from hikari import config from hikari.events import base as base_events - from hikari.impl import config from hikari.models import gateway as gateway_models from hikari.models import intents as intents_ from hikari.models import presences @@ -69,7 +70,7 @@ class AbstractGatewayZookeeper(gateway_zookeeper.IGatewayZookeeperApp, abc.ABC): on the gateway will be dumped to debug logs. This will provide useful debugging context at the cost of performance. Generally you do not need to enable this. - http_settings : hikari.impl.config.HTTPSettings + http_settings : hikari.config.HTTPSettings HTTP-related configuration. initial_activity : hikari.models.presences.Activity or None or hikari.utilities.undefined.UndefinedType The initial activity to have on each shard. @@ -88,7 +89,7 @@ class AbstractGatewayZookeeper(gateway_zookeeper.IGatewayZookeeperApp, abc.ABC): large_threshold : int The number of members that need to be in a guild for the guild to be considered large. Defaults to the maximum, which is `250`. - proxy_settings : hikari.impl.config.ProxySettings + proxy_settings : hikari.config.ProxySettings Proxy-related configuration. shard_ids : typing.Set[int] or None A set of every shard ID that should be created and started on startup. @@ -172,14 +173,14 @@ def __init__( self._request_close_event = asyncio.Event() self._shard_count: int = shard_count if shard_count is not None else 0 self._shard_ids: typing.Set[int] = set() if shard_ids is None else shard_ids - self._shards: typing.Dict[int, gateway.Gateway] = {} + self._shards: typing.Dict[int, gateway.IGatewayShard] = {} self._tasks: typing.Dict[int, asyncio.Task[typing.Any]] = {} self._token = token self._use_compression = compression self._version = version @property - def shards(self) -> typing.Mapping[int, gateway.Gateway]: + def shards(self) -> typing.Mapping[int, gateway.IGatewayShard]: return self._shards @property @@ -322,9 +323,9 @@ async def _init(self) -> None: reset_at = gw_recs.session_start_limit.reset_at.strftime("%d/%m/%y %H:%M:%S %Z").rstrip() - shard_clients: typing.Dict[int, gateway.Gateway] = {} + shard_clients: typing.Dict[int, gateway.IGatewayShard] = {} for shard_id in self._shard_ids: - shard = gateway.Gateway( + shard = gateway_impl.GatewayShardImpl( app=self, debug=self._debug, http_settings=self._http_settings, diff --git a/hikari/impl/rate_limits.py b/hikari/impl/rate_limits.py index e5c730ddc5..2b3bad8260 100644 --- a/hikari/impl/rate_limits.py +++ b/hikari/impl/rate_limits.py @@ -40,7 +40,7 @@ if typing.TYPE_CHECKING: import types -_LOGGER: typing.Final[logging.Logger] = logging.getLogger("hikari.impl.ratelimits") +_LOGGER: typing.Final[logging.Logger] = logging.getLogger("hikari.ratelimits") class BaseRateLimiter(abc.ABC): diff --git a/hikari/impl/helpers.py b/hikari/impl/response_handler.py similarity index 100% rename from hikari/impl/helpers.py rename to hikari/impl/response_handler.py diff --git a/hikari/impl/rest_app.py b/hikari/impl/rest_app.py index 27c3fb1930..54549e9d74 100644 --- a/hikari/impl/rest_app.py +++ b/hikari/impl/rest_app.py @@ -23,19 +23,20 @@ from __future__ import annotations -__all__: typing.Final[typing.Sequence[str]] = ["RESTClientFactoryImpl", "RESTClientImpl"] +__all__: typing.Final[typing.Sequence[str]] = ["RESTAppFactoryImpl", "RESTClientImpl"] import typing import aiohttp -from hikari.api import rest as rest_api -from hikari.impl import config +from hikari import config +from hikari.api import rest_app +from hikari.api import rest_client +from hikari.impl import constants from hikari.impl import entity_factory as entity_factory_impl -from hikari.impl import http as rest_component from hikari.impl import rate_limits +from hikari.impl import rest_client as rest_client_impl from hikari.impl import stateless_cache -from hikari.impl import strings if typing.TYPE_CHECKING: import concurrent.futures @@ -45,7 +46,7 @@ from hikari.api import entity_factory as entity_factory_ -class RESTClientImpl(rest_api.IRESTClientContextManager): +class RESTClientImpl(rest_app.IRESTAppContextManager): """Client for a specific set of credentials within a HTTP-only application. Parameters @@ -61,9 +62,9 @@ class RESTClientImpl(rest_api.IRESTClientContextManager): expect this to be a connection pool). global_ratelimit : hikari.impl.rate_limits.ManualRateLimiter The global ratelimiter. - http_settings : hikari.impl.config.HTTPSettings + http_settings : hikari.config.HTTPSettings HTTP-related settings. - proxy_settings : hikari.impl.config.ProxySettings + proxy_settings : hikari.config.ProxySettings Proxy-related settings. token : str or None If defined, the token to use. If not defined, no token will be injected @@ -98,7 +99,7 @@ def __init__( self._http_settings = http_settings self._proxy_settings = proxy_settings - self._rest = rest_component.HTTP( + self._rest = rest_client_impl.RESTClientImpl( app=self, connector=connector, connector_owner=False, @@ -138,13 +139,13 @@ def proxy_settings(self) -> config.ProxySettings: return self._proxy_settings @property - def rest(self) -> rest_component.HTTP: + def rest(self) -> rest_client.IRESTClient: return self._rest async def close(self) -> None: await self._rest.close() - async def __aenter__(self) -> rest_api.IRESTClientContextManager: + async def __aenter__(self) -> rest_app.IRESTAppContextManager: return self async def __aexit__( @@ -156,11 +157,11 @@ async def __aexit__( await self.close() -class RESTClientFactoryImpl(rest_api.IRESTClientFactory): +class RESTAppFactoryImpl(rest_app.IRESTAppFactory): """The base for a HTTP-only Discord application. This comprises of a shared TCP connector connection pool, and can have - `hikari.api.rest.IRESTClient` instances for specific credentials acquired + `hikari.api.rest.IRESTApp` instances for specific credentials acquired from it. Parameters @@ -205,7 +206,7 @@ def http_settings(self) -> config.HTTPSettings: def proxy_settings(self) -> config.ProxySettings: return self._proxy_settings - def acquire(self, token: str, token_type: str = strings.BEARER_TOKEN) -> rest_api.IRESTClientContextManager: + def acquire(self, token: str, token_type: str = constants.BEARER_TOKEN) -> rest_app.IRESTAppContextManager: return RESTClientImpl( connector=self._connector, debug=self._debug, @@ -223,7 +224,7 @@ async def close(self) -> None: await self._connector.close() self._global_ratelimit.close() - async def __aenter__(self) -> RESTClientFactoryImpl: + async def __aenter__(self) -> RESTAppFactoryImpl: return self async def __aexit__( diff --git a/hikari/impl/http.py b/hikari/impl/rest_client.py similarity index 71% rename from hikari/impl/http.py rename to hikari/impl/rest_client.py index 2be6db696d..e01b43080c 100644 --- a/hikari/impl/http.py +++ b/hikari/impl/rest_client.py @@ -19,7 +19,7 @@ from __future__ import annotations -__all__: typing.Final[typing.Sequence[str]] = ["HTTP"] +__all__: typing.Final[typing.Sequence[str]] = ["RESTClientImpl"] import asyncio import contextlib @@ -33,14 +33,15 @@ import aiohttp +from hikari import config from hikari import errors +from hikari.api import rest_client from hikari.impl import buckets -from hikari.impl import config -from hikari.impl import helpers +from hikari.impl import constants from hikari.impl import rate_limits +from hikari.impl import response_handler from hikari.impl import routes from hikari.impl import special_endpoints -from hikari.impl import strings from hikari.models import embeds as embeds_ from hikari.models import emojis from hikari.utilities import data_binding @@ -51,7 +52,7 @@ from hikari.utilities import undefined if typing.TYPE_CHECKING: - from hikari.api import rest + from hikari.api import rest_app from hikari.models import applications from hikari.models import audit_logs @@ -66,12 +67,10 @@ from hikari.models import voices from hikari.models import webhooks -_LOGGER: typing.Final[logging.Logger] = logging.getLogger("hikari.impl.http") +_LOGGER: typing.Final[logging.Logger] = logging.getLogger("hikari.rest") -# TODO: make a mechanism to allow me to share the same client session but -# use various tokens for HTTP-only apps. -class HTTP: +class RESTClientImpl(rest_client.IRESTClient): """Implementation of the V6 and V7-compatible Discord HTTP API. This manages making HTTP/1.1 requests to the API and using the entity @@ -80,7 +79,7 @@ class HTTP: Parameters ---------- - app : hikari.api.rest.IRESTClient + app : hikari.api.rest_app.IRESTApp The HTTP application containing all other application components that Hikari uses. debug : bool @@ -101,7 +100,13 @@ class HTTP: The HTTP API base URL. This can contain format-string specifiers to interpolate information such as API version in use. version : int - The API version to use. + The API version to use. Currently only supports `6` and `7`. + + !!! warning + The V7 API at the time of writing is considered to be experimental and + is undocumented. While currently almost identical in most places to the + V6 API, it should not be used unless you are sure you understand the + risk that it might break without warning. """ __slots__: typing.Sequence[str] = ( @@ -135,7 +140,7 @@ class _RetryRequest(RuntimeError): def __init__( self, *, - app: rest.IRESTClient, + app: rest_app.IRESTApp, connector: typing.Optional[aiohttp.BaseConnector], connector_owner: bool, debug: bool, @@ -163,17 +168,21 @@ def __init__( full_token = None else: if token_type is None: - token_type = strings.BOT_TOKEN + token_type = constants.BOT_TOKEN full_token = f"{token_type.title()} {token}" self._token: typing.Optional[str] = full_token if rest_url is None: - rest_url = strings.REST_API_URL + rest_url = constants.REST_API_URL self._rest_url = rest_url.format(self) + @property + def app(self) -> rest_app.IRESTApp: + return self._app + @typing.final def _acquire_client_session(self) -> aiohttp.ClientSession: if self._client_session is None: @@ -213,13 +222,13 @@ async def _request( self.buckets.start() headers = data_binding.StringMapBuilder() - headers.setdefault(strings.USER_AGENT_HEADER, strings.HTTP_USER_AGENT) - headers.put(strings.X_RATELIMIT_PRECISION_HEADER, strings.MILLISECOND_PRECISION) + headers.setdefault(constants.USER_AGENT_HEADER, constants.HTTP_USER_AGENT) + headers.put(constants.X_RATELIMIT_PRECISION_HEADER, constants.MILLISECOND_PRECISION) if self._token is not None and not no_auth: - headers[strings.AUTHORIZATION_HEADER] = self._token + headers[constants.AUTHORIZATION_HEADER] = self._token - headers.put(strings.X_AUDIT_LOG_REASON_HEADER, reason) + headers.put(constants.X_AUDIT_LOG_REASON_HEADER, reason) while True: try: @@ -287,7 +296,7 @@ async def _request( # Handle the response. if 200 <= response.status < 300: - if response.content_type == strings.APPLICATION_JSON: + if response.content_type == constants.APPLICATION_JSON: # Only deserializing here stops Cloudflare shenanigans messing us around. return data_binding.load_json(await response.read()) @@ -302,7 +311,7 @@ async def _request( @staticmethod @typing.final async def _handle_error_response(response: aiohttp.ClientResponse) -> typing.NoReturn: - raise await helpers.generate_error_response(response) + raise await response_handler.generate_error_response(response) @typing.final async def _parse_ratelimits(self, compiled_route: routes.CompiledRoute, response: aiohttp.ClientResponse) -> None: @@ -311,13 +320,13 @@ async def _parse_ratelimits(self, compiled_route: routes.CompiledRoute, response # Handle rate limiting. resp_headers = response.headers - limit = int(resp_headers.get(strings.X_RATELIMIT_LIMIT_HEADER, "1")) - remaining = int(resp_headers.get(strings.X_RATELIMIT_REMAINING_HEADER, "1")) - bucket = resp_headers.get(strings.X_RATELIMIT_BUCKET_HEADER, "None") - reset_at = float(resp_headers.get(strings.X_RATELIMIT_RESET_HEADER, "0")) - reset_after = float(resp_headers.get(strings.X_RATELIMIT_RESET_AFTER_HEADER, "0")) + limit = int(resp_headers.get(constants.X_RATELIMIT_LIMIT_HEADER, "1")) + remaining = int(resp_headers.get(constants.X_RATELIMIT_REMAINING_HEADER, "1")) + bucket = resp_headers.get(constants.X_RATELIMIT_BUCKET_HEADER, "None") + reset_at = float(resp_headers.get(constants.X_RATELIMIT_RESET_HEADER, "0")) + reset_after = float(resp_headers.get(constants.X_RATELIMIT_RESET_AFTER_HEADER, "0")) reset_date = datetime.datetime.fromtimestamp(reset_at, tz=datetime.timezone.utc) - now_date = date.rfc7231_datetime_string_to_datetime(resp_headers[strings.DATE_HEADER]) + now_date = date.rfc7231_datetime_string_to_datetime(resp_headers[constants.DATE_HEADER]) is_rate_limited = response.status == http.HTTPStatus.TOO_MANY_REQUESTS @@ -333,7 +342,7 @@ async def _parse_ratelimits(self, compiled_route: routes.CompiledRoute, response if not is_rate_limited: return - if response.content_type != strings.APPLICATION_JSON: + if response.content_type != constants.APPLICATION_JSON: # We don't know exactly what this could imply. It is likely Cloudflare interfering # but I'd rather we just give up than do something resulting in multiple failed # requests repeatedly. @@ -437,30 +446,6 @@ async def close(self) -> None: async def fetch_channel( self, channel: typing.Union[channels.PartialChannel, snowflake.UniqueObject] ) -> channels.PartialChannel: - """Fetch a channel. - - Parameters - ---------- - channel : hikari.models.channels.PartialChannel or hikari.utilities.snowflake.UniqueObject - The channel to fetch. This may be a channel object, or the ID of an - existing channel. - - Returns - ------- - hikari.models.channels.PartialChannel - The fetched channel. - - Raises - ------ - hikari.errors.Unauthorized - If you are unauthorized to make the request (invalid/missing token). - hikari.errors.Forbidden - If you lack permissions to access the channel. - hikari.errors.NotFound - If the channel is not found. - hikari.errors.ServerHTTPErrorResponse - If an internal error occurs on Discord while handling the request. - """ route = routes.GET_CHANNEL.compile(channel=channel) raw_response = await self._request(route) response = typing.cast(data_binding.JSONObject, raw_response) @@ -489,53 +474,6 @@ async def edit_channel( ] = undefined.UNDEFINED, reason: typing.Union[undefined.UndefinedType, str] = undefined.UNDEFINED, ) -> channels.PartialChannel: - """Edit a channel. - - Parameters - ---------- - channel : hikari.models.channels.PartialChannel or hikari.utilities.snowflake.UniqueObject - The channel to edit. This may be a channel object, or the ID of an - existing channel. - name : hikari.utilities.undefined.UndefinedType or str - If provided, the new name for the channel. - position : hikari.utilities.undefined.UndefinedType or int - If provided, the new position for the channel. - topic : hikari.utilities.undefined.UndefinedType or str - If provided, the new topic for the channel. - nsfw : hikari.utilities.undefined.UndefinedType or bool - If provided, whether the channel should be marked as NSFW or not. - bitrate : hikari.utilities.undefined.UndefinedType or int - If provided, the new bitrate for the channel. - user_limit : hikari.utilities.undefined.UndefinedType or int - If provided, the new user limit in the channel. - rate_limit_per_user : hikari.utilities.undefined.UndefinedType or datetime.timedelta or float or int - If provided, the new rate limit per user in the channel. - permission_overwrites : hikari.utilities.undefined.UndefinedType or typing.Sequence[hikari.models.channels.PermissionOverwrite] - If provided, the new permission overwrites for the channel. - parent_category : hikari.utilities.undefined.UndefinedType or hikari.models.channels.GuildCategory or hikari.utilities.snowflake.UniqueObject - If provided, the new guild category for the channel. This may be - a category object, or the ID of an existing category. - reason : hikari.utilities.undefined.UndefinedType or str - If provided, the reason that will be recorded in the audit logs. - - Returns - ------- - hikari.models.channels.PartialChannel - The edited channel. - - Raises - ------ - hikari.errors.BadRequest - If any of the fields that are passed have an invalid value. - hikari.errors.Unauthorized - If you are unauthorized to make the request (invalid/missing token). - hikari.errors.Forbidden - If you lack permissions to edit the channel - hikari.errors.NotFound - If the channel is not found. - hikari.errors.ServerHTTPErrorResponse - If an internal error occurs on Discord while handling the request. - """ # noqa: E501 - Line too long route = routes.PATCH_CHANNEL.compile(channel=channel) body = data_binding.JSONObjectBuilder() body.put("name", name) @@ -557,57 +495,9 @@ async def edit_channel( return self._app.entity_factory.deserialize_channel(response) async def delete_channel(self, channel: typing.Union[channels.PartialChannel, snowflake.UniqueObject]) -> None: - """Delete a channel in a guild, or close a DM. - - Parameters - ---------- - channel : hikari.models.channels.PartialChannel or hikari.utilities.snowflake.UniqueObject - The channel to delete. This may be a channel object, or the ID of an - existing channel. - - Raises - ------ - hikari.errors.Unauthorized - If you are unauthorized to make the request (invalid/missing token). - hikari.errors.Forbidden - If you lack permissions to delete the channel in a guild. - hikari.errors.NotFound - If the channel is not found. - hikari.errors.ServerHTTPErrorResponse - If an internal error occurs on Discord while handling the request. - - !!! note - For Public servers, the set 'Rules' or 'Guidelines' channels and the - 'Public Server Updates' channel cannot be deleted. - """ route = routes.DELETE_CHANNEL.compile(channel=channel) await self._request(route) - @typing.overload - async def edit_permission_overwrites( - self, - channel: typing.Union[channels.GuildChannel, snowflake.UniqueObject], - target: typing.Union[channels.PermissionOverwrite, users.User, guilds.Role], - *, - allow: typing.Union[undefined.UndefinedType, permissions_.Permission] = undefined.UNDEFINED, - deny: typing.Union[undefined.UndefinedType, permissions_.Permission] = undefined.UNDEFINED, - reason: typing.Union[undefined.UndefinedType, str] = undefined.UNDEFINED, - ) -> None: - """Edit permissions for a target entity.""" - - @typing.overload - async def edit_permission_overwrites( - self, - channel: typing.Union[channels.GuildChannel, snowflake.UniqueObject], - target: typing.Union[int, str, snowflake.Snowflake], - *, - target_type: typing.Union[channels.PermissionOverwriteType, str], - allow: typing.Union[undefined.UndefinedType, permissions_.Permission] = undefined.UNDEFINED, - deny: typing.Union[undefined.UndefinedType, permissions_.Permission] = undefined.UNDEFINED, - reason: typing.Union[undefined.UndefinedType, str] = undefined.UNDEFINED, - ) -> None: - """Edit permissions for a given entity ID and type.""" - async def edit_permission_overwrites( self, channel: typing.Union[channels.GuildChannel, snowflake.UniqueObject], @@ -618,43 +508,6 @@ async def edit_permission_overwrites( deny: typing.Union[undefined.UndefinedType, permissions_.Permission] = undefined.UNDEFINED, reason: typing.Union[undefined.UndefinedType, str] = undefined.UNDEFINED, ) -> None: - """Edit permissions for a specific entity in the given guild channel. - - Parameters - ---------- - channel : hikari.models.channels.PartialChannel or hikari.utilities.snowflake.UniqueObject - The channel to edit a permission overwrite in. This may be a channel object, or - the ID of an existing channel. - target : hikari.models.users.User or hikari.models.guilds.Role or hikari.models.channels.PermissionOverwrite or hikari.utilities.snowflake.UniqueObject - The channel overwrite to edit. This may be a overwrite object, or the ID of an - existing channel. - target_type : hikari.utilities.undefined.UndefinedType or hikari.models.channels.PermissionOverwriteType or str - If provided, the type of the target to update. If unset, will attempt to get - the type from `target`. - allow : hikari.utilities.undefined.UndefinedType or hikari.models.permissions.Permission - If provided, the new vale of all allowed permissions. - deny : hikari.utilities.undefined.UndefinedType or hikari.models.permissions.Permission - If provided, the new vale of all disallowed permissions. - reason : hikari.utilities.undefined.UndefinedType or str - If provided, the reason that will be recorded in the audit logs. - - Raises - ------ - TypeError - If `target_type` is unset and we were unable to determine the type - from `target`. - hikari.errors.BadRequest - If any of the fields that are passed have an invalid value. - hikari.errors.Unauthorized - If you are unauthorized to make the request (invalid/missing token). - hikari.errors.Forbidden - If you lack permissions to edit the permission overwrites. - hikari.errors.NotFound - If the channel is not found or the target is not found if it is - a role. - hikari.errors.ServerHTTPErrorResponse - If an internal error occurs on Discord while handling the request. - """ # noqa: E501 - Line too long if target_type is undefined.UNDEFINED: if isinstance(target, users.User): target_type = channels.PermissionOverwriteType.MEMBER @@ -680,57 +533,12 @@ async def delete_permission_overwrite( channel: typing.Union[channels.GuildChannel, snowflake.UniqueObject], target: typing.Union[channels.PermissionOverwrite, guilds.Role, users.User, snowflake.UniqueObject], ) -> None: - """Delete a custom permission for an entity in a given guild channel. - - Parameters - ---------- - channel : hikari.models.channels.PartialChannel or hikari.utilities.snowflake.UniqueObject - The channel to delete a permission overwrite in. This may be a channel - object, or the ID of an existing channel. - target : hikari.models.users.User or hikari.models.guilds.Role or hikari.models.channels.PermissionOverwrite or hikari.utilities.snowflake.UniqueObject - The channel overwrite to delete. - - Raises - ------ - hikari.errors.Unauthorized - If you are unauthorized to make the request (invalid/missing token). - hikari.errors.Forbidden - If you lack permissions to delete the permission overwrite. - hikari.errors.NotFound - If the channel is not found or the target is not found. - hikari.errors.ServerHTTPErrorResponse - If an internal error occurs on Discord while handling the request. - """ # noqa: E501 - Line too long route = routes.DELETE_CHANNEL_PERMISSIONS.compile(channel=channel, overwrite=target) await self._request(route) async def fetch_channel_invites( self, channel: typing.Union[channels.GuildChannel, snowflake.UniqueObject] ) -> typing.Sequence[invites.InviteWithMetadata]: - """Fetch all invites pointing to the given guild channel. - - Parameters - ---------- - channel : hikari.models.channels.PartialChannel or hikari.utilities.snowflake.UniqueObject - The channel to fetch the invites from. This may be a channel - object, or the ID of an existing channel. - - Returns - ------- - typing.Sequence[hikari.models.invites.InviteWithMetadata] - The invites pointing to the given guild channel. - - Raises - ------ - hikari.errors.Unauthorized - If you are unauthorized to make the request (invalid/missing token). - hikari.errors.Forbidden - If you lack permissions to view the invites for the given channel. - hikari.errors.NotFound - If the channel is not found in any guilds you are a member of. - hikari.errors.ServerHTTPErrorResponse - If an internal error occurs on Discord while handling the request. - """ route = routes.GET_CHANNEL_INVITES.compile(channel=channel) raw_response = await self._request(route) response = typing.cast(data_binding.JSONArray, raw_response) @@ -748,48 +556,6 @@ async def create_invite( target_user_type: typing.Union[undefined.UndefinedType, invites.TargetUserType] = undefined.UNDEFINED, reason: typing.Union[undefined.UndefinedType, str] = undefined.UNDEFINED, ) -> invites.InviteWithMetadata: - """Create an invite to the given guild channel. - - Parameters - ---------- - channel : hikari.models.channels.PartialChannel or hikari.utilities.snowflake.UniqueObject - The channel to create a invite for. This may be a channel object, - or the ID of an existing channel. - max_age : hikari.utilities.undefined.UndefinedType or datetime.timedelta or float or int - If provided, the duration of the invite before expiry. - max_uses : hikari.utilities.undefined.UndefinedType or int - If provided, the max uses the invite can have. - temporary : hikari.utilities.undefined.UndefinedType or bool - If provided, whether the invite only grants temporary membership. - unique : hikari.utilities.undefined.UndefinedType or bool - If provided, wheter the invite should be unique. - target_user : hikari.utilities.undefined.UndefinedType or hikari.models.users.User or hikari.utilities.snowflake.UniqueObject - If provided, the target user id for this invite. This may be a - user object, or the ID of an existing user. - target_user_type : hikari.utilities.undefined.UndefinedType or hikari.models.invites.TargetUserType or int - If provided, the type of target user for this invite. - reason : hikari.utilities.undefined.UndefinedType or str - If provided, the reason that will be recorded in the audit logs. - - Returns - ------- - hikari.models.invites.InviteWithMetadata - The invite to the given guild channel. - - Raises - ------ - hikari.errors.BadRequest - If any of the fields that are passed have an invalid value. - hikari.errors.Unauthorized - If you are unauthorized to make the request (invalid/missing token). - hikari.errors.Forbidden - If you lack permissions to create the given channel. - hikari.errors.NotFound - If the channel is not found, or if the target user does not exist, - if specified. - hikari.errors.ServerHTTPErrorResponse - If an internal error occurs on Discord while handling the request. - """ # noqa: E501 - Line too long route = routes.POST_CHANNEL_INVITES.compile(channel=channel) body = data_binding.JSONObjectBuilder() body.put("max_age", max_age, conversion=date.timespan_to_int) @@ -805,66 +571,11 @@ async def create_invite( def trigger_typing( self, channel: typing.Union[channels.TextChannel, snowflake.UniqueObject] ) -> special_endpoints.TypingIndicator: - """Trigger typing in a text channel. - - Parameters - ---------- - channel : hikari.models.channels.PartialChannel or hikari.utilities.snowflake.UniqueObject - The channel to trigger typing in. This may be a channel object, or - the ID of an existing channel. - - Returns - ------- - hikari.impl.special_endpoints.TypingIndicator - A typing indicator to use. - - Raises - ------ - hikari.errors.Unauthorized - If you are unauthorized to make the request (invalid/missing token). - hikari.errors.Forbidden - If you lack permissions to read messages or send messages in the - text channel. - hikari.errors.NotFound - If the channel is not found. - hikari.errors.ServerHTTPErrorResponse - If an internal error occurs on Discord while handling the request. - - !!! note - The exceptions on this endpoint will only be raised once the result - is awaited or interacted with. Invoking this function itself will - not raise any of the above types. - """ return special_endpoints.TypingIndicator(channel, self._request) async def fetch_pins( self, channel: typing.Union[channels.TextChannel, snowflake.UniqueObject] ) -> typing.Sequence[messages_.Message]: - """Fetch the pinned messages in this text channel. - - Parameters - ---------- - channel : hikari.models.channels.PartialChannel or hikari.utilities.snowflake.UniqueObject - The channel to fetch pins from. This may be a channel object, or - the ID of an existing channel. - - Returns - ------- - typing.Sequence[hikari.models.messages.Message] - The pinned messages in this text channel. - - Raises - ------ - hikari.errors.Unauthorized - If you are unauthorized to make the request (invalid/missing token). - hikari.errors.Forbidden - If you lack permissions to read messages or send messages in the - text channel. - hikari.errors.NotFound - If the channel is not found. - hikari.errors.ServerHTTPErrorResponse - If an internal error occurs on Discord while handling the request. - """ route = routes.GET_CHANNEL_PINS.compile(channel=channel) raw_response = await self._request(route) response = typing.cast(data_binding.JSONArray, raw_response) @@ -875,29 +586,6 @@ async def pin_message( channel: typing.Union[channels.TextChannel, snowflake.UniqueObject], message: typing.Union[messages_.Message, snowflake.UniqueObject], ) -> None: - """Pin an existing message in the given text channel. - - Parameters - ---------- - channel : hikari.models.channels.PartialChannel or hikari.utilities.snowflake.UniqueObject - The channel to pin a message in. This may be a channel object, or - the ID of an existing channel. - message : hikari.models.messages.Message or hikari.utilities.snowflake.UniqueObject - The message to pin. This may be a message object, - or the ID of an existing message. - - Raises - ------ - hikari.errors.Unauthorized - If you are unauthorized to make the request (invalid/missing token). - hikari.errors.Forbidden - If you lack permissions to pin messages in the given channel. - hikari.errors.NotFound - If the channel is not found, or if the message does not exist in - the given channel. - hikari.errors.ServerHTTPErrorResponse - If an internal error occurs on Discord while handling the request. - """ route = routes.PUT_CHANNEL_PINS.compile(channel=channel, message=message) await self._request(route) @@ -906,29 +594,6 @@ async def unpin_message( channel: typing.Union[channels.TextChannel, snowflake.UniqueObject], message: typing.Union[messages_.Message, snowflake.UniqueObject], ) -> None: - """Unpin a given message from a given text channel. - - Parameters - ---------- - channel : hikari.models.channels.PartialChannel or hikari.utilities.snowflake.UniqueObject - The channel to unpin a message in. This may be a channel object, or - the ID of an existing channel. - message : hikari.models.messages.Message or hikari.utilities.snowflake.UniqueObject - The message to unpin. This may be a message object, or the ID of an - existing message. - - Raises - ------ - hikari.errors.Unauthorized - If you are unauthorized to make the request (invalid/missing token). - hikari.errors.Forbidden - If you lack permissions to pin messages in the given channel. - hikari.errors.NotFound - If the channel is not found or the message is not a pinned message - in the given channel. - hikari.errors.ServerHTTPErrorResponse - If an internal error occurs on Discord while handling the request. - """ route = routes.DELETE_CHANNEL_PIN.compile(channel=channel, message=message) await self._request(route) @@ -940,48 +605,6 @@ def fetch_messages( after: typing.Union[undefined.UndefinedType, datetime.datetime, snowflake.UniqueObject] = undefined.UNDEFINED, around: typing.Union[undefined.UndefinedType, datetime.datetime, snowflake.UniqueObject] = undefined.UNDEFINED, ) -> iterators.LazyIterator[messages_.Message]: - """Browse the message history for a given text channel. - - Parameters - ---------- - channel : hikari.models.channels.PartialChannel or hikari.utilities.snowflake.UniqueObject - The channel to fetch messages in. This may be a channel object, or - the ID of an existing channel. - before : hikari.utilities.undefined.UndefinedType or datetime.datetime or hikari.utilities.snowflake.UniqueObject - If provided, fetch messages before this snowflake. If you provide - a datetime object, it will be transformed into a snowflake. - after : hikari.utilities.undefined.UndefinedType or datetime.datetime or hikari.utilities.snowflake.UniqueObject - If provided, fetch messages after this snowflake. If you provide - a datetime object, it will be transformed into a snowflake. - around : hikari.utilities.undefined.UndefinedType or datetime.datetime or hikari.utilities.snowflake.UniqueObject - If provided, fetch messages around this snowflake. If you provide - a datetime object, it will be transformed into a snowflake. - - Returns - ------- - hikari.utilities.iterators.LazyIterator[hikari.models.messages.Message] - A iterator to fetch the messages. - - Raises - ------ - TypeError - If you specify more than one of `before`, `after`, `about`. - hikari.errors.Unauthorized - If you are unauthorized to make the request (invalid/missing token). - hikari.errors.Forbidden - If you lack permissions to read message history in the given - channel. - hikari.errors.NotFound - If the channel is not found. - hikari.errors.ServerHTTPErrorResponse - If an internal error occurs on Discord while handling the request. - - !!! note - The exceptions on this endpoint (other than `TypeError`) will only - be raised once the result is awaited or interacted with. Invoking - this function itself will not raise anything (other than - `TypeError`). - """ # noqa: E501 - Line too long if undefined.count(before, after, around) < 2: raise TypeError("Expected no kwargs, or maximum of one of 'before', 'after', 'around'") @@ -1007,35 +630,6 @@ async def fetch_message( channel: typing.Union[channels.TextChannel, snowflake.UniqueObject], message: typing.Union[messages_.Message, snowflake.UniqueObject], ) -> messages_.Message: - """Fetch a specific message in the given text channel. - - Parameters - ---------- - channel : hikari.models.channels.PartialChannel or hikari.utilities.snowflake.UniqueObject - The channel to fetch messages in. This may be a channel object, or - the ID of an existing channel. - message : hikari.models.messages.Message or hikari.utilities.snowflake.UniqueObject - The message to fetch. This may be a channel object, or the ID of an - existing channel. - - Returns - ------- - hikari.models.messages.Message - The requested message. - - Raises - ------ - hikari.errors.Unauthorized - If you are unauthorized to make the request (invalid/missing token). - hikari.errors.Forbidden - If you lack permissions to read message history in the given - channel. - hikari.errors.NotFound - If the channel is not found or the message is not found in the - given text channel. - hikari.errors.ServerHTTPErrorResponse - If an internal error occurs on Discord while handling the request. - """ route = routes.GET_CHANNEL_MESSAGE.compile(channel=channel, message=message) raw_response = await self._request(route) response = typing.cast(data_binding.JSONObject, raw_response) @@ -1057,71 +651,6 @@ async def create_message( user_mentions: typing.Union[typing.Collection[typing.Union[users.User, snowflake.UniqueObject]], bool] = True, role_mentions: typing.Union[typing.Collection[typing.Union[guilds.Role, snowflake.UniqueObject]], bool] = True, ) -> messages_.Message: - """Create a message in the given channel. - - Parameters - ---------- - channel : hikari.models.channels.PartialChannel or hikari.utilities.snowflake.UniqueObject - The channel to create the message in. This may be a channel object, or - the ID of an existing channel. - text : hikari.utilities.undefined.UndefinedType or str - If specified, the message contents. - embed : hikari.utilities.undefined.UndefinedType or hikari.models.embeds.Embed - If specified, the message embed. - attachment : hikari.utilities.undefined.UndefinedType or str or hikari.utilities.files.Resource - If specified, the message attachment. This can be a resource, - or string of a path on your computer or a URL. - attachments : hikari.utilities.undefined.UndefinedType or typing.Sequence[str or hikari.utilities.files.Resource] - If specified, the message attachments. These can be resources, or - strings consisting of paths on your computer or URLs. - tts : hikari.utilities.undefined.UndefinedType or bool - If specified, whether the message will be TTS (Text To Speech). - nonce : hikari.utilities.undefined.UndefinedType or str - If specified, a nonce that can be used for optimistic message sending. - mentions_everyone : bool - If specified, whether the message should parse @everyone/@here mentions. - user_mentions : typing.Collection[hikari.models.users.User or hikari.utilities.snowflake.UniqueObject] or bool - If specified, and `bool`, whether to parse user mentions. If specified and - `list`, the users to parse the mention for. This may be a user object, or - the ID of an existing user. - role_mentions : typing.Collection[hikari.models.guilds.Role or hikari.utilities.snowflake.UniqueObject] or bool - If specified and `bool`, whether to parse role mentions. If specified and - `list`, the roles to parse the mention for. This may be a role object, or - the ID of an existing role. - - Returns - ------- - hikari.models.messages.Message - The created message. - - Raises - ------ - hikari.errors.BadRequest - This may be raised in several discrete situations, such as messages - being empty with no attachments or embeds; messages with more than - 2000 characters in them, embeds that exceed one of the many embed - limits; too many attachments; attachments that are too large; - invalid image URLs in embeds; users in `user_mentions` not being - mentioned in the message content; roles in `role_mentions` not - being mentioned in the message content. - hikari.errors.Unauthorized - If you are unauthorized to make the request (invalid/missing token). - hikari.errors.Forbidden - If you lack permissions to send messages in the given channel. - hikari.errors.NotFound - If the channel is not found. - hikari.errors.ServerHTTPErrorResponse - If an internal error occurs on Discord while handling the request. - ValueError - If more than 100 unique objects/entities are passed for - `role_mentions` or `user_mentions`. - TypeError - If both `attachment` and `attachments` are specified. - - !!! warning - You are expected to make a connection to the gateway and identify - once before being able to use this endpoint for a bot. - """ # noqa: E501 - Line too long if attachment is not undefined.UNDEFINED and attachments is not undefined.UNDEFINED: raise ValueError("You may only specify one of 'attachment' or 'attachments', not both") @@ -1146,7 +675,7 @@ async def create_message( if final_attachments: form = data_binding.URLEncodedForm() - form.add_field("payload_json", data_binding.dump_json(body), content_type=strings.APPLICATION_JSON) + form.add_field("payload_json", data_binding.dump_json(body), content_type=constants.APPLICATION_JSON) stack = contextlib.AsyncExitStack() @@ -1154,7 +683,7 @@ async def create_message( for i, attachment in enumerate(final_attachments): stream = await stack.enter_async_context(attachment.stream(executor=self._app.executor)) form.add_field( - f"file{i}", stream, filename=stream.filename, content_type=strings.APPLICATION_OCTET_STREAM + f"file{i}", stream, filename=stream.filename, content_type=constants.APPLICATION_OCTET_STREAM ) raw_response = await self._request(route, form=form) @@ -1182,48 +711,6 @@ async def edit_message( ] = undefined.UNDEFINED, flags: typing.Union[undefined.UndefinedType, messages_.MessageFlag] = undefined.UNDEFINED, ) -> messages_.Message: - """Edit an existing message in a given channel. - - Parameters - ---------- - channel : hikari.models.channels.PartialChannel or hikari.utilities.snowflake.UniqueObject - The channel to edit the message in. This may be a channel object, or - the ID of an existing channel. - message : hikari.models.messages.Message or hikari.utilities.snowflake.UniqueObject - The message to fetch. - text - embed - mentions_everyone - user_mentions - role_mentions - flags - - Returns - ------- - hikari.models.messages.Message - The edited message. - - Raises - ------ - hikari.errors.BadRequest - This may be raised in several discrete situations, such as messages - being empty with no embeds; messages with more than 2000 characters - in them, embeds that exceed one of the many embed - limits; invalid image URLs in embeds; users in `user_mentions` not - being mentioned in the message content; roles in `role_mentions` not - being mentioned in the message content. - hikari.errors.Unauthorized - If you are unauthorized to make the request (invalid/missing token). - hikari.errors.Forbidden - If you lack permissions to send messages in the given channel; if - you try to change the contents of another user's message; or if you - try to edit the flags on another user's message without the - permissions to manage messages_. - hikari.errors.NotFound - If the channel or message is not found. - hikari.errors.ServerHTTPErrorResponse - If an internal error occurs on Discord while handling the request. - """ route = routes.PATCH_CHANNEL_MESSAGE.compile(channel=channel, message=message) body = data_binding.JSONObjectBuilder() body.put("flags", flags) @@ -1249,25 +736,6 @@ async def delete_message( channel: typing.Union[channels.TextChannel, snowflake.UniqueObject], message: typing.Union[messages_.Message, snowflake.UniqueObject], ) -> None: - """Delete a given message in a given channel. - - Parameters - ---------- - channel - message - - Raises - ------ - hikari.errors.Unauthorized - If you are unauthorized to make the request (invalid/missing token). - hikari.errors.Forbidden - If you lack the permissions to manage messages, and the message is - not composed by your associated user. - hikari.errors.NotFound - If the channel or message is not found. - hikari.errors.ServerHTTPErrorResponse - If an internal error occurs on Discord while handling the request. - """ route = routes.DELETE_CHANNEL_MESSAGE.compile(channel=channel, message=message) await self._request(route) @@ -1277,27 +745,6 @@ async def delete_messages( /, *messages: typing.Union[messages_.Message, snowflake.UniqueObject], ) -> None: - """Bulk-delete between 2 and 100 messages from the given guild channel. - - Parameters - ---------- - channel - *messages - - Raises - ------ - hikari.errors.Unauthorized - If you are unauthorized to make the request (invalid/missing token). - hikari.errors.Forbidden - If you lack the permissions to manage messages, and the message is - not composed by your associated user. - hikari.errors.NotFound - If the channel or message is not found. - hikari.errors.ServerHTTPErrorResponse - If an internal error occurs on Discord while handling the request. - TypeError - If you do not provide between 2 and 100 messages (inclusive). - """ if 2 <= len(messages) <= 100: route = routes.POST_DELETE_CHANNEL_MESSAGES_BULK.compile(channel=channel) body = data_binding.JSONObjectBuilder() @@ -1312,28 +759,6 @@ async def add_reaction( message: typing.Union[messages_.Message, snowflake.UniqueObject], emoji: typing.Union[str, emojis.Emoji], ) -> None: - """Add a reaction emoji to a message in a given channel. - - Parameters - ---------- - channel - message - emoji - - Raises - ------ - hikari.errors.BadRequest - If an invalid unicode emoji is given, or if the given custom emoji - does not exist. - hikari.errors.Unauthorized - If you are unauthorized to make the request (invalid/missing token). - hikari.errors.Forbidden - If you lack permissions to add reactions to messages. - hikari.errors.NotFound - If the channel or message is not found. - hikari.errors.ServerHTTPErrorResponse - If an internal error occurs on Discord while handling the request. - """ route = routes.PUT_MY_REACTION.compile( emoji=emoji.url_name if isinstance(emoji, emojis.CustomEmoji) else str(emoji), channel=channel, @@ -1347,26 +772,6 @@ async def delete_my_reaction( message: typing.Union[messages_.Message, snowflake.UniqueObject], emoji: typing.Union[str, emojis.Emoji], ) -> None: - """Delete a reaction that your application user created. - - Parameters - ---------- - channel - message - emoji - - Raises - ------ - hikari.errors.BadRequest - If an invalid unicode emoji is given, or if the given custom emoji - does not exist. - hikari.errors.Unauthorized - If you are unauthorized to make the request (invalid/missing token). - hikari.errors.NotFound - If the channel or message is not found. - hikari.errors.ServerHTTPErrorResponse - If an internal error occurs on Discord while handling the request. - """ route = routes.DELETE_MY_REACTION.compile( emoji=emoji.url_name if isinstance(emoji, emojis.CustomEmoji) else str(emoji), channel=channel, @@ -1575,7 +980,7 @@ async def execute_webhook( if final_attachments: form = data_binding.URLEncodedForm() - form.add_field("payload_json", data_binding.dump_json(body), content_type=strings.APPLICATION_JSON) + form.add_field("payload_json", data_binding.dump_json(body), content_type=constants.APPLICATION_JSON) stack = contextlib.AsyncExitStack() @@ -1583,7 +988,7 @@ async def execute_webhook( for i, attachment in enumerate(final_attachments): stream = await stack.enter_async_context(attachment.stream(executor=self._app.executor)) form.add_field( - f"file{i}", stream, filename=stream.filename, content_type=strings.APPLICATION_OCTET_STREAM + f"file{i}", stream, filename=stream.filename, content_type=constants.APPLICATION_OCTET_STREAM ) raw_response = await self._request(route, query=query, form=form, no_auth=True) @@ -1639,8 +1044,8 @@ async def edit_my_user( if avatar is None: body.put("avatar", None) elif avatar is not undefined.UNDEFINED: - avatar_resouce = files.ensure_resource(avatar) - async with avatar_resouce.stream(executor=self._app.executor) as stream: + avatar_resource = files.ensure_resource(avatar) + async with avatar_resource.stream(executor=self._app.executor) as stream: body.put("avatar", await stream.data_uri()) raw_response = await self._request(route, json=body) @@ -2173,7 +1578,7 @@ async def remove_role_from_member( route = routes.DELETE_GUILD_MEMBER_ROLE.compile(guild=guild, user=user, role=role) await self._request(route, reason=reason) - async def kick_member( + async def kick_user( self, guild: typing.Union[guilds.Guild, snowflake.UniqueObject], user: typing.Union[users.User, snowflake.UniqueObject], @@ -2183,6 +1588,8 @@ async def kick_member( route = routes.DELETE_GUILD_MEMBER.compile(guild=guild, user=user) await self._request(route, reason=reason) + kick_member = kick_user + async def ban_user( self, guild: typing.Union[guilds.Guild, snowflake.UniqueObject], @@ -2196,6 +1603,8 @@ async def ban_user( route = routes.PUT_GUILD_BAN.compile(guild=guild, user=user) await self._request(route, reason=reason, json=body) + ban_member = ban_user + async def unban_user( self, guild: typing.Union[guilds.Guild, snowflake.UniqueObject], @@ -2206,6 +1615,8 @@ async def unban_user( route = routes.DELETE_GUILD_BAN.compile(guild=guild, user=user) await self._request(route, reason=reason) + unban_member = unban_user + async def fetch_ban( self, guild: typing.Union[guilds.Guild, snowflake.UniqueObject], @@ -2316,38 +1727,6 @@ async def estimate_guild_prune_count( undefined.UndefinedType, typing.Collection[typing.Union[guilds.Role, snowflake.UniqueObject]] ] = undefined.UNDEFINED, ) -> int: - """Estimate the guild prune count. - - Parameters - ---------- - guild : hikari.models.guilds.Guild or hikari.utilities.snowflake.UniqueObject - The guild to estimate the guild prune count for. This may be a guild object, - or the ID of an existing channel. - days : hikari.utilities.undefined.UndefinedType or int - If provided, number of days to count prune for. - include_roles : hikari.utilities.undefined.UndefinedType or typing.Collection[hikari.models.guilds.Role or hikari.utilities.snowflake.UniqueObject] - If provided, the role(s) to include. By default, this endpoint will not count - users with roles. Providing roles using this attribute will make members with - the specified roles also get included into the count. - - Returns - ------- - int - The estimated guild prune count. - - Raises - ------ - hikari.errors.BadRequest - If any of the fields that are passed have an invalid value. - hikari.errors.Unauthorized - If you are unauthorized to make the request (invalid/missing token). - hikari.errors.Forbidden - If you lack the `KICK_MEMBERS` permission. - hikari.errors.NotFound - If the guild is not found. - hikari.errors.ServerHTTPErrorResponse - If an internal error occurs on Discord while handling the request. - """ # noqa: E501 - Line too long route = routes.GET_GUILD_PRUNE.compile(guild=guild) query = data_binding.StringMapBuilder() query.put("days", days) @@ -2369,43 +1748,6 @@ async def begin_guild_prune( ] = undefined.UNDEFINED, reason: typing.Union[undefined.UndefinedType, str] = undefined.UNDEFINED, ) -> typing.Optional[int]: - """Begin the guild prune. - - Parameters - ---------- - guild : hikari.models.guilds.Guild or hikari.utilities.snowflake.UniqueObject - The guild to begin the guild prune in. This may be a guild object, - or the ID of an existing channel. - days : hikari.utilities.undefined.UndefinedType or int - If provided, number of days to count prune for. - compute_prune_count: hikari.utilities.undefined.UndefinedType or bool - If provided, whether to return the prune count. This is discouraged for large - guilds. - include_roles : hikari.utilities.undefined.UndefinedType or typing.Collection[hikari.models.guilds.Role or hikari.utilities.snowflake.UniqueObject] - If provided, the role(s) to include. By default, this endpoint will not count - users with roles. Providing roles using this attribute will make members with - the specified roles also get included into the count. - reason : hikari.utilities.undefined.UndefinedType or str - If provided, the reason that will be recorded in the audit logs. - - Returns - ------- - int or None - If `compute_prune_count` is not provided or `True`, the number of members pruned. - - Raises - ------ - hikari.errors.BadRequest - If any of the fields that are passed have an invalid value. - hikari.errors.Unauthorized - If you are unauthorized to make the request (invalid/missing token). - hikari.errors.Forbidden - If you lack the `KICK_MEMBERS` permission. - hikari.errors.NotFound - If the guild is not found. - hikari.errors.ServerHTTPErrorResponse - If an internal error occurs on Discord while handling the request. - """ # noqa: E501 - Line too long route = routes.POST_GUILD_PRUNE.compile(guild=guild) body = data_binding.JSONObjectBuilder() body.put("days", days) diff --git a/hikari/impl/special_endpoints.py b/hikari/impl/special_endpoints.py index 60387a48c9..4f5cfd4ba9 100644 --- a/hikari/impl/special_endpoints.py +++ b/hikari/impl/special_endpoints.py @@ -30,6 +30,7 @@ import attr +from hikari.api import special_endpoints from hikari.impl import routes from hikari.utilities import data_binding from hikari.utilities import date @@ -39,7 +40,7 @@ if typing.TYPE_CHECKING: import types - from hikari.api import rest + from hikari.api import rest_app from hikari.models import applications from hikari.models import audit_logs from hikari.models import channels @@ -48,12 +49,11 @@ from hikari.models import messages from hikari.models import permissions as permissions_ from hikari.models import users - from hikari.utilities import files from hikari.utilities import snowflake @typing.final -class TypingIndicator: +class TypingIndicator(special_endpoints.TypingIndicator): """Result type of `hiarki.net.rest.trigger_typing`. This is an object that can either be awaited like a coroutine to trigger @@ -78,9 +78,6 @@ def __await__(self) -> typing.Generator[None, typing.Any, None]: route = routes.POST_CHANNEL_TYPING.compile(channel=self._channel) yield from self._request_call(route).__await__() - def __enter__(self) -> typing.NoReturn: - raise TypeError("Use 'async with' rather than 'with' when triggering the typing indicator.") - async def __aenter__(self) -> None: if self._task is not None: raise TypeError("cannot enter a typing indicator context more than once.") @@ -103,76 +100,16 @@ async def _keep_typing(self) -> None: @attr.s(auto_attribs=True, kw_only=True, slots=True) -class GuildBuilder: +class GuildBuilder(special_endpoints.GuildBuilder): """A helper class used to construct a prototype for a guild. This is used to create a guild in a tidy way using the HTTP API, since the logic behind creating a guild on an API level is somewhat confusing and detailed. - - !!! note - This is a helper class that is used by `hikari.impl.http.HTTP`. - You should only ever need to use instances of this class that are - produced by that API, thus, any details about the constructor are - omitted from the following examples for brevity. - - Examples - -------- - Creating an empty guild. - - ```py - guild = await rest.guild_builder("My Server!").create() - ``` - - Creating a guild with an icon - - ```py - from hikari.models.files import WebResourceStream - - guild_builder = rest.guild_builder("My Server!") - guild_builder.icon = WebResourceStream("cat.png", "http://...") - guild = await guild_builder.create() - ``` - - Adding roles to your guild. - - ```py - from hikari.models.permissions import Permission - - guild_builder = rest.guild_builder("My Server!") - - everyone_role_id = guild_builder.add_role("@everyone") - admin_role_id = guild_builder.add_role("Admins", permissions=Permission.ADMINISTRATOR) - - await guild_builder.create() - ``` - - !!! warning - The first role must always be the `@everyone` role. - - !!! note - Functions that return a `hikari.utilities.snowflake.Snowflake` do - **not** provide the final ID that the object will have once the - API call is made. The returned IDs are only able to be used to - re-reference particular objects while building the guild format. - - This is provided to allow creation of channels within categories, - and to provide permission overwrites. - - Adding a text channel to your guild. - - ```py - guild_builder = rest.guild_builder("My Server!") - - category_id = guild_builder.add_category("My safe place") - channel_id = guild_builder.add_text_channel("general", parent_id=category_id) - - await guild_builder.create() - ``` """ # Required arguments. - _app: rest.IRESTClient + _app: rest_app.IRESTApp _name: str # Optional args that we kept hidden. @@ -183,69 +120,11 @@ class GuildBuilder: ] _roles: typing.MutableSequence[data_binding.JSONObject] = attr.ib(factory=list) - default_message_notifications: typing.Union[ - undefined.UndefinedType, guilds.GuildMessageNotificationsLevel - ] = undefined.UNDEFINED - """Default message notification level that can be overwritten. - - If not overridden, this will use the Discord default level. - """ - - explicit_content_filter_level: typing.Union[ - undefined.UndefinedType, guilds.GuildExplicitContentFilterLevel - ] = undefined.UNDEFINED - """Explicit content filter level that can be overwritten. - - If not overridden, this will use the Discord default level. - """ - - icon: typing.Union[undefined.UndefinedType, files.URL] = undefined.UNDEFINED - """Guild icon to use that can be overwritten. - - If not overridden, the guild will not have an icon. - """ - - region: typing.Union[undefined.UndefinedType, str] = undefined.UNDEFINED - """Guild voice channel region to use that can be overwritten. - - If not overridden, the guild will use the default voice region for Discord. - """ - - verification_level: typing.Union[undefined.UndefinedType, guilds.GuildVerificationLevel] = undefined.UNDEFINED - """Verification level required to join the guild that can be overwritten. - - If not overridden, the guild will use the default verification level for - Discord. - """ - @property def name(self) -> str: - """Guild name.""" return self._name async def create(self) -> guilds.Guild: - """Send the request to Discord to create the guild. - - The application user will be added to this guild as soon as it is - created. All IDs that were provided when building this guild will - become invalid and will be replaced with real IDs. - - Returns - ------- - hikari.models.guilds.Guild - The created guild. - - Raises - ------ - hikari.errors.BadRequest - If any values set in the guild builder are invalid. - hikari.errors.Unauthorized - If you are unauthorized to make the request (invalid/missing token). - hikari.errors.Forbidden - If you are already in 10 guilds. - hikari.errors.ServerHTTPErrorResponse - If an internal error occurs on Discord while handling the request. - """ route = routes.POST_GUILDS.compile() payload = data_binding.JSONObjectBuilder() payload.put("name", self.name) @@ -277,55 +156,6 @@ def add_role( permissions: typing.Union[undefined.UndefinedType, permissions_.Permission] = undefined.UNDEFINED, position: typing.Union[undefined.UndefinedType, int] = undefined.UNDEFINED, ) -> snowflake.Snowflake: - """Create a role. - - !!! note - The first role you create must always be the `@everyone` role, and - must have that name. This role will ignore the `hoisted`, `color`, - `colour`, `mentionable` and `position` parameters. - - Parameters - ---------- - name : str - The role name. - color : hikari.utilities.undefined.UndefinedType or hikari.models.colors.Color - The colour of the role to use. If unspecified, then the default - colour is used instead. - colour : hikari.utilities.undefined.UndefinedType or hikari.models.colors.Color - Alias for the `color` parameter for non-american users. - hoisted : hikari.utilities.undefined.UndefinedType or bool - If `True`, the role will show up in the user sidebar in a separate - category if it is the highest hoisted role. If `False`, or - unspecified, then this will not occur. - mentionable : hikari.utilities.undefined.UndefinedType or bool - If `True`, then the role will be able to be mentioned. - permissions : hikari.utilities.undefined.UndefinedType or hikari.models.permissions.Permission - The optional permissions to enforce on the role. If unspecified, - the default permissions for roles will be used. - - !!! note - The default permissions are **NOT** the same as providing - zero permissions. To set no permissions, you should - pass `Permission(0)` explicitly. - position : hikari.utilities.undefined.UndefinedType or int - If specified, the position to place the role in. - - Returns - ------- - hikari.utilities.snowflake.Snowflake - The dummy ID for this role that can be used temporarily to refer - to this object while designing the guild layout. - - When the guild is created, this will be replaced with a different - ID. - - Raises - ------ - ValueError - If you are defining the first role, but did not name it `@everyone`. - TypeError - If you specify both `color` and `colour` together. - """ if len(self._roles) == 0 and name != "@everyone": raise ValueError("First role must always be the @everyone role") @@ -356,30 +186,6 @@ def add_category( ] = undefined.UNDEFINED, nsfw: typing.Union[undefined.UndefinedType, bool] = undefined.UNDEFINED, ) -> snowflake.Snowflake: - """Create a category channel. - - Parameters - ---------- - name : str - The name of the category. - position : hikari.utilities.undefined.UndefinedType or int - The position to place the category in, if specified. - permission_overwrites : hikari.utilities.undefined.UndefinedType or typing.Collection[hikari.models.channels.PermissionOverwrite] - If defined, a collection of one or more - `hikari.models.channels.PermissionOverwrite` objects. - nsfw : hikari.utilities.undefined.UndefinedType or bool - If `True`, the channel is marked as NSFW and only users over - 18 years of age should be given access. - - Returns - ------- - hikari.utilities.snowflake.Snowflake - The dummy ID for this channel that can be used temporarily to refer - to this object while designing the guild layout. - - When the guild is created, this will be replaced with a different - ID. - """ # noqa: E501 - Line too long snowflake_id = self._new_snowflake() payload = data_binding.JSONObjectBuilder() payload.put_snowflake("id", snowflake_id) @@ -411,39 +217,6 @@ def add_text_channel( ] = undefined.UNDEFINED, nsfw: typing.Union[undefined.UndefinedType, bool] = undefined.UNDEFINED, ) -> snowflake.Snowflake: - """Create a text channel. - - Parameters - ---------- - name : str - The name of the category. - position : hikari.utilities.undefined.UndefinedType or int - The position to place the category in, if specified. - permission_overwrites : hikari.utilities.undefined.UndefinedType or typing.Collection[hikari.models.channels.PermissionOverwrite] - If defined, a collection of one or more - `hikari.models.channels.PermissionOverwrite` objects. - nsfw : hikari.utilities.undefined.UndefinedType or bool - If `True`, the channel is marked as NSFW and only users over - 18 years of age should be given access. - parent_id : hikari.utilities.undefined.UndefinedType or hikari.utilities.snowflake.Snowflake - If defined, should be a snowflake ID of a category channel - that was made with this builder. If provided, this channel will - become a child channel of that category. - topic : hikari.utilities.undefined.UndefinedType or str - If specified, the topic to set on the channel. - rate_limit_per_user : hikari.utilities.undefined.UndefinedType or hikari.utilities.date.TimeSpan - If specified, the time to wait between allowing consecutive messages - to be sent. If not specified, this will not be enabled. - - Returns - ------- - hikari.utilities.snowflake.Snowflake - The dummy ID for this channel that can be used temporarily to refer - to this object while designing the guild layout. - - When the guild is created, this will be replaced with a different - ID. - """ # noqa: E501 - Line too long snowflake_id = self._new_snowflake() payload = data_binding.JSONObjectBuilder() payload.put_snowflake("id", snowflake_id) @@ -478,39 +251,6 @@ def add_voice_channel( nsfw: typing.Union[undefined.UndefinedType, bool] = undefined.UNDEFINED, user_limit: typing.Union[undefined.UndefinedType, int] = undefined.UNDEFINED, ) -> snowflake.Snowflake: - """Create a voice channel. - - Parameters - ---------- - name : str - The name of the category. - position : hikari.utilities.undefined.UndefinedType or int - The position to place the category in, if specified. - permission_overwrites : hikari.utilities.undefined.UndefinedType or typing.Collection[hikari.models.channels.PermissionOverwrite] - If defined, a collection of one or more - `hikari.models.channels.PermissionOverwrite` objects. - nsfw : hikari.utilities.undefined.UndefinedType or bool - If `True`, the channel is marked as NSFW and only users over - 18 years of age should be given access. - parent_id : hikari.utilities.undefined.UndefinedType or hikari.utilities.snowflake.Snowflake - If defined, should be a snowflake ID of a category channel - that was made with this builder. If provided, this channel will - become a child channel of that category. - bitrate : hikari.utilities.undefined.UndefinedType or int - If specified, the bitrate to set on the channel. - user_limit : hikari.utilities.undefined.UndefinedType or int - If specified, the maximum number of users to allow in the voice - channel. - - Returns - ------- - hikari.utilities.snowflake.Snowflake - The dummy ID for this channel that can be used temporarily to refer - to this object while designing the guild layout. - - When the guild is created, this will be replaced with a different - ID. - """ # noqa: E501 - Line too long snowflake_id = self._new_snowflake() payload = data_binding.JSONObjectBuilder() payload.put_snowflake("id", snowflake_id) @@ -547,7 +287,7 @@ class MessageIterator(iterators.BufferedLazyIterator["messages.Message"]): def __init__( self, - app: rest.IRESTClient, + app: rest_app.IRESTApp, request_call: typing.Callable[ ..., typing.Coroutine[None, None, typing.Union[None, data_binding.JSONObject, data_binding.JSONArray]] ], @@ -589,7 +329,7 @@ class ReactorIterator(iterators.BufferedLazyIterator["users.User"]): def __init__( self, - app: rest.IRESTClient, + app: rest_app.IRESTApp, request_call: typing.Callable[ ..., typing.Coroutine[None, None, typing.Union[None, data_binding.JSONObject, data_binding.JSONArray]] ], @@ -628,7 +368,7 @@ class OwnGuildIterator(iterators.BufferedLazyIterator["applications.OwnGuild"]): def __init__( self, - app: rest.IRESTClient, + app: rest_app.IRESTApp, request_call: typing.Callable[ ..., typing.Coroutine[None, None, typing.Union[None, data_binding.JSONObject, data_binding.JSONArray]] ], @@ -667,7 +407,7 @@ class MemberIterator(iterators.BufferedLazyIterator["guilds.Member"]): def __init__( self, - app: rest.IRESTClient, + app: rest_app.IRESTApp, request_call: typing.Callable[ ..., typing.Coroutine[None, None, typing.Union[None, data_binding.JSONObject, data_binding.JSONArray]] ], @@ -706,7 +446,7 @@ class AuditLogIterator(iterators.LazyIterator["audit_logs.AuditLog"]): def __init__( self, - app: rest.IRESTClient, + app: rest_app.IRESTApp, request_call: typing.Callable[ ..., typing.Coroutine[None, None, typing.Union[None, data_binding.JSONObject, data_binding.JSONArray]] ], diff --git a/hikari/models/applications.py b/hikari/models/applications.py index f894c633f1..9f63ea8266 100644 --- a/hikari/models/applications.py +++ b/hikari/models/applications.py @@ -41,7 +41,7 @@ from hikari.utilities import snowflake if typing.TYPE_CHECKING: - from hikari.api import rest + from hikari.api import rest_app from hikari.models import permissions as permissions_ from hikari.models import users @@ -261,7 +261,7 @@ def __str__(self) -> str: class TeamMember: """Represents a member of a Team.""" - app: rest.IRESTClient = attr.ib(default=None, repr=False, eq=False, hash=False) + app: rest_app.IRESTApp = attr.ib(default=None, repr=False, eq=False, hash=False) """The client application that models may use for procedures.""" membership_state: TeamMembershipState = attr.ib(eq=False, hash=False, repr=False) @@ -288,7 +288,7 @@ def __str__(self) -> str: class Team(snowflake.Unique): """Represents a development team, along with all its members.""" - app: rest.IRESTClient = attr.ib(default=None, repr=False, eq=False, hash=False) + app: rest_app.IRESTApp = attr.ib(default=None, repr=False, eq=False, hash=False) """The client application that models may use for procedures.""" id: snowflake.Snowflake = attr.ib( @@ -359,7 +359,7 @@ def format_icon(self, *, format_: str = "png", size: int = 4096) -> typing.Optio class Application(snowflake.Unique): """Represents the information of an Oauth2 Application.""" - app: rest.IRESTClient = attr.ib(default=None, repr=False, eq=False, hash=False) + app: rest_app.IRESTApp = attr.ib(default=None, repr=False, eq=False, hash=False) """The client application that models may use for procedures.""" id: snowflake.Snowflake = attr.ib( diff --git a/hikari/models/audit_logs.py b/hikari/models/audit_logs.py index 42d346f5a1..97a67d154a 100644 --- a/hikari/models/audit_logs.py +++ b/hikari/models/audit_logs.py @@ -46,7 +46,7 @@ from hikari.utilities import snowflake if typing.TYPE_CHECKING: - from hikari.api import rest + from hikari.api import rest_app from hikari.models import channels from hikari.models import guilds from hikari.models import users as users_ @@ -284,7 +284,7 @@ def __init__(self, payload: typing.Mapping[str, str]) -> None: class AuditLogEntry(snowflake.Unique): """Represents an entry in a guild's audit log.""" - app: rest.IRESTClient = attr.ib(default=None, repr=False, eq=False, hash=False) + app: rest_app.IRESTApp = attr.ib(default=None, repr=False, eq=False, hash=False) """The client application that models may use for procedures.""" id: snowflake.Snowflake = attr.ib( diff --git a/hikari/models/channels.py b/hikari/models/channels.py index f3531e9def..6db727ac7f 100644 --- a/hikari/models/channels.py +++ b/hikari/models/channels.py @@ -50,7 +50,7 @@ if typing.TYPE_CHECKING: import datetime - from hikari.api import rest + from hikari.api import rest_app from hikari.models import embeds from hikari.models import guilds from hikari.models import messages @@ -167,7 +167,7 @@ class PartialChannel(snowflake.Unique): ) """The ID of this entity.""" - app: rest.IRESTClient = attr.ib(default=None, repr=False, eq=False, hash=False) + app: rest_app.IRESTApp = attr.ib(default=None, repr=False, eq=False, hash=False) """The client application that models may use for procedures.""" name: typing.Optional[str] = attr.ib(eq=False, hash=False, repr=True) diff --git a/hikari/models/emojis.py b/hikari/models/emojis.py index 8f9818fe89..a0c77ed005 100644 --- a/hikari/models/emojis.py +++ b/hikari/models/emojis.py @@ -32,7 +32,7 @@ from hikari.utilities import snowflake if typing.TYPE_CHECKING: - from hikari.api import rest + from hikari.api import rest_app from hikari.models import users _TWEMOJI_PNG_BASE_URL: typing.Final[str] = "https://github.com/twitter/twemoji/raw/master/assets/72x72/" @@ -227,7 +227,7 @@ class CustomEmoji(snowflake.Unique, Emoji): https://github.com/discord/discord-api-docs/issues/1614#issuecomment-628548913 """ - app: rest.IRESTClient = attr.ib(default=None, repr=False, eq=False, hash=False, init=True) + app: rest_app.IRESTApp = attr.ib(default=None, repr=False, eq=False, hash=False, init=True) """The client application that models may use for procedures.""" id: snowflake.Snowflake = attr.ib( diff --git a/hikari/models/guilds.py b/hikari/models/guilds.py index c83dc4c5bc..851da31f6f 100644 --- a/hikari/models/guilds.py +++ b/hikari/models/guilds.py @@ -55,7 +55,7 @@ if typing.TYPE_CHECKING: import datetime - from hikari.api import rest + from hikari.api import rest_app from hikari.models import channels as channels_ from hikari.models import colors from hikari.models import emojis as emojis_ @@ -236,7 +236,7 @@ def __str__(self) -> str: class GuildWidget: """Represents a guild embed.""" - app: rest.IRESTClient = attr.ib(default=None, repr=False, eq=False, hash=False) + app: rest_app.IRESTApp = attr.ib(default=None, repr=False, eq=False, hash=False) """The client application that models may use for procedures.""" channel_id: typing.Optional[snowflake.Snowflake] = attr.ib(repr=True) @@ -250,7 +250,7 @@ class GuildWidget: class Member: """Used to represent a guild bound member.""" - app: rest.IRESTClient = attr.ib(default=None, repr=False, eq=False, hash=False) + app: rest_app.IRESTApp = attr.ib(default=None, repr=False, eq=False, hash=False) """The client application that models may use for procedures.""" # TODO: make Member delegate to user and implement a common base class @@ -310,7 +310,7 @@ class PartialRole(snowflake.Unique): ) """The ID of this entity.""" - app: rest.IRESTClient = attr.ib(default=None, repr=False, eq=False, hash=False) + app: rest_app.IRESTApp = attr.ib(default=None, repr=False, eq=False, hash=False) """The client application that models may use for procedures.""" name: str = attr.ib(eq=False, hash=False, repr=True) @@ -479,7 +479,7 @@ class PartialGuild(snowflake.Unique): ) """The ID of this entity.""" - app: rest.IRESTClient = attr.ib(default=None, repr=False, eq=False, hash=False) + app: rest_app.IRESTApp = attr.ib(default=None, repr=False, eq=False, hash=False) """The client application that models may use for procedures.""" name: str = attr.ib(eq=False, hash=False, repr=True) diff --git a/hikari/models/invites.py b/hikari/models/invites.py index 35d878b667..dcf081bb82 100644 --- a/hikari/models/invites.py +++ b/hikari/models/invites.py @@ -40,7 +40,7 @@ if typing.TYPE_CHECKING: import datetime - from hikari.api import rest + from hikari.api import rest_app from hikari.models import channels from hikari.models import users @@ -61,7 +61,7 @@ def __str__(self) -> str: class VanityURL: """A special case invite object, that represents a guild's vanity url.""" - app: rest.IRESTClient = attr.ib(default=None, repr=False, eq=False, hash=False) + app: rest_app.IRESTApp = attr.ib(default=None, repr=False, eq=False, hash=False) """The client application that models may use for procedures.""" code: str = attr.ib(eq=True, hash=True, repr=True) @@ -174,7 +174,7 @@ def format_banner(self, *, format_: str = "png", size: int = 4096) -> typing.Opt class Invite: """Represents an invite that's used to add users to a guild or group dm.""" - app: rest.IRESTClient = attr.ib(default=None, repr=False, eq=False, hash=False) + app: rest_app.IRESTApp = attr.ib(default=None, repr=False, eq=False, hash=False) """The client application that models may use for procedures.""" code: str = attr.ib(eq=True, hash=True, repr=True) diff --git a/hikari/models/messages.py b/hikari/models/messages.py index fd503278be..dcf0d6ff5a 100644 --- a/hikari/models/messages.py +++ b/hikari/models/messages.py @@ -42,7 +42,7 @@ if typing.TYPE_CHECKING: import datetime - from hikari.api import rest + from hikari.api import rest_app from hikari.models import applications from hikari.models import channels from hikari.models import embeds as embeds_ @@ -221,7 +221,7 @@ class MessageCrosspost: "published" to another. """ - app: rest.IRESTClient = attr.ib(default=None, repr=False, eq=False, hash=False) + app: rest_app.IRESTApp = attr.ib(default=None, repr=False, eq=False, hash=False) """The client application that models may use for procedures.""" id: typing.Optional[snowflake.Snowflake] = attr.ib(repr=True) @@ -250,7 +250,7 @@ class MessageCrosspost: class Message(snowflake.Unique): """Represents a message.""" - app: rest.IRESTClient = attr.ib(default=None, repr=False, eq=False, hash=False) + app: rest_app.IRESTApp = attr.ib(default=None, repr=False, eq=False, hash=False) """The client application that models may use for procedures.""" id: snowflake.Snowflake = attr.ib( diff --git a/hikari/models/presences.py b/hikari/models/presences.py index b42e70f554..a971cd3cba 100644 --- a/hikari/models/presences.py +++ b/hikari/models/presences.py @@ -44,7 +44,7 @@ if typing.TYPE_CHECKING: import datetime - from hikari.api import rest + from hikari.api import rest_app from hikari.models import emojis as emojis_ @@ -280,7 +280,7 @@ class ClientStatus: class MemberPresence: """Used to represent a guild member's presence.""" - app: rest.IRESTClient = attr.ib(default=None, repr=False, eq=False, hash=False) + app: rest_app.IRESTApp = attr.ib(default=None, repr=False, eq=False, hash=False) """The client application that models may use for procedures.""" user: users.PartialUser = attr.ib(eq=True, hash=True, repr=True) diff --git a/hikari/models/users.py b/hikari/models/users.py index 0cc54a0813..c59ea7f795 100644 --- a/hikari/models/users.py +++ b/hikari/models/users.py @@ -32,7 +32,7 @@ from hikari.utilities import undefined if typing.TYPE_CHECKING: - from hikari.api import rest + from hikari.api import rest_app @enum.unique @@ -117,7 +117,7 @@ class PartialUser(snowflake.Unique): ) """The ID of this entity.""" - app: rest.IRESTClient = attr.ib(default=None, repr=False, eq=False, hash=False) + app: rest_app.IRESTApp = attr.ib(default=None, repr=False, eq=False, hash=False) """The client application that models may use for procedures.""" discriminator: typing.Union[str, undefined.UndefinedType] = attr.ib(eq=False, hash=False, repr=True) diff --git a/hikari/models/voices.py b/hikari/models/voices.py index 8d7ccfb7bf..272bda292f 100644 --- a/hikari/models/voices.py +++ b/hikari/models/voices.py @@ -26,7 +26,7 @@ import attr if typing.TYPE_CHECKING: - from hikari.api import rest + from hikari.api import rest_app from hikari.models import guilds from hikari.utilities import snowflake @@ -35,7 +35,7 @@ class VoiceState: """Represents a user's voice connection status.""" - app: rest.IRESTClient = attr.ib(default=None, repr=False, eq=False, hash=False) + app: rest_app.IRESTApp = attr.ib(default=None, repr=False, eq=False, hash=False) """The client application that models may use for procedures.""" guild_id: typing.Optional[snowflake.Snowflake] = attr.ib(eq=False, hash=False, repr=True) diff --git a/hikari/models/webhooks.py b/hikari/models/webhooks.py index 6908bfe385..2ac666b940 100644 --- a/hikari/models/webhooks.py +++ b/hikari/models/webhooks.py @@ -32,7 +32,7 @@ from hikari.utilities import undefined if typing.TYPE_CHECKING: - from hikari.api import rest + from hikari.api import rest_app from hikari.models import channels as channels_ from hikari.models import embeds as embeds_ from hikari.models import guilds as guilds_ @@ -64,7 +64,7 @@ class Webhook(snowflake.Unique): send informational messages to specific channels. """ - app: rest.IRESTClient = attr.ib(default=None, repr=False, eq=False, hash=False) + app: rest_app.IRESTApp = attr.ib(default=None, repr=False, eq=False, hash=False) """The client application that models may use for procedures.""" id: snowflake.Snowflake = attr.ib( diff --git a/hikari/utilities/cdn.py b/hikari/utilities/cdn.py index b8d8875118..e0d34c36b2 100644 --- a/hikari/utilities/cdn.py +++ b/hikari/utilities/cdn.py @@ -24,7 +24,7 @@ import typing import urllib.parse -from hikari.impl import strings +from hikari.impl import constants from hikari.utilities import files @@ -60,7 +60,7 @@ def generate_cdn_url(*route_parts: str, format_: str, size: typing.Optional[int] raise ValueError("Size must be an integer power of 2") path = "/".join(urllib.parse.unquote(part) for part in route_parts) - url = urllib.parse.urljoin(strings.CDN_URL, "/" + path) + "." + str(format_) + url = urllib.parse.urljoin(constants.CDN_URL, "/" + path) + "." + str(format_) query = urllib.parse.urlencode({"size": size}) if size is not None else None return files.URL(f"{url}?{query}" if query else url) diff --git a/hikari/utilities/files.py b/hikari/utilities/files.py index ab48d998b7..0a05e157b7 100644 --- a/hikari/utilities/files.py +++ b/hikari/utilities/files.py @@ -48,7 +48,7 @@ import aiohttp.client import attr -from hikari.impl import helpers +from hikari.impl import response_handler if typing.TYPE_CHECKING: import types @@ -491,7 +491,7 @@ async def __aenter__(self) -> WebReader: head_only=self._head_only, ) else: - raise await helpers.generate_error_response(resp) + raise await response_handler.generate_error_response(resp) except Exception as ex: await ctx.__aexit__(type(ex), ex, ex.__traceback__) diff --git a/tests/hikari/impl/test_entity_factory.py b/tests/hikari/impl/test_entity_factory.py index eb514ce0b8..21c13bc0ef 100644 --- a/tests/hikari/impl/test_entity_factory.py +++ b/tests/hikari/impl/test_entity_factory.py @@ -21,7 +21,7 @@ import pytest -from hikari.api import rest +from hikari.api import rest_app from hikari.impl import entity_factory from hikari.events import channel as channel_events from hikari.events import guild as guild_events @@ -76,8 +76,8 @@ def test__deserialize_max_age_returns_null(): class TestEntityFactoryImpl: @pytest.fixture() - def mock_app(self) -> rest.IRESTClient: - return mock.MagicMock(rest.IRESTClient) + def mock_app(self) -> rest_app.IRESTApp: + return mock.MagicMock(rest_app.IRESTApp) @pytest.fixture() def entity_factory_impl(self, mock_app) -> entity_factory.EntityFactoryComponentImpl: diff --git a/tests/hikari/impl/test_gateway.py b/tests/hikari/impl/test_gateway.py index dacb1f930f..1288b5b122 100644 --- a/tests/hikari/impl/test_gateway.py +++ b/tests/hikari/impl/test_gateway.py @@ -24,9 +24,9 @@ import mock import pytest +from hikari import config from hikari import errors from hikari.models import presences -from hikari.impl import config from hikari.impl import gateway from hikari.utilities import undefined from tests.hikari import client_session_stub @@ -52,7 +52,7 @@ def client_session(): @pytest.fixture() def client(http_settings, proxy_settings): - return gateway.Gateway( + return gateway.GatewayShardImpl( url="wss://gateway.discord.gg", token="lol", app=mock.MagicMock(), @@ -72,7 +72,7 @@ class TestInit: ], ) def test_url_is_correct_json(self, v, use_compression, expect, http_settings, proxy_settings): - g = gateway.Gateway( + g = gateway.GatewayShardImpl( app=mock.MagicMock(), token=mock.MagicMock(), http_settings=http_settings, @@ -88,7 +88,7 @@ def test_url_is_correct_json(self, v, use_compression, expect, http_settings, pr @pytest.mark.parametrize(["v", "use_compression"], [(6, False), (6, True), (7, False), (7, True),]) def test_using_etf_is_unsupported(self, v, use_compression, http_settings, proxy_settings): with pytest.raises(NotImplementedError): - gateway.Gateway( + gateway.GatewayShardImpl( app=mock.MagicMock(), http_settings=http_settings, proxy_settings=proxy_settings, @@ -103,7 +103,7 @@ def test_using_etf_is_unsupported(self, v, use_compression, http_settings, proxy class TestAppProperty: def test_returns_app(self, http_settings, proxy_settings): app = mock.MagicMock() - g = gateway.Gateway( + g = gateway.GatewayShardImpl( url="wss://gateway.discord.gg", token="lol", app=app, @@ -128,7 +128,7 @@ class TestStart: @pytest.mark.parametrize("shard_id", [0, 1, 2]) @hikari_test_helpers.timeout() async def test_starts_task(self, event_loop, shard_id, http_settings=http_settings, proxy_settings=proxy_settings): - g = gateway.Gateway( + g = gateway.GatewayShardImpl( url="wss://gateway.discord.gg", token="lol", app=mock.MagicMock(), @@ -163,7 +163,7 @@ async def test_waits_for_ready(self, client): class TestClose: @pytest.fixture def client(self): - class GatewayStub(gateway.Gateway): + class GatewayStub(gateway.GatewayShardImpl): @property def is_alive(self): return getattr(self, "_is_alive", False) @@ -261,7 +261,7 @@ async def test_sets_handshake_event_on_finish(self, client): class TestRunOnceShielded: @pytest.fixture def client(self, http_settings=http_settings, proxy_settings=proxy_settings): - client = hikari_test_helpers.unslot_class(gateway.Gateway)( + client = hikari_test_helpers.unslot_class(gateway.GatewayShardImpl)( url="wss://gateway.discord.gg", token="lol", app=mock.MagicMock(), @@ -314,7 +314,7 @@ async def test_socket_closed_resets_backoff( def run_once(): client._zombied = zombied - raise gateway.Gateway._SocketClosed() + raise gateway.GatewayShardImpl._SocketClosed() client._run_once = mock.AsyncMock(wraps=run_once) await client._run_once_shielded(client_session) @@ -326,7 +326,7 @@ def run_once(): @hikari_test_helpers.timeout() async def test_invalid_session_resume_does_not_clear_seq_or_session_id(self, client, client_session): - client._run_once = mock.AsyncMock(side_effect=gateway.Gateway._InvalidSession(True)) + client._run_once = mock.AsyncMock(side_effect=gateway.GatewayShardImpl._InvalidSession(True)) client._seq = 1234 client.session_id = "69420" await client._run_once_shielded(client_session) @@ -337,7 +337,7 @@ async def test_invalid_session_resume_does_not_clear_seq_or_session_id(self, cli @hikari_test_helpers.timeout() async def test_socket_closed_is_restartable_if_no_closure_request(self, client, request_close, client_session): client._request_close_event.is_set = mock.MagicMock(return_value=request_close) - client._run_once = mock.AsyncMock(side_effect=gateway.Gateway._SocketClosed()) + client._run_once = mock.AsyncMock(side_effect=gateway.GatewayShardImpl._SocketClosed()) assert await client._run_once_shielded(client_session) is not request_close @hikari_test_helpers.timeout() @@ -352,28 +352,28 @@ async def test_ClientConnectionError_is_restartable(self, client, client_session @hikari_test_helpers.timeout() async def test_invalid_session_is_restartable(self, client, client_session): - client._run_once = mock.AsyncMock(side_effect=gateway.Gateway._InvalidSession()) + client._run_once = mock.AsyncMock(side_effect=gateway.GatewayShardImpl._InvalidSession()) assert await client._run_once_shielded(client_session) is True @hikari_test_helpers.timeout() async def test_invalid_session_resume_does_not_invalidate_session(self, client, client_session): - client._run_once = mock.AsyncMock(side_effect=gateway.Gateway._InvalidSession(True)) + client._run_once = mock.AsyncMock(side_effect=gateway.GatewayShardImpl._InvalidSession(True)) await client._run_once_shielded(client_session) client._close_ws.assert_awaited_once_with( - gateway.Gateway._GatewayCloseCode.DO_NOT_INVALIDATE_SESSION, "invalid session (resume)" + gateway.GatewayShardImpl._GatewayCloseCode.DO_NOT_INVALIDATE_SESSION, "invalid session (resume)" ) @hikari_test_helpers.timeout() async def test_invalid_session_no_resume_invalidates_session(self, client, client_session): - client._run_once = mock.AsyncMock(side_effect=gateway.Gateway._InvalidSession(False)) + client._run_once = mock.AsyncMock(side_effect=gateway.GatewayShardImpl._InvalidSession(False)) await client._run_once_shielded(client_session) client._close_ws.assert_awaited_once_with( - gateway.Gateway._GatewayCloseCode.RFC_6455_NORMAL_CLOSURE, "invalid session (no resume)" + gateway.GatewayShardImpl._GatewayCloseCode.RFC_6455_NORMAL_CLOSURE, "invalid session (no resume)" ) @hikari_test_helpers.timeout() async def test_invalid_session_no_resume_clears_seq_and_session_id(self, client, client_session): - client._run_once = mock.AsyncMock(side_effect=gateway.Gateway._InvalidSession(False)) + client._run_once = mock.AsyncMock(side_effect=gateway.GatewayShardImpl._InvalidSession(False)) client._seq = 1234 client.session_id = "69420" await client._run_once_shielded(client_session) @@ -382,7 +382,7 @@ async def test_invalid_session_no_resume_clears_seq_and_session_id(self, client, @hikari_test_helpers.timeout() async def test_reconnect_is_restartable(self, client, client_session): - client._run_once = mock.AsyncMock(side_effect=gateway.Gateway._Reconnect()) + client._run_once = mock.AsyncMock(side_effect=gateway.GatewayShardImpl._Reconnect()) assert await client._run_once_shielded(client_session) is True @hikari_test_helpers.timeout() @@ -398,7 +398,7 @@ async def test_server_connection_error_closes_websocket_if_reconnectable(self, c client._run_once = mock.AsyncMock(side_effect=errors.GatewayServerClosedConnectionError("blah", None, True)) assert await client._run_once_shielded(client_session) is True client._close_ws.assert_awaited_once_with( - gateway.Gateway._GatewayCloseCode.RFC_6455_NORMAL_CLOSURE, "you hung up on me" + gateway.GatewayShardImpl._GatewayCloseCode.RFC_6455_NORMAL_CLOSURE, "you hung up on me" ) @hikari_test_helpers.timeout() @@ -418,7 +418,7 @@ async def test_server_connection_error_closes_websocket_if_not_reconnectable(sel with pytest.raises(errors.GatewayServerClosedConnectionError): await client._run_once_shielded(client_session) client._close_ws.assert_awaited_once_with( - gateway.Gateway._GatewayCloseCode.RFC_6455_UNEXPECTED_CONDITION, "you broke the connection" + gateway.GatewayShardImpl._GatewayCloseCode.RFC_6455_UNEXPECTED_CONDITION, "you broke the connection" ) @pytest.mark.parametrize( @@ -433,7 +433,7 @@ async def test_socket_closed_resets_backoff( def run_once(_): client._zombied = zombied - raise gateway.Gateway._SocketClosed() + raise gateway.GatewayShardImpl._SocketClosed() client._run_once = mock.AsyncMock(wraps=run_once) await client._run_once_shielded(client_session) @@ -450,7 +450,7 @@ async def test_other_exception_closes_websocket(self, client, client_session): await client._run_once_shielded(client_session) client._close_ws.assert_awaited_once_with( - gateway.Gateway._GatewayCloseCode.RFC_6455_UNEXPECTED_CONDITION, "unexpected error occurred" + gateway.GatewayShardImpl._GatewayCloseCode.RFC_6455_UNEXPECTED_CONDITION, "unexpected error occurred" ) @@ -458,7 +458,7 @@ async def test_other_exception_closes_websocket(self, client, client_session): class TestRunOnce: @pytest.fixture def client(self, http_settings=http_settings, proxy_settings=proxy_settings): - client = hikari_test_helpers.unslot_class(gateway.Gateway)( + client = hikari_test_helpers.unslot_class(gateway.GatewayShardImpl)( url="wss://gateway.discord.gg", token="lol", app=mock.MagicMock(), @@ -705,7 +705,7 @@ async def test_connected_at_reset_to_nan_on_exit(self, client, client_session): class TestUpdatePresence: @pytest.fixture def client(self, proxy_settings, http_settings): - client = hikari_test_helpers.unslot_class(gateway.Gateway)( + client = hikari_test_helpers.unslot_class(gateway.GatewayShardImpl)( url="wss://gateway.discord.gg", token="lol", app=mock.MagicMock(), @@ -744,7 +744,9 @@ async def test_update_presence_transforms_all_params(self, client): idle_since=idle_since, afk=afk, activity=activity, status=status, ) - client._send_json.assert_awaited_once_with({"op": gateway.Gateway._GatewayOpcode.PRESENCE_UPDATE, "d": result}) + client._send_json.assert_awaited_once_with( + {"op": gateway.GatewayShardImpl._GatewayOpcode.PRESENCE_UPDATE, "d": result} + ) @pytest.mark.parametrize("idle_since", [undefined.UNDEFINED, datetime.datetime.now()]) @pytest.mark.parametrize("afk", [undefined.UNDEFINED, True, False]) @@ -789,7 +791,7 @@ async def test_update_presence_ignores_undefined(self, client, idle_since, afk, class TestUpdateVoiceState: @pytest.fixture def client(self, proxy_settings, http_settings): - client = hikari_test_helpers.unslot_class(gateway.Gateway)( + client = hikari_test_helpers.unslot_class(gateway.GatewayShardImpl)( url="wss://gateway.discord.gg", token="lol", app=mock.MagicMock(), @@ -824,7 +826,7 @@ async def test_serialized_result_sent_on_websocket(self, client): await client.update_voice_state("6969420", "12345") client._send_json.assert_awaited_once_with( - {"op": gateway.Gateway._GatewayOpcode.VOICE_STATE_UPDATE, "d": payload} + {"op": gateway.GatewayShardImpl._GatewayOpcode.VOICE_STATE_UPDATE, "d": payload} ) From a02273c6f0391e98abb4da6c133a14f8f0da05d9 Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Tue, 30 Jun 2020 17:24:44 +0100 Subject: [PATCH 609/922] "Fixes" #410 by hiding the bug. --- docs/documentation.mako | 33 +++++---------------------------- 1 file changed, 5 insertions(+), 28 deletions(-) diff --git a/docs/documentation.mako b/docs/documentation.mako index 81fec0cff6..532768744d 100644 --- a/docs/documentation.mako +++ b/docs/documentation.mako @@ -60,7 +60,6 @@ text = project_inventory.data_file(contract=True) ztext = sphobjinv.compress(text) sphobjinv.writebytes('public/objects.inv', ztext) - %> <% @@ -373,10 +372,8 @@ <%def name="show_class(c, is_nested=False)"> <% - class_vars = c.class_variables(show_inherited_members, sort=sort_identifiers) - smethods = c.functions(show_inherited_members, sort=sort_identifiers) - inst_vars = c.instance_variables(show_inherited_members, sort=sort_identifiers) - methods = c.methods(show_inherited_members, sort=sort_identifiers) + variables = c.instance_variables(show_inherited_members, sort=sort_identifiers) + c.class_variables(show_inherited_members, sort=sort_identifiers) + methods = c.methods(show_inherited_members, sort=sort_identifiers) + c.functions(show_inherited_members, sort=sort_identifiers) mro = c.mro() subclasses = c.subclasses() @@ -465,7 +462,7 @@ % endif % if methods: -
    Instance methods
    +
    Methods
    % for m in methods: ${show_func(m)} @@ -475,34 +472,14 @@ % endif % if inst_vars: -
    Instance variables and properties
    +
    Variables and properties
    - % for i in inst_vars: + % for i in variables: ${show_var(i)} % endfor
    % endif - - % if smethods: -
    Class methods
    -
    - % for m in smethods: - ${show_func(m)} - % endfor -
    -
    - % endif - - % if class_vars: -
    Class variables and properties
    -
    - % for cv in class_vars: - ${show_var(cv)} - % endfor -
    -
    - % endif % endif From 7a7c48e47e62e4caf2fdd7e8db1aacfe8bf50232 Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Tue, 30 Jun 2020 17:41:20 +0100 Subject: [PATCH 610/922] Fixed more broken pdoc stuff. --- docs/documentation.mako | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/docs/documentation.mako b/docs/documentation.mako index 532768744d..1cf1b4bc35 100644 --- a/docs/documentation.mako +++ b/docs/documentation.mako @@ -170,7 +170,10 @@ prefix = f"{qual} " else: - prefix = f"{default_type} " + if isinstance(dobj, pdoc.External): + prefix = f"{QUAL_EXTERNAL} {default_type} " + else: + prefix = f"{default_type} " else: name = name or dobj.name or "" @@ -188,7 +191,7 @@ url = discover_source(name) if url is None: - return name if not with_prefixes else f"{QUAL_EXTERNAL} {name}" + return name else: try: ref = dobj if not hasattr(dobj.obj, "__module__") else pdoc._global_context[dobj.obj.__module__ + "." + dobj.obj.__qualname__] @@ -441,8 +444,10 @@
    Subclasses
    % for sc in subclasses: -
    ${link(sc, with_prefixes=True, default_type="class")}
    -
    ${sc.docstring | glimpse, to_html}
    + % if not isinstance(sc, pdoc.External): +
    ${link(sc, with_prefixes=True, default_type="class")}
    +
    ${sc.docstring | glimpse, to_html}
    + % endif % endfor
    @@ -492,7 +497,7 @@ % if not short: % if d.inherits:

    - Inherited from: + Inherited from: % if hasattr(d.inherits, 'cls'): ${link(d.inherits.cls, with_prefixes=False)}.${link(d.inherits, name=d.name, with_prefixes=False)} % else: From 765c872a8096c121872d0f2a1c54b51f709fd8f3 Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Tue, 30 Jun 2020 21:40:52 +0100 Subject: [PATCH 611/922] Fixes #394 mostly, fixes a TONNE of pdoc3 voodoo type shit. --- docs/documentation.mako | 171 +++++++++++++------------------ hikari/api/bot.py | 13 ++- hikari/api/entity_factory.py | 102 +++++++++--------- hikari/api/event_consumer.py | 2 +- hikari/api/event_dispatcher.py | 52 ++++++---- hikari/api/gateway.py | 31 +++--- hikari/api/gateway_zookeeper.py | 19 ++-- hikari/api/rest_app.py | 8 +- hikari/api/rest_client.py | 105 ++++++++++--------- hikari/api/special_endpoints.py | 54 +++++----- hikari/api/voice.py | 12 +-- hikari/config.py | 12 +-- hikari/errors.py | 35 ++++--- hikari/events/channel.py | 10 +- hikari/events/message.py | 10 +- hikari/impl/bot.py | 80 ++++++++------- hikari/impl/buckets.py | 48 ++++----- hikari/impl/gateway.py | 44 ++++---- hikari/impl/gateway_zookeeper.py | 65 ++++++------ hikari/impl/rate_limits.py | 57 ++++++----- hikari/impl/rest_app.py | 30 +++--- hikari/impl/rest_client.py | 18 ++-- hikari/impl/routes.py | 12 +-- hikari/impl/stateless_cache.py | 2 +- hikari/models/applications.py | 68 ++++++------ hikari/models/channels.py | 45 ++++---- hikari/models/colors.py | 42 ++++---- hikari/models/embeds.py | 52 +++++----- hikari/models/emojis.py | 8 +- hikari/models/guilds.py | 130 +++++++++++------------ hikari/models/intents.py | 2 +- hikari/models/invites.py | 40 ++++---- hikari/models/messages.py | 75 +++++++------- hikari/models/presences.py | 6 +- hikari/models/users.py | 35 ++++--- hikari/models/voices.py | 2 +- hikari/models/webhooks.py | 103 ++++++++++--------- hikari/utilities/aio.py | 9 +- hikari/utilities/cdn.py | 14 +-- hikari/utilities/data_binding.py | 35 ++++--- hikari/utilities/date.py | 16 +-- hikari/utilities/files.py | 129 +++++++++++------------ hikari/utilities/iterators.py | 106 ++++++++++--------- hikari/utilities/reflect.py | 10 +- hikari/utilities/snowflake.py | 2 +- 45 files changed, 973 insertions(+), 948 deletions(-) diff --git a/docs/documentation.mako b/docs/documentation.mako index 1cf1b4bc35..ab81e3cbed 100644 --- a/docs/documentation.mako +++ b/docs/documentation.mako @@ -1,5 +1,9 @@ <%! + import typing + typing.TYPE_CHECKING = True + import builtins + import re import sphobjinv inventory_urls = [ @@ -21,14 +25,15 @@ located_external_refs = {} unlocatable_external_refs = set() - def discover_source(fqn): - #print("attempting to find", fqn, "in intersphinx inventories") if fqn in unlocatable_external_refs: return + if fqn.startswith("builtins."): + fqn = fqn.replace("builtins.", "") + if fqn not in located_external_refs: - # print("attempting to find intersphinx reference for", fqn) + print("attempting to find intersphinx reference for", fqn) for base_url, inv in inventories.items(): for obj in inv.values(): if isinstance(obj, dict) and obj["name"] == fqn: @@ -60,17 +65,23 @@ text = project_inventory.data_file(contract=True) ztext = sphobjinv.compress(text) sphobjinv.writebytes('public/objects.inv', ztext) -%> -<% - import typing - typing.TYPE_CHECKING = True + # To get links to work in type hints to builtins, we do a bit of hacky search-replace using regex. + # This generates regex to match general builtins in typehints. + builtin_patterns = [ + re.compile("(? +<% import abc import enum import functools import inspect + import re import textwrap import pdoc @@ -96,24 +107,25 @@ QUAL_VAR = "var" def link( - dobj: pdoc.Doc, - *, - with_prefixes=False, - simple_names=False, - css_classes="", - name=None, - default_type="", - dotted=True, - anchor=False, + dobj: pdoc.Doc, + *, + with_prefixes=False, + simple_names=False, + css_classes="", + name=None, + default_type="", + dotted=True, + anchor=False, fully_qualified=False, hide_ref=False, + recurse=True, ): prefix = "" name = name or dobj.name if name.startswith("builtins."): _, _, name = name.partition("builtins.") - + show_object = False if with_prefixes: if isinstance(dobj, pdoc.Function): @@ -162,7 +174,7 @@ if inspect.isabstract(dobj.obj): qual = f"{QUAL_ABC} {qual}" - + prefix = f"{qual} " elif isinstance(dobj, pdoc.Module): @@ -182,7 +194,9 @@ if isinstance(dobj, pdoc.External): if dobj.module: - fqn = dobj.module.name + "." + dobj.obj.__qualname__ + fqn = dobj.module.obj.__name__ + "." + dobj.obj.__qualname__ + elif hasattr(dobj.obj, "__module__"): + fqn = dobj.obj.__module__ + "." + dobj.obj.__qualname__ else: fqn = dobj.name @@ -191,6 +205,34 @@ url = discover_source(name) if url is None: + if fqn_match := re.match(r"([a-z_]+)\.((?:[^\.]|^\s)+)", fqn): + print("Struggling to resolve", fqn, "in", module.name, "so attempting to see if it is an import alias now instead.") + + if import_match := re.search(f"from (.*) import (.*) as {fqn_match.group(1)}", module.source): + old_fqn = fqn + fqn = import_match.group(1) + "." + import_match.group(2) + "." + fqn_match.group(2) + try: + url = pdoc._global_context[fqn].url(relative_to=module, link_prefix=link_prefix, top_ancestor=not show_inherited_members) + print(old_fqn, "->", fqn, "via", url) + except KeyError: + print("maybe", fqn, "is external but aliased?") + url = discover_source(fqn) + elif import_match := re.search(f"import (.*) as {fqn_match.group(1)}", module.source): + old_fqn = fqn + fqn = import_match.group(1) + "." + fqn_match.group(2) + try: + url = pdoc._global_context[fqn].url(relative_to=module, link_prefix=link_prefix, top_ancestor=not show_inherited_members) + print(old_fqn, "->", fqn, "via", url) + except KeyError: + print("maybe", fqn, "is external but aliased?") + url = discover_source(fqn) + else: + # print("No clue where", fqn, "came from --- it isn't an import that i can see.") + pass + + + if url is None: + # print("Could not resolve where", fqn, "came from :(") return name else: try: @@ -226,7 +268,7 @@ def get_annotation(bound_method, sep=':'): annot = bound_method(link=link) - + annot = annot.replace("NoneType", "None") # Remove quotes. if annot.startswith("'") and annot.endswith("'"): @@ -234,6 +276,9 @@ if annot: annot = ' ' + sep + '\N{NBSP}' + annot + for pattern in builtin_patterns: + annot = pattern.sub(r"builtins.\1", annot) + return annot def to_html(text): @@ -247,7 +292,7 @@ for before, after in replacements: text = text.replace(before, after) - + return text %> @@ -327,11 +372,15 @@ *(f" {p}," for p in params), ")" + return_type + ": ..." )) + elif params: representation = f"{f.funcdef()} {f.name}({', '.join(params)}){return_type}: ..." else: representation = f"{f.funcdef()} {f.name}(){return_type}: ..." + for pattern in builtin_patterns: + representation = pattern.sub(r"builtins.\1", representation) + if f.module.name != f.obj.__module__: try: ref = pdoc._global_context[f.obj.__module__ + "." + f.obj.__qualname__] @@ -394,6 +443,9 @@ else: representation = f"{QUAL_CLASS} {c.name}: ..." + for pattern in builtin_patterns: + representation = pattern.sub(r"builtins.\1", representation) + if c.module.name != c.obj.__module__: try: ref = pdoc._global_context[c.obj.__module__ + "." + c.obj.__qualname__] @@ -476,7 +528,7 @@

    % endif - % if inst_vars: + % if variables:
    Variables and properties
    % for i in variables: @@ -678,80 +730,5 @@ % endif

    - -
    -

    Notation used in this documentation

    -
    -
    ${QUAL_DEF}
    -
    Regular function.
    - -
    ${QUAL_ASYNC_DEF}
    -
    Coroutine function that should be awaited.
    - -
    ${QUAL_CLASS}
    -
    Regular class that provides a certain functionality.
    - -
    ${QUAL_ABC}
    -
    - Abstract member. These must be subclassed/overridden with a - concrete implementation elsewhere to be used. -
    - -
    ${QUAL_DATACLASS}
    -
    - Data class. This is a class designed to model and store information - rather than provide a certain behaviour or functionality. -
    - -
    ${QUAL_ENUM}
    -
    Enumerated type.
    - -
    ${QUAL_ENUM_FLAG}
    -
    Enumerated flag type. Supports being combined.
    - -
    ${QUAL_METACLASS}
    -
    - Metaclass. This is a base type of a class, used to control how implementing - classes are created, exist, operate, and get destroyed. -
    - -
    ${QUAL_MODULE}
    -
    Python module that you can import directly
    - -
    ${QUAL_PACKAGE}
    -
    Python package that can be imported and can contain sub-modules.
    - -
    ${QUAL_PROPERTY}
    -
    - Property type. Will always support read operations. -
    - -
    ${QUAL_NAMESPACE}
    -
    Python namespace package that can contain sub-modules, but is not directly importable.
    - -
    ${QUAL_TYPEHINT}
    -
    - An object or attribute used to denote a certain type or combination of types. - These usually provide no functionality and only exist for documentation purposes - and for static type-checkers. -
    - -
    ${QUAL_VAR}
    -
    - Variable or attribute. -
    - -
    ${QUAL_CONST}
    -
    - Value that should not be changed manually. -
    - -
    ${QUAL_EXTERNAL}
    -
    - Attribute or object that is not covered by this documentation. This usually - denotes types from other dependencies, or from the standard library. -
    -
    -
diff --git a/hikari/api/bot.py b/hikari/api/bot.py index 9de499ac46..93b2c1e055 100644 --- a/hikari/api/bot.py +++ b/hikari/api/bot.py @@ -49,6 +49,11 @@ def uptime(self) -> datetime.timedelta: If the application has not been started, then this will return a `datetime.timedelta` of 0 seconds. + + Returns + ------- + datetime.timedelta + The number of seconds the application has been running. """ @property @@ -57,5 +62,11 @@ def started_at(self) -> typing.Optional[datetime.datetime]: """Return the timestamp when the bot was started. If the application has not been started, then this will return - `None`. + `builtins.None`. + + Returns + ------- + datetime.datetime or builtins.None + The date/time that the application started at, or `builtins.None` if + not yet running. """ diff --git a/hikari/api/entity_factory.py b/hikari/api/entity_factory.py index 4315d562b2..292dd5d6b7 100644 --- a/hikari/api/entity_factory.py +++ b/hikari/api/entity_factory.py @@ -395,7 +395,7 @@ def deserialize_emoji( Returns ------- - hikari.models.emojis.UnicodeEmoji | hikari.models.emoji.CustomEmoji + hikari.models.emojis.UnicodeEmoji or hikari.models.emoji.CustomEmoji The deserialized custom or unicode emoji object. """ @@ -750,7 +750,7 @@ def deserialize_channel_create_event(self, payload: data_binding.JSONObject) -> Parameters ---------- - payload : typing.Mapping[str, typing.Any] + payload : typing.Mapping[builtins.str, typing.Any] The dict payload to parse. Returns @@ -765,7 +765,7 @@ def deserialize_channel_update_event(self, payload: data_binding.JSONObject) -> Parameters ---------- - payload : typing.Mapping[str, typing.Any] + payload : typing.Mapping[builtins.str, typing.Any] The dict payload to parse. Returns @@ -780,7 +780,7 @@ def deserialize_channel_delete_event(self, payload: data_binding.JSONObject) -> Parameters ---------- - payload : typing.Mapping[str, typing.Any] + payload : typing.Mapping[builtins.str, typing.Any] The dict payload to parse. Returns @@ -797,7 +797,7 @@ def deserialize_channel_pins_update_event( Parameters ---------- - payload : typing.Mapping[str, typing.Any] + payload : typing.Mapping[builtins.str, typing.Any] The dict payload to parse. Returns @@ -812,7 +812,7 @@ def deserialize_webhook_update_event(self, payload: data_binding.JSONObject) -> Parameters ---------- - payload : typing.Mapping[str, typing.Any] + payload : typing.Mapping[builtins.str, typing.Any] The dict payload to parse. Returns @@ -827,7 +827,7 @@ def deserialize_typing_start_event(self, payload: data_binding.JSONObject) -> ch Parameters ---------- - payload : typing.Mapping[str, typing.Any] + payload : typing.Mapping[builtins.str, typing.Any] The dict payload to parse. Returns @@ -842,7 +842,7 @@ def deserialize_invite_create_event(self, payload: data_binding.JSONObject) -> c Parameters ---------- - payload : typing.Mapping[str, typing.Any] + payload : typing.Mapping[builtins.str, typing.Any] The dict payload to parse. Returns @@ -857,7 +857,7 @@ def deserialize_invite_delete_event(self, payload: data_binding.JSONObject) -> c Parameters ---------- - payload : typing.Mapping[str, typing.Any] + payload : typing.Mapping[builtins.str, typing.Any] The dict payload to parse. Returns @@ -876,7 +876,7 @@ def deserialize_guild_create_event(self, payload: data_binding.JSONObject) -> gu Parameters ---------- - payload : typing.Mapping[str, typing.Any] + payload : typing.Mapping[builtins.str, typing.Any] The dict payload to parse. Returns @@ -891,7 +891,7 @@ def deserialize_guild_update_event(self, payload: data_binding.JSONObject) -> gu Parameters ---------- - payload : typing.Mapping[str, typing.Any] + payload : typing.Mapping[builtins.str, typing.Any] The dict payload to parse. Returns @@ -906,7 +906,7 @@ def deserialize_guild_leave_event(self, payload: data_binding.JSONObject) -> gui Parameters ---------- - payload : typing.Mapping[str, typing.Any] + payload : typing.Mapping[builtins.str, typing.Any] The dict payload to parse. Returns @@ -923,12 +923,12 @@ def deserialize_guild_unavailable_event( Parameters ---------- - payload : typing.Mapping[str, typing.Any] + payload : typing.Mapping[builtins.str, typing.Any] The dict payload to parse. Returns ------- - hikari.events. + hikari.events.guild.GuildUnavailableEvent The parsed guild unavailable event object. """ @@ -938,7 +938,7 @@ def deserialize_guild_ban_add_event(self, payload: data_binding.JSONObject) -> g Parameters ---------- - payload : typing.Mapping[str, typing.Any] + payload : typing.Mapping[builtins.str, typing.Any] The dict payload to parse. Returns @@ -953,7 +953,7 @@ def deserialize_guild_ban_remove_event(self, payload: data_binding.JSONObject) - Parameters ---------- - payload : typing.Mapping[str, typing.Any] + payload : typing.Mapping[builtins.str, typing.Any] The dict payload to parse. Returns @@ -970,7 +970,7 @@ def deserialize_guild_emojis_update_event( Parameters ---------- - payload : typing.Mapping[str, typing.Any] + payload : typing.Mapping[builtins.str, typing.Any] The dict payload to parse. Returns @@ -987,7 +987,7 @@ def deserialize_guild_integrations_update_event( Parameters ---------- - payload : typing.Mapping[str, typing.Any] + payload : typing.Mapping[builtins.str, typing.Any] The dict payload to parse. Returns @@ -1002,7 +1002,7 @@ def deserialize_guild_member_add_event(self, payload: data_binding.JSONObject) - Parameters ---------- - payload : typing.Mapping[str, typing.Any] + payload : typing.Mapping[builtins.str, typing.Any] The dict payload to parse. Returns @@ -1019,7 +1019,7 @@ def deserialize_guild_member_update_event( Parameters ---------- - payload : typing.Mapping[str, typing.Any] + payload : typing.Mapping[builtins.str, typing.Any] The dict payload to parse. Returns @@ -1036,7 +1036,7 @@ def deserialize_guild_member_remove_event( Parameters ---------- - payload : typing.Mapping[str, typing.Any] + payload : typing.Mapping[builtins.str, typing.Any] The dict payload to parse. Returns @@ -1053,7 +1053,7 @@ def deserialize_guild_role_create_event( Parameters ---------- - payload : typing.Mapping[str, typing.Any] + payload : typing.Mapping[builtins.str, typing.Any] The dict payload to parse. Returns @@ -1070,7 +1070,7 @@ def deserialize_guild_role_update_event( Parameters ---------- - payload : typing.Mapping[str, typing.Any] + payload : typing.Mapping[builtins.str, typing.Any] The dict payload to parse. Returns @@ -1087,7 +1087,7 @@ def deserialize_guild_role_delete_event( Parameters ---------- - payload : typing.Mapping[str, typing.Any] + payload : typing.Mapping[builtins.str, typing.Any] The dict payload to parse. Returns @@ -1102,7 +1102,7 @@ def deserialize_presence_update_event(self, payload: data_binding.JSONObject) -> Parameters ---------- - payload : typing.Mapping[str, typing.Any] + payload : typing.Mapping[builtins.str, typing.Any] The dict payload to parse. Returns @@ -1121,7 +1121,7 @@ def deserialize_message_create_event(self, payload: data_binding.JSONObject) -> Parameters ---------- - payload : typing.Mapping[str, typing.Any] + payload : typing.Mapping[builtins.str, typing.Any] The dict payload to parse. Returns @@ -1136,7 +1136,7 @@ def deserialize_message_update_event(self, payload: data_binding.JSONObject) -> Parameters ---------- - payload : typing.Mapping[str, typing.Any] + payload : typing.Mapping[builtins.str, typing.Any] The dict payload to parse. Returns @@ -1151,7 +1151,7 @@ def deserialize_message_delete_event(self, payload: data_binding.JSONObject) -> Parameters ---------- - payload : typing.Mapping[str, typing.Any] + payload : typing.Mapping[builtins.str, typing.Any] The dict payload to parse. Returns @@ -1168,7 +1168,7 @@ def deserialize_message_delete_bulk_event( Parameters ---------- - payload : typing.Mapping[str, typing.Any] + payload : typing.Mapping[builtins.str, typing.Any] The dict payload to parse. Returns @@ -1185,7 +1185,7 @@ def deserialize_message_reaction_add_event( Parameters ---------- - payload : typing.Mapping[str, typing.Any] + payload : typing.Mapping[builtins.str, typing.Any] The dict payload to parse. Returns @@ -1202,7 +1202,7 @@ def deserialize_message_reaction_remove_event( Parameters ---------- - payload : typing.Mapping[str, typing.Any] + payload : typing.Mapping[builtins.str, typing.Any] The dict payload to parse. Returns @@ -1219,7 +1219,7 @@ def deserialize_message_reaction_remove_all_event( Parameters ---------- - payload : typing.Mapping[str, typing.Any] + payload : typing.Mapping[builtins.str, typing.Any] The dict payload to parse. Returns @@ -1236,7 +1236,7 @@ def deserialize_message_reaction_remove_emoji_event( Parameters ---------- - payload : typing.Mapping[str, typing.Any] + payload : typing.Mapping[builtins.str, typing.Any] The dict payload to parse. Returns @@ -1259,7 +1259,7 @@ def deserialize_ready_event( ---------- shard : hikari.api.gateway.IGatewayShard The shard that was ready. - payload : typing.Mapping[str, typing.Any] + payload : typing.Mapping[builtins.str, typing.Any] The dict payload to parse. Returns @@ -1274,7 +1274,7 @@ def deserialize_own_user_update_event(self, payload: data_binding.JSONObject) -> Parameters ---------- - payload : typing.Mapping[str, typing.Any] + payload : typing.Mapping[builtins.str, typing.Any] The dict payload to parse. Returns @@ -1295,7 +1295,7 @@ def deserialize_voice_state_update_event( Parameters ---------- - payload : typing.Mapping[str, typing.Any] + payload : typing.Mapping[builtins.str, typing.Any] The dict payload to parse. Returns @@ -1312,7 +1312,7 @@ def deserialize_voice_server_update_event( Parameters ---------- - payload : typing.Mapping[str, typing.Any] + payload : typing.Mapping[builtins.str, typing.Any] The dict payload to parse. Returns @@ -1340,11 +1340,11 @@ def serialize_gateway_presence( Parameters ---------- - idle_since : hikari.utilities.undefined.UndefinedType or None or datetime.datetime - The time that the user should appear to be idle since. If `None`, - then the user is marked as not being idle. - afk : hikari.utilities.undefined.UndefinedType or bool - If `True`, the user becomes AFK. This will move them + idle_since : hikari.utilities.undefined.UndefinedType or builtins.None or datetime.datetime + The time that the user should appear to be idle since. If + `builtins.None`, then the user is marked as not being idle. + afk : hikari.utilities.undefined.UndefinedType or builtins.bool + If `builtins.True`, the user becomes AFK. This will move them status : hikari.utilities.undefined.UndefinedType or hikari.models.presences.Status activity : hikari.utilities.undefined.UndefinedType or None or hikari.models.presences.Activity @@ -1369,15 +1369,15 @@ def serialize_gateway_voice_state_update( ---------- guild : hikari.models.guilds.Guild or hikari.utilities.snowflake.UniqueObject The guild to update the voice state in. - channel : hikari.models.channels.GuildVoiceChannel or hikari.utilities.snowflake.UniqueObject or None - The voice channel to change to, or `None` if attempting to leave a - voice channel and disconnect entirely. - self_mute : bool - `True` if the user should be muted, `False` if they should be - unmuted. - self_deaf : bool - `True` if the user should be deafened, `False` if they should be - able to hear other users. + channel : hikari.models.channels.GuildVoiceChannel or hikari.utilities.snowflake.UniqueObject or builtins.None + The voice channel to change to, or `builtins.None` if attempting to + leave a voice channel and disconnect entirely. + self_mute : builtins.bool + `builtins.True` if the user should be muted, `builtins.False` if + they should be unmuted. + self_deaf : builtins.bool + `builtins.True` if the user should be deafened, `builtins.False` + if they should be able to hear other users. Returns ------- diff --git a/hikari/api/event_consumer.py b/hikari/api/event_consumer.py index b24f773800..8e54e59cb6 100644 --- a/hikari/api/event_consumer.py +++ b/hikari/api/event_consumer.py @@ -52,7 +52,7 @@ async def consume_raw_event( ---------- shard : hikari.api.gateway.IGatewayShard The gateway shard that emitted the event. - event_name : str + event_name : builtins.str The event name. payload : hikari.utilities.data_binding.JSONObject The payload provided with the event. diff --git a/hikari/api/event_dispatcher.py b/hikari/api/event_dispatcher.py index ee4ef7623c..09c4bf53b1 100644 --- a/hikari/api/event_dispatcher.py +++ b/hikari/api/event_dispatcher.py @@ -116,7 +116,7 @@ async def on_everyone_mentioned(event): Returns ------- - asyncio.Future + asyncio.Future[typing.Any] A future that can be optionally awaited. If awaited, the future will complete once all corresponding event listeners have been invoked. If not awaited, this will schedule the dispatch of the @@ -184,17 +184,21 @@ def get_listeners( ---------- event_type : typing.Type[hikari.events.base.Event] The event type to look for. - polymorphic : bool - If `True`, this will return `True` if a subclass of the given - event type has a listener registered. If `False`, then only - listeners for this class specifically are returned. The default - is `True`. + polymorphic : builtins.bool + If `builtins.True`, this will return `builtins.True` if a subclass + of the given event type has a listener registered. If + `builtins.False`, then only listeners for this class specifically + are returned. The default is `builtins.True`. Returns ------- - typing.Collection[typing.Callable[[EventT], typing.Coroutine[None, typing.Any, None]] + typing.Collection[typing.Callable[[EventT], typing.Coroutine[builtins.None, typing.Any, builtins.None]] A copy of the collection of listeners for the event. Will return an empty collection if nothing is registered. + + See Also + -------- + `hikari.api.event_dispatcher.IEventDispatcherBase.has_listener` """ @abc.abstractmethod @@ -211,13 +215,17 @@ def has_listener( ---------- event_type : typing.Type[hikari.events.base.Event] The event type to look for. - callback : + callback The callback to look for. - polymorphic : bool - If `True`, this will return `True` if a subclass of the given - event type has a listener registered. If `False`, then only - listeners for this class specifically are checked. The default - is `True`. + polymorphic : builtins.bool + If `builtins.True`, this will return `builtins.True` if a subclass + of the given event type has a listener registered. If + `builtins.False`, then only listeners for this class specifically + are checked. The default is `builtins.True`. + + See Also + -------- + `hikari.api.event_dispatcher.IEventDispatcherBase.get_listeners` """ @abc.abstractmethod @@ -294,14 +302,14 @@ async def wait_for( this type additionally. predicate A function or coroutine taking the event as the single parameter. - This should return `True` if the event is one you want to return, - or `False` if the event should not be returned. - timeout : float or int or None + This should return `builtins.True` if the event is one you want to return, + or `builtins.False` if the event should not be returned. + timeout : builtins.float or builtins.int or builtins.None The amount of time to wait before raising an `asyncio.TimeoutError` - and giving up instead. This is measured in seconds. If `None`, then - no timeout will be waited for (no timeout can result in "leaking" of - coroutines that never complete if called in an uncontrolled way, - so is not recommended). + and giving up instead. This is measured in seconds. If + `builtins.None`, then no timeout will be waited for (no timeout can + result in "leaking" of coroutines that never complete if called in + an uncontrolled way, so is not recommended). Returns ------- @@ -311,8 +319,8 @@ async def wait_for( Raises ------ asyncio.TimeoutError - If the timeout is not `None` and is reached before an event is - received that the predicate returns `True` for. + If the timeout is not `builtins.None` and is reached before an event is + received that the predicate returns `builtins.True` for. See Also -------- diff --git a/hikari/api/gateway.py b/hikari/api/gateway.py index fe1d728683..4baa4358de 100644 --- a/hikari/api/gateway.py +++ b/hikari/api/gateway.py @@ -47,7 +47,7 @@ class IGatewayShard(component.IComponent, abc.ABC): @property @abc.abstractmethod def is_alive(self) -> bool: - """Return `True` if the shard is alive and connected.""" + """Return `builtins.True` if the shard is alive and connected.""" @abc.abstractmethod async def start(self) -> asyncio.Task[None]: @@ -55,7 +55,7 @@ async def start(self) -> asyncio.Task[None]: Returns ------- - asyncio.Task + asyncio.Task[builtins.None] The task containing the shard running logic. Awaiting this will wait until the shard has shut down before returning. """ @@ -77,13 +77,14 @@ async def update_presence( Parameters ---------- - idle_since : datetime.datetime or None or hikari.utilities.undefined.UndefinedType + idle_since : datetime.datetime or builtins.None or hikari.utilities.undefined.UndefinedType The datetime that the user started being idle. If undefined, this will not be changed. - afk : bool or hikari.utilities.undefined.UndefinedType - If `True`, the user is marked as AFK. If `False`, the user is marked - as being active. If undefined, this will not be changed. - activity : hikari.models.presences.Activity or None or hikari.utilities.undefined.UndefinedType + afk : builtins.bool or hikari.utilities.undefined.UndefinedType + If `builtins.True`, the user is marked as AFK. If `builtins.False`, + the user is marked as being active. If undefined, this will not be + changed. + activity : hikari.models.presences.Activity or builtins.None or hikari.utilities.undefined.UndefinedType The activity to appear to be playing. If undefined, this will not be changed. status : hikari.models.presences.Status or hikari.utilities.undefined.UndefinedType @@ -105,14 +106,14 @@ async def update_voice_state( ---------- guild : hikari.models.guilds.PartialGuild or hikari.utilities.snowflake.UniqueObject The guild or guild ID to update the voice state for. - channel : hikari.models.channels.GuildVoiceChannel or hikari.utilities.snowflake.UniqueObject or None - The channel or channel ID to update the voice state for. If `None` + channel : hikari.models.channels.GuildVoiceChannel or hikari.utilities.snowflake.UniqueObject or builtins.None + The channel or channel ID to update the voice state for. If `builtins.None` then the bot will leave the voice channel that it is in for the given guild. - self_mute : bool - If `True`, the bot will mute itself in that voice channel. If - `False`, then it will unmute itself. - self_deaf : bool - If `True`, the bot will deafen itself in that voice channel. If - `False`, then it will undeafen itself. + self_mute : builtins.bool + If `builtins.True`, the bot will mute itself in that voice channel. If + `builtins.False`, then it will unmute itself. + self_deaf : builtins.bool + If `builtins.True`, the bot will deafen itself in that voice channel. If + `builtins.False`, then it will undeafen itself. """ diff --git a/hikari/api/gateway_zookeeper.py b/hikari/api/gateway_zookeeper.py index b39dfec747..1c2470d683 100644 --- a/hikari/api/gateway_zookeeper.py +++ b/hikari/api/gateway_zookeeper.py @@ -38,7 +38,7 @@ class IGatewayZookeeperApp(event_consumer.IEventConsumerApp, abc.ABC): """Component specialization that looks after a set of shards. These events will be produced by a low-level gateway implementation, and - will produce `list` and `dict` types only. + will produce `builtins.list` and `builtins.dict` types only. This may be combined with `IEventDispatcherApp` for most single-process bots, or may be a specific component for large distributed applications @@ -64,7 +64,7 @@ def shards(self) -> typing.Mapping[int, gateway.IGatewayShard]: Returns ------- - typing.Mapping[int, hikari.api.gateway.IGatewayShard] + typing.Mapping[builtins.int, hikari.api.gateway.IGatewayShard] The mapping of shard IDs to gateway connections for the corresponding shard. These shard IDs are 0-indexed. """ @@ -81,7 +81,7 @@ def shard_count(self) -> int: Returns ------- - int + builtins.int The number of shards in the entire application. """ @@ -122,12 +122,13 @@ async def update_presence( If defined, the new status to set. activity : hikari.models.presences.Activity or None or hikari.utilities.undefined.UndefinedType If defined, the new activity to set. - idle_since : datetime.datetime or None or hikari.utilities.undefined.UndefinedType - If defined, the time to show up as being idle since, or `None` if - not applicable. If undefined, then it is not changed. - afk : bool or hikari.utilities.undefined.UndefinedType - If defined, `True` if the user should be marked as AFK, - or `False` if not AFK. + idle_since : datetime.datetime or builtins.None or hikari.utilities.undefined.UndefinedType + If defined, the time to show up as being idle since, or + `builtins.None` if not applicable. If undefined, then it is not + changed. + afk : builtins.bool or hikari.utilities.undefined.UndefinedType + If defined, `builtins.True` if the user should be marked as AFK, + or `builtins.False` if not AFK. """ @abc.abstractmethod diff --git a/hikari/api/rest_app.py b/hikari/api/rest_app.py index 5690552fc1..1a5d88ec90 100644 --- a/hikari/api/rest_app.py +++ b/hikari/api/rest_app.py @@ -88,10 +88,10 @@ def executor(self) -> typing.Optional[concurrent.futures.Executor]: Returns ------- - concurrent.futures.Executor or None + concurrent.futures.Executor or builtins.None The custom thread-pool being used for blocking IO. If the default event loop thread-pool is being used, then this will - return `None` instead. + return `builtins.None` instead. """ @property @@ -142,9 +142,9 @@ def acquire(self, token: str, token_type: str = constants.BEARER_TOKEN) -> IREST Parameters ---------- - token : str + token : builtins.str The token to use. - token_type : str + token_type : builtins.str The token type to use. Defaults to `"Bearer"`. Returns diff --git a/hikari/api/rest_client.py b/hikari/api/rest_client.py index 945a4676aa..59d46e3cfc 100644 --- a/hikari/api/rest_client.py +++ b/hikari/api/rest_client.py @@ -119,26 +119,26 @@ async def edit_channel( channel : hikari.models.channels.PartialChannel or hikari.utilities.snowflake.UniqueObject The channel to edit. This may be a channel object, or the ID of an existing channel. - name : hikari.utilities.undefined.UndefinedType or str + name : hikari.utilities.undefined.UndefinedType or builtins.str If provided, the new name for the channel. - position : hikari.utilities.undefined.UndefinedType or int + position : hikari.utilities.undefined.UndefinedType or builtins.int If provided, the new position for the channel. - topic : hikari.utilities.undefined.UndefinedType or str + topic : hikari.utilities.undefined.UndefinedType or builtins.str If provided, the new topic for the channel. - nsfw : hikari.utilities.undefined.UndefinedType or bool + nsfw : hikari.utilities.undefined.UndefinedType or builtins.bool If provided, whether the channel should be marked as NSFW or not. - bitrate : hikari.utilities.undefined.UndefinedType or int + bitrate : hikari.utilities.undefined.UndefinedType or builtins.int If provided, the new bitrate for the channel. - user_limit : hikari.utilities.undefined.UndefinedType or int + user_limit : hikari.utilities.undefined.UndefinedType or builtins.int If provided, the new user limit in the channel. - rate_limit_per_user : hikari.utilities.undefined.UndefinedType or datetime.timedelta or float or int + rate_limit_per_user : hikari.utilities.undefined.UndefinedType or datetime.timedelta or builtins.float or builtins.int If provided, the new rate limit per user in the channel. permission_overwrites : hikari.utilities.undefined.UndefinedType or typing.Sequence[hikari.models.channels.PermissionOverwrite] If provided, the new permission overwrites for the channel. parent_category : hikari.utilities.undefined.UndefinedType or hikari.models.channels.GuildCategory or hikari.utilities.snowflake.UniqueObject If provided, the new guild category for the channel. This may be a category object, or the ID of an existing category. - reason : hikari.utilities.undefined.UndefinedType or str + reason : hikari.utilities.undefined.UndefinedType or builtins.str If provided, the reason that will be recorded in the audit logs. Returns @@ -234,19 +234,19 @@ async def edit_permission_overwrites( target : hikari.models.users.User or hikari.models.guilds.Role or hikari.models.channels.PermissionOverwrite or hikari.utilities.snowflake.UniqueObject The channel overwrite to edit. This may be a overwrite object, or the ID of an existing channel. - target_type : hikari.utilities.undefined.UndefinedType or hikari.models.channels.PermissionOverwriteType or str + target_type : hikari.utilities.undefined.UndefinedType or hikari.models.channels.PermissionOverwriteType or builtins.str If provided, the type of the target to update. If unset, will attempt to get the type from `target`. allow : hikari.utilities.undefined.UndefinedType or hikari.models.permissions.Permission If provided, the new vale of all allowed permissions. deny : hikari.utilities.undefined.UndefinedType or hikari.models.permissions.Permission If provided, the new vale of all disallowed permissions. - reason : hikari.utilities.undefined.UndefinedType or str + reason : hikari.utilities.undefined.UndefinedType or builtins.str If provided, the reason that will be recorded in the audit logs. Raises ------ - TypeError + builtins.TypeError If `target_type` is unset and we were unable to determine the type from `target`. hikari.errors.BadRequest @@ -339,20 +339,20 @@ async def create_invite( channel : hikari.models.channels.PartialChannel or hikari.utilities.snowflake.UniqueObject The channel to create a invite for. This may be a channel object, or the ID of an existing channel. - max_age : hikari.utilities.undefined.UndefinedType or datetime.timedelta or float or int + max_age : hikari.utilities.undefined.UndefinedType or datetime.timedelta or builtins.float or builtins.int If provided, the duration of the invite before expiry. - max_uses : hikari.utilities.undefined.UndefinedType or int + max_uses : hikari.utilities.undefined.UndefinedType or builtins.int If provided, the max uses the invite can have. - temporary : hikari.utilities.undefined.UndefinedType or bool + temporary : hikari.utilities.undefined.UndefinedType or builtins.bool If provided, whether the invite only grants temporary membership. - unique : hikari.utilities.undefined.UndefinedType or bool + unique : hikari.utilities.undefined.UndefinedType or builtins.bool If provided, wheter the invite should be unique. target_user : hikari.utilities.undefined.UndefinedType or hikari.models.users.User or hikari.utilities.snowflake.UniqueObject If provided, the target user id for this invite. This may be a user object, or the ID of an existing user. - target_user_type : hikari.utilities.undefined.UndefinedType or hikari.models.invites.TargetUserType or int + target_user_type : hikari.utilities.undefined.UndefinedType or hikari.models.invites.TargetUserType or builtins.int If provided, the type of target user for this invite. - reason : hikari.utilities.undefined.UndefinedType or str + reason : hikari.utilities.undefined.UndefinedType or builtins.str If provided, the reason that will be recorded in the audit logs. Returns @@ -389,7 +389,7 @@ def trigger_typing( Returns ------- - hikari.impl.special_endpoints.TypingIndicator + hikari.api.special_endpoints.TypingIndicator A typing indicator to use. Raises @@ -533,7 +533,7 @@ def fetch_messages( Raises ------ - TypeError + builtins.TypeError If you specify more than one of `before`, `after`, `about`. hikari.errors.Unauthorized If you are unauthorized to make the request (invalid/missing token). @@ -546,10 +546,10 @@ def fetch_messages( If an internal error occurs on Discord while handling the request. !!! note - The exceptions on this endpoint (other than `TypeError`) will only + The exceptions on this endpoint (other than `builtins.TypeError`) will only be raised once the result is awaited or interacted with. Invoking this function itself will not raise anything (other than - `TypeError`). + `builtins.TypeError`). """ # noqa: E501 - Line too long @abc.abstractmethod @@ -612,29 +612,29 @@ async def create_message( channel : hikari.models.channels.PartialChannel or hikari.utilities.snowflake.UniqueObject The channel to create the message in. This may be a channel object, or the ID of an existing channel. - text : hikari.utilities.undefined.UndefinedType or str + text : hikari.utilities.undefined.UndefinedType or builtins.str If specified, the message contents. embed : hikari.utilities.undefined.UndefinedType or hikari.models.embeds.Embed If specified, the message embed. - attachment : hikari.utilities.undefined.UndefinedType or str or hikari.utilities.files.Resource + attachment : hikari.utilities.undefined.UndefinedType or builtins.str or hikari.utilities.files.Resource If specified, the message attachment. This can be a resource, or string of a path on your computer or a URL. - attachments : hikari.utilities.undefined.UndefinedType or typing.Sequence[str or hikari.utilities.files.Resource] + attachments : hikari.utilities.undefined.UndefinedType or typing.Sequence[builtins.str or hikari.utilities.files.Resource] If specified, the message attachments. These can be resources, or strings consisting of paths on your computer or URLs. - tts : hikari.utilities.undefined.UndefinedType or bool + tts : hikari.utilities.undefined.UndefinedType or builtins.bool If specified, whether the message will be TTS (Text To Speech). - nonce : hikari.utilities.undefined.UndefinedType or str + nonce : hikari.utilities.undefined.UndefinedType or builtins.str If specified, a nonce that can be used for optimistic message sending. - mentions_everyone : bool + mentions_everyone : builtins.bool If specified, whether the message should parse @everyone/@here mentions. - user_mentions : typing.Collection[hikari.models.users.User or hikari.utilities.snowflake.UniqueObject] or bool - If specified, and `bool`, whether to parse user mentions. If specified and - `list`, the users to parse the mention for. This may be a user object, or - the ID of an existing user. - role_mentions : typing.Collection[hikari.models.guilds.Role or hikari.utilities.snowflake.UniqueObject] or bool - If specified and `bool`, whether to parse role mentions. If specified and - `list`, the roles to parse the mention for. This may be a role object, or + user_mentions : typing.Collection[hikari.models.users.User or hikari.utilities.snowflake.UniqueObject] or builtins.bool + If specified, and a `builtins.bool`, whether to parse user mentions. + If specified and a `builtins.list`, the users to parse the mention + for. This may be a user object, or the ID of an existing user. + role_mentions : typing.Collection[hikari.models.guilds.Role or hikari.utilities.snowflake.UniqueObject] or builtins.bool + If specified and `builtins.bool`, whether to parse role mentions. If specified and + `builtins.list`, the roles to parse the mention for. This may be a role object, or the ID of an existing role. Returns @@ -660,10 +660,10 @@ async def create_message( If the channel is not found. hikari.errors.ServerHTTPErrorResponse If an internal error occurs on Discord while handling the request. - ValueError + builtins.ValueError If more than 100 unique objects/entities are passed for `role_mentions` or `user_mentions`. - TypeError + builtins.TypeError If both `attachment` and `attachments` are specified. !!! warning @@ -1427,16 +1427,17 @@ async def estimate_guild_prune_count( guild : hikari.models.guilds.Guild or hikari.utilities.snowflake.UniqueObject The guild to estimate the guild prune count for. This may be a guild object, or the ID of an existing channel. - days : hikari.utilities.undefined.UndefinedType or int + days : hikari.utilities.undefined.UndefinedType or builtins.int If provided, number of days to count prune for. include_roles : hikari.utilities.undefined.UndefinedType or typing.Collection[hikari.models.guilds.Role or hikari.utilities.snowflake.UniqueObject] - If provided, the role(s) to include. By default, this endpoint will not count - users with roles. Providing roles using this attribute will make members with - the specified roles also get included into the count. + If provided, the role(s) to include. By default, this endpoint will + not count users with roles. Providing roles using this attribute + will make members with the specified roles also get included into + the count. Returns ------- - int + builtins.int The estimated guild prune count. Raises @@ -1472,22 +1473,24 @@ async def begin_guild_prune( guild : hikari.models.guilds.Guild or hikari.utilities.snowflake.UniqueObject The guild to begin the guild prune in. This may be a guild object, or the ID of an existing channel. - days : hikari.utilities.undefined.UndefinedType or int + days : hikari.utilities.undefined.UndefinedType or builtins.int If provided, number of days to count prune for. - compute_prune_count: hikari.utilities.undefined.UndefinedType or bool - If provided, whether to return the prune count. This is discouraged for large - guilds. + compute_prune_count: hikari.utilities.undefined.UndefinedType or builtins.bool + If provided, whether to return the prune count. This is discouraged + for large guilds. include_roles : hikari.utilities.undefined.UndefinedType or typing.Collection[hikari.models.guilds.Role or hikari.utilities.snowflake.UniqueObject] - If provided, the role(s) to include. By default, this endpoint will not count - users with roles. Providing roles using this attribute will make members with - the specified roles also get included into the count. - reason : hikari.utilities.undefined.UndefinedType or str + If provided, the role(s) to include. By default, this endpoint will + not count users with roles. Providing roles using this attribute + will make members with the specified roles also get included into + the count. + reason : hikari.utilities.undefined.UndefinedType or builtins.str If provided, the reason that will be recorded in the audit logs. Returns ------- - int or None - If `compute_prune_count` is not provided or `True`, the number of members pruned. + builtins.int or builtins.None + If `compute_prune_count` is not provided or `builtins.True`, the + number of members pruned. Raises ------ diff --git a/hikari/api/special_endpoints.py b/hikari/api/special_endpoints.py index b736dc7eaa..b69de0df45 100644 --- a/hikari/api/special_endpoints.py +++ b/hikari/api/special_endpoints.py @@ -221,19 +221,19 @@ def add_role( Parameters ---------- - name : str + name : builtins.str The role name. color : hikari.utilities.undefined.UndefinedType or hikari.models.colors.Color The colour of the role to use. If unspecified, then the default colour is used instead. colour : hikari.utilities.undefined.UndefinedType or hikari.models.colors.Color Alias for the `color` parameter for non-american users. - hoisted : hikari.utilities.undefined.UndefinedType or bool - If `True`, the role will show up in the user sidebar in a separate - category if it is the highest hoisted role. If `False`, or + hoisted : hikari.utilities.undefined.UndefinedType or builtins.bool + If `builtins.True`, the role will show up in the user sidebar in a separate + category if it is the highest hoisted role. If `builtins.False`, or unspecified, then this will not occur. - mentionable : hikari.utilities.undefined.UndefinedType or bool - If `True`, then the role will be able to be mentioned. + mentionable : hikari.utilities.undefined.UndefinedType or builtins.bool + If `builtins.True`, then the role will be able to be mentioned. permissions : hikari.utilities.undefined.UndefinedType or hikari.models.permissions.Permission The optional permissions to enforce on the role. If unspecified, the default permissions for roles will be used. @@ -242,7 +242,7 @@ def add_role( The default permissions are **NOT** the same as providing zero permissions. To set no permissions, you should pass `Permission(0)` explicitly. - position : hikari.utilities.undefined.UndefinedType or int + position : hikari.utilities.undefined.UndefinedType or builtins.int If specified, the position to place the role in. Returns @@ -256,9 +256,9 @@ def add_role( Raises ------ - ValueError + builtins.ValueError If you are defining the first role, but did not name it `@everyone`. - TypeError + builtins.TypeError If you specify both `color` and `colour` together. """ @@ -278,16 +278,16 @@ def add_category( Parameters ---------- - name : str + name : builtins.str The name of the category. - position : hikari.utilities.undefined.UndefinedType or int + position : hikari.utilities.undefined.UndefinedType or builtins.int The position to place the category in, if specified. permission_overwrites : hikari.utilities.undefined.UndefinedType or typing.Collection[hikari.models.channels.PermissionOverwrite] If defined, a collection of one or more `hikari.models.channels.PermissionOverwrite` objects. - nsfw : hikari.utilities.undefined.UndefinedType or bool - If `True`, the channel is marked as NSFW and only users over - 18 years of age should be given access. + nsfw : hikari.utilities.undefined.UndefinedType or builtins.bool + If `builtins.True`, the channel is marked as NSFW and only users + over 18 years of age should be given access. Returns ------- @@ -318,21 +318,21 @@ def add_text_channel( Parameters ---------- - name : str + name : builtins.str The name of the category. - position : hikari.utilities.undefined.UndefinedType or int + position : hikari.utilities.undefined.UndefinedType or builtins.int The position to place the category in, if specified. permission_overwrites : hikari.utilities.undefined.UndefinedType or typing.Collection[hikari.models.channels.PermissionOverwrite] If defined, a collection of one or more `hikari.models.channels.PermissionOverwrite` objects. - nsfw : hikari.utilities.undefined.UndefinedType or bool - If `True`, the channel is marked as NSFW and only users over - 18 years of age should be given access. + nsfw : hikari.utilities.undefined.UndefinedType or builtins.bool + If `builtins.True`, the channel is marked as NSFW and only users + over 18 years of age should be given access. parent_id : hikari.utilities.undefined.UndefinedType or hikari.utilities.snowflake.Snowflake If defined, should be a snowflake ID of a category channel that was made with this builder. If provided, this channel will become a child channel of that category. - topic : hikari.utilities.undefined.UndefinedType or str + topic : hikari.utilities.undefined.UndefinedType or builtins.str If specified, the topic to set on the channel. rate_limit_per_user : hikari.utilities.undefined.UndefinedType or hikari.utilities.date.TimeSpan If specified, the time to wait between allowing consecutive messages @@ -367,23 +367,23 @@ def add_voice_channel( Parameters ---------- - name : str + name : builtins.str The name of the category. - position : hikari.utilities.undefined.UndefinedType or int + position : hikari.utilities.undefined.UndefinedType or builtins.int The position to place the category in, if specified. permission_overwrites : hikari.utilities.undefined.UndefinedType or typing.Collection[hikari.models.channels.PermissionOverwrite] If defined, a collection of one or more `hikari.models.channels.PermissionOverwrite` objects. - nsfw : hikari.utilities.undefined.UndefinedType or bool - If `True`, the channel is marked as NSFW and only users over - 18 years of age should be given access. + nsfw : hikari.utilities.undefined.UndefinedType or builtins.bool + If `builtins.True`, the channel is marked as NSFW and only users + over 18 years of age should be given access. parent_id : hikari.utilities.undefined.UndefinedType or hikari.utilities.snowflake.Snowflake If defined, should be a snowflake ID of a category channel that was made with this builder. If provided, this channel will become a child channel of that category. - bitrate : hikari.utilities.undefined.UndefinedType or int + bitrate : hikari.utilities.undefined.UndefinedType or builtins.int If specified, the bitrate to set on the channel. - user_limit : hikari.utilities.undefined.UndefinedType or int + user_limit : hikari.utilities.undefined.UndefinedType or builtins.int If specified, the maximum number of users to allow in the voice channel. diff --git a/hikari/api/voice.py b/hikari/api/voice.py index bef64ee073..479732ef97 100644 --- a/hikari/api/voice.py +++ b/hikari/api/voice.py @@ -44,12 +44,12 @@ async def connect_to( ---------- channel : hikari.models.channels.GuildVoiceChannel or hikari.utilities.snowflake.UniqueObject The channel or channel ID to connect to. - deaf : bool - Defaulting to `False`, if `True`, the client will enter the voice - channel deafened (thus unable to hear other users). - mute : bool - Defaulting to `False`, if `True`, the client will enter the voice - channel muted (thus unable to send audio). + deaf : builtins.bool + Defaulting to `builtins.False`, if `builtins.True`, the client will + enter the voice channel deafened (thus unable to hear other users). + mute : builtins.bool + Defaulting to `builtins.False`, if `builtins.True`, the client will + enter the voice channel muted (thus unable to send audio). """ diff --git a/hikari/config.py b/hikari/config.py index 41a1fefb51..8b381e8a91 100644 --- a/hikari/config.py +++ b/hikari/config.py @@ -72,13 +72,13 @@ class ProxySettings: """The URL of the proxy to use.""" trust_env: bool = False - """If `True`, and no proxy info is given, then `HTTP_PROXY` and + """If `builtins.True`, and no proxy info is given, then `HTTP_PROXY` and `HTTPS_PROXY` will be used from the environment variables if present. Any proxy credentials will be read from the user's `netrc` file (https://www.gnu.org/software/inetutils/manual/html_node/The-_002enetrc-file.html) - If `False`, then this information is instead ignored. - Defaults to `False` if unspecified. + If `builtins.False`, then this information is instead ignored. + Defaults to `builtins.False` if unspecified. """ @property @@ -119,7 +119,7 @@ class HTTPSettings: """Settings to control the HTTP client.""" allow_redirects: bool = False - """If `True`, allow following redirects from `3xx` HTTP responses. + """If `builtins.True`, allow following redirects from `3xx` HTTP responses. Generally you do not want to enable this unless you have a good reason to. """ @@ -127,14 +127,14 @@ class HTTPSettings: max_redirects: int = 10 """The maximum number of redirects to allow. - If `allow_redirects` is `False`, then this is ignored. + If `allow_redirects` is `builtins.False`, then this is ignored. """ timeouts: HTTPTimeoutSettings = attr.ib(factory=HTTPTimeoutSettings) """Settings to control HTTP request timeouts.""" verify_ssl: bool = True - """If `True`, then responses with invalid SSL certificates will be + """If `builtins.True`, then responses with invalid SSL certificates will be rejected. Generally you want to keep this enabled unless you have a problem with SSL and you know exactly what you are doing by disabling this. Disabling SSL verification can have major security implications. diff --git a/hikari/errors.py b/hikari/errors.py index 500f561282..27df68155e 100644 --- a/hikari/errors.py +++ b/hikari/errors.py @@ -93,11 +93,11 @@ def __str__(self) -> str: class GatewayClientClosedError(GatewayError): - """An exception raised when you programmatically shut down the bot client-side. + """An exception raised when you programmatically shut down the bot. Parameters ---------- - reason : str + reason : builtins.str A string explaining the issue. """ @@ -112,13 +112,14 @@ class GatewayServerClosedConnectionError(GatewayError): Parameters ---------- - reason : str or None + reason : builtins.str or builtins.None A string explaining the issue. - code : int or None + code : builtins.int or builtins.None The close code. - can_reconnect : bool - If `True`, a reconnect will occur after this is raised rather than - it being propagated to the caller. If `False`, this will be raised. + can_reconnect : builtins.bool + If `builtins.True`, a reconnect will occur after this is raised rather + than it being propagated to the caller. If `builtins.False`, this will + be raised. """ __slots__: typing.Sequence[str] = ("code", "can_reconnect") @@ -137,9 +138,9 @@ class HTTPError(HikariError): Parameters ---------- - message : str + message : builtins.str The error message. - url : str + url : builtins.str The URL that produced this error. """ @@ -162,9 +163,9 @@ class HTTPErrorResponse(HTTPError): Parameters ---------- - url : str + url : builtins.str The URL that produced the error message. - status : int or http.HTTPStatus + status : builtins.int or http.HTTPStatus The HTTP status code of the response that caused this error. headers : hikari.utilities.data_binding.Headers Any headers that were given in the response. @@ -228,7 +229,7 @@ class BadRequest(ClientHTTPErrorResponse): Parameters ---------- - url : str + url : builtins.str The URL that produced the error message. headers : hikari.utilities.data_binding.Headers Any headers that were given in the response. @@ -250,7 +251,7 @@ class Unauthorized(ClientHTTPErrorResponse): Parameters ---------- - url : str + url : builtins.str The URL that produced the error message. headers : hikari.utilities.data_binding.Headers Any headers that were given in the response. @@ -274,7 +275,7 @@ class Forbidden(ClientHTTPErrorResponse): Parameters ---------- - url : str + url : builtins.str The URL that produced the error message. headers : hikari.utilities.data_binding.Headers Any headers that were given in the response. @@ -294,7 +295,7 @@ class NotFound(ClientHTTPErrorResponse): Parameters ---------- - url : str + url : builtins.str The URL that produced the error message. headers : hikari.utilities.data_binding.Headers Any headers that were given in the response. @@ -339,7 +340,7 @@ class RateLimited(ClientHTTPErrorResponse): Parameters ---------- - url : str + url : builtins.str The URL that produced the error message. route : hikari.impl.routes.CompiledRoute The route that produced this error. @@ -347,7 +348,7 @@ class RateLimited(ClientHTTPErrorResponse): Any headers that were given in the response. raw_body : typing.Any The body that was received. - retry_after : float + retry_after : builtins.float How many seconds to wait before you can reuse the route with the specific request. """ diff --git a/hikari/events/channel.py b/hikari/events/channel.py index fc938b84c7..becf52422d 100644 --- a/hikari/events/channel.py +++ b/hikari/events/channel.py @@ -94,7 +94,7 @@ class ChannelPinsUpdateEvent(base_events.Event): guild_id: typing.Optional[snowflake.Snowflake] = attr.ib(repr=True) """The ID of the guild where this event happened. - Will be `None` if this happened in a DM channel. + Will be `builtins.None` if this happened in a DM channel. """ channel_id: snowflake.Snowflake = attr.ib(repr=True) @@ -103,7 +103,7 @@ class ChannelPinsUpdateEvent(base_events.Event): last_pin_timestamp: typing.Optional[datetime.datetime] = attr.ib(repr=True) """The datetime of when the most recent message was pinned in this channel. - Will be `None` if there are no messages pinned after this change. + Will be `builtins.None` if there are no messages pinned after this change. """ @@ -142,7 +142,7 @@ class TypingStartEvent(base_events.Event): guild_id: typing.Optional[snowflake.Snowflake] = attr.ib(repr=True) """The ID of the guild this typing event is occurring in. - Will be `None` if this event is happening in a DM channel. + Will be `builtins.None` if this event is happening in a DM channel. """ user_id: snowflake.Snowflake = attr.ib(repr=True) @@ -154,7 +154,7 @@ class TypingStartEvent(base_events.Event): member: typing.Optional[guilds.Member] = attr.ib(repr=False) """The member object of the user who triggered this typing event. - Will be `None` if this was triggered in a DM. + Will be `builtins.None` if this was triggered in a DM. """ @@ -187,5 +187,5 @@ class InviteDeleteEvent(base_events.Event): guild_id: typing.Optional[snowflake.Snowflake] = attr.ib(repr=True) """The ID of the guild this invite was deleted in. - This will be `None` if this invite belonged to a DM channel. + This will be `builtins.None` if this invite belonged to a DM channel. """ diff --git a/hikari/events/message.py b/hikari/events/message.py index c9e4a944b5..8d56894459 100644 --- a/hikari/events/message.py +++ b/hikari/events/message.py @@ -107,7 +107,7 @@ class UpdatedMessageFields(snowflake.Unique): edited_timestamp: typing.Union[datetime.datetime, undefined.UndefinedType, None] = attr.ib(repr=False) """The timestamp that the message was last edited at. - Will be `None` if the message wasn't ever edited, or `undefined` if the + Will be `builtins.None` if the message wasn't ever edited, or `undefined` if the info is not available. """ @@ -203,7 +203,7 @@ class MessageDeleteEvent(base_events.Event): guild_id: typing.Optional[snowflake.Snowflake] = attr.ib(repr=True) """The ID of the guild where this message was deleted. - This will be `None` if this message was deleted in a DM channel. + This will be `builtins.None` if this message was deleted in a DM channel. """ message_id: snowflake.Snowflake = attr.ib(repr=True) @@ -228,7 +228,7 @@ class MessageDeleteBulkEvent(base_events.Event): guild_id: typing.Optional[snowflake.Snowflake] = attr.ib(repr=True) """The ID of the channel these messages have been deleted in. - This will be `None` if these messages were bulk deleted in a DM channel. + This will be `builtins.None` if these messages were bulk deleted in a DM channel. """ message_ids: typing.Set[snowflake.Snowflake] = attr.ib(repr=False) @@ -250,7 +250,7 @@ class MessageReactionEvent(base_events.Event): guild_id: typing.Optional[snowflake.Snowflake] = attr.ib(repr=True) """The ID of the guild where this reaction event is happening. - This will be `None` if this is happening in a DM channel. + This will be `builtins.None` if this is happening in a DM channel. """ @@ -266,7 +266,7 @@ class MessageReactionAddEvent(MessageReactionEvent): member: typing.Optional[guilds.Member] = attr.ib(repr=False) """The member object of the user who's adding this reaction. - This will be `None` if this is happening in a DM channel. + This will be `builtins.None` if this is happening in a DM channel. """ emoji: typing.Union[emojis.CustomEmoji, emojis.UnicodeEmoji] = attr.ib(repr=True) diff --git a/hikari/impl/bot.py b/hikari/impl/bot.py index aa6e6c6ef0..7ee34d1d28 100644 --- a/hikari/impl/bot.py +++ b/hikari/impl/bot.py @@ -66,69 +66,71 @@ class BotAppImpl(gateway_zookeeper.AbstractGatewayZookeeper, bot.IBotApp): Parameters ---------- - debug : bool - Defaulting to `False`, if `True`, then each payload sent and received + debug : builtins.bool + Defaulting to `builtins.False`, if `builtins.True`, then each payload sent and received on the gateway will be dumped to debug logs, and every HTTP API request and response will also be dumped to logs. This will provide useful debugging context at the cost of performance. Generally you do not need to enable this. - gateway_compression : bool - Defaulting to `True`, if `True`, then zlib transport compression is used - for each shard connection. If `False`, no compression is used. - gateway_version : int + gateway_compression : builtins.bool + Defaulting to `builtins.True`, if `builtins.True`, then zlib transport + compression is usedfor each shard connection. If `builtins.False`, no + compression is used. + gateway_version : builtins.int The version of the gateway to connect to. At the time of writing, only version `6` and version `7` (undocumented development release) are supported. This defaults to using v6. - http_settings : hikari.config.HTTPSettings or None + http_settings : hikari.config.HTTPSettings or builtins.None The HTTP-related settings to use. - initial_activity : hikari.models.presences.Activity or None or hikari.utilities.undefined.UndefinedType + initial_activity : hikari.models.presences.Activity or builtins.None or hikari.utilities.undefined.UndefinedType The initial activity to have on each shard. initial_activity : hikari.models.presences.Status or hikari.utilities.undefined.UndefinedType The initial status to have on each shard. - initial_idle_since : datetime.datetime or None or hikari.utilities.undefined.UndefinedType - The initial time to show as being idle since, or `None` if not idle, - for each shard. - initial_idle_since : bool or hikari.utilities.undefined.UndefinedType - If `True`, each shard will appear as being AFK on startup. If `False`, + initial_idle_since : datetime.datetime or builtins.None or hikari.utilities.undefined.UndefinedType + The initial time to show as being idle since, or `builtins.None` if not + idle, for each shard. + initial_idle_since : builtins.bool or hikari.utilities.undefined.UndefinedType + If `builtins.True`, each shard will appear as being AFK on startup. If `builtins.False`, each shard will appear as _not_ being AFK. - intents : hikari.models.intents.Intent or None - The intents to use for each shard. If `None`, then no intents are - passed. Note that on the version `7` gateway, this will cause an + intents : hikari.models.intents.Intent or builtins.None + The intents to use for each shard. If `builtins.None`, then no intents + are passed. Note that on the version `7` gateway, this will cause an immediate connection close with an error code. - large_threshold : int + large_threshold : builtins.int The number of members that need to be in a guild for the guild to be considered large. Defaults to the maximum, which is `250`. - logging_level : str or int or None - If not `None`, then this will be the logging level set if you have not - enabled logging already. In this case, it should be a valid + logging_level : builtins.str or builtins.int or builtins.None + If not `builtins.None`, then this will be the logging level set if you + have not enabled logging already. In this case, it should be a valid `logging` level that can be passed to `logging.basicConfig`. If you have already initialized logging, then this is irrelevant and this - parameter can be safely ignored. If you set this to `None`, then no - logging will initialize if you have a reason to not use any logging - or simply wish to initialize it in your own time instead. + parameter can be safely ignored. If you set this to `builtins.None`, + then no logging will initialize if you have a reason to not use any + logging or simply wish to initialize it in your own time instead. !!! note Initializing logging means already have a handler in the root logger. This is usually achieved by calling `logging.basicConfig` or adding the handler manually. - proxy_settings : hikari.config.ProxySettings or None + proxy_settings : hikari.config.ProxySettings or builtins.None Settings to use for the proxy. rest_version : int The version of the HTTP API to connect to. At the time of writing, only version `6` and version `7` (undocumented development release) are supported. This defaults to v6. - shard_ids : typing.Set[int] or None + shard_ids : typing.Set[builtins.int] or builtins.None A set of every shard ID that should be created and started on startup. - If left to `None` along with `shard_count`, then auto-sharding is used + If left to `builtins.None` along with `shard_count`, then auto-sharding + is used instead, which is the default. + shard_count : builtins.int or builtins.None + The number of shards in the entire application. If left to + `builtins.None` along with `shard_ids`, then auto-sharding is used instead, which is the default. - shard_count : int or None - The number of shards in the entire application. If left to `None` - along with `shard_ids`, then auto-sharding is used instead, which is - the default. - stateless : bool - If `True`, the bot will not implement a cache, and will be considered - stateless. If `False`, then a cache will be used (this is the default). - token : str + stateless : builtins.bool + If `builtins.True`, the bot will not implement a cache, and will be + considered stateless. If `builtins.False`, then a cache will be used + (this is the default). + token : builtins.str The bot token to use. This should not start with a prefix such as `Bot `, but instead only contain the token itself. @@ -138,12 +140,12 @@ class BotAppImpl(gateway_zookeeper.AbstractGatewayZookeeper, bot.IBotApp): application will use the Discord-provided recommendation for the number of shards to start. - If only one of these two parameters are specified, expect a `TypeError` - to be raised. + If only one of these two parameters are specified, expect a + `builtins.TypeError` to be raised. Likewise, all shard_ids must be greater-than or equal-to `0`, and less than `shard_count` to be valid. Failing to provide valid - values will result in a `ValueError` being raised. + values will result in a `builtins.ValueError` being raised. !!! note If all four of `initial_activity`, `initial_idle_since`, @@ -153,9 +155,9 @@ class BotAppImpl(gateway_zookeeper.AbstractGatewayZookeeper, bot.IBotApp): Raises ------ - TypeError + builtins.TypeError If sharding information is not specified correctly. - ValueError + builtins.ValueError If sharding information is provided, but is unfeasible or invalid. """ diff --git a/hikari/impl/buckets.py b/hikari/impl/buckets.py index 8f7ac4865f..1aef5492b6 100644 --- a/hikari/impl/buckets.py +++ b/hikari/impl/buckets.py @@ -58,7 +58,7 @@ Rate limits, on the other hand, apply to a bucket and are specific to the major parameters of the compiled route. This means that `POST /channels/123/messages` and `POST /channels/456/messages` do not share the same real bucket, despite -Discord providing the same bucket hash. A real bucket hash is the `str` hash of +Discord providing the same bucket hash. A real bucket hash is the `builtins.str` hash of the bucket that Discord sends us in a response concatenated to the corresponding major parameters. This is used for quick bucket indexing internally in this module. @@ -124,15 +124,15 @@ the response date on the server. This should be parsed to a `datetime.datetime` using `email.utils.parsedate_to_datetime`. * `X-RateLimit-Limit`: - an `int` describing the max requests in the bucket from empty to being rate + an `builtins.int` describing the max requests in the bucket from empty to being rate limited. * `X-RateLimit-Remaining`: - an `int` describing the remaining number of requests before rate limiting + an `builtins.int` describing the remaining number of requests before rate limiting occurs in the current window. * `X-RateLimit-Bucket`: - a `str` containing the initial bucket hash. + a `builtins.str` containing the initial bucket hash. * `X-RateLimit-Reset`: - a `float` containing the number of seconds since + a `builtins.float` containing the number of seconds since 1st January 1970 at 0:00:00 UTC at which the current ratelimit window resets. This should be parsed to a `datetime.datetime` using `datetime.datetime.fromtimestamp`, passing `datetime.timezone.utc` @@ -171,7 +171,7 @@ No information is sent in headers about these specific limits. You will only be made aware that they exist once you get ratelimited. In the 429 ratelimited -response, you will have the `"global"` attribute set to `false`, and a +response, you will have the `"global"` attribute set to `builtins.False`, and a `"reset_after"` attribute that differs entirely to the `X-RateLimit-Reset` header. Thus, it is important to not assume the value in the 429 response for the reset time is the same as the one in the bucket headers. Hikari's @@ -255,7 +255,7 @@ def __init__(self, name: str, compiled_route: routes.CompiledRoute) -> None: @property def is_unknown(self) -> bool: - """Return `True` if the bucket represents an `UNKNOWN` bucket.""" + """Return `builtins.True` if the bucket represents an `UNKNOWN` bucket.""" return self.name.startswith(UNKNOWN_HASH) def acquire(self) -> asyncio.Future[None]: @@ -267,7 +267,7 @@ def acquire(self) -> asyncio.Future[None]: Returns ------- - asyncio.Future + asyncio.Future[builtins.None] A future that should be awaited immediately. Once the future completes, you are allowed to proceed with your operation. """ @@ -278,11 +278,11 @@ def update_rate_limit(self, remaining: int, limit: int, reset_at: float) -> None Parameters ---------- - remaining : int + remaining : builtins.int The calls remaining in this time window. - limit : int + limit : builtins.int The total calls allowed in this time window. - reset_at : float + reset_at : builtins.float The epoch at which to reset the limit. !!! note @@ -364,13 +364,13 @@ def start(self, poll_period: float = _POLL_PERIOD, expire_after: float = _EXPIRE Parameters ---------- - poll_period : float + poll_period : builtins.float Period to poll the garbage collector at in seconds. Defaults to `20` seconds. - expire_after : float + expire_after : builtins.float Time after which the last `reset_at` was hit for a bucket to remove it. Higher values will retain unneeded ratelimit info for - longer, but may produce more effective ratelimiting logic as a + longer, but may produce more effective rate-limiting logic as a result. Using `0` will make the bucket get garbage collected as soon as the rate limit has reset. Defaults to `10` seconds. """ @@ -401,9 +401,9 @@ async def gc(self, poll_period: float, expire_after: float) -> None: # noqa: D4 Parameters ---------- - poll_period : float + poll_period : builtins.float The period to poll at. - expire_after : float + expire_after : builtins.float Time after which the last `reset_at` was hit for a bucket to remove it. Higher values will retain unneeded ratelimit info for longer, but may produce more effective ratelimiting logic as a @@ -438,7 +438,7 @@ def do_gc_pass(self, expire_after: float) -> None: Parameters ---------- - expire_after : float + expire_after : builtins.float Time after which the last `reset_at` was hit for a bucket to remove it. Defaults to `reset_at` + 20 seconds. Higher values will retain unneeded ratelimit info for longer, but may produce more @@ -491,7 +491,7 @@ def acquire(self, compiled_route: routes.CompiledRoute) -> asyncio.Future[None]: Returns ------- - asyncio.Future + asyncio.Future[builtins.None] A future to await that completes when you are allowed to run your request logic. @@ -538,12 +538,12 @@ def update_rate_limits( ---------- compiled_route : hikari.impl.routes.CompiledRoute The compiled _route to get the bucket for. - bucket_header : str, optional + bucket_header : builtins.str or builtins.None The `X-RateLimit-Bucket` header that was provided in the response. - remaining_header : int - The `X-RateLimit-Remaining` header cast to an `int`. - limit_header : int - The `X-RateLimit-Limit`header cast to an `int`. + remaining_header : builtins.int + The `X-RateLimit-Remaining` header cast to an `builtins.int`. + limit_header : builtins.int + The `X-RateLimit-Limit`header cast to an `builtins.int`. date_header : datetime.datetime The `Date` header value as a `datetime.datetime`. reset_at_header : datetime.datetime @@ -582,5 +582,5 @@ def update_rate_limits( @property def is_started(self) -> bool: - """Return `True` if the rate limiter GC task is started.""" + """Return `builtins.True` if the rate limiter GC task is started.""" return self.gc_task is not None diff --git a/hikari/impl/gateway.py b/hikari/impl/gateway.py index 74d3cab6de..c5f51bf350 100644 --- a/hikari/impl/gateway.py +++ b/hikari/impl/gateway.py @@ -59,41 +59,43 @@ class GatewayShardImpl(gateway.IGatewayShard): ---------- app : hikari.api.event_consumer.IEventConsumerApp The base application. - debug : bool - If `True`, each sent and received payload is dumped to the logs. If - `False`, only the fact that data has been sent/received will be logged. + debug : builtins.bool + If `builtins.True`, each sent and received payload is dumped to the + logs. If `builtins.False`, only the fact that data has been + sent/received will be logged. http_settings : hikari.config.HTTPSettings The HTTP-related settings to use while negotiating a websocket. - initial_activity : hikari.models.presences.Activity or None or hikari.utilities.undefined.UndefinedType + initial_activity : hikari.models.presences.Activity or builtins.None or hikari.utilities.undefined.UndefinedType The initial activity to appear to have for this shard. - initial_idle_since : datetime.datetime or None or hikari.utilities.undefined.UndefinedType + initial_idle_since : datetime.datetime or builtins.None or hikari.utilities.undefined.UndefinedType The datetime to appear to be idle since. - initial_is_afk : bool or hikari.utilities.undefined.UndefinedType + initial_is_afk : builtins.bool or hikari.utilities.undefined.UndefinedType Whether to appear to be AFK or not on login. initial_status : hikari.models.presences.Status or hikari.utilities.undefined.UndefinedType The initial status to set on login for the shard. - intents : hikari.models.intents.Intent or None - Collection of intents to use, or `None` to not use intents at all. - large_threshold : int + intents : hikari.models.intents.Intent or builtins.None + Collection of intents to use, or `builtins.None` to not use intents at + all. + large_threshold : builtins.int The number of members to have in a guild for it to be considered large. proxy_settings : hikari.config.ProxySettings The proxy settings to use while negotiating a websocket. - shard_id : int + shard_id : builtins.int The shard ID. - shard_count : int + shard_count : builtins.int The shard count. - token : str + token : builtins.str The bot token to use. - url : str + url : builtins.str The gateway URL to use. This should not contain a query-string or fragments. - use_compression : bool - If `True`, then transport compression is enabled. - use_etf : bool - If `True`, ETF is used to receive payloads instead of JSON. Defaults to - `False`. Currently, setting this to `True` will raise a - `NotImplementedError`. - version : int + use_compression : builtins.bool + If `builtins.True`, then transport compression is enabled. + use_etf : builtins.bool + If `builtins.True`, ETF is used to receive payloads instead of JSON. + Defaults to `builtins.False`. Currently, setting this to `builtins.True` + will raise a `builtins.NotImplementedError`. + version : builtins.int Gateway API version to use. !!! note @@ -301,7 +303,7 @@ async def _run(self) -> None: self._handshake_event.set() async def _run_once_shielded(self, client_session: aiohttp.ClientSession) -> bool: - # Returns `True` if we can reconnect, or `False` otherwise. + # Returns `builtins.True` if we can reconnect, or `builtins.False` otherwise. # Wraps the runner logic in the standard exception handling mechanisms. try: await self._run_once(client_session) diff --git a/hikari/impl/gateway_zookeeper.py b/hikari/impl/gateway_zookeeper.py index fbfa14f2a3..e6357201f8 100644 --- a/hikari/impl/gateway_zookeeper.py +++ b/hikari/impl/gateway_zookeeper.py @@ -62,47 +62,48 @@ class AbstractGatewayZookeeper(gateway_zookeeper.IGatewayZookeeperApp, abc.ABC): Parameters ---------- - compression : bool - Defaulting to `True`, if `True`, then zlib transport compression is used - for each shard connection. If `False`, no compression is used. - debug : bool - Defaulting to `False`, if `True`, then each payload sent and received - on the gateway will be dumped to debug logs. This will provide useful - debugging context at the cost of performance. Generally you do not - need to enable this. + compression : builtins.bool + Defaulting to `builtins.True`, if `builtins.True`, then zlib transport + compression is used for each shard connection. If `builtins.False`, no + compression is used. + debug : builtins.bool + Defaulting to `builtins.False`, if `builtins.True`, then each payload + sent and received on the gateway will be dumped to debug logs. This + will provide useful debugging context at the cost of performance. + Generally you do not need to enable this. http_settings : hikari.config.HTTPSettings HTTP-related configuration. - initial_activity : hikari.models.presences.Activity or None or hikari.utilities.undefined.UndefinedType + initial_activity : hikari.models.presences.Activity or builtins.None or hikari.utilities.undefined.UndefinedType The initial activity to have on each shard. initial_activity : hikari.models.presences.Status or hikari.utilities.undefined.UndefinedType The initial status to have on each shard. - initial_idle_since : datetime.datetime or None or hikari.utilities.undefined.UndefinedType - The initial time to show as being idle since, or `None` if not idle, - for each shard. - initial_idle_since : bool or hikari.utilities.undefined.UndefinedType - If `True`, each shard will appear as being AFK on startup. If `False`, - each shard will appear as _not_ being AFK. - intents : hikari.models.intents.Intent or None - The intents to use for each shard. If `None`, then no intents are - passed. Note that on the version `7` gateway, this will cause an + initial_idle_since : datetime.datetime or builtins.None or hikari.utilities.undefined.UndefinedType + The initial time to show as being idle since, or `builtins.None` if not + idle, for each shard. + initial_idle_since : builtins.bool or hikari.utilities.undefined.UndefinedType + If `builtins.True`, each shard will appear as being AFK on startup. If + `builtins.False`, each shard will appear as _not_ being AFK. + intents : hikari.models.intents.Intent or builtins.None + The intents to use for each shard. If `builtins.None`, then no intents + are passed. Note that on the version `7` gateway, this will cause an immediate connection close with an error code. - large_threshold : int + large_threshold : builtins.int The number of members that need to be in a guild for the guild to be considered large. Defaults to the maximum, which is `250`. proxy_settings : hikari.config.ProxySettings Proxy-related configuration. - shard_ids : typing.Set[int] or None + shard_ids : typing.Set[builtins.int] or builtins.None A set of every shard ID that should be created and started on startup. - If left to `None` along with `shard_count`, then auto-sharding is used + If left to `builtins.None` along with `shard_count`, then auto-sharding + is used instead, which is the default. + shard_count : builtins.int or builtins.None + The number of shards in the entire application. If left to + `builtins.None` along with `shard_ids`, then auto-sharding is used instead, which is the default. - shard_count : int or None - The number of shards in the entire application. If left to `None` - along with `shard_ids`, then auto-sharding is used instead, which is - the default. - token : str + token : builtins.str The bot token to use. This should not start with a prefix such as `Bot `, but instead only contain the token itself. - version : int + version : builtins.int The version of the gateway to connect to. At the time of writing, only version `6` and version `7` (undocumented development release) are supported. This defaults to using v6. @@ -113,12 +114,12 @@ class AbstractGatewayZookeeper(gateway_zookeeper.IGatewayZookeeperApp, abc.ABC): application will use the Discord-provided recommendation for the number of shards to start. - If only one of these two parameters are specified, expect a `TypeError` - to be raised. + If only one of these two parameters are specified, expect a + `builtins.TypeError` to be raised. Likewise, all shard_ids must be greater-than or equal-to `0`, and less than `shard_count` to be valid. Failing to provide valid - values will result in a `ValueError` being raised. + values will result in a `builtins.ValueError` being raised. !!! note If all four of `initial_activity`, `initial_idle_since`, @@ -128,9 +129,9 @@ class AbstractGatewayZookeeper(gateway_zookeeper.IGatewayZookeeperApp, abc.ABC): Raises ------ - ValueError + builtins.ValueError If sharding information is provided, but is unfeasible or invalid. - TypeError + builtins.TypeError If sharding information is not specified correctly. """ diff --git a/hikari/impl/rate_limits.py b/hikari/impl/rate_limits.py index 2b3bad8260..bacaea5003 100644 --- a/hikari/impl/rate_limits.py +++ b/hikari/impl/rate_limits.py @@ -60,7 +60,7 @@ def acquire(self) -> asyncio.Future[None]: Returns ------- - asyncio.Future + asyncio.Future[builtins.None] A future that should be awaited. Once the future is complete, you can proceed to execute your rate-limited task. """ @@ -89,7 +89,7 @@ class BurstRateLimiter(BaseRateLimiter, abc.ABC): """The name of the rate limiter.""" throttle_task: typing.Optional[asyncio.Task[typing.Any]] - """The throttling task, or `None` if it isn't running.""" + """The throttling task, or `builtins.None` if it isn't running.""" queue: typing.Final[typing.List[asyncio.Future[typing.Any]]] """The queue of any futures under a rate limit.""" @@ -108,7 +108,7 @@ def acquire(self) -> asyncio.Future[typing.Any]: Returns ------- - asyncio.Future + asyncio.Future[typing.Any] A future that should be immediately awaited. Once the await completes, you are able to proceed with the operation that is under this rate limit. @@ -140,7 +140,7 @@ def close(self) -> None: @property def is_empty(self) -> bool: - """Return `True` if no futures are on the queue being rate limited.""" + """Return `builtins.True` if no futures are on the queue being rate limited.""" return len(self.queue) == 0 @@ -175,7 +175,7 @@ def acquire(self) -> asyncio.Future[typing.Any]: Returns ------- - asyncio.Future + asyncio.Future[typing.Any] A future that should be immediately awaited. Once the await completes, you are able to proceed with the operation that is under this rate limit. @@ -196,18 +196,18 @@ def throttle(self, retry_after: float) -> None: Parameters ---------- - retry_after : float + retry_after : builtins.float How long to sleep for before unlocking and releasing any futures in the queue. !!! note - This will invoke `ManualRateLimiter.unlock_later` as a scheduled task - in the future (it will not await it to finish). + This will invoke `ManualRateLimiter.unlock_later` as a scheduled + task in the future (it will not await it to finish). When the `ManualRateLimiter.unlock_later` coroutine function completes, it should be expected to set the `throttle_task` to - `None`. This means you can check if throttling is occurring by - checking if `throttle_task` is not `None`. + `builtins.None`. This means you can check if throttling is occurring + by checking if `throttle_task` is not `builtins.None`. If this is invoked while another throttle is in progress, that one is cancelled and a new one is started. This enables new rate limits @@ -224,7 +224,7 @@ async def unlock_later(self, retry_after: float) -> None: Parameters ---------- - retry_after : float + retry_after : builtins.float How long to sleep for before unlocking and releasing any futures in the queue. @@ -234,8 +234,8 @@ async def unlock_later(self, retry_after: float) -> None: When the `ManualRateLimiter.unlock_later` coroutine function completes, it should be expected to set the `throttle_task` to - `None`. This means you can check if throttling is occurring by - checking if `throttle_task` is not `None`. + `builtins.None`. This means you can check if throttling is occurring + by checking if `throttle_task` is not `builtins.None`. """ _LOGGER.warning("you are being globally rate limited for %ss", retry_after) await asyncio.sleep(retry_after) @@ -304,7 +304,7 @@ def acquire(self) -> asyncio.Future[typing.Any]: Returns ------- - asyncio.Future + asyncio.Future[typing.Any] A future that should be immediately awaited. Once the await completes, you are able to proceed with the operation that is under this rate limit. @@ -331,7 +331,7 @@ def get_time_until_reset(self, now: float) -> float: Parameters ---------- - now : float + now : builtins.float The monotonic `time.perf_counter` timestamp. !!! warning @@ -343,7 +343,7 @@ def get_time_until_reset(self, now: float) -> float: Returns ------- - float + builtins.float The time left to sleep before the rate limit is reset. If no rate limit is in effect, then this will return `0.0` instead. """ @@ -356,13 +356,14 @@ def is_rate_limited(self, now: float) -> bool: Parameters ---------- - now : float + now : builtins.float The monotonic `time.perf_counter` timestamp. Returns ------- - bool - `True` if we are being rate limited. `False` if we are not. + builtins.bool + `builtins.True` if we are being rate limited, or `builtins.False` if + we are not. !!! warning Invoking this method will update the internal state if we were @@ -394,8 +395,8 @@ async def throttle(self) -> None: task immediately in `throttle_task`. When this coroutine function completes, it will set the - `throttle_task` to `None`. This means you can check if throttling - is occurring by checking if `throttle_task` is not `None`. + `throttle_task` to `builtins.None`. This means you can check if throttling + is occurring by checking if `throttle_task` is not `builtins.None`. """ _LOGGER.debug( "you are being rate limited on bucket %s, backing off for %ss", @@ -429,16 +430,16 @@ class ExponentialBackOff: Parameters ---------- - base : float + base : builtins.float The base to use. Defaults to `2`. - maximum : float or None - If not `None`, then this is the max value the backoff can be in a - single iteration before an `asyncio.TimeoutError` is raised. + maximum : builtins.float or builtins.None + If not `builtins.None`, then this is the max value the backoff can be + in a single iteration before an `asyncio.TimeoutError` is raised. Defaults to `64` seconds. - jitter_multiplier : float + jitter_multiplier : builtins.float The multiplier for the random jitter. Defaults to `1`. Set to `0` to disable jitter. - initial_increment : int + initial_increment : builtins.int The initial increment to start at. Defaults to `0`. """ @@ -451,7 +452,7 @@ class ExponentialBackOff: """The current increment.""" maximum: typing.Optional[float] - """If not `None`, then this is the max value the backoff can be in a + """If not `builtins.None`, then this is the max value the backoff can be in a single iteration before an `asyncio.TimeoutError` is raised. """ diff --git a/hikari/impl/rest_app.py b/hikari/impl/rest_app.py index 54549e9d74..c6e6b89eba 100644 --- a/hikari/impl/rest_app.py +++ b/hikari/impl/rest_app.py @@ -51,11 +51,11 @@ class RESTClientImpl(rest_app.IRESTAppContextManager): Parameters ---------- - debug : bool - Defaulting to `False`, if `True`, then each payload sent and received - in HTTP requests will be dumped to debug logs. This will provide useful - debugging context at the cost of performance. Generally you do not - need to enable this. + debug : builtins.bool + Defaulting to `builtins.False`, if `builtins.True`, then each payload + sent and received in HTTP requests will be dumped to debug logs. This + will provide useful debugging context at the cost of performance. + Generally you do not need to enable this. connector : aiohttp.BaseConnector The AIOHTTP connector to use. This must be closed by the caller, and will not be terminated when this class closes (since you will generally @@ -66,16 +66,16 @@ class RESTClientImpl(rest_app.IRESTAppContextManager): HTTP-related settings. proxy_settings : hikari.config.ProxySettings Proxy-related settings. - token : str or None + token : builtins.str or builtins.None If defined, the token to use. If not defined, no token will be injected into the `Authorization` header for requests. - token_type : str or None + token_type : builtins.str or builtins.None The token type to use. If undefined, a default is used instead, which will be `Bot`. If no `token` is provided, this is ignored. - url : str or None + url : builtins.str or builtins.None The API URL to hit. Generally you can leave this undefined and use the default. - version : int + version : builtins.int The API version to use. This is interpolated into the default `url` to create the full URL. Currently this only supports `6` or `7`. """ @@ -118,7 +118,7 @@ def cache(self) -> cache_.ICacheComponent: """Return the cache component. !!! warn - This will always return `NotImplemented` for HTTP-only applications. + This will always return `builtins.NotImplemented` for HTTP-only applications. """ return self._cache @@ -166,14 +166,14 @@ class RESTAppFactoryImpl(rest_app.IRESTAppFactory): Parameters ---------- - debug : bool - If `True`, then much more information is logged each time a request is - made. Generally you do not need this to be on, so it will default to - `False` instead. + debug : builtins.bool + If `builtins.True`, then much more information is logged each time a + request is made. Generally you do not need this to be on, so it will + default to `builtins.False` instead. url : str or hikari.utilities.undefined.UndefinedType The base URL for the API. You can generally leave this as being `undefined` and the correct default API base URL will be generated. - version : int + version : builtins.int The Discord API version to use. Can be `6` (stable, default), or `7` (undocumented development release). """ diff --git a/hikari/impl/rest_client.py b/hikari/impl/rest_client.py index e01b43080c..5972e162a8 100644 --- a/hikari/impl/rest_client.py +++ b/hikari/impl/rest_client.py @@ -82,24 +82,24 @@ class RESTClientImpl(rest_client.IRESTClient): app : hikari.api.rest_app.IRESTApp The HTTP application containing all other application components that Hikari uses. - debug : bool - If `True`, this will enable logging of each payload sent and received, - as well as information such as DNS cache hits and misses, and other - information useful for debugging this application. These logs will + debug : builtins.bool + If `builtins.True`, this will enable logging of each payload sent and + received, as well as information such as DNS cache hits and misses, and + other information useful for debugging this application. These logs will be written as DEBUG log entries. For most purposes, this should be - left `False`. + left `builtins.False`. global_ratelimit : hikari.impl.rate_limits.ManualRateLimiter The shared ratelimiter to use for the application. - token : str or hikari.utilities.undefined.UndefinedType + token : builtins.str or hikari.utilities.undefined.UndefinedType The bot or bearer token. If no token is to be used, this can be undefined. - token_type : str or hikari.utilities.undefined.UndefinedType + token_type : builtins.str or hikari.utilities.undefined.UndefinedType The type of token in use. If no token is used, this can be ignored and left to the default value. This can be `"Bot"` or `"Bearer"`. - rest_url : str + rest_url : builtins.str The HTTP API base URL. This can contain format-string specifiers to interpolate information such as API version in use. - version : int + version : builtins.int The API version to use. Currently only supports `6` and `7`. !!! warning diff --git a/hikari/impl/routes.py b/hikari/impl/routes.py index 2a6c202760..45a289fce5 100644 --- a/hikari/impl/routes.py +++ b/hikari/impl/routes.py @@ -62,12 +62,12 @@ def create_url(self, base_url: str) -> str: Parameters ---------- - base_url : str + base_url : builtins.str The base of the URL to prepend to the compiled path. Returns ------- - str + builtins.str The full URL for the route. """ return base_url + self.compiled_path @@ -80,13 +80,13 @@ def create_real_bucket_hash(self, initial_bucket_hash: str) -> str: Parameters ---------- - initial_bucket_hash : str + initial_bucket_hash : builtins.str The initial bucket hash provided by Discord in the HTTP headers for a given response. Returns ------- - str + builtins.str The input hash amalgamated with a hash code produced by the major parameters in this compiled route instance. """ @@ -106,9 +106,9 @@ class Route: Parameters ---------- - method : str + method : builtins.str The HTTP method - path_template : str + path_template : builtins.str The template string for the path to use. """ diff --git a/hikari/impl/stateless_cache.py b/hikari/impl/stateless_cache.py index 0afc5b1f75..aea2656529 100644 --- a/hikari/impl/stateless_cache.py +++ b/hikari/impl/stateless_cache.py @@ -47,7 +47,7 @@ def _generate(): "__doc__": ( "A stateless cache implementation that implements dummy operations for " "each of the required attributes of a functional cache implementation. " - "Any descriptors will always return `NotImplemented`, and any methods " + "Any descriptors will always return `builtins.NotImplemented`, and any methods " "will always raise `hikari.errors.HikariError` when being invoked." ), "__init__": lambda *_, **__: None, diff --git a/hikari/models/applications.py b/hikari/models/applications.py index 9f63ea8266..05fa1f6576 100644 --- a/hikari/models/applications.py +++ b/hikari/models/applications.py @@ -214,19 +214,19 @@ class OwnConnection: """The type of service this connection is for.""" is_revoked: bool = attr.ib(eq=False, hash=False, repr=False) - """`True` if the connection has been revoked.""" + """`builtins.True` if the connection has been revoked.""" integrations: typing.Sequence[guilds.PartialIntegration] = attr.ib(eq=False, hash=False, repr=False) """A sequence of the partial guild integration objects this connection has.""" is_verified: bool = attr.ib(eq=False, hash=False, repr=False) - """`True` if the connection has been verified.""" + """`builtins.True` if the connection has been verified.""" is_friend_sync_enabled: bool = attr.ib(eq=False, hash=False, repr=False) - """`True` if friends should be added based on this connection.""" + """`builtins.True` if friends should be added based on this connection.""" is_activity_visible: bool = attr.ib(eq=False, hash=False, repr=False) - """`True` if this connection's activities are shown in the user's presence.""" + """`builtins.True` if this connection's activities are shown in the user's presence.""" visibility: ConnectionVisibility = attr.ib(eq=False, hash=False, repr=True) """The visibility of the connection.""" @@ -237,7 +237,7 @@ class OwnGuild(guilds.PartialGuild): """Represents a user bound partial guild object.""" is_owner: bool = attr.ib(eq=False, hash=False, repr=True) - """`True` when the current user owns this guild.""" + """`builtins.True` when the current user owns this guild.""" my_permissions: permissions_.Permission = attr.ib(eq=False, hash=False, repr=False) """The guild-level permissions that apply to the current user or bot.""" @@ -270,7 +270,7 @@ class TeamMember: permissions: typing.Set[str] = attr.ib(eq=False, hash=False, repr=False) """This member's permissions within a team. - At the time of writing, this will always be a set of one `str`, which + At the time of writing, this will always be a set of one `builtins.str`, which will always be `"*"`. This may change in the future, however. """ @@ -299,7 +299,7 @@ class Team(snowflake.Unique): icon_hash: typing.Optional[str] = attr.ib(eq=False, hash=False, repr=False) """The CDN hash of this team's icon. - If no icon is provided, this will be `None`. + If no icon is provided, this will be `builtins.None`. """ members: typing.Mapping[snowflake.Snowflake, TeamMember] = attr.ib(eq=False, hash=False, repr=False) @@ -321,8 +321,8 @@ def icon_url(self) -> typing.Optional[files.URL]: Returns ------- - hikari.utilities.files.URL or None - The URL, or `None` if no icon exists. + hikari.utilities.files.URL or builtins.None + The URL, or `builtins.None` if no icon exists. """ return self.format_icon() @@ -331,21 +331,21 @@ def format_icon(self, *, format_: str = "png", size: int = 4096) -> typing.Optio Parameters ---------- - format_ : str + format_ : builtins.str The format to use for this URL, defaults to `png`. Supports `png`, `jpeg`, `jpg` and `webp`. - size : int + size : builtins.int The size to set for the URL, defaults to `4096`. Can be any power of two between `16` and `4096` inclusive. Returns ------- - hikari.utilities.files.URL or None - The URL, or `None` if no icon exists. + hikari.utilities.files.URL or builtins.None + The URL, or `builtins.None` if no icon exists. Raises ------ - ValueError + builtins.ValueError If the size is not an integer power of 2 between 16 and 4096 (inclusive). """ @@ -375,21 +375,21 @@ class Application(snowflake.Unique): """The description of this application, or an empty string if undefined.""" is_bot_public: typing.Optional[bool] = attr.ib(eq=False, hash=False, repr=True) - """`True` if the bot associated with this application is public. + """`builtins.True` if the bot associated with this application is public. - Will be `None` if this application doesn't have an associated bot. + Will be `builtins.None` if this application doesn't have an associated bot. """ is_bot_code_grant_required: typing.Optional[bool] = attr.ib(eq=False, hash=False, repr=False) - """`True` if this application's bot is requiring code grant for invites. + """`builtins.True` if this application's bot is requiring code grant for invites. - Will be `None` if this application doesn't have a bot. + Will be `builtins.None` if this application doesn't have a bot. """ owner: typing.Optional[users.User] = attr.ib(eq=False, hash=False, repr=True) """The application's owner. - This should always be `None` in application objects retrieved outside + This should always be `builtins.None` in application objects retrieved outside Discord's oauth2 flow. """ @@ -411,7 +411,7 @@ class Application(snowflake.Unique): team: typing.Optional[Team] = attr.ib(eq=False, hash=False, repr=False) """The team this application belongs to. - If the application is not part of a team, this will be `None`. + If the application is not part of a team, this will be `builtins.None`. """ guild_id: typing.Optional[snowflake.Snowflake] = attr.ib(eq=False, hash=False, repr=False) @@ -438,8 +438,8 @@ def icon(self) -> typing.Optional[files.URL]: Returns ------- - hikari.utilities.files.URL or None - The URL, or `None` if no icon exists. + hikari.utilities.files.URL or builtins.None + The URL, or `builtins.None` if no icon exists. """ return self.format_icon() @@ -448,21 +448,21 @@ def format_icon(self, *, format_: str = "png", size: int = 4096) -> typing.Optio Parameters ---------- - format_ : str + format_ : builtins.str The format to use for this URL, defaults to `png`. Supports `png`, `jpeg`, `jpg` and `webp`. - size : int + size : builtins.int The size to set for the URL, defaults to `4096`. Can be any power of two between 16 and 4096. Returns ------- - hikari.utilities.files.URL or None - The URL, or `None` if no icon exists. + hikari.utilities.files.URL or builtins.None + The URL, or `builtins.None` if no icon exists. Raises ------ - ValueError + builtins.ValueError If the size is not an integer power of 2 between 16 and 4096 (inclusive). """ @@ -477,8 +477,8 @@ def cover_image(self) -> typing.Optional[files.URL]: Returns ------- - hikari.utilities.files.URL or None - The URL, or `None` if no cover image exists. + hikari.utilities.files.URL or builtins.None + The URL, or `builtins.None` if no cover image exists. """ return self.format_cover_image() @@ -487,21 +487,21 @@ def format_cover_image(self, *, format_: str = "png", size: int = 4096) -> typin Parameters ---------- - format_ : str + format_ : builtins.str The format to use for this URL, defaults to `png`. Supports `png`, `jpeg`, `jpg` and `webp`. - size : int + size : builtins.int The size to set for the URL, defaults to `4096`. Can be any power of two between 16 and 4096. Returns ------- - hikari.utilities.files.URL or None - The URL, or `None` if no cover image exists. + hikari.utilities.files.URL or builtins.None + The URL, or `builtins.None` if no cover image exists. Raises ------ - ValueError + builtins.ValueError If the size is not an integer power of 2 between 16 and 4096 (inclusive). """ diff --git a/hikari/models/channels.py b/hikari/models/channels.py index 6db727ac7f..915e7a8952 100644 --- a/hikari/models/channels.py +++ b/hikari/models/channels.py @@ -217,10 +217,10 @@ async def send( and can usually be ignored. tts : bool or hikari.utilities.undefined.UndefinedType If specified, whether the message will be sent as a TTS message. - attachment : hikari.utilities.files.Resource or str or hikari.utilities.undefined.UndefinedType + attachment : hikari.utilities.files.Resource or builtins.str or hikari.utilities.undefined.UndefinedType If specified, a attachment to upload, if desired. This can be a resource, or string of a path on your computer or a URL. - attachments : typing.Sequence[hikari.utilities.files.Resource or str] or hikari.utilities.undefined.UndefinedType + attachments : typing.Sequence[hikari.utilities.files.Resource or builtins.str] or hikari.utilities.undefined.UndefinedType If specified, a sequence of attachments to upload, if desired. Should be between 1 and 10 objects in size (inclusive), also including embed attachments. These can be resources, or @@ -229,15 +229,16 @@ async def send( If specified, the embed object to send with the message. mentions_everyone : bool Whether `@everyone` and `@here` mentions should be resolved by - discord and lead to actual pings, defaults to `False`. - user_mentions : typing.Collection[hikari.models.users.User or hikari.utilities.snowflake.UniqueObject] or bool + discord and lead to actual pings, defaults to `builtins.False`. + user_mentions : typing.Collection[hikari.models.users.User or hikari.utilities.snowflake.UniqueObject] or builtins.bool Either an array of user objects/IDs to allow mentions for, - `True` to allow all user mentions or `False` to block all - user mentions from resolving, defaults to `True`. - role_mentions: typing.Collection[hikari.models.guilds.Role or hikari.utilities.snowflake.UniqueObject] or bool + `builtins.True` to allow all user mentions or `builtins.False` + to block all user mentions from resolving, defaults to + `builtins.True`. + role_mentions: typing.Collection[hikari.models.guilds.Role or hikari.utilities.snowflake.UniqueObject] or builtins.bool Either an array of guild role objects/IDs to allow mentions for, - `True` to allow all role mentions or `False` to block all - role mentions from resolving, defaults to `True`. + `builtins.True` to allow all role mentions or `builtins.False` to + block all role mentions from resolving, defaults to `builtins.True`. Returns ------- @@ -260,10 +261,10 @@ async def send( hikari.errors.Forbidden If you lack permissions to send to the channel this message belongs to. - ValueError + builtins.ValueError If more than 100 unique objects/entities are passed for `role_mentions` or `user_mentions`. - TypeError + builtins.TypeError If both `attachment` and `attachments` are specified. """ # noqa: E501 - Line too long return await self.app.rest.create_message( @@ -345,7 +346,7 @@ class GroupDMChannel(DMChannel): application_id: typing.Optional[snowflake.Snowflake] = attr.ib(eq=False, hash=False, repr=False) """The ID of the application that created the group DM. - If the group DM was not created by a bot, this will be `None`. + If the group DM was not created by a bot, this will be `builtins.None`. """ def __str__(self) -> str: @@ -362,21 +363,21 @@ def format_icon(self, *, format: str = "png", size: int = 4096) -> typing.Option Parameters ---------- - format : str + format : builtins.str The format to use for this URL, defaults to `png`. Supports `png`, `jpeg`, `jpg` and `webp`. - size : int + size : builtins.int The size to set for the URL, defaults to `4096`. Can be any power of two between 16 and 4096. Returns ------- - hikari.utilities.files.URL or None - The URL, or `None` if no icon is present. + hikari.utilities.files.URL or builtins.None + The URL, or `builtins.None` if no icon is present. Raises ------ - ValueError + builtins.ValueError If `size` is not a power of two between 16 and 4096 (inclusive). """ if self.icon_hash is None: @@ -393,7 +394,7 @@ class GuildChannel(PartialChannel): """The ID of the guild the channel belongs to. !!! warning - This will be `None` when received over the gateway in certain events + This will be `builtins.None` when received over the gateway in certain events (e.g Guild Create). """ @@ -415,14 +416,14 @@ class GuildChannel(PartialChannel): """Whether the channel is marked as NSFW. !!! warning - This will be `None` when received over the gateway in certain events + This will be `builtins.None` when received over the gateway in certain events (e.g Guild Create). """ parent_id: typing.Optional[snowflake.Snowflake] = attr.ib(eq=False, hash=False, repr=True) """The ID of the parent category the channel belongs to. - If no parent category is set for the channel, this will be `None`. + If no parent category is set for the channel, this will be `builtins.None`. """ @@ -463,7 +464,7 @@ class GuildTextChannel(GuildChannel, TextChannel): """The timestamp of the last-pinned message. !!! note - This may be `None` in several cases; Discord does not document what + This may be `builtins.None` in several cases; Discord does not document what these cases are. Trust no one! """ @@ -487,7 +488,7 @@ class GuildNewsChannel(GuildChannel, TextChannel): """The timestamp of the last-pinned message. !!! note - This may be `None` in several cases; Discord does not document what + This may be `builtins.None` in several cases; Discord does not document what these cases are. Trust no one! """ diff --git a/hikari/models/colors.py b/hikari/models/colors.py index 9c6b72f5dc..5f2fd2ba6b 100644 --- a/hikari/models/colors.py +++ b/hikari/models/colors.py @@ -30,7 +30,7 @@ class Color(int): This value is immutable. - This is a specialization of `int` which provides alternative overrides for + This is a specialization of `builtins.int` which provides alternative overrides for common methods and color system conversions. This currently supports: @@ -127,7 +127,7 @@ class Color(int): def __init__(self, raw_rgb: typing.SupportsInt) -> None: if not (0 <= int(raw_rgb) <= 0xFFFFFF): raise ValueError(f"raw_rgb must be in the exclusive range of 0 and {0xFF_FF_FF}") - # The __new__ for `int` initializes the value for us, this super-call does nothing other + # The __new__ for `builtins.int` initializes the value for us, this super-call does nothing other # than keeping the linter happy. super().__init__() @@ -191,7 +191,7 @@ def raw_hex_code(self) -> str: # Ignore docstring not starting in an imperative mood @property def is_web_safe(self) -> bool: # noqa: D401 - """`True` if the color is web safe, `False` otherwise.""" + """`builtins.True` if the color is web safe, `builtins.False` otherwise.""" return not (((self & 0xFF0000) % 0x110000) or ((self & 0xFF00) % 0x1100) or ((self & 0xFF) % 0x11)) @classmethod @@ -202,11 +202,11 @@ def from_rgb(cls, red: int, green: int, blue: int, /) -> Color: Parameters ---------- - red : int + red : builtins.int Red channel. - green : int + green : builtins.int Green channel. - blue : int + blue : builtins.int Blue channel. Returns @@ -216,7 +216,7 @@ def from_rgb(cls, red: int, green: int, blue: int, /) -> Color: Raises ------ - ValueError + builtins.ValueError If red, green, or blue are outside the range [0x0, 0xFF]. """ if not 0 <= red <= 0xFF: @@ -237,11 +237,11 @@ def from_rgb_float(cls, red: float, green: float, blue: float, /) -> Color: Parameters ---------- - red : float + red : builtins.float Red channel. - green : float + green : builtins.float Green channel. - blue : float + blue : builtins.float Blue channel. Returns @@ -251,7 +251,7 @@ def from_rgb_float(cls, red: float, green: float, blue: float, /) -> Color: Raises ------ - ValueError + builtins.ValueError If red, green or blue are outside the range [0, 1]. """ if not 0 <= red <= 1: @@ -273,7 +273,7 @@ def from_hex_code(cls, hex_code: str, /) -> Color: Parameters ---------- - hex_code : str + hex_code : builtins.str A hexadecimal color code to parse. This may optionally start with a case insensitive `0x` or `#`. @@ -284,7 +284,7 @@ def from_hex_code(cls, hex_code: str, /) -> Color: Raises ------ - ValueError + builtins.ValueError If `hex_code` is not a hexadecimal or is a invalid length. """ if hex_code.startswith("#"): @@ -331,14 +331,12 @@ def from_bytes( Parameters ---------- - bytes_ : typing.Iterable[int] + bytes_ : typing.Iterable[builtins.int] A iterable of int byte values. - - byteorder : str + byteorder : builtins.str The endianess of the value represented by the bytes. Can be `"big"` endian or `"little"` endian. - - signed : bool + signed : builtins.bool Whether the value is signed or unsigned. Returns @@ -422,17 +420,17 @@ def to_bytes(self, length: int, byteorder: str, *, signed: bool = True) -> bytes Parameters ---------- - length : int + length : builtins.int The number of bytes to produce. Should be around `3`, but not less. - byteorder : str + byteorder : builtins.str The endianess of the value represented by the bytes. Can be `"big"` endian or `"little"` endian. - signed : bool + signed : builtins.bool Whether the value is signed or unsigned. Returns ------- - bytes + builtins.bytes The bytes representation of the Color. """ return int(self).to_bytes(length, byteorder, signed=signed) diff --git a/hikari/models/embeds.py b/hikari/models/embeds.py index 960f880da5..a4ee362433 100644 --- a/hikari/models/embeds.py +++ b/hikari/models/embeds.py @@ -58,7 +58,7 @@ class EmbedResource(files.Resource): """The resource this object wraps around.""" proxy_resource: typing.Optional[files.Resource] = attr.ib(default=None, repr=False, init=False) - """The proxied version of the resource, or `None` if not present. + """The proxied version of the resource, or `builtins.None` if not present. !!! note This field cannot be set by bots or webhooks while sending an embed @@ -82,14 +82,14 @@ def stream( Parameters ---------- - executor : concurrent.futures.Executor or None + executor : concurrent.futures.Executor or builtins.None The executor to run in for blocking operations. - If `None`, then the default executor is used for the current - event loop. - head_only : bool - Defaults to `False`. If `True`, then the implementation may - only retrieve HEAD information if supported. This currently - only has any effect for web requests. + If `builtins.None`, then the default executor is used for the + current event loop. + head_only : builtins.bool + Defaults to `builtins.False`. If `builtins.True`, then the + implementation may only retrieve HEAD information if supported. + This currently only has any effect for web requests. """ return self.resource.stream(executor=executor, head_only=head_only) @@ -99,10 +99,10 @@ class EmbedFooter: """Represents an embed footer.""" text: typing.Optional[str] = attr.ib(default=None, repr=True) - """The footer text, or `None` if not present.""" + """The footer text, or `builtins.None` if not present.""" icon: typing.Optional[EmbedResource] = attr.ib(default=None, repr=False) - """The URL of the footer icon, or `None` if not present.""" + """The URL of the footer icon, or `builtins.None` if not present.""" @attr.s(eq=True, hash=False, kw_only=True, slots=True) @@ -110,7 +110,7 @@ class EmbedImage(EmbedResource): """Represents an embed image.""" height: typing.Optional[int] = attr.ib(default=None, repr=False, init=False) - """The height of the image, if present and known, otherwise `None`. + """The height of the image, if present and known, otherwise `builtins.None`. !!! note This field cannot be set by bots or webhooks while sending an embed and @@ -119,7 +119,7 @@ class EmbedImage(EmbedResource): """ width: typing.Optional[int] = attr.ib(default=None, repr=False, init=False) - """The width of the image, if present and known, otherwise `None`. + """The width of the image, if present and known, otherwise `builtins.None`. !!! note This field cannot be set by bots or webhooks while sending an embed and @@ -174,16 +174,16 @@ class EmbedAuthor: """Represents an author of an embed.""" name: typing.Optional[str] = attr.ib(default=None, repr=True) - """The name of the author, or `None` if not specified.""" + """The name of the author, or `builtins.None` if not specified.""" url: typing.Optional[str] = attr.ib(default=None, repr=True) """The URL that the author's name should act as a hyperlink to. - This may be `None` if no hyperlink on the author's name is specified. + This may be `builtins.None` if no hyperlink on the author's name is specified. """ icon: typing.Optional[EmbedResource] = attr.ib(default=None, repr=False) - """The author's icon, or `None` if not present.""" + """The author's icon, or `builtins.None` if not present.""" @attr.s(eq=True, hash=False, kw_only=True, slots=True) @@ -202,7 +202,7 @@ class EmbedField: # in the constructor for `_inline`. @property def is_inline(self) -> bool: - """Return `True` if the field should display inline. + """Return `builtins.True` if the field should display inline. Defaults to False. """ @@ -218,11 +218,11 @@ class Embed: """Represents an embed.""" color: typing.Optional[colors.Color] = attr.ib(default=None, repr=False, converter=_maybe_color) - """Colour of the embed, or `None` to use the default.""" + """Colour of the embed, or `builtins.None` to use the default.""" @property def colour(self) -> typing.Optional[colors.Color]: - """Colour of the embed, or `None` to use the default. + """Colour of the embed, or `builtins.None` to use the default. !!! note This is an alias for `color` for people who do not use Americanized @@ -236,16 +236,16 @@ def colour(self, value: typing.Optional[colors.ColorLike]) -> None: self.color = value # type: ignore title: typing.Optional[str] = attr.ib(default=None, repr=True) - """The title of the embed, or `None` if not present.""" + """The title of the embed, or `builtins.None` if not present.""" description: typing.Optional[str] = attr.ib(default=None, repr=False) - """The description of the embed, or `None` if not present.""" + """The description of the embed, or `builtins.None` if not present.""" url: typing.Optional[str] = attr.ib(default=None, repr=False) - """The URL of the embed, or `None` if not present.""" + """The URL of the embed, or `builtins.None` if not present.""" timestamp: typing.Optional[datetime.datetime] = attr.ib(default=None, repr=True) - """The timestamp of the embed, or `None` if not present. + """The timestamp of the embed, or `builtins.None` if not present. !!! note If specified, this should be treated as a UTC timestamp. Ensure any @@ -302,16 +302,16 @@ def colour(self, value: typing.Optional[colors.ColorLike]) -> None: """ footer: typing.Optional[EmbedFooter] = attr.ib(default=None, repr=False) - """The footer of the embed, if present, otherwise `None`.""" + """The footer of the embed, if present, otherwise `builtins.None`.""" image: typing.Optional[EmbedImage] = attr.ib(default=None, repr=False) - """The image to display in the embed, or `None` if not present.""" + """The image to display in the embed, or `builtins.None` if not present.""" thumbnail: typing.Optional[EmbedImage] = attr.ib(default=None, repr=False) - """The thumbnail to show in the embed, or `None` if not present.""" + """The thumbnail to show in the embed, or `builtins.None` if not present.""" video: typing.Optional[EmbedVideo] = attr.ib(default=None, repr=False, init=False) - """The video to show in the embed, or `None` if not present. + """The video to show in the embed, or `builtins.None` if not present. !!! note This object cannot be set by bots or webhooks while sending an embed and diff --git a/hikari/models/emojis.py b/hikari/models/emojis.py index a0c77ed005..41ee1a61bd 100644 --- a/hikari/models/emojis.py +++ b/hikari/models/emojis.py @@ -242,7 +242,7 @@ class CustomEmoji(snowflake.Unique, Emoji): is_animated: typing.Optional[bool] = attr.ib(eq=False, hash=False, repr=True) """Whether the emoji is animated. - Will be `None` when received in Message Reaction Remove and Message + Will be `builtins.None` when received in Message Reaction Remove and Message Reaction Remove Emoji events. """ @@ -292,14 +292,14 @@ class KnownCustomEmoji(CustomEmoji): """The user that created the emoji. !!! note - This will be `None` if you are missing the `MANAGE_EMOJIS` + This will be `builtins.None` if you are missing the `MANAGE_EMOJIS` permission in the server the emoji is from. """ is_animated: bool = attr.ib(eq=False, hash=False, repr=True) """Whether the emoji is animated. - Unlike in `CustomEmoji`, this information is always known, and will thus never be `None`. + Unlike in `CustomEmoji`, this information is always known, and will thus never be `builtins.None`. """ is_colons_required: bool = attr.ib(eq=False, hash=False, repr=False) @@ -311,5 +311,5 @@ class KnownCustomEmoji(CustomEmoji): is_available: bool = attr.ib(eq=False, hash=False, repr=False) """Whether this emoji can currently be used. - May be `False` due to a loss of Sever Boosts on the emoji's guild. + May be `builtins.False` due to a loss of Sever Boosts on the emoji's guild. """ diff --git a/hikari/models/guilds.py b/hikari/models/guilds.py index 851da31f6f..80eab494fb 100644 --- a/hikari/models/guilds.py +++ b/hikari/models/guilds.py @@ -266,7 +266,7 @@ class Member: ) """This member's nickname. - This will be `None` if not set and `hikari.utilities.undefined.UndefinedType` + This will be `builtins.None` if not set and `hikari.utilities.undefined.UndefinedType` if it's state is unknown. """ @@ -281,7 +281,7 @@ class Member: ) """The datetime of when this member started "boosting" this guild. - This will be `None` if they aren't boosting and + This will be `builtins.None` if they aren't boosting and `hikari.utilities.undefined.UndefinedType` if their boosting status is unknown. """ @@ -333,7 +333,7 @@ class Role(PartialRole): is_hoisted: bool = attr.ib(eq=False, hash=False, repr=True) """Whether this role is hoisting the members it's attached to in the member list. - members will be hoisted under their highest role where this is set to `True`. + members will be hoisted under their highest role where this is set to `builtins.True`. """ position: int = attr.ib(eq=False, hash=False, repr=True) @@ -440,7 +440,7 @@ class GuildMemberBan: """Used to represent guild bans.""" reason: typing.Optional[str] = attr.ib(repr=True) - """The reason for this ban, will be `None` if no reason was given.""" + """The reason for this ban, will be `builtins.None` if no reason was given.""" user: users.User = attr.ib(repr=True) """The object of the user this ban targets.""" @@ -463,9 +463,9 @@ class UnavailableGuild(snowflake.Unique): # Ignore docstring not starting in an imperative mood @property def is_unavailable(self) -> bool: # noqa: D401 - """`True` if this guild is unavailable, else `False`. + """`builtins.True` if this guild is unavailable, else `builtins.False`. - This value is always `True`, and is only provided for consistency. + This value is always `builtins.True`, and is only provided for consistency. """ return True @@ -496,7 +496,7 @@ def __str__(self) -> str: @property def icon_url(self) -> typing.Optional[files.URL]: - """Icon for the guild, if set; otherwise `None`.""" + """Icon for the guild, if set; otherwise `builtins.None`.""" return self.format_icon() def format_icon(self, *, format_: typing.Optional[str] = None, size: int = 4096) -> typing.Optional[files.URL]: @@ -504,25 +504,25 @@ def format_icon(self, *, format_: typing.Optional[str] = None, size: int = 4096) Parameters ---------- - format_ : str or None + format_ : builtins.str or builtins.None The format to use for this URL, defaults to `png` or `gif`. Supports `png`, `jpeg`, `jpg`, `webp` and `gif` (when animated). - If `None`, then the correct default format is determined based on - whether the icon is animated or not. - size : int + If `builtins.None`, then the correct default format is determined + based on whether the icon is animated or not. + size : builtins.int The size to set for the URL, defaults to `4096`. Can be any power of two between 16 and 4096. Returns ------- - hikari.utilities.files.URL or None - The URL to the resource, or `None` if no icon is set. + hikari.utilities.files.URL or builtins.None + The URL to the resource, or `builtins.None` if no icon is set. Raises ------ - ValueError + builtins.ValueError If `size` is not a power of two or not between 16 and 4096. """ if self.icon_hash is None: @@ -569,21 +569,21 @@ def format_splash(self, *, format_: str = "png", size: int = 4096) -> typing.Opt Parameters ---------- - format_ : str + format_ : builtins.str The format to use for this URL, defaults to `png`. Supports `png`, `jpeg`, `jpg` and `webp`. - size : int + size : builtins.int The size to set for the URL, defaults to `4096`. Can be any power of two between 16 and 4096. Returns ------- - hikari.utilities.files.URL or None - The URL to the splash, or `None` if not set. + hikari.utilities.files.URL or builtins.None + The URL to the splash, or `builtins.None` if not set. Raises ------ - ValueError + builtins.ValueError If `size` is not a power of two or not between 16 and 4096. """ if self.splash_hash is None: @@ -601,21 +601,21 @@ def format_discovery_splash(self, *, format_: str = "png", size: int = 4096) -> Parameters ---------- - format_ : str + format_ : builtins.str The format to use for this URL, defaults to `png`. Supports `png`, `jpeg`, `jpg` and `webp`. - size : int + size : builtins.int The size to set for the URL, defaults to `4096`. Can be any power of two between 16 and 4096. Returns ------- - hikari.utilities.files.URL or None + hikari.utilities.files.URL or builtins.None The string URL. Raises ------ - ValueError + builtins.ValueError If `size` is not a power of two or not between 16 and 4096. """ if self.discovery_splash_hash is None: @@ -652,7 +652,7 @@ class Guild(PartialGuild): This will not take into account permission overwrites or implied permissions (for example, `ADMINISTRATOR` implies all other permissions). - This will be `None` when this object is retrieved through a HTTP request + This will be `builtins.None` when this object is retrieved through a HTTP request rather than from the gateway. """ @@ -662,7 +662,7 @@ class Guild(PartialGuild): afk_channel_id: typing.Optional[snowflake.Snowflake] = attr.ib(eq=False, hash=False, repr=False) """The ID for the channel that AFK voice users get sent to. - If `None`, then no AFK channel is set up for this guild. + If `builtins.None`, then no AFK channel is set up for this guild. """ afk_timeout: datetime.timedelta = attr.ib(eq=False, hash=False, repr=False) @@ -675,8 +675,8 @@ class Guild(PartialGuild): is_embed_enabled: typing.Optional[bool] = attr.ib(eq=False, hash=False, repr=False) """Defines if the guild embed is enabled or not. - This information may not be present, in which case, it will be `None` - instead. This will be `None` for guilds that the bot is not a member in. + This information may not be present, in which case, it will be `builtins.None` + instead. This will be `builtins.None` for guilds that the bot is not a member in. !!! deprecated Use `is_widget_enabled` instead. @@ -685,7 +685,7 @@ class Guild(PartialGuild): embed_channel_id: typing.Optional[snowflake.Snowflake] = attr.ib(eq=False, hash=False, repr=False) """The channel ID that the guild embed will generate an invite to. - Will be `None` if invites are disabled for this guild's embed. + Will be `builtins.None` if invites are disabled for this guild's embed. !!! deprecated Use `widget_channel_id` instead. @@ -712,7 +712,7 @@ class Guild(PartialGuild): application_id: typing.Optional[snowflake.Snowflake] = attr.ib(eq=False, hash=False, repr=False) """The ID of the application that created this guild. - This will always be `None` for guilds that weren't created by a bot. + This will always be `builtins.None` for guilds that weren't created by a bot. """ is_unavailable: typing.Optional[bool] = attr.ib(eq=False, hash=False, repr=False) @@ -720,7 +720,7 @@ class Guild(PartialGuild): This information is only available if the guild was sent via a `GUILD_CREATE` event. If the guild is received from any other place, this - will always be `None`. + will always be `builtins.None`. An unavailable guild cannot be interacted with, and most information may be outdated if that is the case. @@ -729,18 +729,18 @@ class Guild(PartialGuild): is_widget_enabled: typing.Optional[bool] = attr.ib(eq=False, hash=False, repr=False) """Describes whether the guild widget is enabled or not. - If this information is not present, this will be `None`. + If this information is not present, this will be `builtins.None`. """ widget_channel_id: typing.Optional[snowflake.Snowflake] = attr.ib(eq=False, hash=False, repr=False) """The channel ID that the widget's generated invite will send the user to. If this information is unavailable or this isn't enabled for the guild then - this will be `None`. + this will be `builtins.None`. """ system_channel_id: typing.Optional[snowflake.Snowflake] = attr.ib(eq=False, hash=False, repr=False) - """The ID of the system channel or `None` if it is not enabled. + """The ID of the system channel or `builtins.None` if it is not enabled. Welcome messages and Nitro boost messages may be sent to this channel. """ @@ -752,7 +752,7 @@ class Guild(PartialGuild): """The ID of the channel where guilds with the `GuildFeature.PUBLIC` `features` display rules and guidelines. - If the `GuildFeature.PUBLIC` feature is not defined, then this is `None`. + If the `GuildFeature.PUBLIC` feature is not defined, then this is `builtins.None`. """ joined_at: typing.Optional[datetime.datetime] = attr.ib(eq=False, hash=False, repr=False) @@ -760,7 +760,7 @@ class Guild(PartialGuild): This information is only available if the guild was sent via a `GUILD_CREATE` event. If the guild is received from any other place, this will always be - `None`. + `builtins.None`. """ is_large: typing.Optional[bool] = attr.ib(eq=False, hash=False, repr=False) @@ -768,7 +768,7 @@ class Guild(PartialGuild): This information is only available if the guild was sent via a `GUILD_CREATE` event. If the guild is received from any other place, this will always be - `None`. + `builtins.None`. The implications of a large guild are that presence information will not be sent about members who are offline or invisible. @@ -779,7 +779,7 @@ class Guild(PartialGuild): This information is only available if the guild was sent via a `GUILD_CREATE` event. If the guild is received from any other place, this will always be - `None`. + `builtins.None`. """ members: typing.Optional[typing.Mapping[snowflake.Snowflake, Member]] = attr.ib(eq=False, hash=False, repr=False) @@ -787,7 +787,7 @@ class Guild(PartialGuild): This information is only available if the guild was sent via a `GUILD_CREATE` event. If the guild is received from any other place, this will always be - `None`. + `builtins.None`. Additionally, any offline members may not be included here, especially if there are more members than the large threshold set for the gateway this @@ -809,7 +809,7 @@ class Guild(PartialGuild): This information is only available if the guild was sent via a `GUILD_CREATE` event. If the guild is received from any other place, this will always be - `None`. + `builtins.None`. Additionally, any channels that you lack permissions to see will not be defined here. @@ -830,7 +830,7 @@ class Guild(PartialGuild): This information is only available if the guild was sent via a `GUILD_CREATE` event. If the guild is received from any other place, this will always be - `None`. + `builtins.None`. Additionally, any channels that you lack permissions to see will not be defined here. @@ -845,40 +845,40 @@ class Guild(PartialGuild): max_presences: typing.Optional[int] = attr.ib(eq=False, hash=False, repr=False) """The maximum number of presences for the guild. - If this is `None`, then the default value is used (currently 25000). + If this is `builtins.None`, then the default value is used (currently 25000). """ max_members: typing.Optional[int] = attr.ib(eq=False, hash=False, repr=False) """The maximum number of members allowed in this guild. - This information may not be present, in which case, it will be `None`. + This information may not be present, in which case, it will be `builtins.None`. """ max_video_channel_users: typing.Optional[int] = attr.ib(eq=False, hash=False, repr=False) """The maximum number of users allowed in a video channel together. - This information may not be present, in which case, it will be `None`. + This information may not be present, in which case, it will be `builtins.None`. """ vanity_url_code: typing.Optional[str] = attr.ib(eq=False, hash=False, repr=False) """The vanity URL code for the guild's vanity URL. This is only present if `GuildFeature.VANITY_URL` is in `Guild.features` for - this guild. If not, this will always be `None`. + this guild. If not, this will always be `builtins.None`. """ description: typing.Optional[str] = attr.ib(eq=False, hash=False, repr=False) """The guild's description. This is only present if certain `GuildFeature`'s are set in - `Guild.features` for this guild. Otherwise, this will always be `None`. + `Guild.features` for this guild. Otherwise, this will always be `builtins.None`. """ banner_hash: typing.Optional[str] = attr.ib(eq=False, hash=False, repr=False) """The hash for the guild's banner. This is only present if the guild has `GuildFeature.BANNER` in - `Guild.features` for this guild. For all other purposes, it is `None`. + `Guild.features` for this guild. For all other purposes, it is `builtins.None`. """ premium_tier: GuildPremiumTier = attr.ib(eq=False, hash=False, repr=False) @@ -887,7 +887,7 @@ class Guild(PartialGuild): premium_subscription_count: typing.Optional[int] = attr.ib(eq=False, hash=False, repr=False) """The number of nitro boosts that the server currently has. - This information may not be present, in which case, it will be `None`. + This information may not be present, in which case, it will be `builtins.None`. """ preferred_locale: str = attr.ib(eq=False, hash=False, repr=False) @@ -902,16 +902,16 @@ class Guild(PartialGuild): from Discord. This is only present if `GuildFeature.PUBLIC` is in `Guild.features` for - this guild. For all other purposes, it should be considered to be `None`. + this guild. For all other purposes, it should be considered to be `builtins.None`. """ - # TODO: if this is `None`, then should we attempt to look at the known member count if present? + # TODO: if this is `builtins.None`, then should we attempt to look at the known member count if present? approximate_member_count: typing.Optional[int] = attr.ib(eq=False, hash=False, repr=False) """The approximate number of members in the guild. This information will be provided by HTTP API calls fetching the guilds that a bot account is in. For all other purposes, this should be expected to - remain `None`. + remain `builtins.None`. """ approximate_active_member_count: typing.Optional[int] = attr.ib(eq=False, hash=False, repr=False) @@ -919,7 +919,7 @@ class Guild(PartialGuild): This information will be provided by HTTP API calls fetching the guilds that a bot account is in. For all other purposes, this should be expected to - remain `None`. + remain `builtins.None`. """ @property @@ -932,21 +932,21 @@ def format_splash(self, *, format_: str = "png", size: int = 4096) -> typing.Opt Parameters ---------- - format_ : str + format_ : builtins.str The format to use for this URL, defaults to `png`. Supports `png`, `jpeg`, `jpg` and `webp`. - size : int + size : builtins.int The size to set for the URL, defaults to `4096`. Can be any power of two between 16 and 4096. Returns ------- - hikari.utilities.files.URL or None - The URL to the splash, or `None` if not set. + hikari.utilities.files.URL or builtins.None + The URL to the splash, or `builtins.None` if not set. Raises ------ - ValueError + builtins.ValueError If `size` is not a power of two or not between 16 and 4096. """ if self.splash_hash is None: @@ -964,21 +964,21 @@ def format_discovery_splash(self, *, format_: str = "png", size: int = 4096) -> Parameters ---------- - format_ : str + format_ : builtins.str The format to use for this URL, defaults to `png`. Supports `png`, `jpeg`, `jpg` and `webp`. - size : int + size : builtins.int The size to set for the URL, defaults to `4096`. Can be any power of two between 16 and 4096. Returns ------- - hikari.utilities.files.URL or None + hikari.utilities.files.URL or builtins.None The string URL. Raises ------ - ValueError + builtins.ValueError If `size` is not a power of two or not between 16 and 4096. """ if self.discovery_splash_hash is None: @@ -998,21 +998,21 @@ def format_banner(self, *, format_: str = "png", size: int = 4096) -> typing.Opt Parameters ---------- - format_ : str + format_ : builtins.str The format to use for this URL, defaults to `png`. Supports `png`, `jpeg`, `jpg` and `webp`. - size : int + size : builtins.int The size to set for the URL, defaults to `4096`. Can be any power of two between 16 and 4096. Returns ------- - hikari.utilities.files.URL or None - The URL of the banner, or `None` if no banner is set. + hikari.utilities.files.URL or builtins.None + The URL of the banner, or `builtins.None` if no banner is set. Raises ------ - ValueError + builtins.ValueError If `size` is not a power of two or not between 16 and 4096. """ if self.banner_hash is None: diff --git a/hikari/models/intents.py b/hikari/models/intents.py index 4f9b15274a..b89052cdb3 100644 --- a/hikari/models/intents.py +++ b/hikari/models/intents.py @@ -180,7 +180,7 @@ def __str__(self) -> str: def is_privileged(self) -> bool: """Whether the intent requires elevated privileges. - If this is `True`, you will be required to opt-in to using this intent + If this is `builtins.True`, you will be required to opt-in to using this intent on the Discord Developer Portal before you can utilise it in your application. """ diff --git a/hikari/models/invites.py b/hikari/models/invites.py index dcf081bb82..839f21e27e 100644 --- a/hikari/models/invites.py +++ b/hikari/models/invites.py @@ -85,14 +85,14 @@ class InviteGuild(guilds.PartialGuild): """The hash for the guild's banner. This is only present if `hikari.models.guilds.GuildFeature.BANNER` is in the - `features` for this guild. For all other purposes, it is `None`. + `features` for this guild. For all other purposes, it is `builtins.None`. """ description: typing.Optional[str] = attr.ib(eq=False, hash=False, repr=False) """The guild's description. This is only present if certain `features` are set in this guild. - Otherwise, this will always be `None`. For all other purposes, it is `None`. + Otherwise, this will always be `builtins.None`. For all other purposes, it is `builtins.None`. """ verification_level: guilds.GuildVerificationLevel = attr.ib(eq=False, hash=False, repr=False) @@ -102,7 +102,7 @@ class InviteGuild(guilds.PartialGuild): """The vanity URL code for the guild's vanity URL. This is only present if `hikari.models.guilds.GuildFeature.VANITY_URL` is in the - `features` for this guild. If not, this will always be `None`. + `features` for this guild. If not, this will always be `builtins.None`. """ @property @@ -115,21 +115,21 @@ def format_splash(self, *, format_: str = "png", size: int = 4096) -> typing.Opt Parameters ---------- - format_ : str + format_ : builtins.str The format to use for this URL, defaults to `png`. Supports `png`, `jpeg`, `jpg` and `webp`. - size : int + size : builtins.int The size to set for the URL, defaults to `4096`. Can be any power of two between 16 and 4096. Returns ------- - hikari.utilities.files.URL or None - The URL to the splash, or `None` if not set. + hikari.utilities.files.URL or builtins.None + The URL to the splash, or `builtins.None` if not set. Raises ------ - ValueError + builtins.ValueError If `size` is not a power of two or not between 16 and 4096. """ if self.splash_hash is None: @@ -147,21 +147,21 @@ def format_banner(self, *, format_: str = "png", size: int = 4096) -> typing.Opt Parameters ---------- - format_ : str + format_ : builtins.str The format to use for this URL, defaults to `png`. Supports `png`, `jpeg`, `jpg` and `webp`. - size : int + size : builtins.int The size to set for the URL, defaults to `4096`. Can be any power of two between 16 and 4096. Returns ------- hikari.utilities.files.URL or None - The URL of the banner, or `None` if no banner is set. + The URL of the banner, or `builtins.None` if no banner is set. Raises ------ - ValueError + builtins.ValueError If `size` is not a power of two or not between 16 and 4096. """ if self.banner_hash is None: @@ -183,20 +183,20 @@ class Invite: guild: typing.Optional[InviteGuild] = attr.ib(eq=False, hash=False, repr=False) """The partial object of the guild this invite belongs to. - Will be `None` for group DM invites and when attached to a gateway event; + Will be `builtins.None` for group DM invites and when attached to a gateway event; for invites received over the gateway you should refer to `Invite.guild_id`. """ guild_id: typing.Optional[snowflake.Snowflake] = attr.ib(eq=False, hash=False, repr=True) """The ID of the guild this invite belongs to. - Will be `None` for group DM invites. + Will be `builtins.None` for group DM invites. """ channel: typing.Optional[channels.PartialChannel] = attr.ib(eq=False, hash=False, repr=False) """The partial object of the channel this invite targets. - Will be `None` for invite objects that are attached to gateway events, + Will be `builtins.None` for invite objects that are attached to gateway events, in which case you should refer to `Invite.channel_id`. """ @@ -215,14 +215,14 @@ class Invite: approximate_presence_count: typing.Optional[int] = attr.ib(eq=False, hash=False, repr=False) """The approximate amount of presences in this invite's guild. - This is only present when `with_counts` is passed as `True` to the GET + This is only present when `with_counts` is passed as `builtins.True` to the GET Invites endpoint. """ approximate_member_count: typing.Optional[int] = attr.ib(eq=False, hash=False, repr=False) """The approximate amount of members in this invite's guild. - This is only present when `with_counts` is passed as `True` to the GET + This is only present when `with_counts` is passed as `builtins.True` to the GET Invites endpoint. """ @@ -244,7 +244,7 @@ class InviteWithMetadata(Invite): max_uses: typing.Optional[int] = attr.attrib(eq=False, hash=False, repr=True) """The limit for how many times this invite can be used before it expires. - If set to `None` then this is unlimited. + If set to `builtins.None` then this is unlimited. """ # TODO: can we use a non-None value to represent infinity here somehow, or @@ -252,7 +252,7 @@ class InviteWithMetadata(Invite): max_age: typing.Optional[datetime.timedelta] = attr.attrib(eq=False, hash=False, repr=False) """The timedelta of how long this invite will be valid for. - If set to `None` then this is unlimited. + If set to `builtins.None` then this is unlimited. """ is_temporary: bool = attr.attrib(eq=False, hash=False, repr=True) @@ -265,7 +265,7 @@ class InviteWithMetadata(Invite): def expires_at(self) -> typing.Optional[datetime.datetime]: """When this invite should expire, if `InviteWithMetadata.max_age` is set. - If this invite doesn't have a set expiry then this will be `None`. + If this invite doesn't have a set expiry then this will be `builtins.None`. """ if self.max_age is not None: return self.created_at + self.max_age diff --git a/hikari/models/messages.py b/hikari/models/messages.py index dcf0d6ff5a..470a95384d 100644 --- a/hikari/models/messages.py +++ b/hikari/models/messages.py @@ -228,7 +228,7 @@ class MessageCrosspost: """The ID of the message. !!! warning - This may be `None` in some cases according to the Discord API + This may be `builtins.None` in some cases according to the Discord API documentation, but the situations that cause this to occur are not currently documented. """ @@ -240,7 +240,7 @@ class MessageCrosspost: """The ID of the guild that the message originated from. !!! warning - This may be `None` in some cases according to the Discord API + This may be `builtins.None` in some cases according to the Discord API documentation, but the situations that cause this to occur are not currently documented. """ @@ -279,7 +279,7 @@ class Message(snowflake.Unique): edited_timestamp: typing.Optional[datetime.datetime] = attr.ib(eq=False, hash=False, repr=False) """The timestamp that the message was last edited at. - Will be `None` if it wasn't ever edited. + Will be `builtins.None` if it wasn't ever edited. """ is_tts: bool = attr.ib(eq=False, hash=False, repr=False) @@ -373,23 +373,24 @@ async def edit( Parameters ---------- - text : str or hikari.utilities.undefined.UndefinedType or None - If specified, the message text to set on the message. If `None`, - then the content is removed if already present. - embed : hikari.models.embeds.Embed or hikari.utilities.undefined.UndefinedType or None - If specified, the embed object to set on the message. If `None`, - then the embed is removed if already present. - mentions_everyone : bool + text : builtins.str or hikari.utilities.undefined.UndefinedType or builtins.None + If specified, the message text to set on the message. If + `builtins.None`, then the content is removed if already present. + embed : hikari.models.embeds.Embed or hikari.utilities.undefined.UndefinedType or builtins.None + If specified, the embed object to set on the message. If + `builtins.None`, then the embed is removed if already present. + mentions_everyone : builtins.bool Whether `@everyone` and `@here` mentions should be resolved by - discord and lead to actual pings, defaults to `False`. - user_mentions : typing.Collection[hikari.models.users.User or hikari.utilities.snowflake.UniqueObject] or bool + discord and lead to actual pings, defaults to `builtins.False`. + user_mentions : typing.Collection[hikari.models.users.User or hikari.utilities.snowflake.UniqueObject] or builtins.bool Either an array of user objects/IDs to allow mentions for, - `True` to allow all user mentions or `False` to block all - user mentions from resolving, defaults to `True`. - role_mentions: typing.Collection[hikari.models.guilds.Role or hikari.utilities.snowflake.UniqueObject] or bool + `builtins.True` to allow all user mentions or `builtins.False` + to block all user mentions from resolving. Defaults to + `builtins.True`. + role_mentions: typing.Collection[hikari.models.guilds.Role or hikari.utilities.snowflake.UniqueObject] or builtins.bool Either an array of guild role objects/IDs to allow mentions for, - `True` to allow all role mentions or `False` to block all - role mentions from resolving, defaults to `True`. + `builtins.True` to allow all role mentions or `builtins.False` to + block all role mentions from resolving. Defaults to `builtins.True`. Returns ------- @@ -412,10 +413,10 @@ async def edit( on a message you did not author. If you try to edit the flags on a message you did not author without the `MANAGE_MESSAGES` permission. - ValueError + builtins.ValueError If more than 100 unique objects/entities are passed for `role_mentions` or `user_mentions`. - """ + """ # noqa: E501 - Line too long return await self.app.rest.edit_message( message=self.id, channel=self.channel_id, @@ -449,35 +450,35 @@ async def reply( Parameters ---------- - text : str or hikari.utilities.undefined.UndefinedType + text : builtins.str or hikari.utilities.undefined.UndefinedType If specified, the message text to send with the message. - nonce : str or hikari.utilities.undefined.UndefinedType + nonce : builtins.str or hikari.utilities.undefined.UndefinedType If specified, an optional ID to send for opportunistic message creation. This doesn't serve any real purpose for general use, and can usually be ignored. - tts : bool or hikari.utilities.undefined.UndefinedType + tts : builtins.bool or hikari.utilities.undefined.UndefinedType If specified, whether the message will be sent as a TTS message. - attachment : hikari.utilities.files.Resource or str or hikari.utilities.undefined.UndefinedType + attachment : hikari.utilities.files.Resource or builtins.str or hikari.utilities.undefined.UndefinedType If specified, a attachment to upload, if desired. This can be a resource, or string of a path on your computer or a URL. - attachments : typing.Sequence[hikari.utilities.files.Resource or str] or hikari.utilities.undefined.UndefinedType + attachments : typing.Sequence[hikari.utilities.files.Resource or builtins.str] or hikari.utilities.undefined.UndefinedType If specified, a sequence of attachments to upload, if desired. Should be between 1 and 10 objects in size (inclusive), also including embed attachments. These can be resources, or strings consisting of paths on your computer or URLs. embed : hikari.models.embeds.Embed or hikari.utilities.undefined.UndefinedType If specified, the embed object to send with the message. - mentions_everyone : bool + mentions_everyone : builtins.bool Whether `@everyone` and `@here` mentions should be resolved by - discord and lead to actual pings, defaults to `False`. - user_mentions : typing.Collection[hikari.models.users.User or hikari.utilities.snowflake.UniqueObject] or bool + discord and lead to actual pings, defaults to `builtins.False`. + user_mentions : typing.Collection[hikari.models.users.User or hikari.utilities.snowflake.UniqueObject] or builtins.bool Either an array of user objects/IDs to allow mentions for, - `True` to allow all user mentions or `False` to block all - user mentions from resolving, defaults to `True`. - role_mentions: typing.Collection[hikari.models.guilds.Role or hikari.utilities.snowflake.UniqueObject] or bool + `builtins.True` to allow all user mentions or `builtins.False` to block all + user mentions from resolving, defaults to `builtins.True`. + role_mentions: typing.Collection[hikari.models.guilds.Role or hikari.utilities.snowflake.UniqueObject] or builtins.bool Either an array of guild role objects/IDs to allow mentions for, - `True` to allow all role mentions or `False` to block all - role mentions from resolving, defaults to `True`. + `builtins.True` to allow all role mentions or `builtins.False` to block all + role mentions from resolving, defaults to `builtins.True`. Returns ------- @@ -500,10 +501,10 @@ async def reply( hikari.errors.Forbidden If you lack permissions to send to the channel this message belongs to. - ValueError + builtins.ValueError If more than 100 unique objects/entities are passed for `role_mentions` or `user_mentions`. - TypeError + builtins.TypeError If both `attachment` and `attachments` are specified. """ # noqa: E501 - Line too long return await self.app.rest.create_message( @@ -537,7 +538,7 @@ async def add_reaction(self, emoji: typing.Union[str, emojis_.Emoji]) -> None: Parameters ---------- - emoji : str or hikari.models.emojis.Emoji + emoji : builtins.str or hikari.models.emojis.Emoji The emoji to add. Examples @@ -582,7 +583,7 @@ async def remove_reaction( Parameters ---------- - emoji : str or hikari.models.emojis.Emoji + emoji : builtins.str or hikari.models.emojis.Emoji The emoji to remove. user : hikari.models.users.User or hikari.utilities.undefined.UndefinedType The user of the reaction to remove. If unspecified, then the bot's @@ -630,7 +631,7 @@ async def remove_all_reactions( Parameters ---------- - emoji : str or hikari.models.emojis.Emoji or hikari.utilities.undefined.UndefinedType + emoji : builtins.str or hikari.models.emojis.Emoji or hikari.utilities.undefined.UndefinedType The emoji to remove all reactions for. If not specified, then all emojis are removed. diff --git a/hikari/models/presences.py b/hikari/models/presences.py index a971cd3cba..711c99314c 100644 --- a/hikari/models/presences.py +++ b/hikari/models/presences.py @@ -296,13 +296,13 @@ class MemberPresence: """The ids of the user's current roles in the guild this presence belongs to. !!! info - If this is `None` then this information wasn't provided and is unknown. + If this is `builtins.None` then this information wasn't provided and is unknown. """ guild_id: typing.Optional[snowflake.Snowflake] = attr.ib(eq=True, hash=True, repr=True) """The ID of the guild this presence belongs to. - This will be `None` when received in an array of members attached to a guild + This will be `builtins.None` when received in an array of members attached to a guild object (e.g on Guild Create). """ @@ -320,7 +320,7 @@ class MemberPresence: premium_since: typing.Optional[datetime.datetime] = attr.ib(eq=False, hash=False, repr=False) """The datetime of when this member started "boosting" this guild. - This will be `None` if they aren't boosting. + This will be `builtins.None` if they aren't boosting. """ nickname: typing.Optional[str] = attr.ib(eq=False, hash=False, repr=True) diff --git a/hikari/models/users.py b/hikari/models/users.py index c59ea7f795..955c6550f4 100644 --- a/hikari/models/users.py +++ b/hikari/models/users.py @@ -157,13 +157,13 @@ class User(PartialUser): """This user's username.""" avatar_hash: typing.Optional[str] - """This user's avatar hash, if they have one, otherwise `None`.""" + """This user's avatar hash, if they have one, otherwise `builtins.None`.""" is_bot: bool - """`True` if this user is a bot account, `False` otherwise.""" + """`builtins.True` if this user is a bot account, `builtins.False` otherwise.""" is_system: bool - """`True` if this user is a system account, `False` otherwise.""" + """`builtins.True` if this user is a system account, `builtins.False` otherwise.""" flags: UserFlag """The public flags for this user.""" @@ -185,38 +185,39 @@ async def fetch_self(self) -> User: @property def avatar(self) -> typing.Optional[files.URL]: - """Avatar for the user if set, else `None`.""" + """Avatar for the user if set, else `builtins.None`.""" return self.format_avatar() def format_avatar(self, *, format_: typing.Optional[str] = None, size: int = 4096) -> typing.Optional[files.URL]: """Generate the avatar for this user, if set. - If no custom avatar is set, this returns `None`. You can then use the - `default_avatar_url` attribute instead to fetch the displayed URL. + If no custom avatar is set, this returns `builtins.None`. You can then + use the `default_avatar_url` attribute instead to fetch the displayed + URL. Parameters ---------- - format_ : str or `None` + format_ : builtins.str or builtins.None The format to use for this URL, defaults to `png` or `gif`. Supports `png`, `jpeg`, `jpg`, `webp` and `gif` (when animated). Will be ignored for default avatars which can only be `png`. - If `None`, then the correct default format is determined based on - whether the icon is animated or not. - size : int + If `builtins.None`, then the correct default format is determined + based on whether the icon is animated or not. + size : builtins.int The size to set for the URL, defaults to `4096`. Can be any power of two between 16 and 4096. Will be ignored for default avatars. Returns ------- - hikari.utilities.files.URL - The URL to the avatar, or `None` if not present. + hikari.utilities.files.URL or builtins.None + The URL to the avatar, or `builtins.None` if not present. Raises ------ - ValueError + builtins.ValueError If `size` is not a power of two or not between 16 and 4096. """ if self.avatar_hash is None: @@ -254,21 +255,21 @@ class OwnUser(User): is_verified: typing.Optional[bool] = attr.ib(eq=False, hash=False, repr=False) """Whether the email for this user's account has been verified. - Will be `None` if retrieved through the OAuth2 flow without the `email` + Will be `builtins.None` if retrieved through the OAuth2 flow without the `email` scope. """ email: typing.Optional[str] = attr.ib(eq=False, hash=False, repr=False) """The user's set email. - Will be `None` if retrieved through OAuth2 flow without the `email` - scope. Will always be `None` for bot users. + Will be `builtins.None` if retrieved through OAuth2 flow without the `email` + scope. Will always be `builtins.None` for bot users. """ premium_type: typing.Optional[PremiumType] = attr.ib(eq=False, hash=False, repr=False) """The type of Nitro Subscription this user account had. - This will always be `None` for bots. + This will always be `builtins.None` for bots. """ async def fetch_self(self) -> OwnUser: diff --git a/hikari/models/voices.py b/hikari/models/voices.py index 272bda292f..50f5a14d6f 100644 --- a/hikari/models/voices.py +++ b/hikari/models/voices.py @@ -44,7 +44,7 @@ class VoiceState: channel_id: typing.Optional[snowflake.Snowflake] = attr.ib(eq=False, hash=False, repr=True) """The ID of the channel this user is connected to. - This will be `None` if they are leaving voice. + This will be `builtins.None` if they are leaving voice. """ user_id: snowflake.Snowflake = attr.ib(eq=False, hash=False, repr=True) diff --git a/hikari/models/webhooks.py b/hikari/models/webhooks.py index 2ac666b940..ab0cf340b8 100644 --- a/hikari/models/webhooks.py +++ b/hikari/models/webhooks.py @@ -85,7 +85,7 @@ class Webhook(snowflake.Unique): """The user that created the webhook !!! info - This will be `None` when getting a webhook with bot authorization rather + This will be `builtins.None` when getting a webhook with bot authorization rather than the webhook's token. """ @@ -130,36 +130,36 @@ async def execute( Parameters ---------- - text : str or hikari.utilities.undefined.UndefinedType + text : builtins.str or hikari.utilities.undefined.UndefinedType If specified, the message content to send with the message. - username : str or hikari.utilities.undefined.UndefinedType + username : builtins.str or hikari.utilities.undefined.UndefinedType If specified, the username to override the webhook's username for this request. - avatar_url : str or hikari.utilities.undefined.UndefinedType + avatar_url : builtins.str or hikari.utilities.undefined.UndefinedType If specified, the url of an image to override the webhook's avatar with for this request. - tts : bool or hikari.utilities.undefined.UndefinedType + tts : builtins.bool or hikari.utilities.undefined.UndefinedType If specified, whether the message will be sent as a TTS message. - attachment : hikari.utilities.undefined.UndefinedType or str or hikari.utilities.files.Resource + attachment : hikari.utilities.undefined.UndefinedType or builtins.str or hikari.utilities.files.Resource If specified, the message attachment. This can be a resource, or string of a path on your computer or a URL. - attachments : hikari.utilities.undefined.UndefinedType or typing.Sequence[str or hikari.utilities.files.Resource] + attachments : hikari.utilities.undefined.UndefinedType or typing.Sequence[builtins.str or hikari.utilities.files.Resource] If specified, the message attachments. These can be resources, or strings consisting of paths on your computer or URLs. embeds : typing.Sequence[hikari.models.embeds.Embed] or hikari.utilities.undefined.UndefinedType If specified, a sequence of between `1` to `10` embed objects (inclusive) to send with the embed. - mentions_everyone : bool + mentions_everyone : builtins.bool Whether `@everyone` and `@here` mentions should be resolved by - discord and lead to actual pings, defaults to `True`. - user_mentions : typing.Collection[hikari.models.users.User or hikari.utilities.snowflake.UniqueObject] or bool + discord and lead to actual pings, defaults to `builtins.True`. + user_mentions : typing.Collection[hikari.models.users.User or hikari.utilities.snowflake.UniqueObject] or builtins.bool Either an array of user objects/IDs to allow mentions for, - `True` to allow all user mentions or `False` to block all - user mentions from resolving, defaults to `True`. - role_mentions: typing.Collection[hikari.models.guilds.Role or hikari.utilities.snowflake.UniqueObject] or bool + `builtins.True` to allow all user mentions or `builtins.False` to + block all user mentions from resolving, defaults to `builtins.True`. + role_mentions: typing.Collection[hikari.models.guilds.Role or hikari.utilities.snowflake.UniqueObject] or builtins.bool Either an array of guild role objects/IDs to allow mentions for, - `True` to allow all role mentions or `False` to block all - role mentions from resolving, defaults to `True`. + `builtins.True` to allow all role mentions or `builtins.False` to + block all role mentions from resolving, defaults to `builtins.True`. Returns ------- @@ -179,10 +179,10 @@ async def execute( due to it being outside of the range of a 64 bit integer. hikari.errors.Unauthorized If you pass a token that's invalid for the target webhook. - ValueError - If either `Webhook.token` is `None` or more than 100 unique + builtins.ValueError + If either `Webhook.token` is `builtins.None` or more than 100 unique objects/entities are passed for `role_mentions` or `user_mentions. - TypeError + builtins.TypeError If both `attachment` and `attachments` are specified. """ # noqa: E501 - Line too long if not self.token: @@ -208,11 +208,11 @@ async def delete(self, *, use_token: typing.Union[undefined.UndefinedType, bool] Parameters ---------- - use_token : bool or hikari.utilities.undefined.UndefinedType - If set to `True` then the webhook's token will be used for this - request; if set to `False` then bot authorization will be used; - if not specified then the webhook's token will be used for the - request if it's set else bot authorization. + use_token : builtins.bool or hikari.utilities.undefined.UndefinedType + If set to `builtins.True` then the webhook's token will be used for + this request; if set to `builtins.False` then bot authorization will + be used; if not specified then the webhook's token will be used for + the request if it's set else bot authorization. Raises ------ @@ -221,8 +221,9 @@ async def delete(self, *, use_token: typing.Union[undefined.UndefinedType, bool] hikari.errors.Forbidden If you either lack the `MANAGE_WEBHOOKS` permission or aren't a member of the guild this webhook belongs to. - ValueError - If `use_token` is passed as `True` when `Webhook.token` is `None`. + builtins.ValueError + If `use_token` is passed as `builtins.True` when `Webhook.token` is + `builtins.None`. """ if use_token and self.token is None: raise ValueError("This webhook's token is unknown, so cannot be used.") @@ -247,23 +248,23 @@ async def edit( Parameters ---------- - name : str or hikari.utilities.undefined.UndefinedType + name : builtins.str or hikari.utilities.undefined.UndefinedType If specified, the new name string. avatar : hikari.utilities.files.Resource or None or hikari.utilities.undefined.UndefinedType - If specified, the new avatar image. If `None`, then + If specified, the new avatar image. If `builtins.None`, then it is removed. If not specified, nothing is changed. channel : hikari.models.channels.GuildChannel or hikari.utilities.snowflake.UniqueObject or hikari.utilities.undefined.UndefinedType If specified, the object or ID of the new channel the given webhook should be moved to. - reason : str or hikari.utilities.undefined.UndefinedType + reason : builtins.str or hikari.utilities.undefined.UndefinedType If specified, the audit log reason explaining why the operation was performed. This field will be used when using the webhook's token rather than bot authorization. - use_token : bool or hikari.utilities.undefined.UndefinedType - If set to `True` then the webhook's token will be used for this - request; if set to `False` then bot authorization will be used; - if not specified then the webhook's token will be used for the - request if it's set else bot authorization. + use_token : builtins.bool or hikari.utilities.undefined.UndefinedType + If set to `builtins.True` then the webhook's token will be used for + this request; if set to `builtins.False` then bot authorization will + be used; if not specified then the webhook's token will be used for + the request if it's set else bot authorization. Returns ------- @@ -282,8 +283,8 @@ async def edit( aren't a member of the guild this webhook belongs to. hikari.errors.Unauthorized If you pass a token that's invalid for the target webhook. - ValueError - If `use_token` is passed as `True` when `Webhook.token` is `None`. + builtins.ValueError + If `use_token` is passed as `builtins.True` when `Webhook.token` is `builtins.None`. """ # noqa: E501 - Line too long if use_token and self.token is None: raise ValueError("This webhook's token is unknown, so cannot be used.") @@ -319,11 +320,11 @@ async def fetch_self( Parameters ---------- - use_token : bool or hikari.utilities.undefined.UndefinedType - If set to `True` then the webhook's token will be used for this - request; if set to `False` then bot authorization will be used; - if not specified then the webhook's token will be used for the - request if it's set else bot authorization. + use_token : builtins.bool or hikari.utilities.undefined.UndefinedType + If set to `builtins.True` then the webhook's token will be used for + this request; if set to `builtins.False` then bot authorization will + be used; if not specified then the webhook's token will be used for + the request if it's set else bot authorization. Returns ------- @@ -342,8 +343,9 @@ async def fetch_self( lack the `MANAGE_WEBHOOKS` permission. hikari.errors.Unauthorized If you pass a token that's invalid for the target webhook. - ValueError - If `use_token` is passed as `True` when `Webhook.token` is `None`. + builtins.ValueError + If `use_token` is passed as `builtins.True` when `Webhook.token` + is `builtins.None`. """ if use_token and not self.token: raise ValueError("This webhook's token is unknown, so cannot be used.") @@ -379,28 +381,31 @@ def default_avatar(self) -> files_.URL: return cdn.generate_cdn_url("embed", "avatars", str(self.default_avatar_index), format_="png", size=None) def format_avatar(self, format_: str = "png", size: int = 4096) -> typing.Optional[files_.URL]: - """Generate the avatar URL for this webhook's custom avatar if set, else it's default avatar. + """Generate the avatar URL for this webhook's custom avatar if set. + + If no avatar is specified, return `None`. In this case, you should + use `default_avatar` instead. Parameters ---------- - format_ : str + format_ : builtins.str The format to use for this URL, defaults to `png`. Supports `png`, `jpeg`, `jpg`, `webp`. This will be ignored for default avatars which can only be `png`. - size : int + size : builtins.int The size to set for the URL, defaults to `4096`. Can be any power of two between 16 and 4096. Will be ignored for default avatars. Returns ------- - hikari.utilities.files.URL or None - The URL of the resource. `None` if no avatar is set (in this case, - use the `default_avatar` instead). + hikari.utilities.files.URL or builtins.None + The URL of the resource. `builtins.None` if no avatar is set (in + this case, use the `default_avatar` instead). Raises ------ - ValueError + builtins.ValueError If `size` is not a power of two between 16 and 4096 (inclusive). """ if self.avatar_hash is None: diff --git a/hikari/utilities/aio.py b/hikari/utilities/aio.py index f3dfbf431e..18ed84f459 100644 --- a/hikari/utilities/aio.py +++ b/hikari/utilities/aio.py @@ -33,14 +33,15 @@ def completed_future(result: typing.Optional[T_inv] = None, /) -> asyncio.Future Parameters ---------- - result : T_inv or None + result : T The value to set for the result of the future. - `T_inv` is a generic type placeholder for the type that - the future will have set as the result. + `T` is a generic type placeholder for the type that + the future will have set as the result. `T` may be `builtins.None`, in + which case, this will return `asyncio.Future[builtins.None]`. Returns ------- - asyncio.Future[T_inv or None] + asyncio.Future[T] The completed future. """ future = asyncio.get_event_loop().create_future() diff --git a/hikari/utilities/cdn.py b/hikari/utilities/cdn.py index e0d34c36b2..80f923171b 100644 --- a/hikari/utilities/cdn.py +++ b/hikari/utilities/cdn.py @@ -33,13 +33,13 @@ def generate_cdn_url(*route_parts: str, format_: str, size: typing.Optional[int] Parameters ---------- - *route_parts : str + *route_parts : builtins.str The string _route parts that will be used to form the link. - format_ : str + format_ : builtins.str The format to use for the wanted cdn entity, will usually be one of `webp`, `png`, `jpeg`, `jpg` or `gif` (which will be invalid if the target entity doesn't have an animated version available). - size : int or None + size : builtins.int or builtins.None The size to specify for the image in the query string if applicable, should be passed through as None to avoid the param being set. Must be any power of two between 16 and 4096. @@ -51,7 +51,7 @@ def generate_cdn_url(*route_parts: str, format_: str, size: typing.Optional[int] Raises ------ - ValueError + builtins.ValueError If `size` is not a power of two or not between 16 and 4096. """ if size and not 16 <= size <= 4096: @@ -70,12 +70,12 @@ def get_default_avatar_index(discriminator: str) -> int: Parameters ---------- - discriminator : str + discriminator : builtins.str The integer discriminator, as a string. Returns ------- - int + builtins.int The index. """ return int(discriminator) % 5 @@ -86,7 +86,7 @@ def get_default_avatar_url(discriminator: str) -> files.URL: Parameters ---------- - discriminator : str + discriminator : builtins.str The integer discriminator, as a string. Returns diff --git a/hikari/utilities/data_binding.py b/hikari/utilities/data_binding.py index 50cc6abc57..4539139e98 100644 --- a/hikari/utilities/data_binding.py +++ b/hikari/utilities/data_binding.py @@ -99,7 +99,7 @@ class StringMapBuilder(multidict.MultiDict[str]): low-level HTTP API interaction, amongst other things. !!! warn - Because this subclasses `dict`, you should not use the + Because this subclasses `builtins.dict`, you should not use the index operator to set items on this object. Doing so will skip any form of validation on the type. Use the `put*` methods instead. """ @@ -121,18 +121,19 @@ def put( Parameters ---------- - key : str + key : builtins.str The string key. value : hikari.utilities.undefined.UndefinedType or typing.Any The value to set. - conversion : typing.Callable[[typing.Any], typing.Any] or None + conversion : typing.Callable[[typing.Any], typing.Any] or builtins.None An optional conversion to perform. !!! note - The value will always be cast to a `str` before inserting it. + The value will always be cast to a `builtins.str` before inserting it. - `True` will be translated to `"true"`, `False` will be translated - to `"false"`, and `None` will be translated to `"null"`. + `builtins.True` will be translated to `"true"`, `builtins.False` + ill be translated to `"false"`, and `builtins.None` will be + translated to `"null"`. """ if value is not undefined.UNDEFINED: if conversion is not None: @@ -164,7 +165,7 @@ class JSONObjectBuilder(typing.Dict[str, JSONAny]): API interaction. !!! warn - Because this subclasses `dict`, you should not use the + Because this subclasses `builtins.dict`, you should not use the index operator to set items on this object. Doing so will skip any form of validation on the type. Use the `put*` methods instead. """ @@ -188,13 +189,13 @@ def put( Parameters ---------- - key : str + key : builtins.str The key to give the element. - value : JSONAny or typing.Any or hikari.utilities.undefined.UndefinedType + value : typing.Any or hikari.utilities.undefined.UndefinedType The JSON type to put. This may be a non-JSON type if a conversion is also specified. This may alternatively be undefined. In the latter case, nothing is performed. - conversion : typing.Callable[[typing.Any], JSONAny] or None + conversion : typing.Callable[[typing.Any], JSONAny] or builtins.None Optional conversion to apply. """ if value is not undefined.UNDEFINED: @@ -219,13 +220,13 @@ def put_array( Parameters ---------- - key : str + key : builtins.str The key to give the element. - values : JSONAny or Any or hikari.utilities.undefined.UndefinedType + values : typing.Iterable[T] or hikari.utilities.undefined.UndefinedType The JSON types to put. This may be an iterable of non-JSON types if a conversion is also specified. This may alternatively be undefined. In the latter case, nothing is performed. - conversion : typing.Callable[[typing.Any], JSONType] or None + conversion : typing.Callable[[typing.Any], JSONType] or builtins.None Optional conversion to apply. """ if values is not undefined.UNDEFINED: @@ -239,9 +240,9 @@ def put_snowflake(self, key: str, value: typing.Union[undefined.UndefinedType, s Parameters ---------- - key : str + key : builtins.str The key to give the element. - value : JSONAny or hikari.utilities.undefined.UndefinedType + value : hikari.utilities.snowflake.UniqueObject or hikari.utilities.undefined.UndefinedType The JSON type to put. This may alternatively be undefined. In the latter case, nothing is performed. """ @@ -255,11 +256,11 @@ def put_snowflake_array( If an undefined value is given, it is ignored. - Each snowflake should be castable to an `int`. + Each snowflake should be castable to an `builtins.int`. Parameters ---------- - key : str + key : builtins.str The key to give the element. values : typing.Iterable[typing.SupportsInt] or hikari.utilities.undefined.UndefinedType The JSON snowflakes to put. This may alternatively be undefined. diff --git a/hikari/utilities/date.py b/hikari/utilities/date.py index 57441e7047..63c2dc451e 100644 --- a/hikari/utilities/date.py +++ b/hikari/utilities/date.py @@ -40,7 +40,7 @@ """Type hint representing a naive time period or time span. This is an alias for `typing.Union[int, float, datetime.datetime]`, -where `int` and `float` types are interpreted as a number of seconds. +where `builtins.int` and `builtins.float` types are interpreted as a number of seconds. """ DISCORD_EPOCH: typing.Final[int] = 1_420_070_400 @@ -64,7 +64,7 @@ def rfc7231_datetime_string_to_datetime(date_str: str, /) -> datetime.datetime: Parameters ---------- - date_str : str + date_str : builtins.str The RFC-2822 (section 3.3) compliant date string to parse. Returns @@ -86,7 +86,7 @@ def iso8601_datetime_string_to_datetime(date_string: str, /) -> datetime.datetim Parameters ---------- - date_string : str + date_string : builtins.str The ISO-8601 compliant date string to parse. Returns @@ -124,7 +124,7 @@ def discord_epoch_to_datetime(epoch: int, /) -> datetime.datetime: Parameters ---------- - epoch : int + epoch : builtins.int Number of milliseconds since `1/1/2015 00:00:00 UTC`. Returns @@ -136,7 +136,7 @@ def discord_epoch_to_datetime(epoch: int, /) -> datetime.datetime: def datetime_to_discord_epoch(timestamp: datetime.datetime) -> int: - """Parse a `datetime.datetime` object into an `int` `DISCORD_EPOCH` offset. + """Parse a `datetime.datetime` object into an `builtins.int` `DISCORD_EPOCH` offset. Parameters ---------- @@ -145,7 +145,7 @@ def datetime_to_discord_epoch(timestamp: datetime.datetime) -> int: Returns ------- - int + builtins.int Number of milliseconds since `1/1/2015 00:00:00 UTC`. """ return int((timestamp.timestamp() - DISCORD_EPOCH) * 1_000) @@ -156,7 +156,7 @@ def unix_epoch_to_datetime(epoch: int, /) -> datetime.datetime: Parameters ---------- - epoch : int + epoch : builtins.int Number of milliseconds since `1/1/1970 00:00:00 UTC`. Returns @@ -190,7 +190,7 @@ def timespan_to_int(value: TimeSpan, /) -> int: Returns ------- - int + builtins.int The integer number of seconds. Fractions are discarded. Negative values are removed. """ diff --git a/hikari/utilities/files.py b/hikari/utilities/files.py index 0a05e157b7..a65b3d6eeb 100644 --- a/hikari/utilities/files.py +++ b/hikari/utilities/files.py @@ -77,15 +77,15 @@ def ensure_resource(url_or_resource: typing.Union[None, str, Resource], /) -> ty Parameters ---------- - url_or_resource : None or str or Resource - The item to convert. If the item is `None`, then `None` is returned. - Likewise if a `Resource` is passed, it is simply returned again. - Anything else is converted to a `Resource` first. + url_or_resource : builtins.None or builtins.str or Resource + The item to convert. If the item is `builtins.None`, then + `builtins.None` is returned. Likewise if a `Resource` is passed, it is + simply returned again. Anything else is converted to a `Resource` first. Returns ------- - Resource or None - The resource to use, or `None` if `None` was input. + Resource or builtins.None + The resource to use, or `builtins.None` if `builtins.None` was input. """ if isinstance(url_or_resource, Resource): return url_or_resource @@ -105,13 +105,13 @@ def guess_mimetype_from_filename(name: str, /) -> typing.Optional[str]: Parameters ---------- - name : bytes + name : builtins.bytes The filename to inspect. Returns ------- - str or None - The closest guess to the given filename. May be `None` if + builtins.str or builtins.None + The closest guess to the given filename. May be `builtins.None` if no match was found. """ guess, _ = mimetypes.guess_type(name) @@ -127,14 +127,14 @@ def guess_mimetype_from_data(data: bytes, /) -> typing.Optional[str]: Parameters ---------- - data : bytes + data : builtins.bytes The byte content to inspect. Returns ------- - str or None + builtins.str or builtins.None The mimetype, if it was found. If the header is unrecognised, then - `None` is returned. + `builtins.None` is returned. """ if data.startswith(b"\211PNG\r\n\032\n"): return "image/png" @@ -152,7 +152,7 @@ def guess_file_extension(mimetype: str) -> typing.Optional[str]: Parameters ---------- - mimetype : str + mimetype : builtins.str The mimetype to guess the extension for. Example @@ -164,9 +164,9 @@ def guess_file_extension(mimetype: str) -> typing.Optional[str]: Returns ------- - str or None + builtins.str or builtins.None The file extension, prepended with a `.`. If no match was found, - return `None`. + return `builtins.None`. """ return mimetypes.guess_extension(mimetype) @@ -181,16 +181,16 @@ def generate_filename_from_details( Parameters ---------- - mimetype : str or None - The mimetype of the content, or `None` if not known. - extension : str or None - The file extension to use, or `None` if not known. - data : bytes or None - The data to inspect, or `None` if not known. + mimetype : builtins.str or builtins.None + The mimetype of the content, or `builtins.None` if not known. + extension : builtins.str or builtins.None + The file extension to use, or `builtins.None` if not known. + data : builtins.bytes or builtins.None + The data to inspect, or `builtins.None` if not known. Returns ------- - str + builtins.str A generated quasi-unique filename. """ if data is not None and mimetype is None: @@ -212,14 +212,14 @@ def to_data_uri(data: bytes, mimetype: typing.Optional[str]) -> str: Parameters ---------- - data : bytes + data : builtins.bytes The data to encode as base64. - mimetype : str or None - The mimetype, or `None` if we should attempt to guess it. + mimetype : builtins.str or builtins.None + The mimetype, or `builtins.None` if we should attempt to guess it. Returns ------- - str + builtins.str A data URI string. """ if mimetype is None: @@ -244,7 +244,7 @@ class AsyncReader(typing.AsyncIterable[bytes], abc.ABC): """The filename of the resource.""" mimetype: typing.Optional[str] - """The mimetype of the resource. May be `None` if not known.""" + """The mimetype of the resource. May be `builtins.None` if not known.""" async def data_uri(self) -> str: """Fetch the data URI. @@ -254,7 +254,7 @@ async def data_uri(self) -> str: return to_data_uri(await self.read(), self.mimetype) async def read(self) -> bytes: - """Read the rest of the resource and return it in a `bytes` object.""" + """Read the rest of the resource and return it in a `builtins.bytes` object.""" buff = bytearray() async for chunk in self: buff.extend(chunk) @@ -299,7 +299,7 @@ class WebReader(AsyncReader): """The size of the resource, if known.""" head_only: bool - """If `True`, then only the HEAD was requested. + """If `builtins.True`, then only the HEAD was requested. In this case, neither `__aiter__` nor `read` would return anything other than an empty byte string. @@ -536,14 +536,14 @@ def stream( Parameters ---------- - executor : concurrent.futures.Executor or None + executor : concurrent.futures.Executor or builtins.None The executor to run in for blocking operations. - If `None`, then the default executor is used for the current - event loop. - head_only : bool - Defaults to `False`. If `True`, then the implementation may - only retrieve HEAD information if supported. This currently - only has any effect for web requests. + If `builtins.None`, then the default executor is used for the + current event loop. + head_only : builtins.bool + Defaults to `builtins.False`. If `builtins.True`, then the + implementation may only retrieve HEAD information if supported. + This currently only has any effect for web requests. Returns ------- @@ -571,15 +571,16 @@ class Bytes(Resource[ByteReader]): Parameters ---------- - data : bytes + data : builtins.bytes The raw data. - mimetype : str or None - The mimetype, or `None` if you do not wish to specify this. - filename : str or None - The filename to use, or `None` if one should be generated as needed. - extension : str or None - The file extension to use, or `None` if one should be determined - manually as needed. + mimetype : builtins.str or builtins.None + The mimetype, or `builtins.None` if you do not wish to specify this. + filename : builtins.str or builtins.None + The filename to use, or `builtins.None` if one should be generated as + needed. + extension : builtins.str or builtins.None + The file extension to use, or `builtins.None` if one should be + determined manually as needed. !!! note You only need to provide one of `mimetype`, `filename`, or `extension`. @@ -598,10 +599,10 @@ class Bytes(Resource[ByteReader]): """The raw data to upload.""" mimetype: typing.Optional[str] - """The provided mimetype, if specified. Otherwise `None`.""" + """The provided mimetype, if specified. Otherwise `builtins.None`.""" extension: typing.Optional[str] - """The provided file extension, if specified. Otherwise `None`.""" + """The provided file extension, if specified. Otherwise `builtins.None`.""" def __init__( self, @@ -646,9 +647,9 @@ def stream( Parameters ---------- - executor : concurrent.futures.Executor or None + executor : concurrent.futures.Executor or builtins.None Not used. Provided only to match the underlying interface. - head_only : bool + head_only : builtins.bool Not used. Provided only to match the underlying interface. Returns @@ -675,7 +676,7 @@ class WebResource(Resource[WebReader], abc.ABC): occur is within embeds. If you need to re-upload the resource, you should download it into - a `Bytes` and pass that instead in these cases. + a `builtins.bytes` and pass that instead in these cases. """ __slots__: typing.Sequence[str] = () @@ -690,12 +691,12 @@ def stream( Parameters ---------- - executor : concurrent.futures.Executor or None + executor : concurrent.futures.Executor or builtins.None Not used. Provided only to match the underlying interface. - head_only : bool - Defaults to `False`. If `True`, then the implementation may - only retrieve HEAD information if supported. This currently - only has any effect for web requests. + head_only : builtins.bool + Defaults to `builtins.False`. If `builtins.True`, then the + implementation may only retrieve HEAD information if supported. + This currently only has any effect for web requests. Examples -------- @@ -754,7 +755,7 @@ class URL(WebResource): Parameters ---------- - url : str + url : builtins.str The URL of the resource. !!! note @@ -763,7 +764,7 @@ class URL(WebResource): occur is within embeds. If you need to re-upload the resource, you should download it into - a `Bytes` and pass that instead in these cases. + a `builtins.bytes` and pass that instead in these cases. """ __slots__: typing.Sequence[str] = ("_url",) @@ -786,7 +787,7 @@ class File(Resource[FileReader]): Parameters ---------- - path : str or os.PathLike or pathlib.Path + path : builtins.str or os.PathLike or pathlib.Path The path to use. !!! note @@ -797,8 +798,8 @@ class File(Resource[FileReader]): This will all be performed as required in an executor to prevent blocking the event loop. - filename : str or None - The filename to use. If this is `None`, the name of the file is taken + filename : builtins.str or builtins.None + The filename to use. If this is `builtins.None`, the name of the file is taken from the path instead. """ @@ -829,11 +830,11 @@ def stream( Parameters ---------- - executor : typing.Optional[concurrent.futures.Executor] - The executor to run the blocking read operations in. If `None`, - the default executor for the running event loop will be used - instead. - head_only : bool + executor : concurrent.futures.Executor or builtins.None + The executor to run the blocking read operations in. If + `builtins.None`, the default executor for the running event loop + will be used instead. + head_only : builtins.bool Not used. Provided only to match the underlying interface. Returns diff --git a/hikari/utilities/iterators.py b/hikari/utilities/iterators.py index b39e1d08af..b6e070b1aa 100644 --- a/hikari/utilities/iterators.py +++ b/hikari/utilities/iterators.py @@ -43,7 +43,7 @@ class All(typing.Generic[ValueT]): """Helper that wraps predicates and invokes them together. Calling this object will pass the input item to each item, returning - `True` only when all wrapped predicates return True when called with the + `builtins.True` only when all wrapped predicates return True when called with the given item. For example... @@ -60,11 +60,11 @@ class All(typing.Generic[ValueT]): ... ``` - This behaves like a lazy wrapper implementation of the `all` builtin. + This behaves like a lazy wrapper implementation of the `builtins.all` builtin. !!! note Like the rest of the standard library, this is a short-circuiting - operation. This means that if a predicate returns `False`, no predicates + operation. This means that if a predicate returns `builtins.False`, no predicates after this are invoked, as the result is already known. In this sense, they are invoked in-order. @@ -75,15 +75,15 @@ class All(typing.Generic[ValueT]): Operators --------- * `this(value : T) -> bool`: - Return `True` if all conditions return `True` when invoked with the + Return `builtins.True` if all conditions return `builtins.True` when invoked with the given value. * `~this`: - Return a condition that, when invoked with the value, returns `False` - if all conditions were `True` in this object. + Return a condition that, when invoked with the value, returns `builtins.False` + if all conditions were `builtins.True` in this object. Parameters ---------- - *conditions : typing.Callable[[T], bool] + *conditions : typing.Callable[[T], builtins.bool] The predicates to wrap. """ @@ -106,7 +106,7 @@ class AttrComparator(typing.Generic[ValueT]): Parameters ---------- - attr_name : str + attr_name : builtins.str The attribute name. Can be prepended with a `.` optionally. If the attribute name ends with a `()`, then the call is invoked rather than treated as a property (useful for methods like @@ -164,7 +164,7 @@ class LazyIterator(typing.Generic[ValueT], abc.ABC): Additionally, you can make use of some of the provided helper methods on this class to perform basic operations easily. - Iterating across the items with indexes (like `enumerate` for normal + Iterating across the items with indexes (like `builtins.enumerate` for normal iterables): ```py @@ -192,7 +192,7 @@ def map( Parameters ---------- - transformation : typing.Callable[[ValueT], bool] or str + transformation : typing.Callable[[ValueT], builtins.bool] or builtins.str The function to use to map the attribute. This may alternatively be a string attribute name to replace the input value with. You can provide nested attributes using the `.` operator. @@ -200,7 +200,7 @@ def map( Returns ------- LazyIterator[AnotherValueT] - LazyIterator that maps each value to another value. + `LazyIterator` that maps each value to another value. """ if isinstance(transformation, str): transformation = typing.cast("spel.AttrGetter[ValueT, AnotherValueT]", spel.AttrGetter(transformation)) @@ -212,17 +212,25 @@ def filter( *predicates: typing.Union[typing.Tuple[str, typing.Any], typing.Callable[[ValueT], bool]], **attrs: typing.Any, ) -> LazyIterator[ValueT]: - """Filter the items by one or more conditions that must all be `True`. + """Filter the items by one or more conditions. + + Each condition is treated as a predicate, being called with each item + that this iterator would return when it is requested. + + All conditions must evaluate to `builtins.True` for the item to be + returned. If this is not met, then the item is discared and ignored, + the next matching item will be returned instead, if there is one. Parameters ---------- - *predicates : typing.Callable[[ValueT], bool] or typing.Tuple[str, typing.Any] + *predicates : typing.Callable[[ValueT], builtins.bool] or typing.Tuple[builtins.str, typing.Any] Predicates to invoke. These are functions that take a value and - return `True` if it is of interest, or `False` otherwise. These - may instead include 2-`tuple` objects consisting of a `str` - attribute name (nested attributes are referred to using the `.` - operator), and values to compare for equality. This allows you - to specify conditions such as `members.filter(("user.bot", True))`. + return `builtins.True` if it is of interest, or `builtins.False` + otherwise. These may instead include 2-`builtins.tuple` objects + consisting of a `builtins.str` attribute name (nested attributes + are referred to using the `.` operator), and values to compare for + equality. This allows you to specify conditions such as + `members.filter(("user.bot", True))`. **attrs : typing.Any Alternative to passing 2-tuples. Cannot specify nested attributes using this method. @@ -230,7 +238,7 @@ def filter( Returns ------- LazyIterator[ValueT] - LazyIterator that only emits values where all conditions are + `LazyIterator` that only emits values where all conditions are matched. """ conditions = self._map_predicates_and_attr_getters("filter", *predicates, **attrs) @@ -245,13 +253,13 @@ def take_while( Parameters ---------- - *predicates : typing.Callable[[ValueT], bool] or typing.Tuple[str, typing.Any] + *predicates : typing.Callable[[ValueT], builtins.bool] or typing.Tuple[builtins.str, typing.Any] Predicates to invoke. These are functions that take a value and - return `True` if it is of interest, or `False` otherwise. These - may instead include 2-`tuple` objects consisting of a `str` - attribute name (nested attributes are referred to using the `.` - operator), and values to compare for equality. This allows you - to specify conditions such as + return `builtins.True` if it is of interest, or `builtins.False` + otherwise. These may instead include 2-`builtins.tuple` objects + consisting of a `builtins.str` attribute name (nested attributes + are referred to using the `.` operator), and values to compare for + equality. This allows you to specify conditions such as `members.take_while(("user.bot", True))`. **attrs : typing.Any Alternative to passing 2-tuples. Cannot specify nested attributes @@ -275,13 +283,13 @@ def take_until( Parameters ---------- - *predicates : typing.Callable[[ValueT], bool] or typing.Tuple[str, typing.Any] + *predicates : typing.Callable[[ValueT], builtins.bool] or typing.Tuple[builtins.str, typing.Any] Predicates to invoke. These are functions that take a value and - return `True` if it is of interest, or `False` otherwise. These - may instead include 2-`tuple` objects consisting of a `str` - attribute name (nested attributes are referred to using the `.` - operator), and values to compare for equality. This allows you - to specify conditions such as + return `builtins.True` if it is of interest, or `builtins.False` + otherwise. These may instead include 2-`builtins.tuple` objects + consisting of a `builtins.str` attribute name (nested attributes are + referred to using the `.` operator), and values to compare for + equality. This allows you to specify conditions such as `members.take_until(("user.bot", True))`. **attrs : typing.Any Alternative to passing 2-tuples. Cannot specify nested attributes @@ -307,13 +315,13 @@ def skip_while( Parameters ---------- - *predicates : typing.Callable[[ValueT], bool] or typing.Tuple[str, typing.Any] + *predicates : typing.Callable[[ValueT], builtins.bool] or typing.Tuple[builtins.str, typing.Any] Predicates to invoke. These are functions that take a value and - return `True` if it is of interest, or `False` otherwise. These - may instead include 2-`tuple` objects consisting of a `str` - attribute name (nested attributes are referred to using the `.` - operator), and values to compare for equality. This allows you - to specify conditions such as + return `builtins.True` if it is of interest, or `builtins.False` + otherwise. These may instead include 2-`builtins.tuple` objects + consisting of a `builtins.str` attribute name (nested attributes + are referred to using the `.` operator), and values to compare for + equality. This allows you to specify conditions such as `members.skip_while(("user.bot", True))`. **attrs : typing.Any Alternative to passing 2-tuples. Cannot specify nested attributes @@ -339,13 +347,13 @@ def skip_until( Parameters ---------- - *predicates : typing.Callable[[ValueT], bool] or typing.Tuple[str, typing.Any] + *predicates : typing.Callable[[ValueT], builtins.bool] or typing.Tuple[builtins.str, typing.Any] Predicates to invoke. These are functions that take a value and - return `True` if it is of interest, or `False` otherwise. These - may instead include 2-`tuple` objects consisting of a `str` - attribute name (nested attributes are referred to using the `.` - operator), and values to compare for equality. This allows you - to specify conditions such as + return `builtins.True` if it is of interest, or `builtins.False` + otherwise. These may instead include 2-`builtins.tuple` objects + consisting of a `builtins.str` attribute name (nested attributes are + referred to using the `.` operator), and values to compare for + equality. This allows you to specify conditions such as `members.skip_until(("user.bot", True))`. **attrs : typing.Any Alternative to passing 2-tuples. Cannot specify nested attributes @@ -365,11 +373,11 @@ def enumerate(self, *, start: int = 0) -> LazyIterator[typing.Tuple[int, ValueT] This behaves as an asyncio-friendly version of `builtins.enumerate` which uses much less memory than collecting all the results first and - calling `enumerate` across them. + calling `builtins.enumerate` across them. Parameters ---------- - start : int + start : builtins.int Optional int to start at. If omitted, this is `0`. Examples @@ -398,7 +406,7 @@ def enumerate(self, *, start: int = 0) -> LazyIterator[typing.Tuple[int, ValueT] Returns ------- - LazyIterator[typing.Tuple[int, T]] + LazyIterator[typing.Tuple[builtins.int, T]] A paginated results view that asynchronously yields an increasing counter in a tuple with each result, lazily. """ @@ -409,7 +417,7 @@ def limit(self, limit: int) -> LazyIterator[ValueT]: Parameters ---------- - limit : int + limit : builtins.int The number of items to get. This must be greater than zero. Examples @@ -430,7 +438,7 @@ def skip(self, number: int) -> LazyIterator[ValueT]: Parameters ---------- - number : int + number : builtins.int The max number of items to drop before any items are yielded. Returns @@ -620,7 +628,7 @@ class BufferedLazyIterator(typing.Generic[ValueT], LazyIterator[ValueT], abc.ABC thus reducing the amount of work needed if only a few objects out of, say, 100, need to be deserialized. - This `_next_chunk` should return `None` once the end of all items has been + This `_next_chunk` should return `builtins.None` once the end of all items has been reached. An example would look like the following: diff --git a/hikari/utilities/reflect.py b/hikari/utilities/reflect.py index 3ec5a00620..0bed80543b 100644 --- a/hikari/utilities/reflect.py +++ b/hikari/utilities/reflect.py @@ -38,9 +38,9 @@ def resolve_signature(func: typing.Callable[..., typing.Any]) -> inspect.Signatu The function to get the resolved annotations from. !!! warning - This will use `eval` to resolve string typehints and forward references. - This has a slight performance overhead, so attempt to cache this info - as much as possible. + This will use `builtins.eval` to resolve string type-hints and forward + references. This has a slight performance overhead, so attempt to cache + this info as much as possible. Returns ------- @@ -72,9 +72,9 @@ def get_logger(obj: typing.Union[typing.Type[typing.Any], typing.Any], *addition Parameters ---------- - obj : typing.Type or object + obj : typing.Type or typing.Any A type or instance of a type to make a _LOGGER in the name of. - *additional_args : str + *additional_args : builtins.str Additional tokens to append onto the _LOGGER name, separated by `.`. This is useful in some places to append info such as shard ID to each _LOGGER to enable shard-specific logging, for example. diff --git a/hikari/utilities/snowflake.py b/hikari/utilities/snowflake.py index a4833a23fb..83931f4035 100644 --- a/hikari/utilities/snowflake.py +++ b/hikari/utilities/snowflake.py @@ -34,7 +34,7 @@ class Snowflake(int): """A concrete representation of a unique identifier for an object on Discord. - This object can be treated as a regular `int` for most purposes. + This object can be treated as a regular `builtins.int` for most purposes. """ __slots__: typing.Sequence[str] = () From 1375fad09d6bb0e85f148948080aa1f15f062710 Mon Sep 17 00:00:00 2001 From: davfsa Date: Tue, 30 Jun 2020 23:11:49 +0200 Subject: [PATCH 612/922] Fixed logo in documentation --- docs/config.mako | 2 +- pages/index.html | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/config.mako b/docs/config.mako index 8430940e4e..94b7158c84 100644 --- a/docs/config.mako +++ b/docs/config.mako @@ -40,7 +40,7 @@ search_query = "inurl:github.com/nekokatt/hikari site:nekokatt.gitlab.io/hikari/hikari" site_accent = "#ff029a" - site_logo = "/logo.png" + site_logo = "https://nekokatt.gitlab.io/hikari/logo.png" site_description = "A Discord Bot framework for modern Python and asyncio built on good intentions" # Versions of stuff diff --git a/pages/index.html b/pages/index.html index 463a4b54e5..ef03413ab2 100644 --- a/pages/index.html +++ b/pages/index.html @@ -20,10 +20,10 @@ | You should have received a copy of the GNU Lesser General Public License | along with Hikari. If not, see . !--> - + - + From 93ca797c48a36c5aa48ccc6b204d5ca355027c08 Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Tue, 30 Jun 2020 22:46:17 +0000 Subject: [PATCH 613/922] Update documentation.mako to fix some typo I made --- docs/documentation.mako | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/documentation.mako b/docs/documentation.mako index ab81e3cbed..0a16d295de 100644 --- a/docs/documentation.mako +++ b/docs/documentation.mako @@ -70,7 +70,7 @@ # To get links to work in type hints to builtins, we do a bit of hacky search-replace using regex. # This generates regex to match general builtins in typehints. builtin_patterns = [ - re.compile("(? Date: Wed, 1 Jul 2020 09:00:39 +0000 Subject: [PATCH 614/922] Change is_afk to afk --- hikari/impl/entity_factory.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/hikari/impl/entity_factory.py b/hikari/impl/entity_factory.py index a72e2f489e..cd6e0a243d 100644 --- a/hikari/impl/entity_factory.py +++ b/hikari/impl/entity_factory.py @@ -1841,7 +1841,7 @@ def deserialize_voice_server_update_event( def serialize_gateway_presence( self, idle_since: typing.Union[undefined.UndefinedType, typing.Optional[datetime.datetime]] = undefined.UNDEFINED, - is_afk: typing.Union[undefined.UndefinedType, bool] = undefined.UNDEFINED, + afk: typing.Union[undefined.UndefinedType, bool] = undefined.UNDEFINED, status: typing.Union[undefined.UndefinedType, presence_models.Status] = undefined.UNDEFINED, activity: typing.Union[ undefined.UndefinedType, typing.Optional[presence_models.Activity] @@ -1858,7 +1858,7 @@ def serialize_gateway_presence( payload = data_binding.JSONObjectBuilder() payload.put("since", idle_since, conversion=datetime.datetime.timestamp) - payload.put("afk", is_afk) + payload.put("afk", afk) payload.put("status", status) payload.put("game", game) return payload From 154760e698e166dbcd2347d54b2d99fafa0f29c8 Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Wed, 1 Jul 2020 20:08:16 +0100 Subject: [PATCH 615/922] Fixes #396, exports some stuff to the root 'hikari' module. --- .flake8 | 16 ++-- docs/documentation.mako | 134 +++++++++++++++++++----------- docs/head.mako | 4 +- docs/html.mako | 4 +- hikari/__init__.py | 49 ++++++++++- hikari/api/__init__.py | 2 +- hikari/api/bot.py | 2 +- hikari/api/cache.py | 2 +- hikari/api/component.py | 2 +- hikari/api/entity_factory.py | 2 +- hikari/api/event_consumer.py | 2 +- hikari/api/event_dispatcher.py | 2 +- hikari/api/gateway.py | 2 +- hikari/api/gateway_zookeeper.py | 2 +- hikari/api/rest_app.py | 2 +- hikari/api/rest_client.py | 2 +- hikari/api/special_endpoints.py | 2 +- hikari/api/voice.py | 2 +- hikari/config.py | 2 +- hikari/errors.py | 2 +- hikari/events/__init__.py | 20 ++++- hikari/events/base.py | 2 +- hikari/events/channel.py | 2 +- hikari/events/guild.py | 2 +- hikari/events/message.py | 2 +- hikari/events/other.py | 2 +- hikari/events/voice.py | 2 +- hikari/impl/__init__.py | 2 +- hikari/impl/bot.py | 2 +- hikari/impl/buckets.py | 2 +- hikari/impl/cache.py | 2 +- hikari/impl/constants.py | 2 +- hikari/impl/entity_factory.py | 2 +- hikari/impl/event_manager.py | 2 +- hikari/impl/event_manager_core.py | 2 +- hikari/impl/gateway.py | 2 +- hikari/impl/gateway_zookeeper.py | 2 +- hikari/impl/rate_limits.py | 2 +- hikari/impl/response_handler.py | 2 +- hikari/impl/rest_app.py | 2 +- hikari/impl/rest_client.py | 2 +- hikari/impl/routes.py | 2 +- hikari/impl/special_endpoints.py | 2 +- hikari/impl/stateless_cache.py | 2 +- hikari/models/__init__.py | 58 ++++++++++++- hikari/models/applications.py | 2 +- hikari/models/audit_logs.py | 2 +- hikari/models/channels.py | 2 +- hikari/models/colors.py | 2 +- hikari/models/colours.py | 2 +- hikari/models/embeds.py | 2 +- hikari/models/emojis.py | 2 +- hikari/models/gateway.py | 2 +- hikari/models/guilds.py | 2 +- hikari/models/intents.py | 2 +- hikari/models/invites.py | 2 +- hikari/models/messages.py | 2 +- hikari/models/permissions.py | 2 +- hikari/models/presences.py | 2 +- hikari/models/users.py | 2 +- hikari/models/voices.py | 2 +- hikari/models/webhooks.py | 2 +- hikari/utilities/__init__.py | 2 +- hikari/utilities/aio.py | 2 +- hikari/utilities/cdn.py | 2 +- hikari/utilities/data_binding.py | 2 +- hikari/utilities/date.py | 2 +- hikari/utilities/files.py | 1 - hikari/utilities/iterators.py | 2 +- hikari/utilities/reflect.py | 2 +- hikari/utilities/snowflake.py | 2 +- hikari/utilities/spel.py | 3 + hikari/utilities/undefined.py | 2 +- 73 files changed, 286 insertions(+), 131 deletions(-) diff --git a/.flake8 b/.flake8 index b8af0cfee7..c2bc9eac21 100644 --- a/.flake8 +++ b/.flake8 @@ -10,15 +10,17 @@ ignore = E402, # Module level import not at top of file (isn't compatible with our import style). IFSTMT001 # "use a oneliner here" T101, # TO DO comments only + W503, # line break before binary operator - +# F401: unused import. +# F403: cannot detect unused vars if we use starred import +# FS003: f-string missing prefix. per-file-ignores = - # f-string missing prefix. - hikari/impl/routes.py:FS003 - # f-string missing prefix. - hikari/utilities/date.py:FS003 - # complaints about importing stuff and not using it afterwards - hikari/__init__.py:F401 + hikari/__init__.py: F401,F403 + hikari/events/__init__.py: F401,F403 + hikari/models/__init__.py: F401,F403 + hikari/impl/routes.py: FS003 + hikari/utilities/date.py: FS003 max-complexity = 20 # Technically this is 120, but black has a policy of "1 or 2 over is fine if it is tidier", so we have to raise this. diff --git a/docs/documentation.mako b/docs/documentation.mako index 0a16d295de..1e5c77f1f2 100644 --- a/docs/documentation.mako +++ b/docs/documentation.mako @@ -3,6 +3,8 @@ typing.TYPE_CHECKING = True import builtins + import importlib + import inspect import re import sphobjinv @@ -33,7 +35,7 @@ fqn = fqn.replace("builtins.", "") if fqn not in located_external_refs: - print("attempting to find intersphinx reference for", fqn) + # print("attempting to find intersphinx reference for", fqn) for base_url, inv in inventories.items(): for obj in inv.values(): if isinstance(obj, dict) and obj["name"] == fqn: @@ -48,7 +50,7 @@ try: return located_external_refs[fqn] except KeyError: - print("blacklisting", fqn, "as it cannot be dereferenced from external documentation") + # print("blacklisting", fqn, "as it cannot be dereferenced from external documentation") unlocatable_external_refs.add(fqn) project_inventory = sphobjinv.Inventory() @@ -88,24 +90,29 @@ from pdoc.html_helpers import extract_toc, glimpse, to_html as _to_html, format_git_link - QUAL_ABC = "abstract" - QUAL_ASYNC_DEF = "async def" - QUAL_CLASS = "class" - QUAL_DATACLASS = "dataclass" - QUAL_CACHED_PROPERTY = "cached property" - QUAL_CONST = "const" - QUAL_DEF = "def" - QUAL_ENUM = "enum" - QUAL_ENUM_FLAG = "flag" - QUAL_EXTERNAL = "external" - QUAL_METACLASS = "metaclass" - QUAL_MODULE = "module" - QUAL_NAMESPACE = "namespace" - QUAL_PACKAGE = "package" - QUAL_PROPERTY = "property" - QUAL_TYPEHINT = "type hint" - QUAL_VAR = "var" - + QUAL_ABC = "abstract" + QUAL_ABSTRACT = "abstract" + QUAL_ASYNC_DEF = "async" + QUAL_CLASS = "class" + QUAL_DATACLASS = "dataclass" + QUAL_CACHED_PROPERTY = "cached property" + QUAL_CONST = "const" + QUAL_DEF = "def" + QUAL_ENUM = "enum" + QUAL_ENUM_FLAG = "enum flag" + QUAL_EXCEPTION = "exception" + QUAL_EXTERNAL = "extern" + QUAL_INTERFACE = "abstract trait" + QUAL_METACLASS = "meta" + QUAL_MODULE = "module" + QUAL_NAMESPACE = "namespace" + QUAL_PACKAGE = "package" + QUAL_PROPERTY = "property" + QUAL_TYPEHINT = "type hint" + QUAL_VAR = "var" + QUAL_WARNING = "warning" + + # Help, it is a monster! def link( dobj: pdoc.Doc, *, @@ -132,22 +139,28 @@ qual = dobj.funcdef() if getattr(dobj.obj, "__isabstractmethod__", False): - prefix = f"{QUAL_ABC} " + prefix = f"{QUAL_ABSTRACT} " prefix = "" + prefix + qual + " " elif isinstance(dobj, pdoc.Variable): if getattr(dobj.obj, "__isabstractmethod__", False): - prefix = f"{QUAL_ABC} " + prefix = f"{QUAL_ABSTRACT} " descriptor = None - is_descriptor = hasattr(dobj.cls, "obj") and (descriptor := dobj.cls.obj.__dict__.get(dobj.name)) + is_descriptor = False - if is_descriptor and isinstance(descriptor, (property, functools.cached_property)): + if hasattr(dobj.cls, "obj"): + for cls in dobj.cls.obj.mro(): + if (descriptor := cls.__dict__.get(dobj.name)) is not None: + is_descriptor = True + break + + if is_descriptor: qual = QUAL_CACHED_PROPERTY if isinstance(descriptor, functools.cached_property) else QUAL_PROPERTY prefix = f"{prefix}{qual} " elif dobj.module.name == "typing" or dobj.docstring and dobj.docstring.casefold().startswith(("type hint", "typehint", "type alias")): - show_object = True + show_object = not simple_names prefix = f"{prefix}{QUAL_TYPEHINT} " elif all(not c.isalpha() or c.isupper() for c in dobj.name): prefix = f"{prefix}{QUAL_CONST} " @@ -160,20 +173,24 @@ if issubclass(dobj.obj, type): qual += QUAL_METACLASS else: - if "__call__" in dobj.obj.__dict__: - name += "()" - if enum.Flag in dobj.obj.mro(): qual += QUAL_ENUM_FLAG elif enum.Enum in dobj.obj.mro(): qual += QUAL_ENUM elif hasattr(dobj.obj, "__attrs_attrs__"): qual += QUAL_DATACLASS + elif issubclass(dobj.obj, Warning): + qual += QUAL_WARNING + elif issubclass(dobj.obj, BaseException): + qual += QUAL_EXCEPTION else: qual += QUAL_CLASS if inspect.isabstract(dobj.obj): - qual = f"{QUAL_ABC} {qual}" + if re.match(r"^I[A-Za-z]", dobj.name): + qual = f"{QUAL_INTERFACE} {qual}" + else: + qual = f"{QUAL_ABC} {qual}" prefix = f"{qual} " @@ -206,25 +223,25 @@ if url is None: if fqn_match := re.match(r"([a-z_]+)\.((?:[^\.]|^\s)+)", fqn): - print("Struggling to resolve", fqn, "in", module.name, "so attempting to see if it is an import alias now instead.") + # print("Struggling to resolve", fqn, "in", module.name, "so attempting to see if it is an import alias now instead.") if import_match := re.search(f"from (.*) import (.*) as {fqn_match.group(1)}", module.source): old_fqn = fqn fqn = import_match.group(1) + "." + import_match.group(2) + "." + fqn_match.group(2) try: url = pdoc._global_context[fqn].url(relative_to=module, link_prefix=link_prefix, top_ancestor=not show_inherited_members) - print(old_fqn, "->", fqn, "via", url) + # print(old_fqn, "->", fqn, "via", url) except KeyError: - print("maybe", fqn, "is external but aliased?") + # print("maybe", fqn, "is external but aliased?") url = discover_source(fqn) elif import_match := re.search(f"import (.*) as {fqn_match.group(1)}", module.source): old_fqn = fqn fqn = import_match.group(1) + "." + fqn_match.group(2) try: url = pdoc._global_context[fqn].url(relative_to=module, link_prefix=link_prefix, top_ancestor=not show_inherited_members) - print(old_fqn, "->", fqn, "via", url) + # print(old_fqn, "->", fqn, "via", url) except KeyError: - print("maybe", fqn, "is external but aliased?") + # print("maybe", fqn, "is external but aliased?") url = discover_source(fqn) else: # print("No clue where", fqn, "came from --- it isn't an import that i can see.") @@ -276,8 +293,8 @@ if annot: annot = ' ' + sep + '\N{NBSP}' + annot - for pattern in builtin_patterns: - annot = pattern.sub(r"builtins.\1", annot) + # for pattern in builtin_patterns: + # annot = pattern.sub(r"builtins.\1", annot) return annot @@ -498,7 +515,7 @@ % for sc in subclasses: % if not isinstance(sc, pdoc.External):
${link(sc, with_prefixes=True, default_type="class")}
-
${sc.docstring | glimpse, to_html}
+
${sc.docstring or sc.obj.__doc__ | glimpse, to_html}
% endif % endfor @@ -511,6 +528,16 @@
${link(c, with_prefixes=True)}
That's this class!
% for mro_c in mro: + <% + if mro_c.obj is None: + module, _, cls = mro_c.qualname.rpartition(".") + try: + cls = getattr(importlib.import_module(module), cls) + mro_c.docstring = cls.__doc__ or "" + except: + pass + %> +
${link(mro_c, with_prefixes=True, default_type="class")}
${mro_c.docstring | glimpse, to_html}
% endfor @@ -544,7 +571,7 @@ <%def name="show_desc(d, short=False)"> <% inherits = ' inherited' if d.inherits else '' - docstring = d.docstring + docstring = d.docstring or d.obj.__doc__ %> % if not short: % if d.inherits: @@ -594,9 +621,9 @@
<% - variables = module.variables(sort=sort_identifiers) - classes = module.classes(sort=sort_identifiers) - functions = module.functions(sort=sort_identifiers) + variables = module.variables(sort=sort_identifiers and module.name != "hikari") + classes = module.classes(sort=sort_identifiers and module.name != "hikari") + functions = module.functions(sort=sort_identifiers and module.name != "hikari") submodules = module.submodules() supermodule = module.supermodule @@ -617,7 +644,7 @@ % if submodules:
    % for child_module in submodules: -
  • ${link(child_module, with_prefixes=True, css_classes="sidebar-nav-pill", dotted=False)}
  • +
  • ${link(child_module, with_prefixes=True, css_classes="sidebar-nav-pill", dotted=False, simple_names=True)}
  • % endfor
% endif @@ -629,7 +656,7 @@ % if variables:
    % for variable in variables: -
  • ${link(variable, with_prefixes=True, css_classes="sidebar-nav-pill", dotted=False)}
  • +
  • ${link(variable, with_prefixes=True, css_classes="sidebar-nav-pill", dotted=False, simple_names=True)}
  • % endfor
% endif @@ -637,7 +664,7 @@ % if functions:
    % for function in functions: -
  • ${link(function, with_prefixes=True, css_classes="sidebar-nav-pill", dotted=False)}
  • +
  • ${link(function, with_prefixes=True, css_classes="sidebar-nav-pill", dotted=False, simple_names=True)}
  • % endfor
% endif @@ -647,9 +674,16 @@ ## Purposely using one item per list for layout reasons.
  • - ${link(c, with_prefixes=True, css_classes="sidebar-nav-pill", dotted=False)} - <% + if c.module.name != c.obj.__module__: + try: + ref = pdoc._global_context[c.obj.__module__ + "." + c.obj.__qualname__] + redirect = True + except KeyError: + redirect = False + else: + redirect = False + members = c.functions(sort=sort_identifiers) + c.methods(sort=sort_identifiers) if list_class_variables_in_index: @@ -662,10 +696,14 @@ members = sorted(members) %> + ${link(c, with_prefixes=True, css_classes="sidebar-nav-pill", dotted=False, simple_names=True)} +
      - % if members: + % if members and not redirect: % for member in members: -
    • ${link(member, with_prefixes=True, css_classes="sidebar-nav-pill", dotted=False)}
    • +
    • + ${link(member, with_prefixes=True, css_classes="sidebar-nav-pill", dotted=False, simple_names=True)} +
    • % endfor % endif
    diff --git a/docs/head.mako b/docs/head.mako index c72a8d8727..486aca4435 100644 --- a/docs/head.mako +++ b/docs/head.mako @@ -15,12 +15,12 @@
-
+
From e7688b67458fb2959a32034d5430520c499b7108 Mon Sep 17 00:00:00 2001 From: davfsa Date: Mon, 3 Aug 2020 19:31:25 +0200 Subject: [PATCH 853/922] Fix background script --- pages/index.html | 53 ++++++++++++++++++++++++++++-------------------- 1 file changed, 31 insertions(+), 22 deletions(-) diff --git a/pages/index.html b/pages/index.html index 4e19336663..e702946c65 100644 --- a/pages/index.html +++ b/pages/index.html @@ -47,7 +47,7 @@ 888 888 888 888 `88b. d8( 888 888 888 CPython 3.8.5 o888o o888o o888o o888o o888o `Y888""8o d888b o888o compiled with GCC 10.1.0 (default May 17 2020 18:15:42) ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ glibc 2.31 on Linux 64bit ELF - v2.0.0 + v2.0.0 I 2020-07-15 16:45:11,476 hikari: single-sharded configuration -- you have started 29/1000 sessions prior to connecting (resets at 16/07/20 11:38:05 BST) I 2020-07-15 16:45:11,779 hikari.gateway.0: received HELLO, heartbeat interval is 41.25s @@ -62,49 +62,58 @@ import asyncio import functools import os - + import hikari - + bot = hikari.Bot(token=os.environ["BOT_TOKEN"]) - + + def command(name): def decorator(fn): @functools.wraps(fn) async def wrapper(event): if not event.message.author.is_bot and event.message.content.startswith(name): await fn(event) + return wrapper + return decorator - + + @bot.listen() @command("hk.ping") - async def ping(message: hikari.MessageCreateEvent) -> None: + async def ping(event: hikari.MessageCreateEvent) -> None: await event.message.reply("Pong!") - - @bot.event() + + + @bot.listen() @command("hk.add") async def add_numbers(event: hikari.MessageCreateEvent) -> None: - await event.message.reply("Please enter the first number") - def is_number_check(e): - return event.message.author == e.message.author and e.message.content.isdigit() - + return ( + event.message.author == e.message.author + and event.message.channel_id == e.message.channel_id + and e.message.content.isdigit() + ) + try: - e1 = await bot.wait_for(MessageCreateEvent, predicate=is_number_check, timeout=30) - await event.reply("Please enter the second number") - e2 = await bot.wait_for(MessageCreateEvent, predicate=is_number_check, timeout=30) - + await event.message.reply("Please enter the first number") + e1 = await bot.wait_for(hikari.MessageCreateEvent, predicate=is_number_check, timeout=30) + await event.message.reply("Please enter the second number") + e2 = await bot.wait_for(hikari.MessageCreateEvent, predicate=is_number_check, timeout=30) + except asyncio.TimeoutError: await event.message.reply("You took too long...") - + else: val1 = int(e1.message.content) val2 = int(e2.message.content) - - embed = hikari.Embed(title="Result!", description=f"{val1} + {val2}" color="#3f3") - + + embed = hikari.Embed(title="Result!", description=f"{val1} + {val2} = {val1 + val2}", color="#3f3") + await event.message.reply(embed=embed) - + + bot.run() @@ -177,4 +186,4 @@

- + \ No newline at end of file From c988cd399d5919afbec436a7fbdb349ed48748bd Mon Sep 17 00:00:00 2001 From: davfsa Date: Fri, 7 Aug 2020 15:33:47 +0200 Subject: [PATCH 854/922] Implemented guild chunking --- hikari/api/bot.py | 12 ++++ hikari/api/event_factory.py | 2 +- hikari/api/guild_chunker.py | 50 +++++++++++++++ hikari/api/shard.py | 43 +++++++++++++ hikari/events/guild_events.py | 74 ++++++++++++++++++++++ hikari/events/shard_events.py | 76 ----------------------- hikari/impl/bot.py | 12 ++++ hikari/impl/event_factory.py | 4 +- hikari/impl/shard.py | 39 +++++++++++- hikari/impl/stateful_event_manager.py | 5 +- hikari/impl/stateful_guild_chunker.py | 86 ++++++++++++++++++++++++++ hikari/impl/stateless_event_manager.py | 1 - hikari/impl/stateless_guild_chunker.py | 57 +++++++++++++++++ 13 files changed, 379 insertions(+), 82 deletions(-) create mode 100644 hikari/api/guild_chunker.py create mode 100644 hikari/impl/stateful_guild_chunker.py create mode 100644 hikari/impl/stateless_guild_chunker.py diff --git a/hikari/api/bot.py b/hikari/api/bot.py index 721f04d58b..549f29c4fb 100644 --- a/hikari/api/bot.py +++ b/hikari/api/bot.py @@ -33,6 +33,7 @@ if typing.TYPE_CHECKING: import datetime + from hikari.api import guild_chunker as guild_chunker_ from hikari.models import intents as intents_ from hikari.models import users @@ -50,6 +51,17 @@ class IBotApp(event_consumer.IEventConsumerApp, event_dispatcher.IEventDispatche __slots__: typing.Sequence[str] = () + @property + @abc.abstractmethod + def guild_chunker(self) -> guild_chunker_.IGuildChunkerComponent: + """Guild chunker. + + Returns + ------- + hikari.api.guild_chunker.IGuildChunkerComponent + The guild chunker implementation used in this application. + """ + @property @abc.abstractmethod def heartbeat_latencies(self) -> typing.Mapping[int, typing.Optional[datetime.timedelta]]: diff --git a/hikari/api/event_factory.py b/hikari/api/event_factory.py index 766a17c73e..b7c864bbbd 100644 --- a/hikari/api/event_factory.py +++ b/hikari/api/event_factory.py @@ -710,7 +710,7 @@ def deserialize_own_user_update_event( @abc.abstractmethod def deserialize_guild_member_chunk_event( self, shard: gateway_shard.IGatewayShard, payload: data_binding.JSONObject - ) -> shard_events.MemberChunkEvent: + ) -> guild_events.MemberChunkEvent: """Parse a raw payload from Discord into a member chunk event object. Parameters diff --git a/hikari/api/guild_chunker.py b/hikari/api/guild_chunker.py new file mode 100644 index 0000000000..35302f00c3 --- /dev/null +++ b/hikari/api/guild_chunker.py @@ -0,0 +1,50 @@ +# -*- coding: utf-8 -*- +# cython: language_level=3 +# Copyright © Nekoka.tt 2019-2020 +# +# This file is part of Hikari. +# +# Hikari is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Hikari is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Hikari. If not, see . +"""Component that provides the ability manage guild chunking.""" + +from __future__ import annotations + +__all__: typing.Final[typing.List[str]] = ["IGuildChunkerComponent"] + +import abc +import typing + +from hikari.api import component + +if typing.TYPE_CHECKING: + from hikari.models import guilds + + +class IGuildChunkerComponent(component.IComponent, abc.ABC): + """Component specialization that is used to manage guild chunking.""" + + __slots__: typing.Sequence[str] = () + + @abc.abstractmethod + async def request_guild_chunk(self, guild: guilds.Guild, shard_id: int) -> None: + """Request for a guild chunk. + + Parameters + ---------- + guild: hikari.models.guilds.Guild + The guild to request chunk for. + """ + + def close(self) -> None: + """Close the guild chunker.""" diff --git a/hikari/api/shard.py b/hikari/api/shard.py index fce89a4e96..27178b8309 100644 --- a/hikari/api/shard.py +++ b/hikari/api/shard.py @@ -38,6 +38,7 @@ from hikari.models import guilds from hikari.models import intents as intents_ from hikari.models import presences + from hikari.models import users from hikari.utilities import snowflake @@ -350,3 +351,45 @@ async def update_voice_state( If `builtins.True`, the bot will deafen itself in that voice channel. If `builtins.False`, then it will undeafen itself. """ + + @abc.abstractmethod + async def request_guild_members( + self, + guild: snowflake.SnowflakeishOr[guilds.PartialGuild], + *, + presences: undefined.UndefinedOr[bool] = undefined.UNDEFINED, + query: str = "", + limit: int = 0, + users: undefined.UndefinedOr[typing.Sequence[snowflake.SnowflakeishOr[users.User]]] = undefined.UNDEFINED, + nonce: undefined.UndefinedOr[str] = undefined.UNDEFINED, + ) -> None: + """Request for a guild chunk. + + Parameters + ---------- + guild: hikari.models.guilds.Guild + The guild to request chunk for. + presences: hikari.utilities.undefined.UndefinedOr[builtins.bool] + If specified, whether to request presences. + query: builtins.str + If not `builtins.None`, request the members which username starts with the string. + limit: builtins.int + Maximum number of members to send matching the query. + users: hikari.utilities.undefined.UndefinedOr[typing.Sequence[hikari.utilities.snowflake.SnowflakeishOr[hikari.models.users.User]]] + If specified, the users to request for. + nonce: hikari.utilities.undefined.UndefinedOr[builtins.str] + If specified, the nonce to be sent with guild chunks. + + !!! note + To request the full list of members, set `query` to `builtins.None` or `""` + (empty string) and `limit` to 0. + + Raises + ------ + ValueError + When trying to specify `users` with `query`/`limit`, if `limit` is not between + 0 and 100, both inclusive or if `users` length is over 100. + hikari.errors.MisingIntent + When trying to request presences without the `GUILD_MEMBERS` or when trying to + request the full list of members without `GUILD_PRESENCES`. + """ # noqa: E501 - Line too long diff --git a/hikari/events/guild_events.py b/hikari/events/guild_events.py index 5c84dfbc8b..dc19980433 100644 --- a/hikari/events/guild_events.py +++ b/hikari/events/guild_events.py @@ -32,6 +32,7 @@ "EmojisUpdateEvent", "IntegrationsUpdateEvent", "PresenceUpdateEvent", + "MemberChunkEvent", ] import abc @@ -395,3 +396,76 @@ def guild_id(self) -> snowflake.Snowflake: ID of the guild the event occurred in. """ return self.presence.guild_id + + +@attr.s(kw_only=True, slots=True, weakref_slot=False) +class MemberChunkEvent(shard_events.ShardEvent): + """Used to represent the response to Guild Request Members.""" + + shard: gateway_shard.IGatewayShard = attr.ib() + # <>. + + guild_id: snowflake.Snowflake = attr.ib(repr=True) + # <>. + + members: typing.Mapping[snowflake.Snowflake, guilds.Member] = attr.ib(repr=False) + """Mapping of user IDs to the objects of the members in this chunk. + + Returns + ------- + typing.Mapping[hikari.utilities.snowflake.Snowflake, hikari.models.guilds.Member] + Mapping of user IDs to corresponding member objects. + """ + + index: int = attr.ib(repr=True) + """Zero-indexed position of this within the queued up chunks for this request. + + Returns + ------- + builtins.int + The sequence index for this chunk. + """ + + count: int = attr.ib(repr=True) + """Total number of expected chunks for the request this is associated with. + + Returns + ------- + builtins.int + Total number of chunks to be expected. + """ + + not_found: typing.Sequence[snowflake.Snowflake] = attr.ib(repr=True) + """Sequence of the snowflakes that were not found while making this request. + + This is only applicable when user IDs are specified while making the + member request the chunk is associated with. + + Returns + ------- + typing.Sequence[hikari.utilities.snowflake.Snowflake] + Sequence of user IDs that were not found. + """ + + presences: typing.Mapping[snowflake.Snowflake, presences_.MemberPresence] = attr.ib(repr=False) + """Mapping of user IDs to found member presence objects. + + This will be empty if no presences are found or `presences` is not passed as + `True` while requesting the member chunks. + + Returns + ------- + typing.Mapping[hikari.utilities.snowflake.Snowflake, hikari.models.presences.MemberPresence] + Mapping of user IDs to corresponding presences. + """ + + nonce: typing.Optional[str] = attr.ib(repr=True) + """String nonce used to identify the request member chunks are associated with. + + This is the nonce value passed while requesting member chunks. + + Returns + ------- + builtins.str or builtins.None + The request nonce if specified, or `builtins.None` otherwise. + """ diff --git a/hikari/events/shard_events.py b/hikari/events/shard_events.py index 3f69f34217..d87807172b 100644 --- a/hikari/events/shard_events.py +++ b/hikari/events/shard_events.py @@ -27,7 +27,6 @@ "ShardDisconnectedEvent", "ShardReadyEvent", "ShardResumedEvent", - "MemberChunkEvent", ] import abc @@ -40,8 +39,6 @@ if typing.TYPE_CHECKING: from hikari.api import event_consumer from hikari.api import shard as gateway_shard - from hikari.models import guilds - from hikari.models import presences as presences_ from hikari.models import users from hikari.utilities import snowflake @@ -144,76 +141,3 @@ class ShardResumedEvent(ShardStateEvent): shard: gateway_shard.IGatewayShard = attr.ib() # <>. - - -@attr.s(kw_only=True, slots=True, weakref_slot=False) -class MemberChunkEvent(ShardEvent): - """Used to represent the response to Guild Request Members.""" - - shard: gateway_shard.IGatewayShard = attr.ib() - # <>. - - guild_id: snowflake.Snowflake = attr.ib(repr=True) - # <>. - - members: typing.Mapping[snowflake.Snowflake, guilds.Member] = attr.ib(repr=False) - """Mapping of user IDs to the objects of the members in this chunk. - - Returns - ------- - typing.Mapping[hikari.utilities.snowflake.Snowflake, hikari.models.guilds.Member] - Mapping of user IDs to corresponding member objects. - """ - - index: int = attr.ib(repr=True) - """Zero-indexed position of this within the queued up chunks for this request. - - Returns - ------- - builtins.int - The sequence index for this chunk. - """ - - count: int = attr.ib(repr=True) - """Total number of expected chunks for the request this is associated with. - - Returns - ------- - builtins.int - Total number of chunks to be expected. - """ - - not_found: typing.Sequence[snowflake.Snowflake] = attr.ib(repr=True) - """Sequence of the snowflakes that were not found while making this request. - - This is only applicable when user IDs are specified while making the - member request the chunk is associated with. - - Returns - ------- - typing.Sequence[hikari.utilities.snowflake.Snowflake] - Sequence of user IDs that were not found. - """ - - presences: typing.Mapping[snowflake.Snowflake, presences_.MemberPresence] = attr.ib(repr=False) - """Mapping of user IDs to found member presence objects. - - This will be empty if no presences are found or `presences` is not passed as - `True` while requesting the member chunks. - - Returns - ------- - typing.Mapping[hikari.utilities.snowflake.Snowflake, hikari.models.presences.MemberPresence] - Mapping of user IDs to corresponding presences. - """ - - nonce: typing.Optional[str] = attr.ib(repr=True) - """String nonce used to identify the request member chunks are associated with. - - This is the nonce value passed while requesting member chunks. - - Returns - ------- - builtins.str or builtins.None - The request nonce if specified, or `builtins.None` otherwise. - """ diff --git a/hikari/impl/bot.py b/hikari/impl/bot.py index efe9c66160..521ffc9b3a 100644 --- a/hikari/impl/bot.py +++ b/hikari/impl/bot.py @@ -47,8 +47,10 @@ from hikari.impl import shard as gateway_shard_impl from hikari.impl import stateful_cache as cache_impl from hikari.impl import stateful_event_manager +from hikari.impl import stateful_guild_chunker as guild_chunker_impl from hikari.impl import stateless_cache as stateless_cache_impl from hikari.impl import stateless_event_manager +from hikari.impl import stateless_guild_chunker as stateless_guild_chunker_impl from hikari.impl import voice from hikari.models import intents as intents_ from hikari.models import presences @@ -62,6 +64,7 @@ from hikari.api import cache as cache_ from hikari.api import event_consumer as event_consumer_ from hikari.api import event_dispatcher as event_dispatcher_ + from hikari.api import guild_chunker as guild_chunker_ from hikari.events import base_events from hikari.impl import event_manager_base from hikari.models import users @@ -245,6 +248,7 @@ class BotAppImpl(bot.IBotApp): __slots__: typing.Sequence[str] = ( "_cache", + "_guild_chunker", "_connector_factory", "_debug", "_entity_factory", @@ -312,15 +316,18 @@ def __init__( self._cache: cache_.IMutableCacheComponent self._event_manager: event_manager_base.EventManagerComponentBase + self._guild_chunker: guild_chunker_.IGuildChunkerComponent if stateless: self._cache = stateless_cache_impl.StatelessCacheImpl(app=self) + self._guild_chunker = stateless_guild_chunker_impl.StatelessGuildChunkerImpl(app=self) self._event_manager = stateless_event_manager.StatelessEventManagerImpl( app=self, mutable_cache=self._cache, intents=intents, ) _LOGGER.info("this application is stateless, cache-based operations will not be available") else: self._cache = cache_impl.StatefulCacheImpl(app=self, intents=intents) + self._guild_chunker = guild_chunker_impl.StatefulGuildChunkerImpl(app=self, intents=intents) self._event_manager = stateful_event_manager.StatefulEventManagerImpl( app=self, intents=intents, mutable_cache=self._cache, ) @@ -379,6 +386,10 @@ def __del__(self) -> None: def cache(self) -> cache_.ICacheComponent: return self._cache + @property + def guild_chunker(self) -> guild_chunker_.IGuildChunkerComponent: + return self._guild_chunker + @property def is_debug_enabled(self) -> bool: return self._debug @@ -597,6 +608,7 @@ def dispatch(self, event: base_events.Event) -> asyncio.Future[typing.Any]: async def close(self) -> None: await self._rest.close() await self._connector_factory.close() + self._guild_chunker.close() self._global_ratelimit.close() if self._tasks: diff --git a/hikari/impl/event_factory.py b/hikari/impl/event_factory.py index d0db019ecf..272ade7203 100644 --- a/hikari/impl/event_factory.py +++ b/hikari/impl/event_factory.py @@ -469,7 +469,7 @@ def deserialize_own_user_update_event( def deserialize_guild_member_chunk_event( self, shard: gateway_shard.IGatewayShard, payload: data_binding.JSONObject - ) -> shard_events.MemberChunkEvent: + ) -> guild_events.MemberChunkEvent: guild_id = snowflake.Snowflake(payload["guild_id"]) index = int(payload["chunk_index"]) count = int(payload["chunk_count"]) @@ -492,7 +492,7 @@ def deserialize_guild_member_chunk_event( else: presences = {} - return shard_events.MemberChunkEvent( + return guild_events.MemberChunkEvent( shard=shard, guild_id=guild_id, members=members, diff --git a/hikari/impl/shard.py b/hikari/impl/shard.py index 3d6cfbde43..2154973c63 100644 --- a/hikari/impl/shard.py +++ b/hikari/impl/shard.py @@ -37,6 +37,7 @@ from hikari import errors from hikari.api import shard from hikari.impl import rate_limits +from hikari.models import intents as intents_ from hikari.models import presences from hikari.utilities import constants from hikari.utilities import data_binding @@ -49,7 +50,7 @@ from hikari.api import event_consumer from hikari.models import channels from hikari.models import guilds - from hikari.models import intents as intents_ + from hikari.models import users @typing.final @@ -434,6 +435,42 @@ async def update_voice_state( payload = self._app.event_factory.serialize_gateway_voice_state_update(guild, channel, self_mute, self_deaf) await self._send_json({"op": self._Opcode.VOICE_STATE_UPDATE, "d": payload}) + async def request_guild_members( + self, + guild: snowflake.SnowflakeishOr[guilds.PartialGuild], + *, + presences: undefined.UndefinedOr[bool] = undefined.UNDEFINED, + query: str = "", + limit: int = 0, + users: undefined.UndefinedOr[typing.Sequence[snowflake.SnowflakeishOr[users.User]]] = undefined.UNDEFINED, + nonce: undefined.UndefinedOr[str] = undefined.UNDEFINED, + ) -> None: + if self._intents is not None: + if not query and not limit and not self._intents & intents_.Intent.GUILD_MEMBERS: + raise errors.MissingIntentError(intents_.Intent.GUILD_MEMBERS) + + if presences is not undefined.UNDEFINED and not self._intents & intents_.Intent.GUILD_PRESENCES: + raise errors.MissingIntentError(intents_.Intent.GUILD_PRESENCES) + + if users is not undefined.UNDEFINED and query or limit: + raise ValueError("Cannot specify limit/query with users") + + if not 0 <= limit <= 100: + raise ValueError("'limit' must be between 0 and 100, both inclusive") + + if users is not undefined.UNDEFINED and len(users) >= 100: + raise ValueError("'users' is limited to 100 users") + + payload = data_binding.JSONObjectBuilder() + payload.put_snowflake("guild_id", guild) + payload.put("presences", presences) + payload.put("query", query) + payload.put("limit", limit) + payload.put_snowflake_array("user_ids", users) + payload.put("nonce", nonce) + + await self._send_json({"op": self._Opcode.REQUEST_GUILD_MEMBERS, "d": payload}) + async def _run(self) -> None: """Start the shard and wait for it to shut down.""" async with aiohttp.ClientSession( diff --git a/hikari/impl/stateful_event_manager.py b/hikari/impl/stateful_event_manager.py index 30cc0f1420..5effab2d09 100644 --- a/hikari/impl/stateful_event_manager.py +++ b/hikari/impl/stateful_event_manager.py @@ -27,6 +27,7 @@ from hikari.events import shard_events from hikari.impl import event_manager_base from hikari.models import channels +from hikari.models import intents from hikari.models import presences if typing.TYPE_CHECKING: @@ -142,6 +143,9 @@ async def on_guild_create(self, shard: gateway_shard.IGatewayShard, payload: dat for voice_state in event.voice_states.values(): self._mutable_cache.set_voice_state(voice_state) + if event.guild.is_large and (self._intents is None or self._intents & intents.Intent.GUILD_MEMBERS): + await self._app.guild_chunker.request_guild_chunk(event.guild, shard.id) + await self.dispatch(event) async def on_guild_update(self, shard: gateway_shard.IGatewayShard, payload: data_binding.JSONObject) -> None: @@ -235,7 +239,6 @@ async def on_guild_members_chunk( self, shard: gateway_shard.IGatewayShard, payload: data_binding.JSONObject ) -> None: """See https://discord.com/developers/docs/topics/gateway#guild-members-chunk for more info.""" - # TODO: implement chunking components. event = self.app.event_factory.deserialize_guild_member_chunk_event(shard, payload) for member in event.members.values(): diff --git a/hikari/impl/stateful_guild_chunker.py b/hikari/impl/stateful_guild_chunker.py new file mode 100644 index 0000000000..1ee0804acb --- /dev/null +++ b/hikari/impl/stateful_guild_chunker.py @@ -0,0 +1,86 @@ +# -*- coding: utf-8 -*- +# cython: language_level=3 +# Copyright © Nekoka.tt 2019-2020 +# +# This file is part of Hikari. +# +# Hikari is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Hikari is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Hikari. If not, see . +"""Basic implementation of a guild chunker.""" + +from __future__ import annotations + +__all__: typing.Final[typing.List[str]] = ["StatefulGuildChunkerImpl"] + +import asyncio +import logging +import typing + +from hikari.api import guild_chunker +from hikari.impl import rate_limits +from hikari.models import intents as intents_ + +if typing.TYPE_CHECKING: + from hikari.api import bot + from hikari.models import guilds + +_LOGGER = logging.getLogger("hikari.guild_chunker") + + +class StatefulGuildChunkerImpl(guild_chunker.IGuildChunkerComponent): + """Guild chunker implementation.""" + + __slots__: typing.Sequence[str] = ("_app", "_presences", "_queues", "_chunkers") + + def __init__(self, app: bot.IBotApp, intents: typing.Optional[intents_.Intent]): + self._app = app + self._presences: bool = intents is None or bool(intents & intents_.Intent.GUILD_PRESENCES) + self._queues: typing.Dict[int, typing.List[int]] = {} + self._chunkers: typing.Dict[int, asyncio.Task[None]] = {} + + @property + @typing.final + def app(self) -> bot.IBotApp: + return self._app + + async def request_guild_chunk(self, guild: guilds.Guild, shard_id: int) -> None: + if shard_id not in self._queues: + self._queues[shard_id] = [] + + self._queues[shard_id].append(guild.id) + + if shard_id not in self._chunkers: + task = asyncio.create_task(self._request_chunk(shard_id)) + self._chunkers[shard_id] = task + + async def _request_chunk(self, shard_id: int) -> None: + # Since this is not an endpoint but a request, we dont get the ratelimit info + # to go off from. This will allow 60 requests per 60 seconds, which should be + # a reasonable ratelimit. This will also leave 60 more requests per 60 seconds + # for other shard requests if this were to be exausted. + with rate_limits.WindowedBurstRateLimiter(f"chunking guilds on shard {shard_id}", 60, 60) as limit: + while len(self._queues[shard_id]) != 0: + await limit.acquire() + guild_id = self._queues[shard_id].pop() + message = "requesting guild members for guild %s on shard %s" + if self._presences: + message = f"{message} with presences" + _LOGGER.debug(message, guild_id, shard_id) + await self._app.shards[shard_id].request_guild_members(guild_id, presences=self._presences) + + del self._chunkers[shard_id] + + def close(self) -> None: + while self._chunkers: + _, future = self._chunkers.popitem() + future.cancel() diff --git a/hikari/impl/stateless_event_manager.py b/hikari/impl/stateless_event_manager.py index 343482c1c1..dc71f86762 100644 --- a/hikari/impl/stateless_event_manager.py +++ b/hikari/impl/stateless_event_manager.py @@ -140,7 +140,6 @@ async def on_guild_members_chunk( self, shard: gateway_shard.IGatewayShard, payload: data_binding.JSONObject ) -> None: """See https://discord.com/developers/docs/topics/gateway#guild-members-chunk for more info.""" - # TODO: implement chunking components. await self.dispatch(self.app.event_factory.deserialize_guild_member_chunk_event(shard, payload)) async def on_guild_role_create(self, shard: gateway_shard.IGatewayShard, payload: data_binding.JSONObject) -> None: diff --git a/hikari/impl/stateless_guild_chunker.py b/hikari/impl/stateless_guild_chunker.py new file mode 100644 index 0000000000..36b356cb73 --- /dev/null +++ b/hikari/impl/stateless_guild_chunker.py @@ -0,0 +1,57 @@ +# -*- coding: utf-8 -*- +# cython: language_level=3 +# Copyright © Nekoka.tt 2019-2020 +# +# This file is part of Hikari. +# +# Hikari is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Hikari is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Hikari. If not, see . +"""Basic implementation of a guild chunker.""" + +from __future__ import annotations + +__all__: typing.Final[typing.List[str]] = ["StatelessGuildChunkerImpl"] + +import typing + +from hikari.api import guild_chunker + +if typing.TYPE_CHECKING: + from hikari.api import bot + from hikari.models import guilds + + +class StatelessGuildChunkerImpl(guild_chunker.IGuildChunkerComponent): + """Stateless guild chunker. + + A stateless guild chunker implementation that implements dummy operations + for each of the required attributes of a functional guild chunker + implementation. Any methods will always raise `builtins.NotImplemented` + when being invoked. + """ + + __slots__: typing.Sequence[str] = ("_app") + + def __init__(self, app: bot.IBotApp) -> None: + self._app = app + + @property + @typing.final + def app(self) -> bot.IBotApp: + return self._app + + async def request_guild_chunk(self, guild: guilds.Guild, shard_id: int) -> None: + raise NotImplementedError("This application is stateless, guild chunking operations are not implemented.") + + def close(self) -> None: + return From 8b4658dca32eb5f47aa2f320e3bf2acb0f6f5ff8 Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Fri, 7 Aug 2020 15:04:01 +0100 Subject: [PATCH 855/922] Made banner and version API public, added git refs to versioning. --- ci/deploy.nox.py | 21 +++ coverage.ini | 1 + hikari/_about.py | 3 + hikari/banner.txt | 2 +- hikari/cli.py | 23 +++- hikari/impl/bot.py | 135 +------------------ hikari/utilities/art.py | 199 ++++++++++++++++++++++++++++ hikari/utilities/version_sniffer.py | 49 ++++++- pages/index.html | 36 ++--- 9 files changed, 312 insertions(+), 157 deletions(-) create mode 100644 hikari/utilities/art.py diff --git a/ci/deploy.nox.py b/ci/deploy.nox.py index beca8b1585..6914d96684 100644 --- a/ci/deploy.nox.py +++ b/ci/deploy.nox.py @@ -20,6 +20,7 @@ import os import re import shlex +import subprocess from distutils.version import LooseVersion from ci import config @@ -27,8 +28,28 @@ def update_version_string(version): + git_sha1 = subprocess.check_output( + ["git", "rev-parse", "HEAD"], universal_newlines=True, stderr=subprocess.DEVNULL, + )[:8] + + git_branch = subprocess.check_output( + ["git", "rev-parse", "--abbrev-ref", "HEAD"], universal_newlines=True, stderr=subprocess.DEVNULL, + )[:-1] + + git_when = subprocess.check_output( + ["git", "log", "-1", '-date=format:"%Y/%m/%d"', '--format="%ad"'], + universal_newlines=True, + stderr=subprocess.DEVNULL, + )[:-1] + print("Updating version in version file to", version) nox.shell("sed", shlex.quote(f's|^__version__.*|__version__ = "{version}"|g'), "-i", config.VERSION_FILE) + print("Updating branch in version file to", git_branch) + nox.shell("sed", shlex.quote(f's|^__git_branch__.*|__git_branch__ = "{git_branch}"|g'), "-i", config.VERSION_FILE) + print("Updating sha1 in version file to", git_sha1) + nox.shell("sed", shlex.quote(f's|^__git_sha1__.*|__git_sha1__ = "{git_sha1}"|g'), "-i", config.VERSION_FILE) + print("Updating timestamp in version file to", git_when) + nox.shell("sed", shlex.quote(f's|^__git_when__.*|__git_when__ = "{git_when}"|g'), "-i", config.VERSION_FILE) def set_official_release_flag(value: bool): diff --git a/coverage.ini b/coverage.ini index 49cad85b22..7fb6f3be9b 100644 --- a/coverage.ini +++ b/coverage.ini @@ -8,6 +8,7 @@ omit = hikari/__main__.py hikari/cli.py hikari/_about.py + hikari/utilities/art.py .nox/* [report] diff --git a/hikari/_about.py b/hikari/_about.py index 1990d4565f..660a6c0f62 100644 --- a/hikari/_about.py +++ b/hikari/_about.py @@ -34,3 +34,6 @@ __license__ = "LGPL-3.0-ONLY" __url__ = "https://gitlab.com/nekokatt/hikari" __version__ = "2.0.0.dev0" +__git_branch__ = "development" +__git_sha1__ = "HEAD" +__git_when__ = "2020/07/06" diff --git a/hikari/banner.txt b/hikari/banner.txt index 7f900d60a1..c0ec3b97f9 100644 --- a/hikari/banner.txt +++ b/hikari/banner.txt @@ -7,4 +7,4 @@ ${cyan} 888 888 888 888 `88b. d8( 888 888 888 ${default}${bright}${python_implementation} ${python_version} ${magenta}o888o o888o o888o o888o o888o `Y888""8o d888b o888o ${default}compiled with ${python_compiler} (${python_build}) ${white} ▝▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▘ ${default}${libc_version} on ${platform_system} ${platform_architecture} - ${bright}${white}${framed} v${hikari_version} ${default} + ${bright}${white}${framed} v${hikari_version} ${hikari_git_branch}-${hikari_git_sha1}-${hikari_git_when} ${default} diff --git a/hikari/cli.py b/hikari/cli.py index 72e6f0d91e..9655154c75 100644 --- a/hikari/cli.py +++ b/hikari/cli.py @@ -26,15 +26,24 @@ import typing from hikari import _about +from hikari.utilities import art def main() -> None: """Print package info and exit.""" # noinspection PyTypeChecker - sourcefile = typing.cast(str, inspect.getsourcefile(_about)) - path: typing.Final[str] = os.path.abspath(os.path.dirname(sourcefile)) - version: typing.Final[str] = _about.__version__ - py_impl: typing.Final[str] = platform.python_implementation() - py_ver: typing.Final[str] = platform.python_version() - py_compiler: typing.Final[str] = platform.python_compiler() - sys.stderr.write(f"hikari v{version} (installed in {path}) ({py_impl} {py_ver} {py_compiler})\n") + if "--pretty" in sys.argv[1:] or "-p" in sys.argv[1:]: + sys.stdout.write(art.get_banner() + "\n") + else: + sourcefile = typing.cast(str, inspect.getsourcefile(_about)) + path: typing.Final[str] = os.path.abspath(os.path.dirname(sourcefile)) + branch: typing.Final[str] = _about.__git_branch__ + sha1: typing.Final[str] = _about.__git_sha1__ + date: typing.Final[str] = _about.__git_when__ + version: typing.Final[str] = _about.__version__ + py_impl: typing.Final[str] = platform.python_implementation() + py_ver: typing.Final[str] = platform.python_version() + py_compiler: typing.Final[str] = platform.python_compiler() + sys.stderr.write(f"hikari v{version} {branch}@{sha1}, released on {date}\n") + sys.stderr.write(f"located at {path}\n") + sys.stderr.write(f"{py_impl} {py_ver} {py_compiler}\n") diff --git a/hikari/impl/bot.py b/hikari/impl/bot.py index 521ffc9b3a..48b7753503 100644 --- a/hikari/impl/bot.py +++ b/hikari/impl/bot.py @@ -26,12 +26,10 @@ import contextlib import datetime import logging -import os import reprlib import signal import sys import time -import types import typing import warnings @@ -54,9 +52,11 @@ from hikari.impl import voice from hikari.models import intents as intents_ from hikari.models import presences +from hikari.utilities import art from hikari.utilities import constants from hikari.utilities import date from hikari.utilities import undefined +from hikari.utilities import version_sniffer if typing.TYPE_CHECKING: import concurrent.futures @@ -72,63 +72,6 @@ _LOGGER: typing.Final[logging.Logger] = logging.getLogger("hikari") -def _determine_palette() -> typing.Mapping[str, str]: # pragma: no cover - # Modified from - # https://github.com/django/django/blob/master/django/core/management/color.py - _plat = sys.platform - _supports_color = False - - # isatty is not always implemented, https://code.djangoproject.com/ticket/6223 - _is_a_tty = hasattr(sys.stdout, "isatty") and sys.stdout.isatty() - - if _plat != "Pocket PC": - if _plat == "win32": - _supports_color |= os.getenv("TERM_PROGRAM", None) == "mintty" - _supports_color |= "ANSICON" in os.environ - _supports_color &= _is_a_tty - else: - _supports_color = _is_a_tty - - _supports_color |= bool(os.getenv("PYCHARM_HOSTED", "")) - - _palette_base: typing.Mapping[str, str] = types.MappingProxyType( - { - "default": "\033[0m", - "bright": "\033[1m", - "underline": "\033[4m", - "invert": "\033[7m", - "red": "\033[31m", - "green": "\033[32m", - "yellow": "\033[33m", - "blue": "\033[34m", - "magenta": "\033[35m", - "cyan": "\033[36m", - "white": "\033[37m", - "bright_red": "\033[91m", - "bright_green": "\033[92m", - "bright_yellow": "\033[93m", - "bright_blue": "\033[94m", - "bright_magenta": "\033[95m", - "bright_cyan": "\033[96m", - "bright_white": "\033[97m", - "framed": "\033[51m", - "dim": "\033[2m", - } - ) - - if not _supports_color: - _palette_base = types.MappingProxyType({k: "" for k in _palette_base}) - - return _palette_base - - -_PALETTE: typing.Final[typing.Mapping[str, str]] = _determine_palette() -_LOGGING_FORMAT: typing.Final[str] = ( - "{red}%(levelname)-1.1s{default} {yellow}%(asctime)23.23s" # noqa: FS002, FS003 - "{default} {bright}{green}%(name)s: {default}{cyan}%(message)s{default}" # noqa: FS002, FS003 -).format(**_PALETTE) - - class BotAppImpl(bot.IBotApp): """Implementation of an auto-sharded single-instance bot application. @@ -308,7 +251,7 @@ def __init__( if logging_level is not None and not _LOGGER.hasHandlers(): logging.captureWarnings(True) - logging.basicConfig(format=_LOGGING_FORMAT) + logging.basicConfig(format=art.get_default_logging_format()) logging.root.setLevel(logging_level) if banner_package is not None: @@ -477,7 +420,7 @@ def uptime(self) -> datetime.timedelta: return datetime.timedelta(seconds=raw_uptime) async def start(self) -> None: - await self._check_for_updates() + asyncio.create_task(version_sniffer.log_available_updates(_LOGGER), name="check for hikari library updates") self._start_count += 1 self._started_at_monotonic = date.monotonic() @@ -773,75 +716,7 @@ def _map_signal_handlers( @staticmethod def _dump_banner(banner_package: str) -> None: - import platform - import string - from importlib import resources - - from hikari import _about - - args = { - # Colours: - "" - # Hikari stuff. - "hikari_version": _about.__version__, - "hikari_copyright": _about.__copyright__, - "hikari_license": _about.__license__, - "hikari_install_location": os.path.abspath(os.path.dirname(_about.__file__)), - "hikari_documentation_url": _about.__docs__, - "hikari_discord_invite": _about.__discord_invite__, - "hikari_source_url": _about.__url__, - # Python stuff. - "python_implementation": platform.python_implementation(), - "python_version": platform.python_version(), - "python_build": " ".join(platform.python_build()), - "python_branch": platform.python_branch(), - "python_compiler": platform.python_compiler(), - # Platform specific stuff I might remove later. - "libc_version": " ".join(platform.libc_ver()), - # System stuff. - "platform_system": platform.system(), - "platform_architecture": " ".join(platform.architecture()), - } - - args.update(**_PALETTE) - - with resources.open_text(banner_package, "banner.txt") as banner_fp: - banner = string.Template(banner_fp.read()).safe_substitute(args) - - sys.stdout.write(banner + "\n") + sys.stdout.write(art.get_banner(banner_package) + "\n") sys.stdout.flush() # Give the TTY time to flush properly. time.sleep(0.05) - - @staticmethod - async def _check_for_updates() -> None: - from hikari import _about - from hikari.utilities import version_sniffer - - if not _about.__is_official_distributed_release__: - # If we are on a non-released version, it could be modified or a - # fork, so don't do any checks. - return - - try: - version_info = await version_sniffer.fetch_version_info_from_pypi() - - if version_info.this == version_info.latest: - _LOGGER.info("package is up to date!") - else: - if version_info.this != version_info.latest_compatible: - _LOGGER.warning( - "non-breaking updates are available for hikari, update from v%s to v%s!", - version_info.this, - version_info.latest_compatible, - ) - - if version_info.latest != version_info.latest_compatible: - _LOGGER.info( - "breaking updates are available for hikari, consider upgrading from v%s to v%s!", - version_info.this, - version_info.latest, - ) - - except Exception as ex: - _LOGGER.debug("Error occurred fetching version info", exc_info=ex) diff --git a/hikari/utilities/art.py b/hikari/utilities/art.py new file mode 100644 index 0000000000..ddae2aeba4 --- /dev/null +++ b/hikari/utilities/art.py @@ -0,0 +1,199 @@ +# -*- coding: utf-8 -*- +# Copyright © Nekoka.tt 2019-2020 +# +# This file is part of Hikari. +# +# Hikari is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Hikari is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Hikari. If not, see . +"""Stuff for handling banners and pretty logging.""" +from __future__ import annotations + +__all__: typing.List[str] = [ + "ConsolePalette", + "DEFAULT_PALETTE", + "DEFAULT_BANNER_ARGS", + "get_default_logging_format", + "get_banner", +] + +import os +import platform +import string +import sys +import types +import typing +from importlib import resources + +import attr + +from hikari import _about + + +@typing.final +@attr.s(frozen=True, kw_only=True) +class ConsolePalette: + """Data class containing printable escape codes for colouring console output.""" + + default: str = attr.ib(default="") + bright: str = attr.ib(default="") + underline: str = attr.ib(default="") + invert: str = attr.ib(default="") + red: str = attr.ib(default="") + green: str = attr.ib(default="") + yellow: str = attr.ib(default="") + blue: str = attr.ib(default="") + magenta: str = attr.ib(default="") + cyan: str = attr.ib(default="") + white: str = attr.ib(default="") + bright_red: str = attr.ib(default="") + bright_green: str = attr.ib(default="") + bright_yellow: str = attr.ib(default="") + bright_blue: str = attr.ib(default="") + bright_magenta: str = attr.ib(default="") + bright_cyan: str = attr.ib(default="") + bright_white: str = attr.ib(default="") + framed: str = attr.ib(default="") + dim: str = attr.ib(default="") + + +def _default_palette() -> ConsolePalette: # pragma: no cover + # Modified from + # https://github.com/django/django/blob/master/django/core/management/color.py + _plat = sys.platform + _supports_color = False + + # isatty is not always implemented, https://code.djangoproject.com/ticket/6223 + _is_a_tty = hasattr(sys.stdout, "isatty") and sys.stdout.isatty() + + if _plat != "Pocket PC": + if _plat == "win32": + _supports_color |= os.getenv("TERM_PROGRAM", None) == "mintty" + _supports_color |= "ANSICON" in os.environ + _supports_color &= _is_a_tty + else: + _supports_color = _is_a_tty + + _supports_color |= bool(os.getenv("PYCHARM_HOSTED", "")) + + if _supports_color: + return ConsolePalette( + default="\033[0m", + bright="\033[1m", + underline="\033[4m", + invert="\033[7m", + red="\033[31m", + green="\033[32m", + yellow="\033[33m", + blue="\033[34m", + magenta="\033[35m", + cyan="\033[36m", + white="\033[37m", + bright_red="\033[91m", + bright_green="\033[92m", + bright_yellow="\033[93m", + bright_blue="\033[94m", + bright_magenta="\033[95m", + bright_cyan="\033[96m", + bright_white="\033[97m", + framed="\033[51m", + dim="\033[2m", + ) + + return ConsolePalette() + + +DEFAULT_PALETTE: typing.Final[ConsolePalette] = _default_palette() +"""Contains a set of constant escape codes that are able to be printed. + +These codes will force the console to change colour or style, if supported. + +On unsupported platforms, these will be empty strings, thus making them safe +to be used on non-coloured terminals or in logs specifically. +""" + + +def get_default_logging_format(palette: ConsolePalette = DEFAULT_PALETTE) -> str: + """Generate the default library logger format string. + + Parameters + ---------- + palette : ConsolePalette + The custom palette to use. Defaults to sane environment-dependent + ANSI colour-codes if not specified. + + Returns + ------- + builtins.str + The string logging console format. + """ + return ( + f"{palette.red}%(levelname)-1.1s{palette.default} {palette.yellow}%(asctime)23.23s" + f"{palette.default} {palette.bright}{palette.green}%(name)s: {palette.default}{palette.cyan}%(message)s" + f"{palette.default}" + ) + + +DEFAULT_BANNER_ARGS: typing.Final[typing.Mapping[str, str]] = types.MappingProxyType( + { + # Hikari stuff. + "hikari_version": _about.__version__, + "hikari_git_branch": _about.__git_branch__, + "hikari_git_sha1": _about.__git_sha1__, + "hikari_git_when": _about.__git_when__, + "hikari_copyright": _about.__copyright__, + "hikari_license": _about.__license__, + "hikari_install_location": os.path.abspath(os.path.dirname(_about.__file__)), + "hikari_documentation_url": _about.__docs__, + "hikari_discord_invite": _about.__discord_invite__, + "hikari_source_url": _about.__url__, + # Python stuff. + "python_implementation": platform.python_implementation(), + "python_version": platform.python_version(), + "python_build": " ".join(platform.python_build()), + "python_branch": platform.python_branch(), + "python_compiler": platform.python_compiler(), + # Platform specific stuff I might remove later. + "libc_version": " ".join(platform.libc_ver()), + # System stuff. + "platform_system": platform.system(), + "platform_architecture": " ".join(platform.architecture()), + } +) + + +def get_banner( + package: str = "hikari", + palette: ConsolePalette = DEFAULT_PALETTE, + args: typing.Mapping[str, str] = DEFAULT_BANNER_ARGS, +) -> str: + """Attempt to read a banner.txt from the given package. + + Parameters + ---------- + package : builtins.str + The package to read the banner.txt from. Defaults to `hikari`. + palette : ConsolePalette + The console palette to use (defaults to sane ANSI colour defaults or + empty-strings if colours are not supported by your TTY. + args : typing.Mapping[builtins.str, builtins.str] + The mapping of arguments to interpolate into the banner, if desired. + + Returns + ------- + builtins.str + The raw banner that can be printed to the console. + """ + params = {**attr.asdict(palette), **args} + + with resources.open_text(package, "banner.txt") as banner_fp: + return string.Template(banner_fp.read()).safe_substitute(params) diff --git a/hikari/utilities/version_sniffer.py b/hikari/utilities/version_sniffer.py index 81c348d4cb..7c2dfe1a88 100644 --- a/hikari/utilities/version_sniffer.py +++ b/hikari/utilities/version_sniffer.py @@ -20,8 +20,13 @@ from __future__ import annotations -__all__: typing.Final[typing.List[str]] = [] +__all__: typing.Final[typing.List[str]] = [ + "VersionInfo", + "fetch_version_info_from_pypi", + "log_available_updates", +] +import logging import typing from distutils import version as distutils_version @@ -103,3 +108,45 @@ async def fetch_version_info_from_pypi() -> VersionInfo: latest_compatible = max(same_compatible_releases) return VersionInfo(this=this, latest_compatible=latest_compatible, latest=latest,) + + +async def log_available_updates(logger: logging.Logger) -> None: + """Log if any updates are available for the library. + + Parameters + ---------- + logger : logging.Logger + The logger to write to. + """ + if not _about.__is_official_distributed_release__: + # If we are on a non-released version, it could be modified or a + # fork, so don't do any checks. + return + + try: + version_info = await fetch_version_info_from_pypi() + + if version_info.this == version_info.latest: + logger.info("package is up to date!") + return + + if version_info.this != version_info.latest_compatible: + logger.warning( + "non-breaking updates are available for hikari, update from v%s to v%s!", + version_info.this, + version_info.latest_compatible, + ) + return + + if version_info.latest != version_info.latest_compatible: + logger.info( + "breaking updates are available for hikari, consider upgrading from v%s to v%s!", + version_info.this, + version_info.latest, + ) + return + + logger.info("unknown version status -- check PyPI for updates.") + + except Exception as ex: + logger.debug("Error occurred fetching version info", exc_info=ex) diff --git a/pages/index.html b/pages/index.html index e702946c65..92c51c1e46 100644 --- a/pages/index.html +++ b/pages/index.html @@ -62,30 +62,30 @@ import asyncio import functools import os - + import hikari - + bot = hikari.Bot(token=os.environ["BOT_TOKEN"]) - - + + def command(name): def decorator(fn): @functools.wraps(fn) async def wrapper(event): if not event.message.author.is_bot and event.message.content.startswith(name): await fn(event) - + return wrapper - + return decorator - - + + @bot.listen() @command("hk.ping") async def ping(event: hikari.MessageCreateEvent) -> None: await event.message.reply("Pong!") - - + + @bot.listen() @command("hk.add") async def add_numbers(event: hikari.MessageCreateEvent) -> None: @@ -95,25 +95,25 @@ and event.message.channel_id == e.message.channel_id and e.message.content.isdigit() ) - + try: await event.message.reply("Please enter the first number") e1 = await bot.wait_for(hikari.MessageCreateEvent, predicate=is_number_check, timeout=30) await event.message.reply("Please enter the second number") e2 = await bot.wait_for(hikari.MessageCreateEvent, predicate=is_number_check, timeout=30) - + except asyncio.TimeoutError: await event.message.reply("You took too long...") - + else: val1 = int(e1.message.content) val2 = int(e2.message.content) - + embed = hikari.Embed(title="Result!", description=f"{val1} + {val2} = {val1 + val2}", color="#3f3") - + await event.message.reply(embed=embed) - - + + bot.run() @@ -186,4 +186,4 @@

- \ No newline at end of file + From 29b2d76e4b65ec89a8530683897f1f0e981b9500 Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Fri, 7 Aug 2020 13:26:04 +0100 Subject: [PATCH 856/922] Relicenced future versions to use more permissive MIT license. This will enable us in future to bundle other libraries that may be used to improve performance, such as similar-licensed C extensions. In addition, it will allow the project to grow and become available to a wider audience. As I am the sole copyright owner of the source by each license header, the source remains under my control to relicense. Please contact me if you have any queries with regards to this. --- .gitlab-ci.yml | 29 +- .gitlab/ci/bases.yml | 29 +- .gitlab/ci/installations.yml | 29 +- .gitlab/ci/linting.yml | 29 +- .gitlab/ci/pages.yml | 29 +- .gitlab/ci/releases.yml | 29 +- .gitlab/ci/tests.yml | 29 +- COPYING.LESSER.md | 157 ---- COPYING.md | 675 ------------------ LICENSE | 19 + MANIFEST.in | 3 +- README.md | 2 +- ci/config.py | 29 +- ci/deploy.nox.py | 29 +- ci/docker.nox.py | 29 +- ci/flake8.nox.py | 29 +- ci/format.nox.py | 29 +- ci/mypy.nox.py | 29 +- ci/nox.py | 29 +- ci/pages.nox.py | 29 +- ci/pdoc.nox.py | 29 +- ci/pip.nox.py | 29 +- ci/pytest.nox.py | 29 +- ci/safety.nox.py | 29 +- ci/twemoji.nox.py | 29 +- ci/utils.nox.py | 29 +- docs/config.mako | 37 +- docs/html.mako | 37 +- hikari/__init__.py | 29 +- hikari/__main__.py | 29 +- hikari/_about.py | 33 +- hikari/api/__init__.py | 29 +- hikari/api/app.py | 29 +- hikari/api/bot.py | 29 +- hikari/api/cache.py | 29 +- hikari/api/component.py | 29 +- hikari/api/entity_factory.py | 29 +- hikari/api/event_consumer.py | 29 +- hikari/api/event_dispatcher.py | 29 +- hikari/api/event_factory.py | 29 +- hikari/api/guild_chunker.py | 29 +- hikari/api/rest.py | 29 +- hikari/api/shard.py | 29 +- hikari/api/special_endpoints.py | 29 +- hikari/api/voice.py | 29 +- hikari/banner.txt | 2 +- hikari/cli.py | 29 +- hikari/config.py | 29 +- hikari/errors.py | 29 +- hikari/events/__init__.py | 29 +- hikari/events/base_events.py | 29 +- hikari/events/channel_events.py | 29 +- hikari/events/guild_events.py | 29 +- hikari/events/lifetime_events.py | 29 +- hikari/events/member_events.py | 29 +- hikari/events/message_events.py | 29 +- hikari/events/reaction_events.py | 29 +- hikari/events/role_events.py | 29 +- hikari/events/shard_events.py | 29 +- hikari/events/typing_events.py | 29 +- hikari/events/user_events.py | 29 +- hikari/events/voice_events.py | 29 +- hikari/impl/__init__.py | 29 +- hikari/impl/bot.py | 29 +- hikari/impl/buckets.py | 29 +- hikari/impl/entity_factory.py | 29 +- hikari/impl/event_factory.py | 29 +- hikari/impl/event_manager_base.py | 29 +- hikari/impl/rate_limits.py | 29 +- hikari/impl/rest.py | 29 +- hikari/impl/shard.py | 29 +- hikari/impl/special_endpoints.py | 29 +- hikari/impl/stateful_cache.py | 29 +- hikari/impl/stateful_event_manager.py | 29 +- hikari/impl/stateful_guild_chunker.py | 29 +- hikari/impl/stateless_cache.py | 29 +- hikari/impl/stateless_event_manager.py | 29 +- hikari/impl/stateless_guild_chunker.py | 29 +- hikari/impl/voice.py | 29 +- hikari/models/__init__.py | 29 +- hikari/models/applications.py | 29 +- hikari/models/audit_logs.py | 29 +- hikari/models/channels.py | 29 +- hikari/models/colors.py | 29 +- hikari/models/colours.py | 29 +- hikari/models/embeds.py | 29 +- hikari/models/emojis.py | 29 +- hikari/models/gateway.py | 29 +- hikari/models/guilds.py | 29 +- hikari/models/intents.py | 29 +- hikari/models/invites.py | 29 +- hikari/models/messages.py | 29 +- hikari/models/permissions.py | 29 +- hikari/models/presences.py | 29 +- hikari/models/users.py | 29 +- hikari/models/voices.py | 29 +- hikari/models/webhooks.py | 29 +- hikari/utilities/__init__.py | 29 +- hikari/utilities/aio.py | 29 +- hikari/utilities/art.py | 30 +- hikari/utilities/constants.py | 29 +- hikari/utilities/data_binding.py | 29 +- hikari/utilities/date.py | 29 +- hikari/utilities/files.py | 29 +- hikari/utilities/flag.py | 29 +- hikari/utilities/iterators.py | 29 +- hikari/utilities/net.py | 29 +- hikari/utilities/reflect.py | 29 +- hikari/utilities/routes.py | 29 +- hikari/utilities/snowflake.py | 29 +- hikari/utilities/spel.py | 29 +- hikari/utilities/undefined.py | 29 +- hikari/utilities/version_sniffer.py | 29 +- lgtm.yml | 20 - noxfile.py | 29 +- pages/index.html | 33 +- pyproject.toml | 29 +- scripts/test_twemoji_mapping.py | 29 +- setup.py | 30 +- tests/__init__.py | 29 +- tests/hikari/__init__.py | 29 +- tests/hikari/client_session_stub.py | 29 +- tests/hikari/events/__init__.py | 29 +- tests/hikari/events/test_base_events.py | 29 +- tests/hikari/events/test_guild_events.py | 29 +- tests/hikari/events/test_message_events.py | 29 +- tests/hikari/events/test_reaction_events.py | 29 +- tests/hikari/events/test_role_events.py | 29 +- tests/hikari/events/test_shard_events.py | 29 +- tests/hikari/events/test_voice_events.py | 29 +- tests/hikari/hikari_test_helpers.py | 29 +- tests/hikari/impl/__init__.py | 29 +- tests/hikari/impl/test_buckets.py | 29 +- tests/hikari/impl/test_entity_factory.py | 29 +- tests/hikari/impl/test_rate_limits.py | 29 +- tests/hikari/impl/test_rest.py | 29 +- tests/hikari/impl/test_shard.py | 29 +- tests/hikari/impl/test_stateful_cache.py | 29 +- tests/hikari/impl/test_stateless_cache.py | 29 +- tests/hikari/models/__init__.py | 29 +- tests/hikari/models/test_applications.py | 29 +- tests/hikari/models/test_audit_logs.py | 29 +- tests/hikari/models/test_channels.py | 29 +- tests/hikari/models/test_colors.py | 29 +- tests/hikari/models/test_colours.py | 29 +- tests/hikari/models/test_emojis.py | 29 +- tests/hikari/models/test_gateway.py | 29 +- tests/hikari/models/test_guilds.py | 29 +- tests/hikari/models/test_intents.py | 29 +- tests/hikari/models/test_invites.py | 29 +- tests/hikari/models/test_messages.py | 29 +- tests/hikari/models/test_permissions.py | 29 +- tests/hikari/models/test_presences.py | 29 +- tests/hikari/models/test_users.py | 29 +- tests/hikari/models/test_voices.py | 29 +- tests/hikari/models/test_webhooks.py | 29 +- tests/hikari/test_errors.py | 29 +- tests/hikari/utilities/__init__.py | 29 +- tests/hikari/utilities/test_aio.py | 29 +- tests/hikari/utilities/test_data_binding.py | 29 +- tests/hikari/utilities/test_date.py | 29 +- tests/hikari/utilities/test_flag.py | 29 +- tests/hikari/utilities/test_net.py | 29 +- tests/hikari/utilities/test_reflect.py | 29 +- tests/hikari/utilities/test_routes.py | 29 +- tests/hikari/utilities/test_snowflake.py | 29 +- tests/hikari/utilities/test_undefined.py | 29 +- .../hikari/utilities/test_version_sniffer.py | 29 +- 168 files changed, 2614 insertions(+), 2959 deletions(-) delete mode 100644 COPYING.LESSER.md delete mode 100644 COPYING.md create mode 100644 LICENSE delete mode 100644 lgtm.yml diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 0627c11152..8c02206dc6 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -1,19 +1,22 @@ -# Copyright © Nekoka.tt 2019-2020 +# Copyright (c) 2020 Nekokatt # -# This file is part of Hikari. +# 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: # -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. # -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . +# 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. stages: - test diff --git a/.gitlab/ci/bases.yml b/.gitlab/ci/bases.yml index 96b87ef7ab..18f77d7c58 100644 --- a/.gitlab/ci/bases.yml +++ b/.gitlab/ci/bases.yml @@ -1,19 +1,22 @@ -# Copyright © Nekoka.tt 2019-2020 +# Copyright (c) 2020 Nekokatt # -# This file is part of Hikari. +# 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: # -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. # -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . +# 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. ### ### Mark a job as having reactive defaults, such as firing on merge requests, diff --git a/.gitlab/ci/installations.yml b/.gitlab/ci/installations.yml index e32a4588b8..02f71f7089 100644 --- a/.gitlab/ci/installations.yml +++ b/.gitlab/ci/installations.yml @@ -1,19 +1,22 @@ -# Copyright © Nekoka.tt 2019-2020 +# Copyright (c) 2020 Nekokatt # -# This file is part of Hikari. +# 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: # -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. # -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . +# 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. .pip: extends: .reactive-job diff --git a/.gitlab/ci/linting.yml b/.gitlab/ci/linting.yml index 8d8fb51908..ad0f569607 100644 --- a/.gitlab/ci/linting.yml +++ b/.gitlab/ci/linting.yml @@ -1,19 +1,22 @@ -# Copyright © Nekoka.tt 2019-2020 +# Copyright (c) 2020 Nekokatt # -# This file is part of Hikari. +# 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: # -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. # -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . +# 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. .lint: allow_failure: false diff --git a/.gitlab/ci/pages.yml b/.gitlab/ci/pages.yml index 640ae182e8..8a7f1fafb1 100644 --- a/.gitlab/ci/pages.yml +++ b/.gitlab/ci/pages.yml @@ -1,19 +1,22 @@ -# Copyright © Nekoka.tt 2019-2020 +# Copyright (c) 2020 Nekokatt # -# This file is part of Hikari. +# 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: # -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. # -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . +# 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. .pages: artifacts: diff --git a/.gitlab/ci/releases.yml b/.gitlab/ci/releases.yml index 3c8f362d99..b6ce43f910 100644 --- a/.gitlab/ci/releases.yml +++ b/.gitlab/ci/releases.yml @@ -1,19 +1,22 @@ -# Copyright © Nekoka.tt 2019-2020 +# Copyright (c) 2020 Nekokatt # -# This file is part of Hikari. +# 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: # -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. # -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . +# 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. .release: allow_failure: false diff --git a/.gitlab/ci/tests.yml b/.gitlab/ci/tests.yml index 90fbaf6407..40df41e34c 100644 --- a/.gitlab/ci/tests.yml +++ b/.gitlab/ci/tests.yml @@ -1,19 +1,22 @@ -# Copyright © Nekoka.tt 2019-2020 +# Copyright (c) 2020 Nekokatt # -# This file is part of Hikari. +# 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: # -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. # -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . +# 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. .test: artifacts: diff --git a/COPYING.LESSER.md b/COPYING.LESSER.md deleted file mode 100644 index 0927556b54..0000000000 --- a/COPYING.LESSER.md +++ /dev/null @@ -1,157 +0,0 @@ -### GNU LESSER GENERAL PUBLIC LICENSE - -Version 3, 29 June 2007 - -Copyright (C) 2007 Free Software Foundation, Inc. - - -Everyone is permitted to copy and distribute verbatim copies of this -license document, but changing it is not allowed. - -This version of the GNU Lesser General Public License incorporates the -terms and conditions of version 3 of the GNU General Public License, -supplemented by the additional permissions listed below. - -#### 0. Additional Definitions. - -As used herein, "this License" refers to version 3 of the GNU Lesser -General Public License, and the "GNU GPL" refers to version 3 of the -GNU General Public License. - -"The Library" refers to a covered work governed by this License, other -than an Application or a Combined Work as defined below. - -An "Application" is any work that makes use of an interface provided -by the Library, but which is not otherwise based on the Library. -Defining a subclass of a class defined by the Library is deemed a mode -of using an interface provided by the Library. - -A "Combined Work" is a work produced by combining or linking an -Application with the Library. The particular version of the Library -with which the Combined Work was made is also called the "Linked -Version". - -The "Minimal Corresponding Source" for a Combined Work means the -Corresponding Source for the Combined Work, excluding any source code -for portions of the Combined Work that, considered in isolation, are -based on the Application, and not on the Linked Version. - -The "Corresponding Application Code" for a Combined Work means the -object code and/or source code for the Application, including any data -and utility programs needed for reproducing the Combined Work from the -Application, but excluding the System Libraries of the Combined Work. - -#### 1. Exception to Section 3 of the GNU GPL. - -You may convey a covered work under sections 3 and 4 of this License -without being bound by section 3 of the GNU GPL. - -#### 2. Conveying Modified Versions. - -If you modify a copy of the Library, and, in your modifications, a -facility refers to a function or data to be supplied by an Application -that uses the facility (other than as an argument passed when the -facility is invoked), then you may convey a copy of the modified -version: - -- a) under this License, provided that you make a good faith effort - to ensure that, in the event an Application does not supply the - function or data, the facility still operates, and performs - whatever part of its purpose remains meaningful, or -- b) under the GNU GPL, with none of the additional permissions of - this License applicable to that copy. - -#### 3. Object Code Incorporating Material from Library Header Files. - -The object code form of an Application may incorporate material from a -header file that is part of the Library. You may convey such object -code under terms of your choice, provided that, if the incorporated -material is not limited to numerical parameters, data structure -layouts and accessors, or small macros, inline functions and templates -(ten or fewer lines in length), you do both of the following: - -- a) Give prominent notice with each copy of the object code that - the Library is used in it and that the Library and its use are - covered by this License. -- b) Accompany the object code with a copy of the GNU GPL and this - license document. - -#### 4. Combined Works. - -You may convey a Combined Work under terms of your choice that, taken -together, effectively do not restrict modification of the portions of -the Library contained in the Combined Work and reverse engineering for -debugging such modifications, if you also do each of the following: - -- a) Give prominent notice with each copy of the Combined Work that - the Library is used in it and that the Library and its use are - covered by this License. -- b) Accompany the Combined Work with a copy of the GNU GPL and this - license document. -- c) For a Combined Work that displays copyright notices during - execution, include the copyright notice for the Library among - these notices, as well as a reference directing the user to the - copies of the GNU GPL and this license document. -- d) Do one of the following: - - 0) Convey the Minimal Corresponding Source under the terms of - this License, and the Corresponding Application Code in a form - suitable for, and under terms that permit, the user to - recombine or relink the Application with a modified version of - the Linked Version to produce a modified Combined Work, in the - manner specified by section 6 of the GNU GPL for conveying - Corresponding Source. - - 1) Use a suitable shared library mechanism for linking with - the Library. A suitable mechanism is one that (a) uses at run - time a copy of the Library already present on the user's - computer system, and (b) will operate properly with a modified - version of the Library that is interface-compatible with the - Linked Version. -- e) Provide Installation Information, but only if you would - otherwise be required to provide such information under section 6 - of the GNU GPL, and only to the extent that such information is - necessary to install and execute a modified version of the - Combined Work produced by recombining or relinking the Application - with a modified version of the Linked Version. (If you use option - 4d0, the Installation Information must accompany the Minimal - Corresponding Source and Corresponding Application Code. If you - use option 4d1, you must provide the Installation Information in - the manner specified by section 6 of the GNU GPL for conveying - Corresponding Source.) - -#### 5. Combined Libraries. - -You may place library facilities that are a work based on the Library -side by side in a single library together with other library -facilities that are not Applications and are not covered by this -License, and convey such a combined library under terms of your -choice, if you do both of the following: - -- a) Accompany the combined library with a copy of the same work - based on the Library, uncombined with any other library - facilities, conveyed under the terms of this License. -- b) Give prominent notice with the combined library that part of it - is a work based on the Library, and explaining where to find the - accompanying uncombined form of the same work. - -#### 6. Revised Versions of the GNU Lesser General Public License. - -The Free Software Foundation may publish revised and/or new versions -of the GNU Lesser General Public License from time to time. Such new -versions will be similar in spirit to the present version, but may -differ in detail to address new problems or concerns. - -Each version is given a distinguishing version number. If the Library -as you received it specifies that a certain numbered version of the -GNU Lesser General Public License "or any later version" applies to -it, you have the option of following the terms and conditions either -of that published version or of any later version published by the -Free Software Foundation. If the Library as you received it does not -specify a version number of the GNU Lesser General Public License, you -may choose any version of the GNU Lesser General Public License ever -published by the Free Software Foundation. - -If the Library as you received it specifies that a proxy can decide -whether future versions of the GNU Lesser General Public License shall -apply, that proxy's public statement of acceptance of any version is -permanent authorization for you to choose that version for the -Library. diff --git a/COPYING.md b/COPYING.md deleted file mode 100644 index 2fb2e74d8d..0000000000 --- a/COPYING.md +++ /dev/null @@ -1,675 +0,0 @@ -### GNU GENERAL PUBLIC LICENSE - -Version 3, 29 June 2007 - -Copyright (C) 2007 Free Software Foundation, Inc. - - -Everyone is permitted to copy and distribute verbatim copies of this -license document, but changing it is not allowed. - -### Preamble - -The GNU General Public License is a free, copyleft license for -software and other kinds of works. - -The licenses for most software and other practical works are designed -to take away your freedom to share and change the works. By contrast, -the GNU General Public License is intended to guarantee your freedom -to share and change all versions of a program--to make sure it remains -free software for all its users. We, the Free Software Foundation, use -the GNU General Public License for most of our software; it applies -also to any other work released this way by its authors. You can apply -it to your programs, too. - -When we speak of free software, we are referring to freedom, not -price. Our General Public Licenses are designed to make sure that you -have the freedom to distribute copies of free software (and charge for -them if you wish), that you receive source code or can get it if you -want it, that you can change the software or use pieces of it in new -free programs, and that you know you can do these things. - -To protect your rights, we need to prevent others from denying you -these rights or asking you to surrender the rights. Therefore, you -have certain responsibilities if you distribute copies of the -software, or if you modify it: responsibilities to respect the freedom -of others. - -For example, if you distribute copies of such a program, whether -gratis or for a fee, you must pass on to the recipients the same -freedoms that you received. You must make sure that they, too, receive -or can get the source code. And you must show them these terms so they -know their rights. - -Developers that use the GNU GPL protect your rights with two steps: -(1) assert copyright on the software, and (2) offer you this License -giving you legal permission to copy, distribute and/or modify it. - -For the developers' and authors' protection, the GPL clearly explains -that there is no warranty for this free software. For both users' and -authors' sake, the GPL requires that modified versions be marked as -changed, so that their problems will not be attributed erroneously to -authors of previous versions. - -Some devices are designed to deny users access to install or run -modified versions of the software inside them, although the -manufacturer can do so. This is fundamentally incompatible with the -aim of protecting users' freedom to change the software. The -systematic pattern of such abuse occurs in the area of products for -individuals to use, which is precisely where it is most unacceptable. -Therefore, we have designed this version of the GPL to prohibit the -practice for those products. If such problems arise substantially in -other domains, we stand ready to extend this provision to those -domains in future versions of the GPL, as needed to protect the -freedom of users. - -Finally, every program is threatened constantly by software patents. -States should not allow patents to restrict development and use of -software on general-purpose computers, but in those that do, we wish -to avoid the special danger that patents applied to a free program -could make it effectively proprietary. To prevent this, the GPL -assures that patents cannot be used to render the program non-free. - -The precise terms and conditions for copying, distribution and -modification follow. - -### TERMS AND CONDITIONS - -#### 0. Definitions. - -"This License" refers to version 3 of the GNU General Public License. - -"Copyright" also means copyright-like laws that apply to other kinds -of works, such as semiconductor masks. - -"The Program" refers to any copyrightable work licensed under this -License. Each licensee is addressed as "you". "Licensees" and -"recipients" may be individuals or organizations. - -To "modify" a work means to copy from or adapt all or part of the work -in a fashion requiring copyright permission, other than the making of -an exact copy. The resulting work is called a "modified version" of -the earlier work or a work "based on" the earlier work. - -A "covered work" means either the unmodified Program or a work based -on the Program. - -To "propagate" a work means to do anything with it that, without -permission, would make you directly or secondarily liable for -infringement under applicable copyright law, except executing it on a -computer or modifying a private copy. Propagation includes copying, -distribution (with or without modification), making available to the -public, and in some countries other activities as well. - -To "convey" a work means any kind of propagation that enables other -parties to make or receive copies. Mere interaction with a user -through a computer network, with no transfer of a copy, is not -conveying. - -An interactive user interface displays "Appropriate Legal Notices" to -the extent that it includes a convenient and prominently visible -feature that (1) displays an appropriate copyright notice, and (2) -tells the user that there is no warranty for the work (except to the -extent that warranties are provided), that licensees may convey the -work under this License, and how to view a copy of this License. If -the interface presents a list of user commands or options, such as a -menu, a prominent item in the list meets this criterion. - -#### 1. Source Code. - -The "source code" for a work means the preferred form of the work for -making modifications to it. "Object code" means any non-source form of -a work. - -A "Standard Interface" means an interface that either is an official -standard defined by a recognized standards body, or, in the case of -interfaces specified for a particular programming language, one that -is widely used among developers working in that language. - -The "System Libraries" of an executable work include anything, other -than the work as a whole, that (a) is included in the normal form of -packaging a Major Component, but which is not part of that Major -Component, and (b) serves only to enable use of the work with that -Major Component, or to implement a Standard Interface for which an -implementation is available to the public in source code form. A -"Major Component", in this context, means a major essential component -(kernel, window system, and so on) of the specific operating system -(if any) on which the executable work runs, or a compiler used to -produce the work, or an object code interpreter used to run it. - -The "Corresponding Source" for a work in object code form means all -the source code needed to generate, install, and (for an executable -work) run the object code and to modify the work, including scripts to -control those activities. However, it does not include the work's -System Libraries, or general-purpose tools or generally available free -programs which are used unmodified in performing those activities but -which are not part of the work. For example, Corresponding Source -includes interface definition files associated with source files for -the work, and the source code for shared libraries and dynamically -linked subprograms that the work is specifically designed to require, -such as by intimate data communication or control flow between those -subprograms and other parts of the work. - -The Corresponding Source need not include anything that users can -regenerate automatically from other parts of the Corresponding Source. - -The Corresponding Source for a work in source code form is that same -work. - -#### 2. Basic Permissions. - -All rights granted under this License are granted for the term of -copyright on the Program, and are irrevocable provided the stated -conditions are met. This License explicitly affirms your unlimited -permission to run the unmodified Program. The output from running a -covered work is covered by this License only if the output, given its -content, constitutes a covered work. This License acknowledges your -rights of fair use or other equivalent, as provided by copyright law. - -You may make, run and propagate covered works that you do not convey, -without conditions so long as your license otherwise remains in force. -You may convey covered works to others for the sole purpose of having -them make modifications exclusively for you, or provide you with -facilities for running those works, provided that you comply with the -terms of this License in conveying all material for which you do not -control copyright. Those thus making or running the covered works for -you must do so exclusively on your behalf, under your direction and -control, on terms that prohibit them from making any copies of your -copyrighted material outside their relationship with you. - -Conveying under any other circumstances is permitted solely under the -conditions stated below. Sublicensing is not allowed; section 10 makes -it unnecessary. - -#### 3. Protecting Users' Legal Rights From Anti-Circumvention Law. - -No covered work shall be deemed part of an effective technological -measure under any applicable law fulfilling obligations under article -11 of the WIPO copyright treaty adopted on 20 December 1996, or -similar laws prohibiting or restricting circumvention of such -measures. - -When you convey a covered work, you waive any legal power to forbid -circumvention of technological measures to the extent such -circumvention is effected by exercising rights under this License with -respect to the covered work, and you disclaim any intention to limit -operation or modification of the work as a means of enforcing, against -the work's users, your or third parties' legal rights to forbid -circumvention of technological measures. - -#### 4. Conveying Verbatim Copies. - -You may convey verbatim copies of the Program's source code as you -receive it, in any medium, provided that you conspicuously and -appropriately publish on each copy an appropriate copyright notice; -keep intact all notices stating that this License and any -non-permissive terms added in accord with section 7 apply to the code; -keep intact all notices of the absence of any warranty; and give all -recipients a copy of this License along with the Program. - -You may charge any price or no price for each copy that you convey, -and you may offer support or warranty protection for a fee. - -#### 5. Conveying Modified Source Versions. - -You may convey a work based on the Program, or the modifications to -produce it from the Program, in the form of source code under the -terms of section 4, provided that you also meet all of these -conditions: - -- a) The work must carry prominent notices stating that you modified - it, and giving a relevant date. -- b) The work must carry prominent notices stating that it is - released under this License and any conditions added under - section 7. This requirement modifies the requirement in section 4 - to "keep intact all notices". -- c) You must license the entire work, as a whole, under this - License to anyone who comes into possession of a copy. This - License will therefore apply, along with any applicable section 7 - additional terms, to the whole of the work, and all its parts, - regardless of how they are packaged. This License gives no - permission to license the work in any other way, but it does not - invalidate such permission if you have separately received it. -- d) If the work has interactive user interfaces, each must display - Appropriate Legal Notices; however, if the Program has interactive - interfaces that do not display Appropriate Legal Notices, your - work need not make them do so. - -A compilation of a covered work with other separate and independent -works, which are not by their nature extensions of the covered work, -and which are not combined with it such as to form a larger program, -in or on a volume of a storage or distribution medium, is called an -"aggregate" if the compilation and its resulting copyright are not -used to limit the access or legal rights of the compilation's users -beyond what the individual works permit. Inclusion of a covered work -in an aggregate does not cause this License to apply to the other -parts of the aggregate. - -#### 6. Conveying Non-Source Forms. - -You may convey a covered work in object code form under the terms of -sections 4 and 5, provided that you also convey the machine-readable -Corresponding Source under the terms of this License, in one of these -ways: - -- a) Convey the object code in, or embodied in, a physical product - (including a physical distribution medium), accompanied by the - Corresponding Source fixed on a durable physical medium - customarily used for software interchange. -- b) Convey the object code in, or embodied in, a physical product - (including a physical distribution medium), accompanied by a - written offer, valid for at least three years and valid for as - long as you offer spare parts or customer support for that product - model, to give anyone who possesses the object code either (1) a - copy of the Corresponding Source for all the software in the - product that is covered by this License, on a durable physical - medium customarily used for software interchange, for a price no - more than your reasonable cost of physically performing this - conveying of source, or (2) access to copy the Corresponding - Source from a network server at no charge. -- c) Convey individual copies of the object code with a copy of the - written offer to provide the Corresponding Source. This - alternative is allowed only occasionally and noncommercially, and - only if you received the object code with such an offer, in accord - with subsection 6b. -- d) Convey the object code by offering access from a designated - place (gratis or for a charge), and offer equivalent access to the - Corresponding Source in the same way through the same place at no - further charge. You need not require recipients to copy the - Corresponding Source along with the object code. If the place to - copy the object code is a network server, the Corresponding Source - may be on a different server (operated by you or a third party) - that supports equivalent copying facilities, provided you maintain - clear directions next to the object code saying where to find the - Corresponding Source. Regardless of what server hosts the - Corresponding Source, you remain obligated to ensure that it is - available for as long as needed to satisfy these requirements. -- e) Convey the object code using peer-to-peer transmission, - provided you inform other peers where the object code and - Corresponding Source of the work are being offered to the general - public at no charge under subsection 6d. - -A separable portion of the object code, whose source code is excluded -from the Corresponding Source as a System Library, need not be -included in conveying the object code work. - -A "User Product" is either (1) a "consumer product", which means any -tangible personal property which is normally used for personal, -family, or household purposes, or (2) anything designed or sold for -incorporation into a dwelling. In determining whether a product is a -consumer product, doubtful cases shall be resolved in favor of -coverage. For a particular product received by a particular user, -"normally used" refers to a typical or common use of that class of -product, regardless of the status of the particular user or of the way -in which the particular user actually uses, or expects or is expected -to use, the product. A product is a consumer product regardless of -whether the product has substantial commercial, industrial or -non-consumer uses, unless such uses represent the only significant -mode of use of the product. - -"Installation Information" for a User Product means any methods, -procedures, authorization keys, or other information required to -install and execute modified versions of a covered work in that User -Product from a modified version of its Corresponding Source. The -information must suffice to ensure that the continued functioning of -the modified object code is in no case prevented or interfered with -solely because modification has been made. - -If you convey an object code work under this section in, or with, or -specifically for use in, a User Product, and the conveying occurs as -part of a transaction in which the right of possession and use of the -User Product is transferred to the recipient in perpetuity or for a -fixed term (regardless of how the transaction is characterized), the -Corresponding Source conveyed under this section must be accompanied -by the Installation Information. But this requirement does not apply -if neither you nor any third party retains the ability to install -modified object code on the User Product (for example, the work has -been installed in ROM). - -The requirement to provide Installation Information does not include a -requirement to continue to provide support service, warranty, or -updates for a work that has been modified or installed by the -recipient, or for the User Product in which it has been modified or -installed. Access to a network may be denied when the modification -itself materially and adversely affects the operation of the network -or violates the rules and protocols for communication across the -network. - -Corresponding Source conveyed, and Installation Information provided, -in accord with this section must be in a format that is publicly -documented (and with an implementation available to the public in -source code form), and must require no special password or key for -unpacking, reading or copying. - -#### 7. Additional Terms. - -"Additional permissions" are terms that supplement the terms of this -License by making exceptions from one or more of its conditions. -Additional permissions that are applicable to the entire Program shall -be treated as though they were included in this License, to the extent -that they are valid under applicable law. If additional permissions -apply only to part of the Program, that part may be used separately -under those permissions, but the entire Program remains governed by -this License without regard to the additional permissions. - -When you convey a copy of a covered work, you may at your option -remove any additional permissions from that copy, or from any part of -it. (Additional permissions may be written to require their own -removal in certain cases when you modify the work.) You may place -additional permissions on material, added by you to a covered work, -for which you have or can give appropriate copyright permission. - -Notwithstanding any other provision of this License, for material you -add to a covered work, you may (if authorized by the copyright holders -of that material) supplement the terms of this License with terms: - -- a) Disclaiming warranty or limiting liability differently from the - terms of sections 15 and 16 of this License; or -- b) Requiring preservation of specified reasonable legal notices or - author attributions in that material or in the Appropriate Legal - Notices displayed by works containing it; or -- c) Prohibiting misrepresentation of the origin of that material, - or requiring that modified versions of such material be marked in - reasonable ways as different from the original version; or -- d) Limiting the use for publicity purposes of names of licensors - or authors of the material; or -- e) Declining to grant rights under trademark law for use of some - trade names, trademarks, or service marks; or -- f) Requiring indemnification of licensors and authors of that - material by anyone who conveys the material (or modified versions - of it) with contractual assumptions of liability to the recipient, - for any liability that these contractual assumptions directly - impose on those licensors and authors. - -All other non-permissive additional terms are considered "further -restrictions" within the meaning of section 10. If the Program as you -received it, or any part of it, contains a notice stating that it is -governed by this License along with a term that is a further -restriction, you may remove that term. If a license document contains -a further restriction but permits relicensing or conveying under this -License, you may add to a covered work material governed by the terms -of that license document, provided that the further restriction does -not survive such relicensing or conveying. - -If you add terms to a covered work in accord with this section, you -must place, in the relevant source files, a statement of the -additional terms that apply to those files, or a notice indicating -where to find the applicable terms. - -Additional terms, permissive or non-permissive, may be stated in the -form of a separately written license, or stated as exceptions; the -above requirements apply either way. - -#### 8. Termination. - -You may not propagate or modify a covered work except as expressly -provided under this License. Any attempt otherwise to propagate or -modify it is void, and will automatically terminate your rights under -this License (including any patent licenses granted under the third -paragraph of section 11). - -However, if you cease all violation of this License, then your license -from a particular copyright holder is reinstated (a) provisionally, -unless and until the copyright holder explicitly and finally -terminates your license, and (b) permanently, if the copyright holder -fails to notify you of the violation by some reasonable means prior to -60 days after the cessation. - -Moreover, your license from a particular copyright holder is -reinstated permanently if the copyright holder notifies you of the -violation by some reasonable means, this is the first time you have -received notice of violation of this License (for any work) from that -copyright holder, and you cure the violation prior to 30 days after -your receipt of the notice. - -Termination of your rights under this section does not terminate the -licenses of parties who have received copies or rights from you under -this License. If your rights have been terminated and not permanently -reinstated, you do not qualify to receive new licenses for the same -material under section 10. - -#### 9. Acceptance Not Required for Having Copies. - -You are not required to accept this License in order to receive or run -a copy of the Program. Ancillary propagation of a covered work -occurring solely as a consequence of using peer-to-peer transmission -to receive a copy likewise does not require acceptance. However, -nothing other than this License grants you permission to propagate or -modify any covered work. These actions infringe copyright if you do -not accept this License. Therefore, by modifying or propagating a -covered work, you indicate your acceptance of this License to do so. - -#### 10. Automatic Licensing of Downstream Recipients. - -Each time you convey a covered work, the recipient automatically -receives a license from the original licensors, to run, modify and -propagate that work, subject to this License. You are not responsible -for enforcing compliance by third parties with this License. - -An "entity transaction" is a transaction transferring control of an -organization, or substantially all assets of one, or subdividing an -organization, or merging organizations. If propagation of a covered -work results from an entity transaction, each party to that -transaction who receives a copy of the work also receives whatever -licenses to the work the party's predecessor in interest had or could -give under the previous paragraph, plus a right to possession of the -Corresponding Source of the work from the predecessor in interest, if -the predecessor has it or can get it with reasonable efforts. - -You may not impose any further restrictions on the exercise of the -rights granted or affirmed under this License. For example, you may -not impose a license fee, royalty, or other charge for exercise of -rights granted under this License, and you may not initiate litigation -(including a cross-claim or counterclaim in a lawsuit) alleging that -any patent claim is infringed by making, using, selling, offering for -sale, or importing the Program or any portion of it. - -#### 11. Patents. - -A "contributor" is a copyright holder who authorizes use under this -License of the Program or a work on which the Program is based. The -work thus licensed is called the contributor's "contributor version". - -A contributor's "essential patent claims" are all patent claims owned -or controlled by the contributor, whether already acquired or -hereafter acquired, that would be infringed by some manner, permitted -by this License, of making, using, or selling its contributor version, -but do not include claims that would be infringed only as a -consequence of further modification of the contributor version. For -purposes of this definition, "control" includes the right to grant -patent sublicenses in a manner consistent with the requirements of -this License. - -Each contributor grants you a non-exclusive, worldwide, royalty-free -patent license under the contributor's essential patent claims, to -make, use, sell, offer for sale, import and otherwise run, modify and -propagate the contents of its contributor version. - -In the following three paragraphs, a "patent license" is any express -agreement or commitment, however denominated, not to enforce a patent -(such as an express permission to practice a patent or covenant not to -sue for patent infringement). To "grant" such a patent license to a -party means to make such an agreement or commitment not to enforce a -patent against the party. - -If you convey a covered work, knowingly relying on a patent license, -and the Corresponding Source of the work is not available for anyone -to copy, free of charge and under the terms of this License, through a -publicly available network server or other readily accessible means, -then you must either (1) cause the Corresponding Source to be so -available, or (2) arrange to deprive yourself of the benefit of the -patent license for this particular work, or (3) arrange, in a manner -consistent with the requirements of this License, to extend the patent -license to downstream recipients. "Knowingly relying" means you have -actual knowledge that, but for the patent license, your conveying the -covered work in a country, or your recipient's use of the covered work -in a country, would infringe one or more identifiable patents in that -country that you have reason to believe are valid. - -If, pursuant to or in connection with a single transaction or -arrangement, you convey, or propagate by procuring conveyance of, a -covered work, and grant a patent license to some of the parties -receiving the covered work authorizing them to use, propagate, modify -or convey a specific copy of the covered work, then the patent license -you grant is automatically extended to all recipients of the covered -work and works based on it. - -A patent license is "discriminatory" if it does not include within the -scope of its coverage, prohibits the exercise of, or is conditioned on -the non-exercise of one or more of the rights that are specifically -granted under this License. You may not convey a covered work if you -are a party to an arrangement with a third party that is in the -business of distributing software, under which you make payment to the -third party based on the extent of your activity of conveying the -work, and under which the third party grants, to any of the parties -who would receive the covered work from you, a discriminatory patent -license (a) in connection with copies of the covered work conveyed by -you (or copies made from those copies), or (b) primarily for and in -connection with specific products or compilations that contain the -covered work, unless you entered into that arrangement, or that patent -license was granted, prior to 28 March 2007. - -Nothing in this License shall be construed as excluding or limiting -any implied license or other defenses to infringement that may -otherwise be available to you under applicable patent law. - -#### 12. No Surrender of Others' Freedom. - -If conditions are imposed on you (whether by court order, agreement or -otherwise) that contradict the conditions of this License, they do not -excuse you from the conditions of this License. If you cannot convey a -covered work so as to satisfy simultaneously your obligations under -this License and any other pertinent obligations, then as a -consequence you may not convey it at all. For example, if you agree to -terms that obligate you to collect a royalty for further conveying -from those to whom you convey the Program, the only way you could -satisfy both those terms and this License would be to refrain entirely -from conveying the Program. - -#### 13. Use with the GNU Affero General Public License. - -Notwithstanding any other provision of this License, you have -permission to link or combine any covered work with a work licensed -under version 3 of the GNU Affero General Public License into a single -combined work, and to convey the resulting work. The terms of this -License will continue to apply to the part which is the covered work, -but the special requirements of the GNU Affero General Public License, -section 13, concerning interaction through a network will apply to the -combination as such. - -#### 14. Revised Versions of this License. - -The Free Software Foundation may publish revised and/or new versions -of the GNU General Public License from time to time. Such new versions -will be similar in spirit to the present version, but may differ in -detail to address new problems or concerns. - -Each version is given a distinguishing version number. If the Program -specifies that a certain numbered version of the GNU General Public -License "or any later version" applies to it, you have the option of -following the terms and conditions either of that numbered version or -of any later version published by the Free Software Foundation. If the -Program does not specify a version number of the GNU General Public -License, you may choose any version ever published by the Free -Software Foundation. - -If the Program specifies that a proxy can decide which future versions -of the GNU General Public License can be used, that proxy's public -statement of acceptance of a version permanently authorizes you to -choose that version for the Program. - -Later license versions may give you additional or different -permissions. However, no additional obligations are imposed on any -author or copyright holder as a result of your choosing to follow a -later version. - -#### 15. Disclaimer of Warranty. - -THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY -APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT -HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT -WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT -LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR -A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND -PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE -DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR -CORRECTION. - -#### 16. Limitation of Liability. - -IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING -WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR -CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, -INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES -ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT -NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR -LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM -TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER -PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. - -#### 17. Interpretation of Sections 15 and 16. - -If the disclaimer of warranty and limitation of liability provided -above cannot be given local legal effect according to their terms, -reviewing courts shall apply local law that most closely approximates -an absolute waiver of all civil liability in connection with the -Program, unless a warranty or assumption of liability accompanies a -copy of the Program in return for a fee. - -END OF TERMS AND CONDITIONS - -### How to Apply These Terms to Your New Programs - -If you develop a new program, and you want it to be of the greatest -possible use to the public, the best way to achieve this is to make it -free software which everyone can redistribute and change under these -terms. - -To do so, attach the following notices to the program. It is safest to -attach them to the start of each source file to most effectively state -the exclusion of warranty; and each file should have at least the -"copyright" line and a pointer to where the full notice is found. - - - Copyright (C) - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . - -Also add information on how to contact you by electronic and paper -mail. - -If the program does terminal interaction, make it output a short -notice like this when it starts in an interactive mode: - - Copyright (C) - This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. - This is free software, and you are welcome to redistribute it - under certain conditions; type `show c' for details. - -The hypothetical commands \`show w' and \`show c' should show the -appropriate parts of the General Public License. Of course, your -program's commands might be different; for a GUI interface, you would -use an "about box". - -You should also get your employer (if you work as a programmer) or -school, if any, to sign a "copyright disclaimer" for the program, if -necessary. For more information on this, and how to apply and follow -the GNU GPL, see . - -The GNU General Public License does not permit incorporating your -program into proprietary programs. If your program is a subroutine -library, you may consider it more useful to permit linking proprietary -applications with the library. If this is what you want to do, use the -GNU Lesser General Public License instead of this License. But first, -please read . diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000000..3cb556f642 --- /dev/null +++ b/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2020 Nekokatt + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/MANIFEST.in b/MANIFEST.in index def1a5a24c..8e38add164 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,6 +1,5 @@ graft hikari -include COPYING.md -include COPYING.LESSER.md +include LICENSE include requirements.txt include speedup-requirements.txt include setup.py diff --git a/README.md b/README.md index 9b279a9af3..028eeb0979 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ **Note:** this API is still under active daily development, and is in a -**pre-alpha** stage. If you are looking to give feedback, or want to help us +**alpha** stage. If you are looking to give feedback, or want to help us out, then feel free to join our [Discord server](https://discord.gg/Jx4cNGG) and chat to us. Any help is greatly appreciated, no matter what your experience level may be! :-) diff --git a/ci/config.py b/ci/config.py index 3bb1ac00e7..0e869fd7b9 100644 --- a/ci/config.py +++ b/ci/config.py @@ -1,20 +1,23 @@ # -*- coding: utf-8 -*- -# Copyright © Nekoka.tt 2019-2020 +# Copyright (c) 2020 Nekokatt # -# This file is part of Hikari. +# 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: # -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. # -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . +# 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 os as _os IS_CI = "CI" in _os.environ diff --git a/ci/deploy.nox.py b/ci/deploy.nox.py index 6914d96684..5a83809266 100644 --- a/ci/deploy.nox.py +++ b/ci/deploy.nox.py @@ -1,20 +1,23 @@ # -*- coding: utf-8 -*- -# Copyright © Nekoka.tt 2019-2020 +# Copyright (c) 2020 Nekokatt # -# This file is part of Hikari. +# 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: # -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. # -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . +# 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. """Deployment scripts for CI only.""" import json import os diff --git a/ci/docker.nox.py b/ci/docker.nox.py index 2e027c131e..f021033c14 100644 --- a/ci/docker.nox.py +++ b/ci/docker.nox.py @@ -1,20 +1,23 @@ # -*- coding: utf-8 -*- -# Copyright © Nekoka.tt 2019-2020 +# Copyright (c) 2020 Nekokatt # -# This file is part of Hikari. +# 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: # -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. # -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . +# 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. """Allows running CI scripts within a Docker container.""" import os import random diff --git a/ci/flake8.nox.py b/ci/flake8.nox.py index e67bb72991..4feb7c0ef9 100644 --- a/ci/flake8.nox.py +++ b/ci/flake8.nox.py @@ -1,20 +1,23 @@ # -*- coding: utf-8 -*- -# Copyright © Nekoka.tt 2019-2020 +# Copyright (c) 2020 Nekokatt # -# This file is part of Hikari. +# 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: # -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. # -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . +# 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 os import shutil diff --git a/ci/format.nox.py b/ci/format.nox.py index f6028f9857..b94c5068da 100644 --- a/ci/format.nox.py +++ b/ci/format.nox.py @@ -1,20 +1,23 @@ # -*- coding: utf-8 -*- -# Copyright © Nekoka.tt 2019-2020 +# Copyright (c) 2020 Nekokatt # -# This file is part of Hikari. +# 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: # -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. # -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . +# 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. """Black code-style jobs.""" import os import shutil diff --git a/ci/mypy.nox.py b/ci/mypy.nox.py index b09df84317..849502e62b 100644 --- a/ci/mypy.nox.py +++ b/ci/mypy.nox.py @@ -1,20 +1,23 @@ # -*- coding: utf-8 -*- -# Copyright © Nekoka.tt 2019-2020 +# Copyright (c) 2020 Nekokatt # -# This file is part of Hikari. +# 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: # -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. # -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . +# 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. from ci import config from ci import nox diff --git a/ci/nox.py b/ci/nox.py index ca1291908e..67002f600a 100644 --- a/ci/nox.py +++ b/ci/nox.py @@ -1,20 +1,23 @@ # -*- coding: utf-8 -*- -# Copyright © Nekoka.tt 2019-2020 +# Copyright (c) 2020 Nekokatt # -# This file is part of Hikari. +# 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: # -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. # -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . +# 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. """Wrapper around nox to give default job kwargs.""" import functools import os diff --git a/ci/pages.nox.py b/ci/pages.nox.py index 9553f06501..f93b9be3c9 100644 --- a/ci/pages.nox.py +++ b/ci/pages.nox.py @@ -1,20 +1,23 @@ # -*- coding: utf-8 -*- -# Copyright © Nekoka.tt 2019-2020 +# Copyright (c) 2020 Nekokatt # -# This file is part of Hikari. +# 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: # -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. # -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . +# 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. """Pdoc documentation generation.""" import os import shutil diff --git a/ci/pdoc.nox.py b/ci/pdoc.nox.py index 1a67d3ffd1..beed3bfce8 100644 --- a/ci/pdoc.nox.py +++ b/ci/pdoc.nox.py @@ -1,20 +1,23 @@ # -*- coding: utf-8 -*- -# Copyright © Nekoka.tt 2019-2020 +# Copyright (c) 2020 Nekokatt # -# This file is part of Hikari. +# 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: # -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. # -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . +# 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. """Pdoc documentation generation.""" import os import shutil diff --git a/ci/pip.nox.py b/ci/pip.nox.py index a68b750961..9f85c28f6a 100644 --- a/ci/pip.nox.py +++ b/ci/pip.nox.py @@ -1,20 +1,23 @@ # -*- coding: utf-8 -*- -# Copyright © Nekoka.tt 2019-2020 +# Copyright (c) 2020 Nekokatt # -# This file is part of Hikari. +# 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: # -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. # -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . +# 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. """Installation tests.""" import contextlib import os diff --git a/ci/pytest.nox.py b/ci/pytest.nox.py index ad767a48c8..4c66c72103 100644 --- a/ci/pytest.nox.py +++ b/ci/pytest.nox.py @@ -1,20 +1,23 @@ # -*- coding: utf-8 -*- -# Copyright © Nekoka.tt 2019-2020 +# Copyright (c) 2020 Nekokatt # -# This file is part of Hikari. +# 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: # -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. # -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . +# 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. """Py.test integration.""" import os import shutil diff --git a/ci/safety.nox.py b/ci/safety.nox.py index 7616729eab..1bb27a8f79 100644 --- a/ci/safety.nox.py +++ b/ci/safety.nox.py @@ -1,20 +1,23 @@ # -*- coding: utf-8 -*- -# Copyright © Nekoka.tt 2019-2020 +# Copyright (c) 2020 Nekokatt # -# This file is part of Hikari. +# 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: # -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. # -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . +# 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. """Dependency scanning.""" from ci import nox diff --git a/ci/twemoji.nox.py b/ci/twemoji.nox.py index a3746360a6..d597d485e7 100644 --- a/ci/twemoji.nox.py +++ b/ci/twemoji.nox.py @@ -1,20 +1,23 @@ # -*- coding: utf-8 -*- -# Copyright © Nekoka.tt 2019-2020 +# Copyright (c) 2020 Nekokatt # -# This file is part of Hikari. +# 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: # -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. # -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . +# 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. from ci import nox diff --git a/ci/utils.nox.py b/ci/utils.nox.py index 047bc63c39..092cb1357a 100644 --- a/ci/utils.nox.py +++ b/ci/utils.nox.py @@ -1,20 +1,23 @@ # -*- coding: utf-8 -*- -# Copyright © Nekoka.tt 2019-2020 +# Copyright (c) 2020 Nekokatt # -# This file is part of Hikari. +# 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: # -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. # -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . +# 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. """Additional utilities for Nox.""" import shutil diff --git a/docs/config.mako b/docs/config.mako index c6c1c779aa..a3b7eef318 100644 --- a/docs/config.mako +++ b/docs/config.mako @@ -1,19 +1,24 @@ -## Copyright © Nekoka.tt 2019-2020 -## -## This file is part of Hikari. -## -## Hikari is free software: you can redistribute it and/or modify -## it under the terms of the GNU Lesser General Public License as published by -## the Free Software Foundation, either version 3 of the License, or -## (at your option) any later version. -## -## Hikari is distributed in the hope that it will be useful, -## but WITHOUT ANY WARRANTY; without even the implied warranty of -## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -## GNU Lesser General Public License for more details. -## -## You should have received a copy of the GNU Lesser General Public License -## along with Hikari. If not, see . + <%! from distutils import version as _version diff --git a/docs/html.mako b/docs/html.mako index d897b86884..6d1c0f2faf 100644 --- a/docs/html.mako +++ b/docs/html.mako @@ -1,19 +1,24 @@ -## Copyright © Nekoka.tt 2019-2020 -## -## This file is part of Hikari. -## -## Hikari is free software: you can redistribute it and/or modify -## it under the terms of the GNU Lesser General Public License as published by -## the Free Software Foundation, either version 3 of the License, or -## (at your option) any later version. -## -## Hikari is distributed in the hope that it will be useful, -## but WITHOUT ANY WARRANTY; without even the implied warranty of -## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -## GNU Lesser General Public License for more details. -## -## You should have received a copy of the GNU Lesser General Public License -## along with Hikari. If not, see . + ############################# IMPORTS ############################## <%! diff --git a/hikari/__init__.py b/hikari/__init__.py index 35ec9f0240..98bbe70943 100644 --- a/hikari/__init__.py +++ b/hikari/__init__.py @@ -1,21 +1,24 @@ # -*- coding: utf-8 -*- # cython: language_level=3 -# Copyright © Nekoka.tt 2019-2020 +# Copyright (c) 2020 Nekokatt # -# This file is part of Hikari. +# 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: # -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. # -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . +# 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. """A sane Python framework for writing modern Discord bots. To get started, you will want to initialize an instance of `Bot` diff --git a/hikari/__main__.py b/hikari/__main__.py index 6a55e97db0..f223cae763 100644 --- a/hikari/__main__.py +++ b/hikari/__main__.py @@ -1,21 +1,24 @@ # -*- coding: utf-8 -*- # cython: language_level=3 -# Copyright © Nekoka.tt 2019-2020 +# Copyright (c) 2020 Nekokatt # -# This file is part of Hikari. +# 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: # -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. # -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . +# 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. """Provides a command-line entry point that shows the library version and then exits.""" from hikari import cli diff --git a/hikari/_about.py b/hikari/_about.py index 660a6c0f62..6b6431f2b0 100644 --- a/hikari/_about.py +++ b/hikari/_about.py @@ -1,21 +1,24 @@ # -*- coding: utf-8 -*- # cython: language_level=3 -# Copyright © Nekoka.tt 2019-2020 +# Copyright (c) 2020 Nekokatt # -# This file is part of Hikari. +# 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: # -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. # -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . +# 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. """Package metadata.""" from __future__ import annotations @@ -25,13 +28,13 @@ __author__ = "Nekokatt" __ci__ = "https://gitlab.com/nekokatt/hikari/pipelines" -__copyright__ = "© 2019-2020 Nekokatt" +__copyright__ = "© 2020 Nekokatt" __discord_invite__ = "https://discord.gg/Jx4cNGG" __docs__ = "https://nekokatt.gitlab.io/hikari" __email__ = "3903853-nekokatt@users.noreply.gitlab.com" __issue_tracker__ = "https://gitlab.com/nekokatt/hikari/issues" __is_official_distributed_release__ = False -__license__ = "LGPL-3.0-ONLY" +__license__ = "MIT" __url__ = "https://gitlab.com/nekokatt/hikari" __version__ = "2.0.0.dev0" __git_branch__ = "development" diff --git a/hikari/api/__init__.py b/hikari/api/__init__.py index 43314147d0..5d4ff9b387 100644 --- a/hikari/api/__init__.py +++ b/hikari/api/__init__.py @@ -1,21 +1,24 @@ # -*- coding: utf-8 -*- # cython: language_level=3 -# Copyright © Nekoka.tt 2019-2020 +# Copyright (c) 2020 Nekokatt # -# This file is part of Hikari. +# 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: # -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. # -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . +# 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. """Interfaces for components that make up Hikari applications. These are provided to uncouple specific implementation details from each diff --git a/hikari/api/app.py b/hikari/api/app.py index 9dd626c471..ed56ec0947 100644 --- a/hikari/api/app.py +++ b/hikari/api/app.py @@ -1,21 +1,24 @@ # -*- coding: utf-8 -*- # cython: language_level=3 -# Copyright © Nekoka.tt 2019-2020 +# Copyright (c) 2020 Nekokatt # -# This file is part of Hikari. +# 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: # -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. # -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . +# 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. """Core app interface for application implementations.""" from __future__ import annotations diff --git a/hikari/api/bot.py b/hikari/api/bot.py index 549f29c4fb..51dad630d7 100644 --- a/hikari/api/bot.py +++ b/hikari/api/bot.py @@ -1,21 +1,24 @@ # -*- coding: utf-8 -*- # cython: language_level=3 -# Copyright © Nekoka.tt 2019-2020 +# Copyright (c) 2020 Nekokatt # -# This file is part of Hikari. +# 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: # -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. # -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . +# 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. """Core interfaces for types of Hikari application.""" from __future__ import annotations diff --git a/hikari/api/cache.py b/hikari/api/cache.py index d2bba397a6..d2065b238a 100644 --- a/hikari/api/cache.py +++ b/hikari/api/cache.py @@ -1,21 +1,24 @@ # -*- coding: utf-8 -*- # cython: language_level=3 -# Copyright © Nekoka.tt 2019-2020 +# Copyright (c) 2020 Nekokatt # -# This file is part of Hikari. +# 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: # -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. # -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . +# 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. """Core interface for a cache implementation.""" from __future__ import annotations diff --git a/hikari/api/component.py b/hikari/api/component.py index 8955a00196..f2375c1c34 100644 --- a/hikari/api/component.py +++ b/hikari/api/component.py @@ -1,21 +1,24 @@ # -*- coding: utf-8 -*- # cython: language_level=3 -# Copyright © Nekoka.tt 2019-2020 +# Copyright (c) 2020 Nekokatt # -# This file is part of Hikari. +# 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: # -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. # -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . +# 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. """Base interface for any internal components of an application.""" from __future__ import annotations diff --git a/hikari/api/entity_factory.py b/hikari/api/entity_factory.py index 5708a80b84..fdc3ba73ce 100644 --- a/hikari/api/entity_factory.py +++ b/hikari/api/entity_factory.py @@ -1,21 +1,24 @@ # -*- coding: utf-8 -*- # cython: language_level=3 -# Copyright © Nekoka.tt 2019-2020 +# Copyright (c) 2020 Nekokatt # -# This file is part of Hikari. +# 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: # -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. # -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . +# 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. """Core interface for an object that serializes/deserializes API objects.""" from __future__ import annotations diff --git a/hikari/api/event_consumer.py b/hikari/api/event_consumer.py index fb9609bc50..ce0833e8db 100644 --- a/hikari/api/event_consumer.py +++ b/hikari/api/event_consumer.py @@ -1,21 +1,24 @@ # -*- coding: utf-8 -*- # cython: language_level=3 -# Copyright © Nekoka.tt 2019-2020 +# Copyright (c) 2020 Nekokatt # -# This file is part of Hikari. +# 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: # -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. # -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . +# 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. """Core interface for components that consume raw API event payloads.""" from __future__ import annotations diff --git a/hikari/api/event_dispatcher.py b/hikari/api/event_dispatcher.py index cbf7f06e02..0f02dc7feb 100644 --- a/hikari/api/event_dispatcher.py +++ b/hikari/api/event_dispatcher.py @@ -1,21 +1,24 @@ # -*- coding: utf-8 -*- # cython: language_level=3 -# Copyright © Nekoka.tt 2019-2020 +# Copyright (c) 2020 Nekokatt # -# This file is part of Hikari. +# 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: # -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. # -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . +# 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. """Core interface for components that dispatch events to the library.""" from __future__ import annotations diff --git a/hikari/api/event_factory.py b/hikari/api/event_factory.py index b7c864bbbd..c3bf9a2e9c 100644 --- a/hikari/api/event_factory.py +++ b/hikari/api/event_factory.py @@ -1,21 +1,24 @@ # -*- coding: utf-8 -*- # cython: language_level=3 -# Copyright © Nekoka.tt 2019-2020 +# Copyright (c) 2020 Nekokatt # -# This file is part of Hikari. +# 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: # -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. # -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . +# 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. """Component that provides the ability to generate event models.""" from __future__ import annotations diff --git a/hikari/api/guild_chunker.py b/hikari/api/guild_chunker.py index 35302f00c3..744c4d505c 100644 --- a/hikari/api/guild_chunker.py +++ b/hikari/api/guild_chunker.py @@ -1,21 +1,24 @@ # -*- coding: utf-8 -*- # cython: language_level=3 -# Copyright © Nekoka.tt 2019-2020 +# Copyright (c) 2020 Nekokatt # -# This file is part of Hikari. +# 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: # -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. # -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . +# 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. """Component that provides the ability manage guild chunking.""" from __future__ import annotations diff --git a/hikari/api/rest.py b/hikari/api/rest.py index 1b50b06c82..3f532fdbcb 100644 --- a/hikari/api/rest.py +++ b/hikari/api/rest.py @@ -1,21 +1,24 @@ # -*- coding: utf-8 -*- # cython: language_level=3 -# Copyright © Nekoka.tt 2019-2020 +# Copyright (c) 2020 Nekokatt # -# This file is part of Hikari. +# 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: # -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. # -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . +# 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. """Provides an interface for REST API implementations to follow.""" from __future__ import annotations diff --git a/hikari/api/shard.py b/hikari/api/shard.py index 27178b8309..588f5c18b6 100644 --- a/hikari/api/shard.py +++ b/hikari/api/shard.py @@ -1,21 +1,24 @@ # -*- coding: utf-8 -*- # cython: language_level=3 -# Copyright © Nekoka.tt 2019-2020 +# Copyright (c) 2020 Nekokatt # -# This file is part of Hikari. +# 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: # -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. # -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . +# 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. """Provides an interface for gateway shard implementations to conform to.""" from __future__ import annotations diff --git a/hikari/api/special_endpoints.py b/hikari/api/special_endpoints.py index 19fb143598..284a81c07e 100644 --- a/hikari/api/special_endpoints.py +++ b/hikari/api/special_endpoints.py @@ -1,21 +1,24 @@ # -*- coding: utf-8 -*- # cython: language_level=3 -# Copyright © Nekoka.tt 2019-2020 +# Copyright (c) 2020 Nekokatt # -# This file is part of Hikari. +# 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: # -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. # -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . +# 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. """Special additional endpoints used by the REST API.""" from __future__ import annotations diff --git a/hikari/api/voice.py b/hikari/api/voice.py index 163ba1918e..ea849c45a6 100644 --- a/hikari/api/voice.py +++ b/hikari/api/voice.py @@ -1,21 +1,24 @@ # -*- coding: utf-8 -*- # cython: language_level=3 -# Copyright © Nekoka.tt 2019-2020 +# Copyright (c) 2020 Nekokatt # -# This file is part of Hikari. +# 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: # -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. # -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . +# 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. """Interfaces used to describe voice client implementations.""" from __future__ import annotations diff --git a/hikari/banner.txt b/hikari/banner.txt index c0ec3b97f9..b8929b6bcf 100644 --- a/hikari/banner.txt +++ b/hikari/banner.txt @@ -1,5 +1,5 @@ - ${red}oooo o8o oooo o8o ${default}${bright}${hikari_copyright} ${hikari_license}${default} + ${red}oooo o8o oooo o8o ${default}${bright}${hikari_copyright}, licensed under ${hikari_license}${default} ${bright_red}`888 `"' `888 `"' ${default}${dim}Installed at: ${bright}${cyan}${underline}${hikari_install_location}${default} ${yellow} 888 .oo. oooo 888 oooo .oooo. oooo d8b oooo ${default}${dim}Support: ${bright}${cyan}${underline}${hikari_discord_invite}${default} ${bright_green} 888P"Y88b `888 888 .8P' `P )88b `888""8P `888 ${default}${dim}Documentation: ${bright}${cyan}${underline}${hikari_documentation_url}${default} diff --git a/hikari/cli.py b/hikari/cli.py index 9655154c75..a798570ba8 100644 --- a/hikari/cli.py +++ b/hikari/cli.py @@ -1,21 +1,24 @@ # -*- coding: utf-8 -*- # cython: language_level=3 -# Copyright © Nekoka.tt 2019-2020 +# Copyright (c) 2020 Nekokatt # -# This file is part of Hikari. +# 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: # -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. # -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . +# 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. """Provides the `python -m hikari` and `hikari` commands to the shell.""" from __future__ import annotations diff --git a/hikari/config.py b/hikari/config.py index f9708c7efd..a0f853fe1f 100644 --- a/hikari/config.py +++ b/hikari/config.py @@ -1,21 +1,24 @@ # -*- coding: utf-8 -*- # cython: language_level=3 -# Copyright © Nekoka.tt 2019-2020 +# Copyright (c) 2020 Nekokatt # -# This file is part of Hikari. +# 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: # -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. # -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . +# 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. """Data class containing network-related configuration settings.""" from __future__ import annotations diff --git a/hikari/errors.py b/hikari/errors.py index 8a667b1d48..55a4544598 100644 --- a/hikari/errors.py +++ b/hikari/errors.py @@ -1,21 +1,24 @@ # -*- coding: utf-8 -*- # cython: language_level=3 -# Copyright © Nekoka.tt 2019-2020 +# Copyright (c) 2020 Nekokatt # -# This file is part of Hikari. +# 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: # -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. # -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . +# 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. """Exceptions and warnings that can be thrown by this library.""" from __future__ import annotations diff --git a/hikari/events/__init__.py b/hikari/events/__init__.py index 31f21c90cc..01955643e7 100644 --- a/hikari/events/__init__.py +++ b/hikari/events/__init__.py @@ -1,21 +1,24 @@ # -*- coding: utf-8 -*- # cython: language_level=3 -# Copyright © Nekoka.tt 2019-2020 +# Copyright (c) 2020 Nekokatt # -# This file is part of Hikari. +# 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: # -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. # -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . +# 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. """Events that can be fired by Hikari's gateway implementation.""" from __future__ import annotations diff --git a/hikari/events/base_events.py b/hikari/events/base_events.py index bb837dd4e7..07b52d248c 100644 --- a/hikari/events/base_events.py +++ b/hikari/events/base_events.py @@ -1,21 +1,24 @@ # -*- coding: utf-8 -*- # cython: language_level=3 -# Copyright © Nekoka.tt 2019-2020 +# Copyright (c) 2020 Nekokatt # -# This file is part of Hikari. +# 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: # -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. # -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . +# 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. """Base types and functions for events in Hikari.""" from __future__ import annotations diff --git a/hikari/events/channel_events.py b/hikari/events/channel_events.py index b4d0dc0b8e..a0d4d5e833 100644 --- a/hikari/events/channel_events.py +++ b/hikari/events/channel_events.py @@ -1,21 +1,24 @@ # -*- coding: utf-8 -*- # cython: language_level=3 -# Copyright © Nekoka.tt 2019-2020 +# Copyright (c) 2020 Nekokatt # -# This file is part of Hikari. +# 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: # -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. # -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . +# 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. """Events that fire when channels are modified. This does not include message events, nor reaction events. diff --git a/hikari/events/guild_events.py b/hikari/events/guild_events.py index dc19980433..46fe32f74d 100644 --- a/hikari/events/guild_events.py +++ b/hikari/events/guild_events.py @@ -1,21 +1,24 @@ # -*- coding: utf-8 -*- # cython: language_level=3 -# Copyright © Nekoka.tt 2019-2020 +# Copyright (c) 2020 Nekokatt # -# This file is part of Hikari. +# 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: # -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. # -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . +# 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. """Events that fire when something occurs within a guild.""" from __future__ import annotations diff --git a/hikari/events/lifetime_events.py b/hikari/events/lifetime_events.py index 93c72ead54..1f7fa02791 100644 --- a/hikari/events/lifetime_events.py +++ b/hikari/events/lifetime_events.py @@ -1,21 +1,24 @@ # -*- coding: utf-8 -*- # cython: language_level=3 -# Copyright © Nekoka.tt 2019-2020 +# Copyright (c) 2020 Nekokatt # -# This file is part of Hikari. +# 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: # -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. # -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . +# 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. """Events that are bound to the lifetime of an application. These are types of hooks that can be invoked on startup or shutdown, which can diff --git a/hikari/events/member_events.py b/hikari/events/member_events.py index 1a3bf8cc67..dc5122449d 100644 --- a/hikari/events/member_events.py +++ b/hikari/events/member_events.py @@ -1,21 +1,24 @@ # -*- coding: utf-8 -*- # cython: language_level=3 -# Copyright © Nekoka.tt 2019-2020 +# Copyright (c) 2020 Nekokatt # -# This file is part of Hikari. +# 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: # -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. # -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . +# 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. """Events concerning manipulation of members within guilds.""" from __future__ import annotations diff --git a/hikari/events/message_events.py b/hikari/events/message_events.py index 048963e6cd..f8499877dc 100644 --- a/hikari/events/message_events.py +++ b/hikari/events/message_events.py @@ -1,21 +1,24 @@ # -*- coding: utf-8 -*- # cython: language_level=3 -# Copyright © Nekoka.tt 2019-2020 +# Copyright (c) 2020 Nekokatt # -# This file is part of Hikari. +# 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: # -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. # -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . +# 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. """Events that fire if messages are sent/updated/deleted.""" from __future__ import annotations diff --git a/hikari/events/reaction_events.py b/hikari/events/reaction_events.py index fc1793f5f8..e5fc460e3c 100644 --- a/hikari/events/reaction_events.py +++ b/hikari/events/reaction_events.py @@ -1,21 +1,24 @@ # -*- coding: utf-8 -*- # cython: language_level=3 -# Copyright © Nekoka.tt 2019-2020 +# Copyright (c) 2020 Nekokatt # -# This file is part of Hikari. +# 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: # -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. # -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . +# 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. """Events that fire if messages are reacted to.""" from __future__ import annotations diff --git a/hikari/events/role_events.py b/hikari/events/role_events.py index 90b2ffc2ac..6c6b7a578c 100644 --- a/hikari/events/role_events.py +++ b/hikari/events/role_events.py @@ -1,21 +1,24 @@ # -*- coding: utf-8 -*- # cython: language_level=3 -# Copyright © Nekoka.tt 2019-2020 +# Copyright (c) 2020 Nekokatt # -# This file is part of Hikari. +# 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: # -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. # -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . +# 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. """Events pertaining to manipulation of roles within guilds.""" from __future__ import annotations diff --git a/hikari/events/shard_events.py b/hikari/events/shard_events.py index d87807172b..dd1f486ecb 100644 --- a/hikari/events/shard_events.py +++ b/hikari/events/shard_events.py @@ -1,21 +1,24 @@ # -*- coding: utf-8 -*- # cython: language_level=3 -# Copyright © Nekoka.tt 2019-2020 +# Copyright (c) 2020 Nekokatt # -# This file is part of Hikari. +# 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: # -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. # -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . +# 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. """Events relating to specific shards connecting and disconnecting.""" from __future__ import annotations diff --git a/hikari/events/typing_events.py b/hikari/events/typing_events.py index e9566c24eb..f781016351 100644 --- a/hikari/events/typing_events.py +++ b/hikari/events/typing_events.py @@ -1,21 +1,24 @@ # -*- coding: utf-8 -*- # cython: language_level=3 -# Copyright © Nekoka.tt 2019-2020 +# Copyright (c) 2020 Nekokatt # -# This file is part of Hikari. +# 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: # -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. # -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . +# 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. """Events fired when users begin typing in channels.""" from __future__ import annotations diff --git a/hikari/events/user_events.py b/hikari/events/user_events.py index 3aec23cf4d..305a658e29 100644 --- a/hikari/events/user_events.py +++ b/hikari/events/user_events.py @@ -1,21 +1,24 @@ # -*- coding: utf-8 -*- # cython: language_level=3 -# Copyright © Nekoka.tt 2019-2020 +# Copyright (c) 2020 Nekokatt # -# This file is part of Hikari. +# 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: # -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. # -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . +# 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. """Events fired when the account user is updated.""" from __future__ import annotations diff --git a/hikari/events/voice_events.py b/hikari/events/voice_events.py index 76ba3705bc..f81335206f 100644 --- a/hikari/events/voice_events.py +++ b/hikari/events/voice_events.py @@ -1,21 +1,24 @@ # -*- coding: utf-8 -*- # cython: language_level=3 -# Copyright © Nekoka.tt 2019-2020 +# Copyright (c) 2020 Nekokatt # -# This file is part of Hikari. +# 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: # -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. # -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . +# 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. """Events that fire when voice state changes.""" from __future__ import annotations diff --git a/hikari/impl/__init__.py b/hikari/impl/__init__.py index f2f15412ae..8b80232ee3 100644 --- a/hikari/impl/__init__.py +++ b/hikari/impl/__init__.py @@ -1,21 +1,24 @@ # -*- coding: utf-8 -*- # cython: language_level=3 -# Copyright © Nekoka.tt 2019-2020 +# Copyright (c) 2020 Nekokatt # -# This file is part of Hikari. +# 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: # -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. # -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . +# 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. """Basic implementations of application components. These components implement the interfaces in `hikari.api` to provide the diff --git a/hikari/impl/bot.py b/hikari/impl/bot.py index 48b7753503..6df97c8404 100644 --- a/hikari/impl/bot.py +++ b/hikari/impl/bot.py @@ -1,21 +1,24 @@ # -*- coding: utf-8 -*- # cython: language_level=3 -# Copyright © Nekoka.tt 2019-2020 +# Copyright (c) 2020 Nekokatt # -# This file is part of Hikari. +# 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: # -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. # -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . +# 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. """Basic implementation the components for a single-process bot.""" from __future__ import annotations diff --git a/hikari/impl/buckets.py b/hikari/impl/buckets.py index a72759229d..20ad04e83f 100644 --- a/hikari/impl/buckets.py +++ b/hikari/impl/buckets.py @@ -1,21 +1,24 @@ # -*- coding: utf-8 -*- # cython: language_level=3 -# Copyright © Nekoka.tt 2019-2020 +# Copyright (c) 2020 Nekokatt # -# This file is part of Hikari. +# 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: # -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. # -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . +# 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. """Rate-limit extensions for RESTful bucketed endpoints. Provides implementations for the complex rate limiting mechanisms that Discord diff --git a/hikari/impl/entity_factory.py b/hikari/impl/entity_factory.py index bb8f223794..2f4352fa88 100644 --- a/hikari/impl/entity_factory.py +++ b/hikari/impl/entity_factory.py @@ -1,21 +1,24 @@ # -*- coding: utf-8 -*- # cython: language_level=3 -# Copyright © Nekoka.tt 2019-2020 +# Copyright (c) 2020 Nekokatt # -# This file is part of Hikari. +# 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: # -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. # -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . +# 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. """Basic implementation of an entity factory for general bots and HTTP apps.""" from __future__ import annotations diff --git a/hikari/impl/event_factory.py b/hikari/impl/event_factory.py index 272ade7203..309eb338c5 100644 --- a/hikari/impl/event_factory.py +++ b/hikari/impl/event_factory.py @@ -1,21 +1,24 @@ # -*- coding: utf-8 -*- # cython: language_level=3 -# Copyright © Nekoka.tt 2019-2020 +# Copyright (c) 2020 Nekokatt # -# This file is part of Hikari. +# 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: # -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. # -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . +# 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. """Implementation for a singleton bot event factory.""" from __future__ import annotations diff --git a/hikari/impl/event_manager_base.py b/hikari/impl/event_manager_base.py index 7a66d856f4..896904ec16 100644 --- a/hikari/impl/event_manager_base.py +++ b/hikari/impl/event_manager_base.py @@ -1,21 +1,24 @@ # -*- coding: utf-8 -*- # cython: language_level=3 -# Copyright © Nekoka.tt 2019-2020 +# Copyright (c) 2020 Nekokatt # -# This file is part of Hikari. +# 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: # -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. # -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . +# 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. """A base implementation for an event manager.""" from __future__ import annotations diff --git a/hikari/impl/rate_limits.py b/hikari/impl/rate_limits.py index 0399fd3869..af1f90270c 100644 --- a/hikari/impl/rate_limits.py +++ b/hikari/impl/rate_limits.py @@ -1,21 +1,24 @@ # -*- coding: utf-8 -*- # cython: language_level=3 -# Copyright © Nekoka.tt 2019-2020 +# Copyright (c) 2020 Nekokatt # -# This file is part of Hikari. +# 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: # -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. # -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . +# 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. """Basic lazy ratelimit systems for asyncio. See `hikari.impl.buckets` for HTTP-specific rate-limiting logic. diff --git a/hikari/impl/rest.py b/hikari/impl/rest.py index 49afafee23..a369a3d38c 100644 --- a/hikari/impl/rest.py +++ b/hikari/impl/rest.py @@ -1,21 +1,24 @@ # -*- coding: utf-8 -*- # cython: language_level=3 -# Copyright © Nekoka.tt 2019-2020 +# Copyright (c) 2020 Nekokatt # -# This file is part of Hikari. +# 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: # -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. # -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . +# 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. """Implementation of a V6 and V7 compatible HTTP API for Discord. This also includes implementations of `hikari.api.app.IApp` designed towards diff --git a/hikari/impl/shard.py b/hikari/impl/shard.py index 2154973c63..b4546c5a85 100644 --- a/hikari/impl/shard.py +++ b/hikari/impl/shard.py @@ -1,21 +1,24 @@ # -*- coding: utf-8 -*- # cython: language_level=3 -# Copyright © Nekoka.tt 2019-2020 +# Copyright (c) 2020 Nekokatt # -# This file is part of Hikari. +# 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: # -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. # -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . +# 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. """Single-shard implementation for the V6 and V7 event gateway for Discord.""" from __future__ import annotations diff --git a/hikari/impl/special_endpoints.py b/hikari/impl/special_endpoints.py index a2eb222d2d..1df9994df1 100644 --- a/hikari/impl/special_endpoints.py +++ b/hikari/impl/special_endpoints.py @@ -1,21 +1,24 @@ # -*- coding: utf-8 -*- # cython: language_level=3 -# Copyright © Nekoka.tt 2019-2020 +# Copyright (c) 2020 Nekokatt # -# This file is part of Hikari. +# 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: # -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. # -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . +# 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. """Special endpoint implementations. You should never need to make any of these objects manually. diff --git a/hikari/impl/stateful_cache.py b/hikari/impl/stateful_cache.py index f90af36ce5..ef8005bef5 100644 --- a/hikari/impl/stateful_cache.py +++ b/hikari/impl/stateful_cache.py @@ -1,21 +1,24 @@ # -*- coding: utf-8 -*- # cython: language_level=3 -# Copyright © Nekoka.tt 2019-2020 +# Copyright (c) 2020 Nekokatt # -# This file is part of Hikari. +# 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: # -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. # -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . +# 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. """Basic implementation of a cache for general bots and gateway apps.""" from __future__ import annotations diff --git a/hikari/impl/stateful_event_manager.py b/hikari/impl/stateful_event_manager.py index 5effab2d09..99e4a450e6 100644 --- a/hikari/impl/stateful_event_manager.py +++ b/hikari/impl/stateful_event_manager.py @@ -1,21 +1,24 @@ # -*- coding: utf-8 -*- # cython: language_level=3 -# Copyright © Nekoka.tt 2019-2020 +# Copyright (c) 2020 Nekokatt # -# This file is part of Hikari. +# 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: # -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. # -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . +# 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. """Event handling logic for more info.""" from __future__ import annotations diff --git a/hikari/impl/stateful_guild_chunker.py b/hikari/impl/stateful_guild_chunker.py index 1ee0804acb..01aec285a5 100644 --- a/hikari/impl/stateful_guild_chunker.py +++ b/hikari/impl/stateful_guild_chunker.py @@ -1,21 +1,24 @@ # -*- coding: utf-8 -*- # cython: language_level=3 -# Copyright © Nekoka.tt 2019-2020 +# Copyright (c) 2020 Nekokatt # -# This file is part of Hikari. +# 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: # -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. # -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . +# 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. """Basic implementation of a guild chunker.""" from __future__ import annotations diff --git a/hikari/impl/stateless_cache.py b/hikari/impl/stateless_cache.py index 87f1dfe63f..74c76b63df 100644 --- a/hikari/impl/stateless_cache.py +++ b/hikari/impl/stateless_cache.py @@ -1,21 +1,24 @@ # -*- coding: utf-8 -*- # cython: language_level=3 -# Copyright © Nekoka.tt 2019-2020 +# Copyright (c) 2020 Nekokatt # -# This file is part of Hikari. +# 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: # -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. # -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . +# 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. """Bare-bones implementation of a cache that never stores anything. This is used to enable compatibility with HTTP applications and stateless diff --git a/hikari/impl/stateless_event_manager.py b/hikari/impl/stateless_event_manager.py index dc71f86762..a9ba1d6874 100644 --- a/hikari/impl/stateless_event_manager.py +++ b/hikari/impl/stateless_event_manager.py @@ -1,21 +1,24 @@ # -*- coding: utf-8 -*- # cython: language_level=3 -# Copyright © Nekoka.tt 2019-2020 +# Copyright (c) 2020 Nekokatt # -# This file is part of Hikari. +# 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: # -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. # -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . +# 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. """Event handling logic.""" from __future__ import annotations diff --git a/hikari/impl/stateless_guild_chunker.py b/hikari/impl/stateless_guild_chunker.py index 36b356cb73..c3f7967e4a 100644 --- a/hikari/impl/stateless_guild_chunker.py +++ b/hikari/impl/stateless_guild_chunker.py @@ -1,21 +1,24 @@ # -*- coding: utf-8 -*- # cython: language_level=3 -# Copyright © Nekoka.tt 2019-2020 +# Copyright (c) 2020 Nekokatt # -# This file is part of Hikari. +# 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: # -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. # -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . +# 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. """Basic implementation of a guild chunker.""" from __future__ import annotations diff --git a/hikari/impl/voice.py b/hikari/impl/voice.py index 383152a76b..aeee4c4847 100644 --- a/hikari/impl/voice.py +++ b/hikari/impl/voice.py @@ -1,21 +1,24 @@ # -*- coding: utf-8 -*- # cython: language_level=3 -# Copyright © Nekoka.tt 2019-2020 +# Copyright (c) 2020 Nekokatt # -# This file is part of Hikari. +# 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: # -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. # -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . +# 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. """Implementation of a simple voice management system.""" from __future__ import annotations diff --git a/hikari/models/__init__.py b/hikari/models/__init__.py index 0c53a724c9..93ef17cf81 100644 --- a/hikari/models/__init__.py +++ b/hikari/models/__init__.py @@ -1,21 +1,24 @@ # -*- coding: utf-8 -*- # cython: language_level=3 -# Copyright © Nekoka.tt 2019-2020 +# Copyright (c) 2020 Nekokatt # -# This file is part of Hikari. +# 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: # -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. # -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . +# 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. """Data classes representing Discord entities.""" from hikari.models import applications diff --git a/hikari/models/applications.py b/hikari/models/applications.py index 3ac75f3681..6ca097c4a7 100644 --- a/hikari/models/applications.py +++ b/hikari/models/applications.py @@ -1,21 +1,24 @@ # -*- coding: utf-8 -*- # cython: language_level=3 -# Copyright © Nekoka.tt 2019-2020 +# Copyright (c) 2020 Nekokatt # -# This file is part of Hikari. +# 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: # -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. # -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . +# 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. """Application and entities related to discord's Oauth2 flow.""" from __future__ import annotations diff --git a/hikari/models/audit_logs.py b/hikari/models/audit_logs.py index 30588f23ce..9232d45285 100644 --- a/hikari/models/audit_logs.py +++ b/hikari/models/audit_logs.py @@ -1,21 +1,24 @@ # -*- coding: utf-8 -*- # cython: language_level=3 -# Copyright © Nekoka.tt 2019-2020 +# Copyright (c) 2020 Nekokatt # -# This file is part of Hikari. +# 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: # -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. # -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . +# 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. """Application and entities that are used to describe audit logs on Discord.""" from __future__ import annotations diff --git a/hikari/models/channels.py b/hikari/models/channels.py index 70e8804a1a..0a7331129f 100644 --- a/hikari/models/channels.py +++ b/hikari/models/channels.py @@ -1,21 +1,24 @@ # -*- coding: utf-8 -*- # cython: language_level=3 -# Copyright © Nekoka.tt 2019-2020 +# Copyright (c) 2020 Nekokatt # -# This file is part of Hikari. +# 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: # -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. # -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . +# 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. """Application and entities that are used to describe both DMs and guild channels on Discord.""" from __future__ import annotations diff --git a/hikari/models/colors.py b/hikari/models/colors.py index 415f5da6b9..08e92a00a5 100644 --- a/hikari/models/colors.py +++ b/hikari/models/colors.py @@ -1,21 +1,24 @@ # -*- coding: utf-8 -*- # cython: language_level=3 -# Copyright © Nekoka.tt 2019-2020 +# Copyright (c) 2020 Nekokatt # -# This file is part of Hikari. +# 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: # -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. # -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . +# 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. """Model that represents a common RGB color and provides simple conversions to other common color systems.""" from __future__ import annotations diff --git a/hikari/models/colours.py b/hikari/models/colours.py index f9c5cbc766..912027ced8 100644 --- a/hikari/models/colours.py +++ b/hikari/models/colours.py @@ -1,21 +1,24 @@ # -*- coding: utf-8 -*- # cython: language_level=3 -# Copyright © Nekoka.tt 2019-2020 +# Copyright (c) 2020 Nekokatt # -# This file is part of Hikari. +# 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: # -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. # -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . +# 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. """Alias for the `hikari.models.colors` module.""" from __future__ import annotations diff --git a/hikari/models/embeds.py b/hikari/models/embeds.py index ebe0d6bb52..b3fb6873ab 100644 --- a/hikari/models/embeds.py +++ b/hikari/models/embeds.py @@ -1,21 +1,24 @@ # -*- coding: utf-8 -*- # cython: language_level=3 -# Copyright © Nekoka.tt 2019-2020 +# Copyright (c) 2020 Nekokatt # -# This file is part of Hikari. +# 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: # -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. # -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . +# 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. """Application and entities that are used to describe message embeds on Discord.""" from __future__ import annotations diff --git a/hikari/models/emojis.py b/hikari/models/emojis.py index 478368ccd4..674ec7c9b2 100644 --- a/hikari/models/emojis.py +++ b/hikari/models/emojis.py @@ -1,21 +1,24 @@ # -*- coding: utf-8 -*- # cython: language_level=3 -# Copyright © Nekoka.tt 2019-2020 +# Copyright (c) 2020 Nekokatt # -# This file is part of Hikari. +# 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: # -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. # -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . +# 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. """Application and entities that are used to describe emojis on Discord.""" from __future__ import annotations diff --git a/hikari/models/gateway.py b/hikari/models/gateway.py index 7d6b6c211d..28bb31deb1 100644 --- a/hikari/models/gateway.py +++ b/hikari/models/gateway.py @@ -1,21 +1,24 @@ # -*- coding: utf-8 -*- # cython: language_level=3 -# Copyright © Nekoka.tt 2019-2020 +# Copyright (c) 2020 Nekokatt # -# This file is part of Hikari. +# 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: # -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. # -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . +# 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. """Entities directly related to creating and managing gateway shards.""" from __future__ import annotations diff --git a/hikari/models/guilds.py b/hikari/models/guilds.py index 02eb00dc4b..9800a7960d 100644 --- a/hikari/models/guilds.py +++ b/hikari/models/guilds.py @@ -1,21 +1,24 @@ # -*- coding: utf-8 -*- # cython: language_level=3 -# Copyright © Nekoka.tt 2019-2020 +# Copyright (c) 2020 Nekokatt # -# This file is part of Hikari. +# 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: # -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. # -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . +# 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. """Application and entities that are used to describe guilds on Discord.""" from __future__ import annotations diff --git a/hikari/models/intents.py b/hikari/models/intents.py index d4e0801370..1acc5f860f 100644 --- a/hikari/models/intents.py +++ b/hikari/models/intents.py @@ -1,21 +1,24 @@ # -*- coding: utf-8 -*- # cython: language_level=3 -# Copyright © Nekoka.tt 2019-2020 +# Copyright (c) 2020 Nekokatt # -# This file is part of Hikari. +# 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: # -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. # -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . +# 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. """Shard intents for controlling which events the application receives. All intents in the `Intent` class are exported to this package, diff --git a/hikari/models/invites.py b/hikari/models/invites.py index 66162d2559..3f7a02bb13 100644 --- a/hikari/models/invites.py +++ b/hikari/models/invites.py @@ -1,21 +1,24 @@ # -*- coding: utf-8 -*- # cython: language_level=3 -# Copyright © Nekoka.tt 2019-2020 +# Copyright (c) 2020 Nekokatt # -# This file is part of Hikari. +# 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: # -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. # -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . +# 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. """Application and entities that are used to describe invites on Discord.""" from __future__ import annotations diff --git a/hikari/models/messages.py b/hikari/models/messages.py index bb8c1318b1..371d10978c 100644 --- a/hikari/models/messages.py +++ b/hikari/models/messages.py @@ -1,21 +1,24 @@ # -*- coding: utf-8 -*- # cython: language_level=3 -# Copyright © Nekoka.tt 2019-2020 +# Copyright (c) 2020 Nekokatt # -# This file is part of Hikari. +# 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: # -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. # -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . +# 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. """Application and entities that are used to describe messages on Discord.""" from __future__ import annotations diff --git a/hikari/models/permissions.py b/hikari/models/permissions.py index 0a0511dafe..3b53d82a06 100644 --- a/hikari/models/permissions.py +++ b/hikari/models/permissions.py @@ -1,21 +1,24 @@ # -*- coding: utf-8 -*- # cython: language_level=3 -# Copyright © Nekoka.tt 2019-2020 +# Copyright (c) 2020 Nekokatt # -# This file is part of Hikari. +# 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: # -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. # -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . +# 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. """Bitfield of permissions. All permissions in the `Permissions` class are exported to this package, diff --git a/hikari/models/presences.py b/hikari/models/presences.py index 457c0dbb67..56d6b112a3 100644 --- a/hikari/models/presences.py +++ b/hikari/models/presences.py @@ -1,21 +1,24 @@ # -*- coding: utf-8 -*- # cython: language_level=3 -# Copyright © Nekoka.tt 2019-2020 +# Copyright (c) 2020 Nekokatt # -# This file is part of Hikari. +# 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: # -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. # -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . +# 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. """Application and entities that are used to describe guilds on Discord.""" from __future__ import annotations diff --git a/hikari/models/users.py b/hikari/models/users.py index f416df028f..ccbd056610 100644 --- a/hikari/models/users.py +++ b/hikari/models/users.py @@ -1,21 +1,24 @@ # -*- coding: utf-8 -*- # cython: language_level=3 -# Copyright © Nekoka.tt 2019-2020 +# Copyright (c) 2020 Nekokatt # -# This file is part of Hikari. +# 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: # -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. # -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . +# 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. """Application and entities that are used to describe Users on Discord.""" from __future__ import annotations diff --git a/hikari/models/voices.py b/hikari/models/voices.py index 74f68b1e2b..8ba24f7b94 100644 --- a/hikari/models/voices.py +++ b/hikari/models/voices.py @@ -1,21 +1,24 @@ # -*- coding: utf-8 -*- # cython: language_level=3 -# Copyright © Nekoka.tt 2019-2020 +# Copyright (c) 2020 Nekokatt # -# This file is part of Hikari. +# 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: # -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. # -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . +# 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. """Application and entities that are used to describe voice state on Discord.""" from __future__ import annotations diff --git a/hikari/models/webhooks.py b/hikari/models/webhooks.py index eeda334bd7..ee04a42017 100644 --- a/hikari/models/webhooks.py +++ b/hikari/models/webhooks.py @@ -1,21 +1,24 @@ # -*- coding: utf-8 -*- # cython: language_level=3 -# Copyright © Nekoka.tt 2019-2020 +# Copyright (c) 2020 Nekokatt # -# This file is part of Hikari. +# 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: # -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. # -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . +# 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. """Application and entities that are used to describe webhooks on Discord.""" from __future__ import annotations diff --git a/hikari/utilities/__init__.py b/hikari/utilities/__init__.py index 45d6d4a87d..222ea7ca96 100644 --- a/hikari/utilities/__init__.py +++ b/hikari/utilities/__init__.py @@ -1,21 +1,24 @@ # -*- coding: utf-8 -*- # cython: language_level=3 -# Copyright © Nekoka.tt 2019-2020 +# Copyright (c) 2020 Nekokatt # -# This file is part of Hikari. +# 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: # -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. # -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . +# 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. """Package containing internal utilities used within this API.""" __all__ = [] # type: ignore[var-annotated] diff --git a/hikari/utilities/aio.py b/hikari/utilities/aio.py index e301091f9f..8c5ac19e9c 100644 --- a/hikari/utilities/aio.py +++ b/hikari/utilities/aio.py @@ -1,21 +1,24 @@ # -*- coding: utf-8 -*- # cython: language_level=3 -# Copyright © Nekoka.tt 2019-2020 +# Copyright (c) 2020 Nekokatt # -# This file is part of Hikari. +# 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: # -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. # -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . +# 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. """Asyncio extensions and utilities.""" from __future__ import annotations diff --git a/hikari/utilities/art.py b/hikari/utilities/art.py index ddae2aeba4..39e2eab77e 100644 --- a/hikari/utilities/art.py +++ b/hikari/utilities/art.py @@ -1,20 +1,24 @@ # -*- coding: utf-8 -*- -# Copyright © Nekoka.tt 2019-2020 +# cython: language_level=3 +# Copyright (c) 2020 Nekokatt # -# This file is part of Hikari. +# 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: # -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. # -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . +# 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. """Stuff for handling banners and pretty logging.""" from __future__ import annotations diff --git a/hikari/utilities/constants.py b/hikari/utilities/constants.py index d869bff03e..9043d17386 100644 --- a/hikari/utilities/constants.py +++ b/hikari/utilities/constants.py @@ -1,21 +1,24 @@ # -*- coding: utf-8 -*- # cython: language_level=3 -# Copyright © Nekoka.tt 2019-2020 +# Copyright (c) 2020 Nekokatt # -# This file is part of Hikari. +# 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: # -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. # -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . +# 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. """Various strings used in multiple places.""" from __future__ import annotations diff --git a/hikari/utilities/data_binding.py b/hikari/utilities/data_binding.py index db32569f1e..afc863a3a1 100644 --- a/hikari/utilities/data_binding.py +++ b/hikari/utilities/data_binding.py @@ -1,21 +1,24 @@ # -*- coding: utf-8 -*- # cython: language_level=3 -# Copyright © Nekoka.tt 2019-2020 +# Copyright (c) 2020 Nekokatt # -# This file is part of Hikari. +# 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: # -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. # -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . +# 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. """Data binding utilities.""" from __future__ import annotations diff --git a/hikari/utilities/date.py b/hikari/utilities/date.py index ac1e668827..3bb8db4fe8 100644 --- a/hikari/utilities/date.py +++ b/hikari/utilities/date.py @@ -1,21 +1,24 @@ # -*- coding: utf-8 -*- # cython: language_level=3 -# Copyright © Nekoka.tt 2019-2020 +# Copyright (c) 2020 Nekokatt # -# This file is part of Hikari. +# 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: # -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. # -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . +# 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. """Utility methods used for parsing timestamps and datetimes from Discord.""" from __future__ import annotations diff --git a/hikari/utilities/files.py b/hikari/utilities/files.py index 1a26d59a0a..bad66f0ced 100644 --- a/hikari/utilities/files.py +++ b/hikari/utilities/files.py @@ -1,21 +1,24 @@ # -*- coding: utf-8 -*- # cython: language_level=3 -# Copyright © Nekoka.tt 2019-2020 +# Copyright (c) 2020 Nekokatt # -# This file is part of Hikari. +# 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: # -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. # -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . +# 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. """Utilities and classes for interacting with files and web resources.""" from __future__ import annotations diff --git a/hikari/utilities/flag.py b/hikari/utilities/flag.py index f4baee4c03..7d7418cb13 100644 --- a/hikari/utilities/flag.py +++ b/hikari/utilities/flag.py @@ -1,21 +1,24 @@ # -*- coding: utf-8 -*- # cython: language_level=3 -# Copyright © Nekoka.tt 2019-2020 +# Copyright (c) 2020 Nekokatt # -# This file is part of Hikari. +# 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: # -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. # -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . +# 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. """Stuff to make working with enum flags a bit easier.""" from __future__ import annotations diff --git a/hikari/utilities/iterators.py b/hikari/utilities/iterators.py index 4d305c0006..95350db68b 100644 --- a/hikari/utilities/iterators.py +++ b/hikari/utilities/iterators.py @@ -1,21 +1,24 @@ # -*- coding: utf-8 -*- # cython: language_level=3 -# Copyright © Nekoka.tt 2019-2020 +# Copyright (c) 2020 Nekokatt # -# This file is part of Hikari. +# 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: # -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. # -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . +# 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. """Lazy iterators for data that requires repeated API calls to retrieve. For consumers of this API, the only class you need to worry about is diff --git a/hikari/utilities/net.py b/hikari/utilities/net.py index 8428df926e..c1d0f4ac20 100644 --- a/hikari/utilities/net.py +++ b/hikari/utilities/net.py @@ -1,21 +1,24 @@ # -*- coding: utf-8 -*- # cython: language_level=3 -# Copyright © Nekoka.tt 2019-2020 +# Copyright (c) 2020 Nekokatt # -# This file is part of Hikari. +# 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: # -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. # -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . +# 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. """General bits and pieces that are reused between components.""" from __future__ import annotations diff --git a/hikari/utilities/reflect.py b/hikari/utilities/reflect.py index 09d7b0b22d..b77a6d44e9 100644 --- a/hikari/utilities/reflect.py +++ b/hikari/utilities/reflect.py @@ -1,21 +1,24 @@ # -*- coding: utf-8 -*- # cython: language_level=3 -# Copyright © Nekoka.tt 2019-2020 +# Copyright (c) 2020 Nekokatt # -# This file is part of Hikari. +# 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: # -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. # -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . +# 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. """Reflection utilities.""" from __future__ import annotations diff --git a/hikari/utilities/routes.py b/hikari/utilities/routes.py index 8946e78c86..41a998d531 100644 --- a/hikari/utilities/routes.py +++ b/hikari/utilities/routes.py @@ -1,21 +1,24 @@ # -*- coding: utf-8 -*- # cython: language_level=3 -# Copyright © Nekoka.tt 2019-2020 +# Copyright (c) 2020 Nekokatt # -# This file is part of Hikari. +# 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: # -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. # -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . +# 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. """Provides the valid routes that can be used on the API and the CDN.""" from __future__ import annotations diff --git a/hikari/utilities/snowflake.py b/hikari/utilities/snowflake.py index f05ce5314c..8e1af06768 100644 --- a/hikari/utilities/snowflake.py +++ b/hikari/utilities/snowflake.py @@ -1,21 +1,24 @@ # -*- coding: utf-8 -*- # cython: language_level=3 -# Copyright © Nekoka.tt 2019-2020 +# Copyright (c) 2020 Nekokatt # -# This file is part of Hikari. +# 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: # -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. # -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . +# 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. """Implementation of a Snowflake type.""" from __future__ import annotations diff --git a/hikari/utilities/spel.py b/hikari/utilities/spel.py index a703569b4f..232839faa1 100644 --- a/hikari/utilities/spel.py +++ b/hikari/utilities/spel.py @@ -1,21 +1,24 @@ # -*- coding: utf-8 -*- # cython: language_level=3 -# Copyright © Nekoka.tt 2019-2020 +# Copyright (c) 2020 Nekokatt # -# This file is part of Hikari. +# 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: # -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. # -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . +# 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. """HikariSPEL (Hikari SimPle Expression Language). HikariSPEL (Hikari SimPle Expression Language) is a super-simple expression diff --git a/hikari/utilities/undefined.py b/hikari/utilities/undefined.py index 8aed39ac49..8bb700d31b 100644 --- a/hikari/utilities/undefined.py +++ b/hikari/utilities/undefined.py @@ -1,21 +1,24 @@ # -*- coding: utf-8 -*- # cython: language_level=3 -# Copyright © Nekoka.tt 2019-2020 +# Copyright (c) 2020 Nekokatt # -# This file is part of Hikari. +# 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: # -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. # -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . +# 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. """Singleton used throughout the library to denote values that are not present.""" from __future__ import annotations diff --git a/hikari/utilities/version_sniffer.py b/hikari/utilities/version_sniffer.py index 7c2dfe1a88..723d3b9684 100644 --- a/hikari/utilities/version_sniffer.py +++ b/hikari/utilities/version_sniffer.py @@ -1,21 +1,24 @@ # -*- coding: utf-8 -*- # cython: language_level=3 -# Copyright © Nekoka.tt 2019-2020 +# Copyright (c) 2020 Nekokatt # -# This file is part of Hikari. +# 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: # -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. # -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . +# 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. """Checks PyPI for a newer release of the library.""" from __future__ import annotations diff --git a/lgtm.yml b/lgtm.yml deleted file mode 100644 index 3f3bf860ef..0000000000 --- a/lgtm.yml +++ /dev/null @@ -1,20 +0,0 @@ -path_classifiers: - test: - - tests/ - docs: - - docs/ - -queries: - # Our models violate this, but it is intended behaviour so I don't care for this message everywhere... - - exclude: py/missing-equals - # Again, do this too regularly to care. - - exclude: py/inheritance/incorrect-overridden-signature - # This gives far too many false positives for me to remotely care now. - - exclude: py/similar-function - # Gives false positives everywhere for package `__init__.py`'s that import submodules into their namespace. - - exclude: py/import-own-module - -extraction: - python: - python_setup: - version: 3 diff --git a/noxfile.py b/noxfile.py index 1ec857254c..d7e7ef09e7 100644 --- a/noxfile.py +++ b/noxfile.py @@ -1,20 +1,23 @@ # -*- coding: utf-8 -*- -# Copyright © Nekoka.tt 2019-2020 +# Copyright (c) 2020 Nekokatt # -# This file is part of Hikari. +# 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: # -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. # -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . +# 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 os import runpy import sys diff --git a/pages/index.html b/pages/index.html index 92c51c1e46..8affb061b6 100644 --- a/pages/index.html +++ b/pages/index.html @@ -3,22 +3,25 @@ @@ -39,7 +42,7 @@
-              oooo         o8o  oooo                            o8o       © 2019-2020 Nekokatt LGPL-3.0-ONLY   
+              oooo         o8o  oooo                            o8o       © 2020 Nekokatt, licensed under MIT   
               `888         `"'  `888                            `"'       Installed at:  /home/nekokatt/code/hikari/hikari
                888 .oo.   oooo   888  oooo   .oooo.   oooo d8b oooo       Support:       https://discord.gg/Jx4cNGG
                888P"Y88b  `888   888 .8P'   `P  )88b  `888""8P `888       Documentation: https://nekokatt.gitlab.io/hikari
@@ -161,7 +164,7 @@ 

- © 2019-2020, Nekokatt, LGPL v3.0 + © 2020, Nekokatt, MIT

diff --git a/pyproject.toml b/pyproject.toml index 866e2d2985..a652cd63d4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,19 +1,22 @@ -# Copyright © Nekoka.tt 2019-2020 +# Copyright (c) 2020 Nekokatt # -# This file is part of Hikari. +# 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: # -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. # -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . +# 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. [build-system] requires = ["wheel", "setuptools"] diff --git a/scripts/test_twemoji_mapping.py b/scripts/test_twemoji_mapping.py index 5c3d4263f3..f2f09e8430 100644 --- a/scripts/test_twemoji_mapping.py +++ b/scripts/test_twemoji_mapping.py @@ -1,20 +1,23 @@ # -*- coding: utf-8 -*- -# Copyright © Nekoka.tt 2019-2020 +# Copyright (c) 2020 Nekokatt # -# This file is part of Hikari. +# 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: # -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. # -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . +# 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. """A CI-used script that tests all the Twemoji URLs generated by Discord emojis are actually legitimate URLs, since Discord does not map these on a 1-to-1 basis. diff --git a/setup.py b/setup.py index e7c2b284ec..5a9eba34c7 100644 --- a/setup.py +++ b/setup.py @@ -1,20 +1,23 @@ # -*- coding: utf-8 -*- -# Copyright © Nekoka.tt 2019-2020 +# Copyright (c) 2020 Nekokatt # -# This file is part of Hikari. +# 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: # -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. # -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . +# 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 os import re @@ -81,6 +84,7 @@ def parse_requirements_file(path): "Environment :: Console", "Framework :: AsyncIO", "Intended Audience :: Developers", + "License :: OSI Approved :: MIT License", "Natural Language :: English", "Operating System :: OS Independent", "Programming Language :: Python :: 3.8", diff --git a/tests/__init__.py b/tests/__init__.py index f8ff46825e..ef54d4db0c 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -1,20 +1,23 @@ # -*- coding: utf-8 -*- -# Copyright © Nekoka.tt 2019-2020 +# Copyright (c) 2020 Nekokatt # -# This file is part of Hikari. +# 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: # -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. # -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . +# 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. """Pytest fails without this file in some cases. diff --git a/tests/hikari/__init__.py b/tests/hikari/__init__.py index 5389906824..edc91ff0f3 100644 --- a/tests/hikari/__init__.py +++ b/tests/hikari/__init__.py @@ -1,20 +1,23 @@ # -*- coding: utf-8 -*- -# Copyright © Nekoka.tt 2019-2020 +# Copyright (c) 2020 Nekokatt # -# This file is part of Hikari. +# 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: # -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. # -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . +# 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 asyncio import sys diff --git a/tests/hikari/client_session_stub.py b/tests/hikari/client_session_stub.py index 12ed877380..87b1d53bd2 100644 --- a/tests/hikari/client_session_stub.py +++ b/tests/hikari/client_session_stub.py @@ -1,20 +1,23 @@ # -*- coding: utf-8 -*- -# Copyright © Nekoka.tt 2019-2020 +# Copyright (c) 2020 Nekokatt # -# This file is part of Hikari. +# 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: # -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. # -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . +# 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. from __future__ import annotations import asyncio diff --git a/tests/hikari/events/__init__.py b/tests/hikari/events/__init__.py index 3b080b938f..952b991062 100644 --- a/tests/hikari/events/__init__.py +++ b/tests/hikari/events/__init__.py @@ -1,17 +1,20 @@ # -*- coding: utf-8 -*- -# Copyright © Nekoka.tt 2019-2020 +# Copyright (c) 2020 Nekokatt # -# This file is part of Hikari. +# 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: # -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. # -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . +# 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. diff --git a/tests/hikari/events/test_base_events.py b/tests/hikari/events/test_base_events.py index 2e8d181b09..499b64fcf3 100644 --- a/tests/hikari/events/test_base_events.py +++ b/tests/hikari/events/test_base_events.py @@ -1,20 +1,23 @@ # -*- coding: utf-8 -*- -# Copyright © Nekoka.tt 2019-2020 +# Copyright (c) 2020 Nekokatt # -# This file is part of Hikari. +# 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: # -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. # -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . +# 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 attr import mock import pytest diff --git a/tests/hikari/events/test_guild_events.py b/tests/hikari/events/test_guild_events.py index 7539e2e0e3..6ce74523a5 100644 --- a/tests/hikari/events/test_guild_events.py +++ b/tests/hikari/events/test_guild_events.py @@ -1,20 +1,23 @@ # -*- coding: utf-8 -*- -# Copyright © Nekoka.tt 2019-2020 +# Copyright (c) 2020 Nekokatt # -# This file is part of Hikari. +# 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: # -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. # -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . +# 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 mock import pytest diff --git a/tests/hikari/events/test_message_events.py b/tests/hikari/events/test_message_events.py index 4a6f6fbc2d..19eb08282e 100644 --- a/tests/hikari/events/test_message_events.py +++ b/tests/hikari/events/test_message_events.py @@ -1,20 +1,23 @@ # -*- coding: utf-8 -*- -# Copyright © Nekoka.tt 2019-2020 +# Copyright (c) 2020 Nekokatt # -# This file is part of Hikari. +# 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: # -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. # -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . +# 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 mock import pytest diff --git a/tests/hikari/events/test_reaction_events.py b/tests/hikari/events/test_reaction_events.py index 756fb3fd6a..e6bf15ccb5 100644 --- a/tests/hikari/events/test_reaction_events.py +++ b/tests/hikari/events/test_reaction_events.py @@ -1,20 +1,23 @@ # -*- coding: utf-8 -*- -# Copyright © Nekoka.tt 2019-2020 +# Copyright (c) 2020 Nekokatt # -# This file is part of Hikari. +# 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: # -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. # -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . +# 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 mock import pytest diff --git a/tests/hikari/events/test_role_events.py b/tests/hikari/events/test_role_events.py index fdae6a64cd..4c93d047e4 100644 --- a/tests/hikari/events/test_role_events.py +++ b/tests/hikari/events/test_role_events.py @@ -1,20 +1,23 @@ # -*- coding: utf-8 -*- -# Copyright © Nekoka.tt 2019-2020 +# Copyright (c) 2020 Nekokatt # -# This file is part of Hikari. +# 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: # -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. # -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . +# 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 mock import pytest diff --git a/tests/hikari/events/test_shard_events.py b/tests/hikari/events/test_shard_events.py index 2222b33995..ddf4c45902 100644 --- a/tests/hikari/events/test_shard_events.py +++ b/tests/hikari/events/test_shard_events.py @@ -1,20 +1,23 @@ # -*- coding: utf-8 -*- -# Copyright © Nekoka.tt 2019-2020 +# Copyright (c) 2020 Nekokatt # -# This file is part of Hikari. +# 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: # -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. # -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . +# 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 mock import pytest diff --git a/tests/hikari/events/test_voice_events.py b/tests/hikari/events/test_voice_events.py index f60f043f84..950be5b3b1 100644 --- a/tests/hikari/events/test_voice_events.py +++ b/tests/hikari/events/test_voice_events.py @@ -1,20 +1,23 @@ # -*- coding: utf-8 -*- -# Copyright © Nekoka.tt 2019-2020 +# Copyright (c) 2020 Nekokatt # -# This file is part of Hikari. +# 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: # -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. # -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . +# 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 mock import pytest diff --git a/tests/hikari/hikari_test_helpers.py b/tests/hikari/hikari_test_helpers.py index d5ed20fcd5..4687e45d23 100644 --- a/tests/hikari/hikari_test_helpers.py +++ b/tests/hikari/hikari_test_helpers.py @@ -1,20 +1,23 @@ # -*- coding: utf-8 -*- -# Copyright © Nekoka.tt 2019-2020 +# Copyright (c) 2020 Nekokatt # -# This file is part of Hikari. +# 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: # -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. # -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . +# 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 asyncio import contextlib diff --git a/tests/hikari/impl/__init__.py b/tests/hikari/impl/__init__.py index 3b080b938f..952b991062 100644 --- a/tests/hikari/impl/__init__.py +++ b/tests/hikari/impl/__init__.py @@ -1,17 +1,20 @@ # -*- coding: utf-8 -*- -# Copyright © Nekoka.tt 2019-2020 +# Copyright (c) 2020 Nekokatt # -# This file is part of Hikari. +# 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: # -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. # -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . +# 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. diff --git a/tests/hikari/impl/test_buckets.py b/tests/hikari/impl/test_buckets.py index 71245685b0..a1449f15c4 100644 --- a/tests/hikari/impl/test_buckets.py +++ b/tests/hikari/impl/test_buckets.py @@ -1,20 +1,23 @@ # -*- coding: utf-8 -*- -# Copyright © Nekoka.tt 2019-2020 +# Copyright (c) 2020 Nekokatt # -# This file is part of Hikari. +# 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: # -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. # -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . +# 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 datetime import time diff --git a/tests/hikari/impl/test_entity_factory.py b/tests/hikari/impl/test_entity_factory.py index 49707b56ed..a5b9d277e9 100644 --- a/tests/hikari/impl/test_entity_factory.py +++ b/tests/hikari/impl/test_entity_factory.py @@ -1,20 +1,23 @@ # -*- coding: utf-8 -*- -# Copyright © Nekoka.tt 2019-2020 +# Copyright (c) 2020 Nekokatt # -# This file is part of Hikari. +# 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: # -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. # -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . +# 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 datetime import mock diff --git a/tests/hikari/impl/test_rate_limits.py b/tests/hikari/impl/test_rate_limits.py index 3aac5f2661..8bf8cd90f2 100644 --- a/tests/hikari/impl/test_rate_limits.py +++ b/tests/hikari/impl/test_rate_limits.py @@ -1,20 +1,23 @@ # -*- coding: utf-8 -*- -# Copyright © Nekoka.tt 2019-2020 +# Copyright (c) 2020 Nekokatt # -# This file is part of Hikari. +# 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: # -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. # -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . +# 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 asyncio import contextlib import logging diff --git a/tests/hikari/impl/test_rest.py b/tests/hikari/impl/test_rest.py index dace15a2ab..1ca284a3ee 100644 --- a/tests/hikari/impl/test_rest.py +++ b/tests/hikari/impl/test_rest.py @@ -1,20 +1,23 @@ # -*- coding: utf-8 -*- -# Copyright © Nekoka.tt 2019-2020 +# Copyright (c) 2020 Nekokatt # -# This file is part of Hikari. +# 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: # -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. # -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . +# 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 asyncio import datetime import http diff --git a/tests/hikari/impl/test_shard.py b/tests/hikari/impl/test_shard.py index 32f9b32153..4ec9985606 100644 --- a/tests/hikari/impl/test_shard.py +++ b/tests/hikari/impl/test_shard.py @@ -1,20 +1,23 @@ # -*- coding: utf-8 -*- -# Copyright © Nekoka.tt 2019-2020 +# Copyright (c) 2020 Nekokatt # -# This file is part of Hikari. +# 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: # -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. # -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . +# 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 asyncio import contextlib import datetime diff --git a/tests/hikari/impl/test_stateful_cache.py b/tests/hikari/impl/test_stateful_cache.py index 4957bfcd7d..a7dc644289 100644 --- a/tests/hikari/impl/test_stateful_cache.py +++ b/tests/hikari/impl/test_stateful_cache.py @@ -1,20 +1,23 @@ # -*- coding: utf-8 -*- -# Copyright © Nekoka.tt 2019-2020 +# Copyright (c) 2020 Nekokatt # -# This file is part of Hikari. +# 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: # -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. # -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . +# 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 datetime import mock diff --git a/tests/hikari/impl/test_stateless_cache.py b/tests/hikari/impl/test_stateless_cache.py index a1b0ec4d8b..3eef0c0d73 100644 --- a/tests/hikari/impl/test_stateless_cache.py +++ b/tests/hikari/impl/test_stateless_cache.py @@ -1,20 +1,23 @@ # -*- coding: utf-8 -*- -# Copyright © Nekoka.tt 2019-2020 +# Copyright (c) 2020 Nekokatt # -# This file is part of Hikari. +# 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: # -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. # -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . +# 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 inspect import mock diff --git a/tests/hikari/models/__init__.py b/tests/hikari/models/__init__.py index 3b080b938f..952b991062 100644 --- a/tests/hikari/models/__init__.py +++ b/tests/hikari/models/__init__.py @@ -1,17 +1,20 @@ # -*- coding: utf-8 -*- -# Copyright © Nekoka.tt 2019-2020 +# Copyright (c) 2020 Nekokatt # -# This file is part of Hikari. +# 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: # -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. # -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . +# 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. diff --git a/tests/hikari/models/test_applications.py b/tests/hikari/models/test_applications.py index 8ceccf6138..0dbef6029f 100644 --- a/tests/hikari/models/test_applications.py +++ b/tests/hikari/models/test_applications.py @@ -1,20 +1,23 @@ # -*- coding: utf-8 -*- -# Copyright © Nekoka.tt 2019-2020 +# Copyright (c) 2020 Nekokatt # -# This file is part of Hikari. +# 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: # -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. # -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . +# 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 mock from hikari.models import applications diff --git a/tests/hikari/models/test_audit_logs.py b/tests/hikari/models/test_audit_logs.py index 2a6fcf5be4..06b90b915b 100644 --- a/tests/hikari/models/test_audit_logs.py +++ b/tests/hikari/models/test_audit_logs.py @@ -1,20 +1,23 @@ # -*- coding: utf-8 -*- -# Copyright © Nekoka.tt 2019-2020 +# Copyright (c) 2020 Nekokatt # -# This file is part of Hikari. +# 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: # -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. # -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . +# 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. from hikari.models import audit_logs from hikari.utilities import snowflake diff --git a/tests/hikari/models/test_channels.py b/tests/hikari/models/test_channels.py index 78ae8ce74f..7bc9d35736 100644 --- a/tests/hikari/models/test_channels.py +++ b/tests/hikari/models/test_channels.py @@ -1,20 +1,23 @@ # -*- coding: utf-8 -*- -# Copyright © Nekoka.tt 2019-2020 +# Copyright (c) 2020 Nekokatt # -# This file is part of Hikari. +# 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: # -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. # -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . +# 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 datetime import mock diff --git a/tests/hikari/models/test_colors.py b/tests/hikari/models/test_colors.py index 12ecee3d23..4300d55b8b 100644 --- a/tests/hikari/models/test_colors.py +++ b/tests/hikari/models/test_colors.py @@ -1,20 +1,23 @@ # -*- coding: utf-8 -*- -# Copyright © Nekoka.tt 2019-2020 +# Copyright (c) 2020 Nekokatt # -# This file is part of Hikari. +# 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: # -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. # -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . +# 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. from hikari.models import colors diff --git a/tests/hikari/models/test_colours.py b/tests/hikari/models/test_colours.py index a8110b90ec..f0fcf766ec 100644 --- a/tests/hikari/models/test_colours.py +++ b/tests/hikari/models/test_colours.py @@ -1,20 +1,23 @@ # -*- coding: utf-8 -*- -# Copyright © Nekoka.tt 2019-2020 +# Copyright (c) 2020 Nekokatt # -# This file is part of Hikari. +# 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: # -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. # -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . +# 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. from hikari.models import colors from hikari.models import colours diff --git a/tests/hikari/models/test_emojis.py b/tests/hikari/models/test_emojis.py index e72759e524..22e2918c7b 100644 --- a/tests/hikari/models/test_emojis.py +++ b/tests/hikari/models/test_emojis.py @@ -1,20 +1,23 @@ # -*- coding: utf-8 -*- -# Copyright © Nekoka.tt 2019-2020 +# Copyright (c) 2020 Nekokatt # -# This file is part of Hikari. +# 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: # -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. # -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . +# 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 mock from hikari.models import emojis diff --git a/tests/hikari/models/test_gateway.py b/tests/hikari/models/test_gateway.py index 0e12758548..7669c0a147 100644 --- a/tests/hikari/models/test_gateway.py +++ b/tests/hikari/models/test_gateway.py @@ -1,20 +1,23 @@ # -*- coding: utf-8 -*- -# Copyright © Nekoka.tt 2019-2020 +# Copyright (c) 2020 Nekokatt # -# This file is part of Hikari. +# 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: # -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. # -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . +# 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 datetime diff --git a/tests/hikari/models/test_guilds.py b/tests/hikari/models/test_guilds.py index fe52dbe788..fe6f9feb91 100644 --- a/tests/hikari/models/test_guilds.py +++ b/tests/hikari/models/test_guilds.py @@ -1,20 +1,23 @@ # -*- coding: utf-8 -*- -# Copyright © Nekoka.tt 2019-2020 +# Copyright (c) 2020 Nekokatt # -# This file is part of Hikari. +# 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: # -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. # -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . +# 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 mock from hikari.models import guilds diff --git a/tests/hikari/models/test_intents.py b/tests/hikari/models/test_intents.py index 955e683bca..70c95cff49 100644 --- a/tests/hikari/models/test_intents.py +++ b/tests/hikari/models/test_intents.py @@ -1,20 +1,23 @@ # -*- coding: utf-8 -*- -# Copyright © Nekoka.tt 2019-2020 +# Copyright (c) 2020 Nekokatt # -# This file is part of Hikari. +# 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: # -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. # -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . +# 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. from hikari.models import intents diff --git a/tests/hikari/models/test_invites.py b/tests/hikari/models/test_invites.py index 539df91dcb..25e1dce1e5 100644 --- a/tests/hikari/models/test_invites.py +++ b/tests/hikari/models/test_invites.py @@ -1,20 +1,23 @@ # -*- coding: utf-8 -*- -# Copyright © Nekoka.tt 2019-2020 +# Copyright (c) 2020 Nekokatt # -# This file is part of Hikari. +# 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: # -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. # -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . +# 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 mock from hikari.models import invites diff --git a/tests/hikari/models/test_messages.py b/tests/hikari/models/test_messages.py index 09c572f91c..ad99b27544 100644 --- a/tests/hikari/models/test_messages.py +++ b/tests/hikari/models/test_messages.py @@ -1,20 +1,23 @@ # -*- coding: utf-8 -*- -# Copyright © Nekoka.tt 2019-2020 +# Copyright (c) 2020 Nekokatt # -# This file is part of Hikari. +# 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: # -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. # -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . +# 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. from hikari.models import emojis from hikari.models import messages from tests.hikari import hikari_test_helpers diff --git a/tests/hikari/models/test_permissions.py b/tests/hikari/models/test_permissions.py index 7a1c3f1053..a2d7c705bd 100644 --- a/tests/hikari/models/test_permissions.py +++ b/tests/hikari/models/test_permissions.py @@ -1,20 +1,23 @@ # -*- coding: utf-8 -*- -# Copyright © Nekoka.tt 2019-2020 +# Copyright (c) 2020 Nekokatt # -# This file is part of Hikari. +# 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: # -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. # -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . +# 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. from hikari.models import permissions diff --git a/tests/hikari/models/test_presences.py b/tests/hikari/models/test_presences.py index 8b403cd9c0..bf66c7f22c 100644 --- a/tests/hikari/models/test_presences.py +++ b/tests/hikari/models/test_presences.py @@ -1,20 +1,23 @@ # -*- coding: utf-8 -*- -# Copyright © Nekoka.tt 2019-2020 +# Copyright (c) 2020 Nekokatt # -# This file is part of Hikari. +# 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: # -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. # -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . +# 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. from hikari.models import presences diff --git a/tests/hikari/models/test_users.py b/tests/hikari/models/test_users.py index a93d04a624..7f184f1e97 100644 --- a/tests/hikari/models/test_users.py +++ b/tests/hikari/models/test_users.py @@ -1,20 +1,23 @@ # -*- coding: utf-8 -*- -# Copyright © Nekoka.tt 2019-2020 +# Copyright (c) 2020 Nekokatt # -# This file is part of Hikari. +# 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: # -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. # -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . +# 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 mock from hikari.models import users diff --git a/tests/hikari/models/test_voices.py b/tests/hikari/models/test_voices.py index 0f1a11adae..01cf93e947 100644 --- a/tests/hikari/models/test_voices.py +++ b/tests/hikari/models/test_voices.py @@ -1,20 +1,23 @@ # -*- coding: utf-8 -*- -# Copyright © Nekoka.tt 2019-2020 +# Copyright (c) 2020 Nekokatt # -# This file is part of Hikari. +# 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: # -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. # -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . +# 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 mock from hikari.models import voices diff --git a/tests/hikari/models/test_webhooks.py b/tests/hikari/models/test_webhooks.py index 02e1fa0e6c..6ce865b7b9 100644 --- a/tests/hikari/models/test_webhooks.py +++ b/tests/hikari/models/test_webhooks.py @@ -1,20 +1,23 @@ # -*- coding: utf-8 -*- -# Copyright © Nekoka.tt 2019-2020 +# Copyright (c) 2020 Nekokatt # -# This file is part of Hikari. +# 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: # -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. # -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . +# 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 mock from hikari.models import webhooks diff --git a/tests/hikari/test_errors.py b/tests/hikari/test_errors.py index 0b09e698ba..0ab53330c5 100644 --- a/tests/hikari/test_errors.py +++ b/tests/hikari/test_errors.py @@ -1,20 +1,23 @@ # -*- coding: utf-8 -*- -# Copyright © Nekoka.tt 2019-2020 +# Copyright (c) 2020 Nekokatt # -# This file is part of Hikari. +# 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: # -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. # -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . +# 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. def test_issue_408_is_fixed(): diff --git a/tests/hikari/utilities/__init__.py b/tests/hikari/utilities/__init__.py index 3b080b938f..952b991062 100644 --- a/tests/hikari/utilities/__init__.py +++ b/tests/hikari/utilities/__init__.py @@ -1,17 +1,20 @@ # -*- coding: utf-8 -*- -# Copyright © Nekoka.tt 2019-2020 +# Copyright (c) 2020 Nekokatt # -# This file is part of Hikari. +# 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: # -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. # -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . +# 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. diff --git a/tests/hikari/utilities/test_aio.py b/tests/hikari/utilities/test_aio.py index 994705d497..0cd8da1bdb 100644 --- a/tests/hikari/utilities/test_aio.py +++ b/tests/hikari/utilities/test_aio.py @@ -1,20 +1,23 @@ # -*- coding: utf-8 -*- -# Copyright © Nekoka.tt 2019-2020 +# Copyright (c) 2020 Nekokatt # -# This file is part of Hikari. +# 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: # -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. # -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . +# 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 diff --git a/tests/hikari/utilities/test_data_binding.py b/tests/hikari/utilities/test_data_binding.py index c98acb2edd..d9fcbb0699 100644 --- a/tests/hikari/utilities/test_data_binding.py +++ b/tests/hikari/utilities/test_data_binding.py @@ -1,20 +1,23 @@ # -*- coding: utf-8 -*- -# Copyright © Nekoka.tt 2019-2020 +# Copyright (c) 2020 Nekokatt # -# This file is part of Hikari. +# 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: # -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. # -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . +# 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 typing import attr diff --git a/tests/hikari/utilities/test_date.py b/tests/hikari/utilities/test_date.py index 746779c5e1..be373b527c 100644 --- a/tests/hikari/utilities/test_date.py +++ b/tests/hikari/utilities/test_date.py @@ -1,20 +1,23 @@ # -*- coding: utf-8 -*- -# Copyright © Nekoka.tt 2019-2020 +# Copyright (c) 2020 Nekokatt # -# This file is part of Hikari. +# 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: # -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. # -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . +# 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 datetime import mock diff --git a/tests/hikari/utilities/test_flag.py b/tests/hikari/utilities/test_flag.py index 8aaef0d4de..62182e3f85 100644 --- a/tests/hikari/utilities/test_flag.py +++ b/tests/hikari/utilities/test_flag.py @@ -1,20 +1,23 @@ # -*- coding: utf-8 -*- -# Copyright © Nekoka.tt 2019-2020 +# Copyright (c) 2020 Nekokatt # -# This file is part of Hikari. +# 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: # -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. # -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . +# 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.utilities import flag diff --git a/tests/hikari/utilities/test_net.py b/tests/hikari/utilities/test_net.py index 102f93b6b5..67967fb65c 100644 --- a/tests/hikari/utilities/test_net.py +++ b/tests/hikari/utilities/test_net.py @@ -1,20 +1,23 @@ # -*- coding: utf-8 -*- -# Copyright © Nekoka.tt 2019-2020 +# Copyright (c) 2020 Nekokatt # -# This file is part of Hikari. +# 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: # -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. # -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . +# 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 http diff --git a/tests/hikari/utilities/test_reflect.py b/tests/hikari/utilities/test_reflect.py index 825ea02ba4..a3c0e448b7 100644 --- a/tests/hikari/utilities/test_reflect.py +++ b/tests/hikari/utilities/test_reflect.py @@ -1,20 +1,23 @@ # -*- coding: utf-8 -*- -# Copyright © Nekoka.tt 2019-2020 +# Copyright (c) 2020 Nekokatt # -# This file is part of Hikari. +# 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: # -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. # -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . +# 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 typing import pytest diff --git a/tests/hikari/utilities/test_routes.py b/tests/hikari/utilities/test_routes.py index 40b6c22b86..455f198452 100644 --- a/tests/hikari/utilities/test_routes.py +++ b/tests/hikari/utilities/test_routes.py @@ -1,20 +1,23 @@ # -*- coding: utf-8 -*- -# Copyright © Nekoka.tt 2019-2020 +# Copyright (c) 2020 Nekokatt # -# This file is part of Hikari. +# 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: # -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. # -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . +# 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 mock import pytest diff --git a/tests/hikari/utilities/test_snowflake.py b/tests/hikari/utilities/test_snowflake.py index 9ab1e0fd07..86f57f4104 100644 --- a/tests/hikari/utilities/test_snowflake.py +++ b/tests/hikari/utilities/test_snowflake.py @@ -1,20 +1,23 @@ # -*- coding: utf-8 -*- -# Copyright © Nekoka.tt 2019-2020 +# Copyright (c) 2020 Nekokatt # -# This file is part of Hikari. +# 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: # -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. # -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . +# 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 datetime import pytest diff --git a/tests/hikari/utilities/test_undefined.py b/tests/hikari/utilities/test_undefined.py index d790ab36e7..acf67001d0 100644 --- a/tests/hikari/utilities/test_undefined.py +++ b/tests/hikari/utilities/test_undefined.py @@ -1,20 +1,23 @@ # -*- coding: utf-8 -*- -# Copyright © Nekoka.tt 2019-2020 +# Copyright (c) 2020 Nekokatt # -# This file is part of Hikari. +# 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: # -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. # -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . +# 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.utilities import undefined diff --git a/tests/hikari/utilities/test_version_sniffer.py b/tests/hikari/utilities/test_version_sniffer.py index e6808f7f55..7622e18e3a 100644 --- a/tests/hikari/utilities/test_version_sniffer.py +++ b/tests/hikari/utilities/test_version_sniffer.py @@ -1,20 +1,23 @@ # -*- coding: utf-8 -*- -# Copyright © Nekoka.tt 2019-2020 +# Copyright (c) 2020 Nekokatt # -# This file is part of Hikari. +# 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: # -# Hikari is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. # -# Hikari is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Hikari. If not, see . +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. import contextlib import distutils.version From b615f8438ba9d41b963ca582ee78c803f637f930 Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Fri, 7 Aug 2020 15:19:25 +0000 Subject: [PATCH 857/922] Update deploy.nox.py --- ci/deploy.nox.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ci/deploy.nox.py b/ci/deploy.nox.py index 5a83809266..cb8fb5ff2c 100644 --- a/ci/deploy.nox.py +++ b/ci/deploy.nox.py @@ -40,7 +40,7 @@ def update_version_string(version): )[:-1] git_when = subprocess.check_output( - ["git", "log", "-1", '-date=format:"%Y/%m/%d"', '--format="%ad"'], + ["git", "log", "-1", '--date=format:"%Y/%m/%d"', '--format="%ad"'], universal_newlines=True, stderr=subprocess.DEVNULL, )[:-1] From 86a24c051d7ade923a77ca6d3dd03f435bf8e25d Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Fri, 7 Aug 2020 16:34:03 +0000 Subject: [PATCH 858/922] Fixed period on event_factory property. [skip ci] --- hikari/api/event_consumer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hikari/api/event_consumer.py b/hikari/api/event_consumer.py index ce0833e8db..dadfecb702 100644 --- a/hikari/api/event_consumer.py +++ b/hikari/api/event_consumer.py @@ -98,6 +98,6 @@ def event_factory(self) -> event_factory_.IEventFactoryComponent: Returns ------- - hikari.api.event_factory.IEventFactory. + hikari.api.event_factory.IEventFactory The model factory for events. """ From c80d40847354ac06ee3208720cc56c02d66d8366 Mon Sep 17 00:00:00 2001 From: davfsa Date: Fri, 7 Aug 2020 21:02:15 +0200 Subject: [PATCH 859/922] Fix intent docstrings to remove unknown ones --- hikari/models/intents.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/hikari/models/intents.py b/hikari/models/intents.py index 1acc5f860f..7af1171a7f 100644 --- a/hikari/models/intents.py +++ b/hikari/models/intents.py @@ -76,7 +76,7 @@ class Intent(flag.Flag): ```py # One or two values that fit on one line. - my_intents = Intents.GUILD_MESSAGES | Intents.DIRECT_MESSAGES + my_intents = Intents.GUILD_MESSAGES | Intents.PRIVATE_MESSAGES # Several intents together. You may find it useful to format these like # so to keep your code readable. @@ -86,7 +86,7 @@ class Intent(flag.Flag): Intents.GUILD_EMOJIS | Intents.GUILD_INTEGRATIONS | Intents.GUILD_MESSAGES | - Intents.DIRECT_MESSAGES + Intents.PRIVATE_MESSAGES ) ``` @@ -97,16 +97,17 @@ class Intent(flag.Flag): check in-place with the `&=` operator if needed. ```py + # Check if an intent is set: if (my_intents & Intents.GUILD_MESSAGES) == Intents.GUILD_MESSAGES: print("Guild messages are enabled") # Checking if ALL in a combination are set: - expected_intents = (Intents.GUILD_MESSAGES | Intents.DIRECT_MESSAGES) + expected_intents = (Intents.GUILD_MESSAGES | Intents.PRIVATE_MESSAGES) if (my_intents & expected_intents) == expected_intents: print("Messages are enabled in guilds and direct messages.") # Checking if AT LEAST ONE in a combination is set: - expected_intents = (Intents.GUILD_MESSAGES | Intents.DIRECT_MESSAGES) + expected_intents = (Intents.GUILD_MESSAGES | Intents.PRIVATE_MESSAGES) if my_intents & expected_intents: print("Messages are enabled in guilds or direct messages.") ``` @@ -121,9 +122,9 @@ class Intent(flag.Flag): my_intents ^= Intents.GUILD_MESSAGES # Remove all messages events. - my_intents = my_intents ^ (Intents.GUILD_MESSAGES | Intents.DIRECT_MESSAGES) + my_intents = my_intents ^ (Intents.GUILD_MESSAGES | Intents.PRIVATE_MESSAGES) # or, simplifying - my_intents ^= (Intents.GUILD_MESSAGES | Intents.DIRECT_MESSAGES) + my_intents ^= (Intents.GUILD_MESSAGES | Intents.PRIVATE_MESSAGES) ``` What is and is not covered by intents? From 31cde2bcd4b8a268816392091d9c1ac9cbe9c6dd Mon Sep 17 00:00:00 2001 From: davfsa Date: Fri, 7 Aug 2020 21:41:06 +0200 Subject: [PATCH 860/922] Removed `LURKABLE` from the guild features --- hikari/models/guilds.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/hikari/models/guilds.py b/hikari/models/guilds.py index 9800a7960d..c8169ef3fc 100644 --- a/hikari/models/guilds.py +++ b/hikari/models/guilds.py @@ -112,7 +112,10 @@ class GuildFeature(str, enum.Enum): """Guild has community features enabled.""" DISCOVERABLE = "DISCOVERABLE" - """Guild is able to be discovered in the directory.""" + """Guild is able to be discovered in the directory. + + This also implies the guild can be viewed without joining. + """ FEATURABLE = "FEATURABLE" """Guild is able to be featured in the directory.""" @@ -126,9 +129,6 @@ class GuildFeature(str, enum.Enum): NEWS = "NEWS" """Guild has access to create news channels.""" - LURKABLE = "LURKABLE" - """People can view channels in this guild without joining.""" - PARTNERED = "PARTNERED" """Guild is partnered.""" From 985b41f624b187e8f0afd6997b25ded0f5a6a741 Mon Sep 17 00:00:00 2001 From: Faster Speeding Date: Fri, 7 Aug 2020 19:31:41 +0100 Subject: [PATCH 861/922] Add custom copy and deepcopy implementations for attrs models in `utilities.attr_extensions` * Ignore .app and .shard attributes during deepcopy as these will lead to errors. * Attach custom copy and deepcopy impls to attr entities (except files) * Add some missing abc.ABCs * Switch undefined._UndefinedSentinel.__new__ to returning the singleton instance rather than being NotImplemented as this doesn't work with the copy module. * Remove some unnecessary copies in the cache. --- hikari/api/entity_factory.py | 3 +- hikari/api/special_endpoints.py | 2 +- hikari/config.py | 5 + hikari/errors.py | 3 + hikari/events/base_events.py | 6 +- hikari/events/channel_events.py | 34 +- hikari/events/guild_events.py | 35 +- hikari/events/lifetime_events.py | 13 +- hikari/events/member_events.py | 10 +- hikari/events/message_events.py | 22 +- hikari/events/reaction_events.py | 25 +- hikari/events/role_events.py | 10 +- hikari/events/shard_events.py | 13 +- hikari/events/typing_events.py | 7 +- hikari/events/user_events.py | 4 +- hikari/events/voice_events.py | 7 +- hikari/impl/entity_factory.py | 7 + hikari/impl/special_endpoints.py | 8 +- hikari/impl/stateful_cache.py | 35 +- hikari/models/applications.py | 11 +- hikari/models/audit_logs.py | 11 +- hikari/models/channels.py | 5 +- hikari/models/embeds.py | 6 + hikari/models/emojis.py | 7 +- hikari/models/gateway.py | 3 + hikari/models/guilds.py | 16 +- hikari/models/invites.py | 7 +- hikari/models/messages.py | 10 +- hikari/models/presences.py | 10 +- hikari/models/users.py | 4 +- hikari/models/voices.py | 6 +- hikari/models/webhooks.py | 4 +- hikari/utilities/art.py | 2 + hikari/utilities/attr_extensions.py | 256 +++++++++++ hikari/utilities/routes.py | 4 + hikari/utilities/undefined.py | 14 +- hikari/utilities/version_sniffer.py | 2 + tests/hikari/impl/test_stateful_cache.py | 36 +- .../hikari/utilities/test_attr_extensions.py | 434 ++++++++++++++++++ tests/hikari/utilities/test_undefined.py | 8 + 40 files changed, 1005 insertions(+), 100 deletions(-) create mode 100644 hikari/utilities/attr_extensions.py create mode 100644 tests/hikari/utilities/test_attr_extensions.py diff --git a/hikari/api/entity_factory.py b/hikari/api/entity_factory.py index fdc3ba73ce..b5b242ce73 100644 --- a/hikari/api/entity_factory.py +++ b/hikari/api/entity_factory.py @@ -30,10 +30,10 @@ import attr from hikari.api import component +from hikari.utilities import attr_extensions from hikari.utilities import undefined if typing.TYPE_CHECKING: - from hikari.models import applications as application_models from hikari.models import audit_logs as audit_log_models from hikari.models import channels as channel_models @@ -52,6 +52,7 @@ from hikari.utilities import snowflake +@attr_extensions.with_copy @attr.s(slots=True, weakref_slot=False) class GatewayGuildDefinition: """A structure for handling entities within guild create and update events.""" diff --git a/hikari/api/special_endpoints.py b/hikari/api/special_endpoints.py index 284a81c07e..ec5146f896 100644 --- a/hikari/api/special_endpoints.py +++ b/hikari/api/special_endpoints.py @@ -78,7 +78,7 @@ async def __aexit__( @attr.s(kw_only=True, slots=True, weakref_slot=False) -class GuildBuilder: +class GuildBuilder(abc.ABC): """Result type of `hikari.api.rest.IRESTClient.guild_builder`. This is used to create a guild in a tidy way using the HTTP API, since diff --git a/hikari/config.py b/hikari/config.py index a0f853fe1f..787ad2662d 100644 --- a/hikari/config.py +++ b/hikari/config.py @@ -35,10 +35,12 @@ import attr +from hikari.utilities import attr_extensions from hikari.utilities import constants from hikari.utilities import data_binding +@attr_extensions.with_copy @attr.s(slots=True, kw_only=True, repr=False, weakref_slot=False) class BasicAuthHeader: """An object that can be set as a producer for a basic auth header.""" @@ -62,6 +64,7 @@ def __str__(self) -> str: __repr__ = __str__ +@attr_extensions.with_copy @attr.s(slots=True, kw_only=True, weakref_slot=False) class ProxySettings: """The proxy settings to use.""" @@ -105,6 +108,7 @@ def all_headers(self) -> typing.Optional[data_binding.Headers]: return {**self.headers, constants.PROXY_AUTHENTICATION_HEADER: self.auth} +@attr_extensions.with_copy @attr.s(slots=True, kw_only=True, weakref_slot=False) class HTTPTimeoutSettings: """Settings to control HTTP request timeouts.""" @@ -125,6 +129,7 @@ class HTTPTimeoutSettings: """ +@attr_extensions.with_copy @attr.s(slots=True, kw_only=True, weakref_slot=False) class HTTPSettings: """Settings to control the HTTP client.""" diff --git a/hikari/errors.py b/hikari/errors.py index 55a4544598..5169ec27a2 100644 --- a/hikari/errors.py +++ b/hikari/errors.py @@ -51,6 +51,7 @@ import attr from hikari.models import messages +from hikari.utilities import attr_extensions from hikari.utilities import snowflake if typing.TYPE_CHECKING: @@ -60,6 +61,7 @@ from hikari.utilities import routes +@attr_extensions.with_copy @attr.s(auto_exc=True, slots=True, repr=False, init=False, weakref_slot=False) class HikariError(RuntimeError): """Base for an error raised by this API. @@ -71,6 +73,7 @@ class HikariError(RuntimeError): """ +@attr_extensions.with_copy @attr.s(auto_exc=True, slots=True, repr=False, init=False, weakref_slot=False) class HikariWarning(RuntimeWarning): """Base for a warning raised by this API. diff --git a/hikari/events/base_events.py b/hikari/events/base_events.py index 07b52d248c..3a4b2fc306 100644 --- a/hikari/events/base_events.py +++ b/hikari/events/base_events.py @@ -39,6 +39,7 @@ from hikari.api import shard as gateway_shard from hikari.models import intents +from hikari.utilities import attr_extensions if typing.TYPE_CHECKING: import types @@ -149,6 +150,7 @@ def is_no_recursive_throw_event(obj: typing.Union[T, typing.Type[T]]) -> bool: @no_recursive_throw() +@attr_extensions.with_copy @attr.s(kw_only=True, slots=True, weakref_slot=False) class ExceptionEvent(Event, typing.Generic[FailedEventT]): """Event that is raised when another event handler raises an `Exception`. @@ -161,10 +163,10 @@ class ExceptionEvent(Event, typing.Generic[FailedEventT]): side-effects on the application runtime. """ - app: event_consumer.IEventConsumerApp = attr.ib() + app: event_consumer.IEventConsumerApp = attr.ib(metadata={attr_extensions.SKIP_DEEP_COPY: True}) # <>. - shard: typing.Optional[gateway_shard.IGatewayShard] = attr.ib() + shard: typing.Optional[gateway_shard.IGatewayShard] = attr.ib(metadata={attr_extensions.SKIP_DEEP_COPY: True}) """Shard that received the event. Returns diff --git a/hikari/events/channel_events.py b/hikari/events/channel_events.py index a0d4d5e833..f2a249781c 100644 --- a/hikari/events/channel_events.py +++ b/hikari/events/channel_events.py @@ -55,6 +55,7 @@ from hikari.events import base_events from hikari.events import shard_events from hikari.models import intents +from hikari.utilities import attr_extensions if typing.TYPE_CHECKING: import datetime @@ -154,11 +155,12 @@ def channel_id(self) -> snowflake.Snowflake: @base_events.requires_intents(intents.Intent.GUILDS) +@attr_extensions.with_copy @attr.s(kw_only=True, slots=True, weakref_slot=False) class GuildChannelCreateEvent(GuildChannelEvent, ChannelCreateEvent): """Event fired when a guild channel is created.""" - shard: gateway_shard.IGatewayShard = attr.ib() + shard: gateway_shard.IGatewayShard = attr.ib(metadata={attr_extensions.SKIP_DEEP_COPY: True}) # <>. channel: channels.GuildChannel = attr.ib(repr=True) @@ -177,11 +179,12 @@ def guild_id(self) -> snowflake.Snowflake: @base_events.requires_intents(intents.Intent.PRIVATE_MESSAGES) +@attr_extensions.with_copy @attr.s(kw_only=True, slots=True, weakref_slot=False) class PrivateChannelCreateEvent(PrivateChannelEvent, ChannelCreateEvent): """Event fired when a private channel is created.""" - shard: gateway_shard.IGatewayShard = attr.ib() + shard: gateway_shard.IGatewayShard = attr.ib(metadata={attr_extensions.SKIP_DEEP_COPY: True}) # <>. channel: channels.PrivateChannel = attr.ib(repr=True) @@ -217,11 +220,12 @@ def channel_id(self) -> snowflake.Snowflake: @base_events.requires_intents(intents.Intent.GUILDS) +@attr_extensions.with_copy @attr.s(kw_only=True, slots=True, weakref_slot=False) class GuildChannelUpdateEvent(GuildChannelEvent, ChannelUpdateEvent): """Event fired when a guild channel is edited.""" - shard: gateway_shard.IGatewayShard = attr.ib() + shard: gateway_shard.IGatewayShard = attr.ib(metadata={attr_extensions.SKIP_DEEP_COPY: True}) # <>. channel: channels.GuildChannel = attr.ib(repr=True) @@ -245,11 +249,12 @@ def guild_id(self) -> snowflake.Snowflake: @base_events.requires_intents(intents.Intent.PRIVATE_MESSAGES) +@attr_extensions.with_copy @attr.s(kw_only=True, slots=True, weakref_slot=False) class PrivateChannelUpdateEvent(PrivateChannelEvent, ChannelUpdateEvent): """Event fired when a private channel is edited.""" - shard: gateway_shard.IGatewayShard = attr.ib() + shard: gateway_shard.IGatewayShard = attr.ib(metadata={attr_extensions.SKIP_DEEP_COPY: True}) # <>. channel: channels.PrivateChannel = attr.ib(repr=True) @@ -295,11 +300,12 @@ async def fetch_channel(self) -> typing.NoReturn: @base_events.requires_intents(intents.Intent.GUILDS) +@attr_extensions.with_copy @attr.s(kw_only=True, slots=True, weakref_slot=False) class GuildChannelDeleteEvent(GuildChannelEvent, ChannelDeleteEvent): """Event fired when a guild channel is deleted.""" - shard: gateway_shard.IGatewayShard = attr.ib() + shard: gateway_shard.IGatewayShard = attr.ib(metadata={attr_extensions.SKIP_DEEP_COPY: True}) # <>. channel: channels.GuildChannel = attr.ib(repr=True) @@ -329,11 +335,12 @@ async def fetch_channel(self) -> typing.NoReturn: # TODO: can this actually ever get fired? @base_events.requires_intents(intents.Intent.PRIVATE_MESSAGES) +@attr_extensions.with_copy @attr.s(kw_only=True, slots=True, weakref_slot=False) class PrivateChannelDeleteEvent(PrivateChannelEvent, ChannelDeleteEvent): """Event fired when a private channel is deleted.""" - shard: gateway_shard.IGatewayShard = attr.ib() + shard: gateway_shard.IGatewayShard = attr.ib(metadata={attr_extensions.SKIP_DEEP_COPY: True}) # <>. channel: channels.PrivateChannel = attr.ib(repr=True) @@ -383,11 +390,12 @@ async def fetch_channel(self) -> channels.TextChannel: @base_events.requires_intents(intents.Intent.GUILDS) +@attr_extensions.with_copy @attr.s(kw_only=True, slots=True, weakref_slot=False) class GuildPinsUpdateEvent(PinsUpdateEvent, GuildChannelEvent): """Event fired when a message is pinned/unpinned in a guild channel.""" - shard: gateway_shard.IGatewayShard = attr.ib() + shard: gateway_shard.IGatewayShard = attr.ib(metadata={attr_extensions.SKIP_DEEP_COPY: True}) # <>. channel_id: snowflake.Snowflake = attr.ib() @@ -406,11 +414,12 @@ async def fetch_channel(self) -> channels.GuildTextChannel: # TODO: This is not documented as having an intent, is this right? The guild version requires GUILDS intent. +@attr_extensions.with_copy @attr.s(kw_only=True, slots=True, weakref_slot=False) class PrivatePinsUpdateEvent(PinsUpdateEvent, PrivateChannelEvent): """Event fired when a message is pinned/unpinned in a private channel.""" - shard: gateway_shard.IGatewayShard = attr.ib() + shard: gateway_shard.IGatewayShard = attr.ib(metadata={attr_extensions.SKIP_DEEP_COPY: True}) # <>. channel_id: snowflake.Snowflake = attr.ib() @@ -458,11 +467,12 @@ async def fetch_invite(self) -> invites.Invite: @base_events.requires_intents(intents.Intent.GUILD_INVITES) +@attr_extensions.with_copy @attr.s(kw_only=True, slots=True, weakref_slot=False) class InviteCreateEvent(InviteEvent): """Event fired when an invite is created in a channel.""" - shard: gateway_shard.IGatewayShard = attr.ib() + shard: gateway_shard.IGatewayShard = attr.ib(metadata={attr_extensions.SKIP_DEEP_COPY: True}) # <>. invite: invites.InviteWithMetadata = attr.ib() @@ -492,11 +502,12 @@ def code(self) -> str: @base_events.requires_intents(intents.Intent.GUILD_INVITES) +@attr_extensions.with_copy @attr.s(kw_only=True, slots=True, weakref_slot=False) class InviteDeleteEvent(InviteEvent): """Event fired when an invite is deleted from a channel.""" - shard: gateway_shard.IGatewayShard = attr.ib() + shard: gateway_shard.IGatewayShard = attr.ib(metadata={attr_extensions.SKIP_DEEP_COPY: True}) # <>. channel_id: snowflake.Snowflake = attr.ib() @@ -515,6 +526,7 @@ async def fetch_invite(self) -> typing.NoReturn: @base_events.requires_intents(intents.Intent.GUILD_WEBHOOKS) +@attr_extensions.with_copy @attr.s(kw_only=True, slots=True, weakref_slot=False) class WebhookUpdateEvent(GuildChannelEvent): """Event fired when a webhook is created/updated/deleted in a channel. @@ -525,7 +537,7 @@ class WebhookUpdateEvent(GuildChannelEvent): the channel manually beforehand. """ - shard: gateway_shard.IGatewayShard = attr.ib() + shard: gateway_shard.IGatewayShard = attr.ib(metadata={attr_extensions.SKIP_DEEP_COPY: True}) # <>. channel_id: snowflake.Snowflake = attr.ib() diff --git a/hikari/events/guild_events.py b/hikari/events/guild_events.py index 46fe32f74d..60c2603921 100644 --- a/hikari/events/guild_events.py +++ b/hikari/events/guild_events.py @@ -46,6 +46,7 @@ from hikari.events import base_events from hikari.events import shard_events from hikari.models import intents +from hikari.utilities import attr_extensions if typing.TYPE_CHECKING: from hikari.api import shard as gateway_shard @@ -79,7 +80,7 @@ def guild_id(self) -> snowflake.Snowflake: @attr.s(kw_only=True, slots=True, weakref_slot=False) @base_events.requires_intents(intents.Intent.GUILDS) -class GuildVisibilityEvent(GuildEvent): +class GuildVisibilityEvent(GuildEvent, abc.ABC): """Event base for any event that changes the visibility of a guild. This includes when a guild becomes available after an outage, when a @@ -89,6 +90,7 @@ class GuildVisibilityEvent(GuildEvent): """ +@attr_extensions.with_copy @attr.s(kw_only=True, slots=True, weakref_slot=False) @base_events.requires_intents(intents.Intent.GUILDS) class GuildAvailableEvent(GuildVisibilityEvent): @@ -102,7 +104,7 @@ class GuildAvailableEvent(GuildVisibilityEvent): event models. """ - shard: gateway_shard.IGatewayShard = attr.ib() + shard: gateway_shard.IGatewayShard = attr.ib(metadata={attr_extensions.SKIP_DEEP_COPY: True}) # <>. guild: guilds.GatewayGuild = attr.ib() @@ -174,6 +176,7 @@ def guild_id(self) -> snowflake.Snowflake: return self.guild.id +@attr_extensions.with_copy @attr.s(kw_only=True, slots=True, weakref_slot=False) @base_events.requires_intents(intents.Intent.GUILDS) class GuildLeaveEvent(GuildVisibilityEvent): @@ -182,31 +185,33 @@ class GuildLeaveEvent(GuildVisibilityEvent): This will also fire if the guild was deleted. """ - shard: gateway_shard.IGatewayShard = attr.ib() + shard: gateway_shard.IGatewayShard = attr.ib(metadata={attr_extensions.SKIP_DEEP_COPY: True}) # <>. guild_id: snowflake.Snowflake = attr.ib() # <>. +@attr_extensions.with_copy @attr.s(kw_only=True, slots=True, weakref_slot=False) @base_events.requires_intents(intents.Intent.GUILDS) class GuildUnavailableEvent(GuildVisibilityEvent): """Event fired when a guild becomes unavailable because of an outage.""" - shard: gateway_shard.IGatewayShard = attr.ib() + shard: gateway_shard.IGatewayShard = attr.ib(metadata={attr_extensions.SKIP_DEEP_COPY: True}) # <>. guild_id: snowflake.Snowflake = attr.ib() # <>. +@attr_extensions.with_copy @attr.s(kw_only=True, slots=True, weakref_slot=False) @base_events.requires_intents(intents.Intent.GUILDS) class GuildUpdateEvent(GuildEvent): """Event fired when an existing guild is updated.""" - shard: gateway_shard.IGatewayShard = attr.ib() + shard: gateway_shard.IGatewayShard = attr.ib(metadata={attr_extensions.SKIP_DEEP_COPY: True}) # <>. guild: guilds.GatewayGuild = attr.ib() @@ -244,7 +249,7 @@ def guild_id(self) -> snowflake.Snowflake: @attr.s(kw_only=True, slots=True, weakref_slot=False) @base_events.requires_intents(intents.Intent.GUILD_BANS) -class BanEvent(GuildEvent): +class BanEvent(GuildEvent, abc.ABC): """Event base for any guild ban or unban.""" @property @@ -259,12 +264,13 @@ def user(self) -> users.User: """ +@attr_extensions.with_copy @attr.s(kw_only=True, slots=True, weakref_slot=False) @base_events.requires_intents(intents.Intent.GUILD_BANS) class BanCreateEvent(BanEvent): """Event that is fired when a user is banned from a guild.""" - shard: gateway_shard.IGatewayShard = attr.ib() + shard: gateway_shard.IGatewayShard = attr.ib(metadata={attr_extensions.SKIP_DEEP_COPY: True}) # <>. guild_id: snowflake.Snowflake = attr.ib() @@ -274,12 +280,13 @@ class BanCreateEvent(BanEvent): # <>. +@attr_extensions.with_copy @attr.s(kw_only=True, slots=True, weakref_slot=False) @base_events.requires_intents(intents.Intent.GUILD_BANS) class BanDeleteEvent(BanEvent): """Event that is fired when a user is unbanned from a guild.""" - shard: gateway_shard.IGatewayShard = attr.ib() + shard: gateway_shard.IGatewayShard = attr.ib(metadata={attr_extensions.SKIP_DEEP_COPY: True}) # <>. guild_id: snowflake.Snowflake = attr.ib() @@ -289,12 +296,13 @@ class BanDeleteEvent(BanEvent): # <>. +@attr_extensions.with_copy @attr.s(kw_only=True, slots=True, weakref_slot=False) @base_events.requires_intents(intents.Intent.GUILD_EMOJIS) class EmojisUpdateEvent(GuildEvent): """Event that is fired when the emojis in a guild are updated.""" - shard: gateway_shard.IGatewayShard = attr.ib() + shard: gateway_shard.IGatewayShard = attr.ib(metadata={attr_extensions.SKIP_DEEP_COPY: True}) # <>. guild_id: snowflake.Snowflake = attr.ib() @@ -310,6 +318,7 @@ class EmojisUpdateEvent(GuildEvent): """ +@attr_extensions.with_copy @attr.s(kw_only=True, slots=True, weakref_slot=False) @base_events.requires_intents(intents.Intent.GUILD_EMOJIS) class IntegrationsUpdateEvent(GuildEvent): @@ -327,13 +336,14 @@ class IntegrationsUpdateEvent(GuildEvent): We agree that it is not overly helpful to you. """ - shard: gateway_shard.IGatewayShard = attr.ib() + shard: gateway_shard.IGatewayShard = attr.ib(metadata={attr_extensions.SKIP_DEEP_COPY: True}) # <>. guild_id: snowflake.Snowflake = attr.ib() # <>. +@attr_extensions.with_copy @attr.s(kw_only=True, slots=True, weakref_slot=False) @base_events.requires_intents(intents.Intent.GUILD_PRESENCES) class PresenceUpdateEvent(shard_events.ShardEvent): @@ -350,7 +360,7 @@ class PresenceUpdateEvent(shard_events.ShardEvent): shards that saw the presence update. """ - shard: gateway_shard.IGatewayShard = attr.ib() + shard: gateway_shard.IGatewayShard = attr.ib(metadata={attr_extensions.SKIP_DEEP_COPY: True}) # <>. presence: presences_.MemberPresence = attr.ib() @@ -401,11 +411,12 @@ def guild_id(self) -> snowflake.Snowflake: return self.presence.guild_id +@attr_extensions.with_copy @attr.s(kw_only=True, slots=True, weakref_slot=False) class MemberChunkEvent(shard_events.ShardEvent): """Used to represent the response to Guild Request Members.""" - shard: gateway_shard.IGatewayShard = attr.ib() + shard: gateway_shard.IGatewayShard = attr.ib(metadata={attr_extensions.SKIP_DEEP_COPY: True}) # <>. guild_id: snowflake.Snowflake = attr.ib(repr=True) diff --git a/hikari/events/lifetime_events.py b/hikari/events/lifetime_events.py index 1f7fa02791..5b2b035e55 100644 --- a/hikari/events/lifetime_events.py +++ b/hikari/events/lifetime_events.py @@ -35,11 +35,13 @@ import attr from hikari.events import base_events +from hikari.utilities import attr_extensions if typing.TYPE_CHECKING: from hikari.api import event_consumer +@attr_extensions.with_copy @attr.s(kw_only=True, slots=True, weakref_slot=False) class StartingEvent(base_events.Event): """Event that is triggered before the application connects to discord. @@ -64,10 +66,11 @@ class StartingEvent(base_events.Event): `StoppedEvent` """ - app: event_consumer.IEventConsumerApp = attr.ib() + app: event_consumer.IEventConsumerApp = attr.ib(metadata={attr_extensions.SKIP_DEEP_COPY: True}) # <>. +@attr_extensions.with_copy @attr.s(kw_only=True, slots=True, weakref_slot=False) class StartedEvent(base_events.Event): """Event that is triggered after the application has started. @@ -86,10 +89,11 @@ class StartedEvent(base_events.Event): `StoppedEvent` """ - app: event_consumer.IEventConsumerApp = attr.ib() + app: event_consumer.IEventConsumerApp = attr.ib(metadata={attr_extensions.SKIP_DEEP_COPY: True}) # <>. +@attr_extensions.with_copy @attr.s(kw_only=True, slots=True, weakref_slot=False) class StoppingEvent(base_events.Event): """Event that is triggered as soon as the application is requested to close. @@ -116,10 +120,11 @@ class StoppingEvent(base_events.Event): `StoppedEvent` """ - app: event_consumer.IEventConsumerApp = attr.ib() + app: event_consumer.IEventConsumerApp = attr.ib(metadata={attr_extensions.SKIP_DEEP_COPY: True}) # <>. +@attr_extensions.with_copy @attr.s(kw_only=True, slots=True, weakref_slot=False) class StoppedEvent(base_events.Event): """Event that is triggered once the application has disconnected. @@ -145,5 +150,5 @@ class StoppedEvent(base_events.Event): `StoppingEvent` """ - app: event_consumer.IEventConsumerApp = attr.ib() + app: event_consumer.IEventConsumerApp = attr.ib(metadata={attr_extensions.SKIP_DEEP_COPY: True}) # <>. diff --git a/hikari/events/member_events.py b/hikari/events/member_events.py index dc5122449d..16f7528c38 100644 --- a/hikari/events/member_events.py +++ b/hikari/events/member_events.py @@ -37,6 +37,7 @@ from hikari.events import base_events from hikari.events import shard_events from hikari.models import intents +from hikari.utilities import attr_extensions if typing.TYPE_CHECKING: from hikari.api import shard as gateway_shard @@ -84,12 +85,13 @@ def user_id(self) -> snowflake.Snowflake: return self.user.id +@attr_extensions.with_copy @attr.s(kw_only=True, slots=True, weakref_slot=False) @base_events.requires_intents(intents.Intent.GUILD_MEMBERS) class MemberCreateEvent(MemberEvent): """Event that is fired when a member joins a guild.""" - shard: gateway_shard.IGatewayShard = attr.ib() + shard: gateway_shard.IGatewayShard = attr.ib(metadata={attr_extensions.SKIP_DEEP_COPY: True}) # <>. member: guilds.Member = attr.ib() @@ -112,6 +114,7 @@ def user(self) -> users.User: return self.member.user +@attr_extensions.with_copy @attr.s(kw_only=True, slots=True, weakref_slot=False) @base_events.requires_intents(intents.Intent.GUILD_MEMBERS) class MemberUpdateEvent(MemberEvent): @@ -120,7 +123,7 @@ class MemberUpdateEvent(MemberEvent): This may occur if roles are amended, or if the nickname is changed. """ - shard: gateway_shard.IGatewayShard = attr.ib() + shard: gateway_shard.IGatewayShard = attr.ib(metadata={attr_extensions.SKIP_DEEP_COPY: True}) # <>. member: guilds.Member = attr.ib() @@ -143,12 +146,13 @@ def user(self) -> users.User: return self.member.user +@attr_extensions.with_copy @attr.s(kw_only=True, slots=True, weakref_slot=False) @base_events.requires_intents(intents.Intent.GUILD_MEMBERS) class MemberDeleteEvent(MemberEvent): """Event fired when a member is kicked from or leaves a guild.""" - shard: gateway_shard.IGatewayShard = attr.ib() + shard: gateway_shard.IGatewayShard = attr.ib(metadata={attr_extensions.SKIP_DEEP_COPY: True}) # <>. guild_id: snowflake.Snowflake = attr.ib() diff --git a/hikari/events/message_events.py b/hikari/events/message_events.py index f8499877dc..f1bfaf78d8 100644 --- a/hikari/events/message_events.py +++ b/hikari/events/message_events.py @@ -49,6 +49,7 @@ from hikari.events import base_events from hikari.events import shard_events from hikari.models import intents +from hikari.utilities import attr_extensions if typing.TYPE_CHECKING: from hikari.api import shard as gateway_shard @@ -240,11 +241,12 @@ def channel_id(self) -> snowflake.Snowflake: @base_events.requires_intents(intents.Intent.GUILD_MESSAGES) +@attr_extensions.with_copy @attr.s(kw_only=True, slots=True, weakref_slot=False) class GuildMessageCreateEvent(GuildMessageEvent, MessageCreateEvent): """Event triggered when a message is sent to a guild channel.""" - shard: gateway_shard.IGatewayShard = attr.ib() + shard: gateway_shard.IGatewayShard = attr.ib(metadata={attr_extensions.SKIP_DEEP_COPY: True}) # <>. message: messages.Message = attr.ib() @@ -258,11 +260,12 @@ def guild_id(self) -> snowflake.Snowflake: @base_events.requires_intents(intents.Intent.PRIVATE_MESSAGES) +@attr_extensions.with_copy @attr.s(kw_only=True, slots=True, weakref_slot=False) class PrivateMessageCreateEvent(PrivateMessageEvent, MessageCreateEvent): """Event triggered when a message is sent to a private channel.""" - shard: gateway_shard.IGatewayShard = attr.ib() + shard: gateway_shard.IGatewayShard = attr.ib(metadata={attr_extensions.SKIP_DEEP_COPY: True}) # <>. message: messages.Message = attr.ib() @@ -270,11 +273,12 @@ class PrivateMessageCreateEvent(PrivateMessageEvent, MessageCreateEvent): @base_events.requires_intents(intents.Intent.GUILD_MESSAGES) +@attr_extensions.with_copy @attr.s(kw_only=True, slots=True, weakref_slot=False) class GuildMessageUpdateEvent(GuildMessageEvent, MessageUpdateEvent): """Event triggered when a message is updated in a guild channel.""" - shard: gateway_shard.IGatewayShard = attr.ib() + shard: gateway_shard.IGatewayShard = attr.ib(metadata={attr_extensions.SKIP_DEEP_COPY: True}) # <>. message: messages.PartialMessage = attr.ib() @@ -288,11 +292,12 @@ def guild_id(self) -> snowflake.Snowflake: @base_events.requires_intents(intents.Intent.PRIVATE_MESSAGES) +@attr_extensions.with_copy @attr.s(kw_only=True, slots=True, weakref_slot=False) class PrivateMessageUpdateEvent(PrivateMessageEvent, MessageUpdateEvent): """Event triggered when a message is updated in a private channel.""" - shard: gateway_shard.IGatewayShard = attr.ib() + shard: gateway_shard.IGatewayShard = attr.ib(metadata={attr_extensions.SKIP_DEEP_COPY: True}) # <>. message: messages.PartialMessage = attr.ib() @@ -300,11 +305,12 @@ class PrivateMessageUpdateEvent(PrivateMessageEvent, MessageUpdateEvent): @base_events.requires_intents(intents.Intent.GUILD_MESSAGES) +@attr_extensions.with_copy @attr.s(kw_only=True, slots=True, weakref_slot=False) class GuildMessageDeleteEvent(GuildMessageEvent, MessageDeleteEvent): """Event triggered when a message is deleted from a guild channel.""" - shard: gateway_shard.IGatewayShard = attr.ib() + shard: gateway_shard.IGatewayShard = attr.ib(metadata={attr_extensions.SKIP_DEEP_COPY: True}) # <>. message: messages.PartialMessage = attr.ib() @@ -317,12 +323,13 @@ def guild_id(self) -> snowflake.Snowflake: return typing.cast("snowflake.Snowflake", self.message.guild_id) +@attr_extensions.with_copy @attr.s(kw_only=True, slots=True, weakref_slot=False) @base_events.requires_intents(intents.Intent.PRIVATE_MESSAGES) class PrivateMessageDeleteEvent(PrivateMessageEvent, MessageDeleteEvent): """Event triggered when a message is deleted from a private channel.""" - shard: gateway_shard.IGatewayShard = attr.ib() + shard: gateway_shard.IGatewayShard = attr.ib(metadata={attr_extensions.SKIP_DEEP_COPY: True}) # <>. message: messages.PartialMessage = attr.ib() @@ -351,12 +358,13 @@ class MessageBulkDeleteEvent(MessagesEvent, abc.ABC): """ +@attr_extensions.with_copy @attr.s(kw_only=True, slots=True, weakref_slot=False) @base_events.requires_intents(intents.Intent.GUILD_MESSAGES) class GuildMessageBulkDeleteEvent(MessageBulkDeleteEvent): """Event triggered when messages are bulk-deleted from a guild channel.""" - shard: gateway_shard.IGatewayShard = attr.ib() + shard: gateway_shard.IGatewayShard = attr.ib(metadata={attr_extensions.SKIP_DEEP_COPY: True}) # <>. channel_id: snowflake.Snowflake = attr.ib() diff --git a/hikari/events/reaction_events.py b/hikari/events/reaction_events.py index e5fc460e3c..06712406ed 100644 --- a/hikari/events/reaction_events.py +++ b/hikari/events/reaction_events.py @@ -49,6 +49,7 @@ from hikari.events import base_events from hikari.events import shard_events from hikari.models import intents +from hikari.utilities import attr_extensions if typing.TYPE_CHECKING: from hikari.api import shard as gateway_shard @@ -192,12 +193,13 @@ def emoji(self) -> emojis.Emoji: """ +@attr_extensions.with_copy @attr.s(kw_only=True, slots=True, weakref_slot=False) @base_events.requires_intents(intents.Intent.GUILD_MESSAGE_REACTIONS) class GuildReactionAddEvent(GuildReactionEvent, ReactionAddEvent): """Event fired when a reaction is added to a guild message.""" - shard: gateway_shard.IGatewayShard = attr.ib() + shard: gateway_shard.IGatewayShard = attr.ib(metadata={attr_extensions.SKIP_DEEP_COPY: True}) # <>. member: guilds.Member = attr.ib() @@ -229,12 +231,13 @@ def user_id(self) -> snowflake.Snowflake: return self.member.user.id +@attr_extensions.with_copy @attr.s(kw_only=True, slots=True, weakref_slot=False) @base_events.requires_intents(intents.Intent.GUILD_MESSAGE_REACTIONS) class GuildReactionDeleteEvent(GuildReactionEvent, ReactionDeleteEvent): """Event fired when a reaction is removed from a guild message.""" - shard: gateway_shard.IGatewayShard = attr.ib() + shard: gateway_shard.IGatewayShard = attr.ib(metadata={attr_extensions.SKIP_DEEP_COPY: True}) # <>. user_id: snowflake.Snowflake = attr.ib() @@ -253,12 +256,13 @@ class GuildReactionDeleteEvent(GuildReactionEvent, ReactionDeleteEvent): # <>. +@attr_extensions.with_copy @attr.s(kw_only=True, slots=True, weakref_slot=False) @base_events.requires_intents(intents.Intent.GUILD_MESSAGE_REACTIONS) class GuildReactionDeleteEmojiEvent(GuildReactionEvent, ReactionDeleteEmojiEvent): """Event fired when an emoji is removed from a guild message's reactions.""" - shard: gateway_shard.IGatewayShard = attr.ib() + shard: gateway_shard.IGatewayShard = attr.ib(metadata={attr_extensions.SKIP_DEEP_COPY: True}) # <>. guild_id: snowflake.Snowflake = attr.ib() @@ -274,12 +278,13 @@ class GuildReactionDeleteEmojiEvent(GuildReactionEvent, ReactionDeleteEmojiEvent # <>. +@attr_extensions.with_copy @attr.s(kw_only=True, slots=True, weakref_slot=False) @base_events.requires_intents(intents.Intent.GUILD_MESSAGE_REACTIONS) class GuildReactionDeleteAllEvent(GuildReactionEvent, ReactionDeleteAllEvent): """Event fired when all of a guild message's reactions are removed.""" - shard: gateway_shard.IGatewayShard = attr.ib() + shard: gateway_shard.IGatewayShard = attr.ib(metadata={attr_extensions.SKIP_DEEP_COPY: True}) # <>. guild_id: snowflake.Snowflake = attr.ib() @@ -292,12 +297,13 @@ class GuildReactionDeleteAllEvent(GuildReactionEvent, ReactionDeleteAllEvent): # <>. +@attr_extensions.with_copy @attr.s(kw_only=True, slots=True, weakref_slot=False) @base_events.requires_intents(intents.Intent.PRIVATE_MESSAGE_REACTIONS) class PrivateReactionAddEvent(PrivateReactionEvent, ReactionAddEvent): """Event fired when a reaction is added to a guild message.""" - shard: gateway_shard.IGatewayShard = attr.ib() + shard: gateway_shard.IGatewayShard = attr.ib(metadata={attr_extensions.SKIP_DEEP_COPY: True}) # <>. user_id: snowflake.Snowflake = attr.ib() @@ -313,12 +319,13 @@ class PrivateReactionAddEvent(PrivateReactionEvent, ReactionAddEvent): # <>. +@attr_extensions.with_copy @attr.s(kw_only=True, slots=True, weakref_slot=False) @base_events.requires_intents(intents.Intent.PRIVATE_MESSAGE_REACTIONS) class PrivateReactionDeleteEvent(PrivateReactionEvent, ReactionDeleteEvent): """Event fired when a reaction is removed from a private message.""" - shard: gateway_shard.IGatewayShard = attr.ib() + shard: gateway_shard.IGatewayShard = attr.ib(metadata={attr_extensions.SKIP_DEEP_COPY: True}) # <>. user_id: snowflake.Snowflake = attr.ib() @@ -334,12 +341,13 @@ class PrivateReactionDeleteEvent(PrivateReactionEvent, ReactionDeleteEvent): # <>. +@attr_extensions.with_copy @attr.s(kw_only=True, slots=True, weakref_slot=False) @base_events.requires_intents(intents.Intent.PRIVATE_MESSAGE_REACTIONS) class PrivateReactionDeleteEmojiEvent(PrivateReactionEvent, ReactionDeleteEmojiEvent): """Event fired when an emoji is removed from a private message's reactions.""" - shard: gateway_shard.IGatewayShard = attr.ib() + shard: gateway_shard.IGatewayShard = attr.ib(metadata={attr_extensions.SKIP_DEEP_COPY: True}) # <>. channel_id: snowflake.Snowflake = attr.ib() @@ -352,12 +360,13 @@ class PrivateReactionDeleteEmojiEvent(PrivateReactionEvent, ReactionDeleteEmojiE # <>. +@attr_extensions.with_copy @attr.s(kw_only=True, slots=True, weakref_slot=False) @base_events.requires_intents(intents.Intent.PRIVATE_MESSAGE_REACTIONS) class PrivateReactionDeleteAllEvent(PrivateReactionEvent, ReactionDeleteAllEvent): """Event fired when all of a private message's reactions are removed.""" - shard: gateway_shard.IGatewayShard = attr.ib() + shard: gateway_shard.IGatewayShard = attr.ib(metadata={attr_extensions.SKIP_DEEP_COPY: True}) # <>. channel_id: snowflake.Snowflake = attr.ib() diff --git a/hikari/events/role_events.py b/hikari/events/role_events.py index 6c6b7a578c..2c998a1ecb 100644 --- a/hikari/events/role_events.py +++ b/hikari/events/role_events.py @@ -37,6 +37,7 @@ from hikari.events import base_events from hikari.events import shard_events from hikari.models import intents +from hikari.utilities import attr_extensions if typing.TYPE_CHECKING: from hikari.api import shard as gateway_shard @@ -72,12 +73,13 @@ def role_id(self) -> snowflake.Snowflake: """ +@attr_extensions.with_copy @attr.s(kw_only=True, slots=True, weakref_slot=False) @base_events.requires_intents(intents.Intent.GUILDS) class RoleCreateEvent(RoleEvent): """Event fired when a role is created.""" - shard: gateway_shard.IGatewayShard = attr.ib() + shard: gateway_shard.IGatewayShard = attr.ib(metadata={attr_extensions.SKIP_DEEP_COPY: True}) # <>. role: guilds.Role = attr.ib() @@ -100,12 +102,13 @@ def role_id(self) -> snowflake.Snowflake: return self.role.id +@attr_extensions.with_copy @attr.s(kw_only=True, slots=True, weakref_slot=False) @base_events.requires_intents(intents.Intent.GUILDS) class RoleUpdateEvent(RoleEvent): """Event fired when a role is updated.""" - shard: gateway_shard.IGatewayShard = attr.ib() + shard: gateway_shard.IGatewayShard = attr.ib(metadata={attr_extensions.SKIP_DEEP_COPY: True}) # <>. role: guilds.Role = attr.ib() @@ -128,12 +131,13 @@ def role_id(self) -> snowflake.Snowflake: return self.role.id +@attr_extensions.with_copy @attr.s(kw_only=True, slots=True, weakref_slot=False) @base_events.requires_intents(intents.Intent.GUILDS) class RoleDeleteEvent(RoleEvent): """Event fired when a role is deleted.""" - shard: gateway_shard.IGatewayShard = attr.ib() + shard: gateway_shard.IGatewayShard = attr.ib(metadata={attr_extensions.SKIP_DEEP_COPY: True}) # <>. guild_id: snowflake.Snowflake = attr.ib() diff --git a/hikari/events/shard_events.py b/hikari/events/shard_events.py index dd1f486ecb..84f8e30206 100644 --- a/hikari/events/shard_events.py +++ b/hikari/events/shard_events.py @@ -38,6 +38,7 @@ import attr from hikari.events import base_events +from hikari.utilities import attr_extensions if typing.TYPE_CHECKING: from hikari.api import event_consumer @@ -75,27 +76,30 @@ class ShardStateEvent(ShardEvent, abc.ABC): """ +@attr_extensions.with_copy @attr.s(kw_only=True, slots=True, weakref_slot=False) class ShardConnectedEvent(ShardStateEvent): """Event fired when a shard connects.""" - shard: gateway_shard.IGatewayShard = attr.ib() + shard: gateway_shard.IGatewayShard = attr.ib(metadata={attr_extensions.SKIP_DEEP_COPY: True}) # <>. +@attr_extensions.with_copy @attr.s(kw_only=True, slots=True, weakref_slot=False) class ShardDisconnectedEvent(ShardStateEvent): """Event fired when a shard disconnects.""" - shard: gateway_shard.IGatewayShard = attr.ib() + shard: gateway_shard.IGatewayShard = attr.ib(metadata={attr_extensions.SKIP_DEEP_COPY: True}) # <>. +@attr_extensions.with_copy @attr.s(kw_only=True, slots=True, weakref_slot=False) class ShardReadyEvent(ShardStateEvent): """Event fired when a shard declares it is ready.""" - shard: gateway_shard.IGatewayShard = attr.ib() + shard: gateway_shard.IGatewayShard = attr.ib(metadata={attr_extensions.SKIP_DEEP_COPY: True}) # <>. actual_gateway_version: int = attr.ib(repr=True) @@ -138,9 +142,10 @@ class ShardReadyEvent(ShardStateEvent): """ +@attr_extensions.with_copy @attr.s(kw_only=True, slots=True, weakref_slot=False) class ShardResumedEvent(ShardStateEvent): """Event fired when a shard resumes an existing session.""" - shard: gateway_shard.IGatewayShard = attr.ib() + shard: gateway_shard.IGatewayShard = attr.ib(metadata={attr_extensions.SKIP_DEEP_COPY: True}) # <>. diff --git a/hikari/events/typing_events.py b/hikari/events/typing_events.py index f781016351..5d754207fe 100644 --- a/hikari/events/typing_events.py +++ b/hikari/events/typing_events.py @@ -36,6 +36,7 @@ from hikari.events import base_events from hikari.events import shard_events from hikari.models import intents +from hikari.utilities import attr_extensions if typing.TYPE_CHECKING: import datetime @@ -106,11 +107,12 @@ async def fetch_user(self) -> users.User: @base_events.requires_intents(intents.Intent.GUILD_MESSAGE_TYPING) +@attr_extensions.with_copy @attr.s(kw_only=True, slots=True, weakref_slot=False) class GuildTypingEvent(TypingEvent): """Event fired when a user starts typing in a guild channel.""" - shard: gateway_shard.IGatewayShard = attr.ib() + shard: gateway_shard.IGatewayShard = attr.ib(metadata={attr_extensions.SKIP_DEEP_COPY: True}) # <>. channel_id: snowflake.Snowflake = attr.ib() @@ -177,11 +179,12 @@ async def fetch_guild_preview(self) -> guilds.GuildPreview: @base_events.requires_intents(intents.Intent.PRIVATE_MESSAGES) +@attr_extensions.with_copy @attr.s(kw_only=True, slots=True, weakref_slot=False) class PrivateTypingEvent(TypingEvent): """Event fired when a user starts typing in a guild channel.""" - shard: gateway_shard.IGatewayShard = attr.ib() + shard: gateway_shard.IGatewayShard = attr.ib(metadata={attr_extensions.SKIP_DEEP_COPY: True}) # <>. channel_id: snowflake.Snowflake = attr.ib() diff --git a/hikari/events/user_events.py b/hikari/events/user_events.py index 305a658e29..2e9af56925 100644 --- a/hikari/events/user_events.py +++ b/hikari/events/user_events.py @@ -29,17 +29,19 @@ import attr from hikari.events import shard_events +from hikari.utilities import attr_extensions if typing.TYPE_CHECKING: from hikari.api import shard as gateway_shard from hikari.models import users +@attr_extensions.with_copy @attr.s(kw_only=True, slots=True, weakref_slot=False) class OwnUserUpdateEvent(shard_events.ShardEvent): """Event fired when the account user is updated.""" - shard: gateway_shard.IGatewayShard = attr.ib() + shard: gateway_shard.IGatewayShard = attr.ib(metadata={attr_extensions.SKIP_DEEP_COPY: True}) # <>. user: users.OwnUser = attr.ib() diff --git a/hikari/events/voice_events.py b/hikari/events/voice_events.py index f81335206f..c211742261 100644 --- a/hikari/events/voice_events.py +++ b/hikari/events/voice_events.py @@ -37,6 +37,7 @@ from hikari.events import base_events from hikari.events import shard_events from hikari.models import intents +from hikari.utilities import attr_extensions if typing.TYPE_CHECKING: from hikari.api import shard as gateway_shard @@ -61,6 +62,7 @@ def guild_id(self) -> snowflake.Snowflake: @base_events.requires_intents(intents.Intent.GUILD_VOICE_STATES) +@attr_extensions.with_copy @attr.s(kw_only=True, slots=True, weakref_slot=False) class VoiceStateUpdateEvent(VoiceEvent): """Event fired when a user changes their voice state. @@ -71,7 +73,7 @@ class VoiceStateUpdateEvent(VoiceEvent): to connect to the voice gateway to stream audio or video content. """ - shard: gateway_shard.IGatewayShard = attr.ib() + shard: gateway_shard.IGatewayShard = attr.ib(metadata={attr_extensions.SKIP_DEEP_COPY: True}) # <>. state: voices.VoiceState = attr.ib(repr=True) @@ -89,6 +91,7 @@ def guild_id(self) -> snowflake.Snowflake: return self.state.guild_id +@attr_extensions.with_copy @attr.s(kw_only=True, slots=True, weakref_slot=False) class VoiceServerUpdateEvent(VoiceEvent): """Event fired when a voice server is changed. @@ -97,7 +100,7 @@ class VoiceServerUpdateEvent(VoiceEvent): falls over to a new server. """ - shard: gateway_shard.IGatewayShard = attr.ib() + shard: gateway_shard.IGatewayShard = attr.ib(metadata={attr_extensions.SKIP_DEEP_COPY: True}) # <>. guild_id: snowflake.Snowflake = attr.ib(repr=True) diff --git a/hikari/impl/entity_factory.py b/hikari/impl/entity_factory.py index 2f4352fa88..133b17bc58 100644 --- a/hikari/impl/entity_factory.py +++ b/hikari/impl/entity_factory.py @@ -47,6 +47,7 @@ from hikari.models import users as user_models from hikari.models import voices as voice_models from hikari.models import webhooks as webhook_models +from hikari.utilities import attr_extensions from hikari.utilities import data_binding from hikari.utilities import date from hikari.utilities import files @@ -70,6 +71,7 @@ def _deserialize_max_age(seconds: int) -> typing.Optional[datetime.timedelta]: return datetime.timedelta(seconds=seconds) if seconds > 0 else None +@attr_extensions.with_copy @attr.s(init=True, kw_only=True, repr=False, slots=True, weakref_slot=False) class _PartialGuildFields: id: snowflake.Snowflake = attr.ib() @@ -78,6 +80,7 @@ class _PartialGuildFields: features: typing.Sequence[typing.Union[guild_models.GuildFeature, str]] = attr.ib() +@attr_extensions.with_copy @attr.s(init=True, kw_only=True, repr=False, slots=True, weakref_slot=False) class _GuildChannelFields: id: snowflake.Snowflake = attr.ib() @@ -90,6 +93,7 @@ class _GuildChannelFields: parent_id: typing.Optional[snowflake.Snowflake] = attr.ib() +@attr_extensions.with_copy @attr.s(init=True, kw_only=True, repr=False, slots=True, weakref_slot=False) class _IntegrationFields: id: snowflake.Snowflake = attr.ib() @@ -98,6 +102,7 @@ class _IntegrationFields: account: guild_models.IntegrationAccount = attr.ib() +@attr_extensions.with_copy @attr.s(init=True, kw_only=True, repr=False, slots=True, weakref_slot=False) class _GuildFields(_PartialGuildFields): splash_hash: typing.Optional[str] = attr.ib() @@ -128,6 +133,7 @@ class _GuildFields(_PartialGuildFields): public_updates_channel_id: typing.Optional[snowflake.Snowflake] = attr.ib() +@attr_extensions.with_copy @attr.s(init=True, kw_only=True, repr=False, slots=True, weakref_slot=False) class _InviteFields: code: str = attr.ib() @@ -142,6 +148,7 @@ class _InviteFields: approximate_member_count: typing.Optional[int] = attr.ib() +@attr_extensions.with_copy @attr.s(init=True, kw_only=True, repr=False, slots=True, weakref_slot=False) class _UserFields: id: snowflake.Snowflake = attr.ib() diff --git a/hikari/impl/special_endpoints.py b/hikari/impl/special_endpoints.py index 1df9994df1..33bd00d31f 100644 --- a/hikari/impl/special_endpoints.py +++ b/hikari/impl/special_endpoints.py @@ -37,6 +37,7 @@ from hikari import errors from hikari.api import special_endpoints from hikari.models import channels +from hikari.utilities import attr_extensions from hikari.utilities import data_binding from hikari.utilities import date from hikari.utilities import files @@ -120,6 +121,7 @@ async def _keep_typing(self) -> None: await asyncio.gather(self, asyncio.wait_for(self._rest_close_event.wait(), timeout=9.0)) +@attr_extensions.with_copy @attr.s(kw_only=True, slots=True, weakref_slot=False) class GuildBuilder(special_endpoints.GuildBuilder): """Result type of `hikari.api.rest.IRESTClient.guild_builder`. @@ -195,7 +197,7 @@ class GuildBuilder(special_endpoints.GuildBuilder): """ # Required arguments. - _app: rest.IRESTApp = attr.ib() + _app: rest.IRESTApp = attr.ib(metadata={attr_extensions.SKIP_DEEP_COPY: True}) _name: str = attr.ib() # Optional args that we kept hidden. @@ -203,7 +205,9 @@ class GuildBuilder(special_endpoints.GuildBuilder): _counter: int = attr.ib(default=0, init=False) _request_call: typing.Callable[ ..., typing.Coroutine[None, None, typing.Union[None, data_binding.JSONObject, data_binding.JSONArray]] - ] = attr.ib() + ] = attr.ib( + metadata={attr_extensions.SKIP_DEEP_COPY: True} + ) # TODO: this can't be pickedl right? _roles: typing.MutableSequence[data_binding.JSONObject] = attr.ib(factory=list, init=False) @property diff --git a/hikari/impl/stateful_cache.py b/hikari/impl/stateful_cache.py index ef8005bef5..545752f49d 100644 --- a/hikari/impl/stateful_cache.py +++ b/hikari/impl/stateful_cache.py @@ -47,6 +47,7 @@ from hikari.models import presences from hikari.models import users from hikari.models import voices +from hikari.utilities import attr_extensions from hikari.utilities import date from hikari.utilities import iterators from hikari.utilities import snowflake @@ -171,6 +172,7 @@ def iterator(self) -> iterators.LazyIterator[_ValueT]: return iterators.FlatLazyIterator(()) +@attr_extensions.with_copy @attr.s(slots=True, repr=False, hash=False, weakref_slot=False) class _GuildRecord: is_available: typing.Optional[bool] = attr.ib(default=None) @@ -279,6 +281,7 @@ def replace(self: _DataT, **kwargs: typing.Any) -> _DataT: return data +@attr_extensions.with_copy @attr.s(kw_only=True, slots=True, repr=False, hash=False, weakref_slot=False) class _PrivateTextChannelData(_BaseData[channels.PrivateTextChannel]): id: snowflake.Snowflake = attr.ib() @@ -302,6 +305,7 @@ def build_from_entity( return super().build_from_entity(entity, **kwargs, recipient_id=entity.recipient.id) +@attr_extensions.with_copy @attr.s(kw_only=True, slots=True, repr=False, hash=False, weakref_slot=False) class _InviteData(_BaseData[invites.InviteWithMetadata]): code: str = attr.ib() @@ -351,6 +355,7 @@ def build_from_entity( ) +@attr_extensions.with_copy @attr.s(kw_only=True, slots=True, repr=False, hash=False, weakref_slot=False) class _MemberData(_BaseData[guilds.Member]): id: snowflake.Snowflake = attr.ib() @@ -374,6 +379,7 @@ def build_from_entity(cls: typing.Type[_MemberData], entity: guilds.Member, **kw return super().build_from_entity(entity, **kwargs, id=entity.user.id, role_ids=tuple(entity.role_ids)) +@attr_extensions.with_copy @attr.s(kw_only=True, slots=True, repr=False, hash=False, weakref_slot=False) class _KnownCustomEmojiData(_BaseData[emojis.KnownCustomEmoji]): id: snowflake.Snowflake = attr.ib() @@ -402,13 +408,14 @@ def build_from_entity( ) +@attr_extensions.with_copy @attr.s(kw_only=True, slots=True, repr=False, hash=False, weakref_slot=False) class _RichActivityData(_BaseData[presences.RichActivity]): name: str = attr.ib() url: str = attr.ib() type: presences.ActivityType = attr.ib() created_at: datetime.datetime = attr.ib() - timestamps: presences.ActivityTimestamps = attr.ib() + timestamps: typing.Optional[presences.ActivityTimestamps] = attr.ib() application_id: typing.Optional[snowflake.Snowflake] = attr.ib() details: typing.Optional[str] = attr.ib() state: typing.Optional[str] = attr.ib() @@ -451,14 +458,15 @@ def build_from_entity( def build_entity(self, target: typing.Type[presences.RichActivity], **kwargs: typing.Any) -> presences.RichActivity: return super().build_entity( target, - timestamps=copy.copy(self.timestamps), - party=copy.copy(self.party), - assets=copy.copy(self.assets), - secrets=copy.copy(self.secrets), + timestamps=copy.copy(self.timestamps) if self.timestamps is not None else None, + party=copy.copy(self.party) if self.party is not None else None, + assets=copy.copy(self.assets) if self.assets is not None else None, + secrets=copy.copy(self.secrets) if self.secrets is not None else None, **kwargs, ) +@attr_extensions.with_copy @attr.s(kw_only=True, slots=True, repr=False, hash=False, weakref_slot=False) class _MemberPresenceData(_BaseData[presences.MemberPresence]): user_id: snowflake.Snowflake = attr.ib() @@ -498,6 +506,7 @@ def build_entity( ) +@attr_extensions.with_copy @attr.s(kw_only=True, slots=True, repr=False, hash=False, weakref_slot=False) class _VoiceStateData(_BaseData[voices.VoiceState]): channel_id: typing.Optional[snowflake.Snowflake] = attr.ib() @@ -529,6 +538,7 @@ def get_fields(cls) -> typing.Collection[str]: ) +@attr_extensions.with_copy @attr.s(kw_only=True, slots=True, repr=False, hash=False, weakref_slot=False) class _GenericRefWrapper(typing.Generic[_ValueT]): object: _ValueT = attr.ib() @@ -596,6 +606,12 @@ def _copy(cls, value: channels.GuildChannel) -> channels.GuildChannel: return _copy_guild_channel(value) +class _3DCacheMappingView(_StatefulCacheMappingView[snowflake.Snowflake, cache.ICacheView[_KeyT, _ValueT]]): + @classmethod + def _copy(cls, value: cache.ICacheView[_KeyT, _ValueT]) -> cache.ICacheView[_KeyT, _ValueT]: + return value + + class StatefulCacheImpl(cache.IMutableCacheComponent): """In-memory cache implementation.""" @@ -1405,7 +1421,7 @@ def get_members_view( views[guild_id] = self.get_members_view_for_guild(guild_id) - return _StatefulCacheMappingView(views) + return _3DCacheMappingView(views) def get_members_view_for_guild( self, guild_id: snowflake.Snowflake, / @@ -1466,13 +1482,10 @@ def _build_presence( else: presence_kwargs.append({"emoji": copy.copy(emoji)}) - continue - elif isinstance(identifier, snowflake.Snowflake) and identifier in self._emoji_entries: presence_kwargs.append( {"emoji": self._build_emoji(self._emoji_entries[identifier], cached_users=cached_users)} ) - continue else: presence_kwargs.append({"emoji": copy.copy(self._unknown_emoji_entries[identifier].object)}) @@ -1599,7 +1612,7 @@ def get_presences_view( views[guild_id] = self.get_presences_view_for_guild(guild_id) - return _StatefulCacheMappingView(views) + return _3DCacheMappingView(views) def get_presences_view_for_guild( self, guild_id: snowflake.Snowflake, / @@ -1935,7 +1948,7 @@ def get_voice_states_view( views[guild_id] = self.get_voice_states_view_for_guild(guild_id) - return _StatefulCacheMappingView(views) + return _3DCacheMappingView(views) def get_voice_states_view_for_channel( self, guild_id: snowflake.Snowflake, channel_id: snowflake.Snowflake, / diff --git a/hikari/models/applications.py b/hikari/models/applications.py index 6ca097c4a7..b8147613fd 100644 --- a/hikari/models/applications.py +++ b/hikari/models/applications.py @@ -40,6 +40,7 @@ import attr from hikari.models import guilds +from hikari.utilities import attr_extensions from hikari.utilities import constants from hikari.utilities import files from hikari.utilities import routes @@ -198,6 +199,7 @@ def __str__(self) -> str: return self.name +@attr_extensions.with_copy @attr.s(eq=True, hash=True, init=True, kw_only=True, slots=True, weakref_slot=False) class OwnConnection: """Represents a user's connection with a third party account. @@ -262,11 +264,12 @@ def __str__(self) -> str: return self.name +@attr_extensions.with_copy @attr.s(eq=True, hash=True, init=True, kw_only=True, slots=True, weakref_slot=False) class TeamMember: """Represents a member of a Team.""" - app: rest_app.IRESTApp = attr.ib(repr=False, eq=False, hash=False) + app: rest_app.IRESTApp = attr.ib(repr=False, eq=False, hash=False, metadata={attr_extensions.SKIP_DEEP_COPY: True}) """The client application that models may use for procedures.""" membership_state: TeamMembershipState = attr.ib(eq=False, hash=False, repr=False) @@ -289,11 +292,12 @@ def __str__(self) -> str: return str(self.user) +@attr_extensions.with_copy @attr.s(eq=True, hash=True, init=True, kw_only=True, slots=True, weakref_slot=False) class Team(snowflake.Unique): """Represents a development team, along with all its members.""" - app: rest_app.IRESTApp = attr.ib(repr=False, eq=False, hash=False) + app: rest_app.IRESTApp = attr.ib(repr=False, eq=False, hash=False, metadata={attr_extensions.SKIP_DEEP_COPY: True}) """The client application that models may use for procedures.""" id: snowflake.Snowflake = attr.ib(eq=True, hash=True, repr=True) @@ -361,11 +365,12 @@ def format_icon(self, *, format: str = "png", size: int = 4096) -> typing.Option ) +@attr_extensions.with_copy @attr.s(eq=True, hash=True, init=True, kw_only=True, slots=True, weakref_slot=False) class Application(snowflake.Unique): """Represents the information of an Oauth2 Application.""" - app: rest_app.IRESTApp = attr.ib(repr=False, eq=False, hash=False) + app: rest_app.IRESTApp = attr.ib(repr=False, eq=False, hash=False, metadata={attr_extensions.SKIP_DEEP_COPY: True}) """The client application that models may use for procedures.""" id: snowflake.Snowflake = attr.ib( diff --git a/hikari/models/audit_logs.py b/hikari/models/audit_logs.py index 9232d45285..26e1f3be4a 100644 --- a/hikari/models/audit_logs.py +++ b/hikari/models/audit_logs.py @@ -48,6 +48,7 @@ import attr +from hikari.utilities import attr_extensions from hikari.utilities import snowflake if typing.TYPE_CHECKING: @@ -126,6 +127,7 @@ def __str__(self) -> str: __repr__ = __str__ +@attr_extensions.with_copy @attr.s(eq=True, hash=False, init=True, kw_only=True, slots=True, weakref_slot=False) class AuditLogChange: """Represents a change made to an audit log entry's target entity.""" @@ -190,6 +192,7 @@ class BaseAuditLogEntryInfo(abc.ABC): """A base object that all audit log entry info objects will inherit from.""" +@attr_extensions.with_copy @attr.s(eq=True, hash=False, init=True, kw_only=True, slots=True, weakref_slot=False) class ChannelOverwriteEntryInfo(BaseAuditLogEntryInfo, snowflake.Unique): """Represents the extra information for overwrite related audit log entries. @@ -208,6 +211,7 @@ class ChannelOverwriteEntryInfo(BaseAuditLogEntryInfo, snowflake.Unique): """The name of the role this overwrite targets, if it targets a role.""" +@attr_extensions.with_copy @attr.s(eq=True, hash=False, init=True, kw_only=True, slots=True, weakref_slot=False) class MessagePinEntryInfo(BaseAuditLogEntryInfo): """The extra information for message pin related audit log entries. @@ -222,6 +226,7 @@ class MessagePinEntryInfo(BaseAuditLogEntryInfo): """The ID of the message that's being pinned or unpinned.""" +@attr_extensions.with_copy @attr.s(eq=True, hash=False, init=True, kw_only=True, slots=True, weakref_slot=False) class MemberPruneEntryInfo(BaseAuditLogEntryInfo): """Extra information attached to guild prune log entries.""" @@ -233,6 +238,7 @@ class MemberPruneEntryInfo(BaseAuditLogEntryInfo): """The number of members who were removed by this prune.""" +@attr_extensions.with_copy @attr.s(eq=True, hash=False, init=True, kw_only=True, slots=True, weakref_slot=False) class MessageBulkDeleteEntryInfo(BaseAuditLogEntryInfo): """Extra information for the message bulk delete audit entry.""" @@ -249,6 +255,7 @@ class MessageDeleteEntryInfo(MessageBulkDeleteEntryInfo): """The guild text based channel where these message(s) were deleted.""" +@attr_extensions.with_copy @attr.s(eq=True, hash=False, init=True, kw_only=True, slots=True, weakref_slot=False) class MemberDisconnectEntryInfo(BaseAuditLogEntryInfo): """Extra information for the voice chat member disconnect entry.""" @@ -283,11 +290,12 @@ def __init__(self, payload: typing.Mapping[str, str]) -> None: self.__dict__.update(payload) +@attr_extensions.with_copy @attr.s(eq=True, hash=True, init=True, kw_only=True, slots=True, weakref_slot=False) class AuditLogEntry(snowflake.Unique): """Represents an entry in a guild's audit log.""" - app: rest_app.IRESTApp = attr.ib(repr=False, eq=False, hash=False) + app: rest_app.IRESTApp = attr.ib(repr=False, eq=False, hash=False, metadata={attr_extensions.SKIP_DEEP_COPY: True}) """The client application that models may use for procedures.""" id: snowflake.Snowflake = attr.ib(eq=True, hash=True, repr=True) @@ -312,6 +320,7 @@ class AuditLogEntry(snowflake.Unique): """The reason for this change, if set (between 0-512 characters).""" +@attr_extensions.with_copy @attr.s(eq=True, hash=False, init=True, kw_only=True, repr=False, slots=True, weakref_slot=False) class AuditLog(typing.Sequence[AuditLogEntry]): """Represents a guilds audit log.""" diff --git a/hikari/models/channels.py b/hikari/models/channels.py index 0a7331129f..e871eeb92b 100644 --- a/hikari/models/channels.py +++ b/hikari/models/channels.py @@ -47,6 +47,7 @@ from hikari.models import permissions from hikari.models import users +from hikari.utilities import attr_extensions from hikari.utilities import constants from hikari.utilities import files from hikari.utilities import routes @@ -108,6 +109,7 @@ def __str__(self) -> str: return self.name +@attr_extensions.with_copy @attr.s(eq=True, hash=True, init=True, kw_only=True, slots=True, weakref_slot=False) class PermissionOverwrite(snowflake.Unique): """Represents permission overwrites for a channel or role in a channel. @@ -160,6 +162,7 @@ def unset(self) -> permissions.Permission: return permissions.Permission(~(self.allow | self.deny)) +@attr_extensions.with_copy @attr.s(eq=True, hash=True, init=True, kw_only=True, slots=True, weakref_slot=False) class PartialChannel(snowflake.Unique): """Channel representation for cases where further detail is not provided. @@ -168,7 +171,7 @@ class PartialChannel(snowflake.Unique): not available from Discord. """ - app: rest_app.IRESTApp = attr.ib(repr=False, eq=False, hash=False) + app: rest_app.IRESTApp = attr.ib(repr=False, eq=False, hash=False, metadata={attr_extensions.SKIP_DEEP_COPY: True}) """The client application that models may use for procedures.""" id: snowflake.Snowflake = attr.ib(eq=True, hash=True, repr=True) diff --git a/hikari/models/embeds.py b/hikari/models/embeds.py index b3fb6873ab..5b08a340d3 100644 --- a/hikari/models/embeds.py +++ b/hikari/models/embeds.py @@ -44,6 +44,7 @@ from hikari import errors from hikari.models import colors +from hikari.utilities import attr_extensions from hikari.utilities import files from hikari.utilities import undefined @@ -54,6 +55,7 @@ AsyncReaderT = typing.TypeVar("AsyncReaderT", bound=files.AsyncReader) +@attr_extensions.with_copy @attr.s(eq=True, init=True, slots=True, kw_only=True, weakref_slot=False) class EmbedResource(files.Resource[AsyncReaderT]): """A base type for any resource provided in an embed. @@ -106,6 +108,7 @@ class EmbedResourceWithProxy(EmbedResource[AsyncReaderT]): """ +@attr_extensions.with_copy @attr.s(eq=True, hash=False, init=True, kw_only=True, slots=True, weakref_slot=False) class EmbedFooter: """Represents an embed footer.""" @@ -162,6 +165,7 @@ class yourself.** """The width of the video.""" +@attr_extensions.with_copy @attr.s(eq=True, hash=False, kw_only=True, slots=True, weakref_slot=False) class EmbedProvider: """Represents an embed provider. @@ -183,6 +187,7 @@ class yourself.** """The URL of the provider.""" +@attr_extensions.with_copy @attr.s(eq=True, hash=False, init=True, kw_only=True, slots=True, weakref_slot=False) class EmbedAuthor: """Represents an author of an embed.""" @@ -200,6 +205,7 @@ class EmbedAuthor: """The author's icon, or `builtins.None` if not present.""" +@attr_extensions.with_copy @attr.s(eq=True, hash=False, init=True, kw_only=True, slots=True, weakref_slot=False) class EmbedField: """Represents a field in a embed.""" diff --git a/hikari/models/emojis.py b/hikari/models/emojis.py index 674ec7c9b2..e2bcbb7815 100644 --- a/hikari/models/emojis.py +++ b/hikari/models/emojis.py @@ -31,6 +31,7 @@ import attr +from hikari.utilities import attr_extensions from hikari.utilities import constants from hikari.utilities import files from hikari.utilities import routes @@ -77,6 +78,7 @@ def mention(self) -> str: """Mention string to use to mention the emoji with.""" +@attr_extensions.with_copy @attr.s(hash=True, init=True, kw_only=False, slots=True, eq=False, weakref_slot=False) class UnicodeEmoji(Emoji): """Represents a unicode emoji. @@ -196,6 +198,7 @@ def from_unicode_escape(cls, escape: str) -> UnicodeEmoji: return cls(name=str(escape.encode("utf-8"), "unicode_escape")) +@attr_extensions.with_copy @attr.s(eq=True, hash=True, init=True, kw_only=True, slots=True, weakref_slot=False) class CustomEmoji(snowflake.Unique, Emoji): """Represents a custom emoji. @@ -221,7 +224,9 @@ class CustomEmoji(snowflake.Unique, Emoji): https://github.com/discord/discord-api-docs/issues/1614#issuecomment-628548913 """ - app: rest_app.IRESTApp = attr.ib(repr=False, eq=False, hash=False, init=True) + app: rest_app.IRESTApp = attr.ib( + repr=False, eq=False, hash=False, init=True, metadata={attr_extensions.SKIP_DEEP_COPY: True} + ) """The client application that models may use for procedures.""" id: snowflake.Snowflake = attr.ib(eq=True, hash=True, repr=True) diff --git a/hikari/models/gateway.py b/hikari/models/gateway.py index 28bb31deb1..ade5ff6fc2 100644 --- a/hikari/models/gateway.py +++ b/hikari/models/gateway.py @@ -29,12 +29,14 @@ import attr +from hikari.utilities import attr_extensions from hikari.utilities import date if typing.TYPE_CHECKING: import datetime +@attr_extensions.with_copy @attr.s(eq=True, hash=False, init=True, kw_only=True, slots=True, weakref_slot=False) class SessionStartLimit: """Used to represent information about the current session start limits.""" @@ -75,6 +77,7 @@ def reset_at(self) -> datetime.datetime: return self._created_at + self.reset_after +@attr_extensions.with_copy @attr.s(eq=True, hash=False, init=True, kw_only=True, slots=True, weakref_slot=False) class GatewayBot: """Used to represent gateway information for the connected bot.""" diff --git a/hikari/models/guilds.py b/hikari/models/guilds.py index 9800a7960d..09da0c88e9 100644 --- a/hikari/models/guilds.py +++ b/hikari/models/guilds.py @@ -56,6 +56,7 @@ import attr from hikari.models import users +from hikari.utilities import attr_extensions from hikari.utilities import constants from hikari.utilities import files from hikari.utilities import flag @@ -256,11 +257,12 @@ def __str__(self) -> str: return self.name +@attr_extensions.with_copy @attr.s(eq=True, hash=False, init=True, kw_only=True, slots=True, weakref_slot=False) class GuildWidget: """Represents a guild embed.""" - app: rest_app.IRESTApp = attr.ib(repr=False, eq=False, hash=False) + app: rest_app.IRESTApp = attr.ib(repr=False, eq=False, hash=False, metadata={attr_extensions.SKIP_DEEP_COPY: True}) """The client application that models may use for procedures.""" channel_id: typing.Optional[snowflake.Snowflake] = attr.ib(repr=True) @@ -422,11 +424,12 @@ def __str__(self) -> str: return str(self.user) +@attr_extensions.with_copy @attr.s(eq=True, hash=True, init=True, kw_only=True, slots=True, weakref_slot=False) class PartialRole(snowflake.Unique): """Represents a partial guild bound Role object.""" - app: rest_app.IRESTApp = attr.ib(repr=False, eq=False, hash=False) + app: rest_app.IRESTApp = attr.ib(repr=False, eq=False, hash=False, metadata={attr_extensions.SKIP_DEEP_COPY: True}) """The client application that models may use for procedures.""" id: snowflake.Snowflake = attr.ib(eq=True, hash=True, repr=True) @@ -495,6 +498,7 @@ class IntegrationExpireBehaviour(enum.IntEnum): """Kick the subscriber.""" +@attr_extensions.with_copy @attr.s(eq=True, hash=True, init=True, kw_only=True, slots=True, weakref_slot=False) class IntegrationAccount: """An account that's linked to an integration.""" @@ -509,6 +513,7 @@ def __str__(self) -> str: return self.name +@attr_extensions.with_copy @attr.s(eq=True, hash=True, init=True, kw_only=True, slots=True, weakref_slot=False) class PartialIntegration(snowflake.Unique): """A partial representation of an integration, found in audit logs.""" @@ -564,6 +569,7 @@ class Integration(PartialIntegration): """The datetime of when this integration's subscribers were last synced.""" +@attr_extensions.with_copy @attr.s(eq=True, hash=False, init=True, kw_only=True, slots=True, weakref_slot=False) class GuildMemberBan: """Used to represent guild bans.""" @@ -575,6 +581,7 @@ class GuildMemberBan: """The object of the user this ban targets.""" +@attr_extensions.with_copy @attr.s(eq=True, hash=True, init=True, kw_only=True, slots=True, weakref_slot=False) @typing.final class UnavailableGuild(snowflake.Unique): @@ -597,11 +604,12 @@ def is_unavailable(self) -> bool: # noqa: D401 return True +@attr_extensions.with_copy @attr.s(eq=True, hash=True, init=True, kw_only=True, slots=True, weakref_slot=False) class PartialGuild(snowflake.Unique): """Base object for any partial guild objects.""" - app: rest_app.IRESTApp = attr.ib(repr=False, eq=False, hash=False) + app: rest_app.IRESTApp = attr.ib(repr=False, eq=False, hash=False, metadata={attr_extensions.SKIP_DEEP_COPY: True}) """The client application that models may use for procedures.""" id: snowflake.Snowflake = attr.ib(eq=True, hash=True, repr=True) @@ -769,7 +777,7 @@ def format_discovery_splash(self, *, format_: str = "png", size: int = 4096) -> @attr.s(eq=True, hash=True, init=True, kw_only=True, slots=True, weakref_slot=False) -class Guild(PartialGuild): +class Guild(PartialGuild, abc.ABC): """A representation of a guild on Discord.""" splash_hash: typing.Optional[str] = attr.ib(eq=False, hash=False, repr=False) diff --git a/hikari/models/invites.py b/hikari/models/invites.py index 3f7a02bb13..0bd1cfcb75 100644 --- a/hikari/models/invites.py +++ b/hikari/models/invites.py @@ -39,6 +39,7 @@ import attr from hikari.models import guilds +from hikari.utilities import attr_extensions from hikari.utilities import constants from hikari.utilities import files from hikari.utilities import routes @@ -81,11 +82,12 @@ def code(self) -> str: """ +@attr_extensions.with_copy @attr.s(eq=True, hash=True, init=True, kw_only=True, slots=True, weakref_slot=False) class VanityURL(InviteCode): """A special case invite object, that represents a guild's vanity url.""" - app: rest_app.IRESTApp = attr.ib(repr=False, eq=False, hash=False) + app: rest_app.IRESTApp = attr.ib(repr=False, eq=False, hash=False, metadata={attr_extensions.SKIP_DEEP_COPY: True}) """The client application that models may use for procedures.""" code: str = attr.ib(eq=True, hash=True, repr=True) @@ -198,11 +200,12 @@ def format_banner(self, *, format_: str = "png", size: int = 4096) -> typing.Opt ) +@attr_extensions.with_copy @attr.s(eq=True, hash=True, init=True, kw_only=True, slots=True, weakref_slot=False) class Invite(InviteCode): """Represents an invite that's used to add users to a guild or group dm.""" - app: rest_app.IRESTApp = attr.ib(repr=False, eq=False, hash=False) + app: rest_app.IRESTApp = attr.ib(repr=False, eq=False, hash=False, metadata={attr_extensions.SKIP_DEEP_COPY: True}) """The client application that models may use for procedures.""" code: str = attr.ib(eq=True, hash=True, repr=True) diff --git a/hikari/models/messages.py b/hikari/models/messages.py index 371d10978c..5f871208bf 100644 --- a/hikari/models/messages.py +++ b/hikari/models/messages.py @@ -40,6 +40,7 @@ import attr +from hikari.utilities import attr_extensions from hikari.utilities import constants from hikari.utilities import files from hikari.utilities import flag @@ -154,6 +155,7 @@ def __str__(self) -> str: return self.name +@attr_extensions.with_copy @attr.s(eq=True, hash=False, init=True, kw_only=True, slots=True, weakref_slot=False) class Attachment(snowflake.Unique, files.WebResource): """Represents a file attached to a message. @@ -187,6 +189,7 @@ def __str__(self) -> str: return self.filename +@attr_extensions.with_copy @attr.s(eq=True, hash=True, init=True, kw_only=True, slots=True, weakref_slot=False) class Reaction: """Represents a reaction in a message.""" @@ -204,6 +207,7 @@ def __str__(self) -> str: return str(self.emoji) +@attr_extensions.with_copy @attr.s(eq=True, hash=False, init=True, kw_only=True, slots=True, weakref_slot=False) class MessageActivity: """Represents the activity of a rich presence-enabled message.""" @@ -215,6 +219,7 @@ class MessageActivity: """The party ID of the message activity.""" +@attr_extensions.with_copy @attr.s(eq=True, hash=False, init=True, kw_only=True, slots=True, weakref_slot=False) class MessageCrosspost: """Represents information about a cross-posted message. @@ -223,7 +228,7 @@ class MessageCrosspost: "published" to another. """ - app: rest_app.IRESTApp = attr.ib(repr=False, eq=False, hash=False) + app: rest_app.IRESTApp = attr.ib(repr=False, eq=False, hash=False, metadata={attr_extensions.SKIP_DEEP_COPY: True}) """The client application that models may use for procedures.""" # TODO: get clarification on this! If it cannot happen, this should subclass PartialMessage too. @@ -249,6 +254,7 @@ class MessageCrosspost: """ +@attr_extensions.with_copy @attr.s(slots=True, kw_only=True, init=True, repr=True, eq=False, weakref_slot=False) class PartialMessage(snowflake.Unique): """A message representation containing partially populated information. @@ -264,7 +270,7 @@ class PartialMessage(snowflake.Unique): nullability. """ - app: rest_app.IRESTApp = attr.ib(repr=False, eq=False, hash=False) + app: rest_app.IRESTApp = attr.ib(repr=False, eq=False, hash=False, metadata={attr_extensions.SKIP_DEEP_COPY: True}) """The client application that models may use for procedures.""" id: snowflake.Snowflake = attr.ib(eq=True, hash=True, repr=True) diff --git a/hikari/models/presences.py b/hikari/models/presences.py index 56d6b112a3..f31d81cae2 100644 --- a/hikari/models/presences.py +++ b/hikari/models/presences.py @@ -42,6 +42,7 @@ import attr +from hikari.utilities import attr_extensions from hikari.utilities import flag from hikari.utilities import snowflake @@ -88,6 +89,7 @@ def __str__(self) -> str: return self.name +@attr_extensions.with_copy @attr.s(eq=True, hash=False, init=True, kw_only=True, slots=True, weakref_slot=False) class ActivityTimestamps: """The datetimes for the start and/or end of an activity session.""" @@ -99,6 +101,7 @@ class ActivityTimestamps: """When this activity's session will end, if applicable.""" +@attr_extensions.with_copy @attr.s(eq=True, hash=True, init=True, kw_only=True, slots=True, weakref_slot=False) class ActivityParty: """Used to represent activity groups of users.""" @@ -113,6 +116,7 @@ class ActivityParty: """Maximum size of this party, if applicable.""" +@attr_extensions.with_copy @attr.s(eq=True, hash=False, init=True, kw_only=True, slots=True, weakref_slot=False) class ActivityAssets: """Used to represent possible assets for an activity.""" @@ -130,6 +134,7 @@ class ActivityAssets: """The text that'll appear when hovering over the small image, if set.""" +@attr_extensions.with_copy @attr.s(eq=True, hash=False, init=True, kw_only=True, slots=True, weakref_slot=False) class ActivitySecret: """The secrets used for interacting with an activity party.""" @@ -172,6 +177,7 @@ class ActivityFlag(flag.Flag): # TODO: add strict type checking to gateway for this type in an invariant way. +@attr_extensions.with_copy @attr.s(eq=True, hash=False, init=True, kw_only=True, slots=True, weakref_slot=False) class Activity: """Represents a regular activity that can be associated with a presence.""" @@ -249,6 +255,7 @@ def __str__(self) -> str: return self.name +@attr_extensions.with_copy @attr.s(eq=True, hash=False, init=True, kw_only=True, slots=True, weakref_slot=False) class ClientStatus: """The client statuses for this member.""" @@ -263,11 +270,12 @@ class ClientStatus: """The status of the target user's web session.""" +@attr_extensions.with_copy @attr.s(eq=True, hash=True, init=True, kw_only=True, slots=True, weakref_slot=False) class MemberPresence: """Used to represent a guild member's presence.""" - app: rest_app.IRESTApp = attr.ib(repr=False, eq=False, hash=False) + app: rest_app.IRESTApp = attr.ib(repr=False, eq=False, hash=False, metadata={attr_extensions.SKIP_DEEP_COPY: True}) """The client application that models may use for procedures.""" user_id: snowflake.Snowflake = attr.ib(repr=True, eq=False, hash=True) diff --git a/hikari/models/users.py b/hikari/models/users.py index ccbd056610..177b0afd48 100644 --- a/hikari/models/users.py +++ b/hikari/models/users.py @@ -31,6 +31,7 @@ import attr +from hikari.utilities import attr_extensions from hikari.utilities import constants from hikari.utilities import files from hikari.utilities import flag @@ -231,6 +232,7 @@ def default_avatar(self) -> files.URL: # noqa: D401 imperative mood check """ +@attr_extensions.with_copy @attr.s(eq=True, hash=True, init=True, kw_only=True, slots=True, weakref_slot=False) class PartialUser(snowflake.Unique): """Represents partial information about a user. @@ -242,7 +244,7 @@ class PartialUser(snowflake.Unique): id: snowflake.Snowflake = attr.ib(eq=True, hash=True, repr=True) """The ID of this user.""" - app: rest_app.IRESTApp = attr.ib(repr=False, eq=False, hash=False) + app: rest_app.IRESTApp = attr.ib(repr=False, eq=False, hash=False, metadata={attr_extensions.SKIP_DEEP_COPY: True}) """Reference to the client application that models may use for procedures.""" discriminator: undefined.UndefinedOr[str] = attr.ib(eq=False, hash=False, repr=True) diff --git a/hikari/models/voices.py b/hikari/models/voices.py index 8ba24f7b94..5357d88d32 100644 --- a/hikari/models/voices.py +++ b/hikari/models/voices.py @@ -29,17 +29,20 @@ import attr +from hikari.utilities import attr_extensions + if typing.TYPE_CHECKING: from hikari.api import rest as rest_app from hikari.models import guilds from hikari.utilities import snowflake +@attr_extensions.with_copy @attr.s(eq=True, hash=True, init=True, kw_only=True, slots=True, weakref_slot=False) class VoiceState: """Represents a user's voice connection status.""" - app: rest_app.IRESTApp = attr.ib(repr=False, eq=False, hash=False) + app: rest_app.IRESTApp = attr.ib(repr=False, eq=False, hash=False, metadata={attr_extensions.SKIP_DEEP_COPY: True}) """The client application that models may use for procedures.""" channel_id: typing.Optional[snowflake.Snowflake] = attr.ib(eq=False, hash=False, repr=True) @@ -82,6 +85,7 @@ class VoiceState: """The string ID of this voice state's session.""" +@attr_extensions.with_copy @attr.s(eq=True, hash=True, init=True, kw_only=True, slots=True, weakref_slot=False) class VoiceRegion: """Represents a voice region server.""" diff --git a/hikari/models/webhooks.py b/hikari/models/webhooks.py index ee04a42017..aa158b8878 100644 --- a/hikari/models/webhooks.py +++ b/hikari/models/webhooks.py @@ -30,6 +30,7 @@ import attr +from hikari.utilities import attr_extensions from hikari.utilities import constants from hikari.utilities import files as files_ from hikari.utilities import routes @@ -60,6 +61,7 @@ def __str__(self) -> str: return self.name +@attr_extensions.with_copy @attr.s(eq=True, hash=True, init=True, kw_only=True, slots=True, weakref_slot=False) class Webhook(snowflake.Unique): """Represents a webhook object on Discord. @@ -69,7 +71,7 @@ class Webhook(snowflake.Unique): send informational messages to specific channels. """ - app: rest_app.IRESTApp = attr.ib(repr=False, eq=False, hash=False) + app: rest_app.IRESTApp = attr.ib(repr=False, eq=False, hash=False, metadata={attr_extensions.SKIP_DEEP_COPY: True}) """The client application that models may use for procedures.""" id: snowflake.Snowflake = attr.ib(eq=True, hash=True, repr=True) diff --git a/hikari/utilities/art.py b/hikari/utilities/art.py index 39e2eab77e..72ef673e50 100644 --- a/hikari/utilities/art.py +++ b/hikari/utilities/art.py @@ -41,9 +41,11 @@ import attr from hikari import _about +from hikari.utilities import attr_extensions @typing.final +@attr_extensions.with_copy @attr.s(frozen=True, kw_only=True) class ConsolePalette: """Data class containing printable escape codes for colouring console output.""" diff --git a/hikari/utilities/attr_extensions.py b/hikari/utilities/attr_extensions.py new file mode 100644 index 0000000000..09cec961cc --- /dev/null +++ b/hikari/utilities/attr_extensions.py @@ -0,0 +1,256 @@ +# -*- coding: utf-8 -*- +# cython: language_level=3 +# Copyright (c) 2020 Nekokatt +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. +"""Utility for extending and optimisation the usage of attr models.""" +from __future__ import annotations + +__all__: typing.Final[typing.List[str]] = [ + "with_copy", + "copy_attrs", + "deep_copy_attrs", + "invalidate_deep_copy_cache", + "invalidate_shallow_copy_cache", +] + +import copy as std_copy +import logging +import typing + +import attr + +ModelT = typing.TypeVar("ModelT") +SKIP_DEEP_COPY: typing.Final[str] = "skip_deep_copy" + +_DEEP_COPIERS: typing.MutableMapping[ + typing.Any, typing.Callable[[typing.Any, typing.MutableMapping[int, typing.Any]], None] +] = {} +_SHALLOW_COPIERS: typing.MutableMapping[typing.Any, typing.Callable[[typing.Any], typing.Any]] = {} +_LOGGER = logging.getLogger("hikari") + + +def invalidate_shallow_copy_cache() -> None: + """Remove all the globally cached copy functions.""" + _LOGGER.debug("Invalidating attr extensions shallow copy cache") + _SHALLOW_COPIERS.clear() + + +def invalidate_deep_copy_cache() -> None: + """Remove all the globally cached generated deep copy functions.""" + _LOGGER.debug("Invalidating attr extensions deep copy cache") + _DEEP_COPIERS.clear() + + +def get_fields_definition( + cls: typing.Type[ModelT], +) -> typing.Tuple[ + typing.Sequence[typing.Tuple[attr.Attribute[typing.Any], str]], typing.Sequence[attr.Attribute[typing.Any]] +]: + """Get a sequence of init key-words to their relative attribute. + + Parameters + ---------- + cls : typing.Type[ModelT] + The attrs class to get the fields definition for. + + Returns + ------- + typing.Sequence[typing.Tuple[builtins.str, builtins.str]] + A sequence of tuples of string attribute names to string key-word names. + """ + init_results = [] + non_init_results = [] + + for field in attr.fields(cls): + if field.init: + key_word = field.name[1:] if field.name.startswith("_") else field.name + init_results.append((field, key_word)) + else: + non_init_results.append(field) + + return init_results, non_init_results + + +# TODO: can we get if the init wasn't generated for the class? +def generate_shallow_copier(cls: typing.Type[ModelT]) -> typing.Callable[[ModelT], ModelT]: + """Generate a function for shallow copying an attrs model with `init` enabled. + + Parameters + ---------- + cls : typing.Type[ModelT] + The attrs class to generate a shallow copying function for. + + Returns + ------- + typing.Callable[[ModelT], ModelT] + The generated shallow copying function. + """ + kwargs, setters = get_fields_definition(cls) + kwargs = ",".join(f"{kwarg}=m.{attribute.name}" for attribute, kwarg in kwargs) + setters = ";".join(f"r.{attribute.name}=m.{attribute.name}" for attribute in setters) + ";" if setters else "" + code = f"def copy(m):r=cls({kwargs});{setters}return r" + globals_ = {"cls": cls} + _LOGGER.debug("generating shallow copy function for %r: %r", cls, code) + exec(code, globals_) # noqa: S102 - Use of exec detected. + return typing.cast("typing.Callable[[ModelT], ModelT]", globals_["copy"]) + + +def get_or_generate_shallow_copier(cls: typing.Type[ModelT]) -> typing.Callable[[ModelT], ModelT]: + """Get a cached shallow copying function for a an attrs class or generate it. + + Parameters + ---------- + cls : typing.Type[ModelT] + The class to get or generate and cache a shallow copying function for. + + Returns + ------- + typing.Callable[[ModelT], ModelT] + The cached or generated shallow copying function. + """ + try: + return _SHALLOW_COPIERS[cls] + except KeyError: + copier = generate_shallow_copier(cls) + _SHALLOW_COPIERS[cls] = copier + return copier + + +def copy_attrs(model: ModelT) -> ModelT: + """Shallow copy an attrs model with `init` enabled. + + Parameters + ---------- + model : ModelT + The attrs model to shallow copy. + + Returns + ------- + ModelT + The new shallow copied attrs model. + """ + return get_or_generate_shallow_copier(type(model))(model) + + +def _normalize_kwargs_and_setters( + kwargs: typing.Sequence[typing.Tuple[attr.Attribute[typing.Any], str]], + setters: typing.Sequence[attr.Attribute[typing.Any]], +) -> typing.Iterable[attr.Attribute[typing.Any]]: + for attribute, _ in kwargs: + yield attribute + + yield from setters + + +def generate_deep_copier( + cls: typing.Type[ModelT], +) -> typing.Callable[[ModelT, typing.MutableMapping[int, typing.Any]], None]: + """Generate a function for deep copying an attrs model with `init` enabled. + + Parameters + ---------- + cls : typing.Type[ModelT] + The attrs class to generate a deep copying function for. + + Returns + ------- + typing.Callable[[ModelT], ModelT] + The generated deep copying function. + """ + kwargs, setters = get_fields_definition(cls) + + # Explicitly handle the case of an attrs model with no fields by returning + # an empty lambda to avoid a SyntaxError being raised. + if not kwargs and not setters: + return lambda _, __: None + + setters = ";".join( + f"m.{attribute.name}=std_copy(m.{attribute.name},memo)if(id_:=id(m.{attribute.name}))not in memo else memo[id_]" + for attribute in _normalize_kwargs_and_setters(kwargs, setters) + if not attribute.metadata.get(SKIP_DEEP_COPY) + ) + code = f"def deep_copy(m,memo):{setters}" + globals_ = {"std_copy": std_copy.deepcopy, "cls": cls} + _LOGGER.debug("generating deep copy function for %r: %r", cls, code) + exec(code, globals_) # noqa: S102 - Use of exec detected. + return typing.cast("typing.Callable[[ModelT, typing.MutableMapping[int, typing.Any]], None]", globals_["deep_copy"]) + + +def get_or_generate_deep_copier( + cls: typing.Type[ModelT], +) -> typing.Callable[[ModelT, typing.MutableMapping[int, typing.Any]], None]: + """Get a cached shallow copying function for a an attrs class or generate it. + + Parameters + ---------- + cls : typing.Type[ModelT] + The class to get or generate and cache a shallow copying function for. + + Returns + ------- + typing.Callable[[ModelT], ModelT] + The cached or generated shallow copying function. + """ + try: + return _DEEP_COPIERS[cls] + except KeyError: + copier = generate_deep_copier(cls) + _DEEP_COPIERS[cls] = copier + return copier + + +def deep_copy_attrs(model: ModelT, memo: typing.Optional[typing.MutableMapping[int, typing.Any]] = None) -> ModelT: + """Deep copy an attrs model with `init` enabled. + + Parameters + ---------- + model : ModelT + The attrs model to deep copy. + memo : typing.MutableMapping[builtins.int, typing.Any] or builtins.None + A memo dictionary of objects already copied during the current copying + pass, see https://docs.python.org/3/library/copy.html for more details. + + !!! note + This won't deep copy attributes where "skip_deep_copy" is set to + `builtins.True` in their metadata. + + Returns + ------- + ModelT + The new deep copied attrs model. + """ + if memo is None: + memo = {} + + new_object = std_copy.copy(model) + memo[id(model)] = new_object + get_or_generate_deep_copier(type(model))(new_object, memo) + return new_object + + +def with_copy(cls: typing.Type[ModelT]) -> typing.Type[ModelT]: + """Add a custom implementation for copying attrs models to a class. + + !!! note + This will only work if the class has an attrs generated init. + """ + cls.__copy__ = copy_attrs # type: ignore[attr-defined] + cls.__deepcopy__ = deep_copy_attrs # type: ignore[attr-defined] + return cls diff --git a/hikari/utilities/routes.py b/hikari/utilities/routes.py index 41a998d531..53f1bc5480 100644 --- a/hikari/utilities/routes.py +++ b/hikari/utilities/routes.py @@ -32,6 +32,7 @@ import attr +from hikari.utilities import attr_extensions from hikari.utilities import data_binding from hikari.utilities import files @@ -40,6 +41,7 @@ # This could be frozen, except attrs' docs advise against this for performance # reasons when using slotted classes. +@attr_extensions.with_copy @attr.s(init=True, slots=True, hash=True, weakref_slot=False) @typing.final class CompiledRoute: @@ -102,6 +104,7 @@ def __str__(self) -> str: return f"{self.method} {self.compiled_path}" +@attr_extensions.with_copy @attr.s(hash=True, init=False, slots=True, weakref_slot=False) @typing.final class Route: @@ -171,6 +174,7 @@ def _cdn_valid_formats_converter(values: typing.Set[str]) -> typing.FrozenSet[st return frozenset(v.lower() for v in values) +@attr_extensions.with_copy @attr.s(hash=True, init=True, slots=True, weakref_slot=False) @typing.final class CDNRoute: diff --git a/hikari/utilities/undefined.py b/hikari/utilities/undefined.py index 8bb700d31b..620652d826 100644 --- a/hikari/utilities/undefined.py +++ b/hikari/utilities/undefined.py @@ -33,6 +33,8 @@ import enum import typing +SelfT = typing.TypeVar("SelfT") + class _UndefinedSentinel: __slots__: typing.Sequence[str] = () @@ -40,6 +42,16 @@ class _UndefinedSentinel: def __bool__(self) -> bool: return False + def __copy__(self: SelfT) -> SelfT: + # This is meant to be a singleton + return self + + def __deepcopy__(self: SelfT, memo: typing.MutableMapping[int, typing.Any]) -> SelfT: + memo[id(self)] = self + + # This is meant to be a singleton + return self + def __repr__(self) -> str: return "UNDEFINED" @@ -76,7 +88,7 @@ class UndefinedType(_UndefinedSentinel, enum.Enum): # Prevent making any more instances as much as possible. -setattr(_UndefinedSentinel, "__new__", NotImplemented) +setattr(_UndefinedSentinel, "__new__", lambda _: UNDEFINED) del _UndefinedSentinel diff --git a/hikari/utilities/version_sniffer.py b/hikari/utilities/version_sniffer.py index 723d3b9684..8909c2318a 100644 --- a/hikari/utilities/version_sniffer.py +++ b/hikari/utilities/version_sniffer.py @@ -37,11 +37,13 @@ import attr from hikari import _about +from hikari.utilities import attr_extensions if typing.TYPE_CHECKING: from hikari.utilities import data_binding +@attr_extensions.with_copy @attr.s(kw_only=True, slots=True, weakref_slot=False) class VersionInfo: """PyPI release info.""" diff --git a/tests/hikari/impl/test_stateful_cache.py b/tests/hikari/impl/test_stateful_cache.py index a7dc644289..f02341858b 100644 --- a/tests/hikari/impl/test_stateful_cache.py +++ b/tests/hikari/impl/test_stateful_cache.py @@ -62,6 +62,7 @@ def test__build_private_text_channel_with_cached_user(self, cache_impl): assert channel.type is channels.ChannelType.PRIVATE_TEXT assert channel.last_message_id == snowflake.Snowflake(65345) assert channel.recipient == mock_user + assert channel.recipient is not mock_user def test__build_private_text_channel_with_passed_through_user(self, cache_impl): channel_data = stateful_cache._PrivateTextChannelData( @@ -76,12 +77,13 @@ def test__build_private_text_channel_with_passed_through_user(self, cache_impl): channel_data, cached_users={snowflake.Snowflake(2342344): mock_user} ) assert channel_channel.recipient == mock_user + assert channel_channel.recipient is not mock_user def test_clear_private_text_channels(self, cache_impl): mock_channel_data_1 = mock.Mock(stateful_cache._PrivateTextChannelData) mock_channel_data_2 = mock.Mock(stateful_cache._PrivateTextChannelData) - mock_user_1 = mock.Mock(users.User) - mock_user_2 = mock.Mock(users.User) + mock_user_1 = object() + mock_user_2 = object() mock_channel_1 = mock.Mock(channels.PrivateTextChannel) mock_channel_2 = mock.Mock(channels.PrivateTextChannel) cache_impl._private_text_channel_entries = { @@ -96,7 +98,8 @@ def test_clear_private_text_channels(self, cache_impl): cache_impl._increment_user_ref_count = mock.Mock() cache_impl._garbage_collect_user = mock.Mock() cache_impl._build_private_text_channel = mock.Mock(side_effect=[mock_channel_1, mock_channel_2]) - assert cache_impl.clear_private_text_channels() == { + view = cache_impl.clear_private_text_channels() + assert view == { snowflake.Snowflake(978655): mock_channel_1, snowflake.Snowflake(2342344): mock_channel_2, } @@ -169,7 +172,8 @@ def test_get_private_text_channel_view(self, cache_impl): snowflake.Snowflake(54213): mock_channel_data_1, snowflake.Snowflake(65656): mock_channel_data_2, } - assert cache_impl.get_private_text_channels_view() == { + view = cache_impl.get_private_text_channels_view() + assert view == { snowflake.Snowflake(54213): mock_channel_1, snowflake.Snowflake(65656): mock_channel_2, } @@ -261,6 +265,7 @@ def test__build_emoji(self, cache_impl): assert emoji.name == "OKOKOKOKOK" assert emoji.guild_id == snowflake.Snowflake(65234123) assert emoji.user == mock_user + assert emoji.user is not mock_user assert emoji.is_animated is True assert emoji.is_colons_required is False assert emoji.is_managed is False @@ -282,6 +287,7 @@ def test__build_emoji_with_passed_through_users(self, cache_impl): cache_impl._user_entries = {} emoji = cache_impl._build_emoji(emoji_data, cached_users={snowflake.Snowflake(56234232): mock_user}) assert emoji.user == mock_user + assert emoji.user is not mock_user def test__build_emoji_with_no_user(self, cache_impl): emoji_data = stateful_cache._KnownCustomEmojiData( @@ -325,7 +331,8 @@ def test_clear_emojis(self, cache_impl): } cache_impl._build_emoji = mock.Mock(side_effect=[mock_emoji_1, mock_emoji_2, mock_emoji_3]) cache_impl._garbage_collect_user = mock.Mock() - assert cache_impl.clear_emojis() == { + view = cache_impl.clear_emojis() + assert view == { snowflake.Snowflake(43123123): mock_emoji_1, snowflake.Snowflake(87643523): mock_emoji_2, snowflake.Snowflake(6873451): mock_emoji_3, @@ -726,7 +733,9 @@ def test_get_guild_for_known_guild_when_available(self, cache_impl): snowflake.Snowflake(54234123): stateful_cache._GuildRecord(), snowflake.Snowflake(543123): stateful_cache._GuildRecord(guild=mock_guild, is_available=True), } - assert cache_impl.get_guild(snowflake.Snowflake(543123)) == mock_guild + cached_guild = cache_impl.get_guild(snowflake.Snowflake(543123)) + assert cached_guild == mock_guild + assert cache_impl is not mock_guild def test_get_guild_for_known_guild_when_unavailable(self, cache_impl): mock_guild = mock.Mock(guilds.GatewayGuild) @@ -781,6 +790,7 @@ def test_set_guild(self, cache_impl): assert cache_impl.set_guild(mock_guild) is None assert 5123123 in cache_impl._guild_entries assert cache_impl._guild_entries[snowflake.Snowflake(5123123)].guild == mock_guild + assert cache_impl._guild_entries[snowflake.Snowflake(5123123)].guild is not mock_guild assert cache_impl._guild_entries[snowflake.Snowflake(5123123)].is_available is True def test_set_guild_availability(self, cache_impl): @@ -866,6 +876,8 @@ def test__build_invite(self, cache_impl): assert invite.channel_id == snowflake.Snowflake(87345234) assert invite.inviter == mock_inviter assert invite.target_user == mock_target_user + assert invite.inviter is not mock_inviter + assert invite.target_user is not mock_target_user assert invite.target_user_type is invites.TargetUserType.STREAM assert invite.approximate_presence_count is None assert invite.approximate_member_count is None @@ -896,6 +908,8 @@ def test__build_invite_with_passed_through_members(self, cache_impl): ) assert invite.inviter == mock_inviter assert invite.target_user == mock_target_user + assert invite.inviter is not mock_inviter + assert invite.target_user is not mock_target_user def test_clear_invites(self, cache_impl): mock_invite_data_1 = mock.Mock( @@ -1408,7 +1422,9 @@ def test_delete_me_for_unknown_me(self, cache_impl): def test_get_me_for_known_me(self, cache_impl): mock_own_user = mock.MagicMock(users.OwnUser) cache_impl._me = mock_own_user - assert cache_impl.get_me() == mock_own_user + cached_me = cache_impl.get_me() + assert cached_me == mock_own_user + assert cached_me is not mock_own_user def test_get_me_for_unknown_me(self, cache_impl): assert cache_impl.get_me() is None @@ -1417,6 +1433,7 @@ def test_set_me(self, cache_impl): mock_own_user = mock.MagicMock(users.OwnUser) assert cache_impl.set_me(mock_own_user) is None assert cache_impl._me == mock_own_user + assert cache_impl._me is not mock_own_user def test_update_me_for_cached_me(self, cache_impl): mock_cached_own_user = mock.MagicMock(users.OwnUser) @@ -1445,6 +1462,7 @@ def test__build_member(self, cache_impl): cache_impl._user_entries = {snowflake.Snowflake(512312354): stateful_cache._GenericRefWrapper(object=mock_user)} member = cache_impl._build_member(member_data) assert member.user == mock_user + assert member.user is not mock_user assert member.guild_id == 6434435234 assert member.nickname == "NICK" assert member.role_ids == (snowflake.Snowflake(65234), snowflake.Snowflake(654234123)) @@ -1468,6 +1486,7 @@ def test__build_member_for_passed_through_user(self, cache_impl): cache_impl._user_entries = {} member = cache_impl._build_member(member_data, cached_users={snowflake.Snowflake(512312354): mock_user}) assert member.user == mock_user + assert member.user is not mock_user def test_clear_members(self, cache_impl): mock_view_1 = mock.MagicMock(cache.ICacheView) @@ -1652,6 +1671,7 @@ def test_set_member(self, cache_impl): assert member_entry.guild_id == 67345234 assert member_entry.nickname == "A NICK LOL" assert member_entry.role_ids == (65345234, 123123) + assert member_entry.role_ids is not member_model.role_ids assert isinstance(member_entry.role_ids, tuple) assert member_entry.joined_at == datetime.datetime( 2020, 7, 15, 23, 30, 59, 501602, tzinfo=datetime.timezone.utc @@ -1841,6 +1861,7 @@ def test_set_user(self, cache_impl): assert cache_impl.set_user(mock_user) is None assert 6451234123 in cache_impl._user_entries assert cache_impl._user_entries[snowflake.Snowflake(6451234123)].object == mock_user + assert cache_impl._user_entries[snowflake.Snowflake(6451234123)].object is not mock_user assert cache_impl._user_entries[snowflake.Snowflake(6451234123)].ref_count == 0 def test_set_user_carries_over_ref_count(self, cache_impl): @@ -1852,6 +1873,7 @@ def test_set_user_carries_over_ref_count(self, cache_impl): assert cache_impl.set_user(mock_user) is None assert 6451234123 in cache_impl._user_entries assert cache_impl._user_entries[snowflake.Snowflake(6451234123)].object == mock_user + assert cache_impl._user_entries[snowflake.Snowflake(6451234123)].object is not mock_user assert cache_impl._user_entries[snowflake.Snowflake(6451234123)].ref_count == 42 def test_update_user(self, cache_impl): diff --git a/tests/hikari/utilities/test_attr_extensions.py b/tests/hikari/utilities/test_attr_extensions.py new file mode 100644 index 0000000000..665734984e --- /dev/null +++ b/tests/hikari/utilities/test_attr_extensions.py @@ -0,0 +1,434 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2020 Nekokatt +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. +import contextlib +import copy as stdlib_copy + +import attr +import mock + +from hikari.utilities import attr_extensions + + +def test_invalidate_shallow_copy_cache(): + attr_extensions._SHALLOW_COPIERS = {int: object(), str: object()} + assert attr_extensions.invalidate_shallow_copy_cache() is None + assert attr_extensions._SHALLOW_COPIERS == {} + + +def test_invalidate_deep_copy_cache(): + attr_extensions._DEEP_COPIERS = {str: object(), int: object(), object: object()} + assert attr_extensions.invalidate_deep_copy_cache() is None + assert attr_extensions._DEEP_COPIERS == {} + + +def test_get_fields_definition(): + @attr.s(init=True) + class StubModel: + foo: int = attr.ib(init=True) + bar: bool = attr.ib(init=False) + bam: bool = attr.ib(init=False) + _voodoo: str = attr.ib(init=True) + Bat: bool = attr.ib(init=True) + + fields = {field.name: field for field in attr.fields(StubModel)} + new_model = attr_extensions.get_fields_definition(StubModel) + assert new_model == ( + [(fields["foo"], "foo"), (fields["_voodoo"], "voodoo"), (fields["Bat"], "Bat")], + [fields["bar"], fields["bam"]], + ) + + +def test_generate_shallow_copier(): + @attr.s(init=True) + class StubModel: + _foo: int = attr.ib(init=True) + baaaa: str = attr.ib(init=True) + _blam: bool = attr.ib(init=True) + not_init: int = attr.ib(init=False) + no: bytes = attr.ib(init=True) + + old_model = StubModel(foo=42, baaaa="sheep", blam=True, no=b"okokokok") + old_model.not_init = 54234 + + copier = attr_extensions.generate_shallow_copier(StubModel) + new_model = copier(old_model) + + assert new_model is not old_model + assert new_model._foo is old_model._foo + assert new_model.baaaa is old_model.baaaa + assert new_model._blam is old_model._blam + assert new_model.not_init is old_model.not_init + assert new_model.no is old_model.no + + +def test_generate_shallow_copier_with_init_only_arguments(): + @attr.s(init=True) + class StubModel: + _gfd: int = attr.ib(init=True) + baaaa: str = attr.ib(init=True) + _blambat: bool = attr.ib(init=True) + no: bytes = attr.ib(init=True) + + old_model = StubModel(gfd=42, baaaa="sheep", blambat=True, no=b"okokokok") + + copier = attr_extensions.generate_shallow_copier(StubModel) + new_model = copier(old_model) + + assert new_model is not old_model + assert new_model._gfd is old_model._gfd + assert new_model.baaaa is old_model.baaaa + assert new_model._blambat is old_model._blambat + assert new_model.no is old_model.no + + +def test_generate_shallow_copier_with_only_non_init_attrs(): + @attr.s(init=True) + class StubModel: + _gfd: int = attr.ib(init=False) + baaaa: str = attr.ib(init=False) + _blambat: bool = attr.ib(init=False) + no: bytes = attr.ib(init=False) + + old_model = StubModel() + old_model._gfd = 42 + old_model.baaaa = "sheep" + old_model._blambat = True + old_model.no = b"okokokok" + + copier = attr_extensions.generate_shallow_copier(StubModel) + new_model = copier(old_model) + + assert new_model is not old_model + assert new_model._gfd is old_model._gfd + assert new_model.baaaa is old_model.baaaa + assert new_model._blambat is old_model._blambat + assert new_model.no is old_model.no + + +def test_generate_shallow_copier_with_no_attributes(): + @attr.s(init=True) + class StubModel: + ... + + old_model = StubModel() + + copier = attr_extensions.generate_shallow_copier(StubModel) + new_model = copier(old_model) + + assert new_model is not old_model + assert isinstance(new_model, StubModel) + + +def test_get_or_generate_shallow_copier_for_cached_copier(): + mock_copier = object() + + @attr.s(init=True) + class StubModel: + ... + + attr_extensions._SHALLOW_COPIERS = { + type("b", (), {}): object(), + StubModel: mock_copier, + type("a", (), {}): object(), + } + + assert attr_extensions.get_or_generate_shallow_copier(StubModel) is mock_copier + + +def test_get_or_generate_shallow_copier_for_uncached_copier(): + mock_copier = object() + + @attr.s(init=True) + class StubModel: + ... + + with mock.patch.object(attr_extensions, "generate_shallow_copier", return_value=mock_copier): + assert attr_extensions.get_or_generate_shallow_copier(StubModel) is mock_copier + + attr_extensions.generate_shallow_copier.assert_called_once_with(StubModel) + + assert attr_extensions._SHALLOW_COPIERS[StubModel] is mock_copier + + +def test_copy_attrs(): + mock_result = object() + mock_copier = mock.Mock(return_value=mock_result) + + @attr.s(init=True) + class StubModel: + ... + + model = StubModel() + + with mock.patch.object(attr_extensions, "get_or_generate_shallow_copier", return_value=mock_copier): + assert attr_extensions.copy_attrs(model) is mock_result + + attr_extensions.get_or_generate_shallow_copier.assert_called_once_with(StubModel) + + mock_copier.assert_called_once_with(model) + + +def test_generate_deep_copier(): + @attr.s + class StubBaseClass: + recursor: int = attr.ib(init=True) + _field: bool = attr.ib(init=True) + foo: str = attr.ib(init=True) + end: str = attr.ib(init=False) + _blam: bool = attr.ib(init=False) + + model = StubBaseClass(recursor=431, field=True, foo="blam") + model.end = "the way" + model._blam = "555555" + old_model_fields = stdlib_copy.copy(model) + copied_recursor = object() + copied_field = object() + copied_foo = object() + copied_end = object() + copied_blam = object() + memo = {123: object()} + + with mock.patch.object( + stdlib_copy, "deepcopy", side_effect=[copied_recursor, copied_field, copied_foo, copied_end, copied_blam], + ): + attr_extensions.generate_deep_copier(StubBaseClass)(model, memo) + + stdlib_copy.deepcopy.assert_has_calls( + [ + mock.call(old_model_fields.recursor, memo), + mock.call(old_model_fields._field, memo), + mock.call(old_model_fields.foo, memo), + mock.call(old_model_fields.end, memo), + mock.call(old_model_fields._blam, memo), + ] + ) + + assert model.recursor is copied_recursor + assert model._field is copied_field + assert model.foo is copied_foo + assert model.end is copied_end + assert model._blam is copied_blam + + +def test_generate_deep_copier_with_only_init_attributes(): + @attr.s + class StubBaseClass: + recursor: int = attr.ib(init=True) + _field: bool = attr.ib(init=True) + foo: str = attr.ib(init=True) + + model = StubBaseClass(recursor=431, field=True, foo="blam") + old_model_fields = stdlib_copy.copy(model) + copied_recursor = object() + copied_field = object() + copied_foo = object() + memo = {123: object()} + + with mock.patch.object( + stdlib_copy, "deepcopy", side_effect=[copied_recursor, copied_field, copied_foo], + ): + attr_extensions.generate_deep_copier(StubBaseClass)(model, memo) + + stdlib_copy.deepcopy.assert_has_calls( + [ + mock.call(old_model_fields.recursor, memo), + mock.call(old_model_fields._field, memo), + mock.call(old_model_fields.foo, memo), + ] + ) + + assert model.recursor is copied_recursor + assert model._field is copied_field + assert model.foo is copied_foo + + +def test_generate_deep_copier_with_only_non_init_attributes(): + @attr.s + class StubBaseClass: + end: str = attr.ib(init=False) + _blam: bool = attr.ib(init=False) + + model = StubBaseClass() + model.end = "the way" + model._blam = "555555" + old_model_fields = stdlib_copy.copy(model) + copied_end = object() + copied_blam = object() + memo = {123: object()} + + with mock.patch.object( + stdlib_copy, "deepcopy", side_effect=[copied_end, copied_blam], + ): + attr_extensions.generate_deep_copier(StubBaseClass)(model, memo) + + stdlib_copy.deepcopy.assert_has_calls( + [mock.call(old_model_fields.end, memo), mock.call(old_model_fields._blam, memo),] + ) + + assert model.end is copied_end + assert model._blam is copied_blam + + +def test_generate_deep_copier_with_no_attributes(): + @attr.s + class StubBaseClass: + ... + + model = StubBaseClass() + memo = {123: object()} + + with mock.patch.object( + stdlib_copy, "deepcopy", side_effect=NotImplementedError, + ): + attr_extensions.generate_deep_copier(StubBaseClass)(model, memo) + + stdlib_copy.deepcopy.assert_not_called() + + +def test_get_or_generate_deep_copier_for_cached_function(): + class StubClass: + ... + + mock_copier = object() + attr_extensions._DEEP_COPIERS = {} + + with mock.patch.object(attr_extensions, "generate_deep_copier", return_value=mock_copier): + assert attr_extensions.get_or_generate_deep_copier(StubClass) is mock_copier + + attr_extensions.generate_deep_copier.assert_called_once_with(StubClass) + + assert attr_extensions._DEEP_COPIERS[StubClass] is mock_copier + + +def test_get_or_generate_deep_copier_for_uncached_function(): + class StubClass: + ... + + mock_copier = object() + attr_extensions._DEEP_COPIERS = {StubClass: mock_copier} + + with mock.patch.object(attr_extensions, "generate_deep_copier"): + assert attr_extensions.get_or_generate_deep_copier(StubClass) is mock_copier + + attr_extensions.generate_deep_copier.assert_not_called() + + +def test_deep_copy_attrs_without_memo(): + class StubClass: + ... + + mock_object = StubClass() + mock_result = object() + mock_copier = mock.Mock(mock_result) + + stack = contextlib.ExitStack() + stack.enter_context(mock.patch.object(attr_extensions, "get_or_generate_deep_copier", return_value=mock_copier)) + stack.enter_context(mock.patch.object(stdlib_copy, "copy", return_value=mock_result)) + + with stack: + assert attr_extensions.deep_copy_attrs(mock_object) is mock_result + + stdlib_copy.copy.assert_called_once_with(mock_object) + attr_extensions.get_or_generate_deep_copier.assert_called_once_with(StubClass) + + mock_copier.assert_called_once_with(mock_result, {id(mock_object): mock_result}) + + +def test_deep_copy_attrs_with_memo(): + class StubClass: + ... + + mock_object = StubClass() + mock_result = object() + mock_copier = mock.Mock(mock_result) + mock_other_object = object() + + stack = contextlib.ExitStack() + stack.enter_context(mock.patch.object(attr_extensions, "get_or_generate_deep_copier", return_value=mock_copier)) + stack.enter_context(mock.patch.object(stdlib_copy, "copy", return_value=mock_result)) + + with stack: + assert attr_extensions.deep_copy_attrs(mock_object, {1235342: mock_other_object}) is mock_result + + stdlib_copy.copy.assert_called_once_with(mock_object) + attr_extensions.get_or_generate_deep_copier.assert_called_once_with(StubClass) + + mock_copier.assert_called_once_with(mock_result, {id(mock_object): mock_result, 1235342: mock_other_object}) + + +class TestCopyDecorator: + def test___copy__(self): + mock_result = object() + mock_copier = mock.Mock(return_value=mock_result) + + @attr.s() + @attr_extensions.with_copy + class StubClass: + ... + + model = StubClass() + + with mock.patch.object(attr_extensions, "get_or_generate_shallow_copier", return_value=mock_copier): + assert stdlib_copy.copy(model) is mock_result + + attr_extensions.get_or_generate_shallow_copier.assert_called_once_with(StubClass) + + mock_copier.assert_called_once_with(model) + + def test___deep__copy(self): + class CopyingMock(mock.Mock): + def __call__(self, /, *args, **kwargs): + args = list(args) + args[1] = dict(args[1]) + return super().__call__(*args, **kwargs) + + mock_result = object() + mock_copier = CopyingMock(return_value=mock_result) + + @attr.s() + @attr_extensions.with_copy + class StubClass: + ... + + model = StubClass() + stack = contextlib.ExitStack() + stack.enter_context(mock.patch.object(attr_extensions, "get_or_generate_deep_copier", return_value=mock_copier)) + stack.enter_context(mock.patch.object(stdlib_copy, "copy", return_value=mock_result)) + + with stack: + assert stdlib_copy.deepcopy(model) is mock_result + + stdlib_copy.copy.assert_called_once_with(model) + attr_extensions.get_or_generate_deep_copier.assert_called_once_with(StubClass) + + mock_copier.assert_called_once_with(mock_result, {id(model): mock_result}) + + def test_copy_decorator_inheritance(self): + @attr_extensions.with_copy + @attr.s() + class ParentClass: + ... + + class Foo(ParentClass): + ... + + assert Foo.__copy__ == attr_extensions.copy_attrs + assert Foo.__deepcopy__ == attr_extensions.deep_copy_attrs diff --git a/tests/hikari/utilities/test_undefined.py b/tests/hikari/utilities/test_undefined.py index acf67001d0..dbf9519b75 100644 --- a/tests/hikari/utilities/test_undefined.py +++ b/tests/hikari/utilities/test_undefined.py @@ -18,6 +18,8 @@ # 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 copy + import pytest from hikari.utilities import undefined @@ -46,3 +48,9 @@ def test_count(self): def test_cannot_reinstatiate(self): with pytest.raises(TypeError): type(undefined.UNDEFINED)() + + def test_copy(self): + assert copy.copy(undefined.UNDEFINED) is undefined.UNDEFINED + + def test_deepcopy(self): + assert copy.deepcopy(undefined.UNDEFINED) is undefined.UNDEFINED From 6488ed365e3f317116823da28b5f4be8c5b8eb6c Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Sat, 8 Aug 2020 14:46:57 +0000 Subject: [PATCH 862/922] Update pipelines/config.py, pipelines/deploy.nox.py, pipelines/flake8.nox.py, pipelines/format.nox.py, pipelines/mypy.nox.py, pipelines/nox.py, pipelines/pages.nox.py, pipelines/pdoc.nox.py, pipelines/pip.nox.py, pipelines/pytest.nox.py, pipelines/safety.nox.py, pipelines/twemoji.nox.py, pipelines/utils.nox.py, noxfile.py files Deleted ci.dockerfile, .dockerignore, ci/docker.nox.py files --- .dockerignore | 9 ---- ci.dockerfile | 4 -- ci/docker.nox.py | 81 -------------------------------- noxfile.py | 2 +- {ci => pipelines}/config.py | 9 ---- {ci => pipelines}/deploy.nox.py | 4 +- {ci => pipelines}/flake8.nox.py | 4 +- {ci => pipelines}/format.nox.py | 4 +- {ci => pipelines}/mypy.nox.py | 4 +- {ci => pipelines}/nox.py | 2 +- {ci => pipelines}/pages.nox.py | 4 +- {ci => pipelines}/pdoc.nox.py | 4 +- {ci => pipelines}/pip.nox.py | 4 +- {ci => pipelines}/pytest.nox.py | 4 +- {ci => pipelines}/safety.nox.py | 2 +- {ci => pipelines}/twemoji.nox.py | 2 +- {ci => pipelines}/utils.nox.py | 2 +- 17 files changed, 21 insertions(+), 124 deletions(-) delete mode 100644 .dockerignore delete mode 100644 ci.dockerfile delete mode 100644 ci/docker.nox.py rename {ci => pipelines}/config.py (95%) rename {ci => pipelines}/deploy.nox.py (99%) rename {ci => pipelines}/flake8.nox.py (97%) rename {ci => pipelines}/format.nox.py (99%) rename {ci => pipelines}/mypy.nox.py (96%) rename {ci => pipelines}/nox.py (98%) rename {ci => pipelines}/pages.nox.py (97%) rename {ci => pipelines}/pdoc.nox.py (97%) rename {ci => pipelines}/pip.nox.py (99%) rename {ci => pipelines}/pytest.nox.py (98%) rename {ci => pipelines}/safety.nox.py (98%) rename {ci => pipelines}/twemoji.nox.py (98%) rename {ci => pipelines}/utils.nox.py (98%) diff --git a/.dockerignore b/.dockerignore deleted file mode 100644 index 8e24623532..0000000000 --- a/.dockerignore +++ /dev/null @@ -1,9 +0,0 @@ -.venv/ -.coverage -.pytest_cache/ -gitlab/ -insomnia/ -public/ -.gitlab-ci.yml -lgtm.yml -poetry.lock diff --git a/ci.dockerfile b/ci.dockerfile deleted file mode 100644 index a5c87ff651..0000000000 --- a/ci.dockerfile +++ /dev/null @@ -1,4 +0,0 @@ -FROM python:3.8.3 -COPY . . -RUN pip install nox -RUN nox diff --git a/ci/docker.nox.py b/ci/docker.nox.py deleted file mode 100644 index f021033c14..0000000000 --- a/ci/docker.nox.py +++ /dev/null @@ -1,81 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright (c) 2020 Nekokatt -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in all -# copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -# SOFTWARE. -"""Allows running CI scripts within a Docker container.""" -import os -import random -import shlex -import shutil - -from nox import options - -from ci import config -from ci import nox - -if shutil.which("docker"): - - @nox.session(reuse_venv=True) - def docker(session: nox.Session) -> None: - """Run a nox session in a container that targets a specific Python version. - - This will invoke nox with the given additional arguments. - """ - - try: - args = ["--help"] if not session.posargs else session.posargs - python, *args = args - args = shlex.join(args) - - if python not in config.DOCKER_ENVS: - print(f"\033[31m\033[1mNo environment called {python} found.\033[0m") - raise IndexError - except IndexError: - env_example = random.choice(config.DOCKER_ENVS) - command_example = random.choice(options.sessions) - print( - "USAGE: nox -s docker -- {env} [{arg}, ...]", - "", - docker.__doc__, - f"For example: 'nox -s docker -- {env_example} -s {command_example}'", - "", - "Parameters:", - " {env} The environment to build. Supported environments are:", - *(f" - {e}" for e in config.DOCKER_ENVS), - " {arg} Argument to pass to nox within the container.", - "", - "Supported sessions include:", - *(f" - {s}" for s in options.sessions if s != "docker"), - sep="\n", - ) - return - - print("\033[33m<<<<<<<<<<<<<<<<<<< ENTERING CONTAINER >>>>>>>>>>>>>>>>>>>\033[0m") - print(f"> will run 'nox {args}' in container using '{python}' image.") - nox.shell( - "docker", - "run", - "--mount", - f"type=bind,source={os.path.abspath(os.getcwd())!r},target=/hikari", - "--rm", - "-it", - python, - f"/bin/sh -c 'cd hikari && pip install nox && nox {args}'", - ) - print("\033[33m<<<<<<<<<<<<<<<<<<< EXITING CONTAINER >>>>>>>>>>>>>>>>>>>\033[0m") diff --git a/noxfile.py b/noxfile.py index d7e7ef09e7..d65091ec98 100644 --- a/noxfile.py +++ b/noxfile.py @@ -22,7 +22,7 @@ import runpy import sys -CI_PATH = "ci" +CI_PATH = "pipelines" sys.path.append(os.getcwd()) diff --git a/ci/config.py b/pipelines/config.py similarity index 95% rename from ci/config.py rename to pipelines/config.py index 0e869fd7b9..ceb1dd04fc 100644 --- a/ci/config.py +++ b/pipelines/config.py @@ -69,12 +69,3 @@ PYPI_REPO = "https://upload.pypi.org/legacy/" PYPI = "https://pypi.org/" PYPI_API = f"{PYPI}/pypi/{API_NAME}/json" - -# Docker stuff -DOCKER_ENVS = [ - "python:3.8.0", - "python:3.8.1", - "python:3.8.2", - "python:3.8.3", - "python:3.9-rc", -] diff --git a/ci/deploy.nox.py b/pipelines/deploy.nox.py similarity index 99% rename from ci/deploy.nox.py rename to pipelines/deploy.nox.py index cb8fb5ff2c..1979adf364 100644 --- a/ci/deploy.nox.py +++ b/pipelines/deploy.nox.py @@ -26,8 +26,8 @@ import subprocess from distutils.version import LooseVersion -from ci import config -from ci import nox +from pipelines import config +from pipelines import nox def update_version_string(version): diff --git a/ci/flake8.nox.py b/pipelines/flake8.nox.py similarity index 97% rename from ci/flake8.nox.py rename to pipelines/flake8.nox.py index 4feb7c0ef9..46677f34cd 100644 --- a/ci/flake8.nox.py +++ b/pipelines/flake8.nox.py @@ -21,8 +21,8 @@ import os import shutil -from ci import config -from ci import nox +from pipelines import config +from pipelines import nox @nox.session(reuse_venv=True) diff --git a/ci/format.nox.py b/pipelines/format.nox.py similarity index 99% rename from ci/format.nox.py rename to pipelines/format.nox.py index b94c5068da..c931d0169c 100644 --- a/ci/format.nox.py +++ b/pipelines/format.nox.py @@ -24,13 +24,13 @@ import subprocess import time -from ci import nox +from pipelines import nox REFORMATING_PATHS = [ "hikari", "tests", "scripts", - "ci", + "pipelines", "setup.py", "noxfile.py", ] diff --git a/ci/mypy.nox.py b/pipelines/mypy.nox.py similarity index 96% rename from ci/mypy.nox.py rename to pipelines/mypy.nox.py index 849502e62b..ed85e4a2f0 100644 --- a/ci/mypy.nox.py +++ b/pipelines/mypy.nox.py @@ -19,8 +19,8 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. -from ci import config -from ci import nox +from pipelines import config +from pipelines import nox @nox.session(reuse_venv=True) diff --git a/ci/nox.py b/pipelines/nox.py similarity index 98% rename from ci/nox.py rename to pipelines/nox.py index 67002f600a..7709d265da 100644 --- a/ci/nox.py +++ b/pipelines/nox.py @@ -28,7 +28,7 @@ from nox import session as _session from nox.sessions import Session -from ci import config +from pipelines import config # Default sessions should be defined here _options.sessions = ["reformat-code", "pytest", "pdoc", "pages", "flake8", "mypy", "safety"] diff --git a/ci/pages.nox.py b/pipelines/pages.nox.py similarity index 97% rename from ci/pages.nox.py rename to pipelines/pages.nox.py index f93b9be3c9..ed88d0633e 100644 --- a/ci/pages.nox.py +++ b/pipelines/pages.nox.py @@ -22,8 +22,8 @@ import os import shutil -from ci import config -from ci import nox +from pipelines import config +from pipelines import nox def copy_from_in(src: str, dest: str) -> None: diff --git a/ci/pdoc.nox.py b/pipelines/pdoc.nox.py similarity index 97% rename from ci/pdoc.nox.py rename to pipelines/pdoc.nox.py index beed3bfce8..f12ea21848 100644 --- a/ci/pdoc.nox.py +++ b/pipelines/pdoc.nox.py @@ -22,8 +22,8 @@ import os import shutil -from ci import config -from ci import nox +from pipelines import config +from pipelines import nox @nox.session(reuse_venv=True) diff --git a/ci/pip.nox.py b/pipelines/pip.nox.py similarity index 99% rename from ci/pip.nox.py rename to pipelines/pip.nox.py index 9f85c28f6a..4c63163bac 100644 --- a/ci/pip.nox.py +++ b/pipelines/pip.nox.py @@ -26,8 +26,8 @@ import tarfile import tempfile -from ci import config -from ci import nox +from pipelines import config +from pipelines import nox @contextlib.contextmanager diff --git a/ci/pytest.nox.py b/pipelines/pytest.nox.py similarity index 98% rename from ci/pytest.nox.py rename to pipelines/pytest.nox.py index 4c66c72103..1c58e01e71 100644 --- a/ci/pytest.nox.py +++ b/pipelines/pytest.nox.py @@ -22,8 +22,8 @@ import os import shutil -from ci import config -from ci import nox +from pipelines import config +from pipelines import nox FLAGS = [ "-c", diff --git a/ci/safety.nox.py b/pipelines/safety.nox.py similarity index 98% rename from ci/safety.nox.py rename to pipelines/safety.nox.py index 1bb27a8f79..6dcb83f933 100644 --- a/ci/safety.nox.py +++ b/pipelines/safety.nox.py @@ -20,7 +20,7 @@ # SOFTWARE. """Dependency scanning.""" -from ci import nox +from pipelines import nox @nox.session(reuse_venv=True) diff --git a/ci/twemoji.nox.py b/pipelines/twemoji.nox.py similarity index 98% rename from ci/twemoji.nox.py rename to pipelines/twemoji.nox.py index d597d485e7..0762bf5101 100644 --- a/ci/twemoji.nox.py +++ b/pipelines/twemoji.nox.py @@ -18,7 +18,7 @@ # 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. -from ci import nox +from pipelines import nox @nox.session(reuse_venv=True) diff --git a/ci/utils.nox.py b/pipelines/utils.nox.py similarity index 98% rename from ci/utils.nox.py rename to pipelines/utils.nox.py index 092cb1357a..14894f7134 100644 --- a/ci/utils.nox.py +++ b/pipelines/utils.nox.py @@ -21,7 +21,7 @@ """Additional utilities for Nox.""" import shutil -from ci import nox +from pipelines import nox TRASH = [ ".nox", From 6280d79268d07057bfd21a6fbfe47bc4a56bb534 Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Sat, 8 Aug 2020 21:09:15 +0100 Subject: [PATCH 863/922] Added stubgen hack to deploy script to generate stubs for '__init__.py' files only. This prevents a strange bug in MyPy where strict mode fails to resolve the '__all__' attributes in each package '__init__.py', thus wrongly reporting that members such as 'hikari.Event' do not exist when we all know they clearly do. This bug will still exist if the user installs hikari via git. I am still thinking of a good workaround for all of this. --- .gitignore | 3 +++ pipelines/deploy.nox.py | 57 +++++++++++++++++++++++++++++++++++++++++ pipelines/nox.py | 1 + 3 files changed, 61 insertions(+) diff --git a/.gitignore b/.gitignore index 0bc2d87828..9d1c9fee14 100644 --- a/.gitignore +++ b/.gitignore @@ -53,3 +53,6 @@ credentials.env # Profiling in pytest prof/ + +# Generated stubs for MyPy --strict hackfixes. +hikari/**/__init__.pyi diff --git a/pipelines/deploy.nox.py b/pipelines/deploy.nox.py index 1979adf364..e6fd72f301 100644 --- a/pipelines/deploy.nox.py +++ b/pipelines/deploy.nox.py @@ -210,10 +210,67 @@ def send_notification(version: str, title: str, description: str, color: str) -> ) +@nox.session() +def stubgen_hack(session: nox.Session) -> None: + session.install("-r", "mypy-requirements.txt", "-r", "requirements.txt") + + # MyPy seems to struggle to understand what is exported from `hikari/__init__.py` + # due to disabling implicit exports in strict mode. + # This works around the issue by injecting a helpful stub. Saves risk of error + # and verbosity later by having to hard code 200 classes into an `__all__` + # list manually. + print("Generating stub workaround for __init__.py to allow --strict usage.") + + # Sniff license header from __init__.py + header = [] + with open(os.path.join(config.MAIN_PACKAGE, "__init__.py")) as fp: + while (line := fp.readline()).startswith("#") or not line.strip(): + header.append(line.rstrip()) + + header = "\n".join( + ( + *header, + "\n", + "# This stubfile is generated by Hikari's deploy script as a workaround\n" + "# for our design not working correctly with MyPy's --strict flag. By\n" + "# explicitly generating this stub, MyPy no longer gets confused by what\n" + "# members are re-exported by package level init scripts. This lets you\n" + "# type check as strictly as possible and still get correct results.\n" + "#\n" + "# For all other purposes, you can completely ignore this file!\n", + "\n", + ) + ) + + for root, dirs, files in os.walk(config.MAIN_PACKAGE, topdown=True, followlinks=False): + for f in files: + if f == "__init__.py": + module = ".".join(root.split(os.sep)) + file = os.path.join(root, f) + "i" + nox.shell("stubgen", "-m", module, "-o", ".") + + print("Adding license header to stub", file, "for module", module) + with open(file) as fp: + stub = fp.read() + + with open(file, "w") as fp: + fp.write(header) + fp.write(stub) + + @nox.session() def deploy(session: nox.Session) -> None: """Perform a deployment. This will only work on the CI.""" + print("Injecting stubgen hack to allow for --strict in MyPy for users") + nox.registry["stubgen-hack"](session) + + print("Re-running code formatting fixes") + nox.registry["reformat-code"](session) + + return + nox.shell("pip install requests") + commit_ref = os.getenv("CI_COMMIT_REF_NAME", *session.posargs[0:1]) print("Commit ref is", commit_ref) current_version = get_current_version() diff --git a/pipelines/nox.py b/pipelines/nox.py index 7709d265da..c66074a24c 100644 --- a/pipelines/nox.py +++ b/pipelines/nox.py @@ -26,6 +26,7 @@ from nox import options as _options from nox import session as _session +from nox.registry import _REGISTRY as registry from nox.sessions import Session from pipelines import config From ad382a60b992a1d26cbbb428cd9cbb151e79f140 Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Sat, 8 Aug 2020 21:09:55 +0100 Subject: [PATCH 864/922] Re-enabled deploy script. --- pipelines/deploy.nox.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/pipelines/deploy.nox.py b/pipelines/deploy.nox.py index e6fd72f301..e550de2579 100644 --- a/pipelines/deploy.nox.py +++ b/pipelines/deploy.nox.py @@ -267,8 +267,6 @@ def deploy(session: nox.Session) -> None: print("Re-running code formatting fixes") nox.registry["reformat-code"](session) - return - nox.shell("pip install requests") commit_ref = os.getenv("CI_COMMIT_REF_NAME", *session.posargs[0:1]) From ac0c18a555379e0866c97b6edf285f6fe6dcb8b3 Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Sat, 8 Aug 2020 21:41:06 +0100 Subject: [PATCH 865/922] Added mypy and flake8 html reports to CI. --- .gitlab/ci/linting.yml | 4 ++++ dev-requirements.txt | 1 - flake-requirements.txt | 2 +- mypy-requirements.txt | 1 + pipelines/config.py | 1 + pipelines/flake8.nox.py | 31 +++++++++++++++++++------------ pipelines/mypy.nox.py | 10 +++++++++- 7 files changed, 35 insertions(+), 15 deletions(-) diff --git a/.gitlab/ci/linting.yml b/.gitlab/ci/linting.yml index ad0f569607..faa5252b9c 100644 --- a/.gitlab/ci/linting.yml +++ b/.gitlab/ci/linting.yml @@ -33,6 +33,8 @@ lint:flake8: artifacts: expire_in: 2 days + paths: + - public/flake8 reports: junit: public/flake8-junit.xml when: always @@ -51,6 +53,8 @@ lint:mypy: - nox -s mypy --no-error-on-external-run artifacts: expire_in: 2 days + paths: + - public/mypy reports: junit: public/mypy.xml when: always diff --git a/dev-requirements.txt b/dev-requirements.txt index 71b5e9f448..fadec8eb20 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -20,4 +20,3 @@ MutPy==0.6.1 # Other stuff async-timeout~=3.0.1 # Used for timeouts in some test cases. -isort==5.2.2 diff --git a/flake-requirements.txt b/flake-requirements.txt index 3caba7672a..465f1b82d7 100644 --- a/flake-requirements.txt +++ b/flake-requirements.txt @@ -22,6 +22,6 @@ flake8-mutable~=1.2.0 # mutable default argument detection flake8-pep3101~=1.3.0 # new-style format strings only flake8-print~=3.1.4 # complain about print statements in code flake8-printf-formatting~=1.1.0 # forbey printf-style python2 string formatting -flake8-pytest-style~=1.2.2 # pytest checks +flake8-pytest-style~=1.2.3 # pytest checks flake8-raise~=0.0.5 # exception raising linting flake8-use-fstring~=1.1 # format string checking diff --git a/mypy-requirements.txt b/mypy-requirements.txt index a07027b3bc..62320bd686 100644 --- a/mypy-requirements.txt +++ b/mypy-requirements.txt @@ -1 +1,2 @@ mypy==0.782 +lxml==4.5.2 # HTML reporting. diff --git a/pipelines/config.py b/pipelines/config.py index ceb1dd04fc..f390e5e758 100644 --- a/pipelines/config.py +++ b/pipelines/config.py @@ -39,6 +39,7 @@ FLAKE8_TXT = "public/flake8.txt" MYPY_INI = "mypy.ini" MYPY_JUNIT_OUTPUT_PATH = _os.path.join(ARTIFACT_DIRECTORY, "mypy.xml") +MYPY_HTML_OUTPUT_PATH = _os.path.join(ARTIFACT_DIRECTORY, "mypy") PYDOCSTYLE_INI = "pydocstyle.ini" PYTEST_INI = "pytest.ini" PYTEST_HTML_OUTPUT_PATH = _os.path.join(ARTIFACT_DIRECTORY, "pytest.html") diff --git a/pipelines/flake8.nox.py b/pipelines/flake8.nox.py index 46677f34cd..9326b2bcf5 100644 --- a/pipelines/flake8.nox.py +++ b/pipelines/flake8.nox.py @@ -30,22 +30,29 @@ def flake8(session: nox.Session) -> None: """Run code linting, SAST, and analysis.""" session.install("-r", "requirements.txt", "-r", "flake-requirements.txt") - session.run( - "flake8", "--exit-zero", "--format=html", f"--htmldir={config.FLAKE8_HTML}", config.MAIN_PACKAGE, - ) - if "GITLAB_CI" in os.environ or "--gitlab" in session.posargs: - print("Detected GitLab, will output CodeClimate report instead!") + print("Generating HTML report") + + shutil.rmtree(config.FLAKE8_TXT, ignore_errors=True) + + session.run( + "flake8", "--exit-zero", "--format=html", f"--htmldir={config.FLAKE8_HTML}", config.MAIN_PACKAGE, + ) + + shutil.rmtree(config.FLAKE8_TXT, ignore_errors=True) + + print("Detected GitLab, will output CodeClimate report next!") # If we add the args for --statistics or --show-source, the thing breaks # silently, and I cant find another decent package that actually works # in any of the gitlab-supported formats :( - format_args = ["--format=junit-xml", f"--output-file={config.FLAKE8_JUNIT}"] - else: - format_args = [f"--output-file={config.FLAKE8_TXT}", "--statistics", "--show-source", "--tee"] - # This is because flake8 just appends to the file, so you can end up with - # a huge file with the same errors if you run it a couple of times. - shutil.rmtree(config.FLAKE8_TXT, ignore_errors=True) + session.run( + "flake8", "--exit-zero", "--format=junit-xml", f"--output-file={config.FLAKE8_JUNIT}", config.MAIN_PACKAGE, + ) + + print("Generating console output") + + shutil.rmtree(config.FLAKE8_TXT, ignore_errors=True) session.run( - "flake8", *format_args, config.MAIN_PACKAGE, + "flake8", f"--output-file={config.FLAKE8_TXT}", "--statistics", "--show-source", "--tee", config.MAIN_PACKAGE, ) diff --git a/pipelines/mypy.nox.py b/pipelines/mypy.nox.py index ed85e4a2f0..383a728d7e 100644 --- a/pipelines/mypy.nox.py +++ b/pipelines/mypy.nox.py @@ -28,5 +28,13 @@ def mypy(session: nox.Session) -> None: """Perform static type analysis on Python source code.""" session.install("-r", "requirements.txt", "-r", "mypy-requirements.txt") session.run( - "mypy", "-p", config.MAIN_PACKAGE, "--config", config.MYPY_INI, "--junit-xml", config.MYPY_JUNIT_OUTPUT_PATH, + "mypy", + "-p", + config.MAIN_PACKAGE, + "--config", + config.MYPY_INI, + "--junit-xml", + config.MYPY_JUNIT_OUTPUT_PATH, + "--html-report", + config.MYPY_HTML_OUTPUT_PATH, ) From b46391b34a3d6c4e5613ccf97221a128c56c3e7b Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Sat, 8 Aug 2020 21:55:01 +0100 Subject: [PATCH 866/922] Added MissingIntentWarning hook to wait_for --- hikari/impl/event_manager_base.py | 42 +++++++++++++++++-------------- 1 file changed, 23 insertions(+), 19 deletions(-) diff --git a/hikari/impl/event_manager_base.py b/hikari/impl/event_manager_base.py index 896904ec16..62313b5b7f 100644 --- a/hikari/impl/event_manager_base.py +++ b/hikari/impl/event_manager_base.py @@ -106,7 +106,27 @@ def subscribe( ) -> event_dispatcher.AsyncCallbackT[event_dispatcher.EventT_co]: # `_nested` is used to show the correct source code snippet if an intent # warning is triggered. + self._check_intents(event_type, _nested) + if event_type not in self._listeners: + self._listeners[event_type] = [] + + if not asyncio.iscoroutinefunction(callback): + raise TypeError("Event callbacks must be coroutine functions (`async def')") + + _LOGGER.debug( + "subscribing callback 'async def %s%s' to event-type %s.%s", + getattr(callback, "__name__", ""), + reflect.resolve_signature(callback), + event_type.__module__, + event_type.__qualname__, + ) + + self._listeners[event_type].append(callback) # type: ignore[arg-type] + + return callback + + def _check_intents(self, event_type: typing.Type[event_dispatcher.EventT_co], nested: int) -> None: # If None, the user is on v6 with intents disabled, so we don't care. if self._intents is not None: # Collection of combined bitfield combinations of intents that @@ -124,27 +144,9 @@ def subscribe( f"You have tried to listen to {event_type.__name__}, but this will only ever be triggered if " f"you enable one of the following intents: {expected_intents_str}.", category=errors.MissingIntentWarning, - stacklevel=_nested + 2, + stacklevel=nested + 3, ) - if event_type not in self._listeners: - self._listeners[event_type] = [] - - if not asyncio.iscoroutinefunction(callback): - raise TypeError("Event callbacks must be coroutine functions (`async def')") - - _LOGGER.debug( - "subscribing callback 'async def %s%s' to event-type %s.%s", - getattr(callback, "__name__", ""), - reflect.resolve_signature(callback), - event_type.__module__, - event_type.__qualname__, - ) - - self._listeners[event_type].append(callback) # type: ignore[arg-type] - - return callback - def get_listeners( self, event_type: typing.Type[event_dispatcher.EventT_co], *, polymorphic: bool = True, ) -> typing.Collection[event_dispatcher.AsyncCallbackT[event_dispatcher.EventT_co]]: @@ -308,6 +310,8 @@ async def wait_for( if predicate is None: predicate = _default_predicate + self._check_intents(event_type, 1) + future: asyncio.Future[event_dispatcher.EventT_co] = asyncio.get_event_loop().create_future() try: From 746fcebc8622f273a93c702047d028ba9e94884e Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Sat, 8 Aug 2020 23:17:31 +0100 Subject: [PATCH 867/922] Switched mutation test tool. --- dev-requirements.txt | 3 -- hikari/impl/entity_factory.py | 5 +-- hikari/models/guilds.py | 4 +- hikari/models/users.py | 4 +- pipelines/pytest.nox.py | 71 +++++++++++++++++++++++------------ 5 files changed, 52 insertions(+), 35 deletions(-) diff --git a/dev-requirements.txt b/dev-requirements.txt index fadec8eb20..49f1b2a40a 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -15,8 +15,5 @@ pytest-testdox==1.2.1 # Coverage testing. coverage~=5.2.1 -# Mutation testing -MutPy==0.6.1 - # Other stuff async-timeout~=3.0.1 # Used for timeouts in some test cases. diff --git a/hikari/impl/entity_factory.py b/hikari/impl/entity_factory.py index 133b17bc58..5da915a290 100644 --- a/hikari/impl/entity_factory.py +++ b/hikari/impl/entity_factory.py @@ -987,10 +987,7 @@ def deserialize_member( role_ids = [snowflake.Snowflake(role_id) for role_id in payload["roles"]] - joined_at: undefined.UndefinedOr[datetime.datetime] = undefined.UNDEFINED - raw_joined_at = payload.get("joined_at") - if raw_joined_at is not None: - joined_at = date.iso8601_datetime_string_to_datetime(raw_joined_at) + joined_at = date.iso8601_datetime_string_to_datetime(payload["joined_at"]) premium_since: undefined.UndefinedOr[typing.Optional[datetime.datetime]] = undefined.UNDEFINED if "premium_since" in payload: diff --git a/hikari/models/guilds.py b/hikari/models/guilds.py index 14ff20eabc..e62fe89f56 100644 --- a/hikari/models/guilds.py +++ b/hikari/models/guilds.py @@ -303,7 +303,7 @@ class Member(users.User): role_ids: typing.Sequence[snowflake.Snowflake] = attr.ib(eq=False, hash=False, repr=False) """A sequence of the IDs of the member's current roles.""" - joined_at: undefined.UndefinedOr[datetime.datetime] = attr.ib(eq=False, hash=False, repr=False) + joined_at: datetime.datetime = attr.ib(eq=False, hash=False, repr=True) """The datetime of when this member joined the guild they belong to.""" premium_since: undefined.UndefinedNoneOr[datetime.datetime] = attr.ib(eq=False, hash=False, repr=False) @@ -418,7 +418,7 @@ def mention(self) -> str: builtins.str The mention string to use. """ - return f"<@!{self.id}>" if isinstance(self.nickname, str) else self.user.mention + return f"<@!{self.id}>" if self.nickname is not None else self.user.mention def __str__(self) -> str: return str(self.user) diff --git a/hikari/models/users.py b/hikari/models/users.py index 177b0afd48..a58ef9336b 100644 --- a/hikari/models/users.py +++ b/hikari/models/users.py @@ -256,10 +256,10 @@ class PartialUser(snowflake.Unique): avatar_hash: undefined.UndefinedNoneOr[str] = attr.ib(eq=False, hash=False, repr=False) """Avatar hash of the user, if a custom avatar is set.""" - is_bot: undefined.UndefinedOr[bool] = attr.ib(eq=False, hash=False, repr=False) + is_bot: undefined.UndefinedOr[bool] = attr.ib(eq=False, hash=False, repr=True) """Whether this user is a bot account.""" - is_system: undefined.UndefinedOr[bool] = attr.ib(eq=False, hash=False) + is_system: undefined.UndefinedOr[bool] = attr.ib(eq=False, hash=False, repr=False) """Whether this user is a system account.""" flags: undefined.UndefinedOr[UserFlag] = attr.ib(eq=False, hash=False) diff --git a/pipelines/pytest.nox.py b/pipelines/pytest.nox.py index 1c58e01e71..8b1f34131e 100644 --- a/pipelines/pytest.nox.py +++ b/pipelines/pytest.nox.py @@ -19,6 +19,7 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. """Py.test integration.""" +import contextlib import os import shutil @@ -44,7 +45,6 @@ config.COVERAGE_JUNIT_PATH, "--force-testdox", "--showlocals", - config.TEST_PACKAGE, ] @@ -53,7 +53,7 @@ def pytest(session: nox.Session) -> None: """Run unit tests and measure code coverage.""" session.install("-r", "requirements.txt", "-r", "dev-requirements.txt") shutil.rmtree(".coverage", ignore_errors=True) - session.run("python", "-m", "pytest", *FLAGS, *session.posargs) + session.run("python", "-m", "pytest", *FLAGS, *session.posargs, config.TEST_PACKAGE) @nox.session(reuse_venv=True) @@ -78,36 +78,59 @@ def pytest_profile(session: nox.Session) -> None: @nox.session(reuse_venv=True) -def mutpy(session: nox.Session) -> None: +def mutation_test(session: nox.Session) -> None: """Run mutation tests on a given module and test suite. This randomly mutates the module undergoing testing to make it invalid by altering parts of the code. It will then attempt to run the tests to verify that they now fail. """ - if len(session.posargs) < 2: - print("Please provide two arguments:") - print(" 1. the module to mutate") - print(" 2. the test suite for this module") - print() - print("e.g. nox -s mutpy -- foo test_foo") - exit(1) - - session.install("-r", "requirements.txt", "-r", "dev-requirements.txt") - session.run( - "mut.py", - "--target", - session.posargs[0], - "--unit-test", - session.posargs[1], - "--runner", - "pytest", - "-c", - "--disable-operator", - "SCI", # SCI is buggy for some reason. - *session.posargs[2:], + session.install( + "-r", "requirements.txt", "-r", "dev-requirements.txt", "git+https://github.com/sixty-north/cosmic-ray" ) + with contextlib.suppress(Exception): + os.mkdir(config.ARTIFACT_DIRECTORY) + + toml = os.path.join(config.ARTIFACT_DIRECTORY, "mutation.toml") + + path = input("What module do you wish to mutate? (e.g hikari/utilities) ") + package = input("What module do you wish to test? (e.g tests/hikari/utilities) ") + + print("Creating config file") + + with open(toml, "w") as fp: + cfg = ( + "[cosmic-ray]\n" + f"module-path = {path!r}\n" + "python-version = ''\n" + "timeout = 10\n" + "exclude-modules = []\n" + f"test-command = 'pytest -x {package}'" + "\n" + "[cosmic-ray.execution-engine]\n" + "name = 'local'\n" + "\n" + "[cosmic-ray.cloning]\n" + "method = 'copy'\n" + "commands = []\n" + ) + + print(cfg) + + fp.write(cfg) + + sqlite = os.path.join(config.ARTIFACT_DIRECTORY, "mutation.db") + + print("Initializing session and generating test cases") + session.run("cosmic-ray", "init", toml, sqlite) + + print("Performing mutation tests") + session.run("cosmic-ray", "exec", sqlite) + + print("Generating reports") + session.run("cr-report", sqlite) + @nox.session(reuse_venv=True) def coalesce_coverage(session: nox.Session) -> None: From 7dce7dd13a9c889bb29b04620aa203f4ced1fe29 Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Sat, 8 Aug 2020 23:38:29 +0100 Subject: [PATCH 868/922] Fixed failing test case. --- tests/hikari/impl/test_entity_factory.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/hikari/impl/test_entity_factory.py b/tests/hikari/impl/test_entity_factory.py index a5b9d277e9..6d016f307e 100644 --- a/tests/hikari/impl/test_entity_factory.py +++ b/tests/hikari/impl/test_entity_factory.py @@ -1296,7 +1296,7 @@ def test_deserialize_member_with_null_fields(self, entity_factory_impl, user_pay { "nick": None, "roles": ["11111", "22222", "33333", "44444"], - "joined_at": None, + "joined_at": "2015-04-26T06:26:56.936000+00:00", "premium_since": None, "deaf": False, "mute": True, @@ -1309,7 +1309,6 @@ def test_deserialize_member_with_null_fields(self, entity_factory_impl, user_pay assert member.is_deaf is False assert member.is_mute is True assert isinstance(member, guild_models.Member) - assert member.joined_at is undefined.UNDEFINED def test_deserialize_member_with_undefined_fields(self, entity_factory_impl, user_payload): member = entity_factory_impl.deserialize_member( From ce659a8197b9ffb6af9ea54fe7eec11a11f24529 Mon Sep 17 00:00:00 2001 From: Kyle Tyo Date: Sat, 8 Aug 2020 18:23:02 -0400 Subject: [PATCH 869/922] patched _default_palette() --- hikari/utilities/art.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hikari/utilities/art.py b/hikari/utilities/art.py index 72ef673e50..86bb4a1c76 100644 --- a/hikari/utilities/art.py +++ b/hikari/utilities/art.py @@ -83,7 +83,7 @@ def _default_palette() -> ConsolePalette: # pragma: no cover if _plat != "Pocket PC": if _plat == "win32": - _supports_color |= os.getenv("TERM_PROGRAM", None) == "mintty" + _supports_color |= os.getenv("TERM_PROGRAM", None) in ("mintty", "Terminus") _supports_color |= "ANSICON" in os.environ _supports_color &= _is_a_tty else: From acebba021a52d88f1b84d5582e4db15c25b51bc3 Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Sun, 9 Aug 2020 00:42:20 +0100 Subject: [PATCH 870/922] Fixed stubgen deploy failure. --- pipelines/deploy.nox.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pipelines/deploy.nox.py b/pipelines/deploy.nox.py index e550de2579..ff474b7b94 100644 --- a/pipelines/deploy.nox.py +++ b/pipelines/deploy.nox.py @@ -247,7 +247,7 @@ def stubgen_hack(session: nox.Session) -> None: if f == "__init__.py": module = ".".join(root.split(os.sep)) file = os.path.join(root, f) + "i" - nox.shell("stubgen", "-m", module, "-o", ".") + nox.run("stubgen", "-m", module, "-o", ".") print("Adding license header to stub", file, "for module", module) with open(file) as fp: From 263fdecb8a3efc290ebb6a0fab82d88f11ff70ef Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Sun, 9 Aug 2020 01:29:08 +0100 Subject: [PATCH 871/922] Fixed stubgen deploy issues. --- pipelines/deploy.nox.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pipelines/deploy.nox.py b/pipelines/deploy.nox.py index ff474b7b94..3257aa6e9d 100644 --- a/pipelines/deploy.nox.py +++ b/pipelines/deploy.nox.py @@ -247,7 +247,7 @@ def stubgen_hack(session: nox.Session) -> None: if f == "__init__.py": module = ".".join(root.split(os.sep)) file = os.path.join(root, f) + "i" - nox.run("stubgen", "-m", module, "-o", ".") + session.run("stubgen", "-m", module, "-o", ".") print("Adding license header to stub", file, "for module", module) with open(file) as fp: From b063e5c198a2343c825142765d815dcac208b39d Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Sun, 9 Aug 2020 14:54:41 +0100 Subject: [PATCH 872/922] Added nox session to host a webserver containing documentation for testing purposes. --- pipelines/pdoc.nox.py | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/pipelines/pdoc.nox.py b/pipelines/pdoc.nox.py index f12ea21848..a7389c0abd 100644 --- a/pipelines/pdoc.nox.py +++ b/pipelines/pdoc.nox.py @@ -19,8 +19,15 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. """Pdoc documentation generation.""" +import contextlib +import functools +import http.server +import logging import os import shutil +import socket +import threading +import webbrowser from pipelines import config from pipelines import nox @@ -50,3 +57,33 @@ def pdoc(session: nox.Session) -> None: os.path.join(config.DOCUMENTATION_DIRECTORY, config.LOGO_SOURCE), os.path.join(config.ARTIFACT_DIRECTORY, config.LOGO_SOURCE), ) + + +class HTTPServerThread(threading.Thread): + def __init__(self) -> None: + logging.basicConfig(level="INFO") + + super().__init__(name="HTTP Server", daemon=True) + # Use a socket to obtain a random free port to host the HTTP server on. + with contextlib.closing(socket.socket(socket.AF_INET, socket.SOCK_STREAM)) as sock: + sock.bind(("", 0)) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + self.host, self.port = sock.getsockname() + + handler = functools.partial(http.server.SimpleHTTPRequestHandler, directory=config.ARTIFACT_DIRECTORY) + self.server = http.server.HTTPServer((self.host, self.port), handler) + + def run(self) -> None: + self.server.serve_forever() + + def close(self) -> None: + self.server.shutdown() + + +@nox.session(reuse_venv=True) +def test_pages(_: nox.Session) -> None: + """Start an HTTP server for any generated pages in `/public`.""" + with contextlib.closing(HTTPServerThread()) as thread: + thread.start() + webbrowser.open(f"http://{thread.host}:{thread.port}") + thread.join() From 2de5280a98c782c1d6d697fdd069b3925e32af46 Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Sun, 9 Aug 2020 16:34:39 +0100 Subject: [PATCH 873/922] Fixes #430, fixes #484: Normalize inheritance model for User objects. Removed some unsafe methods from the PartialUser API and normalized inheritance model to allow User to derive from PartialUser tidily. This fixes MyPy incompatibilities elsewhere. --- hikari/impl/event_factory.py | 2 +- hikari/models/users.py | 196 ++++++++++++++---------------- tests/hikari/models/test_users.py | 4 +- 3 files changed, 92 insertions(+), 110 deletions(-) diff --git a/hikari/impl/event_factory.py b/hikari/impl/event_factory.py index 309eb338c5..60c5183684 100644 --- a/hikari/impl/event_factory.py +++ b/hikari/impl/event_factory.py @@ -305,7 +305,7 @@ def deserialize_presence_update_event( if "public_flags" in user_payload: flags = user_models.UserFlag(user_payload["public_flags"]) - user = user_models.PartialUser( + user = user_models.PartialUserImpl( app=self._app, id=snowflake.Snowflake(user_payload["id"]), discriminator=discriminator, diff --git a/hikari/models/users.py b/hikari/models/users.py index a58ef9336b..de82561ce7 100644 --- a/hikari/models/users.py +++ b/hikari/models/users.py @@ -23,7 +23,7 @@ from __future__ import annotations -__all__: typing.Final[typing.List[str]] = ["PartialUser", "User", "UserImpl", "OwnUser", "UserFlag", "PremiumType"] +__all__: typing.Final[typing.List[str]] = ["PartialUser", "User", "OwnUser", "UserFlag", "PremiumType"] import abc import enum @@ -110,7 +110,72 @@ def __str__(self) -> str: @attr.s(eq=True, hash=True, init=True, kw_only=True, slots=True, weakref_slot=False) -class User(snowflake.Unique, abc.ABC): +class PartialUser(snowflake.Unique, abc.ABC): + """A partial interface for a user. + + Fields may or may not be present, and must be checked explicitly if so. + + This is pretty much the same as a normal user, but information may not be + present. + """ + + @property + @abc.abstractmethod + def app(self) -> rest_app.IRESTApp: + """Client application that models may use for procedures.""" + + @property + @abc.abstractmethod + def discriminator(self) -> undefined.UndefinedOr[str]: + """Discriminator for the user.""" + + @property + @abc.abstractmethod + def username(self) -> undefined.UndefinedOr[str]: + """Username for the user.""" + + @property + @abc.abstractmethod + def avatar_hash(self) -> undefined.UndefinedNoneOr[str]: + """Avatar hash for the user, if they have one, otherwise `builtins.None`.""" + + @property + @abc.abstractmethod + def is_bot(self) -> undefined.UndefinedOr[bool]: + """`builtins.True` if this user is a bot account, `builtins.False` otherwise.""" + + @property + @abc.abstractmethod + def is_system(self) -> undefined.UndefinedOr[bool]: + """`builtins.True` if this user is a system account, `builtins.False` otherwise.""" + + @property + @abc.abstractmethod + def flags(self) -> undefined.UndefinedOr[UserFlag]: + """Flag bits that are set for the user.""" + + @property + @abc.abstractmethod + def mention(self) -> str: + """Return a raw mention string for the given user. + + Example + ------- + + ```py + >>> some_user.mention + '<@123456789123456789>' + ``` + + Returns + ------- + builtins.str + The mention string to use. + """ + + +@attr.s(eq=True, hash=True, init=True, kw_only=True, slots=True, weakref_slot=False) +class User(PartialUser, abc.ABC): """Interface for any user-like object. This does not include partial users, as they may not be fully formed. @@ -171,12 +236,11 @@ def mention(self) -> str: """ @property - @abc.abstractmethod def avatar(self) -> files.URL: """Avatar for the user, or the default avatar if not set.""" + return self.format_avatar() or self.default_avatar # noinspection PyShadowingBuiltins - @abc.abstractmethod def format_avatar(self, *, format: typing.Optional[str] = None, size: int = 4096) -> typing.Optional[files.URL]: """Generate the avatar for this user, if set. @@ -208,34 +272,34 @@ def format_avatar(self, *, format: typing.Optional[str] = None, size: int = 4096 ------ builtins.ValueError If `size` is not a power of two or not between 16 and 4096. - builtins.LookupError - If the avatar hash is not known. This will occur if `avatar_hash` - was not provided by Discord, and is - `hikari.utilities.undefined.UNDEFINED`. - This will only ever occur for `PartialUser` objects, regular - `User` objects should never be expected to raise this. """ + if self.avatar_hash is None: + return None + + if format is None: + if self.avatar_hash.startswith("a_"): + # Ignore the fact this shadows `format`, as it is the parameter + # name, which shadows it anyway. + format = "gif" # noqa: A001 shadowing builtin + else: + format = "png" # noqa: A001 shadowing builtin + + return routes.CDN_USER_AVATAR.compile_to_file( + constants.CDN_URL, user_id=self.id, hash=self.avatar_hash, size=size, file_format=format, + ) @property - @abc.abstractmethod def default_avatar(self) -> files.URL: # noqa: D401 imperative mood check - """Placeholder default avatar for the user if no avatar is set. - - Raises - ------ - builtins.LookupError - If the descriminator is not known. This will occur if - `discriminator` was not provided by Discord, and is - `hikari.utilities.undefined.UNDEFINED`. - This will only ever occur for `PartialUser` objects, regular - `User` objects should never be expected to raise this. - """ + """Placeholder default avatar for the user if no avatar is set.""" + return routes.CDN_DEFAULT_USER_AVATAR.compile_to_file( + constants.CDN_URL, discriminator=int(self.discriminator) % 5, file_format="png", + ) @attr_extensions.with_copy @attr.s(eq=True, hash=True, init=True, kw_only=True, slots=True, weakref_slot=False) -class PartialUser(snowflake.Unique): - """Represents partial information about a user. +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. @@ -304,91 +368,9 @@ async def fetch_self(self) -> User: """ return await self.app.rest.fetch_user(user=self.id) - @property - def avatar(self) -> files.URL: - """Avatar for the user, or the default avatar if not set.""" - return self.format_avatar() or self.default_avatar - - # noinspection PyShadowingBuiltins - def format_avatar(self, *, format: typing.Optional[str] = None, size: int = 4096) -> typing.Optional[files.URL]: - """Generate the avatar for this user, if set. - - If no custom avatar is set, this returns `builtins.None`. You can then - use the `default_avatar_url` attribute instead to fetch the displayed - URL. - - Parameters - ---------- - format : builtins.str or builtins.None - The format to use for this URL, defaults to `png` or `gif`. - Supports `png`, `jpeg`, `jpg`, `webp` and `gif` (when - animated). Will be ignored for default avatars which can only be - `png`. - - If `builtins.None`, then the correct default format is determined - based on whether the icon is animated or not. - size : builtins.int - The size to set for the URL, defaults to `4096`. - Can be any power of two between 16 and 4096. - Will be ignored for default avatars. - - Returns - ------- - hikari.utilities.files.URL or builtins.None - The URL to the avatar, or `builtins.None` if not present. - - Raises - ------ - builtins.ValueError - If `size` is not a power of two or not between 16 and 4096. - builtins.LookupError - If the avatar hash is not known. This will occur if `avatar_hash` - was not provided by Discord, and is - `hikari.utilities.undefined.UNDEFINED`. - This will only ever occur for `PartialUser` objects, regular - `User` objects should never be expected to raise this. - """ - if self.avatar_hash is undefined.UNDEFINED: - raise LookupError("Unknown avatar hash for PartialUser") - - if self.avatar_hash is None: - return None - - if format is None: - if self.avatar_hash.startswith("a_"): - # Ignore the fact this shadows `format`, as it is the parameter - # name, which shadows it anyway. - format = "gif" # noqa: A001 shadowing builtin - else: - format = "png" # noqa: A001 shadowing builtin - - return routes.CDN_USER_AVATAR.compile_to_file( - constants.CDN_URL, user_id=self.id, hash=self.avatar_hash, size=size, file_format=format, - ) - - @property - def default_avatar(self) -> files.URL: # noqa: D401 imperative mood check - """Placeholder default avatar for the user if no avatar is set. - - Raises - ------ - builtins.LookupError - If the descriminator is not known. This will occur if - `discriminator` was not provided by Discord, and is - `hikari.utilities.undefined.UNDEFINED`. - This will only ever occur for `PartialUser` objects, regular - `User` objects should never be expected to raise this. - """ - if self.discriminator is undefined.UNDEFINED: - raise LookupError("Unknown discriminator for PartialUser") - - return routes.CDN_DEFAULT_USER_AVATAR.compile_to_file( - constants.CDN_URL, discriminator=int(self.discriminator) % 5, file_format="png", - ) - @attr.s(eq=True, hash=True, init=True, kw_only=True, slots=True, weakref_slot=False) -class UserImpl(PartialUser, User): +class UserImpl(PartialUserImpl, User): """Concrete implementation of user information.""" # These are not attribs on purpose. The idea is to narrow the types of diff --git a/tests/hikari/models/test_users.py b/tests/hikari/models/test_users.py index 7f184f1e97..9088d7c6ea 100644 --- a/tests/hikari/models/test_users.py +++ b/tests/hikari/models/test_users.py @@ -34,5 +34,5 @@ def test_PremiumType_str_operator(): def test_PartialUser_str_operator(): - mock_user = mock.Mock(users.PartialUser, username="thomm.o", discriminator="8637") - assert users.PartialUser.__str__(mock_user) == "thomm.o#8637" + mock_user = mock.Mock(users.PartialUserImpl, username="thomm.o", discriminator="8637") + assert users.PartialUserImpl.__str__(mock_user) == "thomm.o#8637" From c118ab2b189bbcd5f48e9e2e537503a9e7e9dc88 Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Sun, 9 Aug 2020 18:53:22 +0100 Subject: [PATCH 874/922] Added stackless python 3.8.0b3 pipelines. --- .gitlab/ci/bases.yml | 3 +++ .gitlab/ci/installations.yml | 6 ++++++ .gitlab/ci/tests.yml | 6 ++++++ setup.py | 1 + 4 files changed, 16 insertions(+) diff --git a/.gitlab/ci/bases.yml b/.gitlab/ci/bases.yml index 18f77d7c58..b85c642415 100644 --- a/.gitlab/ci/bases.yml +++ b/.gitlab/ci/bases.yml @@ -62,6 +62,9 @@ extends: - .cpython:3.8 +.stackless:3.8-rc: + image: nekokatt/stackless-python-hikari:3.8.0-rc + # Shared windows runner. Does not use docker. # Python will need to be installed manually. .win32: diff --git a/.gitlab/ci/installations.yml b/.gitlab/ci/installations.yml index 02f71f7089..60a4be3a73 100644 --- a/.gitlab/ci/installations.yml +++ b/.gitlab/ci/installations.yml @@ -69,6 +69,11 @@ install:3.9rc: - .cpython:3.9rc - .pip +install:stackless-3.8-rc: + extends: + - .stackless:3.8-rc + - .pip + install:results: extends: - .cpython @@ -82,6 +87,7 @@ install:results: - install:3.8.4 - install:3.8.5 - install:3.9rc + - install:stackless-3.8-rc script: - time eval "pip install -Ur $(echo *requirements.txt | sed 's/ / -Ur /g')" - pip list -o diff --git a/.gitlab/ci/tests.yml b/.gitlab/ci/tests.yml index 40df41e34c..8d05bca538 100644 --- a/.gitlab/ci/tests.yml +++ b/.gitlab/ci/tests.yml @@ -86,6 +86,11 @@ test:3.9rc: - .cpython:3.9rc - .test +test:stackless-3.8-rc: + extends: + - .stackless:3.8-rc + - .test + test:twemoji-mapping: extends: .cpython interruptible: true @@ -116,6 +121,7 @@ test:results: - test:3.8.4 - test:3.8.5 - test:3.9rc + - test:stackless-3.8-rc script: - pip install nox virtualenv - nox -s coalesce-coverage --no-error-on-external-run diff --git a/setup.py b/setup.py index 5a9eba34c7..44313b0c12 100644 --- a/setup.py +++ b/setup.py @@ -90,6 +90,7 @@ def parse_requirements_file(path): "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: Implementation :: CPython", + "Programming Language :: Python :: Implementation :: Stackless", "Topic :: Communications :: Chat", "Topic :: Internet :: WWW/HTTP", "Topic :: Software Development :: Libraries", From ec03540a29c9050e341a550ffa4882b881186e61 Mon Sep 17 00:00:00 2001 From: davfsa Date: Mon, 10 Aug 2020 00:43:43 +0200 Subject: [PATCH 875/922] Some general fixes - Removed unused variables / file references - Update flake8-isort to alpha release --- MANIFEST.in | 1 - dev-requirements.txt | 2 +- flake-requirements.txt | 2 +- pages/index.html | 4 ++-- pipelines/config.py | 3 +-- pipelines/pip.nox.py | 2 +- 6 files changed, 6 insertions(+), 8 deletions(-) diff --git a/MANIFEST.in b/MANIFEST.in index 8e38add164..0101d81936 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,7 +1,6 @@ graft hikari include LICENSE include requirements.txt -include speedup-requirements.txt include setup.py include README.md include CONTRIBUTING.md diff --git a/dev-requirements.txt b/dev-requirements.txt index 49f1b2a40a..4c2b782eb9 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -1,7 +1,7 @@ # Pipelines. nox==2020.5.24 -# Mocks (stdlib ones change between versions of Python, so are not consistent in behaviour like the backport is. +# Mocks (stdlib ones change between versions of Python, so are not consistent in behaviour like the backport is). mock~=4.0.2 # Py.test stuff. diff --git a/flake-requirements.txt b/flake-requirements.txt index 465f1b82d7..31bd9f42fc 100644 --- a/flake-requirements.txt +++ b/flake-requirements.txt @@ -16,7 +16,7 @@ flake8-fixme~=1.1.1 # "fix me" counter flake8-functions~=0.0.4 # function linting flake8-html~=0.4.1 # html output flake8-if-statements~=0.1.0 # condition linting -git+https://github.com/bnavigator/flake8-isort@support-isort5 # runs isort +flake8-isort==4.0.0a0 # runs isort flake8_formatter_junit_xml~=0.0.6 # junit flake8-mutable~=1.2.0 # mutable default argument detection flake8-pep3101~=1.3.0 # new-style format strings only diff --git a/pages/index.html b/pages/index.html index 8affb061b6..b2eb17d14c 100644 --- a/pages/index.html +++ b/pages/index.html @@ -3,7 +3,7 @@ - -1. -2. -3. -4. - -### Expected Result - - -### Actual Result - - -### System info - - -### Further info - - -### Checklist - -- [ ] I have made sure to remove all sensible information (bot token, ). -- [ ] I have searched the issue tracker and have made sure its not a duplicate. If its a follow up of another issue, I have specified it. diff --git a/.gitlab/issue_templates/Feature Request.md b/.gitlab/issue_templates/Feature Request.md deleted file mode 100644 index f22db7d3ef..0000000000 --- a/.gitlab/issue_templates/Feature Request.md +++ /dev/null @@ -1,12 +0,0 @@ -### Summary - - -### Problem - - -### Ideal implementation - - -### Checklist - -- [ ] I have searched the issue tracker and have made sure its not a duplicate. If its a follow up of another issue, I have specified it. diff --git a/.gitlab/issue_templates/Question.md b/.gitlab/issue_templates/Question.md deleted file mode 100644 index 7b77d15eb1..0000000000 --- a/.gitlab/issue_templates/Question.md +++ /dev/null @@ -1 +0,0 @@ -We don't answer questions in the issue tracker, for that please head to our discord server: https://discord.gg/Jx4cNGG diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000000..1eb46e1f54 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,79 @@ +language: python +python: + - 3.8 +os: linux +arch: amd64 + +_test_job: &test_job + install: "${PYTHON_COMMAND} -m pip install nox" + before_script: "mkdir public > /dev/null 2>&1 || true" + script: "${PYTHON_COMMAND} -m nox --sessions pytest twemoji-test" + +_windows_test_job: &windows_test_job + os: windows + language: shell + before_install: "choco install --no-progress python --version=${PYTHON_VERSION} -y && python -V" + env: + - PYTHON_COMMAND="py -3" + <<: *test_job + +_linux_test_job: &linux_test_job + os: linux + env: + - PYTHON_COMMAND=python + <<: *test_job + +jobs: + include: + # Linting + - name: "Linting" + install: "pip install nox" + script: "python -m nox --sessions safety mypy flake8" + + # Windows-specific test jobs. + - name: "Windows 10 Python 3.8.5 AMD64 Tests" + env: PYTHON_VERSION="3.8.5" + arch: amd64 + <<: *windows_test_job + + - name: "Windows 10 Python 3.9 Dev AMD64 Tests" + env: PYTHON_VERSION="3.9.0-rc1 --pre" + arch: amd64 + <<: *windows_test_job + + # Linux-specific test jobs. + - name: "Linux Python 3.8.5 AMD64 Tests" + python: '3.8.5' + arch: amd64 + after_script: nox --sessions coveralls + <<: *linux_test_job + + - name: "Linux Python 3.9 Dev AMD64 Tests" + python: '3.9-dev' + arch: amd64 + <<: *linux_test_job + + - name: "Linux Python 3.8.5 ARM64 Tests" + python: '3.8.5' + arch: arm64 + <<: *linux_test_job + + - name: "Linux Python 3.9 Dev ARM64 Tests" + python: '3.9-dev' + arch: arm64 + <<: *linux_test_job + +deploy: + provider: pypi + user: __token__ + password: + secure: lWzSgAUyZbZMqsOTwTVpLDBqkoN7UpCmVvxyjSVZGse3t9yGxXm+Vw8sWLgYSaOpraknOGN+0+vi5WY3VdXjPo1UgD/47kqRGQLq+QwSl9zm7slpRXNA3XNKDF9xYD48egakEwg3jGHoKVYYZBiL+F8I9K9TDi9MOnHK0nP5j5v5IoBA39PqKEzvoqXuXrgOV3VzpfVYkbwYU/zgcJzWz4b/xumDT0Cu/zP9XR/8T/ZLIhxG4yY+JZVoGPP1BXQZS28OxQzBSXUS3G05NYxz6B2OYRsOOoEqetV4jNajqRLtOdszP/e7vxkVmnkXVckxhjwWqda6C08jWtZB9NbDYoZKc+u+hZfuXFFs+2KHX89de8MUEqivDoM4Bz9mddgWiUfHw1Az9ZvT/CHY1FRSaYneJoq4CCoZeXqo6x5H7DE9BMX52iGJ2Oc2gB7VNyLKMgfEoFUwKER/AlzgfmTuOCoat6PUvoZkIUDA4RwADrJ3TFD4hwM6e4BfrxpoWTGPI3AYgKbHwIhMcqOAtnOhg9Otj7hpbaRpIGNTr/LUKw/eGjwe8iGmB73FmRaS//vyNWKc2Yv29k4ENd3wwKNMVF6qpLkmAliWIwUZQQTNsuhNf0vtfDskhqLBlU/l4x6Vb0XGeMO4n3ddiHhiSgkD37tMO7NBU9chYUDRYhodGxU= + on: + tags: true + distributions: sdist bdist_wheel + repo: nekokatt/hikari + skip_cleanup: 'true' + edge: true # opt in to dpl v2 + +after_deploy: + - bash scripts/post-deploy diff --git a/dev-requirements.txt b/dev-requirements.txt index 4c2b782eb9..47c6be0a20 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -7,9 +7,8 @@ mock~=4.0.2 # Py.test stuff. pytest==6.0.1 pytest-asyncio==0.14.0 -pytest-cov==2.10.0 +pytest-cov==2.10.1 pytest-randomly==3.4.1 -pytest-profiling==1.7.0 pytest-testdox==1.2.1 # Coverage testing. diff --git a/hikari/_about.py b/hikari/_about.py index f6ee8bea32..aa0fad5f44 100644 --- a/hikari/_about.py +++ b/hikari/_about.py @@ -25,16 +25,13 @@ # FROM THE CI SCRIPT AND DOING THIS MAY LEAD TO THE DEPLOY PROCESS FAILING. __author__ = "Nekokatt" -__ci__ = "https://gitlab.com/nekokatt/hikari/pipelines" +__ci__ = "https://travis-ci.org/github/nekokatt/hikari" __copyright__ = "© 2020 Nekokatt" __discord_invite__ = "https://discord.gg/Jx4cNGG" -__docs__ = "https://nekokatt.gitlab.io/hikari" +__docs__ = "https://nekokatt.github.io/hikari" __email__ = "3903853-nekokatt@users.noreply.gitlab.com" -__issue_tracker__ = "https://gitlab.com/nekokatt/hikari/issues" -__is_official_distributed_release__ = False +__issue_tracker__ = "https://github.com/nekokatt/hikari/issues" __license__ = "MIT" -__url__ = "https://gitlab.com/nekokatt/hikari" +__url__ = "https://github.com/nekokatt/hikari" __version__ = "2.0.0.dev0" -__git_branch__ = "development" __git_sha1__ = "HEAD" -__git_when__ = "2020/07/06" diff --git a/hikari/banner.txt b/hikari/banner.txt index cc481779a6..f91a2f209d 100644 --- a/hikari/banner.txt +++ b/hikari/banner.txt @@ -1,5 +1,5 @@ - ${red}oooo o8o oooo o8o ${default}${bright}${white} 光 v${hikari_version} [${hikari_git_branch}/${hikari_git_sha1} - ${hikari_git_when}]${default} + ${red}oooo o8o oooo o8o ${default}${bright}${white} 光 v${hikari_version} [${hikari_git_sha1}]${default} ${bright_red}`888 `"' `888 `"' ${default}${hikari_copyright}, licensed under ${hikari_license}${default} ${yellow} 888 .oo. oooo 888 oooo .oooo. oooo d8b oooo ${default}${dim}Support: ${bright}${cyan}${underline}${hikari_discord_invite}${default} ${bright_green} 888P"Y88b `888 888 .8P' `P )88b `888""8P `888 ${default}${dim}Documentation: ${bright}${cyan}${underline}${hikari_documentation_url}${default} diff --git a/hikari/utilities/art.py b/hikari/utilities/art.py index 726d42a057..f6d365734c 100644 --- a/hikari/utilities/art.py +++ b/hikari/utilities/art.py @@ -161,9 +161,7 @@ def _default_banner_args() -> typing.Mapping[str, str]: { # Hikari stuff. "hikari_version": _about.__version__, - "hikari_git_branch": _about.__git_branch__, "hikari_git_sha1": _about.__git_sha1__, - "hikari_git_when": _about.__git_when__, "hikari_copyright": _about.__copyright__, "hikari_license": _about.__license__, "hikari_install_location": os.path.abspath(os.path.dirname(_about.__file__)), diff --git a/hikari/utilities/version_sniffer.py b/hikari/utilities/version_sniffer.py index 86e6fd65b7..68e221c795 100644 --- a/hikari/utilities/version_sniffer.py +++ b/hikari/utilities/version_sniffer.py @@ -57,13 +57,6 @@ class VersionInfo: latest: distutils_version.LooseVersion = attr.ib() """Latest version. May contain breaking API changes.""" - is_official: bool = attr.ib(default=_about.__is_official_distributed_release__) - """True if this library version is a valid PyPI release. - - This will be False for non-release versions (e.g. cloned from version - control, on forks, or not released using the Hikari CI pipeline). - """ - async def _fetch_all_releases() -> typing.Sequence[distutils_version.LooseVersion]: # Make a client session, it is easier to stub. @@ -123,11 +116,6 @@ async def log_available_updates(logger: logging.Logger) -> None: logger : logging.Logger The logger to write to. """ - if not _about.__is_official_distributed_release__: - # If we are on a non-released version, it could be modified or a - # fork, so don't do any checks. - return - try: version_info = await fetch_version_info_from_pypi() diff --git a/pipelines/config.py b/pipelines/config.py index 68ca95532c..f959b3c4f1 100644 --- a/pipelines/config.py +++ b/pipelines/config.py @@ -22,6 +22,13 @@ IS_CI = "CI" in _os.environ +if "GITLAB_CI" in _os.environ: + CI_PROVIDER = "gitlab" +elif "TRAVIS" in _os.environ: + CI_PROVIDER = "travis" +else: + CI_PROVIDER = "other" + # Packaging MAIN_PACKAGE = "hikari" TEST_PACKAGE = "tests" @@ -49,15 +56,15 @@ # Deployment variables; these only apply to CI stuff specifically. VERSION_FILE = _os.path.join(MAIN_PACKAGE, "_about.py") API_NAME = "hikari" -GIT_SVC_HOST = "gitlab.com" -GIT_TEST_SSH_PATH = "git@gitlab.com" +GIT_SVC_HOST = "github.com" +GIT_TEST_SSH_PATH = "git@github.com" AUTHOR = "Nekokatt" ORIGINAL_REPO_URL = f"https://{GIT_SVC_HOST}/${AUTHOR}/{API_NAME}" SSH_DIR = "~/.ssh" SSH_PRIVATE_KEY_PATH = _os.path.join(SSH_DIR, "id_rsa") SSH_KNOWN_HOSTS = _os.path.join(SSH_DIR, "known_hosts") CI_ROBOT_NAME = AUTHOR -CI_ROBOT_EMAIL = "3903853-nekokatt@users.noreply.gitlab.com" +CI_ROBOT_EMAIL = "nekoka.tt@outlook.com" SKIP_CI_PHRASE = "[skip ci]" SKIP_DEPLOY_PHRASE = "[skip deploy]" SKIP_PAGES_PHRASE = "[skip pages]" diff --git a/pipelines/deploy.nox.py b/pipelines/deploy.nox.py deleted file mode 100644 index 3257aa6e9d..0000000000 --- a/pipelines/deploy.nox.py +++ /dev/null @@ -1,313 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright (c) 2020 Nekokatt -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in all -# copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -# SOFTWARE. -"""Deployment scripts for CI only.""" -import json -import os -import re -import shlex -import subprocess -from distutils.version import LooseVersion - -from pipelines import config -from pipelines import nox - - -def update_version_string(version): - git_sha1 = subprocess.check_output( - ["git", "rev-parse", "HEAD"], universal_newlines=True, stderr=subprocess.DEVNULL, - )[:8] - - git_branch = subprocess.check_output( - ["git", "rev-parse", "--abbrev-ref", "HEAD"], universal_newlines=True, stderr=subprocess.DEVNULL, - )[:-1] - - git_when = subprocess.check_output( - ["git", "log", "-1", '--date=format:"%Y/%m/%d"', '--format="%ad"'], - universal_newlines=True, - stderr=subprocess.DEVNULL, - )[:-1] - - print("Updating version in version file to", version) - nox.shell("sed", shlex.quote(f's|^__version__.*|__version__ = "{version}"|g'), "-i", config.VERSION_FILE) - print("Updating branch in version file to", git_branch) - nox.shell("sed", shlex.quote(f's|^__git_branch__.*|__git_branch__ = "{git_branch}"|g'), "-i", config.VERSION_FILE) - print("Updating sha1 in version file to", git_sha1) - nox.shell("sed", shlex.quote(f's|^__git_sha1__.*|__git_sha1__ = "{git_sha1}"|g'), "-i", config.VERSION_FILE) - print("Updating timestamp in version file to", git_when) - nox.shell("sed", shlex.quote(f's|^__git_when__.*|__git_when__ = "{git_when}"|g'), "-i", config.VERSION_FILE) - - -def set_official_release_flag(value: bool): - print("Marking as", "official" if value else "unofficial", "release") - nox.shell( - "sed", - shlex.quote(f's|^__is_official_distributed_release__.*|__is_official_distributed_release__ = "{value}"|g'), - "-i", - config.VERSION_FILE, - ) - - -def increment_prod_to_next_dev(version): - version_obj = LooseVersion(version) - last_index = len(version_obj.version) - 1 - bits = [*map(str, version_obj.version[:last_index]), f"{version_obj.version[last_index] + 1}.dev"] - next_dev = ".".join(bits) - print(version, "prod version will be incremented to", next_dev) - return next_dev - - -def get_current_version(): - with open(config.VERSION_FILE) as fp: - fp_content = fp.read() - - aboutpy_v = LooseVersion(re.findall(r"^__version__\s*=\s*\"(.*?)\"", fp_content, re.M)[0]) - if not hasattr(aboutpy_v, "vstring"): - print("Corrupt _about.py, using default version 0.0.0") - current = "0.0.0" - else: - current = aboutpy_v.vstring - print("Current version", current) - return current - - -def get_next_prod_version_from_dev(version): - bits = LooseVersion(version).version[:3] - prod = ".".join(map(str, bits)) - print(version, "maps to prod release", prod) - return prod - - -def get_next_dev_version(version): - import requests - - version = LooseVersion(version) - - with requests.get(config.PYPI_API) as resp: - print("Looking at existing versions on", config.PYPI_API) - - if resp.status_code == 404: - print("Package does not seem to yet be deployed, using dummy values.") - return "0.0.1.dev1" - else: - resp.raise_for_status() - root = resp.json() - print("Found existing versions online, so adjusting versions to follow from that where appropriate...") - dev_releases = [LooseVersion(r) for r in root["releases"] if "dev" in r] - same_micro_dev_releases = [r for r in dev_releases if r.version[:3] == version.version[:3]] - latest_matching_staging_v = max(same_micro_dev_releases) if same_micro_dev_releases else version - try: - next_patch = latest_matching_staging_v.version[4] + 1 - except IndexError: - # someone messed the version string up or something, meh, just assume it is fine. - print(latest_matching_staging_v, "doesn't match a patch staging version, so just ignoring it") - next_patch = 1 - print("Using next patch of", next_patch) - bits = [*map(str, latest_matching_staging_v.version[:3]), f"dev{next_patch}"] - return ".".join(bits) - - -def deploy_to_pypi() -> None: - print("Performing PyPI deployment of current code") - nox.shell("pip install -r requirements.txt twine") - nox.shell("python", "setup.py", *config.DISTS) - os.putenv("TWINE_USERNAME", os.environ["PYPI_USER"]) - os.putenv("TWINE_PASSWORD", os.environ["PYPI_PASS"]) - os.putenv("TWINE_REPOSITORY_URL", config.PYPI_REPO) - dists = [os.path.join("dist", n) for n in os.listdir("dist")] - nox.shell("twine", "upload", "--disable-progress-bar", "--skip-existing", *dists) - - -def init_git() -> None: - print("Setting up the git repository ready to make automated changes") - nox.shell("git config user.name", shlex.quote(config.CI_ROBOT_NAME)) - nox.shell("git config user.email", shlex.quote(config.CI_ROBOT_EMAIL)) - nox.shell( - "git remote set-url", - config.REMOTE_NAME, - "$(echo \"$CI_REPOSITORY_URL\" | perl -pe 's#.*@(.+?(\\:\\d+)?)/#git@\\1:#')", - ) - - -def deploy_to_git(next_version: str) -> None: - print("Making deployment commit") - nox.shell( - "git commit -am", shlex.quote(f"(ci) Deployed {next_version} to PyPI {config.SKIP_CI_PHRASE}"), "--allow-empty", - ) - - print("Tagging release") - nox.shell("git tag", next_version) - - print("Merging prod back into preprod") - nox.shell("git checkout", config.PREPROD_BRANCH) - nox.shell(f"git reset --hard {config.REMOTE_NAME}/{config.PREPROD_BRANCH}") - - nox.shell( - f"git merge {config.PROD_BRANCH}", - "--no-ff --strategy-option theirs --allow-unrelated-histories -m", - shlex.quote(f"(ci) Merged {config.PROD_BRANCH} {next_version} into {config.PREPROD_BRANCH}"), - ) - update_version_string(increment_prod_to_next_dev(next_version)) - - print("Making next dev commit on preprod") - nox.shell( - "git commit -am", shlex.quote(f"(ci) Updated version for next development release {config.SKIP_DEPLOY_PHRASE}") - ) - nox.shell("git push --atomic", config.REMOTE_NAME, config.PREPROD_BRANCH, config.PROD_BRANCH, next_version) - - -def rebase_development() -> None: - print("Merging preprod back into dev") - nox.shell("git checkout", config.DEV_BRANCH) - nox.shell(f"git reset --hard {config.REMOTE_NAME}/{config.DEV_BRANCH}") - - nox.shell(f"git rebase {config.PREPROD_BRANCH}") - nox.shell("git push", config.REMOTE_NAME, config.DEV_BRANCH, "-f", "-o", "ci.skip") - - -def send_notification(version: str, title: str, description: str, color: str) -> None: - print("Sending webhook to Discord") - nox.shell( - "curl", - "-X POST", - "-H", - shlex.quote("Content-Type: application/json"), - "-d", - shlex.quote( - json.dumps( - { - "embeds": [ - { - "title": title, - "description": description, - "author": {"name": config.AUTHOR}, - "footer": {"text": f"v{version}"}, - "url": f"{config.PYPI}project/{config.API_NAME}/{version}", - "color": int(color, 16), - } - ] - } - ) - ), - os.environ["RELEASE_WEBHOOK"], - ) - - -@nox.session() -def stubgen_hack(session: nox.Session) -> None: - session.install("-r", "mypy-requirements.txt", "-r", "requirements.txt") - - # MyPy seems to struggle to understand what is exported from `hikari/__init__.py` - # due to disabling implicit exports in strict mode. - # This works around the issue by injecting a helpful stub. Saves risk of error - # and verbosity later by having to hard code 200 classes into an `__all__` - # list manually. - print("Generating stub workaround for __init__.py to allow --strict usage.") - - # Sniff license header from __init__.py - header = [] - with open(os.path.join(config.MAIN_PACKAGE, "__init__.py")) as fp: - while (line := fp.readline()).startswith("#") or not line.strip(): - header.append(line.rstrip()) - - header = "\n".join( - ( - *header, - "\n", - "# This stubfile is generated by Hikari's deploy script as a workaround\n" - "# for our design not working correctly with MyPy's --strict flag. By\n" - "# explicitly generating this stub, MyPy no longer gets confused by what\n" - "# members are re-exported by package level init scripts. This lets you\n" - "# type check as strictly as possible and still get correct results.\n" - "#\n" - "# For all other purposes, you can completely ignore this file!\n", - "\n", - ) - ) - - for root, dirs, files in os.walk(config.MAIN_PACKAGE, topdown=True, followlinks=False): - for f in files: - if f == "__init__.py": - module = ".".join(root.split(os.sep)) - file = os.path.join(root, f) + "i" - session.run("stubgen", "-m", module, "-o", ".") - - print("Adding license header to stub", file, "for module", module) - with open(file) as fp: - stub = fp.read() - - with open(file, "w") as fp: - fp.write(header) - fp.write(stub) - - -@nox.session() -def deploy(session: nox.Session) -> None: - """Perform a deployment. This will only work on the CI.""" - print("Injecting stubgen hack to allow for --strict in MyPy for users") - nox.registry["stubgen-hack"](session) - - print("Re-running code formatting fixes") - nox.registry["reformat-code"](session) - - nox.shell("pip install requests") - - commit_ref = os.getenv("CI_COMMIT_REF_NAME", *session.posargs[0:1]) - print("Commit ref is", commit_ref) - current_version = get_current_version() - - init_git() - if commit_ref == config.PREPROD_BRANCH: - print("preprod release!") - next_version = get_next_dev_version(current_version) - update_version_string(next_version) - set_official_release_flag(True) - deploy_to_pypi() - set_official_release_flag(False) - send_notification( - next_version, - f"{config.API_NAME} v{next_version} has been released", - "Pick up the latest development release from pypi by running:\n" - "```bash\n" - f"pip install -U {config.API_NAME}=={next_version}\n" - "```", - "2C2F33", - ) - rebase_development() - elif commit_ref == config.PROD_BRANCH: - print("prod release!") - next_version = get_next_prod_version_from_dev(current_version) - update_version_string(next_version) - set_official_release_flag(True) - deploy_to_pypi() - set_official_release_flag(False) - send_notification( - next_version, - f"{config.API_NAME} v{next_version} has been released", - "Pick up the latest stable release from pypi by running:\n" - "```bash\n" - f"pip install -U {config.API_NAME}=={next_version}\n" - "```", - "7289DA", - ) - deploy_to_git(next_version) - rebase_development() - else: - print("not preprod or prod branch, nothing will be performed.") diff --git a/pipelines/flake8.nox.py b/pipelines/flake8.nox.py index 9326b2bcf5..2959935d3c 100644 --- a/pipelines/flake8.nox.py +++ b/pipelines/flake8.nox.py @@ -29,30 +29,8 @@ def flake8(session: nox.Session) -> None: """Run code linting, SAST, and analysis.""" session.install("-r", "requirements.txt", "-r", "flake-requirements.txt") - - if "GITLAB_CI" in os.environ or "--gitlab" in session.posargs: - print("Generating HTML report") - - shutil.rmtree(config.FLAKE8_TXT, ignore_errors=True) - - session.run( - "flake8", "--exit-zero", "--format=html", f"--htmldir={config.FLAKE8_HTML}", config.MAIN_PACKAGE, - ) - - shutil.rmtree(config.FLAKE8_TXT, ignore_errors=True) - - print("Detected GitLab, will output CodeClimate report next!") - # If we add the args for --statistics or --show-source, the thing breaks - # silently, and I cant find another decent package that actually works - # in any of the gitlab-supported formats :( - session.run( - "flake8", "--exit-zero", "--format=junit-xml", f"--output-file={config.FLAKE8_JUNIT}", config.MAIN_PACKAGE, - ) - print("Generating console output") - shutil.rmtree(config.FLAKE8_TXT, ignore_errors=True) - session.run( "flake8", f"--output-file={config.FLAKE8_TXT}", "--statistics", "--show-source", "--tee", config.MAIN_PACKAGE, ) diff --git a/pipelines/format.nox.py b/pipelines/format.nox.py index 3c3dea5beb..4a2a858bba 100644 --- a/pipelines/format.nox.py +++ b/pipelines/format.nox.py @@ -71,6 +71,7 @@ ".ps1", ".rb", ".pl", + ".travis.yml" ) LINE_ENDING_PATHS = { @@ -80,6 +81,7 @@ "docs", "insomnia", ".gitlab", + } diff --git a/pipelines/nox.py b/pipelines/nox.py index c66074a24c..7709d265da 100644 --- a/pipelines/nox.py +++ b/pipelines/nox.py @@ -26,7 +26,6 @@ from nox import options as _options from nox import session as _session -from nox.registry import _REGISTRY as registry from nox.sessions import Session from pipelines import config diff --git a/pipelines/pages.nox.py b/pipelines/pages.nox.py index ed88d0633e..4660ceddbe 100644 --- a/pipelines/pages.nox.py +++ b/pipelines/pages.nox.py @@ -38,12 +38,8 @@ def copy_from_in(src: str, dest: str) -> None: @nox.session(reuse_venv=True) -def pages(session: nox.Session) -> None: +def pages(_: nox.Session) -> None: """Generate static pages containing resources and tutorials.""" - for n, v in os.environ.items(): - if n.startswith(("GITLAB_", "CI")) or n == "CI": - session.env[n] = v - if not os.path.exists(config.ARTIFACT_DIRECTORY): os.mkdir(config.ARTIFACT_DIRECTORY) copy_from_in(config.PAGES_DIRECTORY, config.ARTIFACT_DIRECTORY) diff --git a/pipelines/pip.nox.py b/pipelines/pip.nox.py deleted file mode 100644 index fb16bc7393..0000000000 --- a/pipelines/pip.nox.py +++ /dev/null @@ -1,148 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright (c) 2020 Nekokatt -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in all -# copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -# SOFTWARE. -"""Installation tests.""" -import contextlib -import os -import shutil -import subprocess -import tarfile -import tempfile - -from pipelines import config -from pipelines import nox - - -@contextlib.contextmanager -def temp_chdir(session: nox.Session, target: str): - cwd = os.path.abspath(os.getcwd()) - print("Changing directory from", cwd, "to", target) - session.chdir(target) - yield cwd - print("Changing directory from", target, "to", cwd) - session.chdir(cwd) - - -def predicate(): - commit_ref = os.getenv("CI_COMMIT_REF_NAME") - return commit_ref in (config.PROD_BRANCH, config.PREPROD_BRANCH) and config.IS_CI - - -@nox.session(reuse_venv=False, only_if=predicate) -def pip(session: nox.Session): - """Run through sandboxed install of PyPI package.""" - if "--showtime" in session.posargs: - print("Testing we can install packaged pypi object") - session.install(config.MAIN_PACKAGE) - session.run("python", "-m", config.MAIN_PACKAGE) - # Prevent nox caching old versions and using those when tests run. - session.run("pip", "uninstall", "-vvv", "-y", config.MAIN_PACKAGE) - - else: - try: - print("Testing published ref can be installed as a package.") - url = session.env.get("CI_PROJECT_URL") - sha1 = session.env.get("CI_COMMIT_SHA", "master") - slug = f"git+{url}.git@{sha1}" - session.install(slug) - session.run("python", "-m", config.MAIN_PACKAGE) - # Prevent nox caching old versions and using those when tests run. - session.run("pip", "uninstall", "-vvv", "-y", config.MAIN_PACKAGE) - except Exception: - print("Failed to install from GitLab.") - raise KeyError from None - - -@nox.session(reuse_venv=False) -def pip_git(session: nox.Session): - """Test installing repository from Git repository directly via pip.""" - print("Testing installing from git repository only") - - try: - branch = os.environ["CI_COMMIT_SHA"] - except KeyError: - branch = subprocess.check_output(["git", "symbolic-ref", "--short", "HEAD"]).decode("utf8")[0:-1] - - print("Testing for branch", branch) - - with tempfile.TemporaryDirectory() as temp_dir: - with temp_chdir(session, temp_dir) as project_dir: - session.install(f"git+file://{project_dir}") - session.install(config.MAIN_PACKAGE) - session.run("python", "-m", config.MAIN_PACKAGE) - - print("Installed as git dir in temporary environment successfully!") - - -@nox.session(reuse_venv=False) -def pip_sdist(session: nox.Session): - """Test installing as an sdist package.""" - session.install("wheel") - session.run("python", "setup.py", "build", "sdist") - - print("Testing installing from wheel") - with tempfile.TemporaryDirectory() as temp_dir: - with temp_chdir(session, temp_dir) as project_dir: - dist = os.path.join(project_dir, "dist") - wheels = [os.path.join(dist, wheel) for wheel in os.listdir(dist) if wheel.endswith(".tar.gz")] - wheels.sort(key=lambda wheel: os.stat(wheel).st_ctime) - newest_tarball = wheels.pop() - newest_tarball_name = os.path.basename(newest_tarball) - - if newest_tarball_name.lower().endswith(".tar.gz"): - newest_tarball_dir = newest_tarball_name[: -len(".tar.gz")] - else: - newest_tarball_dir = newest_tarball_name[: -len(".tgz")] - - print(f"copying newest tarball found at {newest_tarball} and installing it in temp dir") - shutil.copyfile(newest_tarball, newest_tarball_name) - - print("extracting tarball") - with tarfile.open(newest_tarball_name) as tar: - tar.extractall() - - print("installing sdist") - with temp_chdir(session, newest_tarball_dir): - session.run("python", "setup.py", "install") - session.run("python", "-m", config.MAIN_PACKAGE) - - print("Installed as wheel in temporary environment successfully!") - - -@nox.session(reuse_venv=False) -def pip_bdist_wheel(session: nox.Session): - """Test installing as a platform independent bdist_wheel package.""" - session.install("wheel") - session.run("python", "setup.py", "build", "bdist_wheel") - - print("Testing installing from wheel") - with tempfile.TemporaryDirectory() as temp_dir: - with temp_chdir(session, temp_dir) as project_dir: - dist = os.path.join(project_dir, "dist") - wheels = [os.path.join(dist, wheel) for wheel in os.listdir(dist) if wheel.endswith(".whl")] - wheels.sort(key=lambda wheel: os.stat(wheel).st_ctime) - newest_wheel = wheels.pop() - newest_wheel_name = os.path.basename(newest_wheel) - print(f"copying newest wheel found at {newest_wheel} and installing it in temp dir") - shutil.copyfile(newest_wheel, newest_wheel_name) - session.run("pip", "install", newest_wheel_name) - session.run("python", "-m", config.MAIN_PACKAGE) - - print("Installed as wheel in temporary environment successfully!") diff --git a/pipelines/pytest.nox.py b/pipelines/pytest.nox.py index 8b1f34131e..88d44de517 100644 --- a/pipelines/pytest.nox.py +++ b/pipelines/pytest.nox.py @@ -19,8 +19,6 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. """Py.test integration.""" -import contextlib -import os import shutil from pipelines import config @@ -56,92 +54,9 @@ def pytest(session: nox.Session) -> None: session.run("python", "-m", "pytest", *FLAGS, *session.posargs, config.TEST_PACKAGE) -@nox.session(reuse_venv=True) -def pytest_profile(session: nox.Session) -> None: - """Run pytest with a profiler enabled to debug test times.""" - session.posargs.append("--profile") - session.posargs.append("--durations=0") - pytest(session) - print("Generating profiling reports in `prof' directory.") - - import pstats - - with open("prof/results-by-tottime.txt", "w") as fp: - stats = pstats.Stats("prof/combined.prof", stream=fp) - stats.sort_stats("tottime") - stats.print_stats() - - with open("prof/results-by-ncalls.txt", "w") as fp: - stats = pstats.Stats("prof/combined.prof", stream=fp) - stats.sort_stats("calls") - stats.print_stats() - - -@nox.session(reuse_venv=True) -def mutation_test(session: nox.Session) -> None: - """Run mutation tests on a given module and test suite. - - This randomly mutates the module undergoing testing to make it invalid - by altering parts of the code. It will then attempt to run the tests to - verify that they now fail. - """ - session.install( - "-r", "requirements.txt", "-r", "dev-requirements.txt", "git+https://github.com/sixty-north/cosmic-ray" - ) - - with contextlib.suppress(Exception): - os.mkdir(config.ARTIFACT_DIRECTORY) - - toml = os.path.join(config.ARTIFACT_DIRECTORY, "mutation.toml") - - path = input("What module do you wish to mutate? (e.g hikari/utilities) ") - package = input("What module do you wish to test? (e.g tests/hikari/utilities) ") - - print("Creating config file") - - with open(toml, "w") as fp: - cfg = ( - "[cosmic-ray]\n" - f"module-path = {path!r}\n" - "python-version = ''\n" - "timeout = 10\n" - "exclude-modules = []\n" - f"test-command = 'pytest -x {package}'" - "\n" - "[cosmic-ray.execution-engine]\n" - "name = 'local'\n" - "\n" - "[cosmic-ray.cloning]\n" - "method = 'copy'\n" - "commands = []\n" - ) - - print(cfg) - - fp.write(cfg) - - sqlite = os.path.join(config.ARTIFACT_DIRECTORY, "mutation.db") - - print("Initializing session and generating test cases") - session.run("cosmic-ray", "init", toml, sqlite) - - print("Performing mutation tests") - session.run("cosmic-ray", "exec", sqlite) - - print("Generating reports") - session.run("cr-report", sqlite) - - -@nox.session(reuse_venv=True) -def coalesce_coverage(session: nox.Session) -> None: - """Combine coverage stats from several CI jobs.""" - session.install("coverage") - - coverage_files = [] - for file in os.listdir(config.ARTIFACT_DIRECTORY): - if file.endswith(".coverage"): - coverage_files.append(os.path.join(config.ARTIFACT_DIRECTORY, file)) - print("files for coverage:", coverage_files) - - session.run("coverage", "combine", f"--rcfile={config.COVERAGE_INI}", *coverage_files) - session.run("coverage", "report", "-i", "-m", f"--rcfile={config.COVERAGE_INI}") +@nox.inherit_environment_vars +@nox.session(reuse_venv=False) +def coveralls(session: nox.Session) -> None: + """Run coveralls. This has little effect outside TravisCI.""" + session.install("python-coveralls") + session.run("coveralls") diff --git a/scripts/post-deploy.sh b/scripts/post-deploy.sh new file mode 100755 index 0000000000..43a3a8271d --- /dev/null +++ b/scripts/post-deploy.sh @@ -0,0 +1,33 @@ +#!/bin/sh +VERSION=${TRAVIS_TAG} +REF=${TRAVIS_COMMIT} + +#git config user.name "Nekokatt" +#git config user.email "69713762+nekokatt@users.noreply.github.com" +# +#git remote set-url origin https://nekokatt:${GITHUB_TOKEN}github.com/${TRAVIS_REPO_SLUG} +#git fetch origin master +#git checkout master +# +#echo "Bumping repository version to ${VERSION} (ref: ${REF})" +#sed "s|^__version__.*|__version__ = \"${VERSION}\"|g" -i hikari/_about.py +#sed "s|^__git_sha1__.*|__git_sha1__ = \"${REF}\"|g" -i hikari/_about.py +#echo "==========================================================================" +#cat hikari/_about.py +#echo "==========================================================================" +# +#git add hikari/_about.py +#git commit hikari/_about.py -m "Updated config version from new release ${VERSION} @ ${REF} [skip ci]" +#git push +## Clear from git-config +#git remote set-url origin https://github.com/${TRAVIS_SLUG} + +pip install nox +mkdir public || true +nox --sessions pdoc pages +git branch -D gh-pages || true +git subtree split --prefix public HEAD --branch gh-pages +echo "Deploying pages" +git add -A -n . +echo git commit -am "Deployed pages with ${TRAVIS_COMMIT} [skip ci]" +echo push -f From 1d58550b6e403694a3d64ed9585a122e8c3d57ad Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Sat, 15 Aug 2020 19:12:52 +0100 Subject: [PATCH 918/922] Added post-deploy script. --- .travis.yml | 31 +++++------ hikari/cli.py | 4 +- scripts/post-deploy.sh | 51 ++++++++++--------- .../hikari/utilities/test_version_sniffer.py | 13 ----- 4 files changed, 43 insertions(+), 56 deletions(-) diff --git a/.travis.yml b/.travis.yml index 1eb46e1f54..23dd22f7be 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,6 @@ language: python python: - - 3.8 + - "3.8" os: linux arch: amd64 @@ -63,17 +63,18 @@ jobs: arch: arm64 <<: *linux_test_job -deploy: - provider: pypi - user: __token__ - password: - secure: lWzSgAUyZbZMqsOTwTVpLDBqkoN7UpCmVvxyjSVZGse3t9yGxXm+Vw8sWLgYSaOpraknOGN+0+vi5WY3VdXjPo1UgD/47kqRGQLq+QwSl9zm7slpRXNA3XNKDF9xYD48egakEwg3jGHoKVYYZBiL+F8I9K9TDi9MOnHK0nP5j5v5IoBA39PqKEzvoqXuXrgOV3VzpfVYkbwYU/zgcJzWz4b/xumDT0Cu/zP9XR/8T/ZLIhxG4yY+JZVoGPP1BXQZS28OxQzBSXUS3G05NYxz6B2OYRsOOoEqetV4jNajqRLtOdszP/e7vxkVmnkXVckxhjwWqda6C08jWtZB9NbDYoZKc+u+hZfuXFFs+2KHX89de8MUEqivDoM4Bz9mddgWiUfHw1Az9ZvT/CHY1FRSaYneJoq4CCoZeXqo6x5H7DE9BMX52iGJ2Oc2gB7VNyLKMgfEoFUwKER/AlzgfmTuOCoat6PUvoZkIUDA4RwADrJ3TFD4hwM6e4BfrxpoWTGPI3AYgKbHwIhMcqOAtnOhg9Otj7hpbaRpIGNTr/LUKw/eGjwe8iGmB73FmRaS//vyNWKc2Yv29k4ENd3wwKNMVF6qpLkmAliWIwUZQQTNsuhNf0vtfDskhqLBlU/l4x6Vb0XGeMO4n3ddiHhiSgkD37tMO7NBU9chYUDRYhodGxU= - on: - tags: true - distributions: sdist bdist_wheel - repo: nekokatt/hikari - skip_cleanup: 'true' - edge: true # opt in to dpl v2 - -after_deploy: - - bash scripts/post-deploy + - name: "Deploy new release" + if: tag IS present AND tag =~ /^\d+\.\d+\.\d+(\..*)?$/ + script: echo "Will deploy ${TRAVIS_TAG}" + deploy: + provider: pypi + user: __token__ + password: + secure: lWzSgAUyZbZMqsOTwTVpLDBqkoN7UpCmVvxyjSVZGse3t9yGxXm+Vw8sWLgYSaOpraknOGN+0+vi5WY3VdXjPo1UgD/47kqRGQLq+QwSl9zm7slpRXNA3XNKDF9xYD48egakEwg3jGHoKVYYZBiL+F8I9K9TDi9MOnHK0nP5j5v5IoBA39PqKEzvoqXuXrgOV3VzpfVYkbwYU/zgcJzWz4b/xumDT0Cu/zP9XR/8T/ZLIhxG4yY+JZVoGPP1BXQZS28OxQzBSXUS3G05NYxz6B2OYRsOOoEqetV4jNajqRLtOdszP/e7vxkVmnkXVckxhjwWqda6C08jWtZB9NbDYoZKc+u+hZfuXFFs+2KHX89de8MUEqivDoM4Bz9mddgWiUfHw1Az9ZvT/CHY1FRSaYneJoq4CCoZeXqo6x5H7DE9BMX52iGJ2Oc2gB7VNyLKMgfEoFUwKER/AlzgfmTuOCoat6PUvoZkIUDA4RwADrJ3TFD4hwM6e4BfrxpoWTGPI3AYgKbHwIhMcqOAtnOhg9Otj7hpbaRpIGNTr/LUKw/eGjwe8iGmB73FmRaS//vyNWKc2Yv29k4ENd3wwKNMVF6qpLkmAliWIwUZQQTNsuhNf0vtfDskhqLBlU/l4x6Vb0XGeMO4n3ddiHhiSgkD37tMO7NBU9chYUDRYhodGxU= + on: + tags: true + distributions: "sdist bdist_wheel" + repo: nekokatt/hikari + cleanup: 'true' + after_deploy: + - bash scripts/post-deploy diff --git a/hikari/cli.py b/hikari/cli.py index a798570ba8..82dc9d0538 100644 --- a/hikari/cli.py +++ b/hikari/cli.py @@ -40,13 +40,11 @@ def main() -> None: else: sourcefile = typing.cast(str, inspect.getsourcefile(_about)) path: typing.Final[str] = os.path.abspath(os.path.dirname(sourcefile)) - branch: typing.Final[str] = _about.__git_branch__ sha1: typing.Final[str] = _about.__git_sha1__ - date: typing.Final[str] = _about.__git_when__ version: typing.Final[str] = _about.__version__ py_impl: typing.Final[str] = platform.python_implementation() py_ver: typing.Final[str] = platform.python_version() py_compiler: typing.Final[str] = platform.python_compiler() - sys.stderr.write(f"hikari v{version} {branch}@{sha1}, released on {date}\n") + sys.stderr.write(f"hikari v{version} {sha1}\n") sys.stderr.write(f"located at {path}\n") sys.stderr.write(f"{py_impl} {py_ver} {py_compiler}\n") diff --git a/scripts/post-deploy.sh b/scripts/post-deploy.sh index 43a3a8271d..61083471ac 100755 --- a/scripts/post-deploy.sh +++ b/scripts/post-deploy.sh @@ -2,32 +2,33 @@ VERSION=${TRAVIS_TAG} REF=${TRAVIS_COMMIT} -#git config user.name "Nekokatt" -#git config user.email "69713762+nekokatt@users.noreply.github.com" -# -#git remote set-url origin https://nekokatt:${GITHUB_TOKEN}github.com/${TRAVIS_REPO_SLUG} -#git fetch origin master -#git checkout master -# -#echo "Bumping repository version to ${VERSION} (ref: ${REF})" -#sed "s|^__version__.*|__version__ = \"${VERSION}\"|g" -i hikari/_about.py -#sed "s|^__git_sha1__.*|__git_sha1__ = \"${REF}\"|g" -i hikari/_about.py -#echo "==========================================================================" -#cat hikari/_about.py -#echo "==========================================================================" -# -#git add hikari/_about.py -#git commit hikari/_about.py -m "Updated config version from new release ${VERSION} @ ${REF} [skip ci]" -#git push -## Clear from git-config -#git remote set-url origin https://github.com/${TRAVIS_SLUG} +echo "===== UPDATING VERSIONS =====" +git config user.name "Nekokatt" +git config user.email "69713762+nekokatt@users.noreply.github.com" +git remote set-url origin https://nekokatt:${GITHUB_TOKEN}github.com/${TRAVIS_REPO_SLUG} +git fetch origin master +git checkout master + +echo "Bumping repository version to ${VERSION} (ref: ${REF})" +sed "s|^__version__.*|__version__ = \"${VERSION}\"|g" -i hikari/_about.py +sed "s|^__git_sha1__.*|__git_sha1__ = \"${REF}\"|g" -i hikari/_about.py +echo "==========================================================================" +cat hikari/_about.py +echo "==========================================================================" + +git add hikari/_about.py +git commit hikari/_about.py -m "Updated config version from new release ${VERSION} @ ${REF} [skip ci]" +git push +# Clear from git-config +git remote set-url origin https://github.com/${TRAVIS_SLUG} + +echo "===== DEPLOYING PAGES =====" pip install nox mkdir public || true nox --sessions pdoc pages -git branch -D gh-pages || true -git subtree split --prefix public HEAD --branch gh-pages -echo "Deploying pages" -git add -A -n . -echo git commit -am "Deployed pages with ${TRAVIS_COMMIT} [skip ci]" -echo push -f +cd public +git init +git remote add origin https://nekokatt:${GITHUB_TOKEN}github.com/${TRAVIS_REPO_SLUG} +git commit -am "Deployed documentation [skip ci]" +git push origin gh-pages --force diff --git a/tests/hikari/utilities/test_version_sniffer.py b/tests/hikari/utilities/test_version_sniffer.py index 9fc90a82c7..acdfbedece 100644 --- a/tests/hikari/utilities/test_version_sniffer.py +++ b/tests/hikari/utilities/test_version_sniffer.py @@ -276,20 +276,10 @@ async def test_no_versions_given(self, this_version): @pytest.mark.asyncio class TestLogAvailableUpdates: - async def test_when_not_official_release(self): - logger = mock.Mock() - with mock.patch.object(_about, "__is_official_distributed_release__", new=False): - await version_sniffer.log_available_updates(mock.Mock()) - - logger.debug.assert_not_called() - logger.info.assert_not_called() - logger.warning.assert_not_called() - async def test_when_exception(self): logger = mock.Mock() exception = RuntimeError() stack = contextlib.ExitStack() - stack.enter_context(mock.patch.object(_about, "__is_official_distributed_release__", new=True)) stack.enter_context(mock.patch.object(version_sniffer, "fetch_version_info_from_pypi", side_effect=exception)) with stack: @@ -306,7 +296,6 @@ class StubVersionInfo: logger = mock.Mock() stack = contextlib.ExitStack() - stack.enter_context(mock.patch.object(_about, "__is_official_distributed_release__", new=True)) stack.enter_context( mock.patch.object(version_sniffer, "fetch_version_info_from_pypi", return_value=StubVersionInfo()) ) @@ -327,7 +316,6 @@ class StubVersionInfo: logger = mock.Mock() version = StubVersionInfo() stack = contextlib.ExitStack() - stack.enter_context(mock.patch.object(_about, "__is_official_distributed_release__", new=True)) stack.enter_context(mock.patch.object(version_sniffer, "fetch_version_info_from_pypi", return_value=version)) with stack: @@ -350,7 +338,6 @@ class StubVersionInfo: logger = mock.Mock() version = StubVersionInfo() stack = contextlib.ExitStack() - stack.enter_context(mock.patch.object(_about, "__is_official_distributed_release__", new=True)) stack.enter_context(mock.patch.object(version_sniffer, "fetch_version_info_from_pypi", return_value=version)) with stack: From 773a2814c8ca86df7cc63334c4ee0f07373dfd9c Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Sat, 15 Aug 2020 20:15:36 +0100 Subject: [PATCH 919/922] Travis webhooks and post deploy. --- .travis.yml | 33 +++++++++++++++++++++++++++++++-- scripts/build_webhook.py | 33 +++++++++++++++++++++++++++++++++ scripts/deploy_webhook.py | 22 ++++++++++++++++++++++ scripts/post-deploy.sh | 4 ++++ 4 files changed, 90 insertions(+), 2 deletions(-) create mode 100644 scripts/build_webhook.py create mode 100644 scripts/deploy_webhook.py diff --git a/.travis.yml b/.travis.yml index 23dd22f7be..7e789855b3 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,3 +1,8 @@ +stages: + - test + - deploy + - webhooks + language: python python: - "3.8" @@ -28,53 +33,77 @@ jobs: # Linting - name: "Linting" install: "pip install nox" + stage: test script: "python -m nox --sessions safety mypy flake8" # Windows-specific test jobs. - name: "Windows 10 Python 3.8.5 AMD64 Tests" env: PYTHON_VERSION="3.8.5" arch: amd64 + stage: test <<: *windows_test_job - name: "Windows 10 Python 3.9 Dev AMD64 Tests" env: PYTHON_VERSION="3.9.0-rc1 --pre" arch: amd64 + stage: test <<: *windows_test_job # Linux-specific test jobs. - name: "Linux Python 3.8.5 AMD64 Tests" python: '3.8.5' arch: amd64 + stage: test after_script: nox --sessions coveralls <<: *linux_test_job - name: "Linux Python 3.9 Dev AMD64 Tests" python: '3.9-dev' arch: amd64 + stage: test <<: *linux_test_job - name: "Linux Python 3.8.5 ARM64 Tests" python: '3.8.5' arch: arm64 + stage: test <<: *linux_test_job - name: "Linux Python 3.9 Dev ARM64 Tests" python: '3.9-dev' arch: arm64 + stage: test <<: *linux_test_job - name: "Deploy new release" if: tag IS present AND tag =~ /^\d+\.\d+\.\d+(\..*)?$/ script: echo "Will deploy ${TRAVIS_TAG}" + stage: deploy + language: python + python: 3.8 + arch: amd64 + os: linux + env: deploy: provider: pypi user: __token__ password: - secure: lWzSgAUyZbZMqsOTwTVpLDBqkoN7UpCmVvxyjSVZGse3t9yGxXm+Vw8sWLgYSaOpraknOGN+0+vi5WY3VdXjPo1UgD/47kqRGQLq+QwSl9zm7slpRXNA3XNKDF9xYD48egakEwg3jGHoKVYYZBiL+F8I9K9TDi9MOnHK0nP5j5v5IoBA39PqKEzvoqXuXrgOV3VzpfVYkbwYU/zgcJzWz4b/xumDT0Cu/zP9XR/8T/ZLIhxG4yY+JZVoGPP1BXQZS28OxQzBSXUS3G05NYxz6B2OYRsOOoEqetV4jNajqRLtOdszP/e7vxkVmnkXVckxhjwWqda6C08jWtZB9NbDYoZKc+u+hZfuXFFs+2KHX89de8MUEqivDoM4Bz9mddgWiUfHw1Az9ZvT/CHY1FRSaYneJoq4CCoZeXqo6x5H7DE9BMX52iGJ2Oc2gB7VNyLKMgfEoFUwKER/AlzgfmTuOCoat6PUvoZkIUDA4RwADrJ3TFD4hwM6e4BfrxpoWTGPI3AYgKbHwIhMcqOAtnOhg9Otj7hpbaRpIGNTr/LUKw/eGjwe8iGmB73FmRaS//vyNWKc2Yv29k4ENd3wwKNMVF6qpLkmAliWIwUZQQTNsuhNf0vtfDskhqLBlU/l4x6Vb0XGeMO4n3ddiHhiSgkD37tMO7NBU9chYUDRYhodGxU= + secure: "hRjbVk8VicGkEbv/AmEJMQmWNmkC7amaPpGdaJtDC/RrfvxDXMrPFZChwi8QN42Jy28sFN0om3Fw3AQfp85ddH2itcMFeJerKOpQ2QnjOECLlgL0+6xhZqV50dj8Pd6U6BLrkSu+PXETxloSUv421ojT4dq7EgVLbQBgqs8fT5tVHB2qNIxZoOjrGFZ7Lwj5gRojnrGXf6oZt7wh/2TCnTM5GY+Zc0uAj04gwN9zZnbqWii0EcY9jRQvoE3gf0F1bTlDWjbOrDnGA+2DfZEYK9YjCztXgHwvyOBdaGn56sh9mEXH1e+OxtuJvbMQ+RDGsMW94UtiLdXJUVPyOQLFB129FscGpX5LKatlnJI1HKTi2SBEL+IxAU0NhiuzJVYSYlIAmBcgBxKuk+dxdv2QYRpFhVTKomdp1U4BZgeXFVa7XjBthXm2ng3Yrpc7lrEEmle15X8/C/yv9xeaYrnPz2SCCA3J4MkRBcg+W/+L8l68g8MKyZGamImu5ZQsso4mZ4cnbJCCAhqvtMz2ydEzhwEFx2yeowyW9PhFkF79BQhTx9ZG3I9djbgDXAu3fwXyYIloKw2DGZ8LupsQBtamHtQxq7hjdfP2fUr2tFdXR0SHzFbHAaOh2y+8wdgwW+pGT3rVRE9LbhIFtK5cDheyo+/XIhpngwKpYrSk1J2Mf0k=" on: tags: true distributions: "sdist bdist_wheel" repo: nekokatt/hikari cleanup: 'true' after_deploy: - - bash scripts/post-deploy + - bash scripts/post-deploy.sh + + - name: Build Webhook + stage: webhooks + language: python + python: 3.8 + arch: amd64 + os: linux + env: + secure: "TDaw6Ryljomk6NwuoHCtuV4PGlvBFM/GjEmhXGFTXOn32ENrh14oXWaWIw6ZmKLtekccs2kQpnN+8l5kuzqr9aHvalJzwm05RYfu3wCw09rOeXXWX0GacnCrIh2yBfYXWq/pIS0CSPO+7IRnwwLaVSqUHGXfQV5mEHNbuJTU3tAlgOqrN/L4ZWdlPiclwvKStV4VGRSqxNGEj1G10VEq0+dn6KjrhkQMpDuhQ/3o+8F0vjMNb8rQeKwogeHP3tmwEiCUoxwekjzZU9Oevc75d836/ys2hvsStF8mMtQsd4QgTMYwSWQtKHmKyqgW21nXw8TE1HcnJnc3ahfueOFMbhplJ32h7svTPuPFTaVl99+z2TG8OBbFTQEkQakc5HO7lRH0AGLmhlyXkhYIGxWCS1h266VEzVkcNAliLR/auSFKQ4XKISPZQo6ee0Cx+FkRSKm2OgLw+Nzax8AQQd89C0+hibFq3kdOx0nORpWApjz1vIyCTTEl60UtWf/wkW5o/wTiPFwoHk3k6GxHZDkXCsUb8AjxpjIt9qxo53Yu7bv3msrGLmBScxdLeDs6qdCXXWJWI1D17nhNCclXgzLrZKf5n1zBqOAQjyeoJEOTnVUCQvIHSZYErVv8s0gCArv9ONtaIfq1Vmz+E234/K+IWOYDayvcDjFna7ySgK60W9I=" + install: pip install requests + script: python scripts/build_webhook.py diff --git a/scripts/build_webhook.py b/scripts/build_webhook.py new file mode 100644 index 0000000000..802b9402e5 --- /dev/null +++ b/scripts/build_webhook.py @@ -0,0 +1,33 @@ +import os + +import requests + +webhook_url = os.environ["WEBHOOK_URL"] +success = os.environ["TRAVIS_TEST_RESULT"] == "0" +job_no = os.environ["TRAVIS_BUILD_NUMBER"] +job_url = os.environ["TRAVIS_BUILD_WEB_URL"] +job_commit = os.environ["TRAVIS_COMMIT"][:8] + +if (pr_no := os.getenv("TRAVIS_PULL_REQUEST", "false")) != "false": + ignore = False + + source_slug = os.environ["TRAVIS_PULL_REQUEST_SLUG"] + source_branch = os.environ["TRAVIS_PULL_REQUEST_BRANCH"] + target_slug = os.environ["TRAVIS_REPO_SLUG"] + target_branch = os.environ["TRAVIS_BRANCH"] + + pr = f"[!{pr_no} {source_slug}#{source_branch} → {target_slug}#{target_branch}]" + pr += f"(https://github.com/{target_slug}/pull/{pr_no})" +else: + ignore = os.environ["TRAVIS_BRANCH"] != "master" + pr = "" + +result = "SUCCEEDED" if success else "FAILED" + +message = f"Build [{job_no}]({job_url}) for {job_commit} {result}!" + "\n" + pr + +if not ignore: + print("Sending payload :: ", message) + requests.post(webhook_url, {"content": message, "username": "Travis CI"}) +else: + print("Not sending payload.") diff --git a/scripts/deploy_webhook.py b/scripts/deploy_webhook.py new file mode 100644 index 0000000000..2c3e51b688 --- /dev/null +++ b/scripts/deploy_webhook.py @@ -0,0 +1,22 @@ +import os + +import requests + +webhook_url = os.environ["WEBHOOK_URL"] +tag = os.environ["TRAVIS_TAG"] +build_no = os.environ["TRAVIS_BUILD_NUMBER"] +commit_sha = os.environ["TRAVIS_COMMIT"] + +payload = { + "username": "Travis CI", + "embed": { + "title": f"{tag} has been deployed to PyPI", + "color": 0x663399, + "description": "Install it now!", + "footer": { + "text": f"#{build_no} | {commit_sha}" + } + } +} + +requests.post(webhook_url, data=payload) diff --git a/scripts/post-deploy.sh b/scripts/post-deploy.sh index 61083471ac..7dbbd38c4e 100755 --- a/scripts/post-deploy.sh +++ b/scripts/post-deploy.sh @@ -2,6 +2,10 @@ VERSION=${TRAVIS_TAG} REF=${TRAVIS_COMMIT} +echo "===== SENDING WEBHOOK =====" +python -m pip install requests +python scripts/deploy_webhook.py + echo "===== UPDATING VERSIONS =====" git config user.name "Nekokatt" git config user.email "69713762+nekokatt@users.noreply.github.com" From beb0249dd69acd32be9f80a59e0c4798975dac0c Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Sat, 15 Aug 2020 20:23:34 +0100 Subject: [PATCH 920/922] Fixed lint issue for .travis.yml deployment targets. --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 7e789855b3..c8964d7469 100644 --- a/.travis.yml +++ b/.travis.yml @@ -91,8 +91,8 @@ jobs: secure: "hRjbVk8VicGkEbv/AmEJMQmWNmkC7amaPpGdaJtDC/RrfvxDXMrPFZChwi8QN42Jy28sFN0om3Fw3AQfp85ddH2itcMFeJerKOpQ2QnjOECLlgL0+6xhZqV50dj8Pd6U6BLrkSu+PXETxloSUv421ojT4dq7EgVLbQBgqs8fT5tVHB2qNIxZoOjrGFZ7Lwj5gRojnrGXf6oZt7wh/2TCnTM5GY+Zc0uAj04gwN9zZnbqWii0EcY9jRQvoE3gf0F1bTlDWjbOrDnGA+2DfZEYK9YjCztXgHwvyOBdaGn56sh9mEXH1e+OxtuJvbMQ+RDGsMW94UtiLdXJUVPyOQLFB129FscGpX5LKatlnJI1HKTi2SBEL+IxAU0NhiuzJVYSYlIAmBcgBxKuk+dxdv2QYRpFhVTKomdp1U4BZgeXFVa7XjBthXm2ng3Yrpc7lrEEmle15X8/C/yv9xeaYrnPz2SCCA3J4MkRBcg+W/+L8l68g8MKyZGamImu5ZQsso4mZ4cnbJCCAhqvtMz2ydEzhwEFx2yeowyW9PhFkF79BQhTx9ZG3I9djbgDXAu3fwXyYIloKw2DGZ8LupsQBtamHtQxq7hjdfP2fUr2tFdXR0SHzFbHAaOh2y+8wdgwW+pGT3rVRE9LbhIFtK5cDheyo+/XIhpngwKpYrSk1J2Mf0k=" on: tags: true - distributions: "sdist bdist_wheel" repo: nekokatt/hikari + distributions: "sdist bdist_wheel" cleanup: 'true' after_deploy: - bash scripts/post-deploy.sh From 4d6a9e555819dfd83b0e549d3b823e52782a7bf2 Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Sat, 15 Aug 2020 20:26:58 +0100 Subject: [PATCH 921/922] Fixed bad travis ci creds. --- .travis.yml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index c8964d7469..fa87d0ebb5 100644 --- a/.travis.yml +++ b/.travis.yml @@ -84,11 +84,12 @@ jobs: arch: amd64 os: linux env: + secure: "hRjbVk8VicGkEbv/AmEJMQmWNmkC7amaPpGdaJtDC/RrfvxDXMrPFZChwi8QN42Jy28sFN0om3Fw3AQfp85ddH2itcMFeJerKOpQ2QnjOECLlgL0+6xhZqV50dj8Pd6U6BLrkSu+PXETxloSUv421ojT4dq7EgVLbQBgqs8fT5tVHB2qNIxZoOjrGFZ7Lwj5gRojnrGXf6oZt7wh/2TCnTM5GY+Zc0uAj04gwN9zZnbqWii0EcY9jRQvoE3gf0F1bTlDWjbOrDnGA+2DfZEYK9YjCztXgHwvyOBdaGn56sh9mEXH1e+OxtuJvbMQ+RDGsMW94UtiLdXJUVPyOQLFB129FscGpX5LKatlnJI1HKTi2SBEL+IxAU0NhiuzJVYSYlIAmBcgBxKuk+dxdv2QYRpFhVTKomdp1U4BZgeXFVa7XjBthXm2ng3Yrpc7lrEEmle15X8/C/yv9xeaYrnPz2SCCA3J4MkRBcg+W/+L8l68g8MKyZGamImu5ZQsso4mZ4cnbJCCAhqvtMz2ydEzhwEFx2yeowyW9PhFkF79BQhTx9ZG3I9djbgDXAu3fwXyYIloKw2DGZ8LupsQBtamHtQxq7hjdfP2fUr2tFdXR0SHzFbHAaOh2y+8wdgwW+pGT3rVRE9LbhIFtK5cDheyo+/XIhpngwKpYrSk1J2Mf0k=" deploy: provider: pypi - user: __token__ + username: __token__ password: - secure: "hRjbVk8VicGkEbv/AmEJMQmWNmkC7amaPpGdaJtDC/RrfvxDXMrPFZChwi8QN42Jy28sFN0om3Fw3AQfp85ddH2itcMFeJerKOpQ2QnjOECLlgL0+6xhZqV50dj8Pd6U6BLrkSu+PXETxloSUv421ojT4dq7EgVLbQBgqs8fT5tVHB2qNIxZoOjrGFZ7Lwj5gRojnrGXf6oZt7wh/2TCnTM5GY+Zc0uAj04gwN9zZnbqWii0EcY9jRQvoE3gf0F1bTlDWjbOrDnGA+2DfZEYK9YjCztXgHwvyOBdaGn56sh9mEXH1e+OxtuJvbMQ+RDGsMW94UtiLdXJUVPyOQLFB129FscGpX5LKatlnJI1HKTi2SBEL+IxAU0NhiuzJVYSYlIAmBcgBxKuk+dxdv2QYRpFhVTKomdp1U4BZgeXFVa7XjBthXm2ng3Yrpc7lrEEmle15X8/C/yv9xeaYrnPz2SCCA3J4MkRBcg+W/+L8l68g8MKyZGamImu5ZQsso4mZ4cnbJCCAhqvtMz2ydEzhwEFx2yeowyW9PhFkF79BQhTx9ZG3I9djbgDXAu3fwXyYIloKw2DGZ8LupsQBtamHtQxq7hjdfP2fUr2tFdXR0SHzFbHAaOh2y+8wdgwW+pGT3rVRE9LbhIFtK5cDheyo+/XIhpngwKpYrSk1J2Mf0k=" + secure: "Ln2A9zpYqvQlTNbmRs7SLVyO8pbakrSQ7L0JPXkXM7CwTFfKVKnsnnvG+ktm+/qWzIdsAx3cBDBPqWrpngbFj+/YsGXgkW4mO7EM5bwFItHKnTrlpbvUn0t8gcjAj8k+gT8IA8Q9fn+5Mwun8TmrNjr7wIQLRp09ua0XF3W2OSZlbeCRmxjOqji4Ulc8NQ0GEqkqU5aUSo06/ycQsxdgDWbDYCM7tuFYmAnO3cFeC1LobTJucjSsF1cWNQClwECcfkQWMSMV6lznNru3fnD3UfSp1RImE7rCm8pCF7j2oEgkE9kHpI3CXYwHFcgJ7neYyfvXNalZG1gHwyvH92QlogSoGVNKK0kTEDP8t56oGvRjjX2Zgm85MjAX5g8348DC8lic5LaWX5rewrCwfRz2qZ4NmGvQsIRitsQcKJnUf2eecgc53wluM4ad5NGaKHI7PJD392vhfVUMoiGQg5tKUIukdeKI/+GUgylZiObpTyM2/D3YjIg8hfjmfgVizPiMatfThplXm22alIZ3I1726T5O65Kv0wShl/gzZXDoKNwaMy9PT3LoXCjm091+y7gErkoStnkxt6JbzDqvaeM/itSb5knwYXgMfmo6hQpAG4xPL4S1/COoRgIy7B6Jp4VFNoRTzV5o7sLYOjWR1LrB4dhMccamzEw6Crk1QZiipUw=" on: tags: true repo: nekokatt/hikari @@ -96,6 +97,7 @@ jobs: cleanup: 'true' after_deploy: - bash scripts/post-deploy.sh + - - name: Build Webhook stage: webhooks From f2c111e4157cb5b7d9cd405f1a37ccd96f92d355 Mon Sep 17 00:00:00 2001 From: Nekokatt <69713762+nekokatt@users.noreply.github.com> Date: Sat, 15 Aug 2020 20:28:06 +0100 Subject: [PATCH 922/922] Made webhook always invoke for builds. --- .gitignore | 1 + .travis.yml | 73 ++++++++++++++++++++---------- dev-requirements.txt | 1 - pipelines/config.py | 30 ------------ pipelines/flake8.nox.py | 1 - pipelines/format.nox.py | 3 +- pipelines/mypy.nox.py | 10 +--- pipelines/pytest.nox.py | 5 +- scripts/build_succeeded_webhook.py | 56 +++++++++++++++++++++++ scripts/build_webhook.py | 33 -------------- scripts/deploy_webhook.py | 30 +++++++----- scripts/job_failed_webhook.py | 59 ++++++++++++++++++++++++ scripts/test_twemoji_mapping.py | 4 +- 13 files changed, 188 insertions(+), 118 deletions(-) create mode 100644 scripts/build_succeeded_webhook.py delete mode 100644 scripts/build_webhook.py create mode 100644 scripts/job_failed_webhook.py diff --git a/.gitignore b/.gitignore index 9d1c9fee14..4735cffea9 100644 --- a/.gitignore +++ b/.gitignore @@ -36,6 +36,7 @@ pip-wheel-metadata/ # Coverage-generated files .coverage* +coverage.xml **/*,cover* # PyTest-generated files diff --git a/.travis.yml b/.travis.yml index fa87d0ebb5..d39cf5215e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,40 +3,61 @@ stages: - deploy - webhooks -language: python -python: - - "3.8" -os: linux -arch: amd64 +cache: pip _test_job: &test_job - install: "${PYTHON_COMMAND} -m pip install nox" - before_script: "mkdir public > /dev/null 2>&1 || true" - script: "${PYTHON_COMMAND} -m nox --sessions pytest twemoji-test" + install: + - ${PYTHON_COMMAND} -m pip install nox + before_script: + - mkdir public > /dev/null 2>&1 || true + - ${PYTHON_COMMAND} -V + script: + - ${PYTHON_COMMAND} -m nox --sessions pytest + - bash <(curl -s https://codecov.io/bash) _windows_test_job: &windows_test_job os: windows language: shell - before_install: "choco install --no-progress python --version=${PYTHON_VERSION} -y && python -V" + before_install: + - choco install --no-progress python --version=${PYTHON_VERSION} -y env: - PYTHON_COMMAND="py -3" <<: *test_job _linux_test_job: &linux_test_job os: linux + language: python env: - PYTHON_COMMAND=python <<: *test_job +env: + global: + secure: "TDaw6Ryljomk6NwuoHCtuV4PGlvBFM/GjEmhXGFTXOn32ENrh14oXWaWIw6ZmKLtekccs2kQpnN+8l5kuzqr9aHvalJzwm05RYfu3wCw09rOeXXWX0GacnCrIh2yBfYXWq/pIS0CSPO+7IRnwwLaVSqUHGXfQV5mEHNbuJTU3tAlgOqrN/L4ZWdlPiclwvKStV4VGRSqxNGEj1G10VEq0+dn6KjrhkQMpDuhQ/3o+8F0vjMNb8rQeKwogeHP3tmwEiCUoxwekjzZU9Oevc75d836/ys2hvsStF8mMtQsd4QgTMYwSWQtKHmKyqgW21nXw8TE1HcnJnc3ahfueOFMbhplJ32h7svTPuPFTaVl99+z2TG8OBbFTQEkQakc5HO7lRH0AGLmhlyXkhYIGxWCS1h266VEzVkcNAliLR/auSFKQ4XKISPZQo6ee0Cx+FkRSKm2OgLw+Nzax8AQQd89C0+hibFq3kdOx0nORpWApjz1vIyCTTEl60UtWf/wkW5o/wTiPFwoHk3k6GxHZDkXCsUb8AjxpjIt9qxo53Yu7bv3msrGLmBScxdLeDs6qdCXXWJWI1D17nhNCclXgzLrZKf5n1zBqOAQjyeoJEOTnVUCQvIHSZYErVv8s0gCArv9ONtaIfq1Vmz+E234/K+IWOYDayvcDjFna7ySgK60W9I=" + jobs: include: # Linting - name: "Linting" + language: python + python: "3.8.5" + os: linux + arch: amd64 install: "pip install nox" stage: test - script: "python -m nox --sessions safety mypy flake8" + script: + - python -m nox --sessions safety mypy flake8 + + - name: "Twemoji Mapping Verification" + language: python + python: "3.8.5" + os: linux + arch: amd64 + install: "pip install nox" + stage: test + script: + - python -m nox --sessions twemoji-test - # Windows-specific test jobs. - name: "Windows 10 Python 3.8.5 AMD64 Tests" env: PYTHON_VERSION="3.8.5" arch: amd64 @@ -49,38 +70,37 @@ jobs: stage: test <<: *windows_test_job - # Linux-specific test jobs. - name: "Linux Python 3.8.5 AMD64 Tests" - python: '3.8.5' + python: "3.8.5" arch: amd64 stage: test - after_script: nox --sessions coveralls <<: *linux_test_job - name: "Linux Python 3.9 Dev AMD64 Tests" - python: '3.9-dev' + python: "3.9-dev" arch: amd64 stage: test <<: *linux_test_job - name: "Linux Python 3.8.5 ARM64 Tests" - python: '3.8.5' + python: "3.8.5" arch: arm64 stage: test <<: *linux_test_job - name: "Linux Python 3.9 Dev ARM64 Tests" - python: '3.9-dev' + python: "3.9-dev" arch: arm64 stage: test <<: *linux_test_job - name: "Deploy new release" if: tag IS present AND tag =~ /^\d+\.\d+\.\d+(\..*)?$/ - script: echo "Will deploy ${TRAVIS_TAG}" + script: + - echo "Will deploy ${TRAVIS_TAG}" stage: deploy language: python - python: 3.8 + python: "3.8.5" arch: amd64 os: linux env: @@ -94,18 +114,21 @@ jobs: tags: true repo: nekokatt/hikari distributions: "sdist bdist_wheel" - cleanup: 'true' + cleanup: "true" + edge: "true" after_deploy: - bash scripts/post-deploy.sh - - - name: Build Webhook stage: webhooks language: python - python: 3.8 + python: "3.8.5" arch: amd64 os: linux - env: - secure: "TDaw6Ryljomk6NwuoHCtuV4PGlvBFM/GjEmhXGFTXOn32ENrh14oXWaWIw6ZmKLtekccs2kQpnN+8l5kuzqr9aHvalJzwm05RYfu3wCw09rOeXXWX0GacnCrIh2yBfYXWq/pIS0CSPO+7IRnwwLaVSqUHGXfQV5mEHNbuJTU3tAlgOqrN/L4ZWdlPiclwvKStV4VGRSqxNGEj1G10VEq0+dn6KjrhkQMpDuhQ/3o+8F0vjMNb8rQeKwogeHP3tmwEiCUoxwekjzZU9Oevc75d836/ys2hvsStF8mMtQsd4QgTMYwSWQtKHmKyqgW21nXw8TE1HcnJnc3ahfueOFMbhplJ32h7svTPuPFTaVl99+z2TG8OBbFTQEkQakc5HO7lRH0AGLmhlyXkhYIGxWCS1h266VEzVkcNAliLR/auSFKQ4XKISPZQo6ee0Cx+FkRSKm2OgLw+Nzax8AQQd89C0+hibFq3kdOx0nORpWApjz1vIyCTTEl60UtWf/wkW5o/wTiPFwoHk3k6GxHZDkXCsUb8AjxpjIt9qxo53Yu7bv3msrGLmBScxdLeDs6qdCXXWJWI1D17nhNCclXgzLrZKf5n1zBqOAQjyeoJEOTnVUCQvIHSZYErVv8s0gCArv9ONtaIfq1Vmz+E234/K+IWOYDayvcDjFna7ySgK60W9I=" install: pip install requests - script: python scripts/build_webhook.py + script: python scripts/build_succeeded_webhook.py + + +after_failure: + - ${PYTHON_COMMAND-python} -m pip install requests + - ${PYTHON_COMMAND-python} scripts/job_failed_webhook.py diff --git a/dev-requirements.txt b/dev-requirements.txt index 47c6be0a20..2fe23292ab 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -9,7 +9,6 @@ pytest==6.0.1 pytest-asyncio==0.14.0 pytest-cov==2.10.1 pytest-randomly==3.4.1 -pytest-testdox==1.2.1 # Coverage testing. coverage~=5.2.1 diff --git a/pipelines/config.py b/pipelines/config.py index f959b3c4f1..8cdd085d6f 100644 --- a/pipelines/config.py +++ b/pipelines/config.py @@ -41,38 +41,8 @@ LOGO_SOURCE = "logo.png" # Linting and test configs. -FLAKE8_JUNIT = "public/flake8-junit.xml" -FLAKE8_HTML = "public/flake8" FLAKE8_TXT = "public/flake8.txt" MYPY_INI = "mypy.ini" -MYPY_JUNIT_OUTPUT_PATH = _os.path.join(ARTIFACT_DIRECTORY, "mypy.xml") -MYPY_HTML_OUTPUT_PATH = _os.path.join(ARTIFACT_DIRECTORY, "mypy") PYTEST_INI = "pytest.ini" -PYTEST_HTML_OUTPUT_PATH = _os.path.join(ARTIFACT_DIRECTORY, "pytest.html") COVERAGE_INI = "coverage.ini" COVERAGE_HTML_PATH = _os.path.join(ARTIFACT_DIRECTORY, "coverage", "html") -COVERAGE_JUNIT_PATH = _os.path.join(ARTIFACT_DIRECTORY, "tests.xml") - -# Deployment variables; these only apply to CI stuff specifically. -VERSION_FILE = _os.path.join(MAIN_PACKAGE, "_about.py") -API_NAME = "hikari" -GIT_SVC_HOST = "github.com" -GIT_TEST_SSH_PATH = "git@github.com" -AUTHOR = "Nekokatt" -ORIGINAL_REPO_URL = f"https://{GIT_SVC_HOST}/${AUTHOR}/{API_NAME}" -SSH_DIR = "~/.ssh" -SSH_PRIVATE_KEY_PATH = _os.path.join(SSH_DIR, "id_rsa") -SSH_KNOWN_HOSTS = _os.path.join(SSH_DIR, "known_hosts") -CI_ROBOT_NAME = AUTHOR -CI_ROBOT_EMAIL = "nekoka.tt@outlook.com" -SKIP_CI_PHRASE = "[skip ci]" -SKIP_DEPLOY_PHRASE = "[skip deploy]" -SKIP_PAGES_PHRASE = "[skip pages]" -PROD_BRANCH = "master" -PREPROD_BRANCH = "staging" -DEV_BRANCH = "development" -REMOTE_NAME = "origin" -DISTS = ["sdist", "bdist_wheel"] -PYPI_REPO = "https://upload.pypi.org/legacy/" -PYPI = "https://pypi.org/" -PYPI_API = f"{PYPI}/pypi/{API_NAME}/json" diff --git a/pipelines/flake8.nox.py b/pipelines/flake8.nox.py index 2959935d3c..f9a0a16251 100644 --- a/pipelines/flake8.nox.py +++ b/pipelines/flake8.nox.py @@ -29,7 +29,6 @@ def flake8(session: nox.Session) -> None: """Run code linting, SAST, and analysis.""" session.install("-r", "requirements.txt", "-r", "flake-requirements.txt") - print("Generating console output") shutil.rmtree(config.FLAKE8_TXT, ignore_errors=True) session.run( "flake8", f"--output-file={config.FLAKE8_TXT}", "--statistics", "--show-source", "--tee", config.MAIN_PACKAGE, diff --git a/pipelines/format.nox.py b/pipelines/format.nox.py index 4a2a858bba..fedcbf8fb5 100644 --- a/pipelines/format.nox.py +++ b/pipelines/format.nox.py @@ -71,7 +71,7 @@ ".ps1", ".rb", ".pl", - ".travis.yml" + ".travis.yml", ) LINE_ENDING_PATHS = { @@ -81,7 +81,6 @@ "docs", "insomnia", ".gitlab", - } diff --git a/pipelines/mypy.nox.py b/pipelines/mypy.nox.py index 383a728d7e..ab0a8f52bc 100644 --- a/pipelines/mypy.nox.py +++ b/pipelines/mypy.nox.py @@ -28,13 +28,5 @@ def mypy(session: nox.Session) -> None: """Perform static type analysis on Python source code.""" session.install("-r", "requirements.txt", "-r", "mypy-requirements.txt") session.run( - "mypy", - "-p", - config.MAIN_PACKAGE, - "--config", - config.MYPY_INI, - "--junit-xml", - config.MYPY_JUNIT_OUTPUT_PATH, - "--html-report", - config.MYPY_HTML_OUTPUT_PATH, + "mypy", "-p", config.MAIN_PACKAGE, "--config", config.MYPY_INI, ) diff --git a/pipelines/pytest.nox.py b/pipelines/pytest.nox.py index 88d44de517..ec48c9c468 100644 --- a/pipelines/pytest.nox.py +++ b/pipelines/pytest.nox.py @@ -38,10 +38,9 @@ "term", "--cov-report", f"html:{config.COVERAGE_HTML_PATH}", + "--cov-report", + "xml", "--cov-branch", - "--junitxml", - config.COVERAGE_JUNIT_PATH, - "--force-testdox", "--showlocals", ] diff --git a/scripts/build_succeeded_webhook.py b/scripts/build_succeeded_webhook.py new file mode 100644 index 0000000000..4c92340c4d --- /dev/null +++ b/scripts/build_succeeded_webhook.py @@ -0,0 +1,56 @@ +import os +import textwrap + +import requests + +webhook_url = os.environ["WEBHOOK_URL"] +build_no = os.environ["TRAVIS_BUILD_NUMBER"] +build_url = os.environ["TRAVIS_BUILD_WEB_URL"] +build_commit = os.environ["TRAVIS_COMMIT"] +short_build_commit = build_commit[:8] + +build_type = { + "push": "new commit", + "pull_request": "pull request", + "api": "api-triggered job", + "cron": "scheduled job", +}.get(raw_build_type := os.getenv("TRAVIS_EVENT_TYPE"), raw_build_type) + + +commit_message = textwrap.TextWrapper(width=80, max_lines=4).wrap(os.environ["TRAVIS_COMMIT_MESSAGE"])[0] + +if (pr_no := os.getenv("TRAVIS_PULL_REQUEST", "false")) != "false": + + source_slug = os.environ["TRAVIS_PULL_REQUEST_SLUG"] + source_branch = os.environ["TRAVIS_PULL_REQUEST_BRANCH"] + target_slug = os.environ["TRAVIS_REPO_SLUG"] + target_branch = os.environ["TRAVIS_BRANCH"] + + pr_name = f"!{pr_no} {source_slug}#{source_branch} → {target_slug}#{target_branch}" + pr_url = f"https://github.com/{target_slug}/pull/{pr_no}" + + author_field = dict(name=pr_name, url=pr_url,) +else: + author_field = None + +payload = dict( + username="Travis CI", + embeds=[ + dict( + title=f"Build #{build_no} succeeded for {build_type} (`{short_build_commit}`)", + description=commit_message, + author=author_field, + color=0x00FF00, + url=build_url, + footer=dict(text="Travis CI"), + ) + ], +) + +with requests.post(webhook_url, json=payload) as resp: + print("POST", payload) + print(resp.status_code, resp.reason) + try: + print(resp.json()) + except: + print(resp.content) diff --git a/scripts/build_webhook.py b/scripts/build_webhook.py deleted file mode 100644 index 802b9402e5..0000000000 --- a/scripts/build_webhook.py +++ /dev/null @@ -1,33 +0,0 @@ -import os - -import requests - -webhook_url = os.environ["WEBHOOK_URL"] -success = os.environ["TRAVIS_TEST_RESULT"] == "0" -job_no = os.environ["TRAVIS_BUILD_NUMBER"] -job_url = os.environ["TRAVIS_BUILD_WEB_URL"] -job_commit = os.environ["TRAVIS_COMMIT"][:8] - -if (pr_no := os.getenv("TRAVIS_PULL_REQUEST", "false")) != "false": - ignore = False - - source_slug = os.environ["TRAVIS_PULL_REQUEST_SLUG"] - source_branch = os.environ["TRAVIS_PULL_REQUEST_BRANCH"] - target_slug = os.environ["TRAVIS_REPO_SLUG"] - target_branch = os.environ["TRAVIS_BRANCH"] - - pr = f"[!{pr_no} {source_slug}#{source_branch} → {target_slug}#{target_branch}]" - pr += f"(https://github.com/{target_slug}/pull/{pr_no})" -else: - ignore = os.environ["TRAVIS_BRANCH"] != "master" - pr = "" - -result = "SUCCEEDED" if success else "FAILED" - -message = f"Build [{job_no}]({job_url}) for {job_commit} {result}!" + "\n" + pr - -if not ignore: - print("Sending payload :: ", message) - requests.post(webhook_url, {"content": message, "username": "Travis CI"}) -else: - print("Not sending payload.") diff --git a/scripts/deploy_webhook.py b/scripts/deploy_webhook.py index 2c3e51b688..d6bb98c099 100644 --- a/scripts/deploy_webhook.py +++ b/scripts/deploy_webhook.py @@ -7,16 +7,22 @@ build_no = os.environ["TRAVIS_BUILD_NUMBER"] commit_sha = os.environ["TRAVIS_COMMIT"] -payload = { - "username": "Travis CI", - "embed": { - "title": f"{tag} has been deployed to PyPI", - "color": 0x663399, - "description": "Install it now!", - "footer": { - "text": f"#{build_no} | {commit_sha}" - } - } -} +payload = dict( + username="Travis CI", + embeds=[ + dict( + title=f"{tag} has been deployed to PyPI", + color=0x663399, + description="Install it now!", + footer=dict(text=f"#{build_no} | {commit_sha}"), + ) + ], +) -requests.post(webhook_url, data=payload) +with requests.post(webhook_url, json=payload) as resp: + print("POST", payload) + print(resp.status_code, resp.reason) + try: + print(resp.json()) + except: + print(resp.content) diff --git a/scripts/job_failed_webhook.py b/scripts/job_failed_webhook.py new file mode 100644 index 0000000000..e5ac9c9624 --- /dev/null +++ b/scripts/job_failed_webhook.py @@ -0,0 +1,59 @@ +import os + +import requests + +webhook_url = os.environ["WEBHOOK_URL"] +build_no = os.environ["TRAVIS_BUILD_NUMBER"] +build_url = os.environ["TRAVIS_BUILD_WEB_URL"] +build_commit = os.environ["TRAVIS_COMMIT"] +short_build_commit = build_commit[:8] + +build_type = { + "push": "new commit", + "pull_request": "pull request", + "api": "api-triggered job", + "cron": "scheduled job", +}.get(raw_build_type := os.getenv("TRAVIS_EVENT_TYPE"), raw_build_type) + +if (pr_no := os.getenv("TRAVIS_PULL_REQUEST", "false")) != "false": + + source_slug = os.environ["TRAVIS_PULL_REQUEST_SLUG"] + source_branch = os.environ["TRAVIS_PULL_REQUEST_BRANCH"] + target_slug = os.environ["TRAVIS_REPO_SLUG"] + target_branch = os.environ["TRAVIS_BRANCH"] + + pr_name = f"!{pr_no} {source_slug}#{source_branch} → {target_slug}#{target_branch}" + pr_url = f"https://github.com/{target_slug}/pull/{pr_no}" + + author_field = dict(name=pr_name, url=pr_url,) +else: + author_field = None + +job_name = os.environ["TRAVIS_JOB_NAME"] +job_no = os.environ["TRAVIS_JOB_NUMBER"] +job_url = os.environ["TRAVIS_JOB_WEB_URL"] +job_os = os.environ["TRAVIS_OS_NAME"] +job_dist = os.environ["TRAVIS_DIST"] +job_arch = os.environ["TRAVIS_CPU_ARCH"] + +payload = dict( + username="Travis CI", + embeds=[ + dict( + title=f"Job #{job_no} failed for {build_type} (`{short_build_commit}`)", + url=build_url, + author=author_field, + color=0xFF0000, + footer=dict(text=job_name), + ) + ], +) + +with requests.post(webhook_url, json=payload) as resp: + print("POST", payload) + print(resp.status_code, resp.reason) + try: + print(resp.json()) + except: + print(resp.content) + diff --git a/scripts/test_twemoji_mapping.py b/scripts/test_twemoji_mapping.py index f2f09e8430..7889c4d6e6 100644 --- a/scripts/test_twemoji_mapping.py +++ b/scripts/test_twemoji_mapping.py @@ -70,10 +70,10 @@ def try_fetch(i, n, emoji_surrogates, name): if path.is_file(): valid_emojis.append((emoji_surrogates, name)) - print("\033[1;32m[ OK ]\033[0m", f"{i}/{n}", name, *map(hex, map(ord, emoji_surrogates)), emoji.url) + print("[ OK ]", f"{i}/{n}", name, *map(hex, map(ord, emoji_surrogates)), emoji.url) else: invalid_emojis.append((emoji_surrogates, name)) - print("\033[1;31m[ FAIL ]\033[0m", f"{i}/{n}", name, *map(hex, map(ord, emoji_surrogates)), emoji.url) + print("[ FAIL ]", f"{i}/{n}", name, *map(hex, map(ord, emoji_surrogates)), emoji.url) with tempfile.TemporaryDirectory() as tempdir: